From 478d1652f647c23a2916127385471051d981d587 Mon Sep 17 00:00:00 2001 From: iamnbutler Date: Fri, 12 Dec 2025 14:17:27 -0500 Subject: [PATCH] Remove non-Apache licensed crates - Removed all GPL/AGPL licensed crates (183 crates) - Kept 19 Apache-licensed crates including gpui and dependencies - Restored tooling/perf (Apache-licensed, needed by util_macros) - Removed ztracing/zlog dependencies (GPL) from sum_tree and watch - Removed Zed-specific infrastructure files (Dockerfiles, compose, etc) - Removed extensions and most of tooling directory GPUI and its dependencies now compile successfully. --- Cargo.lock | 13360 +-- Cargo.toml | 553 +- Dockerfile-collab | 37 - Dockerfile-collab.dockerignore | 16 - Dockerfile-cross.dockerignore | 16 - Dockerfile-distros | 26 - Dockerfile-distros.dockerignore | 2 - LICENSE-AGPL | 788 - LICENSE-GPL | 200 - Procfile | 4 - Procfile.web | 1 - compose.yml | 43 - crates/acp_thread/Cargo.toml | 60 - crates/acp_thread/LICENSE-GPL | 1 - crates/acp_thread/src/acp_thread.rs | 4021 - crates/acp_thread/src/connection.rs | 470 - crates/acp_thread/src/diff.rs | 432 - crates/acp_thread/src/mention.rs | 551 - crates/acp_thread/src/terminal.rs | 224 - crates/acp_tools/Cargo.toml | 29 - crates/acp_tools/LICENSE-GPL | 1 - crates/acp_tools/src/acp_tools.rs | 643 - crates/action_log/Cargo.toml | 45 - crates/action_log/LICENSE-GPL | 1 - crates/action_log/src/action_log.rs | 2407 - crates/activity_indicator/Cargo.toml | 34 - crates/activity_indicator/LICENSE-GPL | 1 - .../src/activity_indicator.rs | 947 - crates/agent/Cargo.toml | 105 - crates/agent/LICENSE-GPL | 1 - crates/agent/src/agent.rs | 1608 - crates/agent/src/db.rs | 443 - crates/agent/src/edit_agent.rs | 1505 - .../src/edit_agent/create_file_parser.rs | 237 - crates/agent/src/edit_agent/edit_parser.rs | 1094 - crates/agent/src/edit_agent/evals.rs | 1684 - .../fixtures/add_overwrite_test/before.rs | 1572 - .../fixtures/delete_run_git_blame/after.rs | 328 - .../fixtures/delete_run_git_blame/before.rs | 371 - .../disable_cursor_blinking/before.rs | 21343 ---- .../disable_cursor_blinking/possible-01.diff | 28 - .../disable_cursor_blinking/possible-02.diff | 29 - .../disable_cursor_blinking/possible-03.diff | 34 - .../disable_cursor_blinking/possible-04.diff | 33 - .../extract_handle_command_output/before.rs | 371 - .../possible-01.diff | 11 - .../possible-02.diff | 26 - .../possible-03.diff | 11 - .../possible-04.diff | 24 - .../possible-05.diff | 26 - .../possible-06.diff | 23 - .../possible-07.diff | 26 - .../possible-08.diff | 26 - .../from_pixels_constructor/before.rs | 339 - .../fixtures/translate_doc_comments/before.rs | 339 - .../before.rs | 1629 - .../edit_agent/evals/fixtures/zode/prompt.md | 2193 - .../edit_agent/evals/fixtures/zode/react.py | 14 - .../evals/fixtures/zode/react_test.py | 271 - .../src/edit_agent/streaming_fuzzy_matcher.rs | 806 - crates/agent/src/history_store.rs | 412 - crates/agent/src/legacy_thread.rs | 402 - crates/agent/src/native_agent_server.rs | 122 - crates/agent/src/outline.rs | 218 - crates/agent/src/templates.rs | 90 - .../src/templates/create_file_prompt.hbs | 15 - crates/agent/src/templates/diff_judge.hbs | 23 - .../edit_file_prompt_diff_fenced.hbs | 77 - .../src/templates/edit_file_prompt_xml.hbs | 92 - crates/agent/src/templates/system_prompt.hbs | 188 - crates/agent/src/tests/mod.rs | 2598 - crates/agent/src/tests/test_tools.rs | 221 - crates/agent/src/thread.rs | 2664 - crates/agent/src/tools.rs | 98 - .../src/tools/context_server_registry.rs | 253 - crates/agent/src/tools/copy_path_tool.rs | 113 - .../agent/src/tools/create_directory_tool.rs | 90 - crates/agent/src/tools/delete_path_tool.rs | 140 - crates/agent/src/tools/diagnostics_tool.rs | 165 - crates/agent/src/tools/edit_file_tool.rs | 2210 - crates/agent/src/tools/fetch_tool.rs | 164 - crates/agent/src/tools/find_path_tool.rs | 247 - crates/agent/src/tools/grep_tool.rs | 1183 - crates/agent/src/tools/list_directory_tool.rs | 660 - crates/agent/src/tools/move_path_tool.rs | 124 - crates/agent/src/tools/now_tool.rs | 64 - crates/agent/src/tools/open_tool.rs | 168 - crates/agent/src/tools/read_file_tool.rs | 1037 - crates/agent/src/tools/terminal_tool.rs | 212 - crates/agent/src/tools/thinking_tool.rs | 50 - crates/agent/src/tools/web_search_tool.rs | 127 - crates/agent_servers/Cargo.toml | 67 - crates/agent_servers/LICENSE-GPL | 1 - crates/agent_servers/src/acp.rs | 984 - crates/agent_servers/src/agent_servers.rs | 121 - crates/agent_servers/src/claude.rs | 121 - crates/agent_servers/src/codex.rs | 123 - crates/agent_servers/src/custom.rs | 151 - crates/agent_servers/src/e2e_tests.rs | 552 - crates/agent_servers/src/gemini.rs | 102 - crates/agent_settings/Cargo.toml | 34 - crates/agent_settings/LICENSE-GPL | 1 - crates/agent_settings/src/agent_profile.rs | 202 - crates/agent_settings/src/agent_settings.rs | 182 - .../summarize_thread_detailed_prompt.txt | 6 - .../src/prompts/summarize_thread_prompt.txt | 4 - crates/agent_ui/Cargo.toml | 126 - crates/agent_ui/LICENSE-GPL | 1 - crates/agent_ui/src/acp.rs | 13 - crates/agent_ui/src/acp/entry_view_state.rs | 532 - crates/agent_ui/src/acp/message_editor.rs | 2532 - crates/agent_ui/src/acp/mode_selector.rs | 229 - crates/agent_ui/src/acp/model_selector.rs | 553 - .../src/acp/model_selector_popover.rs | 100 - crates/agent_ui/src/acp/thread_history.rs | 861 - crates/agent_ui/src/acp/thread_view.rs | 7296 -- crates/agent_ui/src/agent_configuration.rs | 1451 - .../add_llm_provider_modal.rs | 848 - .../configure_context_server_modal.rs | 942 - .../configure_context_server_tools_modal.rs | 175 - .../manage_profiles_modal.rs | 939 - .../profile_modal_header.rs | 42 - .../src/agent_configuration/tool_picker.rs | 394 - crates/agent_ui/src/agent_diff.rs | 2191 - crates/agent_ui/src/agent_model_selector.rs | 120 - crates/agent_ui/src/agent_panel.rs | 2802 - crates/agent_ui/src/agent_ui.rs | 548 - crates/agent_ui/src/buffer_codegen.rs | 1696 - crates/agent_ui/src/completion_provider.rs | 1925 - crates/agent_ui/src/context.rs | 63 - .../src/context_server_configuration.rs | 116 - crates/agent_ui/src/evals.rs | 89 - crates/agent_ui/src/inline_assistant.rs | 2206 - crates/agent_ui/src/inline_prompt_editor.rs | 1529 - .../agent_ui/src/language_model_selector.rs | 798 - crates/agent_ui/src/mention_set.rs | 1098 - crates/agent_ui/src/profile_selector.rs | 760 - crates/agent_ui/src/slash_command.rs | 360 - crates/agent_ui/src/slash_command_picker.rs | 348 - crates/agent_ui/src/terminal_codegen.rs | 210 - .../agent_ui/src/terminal_inline_assistant.rs | 456 - crates/agent_ui/src/text_thread_editor.rs | 3370 - crates/agent_ui/src/ui.rs | 19 - .../agent_ui/src/ui/acp_onboarding_modal.rs | 246 - crates/agent_ui/src/ui/agent_notification.rs | 189 - crates/agent_ui/src/ui/burn_mode_tooltip.rs | 69 - .../src/ui/claude_code_onboarding_modal.rs | 254 - crates/agent_ui/src/ui/end_trial_upsell.rs | 121 - crates/agent_ui/src/ui/hold_for_default.rs | 40 - crates/agent_ui/src/ui/onboarding_modal.rs | 174 - .../src/ui/unavailable_editing_tooltip.rs | 29 - crates/agent_ui/src/ui/usage_callout.rs | 200 - crates/ai_onboarding/Cargo.toml | 27 - crates/ai_onboarding/LICENSE-GPL | 1 - .../src/agent_api_keys_onboarding.rs | 141 - .../src/agent_panel_onboarding_card.rs | 83 - .../src/agent_panel_onboarding_content.rs | 91 - crates/ai_onboarding/src/ai_onboarding.rs | 363 - crates/ai_onboarding/src/ai_upsell_card.rs | 370 - .../src/edit_prediction_onboarding_content.rs | 79 - crates/ai_onboarding/src/plan_definitions.rs | 68 - .../ai_onboarding/src/young_account_banner.rs | 22 - crates/anthropic/Cargo.toml | 28 - crates/anthropic/LICENSE-GPL | 1 - crates/anthropic/src/anthropic.rs | 1072 - crates/anthropic/src/batches.rs | 190 - crates/askpass/Cargo.toml | 29 - crates/askpass/LICENSE-GPL | 1 - crates/askpass/src/askpass.rs | 385 - crates/askpass/src/encrypted_password.rs | 102 - crates/assets/Cargo.toml | 17 - crates/assets/LICENSE-GPL | 1 - crates/assets/src/assets.rs | 65 - crates/assistant_slash_command/Cargo.toml | 34 - crates/assistant_slash_command/LICENSE-GPL | 1 - .../src/assistant_slash_command.rs | 573 - .../src/extension_slash_command.rs | 171 - .../src/slash_command_registry.rs | 90 - .../src/slash_command_working_set.rs | 81 - crates/assistant_slash_commands/Cargo.toml | 48 - crates/assistant_slash_commands/LICENSE-GPL | 1 - .../src/assistant_slash_commands.rs | 27 - .../src/cargo_workspace_command.rs | 159 - .../src/context_server_command.rs | 251 - .../src/default_command.rs | 91 - .../src/delta_command.rs | 124 - .../src/diagnostics_command.rs | 432 - .../src/fetch_command.rs | 183 - .../src/file_command.rs | 777 - .../src/now_command.rs | 71 - .../src/prompt_command.rs | 123 - .../src/selection_command.rs | 202 - .../src/streaming_example_command.rs | 118 - .../src/symbols_command.rs | 99 - .../src/tab_command.rs | 317 - crates/assistant_text_thread/Cargo.toml | 63 - crates/assistant_text_thread/LICENSE-GPL | 1 - .../src/assistant_text_thread.rs | 15 - .../src/assistant_text_thread_tests.rs | 1462 - .../assistant_text_thread/src/text_thread.rs | 3335 - .../src/text_thread_store.rs | 941 - crates/audio/Cargo.toml | 32 - crates/audio/LICENSE-GPL | 1 - crates/audio/src/audio.rs | 319 - crates/audio/src/audio_settings.rs | 119 - crates/audio/src/replays.rs | 77 - crates/audio/src/rodio_ext.rs | 763 - crates/auto_update/Cargo.toml | 42 - crates/auto_update/LICENSE-GPL | 1 - crates/auto_update/src/auto_update.rs | 1378 - crates/auto_update_helper/Cargo.toml | 31 - crates/auto_update_helper/LICENSE-GPL | 1 - crates/auto_update_helper/app-icon.ico | Bin 590611 -> 0 bytes crates/auto_update_helper/build.rs | 15 - crates/auto_update_helper/manifest.xml | 32 - .../src/auto_update_helper.rs | 147 - crates/auto_update_helper/src/dialog.rs | 236 - crates/auto_update_helper/src/updater.rs | 357 - crates/auto_update_ui/Cargo.toml | 28 - crates/auto_update_ui/LICENSE-GPL | 1 - crates/auto_update_ui/src/auto_update_ui.rs | 188 - crates/aws_http_client/Cargo.toml | 20 - crates/aws_http_client/LICENSE-GPL | 1 - crates/aws_http_client/src/aws_http_client.rs | 104 - crates/bedrock/Cargo.toml | 27 - crates/bedrock/LICENSE-GPL | 1 - crates/bedrock/src/bedrock.rs | 179 - crates/bedrock/src/models.rs | 987 - crates/breadcrumbs/Cargo.toml | 28 - crates/breadcrumbs/LICENSE-GPL | 1 - crates/breadcrumbs/src/breadcrumbs.rs | 244 - crates/buffer_diff/Cargo.toml | 40 - crates/buffer_diff/LICENSE-GPL | 1 - crates/buffer_diff/src/buffer_diff.rs | 2479 - crates/call/Cargo.toml | 54 - crates/call/LICENSE-GPL | 1 - crates/call/src/call.rs | 5 - crates/call/src/call_impl/mod.rs | 515 - crates/call/src/call_impl/participant.rs | 76 - crates/call/src/call_impl/room.rs | 1724 - crates/call/src/call_settings.rs | 17 - crates/channel/Cargo.toml | 43 - crates/channel/LICENSE-GPL | 1 - crates/channel/src/channel.rs | 17 - crates/channel/src/channel_buffer.rs | 283 - crates/channel/src/channel_store.rs | 1151 - .../src/channel_store/channel_index.rs | 120 - crates/channel/src/channel_store_tests.rs | 274 - crates/cli/Cargo.toml | 51 - crates/cli/LICENSE-GPL | 1 - crates/cli/README.md | 15 - crates/cli/build.rs | 29 - crates/cli/src/cli.rs | 36 - crates/cli/src/main.rs | 1250 - crates/client/Cargo.toml | 84 - crates/client/LICENSE-GPL | 1 - crates/client/src/client.rs | 2180 - crates/client/src/proxy.rs | 66 - crates/client/src/proxy/http_proxy.rs | 193 - crates/client/src/proxy/socks_proxy.rs | 226 - crates/client/src/telemetry.rs | 881 - .../client/src/telemetry/event_coalescer.rs | 283 - crates/client/src/test.rs | 291 - crates/client/src/user.rs | 1007 - crates/client/src/zed_urls.rs | 69 - crates/clock/Cargo.toml | 21 - crates/clock/LICENSE-GPL | 1 - crates/clock/src/clock.rs | 271 - crates/clock/src/system_clock.rs | 53 - crates/codestral/Cargo.toml | 27 - crates/codestral/LICENSE-GPL | 1 - crates/codestral/src/codestral.rs | 396 - crates/collab/.env.toml | 24 - crates/collab/Cargo.toml | 131 - crates/collab/LICENSE-AGPL | 1 - crates/collab/README.md | 65 - crates/collab/k8s/collab.template.yml | 241 - crates/collab/k8s/environments/production.sh | 7 - crates/collab/k8s/environments/staging.sh | 6 - .../20221109000000_test_schema.sql | 462 - .../migrations/20251208000000_test_schema.sql | 899 - crates/collab/seed.default.json | 33 - crates/collab/seed/github_users.json | 602 - crates/collab/src/api.rs | 190 - crates/collab/src/api/contributors.rs | 170 - crates/collab/src/api/events.rs | 232 - crates/collab/src/api/extensions.rs | 417 - crates/collab/src/auth.rs | 399 - crates/collab/src/bin/dotenv.rs | 12 - crates/collab/src/completion.rs | 2 - crates/collab/src/db.rs | 782 - crates/collab/src/db/ids.rs | 282 - crates/collab/src/db/queries.rs | 13 - crates/collab/src/db/queries/access_tokens.rs | 77 - crates/collab/src/db/queries/buffers.rs | 1049 - crates/collab/src/db/queries/channels.rs | 1131 - crates/collab/src/db/queries/contacts.rs | 363 - crates/collab/src/db/queries/contributors.rs | 100 - .../src/db/queries/dev_server_projects.rs | 1 - crates/collab/src/db/queries/dev_servers.rs | 1 - crates/collab/src/db/queries/extensions.rs | 484 - crates/collab/src/db/queries/notifications.rs | 281 - crates/collab/src/db/queries/projects.rs | 1501 - crates/collab/src/db/queries/rooms.rs | 1422 - crates/collab/src/db/queries/servers.rs | 230 - crates/collab/src/db/queries/users.rs | 277 - crates/collab/src/db/tables.rs | 29 - crates/collab/src/db/tables/access_token.rs | 30 - crates/collab/src/db/tables/buffer.rs | 48 - .../collab/src/db/tables/buffer_operation.rs | 34 - .../collab/src/db/tables/buffer_snapshot.rs | 31 - crates/collab/src/db/tables/channel.rs | 95 - .../db/tables/channel_buffer_collaborator.rs | 43 - .../src/db/tables/channel_chat_participant.rs | 41 - crates/collab/src/db/tables/channel_member.rs | 59 - crates/collab/src/db/tables/contact.rs | 32 - crates/collab/src/db/tables/contributor.rs | 30 - crates/collab/src/db/tables/extension.rs | 27 - .../collab/src/db/tables/extension_version.rs | 102 - crates/collab/src/db/tables/follower.rs | 50 - .../collab/src/db/tables/language_server.rs | 32 - crates/collab/src/db/tables/notification.rs | 29 - .../collab/src/db/tables/notification_kind.rs | 15 - .../src/db/tables/observed_buffer_edits.rs | 43 - crates/collab/src/db/tables/project.rs | 93 - .../src/db/tables/project_collaborator.rs | 45 - .../src/db/tables/project_repository.rs | 45 - .../db/tables/project_repository_statuses.rs | 36 - crates/collab/src/db/tables/room.rs | 53 - .../collab/src/db/tables/room_participant.rs | 62 - crates/collab/src/db/tables/server.rs | 15 - crates/collab/src/db/tables/user.rs | 66 - crates/collab/src/db/tables/worktree.rs | 36 - .../db/tables/worktree_diagnostic_summary.rs | 21 - crates/collab/src/db/tables/worktree_entry.rs | 31 - .../src/db/tables/worktree_settings_file.rs | 37 - crates/collab/src/db/tests.rs | 219 - crates/collab/src/db/tests/buffer_tests.rs | 361 - crates/collab/src/db/tests/channel_tests.rs | 982 - .../collab/src/db/tests/contributor_tests.rs | 45 - crates/collab/src/db/tests/db_tests.rs | 590 - crates/collab/src/db/tests/extension_tests.rs | 489 - crates/collab/src/db/tests/migrations.rs | 47 - crates/collab/src/env.rs | 41 - crates/collab/src/errors.rs | 29 - crates/collab/src/executor.rs | 39 - crates/collab/src/lib.rs | 345 - crates/collab/src/main.rs | 273 - crates/collab/src/rpc.rs | 4150 - crates/collab/src/rpc/connection_pool.rs | 259 - crates/collab/src/seed.rs | 136 - crates/collab/src/tests.rs | 50 - .../collab/src/tests/channel_buffer_tests.rs | 720 - .../collab/src/tests/channel_guest_tests.rs | 310 - crates/collab/src/tests/channel_tests.rs | 1466 - crates/collab/src/tests/debug_panel_tests.rs | 2449 - crates/collab/src/tests/editor_tests.rs | 4644 - crates/collab/src/tests/following_tests.rs | 2364 - crates/collab/src/tests/git_tests.rs | 135 - crates/collab/src/tests/integration_tests.rs | 7102 -- crates/collab/src/tests/notification_tests.rs | 164 - .../src/tests/random_channel_buffer_tests.rs | 288 - .../random_project_collaboration_tests.rs | 1689 - .../src/tests/randomized_test_helpers.rs | 692 - .../remote_editing_collaboration_tests.rs | 840 - crates/collab/src/tests/test_server.rs | 915 - crates/collab_ui/Cargo.toml | 78 - crates/collab_ui/LICENSE-GPL | 1 - crates/collab_ui/src/channel_view.rs | 712 - crates/collab_ui/src/collab_panel.rs | 3303 - .../src/collab_panel/channel_modal.rs | 657 - .../src/collab_panel/contact_finder.rs | 171 - crates/collab_ui/src/collab_ui.rs | 61 - crates/collab_ui/src/notification_panel.rs | 772 - crates/collab_ui/src/notifications.rs | 18 - .../src/notifications/collab_notification.rs | 52 - .../incoming_call_notification.rs | 135 - .../project_shared_notification.rs | 154 - crates/collab_ui/src/notifications/stories.rs | 3 - .../stories/collab_notification.rs | 48 - crates/collab_ui/src/panel_settings.rs | 41 - crates/command_palette/Cargo.toml | 51 - crates/command_palette/LICENSE-GPL | 1 - crates/command_palette/src/command_palette.rs | 1204 - crates/command_palette/src/persistence.rs | 236 - crates/command_palette_hooks/Cargo.toml | 19 - crates/command_palette_hooks/LICENSE-GPL | 1 - .../src/command_palette_hooks.rs | 153 - crates/component/Cargo.toml | 26 - crates/component/LICENSE-GPL | 1 - crates/component/src/component.rs | 329 - crates/component/src/component_layout.rs | 205 - crates/context_server/Cargo.toml | 39 - crates/context_server/LICENSE-GPL | 1 - crates/context_server/src/client.rs | 487 - crates/context_server/src/context_server.rs | 164 - crates/context_server/src/listener.rs | 440 - crates/context_server/src/protocol.rs | 125 - crates/context_server/src/test.rs | 130 - crates/context_server/src/transport.rs | 17 - crates/context_server/src/transport/http.rs | 259 - .../src/transport/stdio_transport.rs | 152 - crates/context_server/src/types.rs | 758 - crates/copilot/Cargo.toml | 79 - crates/copilot/LICENSE-GPL | 1 - crates/copilot/src/copilot.rs | 1472 - crates/copilot/src/copilot_chat.rs | 1013 - .../src/copilot_edit_prediction_delegate.rs | 1124 - crates/copilot/src/copilot_responses.rs | 419 - crates/copilot/src/request.rs | 225 - crates/copilot/src/sign_in.rs | 375 - crates/crashes/Cargo.toml | 33 - crates/crashes/LICENSE-GPL | 1 - crates/crashes/src/crashes.rs | 438 - crates/credentials_provider/Cargo.toml | 21 - crates/credentials_provider/LICENSE-GPL | 1 - .../src/credentials_provider.rs | 199 - crates/dap/Cargo.toml | 64 - crates/dap/LICENSE-GPL | 1 - crates/dap/docs/breakpoints.md | 9 - crates/dap/src/adapters.rs | 483 - crates/dap/src/client.rs | 462 - crates/dap/src/dap.rs | 76 - crates/dap/src/debugger_settings.rs | 61 - crates/dap/src/inline_value.rs | 20 - crates/dap/src/proto_conversions.rs | 591 - crates/dap/src/registry.rs | 93 - crates/dap/src/transport.rs | 1091 - crates/dap_adapters/Cargo.toml | 50 - crates/dap_adapters/LICENSE-GPL | 1 - crates/dap_adapters/src/codelldb.rs | 411 - crates/dap_adapters/src/dap_adapters.rs | 104 - crates/dap_adapters/src/gdb.rs | 265 - crates/dap_adapters/src/go.rs | 590 - crates/dap_adapters/src/javascript.rs | 569 - crates/dap_adapters/src/python.rs | 1125 - crates/db/Cargo.toml | 33 - crates/db/LICENSE-GPL | 1 - crates/db/README.md | 5 - crates/db/src/db.rs | 298 - crates/db/src/kvp.rs | 141 - crates/db/src/query.rs | 314 - crates/debug_adapter_extension/Cargo.toml | 23 - crates/debug_adapter_extension/LICENSE-GPL | 1 - .../src/debug_adapter_extension.rs | 66 - .../src/extension_dap_adapter.rs | 121 - .../src/extension_locator_adapter.rs | 52 - crates/debugger_tools/Cargo.toml | 29 - crates/debugger_tools/LICENSE-GPL | 1 - crates/debugger_tools/src/dap_log.rs | 1146 - crates/debugger_tools/src/debugger_tools.rs | 8 - crates/debugger_ui/Cargo.toml | 92 - crates/debugger_ui/LICENSE-GPL | 1 - crates/debugger_ui/src/attach_modal.rs | 439 - crates/debugger_ui/src/debugger_panel.rs | 2003 - crates/debugger_ui/src/debugger_ui.rs | 483 - crates/debugger_ui/src/dropdown_menus.rs | 335 - crates/debugger_ui/src/new_process_modal.rs | 1600 - crates/debugger_ui/src/onboarding_modal.rs | 166 - crates/debugger_ui/src/persistence.rs | 359 - crates/debugger_ui/src/session.rs | 210 - crates/debugger_ui/src/session/running.rs | 1902 - .../src/session/running/breakpoint_list.rs | 1516 - .../src/session/running/console.rs | 1012 - .../src/session/running/loaded_source_list.rs | 97 - .../src/session/running/memory_view.rs | 935 - .../src/session/running/module_list.rs | 286 - .../src/session/running/stack_frame_list.rs | 933 - .../src/session/running/variable_list.rs | 1658 - crates/debugger_ui/src/stack_trace_view.rs | 453 - crates/debugger_ui/src/tests.rs | 153 - crates/debugger_ui/src/tests/attach_modal.rs | 311 - crates/debugger_ui/src/tests/console.rs | 1094 - crates/debugger_ui/src/tests/dap_logger.rs | 117 - .../debugger_ui/src/tests/debugger_panel.rs | 1946 - crates/debugger_ui/src/tests/inline_values.rs | 2522 - crates/debugger_ui/src/tests/module_list.rs | 216 - .../src/tests/new_process_modal.rs | 437 - crates/debugger_ui/src/tests/persistence.rs | 131 - .../debugger_ui/src/tests/stack_frame_list.rs | 1267 - crates/debugger_ui/src/tests/variable_list.rs | 2342 - crates/deepseek/Cargo.toml | 24 - crates/deepseek/LICENSE-GPL | 1 - crates/deepseek/src/deepseek.rs | 308 - crates/denoise/Cargo.toml | 20 - crates/denoise/LICENSE-GPL | 1 - crates/denoise/README.md | 20 - crates/denoise/examples/denoise.rs | 11 - crates/denoise/examples/enable_disable.rs | 23 - .../models/model_1_converted_simplified.onnx | Bin 1466621 -> 0 bytes .../models/model_2_converted_simplified.onnx | Bin 2525727 -> 0 bytes crates/denoise/src/engine.rs | 204 - crates/denoise/src/lib.rs | 273 - crates/diagnostics/Cargo.toml | 51 - crates/diagnostics/LICENSE-GPL | 1 - crates/diagnostics/src/buffer_diagnostics.rs | 1008 - crates/diagnostics/src/diagnostic_renderer.rs | 320 - crates/diagnostics/src/diagnostics.rs | 1149 - crates/diagnostics/src/diagnostics_tests.rs | 2141 - crates/diagnostics/src/items.rs | 226 - crates/diagnostics/src/toolbar_controls.rs | 151 - crates/docs_preprocessor/Cargo.toml | 30 - crates/docs_preprocessor/LICENSE-GPL | 1 - crates/docs_preprocessor/src/main.rs | 694 - crates/edit_prediction/Cargo.toml | 77 - crates/edit_prediction/LICENSE-GPL | 1 - .../edit_prediction/license_examples/0bsd.txt | 13 - .../license_examples/apache-2.0-ex0.txt | 174 - .../license_examples/apache-2.0-ex1.txt | 55 - .../license_examples/apache-2.0-ex2.txt | 189 - .../license_examples/apache-2.0-ex3.txt | 13 - .../license_examples/apache-2.0-ex4.txt | 187 - .../license_examples/bsd-1-clause.txt | 20 - .../license_examples/bsd-2-clause-ex0.txt | 26 - .../license_examples/bsd-3-clause-ex0.txt | 29 - .../license_examples/bsd-3-clause-ex1.txt | 27 - .../license_examples/bsd-3-clause-ex2.txt | 31 - .../license_examples/bsd-3-clause-ex3.txt | 30 - .../license_examples/bsd-3-clause-ex4.txt | 27 - .../edit_prediction/license_examples/isc.txt | 15 - .../license_examples/mit-ex0.txt | 21 - .../license_examples/mit-ex1.txt | 26 - .../license_examples/mit-ex2.txt | 22 - .../license_examples/mit-ex3.txt | 21 - .../license_examples/upl-1.0.txt | 35 - .../license_examples/zlib-ex0.txt | 19 - .../license_patterns/0bsd-pattern | 11 - .../license_patterns/apache-2.0-pattern | 109 - .../apache-2.0-reference-pattern | 14 - .../license_patterns/bsd-pattern | 32 - .../license_patterns/isc-pattern | 12 - .../license_patterns/mit-pattern | 18 - .../license_patterns/upl-1.0-pattern | 32 - .../license_patterns/zlib-pattern | 21 - crates/edit_prediction/src/cursor_excerpt.rs | 78 - crates/edit_prediction/src/edit_prediction.rs | 2025 - .../src/edit_prediction_tests.rs | 1829 - .../edit_prediction/src/license_detection.rs | 846 - crates/edit_prediction/src/mercury.rs | 343 - .../edit_prediction/src/onboarding_modal.rs | 174 - .../edit_prediction/src/open_ai_response.rs | 31 - crates/edit_prediction/src/prediction.rs | 281 - crates/edit_prediction/src/sweep_ai.rs | 426 - crates/edit_prediction/src/udiff.rs | 876 - .../src/zed_edit_prediction_delegate.rs | 239 - crates/edit_prediction/src/zeta1.rs | 667 - crates/edit_prediction/src/zeta2.rs | 240 - crates/edit_prediction_cli/Cargo.toml | 72 - crates/edit_prediction_cli/LICENSE-GPL | 1 - crates/edit_prediction_cli/build.rs | 14 - .../src/anthropic_client.rs | 418 - crates/edit_prediction_cli/src/distill.rs | 22 - crates/edit_prediction_cli/src/example.rs | 379 - .../edit_prediction_cli/src/format_prompt.rs | 287 - crates/edit_prediction_cli/src/headless.rs | 140 - .../edit_prediction_cli/src/load_project.rs | 346 - crates/edit_prediction_cli/src/main.rs | 343 - crates/edit_prediction_cli/src/metrics.rs | 371 - crates/edit_prediction_cli/src/paths.rs | 27 - crates/edit_prediction_cli/src/predict.rs | 291 - crates/edit_prediction_cli/src/progress.rs | 508 - .../src/retrieve_context.rs | 192 - crates/edit_prediction_cli/src/score.rs | 123 - .../edit_prediction_cli/src/teacher.prompt.md | 53 - crates/edit_prediction_context/Cargo.toml | 44 - crates/edit_prediction_context/LICENSE-GPL | 1 - .../src/assemble_excerpts.rs | 156 - .../src/edit_prediction_context.rs | 474 - .../src/edit_prediction_context_tests.rs | 510 - crates/edit_prediction_context/src/excerpt.rs | 556 - .../src/fake_definition_lsp.rs | 329 - crates/edit_prediction_types/Cargo.toml | 18 - crates/edit_prediction_types/LICENSE-GPL | 1 - .../src/edit_prediction_types.rs | 298 - crates/edit_prediction_ui/Cargo.toml | 58 - crates/edit_prediction_ui/LICENSE-GPL | 1 - .../src/edit_prediction_button.rs | 1630 - .../src/edit_prediction_context_view.rs | 370 - .../src/edit_prediction_ui.rs | 128 - .../src/external_provider_api_token_modal.rs | 86 - .../src/rate_prediction_modal.rs | 905 - crates/editor/Cargo.toml | 138 - crates/editor/LICENSE-GPL | 1 - crates/editor/benches/display_map.rs | 107 - crates/editor/benches/editor_render.rs | 167 - crates/editor/src/actions.rs | 865 - crates/editor/src/blink_manager.rs | 117 - crates/editor/src/bracket_colorization.rs | 1374 - crates/editor/src/clangd_ext.rs | 130 - crates/editor/src/code_completion_tests.rs | 446 - crates/editor/src/code_context_menus.rs | 1638 - crates/editor/src/display_map.rs | 3095 - crates/editor/src/display_map/block_map.rs | 3820 - crates/editor/src/display_map/crease_map.rs | 529 - .../src/display_map/custom_highlights.rs | 324 - crates/editor/src/display_map/dimensions.rs | 96 - crates/editor/src/display_map/fold_map.rs | 2407 - crates/editor/src/display_map/inlay_map.rs | 2342 - crates/editor/src/display_map/invisibles.rs | 133 - crates/editor/src/display_map/tab_map.rs | 1622 - crates/editor/src/display_map/wrap_map.rs | 1610 - crates/editor/src/edit_prediction_tests.rs | 585 - crates/editor/src/editor.rs | 26162 ----- crates/editor/src/editor_settings.rs | 290 - crates/editor/src/editor_tests.rs | 29188 ----- crates/editor/src/element.rs | 12506 -- crates/editor/src/git.rs | 1 - crates/editor/src/git/blame.rs | 1233 - .../editor/src/highlight_matching_bracket.rs | 173 - crates/editor/src/hover_links.rs | 1709 - crates/editor/src/hover_popover.rs | 1902 - crates/editor/src/indent_guides.rs | 250 - crates/editor/src/inlays.rs | 193 - crates/editor/src/inlays/inlay_hints.rs | 4180 - crates/editor/src/items.rs | 2175 - crates/editor/src/jsx_tag_auto_close.rs | 861 - crates/editor/src/linked_editing_ranges.rs | 177 - crates/editor/src/lsp_colors.rs | 416 - crates/editor/src/lsp_ext.rs | 181 - crates/editor/src/mouse_context_menu.rs | 373 - crates/editor/src/movement.rs | 1353 - crates/editor/src/persistence.rs | 468 - crates/editor/src/rust_analyzer_ext.rs | 396 - crates/editor/src/scroll.rs | 782 - crates/editor/src/scroll/actions.rs | 130 - crates/editor/src/scroll/autoscroll.rs | 372 - crates/editor/src/scroll/scroll_amount.rs | 82 - crates/editor/src/selections_collection.rs | 1163 - crates/editor/src/signature_help.rs | 459 - crates/editor/src/split.rs | 267 - crates/editor/src/tasks.rs | 110 - crates/editor/src/test.rs | 253 - .../src/test/editor_lsp_test_context.rs | 423 - crates/editor/src/test/editor_test_context.rs | 821 - crates/eval/.gitignore | 3 - crates/eval/Cargo.toml | 69 - crates/eval/LICENSE-GPL | 1 - crates/eval/README.md | 27 - crates/eval/build.rs | 14 - crates/eval/docs/explorer.md | 27 - crates/eval/runner_settings.json | 5 - crates/eval/src/assertions.rs | 170 - crates/eval/src/eval.rs | 740 - crates/eval/src/example.rs | 559 - .../src/examples/add_arg_to_trait_method.rs | 116 - .../eval/src/examples/code_block_citations.rs | 228 - .../eval/src/examples/comment_translation.rs | 60 - .../src/examples/file_change_notification.rs | 74 - crates/eval/src/examples/file_search.rs | 55 - .../examples/find_and_replace_diff_card.toml | 43 - .../src/examples/grep_params_escapement.rs | 59 - .../src/examples/hallucinated_tool_calls.toml | 13 - crates/eval/src/examples/mod.rs | 173 - .../eval/src/examples/no_tools_enabled.toml | 19 - crates/eval/src/examples/overwrite_file.rs | 51 - crates/eval/src/examples/planets.rs | 75 - .../src/examples/threads/overwrite-file.json | 262 - .../tree_sitter_drop_emscripten_dep.toml | 53 - crates/eval/src/explorer.html | 949 - crates/eval/src/explorer.rs | 182 - crates/eval/src/ids.rs | 29 - crates/eval/src/instance.rs | 1424 - crates/eval/src/judge_diff_prompt.hbs | 25 - crates/eval/src/judge_thread_prompt.hbs | 21 - crates/eval/src/tool_metrics.rs | 106 - crates/eval_utils/Cargo.toml | 18 - crates/eval_utils/LICENSE-GPL | 1 - crates/eval_utils/README.md | 3 - crates/eval_utils/src/eval_utils.rs | 128 - .../AppxManifest-Nightly.xml | 78 - .../AppxManifest-Preview.xml | 78 - .../AppxManifest.xml | 79 - crates/explorer_command_injector/Cargo.toml | 27 - crates/explorer_command_injector/LICENSE-GPL | 1 - .../src/explorer_command_injector.rs | 202 - crates/extension/Cargo.toml | 44 - crates/extension/LICENSE-GPL | 1 - crates/extension/src/capabilities.rs | 20 - .../capabilities/download_file_capability.rs | 121 - .../npm_install_package_capability.rs | 39 - .../capabilities/process_exec_capability.rs | 116 - crates/extension/src/extension.rs | 210 - crates/extension/src/extension_builder.rs | 826 - crates/extension/src/extension_events.rs | 43 - crates/extension/src/extension_host_proxy.rs | 448 - crates/extension/src/extension_manifest.rs | 522 - crates/extension/src/types.rs | 71 - crates/extension/src/types/context_server.rs | 10 - crates/extension/src/types/dap.rs | 8 - crates/extension/src/types/lsp.rs | 96 - crates/extension/src/types/slash_command.rs | 43 - crates/extension_cli/Cargo.toml | 32 - crates/extension_cli/LICENSE-GPL | 1 - crates/extension_cli/src/main.rs | 421 - crates/extension_host/Cargo.toml | 73 - crates/extension_host/LICENSE-GPL | 1 - .../extension_compilation_benchmark.rs | 155 - crates/extension_host/build.rs | 46 - .../extension_host/src/capability_granter.rs | 153 - crates/extension_host/src/extension_host.rs | 1878 - .../extension_host/src/extension_settings.rs | 65 - .../src/extension_store_test.rs | 874 - crates/extension_host/src/headless_host.rs | 351 - crates/extension_host/src/wasm_host.rs | 900 - crates/extension_host/src/wasm_host/wit.rs | 1124 - .../src/wasm_host/wit/since_v0_0_1.rs | 158 - .../src/wasm_host/wit/since_v0_0_4.rs | 164 - .../src/wasm_host/wit/since_v0_0_6.rs | 197 - .../src/wasm_host/wit/since_v0_1_0.rs | 575 - .../src/wasm_host/wit/since_v0_2_0.rs | 238 - .../src/wasm_host/wit/since_v0_3_0.rs | 217 - .../src/wasm_host/wit/since_v0_4_0.rs | 217 - .../src/wasm_host/wit/since_v0_5_0.rs | 218 - .../src/wasm_host/wit/since_v0_6_0.rs | 222 - .../src/wasm_host/wit/since_v0_8_0.rs | 1109 - crates/extensions_ui/Cargo.toml | 45 - crates/extensions_ui/LICENSE-GPL | 1 - crates/extensions_ui/src/components.rs | 3 - .../src/components/extension_card.rs | 61 - crates/extensions_ui/src/extension_suggest.rs | 248 - .../src/extension_version_selector.rs | 251 - crates/extensions_ui/src/extensions_ui.rs | 1784 - crates/feature_flags/Cargo.toml | 17 - crates/feature_flags/LICENSE-GPL | 1 - crates/feature_flags/src/feature_flags.rs | 245 - crates/feature_flags/src/flags.rs | 23 - crates/feedback/Cargo.toml | 26 - crates/feedback/LICENSE-GPL | 1 - crates/feedback/src/feedback.rs | 98 - crates/file_finder/Cargo.toml | 46 - crates/file_finder/LICENSE-GPL | 1 - crates/file_finder/src/file_finder.rs | 1911 - .../file_finder/src/file_finder_settings.rs | 51 - crates/file_finder/src/file_finder_tests.rs | 3550 - crates/file_finder/src/open_path_prompt.rs | 995 - .../file_finder/src/open_path_prompt_tests.rs | 463 - crates/file_icons/Cargo.toml | 19 - crates/file_icons/LICENSE-GPL | 1 - crates/file_icons/src/file_icons.rs | 166 - crates/fs/Cargo.toml | 57 - crates/fs/LICENSE-GPL | 1 - crates/fs/src/fake_git_repo.rs | 793 - crates/fs/src/fs.rs | 3453 - crates/fs/src/fs_watcher.rs | 265 - crates/fs/src/mac_watcher.rs | 77 - crates/fs_benchmarks/Cargo.toml | 12 - crates/fs_benchmarks/LICENSE-GPL | 1 - crates/fs_benchmarks/src/main.rs | 32 - crates/fsevent/Cargo.toml | 28 - crates/fsevent/LICENSE-GPL | 1 - crates/fsevent/examples/events.rs | 23 - crates/fsevent/src/fsevent.rs | 515 - crates/fuzzy/Cargo.toml | 21 - crates/fuzzy/LICENSE-GPL | 1 - crates/fuzzy/src/char_bag.rs | 63 - crates/fuzzy/src/fuzzy.rs | 10 - crates/fuzzy/src/matcher.rs | 611 - crates/fuzzy/src/paths.rs | 291 - crates/fuzzy/src/strings.rs | 199 - crates/git/Cargo.toml | 52 - crates/git/LICENSE-GPL | 1 - crates/git/src/blame.rs | 367 - crates/git/src/checkpoint.gitignore | 91 - crates/git/src/commit.rs | 119 - crates/git/src/git.rs | 254 - crates/git/src/hosting_provider.rs | 244 - crates/git/src/remote.rs | 98 - crates/git/src/repository.rs | 3147 - crates/git/src/stash.rs | 223 - crates/git/src/status.rs | 619 - .../git/test_data/blame_incremental_complex | 194 - .../test_data/blame_incremental_not_committed | 64 - crates/git/test_data/blame_incremental_simple | 70 - .../golden/blame_incremental_complex.json | 781 - .../blame_incremental_not_committed.json | 135 - .../golden/blame_incremental_simple.json | 135 - crates/git_hosting_providers/Cargo.toml | 34 - crates/git_hosting_providers/LICENSE-GPL | 1 - .../src/git_hosting_providers.rs | 101 - crates/git_hosting_providers/src/providers.rs | 17 - .../src/providers/bitbucket.rs | 591 - .../src/providers/chromium.rs | 303 - .../src/providers/forgejo.rs | 433 - .../src/providers/gitea.rs | 380 - .../src/providers/gitee.rs | 250 - .../src/providers/github.rs | 529 - .../src/providers/gitlab.rs | 420 - .../src/providers/sourcehut.rs | 385 - crates/git_hosting_providers/src/settings.rs | 76 - crates/git_ui/Cargo.toml | 83 - crates/git_ui/LICENSE-GPL | 1 - crates/git_ui/src/askpass_modal.rs | 147 - crates/git_ui/src/blame_ui.rs | 427 - crates/git_ui/src/branch_picker.rs | 1828 - crates/git_ui/src/commit_message_prompt.txt | 17 - crates/git_ui/src/commit_modal.rs | 593 - crates/git_ui/src/commit_tooltip.rs | 381 - crates/git_ui/src/commit_view.rs | 1028 - crates/git_ui/src/conflict_view.rs | 520 - crates/git_ui/src/file_diff_view.rs | 577 - crates/git_ui/src/file_history_view.rs | 588 - crates/git_ui/src/git_panel.rs | 7008 -- crates/git_ui/src/git_panel_settings.rs | 63 - crates/git_ui/src/git_ui.rs | 888 - crates/git_ui/src/onboarding.rs | 145 - crates/git_ui/src/picker_prompt.rs | 239 - crates/git_ui/src/project_diff.rs | 2380 - crates/git_ui/src/remote_output.rs | 300 - crates/git_ui/src/repository_selector.rs | 238 - crates/git_ui/src/stash_picker.rs | 547 - crates/git_ui/src/text_diff_view.rs | 736 - crates/git_ui/src/worktree_picker.rs | 743 - crates/go_to_line/Cargo.toml | 39 - crates/go_to_line/LICENSE-GPL | 1 - crates/go_to_line/src/cursor_position.rs | 315 - crates/go_to_line/src/go_to_line.rs | 943 - crates/google_ai/Cargo.toml | 25 - crates/google_ai/LICENSE-GPL | 1 - crates/google_ai/src/google_ai.rs | 717 - crates/icons/Cargo.toml | 16 - crates/icons/LICENSE-GPL | 1 - crates/icons/README.md | 29 - crates/icons/src/icons.rs | 282 - crates/image_viewer/Cargo.toml | 35 - crates/image_viewer/LICENSE-GPL | 1 - crates/image_viewer/src/image_info.rs | 105 - crates/image_viewer/src/image_viewer.rs | 470 - .../image_viewer/src/image_viewer_settings.rs | 19 - crates/inspector_ui/Cargo.toml | 30 - crates/inspector_ui/LICENSE-GPL | 1 - crates/inspector_ui/README.md | 112 - crates/inspector_ui/build.rs | 20 - crates/inspector_ui/src/div_inspector.rs | 732 - crates/inspector_ui/src/inspector.rs | 183 - crates/inspector_ui/src/inspector_ui.rs | 24 - crates/install_cli/Cargo.toml | 24 - crates/install_cli/LICENSE-GPL | 1 - crates/install_cli/src/install_cli.rs | 7 - crates/install_cli/src/install_cli_binary.rs | 101 - crates/install_cli/src/register_zed_scheme.rs | 15 - crates/journal/Cargo.toml | 27 - crates/journal/LICENSE-GPL | 1 - crates/journal/src/journal.rs | 294 - crates/json_schema_store/Cargo.toml | 40 - crates/json_schema_store/LICENSE-GPL | 1 - .../src/json_schema_store.rs | 356 - .../src/schemas/package.json | 1309 - .../src/schemas/tsconfig.json | 1472 - crates/keymap_editor/Cargo.toml | 52 - crates/keymap_editor/LICENSE-GPL | 1 - crates/keymap_editor/src/keymap_editor.rs | 3637 - .../src/ui_components/keystroke_input.rs | 1524 - crates/keymap_editor/src/ui_components/mod.rs | 1 - crates/language/Cargo.toml | 98 - crates/language/LICENSE-GPL | 1 - crates/language/build.rs | 5 - crates/language/src/buffer.rs | 5682 - crates/language/src/buffer/row_chunk.rs | 121 - crates/language/src/buffer_tests.rs | 3969 - crates/language/src/diagnostic_set.rs | 359 - crates/language/src/highlight_map.rs | 114 - crates/language/src/language.rs | 3012 - crates/language/src/language_registry.rs | 1290 - crates/language/src/language_settings.rs | 933 - crates/language/src/manifest.rs | 59 - crates/language/src/outline.rs | 327 - crates/language/src/proto.rs | 646 - crates/language/src/syntax_map.rs | 2002 - .../src/syntax_map/syntax_map_tests.rs | 1416 - crates/language/src/task_context.rs | 55 - crates/language/src/text_diff.rs | 375 - crates/language/src/toolchain.rs | 189 - crates/language_extension/Cargo.toml | 28 - crates/language_extension/LICENSE-GPL | 1 - .../src/extension_lsp_adapter.rs | 716 - .../src/language_extension.rs | 66 - crates/language_model/Cargo.toml | 46 - crates/language_model/LICENSE-GPL | 1 - crates/language_model/src/fake_provider.rs | 285 - crates/language_model/src/language_model.rs | 1086 - .../language_model/src/model/cloud_model.rs | 130 - crates/language_model/src/model/mod.rs | 3 - crates/language_model/src/rate_limiter.rs | 77 - crates/language_model/src/registry.rs | 419 - crates/language_model/src/request.rs | 576 - crates/language_model/src/role.rs | 47 - crates/language_model/src/telemetry.rs | 84 - crates/language_model/src/tool_schema.rs | 384 - crates/language_models/Cargo.toml | 68 - crates/language_models/LICENSE-GPL | 1 - crates/language_models/src/api_key.rs | 295 - crates/language_models/src/language_models.rs | 165 - crates/language_models/src/provider.rs | 14 - .../language_models/src/provider/anthropic.rs | 1045 - .../language_models/src/provider/bedrock.rs | 1327 - crates/language_models/src/provider/cloud.rs | 1396 - .../src/provider/copilot_chat.rs | 1565 - .../language_models/src/provider/deepseek.rs | 660 - crates/language_models/src/provider/google.rs | 1328 - .../language_models/src/provider/lmstudio.rs | 777 - .../language_models/src/provider/mistral.rs | 1107 - crates/language_models/src/provider/ollama.rs | 929 - .../language_models/src/provider/open_ai.rs | 911 - .../src/provider/open_ai_compatible.rs | 512 - .../src/provider/open_router.rs | 1095 - crates/language_models/src/provider/vercel.rs | 514 - crates/language_models/src/provider/x_ai.rs | 516 - crates/language_models/src/settings.rs | 116 - crates/language_models/src/ui.rs | 4 - .../src/ui/configured_api_card.rs | 86 - .../src/ui/instruction_list_item.rs | 69 - crates/language_onboarding/Cargo.toml | 29 - crates/language_onboarding/LICENSE-GPL | 1 - crates/language_onboarding/src/python.rs | 102 - crates/language_selector/Cargo.toml | 31 - crates/language_selector/LICENSE-GPL | 1 - .../src/active_buffer_language.rs | 89 - .../src/language_selector.rs | 297 - crates/language_tools/Cargo.toml | 44 - crates/language_tools/LICENSE-GPL | 1 - crates/language_tools/src/key_context_view.rs | 298 - crates/language_tools/src/language_tools.rs | 54 - crates/language_tools/src/lsp_button.rs | 1048 - crates/language_tools/src/lsp_log_view.rs | 1350 - .../language_tools/src/lsp_log_view_tests.rs | 115 - crates/language_tools/src/syntax_tree_view.rs | 735 - crates/languages/Cargo.toml | 111 - crates/languages/LICENSE-GPL | 1 - crates/languages/src/bash.rs | 164 - crates/languages/src/bash/brackets.scm | 12 - crates/languages/src/bash/config.toml | 37 - crates/languages/src/bash/highlights.scm | 103 - crates/languages/src/bash/indents.scm | 12 - crates/languages/src/bash/injections.scm | 3 - crates/languages/src/bash/overrides.scm | 2 - crates/languages/src/bash/redactions.scm | 2 - crates/languages/src/bash/textobjects.scm | 7 - crates/languages/src/c.rs | 643 - crates/languages/src/c/brackets.scm | 5 - crates/languages/src/c/config.toml | 20 - crates/languages/src/c/embedding.scm | 43 - crates/languages/src/c/highlights.scm | 134 - crates/languages/src/c/imports.scm | 7 - crates/languages/src/c/indents.scm | 19 - crates/languages/src/c/injections.scm | 12 - crates/languages/src/c/outline.scm | 72 - crates/languages/src/c/overrides.scm | 2 - crates/languages/src/c/runnables.scm | 10 - crates/languages/src/c/textobjects.scm | 25 - crates/languages/src/cpp.rs | 252 - crates/languages/src/cpp/brackets.scm | 5 - crates/languages/src/cpp/config.toml | 20 - crates/languages/src/cpp/embedding.scm | 61 - crates/languages/src/cpp/highlights.scm | 276 - crates/languages/src/cpp/imports.scm | 5 - crates/languages/src/cpp/indents.scm | 19 - crates/languages/src/cpp/injections.scm | 16 - crates/languages/src/cpp/outline.scm | 157 - crates/languages/src/cpp/overrides.scm | 2 - crates/languages/src/cpp/textobjects.scm | 31 - crates/languages/src/css.rs | 246 - crates/languages/src/css/brackets.scm | 5 - crates/languages/src/css/config.toml | 14 - crates/languages/src/css/highlights.scm | 109 - crates/languages/src/css/indents.scm | 1 - crates/languages/src/css/injections.scm | 3 - crates/languages/src/css/outline.scm | 18 - crates/languages/src/css/overrides.scm | 2 - crates/languages/src/css/textobjects.scm | 30 - crates/languages/src/diff/config.toml | 4 - crates/languages/src/diff/highlights.scm | 48 - crates/languages/src/diff/injections.scm | 2 - crates/languages/src/eslint.rs | 772 - crates/languages/src/gitcommit/config.toml | 24 - crates/languages/src/gitcommit/highlights.scm | 18 - crates/languages/src/gitcommit/injections.scm | 9 - crates/languages/src/go.rs | 1345 - crates/languages/src/go/brackets.scm | 6 - crates/languages/src/go/config.toml | 18 - crates/languages/src/go/debugger.scm | 26 - crates/languages/src/go/embedding.scm | 24 - crates/languages/src/go/highlights.scm | 140 - crates/languages/src/go/imports.scm | 14 - crates/languages/src/go/indents.scm | 9 - crates/languages/src/go/injections.scm | 735 - crates/languages/src/go/outline.scm | 67 - crates/languages/src/go/overrides.scm | 6 - crates/languages/src/go/runnables.scm | 208 - crates/languages/src/go/textobjects.scm | 25 - crates/languages/src/gomod/config.toml | 9 - crates/languages/src/gomod/highlights.scm | 20 - crates/languages/src/gomod/injections.scm | 2 - crates/languages/src/gomod/structure.scm | 35 - crates/languages/src/gowork/config.toml | 9 - crates/languages/src/gowork/highlights.scm | 14 - crates/languages/src/gowork/injections.scm | 2 - crates/languages/src/javascript/brackets.scm | 9 - crates/languages/src/javascript/config.toml | 44 - crates/languages/src/javascript/contexts.scm | 0 crates/languages/src/javascript/debugger.scm | 23 - crates/languages/src/javascript/embedding.scm | 71 - .../languages/src/javascript/highlights.scm | 362 - crates/languages/src/javascript/imports.scm | 14 - crates/languages/src/javascript/indents.scm | 21 - .../languages/src/javascript/injections.scm | 85 - crates/languages/src/javascript/outline.scm | 223 - crates/languages/src/javascript/overrides.scm | 14 - crates/languages/src/javascript/runnables.scm | 46 - .../languages/src/javascript/textobjects.scm | 51 - crates/languages/src/jsdoc/brackets.scm | 2 - crates/languages/src/jsdoc/config.toml | 8 - crates/languages/src/jsdoc/highlights.scm | 3 - crates/languages/src/json.rs | 461 - crates/languages/src/json/brackets.scm | 3 - crates/languages/src/json/config.toml | 17 - crates/languages/src/json/embedding.scm | 14 - crates/languages/src/json/highlights.scm | 28 - crates/languages/src/json/indents.scm | 2 - crates/languages/src/json/outline.scm | 2 - crates/languages/src/json/overrides.scm | 1 - crates/languages/src/json/redactions.scm | 4 - crates/languages/src/json/runnables.scm | 21 - crates/languages/src/json/textobjects.scm | 1 - crates/languages/src/jsonc/brackets.scm | 3 - crates/languages/src/jsonc/config.toml | 16 - crates/languages/src/jsonc/embedding.scm | 14 - crates/languages/src/jsonc/highlights.scm | 28 - crates/languages/src/jsonc/indents.scm | 2 - crates/languages/src/jsonc/injections.scm | 2 - crates/languages/src/jsonc/outline.scm | 2 - crates/languages/src/jsonc/overrides.scm | 2 - crates/languages/src/jsonc/redactions.scm | 4 - crates/languages/src/jsonc/textobjects.scm | 1 - crates/languages/src/lib.rs | 433 - .../languages/src/markdown-inline/config.toml | 3 - .../src/markdown-inline/highlights.scm | 22 - .../src/markdown-inline/injections.scm | 2 - crates/languages/src/markdown/brackets.scm | 7 - crates/languages/src/markdown/config.toml | 32 - crates/languages/src/markdown/highlights.scm | 34 - crates/languages/src/markdown/indents.scm | 3 - crates/languages/src/markdown/injections.scm | 14 - crates/languages/src/markdown/outline.scm | 5 - crates/languages/src/markdown/textobjects.scm | 3 - crates/languages/src/package_json.rs | 128 - crates/languages/src/python.rs | 2535 - crates/languages/src/python/brackets.scm | 4 - crates/languages/src/python/config.toml | 38 - crates/languages/src/python/debugger.scm | 43 - crates/languages/src/python/embedding.scm | 9 - crates/languages/src/python/highlights.scm | 303 - crates/languages/src/python/imports.scm | 32 - crates/languages/src/python/indents.scm | 17 - crates/languages/src/python/injections.scm | 3 - crates/languages/src/python/outline.scm | 11 - crates/languages/src/python/overrides.scm | 2 - crates/languages/src/python/runnables.scm | 151 - crates/languages/src/python/textobjects.scm | 7 - crates/languages/src/regex/brackets.scm | 3 - crates/languages/src/regex/config.toml | 9 - crates/languages/src/regex/highlights.scm | 55 - crates/languages/src/rust.rs | 1821 - crates/languages/src/rust/brackets.scm | 7 - crates/languages/src/rust/config.toml | 21 - crates/languages/src/rust/debugger.scm | 50 - crates/languages/src/rust/embedding.scm | 32 - crates/languages/src/rust/highlights.scm | 219 - crates/languages/src/rust/imports.scm | 27 - crates/languages/src/rust/indents.scm | 14 - crates/languages/src/rust/injections.scm | 63 - crates/languages/src/rust/outline.scm | 73 - crates/languages/src/rust/overrides.scm | 8 - crates/languages/src/rust/runnables.scm | 92 - crates/languages/src/rust/textobjects.scm | 51 - crates/languages/src/tailwind.rs | 228 - crates/languages/src/tsx/brackets.scm | 11 - crates/languages/src/tsx/config.toml | 46 - crates/languages/src/tsx/debugger.scm | 25 - crates/languages/src/tsx/embedding.scm | 85 - crates/languages/src/tsx/highlights.scm | 422 - crates/languages/src/tsx/imports.scm | 14 - crates/languages/src/tsx/indents.scm | 21 - crates/languages/src/tsx/injections.scm | 85 - crates/languages/src/tsx/outline.scm | 230 - crates/languages/src/tsx/overrides.scm | 18 - crates/languages/src/tsx/runnables.scm | 46 - crates/languages/src/tsx/textobjects.scm | 79 - crates/languages/src/typescript.rs | 1472 - crates/languages/src/typescript/brackets.scm | 7 - crates/languages/src/typescript/config.toml | 33 - crates/languages/src/typescript/debugger.scm | 23 - crates/languages/src/typescript/embedding.scm | 85 - .../languages/src/typescript/highlights.scm | 446 - crates/languages/src/typescript/imports.scm | 20 - crates/languages/src/typescript/indents.scm | 17 - .../languages/src/typescript/injections.scm | 126 - crates/languages/src/typescript/outline.scm | 230 - crates/languages/src/typescript/overrides.scm | 9 - crates/languages/src/typescript/runnables.scm | 85 - .../languages/src/typescript/textobjects.scm | 79 - crates/languages/src/vtsls.rs | 362 - crates/languages/src/yaml.rs | 185 - crates/languages/src/yaml/brackets.scm | 4 - crates/languages/src/yaml/config.toml | 24 - crates/languages/src/yaml/highlights.scm | 49 - crates/languages/src/yaml/injections.scm | 25 - crates/languages/src/yaml/outline.scm | 9 - crates/languages/src/yaml/overrides.scm | 5 - crates/languages/src/yaml/redactions.scm | 1 - crates/languages/src/yaml/textobjects.scm | 1 - .../src/zed-keybind-context/brackets.scm | 1 - .../src/zed-keybind-context/config.toml | 6 - .../src/zed-keybind-context/highlights.scm | 23 - crates/line_ending_selector/Cargo.toml | 23 - crates/line_ending_selector/LICENSE-GPL | 1 - .../src/line_ending_indicator.rs | 68 - .../src/line_ending_selector.rs | 192 - crates/livekit_api/Cargo.toml | 30 - crates/livekit_api/LICENSE-GPL | 1 - crates/livekit_api/build.rs | 9 - crates/livekit_api/src/livekit_api.rs | 182 - crates/livekit_api/src/proto.rs | 1 - crates/livekit_api/src/token.rs | 110 - .../livekit_api/vendored/protocol/README.md | 5 - .../vendored/protocol/livekit_analytics.proto | 118 - .../vendored/protocol/livekit_egress.proto | 263 - .../vendored/protocol/livekit_ingress.proto | 134 - .../vendored/protocol/livekit_internal.proto | 138 - .../vendored/protocol/livekit_models.proto | 330 - .../vendored/protocol/livekit_room.proto | 159 - .../protocol/livekit_rpc_internal.proto | 83 - .../vendored/protocol/livekit_rtc.proto | 301 - .../vendored/protocol/livekit_webhook.proto | 36 - crates/livekit_client/Cargo.toml | 73 - crates/livekit_client/LICENSE-GPL | 1 - crates/livekit_client/examples/test_app.rs | 401 - crates/livekit_client/src/lib.rs | 237 - crates/livekit_client/src/livekit_client.rs | 485 - .../src/livekit_client/playback.rs | 1021 - .../src/livekit_client/playback/source.rs | 92 - crates/livekit_client/src/mock_client.rs | 38 - .../src/mock_client/participant.rs | 136 - .../src/mock_client/publication.rs | 91 - .../livekit_client/src/mock_client/track.rs | 56 - crates/livekit_client/src/record.rs | 96 - .../src/remote_video_track_view.rs | 109 - crates/livekit_client/src/test.rs | 765 - crates/lmstudio/Cargo.toml | 24 - crates/lmstudio/LICENSE-GPL | 1 - crates/lmstudio/src/lmstudio.rs | 496 - crates/lsp/Cargo.toml | 41 - crates/lsp/LICENSE-GPL | 1 - crates/lsp/src/input_handler.rs | 157 - crates/lsp/src/lsp.rs | 1993 - crates/markdown/Cargo.toml | 43 - crates/markdown/LICENSE-GPL | 1 - crates/markdown/examples/markdown.rs | 121 - crates/markdown/examples/markdown_as_child.rs | 113 - crates/markdown/src/markdown.rs | 1994 - crates/markdown/src/parser.rs | 830 - crates/markdown/src/path_range.rs | 245 - crates/markdown_preview/Cargo.toml | 40 - crates/markdown_preview/LICENSE-GPL | 1 - .../markdown_preview/src/markdown_elements.rs | 358 - .../markdown_preview/src/markdown_minifier.rs | 829 - .../markdown_preview/src/markdown_parser.rs | 3218 - .../markdown_preview/src/markdown_preview.rs | 44 - .../src/markdown_preview_view.rs | 677 - .../markdown_preview/src/markdown_renderer.rs | 1083 - crates/menu/Cargo.toml | 16 - crates/menu/LICENSE-GPL | 1 - crates/menu/src/menu.rs | 33 - crates/migrator/Cargo.toml | 29 - crates/migrator/LICENSE-GPL | 1 - crates/migrator/src/migrations.rs | 161 - .../src/migrations/m_2025_01_02/settings.rs | 62 - .../src/migrations/m_2025_01_29/keymap.rs | 307 - .../src/migrations/m_2025_01_29/settings.rs | 101 - .../src/migrations/m_2025_01_30/keymap.rs | 82 - .../src/migrations/m_2025_01_30/settings.rs | 83 - .../src/migrations/m_2025_03_03/keymap.rs | 75 - .../src/migrations/m_2025_03_06/keymap.rs | 38 - .../src/migrations/m_2025_03_29/settings.rs | 65 - .../src/migrations/m_2025_04_15/keymap.rs | 31 - .../src/migrations/m_2025_04_15/settings.rs | 29 - .../src/migrations/m_2025_04_21/settings.rs | 25 - .../src/migrations/m_2025_04_23/settings.rs | 27 - .../src/migrations/m_2025_05_05/settings.rs | 41 - .../src/migrations/m_2025_05_08/settings.rs | 26 - .../src/migrations/m_2025_05_29/settings.rs | 51 - .../src/migrations/m_2025_06_16/settings.rs | 90 - .../src/migrations/m_2025_06_25/settings.rs | 133 - .../src/migrations/m_2025_06_27/settings.rs | 132 - .../src/migrations/m_2025_07_08/settings.rs | 37 - .../src/migrations/m_2025_10_01/settings.rs | 74 - .../src/migrations/m_2025_10_02/settings.rs | 41 - .../src/migrations/m_2025_10_03/settings.rs | 30 - .../src/migrations/m_2025_10_16/settings.rs | 71 - .../src/migrations/m_2025_10_17/settings.rs | 24 - .../src/migrations/m_2025_10_21/settings.rs | 16 - .../src/migrations/m_2025_11_12/settings.rs | 84 - .../src/migrations/m_2025_11_20/settings.rs | 76 - .../src/migrations/m_2025_11_25/settings.rs | 17 - .../src/migrations/m_2025_12_01/settings.rs | 55 - crates/migrator/src/migrator.rs | 2418 - crates/migrator/src/patterns.rs | 14 - crates/migrator/src/patterns/keymap.rs | 77 - crates/migrator/src/patterns/settings.rs | 131 - crates/miniprofiler_ui/Cargo.toml | 23 - crates/miniprofiler_ui/LICENSE-GPL | 1 - crates/miniprofiler_ui/src/miniprofiler_ui.rs | 410 - crates/mistral/Cargo.toml | 25 - crates/mistral/LICENSE-GPL | 1 - crates/mistral/src/mistral.rs | 482 - crates/multi_buffer/Cargo.toml | 63 - crates/multi_buffer/LICENSE-GPL | 1 - crates/multi_buffer/src/anchor.rs | 257 - crates/multi_buffer/src/multi_buffer.rs | 8227 -- crates/multi_buffer/src/multi_buffer_tests.rs | 4652 - crates/multi_buffer/src/path_key.rs | 437 - crates/multi_buffer/src/transaction.rs | 538 - crates/nc/Cargo.toml | 19 - crates/nc/LICENSE-GPL | 1 - crates/nc/src/nc.rs | 56 - crates/net/Cargo.toml | 24 - crates/net/LICENSE-GPL | 1 - crates/net/src/async_net.rs | 69 - crates/net/src/listener.rs | 45 - crates/net/src/net.rs | 107 - crates/net/src/socket.rs | 59 - crates/net/src/stream.rs | 60 - crates/net/src/util.rs | 76 - crates/node_runtime/Cargo.toml | 36 - crates/node_runtime/LICENSE-GPL | 1 - crates/node_runtime/src/node_runtime.rs | 880 - crates/notifications/Cargo.toml | 44 - crates/notifications/LICENSE-GPL | 1 - .../notifications/src/notification_store.rs | 432 - crates/notifications/src/notifications.rs | 4 - crates/notifications/src/status_toast.rs | 254 - crates/ollama/Cargo.toml | 25 - crates/ollama/LICENSE-GPL | 1 - crates/ollama/src/ollama.rs | 660 - crates/onboarding/Cargo.toml | 44 - crates/onboarding/LICENSE-GPL | 1 - crates/onboarding/src/base_keymap_picker.rs | 229 - crates/onboarding/src/basics_page.rs | 486 - crates/onboarding/src/multibuffer_hint.rs | 184 - crates/onboarding/src/onboarding.rs | 676 - crates/onboarding/src/theme_preview.rs | 381 - crates/onboarding/src/welcome.rs | 443 - crates/open_ai/Cargo.toml | 28 - crates/open_ai/LICENSE-GPL | 1 - crates/open_ai/src/open_ai.rs | 626 - crates/open_router/Cargo.toml | 27 - crates/open_router/LICENSE-GPL | 1 - crates/open_router/src/open_router.rs | 750 - crates/outline/Cargo.toml | 41 - crates/outline/LICENSE-GPL | 1 - crates/outline/src/outline.rs | 610 - crates/outline_panel/Cargo.toml | 47 - crates/outline_panel/LICENSE-GPL | 1 - crates/outline_panel/src/outline_panel.rs | 7760 -- .../src/outline_panel_settings.rs | 66 - crates/panel/Cargo.toml | 20 - crates/panel/LICENSE-GPL | 1 - crates/panel/src/panel.rs | 136 - crates/paths/Cargo.toml | 20 - crates/paths/LICENSE-GPL | 1 - crates/paths/src/paths.rs | 566 - crates/picker/Cargo.toml | 34 - crates/picker/LICENSE-GPL | 1 - crates/picker/src/head.rs | 68 - .../src/highlighted_match_with_paths.rs | 116 - crates/picker/src/picker.rs | 976 - crates/picker/src/popover_menu.rs | 101 - crates/prettier/Cargo.toml | 36 - crates/prettier/LICENSE-GPL | 1 - crates/prettier/src/prettier.rs | 1191 - crates/prettier/src/prettier_server.js | 266 - crates/project/Cargo.toml | 120 - crates/project/LICENSE-GPL | 1 - crates/project/src/agent_server_store.rs | 2346 - crates/project/src/buffer_store.rs | 1686 - crates/project/src/color_extractor.rs | 295 - crates/project/src/connection_manager.rs | 223 - crates/project/src/context_server_store.rs | 1437 - .../src/context_server_store/extension.rs | 120 - .../src/context_server_store/registry.rs | 84 - crates/project/src/debounced_delay.rs | 54 - crates/project/src/debugger.rs | 23 - .../project/src/debugger/breakpoint_store.rs | 1034 - crates/project/src/debugger/dap_command.rs | 1975 - crates/project/src/debugger/dap_store.rs | 1035 - crates/project/src/debugger/locators.rs | 4 - crates/project/src/debugger/locators/cargo.rs | 219 - crates/project/src/debugger/locators/go.rs | 441 - crates/project/src/debugger/locators/node.rs | 62 - .../project/src/debugger/locators/python.rs | 146 - crates/project/src/debugger/memory.rs | 384 - crates/project/src/debugger/session.rs | 3177 - crates/project/src/debugger/test.rs | 55 - crates/project/src/environment.rs | 421 - crates/project/src/git_store.rs | 6297 - crates/project/src/git_store/branch_diff.rs | 386 - crates/project/src/git_store/conflict_set.rs | 742 - crates/project/src/git_store/git_traversal.rs | 793 - crates/project/src/git_store/pending_op.rs | 147 - crates/project/src/image_store.rs | 987 - crates/project/src/lsp_command.rs | 4643 - .../project/src/lsp_command/signature_help.rs | 793 - crates/project/src/lsp_store.rs | 14274 --- crates/project/src/lsp_store/clangd_ext.rs | 104 - .../project/src/lsp_store/inlay_hint_cache.rs | 233 - .../src/lsp_store/json_language_server_ext.rs | 98 - crates/project/src/lsp_store/log_store.rs | 716 - .../project/src/lsp_store/lsp_ext_command.rs | 806 - .../src/lsp_store/rust_analyzer_ext.rs | 275 - .../src/lsp_store/vue_language_server_ext.rs | 124 - crates/project/src/manifest_tree.rs | 224 - .../src/manifest_tree/manifest_store.rs | 51 - crates/project/src/manifest_tree/path_trie.rs | 265 - .../project/src/manifest_tree/server_tree.rs | 461 - crates/project/src/prettier_store.rs | 965 - crates/project/src/project.rs | 5725 - crates/project/src/project_search.rs | 1002 - crates/project/src/project_settings.rs | 1230 - crates/project/src/project_tests.rs | 10924 -- crates/project/src/search.rs | 732 - crates/project/src/search_history.rs | 250 - crates/project/src/task_inventory.rs | 1610 - crates/project/src/task_store.rs | 501 - crates/project/src/telemetry_snapshot.rs | 127 - crates/project/src/terminals.rs | 598 - crates/project/src/toolchain_store.rs | 777 - crates/project/src/worktree_store.rs | 1212 - crates/project/src/yarn.rs | 179 - crates/project_benchmarks/Cargo.toml | 21 - crates/project_benchmarks/LICENSE-GPL | 1 - crates/project_benchmarks/src/main.rs | 135 - crates/project_panel/Cargo.toml | 57 - crates/project_panel/LICENSE-GPL | 1 - .../benches/linux_repo_snapshot.txt | 96349 ---------------- crates/project_panel/benches/sorting.rs | 83 - crates/project_panel/src/project_panel.rs | 6275 - .../src/project_panel_settings.rs | 124 - .../project_panel/src/project_panel_tests.rs | 8195 -- crates/project_panel/src/utils.rs | 42 - crates/project_symbols/Cargo.toml | 40 - crates/project_symbols/LICENSE-GPL | 1 - crates/project_symbols/src/project_symbols.rs | 463 - crates/prompt_store/Cargo.toml | 33 - crates/prompt_store/LICENSE-GPL | 1 - crates/prompt_store/src/prompt_store.rs | 471 - crates/prompt_store/src/prompts.rs | 476 - crates/proto/Cargo.toml | 29 - crates/proto/LICENSE-GPL | 1 - crates/proto/build.rs | 10 - crates/proto/proto/ai.proto | 220 - crates/proto/proto/app.proto | 76 - crates/proto/proto/buf.yaml | 4 - crates/proto/proto/buffer.proto | 315 - crates/proto/proto/call.proto | 427 - crates/proto/proto/channel.proto | 294 - crates/proto/proto/core.proto | 29 - crates/proto/proto/debugger.proto | 555 - crates/proto/proto/git.proto | 589 - crates/proto/proto/image.proto | 36 - crates/proto/proto/lsp.proto | 971 - crates/proto/proto/notification.proto | 37 - crates/proto/proto/task.proto | 60 - crates/proto/proto/toolchain.proto | 59 - crates/proto/proto/worktree.proto | 160 - crates/proto/proto/zed.proto | 527 - crates/proto/src/error.rs | 250 - crates/proto/src/macros.rs | 100 - crates/proto/src/proto.rs | 923 - crates/proto/src/typed_envelope.rs | 210 - crates/recent_projects/Cargo.toml | 62 - crates/recent_projects/LICENSE-GPL | 1 - crates/recent_projects/src/dev_container.rs | 295 - .../src/dev_container_suggest.rs | 106 - .../src/disconnected_overlay.rs | 205 - crates/recent_projects/src/recent_projects.rs | 1043 - .../recent_projects/src/remote_connections.rs | 880 - crates/recent_projects/src/remote_servers.rs | 2787 - crates/recent_projects/src/ssh_config.rs | 96 - crates/recent_projects/src/wsl_picker.rs | 295 - crates/release_channel/Cargo.toml | 13 - crates/release_channel/LICENSE-GPL | 1 - crates/release_channel/src/lib.rs | 232 - crates/remote/Cargo.toml | 48 - crates/remote/LICENSE-GPL | 1 - crates/remote/src/json_log.rs | 59 - crates/remote/src/protocol.rs | 76 - crates/remote/src/proxy.rs | 25 - crates/remote/src/remote.rs | 15 - crates/remote/src/remote_client.rs | 1714 - crates/remote/src/transport.rs | 428 - crates/remote/src/transport/docker.rs | 757 - crates/remote/src/transport/ssh.rs | 1501 - crates/remote/src/transport/wsl.rs | 607 - crates/remote_server/Cargo.toml | 109 - crates/remote_server/LICENSE-GPL | 1 - crates/remote_server/build.rs | 34 - crates/remote_server/src/headless_project.rs | 893 - crates/remote_server/src/main.rs | 55 - .../remote_server/src/remote_editing_tests.rs | 1981 - crates/remote_server/src/remote_server.rs | 87 - crates/remote_server/src/unix.rs | 1045 - crates/repl/Cargo.toml | 70 - crates/repl/LICENSE-GPL | 1 - crates/repl/src/components.rs | 5 - .../repl/src/components/kernel_list_item.rs | 60 - crates/repl/src/components/kernel_options.rs | 283 - crates/repl/src/jupyter_settings.rs | 28 - crates/repl/src/kernels/mod.rs | 259 - crates/repl/src/kernels/native_kernel.rs | 542 - crates/repl/src/kernels/remote_kernels.rs | 294 - crates/repl/src/notebook.rs | 4 - crates/repl/src/notebook/cell.rs | 746 - crates/repl/src/notebook/notebook_ui.rs | 836 - crates/repl/src/outputs.rs | 617 - crates/repl/src/outputs/image.rs | 98 - crates/repl/src/outputs/markdown.rs | 93 - crates/repl/src/outputs/plain.rs | 351 - crates/repl/src/outputs/table.rs | 290 - crates/repl/src/outputs/user_error.rs | 45 - crates/repl/src/repl.rs | 61 - crates/repl/src/repl_editor.rs | 824 - crates/repl/src/repl_sessions_ui.rs | 282 - crates/repl/src/repl_settings.rs | 27 - crates/repl/src/repl_store.rs | 292 - crates/repl/src/session.rs | 702 - crates/rich_text/Cargo.toml | 29 - crates/rich_text/LICENSE-GPL | 1 - crates/rich_text/src/rich_text.rs | 418 - crates/rope/Cargo.toml | 37 - crates/rope/LICENSE-GPL | 1 - crates/rope/benches/rope_benchmark.rs | 273 - crates/rope/src/chunk.rs | 1141 - crates/rope/src/offset_utf16.rs | 49 - crates/rope/src/point.rs | 146 - crates/rope/src/point_utf16.rs | 119 - crates/rope/src/rope.rs | 2247 - crates/rope/src/unclipped.rs | 51 - crates/rpc/Cargo.toml | 44 - crates/rpc/LICENSE-GPL | 1 - crates/rpc/src/auth.rs | 240 - crates/rpc/src/conn.rs | 102 - crates/rpc/src/extension.rs | 64 - crates/rpc/src/macros.rs | 73 - crates/rpc/src/message_stream.rs | 143 - crates/rpc/src/notification.rs | 99 - crates/rpc/src/peer.rs | 1055 - crates/rpc/src/proto_client.rs | 531 - crates/rpc/src/rpc.rs | 21 - crates/rules_library/Cargo.toml | 34 - crates/rules_library/LICENSE-GPL | 1 - crates/rules_library/src/rules_library.rs | 1431 - crates/schema_generator/Cargo.toml | 18 - crates/schema_generator/LICENSE-GPL | 1 - crates/schema_generator/README.md | 12 - crates/schema_generator/src/main.rs | 36 - crates/search/Cargo.toml | 61 - crates/search/LICENSE-GPL | 1 - crates/search/src/buffer_search.rs | 3118 - crates/search/src/buffer_search/registrar.rs | 176 - crates/search/src/project_search.rs | 4690 - crates/search/src/search.rs | 204 - crates/search/src/search_bar.rs | 91 - crates/search/src/search_status_button.rs | 45 - crates/session/Cargo.toml | 25 - crates/session/LICENSE-GPL | 1 - crates/session/src/session.rs | 127 - crates/settings/Cargo.toml | 50 - crates/settings/LICENSE-GPL | 1 - crates/settings/src/base_keymap_setting.rs | 135 - .../settings/src/editable_setting_control.rs | 30 - crates/settings/src/fallible_options.rs | 112 - crates/settings/src/keymap_file.rs | 1971 - crates/settings/src/merge_from.rs | 176 - crates/settings/src/serde_helper.rs | 135 - crates/settings/src/settings.rs | 144 - crates/settings/src/settings_content.rs | 1053 - crates/settings/src/settings_content/agent.rs | 379 - .../settings/src/settings_content/editor.rs | 983 - .../src/settings_content/extension.rs | 45 - .../settings/src/settings_content/language.rs | 1023 - .../src/settings_content/language_model.rs | 423 - .../settings/src/settings_content/project.rs | 565 - .../settings/src/settings_content/terminal.rs | 517 - crates/settings/src/settings_content/theme.rs | 1269 - .../src/settings_content/workspace.rs | 713 - crates/settings/src/settings_file.rs | 135 - crates/settings/src/settings_store.rs | 2296 - crates/settings/src/vscode_import.rs | 914 - crates/settings_json/Cargo.toml | 35 - crates/settings_json/LICENSE-GPL | 1 - crates/settings_json/src/settings_json.rs | 2635 - crates/settings_macros/Cargo.toml | 23 - crates/settings_macros/LICENSE-GPL | 1 - crates/settings_macros/src/settings_macros.rs | 152 - crates/settings_profile_selector/Cargo.toml | 34 - crates/settings_profile_selector/LICENSE-GPL | 1 - .../src/settings_profile_selector.rs | 612 - crates/settings_ui/Cargo.toml | 58 - crates/settings_ui/LICENSE-GPL | 1 - crates/settings_ui/src/components.rs | 11 - crates/settings_ui/src/components/dropdown.rs | 108 - .../settings_ui/src/components/font_picker.rs | 181 - .../src/components/icon_theme_picker.rs | 189 - .../settings_ui/src/components/input_field.rs | 96 - .../src/components/theme_picker.rs | 179 - crates/settings_ui/src/page_data.rs | 7549 -- crates/settings_ui/src/settings_ui.rs | 4047 - crates/snippet/Cargo.toml | 17 - crates/snippet/LICENSE-GPL | 1 - crates/snippet/src/snippet.rs | 334 - crates/snippet_provider/Cargo.toml | 33 - crates/snippet_provider/LICENSE-GPL | 1 - .../snippet_provider/src/extension_snippet.rs | 26 - crates/snippet_provider/src/format.rs | 78 - crates/snippet_provider/src/lib.rs | 295 - crates/snippet_provider/src/registry.rs | 54 - crates/snippets_ui/Cargo.toml | 25 - crates/snippets_ui/LICENSE-GPL | 1 - crates/snippets_ui/src/snippets_ui.rs | 364 - crates/sqlez/.gitignore | 2 - crates/sqlez/Cargo.toml | 23 - crates/sqlez/LICENSE-GPL | 1 - crates/sqlez/src/bindable.rs | 466 - crates/sqlez/src/connection.rs | 445 - crates/sqlez/src/domain.rs | 64 - crates/sqlez/src/lib.rs | 11 - crates/sqlez/src/migrations.rs | 390 - crates/sqlez/src/savepoint.rs | 150 - crates/sqlez/src/statement.rs | 497 - crates/sqlez/src/thread_safe_connection.rs | 355 - crates/sqlez/src/typed_statements.rs | 96 - crates/sqlez/src/util.rs | 32 - crates/sqlez_macros/Cargo.toml | 19 - crates/sqlez_macros/LICENSE-GPL | 1 - crates/sqlez_macros/src/sqlez_macros.rs | 102 - crates/story/Cargo.toml | 17 - crates/story/LICENSE-GPL | 1 - crates/story/src/story.rs | 209 - crates/storybook/Cargo.toml | 40 - crates/storybook/LICENSE-GPL | 1 - crates/storybook/build.rs | 9 - crates/storybook/docs/thoughts.md | 57 - crates/storybook/src/actions.rs | 2 - crates/storybook/src/app_menus.rs | 10 - crates/storybook/src/assets.rs | 32 - crates/storybook/src/stories.rs | 23 - .../src/stories/auto_height_editor.rs | 36 - crates/storybook/src/stories/cursor.rs | 109 - crates/storybook/src/stories/focus.rs | 123 - crates/storybook/src/stories/indent_guides.rs | 82 - crates/storybook/src/stories/kitchen_sink.rs | 32 - .../storybook/src/stories/overflow_scroll.rs | 41 - crates/storybook/src/stories/picker.rs | 206 - crates/storybook/src/stories/scroll.rs | 52 - crates/storybook/src/stories/text.rs | 120 - .../storybook/src/stories/viewport_units.rs | 32 - crates/storybook/src/stories/with_rem_size.rs | 61 - crates/storybook/src/story_selector.rs | 113 - crates/storybook/src/storybook.rs | 161 - crates/streaming_diff/Cargo.toml | 20 - crates/streaming_diff/LICENSE-GPL | 1 - crates/streaming_diff/src/streaming_diff.rs | 1104 - crates/sum_tree/Cargo.toml | 2 - crates/sum_tree/src/cursor.rs | 2 +- crates/sum_tree/src/sum_tree.rs | 2 +- crates/supermaven/Cargo.toml | 44 - crates/supermaven/LICENSE-GPL | 1 - crates/supermaven/src/messages.rs | 153 - crates/supermaven/src/supermaven.rs | 511 - .../supermaven_edit_prediction_delegate.rs | 302 - crates/supermaven_api/Cargo.toml | 23 - crates/supermaven_api/LICENSE-GPL | 1 - crates/supermaven_api/src/supermaven_api.rs | 291 - crates/svg_preview/Cargo.toml | 20 - crates/svg_preview/LICENSE-GPL | 1 - crates/svg_preview/src/svg_preview.rs | 26 - crates/svg_preview/src/svg_preview_view.rs | 341 - crates/system_specs/Cargo.toml | 28 - crates/system_specs/LICENSE-GPL | 1 - crates/system_specs/src/system_specs.rs | 295 - crates/tab_switcher/Cargo.toml | 39 - crates/tab_switcher/LICENSE-GPL | 1 - crates/tab_switcher/src/tab_switcher.rs | 758 - crates/tab_switcher/src/tab_switcher_tests.rs | 345 - crates/task/Cargo.toml | 41 - crates/task/LICENSE-GPL | 1 - crates/task/src/adapter_schema.rs | 16 - crates/task/src/debug_format.rs | 534 - crates/task/src/serde_helpers.rs | 37 - crates/task/src/static_source.rs | 123 - crates/task/src/task.rs | 431 - crates/task/src/task_template.rs | 974 - crates/task/src/vscode_debug_format.rs | 194 - crates/task/src/vscode_format.rs | 361 - crates/task/test_data/rust-analyzer.json | 67 - crates/task/test_data/typescript.json | 51 - crates/tasks_ui/Cargo.toml | 41 - crates/tasks_ui/LICENSE-GPL | 1 - crates/tasks_ui/src/modal.rs | 1312 - crates/tasks_ui/src/tasks_ui.rs | 611 - crates/telemetry/Cargo.toml | 18 - crates/telemetry/LICENSE-GPL | 1 - crates/telemetry/src/telemetry.rs | 66 - crates/telemetry_events/Cargo.toml | 17 - crates/telemetry_events/LICENSE-GPL | 1 - .../telemetry_events/src/telemetry_events.rs | 126 - crates/terminal/Cargo.toml | 53 - crates/terminal/LICENSE-GPL | 1 - crates/terminal/src/mappings/colors.rs | 11 - crates/terminal/src/mappings/keys.rs | 449 - crates/terminal/src/mappings/mod.rs | 3 - crates/terminal/src/mappings/mouse.rs | 277 - crates/terminal/src/pty_info.rs | 170 - crates/terminal/src/terminal.rs | 2854 - crates/terminal/src/terminal_hyperlinks.rs | 1673 - crates/terminal/src/terminal_settings.rs | 178 - crates/terminal_view/Cargo.toml | 58 - crates/terminal_view/LICENSE-GPL | 1 - crates/terminal_view/README.md | 23 - crates/terminal_view/scripts/print256color.sh | 96 - crates/terminal_view/scripts/truecolor.sh | 19 - crates/terminal_view/src/persistence.rs | 487 - crates/terminal_view/src/terminal_element.rs | 2049 - crates/terminal_view/src/terminal_panel.rs | 1978 - .../src/terminal_path_like_target.rs | 1338 - .../terminal_view/src/terminal_scrollbar.rs | 91 - .../src/terminal_slash_command.rs | 129 - crates/terminal_view/src/terminal_view.rs | 1786 - crates/text/Cargo.toml | 39 - crates/text/LICENSE-GPL | 1 - crates/text/src/anchor.rs | 219 - crates/text/src/locator.rs | 130 - crates/text/src/network.rs | 94 - crates/text/src/operation_queue.rs | 165 - crates/text/src/patch.rs | 608 - crates/text/src/selection.rs | 169 - crates/text/src/subscription.rs | 67 - crates/text/src/tests.rs | 812 - crates/text/src/text.rs | 3564 - crates/text/src/undo_map.rs | 115 - crates/theme/Cargo.toml | 43 - crates/theme/LICENSE-GPL | 1 - crates/theme/src/default_colors.rs | 2504 - crates/theme/src/fallback_themes.rs | 373 - crates/theme/src/font_family_cache.rs | 95 - crates/theme/src/icon_theme.rs | 422 - crates/theme/src/icon_theme_schema.rs | 50 - crates/theme/src/registry.rs | 364 - crates/theme/src/scale.rs | 298 - crates/theme/src/schema.rs | 826 - crates/theme/src/settings.rs | 749 - crates/theme/src/styles.rs | 13 - crates/theme/src/styles/accents.rs | 88 - crates/theme/src/styles/colors.rs | 655 - crates/theme/src/styles/players.rs | 187 - crates/theme/src/styles/status.rs | 191 - crates/theme/src/styles/syntax.rs | 197 - crates/theme/src/styles/system.rs | 22 - crates/theme/src/theme.rs | 527 - crates/theme_extension/Cargo.toml | 19 - crates/theme_extension/LICENSE-GPL | 1 - crates/theme_extension/src/theme_extension.rs | 88 - crates/theme_importer/Cargo.toml | 25 - crates/theme_importer/LICENSE-GPL | 1 - crates/theme_importer/README.md | 5 - crates/theme_importer/src/color.rs | 56 - crates/theme_importer/src/main.rs | 130 - crates/theme_importer/src/vscode.rs | 7 - crates/theme_importer/src/vscode/converter.rs | 276 - crates/theme_importer/src/vscode/syntax.rs | 310 - crates/theme_importer/src/vscode/theme.rs | 40 - crates/theme_selector/Cargo.toml | 30 - crates/theme_selector/LICENSE-GPL | 1 - .../theme_selector/src/icon_theme_selector.rs | 338 - crates/theme_selector/src/theme_selector.rs | 524 - crates/time_format/Cargo.toml | 21 - crates/time_format/LICENSE-GPL | 1 - crates/time_format/src/time_format.rs | 973 - crates/title_bar/Cargo.toml | 71 - crates/title_bar/LICENSE-GPL | 1 - crates/title_bar/src/application_menu.rs | 324 - crates/title_bar/src/collab.rs | 627 - crates/title_bar/src/onboarding_banner.rs | 170 - crates/title_bar/src/platform_title_bar.rs | 192 - crates/title_bar/src/platforms.rs | 3 - .../title_bar/src/platforms/platform_linux.rs | 208 - .../title_bar/src/platforms/platform_mac.rs | 6 - .../src/platforms/platform_windows.rs | 143 - crates/title_bar/src/stories.rs | 3 - .../title_bar/src/stories/application_menu.rs | 29 - crates/title_bar/src/system_window_tabs.rs | 529 - crates/title_bar/src/title_bar.rs | 777 - crates/title_bar/src/title_bar_settings.rs | 27 - crates/toolchain_selector/Cargo.toml | 29 - crates/toolchain_selector/LICENSE-GPL | 1 - .../src/active_toolchain.rs | 264 - .../src/toolchain_selector.rs | 1142 - crates/ui/Cargo.toml | 42 - crates/ui/LICENSE-GPL | 1 - crates/ui/src/component_prelude.rs | 6 - crates/ui/src/components.rs | 89 - crates/ui/src/components/avatar.rs | 303 - crates/ui/src/components/banner.rs | 182 - crates/ui/src/components/button.rs | 12 - crates/ui/src/components/button/button.rs | 612 - .../ui/src/components/button/button_icon.rs | 199 - .../ui/src/components/button/button_like.rs | 849 - .../ui/src/components/button/icon_button.rs | 388 - .../ui/src/components/button/split_button.rs | 97 - .../ui/src/components/button/toggle_button.rs | 769 - crates/ui/src/components/callout.rs | 327 - crates/ui/src/components/chip.rs | 106 - crates/ui/src/components/content_group.rs | 137 - crates/ui/src/components/context_menu.rs | 1392 - crates/ui/src/components/data_table.rs | 1390 - crates/ui/src/components/diff_stat.rs | 85 - crates/ui/src/components/disclosure.rs | 152 - crates/ui/src/components/divider.rs | 248 - crates/ui/src/components/dropdown_menu.rs | 304 - crates/ui/src/components/facepile.rs | 135 - crates/ui/src/components/group.rs | 57 - crates/ui/src/components/icon.rs | 371 - .../ui/src/components/icon/decorated_icon.rs | 106 - .../ui/src/components/icon/icon_decoration.rs | 159 - crates/ui/src/components/image.rs | 184 - crates/ui/src/components/indent_guides.rs | 596 - crates/ui/src/components/indicator.rs | 177 - crates/ui/src/components/keybinding.rs | 740 - crates/ui/src/components/keybinding_hint.rs | 341 - crates/ui/src/components/label.rs | 11 - .../src/components/label/highlighted_label.rs | 242 - crates/ui/src/components/label/label.rs | 266 - crates/ui/src/components/label/label_like.rs | 315 - .../ui/src/components/label/loading_label.rs | 112 - .../ui/src/components/label/spinner_label.rs | 205 - crates/ui/src/components/list.rs | 13 - crates/ui/src/components/list/list.rs | 142 - .../src/components/list/list_bullet_item.rs | 83 - crates/ui/src/components/list/list_header.rs | 218 - crates/ui/src/components/list/list_item.rs | 470 - .../ui/src/components/list/list_separator.rs | 14 - .../ui/src/components/list/list_sub_header.rs | 149 - crates/ui/src/components/modal.rs | 462 - crates/ui/src/components/navigable.rs | 102 - crates/ui/src/components/notification.rs | 3 - .../components/notification/alert_modal.rs | 113 - crates/ui/src/components/popover.rs | 94 - crates/ui/src/components/popover_menu.rs | 488 - crates/ui/src/components/progress.rs | 2 - .../src/components/progress/progress_bar.rs | 156 - crates/ui/src/components/radio.rs | 60 - crates/ui/src/components/right_click_menu.rs | 287 - crates/ui/src/components/scrollbar.rs | 1466 - .../ui/src/components/settings_container.rs | 89 - crates/ui/src/components/settings_group.rs | 110 - crates/ui/src/components/stack.rs | 15 - crates/ui/src/components/sticky_items.rs | 335 - crates/ui/src/components/stories.rs | 3 - .../ui/src/components/stories/context_menu.rs | 81 - crates/ui/src/components/tab.rs | 238 - crates/ui/src/components/tab_bar.rs | 207 - crates/ui/src/components/thread_item.rs | 260 - crates/ui/src/components/toggle.rs | 1063 - crates/ui/src/components/tooltip.rs | 297 - crates/ui/src/components/tree_view_item.rs | 294 - crates/ui/src/prelude.rs | 35 - crates/ui/src/styles.rs | 18 - crates/ui/src/styles/animation.rs | 290 - crates/ui/src/styles/appearance.rs | 19 - crates/ui/src/styles/color.rs | 244 - crates/ui/src/styles/elevation.rs | 129 - crates/ui/src/styles/platform.rs | 25 - crates/ui/src/styles/severity.rs | 10 - crates/ui/src/styles/spacing.rs | 55 - crates/ui/src/styles/typography.rs | 295 - crates/ui/src/styles/units.rs | 29 - crates/ui/src/traits.rs | 8 - crates/ui/src/traits/animation_ext.rs | 42 - crates/ui/src/traits/clickable.rs | 9 - crates/ui/src/traits/disableable.rs | 5 - crates/ui/src/traits/fixed.rs | 10 - crates/ui/src/traits/styled_ext.rs | 134 - crates/ui/src/traits/toggleable.rs | 69 - crates/ui/src/traits/transformable.rs | 7 - crates/ui/src/traits/visible_on_hover.rs | 17 - crates/ui/src/ui.rs | 20 - crates/ui/src/utils.rs | 23 - crates/ui/src/utils/apca_contrast.rs | 479 - crates/ui/src/utils/color_contrast.rs | 70 - crates/ui/src/utils/corner_solver.rs | 61 - crates/ui/src/utils/format_distance.rs | 396 - crates/ui/src/utils/search_input.rs | 20 - crates/ui/src/utils/with_rem_size.rs | 114 - crates/ui_input/Cargo.toml | 24 - crates/ui_input/LICENSE-GPL | 1 - crates/ui_input/src/input_field.rs | 259 - crates/ui_input/src/number_field.rs | 577 - crates/ui_input/src/ui_input.rs | 9 - crates/ui_macros/Cargo.toml | 21 - crates/ui_macros/LICENSE-GPL | 1 - .../src/derive_register_component.rs | 29 - crates/ui_macros/src/dynamic_spacing.rs | 167 - crates/ui_macros/src/ui_macros.rs | 37 - crates/ui_prompt/Cargo.toml | 24 - crates/ui_prompt/LICENSE-GPL | 1 - crates/ui_prompt/src/ui_prompt.rs | 205 - crates/vercel/Cargo.toml | 22 - crates/vercel/LICENSE-GPL | 1 - crates/vercel/src/vercel.rs | 78 - crates/vim/Cargo.toml | 75 - crates/vim/LICENSE-GPL | 1 - crates/vim/README.md | 36 - crates/vim/src/change_list.rs | 220 - crates/vim/src/command.rs | 3211 - crates/vim/src/digraph.rs | 365 - crates/vim/src/digraph/default.rs | 1366 - crates/vim/src/helix.rs | 1669 - crates/vim/src/helix/boundary.rs | 739 - crates/vim/src/helix/duplicate.rs | 234 - crates/vim/src/helix/object.rs | 182 - crates/vim/src/helix/paste.rs | 455 - crates/vim/src/helix/select.rs | 84 - crates/vim/src/indent.rs | 247 - crates/vim/src/insert.rs | 205 - crates/vim/src/mode_indicator.rs | 180 - crates/vim/src/motion.rs | 4484 - crates/vim/src/normal.rs | 2320 - crates/vim/src/normal/change.rs | 706 - crates/vim/src/normal/convert.rs | 469 - crates/vim/src/normal/delete.rs | 763 - crates/vim/src/normal/increment.rs | 849 - crates/vim/src/normal/mark.rs | 397 - crates/vim/src/normal/paste.rs | 1086 - crates/vim/src/normal/repeat.rs | 911 - crates/vim/src/normal/scroll.rs | 568 - crates/vim/src/normal/search.rs | 1176 - crates/vim/src/normal/substitute.rs | 312 - crates/vim/src/normal/toggle_comments.rs | 74 - crates/vim/src/normal/yank.rs | 246 - crates/vim/src/object.rs | 3414 - crates/vim/src/replace.rs | 559 - crates/vim/src/rewrap.rs | 148 - crates/vim/src/state.rs | 1835 - crates/vim/src/surrounds.rs | 1705 - crates/vim/src/test.rs | 2503 - .../src/test/neovim_backed_test_context.rs | 427 - crates/vim/src/test/neovim_connection.rs | 624 - crates/vim/src/test/vim_test_context.rs | 289 - crates/vim/src/vim.rs | 2022 - crates/vim/src/visual.rs | 1978 - .../neovim_backed_test_context_works.json | 3 - crates/vim/test_data/test_a.json | 6 - .../test_around_containing_word_indent.json | 23 - crates/vim/test_data/test_b.json | 54 - crates/vim/test_data/test_backspace.json | 9 - .../test_backspace_non_ascii_bol.json | 4 - crates/vim/test_data/test_backwards_n.json | 8 - .../test_data/test_blackhole_register.json | 11 - crates/vim/test_data/test_builtin_marks.json | 36 - .../test_capital_f_and_capital_t.json | 570 - crates/vim/test_data/test_caret_mark.json | 35 - crates/vim/test_data/test_cc.json | 24 - crates/vim/test_data/test_cgn_nomatch.json | 28 - crates/vim/test_data/test_cgn_repeat.json | 14 - crates/vim/test_data/test_change_0.json | 8 - crates/vim/test_data/test_change_b.json | 24 - .../vim/test_data/test_change_backspace.json | 16 - crates/vim/test_data/test_change_case.json | 23 - .../test_data/test_change_case_motion.json | 27 - .../test_change_case_motion_object.json | 6 - crates/vim/test_data/test_change_cc.json | 12 - crates/vim/test_data/test_change_e.json | 24 - .../test_change_end_of_document.json | 16 - .../test_data/test_change_end_of_line.json | 8 - crates/vim/test_data/test_change_gg.json | 20 - crates/vim/test_data/test_change_h.json | 16 - crates/vim/test_data/test_change_j.json | 20 - crates/vim/test_data/test_change_k.json | 20 - crates/vim/test_data/test_change_l.json | 8 - .../test_data/test_change_list_delete.json | 16 - .../test_data/test_change_list_insert.json | 32 - .../vim/test_data/test_change_paragraph.json | 8 - .../test_change_paragraph_object.json | 430 - ...hange_paragraph_object_with_soft_wrap.json | 72 - .../test_data/test_change_rot13_motion.json | 23 - .../test_data/test_change_rot13_object.json | 6 - .../test_change_sentence_object.json | 270 - ..._change_surrounding_character_objects.json | 520 - crates/vim/test_data/test_change_w.json | 36 - .../test_data/test_change_word_object.json | 460 - crates/vim/test_data/test_clear_counts.json | 7 - .../vim/test_data/test_comma_semicolon.json | 34 - crates/vim/test_data/test_comma_w.json | 15 - crates/vim/test_data/test_command_basics.json | 6 - crates/vim/test_data/test_command_goto.json | 5 - .../test_command_matching_lines.json | 19 - crates/vim/test_data/test_command_ranges.json | 28 - .../vim/test_data/test_command_replace.json | 33 - crates/vim/test_data/test_command_search.json | 11 - .../test_command_visual_replace.json | 12 - .../test_data/test_convert_to_lower_case.json | 12 - .../vim/test_data/test_convert_to_rot13.json | 15 - .../test_data/test_convert_to_upper_case.json | 12 - crates/vim/test_data/test_ctrl_d_u.json | 28 - crates/vim/test_data/test_ctrl_f_b.json | 24 - crates/vim/test_data/test_ctrl_o_dot.json | 11 - .../vim/test_data/test_ctrl_o_position.json | 10 - crates/vim/test_data/test_ctrl_o_visual.json | 14 - crates/vim/test_data/test_ctrl_v.json | 24 - crates/vim/test_data/test_ctrl_v_control.json | 11 - crates/vim/test_data/test_ctrl_v_escape.json | 10 - .../vim/test_data/test_ctrl_w_override.json | 4 - crates/vim/test_data/test_ctrl_y_e.json | 35 - crates/vim/test_data/test_d_search.json | 7 - crates/vim/test_data/test_dd.json | 24 - ...d_then_paste_without_trailing_newline.json | 7 - crates/vim/test_data/test_del_marks.json | 11 - crates/vim/test_data/test_delete_0.json | 8 - crates/vim/test_data/test_delete_b.json | 24 - .../test_delete_end_of_document.json | 16 - .../test_data/test_delete_end_of_line.json | 8 - .../test_delete_end_of_paragraph.json | 8 - crates/vim/test_data/test_delete_gg.json | 20 - crates/vim/test_data/test_delete_h.json | 16 - crates/vim/test_data/test_delete_j.json | 16 - crates/vim/test_data/test_delete_k.json | 16 - ..._delete_key_can_remove_last_character.json | 3 - crates/vim/test_data/test_delete_l.json | 16 - crates/vim/test_data/test_delete_left.json | 15 - .../test_data/test_delete_next_word_end.json | 20 - .../vim/test_data/test_delete_paragraph.json | 14 - .../test_delete_paragraph_motion.json | 18 - .../test_delete_paragraph_object.json | 430 - ...elete_paragraph_object_with_soft_wrap.json | 72 - .../test_delete_paragraph_whitespace.json | 5 - .../vim/test_data/test_delete_sentence.json | 8 - .../test_delete_sentence_object.json | 270 - ..._delete_surrounding_character_objects.json | 518 - .../test_delete_to_adjacent_character.json | 10 - .../test_data/test_delete_to_end_of_line.json | 6 - crates/vim/test_data/test_delete_to_line.json | 15 - .../test_delete_unmatched_brace.json | 12 - crates/vim/test_data/test_delete_w.json | 28 - .../test_data/test_delete_with_counts.json | 16 - .../test_data/test_delete_word_object.json | 460 - crates/vim/test_data/test_dgn_repeat.json | 14 - crates/vim/test_data/test_digraph_find.json | 12 - .../test_data/test_digraph_insert_mode.json | 21 - .../test_digraph_insert_multicursor.json | 0 .../test_digraph_keymap_conflict.json | 7 - .../vim/test_data/test_digraph_replace.json | 6 - .../test_data/test_digraph_replace_mode.json | 19 - crates/vim/test_data/test_dot_mark.json | 8 - crates/vim/test_data/test_dot_repeat.json | 38 - crates/vim/test_data/test_dw_eol.json | 6 - .../vim/test_data/test_end_of_document.json | 15 - .../test_data/test_end_of_line_downward.json | 12 - .../test_end_of_line_with_neovim.json | 9 - crates/vim/test_data/test_end_of_word.json | 32 - crates/vim/test_data/test_enter.json | 11 - .../test_enter_visual_line_mode.json | 15 - .../vim/test_data/test_enter_visual_mode.json | 20 - .../test_data/test_escape_while_waiting.json | 6 - crates/vim/test_data/test_f_and_t.json | 557 - crates/vim/test_data/test_find_multibyte.json | 7 - crates/vim/test_data/test_folds.json | 24 - crates/vim/test_data/test_folds_panic.json | 23 - ...t_forced_motion_delete_to_end_of_line.json | 10 - ...orced_motion_delete_to_middle_of_line.json | 34 - ...forced_motion_delete_to_start_of_line.json | 15 - .../test_data/test_forced_motion_yank.json | 24 - crates/vim/test_data/test_gg.json | 21 - crates/vim/test_data/test_gi.json | 10 - crates/vim/test_data/test_gn.json | 39 - .../vim/test_data/test_go_to_percentage.json | 26 - crates/vim/test_data/test_gq.json | 12 - crates/vim/test_data/test_gv.json | 24 - crates/vim/test_data/test_h.json | 9 - .../vim/test_data/test_h_through_unicode.json | 12 - .../vim/test_data/test_horizontal_scroll.json | 16 - .../test_inclusive_to_exclusive_delete.json | 15 - crates/vim/test_data/test_increment.json | 16 - ...st_increment_bin_wrapping_and_padding.json | 10 - .../test_data/test_increment_hex_casing.json | 5 - ...st_increment_hex_wrapping_and_padding.json | 10 - .../vim/test_data/test_increment_inline.json | 10 - .../vim/test_data/test_increment_radix.json | 18 - .../test_data/test_increment_sign_change.json | 6 - ...rement_sign_change_with_leading_zeros.json | 8 - .../vim/test_data/test_increment_steps.json | 25 - .../test_increment_visual_partial_number.json | 20 - ...increment_with_changing_leading_zeros.json | 6 - .../test_data/test_increment_with_dot.json | 5 - .../test_increment_with_leading_zeros.json | 6 - ...increment_with_leading_zeros_and_zero.json | 7 - .../test_increment_with_two_dots.json | 5 - .../test_data/test_increment_wrapping.json | 13 - .../test_increment_zero_leading_zeros.json | 4 - crates/vim/test_data/test_indent_gv.json | 8 - crates/vim/test_data/test_insert_ctrl_r.json | 12 - crates/vim/test_data/test_insert_ctrl_y.json | 5 - .../vim/test_data/test_insert_empty_line.json | 78 - .../test_data/test_insert_end_of_line.json | 9 - .../test_insert_first_non_whitespace.json | 15 - .../vim/test_data/test_insert_line_above.json | 18 - .../test_data/test_insert_with_counts.json | 36 - .../test_data/test_insert_with_repeat.json | 23 - crates/vim/test_data/test_j.json | 15 - crates/vim/test_data/test_jk.json | 8 - crates/vim/test_data/test_jk_max_count.json | 47 - crates/vim/test_data/test_join_lines.json | 29 - crates/vim/test_data/test_jump_list.json | 14 - crates/vim/test_data/test_jump_to_end.json | 14 - .../test_jump_to_first_non_whitespace.json | 18 - .../test_jump_to_line_boundaries.json | 28 - crates/vim/test_data/test_k.json | 15 - crates/vim/test_data/test_l.json | 15 - .../vim/test_data/test_lowercase_marks.json | 15 - crates/vim/test_data/test_lt_gt_marks.json | 29 - crates/vim/test_data/test_marks.json | 15 - crates/vim/test_data/test_matching.json | 17 - .../test_matching_braces_in_tag.json | 3 - .../test_matching_nested_brackets.json | 5 - crates/vim/test_data/test_matching_tags.json | 20 - .../test_minibrackets_trailing_space.json | 11 - .../vim/test_data/test_named_registers.json | 28 - crates/vim/test_data/test_neovim.json | 16 - .../vim/test_data/test_next_line_start.json | 3 - .../test_next_word_end_newline_last_char.json | 3 - crates/vim/test_data/test_normal_command.json | 64 - .../test_data/test_numbered_registers.json | 45 - crates/vim/test_data/test_o.json | 18 - crates/vim/test_data/test_o_comment.json | 8 - crates/vim/test_data/test_offsets.json | 21 - crates/vim/test_data/test_p_g_v_y.json | 11 - .../test_paragraph_multi_delete.json | 18 - ...ng_positions_not_at_beginning_of_line.json | 0 .../test_data/test_paragraphs_dont_wrap.json | 8 - crates/vim/test_data/test_paste.json | 34 - crates/vim/test_data/test_paste_count.json | 13 - crates/vim/test_data/test_paste_visual.json | 51 - .../test_data/test_paste_visual_block.json | 33 - crates/vim/test_data/test_percent.json | 58 - crates/vim/test_data/test_period_mark.json | 14 - crates/vim/test_data/test_plus_minus.json | 7 - .../vim/test_data/test_previous_word_end.json | 33 - crates/vim/test_data/test_quote_mark.json | 23 - crates/vim/test_data/test_r.json | 40 - crates/vim/test_data/test_record_replay.json | 14 - .../test_data/test_record_replay_count.json | 16 - .../vim/test_data/test_record_replay_dot.json | 17 - .../test_record_replay_interleaved.json | 35 - .../test_data/test_record_replay_of_dot.json | 14 - .../test_record_replay_recursion.json | 6 - .../test_remap_adjacent_dog_cat.json | 24 - .../test_remap_nested_pineapple.json | 28 - .../vim/test_data/test_remap_recursion.json | 11 - .../test_data/test_repeat_clear_count.json | 21 - .../test_data/test_repeat_clear_repeat.json | 8 - .../test_data/test_repeat_grouping_41735.json | 10 - .../test_data/test_repeat_motion_counts.json | 13 - .../vim/test_data/test_repeat_over_blur.json | 11 - crates/vim/test_data/test_repeat_visual.json | 51 - crates/vim/test_data/test_repeated_cb.json | 275 - crates/vim/test_data/test_repeated_ce.json | 275 - crates/vim/test_data/test_repeated_cj.json | 275 - crates/vim/test_data/test_repeated_cl.json | 275 - crates/vim/test_data/test_repeated_word.json | 214 - crates/vim/test_data/test_replace_g.json | 23 - crates/vim/test_data/test_replace_mode.json | 25 - .../test_data/test_replace_mode_repeat.json | 10 - .../vim/test_data/test_replace_mode_undo.json | 93 - .../test_replace_mode_with_counts.json | 14 - crates/vim/test_data/test_replace_n.json | 13 - .../test_data/test_replace_with_range.json | 16 - .../test_replace_with_range_at_start.json | 16 - .../test_scroll_beyond_last_line.json | 12 - crates/vim/test_data/test_scroll_jumps.json | 12 - .../vim/test_data/test_search_skipping.json | 12 - crates/vim/test_data/test_selection_goal.json | 8 - .../test_data/test_sentence_backwards.json | 32 - .../vim/test_data/test_sentence_forwards.json | 8 - crates/vim/test_data/test_shift_y.json | 7 - ...gleline_surrounding_character_objects.json | 37 - ...ounding_character_objects_with_escape.json | 10 - .../vim/test_data/test_space_non_ascii.json | 4 - .../test_data/test_space_non_ascii_eol.json | 4 - .../test_data/test_space_only_ascii_eol.json | 4 - .../vim/test_data/test_special_registers.json | 48 - .../test_start_end_of_paragraph.json | 13 - .../vim/test_data/test_substitute_line.json | 29 - crates/vim/test_data/test_temporary_mode.json | 27 - crates/vim/test_data/test_undo.json | 45 - crates/vim/test_data/test_undo_last_line.json | 14 - .../test_undo_last_line_newline.json | 15 - ...t_undo_last_line_newline_many_changes.json | 21 - .../test_data/test_undo_repeated_insert.json | 8 - .../test_data/test_unmatched_backward.json | 24 - .../test_unmatched_backward_markdown.json | 9 - .../vim/test_data/test_unmatched_forward.json | 28 - .../test_unmatched_forward_markdown.json | 9 - crates/vim/test_data/test_v2ap.json | 6 - crates/vim/test_data/test_v_search.json | 28 - crates/vim/test_data/test_v_search_aa.json | 7 - .../test_data/test_visual_block_insert.json | 18 - .../test_visual_block_issue_2123.json | 5 - .../vim/test_data/test_visual_block_mode.json | 38 - .../test_visual_block_mode_down_right.json | 9 - .../test_visual_block_mode_other_end.json | 11 - ...est_visual_block_mode_shift_other_end.json | 11 - .../test_visual_block_mode_up_left.json | 9 - .../test_data/test_visual_block_search.json | 7 - .../test_visual_block_wrapping_selection.json | 16 - crates/vim/test_data/test_visual_change.json | 47 - crates/vim/test_data/test_visual_delete.json | 48 - .../test_data/test_visual_line_change.json | 35 - .../test_data/test_visual_line_delete.json | 23 - .../vim/test_data/test_visual_match_eol.json | 5 - .../test_visual_mode_insert_before_after.json | 14 - crates/vim/test_data/test_visual_object.json | 19 - .../test_data/test_visual_object_expands.json | 10 - .../test_visual_paragraph_object.json | 80 - ...isual_paragraph_object_with_soft_wrap.json | 72 - .../test_visual_sentence_object.json | 0 crates/vim/test_data/test_visual_shift_d.json | 10 - .../vim/test_data/test_visual_star_hash.json | 6 - .../test_data/test_visual_word_object.json | 236 - crates/vim/test_data/test_visual_yank.json | 42 - crates/vim/test_data/test_w.json | 40 - crates/vim/test_data/test_window_bottom.json | 19 - crates/vim/test_data/test_window_middle.json | 17 - crates/vim/test_data/test_window_top.json | 13 - .../test_wrapped_delete_end_document.json | 10 - crates/vim/test_data/test_wrapped_lines.json | 61 - .../vim/test_data/test_wrapped_motions.json | 15 - crates/vim/test_data/test_x.json | 12 - .../test_yank_line_with_trailing_newline.json | 5 - ...st_yank_line_without_trailing_newline.json | 5 - ...nk_multiline_without_trailing_newline.json | 6 - .../test_yank_paragraph_with_paste.json | 10 - crates/vim/test_data/test_zero.json | 7 - crates/vim_mode_setting/Cargo.toml | 15 - crates/vim_mode_setting/LICENSE-GPL | 1 - .../vim_mode_setting/src/vim_mode_setting.rs | 25 - crates/watch/Cargo.toml | 3 +- crates/watch/src/watch.rs | 12 +- crates/web_search/Cargo.toml | 19 - crates/web_search/LICENSE-GPL | 1 - crates/web_search/src/web_search.rs | 72 - crates/web_search_providers/Cargo.toml | 24 - crates/web_search_providers/LICENSE-GPL | 1 - crates/web_search_providers/src/cloud.rs | 120 - .../src/web_search_providers.rs | 56 - crates/workspace/Cargo.toml | 83 - crates/workspace/LICENSE-GPL | 1 - crates/workspace/src/dock.rs | 1095 - crates/workspace/src/history_manager.rs | 136 - crates/workspace/src/invalid_item_view.rs | 114 - crates/workspace/src/item.rs | 1694 - crates/workspace/src/modal_layer.rs | 208 - crates/workspace/src/notifications.rs | 1115 - crates/workspace/src/pane.rs | 7010 -- crates/workspace/src/pane_group.rs | 1456 - crates/workspace/src/path_list.rs | 196 - crates/workspace/src/persistence.rs | 3051 - crates/workspace/src/persistence/model.rs | 399 - crates/workspace/src/searchable.rs | 461 - crates/workspace/src/shared_screen.rs | 136 - crates/workspace/src/status_bar.rs | 223 - crates/workspace/src/tasks.rs | 134 - crates/workspace/src/theme_preview.rs | 429 - crates/workspace/src/toast_layer.rs | 252 - crates/workspace/src/toolbar.rs | 282 - crates/workspace/src/workspace.rs | 11487 -- crates/workspace/src/workspace_settings.rs | 143 - crates/worktree/Cargo.toml | 63 - crates/worktree/LICENSE-GPL | 1 - crates/worktree/src/ignore.rs | 102 - crates/worktree/src/worktree.rs | 5677 - crates/worktree/src/worktree_settings.rs | 95 - crates/worktree/src/worktree_tests.rs | 2466 - crates/worktree_benchmarks/Cargo.toml | 14 - crates/worktree_benchmarks/LICENSE-GPL | 1 - crates/worktree_benchmarks/src/main.rs | 53 - crates/x_ai/Cargo.toml | 22 - crates/x_ai/LICENSE-GPL | 1 - crates/x_ai/src/x_ai.rs | 208 - crates/zed/Cargo.toml | 231 - crates/zed/LICENSE-GPL | 1 - crates/zed/RELEASE_CHANNEL | 1 - crates/zed/build.rs | 105 - .../contents/dev/embedded.provisionprofile | Bin 12512 -> 0 bytes .../nightly/embedded.provisionprofile | Bin 12512 -> 0 bytes .../preview/embedded.provisionprofile | Bin 12478 -> 0 bytes .../contents/stable/embedded.provisionprofile | Bin 12441 -> 0 bytes crates/zed/resources/app-icon-dev.png | Bin 194029 -> 0 bytes crates/zed/resources/app-icon-dev@2x.png | Bin 857855 -> 0 bytes crates/zed/resources/app-icon-nightly.png | Bin 242801 -> 0 bytes crates/zed/resources/app-icon-nightly@2x.png | Bin 923937 -> 0 bytes crates/zed/resources/app-icon-preview.png | Bin 191163 -> 0 bytes crates/zed/resources/app-icon-preview@2x.png | Bin 679362 -> 0 bytes crates/zed/resources/app-icon.png | Bin 168173 -> 0 bytes crates/zed/resources/app-icon@2x.png | Bin 452124 -> 0 bytes .../resources/flatpak/manifest-template.json | 60 - crates/zed/resources/flatpak/release-info/dev | 0 .../resources/flatpak/release-info/nightly | 0 .../resources/flatpak/release-info/preview | 0 .../zed/resources/flatpak/release-info/stable | 0 .../zed/resources/flatpak/zed.metainfo.xml.in | 85 - crates/zed/resources/info/DocumentTypes.plist | 63 - crates/zed/resources/info/Permissions.plist | 24 - .../resources/info/SupportedPlatforms.plist | 4 - crates/zed/resources/snap/snapcraft.yaml.in | 59 - crates/zed/resources/windows/app-icon-dev.ico | Bin 158013 -> 0 bytes .../resources/windows/app-icon-nightly.ico | Bin 193385 -> 0 bytes .../resources/windows/app-icon-preview.ico | Bin 162443 -> 0 bytes crates/zed/resources/windows/app-icon.ico | Bin 165478 -> 0 bytes .../resources/windows/bin/x64/OpenConsole.exe | Bin 1145344 -> 0 bytes .../zed/resources/windows/bin/x64/conpty.dll | Bin 98304 -> 0 bytes .../windows/messages/Default.zh-cn.isl | 403 - crates/zed/resources/windows/messages/en.isl | 15 - .../zed/resources/windows/messages/zh-cn.isl | 9 - crates/zed/resources/windows/sign.ps1 | 53 - crates/zed/resources/windows/zed.iss | 1421 - crates/zed/resources/windows/zed.sh | 25 - crates/zed/resources/zed.desktop.in | 21 - crates/zed/resources/zed.entitlements | 30 - crates/zed/src/main.rs | 1532 - crates/zed/src/reliability.rs | 337 - crates/zed/src/zed-main.rs | 8 - crates/zed/src/zed.rs | 5163 - crates/zed/src/zed/app_menus.rs | 331 - crates/zed/src/zed/component_preview.rs | 1001 - .../src/zed/component_preview/persistence.rs | 59 - .../zed/src/zed/edit_prediction_registry.rs | 256 - crates/zed/src/zed/mac_only_instance.rs | 151 - crates/zed/src/zed/migrate.rs | 323 - crates/zed/src/zed/open_listener.rs | 932 - crates/zed/src/zed/quick_action_bar.rs | 730 - .../zed/src/zed/quick_action_bar/preview.rs | 96 - .../zed/src/zed/quick_action_bar/repl_menu.rs | 474 - crates/zed/src/zed/windows_only_instance.rs | 217 - crates/zed_actions/Cargo.toml | 15 - crates/zed_actions/LICENSE-GPL | 1 - crates/zed_actions/src/lib.rs | 597 - crates/zed_env_vars/Cargo.toml | 18 - crates/zed_env_vars/LICENSE-GPL | 1 - crates/zed_env_vars/src/zed_env_vars.rs | 44 - crates/zeta_prompt/Cargo.toml | 15 - crates/zeta_prompt/LICENSE-GPL | 1 - crates/zeta_prompt/src/zeta_prompt.rs | 165 - crates/zlog/Cargo.toml | 24 - crates/zlog/LICENSE-GPL | 1 - crates/zlog/README.md | 15 - crates/zlog/src/env_config.rs | 122 - crates/zlog/src/filter.rs | 843 - crates/zlog/src/sink.rs | 324 - crates/zlog/src/zlog.rs | 409 - crates/zlog_settings/Cargo.toml | 21 - crates/zlog_settings/LICENSE-GPL | 1 - crates/zlog_settings/src/zlog_settings.rs | 30 - crates/ztracing/Cargo.toml | 21 - crates/ztracing/LICENSE-AGPL | 1 - crates/ztracing/LICENSE-APACHE | 1 - crates/ztracing/LICENSE-GPL | 1 - crates/ztracing/build.rs | 9 - crates/ztracing/src/lib.rs | 52 - crates/ztracing_macro/Cargo.toml | 11 - crates/ztracing_macro/LICENSE-AGPL | 1 - crates/ztracing_macro/LICENSE-APACHE | 1 - crates/ztracing_macro/LICENSE-GPL | 1 - crates/ztracing_macro/src/lib.rs | 7 - docker-compose.sql | 2 - extensions/.gitignore | 1 - extensions/EXTRACTION.md | 181 - extensions/README.md | 63 - extensions/glsl/Cargo.toml | 16 - extensions/glsl/LICENSE-APACHE | 1 - extensions/glsl/extension.toml | 15 - extensions/glsl/languages/glsl/config.toml | 20 - extensions/glsl/languages/glsl/highlights.scm | 117 - extensions/glsl/src/glsl.rs | 131 - extensions/html/Cargo.toml | 16 - extensions/html/LICENSE-APACHE | 1 - extensions/html/extension.toml | 19 - extensions/html/languages/html/brackets.scm | 5 - extensions/html/languages/html/config.toml | 19 - extensions/html/languages/html/highlights.scm | 19 - extensions/html/languages/html/indents.scm | 6 - extensions/html/languages/html/injections.scm | 21 - extensions/html/languages/html/outline.scm | 5 - extensions/html/languages/html/overrides.scm | 7 - extensions/html/src/html.rs | 115 - extensions/proto/Cargo.toml | 16 - extensions/proto/LICENSE-APACHE | 1 - extensions/proto/extension.toml | 15 - extensions/proto/languages/proto/config.toml | 13 - .../proto/languages/proto/highlights.scm | 61 - extensions/proto/languages/proto/indents.scm | 3 - extensions/proto/languages/proto/outline.scm | 19 - .../proto/languages/proto/textobjects.scm | 18 - extensions/proto/src/proto.rs | 86 - extensions/slash-commands-example/Cargo.toml | 16 - .../slash-commands-example/LICENSE-APACHE | 1 - extensions/slash-commands-example/README.md | 84 - .../slash-commands-example/extension.toml | 15 - .../src/slash_commands_example.rs | 90 - extensions/test-extension/Cargo.toml | 16 - extensions/test-extension/LICENSE-APACHE | 1 - extensions/test-extension/README.md | 5 - extensions/test-extension/extension.toml | 25 - .../languages/gleam/config.toml | 12 - .../languages/gleam/highlights.scm | 130 - .../languages/gleam/indents.scm | 3 - .../languages/gleam/outline.scm | 31 - .../test-extension/src/test_extension.rs | 209 - extensions/workflows/bump_version.yml | 52 - extensions/workflows/release_version.yml | 13 - extensions/workflows/run_tests.yml | 16 - livekit.yaml | 10 - tooling/xtask/Cargo.toml | 23 - tooling/xtask/LICENSE-GPL | 1 - tooling/xtask/src/main.rs | 38 - tooling/xtask/src/tasks.rs | 5 - tooling/xtask/src/tasks/clippy.rs | 64 - tooling/xtask/src/tasks/licenses.rs | 45 - tooling/xtask/src/tasks/package_conformity.rs | 75 - tooling/xtask/src/tasks/publish_gpui.rs | 442 - tooling/xtask/src/tasks/workflows.rs | 138 - .../src/tasks/workflows/after_release.rs | 164 - .../xtask/src/tasks/workflows/cherry_pick.rs | 66 - .../xtask/src/tasks/workflows/compare_perf.rs | 67 - tooling/xtask/src/tasks/workflows/danger.rs | 57 - .../src/tasks/workflows/extension_bump.rs | 306 - .../src/tasks/workflows/extension_release.rs | 72 - .../src/tasks/workflows/extension_tests.rs | 114 - .../workflows/extensions/bump_version.rs | 103 - .../src/tasks/workflows/extensions/mod.rs | 24 - .../workflows/extensions/release_version.rs | 26 - .../tasks/workflows/extensions/run_tests.rs | 27 - .../xtask/src/tasks/workflows/nix_build.rs | 104 - tooling/xtask/src/tasks/workflows/release.rs | 195 - .../src/tasks/workflows/release_nightly.rs | 131 - .../src/tasks/workflows/run_agent_evals.rs | 165 - .../xtask/src/tasks/workflows/run_bundling.rs | 195 - .../xtask/src/tasks/workflows/run_tests.rs | 496 - tooling/xtask/src/tasks/workflows/runners.rs | 66 - tooling/xtask/src/tasks/workflows/steps.rs | 336 - tooling/xtask/src/tasks/workflows/vars.rs | 344 - tooling/xtask/src/workspace.rs | 9 - 2359 files changed, 192 insertions(+), 1021603 deletions(-) delete mode 100644 Dockerfile-collab delete mode 100644 Dockerfile-collab.dockerignore delete mode 100644 Dockerfile-cross.dockerignore delete mode 100644 Dockerfile-distros delete mode 100644 Dockerfile-distros.dockerignore delete mode 100644 LICENSE-AGPL delete mode 100644 LICENSE-GPL delete mode 100644 Procfile delete mode 100644 Procfile.web delete mode 100644 compose.yml delete mode 100644 crates/acp_thread/Cargo.toml delete mode 120000 crates/acp_thread/LICENSE-GPL delete mode 100644 crates/acp_thread/src/acp_thread.rs delete mode 100644 crates/acp_thread/src/connection.rs delete mode 100644 crates/acp_thread/src/diff.rs delete mode 100644 crates/acp_thread/src/mention.rs delete mode 100644 crates/acp_thread/src/terminal.rs delete mode 100644 crates/acp_tools/Cargo.toml delete mode 120000 crates/acp_tools/LICENSE-GPL delete mode 100644 crates/acp_tools/src/acp_tools.rs delete mode 100644 crates/action_log/Cargo.toml delete mode 120000 crates/action_log/LICENSE-GPL delete mode 100644 crates/action_log/src/action_log.rs delete mode 100644 crates/activity_indicator/Cargo.toml delete mode 120000 crates/activity_indicator/LICENSE-GPL delete mode 100644 crates/activity_indicator/src/activity_indicator.rs delete mode 100644 crates/agent/Cargo.toml delete mode 120000 crates/agent/LICENSE-GPL delete mode 100644 crates/agent/src/agent.rs delete mode 100644 crates/agent/src/db.rs delete mode 100644 crates/agent/src/edit_agent.rs delete mode 100644 crates/agent/src/edit_agent/create_file_parser.rs delete mode 100644 crates/agent/src/edit_agent/edit_parser.rs delete mode 100644 crates/agent/src/edit_agent/evals.rs delete mode 100644 crates/agent/src/edit_agent/evals/fixtures/add_overwrite_test/before.rs delete mode 100644 crates/agent/src/edit_agent/evals/fixtures/delete_run_git_blame/after.rs delete mode 100644 crates/agent/src/edit_agent/evals/fixtures/delete_run_git_blame/before.rs delete mode 100644 crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/before.rs delete mode 100644 crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-01.diff delete mode 100644 crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-02.diff delete mode 100644 crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-03.diff delete mode 100644 crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-04.diff delete mode 100644 crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/before.rs delete mode 100644 crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-01.diff delete mode 100644 crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-02.diff delete mode 100644 crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-03.diff delete mode 100644 crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-04.diff delete mode 100644 crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-05.diff delete mode 100644 crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-06.diff delete mode 100644 crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-07.diff delete mode 100644 crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-08.diff delete mode 100644 crates/agent/src/edit_agent/evals/fixtures/from_pixels_constructor/before.rs delete mode 100644 crates/agent/src/edit_agent/evals/fixtures/translate_doc_comments/before.rs delete mode 100644 crates/agent/src/edit_agent/evals/fixtures/use_wasi_sdk_in_compile_parser_to_wasm/before.rs delete mode 100644 crates/agent/src/edit_agent/evals/fixtures/zode/prompt.md delete mode 100644 crates/agent/src/edit_agent/evals/fixtures/zode/react.py delete mode 100644 crates/agent/src/edit_agent/evals/fixtures/zode/react_test.py delete mode 100644 crates/agent/src/edit_agent/streaming_fuzzy_matcher.rs delete mode 100644 crates/agent/src/history_store.rs delete mode 100644 crates/agent/src/legacy_thread.rs delete mode 100644 crates/agent/src/native_agent_server.rs delete mode 100644 crates/agent/src/outline.rs delete mode 100644 crates/agent/src/templates.rs delete mode 100644 crates/agent/src/templates/create_file_prompt.hbs delete mode 100644 crates/agent/src/templates/diff_judge.hbs delete mode 100644 crates/agent/src/templates/edit_file_prompt_diff_fenced.hbs delete mode 100644 crates/agent/src/templates/edit_file_prompt_xml.hbs delete mode 100644 crates/agent/src/templates/system_prompt.hbs delete mode 100644 crates/agent/src/tests/mod.rs delete mode 100644 crates/agent/src/tests/test_tools.rs delete mode 100644 crates/agent/src/thread.rs delete mode 100644 crates/agent/src/tools.rs delete mode 100644 crates/agent/src/tools/context_server_registry.rs delete mode 100644 crates/agent/src/tools/copy_path_tool.rs delete mode 100644 crates/agent/src/tools/create_directory_tool.rs delete mode 100644 crates/agent/src/tools/delete_path_tool.rs delete mode 100644 crates/agent/src/tools/diagnostics_tool.rs delete mode 100644 crates/agent/src/tools/edit_file_tool.rs delete mode 100644 crates/agent/src/tools/fetch_tool.rs delete mode 100644 crates/agent/src/tools/find_path_tool.rs delete mode 100644 crates/agent/src/tools/grep_tool.rs delete mode 100644 crates/agent/src/tools/list_directory_tool.rs delete mode 100644 crates/agent/src/tools/move_path_tool.rs delete mode 100644 crates/agent/src/tools/now_tool.rs delete mode 100644 crates/agent/src/tools/open_tool.rs delete mode 100644 crates/agent/src/tools/read_file_tool.rs delete mode 100644 crates/agent/src/tools/terminal_tool.rs delete mode 100644 crates/agent/src/tools/thinking_tool.rs delete mode 100644 crates/agent/src/tools/web_search_tool.rs delete mode 100644 crates/agent_servers/Cargo.toml delete mode 120000 crates/agent_servers/LICENSE-GPL delete mode 100644 crates/agent_servers/src/acp.rs delete mode 100644 crates/agent_servers/src/agent_servers.rs delete mode 100644 crates/agent_servers/src/claude.rs delete mode 100644 crates/agent_servers/src/codex.rs delete mode 100644 crates/agent_servers/src/custom.rs delete mode 100644 crates/agent_servers/src/e2e_tests.rs delete mode 100644 crates/agent_servers/src/gemini.rs delete mode 100644 crates/agent_settings/Cargo.toml delete mode 120000 crates/agent_settings/LICENSE-GPL delete mode 100644 crates/agent_settings/src/agent_profile.rs delete mode 100644 crates/agent_settings/src/agent_settings.rs delete mode 100644 crates/agent_settings/src/prompts/summarize_thread_detailed_prompt.txt delete mode 100644 crates/agent_settings/src/prompts/summarize_thread_prompt.txt delete mode 100644 crates/agent_ui/Cargo.toml delete mode 120000 crates/agent_ui/LICENSE-GPL delete mode 100644 crates/agent_ui/src/acp.rs delete mode 100644 crates/agent_ui/src/acp/entry_view_state.rs delete mode 100644 crates/agent_ui/src/acp/message_editor.rs delete mode 100644 crates/agent_ui/src/acp/mode_selector.rs delete mode 100644 crates/agent_ui/src/acp/model_selector.rs delete mode 100644 crates/agent_ui/src/acp/model_selector_popover.rs delete mode 100644 crates/agent_ui/src/acp/thread_history.rs delete mode 100644 crates/agent_ui/src/acp/thread_view.rs delete mode 100644 crates/agent_ui/src/agent_configuration.rs delete mode 100644 crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs delete mode 100644 crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs delete mode 100644 crates/agent_ui/src/agent_configuration/configure_context_server_tools_modal.rs delete mode 100644 crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs delete mode 100644 crates/agent_ui/src/agent_configuration/manage_profiles_modal/profile_modal_header.rs delete mode 100644 crates/agent_ui/src/agent_configuration/tool_picker.rs delete mode 100644 crates/agent_ui/src/agent_diff.rs delete mode 100644 crates/agent_ui/src/agent_model_selector.rs delete mode 100644 crates/agent_ui/src/agent_panel.rs delete mode 100644 crates/agent_ui/src/agent_ui.rs delete mode 100644 crates/agent_ui/src/buffer_codegen.rs delete mode 100644 crates/agent_ui/src/completion_provider.rs delete mode 100644 crates/agent_ui/src/context.rs delete mode 100644 crates/agent_ui/src/context_server_configuration.rs delete mode 100644 crates/agent_ui/src/evals.rs delete mode 100644 crates/agent_ui/src/inline_assistant.rs delete mode 100644 crates/agent_ui/src/inline_prompt_editor.rs delete mode 100644 crates/agent_ui/src/language_model_selector.rs delete mode 100644 crates/agent_ui/src/mention_set.rs delete mode 100644 crates/agent_ui/src/profile_selector.rs delete mode 100644 crates/agent_ui/src/slash_command.rs delete mode 100644 crates/agent_ui/src/slash_command_picker.rs delete mode 100644 crates/agent_ui/src/terminal_codegen.rs delete mode 100644 crates/agent_ui/src/terminal_inline_assistant.rs delete mode 100644 crates/agent_ui/src/text_thread_editor.rs delete mode 100644 crates/agent_ui/src/ui.rs delete mode 100644 crates/agent_ui/src/ui/acp_onboarding_modal.rs delete mode 100644 crates/agent_ui/src/ui/agent_notification.rs delete mode 100644 crates/agent_ui/src/ui/burn_mode_tooltip.rs delete mode 100644 crates/agent_ui/src/ui/claude_code_onboarding_modal.rs delete mode 100644 crates/agent_ui/src/ui/end_trial_upsell.rs delete mode 100644 crates/agent_ui/src/ui/hold_for_default.rs delete mode 100644 crates/agent_ui/src/ui/onboarding_modal.rs delete mode 100644 crates/agent_ui/src/ui/unavailable_editing_tooltip.rs delete mode 100644 crates/agent_ui/src/ui/usage_callout.rs delete mode 100644 crates/ai_onboarding/Cargo.toml delete mode 120000 crates/ai_onboarding/LICENSE-GPL delete mode 100644 crates/ai_onboarding/src/agent_api_keys_onboarding.rs delete mode 100644 crates/ai_onboarding/src/agent_panel_onboarding_card.rs delete mode 100644 crates/ai_onboarding/src/agent_panel_onboarding_content.rs delete mode 100644 crates/ai_onboarding/src/ai_onboarding.rs delete mode 100644 crates/ai_onboarding/src/ai_upsell_card.rs delete mode 100644 crates/ai_onboarding/src/edit_prediction_onboarding_content.rs delete mode 100644 crates/ai_onboarding/src/plan_definitions.rs delete mode 100644 crates/ai_onboarding/src/young_account_banner.rs delete mode 100644 crates/anthropic/Cargo.toml delete mode 120000 crates/anthropic/LICENSE-GPL delete mode 100644 crates/anthropic/src/anthropic.rs delete mode 100644 crates/anthropic/src/batches.rs delete mode 100644 crates/askpass/Cargo.toml delete mode 120000 crates/askpass/LICENSE-GPL delete mode 100644 crates/askpass/src/askpass.rs delete mode 100644 crates/askpass/src/encrypted_password.rs delete mode 100644 crates/assets/Cargo.toml delete mode 120000 crates/assets/LICENSE-GPL delete mode 100644 crates/assets/src/assets.rs delete mode 100644 crates/assistant_slash_command/Cargo.toml delete mode 120000 crates/assistant_slash_command/LICENSE-GPL delete mode 100644 crates/assistant_slash_command/src/assistant_slash_command.rs delete mode 100644 crates/assistant_slash_command/src/extension_slash_command.rs delete mode 100644 crates/assistant_slash_command/src/slash_command_registry.rs delete mode 100644 crates/assistant_slash_command/src/slash_command_working_set.rs delete mode 100644 crates/assistant_slash_commands/Cargo.toml delete mode 120000 crates/assistant_slash_commands/LICENSE-GPL delete mode 100644 crates/assistant_slash_commands/src/assistant_slash_commands.rs delete mode 100644 crates/assistant_slash_commands/src/cargo_workspace_command.rs delete mode 100644 crates/assistant_slash_commands/src/context_server_command.rs delete mode 100644 crates/assistant_slash_commands/src/default_command.rs delete mode 100644 crates/assistant_slash_commands/src/delta_command.rs delete mode 100644 crates/assistant_slash_commands/src/diagnostics_command.rs delete mode 100644 crates/assistant_slash_commands/src/fetch_command.rs delete mode 100644 crates/assistant_slash_commands/src/file_command.rs delete mode 100644 crates/assistant_slash_commands/src/now_command.rs delete mode 100644 crates/assistant_slash_commands/src/prompt_command.rs delete mode 100644 crates/assistant_slash_commands/src/selection_command.rs delete mode 100644 crates/assistant_slash_commands/src/streaming_example_command.rs delete mode 100644 crates/assistant_slash_commands/src/symbols_command.rs delete mode 100644 crates/assistant_slash_commands/src/tab_command.rs delete mode 100644 crates/assistant_text_thread/Cargo.toml delete mode 120000 crates/assistant_text_thread/LICENSE-GPL delete mode 100644 crates/assistant_text_thread/src/assistant_text_thread.rs delete mode 100644 crates/assistant_text_thread/src/assistant_text_thread_tests.rs delete mode 100644 crates/assistant_text_thread/src/text_thread.rs delete mode 100644 crates/assistant_text_thread/src/text_thread_store.rs delete mode 100644 crates/audio/Cargo.toml delete mode 120000 crates/audio/LICENSE-GPL delete mode 100644 crates/audio/src/audio.rs delete mode 100644 crates/audio/src/audio_settings.rs delete mode 100644 crates/audio/src/replays.rs delete mode 100644 crates/audio/src/rodio_ext.rs delete mode 100644 crates/auto_update/Cargo.toml delete mode 120000 crates/auto_update/LICENSE-GPL delete mode 100644 crates/auto_update/src/auto_update.rs delete mode 100644 crates/auto_update_helper/Cargo.toml delete mode 120000 crates/auto_update_helper/LICENSE-GPL delete mode 100644 crates/auto_update_helper/app-icon.ico delete mode 100644 crates/auto_update_helper/build.rs delete mode 100644 crates/auto_update_helper/manifest.xml delete mode 100644 crates/auto_update_helper/src/auto_update_helper.rs delete mode 100644 crates/auto_update_helper/src/dialog.rs delete mode 100644 crates/auto_update_helper/src/updater.rs delete mode 100644 crates/auto_update_ui/Cargo.toml delete mode 120000 crates/auto_update_ui/LICENSE-GPL delete mode 100644 crates/auto_update_ui/src/auto_update_ui.rs delete mode 100644 crates/aws_http_client/Cargo.toml delete mode 120000 crates/aws_http_client/LICENSE-GPL delete mode 100644 crates/aws_http_client/src/aws_http_client.rs delete mode 100644 crates/bedrock/Cargo.toml delete mode 120000 crates/bedrock/LICENSE-GPL delete mode 100644 crates/bedrock/src/bedrock.rs delete mode 100644 crates/bedrock/src/models.rs delete mode 100644 crates/breadcrumbs/Cargo.toml delete mode 120000 crates/breadcrumbs/LICENSE-GPL delete mode 100644 crates/breadcrumbs/src/breadcrumbs.rs delete mode 100644 crates/buffer_diff/Cargo.toml delete mode 120000 crates/buffer_diff/LICENSE-GPL delete mode 100644 crates/buffer_diff/src/buffer_diff.rs delete mode 100644 crates/call/Cargo.toml delete mode 120000 crates/call/LICENSE-GPL delete mode 100644 crates/call/src/call.rs delete mode 100644 crates/call/src/call_impl/mod.rs delete mode 100644 crates/call/src/call_impl/participant.rs delete mode 100644 crates/call/src/call_impl/room.rs delete mode 100644 crates/call/src/call_settings.rs delete mode 100644 crates/channel/Cargo.toml delete mode 120000 crates/channel/LICENSE-GPL delete mode 100644 crates/channel/src/channel.rs delete mode 100644 crates/channel/src/channel_buffer.rs delete mode 100644 crates/channel/src/channel_store.rs delete mode 100644 crates/channel/src/channel_store/channel_index.rs delete mode 100644 crates/channel/src/channel_store_tests.rs delete mode 100644 crates/cli/Cargo.toml delete mode 120000 crates/cli/LICENSE-GPL delete mode 100644 crates/cli/README.md delete mode 100644 crates/cli/build.rs delete mode 100644 crates/cli/src/cli.rs delete mode 100644 crates/cli/src/main.rs delete mode 100644 crates/client/Cargo.toml delete mode 120000 crates/client/LICENSE-GPL delete mode 100644 crates/client/src/client.rs delete mode 100644 crates/client/src/proxy.rs delete mode 100644 crates/client/src/proxy/http_proxy.rs delete mode 100644 crates/client/src/proxy/socks_proxy.rs delete mode 100644 crates/client/src/telemetry.rs delete mode 100644 crates/client/src/telemetry/event_coalescer.rs delete mode 100644 crates/client/src/test.rs delete mode 100644 crates/client/src/user.rs delete mode 100644 crates/client/src/zed_urls.rs delete mode 100644 crates/clock/Cargo.toml delete mode 120000 crates/clock/LICENSE-GPL delete mode 100644 crates/clock/src/clock.rs delete mode 100644 crates/clock/src/system_clock.rs delete mode 100644 crates/codestral/Cargo.toml delete mode 120000 crates/codestral/LICENSE-GPL delete mode 100644 crates/codestral/src/codestral.rs delete mode 100644 crates/collab/.env.toml delete mode 100644 crates/collab/Cargo.toml delete mode 120000 crates/collab/LICENSE-AGPL delete mode 100644 crates/collab/README.md delete mode 100644 crates/collab/k8s/collab.template.yml delete mode 100644 crates/collab/k8s/environments/production.sh delete mode 100644 crates/collab/k8s/environments/staging.sh delete mode 100644 crates/collab/migrations.sqlite/20221109000000_test_schema.sql delete mode 100644 crates/collab/migrations/20251208000000_test_schema.sql delete mode 100644 crates/collab/seed.default.json delete mode 100644 crates/collab/seed/github_users.json delete mode 100644 crates/collab/src/api.rs delete mode 100644 crates/collab/src/api/contributors.rs delete mode 100644 crates/collab/src/api/events.rs delete mode 100644 crates/collab/src/api/extensions.rs delete mode 100644 crates/collab/src/auth.rs delete mode 100644 crates/collab/src/bin/dotenv.rs delete mode 100644 crates/collab/src/completion.rs delete mode 100644 crates/collab/src/db.rs delete mode 100644 crates/collab/src/db/ids.rs delete mode 100644 crates/collab/src/db/queries.rs delete mode 100644 crates/collab/src/db/queries/access_tokens.rs delete mode 100644 crates/collab/src/db/queries/buffers.rs delete mode 100644 crates/collab/src/db/queries/channels.rs delete mode 100644 crates/collab/src/db/queries/contacts.rs delete mode 100644 crates/collab/src/db/queries/contributors.rs delete mode 100644 crates/collab/src/db/queries/dev_server_projects.rs delete mode 100644 crates/collab/src/db/queries/dev_servers.rs delete mode 100644 crates/collab/src/db/queries/extensions.rs delete mode 100644 crates/collab/src/db/queries/notifications.rs delete mode 100644 crates/collab/src/db/queries/projects.rs delete mode 100644 crates/collab/src/db/queries/rooms.rs delete mode 100644 crates/collab/src/db/queries/servers.rs delete mode 100644 crates/collab/src/db/queries/users.rs delete mode 100644 crates/collab/src/db/tables.rs delete mode 100644 crates/collab/src/db/tables/access_token.rs delete mode 100644 crates/collab/src/db/tables/buffer.rs delete mode 100644 crates/collab/src/db/tables/buffer_operation.rs delete mode 100644 crates/collab/src/db/tables/buffer_snapshot.rs delete mode 100644 crates/collab/src/db/tables/channel.rs delete mode 100644 crates/collab/src/db/tables/channel_buffer_collaborator.rs delete mode 100644 crates/collab/src/db/tables/channel_chat_participant.rs delete mode 100644 crates/collab/src/db/tables/channel_member.rs delete mode 100644 crates/collab/src/db/tables/contact.rs delete mode 100644 crates/collab/src/db/tables/contributor.rs delete mode 100644 crates/collab/src/db/tables/extension.rs delete mode 100644 crates/collab/src/db/tables/extension_version.rs delete mode 100644 crates/collab/src/db/tables/follower.rs delete mode 100644 crates/collab/src/db/tables/language_server.rs delete mode 100644 crates/collab/src/db/tables/notification.rs delete mode 100644 crates/collab/src/db/tables/notification_kind.rs delete mode 100644 crates/collab/src/db/tables/observed_buffer_edits.rs delete mode 100644 crates/collab/src/db/tables/project.rs delete mode 100644 crates/collab/src/db/tables/project_collaborator.rs delete mode 100644 crates/collab/src/db/tables/project_repository.rs delete mode 100644 crates/collab/src/db/tables/project_repository_statuses.rs delete mode 100644 crates/collab/src/db/tables/room.rs delete mode 100644 crates/collab/src/db/tables/room_participant.rs delete mode 100644 crates/collab/src/db/tables/server.rs delete mode 100644 crates/collab/src/db/tables/user.rs delete mode 100644 crates/collab/src/db/tables/worktree.rs delete mode 100644 crates/collab/src/db/tables/worktree_diagnostic_summary.rs delete mode 100644 crates/collab/src/db/tables/worktree_entry.rs delete mode 100644 crates/collab/src/db/tables/worktree_settings_file.rs delete mode 100644 crates/collab/src/db/tests.rs delete mode 100644 crates/collab/src/db/tests/buffer_tests.rs delete mode 100644 crates/collab/src/db/tests/channel_tests.rs delete mode 100644 crates/collab/src/db/tests/contributor_tests.rs delete mode 100644 crates/collab/src/db/tests/db_tests.rs delete mode 100644 crates/collab/src/db/tests/extension_tests.rs delete mode 100644 crates/collab/src/db/tests/migrations.rs delete mode 100644 crates/collab/src/env.rs delete mode 100644 crates/collab/src/errors.rs delete mode 100644 crates/collab/src/executor.rs delete mode 100644 crates/collab/src/lib.rs delete mode 100644 crates/collab/src/main.rs delete mode 100644 crates/collab/src/rpc.rs delete mode 100644 crates/collab/src/rpc/connection_pool.rs delete mode 100644 crates/collab/src/seed.rs delete mode 100644 crates/collab/src/tests.rs delete mode 100644 crates/collab/src/tests/channel_buffer_tests.rs delete mode 100644 crates/collab/src/tests/channel_guest_tests.rs delete mode 100644 crates/collab/src/tests/channel_tests.rs delete mode 100644 crates/collab/src/tests/debug_panel_tests.rs delete mode 100644 crates/collab/src/tests/editor_tests.rs delete mode 100644 crates/collab/src/tests/following_tests.rs delete mode 100644 crates/collab/src/tests/git_tests.rs delete mode 100644 crates/collab/src/tests/integration_tests.rs delete mode 100644 crates/collab/src/tests/notification_tests.rs delete mode 100644 crates/collab/src/tests/random_channel_buffer_tests.rs delete mode 100644 crates/collab/src/tests/random_project_collaboration_tests.rs delete mode 100644 crates/collab/src/tests/randomized_test_helpers.rs delete mode 100644 crates/collab/src/tests/remote_editing_collaboration_tests.rs delete mode 100644 crates/collab/src/tests/test_server.rs delete mode 100644 crates/collab_ui/Cargo.toml delete mode 120000 crates/collab_ui/LICENSE-GPL delete mode 100644 crates/collab_ui/src/channel_view.rs delete mode 100644 crates/collab_ui/src/collab_panel.rs delete mode 100644 crates/collab_ui/src/collab_panel/channel_modal.rs delete mode 100644 crates/collab_ui/src/collab_panel/contact_finder.rs delete mode 100644 crates/collab_ui/src/collab_ui.rs delete mode 100644 crates/collab_ui/src/notification_panel.rs delete mode 100644 crates/collab_ui/src/notifications.rs delete mode 100644 crates/collab_ui/src/notifications/collab_notification.rs delete mode 100644 crates/collab_ui/src/notifications/incoming_call_notification.rs delete mode 100644 crates/collab_ui/src/notifications/project_shared_notification.rs delete mode 100644 crates/collab_ui/src/notifications/stories.rs delete mode 100644 crates/collab_ui/src/notifications/stories/collab_notification.rs delete mode 100644 crates/collab_ui/src/panel_settings.rs delete mode 100644 crates/command_palette/Cargo.toml delete mode 120000 crates/command_palette/LICENSE-GPL delete mode 100644 crates/command_palette/src/command_palette.rs delete mode 100644 crates/command_palette/src/persistence.rs delete mode 100644 crates/command_palette_hooks/Cargo.toml delete mode 120000 crates/command_palette_hooks/LICENSE-GPL delete mode 100644 crates/command_palette_hooks/src/command_palette_hooks.rs delete mode 100644 crates/component/Cargo.toml delete mode 120000 crates/component/LICENSE-GPL delete mode 100644 crates/component/src/component.rs delete mode 100644 crates/component/src/component_layout.rs delete mode 100644 crates/context_server/Cargo.toml delete mode 120000 crates/context_server/LICENSE-GPL delete mode 100644 crates/context_server/src/client.rs delete mode 100644 crates/context_server/src/context_server.rs delete mode 100644 crates/context_server/src/listener.rs delete mode 100644 crates/context_server/src/protocol.rs delete mode 100644 crates/context_server/src/test.rs delete mode 100644 crates/context_server/src/transport.rs delete mode 100644 crates/context_server/src/transport/http.rs delete mode 100644 crates/context_server/src/transport/stdio_transport.rs delete mode 100644 crates/context_server/src/types.rs delete mode 100644 crates/copilot/Cargo.toml delete mode 120000 crates/copilot/LICENSE-GPL delete mode 100644 crates/copilot/src/copilot.rs delete mode 100644 crates/copilot/src/copilot_chat.rs delete mode 100644 crates/copilot/src/copilot_edit_prediction_delegate.rs delete mode 100644 crates/copilot/src/copilot_responses.rs delete mode 100644 crates/copilot/src/request.rs delete mode 100644 crates/copilot/src/sign_in.rs delete mode 100644 crates/crashes/Cargo.toml delete mode 120000 crates/crashes/LICENSE-GPL delete mode 100644 crates/crashes/src/crashes.rs delete mode 100644 crates/credentials_provider/Cargo.toml delete mode 120000 crates/credentials_provider/LICENSE-GPL delete mode 100644 crates/credentials_provider/src/credentials_provider.rs delete mode 100644 crates/dap/Cargo.toml delete mode 120000 crates/dap/LICENSE-GPL delete mode 100644 crates/dap/docs/breakpoints.md delete mode 100644 crates/dap/src/adapters.rs delete mode 100644 crates/dap/src/client.rs delete mode 100644 crates/dap/src/dap.rs delete mode 100644 crates/dap/src/debugger_settings.rs delete mode 100644 crates/dap/src/inline_value.rs delete mode 100644 crates/dap/src/proto_conversions.rs delete mode 100644 crates/dap/src/registry.rs delete mode 100644 crates/dap/src/transport.rs delete mode 100644 crates/dap_adapters/Cargo.toml delete mode 120000 crates/dap_adapters/LICENSE-GPL delete mode 100644 crates/dap_adapters/src/codelldb.rs delete mode 100644 crates/dap_adapters/src/dap_adapters.rs delete mode 100644 crates/dap_adapters/src/gdb.rs delete mode 100644 crates/dap_adapters/src/go.rs delete mode 100644 crates/dap_adapters/src/javascript.rs delete mode 100644 crates/dap_adapters/src/python.rs delete mode 100644 crates/db/Cargo.toml delete mode 120000 crates/db/LICENSE-GPL delete mode 100644 crates/db/README.md delete mode 100644 crates/db/src/db.rs delete mode 100644 crates/db/src/kvp.rs delete mode 100644 crates/db/src/query.rs delete mode 100644 crates/debug_adapter_extension/Cargo.toml delete mode 120000 crates/debug_adapter_extension/LICENSE-GPL delete mode 100644 crates/debug_adapter_extension/src/debug_adapter_extension.rs delete mode 100644 crates/debug_adapter_extension/src/extension_dap_adapter.rs delete mode 100644 crates/debug_adapter_extension/src/extension_locator_adapter.rs delete mode 100644 crates/debugger_tools/Cargo.toml delete mode 120000 crates/debugger_tools/LICENSE-GPL delete mode 100644 crates/debugger_tools/src/dap_log.rs delete mode 100644 crates/debugger_tools/src/debugger_tools.rs delete mode 100644 crates/debugger_ui/Cargo.toml delete mode 120000 crates/debugger_ui/LICENSE-GPL delete mode 100644 crates/debugger_ui/src/attach_modal.rs delete mode 100644 crates/debugger_ui/src/debugger_panel.rs delete mode 100644 crates/debugger_ui/src/debugger_ui.rs delete mode 100644 crates/debugger_ui/src/dropdown_menus.rs delete mode 100644 crates/debugger_ui/src/new_process_modal.rs delete mode 100644 crates/debugger_ui/src/onboarding_modal.rs delete mode 100644 crates/debugger_ui/src/persistence.rs delete mode 100644 crates/debugger_ui/src/session.rs delete mode 100644 crates/debugger_ui/src/session/running.rs delete mode 100644 crates/debugger_ui/src/session/running/breakpoint_list.rs delete mode 100644 crates/debugger_ui/src/session/running/console.rs delete mode 100644 crates/debugger_ui/src/session/running/loaded_source_list.rs delete mode 100644 crates/debugger_ui/src/session/running/memory_view.rs delete mode 100644 crates/debugger_ui/src/session/running/module_list.rs delete mode 100644 crates/debugger_ui/src/session/running/stack_frame_list.rs delete mode 100644 crates/debugger_ui/src/session/running/variable_list.rs delete mode 100644 crates/debugger_ui/src/stack_trace_view.rs delete mode 100644 crates/debugger_ui/src/tests.rs delete mode 100644 crates/debugger_ui/src/tests/attach_modal.rs delete mode 100644 crates/debugger_ui/src/tests/console.rs delete mode 100644 crates/debugger_ui/src/tests/dap_logger.rs delete mode 100644 crates/debugger_ui/src/tests/debugger_panel.rs delete mode 100644 crates/debugger_ui/src/tests/inline_values.rs delete mode 100644 crates/debugger_ui/src/tests/module_list.rs delete mode 100644 crates/debugger_ui/src/tests/new_process_modal.rs delete mode 100644 crates/debugger_ui/src/tests/persistence.rs delete mode 100644 crates/debugger_ui/src/tests/stack_frame_list.rs delete mode 100644 crates/debugger_ui/src/tests/variable_list.rs delete mode 100644 crates/deepseek/Cargo.toml delete mode 120000 crates/deepseek/LICENSE-GPL delete mode 100644 crates/deepseek/src/deepseek.rs delete mode 100644 crates/denoise/Cargo.toml delete mode 120000 crates/denoise/LICENSE-GPL delete mode 100644 crates/denoise/README.md delete mode 100644 crates/denoise/examples/denoise.rs delete mode 100644 crates/denoise/examples/enable_disable.rs delete mode 100644 crates/denoise/models/model_1_converted_simplified.onnx delete mode 100644 crates/denoise/models/model_2_converted_simplified.onnx delete mode 100644 crates/denoise/src/engine.rs delete mode 100644 crates/denoise/src/lib.rs delete mode 100644 crates/diagnostics/Cargo.toml delete mode 120000 crates/diagnostics/LICENSE-GPL delete mode 100644 crates/diagnostics/src/buffer_diagnostics.rs delete mode 100644 crates/diagnostics/src/diagnostic_renderer.rs delete mode 100644 crates/diagnostics/src/diagnostics.rs delete mode 100644 crates/diagnostics/src/diagnostics_tests.rs delete mode 100644 crates/diagnostics/src/items.rs delete mode 100644 crates/diagnostics/src/toolbar_controls.rs delete mode 100644 crates/docs_preprocessor/Cargo.toml delete mode 120000 crates/docs_preprocessor/LICENSE-GPL delete mode 100644 crates/docs_preprocessor/src/main.rs delete mode 100644 crates/edit_prediction/Cargo.toml delete mode 120000 crates/edit_prediction/LICENSE-GPL delete mode 100644 crates/edit_prediction/license_examples/0bsd.txt delete mode 100644 crates/edit_prediction/license_examples/apache-2.0-ex0.txt delete mode 100644 crates/edit_prediction/license_examples/apache-2.0-ex1.txt delete mode 100644 crates/edit_prediction/license_examples/apache-2.0-ex2.txt delete mode 100644 crates/edit_prediction/license_examples/apache-2.0-ex3.txt delete mode 100644 crates/edit_prediction/license_examples/apache-2.0-ex4.txt delete mode 100644 crates/edit_prediction/license_examples/bsd-1-clause.txt delete mode 100644 crates/edit_prediction/license_examples/bsd-2-clause-ex0.txt delete mode 100644 crates/edit_prediction/license_examples/bsd-3-clause-ex0.txt delete mode 100644 crates/edit_prediction/license_examples/bsd-3-clause-ex1.txt delete mode 100644 crates/edit_prediction/license_examples/bsd-3-clause-ex2.txt delete mode 100644 crates/edit_prediction/license_examples/bsd-3-clause-ex3.txt delete mode 100644 crates/edit_prediction/license_examples/bsd-3-clause-ex4.txt delete mode 100644 crates/edit_prediction/license_examples/isc.txt delete mode 100644 crates/edit_prediction/license_examples/mit-ex0.txt delete mode 100644 crates/edit_prediction/license_examples/mit-ex1.txt delete mode 100644 crates/edit_prediction/license_examples/mit-ex2.txt delete mode 100644 crates/edit_prediction/license_examples/mit-ex3.txt delete mode 100644 crates/edit_prediction/license_examples/upl-1.0.txt delete mode 100644 crates/edit_prediction/license_examples/zlib-ex0.txt delete mode 100644 crates/edit_prediction/license_patterns/0bsd-pattern delete mode 100644 crates/edit_prediction/license_patterns/apache-2.0-pattern delete mode 100644 crates/edit_prediction/license_patterns/apache-2.0-reference-pattern delete mode 100644 crates/edit_prediction/license_patterns/bsd-pattern delete mode 100644 crates/edit_prediction/license_patterns/isc-pattern delete mode 100644 crates/edit_prediction/license_patterns/mit-pattern delete mode 100644 crates/edit_prediction/license_patterns/upl-1.0-pattern delete mode 100644 crates/edit_prediction/license_patterns/zlib-pattern delete mode 100644 crates/edit_prediction/src/cursor_excerpt.rs delete mode 100644 crates/edit_prediction/src/edit_prediction.rs delete mode 100644 crates/edit_prediction/src/edit_prediction_tests.rs delete mode 100644 crates/edit_prediction/src/license_detection.rs delete mode 100644 crates/edit_prediction/src/mercury.rs delete mode 100644 crates/edit_prediction/src/onboarding_modal.rs delete mode 100644 crates/edit_prediction/src/open_ai_response.rs delete mode 100644 crates/edit_prediction/src/prediction.rs delete mode 100644 crates/edit_prediction/src/sweep_ai.rs delete mode 100644 crates/edit_prediction/src/udiff.rs delete mode 100644 crates/edit_prediction/src/zed_edit_prediction_delegate.rs delete mode 100644 crates/edit_prediction/src/zeta1.rs delete mode 100644 crates/edit_prediction/src/zeta2.rs delete mode 100644 crates/edit_prediction_cli/Cargo.toml delete mode 120000 crates/edit_prediction_cli/LICENSE-GPL delete mode 100644 crates/edit_prediction_cli/build.rs delete mode 100644 crates/edit_prediction_cli/src/anthropic_client.rs delete mode 100644 crates/edit_prediction_cli/src/distill.rs delete mode 100644 crates/edit_prediction_cli/src/example.rs delete mode 100644 crates/edit_prediction_cli/src/format_prompt.rs delete mode 100644 crates/edit_prediction_cli/src/headless.rs delete mode 100644 crates/edit_prediction_cli/src/load_project.rs delete mode 100644 crates/edit_prediction_cli/src/main.rs delete mode 100644 crates/edit_prediction_cli/src/metrics.rs delete mode 100644 crates/edit_prediction_cli/src/paths.rs delete mode 100644 crates/edit_prediction_cli/src/predict.rs delete mode 100644 crates/edit_prediction_cli/src/progress.rs delete mode 100644 crates/edit_prediction_cli/src/retrieve_context.rs delete mode 100644 crates/edit_prediction_cli/src/score.rs delete mode 100644 crates/edit_prediction_cli/src/teacher.prompt.md delete mode 100644 crates/edit_prediction_context/Cargo.toml delete mode 120000 crates/edit_prediction_context/LICENSE-GPL delete mode 100644 crates/edit_prediction_context/src/assemble_excerpts.rs delete mode 100644 crates/edit_prediction_context/src/edit_prediction_context.rs delete mode 100644 crates/edit_prediction_context/src/edit_prediction_context_tests.rs delete mode 100644 crates/edit_prediction_context/src/excerpt.rs delete mode 100644 crates/edit_prediction_context/src/fake_definition_lsp.rs delete mode 100644 crates/edit_prediction_types/Cargo.toml delete mode 120000 crates/edit_prediction_types/LICENSE-GPL delete mode 100644 crates/edit_prediction_types/src/edit_prediction_types.rs delete mode 100644 crates/edit_prediction_ui/Cargo.toml delete mode 120000 crates/edit_prediction_ui/LICENSE-GPL delete mode 100644 crates/edit_prediction_ui/src/edit_prediction_button.rs delete mode 100644 crates/edit_prediction_ui/src/edit_prediction_context_view.rs delete mode 100644 crates/edit_prediction_ui/src/edit_prediction_ui.rs delete mode 100644 crates/edit_prediction_ui/src/external_provider_api_token_modal.rs delete mode 100644 crates/edit_prediction_ui/src/rate_prediction_modal.rs delete mode 100644 crates/editor/Cargo.toml delete mode 120000 crates/editor/LICENSE-GPL delete mode 100644 crates/editor/benches/display_map.rs delete mode 100644 crates/editor/benches/editor_render.rs delete mode 100644 crates/editor/src/actions.rs delete mode 100644 crates/editor/src/blink_manager.rs delete mode 100644 crates/editor/src/bracket_colorization.rs delete mode 100644 crates/editor/src/clangd_ext.rs delete mode 100644 crates/editor/src/code_completion_tests.rs delete mode 100644 crates/editor/src/code_context_menus.rs delete mode 100644 crates/editor/src/display_map.rs delete mode 100644 crates/editor/src/display_map/block_map.rs delete mode 100644 crates/editor/src/display_map/crease_map.rs delete mode 100644 crates/editor/src/display_map/custom_highlights.rs delete mode 100644 crates/editor/src/display_map/dimensions.rs delete mode 100644 crates/editor/src/display_map/fold_map.rs delete mode 100644 crates/editor/src/display_map/inlay_map.rs delete mode 100644 crates/editor/src/display_map/invisibles.rs delete mode 100644 crates/editor/src/display_map/tab_map.rs delete mode 100644 crates/editor/src/display_map/wrap_map.rs delete mode 100644 crates/editor/src/edit_prediction_tests.rs delete mode 100644 crates/editor/src/editor.rs delete mode 100644 crates/editor/src/editor_settings.rs delete mode 100644 crates/editor/src/editor_tests.rs delete mode 100644 crates/editor/src/element.rs delete mode 100644 crates/editor/src/git.rs delete mode 100644 crates/editor/src/git/blame.rs delete mode 100644 crates/editor/src/highlight_matching_bracket.rs delete mode 100644 crates/editor/src/hover_links.rs delete mode 100644 crates/editor/src/hover_popover.rs delete mode 100644 crates/editor/src/indent_guides.rs delete mode 100644 crates/editor/src/inlays.rs delete mode 100644 crates/editor/src/inlays/inlay_hints.rs delete mode 100644 crates/editor/src/items.rs delete mode 100644 crates/editor/src/jsx_tag_auto_close.rs delete mode 100644 crates/editor/src/linked_editing_ranges.rs delete mode 100644 crates/editor/src/lsp_colors.rs delete mode 100644 crates/editor/src/lsp_ext.rs delete mode 100644 crates/editor/src/mouse_context_menu.rs delete mode 100644 crates/editor/src/movement.rs delete mode 100644 crates/editor/src/persistence.rs delete mode 100644 crates/editor/src/rust_analyzer_ext.rs delete mode 100644 crates/editor/src/scroll.rs delete mode 100644 crates/editor/src/scroll/actions.rs delete mode 100644 crates/editor/src/scroll/autoscroll.rs delete mode 100644 crates/editor/src/scroll/scroll_amount.rs delete mode 100644 crates/editor/src/selections_collection.rs delete mode 100644 crates/editor/src/signature_help.rs delete mode 100644 crates/editor/src/split.rs delete mode 100644 crates/editor/src/tasks.rs delete mode 100644 crates/editor/src/test.rs delete mode 100644 crates/editor/src/test/editor_lsp_test_context.rs delete mode 100644 crates/editor/src/test/editor_test_context.rs delete mode 100644 crates/eval/.gitignore delete mode 100644 crates/eval/Cargo.toml delete mode 120000 crates/eval/LICENSE-GPL delete mode 100644 crates/eval/README.md delete mode 100644 crates/eval/build.rs delete mode 100644 crates/eval/docs/explorer.md delete mode 100644 crates/eval/runner_settings.json delete mode 100644 crates/eval/src/assertions.rs delete mode 100644 crates/eval/src/eval.rs delete mode 100644 crates/eval/src/example.rs delete mode 100644 crates/eval/src/examples/add_arg_to_trait_method.rs delete mode 100644 crates/eval/src/examples/code_block_citations.rs delete mode 100644 crates/eval/src/examples/comment_translation.rs delete mode 100644 crates/eval/src/examples/file_change_notification.rs delete mode 100644 crates/eval/src/examples/file_search.rs delete mode 100644 crates/eval/src/examples/find_and_replace_diff_card.toml delete mode 100644 crates/eval/src/examples/grep_params_escapement.rs delete mode 100644 crates/eval/src/examples/hallucinated_tool_calls.toml delete mode 100644 crates/eval/src/examples/mod.rs delete mode 100644 crates/eval/src/examples/no_tools_enabled.toml delete mode 100644 crates/eval/src/examples/overwrite_file.rs delete mode 100644 crates/eval/src/examples/planets.rs delete mode 100644 crates/eval/src/examples/threads/overwrite-file.json delete mode 100644 crates/eval/src/examples/tree_sitter_drop_emscripten_dep.toml delete mode 100644 crates/eval/src/explorer.html delete mode 100644 crates/eval/src/explorer.rs delete mode 100644 crates/eval/src/ids.rs delete mode 100644 crates/eval/src/instance.rs delete mode 100644 crates/eval/src/judge_diff_prompt.hbs delete mode 100644 crates/eval/src/judge_thread_prompt.hbs delete mode 100644 crates/eval/src/tool_metrics.rs delete mode 100644 crates/eval_utils/Cargo.toml delete mode 120000 crates/eval_utils/LICENSE-GPL delete mode 100644 crates/eval_utils/README.md delete mode 100644 crates/eval_utils/src/eval_utils.rs delete mode 100644 crates/explorer_command_injector/AppxManifest-Nightly.xml delete mode 100644 crates/explorer_command_injector/AppxManifest-Preview.xml delete mode 100644 crates/explorer_command_injector/AppxManifest.xml delete mode 100644 crates/explorer_command_injector/Cargo.toml delete mode 120000 crates/explorer_command_injector/LICENSE-GPL delete mode 100644 crates/explorer_command_injector/src/explorer_command_injector.rs delete mode 100644 crates/extension/Cargo.toml delete mode 120000 crates/extension/LICENSE-GPL delete mode 100644 crates/extension/src/capabilities.rs delete mode 100644 crates/extension/src/capabilities/download_file_capability.rs delete mode 100644 crates/extension/src/capabilities/npm_install_package_capability.rs delete mode 100644 crates/extension/src/capabilities/process_exec_capability.rs delete mode 100644 crates/extension/src/extension.rs delete mode 100644 crates/extension/src/extension_builder.rs delete mode 100644 crates/extension/src/extension_events.rs delete mode 100644 crates/extension/src/extension_host_proxy.rs delete mode 100644 crates/extension/src/extension_manifest.rs delete mode 100644 crates/extension/src/types.rs delete mode 100644 crates/extension/src/types/context_server.rs delete mode 100644 crates/extension/src/types/dap.rs delete mode 100644 crates/extension/src/types/lsp.rs delete mode 100644 crates/extension/src/types/slash_command.rs delete mode 100644 crates/extension_cli/Cargo.toml delete mode 120000 crates/extension_cli/LICENSE-GPL delete mode 100644 crates/extension_cli/src/main.rs delete mode 100644 crates/extension_host/Cargo.toml delete mode 120000 crates/extension_host/LICENSE-GPL delete mode 100644 crates/extension_host/benches/extension_compilation_benchmark.rs delete mode 100644 crates/extension_host/build.rs delete mode 100644 crates/extension_host/src/capability_granter.rs delete mode 100644 crates/extension_host/src/extension_host.rs delete mode 100644 crates/extension_host/src/extension_settings.rs delete mode 100644 crates/extension_host/src/extension_store_test.rs delete mode 100644 crates/extension_host/src/headless_host.rs delete mode 100644 crates/extension_host/src/wasm_host.rs delete mode 100644 crates/extension_host/src/wasm_host/wit.rs delete mode 100644 crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs delete mode 100644 crates/extension_host/src/wasm_host/wit/since_v0_0_4.rs delete mode 100644 crates/extension_host/src/wasm_host/wit/since_v0_0_6.rs delete mode 100644 crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs delete mode 100644 crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs delete mode 100644 crates/extension_host/src/wasm_host/wit/since_v0_3_0.rs delete mode 100644 crates/extension_host/src/wasm_host/wit/since_v0_4_0.rs delete mode 100644 crates/extension_host/src/wasm_host/wit/since_v0_5_0.rs delete mode 100644 crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs delete mode 100644 crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs delete mode 100644 crates/extensions_ui/Cargo.toml delete mode 120000 crates/extensions_ui/LICENSE-GPL delete mode 100644 crates/extensions_ui/src/components.rs delete mode 100644 crates/extensions_ui/src/components/extension_card.rs delete mode 100644 crates/extensions_ui/src/extension_suggest.rs delete mode 100644 crates/extensions_ui/src/extension_version_selector.rs delete mode 100644 crates/extensions_ui/src/extensions_ui.rs delete mode 100644 crates/feature_flags/Cargo.toml delete mode 120000 crates/feature_flags/LICENSE-GPL delete mode 100644 crates/feature_flags/src/feature_flags.rs delete mode 100644 crates/feature_flags/src/flags.rs delete mode 100644 crates/feedback/Cargo.toml delete mode 120000 crates/feedback/LICENSE-GPL delete mode 100644 crates/feedback/src/feedback.rs delete mode 100644 crates/file_finder/Cargo.toml delete mode 120000 crates/file_finder/LICENSE-GPL delete mode 100644 crates/file_finder/src/file_finder.rs delete mode 100644 crates/file_finder/src/file_finder_settings.rs delete mode 100644 crates/file_finder/src/file_finder_tests.rs delete mode 100644 crates/file_finder/src/open_path_prompt.rs delete mode 100644 crates/file_finder/src/open_path_prompt_tests.rs delete mode 100644 crates/file_icons/Cargo.toml delete mode 120000 crates/file_icons/LICENSE-GPL delete mode 100644 crates/file_icons/src/file_icons.rs delete mode 100644 crates/fs/Cargo.toml delete mode 120000 crates/fs/LICENSE-GPL delete mode 100644 crates/fs/src/fake_git_repo.rs delete mode 100644 crates/fs/src/fs.rs delete mode 100644 crates/fs/src/fs_watcher.rs delete mode 100644 crates/fs/src/mac_watcher.rs delete mode 100644 crates/fs_benchmarks/Cargo.toml delete mode 120000 crates/fs_benchmarks/LICENSE-GPL delete mode 100644 crates/fs_benchmarks/src/main.rs delete mode 100644 crates/fsevent/Cargo.toml delete mode 120000 crates/fsevent/LICENSE-GPL delete mode 100644 crates/fsevent/examples/events.rs delete mode 100644 crates/fsevent/src/fsevent.rs delete mode 100644 crates/fuzzy/Cargo.toml delete mode 120000 crates/fuzzy/LICENSE-GPL delete mode 100644 crates/fuzzy/src/char_bag.rs delete mode 100644 crates/fuzzy/src/fuzzy.rs delete mode 100644 crates/fuzzy/src/matcher.rs delete mode 100644 crates/fuzzy/src/paths.rs delete mode 100644 crates/fuzzy/src/strings.rs delete mode 100644 crates/git/Cargo.toml delete mode 120000 crates/git/LICENSE-GPL delete mode 100644 crates/git/src/blame.rs delete mode 100644 crates/git/src/checkpoint.gitignore delete mode 100644 crates/git/src/commit.rs delete mode 100644 crates/git/src/git.rs delete mode 100644 crates/git/src/hosting_provider.rs delete mode 100644 crates/git/src/remote.rs delete mode 100644 crates/git/src/repository.rs delete mode 100644 crates/git/src/stash.rs delete mode 100644 crates/git/src/status.rs delete mode 100644 crates/git/test_data/blame_incremental_complex delete mode 100644 crates/git/test_data/blame_incremental_not_committed delete mode 100644 crates/git/test_data/blame_incremental_simple delete mode 100644 crates/git/test_data/golden/blame_incremental_complex.json delete mode 100644 crates/git/test_data/golden/blame_incremental_not_committed.json delete mode 100644 crates/git/test_data/golden/blame_incremental_simple.json delete mode 100644 crates/git_hosting_providers/Cargo.toml delete mode 120000 crates/git_hosting_providers/LICENSE-GPL delete mode 100644 crates/git_hosting_providers/src/git_hosting_providers.rs delete mode 100644 crates/git_hosting_providers/src/providers.rs delete mode 100644 crates/git_hosting_providers/src/providers/bitbucket.rs delete mode 100644 crates/git_hosting_providers/src/providers/chromium.rs delete mode 100644 crates/git_hosting_providers/src/providers/forgejo.rs delete mode 100644 crates/git_hosting_providers/src/providers/gitea.rs delete mode 100644 crates/git_hosting_providers/src/providers/gitee.rs delete mode 100644 crates/git_hosting_providers/src/providers/github.rs delete mode 100644 crates/git_hosting_providers/src/providers/gitlab.rs delete mode 100644 crates/git_hosting_providers/src/providers/sourcehut.rs delete mode 100644 crates/git_hosting_providers/src/settings.rs delete mode 100644 crates/git_ui/Cargo.toml delete mode 120000 crates/git_ui/LICENSE-GPL delete mode 100644 crates/git_ui/src/askpass_modal.rs delete mode 100644 crates/git_ui/src/blame_ui.rs delete mode 100644 crates/git_ui/src/branch_picker.rs delete mode 100644 crates/git_ui/src/commit_message_prompt.txt delete mode 100644 crates/git_ui/src/commit_modal.rs delete mode 100644 crates/git_ui/src/commit_tooltip.rs delete mode 100644 crates/git_ui/src/commit_view.rs delete mode 100644 crates/git_ui/src/conflict_view.rs delete mode 100644 crates/git_ui/src/file_diff_view.rs delete mode 100644 crates/git_ui/src/file_history_view.rs delete mode 100644 crates/git_ui/src/git_panel.rs delete mode 100644 crates/git_ui/src/git_panel_settings.rs delete mode 100644 crates/git_ui/src/git_ui.rs delete mode 100644 crates/git_ui/src/onboarding.rs delete mode 100644 crates/git_ui/src/picker_prompt.rs delete mode 100644 crates/git_ui/src/project_diff.rs delete mode 100644 crates/git_ui/src/remote_output.rs delete mode 100644 crates/git_ui/src/repository_selector.rs delete mode 100644 crates/git_ui/src/stash_picker.rs delete mode 100644 crates/git_ui/src/text_diff_view.rs delete mode 100644 crates/git_ui/src/worktree_picker.rs delete mode 100644 crates/go_to_line/Cargo.toml delete mode 120000 crates/go_to_line/LICENSE-GPL delete mode 100644 crates/go_to_line/src/cursor_position.rs delete mode 100644 crates/go_to_line/src/go_to_line.rs delete mode 100644 crates/google_ai/Cargo.toml delete mode 120000 crates/google_ai/LICENSE-GPL delete mode 100644 crates/google_ai/src/google_ai.rs delete mode 100644 crates/icons/Cargo.toml delete mode 120000 crates/icons/LICENSE-GPL delete mode 100644 crates/icons/README.md delete mode 100644 crates/icons/src/icons.rs delete mode 100644 crates/image_viewer/Cargo.toml delete mode 120000 crates/image_viewer/LICENSE-GPL delete mode 100644 crates/image_viewer/src/image_info.rs delete mode 100644 crates/image_viewer/src/image_viewer.rs delete mode 100644 crates/image_viewer/src/image_viewer_settings.rs delete mode 100644 crates/inspector_ui/Cargo.toml delete mode 120000 crates/inspector_ui/LICENSE-GPL delete mode 100644 crates/inspector_ui/README.md delete mode 100644 crates/inspector_ui/build.rs delete mode 100644 crates/inspector_ui/src/div_inspector.rs delete mode 100644 crates/inspector_ui/src/inspector.rs delete mode 100644 crates/inspector_ui/src/inspector_ui.rs delete mode 100644 crates/install_cli/Cargo.toml delete mode 120000 crates/install_cli/LICENSE-GPL delete mode 100644 crates/install_cli/src/install_cli.rs delete mode 100644 crates/install_cli/src/install_cli_binary.rs delete mode 100644 crates/install_cli/src/register_zed_scheme.rs delete mode 100644 crates/journal/Cargo.toml delete mode 120000 crates/journal/LICENSE-GPL delete mode 100644 crates/journal/src/journal.rs delete mode 100644 crates/json_schema_store/Cargo.toml delete mode 120000 crates/json_schema_store/LICENSE-GPL delete mode 100644 crates/json_schema_store/src/json_schema_store.rs delete mode 100644 crates/json_schema_store/src/schemas/package.json delete mode 100644 crates/json_schema_store/src/schemas/tsconfig.json delete mode 100644 crates/keymap_editor/Cargo.toml delete mode 120000 crates/keymap_editor/LICENSE-GPL delete mode 100644 crates/keymap_editor/src/keymap_editor.rs delete mode 100644 crates/keymap_editor/src/ui_components/keystroke_input.rs delete mode 100644 crates/keymap_editor/src/ui_components/mod.rs delete mode 100644 crates/language/Cargo.toml delete mode 120000 crates/language/LICENSE-GPL delete mode 100644 crates/language/build.rs delete mode 100644 crates/language/src/buffer.rs delete mode 100644 crates/language/src/buffer/row_chunk.rs delete mode 100644 crates/language/src/buffer_tests.rs delete mode 100644 crates/language/src/diagnostic_set.rs delete mode 100644 crates/language/src/highlight_map.rs delete mode 100644 crates/language/src/language.rs delete mode 100644 crates/language/src/language_registry.rs delete mode 100644 crates/language/src/language_settings.rs delete mode 100644 crates/language/src/manifest.rs delete mode 100644 crates/language/src/outline.rs delete mode 100644 crates/language/src/proto.rs delete mode 100644 crates/language/src/syntax_map.rs delete mode 100644 crates/language/src/syntax_map/syntax_map_tests.rs delete mode 100644 crates/language/src/task_context.rs delete mode 100644 crates/language/src/text_diff.rs delete mode 100644 crates/language/src/toolchain.rs delete mode 100644 crates/language_extension/Cargo.toml delete mode 120000 crates/language_extension/LICENSE-GPL delete mode 100644 crates/language_extension/src/extension_lsp_adapter.rs delete mode 100644 crates/language_extension/src/language_extension.rs delete mode 100644 crates/language_model/Cargo.toml delete mode 120000 crates/language_model/LICENSE-GPL delete mode 100644 crates/language_model/src/fake_provider.rs delete mode 100644 crates/language_model/src/language_model.rs delete mode 100644 crates/language_model/src/model/cloud_model.rs delete mode 100644 crates/language_model/src/model/mod.rs delete mode 100644 crates/language_model/src/rate_limiter.rs delete mode 100644 crates/language_model/src/registry.rs delete mode 100644 crates/language_model/src/request.rs delete mode 100644 crates/language_model/src/role.rs delete mode 100644 crates/language_model/src/telemetry.rs delete mode 100644 crates/language_model/src/tool_schema.rs delete mode 100644 crates/language_models/Cargo.toml delete mode 120000 crates/language_models/LICENSE-GPL delete mode 100644 crates/language_models/src/api_key.rs delete mode 100644 crates/language_models/src/language_models.rs delete mode 100644 crates/language_models/src/provider.rs delete mode 100644 crates/language_models/src/provider/anthropic.rs delete mode 100644 crates/language_models/src/provider/bedrock.rs delete mode 100644 crates/language_models/src/provider/cloud.rs delete mode 100644 crates/language_models/src/provider/copilot_chat.rs delete mode 100644 crates/language_models/src/provider/deepseek.rs delete mode 100644 crates/language_models/src/provider/google.rs delete mode 100644 crates/language_models/src/provider/lmstudio.rs delete mode 100644 crates/language_models/src/provider/mistral.rs delete mode 100644 crates/language_models/src/provider/ollama.rs delete mode 100644 crates/language_models/src/provider/open_ai.rs delete mode 100644 crates/language_models/src/provider/open_ai_compatible.rs delete mode 100644 crates/language_models/src/provider/open_router.rs delete mode 100644 crates/language_models/src/provider/vercel.rs delete mode 100644 crates/language_models/src/provider/x_ai.rs delete mode 100644 crates/language_models/src/settings.rs delete mode 100644 crates/language_models/src/ui.rs delete mode 100644 crates/language_models/src/ui/configured_api_card.rs delete mode 100644 crates/language_models/src/ui/instruction_list_item.rs delete mode 100644 crates/language_onboarding/Cargo.toml delete mode 120000 crates/language_onboarding/LICENSE-GPL delete mode 100644 crates/language_onboarding/src/python.rs delete mode 100644 crates/language_selector/Cargo.toml delete mode 120000 crates/language_selector/LICENSE-GPL delete mode 100644 crates/language_selector/src/active_buffer_language.rs delete mode 100644 crates/language_selector/src/language_selector.rs delete mode 100644 crates/language_tools/Cargo.toml delete mode 120000 crates/language_tools/LICENSE-GPL delete mode 100644 crates/language_tools/src/key_context_view.rs delete mode 100644 crates/language_tools/src/language_tools.rs delete mode 100644 crates/language_tools/src/lsp_button.rs delete mode 100644 crates/language_tools/src/lsp_log_view.rs delete mode 100644 crates/language_tools/src/lsp_log_view_tests.rs delete mode 100644 crates/language_tools/src/syntax_tree_view.rs delete mode 100644 crates/languages/Cargo.toml delete mode 120000 crates/languages/LICENSE-GPL delete mode 100644 crates/languages/src/bash.rs delete mode 100644 crates/languages/src/bash/brackets.scm delete mode 100644 crates/languages/src/bash/config.toml delete mode 100644 crates/languages/src/bash/highlights.scm delete mode 100644 crates/languages/src/bash/indents.scm delete mode 100644 crates/languages/src/bash/injections.scm delete mode 100644 crates/languages/src/bash/overrides.scm delete mode 100644 crates/languages/src/bash/redactions.scm delete mode 100644 crates/languages/src/bash/textobjects.scm delete mode 100644 crates/languages/src/c.rs delete mode 100644 crates/languages/src/c/brackets.scm delete mode 100644 crates/languages/src/c/config.toml delete mode 100644 crates/languages/src/c/embedding.scm delete mode 100644 crates/languages/src/c/highlights.scm delete mode 100644 crates/languages/src/c/imports.scm delete mode 100644 crates/languages/src/c/indents.scm delete mode 100644 crates/languages/src/c/injections.scm delete mode 100644 crates/languages/src/c/outline.scm delete mode 100644 crates/languages/src/c/overrides.scm delete mode 100644 crates/languages/src/c/runnables.scm delete mode 100644 crates/languages/src/c/textobjects.scm delete mode 100644 crates/languages/src/cpp.rs delete mode 100644 crates/languages/src/cpp/brackets.scm delete mode 100644 crates/languages/src/cpp/config.toml delete mode 100644 crates/languages/src/cpp/embedding.scm delete mode 100644 crates/languages/src/cpp/highlights.scm delete mode 100644 crates/languages/src/cpp/imports.scm delete mode 100644 crates/languages/src/cpp/indents.scm delete mode 100644 crates/languages/src/cpp/injections.scm delete mode 100644 crates/languages/src/cpp/outline.scm delete mode 100644 crates/languages/src/cpp/overrides.scm delete mode 100644 crates/languages/src/cpp/textobjects.scm delete mode 100644 crates/languages/src/css.rs delete mode 100644 crates/languages/src/css/brackets.scm delete mode 100644 crates/languages/src/css/config.toml delete mode 100644 crates/languages/src/css/highlights.scm delete mode 100644 crates/languages/src/css/indents.scm delete mode 100644 crates/languages/src/css/injections.scm delete mode 100644 crates/languages/src/css/outline.scm delete mode 100644 crates/languages/src/css/overrides.scm delete mode 100644 crates/languages/src/css/textobjects.scm delete mode 100644 crates/languages/src/diff/config.toml delete mode 100644 crates/languages/src/diff/highlights.scm delete mode 100644 crates/languages/src/diff/injections.scm delete mode 100644 crates/languages/src/eslint.rs delete mode 100644 crates/languages/src/gitcommit/config.toml delete mode 100644 crates/languages/src/gitcommit/highlights.scm delete mode 100644 crates/languages/src/gitcommit/injections.scm delete mode 100644 crates/languages/src/go.rs delete mode 100644 crates/languages/src/go/brackets.scm delete mode 100644 crates/languages/src/go/config.toml delete mode 100644 crates/languages/src/go/debugger.scm delete mode 100644 crates/languages/src/go/embedding.scm delete mode 100644 crates/languages/src/go/highlights.scm delete mode 100644 crates/languages/src/go/imports.scm delete mode 100644 crates/languages/src/go/indents.scm delete mode 100644 crates/languages/src/go/injections.scm delete mode 100644 crates/languages/src/go/outline.scm delete mode 100644 crates/languages/src/go/overrides.scm delete mode 100644 crates/languages/src/go/runnables.scm delete mode 100644 crates/languages/src/go/textobjects.scm delete mode 100644 crates/languages/src/gomod/config.toml delete mode 100644 crates/languages/src/gomod/highlights.scm delete mode 100644 crates/languages/src/gomod/injections.scm delete mode 100644 crates/languages/src/gomod/structure.scm delete mode 100644 crates/languages/src/gowork/config.toml delete mode 100644 crates/languages/src/gowork/highlights.scm delete mode 100644 crates/languages/src/gowork/injections.scm delete mode 100644 crates/languages/src/javascript/brackets.scm delete mode 100644 crates/languages/src/javascript/config.toml delete mode 100644 crates/languages/src/javascript/contexts.scm delete mode 100644 crates/languages/src/javascript/debugger.scm delete mode 100644 crates/languages/src/javascript/embedding.scm delete mode 100644 crates/languages/src/javascript/highlights.scm delete mode 100644 crates/languages/src/javascript/imports.scm delete mode 100644 crates/languages/src/javascript/indents.scm delete mode 100644 crates/languages/src/javascript/injections.scm delete mode 100644 crates/languages/src/javascript/outline.scm delete mode 100644 crates/languages/src/javascript/overrides.scm delete mode 100644 crates/languages/src/javascript/runnables.scm delete mode 100644 crates/languages/src/javascript/textobjects.scm delete mode 100644 crates/languages/src/jsdoc/brackets.scm delete mode 100644 crates/languages/src/jsdoc/config.toml delete mode 100644 crates/languages/src/jsdoc/highlights.scm delete mode 100644 crates/languages/src/json.rs delete mode 100644 crates/languages/src/json/brackets.scm delete mode 100644 crates/languages/src/json/config.toml delete mode 100644 crates/languages/src/json/embedding.scm delete mode 100644 crates/languages/src/json/highlights.scm delete mode 100644 crates/languages/src/json/indents.scm delete mode 100644 crates/languages/src/json/outline.scm delete mode 100644 crates/languages/src/json/overrides.scm delete mode 100644 crates/languages/src/json/redactions.scm delete mode 100644 crates/languages/src/json/runnables.scm delete mode 100644 crates/languages/src/json/textobjects.scm delete mode 100644 crates/languages/src/jsonc/brackets.scm delete mode 100644 crates/languages/src/jsonc/config.toml delete mode 100644 crates/languages/src/jsonc/embedding.scm delete mode 100644 crates/languages/src/jsonc/highlights.scm delete mode 100644 crates/languages/src/jsonc/indents.scm delete mode 100644 crates/languages/src/jsonc/injections.scm delete mode 100644 crates/languages/src/jsonc/outline.scm delete mode 100644 crates/languages/src/jsonc/overrides.scm delete mode 100644 crates/languages/src/jsonc/redactions.scm delete mode 100644 crates/languages/src/jsonc/textobjects.scm delete mode 100644 crates/languages/src/lib.rs delete mode 100644 crates/languages/src/markdown-inline/config.toml delete mode 100644 crates/languages/src/markdown-inline/highlights.scm delete mode 100644 crates/languages/src/markdown-inline/injections.scm delete mode 100644 crates/languages/src/markdown/brackets.scm delete mode 100644 crates/languages/src/markdown/config.toml delete mode 100644 crates/languages/src/markdown/highlights.scm delete mode 100644 crates/languages/src/markdown/indents.scm delete mode 100644 crates/languages/src/markdown/injections.scm delete mode 100644 crates/languages/src/markdown/outline.scm delete mode 100644 crates/languages/src/markdown/textobjects.scm delete mode 100644 crates/languages/src/package_json.rs delete mode 100644 crates/languages/src/python.rs delete mode 100644 crates/languages/src/python/brackets.scm delete mode 100644 crates/languages/src/python/config.toml delete mode 100644 crates/languages/src/python/debugger.scm delete mode 100644 crates/languages/src/python/embedding.scm delete mode 100644 crates/languages/src/python/highlights.scm delete mode 100644 crates/languages/src/python/imports.scm delete mode 100644 crates/languages/src/python/indents.scm delete mode 100644 crates/languages/src/python/injections.scm delete mode 100644 crates/languages/src/python/outline.scm delete mode 100644 crates/languages/src/python/overrides.scm delete mode 100644 crates/languages/src/python/runnables.scm delete mode 100644 crates/languages/src/python/textobjects.scm delete mode 100644 crates/languages/src/regex/brackets.scm delete mode 100644 crates/languages/src/regex/config.toml delete mode 100644 crates/languages/src/regex/highlights.scm delete mode 100644 crates/languages/src/rust.rs delete mode 100644 crates/languages/src/rust/brackets.scm delete mode 100644 crates/languages/src/rust/config.toml delete mode 100644 crates/languages/src/rust/debugger.scm delete mode 100644 crates/languages/src/rust/embedding.scm delete mode 100644 crates/languages/src/rust/highlights.scm delete mode 100644 crates/languages/src/rust/imports.scm delete mode 100644 crates/languages/src/rust/indents.scm delete mode 100644 crates/languages/src/rust/injections.scm delete mode 100644 crates/languages/src/rust/outline.scm delete mode 100644 crates/languages/src/rust/overrides.scm delete mode 100644 crates/languages/src/rust/runnables.scm delete mode 100644 crates/languages/src/rust/textobjects.scm delete mode 100644 crates/languages/src/tailwind.rs delete mode 100644 crates/languages/src/tsx/brackets.scm delete mode 100644 crates/languages/src/tsx/config.toml delete mode 100644 crates/languages/src/tsx/debugger.scm delete mode 100644 crates/languages/src/tsx/embedding.scm delete mode 100644 crates/languages/src/tsx/highlights.scm delete mode 100644 crates/languages/src/tsx/imports.scm delete mode 100644 crates/languages/src/tsx/indents.scm delete mode 100644 crates/languages/src/tsx/injections.scm delete mode 100644 crates/languages/src/tsx/outline.scm delete mode 100644 crates/languages/src/tsx/overrides.scm delete mode 100644 crates/languages/src/tsx/runnables.scm delete mode 100644 crates/languages/src/tsx/textobjects.scm delete mode 100644 crates/languages/src/typescript.rs delete mode 100644 crates/languages/src/typescript/brackets.scm delete mode 100644 crates/languages/src/typescript/config.toml delete mode 100644 crates/languages/src/typescript/debugger.scm delete mode 100644 crates/languages/src/typescript/embedding.scm delete mode 100644 crates/languages/src/typescript/highlights.scm delete mode 100644 crates/languages/src/typescript/imports.scm delete mode 100644 crates/languages/src/typescript/indents.scm delete mode 100644 crates/languages/src/typescript/injections.scm delete mode 100644 crates/languages/src/typescript/outline.scm delete mode 100644 crates/languages/src/typescript/overrides.scm delete mode 100644 crates/languages/src/typescript/runnables.scm delete mode 100644 crates/languages/src/typescript/textobjects.scm delete mode 100644 crates/languages/src/vtsls.rs delete mode 100644 crates/languages/src/yaml.rs delete mode 100644 crates/languages/src/yaml/brackets.scm delete mode 100644 crates/languages/src/yaml/config.toml delete mode 100644 crates/languages/src/yaml/highlights.scm delete mode 100644 crates/languages/src/yaml/injections.scm delete mode 100644 crates/languages/src/yaml/outline.scm delete mode 100644 crates/languages/src/yaml/overrides.scm delete mode 100644 crates/languages/src/yaml/redactions.scm delete mode 100644 crates/languages/src/yaml/textobjects.scm delete mode 100644 crates/languages/src/zed-keybind-context/brackets.scm delete mode 100644 crates/languages/src/zed-keybind-context/config.toml delete mode 100644 crates/languages/src/zed-keybind-context/highlights.scm delete mode 100644 crates/line_ending_selector/Cargo.toml delete mode 120000 crates/line_ending_selector/LICENSE-GPL delete mode 100644 crates/line_ending_selector/src/line_ending_indicator.rs delete mode 100644 crates/line_ending_selector/src/line_ending_selector.rs delete mode 100644 crates/livekit_api/Cargo.toml delete mode 120000 crates/livekit_api/LICENSE-GPL delete mode 100644 crates/livekit_api/build.rs delete mode 100644 crates/livekit_api/src/livekit_api.rs delete mode 100644 crates/livekit_api/src/proto.rs delete mode 100644 crates/livekit_api/src/token.rs delete mode 100644 crates/livekit_api/vendored/protocol/README.md delete mode 100644 crates/livekit_api/vendored/protocol/livekit_analytics.proto delete mode 100644 crates/livekit_api/vendored/protocol/livekit_egress.proto delete mode 100644 crates/livekit_api/vendored/protocol/livekit_ingress.proto delete mode 100644 crates/livekit_api/vendored/protocol/livekit_internal.proto delete mode 100644 crates/livekit_api/vendored/protocol/livekit_models.proto delete mode 100644 crates/livekit_api/vendored/protocol/livekit_room.proto delete mode 100644 crates/livekit_api/vendored/protocol/livekit_rpc_internal.proto delete mode 100644 crates/livekit_api/vendored/protocol/livekit_rtc.proto delete mode 100644 crates/livekit_api/vendored/protocol/livekit_webhook.proto delete mode 100644 crates/livekit_client/Cargo.toml delete mode 120000 crates/livekit_client/LICENSE-GPL delete mode 100644 crates/livekit_client/examples/test_app.rs delete mode 100644 crates/livekit_client/src/lib.rs delete mode 100644 crates/livekit_client/src/livekit_client.rs delete mode 100644 crates/livekit_client/src/livekit_client/playback.rs delete mode 100644 crates/livekit_client/src/livekit_client/playback/source.rs delete mode 100644 crates/livekit_client/src/mock_client.rs delete mode 100644 crates/livekit_client/src/mock_client/participant.rs delete mode 100644 crates/livekit_client/src/mock_client/publication.rs delete mode 100644 crates/livekit_client/src/mock_client/track.rs delete mode 100644 crates/livekit_client/src/record.rs delete mode 100644 crates/livekit_client/src/remote_video_track_view.rs delete mode 100644 crates/livekit_client/src/test.rs delete mode 100644 crates/lmstudio/Cargo.toml delete mode 120000 crates/lmstudio/LICENSE-GPL delete mode 100644 crates/lmstudio/src/lmstudio.rs delete mode 100644 crates/lsp/Cargo.toml delete mode 120000 crates/lsp/LICENSE-GPL delete mode 100644 crates/lsp/src/input_handler.rs delete mode 100644 crates/lsp/src/lsp.rs delete mode 100644 crates/markdown/Cargo.toml delete mode 120000 crates/markdown/LICENSE-GPL delete mode 100644 crates/markdown/examples/markdown.rs delete mode 100644 crates/markdown/examples/markdown_as_child.rs delete mode 100644 crates/markdown/src/markdown.rs delete mode 100644 crates/markdown/src/parser.rs delete mode 100644 crates/markdown/src/path_range.rs delete mode 100644 crates/markdown_preview/Cargo.toml delete mode 120000 crates/markdown_preview/LICENSE-GPL delete mode 100644 crates/markdown_preview/src/markdown_elements.rs delete mode 100644 crates/markdown_preview/src/markdown_minifier.rs delete mode 100644 crates/markdown_preview/src/markdown_parser.rs delete mode 100644 crates/markdown_preview/src/markdown_preview.rs delete mode 100644 crates/markdown_preview/src/markdown_preview_view.rs delete mode 100644 crates/markdown_preview/src/markdown_renderer.rs delete mode 100644 crates/menu/Cargo.toml delete mode 120000 crates/menu/LICENSE-GPL delete mode 100644 crates/menu/src/menu.rs delete mode 100644 crates/migrator/Cargo.toml delete mode 120000 crates/migrator/LICENSE-GPL delete mode 100644 crates/migrator/src/migrations.rs delete mode 100644 crates/migrator/src/migrations/m_2025_01_02/settings.rs delete mode 100644 crates/migrator/src/migrations/m_2025_01_29/keymap.rs delete mode 100644 crates/migrator/src/migrations/m_2025_01_29/settings.rs delete mode 100644 crates/migrator/src/migrations/m_2025_01_30/keymap.rs delete mode 100644 crates/migrator/src/migrations/m_2025_01_30/settings.rs delete mode 100644 crates/migrator/src/migrations/m_2025_03_03/keymap.rs delete mode 100644 crates/migrator/src/migrations/m_2025_03_06/keymap.rs delete mode 100644 crates/migrator/src/migrations/m_2025_03_29/settings.rs delete mode 100644 crates/migrator/src/migrations/m_2025_04_15/keymap.rs delete mode 100644 crates/migrator/src/migrations/m_2025_04_15/settings.rs delete mode 100644 crates/migrator/src/migrations/m_2025_04_21/settings.rs delete mode 100644 crates/migrator/src/migrations/m_2025_04_23/settings.rs delete mode 100644 crates/migrator/src/migrations/m_2025_05_05/settings.rs delete mode 100644 crates/migrator/src/migrations/m_2025_05_08/settings.rs delete mode 100644 crates/migrator/src/migrations/m_2025_05_29/settings.rs delete mode 100644 crates/migrator/src/migrations/m_2025_06_16/settings.rs delete mode 100644 crates/migrator/src/migrations/m_2025_06_25/settings.rs delete mode 100644 crates/migrator/src/migrations/m_2025_06_27/settings.rs delete mode 100644 crates/migrator/src/migrations/m_2025_07_08/settings.rs delete mode 100644 crates/migrator/src/migrations/m_2025_10_01/settings.rs delete mode 100644 crates/migrator/src/migrations/m_2025_10_02/settings.rs delete mode 100644 crates/migrator/src/migrations/m_2025_10_03/settings.rs delete mode 100644 crates/migrator/src/migrations/m_2025_10_16/settings.rs delete mode 100644 crates/migrator/src/migrations/m_2025_10_17/settings.rs delete mode 100644 crates/migrator/src/migrations/m_2025_10_21/settings.rs delete mode 100644 crates/migrator/src/migrations/m_2025_11_12/settings.rs delete mode 100644 crates/migrator/src/migrations/m_2025_11_20/settings.rs delete mode 100644 crates/migrator/src/migrations/m_2025_11_25/settings.rs delete mode 100644 crates/migrator/src/migrations/m_2025_12_01/settings.rs delete mode 100644 crates/migrator/src/migrator.rs delete mode 100644 crates/migrator/src/patterns.rs delete mode 100644 crates/migrator/src/patterns/keymap.rs delete mode 100644 crates/migrator/src/patterns/settings.rs delete mode 100644 crates/miniprofiler_ui/Cargo.toml delete mode 120000 crates/miniprofiler_ui/LICENSE-GPL delete mode 100644 crates/miniprofiler_ui/src/miniprofiler_ui.rs delete mode 100644 crates/mistral/Cargo.toml delete mode 120000 crates/mistral/LICENSE-GPL delete mode 100644 crates/mistral/src/mistral.rs delete mode 100644 crates/multi_buffer/Cargo.toml delete mode 120000 crates/multi_buffer/LICENSE-GPL delete mode 100644 crates/multi_buffer/src/anchor.rs delete mode 100644 crates/multi_buffer/src/multi_buffer.rs delete mode 100644 crates/multi_buffer/src/multi_buffer_tests.rs delete mode 100644 crates/multi_buffer/src/path_key.rs delete mode 100644 crates/multi_buffer/src/transaction.rs delete mode 100644 crates/nc/Cargo.toml delete mode 120000 crates/nc/LICENSE-GPL delete mode 100644 crates/nc/src/nc.rs delete mode 100644 crates/net/Cargo.toml delete mode 120000 crates/net/LICENSE-GPL delete mode 100644 crates/net/src/async_net.rs delete mode 100644 crates/net/src/listener.rs delete mode 100644 crates/net/src/net.rs delete mode 100644 crates/net/src/socket.rs delete mode 100644 crates/net/src/stream.rs delete mode 100644 crates/net/src/util.rs delete mode 100644 crates/node_runtime/Cargo.toml delete mode 120000 crates/node_runtime/LICENSE-GPL delete mode 100644 crates/node_runtime/src/node_runtime.rs delete mode 100644 crates/notifications/Cargo.toml delete mode 120000 crates/notifications/LICENSE-GPL delete mode 100644 crates/notifications/src/notification_store.rs delete mode 100644 crates/notifications/src/notifications.rs delete mode 100644 crates/notifications/src/status_toast.rs delete mode 100644 crates/ollama/Cargo.toml delete mode 120000 crates/ollama/LICENSE-GPL delete mode 100644 crates/ollama/src/ollama.rs delete mode 100644 crates/onboarding/Cargo.toml delete mode 120000 crates/onboarding/LICENSE-GPL delete mode 100644 crates/onboarding/src/base_keymap_picker.rs delete mode 100644 crates/onboarding/src/basics_page.rs delete mode 100644 crates/onboarding/src/multibuffer_hint.rs delete mode 100644 crates/onboarding/src/onboarding.rs delete mode 100644 crates/onboarding/src/theme_preview.rs delete mode 100644 crates/onboarding/src/welcome.rs delete mode 100644 crates/open_ai/Cargo.toml delete mode 120000 crates/open_ai/LICENSE-GPL delete mode 100644 crates/open_ai/src/open_ai.rs delete mode 100644 crates/open_router/Cargo.toml delete mode 120000 crates/open_router/LICENSE-GPL delete mode 100644 crates/open_router/src/open_router.rs delete mode 100644 crates/outline/Cargo.toml delete mode 120000 crates/outline/LICENSE-GPL delete mode 100644 crates/outline/src/outline.rs delete mode 100644 crates/outline_panel/Cargo.toml delete mode 120000 crates/outline_panel/LICENSE-GPL delete mode 100644 crates/outline_panel/src/outline_panel.rs delete mode 100644 crates/outline_panel/src/outline_panel_settings.rs delete mode 100644 crates/panel/Cargo.toml delete mode 120000 crates/panel/LICENSE-GPL delete mode 100644 crates/panel/src/panel.rs delete mode 100644 crates/paths/Cargo.toml delete mode 120000 crates/paths/LICENSE-GPL delete mode 100644 crates/paths/src/paths.rs delete mode 100644 crates/picker/Cargo.toml delete mode 120000 crates/picker/LICENSE-GPL delete mode 100644 crates/picker/src/head.rs delete mode 100644 crates/picker/src/highlighted_match_with_paths.rs delete mode 100644 crates/picker/src/picker.rs delete mode 100644 crates/picker/src/popover_menu.rs delete mode 100644 crates/prettier/Cargo.toml delete mode 120000 crates/prettier/LICENSE-GPL delete mode 100644 crates/prettier/src/prettier.rs delete mode 100644 crates/prettier/src/prettier_server.js delete mode 100644 crates/project/Cargo.toml delete mode 120000 crates/project/LICENSE-GPL delete mode 100644 crates/project/src/agent_server_store.rs delete mode 100644 crates/project/src/buffer_store.rs delete mode 100644 crates/project/src/color_extractor.rs delete mode 100644 crates/project/src/connection_manager.rs delete mode 100644 crates/project/src/context_server_store.rs delete mode 100644 crates/project/src/context_server_store/extension.rs delete mode 100644 crates/project/src/context_server_store/registry.rs delete mode 100644 crates/project/src/debounced_delay.rs delete mode 100644 crates/project/src/debugger.rs delete mode 100644 crates/project/src/debugger/breakpoint_store.rs delete mode 100644 crates/project/src/debugger/dap_command.rs delete mode 100644 crates/project/src/debugger/dap_store.rs delete mode 100644 crates/project/src/debugger/locators.rs delete mode 100644 crates/project/src/debugger/locators/cargo.rs delete mode 100644 crates/project/src/debugger/locators/go.rs delete mode 100644 crates/project/src/debugger/locators/node.rs delete mode 100644 crates/project/src/debugger/locators/python.rs delete mode 100644 crates/project/src/debugger/memory.rs delete mode 100644 crates/project/src/debugger/session.rs delete mode 100644 crates/project/src/debugger/test.rs delete mode 100644 crates/project/src/environment.rs delete mode 100644 crates/project/src/git_store.rs delete mode 100644 crates/project/src/git_store/branch_diff.rs delete mode 100644 crates/project/src/git_store/conflict_set.rs delete mode 100644 crates/project/src/git_store/git_traversal.rs delete mode 100644 crates/project/src/git_store/pending_op.rs delete mode 100644 crates/project/src/image_store.rs delete mode 100644 crates/project/src/lsp_command.rs delete mode 100644 crates/project/src/lsp_command/signature_help.rs delete mode 100644 crates/project/src/lsp_store.rs delete mode 100644 crates/project/src/lsp_store/clangd_ext.rs delete mode 100644 crates/project/src/lsp_store/inlay_hint_cache.rs delete mode 100644 crates/project/src/lsp_store/json_language_server_ext.rs delete mode 100644 crates/project/src/lsp_store/log_store.rs delete mode 100644 crates/project/src/lsp_store/lsp_ext_command.rs delete mode 100644 crates/project/src/lsp_store/rust_analyzer_ext.rs delete mode 100644 crates/project/src/lsp_store/vue_language_server_ext.rs delete mode 100644 crates/project/src/manifest_tree.rs delete mode 100644 crates/project/src/manifest_tree/manifest_store.rs delete mode 100644 crates/project/src/manifest_tree/path_trie.rs delete mode 100644 crates/project/src/manifest_tree/server_tree.rs delete mode 100644 crates/project/src/prettier_store.rs delete mode 100644 crates/project/src/project.rs delete mode 100644 crates/project/src/project_search.rs delete mode 100644 crates/project/src/project_settings.rs delete mode 100644 crates/project/src/project_tests.rs delete mode 100644 crates/project/src/search.rs delete mode 100644 crates/project/src/search_history.rs delete mode 100644 crates/project/src/task_inventory.rs delete mode 100644 crates/project/src/task_store.rs delete mode 100644 crates/project/src/telemetry_snapshot.rs delete mode 100644 crates/project/src/terminals.rs delete mode 100644 crates/project/src/toolchain_store.rs delete mode 100644 crates/project/src/worktree_store.rs delete mode 100644 crates/project/src/yarn.rs delete mode 100644 crates/project_benchmarks/Cargo.toml delete mode 120000 crates/project_benchmarks/LICENSE-GPL delete mode 100644 crates/project_benchmarks/src/main.rs delete mode 100644 crates/project_panel/Cargo.toml delete mode 120000 crates/project_panel/LICENSE-GPL delete mode 100644 crates/project_panel/benches/linux_repo_snapshot.txt delete mode 100644 crates/project_panel/benches/sorting.rs delete mode 100644 crates/project_panel/src/project_panel.rs delete mode 100644 crates/project_panel/src/project_panel_settings.rs delete mode 100644 crates/project_panel/src/project_panel_tests.rs delete mode 100644 crates/project_panel/src/utils.rs delete mode 100644 crates/project_symbols/Cargo.toml delete mode 120000 crates/project_symbols/LICENSE-GPL delete mode 100644 crates/project_symbols/src/project_symbols.rs delete mode 100644 crates/prompt_store/Cargo.toml delete mode 120000 crates/prompt_store/LICENSE-GPL delete mode 100644 crates/prompt_store/src/prompt_store.rs delete mode 100644 crates/prompt_store/src/prompts.rs delete mode 100644 crates/proto/Cargo.toml delete mode 120000 crates/proto/LICENSE-GPL delete mode 100644 crates/proto/build.rs delete mode 100644 crates/proto/proto/ai.proto delete mode 100644 crates/proto/proto/app.proto delete mode 100644 crates/proto/proto/buf.yaml delete mode 100644 crates/proto/proto/buffer.proto delete mode 100644 crates/proto/proto/call.proto delete mode 100644 crates/proto/proto/channel.proto delete mode 100644 crates/proto/proto/core.proto delete mode 100644 crates/proto/proto/debugger.proto delete mode 100644 crates/proto/proto/git.proto delete mode 100644 crates/proto/proto/image.proto delete mode 100644 crates/proto/proto/lsp.proto delete mode 100644 crates/proto/proto/notification.proto delete mode 100644 crates/proto/proto/task.proto delete mode 100644 crates/proto/proto/toolchain.proto delete mode 100644 crates/proto/proto/worktree.proto delete mode 100644 crates/proto/proto/zed.proto delete mode 100644 crates/proto/src/error.rs delete mode 100644 crates/proto/src/macros.rs delete mode 100644 crates/proto/src/proto.rs delete mode 100644 crates/proto/src/typed_envelope.rs delete mode 100644 crates/recent_projects/Cargo.toml delete mode 120000 crates/recent_projects/LICENSE-GPL delete mode 100644 crates/recent_projects/src/dev_container.rs delete mode 100644 crates/recent_projects/src/dev_container_suggest.rs delete mode 100644 crates/recent_projects/src/disconnected_overlay.rs delete mode 100644 crates/recent_projects/src/recent_projects.rs delete mode 100644 crates/recent_projects/src/remote_connections.rs delete mode 100644 crates/recent_projects/src/remote_servers.rs delete mode 100644 crates/recent_projects/src/ssh_config.rs delete mode 100644 crates/recent_projects/src/wsl_picker.rs delete mode 100644 crates/release_channel/Cargo.toml delete mode 120000 crates/release_channel/LICENSE-GPL delete mode 100644 crates/release_channel/src/lib.rs delete mode 100644 crates/remote/Cargo.toml delete mode 120000 crates/remote/LICENSE-GPL delete mode 100644 crates/remote/src/json_log.rs delete mode 100644 crates/remote/src/protocol.rs delete mode 100644 crates/remote/src/proxy.rs delete mode 100644 crates/remote/src/remote.rs delete mode 100644 crates/remote/src/remote_client.rs delete mode 100644 crates/remote/src/transport.rs delete mode 100644 crates/remote/src/transport/docker.rs delete mode 100644 crates/remote/src/transport/ssh.rs delete mode 100644 crates/remote/src/transport/wsl.rs delete mode 100644 crates/remote_server/Cargo.toml delete mode 120000 crates/remote_server/LICENSE-GPL delete mode 100644 crates/remote_server/build.rs delete mode 100644 crates/remote_server/src/headless_project.rs delete mode 100644 crates/remote_server/src/main.rs delete mode 100644 crates/remote_server/src/remote_editing_tests.rs delete mode 100644 crates/remote_server/src/remote_server.rs delete mode 100644 crates/remote_server/src/unix.rs delete mode 100644 crates/repl/Cargo.toml delete mode 120000 crates/repl/LICENSE-GPL delete mode 100644 crates/repl/src/components.rs delete mode 100644 crates/repl/src/components/kernel_list_item.rs delete mode 100644 crates/repl/src/components/kernel_options.rs delete mode 100644 crates/repl/src/jupyter_settings.rs delete mode 100644 crates/repl/src/kernels/mod.rs delete mode 100644 crates/repl/src/kernels/native_kernel.rs delete mode 100644 crates/repl/src/kernels/remote_kernels.rs delete mode 100644 crates/repl/src/notebook.rs delete mode 100644 crates/repl/src/notebook/cell.rs delete mode 100644 crates/repl/src/notebook/notebook_ui.rs delete mode 100644 crates/repl/src/outputs.rs delete mode 100644 crates/repl/src/outputs/image.rs delete mode 100644 crates/repl/src/outputs/markdown.rs delete mode 100644 crates/repl/src/outputs/plain.rs delete mode 100644 crates/repl/src/outputs/table.rs delete mode 100644 crates/repl/src/outputs/user_error.rs delete mode 100644 crates/repl/src/repl.rs delete mode 100644 crates/repl/src/repl_editor.rs delete mode 100644 crates/repl/src/repl_sessions_ui.rs delete mode 100644 crates/repl/src/repl_settings.rs delete mode 100644 crates/repl/src/repl_store.rs delete mode 100644 crates/repl/src/session.rs delete mode 100644 crates/rich_text/Cargo.toml delete mode 120000 crates/rich_text/LICENSE-GPL delete mode 100644 crates/rich_text/src/rich_text.rs delete mode 100644 crates/rope/Cargo.toml delete mode 120000 crates/rope/LICENSE-GPL delete mode 100644 crates/rope/benches/rope_benchmark.rs delete mode 100644 crates/rope/src/chunk.rs delete mode 100644 crates/rope/src/offset_utf16.rs delete mode 100644 crates/rope/src/point.rs delete mode 100644 crates/rope/src/point_utf16.rs delete mode 100644 crates/rope/src/rope.rs delete mode 100644 crates/rope/src/unclipped.rs delete mode 100644 crates/rpc/Cargo.toml delete mode 120000 crates/rpc/LICENSE-GPL delete mode 100644 crates/rpc/src/auth.rs delete mode 100644 crates/rpc/src/conn.rs delete mode 100644 crates/rpc/src/extension.rs delete mode 100644 crates/rpc/src/macros.rs delete mode 100644 crates/rpc/src/message_stream.rs delete mode 100644 crates/rpc/src/notification.rs delete mode 100644 crates/rpc/src/peer.rs delete mode 100644 crates/rpc/src/proto_client.rs delete mode 100644 crates/rpc/src/rpc.rs delete mode 100644 crates/rules_library/Cargo.toml delete mode 120000 crates/rules_library/LICENSE-GPL delete mode 100644 crates/rules_library/src/rules_library.rs delete mode 100644 crates/schema_generator/Cargo.toml delete mode 120000 crates/schema_generator/LICENSE-GPL delete mode 100644 crates/schema_generator/README.md delete mode 100644 crates/schema_generator/src/main.rs delete mode 100644 crates/search/Cargo.toml delete mode 120000 crates/search/LICENSE-GPL delete mode 100644 crates/search/src/buffer_search.rs delete mode 100644 crates/search/src/buffer_search/registrar.rs delete mode 100644 crates/search/src/project_search.rs delete mode 100644 crates/search/src/search.rs delete mode 100644 crates/search/src/search_bar.rs delete mode 100644 crates/search/src/search_status_button.rs delete mode 100644 crates/session/Cargo.toml delete mode 120000 crates/session/LICENSE-GPL delete mode 100644 crates/session/src/session.rs delete mode 100644 crates/settings/Cargo.toml delete mode 120000 crates/settings/LICENSE-GPL delete mode 100644 crates/settings/src/base_keymap_setting.rs delete mode 100644 crates/settings/src/editable_setting_control.rs delete mode 100644 crates/settings/src/fallible_options.rs delete mode 100644 crates/settings/src/keymap_file.rs delete mode 100644 crates/settings/src/merge_from.rs delete mode 100644 crates/settings/src/serde_helper.rs delete mode 100644 crates/settings/src/settings.rs delete mode 100644 crates/settings/src/settings_content.rs delete mode 100644 crates/settings/src/settings_content/agent.rs delete mode 100644 crates/settings/src/settings_content/editor.rs delete mode 100644 crates/settings/src/settings_content/extension.rs delete mode 100644 crates/settings/src/settings_content/language.rs delete mode 100644 crates/settings/src/settings_content/language_model.rs delete mode 100644 crates/settings/src/settings_content/project.rs delete mode 100644 crates/settings/src/settings_content/terminal.rs delete mode 100644 crates/settings/src/settings_content/theme.rs delete mode 100644 crates/settings/src/settings_content/workspace.rs delete mode 100644 crates/settings/src/settings_file.rs delete mode 100644 crates/settings/src/settings_store.rs delete mode 100644 crates/settings/src/vscode_import.rs delete mode 100644 crates/settings_json/Cargo.toml delete mode 120000 crates/settings_json/LICENSE-GPL delete mode 100644 crates/settings_json/src/settings_json.rs delete mode 100644 crates/settings_macros/Cargo.toml delete mode 120000 crates/settings_macros/LICENSE-GPL delete mode 100644 crates/settings_macros/src/settings_macros.rs delete mode 100644 crates/settings_profile_selector/Cargo.toml delete mode 120000 crates/settings_profile_selector/LICENSE-GPL delete mode 100644 crates/settings_profile_selector/src/settings_profile_selector.rs delete mode 100644 crates/settings_ui/Cargo.toml delete mode 120000 crates/settings_ui/LICENSE-GPL delete mode 100644 crates/settings_ui/src/components.rs delete mode 100644 crates/settings_ui/src/components/dropdown.rs delete mode 100644 crates/settings_ui/src/components/font_picker.rs delete mode 100644 crates/settings_ui/src/components/icon_theme_picker.rs delete mode 100644 crates/settings_ui/src/components/input_field.rs delete mode 100644 crates/settings_ui/src/components/theme_picker.rs delete mode 100644 crates/settings_ui/src/page_data.rs delete mode 100644 crates/settings_ui/src/settings_ui.rs delete mode 100644 crates/snippet/Cargo.toml delete mode 120000 crates/snippet/LICENSE-GPL delete mode 100644 crates/snippet/src/snippet.rs delete mode 100644 crates/snippet_provider/Cargo.toml delete mode 120000 crates/snippet_provider/LICENSE-GPL delete mode 100644 crates/snippet_provider/src/extension_snippet.rs delete mode 100644 crates/snippet_provider/src/format.rs delete mode 100644 crates/snippet_provider/src/lib.rs delete mode 100644 crates/snippet_provider/src/registry.rs delete mode 100644 crates/snippets_ui/Cargo.toml delete mode 120000 crates/snippets_ui/LICENSE-GPL delete mode 100644 crates/snippets_ui/src/snippets_ui.rs delete mode 100644 crates/sqlez/.gitignore delete mode 100644 crates/sqlez/Cargo.toml delete mode 120000 crates/sqlez/LICENSE-GPL delete mode 100644 crates/sqlez/src/bindable.rs delete mode 100644 crates/sqlez/src/connection.rs delete mode 100644 crates/sqlez/src/domain.rs delete mode 100644 crates/sqlez/src/lib.rs delete mode 100644 crates/sqlez/src/migrations.rs delete mode 100644 crates/sqlez/src/savepoint.rs delete mode 100644 crates/sqlez/src/statement.rs delete mode 100644 crates/sqlez/src/thread_safe_connection.rs delete mode 100644 crates/sqlez/src/typed_statements.rs delete mode 100644 crates/sqlez/src/util.rs delete mode 100644 crates/sqlez_macros/Cargo.toml delete mode 120000 crates/sqlez_macros/LICENSE-GPL delete mode 100644 crates/sqlez_macros/src/sqlez_macros.rs delete mode 100644 crates/story/Cargo.toml delete mode 120000 crates/story/LICENSE-GPL delete mode 100644 crates/story/src/story.rs delete mode 100644 crates/storybook/Cargo.toml delete mode 120000 crates/storybook/LICENSE-GPL delete mode 100644 crates/storybook/build.rs delete mode 100644 crates/storybook/docs/thoughts.md delete mode 100644 crates/storybook/src/actions.rs delete mode 100644 crates/storybook/src/app_menus.rs delete mode 100644 crates/storybook/src/assets.rs delete mode 100644 crates/storybook/src/stories.rs delete mode 100644 crates/storybook/src/stories/auto_height_editor.rs delete mode 100644 crates/storybook/src/stories/cursor.rs delete mode 100644 crates/storybook/src/stories/focus.rs delete mode 100644 crates/storybook/src/stories/indent_guides.rs delete mode 100644 crates/storybook/src/stories/kitchen_sink.rs delete mode 100644 crates/storybook/src/stories/overflow_scroll.rs delete mode 100644 crates/storybook/src/stories/picker.rs delete mode 100644 crates/storybook/src/stories/scroll.rs delete mode 100644 crates/storybook/src/stories/text.rs delete mode 100644 crates/storybook/src/stories/viewport_units.rs delete mode 100644 crates/storybook/src/stories/with_rem_size.rs delete mode 100644 crates/storybook/src/story_selector.rs delete mode 100644 crates/storybook/src/storybook.rs delete mode 100644 crates/streaming_diff/Cargo.toml delete mode 120000 crates/streaming_diff/LICENSE-GPL delete mode 100644 crates/streaming_diff/src/streaming_diff.rs delete mode 100644 crates/supermaven/Cargo.toml delete mode 120000 crates/supermaven/LICENSE-GPL delete mode 100644 crates/supermaven/src/messages.rs delete mode 100644 crates/supermaven/src/supermaven.rs delete mode 100644 crates/supermaven/src/supermaven_edit_prediction_delegate.rs delete mode 100644 crates/supermaven_api/Cargo.toml delete mode 120000 crates/supermaven_api/LICENSE-GPL delete mode 100644 crates/supermaven_api/src/supermaven_api.rs delete mode 100644 crates/svg_preview/Cargo.toml delete mode 120000 crates/svg_preview/LICENSE-GPL delete mode 100644 crates/svg_preview/src/svg_preview.rs delete mode 100644 crates/svg_preview/src/svg_preview_view.rs delete mode 100644 crates/system_specs/Cargo.toml delete mode 120000 crates/system_specs/LICENSE-GPL delete mode 100644 crates/system_specs/src/system_specs.rs delete mode 100644 crates/tab_switcher/Cargo.toml delete mode 120000 crates/tab_switcher/LICENSE-GPL delete mode 100644 crates/tab_switcher/src/tab_switcher.rs delete mode 100644 crates/tab_switcher/src/tab_switcher_tests.rs delete mode 100644 crates/task/Cargo.toml delete mode 120000 crates/task/LICENSE-GPL delete mode 100644 crates/task/src/adapter_schema.rs delete mode 100644 crates/task/src/debug_format.rs delete mode 100644 crates/task/src/serde_helpers.rs delete mode 100644 crates/task/src/static_source.rs delete mode 100644 crates/task/src/task.rs delete mode 100644 crates/task/src/task_template.rs delete mode 100644 crates/task/src/vscode_debug_format.rs delete mode 100644 crates/task/src/vscode_format.rs delete mode 100644 crates/task/test_data/rust-analyzer.json delete mode 100644 crates/task/test_data/typescript.json delete mode 100644 crates/tasks_ui/Cargo.toml delete mode 120000 crates/tasks_ui/LICENSE-GPL delete mode 100644 crates/tasks_ui/src/modal.rs delete mode 100644 crates/tasks_ui/src/tasks_ui.rs delete mode 100644 crates/telemetry/Cargo.toml delete mode 120000 crates/telemetry/LICENSE-GPL delete mode 100644 crates/telemetry/src/telemetry.rs delete mode 100644 crates/telemetry_events/Cargo.toml delete mode 120000 crates/telemetry_events/LICENSE-GPL delete mode 100644 crates/telemetry_events/src/telemetry_events.rs delete mode 100644 crates/terminal/Cargo.toml delete mode 120000 crates/terminal/LICENSE-GPL delete mode 100644 crates/terminal/src/mappings/colors.rs delete mode 100644 crates/terminal/src/mappings/keys.rs delete mode 100644 crates/terminal/src/mappings/mod.rs delete mode 100644 crates/terminal/src/mappings/mouse.rs delete mode 100644 crates/terminal/src/pty_info.rs delete mode 100644 crates/terminal/src/terminal.rs delete mode 100644 crates/terminal/src/terminal_hyperlinks.rs delete mode 100644 crates/terminal/src/terminal_settings.rs delete mode 100644 crates/terminal_view/Cargo.toml delete mode 120000 crates/terminal_view/LICENSE-GPL delete mode 100644 crates/terminal_view/README.md delete mode 100755 crates/terminal_view/scripts/print256color.sh delete mode 100755 crates/terminal_view/scripts/truecolor.sh delete mode 100644 crates/terminal_view/src/persistence.rs delete mode 100644 crates/terminal_view/src/terminal_element.rs delete mode 100644 crates/terminal_view/src/terminal_panel.rs delete mode 100644 crates/terminal_view/src/terminal_path_like_target.rs delete mode 100644 crates/terminal_view/src/terminal_scrollbar.rs delete mode 100644 crates/terminal_view/src/terminal_slash_command.rs delete mode 100644 crates/terminal_view/src/terminal_view.rs delete mode 100644 crates/text/Cargo.toml delete mode 120000 crates/text/LICENSE-GPL delete mode 100644 crates/text/src/anchor.rs delete mode 100644 crates/text/src/locator.rs delete mode 100644 crates/text/src/network.rs delete mode 100644 crates/text/src/operation_queue.rs delete mode 100644 crates/text/src/patch.rs delete mode 100644 crates/text/src/selection.rs delete mode 100644 crates/text/src/subscription.rs delete mode 100644 crates/text/src/tests.rs delete mode 100644 crates/text/src/text.rs delete mode 100644 crates/text/src/undo_map.rs delete mode 100644 crates/theme/Cargo.toml delete mode 120000 crates/theme/LICENSE-GPL delete mode 100644 crates/theme/src/default_colors.rs delete mode 100644 crates/theme/src/fallback_themes.rs delete mode 100644 crates/theme/src/font_family_cache.rs delete mode 100644 crates/theme/src/icon_theme.rs delete mode 100644 crates/theme/src/icon_theme_schema.rs delete mode 100644 crates/theme/src/registry.rs delete mode 100644 crates/theme/src/scale.rs delete mode 100644 crates/theme/src/schema.rs delete mode 100644 crates/theme/src/settings.rs delete mode 100644 crates/theme/src/styles.rs delete mode 100644 crates/theme/src/styles/accents.rs delete mode 100644 crates/theme/src/styles/colors.rs delete mode 100644 crates/theme/src/styles/players.rs delete mode 100644 crates/theme/src/styles/status.rs delete mode 100644 crates/theme/src/styles/syntax.rs delete mode 100644 crates/theme/src/styles/system.rs delete mode 100644 crates/theme/src/theme.rs delete mode 100644 crates/theme_extension/Cargo.toml delete mode 120000 crates/theme_extension/LICENSE-GPL delete mode 100644 crates/theme_extension/src/theme_extension.rs delete mode 100644 crates/theme_importer/Cargo.toml delete mode 120000 crates/theme_importer/LICENSE-GPL delete mode 100644 crates/theme_importer/README.md delete mode 100644 crates/theme_importer/src/color.rs delete mode 100644 crates/theme_importer/src/main.rs delete mode 100644 crates/theme_importer/src/vscode.rs delete mode 100644 crates/theme_importer/src/vscode/converter.rs delete mode 100644 crates/theme_importer/src/vscode/syntax.rs delete mode 100644 crates/theme_importer/src/vscode/theme.rs delete mode 100644 crates/theme_selector/Cargo.toml delete mode 120000 crates/theme_selector/LICENSE-GPL delete mode 100644 crates/theme_selector/src/icon_theme_selector.rs delete mode 100644 crates/theme_selector/src/theme_selector.rs delete mode 100644 crates/time_format/Cargo.toml delete mode 120000 crates/time_format/LICENSE-GPL delete mode 100644 crates/time_format/src/time_format.rs delete mode 100644 crates/title_bar/Cargo.toml delete mode 120000 crates/title_bar/LICENSE-GPL delete mode 100644 crates/title_bar/src/application_menu.rs delete mode 100644 crates/title_bar/src/collab.rs delete mode 100644 crates/title_bar/src/onboarding_banner.rs delete mode 100644 crates/title_bar/src/platform_title_bar.rs delete mode 100644 crates/title_bar/src/platforms.rs delete mode 100644 crates/title_bar/src/platforms/platform_linux.rs delete mode 100644 crates/title_bar/src/platforms/platform_mac.rs delete mode 100644 crates/title_bar/src/platforms/platform_windows.rs delete mode 100644 crates/title_bar/src/stories.rs delete mode 100644 crates/title_bar/src/stories/application_menu.rs delete mode 100644 crates/title_bar/src/system_window_tabs.rs delete mode 100644 crates/title_bar/src/title_bar.rs delete mode 100644 crates/title_bar/src/title_bar_settings.rs delete mode 100644 crates/toolchain_selector/Cargo.toml delete mode 120000 crates/toolchain_selector/LICENSE-GPL delete mode 100644 crates/toolchain_selector/src/active_toolchain.rs delete mode 100644 crates/toolchain_selector/src/toolchain_selector.rs delete mode 100644 crates/ui/Cargo.toml delete mode 120000 crates/ui/LICENSE-GPL delete mode 100644 crates/ui/src/component_prelude.rs delete mode 100644 crates/ui/src/components.rs delete mode 100644 crates/ui/src/components/avatar.rs delete mode 100644 crates/ui/src/components/banner.rs delete mode 100644 crates/ui/src/components/button.rs delete mode 100644 crates/ui/src/components/button/button.rs delete mode 100644 crates/ui/src/components/button/button_icon.rs delete mode 100644 crates/ui/src/components/button/button_like.rs delete mode 100644 crates/ui/src/components/button/icon_button.rs delete mode 100644 crates/ui/src/components/button/split_button.rs delete mode 100644 crates/ui/src/components/button/toggle_button.rs delete mode 100644 crates/ui/src/components/callout.rs delete mode 100644 crates/ui/src/components/chip.rs delete mode 100644 crates/ui/src/components/content_group.rs delete mode 100644 crates/ui/src/components/context_menu.rs delete mode 100644 crates/ui/src/components/data_table.rs delete mode 100644 crates/ui/src/components/diff_stat.rs delete mode 100644 crates/ui/src/components/disclosure.rs delete mode 100644 crates/ui/src/components/divider.rs delete mode 100644 crates/ui/src/components/dropdown_menu.rs delete mode 100644 crates/ui/src/components/facepile.rs delete mode 100644 crates/ui/src/components/group.rs delete mode 100644 crates/ui/src/components/icon.rs delete mode 100644 crates/ui/src/components/icon/decorated_icon.rs delete mode 100644 crates/ui/src/components/icon/icon_decoration.rs delete mode 100644 crates/ui/src/components/image.rs delete mode 100644 crates/ui/src/components/indent_guides.rs delete mode 100644 crates/ui/src/components/indicator.rs delete mode 100644 crates/ui/src/components/keybinding.rs delete mode 100644 crates/ui/src/components/keybinding_hint.rs delete mode 100644 crates/ui/src/components/label.rs delete mode 100644 crates/ui/src/components/label/highlighted_label.rs delete mode 100644 crates/ui/src/components/label/label.rs delete mode 100644 crates/ui/src/components/label/label_like.rs delete mode 100644 crates/ui/src/components/label/loading_label.rs delete mode 100644 crates/ui/src/components/label/spinner_label.rs delete mode 100644 crates/ui/src/components/list.rs delete mode 100644 crates/ui/src/components/list/list.rs delete mode 100644 crates/ui/src/components/list/list_bullet_item.rs delete mode 100644 crates/ui/src/components/list/list_header.rs delete mode 100644 crates/ui/src/components/list/list_item.rs delete mode 100644 crates/ui/src/components/list/list_separator.rs delete mode 100644 crates/ui/src/components/list/list_sub_header.rs delete mode 100644 crates/ui/src/components/modal.rs delete mode 100644 crates/ui/src/components/navigable.rs delete mode 100644 crates/ui/src/components/notification.rs delete mode 100644 crates/ui/src/components/notification/alert_modal.rs delete mode 100644 crates/ui/src/components/popover.rs delete mode 100644 crates/ui/src/components/popover_menu.rs delete mode 100644 crates/ui/src/components/progress.rs delete mode 100644 crates/ui/src/components/progress/progress_bar.rs delete mode 100644 crates/ui/src/components/radio.rs delete mode 100644 crates/ui/src/components/right_click_menu.rs delete mode 100644 crates/ui/src/components/scrollbar.rs delete mode 100644 crates/ui/src/components/settings_container.rs delete mode 100644 crates/ui/src/components/settings_group.rs delete mode 100644 crates/ui/src/components/stack.rs delete mode 100644 crates/ui/src/components/sticky_items.rs delete mode 100644 crates/ui/src/components/stories.rs delete mode 100644 crates/ui/src/components/stories/context_menu.rs delete mode 100644 crates/ui/src/components/tab.rs delete mode 100644 crates/ui/src/components/tab_bar.rs delete mode 100644 crates/ui/src/components/thread_item.rs delete mode 100644 crates/ui/src/components/toggle.rs delete mode 100644 crates/ui/src/components/tooltip.rs delete mode 100644 crates/ui/src/components/tree_view_item.rs delete mode 100644 crates/ui/src/prelude.rs delete mode 100644 crates/ui/src/styles.rs delete mode 100644 crates/ui/src/styles/animation.rs delete mode 100644 crates/ui/src/styles/appearance.rs delete mode 100644 crates/ui/src/styles/color.rs delete mode 100644 crates/ui/src/styles/elevation.rs delete mode 100644 crates/ui/src/styles/platform.rs delete mode 100644 crates/ui/src/styles/severity.rs delete mode 100644 crates/ui/src/styles/spacing.rs delete mode 100644 crates/ui/src/styles/typography.rs delete mode 100644 crates/ui/src/styles/units.rs delete mode 100644 crates/ui/src/traits.rs delete mode 100644 crates/ui/src/traits/animation_ext.rs delete mode 100644 crates/ui/src/traits/clickable.rs delete mode 100644 crates/ui/src/traits/disableable.rs delete mode 100644 crates/ui/src/traits/fixed.rs delete mode 100644 crates/ui/src/traits/styled_ext.rs delete mode 100644 crates/ui/src/traits/toggleable.rs delete mode 100644 crates/ui/src/traits/transformable.rs delete mode 100644 crates/ui/src/traits/visible_on_hover.rs delete mode 100644 crates/ui/src/ui.rs delete mode 100644 crates/ui/src/utils.rs delete mode 100644 crates/ui/src/utils/apca_contrast.rs delete mode 100644 crates/ui/src/utils/color_contrast.rs delete mode 100644 crates/ui/src/utils/corner_solver.rs delete mode 100644 crates/ui/src/utils/format_distance.rs delete mode 100644 crates/ui/src/utils/search_input.rs delete mode 100644 crates/ui/src/utils/with_rem_size.rs delete mode 100644 crates/ui_input/Cargo.toml delete mode 120000 crates/ui_input/LICENSE-GPL delete mode 100644 crates/ui_input/src/input_field.rs delete mode 100644 crates/ui_input/src/number_field.rs delete mode 100644 crates/ui_input/src/ui_input.rs delete mode 100644 crates/ui_macros/Cargo.toml delete mode 120000 crates/ui_macros/LICENSE-GPL delete mode 100644 crates/ui_macros/src/derive_register_component.rs delete mode 100644 crates/ui_macros/src/dynamic_spacing.rs delete mode 100644 crates/ui_macros/src/ui_macros.rs delete mode 100644 crates/ui_prompt/Cargo.toml delete mode 120000 crates/ui_prompt/LICENSE-GPL delete mode 100644 crates/ui_prompt/src/ui_prompt.rs delete mode 100644 crates/vercel/Cargo.toml delete mode 120000 crates/vercel/LICENSE-GPL delete mode 100644 crates/vercel/src/vercel.rs delete mode 100644 crates/vim/Cargo.toml delete mode 120000 crates/vim/LICENSE-GPL delete mode 100644 crates/vim/README.md delete mode 100644 crates/vim/src/change_list.rs delete mode 100644 crates/vim/src/command.rs delete mode 100644 crates/vim/src/digraph.rs delete mode 100644 crates/vim/src/digraph/default.rs delete mode 100644 crates/vim/src/helix.rs delete mode 100644 crates/vim/src/helix/boundary.rs delete mode 100644 crates/vim/src/helix/duplicate.rs delete mode 100644 crates/vim/src/helix/object.rs delete mode 100644 crates/vim/src/helix/paste.rs delete mode 100644 crates/vim/src/helix/select.rs delete mode 100644 crates/vim/src/indent.rs delete mode 100644 crates/vim/src/insert.rs delete mode 100644 crates/vim/src/mode_indicator.rs delete mode 100644 crates/vim/src/motion.rs delete mode 100644 crates/vim/src/normal.rs delete mode 100644 crates/vim/src/normal/change.rs delete mode 100644 crates/vim/src/normal/convert.rs delete mode 100644 crates/vim/src/normal/delete.rs delete mode 100644 crates/vim/src/normal/increment.rs delete mode 100644 crates/vim/src/normal/mark.rs delete mode 100644 crates/vim/src/normal/paste.rs delete mode 100644 crates/vim/src/normal/repeat.rs delete mode 100644 crates/vim/src/normal/scroll.rs delete mode 100644 crates/vim/src/normal/search.rs delete mode 100644 crates/vim/src/normal/substitute.rs delete mode 100644 crates/vim/src/normal/toggle_comments.rs delete mode 100644 crates/vim/src/normal/yank.rs delete mode 100644 crates/vim/src/object.rs delete mode 100644 crates/vim/src/replace.rs delete mode 100644 crates/vim/src/rewrap.rs delete mode 100644 crates/vim/src/state.rs delete mode 100644 crates/vim/src/surrounds.rs delete mode 100644 crates/vim/src/test.rs delete mode 100644 crates/vim/src/test/neovim_backed_test_context.rs delete mode 100644 crates/vim/src/test/neovim_connection.rs delete mode 100644 crates/vim/src/test/vim_test_context.rs delete mode 100644 crates/vim/src/vim.rs delete mode 100644 crates/vim/src/visual.rs delete mode 100644 crates/vim/test_data/neovim_backed_test_context_works.json delete mode 100644 crates/vim/test_data/test_a.json delete mode 100644 crates/vim/test_data/test_around_containing_word_indent.json delete mode 100644 crates/vim/test_data/test_b.json delete mode 100644 crates/vim/test_data/test_backspace.json delete mode 100644 crates/vim/test_data/test_backspace_non_ascii_bol.json delete mode 100644 crates/vim/test_data/test_backwards_n.json delete mode 100644 crates/vim/test_data/test_blackhole_register.json delete mode 100644 crates/vim/test_data/test_builtin_marks.json delete mode 100644 crates/vim/test_data/test_capital_f_and_capital_t.json delete mode 100644 crates/vim/test_data/test_caret_mark.json delete mode 100644 crates/vim/test_data/test_cc.json delete mode 100644 crates/vim/test_data/test_cgn_nomatch.json delete mode 100644 crates/vim/test_data/test_cgn_repeat.json delete mode 100644 crates/vim/test_data/test_change_0.json delete mode 100644 crates/vim/test_data/test_change_b.json delete mode 100644 crates/vim/test_data/test_change_backspace.json delete mode 100644 crates/vim/test_data/test_change_case.json delete mode 100644 crates/vim/test_data/test_change_case_motion.json delete mode 100644 crates/vim/test_data/test_change_case_motion_object.json delete mode 100644 crates/vim/test_data/test_change_cc.json delete mode 100644 crates/vim/test_data/test_change_e.json delete mode 100644 crates/vim/test_data/test_change_end_of_document.json delete mode 100644 crates/vim/test_data/test_change_end_of_line.json delete mode 100644 crates/vim/test_data/test_change_gg.json delete mode 100644 crates/vim/test_data/test_change_h.json delete mode 100644 crates/vim/test_data/test_change_j.json delete mode 100644 crates/vim/test_data/test_change_k.json delete mode 100644 crates/vim/test_data/test_change_l.json delete mode 100644 crates/vim/test_data/test_change_list_delete.json delete mode 100644 crates/vim/test_data/test_change_list_insert.json delete mode 100644 crates/vim/test_data/test_change_paragraph.json delete mode 100644 crates/vim/test_data/test_change_paragraph_object.json delete mode 100644 crates/vim/test_data/test_change_paragraph_object_with_soft_wrap.json delete mode 100644 crates/vim/test_data/test_change_rot13_motion.json delete mode 100644 crates/vim/test_data/test_change_rot13_object.json delete mode 100644 crates/vim/test_data/test_change_sentence_object.json delete mode 100644 crates/vim/test_data/test_change_surrounding_character_objects.json delete mode 100644 crates/vim/test_data/test_change_w.json delete mode 100644 crates/vim/test_data/test_change_word_object.json delete mode 100644 crates/vim/test_data/test_clear_counts.json delete mode 100644 crates/vim/test_data/test_comma_semicolon.json delete mode 100644 crates/vim/test_data/test_comma_w.json delete mode 100644 crates/vim/test_data/test_command_basics.json delete mode 100644 crates/vim/test_data/test_command_goto.json delete mode 100644 crates/vim/test_data/test_command_matching_lines.json delete mode 100644 crates/vim/test_data/test_command_ranges.json delete mode 100644 crates/vim/test_data/test_command_replace.json delete mode 100644 crates/vim/test_data/test_command_search.json delete mode 100644 crates/vim/test_data/test_command_visual_replace.json delete mode 100644 crates/vim/test_data/test_convert_to_lower_case.json delete mode 100644 crates/vim/test_data/test_convert_to_rot13.json delete mode 100644 crates/vim/test_data/test_convert_to_upper_case.json delete mode 100644 crates/vim/test_data/test_ctrl_d_u.json delete mode 100644 crates/vim/test_data/test_ctrl_f_b.json delete mode 100644 crates/vim/test_data/test_ctrl_o_dot.json delete mode 100644 crates/vim/test_data/test_ctrl_o_position.json delete mode 100644 crates/vim/test_data/test_ctrl_o_visual.json delete mode 100644 crates/vim/test_data/test_ctrl_v.json delete mode 100644 crates/vim/test_data/test_ctrl_v_control.json delete mode 100644 crates/vim/test_data/test_ctrl_v_escape.json delete mode 100644 crates/vim/test_data/test_ctrl_w_override.json delete mode 100644 crates/vim/test_data/test_ctrl_y_e.json delete mode 100644 crates/vim/test_data/test_d_search.json delete mode 100644 crates/vim/test_data/test_dd.json delete mode 100644 crates/vim/test_data/test_dd_then_paste_without_trailing_newline.json delete mode 100644 crates/vim/test_data/test_del_marks.json delete mode 100644 crates/vim/test_data/test_delete_0.json delete mode 100644 crates/vim/test_data/test_delete_b.json delete mode 100644 crates/vim/test_data/test_delete_end_of_document.json delete mode 100644 crates/vim/test_data/test_delete_end_of_line.json delete mode 100644 crates/vim/test_data/test_delete_end_of_paragraph.json delete mode 100644 crates/vim/test_data/test_delete_gg.json delete mode 100644 crates/vim/test_data/test_delete_h.json delete mode 100644 crates/vim/test_data/test_delete_j.json delete mode 100644 crates/vim/test_data/test_delete_k.json delete mode 100644 crates/vim/test_data/test_delete_key_can_remove_last_character.json delete mode 100644 crates/vim/test_data/test_delete_l.json delete mode 100644 crates/vim/test_data/test_delete_left.json delete mode 100644 crates/vim/test_data/test_delete_next_word_end.json delete mode 100644 crates/vim/test_data/test_delete_paragraph.json delete mode 100644 crates/vim/test_data/test_delete_paragraph_motion.json delete mode 100644 crates/vim/test_data/test_delete_paragraph_object.json delete mode 100644 crates/vim/test_data/test_delete_paragraph_object_with_soft_wrap.json delete mode 100644 crates/vim/test_data/test_delete_paragraph_whitespace.json delete mode 100644 crates/vim/test_data/test_delete_sentence.json delete mode 100644 crates/vim/test_data/test_delete_sentence_object.json delete mode 100644 crates/vim/test_data/test_delete_surrounding_character_objects.json delete mode 100644 crates/vim/test_data/test_delete_to_adjacent_character.json delete mode 100644 crates/vim/test_data/test_delete_to_end_of_line.json delete mode 100644 crates/vim/test_data/test_delete_to_line.json delete mode 100644 crates/vim/test_data/test_delete_unmatched_brace.json delete mode 100644 crates/vim/test_data/test_delete_w.json delete mode 100644 crates/vim/test_data/test_delete_with_counts.json delete mode 100644 crates/vim/test_data/test_delete_word_object.json delete mode 100644 crates/vim/test_data/test_dgn_repeat.json delete mode 100644 crates/vim/test_data/test_digraph_find.json delete mode 100644 crates/vim/test_data/test_digraph_insert_mode.json delete mode 100644 crates/vim/test_data/test_digraph_insert_multicursor.json delete mode 100644 crates/vim/test_data/test_digraph_keymap_conflict.json delete mode 100644 crates/vim/test_data/test_digraph_replace.json delete mode 100644 crates/vim/test_data/test_digraph_replace_mode.json delete mode 100644 crates/vim/test_data/test_dot_mark.json delete mode 100644 crates/vim/test_data/test_dot_repeat.json delete mode 100644 crates/vim/test_data/test_dw_eol.json delete mode 100644 crates/vim/test_data/test_end_of_document.json delete mode 100644 crates/vim/test_data/test_end_of_line_downward.json delete mode 100644 crates/vim/test_data/test_end_of_line_with_neovim.json delete mode 100644 crates/vim/test_data/test_end_of_word.json delete mode 100644 crates/vim/test_data/test_enter.json delete mode 100644 crates/vim/test_data/test_enter_visual_line_mode.json delete mode 100644 crates/vim/test_data/test_enter_visual_mode.json delete mode 100644 crates/vim/test_data/test_escape_while_waiting.json delete mode 100644 crates/vim/test_data/test_f_and_t.json delete mode 100644 crates/vim/test_data/test_find_multibyte.json delete mode 100644 crates/vim/test_data/test_folds.json delete mode 100644 crates/vim/test_data/test_folds_panic.json delete mode 100644 crates/vim/test_data/test_forced_motion_delete_to_end_of_line.json delete mode 100644 crates/vim/test_data/test_forced_motion_delete_to_middle_of_line.json delete mode 100644 crates/vim/test_data/test_forced_motion_delete_to_start_of_line.json delete mode 100644 crates/vim/test_data/test_forced_motion_yank.json delete mode 100644 crates/vim/test_data/test_gg.json delete mode 100644 crates/vim/test_data/test_gi.json delete mode 100644 crates/vim/test_data/test_gn.json delete mode 100644 crates/vim/test_data/test_go_to_percentage.json delete mode 100644 crates/vim/test_data/test_gq.json delete mode 100644 crates/vim/test_data/test_gv.json delete mode 100644 crates/vim/test_data/test_h.json delete mode 100644 crates/vim/test_data/test_h_through_unicode.json delete mode 100644 crates/vim/test_data/test_horizontal_scroll.json delete mode 100644 crates/vim/test_data/test_inclusive_to_exclusive_delete.json delete mode 100644 crates/vim/test_data/test_increment.json delete mode 100644 crates/vim/test_data/test_increment_bin_wrapping_and_padding.json delete mode 100644 crates/vim/test_data/test_increment_hex_casing.json delete mode 100644 crates/vim/test_data/test_increment_hex_wrapping_and_padding.json delete mode 100644 crates/vim/test_data/test_increment_inline.json delete mode 100644 crates/vim/test_data/test_increment_radix.json delete mode 100644 crates/vim/test_data/test_increment_sign_change.json delete mode 100644 crates/vim/test_data/test_increment_sign_change_with_leading_zeros.json delete mode 100644 crates/vim/test_data/test_increment_steps.json delete mode 100644 crates/vim/test_data/test_increment_visual_partial_number.json delete mode 100644 crates/vim/test_data/test_increment_with_changing_leading_zeros.json delete mode 100644 crates/vim/test_data/test_increment_with_dot.json delete mode 100644 crates/vim/test_data/test_increment_with_leading_zeros.json delete mode 100644 crates/vim/test_data/test_increment_with_leading_zeros_and_zero.json delete mode 100644 crates/vim/test_data/test_increment_with_two_dots.json delete mode 100644 crates/vim/test_data/test_increment_wrapping.json delete mode 100644 crates/vim/test_data/test_increment_zero_leading_zeros.json delete mode 100644 crates/vim/test_data/test_indent_gv.json delete mode 100644 crates/vim/test_data/test_insert_ctrl_r.json delete mode 100644 crates/vim/test_data/test_insert_ctrl_y.json delete mode 100644 crates/vim/test_data/test_insert_empty_line.json delete mode 100644 crates/vim/test_data/test_insert_end_of_line.json delete mode 100644 crates/vim/test_data/test_insert_first_non_whitespace.json delete mode 100644 crates/vim/test_data/test_insert_line_above.json delete mode 100644 crates/vim/test_data/test_insert_with_counts.json delete mode 100644 crates/vim/test_data/test_insert_with_repeat.json delete mode 100644 crates/vim/test_data/test_j.json delete mode 100644 crates/vim/test_data/test_jk.json delete mode 100644 crates/vim/test_data/test_jk_max_count.json delete mode 100644 crates/vim/test_data/test_join_lines.json delete mode 100644 crates/vim/test_data/test_jump_list.json delete mode 100644 crates/vim/test_data/test_jump_to_end.json delete mode 100644 crates/vim/test_data/test_jump_to_first_non_whitespace.json delete mode 100644 crates/vim/test_data/test_jump_to_line_boundaries.json delete mode 100644 crates/vim/test_data/test_k.json delete mode 100644 crates/vim/test_data/test_l.json delete mode 100644 crates/vim/test_data/test_lowercase_marks.json delete mode 100644 crates/vim/test_data/test_lt_gt_marks.json delete mode 100644 crates/vim/test_data/test_marks.json delete mode 100644 crates/vim/test_data/test_matching.json delete mode 100644 crates/vim/test_data/test_matching_braces_in_tag.json delete mode 100644 crates/vim/test_data/test_matching_nested_brackets.json delete mode 100644 crates/vim/test_data/test_matching_tags.json delete mode 100644 crates/vim/test_data/test_minibrackets_trailing_space.json delete mode 100644 crates/vim/test_data/test_named_registers.json delete mode 100644 crates/vim/test_data/test_neovim.json delete mode 100644 crates/vim/test_data/test_next_line_start.json delete mode 100644 crates/vim/test_data/test_next_word_end_newline_last_char.json delete mode 100644 crates/vim/test_data/test_normal_command.json delete mode 100644 crates/vim/test_data/test_numbered_registers.json delete mode 100644 crates/vim/test_data/test_o.json delete mode 100644 crates/vim/test_data/test_o_comment.json delete mode 100644 crates/vim/test_data/test_offsets.json delete mode 100644 crates/vim/test_data/test_p_g_v_y.json delete mode 100644 crates/vim/test_data/test_paragraph_multi_delete.json delete mode 100644 crates/vim/test_data/test_paragraph_object_with_landing_positions_not_at_beginning_of_line.json delete mode 100644 crates/vim/test_data/test_paragraphs_dont_wrap.json delete mode 100644 crates/vim/test_data/test_paste.json delete mode 100644 crates/vim/test_data/test_paste_count.json delete mode 100644 crates/vim/test_data/test_paste_visual.json delete mode 100644 crates/vim/test_data/test_paste_visual_block.json delete mode 100644 crates/vim/test_data/test_percent.json delete mode 100644 crates/vim/test_data/test_period_mark.json delete mode 100644 crates/vim/test_data/test_plus_minus.json delete mode 100644 crates/vim/test_data/test_previous_word_end.json delete mode 100644 crates/vim/test_data/test_quote_mark.json delete mode 100644 crates/vim/test_data/test_r.json delete mode 100644 crates/vim/test_data/test_record_replay.json delete mode 100644 crates/vim/test_data/test_record_replay_count.json delete mode 100644 crates/vim/test_data/test_record_replay_dot.json delete mode 100644 crates/vim/test_data/test_record_replay_interleaved.json delete mode 100644 crates/vim/test_data/test_record_replay_of_dot.json delete mode 100644 crates/vim/test_data/test_record_replay_recursion.json delete mode 100644 crates/vim/test_data/test_remap_adjacent_dog_cat.json delete mode 100644 crates/vim/test_data/test_remap_nested_pineapple.json delete mode 100644 crates/vim/test_data/test_remap_recursion.json delete mode 100644 crates/vim/test_data/test_repeat_clear_count.json delete mode 100644 crates/vim/test_data/test_repeat_clear_repeat.json delete mode 100644 crates/vim/test_data/test_repeat_grouping_41735.json delete mode 100644 crates/vim/test_data/test_repeat_motion_counts.json delete mode 100644 crates/vim/test_data/test_repeat_over_blur.json delete mode 100644 crates/vim/test_data/test_repeat_visual.json delete mode 100644 crates/vim/test_data/test_repeated_cb.json delete mode 100644 crates/vim/test_data/test_repeated_ce.json delete mode 100644 crates/vim/test_data/test_repeated_cj.json delete mode 100644 crates/vim/test_data/test_repeated_cl.json delete mode 100644 crates/vim/test_data/test_repeated_word.json delete mode 100644 crates/vim/test_data/test_replace_g.json delete mode 100644 crates/vim/test_data/test_replace_mode.json delete mode 100644 crates/vim/test_data/test_replace_mode_repeat.json delete mode 100644 crates/vim/test_data/test_replace_mode_undo.json delete mode 100644 crates/vim/test_data/test_replace_mode_with_counts.json delete mode 100644 crates/vim/test_data/test_replace_n.json delete mode 100644 crates/vim/test_data/test_replace_with_range.json delete mode 100644 crates/vim/test_data/test_replace_with_range_at_start.json delete mode 100644 crates/vim/test_data/test_scroll_beyond_last_line.json delete mode 100644 crates/vim/test_data/test_scroll_jumps.json delete mode 100644 crates/vim/test_data/test_search_skipping.json delete mode 100644 crates/vim/test_data/test_selection_goal.json delete mode 100644 crates/vim/test_data/test_sentence_backwards.json delete mode 100644 crates/vim/test_data/test_sentence_forwards.json delete mode 100644 crates/vim/test_data/test_shift_y.json delete mode 100644 crates/vim/test_data/test_singleline_surrounding_character_objects.json delete mode 100644 crates/vim/test_data/test_singleline_surrounding_character_objects_with_escape.json delete mode 100644 crates/vim/test_data/test_space_non_ascii.json delete mode 100644 crates/vim/test_data/test_space_non_ascii_eol.json delete mode 100644 crates/vim/test_data/test_space_only_ascii_eol.json delete mode 100644 crates/vim/test_data/test_special_registers.json delete mode 100644 crates/vim/test_data/test_start_end_of_paragraph.json delete mode 100644 crates/vim/test_data/test_substitute_line.json delete mode 100644 crates/vim/test_data/test_temporary_mode.json delete mode 100644 crates/vim/test_data/test_undo.json delete mode 100644 crates/vim/test_data/test_undo_last_line.json delete mode 100644 crates/vim/test_data/test_undo_last_line_newline.json delete mode 100644 crates/vim/test_data/test_undo_last_line_newline_many_changes.json delete mode 100644 crates/vim/test_data/test_undo_repeated_insert.json delete mode 100644 crates/vim/test_data/test_unmatched_backward.json delete mode 100644 crates/vim/test_data/test_unmatched_backward_markdown.json delete mode 100644 crates/vim/test_data/test_unmatched_forward.json delete mode 100644 crates/vim/test_data/test_unmatched_forward_markdown.json delete mode 100644 crates/vim/test_data/test_v2ap.json delete mode 100644 crates/vim/test_data/test_v_search.json delete mode 100644 crates/vim/test_data/test_v_search_aa.json delete mode 100644 crates/vim/test_data/test_visual_block_insert.json delete mode 100644 crates/vim/test_data/test_visual_block_issue_2123.json delete mode 100644 crates/vim/test_data/test_visual_block_mode.json delete mode 100644 crates/vim/test_data/test_visual_block_mode_down_right.json delete mode 100644 crates/vim/test_data/test_visual_block_mode_other_end.json delete mode 100644 crates/vim/test_data/test_visual_block_mode_shift_other_end.json delete mode 100644 crates/vim/test_data/test_visual_block_mode_up_left.json delete mode 100644 crates/vim/test_data/test_visual_block_search.json delete mode 100644 crates/vim/test_data/test_visual_block_wrapping_selection.json delete mode 100644 crates/vim/test_data/test_visual_change.json delete mode 100644 crates/vim/test_data/test_visual_delete.json delete mode 100644 crates/vim/test_data/test_visual_line_change.json delete mode 100644 crates/vim/test_data/test_visual_line_delete.json delete mode 100644 crates/vim/test_data/test_visual_match_eol.json delete mode 100644 crates/vim/test_data/test_visual_mode_insert_before_after.json delete mode 100644 crates/vim/test_data/test_visual_object.json delete mode 100644 crates/vim/test_data/test_visual_object_expands.json delete mode 100644 crates/vim/test_data/test_visual_paragraph_object.json delete mode 100644 crates/vim/test_data/test_visual_paragraph_object_with_soft_wrap.json delete mode 100644 crates/vim/test_data/test_visual_sentence_object.json delete mode 100644 crates/vim/test_data/test_visual_shift_d.json delete mode 100644 crates/vim/test_data/test_visual_star_hash.json delete mode 100644 crates/vim/test_data/test_visual_word_object.json delete mode 100644 crates/vim/test_data/test_visual_yank.json delete mode 100644 crates/vim/test_data/test_w.json delete mode 100644 crates/vim/test_data/test_window_bottom.json delete mode 100644 crates/vim/test_data/test_window_middle.json delete mode 100644 crates/vim/test_data/test_window_top.json delete mode 100644 crates/vim/test_data/test_wrapped_delete_end_document.json delete mode 100644 crates/vim/test_data/test_wrapped_lines.json delete mode 100644 crates/vim/test_data/test_wrapped_motions.json delete mode 100644 crates/vim/test_data/test_x.json delete mode 100644 crates/vim/test_data/test_yank_line_with_trailing_newline.json delete mode 100644 crates/vim/test_data/test_yank_line_without_trailing_newline.json delete mode 100644 crates/vim/test_data/test_yank_multiline_without_trailing_newline.json delete mode 100644 crates/vim/test_data/test_yank_paragraph_with_paste.json delete mode 100644 crates/vim/test_data/test_zero.json delete mode 100644 crates/vim_mode_setting/Cargo.toml delete mode 120000 crates/vim_mode_setting/LICENSE-GPL delete mode 100644 crates/vim_mode_setting/src/vim_mode_setting.rs delete mode 100644 crates/web_search/Cargo.toml delete mode 120000 crates/web_search/LICENSE-GPL delete mode 100644 crates/web_search/src/web_search.rs delete mode 100644 crates/web_search_providers/Cargo.toml delete mode 120000 crates/web_search_providers/LICENSE-GPL delete mode 100644 crates/web_search_providers/src/cloud.rs delete mode 100644 crates/web_search_providers/src/web_search_providers.rs delete mode 100644 crates/workspace/Cargo.toml delete mode 120000 crates/workspace/LICENSE-GPL delete mode 100644 crates/workspace/src/dock.rs delete mode 100644 crates/workspace/src/history_manager.rs delete mode 100644 crates/workspace/src/invalid_item_view.rs delete mode 100644 crates/workspace/src/item.rs delete mode 100644 crates/workspace/src/modal_layer.rs delete mode 100644 crates/workspace/src/notifications.rs delete mode 100644 crates/workspace/src/pane.rs delete mode 100644 crates/workspace/src/pane_group.rs delete mode 100644 crates/workspace/src/path_list.rs delete mode 100644 crates/workspace/src/persistence.rs delete mode 100644 crates/workspace/src/persistence/model.rs delete mode 100644 crates/workspace/src/searchable.rs delete mode 100644 crates/workspace/src/shared_screen.rs delete mode 100644 crates/workspace/src/status_bar.rs delete mode 100644 crates/workspace/src/tasks.rs delete mode 100644 crates/workspace/src/theme_preview.rs delete mode 100644 crates/workspace/src/toast_layer.rs delete mode 100644 crates/workspace/src/toolbar.rs delete mode 100644 crates/workspace/src/workspace.rs delete mode 100644 crates/workspace/src/workspace_settings.rs delete mode 100644 crates/worktree/Cargo.toml delete mode 120000 crates/worktree/LICENSE-GPL delete mode 100644 crates/worktree/src/ignore.rs delete mode 100644 crates/worktree/src/worktree.rs delete mode 100644 crates/worktree/src/worktree_settings.rs delete mode 100644 crates/worktree/src/worktree_tests.rs delete mode 100644 crates/worktree_benchmarks/Cargo.toml delete mode 120000 crates/worktree_benchmarks/LICENSE-GPL delete mode 100644 crates/worktree_benchmarks/src/main.rs delete mode 100644 crates/x_ai/Cargo.toml delete mode 120000 crates/x_ai/LICENSE-GPL delete mode 100644 crates/x_ai/src/x_ai.rs delete mode 100644 crates/zed/Cargo.toml delete mode 120000 crates/zed/LICENSE-GPL delete mode 100644 crates/zed/RELEASE_CHANNEL delete mode 100644 crates/zed/build.rs delete mode 100644 crates/zed/contents/dev/embedded.provisionprofile delete mode 100644 crates/zed/contents/nightly/embedded.provisionprofile delete mode 100644 crates/zed/contents/preview/embedded.provisionprofile delete mode 100644 crates/zed/contents/stable/embedded.provisionprofile delete mode 100644 crates/zed/resources/app-icon-dev.png delete mode 100644 crates/zed/resources/app-icon-dev@2x.png delete mode 100644 crates/zed/resources/app-icon-nightly.png delete mode 100644 crates/zed/resources/app-icon-nightly@2x.png delete mode 100644 crates/zed/resources/app-icon-preview.png delete mode 100644 crates/zed/resources/app-icon-preview@2x.png delete mode 100644 crates/zed/resources/app-icon.png delete mode 100644 crates/zed/resources/app-icon@2x.png delete mode 100644 crates/zed/resources/flatpak/manifest-template.json delete mode 100644 crates/zed/resources/flatpak/release-info/dev delete mode 100644 crates/zed/resources/flatpak/release-info/nightly delete mode 100644 crates/zed/resources/flatpak/release-info/preview delete mode 100644 crates/zed/resources/flatpak/release-info/stable delete mode 100644 crates/zed/resources/flatpak/zed.metainfo.xml.in delete mode 100644 crates/zed/resources/info/DocumentTypes.plist delete mode 100644 crates/zed/resources/info/Permissions.plist delete mode 100644 crates/zed/resources/info/SupportedPlatforms.plist delete mode 100644 crates/zed/resources/snap/snapcraft.yaml.in delete mode 100644 crates/zed/resources/windows/app-icon-dev.ico delete mode 100644 crates/zed/resources/windows/app-icon-nightly.ico delete mode 100644 crates/zed/resources/windows/app-icon-preview.ico delete mode 100644 crates/zed/resources/windows/app-icon.ico delete mode 100644 crates/zed/resources/windows/bin/x64/OpenConsole.exe delete mode 100644 crates/zed/resources/windows/bin/x64/conpty.dll delete mode 100644 crates/zed/resources/windows/messages/Default.zh-cn.isl delete mode 100644 crates/zed/resources/windows/messages/en.isl delete mode 100644 crates/zed/resources/windows/messages/zh-cn.isl delete mode 100644 crates/zed/resources/windows/sign.ps1 delete mode 100644 crates/zed/resources/windows/zed.iss delete mode 100644 crates/zed/resources/windows/zed.sh delete mode 100644 crates/zed/resources/zed.desktop.in delete mode 100644 crates/zed/resources/zed.entitlements delete mode 100644 crates/zed/src/main.rs delete mode 100644 crates/zed/src/reliability.rs delete mode 100644 crates/zed/src/zed-main.rs delete mode 100644 crates/zed/src/zed.rs delete mode 100644 crates/zed/src/zed/app_menus.rs delete mode 100644 crates/zed/src/zed/component_preview.rs delete mode 100644 crates/zed/src/zed/component_preview/persistence.rs delete mode 100644 crates/zed/src/zed/edit_prediction_registry.rs delete mode 100644 crates/zed/src/zed/mac_only_instance.rs delete mode 100644 crates/zed/src/zed/migrate.rs delete mode 100644 crates/zed/src/zed/open_listener.rs delete mode 100644 crates/zed/src/zed/quick_action_bar.rs delete mode 100644 crates/zed/src/zed/quick_action_bar/preview.rs delete mode 100644 crates/zed/src/zed/quick_action_bar/repl_menu.rs delete mode 100644 crates/zed/src/zed/windows_only_instance.rs delete mode 100644 crates/zed_actions/Cargo.toml delete mode 120000 crates/zed_actions/LICENSE-GPL delete mode 100644 crates/zed_actions/src/lib.rs delete mode 100644 crates/zed_env_vars/Cargo.toml delete mode 120000 crates/zed_env_vars/LICENSE-GPL delete mode 100644 crates/zed_env_vars/src/zed_env_vars.rs delete mode 100644 crates/zeta_prompt/Cargo.toml delete mode 120000 crates/zeta_prompt/LICENSE-GPL delete mode 100644 crates/zeta_prompt/src/zeta_prompt.rs delete mode 100644 crates/zlog/Cargo.toml delete mode 120000 crates/zlog/LICENSE-GPL delete mode 100644 crates/zlog/README.md delete mode 100644 crates/zlog/src/env_config.rs delete mode 100644 crates/zlog/src/filter.rs delete mode 100644 crates/zlog/src/sink.rs delete mode 100644 crates/zlog/src/zlog.rs delete mode 100644 crates/zlog_settings/Cargo.toml delete mode 120000 crates/zlog_settings/LICENSE-GPL delete mode 100644 crates/zlog_settings/src/zlog_settings.rs delete mode 100644 crates/ztracing/Cargo.toml delete mode 120000 crates/ztracing/LICENSE-AGPL delete mode 120000 crates/ztracing/LICENSE-APACHE delete mode 120000 crates/ztracing/LICENSE-GPL delete mode 100644 crates/ztracing/build.rs delete mode 100644 crates/ztracing/src/lib.rs delete mode 100644 crates/ztracing_macro/Cargo.toml delete mode 120000 crates/ztracing_macro/LICENSE-AGPL delete mode 120000 crates/ztracing_macro/LICENSE-APACHE delete mode 120000 crates/ztracing_macro/LICENSE-GPL delete mode 100644 crates/ztracing_macro/src/lib.rs delete mode 100644 docker-compose.sql delete mode 100644 extensions/.gitignore delete mode 100644 extensions/EXTRACTION.md delete mode 100644 extensions/README.md delete mode 100644 extensions/glsl/Cargo.toml delete mode 120000 extensions/glsl/LICENSE-APACHE delete mode 100644 extensions/glsl/extension.toml delete mode 100644 extensions/glsl/languages/glsl/config.toml delete mode 100644 extensions/glsl/languages/glsl/highlights.scm delete mode 100644 extensions/glsl/src/glsl.rs delete mode 100644 extensions/html/Cargo.toml delete mode 120000 extensions/html/LICENSE-APACHE delete mode 100644 extensions/html/extension.toml delete mode 100644 extensions/html/languages/html/brackets.scm delete mode 100644 extensions/html/languages/html/config.toml delete mode 100644 extensions/html/languages/html/highlights.scm delete mode 100644 extensions/html/languages/html/indents.scm delete mode 100644 extensions/html/languages/html/injections.scm delete mode 100644 extensions/html/languages/html/outline.scm delete mode 100644 extensions/html/languages/html/overrides.scm delete mode 100644 extensions/html/src/html.rs delete mode 100644 extensions/proto/Cargo.toml delete mode 120000 extensions/proto/LICENSE-APACHE delete mode 100644 extensions/proto/extension.toml delete mode 100644 extensions/proto/languages/proto/config.toml delete mode 100644 extensions/proto/languages/proto/highlights.scm delete mode 100644 extensions/proto/languages/proto/indents.scm delete mode 100644 extensions/proto/languages/proto/outline.scm delete mode 100644 extensions/proto/languages/proto/textobjects.scm delete mode 100644 extensions/proto/src/proto.rs delete mode 100644 extensions/slash-commands-example/Cargo.toml delete mode 120000 extensions/slash-commands-example/LICENSE-APACHE delete mode 100644 extensions/slash-commands-example/README.md delete mode 100644 extensions/slash-commands-example/extension.toml delete mode 100644 extensions/slash-commands-example/src/slash_commands_example.rs delete mode 100644 extensions/test-extension/Cargo.toml delete mode 120000 extensions/test-extension/LICENSE-APACHE delete mode 100644 extensions/test-extension/README.md delete mode 100644 extensions/test-extension/extension.toml delete mode 100644 extensions/test-extension/languages/gleam/config.toml delete mode 100644 extensions/test-extension/languages/gleam/highlights.scm delete mode 100644 extensions/test-extension/languages/gleam/indents.scm delete mode 100644 extensions/test-extension/languages/gleam/outline.scm delete mode 100644 extensions/test-extension/src/test_extension.rs delete mode 100644 extensions/workflows/bump_version.yml delete mode 100644 extensions/workflows/release_version.yml delete mode 100644 extensions/workflows/run_tests.yml delete mode 100644 livekit.yaml delete mode 100644 tooling/xtask/Cargo.toml delete mode 120000 tooling/xtask/LICENSE-GPL delete mode 100644 tooling/xtask/src/main.rs delete mode 100644 tooling/xtask/src/tasks.rs delete mode 100644 tooling/xtask/src/tasks/clippy.rs delete mode 100644 tooling/xtask/src/tasks/licenses.rs delete mode 100644 tooling/xtask/src/tasks/package_conformity.rs delete mode 100644 tooling/xtask/src/tasks/publish_gpui.rs delete mode 100644 tooling/xtask/src/tasks/workflows.rs delete mode 100644 tooling/xtask/src/tasks/workflows/after_release.rs delete mode 100644 tooling/xtask/src/tasks/workflows/cherry_pick.rs delete mode 100644 tooling/xtask/src/tasks/workflows/compare_perf.rs delete mode 100644 tooling/xtask/src/tasks/workflows/danger.rs delete mode 100644 tooling/xtask/src/tasks/workflows/extension_bump.rs delete mode 100644 tooling/xtask/src/tasks/workflows/extension_release.rs delete mode 100644 tooling/xtask/src/tasks/workflows/extension_tests.rs delete mode 100644 tooling/xtask/src/tasks/workflows/extensions/bump_version.rs delete mode 100644 tooling/xtask/src/tasks/workflows/extensions/mod.rs delete mode 100644 tooling/xtask/src/tasks/workflows/extensions/release_version.rs delete mode 100644 tooling/xtask/src/tasks/workflows/extensions/run_tests.rs delete mode 100644 tooling/xtask/src/tasks/workflows/nix_build.rs delete mode 100644 tooling/xtask/src/tasks/workflows/release.rs delete mode 100644 tooling/xtask/src/tasks/workflows/release_nightly.rs delete mode 100644 tooling/xtask/src/tasks/workflows/run_agent_evals.rs delete mode 100644 tooling/xtask/src/tasks/workflows/run_bundling.rs delete mode 100644 tooling/xtask/src/tasks/workflows/run_tests.rs delete mode 100644 tooling/xtask/src/tasks/workflows/runners.rs delete mode 100644 tooling/xtask/src/tasks/workflows/steps.rs delete mode 100644 tooling/xtask/src/tasks/workflows/vars.rs delete mode 100644 tooling/xtask/src/workspace.rs diff --git a/Cargo.lock b/Cargo.lock index 981f59cb5e..1ae85aa6ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,121 +2,13 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "acp_thread" -version = "0.1.0" -dependencies = [ - "action_log", - "agent-client-protocol", - "agent_settings", - "anyhow", - "buffer_diff", - "collections", - "editor", - "env_logger 0.11.8", - "file_icons", - "futures 0.3.31", - "gpui", - "indoc", - "itertools 0.14.0", - "language", - "language_model", - "markdown", - "parking_lot", - "portable-pty", - "project", - "prompt_store", - "rand 0.9.2", - "serde", - "serde_json", - "settings", - "smol", - "task", - "telemetry", - "tempfile", - "terminal", - "ui", - "url", - "util", - "uuid", - "watch", - "zlog", -] - -[[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", -] - -[[package]] -name = "action_log" -version = "0.1.0" -dependencies = [ - "anyhow", - "buffer_diff", - "clock", - "collections", - "ctor", - "futures 0.3.31", - "gpui", - "indoc", - "language", - "log", - "pretty_assertions", - "project", - "rand 0.9.2", - "serde_json", - "settings", - "telemetry", - "text", - "util", - "watch", - "zlog", -] - -[[package]] -name = "activity_indicator" -version = "0.1.0" -dependencies = [ - "anyhow", - "auto_update", - "editor", - "extension_host", - "fs", - "futures 0.3.31", - "gpui", - "language", - "project", - "proto", - "release_channel", - "semver", - "smallvec", - "ui", - "util", - "workspace", -] - [[package]] name = "addr2line" version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" dependencies = [ - "gimli 0.32.3", + "gimli", ] [[package]] @@ -137,287 +29,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "agent" -version = "0.1.0" -dependencies = [ - "acp_thread", - "action_log", - "agent-client-protocol", - "agent_servers", - "agent_settings", - "anyhow", - "assistant_text_thread", - "chrono", - "client", - "clock", - "cloud_llm_client", - "collections", - "context_server", - "ctor", - "db", - "derive_more 0.99.20", - "editor", - "env_logger 0.11.8", - "eval_utils", - "fs", - "futures 0.3.31", - "git", - "gpui", - "gpui_tokio", - "handlebars 4.5.0", - "html_to_markdown", - "http_client", - "indoc", - "itertools 0.14.0", - "language", - "language_model", - "language_models", - "log", - "lsp", - "open", - "parking_lot", - "paths", - "pretty_assertions", - "project", - "prompt_store", - "rand 0.9.2", - "regex", - "reqwest_client", - "rust-embed", - "schemars", - "serde", - "serde_json", - "settings", - "smallvec", - "smol", - "sqlez", - "streaming_diff", - "strsim", - "task", - "telemetry", - "tempfile", - "terminal", - "text", - "theme", - "thiserror 2.0.17", - "tree-sitter-rust", - "ui", - "unindent", - "util", - "uuid", - "watch", - "web_search", - "worktree", - "zed_env_vars", - "zlog", - "zstd", -] - -[[package]] -name = "agent-client-protocol" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ffe7d502c1e451aafc5aff655000f84d09c9af681354ac0012527009b1af13" -dependencies = [ - "agent-client-protocol-schema", - "anyhow", - "async-broadcast", - "async-trait", - "derive_more 2.0.1", - "futures 0.3.31", - "log", - "serde", - "serde_json", -] - -[[package]] -name = "agent-client-protocol-schema" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8af81cc2d5c3f9c04f73db452efd058333735ba9d51c2cf7ef33c9fee038e7e6" -dependencies = [ - "anyhow", - "derive_more 2.0.1", - "schemars", - "serde", - "serde_json", - "strum 0.27.2", -] - -[[package]] -name = "agent_servers" -version = "0.1.0" -dependencies = [ - "acp_thread", - "acp_tools", - "action_log", - "agent-client-protocol", - "anyhow", - "async-trait", - "client", - "collections", - "env_logger 0.11.8", - "fs", - "futures 0.3.31", - "gpui", - "gpui_tokio", - "http_client", - "indoc", - "language", - "language_model", - "language_models", - "libc", - "log", - "nix 0.29.0", - "project", - "release_channel", - "reqwest_client", - "serde", - "serde_json", - "settings", - "smol", - "task", - "tempfile", - "terminal", - "thiserror 2.0.17", - "ui", - "util", - "uuid", - "watch", -] - -[[package]] -name = "agent_settings" -version = "0.1.0" -dependencies = [ - "anyhow", - "cloud_llm_client", - "collections", - "convert_case 0.8.0", - "fs", - "gpui", - "language_model", - "paths", - "project", - "schemars", - "serde", - "serde_json", - "serde_json_lenient", - "settings", - "util", -] - -[[package]] -name = "agent_ui" -version = "0.1.0" -dependencies = [ - "acp_thread", - "action_log", - "agent", - "agent-client-protocol", - "agent_servers", - "agent_settings", - "ai_onboarding", - "anyhow", - "arrayvec", - "assistant_slash_command", - "assistant_slash_commands", - "assistant_text_thread", - "async-fs", - "audio", - "buffer_diff", - "chrono", - "client", - "clock", - "cloud_llm_client", - "collections", - "command_palette_hooks", - "component", - "context_server", - "db", - "editor", - "eval_utils", - "extension", - "extension_host", - "feature_flags", - "file_icons", - "fs", - "futures 0.3.31", - "fuzzy", - "gpui", - "gpui_tokio", - "html_to_markdown", - "http_client", - "image", - "indoc", - "itertools 0.14.0", - "jsonschema", - "language", - "language_model", - "language_models", - "languages", - "log", - "lsp", - "markdown", - "menu", - "multi_buffer", - "notifications", - "ordered-float 2.10.1", - "parking_lot", - "paths", - "picker", - "postage", - "pretty_assertions", - "project", - "prompt_store", - "proto", - "rand 0.9.2", - "release_channel", - "reqwest_client", - "rope", - "rules_library", - "schemars", - "search", - "semver", - "serde", - "serde_json", - "serde_json_lenient", - "settings", - "smol", - "streaming_diff", - "task", - "telemetry", - "telemetry_events", - "terminal", - "terminal_view", - "text", - "theme", - "time", - "time_format", - "tree-sitter-md", - "ui", - "ui_input", - "unindent", - "url", - "util", - "uuid", - "watch", - "workspace", - "zed_actions", -] - -[[package]] -name = "ahash" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" -dependencies = [ - "getrandom 0.2.16", - "once_cell", - "version_check", -] - [[package]] name = "ahash" version = "0.8.12" @@ -426,9 +37,7 @@ checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" dependencies = [ "cfg-if", "const-random", - "getrandom 0.3.4", "once_cell", - "serde", "version_check", "zerocopy", ] @@ -442,53 +51,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "ai_onboarding" -version = "0.1.0" -dependencies = [ - "client", - "cloud_llm_client", - "component", - "gpui", - "language_model", - "serde", - "smallvec", - "telemetry", - "ui", - "zed_actions", -] - -[[package]] -name = "alacritty_terminal" -version = "0.25.1-rc1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cb5f4f1ef69bdb8b2095ddd14b09dd74ee0303aae8bd5372667a54cff689a1b" -dependencies = [ - "base64 0.22.1", - "bitflags 2.9.4", - "home", - "libc", - "log", - "miow", - "parking_lot", - "piper", - "polling", - "regex-automata", - "rustix 1.1.2", - "rustix-openpty", - "serde", - "signal-hook", - "unicode-width", - "vte", - "windows-sys 0.59.0", -] - -[[package]] -name = "aliasable" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" - [[package]] name = "aligned-vec" version = "0.6.4" @@ -498,68 +60,6 @@ dependencies = [ "equator", ] -[[package]] -name = "alloc-no-stdlib" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" - -[[package]] -name = "alloc-stdlib" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" -dependencies = [ - "alloc-no-stdlib", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "alsa" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed7572b7ba83a31e20d1b48970ee402d2e3e0537dcfe0a3ff4d6eb7508617d43" -dependencies = [ - "alsa-sys", - "bitflags 2.9.4", - "cfg-if", - "libc", -] - -[[package]] -name = "alsa-sys" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8fee663d06c4e303404ef5f40488a53e062f89ba8bfed81f42325aafad1527" -dependencies = [ - "libc", - "pkg-config", -] - -[[package]] -name = "ambient-authority" -version = "0.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9d4ee0d472d1cd2e28c97dfa124b3d8d992e10eb0a035f33f5d12e3a177ba3b" - -[[package]] -name = "ammonia" -version = "4.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17e913097e1a2124b46746c980134e8c954bc17a6a59bb3fde96f088d126dde6" -dependencies = [ - "cssparser", - "html5ever 0.35.0", - "maplit", - "tendril", - "url", -] - [[package]] name = "android_system_properties" version = "0.1.5" @@ -569,12 +69,6 @@ dependencies = [ "libc", ] -[[package]] -name = "anes" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" - [[package]] name = "anstream" version = "0.6.21" @@ -625,51 +119,17 @@ dependencies = [ "windows-sys 0.60.2", ] -[[package]] -name = "anthropic" -version = "0.1.0" -dependencies = [ - "anyhow", - "chrono", - "futures 0.3.31", - "http_client", - "schemars", - "serde", - "serde_json", - "settings", - "strum 0.27.2", - "thiserror 2.0.17", -] - -[[package]] -name = "any_vec" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34cd60c5e3152cef0a592f1b296f1cc93715d89d2551d85315828c3a09575ff4" - [[package]] name = "anyhow" version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" -[[package]] -name = "approx" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" -dependencies = [ - "num-traits", -] - [[package]] name = "arbitrary" version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" -dependencies = [ - "derive_arbitrary", -] [[package]] name = "arg_enum_proc_macro" @@ -682,12 +142,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "arraydeque" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" - [[package]] name = "arrayref" version = "0.3.9" @@ -699,9 +153,6 @@ name = "arrayvec" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" -dependencies = [ - "serde", -] [[package]] name = "as-raw-xcb-connection" @@ -709,12 +160,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" -[[package]] -name = "ascii" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d92bec98840b8f03a5ff5413de5293bfcd8bf96467cf5452609f939ec6f5de16" - [[package]] name = "ash" version = "0.38.0+1.3.281" @@ -774,146 +219,6 @@ dependencies = [ "zbus", ] -[[package]] -name = "askpass" -version = "0.1.0" -dependencies = [ - "anyhow", - "futures 0.3.31", - "gpui", - "log", - "net", - "smol", - "tempfile", - "util", - "windows 0.61.3", - "zeroize", -] - -[[package]] -name = "assets" -version = "0.1.0" -dependencies = [ - "anyhow", - "gpui", - "rust-embed", -] - -[[package]] -name = "assistant_slash_command" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "collections", - "derive_more 0.99.20", - "extension", - "futures 0.3.31", - "gpui", - "language", - "language_model", - "parking_lot", - "pretty_assertions", - "serde", - "serde_json", - "ui", - "util", - "workspace", -] - -[[package]] -name = "assistant_slash_commands" -version = "0.1.0" -dependencies = [ - "anyhow", - "assistant_slash_command", - "chrono", - "collections", - "context_server", - "editor", - "feature_flags", - "fs", - "futures 0.3.31", - "fuzzy", - "globset", - "gpui", - "html_to_markdown", - "http_client", - "language", - "pretty_assertions", - "project", - "prompt_store", - "rope", - "serde", - "serde_json", - "settings", - "smol", - "text", - "ui", - "util", - "workspace", - "worktree", - "zlog", -] - -[[package]] -name = "assistant_text_thread" -version = "0.1.0" -dependencies = [ - "agent_settings", - "anyhow", - "assistant_slash_command", - "assistant_slash_commands", - "chrono", - "client", - "clock", - "cloud_llm_client", - "collections", - "context_server", - "fs", - "futures 0.3.31", - "fuzzy", - "gpui", - "indoc", - "itertools 0.14.0", - "language", - "language_model", - "log", - "open_ai", - "parking_lot", - "paths", - "pretty_assertions", - "project", - "prompt_store", - "proto", - "rand 0.9.2", - "regex", - "rpc", - "serde", - "serde_json", - "settings", - "smallvec", - "smol", - "telemetry_events", - "text", - "ui", - "unindent", - "util", - "uuid", - "workspace", - "zed_env_vars", -] - -[[package]] -name = "async-attributes" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" -dependencies = [ - "quote", - "syn 1.0.109", -] - [[package]] name = "async-broadcast" version = "0.7.2" @@ -949,19 +254,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "async-compat" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1ba85bc55464dcbf728b56d97e119d673f4cf9062be330a9a26f3acf504a590" -dependencies = [ - "futures-core", - "futures-io", - "once_cell", - "pin-project-lite", - "tokio", -] - [[package]] name = "async-compression" version = "0.4.32" @@ -975,16 +267,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "async-dispatcher" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8bff43baa5b0ca8f8bcd7f9338f5d30fbd75236a2aa89130a7c5121a06d6ca" -dependencies = [ - "async-task", - "futures-lite 1.13.0", -] - [[package]] name = "async-executor" version = "1.13.3" @@ -1005,7 +287,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" dependencies = [ - "async-lock 3.4.1", + "async-lock", "blocking", "futures-lite 2.6.1", ] @@ -1019,7 +301,7 @@ dependencies = [ "async-channel 2.5.0", "async-executor", "async-io", - "async-lock 3.4.1", + "async-lock", "blocking", "futures-lite 2.6.1", "once_cell", @@ -1043,15 +325,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "async-lock" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" -dependencies = [ - "event-listener 2.5.3", -] - [[package]] name = "async-lock" version = "3.4.1" @@ -1074,15 +347,6 @@ dependencies = [ "futures-lite 2.6.1", ] -[[package]] -name = "async-pipe" -version = "0.1.3" -source = "git+https://github.com/zed-industries/async-pipe-rs?rev=82d00a04211cf4e1236029aa03e6b6ce2a74c553#82d00a04211cf4e1236029aa03e6b6ce2a74c553" -dependencies = [ - "futures 0.3.31", - "log", -] - [[package]] name = "async-process" version = "2.5.0" @@ -1091,7 +355,7 @@ checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" dependencies = [ "async-channel 2.5.0", "async-io", - "async-lock 3.4.1", + "async-lock", "async-signal", "async-task", "blocking", @@ -1119,7 +383,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43c070bbf59cd3570b6b2dd54cd772527c7c3620fce8be898406dd3ed6adc64c" dependencies = [ "async-io", - "async-lock 3.4.1", + "async-lock", "atomic-waker", "cfg-if", "futures-core", @@ -1136,11 +400,10 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c8e079a4ab67ae52b7403632e4618815d6db36d2a010cfe41b02c1b1578f93b" dependencies = [ - "async-attributes", "async-channel 1.9.0", "async-global-executor", "async-io", - "async-lock 3.4.1", + "async-lock", "async-process", "crossbeam-utils", "futures-channel", @@ -1158,28 +421,6 @@ dependencies = [ "wasm-bindgen-futures", ] -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "async-tar" version = "0.5.1" @@ -1211,25 +452,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "async-tungstenite" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee88b4c88ac8c9ea446ad43498955750a4bbe64c4392f21ccfe5d952865e318f" -dependencies = [ - "atomic-waker", - "futures-core", - "futures-io", - "futures-task", - "futures-util", - "log", - "pin-project-lite", - "rustls-pki-types", - "tokio", - "tokio-rustls 0.26.2", - "tungstenite 0.27.0", -] - [[package]] name = "async_zip" version = "0.0.18" @@ -1243,28 +465,6 @@ dependencies = [ "thiserror 2.0.17", ] -[[package]] -name = "asynchronous-codec" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a860072022177f903e59730004fb5dc13db9275b79bb2aef7ba8ce831956c233" -dependencies = [ - "bytes 1.10.1", - "futures-sink", - "futures-util", - "memchr", - "pin-project-lite", -] - -[[package]] -name = "atoi" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" -dependencies = [ - "num-traits", -] - [[package]] name = "atomic" version = "0.5.3" @@ -1277,27 +477,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "audio" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-tar", - "collections", - "crossbeam", - "denoise", - "gpui", - "libwebrtc", - "log", - "parking_lot", - "rodio", - "serde", - "settings", - "smol", - "thiserror 2.0.17", - "util", -] - [[package]] name = "auditable-serde" version = "0.8.0" @@ -1310,66 +489,6 @@ dependencies = [ "topological-sort", ] -[[package]] -name = "auto_update" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "clock", - "ctor", - "db", - "futures 0.3.31", - "gpui", - "http_client", - "log", - "parking_lot", - "paths", - "release_channel", - "semver", - "serde", - "serde_json", - "settings", - "smol", - "tempfile", - "util", - "which 6.0.3", - "workspace", - "zlog", -] - -[[package]] -name = "auto_update_helper" -version = "0.1.0" -dependencies = [ - "anyhow", - "log", - "simplelog", - "tempfile", - "windows 0.61.3", - "winresource", -] - -[[package]] -name = "auto_update_ui" -version = "0.1.0" -dependencies = [ - "anyhow", - "auto_update", - "client", - "editor", - "gpui", - "http_client", - "markdown_preview", - "release_channel", - "semver", - "serde", - "serde_json", - "smol", - "util", - "workspace", -] - [[package]] name = "autocfg" version = "1.5.0" @@ -1399,48 +518,6 @@ dependencies = [ "arrayvec", ] -[[package]] -name = "aws-config" -version = "1.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37cf2b6af2a95a20e266782b4f76f1a5e12bf412a9db2de9c1e9123b9d8c0ad8" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-sdk-sso", - "aws-sdk-ssooidc", - "aws-sdk-sts", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes 1.10.1", - "fastrand 2.3.0", - "hex", - "http 1.3.1", - "ring", - "time", - "tokio", - "tracing", - "url", - "zeroize", -] - -[[package]] -name = "aws-credential-types" -version = "1.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faf26925f4a5b59eb76722b63c2892b1d70d06fa053c72e4a100ec308c1d47bc" -dependencies = [ - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "zeroize", -] - [[package]] name = "aws-lc-rs" version = "1.14.1" @@ -1448,7 +525,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879b6c89592deb404ba4dc0ae6b58ffd1795c78991cbb5b8bc441c48a070440d" dependencies = [ "aws-lc-sys", - "untrusted 0.7.1", "zeroize", ] @@ -1465,502 +541,6 @@ dependencies = [ "fs_extra", ] -[[package]] -name = "aws-runtime" -version = "1.5.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa006bb32360ed90ac51203feafb9d02e3d21046e1fd3a450a404b90ea73e5d" -dependencies = [ - "aws-credential-types", - "aws-sigv4", - "aws-smithy-async", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes 1.10.1", - "fastrand 2.3.0", - "http 0.2.12", - "http-body 0.4.6", - "percent-encoding", - "pin-project-lite", - "tracing", - "uuid", -] - -[[package]] -name = "aws-sdk-bedrockruntime" -version = "1.109.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbfdfd941dcb253c17bf70baddbf1e5b22f19e29d313d2e049bad4b1dadb2011" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-sigv4", - "aws-smithy-async", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes 1.10.1", - "fastrand 2.3.0", - "http 0.2.12", - "hyper 0.14.32", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-kinesis" -version = "1.91.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "699a3d645a2ab5cb12ca02eb23979753953414429fd6584ea8841af6bc4e0516" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes 1.10.1", - "fastrand 2.3.0", - "http 0.2.12", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-s3" -version = "1.108.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "200be4aed61e3c0669f7268bacb768f283f1c32a7014ce57225e1160be2f6ccb" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-sigv4", - "aws-smithy-async", - "aws-smithy-checksums", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-smithy-xml", - "aws-types", - "bytes 1.10.1", - "fastrand 2.3.0", - "hex", - "hmac", - "http 0.2.12", - "http 1.3.1", - "http-body 0.4.6", - "lru", - "percent-encoding", - "regex-lite", - "sha2", - "tracing", - "url", -] - -[[package]] -name = "aws-sdk-sso" -version = "1.86.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a0abbfab841446cce6e87af853a3ba2cc1bc9afcd3f3550dd556c43d434c86d" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes 1.10.1", - "fastrand 2.3.0", - "http 0.2.12", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-ssooidc" -version = "1.88.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a68d675582afea0e94d38b6ca9c5aaae4ca14f1d36faa6edb19b42e687e70d7" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-types", - "bytes 1.10.1", - "fastrand 2.3.0", - "http 0.2.12", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sdk-sts" -version = "1.88.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d30990923f4f675523c51eb1c0dec9b752fb267b36a61e83cbc219c9d86da715" -dependencies = [ - "aws-credential-types", - "aws-runtime", - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-json", - "aws-smithy-query", - "aws-smithy-runtime", - "aws-smithy-runtime-api", - "aws-smithy-types", - "aws-smithy-xml", - "aws-types", - "fastrand 2.3.0", - "http 0.2.12", - "regex-lite", - "tracing", -] - -[[package]] -name = "aws-sigv4" -version = "1.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bffc03068fbb9c8dd5ce1c6fb240678a5cffb86fb2b7b1985c999c4b83c8df68" -dependencies = [ - "aws-credential-types", - "aws-smithy-eventstream", - "aws-smithy-http", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes 1.10.1", - "crypto-bigint 0.5.5", - "form_urlencoded", - "hex", - "hmac", - "http 0.2.12", - "http 1.3.1", - "p256", - "percent-encoding", - "ring", - "sha2", - "subtle", - "time", - "tracing", - "zeroize", -] - -[[package]] -name = "aws-smithy-async" -version = "1.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "127fcfad33b7dfc531141fda7e1c402ac65f88aca5511a4d31e2e3d2cd01ce9c" -dependencies = [ - "futures-util", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "aws-smithy-checksums" -version = "0.63.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "165d8583d8d906e2fb5511d29201d447cc710864f075debcdd9c31c265412806" -dependencies = [ - "aws-smithy-http", - "aws-smithy-types", - "bytes 1.10.1", - "crc-fast", - "hex", - "http 0.2.12", - "http-body 0.4.6", - "md-5", - "pin-project-lite", - "sha1", - "sha2", - "tracing", -] - -[[package]] -name = "aws-smithy-eventstream" -version = "0.60.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9656b85088f8d9dc7ad40f9a6c7228e1e8447cdf4b046c87e152e0805dea02fa" -dependencies = [ - "aws-smithy-types", - "bytes 1.10.1", - "crc32fast", -] - -[[package]] -name = "aws-smithy-http" -version = "0.62.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3feafd437c763db26aa04e0cc7591185d0961e64c61885bece0fb9d50ceac671" -dependencies = [ - "aws-smithy-eventstream", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes 1.10.1", - "bytes-utils", - "futures-core", - "http 0.2.12", - "http 1.3.1", - "http-body 0.4.6", - "percent-encoding", - "pin-project-lite", - "pin-utils", - "tracing", -] - -[[package]] -name = "aws-smithy-http-client" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1053b5e587e6fa40ce5a79ea27957b04ba660baa02b28b7436f64850152234f1" -dependencies = [ - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "h2 0.3.27", - "h2 0.4.12", - "http 0.2.12", - "http 1.3.1", - "http-body 0.4.6", - "hyper 0.14.32", - "hyper 1.7.0", - "hyper-rustls 0.24.2", - "hyper-rustls 0.27.7", - "hyper-util", - "pin-project-lite", - "rustls 0.21.12", - "rustls 0.23.33", - "rustls-native-certs 0.8.2", - "rustls-pki-types", - "tokio", - "tokio-rustls 0.26.2", - "tower 0.5.2", - "tracing", -] - -[[package]] -name = "aws-smithy-json" -version = "0.61.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff418fc8ec5cadf8173b10125f05c2e7e1d46771406187b2c878557d4503390" -dependencies = [ - "aws-smithy-types", -] - -[[package]] -name = "aws-smithy-observability" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d1881b1ea6d313f9890710d65c158bdab6fb08c91ea825f74c1c8c357baf4cc" -dependencies = [ - "aws-smithy-runtime-api", -] - -[[package]] -name = "aws-smithy-query" -version = "0.60.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d28a63441360c477465f80c7abac3b9c4d075ca638f982e605b7dc2a2c7156c9" -dependencies = [ - "aws-smithy-types", - "urlencoding", -] - -[[package]] -name = "aws-smithy-runtime" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ab99739082da5347660c556689256438defae3bcefd66c52b095905730e404" -dependencies = [ - "aws-smithy-async", - "aws-smithy-http", - "aws-smithy-http-client", - "aws-smithy-observability", - "aws-smithy-runtime-api", - "aws-smithy-types", - "bytes 1.10.1", - "fastrand 2.3.0", - "http 0.2.12", - "http 1.3.1", - "http-body 0.4.6", - "http-body 1.0.1", - "pin-project-lite", - "pin-utils", - "tokio", - "tracing", -] - -[[package]] -name = "aws-smithy-runtime-api" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3683c5b152d2ad753607179ed71988e8cfd52964443b4f74fd8e552d0bbfeb46" -dependencies = [ - "aws-smithy-async", - "aws-smithy-types", - "bytes 1.10.1", - "http 0.2.12", - "http 1.3.1", - "pin-project-lite", - "tokio", - "tracing", - "zeroize", -] - -[[package]] -name = "aws-smithy-types" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f5b3a7486f6690ba25952cabf1e7d75e34d69eaff5081904a47bc79074d6457" -dependencies = [ - "base64-simd", - "bytes 1.10.1", - "bytes-utils", - "futures-core", - "http 0.2.12", - "http 1.3.1", - "http-body 0.4.6", - "http-body 1.0.1", - "http-body-util", - "itoa", - "num-integer", - "pin-project-lite", - "pin-utils", - "ryu", - "serde", - "time", - "tokio", - "tokio-util", -] - -[[package]] -name = "aws-smithy-xml" -version = "0.60.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9c34127e8c624bc2999f3b657e749c1393bedc9cd97b92a804db8ced4d2e163" -dependencies = [ - "xmlparser", -] - -[[package]] -name = "aws-types" -version = "1.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2fd329bf0e901ff3f60425691410c69094dc2a1f34b331f37bfc4e9ac1565a1" -dependencies = [ - "aws-credential-types", - "aws-smithy-async", - "aws-smithy-runtime-api", - "aws-smithy-types", - "rustc_version", - "tracing", -] - -[[package]] -name = "aws_http_client" -version = "0.1.0" -dependencies = [ - "aws-smithy-runtime-api", - "aws-smithy-types", - "http_client", -] - -[[package]] -name = "axum" -version = "0.6.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" -dependencies = [ - "async-trait", - "axum-core", - "base64 0.21.7", - "bitflags 1.3.2", - "bytes 1.10.1", - "futures-util", - "headers", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.32", - "itoa", - "matchit", - "memchr", - "mime", - "percent-encoding", - "pin-project-lite", - "rustversion", - "serde", - "serde_json", - "serde_path_to_error", - "serde_urlencoded", - "sha1", - "sync_wrapper 0.1.2", - "tokio", - "tokio-tungstenite 0.20.1", - "tower 0.4.13", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-core" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" -dependencies = [ - "async-trait", - "bytes 1.10.1", - "futures-util", - "http 0.2.12", - "http-body 0.4.6", - "mime", - "rustversion", - "tower-layer", - "tower-service", -] - -[[package]] -name = "axum-extra" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a320103719de37b7b4da4c8eb629d4573f6bcfd3dfe80d3208806895ccf81d" -dependencies = [ - "axum", - "bytes 1.10.1", - "futures-util", - "http 0.2.12", - "mime", - "pin-project-lite", - "serde", - "serde_json", - "tokio", - "tower 0.4.13", - "tower-http 0.3.5", - "tower-layer", - "tower-service", -] - [[package]] name = "backtrace" version = "0.3.76" @@ -1971,83 +551,17 @@ dependencies = [ "cfg-if", "libc", "miniz_oxide", - "object 0.37.3", + "object", "rustc-demangle", "windows-link 0.2.1", ] -[[package]] -name = "base16ct" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "base64-simd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" -dependencies = [ - "outref", - "vsimd", -] - -[[package]] -name = "base64ct" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" - -[[package]] -name = "bedrock" -version = "0.1.0" -dependencies = [ - "anyhow", - "aws-sdk-bedrockruntime", - "aws-smithy-types", - "futures 0.3.31", - "schemars", - "serde", - "serde_json", - "strum 0.27.2", - "thiserror 2.0.17", -] - -[[package]] -name = "bigdecimal" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a22f228ab7a1b23027ccc6c350b72868017af7ea8356fbdf19f8d991c690013" -dependencies = [ - "autocfg", - "libm", - "num-bigint", - "num-integer", - "num-traits", - "serde", -] - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - [[package]] name = "bindgen" version = "0.71.1" @@ -2120,9 +634,6 @@ name = "bitflags" version = "2.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" -dependencies = [ - "serde", -] [[package]] name = "bitstream-io" @@ -2130,18 +641,6 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2" -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - [[package]] name = "blade-graphics" version = "0.7.0" @@ -2152,7 +651,7 @@ dependencies = [ "ash-window", "bitflags 2.9.4", "bytemuck", - "codespan-reporting 0.12.0", + "codespan-reporting", "glow", "gpu-alloc", "gpu-alloc-ash", @@ -2246,84 +745,6 @@ dependencies = [ "piper", ] -[[package]] -name = "bm25" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cbd8ffdfb7b4c2ff038726178a780a94f90525ed0ad264c0afaa75dd8c18a64" -dependencies = [ - "cached", - "deunicode", - "fxhash", - "rust-stemmers", - "stop-words", - "unicode-segmentation", -] - -[[package]] -name = "borrow-or-share" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" - -[[package]] -name = "borsh" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" -dependencies = [ - "borsh-derive", - "cfg_aliases 0.2.1", -] - -[[package]] -name = "borsh-derive" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" -dependencies = [ - "once_cell", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "breadcrumbs" -version = "0.1.0" -dependencies = [ - "editor", - "gpui", - "itertools 0.14.0", - "settings", - "theme", - "ui", - "workspace", - "zed_actions", -] - -[[package]] -name = "brotli" -version = "8.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", - "brotli-decompressor", -] - -[[package]] -name = "brotli-decompressor" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" -dependencies = [ - "alloc-no-stdlib", - "alloc-stdlib", -] - [[package]] name = "bstr" version = "1.12.0" @@ -2331,34 +752,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234113d19d0d7d613b40e86fb654acf958910802bcceab913a4f9e7cda03b1a4" dependencies = [ "memchr", - "regex-automata", "serde", ] -[[package]] -name = "buffer_diff" -version = "0.1.0" -dependencies = [ - "anyhow", - "clock", - "ctor", - "futures 0.3.31", - "git2", - "gpui", - "language", - "log", - "pretty_assertions", - "rand 0.9.2", - "rope", - "serde_json", - "settings", - "sum_tree", - "text", - "unindent", - "util", - "zlog", -] - [[package]] name = "built" version = "0.7.7" @@ -2370,43 +766,6 @@ name = "bumpalo" version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" -dependencies = [ - "allocator-api2", -] - -[[package]] -name = "by_address" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06" - -[[package]] -name = "bytecheck" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" -dependencies = [ - "bytecheck_derive", - "ptr_meta", - "simdutf8", -] - -[[package]] -name = "bytecheck_derive" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "bytecount" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] name = "bytemuck" @@ -2440,110 +799,12 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" -[[package]] -name = "bytes" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" -dependencies = [ - "byteorder", - "iovec", -] - [[package]] name = "bytes" version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" -[[package]] -name = "bytes-utils" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" -dependencies = [ - "bytes 1.10.1", - "either", -] - -[[package]] -name = "bzip2" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" -dependencies = [ - "bzip2-sys", - "libc", -] - -[[package]] -name = "bzip2-sys" -version = "0.1.13+1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" -dependencies = [ - "cc", - "pkg-config", -] - -[[package]] -name = "cached" -version = "0.56.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "801927ee168e17809ab8901d9f01f700cd7d8d6a6527997fee44e4b0327a253c" -dependencies = [ - "ahash 0.8.12", - "cached_proc_macro", - "cached_proc_macro_types", - "hashbrown 0.15.5", - "once_cell", - "thiserror 2.0.17", - "web-time", -] - -[[package]] -name = "cached_proc_macro" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9225bdcf4e4a9a4c08bf16607908eb2fbf746828d5e0b5e019726dbf6571f201" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "cached_proc_macro_types" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0" - -[[package]] -name = "call" -version = "0.1.0" -dependencies = [ - "anyhow", - "audio", - "client", - "collections", - "feature_flags", - "fs", - "futures 0.3.31", - "gpui", - "gpui_tokio", - "http_client", - "language", - "livekit_client", - "log", - "postage", - "project", - "serde", - "settings", - "telemetry", - "util", -] - [[package]] name = "calloop" version = "0.14.3" @@ -2568,179 +829,6 @@ dependencies = [ "wayland-client", ] -[[package]] -name = "camino" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "276a59bf2b2c967788139340c9f0c5b12d7fd6630315c15c217e559de85d2609" -dependencies = [ - "serde_core", -] - -[[package]] -name = "candle-core" -version = "0.9.1" -source = "git+https://github.com/zed-industries/candle?branch=9.1-patched#724d75eb3deebefe83f2a7381a45d4fac6eda383" -dependencies = [ - "byteorder", - "float8", - "gemm 0.17.1", - "half", - "memmap2", - "num-traits", - "num_cpus", - "rand 0.9.2", - "rand_distr", - "rayon", - "safetensors", - "thiserror 1.0.69", - "ug", - "yoke 0.7.5", - "zip 1.1.4", -] - -[[package]] -name = "candle-nn" -version = "0.9.1" -source = "git+https://github.com/zed-industries/candle?branch=9.1-patched#724d75eb3deebefe83f2a7381a45d4fac6eda383" -dependencies = [ - "candle-core", - "half", - "libc", - "num-traits", - "rayon", - "safetensors", - "serde", - "thiserror 1.0.69", -] - -[[package]] -name = "candle-onnx" -version = "0.9.1" -source = "git+https://github.com/zed-industries/candle?branch=9.1-patched#724d75eb3deebefe83f2a7381a45d4fac6eda383" -dependencies = [ - "candle-core", - "candle-nn", - "prost 0.12.6", -] - -[[package]] -name = "cap-fs-ext" -version = "3.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e41cc18551193fe8fa6f15c1e3c799bc5ec9e2cfbfaa8ed46f37013e3e6c173c" -dependencies = [ - "cap-primitives", - "cap-std", - "io-lifetimes", - "windows-sys 0.59.0", -] - -[[package]] -name = "cap-net-ext" -version = "3.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f83833816c66c986e913b22ac887cec216ea09301802054316fc5301809702c" -dependencies = [ - "cap-primitives", - "cap-std", - "rustix 1.1.2", - "smallvec", -] - -[[package]] -name = "cap-primitives" -version = "3.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a1e394ed14f39f8bc26f59d4c0c010dbe7f0a1b9bafff451b1f98b67c8af62a" -dependencies = [ - "ambient-authority", - "fs-set-times", - "io-extras", - "io-lifetimes", - "ipnet", - "maybe-owned", - "rustix 1.1.2", - "rustix-linux-procfs", - "windows-sys 0.59.0", - "winx", -] - -[[package]] -name = "cap-rand" -version = "3.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0acb89ccf798a28683f00089d0630dfaceec087234eae0d308c05ddeaa941b40" -dependencies = [ - "ambient-authority", - "rand 0.8.5", -] - -[[package]] -name = "cap-std" -version = "3.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07c0355ca583dd58f176c3c12489d684163861ede3c9efa6fd8bba314c984189" -dependencies = [ - "cap-primitives", - "io-extras", - "io-lifetimes", - "rustix 1.1.2", -] - -[[package]] -name = "cap-time-ext" -version = "3.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "491af520b8770085daa0466978c75db90368c71896523f2464214e38359b1a5b" -dependencies = [ - "ambient-authority", - "cap-primitives", - "iana-time-zone", - "once_cell", - "rustix 1.1.2", - "winx", -] - -[[package]] -name = "cargo-platform" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" -dependencies = [ - "serde", -] - -[[package]] -name = "cargo_metadata" -version = "0.19.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" -dependencies = [ - "camino", - "cargo-platform", - "semver", - "serde", - "serde_json", - "thiserror 2.0.17", -] - -[[package]] -name = "cargo_toml" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fbd1fe9db3ebf71b89060adaf7b0504c2d6a425cf061313099547e382c2e472" -dependencies = [ - "serde", - "toml 0.8.23", -] - -[[package]] -name = "cast" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" - [[package]] name = "cbc" version = "0.1.2" @@ -2802,7 +890,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02" dependencies = [ "smallvec", - "target-lexicon 0.12.16", + "target-lexicon", ] [[package]] @@ -2811,12 +899,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "cfg_aliases" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" - [[package]] name = "cfg_aliases" version = "0.2.1" @@ -2832,29 +914,6 @@ dependencies = [ "libc", ] -[[package]] -name = "channel" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "clock", - "collections", - "futures 0.3.31", - "gpui", - "http_client", - "language", - "log", - "postage", - "release_channel", - "rpc", - "semver", - "settings", - "text", - "time", - "util", -] - [[package]] name = "chrono" version = "0.4.42" @@ -2869,12 +928,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "chunked_transfer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4de3bc4ea267985becf712dc6d9eed8b04c953b3fcfb339ebc87acd9804901" - [[package]] name = "ciborium" version = "0.2.2" @@ -2930,152 +983,13 @@ dependencies = [ "libloading", ] -[[package]] -name = "clap" -version = "4.5.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4512b90fa68d3a9932cea5184017c5d200f5921df706d45e853537dea51508f" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.5.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0025e98baa12e766c67ba13ff4695a887a1eba19569aad00a472546795bd6730" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", - "terminal_size", -] - -[[package]] -name = "clap_complete" -version = "4.5.59" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2348487adcd4631696ced64ccdb40d38ac4d31cae7f2eec8817fcea1b9d1c43c" -dependencies = [ - "clap", -] - -[[package]] -name = "clap_derive" -version = "4.5.49" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "clap_lex" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" - -[[package]] -name = "cli" -version = "0.1.0" -dependencies = [ - "anyhow", - "askpass", - "clap", - "collections", - "core-foundation 0.10.0", - "core-services", - "exec", - "fork", - "ipc-channel", - "parking_lot", - "paths", - "plist", - "rayon", - "release_channel", - "serde", - "serde_json", - "tempfile", - "util", - "windows 0.61.3", -] - -[[package]] -name = "client" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-tungstenite", - "base64 0.22.1", - "chrono", - "clock", - "cloud_api_client", - "cloud_llm_client", - "collections", - "credentials_provider", - "derive_more 0.99.20", - "feature_flags", - "fs", - "futures 0.3.31", - "gpui", - "gpui_tokio", - "http_client", - "http_client_tls", - "httparse", - "log", - "objc2-foundation", - "parking_lot", - "paths", - "postage", - "rand 0.9.2", - "regex", - "release_channel", - "rpc", - "rustls-pki-types", - "semver", - "serde", - "serde_json", - "serde_urlencoded", - "settings", - "sha2", - "smol", - "telemetry", - "telemetry_events", - "text", - "thiserror 2.0.17", - "time", - "tiny_http", - "tokio", - "tokio-native-tls", - "tokio-rustls 0.26.2", - "tokio-socks", - "url", - "util", - "windows 0.61.3", - "worktree", -] - -[[package]] -name = "clock" -version = "0.1.0" -dependencies = [ - "parking_lot", - "serde", - "smallvec", -] - [[package]] name = "cloud_api_client" version = "0.1.0" dependencies = [ "anyhow", "cloud_api_types", - "futures 0.3.31", + "futures", "gpui", "gpui_tokio", "http_client", @@ -3120,15 +1034,6 @@ dependencies = [ "cc", ] -[[package]] -name = "cobs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" -dependencies = [ - "thiserror 2.0.17", -] - [[package]] name = "cocoa" version = "0.25.0" @@ -3140,7 +1045,7 @@ dependencies = [ "cocoa-foundation 0.1.2", "core-foundation 0.9.4", "core-graphics 0.23.2", - "foreign-types 0.5.0", + "foreign-types", "libc", "objc", ] @@ -3156,7 +1061,7 @@ dependencies = [ "cocoa-foundation 0.2.0", "core-foundation 0.10.0", "core-graphics 0.24.0", - "foreign-types 0.5.0", + "foreign-types", "libc", "objc", ] @@ -3200,183 +1105,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "codespan-reporting" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba7a06c0b31fff5ff2e1e7d37dbf940864e2a974b336e1a2938d10af6e8fb283" -dependencies = [ - "serde", - "termcolor", - "unicode-width", -] - -[[package]] -name = "codestral" -version = "0.1.0" -dependencies = [ - "anyhow", - "edit_prediction_context", - "edit_prediction_types", - "futures 0.3.31", - "gpui", - "http_client", - "language", - "language_models", - "log", - "mistral", - "serde", - "serde_json", - "smol", - "text", -] - -[[package]] -name = "collab" -version = "0.44.0" -dependencies = [ - "agent_settings", - "anyhow", - "assistant_slash_command", - "assistant_text_thread", - "async-trait", - "async-tungstenite", - "audio", - "aws-config", - "aws-sdk-kinesis", - "aws-sdk-s3", - "axum", - "axum-extra", - "base64 0.22.1", - "buffer_diff", - "call", - "channel", - "chrono", - "client", - "clock", - "collab_ui", - "collections", - "command_palette_hooks", - "context_server", - "ctor", - "dap", - "dap-types", - "dap_adapters", - "dashmap 6.1.0", - "debugger_ui", - "editor", - "envy", - "extension", - "file_finder", - "fs", - "futures 0.3.31", - "git", - "git_hosting_providers", - "git_ui", - "gpui", - "gpui_tokio", - "hex", - "http_client", - "hyper 0.14.32", - "indoc", - "language", - "language_model", - "livekit_api", - "livekit_client", - "log", - "lsp", - "menu", - "multi_buffer", - "nanoid", - "node_runtime", - "notifications", - "parking_lot", - "pretty_assertions", - "project", - "prometheus", - "prompt_store", - "prost 0.9.0", - "rand 0.9.2", - "recent_projects", - "release_channel", - "remote", - "remote_server", - "reqwest 0.11.27", - "reqwest_client", - "rpc", - "scrypt", - "sea-orm", - "sea-orm-macros", - "semver", - "serde", - "serde_json", - "session", - "settings", - "sha2", - "smol", - "sqlx", - "strum 0.27.2", - "subtle", - "supermaven_api", - "task", - "telemetry_events", - "text", - "theme", - "time", - "tokio", - "toml 0.8.23", - "tower 0.4.13", - "tower-http 0.4.4", - "tracing", - "tracing-subscriber", - "unindent", - "util", - "uuid", - "workspace", - "worktree", - "zlog", -] - -[[package]] -name = "collab_ui" -version = "0.1.0" -dependencies = [ - "anyhow", - "call", - "channel", - "chrono", - "client", - "collections", - "db", - "editor", - "futures 0.3.31", - "fuzzy", - "gpui", - "http_client", - "log", - "menu", - "notifications", - "picker", - "pretty_assertions", - "project", - "release_channel", - "rpc", - "serde", - "serde_json", - "settings", - "smallvec", - "story", - "telemetry", - "theme", - "time", - "time_format", - "title_bar", - "tree-sitter-md", - "ui", - "util", - "workspace", -] - [[package]] name = "collections" version = "0.1.0" @@ -3403,7 +1131,7 @@ version = "4.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ - "bytes 1.10.1", + "bytes", "memchr", ] @@ -3417,62 +1145,6 @@ dependencies = [ "thiserror 2.0.17", ] -[[package]] -name = "command_palette" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "collections", - "command_palette_hooks", - "ctor", - "db", - "editor", - "env_logger 0.11.8", - "fuzzy", - "go_to_line", - "gpui", - "language", - "log", - "menu", - "picker", - "postage", - "project", - "serde", - "serde_json", - "settings", - "telemetry", - "theme", - "time", - "ui", - "util", - "workspace", - "zed_actions", -] - -[[package]] -name = "command_palette_hooks" -version = "0.1.0" -dependencies = [ - "collections", - "derive_more 0.99.20", - "gpui", - "workspace", -] - -[[package]] -name = "component" -version = "0.1.0" -dependencies = [ - "collections", - "documented", - "gpui", - "inventory", - "parking_lot", - "strum 0.27.2", - "theme", -] - [[package]] name = "compression-codecs" version = "0.4.31" @@ -3500,25 +1172,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "console" -version = "0.15.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" -dependencies = [ - "encode_unicode", - "libc", - "once_cell", - "unicode-width", - "windows-sys 0.59.0", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - [[package]] name = "const-random" version = "0.1.18" @@ -3539,115 +1192,12 @@ dependencies = [ "tiny-keccak", ] -[[package]] -name = "const_format" -version = "0.2.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" -dependencies = [ - "const_format_proc_macros", -] - -[[package]] -name = "const_format_proc_macros" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "constant_time_eq" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" - -[[package]] -name = "context_server" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "collections", - "futures 0.3.31", - "gpui", - "http_client", - "log", - "net", - "parking_lot", - "postage", - "schemars", - "serde", - "serde_json", - "settings", - "smol", - "tempfile", - "terminal", - "url", - "util", -] - [[package]] name = "convert_case" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" -[[package]] -name = "convert_case" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baaaa0ecca5b51987b9423ccdc971514dd8b0bb7b4060b983d3664dad3f1f89f" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "copilot" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-std", - "chrono", - "client", - "clock", - "collections", - "command_palette_hooks", - "ctor", - "dirs 4.0.0", - "edit_prediction_types", - "editor", - "fs", - "futures 0.3.31", - "gpui", - "http_client", - "indoc", - "itertools 0.14.0", - "language", - "log", - "lsp", - "menu", - "node_runtime", - "parking_lot", - "paths", - "project", - "rpc", - "semver", - "serde", - "serde_json", - "settings", - "sum_tree", - "task", - "theme", - "ui", - "util", - "workspace", - "zlog", -] - [[package]] name = "core-foundation" version = "0.9.4" @@ -3683,7 +1233,7 @@ dependencies = [ "bitflags 1.3.2", "core-foundation 0.9.4", "core-graphics-types 0.1.3", - "foreign-types 0.5.0", + "foreign-types", "libc", ] @@ -3696,7 +1246,7 @@ dependencies = [ "bitflags 2.9.4", "core-foundation 0.10.0", "core-graphics-types 0.2.0", - "foreign-types 0.5.0", + "foreign-types", "libc", ] @@ -3709,7 +1259,7 @@ dependencies = [ "bitflags 2.9.4", "core-foundation 0.9.4", "core-graphics-types 0.1.3", - "foreign-types 0.5.0", + "foreign-types", "libc", ] @@ -3748,15 +1298,6 @@ dependencies = [ "libc", ] -[[package]] -name = "core-services" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92567e81db522550ebaf742c5d875624ec7820c2c7ee5f8c60e4ce7c2ae3c0fd" -dependencies = [ - "core-foundation 0.9.4", -] - [[package]] name = "core-text" version = "21.0.0" @@ -3765,7 +1306,7 @@ checksum = "a593227b66cbd4007b2a050dfdd9e1d1318311409c8d600dc82ba1b15ca9c130" dependencies = [ "core-foundation 0.10.0", "core-graphics 0.24.0", - "foreign-types 0.5.0", + "foreign-types", "libc", ] @@ -3792,40 +1333,6 @@ dependencies = [ "libm", ] -[[package]] -name = "coreaudio-rs" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34ca07354f6d0640333ef95f48d460a4bcf34812a7e7967f9b44c728a8f37c28" -dependencies = [ - "bitflags 1.3.2", - "core-foundation-sys", - "coreaudio-sys", -] - -[[package]] -name = "coreaudio-rs" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aae284fbaf7d27aa0e292f7677dfbe26503b0d555026f702940805a630eac17" -dependencies = [ - "bitflags 1.3.2", - "libc", - "objc2-audio-toolbox", - "objc2-core-audio", - "objc2-core-audio-types", - "objc2-core-foundation", -] - -[[package]] -name = "coreaudio-sys" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ceec7a6067e62d6f931a2baf6f3a751f4a892595bcec1461a3c94ef9949864b6" -dependencies = [ - "bindgen 0.72.1", -] - [[package]] name = "cosmic-text" version = "0.14.2" @@ -3849,41 +1356,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "cpal" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbd307f43cc2a697e2d1f8bc7a1d824b5269e052209e28883e5bc04d095aaa3f" -dependencies = [ - "alsa", - "coreaudio-rs 0.13.0", - "dasp_sample", - "jni", - "js-sys", - "libc", - "mach2 0.4.3", - "ndk", - "ndk-context", - "num-derive", - "num-traits", - "objc2-audio-toolbox", - "objc2-core-audio", - "objc2-core-audio-types", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "windows 0.54.0", -] - -[[package]] -name = "cpp_demangle" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2bb79cb74d735044c972aae58ed0aaa9a837e85b01106a54c39e42e97f62253" -dependencies = [ - "cfg-if", -] - [[package]] name = "cpufeatures" version = "0.2.17" @@ -3893,189 +1365,6 @@ dependencies = [ "libc", ] -[[package]] -name = "cranelift-bforest" -version = "0.116.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e15d04a0ce86cb36ead88ad68cf693ffd6cda47052b9e0ac114bc47fd9cd23c4" -dependencies = [ - "cranelift-entity", -] - -[[package]] -name = "cranelift-bitset" -version = "0.116.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c6e3969a7ce267259ce244b7867c5d3bc9e65b0a87e81039588dfdeaede9f34" -dependencies = [ - "serde", - "serde_derive", -] - -[[package]] -name = "cranelift-codegen" -version = "0.116.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c22032c4cb42558371cf516bb47f26cdad1819d3475c133e93c49f50ebf304e" -dependencies = [ - "bumpalo", - "cranelift-bforest", - "cranelift-bitset", - "cranelift-codegen-meta", - "cranelift-codegen-shared", - "cranelift-control", - "cranelift-entity", - "cranelift-isle", - "gimli 0.31.1", - "hashbrown 0.14.5", - "log", - "postcard", - "regalloc2", - "rustc-hash 2.1.1", - "serde", - "serde_derive", - "sha2", - "smallvec", - "target-lexicon 0.13.3", -] - -[[package]] -name = "cranelift-codegen-meta" -version = "0.116.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c904bc71c61b27fc57827f4a1379f29de64fe95653b620a3db77d59655eee0b8" -dependencies = [ - "cranelift-codegen-shared", -] - -[[package]] -name = "cranelift-codegen-shared" -version = "0.116.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40180f5497572f644ce88c255480981ae2ec1d7bb4d8e0c0136a13b87a2f2ceb" - -[[package]] -name = "cranelift-control" -version = "0.116.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d132c6d0bd8a489563472afc171759da0707804a65ece7ceb15a8c6d7dd5ef" -dependencies = [ - "arbitrary", -] - -[[package]] -name = "cranelift-entity" -version = "0.116.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b2d0d9618275474fbf679dd018ac6e009acbd6ae6850f6a67be33fb3b00b323" -dependencies = [ - "cranelift-bitset", - "serde", - "serde_derive", -] - -[[package]] -name = "cranelift-frontend" -version = "0.116.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fac41e16729107393174b0c9e3730fb072866100e1e64e80a1a963b2e484d57" -dependencies = [ - "cranelift-codegen", - "log", - "smallvec", - "target-lexicon 0.13.3", -] - -[[package]] -name = "cranelift-isle" -version = "0.116.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ca20d576e5070044d0a72a9effc2deacf4d6aa650403189d8ea50126483944d" - -[[package]] -name = "cranelift-native" -version = "0.116.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8dee82f3f1f2c4cba9177f1cc5e350fe98764379bcd29340caa7b01f85076c7" -dependencies = [ - "cranelift-codegen", - "libc", - "target-lexicon 0.13.3", -] - -[[package]] -name = "crash-context" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "031ed29858d90cfdf27fe49fae28028a1f20466db97962fa2f4ea34809aeebf3" -dependencies = [ - "cfg-if", - "libc", - "mach2 0.4.3", -] - -[[package]] -name = "crash-handler" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2066907075af649bcb8bcb1b9b986329b243677e6918b2d920aa64b0aac5ace3" -dependencies = [ - "cfg-if", - "crash-context", - "libc", - "mach2 0.4.3", - "parking_lot", -] - -[[package]] -name = "crashes" -version = "0.1.0" -dependencies = [ - "bincode", - "cfg-if", - "crash-handler", - "extension_host", - "log", - "mach2 0.5.0", - "minidumper", - "paths", - "release_channel", - "serde", - "serde_json", - "smol", - "system_specs", - "windows 0.61.3", - "zstd", -] - -[[package]] -name = "crc" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" - -[[package]] -name = "crc-fast" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bf62af4cc77d8fe1c22dde4e721d87f2f54056139d8c412e1366b740305f56f" -dependencies = [ - "crc", - "digest", - "libc", - "rand 0.9.2", - "regex", -] - [[package]] name = "crc32fast" version = "1.5.0" @@ -4085,77 +1374,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "credentials_provider" -version = "0.1.0" -dependencies = [ - "anyhow", - "futures 0.3.31", - "gpui", - "paths", - "release_channel", - "serde", - "serde_json", -] - -[[package]] -name = "criterion" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" -dependencies = [ - "anes", - "cast", - "ciborium", - "clap", - "criterion-plot", - "is-terminal", - "itertools 0.10.5", - "num-traits", - "once_cell", - "oorandom", - "plotters", - "rayon", - "regex", - "serde", - "serde_derive", - "serde_json", - "tinytemplate", - "walkdir", -] - -[[package]] -name = "criterion-plot" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" -dependencies = [ - "cast", - "itertools 0.10.5", -] - -[[package]] -name = "crossbeam" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" -dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-epoch", - "crossbeam-queue", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -4196,28 +1414,6 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" -[[package]] -name = "crypto-bigint" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "crypto-common" version = "0.1.6" @@ -4229,29 +1425,6 @@ dependencies = [ "typenum", ] -[[package]] -name = "cssparser" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e901edd733a1472f944a45116df3f846f54d37e67e68640ac8bb69689aca2aa" -dependencies = [ - "cssparser-macros", - "dtoa-short", - "itoa", - "phf 0.11.3", - "smallvec", -] - -[[package]] -name = "cssparser-macros" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" -dependencies = [ - "quote", - "syn 2.0.106", -] - [[package]] name = "ctor" version = "0.4.3" @@ -4268,473 +1441,31 @@ version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2" -[[package]] -name = "ctrlc" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "881c5d0a13b2f1498e2306e82cbada78390e152d4b1378fb28a84f4dcd0dc4f3" -dependencies = [ - "dispatch", - "nix 0.30.1", - "windows-sys 0.61.2", -] - -[[package]] -name = "cursor-icon" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" - -[[package]] -name = "cxx" -version = "1.0.187" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8465678d499296e2cbf9d3acf14307458fd69b471a31b65b3c519efe8b5e187" -dependencies = [ - "cc", - "cxx-build", - "cxxbridge-cmd", - "cxxbridge-flags", - "cxxbridge-macro", - "foldhash 0.2.0", - "link-cplusplus", -] - -[[package]] -name = "cxx-build" -version = "1.0.187" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d74b6bcf49ebbd91f1b1875b706ea46545032a14003b5557b7dfa4bbeba6766e" -dependencies = [ - "cc", - "codespan-reporting 0.13.0", - "indexmap", - "proc-macro2", - "quote", - "scratch", - "syn 2.0.106", -] - -[[package]] -name = "cxxbridge-cmd" -version = "1.0.187" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94ca2ad69673c4b35585edfa379617ac364bccd0ba0adf319811ba3a74ffa48a" -dependencies = [ - "clap", - "codespan-reporting 0.13.0", - "indexmap", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "cxxbridge-flags" -version = "1.0.187" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29b52102aa395386d77d322b3a0522f2035e716171c2c60aa87cc5e9466e523" - -[[package]] -name = "cxxbridge-macro" -version = "1.0.187" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8ebf0b6138325af3ec73324cb3a48b64d57721f17291b151206782e61f66cd" -dependencies = [ - "indexmap", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "dap" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-compression", - "async-pipe", - "async-tar", - "async-trait", - "client", - "collections", - "dap-types", - "fs", - "futures 0.3.31", - "gpui", - "http_client", - "language", - "libc", - "log", - "node_runtime", - "parking_lot", - "paths", - "proto", - "schemars", - "serde", - "serde_json", - "settings", - "smallvec", - "smol", - "task", - "telemetry", - "tree-sitter", - "tree-sitter-go", - "util", - "zlog", -] - -[[package]] -name = "dap-types" -version = "0.0.1" -source = "git+https://github.com/zed-industries/dap-types?rev=1b461b310481d01e02b2603c16d7144b926339f8#1b461b310481d01e02b2603c16d7144b926339f8" -dependencies = [ - "schemars", - "serde", - "serde_json", -] - -[[package]] -name = "dap_adapters" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "collections", - "dap", - "dotenvy", - "fs", - "futures 0.3.31", - "gpui", - "http_client", - "json_dotpath", - "language", - "log", - "node_runtime", - "paths", - "serde", - "serde_json", - "settings", - "smol", - "task", - "util", -] - -[[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.106", -] - -[[package]] -name = "darling_macro" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "dashmap" -version = "5.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" -dependencies = [ - "cfg-if", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", -] - -[[package]] -name = "dashmap" -version = "6.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" -dependencies = [ - "cfg-if", - "crossbeam-utils", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", -] - -[[package]] -name = "dasp_sample" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" - -[[package]] -name = "data-encoding" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" - [[package]] name = "data-url" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376" -[[package]] -name = "db" -version = "0.1.0" -dependencies = [ - "anyhow", - "gpui", - "indoc", - "log", - "paths", - "release_channel", - "smol", - "sqlez", - "sqlez_macros", - "tempfile", - "util", - "zed_env_vars", -] - -[[package]] -name = "dbus" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190b6255e8ab55a7b568df5a883e9497edc3e4821c06396612048b430e5ad1e9" -dependencies = [ - "libc", - "libdbus-sys", - "windows-sys 0.59.0", -] - -[[package]] -name = "debug_adapter_extension" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "collections", - "dap", - "extension", - "gpui", - "serde_json", - "task", - "util", -] - -[[package]] -name = "debugger_tools" -version = "0.1.0" -dependencies = [ - "anyhow", - "dap", - "editor", - "futures 0.3.31", - "gpui", - "project", - "serde_json", - "settings", - "smol", - "util", - "workspace", -] - -[[package]] -name = "debugger_ui" -version = "0.1.0" -dependencies = [ - "alacritty_terminal", - "anyhow", - "bitflags 2.9.4", - "client", - "collections", - "command_palette_hooks", - "dap", - "dap_adapters", - "db", - "debugger_tools", - "editor", - "feature_flags", - "file_icons", - "futures 0.3.31", - "fuzzy", - "gpui", - "hex", - "indoc", - "itertools 0.14.0", - "language", - "log", - "menu", - "notifications", - "parking_lot", - "parse_int", - "paths", - "picker", - "pretty_assertions", - "project", - "rpc", - "schemars", - "serde", - "serde_json", - "serde_json_lenient", - "settings", - "sysinfo 0.37.2", - "task", - "tasks_ui", - "telemetry", - "terminal_view", - "text", - "theme", - "tree-sitter", - "tree-sitter-go", - "tree-sitter-json", - "ui", - "ui_input", - "unindent", - "util", - "workspace", - "zed_actions", - "zlog", -] - -[[package]] -name = "debugid" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" -dependencies = [ - "uuid", -] - -[[package]] -name = "deepseek" -version = "0.1.0" -dependencies = [ - "anyhow", - "futures 0.3.31", - "http_client", - "schemars", - "serde", - "serde_json", -] - [[package]] name = "deflate64" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26bf8fc351c5ed29b5c2f0cbbac1b209b74f60ecd62e675a998df72c49af5204" -[[package]] -name = "denoise" -version = "0.1.0" -dependencies = [ - "candle-core", - "candle-onnx", - "log", - "realfft", - "rodio", - "rustfft", - "thiserror 2.0.17", -] - -[[package]] -name = "der" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" -dependencies = [ - "const-oid", - "zeroize", -] - -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "deranged" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" -dependencies = [ - "powerfmt", - "serde_core", -] - -[[package]] -name = "derive_arbitrary" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "derive_more" version = "0.99.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" dependencies = [ - "convert_case 0.4.0", + "convert_case", "proc-macro2", "quote", "rustc_version", "syn 2.0.106", ] -[[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.106", - "unicode-xid", -] - [[package]] name = "derive_refineable" version = "0.1.0" @@ -4744,85 +1475,12 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "derive_setters" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae5c625eda104c228c06ecaf988d1c60e542176bd7a490e60eeda3493244c0c9" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "deunicode" -version = "1.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" - -[[package]] -name = "diagnostics" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "collections", - "component", - "ctor", - "editor", - "gpui", - "indoc", - "itertools 0.14.0", - "language", - "log", - "lsp", - "markdown", - "pretty_assertions", - "project", - "rand 0.9.2", - "serde", - "serde_json", - "settings", - "text", - "theme", - "ui", - "unindent", - "util", - "workspace", - "zlog", -] - -[[package]] -name = "dialoguer" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bce805d770f407bc62102fca7c2c64ceef2fbcb2b8bd19d2765ce093980de" -dependencies = [ - "console", - "fuzzy-matcher", - "shell-words", - "tempfile", - "thiserror 1.0.69", - "zeroize", -] - [[package]] name = "diff" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" -[[package]] -name = "diffy" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b545b8c50194bdd008283985ab0b31dba153cfd5b3066a92770634fbc0d7d291" -dependencies = [ - "nu-ansi-term", -] - [[package]] name = "digest" version = "0.10.7" @@ -4830,7 +1488,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", - "const-oid", "crypto-common", "subtle", ] @@ -4853,15 +1510,6 @@ dependencies = [ "dirs-sys 0.4.1", ] -[[package]] -name = "dirs" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" -dependencies = [ - "dirs-sys 0.5.0", -] - [[package]] name = "dirs-sys" version = "0.3.7" @@ -4869,7 +1517,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" dependencies = [ "libc", - "redox_users 0.4.6", + "redox_users", "winapi", ] @@ -4881,22 +1529,10 @@ checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" dependencies = [ "libc", "option-ext", - "redox_users 0.4.6", + "redox_users", "windows-sys 0.48.0", ] -[[package]] -name = "dirs-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" -dependencies = [ - "libc", - "option-ext", - "redox_users 0.5.2", - "windows-sys 0.61.2", -] - [[package]] name = "dispatch" version = "0.2.0" @@ -4933,87 +1569,12 @@ dependencies = [ "libloading", ] -[[package]] -name = "docs_preprocessor" -version = "0.1.0" -dependencies = [ - "anyhow", - "command_palette", - "gpui", - "mdbook", - "regex", - "serde", - "serde_json", - "settings", - "task", - "theme", - "util", - "zed", - "zlog", -] - -[[package]] -name = "documented" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6b3e31251e87acd1b74911aed84071c8364fc9087972748ade2f1094ccce34" -dependencies = [ - "documented-macros", - "phf 0.12.1", - "thiserror 2.0.17", -] - -[[package]] -name = "documented-macros" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1149cf7462e5e79e17a3c05fd5b1f9055092bbfa95e04c319395c3beacc9370f" -dependencies = [ - "convert_case 0.8.0", - "itertools 0.14.0", - "optfield", - "proc-macro2", - "quote", - "strum 0.27.2", - "syn 2.0.106", -] - -[[package]] -name = "dotenvy" -version = "0.15.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" - [[package]] name = "downcast-rs" version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" -[[package]] -name = "doxygen-rs" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "415b6ec780d34dcf624666747194393603d0373b7141eef01d12ee58881507d9" -dependencies = [ - "phf 0.11.3", -] - -[[package]] -name = "dtoa" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6add3b8cff394282be81f3fc1a0605db594ed69890078ca6e2cab1c408bcf04" - -[[package]] -name = "dtoa-short" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" -dependencies = [ - "dtoa", -] - [[package]] name = "dtor" version = "0.0.6" @@ -5053,367 +1614,11 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" -[[package]] -name = "dyn-stack" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e53799688f5632f364f8fb387488dd05db9fe45db7011be066fc20e7027f8b" -dependencies = [ - "bytemuck", - "reborrow", -] - -[[package]] -name = "dyn-stack" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c4713e43e2886ba72b8271aa66c93d722116acf7a75555cce11dcde84388fe8" -dependencies = [ - "bytemuck", - "dyn-stack-macros", -] - -[[package]] -name = "dyn-stack-macros" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d926b4d407d372f141f93bb444696142c29d32962ccbd3531117cf3aa0bfa9" - -[[package]] -name = "ec4rs" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b31a881d38439026e3d5dd938ab20328d36e23caca8fd5981c42e4b677f5842" - -[[package]] -name = "ecdsa" -version = "0.14.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" -dependencies = [ - "der 0.6.1", - "elliptic-curve", - "rfc6979", - "signature 1.6.4", -] - -[[package]] -name = "edit_prediction" -version = "0.1.0" -dependencies = [ - "ai_onboarding", - "anyhow", - "arrayvec", - "brotli", - "client", - "clock", - "cloud_api_types", - "cloud_llm_client", - "collections", - "copilot", - "credentials_provider", - "ctor", - "db", - "edit_prediction_context", - "edit_prediction_types", - "feature_flags", - "fs", - "futures 0.3.31", - "gpui", - "indoc", - "itertools 0.14.0", - "language", - "language_model", - "log", - "lsp", - "menu", - "open_ai", - "parking_lot", - "postage", - "pretty_assertions", - "project", - "rand 0.9.2", - "regex", - "release_channel", - "semver", - "serde", - "serde_json", - "settings", - "strum 0.27.2", - "telemetry", - "telemetry_events", - "thiserror 2.0.17", - "ui", - "util", - "uuid", - "workspace", - "worktree", - "zed_actions", - "zeta_prompt", - "zlog", -] - -[[package]] -name = "edit_prediction_cli" -version = "0.1.0" -dependencies = [ - "anthropic", - "anyhow", - "chrono", - "clap", - "client", - "cloud_llm_client", - "collections", - "debug_adapter_extension", - "dirs 4.0.0", - "edit_prediction", - "extension", - "fs", - "futures 0.3.31", - "gpui", - "gpui_tokio", - "http_client", - "indoc", - "language", - "language_extension", - "language_model", - "language_models", - "languages", - "libc", - "log", - "node_runtime", - "paths", - "pretty_assertions", - "project", - "prompt_store", - "pulldown-cmark 0.12.2", - "release_channel", - "reqwest_client", - "serde", - "serde_json", - "settings", - "shellexpand 2.1.2", - "smol", - "sqlez", - "sqlez_macros", - "terminal_view", - "util", - "wasmtime", - "watch", - "zeta_prompt", -] - -[[package]] -name = "edit_prediction_context" -version = "0.1.0" -dependencies = [ - "anyhow", - "cloud_llm_client", - "collections", - "env_logger 0.11.8", - "futures 0.3.31", - "gpui", - "indoc", - "language", - "log", - "lsp", - "parking_lot", - "pretty_assertions", - "project", - "serde", - "serde_json", - "settings", - "smallvec", - "text", - "tree-sitter", - "util", - "zeta_prompt", - "zlog", -] - -[[package]] -name = "edit_prediction_types" -version = "0.1.0" -dependencies = [ - "client", - "gpui", - "language", - "text", -] - -[[package]] -name = "edit_prediction_ui" -version = "0.1.0" -dependencies = [ - "anyhow", - "buffer_diff", - "client", - "cloud_llm_client", - "codestral", - "command_palette_hooks", - "copilot", - "edit_prediction", - "edit_prediction_types", - "editor", - "feature_flags", - "fs", - "futures 0.3.31", - "gpui", - "indoc", - "language", - "lsp", - "markdown", - "menu", - "multi_buffer", - "paths", - "project", - "regex", - "serde_json", - "settings", - "supermaven", - "telemetry", - "text", - "theme", - "ui", - "ui_input", - "util", - "workspace", - "zed_actions", - "zeta_prompt", -] - -[[package]] -name = "editor" -version = "0.1.0" -dependencies = [ - "aho-corasick", - "anyhow", - "assets", - "buffer_diff", - "client", - "clock", - "collections", - "convert_case 0.8.0", - "criterion", - "ctor", - "dap", - "db", - "edit_prediction_types", - "emojis", - "feature_flags", - "file_icons", - "fs", - "futures 0.3.31", - "fuzzy", - "git", - "gpui", - "http_client", - "indoc", - "itertools 0.14.0", - "language", - "languages", - "linkify", - "log", - "lsp", - "markdown", - "menu", - "multi_buffer", - "ordered-float 2.10.1", - "parking_lot", - "pretty_assertions", - "project", - "rand 0.9.2", - "regex", - "release_channel", - "rope", - "rpc", - "schemars", - "semver", - "serde", - "serde_json", - "settings", - "smallvec", - "smol", - "snippet", - "sum_tree", - "task", - "telemetry", - "tempfile", - "text", - "theme", - "time", - "tracing", - "tree-sitter-bash", - "tree-sitter-c", - "tree-sitter-html", - "tree-sitter-md", - "tree-sitter-python", - "tree-sitter-rust", - "tree-sitter-typescript", - "tree-sitter-yaml", - "ui", - "unicode-script", - "unicode-segmentation", - "unindent", - "url", - "util", - "uuid", - "vim_mode_setting", - "workspace", - "zed_actions", - "zlog", - "ztracing", -] - [[package]] name = "either" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -dependencies = [ - "serde", -] - -[[package]] -name = "elasticlunr-rs" -version = "3.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41e83863a500656dfa214fee6682de9c5b9f03de6860fec531235ed2ae9f6571" -dependencies = [ - "regex", - "serde", - "serde_derive", - "serde_json", -] - -[[package]] -name = "elliptic-curve" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" -dependencies = [ - "base16ct", - "crypto-bigint 0.4.9", - "der 0.6.1", - "digest", - "ff", - "generic-array", - "group", - "pkcs8 0.9.0", - "rand_core 0.6.4", - "sec1", - "subtle", - "zeroize", -] - -[[package]] -name = "email_address" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" -dependencies = [ - "serde", -] [[package]] name = "embed-resource" @@ -5426,36 +1631,9 @@ dependencies = [ "rustc_version", "toml 0.9.8", "vswhom", - "winreg 0.55.0", + "winreg", ] -[[package]] -name = "embedded-io" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" - -[[package]] -name = "embedded-io" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" - -[[package]] -name = "emojis" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99e1f1df1f181f2539bac8bf027d31ca5ffbf9e559e3f2d09413b9107b5c02f4" -dependencies = [ - "phf 0.11.3", -] - -[[package]] -name = "encode_unicode" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" - [[package]] name = "encoding_rs" version = "0.8.35" @@ -5471,18 +1649,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3d8a32ae18130a3c84dd492d4215c3d913c3b07c6b63c2eb3eb7ff1101ab7bf" -[[package]] -name = "enum-as-inner" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" -dependencies = [ - "heck 0.5.0", - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "enumflags2" version = "0.7.12" @@ -5514,19 +1680,6 @@ dependencies = [ "regex", ] -[[package]] -name = "env_logger" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" -dependencies = [ - "humantime", - "is-terminal", - "log", - "regex", - "termcolor", -] - [[package]] name = "env_logger" version = "0.11.8" @@ -5540,15 +1693,6 @@ dependencies = [ "log", ] -[[package]] -name = "envy" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965" -dependencies = [ - "serde", -] - [[package]] name = "equator" version = "0.4.2" @@ -5586,17 +1730,6 @@ dependencies = [ "typeid", ] -[[package]] -name = "errno" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" -dependencies = [ - "errno-dragonfly", - "libc", - "winapi", -] - [[package]] name = "errno" version = "0.3.14" @@ -5607,16 +1740,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "errno-dragonfly" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "etagere" version = "0.2.15" @@ -5627,17 +1750,6 @@ dependencies = [ "svg_fmt", ] -[[package]] -name = "etcetera" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" -dependencies = [ - "cfg-if", - "home", - "windows-sys 0.48.0", -] - [[package]] name = "euclid" version = "0.22.11" @@ -5647,70 +1759,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "eval" -version = "0.1.0" -dependencies = [ - "acp_thread", - "agent", - "agent-client-protocol", - "agent_settings", - "agent_ui", - "anyhow", - "async-trait", - "buffer_diff", - "chrono", - "clap", - "client", - "collections", - "debug_adapter_extension", - "dirs 4.0.0", - "dotenvy", - "env_logger 0.11.8", - "extension", - "fs", - "futures 0.3.31", - "gpui", - "gpui_tokio", - "handlebars 4.5.0", - "language", - "language_extension", - "language_model", - "language_models", - "languages", - "markdown", - "node_runtime", - "pathdiff", - "paths", - "pretty_assertions", - "project", - "prompt_store", - "rand 0.9.2", - "regex", - "release_channel", - "reqwest_client", - "serde", - "serde_json", - "settings", - "shellexpand 2.1.2", - "telemetry", - "terminal_view", - "toml 0.8.23", - "unindent", - "util", - "uuid", - "watch", -] - -[[package]] -name = "eval_utils" -version = "0.1.0" -dependencies = [ - "gpui", - "serde", - "smol", -] - [[package]] name = "event-listener" version = "2.5.3" @@ -5738,25 +1786,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "exec" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "886b70328cba8871bfc025858e1de4be16b1d5088f2ba50b57816f4210672615" -dependencies = [ - "errno 0.2.8", - "libc", -] - -[[package]] -name = "explorer_command_injector" -version = "0.1.0" -dependencies = [ - "windows 0.61.3", - "windows-core 0.61.2", - "windows-registry 0.5.3", -] - [[package]] name = "exr" version = "1.73.0" @@ -5772,175 +1801,6 @@ dependencies = [ "zune-inflate", ] -[[package]] -name = "extended" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af9673d8203fcb076b19dfd17e38b3d4ae9f44959416ea532ce72415a6020365" - -[[package]] -name = "extension" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "collections", - "dap", - "fs", - "futures 0.3.31", - "gpui", - "heck 0.5.0", - "http_client", - "indoc", - "language", - "log", - "lsp", - "parking_lot", - "pretty_assertions", - "proto", - "semver", - "serde", - "serde_json", - "task", - "tempfile", - "toml 0.8.23", - "url", - "util", - "wasm-encoder 0.221.3", - "wasmparser 0.221.3", -] - -[[package]] -name = "extension_cli" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "env_logger 0.11.8", - "extension", - "fs", - "gpui", - "language", - "log", - "reqwest_client", - "rpc", - "serde", - "serde_json", - "theme", - "tokio", - "toml 0.8.23", - "tree-sitter", - "wasmtime", -] - -[[package]] -name = "extension_host" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-compression", - "async-tar", - "async-trait", - "client", - "collections", - "criterion", - "ctor", - "dap", - "extension", - "fs", - "futures 0.3.31", - "gpui", - "gpui_tokio", - "http_client", - "language", - "language_extension", - "log", - "lsp", - "moka", - "node_runtime", - "parking_lot", - "paths", - "project", - "rand 0.9.2", - "release_channel", - "remote", - "reqwest_client", - "semver", - "serde", - "serde_json", - "serde_json_lenient", - "settings", - "task", - "telemetry", - "tempfile", - "theme", - "theme_extension", - "toml 0.8.23", - "url", - "util", - "wasmparser 0.221.3", - "wasmtime", - "wasmtime-wasi", - "zlog", -] - -[[package]] -name = "extensions_ui" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "collections", - "db", - "editor", - "extension", - "extension_host", - "fs", - "fuzzy", - "gpui", - "language", - "log", - "num-format", - "picker", - "project", - "release_channel", - "semver", - "serde", - "settings", - "smallvec", - "strum 0.27.2", - "telemetry", - "theme", - "ui", - "util", - "vim_mode_setting", - "workspace", - "zed_actions", -] - -[[package]] -name = "fallible-iterator" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" - -[[package]] -name = "fancy-regex" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f" -dependencies = [ - "bit-set", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "fast-srgb8" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1" - [[package]] name = "fastrand" version = "1.9.0" @@ -5976,17 +1836,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "fd-lock" -version = "4.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce92ff622d6dadf7349484f42c93271a0d49b7cc4d466a936405bacbe10aa78" -dependencies = [ - "cfg-if", - "rustix 1.1.2", - "windows-sys 0.59.0", -] - [[package]] name = "fdeflate" version = "0.3.7" @@ -5996,78 +1845,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "feature_flags" -version = "0.1.0" -dependencies = [ - "futures 0.3.31", - "gpui", - "smol", -] - -[[package]] -name = "feedback" -version = "0.1.0" -dependencies = [ - "editor", - "gpui", - "system_specs", - "urlencoding", - "util", - "workspace", - "zed_actions", -] - -[[package]] -name = "ff" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "file_finder" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "ctor", - "editor", - "file_icons", - "futures 0.3.31", - "fuzzy", - "gpui", - "language", - "menu", - "picker", - "pretty_assertions", - "project", - "schemars", - "search", - "serde", - "serde_json", - "settings", - "text", - "theme", - "ui", - "util", - "workspace", - "zlog", -] - -[[package]] -name = "file_icons" -version = "0.1.0" -dependencies = [ - "gpui", - "serde", - "theme", - "util", -] - [[package]] name = "filedescriptor" version = "0.8.3" @@ -6097,12 +1874,6 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" -[[package]] -name = "fixedbitset" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" - [[package]] name = "flate2" version = "1.1.4" @@ -6125,35 +1896,12 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce81f49ae8a0482e4c55ea62ebbd7e5a686af544c00b9d090bba3ff9be97b3d" -[[package]] -name = "float8" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4203231de188ebbdfb85c11f3c20ca2b063945710de04e7b59268731e728b462" -dependencies = [ - "half", - "num-traits", - "rand 0.9.2", - "rand_distr", -] - [[package]] name = "float_next_after" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8" -[[package]] -name = "fluent-uri" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e" -dependencies = [ - "borrow-or-share", - "ref-cast", - "serde", -] - [[package]] name = "flume" version = "0.11.1" @@ -6178,12 +1926,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - [[package]] name = "font-types" version = "0.10.0" @@ -6230,15 +1972,6 @@ dependencies = [ "ttf-parser 0.25.1", ] -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared 0.1.1", -] - [[package]] name = "foreign-types" version = "0.5.0" @@ -6246,7 +1979,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" dependencies = [ "foreign-types-macros", - "foreign-types-shared 0.3.1", + "foreign-types-shared", ] [[package]] @@ -6260,27 +1993,12 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "foreign-types-shared" version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" -[[package]] -name = "fork" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30268f1eefccc9d72f43692e8b89e659aeb52e84016c3b32b6e7e9f1c8f38f94" -dependencies = [ - "libc", -] - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -6290,16 +2008,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fraction" -version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f158e3ff0a1b334408dc9fb811cd99b446986f4d8b741bb08f9df1604085ae7" -dependencies = [ - "lazy_static", - "num", -] - [[package]] name = "freetype-sys" version = "0.20.1" @@ -6311,111 +2019,12 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "fs" -version = "0.1.0" -dependencies = [ - "anyhow", - "ashpd 0.11.0", - "async-tar", - "async-trait", - "cocoa 0.26.0", - "collections", - "fsevent", - "futures 0.3.31", - "git", - "gpui", - "ignore", - "is_executable", - "libc", - "log", - "notify 8.2.0", - "objc", - "parking_lot", - "paths", - "proto", - "rope", - "serde", - "serde_json", - "smol", - "tempfile", - "text", - "time", - "util", - "windows 0.61.3", -] - -[[package]] -name = "fs-set-times" -version = "0.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94e7099f6313ecacbe1256e8ff9d617b75d1bcb16a6fddef94866d225a01a14a" -dependencies = [ - "io-lifetimes", - "rustix 1.1.2", - "windows-sys 0.59.0", -] - -[[package]] -name = "fs2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "fs_benchmarks" -version = "0.1.0" -dependencies = [ - "fs", - "gpui", -] - [[package]] name = "fs_extra" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" -[[package]] -name = "fsevent" -version = "0.1.0" -dependencies = [ - "bitflags 2.9.4", - "core-foundation 0.10.0", - "fsevent-sys 3.1.0", - "log", - "parking_lot", - "tempfile", -] - -[[package]] -name = "fsevent-sys" -version = "3.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca6f5e6817058771c10f0eb0f05ddf1e35844266f972004fe8e4b21fda295bd5" -dependencies = [ - "libc", -] - -[[package]] -name = "fsevent-sys" -version = "4.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" -dependencies = [ - "libc", -] - -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - [[package]] name = "futf" version = "0.1.5" @@ -6426,12 +2035,6 @@ dependencies = [ "new_debug_unreachable", ] -[[package]] -name = "futures" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" - [[package]] name = "futures" version = "0.3.31" @@ -6474,17 +2077,6 @@ dependencies = [ "futures-util", ] -[[package]] -name = "futures-intrusive" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" -dependencies = [ - "futures-core", - "lock_api", - "parking_lot", -] - [[package]] name = "futures-io" version = "0.3.31" @@ -6548,7 +2140,6 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ - "futures 0.1.31", "futures-channel", "futures-core", "futures-io", @@ -6559,285 +2150,6 @@ dependencies = [ "pin-project-lite", "pin-utils", "slab", - "tokio-io", -] - -[[package]] -name = "fuzzy" -version = "0.1.0" -dependencies = [ - "gpui", - "log", - "util", -] - -[[package]] -name = "fuzzy-matcher" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" -dependencies = [ - "thread_local", -] - -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - -[[package]] -name = "gemm" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ab24cc62135b40090e31a76a9b2766a501979f3070fa27f689c27ec04377d32" -dependencies = [ - "dyn-stack 0.10.0", - "gemm-c32 0.17.1", - "gemm-c64 0.17.1", - "gemm-common 0.17.1", - "gemm-f16 0.17.1", - "gemm-f32 0.17.1", - "gemm-f64 0.17.1", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 10.7.0", - "seq-macro", -] - -[[package]] -name = "gemm" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab96b703d31950f1aeddded248bc95543c9efc7ac9c4a21fda8703a83ee35451" -dependencies = [ - "dyn-stack 0.13.2", - "gemm-c32 0.18.2", - "gemm-c64 0.18.2", - "gemm-common 0.18.2", - "gemm-f16 0.18.2", - "gemm-f32 0.18.2", - "gemm-f64 0.18.2", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 11.6.0", - "seq-macro", -] - -[[package]] -name = "gemm-c32" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9c030d0b983d1e34a546b86e08f600c11696fde16199f971cd46c12e67512c0" -dependencies = [ - "dyn-stack 0.10.0", - "gemm-common 0.17.1", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 10.7.0", - "seq-macro", -] - -[[package]] -name = "gemm-c32" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6db9fd9f40421d00eea9dd0770045a5603b8d684654816637732463f4073847" -dependencies = [ - "dyn-stack 0.13.2", - "gemm-common 0.18.2", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 11.6.0", - "seq-macro", -] - -[[package]] -name = "gemm-c64" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbb5f2e79fefb9693d18e1066a557b4546cd334b226beadc68b11a8f9431852a" -dependencies = [ - "dyn-stack 0.10.0", - "gemm-common 0.17.1", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 10.7.0", - "seq-macro", -] - -[[package]] -name = "gemm-c64" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfcad8a3d35a43758330b635d02edad980c1e143dc2f21e6fd25f9e4eada8edf" -dependencies = [ - "dyn-stack 0.13.2", - "gemm-common 0.18.2", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 11.6.0", - "seq-macro", -] - -[[package]] -name = "gemm-common" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2e7ea062c987abcd8db95db917b4ffb4ecdfd0668471d8dc54734fdff2354e8" -dependencies = [ - "bytemuck", - "dyn-stack 0.10.0", - "half", - "num-complex", - "num-traits", - "once_cell", - "paste", - "pulp 0.18.22", - "raw-cpuid 10.7.0", - "rayon", - "seq-macro", - "sysctl 0.5.5", -] - -[[package]] -name = "gemm-common" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a352d4a69cbe938b9e2a9cb7a3a63b7e72f9349174a2752a558a8a563510d0f3" -dependencies = [ - "bytemuck", - "dyn-stack 0.13.2", - "half", - "libm", - "num-complex", - "num-traits", - "once_cell", - "paste", - "pulp 0.21.5", - "raw-cpuid 11.6.0", - "rayon", - "seq-macro", - "sysctl 0.6.0", -] - -[[package]] -name = "gemm-f16" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ca4c06b9b11952071d317604acb332e924e817bd891bec8dfb494168c7cedd4" -dependencies = [ - "dyn-stack 0.10.0", - "gemm-common 0.17.1", - "gemm-f32 0.17.1", - "half", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 10.7.0", - "rayon", - "seq-macro", -] - -[[package]] -name = "gemm-f16" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff95ae3259432f3c3410eaa919033cd03791d81cebd18018393dc147952e109" -dependencies = [ - "dyn-stack 0.13.2", - "gemm-common 0.18.2", - "gemm-f32 0.18.2", - "half", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 11.6.0", - "rayon", - "seq-macro", -] - -[[package]] -name = "gemm-f32" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9a69f51aaefbd9cf12d18faf273d3e982d9d711f60775645ed5c8047b4ae113" -dependencies = [ - "dyn-stack 0.10.0", - "gemm-common 0.17.1", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 10.7.0", - "seq-macro", -] - -[[package]] -name = "gemm-f32" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc8d3d4385393304f407392f754cd2dc4b315d05063f62cf09f47b58de276864" -dependencies = [ - "dyn-stack 0.13.2", - "gemm-common 0.18.2", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 11.6.0", - "seq-macro", -] - -[[package]] -name = "gemm-f64" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa397a48544fadf0b81ec8741e5c0fba0043008113f71f2034def1935645d2b0" -dependencies = [ - "dyn-stack 0.10.0", - "gemm-common 0.17.1", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 10.7.0", - "seq-macro", -] - -[[package]] -name = "gemm-f64" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35b2a4f76ce4b8b16eadc11ccf2e083252d8237c1b589558a49b0183545015bd" -dependencies = [ - "dyn-stack 0.13.2", - "gemm-common 0.18.2", - "num-complex", - "num-traits", - "paste", - "raw-cpuid 11.6.0", - "seq-macro", -] - -[[package]] -name = "generator" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "605183a538e3e2a9c1038635cc5c2d194e2ee8fd0d1b66b8349fad7dbacce5a2" -dependencies = [ - "cc", - "cfg-if", - "libc", - "log", - "rustversion", - "windows 0.61.3", ] [[package]] @@ -6887,33 +2199,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "gh-workflow" -version = "0.8.0" -source = "git+https://github.com/zed-industries/gh-workflow?rev=09acfdf2bd5c1d6254abefd609c808ff73547b2c#09acfdf2bd5c1d6254abefd609c808ff73547b2c" -dependencies = [ - "async-trait", - "derive_more 2.0.1", - "derive_setters", - "gh-workflow-macros", - "indexmap", - "merge", - "serde", - "serde_json", - "serde_yaml", - "strum_macros 0.27.2", -] - -[[package]] -name = "gh-workflow-macros" -version = "0.8.0" -source = "git+https://github.com/zed-industries/gh-workflow?rev=09acfdf2bd5c1d6254abefd609c808ff73547b2c#09acfdf2bd5c1d6254abefd609c808ff73547b2c" -dependencies = [ - "heck 0.5.0", - "quote", - "syn 2.0.106", -] - [[package]] name = "gif" version = "0.13.3" @@ -6924,59 +2209,12 @@ dependencies = [ "weezl", ] -[[package]] -name = "gimli" -version = "0.31.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" -dependencies = [ - "fallible-iterator", - "indexmap", - "stable_deref_trait", -] - [[package]] name = "gimli" version = "0.32.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" -[[package]] -name = "git" -version = "0.1.0" -dependencies = [ - "anyhow", - "askpass", - "async-trait", - "collections", - "derive_more 0.99.20", - "futures 0.3.31", - "git2", - "gpui", - "http_client", - "itertools 0.14.0", - "log", - "parking_lot", - "pretty_assertions", - "rand 0.9.2", - "regex", - "rope", - "schemars", - "serde", - "serde_json", - "smol", - "sum_tree", - "tempfile", - "text", - "thiserror 2.0.17", - "time", - "unindent", - "url", - "urlencoding", - "util", - "uuid", -] - [[package]] name = "git2" version = "0.20.2" @@ -6990,88 +2228,6 @@ dependencies = [ "url", ] -[[package]] -name = "git_hosting_providers" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "futures 0.3.31", - "git", - "gpui", - "http_client", - "indoc", - "itertools 0.14.0", - "pretty_assertions", - "regex", - "serde", - "serde_json", - "settings", - "url", - "urlencoding", - "util", -] - -[[package]] -name = "git_ui" -version = "0.1.0" -dependencies = [ - "agent_settings", - "anyhow", - "askpass", - "buffer_diff", - "call", - "cloud_llm_client", - "collections", - "command_palette_hooks", - "component", - "ctor", - "db", - "editor", - "futures 0.3.31", - "fuzzy", - "git", - "git_hosting_providers", - "gpui", - "indoc", - "itertools 0.14.0", - "language", - "language_model", - "linkify", - "log", - "markdown", - "menu", - "multi_buffer", - "notifications", - "panel", - "picker", - "pretty_assertions", - "project", - "recent_projects", - "remote", - "schemars", - "serde", - "serde_json", - "settings", - "smol", - "strum 0.27.2", - "telemetry", - "theme", - "time", - "time_format", - "tracing", - "ui", - "unindent", - "util", - "watch", - "windows 0.61.3", - "workspace", - "zed_actions", - "zeroize", - "zlog", - "ztracing", -] - [[package]] name = "glob" version = "0.3.3" @@ -7115,54 +2271,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "go_to_line" -version = "0.1.0" -dependencies = [ - "editor", - "gpui", - "indoc", - "language", - "menu", - "project", - "rope", - "serde", - "serde_json", - "settings", - "text", - "theme", - "tree-sitter-rust", - "tree-sitter-typescript", - "ui", - "util", - "workspace", -] - -[[package]] -name = "goblin" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" -dependencies = [ - "log", - "plain", - "scroll", -] - -[[package]] -name = "google_ai" -version = "0.1.0" -dependencies = [ - "anyhow", - "futures 0.3.31", - "http_client", - "schemars", - "serde", - "serde_json", - "settings", - "strum 0.27.2", -] - [[package]] name = "gpu-alloc" version = "0.6.0" @@ -7223,14 +2331,14 @@ dependencies = [ "core-video", "cosmic-text", "ctor", - "derive_more 0.99.20", + "derive_more", "embed-resource", - "env_logger 0.11.8", + "env_logger", "etagere", "filedescriptor", "flume", - "foreign-types 0.5.0", - "futures 0.3.31", + "foreign-types", + "futures", "gpui_macros", "http_client", "image", @@ -7239,7 +2347,7 @@ dependencies = [ "libc", "log", "lyon", - "mach2 0.5.0", + "mach2", "media", "metal", "naga", @@ -7326,36 +2434,6 @@ version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "12101ecc8225ea6d675bc70263074eab6169079621c2186fe0c66590b2df9681" -[[package]] -name = "group" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" -dependencies = [ - "ff", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "h2" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" -dependencies = [ - "bytes 1.10.1", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - [[package]] name = "h2" version = "0.4.12" @@ -7363,11 +2441,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", - "bytes 1.10.1", + "bytes", "fnv", "futures-core", "futures-sink", - "http 1.3.1", + "http", "indexmap", "slab", "tokio", @@ -7381,62 +2459,17 @@ version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" dependencies = [ - "bytemuck", "cfg-if", "crunchy", "num-traits", - "rand 0.9.2", - "rand_distr", "zerocopy", ] -[[package]] -name = "handlebars" -version = "4.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "faa67bab9ff362228eb3d00bd024a4965d8231bbb7921167f0cfa66c6626b225" -dependencies = [ - "log", - "pest", - "pest_derive", - "rust-embed", - "serde", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] -name = "handlebars" -version = "5.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d08485b96a0e6393e9e4d1b8d48cf74ad6c063cd905eb33f42c1ce3f0377539b" -dependencies = [ - "log", - "pest", - "pest_derive", - "serde", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" -dependencies = [ - "ahash 0.7.8", -] - [[package]] name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash 0.8.12", - "allocator-api2", -] [[package]] name = "hashbrown" @@ -7444,10 +2477,7 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.1.5", - "serde", + "foldhash", ] [[package]] @@ -7455,71 +2485,12 @@ name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash 0.2.0", -] - -[[package]] -name = "hashlink" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" -dependencies = [ - "hashbrown 0.14.5", -] - -[[package]] -name = "hashlink" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" -dependencies = [ - "hashbrown 0.15.5", -] - -[[package]] -name = "headers" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06683b93020a07e3dbcf5f8c0f6d40080d725bea7936fc01ad345c01b97dc270" -dependencies = [ - "base64 0.21.7", - "bytes 1.10.1", - "headers-core", - "http 0.2.12", - "httpdate", - "mime", - "sha1", -] - -[[package]] -name = "headers-core" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" -dependencies = [ - "http 0.2.12", -] - -[[package]] -name = "heck" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] [[package]] name = "heck" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" -dependencies = [ - "unicode-segmentation", -] [[package]] name = "heck" @@ -7527,44 +2498,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "heed" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd54745cfacb7b97dee45e8fdb91814b62bccddb481debb7de0f9ee6b7bf5b43" -dependencies = [ - "bitflags 2.9.4", - "byteorder", - "heed-traits", - "heed-types", - "libc", - "lmdb-master-sys", - "once_cell", - "page_size", - "serde", - "synchronoise", - "url", -] - -[[package]] -name = "heed-traits" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb3130048d404c57ce5a1ac61a903696e8fcde7e8c2991e9fcfc1f27c3ef74ff" - -[[package]] -name = "heed-types" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c255bdf46e07fb840d120a36dcc81f385140d7191c76a7391672675c01a55d" -dependencies = [ - "bincode", - "byteorder", - "heed-traits", - "serde", - "serde_json", -] - [[package]] name = "hermit-abi" version = "0.5.2" @@ -7621,12 +2554,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "hound" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62adaabb884c94955b19907d60019f4e145d091c75345379e70d1ee696f7854f" - [[package]] name = "html5ever" version = "0.27.0" @@ -7635,76 +2562,43 @@ checksum = "c13771afe0e6e846f1e67d038d4cb29998a6779f93c809212e4e9c32efd244d4" dependencies = [ "log", "mac", - "markup5ever 0.12.1", + "markup5ever", "proc-macro2", "quote", "syn 2.0.106", ] -[[package]] -name = "html5ever" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55d958c2f74b664487a2035fe1dadb032c48718a03b63f3ab0b8537db8549ed4" -dependencies = [ - "log", - "markup5ever 0.35.0", - "match_token", -] - [[package]] name = "html_to_markdown" version = "0.1.0" dependencies = [ "anyhow", - "html5ever 0.27.0", + "html5ever", "indoc", "markup5ever_rcdom", "pretty_assertions", "regex", ] -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes 1.10.1", - "fnv", - "itoa", -] - [[package]] name = "http" version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" dependencies = [ - "bytes 1.10.1", + "bytes", "fnv", "itoa", ] -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes 1.10.1", - "http 0.2.12", - "pin-project-lite", -] - [[package]] name = "http-body" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ - "bytes 1.10.1", - "http 1.3.1", + "bytes", + "http", ] [[package]] @@ -7713,19 +2607,13 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ - "bytes 1.10.1", + "bytes", "futures-core", - "http 1.3.1", - "http-body 1.0.1", + "http", + "http-body", "pin-project-lite", ] -[[package]] -name = "http-range-header" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" - [[package]] name = "http_client" version = "0.1.0" @@ -7734,11 +2622,11 @@ dependencies = [ "async-compression", "async-fs", "async-tar", - "bytes 1.10.1", - "derive_more 0.99.20", - "futures 0.3.31", - "http 1.3.1", - "http-body 1.0.1", + "bytes", + "derive_more", + "futures", + "http", + "http-body", "log", "parking_lot", "serde", @@ -7754,7 +2642,7 @@ dependencies = [ name = "http_client_tls" version = "0.1.0" dependencies = [ - "rustls 0.23.33", + "rustls", "rustls-platform-verifier", ] @@ -7764,48 +2652,6 @@ version = "1.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "human_bytes" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91f255a4535024abf7640cb288260811fc14794f62b063652ed349f9a6c2348e" - -[[package]] -name = "humantime" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" - -[[package]] -name = "hyper" -version = "0.14.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" -dependencies = [ - "bytes 1.10.1", - "futures-channel", - "futures-core", - "futures-util", - "h2 0.3.27", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.5.10", - "tokio", - "tower-service", - "tracing", - "want", -] - [[package]] name = "hyper" version = "1.7.0" @@ -7813,12 +2659,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ "atomic-waker", - "bytes 1.10.1", + "bytes", "futures-channel", "futures-core", - "h2 0.4.12", - "http 1.3.1", - "http-body 1.0.1", + "h2", + "http", + "http-body", "httparse", "itoa", "pin-project-lite", @@ -7828,71 +2674,39 @@ dependencies = [ "want", ] -[[package]] -name = "hyper-rustls" -version = "0.24.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" -dependencies = [ - "futures-util", - "http 0.2.12", - "hyper 0.14.32", - "log", - "rustls 0.21.12", - "rustls-native-certs 0.6.3", - "tokio", - "tokio-rustls 0.24.1", -] - [[package]] name = "hyper-rustls" version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ - "http 1.3.1", - "hyper 1.7.0", + "http", + "hyper", "hyper-util", - "rustls 0.23.33", - "rustls-native-certs 0.8.2", + "rustls", + "rustls-native-certs", "rustls-pki-types", "tokio", - "tokio-rustls 0.26.2", + "tokio-rustls", "tower-service", ] -[[package]] -name = "hyper-tls" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" -dependencies = [ - "bytes 1.10.1", - "hyper 0.14.32", - "native-tls", - "tokio", - "tokio-native-tls", -] - [[package]] name = "hyper-util" version = "0.1.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c6995591a8f1380fcb4ba966a252a4b29188d51d2b89e3a252f5305be65aea8" dependencies = [ - "base64 0.22.1", - "bytes 1.10.1", + "bytes", "futures-channel", "futures-core", "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "hyper 1.7.0", - "ipnet", + "http", + "http-body", + "hyper", "libc", - "percent-encoding", "pin-project-lite", - "socket2 0.6.1", + "socket2", "tokio", "tower-service", "tracing", @@ -7922,14 +2736,6 @@ dependencies = [ "cc", ] -[[package]] -name = "icons" -version = "0.1.0" -dependencies = [ - "serde", - "strum 0.27.2", -] - [[package]] name = "icu_collections" version = "2.0.0" @@ -7938,7 +2744,7 @@ checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", "potential_utf", - "yoke 0.8.0", + "yoke", "zerofrom", "zerovec", ] @@ -8010,7 +2816,7 @@ dependencies = [ "stable_deref_trait", "tinystr", "writeable", - "yoke 0.8.0", + "yoke", "zerofrom", "zerotrie", "zerovec", @@ -8022,12 +2828,6 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - [[package]] name = "idna" version = "1.1.0" @@ -8049,22 +2849,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "ignore" -version = "0.4.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81776e6f9464432afcc28d03e52eb101c93b6f0566f52aef2427663e700f0403" -dependencies = [ - "crossbeam-deque", - "globset", - "log", - "memchr", - "regex-automata", - "same-file", - "walkdir", - "winapi-util", -] - [[package]] name = "image" version = "0.25.8" @@ -8099,41 +2883,12 @@ dependencies = [ "quick-error", ] -[[package]] -name = "image_viewer" -version = "0.1.0" -dependencies = [ - "anyhow", - "db", - "editor", - "file_icons", - "gpui", - "language", - "log", - "project", - "serde", - "settings", - "theme", - "ui", - "util", - "workspace", -] - [[package]] name = "imagesize" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" -[[package]] -name = "imara-diff" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17d34b7d42178945f775e84bc4c36dde7c1c6cdfea656d3354d009056f2bb3d2" -dependencies = [ - "hashbrown 0.15.5", -] - [[package]] name = "imgref" version = "1.12.0" @@ -8158,48 +2913,6 @@ version = "2.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" -[[package]] -name = "inherent" -version = "1.0.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c727f80bfa4a6c6e2508d2f05b6f4bfce242030bd88ed15ae5331c5b5d30fba7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "inotify" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" -dependencies = [ - "bitflags 1.3.2", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3" -dependencies = [ - "bitflags 2.9.4", - "inotify-sys", - "libc", -] - -[[package]] -name = "inotify-sys" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" -dependencies = [ - "libc", -] - [[package]] name = "inout" version = "0.1.4" @@ -8210,41 +2923,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "inspector_ui" -version = "0.1.0" -dependencies = [ - "anyhow", - "command_palette_hooks", - "editor", - "fuzzy", - "gpui", - "language", - "project", - "serde_json", - "serde_json_lenient", - "theme", - "title_bar", - "ui", - "util", - "util_macros", - "workspace", - "zed_actions", -] - -[[package]] -name = "install_cli" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "gpui", - "release_channel", - "smol", - "util", - "workspace", -] - [[package]] name = "instant" version = "0.1.13" @@ -8274,22 +2952,6 @@ dependencies = [ "rustversion", ] -[[package]] -name = "io-extras" -version = "0.18.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65" -dependencies = [ - "io-lifetimes", - "windows-sys 0.59.0", -] - -[[package]] -name = "io-lifetimes" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06432fb54d3be7964ecd3649233cddf80db2832f47fec34c01f65b3d9d774983" - [[package]] name = "io-surface" version = "0.16.1" @@ -8302,50 +2964,12 @@ dependencies = [ "leaky-cow", ] -[[package]] -name = "iovec" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" -dependencies = [ - "libc", -] - -[[package]] -name = "ipc-channel" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb8251fb7bcd9ccd3725ed8deae9fe7db8e586495c9eb5b0c52e6233e5e75ea" -dependencies = [ - "bincode", - "crossbeam-channel", - "fnv", - "lazy_static", - "libc", - "mio 1.1.0", - "rand 0.8.5", - "serde", - "tempfile", - "uuid", - "windows 0.58.0", -] - [[package]] name = "ipnet" version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" -[[package]] -name = "iri-string" -version = "0.7.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" -dependencies = [ - "memchr", - "serde", -] - [[package]] name = "is-docker" version = "0.2.0" @@ -8355,17 +2979,6 @@ dependencies = [ "once_cell", ] -[[package]] -name = "is-terminal" -version = "0.4.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" -dependencies = [ - "hermit-abi", - "libc", - "windows-sys 0.59.0", -] - [[package]] name = "is-wsl" version = "0.4.0" @@ -8376,39 +2989,12 @@ dependencies = [ "once_cell", ] -[[package]] -name = "is_executable" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baabb8b4867b26294d818bf3f651a454b6901431711abb96e296245888d6e8c4" -dependencies = [ - "windows-sys 0.60.2", -] - [[package]] name = "is_terminal_polyfill" version = "1.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.12.1" @@ -8489,21 +3075,6 @@ dependencies = [ "libc", ] -[[package]] -name = "journal" -version = "0.1.0" -dependencies = [ - "anyhow", - "chrono", - "editor", - "gpui", - "log", - "serde", - "settings", - "shellexpand 2.1.2", - "workspace", -] - [[package]] name = "js-sys" version = "0.3.81" @@ -8514,153 +3085,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "json_dotpath" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbdcfef3cf5591f0cef62da413ae795e3d1f5a00936ccec0b2071499a32efd1a" -dependencies = [ - "serde", - "serde_derive", - "serde_json", - "thiserror 1.0.69", -] - -[[package]] -name = "json_schema_store" -version = "0.1.0" -dependencies = [ - "anyhow", - "dap", - "extension", - "gpui", - "language", - "paths", - "project", - "schemars", - "serde", - "serde_json", - "settings", - "snippet_provider", - "task", - "theme", - "util", -] - -[[package]] -name = "jsonschema" -version = "0.37.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73c9ffb2b5c56d58030e1b532d8e8389da94590515f118cf35b5cb68e4764a7e" -dependencies = [ - "ahash 0.8.12", - "bytecount", - "data-encoding", - "email_address", - "fancy-regex", - "fraction", - "getrandom 0.3.4", - "idna", - "itoa", - "num-cmp", - "num-traits", - "percent-encoding", - "referencing", - "regex", - "regex-syntax", - "reqwest 0.12.24", - "serde", - "serde_json", - "unicode-general-category", - "uuid-simd", -] - -[[package]] -name = "jsonwebtoken" -version = "9.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" -dependencies = [ - "base64 0.22.1", - "js-sys", - "pem", - "ring", - "serde", - "serde_json", - "simple_asn1", -] - -[[package]] -name = "jupyter-protocol" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c047f6b5e551563af2ddb13dafed833f0ec5a5b0f9621d5ad740a9ff1e1095" -dependencies = [ - "async-trait", - "bytes 1.10.1", - "chrono", - "futures 0.3.31", - "serde", - "serde_json", - "thiserror 2.0.17", - "uuid", -] - -[[package]] -name = "jupyter-websocket-client" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4197fa926a6b0bddfed7377d9fed3d00a0dec44a1501e020097bd26604699cae" -dependencies = [ - "anyhow", - "async-trait", - "async-tungstenite", - "futures 0.3.31", - "jupyter-protocol", - "serde", - "serde_json", - "tokio", - "url", - "uuid", -] - -[[package]] -name = "keymap_editor" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "command_palette", - "component", - "db", - "editor", - "fs", - "fuzzy", - "gpui", - "itertools 0.14.0", - "json_schema_store", - "language", - "log", - "menu", - "notifications", - "paths", - "project", - "search", - "serde", - "serde_json", - "settings", - "telemetry", - "tempfile", - "theme", - "tree-sitter-json", - "tree-sitter-rust", - "ui", - "ui_input", - "util", - "workspace", - "zed_actions", -] - [[package]] name = "khronos-egl" version = "6.0.0" @@ -8671,26 +3095,6 @@ dependencies = [ "libloading", ] -[[package]] -name = "kqueue" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a" -dependencies = [ - "kqueue-sys", - "libc", -] - -[[package]] -name = "kqueue-sys" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" -dependencies = [ - "bitflags 1.3.2", - "libc", -] - [[package]] name = "kurbo" version = "0.11.3" @@ -8711,305 +3115,6 @@ dependencies = [ "log", ] -[[package]] -name = "language" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "clock", - "collections", - "ctor", - "diffy", - "ec4rs", - "fs", - "futures 0.3.31", - "fuzzy", - "globset", - "gpui", - "http_client", - "imara-diff", - "indoc", - "itertools 0.14.0", - "log", - "lsp", - "parking_lot", - "postage", - "pretty_assertions", - "rand 0.9.2", - "regex", - "rpc", - "schemars", - "serde", - "serde_json", - "settings", - "shellexpand 2.1.2", - "smallvec", - "smol", - "streaming-iterator", - "strsim", - "sum_tree", - "task", - "text", - "theme", - "toml 0.8.23", - "tree-sitter", - "tree-sitter-elixir", - "tree-sitter-embedded-template", - "tree-sitter-heex", - "tree-sitter-html", - "tree-sitter-json", - "tree-sitter-md", - "tree-sitter-python", - "tree-sitter-ruby", - "tree-sitter-rust", - "tree-sitter-typescript", - "unicase", - "unindent", - "util", - "watch", - "zlog", -] - -[[package]] -name = "language_extension" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "collections", - "extension", - "fs", - "futures 0.3.31", - "gpui", - "language", - "log", - "lsp", - "project", - "serde", - "serde_json", - "util", -] - -[[package]] -name = "language_model" -version = "0.1.0" -dependencies = [ - "anthropic", - "anyhow", - "base64 0.22.1", - "client", - "cloud_api_types", - "cloud_llm_client", - "collections", - "futures 0.3.31", - "gpui", - "http_client", - "icons", - "image", - "log", - "open_ai", - "open_router", - "parking_lot", - "proto", - "schemars", - "serde", - "serde_json", - "settings", - "smol", - "telemetry_events", - "thiserror 2.0.17", - "util", -] - -[[package]] -name = "language_models" -version = "0.1.0" -dependencies = [ - "ai_onboarding", - "anthropic", - "anyhow", - "aws-config", - "aws-credential-types", - "aws_http_client", - "bedrock", - "chrono", - "client", - "cloud_llm_client", - "collections", - "component", - "convert_case 0.8.0", - "copilot", - "credentials_provider", - "deepseek", - "editor", - "fs", - "futures 0.3.31", - "google_ai", - "gpui", - "gpui_tokio", - "http_client", - "language", - "language_model", - "lmstudio", - "log", - "menu", - "mistral", - "ollama", - "open_ai", - "open_router", - "partial-json-fixer", - "project", - "release_channel", - "schemars", - "semver", - "serde", - "serde_json", - "settings", - "smol", - "strum 0.27.2", - "thiserror 2.0.17", - "tiktoken-rs", - "tokio", - "ui", - "ui_input", - "util", - "vercel", - "x_ai", - "zed_env_vars", -] - -[[package]] -name = "language_onboarding" -version = "0.1.0" -dependencies = [ - "db", - "editor", - "gpui", - "project", - "ui", - "workspace", -] - -[[package]] -name = "language_selector" -version = "0.1.0" -dependencies = [ - "anyhow", - "editor", - "file_finder", - "file_icons", - "fuzzy", - "gpui", - "language", - "picker", - "project", - "settings", - "ui", - "util", - "workspace", -] - -[[package]] -name = "language_tools" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "collections", - "command_palette_hooks", - "copilot", - "editor", - "futures 0.3.31", - "gpui", - "itertools 0.14.0", - "language", - "lsp", - "project", - "proto", - "release_channel", - "semver", - "serde_json", - "settings", - "theme", - "tree-sitter", - "ui", - "util", - "workspace", - "zed_actions", - "zlog", -] - -[[package]] -name = "languages" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-compression", - "async-fs", - "async-tar", - "async-trait", - "chrono", - "collections", - "futures 0.3.31", - "globset", - "gpui", - "http_client", - "itertools 0.14.0", - "json_schema_store", - "language", - "log", - "lsp", - "node_runtime", - "parking_lot", - "pet", - "pet-conda", - "pet-core", - "pet-fs", - "pet-poetry", - "pet-reporter", - "pet-virtualenv", - "pretty_assertions", - "project", - "regex", - "rope", - "rust-embed", - "serde", - "serde_json", - "serde_json_lenient", - "settings", - "smallvec", - "smol", - "snippet", - "task", - "terminal", - "text", - "theme", - "toml 0.8.23", - "tree-sitter", - "tree-sitter-bash", - "tree-sitter-c", - "tree-sitter-cpp", - "tree-sitter-css", - "tree-sitter-diff", - "tree-sitter-gitcommit", - "tree-sitter-go", - "tree-sitter-gomod", - "tree-sitter-gowork", - "tree-sitter-jsdoc", - "tree-sitter-json", - "tree-sitter-md", - "tree-sitter-python", - "tree-sitter-regex", - "tree-sitter-rust", - "tree-sitter-typescript", - "tree-sitter-yaml", - "unindent", - "url", - "util", - "workspace", -] - [[package]] name = "lazy_static" version = "1.5.0" @@ -9034,12 +3139,6 @@ dependencies = [ "leak", ] -[[package]] -name = "leb128" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" - [[package]] name = "leb128fmt" version = "0.1.0" @@ -9058,16 +3157,6 @@ version = "0.2.177" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" -[[package]] -name = "libdbus-sys" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cbe856efeb50e4681f010e9aaa2bf0a644e10139e54cde10fc83a307c23bd9f" -dependencies = [ - "cc", - "pkg-config", -] - [[package]] name = "libfuzzer-sys" version = "0.4.10" @@ -9106,16 +3195,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" -[[package]] -name = "libmimalloc-sys" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "667f4fec20f29dfc6bc7357c582d91796c169ad7e2fce709468aefeb2c099870" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "libredox" version = "0.1.10" @@ -9127,40 +3206,6 @@ dependencies = [ "redox_syscall 0.5.18", ] -[[package]] -name = "libsqlite3-sys" -version = "0.30.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" -dependencies = [ - "cc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "libwebrtc" -version = "0.3.10" -source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=5f04705ac3f356350ae31534ffbc476abc9ea83d#5f04705ac3f356350ae31534ffbc476abc9ea83d" -dependencies = [ - "cxx", - "jni", - "js-sys", - "lazy_static", - "livekit-protocol", - "livekit-runtime", - "log", - "parking_lot", - "serde", - "serde_json", - "thiserror 1.0.69", - "tokio", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "webrtc-sys", -] - [[package]] name = "libz-sys" version = "1.1.22" @@ -9173,38 +3218,6 @@ dependencies = [ "vcpkg", ] -[[package]] -name = "line_ending_selector" -version = "0.1.0" -dependencies = [ - "editor", - "gpui", - "language", - "picker", - "project", - "ui", - "util", - "workspace", -] - -[[package]] -name = "link-cplusplus" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f78c730aaa7d0b9336a299029ea49f9ee53b0ed06e9202e8cb7db9bae7b8c82" -dependencies = [ - "cc", -] - -[[package]] -name = "linkify" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1dfa36d52c581e9ec783a7ce2a5e0143da6237be5811a0b3153fedfdbe9f780" -dependencies = [ - "memchr", -] - [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -9223,156 +3236,6 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" -[[package]] -name = "livekit" -version = "0.7.8" -source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=5f04705ac3f356350ae31534ffbc476abc9ea83d#5f04705ac3f356350ae31534ffbc476abc9ea83d" -dependencies = [ - "chrono", - "futures-util", - "lazy_static", - "libloading", - "libwebrtc", - "livekit-api", - "livekit-protocol", - "livekit-runtime", - "log", - "parking_lot", - "prost 0.12.6", - "semver", - "serde", - "serde_json", - "thiserror 1.0.69", - "tokio", -] - -[[package]] -name = "livekit-api" -version = "0.4.2" -source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=5f04705ac3f356350ae31534ffbc476abc9ea83d#5f04705ac3f356350ae31534ffbc476abc9ea83d" -dependencies = [ - "futures-util", - "http 0.2.12", - "livekit-protocol", - "livekit-runtime", - "log", - "parking_lot", - "pbjson-types", - "prost 0.12.6", - "rand 0.9.2", - "reqwest 0.11.27", - "scopeguard", - "serde", - "sha2", - "thiserror 1.0.69", - "tokio", - "tokio-tungstenite 0.26.2", - "url", -] - -[[package]] -name = "livekit-protocol" -version = "0.3.9" -source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=5f04705ac3f356350ae31534ffbc476abc9ea83d#5f04705ac3f356350ae31534ffbc476abc9ea83d" -dependencies = [ - "futures-util", - "livekit-runtime", - "parking_lot", - "pbjson", - "pbjson-types", - "prost 0.12.6", - "prost-types 0.12.6", - "serde", - "thiserror 1.0.69", - "tokio", -] - -[[package]] -name = "livekit-runtime" -version = "0.4.0" -source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=5f04705ac3f356350ae31534ffbc476abc9ea83d#5f04705ac3f356350ae31534ffbc476abc9ea83d" -dependencies = [ - "tokio", - "tokio-stream", -] - -[[package]] -name = "livekit_api" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "jsonwebtoken", - "log", - "prost 0.9.0", - "prost-build 0.9.0", - "prost-types 0.9.0", - "serde", - "zed-reqwest", -] - -[[package]] -name = "livekit_client" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "audio", - "collections", - "core-foundation 0.10.0", - "core-video", - "coreaudio-rs 0.12.1", - "cpal", - "futures 0.3.31", - "gpui", - "gpui_tokio", - "http_client_tls", - "image", - "libwebrtc", - "livekit", - "livekit_api", - "log", - "nanoid", - "objc", - "parking_lot", - "postage", - "rodio", - "serde", - "serde_json", - "serde_urlencoded", - "settings", - "sha2", - "simplelog", - "smallvec", - "tokio-tungstenite 0.26.2", - "ui", - "util", - "zed-scap", -] - -[[package]] -name = "lmdb-master-sys" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864808e0b19fb6dd3b70ba94ee671b82fce17554cf80aeb0a155c65bb08027df" -dependencies = [ - "cc", - "doxygen-rs", - "libc", -] - -[[package]] -name = "lmstudio" -version = "0.1.0" -dependencies = [ - "anyhow", - "futures 0.3.31", - "http_client", - "schemars", - "serde", - "serde_json", -] - [[package]] name = "lock_api" version = "0.4.14" @@ -9392,19 +3255,6 @@ dependencies = [ "value-bag", ] -[[package]] -name = "loom" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" -dependencies = [ - "cfg-if", - "generator", - "scoped-tls", - "tracing", - "tracing-subscriber", -] - [[package]] name = "loop9" version = "0.1.5" @@ -9414,56 +3264,12 @@ dependencies = [ "imgref", ] -[[package]] -name = "lru" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" -dependencies = [ - "hashbrown 0.15.5", -] - [[package]] name = "lru-slab" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" -[[package]] -name = "lsp" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-pipe", - "collections", - "ctor", - "futures 0.3.31", - "gpui", - "log", - "lsp-types", - "parking_lot", - "postage", - "release_channel", - "schemars", - "semver", - "serde", - "serde_json", - "smol", - "util", - "zlog", -] - -[[package]] -name = "lsp-types" -version = "0.95.1" -source = "git+https://github.com/zed-industries/lsp-types?rev=b71ab4eeb27d9758be8092020a46fe33fbca4e33#b71ab4eeb27d9758be8092020a46fe33fbca4e33" -dependencies = [ - "bitflags 1.3.2", - "serde", - "serde_json", - "url", -] - [[package]] name = "lyon" version = "1.0.16" @@ -9533,15 +3339,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" -[[package]] -name = "mach2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" -dependencies = [ - "libc", -] - [[package]] name = "mach2" version = "0.5.0" @@ -9560,61 +3357,6 @@ dependencies = [ "libc", ] -[[package]] -name = "maplit" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" - -[[package]] -name = "markdown" -version = "0.1.0" -dependencies = [ - "assets", - "base64 0.22.1", - "collections", - "env_logger 0.11.8", - "fs", - "futures 0.3.31", - "gpui", - "language", - "languages", - "linkify", - "log", - "node_runtime", - "pulldown-cmark 0.12.2", - "settings", - "sum_tree", - "theme", - "ui", - "util", -] - -[[package]] -name = "markdown_preview" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-recursion", - "collections", - "editor", - "fs", - "gpui", - "html5ever 0.27.0", - "language", - "linkify", - "log", - "markup5ever_rcdom", - "pretty_assertions", - "pulldown-cmark 0.12.2", - "settings", - "theme", - "ui", - "urlencoding", - "util", - "workspace", -] - [[package]] name = "markup5ever" version = "0.12.1" @@ -9622,68 +3364,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16ce3abbeba692c8b8441d036ef91aea6df8da2c6b6e21c7e14d3c18e526be45" dependencies = [ "log", - "phf 0.11.3", + "phf", "phf_codegen", "string_cache", "string_cache_codegen", "tendril", ] -[[package]] -name = "markup5ever" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "311fe69c934650f8f19652b3946075f0fc41ad8757dbb68f1ca14e7900ecc1c3" -dependencies = [ - "log", - "tendril", - "web_atoms", -] - [[package]] name = "markup5ever_rcdom" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edaa21ab3701bfee5099ade5f7e1f84553fd19228cf332f13cd6e964bf59be18" dependencies = [ - "html5ever 0.27.0", - "markup5ever 0.12.1", + "html5ever", + "markup5ever", "tendril", "xml5ever", ] -[[package]] -name = "match_token" -version = "0.35.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac84fd3f360fcc43dc5f5d186f02a94192761a080e8bc58621ad4d12296a58cf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - -[[package]] -name = "matchit" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" - -[[package]] -name = "maybe-owned" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" - [[package]] name = "maybe-rayon" version = "0.1.1" @@ -9704,42 +3403,6 @@ dependencies = [ "digest", ] -[[package]] -name = "mdbook" -version = "0.4.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b45a38e19bd200220ef07c892b0157ad3d2365e5b5a267ca01ad12182491eea5" -dependencies = [ - "ammonia", - "anyhow", - "chrono", - "clap", - "clap_complete", - "elasticlunr-rs", - "env_logger 0.11.8", - "futures-util", - "handlebars 5.1.2", - "ignore", - "log", - "memchr", - "notify 6.1.1", - "notify-debouncer-mini", - "once_cell", - "opener", - "pathdiff", - "pulldown-cmark 0.10.3", - "regex", - "serde", - "serde_json", - "shlex", - "tempfile", - "tokio", - "toml 0.5.11", - "topological-sort", - "walkdir", - "warp", -] - [[package]] name = "media" version = "0.1.0" @@ -9749,7 +3412,7 @@ dependencies = [ "core-foundation 0.10.0", "core-video", "ctor", - "foreign-types 0.5.0", + "foreign-types", "metal", "objc", ] @@ -9760,15 +3423,6 @@ version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" -[[package]] -name = "memfd" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad38eb12aea514a0466ea40a80fd8cc83637065948eb4a426e4aa46261175227" -dependencies = [ - "rustix 1.1.2", -] - [[package]] name = "memmap2" version = "0.9.8" @@ -9776,7 +3430,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843a98750cd611cc2965a8213b53b43e715f13c37a9e096c6408e69990961db7" dependencies = [ "libc", - "stable_deref_trait", ] [[package]] @@ -9788,35 +3441,6 @@ dependencies = [ "autocfg", ] -[[package]] -name = "menu" -version = "0.1.0" -dependencies = [ - "gpui", -] - -[[package]] -name = "merge" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10bbef93abb1da61525bbc45eeaff6473a41907d19f8f9aa5168d214e10693e9" -dependencies = [ - "merge_derive", - "num-traits", -] - -[[package]] -name = "merge_derive" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "209d075476da2e63b4b29e72a2ef627b840589588e71400a25e3565c4f849d07" -dependencies = [ - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "metal" version = "0.29.0" @@ -9826,39 +3450,12 @@ dependencies = [ "bitflags 2.9.4", "block", "core-graphics-types 0.1.3", - "foreign-types 0.5.0", + "foreign-types", "log", "objc", "paste", ] -[[package]] -name = "migrator" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "convert_case 0.8.0", - "log", - "pretty_assertions", - "serde_json", - "serde_json_lenient", - "settings_json", - "streaming-iterator", - "tree-sitter", - "tree-sitter-json", - "unindent", -] - -[[package]] -name = "mimalloc" -version = "0.1.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1ee66a4b64c74f4ef288bcbb9192ad9c3feaad75193129ac8509af543894fd8" -dependencies = [ - "libmimalloc-sys", -] - [[package]] name = "mime" version = "0.3.17" @@ -9875,81 +3472,12 @@ dependencies = [ "unicase", ] -[[package]] -name = "minidump-common" -version = "0.21.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c4d14bcca0fd3ed165a03000480aaa364c6860c34e900cb2dafdf3b95340e77" -dependencies = [ - "bitflags 2.9.4", - "debugid", - "num-derive", - "num-traits", - "range-map", - "scroll", - "smart-default", -] - -[[package]] -name = "minidump-writer" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abcd9c8a1e6e1e9d56ce3627851f39a17ea83e17c96bc510f29d7e43d78a7d" -dependencies = [ - "bitflags 2.9.4", - "byteorder", - "cfg-if", - "crash-context", - "goblin", - "libc", - "log", - "mach2 0.4.3", - "memmap2", - "memoffset", - "minidump-common", - "nix 0.28.0", - "procfs-core", - "scroll", - "tempfile", - "thiserror 1.0.69", -] - -[[package]] -name = "minidumper" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4ebc9d1f8847ec1d078f78b35ed598e0ebefa1f242d5f83cd8d7f03960a7d1" -dependencies = [ - "cfg-if", - "crash-context", - "libc", - "log", - "minidump-writer", - "parking_lot", - "polling", - "scroll", - "thiserror 1.0.69", - "uds", -] - [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" -[[package]] -name = "miniprofiler_ui" -version = "0.1.0" -dependencies = [ - "gpui", - "serde_json", - "smol", - "util", - "workspace", - "zed_actions", -] - [[package]] name = "miniz_oxide" version = "0.8.9" @@ -9966,18 +3494,6 @@ version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff" -[[package]] -name = "mio" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" -dependencies = [ - "libc", - "log", - "wasi", - "windows-sys 0.48.0", -] - [[package]] name = "mio" version = "1.1.0" @@ -9985,51 +3501,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" dependencies = [ "libc", - "log", "wasi", "windows-sys 0.61.2", ] -[[package]] -name = "miow" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "536bfad37a309d62069485248eeaba1e8d9853aaf951caaeaed0585a95346f08" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "mistral" -version = "0.1.0" -dependencies = [ - "anyhow", - "futures 0.3.31", - "http_client", - "schemars", - "serde", - "serde_json", - "strum 0.27.2", -] - -[[package]] -name = "moka" -version = "0.12.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8261cd88c312e0004c1d51baad2980c66528dfdb2bee62003e643a4d8f86b077" -dependencies = [ - "crossbeam-channel", - "crossbeam-epoch", - "crossbeam-utils", - "equivalent", - "parking_lot", - "portable-atomic", - "rustc_version", - "smallvec", - "tagptr", - "uuid", -] - [[package]] name = "moxcms" version = "0.7.7" @@ -10040,60 +3515,6 @@ dependencies = [ "pxfm", ] -[[package]] -name = "msvc_spectre_libs" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29e871a9861f3664f18b7e04e9301d4edd55090c2dadb4b1c602e26ab32b1f5b" -dependencies = [ - "cc", -] - -[[package]] -name = "multi_buffer" -version = "0.1.0" -dependencies = [ - "anyhow", - "buffer_diff", - "clock", - "collections", - "ctor", - "gpui", - "indoc", - "itertools 0.14.0", - "language", - "log", - "parking_lot", - "pretty_assertions", - "project", - "rand 0.9.2", - "rope", - "serde", - "settings", - "smallvec", - "smol", - "sum_tree", - "text", - "theme", - "tracing", - "tree-sitter", - "util", - "zlog", - "ztracing", -] - -[[package]] -name = "multimap" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" - -[[package]] -name = "multimap" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" - [[package]] name = "naga" version = "25.0.1" @@ -10103,8 +3524,8 @@ dependencies = [ "arrayvec", "bit-set", "bitflags 2.9.4", - "cfg_aliases 0.2.1", - "codespan-reporting 0.12.0", + "cfg_aliases", + "codespan-reporting", "half", "hashbrown 0.15.5", "hexf-parse", @@ -10119,15 +3540,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "nanoid" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ffa00dec017b5b1a8b7cf5e2c008bfda1aa7e0697ac1508b491fdf2622fb4d8" -dependencies = [ - "rand 0.8.5", -] - [[package]] name = "nanorand" version = "0.7.0" @@ -10137,106 +3549,12 @@ dependencies = [ "getrandom 0.2.16", ] -[[package]] -name = "native-tls" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework 2.11.1", - "security-framework-sys", - "tempfile", -] - -[[package]] -name = "nbformat" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89c7229d604d847227002715e1235cd84e81919285d904ccb290a42ecc409348" -dependencies = [ - "anyhow", - "chrono", - "jupyter-protocol", - "serde", - "serde_json", - "thiserror 1.0.69", - "uuid", -] - -[[package]] -name = "nc" -version = "0.1.0" -dependencies = [ - "anyhow", - "futures 0.3.31", - "net", - "smol", -] - -[[package]] -name = "ndk" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" -dependencies = [ - "bitflags 2.9.4", - "jni-sys", - "log", - "ndk-sys", - "num_enum", - "thiserror 1.0.69", -] - -[[package]] -name = "ndk-context" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" - -[[package]] -name = "ndk-sys" -version = "0.6.0+11769913" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" -dependencies = [ - "jni-sys", -] - -[[package]] -name = "net" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-io", - "smol", - "tempfile", - "windows 0.61.3", -] - [[package]] name = "new_debug_unreachable" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" -[[package]] -name = "nix" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "cfg_aliases 0.1.1", - "libc", -] - [[package]] name = "nix" version = "0.29.0" @@ -10245,7 +3563,7 @@ checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ "bitflags 2.9.4", "cfg-if", - "cfg_aliases 0.2.1", + "cfg_aliases", "libc", ] @@ -10257,33 +3575,11 @@ checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ "bitflags 2.9.4", "cfg-if", - "cfg_aliases 0.2.1", + "cfg_aliases", "libc", "memoffset", ] -[[package]] -name = "node_runtime" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-compression", - "async-std", - "async-tar", - "async-trait", - "futures 0.3.31", - "http_client", - "log", - "paths", - "semver", - "serde", - "serde_json", - "smol", - "util", - "watch", - "which 6.0.3", -] - [[package]] name = "nom" version = "7.1.3" @@ -10309,88 +3605,6 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" -[[package]] -name = "normpath" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf23ab2b905654b4cb177e30b629937b3868311d4e1cba859f899c041046e69b" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "notifications" -version = "0.1.0" -dependencies = [ - "anyhow", - "channel", - "client", - "collections", - "component", - "db", - "gpui", - "rpc", - "settings", - "sum_tree", - "time", - "ui", - "util", - "workspace", - "zed_actions", -] - -[[package]] -name = "notify" -version = "6.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" -dependencies = [ - "bitflags 2.9.4", - "crossbeam-channel", - "filetime", - "fsevent-sys 4.1.0", - "inotify 0.9.6", - "kqueue", - "libc", - "log", - "mio 0.8.11", - "walkdir", - "windows-sys 0.48.0", -] - -[[package]] -name = "notify" -version = "8.2.0" -source = "git+https://github.com/zed-industries/notify.git?rev=b4588b2e5aee68f4c0e100f140e808cbce7b1419#b4588b2e5aee68f4c0e100f140e808cbce7b1419" -dependencies = [ - "bitflags 2.9.4", - "fsevent-sys 4.1.0", - "inotify 0.11.0", - "kqueue", - "libc", - "log", - "mio 1.1.0", - "notify-types", - "walkdir", - "windows-sys 0.60.2", -] - -[[package]] -name = "notify-debouncer-mini" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d40b221972a1fc5ef4d858a2f671fb34c75983eb385463dff3780eeff6a9d43" -dependencies = [ - "crossbeam-channel", - "log", - "notify 6.1.1", -] - -[[package]] -name = "notify-types" -version = "2.0.0" -source = "git+https://github.com/zed-industries/notify.git?rev=b4588b2e5aee68f4c0e100f140e808cbce7b1419#b4588b2e5aee68f4c0e100f140e808cbce7b1419" - [[package]] name = "ntapi" version = "0.4.1" @@ -10400,15 +3614,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys 0.61.2", -] - [[package]] name = "num" version = "0.4.3" @@ -10450,28 +3655,15 @@ dependencies = [ "zeroize", ] -[[package]] -name = "num-cmp" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" - [[package]] name = "num-complex" version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" dependencies = [ - "bytemuck", "num-traits", ] -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - [[package]] name = "num-derive" version = "0.4.2" @@ -10483,16 +3675,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "num-format" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" -dependencies = [ - "arrayvec", - "itoa", -] - [[package]] name = "num-integer" version = "0.1.46" @@ -10544,51 +3726,6 @@ dependencies = [ "libc", ] -[[package]] -name = "num_enum" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a973b4e44ce6cad84ce69d797acf9a044532e4184c4f267913d1b546a0727b7a" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77e878c846a8abae00dd069496dbe8751b16ac1c3d6bd2a7283a938e8228f90d" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "num_threads" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" -dependencies = [ - "libc", -] - -[[package]] -name = "nvim-rs" -version = "0.9.2" -source = "git+https://github.com/KillTheMule/nvim-rs?rev=764dd270c642f77f10f3e19d05cc178a6cbe69f3#764dd270c642f77f10f3e19d05cc178a6cbe69f3" -dependencies = [ - "async-trait", - "futures 0.3.31", - "log", - "rmp", - "rmpv", - "tokio", - "tokio-util", -] - [[package]] name = "objc" version = "0.2.7" @@ -10632,43 +3769,6 @@ dependencies = [ "objc2-quartz-core", ] -[[package]] -name = "objc2-audio-toolbox" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10cbe18d879e20a4aea544f8befe38bcf52255eb63d3f23eca2842f3319e4c07" -dependencies = [ - "bitflags 2.9.4", - "libc", - "objc2", - "objc2-core-audio", - "objc2-core-audio-types", - "objc2-core-foundation", - "objc2-foundation", -] - -[[package]] -name = "objc2-core-audio" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1eebcea8b0dbff5f7c8504f3107c68fc061a3eb44932051c8cf8a68d969c3b2" -dependencies = [ - "dispatch2", - "objc2", - "objc2-core-audio-types", - "objc2-core-foundation", -] - -[[package]] -name = "objc2-core-audio-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c" -dependencies = [ - "bitflags 2.9.4", - "objc2", -] - [[package]] name = "objc2-core-foundation" version = "0.3.2" @@ -10697,16 +3797,6 @@ dependencies = [ "objc2-core-foundation", ] -[[package]] -name = "objc2-io-kit" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15" -dependencies = [ - "libc", - "objc2-core-foundation", -] - [[package]] name = "objc2-metal" version = "0.3.1" @@ -10765,18 +3855,6 @@ dependencies = [ "objc", ] -[[package]] -name = "object" -version = "0.36.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" -dependencies = [ - "crc32fast", - "hashbrown 0.15.5", - "indexmap", - "memchr", -] - [[package]] name = "object" version = "0.37.3" @@ -10786,49 +3864,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "ollama" -version = "0.1.0" -dependencies = [ - "anyhow", - "futures 0.3.31", - "http_client", - "schemars", - "serde", - "serde_json", - "settings", -] - -[[package]] -name = "onboarding" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "component", - "db", - "documented", - "fs", - "fuzzy", - "git", - "gpui", - "menu", - "notifications", - "picker", - "project", - "schemars", - "serde", - "settings", - "telemetry", - "theme", - "ui", - "util", - "vim_mode_setting", - "workspace", - "zed_actions", - "zlog", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -10851,7 +3886,7 @@ dependencies = [ "ashpd 0.12.0", "async-fs", "async-io", - "async-lock 3.4.1", + "async-lock", "blocking", "cbc", "cipher", @@ -10865,7 +3900,7 @@ dependencies = [ "md-5", "num", "num-bigint-dig", - "pbkdf2 0.12.2", + "pbkdf2", "rand 0.9.2", "serde", "sha2", @@ -10876,12 +3911,6 @@ dependencies = [ "zvariant", ] -[[package]] -name = "oorandom" -version = "11.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" - [[package]] name = "open" version = "5.3.2" @@ -10893,128 +3922,18 @@ dependencies = [ "pathdiff", ] -[[package]] -name = "open_ai" -version = "0.1.0" -dependencies = [ - "anyhow", - "futures 0.3.31", - "http_client", - "log", - "schemars", - "serde", - "serde_json", - "settings", - "strum 0.27.2", - "thiserror 2.0.17", -] - -[[package]] -name = "open_router" -version = "0.1.0" -dependencies = [ - "anyhow", - "futures 0.3.31", - "http_client", - "schemars", - "serde", - "serde_json", - "settings", - "strum 0.27.2", - "thiserror 2.0.17", -] - -[[package]] -name = "opener" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0812e5e4df08da354c851a3376fead46db31c2214f849d3de356d774d057681" -dependencies = [ - "bstr", - "dbus", - "normpath", - "windows-sys 0.59.0", -] - -[[package]] -name = "openssl" -version = "0.10.74" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24ad14dd45412269e1a30f52ad8f0664f0f4f4a89ee8fe28c3b3527021ebb654" -dependencies = [ - "bitflags 2.9.4", - "cfg-if", - "foreign-types 0.3.2", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "openssl-probe" version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" -[[package]] -name = "openssl-sys" -version = "0.9.110" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "optfield" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969ccca8ffc4fb105bd131a228107d5c9dd89d9d627edf3295cbe979156f9712" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "option-ext" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" -[[package]] -name = "ordered-float" -version = "2.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f19d67e5a2795c94e73e0bb1cc1a7edeb2e28efd39e2e1c9b7a40c1108b11c" -dependencies = [ - "num-traits", -] - -[[package]] -name = "ordered-float" -version = "4.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" -dependencies = [ - "num-traits", -] - [[package]] name = "ordered-stream" version = "0.2.0" @@ -11025,150 +3944,6 @@ dependencies = [ "pin-project-lite", ] -[[package]] -name = "ouroboros" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e0f050db9c44b97a94723127e6be766ac5c340c48f2c4bb3ffa11713744be59" -dependencies = [ - "aliasable", - "ouroboros_macro", - "static_assertions", -] - -[[package]] -name = "ouroboros_macro" -version = "0.18.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c7028bdd3d43083f6d8d4d5187680d0d3560d54df4cc9d752005268b41e64d0" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "proc-macro2-diagnostics", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "outline" -version = "0.1.0" -dependencies = [ - "editor", - "fuzzy", - "gpui", - "indoc", - "language", - "menu", - "ordered-float 2.10.1", - "picker", - "project", - "rope", - "serde_json", - "settings", - "smol", - "theme", - "tree-sitter-rust", - "tree-sitter-typescript", - "ui", - "util", - "workspace", - "zed_actions", -] - -[[package]] -name = "outline_panel" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "db", - "editor", - "file_icons", - "fuzzy", - "gpui", - "itertools 0.14.0", - "language", - "log", - "menu", - "outline", - "pretty_assertions", - "project", - "search", - "serde", - "serde_json", - "settings", - "smallvec", - "smol", - "theme", - "ui", - "util", - "workspace", - "worktree", - "zed_actions", -] - -[[package]] -name = "outref" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" - -[[package]] -name = "p256" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" -dependencies = [ - "ecdsa", - "elliptic-curve", - "sha2", -] - -[[package]] -name = "page_size" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30d5b2194ed13191c1999ae0704b7839fb18384fa22e49b57eeaa97d79ce40da" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "palette" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6" -dependencies = [ - "approx", - "fast-srgb8", - "palette_derive", -] - -[[package]] -name = "palette_derive" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30" -dependencies = [ - "by_address", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "panel" -version = "0.1.0" -dependencies = [ - "editor", - "gpui", - "settings", - "theme", - "ui", - "workspace", -] - [[package]] name = "parking" version = "2.2.1" @@ -11198,43 +3973,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "parse_int" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c464266693329dd5a8715098c7f86e6c5fd5d985018b8318f53d9c6c2b21a31" -dependencies = [ - "num-traits", -] - -[[package]] -name = "partial-json-fixer" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35ffd90b3f3b6477db7478016b9efb1b7e9d38eafd095f0542fe0ec2ea884a13" - -[[package]] -name = "password-hash" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" -dependencies = [ - "base64ct", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "password-hash" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" -dependencies = [ - "base64ct", - "rand_core 0.6.4", - "subtle", -] - [[package]] name = "paste" version = "1.0.15" @@ -11266,64 +4004,6 @@ dependencies = [ "rustc_version", ] -[[package]] -name = "paths" -version = "0.1.0" -dependencies = [ - "dirs 4.0.0", - "ignore", - "util", -] - -[[package]] -name = "pbjson" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1030c719b0ec2a2d25a5df729d6cff1acf3cc230bf766f4f97833591f7577b90" -dependencies = [ - "base64 0.21.7", - "serde", -] - -[[package]] -name = "pbjson-build" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2580e33f2292d34be285c5bc3dba5259542b083cfad6037b6d70345f24dcb735" -dependencies = [ - "heck 0.4.1", - "itertools 0.11.0", - "prost 0.12.6", - "prost-types 0.12.6", -] - -[[package]] -name = "pbjson-types" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18f596653ba4ac51bdecbb4ef6773bc7f56042dc13927910de1684ad3d32aa12" -dependencies = [ - "bytes 1.10.1", - "chrono", - "pbjson", - "pbjson-build", - "prost 0.12.6", - "prost-build 0.12.6", - "serde", -] - -[[package]] -name = "pbkdf2" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" -dependencies = [ - "digest", - "hmac", - "password-hash 0.4.2", - "sha2", -] - [[package]] name = "pbkdf2" version = "0.12.2" @@ -11334,31 +4014,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.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" -dependencies = [ - "base64 0.22.1", - "serde_core", -] - -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - [[package]] name = "percent-encoding" version = "2.3.2" @@ -11374,472 +4029,13 @@ dependencies = [ "serde_json", ] -[[package]] -name = "pest" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e7521a040efde50c3ab6bbadafbe15ab6dc042686926be59ac35d74607df4" -dependencies = [ - "memchr", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "187da9a3030dbafabbbfb20cb323b976dc7b7ce91fcd84f2f74d6e31d378e2de" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b401d98f5757ebe97a26085998d6c0eecec4995cad6ab7fc30ffdf4b052843" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "pest_meta" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f27a2cfee9f9039c4d86faa5af122a0ac3851441a34865b8a043b46be0065a" -dependencies = [ - "pest", - "sha2", -] - -[[package]] -name = "pet" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "clap", - "env_logger 0.10.2", - "lazy_static", - "log", - "msvc_spectre_libs", - "pet-conda", - "pet-core", - "pet-env-var-path", - "pet-fs", - "pet-global-virtualenvs", - "pet-homebrew", - "pet-jsonrpc", - "pet-linux-global-python", - "pet-mac-commandlinetools", - "pet-mac-python-org", - "pet-mac-xcode", - "pet-pipenv", - "pet-pixi", - "pet-poetry", - "pet-pyenv", - "pet-python-utils", - "pet-reporter", - "pet-telemetry", - "pet-uv", - "pet-venv", - "pet-virtualenv", - "pet-virtualenvwrapper", - "pet-windows-registry", - "pet-windows-store", - "serde", - "serde_json", -] - -[[package]] -name = "pet-conda" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "env_logger 0.10.2", - "lazy_static", - "log", - "msvc_spectre_libs", - "pet-core", - "pet-fs", - "pet-python-utils", - "pet-reporter", - "regex", - "serde", - "serde_json", - "yaml-rust2", -] - -[[package]] -name = "pet-core" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "clap", - "lazy_static", - "log", - "msvc_spectre_libs", - "pet-fs", - "regex", - "serde", - "serde_json", -] - -[[package]] -name = "pet-env-var-path" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "lazy_static", - "log", - "msvc_spectre_libs", - "pet-conda", - "pet-core", - "pet-fs", - "pet-python-utils", - "pet-virtualenv", - "regex", -] - -[[package]] -name = "pet-fs" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "log", - "msvc_spectre_libs", -] - -[[package]] -name = "pet-global-virtualenvs" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "log", - "msvc_spectre_libs", - "pet-conda", - "pet-core", - "pet-fs", - "pet-virtualenv", -] - -[[package]] -name = "pet-homebrew" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "lazy_static", - "log", - "msvc_spectre_libs", - "pet-conda", - "pet-core", - "pet-fs", - "pet-python-utils", - "pet-virtualenv", - "regex", - "serde", - "serde_json", -] - -[[package]] -name = "pet-jsonrpc" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "env_logger 0.10.2", - "log", - "msvc_spectre_libs", - "pet-core", - "serde", - "serde_json", -] - -[[package]] -name = "pet-linux-global-python" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "log", - "msvc_spectre_libs", - "pet-core", - "pet-fs", - "pet-python-utils", - "pet-virtualenv", -] - -[[package]] -name = "pet-mac-commandlinetools" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "log", - "msvc_spectre_libs", - "pet-core", - "pet-fs", - "pet-python-utils", - "pet-virtualenv", -] - -[[package]] -name = "pet-mac-python-org" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "log", - "msvc_spectre_libs", - "pet-core", - "pet-fs", - "pet-python-utils", - "pet-virtualenv", -] - -[[package]] -name = "pet-mac-xcode" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "log", - "msvc_spectre_libs", - "pet-core", - "pet-fs", - "pet-python-utils", - "pet-virtualenv", -] - -[[package]] -name = "pet-pipenv" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "log", - "msvc_spectre_libs", - "pet-core", - "pet-fs", - "pet-python-utils", - "pet-virtualenv", -] - -[[package]] -name = "pet-pixi" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "log", - "msvc_spectre_libs", - "pet-conda", - "pet-core", - "pet-python-utils", -] - -[[package]] -name = "pet-poetry" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "base64 0.22.1", - "lazy_static", - "log", - "msvc_spectre_libs", - "pet-core", - "pet-fs", - "pet-python-utils", - "pet-reporter", - "pet-virtualenv", - "regex", - "serde", - "serde_json", - "sha2", - "toml 0.8.23", -] - -[[package]] -name = "pet-pyenv" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "lazy_static", - "log", - "msvc_spectre_libs", - "pet-conda", - "pet-core", - "pet-fs", - "pet-python-utils", - "pet-reporter", - "regex", - "serde", - "serde_json", -] - -[[package]] -name = "pet-python-utils" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "env_logger 0.10.2", - "lazy_static", - "log", - "msvc_spectre_libs", - "pet-core", - "pet-fs", - "regex", - "serde", - "serde_json", - "sha2", -] - -[[package]] -name = "pet-reporter" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "env_logger 0.10.2", - "log", - "msvc_spectre_libs", - "pet-core", - "pet-jsonrpc", - "serde", - "serde_json", -] - -[[package]] -name = "pet-telemetry" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "env_logger 0.10.2", - "lazy_static", - "log", - "msvc_spectre_libs", - "pet-core", - "pet-fs", - "pet-python-utils", - "regex", -] - -[[package]] -name = "pet-uv" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "log", - "pet-core", - "pet-python-utils", - "serde", - "toml 0.9.8", -] - -[[package]] -name = "pet-venv" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "log", - "msvc_spectre_libs", - "pet-core", - "pet-python-utils", - "pet-virtualenv", -] - -[[package]] -name = "pet-virtualenv" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "log", - "msvc_spectre_libs", - "pet-core", - "pet-fs", - "pet-python-utils", -] - -[[package]] -name = "pet-virtualenvwrapper" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "log", - "msvc_spectre_libs", - "pet-core", - "pet-fs", - "pet-python-utils", - "pet-virtualenv", -] - -[[package]] -name = "pet-windows-registry" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "lazy_static", - "log", - "msvc_spectre_libs", - "pet-conda", - "pet-core", - "pet-fs", - "pet-python-utils", - "pet-virtualenv", - "pet-windows-store", - "regex", - "winreg 0.55.0", -] - -[[package]] -name = "pet-windows-store" -version = "0.1.0" -source = "git+https://github.com/microsoft/python-environment-tools.git?rev=1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da#1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" -dependencies = [ - "lazy_static", - "log", - "msvc_spectre_libs", - "pet-core", - "pet-fs", - "pet-python-utils", - "pet-virtualenv", - "regex", - "winreg 0.55.0", -] - -[[package]] -name = "petgraph" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" -dependencies = [ - "fixedbitset", - "indexmap", -] - -[[package]] -name = "pgvector" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc58e2d255979a31caa7cabfa7aac654af0354220719ab7a68520ae7a91e8c0b" -dependencies = [ - "serde", -] - [[package]] name = "phf" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ - "phf_macros 0.11.3", - "phf_shared 0.11.3", -] - -[[package]] -name = "phf" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7" -dependencies = [ - "phf_macros 0.12.1", - "phf_shared 0.12.1", + "phf_shared", ] [[package]] @@ -11848,8 +4044,8 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", + "phf_generator", + "phf_shared", ] [[package]] @@ -11858,46 +4054,10 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ - "phf_shared 0.11.3", + "phf_shared", "rand 0.8.5", ] -[[package]] -name = "phf_generator" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cbb1126afed61dd6368748dae63b1ee7dc480191c6262a3b4ff1e29d86a6c5b" -dependencies = [ - "fastrand 2.3.0", - "phf_shared 0.12.1", -] - -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "phf_macros" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d713258393a82f091ead52047ca779d37e5766226d009de21696c4e667044368" -dependencies = [ - "phf_generator 0.12.1", - "phf_shared 0.12.1", - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "phf_shared" version = "0.11.3" @@ -11907,33 +4067,6 @@ dependencies = [ "siphasher", ] -[[package]] -name = "phf_shared" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981" -dependencies = [ - "siphasher", -] - -[[package]] -name = "picker" -version = "0.1.0" -dependencies = [ - "anyhow", - "ctor", - "editor", - "env_logger 0.11.8", - "gpui", - "menu", - "schemars", - "serde", - "serde_json", - "theme", - "ui", - "workspace", -] - [[package]] name = "pico-args" version = "0.5.0" @@ -11983,90 +4116,12 @@ dependencies = [ "futures-io", ] -[[package]] -name = "pkcs1" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" -dependencies = [ - "der 0.7.10", - "pkcs8 0.10.2", - "spki 0.7.3", -] - -[[package]] -name = "pkcs8" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" -dependencies = [ - "der 0.6.1", - "spki 0.6.0", -] - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der 0.7.10", - "spki 0.7.3", -] - [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" -[[package]] -name = "plain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" - -[[package]] -name = "plist" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" -dependencies = [ - "base64 0.22.1", - "indexmap", - "quick-xml 0.38.3", - "serde", - "time", -] - -[[package]] -name = "plotters" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" -dependencies = [ - "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "plotters-backend" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" - -[[package]] -name = "plotters-svg" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" -dependencies = [ - "plotters-backend", -] - [[package]] name = "png" version = "0.17.16" @@ -12113,15 +4168,6 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5da3b0203fd7ee5720aa0b5e790b591aa5d3f41c3ed2c34a3a393382198af2f7" -[[package]] -name = "pori" -version = "0.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a63d338dec139f56dacc692ca63ad35a6be6a797442479b55acd611d79e906" -dependencies = [ - "nom 7.1.3", -] - [[package]] name = "portable-atomic" version = "1.11.1" @@ -12137,27 +4183,6 @@ dependencies = [ "portable-atomic", ] -[[package]] -name = "portable-pty" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4a596a2b3d2752d94f51fac2d4a96737b8705dddd311a32b9af47211f08671e" -dependencies = [ - "anyhow", - "bitflags 1.3.2", - "downcast-rs", - "filedescriptor", - "lazy_static", - "libc", - "log", - "nix 0.28.0", - "serial2", - "shared_library", - "shell-words", - "winapi", - "winreg 0.10.1", -] - [[package]] name = "postage" version = "0.5.0" @@ -12166,7 +4191,7 @@ checksum = "af3fb618632874fb76937c2361a7f22afd393c982a2165595407edc75b06d3c1" dependencies = [ "atomic", "crossbeam-queue", - "futures 0.3.31", + "futures", "log", "parking_lot", "pin-project", @@ -12175,18 +4200,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "postcard" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" -dependencies = [ - "cobs", - "embedded-io 0.4.0", - "embedded-io 0.6.1", - "serde", -] - [[package]] name = "potential_utf" version = "0.1.3" @@ -12196,12 +4209,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - [[package]] name = "ppv-lite86" version = "0.2.21" @@ -12217,25 +4224,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" -[[package]] -name = "prettier" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "fs", - "gpui", - "language", - "log", - "lsp", - "node_runtime", - "parking_lot", - "paths", - "serde", - "serde_json", - "util", -] - [[package]] name = "pretty_assertions" version = "1.4.1" @@ -12256,15 +4244,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "primal-check" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0d895b311e3af9902528fbb8f928688abbd95872819320517cc24ca6b2bd08" -dependencies = [ - "num-integer", -] - [[package]] name = "proc-macro-crate" version = "3.4.0" @@ -12274,30 +4253,6 @@ dependencies = [ "toml_edit 0.23.7", ] -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - [[package]] name = "proc-macro-error-attr2" version = "2.0.0" @@ -12329,29 +4284,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "proc-macro2-diagnostics" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", - "version_check", - "yansi", -] - -[[package]] -name = "procfs-core" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d3554923a69f4ce04c4a754260c338f505ce22642d3830e049a399fc2059a29" -dependencies = [ - "bitflags 2.9.4", - "hex", -] - [[package]] name = "profiling" version = "1.0.17" @@ -12371,336 +4303,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "project" -version = "0.1.0" -dependencies = [ - "aho-corasick", - "anyhow", - "askpass", - "async-trait", - "base64 0.22.1", - "buffer_diff", - "circular-buffer", - "client", - "clock", - "collections", - "context_server", - "dap", - "dap_adapters", - "extension", - "fancy-regex", - "fs", - "futures 0.3.31", - "fuzzy", - "git", - "git2", - "git_hosting_providers", - "globset", - "gpui", - "http_client", - "image", - "indexmap", - "itertools 0.14.0", - "language", - "log", - "lsp", - "markdown", - "node_runtime", - "parking_lot", - "paths", - "postage", - "prettier", - "pretty_assertions", - "rand 0.9.2", - "regex", - "release_channel", - "remote", - "rpc", - "schemars", - "semver", - "serde", - "serde_json", - "settings", - "sha2", - "shellexpand 2.1.2", - "smallvec", - "smol", - "snippet", - "snippet_provider", - "sum_tree", - "task", - "tempfile", - "terminal", - "text", - "toml 0.8.23", - "tracing", - "unindent", - "url", - "util", - "watch", - "wax", - "which 6.0.3", - "worktree", - "zeroize", - "zlog", - "ztracing", -] - -[[package]] -name = "project_benchmarks" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "client", - "futures 0.3.31", - "gpui", - "http_client", - "language", - "node_runtime", - "project", - "settings", - "watch", -] - -[[package]] -name = "project_panel" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "collections", - "command_palette_hooks", - "criterion", - "db", - "editor", - "file_icons", - "git", - "git_ui", - "gpui", - "language", - "menu", - "pretty_assertions", - "project", - "rayon", - "schemars", - "search", - "serde", - "serde_json", - "settings", - "smallvec", - "telemetry", - "tempfile", - "theme", - "ui", - "util", - "workspace", - "worktree", - "zed_actions", -] - -[[package]] -name = "project_symbols" -version = "0.1.0" -dependencies = [ - "anyhow", - "editor", - "futures 0.3.31", - "fuzzy", - "gpui", - "language", - "lsp", - "ordered-float 2.10.1", - "picker", - "project", - "release_channel", - "semver", - "serde_json", - "settings", - "theme", - "util", - "workspace", -] - -[[package]] -name = "prometheus" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ca5326d8d0b950a9acd87e6a3f94745394f62e4dae1b1ee22b2bc0c394af43a" -dependencies = [ - "cfg-if", - "fnv", - "lazy_static", - "memchr", - "parking_lot", - "protobuf", - "thiserror 2.0.17", -] - -[[package]] -name = "prompt_store" -version = "0.1.0" -dependencies = [ - "anyhow", - "assets", - "chrono", - "collections", - "fs", - "futures 0.3.31", - "fuzzy", - "gpui", - "handlebars 4.5.0", - "heed", - "language", - "log", - "parking_lot", - "paths", - "rope", - "serde", - "text", - "util", - "uuid", -] - -[[package]] -name = "prost" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" -dependencies = [ - "bytes 1.10.1", - "prost-derive 0.9.0", -] - -[[package]] -name = "prost" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" -dependencies = [ - "bytes 1.10.1", - "prost-derive 0.12.6", -] - -[[package]] -name = "prost-build" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5" -dependencies = [ - "bytes 1.10.1", - "heck 0.3.3", - "itertools 0.10.5", - "lazy_static", - "log", - "multimap 0.8.3", - "petgraph", - "prost 0.9.0", - "prost-types 0.9.0", - "regex", - "tempfile", - "which 4.4.2", -] - -[[package]] -name = "prost-build" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4" -dependencies = [ - "bytes 1.10.1", - "heck 0.5.0", - "itertools 0.12.1", - "log", - "multimap 0.10.1", - "once_cell", - "petgraph", - "prettyplease", - "prost 0.12.6", - "prost-types 0.12.6", - "regex", - "syn 2.0.106", - "tempfile", -] - -[[package]] -name = "prost-derive" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" -dependencies = [ - "anyhow", - "itertools 0.10.5", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "prost-derive" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" -dependencies = [ - "anyhow", - "itertools 0.12.1", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "prost-types" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "534b7a0e836e3c482d2693070f982e39e7611da9695d4d1f5a4b186b51faef0a" -dependencies = [ - "bytes 1.10.1", - "prost 0.9.0", -] - -[[package]] -name = "prost-types" -version = "0.12.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" -dependencies = [ - "prost 0.12.6", -] - -[[package]] -name = "proto" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "prost 0.9.0", - "prost-build 0.9.0", - "serde", - "typed-path", -] - -[[package]] -name = "protobuf" -version = "3.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d65a1d4ddae7d8b5de68153b48f6aa3bba8cb002b243dbdbc55a5afbc98f99f4" -dependencies = [ - "once_cell", - "protobuf-support", - "thiserror 1.0.69", -] - -[[package]] -name = "protobuf-support" -version = "3.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e36c2f31e0a47f9280fb347ef5e461ffcd2c52dd520d8e216b52f93b0b0d7d6" -dependencies = [ - "thiserror 1.0.69", -] - [[package]] name = "psm" version = "0.1.27" @@ -12710,93 +4312,6 @@ dependencies = [ "cc", ] -[[package]] -name = "ptr_meta" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" -dependencies = [ - "ptr_meta_derive", -] - -[[package]] -name = "ptr_meta_derive" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "pulldown-cmark" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76979bea66e7875e7509c4ec5300112b316af87fa7a252ca91c448b32dfe3993" -dependencies = [ - "bitflags 2.9.4", - "memchr", - "pulldown-cmark-escape", - "unicase", -] - -[[package]] -name = "pulldown-cmark" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f86ba2052aebccc42cbbb3ed234b8b13ce76f75c3551a303cb2bcffcff12bb14" -dependencies = [ - "bitflags 2.9.4", - "memchr", - "unicase", -] - -[[package]] -name = "pulldown-cmark-escape" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd348ff538bc9caeda7ee8cad2d1d48236a1f443c1fa3913c6a02fe0043b1dd3" - -[[package]] -name = "pulley-interpreter" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62d95f8575df49a2708398182f49a888cf9dc30210fb1fd2df87c889edcee75d" -dependencies = [ - "cranelift-bitset", - "log", - "sptr", - "wasmtime-math", -] - -[[package]] -name = "pulp" -version = "0.18.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0a01a0dc67cf4558d279f0c25b0962bd08fc6dec0137699eae304103e882fe6" -dependencies = [ - "bytemuck", - "libm", - "num-complex", - "reborrow", -] - -[[package]] -name = "pulp" -version = "0.21.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b86df24f0a7ddd5e4b95c94fc9ed8a98f1ca94d3b01bdce2824097e7835907" -dependencies = [ - "bytemuck", - "cfg-if", - "libm", - "num-complex", - "reborrow", - "version_check", -] - [[package]] name = "pxfm" version = "0.1.25" @@ -12839,29 +4354,20 @@ dependencies = [ "memchr", ] -[[package]] -name = "quick-xml" -version = "0.38.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" -dependencies = [ - "memchr", -] - [[package]] name = "quinn" version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ - "bytes 1.10.1", - "cfg_aliases 0.2.1", + "bytes", + "cfg_aliases", "pin-project-lite", "quinn-proto", "quinn-udp", "rustc-hash 2.1.1", - "rustls 0.23.33", - "socket2 0.6.1", + "rustls", + "socket2", "thiserror 2.0.17", "tokio", "tracing", @@ -12874,13 +4380,13 @@ version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ - "bytes 1.10.1", + "bytes", "getrandom 0.3.4", "lru-slab", "rand 0.9.2", "ring", "rustc-hash 2.1.1", - "rustls 0.23.33", + "rustls", "rustls-pki-types", "slab", "thiserror 2.0.17", @@ -12895,10 +4401,10 @@ version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ - "cfg_aliases 0.2.1", + "cfg_aliases", "libc", "once_cell", - "socket2 0.6.1", + "socket2", "tracing", "windows-sys 0.60.2", ] @@ -12918,12 +4424,6 @@ version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - [[package]] name = "rand" version = "0.8.5" @@ -12983,25 +4483,6 @@ dependencies = [ "getrandom 0.3.4", ] -[[package]] -name = "rand_distr" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8615d50dcf34fa31f7ab52692afec947c4dd0ab803cc87cb3b0b4570ff7463" -dependencies = [ - "num-traits", - "rand 0.9.2", -] - -[[package]] -name = "range-map" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12a5a2d6c7039059af621472a4389be1215a816df61aa4d531cfe85264aee95f" -dependencies = [ - "num-traits", -] - [[package]] name = "rangemap" version = "1.6.0" @@ -13058,24 +4539,6 @@ dependencies = [ "rgb", ] -[[package]] -name = "raw-cpuid" -version = "10.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c297679cb867470fa8c9f67dbba74a78d78e3e98d7cf2b08d6d71540f797332" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "raw-cpuid" -version = "11.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" -dependencies = [ - "bitflags 2.9.4", -] - [[package]] name = "raw-window-handle" version = "0.6.2" @@ -13124,64 +4587,6 @@ dependencies = [ "font-types", ] -[[package]] -name = "realfft" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f821338fddb99d089116342c46e9f1fbf3828dba077674613e734e01d6ea8677" -dependencies = [ - "rustfft", -] - -[[package]] -name = "reborrow" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" - -[[package]] -name = "recent_projects" -version = "0.1.0" -dependencies = [ - "anyhow", - "askpass", - "auto_update", - "dap", - "db", - "editor", - "extension_host", - "file_finder", - "futures 0.3.31", - "fuzzy", - "gpui", - "indoc", - "language", - "log", - "markdown", - "menu", - "node_runtime", - "ordered-float 2.10.1", - "paths", - "picker", - "project", - "release_channel", - "remote", - "semver", - "serde", - "serde_json", - "settings", - "smol", - "task", - "telemetry", - "theme", - "ui", - "util", - "windows-registry 0.6.1", - "workspace", - "worktree", - "zed_actions", -] - [[package]] name = "redox_syscall" version = "0.2.16" @@ -13211,17 +4616,6 @@ dependencies = [ "thiserror 1.0.69", ] -[[package]] -name = "redox_users" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" -dependencies = [ - "getrandom 0.2.16", - "libredox", - "thiserror 2.0.17", -] - [[package]] name = "ref-cast" version = "1.0.25" @@ -13242,21 +4636,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "referencing" -version = "0.37.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4283168a506f0dcbdce31c9f9cce3129c924da4c6bca46e46707fcb746d2d70c" -dependencies = [ - "ahash 0.8.12", - "fluent-uri", - "getrandom 0.3.4", - "hashbrown 0.16.1", - "parking_lot", - "percent-encoding", - "serde_json", -] - [[package]] name = "refineable" version = "0.1.0" @@ -13264,21 +4643,6 @@ dependencies = [ "derive_refineable", ] -[[package]] -name = "regalloc2" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc06e6b318142614e4a48bc725abbf08ff166694835c43c9dae5a9009704639a" -dependencies = [ - "allocator-api2", - "bumpalo", - "hashbrown 0.15.5", - "log", - "rustc-hash 2.1.1", - "serde", - "smallvec", -] - [[package]] name = "regex" version = "1.12.2" @@ -13302,272 +4666,19 @@ dependencies = [ "regex-syntax", ] -[[package]] -name = "regex-lite" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d942b98df5e658f56f20d592c7f868833fe38115e65c33003d8cd224b0155da" - [[package]] name = "regex-syntax" version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" -[[package]] -name = "release_channel" -version = "0.1.0" -dependencies = [ - "gpui", - "semver", -] - -[[package]] -name = "remote" -version = "0.1.0" -dependencies = [ - "anyhow", - "askpass", - "async-trait", - "collections", - "fs", - "futures 0.3.31", - "gpui", - "log", - "parking_lot", - "paths", - "prost 0.9.0", - "release_channel", - "rpc", - "schemars", - "semver", - "serde", - "serde_json", - "settings", - "smol", - "tempfile", - "thiserror 2.0.17", - "urlencoding", - "util", - "which 6.0.3", -] - -[[package]] -name = "remote_server" -version = "0.1.0" -dependencies = [ - "action_log", - "agent", - "anyhow", - "askpass", - "cargo_toml", - "clap", - "client", - "clock", - "collections", - "crash-handler", - "crashes", - "dap", - "dap_adapters", - "debug_adapter_extension", - "editor", - "env_logger 0.11.8", - "extension", - "extension_host", - "fork", - "fs", - "futures 0.3.31", - "git", - "git2", - "git_hosting_providers", - "gpui", - "gpui_tokio", - "http_client", - "image", - "json_schema_store", - "language", - "language_extension", - "language_model", - "languages", - "libc", - "log", - "lsp", - "minidumper", - "node_runtime", - "paths", - "pretty_assertions", - "project", - "prompt_store", - "proto", - "rayon", - "release_channel", - "remote", - "reqwest_client", - "rpc", - "rust-embed", - "semver", - "serde", - "serde_json", - "settings", - "shellexpand 2.1.2", - "smol", - "sysinfo 0.37.2", - "task", - "theme", - "thiserror 2.0.17", - "toml 0.8.23", - "unindent", - "util", - "watch", - "workspace", - "worktree", - "zlog", -] - -[[package]] -name = "rend" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" -dependencies = [ - "bytecheck", -] - -[[package]] -name = "repl" -version = "0.1.0" -dependencies = [ - "alacritty_terminal", - "anyhow", - "async-dispatcher", - "async-tungstenite", - "base64 0.22.1", - "client", - "collections", - "command_palette_hooks", - "editor", - "env_logger 0.11.8", - "feature_flags", - "file_icons", - "futures 0.3.31", - "gpui", - "http_client", - "image", - "indoc", - "jupyter-protocol", - "jupyter-websocket-client", - "language", - "languages", - "log", - "markdown_preview", - "menu", - "multi_buffer", - "nbformat", - "picker", - "project", - "runtimelib", - "serde", - "serde_json", - "settings", - "smol", - "telemetry", - "terminal", - "terminal_view", - "theme", - "tree-sitter-md", - "tree-sitter-python", - "tree-sitter-typescript", - "ui", - "util", - "uuid", - "workspace", -] - -[[package]] -name = "reqwest" -version = "0.11.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" -dependencies = [ - "base64 0.21.7", - "bytes 1.10.1", - "encoding_rs", - "futures-core", - "futures-util", - "h2 0.3.27", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.32", - "hyper-rustls 0.24.2", - "hyper-tls", - "ipnet", - "js-sys", - "log", - "mime", - "native-tls", - "once_cell", - "percent-encoding", - "pin-project-lite", - "rustls 0.21.12", - "rustls-native-certs 0.6.3", - "rustls-pemfile 1.0.4", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper 0.1.2", - "system-configuration 0.5.1", - "tokio", - "tokio-native-tls", - "tokio-rustls 0.24.1", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "winreg 0.50.0", -] - -[[package]] -name = "reqwest" -version = "0.12.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f" -dependencies = [ - "base64 0.22.1", - "bytes 1.10.1", - "futures-channel", - "futures-core", - "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "http-body-util", - "hyper 1.7.0", - "hyper-util", - "js-sys", - "log", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper 1.0.2", - "tokio", - "tower 0.5.2", - "tower-http 0.6.6", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - [[package]] name = "reqwest_client" version = "0.1.0" dependencies = [ "anyhow", - "bytes 1.10.1", - "futures 0.3.31", + "bytes", + "futures", "gpui", "http_client", "http_client_tls", @@ -13592,17 +4703,6 @@ dependencies = [ "usvg", ] -[[package]] -name = "rfc6979" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" -dependencies = [ - "crypto-bigint 0.4.9", - "hmac", - "zeroize", -] - [[package]] name = "rgb" version = "0.8.52" @@ -13612,20 +4712,6 @@ dependencies = [ "bytemuck", ] -[[package]] -name = "rich_text" -version = "0.1.0" -dependencies = [ - "futures 0.3.31", - "gpui", - "language", - "linkify", - "pulldown-cmark 0.12.2", - "theme", - "ui", - "util", -] - [[package]] name = "ring" version = "0.17.14" @@ -13636,202 +4722,16 @@ dependencies = [ "cfg-if", "getrandom 0.2.16", "libc", - "untrusted 0.9.0", + "untrusted", "windows-sys 0.52.0", ] -[[package]] -name = "rkyv" -version = "0.7.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" -dependencies = [ - "bitvec", - "bytecheck", - "bytes 1.10.1", - "hashbrown 0.12.3", - "ptr_meta", - "rend", - "rkyv_derive", - "seahash", - "tinyvec", - "uuid", -] - -[[package]] -name = "rkyv_derive" -version = "0.7.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "rmp" -version = "0.8.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4" -dependencies = [ - "byteorder", - "num-traits", - "paste", -] - -[[package]] -name = "rmpv" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58450723cd9ee93273ce44a20b6ec4efe17f8ed2e3631474387bfdecf18bb2a9" -dependencies = [ - "num-traits", - "rmp", -] - -[[package]] -name = "rodio" -version = "0.21.1" -source = "git+https://github.com/RustAudio/rodio?rev=e2074c6c2acf07b57cf717e076bdda7a9ac6e70b#e2074c6c2acf07b57cf717e076bdda7a9ac6e70b" -dependencies = [ - "cpal", - "dasp_sample", - "hound", - "num-rational", - "rtrb", - "symphonia", - "thiserror 2.0.17", -] - -[[package]] -name = "rope" -version = "0.1.0" -dependencies = [ - "arrayvec", - "criterion", - "ctor", - "gpui", - "log", - "rand 0.9.2", - "rayon", - "sum_tree", - "tracing", - "unicode-segmentation", - "util", - "zlog", - "ztracing", -] - [[package]] name = "roxmltree" version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" -[[package]] -name = "rpc" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-tungstenite", - "base64 0.22.1", - "chrono", - "collections", - "futures 0.3.31", - "gpui", - "parking_lot", - "proto", - "rand 0.9.2", - "rsa", - "serde", - "serde_json", - "sha2", - "strum 0.27.2", - "tracing", - "util", - "zlog", - "zstd", -] - -[[package]] -name = "rsa" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" -dependencies = [ - "const-oid", - "digest", - "num-bigint-dig", - "num-integer", - "num-traits", - "pkcs1", - "pkcs8 0.10.2", - "rand_core 0.6.4", - "signature 2.2.0", - "spki 0.7.3", - "subtle", - "zeroize", -] - -[[package]] -name = "rtrb" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8388ea1a9e0ea807e442e8263a699e7edcb320ecbcd21b4fa8ff859acce3ba" - -[[package]] -name = "rules_library" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "editor", - "gpui", - "language", - "language_model", - "log", - "menu", - "picker", - "prompt_store", - "release_channel", - "rope", - "serde", - "settings", - "theme", - "title_bar", - "ui", - "util", - "workspace", - "zed_actions", -] - -[[package]] -name = "runtimelib" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "481b48894073a0096f28cbe9860af01fc1b861e55b3bc96afafc645ee3de62dc" -dependencies = [ - "async-dispatcher", - "async-std", - "aws-lc-rs", - "base64 0.22.1", - "bytes 1.10.1", - "chrono", - "data-encoding", - "dirs 6.0.0", - "futures 0.3.31", - "glob", - "jupyter-protocol", - "serde", - "serde_json", - "shellexpand 3.1.1", - "smol", - "thiserror 2.0.17", - "uuid", - "zeromq", -] - [[package]] name = "rust-embed" version = "8.7.2" @@ -13867,32 +4767,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "rust-stemmers" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e46a2036019fdb888131db7a4c847a1063a7493f971ed94ea82c67eada63ca54" -dependencies = [ - "serde", - "serde_derive", -] - -[[package]] -name = "rust_decimal" -version = "1.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" -dependencies = [ - "arrayvec", - "borsh", - "bytes 1.10.1", - "num-traits", - "rand 0.8.5", - "rkyv", - "serde", - "serde_json", -] - [[package]] name = "rustc-demangle" version = "0.1.26" @@ -13920,20 +4794,6 @@ dependencies = [ "semver", ] -[[package]] -name = "rustfft" -version = "6.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21db5f9893e91f41798c88680037dba611ca6674703c1a18601b01a72c8adb89" -dependencies = [ - "num-complex", - "num-integer", - "num-traits", - "primal-check", - "strength_reduce", - "transpose", -] - [[package]] name = "rustix" version = "0.38.44" @@ -13941,7 +4801,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ "bitflags 2.9.4", - "errno 0.3.14", + "errno", "libc", "linux-raw-sys 0.4.15", "windows-sys 0.59.0", @@ -13954,45 +4814,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" dependencies = [ "bitflags 2.9.4", - "errno 0.3.14", + "errno", "libc", "linux-raw-sys 0.11.0", "windows-sys 0.61.2", ] -[[package]] -name = "rustix-linux-procfs" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056" -dependencies = [ - "once_cell", - "rustix 1.1.2", -] - -[[package]] -name = "rustix-openpty" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1de16c7c59892b870a6336f185dc10943517f1327447096bbb7bb32cd85e2393" -dependencies = [ - "errno 0.3.14", - "libc", - "rustix 1.1.2", -] - -[[package]] -name = "rustls" -version = "0.21.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" -dependencies = [ - "log", - "ring", - "rustls-webpki 0.101.7", - "sct", -] - [[package]] name = "rustls" version = "0.23.33" @@ -14004,23 +4831,11 @@ dependencies = [ "once_cell", "ring", "rustls-pki-types", - "rustls-webpki 0.103.7", + "rustls-webpki", "subtle", "zeroize", ] -[[package]] -name = "rustls-native-certs" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" -dependencies = [ - "openssl-probe", - "rustls-pemfile 1.0.4", - "schannel", - "security-framework 2.11.1", -] - [[package]] name = "rustls-native-certs" version = "0.8.2" @@ -14030,16 +4845,7 @@ dependencies = [ "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.5.1", -] - -[[package]] -name = "rustls-pemfile" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" -dependencies = [ - "base64 0.21.7", + "security-framework", ] [[package]] @@ -14072,11 +4878,11 @@ dependencies = [ "jni", "log", "once_cell", - "rustls 0.23.33", - "rustls-native-certs 0.8.2", + "rustls", + "rustls-native-certs", "rustls-platform-verifier-android", - "rustls-webpki 0.103.7", - "security-framework 3.5.1", + "rustls-webpki", + "security-framework", "security-framework-sys", "webpki-root-certs", "windows-sys 0.59.0", @@ -14088,16 +4894,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" -[[package]] -name = "rustls-webpki" -version = "0.101.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" -dependencies = [ - "ring", - "untrusted 0.9.0", -] - [[package]] name = "rustls-webpki" version = "0.103.7" @@ -14107,7 +4903,7 @@ dependencies = [ "aws-lc-rs", "ring", "rustls-pki-types", - "untrusted 0.9.0", + "untrusted", ] [[package]] @@ -14157,25 +4953,6 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" -[[package]] -name = "safetensors" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44560c11236a6130a46ce36c836a62936dc81ebf8c36a37947423571be0e55b6" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "salsa20" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" -dependencies = [ - "cipher", -] - [[package]] name = "same-file" version = "1.0.6" @@ -14201,24 +4978,11 @@ dependencies = [ "async-task", "backtrace", "chrono", - "futures 0.3.31", + "futures", "parking_lot", "rand 0.9.2", ] -[[package]] -name = "schema_generator" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "env_logger 0.11.8", - "schemars", - "serde", - "serde_json", - "theme", -] - [[package]] name = "schemars" version = "1.0.4" @@ -14257,12 +5021,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "scratch" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2" - [[package]] name = "screencapturekit" version = "0.2.8" @@ -14286,203 +5044,12 @@ dependencies = [ "once_cell", ] -[[package]] -name = "scroll" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" -dependencies = [ - "scroll_derive", -] - -[[package]] -name = "scroll_derive" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "scrypt" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" -dependencies = [ - "password-hash 0.5.0", - "pbkdf2 0.12.2", - "salsa20", - "sha2", -] - -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted 0.9.0", -] - -[[package]] -name = "sea-bae" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f694a6ab48f14bc063cfadff30ab551d3c7e46d8f81836c51989d548f44a2a25" -dependencies = [ - "heck 0.4.1", - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "sea-orm" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21e61af841881c137d4bc8e0d8411cee9168548b404f9e4788e8af7e8f94bd4e" -dependencies = [ - "async-stream", - "async-trait", - "bigdecimal", - "chrono", - "futures-util", - "log", - "ouroboros", - "pgvector", - "rust_decimal", - "sea-orm-macros", - "sea-query", - "sea-query-binder", - "serde", - "serde_json", - "sqlx", - "strum 0.26.3", - "thiserror 2.0.17", - "time", - "tracing", - "url", - "uuid", -] - -[[package]] -name = "sea-orm-macros" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6b86e3e77b548e6c6c1f612a1ca024d557dffdb81b838bf482ad3222140c77b" -dependencies = [ - "heck 0.4.1", - "proc-macro2", - "quote", - "sea-bae", - "syn 2.0.106", - "unicode-ident", -] - -[[package]] -name = "sea-query" -version = "0.32.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a5d1c518eaf5eda38e5773f902b26ab6d5e9e9e2bb2349ca6c64cf96f80448c" -dependencies = [ - "bigdecimal", - "chrono", - "inherent", - "ordered-float 4.6.0", - "rust_decimal", - "serde_json", - "time", - "uuid", -] - -[[package]] -name = "sea-query-binder" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0019f47430f7995af63deda77e238c17323359af241233ec768aba1faea7608" -dependencies = [ - "bigdecimal", - "chrono", - "rust_decimal", - "sea-query", - "serde_json", - "sqlx", - "time", - "uuid", -] - [[package]] name = "seahash" version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" -[[package]] -name = "search" -version = "0.1.0" -dependencies = [ - "any_vec", - "anyhow", - "bitflags 2.9.4", - "client", - "collections", - "editor", - "futures 0.3.31", - "gpui", - "itertools 0.14.0", - "language", - "lsp", - "menu", - "pretty_assertions", - "project", - "schemars", - "serde", - "serde_json", - "settings", - "smol", - "theme", - "tracing", - "ui", - "unindent", - "util", - "util_macros", - "workspace", - "zed_actions", - "ztracing", -] - -[[package]] -name = "sec1" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" -dependencies = [ - "base16ct", - "der 0.6.1", - "generic-array", - "pkcs8 0.9.0", - "subtle", - "zeroize", -] - -[[package]] -name = "security-framework" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" -dependencies = [ - "bitflags 2.9.4", - "core-foundation 0.9.4", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - [[package]] name = "security-framework" version = "3.5.1" @@ -14522,12 +5089,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "seq-macro" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" - [[package]] name = "serde" version = "1.0.228" @@ -14605,17 +5166,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_path_to_error" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" -dependencies = [ - "itoa", - "serde", - "serde_core", -] - [[package]] name = "serde_repr" version = "0.1.20" @@ -14657,160 +5207,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_yaml" -version = "0.9.34+deprecated" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" -dependencies = [ - "indexmap", - "itoa", - "ryu", - "serde", - "unsafe-libyaml", -] - -[[package]] -name = "serial2" -version = "0.2.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cc76fa68e25e771492ca1e3c53d447ef0be3093e05cd3b47f4b712ba10c6f3c" -dependencies = [ - "cfg-if", - "libc", - "winapi", -] - -[[package]] -name = "session" -version = "0.1.0" -dependencies = [ - "db", - "gpui", - "serde_json", - "util", - "uuid", -] - -[[package]] -name = "settings" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "derive_more 0.99.20", - "ec4rs", - "fs", - "futures 0.3.31", - "gpui", - "indoc", - "inventory", - "log", - "migrator", - "paths", - "pretty_assertions", - "release_channel", - "rust-embed", - "schemars", - "serde", - "serde_json", - "serde_json_lenient", - "serde_repr", - "settings_json", - "settings_macros", - "smallvec", - "strum 0.27.2", - "unindent", - "util", - "zlog", -] - -[[package]] -name = "settings_json" -version = "0.1.0" -dependencies = [ - "anyhow", - "pretty_assertions", - "serde", - "serde_json", - "serde_json_lenient", - "serde_path_to_error", - "tree-sitter", - "tree-sitter-json", - "unindent", - "util", -] - -[[package]] -name = "settings_macros" -version = "0.1.0" -dependencies = [ - "quote", - "settings", - "syn 2.0.106", -] - -[[package]] -name = "settings_profile_selector" -version = "0.1.0" -dependencies = [ - "client", - "editor", - "fuzzy", - "gpui", - "language", - "menu", - "picker", - "project", - "serde_json", - "settings", - "theme", - "ui", - "workspace", - "zed_actions", -] - -[[package]] -name = "settings_ui" -version = "0.1.0" -dependencies = [ - "anyhow", - "assets", - "bm25", - "client", - "editor", - "feature_flags", - "fs", - "futures 0.3.31", - "fuzzy", - "gpui", - "heck 0.5.0", - "language", - "log", - "menu", - "node_runtime", - "paths", - "picker", - "pretty_assertions", - "project", - "release_channel", - "schemars", - "search", - "serde", - "session", - "settings", - "strum 0.27.2", - "telemetry", - "theme", - "title_bar", - "ui", - "ui_input", - "util", - "workspace", - "zed_actions", - "zlog", -] - [[package]] name = "sha1" version = "0.10.6" @@ -14839,65 +5235,12 @@ dependencies = [ "digest", ] -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shared_library" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a9e7e0f2bfae24d8a5b5a66c5b257a83c7412304311512a0c054cd5e619da11" -dependencies = [ - "lazy_static", - "libc", -] - -[[package]] -name = "shell-words" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" - -[[package]] -name = "shellexpand" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ccc8076840c4da029af4f87e4e8daeb0fca6b87bbb02e10cb60b791450e11e4" -dependencies = [ - "dirs 4.0.0", -] - -[[package]] -name = "shellexpand" -version = "3.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1fdf65dd6331831494dd616b30351c38e96e45921a27745cf98490458b90bb" -dependencies = [ - "dirs 6.0.0", -] - [[package]] name = "shlex" version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "signal-hook" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" -dependencies = [ - "libc", - "signal-hook-registry", -] - [[package]] name = "signal-hook-registry" version = "1.4.6" @@ -14907,26 +5250,6 @@ dependencies = [ "libc", ] -[[package]] -name = "signature" -version = "1.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" -dependencies = [ - "digest", - "rand_core 0.6.4", -] - -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest", - "rand_core 0.6.4", -] - [[package]] name = "simd-adler32" version = "0.3.7" @@ -14942,24 +5265,6 @@ dependencies = [ "quote", ] -[[package]] -name = "simdutf8" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" - -[[package]] -name = "simple_asn1" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" -dependencies = [ - "num-bigint", - "num-traits", - "thiserror 2.0.17", - "time", -] - [[package]] name = "simplecss" version = "0.2.2" @@ -14969,17 +5274,6 @@ dependencies = [ "log", ] -[[package]] -name = "simplelog" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" -dependencies = [ - "log", - "termcolor", - "time", -] - [[package]] name = "siphasher" version = "1.0.1" @@ -15002,13 +5296,6 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" -[[package]] -name = "slash_commands_example" -version = "0.1.0" -dependencies = [ - "zed_extension_api 0.1.0", -] - [[package]] name = "slotmap" version = "1.0.7" @@ -15023,20 +5310,6 @@ name = "smallvec" version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -dependencies = [ - "serde", -] - -[[package]] -name = "smart-default" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eb01866308440fc64d6c44d9e86c5cc17adfe33c4d6eed55da9145044d0ffc1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] [[package]] name = "smol" @@ -15048,7 +5321,7 @@ dependencies = [ "async-executor", "async-fs", "async-io", - "async-lock 3.4.1", + "async-lock", "async-net", "async-process", "blocking", @@ -15061,62 +5334,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" -[[package]] -name = "snippet" -version = "0.1.0" -dependencies = [ - "anyhow", - "smallvec", -] - -[[package]] -name = "snippet_provider" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "extension", - "fs", - "futures 0.3.31", - "gpui", - "indoc", - "parking_lot", - "paths", - "schemars", - "serde", - "serde_json", - "serde_json_lenient", - "snippet", - "util", -] - -[[package]] -name = "snippets_ui" -version = "0.1.0" -dependencies = [ - "file_finder", - "file_icons", - "fuzzy", - "gpui", - "language", - "paths", - "picker", - "settings", - "ui", - "util", - "workspace", -] - -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "socket2" version = "0.6.1" @@ -15163,278 +5380,6 @@ dependencies = [ "bitflags 2.9.4", ] -[[package]] -name = "spki" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" -dependencies = [ - "base64ct", - "der 0.6.1", -] - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der 0.7.10", -] - -[[package]] -name = "sptr" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" - -[[package]] -name = "sqlez" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "futures 0.3.31", - "indoc", - "libsqlite3-sys", - "log", - "parking_lot", - "smol", - "sqlformat", - "thread_local", - "util", - "uuid", -] - -[[package]] -name = "sqlez_macros" -version = "0.1.0" -dependencies = [ - "sqlez", - "sqlformat", - "syn 2.0.106", -] - -[[package]] -name = "sqlformat" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" -dependencies = [ - "nom 7.1.3", - "unicode_categories", -] - -[[package]] -name = "sqlx" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" -dependencies = [ - "sqlx-core", - "sqlx-macros", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", -] - -[[package]] -name = "sqlx-core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" -dependencies = [ - "base64 0.22.1", - "bigdecimal", - "bytes 1.10.1", - "chrono", - "crc", - "crossbeam-queue", - "either", - "event-listener 5.4.1", - "futures-core", - "futures-intrusive", - "futures-io", - "futures-util", - "hashbrown 0.15.5", - "hashlink 0.10.0", - "indexmap", - "log", - "memchr", - "once_cell", - "percent-encoding", - "rust_decimal", - "rustls 0.23.33", - "serde", - "serde_json", - "sha2", - "smallvec", - "thiserror 2.0.17", - "time", - "tokio", - "tokio-stream", - "tracing", - "url", - "uuid", - "webpki-roots", -] - -[[package]] -name = "sqlx-macros" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d" -dependencies = [ - "proc-macro2", - "quote", - "sqlx-core", - "sqlx-macros-core", - "syn 2.0.106", -] - -[[package]] -name = "sqlx-macros-core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b" -dependencies = [ - "dotenvy", - "either", - "heck 0.5.0", - "hex", - "once_cell", - "proc-macro2", - "quote", - "serde", - "serde_json", - "sha2", - "sqlx-core", - "sqlx-mysql", - "sqlx-postgres", - "sqlx-sqlite", - "syn 2.0.106", - "tokio", - "url", -] - -[[package]] -name = "sqlx-mysql" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526" -dependencies = [ - "atoi", - "base64 0.22.1", - "bigdecimal", - "bitflags 2.9.4", - "byteorder", - "bytes 1.10.1", - "chrono", - "crc", - "digest", - "dotenvy", - "either", - "futures-channel", - "futures-core", - "futures-io", - "futures-util", - "generic-array", - "hex", - "hkdf", - "hmac", - "itoa", - "log", - "md-5", - "memchr", - "once_cell", - "percent-encoding", - "rand 0.8.5", - "rsa", - "rust_decimal", - "serde", - "sha1", - "sha2", - "smallvec", - "sqlx-core", - "stringprep", - "thiserror 2.0.17", - "time", - "tracing", - "uuid", - "whoami", -] - -[[package]] -name = "sqlx-postgres" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46" -dependencies = [ - "atoi", - "base64 0.22.1", - "bigdecimal", - "bitflags 2.9.4", - "byteorder", - "chrono", - "crc", - "dotenvy", - "etcetera", - "futures-channel", - "futures-core", - "futures-util", - "hex", - "hkdf", - "hmac", - "home", - "itoa", - "log", - "md-5", - "memchr", - "num-bigint", - "once_cell", - "rand 0.8.5", - "rust_decimal", - "serde", - "serde_json", - "sha2", - "smallvec", - "sqlx-core", - "stringprep", - "thiserror 2.0.17", - "time", - "tracing", - "uuid", - "whoami", -] - -[[package]] -name = "sqlx-sqlite" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea" -dependencies = [ - "atoi", - "chrono", - "flume", - "futures-channel", - "futures-core", - "futures-executor", - "futures-intrusive", - "futures-util", - "libsqlite3-sys", - "log", - "percent-encoding", - "serde", - "serde_urlencoded", - "sqlx-core", - "thiserror 2.0.17", - "time", - "tracing", - "url", - "uuid", -] - [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -15481,74 +5426,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "stop-words" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645a3d441ccf4bf47f2e4b7681461986681a6eeea9937d4c3bc9febd61d17c71" -dependencies = [ - "serde_json", -] - -[[package]] -name = "story" -version = "0.1.0" -dependencies = [ - "gpui", - "itertools 0.14.0", - "smallvec", -] - -[[package]] -name = "storybook" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "collab_ui", - "ctrlc", - "dialoguer", - "editor", - "fuzzy", - "gpui", - "indoc", - "language", - "log", - "menu", - "picker", - "reqwest_client", - "rust-embed", - "settings", - "simplelog", - "story", - "strum 0.27.2", - "theme", - "title_bar", - "ui", -] - -[[package]] -name = "streaming-iterator" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520" - -[[package]] -name = "streaming_diff" -version = "0.1.0" -dependencies = [ - "ordered-float 2.10.1", - "rand 0.9.2", - "rope", - "util", -] - -[[package]] -name = "strength_reduce" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" - [[package]] name = "strict-num" version = "0.1.1" @@ -15566,7 +5443,7 @@ checksum = "bf776ba3fa74f83bf4b63c3dcbbf82173db2632ed8452cb2d891d33f459de70f" dependencies = [ "new_debug_unreachable", "parking_lot", - "phf_shared 0.11.3", + "phf_shared", "precomputed-hash", "serde", ] @@ -15577,29 +5454,12 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c711928715f1fe0fe509c53b43e993a9a557babc2d0a3567d0a3006f1ac931a0" dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", + "phf_generator", + "phf_shared", "proc-macro2", "quote", ] -[[package]] -name = "stringprep" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" -dependencies = [ - "unicode-bidi", - "unicode-normalization", - "unicode-properties", -] - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - [[package]] name = "strum" version = "0.26.3" @@ -15659,51 +5519,6 @@ dependencies = [ "rand 0.9.2", "rayon", "tracing", - "zlog", - "ztracing", -] - -[[package]] -name = "supermaven" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "collections", - "edit_prediction_types", - "editor", - "env_logger 0.11.8", - "futures 0.3.31", - "gpui", - "http_client", - "language", - "log", - "postage", - "project", - "serde", - "serde_json", - "settings", - "smol", - "supermaven_api", - "text", - "theme", - "ui", - "unicode-segmentation", - "util", -] - -[[package]] -name = "supermaven_api" -version = "0.1.0" -dependencies = [ - "anyhow", - "futures 0.3.31", - "http_client", - "paths", - "serde", - "serde_json", - "smol", - "util", ] [[package]] @@ -15790,18 +5605,6 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0193cc4331cfd2f3d2011ef287590868599a2f33c3e69bc22c1a3d3acf9e02fb" -[[package]] -name = "svg_preview" -version = "0.1.0" -dependencies = [ - "file_icons", - "gpui", - "language", - "multi_buffer", - "ui", - "workspace", -] - [[package]] name = "svgtypes" version = "0.15.3" @@ -15823,153 +5626,6 @@ dependencies = [ "zeno", ] -[[package]] -name = "symphonia" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5773a4c030a19d9bfaa090f49746ff35c75dfddfa700df7a5939d5e076a57039" -dependencies = [ - "lazy_static", - "symphonia-bundle-flac", - "symphonia-bundle-mp3", - "symphonia-codec-aac", - "symphonia-codec-pcm", - "symphonia-codec-vorbis", - "symphonia-core", - "symphonia-format-isomp4", - "symphonia-format-ogg", - "symphonia-format-riff", - "symphonia-metadata", -] - -[[package]] -name = "symphonia-bundle-flac" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c91565e180aea25d9b80a910c546802526ffd0072d0b8974e3ebe59b686c9976" -dependencies = [ - "log", - "symphonia-core", - "symphonia-metadata", - "symphonia-utils-xiph", -] - -[[package]] -name = "symphonia-bundle-mp3" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4872dd6bb56bf5eac799e3e957aa1981086c3e613b27e0ac23b176054f7c57ed" -dependencies = [ - "lazy_static", - "log", - "symphonia-core", - "symphonia-metadata", -] - -[[package]] -name = "symphonia-codec-aac" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c263845aa86881416849c1729a54c7f55164f8b96111dba59de46849e73a790" -dependencies = [ - "lazy_static", - "log", - "symphonia-core", -] - -[[package]] -name = "symphonia-codec-pcm" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e89d716c01541ad3ebe7c91ce4c8d38a7cf266a3f7b2f090b108fb0cb031d95" -dependencies = [ - "log", - "symphonia-core", -] - -[[package]] -name = "symphonia-codec-vorbis" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f025837c309cd69ffef572750b4a2257b59552c5399a5e49707cc5b1b85d1c73" -dependencies = [ - "log", - "symphonia-core", - "symphonia-utils-xiph", -] - -[[package]] -name = "symphonia-core" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea00cc4f79b7f6bb7ff87eddc065a1066f3a43fe1875979056672c9ef948c2af" -dependencies = [ - "arrayvec", - "bitflags 1.3.2", - "bytemuck", - "lazy_static", - "log", -] - -[[package]] -name = "symphonia-format-isomp4" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243739585d11f81daf8dac8d9f3d18cc7898f6c09a259675fc364b382c30e0a5" -dependencies = [ - "encoding_rs", - "log", - "symphonia-core", - "symphonia-metadata", - "symphonia-utils-xiph", -] - -[[package]] -name = "symphonia-format-ogg" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b4955c67c1ed3aa8ae8428d04ca8397fbef6a19b2b051e73b5da8b1435639cb" -dependencies = [ - "log", - "symphonia-core", - "symphonia-metadata", - "symphonia-utils-xiph", -] - -[[package]] -name = "symphonia-format-riff" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2d7c3df0e7d94efb68401d81906eae73c02b40d5ec1a141962c592d0f11a96f" -dependencies = [ - "extended", - "log", - "symphonia-core", - "symphonia-metadata", -] - -[[package]] -name = "symphonia-metadata" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36306ff42b9ffe6e5afc99d49e121e0bd62fe79b9db7b9681d48e29fa19e6b16" -dependencies = [ - "encoding_rs", - "lazy_static", - "log", - "symphonia-core", -] - -[[package]] -name = "symphonia-utils-xiph" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27c85ab799a338446b68eec77abf42e1a6f1bb490656e121c6e27bfbab9f16" -dependencies = [ - "symphonia-core", - "symphonia-metadata", -] - [[package]] name = "syn" version = "1.0.109" @@ -15992,12 +5648,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - [[package]] name = "sync_wrapper" version = "1.0.2" @@ -16007,15 +5657,6 @@ dependencies = [ "futures-core", ] -[[package]] -name = "synchronoise" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dbc01390fc626ce8d1cffe3376ded2b72a11bb70e1c75f404a210e4daa4def2" -dependencies = [ - "crossbeam-queue", -] - [[package]] name = "synstructure" version = "0.13.2" @@ -16036,34 +5677,6 @@ dependencies = [ "libc", ] -[[package]] -name = "sysctl" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7dddc5f0fee506baf8b9fdb989e242f17e4b11c61dfbb0635b705217199eea" -dependencies = [ - "bitflags 2.9.4", - "byteorder", - "enum-as-inner", - "libc", - "thiserror 1.0.69", - "walkdir", -] - -[[package]] -name = "sysctl" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01198a2debb237c62b6826ec7081082d951f46dbb64b0e8c7649a452230d1dfc" -dependencies = [ - "bitflags 2.9.4", - "byteorder", - "enum-as-inner", - "libc", - "thiserror 1.0.69", - "walkdir", -] - [[package]] name = "sysinfo" version = "0.31.4" @@ -16078,31 +5691,6 @@ dependencies = [ "windows 0.57.0", ] -[[package]] -name = "sysinfo" -version = "0.37.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16607d5caffd1c07ce073528f9ed972d88db15dd44023fa57142963be3feb11f" -dependencies = [ - "libc", - "memchr", - "ntapi", - "objc2-core-foundation", - "objc2-io-kit", - "windows 0.61.3", -] - -[[package]] -name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation 0.9.4", - "system-configuration-sys 0.5.0", -] - [[package]] name = "system-configuration" version = "0.6.1" @@ -16111,17 +5699,7 @@ checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.9.4", "core-foundation 0.9.4", - "system-configuration-sys 0.6.0", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys", - "libc", + "system-configuration-sys", ] [[package]] @@ -16147,63 +5725,6 @@ dependencies = [ "version-compare", ] -[[package]] -name = "system-interface" -version = "0.27.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc4592f674ce18521c2a81483873a49596655b179f71c5e05d10c1fe66c78745" -dependencies = [ - "bitflags 2.9.4", - "cap-fs-ext", - "cap-std", - "fd-lock", - "io-lifetimes", - "rustix 0.38.44", - "windows-sys 0.59.0", - "winx", -] - -[[package]] -name = "system_specs" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "gpui", - "human_bytes", - "pciid-parser", - "release_channel", - "semver", - "serde", - "sysinfo 0.37.2", -] - -[[package]] -name = "tab_switcher" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "ctor", - "editor", - "fuzzy", - "gpui", - "language", - "menu", - "picker", - "project", - "schemars", - "serde", - "serde_json", - "settings", - "smol", - "theme", - "ui", - "util", - "workspace", - "zlog", -] - [[package]] name = "taffy" version = "0.9.0" @@ -16216,12 +5737,6 @@ dependencies = [ "slotmap", ] -[[package]] -name = "tagptr" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" - [[package]] name = "take-until" version = "0.2.0" @@ -16240,92 +5755,12 @@ dependencies = [ "objc", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "target-lexicon" version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" -[[package]] -name = "target-lexicon" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c" - -[[package]] -name = "task" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "futures 0.3.31", - "gpui", - "hex", - "log", - "parking_lot", - "pretty_assertions", - "proto", - "schemars", - "serde", - "serde_json", - "serde_json_lenient", - "sha2", - "shellexpand 2.1.2", - "util", - "zed_actions", -] - -[[package]] -name = "tasks_ui" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "editor", - "file_icons", - "fuzzy", - "gpui", - "itertools 0.14.0", - "language", - "menu", - "picker", - "project", - "serde", - "serde_json", - "task", - "tree-sitter-rust", - "tree-sitter-typescript", - "ui", - "util", - "workspace", - "zed_actions", -] - -[[package]] -name = "telemetry" -version = "0.1.0" -dependencies = [ - "futures 0.3.31", - "serde", - "serde_json", - "telemetry_events", -] - -[[package]] -name = "telemetry_events" -version = "0.1.0" -dependencies = [ - "semver", - "serde", - "serde_json", -] - [[package]] name = "tempfile" version = "3.23.0" @@ -16359,181 +5794,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "terminal" -version = "0.1.0" -dependencies = [ - "alacritty_terminal", - "anyhow", - "collections", - "futures 0.3.31", - "gpui", - "itertools 0.14.0", - "libc", - "log", - "rand 0.9.2", - "regex", - "release_channel", - "schemars", - "serde", - "serde_json", - "settings", - "smol", - "sysinfo 0.37.2", - "task", - "theme", - "thiserror 2.0.17", - "url", - "urlencoding", - "util", - "util_macros", - "windows 0.61.3", -] - -[[package]] -name = "terminal_size" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" -dependencies = [ - "rustix 1.1.2", - "windows-sys 0.60.2", -] - -[[package]] -name = "terminal_view" -version = "0.1.0" -dependencies = [ - "anyhow", - "assistant_slash_command", - "async-recursion", - "breadcrumbs", - "client", - "collections", - "db", - "dirs 4.0.0", - "editor", - "futures 0.3.31", - "gpui", - "itertools 0.14.0", - "language", - "log", - "pretty_assertions", - "project", - "rand 0.9.2", - "regex", - "schemars", - "search", - "serde", - "serde_json", - "settings", - "shellexpand 2.1.2", - "task", - "terminal", - "theme", - "ui", - "util", - "workspace", - "zed_actions", -] - -[[package]] -name = "text" -version = "0.1.0" -dependencies = [ - "anyhow", - "clock", - "collections", - "ctor", - "gpui", - "http_client", - "log", - "parking_lot", - "postage", - "rand 0.9.2", - "regex", - "rope", - "smallvec", - "sum_tree", - "util", - "zlog", -] - -[[package]] -name = "theme" -version = "0.1.0" -dependencies = [ - "anyhow", - "collections", - "derive_more 0.99.20", - "fs", - "futures 0.3.31", - "gpui", - "log", - "palette", - "parking_lot", - "refineable", - "schemars", - "serde", - "serde_json", - "serde_json_lenient", - "settings", - "strum 0.27.2", - "thiserror 2.0.17", - "util", - "uuid", -] - -[[package]] -name = "theme_extension" -version = "0.1.0" -dependencies = [ - "anyhow", - "extension", - "fs", - "gpui", - "theme", -] - -[[package]] -name = "theme_importer" -version = "0.1.0" -dependencies = [ - "anyhow", - "clap", - "collections", - "gpui", - "indexmap", - "log", - "palette", - "serde", - "serde_json", - "serde_json_lenient", - "simplelog", - "strum 0.27.2", - "theme", - "vscode_theme", -] - -[[package]] -name = "theme_selector" -version = "0.1.0" -dependencies = [ - "fs", - "fuzzy", - "gpui", - "log", - "picker", - "serde", - "settings", - "telemetry", - "theme", - "ui", - "util", - "workspace", - "zed_actions", -] - [[package]] name = "thiserror" version = "1.0.69" @@ -16574,15 +5834,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - [[package]] name = "tiff" version = "0.10.3" @@ -16597,63 +5848,6 @@ dependencies = [ "zune-jpeg", ] -[[package]] -name = "tiktoken-rs" -version = "0.9.1" -source = "git+https://github.com/zed-industries/tiktoken-rs?rev=2570c4387a8505fb8f1d3f3557454b474f1e8271#2570c4387a8505fb8f1d3f3557454b474f1e8271" -dependencies = [ - "anyhow", - "base64 0.22.1", - "bstr", - "fancy-regex", - "lazy_static", - "regex", - "rustc-hash 1.1.0", -] - -[[package]] -name = "time" -version = "0.3.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" -dependencies = [ - "deranged", - "itoa", - "libc", - "num-conv", - "num_threads", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" - -[[package]] -name = "time-macros" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "time_format" -version = "0.1.0" -dependencies = [ - "core-foundation 0.10.0", - "core-foundation-sys", - "sys-locale", - "time", -] - [[package]] name = "tiny-keccak" version = "2.0.2" @@ -16689,19 +5883,6 @@ dependencies = [ "strict-num", ] -[[package]] -name = "tiny_http" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce51b50006056f590c9b7c3808c3bd70f0d1101666629713866c227d6e58d39" -dependencies = [ - "ascii", - "chrono", - "chunked_transfer", - "log", - "url", -] - [[package]] name = "tinystr" version = "0.8.1" @@ -16712,16 +5893,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "tinytemplate" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" -dependencies = [ - "serde", - "serde_json", -] - [[package]] name = "tinyvec" version = "1.10.0" @@ -16737,69 +5908,21 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" -[[package]] -name = "title_bar" -version = "0.1.0" -dependencies = [ - "anyhow", - "auto_update", - "call", - "channel", - "chrono", - "client", - "cloud_llm_client", - "collections", - "db", - "gpui", - "http_client", - "notifications", - "pretty_assertions", - "project", - "remote", - "rpc", - "schemars", - "serde", - "settings", - "smallvec", - "story", - "telemetry", - "theme", - "tree-sitter-md", - "ui", - "util", - "windows 0.61.3", - "workspace", - "zed_actions", -] - [[package]] name = "tokio" version = "1.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" dependencies = [ - "bytes 1.10.1", + "bytes", "libc", - "mio 1.1.0", - "parking_lot", + "mio", "pin-project-lite", - "signal-hook-registry", - "socket2 0.6.1", + "socket2", "tokio-macros", "windows-sys 0.61.2", ] -[[package]] -name = "tokio-io" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57fc868aae093479e3131e3d165c93b1c7474109d13c90ec0dda2a1bbfff0674" -dependencies = [ - "bytes 0.4.12", - "futures 0.1.31", - "log", -] - [[package]] name = "tokio-macros" version = "2.6.0" @@ -16811,33 +5934,13 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" -dependencies = [ - "rustls 0.21.12", - "tokio", -] - [[package]] name = "tokio-rustls" version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" dependencies = [ - "rustls 0.23.33", + "rustls", "tokio", ] @@ -16848,85 +5951,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d4770b8024672c1101b3f6733eab95b18007dbe0847a8afe341fcf79e06043f" dependencies = [ "either", - "futures-io", "futures-util", "thiserror 1.0.69", "tokio", ] -[[package]] -name = "tokio-stream" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" -dependencies = [ - "futures-util", - "log", - "tokio", - "tungstenite 0.20.1", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" -dependencies = [ - "futures-util", - "log", - "tokio", - "tungstenite 0.21.0", -] - -[[package]] -name = "tokio-tungstenite" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084" -dependencies = [ - "futures-util", - "log", - "rustls 0.23.33", - "rustls-pki-types", - "tokio", - "tokio-rustls 0.26.2", - "tungstenite 0.26.2", -] - [[package]] name = "tokio-util" version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ - "bytes 1.10.1", + "bytes", "futures-core", - "futures-io", "futures-sink", "pin-project-lite", "tokio", ] -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - [[package]] name = "toml" version = "0.8.23" @@ -17019,48 +6061,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" -[[package]] -name = "toolchain_selector" -version = "0.1.0" -dependencies = [ - "anyhow", - "convert_case 0.8.0", - "editor", - "file_finder", - "futures 0.3.31", - "fuzzy", - "gpui", - "language", - "menu", - "picker", - "project", - "ui", - "util", - "workspace", -] - [[package]] name = "topological-sort" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea68304e134ecd095ac6c3574494fc62b909f416c4fca77e440530221e549d3d" -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tokio", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tower" version = "0.5.2" @@ -17070,67 +6076,12 @@ dependencies = [ "futures-core", "futures-util", "pin-project-lite", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", "tower-layer", "tower-service", ] -[[package]] -name = "tower-http" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f873044bf02dd1e8239e9c1293ea39dad76dc594ec16185d0a1bf31d8dc8d858" -dependencies = [ - "bitflags 1.3.2", - "bytes 1.10.1", - "futures-core", - "futures-util", - "http 0.2.12", - "http-body 0.4.6", - "http-range-header", - "pin-project-lite", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" -dependencies = [ - "bitflags 2.9.4", - "bytes 1.10.1", - "futures-core", - "futures-util", - "http 0.2.12", - "http-body 0.4.6", - "http-range-header", - "pin-project-lite", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-http" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" -dependencies = [ - "bitflags 2.9.4", - "bytes 1.10.1", - "futures-util", - "http 1.3.1", - "http-body 1.0.1", - "iri-string", - "pin-project-lite", - "tower 0.5.2", - "tower-layer", - "tower-service", -] - [[package]] name = "tower-layer" version = "0.3.3" @@ -17173,335 +6124,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" dependencies = [ "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-serde" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" -dependencies = [ - "serde", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "serde", - "serde_json", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", - "tracing-serde", -] - -[[package]] -name = "tracing-tracy" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eaa1852afa96e0fe9e44caa53dc0bd2d9d05e0f2611ce09f97f8677af56e4ba" -dependencies = [ - "tracing-core", - "tracing-subscriber", - "tracy-client", -] - -[[package]] -name = "tracy-client" -version = "0.18.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91d722a05fe49b31fef971c4732a7d4aa6a18283d9ba46abddab35f484872947" -dependencies = [ - "loom", - "once_cell", - "tracy-client-sys", -] - -[[package]] -name = "tracy-client-sys" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fb391ac70462b3097a755618fbf9c8f95ecc1eb379a414f7b46f202ed10db1f" -dependencies = [ - "cc", - "windows-targets 0.52.6", -] - -[[package]] -name = "trait-variant" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70977707304198400eb4835a78f6a9f928bf41bba420deb8fdb175cd965d77a7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "transpose" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" -dependencies = [ - "num-integer", - "strength_reduce", -] - -[[package]] -name = "tree-sitter" -version = "0.25.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78f873475d258561b06f1c595d93308a7ed124d9977cb26b148c2084a4a3cc87" -dependencies = [ - "cc", - "regex", - "regex-syntax", - "serde_json", - "streaming-iterator", - "tree-sitter-language", - "wasmtime-c-api-impl", -] - -[[package]] -name = "tree-sitter-bash" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5ec769279cc91b561d3df0d8a5deb26b0ad40d183127f409494d6d8fc53062" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-c" -version = "0.23.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afd2b1bf1585dc2ef6d69e87d01db8adb059006649dd5f96f31aa789ee6e9c71" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-cpp" -version = "0.23.4" -source = "git+https://github.com/tree-sitter/tree-sitter-cpp?rev=5cb9b693cfd7bfacab1d9ff4acac1a4150700609#5cb9b693cfd7bfacab1d9ff4acac1a4150700609" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-css" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ad6489794d41350d12a7fbe520e5199f688618f43aace5443980d1ddcf1b29e" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-diff" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfe1e5ca280a65dfe5ba4205c1bcc84edf486464fed315db53dee6da9a335889" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-elixir" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e45d444647b4fd53d8fd32474c1b8bedc1baa22669ce3a78d083e365fa9a2d3f" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-embedded-template" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790063ef14e5b67556abc0b3be0ed863fb41d65ee791cf8c0b20eb42a1fa46af" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-gitcommit" -version = "0.0.1" -source = "git+https://github.com/zed-industries/tree-sitter-git-commit?rev=88309716a69dd13ab83443721ba6e0b491d37ee9#88309716a69dd13ab83443721ba6e0b491d37ee9" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-go" -version = "0.23.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b13d476345220dbe600147dd444165c5791bf85ef53e28acbedd46112ee18431" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-gomod" -version = "1.1.1" -source = "git+https://github.com/camdencheek/tree-sitter-go-mod?rev=2e886870578eeba1927a2dc4bd2e2b3f598c5f9a#2e886870578eeba1927a2dc4bd2e2b3f598c5f9a" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-gowork" -version = "0.0.1" -source = "git+https://github.com/zed-industries/tree-sitter-go-work?rev=acb0617bf7f4fda02c6217676cc64acb89536dc7#acb0617bf7f4fda02c6217676cc64acb89536dc7" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-heex" -version = "0.0.1" -source = "git+https://github.com/zed-industries/tree-sitter-heex?rev=1dd45142fbb05562e35b2040c6129c9bca346592#1dd45142fbb05562e35b2040c6129c9bca346592" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-html" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "261b708e5d92061ede329babaaa427b819329a9d427a1d710abb0f67bbef63ee" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-jsdoc" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3862dfcb1038fc5e7812d7df14190afdeb7e1415288fd5f51f58395f8cb0faf" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-json" -version = "0.24.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d727acca406c0020cffc6cf35516764f36c8e3dc4408e5ebe2cb35a947ec471" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-language" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4013970217383f67b18aef68f6fb2e8d409bc5755227092d32efb0422ba24b8" - -[[package]] -name = "tree-sitter-md" -version = "0.3.2" -source = "git+https://github.com/tree-sitter-grammars/tree-sitter-markdown?rev=9a23c1a96c0513d8fc6520972beedd419a973539#9a23c1a96c0513d8fc6520972beedd419a973539" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-python" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bf85fd39652e740bf60f46f4cda9492c3a9ad75880575bf14960f775cb74a1c" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-regex" -version = "0.24.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712656f8c262a5a4b7d6026e6246950787d178d613864952554e1516a33ab0c1" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-ruby" -version = "0.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be0484ea4ef6bb9c575b4fdabde7e31340a8d2dbc7d52b321ac83da703249f95" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-rust" -version = "0.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b9b18034c684a2420722be8b2a91c9c44f2546b631c039edf575ccba8c61be1" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-typescript" -version = "0.23.2" -source = "git+https://github.com/zed-industries/tree-sitter-typescript?rev=e2c53597d6a5d9cf7bbe8dccde576fe1e46c5899#e2c53597d6a5d9cf7bbe8dccde576fe1e46c5899" -dependencies = [ - "cc", - "tree-sitter-language", -] - -[[package]] -name = "tree-sitter-yaml" -version = "0.6.1" -source = "git+https://github.com/zed-industries/tree-sitter-yaml?rev=baff0b51c64ef6a1fb1f8390f3ad6015b83ec13a#baff0b51c64ef6a1fb1f8390f3ad6015b83ec13a" -dependencies = [ - "cc", - "tree-sitter-language", ] [[package]] @@ -17531,88 +6153,6 @@ dependencies = [ "core_maths", ] -[[package]] -name = "tungstenite" -version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" -dependencies = [ - "byteorder", - "bytes 1.10.1", - "data-encoding", - "http 0.2.12", - "httparse", - "log", - "rand 0.8.5", - "sha1", - "thiserror 1.0.69", - "url", - "utf-8", -] - -[[package]] -name = "tungstenite" -version = "0.21.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" -dependencies = [ - "byteorder", - "bytes 1.10.1", - "data-encoding", - "http 1.3.1", - "httparse", - "log", - "rand 0.8.5", - "sha1", - "thiserror 1.0.69", - "url", - "utf-8", -] - -[[package]] -name = "tungstenite" -version = "0.26.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13" -dependencies = [ - "bytes 1.10.1", - "data-encoding", - "http 1.3.1", - "httparse", - "log", - "rand 0.9.2", - "rustls 0.23.33", - "rustls-pki-types", - "sha1", - "thiserror 2.0.17", - "utf-8", -] - -[[package]] -name = "tungstenite" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadc29d668c91fcc564941132e17b28a7ceb2f3ebf0b9dae3e03fd7a6748eb0d" -dependencies = [ - "bytes 1.10.1", - "data-encoding", - "http 1.3.1", - "httparse", - "log", - "rand 0.9.2", - "rustls 0.23.33", - "rustls-pki-types", - "sha1", - "thiserror 2.0.17", - "utf-8", -] - -[[package]] -name = "typed-path" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c462d18470a2857aa657d338af5fa67170bb48bcc80a296710ce3b0802a32566" - [[package]] name = "typeid" version = "1.0.3" @@ -17625,21 +6165,6 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - -[[package]] -name = "uds" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "885c31f06fce836457fe3ef09a59f83fe8db95d270b11cd78f40a4666c4d1661" -dependencies = [ - "libc", -] - [[package]] name = "uds_windows" version = "1.1.0" @@ -17651,87 +6176,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "ug" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90b70b37e9074642bc5f60bb23247fd072a84314ca9e71cdf8527593406a0dd3" -dependencies = [ - "gemm 0.18.2", - "half", - "libloading", - "memmap2", - "num", - "num-traits", - "num_cpus", - "rayon", - "safetensors", - "serde", - "thiserror 1.0.69", - "tracing", - "yoke 0.7.5", -] - -[[package]] -name = "ui" -version = "0.1.0" -dependencies = [ - "chrono", - "component", - "documented", - "gpui", - "gpui_macros", - "icons", - "itertools 0.14.0", - "menu", - "schemars", - "serde", - "settings", - "smallvec", - "story", - "strum 0.27.2", - "theme", - "ui_macros", - "util", - "windows 0.61.3", -] - -[[package]] -name = "ui_input" -version = "0.1.0" -dependencies = [ - "component", - "editor", - "gpui", - "menu", - "settings", - "theme", - "ui", -] - -[[package]] -name = "ui_macros" -version = "0.1.0" -dependencies = [ - "component", - "quote", - "syn 2.0.106", - "ui", -] - -[[package]] -name = "ui_prompt" -version = "0.1.0" -dependencies = [ - "gpui", - "markdown", - "menu", - "settings", - "theme", - "ui", - "workspace", -] - [[package]] name = "unicase" version = "2.8.1" @@ -17768,12 +6212,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e" -[[package]] -name = "unicode-general-category" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" - [[package]] name = "unicode-ident" version = "1.0.19" @@ -17786,15 +6224,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" -[[package]] -name = "unicode-normalization" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" -dependencies = [ - "tinyvec", -] - [[package]] name = "unicode-properties" version = "0.1.3" @@ -17831,30 +6260,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "unicode_categories" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" - -[[package]] -name = "unindent" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" - -[[package]] -name = "unsafe-libyaml" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" - -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - [[package]] name = "untrusted" version = "0.9.0" @@ -17873,19 +6278,13 @@ dependencies = [ "serde", ] -[[package]] -name = "urlencoding" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" - [[package]] name = "usvg" version = "0.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80be9b06fbae3b8b303400ab20778c80bbaf338f563afe567cf3c9eea17b47ef" dependencies = [ - "base64 0.22.1", + "base64", "data-url", "flate2", "fontdb 0.23.0", @@ -17935,7 +6334,7 @@ dependencies = [ "command-fds", "dirs 4.0.0", "dunce", - "futures 0.3.31", + "futures", "futures-lite 1.13.0", "git2", "globset", @@ -17943,7 +6342,7 @@ dependencies = [ "itertools 0.14.0", "libc", "log", - "mach2 0.5.0", + "mach2", "nix 0.29.0", "pretty_assertions", "rand 0.9.2", @@ -17961,7 +6360,7 @@ dependencies = [ "unicase", "util_macros", "walkdir", - "which 6.0.3", + "which", ] [[package]] @@ -17986,16 +6385,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "uuid-simd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" -dependencies = [ - "outref", - "vsimd", -] - [[package]] name = "v_frame" version = "0.3.9" @@ -18007,12 +6396,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - [[package]] name = "value-bag" version = "1.11.1" @@ -18055,16 +6438,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "vercel" -version = "0.1.0" -dependencies = [ - "anyhow", - "schemars", - "serde", - "strum 0.27.2", -] - [[package]] name = "version-compare" version = "0.2.0" @@ -18077,81 +6450,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "vim" -version = "0.1.0" -dependencies = [ - "anyhow", - "assets", - "async-compat", - "async-trait", - "collections", - "command_palette", - "command_palette_hooks", - "db", - "editor", - "env_logger 0.11.8", - "futures 0.3.31", - "fuzzy", - "git_ui", - "gpui", - "indoc", - "itertools 0.14.0", - "language", - "log", - "lsp", - "markdown_preview", - "menu", - "multi_buffer", - "nvim-rs", - "parking_lot", - "perf", - "picker", - "project", - "project_panel", - "regex", - "release_channel", - "schemars", - "search", - "semver", - "serde", - "serde_json", - "settings", - "settings_ui", - "task", - "text", - "theme", - "tokio", - "ui", - "util", - "util_macros", - "vim_mode_setting", - "workspace", - "zed_actions", -] - -[[package]] -name = "vim_mode_setting" -version = "0.1.0" -dependencies = [ - "settings", -] - -[[package]] -name = "vscode_theme" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b3666211944f2e6ba2c359bc9efc1891157e910b1b11c3900892ea9f18179d2" -dependencies = [ - "serde", -] - -[[package]] -name = "vsimd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" - [[package]] name = "vswhom" version = "0.1.0" @@ -18172,20 +6470,6 @@ dependencies = [ "libc", ] -[[package]] -name = "vte" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5924018406ce0063cd67f8e008104968b74b563ee1b85dde3ed1f7cb87d3dbd" -dependencies = [ - "arrayvec", - "bitflags 2.9.4", - "cursor-icon", - "log", - "memchr", - "serde", -] - [[package]] name = "waker-fn" version = "1.2.0" @@ -18211,34 +6495,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "warp" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4378d202ff965b011c64817db11d5829506d3404edeadb61f190d111da3f231c" -dependencies = [ - "bytes 1.10.1", - "futures-channel", - "futures-util", - "headers", - "http 0.2.12", - "hyper 0.14.32", - "log", - "mime", - "mime_guess", - "percent-encoding", - "pin-project", - "scoped-tls", - "serde", - "serde_json", - "serde_urlencoded", - "tokio", - "tokio-tungstenite 0.21.0", - "tokio-util", - "tower-service", - "tracing", -] - [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -18254,12 +6510,6 @@ dependencies = [ "wit-bindgen 0.46.0", ] -[[package]] -name = "wasite" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" - [[package]] name = "wasm-bindgen" version = "0.2.104" @@ -18332,25 +6582,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "wasm-encoder" -version = "0.201.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9c7d2731df60006819b013f64ccc2019691deccf6e11a1804bc850cd6748f1a" -dependencies = [ - "leb128", -] - -[[package]] -name = "wasm-encoder" -version = "0.221.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc8444fe4920de80a4fe5ab564fff2ae58b6b73166b89751f8c6c93509da32e5" -dependencies = [ - "leb128", - "wasmparser 0.221.3", -] - [[package]] name = "wasm-encoder" version = "0.227.1" @@ -18358,23 +6589,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80bb72f02e7fbf07183443b27b0f3d4144abf8c114189f2e088ed95b696a7822" dependencies = [ "leb128fmt", - "wasmparser 0.227.1", -] - -[[package]] -name = "wasm-metadata" -version = "0.201.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fd83062c17b9f4985d438603cde0a5e8c5c8198201a6937f778b607924c7da2" -dependencies = [ - "anyhow", - "indexmap", - "serde", - "serde_derive", - "serde_json", - "spdx", - "wasm-encoder 0.201.0", - "wasmparser 0.201.0", + "wasmparser", ] [[package]] @@ -18392,8 +6607,8 @@ dependencies = [ "serde_json", "spdx", "url", - "wasm-encoder 0.227.1", - "wasmparser 0.227.1", + "wasm-encoder", + "wasmparser", ] [[package]] @@ -18409,30 +6624,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "wasmparser" -version = "0.201.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84e5df6dba6c0d7fafc63a450f1738451ed7a0b52295d83e868218fa286bf708" -dependencies = [ - "bitflags 2.9.4", - "indexmap", - "semver", -] - -[[package]] -name = "wasmparser" -version = "0.221.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d06bfa36ab3ac2be0dee563380147a5b81ba10dd8885d7fbbc9eb574be67d185" -dependencies = [ - "bitflags 2.9.4", - "hashbrown 0.15.5", - "indexmap", - "semver", - "serde", -] - [[package]] name = "wasmparser" version = "0.227.1" @@ -18445,318 +6636,17 @@ dependencies = [ "semver", ] -[[package]] -name = "wasmprinter" -version = "0.221.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7343c42a97f2926c7819ff81b64012092ae954c5d83ddd30c9fcdefd97d0b283" -dependencies = [ - "anyhow", - "termcolor", - "wasmparser 0.221.3", -] - -[[package]] -name = "wasmtime" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11976a250672556d1c4c04c6d5d7656ac9192ac9edc42a4587d6c21460010e69" -dependencies = [ - "anyhow", - "async-trait", - "bitflags 2.9.4", - "bumpalo", - "cc", - "cfg-if", - "encoding_rs", - "hashbrown 0.14.5", - "indexmap", - "libc", - "log", - "mach2 0.4.3", - "memfd", - "object 0.36.7", - "once_cell", - "paste", - "postcard", - "psm", - "pulley-interpreter", - "rayon", - "rustix 0.38.44", - "semver", - "serde", - "serde_derive", - "smallvec", - "sptr", - "target-lexicon 0.13.3", - "trait-variant", - "wasmparser 0.221.3", - "wasmtime-asm-macros", - "wasmtime-component-macro", - "wasmtime-component-util", - "wasmtime-cranelift", - "wasmtime-environ", - "wasmtime-fiber", - "wasmtime-jit-icache-coherence", - "wasmtime-math", - "wasmtime-slab", - "wasmtime-versioned-export-macros", - "wasmtime-winch", - "windows-sys 0.59.0", -] - -[[package]] -name = "wasmtime-asm-macros" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f178b0d125201fbe9f75beaf849bd3e511891f9e45ba216a5b620802ccf64f2" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "wasmtime-c-api-impl" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea30cef3608f2de5797c7bbb94c1ba4f3676d9a7f81ae86ced1b512e2766ed0c" -dependencies = [ - "anyhow", - "log", - "tracing", - "wasmtime", - "wasmtime-c-api-macros", -] - -[[package]] -name = "wasmtime-c-api-macros" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "022a79ebe1124d5d384d82463d7e61c6b4dd857d81f15cb8078974eeb86db65b" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "wasmtime-component-macro" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d74de6592ed945d0a602f71243982a304d5d02f1e501b638addf57f42d57dfaf" -dependencies = [ - "anyhow", - "proc-macro2", - "quote", - "syn 2.0.106", - "wasmtime-component-util", - "wasmtime-wit-bindgen", - "wit-parser 0.221.3", -] - -[[package]] -name = "wasmtime-component-util" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707dc7b3c112ab5a366b30cfe2fb5b2f8e6a0f682f16df96a5ec582bfe6f056e" - -[[package]] -name = "wasmtime-cranelift" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "366be722674d4bf153290fbcbc4d7d16895cc82fb3e869f8d550ff768f9e9e87" -dependencies = [ - "anyhow", - "cfg-if", - "cranelift-codegen", - "cranelift-control", - "cranelift-entity", - "cranelift-frontend", - "cranelift-native", - "gimli 0.31.1", - "itertools 0.12.1", - "log", - "object 0.36.7", - "smallvec", - "target-lexicon 0.13.3", - "thiserror 1.0.69", - "wasmparser 0.221.3", - "wasmtime-environ", - "wasmtime-versioned-export-macros", -] - -[[package]] -name = "wasmtime-environ" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdadc1af7097347aa276a4f008929810f726b5b46946971c660b6d421e9994ad" -dependencies = [ - "anyhow", - "cpp_demangle", - "cranelift-bitset", - "cranelift-entity", - "gimli 0.31.1", - "indexmap", - "log", - "object 0.36.7", - "postcard", - "rustc-demangle", - "semver", - "serde", - "serde_derive", - "smallvec", - "target-lexicon 0.13.3", - "wasm-encoder 0.221.3", - "wasmparser 0.221.3", - "wasmprinter", - "wasmtime-component-util", -] - -[[package]] -name = "wasmtime-fiber" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccba90d4119f081bca91190485650730a617be1fff5228f8c4757ce133d21117" -dependencies = [ - "anyhow", - "cc", - "cfg-if", - "rustix 0.38.44", - "wasmtime-asm-macros", - "wasmtime-versioned-export-macros", - "windows-sys 0.59.0", -] - -[[package]] -name = "wasmtime-jit-icache-coherence" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec5e8552e01692e6c2e5293171704fed8abdec79d1a6995a0870ab190e5747d1" -dependencies = [ - "anyhow", - "cfg-if", - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "wasmtime-math" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29210ec2aa25e00f4d54605cedaf080f39ec01a872c5bd520ad04c67af1dde17" -dependencies = [ - "libm", -] - -[[package]] -name = "wasmtime-slab" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb5821a96fa04ac14bc7b158bb3d5cd7729a053db5a74dad396cd513a5e5ccf" - -[[package]] -name = "wasmtime-versioned-export-macros" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ff86db216dc0240462de40c8290887a613dddf9685508eb39479037ba97b5b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - -[[package]] -name = "wasmtime-wasi" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d1be69bfcab1bdac74daa7a1f9695ab992b9c8e21b9b061e7d66434097e0ca4" -dependencies = [ - "anyhow", - "async-trait", - "bitflags 2.9.4", - "bytes 1.10.1", - "cap-fs-ext", - "cap-net-ext", - "cap-rand", - "cap-std", - "cap-time-ext", - "fs-set-times", - "futures 0.3.31", - "io-extras", - "io-lifetimes", - "rustix 0.38.44", - "system-interface", - "thiserror 1.0.69", - "tokio", - "tracing", - "trait-variant", - "url", - "wasmtime", - "wiggle", - "windows-sys 0.59.0", -] - -[[package]] -name = "wasmtime-winch" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdbabfb8f20502d5e1d81092b9ead3682ae59988487aafcd7567387b7a43cf8f" -dependencies = [ - "anyhow", - "cranelift-codegen", - "gimli 0.31.1", - "object 0.36.7", - "target-lexicon 0.13.3", - "wasmparser 0.221.3", - "wasmtime-cranelift", - "wasmtime-environ", - "winch-codegen", -] - -[[package]] -name = "wasmtime-wit-bindgen" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8358319c2dd1e4db79e3c1c5d3a5af84956615343f9f89f4e4996a36816e06e6" -dependencies = [ - "anyhow", - "heck 0.5.0", - "indexmap", - "wit-parser 0.221.3", -] - -[[package]] -name = "wast" -version = "35.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ef140f1b49946586078353a453a1d28ba90adfc54dde75710bc1931de204d68" -dependencies = [ - "leb128", -] - [[package]] name = "watch" version = "0.1.0" dependencies = [ "ctor", - "futures 0.3.31", + "env_logger", + "futures", "gpui", + "log", "parking_lot", "rand 0.9.2", - "zlog", -] - -[[package]] -name = "wax" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d12a78aa0bab22d2f26ed1a96df7ab58e8a93506a3e20adb47c51a93b4e1357" -dependencies = [ - "const_format", - "itertools 0.11.0", - "nom 7.1.3", - "pori", - "regex", - "thiserror 1.0.69", - "walkdir", ] [[package]] @@ -18889,45 +6779,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "web_atoms" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ffde1dc01240bdf9992e3205668b235e59421fd085e8a317ed98da0178d414" -dependencies = [ - "phf 0.11.3", - "phf_codegen", - "string_cache", - "string_cache_codegen", -] - -[[package]] -name = "web_search" -version = "0.1.0" -dependencies = [ - "anyhow", - "cloud_llm_client", - "collections", - "gpui", - "serde", -] - -[[package]] -name = "web_search_providers" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "cloud_llm_client", - "futures 0.3.31", - "gpui", - "http_client", - "language_model", - "serde", - "serde_json", - "web_search", -] - [[package]] name = "webpki-root-certs" version = "0.26.8" @@ -18946,50 +6797,12 @@ dependencies = [ "rustls-pki-types", ] -[[package]] -name = "webrtc-sys" -version = "0.3.7" -source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=5f04705ac3f356350ae31534ffbc476abc9ea83d#5f04705ac3f356350ae31534ffbc476abc9ea83d" -dependencies = [ - "cc", - "cxx", - "cxx-build", - "glob", - "log", - "webrtc-sys-build", -] - -[[package]] -name = "webrtc-sys-build" -version = "0.3.6" -source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=5f04705ac3f356350ae31534ffbc476abc9ea83d#5f04705ac3f356350ae31534ffbc476abc9ea83d" -dependencies = [ - "fs2", - "regex", - "reqwest 0.11.27", - "scratch", - "semver", - "zip 0.6.6", -] - [[package]] name = "weezl" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.44", -] - [[package]] name = "which" version = "6.0.3" @@ -19002,58 +6815,6 @@ dependencies = [ "winsafe", ] -[[package]] -name = "whoami" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" -dependencies = [ - "libredox", - "wasite", -] - -[[package]] -name = "wiggle" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b9af35bc9629c52c261465320a9a07959164928b4241980ba1cf923b9e6751d" -dependencies = [ - "anyhow", - "async-trait", - "bitflags 2.9.4", - "thiserror 1.0.69", - "tracing", - "wasmtime", - "wiggle-macro", -] - -[[package]] -name = "wiggle-generate" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cf267dd05673912c8138f4b54acabe6bd53407d9d1536f0fadb6520dd16e101" -dependencies = [ - "anyhow", - "heck 0.5.0", - "proc-macro2", - "quote", - "shellexpand 2.1.2", - "syn 2.0.106", - "witx", -] - -[[package]] -name = "wiggle-macro" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c5c473d4198e6c2d377f3809f713ff0c110cab88a0805ae099a82119ee250c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", - "wiggle-generate", -] - [[package]] name = "winapi" version = "0.3.9" @@ -19085,34 +6846,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "winch-codegen" -version = "29.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f849ef2c5f46cb0a20af4b4487aaa239846e52e2c03f13fa3c784684552859c" -dependencies = [ - "anyhow", - "cranelift-codegen", - "gimli 0.31.1", - "regalloc2", - "smallvec", - "target-lexicon 0.13.3", - "thiserror 1.0.69", - "wasmparser 0.221.3", - "wasmtime-cranelift", - "wasmtime-environ", -] - -[[package]] -name = "windows" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" -dependencies = [ - "windows-core 0.54.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows" version = "0.57.0" @@ -19123,16 +6856,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" -dependencies = [ - "windows-core 0.58.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows" version = "0.61.3" @@ -19148,11 +6871,10 @@ dependencies = [ [[package]] name = "windows-capture" -version = "1.4.3" -source = "git+https://github.com/zed-industries/windows-capture.git?rev=f0d6c1b6691db75461b732f6d5ff56eed002eeb9#f0d6c1b6691db75461b732f6d5ff56eed002eeb9" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a4df73e95feddb9ec1a7e9c2ca6323b8c97d5eeeff78d28f1eccdf19c882b24" dependencies = [ - "clap", - "ctrlc", "parking_lot", "rayon", "thiserror 2.0.17", @@ -19169,16 +6891,6 @@ dependencies = [ "windows-core 0.61.2", ] -[[package]] -name = "windows-core" -version = "0.54.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" -dependencies = [ - "windows-result 0.1.2", - "windows-targets 0.52.6", -] - [[package]] name = "windows-core" version = "0.57.0" @@ -19191,19 +6903,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-core" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" -dependencies = [ - "windows-implement 0.58.0", - "windows-interface 0.58.0", - "windows-result 0.2.0", - "windows-strings 0.1.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows-core" version = "0.61.2" @@ -19252,17 +6951,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "windows-implement" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "windows-implement" version = "0.60.2" @@ -19285,17 +6973,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "windows-interface" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", -] - [[package]] name = "windows-interface" version = "0.59.3" @@ -19351,17 +7028,6 @@ dependencies = [ "windows-strings 0.4.2", ] -[[package]] -name = "windows-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - [[package]] name = "windows-result" version = "0.1.2" @@ -19371,15 +7037,6 @@ dependencies = [ "windows-targets 0.52.6", ] -[[package]] -name = "windows-result" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-result" version = "0.3.4" @@ -19398,16 +7055,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "windows-strings" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" -dependencies = [ - "windows-result 0.2.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows-strings" version = "0.3.1" @@ -19750,25 +7397,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "winreg" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" -dependencies = [ - "winapi", -] - -[[package]] -name = "winreg" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - [[package]] name = "winreg" version = "0.55.0" @@ -19779,32 +7407,12 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "winresource" -version = "0.1.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edcacf11b6f48dd21b9ba002f991bdd5de29b2da8cc2800412f4b80f677e4957" -dependencies = [ - "toml 0.8.23", - "version_check", -] - [[package]] name = "winsafe" version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" -[[package]] -name = "winx" -version = "0.36.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d" -dependencies = [ - "bitflags 2.9.4", - "windows-sys 0.59.0", -] - [[package]] name = "wio" version = "0.2.2" @@ -19814,25 +7422,14 @@ dependencies = [ "winapi", ] -[[package]] -name = "wit-bindgen" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "288f992ea30e6b5c531b52cdd5f3be81c148554b09ea416f058d16556ba92c27" -dependencies = [ - "bitflags 2.9.4", - "wit-bindgen-rt 0.22.0", - "wit-bindgen-rust-macro 0.22.0", -] - [[package]] name = "wit-bindgen" version = "0.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "10fb6648689b3929d56bbc7eb1acf70c9a42a29eb5358c67c10f54dbd5d695de" dependencies = [ - "wit-bindgen-rt 0.41.0", - "wit-bindgen-rust-macro 0.41.0", + "wit-bindgen-rt", + "wit-bindgen-rust-macro", ] [[package]] @@ -19841,16 +7438,6 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" -[[package]] -name = "wit-bindgen-core" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e85e72719ffbccf279359ad071497e47eb0675fe22106dea4ed2d8a7fcb60ba4" -dependencies = [ - "anyhow", - "wit-parser 0.201.0", -] - [[package]] name = "wit-bindgen-core" version = "0.41.0" @@ -19859,15 +7446,9 @@ checksum = "92fa781d4f2ff6d3f27f3cc9b74a73327b31ca0dc4a3ef25a0ce2983e0e5af9b" dependencies = [ "anyhow", "heck 0.5.0", - "wit-parser 0.227.1", + "wit-parser", ] -[[package]] -name = "wit-bindgen-rt" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb8738270f32a2d6739973cbbb7c1b6dd8959ce515578a6e19165853272ee64" - [[package]] name = "wit-bindgen-rt" version = "0.41.0" @@ -19875,24 +7456,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4db52a11d4dfb0a59f194c064055794ee6564eb1ced88c25da2cf76e50c5621" dependencies = [ "bitflags 2.9.4", - "futures 0.3.31", + "futures", "once_cell", ] -[[package]] -name = "wit-bindgen-rust" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a39a15d1ae2077688213611209849cad40e9e5cccf6e61951a425850677ff3" -dependencies = [ - "anyhow", - "heck 0.4.1", - "indexmap", - "wasm-metadata 0.201.0", - "wit-bindgen-core 0.22.0", - "wit-component 0.201.0", -] - [[package]] name = "wit-bindgen-rust" version = "0.41.0" @@ -19904,23 +7471,9 @@ dependencies = [ "indexmap", "prettyplease", "syn 2.0.106", - "wasm-metadata 0.227.1", - "wit-bindgen-core 0.41.0", - "wit-component 0.227.1", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d376d3ae5850526dfd00d937faea0d81a06fa18f7ac1e26f386d760f241a8f4b" -dependencies = [ - "anyhow", - "proc-macro2", - "quote", - "syn 2.0.106", - "wit-bindgen-core 0.22.0", - "wit-bindgen-rust 0.22.0", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", ] [[package]] @@ -19934,27 +7487,8 @@ dependencies = [ "proc-macro2", "quote", "syn 2.0.106", - "wit-bindgen-core 0.41.0", - "wit-bindgen-rust 0.41.0", -] - -[[package]] -name = "wit-component" -version = "0.201.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "421c0c848a0660a8c22e2fd217929a0191f14476b68962afd2af89fd22e39825" -dependencies = [ - "anyhow", - "bitflags 2.9.4", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder 0.201.0", - "wasm-metadata 0.201.0", - "wasmparser 0.201.0", - "wit-parser 0.201.0", + "wit-bindgen-core", + "wit-bindgen-rust", ] [[package]] @@ -19970,46 +7504,10 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "wasm-encoder 0.227.1", - "wasm-metadata 0.227.1", - "wasmparser 0.227.1", - "wit-parser 0.227.1", -] - -[[package]] -name = "wit-parser" -version = "0.201.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "196d3ecfc4b759a8573bf86a9b3f8996b304b3732e4c7de81655f875f6efdca6" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser 0.201.0", -] - -[[package]] -name = "wit-parser" -version = "0.221.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "896112579ed56b4a538b07a3d16e562d101ff6265c46b515ce0c701eef16b2ac" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser 0.221.3", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", ] [[package]] @@ -20027,102 +7525,7 @@ dependencies = [ "serde_derive", "serde_json", "unicode-xid", - "wasmparser 0.227.1", -] - -[[package]] -name = "witx" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e366f27a5cabcddb2706a78296a40b8fcc451e1a6aba2fc1d94b4a01bdaaef4b" -dependencies = [ - "anyhow", - "log", - "thiserror 1.0.69", - "wast", -] - -[[package]] -name = "workspace" -version = "0.1.0" -dependencies = [ - "any_vec", - "anyhow", - "async-recursion", - "call", - "client", - "clock", - "collections", - "component", - "dap", - "db", - "fs", - "futures 0.3.31", - "gpui", - "http_client", - "itertools 0.14.0", - "language", - "log", - "menu", - "node_runtime", - "parking_lot", - "postage", - "pretty_assertions", - "project", - "remote", - "schemars", - "serde", - "serde_json", - "session", - "settings", - "smallvec", - "sqlez", - "strum 0.27.2", - "task", - "telemetry", - "tempfile", - "theme", - "ui", - "util", - "uuid", - "windows 0.61.3", - "zed_actions", - "zlog", -] - -[[package]] -name = "worktree" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-lock 2.8.0", - "clock", - "collections", - "fs", - "futures 0.3.31", - "fuzzy", - "git", - "git2", - "gpui", - "http_client", - "ignore", - "language", - "log", - "parking_lot", - "paths", - "postage", - "pretty_assertions", - "rand 0.9.2", - "rpc", - "serde", - "serde_json", - "settings", - "smallvec", - "smol", - "sum_tree", - "text", - "util", - "zlog", + "wasmparser", ] [[package]] @@ -20131,15 +7534,6 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - [[package]] name = "x11" version = "2.21.0" @@ -20180,16 +7574,6 @@ version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" -[[package]] -name = "x_ai" -version = "0.1.0" -dependencies = [ - "anyhow", - "schemars", - "serde", - "strum 0.27.2", -] - [[package]] name = "xattr" version = "0.2.3" @@ -20259,50 +7643,15 @@ checksum = "9bbb26405d8e919bc1547a5aa9abc95cbfa438f04844f5fdd9dc7596b748bf69" dependencies = [ "log", "mac", - "markup5ever 0.12.1", + "markup5ever", ] -[[package]] -name = "xmlparser" -version = "0.13.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" - [[package]] name = "xmlwriter" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" -[[package]] -name = "xtask" -version = "0.1.0" -dependencies = [ - "anyhow", - "backtrace", - "cargo_metadata", - "cargo_toml", - "clap", - "gh-workflow", - "indexmap", - "indoc", - "serde", - "serde_json", - "toml 0.8.23", - "toml_edit 0.22.27", -] - -[[package]] -name = "yaml-rust2" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8" -dependencies = [ - "arraydeque", - "encoding_rs", - "hashlink 0.8.4", -] - [[package]] name = "yansi" version = "1.0.1" @@ -20315,12 +7664,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19a5d82922135b4ae73a079a4ffb5501e9aadb4d785b8c660eaa0a8b899028c5" dependencies = [ - "base64 0.22.1", - "bytes 1.10.1", + "base64", + "bytes", "flate2", - "futures 0.3.31", + "futures", "http-body-util", - "hyper 1.7.0", + "hyper", "hyper-util", "js-sys", "nom 8.0.0", @@ -20329,7 +7678,7 @@ dependencies = [ "sha1", "thiserror 1.0.69", "tokio", - "tokio-rustls 0.26.2", + "tokio-rustls", "tokio-util", "url", "wasm-bindgen", @@ -20355,18 +7704,6 @@ dependencies = [ "pkg-config", ] -[[package]] -name = "yoke" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive 0.7.5", - "zerofrom", -] - [[package]] name = "yoke" version = "0.8.0" @@ -20375,22 +7712,10 @@ checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ "serde", "stable_deref_trait", - "yoke-derive 0.8.0", + "yoke-derive", "zerofrom", ] -[[package]] -name = "yoke-derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.106", - "synstructure", -] - [[package]] name = "yoke-derive" version = "0.8.0" @@ -20412,7 +7737,7 @@ dependencies = [ "async-broadcast", "async-executor", "async-io", - "async-lock 3.4.1", + "async-lock", "async-process", "async-recursion", "async-task", @@ -20464,161 +7789,6 @@ dependencies = [ "zvariant", ] -[[package]] -name = "zed" -version = "0.218.0" -dependencies = [ - "acp_tools", - "activity_indicator", - "agent_settings", - "agent_ui", - "anyhow", - "ashpd 0.11.0", - "askpass", - "assets", - "audio", - "auto_update", - "auto_update_ui", - "bincode", - "breadcrumbs", - "call", - "channel", - "chrono", - "clap", - "cli", - "client", - "codestral", - "collab_ui", - "collections", - "command_palette", - "component", - "copilot", - "crashes", - "dap", - "dap_adapters", - "db", - "debug_adapter_extension", - "debugger_tools", - "debugger_ui", - "diagnostics", - "edit_prediction", - "edit_prediction_ui", - "editor", - "env_logger 0.11.8", - "extension", - "extension_host", - "extensions_ui", - "feature_flags", - "feedback", - "file_finder", - "fs", - "futures 0.3.31", - "git", - "git_hosting_providers", - "git_ui", - "go_to_line", - "gpui", - "gpui_tokio", - "http_client", - "image_viewer", - "inspector_ui", - "install_cli", - "itertools 0.14.0", - "journal", - "json_schema_store", - "keymap_editor", - "language", - "language_extension", - "language_model", - "language_models", - "language_onboarding", - "language_selector", - "language_tools", - "languages", - "line_ending_selector", - "log", - "markdown", - "markdown_preview", - "menu", - "migrator", - "mimalloc", - "miniprofiler_ui", - "nc", - "node_runtime", - "notifications", - "onboarding", - "outline", - "outline_panel", - "parking_lot", - "paths", - "picker", - "pretty_assertions", - "profiling", - "project", - "project_panel", - "project_symbols", - "prompt_store", - "proto", - "rayon", - "recent_projects", - "release_channel", - "remote", - "repl", - "reqwest_client", - "rope", - "search", - "semver", - "serde", - "serde_json", - "session", - "settings", - "settings_profile_selector", - "settings_ui", - "shellexpand 2.1.2", - "smol", - "snippet_provider", - "snippets_ui", - "supermaven", - "svg_preview", - "sysinfo 0.37.2", - "system_specs", - "tab_switcher", - "task", - "tasks_ui", - "telemetry", - "terminal_view", - "theme", - "theme_extension", - "theme_selector", - "time", - "title_bar", - "toolchain_selector", - "tracing", - "tree-sitter-md", - "tree-sitter-rust", - "ui", - "ui_input", - "ui_prompt", - "url", - "urlencoding", - "util", - "uuid", - "vim", - "vim_mode_setting", - "watch", - "web_search", - "web_search_providers", - "windows 0.61.3", - "winresource", - "workspace", - "zed-reqwest", - "zed_actions", - "zed_env_vars", - "zlog", - "zlog_settings", - "ztracing", -] - [[package]] name = "zed-font-kit" version = "0.14.1-zed" @@ -20648,17 +7818,17 @@ name = "zed-reqwest" version = "0.12.15-zed" source = "git+https://github.com/zed-industries/reqwest.git?rev=c15662463bda39148ba154100dd44d3fba5873a4#c15662463bda39148ba154100dd44d3fba5873a4" dependencies = [ - "base64 0.22.1", - "bytes 1.10.1", + "base64", + "bytes", "encoding_rs", "futures-core", "futures-util", - "h2 0.4.12", - "http 1.3.1", - "http-body 1.0.1", + "h2", + "http", + "http-body", "http-body-util", - "hyper 1.7.0", - "hyper-rustls 0.27.7", + "hyper", + "hyper-rustls", "hyper-util", "ipnet", "js-sys", @@ -20669,20 +7839,20 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.33", - "rustls-native-certs 0.8.2", - "rustls-pemfile 2.2.0", + "rustls", + "rustls-native-certs", + "rustls-pemfile", "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 1.0.2", - "system-configuration 0.6.1", + "sync_wrapper", + "system-configuration", "tokio", - "tokio-rustls 0.26.2", + "tokio-rustls", "tokio-socks", "tokio-util", - "tower 0.5.2", + "tower", "tower-service", "url", "wasm-bindgen", @@ -20705,7 +7875,7 @@ dependencies = [ "rand 0.8.5", "screencapturekit", "screencapturekit-sys", - "sysinfo 0.31.4", + "sysinfo", "tao-core-video-sys", "windows 0.61.3", "windows-capture", @@ -20718,7 +7888,7 @@ name = "zed-xim" version = "0.4.0-zed" source = "git+https://github.com/zed-industries/xim-rs.git?rev=16f35a2c881b815a2b6cdfd6687988e84f8447d8#16f35a2c881b815a2b6cdfd6687988e84f8447d8" dependencies = [ - "ahash 0.8.12", + "ahash", "hashbrown 0.14.5", "log", "x11rb", @@ -20726,45 +7896,6 @@ dependencies = [ "xim-parser", ] -[[package]] -name = "zed_actions" -version = "0.1.0" -dependencies = [ - "gpui", - "schemars", - "serde", - "uuid", -] - -[[package]] -name = "zed_env_vars" -version = "0.1.0" -dependencies = [ - "gpui", -] - -[[package]] -name = "zed_extension_api" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "594fd10dd0f2f853eb243e2425e7c95938cef49adb81d9602921d002c5e6d9d9" -dependencies = [ - "serde", - "serde_json", - "wit-bindgen 0.22.0", -] - -[[package]] -name = "zed_extension_api" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0729d50b4ca0a7e28e590bbe32e3ca0194d97ef654961451a424c661a366fca0" -dependencies = [ - "serde", - "serde_json", - "wit-bindgen 0.41.0", -] - [[package]] name = "zed_extension_api" version = "0.8.0" @@ -20774,34 +7905,6 @@ dependencies = [ "wit-bindgen 0.41.0", ] -[[package]] -name = "zed_glsl" -version = "0.1.0" -dependencies = [ - "zed_extension_api 0.1.0", -] - -[[package]] -name = "zed_html" -version = "0.2.3" -dependencies = [ - "zed_extension_api 0.7.0", -] - -[[package]] -name = "zed_proto" -version = "0.2.3" -dependencies = [ - "zed_extension_api 0.1.0", -] - -[[package]] -name = "zed_test_extension" -version = "0.1.0" -dependencies = [ - "zed_extension_api 0.8.0", -] - [[package]] name = "zeno" version = "0.3.3" @@ -20869,30 +7972,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "zeromq" -version = "0.5.0-pre" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1fe92954d37e77bed5e2775cb0fed7dba5f6bc4be6f7f76172a4eb371dc6a9b" -dependencies = [ - "async-dispatcher", - "async-std", - "async-trait", - "asynchronous-codec", - "bytes 1.10.1", - "crossbeam-queue", - "dashmap 5.5.3", - "futures 0.3.31", - "log", - "num-traits", - "once_cell", - "parking_lot", - "rand 0.8.5", - "regex", - "thiserror 1.0.69", - "uuid", -] - [[package]] name = "zerotrie" version = "0.2.2" @@ -20900,7 +7979,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" dependencies = [ "displaydoc", - "yoke 0.8.0", + "yoke", "zerofrom", ] @@ -20910,7 +7989,7 @@ version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ - "yoke 0.8.0", + "yoke", "zerofrom", "zerovec-derive", ] @@ -20926,113 +8005,6 @@ dependencies = [ "syn 2.0.106", ] -[[package]] -name = "zeta_prompt" -version = "0.1.0" -dependencies = [ - "serde", -] - -[[package]] -name = "zip" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" -dependencies = [ - "aes", - "byteorder", - "bzip2", - "constant_time_eq", - "crc32fast", - "crossbeam-utils", - "flate2", - "hmac", - "pbkdf2 0.11.0", - "sha1", - "time", - "zstd", -] - -[[package]] -name = "zip" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cc23c04387f4da0374be4533ad1208cbb091d5c11d070dfef13676ad6497164" -dependencies = [ - "arbitrary", - "crc32fast", - "crossbeam-utils", - "displaydoc", - "indexmap", - "num_enum", - "thiserror 1.0.69", -] - -[[package]] -name = "zlog" -version = "0.1.0" -dependencies = [ - "anyhow", - "chrono", - "collections", - "log", - "tempfile", -] - -[[package]] -name = "zlog_settings" -version = "0.1.0" -dependencies = [ - "collections", - "gpui", - "settings", - "zlog", -] - -[[package]] -name = "zstd" -version = "0.11.2+zstd.1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "5.0.2+zstd.1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" -dependencies = [ - "libc", - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" -dependencies = [ - "cc", - "pkg-config", -] - -[[package]] -name = "ztracing" -version = "0.1.0" -dependencies = [ - "tracing", - "tracing-subscriber", - "tracing-tracy", - "zlog", - "ztracing_macro", -] - -[[package]] -name = "ztracing_macro" -version = "0.1.0" - [[package]] name = "zune-core" version = "0.4.12" @@ -21097,3 +8069,13 @@ dependencies = [ "syn 2.0.106", "winnow", ] + +[[patch.unused]] +name = "notify" +version = "8.2.0" +source = "git+https://github.com/zed-industries/notify.git?rev=b4588b2e5aee68f4c0e100f140e808cbce7b1419#b4588b2e5aee68f4c0e100f140e808cbce7b1419" + +[[patch.unused]] +name = "notify-types" +version = "2.0.0" +source = "git+https://github.com/zed-industries/notify.git?rev=b4588b2e5aee68f4c0e100f140e808cbce7b1419#b4588b2e5aee68f4c0e100f140e808cbce7b1419" diff --git a/Cargo.toml b/Cargo.toml index 523dce229e..c1979c63e2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,230 +1,28 @@ [workspace] resolver = "2" members = [ - "crates/acp_tools", - "crates/acp_thread", - "crates/action_log", - "crates/activity_indicator", - "crates/agent", - "crates/agent_servers", - "crates/agent_settings", - "crates/agent_ui", - "crates/ai_onboarding", - "crates/anthropic", - "crates/askpass", - "crates/assets", - "crates/assistant_text_thread", - "crates/assistant_slash_command", - "crates/assistant_slash_commands", - "crates/audio", - "crates/auto_update", - "crates/auto_update_helper", - "crates/auto_update_ui", - "crates/aws_http_client", - "crates/bedrock", - "crates/breadcrumbs", - "crates/buffer_diff", - "crates/call", - "crates/channel", - "crates/cli", - "crates/client", - "crates/clock", "crates/cloud_api_client", "crates/cloud_api_types", "crates/cloud_llm_client", - "crates/collab", - "crates/collab_ui", "crates/collections", - "crates/command_palette", - "crates/command_palette_hooks", - "crates/component", - "crates/context_server", - "crates/copilot", - "crates/crashes", - "crates/credentials_provider", - "crates/dap", - "crates/dap_adapters", - "crates/db", - "crates/debug_adapter_extension", - "crates/debugger_tools", - "crates/debugger_ui", - "crates/deepseek", - "crates/denoise", - "crates/diagnostics", - "crates/docs_preprocessor", - "crates/edit_prediction", - "crates/edit_prediction_types", - "crates/edit_prediction_ui", - "crates/edit_prediction_context", - "crates/editor", - "crates/eval", - "crates/eval_utils", - "crates/explorer_command_injector", - "crates/extension", "crates/extension_api", - "crates/extension_cli", - "crates/extension_host", - "crates/extensions_ui", - "crates/feature_flags", - "crates/feedback", - "crates/file_finder", - "crates/file_icons", - "crates/fs", - "crates/fs_benchmarks", - "crates/fsevent", - "crates/fuzzy", - "crates/git", - "crates/git_hosting_providers", - "crates/git_ui", - "crates/go_to_line", - "crates/google_ai", "crates/gpui", "crates/gpui_macros", "crates/gpui_tokio", "crates/html_to_markdown", "crates/http_client", "crates/http_client_tls", - "crates/icons", - "crates/image_viewer", - "crates/inspector_ui", - "crates/install_cli", - "crates/journal", - "crates/json_schema_store", - "crates/keymap_editor", - "crates/language", - "crates/language_extension", - "crates/language_model", - "crates/language_models", - "crates/language_onboarding", - "crates/language_selector", - "crates/language_tools", - "crates/languages", - "crates/line_ending_selector", - "crates/livekit_api", - "crates/livekit_client", - "crates/lmstudio", - "crates/lsp", - "crates/markdown", - "crates/markdown_preview", "crates/media", - "crates/menu", - "crates/migrator", - "crates/mistral", - "crates/miniprofiler_ui", - "crates/multi_buffer", - "crates/nc", - "crates/net", - "crates/node_runtime", - "crates/notifications", - "crates/ollama", - "crates/onboarding", - "crates/open_ai", - "crates/open_router", - "crates/outline", - "crates/outline_panel", - "crates/panel", - "crates/paths", - "crates/picker", - "crates/prettier", - "crates/project", - "crates/project_benchmarks", - "crates/project_panel", - "crates/project_symbols", - "crates/prompt_store", - "crates/proto", - "crates/recent_projects", "crates/refineable", "crates/refineable/derive_refineable", - "crates/release_channel", - "crates/scheduler", - "crates/remote", - "crates/remote_server", - "crates/repl", "crates/reqwest_client", - "crates/rich_text", - "crates/rope", - "crates/rpc", - "crates/rules_library", - "crates/schema_generator", - "crates/search", - "crates/session", - "crates/settings", - "crates/settings_json", - "crates/settings_macros", - "crates/settings_profile_selector", - "crates/settings_ui", - "crates/snippet", - "crates/snippet_provider", - "crates/snippets_ui", - "crates/sqlez", - "crates/sqlez_macros", - "crates/story", - "crates/storybook", - "crates/streaming_diff", + "crates/scheduler", "crates/sum_tree", - "crates/supermaven", - "crates/supermaven_api", - "crates/codestral", - "crates/svg_preview", - "crates/system_specs", - "crates/tab_switcher", - "crates/task", - "crates/tasks_ui", - "crates/telemetry", - "crates/telemetry_events", - "crates/terminal", - "crates/terminal_view", - "crates/text", - "crates/theme", - "crates/theme_extension", - "crates/theme_importer", - "crates/theme_selector", - "crates/time_format", - "crates/title_bar", - "crates/toolchain_selector", - "crates/ui", - "crates/ui_input", - "crates/ui_macros", - "crates/ui_prompt", "crates/util", "crates/util_macros", - "crates/vercel", - "crates/vim", - "crates/vim_mode_setting", "crates/watch", - "crates/web_search", - "crates/web_search_providers", - "crates/workspace", - "crates/worktree", - "crates/x_ai", - "crates/zed", - "crates/zed_actions", - "crates/zed_env_vars", - "crates/edit_prediction_cli", - "crates/zeta_prompt", - "crates/zlog", - "crates/zlog_settings", - "crates/ztracing", - "crates/ztracing_macro", - - # - # Extensions - # - - "extensions/glsl", - "extensions/html", - "extensions/proto", - "extensions/slash-commands-example", - "extensions/test-extension", - - # - # Tooling - # - "tooling/perf", - "tooling/xtask", ] -default-members = ["crates/zed"] [workspace.package] publish = false @@ -236,207 +34,33 @@ 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" } -activity_indicator = { path = "crates/activity_indicator" } -agent_ui = { path = "crates/agent_ui" } -agent_settings = { path = "crates/agent_settings" } -agent_servers = { path = "crates/agent_servers" } -ai_onboarding = { path = "crates/ai_onboarding" } -anthropic = { path = "crates/anthropic" } -askpass = { path = "crates/askpass" } -assets = { path = "crates/assets" } -assistant_text_thread = { path = "crates/assistant_text_thread" } -assistant_slash_command = { path = "crates/assistant_slash_command" } -assistant_slash_commands = { path = "crates/assistant_slash_commands" } -audio = { path = "crates/audio" } -auto_update = { path = "crates/auto_update" } -auto_update_ui = { path = "crates/auto_update_ui" } -aws_http_client = { path = "crates/aws_http_client" } -bedrock = { path = "crates/bedrock" } -breadcrumbs = { path = "crates/breadcrumbs" } -buffer_diff = { path = "crates/buffer_diff" } -call = { path = "crates/call" } -channel = { path = "crates/channel" } -cli = { path = "crates/cli" } -client = { path = "crates/client" } -clock = { path = "crates/clock" } cloud_api_client = { path = "crates/cloud_api_client" } cloud_api_types = { path = "crates/cloud_api_types" } cloud_llm_client = { path = "crates/cloud_llm_client" } -collab_ui = { path = "crates/collab_ui" } collections = { path = "crates/collections", version = "0.1.0" } -command_palette = { path = "crates/command_palette" } -command_palette_hooks = { path = "crates/command_palette_hooks" } -component = { path = "crates/component" } -context_server = { path = "crates/context_server" } -copilot = { path = "crates/copilot" } -crashes = { path = "crates/crashes" } -credentials_provider = { path = "crates/credentials_provider" } -crossbeam = "0.8.4" -dap = { path = "crates/dap" } -dap_adapters = { path = "crates/dap_adapters" } -db = { path = "crates/db" } -debug_adapter_extension = { path = "crates/debug_adapter_extension" } -debugger_tools = { path = "crates/debugger_tools" } -debugger_ui = { path = "crates/debugger_ui" } -deepseek = { path = "crates/deepseek" } derive_refineable = { path = "crates/refineable/derive_refineable" } -diagnostics = { path = "crates/diagnostics" } -editor = { path = "crates/editor" } -eval_utils = { path = "crates/eval_utils" } -extension = { path = "crates/extension" } -extension_host = { path = "crates/extension_host" } -extensions_ui = { path = "crates/extensions_ui" } -feature_flags = { path = "crates/feature_flags" } -feedback = { path = "crates/feedback" } -file_finder = { path = "crates/file_finder" } -file_icons = { path = "crates/file_icons" } -fs = { path = "crates/fs" } -fsevent = { path = "crates/fsevent" } -fuzzy = { path = "crates/fuzzy" } -git = { path = "crates/git" } -git_hosting_providers = { path = "crates/git_hosting_providers" } -git_ui = { path = "crates/git_ui" } -go_to_line = { path = "crates/go_to_line" } -google_ai = { path = "crates/google_ai" } gpui = { path = "crates/gpui", default-features = false } gpui_macros = { path = "crates/gpui_macros" } gpui_tokio = { path = "crates/gpui_tokio" } html_to_markdown = { path = "crates/html_to_markdown" } http_client = { path = "crates/http_client" } http_client_tls = { path = "crates/http_client_tls" } -icons = { path = "crates/icons" } -image_viewer = { path = "crates/image_viewer" } -edit_prediction_types = { path = "crates/edit_prediction_types" } -edit_prediction_ui = { path = "crates/edit_prediction_ui" } -edit_prediction_context = { path = "crates/edit_prediction_context" } -inspector_ui = { path = "crates/inspector_ui" } -install_cli = { path = "crates/install_cli" } -journal = { path = "crates/journal" } -json_schema_store = { path = "crates/json_schema_store" } -keymap_editor = { path = "crates/keymap_editor" } -language = { path = "crates/language" } -language_extension = { path = "crates/language_extension" } -language_model = { path = "crates/language_model" } -language_models = { path = "crates/language_models" } -language_onboarding = { path = "crates/language_onboarding" } -language_selector = { path = "crates/language_selector" } -language_tools = { path = "crates/language_tools" } -languages = { path = "crates/languages" } -line_ending_selector = { path = "crates/line_ending_selector" } -livekit_api = { path = "crates/livekit_api" } -livekit_client = { path = "crates/livekit_client" } -lmstudio = { path = "crates/lmstudio" } -lsp = { path = "crates/lsp" } -markdown = { path = "crates/markdown" } -markdown_preview = { path = "crates/markdown_preview" } -svg_preview = { path = "crates/svg_preview" } media = { path = "crates/media" } -menu = { path = "crates/menu" } -migrator = { path = "crates/migrator" } -mistral = { path = "crates/mistral" } -multi_buffer = { path = "crates/multi_buffer" } -miniprofiler_ui = { path = "crates/miniprofiler_ui" } -nc = { path = "crates/nc" } -net = { path = "crates/net" } -node_runtime = { path = "crates/node_runtime" } -notifications = { path = "crates/notifications" } -ollama = { path = "crates/ollama" } -onboarding = { path = "crates/onboarding" } -open_ai = { path = "crates/open_ai" } -open_router = { path = "crates/open_router", features = ["schemars"] } -outline = { path = "crates/outline" } -outline_panel = { path = "crates/outline_panel" } -panel = { path = "crates/panel" } -paths = { path = "crates/paths" } -perf = { path = "tooling/perf" } -picker = { path = "crates/picker" } -prettier = { path = "crates/prettier" } -settings_profile_selector = { path = "crates/settings_profile_selector" } -project = { path = "crates/project" } -project_panel = { path = "crates/project_panel" } -project_symbols = { path = "crates/project_symbols" } -prompt_store = { path = "crates/prompt_store" } -proto = { path = "crates/proto" } -recent_projects = { path = "crates/recent_projects" } refineable = { path = "crates/refineable" } -release_channel = { path = "crates/release_channel" } -remote = { path = "crates/remote" } -remote_server = { path = "crates/remote_server" } -repl = { path = "crates/repl" } reqwest_client = { path = "crates/reqwest_client" } -rodio = { git = "https://github.com/RustAudio/rodio", rev ="e2074c6c2acf07b57cf717e076bdda7a9ac6e70b", features = ["wav", "playback", "wav_output", "recording"] } -rope = { path = "crates/rope" } -rpc = { path = "crates/rpc" } -rules_library = { path = "crates/rules_library" } -search = { path = "crates/search" } -session = { path = "crates/session" } -settings = { path = "crates/settings" } -settings_json = { path = "crates/settings_json" } -settings_macros = { path = "crates/settings_macros" } -settings_ui = { path = "crates/settings_ui" } -snippet = { path = "crates/snippet" } -snippet_provider = { path = "crates/snippet_provider" } -snippets_ui = { path = "crates/snippets_ui" } -sqlez = { path = "crates/sqlez" } -sqlez_macros = { path = "crates/sqlez_macros" } -story = { path = "crates/story" } -streaming_diff = { path = "crates/streaming_diff" } +scheduler = { path = "crates/scheduler" } sum_tree = { path = "crates/sum_tree" } -supermaven = { path = "crates/supermaven" } -supermaven_api = { path = "crates/supermaven_api" } -codestral = { path = "crates/codestral" } -system_specs = { path = "crates/system_specs" } -tab_switcher = { path = "crates/tab_switcher" } -task = { path = "crates/task" } -tasks_ui = { path = "crates/tasks_ui" } -telemetry = { path = "crates/telemetry" } -telemetry_events = { path = "crates/telemetry_events" } -terminal = { path = "crates/terminal" } -terminal_view = { path = "crates/terminal_view" } -text = { path = "crates/text" } -theme = { path = "crates/theme" } -theme_extension = { path = "crates/theme_extension" } -theme_selector = { path = "crates/theme_selector" } -time_format = { path = "crates/time_format" } -title_bar = { path = "crates/title_bar" } -toolchain_selector = { path = "crates/toolchain_selector" } -ui = { path = "crates/ui" } -ui_input = { path = "crates/ui_input" } -ui_macros = { path = "crates/ui_macros" } -ui_prompt = { path = "crates/ui_prompt" } util = { path = "crates/util" } util_macros = { path = "crates/util_macros" } -vercel = { path = "crates/vercel" } -vim = { path = "crates/vim" } -vim_mode_setting = { path = "crates/vim_mode_setting" } - watch = { path = "crates/watch" } -web_search = { path = "crates/web_search" } -web_search_providers = { path = "crates/web_search_providers" } -workspace = { path = "crates/workspace" } -worktree = { path = "crates/worktree" } -x_ai = { path = "crates/x_ai" } -zed = { path = "crates/zed" } -zed_actions = { path = "crates/zed_actions" } -zed_env_vars = { path = "crates/zed_env_vars" } -edit_prediction = { path = "crates/edit_prediction" } -zeta_prompt = { path = "crates/zeta_prompt" } -zlog = { path = "crates/zlog" } -zlog_settings = { path = "crates/zlog_settings" } -ztracing = { path = "crates/ztracing" } -ztracing_macro = { path = "crates/ztracing_macro" } +perf = { path = "tooling/perf" } # # External crates # -agent-client-protocol = { version = "=0.9.0", features = ["unstable"] } aho-corasick = "1.1" -alacritty_terminal = "0.25.1-rc1" +circular-buffer = "1.0" any_vec = "0.14" anyhow = "1.0.86" arrayvec = { version = "0.7.4", features = ["serde"] } @@ -446,22 +70,12 @@ async-compression = { version = "0.4", features = ["gzip", "futures-io"] } async-dispatcher = "0.1" async-fs = "2.1" async-lock = "2.1" -async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" } async-recursion = "1.0.0" async-tar = "0.5.1" async-task = "4.7" async-trait = "0.1" async-tungstenite = "0.31.0" async_zip = { version = "0.0.18", features = ["deflate", "deflate64"] } -aws-config = { version = "1.6.1", features = ["behavior-version-latest"] } -aws-credential-types = { version = "1.2.2", features = [ - "hardcoded-credentials", -] } -aws-sdk-bedrockruntime = { version = "1.80.0", features = [ - "behavior-version-latest", -] } -aws-smithy-runtime-api = { version = "1.7.4", features = ["http-1x", "client"] } -aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] } backtrace = "0.3" base64 = "0.22" bincode = "1.2.1" @@ -471,76 +85,43 @@ blade-macros = { version = "0.3.0" } blade-util = { version = "0.3.0" } brotli = "8.0.2" bytes = "1.0" -cargo_metadata = "0.19" -cargo_toml = "0.21" cfg-if = "1.0.3" chrono = { version = "0.4", features = ["serde"] } -ciborium = "0.2" -circular-buffer = "1.0" -clap = { version = "4.4", features = ["derive", "wrap_help"] } cocoa = "=0.26.0" cocoa-foundation = "=0.2.0" convert_case = "0.8.0" core-foundation = "=0.10.0" core-foundation-sys = "0.8.6" core-video = { version = "0.4.3", features = ["metal"] } -cpal = "0.16" -crash-handler = "0.6" -criterion = { version = "0.5", features = ["html_reports"] } +crossbeam = "0.8.4" ctor = "0.4.0" -dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "1b461b310481d01e02b2603c16d7144b926339f8" } dashmap = "6.0" derive_more = "0.99.17" dirs = "4.0" -documented = "0.9.1" -dotenvy = "0.15.0" -ec4rs = "1.1" -emojis = "0.6.1" env_logger = "0.11" -exec = "0.3.1" -fancy-regex = "0.16.0" -fork = "0.4.0" futures = "0.3" futures-lite = "1.13" -gh-workflow = { git = "https://github.com/zed-industries/gh-workflow", rev = "09acfdf2bd5c1d6254abefd609c808ff73547b2c" } git2 = { version = "0.20.1", default-features = false } globset = "0.4" -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" +indoc = "2" +inventory = "0.3.19" http = "1.1" http-body = "1.0" hyper = "0.14" -ignore = "0.4.22" image = "0.25.1" -imara-diff = "0.1.8" indexmap = { version = "2.7.0", features = ["serde"] } -indoc = "2" -inventory = "0.3.19" itertools = "0.14.0" -json_dotpath = "1.1" -jsonschema = "0.37.0" -jsonwebtoken = "9.3" -jupyter-protocol = "0.10.0" -jupyter-websocket-client = "0.15.0" libc = "0.2" -libsqlite3-sys = { version = "0.30.1", features = ["bundled"] } linkify = "0.10.0" log = { version = "0.4.16", features = ["kv_unstable_serde", "serde"] } -lsp-types = { git = "https://github.com/zed-industries/lsp-types", rev = "b71ab4eeb27d9758be8092020a46fe33fbca4e33" } mach2 = "0.5" markup5ever_rcdom = "0.3.0" metal = "0.29" -minidumper = "0.8" -moka = { version = "0.12.10", features = ["sync"] } naga = { version = "25.0", features = ["wgsl-in"] } -nanoid = "0.4" -nbformat = "0.15.0" nix = "0.29" -num-format = "0.4.4" objc = "0.2" objc2-foundation = { version = "=0.3.1", default-features = false, features = [ "NSArray", @@ -571,25 +152,11 @@ open = "5.0.0" ordered-float = "2.1.1" 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 = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" } -pet-conda = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" } -pet-core = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" } -pet-fs = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" } -pet-poetry = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" } -pet-reporter = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" } -pet-virtualenv = { git = "https://github.com/microsoft/python-environment-tools.git", rev = "1e86914c3ce2f3a08c0cedbcb0615a7f9fa7a5da" } -portable-pty = "0.9.0" postage = { version = "0.5", features = ["futures-traits"] } pretty_assertions = { version = "1.3.0", features = ["unstable"] } proc-macro2 = "1.0.93" profiling = "1" -prost = "0.9" -prost-build = "0.9" -prost-types = "0.9" pulldown-cmark = { version = "0.12.0", default-features = false } quote = "1.0.9" rand = "0.9" @@ -605,10 +172,6 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "c15662 "socks", "stream", ], package = "zed-reqwest", version = "0.12.15-zed" } -rsa = "0.9.6" -runtimelib = { version = "0.30.0", default-features = false, features = [ - "async-dispatcher-runtime", "aws-lc-rs" -] } rust-embed = { version = "8.4", features = ["include-exclude"] } rustc-hash = "2.1.0" rustls = { version = "0.23.26" } @@ -623,19 +186,14 @@ serde_json_lenient = { version = "0.2", features = [ "preserve_order", "raw_value", ] } -serde_path_to_error = "0.1.17" -serde_repr = "0.1" serde_urlencoded = "0.7" sha2 = "0.10" -shellexpand = "2.1.0" shlex = "1.3.0" -simplelog = "0.12.2" +shellexpand = "2.1.0" slotmap = "1.0.6" smallvec = { version = "1.6", features = ["union", "const_new"] } smol = "2.0" -sqlformat = "0.2" stacksafe = "0.1" -streaming-iterator = "0.1" strsim = "0.11" strum = { version = "0.27.2", features = ["derive"] } subtle = "2.5.0" @@ -645,7 +203,6 @@ sysinfo = "0.37.0" take-until = "0.2.0" tempfile = "3.20.0" thiserror = "2.0.12" -tiktoken-rs = { git = "https://github.com/zed-industries/tiktoken-rs", rev = "2570c4387a8505fb8f1d3f3557454b474f1e8271" } time = { version = "0.3", features = [ "macros", "parsing", @@ -654,64 +211,20 @@ time = { version = "0.3", features = [ "formatting", "local-offset", ] } -tiny_http = "0.8" tokio = { version = "1" } tokio-tungstenite = { version = "0.26", features = ["__rustls-tls"] } tokio-socks = { version = "0.5.2", default-features = false, features = ["futures-io", "tokio"] } -toml = "0.8" -toml_edit = { version = "0.22", default-features = false, features = ["display", "parse", "serde"] } -tower-http = "0.4.4" -tree-sitter = { version = "0.25.10", features = ["wasm"] } -tree-sitter-bash = "0.25.1" -tree-sitter-c = "0.23" -tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "5cb9b693cfd7bfacab1d9ff4acac1a4150700609" } -tree-sitter-css = "0.23" -tree-sitter-diff = "0.1.0" -tree-sitter-elixir = "0.3" -tree-sitter-embedded-template = "0.23.0" -tree-sitter-gitcommit = { git = "https://github.com/zed-industries/tree-sitter-git-commit", rev = "88309716a69dd13ab83443721ba6e0b491d37ee9" } -tree-sitter-go = "0.23" -tree-sitter-go-mod = { git = "https://github.com/camdencheek/tree-sitter-go-mod", rev = "2e886870578eeba1927a2dc4bd2e2b3f598c5f9a", package = "tree-sitter-gomod" } -tree-sitter-gowork = { git = "https://github.com/zed-industries/tree-sitter-go-work", rev = "acb0617bf7f4fda02c6217676cc64acb89536dc7" } -tree-sitter-heex = { git = "https://github.com/zed-industries/tree-sitter-heex", rev = "1dd45142fbb05562e35b2040c6129c9bca346592" } -tree-sitter-html = "0.23" -tree-sitter-jsdoc = "0.23" -tree-sitter-json = "0.24" -tree-sitter-md = { git = "https://github.com/tree-sitter-grammars/tree-sitter-markdown", rev = "9a23c1a96c0513d8fc6520972beedd419a973539" } -tree-sitter-python = "0.25" -tree-sitter-regex = "0.24" -tree-sitter-ruby = "0.23" -tree-sitter-rust = "0.24" -tree-sitter-typescript = { git = "https://github.com/zed-industries/tree-sitter-typescript", rev = "e2c53597d6a5d9cf7bbe8dccde576fe1e46c5899" } # https://github.com/tree-sitter/tree-sitter-typescript/pull/347 -tree-sitter-yaml = { git = "https://github.com/zed-industries/tree-sitter-yaml", rev = "baff0b51c64ef6a1fb1f8390f3ad6015b83ec13a" } tracing = "0.1.40" unicase = "2.6" -unicode-script = "0.5.7" unicode-segmentation = "1.10" -unindent = "0.2.0" url = "2.2" -urlencoding = "2.1.2" uuid = { version = "1.1.2", features = ["v4", "v5", "v7", "serde"] } walkdir = "2.5" -wasm-encoder = "0.221" -wasmparser = "0.221" -wasmtime = { version = "29", default-features = false, features = [ - "async", - "demangle", - "runtime", - "cranelift", - "component-model", - "incremental-cache", - "parallel-compilation", -] } -wasmtime-wasi = "29" -wax = "0.6" which = "6.0.0" windows-core = "0.61" yawc = "0.2.5" zeroize = "1.8" -zstd = "0.11" - +ciborium = "0.2" [workspace.dependencies.windows] version = "0.61" @@ -767,7 +280,6 @@ features = [ [patch.crates-io] notify = { git = "https://github.com/zed-industries/notify.git", rev = "b4588b2e5aee68f4c0e100f140e808cbce7b1419" } notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "b4588b2e5aee68f4c0e100f140e808cbce7b1419" } -windows-capture = { git = "https://github.com/zed-industries/windows-capture.git", rev = "f0d6c1b6691db75461b732f6d5ff56eed002eeb9" } calloop = { git = "https://github.com/zed-industries/calloop" } [profile.dev] @@ -785,9 +297,6 @@ codegen-units = 16 # proc-macros start gpui_macros = { opt-level = 3 } derive_refineable = { opt-level = 3 } -settings_macros = { opt-level = 3 } -sqlez_macros = { opt-level = 3, codegen-units = 1 } -ui_macros = { opt-level = 3 } util_macros = { opt-level = 3 } quote = { opt-level = 3 } syn = { opt-level = 3 } @@ -796,52 +305,16 @@ proc-macro2 = { opt-level = 3 } taffy = { opt-level = 3 } resvg = { opt-level = 3 } -wasmtime = { opt-level = 3 } -# Build single-source-file crates with cg=1 as it helps make `cargo build` of a whole workspace a bit faster -activity_indicator = { codegen-units = 1 } -assets = { codegen-units = 1 } -breadcrumbs = { codegen-units = 1 } + collections = { codegen-units = 1 } -command_palette = { codegen-units = 1 } -command_palette_hooks = { codegen-units = 1 } -feature_flags = { codegen-units = 1 } -file_icons = { codegen-units = 1 } -fsevent = { codegen-units = 1 } -image_viewer = { codegen-units = 1 } -edit_prediction_ui = { codegen-units = 1 } -install_cli = { codegen-units = 1 } -journal = { codegen-units = 1 } -json_schema_store = { codegen-units = 1 } -lmstudio = { codegen-units = 1 } -menu = { codegen-units = 1 } -notifications = { codegen-units = 1 } -ollama = { codegen-units = 1 } -outline = { codegen-units = 1 } -paths = { codegen-units = 1 } -prettier = { codegen-units = 1 } -project_symbols = { codegen-units = 1 } refineable = { codegen-units = 1 } -release_channel = { codegen-units = 1 } reqwest_client = { codegen-units = 1 } -session = { codegen-units = 1 } -snippet = { codegen-units = 1 } -snippets_ui = { codegen-units = 1 } -story = { codegen-units = 1 } -supermaven_api = { codegen-units = 1 } -telemetry_events = { codegen-units = 1 } -theme_selector = { codegen-units = 1 } -time_format = { codegen-units = 1 } -ui_input = { codegen-units = 1 } -zed_actions = { codegen-units = 1 } [profile.release] debug = "limited" lto = "thin" codegen-units = 1 -[profile.release.package] -zed = { codegen-units = 16 } - [profile.release-fast] inherits = "release" debug = "full" @@ -897,9 +370,5 @@ nonminimal_bool = "allow" ignored = [ "bindgen", "cbindgen", - "prost_build", "serde", - "component", - "documented", - "sea-orm-macros", -] +] \ No newline at end of file diff --git a/Dockerfile-collab b/Dockerfile-collab deleted file mode 100644 index 68f898618a..0000000000 --- a/Dockerfile-collab +++ /dev/null @@ -1,37 +0,0 @@ -# syntax = docker/dockerfile:1.2 - -FROM rust:1.91.1-bookworm as builder -WORKDIR app -COPY . . - -# Replace the Cargo configuration with the one used by collab. -COPY ./.cargo/collab-config.toml ./.cargo/config.toml - -# Compile collab server -ARG CARGO_PROFILE_RELEASE_PANIC=abort -ARG GITHUB_SHA - -ENV GITHUB_SHA=$GITHUB_SHA - -# Also add `cmake`, since we need it to build `wasmtime`. -RUN apt-get update; \ - apt-get install -y --no-install-recommends cmake - -RUN --mount=type=cache,target=./script/node_modules \ - --mount=type=cache,target=/usr/local/cargo/registry \ - --mount=type=cache,target=/usr/local/cargo/git \ - --mount=type=cache,target=./target \ - cargo build --release --package collab --bin collab - -# Copy collab server binary out of cached directory -RUN --mount=type=cache,target=./target \ - cp /app/target/release/collab /app/collab - -# Copy collab server binary to the runtime image -FROM debian:bookworm-slim as runtime -RUN apt-get update; \ - apt-get install -y --no-install-recommends libcurl4-openssl-dev ca-certificates \ - linux-perf binutils -WORKDIR app -COPY --from=builder /app/collab /app/collab -ENTRYPOINT ["/app/collab"] diff --git a/Dockerfile-collab.dockerignore b/Dockerfile-collab.dockerignore deleted file mode 100644 index 337b4d4262..0000000000 --- a/Dockerfile-collab.dockerignore +++ /dev/null @@ -1,16 +0,0 @@ -.git -.github -**/.gitignore -**/.gitkeep -.gitattributes -.mailmap -**/target -zed.xcworkspace -.DS_Store -compose.yml -plugins/bin -script/node_modules -styles/node_modules -crates/collab/static/styles.css -vendor/bin -assets/themes/ diff --git a/Dockerfile-cross.dockerignore b/Dockerfile-cross.dockerignore deleted file mode 100644 index 337b4d4262..0000000000 --- a/Dockerfile-cross.dockerignore +++ /dev/null @@ -1,16 +0,0 @@ -.git -.github -**/.gitignore -**/.gitkeep -.gitattributes -.mailmap -**/target -zed.xcworkspace -.DS_Store -compose.yml -plugins/bin -script/node_modules -styles/node_modules -crates/collab/static/styles.css -vendor/bin -assets/themes/ diff --git a/Dockerfile-distros b/Dockerfile-distros deleted file mode 100644 index c8a98d2f7d..0000000000 --- a/Dockerfile-distros +++ /dev/null @@ -1,26 +0,0 @@ -# syntax=docker/dockerfile:1 - -ARG BASE_IMAGE -FROM ${BASE_IMAGE} -WORKDIR /app -ARG TZ=Etc/UTC \ - LANG=C.UTF-8 \ - LC_ALL=C.UTF-8 \ - DEBIAN_FRONTEND=noninteractive -ENV CARGO_TERM_COLOR=always - -COPY script/linux script/ -RUN ./script/linux -COPY script/install-mold script/install-cmake script/ -RUN ./script/install-mold "2.34.0" -RUN ./script/install-cmake "3.30.4" - -COPY . . - -# When debugging, make these into individual RUN statements. -# Cleanup to avoid saving big layers we aren't going to use. -RUN . "$HOME/.cargo/env" \ - && cargo fetch \ - && cargo build \ - && cargo run -- --help \ - && cargo clean --quiet diff --git a/Dockerfile-distros.dockerignore b/Dockerfile-distros.dockerignore deleted file mode 100644 index de70e0d167..0000000000 --- a/Dockerfile-distros.dockerignore +++ /dev/null @@ -1,2 +0,0 @@ -**/target -**/node_modules diff --git a/LICENSE-AGPL b/LICENSE-AGPL deleted file mode 100644 index 87a0dea90e..0000000000 --- a/LICENSE-AGPL +++ /dev/null @@ -1,788 +0,0 @@ -Copyright 2022 - 2025 Zed Industries, Inc. - - - - -This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. -You should have received a copy of the GNU Affero General Public License along with this program. If not, see . - - - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - - Preamble - - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - - The precise terms and conditions for copying, distribution and -modification follow. - - - TERMS AND CONDITIONS - - - 0. Definitions. - - - "This License" refers to version 3 of the GNU Affero General Public License. - - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - - A "covered work" means either the unmodified Program or a work based -on the Program. - - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - - 1. Source Code. - - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - - The Corresponding Source for a work in source code form is that -same work. - - - 2. Basic Permissions. - - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - - 4. Conveying Verbatim Copies. - - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - - 5. Conveying Modified Source Versions. - - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - - 6. Conveying Non-Source Forms. - - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - - 7. Additional Terms. - - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - - 8. Termination. - - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - - 9. Acceptance Not Required for Having Copies. - - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - - 10. Automatic Licensing of Downstream Recipients. - - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - - 11. Patents. - - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - - 12. No Surrender of Others' Freedom. - - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - - 13. Remote Network Interaction; Use with the GNU General Public License. - - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - - 14. Revised Versions of this License. - - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - - 15. Disclaimer of Warranty. - - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - - 16. Limitation of Liability. - - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - - 17. Interpretation of Sections 15 and 16. - - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - - END OF TERMS AND CONDITIONS - - - How to Apply These Terms to Your New Programs - - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - - Copyright (C) - - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - - -Also add information on how to contact you by electronic and paper mail. - - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. diff --git a/LICENSE-GPL b/LICENSE-GPL deleted file mode 100644 index cb82534a8d..0000000000 --- a/LICENSE-GPL +++ /dev/null @@ -1,200 +0,0 @@ -GNU GENERAL PUBLIC LICENSE -Version 3, 29 June 2007 - -Copyright © 2007 Free Software Foundation, Inc. - -Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. - -Preamble - -The GNU General Public License is a free, copyleft license for software and other kinds of works. - -The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. - -When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. - -To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. - -For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. - -Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. - -For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. - -Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. - -Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. - -The precise terms and conditions for copying, distribution and modification follow. - -TERMS AND CONDITIONS - -0. Definitions. -"This License" refers to version 3 of the GNU General Public License. - -"Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. - -"The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. - -To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. - -A "covered work" means either the unmodified Program or a work based on the Program. - -To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. - -To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. - -An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. - -1. Source Code. -The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. -A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. - -The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. - -The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. - -The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. - -The Corresponding Source for a work in source code form is that same work. - -2. Basic Permissions. -All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. -You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. - -Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. - -3. Protecting Users' Legal Rights From Anti-Circumvention Law. -No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. -When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. - -4. Conveying Verbatim Copies. -You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. -You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. - -5. Conveying Modified Source Versions. -You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: -a) The work must carry prominent notices stating that you modified it, and giving a relevant date. -b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". -c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. -d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. -A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. - -6. Conveying Non-Source Forms. -You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: -a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. -b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. -c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. -d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. -e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. -A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. - -A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. - -"Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. - -If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). - -The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. - -Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. - -7. Additional Terms. -"Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. -When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. - -Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: - -a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or -b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or -c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or -d) Limiting the use for publicity purposes of names of licensors or authors of the material; or -e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or -f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. -All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. - -If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. - -Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. - -8. Termination. -You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). -However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. - -Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. - -Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. - -9. Acceptance Not Required for Having Copies. -You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. -10. Automatic Licensing of Downstream Recipients. -Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. -An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. - -You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. - -11. Patents. -A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". -A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. - -Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. - -In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. - -If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. - -If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. - -A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. - -Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. - -12. No Surrender of Others' Freedom. -If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. -13. Use with the GNU Affero General Public License. -Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. -14. Revised Versions of this License. -The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. -Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. - -If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. - -Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. - -15. Disclaimer of Warranty. -THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. -16. Limitation of Liability. -IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. -17. Interpretation of Sections 15 and 16. -If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. - -END OF TERMS AND CONDITIONS - -How to Apply These Terms to Your New Programs - -If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. - -To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. - - -Copyright (C) - -This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - -This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - -You should have received a copy of the GNU General Public License along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - -If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: - - Copyright (C) -This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. -This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". - -You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . - -The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . diff --git a/Procfile b/Procfile deleted file mode 100644 index b3f13f66a6..0000000000 --- a/Procfile +++ /dev/null @@ -1,4 +0,0 @@ -collab: RUST_LOG=${RUST_LOG:-info} cargo run --package=collab serve all -cloud: cd ../cloud; cargo make dev -livekit: livekit-server --dev -blob_store: ./script/run-local-minio diff --git a/Procfile.web b/Procfile.web deleted file mode 100644 index 63190fc2ee..0000000000 --- a/Procfile.web +++ /dev/null @@ -1 +0,0 @@ -website: cd ../zed.dev; npm run dev -- --port=3000 diff --git a/compose.yml b/compose.yml deleted file mode 100644 index cee63e968b..0000000000 --- a/compose.yml +++ /dev/null @@ -1,43 +0,0 @@ -services: - postgres: - image: docker.io/library/postgres:15 - container_name: zed_postgres - ports: - - 5432:5432 - environment: - POSTGRES_HOST_AUTH_METHOD: trust - volumes: - - postgres_data:/var/lib/postgresql/data - - ./docker-compose.sql:/docker-entrypoint-initdb.d/init.sql - - blob_store: - image: quay.io/minio/minio - container_name: blob_store - command: server /data - ports: - - 9000:9000 - environment: - MINIO_ROOT_USER: the-blob-store-access-key - MINIO_ROOT_PASSWORD: the-blob-store-secret-key - volumes: - - ./.blob_store:/data - - livekit_server: - image: docker.io/livekit/livekit-server - container_name: livekit_server - entrypoint: /livekit-server --config /livekit.yaml - ports: - - 7880:7880 - - 7881:7881 - - 7882:7882/udp - volumes: - - ./livekit.yaml:/livekit.yaml - - stripe-mock: - image: docker.io/stripe/stripe-mock:v0.178.0 - ports: - - 12111:12111 - - 12112:12112 - -volumes: - postgres_data: diff --git a/crates/acp_thread/Cargo.toml b/crates/acp_thread/Cargo.toml deleted file mode 100644 index 8ef6f1a52c..0000000000 --- a/crates/acp_thread/Cargo.toml +++ /dev/null @@ -1,60 +0,0 @@ -[package] -name = "acp_thread" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/acp_thread.rs" -doctest = false - -[features] -test-support = ["gpui/test-support", "project/test-support", "dep:parking_lot"] - -[dependencies] -action_log.workspace = true -agent-client-protocol.workspace = true -agent_settings.workspace = true -anyhow.workspace = true -buffer_diff.workspace = true -collections.workspace = true -editor.workspace = true -file_icons.workspace = true -futures.workspace = true -gpui.workspace = true -itertools.workspace = true -language.workspace = true -language_model.workspace = true -markdown.workspace = true -parking_lot = { workspace = true, optional = true } -portable-pty.workspace = true -project.workspace = true -prompt_store.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -smol.workspace = true -task.workspace = true -telemetry.workspace = true -terminal.workspace = true -ui.workspace = true -url.workspace = true -util.workspace = true -uuid.workspace = true -watch.workspace = true - -[dev-dependencies] -env_logger.workspace = true -gpui = { workspace = true, "features" = ["test-support"] } -indoc.workspace = true -parking_lot.workspace = true -project = { workspace = true, "features" = ["test-support"] } -rand.workspace = true -tempfile.workspace = true -util.workspace = true -settings.workspace = true -zlog.workspace = true diff --git a/crates/acp_thread/LICENSE-GPL b/crates/acp_thread/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/acp_thread/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs deleted file mode 100644 index 53294a963d..0000000000 --- a/crates/acp_thread/src/acp_thread.rs +++ /dev/null @@ -1,4021 +0,0 @@ -mod connection; -mod diff; -mod mention; -mod terminal; - -use agent_settings::AgentSettings; -use collections::HashSet; -pub use connection::*; -pub use diff::*; -use language::language_settings::FormatOnSave; -pub use mention::*; -use project::lsp_store::{FormatTrigger, LspFormatTarget}; -use serde::{Deserialize, Serialize}; -use settings::Settings as _; -use task::{Shell, ShellBuilder}; -pub use terminal::*; - -use action_log::{ActionLog, ActionLogTelemetry}; -use agent_client_protocol::{self as acp}; -use anyhow::{Context as _, Result, anyhow}; -use editor::Bias; -use futures::{FutureExt, channel::oneshot, future::BoxFuture}; -use gpui::{AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity}; -use itertools::Itertools; -use language::{Anchor, Buffer, BufferSnapshot, LanguageRegistry, Point, ToPoint, text_diff}; -use markdown::Markdown; -use project::{AgentLocation, Project, git_store::GitStoreCheckpoint}; -use std::collections::HashMap; -use std::error::Error; -use std::fmt::{Formatter, Write}; -use std::ops::Range; -use std::process::ExitStatus; -use std::rc::Rc; -use std::time::{Duration, Instant}; -use std::{fmt::Display, mem, path::PathBuf, sync::Arc}; -use ui::App; -use util::{ResultExt, get_default_system_shell_preferring_bash, paths::PathStyle}; -use uuid::Uuid; - -#[derive(Debug)] -pub struct UserMessage { - pub id: Option, - pub content: ContentBlock, - pub chunks: Vec, - pub checkpoint: Option, -} - -#[derive(Debug)] -pub struct Checkpoint { - git_checkpoint: GitStoreCheckpoint, - pub show: bool, -} - -impl UserMessage { - fn to_markdown(&self, cx: &App) -> String { - let mut markdown = String::new(); - if self - .checkpoint - .as_ref() - .is_some_and(|checkpoint| checkpoint.show) - { - writeln!(markdown, "## User (checkpoint)").unwrap(); - } else { - writeln!(markdown, "## User").unwrap(); - } - writeln!(markdown).unwrap(); - writeln!(markdown, "{}", self.content.to_markdown(cx)).unwrap(); - writeln!(markdown).unwrap(); - markdown - } -} - -#[derive(Debug, PartialEq)] -pub struct AssistantMessage { - pub chunks: Vec, -} - -impl AssistantMessage { - pub fn to_markdown(&self, cx: &App) -> String { - format!( - "## Assistant\n\n{}\n\n", - self.chunks - .iter() - .map(|chunk| chunk.to_markdown(cx)) - .join("\n\n") - ) - } -} - -#[derive(Debug, PartialEq)] -pub enum AssistantMessageChunk { - Message { block: ContentBlock }, - Thought { block: ContentBlock }, -} - -impl AssistantMessageChunk { - pub fn from_str( - chunk: &str, - language_registry: &Arc, - path_style: PathStyle, - cx: &mut App, - ) -> Self { - Self::Message { - block: ContentBlock::new(chunk.into(), language_registry, path_style, cx), - } - } - - fn to_markdown(&self, cx: &App) -> String { - match self { - Self::Message { block } => block.to_markdown(cx).to_string(), - Self::Thought { block } => { - format!("\n{}\n", block.to_markdown(cx)) - } - } - } -} - -#[derive(Debug)] -pub enum AgentThreadEntry { - UserMessage(UserMessage), - AssistantMessage(AssistantMessage), - ToolCall(ToolCall), -} - -impl AgentThreadEntry { - pub fn to_markdown(&self, cx: &App) -> String { - match self { - Self::UserMessage(message) => message.to_markdown(cx), - Self::AssistantMessage(message) => message.to_markdown(cx), - Self::ToolCall(tool_call) => tool_call.to_markdown(cx), - } - } - - pub fn user_message(&self) -> Option<&UserMessage> { - if let AgentThreadEntry::UserMessage(message) = self { - Some(message) - } else { - None - } - } - - pub fn diffs(&self) -> impl Iterator> { - if let AgentThreadEntry::ToolCall(call) = self { - itertools::Either::Left(call.diffs()) - } else { - itertools::Either::Right(std::iter::empty()) - } - } - - pub fn terminals(&self) -> impl Iterator> { - if let AgentThreadEntry::ToolCall(call) = self { - itertools::Either::Left(call.terminals()) - } else { - itertools::Either::Right(std::iter::empty()) - } - } - - pub fn location(&self, ix: usize) -> Option<(acp::ToolCallLocation, AgentLocation)> { - if let AgentThreadEntry::ToolCall(ToolCall { - locations, - resolved_locations, - .. - }) = self - { - Some(( - locations.get(ix)?.clone(), - resolved_locations.get(ix)?.clone()?, - )) - } else { - None - } - } -} - -#[derive(Debug)] -pub struct ToolCall { - pub id: acp::ToolCallId, - pub label: Entity, - pub kind: acp::ToolKind, - pub content: Vec, - pub status: ToolCallStatus, - pub locations: Vec, - pub resolved_locations: Vec>, - pub raw_input: Option, - pub raw_output: Option, -} - -impl ToolCall { - fn from_acp( - tool_call: acp::ToolCall, - status: ToolCallStatus, - language_registry: Arc, - path_style: PathStyle, - terminals: &HashMap>, - cx: &mut App, - ) -> Result { - let title = if let Some((first_line, _)) = tool_call.title.split_once("\n") { - first_line.to_owned() + "…" - } else { - tool_call.title - }; - let mut content = Vec::with_capacity(tool_call.content.len()); - for item in tool_call.content { - if let Some(item) = ToolCallContent::from_acp( - item, - language_registry.clone(), - path_style, - terminals, - cx, - )? { - content.push(item); - } - } - - let result = Self { - id: tool_call.tool_call_id, - label: cx - .new(|cx| Markdown::new(title.into(), Some(language_registry.clone()), None, cx)), - kind: tool_call.kind, - content, - locations: tool_call.locations, - resolved_locations: Vec::default(), - status, - raw_input: tool_call.raw_input, - raw_output: tool_call.raw_output, - }; - Ok(result) - } - - fn update_fields( - &mut self, - fields: acp::ToolCallUpdateFields, - language_registry: Arc, - path_style: PathStyle, - terminals: &HashMap>, - cx: &mut App, - ) -> Result<()> { - let acp::ToolCallUpdateFields { - kind, - status, - title, - content, - locations, - raw_input, - raw_output, - .. - } = fields; - - if let Some(kind) = kind { - self.kind = kind; - } - - if let Some(status) = status { - self.status = status.into(); - } - - 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); - } - }); - } - - if let Some(content) = content { - let mut 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()) { - let valid_content = - old.update_from_acp(new, language_registry.clone(), path_style, terminals, cx)?; - if !valid_content { - new_content_len -= 1; - } - } - for new in content { - if let Some(new) = ToolCallContent::from_acp( - new, - language_registry.clone(), - path_style, - terminals, - cx, - )? { - self.content.push(new); - } else { - new_content_len -= 1; - } - } - self.content.truncate(new_content_len); - } - - if let Some(locations) = locations { - self.locations = locations; - } - - if let Some(raw_input) = raw_input { - self.raw_input = Some(raw_input); - } - - if let Some(raw_output) = raw_output { - if self.content.is_empty() - && let Some(markdown) = markdown_for_raw_output(&raw_output, &language_registry, cx) - { - self.content - .push(ToolCallContent::ContentBlock(ContentBlock::Markdown { - markdown, - })); - } - self.raw_output = Some(raw_output); - } - Ok(()) - } - - pub fn diffs(&self) -> impl Iterator> { - self.content.iter().filter_map(|content| match content { - ToolCallContent::Diff(diff) => Some(diff), - ToolCallContent::ContentBlock(_) => None, - ToolCallContent::Terminal(_) => None, - }) - } - - pub fn terminals(&self) -> impl Iterator> { - self.content.iter().filter_map(|content| match content { - ToolCallContent::Terminal(terminal) => Some(terminal), - ToolCallContent::ContentBlock(_) => None, - ToolCallContent::Diff(_) => None, - }) - } - - fn to_markdown(&self, cx: &App) -> String { - let mut markdown = format!( - "**Tool Call: {}**\nStatus: {}\n\n", - self.label.read(cx).source(), - self.status - ); - for content in &self.content { - markdown.push_str(content.to_markdown(cx).as_str()); - markdown.push_str("\n\n"); - } - markdown - } - - async fn resolve_location( - location: acp::ToolCallLocation, - project: WeakEntity, - cx: &mut AsyncApp, - ) -> Option { - let buffer = project - .update(cx, |project, cx| { - project - .project_path_for_absolute_path(&location.path, cx) - .map(|path| project.open_buffer(path, cx)) - }) - .ok()??; - let buffer = buffer.await.log_err()?; - let position = buffer - .update(cx, |buffer, _| { - let snapshot = buffer.snapshot(); - if let Some(row) = location.line { - let column = snapshot.indent_size_for_line(row).len; - let point = snapshot.clip_point(Point::new(row, column), Bias::Left); - snapshot.anchor_before(point) - } else { - Anchor::min_for_buffer(snapshot.remote_id()) - } - }) - .ok()?; - - Some(ResolvedLocation { buffer, position }) - } - - fn resolve_locations( - &self, - project: Entity, - cx: &mut App, - ) -> Task>> { - let locations = self.locations.clone(); - project.update(cx, |_, cx| { - cx.spawn(async move |project, cx| { - let mut new_locations = Vec::new(); - for location in locations { - new_locations.push(Self::resolve_location(location, project.clone(), cx).await); - } - new_locations - }) - }) - } -} - -// Separate so we can hold a strong reference to the buffer -// for saving on the thread -#[derive(Clone, Debug, PartialEq, Eq)] -struct ResolvedLocation { - buffer: Entity, - position: Anchor, -} - -impl From<&ResolvedLocation> for AgentLocation { - fn from(value: &ResolvedLocation) -> Self { - Self { - buffer: value.buffer.downgrade(), - position: value.position, - } - } -} - -#[derive(Debug)] -pub enum ToolCallStatus { - /// The tool call hasn't started running yet, but we start showing it to - /// the user. - Pending, - /// The tool call is waiting for confirmation from the user. - WaitingForConfirmation { - options: Vec, - respond_tx: oneshot::Sender, - }, - /// The tool call is currently running. - InProgress, - /// The tool call completed successfully. - Completed, - /// The tool call failed. - Failed, - /// The user rejected the tool call. - Rejected, - /// The user canceled generation so the tool call was canceled. - Canceled, -} - -impl From for ToolCallStatus { - fn from(status: acp::ToolCallStatus) -> Self { - match status { - acp::ToolCallStatus::Pending => Self::Pending, - acp::ToolCallStatus::InProgress => Self::InProgress, - acp::ToolCallStatus::Completed => Self::Completed, - acp::ToolCallStatus::Failed => Self::Failed, - _ => Self::Pending, - } - } -} - -impl Display for ToolCallStatus { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match self { - ToolCallStatus::Pending => "Pending", - ToolCallStatus::WaitingForConfirmation { .. } => "Waiting for confirmation", - ToolCallStatus::InProgress => "In Progress", - ToolCallStatus::Completed => "Completed", - ToolCallStatus::Failed => "Failed", - ToolCallStatus::Rejected => "Rejected", - ToolCallStatus::Canceled => "Canceled", - } - ) - } -} - -#[derive(Debug, PartialEq, Clone)] -pub enum ContentBlock { - Empty, - Markdown { markdown: Entity }, - ResourceLink { resource_link: acp::ResourceLink }, -} - -impl ContentBlock { - pub fn new( - block: acp::ContentBlock, - language_registry: &Arc, - path_style: PathStyle, - cx: &mut App, - ) -> Self { - let mut this = Self::Empty; - this.append(block, language_registry, path_style, cx); - this - } - - pub fn new_combined( - blocks: impl IntoIterator, - language_registry: Arc, - path_style: PathStyle, - cx: &mut App, - ) -> Self { - let mut this = Self::Empty; - for block in blocks { - this.append(block, &language_registry, path_style, cx); - } - this - } - - pub fn append( - &mut self, - block: acp::ContentBlock, - language_registry: &Arc, - path_style: PathStyle, - cx: &mut App, - ) { - if matches!(self, ContentBlock::Empty) - && let acp::ContentBlock::ResourceLink(resource_link) = block - { - *self = ContentBlock::ResourceLink { resource_link }; - return; - } - - let new_content = self.block_string_contents(block, path_style); - - match self { - ContentBlock::Empty => { - *self = Self::create_markdown_block(new_content, language_registry, cx); - } - ContentBlock::Markdown { markdown } => { - markdown.update(cx, |markdown, cx| markdown.append(&new_content, cx)); - } - ContentBlock::ResourceLink { resource_link } => { - let existing_content = Self::resource_link_md(&resource_link.uri, path_style); - let combined = format!("{}\n{}", existing_content, new_content); - - *self = Self::create_markdown_block(combined, language_registry, cx); - } - } - } - - fn create_markdown_block( - content: String, - language_registry: &Arc, - cx: &mut App, - ) -> ContentBlock { - ContentBlock::Markdown { - markdown: cx - .new(|cx| Markdown::new(content.into(), Some(language_registry.clone()), None, cx)), - } - } - - fn block_string_contents(&self, block: acp::ContentBlock, path_style: PathStyle) -> String { - match block { - acp::ContentBlock::Text(text_content) => text_content.text, - acp::ContentBlock::ResourceLink(resource_link) => { - Self::resource_link_md(&resource_link.uri, path_style) - } - acp::ContentBlock::Resource(acp::EmbeddedResource { - resource: - acp::EmbeddedResourceResource::TextResourceContents(acp::TextResourceContents { - uri, - .. - }), - .. - }) => Self::resource_link_md(&uri, path_style), - acp::ContentBlock::Image(image) => Self::image_md(&image), - _ => String::new(), - } - } - - fn resource_link_md(uri: &str, path_style: PathStyle) -> String { - if let Some(uri) = MentionUri::parse(uri, path_style).log_err() { - uri.as_link().to_string() - } else { - uri.to_string() - } - } - - fn image_md(_image: &acp::ImageContent) -> String { - "`Image`".into() - } - - pub fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str { - match self { - ContentBlock::Empty => "", - ContentBlock::Markdown { markdown } => markdown.read(cx).source(), - ContentBlock::ResourceLink { resource_link } => &resource_link.uri, - } - } - - pub fn markdown(&self) -> Option<&Entity> { - match self { - ContentBlock::Empty => None, - ContentBlock::Markdown { markdown } => Some(markdown), - ContentBlock::ResourceLink { .. } => None, - } - } - - pub fn resource_link(&self) -> Option<&acp::ResourceLink> { - match self { - ContentBlock::ResourceLink { resource_link } => Some(resource_link), - _ => None, - } - } -} - -#[derive(Debug)] -pub enum ToolCallContent { - ContentBlock(ContentBlock), - Diff(Entity), - Terminal(Entity), -} - -impl ToolCallContent { - pub fn from_acp( - content: acp::ToolCallContent, - language_registry: Arc, - path_style: PathStyle, - terminals: &HashMap>, - cx: &mut App, - ) -> Result> { - match content { - acp::ToolCallContent::Content(acp::Content { content, .. }) => { - Ok(Some(Self::ContentBlock(ContentBlock::new( - content, - &language_registry, - path_style, - cx, - )))) - } - acp::ToolCallContent::Diff(diff) => Ok(Some(Self::Diff(cx.new(|cx| { - Diff::finalized( - diff.path.to_string_lossy().into_owned(), - diff.old_text, - diff.new_text, - language_registry, - cx, - ) - })))), - acp::ToolCallContent::Terminal(acp::Terminal { terminal_id, .. }) => terminals - .get(&terminal_id) - .cloned() - .map(|terminal| Some(Self::Terminal(terminal))) - .ok_or_else(|| anyhow::anyhow!("Terminal with id `{}` not found", terminal_id)), - _ => Ok(None), - } - } - - pub fn update_from_acp( - &mut self, - new: acp::ToolCallContent, - language_registry: Arc, - path_style: PathStyle, - terminals: &HashMap>, - cx: &mut App, - ) -> Result { - let needs_update = match (&self, &new) { - (Self::Diff(old_diff), acp::ToolCallContent::Diff(new_diff)) => { - old_diff.read(cx).needs_update( - new_diff.old_text.as_deref().unwrap_or(""), - &new_diff.new_text, - cx, - ) - } - _ => true, - }; - - if let Some(update) = Self::from_acp(new, language_registry, path_style, terminals, cx)? { - if needs_update { - *self = update; - } - Ok(true) - } else { - Ok(false) - } - } - - pub fn to_markdown(&self, cx: &App) -> String { - match self { - Self::ContentBlock(content) => content.to_markdown(cx).to_string(), - Self::Diff(diff) => diff.read(cx).to_markdown(cx), - Self::Terminal(terminal) => terminal.read(cx).to_markdown(cx), - } - } -} - -#[derive(Debug, PartialEq)] -pub enum ToolCallUpdate { - UpdateFields(acp::ToolCallUpdate), - UpdateDiff(ToolCallUpdateDiff), - UpdateTerminal(ToolCallUpdateTerminal), -} - -impl ToolCallUpdate { - fn id(&self) -> &acp::ToolCallId { - match self { - Self::UpdateFields(update) => &update.tool_call_id, - Self::UpdateDiff(diff) => &diff.id, - Self::UpdateTerminal(terminal) => &terminal.id, - } - } -} - -impl From for ToolCallUpdate { - fn from(update: acp::ToolCallUpdate) -> Self { - Self::UpdateFields(update) - } -} - -impl From for ToolCallUpdate { - fn from(diff: ToolCallUpdateDiff) -> Self { - Self::UpdateDiff(diff) - } -} - -#[derive(Debug, PartialEq)] -pub struct ToolCallUpdateDiff { - pub id: acp::ToolCallId, - pub diff: Entity, -} - -impl From for ToolCallUpdate { - fn from(terminal: ToolCallUpdateTerminal) -> Self { - Self::UpdateTerminal(terminal) - } -} - -#[derive(Debug, PartialEq)] -pub struct ToolCallUpdateTerminal { - pub id: acp::ToolCallId, - pub terminal: Entity, -} - -#[derive(Debug, Default)] -pub struct Plan { - pub entries: Vec, -} - -#[derive(Debug)] -pub struct PlanStats<'a> { - pub in_progress_entry: Option<&'a PlanEntry>, - pub pending: u32, - pub completed: u32, -} - -impl Plan { - pub fn is_empty(&self) -> bool { - self.entries.is_empty() - } - - pub fn stats(&self) -> PlanStats<'_> { - let mut stats = PlanStats { - in_progress_entry: None, - pending: 0, - completed: 0, - }; - - for entry in &self.entries { - match &entry.status { - acp::PlanEntryStatus::Pending => { - stats.pending += 1; - } - acp::PlanEntryStatus::InProgress => { - stats.in_progress_entry = stats.in_progress_entry.or(Some(entry)); - } - acp::PlanEntryStatus::Completed => { - stats.completed += 1; - } - _ => {} - } - } - - stats - } -} - -#[derive(Debug)] -pub struct PlanEntry { - pub content: Entity, - pub priority: acp::PlanEntryPriority, - pub status: acp::PlanEntryStatus, -} - -impl PlanEntry { - pub fn from_acp(entry: acp::PlanEntry, cx: &mut App) -> Self { - Self { - content: cx.new(|cx| Markdown::new(entry.content.into(), None, None, cx)), - priority: entry.priority, - status: entry.status, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct TokenUsage { - pub max_tokens: u64, - pub used_tokens: u64, -} - -impl TokenUsage { - pub fn ratio(&self) -> TokenUsageRatio { - #[cfg(debug_assertions)] - let warning_threshold: f32 = std::env::var("ZED_THREAD_WARNING_THRESHOLD") - .unwrap_or("0.8".to_string()) - .parse() - .unwrap(); - #[cfg(not(debug_assertions))] - let warning_threshold: f32 = 0.8; - - // When the maximum is unknown because there is no selected model, - // avoid showing the token limit warning. - if self.max_tokens == 0 { - TokenUsageRatio::Normal - } else if self.used_tokens >= self.max_tokens { - TokenUsageRatio::Exceeded - } else if self.used_tokens as f32 / self.max_tokens as f32 >= warning_threshold { - TokenUsageRatio::Warning - } else { - TokenUsageRatio::Normal - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum TokenUsageRatio { - Normal, - Warning, - Exceeded, -} - -#[derive(Debug, Clone)] -pub struct RetryStatus { - pub last_error: SharedString, - pub attempt: usize, - pub max_attempts: usize, - pub started_at: Instant, - pub duration: Duration, -} - -pub struct AcpThread { - title: SharedString, - entries: Vec, - plan: Plan, - project: Entity, - action_log: Entity, - shared_buffers: HashMap, BufferSnapshot>, - send_task: Option>, - connection: Rc, - session_id: acp::SessionId, - token_usage: Option, - prompt_capabilities: acp::PromptCapabilities, - _observe_prompt_capabilities: Task>, - terminals: HashMap>, - pending_terminal_output: HashMap>>, - pending_terminal_exit: HashMap, -} - -impl From<&AcpThread> for ActionLogTelemetry { - fn from(value: &AcpThread) -> Self { - Self { - agent_telemetry_id: value.connection().telemetry_id(), - session_id: value.session_id.0.clone(), - } - } -} - -#[derive(Debug)] -pub enum AcpThreadEvent { - NewEntry, - TitleUpdated, - TokenUsageUpdated, - EntryUpdated(usize), - EntriesRemoved(Range), - ToolAuthorizationRequired, - Retry(RetryStatus), - Stopped, - Error, - LoadError(LoadError), - PromptCapabilitiesUpdated, - Refusal, - AvailableCommandsUpdated(Vec), - ModeUpdated(acp::SessionModeId), -} - -impl EventEmitter for AcpThread {} - -#[derive(Debug, Clone)] -pub enum TerminalProviderEvent { - Created { - terminal_id: acp::TerminalId, - label: String, - cwd: Option, - output_byte_limit: Option, - terminal: Entity<::terminal::Terminal>, - }, - Output { - terminal_id: acp::TerminalId, - data: Vec, - }, - TitleChanged { - terminal_id: acp::TerminalId, - title: String, - }, - Exit { - terminal_id: acp::TerminalId, - status: acp::TerminalExitStatus, - }, -} - -#[derive(Debug, Clone)] -pub enum TerminalProviderCommand { - WriteInput { - terminal_id: acp::TerminalId, - bytes: Vec, - }, - Resize { - terminal_id: acp::TerminalId, - cols: u16, - rows: u16, - }, - Close { - terminal_id: acp::TerminalId, - }, -} - -impl AcpThread { - pub fn on_terminal_provider_event( - &mut self, - event: TerminalProviderEvent, - cx: &mut Context, - ) { - match event { - TerminalProviderEvent::Created { - terminal_id, - label, - cwd, - output_byte_limit, - terminal, - } => { - let entity = self.register_terminal_created( - terminal_id.clone(), - label, - cwd, - output_byte_limit, - terminal, - cx, - ); - - if let Some(mut chunks) = self.pending_terminal_output.remove(&terminal_id) { - for data in chunks.drain(..) { - entity.update(cx, |term, cx| { - term.inner().update(cx, |inner, cx| { - inner.write_output(&data, cx); - }) - }); - } - } - - if let Some(_status) = self.pending_terminal_exit.remove(&terminal_id) { - entity.update(cx, |_term, cx| { - cx.notify(); - }); - } - - cx.notify(); - } - TerminalProviderEvent::Output { terminal_id, data } => { - if let Some(entity) = self.terminals.get(&terminal_id) { - entity.update(cx, |term, cx| { - term.inner().update(cx, |inner, cx| { - inner.write_output(&data, cx); - }) - }); - } else { - self.pending_terminal_output - .entry(terminal_id) - .or_default() - .push(data); - } - } - TerminalProviderEvent::TitleChanged { terminal_id, title } => { - if let Some(entity) = self.terminals.get(&terminal_id) { - entity.update(cx, |term, cx| { - term.inner().update(cx, |inner, cx| { - inner.breadcrumb_text = title; - cx.emit(::terminal::Event::BreadcrumbsChanged); - }) - }); - } - } - TerminalProviderEvent::Exit { - terminal_id, - status, - } => { - if let Some(entity) = self.terminals.get(&terminal_id) { - entity.update(cx, |_term, cx| { - cx.notify(); - }); - } else { - self.pending_terminal_exit.insert(terminal_id, status); - } - } - } - } -} - -#[derive(PartialEq, Eq, Debug)] -pub enum ThreadStatus { - Idle, - Generating, -} - -#[derive(Debug, Clone)] -pub enum LoadError { - Unsupported { - command: SharedString, - current_version: SharedString, - minimum_version: SharedString, - }, - FailedToInstall(SharedString), - Exited { - status: ExitStatus, - }, - Other(SharedString), -} - -impl Display for LoadError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - LoadError::Unsupported { - command: path, - current_version, - minimum_version, - } => { - write!( - f, - "version {current_version} from {path} is not supported (need at least {minimum_version})" - ) - } - LoadError::FailedToInstall(msg) => write!(f, "Failed to install: {msg}"), - LoadError::Exited { status } => write!(f, "Server exited with status {status}"), - LoadError::Other(msg) => write!(f, "{msg}"), - } - } -} - -impl Error for LoadError {} - -impl AcpThread { - pub fn new( - title: impl Into, - connection: Rc, - 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().clone(); - 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(), - entries: Default::default(), - plan: Default::default(), - title: title.into(), - project, - send_task: None, - connection, - session_id, - token_usage: None, - prompt_capabilities, - _observe_prompt_capabilities: task, - terminals: HashMap::default(), - pending_terminal_output: HashMap::default(), - pending_terminal_exit: HashMap::default(), - } - } - - pub fn prompt_capabilities(&self) -> acp::PromptCapabilities { - self.prompt_capabilities.clone() - } - - pub fn connection(&self) -> &Rc { - &self.connection - } - - pub fn action_log(&self) -> &Entity { - &self.action_log - } - - pub fn project(&self) -> &Entity { - &self.project - } - - pub fn title(&self) -> SharedString { - self.title.clone() - } - - pub fn entries(&self) -> &[AgentThreadEntry] { - &self.entries - } - - pub fn session_id(&self) -> &acp::SessionId { - &self.session_id - } - - pub fn status(&self) -> ThreadStatus { - if self.send_task.is_some() { - ThreadStatus::Generating - } else { - ThreadStatus::Idle - } - } - - pub fn token_usage(&self) -> Option<&TokenUsage> { - self.token_usage.as_ref() - } - - pub fn has_pending_edit_tool_calls(&self) -> bool { - for entry in self.entries.iter().rev() { - match entry { - AgentThreadEntry::UserMessage(_) => return false, - AgentThreadEntry::ToolCall( - call @ ToolCall { - status: ToolCallStatus::InProgress | ToolCallStatus::Pending, - .. - }, - ) if call.diffs().next().is_some() => { - return true; - } - AgentThreadEntry::ToolCall(_) | AgentThreadEntry::AssistantMessage(_) => {} - } - } - - false - } - - pub fn used_tools_since_last_user_message(&self) -> bool { - for entry in self.entries.iter().rev() { - match entry { - AgentThreadEntry::UserMessage(..) => return false, - AgentThreadEntry::AssistantMessage(..) => continue, - AgentThreadEntry::ToolCall(..) => return true, - } - } - - false - } - - pub fn handle_session_update( - &mut self, - update: acp::SessionUpdate, - cx: &mut Context, - ) -> Result<(), acp::Error> { - match update { - acp::SessionUpdate::UserMessageChunk(acp::ContentChunk { content, .. }) => { - self.push_user_content_block(None, content, cx); - } - acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk { content, .. }) => { - self.push_assistant_content_block(content, false, cx); - } - acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk { content, .. }) => { - self.push_assistant_content_block(content, true, cx); - } - acp::SessionUpdate::ToolCall(tool_call) => { - self.upsert_tool_call(tool_call, cx)?; - } - acp::SessionUpdate::ToolCallUpdate(tool_call_update) => { - self.update_tool_call(tool_call_update, cx)?; - } - acp::SessionUpdate::Plan(plan) => { - self.update_plan(plan, cx); - } - acp::SessionUpdate::AvailableCommandsUpdate(acp::AvailableCommandsUpdate { - available_commands, - .. - }) => cx.emit(AcpThreadEvent::AvailableCommandsUpdated(available_commands)), - acp::SessionUpdate::CurrentModeUpdate(acp::CurrentModeUpdate { - current_mode_id, - .. - }) => cx.emit(AcpThreadEvent::ModeUpdated(current_mode_id)), - _ => {} - } - Ok(()) - } - - pub fn push_user_content_block( - &mut self, - message_id: Option, - chunk: acp::ContentBlock, - cx: &mut Context, - ) { - let language_registry = self.project.read(cx).languages().clone(); - let path_style = self.project.read(cx).path_style(cx); - let entries_len = self.entries.len(); - - if let Some(last_entry) = self.entries.last_mut() - && let AgentThreadEntry::UserMessage(UserMessage { - id, - content, - chunks, - .. - }) = last_entry - { - *id = message_id.or(id.take()); - content.append(chunk.clone(), &language_registry, path_style, cx); - chunks.push(chunk); - let idx = entries_len - 1; - cx.emit(AcpThreadEvent::EntryUpdated(idx)); - } else { - let content = ContentBlock::new(chunk.clone(), &language_registry, path_style, cx); - self.push_entry( - AgentThreadEntry::UserMessage(UserMessage { - id: message_id, - content, - chunks: vec![chunk], - checkpoint: None, - }), - cx, - ); - } - } - - pub fn push_assistant_content_block( - &mut self, - chunk: acp::ContentBlock, - is_thought: bool, - cx: &mut Context, - ) { - let language_registry = self.project.read(cx).languages().clone(); - let path_style = self.project.read(cx).path_style(cx); - let entries_len = self.entries.len(); - if let Some(last_entry) = self.entries.last_mut() - && let AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) = last_entry - { - let idx = entries_len - 1; - cx.emit(AcpThreadEvent::EntryUpdated(idx)); - match (chunks.last_mut(), is_thought) { - (Some(AssistantMessageChunk::Message { block }), false) - | (Some(AssistantMessageChunk::Thought { block }), true) => { - block.append(chunk, &language_registry, path_style, cx) - } - _ => { - let block = ContentBlock::new(chunk, &language_registry, path_style, cx); - if is_thought { - chunks.push(AssistantMessageChunk::Thought { block }) - } else { - chunks.push(AssistantMessageChunk::Message { block }) - } - } - } - } else { - let block = ContentBlock::new(chunk, &language_registry, path_style, cx); - let chunk = if is_thought { - AssistantMessageChunk::Thought { block } - } else { - AssistantMessageChunk::Message { block } - }; - - self.push_entry( - AgentThreadEntry::AssistantMessage(AssistantMessage { - chunks: vec![chunk], - }), - cx, - ); - } - } - - fn push_entry(&mut self, entry: AgentThreadEntry, cx: &mut Context) { - self.entries.push(entry); - 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_token_usage(&mut self, usage: Option, cx: &mut Context) { - self.token_usage = usage; - cx.emit(AcpThreadEvent::TokenUsageUpdated); - } - - pub fn update_retry_status(&mut self, status: RetryStatus, cx: &mut Context) { - cx.emit(AcpThreadEvent::Retry(status)); - } - - pub fn update_tool_call( - &mut self, - update: impl Into, - cx: &mut Context, - ) -> Result<()> { - let update = update.into(); - let languages = self.project.read(cx).languages().clone(); - let path_style = self.project.read(cx).path_style(cx); - - let ix = match self.index_for_tool_call(update.id()) { - Some(ix) => ix, - None => { - // Tool call not found - create a failed tool call entry - let failed_tool_call = ToolCall { - id: update.id().clone(), - label: cx.new(|cx| Markdown::new("Tool call not found".into(), None, None, cx)), - kind: acp::ToolKind::Fetch, - content: vec![ToolCallContent::ContentBlock(ContentBlock::new( - "Tool call not found".into(), - &languages, - path_style, - cx, - ))], - status: ToolCallStatus::Failed, - locations: Vec::new(), - resolved_locations: Vec::new(), - raw_input: None, - raw_output: None, - }; - self.push_entry(AgentThreadEntry::ToolCall(failed_tool_call), cx); - return Ok(()); - } - }; - let AgentThreadEntry::ToolCall(call) = &mut self.entries[ix] else { - unreachable!() - }; - - match update { - ToolCallUpdate::UpdateFields(update) => { - let location_updated = update.fields.locations.is_some(); - call.update_fields(update.fields, languages, path_style, &self.terminals, cx)?; - if location_updated { - self.resolve_locations(update.tool_call_id, cx); - } - } - ToolCallUpdate::UpdateDiff(update) => { - call.content.clear(); - call.content.push(ToolCallContent::Diff(update.diff)); - } - ToolCallUpdate::UpdateTerminal(update) => { - call.content.clear(); - call.content - .push(ToolCallContent::Terminal(update.terminal)); - } - } - - cx.emit(AcpThreadEvent::EntryUpdated(ix)); - - Ok(()) - } - - /// Updates a tool call if id matches an existing entry, otherwise inserts a new one. - pub fn upsert_tool_call( - &mut self, - tool_call: acp::ToolCall, - cx: &mut Context, - ) -> Result<(), acp::Error> { - let status = tool_call.status.into(); - self.upsert_tool_call_inner(tool_call.into(), status, cx) - } - - /// Fails if id does not match an existing entry. - pub fn upsert_tool_call_inner( - &mut self, - update: acp::ToolCallUpdate, - status: ToolCallStatus, - cx: &mut Context, - ) -> Result<(), acp::Error> { - let language_registry = self.project.read(cx).languages().clone(); - let path_style = self.project.read(cx).path_style(cx); - let id = update.tool_call_id.clone(); - - let agent_telemetry_id = self.connection().telemetry_id(); - let session = self.session_id(); - if let ToolCallStatus::Completed | ToolCallStatus::Failed = status { - let status = if matches!(status, ToolCallStatus::Completed) { - "completed" - } else { - "failed" - }; - telemetry::event!( - "Agent Tool Call Completed", - agent_telemetry_id, - session, - status - ); - } - - if let Some(ix) = self.index_for_tool_call(&id) { - let AgentThreadEntry::ToolCall(call) = &mut self.entries[ix] else { - unreachable!() - }; - - call.update_fields( - update.fields, - language_registry, - path_style, - &self.terminals, - cx, - )?; - call.status = status; - - cx.emit(AcpThreadEvent::EntryUpdated(ix)); - } else { - let call = ToolCall::from_acp( - update.try_into()?, - status, - language_registry, - self.project.read(cx).path_style(cx), - &self.terminals, - cx, - )?; - self.push_entry(AgentThreadEntry::ToolCall(call), cx); - }; - - self.resolve_locations(id, cx); - Ok(()) - } - - fn index_for_tool_call(&self, id: &acp::ToolCallId) -> Option { - self.entries - .iter() - .enumerate() - .rev() - .find_map(|(index, entry)| { - if let AgentThreadEntry::ToolCall(tool_call) = entry - && &tool_call.id == id - { - Some(index) - } else { - None - } - }) - } - - fn tool_call_mut(&mut self, id: &acp::ToolCallId) -> Option<(usize, &mut ToolCall)> { - // The tool call we are looking for is typically the last one, or very close to the end. - // At the moment, it doesn't seem like a hashmap would be a good fit for this use case. - self.entries - .iter_mut() - .enumerate() - .rev() - .find_map(|(index, tool_call)| { - if let AgentThreadEntry::ToolCall(tool_call) = tool_call - && &tool_call.id == id - { - Some((index, tool_call)) - } else { - None - } - }) - } - - pub fn tool_call(&mut self, id: &acp::ToolCallId) -> Option<(usize, &ToolCall)> { - self.entries - .iter() - .enumerate() - .rev() - .find_map(|(index, tool_call)| { - if let AgentThreadEntry::ToolCall(tool_call) = tool_call - && &tool_call.id == id - { - Some((index, tool_call)) - } else { - None - } - }) - } - - pub fn resolve_locations(&mut self, id: acp::ToolCallId, cx: &mut Context) { - let project = self.project.clone(); - let Some((_, tool_call)) = self.tool_call_mut(&id) else { - return; - }; - let task = tool_call.resolve_locations(project, cx); - cx.spawn(async move |this, cx| { - let resolved_locations = task.await; - - this.update(cx, |this, cx| { - let project = this.project.clone(); - - for location in resolved_locations.iter().flatten() { - this.shared_buffers - .insert(location.buffer.clone(), location.buffer.read(cx).snapshot()); - } - let Some((ix, tool_call)) = this.tool_call_mut(&id) else { - return; - }; - - if let Some(Some(location)) = resolved_locations.last() { - project.update(cx, |project, cx| { - let should_ignore = if let Some(agent_location) = project - .agent_location() - .filter(|agent_location| agent_location.buffer == location.buffer) - { - let snapshot = location.buffer.read(cx).snapshot(); - let old_position = agent_location.position.to_point(&snapshot); - let new_position = location.position.to_point(&snapshot); - - // ignore this so that when we get updates from the edit tool - // the position doesn't reset to the startof line - old_position.row == new_position.row - && old_position.column > new_position.column - } else { - false - }; - if !should_ignore { - project.set_agent_location(Some(location.into()), cx); - } - }); - } - - let resolved_locations = resolved_locations - .iter() - .map(|l| l.as_ref().map(|l| AgentLocation::from(l))) - .collect::>(); - - if tool_call.resolved_locations != resolved_locations { - tool_call.resolved_locations = resolved_locations; - cx.emit(AcpThreadEvent::EntryUpdated(ix)); - } - }) - }) - .detach(); - } - - pub fn request_tool_call_authorization( - &mut self, - tool_call: acp::ToolCallUpdate, - options: Vec, - respect_always_allow_setting: bool, - cx: &mut Context, - ) -> Result> { - let (tx, rx) = oneshot::channel(); - - if respect_always_allow_setting && AgentSettings::get_global(cx).always_allow_tool_actions { - // Don't use AllowAlways, because then if you were to turn off always_allow_tool_actions, - // some tools would (incorrectly) continue to auto-accept. - if let Some(allow_once_option) = options.iter().find_map(|option| { - if matches!(option.kind, acp::PermissionOptionKind::AllowOnce) { - Some(option.option_id.clone()) - } else { - None - } - }) { - self.upsert_tool_call_inner(tool_call, ToolCallStatus::Pending, cx)?; - return Ok(async { - acp::RequestPermissionOutcome::Selected(acp::SelectedPermissionOutcome::new( - allow_once_option, - )) - } - .boxed()); - } - } - - let status = ToolCallStatus::WaitingForConfirmation { - options, - respond_tx: tx, - }; - - self.upsert_tool_call_inner(tool_call, status, cx)?; - cx.emit(AcpThreadEvent::ToolAuthorizationRequired); - - let fut = async { - match rx.await { - Ok(option) => acp::RequestPermissionOutcome::Selected( - acp::SelectedPermissionOutcome::new(option), - ), - Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled, - } - } - .boxed(); - - Ok(fut) - } - - pub fn authorize_tool_call( - &mut self, - id: acp::ToolCallId, - option_id: acp::PermissionOptionId, - option_kind: acp::PermissionOptionKind, - cx: &mut Context, - ) { - let Some((ix, call)) = self.tool_call_mut(&id) else { - return; - }; - - let new_status = match option_kind { - acp::PermissionOptionKind::RejectOnce | acp::PermissionOptionKind::RejectAlways => { - ToolCallStatus::Rejected - } - acp::PermissionOptionKind::AllowOnce | acp::PermissionOptionKind::AllowAlways => { - ToolCallStatus::InProgress - } - _ => ToolCallStatus::InProgress, - }; - - let curr_status = mem::replace(&mut call.status, new_status); - - if let ToolCallStatus::WaitingForConfirmation { respond_tx, .. } = curr_status { - respond_tx.send(option_id).log_err(); - } else if cfg!(debug_assertions) { - panic!("tried to authorize an already authorized tool call"); - } - - cx.emit(AcpThreadEvent::EntryUpdated(ix)); - } - - pub fn first_tool_awaiting_confirmation(&self) -> Option<&ToolCall> { - let mut first_tool_call = None; - - for entry in self.entries.iter().rev() { - match &entry { - AgentThreadEntry::ToolCall(call) => { - if let ToolCallStatus::WaitingForConfirmation { .. } = call.status { - first_tool_call = Some(call); - } else { - continue; - } - } - AgentThreadEntry::UserMessage(_) | AgentThreadEntry::AssistantMessage(_) => { - // Reached the beginning of the turn. - // If we had pending permission requests in the previous turn, they have been cancelled. - break; - } - } - } - - first_tool_call - } - - pub fn plan(&self) -> &Plan { - &self.plan - } - - pub fn update_plan(&mut self, request: acp::Plan, cx: &mut Context) { - let new_entries_len = request.entries.len(); - let mut new_entries = request.entries.into_iter(); - - // Reuse existing markdown to prevent flickering - for (old, new) in self.plan.entries.iter_mut().zip(new_entries.by_ref()) { - let PlanEntry { - content, - priority, - status, - } = old; - content.update(cx, |old, cx| { - old.replace(new.content, cx); - }); - *priority = new.priority; - *status = new.status; - } - for new in new_entries { - self.plan.entries.push(PlanEntry::from_acp(new, cx)) - } - self.plan.entries.truncate(new_entries_len); - - cx.notify(); - } - - fn clear_completed_plan_entries(&mut self, cx: &mut Context) { - self.plan - .entries - .retain(|entry| !matches!(entry.status, acp::PlanEntryStatus::Completed)); - cx.notify(); - } - - #[cfg(any(test, feature = "test-support"))] - pub fn send_raw( - &mut self, - message: &str, - cx: &mut Context, - ) -> BoxFuture<'static, Result<()>> { - self.send(vec![message.into()], cx) - } - - pub fn send( - &mut self, - message: Vec, - cx: &mut Context, - ) -> BoxFuture<'static, Result<()>> { - let block = ContentBlock::new_combined( - message.clone(), - self.project.read(cx).languages().clone(), - self.project.read(cx).path_style(cx), - cx, - ); - let request = acp::PromptRequest::new(self.session_id.clone(), message.clone()); - let git_store = self.project.read(cx).git_store().clone(); - - let message_id = if self.connection.truncate(&self.session_id, cx).is_some() { - Some(UserMessageId::new()) - } else { - None - }; - - self.run_turn(cx, async move |this, cx| { - this.update(cx, |this, cx| { - this.push_entry( - AgentThreadEntry::UserMessage(UserMessage { - id: message_id.clone(), - content: block, - chunks: message, - checkpoint: None, - }), - cx, - ); - }) - .ok(); - - let old_checkpoint = git_store - .update(cx, |git, cx| git.checkpoint(cx))? - .await - .context("failed to get old checkpoint") - .log_err(); - this.update(cx, |this, cx| { - if let Some((_ix, message)) = this.last_user_message() { - message.checkpoint = old_checkpoint.map(|git_checkpoint| Checkpoint { - git_checkpoint, - show: false, - }); - } - this.connection.prompt(message_id, request, cx) - })? - .await - }) - } - - 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| { - this.connection - .resume(&this.session_id, cx) - .map(|resume| resume.run(cx)) - })? - .context("resuming a session is not supported")? - .await - }) - } - - fn run_turn( - &mut self, - cx: &mut Context, - f: impl 'static + AsyncFnOnce(WeakEntity, &mut AsyncApp) -> Result, - ) -> BoxFuture<'static, Result<()>> { - self.clear_completed_plan_entries(cx); - - let (tx, rx) = oneshot::channel(); - let cancel_task = self.cancel(cx); - - self.send_task = Some(cx.spawn(async move |this, cx| { - cancel_task.await; - tx.send(f(this, cx).await).ok(); - })); - - cx.spawn(async move |this, cx| { - let response = rx.await; - - this.update(cx, |this, cx| this.update_last_checkpoint(cx))? - .await?; - - this.update(cx, |this, cx| { - this.project - .update(cx, |project, cx| project.set_agent_location(None, cx)); - match response { - Ok(Err(e)) => { - this.send_task.take(); - cx.emit(AcpThreadEvent::Error); - Err(e) - } - result => { - let canceled = matches!( - result, - Ok(Ok(acp::PromptResponse { - stop_reason: acp::StopReason::Cancelled, - .. - })) - ); - - // We only take the task if the current prompt wasn't canceled. - // - // This prompt may have been canceled because another one was sent - // while it was still generating. In these cases, dropping `send_task` - // would cause the next generation to be canceled. - if !canceled { - this.send_task.take(); - } - - // Handle refusal - distinguish between user prompt and tool call refusals - if let Ok(Ok(acp::PromptResponse { - stop_reason: acp::StopReason::Refusal, - .. - })) = result - { - if let Some((user_msg_ix, _)) = this.last_user_message() { - // Check if there's a completed tool call with results after the last user message - // This indicates the refusal is in response to tool output, not the user's prompt - let has_completed_tool_call_after_user_msg = - this.entries.iter().skip(user_msg_ix + 1).any(|entry| { - if let AgentThreadEntry::ToolCall(tool_call) = entry { - // Check if the tool call has completed and has output - matches!(tool_call.status, ToolCallStatus::Completed) - && tool_call.raw_output.is_some() - } else { - false - } - }); - - if has_completed_tool_call_after_user_msg { - // Refusal is due to tool output - don't truncate, just notify - // The model refused based on what the tool returned - cx.emit(AcpThreadEvent::Refusal); - } else { - // User prompt was refused - truncate back to before the user message - let range = user_msg_ix..this.entries.len(); - if range.start < range.end { - this.entries.truncate(user_msg_ix); - cx.emit(AcpThreadEvent::EntriesRemoved(range)); - } - cx.emit(AcpThreadEvent::Refusal); - } - } else { - // No user message found, treat as general refusal - cx.emit(AcpThreadEvent::Refusal); - } - } - - cx.emit(AcpThreadEvent::Stopped); - Ok(()) - } - } - })? - }) - .boxed() - } - - pub fn cancel(&mut self, cx: &mut Context) -> Task<()> { - let Some(send_task) = self.send_task.take() else { - return Task::ready(()); - }; - - for entry in self.entries.iter_mut() { - if let AgentThreadEntry::ToolCall(call) = entry { - let cancel = matches!( - call.status, - ToolCallStatus::Pending - | ToolCallStatus::WaitingForConfirmation { .. } - | ToolCallStatus::InProgress - ); - - if cancel { - call.status = ToolCallStatus::Canceled; - } - } - } - - self.connection.cancel(&self.session_id, cx); - - // Wait for the send task to complete - cx.foreground_executor().spawn(send_task) - } - - /// Restores the git working tree to the state at the given checkpoint (if one exists) - pub fn restore_checkpoint( - &mut self, - id: UserMessageId, - cx: &mut Context, - ) -> Task> { - let Some((_, message)) = self.user_message_mut(&id) else { - return Task::ready(Err(anyhow!("message not found"))); - }; - - let checkpoint = message - .checkpoint - .as_ref() - .map(|c| c.git_checkpoint.clone()); - - // Cancel any in-progress generation before restoring - let cancel_task = self.cancel(cx); - let rewind = self.rewind(id.clone(), cx); - let git_store = self.project.read(cx).git_store().clone(); - - cx.spawn(async move |_, cx| { - cancel_task.await; - rewind.await?; - if let Some(checkpoint) = checkpoint { - git_store - .update(cx, |git, cx| git.restore_checkpoint(checkpoint, cx))? - .await?; - } - - Ok(()) - }) - } - - /// Rewinds this thread to before the entry at `index`, removing it and all - /// subsequent entries while rejecting any action_log changes made from that point. - /// Unlike `restore_checkpoint`, this method does not restore from git. - pub fn rewind(&mut self, id: UserMessageId, cx: &mut Context) -> Task> { - let Some(truncate) = self.connection.truncate(&self.session_id, cx) else { - return Task::ready(Err(anyhow!("not supported"))); - }; - - let telemetry = ActionLogTelemetry::from(&*self); - cx.spawn(async move |this, cx| { - cx.update(|cx| truncate.run(id.clone(), cx))?.await?; - this.update(cx, |this, cx| { - if let Some((ix, _)) = this.user_message_mut(&id) { - // Collect all terminals from entries that will be removed - let terminals_to_remove: Vec = this.entries[ix..] - .iter() - .flat_map(|entry| entry.terminals()) - .filter_map(|terminal| terminal.read(cx).id().clone().into()) - .collect(); - - let range = ix..this.entries.len(); - this.entries.truncate(ix); - cx.emit(AcpThreadEvent::EntriesRemoved(range)); - - // Kill and remove the terminals - for terminal_id in terminals_to_remove { - if let Some(terminal) = this.terminals.remove(&terminal_id) { - terminal.update(cx, |terminal, cx| { - terminal.kill(cx); - }); - } - } - } - this.action_log().update(cx, |action_log, cx| { - action_log.reject_all_edits(Some(telemetry), cx) - }) - })? - .await; - Ok(()) - }) - } - - fn update_last_checkpoint(&mut self, cx: &mut Context) -> Task> { - let git_store = self.project.read(cx).git_store().clone(); - - let old_checkpoint = if let Some((_, message)) = self.last_user_message() { - if let Some(checkpoint) = message.checkpoint.as_ref() { - checkpoint.git_checkpoint.clone() - } else { - return Task::ready(Ok(())); - } - } else { - return Task::ready(Ok(())); - }; - - let new_checkpoint = git_store.update(cx, |git, cx| git.checkpoint(cx)); - cx.spawn(async move |this, cx| { - let new_checkpoint = new_checkpoint - .await - .context("failed to get new checkpoint") - .log_err(); - if let Some(new_checkpoint) = new_checkpoint { - let equal = git_store - .update(cx, |git, cx| { - git.compare_checkpoints(old_checkpoint.clone(), new_checkpoint, cx) - })? - .await - .unwrap_or(true); - this.update(cx, |this, cx| { - let (ix, message) = this.last_user_message().context("no user message")?; - let checkpoint = message.checkpoint.as_mut().context("no checkpoint")?; - checkpoint.show = !equal; - cx.emit(AcpThreadEvent::EntryUpdated(ix)); - anyhow::Ok(()) - })??; - } - - Ok(()) - }) - } - - fn last_user_message(&mut self) -> Option<(usize, &mut UserMessage)> { - self.entries - .iter_mut() - .enumerate() - .rev() - .find_map(|(ix, entry)| { - if let AgentThreadEntry::UserMessage(message) = entry { - Some((ix, message)) - } else { - None - } - }) - } - - fn user_message_mut(&mut self, id: &UserMessageId) -> Option<(usize, &mut UserMessage)> { - self.entries.iter_mut().enumerate().find_map(|(ix, entry)| { - if let AgentThreadEntry::UserMessage(message) = entry { - if message.id.as_ref() == Some(id) { - Some((ix, message)) - } else { - None - } - } else { - None - } - }) - } - - pub fn read_text_file( - &self, - path: PathBuf, - line: Option, - limit: Option, - reuse_shared_snapshot: bool, - cx: &mut Context, - ) -> Task> { - // Args are 1-based, move to 0-based - let line = line.unwrap_or_default().saturating_sub(1); - let limit = limit.unwrap_or(u32::MAX); - let project = self.project.clone(); - let action_log = self.action_log.clone(); - cx.spawn(async move |this, cx| { - let load = project - .update(cx, |project, cx| { - let path = project - .project_path_for_absolute_path(&path, cx) - .ok_or_else(|| { - acp::Error::resource_not_found(Some(path.display().to_string())) - })?; - Ok(project.open_buffer(path, cx)) - }) - .map_err(|e| acp::Error::internal_error().data(e.to_string())) - .flatten()?; - - let buffer = load.await?; - - let snapshot = if reuse_shared_snapshot { - this.read_with(cx, |this, _| { - this.shared_buffers.get(&buffer.clone()).cloned() - }) - .log_err() - .flatten() - } else { - None - }; - - let snapshot = if let Some(snapshot) = snapshot { - snapshot - } else { - action_log.update(cx, |action_log, cx| { - action_log.buffer_read(buffer.clone(), cx); - })?; - - let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?; - this.update(cx, |this, _| { - this.shared_buffers.insert(buffer.clone(), snapshot.clone()); - })?; - snapshot - }; - - let max_point = snapshot.max_point(); - let start_position = Point::new(line, 0); - - if start_position > max_point { - return Err(acp::Error::invalid_params().data(format!( - "Attempting to read beyond the end of the file, line {}:{}", - max_point.row + 1, - max_point.column - ))); - } - - let start = snapshot.anchor_before(start_position); - let end = snapshot.anchor_before(Point::new(line.saturating_add(limit), 0)); - - project.update(cx, |project, cx| { - project.set_agent_location( - Some(AgentLocation { - buffer: buffer.downgrade(), - position: start, - }), - cx, - ); - })?; - - Ok(snapshot.text_for_range(start..end).collect::()) - }) - } - - pub fn write_text_file( - &self, - path: PathBuf, - content: String, - cx: &mut Context, - ) -> Task> { - let project = self.project.clone(); - let action_log = self.action_log.clone(); - cx.spawn(async move |this, cx| { - let load = project.update(cx, |project, cx| { - let path = project - .project_path_for_absolute_path(&path, cx) - .context("invalid path")?; - anyhow::Ok(project.open_buffer(path, cx)) - }); - let buffer = load??.await?; - let snapshot = this.update(cx, |this, cx| { - this.shared_buffers - .get(&buffer) - .cloned() - .unwrap_or_else(|| buffer.read(cx).snapshot()) - })?; - let edits = cx - .background_executor() - .spawn(async move { - let old_text = snapshot.text(); - text_diff(old_text.as_str(), &content) - .into_iter() - .map(|(range, replacement)| { - ( - snapshot.anchor_after(range.start) - ..snapshot.anchor_before(range.end), - replacement, - ) - }) - .collect::>() - }) - .await; - - project.update(cx, |project, cx| { - project.set_agent_location( - Some(AgentLocation { - buffer: buffer.downgrade(), - position: edits - .last() - .map(|(range, _)| range.end) - .unwrap_or(Anchor::min_for_buffer(buffer.read(cx).remote_id())), - }), - cx, - ); - })?; - - let format_on_save = cx.update(|cx| { - action_log.update(cx, |action_log, cx| { - action_log.buffer_read(buffer.clone(), cx); - }); - - let format_on_save = buffer.update(cx, |buffer, cx| { - buffer.edit(edits, None, cx); - - let settings = language::language_settings::language_settings( - buffer.language().map(|l| l.name()), - buffer.file(), - cx, - ); - - settings.format_on_save != FormatOnSave::Off - }); - action_log.update(cx, |action_log, cx| { - action_log.buffer_edited(buffer.clone(), cx); - }); - format_on_save - })?; - - if format_on_save { - let format_task = project.update(cx, |project, cx| { - project.format( - HashSet::from_iter([buffer.clone()]), - LspFormatTarget::Buffers, - false, - FormatTrigger::Save, - cx, - ) - })?; - format_task.await.log_err(); - - action_log.update(cx, |action_log, cx| { - action_log.buffer_edited(buffer.clone(), cx); - })?; - } - - project - .update(cx, |project, cx| project.save_buffer(buffer, cx))? - .await - }) - } - - pub fn create_terminal( - &self, - command: String, - args: Vec, - extra_env: Vec, - cwd: Option, - output_byte_limit: Option, - cx: &mut Context, - ) -> Task>> { - let env = match &cwd { - Some(dir) => self.project.update(cx, |project, cx| { - project.environment().update(cx, |env, cx| { - env.directory_environment(dir.as_path().into(), cx) - }) - }), - None => Task::ready(None).shared(), - }; - let env = cx.spawn(async move |_, _| { - let mut env = env.await.unwrap_or_default(); - // Disables paging for `git` and hopefully other commands - env.insert("PAGER".into(), "".into()); - for var in extra_env { - env.insert(var.name, var.value); - } - env - }); - - let project = self.project.clone(); - let language_registry = project.read(cx).languages().clone(); - let is_windows = project.read(cx).path_style(cx).is_windows(); - - let terminal_id = acp::TerminalId::new(Uuid::new_v4().to_string()); - let terminal_task = cx.spawn({ - let terminal_id = terminal_id.clone(); - async move |_this, cx| { - let env = env.await; - let shell = project - .update(cx, |project, cx| { - project - .remote_client() - .and_then(|r| r.read(cx).default_system_shell()) - })? - .unwrap_or_else(|| get_default_system_shell_preferring_bash()); - let (task_command, task_args) = - ShellBuilder::new(&Shell::Program(shell), is_windows) - .redirect_stdin_to_dev_null() - .build(Some(command.clone()), &args); - let terminal = project - .update(cx, |project, cx| { - project.create_terminal_task( - task::SpawnInTerminal { - command: Some(task_command), - args: task_args, - cwd: cwd.clone(), - env, - ..Default::default() - }, - cx, - ) - })? - .await?; - - cx.new(|cx| { - Terminal::new( - terminal_id, - &format!("{} {}", command, args.join(" ")), - cwd, - output_byte_limit.map(|l| l as usize), - terminal, - language_registry, - cx, - ) - }) - } - }); - - cx.spawn(async move |this, cx| { - let terminal = terminal_task.await?; - this.update(cx, |this, _cx| { - this.terminals.insert(terminal_id, terminal.clone()); - terminal - }) - }) - } - - pub fn kill_terminal( - &mut self, - terminal_id: acp::TerminalId, - cx: &mut Context, - ) -> Result<()> { - self.terminals - .get(&terminal_id) - .context("Terminal not found")? - .update(cx, |terminal, cx| { - terminal.kill(cx); - }); - - Ok(()) - } - - pub fn release_terminal( - &mut self, - terminal_id: acp::TerminalId, - cx: &mut Context, - ) -> Result<()> { - self.terminals - .remove(&terminal_id) - .context("Terminal not found")? - .update(cx, |terminal, cx| { - terminal.kill(cx); - }); - - Ok(()) - } - - pub fn terminal(&self, terminal_id: acp::TerminalId) -> Result> { - self.terminals - .get(&terminal_id) - .context("Terminal not found") - .cloned() - } - - pub fn to_markdown(&self, cx: &App) -> String { - self.entries.iter().map(|e| e.to_markdown(cx)).collect() - } - - pub fn emit_load_error(&mut self, error: LoadError, cx: &mut Context) { - cx.emit(AcpThreadEvent::LoadError(error)); - } - - pub fn register_terminal_created( - &mut self, - terminal_id: acp::TerminalId, - command_label: String, - working_dir: Option, - output_byte_limit: Option, - terminal: Entity<::terminal::Terminal>, - cx: &mut Context, - ) -> Entity { - let language_registry = self.project.read(cx).languages().clone(); - - let entity = cx.new(|cx| { - Terminal::new( - terminal_id.clone(), - &command_label, - working_dir.clone(), - output_byte_limit.map(|l| l as usize), - terminal, - language_registry, - cx, - ) - }); - self.terminals.insert(terminal_id.clone(), entity.clone()); - entity - } -} - -fn markdown_for_raw_output( - raw_output: &serde_json::Value, - language_registry: &Arc, - cx: &mut App, -) -> Option> { - match raw_output { - serde_json::Value::Null => None, - serde_json::Value::Bool(value) => Some(cx.new(|cx| { - Markdown::new( - value.to_string().into(), - Some(language_registry.clone()), - None, - cx, - ) - })), - serde_json::Value::Number(value) => Some(cx.new(|cx| { - Markdown::new( - value.to_string().into(), - Some(language_registry.clone()), - None, - cx, - ) - })), - serde_json::Value::String(value) => Some(cx.new(|cx| { - Markdown::new( - value.clone().into(), - Some(language_registry.clone()), - None, - cx, - ) - })), - value => Some(cx.new(|cx| { - Markdown::new( - format!("```json\n{}\n```", value).into(), - Some(language_registry.clone()), - None, - cx, - ) - })), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use anyhow::anyhow; - use futures::{channel::mpsc, future::LocalBoxFuture, select}; - use gpui::{App, AsyncApp, TestAppContext, WeakEntity}; - use indoc::indoc; - use project::{FakeFs, Fs}; - use rand::{distr, prelude::*}; - use serde_json::json; - use settings::SettingsStore; - use smol::stream::StreamExt as _; - use std::{ - any::Any, - cell::RefCell, - path::Path, - rc::Rc, - sync::atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst}, - time::Duration, - }; - use util::path; - - fn init_test(cx: &mut TestAppContext) { - env_logger::try_init().ok(); - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - }); - } - - #[gpui::test] - async fn test_terminal_output_buffered_before_created_renders(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - let connection = Rc::new(FakeAgentConnection::new()); - let thread = cx - .update(|cx| connection.new_thread(project, std::path::Path::new(path!("/test")), cx)) - .await - .unwrap(); - - let terminal_id = acp::TerminalId::new(uuid::Uuid::new_v4().to_string()); - - // Send Output BEFORE Created - should be buffered by acp_thread - thread.update(cx, |thread, cx| { - thread.on_terminal_provider_event( - TerminalProviderEvent::Output { - terminal_id: terminal_id.clone(), - data: b"hello buffered".to_vec(), - }, - cx, - ); - }); - - // Create a display-only terminal and then send Created - let lower = cx.new(|cx| { - let builder = ::terminal::TerminalBuilder::new_display_only( - ::terminal::terminal_settings::CursorShape::default(), - ::terminal::terminal_settings::AlternateScroll::On, - None, - 0, - ) - .unwrap(); - builder.subscribe(cx) - }); - - thread.update(cx, |thread, cx| { - thread.on_terminal_provider_event( - TerminalProviderEvent::Created { - terminal_id: terminal_id.clone(), - label: "Buffered Test".to_string(), - cwd: None, - output_byte_limit: None, - terminal: lower.clone(), - }, - cx, - ); - }); - - // After Created, buffered Output should have been flushed into the renderer - let content = thread.read_with(cx, |thread, cx| { - let term = thread.terminal(terminal_id.clone()).unwrap(); - term.read_with(cx, |t, cx| t.inner().read(cx).get_content()) - }); - - assert!( - content.contains("hello buffered"), - "expected buffered output to render, got: {content}" - ); - } - - #[gpui::test] - async fn test_terminal_output_and_exit_buffered_before_created(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - let connection = Rc::new(FakeAgentConnection::new()); - let thread = cx - .update(|cx| connection.new_thread(project, std::path::Path::new(path!("/test")), cx)) - .await - .unwrap(); - - let terminal_id = acp::TerminalId::new(uuid::Uuid::new_v4().to_string()); - - // Send Output BEFORE Created - thread.update(cx, |thread, cx| { - thread.on_terminal_provider_event( - TerminalProviderEvent::Output { - terminal_id: terminal_id.clone(), - data: b"pre-exit data".to_vec(), - }, - cx, - ); - }); - - // Send Exit BEFORE Created - thread.update(cx, |thread, cx| { - thread.on_terminal_provider_event( - TerminalProviderEvent::Exit { - terminal_id: terminal_id.clone(), - status: acp::TerminalExitStatus::new().exit_code(0), - }, - cx, - ); - }); - - // Now create a display-only lower-level terminal and send Created - let lower = cx.new(|cx| { - let builder = ::terminal::TerminalBuilder::new_display_only( - ::terminal::terminal_settings::CursorShape::default(), - ::terminal::terminal_settings::AlternateScroll::On, - None, - 0, - ) - .unwrap(); - builder.subscribe(cx) - }); - - thread.update(cx, |thread, cx| { - thread.on_terminal_provider_event( - TerminalProviderEvent::Created { - terminal_id: terminal_id.clone(), - label: "Buffered Exit Test".to_string(), - cwd: None, - output_byte_limit: None, - terminal: lower.clone(), - }, - cx, - ); - }); - - // Output should be present after Created (flushed from buffer) - let content = thread.read_with(cx, |thread, cx| { - let term = thread.terminal(terminal_id.clone()).unwrap(); - term.read_with(cx, |t, cx| t.inner().read(cx).get_content()) - }); - - assert!( - content.contains("pre-exit data"), - "expected pre-exit data to render, got: {content}" - ); - } - - #[gpui::test] - async fn test_push_user_content_block(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - let connection = Rc::new(FakeAgentConnection::new()); - let thread = cx - .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) - .await - .unwrap(); - - // Test creating a new user message - thread.update(cx, |thread, cx| { - thread.push_user_content_block(None, "Hello, ".into(), cx); - }); - - thread.update(cx, |thread, cx| { - assert_eq!(thread.entries.len(), 1); - if let AgentThreadEntry::UserMessage(user_msg) = &thread.entries[0] { - assert_eq!(user_msg.id, None); - assert_eq!(user_msg.content.to_markdown(cx), "Hello, "); - } else { - panic!("Expected UserMessage"); - } - }); - - // Test appending to existing user message - let message_1_id = UserMessageId::new(); - thread.update(cx, |thread, cx| { - thread.push_user_content_block(Some(message_1_id.clone()), "world!".into(), cx); - }); - - thread.update(cx, |thread, cx| { - assert_eq!(thread.entries.len(), 1); - if let AgentThreadEntry::UserMessage(user_msg) = &thread.entries[0] { - assert_eq!(user_msg.id, Some(message_1_id)); - assert_eq!(user_msg.content.to_markdown(cx), "Hello, world!"); - } else { - panic!("Expected UserMessage"); - } - }); - - // Test creating new user message after assistant message - thread.update(cx, |thread, cx| { - thread.push_assistant_content_block("Assistant response".into(), false, cx); - }); - - let message_2_id = UserMessageId::new(); - thread.update(cx, |thread, cx| { - thread.push_user_content_block( - Some(message_2_id.clone()), - "New user message".into(), - cx, - ); - }); - - thread.update(cx, |thread, cx| { - assert_eq!(thread.entries.len(), 3); - if let AgentThreadEntry::UserMessage(user_msg) = &thread.entries[2] { - assert_eq!(user_msg.id, Some(message_2_id)); - assert_eq!(user_msg.content.to_markdown(cx), "New user message"); - } else { - panic!("Expected UserMessage at index 2"); - } - }); - } - - #[gpui::test] - async fn test_thinking_concatenation(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - let connection = Rc::new(FakeAgentConnection::new().on_user_message( - |_, thread, mut cx| { - async move { - thread.update(&mut cx, |thread, cx| { - thread - .handle_session_update( - acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk::new( - "Thinking ".into(), - )), - cx, - ) - .unwrap(); - thread - .handle_session_update( - acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk::new( - "hard!".into(), - )), - cx, - ) - .unwrap(); - })?; - Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)) - } - .boxed_local() - }, - )); - - let thread = cx - .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) - .await - .unwrap(); - - thread - .update(cx, |thread, cx| thread.send_raw("Hello from Zed!", cx)) - .await - .unwrap(); - - let output = thread.read_with(cx, |thread, cx| thread.to_markdown(cx)); - assert_eq!( - output, - indoc! {r#" - ## User - - Hello from Zed! - - ## Assistant - - - Thinking hard! - - - "#} - ); - } - - #[gpui::test] - async fn test_edits_concurrently_to_user(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/tmp"), json!({"foo": "one\ntwo\nthree\n"})) - .await; - let project = Project::test(fs.clone(), [], cx).await; - let (read_file_tx, read_file_rx) = oneshot::channel::<()>(); - let read_file_tx = Rc::new(RefCell::new(Some(read_file_tx))); - let connection = Rc::new(FakeAgentConnection::new().on_user_message( - move |_, thread, mut cx| { - let read_file_tx = read_file_tx.clone(); - async move { - let content = thread - .update(&mut cx, |thread, cx| { - thread.read_text_file(path!("/tmp/foo").into(), None, None, false, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(content, "one\ntwo\nthree\n"); - read_file_tx.take().unwrap().send(()).unwrap(); - thread - .update(&mut cx, |thread, cx| { - thread.write_text_file( - path!("/tmp/foo").into(), - "one\ntwo\nthree\nfour\nfive\n".to_string(), - cx, - ) - }) - .unwrap() - .await - .unwrap(); - Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)) - } - .boxed_local() - }, - )); - - let (worktree, pathbuf) = project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/tmp/foo"), true, cx) - }) - .await - .unwrap(); - let buffer = project - .update(cx, |project, cx| { - project.open_buffer((worktree.read(cx).id(), pathbuf), cx) - }) - .await - .unwrap(); - - let thread = cx - .update(|cx| connection.new_thread(project, Path::new(path!("/tmp")), cx)) - .await - .unwrap(); - - let request = thread.update(cx, |thread, cx| { - thread.send_raw("Extend the count in /tmp/foo", cx) - }); - read_file_rx.await.ok(); - buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, "zero\n".to_string())], None, cx); - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "zero\none\ntwo\nthree\nfour\nfive\n" - ); - assert_eq!( - String::from_utf8(fs.read_file_sync(path!("/tmp/foo")).unwrap()).unwrap(), - "zero\none\ntwo\nthree\nfour\nfive\n" - ); - request.await.unwrap(); - } - - #[gpui::test] - async fn test_reading_from_line(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/tmp"), json!({"foo": "one\ntwo\nthree\nfour\n"})) - .await; - let project = Project::test(fs.clone(), [], cx).await; - project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/tmp/foo"), true, cx) - }) - .await - .unwrap(); - - let connection = Rc::new(FakeAgentConnection::new()); - - let thread = cx - .update(|cx| connection.new_thread(project, Path::new(path!("/tmp")), cx)) - .await - .unwrap(); - - // Whole file - let content = thread - .update(cx, |thread, cx| { - thread.read_text_file(path!("/tmp/foo").into(), None, None, false, cx) - }) - .await - .unwrap(); - - assert_eq!(content, "one\ntwo\nthree\nfour\n"); - - // Only start line - let content = thread - .update(cx, |thread, cx| { - thread.read_text_file(path!("/tmp/foo").into(), Some(3), None, false, cx) - }) - .await - .unwrap(); - - assert_eq!(content, "three\nfour\n"); - - // Only limit - let content = thread - .update(cx, |thread, cx| { - thread.read_text_file(path!("/tmp/foo").into(), None, Some(2), false, cx) - }) - .await - .unwrap(); - - assert_eq!(content, "one\ntwo\n"); - - // Range - let content = thread - .update(cx, |thread, cx| { - thread.read_text_file(path!("/tmp/foo").into(), Some(2), Some(2), false, cx) - }) - .await - .unwrap(); - - assert_eq!(content, "two\nthree\n"); - - // Invalid - let err = thread - .update(cx, |thread, cx| { - thread.read_text_file(path!("/tmp/foo").into(), Some(6), Some(2), false, cx) - }) - .await - .unwrap_err(); - - assert_eq!( - err.to_string(), - "Invalid params: \"Attempting to read beyond the end of the file, line 5:0\"" - ); - } - - #[gpui::test] - async fn test_reading_empty_file(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/tmp"), json!({"foo": ""})).await; - let project = Project::test(fs.clone(), [], cx).await; - project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/tmp/foo"), true, cx) - }) - .await - .unwrap(); - - let connection = Rc::new(FakeAgentConnection::new()); - - let thread = cx - .update(|cx| connection.new_thread(project, Path::new(path!("/tmp")), cx)) - .await - .unwrap(); - - // Whole file - let content = thread - .update(cx, |thread, cx| { - thread.read_text_file(path!("/tmp/foo").into(), None, None, false, cx) - }) - .await - .unwrap(); - - assert_eq!(content, ""); - - // Only start line - let content = thread - .update(cx, |thread, cx| { - thread.read_text_file(path!("/tmp/foo").into(), Some(1), None, false, cx) - }) - .await - .unwrap(); - - assert_eq!(content, ""); - - // Only limit - let content = thread - .update(cx, |thread, cx| { - thread.read_text_file(path!("/tmp/foo").into(), None, Some(2), false, cx) - }) - .await - .unwrap(); - - assert_eq!(content, ""); - - // Range - let content = thread - .update(cx, |thread, cx| { - thread.read_text_file(path!("/tmp/foo").into(), Some(1), Some(1), false, cx) - }) - .await - .unwrap(); - - assert_eq!(content, ""); - - // Invalid - let err = thread - .update(cx, |thread, cx| { - thread.read_text_file(path!("/tmp/foo").into(), Some(5), Some(2), false, cx) - }) - .await - .unwrap_err(); - - assert_eq!( - err.to_string(), - "Invalid params: \"Attempting to read beyond the end of the file, line 1:0\"" - ); - } - #[gpui::test] - async fn test_reading_non_existing_file(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/tmp"), json!({})).await; - let project = Project::test(fs.clone(), [], cx).await; - project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/tmp"), true, cx) - }) - .await - .unwrap(); - - let connection = Rc::new(FakeAgentConnection::new()); - - let thread = cx - .update(|cx| connection.new_thread(project, Path::new(path!("/tmp")), cx)) - .await - .unwrap(); - - // Out of project file - let err = thread - .update(cx, |thread, cx| { - thread.read_text_file(path!("/foo").into(), None, None, false, cx) - }) - .await - .unwrap_err(); - - assert_eq!(err.code, acp::ErrorCode::ResourceNotFound); - } - - #[gpui::test] - async fn test_succeeding_canceled_toolcall(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - let id = acp::ToolCallId::new("test"); - - let connection = Rc::new(FakeAgentConnection::new().on_user_message({ - let id = id.clone(); - move |_, thread, mut cx| { - let id = id.clone(); - async move { - thread - .update(&mut cx, |thread, cx| { - thread.handle_session_update( - acp::SessionUpdate::ToolCall( - acp::ToolCall::new(id.clone(), "Label") - .kind(acp::ToolKind::Fetch) - .status(acp::ToolCallStatus::InProgress), - ), - cx, - ) - }) - .unwrap() - .unwrap(); - Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)) - } - .boxed_local() - } - })); - - let thread = cx - .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) - .await - .unwrap(); - - let request = thread.update(cx, |thread, cx| { - thread.send_raw("Fetch https://example.com", cx) - }); - - run_until_first_tool_call(&thread, cx).await; - - thread.read_with(cx, |thread, _| { - assert!(matches!( - thread.entries[1], - AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::InProgress, - .. - }) - )); - }); - - thread.update(cx, |thread, cx| thread.cancel(cx)).await; - - thread.read_with(cx, |thread, _| { - assert!(matches!( - &thread.entries[1], - AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::Canceled, - .. - }) - )); - }); - - thread - .update(cx, |thread, cx| { - thread.handle_session_update( - acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate::new( - id, - acp::ToolCallUpdateFields::new().status(acp::ToolCallStatus::Completed), - )), - cx, - ) - }) - .unwrap(); - - request.await.unwrap(); - - thread.read_with(cx, |thread, _| { - assert!(matches!( - thread.entries[1], - AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::Completed, - .. - }) - )); - }); - } - - #[gpui::test] - async fn test_no_pending_edits_if_tool_calls_are_completed(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree(path!("/test"), json!({})).await; - let project = Project::test(fs, [path!("/test").as_ref()], cx).await; - - let connection = Rc::new(FakeAgentConnection::new().on_user_message({ - move |_, thread, mut cx| { - async move { - thread - .update(&mut cx, |thread, cx| { - thread.handle_session_update( - acp::SessionUpdate::ToolCall( - acp::ToolCall::new("test", "Label") - .kind(acp::ToolKind::Edit) - .status(acp::ToolCallStatus::Completed) - .content(vec![acp::ToolCallContent::Diff(acp::Diff::new( - "/test/test.txt", - "foo", - ))]), - ), - cx, - ) - }) - .unwrap() - .unwrap(); - Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)) - } - .boxed_local() - } - })); - - let thread = cx - .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) - .await - .unwrap(); - - cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["Hi".into()], cx))) - .await - .unwrap(); - - assert!(cx.read(|cx| !thread.read(cx).has_pending_edit_tool_calls())); - } - - #[gpui::test(iterations = 10)] - async fn test_checkpoints(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/test"), - json!({ - ".git": {} - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; - - let simulate_changes = Arc::new(AtomicBool::new(true)); - let next_filename = Arc::new(AtomicUsize::new(0)); - let connection = Rc::new(FakeAgentConnection::new().on_user_message({ - let simulate_changes = simulate_changes.clone(); - let next_filename = next_filename.clone(); - let fs = fs.clone(); - move |request, thread, mut cx| { - let fs = fs.clone(); - let simulate_changes = simulate_changes.clone(); - let next_filename = next_filename.clone(); - async move { - if simulate_changes.load(SeqCst) { - let filename = format!("/test/file-{}", next_filename.fetch_add(1, SeqCst)); - fs.write(Path::new(&filename), b"").await?; - } - - let acp::ContentBlock::Text(content) = &request.prompt[0] else { - panic!("expected text content block"); - }; - thread.update(&mut cx, |thread, cx| { - thread - .handle_session_update( - acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new( - content.text.to_uppercase().into(), - )), - cx, - ) - .unwrap(); - })?; - Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)) - } - .boxed_local() - } - })); - let thread = cx - .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) - .await - .unwrap(); - - cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["Lorem".into()], cx))) - .await - .unwrap(); - thread.read_with(cx, |thread, cx| { - assert_eq!( - thread.to_markdown(cx), - indoc! {" - ## User (checkpoint) - - Lorem - - ## Assistant - - LOREM - - "} - ); - }); - assert_eq!(fs.files(), vec![Path::new(path!("/test/file-0"))]); - - cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["ipsum".into()], cx))) - .await - .unwrap(); - thread.read_with(cx, |thread, cx| { - assert_eq!( - thread.to_markdown(cx), - indoc! {" - ## User (checkpoint) - - Lorem - - ## Assistant - - LOREM - - ## User (checkpoint) - - ipsum - - ## Assistant - - IPSUM - - "} - ); - }); - assert_eq!( - fs.files(), - vec![ - Path::new(path!("/test/file-0")), - Path::new(path!("/test/file-1")) - ] - ); - - // Checkpoint isn't stored when there are no changes. - simulate_changes.store(false, SeqCst); - cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["dolor".into()], cx))) - .await - .unwrap(); - thread.read_with(cx, |thread, cx| { - assert_eq!( - thread.to_markdown(cx), - indoc! {" - ## User (checkpoint) - - Lorem - - ## Assistant - - LOREM - - ## User (checkpoint) - - ipsum - - ## Assistant - - IPSUM - - ## User - - dolor - - ## Assistant - - DOLOR - - "} - ); - }); - assert_eq!( - fs.files(), - vec![ - Path::new(path!("/test/file-0")), - Path::new(path!("/test/file-1")) - ] - ); - - // Rewinding the conversation truncates the history and restores the checkpoint. - thread - .update(cx, |thread, cx| { - let AgentThreadEntry::UserMessage(message) = &thread.entries[2] else { - panic!("unexpected entries {:?}", thread.entries) - }; - thread.restore_checkpoint(message.id.clone().unwrap(), cx) - }) - .await - .unwrap(); - thread.read_with(cx, |thread, cx| { - assert_eq!( - thread.to_markdown(cx), - indoc! {" - ## User (checkpoint) - - Lorem - - ## Assistant - - LOREM - - "} - ); - }); - assert_eq!(fs.files(), vec![Path::new(path!("/test/file-0"))]); - } - - #[gpui::test] - async fn test_tool_result_refusal(cx: &mut TestAppContext) { - use std::sync::atomic::AtomicUsize; - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, None, cx).await; - - // Create a connection that simulates refusal after tool result - let prompt_count = Arc::new(AtomicUsize::new(0)); - let connection = Rc::new(FakeAgentConnection::new().on_user_message({ - let prompt_count = prompt_count.clone(); - move |_request, thread, mut cx| { - let count = prompt_count.fetch_add(1, SeqCst); - async move { - if count == 0 { - // First prompt: Generate a tool call with result - thread.update(&mut cx, |thread, cx| { - thread - .handle_session_update( - acp::SessionUpdate::ToolCall( - acp::ToolCall::new("tool1", "Test Tool") - .kind(acp::ToolKind::Fetch) - .status(acp::ToolCallStatus::Completed) - .raw_input(serde_json::json!({"query": "test"})) - .raw_output(serde_json::json!({"result": "inappropriate content"})), - ), - cx, - ) - .unwrap(); - })?; - - // Now return refusal because of the tool result - Ok(acp::PromptResponse::new(acp::StopReason::Refusal)) - } else { - Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)) - } - } - .boxed_local() - } - })); - - let thread = cx - .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) - .await - .unwrap(); - - // Track if we see a Refusal event - let saw_refusal_event = Arc::new(std::sync::Mutex::new(false)); - let saw_refusal_event_captured = saw_refusal_event.clone(); - thread.update(cx, |_thread, cx| { - cx.subscribe( - &thread, - move |_thread, _event_thread, event: &AcpThreadEvent, _cx| { - if matches!(event, AcpThreadEvent::Refusal) { - *saw_refusal_event_captured.lock().unwrap() = true; - } - }, - ) - .detach(); - }); - - // Send a user message - this will trigger tool call and then refusal - let send_task = thread.update(cx, |thread, cx| thread.send(vec!["Hello".into()], cx)); - cx.background_executor.spawn(send_task).detach(); - cx.run_until_parked(); - - // Verify that: - // 1. A Refusal event WAS emitted (because it's a tool result refusal, not user prompt) - // 2. The user message was NOT truncated - assert!( - *saw_refusal_event.lock().unwrap(), - "Refusal event should be emitted for tool result refusals" - ); - - thread.read_with(cx, |thread, _| { - let entries = thread.entries(); - assert!(entries.len() >= 2, "Should have user message and tool call"); - - // Verify user message is still there - assert!( - matches!(entries[0], AgentThreadEntry::UserMessage(_)), - "User message should not be truncated" - ); - - // Verify tool call is there with result - if let AgentThreadEntry::ToolCall(tool_call) = &entries[1] { - assert!( - tool_call.raw_output.is_some(), - "Tool call should have output" - ); - } else { - panic!("Expected tool call at index 1"); - } - }); - } - - #[gpui::test] - async fn test_user_prompt_refusal_emits_event(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, None, cx).await; - - let refuse_next = Arc::new(AtomicBool::new(false)); - let connection = Rc::new(FakeAgentConnection::new().on_user_message({ - let refuse_next = refuse_next.clone(); - move |_request, _thread, _cx| { - if refuse_next.load(SeqCst) { - async move { Ok(acp::PromptResponse::new(acp::StopReason::Refusal)) } - .boxed_local() - } else { - async move { Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)) } - .boxed_local() - } - } - })); - - let thread = cx - .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) - .await - .unwrap(); - - // Track if we see a Refusal event - let saw_refusal_event = Arc::new(std::sync::Mutex::new(false)); - let saw_refusal_event_captured = saw_refusal_event.clone(); - thread.update(cx, |_thread, cx| { - cx.subscribe( - &thread, - move |_thread, _event_thread, event: &AcpThreadEvent, _cx| { - if matches!(event, AcpThreadEvent::Refusal) { - *saw_refusal_event_captured.lock().unwrap() = true; - } - }, - ) - .detach(); - }); - - // Send a message that will be refused - refuse_next.store(true, SeqCst); - cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["hello".into()], cx))) - .await - .unwrap(); - - // Verify that a Refusal event WAS emitted for user prompt refusal - assert!( - *saw_refusal_event.lock().unwrap(), - "Refusal event should be emitted for user prompt refusals" - ); - - // Verify the message was truncated (user prompt refusal) - thread.read_with(cx, |thread, cx| { - assert_eq!(thread.to_markdown(cx), ""); - }); - } - - #[gpui::test] - async fn test_refusal(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree(path!("/"), json!({})).await; - let project = Project::test(fs.clone(), [path!("/").as_ref()], cx).await; - - let refuse_next = Arc::new(AtomicBool::new(false)); - let connection = Rc::new(FakeAgentConnection::new().on_user_message({ - let refuse_next = refuse_next.clone(); - move |request, thread, mut cx| { - let refuse_next = refuse_next.clone(); - async move { - if refuse_next.load(SeqCst) { - return Ok(acp::PromptResponse::new(acp::StopReason::Refusal)); - } - - let acp::ContentBlock::Text(content) = &request.prompt[0] else { - panic!("expected text content block"); - }; - thread.update(&mut cx, |thread, cx| { - thread - .handle_session_update( - acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new( - content.text.to_uppercase().into(), - )), - cx, - ) - .unwrap(); - })?; - Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)) - } - .boxed_local() - } - })); - let thread = cx - .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) - .await - .unwrap(); - - cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["hello".into()], cx))) - .await - .unwrap(); - thread.read_with(cx, |thread, cx| { - assert_eq!( - thread.to_markdown(cx), - indoc! {" - ## User - - hello - - ## Assistant - - HELLO - - "} - ); - }); - - // Simulate refusing the second message. The message should be truncated - // when a user prompt is refused. - refuse_next.store(true, SeqCst); - cx.update(|cx| thread.update(cx, |thread, cx| thread.send(vec!["world".into()], cx))) - .await - .unwrap(); - thread.read_with(cx, |thread, cx| { - assert_eq!( - thread.to_markdown(cx), - indoc! {" - ## User - - hello - - ## Assistant - - HELLO - - "} - ); - }); - } - - async fn run_until_first_tool_call( - thread: &Entity, - cx: &mut TestAppContext, - ) -> usize { - let (mut tx, mut rx) = mpsc::channel::(1); - - let subscription = cx.update(|cx| { - cx.subscribe(thread, move |thread, _, cx| { - for (ix, entry) in thread.read(cx).entries.iter().enumerate() { - if matches!(entry, AgentThreadEntry::ToolCall(_)) { - return tx.try_send(ix).unwrap(); - } - } - }) - }); - - select! { - _ = futures::FutureExt::fuse(smol::Timer::after(Duration::from_secs(10))) => { - panic!("Timeout waiting for tool call") - } - ix = rx.next().fuse() => { - drop(subscription); - ix.unwrap() - } - } - } - - #[derive(Clone, Default)] - struct FakeAgentConnection { - auth_methods: Vec, - sessions: Arc>>>, - on_user_message: Option< - Rc< - dyn Fn( - acp::PromptRequest, - WeakEntity, - AsyncApp, - ) -> LocalBoxFuture<'static, Result> - + 'static, - >, - >, - } - - impl FakeAgentConnection { - fn new() -> Self { - Self { - auth_methods: Vec::new(), - on_user_message: None, - sessions: Arc::default(), - } - } - - #[expect(unused)] - fn with_auth_methods(mut self, auth_methods: Vec) -> Self { - self.auth_methods = auth_methods; - self - } - - fn on_user_message( - mut self, - handler: impl Fn( - acp::PromptRequest, - WeakEntity, - AsyncApp, - ) -> LocalBoxFuture<'static, Result> - + 'static, - ) -> Self { - self.on_user_message.replace(Rc::new(handler)); - self - } - } - - impl AgentConnection for FakeAgentConnection { - fn telemetry_id(&self) -> SharedString { - "fake".into() - } - - fn auth_methods(&self) -> &[acp::AuthMethod] { - &self.auth_methods - } - - fn new_thread( - self: Rc, - project: Entity, - _cwd: &Path, - cx: &mut App, - ) -> Task>> { - let session_id = acp::SessionId::new( - rand::rng() - .sample_iter(&distr::Alphanumeric) - .take(7) - .map(char::from) - .collect::(), - ); - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let thread = cx.new(|cx| { - AcpThread::new( - "Test", - self.clone(), - project, - action_log, - session_id.clone(), - watch::Receiver::constant( - acp::PromptCapabilities::new() - .image(true) - .audio(true) - .embedded_context(true), - ), - cx, - ) - }); - self.sessions.lock().insert(session_id, thread.downgrade()); - Task::ready(Ok(thread)) - } - - fn authenticate(&self, method: acp::AuthMethodId, _cx: &mut App) -> Task> { - if self.auth_methods().iter().any(|m| m.id == method) { - Task::ready(Ok(())) - } else { - Task::ready(Err(anyhow!("Invalid Auth Method"))) - } - } - - fn prompt( - &self, - _id: Option, - params: acp::PromptRequest, - cx: &mut App, - ) -> Task> { - let sessions = self.sessions.lock(); - let thread = sessions.get(¶ms.session_id).unwrap(); - if let Some(handler) = &self.on_user_message { - let handler = handler.clone(); - let thread = thread.clone(); - cx.spawn(async move |cx| handler(params, thread, cx.clone()).await) - } else { - Task::ready(Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))) - } - } - - fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { - let sessions = self.sessions.lock(); - let thread = sessions.get(session_id).unwrap().clone(); - - cx.spawn(async move |cx| { - thread - .update(cx, |thread, cx| thread.cancel(cx)) - .unwrap() - .await - }) - .detach(); - } - - fn truncate( - &self, - session_id: &acp::SessionId, - _cx: &App, - ) -> Option> { - Some(Rc::new(FakeAgentSessionEditor { - _session_id: session_id.clone(), - })) - } - - fn into_any(self: Rc) -> Rc { - self - } - } - - struct FakeAgentSessionEditor { - _session_id: acp::SessionId, - } - - impl AgentSessionTruncate for FakeAgentSessionEditor { - fn run(&self, _message_id: UserMessageId, _cx: &mut App) -> Task> { - Task::ready(Ok(())) - } - } - - #[gpui::test] - async fn test_tool_call_not_found_creates_failed_entry(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - let connection = Rc::new(FakeAgentConnection::new()); - let thread = cx - .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) - .await - .unwrap(); - - // Try to update a tool call that doesn't exist - let nonexistent_id = acp::ToolCallId::new("nonexistent-tool-call"); - thread.update(cx, |thread, cx| { - let result = thread.handle_session_update( - acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate::new( - nonexistent_id.clone(), - acp::ToolCallUpdateFields::new().status(acp::ToolCallStatus::Completed), - )), - cx, - ); - - // The update should succeed (not return an error) - assert!(result.is_ok()); - - // There should now be exactly one entry in the thread - assert_eq!(thread.entries.len(), 1); - - // The entry should be a failed tool call - if let AgentThreadEntry::ToolCall(tool_call) = &thread.entries[0] { - assert_eq!(tool_call.id, nonexistent_id); - assert!(matches!(tool_call.status, ToolCallStatus::Failed)); - assert_eq!(tool_call.kind, acp::ToolKind::Fetch); - - // Check that the content contains the error message - assert_eq!(tool_call.content.len(), 1); - if let ToolCallContent::ContentBlock(content_block) = &tool_call.content[0] { - match content_block { - ContentBlock::Markdown { markdown } => { - let markdown_text = markdown.read(cx).source(); - assert!(markdown_text.contains("Tool call not found")); - } - ContentBlock::Empty => panic!("Expected markdown content, got empty"), - ContentBlock::ResourceLink { .. } => { - panic!("Expected markdown content, got resource link") - } - } - } else { - panic!("Expected ContentBlock, got: {:?}", tool_call.content[0]); - } - } else { - panic!("Expected ToolCall entry, got: {:?}", thread.entries[0]); - } - }); - } - - /// Tests that restoring a checkpoint properly cleans up terminals that were - /// created after that checkpoint, and cancels any in-progress generation. - /// - /// Reproduces issue #35142: When a checkpoint is restored, any terminal processes - /// that were started after that checkpoint should be terminated, and any in-progress - /// AI generation should be canceled. - #[gpui::test] - async fn test_restore_checkpoint_kills_terminal(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - let connection = Rc::new(FakeAgentConnection::new()); - let thread = cx - .update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx)) - .await - .unwrap(); - - // Send first user message to create a checkpoint - cx.update(|cx| { - thread.update(cx, |thread, cx| { - thread.send(vec!["first message".into()], cx) - }) - }) - .await - .unwrap(); - - // Send second message (creates another checkpoint) - we'll restore to this one - cx.update(|cx| { - thread.update(cx, |thread, cx| { - thread.send(vec!["second message".into()], cx) - }) - }) - .await - .unwrap(); - - // Create 2 terminals BEFORE the checkpoint that have completed running - let terminal_id_1 = acp::TerminalId::new(uuid::Uuid::new_v4().to_string()); - let mock_terminal_1 = cx.new(|cx| { - let builder = ::terminal::TerminalBuilder::new_display_only( - ::terminal::terminal_settings::CursorShape::default(), - ::terminal::terminal_settings::AlternateScroll::On, - None, - 0, - ) - .unwrap(); - builder.subscribe(cx) - }); - - thread.update(cx, |thread, cx| { - thread.on_terminal_provider_event( - TerminalProviderEvent::Created { - terminal_id: terminal_id_1.clone(), - label: "echo 'first'".to_string(), - cwd: Some(PathBuf::from("/test")), - output_byte_limit: None, - terminal: mock_terminal_1.clone(), - }, - cx, - ); - }); - - thread.update(cx, |thread, cx| { - thread.on_terminal_provider_event( - TerminalProviderEvent::Output { - terminal_id: terminal_id_1.clone(), - data: b"first\n".to_vec(), - }, - cx, - ); - }); - - thread.update(cx, |thread, cx| { - thread.on_terminal_provider_event( - TerminalProviderEvent::Exit { - terminal_id: terminal_id_1.clone(), - status: acp::TerminalExitStatus::new().exit_code(0), - }, - cx, - ); - }); - - let terminal_id_2 = acp::TerminalId::new(uuid::Uuid::new_v4().to_string()); - let mock_terminal_2 = cx.new(|cx| { - let builder = ::terminal::TerminalBuilder::new_display_only( - ::terminal::terminal_settings::CursorShape::default(), - ::terminal::terminal_settings::AlternateScroll::On, - None, - 0, - ) - .unwrap(); - builder.subscribe(cx) - }); - - thread.update(cx, |thread, cx| { - thread.on_terminal_provider_event( - TerminalProviderEvent::Created { - terminal_id: terminal_id_2.clone(), - label: "echo 'second'".to_string(), - cwd: Some(PathBuf::from("/test")), - output_byte_limit: None, - terminal: mock_terminal_2.clone(), - }, - cx, - ); - }); - - thread.update(cx, |thread, cx| { - thread.on_terminal_provider_event( - TerminalProviderEvent::Output { - terminal_id: terminal_id_2.clone(), - data: b"second\n".to_vec(), - }, - cx, - ); - }); - - thread.update(cx, |thread, cx| { - thread.on_terminal_provider_event( - TerminalProviderEvent::Exit { - terminal_id: terminal_id_2.clone(), - status: acp::TerminalExitStatus::new().exit_code(0), - }, - cx, - ); - }); - - // Get the second message ID to restore to - let second_message_id = thread.read_with(cx, |thread, _| { - // At this point we have: - // - Index 0: First user message (with checkpoint) - // - Index 1: Second user message (with checkpoint) - // No assistant responses because FakeAgentConnection just returns EndTurn - let AgentThreadEntry::UserMessage(message) = &thread.entries[1] else { - panic!("expected user message at index 1"); - }; - message.id.clone().unwrap() - }); - - // Create a terminal AFTER the checkpoint we'll restore to. - // This simulates the AI agent starting a long-running terminal command. - let terminal_id = acp::TerminalId::new(uuid::Uuid::new_v4().to_string()); - let mock_terminal = cx.new(|cx| { - let builder = ::terminal::TerminalBuilder::new_display_only( - ::terminal::terminal_settings::CursorShape::default(), - ::terminal::terminal_settings::AlternateScroll::On, - None, - 0, - ) - .unwrap(); - builder.subscribe(cx) - }); - - // Register the terminal as created - thread.update(cx, |thread, cx| { - thread.on_terminal_provider_event( - TerminalProviderEvent::Created { - terminal_id: terminal_id.clone(), - label: "sleep 1000".to_string(), - cwd: Some(PathBuf::from("/test")), - output_byte_limit: None, - terminal: mock_terminal.clone(), - }, - cx, - ); - }); - - // Simulate the terminal producing output (still running) - thread.update(cx, |thread, cx| { - thread.on_terminal_provider_event( - TerminalProviderEvent::Output { - terminal_id: terminal_id.clone(), - data: b"terminal is running...\n".to_vec(), - }, - cx, - ); - }); - - // Create a tool call entry that references this terminal - // This represents the agent requesting a terminal command - thread.update(cx, |thread, cx| { - thread - .handle_session_update( - acp::SessionUpdate::ToolCall( - acp::ToolCall::new("terminal-tool-1", "Running command") - .kind(acp::ToolKind::Execute) - .status(acp::ToolCallStatus::InProgress) - .content(vec![acp::ToolCallContent::Terminal(acp::Terminal::new( - terminal_id.clone(), - ))]) - .raw_input(serde_json::json!({"command": "sleep 1000", "cd": "/test"})), - ), - cx, - ) - .unwrap(); - }); - - // Verify terminal exists and is in the thread - let terminal_exists_before = - thread.read_with(cx, |thread, _| thread.terminals.contains_key(&terminal_id)); - assert!( - terminal_exists_before, - "Terminal should exist before checkpoint restore" - ); - - // Verify the terminal's underlying task is still running (not completed) - let terminal_running_before = thread.read_with(cx, |thread, _cx| { - let terminal_entity = thread.terminals.get(&terminal_id).unwrap(); - terminal_entity.read_with(cx, |term, _cx| { - term.output().is_none() // output is None means it's still running - }) - }); - assert!( - terminal_running_before, - "Terminal should be running before checkpoint restore" - ); - - // Verify we have the expected entries before restore - let entry_count_before = thread.read_with(cx, |thread, _| thread.entries.len()); - assert!( - entry_count_before > 1, - "Should have multiple entries before restore" - ); - - // Restore the checkpoint to the second message. - // This should: - // 1. Cancel any in-progress generation (via the cancel() call) - // 2. Remove the terminal that was created after that point - thread - .update(cx, |thread, cx| { - thread.restore_checkpoint(second_message_id, cx) - }) - .await - .unwrap(); - - // Verify that no send_task is in progress after restore - // (cancel() clears the send_task) - let has_send_task_after = thread.read_with(cx, |thread, _| thread.send_task.is_some()); - assert!( - !has_send_task_after, - "Should not have a send_task after restore (cancel should have cleared it)" - ); - - // Verify the entries were truncated (restoring to index 1 truncates at 1, keeping only index 0) - let entry_count = thread.read_with(cx, |thread, _| thread.entries.len()); - assert_eq!( - entry_count, 1, - "Should have 1 entry after restore (only the first user message)" - ); - - // Verify the 2 completed terminals from before the checkpoint still exist - let terminal_1_exists = thread.read_with(cx, |thread, _| { - thread.terminals.contains_key(&terminal_id_1) - }); - assert!( - terminal_1_exists, - "Terminal 1 (from before checkpoint) should still exist" - ); - - let terminal_2_exists = thread.read_with(cx, |thread, _| { - thread.terminals.contains_key(&terminal_id_2) - }); - assert!( - terminal_2_exists, - "Terminal 2 (from before checkpoint) should still exist" - ); - - // Verify they're still in completed state - let terminal_1_completed = thread.read_with(cx, |thread, _cx| { - let terminal_entity = thread.terminals.get(&terminal_id_1).unwrap(); - terminal_entity.read_with(cx, |term, _cx| term.output().is_some()) - }); - assert!(terminal_1_completed, "Terminal 1 should still be completed"); - - let terminal_2_completed = thread.read_with(cx, |thread, _cx| { - let terminal_entity = thread.terminals.get(&terminal_id_2).unwrap(); - terminal_entity.read_with(cx, |term, _cx| term.output().is_some()) - }); - assert!(terminal_2_completed, "Terminal 2 should still be completed"); - - // Verify the running terminal (created after checkpoint) was removed - let terminal_3_exists = - thread.read_with(cx, |thread, _| thread.terminals.contains_key(&terminal_id)); - assert!( - !terminal_3_exists, - "Terminal 3 (created after checkpoint) should have been removed" - ); - - // Verify total count is 2 (the two from before the checkpoint) - let terminal_count = thread.read_with(cx, |thread, _| thread.terminals.len()); - assert_eq!( - terminal_count, 2, - "Should have exactly 2 terminals (the completed ones from before checkpoint)" - ); - } -} diff --git a/crates/acp_thread/src/connection.rs b/crates/acp_thread/src/connection.rs deleted file mode 100644 index 3c8c56b2c0..0000000000 --- a/crates/acp_thread/src/connection.rs +++ /dev/null @@ -1,470 +0,0 @@ -use crate::AcpThread; -use agent_client_protocol::{self as acp}; -use anyhow::Result; -use collections::IndexMap; -use gpui::{Entity, SharedString, Task}; -use language_model::LanguageModelProviderId; -use project::Project; -use serde::{Deserialize, Serialize}; -use std::{any::Any, error::Error, fmt, path::Path, rc::Rc, sync::Arc}; -use ui::{App, IconName}; -use uuid::Uuid; - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Hash)] -pub struct UserMessageId(Arc); - -impl UserMessageId { - pub fn new() -> Self { - Self(Uuid::new_v4().to_string().into()) - } -} - -pub trait AgentConnection { - fn telemetry_id(&self) -> SharedString; - - fn new_thread( - self: Rc, - project: Entity, - cwd: &Path, - cx: &mut App, - ) -> Task>>; - - fn auth_methods(&self) -> &[acp::AuthMethod]; - - fn authenticate(&self, method: acp::AuthMethodId, cx: &mut App) -> Task>; - - fn prompt( - &self, - user_message_id: Option, - params: acp::PromptRequest, - cx: &mut App, - ) -> Task>; - - fn resume( - &self, - _session_id: &acp::SessionId, - _cx: &App, - ) -> Option> { - None - } - - fn cancel(&self, session_id: &acp::SessionId, cx: &mut App); - - fn truncate( - &self, - _session_id: &acp::SessionId, - _cx: &App, - ) -> Option> { - None - } - - fn set_title( - &self, - _session_id: &acp::SessionId, - _cx: &App, - ) -> Option> { - None - } - - /// Returns this agent as an [Rc] if the model selection capability is supported. - /// - /// If the agent does not support model selection, returns [None]. - /// This allows sharing the selector in UI components. - fn model_selector(&self, _session_id: &acp::SessionId) -> Option> { - None - } - - fn telemetry(&self) -> Option> { - None - } - - fn session_modes( - &self, - _session_id: &acp::SessionId, - _cx: &App, - ) -> Option> { - None - } - - fn into_any(self: Rc) -> Rc; -} - -impl dyn AgentConnection { - pub fn downcast(self: Rc) -> Option> { - self.into_any().downcast().ok() - } -} - -pub trait AgentSessionTruncate { - fn run(&self, message_id: UserMessageId, cx: &mut App) -> Task>; -} - -pub trait AgentSessionResume { - fn run(&self, cx: &mut App) -> Task>; -} - -pub trait AgentSessionSetTitle { - fn run(&self, title: SharedString, cx: &mut App) -> Task>; -} - -pub trait AgentTelemetry { - /// A representation of the current thread state that can be serialized for - /// storage with telemetry events. - fn thread_data( - &self, - session_id: &acp::SessionId, - cx: &mut App, - ) -> Task>; -} - -pub trait AgentSessionModes { - fn current_mode(&self) -> acp::SessionModeId; - - fn all_modes(&self) -> Vec; - - fn set_mode(&self, mode: acp::SessionModeId, cx: &mut App) -> Task>; -} - -#[derive(Debug)] -pub struct AuthRequired { - pub description: Option, - pub provider_id: Option, -} - -impl AuthRequired { - pub fn new() -> Self { - Self { - description: None, - provider_id: None, - } - } - - pub fn with_description(mut self, description: String) -> Self { - self.description = Some(description); - self - } - - pub fn with_language_model_provider(mut self, provider_id: LanguageModelProviderId) -> Self { - self.provider_id = Some(provider_id); - self - } -} - -impl Error for AuthRequired {} -impl fmt::Display for AuthRequired { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Authentication required") - } -} - -/// Trait for agents that support listing, selecting, and querying language models. -/// -/// This is an optional capability; agents indicate support via [AgentConnection::model_selector]. -pub trait AgentModelSelector: 'static { - /// Lists all available language models for this agent. - /// - /// # Parameters - /// - `cx`: The GPUI app context for async operations and global access. - /// - /// # Returns - /// A task resolving to the list of models or an error (e.g., if no models are configured). - fn list_models(&self, cx: &mut App) -> Task>; - - /// Selects a model for a specific session (thread). - /// - /// This sets the default model for future interactions in the session. - /// If the session doesn't exist or the model is invalid, it returns an error. - /// - /// # Parameters - /// - `model`: The model to select (should be one from [list_models]). - /// - `cx`: The GPUI app context. - /// - /// # Returns - /// A task resolving to `Ok(())` on success or an error. - fn select_model(&self, model_id: acp::ModelId, cx: &mut App) -> Task>; - - /// Retrieves the currently selected model for a specific session (thread). - /// - /// # Parameters - /// - `cx`: The GPUI app context. - /// - /// # Returns - /// A task resolving to the selected model (always set) or an error (e.g., session not found). - fn selected_model(&self, cx: &mut App) -> Task>; - - /// Whenever the model list is updated the receiver will be notified. - /// Optional for agents that don't update their model list. - fn watch(&self, _cx: &mut App) -> Option> { - None - } - - /// Returns whether the model picker should render a footer. - fn should_render_footer(&self) -> bool { - false - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct AgentModelInfo { - pub id: acp::ModelId, - pub name: SharedString, - pub description: Option, - pub icon: Option, -} - -impl From for AgentModelInfo { - fn from(info: acp::ModelInfo) -> Self { - Self { - id: info.model_id, - name: info.name.into(), - description: info.description.map(|desc| desc.into()), - icon: None, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct AgentModelGroupName(pub SharedString); - -#[derive(Debug, Clone)] -pub enum AgentModelList { - Flat(Vec), - Grouped(IndexMap>), -} - -impl AgentModelList { - pub fn is_empty(&self) -> bool { - match self { - AgentModelList::Flat(models) => models.is_empty(), - AgentModelList::Grouped(groups) => groups.is_empty(), - } - } -} - -#[cfg(feature = "test-support")] -mod test_support { - use std::sync::Arc; - - use action_log::ActionLog; - use collections::HashMap; - use futures::{channel::oneshot, future::try_join_all}; - use gpui::{AppContext as _, WeakEntity}; - use parking_lot::Mutex; - - use super::*; - - #[derive(Clone, Default)] - pub struct StubAgentConnection { - sessions: Arc>>, - permission_requests: HashMap>, - next_prompt_updates: Arc>>, - } - - struct Session { - thread: WeakEntity, - response_tx: Option>, - } - - impl StubAgentConnection { - pub fn new() -> Self { - Self { - next_prompt_updates: Default::default(), - permission_requests: HashMap::default(), - sessions: Arc::default(), - } - } - - pub fn set_next_prompt_updates(&self, updates: Vec) { - *self.next_prompt_updates.lock() = updates; - } - - pub fn with_permission_requests( - mut self, - permission_requests: HashMap>, - ) -> Self { - self.permission_requests = permission_requests; - self - } - - pub fn send_update( - &self, - session_id: acp::SessionId, - update: acp::SessionUpdate, - cx: &mut App, - ) { - assert!( - self.next_prompt_updates.lock().is_empty(), - "Use either send_update or set_next_prompt_updates" - ); - - self.sessions - .lock() - .get(&session_id) - .unwrap() - .thread - .update(cx, |thread, cx| { - thread.handle_session_update(update, cx).unwrap(); - }) - .unwrap(); - } - - pub fn end_turn(&self, session_id: acp::SessionId, stop_reason: acp::StopReason) { - self.sessions - .lock() - .get_mut(&session_id) - .unwrap() - .response_tx - .take() - .expect("No pending turn") - .send(stop_reason) - .unwrap(); - } - } - - impl AgentConnection for StubAgentConnection { - fn telemetry_id(&self) -> SharedString { - "stub".into() - } - - fn auth_methods(&self) -> &[acp::AuthMethod] { - &[] - } - - fn new_thread( - self: Rc, - project: Entity, - _cwd: &Path, - cx: &mut gpui::App, - ) -> Task>> { - let session_id = acp::SessionId::new(self.sessions.lock().len().to_string()); - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let thread = cx.new(|cx| { - AcpThread::new( - "Test", - self.clone(), - project, - action_log, - session_id.clone(), - watch::Receiver::constant( - acp::PromptCapabilities::new() - .image(true) - .audio(true) - .embedded_context(true), - ), - cx, - ) - }); - self.sessions.lock().insert( - session_id, - Session { - thread: thread.downgrade(), - response_tx: None, - }, - ); - Task::ready(Ok(thread)) - } - - fn authenticate( - &self, - _method_id: acp::AuthMethodId, - _cx: &mut App, - ) -> Task> { - unimplemented!() - } - - fn prompt( - &self, - _id: Option, - params: acp::PromptRequest, - cx: &mut App, - ) -> Task> { - let mut sessions = self.sessions.lock(); - let Session { - thread, - response_tx, - } = sessions.get_mut(¶ms.session_id).unwrap(); - let mut tasks = vec![]; - if self.next_prompt_updates.lock().is_empty() { - let (tx, rx) = oneshot::channel(); - response_tx.replace(tx); - cx.spawn(async move |_| { - let stop_reason = rx.await?; - Ok(acp::PromptResponse::new(stop_reason)) - }) - } else { - for update in self.next_prompt_updates.lock().drain(..) { - let thread = thread.clone(); - let update = update.clone(); - let permission_request = if let acp::SessionUpdate::ToolCall(tool_call) = - &update - && let Some(options) = self.permission_requests.get(&tool_call.tool_call_id) - { - Some((tool_call.clone(), options.clone())) - } else { - None - }; - let task = cx.spawn(async move |cx| { - if let Some((tool_call, options)) = permission_request { - thread - .update(cx, |thread, cx| { - thread.request_tool_call_authorization( - tool_call.clone().into(), - options.clone(), - false, - cx, - ) - })?? - .await; - } - thread.update(cx, |thread, cx| { - thread.handle_session_update(update.clone(), cx).unwrap(); - })?; - anyhow::Ok(()) - }); - tasks.push(task); - } - - cx.spawn(async move |_| { - try_join_all(tasks).await?; - Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)) - }) - } - } - - fn cancel(&self, session_id: &acp::SessionId, _cx: &mut App) { - if let Some(end_turn_tx) = self - .sessions - .lock() - .get_mut(session_id) - .unwrap() - .response_tx - .take() - { - end_turn_tx.send(acp::StopReason::Cancelled).unwrap(); - } - } - - fn truncate( - &self, - _session_id: &agent_client_protocol::SessionId, - _cx: &App, - ) -> Option> { - Some(Rc::new(StubAgentSessionEditor)) - } - - fn into_any(self: Rc) -> Rc { - self - } - } - - struct StubAgentSessionEditor; - - impl AgentSessionTruncate for StubAgentSessionEditor { - fn run(&self, _: UserMessageId, _: &mut App) -> Task> { - Task::ready(Ok(())) - } - } -} - -#[cfg(feature = "test-support")] -pub use test_support::*; diff --git a/crates/acp_thread/src/diff.rs b/crates/acp_thread/src/diff.rs deleted file mode 100644 index f17e9d0fce..0000000000 --- a/crates/acp_thread/src/diff.rs +++ /dev/null @@ -1,432 +0,0 @@ -use anyhow::Result; -use buffer_diff::{BufferDiff, BufferDiffSnapshot}; -use editor::{MultiBuffer, PathKey, multibuffer_context_lines}; -use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task}; -use itertools::Itertools; -use language::{ - Anchor, Buffer, Capability, LanguageRegistry, OffsetRangeExt as _, Point, Rope, TextBuffer, -}; -use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; -use util::ResultExt; - -pub enum Diff { - Pending(PendingDiff), - Finalized(FinalizedDiff), -} - -impl Diff { - pub fn finalized( - path: String, - old_text: Option, - new_text: String, - language_registry: Arc, - 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 task = cx.spawn({ - let multibuffer = multibuffer.clone(); - let path = path.clone(); - let buffer = new_buffer.clone(); - async move |_, cx| { - let language = language_registry - .load_language_for_file_path(Path::new(&path)) - .await - .log_err(); - - buffer.update(cx, |buffer, cx| buffer.set_language(language.clone(), cx))?; - - let diff = build_buffer_diff( - old_text.unwrap_or("".into()).into(), - &buffer, - Some(language_registry.clone()), - cx, - ) - .await?; - - multibuffer - .update(cx, |multibuffer, cx| { - let hunk_ranges = { - let buffer = buffer.read(cx); - let diff = diff.read(cx); - diff.hunks_intersecting_range( - Anchor::min_for_buffer(buffer.remote_id()) - ..Anchor::max_for_buffer(buffer.remote_id()), - buffer, - cx, - ) - .map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer)) - .collect::>() - }; - - multibuffer.set_excerpts_for_path( - PathKey::for_buffer(&buffer, cx), - buffer.clone(), - hunk_ranges, - multibuffer_context_lines(cx), - cx, - ); - multibuffer.add_diff(diff, cx); - }) - .log_err(); - - anyhow::Ok(()) - } - }); - - 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_diff = cx.new(|cx| { - let mut diff = BufferDiff::new_unchanged(&buffer_text_snapshot, base_text_snapshot); - let snapshot = diff.snapshot(cx); - let secondary_diff = cx.new(|cx| { - let mut diff = BufferDiff::new(&buffer_text_snapshot, cx); - diff.set_snapshot(snapshot, &buffer_text_snapshot, cx); - diff - }); - diff.set_secondary_diff(secondary_diff); - diff - }); - - let multibuffer = cx.new(|cx| { - let mut multibuffer = MultiBuffer::without_headers(Capability::ReadOnly); - multibuffer.add_diff(buffer_diff.clone(), cx); - multibuffer - }); - - Self::Pending(PendingDiff { - multibuffer, - base_text: Arc::new(base_text), - _subscription: cx.observe(&buffer, |this, _, cx| { - if let Diff::Pending(diff) = this { - diff.update(cx); - } - }), - new_buffer: buffer, - diff: buffer_diff, - revealed_ranges: Vec::new(), - update_diff: Task::ready(Ok(())), - }) - } - - pub fn reveal_range(&mut self, range: Range, cx: &mut Context) { - if let Self::Pending(diff) = self { - diff.reveal_range(range, cx); - } - } - - pub fn finalize(&mut self, cx: &mut Context) { - if let Self::Pending(diff) = self { - *self = Self::Finalized(diff.finalize(cx)); - } - } - - pub fn multibuffer(&self) -> &Entity { - match self { - Self::Pending(PendingDiff { multibuffer, .. }) => multibuffer, - Self::Finalized(FinalizedDiff { multibuffer, .. }) => multibuffer, - } - } - - pub fn to_markdown(&self, cx: &App) -> String { - let buffer_text = self - .multibuffer() - .read(cx) - .all_buffers() - .iter() - .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().display(file.path_style(cx))), - Diff::Finalized(FinalizedDiff { path, .. }) => Some(path.as_str().into()), - }; - format!( - "Diff: {}\n```\n{}\n```\n", - path.unwrap_or("untitled".into()), - buffer_text - ) - } - - 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, - diff: Entity, - revealed_ranges: Vec>, - _subscription: Subscription, - update_diff: Task>, -} - -impl PendingDiff { - pub fn update(&mut self, cx: &mut Context) { - let buffer = self.new_buffer.clone(); - let buffer_diff = self.diff.clone(); - let base_text = self.base_text.clone(); - self.update_diff = cx.spawn(async move |diff, cx| { - let text_snapshot = buffer.read_with(cx, |buffer, _| buffer.text_snapshot())?; - let diff_snapshot = BufferDiff::update_diff( - buffer_diff.clone(), - text_snapshot.clone(), - Some(base_text), - false, - false, - None, - None, - cx, - ) - .await?; - buffer_diff.update(cx, |diff, cx| { - diff.set_snapshot(diff_snapshot.clone(), &text_snapshot, cx); - diff.secondary_diff().unwrap().update(cx, |diff, cx| { - diff.set_snapshot(diff_snapshot.clone(), &text_snapshot, cx); - }); - })?; - diff.update(cx, |diff, cx| { - if let Diff::Pending(diff) = diff { - diff.update_visible_ranges(cx); - } - }) - }); - } - - pub fn reveal_range(&mut self, range: Range, cx: &mut Context) { - self.revealed_ranges.push(range); - self.update_visible_ranges(cx); - } - - fn finalize(&self, cx: &mut Context) -> FinalizedDiff { - let ranges = self.excerpt_ranges(cx); - let base_text = self.base_text.clone(); - let new_buffer = self.new_buffer.read(cx); - let language_registry = new_buffer.language_registry(); - - let path = new_buffer - .file() - .map(|file| file.path().display(file.path_style(cx))) - .unwrap_or("untitled".into()) - .into(); - let replica_id = new_buffer.replica_id(); - - // Replace the buffer in the multibuffer with the snapshot - let buffer = cx.new(|cx| { - let language = self.new_buffer.read(cx).language().cloned(); - let buffer = TextBuffer::new_normalized( - replica_id, - cx.entity_id().as_non_zero_u64().into(), - self.new_buffer.read(cx).line_ending(), - self.new_buffer.read(cx).as_rope().clone(), - ); - let mut buffer = Buffer::build(buffer, None, Capability::ReadWrite); - buffer.set_language(language, cx); - buffer - }); - - let buffer_diff = cx.spawn({ - let buffer = buffer.clone(); - async move |_this, cx| { - build_buffer_diff(base_text, &buffer, language_registry, cx).await - } - }); - - let update_diff = cx.spawn(async move |this, cx| { - let buffer_diff = buffer_diff.await?; - this.update(cx, |this, cx| { - this.multibuffer().update(cx, |multibuffer, cx| { - let path_key = PathKey::for_buffer(&buffer, cx); - multibuffer.clear(cx); - multibuffer.set_excerpts_for_path( - path_key, - buffer, - ranges, - multibuffer_context_lines(cx), - cx, - ); - multibuffer.add_diff(buffer_diff.clone(), cx); - }); - - cx.notify(); - }) - }); - - FinalizedDiff { - path, - base_text: self.base_text.clone(), - multibuffer: self.multibuffer.clone(), - new_buffer: self.new_buffer.clone(), - _update_diff: update_diff, - } - } - - fn update_visible_ranges(&mut self, cx: &mut Context) { - 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(), - ranges, - multibuffer_context_lines(cx), - cx, - ); - let end = multibuffer.len(cx); - Some(multibuffer.snapshot(cx).offset_to_point(end).row + 1) - }); - cx.notify(); - } - - fn excerpt_ranges(&self, cx: &App) -> Vec> { - let buffer = self.new_buffer.read(cx); - let diff = self.diff.read(cx); - let mut ranges = diff - .hunks_intersecting_range( - Anchor::min_for_buffer(buffer.remote_id()) - ..Anchor::max_for_buffer(buffer.remote_id()), - buffer, - cx, - ) - .map(|diff_hunk| diff_hunk.buffer_range.to_point(buffer)) - .collect::>(); - ranges.extend( - self.revealed_ranges - .iter() - .map(|range| range.to_point(buffer)), - ); - ranges.sort_unstable_by_key(|range| (range.start, Reverse(range.end))); - - // Merge adjacent ranges - let mut ranges = ranges.into_iter().peekable(); - let mut merged_ranges = Vec::new(); - while let Some(mut range) = ranges.next() { - while let Some(next_range) = ranges.peek() { - if range.end >= next_range.start { - range.end = range.end.max(next_range.end); - ranges.next(); - } else { - break; - } - } - - merged_ranges.push(range); - } - merged_ranges - } -} - -pub struct FinalizedDiff { - path: String, - base_text: Arc, - new_buffer: Entity, - multibuffer: Entity, - _update_diff: Task>, -} - -async fn build_buffer_diff( - old_text: Arc, - buffer: &Entity, - language_registry: Option>, - cx: &mut AsyncApp, -) -> Result> { - let buffer = cx.update(|cx| buffer.read(cx).snapshot())?; - - let old_text_rope = cx - .background_spawn({ - let old_text = old_text.clone(); - async move { Rope::from(old_text.as_str()) } - }) - .await; - let base_buffer = cx - .update(|cx| { - Buffer::build_snapshot( - old_text_rope, - buffer.language().cloned(), - language_registry, - cx, - ) - })? - .await; - - let diff_snapshot = cx - .update(|cx| { - BufferDiffSnapshot::new_with_base_buffer( - buffer.text.clone(), - Some(old_text), - base_buffer, - cx, - ) - })? - .await; - - let secondary_diff = cx.new(|cx| { - let mut diff = BufferDiff::new(&buffer, cx); - diff.set_snapshot(diff_snapshot.clone(), &buffer, cx); - diff - })?; - - cx.new(|cx| { - let mut diff = BufferDiff::new(&buffer.text, cx); - diff.set_snapshot(diff_snapshot, &buffer, cx); - diff.set_secondary_diff(secondary_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 deleted file mode 100644 index c1b7032cfa..0000000000 --- a/crates/acp_thread/src/mention.rs +++ /dev/null @@ -1,551 +0,0 @@ -use agent_client_protocol as acp; -use anyhow::{Context as _, Result, bail}; -use file_icons::FileIcons; -use prompt_store::{PromptId, UserPromptId}; -use serde::{Deserialize, Serialize}; -use std::{ - fmt, - ops::RangeInclusive, - path::{Path, PathBuf}, -}; -use ui::{App, IconName, SharedString}; -use url::Url; -use util::paths::PathStyle; - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)] -pub enum MentionUri { - File { - abs_path: PathBuf, - }, - PastedImage, - Directory { - abs_path: PathBuf, - }, - Symbol { - abs_path: PathBuf, - name: String, - line_range: RangeInclusive, - }, - Thread { - id: acp::SessionId, - name: String, - }, - TextThread { - path: PathBuf, - name: String, - }, - Rule { - id: PromptId, - name: String, - }, - Selection { - #[serde(default, skip_serializing_if = "Option::is_none")] - abs_path: Option, - line_range: RangeInclusive, - }, - Fetch { - url: Url, - }, -} - -impl MentionUri { - pub fn parse(input: &str, path_style: PathStyle) -> 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 = if path_style.is_windows() { - path.trim_start_matches("/") - } else { - path - }; - - if let Some(fragment) = url.fragment() { - let line_range = parse_line_range(fragment)?; - if let Some(name) = single_query_param(&url, "symbol")? { - Ok(Self::Symbol { - name, - abs_path: path.into(), - line_range, - }) - } else { - Ok(Self::Selection { - abs_path: Some(path.into()), - line_range, - }) - } - } else if input.ends_with("/") { - Ok(Self::Directory { - abs_path: path.into(), - }) - } else { - Ok(Self::File { - abs_path: path.into(), - }) - } - } - "zed" => { - if let Some(thread_id) = path.strip_prefix("/agent/thread/") { - let name = single_query_param(&url, "name")?.context("Missing thread name")?; - Ok(Self::Thread { - id: acp::SessionId::new(thread_id), - name, - }) - } else if let Some(path) = path.strip_prefix("/agent/text-thread/") { - let name = single_query_param(&url, "name")?.context("Missing thread name")?; - Ok(Self::TextThread { - path: path.into(), - name, - }) - } else if let Some(rule_id) = path.strip_prefix("/agent/rule/") { - let name = single_query_param(&url, "name")?.context("Missing rule name")?; - let rule_id = UserPromptId(rule_id.parse()?); - Ok(Self::Rule { - 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 if let Some(name) = path.strip_prefix("/agent/symbol/") { - let fragment = url - .fragment() - .context("Missing fragment for untitled buffer selection")?; - let line_range = parse_line_range(fragment)?; - let path = - single_query_param(&url, "path")?.context("Missing path for symbol")?; - Ok(Self::Symbol { - name: name.to_string(), - abs_path: path.into(), - line_range, - }) - } else if path.starts_with("/agent/file") { - let path = - single_query_param(&url, "path")?.context("Missing path for file")?; - Ok(Self::File { - abs_path: path.into(), - }) - } else if path.starts_with("/agent/directory") { - let path = - single_query_param(&url, "path")?.context("Missing path for directory")?; - Ok(Self::Directory { - abs_path: path.into(), - }) - } else if path.starts_with("/agent/selection") { - let fragment = url.fragment().context("Missing fragment for selection")?; - let line_range = parse_line_range(fragment)?; - let path = - single_query_param(&url, "path")?.context("Missing path for selection")?; - Ok(Self::Selection { - abs_path: Some(path.into()), - line_range, - }) - } else { - bail!("invalid zed url: {:?}", input); - } - } - "http" | "https" => Ok(MentionUri::Fetch { url }), - other => bail!("unrecognized scheme {:?}", other), - } - } - - pub fn name(&self) -> String { - match self { - MentionUri::File { abs_path, .. } | MentionUri::Directory { abs_path, .. } => abs_path - .file_name() - .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), - MentionUri::Fetch { url } => url.to_string(), - } - } - - pub fn icon_path(&self, cx: &mut App) -> SharedString { - match self { - 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 { abs_path } => FileIcons::get_folder_icon(false, abs_path, cx) - .unwrap_or_else(|| IconName::Folder.path().into()), - MentionUri::Symbol { .. } => IconName::Code.path().into(), - MentionUri::Thread { .. } => IconName::Thread.path().into(), - MentionUri::TextThread { .. } => IconName::Thread.path().into(), - MentionUri::Rule { .. } => IconName::Reader.path().into(), - MentionUri::Selection { .. } => IconName::Reader.path().into(), - MentionUri::Fetch { .. } => IconName::ToolWeb.path().into(), - } - } - - pub fn as_link<'a>(&'a self) -> MentionLink<'a> { - MentionLink(self) - } - - pub fn to_uri(&self) -> Url { - match self { - MentionUri::File { abs_path } => { - let mut url = Url::parse("file:///").unwrap(); - url.set_path(&abs_path.to_string_lossy()); - url - } - MentionUri::PastedImage => Url::parse("zed:///agent/pasted-image").unwrap(), - MentionUri::Directory { abs_path } => { - let mut url = Url::parse("file:///").unwrap(); - url.set_path(&abs_path.to_string_lossy()); - url - } - MentionUri::Symbol { - abs_path, - name, - line_range, - } => { - let mut url = Url::parse("file:///").unwrap(); - url.set_path(&abs_path.to_string_lossy()); - url.query_pairs_mut().append_pair("symbol", name); - url.set_fragment(Some(&format!( - "L{}:{}", - line_range.start() + 1, - line_range.end() + 1 - ))); - url - } - MentionUri::Selection { - abs_path, - line_range, - } => { - let mut url = if let Some(path) = abs_path { - let mut url = Url::parse("file:///").unwrap(); - url.set_path(&path.to_string_lossy()); - url - } else { - let mut url = Url::parse("zed:///").unwrap(); - url.set_path("/agent/untitled-buffer"); - url - }; - url.set_fragment(Some(&format!( - "L{}:{}", - line_range.start() + 1, - line_range.end() + 1 - ))); - url - } - MentionUri::Thread { name, id } => { - let mut url = Url::parse("zed:///").unwrap(); - url.set_path(&format!("/agent/thread/{id}")); - url.query_pairs_mut().append_pair("name", name); - url - } - 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.query_pairs_mut().append_pair("name", name); - url - } - MentionUri::Rule { name, id } => { - let mut url = Url::parse("zed:///").unwrap(); - url.set_path(&format!("/agent/rule/{id}")); - url.query_pairs_mut().append_pair("name", name); - url - } - MentionUri::Fetch { url } => url.clone(), - } - } -} - -pub struct MentionLink<'a>(&'a MentionUri); - -impl fmt::Display for MentionLink<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "[@{}]({})", self.0.name(), self.0.to_uri()) - } -} - -fn single_query_param(url: &Url, name: &'static str) -> Result> { - let pairs = url.query_pairs().collect::>(); - match pairs.as_slice() { - [] => Ok(None), - [(k, v)] => { - if k != name { - bail!("invalid query parameter") - } - - Ok(Some(v.to_string())) - } - _ => bail!("too many query pairs"), - } -} - -pub fn selection_name(path: Option<&Path>, line_range: &RangeInclusive) -> String { - format!( - "{} ({}:{})", - path.and_then(|path| path.file_name()) - .unwrap_or("Untitled".as_ref()) - .display(), - *line_range.start() + 1, - *line_range.end() + 1 - ) -} - -#[cfg(test)] -mod tests { - use util::{path, uri}; - - use super::*; - - #[test] - fn test_parse_file_uri() { - let file_uri = uri!("file:///path/to/file.rs"); - let parsed = MentionUri::parse(file_uri, PathStyle::local()).unwrap(); - match &parsed { - MentionUri::File { abs_path } => { - assert_eq!(abs_path, Path::new(path!("/path/to/file.rs"))); - } - _ => panic!("Expected File variant"), - } - assert_eq!(parsed.to_uri().to_string(), file_uri); - } - - #[test] - fn test_parse_directory_uri() { - let file_uri = uri!("file:///path/to/dir/"); - let parsed = MentionUri::parse(file_uri, PathStyle::local()).unwrap(); - match &parsed { - MentionUri::Directory { abs_path } => { - assert_eq!(abs_path, Path::new(path!("/path/to/dir/"))); - } - _ => panic!("Expected Directory variant"), - } - assert_eq!(parsed.to_uri().to_string(), file_uri); - } - - #[test] - fn test_to_directory_uri_without_slash() { - let uri = MentionUri::Directory { - abs_path: PathBuf::from(path!("/path/to/dir/")), - }; - let expected = uri!("file:///path/to/dir/"); - assert_eq!(uri.to_uri().to_string(), expected); - } - - #[test] - fn test_parse_symbol_uri() { - let symbol_uri = uri!("file:///path/to/file.rs?symbol=MySymbol#L10:20"); - let parsed = MentionUri::parse(symbol_uri, PathStyle::local()).unwrap(); - match &parsed { - MentionUri::Symbol { - abs_path: path, - name, - line_range, - } => { - assert_eq!(path, Path::new(path!("/path/to/file.rs"))); - assert_eq!(name, "MySymbol"); - assert_eq!(line_range.start(), &9); - assert_eq!(line_range.end(), &19); - } - _ => panic!("Expected Symbol variant"), - } - assert_eq!(parsed.to_uri().to_string(), symbol_uri); - } - - #[test] - fn test_parse_selection_uri() { - let selection_uri = uri!("file:///path/to/file.rs#L5:15"); - let parsed = MentionUri::parse(selection_uri, PathStyle::local()).unwrap(); - match &parsed { - MentionUri::Selection { - abs_path: path, - line_range, - } => { - assert_eq!(path.as_ref().unwrap(), Path::new(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, PathStyle::local()).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"; - let parsed = MentionUri::parse(thread_uri, PathStyle::local()).unwrap(); - match &parsed { - MentionUri::Thread { - id: thread_id, - name, - } => { - assert_eq!(thread_id.to_string(), "session123"); - assert_eq!(name, "Thread name"); - } - _ => panic!("Expected Thread variant"), - } - assert_eq!(parsed.to_uri().to_string(), thread_uri); - } - - #[test] - fn test_parse_rule_uri() { - let rule_uri = "zed:///agent/rule/d8694ff2-90d5-4b6f-be33-33c1763acd52?name=Some+rule"; - let parsed = MentionUri::parse(rule_uri, PathStyle::local()).unwrap(); - match &parsed { - MentionUri::Rule { id, name } => { - assert_eq!(id.to_string(), "d8694ff2-90d5-4b6f-be33-33c1763acd52"); - assert_eq!(name, "Some rule"); - } - _ => panic!("Expected Rule variant"), - } - assert_eq!(parsed.to_uri().to_string(), rule_uri); - } - - #[test] - fn test_parse_fetch_http_uri() { - let http_uri = "http://example.com/path?query=value#fragment"; - let parsed = MentionUri::parse(http_uri, PathStyle::local()).unwrap(); - match &parsed { - MentionUri::Fetch { url } => { - assert_eq!(url.to_string(), http_uri); - } - _ => panic!("Expected Fetch variant"), - } - assert_eq!(parsed.to_uri().to_string(), http_uri); - } - - #[test] - fn test_parse_fetch_https_uri() { - let https_uri = "https://example.com/api/endpoint"; - let parsed = MentionUri::parse(https_uri, PathStyle::local()).unwrap(); - match &parsed { - MentionUri::Fetch { url } => { - assert_eq!(url.to_string(), https_uri); - } - _ => panic!("Expected Fetch variant"), - } - assert_eq!(parsed.to_uri().to_string(), https_uri); - } - - #[test] - fn test_invalid_scheme() { - assert!(MentionUri::parse("ftp://example.com", PathStyle::local()).is_err()); - assert!(MentionUri::parse("ssh://example.com", PathStyle::local()).is_err()); - assert!(MentionUri::parse("unknown://example.com", PathStyle::local()).is_err()); - } - - #[test] - fn test_invalid_zed_path() { - assert!(MentionUri::parse("zed:///invalid/path", PathStyle::local()).is_err()); - assert!(MentionUri::parse("zed:///agent/unknown/test", PathStyle::local()).is_err()); - } - - #[test] - fn test_invalid_line_range_format() { - // Missing L prefix - assert!( - MentionUri::parse(uri!("file:///path/to/file.rs#10:20"), PathStyle::local()).is_err() - ); - - // Missing colon separator - assert!( - MentionUri::parse(uri!("file:///path/to/file.rs#L1020"), PathStyle::local()).is_err() - ); - - // Invalid numbers - assert!( - MentionUri::parse(uri!("file:///path/to/file.rs#L10:abc"), PathStyle::local()).is_err() - ); - assert!( - MentionUri::parse(uri!("file:///path/to/file.rs#Labc:20"), PathStyle::local()).is_err() - ); - } - - #[test] - fn test_invalid_query_parameters() { - // Invalid query parameter name - assert!( - MentionUri::parse( - uri!("file:///path/to/file.rs#L10:20?invalid=test"), - PathStyle::local() - ) - .is_err() - ); - - // Too many query parameters - assert!( - MentionUri::parse( - uri!("file:///path/to/file.rs#L10:20?symbol=test&another=param"), - PathStyle::local() - ) - .is_err() - ); - } - - #[test] - fn test_zero_based_line_numbers() { - // Test that 0-based line numbers are rejected (should be 1-based) - assert!( - MentionUri::parse(uri!("file:///path/to/file.rs#L0:10"), PathStyle::local()).is_err() - ); - assert!( - MentionUri::parse(uri!("file:///path/to/file.rs#L1:0"), PathStyle::local()).is_err() - ); - assert!( - MentionUri::parse(uri!("file:///path/to/file.rs#L0:0"), PathStyle::local()).is_err() - ); - } -} diff --git a/crates/acp_thread/src/terminal.rs b/crates/acp_thread/src/terminal.rs deleted file mode 100644 index 2da4125209..0000000000 --- a/crates/acp_thread/src/terminal.rs +++ /dev/null @@ -1,224 +0,0 @@ -use agent_client_protocol as acp; -use anyhow::Result; -use futures::{FutureExt as _, future::Shared}; -use gpui::{App, AppContext, AsyncApp, Context, Entity, Task}; -use language::LanguageRegistry; -use markdown::Markdown; -use project::Project; -use std::{path::PathBuf, process::ExitStatus, sync::Arc, time::Instant}; -use task::Shell; -use util::get_default_system_shell_preferring_bash; - -pub struct Terminal { - id: acp::TerminalId, - command: Entity, - working_dir: Option, - terminal: Entity, - started_at: Instant, - output: Option, - output_byte_limit: Option, - _output_task: Shared>, -} - -pub struct TerminalOutput { - pub ended_at: Instant, - pub exit_status: Option, - pub content: String, - pub original_content_len: usize, - pub content_line_count: usize, -} - -impl Terminal { - pub fn new( - id: acp::TerminalId, - command_label: &str, - working_dir: Option, - output_byte_limit: Option, - terminal: Entity, - language_registry: Arc, - cx: &mut Context, - ) -> Self { - let command_task = terminal.read(cx).wait_for_completed_task(cx); - Self { - id, - command: cx.new(|cx| { - Markdown::new( - format!("```\n{}\n```", command_label).into(), - Some(language_registry.clone()), - None, - cx, - ) - }), - working_dir, - terminal, - started_at: Instant::now(), - output: None, - output_byte_limit, - _output_task: cx - .spawn(async move |this, cx| { - let exit_status = command_task.await; - - this.update(cx, |this, cx| { - let (content, original_content_len) = this.truncated_output(cx); - let content_line_count = this.terminal.read(cx).total_lines(); - - this.output = Some(TerminalOutput { - ended_at: Instant::now(), - exit_status, - content, - original_content_len, - content_line_count, - }); - cx.notify(); - }) - .ok(); - - let exit_status = exit_status.map(portable_pty::ExitStatus::from); - - acp::TerminalExitStatus::new() - .exit_code(exit_status.as_ref().map(|e| e.exit_code())) - .signal(exit_status.and_then(|e| e.signal().map(ToOwned::to_owned))) - }) - .shared(), - } - } - - pub fn id(&self) -> &acp::TerminalId { - &self.id - } - - pub fn wait_for_exit(&self) -> Shared> { - self._output_task.clone() - } - - pub fn kill(&mut self, cx: &mut App) { - self.terminal.update(cx, |terminal, _cx| { - terminal.kill_active_task(); - }); - } - - pub fn current_output(&self, cx: &App) -> acp::TerminalOutputResponse { - if let Some(output) = self.output.as_ref() { - let exit_status = output.exit_status.map(portable_pty::ExitStatus::from); - - acp::TerminalOutputResponse::new( - output.content.clone(), - output.original_content_len > output.content.len(), - ) - .exit_status( - acp::TerminalExitStatus::new() - .exit_code(exit_status.as_ref().map(|e| e.exit_code())) - .signal(exit_status.and_then(|e| e.signal().map(ToOwned::to_owned))), - ) - } else { - let (current_content, original_len) = self.truncated_output(cx); - let truncated = current_content.len() < original_len; - acp::TerminalOutputResponse::new(current_content, truncated) - } - } - - fn truncated_output(&self, cx: &App) -> (String, usize) { - let terminal = self.terminal.read(cx); - let mut content = terminal.get_content(); - - let original_content_len = content.len(); - - if let Some(limit) = self.output_byte_limit - && content.len() > limit - { - let mut end_ix = limit.min(content.len()); - while !content.is_char_boundary(end_ix) { - end_ix -= 1; - } - // Don't truncate mid-line, clear the remainder of the last line - end_ix = content[..end_ix].rfind('\n').unwrap_or(end_ix); - content.truncate(end_ix); - } - - (content, original_content_len) - } - - pub fn command(&self) -> &Entity { - &self.command - } - - pub fn working_dir(&self) -> &Option { - &self.working_dir - } - - pub fn started_at(&self) -> Instant { - self.started_at - } - - pub fn output(&self) -> Option<&TerminalOutput> { - self.output.as_ref() - } - - pub fn inner(&self) -> &Entity { - &self.terminal - } - - pub fn to_markdown(&self, cx: &App) -> String { - format!( - "Terminal:\n```\n{}\n```\n", - self.terminal.read(cx).get_content() - ) - } -} - -pub async fn create_terminal_entity( - command: String, - args: &[String], - env_vars: Vec<(String, String)>, - cwd: Option, - project: &Entity, - cx: &mut AsyncApp, -) -> Result> { - let mut env = if let Some(dir) = &cwd { - project - .update(cx, |project, cx| { - project.environment().update(cx, |env, cx| { - env.directory_environment(dir.clone().into(), cx) - }) - })? - .await - .unwrap_or_default() - } else { - Default::default() - }; - - // Disables paging for `git` and hopefully other commands - env.insert("PAGER".into(), "".into()); - env.extend(env_vars); - - // Use remote shell or default system shell, as appropriate - let shell = project - .update(cx, |project, cx| { - project - .remote_client() - .and_then(|r| r.read(cx).default_system_shell()) - .map(Shell::Program) - })? - .unwrap_or_else(|| Shell::Program(get_default_system_shell_preferring_bash())); - let is_windows = project - .read_with(cx, |project, cx| project.path_style(cx).is_windows()) - .unwrap_or(cfg!(windows)); - let (task_command, task_args) = task::ShellBuilder::new(&shell, is_windows) - .redirect_stdin_to_dev_null() - .build(Some(command.clone()), &args); - - project - .update(cx, |project, cx| { - project.create_terminal_task( - task::SpawnInTerminal { - command: Some(task_command), - args: task_args, - cwd, - env, - ..Default::default() - }, - cx, - ) - })? - .await -} diff --git a/crates/acp_tools/Cargo.toml b/crates/acp_tools/Cargo.toml deleted file mode 100644 index 0720c4b668..0000000000 --- a/crates/acp_tools/Cargo.toml +++ /dev/null @@ -1,29 +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.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 0905effce3..0000000000 --- a/crates/acp_tools/src/acp_tools.rs +++ /dev/null @@ -1,643 +0,0 @@ -use std::{ - cell::RefCell, - collections::HashSet, - fmt::Display, - rc::{Rc, Weak}, - sync::Arc, - time::Duration, -}; - -use agent_client_protocol as acp; -use collections::HashMap; -use gpui::{ - App, ClipboardItem, 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::{Tooltip, WithScrollbar, prelude::*}; -use util::ResultExt as _; -use workspace::{ - Item, ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, 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.clone(), 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 serialize_observed_messages(&self) -> Option { - let connection = self.watched_connection.as_ref()?; - - let messages: Vec = connection - .messages - .iter() - .filter_map(|message| { - let params = match &message.params { - Ok(Some(params)) => params.clone(), - Ok(None) => serde_json::Value::Null, - Err(err) => serde_json::to_value(err).ok()?, - }; - Some(serde_json::json!({ - "_direction": match message.direction { - acp::StreamMessageDirection::Incoming => "incoming", - acp::StreamMessageDirection::Outgoing => "outgoing", - }, - "_type": message.message_type.to_string().to_lowercase(), - "id": message.request_id, - "method": message.name.to_string(), - "params": params, - })) - }) - .collect(); - - serde_json::to_string_pretty(&messages).ok() - } - - fn clear_messages(&mut self, cx: &mut Context) { - if let Some(connection) = self.watched_connection.as_mut() { - connection.messages.clear(); - connection.list_state.reset(0); - self.expanded.clear(); - 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() - .id(index) - .group("message") - .cursor_pointer() - .font_buffer(cx) - .w_full() - .py_3() - .pl_4() - .pr_5() - .gap_2() - .items_start() - .text_size(base_size) - .border_color(colors.border) - .border_b_1() - .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() - .flex_shrink_0() - .child(match message.direction { - acp::StreamMessageDirection::Incoming => Icon::new(IconName::ArrowDown) - .color(Color::Error) - .size(IconSize::Small), - acp::StreamMessageDirection::Outgoing => Icon::new(IconName::ArrowUp) - .color(Color::Success) - .size(IconSize::Small), - }) - .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 - .as_ref() - .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 { - div() - .size_full() - .flex_grow() - .child( - list( - connection.list_state.clone(), - cx.processor(Self::render_message), - ) - .with_sizing_behavior(gpui::ListSizingBehavior::Auto) - .size_full(), - ) - .vertical_scrollbar_for(&connection.list_state, window, cx) - .into_any() - } - } - None => h_flex() - .size_full() - .justify_center() - .items_center() - .child("No active connection") - .into_any(), - }) - } -} - -pub struct AcpToolsToolbarItemView { - acp_tools: Option>, - just_copied: bool, -} - -impl AcpToolsToolbarItemView { - pub fn new() -> Self { - Self { - acp_tools: None, - just_copied: false, - } - } -} - -impl Render for AcpToolsToolbarItemView { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let Some(acp_tools) = self.acp_tools.as_ref() else { - return Empty.into_any_element(); - }; - - let acp_tools = acp_tools.clone(); - let has_messages = acp_tools - .read(cx) - .watched_connection - .as_ref() - .is_some_and(|connection| !connection.messages.is_empty()); - - h_flex() - .gap_2() - .child({ - let acp_tools = acp_tools.clone(); - IconButton::new( - "copy_all_messages", - if self.just_copied { - IconName::Check - } else { - IconName::Copy - }, - ) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text(if self.just_copied { - "Copied!" - } else { - "Copy All Messages" - })) - .disabled(!has_messages) - .on_click(cx.listener(move |this, _, _window, cx| { - if let Some(content) = acp_tools.read(cx).serialize_observed_messages() { - cx.write_to_clipboard(ClipboardItem::new_string(content)); - - this.just_copied = true; - cx.spawn(async move |this, cx| { - cx.background_executor().timer(Duration::from_secs(2)).await; - this.update(cx, |this, cx| { - this.just_copied = false; - cx.notify(); - }) - }) - .detach(); - } - })) - }) - .child( - IconButton::new("clear_messages", IconName::Trash) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Clear Messages")) - .disabled(!has_messages) - .on_click(cx.listener(move |_this, _, _window, cx| { - acp_tools.update(cx, |acp_tools, cx| { - acp_tools.clear_messages(cx); - }); - })), - ) - .into_any() - } -} - -impl EventEmitter for AcpToolsToolbarItemView {} - -impl ToolbarItemView for AcpToolsToolbarItemView { - fn set_active_pane_item( - &mut self, - active_pane_item: Option<&dyn ItemHandle>, - _window: &mut Window, - cx: &mut Context, - ) -> ToolbarItemLocation { - if let Some(item) = active_pane_item - && let Some(acp_tools) = item.downcast::() - { - self.acp_tools = Some(acp_tools); - cx.notify(); - return ToolbarItemLocation::PrimaryRight; - } - if self.acp_tools.take().is_some() { - cx.notify(); - } - ToolbarItemLocation::Hidden - } -} diff --git a/crates/action_log/Cargo.toml b/crates/action_log/Cargo.toml deleted file mode 100644 index 699d548593..0000000000 --- a/crates/action_log/Cargo.toml +++ /dev/null @@ -1,45 +0,0 @@ -[package] -name = "action_log" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lib] -path = "src/action_log.rs" - -[lints] -workspace = true - -[dependencies] -anyhow.workspace = true -buffer_diff.workspace = true -clock.workspace = true -collections.workspace = true -futures.workspace = true -gpui.workspace = true -language.workspace = true -project.workspace = true -telemetry.workspace = true -text.workspace = true -util.workspace = true -watch.workspace = true - - -[dev-dependencies] -buffer_diff = { workspace = true, features = ["test-support"] } -collections = { workspace = true, features = ["test-support"] } -clock = { workspace = true, features = ["test-support"] } -ctor.workspace = true -gpui = { workspace = true, features = ["test-support"] } -indoc.workspace = true -language = { workspace = true, features = ["test-support"] } -log.workspace = true -pretty_assertions.workspace = true -project = { workspace = true, features = ["test-support"] } -rand.workspace = true -serde_json.workspace = true -settings = { workspace = true, features = ["test-support"] } -text = { workspace = true, features = ["test-support"] } -util = { workspace = true, features = ["test-support"] } -zlog.workspace = true diff --git a/crates/action_log/LICENSE-GPL b/crates/action_log/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/action_log/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/action_log/src/action_log.rs b/crates/action_log/src/action_log.rs deleted file mode 100644 index 6eb18a4f12..0000000000 --- a/crates/action_log/src/action_log.rs +++ /dev/null @@ -1,2407 +0,0 @@ -use anyhow::{Context as _, Result}; -use buffer_diff::BufferDiff; -use clock; -use collections::BTreeMap; -use futures::{FutureExt, StreamExt, channel::mpsc}; -use gpui::{ - App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity, -}; -use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint}; -use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle}; -use std::{cmp, ops::Range, sync::Arc}; -use text::{Edit, Patch, Rope}; -use util::{RangeExt, ResultExt as _}; - -/// Tracks actions performed by tools in a thread -pub struct ActionLog { - /// Buffers that we want to notify the model about when they change. - tracked_buffers: BTreeMap, TrackedBuffer>, - /// The project this action log is associated with - project: Entity, -} - -impl ActionLog { - /// Creates a new, empty action log associated with the given project. - pub fn new(project: Entity) -> Self { - Self { - tracked_buffers: BTreeMap::default(), - project, - } - } - - pub fn project(&self) -> &Entity { - &self.project - } - - fn track_buffer_internal( - &mut self, - buffer: Entity, - is_created: bool, - cx: &mut Context, - ) -> &mut TrackedBuffer { - let status = if is_created { - if let Some(tracked) = self.tracked_buffers.remove(&buffer) { - match tracked.status { - TrackedBufferStatus::Created { - existing_file_content, - } => TrackedBufferStatus::Created { - existing_file_content, - }, - TrackedBufferStatus::Modified | TrackedBufferStatus::Deleted => { - TrackedBufferStatus::Created { - existing_file_content: Some(tracked.diff_base), - } - } - } - } else if buffer - .read(cx) - .file() - .is_some_and(|file| file.disk_state().exists()) - { - TrackedBufferStatus::Created { - existing_file_content: Some(buffer.read(cx).as_rope().clone()), - } - } else { - TrackedBufferStatus::Created { - existing_file_content: None, - } - } - } else { - TrackedBufferStatus::Modified - }; - - let tracked_buffer = self - .tracked_buffers - .entry(buffer.clone()) - .or_insert_with(|| { - let open_lsp_handle = self.project.update(cx, |project, cx| { - project.register_buffer_with_language_servers(&buffer, cx) - }); - - let text_snapshot = buffer.read(cx).text_snapshot(); - let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx)); - let (diff_update_tx, diff_update_rx) = mpsc::unbounded(); - let diff_base; - let unreviewed_edits; - if is_created { - diff_base = Rope::default(); - unreviewed_edits = Patch::new(vec![Edit { - old: 0..1, - new: 0..text_snapshot.max_point().row + 1, - }]) - } else { - diff_base = buffer.read(cx).as_rope().clone(); - unreviewed_edits = Patch::default(); - } - TrackedBuffer { - buffer: buffer.clone(), - diff_base, - unreviewed_edits, - snapshot: text_snapshot, - status, - version: buffer.read(cx).version(), - diff, - diff_update: diff_update_tx, - _open_lsp_handle: open_lsp_handle, - _maintain_diff: cx.spawn({ - let buffer = buffer.clone(); - async move |this, cx| { - Self::maintain_diff(this, buffer, diff_update_rx, cx) - .await - .ok(); - } - }), - _subscription: cx.subscribe(&buffer, Self::handle_buffer_event), - } - }); - tracked_buffer.version = buffer.read(cx).version(); - tracked_buffer - } - - fn handle_buffer_event( - &mut self, - buffer: Entity, - event: &BufferEvent, - cx: &mut Context, - ) { - match event { - BufferEvent::Edited => self.handle_buffer_edited(buffer, cx), - BufferEvent::FileHandleChanged => { - self.handle_buffer_file_changed(buffer, cx); - } - _ => {} - }; - } - - fn handle_buffer_edited(&mut self, buffer: Entity, cx: &mut Context) { - let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else { - return; - }; - tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx); - } - - fn handle_buffer_file_changed(&mut self, buffer: Entity, cx: &mut Context) { - let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else { - return; - }; - - match tracked_buffer.status { - TrackedBufferStatus::Created { .. } | TrackedBufferStatus::Modified => { - if buffer - .read(cx) - .file() - .is_some_and(|file| file.disk_state() == DiskState::Deleted) - { - // If the buffer had been edited by a tool, but it got - // deleted externally, we want to stop tracking it. - self.tracked_buffers.remove(&buffer); - } - cx.notify(); - } - TrackedBufferStatus::Deleted => { - if buffer - .read(cx) - .file() - .is_some_and(|file| file.disk_state() != DiskState::Deleted) - { - // If the buffer had been deleted by a tool, but it got - // resurrected externally, we want to clear the edits we - // were tracking and reset the buffer's state. - self.tracked_buffers.remove(&buffer); - self.track_buffer_internal(buffer, false, cx); - } - cx.notify(); - } - } - } - - async fn maintain_diff( - this: WeakEntity, - buffer: Entity, - mut buffer_updates: mpsc::UnboundedReceiver<(ChangeAuthor, text::BufferSnapshot)>, - cx: &mut AsyncApp, - ) -> Result<()> { - let git_store = this.read_with(cx, |this, cx| this.project.read(cx).git_store().clone())?; - let git_diff = this - .update(cx, |this, cx| { - this.project.update(cx, |project, cx| { - project.open_uncommitted_diff(buffer.clone(), cx) - }) - })? - .await - .ok(); - let buffer_repo = git_store.read_with(cx, |git_store, cx| { - git_store.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx) - })?; - - let (mut git_diff_updates_tx, mut git_diff_updates_rx) = watch::channel(()); - let _repo_subscription = - if let Some((git_diff, (buffer_repo, _))) = git_diff.as_ref().zip(buffer_repo) { - cx.update(|cx| { - let mut old_head = buffer_repo.read(cx).head_commit.clone(); - Some(cx.subscribe(git_diff, move |_, event, cx| { - if let buffer_diff::BufferDiffEvent::DiffChanged { .. } = event { - let new_head = buffer_repo.read(cx).head_commit.clone(); - if new_head != old_head { - old_head = new_head; - git_diff_updates_tx.send(()).ok(); - } - } - })) - })? - } else { - None - }; - - loop { - futures::select_biased! { - buffer_update = buffer_updates.next() => { - if let Some((author, buffer_snapshot)) = buffer_update { - Self::track_edits(&this, &buffer, author, buffer_snapshot, cx).await?; - } else { - break; - } - } - _ = git_diff_updates_rx.changed().fuse() => { - if let Some(git_diff) = git_diff.as_ref() { - Self::keep_committed_edits(&this, &buffer, git_diff, cx).await?; - } - } - } - } - - Ok(()) - } - - async fn track_edits( - this: &WeakEntity, - buffer: &Entity, - author: ChangeAuthor, - buffer_snapshot: text::BufferSnapshot, - cx: &mut AsyncApp, - ) -> Result<()> { - let rebase = this.update(cx, |this, cx| { - let tracked_buffer = this - .tracked_buffers - .get_mut(buffer) - .context("buffer not tracked")?; - - let rebase = cx.background_spawn({ - let mut base_text = tracked_buffer.diff_base.clone(); - let old_snapshot = tracked_buffer.snapshot.clone(); - let new_snapshot = buffer_snapshot.clone(); - let unreviewed_edits = tracked_buffer.unreviewed_edits.clone(); - let edits = diff_snapshots(&old_snapshot, &new_snapshot); - async move { - if let ChangeAuthor::User = author { - apply_non_conflicting_edits( - &unreviewed_edits, - edits, - &mut base_text, - new_snapshot.as_rope(), - ); - } - - (Arc::new(base_text.to_string()), base_text) - } - }); - - anyhow::Ok(rebase) - })??; - let (new_base_text, new_diff_base) = rebase.await; - - Self::update_diff( - this, - buffer, - buffer_snapshot, - new_base_text, - new_diff_base, - cx, - ) - .await - } - - async fn keep_committed_edits( - this: &WeakEntity, - buffer: &Entity, - git_diff: &Entity, - cx: &mut AsyncApp, - ) -> Result<()> { - let buffer_snapshot = this.read_with(cx, |this, _cx| { - let tracked_buffer = this - .tracked_buffers - .get(buffer) - .context("buffer not tracked")?; - anyhow::Ok(tracked_buffer.snapshot.clone()) - })??; - let (new_base_text, new_diff_base) = this - .read_with(cx, |this, cx| { - let tracked_buffer = this - .tracked_buffers - .get(buffer) - .context("buffer not tracked")?; - let old_unreviewed_edits = tracked_buffer.unreviewed_edits.clone(); - let agent_diff_base = tracked_buffer.diff_base.clone(); - let git_diff_base = git_diff.read(cx).base_text().as_rope().clone(); - let buffer_text = tracked_buffer.snapshot.as_rope().clone(); - anyhow::Ok(cx.background_spawn(async move { - let mut old_unreviewed_edits = old_unreviewed_edits.into_iter().peekable(); - let committed_edits = language::line_diff( - &agent_diff_base.to_string(), - &git_diff_base.to_string(), - ) - .into_iter() - .map(|(old, new)| Edit { old, new }); - - let mut new_agent_diff_base = agent_diff_base.clone(); - let mut row_delta = 0i32; - for committed in committed_edits { - while let Some(unreviewed) = old_unreviewed_edits.peek() { - // If the committed edit matches the unreviewed - // edit, assume the user wants to keep it. - if committed.old == unreviewed.old { - let unreviewed_new = - buffer_text.slice_rows(unreviewed.new.clone()).to_string(); - let committed_new = - git_diff_base.slice_rows(committed.new.clone()).to_string(); - if unreviewed_new == committed_new { - let old_byte_start = - new_agent_diff_base.point_to_offset(Point::new( - (unreviewed.old.start as i32 + row_delta) as u32, - 0, - )); - let old_byte_end = - new_agent_diff_base.point_to_offset(cmp::min( - Point::new( - (unreviewed.old.end as i32 + row_delta) as u32, - 0, - ), - new_agent_diff_base.max_point(), - )); - new_agent_diff_base - .replace(old_byte_start..old_byte_end, &unreviewed_new); - row_delta += - unreviewed.new_len() as i32 - unreviewed.old_len() as i32; - } - } else if unreviewed.old.start >= committed.old.end { - break; - } - - old_unreviewed_edits.next().unwrap(); - } - } - - ( - Arc::new(new_agent_diff_base.to_string()), - new_agent_diff_base, - ) - })) - })?? - .await; - - Self::update_diff( - this, - buffer, - buffer_snapshot, - new_base_text, - new_diff_base, - cx, - ) - .await - } - - async fn update_diff( - this: &WeakEntity, - buffer: &Entity, - buffer_snapshot: text::BufferSnapshot, - new_base_text: Arc, - new_diff_base: Rope, - cx: &mut AsyncApp, - ) -> Result<()> { - let (diff, language, language_registry) = this.read_with(cx, |this, cx| { - let tracked_buffer = this - .tracked_buffers - .get(buffer) - .context("buffer not tracked")?; - anyhow::Ok(( - tracked_buffer.diff.clone(), - buffer.read(cx).language().cloned(), - buffer.read(cx).language_registry(), - )) - })??; - let diff_snapshot = BufferDiff::update_diff( - diff.clone(), - buffer_snapshot.clone(), - Some(new_base_text), - true, - false, - language, - language_registry, - cx, - ) - .await; - let mut unreviewed_edits = Patch::default(); - if let Ok(diff_snapshot) = diff_snapshot { - unreviewed_edits = cx - .background_spawn({ - let diff_snapshot = diff_snapshot.clone(); - let buffer_snapshot = buffer_snapshot.clone(); - let new_diff_base = new_diff_base.clone(); - async move { - let mut unreviewed_edits = Patch::default(); - for hunk in diff_snapshot.hunks_intersecting_range( - Anchor::min_for_buffer(buffer_snapshot.remote_id()) - ..Anchor::max_for_buffer(buffer_snapshot.remote_id()), - &buffer_snapshot, - ) { - let old_range = new_diff_base - .offset_to_point(hunk.diff_base_byte_range.start) - ..new_diff_base.offset_to_point(hunk.diff_base_byte_range.end); - let new_range = hunk.range.start..hunk.range.end; - unreviewed_edits.push(point_to_row_edit( - Edit { - old: old_range, - new: new_range, - }, - &new_diff_base, - buffer_snapshot.as_rope(), - )); - } - unreviewed_edits - } - }) - .await; - - diff.update(cx, |diff, cx| { - diff.set_snapshot(diff_snapshot, &buffer_snapshot, cx); - })?; - } - this.update(cx, |this, cx| { - let tracked_buffer = this - .tracked_buffers - .get_mut(buffer) - .context("buffer not tracked")?; - tracked_buffer.diff_base = new_diff_base; - tracked_buffer.snapshot = buffer_snapshot; - tracked_buffer.unreviewed_edits = unreviewed_edits; - cx.notify(); - anyhow::Ok(()) - })? - } - - /// Track a buffer as read by agent, so we can notify the model about user edits. - pub fn buffer_read(&mut self, buffer: Entity, cx: &mut Context) { - self.track_buffer_internal(buffer, false, cx); - } - - /// Mark a buffer as created by agent, so we can refresh it in the context - pub fn buffer_created(&mut self, buffer: Entity, cx: &mut Context) { - self.track_buffer_internal(buffer, true, cx); - } - - /// Mark a buffer as edited by agent, so we can refresh it in the context - pub fn buffer_edited(&mut self, buffer: Entity, cx: &mut Context) { - let tracked_buffer = self.track_buffer_internal(buffer, false, cx); - if let TrackedBufferStatus::Deleted = tracked_buffer.status { - tracked_buffer.status = TrackedBufferStatus::Modified; - } - tracked_buffer.schedule_diff_update(ChangeAuthor::Agent, cx); - } - - pub fn will_delete_buffer(&mut self, buffer: Entity, cx: &mut Context) { - let tracked_buffer = self.track_buffer_internal(buffer.clone(), false, cx); - match tracked_buffer.status { - TrackedBufferStatus::Created { .. } => { - self.tracked_buffers.remove(&buffer); - cx.notify(); - } - TrackedBufferStatus::Modified => { - buffer.update(cx, |buffer, cx| buffer.set_text("", cx)); - tracked_buffer.status = TrackedBufferStatus::Deleted; - tracked_buffer.schedule_diff_update(ChangeAuthor::Agent, cx); - } - TrackedBufferStatus::Deleted => {} - } - cx.notify(); - } - - pub fn keep_edits_in_range( - &mut self, - buffer: Entity, - buffer_range: Range, - telemetry: Option, - cx: &mut Context, - ) { - let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else { - return; - }; - - let mut metrics = ActionLogMetrics::for_buffer(buffer.read(cx)); - match tracked_buffer.status { - TrackedBufferStatus::Deleted => { - metrics.add_edits(tracked_buffer.unreviewed_edits.edits()); - self.tracked_buffers.remove(&buffer); - cx.notify(); - } - _ => { - let buffer = buffer.read(cx); - let buffer_range = - buffer_range.start.to_point(buffer)..buffer_range.end.to_point(buffer); - let mut delta = 0i32; - tracked_buffer.unreviewed_edits.retain_mut(|edit| { - edit.old.start = (edit.old.start as i32 + delta) as u32; - edit.old.end = (edit.old.end as i32 + delta) as u32; - - if buffer_range.end.row < edit.new.start - || buffer_range.start.row > edit.new.end - { - true - } else { - let old_range = tracked_buffer - .diff_base - .point_to_offset(Point::new(edit.old.start, 0)) - ..tracked_buffer.diff_base.point_to_offset(cmp::min( - Point::new(edit.old.end, 0), - tracked_buffer.diff_base.max_point(), - )); - let new_range = tracked_buffer - .snapshot - .point_to_offset(Point::new(edit.new.start, 0)) - ..tracked_buffer.snapshot.point_to_offset(cmp::min( - Point::new(edit.new.end, 0), - tracked_buffer.snapshot.max_point(), - )); - tracked_buffer.diff_base.replace( - old_range, - &tracked_buffer - .snapshot - .text_for_range(new_range) - .collect::(), - ); - delta += edit.new_len() as i32 - edit.old_len() as i32; - metrics.add_edit(edit); - false - } - }); - if tracked_buffer.unreviewed_edits.is_empty() - && let TrackedBufferStatus::Created { .. } = &mut tracked_buffer.status - { - tracked_buffer.status = TrackedBufferStatus::Modified; - } - tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx); - } - } - if let Some(telemetry) = telemetry { - telemetry_report_accepted_edits(&telemetry, metrics); - } - } - - pub fn reject_edits_in_ranges( - &mut self, - buffer: Entity, - buffer_ranges: Vec>, - telemetry: Option, - cx: &mut Context, - ) -> Task> { - let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else { - return Task::ready(Ok(())); - }; - - let mut metrics = ActionLogMetrics::for_buffer(buffer.read(cx)); - let task = match &tracked_buffer.status { - TrackedBufferStatus::Created { - existing_file_content, - } => { - let task = if let Some(existing_file_content) = existing_file_content { - buffer.update(cx, |buffer, cx| { - buffer.start_transaction(); - buffer.set_text("", cx); - for chunk in existing_file_content.chunks() { - buffer.append(chunk, cx); - } - buffer.end_transaction(cx); - }); - self.project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) - } else { - // For a file created by AI with no pre-existing content, - // only delete the file if we're certain it contains only AI content - // with no edits from the user. - - let initial_version = tracked_buffer.version.clone(); - let current_version = buffer.read(cx).version(); - - let current_content = buffer.read(cx).text(); - let tracked_content = tracked_buffer.snapshot.text(); - - let is_ai_only_content = - initial_version == current_version && current_content == tracked_content; - - if is_ai_only_content { - buffer - .read(cx) - .entry_id(cx) - .and_then(|entry_id| { - self.project.update(cx, |project, cx| { - project.delete_entry(entry_id, false, cx) - }) - }) - .unwrap_or(Task::ready(Ok(()))) - } else { - // Not sure how to disentangle edits made by the user - // from edits made by the AI at this point. - // For now, preserve both to avoid data loss. - // - // TODO: Better solution (disable "Reject" after user makes some - // edit or find a way to differentiate between AI and user edits) - Task::ready(Ok(())) - } - }; - - metrics.add_edits(tracked_buffer.unreviewed_edits.edits()); - self.tracked_buffers.remove(&buffer); - cx.notify(); - task - } - TrackedBufferStatus::Deleted => { - buffer.update(cx, |buffer, cx| { - buffer.set_text(tracked_buffer.diff_base.to_string(), cx) - }); - let save = self - .project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)); - - // Clear all tracked edits for this buffer and start over as if we just read it. - metrics.add_edits(tracked_buffer.unreviewed_edits.edits()); - self.tracked_buffers.remove(&buffer); - self.buffer_read(buffer.clone(), cx); - cx.notify(); - save - } - TrackedBufferStatus::Modified => { - buffer.update(cx, |buffer, cx| { - let mut buffer_row_ranges = buffer_ranges - .into_iter() - .map(|range| { - range.start.to_point(buffer).row..range.end.to_point(buffer).row - }) - .peekable(); - - let mut edits_to_revert = Vec::new(); - for edit in tracked_buffer.unreviewed_edits.edits() { - let new_range = tracked_buffer - .snapshot - .anchor_before(Point::new(edit.new.start, 0)) - ..tracked_buffer.snapshot.anchor_after(cmp::min( - Point::new(edit.new.end, 0), - tracked_buffer.snapshot.max_point(), - )); - let new_row_range = new_range.start.to_point(buffer).row - ..new_range.end.to_point(buffer).row; - - let mut revert = false; - while let Some(buffer_row_range) = buffer_row_ranges.peek() { - if buffer_row_range.end < new_row_range.start { - buffer_row_ranges.next(); - } else if buffer_row_range.start > new_row_range.end { - break; - } else { - revert = true; - break; - } - } - - if revert { - metrics.add_edit(edit); - let old_range = tracked_buffer - .diff_base - .point_to_offset(Point::new(edit.old.start, 0)) - ..tracked_buffer.diff_base.point_to_offset(cmp::min( - Point::new(edit.old.end, 0), - tracked_buffer.diff_base.max_point(), - )); - let old_text = tracked_buffer - .diff_base - .chunks_in_range(old_range) - .collect::(); - edits_to_revert.push((new_range, old_text)); - } - } - - buffer.edit(edits_to_revert, None, cx); - }); - self.project - .update(cx, |project, cx| project.save_buffer(buffer, cx)) - } - }; - if let Some(telemetry) = telemetry { - telemetry_report_rejected_edits(&telemetry, metrics); - } - task - } - - pub fn keep_all_edits( - &mut self, - telemetry: Option, - cx: &mut Context, - ) { - self.tracked_buffers.retain(|buffer, tracked_buffer| { - let mut metrics = ActionLogMetrics::for_buffer(buffer.read(cx)); - metrics.add_edits(tracked_buffer.unreviewed_edits.edits()); - if let Some(telemetry) = telemetry.as_ref() { - telemetry_report_accepted_edits(telemetry, metrics); - } - match tracked_buffer.status { - TrackedBufferStatus::Deleted => false, - _ => { - if let TrackedBufferStatus::Created { .. } = &mut tracked_buffer.status { - tracked_buffer.status = TrackedBufferStatus::Modified; - } - tracked_buffer.unreviewed_edits.clear(); - tracked_buffer.diff_base = tracked_buffer.snapshot.as_rope().clone(); - tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx); - true - } - } - }); - - cx.notify(); - } - - pub fn reject_all_edits( - &mut self, - telemetry: Option, - cx: &mut Context, - ) -> Task<()> { - let futures = self.changed_buffers(cx).into_keys().map(|buffer| { - let buffer_ranges = vec![Anchor::min_max_range_for_buffer( - buffer.read(cx).remote_id(), - )]; - let reject = self.reject_edits_in_ranges(buffer, buffer_ranges, telemetry.clone(), cx); - - async move { - reject.await.log_err(); - } - }); - - let task = futures::future::join_all(futures); - cx.background_spawn(async move { - task.await; - }) - } - - /// Returns the set of buffers that contain edits that haven't been reviewed by the user. - pub fn changed_buffers(&self, cx: &App) -> BTreeMap, Entity> { - self.tracked_buffers - .iter() - .filter(|(_, tracked)| tracked.has_edits(cx)) - .map(|(buffer, tracked)| (buffer.clone(), tracked.diff.clone())) - .collect() - } - - /// Iterate over buffers changed since last read or edited by the model - pub fn stale_buffers<'a>(&'a self, cx: &'a App) -> impl Iterator> { - self.tracked_buffers - .iter() - .filter(|(buffer, tracked)| { - let buffer = buffer.read(cx); - - tracked.version != buffer.version - && buffer - .file() - .is_some_and(|file| file.disk_state() != DiskState::Deleted) - }) - .map(|(buffer, _)| buffer) - } -} - -#[derive(Clone)] -pub struct ActionLogTelemetry { - pub agent_telemetry_id: SharedString, - pub session_id: Arc, -} - -struct ActionLogMetrics { - lines_removed: u32, - lines_added: u32, - language: Option, -} - -impl ActionLogMetrics { - fn for_buffer(buffer: &Buffer) -> Self { - Self { - language: buffer.language().map(|l| l.name().0), - lines_removed: 0, - lines_added: 0, - } - } - - fn add_edits(&mut self, edits: &[Edit]) { - for edit in edits { - self.add_edit(edit); - } - } - - fn add_edit(&mut self, edit: &Edit) { - self.lines_added += edit.new_len(); - self.lines_removed += edit.old_len(); - } -} - -fn telemetry_report_accepted_edits(telemetry: &ActionLogTelemetry, metrics: ActionLogMetrics) { - telemetry::event!( - "Agent Edits Accepted", - agent = telemetry.agent_telemetry_id, - session = telemetry.session_id, - language = metrics.language, - lines_added = metrics.lines_added, - lines_removed = metrics.lines_removed - ); -} - -fn telemetry_report_rejected_edits(telemetry: &ActionLogTelemetry, metrics: ActionLogMetrics) { - telemetry::event!( - "Agent Edits Rejected", - agent = telemetry.agent_telemetry_id, - session = telemetry.session_id, - language = metrics.language, - lines_added = metrics.lines_added, - lines_removed = metrics.lines_removed - ); -} - -fn apply_non_conflicting_edits( - patch: &Patch, - edits: Vec>, - old_text: &mut Rope, - new_text: &Rope, -) -> bool { - let mut old_edits = patch.edits().iter().cloned().peekable(); - let mut new_edits = edits.into_iter().peekable(); - let mut applied_delta = 0i32; - let mut rebased_delta = 0i32; - let mut has_made_changes = false; - - while let Some(mut new_edit) = new_edits.next() { - let mut conflict = false; - - // Push all the old edits that are before this new edit or that intersect with it. - while let Some(old_edit) = old_edits.peek() { - if new_edit.old.end < old_edit.new.start - || (!old_edit.new.is_empty() && new_edit.old.end == old_edit.new.start) - { - break; - } else if new_edit.old.start > old_edit.new.end - || (!old_edit.new.is_empty() && new_edit.old.start == old_edit.new.end) - { - let old_edit = old_edits.next().unwrap(); - rebased_delta += old_edit.new_len() as i32 - old_edit.old_len() as i32; - } else { - conflict = true; - if new_edits - .peek() - .is_some_and(|next_edit| next_edit.old.overlaps(&old_edit.new)) - { - new_edit = new_edits.next().unwrap(); - } else { - let old_edit = old_edits.next().unwrap(); - rebased_delta += old_edit.new_len() as i32 - old_edit.old_len() as i32; - } - } - } - - if !conflict { - // This edit doesn't intersect with any old edit, so we can apply it to the old text. - new_edit.old.start = (new_edit.old.start as i32 + applied_delta - rebased_delta) as u32; - new_edit.old.end = (new_edit.old.end as i32 + applied_delta - rebased_delta) as u32; - let old_bytes = old_text.point_to_offset(Point::new(new_edit.old.start, 0)) - ..old_text.point_to_offset(cmp::min( - Point::new(new_edit.old.end, 0), - old_text.max_point(), - )); - let new_bytes = new_text.point_to_offset(Point::new(new_edit.new.start, 0)) - ..new_text.point_to_offset(cmp::min( - Point::new(new_edit.new.end, 0), - new_text.max_point(), - )); - - old_text.replace( - old_bytes, - &new_text.chunks_in_range(new_bytes).collect::(), - ); - applied_delta += new_edit.new_len() as i32 - new_edit.old_len() as i32; - has_made_changes = true; - } - } - has_made_changes -} - -fn diff_snapshots( - old_snapshot: &text::BufferSnapshot, - new_snapshot: &text::BufferSnapshot, -) -> Vec> { - let mut edits = new_snapshot - .edits_since::(&old_snapshot.version) - .map(|edit| point_to_row_edit(edit, old_snapshot.as_rope(), new_snapshot.as_rope())) - .peekable(); - let mut row_edits = Vec::new(); - while let Some(mut edit) = edits.next() { - while let Some(next_edit) = edits.peek() { - if edit.old.end >= next_edit.old.start { - edit.old.end = next_edit.old.end; - edit.new.end = next_edit.new.end; - edits.next(); - } else { - break; - } - } - row_edits.push(edit); - } - row_edits -} - -fn point_to_row_edit(edit: Edit, old_text: &Rope, new_text: &Rope) -> Edit { - if edit.old.start.column == old_text.line_len(edit.old.start.row) - && new_text - .chars_at(new_text.point_to_offset(edit.new.start)) - .next() - == Some('\n') - && edit.old.start != old_text.max_point() - { - Edit { - old: edit.old.start.row + 1..edit.old.end.row + 1, - new: edit.new.start.row + 1..edit.new.end.row + 1, - } - } else if edit.old.start.column == 0 && edit.old.end.column == 0 && edit.new.end.column == 0 { - Edit { - old: edit.old.start.row..edit.old.end.row, - new: edit.new.start.row..edit.new.end.row, - } - } else { - Edit { - old: edit.old.start.row..edit.old.end.row + 1, - new: edit.new.start.row..edit.new.end.row + 1, - } - } -} - -#[derive(Copy, Clone, Debug)] -enum ChangeAuthor { - User, - Agent, -} - -enum TrackedBufferStatus { - Created { existing_file_content: Option }, - Modified, - Deleted, -} - -struct TrackedBuffer { - buffer: Entity, - diff_base: Rope, - unreviewed_edits: Patch, - status: TrackedBufferStatus, - version: clock::Global, - diff: Entity, - snapshot: text::BufferSnapshot, - diff_update: mpsc::UnboundedSender<(ChangeAuthor, text::BufferSnapshot)>, - _open_lsp_handle: OpenLspBufferHandle, - _maintain_diff: Task<()>, - _subscription: Subscription, -} - -impl TrackedBuffer { - fn has_edits(&self, cx: &App) -> bool { - self.diff - .read(cx) - .hunks(self.buffer.read(cx), cx) - .next() - .is_some() - } - - fn schedule_diff_update(&self, author: ChangeAuthor, cx: &App) { - self.diff_update - .unbounded_send((author, self.buffer.read(cx).text_snapshot())) - .ok(); - } -} - -pub struct ChangedBuffer { - pub diff: Entity, -} - -#[cfg(test)] -mod tests { - use super::*; - use buffer_diff::DiffHunkStatusKind; - use gpui::TestAppContext; - use language::Point; - use project::{FakeFs, Fs, Project, RemoveOptions}; - use rand::prelude::*; - use serde_json::json; - use settings::SettingsStore; - use std::env; - use util::{RandomCharIter, path}; - - #[ctor::ctor] - fn init_logger() { - zlog::init_test(); - } - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - }); - } - - #[gpui::test(iterations = 10)] - async fn test_keep_edits(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"})) - .await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let file_path = project - .read_with(cx, |project, cx| project.find_project_path("dir/file", cx)) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(1, 1)..Point::new(1, 2), "E")], None, cx) - .unwrap() - }); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(4, 2)..Point::new(4, 3), "O")], None, cx) - .unwrap() - }); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndEf\nghi\njkl\nmnO" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![ - HunkStatus { - range: Point::new(1, 0)..Point::new(2, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "def\n".into(), - }, - HunkStatus { - range: Point::new(4, 0)..Point::new(4, 3), - diff_status: DiffHunkStatusKind::Modified, - old_text: "mno".into(), - } - ], - )] - ); - - action_log.update(cx, |log, cx| { - log.keep_edits_in_range(buffer.clone(), Point::new(3, 0)..Point::new(4, 3), None, cx) - }); - cx.run_until_parked(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(1, 0)..Point::new(2, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "def\n".into(), - }], - )] - ); - - action_log.update(cx, |log, cx| { - log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(4, 3), None, cx) - }); - cx.run_until_parked(); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 10)] - async fn test_deletions(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/dir"), - json!({"file": "abc\ndef\nghi\njkl\nmno\npqr"}), - ) - .await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let file_path = project - .read_with(cx, |project, cx| project.find_project_path("dir/file", cx)) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(1, 0)..Point::new(2, 0), "")], None, cx) - .unwrap(); - buffer.finalize_last_transaction(); - }); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(3, 0)..Point::new(4, 0), "")], None, cx) - .unwrap(); - buffer.finalize_last_transaction(); - }); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\nghi\njkl\npqr" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![ - HunkStatus { - range: Point::new(1, 0)..Point::new(1, 0), - diff_status: DiffHunkStatusKind::Deleted, - old_text: "def\n".into(), - }, - HunkStatus { - range: Point::new(3, 0)..Point::new(3, 0), - diff_status: DiffHunkStatusKind::Deleted, - old_text: "mno\n".into(), - } - ], - )] - ); - - buffer.update(cx, |buffer, cx| buffer.undo(cx)); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\nghi\njkl\nmno\npqr" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(1, 0)..Point::new(1, 0), - diff_status: DiffHunkStatusKind::Deleted, - old_text: "def\n".into(), - }], - )] - ); - - action_log.update(cx, |log, cx| { - log.keep_edits_in_range(buffer.clone(), Point::new(1, 0)..Point::new(1, 0), None, cx) - }); - cx.run_until_parked(); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 10)] - async fn test_overlapping_user_edits(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"})) - .await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let file_path = project - .read_with(cx, |project, cx| project.find_project_path("dir/file", cx)) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(1, 2)..Point::new(2, 3), "F\nGHI")], None, cx) - .unwrap() - }); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndeF\nGHI\njkl\nmno" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(1, 0)..Point::new(3, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "def\nghi\n".into(), - }], - )] - ); - - buffer.update(cx, |buffer, cx| { - buffer.edit( - [ - (Point::new(0, 2)..Point::new(0, 2), "X"), - (Point::new(3, 0)..Point::new(3, 0), "Y"), - ], - None, - cx, - ) - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abXc\ndeF\nGHI\nYjkl\nmno" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(1, 0)..Point::new(3, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "def\nghi\n".into(), - }], - )] - ); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "Z")], None, cx) - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abXc\ndZeF\nGHI\nYjkl\nmno" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(1, 0)..Point::new(3, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "def\nghi\n".into(), - }], - )] - ); - - action_log.update(cx, |log, cx| { - log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), None, cx) - }); - cx.run_until_parked(); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 10)] - async fn test_creating_files(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/dir"), json!({})).await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let file_path = project - .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx)) - .unwrap(); - - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| buffer.set_text("lorem", cx)); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 5), - diff_status: DiffHunkStatusKind::Added, - old_text: "".into(), - }], - )] - ); - - buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "X")], None, cx)); - cx.run_until_parked(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 6), - diff_status: DiffHunkStatusKind::Added, - old_text: "".into(), - }], - )] - ); - - action_log.update(cx, |log, cx| { - log.keep_edits_in_range(buffer.clone(), 0..5, None, cx) - }); - cx.run_until_parked(); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 10)] - async fn test_overwriting_files(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/dir"), - json!({ - "file1": "Lorem ipsum dolor" - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let file_path = project - .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx)) - .unwrap(); - - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| buffer.set_text("sit amet consecteur", cx)); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 19), - diff_status: DiffHunkStatusKind::Added, - old_text: "".into(), - }], - )] - ); - - action_log - .update(cx, |log, cx| { - log.reject_edits_in_ranges(buffer.clone(), vec![2..5], None, cx) - }) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - assert_eq!( - buffer.read_with(cx, |buffer, _cx| buffer.text()), - "Lorem ipsum dolor" - ); - } - - #[gpui::test(iterations = 10)] - async fn test_overwriting_previously_edited_files(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/dir"), - json!({ - "file1": "Lorem ipsum dolor" - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let file_path = project - .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx)) - .unwrap(); - - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| buffer.append(" sit amet consecteur", cx)); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 37), - diff_status: DiffHunkStatusKind::Modified, - old_text: "Lorem ipsum dolor".into(), - }], - )] - ); - - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| buffer.set_text("rewritten", cx)); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 9), - diff_status: DiffHunkStatusKind::Added, - old_text: "".into(), - }], - )] - ); - - action_log - .update(cx, |log, cx| { - log.reject_edits_in_ranges(buffer.clone(), vec![2..5], None, cx) - }) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - assert_eq!( - buffer.read_with(cx, |buffer, _cx| buffer.text()), - "Lorem ipsum dolor" - ); - } - - #[gpui::test(iterations = 10)] - async fn test_deleting_files(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/dir"), - json!({"file1": "lorem\n", "file2": "ipsum\n"}), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let file1_path = project - .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx)) - .unwrap(); - let file2_path = project - .read_with(cx, |project, cx| project.find_project_path("dir/file2", cx)) - .unwrap(); - - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let buffer1 = project - .update(cx, |project, cx| { - project.open_buffer(file1_path.clone(), cx) - }) - .await - .unwrap(); - let buffer2 = project - .update(cx, |project, cx| { - project.open_buffer(file2_path.clone(), cx) - }) - .await - .unwrap(); - - action_log.update(cx, |log, cx| log.will_delete_buffer(buffer1.clone(), cx)); - action_log.update(cx, |log, cx| log.will_delete_buffer(buffer2.clone(), cx)); - project - .update(cx, |project, cx| { - project.delete_file(file1_path.clone(), false, cx) - }) - .unwrap() - .await - .unwrap(); - project - .update(cx, |project, cx| { - project.delete_file(file2_path.clone(), false, cx) - }) - .unwrap() - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![ - ( - buffer1.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 0), - diff_status: DiffHunkStatusKind::Deleted, - old_text: "lorem\n".into(), - }] - ), - ( - buffer2.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 0), - diff_status: DiffHunkStatusKind::Deleted, - old_text: "ipsum\n".into(), - }], - ) - ] - ); - - // Simulate file1 being recreated externally. - fs.insert_file(path!("/dir/file1"), "LOREM".as_bytes().to_vec()) - .await; - - // Simulate file2 being recreated by a tool. - let buffer2 = project - .update(cx, |project, cx| project.open_buffer(file2_path, cx)) - .await - .unwrap(); - action_log.update(cx, |log, cx| log.buffer_created(buffer2.clone(), cx)); - buffer2.update(cx, |buffer, cx| buffer.set_text("IPSUM", cx)); - action_log.update(cx, |log, cx| log.buffer_edited(buffer2.clone(), cx)); - project - .update(cx, |project, cx| project.save_buffer(buffer2.clone(), cx)) - .await - .unwrap(); - - cx.run_until_parked(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer2.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 5), - diff_status: DiffHunkStatusKind::Added, - old_text: "".into(), - }], - )] - ); - - // Simulate file2 being deleted externally. - fs.remove_file(path!("/dir/file2").as_ref(), RemoveOptions::default()) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 10)] - async fn test_reject_edits(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"})) - .await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let file_path = project - .read_with(cx, |project, cx| project.find_project_path("dir/file", cx)) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(1, 1)..Point::new(1, 2), "E\nXYZ")], None, cx) - .unwrap() - }); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(5, 2)..Point::new(5, 3), "O")], None, cx) - .unwrap() - }); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndE\nXYZf\nghi\njkl\nmnO" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![ - HunkStatus { - range: Point::new(1, 0)..Point::new(3, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "def\n".into(), - }, - HunkStatus { - range: Point::new(5, 0)..Point::new(5, 3), - diff_status: DiffHunkStatusKind::Modified, - old_text: "mno".into(), - } - ], - )] - ); - - // If the rejected range doesn't overlap with any hunk, we ignore it. - action_log - .update(cx, |log, cx| { - log.reject_edits_in_ranges( - buffer.clone(), - vec![Point::new(4, 0)..Point::new(4, 0)], - None, - cx, - ) - }) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndE\nXYZf\nghi\njkl\nmnO" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![ - HunkStatus { - range: Point::new(1, 0)..Point::new(3, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "def\n".into(), - }, - HunkStatus { - range: Point::new(5, 0)..Point::new(5, 3), - diff_status: DiffHunkStatusKind::Modified, - old_text: "mno".into(), - } - ], - )] - ); - - action_log - .update(cx, |log, cx| { - log.reject_edits_in_ranges( - buffer.clone(), - vec![Point::new(0, 0)..Point::new(1, 0)], - None, - cx, - ) - }) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndef\nghi\njkl\nmnO" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(4, 0)..Point::new(4, 3), - diff_status: DiffHunkStatusKind::Modified, - old_text: "mno".into(), - }], - )] - ); - - action_log - .update(cx, |log, cx| { - log.reject_edits_in_ranges( - buffer.clone(), - vec![Point::new(4, 0)..Point::new(4, 0)], - None, - cx, - ) - }) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndef\nghi\njkl\nmno" - ); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 10)] - async fn test_reject_multiple_edits(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"})) - .await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let file_path = project - .read_with(cx, |project, cx| project.find_project_path("dir/file", cx)) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(1, 1)..Point::new(1, 2), "E\nXYZ")], None, cx) - .unwrap() - }); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(5, 2)..Point::new(5, 3), "O")], None, cx) - .unwrap() - }); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndE\nXYZf\nghi\njkl\nmnO" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![ - HunkStatus { - range: Point::new(1, 0)..Point::new(3, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "def\n".into(), - }, - HunkStatus { - range: Point::new(5, 0)..Point::new(5, 3), - diff_status: DiffHunkStatusKind::Modified, - old_text: "mno".into(), - } - ], - )] - ); - - action_log.update(cx, |log, cx| { - let range_1 = buffer.read(cx).anchor_before(Point::new(0, 0)) - ..buffer.read(cx).anchor_before(Point::new(1, 0)); - let range_2 = buffer.read(cx).anchor_before(Point::new(5, 0)) - ..buffer.read(cx).anchor_before(Point::new(5, 3)); - - log.reject_edits_in_ranges(buffer.clone(), vec![range_1, range_2], None, cx) - .detach(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndef\nghi\njkl\nmno" - ); - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndef\nghi\njkl\nmno" - ); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 10)] - async fn test_reject_deleted_file(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/dir"), json!({"file": "content"})) - .await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let file_path = project - .read_with(cx, |project, cx| project.find_project_path("dir/file", cx)) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path.clone(), cx)) - .await - .unwrap(); - - cx.update(|cx| { - action_log.update(cx, |log, cx| log.will_delete_buffer(buffer.clone(), cx)); - }); - project - .update(cx, |project, cx| { - project.delete_file(file_path.clone(), false, cx) - }) - .unwrap() - .await - .unwrap(); - cx.run_until_parked(); - assert!(!fs.is_file(path!("/dir/file").as_ref()).await); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 0), - diff_status: DiffHunkStatusKind::Deleted, - old_text: "content".into(), - }] - )] - ); - - action_log - .update(cx, |log, cx| { - log.reject_edits_in_ranges( - buffer.clone(), - vec![Point::new(0, 0)..Point::new(0, 0)], - None, - cx, - ) - }) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!(buffer.read_with(cx, |buffer, _| buffer.text()), "content"); - assert!(fs.is_file(path!("/dir/file").as_ref()).await); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 10)] - async fn test_reject_created_file(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let file_path = project - .read_with(cx, |project, cx| { - project.find_project_path("dir/new_file", cx) - }) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| buffer.set_text("content", cx)); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) - .await - .unwrap(); - assert!(fs.is_file(path!("/dir/new_file").as_ref()).await); - cx.run_until_parked(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 7), - diff_status: DiffHunkStatusKind::Added, - old_text: "".into(), - }], - )] - ); - - action_log - .update(cx, |log, cx| { - log.reject_edits_in_ranges( - buffer.clone(), - vec![Point::new(0, 0)..Point::new(0, 11)], - None, - cx, - ) - }) - .await - .unwrap(); - cx.run_until_parked(); - assert!(!fs.is_file(path!("/dir/new_file").as_ref()).await); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test] - async fn test_reject_created_file_with_user_edits(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - - let file_path = project - .read_with(cx, |project, cx| { - project.find_project_path("dir/new_file", cx) - }) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - - // AI creates file with initial content - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| buffer.set_text("ai content", cx)); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - - project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) - .await - .unwrap(); - - cx.run_until_parked(); - - // User makes additional edits - cx.update(|cx| { - buffer.update(cx, |buffer, cx| { - buffer.edit([(10..10, "\nuser added this line")], None, cx); - }); - }); - - project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) - .await - .unwrap(); - - assert!(fs.is_file(path!("/dir/new_file").as_ref()).await); - - // Reject all - action_log - .update(cx, |log, cx| { - log.reject_edits_in_ranges( - buffer.clone(), - vec![Point::new(0, 0)..Point::new(100, 0)], - None, - cx, - ) - }) - .await - .unwrap(); - cx.run_until_parked(); - - // File should still contain all the content - assert!(fs.is_file(path!("/dir/new_file").as_ref()).await); - - let content = buffer.read_with(cx, |buffer, _| buffer.text()); - assert_eq!(content, "ai content\nuser added this line"); - } - - #[gpui::test] - async fn test_reject_after_accepting_hunk_on_created_file(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - - let file_path = project - .read_with(cx, |project, cx| { - project.find_project_path("dir/new_file", cx) - }) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path.clone(), cx)) - .await - .unwrap(); - - // AI creates file with initial content - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| buffer.set_text("ai content v1", cx)); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) - .await - .unwrap(); - cx.run_until_parked(); - assert_ne!(unreviewed_hunks(&action_log, cx), vec![]); - - // User accepts the single hunk - action_log.update(cx, |log, cx| { - let buffer_range = Anchor::min_max_range_for_buffer(buffer.read(cx).remote_id()); - log.keep_edits_in_range(buffer.clone(), buffer_range, None, cx) - }); - cx.run_until_parked(); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - assert!(fs.is_file(path!("/dir/new_file").as_ref()).await); - - // AI modifies the file - cx.update(|cx| { - buffer.update(cx, |buffer, cx| buffer.set_text("ai content v2", cx)); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) - .await - .unwrap(); - cx.run_until_parked(); - assert_ne!(unreviewed_hunks(&action_log, cx), vec![]); - - // User rejects the hunk - action_log - .update(cx, |log, cx| { - log.reject_edits_in_ranges( - buffer.clone(), - vec![Anchor::min_max_range_for_buffer( - buffer.read(cx).remote_id(), - )], - None, - cx, - ) - }) - .await - .unwrap(); - cx.run_until_parked(); - assert!(fs.is_file(path!("/dir/new_file").as_ref()).await,); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "ai content v1" - ); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test] - async fn test_reject_edits_on_previously_accepted_created_file(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - - let file_path = project - .read_with(cx, |project, cx| { - project.find_project_path("dir/new_file", cx) - }) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path.clone(), cx)) - .await - .unwrap(); - - // AI creates file with initial content - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| buffer.set_text("ai content v1", cx)); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) - .await - .unwrap(); - cx.run_until_parked(); - - // User clicks "Accept All" - action_log.update(cx, |log, cx| log.keep_all_edits(None, cx)); - cx.run_until_parked(); - assert!(fs.is_file(path!("/dir/new_file").as_ref()).await); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); // Hunks are cleared - - // AI modifies file again - cx.update(|cx| { - buffer.update(cx, |buffer, cx| buffer.set_text("ai content v2", cx)); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) - .await - .unwrap(); - cx.run_until_parked(); - assert_ne!(unreviewed_hunks(&action_log, cx), vec![]); - - // User clicks "Reject All" - action_log - .update(cx, |log, cx| log.reject_all_edits(None, cx)) - .await; - cx.run_until_parked(); - assert!(fs.is_file(path!("/dir/new_file").as_ref()).await); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "ai content v1" - ); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 100)] - async fn test_random_diffs(mut rng: StdRng, cx: &mut TestAppContext) { - init_test(cx); - - let operations = env::var("OPERATIONS") - .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) - .unwrap_or(20); - - let text = RandomCharIter::new(&mut rng).take(50).collect::(); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/dir"), json!({"file": text})).await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let file_path = project - .read_with(cx, |project, cx| project.find_project_path("dir/file", cx)) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - - action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); - - for _ in 0..operations { - match rng.random_range(0..100) { - 0..25 => { - action_log.update(cx, |log, cx| { - let range = buffer.read(cx).random_byte_range(0, &mut rng); - log::info!("keeping edits in range {:?}", range); - log.keep_edits_in_range(buffer.clone(), range, None, cx) - }); - } - 25..50 => { - action_log - .update(cx, |log, cx| { - let range = buffer.read(cx).random_byte_range(0, &mut rng); - log::info!("rejecting edits in range {:?}", range); - log.reject_edits_in_ranges(buffer.clone(), vec![range], None, cx) - }) - .await - .unwrap(); - } - _ => { - let is_agent_edit = rng.random_bool(0.5); - if is_agent_edit { - log::info!("agent edit"); - } else { - log::info!("user edit"); - } - cx.update(|cx| { - buffer.update(cx, |buffer, cx| buffer.randomly_edit(&mut rng, 1, cx)); - if is_agent_edit { - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - } - }); - } - } - - if rng.random_bool(0.2) { - quiesce(&action_log, &buffer, cx); - } - } - - quiesce(&action_log, &buffer, cx); - - fn quiesce( - action_log: &Entity, - buffer: &Entity, - cx: &mut TestAppContext, - ) { - log::info!("quiescing..."); - cx.run_until_parked(); - action_log.update(cx, |log, cx| { - let tracked_buffer = log.tracked_buffers.get(buffer).unwrap(); - let mut old_text = tracked_buffer.diff_base.clone(); - let new_text = buffer.read(cx).as_rope(); - for edit in tracked_buffer.unreviewed_edits.edits() { - let old_start = old_text.point_to_offset(Point::new(edit.new.start, 0)); - let old_end = old_text.point_to_offset(cmp::min( - Point::new(edit.new.start + edit.old_len(), 0), - old_text.max_point(), - )); - old_text.replace( - old_start..old_end, - &new_text.slice_rows(edit.new.clone()).to_string(), - ); - } - pretty_assertions::assert_eq!(old_text.to_string(), new_text.to_string()); - }) - } - } - - #[gpui::test] - async fn test_keep_edits_on_commit(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/project"), - json!({ - ".git": {}, - "file.txt": "a\nb\nc\nd\ne\nf\ng\nh\ni\nj", - }), - ) - .await; - fs.set_head_for_repo( - path!("/project/.git").as_ref(), - &[("file.txt", "a\nb\nc\nd\ne\nf\ng\nh\ni\nj".into())], - "0000000", - ); - cx.run_until_parked(); - - let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - - let file_path = project - .read_with(cx, |project, cx| { - project.find_project_path(path!("/project/file.txt"), cx) - }) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| { - buffer.edit( - [ - // Edit at the very start: a -> A - (Point::new(0, 0)..Point::new(0, 1), "A"), - // Deletion in the middle: remove lines d and e - (Point::new(3, 0)..Point::new(5, 0), ""), - // Modification: g -> GGG - (Point::new(6, 0)..Point::new(6, 1), "GGG"), - // Addition: insert new line after h - (Point::new(7, 1)..Point::new(7, 1), "\nNEW"), - // Edit the very last character: j -> J - (Point::new(9, 0)..Point::new(9, 1), "J"), - ], - None, - cx, - ); - }); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - cx.run_until_parked(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![ - HunkStatus { - range: Point::new(0, 0)..Point::new(1, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "a\n".into() - }, - HunkStatus { - range: Point::new(3, 0)..Point::new(3, 0), - diff_status: DiffHunkStatusKind::Deleted, - old_text: "d\ne\n".into() - }, - HunkStatus { - range: Point::new(4, 0)..Point::new(5, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "g\n".into() - }, - HunkStatus { - range: Point::new(6, 0)..Point::new(7, 0), - diff_status: DiffHunkStatusKind::Added, - old_text: "".into() - }, - HunkStatus { - range: Point::new(8, 0)..Point::new(8, 1), - diff_status: DiffHunkStatusKind::Modified, - old_text: "j".into() - } - ] - )] - ); - - // Simulate a git commit that matches some edits but not others: - // - Accepts the first edit (a -> A) - // - Accepts the deletion (remove d and e) - // - Makes a different change to g (g -> G instead of GGG) - // - Ignores the NEW line addition - // - Ignores the last line edit (j stays as j) - fs.set_head_for_repo( - path!("/project/.git").as_ref(), - &[("file.txt", "A\nb\nc\nf\nG\nh\ni\nj".into())], - "0000001", - ); - cx.run_until_parked(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![ - HunkStatus { - range: Point::new(4, 0)..Point::new(5, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "g\n".into() - }, - HunkStatus { - range: Point::new(6, 0)..Point::new(7, 0), - diff_status: DiffHunkStatusKind::Added, - old_text: "".into() - }, - HunkStatus { - range: Point::new(8, 0)..Point::new(8, 1), - diff_status: DiffHunkStatusKind::Modified, - old_text: "j".into() - } - ] - )] - ); - - // Make another commit that accepts the NEW line but with different content - fs.set_head_for_repo( - path!("/project/.git").as_ref(), - &[("file.txt", "A\nb\nc\nf\nGGG\nh\nDIFFERENT\ni\nj".into())], - "0000002", - ); - cx.run_until_parked(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer, - vec![ - HunkStatus { - range: Point::new(6, 0)..Point::new(7, 0), - diff_status: DiffHunkStatusKind::Added, - old_text: "".into() - }, - HunkStatus { - range: Point::new(8, 0)..Point::new(8, 1), - diff_status: DiffHunkStatusKind::Modified, - old_text: "j".into() - } - ] - )] - ); - - // Final commit that accepts all remaining edits - fs.set_head_for_repo( - path!("/project/.git").as_ref(), - &[("file.txt", "A\nb\nc\nf\nGGG\nh\nNEW\ni\nJ".into())], - "0000003", - ); - cx.run_until_parked(); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[derive(Debug, Clone, PartialEq, Eq)] - struct HunkStatus { - range: Range, - diff_status: DiffHunkStatusKind, - old_text: String, - } - - fn unreviewed_hunks( - action_log: &Entity, - cx: &TestAppContext, - ) -> Vec<(Entity, Vec)> { - cx.read(|cx| { - action_log - .read(cx) - .changed_buffers(cx) - .into_iter() - .map(|(buffer, diff)| { - let snapshot = buffer.read(cx).snapshot(); - ( - buffer, - diff.read(cx) - .hunks(&snapshot, cx) - .map(|hunk| HunkStatus { - diff_status: hunk.status().kind, - range: hunk.range, - old_text: diff - .read(cx) - .base_text() - .text_for_range(hunk.diff_base_byte_range) - .collect(), - }) - .collect(), - ) - }) - .collect() - }) - } -} diff --git a/crates/activity_indicator/Cargo.toml b/crates/activity_indicator/Cargo.toml deleted file mode 100644 index 8587e52723..0000000000 --- a/crates/activity_indicator/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "activity_indicator" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/activity_indicator.rs" -doctest = false - -[dependencies] -anyhow.workspace = true -auto_update.workspace = true -editor.workspace = true -extension_host.workspace = true -fs.workspace = true -futures.workspace = true -gpui.workspace = true -language.workspace = true -project.workspace = true -proto.workspace = true -semver.workspace = true -smallvec.workspace = true -ui.workspace = true -util.workspace = true -workspace.workspace = true - -[dev-dependencies] -editor = { workspace = true, features = ["test-support"] } -release_channel.workspace = true diff --git a/crates/activity_indicator/LICENSE-GPL b/crates/activity_indicator/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/activity_indicator/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs deleted file mode 100644 index b537fabc9b..0000000000 --- a/crates/activity_indicator/src/activity_indicator.rs +++ /dev/null @@ -1,947 +0,0 @@ -use auto_update::{AutoUpdateStatus, AutoUpdater, DismissMessage, VersionCheckType}; -use editor::Editor; -use extension_host::{ExtensionOperation, ExtensionStore}; -use futures::StreamExt; -use gpui::{ - App, Context, CursorStyle, Entity, EventEmitter, InteractiveElement as _, ParentElement as _, - Render, SharedString, StatefulInteractiveElement, Styled, Window, actions, -}; -use language::{ - BinaryStatus, LanguageRegistry, LanguageServerId, LanguageServerName, - LanguageServerStatusUpdate, ServerHealth, -}; -use project::{ - LanguageServerProgress, LspStoreEvent, ProgressToken, Project, ProjectEnvironmentEvent, - git_store::{GitStoreEvent, Repository}, -}; -use smallvec::SmallVec; -use std::{ - cmp::Reverse, - collections::HashSet, - fmt::Write, - sync::Arc, - time::{Duration, Instant}, -}; -use ui::{ - ButtonLike, CommonAnimationExt, ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, - prelude::*, -}; -use util::truncate_and_trailoff; -use workspace::{StatusItemView, Workspace, item::ItemHandle}; - -const GIT_OPERATION_DELAY: Duration = Duration::from_millis(0); - -actions!( - activity_indicator, - [ - /// Displays error messages from language servers in the status bar. - ShowErrorMessage - ] -); - -pub enum Event { - ShowStatus { - server_name: LanguageServerName, - status: SharedString, - }, -} - -pub struct ActivityIndicator { - statuses: Vec, - project: Entity, - auto_updater: Option>, - context_menu_handle: PopoverMenuHandle, - fs_jobs: Vec, -} - -#[derive(Debug)] -struct ServerStatus { - name: LanguageServerName, - status: LanguageServerStatusUpdate, -} - -struct PendingWork<'a> { - language_server_id: LanguageServerId, - progress_token: &'a ProgressToken, - progress: &'a LanguageServerProgress, -} - -struct Content { - icon: Option, - message: String, - on_click: - Option)>>, - tooltip_message: Option, -} - -impl ActivityIndicator { - pub fn new( - workspace: &mut Workspace, - languages: Arc, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - let project = workspace.project().clone(); - let auto_updater = AutoUpdater::get(cx); - let this = cx.new(|cx| { - let mut status_events = languages.language_server_binary_statuses(); - cx.spawn(async move |this, cx| { - while let Some((name, binary_status)) = status_events.next().await { - this.update(cx, |this: &mut ActivityIndicator, cx| { - this.statuses.retain(|s| s.name != name); - this.statuses.push(ServerStatus { - name, - status: LanguageServerStatusUpdate::Binary(binary_status), - }); - cx.notify(); - })?; - } - anyhow::Ok(()) - }) - .detach(); - - let fs = project.read(cx).fs().clone(); - let mut job_events = fs.subscribe_to_jobs(); - cx.spawn(async move |this, cx| { - while let Some(job_event) = job_events.next().await { - this.update(cx, |this: &mut ActivityIndicator, cx| { - match job_event { - fs::JobEvent::Started { info } => { - this.fs_jobs.retain(|j| j.id != info.id); - this.fs_jobs.push(info); - } - fs::JobEvent::Completed { id } => { - this.fs_jobs.retain(|j| j.id != id); - } - } - cx.notify(); - })?; - } - anyhow::Ok(()) - }) - .detach(); - - cx.subscribe( - &project.read(cx).lsp_store(), - |activity_indicator, _, event, cx| { - if let LspStoreEvent::LanguageServerUpdate { name, message, .. } = event { - if let proto::update_language_server::Variant::StatusUpdate(status_update) = - message - { - let Some(name) = name.clone() else { - return; - }; - let status = match &status_update.status { - Some(proto::status_update::Status::Binary(binary_status)) => { - if let Some(binary_status) = - proto::ServerBinaryStatus::from_i32(*binary_status) - { - let binary_status = match binary_status { - proto::ServerBinaryStatus::None => BinaryStatus::None, - proto::ServerBinaryStatus::CheckingForUpdate => { - BinaryStatus::CheckingForUpdate - } - proto::ServerBinaryStatus::Downloading => { - BinaryStatus::Downloading - } - proto::ServerBinaryStatus::Starting => { - BinaryStatus::Starting - } - proto::ServerBinaryStatus::Stopping => { - BinaryStatus::Stopping - } - proto::ServerBinaryStatus::Stopped => { - BinaryStatus::Stopped - } - proto::ServerBinaryStatus::Failed => { - let Some(error) = status_update.message.clone() - else { - return; - }; - BinaryStatus::Failed { error } - } - }; - LanguageServerStatusUpdate::Binary(binary_status) - } else { - return; - } - } - Some(proto::status_update::Status::Health(health_status)) => { - if let Some(health) = - proto::ServerHealth::from_i32(*health_status) - { - let health = match health { - proto::ServerHealth::Ok => ServerHealth::Ok, - proto::ServerHealth::Warning => ServerHealth::Warning, - proto::ServerHealth::Error => ServerHealth::Error, - }; - LanguageServerStatusUpdate::Health( - health, - status_update.message.clone().map(SharedString::from), - ) - } else { - return; - } - } - None => return, - }; - - activity_indicator.statuses.retain(|s| s.name != name); - activity_indicator - .statuses - .push(ServerStatus { name, status }); - } - cx.notify() - } - }, - ) - .detach(); - - cx.subscribe( - &project.read(cx).environment().clone(), - |_, _, event, cx| match event { - ProjectEnvironmentEvent::ErrorsUpdated => cx.notify(), - }, - ) - .detach(); - - cx.subscribe( - &project.read(cx).git_store().clone(), - |_, _, event: &GitStoreEvent, cx| { - if let project::git_store::GitStoreEvent::JobsUpdated = event { - cx.notify() - } - }, - ) - .detach(); - - if let Some(auto_updater) = auto_updater.as_ref() { - cx.observe(auto_updater, |_, _, cx| cx.notify()).detach(); - } - - Self { - statuses: Vec::new(), - project: project.clone(), - auto_updater, - context_menu_handle: PopoverMenuHandle::default(), - fs_jobs: Vec::new(), - } - }); - - cx.subscribe_in(&this, window, move |_, _, event, window, cx| match event { - Event::ShowStatus { - server_name, - status, - } => { - let create_buffer = - project.update(cx, |project, cx| project.create_buffer(false, cx)); - let status = status.clone(); - let server_name = server_name.clone(); - cx.spawn_in(window, async move |workspace, cx| { - let buffer = create_buffer.await?; - buffer.update(cx, |buffer, cx| { - buffer.edit( - [(0..0, format!("Language server {server_name}:\n\n{status}"))], - None, - cx, - ); - buffer.set_capability(language::Capability::ReadOnly, cx); - })?; - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane( - Box::new(cx.new(|cx| { - let mut editor = Editor::for_buffer(buffer, None, window, cx); - editor.set_read_only(true); - editor - })), - None, - true, - window, - cx, - ); - })?; - - anyhow::Ok(()) - }) - .detach(); - } - }) - .detach(); - this - } - - fn show_error_message(&mut self, _: &ShowErrorMessage, _: &mut Window, cx: &mut Context) { - let mut status_message_shown = false; - self.statuses.retain(|status| match &status.status { - LanguageServerStatusUpdate::Binary(BinaryStatus::Failed { error }) - if !status_message_shown => - { - cx.emit(Event::ShowStatus { - server_name: status.name.clone(), - status: SharedString::from(error), - }); - status_message_shown = true; - false - } - LanguageServerStatusUpdate::Health( - ServerHealth::Error | ServerHealth::Warning, - status_string, - ) if !status_message_shown => match status_string { - Some(error) => { - cx.emit(Event::ShowStatus { - server_name: status.name.clone(), - status: error.clone(), - }); - status_message_shown = true; - false - } - None => false, - }, - _ => true, - }); - } - - fn dismiss_message(&mut self, _: &DismissMessage, _: &mut Window, cx: &mut Context) { - let dismissed = if let Some(updater) = &self.auto_updater { - updater.update(cx, |updater, cx| updater.dismiss(cx)) - } else { - false - }; - if dismissed { - return; - } - - self.project.update(cx, |project, cx| { - if project.last_formatting_failure(cx).is_some() { - project.reset_last_formatting_failure(cx); - true - } else { - false - } - }); - } - - fn pending_language_server_work<'a>( - &self, - cx: &'a App, - ) -> impl Iterator> { - self.project - .read(cx) - .language_server_statuses(cx) - .rev() - .filter_map(|(server_id, status)| { - if status.pending_work.is_empty() { - None - } else { - let mut pending_work = status - .pending_work - .iter() - .map(|(progress_token, progress)| PendingWork { - language_server_id: server_id, - progress_token, - progress, - }) - .collect::>(); - pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at)); - Some(pending_work) - } - }) - .flatten() - } - - fn pending_environment_error<'a>(&'a self, cx: &'a App) -> Option<&'a String> { - self.project.read(cx).peek_environment_error(cx) - } - - fn content_to_render(&mut self, cx: &mut Context) -> Option { - // Show if any direnv calls failed - if let Some(message) = self.pending_environment_error(cx) { - return Some(Content { - icon: Some( - Icon::new(IconName::Warning) - .size(IconSize::Small) - .into_any_element(), - ), - message: message.clone(), - on_click: Some(Arc::new(move |this, window, cx| { - this.project.update(cx, |project, cx| { - project.pop_environment_error(cx); - }); - window.dispatch_action(Box::new(workspace::OpenLog), cx); - })), - tooltip_message: None, - }); - } - // Show any language server has pending activity. - { - let mut pending_work = self.pending_language_server_work(cx); - if let Some(PendingWork { - progress_token, - progress, - .. - }) = pending_work.next() - { - let mut message = progress.title.clone().unwrap_or(progress_token.to_string()); - - if let Some(percentage) = progress.percentage { - write!(&mut message, " ({}%)", percentage).unwrap(); - } - - if let Some(progress_message) = progress.message.as_ref() { - message.push_str(": "); - message.push_str(progress_message); - } - - let additional_work_count = pending_work.count(); - if additional_work_count > 0 { - write!(&mut message, " + {} more", additional_work_count).unwrap(); - } - - return Some(Content { - icon: Some( - Icon::new(IconName::ArrowCircle) - .size(IconSize::Small) - .with_rotate_animation(2) - .into_any_element(), - ), - message, - on_click: Some(Arc::new(Self::toggle_language_server_work_context_menu)), - tooltip_message: None, - }); - } - } - - if let Some(session) = self - .project - .read(cx) - .dap_store() - .read(cx) - .sessions() - .find(|s| !s.read(cx).is_started()) - { - return Some(Content { - icon: Some( - Icon::new(IconName::ArrowCircle) - .size(IconSize::Small) - .with_rotate_animation(2) - .into_any_element(), - ), - message: format!("Debug: {}", session.read(cx).adapter()), - tooltip_message: session.read(cx).label().map(|label| label.to_string()), - on_click: None, - }); - } - - let current_job = self - .project - .read(cx) - .active_repository(cx) - .map(|r| r.read(cx)) - .and_then(Repository::current_job); - // Show any long-running git command - if let Some(job_info) = current_job - && Instant::now() - job_info.start >= GIT_OPERATION_DELAY - { - return Some(Content { - icon: Some( - Icon::new(IconName::ArrowCircle) - .size(IconSize::Small) - .with_rotate_animation(2) - .into_any_element(), - ), - message: job_info.message.into(), - on_click: None, - tooltip_message: None, - }); - } - - // Show any long-running fs command - for fs_job in &self.fs_jobs { - if Instant::now().duration_since(fs_job.start) >= GIT_OPERATION_DELAY { - return Some(Content { - icon: Some( - Icon::new(IconName::ArrowCircle) - .size(IconSize::Small) - .with_rotate_animation(2) - .into_any_element(), - ), - message: fs_job.message.clone().into(), - on_click: None, - tooltip_message: None, - }); - } - } - - // Show any language server installation info. - let mut downloading = SmallVec::<[_; 3]>::new(); - let mut checking_for_update = SmallVec::<[_; 3]>::new(); - let mut failed = SmallVec::<[_; 3]>::new(); - let mut health_messages = SmallVec::<[_; 3]>::new(); - let mut servers_to_clear_statuses = HashSet::::default(); - for status in &self.statuses { - match &status.status { - LanguageServerStatusUpdate::Binary( - BinaryStatus::Starting | BinaryStatus::Stopping, - ) => {} - LanguageServerStatusUpdate::Binary(BinaryStatus::Stopped) => { - servers_to_clear_statuses.insert(status.name.clone()); - } - LanguageServerStatusUpdate::Binary(BinaryStatus::CheckingForUpdate) => { - checking_for_update.push(status.name.clone()); - } - LanguageServerStatusUpdate::Binary(BinaryStatus::Downloading) => { - downloading.push(status.name.clone()); - } - LanguageServerStatusUpdate::Binary(BinaryStatus::Failed { .. }) => { - failed.push(status.name.clone()); - } - LanguageServerStatusUpdate::Binary(BinaryStatus::None) => {} - LanguageServerStatusUpdate::Health(health, server_status) => match server_status { - Some(server_status) => { - health_messages.push((status.name.clone(), *health, server_status.clone())); - } - None => { - servers_to_clear_statuses.insert(status.name.clone()); - } - }, - } - } - self.statuses - .retain(|status| !servers_to_clear_statuses.contains(&status.name)); - - health_messages.sort_by_key(|(_, health, _)| match health { - ServerHealth::Error => 2, - ServerHealth::Warning => 1, - ServerHealth::Ok => 0, - }); - - if !downloading.is_empty() { - return Some(Content { - icon: Some( - Icon::new(IconName::Download) - .size(IconSize::Small) - .into_any_element(), - ), - message: format!( - "Downloading {}...", - downloading.iter().map(|name| name.as_ref()).fold( - String::new(), - |mut acc, s| { - if !acc.is_empty() { - acc.push_str(", "); - } - acc.push_str(s); - acc - } - ) - ), - on_click: Some(Arc::new(move |this, window, cx| { - this.statuses - .retain(|status| !downloading.contains(&status.name)); - this.dismiss_message(&DismissMessage, window, cx) - })), - tooltip_message: None, - }); - } - - if !checking_for_update.is_empty() { - return Some(Content { - icon: Some( - Icon::new(IconName::Download) - .size(IconSize::Small) - .into_any_element(), - ), - message: format!( - "Checking for updates to {}...", - checking_for_update.iter().map(|name| name.as_ref()).fold( - String::new(), - |mut acc, s| { - if !acc.is_empty() { - acc.push_str(", "); - } - acc.push_str(s); - acc - } - ), - ), - on_click: Some(Arc::new(move |this, window, cx| { - this.statuses - .retain(|status| !checking_for_update.contains(&status.name)); - this.dismiss_message(&DismissMessage, window, cx) - })), - tooltip_message: None, - }); - } - - if !failed.is_empty() { - return Some(Content { - icon: Some( - Icon::new(IconName::Warning) - .size(IconSize::Small) - .into_any_element(), - ), - message: format!( - "Failed to run {}. Click to show error.", - failed - .iter() - .map(|name| name.as_ref()) - .fold(String::new(), |mut acc, s| { - if !acc.is_empty() { - acc.push_str(", "); - } - acc.push_str(s); - acc - }), - ), - on_click: Some(Arc::new(|this, window, cx| { - this.show_error_message(&ShowErrorMessage, window, cx) - })), - tooltip_message: None, - }); - } - - // Show any formatting failure - if let Some(failure) = self.project.read(cx).last_formatting_failure(cx) { - return Some(Content { - icon: Some( - Icon::new(IconName::Warning) - .size(IconSize::Small) - .into_any_element(), - ), - message: format!("Formatting failed: {failure}. Click to see logs."), - on_click: Some(Arc::new(|indicator, window, cx| { - indicator.project.update(cx, |project, cx| { - project.reset_last_formatting_failure(cx); - }); - window.dispatch_action(Box::new(workspace::OpenLog), cx); - })), - tooltip_message: None, - }); - } - - // Show any health messages for the language servers - if let Some((server_name, health, message)) = health_messages.pop() { - let health_str = match health { - ServerHealth::Ok => format!("({server_name}) "), - ServerHealth::Warning => format!("({server_name}) Warning: "), - ServerHealth::Error => format!("({server_name}) Error: "), - }; - let single_line_message = message - .lines() - .filter_map(|line| { - let line = line.trim(); - if line.is_empty() { None } else { Some(line) } - }) - .collect::>() - .join(" "); - let mut altered_message = single_line_message != message; - let truncated_message = truncate_and_trailoff( - &single_line_message, - MAX_MESSAGE_LEN.saturating_sub(health_str.len()), - ); - altered_message |= truncated_message != single_line_message; - let final_message = format!("{health_str}{truncated_message}"); - - let tooltip_message = if altered_message { - Some(format!("{health_str}{message}")) - } else { - None - }; - - return Some(Content { - icon: Some( - Icon::new(IconName::Warning) - .size(IconSize::Small) - .into_any_element(), - ), - message: final_message, - tooltip_message, - on_click: Some(Arc::new(move |activity_indicator, window, cx| { - if altered_message { - activity_indicator.show_error_message(&ShowErrorMessage, window, cx) - } else { - activity_indicator - .statuses - .retain(|status| status.name != server_name); - cx.notify(); - } - })), - }); - } - - // Show any application auto-update info. - self.auto_updater - .as_ref() - .and_then(|updater| match &updater.read(cx).status() { - AutoUpdateStatus::Checking => Some(Content { - icon: Some( - Icon::new(IconName::LoadCircle) - .size(IconSize::Small) - .with_rotate_animation(3) - .into_any_element(), - ), - message: "Checking for Zed updates…".to_string(), - on_click: Some(Arc::new(|this, window, cx| { - this.dismiss_message(&DismissMessage, window, cx) - })), - tooltip_message: None, - }), - AutoUpdateStatus::Downloading { version } => Some(Content { - icon: Some( - Icon::new(IconName::Download) - .size(IconSize::Small) - .into_any_element(), - ), - message: "Downloading Zed update…".to_string(), - on_click: Some(Arc::new(|this, window, cx| { - this.dismiss_message(&DismissMessage, window, cx) - })), - tooltip_message: Some(Self::version_tooltip_message(version)), - }), - AutoUpdateStatus::Installing { version } => Some(Content { - icon: Some( - Icon::new(IconName::LoadCircle) - .size(IconSize::Small) - .with_rotate_animation(3) - .into_any_element(), - ), - message: "Installing Zed update…".to_string(), - on_click: Some(Arc::new(|this, window, cx| { - this.dismiss_message(&DismissMessage, window, cx) - })), - tooltip_message: Some(Self::version_tooltip_message(version)), - }), - AutoUpdateStatus::Updated { version } => Some(Content { - icon: None, - message: "Click to restart and update Zed".to_string(), - on_click: Some(Arc::new(move |_, _, cx| workspace::reload(cx))), - tooltip_message: Some(Self::version_tooltip_message(version)), - }), - AutoUpdateStatus::Errored { error } => Some(Content { - icon: Some( - Icon::new(IconName::Warning) - .size(IconSize::Small) - .into_any_element(), - ), - message: "Failed to update Zed".to_string(), - on_click: Some(Arc::new(|this, window, cx| { - window.dispatch_action(Box::new(workspace::OpenLog), cx); - this.dismiss_message(&DismissMessage, window, cx); - })), - tooltip_message: Some(format!("{error}")), - }), - AutoUpdateStatus::Idle => None, - }) - .or_else(|| { - if let Some(extension_store) = - ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx)) - && let Some((extension_id, operation)) = - extension_store.outstanding_operations().iter().next() - { - let (message, icon, rotate) = match operation { - ExtensionOperation::Install => ( - format!("Installing {extension_id} extension…"), - IconName::LoadCircle, - true, - ), - ExtensionOperation::Upgrade => ( - format!("Updating {extension_id} extension…"), - IconName::Download, - false, - ), - ExtensionOperation::Remove => ( - format!("Removing {extension_id} extension…"), - IconName::LoadCircle, - true, - ), - }; - - Some(Content { - icon: Some(Icon::new(icon).size(IconSize::Small).map(|this| { - if rotate { - this.with_rotate_animation(3).into_any_element() - } else { - this.into_any_element() - } - })), - message, - on_click: Some(Arc::new(|this, window, cx| { - this.dismiss_message(&Default::default(), window, cx) - })), - tooltip_message: None, - }) - } else { - None - } - }) - } - - fn version_tooltip_message(version: &VersionCheckType) -> String { - format!("Version: {}", { - match version { - auto_update::VersionCheckType::Sha(sha) => format!("{}…", sha.short()), - auto_update::VersionCheckType::Semantic(semantic_version) => { - semantic_version.to_string() - } - } - }) - } - - fn toggle_language_server_work_context_menu( - &mut self, - window: &mut Window, - cx: &mut Context, - ) { - self.context_menu_handle.toggle(window, cx); - } -} - -impl EventEmitter for ActivityIndicator {} - -const MAX_MESSAGE_LEN: usize = 50; - -impl Render for ActivityIndicator { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let result = h_flex() - .id("activity-indicator") - .on_action(cx.listener(Self::show_error_message)) - .on_action(cx.listener(Self::dismiss_message)); - let Some(content) = self.content_to_render(cx) else { - return result; - }; - let activity_indicator = cx.entity().downgrade(); - let truncate_content = content.message.len() > MAX_MESSAGE_LEN; - result.gap_2().child( - PopoverMenu::new("activity-indicator-popover") - .trigger( - ButtonLike::new("activity-indicator-trigger").child( - h_flex() - .id("activity-indicator-status") - .gap_2() - .children(content.icon) - .map(|button| { - if truncate_content { - button - .child( - Label::new(truncate_and_trailoff( - &content.message, - MAX_MESSAGE_LEN, - )) - .size(LabelSize::Small), - ) - .tooltip(Tooltip::text(content.message)) - } else { - button - .child(Label::new(content.message).size(LabelSize::Small)) - .when_some( - content.tooltip_message, - |this, tooltip_message| { - this.tooltip(Tooltip::text(tooltip_message)) - }, - ) - } - }) - .when_some(content.on_click, |this, handler| { - this.on_click(cx.listener(move |this, _, window, cx| { - handler(this, window, cx); - })) - .cursor(CursorStyle::PointingHand) - }), - ), - ) - .anchor(gpui::Corner::BottomLeft) - .menu(move |window, cx| { - let strong_this = activity_indicator.upgrade()?; - let mut has_work = false; - let menu = ContextMenu::build(window, cx, |mut menu, _, cx| { - for work in strong_this.read(cx).pending_language_server_work(cx) { - has_work = true; - let activity_indicator = activity_indicator.clone(); - let mut title = work - .progress - .title - .clone() - .unwrap_or(work.progress_token.to_string()); - - if work.progress.is_cancellable { - let language_server_id = work.language_server_id; - let token = work.progress_token.clone(); - let title = SharedString::from(title); - menu = menu.custom_entry( - move |_, _| { - h_flex() - .w_full() - .justify_between() - .child(Label::new(title.clone())) - .child(Icon::new(IconName::XCircle)) - .into_any_element() - }, - move |_, cx| { - let token = token.clone(); - activity_indicator - .update(cx, |activity_indicator, cx| { - activity_indicator.project.update( - cx, - |project, cx| { - project.cancel_language_server_work( - language_server_id, - Some(token), - cx, - ); - }, - ); - activity_indicator.context_menu_handle.hide(cx); - cx.notify(); - }) - .ok(); - }, - ); - } else { - if let Some(progress_message) = work.progress.message.as_ref() { - title.push_str(": "); - title.push_str(progress_message); - } - - menu = menu.label(title); - } - } - menu - }); - has_work.then_some(menu) - }), - ) - } -} - -impl StatusItemView for ActivityIndicator { - fn set_active_pane_item( - &mut self, - _: Option<&dyn ItemHandle>, - _window: &mut Window, - _: &mut Context, - ) { - } -} - -#[cfg(test)] -mod tests { - use release_channel::AppCommitSha; - use semver::Version; - - use super::*; - - #[test] - fn test_version_tooltip_message() { - let message = ActivityIndicator::version_tooltip_message(&VersionCheckType::Semantic( - Version::new(1, 0, 0), - )); - - assert_eq!(message, "Version: 1.0.0"); - - let message = ActivityIndicator::version_tooltip_message(&VersionCheckType::Sha( - AppCommitSha::new("14d9a4189f058d8736339b06ff2340101eaea5af".to_string()), - )); - - assert_eq!(message, "Version: 14d9a41…"); - } -} diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml deleted file mode 100644 index 667033a1bb..0000000000 --- a/crates/agent/Cargo.toml +++ /dev/null @@ -1,105 +0,0 @@ -[package] -name = "agent" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lib] -path = "src/agent.rs" - -[features] -test-support = ["db/test-support"] -eval = [] -unit-eval = [] -e2e = [] - -[lints] -workspace = true - -[dependencies] -acp_thread.workspace = true -action_log.workspace = true -agent-client-protocol.workspace = true -agent_servers.workspace = true -agent_settings.workspace = true -anyhow.workspace = true -assistant_text_thread.workspace = true -chrono.workspace = true -client.workspace = true -cloud_llm_client.workspace = true -collections.workspace = true -context_server.workspace = true -db.workspace = true -derive_more.workspace = true -fs.workspace = true -futures.workspace = true -git.workspace = true -gpui.workspace = true -handlebars = { workspace = true, features = ["rust-embed"] } -html_to_markdown.workspace = true -http_client.workspace = true -indoc.workspace = true -itertools.workspace = true -language.workspace = true -language_model.workspace = true -language_models.workspace = true -log.workspace = true -open.workspace = true -parking_lot.workspace = true -paths.workspace = true -project.workspace = true -prompt_store.workspace = true -regex.workspace = true -rust-embed.workspace = true -schemars.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -smallvec.workspace = true -smol.workspace = true -sqlez.workspace = true -streaming_diff.workspace = true -strsim.workspace = true -task.workspace = true -telemetry.workspace = true -text.workspace = true -thiserror.workspace = true -ui.workspace = true -util.workspace = true -uuid.workspace = true -watch.workspace = true -web_search.workspace = true -zed_env_vars.workspace = true -zstd.workspace = true - -[dev-dependencies] -agent_servers = { workspace = true, "features" = ["test-support"] } -assistant_text_thread = { workspace = true, "features" = ["test-support"] } -client = { workspace = true, "features" = ["test-support"] } -clock = { workspace = true, "features" = ["test-support"] } -context_server = { workspace = true, "features" = ["test-support"] } -ctor.workspace = true -db = { workspace = true, "features" = ["test-support"] } -editor = { workspace = true, "features" = ["test-support"] } -env_logger.workspace = true -eval_utils.workspace = true -fs = { workspace = true, "features" = ["test-support"] } -git = { workspace = true, "features" = ["test-support"] } -gpui = { workspace = true, "features" = ["test-support"] } -gpui_tokio.workspace = true -language = { workspace = true, "features" = ["test-support"] } -language_model = { workspace = true, "features" = ["test-support"] } -lsp = { workspace = true, "features" = ["test-support"] } -pretty_assertions.workspace = true -project = { workspace = true, "features" = ["test-support"] } -rand.workspace = true -reqwest_client.workspace = true -settings = { workspace = true, "features" = ["test-support"] } -tempfile.workspace = true -terminal = { workspace = true, "features" = ["test-support"] } -theme = { workspace = true, "features" = ["test-support"] } -tree-sitter-rust.workspace = true -unindent = { workspace = true } -worktree = { workspace = true, "features" = ["test-support"] } -zlog.workspace = true diff --git a/crates/agent/LICENSE-GPL b/crates/agent/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/agent/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs deleted file mode 100644 index cf98a24ac5..0000000000 --- a/crates/agent/src/agent.rs +++ /dev/null @@ -1,1608 +0,0 @@ -mod db; -mod edit_agent; -mod history_store; -mod legacy_thread; -mod native_agent_server; -pub mod outline; -mod templates; -mod thread; -mod tools; - -#[cfg(test)] -mod tests; - -pub use db::*; -pub use history_store::*; -pub use native_agent_server::NativeAgentServer; -pub use templates::*; -pub use thread::*; -pub use tools::*; - -use acp_thread::{AcpThread, AgentModelSelector}; -use agent_client_protocol as acp; -use anyhow::{Context as _, Result, anyhow}; -use chrono::{DateTime, Utc}; -use collections::{HashSet, IndexMap}; -use fs::Fs; -use futures::channel::{mpsc, oneshot}; -use futures::future::Shared; -use futures::{StreamExt, future}; -use gpui::{ - App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity, -}; -use language_model::{LanguageModel, LanguageModelProvider, LanguageModelRegistry}; -use project::{Project, ProjectItem, ProjectPath, Worktree}; -use prompt_store::{ - ProjectContext, PromptStore, RulesFileContext, UserRulesContext, WorktreeContext, -}; -use serde::{Deserialize, Serialize}; -use settings::{LanguageModelSelection, update_settings_file}; -use std::any::Any; -use std::collections::HashMap; -use std::path::{Path, PathBuf}; -use std::rc::Rc; -use std::sync::Arc; -use util::ResultExt; -use util::rel_path::RelPath; - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] -pub struct ProjectSnapshot { - pub worktree_snapshots: Vec, - pub timestamp: DateTime, -} - -const RULES_FILE_NAMES: [&str; 9] = [ - ".rules", - ".cursorrules", - ".windsurfrules", - ".clinerules", - ".github/copilot-instructions.md", - "CLAUDE.md", - "AGENT.md", - "AGENTS.md", - "GEMINI.md", -]; - -pub struct RulesLoadingError { - pub message: SharedString, -} - -/// Holds both the internal Thread and the AcpThread for a session -struct Session { - /// The internal thread that processes messages - thread: Entity, - /// The ACP thread that handles protocol communication - acp_thread: WeakEntity, - pending_save: Task<()>, - _subscriptions: Vec, -} - -pub struct LanguageModels { - /// Access language model by ID - models: HashMap>, - /// Cached list for returning language model information - model_list: acp_thread::AgentModelList, - refresh_models_rx: watch::Receiver<()>, - refresh_models_tx: watch::Sender<()>, - _authenticate_all_providers_task: Task<()>, -} - -impl LanguageModels { - fn new(cx: &mut App) -> Self { - let (refresh_models_tx, refresh_models_rx) = watch::channel(()); - - let mut this = Self { - models: HashMap::default(), - model_list: acp_thread::AgentModelList::Grouped(IndexMap::default()), - refresh_models_rx, - refresh_models_tx, - _authenticate_all_providers_task: Self::authenticate_all_language_model_providers(cx), - }; - this.refresh_list(cx); - this - } - - fn refresh_list(&mut self, cx: &App) { - let providers = LanguageModelRegistry::global(cx) - .read(cx) - .providers() - .into_iter() - .filter(|provider| provider.is_authenticated(cx)) - .collect::>(); - - let mut language_model_list = IndexMap::default(); - let mut recommended_models = HashSet::default(); - - let mut recommended = Vec::new(); - for provider in &providers { - for model in provider.recommended_models(cx) { - recommended_models.insert((model.provider_id(), model.id())); - recommended.push(Self::map_language_model_to_info(&model, provider)); - } - } - if !recommended.is_empty() { - language_model_list.insert( - acp_thread::AgentModelGroupName("Recommended".into()), - recommended, - ); - } - - let mut models = HashMap::default(); - for provider in providers { - let mut provider_models = Vec::new(); - for model in provider.provided_models(cx) { - let model_info = Self::map_language_model_to_info(&model, &provider); - let model_id = model_info.id.clone(); - provider_models.push(model_info); - models.insert(model_id, model); - } - if !provider_models.is_empty() { - language_model_list.insert( - acp_thread::AgentModelGroupName(provider.name().0.clone()), - provider_models, - ); - } - } - - self.models = models; - self.model_list = acp_thread::AgentModelList::Grouped(language_model_list); - self.refresh_models_tx.send(()).ok(); - } - - fn watch(&self) -> watch::Receiver<()> { - self.refresh_models_rx.clone() - } - - pub fn model_from_id(&self, model_id: &acp::ModelId) -> Option> { - self.models.get(model_id).cloned() - } - - fn map_language_model_to_info( - model: &Arc, - provider: &Arc, - ) -> acp_thread::AgentModelInfo { - acp_thread::AgentModelInfo { - id: Self::model_id(model), - name: model.name().0, - description: None, - icon: Some(provider.icon()), - } - } - - fn model_id(model: &Arc) -> acp::ModelId { - acp::ModelId::new(format!("{}/{}", model.provider_id().0, model.id().0)) - } - - fn authenticate_all_language_model_providers(cx: &mut App) -> Task<()> { - let authenticate_all_providers = LanguageModelRegistry::global(cx) - .read(cx) - .providers() - .iter() - .map(|provider| (provider.id(), provider.name(), provider.authenticate(cx))) - .collect::>(); - - cx.background_spawn(async move { - for (provider_id, provider_name, authenticate_task) in authenticate_all_providers { - if let Err(err) = authenticate_task.await { - match err { - language_model::AuthenticateError::CredentialsNotFound => { - // Since we're authenticating these providers in the - // background for the purposes of populating the - // language selector, we don't care about providers - // where the credentials are not found. - } - language_model::AuthenticateError::ConnectionRefused => { - // Not logging connection refused errors as they are mostly from LM Studio's noisy auth failures. - // LM Studio only has one auth method (endpoint call) which fails for users who haven't enabled it. - // TODO: Better manage LM Studio auth logic to avoid these noisy failures. - } - _ => { - // Some providers have noisy failure states that we - // don't want to spam the logs with every time the - // language model selector is initialized. - // - // Ideally these should have more clear failure modes - // that we know are safe to ignore here, like what we do - // with `CredentialsNotFound` above. - match provider_id.0.as_ref() { - "lmstudio" | "ollama" => { - // LM Studio and Ollama both make fetch requests to the local APIs to determine if they are "authenticated". - // - // These fail noisily, so we don't log them. - } - "copilot_chat" => { - // Copilot Chat returns an error if Copilot is not enabled, so we don't log those errors. - } - _ => { - log::error!( - "Failed to authenticate provider: {}: {err:#}", - provider_name.0 - ); - } - } - } - } - } - } - }) - } -} - -pub struct NativeAgent { - /// Session ID -> Session mapping - sessions: HashMap, - history: Entity, - /// Shared project context for all threads - project_context: Entity, - project_context_needs_refresh: watch::Sender<()>, - _maintain_project_context: Task>, - context_server_registry: Entity, - /// Shared templates for all threads - templates: Arc, - /// Cached model information - models: LanguageModels, - project: Entity, - prompt_store: Option>, - fs: Arc, - _subscriptions: Vec, -} - -impl NativeAgent { - pub async fn new( - project: Entity, - history: Entity, - templates: Arc, - prompt_store: Option>, - fs: Arc, - cx: &mut AsyncApp, - ) -> Result> { - log::debug!("Creating new NativeAgent"); - - let project_context = cx - .update(|cx| Self::build_project_context(&project, prompt_store.as_ref(), cx))? - .await; - - cx.new(|cx| { - let mut subscriptions = vec![ - cx.subscribe(&project, Self::handle_project_event), - cx.subscribe( - &LanguageModelRegistry::global(cx), - Self::handle_models_updated_event, - ), - ]; - if let Some(prompt_store) = prompt_store.as_ref() { - subscriptions.push(cx.subscribe(prompt_store, Self::handle_prompts_updated_event)) - } - - let (project_context_needs_refresh_tx, project_context_needs_refresh_rx) = - watch::channel(()); - Self { - sessions: HashMap::new(), - history, - project_context: cx.new(|_| project_context), - project_context_needs_refresh: project_context_needs_refresh_tx, - _maintain_project_context: cx.spawn(async move |this, cx| { - Self::maintain_project_context(this, project_context_needs_refresh_rx, cx).await - }), - context_server_registry: cx.new(|cx| { - ContextServerRegistry::new(project.read(cx).context_server_store(), cx) - }), - templates, - models: LanguageModels::new(cx), - project, - prompt_store, - fs, - _subscriptions: subscriptions, - } - }) - } - - fn register_session( - &mut self, - thread_handle: Entity, - cx: &mut Context, - ) -> Entity { - let connection = Rc::new(NativeAgentConnection(cx.entity())); - - let thread = thread_handle.read(cx); - let session_id = thread.id().clone(); - 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| { - acp_thread::AcpThread::new( - title, - connection, - project.clone(), - action_log.clone(), - session_id.clone(), - prompt_capabilities_rx, - cx, - ) - }); - - let registry = LanguageModelRegistry::read_global(cx); - let summarization_model = registry.thread_summary_model().map(|c| c.model); - - thread_handle.update(cx, |thread, cx| { - thread.set_summarization_model(summarization_model, cx); - thread.add_default_tools( - Rc::new(AcpThreadEnvironment { - acp_thread: acp_thread.downgrade(), - }) as _, - 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) - }), - ]; - - self.sessions.insert( - session_id, - Session { - thread: thread_handle, - acp_thread: acp_thread.downgrade(), - _subscriptions: subscriptions, - pending_save: Task::ready(()), - }, - ); - acp_thread - } - - pub fn models(&self) -> &LanguageModels { - &self.models - } - - async fn maintain_project_context( - this: WeakEntity, - mut needs_refresh: watch::Receiver<()>, - cx: &mut AsyncApp, - ) -> Result<()> { - while needs_refresh.changed().await.is_ok() { - let project_context = this - .update(cx, |this, cx| { - Self::build_project_context(&this.project, this.prompt_store.as_ref(), cx) - })? - .await; - this.update(cx, |this, cx| { - this.project_context = cx.new(|_| project_context); - })?; - } - - Ok(()) - } - - fn build_project_context( - project: &Entity, - prompt_store: Option<&Entity>, - cx: &mut App, - ) -> Task { - let worktrees = project.read(cx).visible_worktrees(cx).collect::>(); - let worktree_tasks = worktrees - .into_iter() - .map(|worktree| { - Self::load_worktree_info_for_system_prompt(worktree, project.clone(), cx) - }) - .collect::>(); - let default_user_rules_task = if let Some(prompt_store) = prompt_store.as_ref() { - prompt_store.read_with(cx, |prompt_store, cx| { - let prompts = prompt_store.default_prompt_metadata(); - let load_tasks = prompts.into_iter().map(|prompt_metadata| { - let contents = prompt_store.load(prompt_metadata.id, cx); - async move { (contents.await, prompt_metadata) } - }); - cx.background_spawn(future::join_all(load_tasks)) - }) - } else { - Task::ready(vec![]) - }; - - cx.spawn(async move |_cx| { - let (worktrees, default_user_rules) = - future::join(future::join_all(worktree_tasks), default_user_rules_task).await; - - let worktrees = worktrees - .into_iter() - .map(|(worktree, _rules_error)| { - // TODO: show error message - // if let Some(rules_error) = rules_error { - // this.update(cx, |_, cx| cx.emit(rules_error)).ok(); - // } - worktree - }) - .collect::>(); - - let default_user_rules = default_user_rules - .into_iter() - .flat_map(|(contents, prompt_metadata)| match contents { - Ok(contents) => Some(UserRulesContext { - uuid: match prompt_metadata.id { - prompt_store::PromptId::User { uuid } => uuid, - prompt_store::PromptId::EditWorkflow => return None, - }, - title: prompt_metadata.title.map(|title| title.to_string()), - contents, - }), - Err(_err) => { - // TODO: show error message - // this.update(cx, |_, cx| { - // cx.emit(RulesLoadingError { - // message: format!("{err:?}").into(), - // }); - // }) - // .ok(); - None - } - }) - .collect::>(); - - ProjectContext::new(worktrees, default_user_rules) - }) - } - - fn load_worktree_info_for_system_prompt( - worktree: Entity, - project: Entity, - cx: &mut App, - ) -> Task<(WorktreeContext, Option)> { - let tree = worktree.read(cx); - let root_name = tree.root_name_str().into(); - let abs_path = tree.abs_path(); - - let mut context = WorktreeContext { - root_name, - abs_path, - rules_file: None, - }; - - let rules_task = Self::load_worktree_rules_file(worktree, project, cx); - let Some(rules_task) = rules_task else { - return Task::ready((context, None)); - }; - - cx.spawn(async move |_| { - let (rules_file, rules_file_error) = match rules_task.await { - Ok(rules_file) => (Some(rules_file), None), - Err(err) => ( - None, - Some(RulesLoadingError { - message: format!("{err}").into(), - }), - ), - }; - context.rules_file = rules_file; - (context, rules_file_error) - }) - } - - fn load_worktree_rules_file( - worktree: Entity, - project: Entity, - cx: &mut App, - ) -> Option>> { - let worktree = worktree.read(cx); - let worktree_id = worktree.id(); - let selected_rules_file = RULES_FILE_NAMES - .into_iter() - .filter_map(|name| { - worktree - .entry_for_path(RelPath::unix(name).unwrap()) - .filter(|entry| entry.is_file()) - .map(|entry| entry.path.clone()) - }) - .next(); - - // Note that Cline supports `.clinerules` being a directory, but that is not currently - // supported. This doesn't seem to occur often in GitHub repositories. - selected_rules_file.map(|path_in_worktree| { - let project_path = ProjectPath { - worktree_id, - path: path_in_worktree.clone(), - }; - let buffer_task = - project.update(cx, |project, cx| project.open_buffer(project_path, cx)); - let rope_task = cx.spawn(async move |cx| { - buffer_task.await?.read_with(cx, |buffer, cx| { - let project_entry_id = buffer.entry_id(cx).context("buffer has no file")?; - anyhow::Ok((project_entry_id, buffer.as_rope().clone())) - })? - }); - // Build a string from the rope on a background thread. - cx.background_spawn(async move { - let (project_entry_id, rope) = rope_task.await?; - anyhow::Ok(RulesFileContext { - path_in_worktree, - text: rope.to_string().trim().to_string(), - project_entry_id: project_entry_id.to_usize(), - }) - }) - }) - } - - fn handle_thread_title_updated( - &mut self, - thread: Entity, - _: &TitleUpdated, - cx: &mut Context, - ) { - let session_id = thread.read(cx).id(); - let Some(session) = self.sessions.get(session_id) else { - return; - }; - let thread = thread.downgrade(); - let acp_thread = session.acp_thread.clone(); - cx.spawn(async move |_, cx| { - let title = thread.read_with(cx, |thread, _| thread.title())?; - let task = acp_thread.update(cx, |acp_thread, cx| acp_thread.set_title(title, cx))?; - task.await - }) - .detach_and_log_err(cx); - } - - fn handle_thread_token_usage_updated( - &mut self, - thread: Entity, - usage: &TokenUsageUpdated, - cx: &mut Context, - ) { - let Some(session) = self.sessions.get(thread.read(cx).id()) else { - return; - }; - session - .acp_thread - .update(cx, |acp_thread, cx| { - acp_thread.update_token_usage(usage.0.clone(), cx); - }) - .ok(); - } - - fn handle_project_event( - &mut self, - _project: Entity, - event: &project::Event, - _cx: &mut Context, - ) { - match event { - project::Event::WorktreeAdded(_) | project::Event::WorktreeRemoved(_) => { - self.project_context_needs_refresh.send(()).ok(); - } - project::Event::WorktreeUpdatedEntries(_, items) => { - if items.iter().any(|(path, _, _)| { - RULES_FILE_NAMES - .iter() - .any(|name| path.as_ref() == RelPath::unix(name).unwrap()) - }) { - self.project_context_needs_refresh.send(()).ok(); - } - } - _ => {} - } - } - - fn handle_prompts_updated_event( - &mut self, - _prompt_store: Entity, - _event: &prompt_store::PromptsUpdatedEvent, - _cx: &mut Context, - ) { - self.project_context_needs_refresh.send(()).ok(); - } - - fn handle_models_updated_event( - &mut self, - _registry: Entity, - _event: &language_model::Event, - cx: &mut Context, - ) { - self.models.refresh_list(cx); - - let registry = LanguageModelRegistry::read_global(cx); - let default_model = registry.default_model().map(|m| m.model); - let summarization_model = registry.thread_summary_model().map(|m| m.model); - - for session in self.sessions.values_mut() { - session.thread.update(cx, |thread, cx| { - if thread.model().is_none() - && let Some(model) = default_model.clone() - { - thread.set_model(model, cx); - cx.notify(); - } - thread.set_summarization_model(summarization_model.clone(), cx); - }); - } - } - - pub fn load_thread( - &mut self, - id: acp::SessionId, - cx: &mut Context, - ) -> Task>> { - let database_future = ThreadsDatabase::connect(cx); - cx.spawn(async move |this, cx| { - let database = database_future.await.map_err(|err| anyhow!(err))?; - let db_thread = database - .load_thread(id.clone()) - .await? - .with_context(|| format!("no thread found with ID: {id:?}"))?; - - this.update(cx, |this, cx| { - let summarization_model = LanguageModelRegistry::read_global(cx) - .thread_summary_model() - .map(|c| c.model); - - cx.new(|cx| { - let mut thread = Thread::from_db( - id.clone(), - db_thread, - this.project.clone(), - this.project_context.clone(), - this.context_server_registry.clone(), - this.templates.clone(), - cx, - ); - thread.set_summarization_model(summarization_model, cx); - thread - }) - }) - }) - } - - pub fn open_thread( - &mut self, - id: acp::SessionId, - cx: &mut Context, - ) -> Task>> { - let task = self.load_thread(id, cx); - cx.spawn(async move |this, cx| { - let thread = task.await?; - let acp_thread = - this.update(cx, |this, cx| this.register_session(thread.clone(), cx))?; - let events = thread.update(cx, |thread, cx| thread.replay(cx))?; - cx.update(|cx| { - NativeAgentConnection::handle_thread_events(events, acp_thread.downgrade(), cx) - })? - .await?; - Ok(acp_thread) - }) - } - - pub fn thread_summary( - &mut self, - id: acp::SessionId, - cx: &mut Context, - ) -> Task> { - let thread = self.open_thread(id.clone(), cx); - cx.spawn(async move |this, cx| { - let acp_thread = thread.await?; - let result = this - .update(cx, |this, cx| { - this.sessions - .get(&id) - .unwrap() - .thread - .update(cx, |thread, cx| thread.summary(cx)) - })? - .await - .context("Failed to generate summary")?; - drop(acp_thread); - Ok(result) - }) - } - - fn save_thread(&mut self, thread: Entity, cx: &mut Context) { - if thread.read(cx).is_empty() { - return; - } - - let database_future = ThreadsDatabase::connect(cx); - let (id, db_thread) = - thread.update(cx, |thread, cx| (thread.id().clone(), thread.to_db(cx))); - let Some(session) = self.sessions.get_mut(&id) else { - return; - }; - let history = self.history.clone(); - session.pending_save = cx.spawn(async move |_, cx| { - let Some(database) = database_future.await.map_err(|err| anyhow!(err)).log_err() else { - return; - }; - let db_thread = db_thread.await; - database.save_thread(id, db_thread).await.log_err(); - history.update(cx, |history, cx| history.reload(cx)).ok(); - }); - } -} - -/// Wrapper struct that implements the AgentConnection trait -#[derive(Clone)] -pub struct NativeAgentConnection(pub Entity); - -impl NativeAgentConnection { - pub fn thread(&self, session_id: &acp::SessionId, cx: &App) -> Option> { - self.0 - .read(cx) - .sessions - .get(session_id) - .map(|session| session.thread.clone()) - } - - pub fn load_thread(&self, id: acp::SessionId, cx: &mut App) -> Task>> { - self.0.update(cx, |this, cx| this.load_thread(id, cx)) - } - - fn run_turn( - &self, - session_id: acp::SessionId, - cx: &mut App, - f: impl 'static - + FnOnce(Entity, &mut App) -> Result>>, - ) -> Task> { - let Some((thread, acp_thread)) = self.0.update(cx, |agent, _cx| { - agent - .sessions - .get_mut(&session_id) - .map(|s| (s.thread.clone(), s.acp_thread.clone())) - }) else { - return Task::ready(Err(anyhow!("Session not found"))); - }; - log::debug!("Found session for: {}", session_id); - - let response_stream = match f(thread, cx) { - Ok(stream) => stream, - Err(err) => return Task::ready(Err(err)), - }; - Self::handle_thread_events(response_stream, acp_thread, cx) - } - - fn handle_thread_events( - mut events: mpsc::UnboundedReceiver>, - acp_thread: WeakEntity, - cx: &App, - ) -> Task> { - cx.spawn(async move |cx| { - // Handle response stream and forward to session.acp_thread - while let Some(result) = events.next().await { - match result { - Ok(event) => { - log::trace!("Received completion event: {:?}", event); - - match event { - ThreadEvent::UserMessage(message) => { - acp_thread.update(cx, |thread, cx| { - for content in message.content { - thread.push_user_content_block( - Some(message.id.clone()), - content.into(), - cx, - ); - } - })?; - } - ThreadEvent::AgentText(text) => { - acp_thread.update(cx, |thread, cx| { - thread.push_assistant_content_block(text.into(), false, cx) - })?; - } - ThreadEvent::AgentThinking(text) => { - acp_thread.update(cx, |thread, cx| { - thread.push_assistant_content_block(text.into(), true, cx) - })?; - } - ThreadEvent::ToolCallAuthorization(ToolCallAuthorization { - tool_call, - options, - response, - }) => { - let outcome_task = acp_thread.update(cx, |thread, cx| { - thread.request_tool_call_authorization( - tool_call, options, true, cx, - ) - })??; - cx.background_spawn(async move { - if let acp::RequestPermissionOutcome::Selected( - acp::SelectedPermissionOutcome { option_id, .. }, - ) = outcome_task.await - { - response - .send(option_id) - .map(|_| anyhow!("authorization receiver was dropped")) - .log_err(); - } - }) - .detach(); - } - ThreadEvent::ToolCall(tool_call) => { - acp_thread.update(cx, |thread, cx| { - thread.upsert_tool_call(tool_call, cx) - })??; - } - ThreadEvent::ToolCallUpdate(update) => { - acp_thread.update(cx, |thread, cx| { - thread.update_tool_call(update, cx) - })??; - } - ThreadEvent::Retry(status) => { - acp_thread.update(cx, |thread, cx| { - thread.update_retry_status(status, cx) - })?; - } - ThreadEvent::Stop(stop_reason) => { - log::debug!("Assistant message complete: {:?}", stop_reason); - return Ok(acp::PromptResponse::new(stop_reason)); - } - } - } - Err(e) => { - log::error!("Error in model response stream: {:?}", e); - return Err(e); - } - } - } - - log::debug!("Response stream completed"); - anyhow::Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)) - }) - } -} - -struct NativeAgentModelSelector { - session_id: acp::SessionId, - connection: NativeAgentConnection, -} - -impl acp_thread::AgentModelSelector for NativeAgentModelSelector { - fn list_models(&self, cx: &mut App) -> Task> { - log::debug!("NativeAgentConnection::list_models called"); - let list = self.connection.0.read(cx).models.model_list.clone(); - Task::ready(if list.is_empty() { - Err(anyhow::anyhow!("No models available")) - } else { - Ok(list) - }) - } - - fn select_model(&self, model_id: acp::ModelId, cx: &mut App) -> Task> { - log::debug!( - "Setting model for session {}: {}", - self.session_id, - model_id - ); - let Some(thread) = self - .connection - .0 - .read(cx) - .sessions - .get(&self.session_id) - .map(|session| session.thread.clone()) - else { - return Task::ready(Err(anyhow!("Session not found"))); - }; - - let Some(model) = self.connection.0.read(cx).models.model_from_id(&model_id) else { - return Task::ready(Err(anyhow!("Invalid model ID {}", model_id))); - }; - - thread.update(cx, |thread, cx| { - thread.set_model(model.clone(), cx); - }); - - update_settings_file( - self.connection.0.read(cx).fs.clone(), - cx, - move |settings, _cx| { - let provider = model.provider_id().0.to_string(); - let model = model.id().0.to_string(); - settings - .agent - .get_or_insert_default() - .set_model(LanguageModelSelection { - provider: provider.into(), - model, - }); - }, - ); - - Task::ready(Ok(())) - } - - fn selected_model(&self, cx: &mut App) -> Task> { - let Some(thread) = self - .connection - .0 - .read(cx) - .sessions - .get(&self.session_id) - .map(|session| session.thread.clone()) - else { - return Task::ready(Err(anyhow!("Session not found"))); - }; - let Some(model) = thread.read(cx).model() else { - return Task::ready(Err(anyhow!("Model not found"))); - }; - let Some(provider) = LanguageModelRegistry::read_global(cx).provider(&model.provider_id()) - else { - return Task::ready(Err(anyhow!("Provider not found"))); - }; - Task::ready(Ok(LanguageModels::map_language_model_to_info( - model, &provider, - ))) - } - - fn watch(&self, cx: &mut App) -> Option> { - Some(self.connection.0.read(cx).models.watch()) - } - - fn should_render_footer(&self) -> bool { - true - } -} - -impl acp_thread::AgentConnection for NativeAgentConnection { - fn telemetry_id(&self) -> SharedString { - "zed".into() - } - - fn new_thread( - self: Rc, - project: Entity, - cwd: &Path, - cx: &mut App, - ) -> Task>> { - let agent = self.0.clone(); - log::debug!("Creating new thread for project at: {:?}", cwd); - - cx.spawn(async move |cx| { - log::debug!("Starting thread creation in async context"); - - // Create Thread - let thread = agent.update( - cx, - |agent, cx: &mut gpui::Context| -> Result<_> { - // Fetch default model from registry settings - let registry = LanguageModelRegistry::read_global(cx); - // Log available models for debugging - let available_count = registry.available_models(cx).count(); - log::debug!("Total available models: {}", available_count); - - let default_model = registry.default_model().and_then(|default_model| { - agent - .models - .model_from_id(&LanguageModels::model_id(&default_model.model)) - }); - Ok(cx.new(|cx| { - Thread::new( - project.clone(), - agent.project_context.clone(), - agent.context_server_registry.clone(), - agent.templates.clone(), - default_model, - cx, - ) - })) - }, - )??; - agent.update(cx, |agent, cx| agent.register_session(thread, cx)) - }) - } - - fn auth_methods(&self) -> &[acp::AuthMethod] { - &[] // No auth for in-process - } - - fn authenticate(&self, _method: acp::AuthMethodId, _cx: &mut App) -> Task> { - Task::ready(Ok(())) - } - - fn model_selector(&self, session_id: &acp::SessionId) -> Option> { - Some(Rc::new(NativeAgentModelSelector { - session_id: session_id.clone(), - connection: self.clone(), - }) as Rc) - } - - fn prompt( - &self, - id: Option, - params: acp::PromptRequest, - cx: &mut App, - ) -> Task> { - let id = id.expect("UserMessageId is required"); - let session_id = params.session_id.clone(); - log::info!("Received prompt request for session: {}", session_id); - log::debug!("Prompt blocks count: {}", params.prompt.len()); - let path_style = self.0.read(cx).project.read(cx).path_style(cx); - - self.run_turn(session_id, cx, move |thread, cx| { - let content: Vec = params - .prompt - .into_iter() - .map(|block| UserMessageContent::from_content_block(block, path_style)) - .collect::>(); - log::debug!("Converted prompt to message: {} chars", content.len()); - log::debug!("Message id: {:?}", id); - log::debug!("Message content: {:?}", content); - - thread.update(cx, |thread, cx| thread.send(id, content, cx)) - }) - } - - fn resume( - &self, - session_id: &acp::SessionId, - _cx: &App, - ) -> Option> { - Some(Rc::new(NativeAgentSessionResume { - connection: self.clone(), - session_id: session_id.clone(), - }) as _) - } - - fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) { - log::info!("Cancelling on session: {}", session_id); - self.0.update(cx, |agent, cx| { - if let Some(agent) = agent.sessions.get(session_id) { - agent.thread.update(cx, |thread, cx| thread.cancel(cx)); - } - }); - } - - fn truncate( - &self, - session_id: &agent_client_protocol::SessionId, - cx: &App, - ) -> Option> { - self.0.read_with(cx, |agent, _cx| { - agent.sessions.get(session_id).map(|session| { - Rc::new(NativeAgentSessionTruncate { - thread: session.thread.clone(), - acp_thread: session.acp_thread.clone(), - }) as _ - }) - }) - } - - fn set_title( - &self, - session_id: &acp::SessionId, - _cx: &App, - ) -> Option> { - Some(Rc::new(NativeAgentSessionSetTitle { - connection: self.clone(), - session_id: session_id.clone(), - }) as _) - } - - fn telemetry(&self) -> Option> { - Some(Rc::new(self.clone()) as Rc) - } - - fn into_any(self: Rc) -> Rc { - self - } -} - -impl acp_thread::AgentTelemetry for NativeAgentConnection { - fn thread_data( - &self, - session_id: &acp::SessionId, - cx: &mut App, - ) -> Task> { - let Some(session) = self.0.read(cx).sessions.get(session_id) else { - return Task::ready(Err(anyhow!("Session not found"))); - }; - - let task = session.thread.read(cx).to_db(cx); - cx.background_spawn(async move { - serde_json::to_value(task.await).context("Failed to serialize thread") - }) - } -} - -struct NativeAgentSessionTruncate { - thread: Entity, - acp_thread: WeakEntity, -} - -impl acp_thread::AgentSessionTruncate for NativeAgentSessionTruncate { - fn run(&self, message_id: acp_thread::UserMessageId, cx: &mut App) -> Task> { - match self.thread.update(cx, |thread, cx| { - thread.truncate(message_id.clone(), cx)?; - Ok(thread.latest_token_usage()) - }) { - Ok(usage) => { - self.acp_thread - .update(cx, |thread, cx| { - thread.update_token_usage(usage, cx); - }) - .ok(); - Task::ready(Ok(())) - } - Err(error) => Task::ready(Err(error)), - } - } -} - -struct NativeAgentSessionResume { - connection: NativeAgentConnection, - session_id: acp::SessionId, -} - -impl acp_thread::AgentSessionResume for NativeAgentSessionResume { - fn run(&self, cx: &mut App) -> Task> { - self.connection - .run_turn(self.session_id.clone(), cx, |thread, cx| { - thread.update(cx, |thread, cx| thread.resume(cx)) - }) - } -} - -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(())) - } -} - -pub struct AcpThreadEnvironment { - acp_thread: WeakEntity, -} - -impl ThreadEnvironment for AcpThreadEnvironment { - fn create_terminal( - &self, - command: String, - cwd: Option, - output_byte_limit: Option, - cx: &mut AsyncApp, - ) -> Task>> { - let task = self.acp_thread.update(cx, |thread, cx| { - thread.create_terminal(command, vec![], vec![], cwd, output_byte_limit, cx) - }); - - let acp_thread = self.acp_thread.clone(); - cx.spawn(async move |cx| { - let terminal = task?.await?; - - let (drop_tx, drop_rx) = oneshot::channel(); - let terminal_id = terminal.read_with(cx, |terminal, _cx| terminal.id().clone())?; - - cx.spawn(async move |cx| { - drop_rx.await.ok(); - acp_thread.update(cx, |thread, cx| thread.release_terminal(terminal_id, cx)) - }) - .detach(); - - let handle = AcpTerminalHandle { - terminal, - _drop_tx: Some(drop_tx), - }; - - Ok(Rc::new(handle) as _) - }) - } -} - -pub struct AcpTerminalHandle { - terminal: Entity, - _drop_tx: Option>, -} - -impl TerminalHandle for AcpTerminalHandle { - fn id(&self, cx: &AsyncApp) -> Result { - self.terminal.read_with(cx, |term, _cx| term.id().clone()) - } - - fn wait_for_exit(&self, cx: &AsyncApp) -> Result>> { - self.terminal - .read_with(cx, |term, _cx| term.wait_for_exit()) - } - - fn current_output(&self, cx: &AsyncApp) -> Result { - self.terminal - .read_with(cx, |term, cx| term.current_output(cx)) - } -} - -#[cfg(test)] -mod internal_tests { - use crate::HistoryEntryId; - - use super::*; - use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelInfo, MentionUri}; - use fs::FakeFs; - use gpui::TestAppContext; - use indoc::formatdoc; - use language_model::fake_provider::FakeLanguageModel; - use serde_json::json; - use settings::SettingsStore; - use util::{path, rel_path::rel_path}; - - #[gpui::test] - async fn test_maintaining_project_context(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/", - json!({ - "a": {} - }), - ) - .await; - let project = Project::test(fs.clone(), [], cx).await; - let text_thread_store = - cx.new(|cx| assistant_text_thread::TextThreadStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); - let agent = NativeAgent::new( - project.clone(), - history_store, - Templates::new(), - None, - fs.clone(), - &mut cx.to_async(), - ) - .await - .unwrap(); - agent.read_with(cx, |agent, cx| { - assert_eq!(agent.project_context.read(cx).worktrees, vec![]) - }); - - let worktree = project - .update(cx, |project, cx| project.create_worktree("/a", true, cx)) - .await - .unwrap(); - cx.run_until_parked(); - agent.read_with(cx, |agent, cx| { - assert_eq!( - agent.project_context.read(cx).worktrees, - vec![WorktreeContext { - root_name: "a".into(), - abs_path: Path::new("/a").into(), - rules_file: None - }] - ) - }); - - // Creating `/a/.rules` updates the project context. - fs.insert_file("/a/.rules", Vec::new()).await; - cx.run_until_parked(); - agent.read_with(cx, |agent, cx| { - let rules_entry = worktree - .read(cx) - .entry_for_path(rel_path(".rules")) - .unwrap(); - assert_eq!( - agent.project_context.read(cx).worktrees, - vec![WorktreeContext { - root_name: "a".into(), - abs_path: Path::new("/a").into(), - rules_file: Some(RulesFileContext { - path_in_worktree: rel_path(".rules").into(), - text: "".into(), - project_entry_id: rules_entry.id.to_usize() - }) - }] - ) - }); - } - - #[gpui::test] - async fn test_listing_models(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree("/", json!({ "a": {} })).await; - let project = Project::test(fs.clone(), [], cx).await; - let text_thread_store = - cx.new(|cx| assistant_text_thread::TextThreadStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); - let connection = NativeAgentConnection( - NativeAgent::new( - project.clone(), - history_store, - Templates::new(), - None, - fs.clone(), - &mut cx.to_async(), - ) - .await - .unwrap(), - ); - - // Create a thread/session - let acp_thread = cx - .update(|cx| { - Rc::new(connection.clone()).new_thread(project.clone(), Path::new("/a"), cx) - }) - .await - .unwrap(); - - let session_id = cx.update(|cx| acp_thread.read(cx).session_id().clone()); - - let models = cx - .update(|cx| { - connection - .model_selector(&session_id) - .unwrap() - .list_models(cx) - }) - .await - .unwrap(); - - let acp_thread::AgentModelList::Grouped(models) = models else { - panic!("Unexpected model group"); - }; - assert_eq!( - models, - IndexMap::from_iter([( - AgentModelGroupName("Fake".into()), - vec![AgentModelInfo { - id: acp::ModelId::new("fake/fake"), - name: "Fake".into(), - description: None, - icon: Some(ui::IconName::ZedAssistant), - }] - )]) - ); - } - - #[gpui::test] - async fn test_model_selection_persists_to_settings(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - fs.create_dir(paths::settings_file().parent().unwrap()) - .await - .unwrap(); - fs.insert_file( - paths::settings_file(), - json!({ - "agent": { - "default_model": { - "provider": "foo", - "model": "bar" - } - } - }) - .to_string() - .into_bytes(), - ) - .await; - let project = Project::test(fs.clone(), [], cx).await; - - let text_thread_store = - cx.new(|cx| assistant_text_thread::TextThreadStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); - - // Create the agent and connection - let agent = NativeAgent::new( - project.clone(), - history_store, - Templates::new(), - None, - fs.clone(), - &mut cx.to_async(), - ) - .await - .unwrap(); - let connection = NativeAgentConnection(agent.clone()); - - // Create a thread/session - let acp_thread = cx - .update(|cx| { - Rc::new(connection.clone()).new_thread(project.clone(), Path::new("/a"), cx) - }) - .await - .unwrap(); - - let session_id = cx.update(|cx| acp_thread.read(cx).session_id().clone()); - - // Select a model - let selector = connection.model_selector(&session_id).unwrap(); - let model_id = acp::ModelId::new("fake/fake"); - cx.update(|cx| selector.select_model(model_id.clone(), cx)) - .await - .unwrap(); - - // Verify the thread has the selected model - agent.read_with(cx, |agent, _| { - let session = agent.sessions.get(&session_id).unwrap(); - session.thread.read_with(cx, |thread, _| { - assert_eq!(thread.model().unwrap().id().0, "fake"); - }); - }); - - cx.run_until_parked(); - - // Verify settings file was updated - let settings_content = fs.load(paths::settings_file()).await.unwrap(); - let settings_json: serde_json::Value = serde_json::from_str(&settings_content).unwrap(); - - // Check that the agent settings contain the selected model - assert_eq!( - settings_json["agent"]["default_model"]["model"], - json!("fake") - ); - assert_eq!( - settings_json["agent"]["default_model"]["provider"], - json!("fake") - ); - } - - #[gpui::test] - async fn test_save_load_thread(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/", - json!({ - "a": { - "b.md": "Lorem" - } - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/a").as_ref()], cx).await; - let text_thread_store = - cx.new(|cx| assistant_text_thread::TextThreadStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); - let agent = NativeAgent::new( - project.clone(), - history_store.clone(), - Templates::new(), - None, - fs.clone(), - &mut cx.to_async(), - ) - .await - .unwrap(); - let connection = Rc::new(NativeAgentConnection(agent.clone())); - - let acp_thread = cx - .update(|cx| { - connection - .clone() - .new_thread(project.clone(), Path::new(""), cx) - }) - .await - .unwrap(); - let session_id = acp_thread.read_with(cx, |thread, _| thread.session_id().clone()); - let thread = agent.read_with(cx, |agent, _| { - agent.sessions.get(&session_id).unwrap().thread.clone() - }); - - // Ensure empty threads are not saved, even if they get mutated. - let model = Arc::new(FakeLanguageModel::default()); - let summary_model = Arc::new(FakeLanguageModel::default()); - thread.update(cx, |thread, cx| { - thread.set_model(model.clone(), cx); - thread.set_summarization_model(Some(summary_model.clone()), cx); - }); - cx.run_until_parked(); - assert_eq!(history_entries(&history_store, cx), vec![]); - - let send = acp_thread.update(cx, |thread, cx| { - thread.send( - vec![ - "What does ".into(), - acp::ContentBlock::ResourceLink(acp::ResourceLink::new( - "b.md", - MentionUri::File { - abs_path: path!("/a/b.md").into(), - } - .to_uri() - .to_string(), - )), - " mean?".into(), - ], - cx, - ) - }); - let send = cx.foreground_executor().spawn(send); - cx.run_until_parked(); - - model.send_last_completion_stream_text_chunk("Lorem."); - model.end_last_completion_stream(); - cx.run_until_parked(); - summary_model - .send_last_completion_stream_text_chunk(&format!("Explaining {}", path!("/a/b.md"))); - summary_model.end_last_completion_stream(); - - send.await.unwrap(); - let uri = MentionUri::File { - abs_path: path!("/a/b.md").into(), - } - .to_uri(); - acp_thread.read_with(cx, |thread, cx| { - assert_eq!( - thread.to_markdown(cx), - formatdoc! {" - ## User - - What does [@b.md]({uri}) mean? - - ## Assistant - - Lorem. - - "} - ) - }); - - cx.run_until_parked(); - - // Drop the ACP thread, which should cause the session to be dropped as well. - cx.update(|_| { - drop(thread); - drop(acp_thread); - }); - agent.read_with(cx, |agent, _| { - assert_eq!(agent.sessions.keys().cloned().collect::>(), []); - }); - - // Ensure the thread can be reloaded from disk. - assert_eq!( - history_entries(&history_store, cx), - vec![( - HistoryEntryId::AcpThread(session_id.clone()), - format!("Explaining {}", path!("/a/b.md")) - )] - ); - let acp_thread = agent - .update(cx, |agent, cx| agent.open_thread(session_id.clone(), cx)) - .await - .unwrap(); - acp_thread.read_with(cx, |thread, cx| { - assert_eq!( - thread.to_markdown(cx), - formatdoc! {" - ## User - - What does [@b.md]({uri}) mean? - - ## Assistant - - Lorem. - - "} - ) - }); - } - - fn history_entries( - history: &Entity, - cx: &mut TestAppContext, - ) -> Vec<(HistoryEntryId, String)> { - history.read_with(cx, |history, _| { - history - .entries() - .map(|e| (e.id(), e.title().to_string())) - .collect::>() - }) - } - - fn init_test(cx: &mut TestAppContext) { - env_logger::try_init().ok(); - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - - LanguageModelRegistry::test(cx); - }); - } -} diff --git a/crates/agent/src/db.rs b/crates/agent/src/db.rs deleted file mode 100644 index 7a88c58705..0000000000 --- a/crates/agent/src/db.rs +++ /dev/null @@ -1,443 +0,0 @@ -use crate::{AgentMessage, AgentMessageContent, UserMessage, UserMessageContent}; -use acp_thread::UserMessageId; -use agent_client_protocol as acp; -use agent_settings::{AgentProfileId, CompletionMode}; -use anyhow::{Result, anyhow}; -use chrono::{DateTime, Utc}; -use collections::{HashMap, IndexMap}; -use futures::{FutureExt, future::Shared}; -use gpui::{BackgroundExecutor, Global, Task}; -use indoc::indoc; -use parking_lot::Mutex; -use serde::{Deserialize, Serialize}; -use sqlez::{ - bindable::{Bind, Column}, - connection::Connection, - statement::Statement, -}; -use std::sync::Arc; -use ui::{App, SharedString}; -use zed_env_vars::ZED_STATELESS; - -pub type DbMessage = crate::Message; -pub type DbSummary = crate::legacy_thread::DetailedSummaryState; -pub type DbLanguageModel = crate::legacy_thread::SerializedLanguageModel; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DbThreadMetadata { - pub id: acp::SessionId, - #[serde(alias = "summary")] - pub title: SharedString, - pub updated_at: DateTime, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct DbThread { - pub title: SharedString, - pub messages: Vec, - pub updated_at: DateTime, - #[serde(default)] - pub detailed_summary: Option, - #[serde(default)] - pub initial_project_snapshot: Option>, - #[serde(default)] - pub cumulative_token_usage: language_model::TokenUsage, - #[serde(default)] - pub request_token_usage: HashMap, - #[serde(default)] - pub model: Option, - #[serde(default)] - pub completion_mode: Option, - #[serde(default)] - pub profile: Option, -} - -impl DbThread { - pub const VERSION: &'static str = "0.3.0"; - - pub fn from_json(json: &[u8]) -> Result { - let saved_thread_json = serde_json::from_slice::(json)?; - match saved_thread_json.get("version") { - Some(serde_json::Value::String(version)) => match version.as_str() { - Self::VERSION => Ok(serde_json::from_value(saved_thread_json)?), - _ => Self::upgrade_from_agent_1(crate::legacy_thread::SerializedThread::from_json( - json, - )?), - }, - _ => { - Self::upgrade_from_agent_1(crate::legacy_thread::SerializedThread::from_json(json)?) - } - } - } - - fn upgrade_from_agent_1(thread: crate::legacy_thread::SerializedThread) -> Result { - let mut messages = Vec::new(); - let mut request_token_usage = HashMap::default(); - - let mut last_user_message_id = None; - for (ix, msg) in thread.messages.into_iter().enumerate() { - let message = match msg.role { - language_model::Role::User => { - let mut content = Vec::new(); - - // Convert segments to content - for segment in msg.segments { - match segment { - crate::legacy_thread::SerializedMessageSegment::Text { text } => { - content.push(UserMessageContent::Text(text)); - } - crate::legacy_thread::SerializedMessageSegment::Thinking { - text, - .. - } => { - // User messages don't have thinking segments, but handle gracefully - content.push(UserMessageContent::Text(text)); - } - crate::legacy_thread::SerializedMessageSegment::RedactedThinking { - .. - } => { - // User messages don't have redacted thinking, skip. - } - } - } - - // If no content was added, add context as text if available - if content.is_empty() && !msg.context.is_empty() { - content.push(UserMessageContent::Text(msg.context)); - } - - let id = UserMessageId::new(); - last_user_message_id = Some(id.clone()); - - crate::Message::User(UserMessage { - // MessageId from old format can't be meaningfully converted, so generate a new one - id, - content, - }) - } - language_model::Role::Assistant => { - let mut content = Vec::new(); - - // Convert segments to content - for segment in msg.segments { - match segment { - crate::legacy_thread::SerializedMessageSegment::Text { text } => { - content.push(AgentMessageContent::Text(text)); - } - crate::legacy_thread::SerializedMessageSegment::Thinking { - text, - signature, - } => { - content.push(AgentMessageContent::Thinking { text, signature }); - } - crate::legacy_thread::SerializedMessageSegment::RedactedThinking { - data, - } => { - content.push(AgentMessageContent::RedactedThinking(data)); - } - } - } - - // Convert tool uses - let mut tool_names_by_id = HashMap::default(); - for tool_use in msg.tool_uses { - tool_names_by_id.insert(tool_use.id.clone(), tool_use.name.clone()); - content.push(AgentMessageContent::ToolUse( - language_model::LanguageModelToolUse { - id: tool_use.id, - name: tool_use.name.into(), - raw_input: serde_json::to_string(&tool_use.input) - .unwrap_or_default(), - input: tool_use.input, - is_input_complete: true, - thought_signature: None, - }, - )); - } - - // Convert tool results - let mut tool_results = IndexMap::default(); - for tool_result in msg.tool_results { - let name = tool_names_by_id - .remove(&tool_result.tool_use_id) - .unwrap_or_else(|| SharedString::from("unknown")); - tool_results.insert( - tool_result.tool_use_id.clone(), - language_model::LanguageModelToolResult { - tool_use_id: tool_result.tool_use_id, - tool_name: name.into(), - is_error: tool_result.is_error, - content: tool_result.content, - output: tool_result.output, - }, - ); - } - - if let Some(last_user_message_id) = &last_user_message_id - && let Some(token_usage) = thread.request_token_usage.get(ix).copied() - { - request_token_usage.insert(last_user_message_id.clone(), token_usage); - } - - crate::Message::Agent(AgentMessage { - content, - tool_results, - reasoning_details: None, - }) - } - language_model::Role::System => { - // Skip system messages as they're not supported in the new format - continue; - } - }; - - messages.push(message); - } - - Ok(Self { - title: thread.summary, - messages, - updated_at: thread.updated_at, - detailed_summary: match thread.detailed_summary_state { - crate::legacy_thread::DetailedSummaryState::NotGenerated - | crate::legacy_thread::DetailedSummaryState::Generating => None, - crate::legacy_thread::DetailedSummaryState::Generated { text, .. } => Some(text), - }, - initial_project_snapshot: thread.initial_project_snapshot, - cumulative_token_usage: thread.cumulative_token_usage, - request_token_usage, - model: thread.model, - completion_mode: thread.completion_mode, - profile: thread.profile, - }) - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum DataType { - #[serde(rename = "json")] - Json, - #[serde(rename = "zstd")] - Zstd, -} - -impl Bind for DataType { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - let value = match self { - DataType::Json => "json", - DataType::Zstd => "zstd", - }; - value.bind(statement, start_index) - } -} - -impl Column for DataType { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let (value, next_index) = String::column(statement, start_index)?; - let data_type = match value.as_str() { - "json" => DataType::Json, - "zstd" => DataType::Zstd, - _ => anyhow::bail!("Unknown data type: {}", value), - }; - Ok((data_type, next_index)) - } -} - -pub(crate) struct ThreadsDatabase { - executor: BackgroundExecutor, - connection: Arc>, -} - -struct GlobalThreadsDatabase(Shared, Arc>>>); - -impl Global for GlobalThreadsDatabase {} - -impl ThreadsDatabase { - pub fn connect(cx: &mut App) -> Shared, Arc>>> { - if cx.has_global::() { - return cx.global::().0.clone(); - } - let executor = cx.background_executor().clone(); - let task = executor - .spawn({ - let executor = executor.clone(); - async move { - match ThreadsDatabase::new(executor) { - Ok(db) => Ok(Arc::new(db)), - Err(err) => Err(Arc::new(err)), - } - } - }) - .shared(); - - cx.set_global(GlobalThreadsDatabase(task.clone())); - task - } - - pub fn new(executor: BackgroundExecutor) -> Result { - let connection = if *ZED_STATELESS { - Connection::open_memory(Some("THREAD_FALLBACK_DB")) - } else if cfg!(any(feature = "test-support", test)) { - // rust stores the name of the test on the current thread. - // We use this to automatically create a database that will - // be shared within the test (for the test_retrieve_old_thread) - // but not with concurrent tests. - let thread = std::thread::current(); - let test_name = thread.name(); - Connection::open_memory(Some(&format!( - "THREAD_FALLBACK_{}", - test_name.unwrap_or_default() - ))) - } else { - let threads_dir = paths::data_dir().join("threads"); - std::fs::create_dir_all(&threads_dir)?; - let sqlite_path = threads_dir.join("threads.db"); - Connection::open_file(&sqlite_path.to_string_lossy()) - }; - - connection.exec(indoc! {" - CREATE TABLE IF NOT EXISTS threads ( - id TEXT PRIMARY KEY, - summary TEXT NOT NULL, - updated_at TEXT NOT NULL, - data_type TEXT NOT NULL, - data BLOB NOT NULL - ) - "})?() - .map_err(|e| anyhow!("Failed to create threads table: {}", e))?; - - let db = Self { - executor, - connection: Arc::new(Mutex::new(connection)), - }; - - Ok(db) - } - - fn save_thread_sync( - connection: &Arc>, - id: acp::SessionId, - thread: DbThread, - ) -> Result<()> { - const COMPRESSION_LEVEL: i32 = 3; - - #[derive(Serialize)] - struct SerializedThread { - #[serde(flatten)] - thread: DbThread, - version: &'static str, - } - - let title = thread.title.to_string(); - let updated_at = thread.updated_at.to_rfc3339(); - let json_data = serde_json::to_string(&SerializedThread { - thread, - version: DbThread::VERSION, - })?; - - let connection = connection.lock(); - - let compressed = zstd::encode_all(json_data.as_bytes(), COMPRESSION_LEVEL)?; - let data_type = DataType::Zstd; - let data = compressed; - - let mut insert = connection.exec_bound::<(Arc, String, String, DataType, Vec)>(indoc! {" - INSERT OR REPLACE INTO threads (id, summary, updated_at, data_type, data) VALUES (?, ?, ?, ?, ?) - "})?; - - insert((id.0, title, updated_at, data_type, data))?; - - Ok(()) - } - - pub fn list_threads(&self) -> Task>> { - let connection = self.connection.clone(); - - self.executor.spawn(async move { - let connection = connection.lock(); - - let mut select = - connection.select_bound::<(), (Arc, String, String)>(indoc! {" - SELECT id, summary, updated_at FROM threads ORDER BY updated_at DESC - "})?; - - let rows = select(())?; - let mut threads = Vec::new(); - - for (id, summary, updated_at) in rows { - threads.push(DbThreadMetadata { - id: acp::SessionId::new(id), - title: summary.into(), - updated_at: DateTime::parse_from_rfc3339(&updated_at)?.with_timezone(&Utc), - }); - } - - Ok(threads) - }) - } - - pub fn load_thread(&self, id: acp::SessionId) -> Task>> { - let connection = self.connection.clone(); - - self.executor.spawn(async move { - let connection = connection.lock(); - let mut select = connection.select_bound::, (DataType, Vec)>(indoc! {" - SELECT data_type, data FROM threads WHERE id = ? LIMIT 1 - "})?; - - let rows = select(id.0)?; - if let Some((data_type, data)) = rows.into_iter().next() { - let json_data = match data_type { - DataType::Zstd => { - let decompressed = zstd::decode_all(&data[..])?; - String::from_utf8(decompressed)? - } - DataType::Json => String::from_utf8(data)?, - }; - let thread = DbThread::from_json(json_data.as_bytes())?; - Ok(Some(thread)) - } else { - Ok(None) - } - }) - } - - pub fn save_thread(&self, id: acp::SessionId, thread: DbThread) -> Task> { - let connection = self.connection.clone(); - - self.executor - .spawn(async move { Self::save_thread_sync(&connection, id, thread) }) - } - - pub fn delete_thread(&self, id: acp::SessionId) -> Task> { - let connection = self.connection.clone(); - - self.executor.spawn(async move { - let connection = connection.lock(); - - let mut delete = connection.exec_bound::>(indoc! {" - DELETE FROM threads WHERE id = ? - "})?; - - delete(id.0)?; - - Ok(()) - }) - } - - pub fn delete_threads(&self) -> Task> { - let connection = self.connection.clone(); - - self.executor.spawn(async move { - let connection = connection.lock(); - - let mut delete = connection.exec_bound::<()>(indoc! {" - DELETE FROM threads - "})?; - - delete(())?; - - Ok(()) - }) - } -} diff --git a/crates/agent/src/edit_agent.rs b/crates/agent/src/edit_agent.rs deleted file mode 100644 index 5ea04729a4..0000000000 --- a/crates/agent/src/edit_agent.rs +++ /dev/null @@ -1,1505 +0,0 @@ -mod create_file_parser; -mod edit_parser; -#[cfg(test)] -mod evals; -mod streaming_fuzzy_matcher; - -use crate::{Template, Templates}; -use action_log::ActionLog; -use anyhow::Result; -use cloud_llm_client::CompletionIntent; -use create_file_parser::{CreateFileParser, CreateFileParserEvent}; -pub use edit_parser::EditFormat; -use edit_parser::{EditParser, EditParserEvent, EditParserMetrics}; -use futures::{ - Stream, StreamExt, - channel::mpsc::{self, UnboundedReceiver}, - pin_mut, - stream::BoxStream, -}; -use gpui::{AppContext, AsyncApp, Entity, Task}; -use language::{Anchor, Buffer, BufferSnapshot, LineIndent, Point, TextBufferSnapshot}; -use language_model::{ - LanguageModel, LanguageModelCompletionError, LanguageModelRequest, LanguageModelRequestMessage, - LanguageModelToolChoice, MessageContent, Role, -}; -use project::{AgentLocation, Project}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::{cmp, iter, mem, ops::Range, pin::Pin, sync::Arc, task::Poll}; -use streaming_diff::{CharOperation, StreamingDiff}; -use streaming_fuzzy_matcher::StreamingFuzzyMatcher; - -#[derive(Serialize)] -struct CreateFilePromptTemplate { - path: Option, - edit_description: String, -} - -impl Template for CreateFilePromptTemplate { - const TEMPLATE_NAME: &'static str = "create_file_prompt.hbs"; -} - -#[derive(Serialize)] -struct EditFileXmlPromptTemplate { - path: Option, - edit_description: String, -} - -impl Template for EditFileXmlPromptTemplate { - const TEMPLATE_NAME: &'static str = "edit_file_prompt_xml.hbs"; -} - -#[derive(Serialize)] -struct EditFileDiffFencedPromptTemplate { - path: Option, - edit_description: String, -} - -impl Template for EditFileDiffFencedPromptTemplate { - const TEMPLATE_NAME: &'static str = "edit_file_prompt_diff_fenced.hbs"; -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum EditAgentOutputEvent { - ResolvingEditRange(Range), - UnresolvedEditRange, - AmbiguousEditRange(Vec>), - Edited(Range), -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -pub struct EditAgentOutput { - pub raw_edits: String, - pub parser_metrics: EditParserMetrics, -} - -#[derive(Clone)] -pub struct EditAgent { - model: Arc, - action_log: Entity, - project: Entity, - templates: Arc, - edit_format: EditFormat, -} - -impl EditAgent { - pub fn new( - model: Arc, - project: Entity, - action_log: Entity, - templates: Arc, - edit_format: EditFormat, - ) -> Self { - EditAgent { - model, - project, - action_log, - templates, - edit_format, - } - } - - pub fn overwrite( - &self, - buffer: Entity, - edit_description: String, - conversation: &LanguageModelRequest, - cx: &mut AsyncApp, - ) -> ( - Task>, - mpsc::UnboundedReceiver, - ) { - let this = self.clone(); - let (events_tx, events_rx) = mpsc::unbounded(); - let conversation = conversation.clone(); - let output = cx.spawn(async move |cx| { - let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?; - let path = cx.update(|cx| snapshot.resolve_file_path(true, cx))?; - let prompt = CreateFilePromptTemplate { - path, - edit_description, - } - .render(&this.templates)?; - let new_chunks = this - .request(conversation, CompletionIntent::CreateFile, prompt, cx) - .await?; - - let (output, mut inner_events) = this.overwrite_with_chunks(buffer, new_chunks, cx); - while let Some(event) = inner_events.next().await { - events_tx.unbounded_send(event).ok(); - } - output.await - }); - (output, events_rx) - } - - fn overwrite_with_chunks( - &self, - buffer: Entity, - edit_chunks: impl 'static + Send + Stream>, - cx: &mut AsyncApp, - ) -> ( - Task>, - mpsc::UnboundedReceiver, - ) { - let (output_events_tx, output_events_rx) = mpsc::unbounded(); - let (parse_task, parse_rx) = Self::parse_create_file_chunks(edit_chunks, cx); - let this = self.clone(); - let task = cx.spawn(async move |cx| { - this.action_log - .update(cx, |log, cx| log.buffer_created(buffer.clone(), cx))?; - this.overwrite_with_chunks_internal(buffer, parse_rx, output_events_tx, cx) - .await?; - parse_task.await - }); - (task, output_events_rx) - } - - async fn overwrite_with_chunks_internal( - &self, - buffer: Entity, - mut parse_rx: UnboundedReceiver>, - output_events_tx: mpsc::UnboundedSender, - cx: &mut AsyncApp, - ) -> Result<()> { - cx.update(|cx| { - buffer.update(cx, |buffer, cx| buffer.set_text("", cx)); - self.action_log.update(cx, |log, cx| { - log.buffer_edited(buffer.clone(), cx); - }); - self.project.update(cx, |project, cx| { - project.set_agent_location( - Some(AgentLocation { - buffer: buffer.downgrade(), - position: language::Anchor::max_for_buffer(buffer.read(cx).remote_id()), - }), - cx, - ) - }); - output_events_tx - .unbounded_send(EditAgentOutputEvent::Edited( - Anchor::min_max_range_for_buffer(buffer.read(cx).remote_id()), - )) - .ok(); - })?; - - while let Some(event) = parse_rx.next().await { - match event? { - CreateFileParserEvent::NewTextChunk { chunk } => { - let buffer_id = cx.update(|cx| { - buffer.update(cx, |buffer, cx| buffer.append(chunk, cx)); - self.action_log - .update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - self.project.update(cx, |project, cx| { - project.set_agent_location( - Some(AgentLocation { - buffer: buffer.downgrade(), - position: language::Anchor::max_for_buffer( - buffer.read(cx).remote_id(), - ), - }), - cx, - ) - }); - buffer.read(cx).remote_id() - })?; - output_events_tx - .unbounded_send(EditAgentOutputEvent::Edited( - Anchor::min_max_range_for_buffer(buffer_id), - )) - .ok(); - } - } - } - - Ok(()) - } - - pub fn edit( - &self, - buffer: Entity, - edit_description: String, - conversation: &LanguageModelRequest, - cx: &mut AsyncApp, - ) -> ( - Task>, - mpsc::UnboundedReceiver, - ) { - let this = self.clone(); - let (events_tx, events_rx) = mpsc::unbounded(); - let conversation = conversation.clone(); - let edit_format = self.edit_format; - let output = cx.spawn(async move |cx| { - let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?; - let path = cx.update(|cx| snapshot.resolve_file_path(true, cx))?; - let prompt = match edit_format { - EditFormat::XmlTags => EditFileXmlPromptTemplate { - path, - edit_description, - } - .render(&this.templates)?, - EditFormat::DiffFenced => EditFileDiffFencedPromptTemplate { - path, - edit_description, - } - .render(&this.templates)?, - }; - - let edit_chunks = this - .request(conversation, CompletionIntent::EditFile, prompt, cx) - .await?; - this.apply_edit_chunks(buffer, edit_chunks, events_tx, cx) - .await - }); - (output, events_rx) - } - - async fn apply_edit_chunks( - &self, - buffer: Entity, - edit_chunks: impl 'static + Send + Stream>, - output_events: mpsc::UnboundedSender, - cx: &mut AsyncApp, - ) -> Result { - self.action_log - .update(cx, |log, cx| log.buffer_read(buffer.clone(), cx))?; - - let (output, edit_events) = Self::parse_edit_chunks(edit_chunks, self.edit_format, cx); - let mut edit_events = edit_events.peekable(); - while let Some(edit_event) = Pin::new(&mut edit_events).peek().await { - // Skip events until we're at the start of a new edit. - let Ok(EditParserEvent::OldTextChunk { .. }) = edit_event else { - edit_events.next().await.unwrap()?; - continue; - }; - - let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?; - - // Resolve the old text in the background, updating the agent - // location as we keep refining which range it corresponds to. - let (resolve_old_text, mut old_range) = - Self::resolve_old_text(snapshot.text.clone(), edit_events, cx); - while let Ok(old_range) = old_range.recv().await { - if let Some(old_range) = old_range { - let old_range = snapshot.anchor_before(old_range.start) - ..snapshot.anchor_before(old_range.end); - self.project.update(cx, |project, cx| { - project.set_agent_location( - Some(AgentLocation { - buffer: buffer.downgrade(), - position: old_range.end, - }), - cx, - ); - })?; - output_events - .unbounded_send(EditAgentOutputEvent::ResolvingEditRange(old_range)) - .ok(); - } - } - - let (edit_events_, mut resolved_old_text) = resolve_old_text.await?; - edit_events = edit_events_; - - // If we can't resolve the old text, restart the loop waiting for a - // new edit (or for the stream to end). - let resolved_old_text = match resolved_old_text.len() { - 1 => resolved_old_text.pop().unwrap(), - 0 => { - output_events - .unbounded_send(EditAgentOutputEvent::UnresolvedEditRange) - .ok(); - continue; - } - _ => { - let ranges = resolved_old_text - .into_iter() - .map(|text| { - let start_line = - (snapshot.offset_to_point(text.range.start).row + 1) as usize; - let end_line = - (snapshot.offset_to_point(text.range.end).row + 1) as usize; - start_line..end_line - }) - .collect(); - output_events - .unbounded_send(EditAgentOutputEvent::AmbiguousEditRange(ranges)) - .ok(); - continue; - } - }; - - // Compute edits in the background and apply them as they become - // available. - let (compute_edits, edits) = - Self::compute_edits(snapshot, resolved_old_text, edit_events, cx); - let mut edits = edits.ready_chunks(32); - while let Some(edits) = edits.next().await { - if edits.is_empty() { - continue; - } - - // Edit the buffer and report edits to the action log as part of the - // same effect cycle, otherwise the edit will be reported as if the - // user made it. - let (min_edit_start, max_edit_end) = cx.update(|cx| { - let (min_edit_start, max_edit_end) = buffer.update(cx, |buffer, cx| { - buffer.edit(edits.iter().cloned(), None, cx); - let max_edit_end = buffer - .summaries_for_anchors::( - edits.iter().map(|(range, _)| &range.end), - ) - .max() - .unwrap(); - let min_edit_start = buffer - .summaries_for_anchors::( - edits.iter().map(|(range, _)| &range.start), - ) - .min() - .unwrap(); - ( - buffer.anchor_after(min_edit_start), - buffer.anchor_before(max_edit_end), - ) - }); - self.action_log - .update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - self.project.update(cx, |project, cx| { - project.set_agent_location( - Some(AgentLocation { - buffer: buffer.downgrade(), - position: max_edit_end, - }), - cx, - ); - }); - (min_edit_start, max_edit_end) - })?; - output_events - .unbounded_send(EditAgentOutputEvent::Edited(min_edit_start..max_edit_end)) - .ok(); - } - - edit_events = compute_edits.await?; - } - - output.await - } - - fn parse_edit_chunks( - chunks: impl 'static + Send + Stream>, - edit_format: EditFormat, - cx: &mut AsyncApp, - ) -> ( - Task>, - UnboundedReceiver>, - ) { - let (tx, rx) = mpsc::unbounded(); - let output = cx.background_spawn(async move { - pin_mut!(chunks); - - let mut parser = EditParser::new(edit_format); - let mut raw_edits = String::new(); - while let Some(chunk) = chunks.next().await { - match chunk { - Ok(chunk) => { - raw_edits.push_str(&chunk); - for event in parser.push(&chunk) { - tx.unbounded_send(Ok(event))?; - } - } - Err(error) => { - tx.unbounded_send(Err(error.into()))?; - } - } - } - Ok(EditAgentOutput { - raw_edits, - parser_metrics: parser.finish(), - }) - }); - (output, rx) - } - - fn parse_create_file_chunks( - chunks: impl 'static + Send + Stream>, - cx: &mut AsyncApp, - ) -> ( - Task>, - UnboundedReceiver>, - ) { - let (tx, rx) = mpsc::unbounded(); - let output = cx.background_spawn(async move { - pin_mut!(chunks); - - let mut parser = CreateFileParser::new(); - let mut raw_edits = String::new(); - while let Some(chunk) = chunks.next().await { - match chunk { - Ok(chunk) => { - raw_edits.push_str(&chunk); - for event in parser.push(Some(&chunk)) { - tx.unbounded_send(Ok(event))?; - } - } - Err(error) => { - tx.unbounded_send(Err(error.into()))?; - } - } - } - // Send final events with None to indicate completion - for event in parser.push(None) { - tx.unbounded_send(Ok(event))?; - } - Ok(EditAgentOutput { - raw_edits, - parser_metrics: EditParserMetrics::default(), - }) - }); - (output, rx) - } - - fn resolve_old_text( - snapshot: TextBufferSnapshot, - mut edit_events: T, - cx: &mut AsyncApp, - ) -> ( - Task)>>, - watch::Receiver>>, - ) - where - T: 'static + Send + Unpin + Stream>, - { - let (mut old_range_tx, old_range_rx) = watch::channel(None); - let task = cx.background_spawn(async move { - let mut matcher = StreamingFuzzyMatcher::new(snapshot); - while let Some(edit_event) = edit_events.next().await { - let EditParserEvent::OldTextChunk { - chunk, - done, - line_hint, - } = edit_event? - else { - break; - }; - - old_range_tx.send(matcher.push(&chunk, line_hint))?; - if done { - break; - } - } - - let matches = matcher.finish(); - let best_match = matcher.select_best_match(); - - old_range_tx.send(best_match.clone())?; - - let indent = LineIndent::from_iter( - matcher - .query_lines() - .first() - .unwrap_or(&String::new()) - .chars(), - ); - - let resolved_old_texts = if let Some(best_match) = best_match { - vec![ResolvedOldText { - range: best_match, - indent, - }] - } else { - matches - .into_iter() - .map(|range| ResolvedOldText { range, indent }) - .collect::>() - }; - - Ok((edit_events, resolved_old_texts)) - }); - - (task, old_range_rx) - } - - fn compute_edits( - snapshot: BufferSnapshot, - resolved_old_text: ResolvedOldText, - mut edit_events: T, - cx: &mut AsyncApp, - ) -> ( - Task>, - UnboundedReceiver<(Range, Arc)>, - ) - where - T: 'static + Send + Unpin + Stream>, - { - let (edits_tx, edits_rx) = mpsc::unbounded(); - let compute_edits = cx.background_spawn(async move { - let buffer_start_indent = snapshot - .line_indent_for_row(snapshot.offset_to_point(resolved_old_text.range.start).row); - let indent_delta = if buffer_start_indent.tabs > 0 { - IndentDelta::Tabs( - buffer_start_indent.tabs as isize - resolved_old_text.indent.tabs as isize, - ) - } else { - IndentDelta::Spaces( - buffer_start_indent.spaces as isize - resolved_old_text.indent.spaces as isize, - ) - }; - - let old_text = snapshot - .text_for_range(resolved_old_text.range.clone()) - .collect::(); - let mut diff = StreamingDiff::new(old_text); - let mut edit_start = resolved_old_text.range.start; - let mut new_text_chunks = - Self::reindent_new_text_chunks(indent_delta, &mut edit_events); - let mut done = false; - while !done { - let char_operations = if let Some(new_text_chunk) = new_text_chunks.next().await { - diff.push_new(&new_text_chunk?) - } else { - done = true; - mem::take(&mut diff).finish() - }; - - for op in char_operations { - match op { - CharOperation::Insert { text } => { - let edit_start = snapshot.anchor_after(edit_start); - edits_tx.unbounded_send((edit_start..edit_start, Arc::from(text)))?; - } - CharOperation::Delete { bytes } => { - let edit_end = edit_start + bytes; - let edit_range = - snapshot.anchor_after(edit_start)..snapshot.anchor_before(edit_end); - edit_start = edit_end; - edits_tx.unbounded_send((edit_range, Arc::from("")))?; - } - CharOperation::Keep { bytes } => edit_start += bytes, - } - } - } - - drop(new_text_chunks); - anyhow::Ok(edit_events) - }); - - (compute_edits, edits_rx) - } - - fn reindent_new_text_chunks( - delta: IndentDelta, - mut stream: impl Unpin + Stream>, - ) -> impl Stream> { - let mut buffer = String::new(); - let mut in_leading_whitespace = true; - let mut done = false; - futures::stream::poll_fn(move |cx| { - while !done { - let (chunk, is_last_chunk) = match stream.poll_next_unpin(cx) { - Poll::Ready(Some(Ok(EditParserEvent::NewTextChunk { chunk, done }))) => { - (chunk, done) - } - Poll::Ready(Some(Err(err))) => return Poll::Ready(Some(Err(err))), - Poll::Pending => return Poll::Pending, - _ => return Poll::Ready(None), - }; - - buffer.push_str(&chunk); - - let mut indented_new_text = String::new(); - let mut start_ix = 0; - let mut newlines = buffer.match_indices('\n').peekable(); - loop { - let (line_end, is_pending_line) = match newlines.next() { - Some((ix, _)) => (ix, false), - None => (buffer.len(), true), - }; - let line = &buffer[start_ix..line_end]; - - if in_leading_whitespace { - if let Some(non_whitespace_ix) = line.find(|c| delta.character() != c) { - // We found a non-whitespace character, adjust - // indentation based on the delta. - let new_indent_len = - cmp::max(0, non_whitespace_ix as isize + delta.len()) as usize; - indented_new_text - .extend(iter::repeat(delta.character()).take(new_indent_len)); - indented_new_text.push_str(&line[non_whitespace_ix..]); - in_leading_whitespace = false; - } else if is_pending_line { - // We're still in leading whitespace and this line is incomplete. - // Stop processing until we receive more input. - break; - } else { - // This line is entirely whitespace. Push it without indentation. - indented_new_text.push_str(line); - } - } else { - indented_new_text.push_str(line); - } - - if is_pending_line { - start_ix = line_end; - break; - } else { - in_leading_whitespace = true; - indented_new_text.push('\n'); - start_ix = line_end + 1; - } - } - buffer.replace_range(..start_ix, ""); - - // This was the last chunk, push all the buffered content as-is. - if is_last_chunk { - indented_new_text.push_str(&buffer); - buffer.clear(); - done = true; - } - - if !indented_new_text.is_empty() { - return Poll::Ready(Some(Ok(indented_new_text))); - } - } - - Poll::Ready(None) - }) - } - - async fn request( - &self, - mut conversation: LanguageModelRequest, - intent: CompletionIntent, - prompt: String, - cx: &mut AsyncApp, - ) -> Result>> { - let mut messages_iter = conversation.messages.iter_mut(); - if let Some(last_message) = messages_iter.next_back() - && last_message.role == Role::Assistant - { - let old_content_len = last_message.content.len(); - last_message - .content - .retain(|content| !matches!(content, MessageContent::ToolUse(_))); - let new_content_len = last_message.content.len(); - - // We just removed pending tool uses from the content of the - // last message, so it doesn't make sense to cache it anymore - // (e.g., the message will look very different on the next - // request). Thus, we move the flag to the message prior to it, - // as it will still be a valid prefix of the conversation. - if old_content_len != new_content_len - && last_message.cache - && let Some(prev_message) = messages_iter.next_back() - { - last_message.cache = false; - prev_message.cache = true; - } - - if last_message.content.is_empty() { - conversation.messages.pop(); - } - } - - conversation.messages.push(LanguageModelRequestMessage { - role: Role::User, - content: vec![MessageContent::Text(prompt)], - cache: false, - reasoning_details: None, - }); - - // Include tools in the request so that we can take advantage of - // caching when ToolChoice::None is supported. - let mut tool_choice = None; - let mut tools = Vec::new(); - if !conversation.tools.is_empty() - && self - .model - .supports_tool_choice(LanguageModelToolChoice::None) - { - tool_choice = Some(LanguageModelToolChoice::None); - tools = conversation.tools.clone(); - } - - let request = LanguageModelRequest { - thread_id: conversation.thread_id, - prompt_id: conversation.prompt_id, - intent: Some(intent), - mode: conversation.mode, - messages: conversation.messages, - tool_choice, - tools, - stop: Vec::new(), - temperature: None, - thinking_allowed: true, - }; - - Ok(self.model.stream_completion_text(request, cx).await?.stream) - } -} - -struct ResolvedOldText { - range: Range, - indent: LineIndent, -} - -#[derive(Copy, Clone, Debug)] -enum IndentDelta { - Spaces(isize), - Tabs(isize), -} - -impl IndentDelta { - fn character(&self) -> char { - match self { - IndentDelta::Spaces(_) => ' ', - IndentDelta::Tabs(_) => '\t', - } - } - - fn len(&self) -> isize { - match self { - IndentDelta::Spaces(n) => *n, - IndentDelta::Tabs(n) => *n, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use fs::FakeFs; - use futures::stream; - use gpui::{AppContext, TestAppContext}; - use indoc::indoc; - use language_model::fake_provider::FakeLanguageModel; - use pretty_assertions::assert_matches; - use project::{AgentLocation, Project}; - use rand::prelude::*; - use rand::rngs::StdRng; - use std::cmp; - - #[gpui::test(iterations = 100)] - async fn test_empty_old_text(cx: &mut TestAppContext, mut rng: StdRng) { - let agent = init_test(cx).await; - let buffer = cx.new(|cx| { - Buffer::local( - indoc! {" - abc - def - ghi - "}, - cx, - ) - }); - let (apply, _events) = agent.edit( - buffer.clone(), - String::new(), - &LanguageModelRequest::default(), - &mut cx.to_async(), - ); - cx.run_until_parked(); - - simulate_llm_output( - &agent, - indoc! {" - - jkl - def - DEF - "}, - &mut rng, - cx, - ); - apply.await.unwrap(); - - pretty_assertions::assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - indoc! {" - abc - DEF - ghi - "} - ); - } - - #[gpui::test(iterations = 100)] - async fn test_indentation(cx: &mut TestAppContext, mut rng: StdRng) { - let agent = init_test(cx).await; - let buffer = cx.new(|cx| { - Buffer::local( - indoc! {" - lorem - ipsum - dolor - sit - "}, - cx, - ) - }); - let (apply, _events) = agent.edit( - buffer.clone(), - String::new(), - &LanguageModelRequest::default(), - &mut cx.to_async(), - ); - cx.run_until_parked(); - - simulate_llm_output( - &agent, - indoc! {" - - ipsum - dolor - sit - - - ipsum - dolor - sit - amet - - "}, - &mut rng, - cx, - ); - apply.await.unwrap(); - - pretty_assertions::assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - indoc! {" - lorem - ipsum - dolor - sit - amet - "} - ); - } - - #[gpui::test(iterations = 100)] - async fn test_dependent_edits(cx: &mut TestAppContext, mut rng: StdRng) { - let agent = init_test(cx).await; - let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi", cx)); - let (apply, _events) = agent.edit( - buffer.clone(), - String::new(), - &LanguageModelRequest::default(), - &mut cx.to_async(), - ); - cx.run_until_parked(); - - simulate_llm_output( - &agent, - indoc! {" - - def - - - DEF - - - - DEF - - - DeF - - "}, - &mut rng, - cx, - ); - apply.await.unwrap(); - - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "abc\nDeF\nghi" - ); - } - - #[gpui::test(iterations = 100)] - async fn test_old_text_hallucination(cx: &mut TestAppContext, mut rng: StdRng) { - let agent = init_test(cx).await; - let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi", cx)); - let (apply, _events) = agent.edit( - buffer.clone(), - String::new(), - &LanguageModelRequest::default(), - &mut cx.to_async(), - ); - cx.run_until_parked(); - - simulate_llm_output( - &agent, - indoc! {" - - jkl - - - mno - - - - abc - - - ABC - - "}, - &mut rng, - cx, - ); - apply.await.unwrap(); - - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "ABC\ndef\nghi" - ); - } - - #[gpui::test] - async fn test_edit_events(cx: &mut TestAppContext) { - let agent = init_test(cx).await; - let model = agent.model.as_fake(); - let project = agent - .action_log - .read_with(cx, |log, _| log.project().clone()); - let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi\njkl", cx)); - - let mut async_cx = cx.to_async(); - let (apply, mut events) = agent.edit( - buffer.clone(), - String::new(), - &LanguageModelRequest::default(), - &mut async_cx, - ); - cx.run_until_parked(); - - model.send_last_completion_stream_text_chunk("a"); - cx.run_until_parked(); - assert_eq!(drain_events(&mut events), vec![]); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "abc\ndef\nghi\njkl" - ); - assert_eq!( - project.read_with(cx, |project, _| project.agent_location()), - None - ); - - model.send_last_completion_stream_text_chunk("bc"); - cx.run_until_parked(); - assert_eq!( - drain_events(&mut events), - vec![EditAgentOutputEvent::ResolvingEditRange(buffer.read_with( - cx, - |buffer, _| buffer.anchor_before(Point::new(0, 0)) - ..buffer.anchor_before(Point::new(0, 3)) - ))] - ); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "abc\ndef\nghi\njkl" - ); - assert_eq!( - project.read_with(cx, |project, _| project.agent_location()), - Some(AgentLocation { - buffer: buffer.downgrade(), - position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(0, 3))) - }) - ); - - model.send_last_completion_stream_text_chunk("abX"); - cx.run_until_parked(); - assert_matches!( - drain_events(&mut events).as_slice(), - [EditAgentOutputEvent::Edited(_)] - ); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "abXc\ndef\nghi\njkl" - ); - assert_eq!( - project.read_with(cx, |project, _| project.agent_location()), - Some(AgentLocation { - buffer: buffer.downgrade(), - position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(0, 3))) - }) - ); - - model.send_last_completion_stream_text_chunk("cY"); - cx.run_until_parked(); - assert_matches!( - drain_events(&mut events).as_slice(), - [EditAgentOutputEvent::Edited { .. }] - ); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "abXcY\ndef\nghi\njkl" - ); - assert_eq!( - project.read_with(cx, |project, _| project.agent_location()), - Some(AgentLocation { - buffer: buffer.downgrade(), - position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(0, 5))) - }) - ); - - model.send_last_completion_stream_text_chunk(""); - model.send_last_completion_stream_text_chunk("hall"); - cx.run_until_parked(); - assert_eq!(drain_events(&mut events), vec![]); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "abXcY\ndef\nghi\njkl" - ); - assert_eq!( - project.read_with(cx, |project, _| project.agent_location()), - Some(AgentLocation { - buffer: buffer.downgrade(), - position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(0, 5))) - }) - ); - - model.send_last_completion_stream_text_chunk("ucinated old"); - model.send_last_completion_stream_text_chunk(""); - cx.run_until_parked(); - assert_eq!( - drain_events(&mut events), - vec![EditAgentOutputEvent::UnresolvedEditRange] - ); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "abXcY\ndef\nghi\njkl" - ); - assert_eq!( - project.read_with(cx, |project, _| project.agent_location()), - Some(AgentLocation { - buffer: buffer.downgrade(), - position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(0, 5))) - }) - ); - - model.send_last_completion_stream_text_chunk("hallucinated new"); - cx.run_until_parked(); - assert_eq!(drain_events(&mut events), vec![]); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "abXcY\ndef\nghi\njkl" - ); - assert_eq!( - project.read_with(cx, |project, _| project.agent_location()), - Some(AgentLocation { - buffer: buffer.downgrade(), - position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(0, 5))) - }) - ); - - model.send_last_completion_stream_text_chunk("\nghi\nj"); - cx.run_until_parked(); - assert_eq!( - drain_events(&mut events), - vec![EditAgentOutputEvent::ResolvingEditRange(buffer.read_with( - cx, - |buffer, _| buffer.anchor_before(Point::new(2, 0)) - ..buffer.anchor_before(Point::new(2, 3)) - ))] - ); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "abXcY\ndef\nghi\njkl" - ); - assert_eq!( - project.read_with(cx, |project, _| project.agent_location()), - Some(AgentLocation { - buffer: buffer.downgrade(), - position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(2, 3))) - }) - ); - - model.send_last_completion_stream_text_chunk("kl"); - model.send_last_completion_stream_text_chunk(""); - cx.run_until_parked(); - assert_eq!( - drain_events(&mut events), - vec![EditAgentOutputEvent::ResolvingEditRange(buffer.read_with( - cx, - |buffer, _| buffer.anchor_before(Point::new(2, 0)) - ..buffer.anchor_before(Point::new(3, 3)) - ))] - ); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "abXcY\ndef\nghi\njkl" - ); - assert_eq!( - project.read_with(cx, |project, _| project.agent_location()), - Some(AgentLocation { - buffer: buffer.downgrade(), - position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(3, 3))) - }) - ); - - model.send_last_completion_stream_text_chunk("GHI"); - cx.run_until_parked(); - assert_matches!( - drain_events(&mut events).as_slice(), - [EditAgentOutputEvent::Edited { .. }] - ); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "abXcY\ndef\nGHI" - ); - assert_eq!( - project.read_with(cx, |project, _| project.agent_location()), - Some(AgentLocation { - buffer: buffer.downgrade(), - position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(2, 3))) - }) - ); - - model.end_last_completion_stream(); - apply.await.unwrap(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "abXcY\ndef\nGHI" - ); - assert_eq!(drain_events(&mut events), vec![]); - assert_eq!( - project.read_with(cx, |project, _| project.agent_location()), - Some(AgentLocation { - buffer: buffer.downgrade(), - position: buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(2, 3))) - }) - ); - } - - #[gpui::test] - async fn test_overwrite_events(cx: &mut TestAppContext) { - let agent = init_test(cx).await; - let project = agent - .action_log - .read_with(cx, |log, _| log.project().clone()); - let buffer = cx.new(|cx| Buffer::local("abc\ndef\nghi", cx)); - let (chunks_tx, chunks_rx) = mpsc::unbounded(); - let (apply, mut events) = agent.overwrite_with_chunks( - buffer.clone(), - chunks_rx.map(|chunk: &str| Ok(chunk.to_string())), - &mut cx.to_async(), - ); - - cx.run_until_parked(); - assert_matches!( - drain_events(&mut events).as_slice(), - [EditAgentOutputEvent::Edited(_)] - ); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "" - ); - assert_eq!( - project.read_with(cx, |project, _| project.agent_location()), - Some(AgentLocation { - buffer: buffer.downgrade(), - position: language::Anchor::max_for_buffer( - cx.update(|cx| buffer.read(cx).remote_id()) - ), - }) - ); - - chunks_tx.unbounded_send("```\njkl\n").unwrap(); - cx.run_until_parked(); - assert_matches!( - drain_events(&mut events).as_slice(), - [EditAgentOutputEvent::Edited { .. }] - ); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "jkl" - ); - assert_eq!( - project.read_with(cx, |project, _| project.agent_location()), - Some(AgentLocation { - buffer: buffer.downgrade(), - position: language::Anchor::max_for_buffer( - cx.update(|cx| buffer.read(cx).remote_id()) - ), - }) - ); - - chunks_tx.unbounded_send("mno\n").unwrap(); - cx.run_until_parked(); - assert_matches!( - drain_events(&mut events).as_slice(), - [EditAgentOutputEvent::Edited { .. }] - ); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "jkl\nmno" - ); - assert_eq!( - project.read_with(cx, |project, _| project.agent_location()), - Some(AgentLocation { - buffer: buffer.downgrade(), - position: language::Anchor::max_for_buffer( - cx.update(|cx| buffer.read(cx).remote_id()) - ), - }) - ); - - chunks_tx.unbounded_send("pqr\n```").unwrap(); - cx.run_until_parked(); - assert_matches!( - drain_events(&mut events).as_slice(), - [EditAgentOutputEvent::Edited(_)], - ); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "jkl\nmno\npqr" - ); - assert_eq!( - project.read_with(cx, |project, _| project.agent_location()), - Some(AgentLocation { - buffer: buffer.downgrade(), - position: language::Anchor::max_for_buffer( - cx.update(|cx| buffer.read(cx).remote_id()) - ), - }) - ); - - drop(chunks_tx); - apply.await.unwrap(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.snapshot().text()), - "jkl\nmno\npqr" - ); - assert_eq!(drain_events(&mut events), vec![]); - assert_eq!( - project.read_with(cx, |project, _| project.agent_location()), - Some(AgentLocation { - buffer: buffer.downgrade(), - position: language::Anchor::max_for_buffer( - cx.update(|cx| buffer.read(cx).remote_id()) - ), - }) - ); - } - - #[gpui::test(iterations = 100)] - async fn test_indent_new_text_chunks(mut rng: StdRng) { - let chunks = to_random_chunks(&mut rng, " abc\n def\n ghi"); - let new_text_chunks = stream::iter(chunks.iter().enumerate().map(|(index, chunk)| { - Ok(EditParserEvent::NewTextChunk { - chunk: chunk.clone(), - done: index == chunks.len() - 1, - }) - })); - let indented_chunks = - EditAgent::reindent_new_text_chunks(IndentDelta::Spaces(2), new_text_chunks) - .collect::>() - .await; - let new_text = indented_chunks - .into_iter() - .collect::>() - .unwrap(); - assert_eq!(new_text, " abc\n def\n ghi"); - } - - #[gpui::test(iterations = 100)] - async fn test_outdent_new_text_chunks(mut rng: StdRng) { - let chunks = to_random_chunks(&mut rng, "\t\t\t\tabc\n\t\tdef\n\t\t\t\t\t\tghi"); - let new_text_chunks = stream::iter(chunks.iter().enumerate().map(|(index, chunk)| { - Ok(EditParserEvent::NewTextChunk { - chunk: chunk.clone(), - done: index == chunks.len() - 1, - }) - })); - let indented_chunks = - EditAgent::reindent_new_text_chunks(IndentDelta::Tabs(-2), new_text_chunks) - .collect::>() - .await; - let new_text = indented_chunks - .into_iter() - .collect::>() - .unwrap(); - assert_eq!(new_text, "\t\tabc\ndef\n\t\t\t\tghi"); - } - - #[gpui::test(iterations = 100)] - async fn test_random_indents(mut rng: StdRng) { - let len = rng.random_range(1..=100); - let new_text = util::RandomCharIter::new(&mut rng) - .with_simple_text() - .take(len) - .collect::(); - let new_text = new_text - .split('\n') - .map(|line| format!("{}{}", " ".repeat(rng.random_range(0..=8)), line)) - .collect::>() - .join("\n"); - let delta = IndentDelta::Spaces(rng.random_range(-4i8..=4i8) as isize); - - let chunks = to_random_chunks(&mut rng, &new_text); - let new_text_chunks = stream::iter(chunks.iter().enumerate().map(|(index, chunk)| { - Ok(EditParserEvent::NewTextChunk { - chunk: chunk.clone(), - done: index == chunks.len() - 1, - }) - })); - let reindented_chunks = EditAgent::reindent_new_text_chunks(delta, new_text_chunks) - .collect::>() - .await; - let actual_reindented_text = reindented_chunks - .into_iter() - .collect::>() - .unwrap(); - let expected_reindented_text = new_text - .split('\n') - .map(|line| { - if let Some(ix) = line.find(|c| c != ' ') { - let new_indent = cmp::max(0, ix as isize + delta.len()) as usize; - format!("{}{}", " ".repeat(new_indent), &line[ix..]) - } else { - line.to_string() - } - }) - .collect::>() - .join("\n"); - assert_eq!(actual_reindented_text, expected_reindented_text); - } - - fn to_random_chunks(rng: &mut StdRng, input: &str) -> Vec { - let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50)); - let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count); - chunk_indices.sort(); - chunk_indices.push(input.len()); - - let mut chunks = Vec::new(); - let mut last_ix = 0; - for chunk_ix in chunk_indices { - chunks.push(input[last_ix..chunk_ix].to_string()); - last_ix = chunk_ix; - } - chunks - } - - fn simulate_llm_output( - agent: &EditAgent, - output: &str, - rng: &mut StdRng, - cx: &mut TestAppContext, - ) { - let executor = cx.executor(); - let chunks = to_random_chunks(rng, output); - let model = agent.model.clone(); - cx.background_spawn(async move { - for chunk in chunks { - executor.simulate_random_delay().await; - model - .as_fake() - .send_last_completion_stream_text_chunk(chunk); - } - model.as_fake().end_last_completion_stream(); - }) - .detach(); - } - - async fn init_test(cx: &mut TestAppContext) -> EditAgent { - cx.update(settings::init); - - let project = Project::test(FakeFs::new(cx.executor()), [], cx).await; - let model = Arc::new(FakeLanguageModel::default()); - let action_log = cx.new(|_| ActionLog::new(project.clone())); - EditAgent::new( - model, - project, - action_log, - Templates::new(), - EditFormat::XmlTags, - ) - } - - #[gpui::test(iterations = 10)] - async fn test_non_unique_text_error(cx: &mut TestAppContext, mut rng: StdRng) { - let agent = init_test(cx).await; - let original_text = indoc! {" - function foo() { - return 42; - } - - function bar() { - return 42; - } - - function baz() { - return 42; - } - "}; - let buffer = cx.new(|cx| Buffer::local(original_text, cx)); - let (apply, mut events) = agent.edit( - buffer.clone(), - String::new(), - &LanguageModelRequest::default(), - &mut cx.to_async(), - ); - cx.run_until_parked(); - - // When matches text in more than one place - simulate_llm_output( - &agent, - indoc! {" - - return 42; - } - - - return 100; - } - - "}, - &mut rng, - cx, - ); - apply.await.unwrap(); - - // Then the text should remain unchanged - let result_text = buffer.read_with(cx, |buffer, _| buffer.snapshot().text()); - assert_eq!( - result_text, - indoc! {" - function foo() { - return 42; - } - - function bar() { - return 42; - } - - function baz() { - return 42; - } - "}, - "Text should remain unchanged when there are multiple matches" - ); - - // And AmbiguousEditRange even should be emitted - let events = drain_events(&mut events); - let ambiguous_ranges = vec![2..3, 6..7, 10..11]; - assert!( - events.contains(&EditAgentOutputEvent::AmbiguousEditRange(ambiguous_ranges)), - "Should emit AmbiguousEditRange for non-unique text" - ); - } - - fn drain_events( - stream: &mut UnboundedReceiver, - ) -> Vec { - let mut events = Vec::new(); - while let Ok(Some(event)) = stream.try_next() { - events.push(event); - } - events - } -} diff --git a/crates/agent/src/edit_agent/create_file_parser.rs b/crates/agent/src/edit_agent/create_file_parser.rs deleted file mode 100644 index 2272434d79..0000000000 --- a/crates/agent/src/edit_agent/create_file_parser.rs +++ /dev/null @@ -1,237 +0,0 @@ -use std::sync::OnceLock; - -use regex::Regex; -use smallvec::SmallVec; -use util::debug_panic; - -static START_MARKER: OnceLock = OnceLock::new(); -static END_MARKER: OnceLock = OnceLock::new(); - -#[derive(Debug)] -pub enum CreateFileParserEvent { - NewTextChunk { chunk: String }, -} - -#[derive(Debug)] -pub struct CreateFileParser { - state: ParserState, - buffer: String, -} - -#[derive(Debug, PartialEq)] -enum ParserState { - Pending, - WithinText, - Finishing, - Finished, -} - -impl CreateFileParser { - pub fn new() -> Self { - CreateFileParser { - state: ParserState::Pending, - buffer: String::new(), - } - } - - pub fn push(&mut self, chunk: Option<&str>) -> SmallVec<[CreateFileParserEvent; 1]> { - if chunk.is_none() { - self.state = ParserState::Finishing; - } - - let chunk = chunk.unwrap_or_default(); - - self.buffer.push_str(chunk); - - let mut edit_events = SmallVec::new(); - let start_marker_regex = START_MARKER.get_or_init(|| Regex::new(r"\n?```\S*\n").unwrap()); - let end_marker_regex = END_MARKER.get_or_init(|| Regex::new(r"(^|\n)```\s*$").unwrap()); - loop { - match &mut self.state { - ParserState::Pending => { - if let Some(m) = start_marker_regex.find(&self.buffer) { - self.buffer.drain(..m.end()); - self.state = ParserState::WithinText; - } else { - break; - } - } - ParserState::WithinText => { - let text = self.buffer.trim_end_matches(&['`', '\n', ' ']); - let text_len = text.len(); - - if text_len > 0 { - edit_events.push(CreateFileParserEvent::NewTextChunk { - chunk: self.buffer.drain(..text_len).collect(), - }); - } - break; - } - ParserState::Finishing => { - if let Some(m) = end_marker_regex.find(&self.buffer) { - self.buffer.drain(m.start()..); - } - if !self.buffer.is_empty() { - if !self.buffer.ends_with('\n') { - self.buffer.push('\n'); - } - edit_events.push(CreateFileParserEvent::NewTextChunk { - chunk: self.buffer.drain(..).collect(), - }); - } - self.state = ParserState::Finished; - break; - } - ParserState::Finished => debug_panic!("Can't call parser after finishing"), - } - } - edit_events - } -} - -#[cfg(test)] -mod tests { - use super::*; - use indoc::indoc; - use rand::prelude::*; - use std::cmp; - - #[gpui::test(iterations = 100)] - fn test_happy_path(mut rng: StdRng) { - let mut parser = CreateFileParser::new(); - assert_eq!( - parse_random_chunks("```\nHello world\n```", &mut parser, &mut rng), - "Hello world".to_string() - ); - } - - #[gpui::test(iterations = 100)] - fn test_cut_prefix(mut rng: StdRng) { - let mut parser = CreateFileParser::new(); - assert_eq!( - parse_random_chunks( - indoc! {" - Let me write this file for you: - - ``` - Hello world - ``` - - "}, - &mut parser, - &mut rng - ), - "Hello world".to_string() - ); - } - - #[gpui::test(iterations = 100)] - fn test_language_name_on_fences(mut rng: StdRng) { - let mut parser = CreateFileParser::new(); - assert_eq!( - parse_random_chunks( - indoc! {" - ```rust - Hello world - ``` - - "}, - &mut parser, - &mut rng - ), - "Hello world".to_string() - ); - } - - #[gpui::test(iterations = 100)] - fn test_leave_suffix(mut rng: StdRng) { - let mut parser = CreateFileParser::new(); - assert_eq!( - parse_random_chunks( - indoc! {" - Let me write this file for you: - - ``` - Hello world - ``` - - The end - "}, - &mut parser, - &mut rng - ), - // This output is malformed, so we're doing our best effort - "Hello world\n```\n\nThe end\n".to_string() - ); - } - - #[gpui::test(iterations = 100)] - fn test_inner_fences(mut rng: StdRng) { - let mut parser = CreateFileParser::new(); - assert_eq!( - parse_random_chunks( - indoc! {" - Let me write this file for you: - - ``` - ``` - Hello world - ``` - ``` - "}, - &mut parser, - &mut rng - ), - // This output is malformed, so we're doing our best effort - "```\nHello world\n```\n".to_string() - ); - } - - #[gpui::test(iterations = 10)] - fn test_empty_file(mut rng: StdRng) { - let mut parser = CreateFileParser::new(); - assert_eq!( - parse_random_chunks( - indoc! {" - ``` - ``` - "}, - &mut parser, - &mut rng - ), - "".to_string() - ); - } - - fn parse_random_chunks(input: &str, parser: &mut CreateFileParser, rng: &mut StdRng) -> String { - let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50)); - let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count); - chunk_indices.sort(); - chunk_indices.push(input.len()); - - let chunk_indices = chunk_indices - .into_iter() - .map(Some) - .chain(vec![None]) - .collect::>>(); - - let mut edit = String::default(); - let mut last_ix = 0; - for chunk_ix in chunk_indices { - let mut chunk = None; - if let Some(chunk_ix) = chunk_ix { - chunk = Some(&input[last_ix..chunk_ix]); - last_ix = chunk_ix; - } - - for event in parser.push(chunk) { - match event { - CreateFileParserEvent::NewTextChunk { chunk } => { - edit.push_str(&chunk); - } - } - } - } - edit - } -} diff --git a/crates/agent/src/edit_agent/edit_parser.rs b/crates/agent/src/edit_agent/edit_parser.rs deleted file mode 100644 index c1aa61e18d..0000000000 --- a/crates/agent/src/edit_agent/edit_parser.rs +++ /dev/null @@ -1,1094 +0,0 @@ -use anyhow::bail; -use derive_more::{Add, AddAssign}; -use language_model::LanguageModel; -use regex::Regex; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use smallvec::SmallVec; -use std::{mem, ops::Range, str::FromStr, sync::Arc}; - -const OLD_TEXT_END_TAG: &str = ""; -const NEW_TEXT_END_TAG: &str = ""; -const EDITS_END_TAG: &str = ""; -const SEARCH_MARKER: &str = "<<<<<<< SEARCH"; -const SEPARATOR_MARKER: &str = "======="; -const REPLACE_MARKER: &str = ">>>>>>> REPLACE"; -const SONNET_PARAMETER_INVOKE_1: &str = "\n"; -const SONNET_PARAMETER_INVOKE_2: &str = ""; -const SONNET_PARAMETER_INVOKE_3: &str = ""; -const END_TAGS: [&str; 6] = [ - OLD_TEXT_END_TAG, - NEW_TEXT_END_TAG, - EDITS_END_TAG, - SONNET_PARAMETER_INVOKE_1, // Remove these after switching to streaming tool call - SONNET_PARAMETER_INVOKE_2, - SONNET_PARAMETER_INVOKE_3, -]; - -#[derive(Debug)] -pub enum EditParserEvent { - OldTextChunk { - chunk: String, - done: bool, - line_hint: Option, - }, - NewTextChunk { - chunk: String, - done: bool, - }, -} - -#[derive( - Clone, Debug, Default, PartialEq, Eq, Add, AddAssign, Serialize, Deserialize, JsonSchema, -)] -pub struct EditParserMetrics { - pub tags: usize, - pub mismatched_tags: usize, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum EditFormat { - /// XML-like tags: - /// ... - /// ... - XmlTags, - /// Diff-fenced format, in which: - /// - Text before the SEARCH marker is ignored - /// - Fences are optional - /// - Line hint is optional. - /// - /// Example: - /// - /// ```diff - /// <<<<<<< SEARCH line=42 - /// ... - /// ======= - /// ... - /// >>>>>>> REPLACE - /// ``` - DiffFenced, -} - -impl FromStr for EditFormat { - type Err = anyhow::Error; - - fn from_str(s: &str) -> anyhow::Result { - match s.to_lowercase().as_str() { - "xml_tags" | "xml" => Ok(EditFormat::XmlTags), - "diff_fenced" | "diff-fenced" | "diff" => Ok(EditFormat::DiffFenced), - _ => bail!("Unknown EditFormat: {}", s), - } - } -} - -impl EditFormat { - /// Return an optimal edit format for the language model - pub fn from_model(model: Arc) -> anyhow::Result { - if model.provider_id().0 == "google" || model.id().0.to_lowercase().contains("gemini") { - Ok(EditFormat::DiffFenced) - } else { - Ok(EditFormat::XmlTags) - } - } - - /// Return an optimal edit format for the language model, - /// with the ability to override it by setting the - /// `ZED_EDIT_FORMAT` environment variable - #[allow(dead_code)] - pub fn from_env(model: Arc) -> anyhow::Result { - let default = EditFormat::from_model(model)?; - std::env::var("ZED_EDIT_FORMAT").map_or(Ok(default), |s| EditFormat::from_str(&s)) - } -} - -pub trait EditFormatParser: Send + std::fmt::Debug { - fn push(&mut self, chunk: &str) -> SmallVec<[EditParserEvent; 1]>; - fn take_metrics(&mut self) -> EditParserMetrics; -} - -#[derive(Debug)] -pub struct XmlEditParser { - state: XmlParserState, - buffer: String, - metrics: EditParserMetrics, -} - -#[derive(Debug, PartialEq)] -enum XmlParserState { - Pending, - WithinOldText { start: bool, line_hint: Option }, - AfterOldText, - WithinNewText { start: bool }, -} - -#[derive(Debug)] -pub struct DiffFencedEditParser { - state: DiffParserState, - buffer: String, - metrics: EditParserMetrics, -} - -#[derive(Debug, PartialEq)] -enum DiffParserState { - Pending, - WithinSearch { start: bool, line_hint: Option }, - WithinReplace { start: bool }, -} - -/// Main parser that delegates to format-specific parsers -pub struct EditParser { - parser: Box, -} - -impl XmlEditParser { - pub fn new() -> Self { - XmlEditParser { - state: XmlParserState::Pending, - buffer: String::new(), - metrics: EditParserMetrics::default(), - } - } - - fn find_end_tag(&self) -> Option> { - let (tag, start_ix) = END_TAGS - .iter() - .flat_map(|tag| Some((tag, self.buffer.find(tag)?))) - .min_by_key(|(_, ix)| *ix)?; - Some(start_ix..start_ix + tag.len()) - } - - fn ends_with_tag_prefix(&self) -> bool { - let mut end_prefixes = END_TAGS - .iter() - .flat_map(|tag| (1..tag.len()).map(move |i| &tag[..i])) - .chain(["\n"]); - end_prefixes.any(|prefix| self.buffer.ends_with(&prefix)) - } - - fn parse_line_hint(&self, tag: &str) -> Option { - use std::sync::LazyLock; - static LINE_HINT_REGEX: LazyLock = - LazyLock::new(|| Regex::new(r#"line=(?:"?)(\d+)"#).unwrap()); - - LINE_HINT_REGEX - .captures(tag) - .and_then(|caps| caps.get(1)) - .and_then(|m| m.as_str().parse::().ok()) - } -} - -impl EditFormatParser for XmlEditParser { - fn push(&mut self, chunk: &str) -> SmallVec<[EditParserEvent; 1]> { - self.buffer.push_str(chunk); - - let mut edit_events = SmallVec::new(); - loop { - match &mut self.state { - XmlParserState::Pending => { - if let Some(start) = self.buffer.find("') { - let tag_end = start + tag_end + 1; - let tag = &self.buffer[start..tag_end]; - let line_hint = self.parse_line_hint(tag); - self.buffer.drain(..tag_end); - self.state = XmlParserState::WithinOldText { - start: true, - line_hint, - }; - } else { - break; - } - } else { - break; - } - } - XmlParserState::WithinOldText { start, line_hint } => { - if !self.buffer.is_empty() { - if *start && self.buffer.starts_with('\n') { - self.buffer.remove(0); - } - *start = false; - } - - let line_hint = *line_hint; - if let Some(tag_range) = self.find_end_tag() { - let mut chunk = self.buffer[..tag_range.start].to_string(); - if chunk.ends_with('\n') { - chunk.pop(); - } - - self.metrics.tags += 1; - if &self.buffer[tag_range.clone()] != OLD_TEXT_END_TAG { - self.metrics.mismatched_tags += 1; - } - - self.buffer.drain(..tag_range.end); - self.state = XmlParserState::AfterOldText; - edit_events.push(EditParserEvent::OldTextChunk { - chunk, - done: true, - line_hint, - }); - } else { - if !self.ends_with_tag_prefix() { - edit_events.push(EditParserEvent::OldTextChunk { - chunk: mem::take(&mut self.buffer), - done: false, - line_hint, - }); - } - break; - } - } - XmlParserState::AfterOldText => { - if let Some(start) = self.buffer.find("") { - self.buffer.drain(..start + "".len()); - self.state = XmlParserState::WithinNewText { start: true }; - } else { - break; - } - } - XmlParserState::WithinNewText { start } => { - if !self.buffer.is_empty() { - if *start && self.buffer.starts_with('\n') { - self.buffer.remove(0); - } - *start = false; - } - - if let Some(tag_range) = self.find_end_tag() { - let mut chunk = self.buffer[..tag_range.start].to_string(); - if chunk.ends_with('\n') { - chunk.pop(); - } - - self.metrics.tags += 1; - if &self.buffer[tag_range.clone()] != NEW_TEXT_END_TAG { - self.metrics.mismatched_tags += 1; - } - - self.buffer.drain(..tag_range.end); - self.state = XmlParserState::Pending; - edit_events.push(EditParserEvent::NewTextChunk { chunk, done: true }); - } else { - if !self.ends_with_tag_prefix() { - edit_events.push(EditParserEvent::NewTextChunk { - chunk: mem::take(&mut self.buffer), - done: false, - }); - } - break; - } - } - } - } - edit_events - } - - fn take_metrics(&mut self) -> EditParserMetrics { - std::mem::take(&mut self.metrics) - } -} - -impl DiffFencedEditParser { - pub fn new() -> Self { - DiffFencedEditParser { - state: DiffParserState::Pending, - buffer: String::new(), - metrics: EditParserMetrics::default(), - } - } - - fn ends_with_diff_marker_prefix(&self) -> bool { - let diff_markers = [SEPARATOR_MARKER, REPLACE_MARKER]; - let mut diff_prefixes = diff_markers - .iter() - .flat_map(|marker| (1..marker.len()).map(move |i| &marker[..i])) - .chain(["\n"]); - diff_prefixes.any(|prefix| self.buffer.ends_with(&prefix)) - } - - fn parse_line_hint(&self, search_line: &str) -> Option { - use regex::Regex; - use std::sync::LazyLock; - static LINE_HINT_REGEX: LazyLock = - LazyLock::new(|| Regex::new(r#"line=(?:"?)(\d+)"#).unwrap()); - - LINE_HINT_REGEX - .captures(search_line) - .and_then(|caps| caps.get(1)) - .and_then(|m| m.as_str().parse::().ok()) - } -} - -impl EditFormatParser for DiffFencedEditParser { - fn push(&mut self, chunk: &str) -> SmallVec<[EditParserEvent; 1]> { - self.buffer.push_str(chunk); - - let mut edit_events = SmallVec::new(); - loop { - match &mut self.state { - DiffParserState::Pending => { - if let Some(diff) = self.buffer.find(SEARCH_MARKER) { - let search_end = diff + SEARCH_MARKER.len(); - if let Some(newline_pos) = self.buffer[search_end..].find('\n') { - let search_line = &self.buffer[diff..search_end + newline_pos]; - let line_hint = self.parse_line_hint(search_line); - self.buffer.drain(..search_end + newline_pos + 1); - self.state = DiffParserState::WithinSearch { - start: true, - line_hint, - }; - } else { - break; - } - } else { - break; - } - } - DiffParserState::WithinSearch { start, line_hint } => { - if !self.buffer.is_empty() { - if *start && self.buffer.starts_with('\n') { - self.buffer.remove(0); - } - *start = false; - } - - let line_hint = *line_hint; - if let Some(separator_pos) = self.buffer.find(SEPARATOR_MARKER) { - let mut chunk = self.buffer[..separator_pos].to_string(); - if chunk.ends_with('\n') { - chunk.pop(); - } - - let separator_end = separator_pos + SEPARATOR_MARKER.len(); - if let Some(newline_pos) = self.buffer[separator_end..].find('\n') { - self.buffer.drain(..separator_end + newline_pos + 1); - self.state = DiffParserState::WithinReplace { start: true }; - edit_events.push(EditParserEvent::OldTextChunk { - chunk, - done: true, - line_hint, - }); - } else { - break; - } - } else { - if !self.ends_with_diff_marker_prefix() { - edit_events.push(EditParserEvent::OldTextChunk { - chunk: mem::take(&mut self.buffer), - done: false, - line_hint, - }); - } - break; - } - } - DiffParserState::WithinReplace { start } => { - if !self.buffer.is_empty() { - if *start && self.buffer.starts_with('\n') { - self.buffer.remove(0); - } - *start = false; - } - - if let Some(replace_pos) = self.buffer.find(REPLACE_MARKER) { - let mut chunk = self.buffer[..replace_pos].to_string(); - if chunk.ends_with('\n') { - chunk.pop(); - } - - self.buffer.drain(..replace_pos + REPLACE_MARKER.len()); - if let Some(newline_pos) = self.buffer.find('\n') { - self.buffer.drain(..newline_pos + 1); - } else { - self.buffer.clear(); - } - - self.state = DiffParserState::Pending; - edit_events.push(EditParserEvent::NewTextChunk { chunk, done: true }); - } else { - if !self.ends_with_diff_marker_prefix() { - edit_events.push(EditParserEvent::NewTextChunk { - chunk: mem::take(&mut self.buffer), - done: false, - }); - } - break; - } - } - } - } - edit_events - } - - fn take_metrics(&mut self) -> EditParserMetrics { - std::mem::take(&mut self.metrics) - } -} - -impl EditParser { - pub fn new(format: EditFormat) -> Self { - let parser: Box = match format { - EditFormat::XmlTags => Box::new(XmlEditParser::new()), - EditFormat::DiffFenced => Box::new(DiffFencedEditParser::new()), - }; - EditParser { parser } - } - - pub fn push(&mut self, chunk: &str) -> SmallVec<[EditParserEvent; 1]> { - self.parser.push(chunk) - } - - pub fn finish(mut self) -> EditParserMetrics { - self.parser.take_metrics() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use indoc::indoc; - use rand::prelude::*; - use std::cmp; - - #[gpui::test(iterations = 1000)] - fn test_xml_single_edit(mut rng: StdRng) { - let mut parser = EditParser::new(EditFormat::XmlTags); - assert_eq!( - parse_random_chunks( - "originalupdated", - &mut parser, - &mut rng - ), - vec![Edit { - old_text: "original".to_string(), - new_text: "updated".to_string(), - line_hint: None, - }] - ); - assert_eq!( - parser.finish(), - EditParserMetrics { - tags: 2, - mismatched_tags: 0 - } - ); - } - - #[gpui::test(iterations = 1000)] - fn test_xml_multiple_edits(mut rng: StdRng) { - let mut parser = EditParser::new(EditFormat::XmlTags); - assert_eq!( - parse_random_chunks( - indoc! {" - - first old - first new - second old - second new - - "}, - &mut parser, - &mut rng - ), - vec![ - Edit { - old_text: "first old".to_string(), - new_text: "first new".to_string(), - line_hint: None, - }, - Edit { - old_text: "second old".to_string(), - new_text: "second new".to_string(), - line_hint: None, - }, - ] - ); - assert_eq!( - parser.finish(), - EditParserMetrics { - tags: 4, - mismatched_tags: 0 - } - ); - } - - #[gpui::test(iterations = 1000)] - fn test_xml_edits_with_extra_text(mut rng: StdRng) { - let mut parser = EditParser::new(EditFormat::XmlTags); - assert_eq!( - parse_random_chunks( - indoc! {" - ignore this - contentextra stuffupdated contenttrailing data - more text second item - middle textmodified second itemend - third caseimproved third case with trailing text - "}, - &mut parser, - &mut rng - ), - vec![ - Edit { - old_text: "content".to_string(), - new_text: "updated content".to_string(), - line_hint: None, - }, - Edit { - old_text: "second item".to_string(), - new_text: "modified second item".to_string(), - line_hint: None, - }, - Edit { - old_text: "third case".to_string(), - new_text: "improved third case".to_string(), - line_hint: None, - }, - ] - ); - assert_eq!( - parser.finish(), - EditParserMetrics { - tags: 6, - mismatched_tags: 0 - } - ); - } - - #[gpui::test(iterations = 1000)] - fn test_xml_edits_with_closing_parameter_invoke(mut rng: StdRng) { - // This case is a regression with Claude Sonnet 4.5. - // Sometimes Sonnet thinks that it's doing a tool call - // and closes its response with '' - // instead of properly closing - - let mut parser = EditParser::new(EditFormat::XmlTags); - assert_eq!( - parse_random_chunks( - indoc! {" - some textupdated text - more textupd - "}, - &mut parser, - &mut rng - ), - vec![ - Edit { - old_text: "some text".to_string(), - new_text: "updated text".to_string(), - line_hint: None, - }, - Edit { - old_text: "more text".to_string(), - new_text: "upd".to_string(), - line_hint: None, - }, - ] - ); - assert_eq!( - parser.finish(), - EditParserMetrics { - tags: 4, - mismatched_tags: 2 - } - ); - } - - #[gpui::test(iterations = 1000)] - fn test_xml_nested_tags(mut rng: StdRng) { - let mut parser = EditParser::new(EditFormat::XmlTags); - assert_eq!( - parse_random_chunks( - "code with nested elementsnew content", - &mut parser, - &mut rng - ), - vec![Edit { - old_text: "code with nested elements".to_string(), - new_text: "new content".to_string(), - line_hint: None, - }] - ); - assert_eq!( - parser.finish(), - EditParserMetrics { - tags: 2, - mismatched_tags: 0 - } - ); - } - - #[gpui::test(iterations = 1000)] - fn test_xml_empty_old_and_new_text(mut rng: StdRng) { - let mut parser = EditParser::new(EditFormat::XmlTags); - assert_eq!( - parse_random_chunks( - "", - &mut parser, - &mut rng - ), - vec![Edit { - old_text: "".to_string(), - new_text: "".to_string(), - line_hint: None, - }] - ); - assert_eq!( - parser.finish(), - EditParserMetrics { - tags: 2, - mismatched_tags: 0 - } - ); - } - - #[gpui::test(iterations = 100)] - fn test_xml_multiline_content(mut rng: StdRng) { - let mut parser = EditParser::new(EditFormat::XmlTags); - assert_eq!( - parse_random_chunks( - "line1\nline2\nline3line1\nmodified line2\nline3", - &mut parser, - &mut rng - ), - vec![Edit { - old_text: "line1\nline2\nline3".to_string(), - new_text: "line1\nmodified line2\nline3".to_string(), - line_hint: None, - }] - ); - assert_eq!( - parser.finish(), - EditParserMetrics { - tags: 2, - mismatched_tags: 0 - } - ); - } - - #[gpui::test(iterations = 1000)] - fn test_xml_mismatched_tags(mut rng: StdRng) { - let mut parser = EditParser::new(EditFormat::XmlTags); - assert_eq!( - parse_random_chunks( - // Reduced from an actual Sonnet 3.7 output - indoc! {" - - a - b - c - - - a - B - c - - - d - e - f - - - D - e - F - - "}, - &mut parser, - &mut rng - ), - vec![ - Edit { - old_text: "a\nb\nc".to_string(), - new_text: "a\nB\nc".to_string(), - line_hint: None, - }, - Edit { - old_text: "d\ne\nf".to_string(), - new_text: "D\ne\nF".to_string(), - line_hint: None, - } - ] - ); - assert_eq!( - parser.finish(), - EditParserMetrics { - tags: 4, - mismatched_tags: 4 - } - ); - - let mut parser = EditParser::new(EditFormat::XmlTags); - assert_eq!( - parse_random_chunks( - // Reduced from an actual Opus 4 output - indoc! {" - - - Lorem - - - LOREM - - "}, - &mut parser, - &mut rng - ), - vec![Edit { - old_text: "Lorem".to_string(), - new_text: "LOREM".to_string(), - line_hint: None, - },] - ); - assert_eq!( - parser.finish(), - EditParserMetrics { - tags: 2, - mismatched_tags: 1 - } - ); - } - - #[gpui::test(iterations = 1000)] - fn test_diff_fenced_single_edit(mut rng: StdRng) { - let mut parser = EditParser::new(EditFormat::DiffFenced); - assert_eq!( - parse_random_chunks( - indoc! {" - <<<<<<< SEARCH - original text - ======= - updated text - >>>>>>> REPLACE - "}, - &mut parser, - &mut rng - ), - vec![Edit { - old_text: "original text".to_string(), - new_text: "updated text".to_string(), - line_hint: None, - }] - ); - assert_eq!( - parser.finish(), - EditParserMetrics { - tags: 0, - mismatched_tags: 0 - } - ); - } - - #[gpui::test(iterations = 100)] - fn test_diff_fenced_with_markdown_fences(mut rng: StdRng) { - let mut parser = EditParser::new(EditFormat::DiffFenced); - assert_eq!( - parse_random_chunks( - indoc! {" - ```diff - <<<<<<< SEARCH - from flask import Flask - ======= - import math - from flask import Flask - >>>>>>> REPLACE - ``` - "}, - &mut parser, - &mut rng - ), - vec![Edit { - old_text: "from flask import Flask".to_string(), - new_text: "import math\nfrom flask import Flask".to_string(), - line_hint: None, - }] - ); - assert_eq!( - parser.finish(), - EditParserMetrics { - tags: 0, - mismatched_tags: 0 - } - ); - } - - #[gpui::test(iterations = 100)] - fn test_diff_fenced_multiple_edits(mut rng: StdRng) { - let mut parser = EditParser::new(EditFormat::DiffFenced); - assert_eq!( - parse_random_chunks( - indoc! {" - <<<<<<< SEARCH - first old - ======= - first new - >>>>>>> REPLACE - - <<<<<<< SEARCH - second old - ======= - second new - >>>>>>> REPLACE - "}, - &mut parser, - &mut rng - ), - vec![ - Edit { - old_text: "first old".to_string(), - new_text: "first new".to_string(), - line_hint: None, - }, - Edit { - old_text: "second old".to_string(), - new_text: "second new".to_string(), - line_hint: None, - }, - ] - ); - assert_eq!( - parser.finish(), - EditParserMetrics { - tags: 0, - mismatched_tags: 0 - } - ); - } - - #[gpui::test(iterations = 100)] - fn test_mixed_formats(mut rng: StdRng) { - // Test XML format parser only parses XML tags - let mut xml_parser = EditParser::new(EditFormat::XmlTags); - assert_eq!( - parse_random_chunks( - indoc! {" - xml style oldxml style new - - <<<<<<< SEARCH - diff style old - ======= - diff style new - >>>>>>> REPLACE - "}, - &mut xml_parser, - &mut rng - ), - vec![Edit { - old_text: "xml style old".to_string(), - new_text: "xml style new".to_string(), - line_hint: None, - },] - ); - assert_eq!( - xml_parser.finish(), - EditParserMetrics { - tags: 2, - mismatched_tags: 0 - } - ); - - // Test diff-fenced format parser only parses diff markers - let mut diff_parser = EditParser::new(EditFormat::DiffFenced); - assert_eq!( - parse_random_chunks( - indoc! {" - xml style oldxml style new - - <<<<<<< SEARCH - diff style old - ======= - diff style new - >>>>>>> REPLACE - "}, - &mut diff_parser, - &mut rng - ), - vec![Edit { - old_text: "diff style old".to_string(), - new_text: "diff style new".to_string(), - line_hint: None, - },] - ); - assert_eq!( - diff_parser.finish(), - EditParserMetrics { - tags: 0, - mismatched_tags: 0 - } - ); - } - - #[gpui::test(iterations = 100)] - fn test_diff_fenced_empty_sections(mut rng: StdRng) { - let mut parser = EditParser::new(EditFormat::DiffFenced); - assert_eq!( - parse_random_chunks( - indoc! {" - <<<<<<< SEARCH - ======= - >>>>>>> REPLACE - "}, - &mut parser, - &mut rng - ), - vec![Edit { - old_text: "".to_string(), - new_text: "".to_string(), - line_hint: None, - }] - ); - assert_eq!( - parser.finish(), - EditParserMetrics { - tags: 0, - mismatched_tags: 0 - } - ); - } - - #[gpui::test(iterations = 100)] - fn test_diff_fenced_with_line_hint(mut rng: StdRng) { - let mut parser = EditParser::new(EditFormat::DiffFenced); - let edits = parse_random_chunks( - indoc! {" - <<<<<<< SEARCH line=42 - original text - ======= - updated text - >>>>>>> REPLACE - "}, - &mut parser, - &mut rng, - ); - assert_eq!( - edits, - vec![Edit { - old_text: "original text".to_string(), - line_hint: Some(42), - new_text: "updated text".to_string(), - }] - ); - } - #[gpui::test(iterations = 100)] - fn test_xml_line_hints(mut rng: StdRng) { - // Line hint is a single quoted line number - let mut parser = EditParser::new(EditFormat::XmlTags); - - let edits = parse_random_chunks( - r#" - original code - updated code"#, - &mut parser, - &mut rng, - ); - - assert_eq!(edits.len(), 1); - assert_eq!(edits[0].old_text, "original code"); - assert_eq!(edits[0].line_hint, Some(23)); - assert_eq!(edits[0].new_text, "updated code"); - - // Line hint is a single unquoted line number - let mut parser = EditParser::new(EditFormat::XmlTags); - - let edits = parse_random_chunks( - r#" - original code - updated code"#, - &mut parser, - &mut rng, - ); - - assert_eq!(edits.len(), 1); - assert_eq!(edits[0].old_text, "original code"); - assert_eq!(edits[0].line_hint, Some(45)); - assert_eq!(edits[0].new_text, "updated code"); - - // Line hint is a range - let mut parser = EditParser::new(EditFormat::XmlTags); - - let edits = parse_random_chunks( - r#" - original code - updated code"#, - &mut parser, - &mut rng, - ); - - assert_eq!(edits.len(), 1); - assert_eq!(edits[0].old_text, "original code"); - assert_eq!(edits[0].line_hint, Some(23)); - assert_eq!(edits[0].new_text, "updated code"); - - // No line hint - let mut parser = EditParser::new(EditFormat::XmlTags); - let edits = parse_random_chunks( - r#" - old - new"#, - &mut parser, - &mut rng, - ); - - assert_eq!(edits.len(), 1); - assert_eq!(edits[0].old_text, "old"); - assert_eq!(edits[0].line_hint, None); - assert_eq!(edits[0].new_text, "new"); - } - - #[derive(Default, Debug, PartialEq, Eq)] - struct Edit { - old_text: String, - new_text: String, - line_hint: Option, - } - - fn parse_random_chunks(input: &str, parser: &mut EditParser, rng: &mut StdRng) -> Vec { - let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50)); - let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count); - chunk_indices.sort(); - chunk_indices.push(input.len()); - - let mut old_text = Some(String::new()); - let mut new_text = None; - let mut pending_edit = Edit::default(); - let mut edits = Vec::new(); - let mut last_ix = 0; - for chunk_ix in chunk_indices { - for event in parser.push(&input[last_ix..chunk_ix]) { - match event { - EditParserEvent::OldTextChunk { - chunk, - done, - line_hint, - } => { - old_text.as_mut().unwrap().push_str(&chunk); - if done { - pending_edit.old_text = old_text.take().unwrap(); - pending_edit.line_hint = line_hint; - new_text = Some(String::new()); - } - } - EditParserEvent::NewTextChunk { chunk, done } => { - new_text.as_mut().unwrap().push_str(&chunk); - if done { - pending_edit.new_text = new_text.take().unwrap(); - edits.push(pending_edit); - pending_edit = Edit::default(); - old_text = Some(String::new()); - } - } - } - } - last_ix = chunk_ix; - } - - if new_text.is_some() { - pending_edit.new_text = new_text.take().unwrap(); - edits.push(pending_edit); - } - - edits - } -} diff --git a/crates/agent/src/edit_agent/evals.rs b/crates/agent/src/edit_agent/evals.rs deleted file mode 100644 index edf8a0f671..0000000000 --- a/crates/agent/src/edit_agent/evals.rs +++ /dev/null @@ -1,1684 +0,0 @@ -use super::*; -use crate::{ - EditFileMode, EditFileToolInput, GrepToolInput, ListDirectoryToolInput, ReadFileToolInput, -}; -use Role::*; -use client::{Client, UserStore}; -use eval_utils::{EvalOutput, EvalOutputProcessor, OutcomeKind}; -use fs::FakeFs; -use futures::{FutureExt, future::LocalBoxFuture}; -use gpui::{AppContext, TestAppContext, Timer}; -use http_client::StatusCode; -use indoc::{formatdoc, indoc}; -use language_model::{ - LanguageModelRegistry, LanguageModelToolResult, LanguageModelToolResultContent, - LanguageModelToolUse, LanguageModelToolUseId, SelectedModel, -}; -use project::Project; -use prompt_store::{ProjectContext, WorktreeContext}; -use rand::prelude::*; -use reqwest_client::ReqwestClient; -use serde_json::json; -use std::{ - fmt::{self, Display}, - path::Path, - str::FromStr, - time::Duration, -}; -use util::path; - -#[derive(Default, Clone, Debug)] -struct EditAgentOutputProcessor { - mismatched_tag_threshold: f32, - cumulative_tags: usize, - cumulative_mismatched_tags: usize, - eval_outputs: Vec>, -} - -fn mismatched_tag_threshold(mismatched_tag_threshold: f32) -> EditAgentOutputProcessor { - EditAgentOutputProcessor { - mismatched_tag_threshold, - cumulative_tags: 0, - cumulative_mismatched_tags: 0, - eval_outputs: Vec::new(), - } -} - -#[derive(Clone, Debug)] -struct EditEvalMetadata { - tags: usize, - mismatched_tags: usize, -} - -impl EvalOutputProcessor for EditAgentOutputProcessor { - type Metadata = EditEvalMetadata; - - fn process(&mut self, output: &EvalOutput) { - if matches!(output.outcome, OutcomeKind::Passed | OutcomeKind::Failed) { - self.cumulative_mismatched_tags += output.metadata.mismatched_tags; - self.cumulative_tags += output.metadata.tags; - self.eval_outputs.push(output.clone()); - } - } - - fn assert(&mut self) { - let mismatched_tag_ratio = - self.cumulative_mismatched_tags as f32 / self.cumulative_tags as f32; - if mismatched_tag_ratio > self.mismatched_tag_threshold { - for eval_output in &self.eval_outputs { - println!("{}", eval_output.data); - } - panic!( - "Too many mismatched tags: {:?}", - self.cumulative_mismatched_tags - ); - } - } -} - -#[test] -#[cfg_attr(not(feature = "unit-eval"), ignore)] -fn eval_extract_handle_command_output() { - // Test how well agent generates multiple edit hunks. - // - // Model | Pass rate - // ----------------------------|---------- - // claude-3.7-sonnet | 0.99 (2025-06-14) - // claude-sonnet-4 | 0.97 (2025-06-14) - // gemini-2.5-pro-06-05 | 0.98 (2025-06-16) - // gemini-2.5-flash | 0.11 (2025-05-22) - // gpt-4.1 | 1.00 (2025-05-22) - - let input_file_path = "root/blame.rs"; - let input_file_content = include_str!("evals/fixtures/extract_handle_command_output/before.rs"); - let possible_diffs = vec![ - include_str!("evals/fixtures/extract_handle_command_output/possible-01.diff"), - include_str!("evals/fixtures/extract_handle_command_output/possible-02.diff"), - include_str!("evals/fixtures/extract_handle_command_output/possible-03.diff"), - include_str!("evals/fixtures/extract_handle_command_output/possible-04.diff"), - include_str!("evals/fixtures/extract_handle_command_output/possible-05.diff"), - include_str!("evals/fixtures/extract_handle_command_output/possible-06.diff"), - include_str!("evals/fixtures/extract_handle_command_output/possible-07.diff"), - ]; - let edit_description = "Extract `handle_command_output` method from `run_git_blame`."; - eval_utils::eval(100, 0.95, mismatched_tag_threshold(0.05), move || { - run_eval(EvalInput::from_conversation( - vec![ - message( - User, - [text(formatdoc! {" - Read the `{input_file_path}` file and extract a method in - the final stanza of `run_git_blame` to deal with command failures, - call it `handle_command_output` and take the std::process::Output as the only parameter. - Do not document the method and do not add any comments. - - Add it right next to `run_git_blame` and copy it verbatim from `run_git_blame`. - "})], - ), - message( - Assistant, - [tool_use( - "tool_1", - "read_file", - ReadFileToolInput { - path: input_file_path.into(), - start_line: None, - end_line: None, - }, - )], - ), - message( - User, - [tool_result("tool_1", "read_file", input_file_content)], - ), - message( - Assistant, - [tool_use( - "tool_2", - "edit_file", - EditFileToolInput { - display_description: edit_description.into(), - path: input_file_path.into(), - mode: EditFileMode::Edit, - }, - )], - ), - ], - Some(input_file_content.into()), - EvalAssertion::assert_diff_any(possible_diffs.clone()), - )) - }); -} - -#[test] -#[cfg_attr(not(feature = "unit-eval"), ignore)] -fn eval_delete_run_git_blame() { - // Model | Pass rate - // ----------------------------|---------- - // claude-3.7-sonnet | 1.0 (2025-06-14) - // claude-sonnet-4 | 0.96 (2025-06-14) - // gemini-2.5-pro-06-05 | 1.0 (2025-06-16) - // gemini-2.5-flash | - // gpt-4.1 | - - let input_file_path = "root/blame.rs"; - let input_file_content = include_str!("evals/fixtures/delete_run_git_blame/before.rs"); - let output_file_content = include_str!("evals/fixtures/delete_run_git_blame/after.rs"); - let edit_description = "Delete the `run_git_blame` function."; - - eval_utils::eval(100, 0.95, mismatched_tag_threshold(0.05), move || { - run_eval(EvalInput::from_conversation( - vec![ - message( - User, - [text(formatdoc! {" - Read the `{input_file_path}` file and delete `run_git_blame`. Just that - one function, not its usages. - "})], - ), - message( - Assistant, - [tool_use( - "tool_1", - "read_file", - ReadFileToolInput { - path: input_file_path.into(), - start_line: None, - end_line: None, - }, - )], - ), - message( - User, - [tool_result("tool_1", "read_file", input_file_content)], - ), - message( - Assistant, - [tool_use( - "tool_2", - "edit_file", - EditFileToolInput { - display_description: edit_description.into(), - path: input_file_path.into(), - mode: EditFileMode::Edit, - }, - )], - ), - ], - Some(input_file_content.into()), - EvalAssertion::assert_eq(output_file_content), - )) - }); -} - -#[test] -#[cfg_attr(not(feature = "unit-eval"), ignore)] -fn eval_translate_doc_comments() { - // Model | Pass rate - // ============================================ - // - // claude-3.7-sonnet | 1.0 (2025-06-14) - // claude-sonnet-4 | 1.0 (2025-06-14) - // gemini-2.5-pro-preview-03-25 | 1.0 (2025-05-22) - // gemini-2.5-flash-preview-04-17 | - // gpt-4.1 | - - let input_file_path = "root/canvas.rs"; - let input_file_content = include_str!("evals/fixtures/translate_doc_comments/before.rs"); - let edit_description = "Translate all doc comments to Italian"; - - eval_utils::eval(200, 1., mismatched_tag_threshold(0.05), move || { - run_eval(EvalInput::from_conversation( - vec![ - message( - User, - [text(formatdoc! {" - Read the {input_file_path} file and edit it (without overwriting it), - translating all the doc comments to italian. - "})], - ), - message( - Assistant, - [tool_use( - "tool_1", - "read_file", - ReadFileToolInput { - path: input_file_path.into(), - start_line: None, - end_line: None, - }, - )], - ), - message( - User, - [tool_result("tool_1", "read_file", input_file_content)], - ), - message( - Assistant, - [tool_use( - "tool_2", - "edit_file", - EditFileToolInput { - display_description: edit_description.into(), - path: input_file_path.into(), - mode: EditFileMode::Edit, - }, - )], - ), - ], - Some(input_file_content.into()), - EvalAssertion::judge_diff("Doc comments were translated to Italian"), - )) - }); -} - -#[test] -#[cfg_attr(not(feature = "unit-eval"), ignore)] -fn eval_use_wasi_sdk_in_compile_parser_to_wasm() { - // Model | Pass rate - // ============================================ - // - // claude-3.7-sonnet | 0.96 (2025-06-14) - // claude-sonnet-4 | 0.11 (2025-06-14) - // gemini-2.5-pro-preview-latest | 0.99 (2025-06-16) - // gemini-2.5-flash-preview-04-17 | - // gpt-4.1 | - - let input_file_path = "root/lib.rs"; - let input_file_content = - include_str!("evals/fixtures/use_wasi_sdk_in_compile_parser_to_wasm/before.rs"); - let edit_description = "Update compile_parser_to_wasm to use wasi-sdk instead of emscripten"; - - eval_utils::eval(100, 0.95, mismatched_tag_threshold(0.05), move || { - run_eval(EvalInput::from_conversation( - vec![ - message( - User, - [text(formatdoc! {" - Read the `{input_file_path}` file and change `compile_parser_to_wasm` to use `wasi-sdk` instead of emscripten. - Use `ureq` to download the SDK for the current platform and architecture. - Extract the archive into a sibling of `lib` inside the `tree-sitter` directory in the cache_dir. - Compile the parser to wasm using the `bin/clang` executable (or `bin/clang.exe` on windows) - that's inside of the archive. - Don't re-download the SDK if that executable already exists. - - Use these clang flags: -fPIC -shared -Os -Wl,--export=tree_sitter_{{language_name}} - - Here are the available wasi-sdk assets: - - wasi-sdk-25.0-x86_64-macos.tar.gz - - wasi-sdk-25.0-arm64-macos.tar.gz - - wasi-sdk-25.0-x86_64-linux.tar.gz - - wasi-sdk-25.0-arm64-linux.tar.gz - - wasi-sdk-25.0-x86_64-linux.tar.gz - - wasi-sdk-25.0-arm64-linux.tar.gz - - wasi-sdk-25.0-x86_64-windows.tar.gz - "})], - ), - message( - Assistant, - [tool_use( - "tool_1", - "read_file", - ReadFileToolInput { - path: input_file_path.into(), - start_line: Some(971), - end_line: Some(1050), - }, - )], - ), - message( - User, - [tool_result( - "tool_1", - "read_file", - lines(input_file_content, 971..1050), - )], - ), - message( - Assistant, - [tool_use( - "tool_2", - "read_file", - ReadFileToolInput { - path: input_file_path.into(), - start_line: Some(1050), - end_line: Some(1100), - }, - )], - ), - message( - User, - [tool_result( - "tool_2", - "read_file", - lines(input_file_content, 1050..1100), - )], - ), - message( - Assistant, - [tool_use( - "tool_3", - "read_file", - ReadFileToolInput { - path: input_file_path.into(), - start_line: Some(1100), - end_line: Some(1150), - }, - )], - ), - message( - User, - [tool_result( - "tool_3", - "read_file", - lines(input_file_content, 1100..1150), - )], - ), - message( - Assistant, - [tool_use( - "tool_4", - "edit_file", - EditFileToolInput { - display_description: edit_description.into(), - path: input_file_path.into(), - mode: EditFileMode::Edit, - }, - )], - ), - ], - Some(input_file_content.into()), - EvalAssertion::judge_diff(indoc! {" - - The compile_parser_to_wasm method has been changed to use wasi-sdk - - ureq is used to download the SDK for current platform and architecture - "}), - )) - }); -} - -#[test] -#[cfg_attr(not(feature = "unit-eval"), ignore)] -fn eval_disable_cursor_blinking() { - // Model | Pass rate - // ============================================ - // - // claude-3.7-sonnet | 0.59 (2025-07-14) - // claude-sonnet-4 | 0.81 (2025-07-14) - // gemini-2.5-pro | 0.95 (2025-07-14) - // gemini-2.5-flash-preview-04-17 | 0.78 (2025-07-14) - // gpt-4.1 | 0.00 (2025-07-14) (follows edit_description too literally) - - let input_file_path = "root/editor.rs"; - let input_file_content = include_str!("evals/fixtures/disable_cursor_blinking/before.rs"); - let edit_description = "Comment out the call to `BlinkManager::enable`"; - let possible_diffs = vec![ - include_str!("evals/fixtures/disable_cursor_blinking/possible-01.diff"), - include_str!("evals/fixtures/disable_cursor_blinking/possible-02.diff"), - include_str!("evals/fixtures/disable_cursor_blinking/possible-03.diff"), - include_str!("evals/fixtures/disable_cursor_blinking/possible-04.diff"), - ]; - eval_utils::eval(100, 0.51, mismatched_tag_threshold(0.05), move || { - run_eval(EvalInput::from_conversation( - vec![ - message(User, [text("Let's research how to cursor blinking works.")]), - message( - Assistant, - [tool_use( - "tool_1", - "grep", - GrepToolInput { - regex: "blink".into(), - include_pattern: None, - offset: 0, - case_sensitive: false, - }, - )], - ), - message( - User, - [tool_result( - "tool_1", - "grep", - [ - lines(input_file_content, 100..400), - lines(input_file_content, 800..1300), - lines(input_file_content, 1600..2000), - lines(input_file_content, 5000..5500), - lines(input_file_content, 8000..9000), - lines(input_file_content, 18455..18470), - lines(input_file_content, 20000..20500), - lines(input_file_content, 21000..21300), - ] - .join("Match found:\n\n"), - )], - ), - message( - User, - [text(indoc! {" - Comment out the lines that interact with the BlinkManager. - Keep the outer `update` blocks, but comments everything that's inside (including if statements). - Don't add additional comments. - "})], - ), - message( - Assistant, - [tool_use( - "tool_4", - "edit_file", - EditFileToolInput { - display_description: edit_description.into(), - path: input_file_path.into(), - mode: EditFileMode::Edit, - }, - )], - ), - ], - Some(input_file_content.into()), - EvalAssertion::assert_diff_any(possible_diffs.clone()), - )) - }); -} - -#[test] -#[cfg_attr(not(feature = "unit-eval"), ignore)] -fn eval_from_pixels_constructor() { - // Results for 2025-06-13 - // - // The outcome of this evaluation depends heavily on the LINE_HINT_TOLERANCE - // value. Higher values improve the pass rate but may sometimes cause - // edits to be misapplied. In the context of this eval, this means - // the agent might add from_pixels tests in incorrect locations - // (e.g., at the beginning of the file), yet the evaluation may still - // rate it highly. - // - // Model | Date | Pass rate - // ========================================================= - // claude-4.0-sonnet | 2025-06-14 | 0.99 - // claude-3.7-sonnet | 2025-06-14 | 0.88 - // gemini-2.5-pro-preview-06-05 | 2025-06-16 | 0.98 - // gpt-4.1 | - - let input_file_path = "root/canvas.rs"; - let input_file_content = include_str!("evals/fixtures/from_pixels_constructor/before.rs"); - let edit_description = "Implement from_pixels constructor and add tests."; - - eval_utils::eval(100, 0.95, mismatched_tag_threshold(0.25), move || { - run_eval(EvalInput::from_conversation( - vec![ - message( - User, - [text(indoc! {" - Introduce a new `from_pixels` constructor in Canvas and - also add tests for it in the same file. - "})], - ), - message( - Assistant, - [tool_use( - "tool_1", - "read_file", - ReadFileToolInput { - path: input_file_path.into(), - start_line: None, - end_line: None, - }, - )], - ), - message( - User, - [tool_result("tool_1", "read_file", input_file_content)], - ), - message( - Assistant, - [tool_use( - "tool_2", - "grep", - GrepToolInput { - regex: "mod\\s+tests".into(), - include_pattern: Some("font-kit/src/canvas.rs".into()), - offset: 0, - case_sensitive: false, - }, - )], - ), - message(User, [tool_result("tool_2", "grep", "No matches found")]), - message( - Assistant, - [tool_use( - "tool_3", - "grep", - GrepToolInput { - regex: "mod\\s+tests".into(), - include_pattern: Some("font-kit/src/**/*.rs".into()), - offset: 0, - case_sensitive: false, - }, - )], - ), - message(User, [tool_result("tool_3", "grep", "No matches found")]), - message( - Assistant, - [tool_use( - "tool_4", - "grep", - GrepToolInput { - regex: "#\\[test\\]".into(), - include_pattern: Some("font-kit/src/**/*.rs".into()), - offset: 0, - case_sensitive: false, - }, - )], - ), - message( - User, - [tool_result( - "tool_4", - "grep", - indoc! {" - Found 6 matches: - - ## Matches in font-kit/src/loaders/core_text.rs - - ### mod test › L926-936 - ``` - mod test { - use super::Font; - use crate::properties::{Stretch, Weight}; - - #[cfg(feature = \"source\")] - use crate::source::SystemSource; - - static TEST_FONT_POSTSCRIPT_NAME: &'static str = \"ArialMT\"; - - #[cfg(feature = \"source\")] - #[test] - ``` - - 55 lines remaining in ancestor node. Read the file to see all. - - ### mod test › L947-951 - ``` - } - - #[test] - fn test_core_text_to_css_font_weight() { - // Exact matches - ``` - - ### mod test › L959-963 - ``` - } - - #[test] - fn test_core_text_to_css_font_stretch() { - // Exact matches - ``` - - ## Matches in font-kit/src/loaders/freetype.rs - - ### mod test › L1238-1248 - ``` - mod test { - use crate::loaders::freetype::Font; - - static PCF_FONT_PATH: &str = \"resources/tests/times-roman-pcf/timR12.pcf\"; - static PCF_FONT_POSTSCRIPT_NAME: &str = \"Times-Roman\"; - - #[test] - fn get_pcf_postscript_name() { - let font = Font::from_path(PCF_FONT_PATH, 0).unwrap(); - assert_eq!(font.postscript_name().unwrap(), PCF_FONT_POSTSCRIPT_NAME); - } - ``` - - 1 lines remaining in ancestor node. Read the file to see all. - - ## Matches in font-kit/src/sources/core_text.rs - - ### mod test › L265-275 - ``` - mod test { - use crate::properties::{Stretch, Weight}; - - #[test] - fn test_css_to_core_text_font_weight() { - // Exact matches - assert_eq!(super::css_to_core_text_font_weight(Weight(100.0)), -0.7); - assert_eq!(super::css_to_core_text_font_weight(Weight(400.0)), 0.0); - assert_eq!(super::css_to_core_text_font_weight(Weight(700.0)), 0.4); - assert_eq!(super::css_to_core_text_font_weight(Weight(900.0)), 0.8); - - ``` - - 27 lines remaining in ancestor node. Read the file to see all. - - ### mod test › L278-282 - ``` - } - - #[test] - fn test_css_to_core_text_font_stretch() { - // Exact matches - ``` - "}, - )], - ), - message( - Assistant, - [tool_use( - "tool_5", - "edit_file", - EditFileToolInput { - display_description: edit_description.into(), - path: input_file_path.into(), - mode: EditFileMode::Edit, - }, - )], - ), - ], - Some(input_file_content.into()), - EvalAssertion::judge_diff(indoc! {" - - The diff contains a new `from_pixels` constructor - - The diff contains new tests for the `from_pixels` constructor - "}), - )) - }); -} - -#[test] -#[cfg_attr(not(feature = "unit-eval"), ignore)] -fn eval_zode() { - // Model | Pass rate - // ============================================ - // - // claude-3.7-sonnet | 1.0 (2025-06-14) - // claude-sonnet-4 | 1.0 (2025-06-14) - // gemini-2.5-pro-preview-03-25 | 1.0 (2025-05-22) - // gemini-2.5-flash-preview-04-17 | 1.0 (2025-05-22) - // gpt-4.1 | 1.0 (2025-05-22) - - let input_file_path = "root/zode.py"; - let input_content = None; - let edit_description = "Create the main Zode CLI script"; - - eval_utils::eval(50, 1., mismatched_tag_threshold(0.05), move || { - run_eval(EvalInput::from_conversation( - vec![ - message(User, [text(include_str!("evals/fixtures/zode/prompt.md"))]), - message( - Assistant, - [ - tool_use( - "tool_1", - "read_file", - ReadFileToolInput { - path: "root/eval/react.py".into(), - start_line: None, - end_line: None, - }, - ), - tool_use( - "tool_2", - "read_file", - ReadFileToolInput { - path: "root/eval/react_test.py".into(), - start_line: None, - end_line: None, - }, - ), - ], - ), - message( - User, - [ - tool_result( - "tool_1", - "read_file", - include_str!("evals/fixtures/zode/react.py"), - ), - tool_result( - "tool_2", - "read_file", - include_str!("evals/fixtures/zode/react_test.py"), - ), - ], - ), - message( - Assistant, - [ - text( - "Now that I understand what we need to build, I'll create the main Python script:", - ), - tool_use( - "tool_3", - "edit_file", - EditFileToolInput { - display_description: edit_description.into(), - path: input_file_path.into(), - mode: EditFileMode::Create, - }, - ), - ], - ), - ], - input_content.clone(), - EvalAssertion::new(async move |sample, _, _cx| { - let invalid_starts = [' ', '`', '\n']; - let mut message = String::new(); - for start in invalid_starts { - if sample.text_after.starts_with(start) { - message.push_str(&format!("The sample starts with a {:?}\n", start)); - break; - } - } - // Remove trailing newline. - message.pop(); - - if message.is_empty() { - Ok(EvalAssertionOutcome { - score: 100, - message: None, - }) - } else { - Ok(EvalAssertionOutcome { - score: 0, - message: Some(message), - }) - } - }), - )) - }); -} - -#[test] -#[cfg_attr(not(feature = "unit-eval"), ignore)] -fn eval_add_overwrite_test() { - // Model | Pass rate - // ============================================ - // - // claude-3.7-sonnet | 0.65 (2025-06-14) - // claude-sonnet-4 | 0.07 (2025-06-14) - // gemini-2.5-pro-preview-03-25 | 0.35 (2025-05-22) - // gemini-2.5-flash-preview-04-17 | - // gpt-4.1 | - - let input_file_path = "root/action_log.rs"; - let input_file_content = include_str!("evals/fixtures/add_overwrite_test/before.rs"); - let edit_description = "Add a new test for overwriting a file in action_log.rs"; - - eval_utils::eval(200, 0.5, mismatched_tag_threshold(0.05), move || { - run_eval(EvalInput::from_conversation( - vec![ - message( - User, - [text(indoc! {" - Introduce a new test in `action_log.rs` to test overwriting a file. - That is, a file already exists, but we call `buffer_created` as if the file were new. - Take inspiration from all the other tests in the file. - "})], - ), - message( - Assistant, - [tool_use( - "tool_1", - "read_file", - ReadFileToolInput { - path: input_file_path.into(), - start_line: None, - end_line: None, - }, - )], - ), - message( - User, - [tool_result( - "tool_1", - "read_file", - indoc! {" - pub struct ActionLog [L13-20] - tracked_buffers [L15] - edited_since_project_diagnostics_check [L17] - project [L19] - impl ActionLog [L22-498] - pub fn new [L24-30] - pub fn project [L32-34] - pub fn checked_project_diagnostics [L37-39] - pub fn has_edited_files_since_project_diagnostics_check [L42-44] - fn track_buffer_internal [L46-101] - fn handle_buffer_event [L103-116] - fn handle_buffer_edited [L118-123] - fn handle_buffer_file_changed [L125-158] - async fn maintain_diff [L160-264] - pub fn buffer_read [L267-269] - pub fn buffer_created [L272-276] - pub fn buffer_edited [L279-287] - pub fn will_delete_buffer [L289-304] - pub fn keep_edits_in_range [L306-364] - pub fn reject_edits_in_ranges [L366-459] - pub fn keep_all_edits [L461-473] - pub fn changed_buffers [L476-482] - pub fn stale_buffers [L485-497] - fn apply_non_conflicting_edits [L500-561] - fn diff_snapshots [L563-585] - fn point_to_row_edit [L587-614] - enum ChangeAuthor [L617-620] - User [L618] - Agent [L619] - enum TrackedBufferStatus [L623-627] - Created [L624] - Modified [L625] - Deleted [L626] - struct TrackedBuffer [L629-641] - buffer [L630] - base_text [L631] - unreviewed_changes [L632] - status [L633] - version [L634] - diff [L635] - snapshot [L636] - diff_update [L637] - _open_lsp_handle [L638] - _maintain_diff [L639] - _subscription [L640] - impl TrackedBuffer [L643-657] - fn has_changes [L644-650] - fn schedule_diff_update [L652-656] - pub struct ChangedBuffer [L659-661] - pub diff [L660] - mod tests [L664-1574] - fn init_logger [L678-682] - fn init_test [L684-691] - async fn test_keep_edits [L694-769] - async fn test_deletions [L772-854] - async fn test_overlapping_user_edits [L857-951] - async fn test_creating_files [L954-1010] - async fn test_deleting_files [L1013-1120] - async fn test_reject_edits [L1123-1255] - async fn test_reject_multiple_edits [L1258-1331] - async fn test_reject_deleted_file [L1334-1388] - async fn test_reject_created_file [L1391-1443] - async fn test_random_diffs [L1446-1535] - fn quiesce [L1510-1534] - struct HunkStatus [L1538-1542] - range [L1539] - diff_status [L1540] - old_text [L1541] - fn unreviewed_hunks [L1544-1573] - - Showing symbols 1-69 (total symbols: 69) - - Using the line numbers in this outline, you can call this tool again while specifying - the start_line and end_line fields to see the implementations of symbols in the outline. - "}, - )], - ), - message( - Assistant, - [ - text( - "Let's look at the existing test cases that are related to file creation to understand the pattern:", - ), - tool_use( - "tool_2", - "read_file", - ReadFileToolInput { - path: input_file_path.into(), - start_line: Some(953), - end_line: Some(1010), - }, - ), - ], - ), - message( - User, - [tool_result( - "tool_2", - "read_file", - lines(input_file_content, 953..1010), - )], - ), - message( - Assistant, - [ - text( - "Now let's look at another relevant test to understand how they're structured:", - ), - tool_use( - "tool_3", - "read_file", - ReadFileToolInput { - path: input_file_path.into(), - start_line: Some(1012), - end_line: Some(1120), - }, - ), - ], - ), - message( - User, - [tool_result( - "tool_3", - "read_file", - lines(input_file_content, 1012..1120), - )], - ), - message( - Assistant, - [ - text("Now let's look at how `buffer_created` is implemented:"), - tool_use( - "tool_4", - "read_file", - ReadFileToolInput { - path: input_file_path.into(), - start_line: Some(271), - end_line: Some(276), - }, - ), - ], - ), - message( - User, - [tool_result( - "tool_4", - "read_file", - lines(input_file_content, 271..276), - )], - ), - message( - Assistant, - [ - text(indoc! {" - Based on my understanding of the existing tests and code, I'll now create a new test for overwriting a file. In this test, we'll: - - 1. Create a file - 2. Open a buffer for it - 3. Track it in the action log - 4. Modify the buffer - 5. Call `buffer_created` again as if the file were new - 6. Check that the changes are properly tracked - - Let's write the test: - "}), - tool_use( - "tool_5", - "edit_file", - EditFileToolInput { - display_description: edit_description.into(), - path: input_file_path.into(), - mode: EditFileMode::Edit, - }, - ), - ], - ), - ], - Some(input_file_content.into()), - EvalAssertion::judge_diff( - "A new test for overwritten files was created, without changing any previous test", - ), - )) - }); -} - -#[test] -#[cfg_attr(not(feature = "unit-eval"), ignore)] -fn eval_create_empty_file() { - // Check that Edit Agent can create a file without writing its - // thoughts into it. This issue is not specific to empty files, but - // it's easier to reproduce with them. - // - // Model | Pass rate - // ============================================ - // - // claude-3.7-sonnet | 1.00 (2025-06-14) - // claude-sonnet-4 | 1.00 (2025-06-14) - // gemini-2.5-pro-preview-03-25 | 1.00 (2025-05-21) - // gemini-2.5-flash-preview-04-17 | 1.00 (2025-05-21) - // gpt-4.1 | 1.00 (2025-05-21) - // - // - // TODO: gpt-4.1-mini errored 38 times: - // "data did not match any variant of untagged enum ResponseStreamResult" - - let input_file_content = None; - let expected_output_content = String::new(); - - eval_utils::eval(100, 0.99, mismatched_tag_threshold(0.05), move || { - run_eval(EvalInput::from_conversation( - vec![ - message(User, [text("Create a second empty todo file ")]), - message( - Assistant, - [ - text(formatdoc! {" - I'll help you create a second empty todo file. - First, let me examine the project structure to see if there's already a todo file, which will help me determine the appropriate name and location for the second one. - "}), - tool_use( - "toolu_01GAF8TtsgpjKxCr8fgQLDgR", - "list_directory", - ListDirectoryToolInput { - path: "root".to_string(), - }, - ), - ], - ), - message( - User, - [tool_result( - "toolu_01GAF8TtsgpjKxCr8fgQLDgR", - "list_directory", - "root/TODO\nroot/TODO2\nroot/new.txt\n", - )], - ), - message( - Assistant, - [ - text(formatdoc! {" - I can see there's already a `TODO` file in the `root` directory. Let me create a second empty todo file called `TODO3` in the same directory: - "}), - tool_use( - "toolu_01Tb3iQ9griqSYMmVuykQPWU", - "edit_file", - EditFileToolInput { - display_description: "Create empty TODO3 file".to_string(), - mode: EditFileMode::Create, - path: "root/TODO3".into(), - }, - ), - ], - ), - ], - input_file_content.clone(), - // Bad behavior is to write something like - // "I'll create an empty TODO3 file as requested." - EvalAssertion::assert_eq(expected_output_content.clone()), - )) - }); -} - -fn message( - role: Role, - contents: impl IntoIterator, -) -> LanguageModelRequestMessage { - LanguageModelRequestMessage { - role, - content: contents.into_iter().collect(), - cache: false, - reasoning_details: None, - } -} - -fn text(text: impl Into) -> MessageContent { - MessageContent::Text(text.into()) -} - -fn lines(input: &str, range: Range) -> String { - input - .lines() - .skip(range.start) - .take(range.len()) - .collect::>() - .join("\n") -} - -fn tool_use( - id: impl Into>, - name: impl Into>, - input: impl Serialize, -) -> MessageContent { - MessageContent::ToolUse(LanguageModelToolUse { - id: LanguageModelToolUseId::from(id.into()), - name: name.into(), - raw_input: serde_json::to_string_pretty(&input).unwrap(), - input: serde_json::to_value(input).unwrap(), - is_input_complete: true, - thought_signature: None, - }) -} - -fn tool_result( - id: impl Into>, - name: impl Into>, - result: impl Into>, -) -> MessageContent { - MessageContent::ToolResult(LanguageModelToolResult { - tool_use_id: LanguageModelToolUseId::from(id.into()), - tool_name: name.into(), - is_error: false, - content: LanguageModelToolResultContent::Text(result.into()), - output: None, - }) -} - -#[derive(Clone)] -struct EvalInput { - conversation: Vec, - edit_file_input: EditFileToolInput, - input_content: Option, - assertion: EvalAssertion, -} - -impl EvalInput { - fn from_conversation( - conversation: Vec, - input_content: Option, - assertion: EvalAssertion, - ) -> Self { - let msg = conversation.last().expect("Conversation must not be empty"); - if msg.role != Role::Assistant { - panic!("Conversation must end with an assistant message"); - } - let tool_use = msg - .content - .iter() - .flat_map(|content| match content { - MessageContent::ToolUse(tool_use) if tool_use.name == "edit_file".into() => { - Some(tool_use) - } - _ => None, - }) - .next() - .expect("Conversation must end with an edit_file tool use") - .clone(); - - let edit_file_input: EditFileToolInput = serde_json::from_value(tool_use.input).unwrap(); - - EvalInput { - conversation, - edit_file_input, - input_content, - assertion, - } - } -} - -#[derive(Clone)] -struct EvalSample { - text_before: String, - text_after: String, - edit_output: EditAgentOutput, - diff: String, -} - -trait AssertionFn: 'static + Send + Sync { - fn assert<'a>( - &'a self, - sample: &'a EvalSample, - judge_model: Arc, - cx: &'a mut TestAppContext, - ) -> LocalBoxFuture<'a, Result>; -} - -impl AssertionFn for F -where - F: 'static - + Send - + Sync - + AsyncFn( - &EvalSample, - Arc, - &mut TestAppContext, - ) -> Result, -{ - fn assert<'a>( - &'a self, - sample: &'a EvalSample, - judge_model: Arc, - cx: &'a mut TestAppContext, - ) -> LocalBoxFuture<'a, Result> { - (self)(sample, judge_model, cx).boxed_local() - } -} - -#[derive(Clone)] -struct EvalAssertion(Arc); - -impl EvalAssertion { - fn new(f: F) -> Self - where - F: 'static - + Send - + Sync - + AsyncFn( - &EvalSample, - Arc, - &mut TestAppContext, - ) -> Result, - { - EvalAssertion(Arc::new(f)) - } - - fn assert_eq(expected: impl Into) -> Self { - let expected = expected.into(); - Self::new(async move |sample, _judge, _cx| { - Ok(EvalAssertionOutcome { - score: if strip_empty_lines(&sample.text_after) == strip_empty_lines(&expected) { - 100 - } else { - 0 - }, - message: None, - }) - }) - } - - fn assert_diff_any(expected_diffs: Vec>) -> Self { - let expected_diffs: Vec = expected_diffs.into_iter().map(Into::into).collect(); - Self::new(async move |sample, _judge, _cx| { - let matches = expected_diffs.iter().any(|possible_diff| { - let expected = - language::apply_diff_patch(&sample.text_before, possible_diff).unwrap(); - strip_empty_lines(&expected) == strip_empty_lines(&sample.text_after) - }); - - Ok(EvalAssertionOutcome { - score: if matches { 100 } else { 0 }, - message: None, - }) - }) - } - - fn judge_diff(assertions: &'static str) -> Self { - Self::new(async move |sample, judge, cx| { - let prompt = DiffJudgeTemplate { - diff: sample.diff.clone(), - assertions, - } - .render(&Templates::new()) - .unwrap(); - - let request = LanguageModelRequest { - messages: vec![LanguageModelRequestMessage { - role: Role::User, - content: vec![prompt.into()], - cache: false, - reasoning_details: None, - }], - thinking_allowed: true, - ..Default::default() - }; - let mut response = retry_on_rate_limit(async || { - Ok(judge - .stream_completion_text(request.clone(), &cx.to_async()) - .await?) - }) - .await?; - let mut output = String::new(); - while let Some(chunk) = response.stream.next().await { - let chunk = chunk?; - output.push_str(&chunk); - } - - // Parse the score from the response - let re = regex::Regex::new(r"(\d+)").unwrap(); - if let Some(captures) = re.captures(&output) - && let Some(score_match) = captures.get(1) - { - let score = score_match.as_str().parse().unwrap_or(0); - return Ok(EvalAssertionOutcome { - score, - message: Some(output), - }); - } - - anyhow::bail!("No score found in response. Raw output: {output}"); - }) - } - - async fn run( - &self, - input: &EvalSample, - judge_model: Arc, - cx: &mut TestAppContext, - ) -> Result { - self.0.assert(input, judge_model, cx).await - } -} - -fn run_eval(eval: EvalInput) -> eval_utils::EvalOutput { - let dispatcher = gpui::TestDispatcher::new(StdRng::from_os_rng()); - let mut cx = TestAppContext::build(dispatcher, None); - let result = cx.executor().block_test(async { - let test = EditAgentTest::new(&mut cx).await; - test.eval(eval, &mut cx).await - }); - match result { - Ok(output) => eval_utils::EvalOutput { - data: output.to_string(), - outcome: if output.assertion.score < 80 { - eval_utils::OutcomeKind::Failed - } else { - eval_utils::OutcomeKind::Passed - }, - metadata: EditEvalMetadata { - tags: output.sample.edit_output.parser_metrics.tags, - mismatched_tags: output.sample.edit_output.parser_metrics.mismatched_tags, - }, - }, - Err(e) => eval_utils::EvalOutput { - data: format!("{e:?}"), - outcome: eval_utils::OutcomeKind::Error, - metadata: EditEvalMetadata { - tags: 0, - mismatched_tags: 0, - }, - }, - } -} - -#[derive(Clone)] -struct EditEvalOutput { - sample: EvalSample, - assertion: EvalAssertionOutcome, -} - -impl Display for EditEvalOutput { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f, "Score: {:?}", self.assertion.score)?; - if let Some(message) = self.assertion.message.as_ref() { - writeln!(f, "Message: {}", message)?; - } - - writeln!(f, "Diff:\n{}", self.sample.diff)?; - - writeln!( - f, - "Parser Metrics:\n{:#?}", - self.sample.edit_output.parser_metrics - )?; - writeln!(f, "Raw Edits:\n{}", self.sample.edit_output.raw_edits)?; - Ok(()) - } -} - -struct EditAgentTest { - agent: EditAgent, - project: Entity, - judge_model: Arc, -} - -impl EditAgentTest { - async fn new(cx: &mut TestAppContext) -> Self { - cx.executor().allow_parking(); - - let fs = FakeFs::new(cx.executor()); - cx.update(|cx| { - settings::init(cx); - gpui_tokio::init(cx); - let http_client = Arc::new(ReqwestClient::user_agent("agent tests").unwrap()); - cx.set_http_client(http_client); - let client = Client::production(cx); - let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); - settings::init(cx); - language_model::init(client.clone(), cx); - language_models::init(user_store, client.clone(), cx); - }); - - fs.insert_tree("/root", json!({})).await; - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let agent_model = SelectedModel::from_str( - &std::env::var("ZED_AGENT_MODEL").unwrap_or("anthropic/claude-sonnet-4-latest".into()), - ) - .unwrap(); - let judge_model = SelectedModel::from_str( - &std::env::var("ZED_JUDGE_MODEL").unwrap_or("anthropic/claude-sonnet-4-latest".into()), - ) - .unwrap(); - - let authenticate_provider_tasks = cx.update(|cx| { - LanguageModelRegistry::global(cx).update(cx, |registry, cx| { - registry - .providers() - .iter() - .map(|p| p.authenticate(cx)) - .collect::>() - }) - }); - let (agent_model, judge_model) = cx - .update(|cx| { - cx.spawn(async move |cx| { - futures::future::join_all(authenticate_provider_tasks).await; - let agent_model = Self::load_model(&agent_model, cx).await; - let judge_model = Self::load_model(&judge_model, cx).await; - (agent_model.unwrap(), judge_model.unwrap()) - }) - }) - .await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - - let edit_format = EditFormat::from_env(agent_model.clone()).unwrap(); - - Self { - agent: EditAgent::new( - agent_model, - project.clone(), - action_log, - Templates::new(), - edit_format, - ), - project, - judge_model, - } - } - - async fn load_model( - selected_model: &SelectedModel, - cx: &mut AsyncApp, - ) -> Result> { - cx.update(|cx| { - let registry = LanguageModelRegistry::read_global(cx); - let provider = registry - .provider(&selected_model.provider) - .expect("Provider not found"); - provider.authenticate(cx) - })? - .await?; - cx.update(|cx| { - let models = LanguageModelRegistry::read_global(cx); - let model = models - .available_models(cx) - .find(|model| { - model.provider_id() == selected_model.provider - && model.id() == selected_model.model - }) - .unwrap_or_else(|| panic!("Model {} not found", selected_model.model.0)); - model - }) - } - - async fn eval(&self, mut eval: EvalInput, cx: &mut TestAppContext) -> Result { - // Make sure the last message in the conversation is cached. - eval.conversation.last_mut().unwrap().cache = true; - - let path = self - .project - .read_with(cx, |project, cx| { - project.find_project_path(eval.edit_file_input.path, cx) - }) - .unwrap(); - let buffer = self - .project - .update(cx, |project, cx| project.open_buffer(path, cx)) - .await - .unwrap(); - - let tools = crate::built_in_tools().collect::>(); - - let system_prompt = { - let worktrees = vec![WorktreeContext { - root_name: "root".to_string(), - abs_path: Path::new("/path/to/root").into(), - rules_file: None, - }]; - let project_context = ProjectContext::new(worktrees, Vec::default()); - let tool_names = tools - .iter() - .map(|tool| tool.name.clone().into()) - .collect::>(); - let template = crate::SystemPromptTemplate { - project: &project_context, - available_tools: tool_names, - model_name: None, - }; - let templates = Templates::new(); - template.render(&templates).unwrap() - }; - - let has_system_prompt = eval - .conversation - .first() - .is_some_and(|msg| msg.role == Role::System); - let messages = if has_system_prompt { - eval.conversation - } else { - [LanguageModelRequestMessage { - role: Role::System, - content: vec![MessageContent::Text(system_prompt)], - cache: true, - reasoning_details: None, - }] - .into_iter() - .chain(eval.conversation) - .collect::>() - }; - - let conversation = LanguageModelRequest { - messages, - tools, - thinking_allowed: true, - ..Default::default() - }; - - let edit_output = if matches!(eval.edit_file_input.mode, EditFileMode::Edit) { - if let Some(input_content) = eval.input_content.as_deref() { - buffer.update(cx, |buffer, cx| buffer.set_text(input_content, cx)); - } - retry_on_rate_limit(async || { - self.agent - .edit( - buffer.clone(), - eval.edit_file_input.display_description.clone(), - &conversation, - &mut cx.to_async(), - ) - .0 - .await - }) - .await? - } else { - retry_on_rate_limit(async || { - self.agent - .overwrite( - buffer.clone(), - eval.edit_file_input.display_description.clone(), - &conversation, - &mut cx.to_async(), - ) - .0 - .await - }) - .await? - }; - - let buffer_text = buffer.read_with(cx, |buffer, _| buffer.text()); - let sample = EvalSample { - edit_output, - diff: language::unified_diff( - eval.input_content.as_deref().unwrap_or_default(), - &buffer_text, - ), - text_before: eval.input_content.unwrap_or_default(), - text_after: buffer_text, - }; - let assertion = eval - .assertion - .run(&sample, self.judge_model.clone(), cx) - .await?; - - Ok(EditEvalOutput { assertion, sample }) - } -} - -async fn retry_on_rate_limit(mut request: impl AsyncFnMut() -> Result) -> Result { - const MAX_RETRIES: usize = 20; - let mut attempt = 0; - - loop { - attempt += 1; - let response = request().await; - - if attempt >= MAX_RETRIES { - return response; - } - - let retry_delay = match &response { - Ok(_) => None, - Err(err) => match err.downcast_ref::() { - Some(err) => match &err { - LanguageModelCompletionError::RateLimitExceeded { retry_after, .. } - | LanguageModelCompletionError::ServerOverloaded { retry_after, .. } => { - Some(retry_after.unwrap_or(Duration::from_secs(5))) - } - LanguageModelCompletionError::UpstreamProviderError { - status, - retry_after, - .. - } => { - // Only retry for specific status codes - let should_retry = matches!( - *status, - StatusCode::TOO_MANY_REQUESTS | StatusCode::SERVICE_UNAVAILABLE - ) || status.as_u16() == 529; - - if should_retry { - // Use server-provided retry_after if available, otherwise use default - Some(retry_after.unwrap_or(Duration::from_secs(5))) - } else { - None - } - } - LanguageModelCompletionError::ApiReadResponseError { .. } - | LanguageModelCompletionError::ApiInternalServerError { .. } - | LanguageModelCompletionError::HttpSend { .. } => { - // Exponential backoff for transient I/O and internal server errors - Some(Duration::from_secs(2_u64.pow((attempt - 1) as u32).min(30))) - } - _ => None, - }, - _ => None, - }, - }; - - if let Some(retry_after) = retry_delay { - let jitter = retry_after.mul_f64(rand::rng().random_range(0.0..1.0)); - eprintln!("Attempt #{attempt}: Retry after {retry_after:?} + jitter of {jitter:?}"); - Timer::after(retry_after + jitter).await; - } else { - return response; - } - } -} - -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -struct EvalAssertionOutcome { - score: usize, - message: Option, -} - -#[derive(Serialize)] -pub struct DiffJudgeTemplate { - diff: String, - assertions: &'static str, -} - -impl Template for DiffJudgeTemplate { - const TEMPLATE_NAME: &'static str = "diff_judge.hbs"; -} - -fn strip_empty_lines(text: &str) -> String { - text.lines() - .filter(|line| !line.trim().is_empty()) - .collect::>() - .join("\n") -} diff --git a/crates/agent/src/edit_agent/evals/fixtures/add_overwrite_test/before.rs b/crates/agent/src/edit_agent/evals/fixtures/add_overwrite_test/before.rs deleted file mode 100644 index 0d2a0be1fb..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/add_overwrite_test/before.rs +++ /dev/null @@ -1,1572 +0,0 @@ -use anyhow::{Context as _, Result}; -use buffer_diff::BufferDiff; -use collections::BTreeMap; -use futures::{StreamExt, channel::mpsc}; -use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task, WeakEntity}; -use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint}; -use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle}; -use std::{cmp, ops::Range, sync::Arc}; -use text::{Edit, Patch, Rope}; -use util::RangeExt; - -/// Tracks actions performed by tools in a thread -pub struct ActionLog { - /// Buffers that we want to notify the model about when they change. - tracked_buffers: BTreeMap, TrackedBuffer>, - /// Has the model edited a file since it last checked diagnostics? - edited_since_project_diagnostics_check: bool, - /// The project this action log is associated with - project: Entity, -} - -impl ActionLog { - /// Creates a new, empty action log associated with the given project. - pub fn new(project: Entity) -> Self { - Self { - tracked_buffers: BTreeMap::default(), - edited_since_project_diagnostics_check: false, - project, - } - } - - pub fn project(&self) -> &Entity { - &self.project - } - - /// Notifies a diagnostics check - pub fn checked_project_diagnostics(&mut self) { - self.edited_since_project_diagnostics_check = false; - } - - /// Returns true if any files have been edited since the last project diagnostics check - pub fn has_edited_files_since_project_diagnostics_check(&self) -> bool { - self.edited_since_project_diagnostics_check - } - - fn track_buffer_internal( - &mut self, - buffer: Entity, - is_created: bool, - cx: &mut Context, - ) -> &mut TrackedBuffer { - let tracked_buffer = self - .tracked_buffers - .entry(buffer.clone()) - .or_insert_with(|| { - let open_lsp_handle = self.project.update(cx, |project, cx| { - project.register_buffer_with_language_servers(&buffer, cx) - }); - - let text_snapshot = buffer.read(cx).text_snapshot(); - let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx)); - let (diff_update_tx, diff_update_rx) = mpsc::unbounded(); - let base_text; - let status; - let unreviewed_changes; - if is_created { - base_text = Rope::default(); - status = TrackedBufferStatus::Created; - unreviewed_changes = Patch::new(vec![Edit { - old: 0..1, - new: 0..text_snapshot.max_point().row + 1, - }]) - } else { - base_text = buffer.read(cx).as_rope().clone(); - status = TrackedBufferStatus::Modified; - unreviewed_changes = Patch::default(); - } - TrackedBuffer { - buffer: buffer.clone(), - base_text, - unreviewed_changes, - snapshot: text_snapshot.clone(), - status, - version: buffer.read(cx).version(), - diff, - diff_update: diff_update_tx, - _open_lsp_handle: open_lsp_handle, - _maintain_diff: cx.spawn({ - let buffer = buffer.clone(); - async move |this, cx| { - Self::maintain_diff(this, buffer, diff_update_rx, cx) - .await - .ok(); - } - }), - _subscription: cx.subscribe(&buffer, Self::handle_buffer_event), - } - }); - tracked_buffer.version = buffer.read(cx).version(); - tracked_buffer - } - - fn handle_buffer_event( - &mut self, - buffer: Entity, - event: &BufferEvent, - cx: &mut Context, - ) { - match event { - BufferEvent::Edited { .. } => self.handle_buffer_edited(buffer, cx), - BufferEvent::FileHandleChanged => { - self.handle_buffer_file_changed(buffer, cx); - } - _ => {} - }; - } - - fn handle_buffer_edited(&mut self, buffer: Entity, cx: &mut Context) { - let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else { - return; - }; - tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx); - } - - fn handle_buffer_file_changed(&mut self, buffer: Entity, cx: &mut Context) { - let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else { - return; - }; - - match tracked_buffer.status { - TrackedBufferStatus::Created | TrackedBufferStatus::Modified => { - if buffer - .read(cx) - .file() - .map_or(false, |file| file.disk_state() == DiskState::Deleted) - { - // If the buffer had been edited by a tool, but it got - // deleted externally, we want to stop tracking it. - self.tracked_buffers.remove(&buffer); - } - cx.notify(); - } - TrackedBufferStatus::Deleted => { - if buffer - .read(cx) - .file() - .map_or(false, |file| file.disk_state() != DiskState::Deleted) - { - // If the buffer had been deleted by a tool, but it got - // resurrected externally, we want to clear the changes we - // were tracking and reset the buffer's state. - self.tracked_buffers.remove(&buffer); - self.track_buffer_internal(buffer, false, cx); - } - cx.notify(); - } - } - } - - async fn maintain_diff( - this: WeakEntity, - buffer: Entity, - mut diff_update: mpsc::UnboundedReceiver<(ChangeAuthor, text::BufferSnapshot)>, - cx: &mut AsyncApp, - ) -> Result<()> { - while let Some((author, buffer_snapshot)) = diff_update.next().await { - let (rebase, diff, language, language_registry) = - this.read_with(cx, |this, cx| { - let tracked_buffer = this - .tracked_buffers - .get(&buffer) - .context("buffer not tracked")?; - - let rebase = cx.background_spawn({ - let mut base_text = tracked_buffer.base_text.clone(); - let old_snapshot = tracked_buffer.snapshot.clone(); - let new_snapshot = buffer_snapshot.clone(); - let unreviewed_changes = tracked_buffer.unreviewed_changes.clone(); - async move { - let edits = diff_snapshots(&old_snapshot, &new_snapshot); - if let ChangeAuthor::User = author { - apply_non_conflicting_edits( - &unreviewed_changes, - edits, - &mut base_text, - new_snapshot.as_rope(), - ); - } - (Arc::new(base_text.to_string()), base_text) - } - }); - - anyhow::Ok(( - rebase, - tracked_buffer.diff.clone(), - tracked_buffer.buffer.read(cx).language().cloned(), - tracked_buffer.buffer.read(cx).language_registry(), - )) - })??; - - let (new_base_text, new_base_text_rope) = rebase.await; - let diff_snapshot = BufferDiff::update_diff( - diff.clone(), - buffer_snapshot.clone(), - Some(new_base_text), - true, - false, - language, - language_registry, - cx, - ) - .await; - - let mut unreviewed_changes = Patch::default(); - if let Ok(diff_snapshot) = diff_snapshot { - unreviewed_changes = cx - .background_spawn({ - let diff_snapshot = diff_snapshot.clone(); - let buffer_snapshot = buffer_snapshot.clone(); - let new_base_text_rope = new_base_text_rope.clone(); - async move { - let mut unreviewed_changes = Patch::default(); - for hunk in diff_snapshot.hunks_intersecting_range( - Anchor::MIN..Anchor::MAX, - &buffer_snapshot, - ) { - let old_range = new_base_text_rope - .offset_to_point(hunk.diff_base_byte_range.start) - ..new_base_text_rope - .offset_to_point(hunk.diff_base_byte_range.end); - let new_range = hunk.range.start..hunk.range.end; - unreviewed_changes.push(point_to_row_edit( - Edit { - old: old_range, - new: new_range, - }, - &new_base_text_rope, - &buffer_snapshot.as_rope(), - )); - } - unreviewed_changes - } - }) - .await; - - diff.update(cx, |diff, cx| { - diff.set_snapshot(diff_snapshot, &buffer_snapshot, cx) - })?; - } - this.update(cx, |this, cx| { - let tracked_buffer = this - .tracked_buffers - .get_mut(&buffer) - .context("buffer not tracked")?; - tracked_buffer.base_text = new_base_text_rope; - tracked_buffer.snapshot = buffer_snapshot; - tracked_buffer.unreviewed_changes = unreviewed_changes; - cx.notify(); - anyhow::Ok(()) - })??; - } - - Ok(()) - } - - /// Track a buffer as read, so we can notify the model about user edits. - pub fn buffer_read(&mut self, buffer: Entity, cx: &mut Context) { - self.track_buffer_internal(buffer, false, cx); - } - - /// Mark a buffer as edited, so we can refresh it in the context - pub fn buffer_created(&mut self, buffer: Entity, cx: &mut Context) { - self.edited_since_project_diagnostics_check = true; - self.tracked_buffers.remove(&buffer); - self.track_buffer_internal(buffer.clone(), true, cx); - } - - /// Mark a buffer as edited, so we can refresh it in the context - pub fn buffer_edited(&mut self, buffer: Entity, cx: &mut Context) { - self.edited_since_project_diagnostics_check = true; - - let tracked_buffer = self.track_buffer_internal(buffer.clone(), false, cx); - if let TrackedBufferStatus::Deleted = tracked_buffer.status { - tracked_buffer.status = TrackedBufferStatus::Modified; - } - tracked_buffer.schedule_diff_update(ChangeAuthor::Agent, cx); - } - - pub fn will_delete_buffer(&mut self, buffer: Entity, cx: &mut Context) { - let tracked_buffer = self.track_buffer_internal(buffer.clone(), false, cx); - match tracked_buffer.status { - TrackedBufferStatus::Created => { - self.tracked_buffers.remove(&buffer); - cx.notify(); - } - TrackedBufferStatus::Modified => { - buffer.update(cx, |buffer, cx| buffer.set_text("", cx)); - tracked_buffer.status = TrackedBufferStatus::Deleted; - tracked_buffer.schedule_diff_update(ChangeAuthor::Agent, cx); - } - TrackedBufferStatus::Deleted => {} - } - cx.notify(); - } - - pub fn keep_edits_in_range( - &mut self, - buffer: Entity, - buffer_range: Range, - cx: &mut Context, - ) { - let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else { - return; - }; - - match tracked_buffer.status { - TrackedBufferStatus::Deleted => { - self.tracked_buffers.remove(&buffer); - cx.notify(); - } - _ => { - let buffer = buffer.read(cx); - let buffer_range = - buffer_range.start.to_point(buffer)..buffer_range.end.to_point(buffer); - let mut delta = 0i32; - - tracked_buffer.unreviewed_changes.retain_mut(|edit| { - edit.old.start = (edit.old.start as i32 + delta) as u32; - edit.old.end = (edit.old.end as i32 + delta) as u32; - - if buffer_range.end.row < edit.new.start - || buffer_range.start.row > edit.new.end - { - true - } else { - let old_range = tracked_buffer - .base_text - .point_to_offset(Point::new(edit.old.start, 0)) - ..tracked_buffer.base_text.point_to_offset(cmp::min( - Point::new(edit.old.end, 0), - tracked_buffer.base_text.max_point(), - )); - let new_range = tracked_buffer - .snapshot - .point_to_offset(Point::new(edit.new.start, 0)) - ..tracked_buffer.snapshot.point_to_offset(cmp::min( - Point::new(edit.new.end, 0), - tracked_buffer.snapshot.max_point(), - )); - tracked_buffer.base_text.replace( - old_range, - &tracked_buffer - .snapshot - .text_for_range(new_range) - .collect::(), - ); - delta += edit.new_len() as i32 - edit.old_len() as i32; - false - } - }); - tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx); - } - } - } - - pub fn reject_edits_in_ranges( - &mut self, - buffer: Entity, - buffer_ranges: Vec>, - cx: &mut Context, - ) -> Task> { - let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else { - return Task::ready(Ok(())); - }; - - match tracked_buffer.status { - TrackedBufferStatus::Created => { - let delete = buffer - .read(cx) - .entry_id(cx) - .and_then(|entry_id| { - self.project - .update(cx, |project, cx| project.delete_entry(entry_id, false, cx)) - }) - .unwrap_or(Task::ready(Ok(()))); - self.tracked_buffers.remove(&buffer); - cx.notify(); - delete - } - TrackedBufferStatus::Deleted => { - buffer.update(cx, |buffer, cx| { - buffer.set_text(tracked_buffer.base_text.to_string(), cx) - }); - let save = self - .project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)); - - // Clear all tracked changes for this buffer and start over as if we just read it. - self.tracked_buffers.remove(&buffer); - self.buffer_read(buffer.clone(), cx); - cx.notify(); - save - } - TrackedBufferStatus::Modified => { - buffer.update(cx, |buffer, cx| { - let mut buffer_row_ranges = buffer_ranges - .into_iter() - .map(|range| { - range.start.to_point(buffer).row..range.end.to_point(buffer).row - }) - .peekable(); - - let mut edits_to_revert = Vec::new(); - for edit in tracked_buffer.unreviewed_changes.edits() { - let new_range = tracked_buffer - .snapshot - .anchor_before(Point::new(edit.new.start, 0)) - ..tracked_buffer.snapshot.anchor_after(cmp::min( - Point::new(edit.new.end, 0), - tracked_buffer.snapshot.max_point(), - )); - let new_row_range = new_range.start.to_point(buffer).row - ..new_range.end.to_point(buffer).row; - - let mut revert = false; - while let Some(buffer_row_range) = buffer_row_ranges.peek() { - if buffer_row_range.end < new_row_range.start { - buffer_row_ranges.next(); - } else if buffer_row_range.start > new_row_range.end { - break; - } else { - revert = true; - break; - } - } - - if revert { - let old_range = tracked_buffer - .base_text - .point_to_offset(Point::new(edit.old.start, 0)) - ..tracked_buffer.base_text.point_to_offset(cmp::min( - Point::new(edit.old.end, 0), - tracked_buffer.base_text.max_point(), - )); - let old_text = tracked_buffer - .base_text - .chunks_in_range(old_range) - .collect::(); - edits_to_revert.push((new_range, old_text)); - } - } - - buffer.edit(edits_to_revert, None, cx); - }); - self.project - .update(cx, |project, cx| project.save_buffer(buffer, cx)) - } - } - } - - pub fn keep_all_edits(&mut self, cx: &mut Context) { - self.tracked_buffers - .retain(|_buffer, tracked_buffer| match tracked_buffer.status { - TrackedBufferStatus::Deleted => false, - _ => { - tracked_buffer.unreviewed_changes.clear(); - tracked_buffer.base_text = tracked_buffer.snapshot.as_rope().clone(); - tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx); - true - } - }); - cx.notify(); - } - - /// Returns the set of buffers that contain changes that haven't been reviewed by the user. - pub fn changed_buffers(&self, cx: &App) -> BTreeMap, Entity> { - self.tracked_buffers - .iter() - .filter(|(_, tracked)| tracked.has_changes(cx)) - .map(|(buffer, tracked)| (buffer.clone(), tracked.diff.clone())) - .collect() - } - - /// Iterate over buffers changed since last read or edited by the model - pub fn stale_buffers<'a>(&'a self, cx: &'a App) -> impl Iterator> { - self.tracked_buffers - .iter() - .filter(|(buffer, tracked)| { - let buffer = buffer.read(cx); - - tracked.version != buffer.version - && buffer - .file() - .map_or(false, |file| file.disk_state() != DiskState::Deleted) - }) - .map(|(buffer, _)| buffer) - } -} - -fn apply_non_conflicting_edits( - patch: &Patch, - edits: Vec>, - old_text: &mut Rope, - new_text: &Rope, -) { - let mut old_edits = patch.edits().iter().cloned().peekable(); - let mut new_edits = edits.into_iter().peekable(); - let mut applied_delta = 0i32; - let mut rebased_delta = 0i32; - - while let Some(mut new_edit) = new_edits.next() { - let mut conflict = false; - - // Push all the old edits that are before this new edit or that intersect with it. - while let Some(old_edit) = old_edits.peek() { - if new_edit.old.end < old_edit.new.start - || (!old_edit.new.is_empty() && new_edit.old.end == old_edit.new.start) - { - break; - } else if new_edit.old.start > old_edit.new.end - || (!old_edit.new.is_empty() && new_edit.old.start == old_edit.new.end) - { - let old_edit = old_edits.next().unwrap(); - rebased_delta += old_edit.new_len() as i32 - old_edit.old_len() as i32; - } else { - conflict = true; - if new_edits - .peek() - .map_or(false, |next_edit| next_edit.old.overlaps(&old_edit.new)) - { - new_edit = new_edits.next().unwrap(); - } else { - let old_edit = old_edits.next().unwrap(); - rebased_delta += old_edit.new_len() as i32 - old_edit.old_len() as i32; - } - } - } - - if !conflict { - // This edit doesn't intersect with any old edit, so we can apply it to the old text. - new_edit.old.start = (new_edit.old.start as i32 + applied_delta - rebased_delta) as u32; - new_edit.old.end = (new_edit.old.end as i32 + applied_delta - rebased_delta) as u32; - let old_bytes = old_text.point_to_offset(Point::new(new_edit.old.start, 0)) - ..old_text.point_to_offset(cmp::min( - Point::new(new_edit.old.end, 0), - old_text.max_point(), - )); - let new_bytes = new_text.point_to_offset(Point::new(new_edit.new.start, 0)) - ..new_text.point_to_offset(cmp::min( - Point::new(new_edit.new.end, 0), - new_text.max_point(), - )); - - old_text.replace( - old_bytes, - &new_text.chunks_in_range(new_bytes).collect::(), - ); - applied_delta += new_edit.new_len() as i32 - new_edit.old_len() as i32; - } - } -} - -fn diff_snapshots( - old_snapshot: &text::BufferSnapshot, - new_snapshot: &text::BufferSnapshot, -) -> Vec> { - let mut edits = new_snapshot - .edits_since::(&old_snapshot.version) - .map(|edit| point_to_row_edit(edit, old_snapshot.as_rope(), new_snapshot.as_rope())) - .peekable(); - let mut row_edits = Vec::new(); - while let Some(mut edit) = edits.next() { - while let Some(next_edit) = edits.peek() { - if edit.old.end >= next_edit.old.start { - edit.old.end = next_edit.old.end; - edit.new.end = next_edit.new.end; - edits.next(); - } else { - break; - } - } - row_edits.push(edit); - } - row_edits -} - -fn point_to_row_edit(edit: Edit, old_text: &Rope, new_text: &Rope) -> Edit { - if edit.old.start.column == old_text.line_len(edit.old.start.row) - && new_text - .chars_at(new_text.point_to_offset(edit.new.start)) - .next() - == Some('\n') - && edit.old.start != old_text.max_point() - { - Edit { - old: edit.old.start.row + 1..edit.old.end.row + 1, - new: edit.new.start.row + 1..edit.new.end.row + 1, - } - } else if edit.old.start.column == 0 - && edit.old.end.column == 0 - && edit.new.end.column == 0 - && edit.old.end != old_text.max_point() - { - Edit { - old: edit.old.start.row..edit.old.end.row, - new: edit.new.start.row..edit.new.end.row, - } - } else { - Edit { - old: edit.old.start.row..edit.old.end.row + 1, - new: edit.new.start.row..edit.new.end.row + 1, - } - } -} - -#[derive(Copy, Clone, Debug)] -enum ChangeAuthor { - User, - Agent, -} - -#[derive(Copy, Clone, Eq, PartialEq)] -enum TrackedBufferStatus { - Created, - Modified, - Deleted, -} - -struct TrackedBuffer { - buffer: Entity, - base_text: Rope, - unreviewed_changes: Patch, - status: TrackedBufferStatus, - version: clock::Global, - diff: Entity, - snapshot: text::BufferSnapshot, - diff_update: mpsc::UnboundedSender<(ChangeAuthor, text::BufferSnapshot)>, - _open_lsp_handle: OpenLspBufferHandle, - _maintain_diff: Task<()>, - _subscription: Subscription, -} - -impl TrackedBuffer { - fn has_changes(&self, cx: &App) -> bool { - self.diff - .read(cx) - .hunks(&self.buffer.read(cx), cx) - .next() - .is_some() - } - - fn schedule_diff_update(&self, author: ChangeAuthor, cx: &App) { - self.diff_update - .unbounded_send((author, self.buffer.read(cx).text_snapshot())) - .ok(); - } -} - -pub struct ChangedBuffer { - pub diff: Entity, -} - -#[cfg(test)] -mod tests { - use std::env; - - use super::*; - use buffer_diff::DiffHunkStatusKind; - use gpui::TestAppContext; - use language::Point; - use project::{FakeFs, Fs, Project, RemoveOptions}; - use rand::prelude::*; - use serde_json::json; - use settings::SettingsStore; - use util::{RandomCharIter, path}; - - #[ctor::ctor] - fn init_logger() { - zlog::init_test(); - } - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - language::init(cx); - Project::init_settings(cx); - }); - } - - #[gpui::test(iterations = 10)] - async fn test_keep_edits(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"})) - .await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let file_path = project - .read_with(cx, |project, cx| project.find_project_path("dir/file", cx)) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(1, 1)..Point::new(1, 2), "E")], None, cx) - .unwrap() - }); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(4, 2)..Point::new(4, 3), "O")], None, cx) - .unwrap() - }); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndEf\nghi\njkl\nmnO" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![ - HunkStatus { - range: Point::new(1, 0)..Point::new(2, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "def\n".into(), - }, - HunkStatus { - range: Point::new(4, 0)..Point::new(4, 3), - diff_status: DiffHunkStatusKind::Modified, - old_text: "mno".into(), - } - ], - )] - ); - - action_log.update(cx, |log, cx| { - log.keep_edits_in_range(buffer.clone(), Point::new(3, 0)..Point::new(4, 3), cx) - }); - cx.run_until_parked(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(1, 0)..Point::new(2, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "def\n".into(), - }], - )] - ); - - action_log.update(cx, |log, cx| { - log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(4, 3), cx) - }); - cx.run_until_parked(); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 10)] - async fn test_deletions(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/dir"), - json!({"file": "abc\ndef\nghi\njkl\nmno\npqr"}), - ) - .await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let file_path = project - .read_with(cx, |project, cx| project.find_project_path("dir/file", cx)) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(1, 0)..Point::new(2, 0), "")], None, cx) - .unwrap(); - buffer.finalize_last_transaction(); - }); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(3, 0)..Point::new(4, 0), "")], None, cx) - .unwrap(); - buffer.finalize_last_transaction(); - }); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\nghi\njkl\npqr" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![ - HunkStatus { - range: Point::new(1, 0)..Point::new(1, 0), - diff_status: DiffHunkStatusKind::Deleted, - old_text: "def\n".into(), - }, - HunkStatus { - range: Point::new(3, 0)..Point::new(3, 0), - diff_status: DiffHunkStatusKind::Deleted, - old_text: "mno\n".into(), - } - ], - )] - ); - - buffer.update(cx, |buffer, cx| buffer.undo(cx)); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\nghi\njkl\nmno\npqr" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(1, 0)..Point::new(1, 0), - diff_status: DiffHunkStatusKind::Deleted, - old_text: "def\n".into(), - }], - )] - ); - - action_log.update(cx, |log, cx| { - log.keep_edits_in_range(buffer.clone(), Point::new(1, 0)..Point::new(1, 0), cx) - }); - cx.run_until_parked(); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 10)] - async fn test_overlapping_user_edits(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"})) - .await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let file_path = project - .read_with(cx, |project, cx| project.find_project_path("dir/file", cx)) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(1, 2)..Point::new(2, 3), "F\nGHI")], None, cx) - .unwrap() - }); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndeF\nGHI\njkl\nmno" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(1, 0)..Point::new(3, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "def\nghi\n".into(), - }], - )] - ); - - buffer.update(cx, |buffer, cx| { - buffer.edit( - [ - (Point::new(0, 2)..Point::new(0, 2), "X"), - (Point::new(3, 0)..Point::new(3, 0), "Y"), - ], - None, - cx, - ) - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abXc\ndeF\nGHI\nYjkl\nmno" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(1, 0)..Point::new(3, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "def\nghi\n".into(), - }], - )] - ); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(Point::new(1, 1)..Point::new(1, 1), "Z")], None, cx) - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abXc\ndZeF\nGHI\nYjkl\nmno" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(1, 0)..Point::new(3, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "def\nghi\n".into(), - }], - )] - ); - - action_log.update(cx, |log, cx| { - log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), cx) - }); - cx.run_until_parked(); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 10)] - async fn test_creating_files(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/dir"), json!({})).await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let file_path = project - .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx)) - .unwrap(); - - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| buffer.set_text("lorem", cx)); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 5), - diff_status: DiffHunkStatusKind::Added, - old_text: "".into(), - }], - )] - ); - - buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "X")], None, cx)); - cx.run_until_parked(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 6), - diff_status: DiffHunkStatusKind::Added, - old_text: "".into(), - }], - )] - ); - - action_log.update(cx, |log, cx| { - log.keep_edits_in_range(buffer.clone(), 0..5, cx) - }); - cx.run_until_parked(); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 10)] - async fn test_deleting_files(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/dir"), - json!({"file1": "lorem\n", "file2": "ipsum\n"}), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let file1_path = project - .read_with(cx, |project, cx| project.find_project_path("dir/file1", cx)) - .unwrap(); - let file2_path = project - .read_with(cx, |project, cx| project.find_project_path("dir/file2", cx)) - .unwrap(); - - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let buffer1 = project - .update(cx, |project, cx| { - project.open_buffer(file1_path.clone(), cx) - }) - .await - .unwrap(); - let buffer2 = project - .update(cx, |project, cx| { - project.open_buffer(file2_path.clone(), cx) - }) - .await - .unwrap(); - - action_log.update(cx, |log, cx| log.will_delete_buffer(buffer1.clone(), cx)); - action_log.update(cx, |log, cx| log.will_delete_buffer(buffer2.clone(), cx)); - project - .update(cx, |project, cx| { - project.delete_file(file1_path.clone(), false, cx) - }) - .unwrap() - .await - .unwrap(); - project - .update(cx, |project, cx| { - project.delete_file(file2_path.clone(), false, cx) - }) - .unwrap() - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![ - ( - buffer1.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 0), - diff_status: DiffHunkStatusKind::Deleted, - old_text: "lorem\n".into(), - }] - ), - ( - buffer2.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 0), - diff_status: DiffHunkStatusKind::Deleted, - old_text: "ipsum\n".into(), - }], - ) - ] - ); - - // Simulate file1 being recreated externally. - fs.insert_file(path!("/dir/file1"), "LOREM".as_bytes().to_vec()) - .await; - - // Simulate file2 being recreated by a tool. - let buffer2 = project - .update(cx, |project, cx| project.open_buffer(file2_path, cx)) - .await - .unwrap(); - action_log.update(cx, |log, cx| log.buffer_read(buffer2.clone(), cx)); - buffer2.update(cx, |buffer, cx| buffer.set_text("IPSUM", cx)); - action_log.update(cx, |log, cx| log.buffer_edited(buffer2.clone(), cx)); - project - .update(cx, |project, cx| project.save_buffer(buffer2.clone(), cx)) - .await - .unwrap(); - - cx.run_until_parked(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer2.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 5), - diff_status: DiffHunkStatusKind::Modified, - old_text: "ipsum\n".into(), - }], - )] - ); - - // Simulate file2 being deleted externally. - fs.remove_file(path!("/dir/file2").as_ref(), RemoveOptions::default()) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 10)] - async fn test_reject_edits(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"})) - .await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let file_path = project - .read_with(cx, |project, cx| project.find_project_path("dir/file", cx)) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(1, 1)..Point::new(1, 2), "E\nXYZ")], None, cx) - .unwrap() - }); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(5, 2)..Point::new(5, 3), "O")], None, cx) - .unwrap() - }); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndE\nXYZf\nghi\njkl\nmnO" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![ - HunkStatus { - range: Point::new(1, 0)..Point::new(3, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "def\n".into(), - }, - HunkStatus { - range: Point::new(5, 0)..Point::new(5, 3), - diff_status: DiffHunkStatusKind::Modified, - old_text: "mno".into(), - } - ], - )] - ); - - // If the rejected range doesn't overlap with any hunk, we ignore it. - action_log - .update(cx, |log, cx| { - log.reject_edits_in_ranges( - buffer.clone(), - vec![Point::new(4, 0)..Point::new(4, 0)], - cx, - ) - }) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndE\nXYZf\nghi\njkl\nmnO" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![ - HunkStatus { - range: Point::new(1, 0)..Point::new(3, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "def\n".into(), - }, - HunkStatus { - range: Point::new(5, 0)..Point::new(5, 3), - diff_status: DiffHunkStatusKind::Modified, - old_text: "mno".into(), - } - ], - )] - ); - - action_log - .update(cx, |log, cx| { - log.reject_edits_in_ranges( - buffer.clone(), - vec![Point::new(0, 0)..Point::new(1, 0)], - cx, - ) - }) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndef\nghi\njkl\nmnO" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(4, 0)..Point::new(4, 3), - diff_status: DiffHunkStatusKind::Modified, - old_text: "mno".into(), - }], - )] - ); - - action_log - .update(cx, |log, cx| { - log.reject_edits_in_ranges( - buffer.clone(), - vec![Point::new(4, 0)..Point::new(4, 0)], - cx, - ) - }) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndef\nghi\njkl\nmno" - ); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 10)] - async fn test_reject_multiple_edits(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/dir"), json!({"file": "abc\ndef\nghi\njkl\nmno"})) - .await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let file_path = project - .read_with(cx, |project, cx| project.find_project_path("dir/file", cx)) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(1, 1)..Point::new(1, 2), "E\nXYZ")], None, cx) - .unwrap() - }); - buffer.update(cx, |buffer, cx| { - buffer - .edit([(Point::new(5, 2)..Point::new(5, 3), "O")], None, cx) - .unwrap() - }); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndE\nXYZf\nghi\njkl\nmnO" - ); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![ - HunkStatus { - range: Point::new(1, 0)..Point::new(3, 0), - diff_status: DiffHunkStatusKind::Modified, - old_text: "def\n".into(), - }, - HunkStatus { - range: Point::new(5, 0)..Point::new(5, 3), - diff_status: DiffHunkStatusKind::Modified, - old_text: "mno".into(), - } - ], - )] - ); - - action_log.update(cx, |log, cx| { - let range_1 = buffer.read(cx).anchor_before(Point::new(0, 0)) - ..buffer.read(cx).anchor_before(Point::new(1, 0)); - let range_2 = buffer.read(cx).anchor_before(Point::new(5, 0)) - ..buffer.read(cx).anchor_before(Point::new(5, 3)); - - log.reject_edits_in_ranges(buffer.clone(), vec![range_1, range_2], cx) - .detach(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndef\nghi\njkl\nmno" - ); - }); - cx.run_until_parked(); - assert_eq!( - buffer.read_with(cx, |buffer, _| buffer.text()), - "abc\ndef\nghi\njkl\nmno" - ); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 10)] - async fn test_reject_deleted_file(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/dir"), json!({"file": "content"})) - .await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let file_path = project - .read_with(cx, |project, cx| project.find_project_path("dir/file", cx)) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path.clone(), cx)) - .await - .unwrap(); - - cx.update(|cx| { - action_log.update(cx, |log, cx| log.will_delete_buffer(buffer.clone(), cx)); - }); - project - .update(cx, |project, cx| { - project.delete_file(file_path.clone(), false, cx) - }) - .unwrap() - .await - .unwrap(); - cx.run_until_parked(); - assert!(!fs.is_file(path!("/dir/file").as_ref()).await); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 0), - diff_status: DiffHunkStatusKind::Deleted, - old_text: "content".into(), - }] - )] - ); - - action_log - .update(cx, |log, cx| { - log.reject_edits_in_ranges( - buffer.clone(), - vec![Point::new(0, 0)..Point::new(0, 0)], - cx, - ) - }) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!(buffer.read_with(cx, |buffer, _| buffer.text()), "content"); - assert!(fs.is_file(path!("/dir/file").as_ref()).await); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 10)] - async fn test_reject_created_file(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let file_path = project - .read_with(cx, |project, cx| { - project.find_project_path("dir/new_file", cx) - }) - .unwrap(); - - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - cx.update(|cx| { - action_log.update(cx, |log, cx| log.buffer_created(buffer.clone(), cx)); - buffer.update(cx, |buffer, cx| buffer.set_text("content", cx)); - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - }); - project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) - .await - .unwrap(); - assert!(fs.is_file(path!("/dir/new_file").as_ref()).await); - cx.run_until_parked(); - assert_eq!( - unreviewed_hunks(&action_log, cx), - vec![( - buffer.clone(), - vec![HunkStatus { - range: Point::new(0, 0)..Point::new(0, 7), - diff_status: DiffHunkStatusKind::Added, - old_text: "".into(), - }], - )] - ); - - action_log - .update(cx, |log, cx| { - log.reject_edits_in_ranges( - buffer.clone(), - vec![Point::new(0, 0)..Point::new(0, 11)], - cx, - ) - }) - .await - .unwrap(); - cx.run_until_parked(); - assert!(!fs.is_file(path!("/dir/new_file").as_ref()).await); - assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); - } - - #[gpui::test(iterations = 100)] - async fn test_random_diffs(mut rng: StdRng, cx: &mut TestAppContext) { - init_test(cx); - - let operations = env::var("OPERATIONS") - .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) - .unwrap_or(20); - - let text = RandomCharIter::new(&mut rng).take(50).collect::(); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/dir"), json!({"file": text})).await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let action_log = cx.new(|_| ActionLog::new(project.clone())); - let file_path = project - .read_with(cx, |project, cx| project.find_project_path("dir/file", cx)) - .unwrap(); - let buffer = project - .update(cx, |project, cx| project.open_buffer(file_path, cx)) - .await - .unwrap(); - - action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx)); - - for _ in 0..operations { - match rng.gen_range(0..100) { - 0..25 => { - action_log.update(cx, |log, cx| { - let range = buffer.read(cx).random_byte_range(0, &mut rng); - log::info!("keeping edits in range {:?}", range); - log.keep_edits_in_range(buffer.clone(), range, cx) - }); - } - 25..50 => { - action_log - .update(cx, |log, cx| { - let range = buffer.read(cx).random_byte_range(0, &mut rng); - log::info!("rejecting edits in range {:?}", range); - log.reject_edits_in_ranges(buffer.clone(), vec![range], cx) - }) - .await - .unwrap(); - } - _ => { - let is_agent_change = rng.gen_bool(0.5); - if is_agent_change { - log::info!("agent edit"); - } else { - log::info!("user edit"); - } - cx.update(|cx| { - buffer.update(cx, |buffer, cx| buffer.randomly_edit(&mut rng, 1, cx)); - if is_agent_change { - action_log.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx)); - } - }); - } - } - - if rng.gen_bool(0.2) { - quiesce(&action_log, &buffer, cx); - } - } - - quiesce(&action_log, &buffer, cx); - - fn quiesce( - action_log: &Entity, - buffer: &Entity, - cx: &mut TestAppContext, - ) { - log::info!("quiescing..."); - cx.run_until_parked(); - action_log.update(cx, |log, cx| { - let tracked_buffer = log.tracked_buffers.get(&buffer).unwrap(); - let mut old_text = tracked_buffer.base_text.clone(); - let new_text = buffer.read(cx).as_rope(); - for edit in tracked_buffer.unreviewed_changes.edits() { - let old_start = old_text.point_to_offset(Point::new(edit.new.start, 0)); - let old_end = old_text.point_to_offset(cmp::min( - Point::new(edit.new.start + edit.old_len(), 0), - old_text.max_point(), - )); - old_text.replace( - old_start..old_end, - &new_text.slice_rows(edit.new.clone()).to_string(), - ); - } - pretty_assertions::assert_eq!(old_text.to_string(), new_text.to_string()); - }) - } - } - - #[derive(Debug, Clone, PartialEq, Eq)] - struct HunkStatus { - range: Range, - diff_status: DiffHunkStatusKind, - old_text: String, - } - - fn unreviewed_hunks( - action_log: &Entity, - cx: &TestAppContext, - ) -> Vec<(Entity, Vec)> { - cx.read(|cx| { - action_log - .read(cx) - .changed_buffers(cx) - .into_iter() - .map(|(buffer, diff)| { - let snapshot = buffer.read(cx).snapshot(); - ( - buffer, - diff.read(cx) - .hunks(&snapshot, cx) - .map(|hunk| HunkStatus { - diff_status: hunk.status().kind, - range: hunk.range, - old_text: diff - .read(cx) - .base_text() - .text_for_range(hunk.diff_base_byte_range) - .collect(), - }) - .collect(), - ) - }) - .collect() - }) - } -} diff --git a/crates/agent/src/edit_agent/evals/fixtures/delete_run_git_blame/after.rs b/crates/agent/src/edit_agent/evals/fixtures/delete_run_git_blame/after.rs deleted file mode 100644 index 89277be443..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/delete_run_git_blame/after.rs +++ /dev/null @@ -1,328 +0,0 @@ -use crate::commit::get_messages; -use crate::{GitRemote, Oid}; -use anyhow::{Context as _, Result, anyhow}; -use collections::{HashMap, HashSet}; -use futures::AsyncWriteExt; -use gpui::SharedString; -use serde::{Deserialize, Serialize}; -use std::process::Stdio; -use std::{ops::Range, path::Path}; -use text::Rope; -use time::OffsetDateTime; -use time::UtcOffset; -use time::macros::format_description; - -pub use git2 as libgit; - -#[derive(Debug, Clone, Default)] -pub struct Blame { - pub entries: Vec, - pub messages: HashMap, - pub remote_url: Option, -} - -#[derive(Clone, Debug, Default)] -pub struct ParsedCommitMessage { - pub message: SharedString, - pub permalink: Option, - pub pull_request: Option, - pub remote: Option, -} - -impl Blame { - pub async fn for_path( - git_binary: &Path, - working_directory: &Path, - path: &Path, - content: &Rope, - remote_url: Option, - ) -> Result { - let output = run_git_blame(git_binary, working_directory, path, content).await?; - let mut entries = parse_git_blame(&output)?; - entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start)); - - let mut unique_shas = HashSet::default(); - - for entry in entries.iter_mut() { - unique_shas.insert(entry.sha); - } - - let shas = unique_shas.into_iter().collect::>(); - let messages = get_messages(working_directory, &shas) - .await - .context("failed to get commit messages")?; - - Ok(Self { - entries, - messages, - remote_url, - }) - } -} - -const GIT_BLAME_NO_COMMIT_ERROR: &str = "fatal: no such ref: HEAD"; -const GIT_BLAME_NO_PATH: &str = "fatal: no such path"; - -#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] -pub struct BlameEntry { - pub sha: Oid, - - pub range: Range, - - pub original_line_number: u32, - - pub author: Option, - pub author_mail: Option, - pub author_time: Option, - pub author_tz: Option, - - pub committer_name: Option, - pub committer_email: Option, - pub committer_time: Option, - pub committer_tz: Option, - - pub summary: Option, - - pub previous: Option, - pub filename: String, -} - -impl BlameEntry { - // Returns a BlameEntry by parsing the first line of a `git blame --incremental` - // entry. The line MUST have this format: - // - // <40-byte-hex-sha1> - fn new_from_blame_line(line: &str) -> Result { - let mut parts = line.split_whitespace(); - - let sha = parts - .next() - .and_then(|line| line.parse::().ok()) - .with_context(|| format!("parsing sha from {line}"))?; - - let original_line_number = parts - .next() - .and_then(|line| line.parse::().ok()) - .with_context(|| format!("parsing original line number from {line}"))?; - let final_line_number = parts - .next() - .and_then(|line| line.parse::().ok()) - .with_context(|| format!("parsing final line number from {line}"))?; - - let line_count = parts - .next() - .and_then(|line| line.parse::().ok()) - .with_context(|| format!("parsing line count from {line}"))?; - - let start_line = final_line_number.saturating_sub(1); - let end_line = start_line + line_count; - let range = start_line..end_line; - - Ok(Self { - sha, - range, - original_line_number, - ..Default::default() - }) - } - - pub fn author_offset_date_time(&self) -> Result { - if let (Some(author_time), Some(author_tz)) = (self.author_time, &self.author_tz) { - let format = format_description!("[offset_hour][offset_minute]"); - let offset = UtcOffset::parse(author_tz, &format)?; - let date_time_utc = OffsetDateTime::from_unix_timestamp(author_time)?; - - Ok(date_time_utc.to_offset(offset)) - } else { - // Directly return current time in UTC if there's no committer time or timezone - Ok(time::OffsetDateTime::now_utc()) - } - } -} - -// parse_git_blame parses the output of `git blame --incremental`, which returns -// all the blame-entries for a given path incrementally, as it finds them. -// -// Each entry *always* starts with: -// -// <40-byte-hex-sha1> -// -// Each entry *always* ends with: -// -// filename -// -// Line numbers are 1-indexed. -// -// A `git blame --incremental` entry looks like this: -// -// 6ad46b5257ba16d12c5ca9f0d4900320959df7f4 2 2 1 -// author Joe Schmoe -// author-mail -// author-time 1709741400 -// author-tz +0100 -// committer Joe Schmoe -// committer-mail -// committer-time 1709741400 -// committer-tz +0100 -// summary Joe's cool commit -// previous 486c2409237a2c627230589e567024a96751d475 index.js -// filename index.js -// -// If the entry has the same SHA as an entry that was already printed then no -// signature information is printed: -// -// 6ad46b5257ba16d12c5ca9f0d4900320959df7f4 3 4 1 -// previous 486c2409237a2c627230589e567024a96751d475 index.js -// filename index.js -// -// More about `--incremental` output: https://mirrors.edge.kernel.org/pub/software/scm/git/docs/git-blame.html -fn parse_git_blame(output: &str) -> Result> { - let mut entries: Vec = Vec::new(); - let mut index: HashMap = HashMap::default(); - - let mut current_entry: Option = None; - - for line in output.lines() { - let mut done = false; - - match &mut current_entry { - None => { - let mut new_entry = BlameEntry::new_from_blame_line(line)?; - - if let Some(existing_entry) = index - .get(&new_entry.sha) - .and_then(|slot| entries.get(*slot)) - { - new_entry.author.clone_from(&existing_entry.author); - new_entry - .author_mail - .clone_from(&existing_entry.author_mail); - new_entry.author_time = existing_entry.author_time; - new_entry.author_tz.clone_from(&existing_entry.author_tz); - new_entry - .committer_name - .clone_from(&existing_entry.committer_name); - new_entry - .committer_email - .clone_from(&existing_entry.committer_email); - new_entry.committer_time = existing_entry.committer_time; - new_entry - .committer_tz - .clone_from(&existing_entry.committer_tz); - new_entry.summary.clone_from(&existing_entry.summary); - } - - current_entry.replace(new_entry); - } - Some(entry) => { - let Some((key, value)) = line.split_once(' ') else { - continue; - }; - let is_committed = !entry.sha.is_zero(); - match key { - "filename" => { - entry.filename = value.into(); - done = true; - } - "previous" => entry.previous = Some(value.into()), - - "summary" if is_committed => entry.summary = Some(value.into()), - "author" if is_committed => entry.author = Some(value.into()), - "author-mail" if is_committed => entry.author_mail = Some(value.into()), - "author-time" if is_committed => { - entry.author_time = Some(value.parse::()?) - } - "author-tz" if is_committed => entry.author_tz = Some(value.into()), - - "committer" if is_committed => entry.committer_name = Some(value.into()), - "committer-mail" if is_committed => entry.committer_email = Some(value.into()), - "committer-time" if is_committed => { - entry.committer_time = Some(value.parse::()?) - } - "committer-tz" if is_committed => entry.committer_tz = Some(value.into()), - _ => {} - } - } - }; - - if done { - if let Some(entry) = current_entry.take() { - index.insert(entry.sha, entries.len()); - - // We only want annotations that have a commit. - if !entry.sha.is_zero() { - entries.push(entry); - } - } - } - } - - Ok(entries) -} - -#[cfg(test)] -mod tests { - use std::path::PathBuf; - - use super::BlameEntry; - use super::parse_git_blame; - - fn read_test_data(filename: &str) -> String { - let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - path.push("test_data"); - path.push(filename); - - std::fs::read_to_string(&path) - .unwrap_or_else(|_| panic!("Could not read test data at {:?}. Is it generated?", path)) - } - - fn assert_eq_golden(entries: &Vec, golden_filename: &str) { - let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - path.push("test_data"); - path.push("golden"); - path.push(format!("{}.json", golden_filename)); - - let mut have_json = - serde_json::to_string_pretty(&entries).expect("could not serialize entries to JSON"); - // We always want to save with a trailing newline. - have_json.push('\n'); - - let update = std::env::var("UPDATE_GOLDEN") - .map(|val| val.eq_ignore_ascii_case("true")) - .unwrap_or(false); - - if update { - std::fs::create_dir_all(path.parent().unwrap()) - .expect("could not create golden test data directory"); - std::fs::write(&path, have_json).expect("could not write out golden data"); - } else { - let want_json = - std::fs::read_to_string(&path).unwrap_or_else(|_| { - panic!("could not read golden test data file at {:?}. Did you run the test with UPDATE_GOLDEN=true before?", path); - }).replace("\r\n", "\n"); - - pretty_assertions::assert_eq!(have_json, want_json, "wrong blame entries"); - } - } - - #[test] - fn test_parse_git_blame_not_committed() { - let output = read_test_data("blame_incremental_not_committed"); - let entries = parse_git_blame(&output).unwrap(); - assert_eq_golden(&entries, "blame_incremental_not_committed"); - } - - #[test] - fn test_parse_git_blame_simple() { - let output = read_test_data("blame_incremental_simple"); - let entries = parse_git_blame(&output).unwrap(); - assert_eq_golden(&entries, "blame_incremental_simple"); - } - - #[test] - fn test_parse_git_blame_complex() { - let output = read_test_data("blame_incremental_complex"); - let entries = parse_git_blame(&output).unwrap(); - assert_eq_golden(&entries, "blame_incremental_complex"); - } -} diff --git a/crates/agent/src/edit_agent/evals/fixtures/delete_run_git_blame/before.rs b/crates/agent/src/edit_agent/evals/fixtures/delete_run_git_blame/before.rs deleted file mode 100644 index 36fccb5132..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/delete_run_git_blame/before.rs +++ /dev/null @@ -1,371 +0,0 @@ -use crate::commit::get_messages; -use crate::{GitRemote, Oid}; -use anyhow::{Context as _, Result, anyhow}; -use collections::{HashMap, HashSet}; -use futures::AsyncWriteExt; -use gpui::SharedString; -use serde::{Deserialize, Serialize}; -use std::process::Stdio; -use std::{ops::Range, path::Path}; -use text::Rope; -use time::OffsetDateTime; -use time::UtcOffset; -use time::macros::format_description; - -pub use git2 as libgit; - -#[derive(Debug, Clone, Default)] -pub struct Blame { - pub entries: Vec, - pub messages: HashMap, - pub remote_url: Option, -} - -#[derive(Clone, Debug, Default)] -pub struct ParsedCommitMessage { - pub message: SharedString, - pub permalink: Option, - pub pull_request: Option, - pub remote: Option, -} - -impl Blame { - pub async fn for_path( - git_binary: &Path, - working_directory: &Path, - path: &Path, - content: &Rope, - remote_url: Option, - ) -> Result { - let output = run_git_blame(git_binary, working_directory, path, content).await?; - let mut entries = parse_git_blame(&output)?; - entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start)); - - let mut unique_shas = HashSet::default(); - - for entry in entries.iter_mut() { - unique_shas.insert(entry.sha); - } - - let shas = unique_shas.into_iter().collect::>(); - let messages = get_messages(working_directory, &shas) - .await - .context("failed to get commit messages")?; - - Ok(Self { - entries, - messages, - remote_url, - }) - } -} - -const GIT_BLAME_NO_COMMIT_ERROR: &str = "fatal: no such ref: HEAD"; -const GIT_BLAME_NO_PATH: &str = "fatal: no such path"; - -async fn run_git_blame( - git_binary: &Path, - working_directory: &Path, - path: &Path, - contents: &Rope, -) -> Result { - let mut child = util::command::new_smol_command(git_binary) - .current_dir(working_directory) - .arg("blame") - .arg("--incremental") - .arg("--contents") - .arg("-") - .arg(path.as_os_str()) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .context("starting git blame process")?; - - let stdin = child - .stdin - .as_mut() - .context("failed to get pipe to stdin of git blame command")?; - - for chunk in contents.chunks() { - stdin.write_all(chunk.as_bytes()).await?; - } - stdin.flush().await?; - - let output = child.output().await.context("reading git blame output")?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - let trimmed = stderr.trim(); - if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) { - return Ok(String::new()); - } - anyhow::bail!("git blame process failed: {stderr}"); - } - - Ok(String::from_utf8(output.stdout)?) -} - -#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] -pub struct BlameEntry { - pub sha: Oid, - - pub range: Range, - - pub original_line_number: u32, - - pub author: Option, - pub author_mail: Option, - pub author_time: Option, - pub author_tz: Option, - - pub committer_name: Option, - pub committer_email: Option, - pub committer_time: Option, - pub committer_tz: Option, - - pub summary: Option, - - pub previous: Option, - pub filename: String, -} - -impl BlameEntry { - // Returns a BlameEntry by parsing the first line of a `git blame --incremental` - // entry. The line MUST have this format: - // - // <40-byte-hex-sha1> - fn new_from_blame_line(line: &str) -> Result { - let mut parts = line.split_whitespace(); - - let sha = parts - .next() - .and_then(|line| line.parse::().ok()) - .with_context(|| format!("parsing sha from {line}"))?; - - let original_line_number = parts - .next() - .and_then(|line| line.parse::().ok()) - .with_context(|| format!("parsing original line number from {line}"))?; - let final_line_number = parts - .next() - .and_then(|line| line.parse::().ok()) - .with_context(|| format!("parsing final line number from {line}"))?; - - let line_count = parts - .next() - .and_then(|line| line.parse::().ok()) - .with_context(|| format!("parsing line count from {line}"))?; - - let start_line = final_line_number.saturating_sub(1); - let end_line = start_line + line_count; - let range = start_line..end_line; - - Ok(Self { - sha, - range, - original_line_number, - ..Default::default() - }) - } - - pub fn author_offset_date_time(&self) -> Result { - if let (Some(author_time), Some(author_tz)) = (self.author_time, &self.author_tz) { - let format = format_description!("[offset_hour][offset_minute]"); - let offset = UtcOffset::parse(author_tz, &format)?; - let date_time_utc = OffsetDateTime::from_unix_timestamp(author_time)?; - - Ok(date_time_utc.to_offset(offset)) - } else { - // Directly return current time in UTC if there's no committer time or timezone - Ok(time::OffsetDateTime::now_utc()) - } - } -} - -// parse_git_blame parses the output of `git blame --incremental`, which returns -// all the blame-entries for a given path incrementally, as it finds them. -// -// Each entry *always* starts with: -// -// <40-byte-hex-sha1> -// -// Each entry *always* ends with: -// -// filename -// -// Line numbers are 1-indexed. -// -// A `git blame --incremental` entry looks like this: -// -// 6ad46b5257ba16d12c5ca9f0d4900320959df7f4 2 2 1 -// author Joe Schmoe -// author-mail -// author-time 1709741400 -// author-tz +0100 -// committer Joe Schmoe -// committer-mail -// committer-time 1709741400 -// committer-tz +0100 -// summary Joe's cool commit -// previous 486c2409237a2c627230589e567024a96751d475 index.js -// filename index.js -// -// If the entry has the same SHA as an entry that was already printed then no -// signature information is printed: -// -// 6ad46b5257ba16d12c5ca9f0d4900320959df7f4 3 4 1 -// previous 486c2409237a2c627230589e567024a96751d475 index.js -// filename index.js -// -// More about `--incremental` output: https://mirrors.edge.kernel.org/pub/software/scm/git/docs/git-blame.html -fn parse_git_blame(output: &str) -> Result> { - let mut entries: Vec = Vec::new(); - let mut index: HashMap = HashMap::default(); - - let mut current_entry: Option = None; - - for line in output.lines() { - let mut done = false; - - match &mut current_entry { - None => { - let mut new_entry = BlameEntry::new_from_blame_line(line)?; - - if let Some(existing_entry) = index - .get(&new_entry.sha) - .and_then(|slot| entries.get(*slot)) - { - new_entry.author.clone_from(&existing_entry.author); - new_entry - .author_mail - .clone_from(&existing_entry.author_mail); - new_entry.author_time = existing_entry.author_time; - new_entry.author_tz.clone_from(&existing_entry.author_tz); - new_entry - .committer_name - .clone_from(&existing_entry.committer_name); - new_entry - .committer_email - .clone_from(&existing_entry.committer_email); - new_entry.committer_time = existing_entry.committer_time; - new_entry - .committer_tz - .clone_from(&existing_entry.committer_tz); - new_entry.summary.clone_from(&existing_entry.summary); - } - - current_entry.replace(new_entry); - } - Some(entry) => { - let Some((key, value)) = line.split_once(' ') else { - continue; - }; - let is_committed = !entry.sha.is_zero(); - match key { - "filename" => { - entry.filename = value.into(); - done = true; - } - "previous" => entry.previous = Some(value.into()), - - "summary" if is_committed => entry.summary = Some(value.into()), - "author" if is_committed => entry.author = Some(value.into()), - "author-mail" if is_committed => entry.author_mail = Some(value.into()), - "author-time" if is_committed => { - entry.author_time = Some(value.parse::()?) - } - "author-tz" if is_committed => entry.author_tz = Some(value.into()), - - "committer" if is_committed => entry.committer_name = Some(value.into()), - "committer-mail" if is_committed => entry.committer_email = Some(value.into()), - "committer-time" if is_committed => { - entry.committer_time = Some(value.parse::()?) - } - "committer-tz" if is_committed => entry.committer_tz = Some(value.into()), - _ => {} - } - } - }; - - if done { - if let Some(entry) = current_entry.take() { - index.insert(entry.sha, entries.len()); - - // We only want annotations that have a commit. - if !entry.sha.is_zero() { - entries.push(entry); - } - } - } - } - - Ok(entries) -} - -#[cfg(test)] -mod tests { - use std::path::PathBuf; - - use super::BlameEntry; - use super::parse_git_blame; - - fn read_test_data(filename: &str) -> String { - let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - path.push("test_data"); - path.push(filename); - - std::fs::read_to_string(&path) - .unwrap_or_else(|_| panic!("Could not read test data at {:?}. Is it generated?", path)) - } - - fn assert_eq_golden(entries: &Vec, golden_filename: &str) { - let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - path.push("test_data"); - path.push("golden"); - path.push(format!("{}.json", golden_filename)); - - let mut have_json = - serde_json::to_string_pretty(&entries).expect("could not serialize entries to JSON"); - // We always want to save with a trailing newline. - have_json.push('\n'); - - let update = std::env::var("UPDATE_GOLDEN") - .map(|val| val.eq_ignore_ascii_case("true")) - .unwrap_or(false); - - if update { - std::fs::create_dir_all(path.parent().unwrap()) - .expect("could not create golden test data directory"); - std::fs::write(&path, have_json).expect("could not write out golden data"); - } else { - let want_json = - std::fs::read_to_string(&path).unwrap_or_else(|_| { - panic!("could not read golden test data file at {:?}. Did you run the test with UPDATE_GOLDEN=true before?", path); - }).replace("\r\n", "\n"); - - pretty_assertions::assert_eq!(have_json, want_json, "wrong blame entries"); - } - } - - #[test] - fn test_parse_git_blame_not_committed() { - let output = read_test_data("blame_incremental_not_committed"); - let entries = parse_git_blame(&output).unwrap(); - assert_eq_golden(&entries, "blame_incremental_not_committed"); - } - - #[test] - fn test_parse_git_blame_simple() { - let output = read_test_data("blame_incremental_simple"); - let entries = parse_git_blame(&output).unwrap(); - assert_eq_golden(&entries, "blame_incremental_simple"); - } - - #[test] - fn test_parse_git_blame_complex() { - let output = read_test_data("blame_incremental_complex"); - let entries = parse_git_blame(&output).unwrap(); - assert_eq_golden(&entries, "blame_incremental_complex"); - } -} diff --git a/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/before.rs b/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/before.rs deleted file mode 100644 index 607daa8ce3..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/before.rs +++ /dev/null @@ -1,21343 +0,0 @@ -#![allow(rustdoc::private_intra_doc_links)] -//! This is the place where everything editor-related is stored (data-wise) and displayed (ui-wise). -//! The main point of interest in this crate is [`Editor`] type, which is used in every other Zed part as a user input element. -//! It comes in different flavors: single line, multiline and a fixed height one. -//! -//! Editor contains of multiple large submodules: -//! * [`element`] — the place where all rendering happens -//! * [`display_map`] - chunks up text in the editor into the logical blocks, establishes coordinates and mapping between each of them. -//! Contains all metadata related to text transformations (folds, fake inlay text insertions, soft wraps, tab markup, etc.). -//! * [`inlay_hint_cache`] - is a storage of inlay hints out of LSP requests, responsible for querying LSP and updating `display_map`'s state accordingly. -//! -//! All other submodules and structs are mostly concerned with holding editor data about the way it displays current buffer region(s). -//! -//! If you're looking to improve Vim mode, you should check out Vim crate that wraps Editor and overrides its behavior. -pub mod actions; -mod blink_manager; -mod clangd_ext; -mod code_context_menus; -pub mod display_map; -mod editor_settings; -mod editor_settings_controls; -mod element; -mod git; -mod highlight_matching_bracket; -mod hover_links; -pub mod hover_popover; -mod indent_guides; -mod inlay_hint_cache; -pub mod items; -mod jsx_tag_auto_close; -mod linked_editing_ranges; -mod lsp_ext; -mod mouse_context_menu; -pub mod movement; -mod persistence; -mod proposed_changes_editor; -mod rust_analyzer_ext; -pub mod scroll; -mod selections_collection; -pub mod tasks; - -#[cfg(test)] -mod code_completion_tests; -#[cfg(test)] -mod editor_tests; -#[cfg(test)] -mod inline_completion_tests; -mod signature_help; -#[cfg(any(test, feature = "test-support"))] -pub mod test; - -pub(crate) use actions::*; -pub use actions::{AcceptEditPrediction, OpenExcerpts, OpenExcerptsSplit}; -use aho_corasick::AhoCorasick; -use anyhow::{Context as _, Result, anyhow}; -use blink_manager::BlinkManager; -use buffer_diff::DiffHunkStatus; -use client::{Collaborator, ParticipantIndex}; -use clock::ReplicaId; -use collections::{BTreeMap, HashMap, HashSet, VecDeque}; -use convert_case::{Case, Casing}; -use display_map::*; -pub use display_map::{ChunkRenderer, ChunkRendererContext, DisplayPoint, FoldPlaceholder}; -use editor_settings::GoToDefinitionFallback; -pub use editor_settings::{ - CurrentLineHighlight, EditorSettings, HideMouseMode, ScrollBeyondLastLine, SearchSettings, - ShowScrollbar, -}; -pub use editor_settings_controls::*; -use element::{AcceptEditPredictionBinding, LineWithInvisibles, PositionMap, layout_line}; -pub use element::{ - CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition, -}; -use feature_flags::{DebuggerFeatureFlag, FeatureFlagAppExt}; -use futures::{ - FutureExt, - future::{self, Shared, join}, -}; -use fuzzy::StringMatchCandidate; - -use ::git::blame::BlameEntry; -use ::git::{Restore, blame::ParsedCommitMessage}; -use code_context_menus::{ - AvailableCodeAction, CodeActionContents, CodeActionsItem, CodeActionsMenu, CodeContextMenu, - CompletionsMenu, ContextMenuOrigin, -}; -use git::blame::{GitBlame, GlobalBlameRenderer}; -use gpui::{ - Action, Animation, AnimationExt, AnyElement, App, AppContext, AsyncWindowContext, - AvailableSpace, Background, Bounds, ClickEvent, ClipboardEntry, ClipboardItem, Context, - DispatchPhase, Edges, Entity, EntityInputHandler, EventEmitter, FocusHandle, FocusOutEvent, - Focusable, FontId, FontWeight, Global, HighlightStyle, Hsla, KeyContext, Modifiers, - MouseButton, MouseDownEvent, PaintQuad, ParentElement, Pixels, Render, ScrollHandle, - SharedString, Size, Stateful, Styled, Subscription, Task, TextStyle, TextStyleRefinement, - UTF16Selection, UnderlineStyle, UniformListScrollHandle, WeakEntity, WeakFocusHandle, Window, - div, impl_actions, point, prelude::*, pulsating_between, px, relative, size, -}; -use highlight_matching_bracket::refresh_matching_bracket_highlights; -use hover_links::{HoverLink, HoveredLinkState, InlayHighlight, find_file}; -pub use hover_popover::hover_markdown_style; -use hover_popover::{HoverState, hide_hover}; -use indent_guides::ActiveIndentGuidesState; -use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy}; -pub use inline_completion::Direction; -use inline_completion::{EditPredictionProvider, InlineCompletionProviderHandle}; -pub use items::MAX_TAB_TITLE_LEN; -use itertools::Itertools; -use language::{ - AutoindentMode, BracketMatch, BracketPair, Buffer, Capability, CharKind, CodeLabel, - CursorShape, DiagnosticEntry, DiffOptions, EditPredictionsMode, EditPreview, HighlightedText, - IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection, SelectionGoal, TextObject, - TransactionId, TreeSitterOptions, WordsQuery, - language_settings::{ - self, InlayHintSettings, LspInsertMode, RewrapBehavior, WordsCompletionMode, - all_language_settings, language_settings, - }, - point_from_lsp, text_diff_with_options, -}; -use language::{BufferRow, CharClassifier, Runnable, RunnableRange, point_to_lsp}; -use linked_editing_ranges::refresh_linked_ranges; -use markdown::Markdown; -use mouse_context_menu::MouseContextMenu; -use persistence::DB; -use project::{ - ProjectPath, - debugger::{ - breakpoint_store::{ - BreakpointEditAction, BreakpointState, BreakpointStore, BreakpointStoreEvent, - }, - session::{Session, SessionEvent}, - }, -}; - -pub use git::blame::BlameRenderer; -pub use proposed_changes_editor::{ - ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar, -}; -use smallvec::smallvec; -use std::{cell::OnceCell, iter::Peekable}; -use task::{ResolvedTask, RunnableTag, TaskTemplate, TaskVariables}; - -pub use lsp::CompletionContext; -use lsp::{ - CodeActionKind, CompletionItemKind, CompletionTriggerKind, DiagnosticSeverity, - InsertTextFormat, InsertTextMode, LanguageServerId, LanguageServerName, -}; - -use language::BufferSnapshot; -pub use lsp_ext::lsp_tasks; -use movement::TextLayoutDetails; -pub use multi_buffer::{ - Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, PathKey, - RowInfo, ToOffset, ToPoint, -}; -use multi_buffer::{ - ExcerptInfo, ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, - MultiOrSingleBufferOffsetRange, ToOffsetUtf16, -}; -use parking_lot::Mutex; -use project::{ - CodeAction, Completion, CompletionIntent, CompletionSource, DocumentHighlight, InlayHint, - Location, LocationLink, PrepareRenameResponse, Project, ProjectItem, ProjectTransaction, - TaskSourceKind, - debugger::breakpoint_store::Breakpoint, - lsp_store::{CompletionDocumentation, FormatTrigger, LspFormatTarget, OpenLspBufferHandle}, - project_settings::{GitGutterSetting, ProjectSettings}, -}; -use rand::prelude::*; -use rpc::{ErrorExt, proto::*}; -use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide}; -use selections_collection::{ - MutableSelectionsCollection, SelectionsCollection, resolve_selections, -}; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsLocation, SettingsStore, update_settings_file}; -use smallvec::SmallVec; -use snippet::Snippet; -use std::sync::Arc; -use std::{ - any::TypeId, - borrow::Cow, - cell::RefCell, - cmp::{self, Ordering, Reverse}, - mem, - num::NonZeroU32, - ops::{ControlFlow, Deref, DerefMut, Not as _, Range, RangeInclusive}, - path::{Path, PathBuf}, - rc::Rc, - time::{Duration, Instant}, -}; -pub use sum_tree::Bias; -use sum_tree::TreeMap; -use text::{BufferId, FromAnchor, OffsetUtf16, Rope}; -use theme::{ - ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, ThemeColors, ThemeSettings, - observe_buffer_font_size_adjustment, -}; -use ui::{ - ButtonSize, ButtonStyle, ContextMenu, Disclosure, IconButton, IconButtonShape, IconName, - IconSize, Key, Tooltip, h_flex, prelude::*, -}; -use util::{RangeExt, ResultExt, TryFutureExt, maybe, post_inc}; -use workspace::{ - Item as WorkspaceItem, ItemId, ItemNavHistory, OpenInTerminal, OpenTerminal, - RestoreOnStartupBehavior, SERIALIZATION_THROTTLE_TIME, SplitDirection, TabBarSettings, Toast, - ViewId, Workspace, WorkspaceId, WorkspaceSettings, - item::{ItemHandle, PreviewTabsSettings}, - notifications::{DetachAndPromptErr, NotificationId, NotifyTaskExt}, - searchable::SearchEvent, -}; - -use crate::hover_links::{find_url, find_url_from_range}; -use crate::signature_help::{SignatureHelpHiddenBy, SignatureHelpState}; - -pub const FILE_HEADER_HEIGHT: u32 = 2; -pub const MULTI_BUFFER_EXCERPT_HEADER_HEIGHT: u32 = 1; -pub const DEFAULT_MULTIBUFFER_CONTEXT: u32 = 2; -const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); -const MAX_LINE_LEN: usize = 1024; -const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10; -const MAX_SELECTION_HISTORY_LEN: usize = 1024; -pub(crate) const CURSORS_VISIBLE_FOR: Duration = Duration::from_millis(2000); -#[doc(hidden)] -pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250); -const SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(100); - -pub(crate) const CODE_ACTION_TIMEOUT: Duration = Duration::from_secs(5); -pub(crate) const FORMAT_TIMEOUT: Duration = Duration::from_secs(5); -pub(crate) const SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1); - -pub(crate) const EDIT_PREDICTION_KEY_CONTEXT: &str = "edit_prediction"; -pub(crate) const EDIT_PREDICTION_CONFLICT_KEY_CONTEXT: &str = "edit_prediction_conflict"; -pub(crate) const MIN_LINE_NUMBER_DIGITS: u32 = 4; - -pub type RenderDiffHunkControlsFn = Arc< - dyn Fn( - u32, - &DiffHunkStatus, - Range, - bool, - Pixels, - &Entity, - &mut Window, - &mut App, - ) -> AnyElement, ->; - -const COLUMNAR_SELECTION_MODIFIERS: Modifiers = Modifiers { - alt: true, - shift: true, - control: false, - platform: false, - function: false, -}; - -struct InlineValueCache { - enabled: bool, - inlays: Vec, - refresh_task: Task>, -} - -impl InlineValueCache { - fn new(enabled: bool) -> Self { - Self { - enabled, - inlays: Vec::new(), - refresh_task: Task::ready(None), - } - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub enum InlayId { - InlineCompletion(usize), - Hint(usize), - DebuggerValue(usize), -} - -impl InlayId { - fn id(&self) -> usize { - match self { - Self::InlineCompletion(id) => *id, - Self::Hint(id) => *id, - Self::DebuggerValue(id) => *id, - } - } -} - -pub enum ActiveDebugLine {} -enum DocumentHighlightRead {} -enum DocumentHighlightWrite {} -enum InputComposition {} -enum SelectedTextHighlight {} - -pub enum ConflictsOuter {} -pub enum ConflictsOurs {} -pub enum ConflictsTheirs {} -pub enum ConflictsOursMarker {} -pub enum ConflictsTheirsMarker {} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub enum Navigated { - Yes, - No, -} - -impl Navigated { - pub fn from_bool(yes: bool) -> Navigated { - if yes { Navigated::Yes } else { Navigated::No } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -enum DisplayDiffHunk { - Folded { - display_row: DisplayRow, - }, - Unfolded { - is_created_file: bool, - diff_base_byte_range: Range, - display_row_range: Range, - multi_buffer_range: Range, - status: DiffHunkStatus, - }, -} - -pub enum HideMouseCursorOrigin { - TypingAction, - MovementAction, -} - -pub fn init_settings(cx: &mut App) { - EditorSettings::register(cx); -} - -pub fn init(cx: &mut App) { - init_settings(cx); - - cx.set_global(GlobalBlameRenderer(Arc::new(()))); - - workspace::register_project_item::(cx); - workspace::FollowableViewRegistry::register::(cx); - workspace::register_serializable_item::(cx); - - cx.observe_new( - |workspace: &mut Workspace, _: Option<&mut Window>, _cx: &mut Context| { - workspace.register_action(Editor::new_file); - workspace.register_action(Editor::new_file_vertical); - workspace.register_action(Editor::new_file_horizontal); - workspace.register_action(Editor::cancel_language_server_work); - }, - ) - .detach(); - - cx.on_action(move |_: &workspace::NewFile, cx| { - let app_state = workspace::AppState::global(cx); - if let Some(app_state) = app_state.upgrade() { - workspace::open_new( - Default::default(), - app_state, - cx, - |workspace, window, cx| { - Editor::new_file(workspace, &Default::default(), window, cx) - }, - ) - .detach(); - } - }); - cx.on_action(move |_: &workspace::NewWindow, cx| { - let app_state = workspace::AppState::global(cx); - if let Some(app_state) = app_state.upgrade() { - workspace::open_new( - Default::default(), - app_state, - cx, - |workspace, window, cx| { - cx.activate(true); - Editor::new_file(workspace, &Default::default(), window, cx) - }, - ) - .detach(); - } - }); -} - -pub fn set_blame_renderer(renderer: impl BlameRenderer + 'static, cx: &mut App) { - cx.set_global(GlobalBlameRenderer(Arc::new(renderer))); -} - -pub trait DiagnosticRenderer { - fn render_group( - &self, - diagnostic_group: Vec>, - buffer_id: BufferId, - snapshot: EditorSnapshot, - editor: WeakEntity, - cx: &mut App, - ) -> Vec>; - - fn render_hover( - &self, - diagnostic_group: Vec>, - range: Range, - buffer_id: BufferId, - cx: &mut App, - ) -> Option>; - - fn open_link( - &self, - editor: &mut Editor, - link: SharedString, - window: &mut Window, - cx: &mut Context, - ); -} - -pub(crate) struct GlobalDiagnosticRenderer(pub Arc); - -impl GlobalDiagnosticRenderer { - fn global(cx: &App) -> Option> { - cx.try_global::().map(|g| g.0.clone()) - } -} - -impl gpui::Global for GlobalDiagnosticRenderer {} -pub fn set_diagnostic_renderer(renderer: impl DiagnosticRenderer + 'static, cx: &mut App) { - cx.set_global(GlobalDiagnosticRenderer(Arc::new(renderer))); -} - -pub struct SearchWithinRange; - -trait InvalidationRegion { - fn ranges(&self) -> &[Range]; -} - -#[derive(Clone, Debug, PartialEq)] -pub enum SelectPhase { - Begin { - position: DisplayPoint, - add: bool, - click_count: usize, - }, - BeginColumnar { - position: DisplayPoint, - reset: bool, - goal_column: u32, - }, - Extend { - position: DisplayPoint, - click_count: usize, - }, - Update { - position: DisplayPoint, - goal_column: u32, - scroll_delta: gpui::Point, - }, - End, -} - -#[derive(Clone, Debug)] -pub enum SelectMode { - Character, - Word(Range), - Line(Range), - All, -} - -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub enum EditorMode { - SingleLine { - auto_width: bool, - }, - AutoHeight { - max_lines: usize, - }, - Full { - /// When set to `true`, the editor will scale its UI elements with the buffer font size. - scale_ui_elements_with_buffer_font_size: bool, - /// When set to `true`, the editor will render a background for the active line. - show_active_line_background: bool, - /// When set to `true`, the editor's height will be determined by its content. - sized_by_content: bool, - }, -} - -impl EditorMode { - pub fn full() -> Self { - Self::Full { - scale_ui_elements_with_buffer_font_size: true, - show_active_line_background: true, - sized_by_content: false, - } - } - - pub fn is_full(&self) -> bool { - matches!(self, Self::Full { .. }) - } -} - -#[derive(Copy, Clone, Debug)] -pub enum SoftWrap { - /// Prefer not to wrap at all. - /// - /// Note: this is currently internal, as actually limited by [`crate::MAX_LINE_LEN`] until it wraps. - /// The mode is used inside git diff hunks, where it's seems currently more useful to not wrap as much as possible. - GitDiff, - /// Prefer a single line generally, unless an overly long line is encountered. - None, - /// Soft wrap lines that exceed the editor width. - EditorWidth, - /// Soft wrap lines at the preferred line length. - Column(u32), - /// Soft wrap line at the preferred line length or the editor width (whichever is smaller). - Bounded(u32), -} - -#[derive(Clone)] -pub struct EditorStyle { - pub background: Hsla, - pub local_player: PlayerColor, - pub text: TextStyle, - pub scrollbar_width: Pixels, - pub syntax: Arc, - pub status: StatusColors, - pub inlay_hints_style: HighlightStyle, - pub inline_completion_styles: InlineCompletionStyles, - pub unnecessary_code_fade: f32, -} - -impl Default for EditorStyle { - fn default() -> Self { - Self { - background: Hsla::default(), - local_player: PlayerColor::default(), - text: TextStyle::default(), - scrollbar_width: Pixels::default(), - syntax: Default::default(), - // HACK: Status colors don't have a real default. - // We should look into removing the status colors from the editor - // style and retrieve them directly from the theme. - status: StatusColors::dark(), - inlay_hints_style: HighlightStyle::default(), - inline_completion_styles: InlineCompletionStyles { - insertion: HighlightStyle::default(), - whitespace: HighlightStyle::default(), - }, - unnecessary_code_fade: Default::default(), - } - } -} - -pub fn make_inlay_hints_style(cx: &mut App) -> HighlightStyle { - let show_background = language_settings::language_settings(None, None, cx) - .inlay_hints - .show_background; - - HighlightStyle { - color: Some(cx.theme().status().hint), - background_color: show_background.then(|| cx.theme().status().hint_background), - ..HighlightStyle::default() - } -} - -pub fn make_suggestion_styles(cx: &mut App) -> InlineCompletionStyles { - InlineCompletionStyles { - insertion: HighlightStyle { - color: Some(cx.theme().status().predictive), - ..HighlightStyle::default() - }, - whitespace: HighlightStyle { - background_color: Some(cx.theme().status().created_background), - ..HighlightStyle::default() - }, - } -} - -type CompletionId = usize; - -pub(crate) enum EditDisplayMode { - TabAccept, - DiffPopover, - Inline, -} - -enum InlineCompletion { - Edit { - edits: Vec<(Range, String)>, - edit_preview: Option, - display_mode: EditDisplayMode, - snapshot: BufferSnapshot, - }, - Move { - target: Anchor, - snapshot: BufferSnapshot, - }, -} - -struct InlineCompletionState { - inlay_ids: Vec, - completion: InlineCompletion, - completion_id: Option, - invalidation_range: Range, -} - -enum EditPredictionSettings { - Disabled, - Enabled { - show_in_menu: bool, - preview_requires_modifier: bool, - }, -} - -enum InlineCompletionHighlight {} - -#[derive(Debug, Clone)] -struct InlineDiagnostic { - message: SharedString, - group_id: usize, - is_primary: bool, - start: Point, - severity: DiagnosticSeverity, -} - -pub enum MenuInlineCompletionsPolicy { - Never, - ByProvider, -} - -pub enum EditPredictionPreview { - /// Modifier is not pressed - Inactive { released_too_fast: bool }, - /// Modifier pressed - Active { - since: Instant, - previous_scroll_position: Option, - }, -} - -impl EditPredictionPreview { - pub fn released_too_fast(&self) -> bool { - match self { - EditPredictionPreview::Inactive { released_too_fast } => *released_too_fast, - EditPredictionPreview::Active { .. } => false, - } - } - - pub fn set_previous_scroll_position(&mut self, scroll_position: Option) { - if let EditPredictionPreview::Active { - previous_scroll_position, - .. - } = self - { - *previous_scroll_position = scroll_position; - } - } -} - -pub struct ContextMenuOptions { - pub min_entries_visible: usize, - pub max_entries_visible: usize, - pub placement: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ContextMenuPlacement { - Above, - Below, -} - -#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug, Default)] -struct EditorActionId(usize); - -impl EditorActionId { - pub fn post_inc(&mut self) -> Self { - let answer = self.0; - - *self = Self(answer + 1); - - Self(answer) - } -} - -// type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor; -// type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option; - -type BackgroundHighlight = (fn(&ThemeColors) -> Hsla, Arc<[Range]>); -type GutterHighlight = (fn(&App) -> Hsla, Arc<[Range]>); - -#[derive(Default)] -struct ScrollbarMarkerState { - scrollbar_size: Size, - dirty: bool, - markers: Arc<[PaintQuad]>, - pending_refresh: Option>>, -} - -impl ScrollbarMarkerState { - fn should_refresh(&self, scrollbar_size: Size) -> bool { - self.pending_refresh.is_none() && (self.scrollbar_size != scrollbar_size || self.dirty) - } -} - -#[derive(Clone, Debug)] -struct RunnableTasks { - templates: Vec<(TaskSourceKind, TaskTemplate)>, - offset: multi_buffer::Anchor, - // We need the column at which the task context evaluation should take place (when we're spawning it via gutter). - column: u32, - // Values of all named captures, including those starting with '_' - extra_variables: HashMap, - // Full range of the tagged region. We use it to determine which `extra_variables` to grab for context resolution in e.g. a modal. - context_range: Range, -} - -impl RunnableTasks { - fn resolve<'a>( - &'a self, - cx: &'a task::TaskContext, - ) -> impl Iterator + 'a { - self.templates.iter().filter_map(|(kind, template)| { - template - .resolve_task(&kind.to_id_base(), cx) - .map(|task| (kind.clone(), task)) - }) - } -} - -#[derive(Clone)] -struct ResolvedTasks { - templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>, - position: Anchor, -} - -#[derive(Copy, Clone, Debug, PartialEq, PartialOrd)] -struct BufferOffset(usize); - -// Addons allow storing per-editor state in other crates (e.g. Vim) -pub trait Addon: 'static { - fn extend_key_context(&self, _: &mut KeyContext, _: &App) {} - - fn render_buffer_header_controls( - &self, - _: &ExcerptInfo, - _: &Window, - _: &App, - ) -> Option { - None - } - - fn to_any(&self) -> &dyn std::any::Any; - - fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> { - None - } -} - -/// A set of caret positions, registered when the editor was edited. -pub struct ChangeList { - changes: Vec>, - /// Currently "selected" change. - position: Option, -} - -impl ChangeList { - pub fn new() -> Self { - Self { - changes: Vec::new(), - position: None, - } - } - - /// Moves to the next change in the list (based on the direction given) and returns the caret positions for the next change. - /// If reaches the end of the list in the direction, returns the corresponding change until called for a different direction. - pub fn next_change(&mut self, count: usize, direction: Direction) -> Option<&[Anchor]> { - if self.changes.is_empty() { - return None; - } - - let prev = self.position.unwrap_or(self.changes.len()); - let next = if direction == Direction::Prev { - prev.saturating_sub(count) - } else { - (prev + count).min(self.changes.len() - 1) - }; - self.position = Some(next); - self.changes.get(next).map(|anchors| anchors.as_slice()) - } - - /// Adds a new change to the list, resetting the change list position. - pub fn push_to_change_list(&mut self, pop_state: bool, new_positions: Vec) { - self.position.take(); - if pop_state { - self.changes.pop(); - } - self.changes.push(new_positions.clone()); - } - - pub fn last(&self) -> Option<&[Anchor]> { - self.changes.last().map(|anchors| anchors.as_slice()) - } -} - -#[derive(Clone)] -struct InlineBlamePopoverState { - scroll_handle: ScrollHandle, - commit_message: Option, - markdown: Entity, -} - -struct InlineBlamePopover { - position: gpui::Point, - show_task: Option>, - hide_task: Option>, - popover_bounds: Option>, - popover_state: InlineBlamePopoverState, -} - -/// Represents a breakpoint indicator that shows up when hovering over lines in the gutter that don't have -/// a breakpoint on them. -#[derive(Clone, Copy, Debug)] -struct PhantomBreakpointIndicator { - display_row: DisplayRow, - /// There's a small debounce between hovering over the line and showing the indicator. - /// We don't want to show the indicator when moving the mouse from editor to e.g. project panel. - is_active: bool, - collides_with_existing_breakpoint: bool, -} -/// Zed's primary implementation of text input, allowing users to edit a [`MultiBuffer`]. -/// -/// See the [module level documentation](self) for more information. -pub struct Editor { - focus_handle: FocusHandle, - last_focused_descendant: Option, - /// The text buffer being edited - buffer: Entity, - /// Map of how text in the buffer should be displayed. - /// Handles soft wraps, folds, fake inlay text insertions, etc. - pub display_map: Entity, - pub selections: SelectionsCollection, - pub scroll_manager: ScrollManager, - /// When inline assist editors are linked, they all render cursors because - /// typing enters text into each of them, even the ones that aren't focused. - pub(crate) show_cursor_when_unfocused: bool, - columnar_selection_tail: Option, - add_selections_state: Option, - select_next_state: Option, - select_prev_state: Option, - selection_history: SelectionHistory, - autoclose_regions: Vec, - snippet_stack: InvalidationStack, - select_syntax_node_history: SelectSyntaxNodeHistory, - ime_transaction: Option, - active_diagnostics: ActiveDiagnostic, - show_inline_diagnostics: bool, - inline_diagnostics_update: Task<()>, - inline_diagnostics_enabled: bool, - inline_diagnostics: Vec<(Anchor, InlineDiagnostic)>, - soft_wrap_mode_override: Option, - hard_wrap: Option, - - // TODO: make this a access method - pub project: Option>, - semantics_provider: Option>, - completion_provider: Option>, - collaboration_hub: Option>, - blink_manager: Entity, - show_cursor_names: bool, - hovered_cursors: HashMap>, - pub show_local_selections: bool, - mode: EditorMode, - show_breadcrumbs: bool, - show_gutter: bool, - show_scrollbars: bool, - disable_scrolling: bool, - disable_expand_excerpt_buttons: bool, - show_line_numbers: Option, - use_relative_line_numbers: Option, - show_git_diff_gutter: Option, - show_code_actions: Option, - show_runnables: Option, - show_breakpoints: Option, - show_wrap_guides: Option, - show_indent_guides: Option, - placeholder_text: Option>, - highlight_order: usize, - highlighted_rows: HashMap>, - background_highlights: TreeMap, - gutter_highlights: TreeMap, - scrollbar_marker_state: ScrollbarMarkerState, - active_indent_guides_state: ActiveIndentGuidesState, - nav_history: Option, - context_menu: RefCell>, - context_menu_options: Option, - mouse_context_menu: Option, - completion_tasks: Vec<(CompletionId, Task>)>, - inline_blame_popover: Option, - signature_help_state: SignatureHelpState, - auto_signature_help: Option, - find_all_references_task_sources: Vec, - next_completion_id: CompletionId, - available_code_actions: Option<(Location, Rc<[AvailableCodeAction]>)>, - code_actions_task: Option>>, - quick_selection_highlight_task: Option<(Range, Task<()>)>, - debounced_selection_highlight_task: Option<(Range, Task<()>)>, - document_highlights_task: Option>, - linked_editing_range_task: Option>>, - linked_edit_ranges: linked_editing_ranges::LinkedEditingRanges, - pending_rename: Option, - searchable: bool, - cursor_shape: CursorShape, - current_line_highlight: Option, - collapse_matches: bool, - autoindent_mode: Option, - workspace: Option<(WeakEntity, Option)>, - input_enabled: bool, - use_modal_editing: bool, - read_only: bool, - leader_peer_id: Option, - remote_id: Option, - pub hover_state: HoverState, - pending_mouse_down: Option>>>, - gutter_hovered: bool, - hovered_link_state: Option, - edit_prediction_provider: Option, - code_action_providers: Vec>, - active_inline_completion: Option, - /// Used to prevent flickering as the user types while the menu is open - stale_inline_completion_in_menu: Option, - edit_prediction_settings: EditPredictionSettings, - inline_completions_hidden_for_vim_mode: bool, - show_inline_completions_override: Option, - menu_inline_completions_policy: MenuInlineCompletionsPolicy, - edit_prediction_preview: EditPredictionPreview, - edit_prediction_indent_conflict: bool, - edit_prediction_requires_modifier_in_indent_conflict: bool, - inlay_hint_cache: InlayHintCache, - next_inlay_id: usize, - _subscriptions: Vec, - pixel_position_of_newest_cursor: Option>, - gutter_dimensions: GutterDimensions, - style: Option, - text_style_refinement: Option, - next_editor_action_id: EditorActionId, - editor_actions: - Rc)>>>>, - use_autoclose: bool, - use_auto_surround: bool, - auto_replace_emoji_shortcode: bool, - jsx_tag_auto_close_enabled_in_any_buffer: bool, - show_git_blame_gutter: bool, - show_git_blame_inline: bool, - show_git_blame_inline_delay_task: Option>, - git_blame_inline_enabled: bool, - render_diff_hunk_controls: RenderDiffHunkControlsFn, - serialize_dirty_buffers: bool, - show_selection_menu: Option, - blame: Option>, - blame_subscription: Option, - custom_context_menu: Option< - Box< - dyn 'static - + Fn( - &mut Self, - DisplayPoint, - &mut Window, - &mut Context, - ) -> Option>, - >, - >, - last_bounds: Option>, - last_position_map: Option>, - expect_bounds_change: Option>, - tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>, - tasks_update_task: Option>, - breakpoint_store: Option>, - gutter_breakpoint_indicator: (Option, Option>), - in_project_search: bool, - previous_search_ranges: Option]>>, - breadcrumb_header: Option, - focused_block: Option, - next_scroll_position: NextScrollCursorCenterTopBottom, - addons: HashMap>, - registered_buffers: HashMap, - load_diff_task: Option>>, - selection_mark_mode: bool, - toggle_fold_multiple_buffers: Task<()>, - _scroll_cursor_center_top_bottom_task: Task<()>, - serialize_selections: Task<()>, - serialize_folds: Task<()>, - mouse_cursor_hidden: bool, - hide_mouse_mode: HideMouseMode, - pub change_list: ChangeList, - inline_value_cache: InlineValueCache, -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)] -enum NextScrollCursorCenterTopBottom { - #[default] - Center, - Top, - Bottom, -} - -impl NextScrollCursorCenterTopBottom { - fn next(&self) -> Self { - match self { - Self::Center => Self::Top, - Self::Top => Self::Bottom, - Self::Bottom => Self::Center, - } - } -} - -#[derive(Clone)] -pub struct EditorSnapshot { - pub mode: EditorMode, - show_gutter: bool, - show_line_numbers: Option, - show_git_diff_gutter: Option, - show_code_actions: Option, - show_runnables: Option, - show_breakpoints: Option, - git_blame_gutter_max_author_length: Option, - pub display_snapshot: DisplaySnapshot, - pub placeholder_text: Option>, - is_focused: bool, - scroll_anchor: ScrollAnchor, - ongoing_scroll: OngoingScroll, - current_line_highlight: CurrentLineHighlight, - gutter_hovered: bool, -} - -#[derive(Default, Debug, Clone, Copy)] -pub struct GutterDimensions { - pub left_padding: Pixels, - pub right_padding: Pixels, - pub width: Pixels, - pub margin: Pixels, - pub git_blame_entries_width: Option, -} - -impl GutterDimensions { - /// The full width of the space taken up by the gutter. - pub fn full_width(&self) -> Pixels { - self.margin + self.width - } - - /// The width of the space reserved for the fold indicators, - /// use alongside 'justify_end' and `gutter_width` to - /// right align content with the line numbers - pub fn fold_area_width(&self) -> Pixels { - self.margin + self.right_padding - } -} - -#[derive(Debug)] -pub struct RemoteSelection { - pub replica_id: ReplicaId, - pub selection: Selection, - pub cursor_shape: CursorShape, - pub peer_id: PeerId, - pub line_mode: bool, - pub participant_index: Option, - pub user_name: Option, -} - -#[derive(Clone, Debug)] -struct SelectionHistoryEntry { - selections: Arc<[Selection]>, - select_next_state: Option, - select_prev_state: Option, - add_selections_state: Option, -} - -enum SelectionHistoryMode { - Normal, - Undoing, - Redoing, -} - -#[derive(Clone, PartialEq, Eq, Hash)] -struct HoveredCursor { - replica_id: u16, - selection_id: usize, -} - -impl Default for SelectionHistoryMode { - fn default() -> Self { - Self::Normal - } -} - -#[derive(Default)] -struct SelectionHistory { - #[allow(clippy::type_complexity)] - selections_by_transaction: - HashMap]>, Option]>>)>, - mode: SelectionHistoryMode, - undo_stack: VecDeque, - redo_stack: VecDeque, -} - -impl SelectionHistory { - fn insert_transaction( - &mut self, - transaction_id: TransactionId, - selections: Arc<[Selection]>, - ) { - self.selections_by_transaction - .insert(transaction_id, (selections, None)); - } - - #[allow(clippy::type_complexity)] - fn transaction( - &self, - transaction_id: TransactionId, - ) -> Option<&(Arc<[Selection]>, Option]>>)> { - self.selections_by_transaction.get(&transaction_id) - } - - #[allow(clippy::type_complexity)] - fn transaction_mut( - &mut self, - transaction_id: TransactionId, - ) -> Option<&mut (Arc<[Selection]>, Option]>>)> { - self.selections_by_transaction.get_mut(&transaction_id) - } - - fn push(&mut self, entry: SelectionHistoryEntry) { - if !entry.selections.is_empty() { - match self.mode { - SelectionHistoryMode::Normal => { - self.push_undo(entry); - self.redo_stack.clear(); - } - SelectionHistoryMode::Undoing => self.push_redo(entry), - SelectionHistoryMode::Redoing => self.push_undo(entry), - } - } - } - - fn push_undo(&mut self, entry: SelectionHistoryEntry) { - if self - .undo_stack - .back() - .map_or(true, |e| e.selections != entry.selections) - { - self.undo_stack.push_back(entry); - if self.undo_stack.len() > MAX_SELECTION_HISTORY_LEN { - self.undo_stack.pop_front(); - } - } - } - - fn push_redo(&mut self, entry: SelectionHistoryEntry) { - if self - .redo_stack - .back() - .map_or(true, |e| e.selections != entry.selections) - { - self.redo_stack.push_back(entry); - if self.redo_stack.len() > MAX_SELECTION_HISTORY_LEN { - self.redo_stack.pop_front(); - } - } - } -} - -#[derive(Clone, Copy)] -pub struct RowHighlightOptions { - pub autoscroll: bool, - pub include_gutter: bool, -} - -impl Default for RowHighlightOptions { - fn default() -> Self { - Self { - autoscroll: Default::default(), - include_gutter: true, - } - } -} - -struct RowHighlight { - index: usize, - range: Range, - color: Hsla, - options: RowHighlightOptions, - type_id: TypeId, -} - -#[derive(Clone, Debug)] -struct AddSelectionsState { - above: bool, - stack: Vec, -} - -#[derive(Clone)] -struct SelectNextState { - query: AhoCorasick, - wordwise: bool, - done: bool, -} - -impl std::fmt::Debug for SelectNextState { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct(std::any::type_name::()) - .field("wordwise", &self.wordwise) - .field("done", &self.done) - .finish() - } -} - -#[derive(Debug)] -struct AutocloseRegion { - selection_id: usize, - range: Range, - pair: BracketPair, -} - -#[derive(Debug)] -struct SnippetState { - ranges: Vec>>, - active_index: usize, - choices: Vec>>, -} - -#[doc(hidden)] -pub struct RenameState { - pub range: Range, - pub old_name: Arc, - pub editor: Entity, - block_id: CustomBlockId, -} - -struct InvalidationStack(Vec); - -struct RegisteredInlineCompletionProvider { - provider: Arc, - _subscription: Subscription, -} - -#[derive(Debug, PartialEq, Eq)] -pub struct ActiveDiagnosticGroup { - pub active_range: Range, - pub active_message: String, - pub group_id: usize, - pub blocks: HashSet, -} - -#[derive(Debug, PartialEq, Eq)] - -pub(crate) enum ActiveDiagnostic { - None, - All, - Group(ActiveDiagnosticGroup), -} - -#[derive(Serialize, Deserialize, Clone, Debug)] -pub struct ClipboardSelection { - /// The number of bytes in this selection. - pub len: usize, - /// Whether this was a full-line selection. - pub is_entire_line: bool, - /// The indentation of the first line when this content was originally copied. - pub first_line_indent: u32, -} - -// selections, scroll behavior, was newest selection reversed -type SelectSyntaxNodeHistoryState = ( - Box<[Selection]>, - SelectSyntaxNodeScrollBehavior, - bool, -); - -#[derive(Default)] -struct SelectSyntaxNodeHistory { - stack: Vec, - // disable temporarily to allow changing selections without losing the stack - pub disable_clearing: bool, -} - -impl SelectSyntaxNodeHistory { - pub fn try_clear(&mut self) { - if !self.disable_clearing { - self.stack.clear(); - } - } - - pub fn push(&mut self, selection: SelectSyntaxNodeHistoryState) { - self.stack.push(selection); - } - - pub fn pop(&mut self) -> Option { - self.stack.pop() - } -} - -enum SelectSyntaxNodeScrollBehavior { - CursorTop, - FitSelection, - CursorBottom, -} - -#[derive(Debug)] -pub(crate) struct NavigationData { - cursor_anchor: Anchor, - cursor_position: Point, - scroll_anchor: ScrollAnchor, - scroll_top_row: u32, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum GotoDefinitionKind { - Symbol, - Declaration, - Type, - Implementation, -} - -#[derive(Debug, Clone)] -enum InlayHintRefreshReason { - ModifiersChanged(bool), - Toggle(bool), - SettingsChange(InlayHintSettings), - NewLinesShown, - BufferEdited(HashSet>), - RefreshRequested, - ExcerptsRemoved(Vec), -} - -impl InlayHintRefreshReason { - fn description(&self) -> &'static str { - match self { - Self::ModifiersChanged(_) => "modifiers changed", - Self::Toggle(_) => "toggle", - Self::SettingsChange(_) => "settings change", - Self::NewLinesShown => "new lines shown", - Self::BufferEdited(_) => "buffer edited", - Self::RefreshRequested => "refresh requested", - Self::ExcerptsRemoved(_) => "excerpts removed", - } - } -} - -pub enum FormatTarget { - Buffers, - Ranges(Vec>), -} - -pub(crate) struct FocusedBlock { - id: BlockId, - focus_handle: WeakFocusHandle, -} - -#[derive(Clone)] -enum JumpData { - MultiBufferRow { - row: MultiBufferRow, - line_offset_from_top: u32, - }, - MultiBufferPoint { - excerpt_id: ExcerptId, - position: Point, - anchor: text::Anchor, - line_offset_from_top: u32, - }, -} - -pub enum MultibufferSelectionMode { - First, - All, -} - -#[derive(Clone, Copy, Debug, Default)] -pub struct RewrapOptions { - pub override_language_settings: bool, - pub preserve_existing_whitespace: bool, -} - -impl Editor { - pub fn single_line(window: &mut Window, cx: &mut Context) -> Self { - let buffer = cx.new(|cx| Buffer::local("", cx)); - let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); - Self::new( - EditorMode::SingleLine { auto_width: false }, - buffer, - None, - window, - cx, - ) - } - - pub fn multi_line(window: &mut Window, cx: &mut Context) -> Self { - let buffer = cx.new(|cx| Buffer::local("", cx)); - let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); - Self::new(EditorMode::full(), buffer, None, window, cx) - } - - pub fn auto_width(window: &mut Window, cx: &mut Context) -> Self { - let buffer = cx.new(|cx| Buffer::local("", cx)); - let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); - Self::new( - EditorMode::SingleLine { auto_width: true }, - buffer, - None, - window, - cx, - ) - } - - pub fn auto_height(max_lines: usize, window: &mut Window, cx: &mut Context) -> Self { - let buffer = cx.new(|cx| Buffer::local("", cx)); - let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); - Self::new( - EditorMode::AutoHeight { max_lines }, - buffer, - None, - window, - cx, - ) - } - - pub fn for_buffer( - buffer: Entity, - project: Option>, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); - Self::new(EditorMode::full(), buffer, project, window, cx) - } - - pub fn for_multibuffer( - buffer: Entity, - project: Option>, - window: &mut Window, - cx: &mut Context, - ) -> Self { - Self::new(EditorMode::full(), buffer, project, window, cx) - } - - pub fn clone(&self, window: &mut Window, cx: &mut Context) -> Self { - let mut clone = Self::new( - self.mode, - self.buffer.clone(), - self.project.clone(), - window, - cx, - ); - self.display_map.update(cx, |display_map, cx| { - let snapshot = display_map.snapshot(cx); - clone.display_map.update(cx, |display_map, cx| { - display_map.set_state(&snapshot, cx); - }); - }); - clone.folds_did_change(cx); - clone.selections.clone_state(&self.selections); - clone.scroll_manager.clone_state(&self.scroll_manager); - clone.searchable = self.searchable; - clone.read_only = self.read_only; - clone - } - - pub fn new( - mode: EditorMode, - buffer: Entity, - project: Option>, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let style = window.text_style(); - let font_size = style.font_size.to_pixels(window.rem_size()); - let editor = cx.entity().downgrade(); - let fold_placeholder = FoldPlaceholder { - constrain_width: true, - render: Arc::new(move |fold_id, fold_range, cx| { - let editor = editor.clone(); - div() - .id(fold_id) - .bg(cx.theme().colors().ghost_element_background) - .hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) - .active(|style| style.bg(cx.theme().colors().ghost_element_active)) - .rounded_xs() - .size_full() - .cursor_pointer() - .child("⋯") - .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) - .on_click(move |_, _window, cx| { - editor - .update(cx, |editor, cx| { - editor.unfold_ranges( - &[fold_range.start..fold_range.end], - true, - false, - cx, - ); - cx.stop_propagation(); - }) - .ok(); - }) - .into_any() - }), - merge_adjacent: true, - ..Default::default() - }; - let display_map = cx.new(|cx| { - DisplayMap::new( - buffer.clone(), - style.font(), - font_size, - None, - FILE_HEADER_HEIGHT, - MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, - fold_placeholder, - cx, - ) - }); - - let selections = SelectionsCollection::new(display_map.clone(), buffer.clone()); - - let blink_manager = cx.new(|cx| BlinkManager::new(CURSOR_BLINK_INTERVAL, cx)); - - let soft_wrap_mode_override = matches!(mode, EditorMode::SingleLine { .. }) - .then(|| language_settings::SoftWrap::None); - - let mut project_subscriptions = Vec::new(); - if mode.is_full() { - if let Some(project) = project.as_ref() { - project_subscriptions.push(cx.subscribe_in( - project, - window, - |editor, _, event, window, cx| match event { - project::Event::RefreshCodeLens => { - // we always query lens with actions, without storing them, always refreshing them - } - project::Event::RefreshInlayHints => { - editor - .refresh_inlay_hints(InlayHintRefreshReason::RefreshRequested, cx); - } - project::Event::SnippetEdit(id, snippet_edits) => { - if let Some(buffer) = editor.buffer.read(cx).buffer(*id) { - let focus_handle = editor.focus_handle(cx); - if focus_handle.is_focused(window) { - let snapshot = buffer.read(cx).snapshot(); - for (range, snippet) in snippet_edits { - let editor_range = - language::range_from_lsp(*range).to_offset(&snapshot); - editor - .insert_snippet( - &[editor_range], - snippet.clone(), - window, - cx, - ) - .ok(); - } - } - } - } - _ => {} - }, - )); - if let Some(task_inventory) = project - .read(cx) - .task_store() - .read(cx) - .task_inventory() - .cloned() - { - project_subscriptions.push(cx.observe_in( - &task_inventory, - window, - |editor, _, window, cx| { - editor.tasks_update_task = Some(editor.refresh_runnables(window, cx)); - }, - )); - }; - - project_subscriptions.push(cx.subscribe_in( - &project.read(cx).breakpoint_store(), - window, - |editor, _, event, window, cx| match event { - BreakpointStoreEvent::ClearDebugLines => { - editor.clear_row_highlights::(); - editor.refresh_inline_values(cx); - } - BreakpointStoreEvent::SetDebugLine => { - if editor.go_to_active_debug_line(window, cx) { - cx.stop_propagation(); - } - - editor.refresh_inline_values(cx); - } - _ => {} - }, - )); - } - } - - let buffer_snapshot = buffer.read(cx).snapshot(cx); - - let inlay_hint_settings = - inlay_hint_settings(selections.newest_anchor().head(), &buffer_snapshot, cx); - let focus_handle = cx.focus_handle(); - cx.on_focus(&focus_handle, window, Self::handle_focus) - .detach(); - cx.on_focus_in(&focus_handle, window, Self::handle_focus_in) - .detach(); - cx.on_focus_out(&focus_handle, window, Self::handle_focus_out) - .detach(); - cx.on_blur(&focus_handle, window, Self::handle_blur) - .detach(); - - let show_indent_guides = if matches!(mode, EditorMode::SingleLine { .. }) { - Some(false) - } else { - None - }; - - let breakpoint_store = match (mode, project.as_ref()) { - (EditorMode::Full { .. }, Some(project)) => Some(project.read(cx).breakpoint_store()), - _ => None, - }; - - let mut code_action_providers = Vec::new(); - let mut load_uncommitted_diff = None; - if let Some(project) = project.clone() { - load_uncommitted_diff = Some( - get_uncommitted_diff_for_buffer( - &project, - buffer.read(cx).all_buffers(), - buffer.clone(), - cx, - ) - .shared(), - ); - code_action_providers.push(Rc::new(project) as Rc<_>); - } - - let mut this = Self { - focus_handle, - show_cursor_when_unfocused: false, - last_focused_descendant: None, - buffer: buffer.clone(), - display_map: display_map.clone(), - selections, - scroll_manager: ScrollManager::new(cx), - columnar_selection_tail: None, - add_selections_state: None, - select_next_state: None, - select_prev_state: None, - selection_history: Default::default(), - autoclose_regions: Default::default(), - snippet_stack: Default::default(), - select_syntax_node_history: SelectSyntaxNodeHistory::default(), - ime_transaction: Default::default(), - active_diagnostics: ActiveDiagnostic::None, - show_inline_diagnostics: ProjectSettings::get_global(cx).diagnostics.inline.enabled, - inline_diagnostics_update: Task::ready(()), - inline_diagnostics: Vec::new(), - soft_wrap_mode_override, - hard_wrap: None, - completion_provider: project.clone().map(|project| Box::new(project) as _), - semantics_provider: project.clone().map(|project| Rc::new(project) as _), - collaboration_hub: project.clone().map(|project| Box::new(project) as _), - project, - blink_manager: blink_manager.clone(), - show_local_selections: true, - show_scrollbars: true, - disable_scrolling: false, - mode, - show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs, - show_gutter: mode.is_full(), - show_line_numbers: None, - use_relative_line_numbers: None, - disable_expand_excerpt_buttons: false, - show_git_diff_gutter: None, - show_code_actions: None, - show_runnables: None, - show_breakpoints: None, - show_wrap_guides: None, - show_indent_guides, - placeholder_text: None, - highlight_order: 0, - highlighted_rows: HashMap::default(), - background_highlights: Default::default(), - gutter_highlights: TreeMap::default(), - scrollbar_marker_state: ScrollbarMarkerState::default(), - active_indent_guides_state: ActiveIndentGuidesState::default(), - nav_history: None, - context_menu: RefCell::new(None), - context_menu_options: None, - mouse_context_menu: None, - completion_tasks: Default::default(), - inline_blame_popover: Default::default(), - signature_help_state: SignatureHelpState::default(), - auto_signature_help: None, - find_all_references_task_sources: Vec::new(), - next_completion_id: 0, - next_inlay_id: 0, - code_action_providers, - available_code_actions: Default::default(), - code_actions_task: Default::default(), - quick_selection_highlight_task: Default::default(), - debounced_selection_highlight_task: Default::default(), - document_highlights_task: Default::default(), - linked_editing_range_task: Default::default(), - pending_rename: Default::default(), - searchable: true, - cursor_shape: EditorSettings::get_global(cx) - .cursor_shape - .unwrap_or_default(), - current_line_highlight: None, - autoindent_mode: Some(AutoindentMode::EachLine), - collapse_matches: false, - workspace: None, - input_enabled: true, - use_modal_editing: mode.is_full(), - read_only: false, - use_autoclose: true, - use_auto_surround: true, - auto_replace_emoji_shortcode: false, - jsx_tag_auto_close_enabled_in_any_buffer: false, - leader_peer_id: None, - remote_id: None, - hover_state: Default::default(), - pending_mouse_down: None, - hovered_link_state: Default::default(), - edit_prediction_provider: None, - active_inline_completion: None, - stale_inline_completion_in_menu: None, - edit_prediction_preview: EditPredictionPreview::Inactive { - released_too_fast: false, - }, - inline_diagnostics_enabled: mode.is_full(), - inline_value_cache: InlineValueCache::new(inlay_hint_settings.show_value_hints), - inlay_hint_cache: InlayHintCache::new(inlay_hint_settings), - - gutter_hovered: false, - pixel_position_of_newest_cursor: None, - last_bounds: None, - last_position_map: None, - expect_bounds_change: None, - gutter_dimensions: GutterDimensions::default(), - style: None, - show_cursor_names: false, - hovered_cursors: Default::default(), - next_editor_action_id: EditorActionId::default(), - editor_actions: Rc::default(), - inline_completions_hidden_for_vim_mode: false, - show_inline_completions_override: None, - menu_inline_completions_policy: MenuInlineCompletionsPolicy::ByProvider, - edit_prediction_settings: EditPredictionSettings::Disabled, - edit_prediction_indent_conflict: false, - edit_prediction_requires_modifier_in_indent_conflict: true, - custom_context_menu: None, - show_git_blame_gutter: false, - show_git_blame_inline: false, - show_selection_menu: None, - show_git_blame_inline_delay_task: None, - git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(), - render_diff_hunk_controls: Arc::new(render_diff_hunk_controls), - serialize_dirty_buffers: ProjectSettings::get_global(cx) - .session - .restore_unsaved_buffers, - blame: None, - blame_subscription: None, - tasks: Default::default(), - - breakpoint_store, - gutter_breakpoint_indicator: (None, None), - _subscriptions: vec![ - cx.observe(&buffer, Self::on_buffer_changed), - cx.subscribe_in(&buffer, window, Self::on_buffer_event), - cx.observe_in(&display_map, window, Self::on_display_map_changed), - cx.observe(&blink_manager, |_, _, cx| cx.notify()), - cx.observe_global_in::(window, Self::settings_changed), - observe_buffer_font_size_adjustment(cx, |_, cx| cx.notify()), - cx.observe_window_activation(window, |editor, window, cx| { - let active = window.is_window_active(); - editor.blink_manager.update(cx, |blink_manager, cx| { - if active { - blink_manager.enable(cx); - } else { - blink_manager.disable(cx); - } - }); - }), - ], - tasks_update_task: None, - linked_edit_ranges: Default::default(), - in_project_search: false, - previous_search_ranges: None, - breadcrumb_header: None, - focused_block: None, - next_scroll_position: NextScrollCursorCenterTopBottom::default(), - addons: HashMap::default(), - registered_buffers: HashMap::default(), - _scroll_cursor_center_top_bottom_task: Task::ready(()), - selection_mark_mode: false, - toggle_fold_multiple_buffers: Task::ready(()), - serialize_selections: Task::ready(()), - serialize_folds: Task::ready(()), - text_style_refinement: None, - load_diff_task: load_uncommitted_diff, - mouse_cursor_hidden: false, - hide_mouse_mode: EditorSettings::get_global(cx) - .hide_mouse - .unwrap_or_default(), - change_list: ChangeList::new(), - }; - if let Some(breakpoints) = this.breakpoint_store.as_ref() { - this._subscriptions - .push(cx.observe(breakpoints, |_, _, cx| { - cx.notify(); - })); - } - this.tasks_update_task = Some(this.refresh_runnables(window, cx)); - this._subscriptions.extend(project_subscriptions); - - this._subscriptions.push(cx.subscribe_in( - &cx.entity(), - window, - |editor, _, e: &EditorEvent, window, cx| match e { - EditorEvent::ScrollPositionChanged { local, .. } => { - if *local { - let new_anchor = editor.scroll_manager.anchor(); - let snapshot = editor.snapshot(window, cx); - editor.update_restoration_data(cx, move |data| { - data.scroll_position = ( - new_anchor.top_row(&snapshot.buffer_snapshot), - new_anchor.offset, - ); - }); - editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape); - editor.inline_blame_popover.take(); - } - } - EditorEvent::Edited { .. } => { - if !vim_enabled(cx) { - let (map, selections) = editor.selections.all_adjusted_display(cx); - let pop_state = editor - .change_list - .last() - .map(|previous| { - previous.len() == selections.len() - && previous.iter().enumerate().all(|(ix, p)| { - p.to_display_point(&map).row() - == selections[ix].head().row() - }) - }) - .unwrap_or(false); - let new_positions = selections - .into_iter() - .map(|s| map.display_point_to_anchor(s.head(), Bias::Left)) - .collect(); - editor - .change_list - .push_to_change_list(pop_state, new_positions); - } - } - _ => (), - }, - )); - - if let Some(dap_store) = this - .project - .as_ref() - .map(|project| project.read(cx).dap_store()) - { - let weak_editor = cx.weak_entity(); - - this._subscriptions - .push( - cx.observe_new::(move |_, _, cx| { - let session_entity = cx.entity(); - weak_editor - .update(cx, |editor, cx| { - editor._subscriptions.push( - cx.subscribe(&session_entity, Self::on_debug_session_event), - ); - }) - .ok(); - }), - ); - - for session in dap_store.read(cx).sessions().cloned().collect::>() { - this._subscriptions - .push(cx.subscribe(&session, Self::on_debug_session_event)); - } - } - - this.end_selection(window, cx); - this.scroll_manager.show_scrollbars(window, cx); - jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut this, &buffer, cx); - - if mode.is_full() { - let should_auto_hide_scrollbars = cx.should_auto_hide_scrollbars(); - cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars)); - - if this.git_blame_inline_enabled { - this.git_blame_inline_enabled = true; - this.start_git_blame_inline(false, window, cx); - } - - this.go_to_active_debug_line(window, cx); - - if let Some(buffer) = buffer.read(cx).as_singleton() { - if let Some(project) = this.project.as_ref() { - let handle = project.update(cx, |project, cx| { - project.register_buffer_with_language_servers(&buffer, cx) - }); - this.registered_buffers - .insert(buffer.read(cx).remote_id(), handle); - } - } - } - - this.report_editor_event("Editor Opened", None, cx); - this - } - - pub fn deploy_mouse_context_menu( - &mut self, - position: gpui::Point, - context_menu: Entity, - window: &mut Window, - cx: &mut Context, - ) { - self.mouse_context_menu = Some(MouseContextMenu::new( - self, - crate::mouse_context_menu::MenuPosition::PinnedToScreen(position), - context_menu, - window, - cx, - )); - } - - pub fn mouse_menu_is_focused(&self, window: &Window, cx: &App) -> bool { - self.mouse_context_menu - .as_ref() - .is_some_and(|menu| menu.context_menu.focus_handle(cx).is_focused(window)) - } - - fn key_context(&self, window: &Window, cx: &App) -> KeyContext { - self.key_context_internal(self.has_active_inline_completion(), window, cx) - } - - fn key_context_internal( - &self, - has_active_edit_prediction: bool, - window: &Window, - cx: &App, - ) -> KeyContext { - let mut key_context = KeyContext::new_with_defaults(); - key_context.add("Editor"); - let mode = match self.mode { - EditorMode::SingleLine { .. } => "single_line", - EditorMode::AutoHeight { .. } => "auto_height", - EditorMode::Full { .. } => "full", - }; - - if EditorSettings::jupyter_enabled(cx) { - key_context.add("jupyter"); - } - - key_context.set("mode", mode); - if self.pending_rename.is_some() { - key_context.add("renaming"); - } - - match self.context_menu.borrow().as_ref() { - Some(CodeContextMenu::Completions(_)) => { - key_context.add("menu"); - key_context.add("showing_completions"); - } - Some(CodeContextMenu::CodeActions(_)) => { - key_context.add("menu"); - key_context.add("showing_code_actions") - } - None => {} - } - - // Disable vim contexts when a sub-editor (e.g. rename/inline assistant) is focused. - if !self.focus_handle(cx).contains_focused(window, cx) - || (self.is_focused(window) || self.mouse_menu_is_focused(window, cx)) - { - for addon in self.addons.values() { - addon.extend_key_context(&mut key_context, cx) - } - } - - if let Some(singleton_buffer) = self.buffer.read(cx).as_singleton() { - if let Some(extension) = singleton_buffer - .read(cx) - .file() - .and_then(|file| file.path().extension()?.to_str()) - { - key_context.set("extension", extension.to_string()); - } - } else { - key_context.add("multibuffer"); - } - - if has_active_edit_prediction { - if self.edit_prediction_in_conflict() { - key_context.add(EDIT_PREDICTION_CONFLICT_KEY_CONTEXT); - } else { - key_context.add(EDIT_PREDICTION_KEY_CONTEXT); - key_context.add("copilot_suggestion"); - } - } - - if self.selection_mark_mode { - key_context.add("selection_mode"); - } - - key_context - } - - pub fn hide_mouse_cursor(&mut self, origin: &HideMouseCursorOrigin) { - self.mouse_cursor_hidden = match origin { - HideMouseCursorOrigin::TypingAction => { - matches!( - self.hide_mouse_mode, - HideMouseMode::OnTyping | HideMouseMode::OnTypingAndMovement - ) - } - HideMouseCursorOrigin::MovementAction => { - matches!(self.hide_mouse_mode, HideMouseMode::OnTypingAndMovement) - } - }; - } - - pub fn edit_prediction_in_conflict(&self) -> bool { - if !self.show_edit_predictions_in_menu() { - return false; - } - - let showing_completions = self - .context_menu - .borrow() - .as_ref() - .map_or(false, |context| { - matches!(context, CodeContextMenu::Completions(_)) - }); - - showing_completions - || self.edit_prediction_requires_modifier() - // Require modifier key when the cursor is on leading whitespace, to allow `tab` - // bindings to insert tab characters. - || (self.edit_prediction_requires_modifier_in_indent_conflict && self.edit_prediction_indent_conflict) - } - - pub fn accept_edit_prediction_keybind( - &self, - window: &Window, - cx: &App, - ) -> AcceptEditPredictionBinding { - let key_context = self.key_context_internal(true, window, cx); - let in_conflict = self.edit_prediction_in_conflict(); - - AcceptEditPredictionBinding( - window - .bindings_for_action_in_context(&AcceptEditPrediction, key_context) - .into_iter() - .filter(|binding| { - !in_conflict - || binding - .keystrokes() - .first() - .map_or(false, |keystroke| keystroke.modifiers.modified()) - }) - .rev() - .min_by_key(|binding| { - binding - .keystrokes() - .first() - .map_or(u8::MAX, |k| k.modifiers.number_of_modifiers()) - }), - ) - } - - pub fn new_file( - workspace: &mut Workspace, - _: &workspace::NewFile, - window: &mut Window, - cx: &mut Context, - ) { - Self::new_in_workspace(workspace, window, cx).detach_and_prompt_err( - "Failed to create buffer", - window, - cx, - |e, _, _| match e.error_code() { - ErrorCode::RemoteUpgradeRequired => Some(format!( - "The remote instance of Zed does not support this yet. It must be upgraded to {}", - e.error_tag("required").unwrap_or("the latest version") - )), - _ => None, - }, - ); - } - - pub fn new_in_workspace( - workspace: &mut Workspace, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - let project = workspace.project().clone(); - let create = project.update(cx, |project, cx| project.create_buffer(cx)); - - cx.spawn_in(window, async move |workspace, cx| { - let buffer = create.await?; - workspace.update_in(cx, |workspace, window, cx| { - let editor = - cx.new(|cx| Editor::for_buffer(buffer, Some(project.clone()), window, cx)); - workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx); - editor - }) - }) - } - - fn new_file_vertical( - workspace: &mut Workspace, - _: &workspace::NewFileSplitVertical, - window: &mut Window, - cx: &mut Context, - ) { - Self::new_file_in_direction(workspace, SplitDirection::vertical(cx), window, cx) - } - - fn new_file_horizontal( - workspace: &mut Workspace, - _: &workspace::NewFileSplitHorizontal, - window: &mut Window, - cx: &mut Context, - ) { - Self::new_file_in_direction(workspace, SplitDirection::horizontal(cx), window, cx) - } - - fn new_file_in_direction( - workspace: &mut Workspace, - direction: SplitDirection, - window: &mut Window, - cx: &mut Context, - ) { - let project = workspace.project().clone(); - let create = project.update(cx, |project, cx| project.create_buffer(cx)); - - cx.spawn_in(window, async move |workspace, cx| { - let buffer = create.await?; - workspace.update_in(cx, move |workspace, window, cx| { - workspace.split_item( - direction, - Box::new( - cx.new(|cx| Editor::for_buffer(buffer, Some(project.clone()), window, cx)), - ), - window, - cx, - ) - })?; - anyhow::Ok(()) - }) - .detach_and_prompt_err("Failed to create buffer", window, cx, |e, _, _| { - match e.error_code() { - ErrorCode::RemoteUpgradeRequired => Some(format!( - "The remote instance of Zed does not support this yet. It must be upgraded to {}", - e.error_tag("required").unwrap_or("the latest version") - )), - _ => None, - } - }); - } - - pub fn leader_peer_id(&self) -> Option { - self.leader_peer_id - } - - pub fn buffer(&self) -> &Entity { - &self.buffer - } - - pub fn workspace(&self) -> Option> { - self.workspace.as_ref()?.0.upgrade() - } - - pub fn title<'a>(&self, cx: &'a App) -> Cow<'a, str> { - self.buffer().read(cx).title(cx) - } - - pub fn snapshot(&self, window: &mut Window, cx: &mut App) -> EditorSnapshot { - let git_blame_gutter_max_author_length = self - .render_git_blame_gutter(cx) - .then(|| { - if let Some(blame) = self.blame.as_ref() { - let max_author_length = - blame.update(cx, |blame, cx| blame.max_author_length(cx)); - Some(max_author_length) - } else { - None - } - }) - .flatten(); - - EditorSnapshot { - mode: self.mode, - show_gutter: self.show_gutter, - show_line_numbers: self.show_line_numbers, - show_git_diff_gutter: self.show_git_diff_gutter, - show_code_actions: self.show_code_actions, - show_runnables: self.show_runnables, - show_breakpoints: self.show_breakpoints, - git_blame_gutter_max_author_length, - display_snapshot: self.display_map.update(cx, |map, cx| map.snapshot(cx)), - scroll_anchor: self.scroll_manager.anchor(), - ongoing_scroll: self.scroll_manager.ongoing_scroll(), - placeholder_text: self.placeholder_text.clone(), - is_focused: self.focus_handle.is_focused(window), - current_line_highlight: self - .current_line_highlight - .unwrap_or_else(|| EditorSettings::get_global(cx).current_line_highlight), - gutter_hovered: self.gutter_hovered, - } - } - - pub fn language_at(&self, point: T, cx: &App) -> Option> { - self.buffer.read(cx).language_at(point, cx) - } - - pub fn file_at(&self, point: T, cx: &App) -> Option> { - self.buffer.read(cx).read(cx).file_at(point).cloned() - } - - pub fn active_excerpt( - &self, - cx: &App, - ) -> Option<(ExcerptId, Entity, Range)> { - self.buffer - .read(cx) - .excerpt_containing(self.selections.newest_anchor().head(), cx) - } - - pub fn mode(&self) -> EditorMode { - self.mode - } - - pub fn set_mode(&mut self, mode: EditorMode) { - self.mode = mode; - } - - pub fn collaboration_hub(&self) -> Option<&dyn CollaborationHub> { - self.collaboration_hub.as_deref() - } - - pub fn set_collaboration_hub(&mut self, hub: Box) { - self.collaboration_hub = Some(hub); - } - - pub fn set_in_project_search(&mut self, in_project_search: bool) { - self.in_project_search = in_project_search; - } - - pub fn set_custom_context_menu( - &mut self, - f: impl 'static - + Fn( - &mut Self, - DisplayPoint, - &mut Window, - &mut Context, - ) -> Option>, - ) { - self.custom_context_menu = Some(Box::new(f)) - } - - pub fn set_completion_provider(&mut self, provider: Option>) { - self.completion_provider = provider; - } - - pub fn semantics_provider(&self) -> Option> { - self.semantics_provider.clone() - } - - pub fn set_semantics_provider(&mut self, provider: Option>) { - self.semantics_provider = provider; - } - - pub fn set_edit_prediction_provider( - &mut self, - provider: Option>, - window: &mut Window, - cx: &mut Context, - ) where - T: EditPredictionProvider, - { - self.edit_prediction_provider = - provider.map(|provider| RegisteredInlineCompletionProvider { - _subscription: cx.observe_in(&provider, window, |this, _, window, cx| { - if this.focus_handle.is_focused(window) { - this.update_visible_inline_completion(window, cx); - } - }), - provider: Arc::new(provider), - }); - self.update_edit_prediction_settings(cx); - self.refresh_inline_completion(false, false, window, cx); - } - - pub fn placeholder_text(&self) -> Option<&str> { - self.placeholder_text.as_deref() - } - - pub fn set_placeholder_text( - &mut self, - placeholder_text: impl Into>, - cx: &mut Context, - ) { - let placeholder_text = Some(placeholder_text.into()); - if self.placeholder_text != placeholder_text { - self.placeholder_text = placeholder_text; - cx.notify(); - } - } - - pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape, cx: &mut Context) { - self.cursor_shape = cursor_shape; - - // Disrupt blink for immediate user feedback that the cursor shape has changed - self.blink_manager.update(cx, BlinkManager::show_cursor); - - cx.notify(); - } - - pub fn set_current_line_highlight( - &mut self, - current_line_highlight: Option, - ) { - self.current_line_highlight = current_line_highlight; - } - - pub fn set_collapse_matches(&mut self, collapse_matches: bool) { - self.collapse_matches = collapse_matches; - } - - fn register_buffers_with_language_servers(&mut self, cx: &mut Context) { - let buffers = self.buffer.read(cx).all_buffers(); - let Some(project) = self.project.as_ref() else { - return; - }; - project.update(cx, |project, cx| { - for buffer in buffers { - self.registered_buffers - .entry(buffer.read(cx).remote_id()) - .or_insert_with(|| project.register_buffer_with_language_servers(&buffer, cx)); - } - }) - } - - pub fn range_for_match(&self, range: &Range) -> Range { - if self.collapse_matches { - return range.start..range.start; - } - range.clone() - } - - pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut Context) { - if self.display_map.read(cx).clip_at_line_ends != clip { - self.display_map - .update(cx, |map, _| map.clip_at_line_ends = clip); - } - } - - pub fn set_input_enabled(&mut self, input_enabled: bool) { - self.input_enabled = input_enabled; - } - - pub fn set_inline_completions_hidden_for_vim_mode( - &mut self, - hidden: bool, - window: &mut Window, - cx: &mut Context, - ) { - if hidden != self.inline_completions_hidden_for_vim_mode { - self.inline_completions_hidden_for_vim_mode = hidden; - if hidden { - self.update_visible_inline_completion(window, cx); - } else { - self.refresh_inline_completion(true, false, window, cx); - } - } - } - - pub fn set_menu_inline_completions_policy(&mut self, value: MenuInlineCompletionsPolicy) { - self.menu_inline_completions_policy = value; - } - - pub fn set_autoindent(&mut self, autoindent: bool) { - if autoindent { - self.autoindent_mode = Some(AutoindentMode::EachLine); - } else { - self.autoindent_mode = None; - } - } - - pub fn read_only(&self, cx: &App) -> bool { - self.read_only || self.buffer.read(cx).read_only() - } - - pub fn set_read_only(&mut self, read_only: bool) { - self.read_only = read_only; - } - - pub fn set_use_autoclose(&mut self, autoclose: bool) { - self.use_autoclose = autoclose; - } - - pub fn set_use_auto_surround(&mut self, auto_surround: bool) { - self.use_auto_surround = auto_surround; - } - - pub fn set_auto_replace_emoji_shortcode(&mut self, auto_replace: bool) { - self.auto_replace_emoji_shortcode = auto_replace; - } - - pub fn toggle_edit_predictions( - &mut self, - _: &ToggleEditPrediction, - window: &mut Window, - cx: &mut Context, - ) { - if self.show_inline_completions_override.is_some() { - self.set_show_edit_predictions(None, window, cx); - } else { - let show_edit_predictions = !self.edit_predictions_enabled(); - self.set_show_edit_predictions(Some(show_edit_predictions), window, cx); - } - } - - pub fn set_show_edit_predictions( - &mut self, - show_edit_predictions: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.show_inline_completions_override = show_edit_predictions; - self.update_edit_prediction_settings(cx); - - if let Some(false) = show_edit_predictions { - self.discard_inline_completion(false, cx); - } else { - self.refresh_inline_completion(false, true, window, cx); - } - } - - fn inline_completions_disabled_in_scope( - &self, - buffer: &Entity, - buffer_position: language::Anchor, - cx: &App, - ) -> bool { - let snapshot = buffer.read(cx).snapshot(); - let settings = snapshot.settings_at(buffer_position, cx); - - let Some(scope) = snapshot.language_scope_at(buffer_position) else { - return false; - }; - - scope.override_name().map_or(false, |scope_name| { - settings - .edit_predictions_disabled_in - .iter() - .any(|s| s == scope_name) - }) - } - - pub fn set_use_modal_editing(&mut self, to: bool) { - self.use_modal_editing = to; - } - - pub fn use_modal_editing(&self) -> bool { - self.use_modal_editing - } - - fn selections_did_change( - &mut self, - local: bool, - old_cursor_position: &Anchor, - show_completions: bool, - window: &mut Window, - cx: &mut Context, - ) { - window.invalidate_character_coordinates(); - - // Copy selections to primary selection buffer - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - if local { - let selections = self.selections.all::(cx); - let buffer_handle = self.buffer.read(cx).read(cx); - - let mut text = String::new(); - for (index, selection) in selections.iter().enumerate() { - let text_for_selection = buffer_handle - .text_for_range(selection.start..selection.end) - .collect::(); - - text.push_str(&text_for_selection); - if index != selections.len() - 1 { - text.push('\n'); - } - } - - if !text.is_empty() { - cx.write_to_primary(ClipboardItem::new_string(text)); - } - } - - if self.focus_handle.is_focused(window) && self.leader_peer_id.is_none() { - self.buffer.update(cx, |buffer, cx| { - buffer.set_active_selections( - &self.selections.disjoint_anchors(), - self.selections.line_mode, - self.cursor_shape, - cx, - ) - }); - } - let display_map = self - .display_map - .update(cx, |display_map, cx| display_map.snapshot(cx)); - let buffer = &display_map.buffer_snapshot; - self.add_selections_state = None; - self.select_next_state = None; - self.select_prev_state = None; - self.select_syntax_node_history.try_clear(); - self.invalidate_autoclose_regions(&self.selections.disjoint_anchors(), buffer); - self.snippet_stack - .invalidate(&self.selections.disjoint_anchors(), buffer); - self.take_rename(false, window, cx); - - let new_cursor_position = self.selections.newest_anchor().head(); - - self.push_to_nav_history( - *old_cursor_position, - Some(new_cursor_position.to_point(buffer)), - false, - cx, - ); - - if local { - let new_cursor_position = self.selections.newest_anchor().head(); - let mut context_menu = self.context_menu.borrow_mut(); - let completion_menu = match context_menu.as_ref() { - Some(CodeContextMenu::Completions(menu)) => Some(menu), - _ => { - *context_menu = None; - None - } - }; - if let Some(buffer_id) = new_cursor_position.buffer_id { - if !self.registered_buffers.contains_key(&buffer_id) { - if let Some(project) = self.project.as_ref() { - project.update(cx, |project, cx| { - let Some(buffer) = self.buffer.read(cx).buffer(buffer_id) else { - return; - }; - self.registered_buffers.insert( - buffer_id, - project.register_buffer_with_language_servers(&buffer, cx), - ); - }) - } - } - } - - if let Some(completion_menu) = completion_menu { - let cursor_position = new_cursor_position.to_offset(buffer); - let (word_range, kind) = - buffer.surrounding_word(completion_menu.initial_position, true); - if kind == Some(CharKind::Word) - && word_range.to_inclusive().contains(&cursor_position) - { - let mut completion_menu = completion_menu.clone(); - drop(context_menu); - - let query = Self::completion_query(buffer, cursor_position); - cx.spawn(async move |this, cx| { - completion_menu - .filter(query.as_deref(), cx.background_executor().clone()) - .await; - - this.update(cx, |this, cx| { - let mut context_menu = this.context_menu.borrow_mut(); - let Some(CodeContextMenu::Completions(menu)) = context_menu.as_ref() - else { - return; - }; - - if menu.id > completion_menu.id { - return; - } - - *context_menu = Some(CodeContextMenu::Completions(completion_menu)); - drop(context_menu); - cx.notify(); - }) - }) - .detach(); - - if show_completions { - self.show_completions(&ShowCompletions { trigger: None }, window, cx); - } - } else { - drop(context_menu); - self.hide_context_menu(window, cx); - } - } else { - drop(context_menu); - } - - hide_hover(self, cx); - - if old_cursor_position.to_display_point(&display_map).row() - != new_cursor_position.to_display_point(&display_map).row() - { - self.available_code_actions.take(); - } - self.refresh_code_actions(window, cx); - self.refresh_document_highlights(cx); - self.refresh_selected_text_highlights(false, window, cx); - refresh_matching_bracket_highlights(self, window, cx); - self.update_visible_inline_completion(window, cx); - self.edit_prediction_requires_modifier_in_indent_conflict = true; - linked_editing_ranges::refresh_linked_ranges(self, window, cx); - self.inline_blame_popover.take(); - if self.git_blame_inline_enabled { - self.start_inline_blame_timer(window, cx); - } - } - - self.blink_manager.update(cx, BlinkManager::pause_blinking); - cx.emit(EditorEvent::SelectionsChanged { local }); - - let selections = &self.selections.disjoint; - if selections.len() == 1 { - cx.emit(SearchEvent::ActiveMatchChanged) - } - if local { - if let Some((_, _, buffer_snapshot)) = buffer.as_singleton() { - let inmemory_selections = selections - .iter() - .map(|s| { - text::ToPoint::to_point(&s.range().start.text_anchor, buffer_snapshot) - ..text::ToPoint::to_point(&s.range().end.text_anchor, buffer_snapshot) - }) - .collect(); - self.update_restoration_data(cx, |data| { - data.selections = inmemory_selections; - }); - - if WorkspaceSettings::get(None, cx).restore_on_startup - != RestoreOnStartupBehavior::None - { - if let Some(workspace_id) = - self.workspace.as_ref().and_then(|workspace| workspace.1) - { - let snapshot = self.buffer().read(cx).snapshot(cx); - let selections = selections.clone(); - let background_executor = cx.background_executor().clone(); - let editor_id = cx.entity().entity_id().as_u64() as ItemId; - self.serialize_selections = cx.background_spawn(async move { - background_executor.timer(SERIALIZATION_THROTTLE_TIME).await; - let db_selections = selections - .iter() - .map(|selection| { - ( - selection.start.to_offset(&snapshot), - selection.end.to_offset(&snapshot), - ) - }) - .collect(); - - DB.save_editor_selections(editor_id, workspace_id, db_selections) - .await - .with_context(|| format!("persisting editor selections for editor {editor_id}, workspace {workspace_id:?}")) - .log_err(); - }); - } - } - } - } - - cx.notify(); - } - - fn folds_did_change(&mut self, cx: &mut Context) { - use text::ToOffset as _; - use text::ToPoint as _; - - if WorkspaceSettings::get(None, cx).restore_on_startup == RestoreOnStartupBehavior::None { - return; - } - - let Some(singleton) = self.buffer().read(cx).as_singleton() else { - return; - }; - - let snapshot = singleton.read(cx).snapshot(); - let inmemory_folds = self.display_map.update(cx, |display_map, cx| { - let display_snapshot = display_map.snapshot(cx); - - display_snapshot - .folds_in_range(0..display_snapshot.buffer_snapshot.len()) - .map(|fold| { - fold.range.start.text_anchor.to_point(&snapshot) - ..fold.range.end.text_anchor.to_point(&snapshot) - }) - .collect() - }); - self.update_restoration_data(cx, |data| { - data.folds = inmemory_folds; - }); - - let Some(workspace_id) = self.workspace.as_ref().and_then(|workspace| workspace.1) else { - return; - }; - let background_executor = cx.background_executor().clone(); - let editor_id = cx.entity().entity_id().as_u64() as ItemId; - let db_folds = self.display_map.update(cx, |display_map, cx| { - display_map - .snapshot(cx) - .folds_in_range(0..snapshot.len()) - .map(|fold| { - ( - fold.range.start.text_anchor.to_offset(&snapshot), - fold.range.end.text_anchor.to_offset(&snapshot), - ) - }) - .collect() - }); - self.serialize_folds = cx.background_spawn(async move { - background_executor.timer(SERIALIZATION_THROTTLE_TIME).await; - DB.save_editor_folds(editor_id, workspace_id, db_folds) - .await - .with_context(|| { - format!( - "persisting editor folds for editor {editor_id}, workspace {workspace_id:?}" - ) - }) - .log_err(); - }); - } - - pub fn sync_selections( - &mut self, - other: Entity, - cx: &mut Context, - ) -> gpui::Subscription { - let other_selections = other.read(cx).selections.disjoint.to_vec(); - self.selections.change_with(cx, |selections| { - selections.select_anchors(other_selections); - }); - - let other_subscription = - cx.subscribe(&other, |this, other, other_evt, cx| match other_evt { - EditorEvent::SelectionsChanged { local: true } => { - let other_selections = other.read(cx).selections.disjoint.to_vec(); - if other_selections.is_empty() { - return; - } - this.selections.change_with(cx, |selections| { - selections.select_anchors(other_selections); - }); - } - _ => {} - }); - - let this_subscription = - cx.subscribe_self::(move |this, this_evt, cx| match this_evt { - EditorEvent::SelectionsChanged { local: true } => { - let these_selections = this.selections.disjoint.to_vec(); - if these_selections.is_empty() { - return; - } - other.update(cx, |other_editor, cx| { - other_editor.selections.change_with(cx, |selections| { - selections.select_anchors(these_selections); - }) - }); - } - _ => {} - }); - - Subscription::join(other_subscription, this_subscription) - } - - pub fn change_selections( - &mut self, - autoscroll: Option, - window: &mut Window, - cx: &mut Context, - change: impl FnOnce(&mut MutableSelectionsCollection<'_>) -> R, - ) -> R { - self.change_selections_inner(autoscroll, true, window, cx, change) - } - - fn change_selections_inner( - &mut self, - autoscroll: Option, - request_completions: bool, - window: &mut Window, - cx: &mut Context, - change: impl FnOnce(&mut MutableSelectionsCollection<'_>) -> R, - ) -> R { - let old_cursor_position = self.selections.newest_anchor().head(); - self.push_to_selection_history(); - - let (changed, result) = self.selections.change_with(cx, change); - - if changed { - if let Some(autoscroll) = autoscroll { - self.request_autoscroll(autoscroll, cx); - } - self.selections_did_change(true, &old_cursor_position, request_completions, window, cx); - - if self.should_open_signature_help_automatically( - &old_cursor_position, - self.signature_help_state.backspace_pressed(), - cx, - ) { - self.show_signature_help(&ShowSignatureHelp, window, cx); - } - self.signature_help_state.set_backspace_pressed(false); - } - - result - } - - pub fn edit(&mut self, edits: I, cx: &mut Context) - where - I: IntoIterator, T)>, - S: ToOffset, - T: Into>, - { - if self.read_only(cx) { - return; - } - - self.buffer - .update(cx, |buffer, cx| buffer.edit(edits, None, cx)); - } - - pub fn edit_with_autoindent(&mut self, edits: I, cx: &mut Context) - where - I: IntoIterator, T)>, - S: ToOffset, - T: Into>, - { - if self.read_only(cx) { - return; - } - - self.buffer.update(cx, |buffer, cx| { - buffer.edit(edits, self.autoindent_mode.clone(), cx) - }); - } - - pub fn edit_with_block_indent( - &mut self, - edits: I, - original_indent_columns: Vec>, - cx: &mut Context, - ) where - I: IntoIterator, T)>, - S: ToOffset, - T: Into>, - { - if self.read_only(cx) { - return; - } - - self.buffer.update(cx, |buffer, cx| { - buffer.edit( - edits, - Some(AutoindentMode::Block { - original_indent_columns, - }), - cx, - ) - }); - } - - fn select(&mut self, phase: SelectPhase, window: &mut Window, cx: &mut Context) { - self.hide_context_menu(window, cx); - - match phase { - SelectPhase::Begin { - position, - add, - click_count, - } => self.begin_selection(position, add, click_count, window, cx), - SelectPhase::BeginColumnar { - position, - goal_column, - reset, - } => self.begin_columnar_selection(position, goal_column, reset, window, cx), - SelectPhase::Extend { - position, - click_count, - } => self.extend_selection(position, click_count, window, cx), - SelectPhase::Update { - position, - goal_column, - scroll_delta, - } => self.update_selection(position, goal_column, scroll_delta, window, cx), - SelectPhase::End => self.end_selection(window, cx), - } - } - - fn extend_selection( - &mut self, - position: DisplayPoint, - click_count: usize, - window: &mut Window, - cx: &mut Context, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let tail = self.selections.newest::(cx).tail(); - self.begin_selection(position, false, click_count, window, cx); - - let position = position.to_offset(&display_map, Bias::Left); - let tail_anchor = display_map.buffer_snapshot.anchor_before(tail); - - let mut pending_selection = self - .selections - .pending_anchor() - .expect("extend_selection not called with pending selection"); - if position >= tail { - pending_selection.start = tail_anchor; - } else { - pending_selection.end = tail_anchor; - pending_selection.reversed = true; - } - - let mut pending_mode = self.selections.pending_mode().unwrap(); - match &mut pending_mode { - SelectMode::Word(range) | SelectMode::Line(range) => *range = tail_anchor..tail_anchor, - _ => {} - } - - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.set_pending(pending_selection, pending_mode) - }); - } - - fn begin_selection( - &mut self, - position: DisplayPoint, - add: bool, - click_count: usize, - window: &mut Window, - cx: &mut Context, - ) { - if !self.focus_handle.is_focused(window) { - self.last_focused_descendant = None; - window.focus(&self.focus_handle); - } - - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = &display_map.buffer_snapshot; - let newest_selection = self.selections.newest_anchor().clone(); - let position = display_map.clip_point(position, Bias::Left); - - let start; - let end; - let mode; - let mut auto_scroll; - match click_count { - 1 => { - start = buffer.anchor_before(position.to_point(&display_map)); - end = start; - mode = SelectMode::Character; - auto_scroll = true; - } - 2 => { - let range = movement::surrounding_word(&display_map, position); - start = buffer.anchor_before(range.start.to_point(&display_map)); - end = buffer.anchor_before(range.end.to_point(&display_map)); - mode = SelectMode::Word(start..end); - auto_scroll = true; - } - 3 => { - let position = display_map - .clip_point(position, Bias::Left) - .to_point(&display_map); - let line_start = display_map.prev_line_boundary(position).0; - let next_line_start = buffer.clip_point( - display_map.next_line_boundary(position).0 + Point::new(1, 0), - Bias::Left, - ); - start = buffer.anchor_before(line_start); - end = buffer.anchor_before(next_line_start); - mode = SelectMode::Line(start..end); - auto_scroll = true; - } - _ => { - start = buffer.anchor_before(0); - end = buffer.anchor_before(buffer.len()); - mode = SelectMode::All; - auto_scroll = false; - } - } - auto_scroll &= EditorSettings::get_global(cx).autoscroll_on_clicks; - - let point_to_delete: Option = { - let selected_points: Vec> = - self.selections.disjoint_in_range(start..end, cx); - - if !add || click_count > 1 { - None - } else if !selected_points.is_empty() { - Some(selected_points[0].id) - } else { - let clicked_point_already_selected = - self.selections.disjoint.iter().find(|selection| { - selection.start.to_point(buffer) == start.to_point(buffer) - || selection.end.to_point(buffer) == end.to_point(buffer) - }); - - clicked_point_already_selected.map(|selection| selection.id) - } - }; - - let selections_count = self.selections.count(); - - self.change_selections(auto_scroll.then(Autoscroll::newest), window, cx, |s| { - if let Some(point_to_delete) = point_to_delete { - s.delete(point_to_delete); - - if selections_count == 1 { - s.set_pending_anchor_range(start..end, mode); - } - } else { - if !add { - s.clear_disjoint(); - } else if click_count > 1 { - s.delete(newest_selection.id) - } - - s.set_pending_anchor_range(start..end, mode); - } - }); - } - - fn begin_columnar_selection( - &mut self, - position: DisplayPoint, - goal_column: u32, - reset: bool, - window: &mut Window, - cx: &mut Context, - ) { - if !self.focus_handle.is_focused(window) { - self.last_focused_descendant = None; - window.focus(&self.focus_handle); - } - - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - - if reset { - let pointer_position = display_map - .buffer_snapshot - .anchor_before(position.to_point(&display_map)); - - self.change_selections(Some(Autoscroll::newest()), window, cx, |s| { - s.clear_disjoint(); - s.set_pending_anchor_range( - pointer_position..pointer_position, - SelectMode::Character, - ); - }); - } - - let tail = self.selections.newest::(cx).tail(); - self.columnar_selection_tail = Some(display_map.buffer_snapshot.anchor_before(tail)); - - if !reset { - self.select_columns( - tail.to_display_point(&display_map), - position, - goal_column, - &display_map, - window, - cx, - ); - } - } - - fn update_selection( - &mut self, - position: DisplayPoint, - goal_column: u32, - scroll_delta: gpui::Point, - window: &mut Window, - cx: &mut Context, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - - if let Some(tail) = self.columnar_selection_tail.as_ref() { - let tail = tail.to_display_point(&display_map); - self.select_columns(tail, position, goal_column, &display_map, window, cx); - } else if let Some(mut pending) = self.selections.pending_anchor() { - let buffer = self.buffer.read(cx).snapshot(cx); - let head; - let tail; - let mode = self.selections.pending_mode().unwrap(); - match &mode { - SelectMode::Character => { - head = position.to_point(&display_map); - tail = pending.tail().to_point(&buffer); - } - SelectMode::Word(original_range) => { - let original_display_range = original_range.start.to_display_point(&display_map) - ..original_range.end.to_display_point(&display_map); - let original_buffer_range = original_display_range.start.to_point(&display_map) - ..original_display_range.end.to_point(&display_map); - if movement::is_inside_word(&display_map, position) - || original_display_range.contains(&position) - { - let word_range = movement::surrounding_word(&display_map, position); - if word_range.start < original_display_range.start { - head = word_range.start.to_point(&display_map); - } else { - head = word_range.end.to_point(&display_map); - } - } else { - head = position.to_point(&display_map); - } - - if head <= original_buffer_range.start { - tail = original_buffer_range.end; - } else { - tail = original_buffer_range.start; - } - } - SelectMode::Line(original_range) => { - let original_range = original_range.to_point(&display_map.buffer_snapshot); - - let position = display_map - .clip_point(position, Bias::Left) - .to_point(&display_map); - let line_start = display_map.prev_line_boundary(position).0; - let next_line_start = buffer.clip_point( - display_map.next_line_boundary(position).0 + Point::new(1, 0), - Bias::Left, - ); - - if line_start < original_range.start { - head = line_start - } else { - head = next_line_start - } - - if head <= original_range.start { - tail = original_range.end; - } else { - tail = original_range.start; - } - } - SelectMode::All => { - return; - } - }; - - if head < tail { - pending.start = buffer.anchor_before(head); - pending.end = buffer.anchor_before(tail); - pending.reversed = true; - } else { - pending.start = buffer.anchor_before(tail); - pending.end = buffer.anchor_before(head); - pending.reversed = false; - } - - self.change_selections(None, window, cx, |s| { - s.set_pending(pending, mode); - }); - } else { - log::error!("update_selection dispatched with no pending selection"); - return; - } - - self.apply_scroll_delta(scroll_delta, window, cx); - cx.notify(); - } - - fn end_selection(&mut self, window: &mut Window, cx: &mut Context) { - self.columnar_selection_tail.take(); - if self.selections.pending_anchor().is_some() { - let selections = self.selections.all::(cx); - self.change_selections(None, window, cx, |s| { - s.select(selections); - s.clear_pending(); - }); - } - } - - fn select_columns( - &mut self, - tail: DisplayPoint, - head: DisplayPoint, - goal_column: u32, - display_map: &DisplaySnapshot, - window: &mut Window, - cx: &mut Context, - ) { - let start_row = cmp::min(tail.row(), head.row()); - let end_row = cmp::max(tail.row(), head.row()); - let start_column = cmp::min(tail.column(), goal_column); - let end_column = cmp::max(tail.column(), goal_column); - let reversed = start_column < tail.column(); - - let selection_ranges = (start_row.0..=end_row.0) - .map(DisplayRow) - .filter_map(|row| { - if start_column <= display_map.line_len(row) && !display_map.is_block_line(row) { - let start = display_map - .clip_point(DisplayPoint::new(row, start_column), Bias::Left) - .to_point(display_map); - let end = display_map - .clip_point(DisplayPoint::new(row, end_column), Bias::Right) - .to_point(display_map); - if reversed { - Some(end..start) - } else { - Some(start..end) - } - } else { - None - } - }) - .collect::>(); - - self.change_selections(None, window, cx, |s| { - s.select_ranges(selection_ranges); - }); - cx.notify(); - } - - pub fn has_non_empty_selection(&self, cx: &mut App) -> bool { - self.selections - .all_adjusted(cx) - .iter() - .any(|selection| !selection.is_empty()) - } - - pub fn has_pending_nonempty_selection(&self) -> bool { - let pending_nonempty_selection = match self.selections.pending_anchor() { - Some(Selection { start, end, .. }) => start != end, - None => false, - }; - - pending_nonempty_selection - || (self.columnar_selection_tail.is_some() && self.selections.disjoint.len() > 1) - } - - pub fn has_pending_selection(&self) -> bool { - self.selections.pending_anchor().is_some() || self.columnar_selection_tail.is_some() - } - - pub fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context) { - self.selection_mark_mode = false; - - if self.clear_expanded_diff_hunks(cx) { - cx.notify(); - return; - } - if self.dismiss_menus_and_popups(true, window, cx) { - return; - } - - if self.mode.is_full() - && self.change_selections(Some(Autoscroll::fit()), window, cx, |s| s.try_cancel()) - { - return; - } - - cx.propagate(); - } - - pub fn dismiss_menus_and_popups( - &mut self, - is_user_requested: bool, - window: &mut Window, - cx: &mut Context, - ) -> bool { - if self.take_rename(false, window, cx).is_some() { - return true; - } - - if hide_hover(self, cx) { - return true; - } - - if self.hide_signature_help(cx, SignatureHelpHiddenBy::Escape) { - return true; - } - - if self.hide_context_menu(window, cx).is_some() { - return true; - } - - if self.mouse_context_menu.take().is_some() { - return true; - } - - if is_user_requested && self.discard_inline_completion(true, cx) { - return true; - } - - if self.snippet_stack.pop().is_some() { - return true; - } - - if self.mode.is_full() && matches!(self.active_diagnostics, ActiveDiagnostic::Group(_)) { - self.dismiss_diagnostics(cx); - return true; - } - - false - } - - fn linked_editing_ranges_for( - &self, - selection: Range, - cx: &App, - ) -> Option, Vec>>> { - if self.linked_edit_ranges.is_empty() { - return None; - } - let ((base_range, linked_ranges), buffer_snapshot, buffer) = - selection.end.buffer_id.and_then(|end_buffer_id| { - if selection.start.buffer_id != Some(end_buffer_id) { - return None; - } - let buffer = self.buffer.read(cx).buffer(end_buffer_id)?; - let snapshot = buffer.read(cx).snapshot(); - self.linked_edit_ranges - .get(end_buffer_id, selection.start..selection.end, &snapshot) - .map(|ranges| (ranges, snapshot, buffer)) - })?; - use text::ToOffset as TO; - // find offset from the start of current range to current cursor position - let start_byte_offset = TO::to_offset(&base_range.start, &buffer_snapshot); - - let start_offset = TO::to_offset(&selection.start, &buffer_snapshot); - let start_difference = start_offset - start_byte_offset; - let end_offset = TO::to_offset(&selection.end, &buffer_snapshot); - let end_difference = end_offset - start_byte_offset; - // Current range has associated linked ranges. - let mut linked_edits = HashMap::<_, Vec<_>>::default(); - for range in linked_ranges.iter() { - let start_offset = TO::to_offset(&range.start, &buffer_snapshot); - let end_offset = start_offset + end_difference; - let start_offset = start_offset + start_difference; - if start_offset > buffer_snapshot.len() || end_offset > buffer_snapshot.len() { - continue; - } - if self.selections.disjoint_anchor_ranges().any(|s| { - if s.start.buffer_id != selection.start.buffer_id - || s.end.buffer_id != selection.end.buffer_id - { - return false; - } - TO::to_offset(&s.start.text_anchor, &buffer_snapshot) <= end_offset - && TO::to_offset(&s.end.text_anchor, &buffer_snapshot) >= start_offset - }) { - continue; - } - let start = buffer_snapshot.anchor_after(start_offset); - let end = buffer_snapshot.anchor_after(end_offset); - linked_edits - .entry(buffer.clone()) - .or_default() - .push(start..end); - } - Some(linked_edits) - } - - pub fn handle_input(&mut self, text: &str, window: &mut Window, cx: &mut Context) { - let text: Arc = text.into(); - - if self.read_only(cx) { - return; - } - - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - - let selections = self.selections.all_adjusted(cx); - let mut bracket_inserted = false; - let mut edits = Vec::new(); - let mut linked_edits = HashMap::<_, Vec<_>>::default(); - let mut new_selections = Vec::with_capacity(selections.len()); - let mut new_autoclose_regions = Vec::new(); - let snapshot = self.buffer.read(cx).read(cx); - let mut clear_linked_edit_ranges = false; - - for (selection, autoclose_region) in - self.selections_with_autoclose_regions(selections, &snapshot) - { - if let Some(scope) = snapshot.language_scope_at(selection.head()) { - // Determine if the inserted text matches the opening or closing - // bracket of any of this language's bracket pairs. - let mut bracket_pair = None; - let mut is_bracket_pair_start = false; - let mut is_bracket_pair_end = false; - if !text.is_empty() { - let mut bracket_pair_matching_end = None; - // `text` can be empty when a user is using IME (e.g. Chinese Wubi Simplified) - // and they are removing the character that triggered IME popup. - for (pair, enabled) in scope.brackets() { - if !pair.close && !pair.surround { - continue; - } - - if enabled && pair.start.ends_with(text.as_ref()) { - let prefix_len = pair.start.len() - text.len(); - let preceding_text_matches_prefix = prefix_len == 0 - || (selection.start.column >= (prefix_len as u32) - && snapshot.contains_str_at( - Point::new( - selection.start.row, - selection.start.column - (prefix_len as u32), - ), - &pair.start[..prefix_len], - )); - if preceding_text_matches_prefix { - bracket_pair = Some(pair.clone()); - is_bracket_pair_start = true; - break; - } - } - if pair.end.as_str() == text.as_ref() && bracket_pair_matching_end.is_none() - { - // take first bracket pair matching end, but don't break in case a later bracket - // pair matches start - bracket_pair_matching_end = Some(pair.clone()); - } - } - if bracket_pair.is_none() && bracket_pair_matching_end.is_some() { - bracket_pair = Some(bracket_pair_matching_end.unwrap()); - is_bracket_pair_end = true; - } - } - - if let Some(bracket_pair) = bracket_pair { - let snapshot_settings = snapshot.language_settings_at(selection.start, cx); - let autoclose = self.use_autoclose && snapshot_settings.use_autoclose; - let auto_surround = - self.use_auto_surround && snapshot_settings.use_auto_surround; - if selection.is_empty() { - if is_bracket_pair_start { - // If the inserted text is a suffix of an opening bracket and the - // selection is preceded by the rest of the opening bracket, then - // insert the closing bracket. - let following_text_allows_autoclose = snapshot - .chars_at(selection.start) - .next() - .map_or(true, |c| scope.should_autoclose_before(c)); - - let preceding_text_allows_autoclose = selection.start.column == 0 - || snapshot.reversed_chars_at(selection.start).next().map_or( - true, - |c| { - bracket_pair.start != bracket_pair.end - || !snapshot - .char_classifier_at(selection.start) - .is_word(c) - }, - ); - - let is_closing_quote = if bracket_pair.end == bracket_pair.start - && bracket_pair.start.len() == 1 - { - let target = bracket_pair.start.chars().next().unwrap(); - let current_line_count = snapshot - .reversed_chars_at(selection.start) - .take_while(|&c| c != '\n') - .filter(|&c| c == target) - .count(); - current_line_count % 2 == 1 - } else { - false - }; - - if autoclose - && bracket_pair.close - && following_text_allows_autoclose - && preceding_text_allows_autoclose - && !is_closing_quote - { - let anchor = snapshot.anchor_before(selection.end); - new_selections.push((selection.map(|_| anchor), text.len())); - new_autoclose_regions.push(( - anchor, - text.len(), - selection.id, - bracket_pair.clone(), - )); - edits.push(( - selection.range(), - format!("{}{}", text, bracket_pair.end).into(), - )); - bracket_inserted = true; - continue; - } - } - - if let Some(region) = autoclose_region { - // If the selection is followed by an auto-inserted closing bracket, - // then don't insert that closing bracket again; just move the selection - // past the closing bracket. - let should_skip = selection.end == region.range.end.to_point(&snapshot) - && text.as_ref() == region.pair.end.as_str(); - if should_skip { - let anchor = snapshot.anchor_after(selection.end); - new_selections - .push((selection.map(|_| anchor), region.pair.end.len())); - continue; - } - } - - let always_treat_brackets_as_autoclosed = snapshot - .language_settings_at(selection.start, cx) - .always_treat_brackets_as_autoclosed; - if always_treat_brackets_as_autoclosed - && is_bracket_pair_end - && snapshot.contains_str_at(selection.end, text.as_ref()) - { - // Otherwise, when `always_treat_brackets_as_autoclosed` is set to `true - // and the inserted text is a closing bracket and the selection is followed - // by the closing bracket then move the selection past the closing bracket. - let anchor = snapshot.anchor_after(selection.end); - new_selections.push((selection.map(|_| anchor), text.len())); - continue; - } - } - // If an opening bracket is 1 character long and is typed while - // text is selected, then surround that text with the bracket pair. - else if auto_surround - && bracket_pair.surround - && is_bracket_pair_start - && bracket_pair.start.chars().count() == 1 - { - edits.push((selection.start..selection.start, text.clone())); - edits.push(( - selection.end..selection.end, - bracket_pair.end.as_str().into(), - )); - bracket_inserted = true; - new_selections.push(( - Selection { - id: selection.id, - start: snapshot.anchor_after(selection.start), - end: snapshot.anchor_before(selection.end), - reversed: selection.reversed, - goal: selection.goal, - }, - 0, - )); - continue; - } - } - } - - if self.auto_replace_emoji_shortcode - && selection.is_empty() - && text.as_ref().ends_with(':') - { - if let Some(possible_emoji_short_code) = - Self::find_possible_emoji_shortcode_at_position(&snapshot, selection.start) - { - if !possible_emoji_short_code.is_empty() { - if let Some(emoji) = emojis::get_by_shortcode(&possible_emoji_short_code) { - let emoji_shortcode_start = Point::new( - selection.start.row, - selection.start.column - possible_emoji_short_code.len() as u32 - 1, - ); - - // Remove shortcode from buffer - edits.push(( - emoji_shortcode_start..selection.start, - "".to_string().into(), - )); - new_selections.push(( - Selection { - id: selection.id, - start: snapshot.anchor_after(emoji_shortcode_start), - end: snapshot.anchor_before(selection.start), - reversed: selection.reversed, - goal: selection.goal, - }, - 0, - )); - - // Insert emoji - let selection_start_anchor = snapshot.anchor_after(selection.start); - new_selections.push((selection.map(|_| selection_start_anchor), 0)); - edits.push((selection.start..selection.end, emoji.to_string().into())); - - continue; - } - } - } - } - - // If not handling any auto-close operation, then just replace the selected - // text with the given input and move the selection to the end of the - // newly inserted text. - let anchor = snapshot.anchor_after(selection.end); - if !self.linked_edit_ranges.is_empty() { - let start_anchor = snapshot.anchor_before(selection.start); - - let is_word_char = text.chars().next().map_or(true, |char| { - let classifier = snapshot.char_classifier_at(start_anchor.to_offset(&snapshot)); - classifier.is_word(char) - }); - - if is_word_char { - if let Some(ranges) = self - .linked_editing_ranges_for(start_anchor.text_anchor..anchor.text_anchor, cx) - { - for (buffer, edits) in ranges { - linked_edits - .entry(buffer.clone()) - .or_default() - .extend(edits.into_iter().map(|range| (range, text.clone()))); - } - } - } else { - clear_linked_edit_ranges = true; - } - } - - new_selections.push((selection.map(|_| anchor), 0)); - edits.push((selection.start..selection.end, text.clone())); - } - - drop(snapshot); - - self.transact(window, cx, |this, window, cx| { - if clear_linked_edit_ranges { - this.linked_edit_ranges.clear(); - } - let initial_buffer_versions = - jsx_tag_auto_close::construct_initial_buffer_versions_map(this, &edits, cx); - - this.buffer.update(cx, |buffer, cx| { - buffer.edit(edits, this.autoindent_mode.clone(), cx); - }); - for (buffer, edits) in linked_edits { - buffer.update(cx, |buffer, cx| { - let snapshot = buffer.snapshot(); - let edits = edits - .into_iter() - .map(|(range, text)| { - use text::ToPoint as TP; - let end_point = TP::to_point(&range.end, &snapshot); - let start_point = TP::to_point(&range.start, &snapshot); - (start_point..end_point, text) - }) - .sorted_by_key(|(range, _)| range.start); - buffer.edit(edits, None, cx); - }) - } - let new_anchor_selections = new_selections.iter().map(|e| &e.0); - let new_selection_deltas = new_selections.iter().map(|e| e.1); - let map = this.display_map.update(cx, |map, cx| map.snapshot(cx)); - let new_selections = resolve_selections::(new_anchor_selections, &map) - .zip(new_selection_deltas) - .map(|(selection, delta)| Selection { - id: selection.id, - start: selection.start + delta, - end: selection.end + delta, - reversed: selection.reversed, - goal: SelectionGoal::None, - }) - .collect::>(); - - let mut i = 0; - for (position, delta, selection_id, pair) in new_autoclose_regions { - let position = position.to_offset(&map.buffer_snapshot) + delta; - let start = map.buffer_snapshot.anchor_before(position); - let end = map.buffer_snapshot.anchor_after(position); - while let Some(existing_state) = this.autoclose_regions.get(i) { - match existing_state.range.start.cmp(&start, &map.buffer_snapshot) { - Ordering::Less => i += 1, - Ordering::Greater => break, - Ordering::Equal => { - match end.cmp(&existing_state.range.end, &map.buffer_snapshot) { - Ordering::Less => i += 1, - Ordering::Equal => break, - Ordering::Greater => break, - } - } - } - } - this.autoclose_regions.insert( - i, - AutocloseRegion { - selection_id, - range: start..end, - pair, - }, - ); - } - - let had_active_inline_completion = this.has_active_inline_completion(); - this.change_selections_inner(Some(Autoscroll::fit()), false, window, cx, |s| { - s.select(new_selections) - }); - - if !bracket_inserted { - if let Some(on_type_format_task) = - this.trigger_on_type_formatting(text.to_string(), window, cx) - { - on_type_format_task.detach_and_log_err(cx); - } - } - - let editor_settings = EditorSettings::get_global(cx); - if bracket_inserted - && (editor_settings.auto_signature_help - || editor_settings.show_signature_help_after_edits) - { - this.show_signature_help(&ShowSignatureHelp, window, cx); - } - - let trigger_in_words = - this.show_edit_predictions_in_menu() || !had_active_inline_completion; - if this.hard_wrap.is_some() { - let latest: Range = this.selections.newest(cx).range(); - if latest.is_empty() - && this - .buffer() - .read(cx) - .snapshot(cx) - .line_len(MultiBufferRow(latest.start.row)) - == latest.start.column - { - this.rewrap_impl( - RewrapOptions { - override_language_settings: true, - preserve_existing_whitespace: true, - }, - cx, - ) - } - } - this.trigger_completion_on_input(&text, trigger_in_words, window, cx); - linked_editing_ranges::refresh_linked_ranges(this, window, cx); - this.refresh_inline_completion(true, false, window, cx); - jsx_tag_auto_close::handle_from(this, initial_buffer_versions, window, cx); - }); - } - - fn find_possible_emoji_shortcode_at_position( - snapshot: &MultiBufferSnapshot, - position: Point, - ) -> Option { - let mut chars = Vec::new(); - let mut found_colon = false; - for char in snapshot.reversed_chars_at(position).take(100) { - // Found a possible emoji shortcode in the middle of the buffer - if found_colon { - if char.is_whitespace() { - chars.reverse(); - return Some(chars.iter().collect()); - } - // If the previous character is not a whitespace, we are in the middle of a word - // and we only want to complete the shortcode if the word is made up of other emojis - let mut containing_word = String::new(); - for ch in snapshot - .reversed_chars_at(position) - .skip(chars.len() + 1) - .take(100) - { - if ch.is_whitespace() { - break; - } - containing_word.push(ch); - } - let containing_word = containing_word.chars().rev().collect::(); - if util::word_consists_of_emojis(containing_word.as_str()) { - chars.reverse(); - return Some(chars.iter().collect()); - } - } - - if char.is_whitespace() || !char.is_ascii() { - return None; - } - if char == ':' { - found_colon = true; - } else { - chars.push(char); - } - } - // Found a possible emoji shortcode at the beginning of the buffer - chars.reverse(); - Some(chars.iter().collect()) - } - - pub fn newline(&mut self, _: &Newline, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.transact(window, cx, |this, window, cx| { - let (edits, selection_fixup_info): (Vec<_>, Vec<_>) = { - let selections = this.selections.all::(cx); - let multi_buffer = this.buffer.read(cx); - let buffer = multi_buffer.snapshot(cx); - selections - .iter() - .map(|selection| { - let start_point = selection.start.to_point(&buffer); - let mut indent = - buffer.indent_size_for_line(MultiBufferRow(start_point.row)); - indent.len = cmp::min(indent.len, start_point.column); - let start = selection.start; - let end = selection.end; - let selection_is_empty = start == end; - let language_scope = buffer.language_scope_at(start); - let (comment_delimiter, insert_extra_newline) = if let Some(language) = - &language_scope - { - let insert_extra_newline = - insert_extra_newline_brackets(&buffer, start..end, language) - || insert_extra_newline_tree_sitter(&buffer, start..end); - - // Comment extension on newline is allowed only for cursor selections - let comment_delimiter = maybe!({ - if !selection_is_empty { - return None; - } - - if !multi_buffer.language_settings(cx).extend_comment_on_newline { - return None; - } - - let delimiters = language.line_comment_prefixes(); - let max_len_of_delimiter = - delimiters.iter().map(|delimiter| delimiter.len()).max()?; - let (snapshot, range) = - buffer.buffer_line_for_row(MultiBufferRow(start_point.row))?; - - let mut index_of_first_non_whitespace = 0; - let comment_candidate = snapshot - .chars_for_range(range) - .skip_while(|c| { - let should_skip = c.is_whitespace(); - if should_skip { - index_of_first_non_whitespace += 1; - } - should_skip - }) - .take(max_len_of_delimiter) - .collect::(); - let comment_prefix = delimiters.iter().find(|comment_prefix| { - comment_candidate.starts_with(comment_prefix.as_ref()) - })?; - let cursor_is_placed_after_comment_marker = - index_of_first_non_whitespace + comment_prefix.len() - <= start_point.column as usize; - if cursor_is_placed_after_comment_marker { - Some(comment_prefix.clone()) - } else { - None - } - }); - (comment_delimiter, insert_extra_newline) - } else { - (None, false) - }; - - let capacity_for_delimiter = comment_delimiter - .as_deref() - .map(str::len) - .unwrap_or_default(); - let mut new_text = - String::with_capacity(1 + capacity_for_delimiter + indent.len as usize); - new_text.push('\n'); - new_text.extend(indent.chars()); - if let Some(delimiter) = &comment_delimiter { - new_text.push_str(delimiter); - } - if insert_extra_newline { - new_text = new_text.repeat(2); - } - - let anchor = buffer.anchor_after(end); - let new_selection = selection.map(|_| anchor); - ( - (start..end, new_text), - (insert_extra_newline, new_selection), - ) - }) - .unzip() - }; - - this.edit_with_autoindent(edits, cx); - let buffer = this.buffer.read(cx).snapshot(cx); - let new_selections = selection_fixup_info - .into_iter() - .map(|(extra_newline_inserted, new_selection)| { - let mut cursor = new_selection.end.to_point(&buffer); - if extra_newline_inserted { - cursor.row -= 1; - cursor.column = buffer.line_len(MultiBufferRow(cursor.row)); - } - new_selection.map(|_| cursor) - }) - .collect(); - - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(new_selections) - }); - this.refresh_inline_completion(true, false, window, cx); - }); - } - - pub fn newline_above(&mut self, _: &NewlineAbove, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - - let buffer = self.buffer.read(cx); - let snapshot = buffer.snapshot(cx); - - let mut edits = Vec::new(); - let mut rows = Vec::new(); - - for (rows_inserted, selection) in self.selections.all_adjusted(cx).into_iter().enumerate() { - let cursor = selection.head(); - let row = cursor.row; - - let start_of_line = snapshot.clip_point(Point::new(row, 0), Bias::Left); - - let newline = "\n".to_string(); - edits.push((start_of_line..start_of_line, newline)); - - rows.push(row + rows_inserted as u32); - } - - self.transact(window, cx, |editor, window, cx| { - editor.edit(edits, cx); - - editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - let mut index = 0; - s.move_cursors_with(|map, _, _| { - let row = rows[index]; - index += 1; - - let point = Point::new(row, 0); - let boundary = map.next_line_boundary(point).1; - let clipped = map.clip_point(boundary, Bias::Left); - - (clipped, SelectionGoal::None) - }); - }); - - let mut indent_edits = Vec::new(); - let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx); - for row in rows { - let indents = multibuffer_snapshot.suggested_indents(row..row + 1, cx); - for (row, indent) in indents { - if indent.len == 0 { - continue; - } - - let text = match indent.kind { - IndentKind::Space => " ".repeat(indent.len as usize), - IndentKind::Tab => "\t".repeat(indent.len as usize), - }; - let point = Point::new(row.0, 0); - indent_edits.push((point..point, text)); - } - } - editor.edit(indent_edits, cx); - }); - } - - pub fn newline_below(&mut self, _: &NewlineBelow, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - - let buffer = self.buffer.read(cx); - let snapshot = buffer.snapshot(cx); - - let mut edits = Vec::new(); - let mut rows = Vec::new(); - let mut rows_inserted = 0; - - for selection in self.selections.all_adjusted(cx) { - let cursor = selection.head(); - let row = cursor.row; - - let point = Point::new(row + 1, 0); - let start_of_line = snapshot.clip_point(point, Bias::Left); - - let newline = "\n".to_string(); - edits.push((start_of_line..start_of_line, newline)); - - rows_inserted += 1; - rows.push(row + rows_inserted); - } - - self.transact(window, cx, |editor, window, cx| { - editor.edit(edits, cx); - - editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - let mut index = 0; - s.move_cursors_with(|map, _, _| { - let row = rows[index]; - index += 1; - - let point = Point::new(row, 0); - let boundary = map.next_line_boundary(point).1; - let clipped = map.clip_point(boundary, Bias::Left); - - (clipped, SelectionGoal::None) - }); - }); - - let mut indent_edits = Vec::new(); - let multibuffer_snapshot = editor.buffer.read(cx).snapshot(cx); - for row in rows { - let indents = multibuffer_snapshot.suggested_indents(row..row + 1, cx); - for (row, indent) in indents { - if indent.len == 0 { - continue; - } - - let text = match indent.kind { - IndentKind::Space => " ".repeat(indent.len as usize), - IndentKind::Tab => "\t".repeat(indent.len as usize), - }; - let point = Point::new(row.0, 0); - indent_edits.push((point..point, text)); - } - } - editor.edit(indent_edits, cx); - }); - } - - pub fn insert(&mut self, text: &str, window: &mut Window, cx: &mut Context) { - let autoindent = text.is_empty().not().then(|| AutoindentMode::Block { - original_indent_columns: Vec::new(), - }); - self.insert_with_autoindent_mode(text, autoindent, window, cx); - } - - fn insert_with_autoindent_mode( - &mut self, - text: &str, - autoindent_mode: Option, - window: &mut Window, - cx: &mut Context, - ) { - if self.read_only(cx) { - return; - } - - let text: Arc = text.into(); - self.transact(window, cx, |this, window, cx| { - let old_selections = this.selections.all_adjusted(cx); - let selection_anchors = this.buffer.update(cx, |buffer, cx| { - let anchors = { - let snapshot = buffer.read(cx); - old_selections - .iter() - .map(|s| { - let anchor = snapshot.anchor_after(s.head()); - s.map(|_| anchor) - }) - .collect::>() - }; - buffer.edit( - old_selections - .iter() - .map(|s| (s.start..s.end, text.clone())), - autoindent_mode, - cx, - ); - anchors - }); - - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select_anchors(selection_anchors); - }); - - cx.notify(); - }); - } - - fn trigger_completion_on_input( - &mut self, - text: &str, - trigger_in_words: bool, - window: &mut Window, - cx: &mut Context, - ) { - let ignore_completion_provider = self - .context_menu - .borrow() - .as_ref() - .map(|menu| match menu { - CodeContextMenu::Completions(completions_menu) => { - completions_menu.ignore_completion_provider - } - CodeContextMenu::CodeActions(_) => false, - }) - .unwrap_or(false); - - if ignore_completion_provider { - self.show_word_completions(&ShowWordCompletions, window, cx); - } else if self.is_completion_trigger(text, trigger_in_words, cx) { - self.show_completions( - &ShowCompletions { - trigger: Some(text.to_owned()).filter(|x| !x.is_empty()), - }, - window, - cx, - ); - } else { - self.hide_context_menu(window, cx); - } - } - - fn is_completion_trigger( - &self, - text: &str, - trigger_in_words: bool, - cx: &mut Context, - ) -> bool { - let position = self.selections.newest_anchor().head(); - let multibuffer = self.buffer.read(cx); - let Some(buffer) = position - .buffer_id - .and_then(|buffer_id| multibuffer.buffer(buffer_id).clone()) - else { - return false; - }; - - if let Some(completion_provider) = &self.completion_provider { - completion_provider.is_completion_trigger( - &buffer, - position.text_anchor, - text, - trigger_in_words, - cx, - ) - } else { - false - } - } - - /// If any empty selections is touching the start of its innermost containing autoclose - /// region, expand it to select the brackets. - fn select_autoclose_pair(&mut self, window: &mut Window, cx: &mut Context) { - let selections = self.selections.all::(cx); - let buffer = self.buffer.read(cx).read(cx); - let new_selections = self - .selections_with_autoclose_regions(selections, &buffer) - .map(|(mut selection, region)| { - if !selection.is_empty() { - return selection; - } - - if let Some(region) = region { - let mut range = region.range.to_offset(&buffer); - if selection.start == range.start && range.start >= region.pair.start.len() { - range.start -= region.pair.start.len(); - if buffer.contains_str_at(range.start, ®ion.pair.start) - && buffer.contains_str_at(range.end, ®ion.pair.end) - { - range.end += region.pair.end.len(); - selection.start = range.start; - selection.end = range.end; - - return selection; - } - } - } - - let always_treat_brackets_as_autoclosed = buffer - .language_settings_at(selection.start, cx) - .always_treat_brackets_as_autoclosed; - - if !always_treat_brackets_as_autoclosed { - return selection; - } - - if let Some(scope) = buffer.language_scope_at(selection.start) { - for (pair, enabled) in scope.brackets() { - if !enabled || !pair.close { - continue; - } - - if buffer.contains_str_at(selection.start, &pair.end) { - let pair_start_len = pair.start.len(); - if buffer.contains_str_at( - selection.start.saturating_sub(pair_start_len), - &pair.start, - ) { - selection.start -= pair_start_len; - selection.end += pair.end.len(); - - return selection; - } - } - } - } - - selection - }) - .collect(); - - drop(buffer); - self.change_selections(None, window, cx, |selections| { - selections.select(new_selections) - }); - } - - /// Iterate the given selections, and for each one, find the smallest surrounding - /// autoclose region. This uses the ordering of the selections and the autoclose - /// regions to avoid repeated comparisons. - fn selections_with_autoclose_regions<'a, D: ToOffset + Clone>( - &'a self, - selections: impl IntoIterator>, - buffer: &'a MultiBufferSnapshot, - ) -> impl Iterator, Option<&'a AutocloseRegion>)> { - let mut i = 0; - let mut regions = self.autoclose_regions.as_slice(); - selections.into_iter().map(move |selection| { - let range = selection.start.to_offset(buffer)..selection.end.to_offset(buffer); - - let mut enclosing = None; - while let Some(pair_state) = regions.get(i) { - if pair_state.range.end.to_offset(buffer) < range.start { - regions = ®ions[i + 1..]; - i = 0; - } else if pair_state.range.start.to_offset(buffer) > range.end { - break; - } else { - if pair_state.selection_id == selection.id { - enclosing = Some(pair_state); - } - i += 1; - } - } - - (selection, enclosing) - }) - } - - /// Remove any autoclose regions that no longer contain their selection. - fn invalidate_autoclose_regions( - &mut self, - mut selections: &[Selection], - buffer: &MultiBufferSnapshot, - ) { - self.autoclose_regions.retain(|state| { - let mut i = 0; - while let Some(selection) = selections.get(i) { - if selection.end.cmp(&state.range.start, buffer).is_lt() { - selections = &selections[1..]; - continue; - } - if selection.start.cmp(&state.range.end, buffer).is_gt() { - break; - } - if selection.id == state.selection_id { - return true; - } else { - i += 1; - } - } - false - }); - } - - fn completion_query(buffer: &MultiBufferSnapshot, position: impl ToOffset) -> Option { - let offset = position.to_offset(buffer); - let (word_range, kind) = buffer.surrounding_word(offset, true); - if offset > word_range.start && kind == Some(CharKind::Word) { - Some( - buffer - .text_for_range(word_range.start..offset) - .collect::(), - ) - } else { - None - } - } - - pub fn toggle_inline_values( - &mut self, - _: &ToggleInlineValues, - _: &mut Window, - cx: &mut Context, - ) { - self.inline_value_cache.enabled = !self.inline_value_cache.enabled; - - self.refresh_inline_values(cx); - } - - pub fn toggle_inlay_hints( - &mut self, - _: &ToggleInlayHints, - _: &mut Window, - cx: &mut Context, - ) { - self.refresh_inlay_hints( - InlayHintRefreshReason::Toggle(!self.inlay_hints_enabled()), - cx, - ); - } - - pub fn inlay_hints_enabled(&self) -> bool { - self.inlay_hint_cache.enabled - } - - pub fn inline_values_enabled(&self) -> bool { - self.inline_value_cache.enabled - } - - fn refresh_inlay_hints(&mut self, reason: InlayHintRefreshReason, cx: &mut Context) { - if self.semantics_provider.is_none() || !self.mode.is_full() { - return; - } - - let reason_description = reason.description(); - let ignore_debounce = matches!( - reason, - InlayHintRefreshReason::SettingsChange(_) - | InlayHintRefreshReason::Toggle(_) - | InlayHintRefreshReason::ExcerptsRemoved(_) - | InlayHintRefreshReason::ModifiersChanged(_) - ); - let (invalidate_cache, required_languages) = match reason { - InlayHintRefreshReason::ModifiersChanged(enabled) => { - match self.inlay_hint_cache.modifiers_override(enabled) { - Some(enabled) => { - if enabled { - (InvalidationStrategy::RefreshRequested, None) - } else { - self.splice_inlays( - &self - .visible_inlay_hints(cx) - .iter() - .map(|inlay| inlay.id) - .collect::>(), - Vec::new(), - cx, - ); - return; - } - } - None => return, - } - } - InlayHintRefreshReason::Toggle(enabled) => { - if self.inlay_hint_cache.toggle(enabled) { - if enabled { - (InvalidationStrategy::RefreshRequested, None) - } else { - self.splice_inlays( - &self - .visible_inlay_hints(cx) - .iter() - .map(|inlay| inlay.id) - .collect::>(), - Vec::new(), - cx, - ); - return; - } - } else { - return; - } - } - InlayHintRefreshReason::SettingsChange(new_settings) => { - match self.inlay_hint_cache.update_settings( - &self.buffer, - new_settings, - self.visible_inlay_hints(cx), - cx, - ) { - ControlFlow::Break(Some(InlaySplice { - to_remove, - to_insert, - })) => { - self.splice_inlays(&to_remove, to_insert, cx); - return; - } - ControlFlow::Break(None) => return, - ControlFlow::Continue(()) => (InvalidationStrategy::RefreshRequested, None), - } - } - InlayHintRefreshReason::ExcerptsRemoved(excerpts_removed) => { - if let Some(InlaySplice { - to_remove, - to_insert, - }) = self.inlay_hint_cache.remove_excerpts(&excerpts_removed) - { - self.splice_inlays(&to_remove, to_insert, cx); - } - self.display_map.update(cx, |display_map, _| { - display_map.remove_inlays_for_excerpts(&excerpts_removed) - }); - return; - } - InlayHintRefreshReason::NewLinesShown => (InvalidationStrategy::None, None), - InlayHintRefreshReason::BufferEdited(buffer_languages) => { - (InvalidationStrategy::BufferEdited, Some(buffer_languages)) - } - InlayHintRefreshReason::RefreshRequested => { - (InvalidationStrategy::RefreshRequested, None) - } - }; - - if let Some(InlaySplice { - to_remove, - to_insert, - }) = self.inlay_hint_cache.spawn_hint_refresh( - reason_description, - self.excerpts_for_inlay_hints_query(required_languages.as_ref(), cx), - invalidate_cache, - ignore_debounce, - cx, - ) { - self.splice_inlays(&to_remove, to_insert, cx); - } - } - - fn visible_inlay_hints(&self, cx: &Context) -> Vec { - self.display_map - .read(cx) - .current_inlays() - .filter(move |inlay| matches!(inlay.id, InlayId::Hint(_))) - .cloned() - .collect() - } - - pub fn excerpts_for_inlay_hints_query( - &self, - restrict_to_languages: Option<&HashSet>>, - cx: &mut Context, - ) -> HashMap, clock::Global, Range)> { - let Some(project) = self.project.as_ref() else { - return HashMap::default(); - }; - let project = project.read(cx); - let multi_buffer = self.buffer().read(cx); - let multi_buffer_snapshot = multi_buffer.snapshot(cx); - let multi_buffer_visible_start = self - .scroll_manager - .anchor() - .anchor - .to_point(&multi_buffer_snapshot); - let multi_buffer_visible_end = multi_buffer_snapshot.clip_point( - multi_buffer_visible_start - + Point::new(self.visible_line_count().unwrap_or(0.).ceil() as u32, 0), - Bias::Left, - ); - let multi_buffer_visible_range = multi_buffer_visible_start..multi_buffer_visible_end; - multi_buffer_snapshot - .range_to_buffer_ranges(multi_buffer_visible_range) - .into_iter() - .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty()) - .filter_map(|(buffer, excerpt_visible_range, excerpt_id)| { - let buffer_file = project::File::from_dyn(buffer.file())?; - let buffer_worktree = project.worktree_for_id(buffer_file.worktree_id(cx), cx)?; - let worktree_entry = buffer_worktree - .read(cx) - .entry_for_id(buffer_file.project_entry_id(cx)?)?; - if worktree_entry.is_ignored { - return None; - } - - let language = buffer.language()?; - if let Some(restrict_to_languages) = restrict_to_languages { - if !restrict_to_languages.contains(language) { - return None; - } - } - Some(( - excerpt_id, - ( - multi_buffer.buffer(buffer.remote_id()).unwrap(), - buffer.version().clone(), - excerpt_visible_range, - ), - )) - }) - .collect() - } - - pub fn text_layout_details(&self, window: &mut Window) -> TextLayoutDetails { - TextLayoutDetails { - text_system: window.text_system().clone(), - editor_style: self.style.clone().unwrap(), - rem_size: window.rem_size(), - scroll_anchor: self.scroll_manager.anchor(), - visible_rows: self.visible_line_count(), - vertical_scroll_margin: self.scroll_manager.vertical_scroll_margin, - } - } - - pub fn splice_inlays( - &self, - to_remove: &[InlayId], - to_insert: Vec, - cx: &mut Context, - ) { - self.display_map.update(cx, |display_map, cx| { - display_map.splice_inlays(to_remove, to_insert, cx) - }); - cx.notify(); - } - - fn trigger_on_type_formatting( - &self, - input: String, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - if input.len() != 1 { - return None; - } - - let project = self.project.as_ref()?; - let position = self.selections.newest_anchor().head(); - let (buffer, buffer_position) = self - .buffer - .read(cx) - .text_anchor_for_position(position, cx)?; - - let settings = language_settings::language_settings( - buffer - .read(cx) - .language_at(buffer_position) - .map(|l| l.name()), - buffer.read(cx).file(), - cx, - ); - if !settings.use_on_type_format { - return None; - } - - // OnTypeFormatting returns a list of edits, no need to pass them between Zed instances, - // hence we do LSP request & edit on host side only — add formats to host's history. - let push_to_lsp_host_history = true; - // If this is not the host, append its history with new edits. - let push_to_client_history = project.read(cx).is_via_collab(); - - let on_type_formatting = project.update(cx, |project, cx| { - project.on_type_format( - buffer.clone(), - buffer_position, - input, - push_to_lsp_host_history, - cx, - ) - }); - Some(cx.spawn_in(window, async move |editor, cx| { - if let Some(transaction) = on_type_formatting.await? { - if push_to_client_history { - buffer - .update(cx, |buffer, _| { - buffer.push_transaction(transaction, Instant::now()); - buffer.finalize_last_transaction(); - }) - .ok(); - } - editor.update(cx, |editor, cx| { - editor.refresh_document_highlights(cx); - })?; - } - Ok(()) - })) - } - - pub fn show_word_completions( - &mut self, - _: &ShowWordCompletions, - window: &mut Window, - cx: &mut Context, - ) { - self.open_completions_menu(true, None, window, cx); - } - - pub fn show_completions( - &mut self, - options: &ShowCompletions, - window: &mut Window, - cx: &mut Context, - ) { - self.open_completions_menu(false, options.trigger.as_deref(), window, cx); - } - - fn open_completions_menu( - &mut self, - ignore_completion_provider: bool, - trigger: Option<&str>, - window: &mut Window, - cx: &mut Context, - ) { - if self.pending_rename.is_some() { - return; - } - if !self.snippet_stack.is_empty() && self.context_menu.borrow().as_ref().is_some() { - return; - } - - let position = self.selections.newest_anchor().head(); - if position.diff_base_anchor.is_some() { - return; - } - let (buffer, buffer_position) = - if let Some(output) = self.buffer.read(cx).text_anchor_for_position(position, cx) { - output - } else { - return; - }; - let buffer_snapshot = buffer.read(cx).snapshot(); - let show_completion_documentation = buffer_snapshot - .settings_at(buffer_position, cx) - .show_completion_documentation; - - let query = Self::completion_query(&self.buffer.read(cx).read(cx), position); - - let trigger_kind = match trigger { - Some(trigger) if buffer.read(cx).completion_triggers().contains(trigger) => { - CompletionTriggerKind::TRIGGER_CHARACTER - } - _ => CompletionTriggerKind::INVOKED, - }; - let completion_context = CompletionContext { - trigger_character: trigger.and_then(|trigger| { - if trigger_kind == CompletionTriggerKind::TRIGGER_CHARACTER { - Some(String::from(trigger)) - } else { - None - } - }), - trigger_kind, - }; - - let (old_range, word_kind) = buffer_snapshot.surrounding_word(buffer_position); - let (old_range, word_to_exclude) = if word_kind == Some(CharKind::Word) { - let word_to_exclude = buffer_snapshot - .text_for_range(old_range.clone()) - .collect::(); - ( - buffer_snapshot.anchor_before(old_range.start) - ..buffer_snapshot.anchor_after(old_range.end), - Some(word_to_exclude), - ) - } else { - (buffer_position..buffer_position, None) - }; - - let completion_settings = language_settings( - buffer_snapshot - .language_at(buffer_position) - .map(|language| language.name()), - buffer_snapshot.file(), - cx, - ) - .completions; - - // The document can be large, so stay in reasonable bounds when searching for words, - // otherwise completion pop-up might be slow to appear. - const WORD_LOOKUP_ROWS: u32 = 5_000; - let buffer_row = text::ToPoint::to_point(&buffer_position, &buffer_snapshot).row; - let min_word_search = buffer_snapshot.clip_point( - Point::new(buffer_row.saturating_sub(WORD_LOOKUP_ROWS), 0), - Bias::Left, - ); - let max_word_search = buffer_snapshot.clip_point( - Point::new(buffer_row + WORD_LOOKUP_ROWS, 0).min(buffer_snapshot.max_point()), - Bias::Right, - ); - let word_search_range = buffer_snapshot.point_to_offset(min_word_search) - ..buffer_snapshot.point_to_offset(max_word_search); - - let provider = self - .completion_provider - .as_ref() - .filter(|_| !ignore_completion_provider); - let skip_digits = query - .as_ref() - .map_or(true, |query| !query.chars().any(|c| c.is_digit(10))); - - let (mut words, provided_completions) = match provider { - Some(provider) => { - let completions = provider.completions( - position.excerpt_id, - &buffer, - buffer_position, - completion_context, - window, - 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, - range: word_search_range, - skip_digits, - }) - }), - }; - - (words, completions) - } - None => ( - cx.background_spawn(async move { - buffer_snapshot.words_in_range(WordsQuery { - fuzzy_contents: None, - range: word_search_range, - skip_digits, - }) - }), - Task::ready(Ok(None)), - ), - }; - - let sort_completions = provider - .as_ref() - .map_or(false, |provider| provider.sort_completions()); - - let filter_completions = provider - .as_ref() - .map_or(true, |provider| provider.filter_completions()); - - let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order; - - let id = post_inc(&mut self.next_completion_id); - let task = cx.spawn_in(window, async move |editor, cx| { - async move { - editor.update(cx, |this, _| { - this.completion_tasks.retain(|(task_id, _)| *task_id >= id); - })?; - - let mut completions = Vec::new(); - if let Some(provided_completions) = provided_completions.await.log_err().flatten() { - completions.extend(provided_completions); - if completion_settings.words == WordsCompletionMode::Fallback { - words = Task::ready(BTreeMap::default()); - } - } - - let mut words = words.await; - if let Some(word_to_exclude) = &word_to_exclude { - words.remove(word_to_exclude); - } - for lsp_completion in &completions { - words.remove(&lsp_completion.new_text); - } - completions.extend(words.into_iter().map(|(word, word_range)| Completion { - replace_range: old_range.clone(), - new_text: word.clone(), - label: CodeLabel::plain(word, None), - icon_path: None, - documentation: None, - source: CompletionSource::BufferWord { - word_range, - resolved: false, - }, - insert_text_mode: Some(InsertTextMode::AS_IS), - confirm: None, - })); - - let menu = if completions.is_empty() { - None - } else { - let mut menu = CompletionsMenu::new( - id, - sort_completions, - show_completion_documentation, - ignore_completion_provider, - position, - buffer.clone(), - completions.into(), - snippet_sort_order, - ); - - menu.filter( - if filter_completions { - query.as_deref() - } else { - None - }, - cx.background_executor().clone(), - ) - .await; - - menu.visible().then_some(menu) - }; - - editor.update_in(cx, |editor, window, cx| { - match editor.context_menu.borrow().as_ref() { - None => {} - Some(CodeContextMenu::Completions(prev_menu)) => { - if prev_menu.id > id { - return; - } - } - _ => return, - } - - if editor.focus_handle.is_focused(window) && menu.is_some() { - let mut menu = menu.unwrap(); - menu.resolve_visible_completions(editor.completion_provider.as_deref(), cx); - - *editor.context_menu.borrow_mut() = - Some(CodeContextMenu::Completions(menu)); - - if editor.show_edit_predictions_in_menu() { - editor.update_visible_inline_completion(window, cx); - } else { - editor.discard_inline_completion(false, cx); - } - - cx.notify(); - } else if editor.completion_tasks.len() <= 1 { - // If there are no more completion tasks and the last menu was - // empty, we should hide it. - let was_hidden = editor.hide_context_menu(window, cx).is_none(); - // If it was already hidden and we don't show inline - // completions in the menu, we should also show the - // inline-completion when available. - if was_hidden && editor.show_edit_predictions_in_menu() { - editor.update_visible_inline_completion(window, cx); - } - } - })?; - - anyhow::Ok(()) - } - .log_err() - .await - }); - - self.completion_tasks.push((id, task)); - } - - #[cfg(feature = "test-support")] - pub fn current_completions(&self) -> Option> { - let menu = self.context_menu.borrow(); - if let CodeContextMenu::Completions(menu) = menu.as_ref()? { - let completions = menu.completions.borrow(); - Some(completions.to_vec()) - } else { - None - } - } - - pub fn confirm_completion( - &mut self, - action: &ConfirmCompletion, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.do_completion(action.item_ix, CompletionIntent::Complete, window, cx) - } - - pub fn confirm_completion_insert( - &mut self, - _: &ConfirmCompletionInsert, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.do_completion(None, CompletionIntent::CompleteWithInsert, window, cx) - } - - pub fn confirm_completion_replace( - &mut self, - _: &ConfirmCompletionReplace, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.do_completion(None, CompletionIntent::CompleteWithReplace, window, cx) - } - - pub fn compose_completion( - &mut self, - action: &ComposeCompletion, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.do_completion(action.item_ix, CompletionIntent::Compose, window, cx) - } - - fn do_completion( - &mut self, - item_ix: Option, - intent: CompletionIntent, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - use language::ToOffset as _; - - let CodeContextMenu::Completions(completions_menu) = self.hide_context_menu(window, cx)? - else { - return None; - }; - - let candidate_id = { - let entries = completions_menu.entries.borrow(); - let mat = entries.get(item_ix.unwrap_or(completions_menu.selected_item))?; - if self.show_edit_predictions_in_menu() { - self.discard_inline_completion(true, cx); - } - mat.candidate_id - }; - - let buffer_handle = completions_menu.buffer; - let completion = completions_menu - .completions - .borrow() - .get(candidate_id)? - .clone(); - cx.stop_propagation(); - - let snippet; - let new_text; - if completion.is_snippet() { - snippet = Some(Snippet::parse(&completion.new_text).log_err()?); - new_text = snippet.as_ref().unwrap().text.clone(); - } else { - snippet = None; - new_text = completion.new_text.clone(); - }; - - let replace_range = choose_completion_range(&completion, intent, &buffer_handle, cx); - let buffer = buffer_handle.read(cx); - let snapshot = self.buffer.read(cx).snapshot(cx); - let replace_range_multibuffer = { - let excerpt = snapshot - .excerpt_containing(self.selections.newest_anchor().range()) - .unwrap(); - let multibuffer_anchor = snapshot - .anchor_in_excerpt(excerpt.id(), buffer.anchor_before(replace_range.start)) - .unwrap() - ..snapshot - .anchor_in_excerpt(excerpt.id(), buffer.anchor_before(replace_range.end)) - .unwrap(); - multibuffer_anchor.start.to_offset(&snapshot) - ..multibuffer_anchor.end.to_offset(&snapshot) - }; - let newest_anchor = self.selections.newest_anchor(); - if newest_anchor.head().buffer_id != Some(buffer.remote_id()) { - return None; - } - - let old_text = buffer - .text_for_range(replace_range.clone()) - .collect::(); - let lookbehind = newest_anchor - .start - .text_anchor - .to_offset(buffer) - .saturating_sub(replace_range.start); - let lookahead = replace_range - .end - .saturating_sub(newest_anchor.end.text_anchor.to_offset(buffer)); - let prefix = &old_text[..old_text.len().saturating_sub(lookahead)]; - let suffix = &old_text[lookbehind.min(old_text.len())..]; - - let selections = self.selections.all::(cx); - let mut ranges = Vec::new(); - let mut linked_edits = HashMap::<_, Vec<_>>::default(); - - for selection in &selections { - let range = if selection.id == newest_anchor.id { - replace_range_multibuffer.clone() - } else { - let mut range = selection.range(); - - // if prefix is present, don't duplicate it - if snapshot.contains_str_at(range.start.saturating_sub(lookbehind), prefix) { - range.start = range.start.saturating_sub(lookbehind); - - // if suffix is also present, mimic the newest cursor and replace it - if selection.id != newest_anchor.id - && snapshot.contains_str_at(range.end, suffix) - { - range.end += lookahead; - } - } - range - }; - - ranges.push(range); - - if !self.linked_edit_ranges.is_empty() { - let start_anchor = snapshot.anchor_before(selection.head()); - let end_anchor = snapshot.anchor_after(selection.tail()); - if let Some(ranges) = self - .linked_editing_ranges_for(start_anchor.text_anchor..end_anchor.text_anchor, cx) - { - for (buffer, edits) in ranges { - linked_edits - .entry(buffer.clone()) - .or_default() - .extend(edits.into_iter().map(|range| (range, new_text.to_owned()))); - } - } - } - } - - cx.emit(EditorEvent::InputHandled { - utf16_range_to_replace: None, - text: new_text.clone().into(), - }); - - self.transact(window, cx, |this, window, cx| { - if let Some(mut snippet) = snippet { - snippet.text = new_text.to_string(); - this.insert_snippet(&ranges, snippet, window, cx).log_err(); - } else { - this.buffer.update(cx, |buffer, cx| { - let auto_indent = match completion.insert_text_mode { - Some(InsertTextMode::AS_IS) => None, - _ => this.autoindent_mode.clone(), - }; - let edits = ranges.into_iter().map(|range| (range, new_text.as_str())); - buffer.edit(edits, auto_indent, cx); - }); - } - for (buffer, edits) in linked_edits { - buffer.update(cx, |buffer, cx| { - let snapshot = buffer.snapshot(); - let edits = edits - .into_iter() - .map(|(range, text)| { - use text::ToPoint as TP; - let end_point = TP::to_point(&range.end, &snapshot); - let start_point = TP::to_point(&range.start, &snapshot); - (start_point..end_point, text) - }) - .sorted_by_key(|(range, _)| range.start); - buffer.edit(edits, None, cx); - }) - } - - this.refresh_inline_completion(true, false, window, cx); - }); - - let show_new_completions_on_confirm = completion - .confirm - .as_ref() - .map_or(false, |confirm| confirm(intent, window, cx)); - if show_new_completions_on_confirm { - self.show_completions(&ShowCompletions { trigger: None }, window, cx); - } - - let provider = self.completion_provider.as_ref()?; - drop(completion); - let apply_edits = provider.apply_additional_edits_for_completion( - buffer_handle, - completions_menu.completions.clone(), - candidate_id, - true, - cx, - ); - - let editor_settings = EditorSettings::get_global(cx); - if editor_settings.show_signature_help_after_edits || editor_settings.auto_signature_help { - // After the code completion is finished, users often want to know what signatures are needed. - // so we should automatically call signature_help - self.show_signature_help(&ShowSignatureHelp, window, cx); - } - - Some(cx.foreground_executor().spawn(async move { - apply_edits.await?; - Ok(()) - })) - } - - pub fn toggle_code_actions( - &mut self, - action: &ToggleCodeActions, - window: &mut Window, - cx: &mut Context, - ) { - let quick_launch = action.quick_launch; - let mut context_menu = self.context_menu.borrow_mut(); - if let Some(CodeContextMenu::CodeActions(code_actions)) = context_menu.as_ref() { - if code_actions.deployed_from_indicator == action.deployed_from_indicator { - // Toggle if we're selecting the same one - *context_menu = None; - cx.notify(); - return; - } else { - // Otherwise, clear it and start a new one - *context_menu = None; - cx.notify(); - } - } - drop(context_menu); - let snapshot = self.snapshot(window, cx); - let deployed_from_indicator = action.deployed_from_indicator; - let mut task = self.code_actions_task.take(); - let action = action.clone(); - cx.spawn_in(window, async move |editor, cx| { - while let Some(prev_task) = task { - prev_task.await.log_err(); - task = editor.update(cx, |this, _| this.code_actions_task.take())?; - } - - let spawned_test_task = editor.update_in(cx, |editor, window, cx| { - if editor.focus_handle.is_focused(window) { - let multibuffer_point = action - .deployed_from_indicator - .map(|row| DisplayPoint::new(row, 0).to_point(&snapshot)) - .unwrap_or_else(|| editor.selections.newest::(cx).head()); - let (buffer, buffer_row) = snapshot - .buffer_snapshot - .buffer_line_for_row(MultiBufferRow(multibuffer_point.row)) - .and_then(|(buffer_snapshot, range)| { - editor - .buffer - .read(cx) - .buffer(buffer_snapshot.remote_id()) - .map(|buffer| (buffer, range.start.row)) - })?; - let (_, code_actions) = editor - .available_code_actions - .clone() - .and_then(|(location, code_actions)| { - let snapshot = location.buffer.read(cx).snapshot(); - let point_range = location.range.to_point(&snapshot); - let point_range = point_range.start.row..=point_range.end.row; - if point_range.contains(&buffer_row) { - Some((location, code_actions)) - } else { - None - } - }) - .unzip(); - let buffer_id = buffer.read(cx).remote_id(); - let tasks = editor - .tasks - .get(&(buffer_id, buffer_row)) - .map(|t| Arc::new(t.to_owned())); - if tasks.is_none() && code_actions.is_none() { - return None; - } - - editor.completion_tasks.clear(); - editor.discard_inline_completion(false, cx); - let task_context = - tasks - .as_ref() - .zip(editor.project.clone()) - .map(|(tasks, project)| { - Self::build_tasks_context(&project, &buffer, buffer_row, tasks, cx) - }); - - Some(cx.spawn_in(window, async move |editor, cx| { - let task_context = match task_context { - Some(task_context) => task_context.await, - None => None, - }; - let resolved_tasks = - tasks - .zip(task_context.clone()) - .map(|(tasks, task_context)| ResolvedTasks { - templates: tasks.resolve(&task_context).collect(), - position: snapshot.buffer_snapshot.anchor_before(Point::new( - multibuffer_point.row, - tasks.column, - )), - }); - let spawn_straight_away = quick_launch - && resolved_tasks - .as_ref() - .map_or(false, |tasks| tasks.templates.len() == 1) - && code_actions - .as_ref() - .map_or(true, |actions| actions.is_empty()); - let debug_scenarios = editor.update(cx, |editor, cx| { - if cx.has_flag::() { - maybe!({ - let project = editor.project.as_ref()?; - let dap_store = project.read(cx).dap_store(); - let mut scenarios = vec![]; - let resolved_tasks = resolved_tasks.as_ref()?; - let debug_adapter: SharedString = buffer - .read(cx) - .language()? - .context_provider()? - .debug_adapter()? - .into(); - dap_store.update(cx, |this, cx| { - for (_, task) in &resolved_tasks.templates { - if let Some(scenario) = this - .debug_scenario_for_build_task( - task.resolved.clone(), - SharedString::from( - task.original_task().label.clone(), - ), - debug_adapter.clone(), - cx, - ) - { - scenarios.push(scenario); - } - } - }); - Some(scenarios) - }) - .unwrap_or_default() - } else { - vec![] - } - })?; - if let Ok(task) = editor.update_in(cx, |editor, window, cx| { - *editor.context_menu.borrow_mut() = - Some(CodeContextMenu::CodeActions(CodeActionsMenu { - buffer, - actions: CodeActionContents::new( - resolved_tasks, - code_actions, - debug_scenarios, - task_context.unwrap_or_default(), - ), - selected_item: Default::default(), - scroll_handle: UniformListScrollHandle::default(), - deployed_from_indicator, - })); - if spawn_straight_away { - if let Some(task) = editor.confirm_code_action( - &ConfirmCodeAction { item_ix: Some(0) }, - window, - cx, - ) { - cx.notify(); - return task; - } - } - cx.notify(); - Task::ready(Ok(())) - }) { - task.await - } else { - Ok(()) - } - })) - } else { - Some(Task::ready(Ok(()))) - } - })?; - if let Some(task) = spawned_test_task { - task.await?; - } - - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - - pub fn confirm_code_action( - &mut self, - action: &ConfirmCodeAction, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - - let actions_menu = - if let CodeContextMenu::CodeActions(menu) = self.hide_context_menu(window, cx)? { - menu - } else { - return None; - }; - - let action_ix = action.item_ix.unwrap_or(actions_menu.selected_item); - let action = actions_menu.actions.get(action_ix)?; - let title = action.label(); - let buffer = actions_menu.buffer; - let workspace = self.workspace()?; - - match action { - CodeActionsItem::Task(task_source_kind, resolved_task) => { - workspace.update(cx, |workspace, cx| { - workspace.schedule_resolved_task( - task_source_kind, - resolved_task, - false, - window, - cx, - ); - - Some(Task::ready(Ok(()))) - }) - } - CodeActionsItem::CodeAction { - excerpt_id, - action, - provider, - } => { - let apply_code_action = - provider.apply_code_action(buffer, action, excerpt_id, true, window, cx); - let workspace = workspace.downgrade(); - Some(cx.spawn_in(window, async move |editor, cx| { - let project_transaction = apply_code_action.await?; - Self::open_project_transaction( - &editor, - workspace, - project_transaction, - title, - cx, - ) - .await - })) - } - CodeActionsItem::DebugScenario(scenario) => { - let context = actions_menu.actions.context.clone(); - - workspace.update(cx, |workspace, cx| { - workspace.start_debug_session(scenario, context, Some(buffer), window, cx); - }); - Some(Task::ready(Ok(()))) - } - } - } - - pub async fn open_project_transaction( - this: &WeakEntity, - workspace: WeakEntity, - transaction: ProjectTransaction, - title: String, - cx: &mut AsyncWindowContext, - ) -> Result<()> { - let mut entries = transaction.0.into_iter().collect::>(); - cx.update(|_, cx| { - entries.sort_unstable_by_key(|(buffer, _)| { - buffer.read(cx).file().map(|f| f.path().clone()) - }); - })?; - - // If the project transaction's edits are all contained within this editor, then - // avoid opening a new editor to display them. - - if let Some((buffer, transaction)) = entries.first() { - if entries.len() == 1 { - let excerpt = this.update(cx, |editor, cx| { - editor - .buffer() - .read(cx) - .excerpt_containing(editor.selections.newest_anchor().head(), cx) - })?; - if let Some((_, excerpted_buffer, excerpt_range)) = excerpt { - if excerpted_buffer == *buffer { - let all_edits_within_excerpt = buffer.read_with(cx, |buffer, _| { - let excerpt_range = excerpt_range.to_offset(buffer); - buffer - .edited_ranges_for_transaction::(transaction) - .all(|range| { - excerpt_range.start <= range.start - && excerpt_range.end >= range.end - }) - })?; - - if all_edits_within_excerpt { - return Ok(()); - } - } - } - } - } else { - return Ok(()); - } - - let mut ranges_to_highlight = Vec::new(); - let excerpt_buffer = cx.new(|cx| { - let mut multibuffer = MultiBuffer::new(Capability::ReadWrite).with_title(title); - for (buffer_handle, transaction) in &entries { - let edited_ranges = buffer_handle - .read(cx) - .edited_ranges_for_transaction::(transaction) - .collect::>(); - let (ranges, _) = multibuffer.set_excerpts_for_path( - PathKey::for_buffer(buffer_handle, cx), - buffer_handle.clone(), - edited_ranges, - DEFAULT_MULTIBUFFER_CONTEXT, - cx, - ); - - ranges_to_highlight.extend(ranges); - } - multibuffer.push_transaction(entries.iter().map(|(b, t)| (b, t)), cx); - multibuffer - })?; - - workspace.update_in(cx, |workspace, window, cx| { - let project = workspace.project().clone(); - let editor = - cx.new(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), window, cx)); - workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx); - editor.update(cx, |editor, cx| { - editor.highlight_background::( - &ranges_to_highlight, - |theme| theme.editor_highlighted_line_background, - cx, - ); - }); - })?; - - Ok(()) - } - - pub fn clear_code_action_providers(&mut self) { - self.code_action_providers.clear(); - self.available_code_actions.take(); - } - - pub fn add_code_action_provider( - &mut self, - provider: Rc, - window: &mut Window, - cx: &mut Context, - ) { - if self - .code_action_providers - .iter() - .any(|existing_provider| existing_provider.id() == provider.id()) - { - return; - } - - self.code_action_providers.push(provider); - self.refresh_code_actions(window, cx); - } - - pub fn remove_code_action_provider( - &mut self, - id: Arc, - window: &mut Window, - cx: &mut Context, - ) { - self.code_action_providers - .retain(|provider| provider.id() != id); - self.refresh_code_actions(window, cx); - } - - fn refresh_code_actions(&mut self, window: &mut Window, cx: &mut Context) -> Option<()> { - let newest_selection = self.selections.newest_anchor().clone(); - let newest_selection_adjusted = self.selections.newest_adjusted(cx).clone(); - let buffer = self.buffer.read(cx); - if newest_selection.head().diff_base_anchor.is_some() { - return None; - } - let (start_buffer, start) = - buffer.text_anchor_for_position(newest_selection_adjusted.start, cx)?; - let (end_buffer, end) = - buffer.text_anchor_for_position(newest_selection_adjusted.end, cx)?; - if start_buffer != end_buffer { - return None; - } - - self.code_actions_task = Some(cx.spawn_in(window, async move |this, cx| { - cx.background_executor() - .timer(CODE_ACTIONS_DEBOUNCE_TIMEOUT) - .await; - - let (providers, tasks) = this.update_in(cx, |this, window, cx| { - let providers = this.code_action_providers.clone(); - let tasks = this - .code_action_providers - .iter() - .map(|provider| provider.code_actions(&start_buffer, start..end, window, cx)) - .collect::>(); - (providers, tasks) - })?; - - let mut actions = Vec::new(); - for (provider, provider_actions) in - providers.into_iter().zip(future::join_all(tasks).await) - { - if let Some(provider_actions) = provider_actions.log_err() { - actions.extend(provider_actions.into_iter().map(|action| { - AvailableCodeAction { - excerpt_id: newest_selection.start.excerpt_id, - action, - provider: provider.clone(), - } - })); - } - } - - this.update(cx, |this, cx| { - this.available_code_actions = if actions.is_empty() { - None - } else { - Some(( - Location { - buffer: start_buffer, - range: start..end, - }, - actions.into(), - )) - }; - cx.notify(); - }) - })); - None - } - - fn start_inline_blame_timer(&mut self, window: &mut Window, cx: &mut Context) { - if let Some(delay) = ProjectSettings::get_global(cx).git.inline_blame_delay() { - self.show_git_blame_inline = false; - - self.show_git_blame_inline_delay_task = - Some(cx.spawn_in(window, async move |this, cx| { - cx.background_executor().timer(delay).await; - - this.update(cx, |this, cx| { - this.show_git_blame_inline = true; - cx.notify(); - }) - .log_err(); - })); - } - } - - fn show_blame_popover( - &mut self, - blame_entry: &BlameEntry, - position: gpui::Point, - cx: &mut Context, - ) { - if let Some(state) = &mut self.inline_blame_popover { - state.hide_task.take(); - cx.notify(); - } else { - let delay = EditorSettings::get_global(cx).hover_popover_delay; - let show_task = cx.spawn(async move |editor, cx| { - cx.background_executor() - .timer(std::time::Duration::from_millis(delay)) - .await; - editor - .update(cx, |editor, cx| { - if let Some(state) = &mut editor.inline_blame_popover { - state.show_task = None; - cx.notify(); - } - }) - .ok(); - }); - let Some(blame) = self.blame.as_ref() else { - return; - }; - let blame = blame.read(cx); - let details = blame.details_for_entry(&blame_entry); - let markdown = cx.new(|cx| { - Markdown::new( - details - .as_ref() - .map(|message| message.message.clone()) - .unwrap_or_default(), - None, - None, - cx, - ) - }); - self.inline_blame_popover = Some(InlineBlamePopover { - position, - show_task: Some(show_task), - hide_task: None, - popover_bounds: None, - popover_state: InlineBlamePopoverState { - scroll_handle: ScrollHandle::new(), - commit_message: details, - markdown, - }, - }); - } - } - - fn hide_blame_popover(&mut self, cx: &mut Context) { - if let Some(state) = &mut self.inline_blame_popover { - if state.show_task.is_some() { - self.inline_blame_popover.take(); - cx.notify(); - } else { - let hide_task = cx.spawn(async move |editor, cx| { - cx.background_executor() - .timer(std::time::Duration::from_millis(100)) - .await; - editor - .update(cx, |editor, cx| { - editor.inline_blame_popover.take(); - cx.notify(); - }) - .ok(); - }); - state.hide_task = Some(hide_task); - } - } - } - - fn refresh_document_highlights(&mut self, cx: &mut Context) -> Option<()> { - if self.pending_rename.is_some() { - return None; - } - - let provider = self.semantics_provider.clone()?; - let buffer = self.buffer.read(cx); - let newest_selection = self.selections.newest_anchor().clone(); - let cursor_position = newest_selection.head(); - let (cursor_buffer, cursor_buffer_position) = - buffer.text_anchor_for_position(cursor_position, cx)?; - let (tail_buffer, _) = buffer.text_anchor_for_position(newest_selection.tail(), cx)?; - if cursor_buffer != tail_buffer { - return None; - } - let debounce = EditorSettings::get_global(cx).lsp_highlight_debounce; - self.document_highlights_task = Some(cx.spawn(async move |this, cx| { - cx.background_executor() - .timer(Duration::from_millis(debounce)) - .await; - - let highlights = if let Some(highlights) = cx - .update(|cx| { - provider.document_highlights(&cursor_buffer, cursor_buffer_position, cx) - }) - .ok() - .flatten() - { - highlights.await.log_err() - } else { - None - }; - - if let Some(highlights) = highlights { - this.update(cx, |this, cx| { - if this.pending_rename.is_some() { - return; - } - - let buffer_id = cursor_position.buffer_id; - let buffer = this.buffer.read(cx); - if !buffer - .text_anchor_for_position(cursor_position, cx) - .map_or(false, |(buffer, _)| buffer == cursor_buffer) - { - return; - } - - let cursor_buffer_snapshot = cursor_buffer.read(cx); - let mut write_ranges = Vec::new(); - let mut read_ranges = Vec::new(); - for highlight in highlights { - for (excerpt_id, excerpt_range) in - buffer.excerpts_for_buffer(cursor_buffer.read(cx).remote_id(), cx) - { - let start = highlight - .range - .start - .max(&excerpt_range.context.start, cursor_buffer_snapshot); - let end = highlight - .range - .end - .min(&excerpt_range.context.end, cursor_buffer_snapshot); - if start.cmp(&end, cursor_buffer_snapshot).is_ge() { - continue; - } - - let range = Anchor { - buffer_id, - excerpt_id, - text_anchor: start, - diff_base_anchor: None, - }..Anchor { - buffer_id, - excerpt_id, - text_anchor: end, - diff_base_anchor: None, - }; - if highlight.kind == lsp::DocumentHighlightKind::WRITE { - write_ranges.push(range); - } else { - read_ranges.push(range); - } - } - } - - this.highlight_background::( - &read_ranges, - |theme| theme.editor_document_highlight_read_background, - cx, - ); - this.highlight_background::( - &write_ranges, - |theme| theme.editor_document_highlight_write_background, - cx, - ); - cx.notify(); - }) - .log_err(); - } - })); - None - } - - fn prepare_highlight_query_from_selection( - &mut self, - cx: &mut Context, - ) -> Option<(String, Range)> { - if matches!(self.mode, EditorMode::SingleLine { .. }) { - return None; - } - if !EditorSettings::get_global(cx).selection_highlight { - return None; - } - if self.selections.count() != 1 || self.selections.line_mode { - return None; - } - let selection = self.selections.newest::(cx); - if selection.is_empty() || selection.start.row != selection.end.row { - return None; - } - let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx); - let selection_anchor_range = selection.range().to_anchors(&multi_buffer_snapshot); - let query = multi_buffer_snapshot - .text_for_range(selection_anchor_range.clone()) - .collect::(); - if query.trim().is_empty() { - return None; - } - Some((query, selection_anchor_range)) - } - - fn update_selection_occurrence_highlights( - &mut self, - query_text: String, - query_range: Range, - multi_buffer_range_to_query: Range, - use_debounce: bool, - window: &mut Window, - cx: &mut Context, - ) -> Task<()> { - let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx); - cx.spawn_in(window, async move |editor, cx| { - if use_debounce { - cx.background_executor() - .timer(SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT) - .await; - } - let match_task = cx.background_spawn(async move { - let buffer_ranges = multi_buffer_snapshot - .range_to_buffer_ranges(multi_buffer_range_to_query) - .into_iter() - .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty()); - let mut match_ranges = Vec::new(); - for (buffer_snapshot, search_range, excerpt_id) in buffer_ranges { - match_ranges.extend( - project::search::SearchQuery::text( - query_text.clone(), - false, - false, - false, - Default::default(), - Default::default(), - false, - None, - ) - .unwrap() - .search(&buffer_snapshot, Some(search_range.clone())) - .await - .into_iter() - .filter_map(|match_range| { - let match_start = buffer_snapshot - .anchor_after(search_range.start + match_range.start); - let match_end = - buffer_snapshot.anchor_before(search_range.start + match_range.end); - let match_anchor_range = Anchor::range_in_buffer( - excerpt_id, - buffer_snapshot.remote_id(), - match_start..match_end, - ); - (match_anchor_range != query_range).then_some(match_anchor_range) - }), - ); - } - match_ranges - }); - let match_ranges = match_task.await; - editor - .update_in(cx, |editor, _, cx| { - editor.clear_background_highlights::(cx); - if !match_ranges.is_empty() { - editor.highlight_background::( - &match_ranges, - |theme| theme.editor_document_highlight_bracket_background, - cx, - ) - } - }) - .log_err(); - }) - } - - fn refresh_selected_text_highlights( - &mut self, - on_buffer_edit: bool, - window: &mut Window, - cx: &mut Context, - ) { - let Some((query_text, query_range)) = self.prepare_highlight_query_from_selection(cx) - else { - self.clear_background_highlights::(cx); - self.quick_selection_highlight_task.take(); - self.debounced_selection_highlight_task.take(); - return; - }; - let multi_buffer_snapshot = self.buffer().read(cx).snapshot(cx); - if on_buffer_edit - || self - .quick_selection_highlight_task - .as_ref() - .map_or(true, |(prev_anchor_range, _)| { - prev_anchor_range != &query_range - }) - { - let multi_buffer_visible_start = self - .scroll_manager - .anchor() - .anchor - .to_point(&multi_buffer_snapshot); - let multi_buffer_visible_end = multi_buffer_snapshot.clip_point( - multi_buffer_visible_start - + Point::new(self.visible_line_count().unwrap_or(0.).ceil() as u32, 0), - Bias::Left, - ); - let multi_buffer_visible_range = multi_buffer_visible_start..multi_buffer_visible_end; - self.quick_selection_highlight_task = Some(( - query_range.clone(), - self.update_selection_occurrence_highlights( - query_text.clone(), - query_range.clone(), - multi_buffer_visible_range, - false, - window, - cx, - ), - )); - } - if on_buffer_edit - || self - .debounced_selection_highlight_task - .as_ref() - .map_or(true, |(prev_anchor_range, _)| { - prev_anchor_range != &query_range - }) - { - let multi_buffer_start = multi_buffer_snapshot - .anchor_before(0) - .to_point(&multi_buffer_snapshot); - let multi_buffer_end = multi_buffer_snapshot - .anchor_after(multi_buffer_snapshot.len()) - .to_point(&multi_buffer_snapshot); - let multi_buffer_full_range = multi_buffer_start..multi_buffer_end; - self.debounced_selection_highlight_task = Some(( - query_range.clone(), - self.update_selection_occurrence_highlights( - query_text, - query_range, - multi_buffer_full_range, - true, - window, - cx, - ), - )); - } - } - - pub fn refresh_inline_completion( - &mut self, - debounce: bool, - user_requested: bool, - window: &mut Window, - cx: &mut Context, - ) -> Option<()> { - let provider = self.edit_prediction_provider()?; - let cursor = self.selections.newest_anchor().head(); - let (buffer, cursor_buffer_position) = - self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; - - if !self.edit_predictions_enabled_in_buffer(&buffer, cursor_buffer_position, cx) { - self.discard_inline_completion(false, cx); - return None; - } - - if !user_requested - && (!self.should_show_edit_predictions() - || !self.is_focused(window) - || buffer.read(cx).is_empty()) - { - self.discard_inline_completion(false, cx); - return None; - } - - self.update_visible_inline_completion(window, cx); - provider.refresh( - self.project.clone(), - buffer, - cursor_buffer_position, - debounce, - cx, - ); - Some(()) - } - - fn show_edit_predictions_in_menu(&self) -> bool { - match self.edit_prediction_settings { - EditPredictionSettings::Disabled => false, - EditPredictionSettings::Enabled { show_in_menu, .. } => show_in_menu, - } - } - - pub fn edit_predictions_enabled(&self) -> bool { - match self.edit_prediction_settings { - EditPredictionSettings::Disabled => false, - EditPredictionSettings::Enabled { .. } => true, - } - } - - fn edit_prediction_requires_modifier(&self) -> bool { - match self.edit_prediction_settings { - EditPredictionSettings::Disabled => false, - EditPredictionSettings::Enabled { - preview_requires_modifier, - .. - } => preview_requires_modifier, - } - } - - pub fn update_edit_prediction_settings(&mut self, cx: &mut Context) { - if self.edit_prediction_provider.is_none() { - self.edit_prediction_settings = EditPredictionSettings::Disabled; - } else { - let selection = self.selections.newest_anchor(); - let cursor = selection.head(); - - if let Some((buffer, cursor_buffer_position)) = - self.buffer.read(cx).text_anchor_for_position(cursor, cx) - { - self.edit_prediction_settings = - self.edit_prediction_settings_at_position(&buffer, cursor_buffer_position, cx); - } - } - } - - fn edit_prediction_settings_at_position( - &self, - buffer: &Entity, - buffer_position: language::Anchor, - cx: &App, - ) -> EditPredictionSettings { - if !self.mode.is_full() - || !self.show_inline_completions_override.unwrap_or(true) - || self.inline_completions_disabled_in_scope(buffer, buffer_position, cx) - { - return EditPredictionSettings::Disabled; - } - - let buffer = buffer.read(cx); - - let file = buffer.file(); - - if !language_settings(buffer.language().map(|l| l.name()), file, cx).show_edit_predictions { - return EditPredictionSettings::Disabled; - }; - - let by_provider = matches!( - self.menu_inline_completions_policy, - MenuInlineCompletionsPolicy::ByProvider - ); - - let show_in_menu = by_provider - && self - .edit_prediction_provider - .as_ref() - .map_or(false, |provider| { - provider.provider.show_completions_in_menu() - }); - - let preview_requires_modifier = - all_language_settings(file, cx).edit_predictions_mode() == EditPredictionsMode::Subtle; - - EditPredictionSettings::Enabled { - show_in_menu, - preview_requires_modifier, - } - } - - fn should_show_edit_predictions(&self) -> bool { - self.snippet_stack.is_empty() && self.edit_predictions_enabled() - } - - pub fn edit_prediction_preview_is_active(&self) -> bool { - matches!( - self.edit_prediction_preview, - EditPredictionPreview::Active { .. } - ) - } - - pub fn edit_predictions_enabled_at_cursor(&self, cx: &App) -> bool { - let cursor = self.selections.newest_anchor().head(); - if let Some((buffer, cursor_position)) = - self.buffer.read(cx).text_anchor_for_position(cursor, cx) - { - self.edit_predictions_enabled_in_buffer(&buffer, cursor_position, cx) - } else { - false - } - } - - fn edit_predictions_enabled_in_buffer( - &self, - buffer: &Entity, - buffer_position: language::Anchor, - cx: &App, - ) -> bool { - maybe!({ - if self.read_only(cx) { - return Some(false); - } - let provider = self.edit_prediction_provider()?; - if !provider.is_enabled(&buffer, buffer_position, cx) { - return Some(false); - } - let buffer = buffer.read(cx); - let Some(file) = buffer.file() else { - return Some(true); - }; - let settings = all_language_settings(Some(file), cx); - Some(settings.edit_predictions_enabled_for_file(file, cx)) - }) - .unwrap_or(false) - } - - fn cycle_inline_completion( - &mut self, - direction: Direction, - window: &mut Window, - cx: &mut Context, - ) -> Option<()> { - let provider = self.edit_prediction_provider()?; - let cursor = self.selections.newest_anchor().head(); - let (buffer, cursor_buffer_position) = - self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; - if self.inline_completions_hidden_for_vim_mode || !self.should_show_edit_predictions() { - return None; - } - - provider.cycle(buffer, cursor_buffer_position, direction, cx); - self.update_visible_inline_completion(window, cx); - - Some(()) - } - - pub fn show_inline_completion( - &mut self, - _: &ShowEditPrediction, - window: &mut Window, - cx: &mut Context, - ) { - if !self.has_active_inline_completion() { - self.refresh_inline_completion(false, true, window, cx); - return; - } - - self.update_visible_inline_completion(window, cx); - } - - pub fn display_cursor_names( - &mut self, - _: &DisplayCursorNames, - window: &mut Window, - cx: &mut Context, - ) { - self.show_cursor_names(window, cx); - } - - fn show_cursor_names(&mut self, window: &mut Window, cx: &mut Context) { - self.show_cursor_names = true; - cx.notify(); - cx.spawn_in(window, async move |this, cx| { - cx.background_executor().timer(CURSORS_VISIBLE_FOR).await; - this.update(cx, |this, cx| { - this.show_cursor_names = false; - cx.notify() - }) - .ok() - }) - .detach(); - } - - pub fn next_edit_prediction( - &mut self, - _: &NextEditPrediction, - window: &mut Window, - cx: &mut Context, - ) { - if self.has_active_inline_completion() { - self.cycle_inline_completion(Direction::Next, window, cx); - } else { - let is_copilot_disabled = self - .refresh_inline_completion(false, true, window, cx) - .is_none(); - if is_copilot_disabled { - cx.propagate(); - } - } - } - - pub fn previous_edit_prediction( - &mut self, - _: &PreviousEditPrediction, - window: &mut Window, - cx: &mut Context, - ) { - if self.has_active_inline_completion() { - self.cycle_inline_completion(Direction::Prev, window, cx); - } else { - let is_copilot_disabled = self - .refresh_inline_completion(false, true, window, cx) - .is_none(); - if is_copilot_disabled { - cx.propagate(); - } - } - } - - pub fn accept_edit_prediction( - &mut self, - _: &AcceptEditPrediction, - window: &mut Window, - cx: &mut Context, - ) { - if self.show_edit_predictions_in_menu() { - self.hide_context_menu(window, cx); - } - - let Some(active_inline_completion) = self.active_inline_completion.as_ref() else { - return; - }; - - self.report_inline_completion_event( - active_inline_completion.completion_id.clone(), - true, - cx, - ); - - match &active_inline_completion.completion { - InlineCompletion::Move { target, .. } => { - let target = *target; - - if let Some(position_map) = &self.last_position_map { - if position_map - .visible_row_range - .contains(&target.to_display_point(&position_map.snapshot).row()) - || !self.edit_prediction_requires_modifier() - { - self.unfold_ranges(&[target..target], true, false, cx); - // Note that this is also done in vim's handler of the Tab action. - self.change_selections( - Some(Autoscroll::newest()), - window, - cx, - |selections| { - selections.select_anchor_ranges([target..target]); - }, - ); - self.clear_row_highlights::(); - - self.edit_prediction_preview - .set_previous_scroll_position(None); - } else { - self.edit_prediction_preview - .set_previous_scroll_position(Some( - position_map.snapshot.scroll_anchor, - )); - - self.highlight_rows::( - target..target, - cx.theme().colors().editor_highlighted_line_background, - RowHighlightOptions { - autoscroll: true, - ..Default::default() - }, - cx, - ); - self.request_autoscroll(Autoscroll::fit(), cx); - } - } - } - InlineCompletion::Edit { edits, .. } => { - if let Some(provider) = self.edit_prediction_provider() { - provider.accept(cx); - } - - let snapshot = self.buffer.read(cx).snapshot(cx); - let last_edit_end = edits.last().unwrap().0.end.bias_right(&snapshot); - - self.buffer.update(cx, |buffer, cx| { - buffer.edit(edits.iter().cloned(), None, cx) - }); - - self.change_selections(None, window, cx, |s| { - s.select_anchor_ranges([last_edit_end..last_edit_end]) - }); - - self.update_visible_inline_completion(window, cx); - if self.active_inline_completion.is_none() { - self.refresh_inline_completion(true, true, window, cx); - } - - cx.notify(); - } - } - - self.edit_prediction_requires_modifier_in_indent_conflict = false; - } - - pub fn accept_partial_inline_completion( - &mut self, - _: &AcceptPartialEditPrediction, - window: &mut Window, - cx: &mut Context, - ) { - let Some(active_inline_completion) = self.active_inline_completion.as_ref() else { - return; - }; - if self.selections.count() != 1 { - return; - } - - self.report_inline_completion_event( - active_inline_completion.completion_id.clone(), - true, - cx, - ); - - match &active_inline_completion.completion { - InlineCompletion::Move { target, .. } => { - let target = *target; - self.change_selections(Some(Autoscroll::newest()), window, cx, |selections| { - selections.select_anchor_ranges([target..target]); - }); - } - InlineCompletion::Edit { edits, .. } => { - // Find an insertion that starts at the cursor position. - let snapshot = self.buffer.read(cx).snapshot(cx); - let cursor_offset = self.selections.newest::(cx).head(); - let insertion = edits.iter().find_map(|(range, text)| { - let range = range.to_offset(&snapshot); - if range.is_empty() && range.start == cursor_offset { - Some(text) - } else { - None - } - }); - - if let Some(text) = insertion { - let mut partial_completion = text - .chars() - .by_ref() - .take_while(|c| c.is_alphabetic()) - .collect::(); - if partial_completion.is_empty() { - partial_completion = text - .chars() - .by_ref() - .take_while(|c| c.is_whitespace() || !c.is_alphabetic()) - .collect::(); - } - - cx.emit(EditorEvent::InputHandled { - utf16_range_to_replace: None, - text: partial_completion.clone().into(), - }); - - self.insert_with_autoindent_mode(&partial_completion, None, window, cx); - - self.refresh_inline_completion(true, true, window, cx); - cx.notify(); - } else { - self.accept_edit_prediction(&Default::default(), window, cx); - } - } - } - } - - fn discard_inline_completion( - &mut self, - should_report_inline_completion_event: bool, - cx: &mut Context, - ) -> bool { - if should_report_inline_completion_event { - let completion_id = self - .active_inline_completion - .as_ref() - .and_then(|active_completion| active_completion.completion_id.clone()); - - self.report_inline_completion_event(completion_id, false, cx); - } - - if let Some(provider) = self.edit_prediction_provider() { - provider.discard(cx); - } - - self.take_active_inline_completion(cx) - } - - fn report_inline_completion_event(&self, id: Option, accepted: bool, cx: &App) { - let Some(provider) = self.edit_prediction_provider() else { - return; - }; - - let Some((_, buffer, _)) = self - .buffer - .read(cx) - .excerpt_containing(self.selections.newest_anchor().head(), cx) - else { - return; - }; - - let extension = buffer - .read(cx) - .file() - .and_then(|file| Some(file.path().extension()?.to_string_lossy().to_string())); - - let event_type = match accepted { - true => "Edit Prediction Accepted", - false => "Edit Prediction Discarded", - }; - telemetry::event!( - event_type, - provider = provider.name(), - prediction_id = id, - suggestion_accepted = accepted, - file_extension = extension, - ); - } - - pub fn has_active_inline_completion(&self) -> bool { - self.active_inline_completion.is_some() - } - - fn take_active_inline_completion(&mut self, cx: &mut Context) -> bool { - let Some(active_inline_completion) = self.active_inline_completion.take() else { - return false; - }; - - self.splice_inlays(&active_inline_completion.inlay_ids, Default::default(), cx); - self.clear_highlights::(cx); - self.stale_inline_completion_in_menu = Some(active_inline_completion); - true - } - - /// Returns true when we're displaying the edit prediction popover below the cursor - /// like we are not previewing and the LSP autocomplete menu is visible - /// or we are in `when_holding_modifier` mode. - pub fn edit_prediction_visible_in_cursor_popover(&self, has_completion: bool) -> bool { - if self.edit_prediction_preview_is_active() - || !self.show_edit_predictions_in_menu() - || !self.edit_predictions_enabled() - { - return false; - } - - if self.has_visible_completions_menu() { - return true; - } - - has_completion && self.edit_prediction_requires_modifier() - } - - fn handle_modifiers_changed( - &mut self, - modifiers: Modifiers, - position_map: &PositionMap, - window: &mut Window, - cx: &mut Context, - ) { - if self.show_edit_predictions_in_menu() { - self.update_edit_prediction_preview(&modifiers, window, cx); - } - - self.update_selection_mode(&modifiers, position_map, window, cx); - - let mouse_position = window.mouse_position(); - if !position_map.text_hitbox.is_hovered(window) { - return; - } - - self.update_hovered_link( - position_map.point_for_position(mouse_position), - &position_map.snapshot, - modifiers, - window, - cx, - ) - } - - fn update_selection_mode( - &mut self, - modifiers: &Modifiers, - position_map: &PositionMap, - window: &mut Window, - cx: &mut Context, - ) { - if modifiers != &COLUMNAR_SELECTION_MODIFIERS || self.selections.pending.is_none() { - return; - } - - let mouse_position = window.mouse_position(); - let point_for_position = position_map.point_for_position(mouse_position); - let position = point_for_position.previous_valid; - - self.select( - SelectPhase::BeginColumnar { - position, - reset: false, - goal_column: point_for_position.exact_unclipped.column(), - }, - window, - cx, - ); - } - - fn update_edit_prediction_preview( - &mut self, - modifiers: &Modifiers, - window: &mut Window, - cx: &mut Context, - ) { - let accept_keybind = self.accept_edit_prediction_keybind(window, cx); - let Some(accept_keystroke) = accept_keybind.keystroke() else { - return; - }; - - if &accept_keystroke.modifiers == modifiers && accept_keystroke.modifiers.modified() { - if matches!( - self.edit_prediction_preview, - EditPredictionPreview::Inactive { .. } - ) { - self.edit_prediction_preview = EditPredictionPreview::Active { - previous_scroll_position: None, - since: Instant::now(), - }; - - self.update_visible_inline_completion(window, cx); - cx.notify(); - } - } else if let EditPredictionPreview::Active { - previous_scroll_position, - since, - } = self.edit_prediction_preview - { - if let (Some(previous_scroll_position), Some(position_map)) = - (previous_scroll_position, self.last_position_map.as_ref()) - { - self.set_scroll_position( - previous_scroll_position - .scroll_position(&position_map.snapshot.display_snapshot), - window, - cx, - ); - } - - self.edit_prediction_preview = EditPredictionPreview::Inactive { - released_too_fast: since.elapsed() < Duration::from_millis(200), - }; - self.clear_row_highlights::(); - self.update_visible_inline_completion(window, cx); - cx.notify(); - } - } - - fn update_visible_inline_completion( - &mut self, - _window: &mut Window, - cx: &mut Context, - ) -> Option<()> { - let selection = self.selections.newest_anchor(); - let cursor = selection.head(); - let multibuffer = self.buffer.read(cx).snapshot(cx); - let offset_selection = selection.map(|endpoint| endpoint.to_offset(&multibuffer)); - let excerpt_id = cursor.excerpt_id; - - let show_in_menu = self.show_edit_predictions_in_menu(); - let completions_menu_has_precedence = !show_in_menu - && (self.context_menu.borrow().is_some() - || (!self.completion_tasks.is_empty() && !self.has_active_inline_completion())); - - if completions_menu_has_precedence - || !offset_selection.is_empty() - || self - .active_inline_completion - .as_ref() - .map_or(false, |completion| { - let invalidation_range = completion.invalidation_range.to_offset(&multibuffer); - let invalidation_range = invalidation_range.start..=invalidation_range.end; - !invalidation_range.contains(&offset_selection.head()) - }) - { - self.discard_inline_completion(false, cx); - return None; - } - - self.take_active_inline_completion(cx); - let Some(provider) = self.edit_prediction_provider() else { - self.edit_prediction_settings = EditPredictionSettings::Disabled; - return None; - }; - - let (buffer, cursor_buffer_position) = - self.buffer.read(cx).text_anchor_for_position(cursor, cx)?; - - self.edit_prediction_settings = - self.edit_prediction_settings_at_position(&buffer, cursor_buffer_position, cx); - - self.edit_prediction_indent_conflict = multibuffer.is_line_whitespace_upto(cursor); - - if self.edit_prediction_indent_conflict { - let cursor_point = cursor.to_point(&multibuffer); - - let indents = multibuffer.suggested_indents(cursor_point.row..cursor_point.row + 1, cx); - - if let Some((_, indent)) = indents.iter().next() { - if indent.len == cursor_point.column { - self.edit_prediction_indent_conflict = false; - } - } - } - - let inline_completion = provider.suggest(&buffer, cursor_buffer_position, cx)?; - let edits = inline_completion - .edits - .into_iter() - .flat_map(|(range, new_text)| { - let start = multibuffer.anchor_in_excerpt(excerpt_id, range.start)?; - let end = multibuffer.anchor_in_excerpt(excerpt_id, range.end)?; - Some((start..end, new_text)) - }) - .collect::>(); - if edits.is_empty() { - return None; - } - - let first_edit_start = edits.first().unwrap().0.start; - let first_edit_start_point = first_edit_start.to_point(&multibuffer); - let edit_start_row = first_edit_start_point.row.saturating_sub(2); - - let last_edit_end = edits.last().unwrap().0.end; - let last_edit_end_point = last_edit_end.to_point(&multibuffer); - let edit_end_row = cmp::min(multibuffer.max_point().row, last_edit_end_point.row + 2); - - let cursor_row = cursor.to_point(&multibuffer).row; - - let snapshot = multibuffer.buffer_for_excerpt(excerpt_id).cloned()?; - - let mut inlay_ids = Vec::new(); - let invalidation_row_range; - let move_invalidation_row_range = if cursor_row < edit_start_row { - Some(cursor_row..edit_end_row) - } else if cursor_row > edit_end_row { - Some(edit_start_row..cursor_row) - } else { - None - }; - let is_move = - move_invalidation_row_range.is_some() || self.inline_completions_hidden_for_vim_mode; - let completion = if is_move { - invalidation_row_range = - move_invalidation_row_range.unwrap_or(edit_start_row..edit_end_row); - let target = first_edit_start; - InlineCompletion::Move { target, snapshot } - } else { - let show_completions_in_buffer = !self.edit_prediction_visible_in_cursor_popover(true) - && !self.inline_completions_hidden_for_vim_mode; - - if show_completions_in_buffer { - if edits - .iter() - .all(|(range, _)| range.to_offset(&multibuffer).is_empty()) - { - let mut inlays = Vec::new(); - for (range, new_text) in &edits { - let inlay = Inlay::inline_completion( - post_inc(&mut self.next_inlay_id), - range.start, - new_text.as_str(), - ); - inlay_ids.push(inlay.id); - inlays.push(inlay); - } - - self.splice_inlays(&[], inlays, cx); - } else { - let background_color = cx.theme().status().deleted_background; - self.highlight_text::( - edits.iter().map(|(range, _)| range.clone()).collect(), - HighlightStyle { - background_color: Some(background_color), - ..Default::default() - }, - cx, - ); - } - } - - invalidation_row_range = edit_start_row..edit_end_row; - - let display_mode = if all_edits_insertions_or_deletions(&edits, &multibuffer) { - if provider.show_tab_accept_marker() { - EditDisplayMode::TabAccept - } else { - EditDisplayMode::Inline - } - } else { - EditDisplayMode::DiffPopover - }; - - InlineCompletion::Edit { - edits, - edit_preview: inline_completion.edit_preview, - display_mode, - snapshot, - } - }; - - let invalidation_range = multibuffer - .anchor_before(Point::new(invalidation_row_range.start, 0)) - ..multibuffer.anchor_after(Point::new( - invalidation_row_range.end, - multibuffer.line_len(MultiBufferRow(invalidation_row_range.end)), - )); - - self.stale_inline_completion_in_menu = None; - self.active_inline_completion = Some(InlineCompletionState { - inlay_ids, - completion, - completion_id: inline_completion.id, - invalidation_range, - }); - - cx.notify(); - - Some(()) - } - - pub fn edit_prediction_provider(&self) -> Option> { - Some(self.edit_prediction_provider.as_ref()?.provider.clone()) - } - - fn render_code_actions_indicator( - &self, - _style: &EditorStyle, - row: DisplayRow, - is_active: bool, - breakpoint: Option<&(Anchor, Breakpoint)>, - cx: &mut Context, - ) -> Option { - let color = Color::Muted; - let position = breakpoint.as_ref().map(|(anchor, _)| *anchor); - let show_tooltip = !self.context_menu_visible(); - - if self.available_code_actions.is_some() { - Some( - IconButton::new("code_actions_indicator", ui::IconName::Bolt) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::XSmall) - .icon_color(color) - .toggle_state(is_active) - .when(show_tooltip, |this| { - this.tooltip({ - let focus_handle = self.focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Toggle Code Actions", - &ToggleCodeActions { - deployed_from_indicator: None, - quick_launch: false, - }, - &focus_handle, - window, - cx, - ) - } - }) - }) - .on_click(cx.listener(move |editor, e: &ClickEvent, window, cx| { - let quick_launch = e.down.button == MouseButton::Left; - window.focus(&editor.focus_handle(cx)); - editor.toggle_code_actions( - &ToggleCodeActions { - deployed_from_indicator: Some(row), - quick_launch, - }, - window, - cx, - ); - })) - .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| { - editor.set_breakpoint_context_menu( - row, - position, - event.down.position, - window, - cx, - ); - })), - ) - } else { - None - } - } - - fn clear_tasks(&mut self) { - self.tasks.clear() - } - - fn insert_tasks(&mut self, key: (BufferId, BufferRow), value: RunnableTasks) { - if self.tasks.insert(key, value).is_some() { - // This case should hopefully be rare, but just in case... - log::error!( - "multiple different run targets found on a single line, only the last target will be rendered" - ) - } - } - - /// Get all display points of breakpoints that will be rendered within editor - /// - /// This function is used to handle overlaps between breakpoints and Code action/runner symbol. - /// It's also used to set the color of line numbers with breakpoints to the breakpoint color. - /// TODO debugger: Use this function to color toggle symbols that house nested breakpoints - fn active_breakpoints( - &self, - range: Range, - window: &mut Window, - cx: &mut Context, - ) -> HashMap { - let mut breakpoint_display_points = HashMap::default(); - - let Some(breakpoint_store) = self.breakpoint_store.clone() else { - return breakpoint_display_points; - }; - - let snapshot = self.snapshot(window, cx); - - let multi_buffer_snapshot = &snapshot.display_snapshot.buffer_snapshot; - let Some(project) = self.project.as_ref() else { - return breakpoint_display_points; - }; - - let range = snapshot.display_point_to_point(DisplayPoint::new(range.start, 0), Bias::Left) - ..snapshot.display_point_to_point(DisplayPoint::new(range.end, 0), Bias::Right); - - for (buffer_snapshot, range, excerpt_id) in - multi_buffer_snapshot.range_to_buffer_ranges(range) - { - let Some(buffer) = project.read_with(cx, |this, cx| { - this.buffer_for_id(buffer_snapshot.remote_id(), cx) - }) else { - continue; - }; - let breakpoints = breakpoint_store.read(cx).breakpoints( - &buffer, - Some( - buffer_snapshot.anchor_before(range.start) - ..buffer_snapshot.anchor_after(range.end), - ), - buffer_snapshot, - cx, - ); - for (anchor, breakpoint) in breakpoints { - let multi_buffer_anchor = - Anchor::in_buffer(excerpt_id, buffer_snapshot.remote_id(), *anchor); - let position = multi_buffer_anchor - .to_point(&multi_buffer_snapshot) - .to_display_point(&snapshot); - - breakpoint_display_points - .insert(position.row(), (multi_buffer_anchor, breakpoint.clone())); - } - } - - breakpoint_display_points - } - - fn breakpoint_context_menu( - &self, - anchor: Anchor, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - let weak_editor = cx.weak_entity(); - let focus_handle = self.focus_handle(cx); - - let row = self - .buffer - .read(cx) - .snapshot(cx) - .summary_for_anchor::(&anchor) - .row; - - let breakpoint = self - .breakpoint_at_row(row, window, cx) - .map(|(anchor, bp)| (anchor, Arc::from(bp))); - - let log_breakpoint_msg = if breakpoint.as_ref().is_some_and(|bp| bp.1.message.is_some()) { - "Edit Log Breakpoint" - } else { - "Set Log Breakpoint" - }; - - let condition_breakpoint_msg = if breakpoint - .as_ref() - .is_some_and(|bp| bp.1.condition.is_some()) - { - "Edit Condition Breakpoint" - } else { - "Set Condition Breakpoint" - }; - - let hit_condition_breakpoint_msg = if breakpoint - .as_ref() - .is_some_and(|bp| bp.1.hit_condition.is_some()) - { - "Edit Hit Condition Breakpoint" - } else { - "Set Hit Condition Breakpoint" - }; - - let set_breakpoint_msg = if breakpoint.as_ref().is_some() { - "Unset Breakpoint" - } else { - "Set Breakpoint" - }; - - let run_to_cursor = command_palette_hooks::CommandPaletteFilter::try_global(cx) - .map_or(false, |filter| !filter.is_hidden(&DebuggerRunToCursor)); - - let toggle_state_msg = breakpoint.as_ref().map_or(None, |bp| match bp.1.state { - BreakpointState::Enabled => Some("Disable"), - BreakpointState::Disabled => Some("Enable"), - }); - - let (anchor, breakpoint) = - breakpoint.unwrap_or_else(|| (anchor, Arc::new(Breakpoint::new_standard()))); - - ui::ContextMenu::build(window, cx, |menu, _, _cx| { - menu.on_blur_subscription(Subscription::new(|| {})) - .context(focus_handle) - .when(run_to_cursor, |this| { - let weak_editor = weak_editor.clone(); - this.entry("Run to cursor", None, move |window, cx| { - weak_editor - .update(cx, |editor, cx| { - editor.change_selections(None, window, cx, |s| { - s.select_ranges([Point::new(row, 0)..Point::new(row, 0)]) - }); - }) - .ok(); - - window.dispatch_action(Box::new(DebuggerRunToCursor), cx); - }) - .separator() - }) - .when_some(toggle_state_msg, |this, msg| { - this.entry(msg, None, { - let weak_editor = weak_editor.clone(); - let breakpoint = breakpoint.clone(); - move |_window, cx| { - weak_editor - .update(cx, |this, cx| { - this.edit_breakpoint_at_anchor( - anchor, - breakpoint.as_ref().clone(), - BreakpointEditAction::InvertState, - cx, - ); - }) - .log_err(); - } - }) - }) - .entry(set_breakpoint_msg, None, { - let weak_editor = weak_editor.clone(); - let breakpoint = breakpoint.clone(); - move |_window, cx| { - weak_editor - .update(cx, |this, cx| { - this.edit_breakpoint_at_anchor( - anchor, - breakpoint.as_ref().clone(), - BreakpointEditAction::Toggle, - cx, - ); - }) - .log_err(); - } - }) - .entry(log_breakpoint_msg, None, { - let breakpoint = breakpoint.clone(); - let weak_editor = weak_editor.clone(); - move |window, cx| { - weak_editor - .update(cx, |this, cx| { - this.add_edit_breakpoint_block( - anchor, - breakpoint.as_ref(), - BreakpointPromptEditAction::Log, - window, - cx, - ); - }) - .log_err(); - } - }) - .entry(condition_breakpoint_msg, None, { - let breakpoint = breakpoint.clone(); - let weak_editor = weak_editor.clone(); - move |window, cx| { - weak_editor - .update(cx, |this, cx| { - this.add_edit_breakpoint_block( - anchor, - breakpoint.as_ref(), - BreakpointPromptEditAction::Condition, - window, - cx, - ); - }) - .log_err(); - } - }) - .entry(hit_condition_breakpoint_msg, None, move |window, cx| { - weak_editor - .update(cx, |this, cx| { - this.add_edit_breakpoint_block( - anchor, - breakpoint.as_ref(), - BreakpointPromptEditAction::HitCondition, - window, - cx, - ); - }) - .log_err(); - }) - }) - } - - fn render_breakpoint( - &self, - position: Anchor, - row: DisplayRow, - breakpoint: &Breakpoint, - cx: &mut Context, - ) -> IconButton { - // Is it a breakpoint that shows up when hovering over gutter? - let (is_phantom, collides_with_existing) = self.gutter_breakpoint_indicator.0.map_or( - (false, false), - |PhantomBreakpointIndicator { - is_active, - display_row, - collides_with_existing_breakpoint, - }| { - ( - is_active && display_row == row, - collides_with_existing_breakpoint, - ) - }, - ); - - let (color, icon) = { - let icon = match (&breakpoint.message.is_some(), breakpoint.is_disabled()) { - (false, false) => ui::IconName::DebugBreakpoint, - (true, false) => ui::IconName::DebugLogBreakpoint, - (false, true) => ui::IconName::DebugDisabledBreakpoint, - (true, true) => ui::IconName::DebugDisabledLogBreakpoint, - }; - - let color = if is_phantom { - Color::Hint - } else { - Color::Debugger - }; - - (color, icon) - }; - - let breakpoint = Arc::from(breakpoint.clone()); - - let alt_as_text = gpui::Keystroke { - modifiers: Modifiers::secondary_key(), - ..Default::default() - }; - let primary_action_text = if breakpoint.is_disabled() { - "enable" - } else if is_phantom && !collides_with_existing { - "set" - } else { - "unset" - }; - let mut primary_text = format!("Click to {primary_action_text}"); - if collides_with_existing && !breakpoint.is_disabled() { - use std::fmt::Write; - write!(primary_text, ", {alt_as_text}-click to disable").ok(); - } - let primary_text = SharedString::from(primary_text); - let focus_handle = self.focus_handle.clone(); - IconButton::new(("breakpoint_indicator", row.0 as usize), icon) - .icon_size(IconSize::XSmall) - .size(ui::ButtonSize::None) - .icon_color(color) - .style(ButtonStyle::Transparent) - .on_click(cx.listener({ - let breakpoint = breakpoint.clone(); - - move |editor, event: &ClickEvent, window, cx| { - let edit_action = if event.modifiers().platform || breakpoint.is_disabled() { - BreakpointEditAction::InvertState - } else { - BreakpointEditAction::Toggle - }; - - window.focus(&editor.focus_handle(cx)); - editor.edit_breakpoint_at_anchor( - position, - breakpoint.as_ref().clone(), - edit_action, - cx, - ); - } - })) - .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| { - editor.set_breakpoint_context_menu( - row, - Some(position), - event.down.position, - window, - cx, - ); - })) - .tooltip(move |window, cx| { - Tooltip::with_meta_in( - primary_text.clone(), - None, - "Right-click for more options", - &focus_handle, - window, - cx, - ) - }) - } - - fn build_tasks_context( - project: &Entity, - buffer: &Entity, - buffer_row: u32, - tasks: &Arc, - cx: &mut Context, - ) -> Task> { - let position = Point::new(buffer_row, tasks.column); - let range_start = buffer.read(cx).anchor_at(position, Bias::Right); - let location = Location { - buffer: buffer.clone(), - range: range_start..range_start, - }; - // Fill in the environmental variables from the tree-sitter captures - let mut captured_task_variables = TaskVariables::default(); - for (capture_name, value) in tasks.extra_variables.clone() { - captured_task_variables.insert( - task::VariableName::Custom(capture_name.into()), - value.clone(), - ); - } - project.update(cx, |project, cx| { - project.task_store().update(cx, |task_store, cx| { - task_store.task_context_for_location(captured_task_variables, location, cx) - }) - }) - } - - pub fn spawn_nearest_task( - &mut self, - action: &SpawnNearestTask, - window: &mut Window, - cx: &mut Context, - ) { - let Some((workspace, _)) = self.workspace.clone() else { - return; - }; - let Some(project) = self.project.clone() else { - return; - }; - - // Try to find a closest, enclosing node using tree-sitter that has a - // task - let Some((buffer, buffer_row, tasks)) = self - .find_enclosing_node_task(cx) - // Or find the task that's closest in row-distance. - .or_else(|| self.find_closest_task(cx)) - else { - return; - }; - - let reveal_strategy = action.reveal; - let task_context = Self::build_tasks_context(&project, &buffer, buffer_row, &tasks, cx); - cx.spawn_in(window, async move |_, cx| { - let context = task_context.await?; - let (task_source_kind, mut resolved_task) = tasks.resolve(&context).next()?; - - let resolved = &mut resolved_task.resolved; - resolved.reveal = reveal_strategy; - - workspace - .update_in(cx, |workspace, window, cx| { - workspace.schedule_resolved_task( - task_source_kind, - resolved_task, - false, - window, - cx, - ); - }) - .ok() - }) - .detach(); - } - - fn find_closest_task( - &mut self, - cx: &mut Context, - ) -> Option<(Entity, u32, Arc)> { - let cursor_row = self.selections.newest_adjusted(cx).head().row; - - let ((buffer_id, row), tasks) = self - .tasks - .iter() - .min_by_key(|((_, row), _)| cursor_row.abs_diff(*row))?; - - let buffer = self.buffer.read(cx).buffer(*buffer_id)?; - let tasks = Arc::new(tasks.to_owned()); - Some((buffer, *row, tasks)) - } - - fn find_enclosing_node_task( - &mut self, - cx: &mut Context, - ) -> Option<(Entity, u32, Arc)> { - let snapshot = self.buffer.read(cx).snapshot(cx); - let offset = self.selections.newest::(cx).head(); - let excerpt = snapshot.excerpt_containing(offset..offset)?; - let buffer_id = excerpt.buffer().remote_id(); - - let layer = excerpt.buffer().syntax_layer_at(offset)?; - let mut cursor = layer.node().walk(); - - while cursor.goto_first_child_for_byte(offset).is_some() { - if cursor.node().end_byte() == offset { - cursor.goto_next_sibling(); - } - } - - // Ascend to the smallest ancestor that contains the range and has a task. - loop { - let node = cursor.node(); - let node_range = node.byte_range(); - let symbol_start_row = excerpt.buffer().offset_to_point(node.start_byte()).row; - - // Check if this node contains our offset - if node_range.start <= offset && node_range.end >= offset { - // If it contains offset, check for task - if let Some(tasks) = self.tasks.get(&(buffer_id, symbol_start_row)) { - let buffer = self.buffer.read(cx).buffer(buffer_id)?; - return Some((buffer, symbol_start_row, Arc::new(tasks.to_owned()))); - } - } - - if !cursor.goto_parent() { - break; - } - } - None - } - - fn render_run_indicator( - &self, - _style: &EditorStyle, - is_active: bool, - row: DisplayRow, - breakpoint: Option<(Anchor, Breakpoint)>, - cx: &mut Context, - ) -> IconButton { - let color = Color::Muted; - let position = breakpoint.as_ref().map(|(anchor, _)| *anchor); - - IconButton::new(("run_indicator", row.0 as usize), ui::IconName::Play) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::XSmall) - .icon_color(color) - .toggle_state(is_active) - .on_click(cx.listener(move |editor, e: &ClickEvent, window, cx| { - let quick_launch = e.down.button == MouseButton::Left; - window.focus(&editor.focus_handle(cx)); - editor.toggle_code_actions( - &ToggleCodeActions { - deployed_from_indicator: Some(row), - quick_launch, - }, - window, - cx, - ); - })) - .on_right_click(cx.listener(move |editor, event: &ClickEvent, window, cx| { - editor.set_breakpoint_context_menu(row, position, event.down.position, window, cx); - })) - } - - pub fn context_menu_visible(&self) -> bool { - !self.edit_prediction_preview_is_active() - && self - .context_menu - .borrow() - .as_ref() - .map_or(false, |menu| menu.visible()) - } - - fn context_menu_origin(&self) -> Option { - self.context_menu - .borrow() - .as_ref() - .map(|menu| menu.origin()) - } - - pub fn set_context_menu_options(&mut self, options: ContextMenuOptions) { - self.context_menu_options = Some(options); - } - - const EDIT_PREDICTION_POPOVER_PADDING_X: Pixels = Pixels(24.); - const EDIT_PREDICTION_POPOVER_PADDING_Y: Pixels = Pixels(2.); - - fn render_edit_prediction_popover( - &mut self, - text_bounds: &Bounds, - content_origin: gpui::Point, - editor_snapshot: &EditorSnapshot, - visible_row_range: Range, - scroll_top: f32, - scroll_bottom: f32, - line_layouts: &[LineWithInvisibles], - line_height: Pixels, - scroll_pixel_position: gpui::Point, - newest_selection_head: Option, - editor_width: Pixels, - style: &EditorStyle, - window: &mut Window, - cx: &mut App, - ) -> Option<(AnyElement, gpui::Point)> { - let active_inline_completion = self.active_inline_completion.as_ref()?; - - if self.edit_prediction_visible_in_cursor_popover(true) { - return None; - } - - match &active_inline_completion.completion { - InlineCompletion::Move { target, .. } => { - let target_display_point = target.to_display_point(editor_snapshot); - - if self.edit_prediction_requires_modifier() { - if !self.edit_prediction_preview_is_active() { - return None; - } - - self.render_edit_prediction_modifier_jump_popover( - text_bounds, - content_origin, - visible_row_range, - line_layouts, - line_height, - scroll_pixel_position, - newest_selection_head, - target_display_point, - window, - cx, - ) - } else { - self.render_edit_prediction_eager_jump_popover( - text_bounds, - content_origin, - editor_snapshot, - visible_row_range, - scroll_top, - scroll_bottom, - line_height, - scroll_pixel_position, - target_display_point, - editor_width, - window, - cx, - ) - } - } - InlineCompletion::Edit { - display_mode: EditDisplayMode::Inline, - .. - } => None, - InlineCompletion::Edit { - display_mode: EditDisplayMode::TabAccept, - edits, - .. - } => { - let range = &edits.first()?.0; - let target_display_point = range.end.to_display_point(editor_snapshot); - - self.render_edit_prediction_end_of_line_popover( - "Accept", - editor_snapshot, - visible_row_range, - target_display_point, - line_height, - scroll_pixel_position, - content_origin, - editor_width, - window, - cx, - ) - } - InlineCompletion::Edit { - edits, - edit_preview, - display_mode: EditDisplayMode::DiffPopover, - snapshot, - } => self.render_edit_prediction_diff_popover( - text_bounds, - content_origin, - editor_snapshot, - visible_row_range, - line_layouts, - line_height, - scroll_pixel_position, - newest_selection_head, - editor_width, - style, - edits, - edit_preview, - snapshot, - window, - cx, - ), - } - } - - fn render_edit_prediction_modifier_jump_popover( - &mut self, - text_bounds: &Bounds, - content_origin: gpui::Point, - visible_row_range: Range, - line_layouts: &[LineWithInvisibles], - line_height: Pixels, - scroll_pixel_position: gpui::Point, - newest_selection_head: Option, - target_display_point: DisplayPoint, - window: &mut Window, - cx: &mut App, - ) -> Option<(AnyElement, gpui::Point)> { - let scrolled_content_origin = - content_origin - gpui::Point::new(scroll_pixel_position.x, Pixels(0.0)); - - const SCROLL_PADDING_Y: Pixels = px(12.); - - if target_display_point.row() < visible_row_range.start { - return self.render_edit_prediction_scroll_popover( - |_| SCROLL_PADDING_Y, - IconName::ArrowUp, - visible_row_range, - line_layouts, - newest_selection_head, - scrolled_content_origin, - window, - cx, - ); - } else if target_display_point.row() >= visible_row_range.end { - return self.render_edit_prediction_scroll_popover( - |size| text_bounds.size.height - size.height - SCROLL_PADDING_Y, - IconName::ArrowDown, - visible_row_range, - line_layouts, - newest_selection_head, - scrolled_content_origin, - window, - cx, - ); - } - - const POLE_WIDTH: Pixels = px(2.); - - let line_layout = - line_layouts.get(target_display_point.row().minus(visible_row_range.start) as usize)?; - let target_column = target_display_point.column() as usize; - - let target_x = line_layout.x_for_index(target_column); - let target_y = - (target_display_point.row().as_f32() * line_height) - scroll_pixel_position.y; - - let flag_on_right = target_x < text_bounds.size.width / 2.; - - let mut border_color = Self::edit_prediction_callout_popover_border_color(cx); - border_color.l += 0.001; - - let mut element = v_flex() - .items_end() - .when(flag_on_right, |el| el.items_start()) - .child(if flag_on_right { - self.render_edit_prediction_line_popover("Jump", None, window, cx)? - .rounded_bl(px(0.)) - .rounded_tl(px(0.)) - .border_l_2() - .border_color(border_color) - } else { - self.render_edit_prediction_line_popover("Jump", None, window, cx)? - .rounded_br(px(0.)) - .rounded_tr(px(0.)) - .border_r_2() - .border_color(border_color) - }) - .child(div().w(POLE_WIDTH).bg(border_color).h(line_height)) - .into_any(); - - let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); - - let mut origin = scrolled_content_origin + point(target_x, target_y) - - point( - if flag_on_right { - POLE_WIDTH - } else { - size.width - POLE_WIDTH - }, - size.height - line_height, - ); - - origin.x = origin.x.max(content_origin.x); - - element.prepaint_at(origin, window, cx); - - Some((element, origin)) - } - - fn render_edit_prediction_scroll_popover( - &mut self, - to_y: impl Fn(Size) -> Pixels, - scroll_icon: IconName, - visible_row_range: Range, - line_layouts: &[LineWithInvisibles], - newest_selection_head: Option, - scrolled_content_origin: gpui::Point, - window: &mut Window, - cx: &mut App, - ) -> Option<(AnyElement, gpui::Point)> { - let mut element = self - .render_edit_prediction_line_popover("Scroll", Some(scroll_icon), window, cx)? - .into_any(); - - let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); - - let cursor = newest_selection_head?; - let cursor_row_layout = - line_layouts.get(cursor.row().minus(visible_row_range.start) as usize)?; - let cursor_column = cursor.column() as usize; - - let cursor_character_x = cursor_row_layout.x_for_index(cursor_column); - - let origin = scrolled_content_origin + point(cursor_character_x, to_y(size)); - - element.prepaint_at(origin, window, cx); - Some((element, origin)) - } - - fn render_edit_prediction_eager_jump_popover( - &mut self, - text_bounds: &Bounds, - content_origin: gpui::Point, - editor_snapshot: &EditorSnapshot, - visible_row_range: Range, - scroll_top: f32, - scroll_bottom: f32, - line_height: Pixels, - scroll_pixel_position: gpui::Point, - target_display_point: DisplayPoint, - editor_width: Pixels, - window: &mut Window, - cx: &mut App, - ) -> Option<(AnyElement, gpui::Point)> { - if target_display_point.row().as_f32() < scroll_top { - let mut element = self - .render_edit_prediction_line_popover( - "Jump to Edit", - Some(IconName::ArrowUp), - window, - cx, - )? - .into_any(); - - let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); - let offset = point( - (text_bounds.size.width - size.width) / 2., - Self::EDIT_PREDICTION_POPOVER_PADDING_Y, - ); - - let origin = text_bounds.origin + offset; - element.prepaint_at(origin, window, cx); - Some((element, origin)) - } else if (target_display_point.row().as_f32() + 1.) > scroll_bottom { - let mut element = self - .render_edit_prediction_line_popover( - "Jump to Edit", - Some(IconName::ArrowDown), - window, - cx, - )? - .into_any(); - - let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); - let offset = point( - (text_bounds.size.width - size.width) / 2., - text_bounds.size.height - size.height - Self::EDIT_PREDICTION_POPOVER_PADDING_Y, - ); - - let origin = text_bounds.origin + offset; - element.prepaint_at(origin, window, cx); - Some((element, origin)) - } else { - self.render_edit_prediction_end_of_line_popover( - "Jump to Edit", - editor_snapshot, - visible_row_range, - target_display_point, - line_height, - scroll_pixel_position, - content_origin, - editor_width, - window, - cx, - ) - } - } - - fn render_edit_prediction_end_of_line_popover( - self: &mut Editor, - label: &'static str, - editor_snapshot: &EditorSnapshot, - visible_row_range: Range, - target_display_point: DisplayPoint, - line_height: Pixels, - scroll_pixel_position: gpui::Point, - content_origin: gpui::Point, - editor_width: Pixels, - window: &mut Window, - cx: &mut App, - ) -> Option<(AnyElement, gpui::Point)> { - let target_line_end = DisplayPoint::new( - target_display_point.row(), - editor_snapshot.line_len(target_display_point.row()), - ); - - let mut element = self - .render_edit_prediction_line_popover(label, None, window, cx)? - .into_any(); - - let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); - - let line_origin = self.display_to_pixel_point(target_line_end, editor_snapshot, window)?; - - let start_point = content_origin - point(scroll_pixel_position.x, Pixels::ZERO); - let mut origin = start_point - + line_origin - + point(Self::EDIT_PREDICTION_POPOVER_PADDING_X, Pixels::ZERO); - origin.x = origin.x.max(content_origin.x); - - let max_x = content_origin.x + editor_width - size.width; - - if origin.x > max_x { - let offset = line_height + Self::EDIT_PREDICTION_POPOVER_PADDING_Y; - - let icon = if visible_row_range.contains(&(target_display_point.row() + 2)) { - origin.y += offset; - IconName::ArrowUp - } else { - origin.y -= offset; - IconName::ArrowDown - }; - - element = self - .render_edit_prediction_line_popover(label, Some(icon), window, cx)? - .into_any(); - - let size = element.layout_as_root(AvailableSpace::min_size(), window, cx); - - origin.x = content_origin.x + editor_width - size.width - px(2.); - } - - element.prepaint_at(origin, window, cx); - Some((element, origin)) - } - - fn render_edit_prediction_diff_popover( - self: &Editor, - text_bounds: &Bounds, - content_origin: gpui::Point, - editor_snapshot: &EditorSnapshot, - visible_row_range: Range, - line_layouts: &[LineWithInvisibles], - line_height: Pixels, - scroll_pixel_position: gpui::Point, - newest_selection_head: Option, - editor_width: Pixels, - style: &EditorStyle, - edits: &Vec<(Range, String)>, - edit_preview: &Option, - snapshot: &language::BufferSnapshot, - window: &mut Window, - cx: &mut App, - ) -> Option<(AnyElement, gpui::Point)> { - let edit_start = edits - .first() - .unwrap() - .0 - .start - .to_display_point(editor_snapshot); - let edit_end = edits - .last() - .unwrap() - .0 - .end - .to_display_point(editor_snapshot); - - let is_visible = visible_row_range.contains(&edit_start.row()) - || visible_row_range.contains(&edit_end.row()); - if !is_visible { - return None; - } - - let highlighted_edits = - crate::inline_completion_edit_text(&snapshot, edits, edit_preview.as_ref()?, false, cx); - - let styled_text = highlighted_edits.to_styled_text(&style.text); - let line_count = highlighted_edits.text.lines().count(); - - const BORDER_WIDTH: Pixels = px(1.); - - let keybind = self.render_edit_prediction_accept_keybind(window, cx); - let has_keybind = keybind.is_some(); - - let mut element = h_flex() - .items_start() - .child( - h_flex() - .bg(cx.theme().colors().editor_background) - .border(BORDER_WIDTH) - .shadow_sm() - .border_color(cx.theme().colors().border) - .rounded_l_lg() - .when(line_count > 1, |el| el.rounded_br_lg()) - .pr_1() - .child(styled_text), - ) - .child( - h_flex() - .h(line_height + BORDER_WIDTH * 2.) - .px_1p5() - .gap_1() - // Workaround: For some reason, there's a gap if we don't do this - .ml(-BORDER_WIDTH) - .shadow(vec![gpui::BoxShadow { - color: gpui::black().opacity(0.05), - offset: point(px(1.), px(1.)), - blur_radius: px(2.), - spread_radius: px(0.), - }]) - .bg(Editor::edit_prediction_line_popover_bg_color(cx)) - .border(BORDER_WIDTH) - .border_color(cx.theme().colors().border) - .rounded_r_lg() - .id("edit_prediction_diff_popover_keybind") - .when(!has_keybind, |el| { - let status_colors = cx.theme().status(); - - el.bg(status_colors.error_background) - .border_color(status_colors.error.opacity(0.6)) - .child(Icon::new(IconName::Info).color(Color::Error)) - .cursor_default() - .hoverable_tooltip(move |_window, cx| { - cx.new(|_| MissingEditPredictionKeybindingTooltip).into() - }) - }) - .children(keybind), - ) - .into_any(); - - let longest_row = - editor_snapshot.longest_row_in_range(edit_start.row()..edit_end.row() + 1); - let longest_line_width = if visible_row_range.contains(&longest_row) { - line_layouts[(longest_row.0 - visible_row_range.start.0) as usize].width - } else { - layout_line( - longest_row, - editor_snapshot, - style, - editor_width, - |_| false, - window, - cx, - ) - .width - }; - - let viewport_bounds = - Bounds::new(Default::default(), window.viewport_size()).extend(Edges { - right: -EditorElement::SCROLLBAR_WIDTH, - ..Default::default() - }); - - let x_after_longest = - text_bounds.origin.x + longest_line_width + Self::EDIT_PREDICTION_POPOVER_PADDING_X - - scroll_pixel_position.x; - - let element_bounds = element.layout_as_root(AvailableSpace::min_size(), window, cx); - - // Fully visible if it can be displayed within the window (allow overlapping other - // panes). However, this is only allowed if the popover starts within text_bounds. - let can_position_to_the_right = x_after_longest < text_bounds.right() - && x_after_longest + element_bounds.width < viewport_bounds.right(); - - let mut origin = if can_position_to_the_right { - point( - x_after_longest, - text_bounds.origin.y + edit_start.row().as_f32() * line_height - - scroll_pixel_position.y, - ) - } else { - let cursor_row = newest_selection_head.map(|head| head.row()); - let above_edit = edit_start - .row() - .0 - .checked_sub(line_count as u32) - .map(DisplayRow); - let below_edit = Some(edit_end.row() + 1); - let above_cursor = - cursor_row.and_then(|row| row.0.checked_sub(line_count as u32).map(DisplayRow)); - let below_cursor = cursor_row.map(|cursor_row| cursor_row + 1); - - // Place the edit popover adjacent to the edit if there is a location - // available that is onscreen and does not obscure the cursor. Otherwise, - // place it adjacent to the cursor. - let row_target = [above_edit, below_edit, above_cursor, below_cursor] - .into_iter() - .flatten() - .find(|&start_row| { - let end_row = start_row + line_count as u32; - visible_row_range.contains(&start_row) - && visible_row_range.contains(&end_row) - && cursor_row.map_or(true, |cursor_row| { - !((start_row..end_row).contains(&cursor_row)) - }) - })?; - - content_origin - + point( - -scroll_pixel_position.x, - row_target.as_f32() * line_height - scroll_pixel_position.y, - ) - }; - - origin.x -= BORDER_WIDTH; - - window.defer_draw(element, origin, 1); - - // Do not return an element, since it will already be drawn due to defer_draw. - None - } - - fn edit_prediction_cursor_popover_height(&self) -> Pixels { - px(30.) - } - - fn current_user_player_color(&self, cx: &mut App) -> PlayerColor { - if self.read_only(cx) { - cx.theme().players().read_only() - } else { - self.style.as_ref().unwrap().local_player - } - } - - fn render_edit_prediction_accept_keybind( - &self, - window: &mut Window, - cx: &App, - ) -> Option { - let accept_binding = self.accept_edit_prediction_keybind(window, cx); - let accept_keystroke = accept_binding.keystroke()?; - - let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac; - - let modifiers_color = if accept_keystroke.modifiers == window.modifiers() { - Color::Accent - } else { - Color::Muted - }; - - h_flex() - .px_0p5() - .when(is_platform_style_mac, |parent| parent.gap_0p5()) - .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) - .text_size(TextSize::XSmall.rems(cx)) - .child(h_flex().children(ui::render_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.key.clone()) - }) - .when(!is_platform_style_mac, |parent| { - parent.child( - Key::new( - util::capitalize(&accept_keystroke.key), - Some(Color::Default), - ) - .size(Some(IconSize::XSmall.rems().into())), - ) - }) - .into_any() - .into() - } - - fn render_edit_prediction_line_popover( - &self, - label: impl Into, - icon: Option, - window: &mut Window, - cx: &App, - ) -> Option> { - let padding_right = if icon.is_some() { px(4.) } else { px(8.) }; - - let keybind = self.render_edit_prediction_accept_keybind(window, cx); - let has_keybind = keybind.is_some(); - - let result = h_flex() - .id("ep-line-popover") - .py_0p5() - .pl_1() - .pr(padding_right) - .gap_1() - .rounded_md() - .border_1() - .bg(Self::edit_prediction_line_popover_bg_color(cx)) - .border_color(Self::edit_prediction_callout_popover_border_color(cx)) - .shadow_sm() - .when(!has_keybind, |el| { - let status_colors = cx.theme().status(); - - el.bg(status_colors.error_background) - .border_color(status_colors.error.opacity(0.6)) - .pl_2() - .child(Icon::new(IconName::ZedPredictError).color(Color::Error)) - .cursor_default() - .hoverable_tooltip(move |_window, cx| { - cx.new(|_| MissingEditPredictionKeybindingTooltip).into() - }) - }) - .children(keybind) - .child( - Label::new(label) - .size(LabelSize::Small) - .when(!has_keybind, |el| { - el.color(cx.theme().status().error.into()).strikethrough() - }), - ) - .when(!has_keybind, |el| { - el.child( - h_flex().ml_1().child( - Icon::new(IconName::Info) - .size(IconSize::Small) - .color(cx.theme().status().error.into()), - ), - ) - }) - .when_some(icon, |element, icon| { - element.child( - div() - .mt(px(1.5)) - .child(Icon::new(icon).size(IconSize::Small)), - ) - }); - - Some(result) - } - - fn edit_prediction_line_popover_bg_color(cx: &App) -> Hsla { - let accent_color = cx.theme().colors().text_accent; - let editor_bg_color = cx.theme().colors().editor_background; - editor_bg_color.blend(accent_color.opacity(0.1)) - } - - fn edit_prediction_callout_popover_border_color(cx: &App) -> Hsla { - let accent_color = cx.theme().colors().text_accent; - let editor_bg_color = cx.theme().colors().editor_background; - editor_bg_color.blend(accent_color.opacity(0.6)) - } - - fn render_edit_prediction_cursor_popover( - &self, - min_width: Pixels, - max_width: Pixels, - cursor_point: Point, - style: &EditorStyle, - accept_keystroke: Option<&gpui::Keystroke>, - _window: &Window, - cx: &mut Context, - ) -> Option { - let provider = self.edit_prediction_provider.as_ref()?; - - 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("Edit Prediction Provider ToS Clicked", None, cx); - window.dispatch_action( - zed_actions::OpenZedPredictOnboarding.boxed_clone(), - cx, - ); - })) - .child( - h_flex() - .flex_1() - .gap_2() - .child(Icon::new(IconName::ZedPredict)) - .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() -> Div { - h_flex() - .h_full() - .flex_1() - .gap_2() - .child(Icon::new(IconName::ZedPredict)) - } - - let completion = match &self.active_inline_completion { - Some(prediction) => { - if !self.has_visible_completions_menu() { - const RADIUS: Pixels = px(6.); - const BORDER_WIDTH: Pixels = px(1.); - - return Some( - h_flex() - .elevation_2(cx) - .border(BORDER_WIDTH) - .border_color(cx.theme().colors().border) - .when(accept_keystroke.is_none(), |el| { - el.border_color(cx.theme().status().error) - }) - .rounded(RADIUS) - .rounded_tl(px(0.)) - .overflow_hidden() - .child(div().px_1p5().child(match &prediction.completion { - InlineCompletion::Move { target, snapshot } => { - use text::ToPoint as _; - if target.text_anchor.to_point(&snapshot).row > cursor_point.row - { - Icon::new(IconName::ZedPredictDown) - } else { - Icon::new(IconName::ZedPredictUp) - } - } - InlineCompletion::Edit { .. } => Icon::new(IconName::ZedPredict), - })) - .child( - h_flex() - .gap_1() - .py_1() - .px_2() - .rounded_r(RADIUS - BORDER_WIDTH) - .border_l_1() - .border_color(cx.theme().colors().border) - .bg(Self::edit_prediction_line_popover_bg_color(cx)) - .when(self.edit_prediction_preview.released_too_fast(), |el| { - el.child( - Label::new("Hold") - .size(LabelSize::Small) - .when(accept_keystroke.is_none(), |el| { - el.strikethrough() - }) - .line_height_style(LineHeightStyle::UiLabel), - ) - }) - .id("edit_prediction_cursor_popover_keybind") - .when(accept_keystroke.is_none(), |el| { - let status_colors = cx.theme().status(); - - el.bg(status_colors.error_background) - .border_color(status_colors.error.opacity(0.6)) - .child(Icon::new(IconName::Info).color(Color::Error)) - .cursor_default() - .hoverable_tooltip(move |_window, cx| { - cx.new(|_| MissingEditPredictionKeybindingTooltip) - .into() - }) - }) - .when_some( - accept_keystroke.as_ref(), - |el, accept_keystroke| { - el.child(h_flex().children(ui::render_modifiers( - &accept_keystroke.modifiers, - PlatformStyle::platform(), - Some(Color::Default), - Some(IconSize::XSmall.rems().into()), - false, - ))) - }, - ), - ) - .into_any(), - ); - } - - self.render_edit_prediction_cursor_popover_preview( - prediction, - cursor_point, - style, - cx, - )? - } - - None if is_refreshing => match &self.stale_inline_completion_in_menu { - Some(stale_completion) => self.render_edit_prediction_cursor_popover_preview( - stale_completion, - cursor_point, - style, - cx, - )?, - - None => { - pending_completion_container().child(Label::new("...").size(LabelSize::Small)) - } - }, - - None => pending_completion_container().child(Label::new("No Prediction")), - }; - - let completion = if is_refreshing { - completion - .with_animation( - "loading-completion", - Animation::new(Duration::from_secs(2)) - .repeat() - .with_easing(pulsating_between(0.4, 0.8)), - |label, delta| label.opacity(delta), - ) - .into_any_element() - } else { - completion.into_any_element() - }; - - let has_completion = self.active_inline_completion.is_some(); - - let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac; - Some( - h_flex() - .min_w(min_width) - .max_w(max_width) - .flex_1() - .elevation_2(cx) - .border_color(cx.theme().colors().border) - .child( - div() - .flex_1() - .py_1() - .px_2() - .overflow_hidden() - .child(completion), - ) - .when_some(accept_keystroke, |el, accept_keystroke| { - if !accept_keystroke.modifiers.modified() { - return el; - } - - el.child( - h_flex() - .h_full() - .border_l_1() - .rounded_r_lg() - .border_color(cx.theme().colors().border) - .bg(Self::edit_prediction_line_popover_bg_color(cx)) - .gap_1() - .py_1() - .px_2() - .child( - h_flex() - .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.modifiers, - PlatformStyle::platform(), - Some(if !has_completion { - Color::Muted - } else { - Color::Default - }), - None, - false, - ))), - ) - .child(Label::new("Preview").into_any_element()) - .opacity(if has_completion { 1.0 } else { 0.4 }), - ) - }) - .into_any(), - ) - } - - fn render_edit_prediction_cursor_popover_preview( - &self, - completion: &InlineCompletionState, - cursor_point: Point, - style: &EditorStyle, - cx: &mut Context, - ) -> Option
{ - use text::ToPoint as _; - - fn render_relative_row_jump( - prefix: impl Into, - current_row: u32, - target_row: u32, - ) -> Div { - let (row_diff, arrow) = if target_row < current_row { - (current_row - target_row, IconName::ArrowUp) - } else { - (target_row - current_row, IconName::ArrowDown) - }; - - h_flex() - .child( - Label::new(format!("{}{}", prefix.into(), row_diff)) - .color(Color::Muted) - .size(LabelSize::Small), - ) - .child(Icon::new(arrow).color(Color::Muted).size(IconSize::Small)) - } - - match &completion.completion { - InlineCompletion::Move { - target, snapshot, .. - } => Some( - h_flex() - .px_2() - .gap_2() - .flex_1() - .child( - if target.text_anchor.to_point(&snapshot).row > cursor_point.row { - Icon::new(IconName::ZedPredictDown) - } else { - Icon::new(IconName::ZedPredictUp) - }, - ) - .child(Label::new("Jump to Edit")), - ), - - InlineCompletion::Edit { - edits, - edit_preview, - snapshot, - display_mode: _, - } => { - let first_edit_row = edits.first()?.0.start.text_anchor.to_point(&snapshot).row; - - let (highlighted_edits, has_more_lines) = crate::inline_completion_edit_text( - &snapshot, - &edits, - edit_preview.as_ref()?, - true, - cx, - ) - .first_line_preview(); - - let styled_text = gpui::StyledText::new(highlighted_edits.text) - .with_default_highlights(&style.text, highlighted_edits.highlights); - - let preview = h_flex() - .gap_1() - .min_w_16() - .child(styled_text) - .when(has_more_lines, |parent| parent.child("…")); - - let left = if first_edit_row != cursor_point.row { - render_relative_row_jump("", cursor_point.row, first_edit_row) - .into_any_element() - } else { - Icon::new(IconName::ZedPredict).into_any_element() - }; - - Some( - h_flex() - .h_full() - .flex_1() - .gap_2() - .pr_1() - .overflow_x_hidden() - .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) - .child(left) - .child(preview), - ) - } - } - } - - fn render_context_menu( - &self, - style: &EditorStyle, - max_height_in_lines: u32, - window: &mut Window, - cx: &mut Context, - ) -> Option { - let menu = self.context_menu.borrow(); - let menu = menu.as_ref()?; - if !menu.visible() { - return None; - }; - Some(menu.render(style, max_height_in_lines, window, cx)) - } - - fn render_context_menu_aside( - &mut self, - max_size: Size, - window: &mut Window, - cx: &mut Context, - ) -> Option { - self.context_menu.borrow_mut().as_mut().and_then(|menu| { - if menu.visible() { - menu.render_aside(self, max_size, window, cx) - } else { - None - } - }) - } - - fn hide_context_menu( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> Option { - cx.notify(); - self.completion_tasks.clear(); - let context_menu = self.context_menu.borrow_mut().take(); - self.stale_inline_completion_in_menu.take(); - self.update_visible_inline_completion(window, cx); - context_menu - } - - fn show_snippet_choices( - &mut self, - choices: &Vec, - selection: Range, - cx: &mut Context, - ) { - if selection.start.buffer_id.is_none() { - return; - } - let buffer_id = selection.start.buffer_id.unwrap(); - let buffer = self.buffer().read(cx).buffer(buffer_id); - let id = post_inc(&mut self.next_completion_id); - let snippet_sort_order = EditorSettings::get_global(cx).snippet_sort_order; - - if let Some(buffer) = buffer { - *self.context_menu.borrow_mut() = Some(CodeContextMenu::Completions( - CompletionsMenu::new_snippet_choices( - id, - true, - choices, - selection, - buffer, - snippet_sort_order, - ), - )); - } - } - - pub fn insert_snippet( - &mut self, - insertion_ranges: &[Range], - snippet: Snippet, - window: &mut Window, - cx: &mut Context, - ) -> Result<()> { - struct Tabstop { - is_end_tabstop: bool, - ranges: Vec>, - choices: Option>, - } - - let tabstops = self.buffer.update(cx, |buffer, cx| { - let snippet_text: Arc = snippet.text.clone().into(); - let edits = insertion_ranges - .iter() - .cloned() - .map(|range| (range, snippet_text.clone())); - buffer.edit(edits, Some(AutoindentMode::EachLine), cx); - - let snapshot = &*buffer.read(cx); - let snippet = &snippet; - snippet - .tabstops - .iter() - .map(|tabstop| { - let is_end_tabstop = tabstop.ranges.first().map_or(false, |tabstop| { - tabstop.is_empty() && tabstop.start == snippet.text.len() as isize - }); - let mut tabstop_ranges = tabstop - .ranges - .iter() - .flat_map(|tabstop_range| { - let mut delta = 0_isize; - insertion_ranges.iter().map(move |insertion_range| { - let insertion_start = insertion_range.start as isize + delta; - delta += - snippet.text.len() as isize - insertion_range.len() as isize; - - let start = ((insertion_start + tabstop_range.start) as usize) - .min(snapshot.len()); - let end = ((insertion_start + tabstop_range.end) as usize) - .min(snapshot.len()); - snapshot.anchor_before(start)..snapshot.anchor_after(end) - }) - }) - .collect::>(); - tabstop_ranges.sort_unstable_by(|a, b| a.start.cmp(&b.start, snapshot)); - - Tabstop { - is_end_tabstop, - ranges: tabstop_ranges, - choices: tabstop.choices.clone(), - } - }) - .collect::>() - }); - if let Some(tabstop) = tabstops.first() { - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select_ranges(tabstop.ranges.iter().cloned()); - }); - - if let Some(choices) = &tabstop.choices { - if let Some(selection) = tabstop.ranges.first() { - self.show_snippet_choices(choices, selection.clone(), cx) - } - } - - // If we're already at the last tabstop and it's at the end of the snippet, - // we're done, we don't need to keep the state around. - if !tabstop.is_end_tabstop { - let choices = tabstops - .iter() - .map(|tabstop| tabstop.choices.clone()) - .collect(); - - let ranges = tabstops - .into_iter() - .map(|tabstop| tabstop.ranges) - .collect::>(); - - self.snippet_stack.push(SnippetState { - active_index: 0, - ranges, - choices, - }); - } - - // Check whether the just-entered snippet ends with an auto-closable bracket. - if self.autoclose_regions.is_empty() { - let snapshot = self.buffer.read(cx).snapshot(cx); - for selection in &mut self.selections.all::(cx) { - let selection_head = selection.head(); - let Some(scope) = snapshot.language_scope_at(selection_head) else { - continue; - }; - - let mut bracket_pair = None; - let next_chars = snapshot.chars_at(selection_head).collect::(); - let prev_chars = snapshot - .reversed_chars_at(selection_head) - .collect::(); - for (pair, enabled) in scope.brackets() { - if enabled - && pair.close - && prev_chars.starts_with(pair.start.as_str()) - && next_chars.starts_with(pair.end.as_str()) - { - bracket_pair = Some(pair.clone()); - break; - } - } - if let Some(pair) = bracket_pair { - let snapshot_settings = snapshot.language_settings_at(selection_head, cx); - let autoclose_enabled = - self.use_autoclose && snapshot_settings.use_autoclose; - if autoclose_enabled { - let start = snapshot.anchor_after(selection_head); - let end = snapshot.anchor_after(selection_head); - self.autoclose_regions.push(AutocloseRegion { - selection_id: selection.id, - range: start..end, - pair, - }); - } - } - } - } - } - Ok(()) - } - - pub fn move_to_next_snippet_tabstop( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> bool { - self.move_to_snippet_tabstop(Bias::Right, window, cx) - } - - pub fn move_to_prev_snippet_tabstop( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> bool { - self.move_to_snippet_tabstop(Bias::Left, window, cx) - } - - pub fn move_to_snippet_tabstop( - &mut self, - bias: Bias, - window: &mut Window, - cx: &mut Context, - ) -> bool { - if let Some(mut snippet) = self.snippet_stack.pop() { - match bias { - Bias::Left => { - if snippet.active_index > 0 { - snippet.active_index -= 1; - } else { - self.snippet_stack.push(snippet); - return false; - } - } - Bias::Right => { - if snippet.active_index + 1 < snippet.ranges.len() { - snippet.active_index += 1; - } else { - self.snippet_stack.push(snippet); - return false; - } - } - } - if let Some(current_ranges) = snippet.ranges.get(snippet.active_index) { - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select_anchor_ranges(current_ranges.iter().cloned()) - }); - - if let Some(choices) = &snippet.choices[snippet.active_index] { - if let Some(selection) = current_ranges.first() { - self.show_snippet_choices(&choices, selection.clone(), cx); - } - } - - // If snippet state is not at the last tabstop, push it back on the stack - if snippet.active_index + 1 < snippet.ranges.len() { - self.snippet_stack.push(snippet); - } - return true; - } - } - - false - } - - pub fn clear(&mut self, window: &mut Window, cx: &mut Context) { - self.transact(window, cx, |this, window, cx| { - this.select_all(&SelectAll, window, cx); - this.insert("", window, cx); - }); - } - - pub fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.transact(window, cx, |this, window, cx| { - this.select_autoclose_pair(window, cx); - let mut linked_ranges = HashMap::<_, Vec<_>>::default(); - if !this.linked_edit_ranges.is_empty() { - let selections = this.selections.all::(cx); - let snapshot = this.buffer.read(cx).snapshot(cx); - - for selection in selections.iter() { - let selection_start = snapshot.anchor_before(selection.start).text_anchor; - let selection_end = snapshot.anchor_after(selection.end).text_anchor; - if selection_start.buffer_id != selection_end.buffer_id { - continue; - } - if let Some(ranges) = - this.linked_editing_ranges_for(selection_start..selection_end, cx) - { - for (buffer, entries) in ranges { - linked_ranges.entry(buffer).or_default().extend(entries); - } - } - } - } - - let mut selections = this.selections.all::(cx); - let display_map = this.display_map.update(cx, |map, cx| map.snapshot(cx)); - for selection in &mut selections { - if selection.is_empty() { - let old_head = selection.head(); - let mut new_head = - movement::left(&display_map, old_head.to_display_point(&display_map)) - .to_point(&display_map); - if let Some((buffer, line_buffer_range)) = display_map - .buffer_snapshot - .buffer_line_for_row(MultiBufferRow(old_head.row)) - { - let indent_size = buffer.indent_size_for_line(line_buffer_range.start.row); - let indent_len = match indent_size.kind { - IndentKind::Space => { - buffer.settings_at(line_buffer_range.start, cx).tab_size - } - IndentKind::Tab => NonZeroU32::new(1).unwrap(), - }; - if old_head.column <= indent_size.len && old_head.column > 0 { - let indent_len = indent_len.get(); - new_head = cmp::min( - new_head, - MultiBufferPoint::new( - old_head.row, - ((old_head.column - 1) / indent_len) * indent_len, - ), - ); - } - } - - selection.set_head(new_head, SelectionGoal::None); - } - } - - this.signature_help_state.set_backspace_pressed(true); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(selections) - }); - this.insert("", window, cx); - let empty_str: Arc = Arc::from(""); - for (buffer, edits) in linked_ranges { - let snapshot = buffer.read(cx).snapshot(); - use text::ToPoint as TP; - - let edits = edits - .into_iter() - .map(|range| { - let end_point = TP::to_point(&range.end, &snapshot); - let mut start_point = TP::to_point(&range.start, &snapshot); - - if end_point == start_point { - let offset = text::ToOffset::to_offset(&range.start, &snapshot) - .saturating_sub(1); - start_point = - snapshot.clip_point(TP::to_point(&offset, &snapshot), Bias::Left); - }; - - (start_point..end_point, empty_str.clone()) - }) - .sorted_by_key(|(range, _)| range.start) - .collect::>(); - buffer.update(cx, |this, cx| { - this.edit(edits, None, cx); - }) - } - this.refresh_inline_completion(true, false, window, cx); - linked_editing_ranges::refresh_linked_ranges(this, window, cx); - }); - } - - pub fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.transact(window, cx, |this, window, cx| { - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - if selection.is_empty() { - let cursor = movement::right(map, selection.head()); - selection.end = cursor; - selection.reversed = true; - selection.goal = SelectionGoal::None; - } - }) - }); - this.insert("", window, cx); - this.refresh_inline_completion(true, false, window, cx); - }); - } - - pub fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - if self.move_to_prev_snippet_tabstop(window, cx) { - return; - } - self.outdent(&Outdent, window, cx); - } - - pub fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context) { - if self.move_to_next_snippet_tabstop(window, cx) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - return; - } - if self.read_only(cx) { - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - let mut selections = self.selections.all_adjusted(cx); - let buffer = self.buffer.read(cx); - let snapshot = buffer.snapshot(cx); - let rows_iter = selections.iter().map(|s| s.head().row); - let suggested_indents = snapshot.suggested_indents(rows_iter, cx); - - let has_some_cursor_in_whitespace = selections - .iter() - .filter(|selection| selection.is_empty()) - .any(|selection| { - let cursor = selection.head(); - let current_indent = snapshot.indent_size_for_line(MultiBufferRow(cursor.row)); - cursor.column < current_indent.len - }); - - let mut edits = Vec::new(); - let mut prev_edited_row = 0; - let mut row_delta = 0; - for selection in &mut selections { - if selection.start.row != prev_edited_row { - row_delta = 0; - } - prev_edited_row = selection.end.row; - - // If the selection is non-empty, then increase the indentation of the selected lines. - if !selection.is_empty() { - row_delta = - Self::indent_selection(buffer, &snapshot, selection, &mut edits, row_delta, cx); - continue; - } - - // If the selection is empty and the cursor is in the leading whitespace before the - // suggested indentation, then auto-indent the line. - let cursor = selection.head(); - let current_indent = snapshot.indent_size_for_line(MultiBufferRow(cursor.row)); - if let Some(suggested_indent) = - suggested_indents.get(&MultiBufferRow(cursor.row)).copied() - { - // If there exist any empty selection in the leading whitespace, then skip - // indent for selections at the boundary. - if has_some_cursor_in_whitespace - && cursor.column == current_indent.len - && current_indent.len == suggested_indent.len - { - continue; - } - - if cursor.column < suggested_indent.len - && cursor.column <= current_indent.len - && current_indent.len <= suggested_indent.len - { - selection.start = Point::new(cursor.row, suggested_indent.len); - selection.end = selection.start; - if row_delta == 0 { - edits.extend(Buffer::edit_for_indent_size_adjustment( - cursor.row, - current_indent, - suggested_indent, - )); - row_delta = suggested_indent.len - current_indent.len; - } - continue; - } - } - - // Otherwise, insert a hard or soft tab. - let settings = buffer.language_settings_at(cursor, cx); - let tab_size = if settings.hard_tabs { - IndentSize::tab() - } else { - let tab_size = settings.tab_size.get(); - let indent_remainder = snapshot - .text_for_range(Point::new(cursor.row, 0)..cursor) - .flat_map(str::chars) - .fold(row_delta % tab_size, |counter: u32, c| { - if c == '\t' { - 0 - } else { - (counter + 1) % tab_size - } - }); - - let chars_to_next_tab_stop = tab_size - indent_remainder; - IndentSize::spaces(chars_to_next_tab_stop) - }; - selection.start = Point::new(cursor.row, cursor.column + row_delta + tab_size.len); - selection.end = selection.start; - edits.push((cursor..cursor, tab_size.chars().collect::())); - row_delta += tab_size.len; - } - - self.transact(window, cx, |this, window, cx| { - this.buffer.update(cx, |b, cx| b.edit(edits, None, cx)); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(selections) - }); - this.refresh_inline_completion(true, false, window, cx); - }); - } - - pub fn indent(&mut self, _: &Indent, window: &mut Window, cx: &mut Context) { - if self.read_only(cx) { - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - let mut selections = self.selections.all::(cx); - let mut prev_edited_row = 0; - let mut row_delta = 0; - let mut edits = Vec::new(); - let buffer = self.buffer.read(cx); - let snapshot = buffer.snapshot(cx); - for selection in &mut selections { - if selection.start.row != prev_edited_row { - row_delta = 0; - } - prev_edited_row = selection.end.row; - - row_delta = - Self::indent_selection(buffer, &snapshot, selection, &mut edits, row_delta, cx); - } - - self.transact(window, cx, |this, window, cx| { - this.buffer.update(cx, |b, cx| b.edit(edits, None, cx)); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(selections) - }); - }); - } - - fn indent_selection( - buffer: &MultiBuffer, - snapshot: &MultiBufferSnapshot, - selection: &mut Selection, - edits: &mut Vec<(Range, String)>, - delta_for_start_row: u32, - cx: &App, - ) -> u32 { - let settings = buffer.language_settings_at(selection.start, cx); - let tab_size = settings.tab_size.get(); - let indent_kind = if settings.hard_tabs { - IndentKind::Tab - } else { - IndentKind::Space - }; - let mut start_row = selection.start.row; - let mut end_row = selection.end.row + 1; - - // If a selection ends at the beginning of a line, don't indent - // that last line. - if selection.end.column == 0 && selection.end.row > selection.start.row { - end_row -= 1; - } - - // Avoid re-indenting a row that has already been indented by a - // previous selection, but still update this selection's column - // to reflect that indentation. - if delta_for_start_row > 0 { - start_row += 1; - selection.start.column += delta_for_start_row; - if selection.end.row == selection.start.row { - selection.end.column += delta_for_start_row; - } - } - - let mut delta_for_end_row = 0; - let has_multiple_rows = start_row + 1 != end_row; - for row in start_row..end_row { - let current_indent = snapshot.indent_size_for_line(MultiBufferRow(row)); - let indent_delta = match (current_indent.kind, indent_kind) { - (IndentKind::Space, IndentKind::Space) => { - let columns_to_next_tab_stop = tab_size - (current_indent.len % tab_size); - IndentSize::spaces(columns_to_next_tab_stop) - } - (IndentKind::Tab, IndentKind::Space) => IndentSize::spaces(tab_size), - (_, IndentKind::Tab) => IndentSize::tab(), - }; - - let start = if has_multiple_rows || current_indent.len < selection.start.column { - 0 - } else { - selection.start.column - }; - let row_start = Point::new(row, start); - edits.push(( - row_start..row_start, - indent_delta.chars().collect::(), - )); - - // Update this selection's endpoints to reflect the indentation. - if row == selection.start.row { - selection.start.column += indent_delta.len; - } - if row == selection.end.row { - selection.end.column += indent_delta.len; - delta_for_end_row = indent_delta.len; - } - } - - if selection.start.row == selection.end.row { - delta_for_start_row + delta_for_end_row - } else { - delta_for_end_row - } - } - - pub fn outdent(&mut self, _: &Outdent, window: &mut Window, cx: &mut Context) { - if self.read_only(cx) { - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let selections = self.selections.all::(cx); - let mut deletion_ranges = Vec::new(); - let mut last_outdent = None; - { - let buffer = self.buffer.read(cx); - let snapshot = buffer.snapshot(cx); - for selection in &selections { - let settings = buffer.language_settings_at(selection.start, cx); - let tab_size = settings.tab_size.get(); - let mut rows = selection.spanned_rows(false, &display_map); - - // Avoid re-outdenting a row that has already been outdented by a - // previous selection. - if let Some(last_row) = last_outdent { - if last_row == rows.start { - rows.start = rows.start.next_row(); - } - } - let has_multiple_rows = rows.len() > 1; - for row in rows.iter_rows() { - let indent_size = snapshot.indent_size_for_line(row); - if indent_size.len > 0 { - let deletion_len = match indent_size.kind { - IndentKind::Space => { - let columns_to_prev_tab_stop = indent_size.len % tab_size; - if columns_to_prev_tab_stop == 0 { - tab_size - } else { - columns_to_prev_tab_stop - } - } - IndentKind::Tab => 1, - }; - let start = if has_multiple_rows - || deletion_len > selection.start.column - || indent_size.len < selection.start.column - { - 0 - } else { - selection.start.column - deletion_len - }; - deletion_ranges.push( - Point::new(row.0, start)..Point::new(row.0, start + deletion_len), - ); - last_outdent = Some(row); - } - } - } - } - - self.transact(window, cx, |this, window, cx| { - this.buffer.update(cx, |buffer, cx| { - let empty_str: Arc = Arc::default(); - buffer.edit( - deletion_ranges - .into_iter() - .map(|range| (range, empty_str.clone())), - None, - cx, - ); - }); - let selections = this.selections.all::(cx); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(selections) - }); - }); - } - - pub fn autoindent(&mut self, _: &AutoIndent, window: &mut Window, cx: &mut Context) { - if self.read_only(cx) { - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - let selections = self - .selections - .all::(cx) - .into_iter() - .map(|s| s.range()); - - self.transact(window, cx, |this, window, cx| { - this.buffer.update(cx, |buffer, cx| { - buffer.autoindent_ranges(selections, cx); - }); - let selections = this.selections.all::(cx); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(selections) - }); - }); - } - - pub fn delete_line(&mut self, _: &DeleteLine, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let selections = self.selections.all::(cx); - - let mut new_cursors = Vec::new(); - let mut edit_ranges = Vec::new(); - let mut selections = selections.iter().peekable(); - while let Some(selection) = selections.next() { - let mut rows = selection.spanned_rows(false, &display_map); - let goal_display_column = selection.head().to_display_point(&display_map).column(); - - // Accumulate contiguous regions of rows that we want to delete. - while let Some(next_selection) = selections.peek() { - let next_rows = next_selection.spanned_rows(false, &display_map); - if next_rows.start <= rows.end { - rows.end = next_rows.end; - selections.next().unwrap(); - } else { - break; - } - } - - let buffer = &display_map.buffer_snapshot; - let mut edit_start = Point::new(rows.start.0, 0).to_offset(buffer); - let edit_end; - let cursor_buffer_row; - if buffer.max_point().row >= rows.end.0 { - // If there's a line after the range, delete the \n from the end of the row range - // and position the cursor on the next line. - edit_end = Point::new(rows.end.0, 0).to_offset(buffer); - cursor_buffer_row = rows.end; - } else { - // If there isn't a line after the range, delete the \n from the line before the - // start of the row range and position the cursor there. - edit_start = edit_start.saturating_sub(1); - edit_end = buffer.len(); - cursor_buffer_row = rows.start.previous_row(); - } - - let mut cursor = Point::new(cursor_buffer_row.0, 0).to_display_point(&display_map); - *cursor.column_mut() = - cmp::min(goal_display_column, display_map.line_len(cursor.row())); - - new_cursors.push(( - selection.id, - buffer.anchor_after(cursor.to_point(&display_map)), - )); - edit_ranges.push(edit_start..edit_end); - } - - self.transact(window, cx, |this, window, cx| { - let buffer = this.buffer.update(cx, |buffer, cx| { - let empty_str: Arc = Arc::default(); - buffer.edit( - edit_ranges - .into_iter() - .map(|range| (range, empty_str.clone())), - None, - cx, - ); - buffer.snapshot(cx) - }); - let new_selections = new_cursors - .into_iter() - .map(|(id, cursor)| { - let cursor = cursor.to_point(&buffer); - Selection { - id, - start: cursor, - end: cursor, - reversed: false, - goal: SelectionGoal::None, - } - }) - .collect(); - - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(new_selections); - }); - }); - } - - pub fn join_lines_impl( - &mut self, - insert_whitespace: bool, - window: &mut Window, - cx: &mut Context, - ) { - if self.read_only(cx) { - return; - } - let mut row_ranges = Vec::>::new(); - for selection in self.selections.all::(cx) { - let start = MultiBufferRow(selection.start.row); - // Treat single line selections as if they include the next line. Otherwise this action - // would do nothing for single line selections individual cursors. - let end = if selection.start.row == selection.end.row { - MultiBufferRow(selection.start.row + 1) - } else { - MultiBufferRow(selection.end.row) - }; - - if let Some(last_row_range) = row_ranges.last_mut() { - if start <= last_row_range.end { - last_row_range.end = end; - continue; - } - } - row_ranges.push(start..end); - } - - let snapshot = self.buffer.read(cx).snapshot(cx); - let mut cursor_positions = Vec::new(); - for row_range in &row_ranges { - let anchor = snapshot.anchor_before(Point::new( - row_range.end.previous_row().0, - snapshot.line_len(row_range.end.previous_row()), - )); - cursor_positions.push(anchor..anchor); - } - - self.transact(window, cx, |this, window, cx| { - for row_range in row_ranges.into_iter().rev() { - for row in row_range.iter_rows().rev() { - let end_of_line = Point::new(row.0, snapshot.line_len(row)); - let next_line_row = row.next_row(); - let indent = snapshot.indent_size_for_line(next_line_row); - let start_of_next_line = Point::new(next_line_row.0, indent.len); - - let replace = - if snapshot.line_len(next_line_row) > indent.len && insert_whitespace { - " " - } else { - "" - }; - - this.buffer.update(cx, |buffer, cx| { - buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx) - }); - } - } - - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select_anchor_ranges(cursor_positions) - }); - }); - } - - pub fn join_lines(&mut self, _: &JoinLines, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.join_lines_impl(true, window, cx); - } - - pub fn sort_lines_case_sensitive( - &mut self, - _: &SortLinesCaseSensitive, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_lines(window, cx, |lines| lines.sort()) - } - - pub fn sort_lines_case_insensitive( - &mut self, - _: &SortLinesCaseInsensitive, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_lines(window, cx, |lines| { - lines.sort_by_key(|line| line.to_lowercase()) - }) - } - - pub fn unique_lines_case_insensitive( - &mut self, - _: &UniqueLinesCaseInsensitive, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_lines(window, cx, |lines| { - let mut seen = HashSet::default(); - lines.retain(|line| seen.insert(line.to_lowercase())); - }) - } - - pub fn unique_lines_case_sensitive( - &mut self, - _: &UniqueLinesCaseSensitive, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_lines(window, cx, |lines| { - let mut seen = HashSet::default(); - lines.retain(|line| seen.insert(*line)); - }) - } - - pub fn reload_file(&mut self, _: &ReloadFile, window: &mut Window, cx: &mut Context) { - let Some(project) = self.project.clone() else { - return; - }; - self.reload(project, window, cx) - .detach_and_notify_err(window, cx); - } - - pub fn restore_file( - &mut self, - _: &::git::RestoreFile, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - let mut buffer_ids = HashSet::default(); - let snapshot = self.buffer().read(cx).snapshot(cx); - for selection in self.selections.all::(cx) { - buffer_ids.extend(snapshot.buffer_ids_for_range(selection.range())) - } - - let buffer = self.buffer().read(cx); - let ranges = buffer_ids - .into_iter() - .flat_map(|buffer_id| buffer.excerpt_ranges_for_buffer(buffer_id, cx)) - .collect::>(); - - self.restore_hunks_in_ranges(ranges, window, cx); - } - - pub fn git_restore(&mut self, _: &Restore, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - let selections = self - .selections - .all(cx) - .into_iter() - .map(|s| s.range()) - .collect(); - self.restore_hunks_in_ranges(selections, window, cx); - } - - pub fn restore_hunks_in_ranges( - &mut self, - ranges: Vec>, - window: &mut Window, - cx: &mut Context, - ) { - let mut revert_changes = HashMap::default(); - let chunk_by = self - .snapshot(window, cx) - .hunks_for_ranges(ranges) - .into_iter() - .chunk_by(|hunk| hunk.buffer_id); - for (buffer_id, hunks) in &chunk_by { - let hunks = hunks.collect::>(); - for hunk in &hunks { - self.prepare_restore_change(&mut revert_changes, hunk, cx); - } - self.do_stage_or_unstage(false, buffer_id, hunks.into_iter(), cx); - } - drop(chunk_by); - if !revert_changes.is_empty() { - self.transact(window, cx, |editor, window, cx| { - editor.restore(revert_changes, window, cx); - }); - } - } - - pub fn open_active_item_in_terminal( - &mut self, - _: &OpenInTerminal, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(working_directory) = self.active_excerpt(cx).and_then(|(_, buffer, _)| { - let project_path = buffer.read(cx).project_path(cx)?; - let project = self.project.as_ref()?.read(cx); - let entry = project.entry_for_path(&project_path, cx)?; - let parent = match &entry.canonical_path { - Some(canonical_path) => canonical_path.to_path_buf(), - None => project.absolute_path(&project_path, cx)?, - } - .parent()? - .to_path_buf(); - Some(parent) - }) { - window.dispatch_action(OpenTerminal { working_directory }.boxed_clone(), cx); - } - } - - fn set_breakpoint_context_menu( - &mut self, - display_row: DisplayRow, - position: Option, - clicked_point: gpui::Point, - window: &mut Window, - cx: &mut Context, - ) { - if !cx.has_flag::() { - return; - } - let source = self - .buffer - .read(cx) - .snapshot(cx) - .anchor_before(Point::new(display_row.0, 0u32)); - - let context_menu = self.breakpoint_context_menu(position.unwrap_or(source), window, cx); - - self.mouse_context_menu = MouseContextMenu::pinned_to_editor( - self, - source, - clicked_point, - context_menu, - window, - cx, - ); - } - - fn add_edit_breakpoint_block( - &mut self, - anchor: Anchor, - breakpoint: &Breakpoint, - edit_action: BreakpointPromptEditAction, - window: &mut Window, - cx: &mut Context, - ) { - let weak_editor = cx.weak_entity(); - let bp_prompt = cx.new(|cx| { - BreakpointPromptEditor::new( - weak_editor, - anchor, - breakpoint.clone(), - edit_action, - window, - cx, - ) - }); - - let height = bp_prompt.update(cx, |this, cx| { - this.prompt - .update(cx, |prompt, cx| prompt.max_point(cx).row().0 + 1 + 2) - }); - let cloned_prompt = bp_prompt.clone(); - let blocks = vec![BlockProperties { - style: BlockStyle::Sticky, - placement: BlockPlacement::Above(anchor), - height: Some(height), - render: Arc::new(move |cx| { - *cloned_prompt.read(cx).gutter_dimensions.lock() = *cx.gutter_dimensions; - cloned_prompt.clone().into_any_element() - }), - priority: 0, - }]; - - let focus_handle = bp_prompt.focus_handle(cx); - window.focus(&focus_handle); - - let block_ids = self.insert_blocks(blocks, None, cx); - bp_prompt.update(cx, |prompt, _| { - prompt.add_block_ids(block_ids); - }); - } - - pub(crate) fn breakpoint_at_row( - &self, - row: u32, - window: &mut Window, - cx: &mut Context, - ) -> Option<(Anchor, Breakpoint)> { - let snapshot = self.snapshot(window, cx); - let breakpoint_position = snapshot.buffer_snapshot.anchor_before(Point::new(row, 0)); - - self.breakpoint_at_anchor(breakpoint_position, &snapshot, cx) - } - - pub(crate) fn breakpoint_at_anchor( - &self, - breakpoint_position: Anchor, - snapshot: &EditorSnapshot, - cx: &mut Context, - ) -> Option<(Anchor, Breakpoint)> { - let project = self.project.clone()?; - - let buffer_id = breakpoint_position.buffer_id.or_else(|| { - snapshot - .buffer_snapshot - .buffer_id_for_excerpt(breakpoint_position.excerpt_id) - })?; - - let enclosing_excerpt = breakpoint_position.excerpt_id; - let buffer = project.read_with(cx, |project, cx| project.buffer_for_id(buffer_id, cx))?; - let buffer_snapshot = buffer.read(cx).snapshot(); - - let row = buffer_snapshot - .summary_for_anchor::(&breakpoint_position.text_anchor) - .row; - - let line_len = snapshot.buffer_snapshot.line_len(MultiBufferRow(row)); - let anchor_end = snapshot - .buffer_snapshot - .anchor_after(Point::new(row, line_len)); - - let bp = self - .breakpoint_store - .as_ref()? - .read_with(cx, |breakpoint_store, cx| { - breakpoint_store - .breakpoints( - &buffer, - Some(breakpoint_position.text_anchor..anchor_end.text_anchor), - &buffer_snapshot, - cx, - ) - .next() - .and_then(|(anchor, bp)| { - let breakpoint_row = buffer_snapshot - .summary_for_anchor::(anchor) - .row; - - if breakpoint_row == row { - snapshot - .buffer_snapshot - .anchor_in_excerpt(enclosing_excerpt, *anchor) - .map(|anchor| (anchor, bp.clone())) - } else { - None - } - }) - }); - bp - } - - pub fn edit_log_breakpoint( - &mut self, - _: &EditLogBreakpoint, - window: &mut Window, - cx: &mut Context, - ) { - for (anchor, breakpoint) in self.breakpoints_at_cursors(window, cx) { - let breakpoint = breakpoint.unwrap_or_else(|| Breakpoint { - message: None, - state: BreakpointState::Enabled, - condition: None, - hit_condition: None, - }); - - self.add_edit_breakpoint_block( - anchor, - &breakpoint, - BreakpointPromptEditAction::Log, - window, - cx, - ); - } - } - - fn breakpoints_at_cursors( - &self, - window: &mut Window, - cx: &mut Context, - ) -> Vec<(Anchor, Option)> { - let snapshot = self.snapshot(window, cx); - let cursors = self - .selections - .disjoint_anchors() - .into_iter() - .map(|selection| { - let cursor_position: Point = selection.head().to_point(&snapshot.buffer_snapshot); - - let breakpoint_position = self - .breakpoint_at_row(cursor_position.row, window, cx) - .map(|bp| bp.0) - .unwrap_or_else(|| { - snapshot - .display_snapshot - .buffer_snapshot - .anchor_after(Point::new(cursor_position.row, 0)) - }); - - let breakpoint = self - .breakpoint_at_anchor(breakpoint_position, &snapshot, cx) - .map(|(anchor, breakpoint)| (anchor, Some(breakpoint))); - - breakpoint.unwrap_or_else(|| (breakpoint_position, None)) - }) - // There might be multiple cursors on the same line; all of them should have the same anchors though as their breakpoints positions, which makes it possible to sort and dedup the list. - .collect::>(); - - cursors.into_iter().collect() - } - - pub fn enable_breakpoint( - &mut self, - _: &crate::actions::EnableBreakpoint, - window: &mut Window, - cx: &mut Context, - ) { - for (anchor, breakpoint) in self.breakpoints_at_cursors(window, cx) { - let Some(breakpoint) = breakpoint.filter(|breakpoint| breakpoint.is_disabled()) else { - continue; - }; - self.edit_breakpoint_at_anchor( - anchor, - breakpoint, - BreakpointEditAction::InvertState, - cx, - ); - } - } - - pub fn disable_breakpoint( - &mut self, - _: &crate::actions::DisableBreakpoint, - window: &mut Window, - cx: &mut Context, - ) { - for (anchor, breakpoint) in self.breakpoints_at_cursors(window, cx) { - let Some(breakpoint) = breakpoint.filter(|breakpoint| breakpoint.is_enabled()) else { - continue; - }; - self.edit_breakpoint_at_anchor( - anchor, - breakpoint, - BreakpointEditAction::InvertState, - cx, - ); - } - } - - pub fn toggle_breakpoint( - &mut self, - _: &crate::actions::ToggleBreakpoint, - window: &mut Window, - cx: &mut Context, - ) { - for (anchor, breakpoint) in self.breakpoints_at_cursors(window, cx) { - if let Some(breakpoint) = breakpoint { - self.edit_breakpoint_at_anchor( - anchor, - breakpoint, - BreakpointEditAction::Toggle, - cx, - ); - } else { - self.edit_breakpoint_at_anchor( - anchor, - Breakpoint::new_standard(), - BreakpointEditAction::Toggle, - cx, - ); - } - } - } - - pub fn edit_breakpoint_at_anchor( - &mut self, - breakpoint_position: Anchor, - breakpoint: Breakpoint, - edit_action: BreakpointEditAction, - cx: &mut Context, - ) { - let Some(breakpoint_store) = &self.breakpoint_store else { - return; - }; - - let Some(buffer_id) = breakpoint_position.buffer_id.or_else(|| { - if breakpoint_position == Anchor::min() { - self.buffer() - .read(cx) - .excerpt_buffer_ids() - .into_iter() - .next() - } else { - None - } - }) else { - return; - }; - - let Some(buffer) = self.buffer().read(cx).buffer(buffer_id) else { - return; - }; - - breakpoint_store.update(cx, |breakpoint_store, cx| { - breakpoint_store.toggle_breakpoint( - buffer, - (breakpoint_position.text_anchor, breakpoint), - edit_action, - cx, - ); - }); - - cx.notify(); - } - - #[cfg(any(test, feature = "test-support"))] - pub fn breakpoint_store(&self) -> Option> { - self.breakpoint_store.clone() - } - - pub fn prepare_restore_change( - &self, - revert_changes: &mut HashMap, Rope)>>, - hunk: &MultiBufferDiffHunk, - cx: &mut App, - ) -> Option<()> { - if hunk.is_created_file() { - return None; - } - let buffer = self.buffer.read(cx); - let diff = buffer.diff_for(hunk.buffer_id)?; - let buffer = buffer.buffer(hunk.buffer_id)?; - let buffer = buffer.read(cx); - let original_text = diff - .read(cx) - .base_text() - .as_rope() - .slice(hunk.diff_base_byte_range.clone()); - let buffer_snapshot = buffer.snapshot(); - let buffer_revert_changes = revert_changes.entry(buffer.remote_id()).or_default(); - if let Err(i) = buffer_revert_changes.binary_search_by(|probe| { - probe - .0 - .start - .cmp(&hunk.buffer_range.start, &buffer_snapshot) - .then(probe.0.end.cmp(&hunk.buffer_range.end, &buffer_snapshot)) - }) { - buffer_revert_changes.insert(i, (hunk.buffer_range.clone(), original_text)); - Some(()) - } else { - None - } - } - - pub fn reverse_lines(&mut self, _: &ReverseLines, window: &mut Window, cx: &mut Context) { - self.manipulate_lines(window, cx, |lines| lines.reverse()) - } - - pub fn shuffle_lines(&mut self, _: &ShuffleLines, window: &mut Window, cx: &mut Context) { - self.manipulate_lines(window, cx, |lines| lines.shuffle(&mut thread_rng())) - } - - fn manipulate_lines( - &mut self, - window: &mut Window, - cx: &mut Context, - mut callback: Fn, - ) where - Fn: FnMut(&mut Vec<&str>), - { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = self.buffer.read(cx).snapshot(cx); - - let mut edits = Vec::new(); - - let selections = self.selections.all::(cx); - let mut selections = selections.iter().peekable(); - let mut contiguous_row_selections = Vec::new(); - let mut new_selections = Vec::new(); - let mut added_lines = 0; - let mut removed_lines = 0; - - while let Some(selection) = selections.next() { - let (start_row, end_row) = consume_contiguous_rows( - &mut contiguous_row_selections, - selection, - &display_map, - &mut selections, - ); - - let start_point = Point::new(start_row.0, 0); - let end_point = Point::new( - end_row.previous_row().0, - buffer.line_len(end_row.previous_row()), - ); - let text = buffer - .text_for_range(start_point..end_point) - .collect::(); - - let mut lines = text.split('\n').collect_vec(); - - let lines_before = lines.len(); - callback(&mut lines); - let lines_after = lines.len(); - - edits.push((start_point..end_point, lines.join("\n"))); - - // Selections must change based on added and removed line count - let start_row = - MultiBufferRow(start_point.row + added_lines as u32 - removed_lines as u32); - let end_row = MultiBufferRow(start_row.0 + lines_after.saturating_sub(1) as u32); - new_selections.push(Selection { - id: selection.id, - start: start_row, - end: end_row, - goal: SelectionGoal::None, - reversed: selection.reversed, - }); - - if lines_after > lines_before { - added_lines += lines_after - lines_before; - } else if lines_before > lines_after { - removed_lines += lines_before - lines_after; - } - } - - self.transact(window, cx, |this, window, cx| { - let buffer = this.buffer.update(cx, |buffer, cx| { - buffer.edit(edits, None, cx); - buffer.snapshot(cx) - }); - - // Recalculate offsets on newly edited buffer - let new_selections = new_selections - .iter() - .map(|s| { - let start_point = Point::new(s.start.0, 0); - let end_point = Point::new(s.end.0, buffer.line_len(s.end)); - Selection { - id: s.id, - start: buffer.point_to_offset(start_point), - end: buffer.point_to_offset(end_point), - goal: s.goal, - reversed: s.reversed, - } - }) - .collect(); - - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(new_selections); - }); - - this.request_autoscroll(Autoscroll::fit(), cx); - }); - } - - pub fn toggle_case(&mut self, _: &ToggleCase, window: &mut Window, cx: &mut Context) { - self.manipulate_text(window, cx, |text| { - let has_upper_case_characters = text.chars().any(|c| c.is_uppercase()); - if has_upper_case_characters { - text.to_lowercase() - } else { - text.to_uppercase() - } - }) - } - - pub fn convert_to_upper_case( - &mut self, - _: &ConvertToUpperCase, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_text(window, cx, |text| text.to_uppercase()) - } - - pub fn convert_to_lower_case( - &mut self, - _: &ConvertToLowerCase, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_text(window, cx, |text| text.to_lowercase()) - } - - pub fn convert_to_title_case( - &mut self, - _: &ConvertToTitleCase, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_text(window, cx, |text| { - text.split('\n') - .map(|line| line.to_case(Case::Title)) - .join("\n") - }) - } - - pub fn convert_to_snake_case( - &mut self, - _: &ConvertToSnakeCase, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_text(window, cx, |text| text.to_case(Case::Snake)) - } - - pub fn convert_to_kebab_case( - &mut self, - _: &ConvertToKebabCase, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_text(window, cx, |text| text.to_case(Case::Kebab)) - } - - pub fn convert_to_upper_camel_case( - &mut self, - _: &ConvertToUpperCamelCase, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_text(window, cx, |text| { - text.split('\n') - .map(|line| line.to_case(Case::UpperCamel)) - .join("\n") - }) - } - - pub fn convert_to_lower_camel_case( - &mut self, - _: &ConvertToLowerCamelCase, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_text(window, cx, |text| text.to_case(Case::Camel)) - } - - pub fn convert_to_opposite_case( - &mut self, - _: &ConvertToOppositeCase, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_text(window, cx, |text| { - text.chars() - .fold(String::with_capacity(text.len()), |mut t, c| { - if c.is_uppercase() { - t.extend(c.to_lowercase()); - } else { - t.extend(c.to_uppercase()); - } - t - }) - }) - } - - pub fn convert_to_rot13( - &mut self, - _: &ConvertToRot13, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_text(window, cx, |text| { - text.chars() - .map(|c| match c { - 'A'..='M' | 'a'..='m' => ((c as u8) + 13) as char, - 'N'..='Z' | 'n'..='z' => ((c as u8) - 13) as char, - _ => c, - }) - .collect() - }) - } - - pub fn convert_to_rot47( - &mut self, - _: &ConvertToRot47, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_text(window, cx, |text| { - text.chars() - .map(|c| { - let code_point = c as u32; - if code_point >= 33 && code_point <= 126 { - return char::from_u32(33 + ((code_point + 14) % 94)).unwrap(); - } - c - }) - .collect() - }) - } - - fn manipulate_text(&mut self, window: &mut Window, cx: &mut Context, mut callback: Fn) - where - Fn: FnMut(&str) -> String, - { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = self.buffer.read(cx).snapshot(cx); - - let mut new_selections = Vec::new(); - let mut edits = Vec::new(); - let mut selection_adjustment = 0i32; - - for selection in self.selections.all::(cx) { - let selection_is_empty = selection.is_empty(); - - let (start, end) = if selection_is_empty { - let word_range = movement::surrounding_word( - &display_map, - selection.start.to_display_point(&display_map), - ); - let start = word_range.start.to_offset(&display_map, Bias::Left); - let end = word_range.end.to_offset(&display_map, Bias::Left); - (start, end) - } else { - (selection.start, selection.end) - }; - - let text = buffer.text_for_range(start..end).collect::(); - let old_length = text.len() as i32; - let text = callback(&text); - - new_selections.push(Selection { - start: (start as i32 - selection_adjustment) as usize, - end: ((start + text.len()) as i32 - selection_adjustment) as usize, - goal: SelectionGoal::None, - ..selection - }); - - selection_adjustment += old_length - text.len() as i32; - - edits.push((start..end, text)); - } - - self.transact(window, cx, |this, window, cx| { - this.buffer.update(cx, |buffer, cx| { - buffer.edit(edits, None, cx); - }); - - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(new_selections); - }); - - this.request_autoscroll(Autoscroll::fit(), cx); - }); - } - - pub fn duplicate( - &mut self, - upwards: bool, - whole_lines: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = &display_map.buffer_snapshot; - let selections = self.selections.all::(cx); - - let mut edits = Vec::new(); - let mut selections_iter = selections.iter().peekable(); - while let Some(selection) = selections_iter.next() { - let mut rows = selection.spanned_rows(false, &display_map); - // duplicate line-wise - if whole_lines || selection.start == selection.end { - // Avoid duplicating the same lines twice. - while let Some(next_selection) = selections_iter.peek() { - let next_rows = next_selection.spanned_rows(false, &display_map); - if next_rows.start < rows.end { - rows.end = next_rows.end; - selections_iter.next().unwrap(); - } else { - break; - } - } - - // Copy the text from the selected row region and splice it either at the start - // or end of the region. - let start = Point::new(rows.start.0, 0); - let end = Point::new( - rows.end.previous_row().0, - buffer.line_len(rows.end.previous_row()), - ); - let text = buffer - .text_for_range(start..end) - .chain(Some("\n")) - .collect::(); - let insert_location = if upwards { - Point::new(rows.end.0, 0) - } else { - start - }; - edits.push((insert_location..insert_location, text)); - } else { - // duplicate character-wise - let start = selection.start; - let end = selection.end; - let text = buffer.text_for_range(start..end).collect::(); - edits.push((selection.end..selection.end, text)); - } - } - - self.transact(window, cx, |this, _, cx| { - this.buffer.update(cx, |buffer, cx| { - buffer.edit(edits, None, cx); - }); - - this.request_autoscroll(Autoscroll::fit(), cx); - }); - } - - pub fn duplicate_line_up( - &mut self, - _: &DuplicateLineUp, - window: &mut Window, - cx: &mut Context, - ) { - self.duplicate(true, true, window, cx); - } - - pub fn duplicate_line_down( - &mut self, - _: &DuplicateLineDown, - window: &mut Window, - cx: &mut Context, - ) { - self.duplicate(false, true, window, cx); - } - - pub fn duplicate_selection( - &mut self, - _: &DuplicateSelection, - window: &mut Window, - cx: &mut Context, - ) { - self.duplicate(false, false, window, cx); - } - - pub fn move_line_up(&mut self, _: &MoveLineUp, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = self.buffer.read(cx).snapshot(cx); - - let mut edits = Vec::new(); - let mut unfold_ranges = Vec::new(); - let mut refold_creases = Vec::new(); - - let selections = self.selections.all::(cx); - let mut selections = selections.iter().peekable(); - let mut contiguous_row_selections = Vec::new(); - let mut new_selections = Vec::new(); - - while let Some(selection) = selections.next() { - // Find all the selections that span a contiguous row range - let (start_row, end_row) = consume_contiguous_rows( - &mut contiguous_row_selections, - selection, - &display_map, - &mut selections, - ); - - // Move the text spanned by the row range to be before the line preceding the row range - if start_row.0 > 0 { - let range_to_move = Point::new( - start_row.previous_row().0, - buffer.line_len(start_row.previous_row()), - ) - ..Point::new( - end_row.previous_row().0, - buffer.line_len(end_row.previous_row()), - ); - let insertion_point = display_map - .prev_line_boundary(Point::new(start_row.previous_row().0, 0)) - .0; - - // Don't move lines across excerpts - if buffer - .excerpt_containing(insertion_point..range_to_move.end) - .is_some() - { - let text = buffer - .text_for_range(range_to_move.clone()) - .flat_map(|s| s.chars()) - .skip(1) - .chain(['\n']) - .collect::(); - - edits.push(( - buffer.anchor_after(range_to_move.start) - ..buffer.anchor_before(range_to_move.end), - String::new(), - )); - let insertion_anchor = buffer.anchor_after(insertion_point); - edits.push((insertion_anchor..insertion_anchor, text)); - - let row_delta = range_to_move.start.row - insertion_point.row + 1; - - // Move selections up - new_selections.extend(contiguous_row_selections.drain(..).map( - |mut selection| { - selection.start.row -= row_delta; - selection.end.row -= row_delta; - selection - }, - )); - - // Move folds up - unfold_ranges.push(range_to_move.clone()); - for fold in display_map.folds_in_range( - buffer.anchor_before(range_to_move.start) - ..buffer.anchor_after(range_to_move.end), - ) { - let mut start = fold.range.start.to_point(&buffer); - let mut end = fold.range.end.to_point(&buffer); - start.row -= row_delta; - end.row -= row_delta; - refold_creases.push(Crease::simple(start..end, fold.placeholder.clone())); - } - } - } - - // If we didn't move line(s), preserve the existing selections - new_selections.append(&mut contiguous_row_selections); - } - - self.transact(window, cx, |this, window, cx| { - this.unfold_ranges(&unfold_ranges, true, true, cx); - this.buffer.update(cx, |buffer, cx| { - for (range, text) in edits { - buffer.edit([(range, text)], None, cx); - } - }); - this.fold_creases(refold_creases, true, window, cx); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(new_selections); - }) - }); - } - - pub fn move_line_down( - &mut self, - _: &MoveLineDown, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = self.buffer.read(cx).snapshot(cx); - - let mut edits = Vec::new(); - let mut unfold_ranges = Vec::new(); - let mut refold_creases = Vec::new(); - - let selections = self.selections.all::(cx); - let mut selections = selections.iter().peekable(); - let mut contiguous_row_selections = Vec::new(); - let mut new_selections = Vec::new(); - - while let Some(selection) = selections.next() { - // Find all the selections that span a contiguous row range - let (start_row, end_row) = consume_contiguous_rows( - &mut contiguous_row_selections, - selection, - &display_map, - &mut selections, - ); - - // Move the text spanned by the row range to be after the last line of the row range - if end_row.0 <= buffer.max_point().row { - let range_to_move = - MultiBufferPoint::new(start_row.0, 0)..MultiBufferPoint::new(end_row.0, 0); - let insertion_point = display_map - .next_line_boundary(MultiBufferPoint::new(end_row.0, 0)) - .0; - - // Don't move lines across excerpt boundaries - if buffer - .excerpt_containing(range_to_move.start..insertion_point) - .is_some() - { - let mut text = String::from("\n"); - text.extend(buffer.text_for_range(range_to_move.clone())); - text.pop(); // Drop trailing newline - edits.push(( - buffer.anchor_after(range_to_move.start) - ..buffer.anchor_before(range_to_move.end), - String::new(), - )); - let insertion_anchor = buffer.anchor_after(insertion_point); - edits.push((insertion_anchor..insertion_anchor, text)); - - let row_delta = insertion_point.row - range_to_move.end.row + 1; - - // Move selections down - new_selections.extend(contiguous_row_selections.drain(..).map( - |mut selection| { - selection.start.row += row_delta; - selection.end.row += row_delta; - selection - }, - )); - - // Move folds down - unfold_ranges.push(range_to_move.clone()); - for fold in display_map.folds_in_range( - buffer.anchor_before(range_to_move.start) - ..buffer.anchor_after(range_to_move.end), - ) { - let mut start = fold.range.start.to_point(&buffer); - let mut end = fold.range.end.to_point(&buffer); - start.row += row_delta; - end.row += row_delta; - refold_creases.push(Crease::simple(start..end, fold.placeholder.clone())); - } - } - } - - // If we didn't move line(s), preserve the existing selections - new_selections.append(&mut contiguous_row_selections); - } - - self.transact(window, cx, |this, window, cx| { - this.unfold_ranges(&unfold_ranges, true, true, cx); - this.buffer.update(cx, |buffer, cx| { - for (range, text) in edits { - buffer.edit([(range, text)], None, cx); - } - }); - this.fold_creases(refold_creases, true, window, cx); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(new_selections) - }); - }); - } - - pub fn transpose(&mut self, _: &Transpose, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - let text_layout_details = &self.text_layout_details(window); - self.transact(window, cx, |this, window, cx| { - let edits = this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - let mut edits: Vec<(Range, String)> = Default::default(); - s.move_with(|display_map, selection| { - if !selection.is_empty() { - return; - } - - let mut head = selection.head(); - let mut transpose_offset = head.to_offset(display_map, Bias::Right); - if head.column() == display_map.line_len(head.row()) { - transpose_offset = display_map - .buffer_snapshot - .clip_offset(transpose_offset.saturating_sub(1), Bias::Left); - } - - if transpose_offset == 0 { - return; - } - - *head.column_mut() += 1; - head = display_map.clip_point(head, Bias::Right); - let goal = SelectionGoal::HorizontalPosition( - display_map - .x_for_display_point(head, text_layout_details) - .into(), - ); - selection.collapse_to(head, goal); - - let transpose_start = display_map - .buffer_snapshot - .clip_offset(transpose_offset.saturating_sub(1), Bias::Left); - if edits.last().map_or(true, |e| e.0.end <= transpose_start) { - let transpose_end = display_map - .buffer_snapshot - .clip_offset(transpose_offset + 1, Bias::Right); - if let Some(ch) = - display_map.buffer_snapshot.chars_at(transpose_start).next() - { - edits.push((transpose_start..transpose_offset, String::new())); - edits.push((transpose_end..transpose_end, ch.to_string())); - } - } - }); - edits - }); - this.buffer - .update(cx, |buffer, cx| buffer.edit(edits, None, cx)); - let selections = this.selections.all::(cx); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(selections); - }); - }); - } - - pub fn rewrap(&mut self, _: &Rewrap, _: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.rewrap_impl(RewrapOptions::default(), cx) - } - - pub fn rewrap_impl(&mut self, options: RewrapOptions, cx: &mut Context) { - let buffer = self.buffer.read(cx).snapshot(cx); - let selections = self.selections.all::(cx); - let mut selections = selections.iter().peekable(); - - let mut edits = Vec::new(); - let mut rewrapped_row_ranges = Vec::>::new(); - - while let Some(selection) = selections.next() { - let mut start_row = selection.start.row; - let mut end_row = selection.end.row; - - // Skip selections that overlap with a range that has already been rewrapped. - let selection_range = start_row..end_row; - if rewrapped_row_ranges - .iter() - .any(|range| range.overlaps(&selection_range)) - { - continue; - } - - let tab_size = buffer.language_settings_at(selection.head(), cx).tab_size; - - // Since not all lines in the selection may be at the same indent - // level, choose the indent size that is the most common between all - // of the lines. - // - // If there is a tie, we use the deepest indent. - let (indent_size, indent_end) = { - let mut indent_size_occurrences = HashMap::default(); - let mut rows_by_indent_size = HashMap::>::default(); - - for row in start_row..=end_row { - let indent = buffer.indent_size_for_line(MultiBufferRow(row)); - rows_by_indent_size.entry(indent).or_default().push(row); - *indent_size_occurrences.entry(indent).or_insert(0) += 1; - } - - let indent_size = indent_size_occurrences - .into_iter() - .max_by_key(|(indent, count)| (*count, indent.len_with_expanded_tabs(tab_size))) - .map(|(indent, _)| indent) - .unwrap_or_default(); - let row = rows_by_indent_size[&indent_size][0]; - let indent_end = Point::new(row, indent_size.len); - - (indent_size, indent_end) - }; - - let mut line_prefix = indent_size.chars().collect::(); - - let mut inside_comment = false; - if let Some(comment_prefix) = - buffer - .language_scope_at(selection.head()) - .and_then(|language| { - language - .line_comment_prefixes() - .iter() - .find(|prefix| buffer.contains_str_at(indent_end, prefix)) - .cloned() - }) - { - line_prefix.push_str(&comment_prefix); - inside_comment = true; - } - - let language_settings = buffer.language_settings_at(selection.head(), cx); - let allow_rewrap_based_on_language = match language_settings.allow_rewrap { - RewrapBehavior::InComments => inside_comment, - RewrapBehavior::InSelections => !selection.is_empty(), - RewrapBehavior::Anywhere => true, - }; - - let should_rewrap = options.override_language_settings - || allow_rewrap_based_on_language - || self.hard_wrap.is_some(); - if !should_rewrap { - continue; - } - - if selection.is_empty() { - 'expand_upwards: while start_row > 0 { - let prev_row = start_row - 1; - if buffer.contains_str_at(Point::new(prev_row, 0), &line_prefix) - && buffer.line_len(MultiBufferRow(prev_row)) as usize > line_prefix.len() - { - start_row = prev_row; - } else { - break 'expand_upwards; - } - } - - 'expand_downwards: while end_row < buffer.max_point().row { - let next_row = end_row + 1; - if buffer.contains_str_at(Point::new(next_row, 0), &line_prefix) - && buffer.line_len(MultiBufferRow(next_row)) as usize > line_prefix.len() - { - end_row = next_row; - } else { - break 'expand_downwards; - } - } - } - - let start = Point::new(start_row, 0); - let start_offset = start.to_offset(&buffer); - let end = Point::new(end_row, buffer.line_len(MultiBufferRow(end_row))); - let selection_text = buffer.text_for_range(start..end).collect::(); - let Some(lines_without_prefixes) = selection_text - .lines() - .map(|line| { - line.strip_prefix(&line_prefix) - .or_else(|| line.trim_start().strip_prefix(&line_prefix.trim_start())) - .with_context(|| { - format!("line did not start with prefix {line_prefix:?}: {line:?}") - }) - }) - .collect::, _>>() - .log_err() - else { - continue; - }; - - let wrap_column = self.hard_wrap.unwrap_or_else(|| { - buffer - .language_settings_at(Point::new(start_row, 0), cx) - .preferred_line_length as usize - }); - let wrapped_text = wrap_with_prefix( - line_prefix, - lines_without_prefixes.join("\n"), - wrap_column, - tab_size, - options.preserve_existing_whitespace, - ); - - // TODO: should always use char-based diff while still supporting cursor behavior that - // matches vim. - let mut diff_options = DiffOptions::default(); - if options.override_language_settings { - diff_options.max_word_diff_len = 0; - diff_options.max_word_diff_line_count = 0; - } else { - diff_options.max_word_diff_len = usize::MAX; - diff_options.max_word_diff_line_count = usize::MAX; - } - - for (old_range, new_text) in - text_diff_with_options(&selection_text, &wrapped_text, diff_options) - { - let edit_start = buffer.anchor_after(start_offset + old_range.start); - let edit_end = buffer.anchor_after(start_offset + old_range.end); - edits.push((edit_start..edit_end, new_text)); - } - - rewrapped_row_ranges.push(start_row..=end_row); - } - - self.buffer - .update(cx, |buffer, cx| buffer.edit(edits, None, cx)); - } - - pub fn cut_common(&mut self, window: &mut Window, cx: &mut Context) -> ClipboardItem { - let mut text = String::new(); - let buffer = self.buffer.read(cx).snapshot(cx); - let mut selections = self.selections.all::(cx); - let mut clipboard_selections = Vec::with_capacity(selections.len()); - { - let max_point = buffer.max_point(); - let mut is_first = true; - for selection in &mut selections { - let is_entire_line = selection.is_empty() || self.selections.line_mode; - if is_entire_line { - selection.start = Point::new(selection.start.row, 0); - if !selection.is_empty() && selection.end.column == 0 { - selection.end = cmp::min(max_point, selection.end); - } else { - selection.end = cmp::min(max_point, Point::new(selection.end.row + 1, 0)); - } - selection.goal = SelectionGoal::None; - } - if is_first { - is_first = false; - } else { - text += "\n"; - } - let mut len = 0; - for chunk in buffer.text_for_range(selection.start..selection.end) { - text.push_str(chunk); - len += chunk.len(); - } - clipboard_selections.push(ClipboardSelection { - len, - is_entire_line, - first_line_indent: buffer - .indent_size_for_line(MultiBufferRow(selection.start.row)) - .len, - }); - } - } - - self.transact(window, cx, |this, window, cx| { - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(selections); - }); - this.insert("", window, cx); - }); - ClipboardItem::new_string_with_json_metadata(text, clipboard_selections) - } - - pub fn cut(&mut self, _: &Cut, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - let item = self.cut_common(window, cx); - cx.write_to_clipboard(item); - } - - pub fn kill_ring_cut(&mut self, _: &KillRingCut, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.change_selections(None, window, cx, |s| { - s.move_with(|snapshot, sel| { - if sel.is_empty() { - sel.end = DisplayPoint::new(sel.end.row(), snapshot.line_len(sel.end.row())) - } - }); - }); - let item = self.cut_common(window, cx); - cx.set_global(KillRing(item)) - } - - pub fn kill_ring_yank( - &mut self, - _: &KillRingYank, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - let (text, metadata) = if let Some(KillRing(item)) = cx.try_global() { - if let Some(ClipboardEntry::String(kill_ring)) = item.entries().first() { - (kill_ring.text().to_string(), kill_ring.metadata_json()) - } else { - return; - } - } else { - return; - }; - self.do_paste(&text, metadata, false, window, cx); - } - - pub fn copy_and_trim(&mut self, _: &CopyAndTrim, _: &mut Window, cx: &mut Context) { - self.do_copy(true, cx); - } - - pub fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context) { - self.do_copy(false, cx); - } - - fn do_copy(&self, strip_leading_indents: bool, cx: &mut Context) { - let selections = self.selections.all::(cx); - let buffer = self.buffer.read(cx).read(cx); - let mut text = String::new(); - - let mut clipboard_selections = Vec::with_capacity(selections.len()); - { - let max_point = buffer.max_point(); - let mut is_first = true; - for selection in &selections { - let mut start = selection.start; - let mut end = selection.end; - let is_entire_line = selection.is_empty() || self.selections.line_mode; - if is_entire_line { - start = Point::new(start.row, 0); - end = cmp::min(max_point, Point::new(end.row + 1, 0)); - } - - let mut trimmed_selections = Vec::new(); - if strip_leading_indents && end.row.saturating_sub(start.row) > 0 { - let row = MultiBufferRow(start.row); - let first_indent = buffer.indent_size_for_line(row); - if first_indent.len == 0 || start.column > first_indent.len { - trimmed_selections.push(start..end); - } else { - trimmed_selections.push( - Point::new(row.0, first_indent.len) - ..Point::new(row.0, buffer.line_len(row)), - ); - for row in start.row + 1..=end.row { - let mut line_len = buffer.line_len(MultiBufferRow(row)); - if row == end.row { - line_len = end.column; - } - if line_len == 0 { - trimmed_selections - .push(Point::new(row, 0)..Point::new(row, line_len)); - continue; - } - let row_indent_size = buffer.indent_size_for_line(MultiBufferRow(row)); - if row_indent_size.len >= first_indent.len { - trimmed_selections.push( - Point::new(row, first_indent.len)..Point::new(row, line_len), - ); - } else { - trimmed_selections.clear(); - trimmed_selections.push(start..end); - break; - } - } - } - } else { - trimmed_selections.push(start..end); - } - - for trimmed_range in trimmed_selections { - if is_first { - is_first = false; - } else { - text += "\n"; - } - let mut len = 0; - for chunk in buffer.text_for_range(trimmed_range.start..trimmed_range.end) { - text.push_str(chunk); - len += chunk.len(); - } - clipboard_selections.push(ClipboardSelection { - len, - is_entire_line, - first_line_indent: buffer - .indent_size_for_line(MultiBufferRow(trimmed_range.start.row)) - .len, - }); - } - } - } - - cx.write_to_clipboard(ClipboardItem::new_string_with_json_metadata( - text, - clipboard_selections, - )); - } - - pub fn do_paste( - &mut self, - text: &String, - clipboard_selections: Option>, - handle_entire_lines: bool, - window: &mut Window, - cx: &mut Context, - ) { - if self.read_only(cx) { - return; - } - - let clipboard_text = Cow::Borrowed(text); - - self.transact(window, cx, |this, window, cx| { - if let Some(mut clipboard_selections) = clipboard_selections { - let old_selections = this.selections.all::(cx); - let all_selections_were_entire_line = - clipboard_selections.iter().all(|s| s.is_entire_line); - let first_selection_indent_column = - clipboard_selections.first().map(|s| s.first_line_indent); - if clipboard_selections.len() != old_selections.len() { - clipboard_selections.drain(..); - } - let cursor_offset = this.selections.last::(cx).head(); - let mut auto_indent_on_paste = true; - - this.buffer.update(cx, |buffer, cx| { - let snapshot = buffer.read(cx); - auto_indent_on_paste = snapshot - .language_settings_at(cursor_offset, cx) - .auto_indent_on_paste; - - let mut start_offset = 0; - let mut edits = Vec::new(); - let mut original_indent_columns = Vec::new(); - for (ix, selection) in old_selections.iter().enumerate() { - let to_insert; - let entire_line; - let original_indent_column; - if let Some(clipboard_selection) = clipboard_selections.get(ix) { - let end_offset = start_offset + clipboard_selection.len; - to_insert = &clipboard_text[start_offset..end_offset]; - entire_line = clipboard_selection.is_entire_line; - start_offset = end_offset + 1; - original_indent_column = Some(clipboard_selection.first_line_indent); - } else { - to_insert = clipboard_text.as_str(); - entire_line = all_selections_were_entire_line; - original_indent_column = first_selection_indent_column - } - - // If the corresponding selection was empty when this slice of the - // clipboard text was written, then the entire line containing the - // selection was copied. If this selection is also currently empty, - // then paste the line before the current line of the buffer. - let range = if selection.is_empty() && handle_entire_lines && entire_line { - let column = selection.start.to_point(&snapshot).column as usize; - let line_start = selection.start - column; - line_start..line_start - } else { - selection.range() - }; - - edits.push((range, to_insert)); - original_indent_columns.push(original_indent_column); - } - drop(snapshot); - - buffer.edit( - edits, - if auto_indent_on_paste { - Some(AutoindentMode::Block { - original_indent_columns, - }) - } else { - None - }, - cx, - ); - }); - - let selections = this.selections.all::(cx); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(selections) - }); - } else { - this.insert(&clipboard_text, window, cx); - } - }); - } - - pub fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - if let Some(item) = cx.read_from_clipboard() { - let entries = item.entries(); - - match entries.first() { - // For now, we only support applying metadata if there's one string. In the future, we can incorporate all the selections - // of all the pasted entries. - Some(ClipboardEntry::String(clipboard_string)) if entries.len() == 1 => self - .do_paste( - clipboard_string.text(), - clipboard_string.metadata_json::>(), - true, - window, - cx, - ), - _ => self.do_paste(&item.text().unwrap_or_default(), None, true, window, cx), - } - } - } - - pub fn undo(&mut self, _: &Undo, window: &mut Window, cx: &mut Context) { - if self.read_only(cx) { - return; - } - - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - - if let Some(transaction_id) = self.buffer.update(cx, |buffer, cx| buffer.undo(cx)) { - if let Some((selections, _)) = - self.selection_history.transaction(transaction_id).cloned() - { - self.change_selections(None, window, cx, |s| { - s.select_anchors(selections.to_vec()); - }); - } else { - log::error!( - "No entry in selection_history found for undo. \ - This may correspond to a bug where undo does not update the selection. \ - If this is occurring, please add details to \ - https://github.com/zed-industries/zed/issues/22692" - ); - } - self.request_autoscroll(Autoscroll::fit(), cx); - self.unmark_text(window, cx); - self.refresh_inline_completion(true, false, window, cx); - cx.emit(EditorEvent::Edited { transaction_id }); - cx.emit(EditorEvent::TransactionUndone { transaction_id }); - } - } - - pub fn redo(&mut self, _: &Redo, window: &mut Window, cx: &mut Context) { - if self.read_only(cx) { - return; - } - - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - - if let Some(transaction_id) = self.buffer.update(cx, |buffer, cx| buffer.redo(cx)) { - if let Some((_, Some(selections))) = - self.selection_history.transaction(transaction_id).cloned() - { - self.change_selections(None, window, cx, |s| { - s.select_anchors(selections.to_vec()); - }); - } else { - log::error!( - "No entry in selection_history found for redo. \ - This may correspond to a bug where undo does not update the selection. \ - If this is occurring, please add details to \ - https://github.com/zed-industries/zed/issues/22692" - ); - } - self.request_autoscroll(Autoscroll::fit(), cx); - self.unmark_text(window, cx); - self.refresh_inline_completion(true, false, window, cx); - cx.emit(EditorEvent::Edited { transaction_id }); - } - } - - pub fn finalize_last_transaction(&mut self, cx: &mut Context) { - self.buffer - .update(cx, |buffer, cx| buffer.finalize_last_transaction(cx)); - } - - pub fn group_until_transaction(&mut self, tx_id: TransactionId, cx: &mut Context) { - self.buffer - .update(cx, |buffer, cx| buffer.group_until_transaction(tx_id, cx)); - } - - pub fn move_left(&mut self, _: &MoveLeft, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - let cursor = if selection.is_empty() { - movement::left(map, selection.start) - } else { - selection.start - }; - selection.collapse_to(cursor, SelectionGoal::None); - }); - }) - } - - pub fn select_left(&mut self, _: &SelectLeft, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, _| (movement::left(map, head), SelectionGoal::None)); - }) - } - - pub fn move_right(&mut self, _: &MoveRight, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - let cursor = if selection.is_empty() { - movement::right(map, selection.end) - } else { - selection.end - }; - selection.collapse_to(cursor, SelectionGoal::None) - }); - }) - } - - pub fn select_right(&mut self, _: &SelectRight, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, _| (movement::right(map, head), SelectionGoal::None)); - }) - } - - pub fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context) { - if self.take_rename(true, window, cx).is_some() { - return; - } - - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - - let text_layout_details = &self.text_layout_details(window); - let selection_count = self.selections.count(); - let first_selection = self.selections.first_anchor(); - - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - if !selection.is_empty() { - selection.goal = SelectionGoal::None; - } - let (cursor, goal) = movement::up( - map, - selection.start, - selection.goal, - false, - text_layout_details, - ); - selection.collapse_to(cursor, goal); - }); - }); - - if selection_count == 1 && first_selection.range() == self.selections.first_anchor().range() - { - cx.propagate(); - } - } - - pub fn move_up_by_lines( - &mut self, - action: &MoveUpByLines, - window: &mut Window, - cx: &mut Context, - ) { - if self.take_rename(true, window, cx).is_some() { - return; - } - - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - - let text_layout_details = &self.text_layout_details(window); - - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - if !selection.is_empty() { - selection.goal = SelectionGoal::None; - } - let (cursor, goal) = movement::up_by_rows( - map, - selection.start, - action.lines, - selection.goal, - false, - text_layout_details, - ); - selection.collapse_to(cursor, goal); - }); - }) - } - - pub fn move_down_by_lines( - &mut self, - action: &MoveDownByLines, - window: &mut Window, - cx: &mut Context, - ) { - if self.take_rename(true, window, cx).is_some() { - return; - } - - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - - let text_layout_details = &self.text_layout_details(window); - - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - if !selection.is_empty() { - selection.goal = SelectionGoal::None; - } - let (cursor, goal) = movement::down_by_rows( - map, - selection.start, - action.lines, - selection.goal, - false, - text_layout_details, - ); - selection.collapse_to(cursor, goal); - }); - }) - } - - pub fn select_down_by_lines( - &mut self, - action: &SelectDownByLines, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - let text_layout_details = &self.text_layout_details(window); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, goal| { - movement::down_by_rows(map, head, action.lines, goal, false, text_layout_details) - }) - }) - } - - pub fn select_up_by_lines( - &mut self, - action: &SelectUpByLines, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - let text_layout_details = &self.text_layout_details(window); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, goal| { - movement::up_by_rows(map, head, action.lines, goal, false, text_layout_details) - }) - }) - } - - pub fn select_page_up( - &mut self, - _: &SelectPageUp, - window: &mut Window, - cx: &mut Context, - ) { - let Some(row_count) = self.visible_row_count() else { - return; - }; - - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - - let text_layout_details = &self.text_layout_details(window); - - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, goal| { - movement::up_by_rows(map, head, row_count, goal, false, text_layout_details) - }) - }) - } - - pub fn move_page_up( - &mut self, - action: &MovePageUp, - window: &mut Window, - cx: &mut Context, - ) { - if self.take_rename(true, window, cx).is_some() { - return; - } - - if self - .context_menu - .borrow_mut() - .as_mut() - .map(|menu| menu.select_first(self.completion_provider.as_deref(), cx)) - .unwrap_or(false) - { - return; - } - - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - - let Some(row_count) = self.visible_row_count() else { - return; - }; - - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - - let autoscroll = if action.center_cursor { - Autoscroll::center() - } else { - Autoscroll::fit() - }; - - let text_layout_details = &self.text_layout_details(window); - - self.change_selections(Some(autoscroll), window, cx, |s| { - s.move_with(|map, selection| { - if !selection.is_empty() { - selection.goal = SelectionGoal::None; - } - let (cursor, goal) = movement::up_by_rows( - map, - selection.end, - row_count, - selection.goal, - false, - text_layout_details, - ); - selection.collapse_to(cursor, goal); - }); - }); - } - - pub fn select_up(&mut self, _: &SelectUp, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - let text_layout_details = &self.text_layout_details(window); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, goal| { - movement::up(map, head, goal, false, text_layout_details) - }) - }) - } - - pub fn move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut Context) { - self.take_rename(true, window, cx); - - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - - let text_layout_details = &self.text_layout_details(window); - let selection_count = self.selections.count(); - let first_selection = self.selections.first_anchor(); - - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - if !selection.is_empty() { - selection.goal = SelectionGoal::None; - } - let (cursor, goal) = movement::down( - map, - selection.end, - selection.goal, - false, - text_layout_details, - ); - selection.collapse_to(cursor, goal); - }); - }); - - if selection_count == 1 && first_selection.range() == self.selections.first_anchor().range() - { - cx.propagate(); - } - } - - pub fn select_page_down( - &mut self, - _: &SelectPageDown, - window: &mut Window, - cx: &mut Context, - ) { - let Some(row_count) = self.visible_row_count() else { - return; - }; - - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - - let text_layout_details = &self.text_layout_details(window); - - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, goal| { - movement::down_by_rows(map, head, row_count, goal, false, text_layout_details) - }) - }) - } - - pub fn move_page_down( - &mut self, - action: &MovePageDown, - window: &mut Window, - cx: &mut Context, - ) { - if self.take_rename(true, window, cx).is_some() { - return; - } - - if self - .context_menu - .borrow_mut() - .as_mut() - .map(|menu| menu.select_last(self.completion_provider.as_deref(), cx)) - .unwrap_or(false) - { - return; - } - - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - - let Some(row_count) = self.visible_row_count() else { - return; - }; - - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - - let autoscroll = if action.center_cursor { - Autoscroll::center() - } else { - Autoscroll::fit() - }; - - let text_layout_details = &self.text_layout_details(window); - self.change_selections(Some(autoscroll), window, cx, |s| { - s.move_with(|map, selection| { - if !selection.is_empty() { - selection.goal = SelectionGoal::None; - } - let (cursor, goal) = movement::down_by_rows( - map, - selection.end, - row_count, - selection.goal, - false, - text_layout_details, - ); - selection.collapse_to(cursor, goal); - }); - }); - } - - pub fn select_down(&mut self, _: &SelectDown, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - let text_layout_details = &self.text_layout_details(window); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, goal| { - movement::down(map, head, goal, false, text_layout_details) - }) - }); - } - - pub fn context_menu_first( - &mut self, - _: &ContextMenuFirst, - _window: &mut Window, - cx: &mut Context, - ) { - if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { - context_menu.select_first(self.completion_provider.as_deref(), cx); - } - } - - pub fn context_menu_prev( - &mut self, - _: &ContextMenuPrevious, - _window: &mut Window, - cx: &mut Context, - ) { - if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { - context_menu.select_prev(self.completion_provider.as_deref(), cx); - } - } - - pub fn context_menu_next( - &mut self, - _: &ContextMenuNext, - _window: &mut Window, - cx: &mut Context, - ) { - if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { - context_menu.select_next(self.completion_provider.as_deref(), cx); - } - } - - pub fn context_menu_last( - &mut self, - _: &ContextMenuLast, - _window: &mut Window, - cx: &mut Context, - ) { - if let Some(context_menu) = self.context_menu.borrow_mut().as_mut() { - context_menu.select_last(self.completion_provider.as_deref(), cx); - } - } - - pub fn move_to_previous_word_start( - &mut self, - _: &MoveToPreviousWordStart, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_cursors_with(|map, head, _| { - ( - movement::previous_word_start(map, head), - SelectionGoal::None, - ) - }); - }) - } - - pub fn move_to_previous_subword_start( - &mut self, - _: &MoveToPreviousSubwordStart, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_cursors_with(|map, head, _| { - ( - movement::previous_subword_start(map, head), - SelectionGoal::None, - ) - }); - }) - } - - pub fn select_to_previous_word_start( - &mut self, - _: &SelectToPreviousWordStart, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, _| { - ( - movement::previous_word_start(map, head), - SelectionGoal::None, - ) - }); - }) - } - - pub fn select_to_previous_subword_start( - &mut self, - _: &SelectToPreviousSubwordStart, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, _| { - ( - movement::previous_subword_start(map, head), - SelectionGoal::None, - ) - }); - }) - } - - pub fn delete_to_previous_word_start( - &mut self, - action: &DeleteToPreviousWordStart, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.transact(window, cx, |this, window, cx| { - this.select_autoclose_pair(window, cx); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - if selection.is_empty() { - let cursor = if action.ignore_newlines { - movement::previous_word_start(map, selection.head()) - } else { - movement::previous_word_start_or_newline(map, selection.head()) - }; - selection.set_head(cursor, SelectionGoal::None); - } - }); - }); - this.insert("", window, cx); - }); - } - - pub fn delete_to_previous_subword_start( - &mut self, - _: &DeleteToPreviousSubwordStart, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.transact(window, cx, |this, window, cx| { - this.select_autoclose_pair(window, cx); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - if selection.is_empty() { - let cursor = movement::previous_subword_start(map, selection.head()); - selection.set_head(cursor, SelectionGoal::None); - } - }); - }); - this.insert("", window, cx); - }); - } - - pub fn move_to_next_word_end( - &mut self, - _: &MoveToNextWordEnd, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_cursors_with(|map, head, _| { - (movement::next_word_end(map, head), SelectionGoal::None) - }); - }) - } - - pub fn move_to_next_subword_end( - &mut self, - _: &MoveToNextSubwordEnd, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_cursors_with(|map, head, _| { - (movement::next_subword_end(map, head), SelectionGoal::None) - }); - }) - } - - pub fn select_to_next_word_end( - &mut self, - _: &SelectToNextWordEnd, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, _| { - (movement::next_word_end(map, head), SelectionGoal::None) - }); - }) - } - - pub fn select_to_next_subword_end( - &mut self, - _: &SelectToNextSubwordEnd, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, _| { - (movement::next_subword_end(map, head), SelectionGoal::None) - }); - }) - } - - pub fn delete_to_next_word_end( - &mut self, - action: &DeleteToNextWordEnd, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.transact(window, cx, |this, window, cx| { - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - if selection.is_empty() { - let cursor = if action.ignore_newlines { - movement::next_word_end(map, selection.head()) - } else { - movement::next_word_end_or_newline(map, selection.head()) - }; - selection.set_head(cursor, SelectionGoal::None); - } - }); - }); - this.insert("", window, cx); - }); - } - - pub fn delete_to_next_subword_end( - &mut self, - _: &DeleteToNextSubwordEnd, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.transact(window, cx, |this, window, cx| { - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - if selection.is_empty() { - let cursor = movement::next_subword_end(map, selection.head()); - selection.set_head(cursor, SelectionGoal::None); - } - }); - }); - this.insert("", window, cx); - }); - } - - pub fn move_to_beginning_of_line( - &mut self, - action: &MoveToBeginningOfLine, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_cursors_with(|map, head, _| { - ( - movement::indented_line_beginning( - map, - head, - action.stop_at_soft_wraps, - action.stop_at_indent, - ), - SelectionGoal::None, - ) - }); - }) - } - - pub fn select_to_beginning_of_line( - &mut self, - action: &SelectToBeginningOfLine, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, _| { - ( - movement::indented_line_beginning( - map, - head, - action.stop_at_soft_wraps, - action.stop_at_indent, - ), - SelectionGoal::None, - ) - }); - }); - } - - pub fn delete_to_beginning_of_line( - &mut self, - action: &DeleteToBeginningOfLine, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.transact(window, cx, |this, window, cx| { - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|_, selection| { - selection.reversed = true; - }); - }); - - this.select_to_beginning_of_line( - &SelectToBeginningOfLine { - stop_at_soft_wraps: false, - stop_at_indent: action.stop_at_indent, - }, - window, - cx, - ); - this.backspace(&Backspace, window, cx); - }); - } - - pub fn move_to_end_of_line( - &mut self, - action: &MoveToEndOfLine, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_cursors_with(|map, head, _| { - ( - movement::line_end(map, head, action.stop_at_soft_wraps), - SelectionGoal::None, - ) - }); - }) - } - - pub fn select_to_end_of_line( - &mut self, - action: &SelectToEndOfLine, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, _| { - ( - movement::line_end(map, head, action.stop_at_soft_wraps), - SelectionGoal::None, - ) - }); - }) - } - - pub fn delete_to_end_of_line( - &mut self, - _: &DeleteToEndOfLine, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.transact(window, cx, |this, window, cx| { - this.select_to_end_of_line( - &SelectToEndOfLine { - stop_at_soft_wraps: false, - }, - window, - cx, - ); - this.delete(&Delete, window, cx); - }); - } - - pub fn cut_to_end_of_line( - &mut self, - _: &CutToEndOfLine, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.transact(window, cx, |this, window, cx| { - this.select_to_end_of_line( - &SelectToEndOfLine { - stop_at_soft_wraps: false, - }, - window, - cx, - ); - this.cut(&Cut, window, cx); - }); - } - - pub fn move_to_start_of_paragraph( - &mut self, - _: &MoveToStartOfParagraph, - window: &mut Window, - cx: &mut Context, - ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - selection.collapse_to( - movement::start_of_paragraph(map, selection.head(), 1), - SelectionGoal::None, - ) - }); - }) - } - - pub fn move_to_end_of_paragraph( - &mut self, - _: &MoveToEndOfParagraph, - window: &mut Window, - cx: &mut Context, - ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - selection.collapse_to( - movement::end_of_paragraph(map, selection.head(), 1), - SelectionGoal::None, - ) - }); - }) - } - - pub fn select_to_start_of_paragraph( - &mut self, - _: &SelectToStartOfParagraph, - window: &mut Window, - cx: &mut Context, - ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, _| { - ( - movement::start_of_paragraph(map, head, 1), - SelectionGoal::None, - ) - }); - }) - } - - pub fn select_to_end_of_paragraph( - &mut self, - _: &SelectToEndOfParagraph, - window: &mut Window, - cx: &mut Context, - ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, _| { - ( - movement::end_of_paragraph(map, head, 1), - SelectionGoal::None, - ) - }); - }) - } - - pub fn move_to_start_of_excerpt( - &mut self, - _: &MoveToStartOfExcerpt, - window: &mut Window, - cx: &mut Context, - ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - selection.collapse_to( - movement::start_of_excerpt( - map, - selection.head(), - workspace::searchable::Direction::Prev, - ), - SelectionGoal::None, - ) - }); - }) - } - - pub fn move_to_start_of_next_excerpt( - &mut self, - _: &MoveToStartOfNextExcerpt, - window: &mut Window, - cx: &mut Context, - ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - selection.collapse_to( - movement::start_of_excerpt( - map, - selection.head(), - workspace::searchable::Direction::Next, - ), - SelectionGoal::None, - ) - }); - }) - } - - pub fn move_to_end_of_excerpt( - &mut self, - _: &MoveToEndOfExcerpt, - window: &mut Window, - cx: &mut Context, - ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - selection.collapse_to( - movement::end_of_excerpt( - map, - selection.head(), - workspace::searchable::Direction::Next, - ), - SelectionGoal::None, - ) - }); - }) - } - - pub fn move_to_end_of_previous_excerpt( - &mut self, - _: &MoveToEndOfPreviousExcerpt, - window: &mut Window, - cx: &mut Context, - ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_with(|map, selection| { - selection.collapse_to( - movement::end_of_excerpt( - map, - selection.head(), - workspace::searchable::Direction::Prev, - ), - SelectionGoal::None, - ) - }); - }) - } - - pub fn select_to_start_of_excerpt( - &mut self, - _: &SelectToStartOfExcerpt, - window: &mut Window, - cx: &mut Context, - ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, _| { - ( - movement::start_of_excerpt(map, head, workspace::searchable::Direction::Prev), - SelectionGoal::None, - ) - }); - }) - } - - pub fn select_to_start_of_next_excerpt( - &mut self, - _: &SelectToStartOfNextExcerpt, - window: &mut Window, - cx: &mut Context, - ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, _| { - ( - movement::start_of_excerpt(map, head, workspace::searchable::Direction::Next), - SelectionGoal::None, - ) - }); - }) - } - - pub fn select_to_end_of_excerpt( - &mut self, - _: &SelectToEndOfExcerpt, - window: &mut Window, - cx: &mut Context, - ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, _| { - ( - movement::end_of_excerpt(map, head, workspace::searchable::Direction::Next), - SelectionGoal::None, - ) - }); - }) - } - - pub fn select_to_end_of_previous_excerpt( - &mut self, - _: &SelectToEndOfPreviousExcerpt, - window: &mut Window, - cx: &mut Context, - ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_heads_with(|map, head, _| { - ( - movement::end_of_excerpt(map, head, workspace::searchable::Direction::Prev), - SelectionGoal::None, - ) - }); - }) - } - - pub fn move_to_beginning( - &mut self, - _: &MoveToBeginning, - window: &mut Window, - cx: &mut Context, - ) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select_ranges(vec![0..0]); - }); - } - - pub fn select_to_beginning( - &mut self, - _: &SelectToBeginning, - window: &mut Window, - cx: &mut Context, - ) { - let mut selection = self.selections.last::(cx); - selection.set_head(Point::zero(), SelectionGoal::None); - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(vec![selection]); - }); - } - - pub fn move_to_end(&mut self, _: &MoveToEnd, window: &mut Window, cx: &mut Context) { - if matches!(self.mode, EditorMode::SingleLine { .. }) { - cx.propagate(); - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - let cursor = self.buffer.read(cx).read(cx).len(); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select_ranges(vec![cursor..cursor]) - }); - } - - pub fn set_nav_history(&mut self, nav_history: Option) { - self.nav_history = nav_history; - } - - pub fn nav_history(&self) -> Option<&ItemNavHistory> { - self.nav_history.as_ref() - } - - pub fn create_nav_history_entry(&mut self, cx: &mut Context) { - self.push_to_nav_history(self.selections.newest_anchor().head(), None, false, cx); - } - - fn push_to_nav_history( - &mut self, - cursor_anchor: Anchor, - new_position: Option, - is_deactivate: bool, - cx: &mut Context, - ) { - if let Some(nav_history) = self.nav_history.as_mut() { - let buffer = self.buffer.read(cx).read(cx); - let cursor_position = cursor_anchor.to_point(&buffer); - let scroll_state = self.scroll_manager.anchor(); - let scroll_top_row = scroll_state.top_row(&buffer); - drop(buffer); - - if let Some(new_position) = new_position { - let row_delta = (new_position.row as i64 - cursor_position.row as i64).abs(); - if row_delta < MIN_NAVIGATION_HISTORY_ROW_DELTA { - return; - } - } - - nav_history.push( - Some(NavigationData { - cursor_anchor, - cursor_position, - scroll_anchor: scroll_state, - scroll_top_row, - }), - cx, - ); - cx.emit(EditorEvent::PushedToNavHistory { - anchor: cursor_anchor, - is_deactivate, - }) - } - } - - pub fn select_to_end(&mut self, _: &SelectToEnd, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - let buffer = self.buffer.read(cx).snapshot(cx); - let mut selection = self.selections.first::(cx); - selection.set_head(buffer.len(), SelectionGoal::None); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(vec![selection]); - }); - } - - pub fn select_all(&mut self, _: &SelectAll, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - let end = self.buffer.read(cx).read(cx).len(); - self.change_selections(None, window, cx, |s| { - s.select_ranges(vec![0..end]); - }); - } - - pub fn select_line(&mut self, _: &SelectLine, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.selections.all::(cx); - let max_point = display_map.buffer_snapshot.max_point(); - for selection in &mut selections { - let rows = selection.spanned_rows(true, &display_map); - selection.start = Point::new(rows.start.0, 0); - selection.end = cmp::min(max_point, Point::new(rows.end.0, 0)); - selection.reversed = false; - } - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(selections); - }); - } - - pub fn split_selection_into_lines( - &mut self, - _: &SplitSelectionIntoLines, - window: &mut Window, - cx: &mut Context, - ) { - let selections = self - .selections - .all::(cx) - .into_iter() - .map(|selection| selection.start..selection.end) - .collect::>(); - self.unfold_ranges(&selections, true, true, cx); - - let mut new_selection_ranges = Vec::new(); - { - let buffer = self.buffer.read(cx).read(cx); - for selection in selections { - for row in selection.start.row..selection.end.row { - let cursor = Point::new(row, buffer.line_len(MultiBufferRow(row))); - new_selection_ranges.push(cursor..cursor); - } - - let is_multiline_selection = selection.start.row != selection.end.row; - // Don't insert last one if it's a multi-line selection ending at the start of a line, - // so this action feels more ergonomic when paired with other selection operations - let should_skip_last = is_multiline_selection && selection.end.column == 0; - if !should_skip_last { - new_selection_ranges.push(selection.end..selection.end); - } - } - } - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select_ranges(new_selection_ranges); - }); - } - - pub fn add_selection_above( - &mut self, - _: &AddSelectionAbove, - window: &mut Window, - cx: &mut Context, - ) { - self.add_selection(true, window, cx); - } - - pub fn add_selection_below( - &mut self, - _: &AddSelectionBelow, - window: &mut Window, - cx: &mut Context, - ) { - self.add_selection(false, window, cx); - } - - fn add_selection(&mut self, above: bool, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.selections.all::(cx); - let text_layout_details = self.text_layout_details(window); - let mut state = self.add_selections_state.take().unwrap_or_else(|| { - let oldest_selection = selections.iter().min_by_key(|s| s.id).unwrap().clone(); - let range = oldest_selection.display_range(&display_map).sorted(); - - let start_x = display_map.x_for_display_point(range.start, &text_layout_details); - let end_x = display_map.x_for_display_point(range.end, &text_layout_details); - let positions = start_x.min(end_x)..start_x.max(end_x); - - selections.clear(); - let mut stack = Vec::new(); - for row in range.start.row().0..=range.end.row().0 { - if let Some(selection) = self.selections.build_columnar_selection( - &display_map, - DisplayRow(row), - &positions, - oldest_selection.reversed, - &text_layout_details, - ) { - stack.push(selection.id); - selections.push(selection); - } - } - - if above { - stack.reverse(); - } - - AddSelectionsState { above, stack } - }); - - let last_added_selection = *state.stack.last().unwrap(); - let mut new_selections = Vec::new(); - if above == state.above { - let end_row = if above { - DisplayRow(0) - } else { - display_map.max_point().row() - }; - - 'outer: for selection in selections { - if selection.id == last_added_selection { - let range = selection.display_range(&display_map).sorted(); - debug_assert_eq!(range.start.row(), range.end.row()); - let mut row = range.start.row(); - let positions = - if let SelectionGoal::HorizontalRange { start, end } = selection.goal { - px(start)..px(end) - } else { - let start_x = - display_map.x_for_display_point(range.start, &text_layout_details); - let end_x = - display_map.x_for_display_point(range.end, &text_layout_details); - start_x.min(end_x)..start_x.max(end_x) - }; - - while row != end_row { - if above { - row.0 -= 1; - } else { - row.0 += 1; - } - - if let Some(new_selection) = self.selections.build_columnar_selection( - &display_map, - row, - &positions, - selection.reversed, - &text_layout_details, - ) { - state.stack.push(new_selection.id); - if above { - new_selections.push(new_selection); - new_selections.push(selection); - } else { - new_selections.push(selection); - new_selections.push(new_selection); - } - - continue 'outer; - } - } - } - - new_selections.push(selection); - } - } else { - new_selections = selections; - new_selections.retain(|s| s.id != last_added_selection); - state.stack.pop(); - } - - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(new_selections); - }); - if state.stack.len() > 1 { - self.add_selections_state = Some(state); - } - } - - pub fn select_next_match_internal( - &mut self, - display_map: &DisplaySnapshot, - replace_newest: bool, - autoscroll: Option, - window: &mut Window, - cx: &mut Context, - ) -> Result<()> { - fn select_next_match_ranges( - this: &mut Editor, - range: Range, - reversed: bool, - replace_newest: bool, - auto_scroll: Option, - window: &mut Window, - cx: &mut Context, - ) { - this.unfold_ranges(&[range.clone()], false, auto_scroll.is_some(), cx); - this.change_selections(auto_scroll, window, cx, |s| { - if replace_newest { - s.delete(s.newest_anchor().id); - } - if reversed { - s.insert_range(range.end..range.start); - } else { - s.insert_range(range); - } - }); - } - - let buffer = &display_map.buffer_snapshot; - let mut selections = self.selections.all::(cx); - if let Some(mut select_next_state) = self.select_next_state.take() { - let query = &select_next_state.query; - if !select_next_state.done { - let first_selection = selections.iter().min_by_key(|s| s.id).unwrap(); - let last_selection = selections.iter().max_by_key(|s| s.id).unwrap(); - let mut next_selected_range = None; - - let bytes_after_last_selection = - buffer.bytes_in_range(last_selection.end..buffer.len()); - let bytes_before_first_selection = buffer.bytes_in_range(0..first_selection.start); - let query_matches = query - .stream_find_iter(bytes_after_last_selection) - .map(|result| (last_selection.end, result)) - .chain( - query - .stream_find_iter(bytes_before_first_selection) - .map(|result| (0, result)), - ); - - for (start_offset, query_match) in query_matches { - let query_match = query_match.unwrap(); // can only fail due to I/O - let offset_range = - start_offset + query_match.start()..start_offset + query_match.end(); - let display_range = offset_range.start.to_display_point(display_map) - ..offset_range.end.to_display_point(display_map); - - if !select_next_state.wordwise - || (!movement::is_inside_word(display_map, display_range.start) - && !movement::is_inside_word(display_map, display_range.end)) - { - // TODO: This is n^2, because we might check all the selections - if !selections - .iter() - .any(|selection| selection.range().overlaps(&offset_range)) - { - next_selected_range = Some(offset_range); - break; - } - } - } - - if let Some(next_selected_range) = next_selected_range { - select_next_match_ranges( - self, - next_selected_range, - last_selection.reversed, - replace_newest, - autoscroll, - window, - cx, - ); - } else { - select_next_state.done = true; - } - } - - self.select_next_state = Some(select_next_state); - } else { - let mut only_carets = true; - let mut same_text_selected = true; - let mut selected_text = None; - - let mut selections_iter = selections.iter().peekable(); - while let Some(selection) = selections_iter.next() { - if selection.start != selection.end { - only_carets = false; - } - - if same_text_selected { - if selected_text.is_none() { - selected_text = - Some(buffer.text_for_range(selection.range()).collect::()); - } - - if let Some(next_selection) = selections_iter.peek() { - if next_selection.range().len() == selection.range().len() { - let next_selected_text = buffer - .text_for_range(next_selection.range()) - .collect::(); - if Some(next_selected_text) != selected_text { - same_text_selected = false; - selected_text = None; - } - } else { - same_text_selected = false; - selected_text = None; - } - } - } - } - - if only_carets { - for selection in &mut selections { - let word_range = movement::surrounding_word( - display_map, - selection.start.to_display_point(display_map), - ); - selection.start = word_range.start.to_offset(display_map, Bias::Left); - selection.end = word_range.end.to_offset(display_map, Bias::Left); - selection.goal = SelectionGoal::None; - selection.reversed = false; - select_next_match_ranges( - self, - selection.start..selection.end, - selection.reversed, - replace_newest, - autoscroll, - window, - cx, - ); - } - - if selections.len() == 1 { - let selection = selections - .last() - .expect("ensured that there's only one selection"); - let query = buffer - .text_for_range(selection.start..selection.end) - .collect::(); - let is_empty = query.is_empty(); - let select_state = SelectNextState { - query: AhoCorasick::new(&[query])?, - wordwise: true, - done: is_empty, - }; - self.select_next_state = Some(select_state); - } else { - self.select_next_state = None; - } - } else if let Some(selected_text) = selected_text { - self.select_next_state = Some(SelectNextState { - query: AhoCorasick::new(&[selected_text])?, - wordwise: false, - done: false, - }); - self.select_next_match_internal( - display_map, - replace_newest, - autoscroll, - window, - cx, - )?; - } - } - Ok(()) - } - - pub fn select_all_matches( - &mut self, - _action: &SelectAllMatches, - window: &mut Window, - cx: &mut Context, - ) -> Result<()> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - - self.push_to_selection_history(); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - - self.select_next_match_internal(&display_map, false, None, window, cx)?; - let Some(select_next_state) = self.select_next_state.as_mut() else { - return Ok(()); - }; - if select_next_state.done { - return Ok(()); - } - - let mut new_selections = Vec::new(); - - let reversed = self.selections.oldest::(cx).reversed; - let buffer = &display_map.buffer_snapshot; - let query_matches = select_next_state - .query - .stream_find_iter(buffer.bytes_in_range(0..buffer.len())); - - for query_match in query_matches.into_iter() { - let query_match = query_match.context("query match for select all action")?; // can only fail due to I/O - let offset_range = if reversed { - query_match.end()..query_match.start() - } else { - query_match.start()..query_match.end() - }; - let display_range = offset_range.start.to_display_point(&display_map) - ..offset_range.end.to_display_point(&display_map); - - if !select_next_state.wordwise - || (!movement::is_inside_word(&display_map, display_range.start) - && !movement::is_inside_word(&display_map, display_range.end)) - { - new_selections.push(offset_range.start..offset_range.end); - } - } - - select_next_state.done = true; - self.unfold_ranges(&new_selections.clone(), false, false, cx); - self.change_selections(None, window, cx, |selections| { - selections.select_ranges(new_selections) - }); - - Ok(()) - } - - pub fn select_next( - &mut self, - action: &SelectNext, - window: &mut Window, - cx: &mut Context, - ) -> Result<()> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.push_to_selection_history(); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - self.select_next_match_internal( - &display_map, - action.replace_newest, - Some(Autoscroll::newest()), - window, - cx, - )?; - Ok(()) - } - - pub fn select_previous( - &mut self, - action: &SelectPrevious, - window: &mut Window, - cx: &mut Context, - ) -> Result<()> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.push_to_selection_history(); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = &display_map.buffer_snapshot; - let mut selections = self.selections.all::(cx); - if let Some(mut select_prev_state) = self.select_prev_state.take() { - let query = &select_prev_state.query; - if !select_prev_state.done { - let first_selection = selections.iter().min_by_key(|s| s.id).unwrap(); - let last_selection = selections.iter().max_by_key(|s| s.id).unwrap(); - let mut next_selected_range = None; - // When we're iterating matches backwards, the oldest match will actually be the furthest one in the buffer. - let bytes_before_last_selection = - buffer.reversed_bytes_in_range(0..last_selection.start); - let bytes_after_first_selection = - buffer.reversed_bytes_in_range(first_selection.end..buffer.len()); - let query_matches = query - .stream_find_iter(bytes_before_last_selection) - .map(|result| (last_selection.start, result)) - .chain( - query - .stream_find_iter(bytes_after_first_selection) - .map(|result| (buffer.len(), result)), - ); - for (end_offset, query_match) in query_matches { - let query_match = query_match.unwrap(); // can only fail due to I/O - let offset_range = - end_offset - query_match.end()..end_offset - query_match.start(); - let display_range = offset_range.start.to_display_point(&display_map) - ..offset_range.end.to_display_point(&display_map); - - if !select_prev_state.wordwise - || (!movement::is_inside_word(&display_map, display_range.start) - && !movement::is_inside_word(&display_map, display_range.end)) - { - next_selected_range = Some(offset_range); - break; - } - } - - if let Some(next_selected_range) = next_selected_range { - self.unfold_ranges(&[next_selected_range.clone()], false, true, cx); - self.change_selections(Some(Autoscroll::newest()), window, cx, |s| { - if action.replace_newest { - s.delete(s.newest_anchor().id); - } - if last_selection.reversed { - s.insert_range(next_selected_range.end..next_selected_range.start); - } else { - s.insert_range(next_selected_range); - } - }); - } else { - select_prev_state.done = true; - } - } - - self.select_prev_state = Some(select_prev_state); - } else { - let mut only_carets = true; - let mut same_text_selected = true; - let mut selected_text = None; - - let mut selections_iter = selections.iter().peekable(); - while let Some(selection) = selections_iter.next() { - if selection.start != selection.end { - only_carets = false; - } - - if same_text_selected { - if selected_text.is_none() { - selected_text = - Some(buffer.text_for_range(selection.range()).collect::()); - } - - if let Some(next_selection) = selections_iter.peek() { - if next_selection.range().len() == selection.range().len() { - let next_selected_text = buffer - .text_for_range(next_selection.range()) - .collect::(); - if Some(next_selected_text) != selected_text { - same_text_selected = false; - selected_text = None; - } - } else { - same_text_selected = false; - selected_text = None; - } - } - } - } - - if only_carets { - for selection in &mut selections { - let word_range = movement::surrounding_word( - &display_map, - selection.start.to_display_point(&display_map), - ); - selection.start = word_range.start.to_offset(&display_map, Bias::Left); - selection.end = word_range.end.to_offset(&display_map, Bias::Left); - selection.goal = SelectionGoal::None; - selection.reversed = false; - } - if selections.len() == 1 { - let selection = selections - .last() - .expect("ensured that there's only one selection"); - let query = buffer - .text_for_range(selection.start..selection.end) - .collect::(); - let is_empty = query.is_empty(); - let select_state = SelectNextState { - query: AhoCorasick::new(&[query.chars().rev().collect::()])?, - wordwise: true, - done: is_empty, - }; - self.select_prev_state = Some(select_state); - } else { - self.select_prev_state = None; - } - - self.unfold_ranges( - &selections.iter().map(|s| s.range()).collect::>(), - false, - true, - cx, - ); - self.change_selections(Some(Autoscroll::newest()), window, cx, |s| { - s.select(selections); - }); - } else if let Some(selected_text) = selected_text { - self.select_prev_state = Some(SelectNextState { - query: AhoCorasick::new(&[selected_text.chars().rev().collect::()])?, - wordwise: false, - done: false, - }); - self.select_previous(action, window, cx)?; - } - } - Ok(()) - } - - pub fn find_next_match( - &mut self, - _: &FindNextMatch, - window: &mut Window, - cx: &mut Context, - ) -> Result<()> { - let selections = self.selections.disjoint_anchors(); - match selections.first() { - Some(first) if selections.len() >= 2 => { - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select_ranges([first.range()]); - }); - } - _ => self.select_next( - &SelectNext { - replace_newest: true, - }, - window, - cx, - )?, - } - Ok(()) - } - - pub fn find_previous_match( - &mut self, - _: &FindPreviousMatch, - window: &mut Window, - cx: &mut Context, - ) -> Result<()> { - let selections = self.selections.disjoint_anchors(); - match selections.last() { - Some(last) if selections.len() >= 2 => { - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select_ranges([last.range()]); - }); - } - _ => self.select_previous( - &SelectPrevious { - replace_newest: true, - }, - window, - cx, - )?, - } - Ok(()) - } - - pub fn toggle_comments( - &mut self, - action: &ToggleComments, - window: &mut Window, - cx: &mut Context, - ) { - if self.read_only(cx) { - return; - } - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - let text_layout_details = &self.text_layout_details(window); - self.transact(window, cx, |this, window, cx| { - let mut selections = this.selections.all::(cx); - let mut edits = Vec::new(); - let mut selection_edit_ranges = Vec::new(); - let mut last_toggled_row = None; - let snapshot = this.buffer.read(cx).read(cx); - let empty_str: Arc = Arc::default(); - let mut suffixes_inserted = Vec::new(); - let ignore_indent = action.ignore_indent; - - fn comment_prefix_range( - snapshot: &MultiBufferSnapshot, - row: MultiBufferRow, - comment_prefix: &str, - comment_prefix_whitespace: &str, - ignore_indent: bool, - ) -> Range { - let indent_size = if ignore_indent { - 0 - } else { - snapshot.indent_size_for_line(row).len - }; - - let start = Point::new(row.0, indent_size); - - let mut line_bytes = snapshot - .bytes_in_range(start..snapshot.max_point()) - .flatten() - .copied(); - - // If this line currently begins with the line comment prefix, then record - // the range containing the prefix. - if line_bytes - .by_ref() - .take(comment_prefix.len()) - .eq(comment_prefix.bytes()) - { - // Include any whitespace that matches the comment prefix. - let matching_whitespace_len = line_bytes - .zip(comment_prefix_whitespace.bytes()) - .take_while(|(a, b)| a == b) - .count() as u32; - let end = Point::new( - start.row, - start.column + comment_prefix.len() as u32 + matching_whitespace_len, - ); - start..end - } else { - start..start - } - } - - fn comment_suffix_range( - snapshot: &MultiBufferSnapshot, - row: MultiBufferRow, - comment_suffix: &str, - comment_suffix_has_leading_space: bool, - ) -> Range { - let end = Point::new(row.0, snapshot.line_len(row)); - let suffix_start_column = end.column.saturating_sub(comment_suffix.len() as u32); - - let mut line_end_bytes = snapshot - .bytes_in_range(Point::new(end.row, suffix_start_column.saturating_sub(1))..end) - .flatten() - .copied(); - - let leading_space_len = if suffix_start_column > 0 - && line_end_bytes.next() == Some(b' ') - && comment_suffix_has_leading_space - { - 1 - } else { - 0 - }; - - // If this line currently begins with the line comment prefix, then record - // the range containing the prefix. - if line_end_bytes.by_ref().eq(comment_suffix.bytes()) { - let start = Point::new(end.row, suffix_start_column - leading_space_len); - start..end - } else { - end..end - } - } - - // TODO: Handle selections that cross excerpts - for selection in &mut selections { - let start_column = snapshot - .indent_size_for_line(MultiBufferRow(selection.start.row)) - .len; - let language = if let Some(language) = - snapshot.language_scope_at(Point::new(selection.start.row, start_column)) - { - language - } else { - continue; - }; - - selection_edit_ranges.clear(); - - // If multiple selections contain a given row, avoid processing that - // row more than once. - let mut start_row = MultiBufferRow(selection.start.row); - if last_toggled_row == Some(start_row) { - start_row = start_row.next_row(); - } - let end_row = - if selection.end.row > selection.start.row && selection.end.column == 0 { - MultiBufferRow(selection.end.row - 1) - } else { - MultiBufferRow(selection.end.row) - }; - last_toggled_row = Some(end_row); - - if start_row > end_row { - continue; - } - - // If the language has line comments, toggle those. - let mut full_comment_prefixes = language.line_comment_prefixes().to_vec(); - - // If ignore_indent is set, trim spaces from the right side of all full_comment_prefixes - if ignore_indent { - full_comment_prefixes = full_comment_prefixes - .into_iter() - .map(|s| Arc::from(s.trim_end())) - .collect(); - } - - if !full_comment_prefixes.is_empty() { - let first_prefix = full_comment_prefixes - .first() - .expect("prefixes is non-empty"); - let prefix_trimmed_lengths = full_comment_prefixes - .iter() - .map(|p| p.trim_end_matches(' ').len()) - .collect::>(); - - let mut all_selection_lines_are_comments = true; - - for row in start_row.0..=end_row.0 { - let row = MultiBufferRow(row); - if start_row < end_row && snapshot.is_line_blank(row) { - continue; - } - - let prefix_range = full_comment_prefixes - .iter() - .zip(prefix_trimmed_lengths.iter().copied()) - .map(|(prefix, trimmed_prefix_len)| { - comment_prefix_range( - snapshot.deref(), - row, - &prefix[..trimmed_prefix_len], - &prefix[trimmed_prefix_len..], - ignore_indent, - ) - }) - .max_by_key(|range| range.end.column - range.start.column) - .expect("prefixes is non-empty"); - - if prefix_range.is_empty() { - all_selection_lines_are_comments = false; - } - - selection_edit_ranges.push(prefix_range); - } - - if all_selection_lines_are_comments { - edits.extend( - selection_edit_ranges - .iter() - .cloned() - .map(|range| (range, empty_str.clone())), - ); - } else { - let min_column = selection_edit_ranges - .iter() - .map(|range| range.start.column) - .min() - .unwrap_or(0); - edits.extend(selection_edit_ranges.iter().map(|range| { - let position = Point::new(range.start.row, min_column); - (position..position, first_prefix.clone()) - })); - } - } else if let Some((full_comment_prefix, comment_suffix)) = - language.block_comment_delimiters() - { - let comment_prefix = full_comment_prefix.trim_end_matches(' '); - let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..]; - let prefix_range = comment_prefix_range( - snapshot.deref(), - start_row, - comment_prefix, - comment_prefix_whitespace, - ignore_indent, - ); - let suffix_range = comment_suffix_range( - snapshot.deref(), - end_row, - comment_suffix.trim_start_matches(' '), - comment_suffix.starts_with(' '), - ); - - if prefix_range.is_empty() || suffix_range.is_empty() { - edits.push(( - prefix_range.start..prefix_range.start, - full_comment_prefix.clone(), - )); - edits.push((suffix_range.end..suffix_range.end, comment_suffix.clone())); - suffixes_inserted.push((end_row, comment_suffix.len())); - } else { - edits.push((prefix_range, empty_str.clone())); - edits.push((suffix_range, empty_str.clone())); - } - } else { - continue; - } - } - - drop(snapshot); - this.buffer.update(cx, |buffer, cx| { - buffer.edit(edits, None, cx); - }); - - // Adjust selections so that they end before any comment suffixes that - // were inserted. - let mut suffixes_inserted = suffixes_inserted.into_iter().peekable(); - let mut selections = this.selections.all::(cx); - let snapshot = this.buffer.read(cx).read(cx); - for selection in &mut selections { - while let Some((row, suffix_len)) = suffixes_inserted.peek().copied() { - match row.cmp(&MultiBufferRow(selection.end.row)) { - Ordering::Less => { - suffixes_inserted.next(); - continue; - } - Ordering::Greater => break, - Ordering::Equal => { - if selection.end.column == snapshot.line_len(row) { - if selection.is_empty() { - selection.start.column -= suffix_len as u32; - } - selection.end.column -= suffix_len as u32; - } - break; - } - } - } - } - - drop(snapshot); - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(selections) - }); - - let selections = this.selections.all::(cx); - let selections_on_single_row = selections.windows(2).all(|selections| { - selections[0].start.row == selections[1].start.row - && selections[0].end.row == selections[1].end.row - && selections[0].start.row == selections[0].end.row - }); - let selections_selecting = selections - .iter() - .any(|selection| selection.start != selection.end); - let advance_downwards = action.advance_downwards - && selections_on_single_row - && !selections_selecting - && !matches!(this.mode, EditorMode::SingleLine { .. }); - - if advance_downwards { - let snapshot = this.buffer.read(cx).snapshot(cx); - - this.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_cursors_with(|display_snapshot, display_point, _| { - let mut point = display_point.to_point(display_snapshot); - point.row += 1; - point = snapshot.clip_point(point, Bias::Left); - let display_point = point.to_display_point(display_snapshot); - let goal = SelectionGoal::HorizontalPosition( - display_snapshot - .x_for_display_point(display_point, text_layout_details) - .into(), - ); - (display_point, goal) - }) - }); - } - }); - } - - pub fn select_enclosing_symbol( - &mut self, - _: &SelectEnclosingSymbol, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - - let buffer = self.buffer.read(cx).snapshot(cx); - let old_selections = self.selections.all::(cx).into_boxed_slice(); - - fn update_selection( - selection: &Selection, - buffer_snap: &MultiBufferSnapshot, - ) -> Option> { - let cursor = selection.head(); - let (_buffer_id, symbols) = buffer_snap.symbols_containing(cursor, None)?; - for symbol in symbols.iter().rev() { - let start = symbol.range.start.to_offset(buffer_snap); - let end = symbol.range.end.to_offset(buffer_snap); - let new_range = start..end; - if start < selection.start || end > selection.end { - return Some(Selection { - id: selection.id, - start: new_range.start, - end: new_range.end, - goal: SelectionGoal::None, - reversed: selection.reversed, - }); - } - } - None - } - - let mut selected_larger_symbol = false; - let new_selections = old_selections - .iter() - .map(|selection| match update_selection(selection, &buffer) { - Some(new_selection) => { - if new_selection.range() != selection.range() { - selected_larger_symbol = true; - } - new_selection - } - None => selection.clone(), - }) - .collect::>(); - - if selected_larger_symbol { - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select(new_selections); - }); - } - } - - pub fn select_larger_syntax_node( - &mut self, - _: &SelectLargerSyntaxNode, - window: &mut Window, - cx: &mut Context, - ) { - let Some(visible_row_count) = self.visible_row_count() else { - return; - }; - let old_selections: Box<[_]> = self.selections.all::(cx).into(); - if old_selections.is_empty() { - return; - } - - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = self.buffer.read(cx).snapshot(cx); - - let mut selected_larger_node = false; - let mut new_selections = old_selections - .iter() - .map(|selection| { - let old_range = selection.start..selection.end; - - if let Some((node, _)) = buffer.syntax_ancestor(old_range.clone()) { - // manually select word at selection - if ["string_content", "inline"].contains(&node.kind()) { - let word_range = { - let display_point = buffer - .offset_to_point(old_range.start) - .to_display_point(&display_map); - let Range { start, end } = - movement::surrounding_word(&display_map, display_point); - start.to_point(&display_map).to_offset(&buffer) - ..end.to_point(&display_map).to_offset(&buffer) - }; - // ignore if word is already selected - if !word_range.is_empty() && old_range != word_range { - let last_word_range = { - let display_point = buffer - .offset_to_point(old_range.end) - .to_display_point(&display_map); - let Range { start, end } = - movement::surrounding_word(&display_map, display_point); - start.to_point(&display_map).to_offset(&buffer) - ..end.to_point(&display_map).to_offset(&buffer) - }; - // only select word if start and end point belongs to same word - if word_range == last_word_range { - selected_larger_node = true; - return Selection { - id: selection.id, - start: word_range.start, - end: word_range.end, - goal: SelectionGoal::None, - reversed: selection.reversed, - }; - } - } - } - } - - let mut new_range = old_range.clone(); - let mut new_node = None; - while let Some((node, containing_range)) = buffer.syntax_ancestor(new_range.clone()) - { - new_node = Some(node); - new_range = match containing_range { - MultiOrSingleBufferOffsetRange::Single(_) => break, - MultiOrSingleBufferOffsetRange::Multi(range) => range, - }; - if !display_map.intersects_fold(new_range.start) - && !display_map.intersects_fold(new_range.end) - { - break; - } - } - - if let Some(node) = new_node { - // Log the ancestor, to support using this action as a way to explore TreeSitter - // nodes. Parent and grandparent are also logged because this operation will not - // visit nodes that have the same range as their parent. - log::info!("Node: {node:?}"); - let parent = node.parent(); - log::info!("Parent: {parent:?}"); - let grandparent = parent.and_then(|x| x.parent()); - log::info!("Grandparent: {grandparent:?}"); - } - - selected_larger_node |= new_range != old_range; - Selection { - id: selection.id, - start: new_range.start, - end: new_range.end, - goal: SelectionGoal::None, - reversed: selection.reversed, - } - }) - .collect::>(); - - if !selected_larger_node { - return; // don't put this call in the history - } - - // scroll based on transformation done to the last selection created by the user - let (last_old, last_new) = old_selections - .last() - .zip(new_selections.last().cloned()) - .expect("old_selections isn't empty"); - - // revert selection - let is_selection_reversed = { - let should_newest_selection_be_reversed = last_old.start != last_new.start; - new_selections.last_mut().expect("checked above").reversed = - should_newest_selection_be_reversed; - should_newest_selection_be_reversed - }; - - if selected_larger_node { - self.select_syntax_node_history.disable_clearing = true; - self.change_selections(None, window, cx, |s| { - s.select(new_selections.clone()); - }); - self.select_syntax_node_history.disable_clearing = false; - } - - let start_row = last_new.start.to_display_point(&display_map).row().0; - let end_row = last_new.end.to_display_point(&display_map).row().0; - let selection_height = end_row - start_row + 1; - let scroll_margin_rows = self.vertical_scroll_margin() as u32; - - let fits_on_the_screen = visible_row_count >= selection_height + scroll_margin_rows * 2; - let scroll_behavior = if fits_on_the_screen { - self.request_autoscroll(Autoscroll::fit(), cx); - SelectSyntaxNodeScrollBehavior::FitSelection - } else if is_selection_reversed { - self.scroll_cursor_top(&ScrollCursorTop, window, cx); - SelectSyntaxNodeScrollBehavior::CursorTop - } else { - self.scroll_cursor_bottom(&ScrollCursorBottom, window, cx); - SelectSyntaxNodeScrollBehavior::CursorBottom - }; - - self.select_syntax_node_history.push(( - old_selections, - scroll_behavior, - is_selection_reversed, - )); - } - - pub fn select_smaller_syntax_node( - &mut self, - _: &SelectSmallerSyntaxNode, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - - if let Some((mut selections, scroll_behavior, is_selection_reversed)) = - self.select_syntax_node_history.pop() - { - if let Some(selection) = selections.last_mut() { - selection.reversed = is_selection_reversed; - } - - self.select_syntax_node_history.disable_clearing = true; - self.change_selections(None, window, cx, |s| { - s.select(selections.to_vec()); - }); - self.select_syntax_node_history.disable_clearing = false; - - match scroll_behavior { - SelectSyntaxNodeScrollBehavior::CursorTop => { - self.scroll_cursor_top(&ScrollCursorTop, window, cx); - } - SelectSyntaxNodeScrollBehavior::FitSelection => { - self.request_autoscroll(Autoscroll::fit(), cx); - } - SelectSyntaxNodeScrollBehavior::CursorBottom => { - self.scroll_cursor_bottom(&ScrollCursorBottom, window, cx); - } - } - } - } - - fn refresh_runnables(&mut self, window: &mut Window, cx: &mut Context) -> Task<()> { - if !EditorSettings::get_global(cx).gutter.runnables { - self.clear_tasks(); - return Task::ready(()); - } - let project = self.project.as_ref().map(Entity::downgrade); - let task_sources = self.lsp_task_sources(cx); - cx.spawn_in(window, async move |editor, cx| { - cx.background_executor().timer(UPDATE_DEBOUNCE).await; - let Some(project) = project.and_then(|p| p.upgrade()) else { - return; - }; - let Ok(display_snapshot) = editor.update(cx, |this, cx| { - this.display_map.update(cx, |map, cx| map.snapshot(cx)) - }) else { - return; - }; - - let hide_runnables = project - .update(cx, |project, cx| { - // Do not display any test indicators in non-dev server remote projects. - project.is_via_collab() && project.ssh_connection_string(cx).is_none() - }) - .unwrap_or(true); - if hide_runnables { - return; - } - let new_rows = - cx.background_spawn({ - let snapshot = display_snapshot.clone(); - async move { - Self::fetch_runnable_ranges(&snapshot, Anchor::min()..Anchor::max()) - } - }) - .await; - let Ok(lsp_tasks) = - cx.update(|_, cx| crate::lsp_tasks(project.clone(), &task_sources, None, cx)) - else { - return; - }; - let lsp_tasks = lsp_tasks.await; - - let Ok(mut lsp_tasks_by_rows) = cx.update(|_, cx| { - lsp_tasks - .into_iter() - .flat_map(|(kind, tasks)| { - tasks.into_iter().filter_map(move |(location, task)| { - Some((kind.clone(), location?, task)) - }) - }) - .fold(HashMap::default(), |mut acc, (kind, location, task)| { - let buffer = location.target.buffer; - let buffer_snapshot = buffer.read(cx).snapshot(); - let offset = display_snapshot.buffer_snapshot.excerpts().find_map( - |(excerpt_id, snapshot, _)| { - if snapshot.remote_id() == buffer_snapshot.remote_id() { - display_snapshot - .buffer_snapshot - .anchor_in_excerpt(excerpt_id, location.target.range.start) - } else { - None - } - }, - ); - if let Some(offset) = offset { - let task_buffer_range = - location.target.range.to_point(&buffer_snapshot); - let context_buffer_range = - task_buffer_range.to_offset(&buffer_snapshot); - let context_range = BufferOffset(context_buffer_range.start) - ..BufferOffset(context_buffer_range.end); - - acc.entry((buffer_snapshot.remote_id(), task_buffer_range.start.row)) - .or_insert_with(|| RunnableTasks { - templates: Vec::new(), - offset, - column: task_buffer_range.start.column, - extra_variables: HashMap::default(), - context_range, - }) - .templates - .push((kind, task.original_task().clone())); - } - - acc - }) - }) else { - return; - }; - - let rows = Self::runnable_rows(project, display_snapshot, new_rows, cx.clone()); - editor - .update(cx, |editor, _| { - editor.clear_tasks(); - for (key, mut value) in rows { - if let Some(lsp_tasks) = lsp_tasks_by_rows.remove(&key) { - value.templates.extend(lsp_tasks.templates); - } - - editor.insert_tasks(key, value); - } - for (key, value) in lsp_tasks_by_rows { - editor.insert_tasks(key, value); - } - }) - .ok(); - }) - } - fn fetch_runnable_ranges( - snapshot: &DisplaySnapshot, - range: Range, - ) -> Vec { - snapshot.buffer_snapshot.runnable_ranges(range).collect() - } - - fn runnable_rows( - project: Entity, - snapshot: DisplaySnapshot, - runnable_ranges: Vec, - mut cx: AsyncWindowContext, - ) -> Vec<((BufferId, BufferRow), RunnableTasks)> { - runnable_ranges - .into_iter() - .filter_map(|mut runnable| { - let tasks = cx - .update(|_, cx| Self::templates_with_tags(&project, &mut runnable.runnable, cx)) - .ok()?; - if tasks.is_empty() { - return None; - } - - let point = runnable.run_range.start.to_point(&snapshot.buffer_snapshot); - - let row = snapshot - .buffer_snapshot - .buffer_line_for_row(MultiBufferRow(point.row))? - .1 - .start - .row; - - let context_range = - BufferOffset(runnable.full_range.start)..BufferOffset(runnable.full_range.end); - Some(( - (runnable.buffer_id, row), - RunnableTasks { - templates: tasks, - offset: snapshot - .buffer_snapshot - .anchor_before(runnable.run_range.start), - context_range, - column: point.column, - extra_variables: runnable.extra_captures, - }, - )) - }) - .collect() - } - - fn templates_with_tags( - project: &Entity, - runnable: &mut Runnable, - cx: &mut App, - ) -> Vec<(TaskSourceKind, TaskTemplate)> { - let (inventory, worktree_id, file) = project.read_with(cx, |project, cx| { - let (worktree_id, file) = project - .buffer_for_id(runnable.buffer, cx) - .and_then(|buffer| buffer.read(cx).file()) - .map(|file| (file.worktree_id(cx), file.clone())) - .unzip(); - - ( - project.task_store().read(cx).task_inventory().cloned(), - worktree_id, - file, - ) - }); - - let mut templates_with_tags = mem::take(&mut runnable.tags) - .into_iter() - .flat_map(|RunnableTag(tag)| { - inventory - .as_ref() - .into_iter() - .flat_map(|inventory| { - inventory.read(cx).list_tasks( - file.clone(), - Some(runnable.language.clone()), - worktree_id, - cx, - ) - }) - .filter(move |(_, template)| { - template.tags.iter().any(|source_tag| source_tag == &tag) - }) - }) - .sorted_by_key(|(kind, _)| kind.to_owned()) - .collect::>(); - if let Some((leading_tag_source, _)) = templates_with_tags.first() { - // Strongest source wins; if we have worktree tag binding, prefer that to - // global and language bindings; - // if we have a global binding, prefer that to language binding. - let first_mismatch = templates_with_tags - .iter() - .position(|(tag_source, _)| tag_source != leading_tag_source); - if let Some(index) = first_mismatch { - templates_with_tags.truncate(index); - } - } - - templates_with_tags - } - - pub fn move_to_enclosing_bracket( - &mut self, - _: &MoveToEnclosingBracket, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.move_offsets_with(|snapshot, selection| { - let Some(enclosing_bracket_ranges) = - snapshot.enclosing_bracket_ranges(selection.start..selection.end) - else { - return; - }; - - let mut best_length = usize::MAX; - let mut best_inside = false; - let mut best_in_bracket_range = false; - let mut best_destination = None; - for (open, close) in enclosing_bracket_ranges { - let close = close.to_inclusive(); - let length = close.end() - open.start; - let inside = selection.start >= open.end && selection.end <= *close.start(); - let in_bracket_range = open.to_inclusive().contains(&selection.head()) - || close.contains(&selection.head()); - - // If best is next to a bracket and current isn't, skip - if !in_bracket_range && best_in_bracket_range { - continue; - } - - // Prefer smaller lengths unless best is inside and current isn't - if length > best_length && (best_inside || !inside) { - continue; - } - - best_length = length; - best_inside = inside; - best_in_bracket_range = in_bracket_range; - best_destination = Some( - if close.contains(&selection.start) && close.contains(&selection.end) { - if inside { open.end } else { open.start } - } else if inside { - *close.start() - } else { - *close.end() - }, - ); - } - - if let Some(destination) = best_destination { - selection.collapse_to(destination, SelectionGoal::None); - } - }) - }); - } - - pub fn undo_selection( - &mut self, - _: &UndoSelection, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.end_selection(window, cx); - self.selection_history.mode = SelectionHistoryMode::Undoing; - if let Some(entry) = self.selection_history.undo_stack.pop_back() { - self.change_selections(None, window, cx, |s| { - s.select_anchors(entry.selections.to_vec()) - }); - self.select_next_state = entry.select_next_state; - self.select_prev_state = entry.select_prev_state; - self.add_selections_state = entry.add_selections_state; - self.request_autoscroll(Autoscroll::newest(), cx); - } - self.selection_history.mode = SelectionHistoryMode::Normal; - } - - pub fn redo_selection( - &mut self, - _: &RedoSelection, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.end_selection(window, cx); - self.selection_history.mode = SelectionHistoryMode::Redoing; - if let Some(entry) = self.selection_history.redo_stack.pop_back() { - self.change_selections(None, window, cx, |s| { - s.select_anchors(entry.selections.to_vec()) - }); - self.select_next_state = entry.select_next_state; - self.select_prev_state = entry.select_prev_state; - self.add_selections_state = entry.add_selections_state; - self.request_autoscroll(Autoscroll::newest(), cx); - } - self.selection_history.mode = SelectionHistoryMode::Normal; - } - - pub fn expand_excerpts( - &mut self, - action: &ExpandExcerpts, - _: &mut Window, - cx: &mut Context, - ) { - self.expand_excerpts_for_direction(action.lines, ExpandExcerptDirection::UpAndDown, cx) - } - - pub fn expand_excerpts_down( - &mut self, - action: &ExpandExcerptsDown, - _: &mut Window, - cx: &mut Context, - ) { - self.expand_excerpts_for_direction(action.lines, ExpandExcerptDirection::Down, cx) - } - - pub fn expand_excerpts_up( - &mut self, - action: &ExpandExcerptsUp, - _: &mut Window, - cx: &mut Context, - ) { - self.expand_excerpts_for_direction(action.lines, ExpandExcerptDirection::Up, cx) - } - - pub fn expand_excerpts_for_direction( - &mut self, - lines: u32, - direction: ExpandExcerptDirection, - - cx: &mut Context, - ) { - let selections = self.selections.disjoint_anchors(); - - let lines = if lines == 0 { - EditorSettings::get_global(cx).expand_excerpt_lines - } else { - lines - }; - - self.buffer.update(cx, |buffer, cx| { - let snapshot = buffer.snapshot(cx); - let mut excerpt_ids = selections - .iter() - .flat_map(|selection| snapshot.excerpt_ids_for_range(selection.range())) - .collect::>(); - excerpt_ids.sort(); - excerpt_ids.dedup(); - buffer.expand_excerpts(excerpt_ids, lines, direction, cx) - }) - } - - pub fn expand_excerpt( - &mut self, - excerpt: ExcerptId, - direction: ExpandExcerptDirection, - window: &mut Window, - cx: &mut Context, - ) { - let current_scroll_position = self.scroll_position(cx); - let lines_to_expand = EditorSettings::get_global(cx).expand_excerpt_lines; - let mut should_scroll_up = false; - - if direction == ExpandExcerptDirection::Down { - let multi_buffer = self.buffer.read(cx); - let snapshot = multi_buffer.snapshot(cx); - if let Some(buffer_id) = snapshot.buffer_id_for_excerpt(excerpt) { - if let Some(buffer) = multi_buffer.buffer(buffer_id) { - if let Some(excerpt_range) = snapshot.buffer_range_for_excerpt(excerpt) { - let buffer_snapshot = buffer.read(cx).snapshot(); - let excerpt_end_row = - Point::from_anchor(&excerpt_range.end, &buffer_snapshot).row; - let last_row = buffer_snapshot.max_point().row; - let lines_below = last_row.saturating_sub(excerpt_end_row); - should_scroll_up = lines_below >= lines_to_expand; - } - } - } - } - - self.buffer.update(cx, |buffer, cx| { - buffer.expand_excerpts([excerpt], lines_to_expand, direction, cx) - }); - - if should_scroll_up { - let new_scroll_position = - current_scroll_position + gpui::Point::new(0.0, lines_to_expand as f32); - self.set_scroll_position(new_scroll_position, window, cx); - } - } - - pub fn go_to_singleton_buffer_point( - &mut self, - point: Point, - window: &mut Window, - cx: &mut Context, - ) { - self.go_to_singleton_buffer_range(point..point, window, cx); - } - - pub fn go_to_singleton_buffer_range( - &mut self, - range: Range, - window: &mut Window, - cx: &mut Context, - ) { - let multibuffer = self.buffer().read(cx); - let Some(buffer) = multibuffer.as_singleton() else { - return; - }; - let Some(start) = multibuffer.buffer_point_to_anchor(&buffer, range.start, cx) else { - return; - }; - let Some(end) = multibuffer.buffer_point_to_anchor(&buffer, range.end, cx) else { - return; - }; - self.change_selections(Some(Autoscroll::center()), window, cx, |s| { - s.select_anchor_ranges([start..end]) - }); - } - - pub fn go_to_diagnostic( - &mut self, - _: &GoToDiagnostic, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.go_to_diagnostic_impl(Direction::Next, window, cx) - } - - pub fn go_to_prev_diagnostic( - &mut self, - _: &GoToPreviousDiagnostic, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - self.go_to_diagnostic_impl(Direction::Prev, window, cx) - } - - pub fn go_to_diagnostic_impl( - &mut self, - direction: Direction, - window: &mut Window, - cx: &mut Context, - ) { - let buffer = self.buffer.read(cx).snapshot(cx); - let selection = self.selections.newest::(cx); - - let mut active_group_id = None; - if let ActiveDiagnostic::Group(active_group) = &self.active_diagnostics { - if active_group.active_range.start.to_offset(&buffer) == selection.start { - active_group_id = Some(active_group.group_id); - } - } - - fn filtered( - snapshot: EditorSnapshot, - diagnostics: impl Iterator>, - ) -> impl Iterator> { - diagnostics - .filter(|entry| entry.range.start != entry.range.end) - .filter(|entry| !entry.diagnostic.is_unnecessary) - .filter(move |entry| !snapshot.intersects_fold(entry.range.start)) - } - - let snapshot = self.snapshot(window, cx); - let before = filtered( - snapshot.clone(), - buffer - .diagnostics_in_range(0..selection.start) - .filter(|entry| entry.range.start <= selection.start), - ); - let after = filtered( - snapshot, - buffer - .diagnostics_in_range(selection.start..buffer.len()) - .filter(|entry| entry.range.start >= selection.start), - ); - - let mut found: Option> = None; - if direction == Direction::Prev { - 'outer: for prev_diagnostics in [before.collect::>(), after.collect::>()] - { - for diagnostic in prev_diagnostics.into_iter().rev() { - if diagnostic.range.start != selection.start - || active_group_id - .is_some_and(|active| diagnostic.diagnostic.group_id < active) - { - found = Some(diagnostic); - break 'outer; - } - } - } - } else { - for diagnostic in after.chain(before) { - if diagnostic.range.start != selection.start - || active_group_id.is_some_and(|active| diagnostic.diagnostic.group_id > active) - { - found = Some(diagnostic); - break; - } - } - } - let Some(next_diagnostic) = found else { - return; - }; - - let Some(buffer_id) = buffer.anchor_after(next_diagnostic.range.start).buffer_id else { - return; - }; - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select_ranges(vec![ - next_diagnostic.range.start..next_diagnostic.range.start, - ]) - }); - self.activate_diagnostics(buffer_id, next_diagnostic, window, cx); - self.refresh_inline_completion(false, true, window, cx); - } - - fn go_to_next_hunk(&mut self, _: &GoToHunk, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - let snapshot = self.snapshot(window, cx); - let selection = self.selections.newest::(cx); - self.go_to_hunk_before_or_after_position( - &snapshot, - selection.head(), - Direction::Next, - window, - cx, - ); - } - - pub fn go_to_hunk_before_or_after_position( - &mut self, - snapshot: &EditorSnapshot, - position: Point, - direction: Direction, - window: &mut Window, - cx: &mut Context, - ) { - let row = if direction == Direction::Next { - self.hunk_after_position(snapshot, position) - .map(|hunk| hunk.row_range.start) - } else { - self.hunk_before_position(snapshot, position) - }; - - if let Some(row) = row { - let destination = Point::new(row.0, 0); - let autoscroll = Autoscroll::center(); - - self.unfold_ranges(&[destination..destination], false, false, cx); - self.change_selections(Some(autoscroll), window, cx, |s| { - s.select_ranges([destination..destination]); - }); - } - } - - fn hunk_after_position( - &mut self, - snapshot: &EditorSnapshot, - position: Point, - ) -> Option { - snapshot - .buffer_snapshot - .diff_hunks_in_range(position..snapshot.buffer_snapshot.max_point()) - .find(|hunk| hunk.row_range.start.0 > position.row) - .or_else(|| { - snapshot - .buffer_snapshot - .diff_hunks_in_range(Point::zero()..position) - .find(|hunk| hunk.row_range.end.0 < position.row) - }) - } - - fn go_to_prev_hunk( - &mut self, - _: &GoToPreviousHunk, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::MovementAction); - let snapshot = self.snapshot(window, cx); - let selection = self.selections.newest::(cx); - self.go_to_hunk_before_or_after_position( - &snapshot, - selection.head(), - Direction::Prev, - window, - cx, - ); - } - - fn hunk_before_position( - &mut self, - snapshot: &EditorSnapshot, - position: Point, - ) -> Option { - snapshot - .buffer_snapshot - .diff_hunk_before(position) - .or_else(|| snapshot.buffer_snapshot.diff_hunk_before(Point::MAX)) - } - - fn go_to_next_change( - &mut self, - _: &GoToNextChange, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(selections) = self - .change_list - .next_change(1, Direction::Next) - .map(|s| s.to_vec()) - { - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - let map = s.display_map(); - s.select_display_ranges(selections.iter().map(|a| { - let point = a.to_display_point(&map); - point..point - })) - }) - } - } - - fn go_to_previous_change( - &mut self, - _: &GoToPreviousChange, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(selections) = self - .change_list - .next_change(1, Direction::Prev) - .map(|s| s.to_vec()) - { - self.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - let map = s.display_map(); - s.select_display_ranges(selections.iter().map(|a| { - let point = a.to_display_point(&map); - point..point - })) - }) - } - } - - fn go_to_line( - &mut self, - position: Anchor, - highlight_color: Option, - window: &mut Window, - cx: &mut Context, - ) { - let snapshot = self.snapshot(window, cx).display_snapshot; - let position = position.to_point(&snapshot.buffer_snapshot); - let start = snapshot - .buffer_snapshot - .clip_point(Point::new(position.row, 0), Bias::Left); - let end = start + Point::new(1, 0); - let start = snapshot.buffer_snapshot.anchor_before(start); - let end = snapshot.buffer_snapshot.anchor_before(end); - - self.highlight_rows::( - start..end, - highlight_color - .unwrap_or_else(|| cx.theme().colors().editor_highlighted_line_background), - Default::default(), - cx, - ); - self.request_autoscroll(Autoscroll::center().for_anchor(start), cx); - } - - pub fn go_to_definition( - &mut self, - _: &GoToDefinition, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - let definition = - self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, false, window, cx); - let fallback_strategy = EditorSettings::get_global(cx).go_to_definition_fallback; - cx.spawn_in(window, async move |editor, cx| { - if definition.await? == Navigated::Yes { - return Ok(Navigated::Yes); - } - match fallback_strategy { - GoToDefinitionFallback::None => Ok(Navigated::No), - GoToDefinitionFallback::FindAllReferences => { - match editor.update_in(cx, |editor, window, cx| { - editor.find_all_references(&FindAllReferences, window, cx) - })? { - Some(references) => references.await, - None => Ok(Navigated::No), - } - } - } - }) - } - - pub fn go_to_declaration( - &mut self, - _: &GoToDeclaration, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.go_to_definition_of_kind(GotoDefinitionKind::Declaration, false, window, cx) - } - - pub fn go_to_declaration_split( - &mut self, - _: &GoToDeclaration, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.go_to_definition_of_kind(GotoDefinitionKind::Declaration, true, window, cx) - } - - pub fn go_to_implementation( - &mut self, - _: &GoToImplementation, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.go_to_definition_of_kind(GotoDefinitionKind::Implementation, false, window, cx) - } - - pub fn go_to_implementation_split( - &mut self, - _: &GoToImplementationSplit, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.go_to_definition_of_kind(GotoDefinitionKind::Implementation, true, window, cx) - } - - pub fn go_to_type_definition( - &mut self, - _: &GoToTypeDefinition, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.go_to_definition_of_kind(GotoDefinitionKind::Type, false, window, cx) - } - - pub fn go_to_definition_split( - &mut self, - _: &GoToDefinitionSplit, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.go_to_definition_of_kind(GotoDefinitionKind::Symbol, true, window, cx) - } - - pub fn go_to_type_definition_split( - &mut self, - _: &GoToTypeDefinitionSplit, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.go_to_definition_of_kind(GotoDefinitionKind::Type, true, window, cx) - } - - fn go_to_definition_of_kind( - &mut self, - kind: GotoDefinitionKind, - split: bool, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - let Some(provider) = self.semantics_provider.clone() else { - return Task::ready(Ok(Navigated::No)); - }; - let head = self.selections.newest::(cx).head(); - let buffer = self.buffer.read(cx); - let (buffer, head) = if let Some(text_anchor) = buffer.text_anchor_for_position(head, cx) { - text_anchor - } else { - return Task::ready(Ok(Navigated::No)); - }; - - let Some(definitions) = provider.definitions(&buffer, head, kind, cx) else { - return Task::ready(Ok(Navigated::No)); - }; - - cx.spawn_in(window, async move |editor, cx| { - let definitions = definitions.await?; - let navigated = editor - .update_in(cx, |editor, window, cx| { - editor.navigate_to_hover_links( - Some(kind), - definitions - .into_iter() - .filter(|location| { - hover_links::exclude_link_to_position(&buffer, &head, location, cx) - }) - .map(HoverLink::Text) - .collect::>(), - split, - window, - cx, - ) - })? - .await?; - anyhow::Ok(navigated) - }) - } - - pub fn open_url(&mut self, _: &OpenUrl, window: &mut Window, cx: &mut Context) { - let selection = self.selections.newest_anchor(); - let head = selection.head(); - let tail = selection.tail(); - - let Some((buffer, start_position)) = - self.buffer.read(cx).text_anchor_for_position(head, cx) - else { - return; - }; - - let end_position = if head != tail { - let Some((_, pos)) = self.buffer.read(cx).text_anchor_for_position(tail, cx) else { - return; - }; - Some(pos) - } else { - None - }; - - let url_finder = cx.spawn_in(window, async move |editor, cx| { - let url = if let Some(end_pos) = end_position { - find_url_from_range(&buffer, start_position..end_pos, cx.clone()) - } else { - find_url(&buffer, start_position, cx.clone()).map(|(_, url)| url) - }; - - if let Some(url) = url { - editor.update(cx, |_, cx| { - cx.open_url(&url); - }) - } else { - Ok(()) - } - }); - - url_finder.detach(); - } - - pub fn open_selected_filename( - &mut self, - _: &OpenSelectedFilename, - window: &mut Window, - cx: &mut Context, - ) { - let Some(workspace) = self.workspace() else { - return; - }; - - let position = self.selections.newest_anchor().head(); - - let Some((buffer, buffer_position)) = - self.buffer.read(cx).text_anchor_for_position(position, cx) - else { - return; - }; - - let project = self.project.clone(); - - cx.spawn_in(window, async move |_, cx| { - let result = find_file(&buffer, project, buffer_position, cx).await; - - if let Some((_, path)) = result { - workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_resolved_path(path, window, cx) - })? - .await?; - } - anyhow::Ok(()) - }) - .detach(); - } - - pub(crate) fn navigate_to_hover_links( - &mut self, - kind: Option, - mut definitions: Vec, - split: bool, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - // If there is one definition, just open it directly - if definitions.len() == 1 { - let definition = definitions.pop().unwrap(); - - enum TargetTaskResult { - Location(Option), - AlreadyNavigated, - } - - let target_task = match definition { - HoverLink::Text(link) => { - Task::ready(anyhow::Ok(TargetTaskResult::Location(Some(link.target)))) - } - HoverLink::InlayHint(lsp_location, server_id) => { - let computation = - self.compute_target_location(lsp_location, server_id, window, cx); - cx.background_spawn(async move { - let location = computation.await?; - Ok(TargetTaskResult::Location(location)) - }) - } - HoverLink::Url(url) => { - cx.open_url(&url); - Task::ready(Ok(TargetTaskResult::AlreadyNavigated)) - } - HoverLink::File(path) => { - if let Some(workspace) = self.workspace() { - cx.spawn_in(window, async move |_, cx| { - workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_resolved_path(path, window, cx) - })? - .await - .map(|_| TargetTaskResult::AlreadyNavigated) - }) - } else { - Task::ready(Ok(TargetTaskResult::Location(None))) - } - } - }; - cx.spawn_in(window, async move |editor, cx| { - let target = match target_task.await.context("target resolution task")? { - TargetTaskResult::AlreadyNavigated => return Ok(Navigated::Yes), - TargetTaskResult::Location(None) => return Ok(Navigated::No), - TargetTaskResult::Location(Some(target)) => target, - }; - - editor.update_in(cx, |editor, window, cx| { - let Some(workspace) = editor.workspace() else { - return Navigated::No; - }; - let pane = workspace.read(cx).active_pane().clone(); - - let range = target.range.to_point(target.buffer.read(cx)); - let range = editor.range_for_match(&range); - let range = collapse_multiline_range(range); - - if !split - && Some(&target.buffer) == editor.buffer.read(cx).as_singleton().as_ref() - { - editor.go_to_singleton_buffer_range(range.clone(), window, cx); - } else { - window.defer(cx, move |window, cx| { - let target_editor: Entity = - workspace.update(cx, |workspace, cx| { - let pane = if split { - workspace.adjacent_pane(window, cx) - } else { - workspace.active_pane().clone() - }; - - workspace.open_project_item( - pane, - target.buffer.clone(), - true, - true, - window, - cx, - ) - }); - target_editor.update(cx, |target_editor, cx| { - // When selecting a definition in a different buffer, disable the nav history - // to avoid creating a history entry at the previous cursor location. - pane.update(cx, |pane, _| pane.disable_history()); - target_editor.go_to_singleton_buffer_range(range, window, cx); - pane.update(cx, |pane, _| pane.enable_history()); - }); - }); - } - Navigated::Yes - }) - }) - } else if !definitions.is_empty() { - cx.spawn_in(window, async move |editor, cx| { - let (title, location_tasks, workspace) = editor - .update_in(cx, |editor, window, cx| { - let tab_kind = match kind { - Some(GotoDefinitionKind::Implementation) => "Implementations", - _ => "Definitions", - }; - let title = definitions - .iter() - .find_map(|definition| match definition { - HoverLink::Text(link) => link.origin.as_ref().map(|origin| { - let buffer = origin.buffer.read(cx); - format!( - "{} for {}", - tab_kind, - buffer - .text_for_range(origin.range.clone()) - .collect::() - ) - }), - HoverLink::InlayHint(_, _) => None, - HoverLink::Url(_) => None, - HoverLink::File(_) => None, - }) - .unwrap_or(tab_kind.to_string()); - let location_tasks = definitions - .into_iter() - .map(|definition| match definition { - HoverLink::Text(link) => Task::ready(Ok(Some(link.target))), - HoverLink::InlayHint(lsp_location, server_id) => editor - .compute_target_location(lsp_location, server_id, window, cx), - HoverLink::Url(_) => Task::ready(Ok(None)), - HoverLink::File(_) => Task::ready(Ok(None)), - }) - .collect::>(); - (title, location_tasks, editor.workspace().clone()) - }) - .context("location tasks preparation")?; - - let locations = future::join_all(location_tasks) - .await - .into_iter() - .filter_map(|location| location.transpose()) - .collect::>() - .context("location tasks")?; - - let Some(workspace) = workspace else { - return Ok(Navigated::No); - }; - let opened = workspace - .update_in(cx, |workspace, window, cx| { - Self::open_locations_in_multibuffer( - workspace, - locations, - title, - split, - MultibufferSelectionMode::First, - window, - cx, - ) - }) - .ok(); - - anyhow::Ok(Navigated::from_bool(opened.is_some())) - }) - } else { - Task::ready(Ok(Navigated::No)) - } - } - - fn compute_target_location( - &self, - lsp_location: lsp::Location, - server_id: LanguageServerId, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - let Some(project) = self.project.clone() else { - return Task::ready(Ok(None)); - }; - - cx.spawn_in(window, async move |editor, cx| { - let location_task = editor.update(cx, |_, cx| { - project.update(cx, |project, cx| { - let language_server_name = project - .language_server_statuses(cx) - .find(|(id, _)| server_id == *id) - .map(|(_, status)| LanguageServerName::from(status.name.as_str())); - language_server_name.map(|language_server_name| { - project.open_local_buffer_via_lsp( - lsp_location.uri.clone(), - server_id, - language_server_name, - cx, - ) - }) - }) - })?; - let location = match location_task { - Some(task) => Some({ - let target_buffer_handle = task.await.context("open local buffer")?; - let range = target_buffer_handle.update(cx, |target_buffer, _| { - let target_start = target_buffer - .clip_point_utf16(point_from_lsp(lsp_location.range.start), Bias::Left); - let target_end = target_buffer - .clip_point_utf16(point_from_lsp(lsp_location.range.end), Bias::Left); - target_buffer.anchor_after(target_start) - ..target_buffer.anchor_before(target_end) - })?; - Location { - buffer: target_buffer_handle, - range, - } - }), - None => None, - }; - Ok(location) - }) - } - - pub fn find_all_references( - &mut self, - _: &FindAllReferences, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - let selection = self.selections.newest::(cx); - let multi_buffer = self.buffer.read(cx); - let head = selection.head(); - - let multi_buffer_snapshot = multi_buffer.snapshot(cx); - let head_anchor = multi_buffer_snapshot.anchor_at( - head, - if head < selection.tail() { - Bias::Right - } else { - Bias::Left - }, - ); - - match self - .find_all_references_task_sources - .binary_search_by(|anchor| anchor.cmp(&head_anchor, &multi_buffer_snapshot)) - { - Ok(_) => { - log::info!( - "Ignoring repeated FindAllReferences invocation with the position of already running task" - ); - return None; - } - Err(i) => { - self.find_all_references_task_sources.insert(i, head_anchor); - } - } - - let (buffer, head) = multi_buffer.text_anchor_for_position(head, cx)?; - let workspace = self.workspace()?; - let project = workspace.read(cx).project().clone(); - let references = project.update(cx, |project, cx| project.references(&buffer, head, cx)); - Some(cx.spawn_in(window, async move |editor, cx| { - let _cleanup = cx.on_drop(&editor, move |editor, _| { - if let Ok(i) = editor - .find_all_references_task_sources - .binary_search_by(|anchor| anchor.cmp(&head_anchor, &multi_buffer_snapshot)) - { - editor.find_all_references_task_sources.remove(i); - } - }); - - let locations = references.await?; - if locations.is_empty() { - return anyhow::Ok(Navigated::No); - } - - workspace.update_in(cx, |workspace, window, cx| { - let title = locations - .first() - .as_ref() - .map(|location| { - let buffer = location.buffer.read(cx); - format!( - "References to `{}`", - buffer - .text_for_range(location.range.clone()) - .collect::() - ) - }) - .unwrap(); - Self::open_locations_in_multibuffer( - workspace, - locations, - title, - false, - MultibufferSelectionMode::First, - window, - cx, - ); - Navigated::Yes - }) - })) - } - - /// Opens a multibuffer with the given project locations in it - pub fn open_locations_in_multibuffer( - workspace: &mut Workspace, - mut locations: Vec, - title: String, - split: bool, - multibuffer_selection_mode: MultibufferSelectionMode, - window: &mut Window, - cx: &mut Context, - ) { - // If there are multiple definitions, open them in a multibuffer - locations.sort_by_key(|location| location.buffer.read(cx).remote_id()); - let mut locations = locations.into_iter().peekable(); - let mut ranges: Vec> = Vec::new(); - let capability = workspace.project().read(cx).capability(); - - let excerpt_buffer = cx.new(|cx| { - let mut multibuffer = MultiBuffer::new(capability); - while let Some(location) = locations.next() { - let buffer = location.buffer.read(cx); - let mut ranges_for_buffer = Vec::new(); - let range = location.range.to_point(buffer); - ranges_for_buffer.push(range.clone()); - - while let Some(next_location) = locations.peek() { - if next_location.buffer == location.buffer { - ranges_for_buffer.push(next_location.range.to_point(buffer)); - locations.next(); - } else { - break; - } - } - - ranges_for_buffer.sort_by_key(|range| (range.start, Reverse(range.end))); - let (new_ranges, _) = multibuffer.set_excerpts_for_path( - PathKey::for_buffer(&location.buffer, cx), - location.buffer.clone(), - ranges_for_buffer, - DEFAULT_MULTIBUFFER_CONTEXT, - cx, - ); - ranges.extend(new_ranges) - } - - multibuffer.with_title(title) - }); - - let editor = cx.new(|cx| { - Editor::for_multibuffer( - excerpt_buffer, - Some(workspace.project().clone()), - window, - cx, - ) - }); - editor.update(cx, |editor, cx| { - match multibuffer_selection_mode { - MultibufferSelectionMode::First => { - if let Some(first_range) = ranges.first() { - editor.change_selections(None, window, cx, |selections| { - selections.clear_disjoint(); - selections.select_anchor_ranges(std::iter::once(first_range.clone())); - }); - } - editor.highlight_background::( - &ranges, - |theme| theme.editor_highlighted_line_background, - cx, - ); - } - MultibufferSelectionMode::All => { - editor.change_selections(None, window, cx, |selections| { - selections.clear_disjoint(); - selections.select_anchor_ranges(ranges); - }); - } - } - editor.register_buffers_with_language_servers(cx); - }); - - let item = Box::new(editor); - let item_id = item.item_id(); - - if split { - workspace.split_item(SplitDirection::Right, item.clone(), window, cx); - } else { - if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation { - let (preview_item_id, preview_item_idx) = - workspace.active_pane().update(cx, |pane, _| { - (pane.preview_item_id(), pane.preview_item_idx()) - }); - - workspace.add_item_to_active_pane(item.clone(), preview_item_idx, true, window, cx); - - if let Some(preview_item_id) = preview_item_id { - workspace.active_pane().update(cx, |pane, cx| { - pane.remove_item(preview_item_id, false, false, window, cx); - }); - } - } else { - workspace.add_item_to_active_pane(item.clone(), None, true, window, cx); - } - } - workspace.active_pane().update(cx, |pane, cx| { - pane.set_preview_item_id(Some(item_id), cx); - }); - } - - pub fn rename( - &mut self, - _: &Rename, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - use language::ToOffset as _; - - let provider = self.semantics_provider.clone()?; - let selection = self.selections.newest_anchor().clone(); - let (cursor_buffer, cursor_buffer_position) = self - .buffer - .read(cx) - .text_anchor_for_position(selection.head(), cx)?; - let (tail_buffer, cursor_buffer_position_end) = self - .buffer - .read(cx) - .text_anchor_for_position(selection.tail(), cx)?; - if tail_buffer != cursor_buffer { - return None; - } - - let snapshot = cursor_buffer.read(cx).snapshot(); - let cursor_buffer_offset = cursor_buffer_position.to_offset(&snapshot); - let cursor_buffer_offset_end = cursor_buffer_position_end.to_offset(&snapshot); - let prepare_rename = provider - .range_for_rename(&cursor_buffer, cursor_buffer_position, cx) - .unwrap_or_else(|| Task::ready(Ok(None))); - drop(snapshot); - - Some(cx.spawn_in(window, async move |this, cx| { - let rename_range = if let Some(range) = prepare_rename.await? { - Some(range) - } else { - this.update(cx, |this, cx| { - let buffer = this.buffer.read(cx).snapshot(cx); - let mut buffer_highlights = this - .document_highlights_for_position(selection.head(), &buffer) - .filter(|highlight| { - highlight.start.excerpt_id == selection.head().excerpt_id - && highlight.end.excerpt_id == selection.head().excerpt_id - }); - buffer_highlights - .next() - .map(|highlight| highlight.start.text_anchor..highlight.end.text_anchor) - })? - }; - if let Some(rename_range) = rename_range { - this.update_in(cx, |this, window, cx| { - let snapshot = cursor_buffer.read(cx).snapshot(); - let rename_buffer_range = rename_range.to_offset(&snapshot); - let cursor_offset_in_rename_range = - cursor_buffer_offset.saturating_sub(rename_buffer_range.start); - let cursor_offset_in_rename_range_end = - cursor_buffer_offset_end.saturating_sub(rename_buffer_range.start); - - this.take_rename(false, window, cx); - let buffer = this.buffer.read(cx).read(cx); - let cursor_offset = selection.head().to_offset(&buffer); - let rename_start = cursor_offset.saturating_sub(cursor_offset_in_rename_range); - let rename_end = rename_start + rename_buffer_range.len(); - let range = buffer.anchor_before(rename_start)..buffer.anchor_after(rename_end); - let mut old_highlight_id = None; - let old_name: Arc = buffer - .chunks(rename_start..rename_end, true) - .map(|chunk| { - if old_highlight_id.is_none() { - old_highlight_id = chunk.syntax_highlight_id; - } - chunk.text - }) - .collect::() - .into(); - - drop(buffer); - - // Position the selection in the rename editor so that it matches the current selection. - this.show_local_selections = false; - let rename_editor = cx.new(|cx| { - let mut editor = Editor::single_line(window, cx); - editor.buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, old_name.clone())], None, cx) - }); - let rename_selection_range = match cursor_offset_in_rename_range - .cmp(&cursor_offset_in_rename_range_end) - { - Ordering::Equal => { - editor.select_all(&SelectAll, window, cx); - return editor; - } - Ordering::Less => { - cursor_offset_in_rename_range..cursor_offset_in_rename_range_end - } - Ordering::Greater => { - cursor_offset_in_rename_range_end..cursor_offset_in_rename_range - } - }; - if rename_selection_range.end > old_name.len() { - editor.select_all(&SelectAll, window, cx); - } else { - editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| { - s.select_ranges([rename_selection_range]); - }); - } - editor - }); - cx.subscribe(&rename_editor, |_, _, e: &EditorEvent, cx| { - if e == &EditorEvent::Focused { - cx.emit(EditorEvent::FocusedIn) - } - }) - .detach(); - - let write_highlights = - this.clear_background_highlights::(cx); - let read_highlights = - this.clear_background_highlights::(cx); - let ranges = write_highlights - .iter() - .flat_map(|(_, ranges)| ranges.iter()) - .chain(read_highlights.iter().flat_map(|(_, ranges)| ranges.iter())) - .cloned() - .collect(); - - this.highlight_text::( - ranges, - HighlightStyle { - fade_out: Some(0.6), - ..Default::default() - }, - cx, - ); - let rename_focus_handle = rename_editor.focus_handle(cx); - window.focus(&rename_focus_handle); - let block_id = this.insert_blocks( - [BlockProperties { - style: BlockStyle::Flex, - placement: BlockPlacement::Below(range.start), - height: Some(1), - render: Arc::new({ - let rename_editor = rename_editor.clone(); - move |cx: &mut BlockContext| { - let mut text_style = cx.editor_style.text.clone(); - if let Some(highlight_style) = old_highlight_id - .and_then(|h| h.style(&cx.editor_style.syntax)) - { - text_style = text_style.highlight(highlight_style); - } - div() - .block_mouse_down() - .pl(cx.anchor_x) - .child(EditorElement::new( - &rename_editor, - EditorStyle { - background: cx.theme().system().transparent, - local_player: cx.editor_style.local_player, - text: text_style, - scrollbar_width: cx.editor_style.scrollbar_width, - syntax: cx.editor_style.syntax.clone(), - status: cx.editor_style.status.clone(), - inlay_hints_style: HighlightStyle { - font_weight: Some(FontWeight::BOLD), - ..make_inlay_hints_style(cx.app) - }, - inline_completion_styles: make_suggestion_styles( - cx.app, - ), - ..EditorStyle::default() - }, - )) - .into_any_element() - } - }), - priority: 0, - }], - Some(Autoscroll::fit()), - cx, - )[0]; - this.pending_rename = Some(RenameState { - range, - old_name, - editor: rename_editor, - block_id, - }); - })?; - } - - Ok(()) - })) - } - - pub fn confirm_rename( - &mut self, - _: &ConfirmRename, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - let rename = self.take_rename(false, window, cx)?; - let workspace = self.workspace()?.downgrade(); - let (buffer, start) = self - .buffer - .read(cx) - .text_anchor_for_position(rename.range.start, cx)?; - let (end_buffer, _) = self - .buffer - .read(cx) - .text_anchor_for_position(rename.range.end, cx)?; - if buffer != end_buffer { - return None; - } - - let old_name = rename.old_name; - let new_name = rename.editor.read(cx).text(cx); - - let rename = self.semantics_provider.as_ref()?.perform_rename( - &buffer, - start, - new_name.clone(), - cx, - )?; - - Some(cx.spawn_in(window, async move |editor, cx| { - let project_transaction = rename.await?; - Self::open_project_transaction( - &editor, - workspace, - project_transaction, - format!("Rename: {} → {}", old_name, new_name), - cx, - ) - .await?; - - editor.update(cx, |editor, cx| { - editor.refresh_document_highlights(cx); - })?; - Ok(()) - })) - } - - fn take_rename( - &mut self, - moving_cursor: bool, - window: &mut Window, - cx: &mut Context, - ) -> Option { - let rename = self.pending_rename.take()?; - if rename.editor.focus_handle(cx).is_focused(window) { - window.focus(&self.focus_handle); - } - - self.remove_blocks( - [rename.block_id].into_iter().collect(), - Some(Autoscroll::fit()), - cx, - ); - self.clear_highlights::(cx); - self.show_local_selections = true; - - if moving_cursor { - let cursor_in_rename_editor = rename.editor.update(cx, |editor, cx| { - editor.selections.newest::(cx).head() - }); - - // Update the selection to match the position of the selection inside - // the rename editor. - let snapshot = self.buffer.read(cx).read(cx); - let rename_range = rename.range.to_offset(&snapshot); - let cursor_in_editor = snapshot - .clip_offset(rename_range.start + cursor_in_rename_editor, Bias::Left) - .min(rename_range.end); - drop(snapshot); - - self.change_selections(None, window, cx, |s| { - s.select_ranges(vec![cursor_in_editor..cursor_in_editor]) - }); - } else { - self.refresh_document_highlights(cx); - } - - Some(rename) - } - - pub fn pending_rename(&self) -> Option<&RenameState> { - self.pending_rename.as_ref() - } - - fn format( - &mut self, - _: &Format, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - - let project = match &self.project { - Some(project) => project.clone(), - None => return None, - }; - - Some(self.perform_format( - project, - FormatTrigger::Manual, - FormatTarget::Buffers, - window, - cx, - )) - } - - fn format_selections( - &mut self, - _: &FormatSelections, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - - let project = match &self.project { - Some(project) => project.clone(), - None => return None, - }; - - let ranges = self - .selections - .all_adjusted(cx) - .into_iter() - .map(|selection| selection.range()) - .collect_vec(); - - Some(self.perform_format( - project, - FormatTrigger::Manual, - FormatTarget::Ranges(ranges), - window, - cx, - )) - } - - fn perform_format( - &mut self, - project: Entity, - trigger: FormatTrigger, - target: FormatTarget, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - let buffer = self.buffer.clone(); - let (buffers, target) = match target { - FormatTarget::Buffers => { - let mut buffers = buffer.read(cx).all_buffers(); - if trigger == FormatTrigger::Save { - buffers.retain(|buffer| buffer.read(cx).is_dirty()); - } - (buffers, LspFormatTarget::Buffers) - } - FormatTarget::Ranges(selection_ranges) => { - let multi_buffer = buffer.read(cx); - let snapshot = multi_buffer.read(cx); - let mut buffers = HashSet::default(); - let mut buffer_id_to_ranges: BTreeMap>> = - BTreeMap::new(); - for selection_range in selection_ranges { - for (buffer, buffer_range, _) in - snapshot.range_to_buffer_ranges(selection_range) - { - let buffer_id = buffer.remote_id(); - let start = buffer.anchor_before(buffer_range.start); - let end = buffer.anchor_after(buffer_range.end); - buffers.insert(multi_buffer.buffer(buffer_id).unwrap()); - buffer_id_to_ranges - .entry(buffer_id) - .and_modify(|buffer_ranges| buffer_ranges.push(start..end)) - .or_insert_with(|| vec![start..end]); - } - } - (buffers, LspFormatTarget::Ranges(buffer_id_to_ranges)) - } - }; - - let transaction_id_prev = buffer.read_with(cx, |b, cx| b.last_transaction_id(cx)); - let selections_prev = transaction_id_prev - .and_then(|transaction_id_prev| { - // default to selections as they were after the last edit, if we have them, - // instead of how they are now. - // This will make it so that editing, moving somewhere else, formatting, then undoing the format - // will take you back to where you made the last edit, instead of staying where you scrolled - self.selection_history - .transaction(transaction_id_prev) - .map(|t| t.0.clone()) - }) - .unwrap_or_else(|| { - log::info!("Failed to determine selections from before format. Falling back to selections when format was initiated"); - self.selections.disjoint_anchors() - }); - - let mut timeout = cx.background_executor().timer(FORMAT_TIMEOUT).fuse(); - let format = project.update(cx, |project, cx| { - project.format(buffers, target, true, trigger, cx) - }); - - cx.spawn_in(window, async move |editor, cx| { - let transaction = futures::select_biased! { - transaction = format.log_err().fuse() => transaction, - () = timeout => { - log::warn!("timed out waiting for formatting"); - None - } - }; - - buffer - .update(cx, |buffer, cx| { - if let Some(transaction) = transaction { - if !buffer.is_singleton() { - buffer.push_transaction(&transaction.0, cx); - } - } - cx.notify(); - }) - .ok(); - - if let Some(transaction_id_now) = - buffer.read_with(cx, |b, cx| b.last_transaction_id(cx))? - { - let has_new_transaction = transaction_id_prev != Some(transaction_id_now); - if has_new_transaction { - _ = editor.update(cx, |editor, _| { - editor - .selection_history - .insert_transaction(transaction_id_now, selections_prev); - }); - } - } - - Ok(()) - }) - } - - fn organize_imports( - &mut self, - _: &OrganizeImports, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - let project = match &self.project { - Some(project) => project.clone(), - None => return None, - }; - Some(self.perform_code_action_kind( - project, - CodeActionKind::SOURCE_ORGANIZE_IMPORTS, - window, - cx, - )) - } - - fn perform_code_action_kind( - &mut self, - project: Entity, - kind: CodeActionKind, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - let buffer = self.buffer.clone(); - let buffers = buffer.read(cx).all_buffers(); - let mut timeout = cx.background_executor().timer(CODE_ACTION_TIMEOUT).fuse(); - let apply_action = project.update(cx, |project, cx| { - project.apply_code_action_kind(buffers, kind, true, cx) - }); - cx.spawn_in(window, async move |_, cx| { - let transaction = futures::select_biased! { - () = timeout => { - log::warn!("timed out waiting for executing code action"); - None - } - transaction = apply_action.log_err().fuse() => transaction, - }; - buffer - .update(cx, |buffer, cx| { - // check if we need this - if let Some(transaction) = transaction { - if !buffer.is_singleton() { - buffer.push_transaction(&transaction.0, cx); - } - } - cx.notify(); - }) - .ok(); - Ok(()) - }) - } - - fn restart_language_server( - &mut self, - _: &RestartLanguageServer, - _: &mut Window, - cx: &mut Context, - ) { - if let Some(project) = self.project.clone() { - self.buffer.update(cx, |multi_buffer, cx| { - project.update(cx, |project, cx| { - project.restart_language_servers_for_buffers( - multi_buffer.all_buffers().into_iter().collect(), - cx, - ); - }); - }) - } - } - - fn stop_language_server( - &mut self, - _: &StopLanguageServer, - _: &mut Window, - cx: &mut Context, - ) { - if let Some(project) = self.project.clone() { - self.buffer.update(cx, |multi_buffer, cx| { - project.update(cx, |project, cx| { - project.stop_language_servers_for_buffers( - multi_buffer.all_buffers().into_iter().collect(), - cx, - ); - cx.emit(project::Event::RefreshInlayHints); - }); - }); - } - } - - fn cancel_language_server_work( - workspace: &mut Workspace, - _: &actions::CancelLanguageServerWork, - _: &mut Window, - cx: &mut Context, - ) { - let project = workspace.project(); - let buffers = workspace - .active_item(cx) - .and_then(|item| item.act_as::(cx)) - .map_or(HashSet::default(), |editor| { - editor.read(cx).buffer.read(cx).all_buffers() - }); - project.update(cx, |project, cx| { - project.cancel_language_server_work_for_buffers(buffers, cx); - }); - } - - fn show_character_palette( - &mut self, - _: &ShowCharacterPalette, - window: &mut Window, - _: &mut Context, - ) { - window.show_character_palette(); - } - - fn refresh_active_diagnostics(&mut self, cx: &mut Context) { - if let ActiveDiagnostic::Group(active_diagnostics) = &mut self.active_diagnostics { - let buffer = self.buffer.read(cx).snapshot(cx); - let primary_range_start = active_diagnostics.active_range.start.to_offset(&buffer); - let primary_range_end = active_diagnostics.active_range.end.to_offset(&buffer); - let is_valid = buffer - .diagnostics_in_range::(primary_range_start..primary_range_end) - .any(|entry| { - entry.diagnostic.is_primary - && !entry.range.is_empty() - && entry.range.start == primary_range_start - && entry.diagnostic.message == active_diagnostics.active_message - }); - - if !is_valid { - self.dismiss_diagnostics(cx); - } - } - } - - pub fn active_diagnostic_group(&self) -> Option<&ActiveDiagnosticGroup> { - match &self.active_diagnostics { - ActiveDiagnostic::Group(group) => Some(group), - _ => None, - } - } - - pub fn set_all_diagnostics_active(&mut self, cx: &mut Context) { - self.dismiss_diagnostics(cx); - self.active_diagnostics = ActiveDiagnostic::All; - } - - fn activate_diagnostics( - &mut self, - buffer_id: BufferId, - diagnostic: DiagnosticEntry, - window: &mut Window, - cx: &mut Context, - ) { - if matches!(self.active_diagnostics, ActiveDiagnostic::All) { - return; - } - self.dismiss_diagnostics(cx); - let snapshot = self.snapshot(window, cx); - let buffer = self.buffer.read(cx).snapshot(cx); - let Some(renderer) = GlobalDiagnosticRenderer::global(cx) else { - return; - }; - - let diagnostic_group = buffer - .diagnostic_group(buffer_id, diagnostic.diagnostic.group_id) - .collect::>(); - - let blocks = - renderer.render_group(diagnostic_group, buffer_id, snapshot, cx.weak_entity(), cx); - - let blocks = self.display_map.update(cx, |display_map, cx| { - display_map.insert_blocks(blocks, cx).into_iter().collect() - }); - self.active_diagnostics = ActiveDiagnostic::Group(ActiveDiagnosticGroup { - active_range: buffer.anchor_before(diagnostic.range.start) - ..buffer.anchor_after(diagnostic.range.end), - active_message: diagnostic.diagnostic.message.clone(), - group_id: diagnostic.diagnostic.group_id, - blocks, - }); - cx.notify(); - } - - fn dismiss_diagnostics(&mut self, cx: &mut Context) { - if matches!(self.active_diagnostics, ActiveDiagnostic::All) { - return; - }; - - let prev = mem::replace(&mut self.active_diagnostics, ActiveDiagnostic::None); - if let ActiveDiagnostic::Group(group) = prev { - self.display_map.update(cx, |display_map, cx| { - display_map.remove_blocks(group.blocks, cx); - }); - cx.notify(); - } - } - - /// Disable inline diagnostics rendering for this editor. - pub fn disable_inline_diagnostics(&mut self) { - self.inline_diagnostics_enabled = false; - self.inline_diagnostics_update = Task::ready(()); - self.inline_diagnostics.clear(); - } - - pub fn inline_diagnostics_enabled(&self) -> bool { - self.inline_diagnostics_enabled - } - - pub fn show_inline_diagnostics(&self) -> bool { - self.show_inline_diagnostics - } - - pub fn toggle_inline_diagnostics( - &mut self, - _: &ToggleInlineDiagnostics, - window: &mut Window, - cx: &mut Context, - ) { - self.show_inline_diagnostics = !self.show_inline_diagnostics; - self.refresh_inline_diagnostics(false, window, cx); - } - - fn refresh_inline_diagnostics( - &mut self, - debounce: bool, - window: &mut Window, - cx: &mut Context, - ) { - if !self.inline_diagnostics_enabled || !self.show_inline_diagnostics { - self.inline_diagnostics_update = Task::ready(()); - self.inline_diagnostics.clear(); - return; - } - - let debounce_ms = ProjectSettings::get_global(cx) - .diagnostics - .inline - .update_debounce_ms; - let debounce = if debounce && debounce_ms > 0 { - Some(Duration::from_millis(debounce_ms)) - } else { - None - }; - self.inline_diagnostics_update = cx.spawn_in(window, async move |editor, cx| { - let editor = editor.upgrade().unwrap(); - - if let Some(debounce) = debounce { - cx.background_executor().timer(debounce).await; - } - let Some(snapshot) = editor - .update(cx, |editor, cx| editor.buffer().read(cx).snapshot(cx)) - .ok() - else { - return; - }; - - let new_inline_diagnostics = cx - .background_spawn(async move { - let mut inline_diagnostics = Vec::<(Anchor, InlineDiagnostic)>::new(); - for diagnostic_entry in snapshot.diagnostics_in_range(0..snapshot.len()) { - let message = diagnostic_entry - .diagnostic - .message - .split_once('\n') - .map(|(line, _)| line) - .map(SharedString::new) - .unwrap_or_else(|| { - SharedString::from(diagnostic_entry.diagnostic.message) - }); - let start_anchor = snapshot.anchor_before(diagnostic_entry.range.start); - let (Ok(i) | Err(i)) = inline_diagnostics - .binary_search_by(|(probe, _)| probe.cmp(&start_anchor, &snapshot)); - inline_diagnostics.insert( - i, - ( - start_anchor, - InlineDiagnostic { - message, - group_id: diagnostic_entry.diagnostic.group_id, - start: diagnostic_entry.range.start.to_point(&snapshot), - is_primary: diagnostic_entry.diagnostic.is_primary, - severity: diagnostic_entry.diagnostic.severity, - }, - ), - ); - } - inline_diagnostics - }) - .await; - - editor - .update(cx, |editor, cx| { - editor.inline_diagnostics = new_inline_diagnostics; - cx.notify(); - }) - .ok(); - }); - } - - pub fn set_selections_from_remote( - &mut self, - selections: Vec>, - pending_selection: Option>, - window: &mut Window, - cx: &mut Context, - ) { - let old_cursor_position = self.selections.newest_anchor().head(); - self.selections.change_with(cx, |s| { - s.select_anchors(selections); - if let Some(pending_selection) = pending_selection { - s.set_pending(pending_selection, SelectMode::Character); - } else { - s.clear_pending(); - } - }); - self.selections_did_change(false, &old_cursor_position, true, window, cx); - } - - fn push_to_selection_history(&mut self) { - self.selection_history.push(SelectionHistoryEntry { - selections: self.selections.disjoint_anchors(), - select_next_state: self.select_next_state.clone(), - select_prev_state: self.select_prev_state.clone(), - add_selections_state: self.add_selections_state.clone(), - }); - } - - pub fn transact( - &mut self, - window: &mut Window, - cx: &mut Context, - update: impl FnOnce(&mut Self, &mut Window, &mut Context), - ) -> Option { - self.start_transaction_at(Instant::now(), window, cx); - update(self, window, cx); - self.end_transaction_at(Instant::now(), cx) - } - - pub fn start_transaction_at( - &mut self, - now: Instant, - window: &mut Window, - cx: &mut Context, - ) { - self.end_selection(window, cx); - if let Some(tx_id) = self - .buffer - .update(cx, |buffer, cx| buffer.start_transaction_at(now, cx)) - { - self.selection_history - .insert_transaction(tx_id, self.selections.disjoint_anchors()); - cx.emit(EditorEvent::TransactionBegun { - transaction_id: tx_id, - }) - } - } - - pub fn end_transaction_at( - &mut self, - now: Instant, - cx: &mut Context, - ) -> Option { - if let Some(transaction_id) = self - .buffer - .update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)) - { - if let Some((_, end_selections)) = - self.selection_history.transaction_mut(transaction_id) - { - *end_selections = Some(self.selections.disjoint_anchors()); - } else { - log::error!("unexpectedly ended a transaction that wasn't started by this editor"); - } - - cx.emit(EditorEvent::Edited { transaction_id }); - Some(transaction_id) - } else { - None - } - } - - pub fn set_mark(&mut self, _: &actions::SetMark, window: &mut Window, cx: &mut Context) { - if self.selection_mark_mode { - self.change_selections(None, window, cx, |s| { - s.move_with(|_, sel| { - sel.collapse_to(sel.head(), SelectionGoal::None); - }); - }) - } - self.selection_mark_mode = true; - cx.notify(); - } - - pub fn swap_selection_ends( - &mut self, - _: &actions::SwapSelectionEnds, - window: &mut Window, - cx: &mut Context, - ) { - self.change_selections(None, window, cx, |s| { - s.move_with(|_, sel| { - if sel.start != sel.end { - sel.reversed = !sel.reversed - } - }); - }); - self.request_autoscroll(Autoscroll::newest(), cx); - cx.notify(); - } - - pub fn toggle_fold( - &mut self, - _: &actions::ToggleFold, - window: &mut Window, - cx: &mut Context, - ) { - if self.is_singleton(cx) { - let selection = self.selections.newest::(cx); - - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let range = if selection.is_empty() { - let point = selection.head().to_display_point(&display_map); - let start = DisplayPoint::new(point.row(), 0).to_point(&display_map); - let end = DisplayPoint::new(point.row(), display_map.line_len(point.row())) - .to_point(&display_map); - start..end - } else { - selection.range() - }; - if display_map.folds_in_range(range).next().is_some() { - self.unfold_lines(&Default::default(), window, cx) - } else { - self.fold(&Default::default(), window, cx) - } - } else { - let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); - let buffer_ids: HashSet<_> = self - .selections - .disjoint_anchor_ranges() - .flat_map(|range| multi_buffer_snapshot.buffer_ids_for_range(range)) - .collect(); - - let should_unfold = buffer_ids - .iter() - .any(|buffer_id| self.is_buffer_folded(*buffer_id, cx)); - - for buffer_id in buffer_ids { - if should_unfold { - self.unfold_buffer(buffer_id, cx); - } else { - self.fold_buffer(buffer_id, cx); - } - } - } - } - - pub fn toggle_fold_recursive( - &mut self, - _: &actions::ToggleFoldRecursive, - window: &mut Window, - cx: &mut Context, - ) { - let selection = self.selections.newest::(cx); - - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let range = if selection.is_empty() { - let point = selection.head().to_display_point(&display_map); - let start = DisplayPoint::new(point.row(), 0).to_point(&display_map); - let end = DisplayPoint::new(point.row(), display_map.line_len(point.row())) - .to_point(&display_map); - start..end - } else { - selection.range() - }; - if display_map.folds_in_range(range).next().is_some() { - self.unfold_recursive(&Default::default(), window, cx) - } else { - self.fold_recursive(&Default::default(), window, cx) - } - } - - pub fn fold(&mut self, _: &actions::Fold, window: &mut Window, cx: &mut Context) { - if self.is_singleton(cx) { - let mut to_fold = Vec::new(); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let selections = self.selections.all_adjusted(cx); - - for selection in selections { - let range = selection.range().sorted(); - let buffer_start_row = range.start.row; - - if range.start.row != range.end.row { - let mut found = false; - let mut row = range.start.row; - while row <= range.end.row { - if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) - { - found = true; - row = crease.range().end.row + 1; - to_fold.push(crease); - } else { - row += 1 - } - } - if found { - continue; - } - } - - for row in (0..=range.start.row).rev() { - if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) { - if crease.range().end.row >= buffer_start_row { - to_fold.push(crease); - if row <= range.start.row { - break; - } - } - } - } - } - - self.fold_creases(to_fold, true, window, cx); - } else { - let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); - let buffer_ids = self - .selections - .disjoint_anchor_ranges() - .flat_map(|range| multi_buffer_snapshot.buffer_ids_for_range(range)) - .collect::>(); - for buffer_id in buffer_ids { - self.fold_buffer(buffer_id, cx); - } - } - } - - fn fold_at_level( - &mut self, - fold_at: &FoldAtLevel, - window: &mut Window, - cx: &mut Context, - ) { - if !self.buffer.read(cx).is_singleton() { - return; - } - - let fold_at_level = fold_at.0; - let snapshot = self.buffer.read(cx).snapshot(cx); - let mut to_fold = Vec::new(); - let mut stack = vec![(0, snapshot.max_row().0, 1)]; - - while let Some((mut start_row, end_row, current_level)) = stack.pop() { - while start_row < end_row { - match self - .snapshot(window, cx) - .crease_for_buffer_row(MultiBufferRow(start_row)) - { - Some(crease) => { - let nested_start_row = crease.range().start.row + 1; - let nested_end_row = crease.range().end.row; - - if current_level < fold_at_level { - stack.push((nested_start_row, nested_end_row, current_level + 1)); - } else if current_level == fold_at_level { - to_fold.push(crease); - } - - start_row = nested_end_row + 1; - } - None => start_row += 1, - } - } - } - - self.fold_creases(to_fold, true, window, cx); - } - - pub fn fold_all(&mut self, _: &actions::FoldAll, window: &mut Window, cx: &mut Context) { - if self.buffer.read(cx).is_singleton() { - let mut fold_ranges = Vec::new(); - let snapshot = self.buffer.read(cx).snapshot(cx); - - for row in 0..snapshot.max_row().0 { - if let Some(foldable_range) = self - .snapshot(window, cx) - .crease_for_buffer_row(MultiBufferRow(row)) - { - fold_ranges.push(foldable_range); - } - } - - self.fold_creases(fold_ranges, true, window, cx); - } else { - self.toggle_fold_multiple_buffers = cx.spawn_in(window, async move |editor, cx| { - editor - .update_in(cx, |editor, _, cx| { - for buffer_id in editor.buffer.read(cx).excerpt_buffer_ids() { - editor.fold_buffer(buffer_id, cx); - } - }) - .ok(); - }); - } - } - - pub fn fold_function_bodies( - &mut self, - _: &actions::FoldFunctionBodies, - window: &mut Window, - cx: &mut Context, - ) { - let snapshot = self.buffer.read(cx).snapshot(cx); - - let ranges = snapshot - .text_object_ranges(0..snapshot.len(), TreeSitterOptions::default()) - .filter_map(|(range, obj)| (obj == TextObject::InsideFunction).then_some(range)) - .collect::>(); - - let creases = ranges - .into_iter() - .map(|range| Crease::simple(range, self.display_map.read(cx).fold_placeholder.clone())) - .collect(); - - self.fold_creases(creases, true, window, cx); - } - - pub fn fold_recursive( - &mut self, - _: &actions::FoldRecursive, - window: &mut Window, - cx: &mut Context, - ) { - let mut to_fold = Vec::new(); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let selections = self.selections.all_adjusted(cx); - - for selection in selections { - let range = selection.range().sorted(); - let buffer_start_row = range.start.row; - - if range.start.row != range.end.row { - let mut found = false; - for row in range.start.row..=range.end.row { - if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) { - found = true; - to_fold.push(crease); - } - } - if found { - continue; - } - } - - for row in (0..=range.start.row).rev() { - if let Some(crease) = display_map.crease_for_buffer_row(MultiBufferRow(row)) { - if crease.range().end.row >= buffer_start_row { - to_fold.push(crease); - } else { - break; - } - } - } - } - - self.fold_creases(to_fold, true, window, cx); - } - - pub fn fold_at( - &mut self, - buffer_row: MultiBufferRow, - window: &mut Window, - cx: &mut Context, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - - if let Some(crease) = display_map.crease_for_buffer_row(buffer_row) { - let autoscroll = self - .selections - .all::(cx) - .iter() - .any(|selection| crease.range().overlaps(&selection.range())); - - self.fold_creases(vec![crease], autoscroll, window, cx); - } - } - - pub fn unfold_lines(&mut self, _: &UnfoldLines, _window: &mut Window, cx: &mut Context) { - if self.is_singleton(cx) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let buffer = &display_map.buffer_snapshot; - let selections = self.selections.all::(cx); - let ranges = selections - .iter() - .map(|s| { - let range = s.display_range(&display_map).sorted(); - let mut start = range.start.to_point(&display_map); - let mut end = range.end.to_point(&display_map); - start.column = 0; - end.column = buffer.line_len(MultiBufferRow(end.row)); - start..end - }) - .collect::>(); - - self.unfold_ranges(&ranges, true, true, cx); - } else { - let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); - let buffer_ids = self - .selections - .disjoint_anchor_ranges() - .flat_map(|range| multi_buffer_snapshot.buffer_ids_for_range(range)) - .collect::>(); - for buffer_id in buffer_ids { - self.unfold_buffer(buffer_id, cx); - } - } - } - - pub fn unfold_recursive( - &mut self, - _: &UnfoldRecursive, - _window: &mut Window, - cx: &mut Context, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let selections = self.selections.all::(cx); - let ranges = selections - .iter() - .map(|s| { - let mut range = s.display_range(&display_map).sorted(); - *range.start.column_mut() = 0; - *range.end.column_mut() = display_map.line_len(range.end.row()); - let start = range.start.to_point(&display_map); - let end = range.end.to_point(&display_map); - start..end - }) - .collect::>(); - - self.unfold_ranges(&ranges, true, true, cx); - } - - pub fn unfold_at( - &mut self, - buffer_row: MultiBufferRow, - _window: &mut Window, - cx: &mut Context, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - - let intersection_range = Point::new(buffer_row.0, 0) - ..Point::new( - buffer_row.0, - display_map.buffer_snapshot.line_len(buffer_row), - ); - - let autoscroll = self - .selections - .all::(cx) - .iter() - .any(|selection| RangeExt::overlaps(&selection.range(), &intersection_range)); - - self.unfold_ranges(&[intersection_range], true, autoscroll, cx); - } - - pub fn unfold_all( - &mut self, - _: &actions::UnfoldAll, - _window: &mut Window, - cx: &mut Context, - ) { - if self.buffer.read(cx).is_singleton() { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - self.unfold_ranges(&[0..display_map.buffer_snapshot.len()], true, true, cx); - } else { - self.toggle_fold_multiple_buffers = cx.spawn(async move |editor, cx| { - editor - .update(cx, |editor, cx| { - for buffer_id in editor.buffer.read(cx).excerpt_buffer_ids() { - editor.unfold_buffer(buffer_id, cx); - } - }) - .ok(); - }); - } - } - - pub fn fold_selected_ranges( - &mut self, - _: &FoldSelectedRanges, - window: &mut Window, - cx: &mut Context, - ) { - let selections = self.selections.all_adjusted(cx); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let ranges = selections - .into_iter() - .map(|s| Crease::simple(s.range(), display_map.fold_placeholder.clone())) - .collect::>(); - self.fold_creases(ranges, true, window, cx); - } - - pub fn fold_ranges( - &mut self, - ranges: Vec>, - auto_scroll: bool, - window: &mut Window, - cx: &mut Context, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let ranges = ranges - .into_iter() - .map(|r| Crease::simple(r, display_map.fold_placeholder.clone())) - .collect::>(); - self.fold_creases(ranges, auto_scroll, window, cx); - } - - pub fn fold_creases( - &mut self, - creases: Vec>, - auto_scroll: bool, - _window: &mut Window, - cx: &mut Context, - ) { - if creases.is_empty() { - return; - } - - let mut buffers_affected = HashSet::default(); - let multi_buffer = self.buffer().read(cx); - for crease in &creases { - if let Some((_, buffer, _)) = - multi_buffer.excerpt_containing(crease.range().start.clone(), cx) - { - buffers_affected.insert(buffer.read(cx).remote_id()); - }; - } - - self.display_map.update(cx, |map, cx| map.fold(creases, cx)); - - if auto_scroll { - self.request_autoscroll(Autoscroll::fit(), cx); - } - - cx.notify(); - - self.scrollbar_marker_state.dirty = true; - self.folds_did_change(cx); - } - - /// Removes any folds whose ranges intersect any of the given ranges. - pub fn unfold_ranges( - &mut self, - ranges: &[Range], - inclusive: bool, - auto_scroll: bool, - cx: &mut Context, - ) { - self.remove_folds_with(ranges, auto_scroll, cx, |map, cx| { - map.unfold_intersecting(ranges.iter().cloned(), inclusive, cx) - }); - self.folds_did_change(cx); - } - - pub fn fold_buffer(&mut self, buffer_id: BufferId, cx: &mut Context) { - if self.buffer().read(cx).is_singleton() || self.is_buffer_folded(buffer_id, cx) { - return; - } - let folded_excerpts = self.buffer().read(cx).excerpts_for_buffer(buffer_id, cx); - self.display_map.update(cx, |display_map, cx| { - display_map.fold_buffers([buffer_id], cx) - }); - cx.emit(EditorEvent::BufferFoldToggled { - ids: folded_excerpts.iter().map(|&(id, _)| id).collect(), - folded: true, - }); - cx.notify(); - } - - pub fn unfold_buffer(&mut self, buffer_id: BufferId, cx: &mut Context) { - if self.buffer().read(cx).is_singleton() || !self.is_buffer_folded(buffer_id, cx) { - return; - } - let unfolded_excerpts = self.buffer().read(cx).excerpts_for_buffer(buffer_id, cx); - self.display_map.update(cx, |display_map, cx| { - display_map.unfold_buffers([buffer_id], cx); - }); - cx.emit(EditorEvent::BufferFoldToggled { - ids: unfolded_excerpts.iter().map(|&(id, _)| id).collect(), - folded: false, - }); - cx.notify(); - } - - pub fn is_buffer_folded(&self, buffer: BufferId, cx: &App) -> bool { - self.display_map.read(cx).is_buffer_folded(buffer) - } - - pub fn folded_buffers<'a>(&self, cx: &'a App) -> &'a HashSet { - self.display_map.read(cx).folded_buffers() - } - - pub fn disable_header_for_buffer(&mut self, buffer_id: BufferId, cx: &mut Context) { - self.display_map.update(cx, |display_map, cx| { - display_map.disable_header_for_buffer(buffer_id, cx); - }); - cx.notify(); - } - - /// Removes any folds with the given ranges. - pub fn remove_folds_with_type( - &mut self, - ranges: &[Range], - type_id: TypeId, - auto_scroll: bool, - cx: &mut Context, - ) { - self.remove_folds_with(ranges, auto_scroll, cx, |map, cx| { - map.remove_folds_with_type(ranges.iter().cloned(), type_id, cx) - }); - self.folds_did_change(cx); - } - - fn remove_folds_with( - &mut self, - ranges: &[Range], - auto_scroll: bool, - cx: &mut Context, - update: impl FnOnce(&mut DisplayMap, &mut Context), - ) { - if ranges.is_empty() { - return; - } - - let mut buffers_affected = HashSet::default(); - let multi_buffer = self.buffer().read(cx); - for range in ranges { - if let Some((_, buffer, _)) = multi_buffer.excerpt_containing(range.start.clone(), cx) { - buffers_affected.insert(buffer.read(cx).remote_id()); - }; - } - - self.display_map.update(cx, update); - - if auto_scroll { - self.request_autoscroll(Autoscroll::fit(), cx); - } - - cx.notify(); - self.scrollbar_marker_state.dirty = true; - self.active_indent_guides_state.dirty = true; - } - - pub fn update_fold_widths( - &mut self, - widths: impl IntoIterator, - cx: &mut Context, - ) -> bool { - self.display_map - .update(cx, |map, cx| map.update_fold_widths(widths, cx)) - } - - pub fn default_fold_placeholder(&self, cx: &App) -> FoldPlaceholder { - self.display_map.read(cx).fold_placeholder.clone() - } - - pub fn set_expand_all_diff_hunks(&mut self, cx: &mut App) { - self.buffer.update(cx, |buffer, cx| { - buffer.set_all_diff_hunks_expanded(cx); - }); - } - - pub fn expand_all_diff_hunks( - &mut self, - _: &ExpandAllDiffHunks, - _window: &mut Window, - cx: &mut Context, - ) { - self.buffer.update(cx, |buffer, cx| { - buffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx) - }); - } - - pub fn toggle_selected_diff_hunks( - &mut self, - _: &ToggleSelectedDiffHunks, - _window: &mut Window, - cx: &mut Context, - ) { - let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect(); - self.toggle_diff_hunks_in_ranges(ranges, cx); - } - - pub fn diff_hunks_in_ranges<'a>( - &'a self, - ranges: &'a [Range], - buffer: &'a MultiBufferSnapshot, - ) -> impl 'a + Iterator { - ranges.iter().flat_map(move |range| { - let end_excerpt_id = range.end.excerpt_id; - let range = range.to_point(buffer); - let mut peek_end = range.end; - if range.end.row < buffer.max_row().0 { - peek_end = Point::new(range.end.row + 1, 0); - } - buffer - .diff_hunks_in_range(range.start..peek_end) - .filter(move |hunk| hunk.excerpt_id.cmp(&end_excerpt_id, buffer).is_le()) - }) - } - - pub fn has_stageable_diff_hunks_in_ranges( - &self, - ranges: &[Range], - snapshot: &MultiBufferSnapshot, - ) -> bool { - let mut hunks = self.diff_hunks_in_ranges(ranges, &snapshot); - hunks.any(|hunk| hunk.status().has_secondary_hunk()) - } - - pub fn toggle_staged_selected_diff_hunks( - &mut self, - _: &::git::ToggleStaged, - _: &mut Window, - cx: &mut Context, - ) { - let snapshot = self.buffer.read(cx).snapshot(cx); - let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect(); - let stage = self.has_stageable_diff_hunks_in_ranges(&ranges, &snapshot); - self.stage_or_unstage_diff_hunks(stage, ranges, cx); - } - - pub fn set_render_diff_hunk_controls( - &mut self, - render_diff_hunk_controls: RenderDiffHunkControlsFn, - cx: &mut Context, - ) { - self.render_diff_hunk_controls = render_diff_hunk_controls; - cx.notify(); - } - - pub fn stage_and_next( - &mut self, - _: &::git::StageAndNext, - window: &mut Window, - cx: &mut Context, - ) { - self.do_stage_or_unstage_and_next(true, window, cx); - } - - pub fn unstage_and_next( - &mut self, - _: &::git::UnstageAndNext, - window: &mut Window, - cx: &mut Context, - ) { - self.do_stage_or_unstage_and_next(false, window, cx); - } - - pub fn stage_or_unstage_diff_hunks( - &mut self, - stage: bool, - ranges: Vec>, - cx: &mut Context, - ) { - let task = self.save_buffers_for_ranges_if_needed(&ranges, cx); - cx.spawn(async move |this, cx| { - task.await?; - this.update(cx, |this, cx| { - let snapshot = this.buffer.read(cx).snapshot(cx); - let chunk_by = this - .diff_hunks_in_ranges(&ranges, &snapshot) - .chunk_by(|hunk| hunk.buffer_id); - for (buffer_id, hunks) in &chunk_by { - this.do_stage_or_unstage(stage, buffer_id, hunks, cx); - } - }) - }) - .detach_and_log_err(cx); - } - - fn save_buffers_for_ranges_if_needed( - &mut self, - ranges: &[Range], - cx: &mut Context, - ) -> Task> { - let multibuffer = self.buffer.read(cx); - let snapshot = multibuffer.read(cx); - let buffer_ids: HashSet<_> = ranges - .iter() - .flat_map(|range| snapshot.buffer_ids_for_range(range.clone())) - .collect(); - drop(snapshot); - - let mut buffers = HashSet::default(); - for buffer_id in buffer_ids { - if let Some(buffer_entity) = multibuffer.buffer(buffer_id) { - let buffer = buffer_entity.read(cx); - if buffer.file().is_some_and(|file| file.disk_state().exists()) && buffer.is_dirty() - { - buffers.insert(buffer_entity); - } - } - } - - if let Some(project) = &self.project { - project.update(cx, |project, cx| project.save_buffers(buffers, cx)) - } else { - Task::ready(Ok(())) - } - } - - fn do_stage_or_unstage_and_next( - &mut self, - stage: bool, - window: &mut Window, - cx: &mut Context, - ) { - let ranges = self.selections.disjoint_anchor_ranges().collect::>(); - - if ranges.iter().any(|range| range.start != range.end) { - self.stage_or_unstage_diff_hunks(stage, ranges, cx); - return; - } - - self.stage_or_unstage_diff_hunks(stage, ranges, cx); - let snapshot = self.snapshot(window, cx); - let position = self.selections.newest::(cx).head(); - let mut row = snapshot - .buffer_snapshot - .diff_hunks_in_range(position..snapshot.buffer_snapshot.max_point()) - .find(|hunk| hunk.row_range.start.0 > position.row) - .map(|hunk| hunk.row_range.start); - - let all_diff_hunks_expanded = self.buffer().read(cx).all_diff_hunks_expanded(); - // Outside of the project diff editor, wrap around to the beginning. - if !all_diff_hunks_expanded { - row = row.or_else(|| { - snapshot - .buffer_snapshot - .diff_hunks_in_range(Point::zero()..position) - .find(|hunk| hunk.row_range.end.0 < position.row) - .map(|hunk| hunk.row_range.start) - }); - } - - if let Some(row) = row { - let destination = Point::new(row.0, 0); - let autoscroll = Autoscroll::center(); - - self.unfold_ranges(&[destination..destination], false, false, cx); - self.change_selections(Some(autoscroll), window, cx, |s| { - s.select_ranges([destination..destination]); - }); - } - } - - fn do_stage_or_unstage( - &self, - stage: bool, - buffer_id: BufferId, - hunks: impl Iterator, - cx: &mut App, - ) -> Option<()> { - let project = self.project.as_ref()?; - let buffer = project.read(cx).buffer_for_id(buffer_id, cx)?; - let diff = self.buffer.read(cx).diff_for(buffer_id)?; - let buffer_snapshot = buffer.read(cx).snapshot(); - let file_exists = buffer_snapshot - .file() - .is_some_and(|file| file.disk_state().exists()); - diff.update(cx, |diff, cx| { - diff.stage_or_unstage_hunks( - stage, - &hunks - .map(|hunk| buffer_diff::DiffHunk { - buffer_range: hunk.buffer_range, - diff_base_byte_range: hunk.diff_base_byte_range, - secondary_status: hunk.secondary_status, - range: Point::zero()..Point::zero(), // unused - }) - .collect::>(), - &buffer_snapshot, - file_exists, - cx, - ) - }); - None - } - - pub fn expand_selected_diff_hunks(&mut self, cx: &mut Context) { - let ranges: Vec<_> = self.selections.disjoint.iter().map(|s| s.range()).collect(); - self.buffer - .update(cx, |buffer, cx| buffer.expand_diff_hunks(ranges, cx)) - } - - pub fn clear_expanded_diff_hunks(&mut self, cx: &mut Context) -> bool { - self.buffer.update(cx, |buffer, cx| { - let ranges = vec![Anchor::min()..Anchor::max()]; - if !buffer.all_diff_hunks_expanded() - && buffer.has_expanded_diff_hunks_in_ranges(&ranges, cx) - { - buffer.collapse_diff_hunks(ranges, cx); - true - } else { - false - } - }) - } - - fn toggle_diff_hunks_in_ranges( - &mut self, - ranges: Vec>, - cx: &mut Context, - ) { - self.buffer.update(cx, |buffer, cx| { - let expand = !buffer.has_expanded_diff_hunks_in_ranges(&ranges, cx); - buffer.expand_or_collapse_diff_hunks(ranges, expand, cx); - }) - } - - fn toggle_single_diff_hunk(&mut self, range: Range, cx: &mut Context) { - self.buffer.update(cx, |buffer, cx| { - let snapshot = buffer.snapshot(cx); - let excerpt_id = range.end.excerpt_id; - let point_range = range.to_point(&snapshot); - let expand = !buffer.single_hunk_is_expanded(range, cx); - buffer.expand_or_collapse_diff_hunks_inner([(point_range, excerpt_id)], expand, cx); - }) - } - - pub(crate) fn apply_all_diff_hunks( - &mut self, - _: &ApplyAllDiffHunks, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - - let buffers = self.buffer.read(cx).all_buffers(); - for branch_buffer in buffers { - branch_buffer.update(cx, |branch_buffer, cx| { - branch_buffer.merge_into_base(Vec::new(), cx); - }); - } - - if let Some(project) = self.project.clone() { - self.save(true, project, window, cx).detach_and_log_err(cx); - } - } - - pub(crate) fn apply_selected_diff_hunks( - &mut self, - _: &ApplyDiffHunk, - window: &mut Window, - cx: &mut Context, - ) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - let snapshot = self.snapshot(window, cx); - let hunks = snapshot.hunks_for_ranges(self.selections.ranges(cx)); - let mut ranges_by_buffer = HashMap::default(); - self.transact(window, cx, |editor, _window, cx| { - for hunk in hunks { - if let Some(buffer) = editor.buffer.read(cx).buffer(hunk.buffer_id) { - ranges_by_buffer - .entry(buffer.clone()) - .or_insert_with(Vec::new) - .push(hunk.buffer_range.to_offset(buffer.read(cx))); - } - } - - for (buffer, ranges) in ranges_by_buffer { - buffer.update(cx, |buffer, cx| { - buffer.merge_into_base(ranges, cx); - }); - } - }); - - if let Some(project) = self.project.clone() { - self.save(true, project, window, cx).detach_and_log_err(cx); - } - } - - pub fn set_gutter_hovered(&mut self, hovered: bool, cx: &mut Context) { - if hovered != self.gutter_hovered { - self.gutter_hovered = hovered; - cx.notify(); - } - } - - pub fn insert_blocks( - &mut self, - blocks: impl IntoIterator>, - autoscroll: Option, - cx: &mut Context, - ) -> Vec { - let blocks = self - .display_map - .update(cx, |display_map, cx| display_map.insert_blocks(blocks, cx)); - if let Some(autoscroll) = autoscroll { - self.request_autoscroll(autoscroll, cx); - } - cx.notify(); - blocks - } - - pub fn resize_blocks( - &mut self, - heights: HashMap, - autoscroll: Option, - cx: &mut Context, - ) { - self.display_map - .update(cx, |display_map, cx| display_map.resize_blocks(heights, cx)); - if let Some(autoscroll) = autoscroll { - self.request_autoscroll(autoscroll, cx); - } - cx.notify(); - } - - pub fn replace_blocks( - &mut self, - renderers: HashMap, - autoscroll: Option, - cx: &mut Context, - ) { - self.display_map - .update(cx, |display_map, _cx| display_map.replace_blocks(renderers)); - if let Some(autoscroll) = autoscroll { - self.request_autoscroll(autoscroll, cx); - } - cx.notify(); - } - - pub fn remove_blocks( - &mut self, - block_ids: HashSet, - autoscroll: Option, - cx: &mut Context, - ) { - self.display_map.update(cx, |display_map, cx| { - display_map.remove_blocks(block_ids, cx) - }); - if let Some(autoscroll) = autoscroll { - self.request_autoscroll(autoscroll, cx); - } - cx.notify(); - } - - pub fn row_for_block( - &self, - block_id: CustomBlockId, - cx: &mut Context, - ) -> Option { - self.display_map - .update(cx, |map, cx| map.row_for_block(block_id, cx)) - } - - pub(crate) fn set_focused_block(&mut self, focused_block: FocusedBlock) { - self.focused_block = Some(focused_block); - } - - pub(crate) fn take_focused_block(&mut self) -> Option { - self.focused_block.take() - } - - pub fn insert_creases( - &mut self, - creases: impl IntoIterator>, - cx: &mut Context, - ) -> Vec { - self.display_map - .update(cx, |map, cx| map.insert_creases(creases, cx)) - } - - pub fn remove_creases( - &mut self, - ids: impl IntoIterator, - cx: &mut Context, - ) { - self.display_map - .update(cx, |map, cx| map.remove_creases(ids, cx)); - } - - pub fn longest_row(&self, cx: &mut App) -> DisplayRow { - self.display_map - .update(cx, |map, cx| map.snapshot(cx)) - .longest_row() - } - - pub fn max_point(&self, cx: &mut App) -> DisplayPoint { - self.display_map - .update(cx, |map, cx| map.snapshot(cx)) - .max_point() - } - - pub fn text(&self, cx: &App) -> String { - self.buffer.read(cx).read(cx).text() - } - - pub fn is_empty(&self, cx: &App) -> bool { - self.buffer.read(cx).read(cx).is_empty() - } - - pub fn text_option(&self, cx: &App) -> Option { - let text = self.text(cx); - let text = text.trim(); - - if text.is_empty() { - return None; - } - - Some(text.to_string()) - } - - pub fn set_text( - &mut self, - text: impl Into>, - window: &mut Window, - cx: &mut Context, - ) { - self.transact(window, cx, |this, _, cx| { - this.buffer - .read(cx) - .as_singleton() - .expect("you can only call set_text on editors for singleton buffers") - .update(cx, |buffer, cx| buffer.set_text(text, cx)); - }); - } - - pub fn display_text(&self, cx: &mut App) -> String { - self.display_map - .update(cx, |map, cx| map.snapshot(cx)) - .text() - } - - pub fn wrap_guides(&self, cx: &App) -> SmallVec<[(usize, bool); 2]> { - let mut wrap_guides = smallvec::smallvec![]; - - if self.show_wrap_guides == Some(false) { - return wrap_guides; - } - - let settings = self.buffer.read(cx).language_settings(cx); - if settings.show_wrap_guides { - match self.soft_wrap_mode(cx) { - SoftWrap::Column(soft_wrap) => { - wrap_guides.push((soft_wrap as usize, true)); - } - SoftWrap::Bounded(soft_wrap) => { - wrap_guides.push((soft_wrap as usize, true)); - } - SoftWrap::GitDiff | SoftWrap::None | SoftWrap::EditorWidth => {} - } - wrap_guides.extend(settings.wrap_guides.iter().map(|guide| (*guide, false))) - } - - wrap_guides - } - - pub fn soft_wrap_mode(&self, cx: &App) -> SoftWrap { - let settings = self.buffer.read(cx).language_settings(cx); - let mode = self.soft_wrap_mode_override.unwrap_or(settings.soft_wrap); - match mode { - language_settings::SoftWrap::PreferLine | language_settings::SoftWrap::None => { - SoftWrap::None - } - language_settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth, - language_settings::SoftWrap::PreferredLineLength => { - SoftWrap::Column(settings.preferred_line_length) - } - language_settings::SoftWrap::Bounded => { - SoftWrap::Bounded(settings.preferred_line_length) - } - } - } - - pub fn set_soft_wrap_mode( - &mut self, - mode: language_settings::SoftWrap, - - cx: &mut Context, - ) { - self.soft_wrap_mode_override = Some(mode); - cx.notify(); - } - - pub fn set_hard_wrap(&mut self, hard_wrap: Option, cx: &mut Context) { - self.hard_wrap = hard_wrap; - cx.notify(); - } - - pub fn set_text_style_refinement(&mut self, style: TextStyleRefinement) { - self.text_style_refinement = Some(style); - } - - /// called by the Element so we know what style we were most recently rendered with. - pub(crate) fn set_style( - &mut self, - style: EditorStyle, - window: &mut Window, - cx: &mut Context, - ) { - let rem_size = window.rem_size(); - self.display_map.update(cx, |map, cx| { - map.set_font( - style.text.font(), - style.text.font_size.to_pixels(rem_size), - cx, - ) - }); - self.style = Some(style); - } - - pub fn style(&self) -> Option<&EditorStyle> { - self.style.as_ref() - } - - // Called by the element. This method is not designed to be called outside of the editor - // element's layout code because it does not notify when rewrapping is computed synchronously. - pub(crate) fn set_wrap_width(&self, width: Option, cx: &mut App) -> bool { - self.display_map - .update(cx, |map, cx| map.set_wrap_width(width, cx)) - } - - pub fn set_soft_wrap(&mut self) { - self.soft_wrap_mode_override = Some(language_settings::SoftWrap::EditorWidth) - } - - pub fn toggle_soft_wrap(&mut self, _: &ToggleSoftWrap, _: &mut Window, cx: &mut Context) { - if self.soft_wrap_mode_override.is_some() { - self.soft_wrap_mode_override.take(); - } else { - let soft_wrap = match self.soft_wrap_mode(cx) { - SoftWrap::GitDiff => return, - SoftWrap::None => language_settings::SoftWrap::EditorWidth, - SoftWrap::EditorWidth | SoftWrap::Column(_) | SoftWrap::Bounded(_) => { - language_settings::SoftWrap::None - } - }; - self.soft_wrap_mode_override = Some(soft_wrap); - } - cx.notify(); - } - - pub fn toggle_tab_bar(&mut self, _: &ToggleTabBar, _: &mut Window, cx: &mut Context) { - let Some(workspace) = self.workspace() else { - return; - }; - let fs = workspace.read(cx).app_state().fs.clone(); - let current_show = TabBarSettings::get_global(cx).show; - update_settings_file::(fs, cx, move |setting, _| { - setting.show = Some(!current_show); - }); - } - - pub fn toggle_indent_guides( - &mut self, - _: &ToggleIndentGuides, - _: &mut Window, - cx: &mut Context, - ) { - let currently_enabled = self.should_show_indent_guides().unwrap_or_else(|| { - self.buffer - .read(cx) - .language_settings(cx) - .indent_guides - .enabled - }); - self.show_indent_guides = Some(!currently_enabled); - cx.notify(); - } - - fn should_show_indent_guides(&self) -> Option { - self.show_indent_guides - } - - pub fn toggle_line_numbers( - &mut self, - _: &ToggleLineNumbers, - _: &mut Window, - cx: &mut Context, - ) { - let mut editor_settings = EditorSettings::get_global(cx).clone(); - editor_settings.gutter.line_numbers = !editor_settings.gutter.line_numbers; - EditorSettings::override_global(editor_settings, cx); - } - - pub fn line_numbers_enabled(&self, cx: &App) -> bool { - if let Some(show_line_numbers) = self.show_line_numbers { - return show_line_numbers; - } - EditorSettings::get_global(cx).gutter.line_numbers - } - - pub fn should_use_relative_line_numbers(&self, cx: &mut App) -> bool { - self.use_relative_line_numbers - .unwrap_or(EditorSettings::get_global(cx).relative_line_numbers) - } - - pub fn toggle_relative_line_numbers( - &mut self, - _: &ToggleRelativeLineNumbers, - _: &mut Window, - cx: &mut Context, - ) { - let is_relative = self.should_use_relative_line_numbers(cx); - self.set_relative_line_number(Some(!is_relative), cx) - } - - pub fn set_relative_line_number(&mut self, is_relative: Option, cx: &mut Context) { - self.use_relative_line_numbers = is_relative; - cx.notify(); - } - - pub fn set_show_gutter(&mut self, show_gutter: bool, cx: &mut Context) { - self.show_gutter = show_gutter; - cx.notify(); - } - - pub fn set_show_scrollbars(&mut self, show_scrollbars: bool, cx: &mut Context) { - self.show_scrollbars = show_scrollbars; - cx.notify(); - } - - pub fn disable_scrolling(&mut self, cx: &mut Context) { - self.disable_scrolling = true; - cx.notify(); - } - - pub fn set_show_line_numbers(&mut self, show_line_numbers: bool, cx: &mut Context) { - self.show_line_numbers = Some(show_line_numbers); - cx.notify(); - } - - pub fn disable_expand_excerpt_buttons(&mut self, cx: &mut Context) { - self.disable_expand_excerpt_buttons = true; - cx.notify(); - } - - pub fn set_show_git_diff_gutter(&mut self, show_git_diff_gutter: bool, cx: &mut Context) { - self.show_git_diff_gutter = Some(show_git_diff_gutter); - cx.notify(); - } - - pub fn set_show_code_actions(&mut self, show_code_actions: bool, cx: &mut Context) { - self.show_code_actions = Some(show_code_actions); - cx.notify(); - } - - pub fn set_show_runnables(&mut self, show_runnables: bool, cx: &mut Context) { - self.show_runnables = Some(show_runnables); - cx.notify(); - } - - pub fn set_show_breakpoints(&mut self, show_breakpoints: bool, cx: &mut Context) { - self.show_breakpoints = Some(show_breakpoints); - cx.notify(); - } - - pub fn set_masked(&mut self, masked: bool, cx: &mut Context) { - if self.display_map.read(cx).masked != masked { - self.display_map.update(cx, |map, _| map.masked = masked); - } - cx.notify() - } - - pub fn set_show_wrap_guides(&mut self, show_wrap_guides: bool, cx: &mut Context) { - self.show_wrap_guides = Some(show_wrap_guides); - cx.notify(); - } - - pub fn set_show_indent_guides(&mut self, show_indent_guides: bool, cx: &mut Context) { - self.show_indent_guides = Some(show_indent_guides); - cx.notify(); - } - - pub fn working_directory(&self, cx: &App) -> Option { - if let Some(buffer) = self.buffer().read(cx).as_singleton() { - if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) { - if let Some(dir) = file.abs_path(cx).parent() { - return Some(dir.to_owned()); - } - } - - if let Some(project_path) = buffer.read(cx).project_path(cx) { - return Some(project_path.path.to_path_buf()); - } - } - - None - } - - fn target_file<'a>(&self, cx: &'a App) -> Option<&'a dyn language::LocalFile> { - self.active_excerpt(cx)? - .1 - .read(cx) - .file() - .and_then(|f| f.as_local()) - } - - pub fn target_file_abs_path(&self, cx: &mut Context) -> Option { - self.active_excerpt(cx).and_then(|(_, buffer, _)| { - let buffer = buffer.read(cx); - if let Some(project_path) = buffer.project_path(cx) { - let project = self.project.as_ref()?.read(cx); - project.absolute_path(&project_path, cx) - } else { - buffer - .file() - .and_then(|file| file.as_local().map(|file| file.abs_path(cx))) - } - }) - } - - fn target_file_path(&self, cx: &mut Context) -> Option { - self.active_excerpt(cx).and_then(|(_, buffer, _)| { - let project_path = buffer.read(cx).project_path(cx)?; - let project = self.project.as_ref()?.read(cx); - let entry = project.entry_for_path(&project_path, cx)?; - let path = entry.path.to_path_buf(); - Some(path) - }) - } - - pub fn reveal_in_finder( - &mut self, - _: &RevealInFileManager, - _window: &mut Window, - cx: &mut Context, - ) { - if let Some(target) = self.target_file(cx) { - cx.reveal_path(&target.abs_path(cx)); - } - } - - pub fn copy_path( - &mut self, - _: &zed_actions::workspace::CopyPath, - _window: &mut Window, - cx: &mut Context, - ) { - if let Some(path) = self.target_file_abs_path(cx) { - if let Some(path) = path.to_str() { - cx.write_to_clipboard(ClipboardItem::new_string(path.to_string())); - } - } - } - - pub fn copy_relative_path( - &mut self, - _: &zed_actions::workspace::CopyRelativePath, - _window: &mut Window, - cx: &mut Context, - ) { - if let Some(path) = self.target_file_path(cx) { - if let Some(path) = path.to_str() { - cx.write_to_clipboard(ClipboardItem::new_string(path.to_string())); - } - } - } - - pub fn project_path(&self, cx: &App) -> Option { - if let Some(buffer) = self.buffer.read(cx).as_singleton() { - buffer.read(cx).project_path(cx) - } else { - None - } - } - - // Returns true if the editor handled a go-to-line request - pub fn go_to_active_debug_line(&mut self, window: &mut Window, cx: &mut Context) -> bool { - maybe!({ - let breakpoint_store = self.breakpoint_store.as_ref()?; - - let Some(active_stack_frame) = breakpoint_store.read(cx).active_position().cloned() - else { - self.clear_row_highlights::(); - return None; - }; - - let position = active_stack_frame.position; - let buffer_id = position.buffer_id?; - let snapshot = self - .project - .as_ref()? - .read(cx) - .buffer_for_id(buffer_id, cx)? - .read(cx) - .snapshot(); - - let mut handled = false; - for (id, ExcerptRange { context, .. }) in - self.buffer.read(cx).excerpts_for_buffer(buffer_id, cx) - { - if context.start.cmp(&position, &snapshot).is_ge() - || context.end.cmp(&position, &snapshot).is_lt() - { - continue; - } - let snapshot = self.buffer.read(cx).snapshot(cx); - let multibuffer_anchor = snapshot.anchor_in_excerpt(id, position)?; - - handled = true; - self.clear_row_highlights::(); - self.go_to_line::( - multibuffer_anchor, - Some(cx.theme().colors().editor_debugger_active_line_background), - window, - cx, - ); - - cx.notify(); - } - - handled.then_some(()) - }) - .is_some() - } - - pub fn copy_file_name_without_extension( - &mut self, - _: &CopyFileNameWithoutExtension, - _: &mut Window, - cx: &mut Context, - ) { - if let Some(file) = self.target_file(cx) { - if let Some(file_stem) = file.path().file_stem() { - if let Some(name) = file_stem.to_str() { - cx.write_to_clipboard(ClipboardItem::new_string(name.to_string())); - } - } - } - } - - pub fn copy_file_name(&mut self, _: &CopyFileName, _: &mut Window, cx: &mut Context) { - if let Some(file) = self.target_file(cx) { - if let Some(file_name) = file.path().file_name() { - if let Some(name) = file_name.to_str() { - cx.write_to_clipboard(ClipboardItem::new_string(name.to_string())); - } - } - } - } - - pub fn toggle_git_blame( - &mut self, - _: &::git::Blame, - window: &mut Window, - cx: &mut Context, - ) { - self.show_git_blame_gutter = !self.show_git_blame_gutter; - - if self.show_git_blame_gutter && !self.has_blame_entries(cx) { - self.start_git_blame(true, window, cx); - } - - cx.notify(); - } - - pub fn toggle_git_blame_inline( - &mut self, - _: &ToggleGitBlameInline, - window: &mut Window, - cx: &mut Context, - ) { - self.toggle_git_blame_inline_internal(true, window, cx); - cx.notify(); - } - - pub fn open_git_blame_commit( - &mut self, - _: &OpenGitBlameCommit, - window: &mut Window, - cx: &mut Context, - ) { - self.open_git_blame_commit_internal(window, cx); - } - - fn open_git_blame_commit_internal( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> Option<()> { - let blame = self.blame.as_ref()?; - let snapshot = self.snapshot(window, cx); - let cursor = self.selections.newest::(cx).head(); - let (buffer, point, _) = snapshot.buffer_snapshot.point_to_buffer_point(cursor)?; - let blame_entry = blame - .update(cx, |blame, cx| { - blame - .blame_for_rows( - &[RowInfo { - buffer_id: Some(buffer.remote_id()), - buffer_row: Some(point.row), - ..Default::default() - }], - cx, - ) - .next() - }) - .flatten()?; - let renderer = cx.global::().0.clone(); - let repo = blame.read(cx).repository(cx)?; - let workspace = self.workspace()?.downgrade(); - renderer.open_blame_commit(blame_entry, repo, workspace, window, cx); - None - } - - pub fn git_blame_inline_enabled(&self) -> bool { - self.git_blame_inline_enabled - } - - pub fn toggle_selection_menu( - &mut self, - _: &ToggleSelectionMenu, - _: &mut Window, - cx: &mut Context, - ) { - self.show_selection_menu = self - .show_selection_menu - .map(|show_selections_menu| !show_selections_menu) - .or_else(|| Some(!EditorSettings::get_global(cx).toolbar.selections_menu)); - - cx.notify(); - } - - pub fn selection_menu_enabled(&self, cx: &App) -> bool { - self.show_selection_menu - .unwrap_or_else(|| EditorSettings::get_global(cx).toolbar.selections_menu) - } - - fn start_git_blame( - &mut self, - user_triggered: bool, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(project) = self.project.as_ref() { - let Some(buffer) = self.buffer().read(cx).as_singleton() else { - return; - }; - - if buffer.read(cx).file().is_none() { - return; - } - - let focused = self.focus_handle(cx).contains_focused(window, cx); - - let project = project.clone(); - let blame = cx.new(|cx| GitBlame::new(buffer, project, user_triggered, focused, cx)); - self.blame_subscription = - Some(cx.observe_in(&blame, window, |_, _, _, cx| cx.notify())); - self.blame = Some(blame); - } - } - - fn toggle_git_blame_inline_internal( - &mut self, - user_triggered: bool, - window: &mut Window, - cx: &mut Context, - ) { - if self.git_blame_inline_enabled { - self.git_blame_inline_enabled = false; - self.show_git_blame_inline = false; - self.show_git_blame_inline_delay_task.take(); - } else { - self.git_blame_inline_enabled = true; - self.start_git_blame_inline(user_triggered, window, cx); - } - - cx.notify(); - } - - fn start_git_blame_inline( - &mut self, - user_triggered: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.start_git_blame(user_triggered, window, cx); - - if ProjectSettings::get_global(cx) - .git - .inline_blame_delay() - .is_some() - { - self.start_inline_blame_timer(window, cx); - } else { - self.show_git_blame_inline = true - } - } - - pub fn blame(&self) -> Option<&Entity> { - self.blame.as_ref() - } - - pub fn show_git_blame_gutter(&self) -> bool { - self.show_git_blame_gutter - } - - pub fn render_git_blame_gutter(&self, cx: &App) -> bool { - self.show_git_blame_gutter && self.has_blame_entries(cx) - } - - pub fn render_git_blame_inline(&self, window: &Window, cx: &App) -> bool { - self.show_git_blame_inline - && (self.focus_handle.is_focused(window) || self.inline_blame_popover.is_some()) - && !self.newest_selection_head_on_empty_line(cx) - && self.has_blame_entries(cx) - } - - fn has_blame_entries(&self, cx: &App) -> bool { - self.blame() - .map_or(false, |blame| blame.read(cx).has_generated_entries()) - } - - fn newest_selection_head_on_empty_line(&self, cx: &App) -> bool { - let cursor_anchor = self.selections.newest_anchor().head(); - - let snapshot = self.buffer.read(cx).snapshot(cx); - let buffer_row = MultiBufferRow(cursor_anchor.to_point(&snapshot).row); - - snapshot.line_len(buffer_row) == 0 - } - - fn get_permalink_to_line(&self, cx: &mut Context) -> Task> { - let buffer_and_selection = maybe!({ - let selection = self.selections.newest::(cx); - let selection_range = selection.range(); - - let multi_buffer = self.buffer().read(cx); - let multi_buffer_snapshot = multi_buffer.snapshot(cx); - let buffer_ranges = multi_buffer_snapshot.range_to_buffer_ranges(selection_range); - - let (buffer, range, _) = if selection.reversed { - buffer_ranges.first() - } else { - buffer_ranges.last() - }?; - - let selection = text::ToPoint::to_point(&range.start, &buffer).row - ..text::ToPoint::to_point(&range.end, &buffer).row; - Some(( - multi_buffer.buffer(buffer.remote_id()).unwrap().clone(), - selection, - )) - }); - - let Some((buffer, selection)) = buffer_and_selection else { - return Task::ready(Err(anyhow!("failed to determine buffer and selection"))); - }; - - let Some(project) = self.project.as_ref() else { - return Task::ready(Err(anyhow!("editor does not have project"))); - }; - - project.update(cx, |project, cx| { - project.get_permalink_to_line(&buffer, selection, cx) - }) - } - - pub fn copy_permalink_to_line( - &mut self, - _: &CopyPermalinkToLine, - window: &mut Window, - cx: &mut Context, - ) { - let permalink_task = self.get_permalink_to_line(cx); - let workspace = self.workspace(); - - cx.spawn_in(window, async move |_, cx| match permalink_task.await { - Ok(permalink) => { - cx.update(|_, cx| { - cx.write_to_clipboard(ClipboardItem::new_string(permalink.to_string())); - }) - .ok(); - } - Err(err) => { - let message = format!("Failed to copy permalink: {err}"); - - anyhow::Result::<()>::Err(err).log_err(); - - if let Some(workspace) = workspace { - workspace - .update_in(cx, |workspace, _, cx| { - struct CopyPermalinkToLine; - - workspace.show_toast( - Toast::new( - NotificationId::unique::(), - message, - ), - cx, - ) - }) - .ok(); - } - } - }) - .detach(); - } - - pub fn copy_file_location( - &mut self, - _: &CopyFileLocation, - _: &mut Window, - cx: &mut Context, - ) { - let selection = self.selections.newest::(cx).start.row + 1; - if let Some(file) = self.target_file(cx) { - if let Some(path) = file.path().to_str() { - cx.write_to_clipboard(ClipboardItem::new_string(format!("{path}:{selection}"))); - } - } - } - - pub fn open_permalink_to_line( - &mut self, - _: &OpenPermalinkToLine, - window: &mut Window, - cx: &mut Context, - ) { - let permalink_task = self.get_permalink_to_line(cx); - let workspace = self.workspace(); - - cx.spawn_in(window, async move |_, cx| match permalink_task.await { - Ok(permalink) => { - cx.update(|_, cx| { - cx.open_url(permalink.as_ref()); - }) - .ok(); - } - Err(err) => { - let message = format!("Failed to open permalink: {err}"); - - anyhow::Result::<()>::Err(err).log_err(); - - if let Some(workspace) = workspace { - workspace - .update(cx, |workspace, cx| { - struct OpenPermalinkToLine; - - workspace.show_toast( - Toast::new( - NotificationId::unique::(), - message, - ), - cx, - ) - }) - .ok(); - } - } - }) - .detach(); - } - - pub fn insert_uuid_v4( - &mut self, - _: &InsertUuidV4, - window: &mut Window, - cx: &mut Context, - ) { - self.insert_uuid(UuidVersion::V4, window, cx); - } - - pub fn insert_uuid_v7( - &mut self, - _: &InsertUuidV7, - window: &mut Window, - cx: &mut Context, - ) { - self.insert_uuid(UuidVersion::V7, window, cx); - } - - fn insert_uuid(&mut self, version: UuidVersion, window: &mut Window, cx: &mut Context) { - self.hide_mouse_cursor(&HideMouseCursorOrigin::TypingAction); - self.transact(window, cx, |this, window, cx| { - let edits = this - .selections - .all::(cx) - .into_iter() - .map(|selection| { - let uuid = match version { - UuidVersion::V4 => uuid::Uuid::new_v4(), - UuidVersion::V7 => uuid::Uuid::now_v7(), - }; - - (selection.range(), uuid.to_string()) - }); - this.edit(edits, cx); - this.refresh_inline_completion(true, false, window, cx); - }); - } - - pub fn open_selections_in_multibuffer( - &mut self, - _: &OpenSelectionsInMultibuffer, - window: &mut Window, - cx: &mut Context, - ) { - let multibuffer = self.buffer.read(cx); - - let Some(buffer) = multibuffer.as_singleton() else { - return; - }; - - let Some(workspace) = self.workspace() else { - return; - }; - - let locations = self - .selections - .disjoint_anchors() - .iter() - .map(|range| Location { - buffer: buffer.clone(), - range: range.start.text_anchor..range.end.text_anchor, - }) - .collect::>(); - - let title = multibuffer.title(cx).to_string(); - - cx.spawn_in(window, async move |_, cx| { - workspace.update_in(cx, |workspace, window, cx| { - Self::open_locations_in_multibuffer( - workspace, - locations, - format!("Selections for '{title}'"), - false, - MultibufferSelectionMode::All, - window, - cx, - ); - }) - }) - .detach(); - } - - /// Adds a row highlight for the given range. If a row has multiple highlights, the - /// last highlight added will be used. - /// - /// If the range ends at the beginning of a line, then that line will not be highlighted. - pub fn highlight_rows( - &mut self, - range: Range, - color: Hsla, - options: RowHighlightOptions, - cx: &mut Context, - ) { - let snapshot = self.buffer().read(cx).snapshot(cx); - let row_highlights = self.highlighted_rows.entry(TypeId::of::()).or_default(); - let ix = row_highlights.binary_search_by(|highlight| { - Ordering::Equal - .then_with(|| highlight.range.start.cmp(&range.start, &snapshot)) - .then_with(|| highlight.range.end.cmp(&range.end, &snapshot)) - }); - - if let Err(mut ix) = ix { - let index = post_inc(&mut self.highlight_order); - - // If this range intersects with the preceding highlight, then merge it with - // the preceding highlight. Otherwise insert a new highlight. - let mut merged = false; - if ix > 0 { - let prev_highlight = &mut row_highlights[ix - 1]; - if prev_highlight - .range - .end - .cmp(&range.start, &snapshot) - .is_ge() - { - ix -= 1; - if prev_highlight.range.end.cmp(&range.end, &snapshot).is_lt() { - prev_highlight.range.end = range.end; - } - merged = true; - prev_highlight.index = index; - prev_highlight.color = color; - prev_highlight.options = options; - } - } - - if !merged { - row_highlights.insert( - ix, - RowHighlight { - range: range.clone(), - index, - color, - options, - type_id: TypeId::of::(), - }, - ); - } - - // If any of the following highlights intersect with this one, merge them. - while let Some(next_highlight) = row_highlights.get(ix + 1) { - let highlight = &row_highlights[ix]; - if next_highlight - .range - .start - .cmp(&highlight.range.end, &snapshot) - .is_le() - { - if next_highlight - .range - .end - .cmp(&highlight.range.end, &snapshot) - .is_gt() - { - row_highlights[ix].range.end = next_highlight.range.end; - } - row_highlights.remove(ix + 1); - } else { - break; - } - } - } - } - - /// Remove any highlighted row ranges of the given type that intersect the - /// given ranges. - pub fn remove_highlighted_rows( - &mut self, - ranges_to_remove: Vec>, - cx: &mut Context, - ) { - let snapshot = self.buffer().read(cx).snapshot(cx); - let row_highlights = self.highlighted_rows.entry(TypeId::of::()).or_default(); - let mut ranges_to_remove = ranges_to_remove.iter().peekable(); - row_highlights.retain(|highlight| { - while let Some(range_to_remove) = ranges_to_remove.peek() { - match range_to_remove.end.cmp(&highlight.range.start, &snapshot) { - Ordering::Less | Ordering::Equal => { - ranges_to_remove.next(); - } - Ordering::Greater => { - match range_to_remove.start.cmp(&highlight.range.end, &snapshot) { - Ordering::Less | Ordering::Equal => { - return false; - } - Ordering::Greater => break, - } - } - } - } - - true - }) - } - - /// Clear all anchor ranges for a certain highlight context type, so no corresponding rows will be highlighted. - pub fn clear_row_highlights(&mut self) { - self.highlighted_rows.remove(&TypeId::of::()); - } - - /// For a highlight given context type, gets all anchor ranges that will be used for row highlighting. - pub fn highlighted_rows(&self) -> impl '_ + Iterator, Hsla)> { - self.highlighted_rows - .get(&TypeId::of::()) - .map_or(&[] as &[_], |vec| vec.as_slice()) - .iter() - .map(|highlight| (highlight.range.clone(), highlight.color)) - } - - /// Merges all anchor ranges for all context types ever set, picking the last highlight added in case of a row conflict. - /// Returns a map of display rows that are highlighted and their corresponding highlight color. - /// Allows to ignore certain kinds of highlights. - pub fn highlighted_display_rows( - &self, - window: &mut Window, - cx: &mut App, - ) -> BTreeMap { - let snapshot = self.snapshot(window, cx); - let mut used_highlight_orders = HashMap::default(); - self.highlighted_rows - .iter() - .flat_map(|(_, highlighted_rows)| highlighted_rows.iter()) - .fold( - BTreeMap::::new(), - |mut unique_rows, highlight| { - let start = highlight.range.start.to_display_point(&snapshot); - let end = highlight.range.end.to_display_point(&snapshot); - let start_row = start.row().0; - let end_row = if highlight.range.end.text_anchor != text::Anchor::MAX - && end.column() == 0 - { - end.row().0.saturating_sub(1) - } else { - end.row().0 - }; - for row in start_row..=end_row { - let used_index = - used_highlight_orders.entry(row).or_insert(highlight.index); - if highlight.index >= *used_index { - *used_index = highlight.index; - unique_rows.insert( - DisplayRow(row), - LineHighlight { - include_gutter: highlight.options.include_gutter, - border: None, - background: highlight.color.into(), - type_id: Some(highlight.type_id), - }, - ); - } - } - unique_rows - }, - ) - } - - pub fn highlighted_display_row_for_autoscroll( - &self, - snapshot: &DisplaySnapshot, - ) -> Option { - self.highlighted_rows - .values() - .flat_map(|highlighted_rows| highlighted_rows.iter()) - .filter_map(|highlight| { - if highlight.options.autoscroll { - Some(highlight.range.start.to_display_point(snapshot).row()) - } else { - None - } - }) - .min() - } - - pub fn set_search_within_ranges(&mut self, ranges: &[Range], cx: &mut Context) { - self.highlight_background::( - ranges, - |colors| colors.editor_document_highlight_read_background, - cx, - ) - } - - pub fn set_breadcrumb_header(&mut self, new_header: String) { - self.breadcrumb_header = Some(new_header); - } - - pub fn clear_search_within_ranges(&mut self, cx: &mut Context) { - self.clear_background_highlights::(cx); - } - - pub fn highlight_background( - &mut self, - ranges: &[Range], - color_fetcher: fn(&ThemeColors) -> Hsla, - cx: &mut Context, - ) { - self.background_highlights - .insert(TypeId::of::(), (color_fetcher, Arc::from(ranges))); - self.scrollbar_marker_state.dirty = true; - cx.notify(); - } - - pub fn clear_background_highlights( - &mut self, - cx: &mut Context, - ) -> Option { - let text_highlights = self.background_highlights.remove(&TypeId::of::())?; - if !text_highlights.1.is_empty() { - self.scrollbar_marker_state.dirty = true; - cx.notify(); - } - Some(text_highlights) - } - - pub fn highlight_gutter( - &mut self, - ranges: &[Range], - color_fetcher: fn(&App) -> Hsla, - cx: &mut Context, - ) { - self.gutter_highlights - .insert(TypeId::of::(), (color_fetcher, Arc::from(ranges))); - cx.notify(); - } - - pub fn clear_gutter_highlights( - &mut self, - cx: &mut Context, - ) -> Option { - cx.notify(); - self.gutter_highlights.remove(&TypeId::of::()) - } - - #[cfg(feature = "test-support")] - pub fn all_text_background_highlights( - &self, - window: &mut Window, - cx: &mut Context, - ) -> Vec<(Range, Hsla)> { - let snapshot = self.snapshot(window, cx); - let buffer = &snapshot.buffer_snapshot; - let start = buffer.anchor_before(0); - let end = buffer.anchor_after(buffer.len()); - let theme = cx.theme().colors(); - self.background_highlights_in_range(start..end, &snapshot, theme) - } - - #[cfg(feature = "test-support")] - pub fn search_background_highlights(&mut self, cx: &mut Context) -> Vec> { - let snapshot = self.buffer().read(cx).snapshot(cx); - - let highlights = self - .background_highlights - .get(&TypeId::of::()); - - if let Some((_color, ranges)) = highlights { - ranges - .iter() - .map(|range| range.start.to_point(&snapshot)..range.end.to_point(&snapshot)) - .collect_vec() - } else { - vec![] - } - } - - fn document_highlights_for_position<'a>( - &'a self, - position: Anchor, - buffer: &'a MultiBufferSnapshot, - ) -> impl 'a + Iterator> { - let read_highlights = self - .background_highlights - .get(&TypeId::of::()) - .map(|h| &h.1); - let write_highlights = self - .background_highlights - .get(&TypeId::of::()) - .map(|h| &h.1); - let left_position = position.bias_left(buffer); - let right_position = position.bias_right(buffer); - read_highlights - .into_iter() - .chain(write_highlights) - .flat_map(move |ranges| { - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&left_position, buffer); - if cmp.is_ge() { - Ordering::Greater - } else { - Ordering::Less - } - }) { - Ok(i) | Err(i) => i, - }; - - ranges[start_ix..] - .iter() - .take_while(move |range| range.start.cmp(&right_position, buffer).is_le()) - }) - } - - pub fn has_background_highlights(&self) -> bool { - self.background_highlights - .get(&TypeId::of::()) - .map_or(false, |(_, highlights)| !highlights.is_empty()) - } - - pub fn background_highlights_in_range( - &self, - search_range: Range, - display_snapshot: &DisplaySnapshot, - theme: &ThemeColors, - ) -> Vec<(Range, Hsla)> { - let mut results = Vec::new(); - for (color_fetcher, ranges) in self.background_highlights.values() { - let color = color_fetcher(theme); - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe - .end - .cmp(&search_range.start, &display_snapshot.buffer_snapshot); - if cmp.is_gt() { - Ordering::Greater - } else { - Ordering::Less - } - }) { - Ok(i) | Err(i) => i, - }; - for range in &ranges[start_ix..] { - if range - .start - .cmp(&search_range.end, &display_snapshot.buffer_snapshot) - .is_ge() - { - break; - } - - let start = range.start.to_display_point(display_snapshot); - let end = range.end.to_display_point(display_snapshot); - results.push((start..end, color)) - } - } - results - } - - pub fn background_highlight_row_ranges( - &self, - search_range: Range, - display_snapshot: &DisplaySnapshot, - count: usize, - ) -> Vec> { - let mut results = Vec::new(); - let Some((_, ranges)) = self.background_highlights.get(&TypeId::of::()) else { - return vec![]; - }; - - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe - .end - .cmp(&search_range.start, &display_snapshot.buffer_snapshot); - if cmp.is_gt() { - Ordering::Greater - } else { - Ordering::Less - } - }) { - Ok(i) | Err(i) => i, - }; - let mut push_region = |start: Option, end: Option| { - if let (Some(start_display), Some(end_display)) = (start, end) { - results.push( - start_display.to_display_point(display_snapshot) - ..=end_display.to_display_point(display_snapshot), - ); - } - }; - let mut start_row: Option = None; - let mut end_row: Option = None; - if ranges.len() > count { - return Vec::new(); - } - for range in &ranges[start_ix..] { - if range - .start - .cmp(&search_range.end, &display_snapshot.buffer_snapshot) - .is_ge() - { - break; - } - let end = range.end.to_point(&display_snapshot.buffer_snapshot); - if let Some(current_row) = &end_row { - if end.row == current_row.row { - continue; - } - } - let start = range.start.to_point(&display_snapshot.buffer_snapshot); - if start_row.is_none() { - assert_eq!(end_row, None); - start_row = Some(start); - end_row = Some(end); - continue; - } - if let Some(current_end) = end_row.as_mut() { - if start.row > current_end.row + 1 { - push_region(start_row, end_row); - start_row = Some(start); - end_row = Some(end); - } else { - // Merge two hunks. - *current_end = end; - } - } else { - unreachable!(); - } - } - // We might still have a hunk that was not rendered (if there was a search hit on the last line) - push_region(start_row, end_row); - results - } - - pub fn gutter_highlights_in_range( - &self, - search_range: Range, - display_snapshot: &DisplaySnapshot, - cx: &App, - ) -> Vec<(Range, Hsla)> { - let mut results = Vec::new(); - for (color_fetcher, ranges) in self.gutter_highlights.values() { - let color = color_fetcher(cx); - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe - .end - .cmp(&search_range.start, &display_snapshot.buffer_snapshot); - if cmp.is_gt() { - Ordering::Greater - } else { - Ordering::Less - } - }) { - Ok(i) | Err(i) => i, - }; - for range in &ranges[start_ix..] { - if range - .start - .cmp(&search_range.end, &display_snapshot.buffer_snapshot) - .is_ge() - { - break; - } - - let start = range.start.to_display_point(display_snapshot); - let end = range.end.to_display_point(display_snapshot); - results.push((start..end, color)) - } - } - results - } - - /// Get the text ranges corresponding to the redaction query - pub fn redacted_ranges( - &self, - search_range: Range, - display_snapshot: &DisplaySnapshot, - cx: &App, - ) -> Vec> { - display_snapshot - .buffer_snapshot - .redacted_ranges(search_range, |file| { - if let Some(file) = file { - file.is_private() - && EditorSettings::get( - Some(SettingsLocation { - worktree_id: file.worktree_id(cx), - path: file.path().as_ref(), - }), - cx, - ) - .redact_private_values - } else { - false - } - }) - .map(|range| { - range.start.to_display_point(display_snapshot) - ..range.end.to_display_point(display_snapshot) - }) - .collect() - } - - pub fn highlight_text( - &mut self, - ranges: Vec>, - style: HighlightStyle, - cx: &mut Context, - ) { - self.display_map.update(cx, |map, _| { - map.highlight_text(TypeId::of::(), ranges, style) - }); - cx.notify(); - } - - pub(crate) fn highlight_inlays( - &mut self, - highlights: Vec, - style: HighlightStyle, - cx: &mut Context, - ) { - self.display_map.update(cx, |map, _| { - map.highlight_inlays(TypeId::of::(), highlights, style) - }); - cx.notify(); - } - - pub fn text_highlights<'a, T: 'static>( - &'a self, - cx: &'a App, - ) -> Option<(HighlightStyle, &'a [Range])> { - self.display_map.read(cx).text_highlights(TypeId::of::()) - } - - pub fn clear_highlights(&mut self, cx: &mut Context) { - let cleared = self - .display_map - .update(cx, |map, _| map.clear_highlights(TypeId::of::())); - if cleared { - cx.notify(); - } - } - - pub fn show_local_cursors(&self, window: &mut Window, cx: &mut App) -> bool { - (self.read_only(cx) || self.blink_manager.read(cx).visible()) - && self.focus_handle.is_focused(window) - } - - pub fn set_show_cursor_when_unfocused(&mut self, is_enabled: bool, cx: &mut Context) { - self.show_cursor_when_unfocused = is_enabled; - cx.notify(); - } - - fn on_buffer_changed(&mut self, _: Entity, cx: &mut Context) { - cx.notify(); - } - - fn on_debug_session_event( - &mut self, - _session: Entity, - event: &SessionEvent, - cx: &mut Context, - ) { - match event { - SessionEvent::InvalidateInlineValue => { - self.refresh_inline_values(cx); - } - _ => {} - } - } - - fn refresh_inline_values(&mut self, cx: &mut Context) { - let Some(project) = self.project.clone() else { - return; - }; - let Some(buffer) = self.buffer.read(cx).as_singleton() else { - return; - }; - if !self.inline_value_cache.enabled { - let inlays = std::mem::take(&mut self.inline_value_cache.inlays); - self.splice_inlays(&inlays, Vec::new(), cx); - return; - } - - let current_execution_position = self - .highlighted_rows - .get(&TypeId::of::()) - .and_then(|lines| lines.last().map(|line| line.range.start)); - - self.inline_value_cache.refresh_task = cx.spawn(async move |editor, cx| { - let snapshot = editor - .update(cx, |editor, cx| editor.buffer().read(cx).snapshot(cx)) - .ok()?; - - let inline_values = editor - .update(cx, |_, cx| { - let Some(current_execution_position) = current_execution_position else { - return Some(Task::ready(Ok(Vec::new()))); - }; - - // todo(debugger) when introducing multi buffer inline values check execution position's buffer id to make sure the text - // anchor is in the same buffer - let range = - buffer.read(cx).anchor_before(0)..current_execution_position.text_anchor; - project.inline_values(buffer, range, cx) - }) - .ok() - .flatten()? - .await - .context("refreshing debugger inlays") - .log_err()?; - - let (excerpt_id, buffer_id) = snapshot - .excerpts() - .next() - .map(|excerpt| (excerpt.0, excerpt.1.remote_id()))?; - editor - .update(cx, |editor, cx| { - let new_inlays = inline_values - .into_iter() - .map(|debugger_value| { - Inlay::debugger_hint( - post_inc(&mut editor.next_inlay_id), - Anchor::in_buffer(excerpt_id, buffer_id, debugger_value.position), - debugger_value.text(), - ) - }) - .collect::>(); - let mut inlay_ids = new_inlays.iter().map(|inlay| inlay.id).collect(); - std::mem::swap(&mut editor.inline_value_cache.inlays, &mut inlay_ids); - - editor.splice_inlays(&inlay_ids, new_inlays, cx); - }) - .ok()?; - Some(()) - }); - } - - fn on_buffer_event( - &mut self, - multibuffer: &Entity, - event: &multi_buffer::Event, - window: &mut Window, - cx: &mut Context, - ) { - match event { - multi_buffer::Event::Edited { - singleton_buffer_edited, - edited_buffer: buffer_edited, - } => { - self.scrollbar_marker_state.dirty = true; - self.active_indent_guides_state.dirty = true; - self.refresh_active_diagnostics(cx); - self.refresh_code_actions(window, cx); - self.refresh_selected_text_highlights(true, window, cx); - refresh_matching_bracket_highlights(self, window, cx); - if self.has_active_inline_completion() { - self.update_visible_inline_completion(window, cx); - } - if let Some(buffer) = buffer_edited { - let buffer_id = buffer.read(cx).remote_id(); - if !self.registered_buffers.contains_key(&buffer_id) { - if let Some(project) = self.project.as_ref() { - project.update(cx, |project, cx| { - self.registered_buffers.insert( - buffer_id, - project.register_buffer_with_language_servers(&buffer, cx), - ); - }) - } - } - } - cx.emit(EditorEvent::BufferEdited); - cx.emit(SearchEvent::MatchesInvalidated); - if *singleton_buffer_edited { - if let Some(project) = &self.project { - #[allow(clippy::mutable_key_type)] - let languages_affected = multibuffer.update(cx, |multibuffer, cx| { - multibuffer - .all_buffers() - .into_iter() - .filter_map(|buffer| { - buffer.update(cx, |buffer, cx| { - let language = buffer.language()?; - let should_discard = project.update(cx, |project, cx| { - project.is_local() - && !project.has_language_servers_for(buffer, cx) - }); - should_discard.not().then_some(language.clone()) - }) - }) - .collect::>() - }); - if !languages_affected.is_empty() { - self.refresh_inlay_hints( - InlayHintRefreshReason::BufferEdited(languages_affected), - cx, - ); - } - } - } - - let Some(project) = &self.project else { return }; - let (telemetry, is_via_ssh) = { - let project = project.read(cx); - let telemetry = project.client().telemetry().clone(); - let is_via_ssh = project.is_via_ssh(); - (telemetry, is_via_ssh) - }; - refresh_linked_ranges(self, window, cx); - telemetry.log_edit_event("editor", is_via_ssh); - } - multi_buffer::Event::ExcerptsAdded { - buffer, - predecessor, - excerpts, - } => { - self.tasks_update_task = Some(self.refresh_runnables(window, cx)); - let buffer_id = buffer.read(cx).remote_id(); - if self.buffer.read(cx).diff_for(buffer_id).is_none() { - if let Some(project) = &self.project { - get_uncommitted_diff_for_buffer( - project, - [buffer.clone()], - self.buffer.clone(), - cx, - ) - .detach(); - } - } - cx.emit(EditorEvent::ExcerptsAdded { - buffer: buffer.clone(), - predecessor: *predecessor, - excerpts: excerpts.clone(), - }); - self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); - } - multi_buffer::Event::ExcerptsRemoved { - ids, - removed_buffer_ids, - } => { - self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx); - let buffer = self.buffer.read(cx); - self.registered_buffers - .retain(|buffer_id, _| buffer.buffer(*buffer_id).is_some()); - jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx); - cx.emit(EditorEvent::ExcerptsRemoved { - ids: ids.clone(), - removed_buffer_ids: removed_buffer_ids.clone(), - }) - } - multi_buffer::Event::ExcerptsEdited { - excerpt_ids, - buffer_ids, - } => { - self.display_map.update(cx, |map, cx| { - map.unfold_buffers(buffer_ids.iter().copied(), cx) - }); - cx.emit(EditorEvent::ExcerptsEdited { - ids: excerpt_ids.clone(), - }) - } - multi_buffer::Event::ExcerptsExpanded { ids } => { - self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); - cx.emit(EditorEvent::ExcerptsExpanded { ids: ids.clone() }) - } - multi_buffer::Event::Reparsed(buffer_id) => { - self.tasks_update_task = Some(self.refresh_runnables(window, cx)); - jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx); - - cx.emit(EditorEvent::Reparsed(*buffer_id)); - } - multi_buffer::Event::DiffHunksToggled => { - self.tasks_update_task = Some(self.refresh_runnables(window, cx)); - } - multi_buffer::Event::LanguageChanged(buffer_id) => { - linked_editing_ranges::refresh_linked_ranges(self, window, cx); - jsx_tag_auto_close::refresh_enabled_in_any_buffer(self, multibuffer, cx); - cx.emit(EditorEvent::Reparsed(*buffer_id)); - cx.notify(); - } - multi_buffer::Event::DirtyChanged => cx.emit(EditorEvent::DirtyChanged), - multi_buffer::Event::Saved => cx.emit(EditorEvent::Saved), - multi_buffer::Event::FileHandleChanged - | multi_buffer::Event::Reloaded - | multi_buffer::Event::BufferDiffChanged => cx.emit(EditorEvent::TitleChanged), - multi_buffer::Event::Closed => cx.emit(EditorEvent::Closed), - multi_buffer::Event::DiagnosticsUpdated => { - self.refresh_active_diagnostics(cx); - self.refresh_inline_diagnostics(true, window, cx); - self.scrollbar_marker_state.dirty = true; - cx.notify(); - } - _ => {} - }; - } - - fn on_display_map_changed( - &mut self, - _: Entity, - _: &mut Window, - cx: &mut Context, - ) { - cx.notify(); - } - - fn settings_changed(&mut self, window: &mut Window, cx: &mut Context) { - self.tasks_update_task = Some(self.refresh_runnables(window, cx)); - self.update_edit_prediction_settings(cx); - self.refresh_inline_completion(true, false, window, cx); - self.refresh_inlay_hints( - InlayHintRefreshReason::SettingsChange(inlay_hint_settings( - self.selections.newest_anchor().head(), - &self.buffer.read(cx).snapshot(cx), - cx, - )), - cx, - ); - - let old_cursor_shape = self.cursor_shape; - - { - let editor_settings = EditorSettings::get_global(cx); - self.scroll_manager.vertical_scroll_margin = editor_settings.vertical_scroll_margin; - self.show_breadcrumbs = editor_settings.toolbar.breadcrumbs; - self.cursor_shape = editor_settings.cursor_shape.unwrap_or_default(); - self.hide_mouse_mode = editor_settings.hide_mouse.unwrap_or_default(); - } - - if old_cursor_shape != self.cursor_shape { - cx.emit(EditorEvent::CursorShapeChanged); - } - - let project_settings = ProjectSettings::get_global(cx); - self.serialize_dirty_buffers = project_settings.session.restore_unsaved_buffers; - - if self.mode.is_full() { - let show_inline_diagnostics = project_settings.diagnostics.inline.enabled; - let inline_blame_enabled = project_settings.git.inline_blame_enabled(); - if self.show_inline_diagnostics != show_inline_diagnostics { - self.show_inline_diagnostics = show_inline_diagnostics; - self.refresh_inline_diagnostics(false, window, cx); - } - - if self.git_blame_inline_enabled != inline_blame_enabled { - self.toggle_git_blame_inline_internal(false, window, cx); - } - } - - cx.notify(); - } - - pub fn set_searchable(&mut self, searchable: bool) { - self.searchable = searchable; - } - - pub fn searchable(&self) -> bool { - self.searchable - } - - fn open_proposed_changes_editor( - &mut self, - _: &OpenProposedChangesEditor, - window: &mut Window, - cx: &mut Context, - ) { - let Some(workspace) = self.workspace() else { - cx.propagate(); - return; - }; - - let selections = self.selections.all::(cx); - let multi_buffer = self.buffer.read(cx); - let multi_buffer_snapshot = multi_buffer.snapshot(cx); - let mut new_selections_by_buffer = HashMap::default(); - for selection in selections { - for (buffer, range, _) in - multi_buffer_snapshot.range_to_buffer_ranges(selection.start..selection.end) - { - let mut range = range.to_point(buffer); - range.start.column = 0; - range.end.column = buffer.line_len(range.end.row); - new_selections_by_buffer - .entry(multi_buffer.buffer(buffer.remote_id()).unwrap()) - .or_insert(Vec::new()) - .push(range) - } - } - - let proposed_changes_buffers = new_selections_by_buffer - .into_iter() - .map(|(buffer, ranges)| ProposedChangeLocation { buffer, ranges }) - .collect::>(); - let proposed_changes_editor = cx.new(|cx| { - ProposedChangesEditor::new( - "Proposed changes", - proposed_changes_buffers, - self.project.clone(), - window, - cx, - ) - }); - - window.defer(cx, move |window, cx| { - workspace.update(cx, |workspace, cx| { - workspace.active_pane().update(cx, |pane, cx| { - pane.add_item( - Box::new(proposed_changes_editor), - true, - true, - None, - window, - cx, - ); - }); - }); - }); - } - - pub fn open_excerpts_in_split( - &mut self, - _: &OpenExcerptsSplit, - window: &mut Window, - cx: &mut Context, - ) { - self.open_excerpts_common(None, true, window, cx) - } - - pub fn open_excerpts(&mut self, _: &OpenExcerpts, window: &mut Window, cx: &mut Context) { - self.open_excerpts_common(None, false, window, cx) - } - - fn open_excerpts_common( - &mut self, - jump_data: Option, - split: bool, - window: &mut Window, - cx: &mut Context, - ) { - let Some(workspace) = self.workspace() else { - cx.propagate(); - return; - }; - - if self.buffer.read(cx).is_singleton() { - cx.propagate(); - return; - } - - let mut new_selections_by_buffer = HashMap::default(); - match &jump_data { - Some(JumpData::MultiBufferPoint { - excerpt_id, - position, - anchor, - line_offset_from_top, - }) => { - let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); - if let Some(buffer) = multi_buffer_snapshot - .buffer_id_for_excerpt(*excerpt_id) - .and_then(|buffer_id| self.buffer.read(cx).buffer(buffer_id)) - { - let buffer_snapshot = buffer.read(cx).snapshot(); - let jump_to_point = if buffer_snapshot.can_resolve(anchor) { - language::ToPoint::to_point(anchor, &buffer_snapshot) - } else { - buffer_snapshot.clip_point(*position, Bias::Left) - }; - let jump_to_offset = buffer_snapshot.point_to_offset(jump_to_point); - new_selections_by_buffer.insert( - buffer, - ( - vec![jump_to_offset..jump_to_offset], - Some(*line_offset_from_top), - ), - ); - } - } - Some(JumpData::MultiBufferRow { - row, - line_offset_from_top, - }) => { - let point = MultiBufferPoint::new(row.0, 0); - if let Some((buffer, buffer_point, _)) = - self.buffer.read(cx).point_to_buffer_point(point, cx) - { - let buffer_offset = buffer.read(cx).point_to_offset(buffer_point); - new_selections_by_buffer - .entry(buffer) - .or_insert((Vec::new(), Some(*line_offset_from_top))) - .0 - .push(buffer_offset..buffer_offset) - } - } - None => { - let selections = self.selections.all::(cx); - let multi_buffer = self.buffer.read(cx); - for selection in selections { - for (snapshot, range, _, anchor) in multi_buffer - .snapshot(cx) - .range_to_buffer_ranges_with_deleted_hunks(selection.range()) - { - if let Some(anchor) = anchor { - // selection is in a deleted hunk - let Some(buffer_id) = anchor.buffer_id else { - continue; - }; - let Some(buffer_handle) = multi_buffer.buffer(buffer_id) else { - continue; - }; - let offset = text::ToOffset::to_offset( - &anchor.text_anchor, - &buffer_handle.read(cx).snapshot(), - ); - let range = offset..offset; - new_selections_by_buffer - .entry(buffer_handle) - .or_insert((Vec::new(), None)) - .0 - .push(range) - } else { - let Some(buffer_handle) = multi_buffer.buffer(snapshot.remote_id()) - else { - continue; - }; - new_selections_by_buffer - .entry(buffer_handle) - .or_insert((Vec::new(), None)) - .0 - .push(range) - } - } - } - } - } - - new_selections_by_buffer - .retain(|buffer, _| Self::can_open_excerpts_in_file(buffer.read(cx).file())); - - if new_selections_by_buffer.is_empty() { - return; - } - - // We defer the pane interaction because we ourselves are a workspace item - // and activating a new item causes the pane to call a method on us reentrantly, - // which panics if we're on the stack. - window.defer(cx, move |window, cx| { - workspace.update(cx, |workspace, cx| { - let pane = if split { - workspace.adjacent_pane(window, cx) - } else { - workspace.active_pane().clone() - }; - - for (buffer, (ranges, scroll_offset)) in new_selections_by_buffer { - let editor = buffer - .read(cx) - .file() - .is_none() - .then(|| { - // Handle file-less buffers separately: those are not really the project items, so won't have a project path or entity id, - // so `workspace.open_project_item` will never find them, always opening a new editor. - // Instead, we try to activate the existing editor in the pane first. - let (editor, pane_item_index) = - pane.read(cx).items().enumerate().find_map(|(i, item)| { - let editor = item.downcast::()?; - let singleton_buffer = - editor.read(cx).buffer().read(cx).as_singleton()?; - if singleton_buffer == buffer { - Some((editor, i)) - } else { - None - } - })?; - pane.update(cx, |pane, cx| { - pane.activate_item(pane_item_index, true, true, window, cx) - }); - Some(editor) - }) - .flatten() - .unwrap_or_else(|| { - workspace.open_project_item::( - pane.clone(), - buffer, - true, - true, - window, - cx, - ) - }); - - editor.update(cx, |editor, cx| { - let autoscroll = match scroll_offset { - Some(scroll_offset) => Autoscroll::top_relative(scroll_offset as usize), - None => Autoscroll::newest(), - }; - let nav_history = editor.nav_history.take(); - editor.change_selections(Some(autoscroll), window, cx, |s| { - s.select_ranges(ranges); - }); - editor.nav_history = nav_history; - }); - } - }) - }); - } - - // For now, don't allow opening excerpts in buffers that aren't backed by - // regular project files. - fn can_open_excerpts_in_file(file: Option<&Arc>) -> bool { - file.map_or(true, |file| project::File::from_dyn(Some(file)).is_some()) - } - - fn marked_text_ranges(&self, cx: &App) -> Option>> { - let snapshot = self.buffer.read(cx).read(cx); - let (_, ranges) = self.text_highlights::(cx)?; - Some( - ranges - .iter() - .map(move |range| { - range.start.to_offset_utf16(&snapshot)..range.end.to_offset_utf16(&snapshot) - }) - .collect(), - ) - } - - fn selection_replacement_ranges( - &self, - range: Range, - cx: &mut App, - ) -> Vec> { - let selections = self.selections.all::(cx); - let newest_selection = selections - .iter() - .max_by_key(|selection| selection.id) - .unwrap(); - let start_delta = range.start.0 as isize - newest_selection.start.0 as isize; - let end_delta = range.end.0 as isize - newest_selection.end.0 as isize; - let snapshot = self.buffer.read(cx).read(cx); - selections - .into_iter() - .map(|mut selection| { - selection.start.0 = - (selection.start.0 as isize).saturating_add(start_delta) as usize; - selection.end.0 = (selection.end.0 as isize).saturating_add(end_delta) as usize; - snapshot.clip_offset_utf16(selection.start, Bias::Left) - ..snapshot.clip_offset_utf16(selection.end, Bias::Right) - }) - .collect() - } - - fn report_editor_event( - &self, - event_type: &'static str, - file_extension: Option, - cx: &App, - ) { - if cfg!(any(test, feature = "test-support")) { - return; - } - - let Some(project) = &self.project else { return }; - - // If None, we are in a file without an extension - let file = self - .buffer - .read(cx) - .as_singleton() - .and_then(|b| b.read(cx).file()); - let file_extension = file_extension.or(file - .as_ref() - .and_then(|file| Path::new(file.file_name(cx)).extension()) - .and_then(|e| e.to_str()) - .map(|a| a.to_string())); - - let vim_mode = vim_enabled(cx); - - let edit_predictions_provider = all_language_settings(file, cx).edit_predictions.provider; - let copilot_enabled = edit_predictions_provider - == language::language_settings::EditPredictionProvider::Copilot; - let copilot_enabled_for_language = self - .buffer - .read(cx) - .language_settings(cx) - .show_edit_predictions; - - let project = project.read(cx); - telemetry::event!( - event_type, - file_extension, - vim_mode, - copilot_enabled, - copilot_enabled_for_language, - edit_predictions_provider, - is_via_ssh = project.is_via_ssh(), - ); - } - - /// Copy the highlighted chunks to the clipboard as JSON. The format is an array of lines, - /// with each line being an array of {text, highlight} objects. - fn copy_highlight_json( - &mut self, - _: &CopyHighlightJson, - window: &mut Window, - cx: &mut Context, - ) { - #[derive(Serialize)] - struct Chunk<'a> { - text: String, - highlight: Option<&'a str>, - } - - let snapshot = self.buffer.read(cx).snapshot(cx); - let range = self - .selected_text_range(false, window, cx) - .and_then(|selection| { - if selection.range.is_empty() { - None - } else { - Some(selection.range) - } - }) - .unwrap_or_else(|| 0..snapshot.len()); - - let chunks = snapshot.chunks(range, true); - let mut lines = Vec::new(); - let mut line: VecDeque = VecDeque::new(); - - let Some(style) = self.style.as_ref() else { - return; - }; - - for chunk in chunks { - let highlight = chunk - .syntax_highlight_id - .and_then(|id| id.name(&style.syntax)); - let mut chunk_lines = chunk.text.split('\n').peekable(); - while let Some(text) = chunk_lines.next() { - let mut merged_with_last_token = false; - if let Some(last_token) = line.back_mut() { - if last_token.highlight == highlight { - last_token.text.push_str(text); - merged_with_last_token = true; - } - } - - if !merged_with_last_token { - line.push_back(Chunk { - text: text.into(), - highlight, - }); - } - - if chunk_lines.peek().is_some() { - if line.len() > 1 && line.front().unwrap().text.is_empty() { - line.pop_front(); - } - if line.len() > 1 && line.back().unwrap().text.is_empty() { - line.pop_back(); - } - - lines.push(mem::take(&mut line)); - } - } - } - - let Some(lines) = serde_json::to_string_pretty(&lines).log_err() else { - return; - }; - cx.write_to_clipboard(ClipboardItem::new_string(lines)); - } - - pub fn open_context_menu( - &mut self, - _: &OpenContextMenu, - window: &mut Window, - cx: &mut Context, - ) { - self.request_autoscroll(Autoscroll::newest(), cx); - let position = self.selections.newest_display(cx).start; - mouse_context_menu::deploy_context_menu(self, None, position, window, cx); - } - - pub fn inlay_hint_cache(&self) -> &InlayHintCache { - &self.inlay_hint_cache - } - - pub fn replay_insert_event( - &mut self, - text: &str, - relative_utf16_range: Option>, - window: &mut Window, - cx: &mut Context, - ) { - if !self.input_enabled { - cx.emit(EditorEvent::InputIgnored { text: text.into() }); - return; - } - if let Some(relative_utf16_range) = relative_utf16_range { - let selections = self.selections.all::(cx); - self.change_selections(None, window, cx, |s| { - let new_ranges = selections.into_iter().map(|range| { - let start = OffsetUtf16( - range - .head() - .0 - .saturating_add_signed(relative_utf16_range.start), - ); - let end = OffsetUtf16( - range - .head() - .0 - .saturating_add_signed(relative_utf16_range.end), - ); - start..end - }); - s.select_ranges(new_ranges); - }); - } - - self.handle_input(text, window, cx); - } - - pub fn supports_inlay_hints(&self, cx: &mut App) -> bool { - let Some(provider) = self.semantics_provider.as_ref() else { - return false; - }; - - let mut supports = false; - self.buffer().update(cx, |this, cx| { - this.for_each_buffer(|buffer| { - supports |= provider.supports_inlay_hints(buffer, cx); - }); - }); - - supports - } - - pub fn is_focused(&self, window: &Window) -> bool { - self.focus_handle.is_focused(window) - } - - fn handle_focus(&mut self, window: &mut Window, cx: &mut Context) { - cx.emit(EditorEvent::Focused); - - if let Some(descendant) = self - .last_focused_descendant - .take() - .and_then(|descendant| descendant.upgrade()) - { - window.focus(&descendant); - } else { - if let Some(blame) = self.blame.as_ref() { - blame.update(cx, GitBlame::focus) - } - - self.blink_manager.update(cx, |blink_manager, cx| { - blink_manager.enable(cx); - }); - self.show_cursor_names(window, cx); - self.buffer.update(cx, |buffer, cx| { - buffer.finalize_last_transaction(cx); - if self.leader_peer_id.is_none() { - buffer.set_active_selections( - &self.selections.disjoint_anchors(), - self.selections.line_mode, - self.cursor_shape, - cx, - ); - } - }); - } - } - - fn handle_focus_in(&mut self, _: &mut Window, cx: &mut Context) { - cx.emit(EditorEvent::FocusedIn) - } - - fn handle_focus_out( - &mut self, - event: FocusOutEvent, - _window: &mut Window, - cx: &mut Context, - ) { - if event.blurred != self.focus_handle { - self.last_focused_descendant = Some(event.blurred); - } - self.refresh_inlay_hints(InlayHintRefreshReason::ModifiersChanged(false), cx); - } - - pub fn handle_blur(&mut self, window: &mut Window, cx: &mut Context) { - self.blink_manager.update(cx, BlinkManager::disable); - self.buffer - .update(cx, |buffer, cx| buffer.remove_active_selections(cx)); - - if let Some(blame) = self.blame.as_ref() { - blame.update(cx, GitBlame::blur) - } - if !self.hover_state.focused(window, cx) { - hide_hover(self, cx); - } - if !self - .context_menu - .borrow() - .as_ref() - .is_some_and(|context_menu| context_menu.focused(window, cx)) - { - self.hide_context_menu(window, cx); - } - self.discard_inline_completion(false, cx); - cx.emit(EditorEvent::Blurred); - cx.notify(); - } - - pub fn register_action( - &mut self, - listener: impl Fn(&A, &mut Window, &mut App) + 'static, - ) -> Subscription { - let id = self.next_editor_action_id.post_inc(); - let listener = Arc::new(listener); - self.editor_actions.borrow_mut().insert( - id, - Box::new(move |window, _| { - let listener = listener.clone(); - window.on_action(TypeId::of::(), move |action, phase, window, cx| { - let action = action.downcast_ref().unwrap(); - if phase == DispatchPhase::Bubble { - listener(action, window, cx) - } - }) - }), - ); - - let editor_actions = self.editor_actions.clone(); - Subscription::new(move || { - editor_actions.borrow_mut().remove(&id); - }) - } - - pub fn file_header_size(&self) -> u32 { - FILE_HEADER_HEIGHT - } - - pub fn restore( - &mut self, - revert_changes: HashMap, Rope)>>, - window: &mut Window, - cx: &mut Context, - ) { - let workspace = self.workspace(); - let project = self.project.as_ref(); - let save_tasks = self.buffer().update(cx, |multi_buffer, cx| { - let mut tasks = Vec::new(); - for (buffer_id, changes) in revert_changes { - if let Some(buffer) = multi_buffer.buffer(buffer_id) { - buffer.update(cx, |buffer, cx| { - buffer.edit( - changes - .into_iter() - .map(|(range, text)| (range, text.to_string())), - None, - cx, - ); - }); - - if let Some(project) = - project.filter(|_| multi_buffer.all_diff_hunks_expanded()) - { - project.update(cx, |project, cx| { - tasks.push((buffer.clone(), project.save_buffer(buffer, cx))); - }) - } - } - } - tasks - }); - cx.spawn_in(window, async move |_, cx| { - for (buffer, task) in save_tasks { - let result = task.await; - if result.is_err() { - let Some(path) = buffer - .read_with(cx, |buffer, cx| buffer.project_path(cx)) - .ok() - else { - continue; - }; - if let Some((workspace, path)) = workspace.as_ref().zip(path) { - let Some(task) = cx - .update_window_entity(&workspace, |workspace, window, cx| { - workspace - .open_path_preview(path, None, false, false, false, window, cx) - }) - .ok() - else { - continue; - }; - task.await.log_err(); - } - } - } - }) - .detach(); - self.change_selections(None, window, cx, |selections| selections.refresh()); - } - - pub fn to_pixel_point( - &self, - source: multi_buffer::Anchor, - editor_snapshot: &EditorSnapshot, - window: &mut Window, - ) -> Option> { - let source_point = source.to_display_point(editor_snapshot); - self.display_to_pixel_point(source_point, editor_snapshot, window) - } - - pub fn display_to_pixel_point( - &self, - source: DisplayPoint, - editor_snapshot: &EditorSnapshot, - window: &mut Window, - ) -> Option> { - let line_height = self.style()?.text.line_height_in_pixels(window.rem_size()); - let text_layout_details = self.text_layout_details(window); - let scroll_top = text_layout_details - .scroll_anchor - .scroll_position(editor_snapshot) - .y; - - if source.row().as_f32() < scroll_top.floor() { - return None; - } - let source_x = editor_snapshot.x_for_display_point(source, &text_layout_details); - let source_y = line_height * (source.row().as_f32() - scroll_top); - Some(gpui::Point::new(source_x, source_y)) - } - - pub fn has_visible_completions_menu(&self) -> bool { - !self.edit_prediction_preview_is_active() - && self.context_menu.borrow().as_ref().map_or(false, |menu| { - menu.visible() && matches!(menu, CodeContextMenu::Completions(_)) - }) - } - - pub fn register_addon(&mut self, instance: T) { - self.addons - .insert(std::any::TypeId::of::(), Box::new(instance)); - } - - pub fn unregister_addon(&mut self) { - self.addons.remove(&std::any::TypeId::of::()); - } - - pub fn addon(&self) -> Option<&T> { - let type_id = std::any::TypeId::of::(); - self.addons - .get(&type_id) - .and_then(|item| item.to_any().downcast_ref::()) - } - - pub fn addon_mut(&mut self) -> Option<&mut T> { - let type_id = std::any::TypeId::of::(); - self.addons - .get_mut(&type_id) - .and_then(|item| item.to_any_mut()?.downcast_mut::()) - } - - fn character_size(&self, window: &mut Window) -> gpui::Size { - let text_layout_details = self.text_layout_details(window); - let style = &text_layout_details.editor_style; - let font_id = window.text_system().resolve_font(&style.text.font()); - let font_size = style.text.font_size.to_pixels(window.rem_size()); - let line_height = style.text.line_height_in_pixels(window.rem_size()); - let em_width = window.text_system().em_width(font_id, font_size).unwrap(); - - gpui::Size::new(em_width, line_height) - } - - pub fn wait_for_diff_to_load(&self) -> Option>> { - self.load_diff_task.clone() - } - - fn read_metadata_from_db( - &mut self, - item_id: u64, - workspace_id: WorkspaceId, - window: &mut Window, - cx: &mut Context, - ) { - if self.is_singleton(cx) - && WorkspaceSettings::get(None, cx).restore_on_startup != RestoreOnStartupBehavior::None - { - let buffer_snapshot = OnceCell::new(); - - if let Some(folds) = DB.get_editor_folds(item_id, workspace_id).log_err() { - if !folds.is_empty() { - let snapshot = - buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx)); - self.fold_ranges( - folds - .into_iter() - .map(|(start, end)| { - snapshot.clip_offset(start, Bias::Left) - ..snapshot.clip_offset(end, Bias::Right) - }) - .collect(), - false, - window, - cx, - ); - } - } - - if let Some(selections) = DB.get_editor_selections(item_id, workspace_id).log_err() { - if !selections.is_empty() { - let snapshot = - buffer_snapshot.get_or_init(|| self.buffer.read(cx).snapshot(cx)); - self.change_selections(None, window, cx, |s| { - s.select_ranges(selections.into_iter().map(|(start, end)| { - snapshot.clip_offset(start, Bias::Left) - ..snapshot.clip_offset(end, Bias::Right) - })); - }); - } - }; - } - - self.read_scroll_position_from_db(item_id, workspace_id, window, cx); - } -} - -fn vim_enabled(cx: &App) -> bool { - cx.global::() - .raw_user_settings() - .get("vim_mode") - == Some(&serde_json::Value::Bool(true)) -} - -// Consider user intent and default settings -fn choose_completion_range( - completion: &Completion, - intent: CompletionIntent, - buffer: &Entity, - cx: &mut Context, -) -> Range { - fn should_replace( - completion: &Completion, - insert_range: &Range, - intent: CompletionIntent, - completion_mode_setting: LspInsertMode, - buffer: &Buffer, - ) -> bool { - // specific actions take precedence over settings - match intent { - CompletionIntent::CompleteWithInsert => return false, - CompletionIntent::CompleteWithReplace => return true, - CompletionIntent::Complete | CompletionIntent::Compose => {} - } - - match completion_mode_setting { - LspInsertMode::Insert => false, - LspInsertMode::Replace => true, - LspInsertMode::ReplaceSubsequence => { - let mut text_to_replace = buffer.chars_for_range( - buffer.anchor_before(completion.replace_range.start) - ..buffer.anchor_after(completion.replace_range.end), - ); - let mut completion_text = completion.new_text.chars(); - - // is `text_to_replace` a subsequence of `completion_text` - text_to_replace - .all(|needle_ch| completion_text.any(|haystack_ch| haystack_ch == needle_ch)) - } - LspInsertMode::ReplaceSuffix => { - let range_after_cursor = insert_range.end..completion.replace_range.end; - - let text_after_cursor = buffer - .text_for_range( - buffer.anchor_before(range_after_cursor.start) - ..buffer.anchor_after(range_after_cursor.end), - ) - .collect::(); - completion.new_text.ends_with(&text_after_cursor) - } - } - } - - let buffer = buffer.read(cx); - - if let CompletionSource::Lsp { - insert_range: Some(insert_range), - .. - } = &completion.source - { - let completion_mode_setting = - language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx) - .completions - .lsp_insert_mode; - - if !should_replace( - completion, - &insert_range, - intent, - completion_mode_setting, - buffer, - ) { - return insert_range.to_offset(buffer); - } - } - - completion.replace_range.to_offset(buffer) -} - -fn insert_extra_newline_brackets( - buffer: &MultiBufferSnapshot, - range: Range, - language: &language::LanguageScope, -) -> bool { - let leading_whitespace_len = buffer - .reversed_chars_at(range.start) - .take_while(|c| c.is_whitespace() && *c != '\n') - .map(|c| c.len_utf8()) - .sum::(); - let trailing_whitespace_len = buffer - .chars_at(range.end) - .take_while(|c| c.is_whitespace() && *c != '\n') - .map(|c| c.len_utf8()) - .sum::(); - let range = range.start - leading_whitespace_len..range.end + trailing_whitespace_len; - - language.brackets().any(|(pair, enabled)| { - let pair_start = pair.start.trim_end(); - let pair_end = pair.end.trim_start(); - - enabled - && pair.newline - && buffer.contains_str_at(range.end, pair_end) - && buffer.contains_str_at(range.start.saturating_sub(pair_start.len()), pair_start) - }) -} - -fn insert_extra_newline_tree_sitter(buffer: &MultiBufferSnapshot, range: Range) -> bool { - let (buffer, range) = match buffer.range_to_buffer_ranges(range).as_slice() { - [(buffer, range, _)] => (*buffer, range.clone()), - _ => return false, - }; - let pair = { - let mut result: Option = None; - - for pair in buffer - .all_bracket_ranges(range.clone()) - .filter(move |pair| { - pair.open_range.start <= range.start && pair.close_range.end >= range.end - }) - { - let len = pair.close_range.end - pair.open_range.start; - - if let Some(existing) = &result { - let existing_len = existing.close_range.end - existing.open_range.start; - if len > existing_len { - continue; - } - } - - result = Some(pair); - } - - result - }; - let Some(pair) = pair else { - return false; - }; - pair.newline_only - && buffer - .chars_for_range(pair.open_range.end..range.start) - .chain(buffer.chars_for_range(range.end..pair.close_range.start)) - .all(|c| c.is_whitespace() && c != '\n') -} - -fn get_uncommitted_diff_for_buffer( - project: &Entity, - buffers: impl IntoIterator>, - buffer: Entity, - cx: &mut App, -) -> Task<()> { - let mut tasks = Vec::new(); - project.update(cx, |project, cx| { - for buffer in buffers { - if project::File::from_dyn(buffer.read(cx).file()).is_some() { - tasks.push(project.open_uncommitted_diff(buffer.clone(), cx)) - } - } - }); - cx.spawn(async move |cx| { - let diffs = future::join_all(tasks).await; - buffer - .update(cx, |buffer, cx| { - for diff in diffs.into_iter().flatten() { - buffer.add_diff(diff, cx); - } - }) - .ok(); - }) -} - -fn char_len_with_expanded_tabs(offset: usize, text: &str, tab_size: NonZeroU32) -> usize { - let tab_size = tab_size.get() as usize; - let mut width = offset; - - for ch in text.chars() { - width += if ch == '\t' { - tab_size - (width % tab_size) - } else { - 1 - }; - } - - width - offset -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_string_size_with_expanded_tabs() { - let nz = |val| NonZeroU32::new(val).unwrap(); - assert_eq!(char_len_with_expanded_tabs(0, "", nz(4)), 0); - assert_eq!(char_len_with_expanded_tabs(0, "hello", nz(4)), 5); - assert_eq!(char_len_with_expanded_tabs(0, "\thello", nz(4)), 9); - assert_eq!(char_len_with_expanded_tabs(0, "abc\tab", nz(4)), 6); - assert_eq!(char_len_with_expanded_tabs(0, "hello\t", nz(4)), 8); - assert_eq!(char_len_with_expanded_tabs(0, "\t\t", nz(8)), 16); - assert_eq!(char_len_with_expanded_tabs(0, "x\t", nz(8)), 8); - assert_eq!(char_len_with_expanded_tabs(7, "x\t", nz(8)), 9); - } -} - -/// Tokenizes a string into runs of text that should stick together, or that is whitespace. -struct WordBreakingTokenizer<'a> { - input: &'a str, -} - -impl<'a> WordBreakingTokenizer<'a> { - fn new(input: &'a str) -> Self { - Self { input } - } -} - -fn is_char_ideographic(ch: char) -> bool { - use unicode_script::Script::*; - use unicode_script::UnicodeScript; - matches!(ch.script(), Han | Tangut | Yi) -} - -fn is_grapheme_ideographic(text: &str) -> bool { - text.chars().any(is_char_ideographic) -} - -fn is_grapheme_whitespace(text: &str) -> bool { - text.chars().any(|x| x.is_whitespace()) -} - -fn should_stay_with_preceding_ideograph(text: &str) -> bool { - text.chars().next().map_or(false, |ch| { - matches!(ch, '。' | '、' | ',' | '?' | '!' | ':' | ';' | '…') - }) -} - -#[derive(PartialEq, Eq, Debug, Clone, Copy)] -enum WordBreakToken<'a> { - Word { token: &'a str, grapheme_len: usize }, - InlineWhitespace { token: &'a str, grapheme_len: usize }, - Newline, -} - -impl<'a> Iterator for WordBreakingTokenizer<'a> { - /// Yields a span, the count of graphemes in the token, and whether it was - /// whitespace. Note that it also breaks at word boundaries. - type Item = WordBreakToken<'a>; - - fn next(&mut self) -> Option { - use unicode_segmentation::UnicodeSegmentation; - if self.input.is_empty() { - return None; - } - - let mut iter = self.input.graphemes(true).peekable(); - let mut offset = 0; - let mut grapheme_len = 0; - if let Some(first_grapheme) = iter.next() { - let is_newline = first_grapheme == "\n"; - let is_whitespace = is_grapheme_whitespace(first_grapheme); - offset += first_grapheme.len(); - grapheme_len += 1; - if is_grapheme_ideographic(first_grapheme) && !is_whitespace { - if let Some(grapheme) = iter.peek().copied() { - if should_stay_with_preceding_ideograph(grapheme) { - offset += grapheme.len(); - grapheme_len += 1; - } - } - } else { - let mut words = self.input[offset..].split_word_bound_indices().peekable(); - let mut next_word_bound = words.peek().copied(); - if next_word_bound.map_or(false, |(i, _)| i == 0) { - next_word_bound = words.next(); - } - while let Some(grapheme) = iter.peek().copied() { - if next_word_bound.map_or(false, |(i, _)| i == offset) { - break; - }; - if is_grapheme_whitespace(grapheme) != is_whitespace - || (grapheme == "\n") != is_newline - { - break; - }; - offset += grapheme.len(); - grapheme_len += 1; - iter.next(); - } - } - let token = &self.input[..offset]; - self.input = &self.input[offset..]; - if token == "\n" { - Some(WordBreakToken::Newline) - } else if is_whitespace { - Some(WordBreakToken::InlineWhitespace { - token, - grapheme_len, - }) - } else { - Some(WordBreakToken::Word { - token, - grapheme_len, - }) - } - } else { - None - } - } -} - -#[test] -fn test_word_breaking_tokenizer() { - let tests: &[(&str, &[WordBreakToken<'static>])] = &[ - ("", &[]), - (" ", &[whitespace(" ", 2)]), - ("Ʒ", &[word("Ʒ", 1)]), - ("Ǽ", &[word("Ǽ", 1)]), - ("⋑", &[word("⋑", 1)]), - ("⋑⋑", &[word("⋑⋑", 2)]), - ( - "原理,进而", - &[word("原", 1), word("理,", 2), word("进", 1), word("而", 1)], - ), - ( - "hello world", - &[word("hello", 5), whitespace(" ", 1), word("world", 5)], - ), - ( - "hello, world", - &[word("hello,", 6), whitespace(" ", 1), word("world", 5)], - ), - ( - " hello world", - &[ - whitespace(" ", 2), - word("hello", 5), - whitespace(" ", 1), - word("world", 5), - ], - ), - ( - "这是什么 \n 钢笔", - &[ - word("这", 1), - word("是", 1), - word("什", 1), - word("么", 1), - whitespace(" ", 1), - newline(), - whitespace(" ", 1), - word("钢", 1), - word("笔", 1), - ], - ), - (" mutton", &[whitespace(" ", 1), word("mutton", 6)]), - ]; - - fn word(token: &'static str, grapheme_len: usize) -> WordBreakToken<'static> { - WordBreakToken::Word { - token, - grapheme_len, - } - } - - fn whitespace(token: &'static str, grapheme_len: usize) -> WordBreakToken<'static> { - WordBreakToken::InlineWhitespace { - token, - grapheme_len, - } - } - - fn newline() -> WordBreakToken<'static> { - WordBreakToken::Newline - } - - for (input, result) in tests { - assert_eq!( - WordBreakingTokenizer::new(input) - .collect::>() - .as_slice(), - *result, - ); - } -} - -fn wrap_with_prefix( - line_prefix: String, - unwrapped_text: String, - wrap_column: usize, - tab_size: NonZeroU32, - preserve_existing_whitespace: bool, -) -> String { - let line_prefix_len = char_len_with_expanded_tabs(0, &line_prefix, tab_size); - let mut wrapped_text = String::new(); - let mut current_line = line_prefix.clone(); - - let tokenizer = WordBreakingTokenizer::new(&unwrapped_text); - let mut current_line_len = line_prefix_len; - let mut in_whitespace = false; - for token in tokenizer { - let have_preceding_whitespace = in_whitespace; - match token { - WordBreakToken::Word { - token, - grapheme_len, - } => { - in_whitespace = false; - if current_line_len + grapheme_len > wrap_column - && current_line_len != line_prefix_len - { - wrapped_text.push_str(current_line.trim_end()); - wrapped_text.push('\n'); - current_line.truncate(line_prefix.len()); - current_line_len = line_prefix_len; - } - current_line.push_str(token); - current_line_len += grapheme_len; - } - WordBreakToken::InlineWhitespace { - mut token, - mut grapheme_len, - } => { - in_whitespace = true; - if have_preceding_whitespace && !preserve_existing_whitespace { - continue; - } - if !preserve_existing_whitespace { - token = " "; - grapheme_len = 1; - } - if current_line_len + grapheme_len > wrap_column { - wrapped_text.push_str(current_line.trim_end()); - wrapped_text.push('\n'); - current_line.truncate(line_prefix.len()); - current_line_len = line_prefix_len; - } else if current_line_len != line_prefix_len || preserve_existing_whitespace { - current_line.push_str(token); - current_line_len += grapheme_len; - } - } - WordBreakToken::Newline => { - in_whitespace = true; - if preserve_existing_whitespace { - wrapped_text.push_str(current_line.trim_end()); - wrapped_text.push('\n'); - current_line.truncate(line_prefix.len()); - current_line_len = line_prefix_len; - } else if have_preceding_whitespace { - continue; - } else if current_line_len + 1 > wrap_column && current_line_len != line_prefix_len - { - wrapped_text.push_str(current_line.trim_end()); - wrapped_text.push('\n'); - current_line.truncate(line_prefix.len()); - current_line_len = line_prefix_len; - } else if current_line_len != line_prefix_len { - current_line.push(' '); - current_line_len += 1; - } - } - } - } - - if !current_line.is_empty() { - wrapped_text.push_str(¤t_line); - } - wrapped_text -} - -#[test] -fn test_wrap_with_prefix() { - assert_eq!( - wrap_with_prefix( - "# ".to_string(), - "abcdefg".to_string(), - 4, - NonZeroU32::new(4).unwrap(), - false, - ), - "# abcdefg" - ); - assert_eq!( - wrap_with_prefix( - "".to_string(), - "\thello world".to_string(), - 8, - NonZeroU32::new(4).unwrap(), - false, - ), - "hello\nworld" - ); - assert_eq!( - wrap_with_prefix( - "// ".to_string(), - "xx \nyy zz aa bb cc".to_string(), - 12, - NonZeroU32::new(4).unwrap(), - false, - ), - "// xx yy zz\n// aa bb cc" - ); - assert_eq!( - wrap_with_prefix( - String::new(), - "这是什么 \n 钢笔".to_string(), - 3, - NonZeroU32::new(4).unwrap(), - false, - ), - "这是什\n么 钢\n笔" - ); -} - -pub trait CollaborationHub { - fn collaborators<'a>(&self, cx: &'a App) -> &'a HashMap; - fn user_participant_indices<'a>(&self, cx: &'a App) -> &'a HashMap; - fn user_names(&self, cx: &App) -> HashMap; -} - -impl CollaborationHub for Entity { - fn collaborators<'a>(&self, cx: &'a App) -> &'a HashMap { - self.read(cx).collaborators() - } - - fn user_participant_indices<'a>(&self, cx: &'a App) -> &'a HashMap { - self.read(cx).user_store().read(cx).participant_indices() - } - - fn user_names(&self, cx: &App) -> HashMap { - let this = self.read(cx); - let user_ids = this.collaborators().values().map(|c| c.user_id); - this.user_store().read_with(cx, |user_store, cx| { - user_store.participant_names(user_ids, cx) - }) - } -} - -pub trait SemanticsProvider { - fn hover( - &self, - buffer: &Entity, - position: text::Anchor, - cx: &mut App, - ) -> Option>>; - - fn inline_values( - &self, - buffer_handle: Entity, - range: Range, - cx: &mut App, - ) -> Option>>>; - - fn inlay_hints( - &self, - buffer_handle: Entity, - range: Range, - cx: &mut App, - ) -> Option>>>; - - fn resolve_inlay_hint( - &self, - hint: InlayHint, - buffer_handle: Entity, - server_id: LanguageServerId, - cx: &mut App, - ) -> Option>>; - - fn supports_inlay_hints(&self, buffer: &Entity, cx: &mut App) -> bool; - - fn document_highlights( - &self, - buffer: &Entity, - position: text::Anchor, - cx: &mut App, - ) -> Option>>>; - - fn definitions( - &self, - buffer: &Entity, - position: text::Anchor, - kind: GotoDefinitionKind, - cx: &mut App, - ) -> Option>>>; - - fn range_for_rename( - &self, - buffer: &Entity, - position: text::Anchor, - cx: &mut App, - ) -> Option>>>>; - - fn perform_rename( - &self, - buffer: &Entity, - position: text::Anchor, - new_name: String, - cx: &mut App, - ) -> Option>>; -} - -pub trait CompletionProvider { - fn completions( - &self, - excerpt_id: ExcerptId, - buffer: &Entity, - buffer_position: text::Anchor, - trigger: CompletionContext, - window: &mut Window, - cx: &mut Context, - ) -> Task>>>; - - fn resolve_completions( - &self, - buffer: Entity, - completion_indices: Vec, - completions: Rc>>, - cx: &mut Context, - ) -> Task>; - - fn apply_additional_edits_for_completion( - &self, - _buffer: Entity, - _completions: Rc>>, - _completion_index: usize, - _push_to_history: bool, - _cx: &mut Context, - ) -> Task>> { - Task::ready(Ok(None)) - } - - fn is_completion_trigger( - &self, - buffer: &Entity, - position: language::Anchor, - text: &str, - trigger_in_words: bool, - cx: &mut Context, - ) -> bool; - - fn sort_completions(&self) -> bool { - true - } - - fn filter_completions(&self) -> bool { - true - } -} - -pub trait CodeActionProvider { - fn id(&self) -> Arc; - - fn code_actions( - &self, - buffer: &Entity, - range: Range, - window: &mut Window, - cx: &mut App, - ) -> Task>>; - - fn apply_code_action( - &self, - buffer_handle: Entity, - action: CodeAction, - excerpt_id: ExcerptId, - push_to_history: bool, - window: &mut Window, - cx: &mut App, - ) -> Task>; -} - -impl CodeActionProvider for Entity { - fn id(&self) -> Arc { - "project".into() - } - - fn code_actions( - &self, - buffer: &Entity, - range: Range, - _window: &mut Window, - cx: &mut App, - ) -> Task>> { - self.update(cx, |project, cx| { - let code_lens = project.code_lens(buffer, range.clone(), cx); - let code_actions = project.code_actions(buffer, range, None, cx); - cx.background_spawn(async move { - let (code_lens, code_actions) = join(code_lens, code_actions).await; - Ok(code_lens - .context("code lens fetch")? - .into_iter() - .chain(code_actions.context("code action fetch")?) - .collect()) - }) - }) - } - - fn apply_code_action( - &self, - buffer_handle: Entity, - action: CodeAction, - _excerpt_id: ExcerptId, - push_to_history: bool, - _window: &mut Window, - cx: &mut App, - ) -> Task> { - self.update(cx, |project, cx| { - project.apply_code_action(buffer_handle, action, push_to_history, cx) - }) - } -} - -fn snippet_completions( - project: &Project, - buffer: &Entity, - buffer_position: text::Anchor, - cx: &mut App, -) -> Task>> { - let languages = buffer.read(cx).languages_at(buffer_position); - let snippet_store = project.snippets().read(cx); - - let scopes: Vec<_> = languages - .iter() - .filter_map(|language| { - let language_name = language.lsp_id(); - let snippets = snippet_store.snippets_for(Some(language_name), cx); - - if snippets.is_empty() { - None - } else { - Some((language.default_scope(), snippets)) - } - }) - .collect(); - - if scopes.is_empty() { - return Task::ready(Ok(vec![])); - } - - let snapshot = buffer.read(cx).text_snapshot(); - let chars: String = snapshot - .reversed_chars_for_range(text::Anchor::MIN..buffer_position) - .collect(); - let executor = cx.background_executor().clone(); - - cx.background_spawn(async move { - let mut all_results: Vec = Vec::new(); - for (scope, snippets) in scopes.into_iter() { - let classifier = CharClassifier::new(Some(scope)).for_completion(true); - let mut last_word = chars - .chars() - .take_while(|c| classifier.is_word(*c)) - .collect::(); - last_word = last_word.chars().rev().collect(); - - if last_word.is_empty() { - return Ok(vec![]); - } - - let as_offset = text::ToOffset::to_offset(&buffer_position, &snapshot); - let to_lsp = |point: &text::Anchor| { - let end = text::ToPointUtf16::to_point_utf16(point, &snapshot); - point_to_lsp(end) - }; - let lsp_end = to_lsp(&buffer_position); - - let candidates = snippets - .iter() - .enumerate() - .flat_map(|(ix, snippet)| { - snippet - .prefix - .iter() - .map(move |prefix| StringMatchCandidate::new(ix, &prefix)) - }) - .collect::>(); - - let mut matches = fuzzy::match_strings( - &candidates, - &last_word, - last_word.chars().any(|c| c.is_uppercase()), - 100, - &Default::default(), - executor.clone(), - ) - .await; - - // Remove all candidates where the query's start does not match the start of any word in the candidate - if let Some(query_start) = last_word.chars().next() { - matches.retain(|string_match| { - split_words(&string_match.string).any(|word| { - // Check that the first codepoint of the word as lowercase matches the first - // codepoint of the query as lowercase - word.chars() - .flat_map(|codepoint| codepoint.to_lowercase()) - .zip(query_start.to_lowercase()) - .all(|(word_cp, query_cp)| word_cp == query_cp) - }) - }); - } - - let matched_strings = matches - .into_iter() - .map(|m| m.string) - .collect::>(); - - let mut result: Vec = snippets - .iter() - .filter_map(|snippet| { - let matching_prefix = snippet - .prefix - .iter() - .find(|prefix| matched_strings.contains(*prefix))?; - let start = as_offset - last_word.len(); - let start = snapshot.anchor_before(start); - let range = start..buffer_position; - let lsp_start = to_lsp(&start); - let lsp_range = lsp::Range { - start: lsp_start, - end: lsp_end, - }; - Some(Completion { - replace_range: range, - new_text: snippet.body.clone(), - source: CompletionSource::Lsp { - insert_range: None, - server_id: LanguageServerId(usize::MAX), - resolved: true, - lsp_completion: Box::new(lsp::CompletionItem { - label: snippet.prefix.first().unwrap().clone(), - kind: Some(CompletionItemKind::SNIPPET), - label_details: snippet.description.as_ref().map(|description| { - lsp::CompletionItemLabelDetails { - detail: Some(description.clone()), - description: None, - } - }), - insert_text_format: Some(InsertTextFormat::SNIPPET), - text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace( - lsp::InsertReplaceEdit { - new_text: snippet.body.clone(), - insert: lsp_range, - replace: lsp_range, - }, - )), - filter_text: Some(snippet.body.clone()), - sort_text: Some(char::MAX.to_string()), - ..lsp::CompletionItem::default() - }), - lsp_defaults: None, - }, - label: CodeLabel { - text: matching_prefix.clone(), - runs: Vec::new(), - filter_range: 0..matching_prefix.len(), - }, - icon_path: None, - documentation: snippet.description.clone().map(|description| { - CompletionDocumentation::SingleLine(description.into()) - }), - insert_text_mode: None, - confirm: None, - }) - }) - .collect(); - - all_results.append(&mut result); - } - - Ok(all_results) - }) -} - -impl CompletionProvider for Entity { - fn completions( - &self, - _excerpt_id: ExcerptId, - buffer: &Entity, - buffer_position: text::Anchor, - options: CompletionContext, - _window: &mut Window, - cx: &mut Context, - ) -> Task>>> { - self.update(cx, |project, cx| { - let snippets = snippet_completions(project, buffer, buffer_position, cx); - let project_completions = project.completions(buffer, buffer_position, options, cx); - cx.background_spawn(async move { - let snippets_completions = snippets.await?; - match project_completions.await? { - Some(mut completions) => { - completions.extend(snippets_completions); - Ok(Some(completions)) - } - None => { - if snippets_completions.is_empty() { - Ok(None) - } else { - Ok(Some(snippets_completions)) - } - } - } - }) - }) - } - - fn resolve_completions( - &self, - buffer: Entity, - completion_indices: Vec, - completions: Rc>>, - cx: &mut Context, - ) -> Task> { - self.update(cx, |project, cx| { - project.lsp_store().update(cx, |lsp_store, cx| { - lsp_store.resolve_completions(buffer, completion_indices, completions, cx) - }) - }) - } - - fn apply_additional_edits_for_completion( - &self, - buffer: Entity, - completions: Rc>>, - completion_index: usize, - push_to_history: bool, - cx: &mut Context, - ) -> Task>> { - self.update(cx, |project, cx| { - project.lsp_store().update(cx, |lsp_store, cx| { - lsp_store.apply_additional_edits_for_completion( - buffer, - completions, - completion_index, - push_to_history, - cx, - ) - }) - }) - } - - fn is_completion_trigger( - &self, - buffer: &Entity, - position: language::Anchor, - text: &str, - trigger_in_words: bool, - cx: &mut Context, - ) -> bool { - let mut chars = text.chars(); - let char = if let Some(char) = chars.next() { - char - } else { - return false; - }; - if chars.next().is_some() { - return false; - } - - let buffer = buffer.read(cx); - let snapshot = buffer.snapshot(); - if !snapshot.settings_at(position, cx).show_completions_on_input { - return false; - } - let classifier = snapshot.char_classifier_at(position).for_completion(true); - if trigger_in_words && classifier.is_word(char) { - return true; - } - - buffer.completion_triggers().contains(text) - } -} - -impl SemanticsProvider for Entity { - fn hover( - &self, - buffer: &Entity, - position: text::Anchor, - cx: &mut App, - ) -> Option>> { - Some(self.update(cx, |project, cx| project.hover(buffer, position, cx))) - } - - fn document_highlights( - &self, - buffer: &Entity, - position: text::Anchor, - cx: &mut App, - ) -> Option>>> { - Some(self.update(cx, |project, cx| { - project.document_highlights(buffer, position, cx) - })) - } - - fn definitions( - &self, - buffer: &Entity, - position: text::Anchor, - kind: GotoDefinitionKind, - cx: &mut App, - ) -> Option>>> { - Some(self.update(cx, |project, cx| match kind { - GotoDefinitionKind::Symbol => project.definition(&buffer, position, cx), - GotoDefinitionKind::Declaration => project.declaration(&buffer, position, cx), - GotoDefinitionKind::Type => project.type_definition(&buffer, position, cx), - GotoDefinitionKind::Implementation => project.implementation(&buffer, position, cx), - })) - } - - fn supports_inlay_hints(&self, buffer: &Entity, cx: &mut App) -> bool { - // TODO: make this work for remote projects - self.update(cx, |project, cx| { - if project - .active_debug_session(cx) - .is_some_and(|(session, _)| session.read(cx).any_stopped_thread()) - { - return true; - } - - buffer.update(cx, |buffer, cx| { - project.any_language_server_supports_inlay_hints(buffer, cx) - }) - }) - } - - fn inline_values( - &self, - buffer_handle: Entity, - range: Range, - cx: &mut App, - ) -> Option>>> { - self.update(cx, |project, cx| { - let (session, active_stack_frame) = project.active_debug_session(cx)?; - - Some(project.inline_values(session, active_stack_frame, buffer_handle, range, cx)) - }) - } - - fn inlay_hints( - &self, - buffer_handle: Entity, - range: Range, - cx: &mut App, - ) -> Option>>> { - Some(self.update(cx, |project, cx| { - project.inlay_hints(buffer_handle, range, cx) - })) - } - - fn resolve_inlay_hint( - &self, - hint: InlayHint, - buffer_handle: Entity, - server_id: LanguageServerId, - cx: &mut App, - ) -> Option>> { - Some(self.update(cx, |project, cx| { - project.resolve_inlay_hint(hint, buffer_handle, server_id, cx) - })) - } - - fn range_for_rename( - &self, - buffer: &Entity, - position: text::Anchor, - cx: &mut App, - ) -> Option>>>> { - Some(self.update(cx, |project, cx| { - let buffer = buffer.clone(); - let task = project.prepare_rename(buffer.clone(), position, cx); - cx.spawn(async move |_, cx| { - Ok(match task.await? { - PrepareRenameResponse::Success(range) => Some(range), - PrepareRenameResponse::InvalidPosition => None, - PrepareRenameResponse::OnlyUnpreparedRenameSupported => { - // Fallback on using TreeSitter info to determine identifier range - buffer.update(cx, |buffer, _| { - let snapshot = buffer.snapshot(); - let (range, kind) = snapshot.surrounding_word(position); - if kind != Some(CharKind::Word) { - return None; - } - Some( - snapshot.anchor_before(range.start) - ..snapshot.anchor_after(range.end), - ) - })? - } - }) - }) - })) - } - - fn perform_rename( - &self, - buffer: &Entity, - position: text::Anchor, - new_name: String, - cx: &mut App, - ) -> Option>> { - Some(self.update(cx, |project, cx| { - project.perform_rename(buffer.clone(), position, new_name, cx) - })) - } -} - -fn inlay_hint_settings( - location: Anchor, - snapshot: &MultiBufferSnapshot, - cx: &mut Context, -) -> InlayHintSettings { - let file = snapshot.file_at(location); - let language = snapshot.language_at(location).map(|l| l.name()); - language_settings(language, file, cx).inlay_hints -} - -fn consume_contiguous_rows( - contiguous_row_selections: &mut Vec>, - selection: &Selection, - display_map: &DisplaySnapshot, - selections: &mut Peekable>>, -) -> (MultiBufferRow, MultiBufferRow) { - contiguous_row_selections.push(selection.clone()); - let start_row = MultiBufferRow(selection.start.row); - let mut end_row = ending_row(selection, display_map); - - while let Some(next_selection) = selections.peek() { - if next_selection.start.row <= end_row.0 { - end_row = ending_row(next_selection, display_map); - contiguous_row_selections.push(selections.next().unwrap().clone()); - } else { - break; - } - } - (start_row, end_row) -} - -fn ending_row(next_selection: &Selection, display_map: &DisplaySnapshot) -> MultiBufferRow { - if next_selection.end.column > 0 || next_selection.is_empty() { - MultiBufferRow(display_map.next_line_boundary(next_selection.end).0.row + 1) - } else { - MultiBufferRow(next_selection.end.row) - } -} - -impl EditorSnapshot { - pub fn remote_selections_in_range<'a>( - &'a self, - range: &'a Range, - collaboration_hub: &dyn CollaborationHub, - cx: &'a App, - ) -> impl 'a + Iterator { - let participant_names = collaboration_hub.user_names(cx); - let participant_indices = collaboration_hub.user_participant_indices(cx); - let collaborators_by_peer_id = collaboration_hub.collaborators(cx); - let collaborators_by_replica_id = collaborators_by_peer_id - .iter() - .map(|(_, collaborator)| (collaborator.replica_id, collaborator)) - .collect::>(); - self.buffer_snapshot - .selections_in_range(range, false) - .filter_map(move |(replica_id, line_mode, cursor_shape, selection)| { - let collaborator = collaborators_by_replica_id.get(&replica_id)?; - let participant_index = participant_indices.get(&collaborator.user_id).copied(); - let user_name = participant_names.get(&collaborator.user_id).cloned(); - Some(RemoteSelection { - replica_id, - selection, - cursor_shape, - line_mode, - participant_index, - peer_id: collaborator.peer_id, - user_name, - }) - }) - } - - pub fn hunks_for_ranges( - &self, - ranges: impl IntoIterator>, - ) -> Vec { - let mut hunks = Vec::new(); - let mut processed_buffer_rows: HashMap>> = - HashMap::default(); - for query_range in ranges { - let query_rows = - MultiBufferRow(query_range.start.row)..MultiBufferRow(query_range.end.row + 1); - for hunk in self.buffer_snapshot.diff_hunks_in_range( - Point::new(query_rows.start.0, 0)..Point::new(query_rows.end.0, 0), - ) { - // Include deleted hunks that are adjacent to the query range, because - // otherwise they would be missed. - let mut intersects_range = hunk.row_range.overlaps(&query_rows); - if hunk.status().is_deleted() { - intersects_range |= hunk.row_range.start == query_rows.end; - intersects_range |= hunk.row_range.end == query_rows.start; - } - if intersects_range { - if !processed_buffer_rows - .entry(hunk.buffer_id) - .or_default() - .insert(hunk.buffer_range.start..hunk.buffer_range.end) - { - continue; - } - hunks.push(hunk); - } - } - } - - hunks - } - - fn display_diff_hunks_for_rows<'a>( - &'a self, - display_rows: Range, - folded_buffers: &'a HashSet, - ) -> impl 'a + Iterator { - let buffer_start = DisplayPoint::new(display_rows.start, 0).to_point(self); - let buffer_end = DisplayPoint::new(display_rows.end, 0).to_point(self); - - self.buffer_snapshot - .diff_hunks_in_range(buffer_start..buffer_end) - .filter_map(|hunk| { - if folded_buffers.contains(&hunk.buffer_id) { - return None; - } - - let hunk_start_point = Point::new(hunk.row_range.start.0, 0); - let hunk_end_point = Point::new(hunk.row_range.end.0, 0); - - let hunk_display_start = self.point_to_display_point(hunk_start_point, Bias::Left); - let hunk_display_end = self.point_to_display_point(hunk_end_point, Bias::Right); - - let display_hunk = if hunk_display_start.column() != 0 { - DisplayDiffHunk::Folded { - display_row: hunk_display_start.row(), - } - } else { - let mut end_row = hunk_display_end.row(); - if hunk_display_end.column() > 0 { - end_row.0 += 1; - } - let is_created_file = hunk.is_created_file(); - DisplayDiffHunk::Unfolded { - status: hunk.status(), - diff_base_byte_range: hunk.diff_base_byte_range, - display_row_range: hunk_display_start.row()..end_row, - multi_buffer_range: Anchor::range_in_buffer( - hunk.excerpt_id, - hunk.buffer_id, - hunk.buffer_range, - ), - is_created_file, - } - }; - - Some(display_hunk) - }) - } - - pub fn language_at(&self, position: T) -> Option<&Arc> { - self.display_snapshot.buffer_snapshot.language_at(position) - } - - pub fn is_focused(&self) -> bool { - self.is_focused - } - - pub fn placeholder_text(&self) -> Option<&Arc> { - self.placeholder_text.as_ref() - } - - pub fn scroll_position(&self) -> gpui::Point { - self.scroll_anchor.scroll_position(&self.display_snapshot) - } - - fn gutter_dimensions( - &self, - font_id: FontId, - font_size: Pixels, - max_line_number_width: Pixels, - cx: &App, - ) -> Option { - if !self.show_gutter { - return None; - } - - let descent = cx.text_system().descent(font_id, font_size); - let em_width = cx.text_system().em_width(font_id, font_size).log_err()?; - let em_advance = cx.text_system().em_advance(font_id, font_size).log_err()?; - - let show_git_gutter = self.show_git_diff_gutter.unwrap_or_else(|| { - matches!( - ProjectSettings::get_global(cx).git.git_gutter, - Some(GitGutterSetting::TrackedFiles) - ) - }); - let gutter_settings = EditorSettings::get_global(cx).gutter; - let show_line_numbers = self - .show_line_numbers - .unwrap_or(gutter_settings.line_numbers); - let line_gutter_width = if show_line_numbers { - // Avoid flicker-like gutter resizes when the line number gains another digit and only resize the gutter on files with N*10^5 lines. - let min_width_for_number_on_gutter = em_advance * MIN_LINE_NUMBER_DIGITS as f32; - max_line_number_width.max(min_width_for_number_on_gutter) - } else { - 0.0.into() - }; - - let show_code_actions = self - .show_code_actions - .unwrap_or(gutter_settings.code_actions); - - let show_runnables = self.show_runnables.unwrap_or(gutter_settings.runnables); - let show_breakpoints = self.show_breakpoints.unwrap_or(gutter_settings.breakpoints); - - let git_blame_entries_width = - self.git_blame_gutter_max_author_length - .map(|max_author_length| { - let renderer = cx.global::().0.clone(); - const MAX_RELATIVE_TIMESTAMP: &str = "60 minutes ago"; - - /// The number of characters to dedicate to gaps and margins. - const SPACING_WIDTH: usize = 4; - - let max_char_count = max_author_length.min(renderer.max_author_length()) - + ::git::SHORT_SHA_LENGTH - + MAX_RELATIVE_TIMESTAMP.len() - + SPACING_WIDTH; - - em_advance * max_char_count - }); - - let is_singleton = self.buffer_snapshot.is_singleton(); - - let mut left_padding = git_blame_entries_width.unwrap_or(Pixels::ZERO); - left_padding += if !is_singleton { - em_width * 4.0 - } else if show_code_actions || show_runnables || show_breakpoints { - em_width * 3.0 - } else if show_git_gutter && show_line_numbers { - em_width * 2.0 - } else if show_git_gutter || show_line_numbers { - em_width - } else { - px(0.) - }; - - let shows_folds = is_singleton && gutter_settings.folds; - - let right_padding = if shows_folds && show_line_numbers { - em_width * 4.0 - } else if shows_folds || (!is_singleton && show_line_numbers) { - em_width * 3.0 - } else if show_line_numbers { - em_width - } else { - px(0.) - }; - - Some(GutterDimensions { - left_padding, - right_padding, - width: line_gutter_width + left_padding + right_padding, - margin: -descent, - git_blame_entries_width, - }) - } - - pub fn render_crease_toggle( - &self, - buffer_row: MultiBufferRow, - row_contains_cursor: bool, - editor: Entity, - window: &mut Window, - cx: &mut App, - ) -> Option { - let folded = self.is_line_folded(buffer_row); - let mut is_foldable = false; - - if let Some(crease) = self - .crease_snapshot - .query_row(buffer_row, &self.buffer_snapshot) - { - is_foldable = true; - match crease { - Crease::Inline { render_toggle, .. } | Crease::Block { render_toggle, .. } => { - if let Some(render_toggle) = render_toggle { - let toggle_callback = - Arc::new(move |folded, window: &mut Window, cx: &mut App| { - if folded { - editor.update(cx, |editor, cx| { - editor.fold_at(buffer_row, window, cx) - }); - } else { - editor.update(cx, |editor, cx| { - editor.unfold_at(buffer_row, window, cx) - }); - } - }); - return Some((render_toggle)( - buffer_row, - folded, - toggle_callback, - window, - cx, - )); - } - } - } - } - - is_foldable |= self.starts_indent(buffer_row); - - if folded || (is_foldable && (row_contains_cursor || self.gutter_hovered)) { - Some( - Disclosure::new(("gutter_crease", buffer_row.0), !folded) - .toggle_state(folded) - .on_click(window.listener_for(&editor, move |this, _e, window, cx| { - if folded { - this.unfold_at(buffer_row, window, cx); - } else { - this.fold_at(buffer_row, window, cx); - } - })) - .into_any_element(), - ) - } else { - None - } - } - - pub fn render_crease_trailer( - &self, - buffer_row: MultiBufferRow, - window: &mut Window, - cx: &mut App, - ) -> Option { - let folded = self.is_line_folded(buffer_row); - if let Crease::Inline { render_trailer, .. } = self - .crease_snapshot - .query_row(buffer_row, &self.buffer_snapshot)? - { - let render_trailer = render_trailer.as_ref()?; - Some(render_trailer(buffer_row, folded, window, cx)) - } else { - None - } - } -} - -impl Deref for EditorSnapshot { - type Target = DisplaySnapshot; - - fn deref(&self) -> &Self::Target { - &self.display_snapshot - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum EditorEvent { - InputIgnored { - text: Arc, - }, - InputHandled { - utf16_range_to_replace: Option>, - text: Arc, - }, - ExcerptsAdded { - buffer: Entity, - predecessor: ExcerptId, - excerpts: Vec<(ExcerptId, ExcerptRange)>, - }, - ExcerptsRemoved { - ids: Vec, - removed_buffer_ids: Vec, - }, - BufferFoldToggled { - ids: Vec, - folded: bool, - }, - ExcerptsEdited { - ids: Vec, - }, - ExcerptsExpanded { - ids: Vec, - }, - BufferEdited, - Edited { - transaction_id: clock::Lamport, - }, - Reparsed(BufferId), - Focused, - FocusedIn, - Blurred, - DirtyChanged, - Saved, - TitleChanged, - DiffBaseChanged, - SelectionsChanged { - local: bool, - }, - ScrollPositionChanged { - local: bool, - autoscroll: bool, - }, - Closed, - TransactionUndone { - transaction_id: clock::Lamport, - }, - TransactionBegun { - transaction_id: clock::Lamport, - }, - Reloaded, - CursorShapeChanged, - PushedToNavHistory { - anchor: Anchor, - is_deactivate: bool, - }, -} - -impl EventEmitter for Editor {} - -impl Focusable for Editor { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Render for Editor { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let settings = ThemeSettings::get_global(cx); - - let mut text_style = match self.mode { - EditorMode::SingleLine { .. } | EditorMode::AutoHeight { .. } => TextStyle { - color: cx.theme().colors().editor_foreground, - font_family: settings.ui_font.family.clone(), - font_features: settings.ui_font.features.clone(), - font_fallbacks: settings.ui_font.fallbacks.clone(), - font_size: rems(0.875).into(), - font_weight: settings.ui_font.weight, - line_height: relative(settings.buffer_line_height.value()), - ..Default::default() - }, - EditorMode::Full { .. } => TextStyle { - color: cx.theme().colors().editor_foreground, - font_family: settings.buffer_font.family.clone(), - font_features: settings.buffer_font.features.clone(), - font_fallbacks: settings.buffer_font.fallbacks.clone(), - font_size: settings.buffer_font_size(cx).into(), - font_weight: settings.buffer_font.weight, - line_height: relative(settings.buffer_line_height.value()), - ..Default::default() - }, - }; - if let Some(text_style_refinement) = &self.text_style_refinement { - text_style.refine(text_style_refinement) - } - - let background = match self.mode { - EditorMode::SingleLine { .. } => cx.theme().system().transparent, - EditorMode::AutoHeight { max_lines: _ } => cx.theme().system().transparent, - EditorMode::Full { .. } => cx.theme().colors().editor_background, - }; - - EditorElement::new( - &cx.entity(), - EditorStyle { - background, - local_player: cx.theme().players().local(), - text: text_style, - scrollbar_width: EditorElement::SCROLLBAR_WIDTH, - syntax: cx.theme().syntax().clone(), - status: cx.theme().status().clone(), - inlay_hints_style: make_inlay_hints_style(cx), - inline_completion_styles: make_suggestion_styles(cx), - unnecessary_code_fade: ThemeSettings::get_global(cx).unnecessary_code_fade, - }, - ) - } -} - -impl EntityInputHandler for Editor { - fn text_for_range( - &mut self, - range_utf16: Range, - adjusted_range: &mut Option>, - _: &mut Window, - cx: &mut Context, - ) -> Option { - let snapshot = self.buffer.read(cx).read(cx); - let start = snapshot.clip_offset_utf16(OffsetUtf16(range_utf16.start), Bias::Left); - let end = snapshot.clip_offset_utf16(OffsetUtf16(range_utf16.end), Bias::Right); - if (start.0..end.0) != range_utf16 { - adjusted_range.replace(start.0..end.0); - } - Some(snapshot.text_for_range(start..end).collect()) - } - - fn selected_text_range( - &mut self, - ignore_disabled_input: bool, - _: &mut Window, - cx: &mut Context, - ) -> Option { - // Prevent the IME menu from appearing when holding down an alphabetic key - // while input is disabled. - if !ignore_disabled_input && !self.input_enabled { - return None; - } - - let selection = self.selections.newest::(cx); - let range = selection.range(); - - Some(UTF16Selection { - range: range.start.0..range.end.0, - reversed: selection.reversed, - }) - } - - fn marked_text_range(&self, _: &mut Window, cx: &mut Context) -> Option> { - let snapshot = self.buffer.read(cx).read(cx); - let range = self.text_highlights::(cx)?.1.first()?; - Some(range.start.to_offset_utf16(&snapshot).0..range.end.to_offset_utf16(&snapshot).0) - } - - fn unmark_text(&mut self, _: &mut Window, cx: &mut Context) { - self.clear_highlights::(cx); - self.ime_transaction.take(); - } - - fn replace_text_in_range( - &mut self, - range_utf16: Option>, - text: &str, - window: &mut Window, - cx: &mut Context, - ) { - if !self.input_enabled { - cx.emit(EditorEvent::InputIgnored { text: text.into() }); - return; - } - - self.transact(window, cx, |this, window, cx| { - let new_selected_ranges = if let Some(range_utf16) = range_utf16 { - let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end); - Some(this.selection_replacement_ranges(range_utf16, cx)) - } else { - this.marked_text_ranges(cx) - }; - - let range_to_replace = new_selected_ranges.as_ref().and_then(|ranges_to_replace| { - let newest_selection_id = this.selections.newest_anchor().id; - this.selections - .all::(cx) - .iter() - .zip(ranges_to_replace.iter()) - .find_map(|(selection, range)| { - if selection.id == newest_selection_id { - Some( - (range.start.0 as isize - selection.head().0 as isize) - ..(range.end.0 as isize - selection.head().0 as isize), - ) - } else { - None - } - }) - }); - - cx.emit(EditorEvent::InputHandled { - utf16_range_to_replace: range_to_replace, - text: text.into(), - }); - - if let Some(new_selected_ranges) = new_selected_ranges { - this.change_selections(None, window, cx, |selections| { - selections.select_ranges(new_selected_ranges) - }); - this.backspace(&Default::default(), window, cx); - } - - this.handle_input(text, window, cx); - }); - - if let Some(transaction) = self.ime_transaction { - self.buffer.update(cx, |buffer, cx| { - buffer.group_until_transaction(transaction, cx); - }); - } - - self.unmark_text(window, cx); - } - - fn replace_and_mark_text_in_range( - &mut self, - range_utf16: Option>, - text: &str, - new_selected_range_utf16: Option>, - window: &mut Window, - cx: &mut Context, - ) { - if !self.input_enabled { - return; - } - - let transaction = self.transact(window, cx, |this, window, cx| { - let ranges_to_replace = if let Some(mut marked_ranges) = this.marked_text_ranges(cx) { - let snapshot = this.buffer.read(cx).read(cx); - if let Some(relative_range_utf16) = range_utf16.as_ref() { - for marked_range in &mut marked_ranges { - marked_range.end.0 = marked_range.start.0 + relative_range_utf16.end; - marked_range.start.0 += relative_range_utf16.start; - marked_range.start = - snapshot.clip_offset_utf16(marked_range.start, Bias::Left); - marked_range.end = - snapshot.clip_offset_utf16(marked_range.end, Bias::Right); - } - } - Some(marked_ranges) - } else if let Some(range_utf16) = range_utf16 { - let range_utf16 = OffsetUtf16(range_utf16.start)..OffsetUtf16(range_utf16.end); - Some(this.selection_replacement_ranges(range_utf16, cx)) - } else { - None - }; - - let range_to_replace = ranges_to_replace.as_ref().and_then(|ranges_to_replace| { - let newest_selection_id = this.selections.newest_anchor().id; - this.selections - .all::(cx) - .iter() - .zip(ranges_to_replace.iter()) - .find_map(|(selection, range)| { - if selection.id == newest_selection_id { - Some( - (range.start.0 as isize - selection.head().0 as isize) - ..(range.end.0 as isize - selection.head().0 as isize), - ) - } else { - None - } - }) - }); - - cx.emit(EditorEvent::InputHandled { - utf16_range_to_replace: range_to_replace, - text: text.into(), - }); - - if let Some(ranges) = ranges_to_replace { - this.change_selections(None, window, cx, |s| s.select_ranges(ranges)); - } - - let marked_ranges = { - let snapshot = this.buffer.read(cx).read(cx); - this.selections - .disjoint_anchors() - .iter() - .map(|selection| { - selection.start.bias_left(&snapshot)..selection.end.bias_right(&snapshot) - }) - .collect::>() - }; - - if text.is_empty() { - this.unmark_text(window, cx); - } else { - this.highlight_text::( - marked_ranges.clone(), - HighlightStyle { - underline: Some(UnderlineStyle { - thickness: px(1.), - color: None, - wavy: false, - }), - ..Default::default() - }, - cx, - ); - } - - // Disable auto-closing when composing text (i.e. typing a `"` on a Brazilian keyboard) - let use_autoclose = this.use_autoclose; - let use_auto_surround = this.use_auto_surround; - this.set_use_autoclose(false); - this.set_use_auto_surround(false); - this.handle_input(text, window, cx); - this.set_use_autoclose(use_autoclose); - this.set_use_auto_surround(use_auto_surround); - - if let Some(new_selected_range) = new_selected_range_utf16 { - let snapshot = this.buffer.read(cx).read(cx); - let new_selected_ranges = marked_ranges - .into_iter() - .map(|marked_range| { - let insertion_start = marked_range.start.to_offset_utf16(&snapshot).0; - let new_start = OffsetUtf16(new_selected_range.start + insertion_start); - let new_end = OffsetUtf16(new_selected_range.end + insertion_start); - snapshot.clip_offset_utf16(new_start, Bias::Left) - ..snapshot.clip_offset_utf16(new_end, Bias::Right) - }) - .collect::>(); - - drop(snapshot); - this.change_selections(None, window, cx, |selections| { - selections.select_ranges(new_selected_ranges) - }); - } - }); - - self.ime_transaction = self.ime_transaction.or(transaction); - if let Some(transaction) = self.ime_transaction { - self.buffer.update(cx, |buffer, cx| { - buffer.group_until_transaction(transaction, cx); - }); - } - - if self.text_highlights::(cx).is_none() { - self.ime_transaction.take(); - } - } - - fn bounds_for_range( - &mut self, - range_utf16: Range, - element_bounds: gpui::Bounds, - window: &mut Window, - cx: &mut Context, - ) -> Option> { - let text_layout_details = self.text_layout_details(window); - let gpui::Size { - width: em_width, - height: line_height, - } = self.character_size(window); - - let snapshot = self.snapshot(window, cx); - let scroll_position = snapshot.scroll_position(); - let scroll_left = scroll_position.x * em_width; - - let start = OffsetUtf16(range_utf16.start).to_display_point(&snapshot); - let x = snapshot.x_for_display_point(start, &text_layout_details) - scroll_left - + self.gutter_dimensions.width - + self.gutter_dimensions.margin; - let y = line_height * (start.row().as_f32() - scroll_position.y); - - Some(Bounds { - origin: element_bounds.origin + point(x, y), - size: size(em_width, line_height), - }) - } - - fn character_index_for_point( - &mut self, - point: gpui::Point, - _window: &mut Window, - _cx: &mut Context, - ) -> Option { - let position_map = self.last_position_map.as_ref()?; - if !position_map.text_hitbox.contains(&point) { - return None; - } - let display_point = position_map.point_for_position(point).previous_valid; - let anchor = position_map - .snapshot - .display_point_to_anchor(display_point, Bias::Left); - let utf16_offset = anchor.to_offset_utf16(&position_map.snapshot.buffer_snapshot); - Some(utf16_offset.0) - } -} - -trait SelectionExt { - fn display_range(&self, map: &DisplaySnapshot) -> Range; - fn spanned_rows( - &self, - include_end_if_at_line_start: bool, - map: &DisplaySnapshot, - ) -> Range; -} - -impl SelectionExt for Selection { - fn display_range(&self, map: &DisplaySnapshot) -> Range { - let start = self - .start - .to_point(&map.buffer_snapshot) - .to_display_point(map); - let end = self - .end - .to_point(&map.buffer_snapshot) - .to_display_point(map); - if self.reversed { - end..start - } else { - start..end - } - } - - fn spanned_rows( - &self, - include_end_if_at_line_start: bool, - map: &DisplaySnapshot, - ) -> Range { - let start = self.start.to_point(&map.buffer_snapshot); - let mut end = self.end.to_point(&map.buffer_snapshot); - if !include_end_if_at_line_start && start.row != end.row && end.column == 0 { - end.row -= 1; - } - - let buffer_start = map.prev_line_boundary(start).0; - let buffer_end = map.next_line_boundary(end).0; - MultiBufferRow(buffer_start.row)..MultiBufferRow(buffer_end.row + 1) - } -} - -impl InvalidationStack { - fn invalidate(&mut self, selections: &[Selection], buffer: &MultiBufferSnapshot) - where - S: Clone + ToOffset, - { - while let Some(region) = self.last() { - let all_selections_inside_invalidation_ranges = - if selections.len() == region.ranges().len() { - selections - .iter() - .zip(region.ranges().iter().map(|r| r.to_offset(buffer))) - .all(|(selection, invalidation_range)| { - let head = selection.head().to_offset(buffer); - invalidation_range.start <= head && invalidation_range.end >= head - }) - } else { - false - }; - - if all_selections_inside_invalidation_ranges { - break; - } else { - self.pop(); - } - } - } -} - -impl Default for InvalidationStack { - fn default() -> Self { - Self(Default::default()) - } -} - -impl Deref for InvalidationStack { - type Target = Vec; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for InvalidationStack { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl InvalidationRegion for SnippetState { - fn ranges(&self) -> &[Range] { - &self.ranges[self.active_index] - } -} - -fn inline_completion_edit_text( - current_snapshot: &BufferSnapshot, - edits: &[(Range, String)], - edit_preview: &EditPreview, - include_deletions: bool, - cx: &App, -) -> HighlightedText { - let edits = edits - .iter() - .map(|(anchor, text)| { - ( - anchor.start.text_anchor..anchor.end.text_anchor, - text.clone(), - ) - }) - .collect::>(); - - edit_preview.highlight_edits(current_snapshot, &edits, include_deletions, cx) -} - -pub fn diagnostic_style(severity: DiagnosticSeverity, colors: &StatusColors) -> Hsla { - match severity { - DiagnosticSeverity::ERROR => colors.error, - DiagnosticSeverity::WARNING => colors.warning, - DiagnosticSeverity::INFORMATION => colors.info, - DiagnosticSeverity::HINT => colors.info, - _ => colors.ignored, - } -} - -pub fn styled_runs_for_code_label<'a>( - label: &'a CodeLabel, - syntax_theme: &'a theme::SyntaxTheme, -) -> impl 'a + Iterator, HighlightStyle)> { - let fade_out = HighlightStyle { - fade_out: Some(0.35), - ..Default::default() - }; - - let mut prev_end = label.filter_range.end; - label - .runs - .iter() - .enumerate() - .flat_map(move |(ix, (range, highlight_id))| { - let style = if let Some(style) = highlight_id.style(syntax_theme) { - style - } else { - return Default::default(); - }; - let mut muted_style = style; - muted_style.highlight(fade_out); - - let mut runs = SmallVec::<[(Range, HighlightStyle); 3]>::new(); - if range.start >= label.filter_range.end { - if range.start > prev_end { - runs.push((prev_end..range.start, fade_out)); - } - runs.push((range.clone(), muted_style)); - } else if range.end <= label.filter_range.end { - runs.push((range.clone(), style)); - } else { - runs.push((range.start..label.filter_range.end, style)); - runs.push((label.filter_range.end..range.end, muted_style)); - } - prev_end = cmp::max(prev_end, range.end); - - if ix + 1 == label.runs.len() && label.text.len() > prev_end { - runs.push((prev_end..label.text.len(), fade_out)); - } - - runs - }) -} - -pub(crate) fn split_words(text: &str) -> impl std::iter::Iterator + '_ { - let mut prev_index = 0; - let mut prev_codepoint: Option = None; - text.char_indices() - .chain([(text.len(), '\0')]) - .filter_map(move |(index, codepoint)| { - let prev_codepoint = prev_codepoint.replace(codepoint)?; - let is_boundary = index == text.len() - || !prev_codepoint.is_uppercase() && codepoint.is_uppercase() - || !prev_codepoint.is_alphanumeric() && codepoint.is_alphanumeric(); - if is_boundary { - let chunk = &text[prev_index..index]; - prev_index = index; - Some(chunk) - } else { - None - } - }) -} - -pub trait RangeToAnchorExt: Sized { - fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range; - - fn to_display_points(self, snapshot: &EditorSnapshot) -> Range { - let anchor_range = self.to_anchors(&snapshot.buffer_snapshot); - anchor_range.start.to_display_point(snapshot)..anchor_range.end.to_display_point(snapshot) - } -} - -impl RangeToAnchorExt for Range { - fn to_anchors(self, snapshot: &MultiBufferSnapshot) -> Range { - let start_offset = self.start.to_offset(snapshot); - let end_offset = self.end.to_offset(snapshot); - if start_offset == end_offset { - snapshot.anchor_before(start_offset)..snapshot.anchor_before(end_offset) - } else { - snapshot.anchor_after(self.start)..snapshot.anchor_before(self.end) - } - } -} - -pub trait RowExt { - fn as_f32(&self) -> f32; - - fn next_row(&self) -> Self; - - fn previous_row(&self) -> Self; - - fn minus(&self, other: Self) -> u32; -} - -impl RowExt for DisplayRow { - fn as_f32(&self) -> f32 { - self.0 as f32 - } - - fn next_row(&self) -> Self { - Self(self.0 + 1) - } - - fn previous_row(&self) -> Self { - Self(self.0.saturating_sub(1)) - } - - fn minus(&self, other: Self) -> u32 { - self.0 - other.0 - } -} - -impl RowExt for MultiBufferRow { - fn as_f32(&self) -> f32 { - self.0 as f32 - } - - fn next_row(&self) -> Self { - Self(self.0 + 1) - } - - fn previous_row(&self) -> Self { - Self(self.0.saturating_sub(1)) - } - - fn minus(&self, other: Self) -> u32 { - self.0 - other.0 - } -} - -trait RowRangeExt { - type Row; - - fn len(&self) -> usize; - - fn iter_rows(&self) -> impl DoubleEndedIterator; -} - -impl RowRangeExt for Range { - type Row = MultiBufferRow; - - fn len(&self) -> usize { - (self.end.0 - self.start.0) as usize - } - - fn iter_rows(&self) -> impl DoubleEndedIterator { - (self.start.0..self.end.0).map(MultiBufferRow) - } -} - -impl RowRangeExt for Range { - type Row = DisplayRow; - - fn len(&self) -> usize { - (self.end.0 - self.start.0) as usize - } - - fn iter_rows(&self) -> impl DoubleEndedIterator { - (self.start.0..self.end.0).map(DisplayRow) - } -} - -/// If select range has more than one line, we -/// just point the cursor to range.start. -fn collapse_multiline_range(range: Range) -> Range { - if range.start.row == range.end.row { - range - } else { - range.start..range.start - } -} -pub struct KillRing(ClipboardItem); -impl Global for KillRing {} - -const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50); - -enum BreakpointPromptEditAction { - Log, - Condition, - HitCondition, -} - -struct BreakpointPromptEditor { - pub(crate) prompt: Entity, - editor: WeakEntity, - breakpoint_anchor: Anchor, - breakpoint: Breakpoint, - edit_action: BreakpointPromptEditAction, - block_ids: HashSet, - gutter_dimensions: Arc>, - _subscriptions: Vec, -} - -impl BreakpointPromptEditor { - const MAX_LINES: u8 = 4; - - fn new( - editor: WeakEntity, - breakpoint_anchor: Anchor, - breakpoint: Breakpoint, - edit_action: BreakpointPromptEditAction, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let base_text = match edit_action { - BreakpointPromptEditAction::Log => breakpoint.message.as_ref(), - BreakpointPromptEditAction::Condition => breakpoint.condition.as_ref(), - BreakpointPromptEditAction::HitCondition => breakpoint.hit_condition.as_ref(), - } - .map(|msg| msg.to_string()) - .unwrap_or_default(); - - let buffer = cx.new(|cx| Buffer::local(base_text, cx)); - let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); - - let prompt = cx.new(|cx| { - let mut prompt = Editor::new( - EditorMode::AutoHeight { - max_lines: Self::MAX_LINES as usize, - }, - buffer, - None, - window, - cx, - ); - prompt.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); - prompt.set_show_cursor_when_unfocused(false, cx); - prompt.set_placeholder_text( - match edit_action { - BreakpointPromptEditAction::Log => "Message to log when a breakpoint is hit. Expressions within {} are interpolated.", - BreakpointPromptEditAction::Condition => "Condition when a breakpoint is hit. Expressions within {} are interpolated.", - BreakpointPromptEditAction::HitCondition => "How many breakpoint hits to ignore", - }, - cx, - ); - - prompt - }); - - Self { - prompt, - editor, - breakpoint_anchor, - breakpoint, - edit_action, - gutter_dimensions: Arc::new(Mutex::new(GutterDimensions::default())), - block_ids: Default::default(), - _subscriptions: vec![], - } - } - - pub(crate) fn add_block_ids(&mut self, block_ids: Vec) { - self.block_ids.extend(block_ids) - } - - fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) { - if let Some(editor) = self.editor.upgrade() { - let message = self - .prompt - .read(cx) - .buffer - .read(cx) - .as_singleton() - .expect("A multi buffer in breakpoint prompt isn't possible") - .read(cx) - .as_rope() - .to_string(); - - editor.update(cx, |editor, cx| { - editor.edit_breakpoint_at_anchor( - self.breakpoint_anchor, - self.breakpoint.clone(), - match self.edit_action { - BreakpointPromptEditAction::Log => { - BreakpointEditAction::EditLogMessage(message.into()) - } - BreakpointPromptEditAction::Condition => { - BreakpointEditAction::EditCondition(message.into()) - } - BreakpointPromptEditAction::HitCondition => { - BreakpointEditAction::EditHitCondition(message.into()) - } - }, - cx, - ); - - editor.remove_blocks(self.block_ids.clone(), None, cx); - cx.focus_self(window); - }); - } - } - - fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context) { - self.editor - .update(cx, |editor, cx| { - editor.remove_blocks(self.block_ids.clone(), None, cx); - window.focus(&editor.focus_handle); - }) - .log_err(); - } - - fn render_prompt_editor(&self, cx: &mut Context) -> impl IntoElement { - let settings = ThemeSettings::get_global(cx); - let text_style = TextStyle { - color: if self.prompt.read(cx).read_only(cx) { - cx.theme().colors().text_disabled - } else { - cx.theme().colors().text - }, - font_family: settings.buffer_font.family.clone(), - font_fallbacks: settings.buffer_font.fallbacks.clone(), - font_size: settings.buffer_font_size(cx).into(), - font_weight: settings.buffer_font.weight, - line_height: relative(settings.buffer_line_height.value()), - ..Default::default() - }; - EditorElement::new( - &self.prompt, - EditorStyle { - background: cx.theme().colors().editor_background, - local_player: cx.theme().players().local(), - text: text_style, - ..Default::default() - }, - ) - } -} - -impl Render for BreakpointPromptEditor { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let gutter_dimensions = *self.gutter_dimensions.lock(); - h_flex() - .key_context("Editor") - .bg(cx.theme().colors().editor_background) - .border_y_1() - .border_color(cx.theme().status().info_border) - .size_full() - .py(window.line_height() / 2.5) - .on_action(cx.listener(Self::confirm)) - .on_action(cx.listener(Self::cancel)) - .child(h_flex().w(gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0))) - .child(div().flex_1().child(self.render_prompt_editor(cx))) - } -} - -impl Focusable for BreakpointPromptEditor { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.prompt.focus_handle(cx) - } -} - -fn all_edits_insertions_or_deletions( - edits: &Vec<(Range, String)>, - snapshot: &MultiBufferSnapshot, -) -> bool { - let mut all_insertions = true; - let mut all_deletions = true; - - for (range, new_text) in edits.iter() { - let range_is_empty = range.to_offset(&snapshot).is_empty(); - let text_is_empty = new_text.is_empty(); - - if range_is_empty != text_is_empty { - if range_is_empty { - all_deletions = false; - } else { - all_insertions = false; - } - } else { - return false; - } - - if !all_insertions && !all_deletions { - return false; - } - } - all_insertions || all_deletions -} - -struct MissingEditPredictionKeybindingTooltip; - -impl Render for MissingEditPredictionKeybindingTooltip { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - ui::tooltip_container(window, cx, |container, _, cx| { - container - .flex_shrink_0() - .max_w_80() - .min_h(rems_from_px(124.)) - .justify_between() - .child( - v_flex() - .flex_1() - .text_ui_sm(cx) - .child(Label::new("Conflict with Accept Keybinding")) - .child("Your keymap currently overrides the default accept keybinding. To continue, assign one keybinding for the `editor::AcceptEditPrediction` action.") - ) - .child( - h_flex() - .pb_1() - .gap_1() - .items_end() - .w_full() - .child(Button::new("open-keymap", "Assign Keybinding").size(ButtonSize::Compact).on_click(|_ev, window, cx| { - window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx) - })) - .child(Button::new("see-docs", "See Docs").size(ButtonSize::Compact).on_click(|_ev, _window, cx| { - cx.open_url("https://zed.dev/docs/completions#edit-predictions-missing-keybinding"); - })), - ) - }) - } -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub struct LineHighlight { - pub background: Background, - pub border: Option, - pub include_gutter: bool, - pub type_id: Option, -} - -fn render_diff_hunk_controls( - row: u32, - status: &DiffHunkStatus, - hunk_range: Range, - is_created_file: bool, - line_height: Pixels, - editor: &Entity, - _window: &mut Window, - cx: &mut App, -) -> AnyElement { - h_flex() - .h(line_height) - .mr_1() - .gap_1() - .px_0p5() - .pb_1() - .border_x_1() - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .rounded_b_lg() - .bg(cx.theme().colors().editor_background) - .gap_1() - .occlude() - .shadow_md() - .child(if status.has_secondary_hunk() { - Button::new(("stage", row as u64), "Stage") - .alpha(if status.is_pending() { 0.66 } else { 1.0 }) - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |window, cx| { - Tooltip::for_action_in( - "Stage Hunk", - &::git::ToggleStaged, - &focus_handle, - window, - cx, - ) - } - }) - .on_click({ - let editor = editor.clone(); - move |_event, _window, cx| { - editor.update(cx, |editor, cx| { - editor.stage_or_unstage_diff_hunks( - true, - vec![hunk_range.start..hunk_range.start], - cx, - ); - }); - } - }) - } else { - Button::new(("unstage", row as u64), "Unstage") - .alpha(if status.is_pending() { 0.66 } else { 1.0 }) - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |window, cx| { - Tooltip::for_action_in( - "Unstage Hunk", - &::git::ToggleStaged, - &focus_handle, - window, - cx, - ) - } - }) - .on_click({ - let editor = editor.clone(); - move |_event, _window, cx| { - editor.update(cx, |editor, cx| { - editor.stage_or_unstage_diff_hunks( - false, - vec![hunk_range.start..hunk_range.start], - cx, - ); - }); - } - }) - }) - .child( - Button::new(("restore", row as u64), "Restore") - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |window, cx| { - Tooltip::for_action_in( - "Restore Hunk", - &::git::Restore, - &focus_handle, - window, - cx, - ) - } - }) - .on_click({ - let editor = editor.clone(); - move |_event, window, cx| { - editor.update(cx, |editor, cx| { - let snapshot = editor.snapshot(window, cx); - let point = hunk_range.start.to_point(&snapshot.buffer_snapshot); - editor.restore_hunks_in_ranges(vec![point..point], window, cx); - }); - } - }) - .disabled(is_created_file), - ) - .when( - !editor.read(cx).buffer().read(cx).all_diff_hunks_expanded(), - |el| { - el.child( - IconButton::new(("next-hunk", row as u64), IconName::ArrowDown) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - // .disabled(!has_multiple_hunks) - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |window, cx| { - Tooltip::for_action_in( - "Next Hunk", - &GoToHunk, - &focus_handle, - window, - cx, - ) - } - }) - .on_click({ - let editor = editor.clone(); - move |_event, window, cx| { - editor.update(cx, |editor, cx| { - let snapshot = editor.snapshot(window, cx); - let position = - hunk_range.end.to_point(&snapshot.buffer_snapshot); - editor.go_to_hunk_before_or_after_position( - &snapshot, - position, - Direction::Next, - window, - cx, - ); - editor.expand_selected_diff_hunks(cx); - }); - } - }), - ) - .child( - IconButton::new(("prev-hunk", row as u64), IconName::ArrowUp) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - // .disabled(!has_multiple_hunks) - .tooltip({ - let focus_handle = editor.focus_handle(cx); - move |window, cx| { - Tooltip::for_action_in( - "Previous Hunk", - &GoToPreviousHunk, - &focus_handle, - window, - cx, - ) - } - }) - .on_click({ - let editor = editor.clone(); - move |_event, window, cx| { - editor.update(cx, |editor, cx| { - let snapshot = editor.snapshot(window, cx); - let point = - hunk_range.start.to_point(&snapshot.buffer_snapshot); - editor.go_to_hunk_before_or_after_position( - &snapshot, - point, - Direction::Prev, - window, - cx, - ); - editor.expand_selected_diff_hunks(cx); - }); - } - }), - ) - }, - ) - .into_any_element() -} diff --git a/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-01.diff b/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-01.diff deleted file mode 100644 index 1a38a1967f..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-01.diff +++ /dev/null @@ -1,28 +0,0 @@ ---- before.rs 2025-07-07 11:37:48.434629001 +0300 -+++ expected.rs 2025-07-14 10:33:53.346906775 +0300 -@@ -1780,11 +1780,11 @@ - cx.observe_window_activation(window, |editor, window, cx| { - let active = window.is_window_active(); - editor.blink_manager.update(cx, |blink_manager, cx| { -- if active { -- blink_manager.enable(cx); -- } else { -- blink_manager.disable(cx); -- } -+ // if active { -+ // blink_manager.enable(cx); -+ // } else { -+ // blink_manager.disable(cx); -+ // } - }); - }), - ], -@@ -18463,7 +18463,7 @@ - } - - self.blink_manager.update(cx, |blink_manager, cx| { -- blink_manager.enable(cx); -+ // blink_manager.enable(cx); - }); - self.show_cursor_names(window, cx); - self.buffer.update(cx, |buffer, cx| { diff --git a/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-02.diff b/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-02.diff deleted file mode 100644 index b484cce48f..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-02.diff +++ /dev/null @@ -1,29 +0,0 @@ -@@ -1778,13 +1778,13 @@ - cx.observe_global_in::(window, Self::settings_changed), - observe_buffer_font_size_adjustment(cx, |_, cx| cx.notify()), - cx.observe_window_activation(window, |editor, window, cx| { -- let active = window.is_window_active(); -+ // let active = window.is_window_active(); - editor.blink_manager.update(cx, |blink_manager, cx| { -- if active { -- blink_manager.enable(cx); -- } else { -- blink_manager.disable(cx); -- } -+ // if active { -+ // blink_manager.enable(cx); -+ // } else { -+ // blink_manager.disable(cx); -+ // } - }); - }), - ], -@@ -18463,7 +18463,7 @@ - } - - self.blink_manager.update(cx, |blink_manager, cx| { -- blink_manager.enable(cx); -+ // blink_manager.enable(cx); - }); - self.show_cursor_names(window, cx); - self.buffer.update(cx, |buffer, cx| { diff --git a/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-03.diff b/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-03.diff deleted file mode 100644 index 431e34e48a..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-03.diff +++ /dev/null @@ -1,34 +0,0 @@ -@@ -1774,17 +1774,17 @@ - cx.observe(&buffer, Self::on_buffer_changed), - cx.subscribe_in(&buffer, window, Self::on_buffer_event), - cx.observe_in(&display_map, window, Self::on_display_map_changed), -- cx.observe(&blink_manager, |_, _, cx| cx.notify()), -+ // cx.observe(&blink_manager, |_, _, cx| cx.notify()), - cx.observe_global_in::(window, Self::settings_changed), - observe_buffer_font_size_adjustment(cx, |_, cx| cx.notify()), - cx.observe_window_activation(window, |editor, window, cx| { -- let active = window.is_window_active(); -+ // let active = window.is_window_active(); - editor.blink_manager.update(cx, |blink_manager, cx| { -- if active { -- blink_manager.enable(cx); -- } else { -- blink_manager.disable(cx); -- } -+ // if active { -+ // blink_manager.enable(cx); -+ // } else { -+ // blink_manager.disable(cx); -+ // } - }); - }), - ], -@@ -18463,7 +18463,7 @@ - } - - self.blink_manager.update(cx, |blink_manager, cx| { -- blink_manager.enable(cx); -+ // blink_manager.enable(cx); - }); - self.show_cursor_names(window, cx); - self.buffer.update(cx, |buffer, cx| { diff --git a/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-04.diff b/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-04.diff deleted file mode 100644 index 64a6b85dd3..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/disable_cursor_blinking/possible-04.diff +++ /dev/null @@ -1,33 +0,0 @@ -@@ -1774,17 +1774,17 @@ - cx.observe(&buffer, Self::on_buffer_changed), - cx.subscribe_in(&buffer, window, Self::on_buffer_event), - cx.observe_in(&display_map, window, Self::on_display_map_changed), -- cx.observe(&blink_manager, |_, _, cx| cx.notify()), -+ // cx.observe(&blink_manager, |_, _, cx| cx.notify()), - cx.observe_global_in::(window, Self::settings_changed), - observe_buffer_font_size_adjustment(cx, |_, cx| cx.notify()), - cx.observe_window_activation(window, |editor, window, cx| { - let active = window.is_window_active(); - editor.blink_manager.update(cx, |blink_manager, cx| { -- if active { -- blink_manager.enable(cx); -- } else { -- blink_manager.disable(cx); -- } -+ // if active { -+ // blink_manager.enable(cx); -+ // } else { -+ // blink_manager.disable(cx); -+ // } - }); - }), - ], -@@ -18463,7 +18463,7 @@ - } - - self.blink_manager.update(cx, |blink_manager, cx| { -- blink_manager.enable(cx); -+ // blink_manager.enable(cx); - }); - self.show_cursor_names(window, cx); - self.buffer.update(cx, |buffer, cx| { diff --git a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/before.rs b/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/before.rs deleted file mode 100644 index 36fccb5132..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/before.rs +++ /dev/null @@ -1,371 +0,0 @@ -use crate::commit::get_messages; -use crate::{GitRemote, Oid}; -use anyhow::{Context as _, Result, anyhow}; -use collections::{HashMap, HashSet}; -use futures::AsyncWriteExt; -use gpui::SharedString; -use serde::{Deserialize, Serialize}; -use std::process::Stdio; -use std::{ops::Range, path::Path}; -use text::Rope; -use time::OffsetDateTime; -use time::UtcOffset; -use time::macros::format_description; - -pub use git2 as libgit; - -#[derive(Debug, Clone, Default)] -pub struct Blame { - pub entries: Vec, - pub messages: HashMap, - pub remote_url: Option, -} - -#[derive(Clone, Debug, Default)] -pub struct ParsedCommitMessage { - pub message: SharedString, - pub permalink: Option, - pub pull_request: Option, - pub remote: Option, -} - -impl Blame { - pub async fn for_path( - git_binary: &Path, - working_directory: &Path, - path: &Path, - content: &Rope, - remote_url: Option, - ) -> Result { - let output = run_git_blame(git_binary, working_directory, path, content).await?; - let mut entries = parse_git_blame(&output)?; - entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start)); - - let mut unique_shas = HashSet::default(); - - for entry in entries.iter_mut() { - unique_shas.insert(entry.sha); - } - - let shas = unique_shas.into_iter().collect::>(); - let messages = get_messages(working_directory, &shas) - .await - .context("failed to get commit messages")?; - - Ok(Self { - entries, - messages, - remote_url, - }) - } -} - -const GIT_BLAME_NO_COMMIT_ERROR: &str = "fatal: no such ref: HEAD"; -const GIT_BLAME_NO_PATH: &str = "fatal: no such path"; - -async fn run_git_blame( - git_binary: &Path, - working_directory: &Path, - path: &Path, - contents: &Rope, -) -> Result { - let mut child = util::command::new_smol_command(git_binary) - .current_dir(working_directory) - .arg("blame") - .arg("--incremental") - .arg("--contents") - .arg("-") - .arg(path.as_os_str()) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .context("starting git blame process")?; - - let stdin = child - .stdin - .as_mut() - .context("failed to get pipe to stdin of git blame command")?; - - for chunk in contents.chunks() { - stdin.write_all(chunk.as_bytes()).await?; - } - stdin.flush().await?; - - let output = child.output().await.context("reading git blame output")?; - - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - let trimmed = stderr.trim(); - if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) { - return Ok(String::new()); - } - anyhow::bail!("git blame process failed: {stderr}"); - } - - Ok(String::from_utf8(output.stdout)?) -} - -#[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] -pub struct BlameEntry { - pub sha: Oid, - - pub range: Range, - - pub original_line_number: u32, - - pub author: Option, - pub author_mail: Option, - pub author_time: Option, - pub author_tz: Option, - - pub committer_name: Option, - pub committer_email: Option, - pub committer_time: Option, - pub committer_tz: Option, - - pub summary: Option, - - pub previous: Option, - pub filename: String, -} - -impl BlameEntry { - // Returns a BlameEntry by parsing the first line of a `git blame --incremental` - // entry. The line MUST have this format: - // - // <40-byte-hex-sha1> - fn new_from_blame_line(line: &str) -> Result { - let mut parts = line.split_whitespace(); - - let sha = parts - .next() - .and_then(|line| line.parse::().ok()) - .with_context(|| format!("parsing sha from {line}"))?; - - let original_line_number = parts - .next() - .and_then(|line| line.parse::().ok()) - .with_context(|| format!("parsing original line number from {line}"))?; - let final_line_number = parts - .next() - .and_then(|line| line.parse::().ok()) - .with_context(|| format!("parsing final line number from {line}"))?; - - let line_count = parts - .next() - .and_then(|line| line.parse::().ok()) - .with_context(|| format!("parsing line count from {line}"))?; - - let start_line = final_line_number.saturating_sub(1); - let end_line = start_line + line_count; - let range = start_line..end_line; - - Ok(Self { - sha, - range, - original_line_number, - ..Default::default() - }) - } - - pub fn author_offset_date_time(&self) -> Result { - if let (Some(author_time), Some(author_tz)) = (self.author_time, &self.author_tz) { - let format = format_description!("[offset_hour][offset_minute]"); - let offset = UtcOffset::parse(author_tz, &format)?; - let date_time_utc = OffsetDateTime::from_unix_timestamp(author_time)?; - - Ok(date_time_utc.to_offset(offset)) - } else { - // Directly return current time in UTC if there's no committer time or timezone - Ok(time::OffsetDateTime::now_utc()) - } - } -} - -// parse_git_blame parses the output of `git blame --incremental`, which returns -// all the blame-entries for a given path incrementally, as it finds them. -// -// Each entry *always* starts with: -// -// <40-byte-hex-sha1> -// -// Each entry *always* ends with: -// -// filename -// -// Line numbers are 1-indexed. -// -// A `git blame --incremental` entry looks like this: -// -// 6ad46b5257ba16d12c5ca9f0d4900320959df7f4 2 2 1 -// author Joe Schmoe -// author-mail -// author-time 1709741400 -// author-tz +0100 -// committer Joe Schmoe -// committer-mail -// committer-time 1709741400 -// committer-tz +0100 -// summary Joe's cool commit -// previous 486c2409237a2c627230589e567024a96751d475 index.js -// filename index.js -// -// If the entry has the same SHA as an entry that was already printed then no -// signature information is printed: -// -// 6ad46b5257ba16d12c5ca9f0d4900320959df7f4 3 4 1 -// previous 486c2409237a2c627230589e567024a96751d475 index.js -// filename index.js -// -// More about `--incremental` output: https://mirrors.edge.kernel.org/pub/software/scm/git/docs/git-blame.html -fn parse_git_blame(output: &str) -> Result> { - let mut entries: Vec = Vec::new(); - let mut index: HashMap = HashMap::default(); - - let mut current_entry: Option = None; - - for line in output.lines() { - let mut done = false; - - match &mut current_entry { - None => { - let mut new_entry = BlameEntry::new_from_blame_line(line)?; - - if let Some(existing_entry) = index - .get(&new_entry.sha) - .and_then(|slot| entries.get(*slot)) - { - new_entry.author.clone_from(&existing_entry.author); - new_entry - .author_mail - .clone_from(&existing_entry.author_mail); - new_entry.author_time = existing_entry.author_time; - new_entry.author_tz.clone_from(&existing_entry.author_tz); - new_entry - .committer_name - .clone_from(&existing_entry.committer_name); - new_entry - .committer_email - .clone_from(&existing_entry.committer_email); - new_entry.committer_time = existing_entry.committer_time; - new_entry - .committer_tz - .clone_from(&existing_entry.committer_tz); - new_entry.summary.clone_from(&existing_entry.summary); - } - - current_entry.replace(new_entry); - } - Some(entry) => { - let Some((key, value)) = line.split_once(' ') else { - continue; - }; - let is_committed = !entry.sha.is_zero(); - match key { - "filename" => { - entry.filename = value.into(); - done = true; - } - "previous" => entry.previous = Some(value.into()), - - "summary" if is_committed => entry.summary = Some(value.into()), - "author" if is_committed => entry.author = Some(value.into()), - "author-mail" if is_committed => entry.author_mail = Some(value.into()), - "author-time" if is_committed => { - entry.author_time = Some(value.parse::()?) - } - "author-tz" if is_committed => entry.author_tz = Some(value.into()), - - "committer" if is_committed => entry.committer_name = Some(value.into()), - "committer-mail" if is_committed => entry.committer_email = Some(value.into()), - "committer-time" if is_committed => { - entry.committer_time = Some(value.parse::()?) - } - "committer-tz" if is_committed => entry.committer_tz = Some(value.into()), - _ => {} - } - } - }; - - if done { - if let Some(entry) = current_entry.take() { - index.insert(entry.sha, entries.len()); - - // We only want annotations that have a commit. - if !entry.sha.is_zero() { - entries.push(entry); - } - } - } - } - - Ok(entries) -} - -#[cfg(test)] -mod tests { - use std::path::PathBuf; - - use super::BlameEntry; - use super::parse_git_blame; - - fn read_test_data(filename: &str) -> String { - let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - path.push("test_data"); - path.push(filename); - - std::fs::read_to_string(&path) - .unwrap_or_else(|_| panic!("Could not read test data at {:?}. Is it generated?", path)) - } - - fn assert_eq_golden(entries: &Vec, golden_filename: &str) { - let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - path.push("test_data"); - path.push("golden"); - path.push(format!("{}.json", golden_filename)); - - let mut have_json = - serde_json::to_string_pretty(&entries).expect("could not serialize entries to JSON"); - // We always want to save with a trailing newline. - have_json.push('\n'); - - let update = std::env::var("UPDATE_GOLDEN") - .map(|val| val.eq_ignore_ascii_case("true")) - .unwrap_or(false); - - if update { - std::fs::create_dir_all(path.parent().unwrap()) - .expect("could not create golden test data directory"); - std::fs::write(&path, have_json).expect("could not write out golden data"); - } else { - let want_json = - std::fs::read_to_string(&path).unwrap_or_else(|_| { - panic!("could not read golden test data file at {:?}. Did you run the test with UPDATE_GOLDEN=true before?", path); - }).replace("\r\n", "\n"); - - pretty_assertions::assert_eq!(have_json, want_json, "wrong blame entries"); - } - } - - #[test] - fn test_parse_git_blame_not_committed() { - let output = read_test_data("blame_incremental_not_committed"); - let entries = parse_git_blame(&output).unwrap(); - assert_eq_golden(&entries, "blame_incremental_not_committed"); - } - - #[test] - fn test_parse_git_blame_simple() { - let output = read_test_data("blame_incremental_simple"); - let entries = parse_git_blame(&output).unwrap(); - assert_eq_golden(&entries, "blame_incremental_simple"); - } - - #[test] - fn test_parse_git_blame_complex() { - let output = read_test_data("blame_incremental_complex"); - let entries = parse_git_blame(&output).unwrap(); - assert_eq_golden(&entries, "blame_incremental_complex"); - } -} diff --git a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-01.diff b/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-01.diff deleted file mode 100644 index c13a223c63..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-01.diff +++ /dev/null @@ -1,11 +0,0 @@ -@@ -94,6 +94,10 @@ - - let output = child.output().await.context("reading git blame output")?; - -+ handle_command_output(output) -+} -+ -+fn handle_command_output(output: std::process::Output) -> Result { - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - let trimmed = stderr.trim(); diff --git a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-02.diff b/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-02.diff deleted file mode 100644 index aa36a9241e..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-02.diff +++ /dev/null @@ -1,26 +0,0 @@ -@@ -95,15 +95,19 @@ - let output = child.output().await.context("reading git blame output")?; - - if !output.status.success() { -- let stderr = String::from_utf8_lossy(&output.stderr); -- let trimmed = stderr.trim(); -- if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) { -- return Ok(String::new()); -- } -- anyhow::bail!("git blame process failed: {stderr}"); -+ return handle_command_output(output); - } - - Ok(String::from_utf8(output.stdout)?) -+} -+ -+fn handle_command_output(output: std::process::Output) -> Result { -+ let stderr = String::from_utf8_lossy(&output.stderr); -+ let trimmed = stderr.trim(); -+ if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) { -+ return Ok(String::new()); -+ } -+ anyhow::bail!("git blame process failed: {stderr}"); - } - - #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] diff --git a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-03.diff b/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-03.diff deleted file mode 100644 index d3c19b4380..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-03.diff +++ /dev/null @@ -1,11 +0,0 @@ -@@ -93,7 +93,10 @@ - stdin.flush().await?; - - let output = child.output().await.context("reading git blame output")?; -+ handle_command_output(output) -+} - -+fn handle_command_output(output: std::process::Output) -> Result { - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - let trimmed = stderr.trim(); diff --git a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-04.diff b/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-04.diff deleted file mode 100644 index 1f87e4352c..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-04.diff +++ /dev/null @@ -1,24 +0,0 @@ -@@ -93,17 +93,20 @@ - stdin.flush().await?; - - let output = child.output().await.context("reading git blame output")?; -+ handle_command_output(&output)?; -+ Ok(String::from_utf8(output.stdout)?) -+} - -+fn handle_command_output(output: &std::process::Output) -> Result<()> { - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - let trimmed = stderr.trim(); - if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) { -- return Ok(String::new()); -+ return Ok(()); - } - anyhow::bail!("git blame process failed: {stderr}"); - } -- -- Ok(String::from_utf8(output.stdout)?) -+ Ok(()) - } - - #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] diff --git a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-05.diff b/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-05.diff deleted file mode 100644 index 8f4b745b9a..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-05.diff +++ /dev/null @@ -1,26 +0,0 @@ -@@ -95,15 +95,19 @@ - let output = child.output().await.context("reading git blame output")?; - - if !output.status.success() { -- let stderr = String::from_utf8_lossy(&output.stderr); -- let trimmed = stderr.trim(); -- if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) { -- return Ok(String::new()); -- } -- anyhow::bail!("git blame process failed: {stderr}"); -+ return handle_command_output(&output); - } - - Ok(String::from_utf8(output.stdout)?) -+} -+ -+fn handle_command_output(output: &std::process::Output) -> Result { -+ let stderr = String::from_utf8_lossy(&output.stderr); -+ let trimmed = stderr.trim(); -+ if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) { -+ return Ok(String::new()); -+ } -+ anyhow::bail!("git blame process failed: {stderr}"); - } - - #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] diff --git a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-06.diff b/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-06.diff deleted file mode 100644 index 3514d9c8e2..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-06.diff +++ /dev/null @@ -1,23 +0,0 @@ -@@ -93,7 +93,12 @@ - stdin.flush().await?; - - let output = child.output().await.context("reading git blame output")?; -+ handle_command_output(&output)?; - -+ Ok(String::from_utf8(output.stdout)?) -+} -+ -+fn handle_command_output(output: &std::process::Output) -> Result { - if !output.status.success() { - let stderr = String::from_utf8_lossy(&output.stderr); - let trimmed = stderr.trim(); -@@ -102,8 +107,7 @@ - } - anyhow::bail!("git blame process failed: {stderr}"); - } -- -- Ok(String::from_utf8(output.stdout)?) -+ Ok(String::from_utf8_lossy(&output.stdout).into_owned()) - } - - #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] diff --git a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-07.diff b/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-07.diff deleted file mode 100644 index 9691479e29..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-07.diff +++ /dev/null @@ -1,26 +0,0 @@ -@@ -95,15 +95,19 @@ - let output = child.output().await.context("reading git blame output")?; - - if !output.status.success() { -- let stderr = String::from_utf8_lossy(&output.stderr); -- let trimmed = stderr.trim(); -- if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) { -- return Ok(String::new()); -- } -- anyhow::bail!("git blame process failed: {stderr}"); -+ return handle_command_output(output); - } - - Ok(String::from_utf8(output.stdout)?) -+} -+ -+fn handle_command_output(output: std::process::Output) -> Result { -+ let stderr = String::from_utf8_lossy(&output.stderr); -+ let trimmed = stderr.trim(); -+ if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) { -+ return Ok(String::new()); -+ } -+ anyhow::bail!("git blame process failed: {stderr}"); - } - - #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] diff --git a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-08.diff b/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-08.diff deleted file mode 100644 index f5da859005..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/extract_handle_command_output/possible-08.diff +++ /dev/null @@ -1,26 +0,0 @@ -@@ -95,15 +95,19 @@ - let output = child.output().await.context("reading git blame output")?; - - if !output.status.success() { -- let stderr = String::from_utf8_lossy(&output.stderr); -- let trimmed = stderr.trim(); -- if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) { -- return Ok(String::new()); -- } -- anyhow::bail!("git blame process failed: {stderr}"); -+ return handle_command_output(output); - } - - Ok(String::from_utf8(output.stdout)?) -+} -+ -+fn handle_command_output(output: std::process::Output) -> Result { -+ let stderr = String::from_utf8_lossy(&output.stderr); -+ let trimmed = stderr.trim(); -+ if trimmed == GIT_BLAME_NO_COMMIT_ERROR || trimmed.contains(GIT_BLAME_NO_PATH) { -+ return Ok(String::new()); -+ } -+ anyhow::bail!("git blame process failed: {stderr}") - } - - #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq, Eq)] diff --git a/crates/agent/src/edit_agent/evals/fixtures/from_pixels_constructor/before.rs b/crates/agent/src/edit_agent/evals/fixtures/from_pixels_constructor/before.rs deleted file mode 100644 index 12590fe6e9..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/from_pixels_constructor/before.rs +++ /dev/null @@ -1,339 +0,0 @@ -// font-kit/src/canvas.rs -// -// Copyright © 2018 The Pathfinder Project Developers. -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -//! An in-memory bitmap surface for glyph rasterization. - -use lazy_static::lazy_static; -use pathfinder_geometry::rect::RectI; -use pathfinder_geometry::vector::Vector2I; -use std::cmp; -use std::fmt; - -use crate::utils; - -lazy_static! { - static ref BITMAP_1BPP_TO_8BPP_LUT: [[u8; 8]; 256] = { - let mut lut = [[0; 8]; 256]; - for byte in 0..0x100 { - let mut value = [0; 8]; - for bit in 0..8 { - if (byte & (0x80 >> bit)) != 0 { - value[bit] = 0xff; - } - } - lut[byte] = value - } - lut - }; -} - -/// An in-memory bitmap surface for glyph rasterization. -pub struct Canvas { - /// The raw pixel data. - pub pixels: Vec, - /// The size of the buffer, in pixels. - pub size: Vector2I, - /// The number of *bytes* between successive rows. - pub stride: usize, - /// The image format of the canvas. - pub format: Format, -} - -impl Canvas { - /// Creates a new blank canvas with the given pixel size and format. - /// - /// Stride is automatically calculated from width. - /// - /// The canvas is initialized with transparent black (all values 0). - #[inline] - pub fn new(size: Vector2I, format: Format) -> Canvas { - Canvas::with_stride( - size, - size.x() as usize * format.bytes_per_pixel() as usize, - format, - ) - } - - /// Creates a new blank canvas with the given pixel size, stride (number of bytes between - /// successive rows), and format. - /// - /// The canvas is initialized with transparent black (all values 0). - pub fn with_stride(size: Vector2I, stride: usize, format: Format) -> Canvas { - Canvas { - pixels: vec![0; stride * size.y() as usize], - size, - stride, - format, - } - } - - #[allow(dead_code)] - pub(crate) fn blit_from_canvas(&mut self, src: &Canvas) { - self.blit_from( - Vector2I::default(), - &src.pixels, - src.size, - src.stride, - src.format, - ) - } - - /// Blits to a rectangle with origin at `dst_point` and size according to `src_size`. - /// If the target area overlaps the boundaries of the canvas, only the drawable region is blitted. - /// `dst_point` and `src_size` are specified in pixels. `src_stride` is specified in bytes. - /// `src_stride` must be equal or larger than the actual data length. - #[allow(dead_code)] - pub(crate) fn blit_from( - &mut self, - dst_point: Vector2I, - src_bytes: &[u8], - src_size: Vector2I, - src_stride: usize, - src_format: Format, - ) { - assert_eq!( - src_stride * src_size.y() as usize, - src_bytes.len(), - "Number of pixels in src_bytes does not match stride and size." - ); - assert!( - src_stride >= src_size.x() as usize * src_format.bytes_per_pixel() as usize, - "src_stride must be >= than src_size.x()" - ); - - let dst_rect = RectI::new(dst_point, src_size); - let dst_rect = dst_rect.intersection(RectI::new(Vector2I::default(), self.size)); - let dst_rect = match dst_rect { - Some(dst_rect) => dst_rect, - None => return, - }; - - match (self.format, src_format) { - (Format::A8, Format::A8) - | (Format::Rgb24, Format::Rgb24) - | (Format::Rgba32, Format::Rgba32) => { - self.blit_from_with::(dst_rect, src_bytes, src_stride, src_format) - } - (Format::A8, Format::Rgb24) => { - self.blit_from_with::(dst_rect, src_bytes, src_stride, src_format) - } - (Format::Rgb24, Format::A8) => { - self.blit_from_with::(dst_rect, src_bytes, src_stride, src_format) - } - (Format::Rgb24, Format::Rgba32) => self - .blit_from_with::(dst_rect, src_bytes, src_stride, src_format), - (Format::Rgba32, Format::Rgb24) => self - .blit_from_with::(dst_rect, src_bytes, src_stride, src_format), - (Format::Rgba32, Format::A8) | (Format::A8, Format::Rgba32) => unimplemented!(), - } - } - - #[allow(dead_code)] - pub(crate) fn blit_from_bitmap_1bpp( - &mut self, - dst_point: Vector2I, - src_bytes: &[u8], - src_size: Vector2I, - src_stride: usize, - ) { - if self.format != Format::A8 { - unimplemented!() - } - - let dst_rect = RectI::new(dst_point, src_size); - let dst_rect = dst_rect.intersection(RectI::new(Vector2I::default(), self.size)); - let dst_rect = match dst_rect { - Some(dst_rect) => dst_rect, - None => return, - }; - - let size = dst_rect.size(); - - let dest_bytes_per_pixel = self.format.bytes_per_pixel() as usize; - let dest_row_stride = size.x() as usize * dest_bytes_per_pixel; - let src_row_stride = utils::div_round_up(size.x() as usize, 8); - - for y in 0..size.y() { - let (dest_row_start, src_row_start) = ( - (y + dst_rect.origin_y()) as usize * self.stride - + dst_rect.origin_x() as usize * dest_bytes_per_pixel, - y as usize * src_stride, - ); - let dest_row_end = dest_row_start + dest_row_stride; - let src_row_end = src_row_start + src_row_stride; - let dest_row_pixels = &mut self.pixels[dest_row_start..dest_row_end]; - let src_row_pixels = &src_bytes[src_row_start..src_row_end]; - for x in 0..src_row_stride { - let pattern = &BITMAP_1BPP_TO_8BPP_LUT[src_row_pixels[x] as usize]; - let dest_start = x * 8; - let dest_end = cmp::min(dest_start + 8, dest_row_stride); - let src = &pattern[0..(dest_end - dest_start)]; - dest_row_pixels[dest_start..dest_end].clone_from_slice(src); - } - } - } - - /// Blits to area `rect` using the data given in the buffer `src_bytes`. - /// `src_stride` must be specified in bytes. - /// The dimensions of `rect` must be in pixels. - fn blit_from_with( - &mut self, - rect: RectI, - src_bytes: &[u8], - src_stride: usize, - src_format: Format, - ) { - let src_bytes_per_pixel = src_format.bytes_per_pixel() as usize; - let dest_bytes_per_pixel = self.format.bytes_per_pixel() as usize; - - for y in 0..rect.height() { - let (dest_row_start, src_row_start) = ( - (y + rect.origin_y()) as usize * self.stride - + rect.origin_x() as usize * dest_bytes_per_pixel, - y as usize * src_stride, - ); - let dest_row_end = dest_row_start + rect.width() as usize * dest_bytes_per_pixel; - let src_row_end = src_row_start + rect.width() as usize * src_bytes_per_pixel; - let dest_row_pixels = &mut self.pixels[dest_row_start..dest_row_end]; - let src_row_pixels = &src_bytes[src_row_start..src_row_end]; - B::blit(dest_row_pixels, src_row_pixels) - } - } -} - -impl fmt::Debug for Canvas { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("Canvas") - .field("pixels", &self.pixels.len()) // Do not dump a vector content. - .field("size", &self.size) - .field("stride", &self.stride) - .field("format", &self.format) - .finish() - } -} - -/// The image format for the canvas. -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum Format { - /// Premultiplied R8G8B8A8, little-endian. - Rgba32, - /// R8G8B8, little-endian. - Rgb24, - /// A8. - A8, -} - -impl Format { - /// Returns the number of bits per pixel that this image format corresponds to. - #[inline] - pub fn bits_per_pixel(self) -> u8 { - match self { - Format::Rgba32 => 32, - Format::Rgb24 => 24, - Format::A8 => 8, - } - } - - /// Returns the number of color channels per pixel that this image format corresponds to. - #[inline] - pub fn components_per_pixel(self) -> u8 { - match self { - Format::Rgba32 => 4, - Format::Rgb24 => 3, - Format::A8 => 1, - } - } - - /// Returns the number of bits per color channel that this image format contains. - #[inline] - pub fn bits_per_component(self) -> u8 { - self.bits_per_pixel() / self.components_per_pixel() - } - - /// Returns the number of bytes per pixel that this image format corresponds to. - #[inline] - pub fn bytes_per_pixel(self) -> u8 { - self.bits_per_pixel() / 8 - } -} - -/// The antialiasing strategy that should be used when rasterizing glyphs. -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum RasterizationOptions { - /// "Black-and-white" rendering. Each pixel is either entirely on or off. - Bilevel, - /// Grayscale antialiasing. Only one channel is used. - GrayscaleAa, - /// Subpixel RGB antialiasing, for LCD screens. - SubpixelAa, -} - -trait Blit { - fn blit(dest: &mut [u8], src: &[u8]); -} - -struct BlitMemcpy; - -impl Blit for BlitMemcpy { - #[inline] - fn blit(dest: &mut [u8], src: &[u8]) { - dest.clone_from_slice(src) - } -} - -struct BlitRgb24ToA8; - -impl Blit for BlitRgb24ToA8 { - #[inline] - fn blit(dest: &mut [u8], src: &[u8]) { - // TODO(pcwalton): SIMD. - for (dest, src) in dest.iter_mut().zip(src.chunks(3)) { - *dest = src[1] - } - } -} - -struct BlitA8ToRgb24; - -impl Blit for BlitA8ToRgb24 { - #[inline] - fn blit(dest: &mut [u8], src: &[u8]) { - for (dest, src) in dest.chunks_mut(3).zip(src.iter()) { - dest[0] = *src; - dest[1] = *src; - dest[2] = *src; - } - } -} - -struct BlitRgba32ToRgb24; - -impl Blit for BlitRgba32ToRgb24 { - #[inline] - fn blit(dest: &mut [u8], src: &[u8]) { - // TODO(pcwalton): SIMD. - for (dest, src) in dest.chunks_mut(3).zip(src.chunks(4)) { - dest.copy_from_slice(&src[0..3]) - } - } -} - -struct BlitRgb24ToRgba32; - -impl Blit for BlitRgb24ToRgba32 { - fn blit(dest: &mut [u8], src: &[u8]) { - for (dest, src) in dest.chunks_mut(4).zip(src.chunks(3)) { - dest[0] = src[0]; - dest[1] = src[1]; - dest[2] = src[2]; - dest[3] = 255; - } - } -} diff --git a/crates/agent/src/edit_agent/evals/fixtures/translate_doc_comments/before.rs b/crates/agent/src/edit_agent/evals/fixtures/translate_doc_comments/before.rs deleted file mode 100644 index 12590fe6e9..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/translate_doc_comments/before.rs +++ /dev/null @@ -1,339 +0,0 @@ -// font-kit/src/canvas.rs -// -// Copyright © 2018 The Pathfinder Project Developers. -// -// Licensed under the Apache License, Version 2.0 or the MIT license -// , at your -// option. This file may not be copied, modified, or distributed -// except according to those terms. - -//! An in-memory bitmap surface for glyph rasterization. - -use lazy_static::lazy_static; -use pathfinder_geometry::rect::RectI; -use pathfinder_geometry::vector::Vector2I; -use std::cmp; -use std::fmt; - -use crate::utils; - -lazy_static! { - static ref BITMAP_1BPP_TO_8BPP_LUT: [[u8; 8]; 256] = { - let mut lut = [[0; 8]; 256]; - for byte in 0..0x100 { - let mut value = [0; 8]; - for bit in 0..8 { - if (byte & (0x80 >> bit)) != 0 { - value[bit] = 0xff; - } - } - lut[byte] = value - } - lut - }; -} - -/// An in-memory bitmap surface for glyph rasterization. -pub struct Canvas { - /// The raw pixel data. - pub pixels: Vec, - /// The size of the buffer, in pixels. - pub size: Vector2I, - /// The number of *bytes* between successive rows. - pub stride: usize, - /// The image format of the canvas. - pub format: Format, -} - -impl Canvas { - /// Creates a new blank canvas with the given pixel size and format. - /// - /// Stride is automatically calculated from width. - /// - /// The canvas is initialized with transparent black (all values 0). - #[inline] - pub fn new(size: Vector2I, format: Format) -> Canvas { - Canvas::with_stride( - size, - size.x() as usize * format.bytes_per_pixel() as usize, - format, - ) - } - - /// Creates a new blank canvas with the given pixel size, stride (number of bytes between - /// successive rows), and format. - /// - /// The canvas is initialized with transparent black (all values 0). - pub fn with_stride(size: Vector2I, stride: usize, format: Format) -> Canvas { - Canvas { - pixels: vec![0; stride * size.y() as usize], - size, - stride, - format, - } - } - - #[allow(dead_code)] - pub(crate) fn blit_from_canvas(&mut self, src: &Canvas) { - self.blit_from( - Vector2I::default(), - &src.pixels, - src.size, - src.stride, - src.format, - ) - } - - /// Blits to a rectangle with origin at `dst_point` and size according to `src_size`. - /// If the target area overlaps the boundaries of the canvas, only the drawable region is blitted. - /// `dst_point` and `src_size` are specified in pixels. `src_stride` is specified in bytes. - /// `src_stride` must be equal or larger than the actual data length. - #[allow(dead_code)] - pub(crate) fn blit_from( - &mut self, - dst_point: Vector2I, - src_bytes: &[u8], - src_size: Vector2I, - src_stride: usize, - src_format: Format, - ) { - assert_eq!( - src_stride * src_size.y() as usize, - src_bytes.len(), - "Number of pixels in src_bytes does not match stride and size." - ); - assert!( - src_stride >= src_size.x() as usize * src_format.bytes_per_pixel() as usize, - "src_stride must be >= than src_size.x()" - ); - - let dst_rect = RectI::new(dst_point, src_size); - let dst_rect = dst_rect.intersection(RectI::new(Vector2I::default(), self.size)); - let dst_rect = match dst_rect { - Some(dst_rect) => dst_rect, - None => return, - }; - - match (self.format, src_format) { - (Format::A8, Format::A8) - | (Format::Rgb24, Format::Rgb24) - | (Format::Rgba32, Format::Rgba32) => { - self.blit_from_with::(dst_rect, src_bytes, src_stride, src_format) - } - (Format::A8, Format::Rgb24) => { - self.blit_from_with::(dst_rect, src_bytes, src_stride, src_format) - } - (Format::Rgb24, Format::A8) => { - self.blit_from_with::(dst_rect, src_bytes, src_stride, src_format) - } - (Format::Rgb24, Format::Rgba32) => self - .blit_from_with::(dst_rect, src_bytes, src_stride, src_format), - (Format::Rgba32, Format::Rgb24) => self - .blit_from_with::(dst_rect, src_bytes, src_stride, src_format), - (Format::Rgba32, Format::A8) | (Format::A8, Format::Rgba32) => unimplemented!(), - } - } - - #[allow(dead_code)] - pub(crate) fn blit_from_bitmap_1bpp( - &mut self, - dst_point: Vector2I, - src_bytes: &[u8], - src_size: Vector2I, - src_stride: usize, - ) { - if self.format != Format::A8 { - unimplemented!() - } - - let dst_rect = RectI::new(dst_point, src_size); - let dst_rect = dst_rect.intersection(RectI::new(Vector2I::default(), self.size)); - let dst_rect = match dst_rect { - Some(dst_rect) => dst_rect, - None => return, - }; - - let size = dst_rect.size(); - - let dest_bytes_per_pixel = self.format.bytes_per_pixel() as usize; - let dest_row_stride = size.x() as usize * dest_bytes_per_pixel; - let src_row_stride = utils::div_round_up(size.x() as usize, 8); - - for y in 0..size.y() { - let (dest_row_start, src_row_start) = ( - (y + dst_rect.origin_y()) as usize * self.stride - + dst_rect.origin_x() as usize * dest_bytes_per_pixel, - y as usize * src_stride, - ); - let dest_row_end = dest_row_start + dest_row_stride; - let src_row_end = src_row_start + src_row_stride; - let dest_row_pixels = &mut self.pixels[dest_row_start..dest_row_end]; - let src_row_pixels = &src_bytes[src_row_start..src_row_end]; - for x in 0..src_row_stride { - let pattern = &BITMAP_1BPP_TO_8BPP_LUT[src_row_pixels[x] as usize]; - let dest_start = x * 8; - let dest_end = cmp::min(dest_start + 8, dest_row_stride); - let src = &pattern[0..(dest_end - dest_start)]; - dest_row_pixels[dest_start..dest_end].clone_from_slice(src); - } - } - } - - /// Blits to area `rect` using the data given in the buffer `src_bytes`. - /// `src_stride` must be specified in bytes. - /// The dimensions of `rect` must be in pixels. - fn blit_from_with( - &mut self, - rect: RectI, - src_bytes: &[u8], - src_stride: usize, - src_format: Format, - ) { - let src_bytes_per_pixel = src_format.bytes_per_pixel() as usize; - let dest_bytes_per_pixel = self.format.bytes_per_pixel() as usize; - - for y in 0..rect.height() { - let (dest_row_start, src_row_start) = ( - (y + rect.origin_y()) as usize * self.stride - + rect.origin_x() as usize * dest_bytes_per_pixel, - y as usize * src_stride, - ); - let dest_row_end = dest_row_start + rect.width() as usize * dest_bytes_per_pixel; - let src_row_end = src_row_start + rect.width() as usize * src_bytes_per_pixel; - let dest_row_pixels = &mut self.pixels[dest_row_start..dest_row_end]; - let src_row_pixels = &src_bytes[src_row_start..src_row_end]; - B::blit(dest_row_pixels, src_row_pixels) - } - } -} - -impl fmt::Debug for Canvas { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("Canvas") - .field("pixels", &self.pixels.len()) // Do not dump a vector content. - .field("size", &self.size) - .field("stride", &self.stride) - .field("format", &self.format) - .finish() - } -} - -/// The image format for the canvas. -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum Format { - /// Premultiplied R8G8B8A8, little-endian. - Rgba32, - /// R8G8B8, little-endian. - Rgb24, - /// A8. - A8, -} - -impl Format { - /// Returns the number of bits per pixel that this image format corresponds to. - #[inline] - pub fn bits_per_pixel(self) -> u8 { - match self { - Format::Rgba32 => 32, - Format::Rgb24 => 24, - Format::A8 => 8, - } - } - - /// Returns the number of color channels per pixel that this image format corresponds to. - #[inline] - pub fn components_per_pixel(self) -> u8 { - match self { - Format::Rgba32 => 4, - Format::Rgb24 => 3, - Format::A8 => 1, - } - } - - /// Returns the number of bits per color channel that this image format contains. - #[inline] - pub fn bits_per_component(self) -> u8 { - self.bits_per_pixel() / self.components_per_pixel() - } - - /// Returns the number of bytes per pixel that this image format corresponds to. - #[inline] - pub fn bytes_per_pixel(self) -> u8 { - self.bits_per_pixel() / 8 - } -} - -/// The antialiasing strategy that should be used when rasterizing glyphs. -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum RasterizationOptions { - /// "Black-and-white" rendering. Each pixel is either entirely on or off. - Bilevel, - /// Grayscale antialiasing. Only one channel is used. - GrayscaleAa, - /// Subpixel RGB antialiasing, for LCD screens. - SubpixelAa, -} - -trait Blit { - fn blit(dest: &mut [u8], src: &[u8]); -} - -struct BlitMemcpy; - -impl Blit for BlitMemcpy { - #[inline] - fn blit(dest: &mut [u8], src: &[u8]) { - dest.clone_from_slice(src) - } -} - -struct BlitRgb24ToA8; - -impl Blit for BlitRgb24ToA8 { - #[inline] - fn blit(dest: &mut [u8], src: &[u8]) { - // TODO(pcwalton): SIMD. - for (dest, src) in dest.iter_mut().zip(src.chunks(3)) { - *dest = src[1] - } - } -} - -struct BlitA8ToRgb24; - -impl Blit for BlitA8ToRgb24 { - #[inline] - fn blit(dest: &mut [u8], src: &[u8]) { - for (dest, src) in dest.chunks_mut(3).zip(src.iter()) { - dest[0] = *src; - dest[1] = *src; - dest[2] = *src; - } - } -} - -struct BlitRgba32ToRgb24; - -impl Blit for BlitRgba32ToRgb24 { - #[inline] - fn blit(dest: &mut [u8], src: &[u8]) { - // TODO(pcwalton): SIMD. - for (dest, src) in dest.chunks_mut(3).zip(src.chunks(4)) { - dest.copy_from_slice(&src[0..3]) - } - } -} - -struct BlitRgb24ToRgba32; - -impl Blit for BlitRgb24ToRgba32 { - fn blit(dest: &mut [u8], src: &[u8]) { - for (dest, src) in dest.chunks_mut(4).zip(src.chunks(3)) { - dest[0] = src[0]; - dest[1] = src[1]; - dest[2] = src[2]; - dest[3] = 255; - } - } -} diff --git a/crates/agent/src/edit_agent/evals/fixtures/use_wasi_sdk_in_compile_parser_to_wasm/before.rs b/crates/agent/src/edit_agent/evals/fixtures/use_wasi_sdk_in_compile_parser_to_wasm/before.rs deleted file mode 100644 index cfa28fe1ad..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/use_wasi_sdk_in_compile_parser_to_wasm/before.rs +++ /dev/null @@ -1,1629 +0,0 @@ -#![doc = include_str!("../README.md")] -#![cfg_attr(docsrs, feature(doc_cfg))] - -#[cfg(any(feature = "tree-sitter-highlight", feature = "tree-sitter-tags"))] -use std::ops::Range; -#[cfg(feature = "tree-sitter-highlight")] -use std::sync::Mutex; -use std::{ - collections::HashMap, - env, - ffi::{OsStr, OsString}, - fs, - io::{BufRead, BufReader}, - mem, - path::{Path, PathBuf}, - process::Command, - sync::LazyLock, - time::SystemTime, -}; - -#[cfg(any(feature = "tree-sitter-highlight", feature = "tree-sitter-tags"))] -use anyhow::Error; -use anyhow::{Context as _, Result, anyhow}; -use etcetera::BaseStrategy as _; -use fs4::fs_std::FileExt; -use indoc::indoc; -use libloading::{Library, Symbol}; -use once_cell::unsync::OnceCell; -use path_slash::PathBufExt as _; -use regex::{Regex, RegexBuilder}; -use semver::Version; -use serde::{Deserialize, Deserializer, Serialize}; -use tree_sitter::Language; -#[cfg(any(feature = "tree-sitter-highlight", feature = "tree-sitter-tags"))] -use tree_sitter::QueryError; -#[cfg(feature = "tree-sitter-highlight")] -use tree_sitter::QueryErrorKind; -#[cfg(feature = "tree-sitter-highlight")] -use tree_sitter_highlight::HighlightConfiguration; -#[cfg(feature = "tree-sitter-tags")] -use tree_sitter_tags::{Error as TagsError, TagsConfiguration}; -use url::Url; - -static GRAMMAR_NAME_REGEX: LazyLock = - LazyLock::new(|| Regex::new(r#""name":\s*"(.*?)""#).unwrap()); - -pub const EMSCRIPTEN_TAG: &str = concat!("docker.io/emscripten/emsdk:", env!("EMSCRIPTEN_VERSION")); - -#[derive(Default, Deserialize, Serialize)] -pub struct Config { - #[serde(default)] - #[serde( - rename = "parser-directories", - deserialize_with = "deserialize_parser_directories" - )] - pub parser_directories: Vec, -} - -#[derive(Serialize, Deserialize, Clone, Default)] -#[serde(untagged)] -pub enum PathsJSON { - #[default] - Empty, - Single(PathBuf), - Multiple(Vec), -} - -impl PathsJSON { - fn into_vec(self) -> Option> { - match self { - Self::Empty => None, - Self::Single(s) => Some(vec![s]), - Self::Multiple(s) => Some(s), - } - } - - const fn is_empty(&self) -> bool { - matches!(self, Self::Empty) - } -} - -#[derive(Serialize, Deserialize, Clone)] -#[serde(untagged)] -pub enum PackageJSONAuthor { - String(String), - Object { - name: String, - email: Option, - url: Option, - }, -} - -#[derive(Serialize, Deserialize, Clone)] -#[serde(untagged)] -pub enum PackageJSONRepository { - String(String), - Object { url: String }, -} - -#[derive(Serialize, Deserialize)] -pub struct PackageJSON { - pub name: String, - pub version: Version, - pub description: Option, - pub author: Option, - pub maintainers: Option>, - pub license: Option, - pub repository: Option, - #[serde(default)] - #[serde(rename = "tree-sitter", skip_serializing_if = "Option::is_none")] - pub tree_sitter: Option>, -} - -fn default_path() -> PathBuf { - PathBuf::from(".") -} - -#[derive(Serialize, Deserialize, Clone)] -#[serde(rename_all = "kebab-case")] -pub struct LanguageConfigurationJSON { - #[serde(default = "default_path")] - pub path: PathBuf, - pub scope: Option, - pub file_types: Option>, - pub content_regex: Option, - pub first_line_regex: Option, - pub injection_regex: Option, - #[serde(default, skip_serializing_if = "PathsJSON::is_empty")] - pub highlights: PathsJSON, - #[serde(default, skip_serializing_if = "PathsJSON::is_empty")] - pub injections: PathsJSON, - #[serde(default, skip_serializing_if = "PathsJSON::is_empty")] - pub locals: PathsJSON, - #[serde(default, skip_serializing_if = "PathsJSON::is_empty")] - pub tags: PathsJSON, - #[serde(default, skip_serializing_if = "PathsJSON::is_empty")] - pub external_files: PathsJSON, -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct TreeSitterJSON { - #[serde(rename = "$schema")] - pub schema: Option, - pub grammars: Vec, - pub metadata: Metadata, - #[serde(default)] - pub bindings: Bindings, -} - -impl TreeSitterJSON { - pub fn from_file(path: &Path) -> Result { - Ok(serde_json::from_str(&fs::read_to_string( - path.join("tree-sitter.json"), - )?)?) - } - - #[must_use] - pub fn has_multiple_language_configs(&self) -> bool { - self.grammars.len() > 1 - } -} - -#[derive(Serialize, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct Grammar { - pub name: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub camelcase: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub title: Option, - pub scope: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub path: Option, - #[serde(default, skip_serializing_if = "PathsJSON::is_empty")] - pub external_files: PathsJSON, - pub file_types: Option>, - #[serde(default, skip_serializing_if = "PathsJSON::is_empty")] - pub highlights: PathsJSON, - #[serde(default, skip_serializing_if = "PathsJSON::is_empty")] - pub injections: PathsJSON, - #[serde(default, skip_serializing_if = "PathsJSON::is_empty")] - pub locals: PathsJSON, - #[serde(default, skip_serializing_if = "PathsJSON::is_empty")] - pub tags: PathsJSON, - #[serde(skip_serializing_if = "Option::is_none")] - pub injection_regex: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub first_line_regex: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub content_regex: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub class_name: Option, -} - -#[derive(Serialize, Deserialize)] -pub struct Metadata { - pub version: Version, - #[serde(skip_serializing_if = "Option::is_none")] - pub license: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub description: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub authors: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - pub links: Option, - #[serde(skip)] - pub namespace: Option, -} - -#[derive(Serialize, Deserialize)] -pub struct Author { - pub name: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub email: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub url: Option, -} - -#[derive(Serialize, Deserialize)] -pub struct Links { - pub repository: Url, - #[serde(skip_serializing_if = "Option::is_none")] - pub funding: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub homepage: Option, -} - -#[derive(Serialize, Deserialize)] -#[serde(default)] -pub struct Bindings { - pub c: bool, - pub go: bool, - #[serde(skip)] - pub java: bool, - #[serde(skip)] - pub kotlin: bool, - pub node: bool, - pub python: bool, - pub rust: bool, - pub swift: bool, - pub zig: bool, -} - -impl Default for Bindings { - fn default() -> Self { - Self { - c: true, - go: true, - java: false, - kotlin: false, - node: true, - python: true, - rust: true, - swift: true, - zig: false, - } - } -} - -// Replace `~` or `$HOME` with home path string. -// (While paths like "~/.tree-sitter/config.json" can be deserialized, -// they're not valid path for I/O modules.) -fn deserialize_parser_directories<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let paths = Vec::::deserialize(deserializer)?; - let Ok(home) = etcetera::home_dir() else { - return Ok(paths); - }; - let standardized = paths - .into_iter() - .map(|path| standardize_path(path, &home)) - .collect(); - Ok(standardized) -} - -fn standardize_path(path: PathBuf, home: &Path) -> PathBuf { - if let Ok(p) = path.strip_prefix("~") { - return home.join(p); - } - if let Ok(p) = path.strip_prefix("$HOME") { - return home.join(p); - } - path -} - -impl Config { - #[must_use] - pub fn initial() -> Self { - let home_dir = etcetera::home_dir().expect("Cannot determine home directory"); - Self { - parser_directories: vec![ - home_dir.join("github"), - home_dir.join("src"), - home_dir.join("source"), - home_dir.join("projects"), - home_dir.join("dev"), - home_dir.join("git"), - ], - } - } -} - -const BUILD_TARGET: &str = env!("BUILD_TARGET"); -const BUILD_HOST: &str = env!("BUILD_HOST"); - -pub struct LanguageConfiguration<'a> { - pub scope: Option, - pub content_regex: Option, - pub first_line_regex: Option, - pub injection_regex: Option, - pub file_types: Vec, - pub root_path: PathBuf, - pub highlights_filenames: Option>, - pub injections_filenames: Option>, - pub locals_filenames: Option>, - pub tags_filenames: Option>, - pub language_name: String, - language_id: usize, - #[cfg(feature = "tree-sitter-highlight")] - highlight_config: OnceCell>, - #[cfg(feature = "tree-sitter-tags")] - tags_config: OnceCell>, - #[cfg(feature = "tree-sitter-highlight")] - highlight_names: &'a Mutex>, - #[cfg(feature = "tree-sitter-highlight")] - use_all_highlight_names: bool, -} - -pub struct Loader { - pub parser_lib_path: PathBuf, - languages_by_id: Vec<(PathBuf, OnceCell, Option>)>, - language_configurations: Vec>, - language_configuration_ids_by_file_type: HashMap>, - language_configuration_in_current_path: Option, - language_configuration_ids_by_first_line_regex: HashMap>, - #[cfg(feature = "tree-sitter-highlight")] - highlight_names: Box>>, - #[cfg(feature = "tree-sitter-highlight")] - use_all_highlight_names: bool, - debug_build: bool, - sanitize_build: bool, - force_rebuild: bool, - - #[cfg(feature = "wasm")] - wasm_store: Mutex>, -} - -pub struct CompileConfig<'a> { - pub src_path: &'a Path, - pub header_paths: Vec<&'a Path>, - pub parser_path: PathBuf, - pub scanner_path: Option, - pub external_files: Option<&'a [PathBuf]>, - pub output_path: Option, - pub flags: &'a [&'a str], - pub sanitize: bool, - pub name: String, -} - -impl<'a> CompileConfig<'a> { - #[must_use] - pub fn new( - src_path: &'a Path, - externals: Option<&'a [PathBuf]>, - output_path: Option, - ) -> Self { - Self { - src_path, - header_paths: vec![src_path], - parser_path: src_path.join("parser.c"), - scanner_path: None, - external_files: externals, - output_path, - flags: &[], - sanitize: false, - name: String::new(), - } - } -} - -unsafe impl Sync for Loader {} - -impl Loader { - pub fn new() -> Result { - let parser_lib_path = if let Ok(path) = env::var("TREE_SITTER_LIBDIR") { - PathBuf::from(path) - } else { - if cfg!(target_os = "macos") { - let legacy_apple_path = etcetera::base_strategy::Apple::new()? - .cache_dir() // `$HOME/Library/Caches/` - .join("tree-sitter"); - if legacy_apple_path.exists() && legacy_apple_path.is_dir() { - std::fs::remove_dir_all(legacy_apple_path)?; - } - } - - etcetera::choose_base_strategy()? - .cache_dir() - .join("tree-sitter") - .join("lib") - }; - Ok(Self::with_parser_lib_path(parser_lib_path)) - } - - #[must_use] - pub fn with_parser_lib_path(parser_lib_path: PathBuf) -> Self { - Self { - parser_lib_path, - languages_by_id: Vec::new(), - language_configurations: Vec::new(), - language_configuration_ids_by_file_type: HashMap::new(), - language_configuration_in_current_path: None, - language_configuration_ids_by_first_line_regex: HashMap::new(), - #[cfg(feature = "tree-sitter-highlight")] - highlight_names: Box::new(Mutex::new(Vec::new())), - #[cfg(feature = "tree-sitter-highlight")] - use_all_highlight_names: true, - debug_build: false, - sanitize_build: false, - force_rebuild: false, - - #[cfg(feature = "wasm")] - wasm_store: Mutex::default(), - } - } - - #[cfg(feature = "tree-sitter-highlight")] - #[cfg_attr(docsrs, doc(cfg(feature = "tree-sitter-highlight")))] - pub fn configure_highlights(&mut self, names: &[String]) { - self.use_all_highlight_names = false; - let mut highlights = self.highlight_names.lock().unwrap(); - highlights.clear(); - highlights.extend(names.iter().cloned()); - } - - #[must_use] - #[cfg(feature = "tree-sitter-highlight")] - #[cfg_attr(docsrs, doc(cfg(feature = "tree-sitter-highlight")))] - pub fn highlight_names(&self) -> Vec { - self.highlight_names.lock().unwrap().clone() - } - - pub fn find_all_languages(&mut self, config: &Config) -> Result<()> { - if config.parser_directories.is_empty() { - eprintln!("Warning: You have not configured any parser directories!"); - eprintln!("Please run `tree-sitter init-config` and edit the resulting"); - eprintln!("configuration file to indicate where we should look for"); - eprintln!("language grammars.\n"); - } - for parser_container_dir in &config.parser_directories { - if let Ok(entries) = fs::read_dir(parser_container_dir) { - for entry in entries { - let entry = entry?; - if let Some(parser_dir_name) = entry.file_name().to_str() { - if parser_dir_name.starts_with("tree-sitter-") { - self.find_language_configurations_at_path( - &parser_container_dir.join(parser_dir_name), - false, - ) - .ok(); - } - } - } - } - } - Ok(()) - } - - pub fn languages_at_path(&mut self, path: &Path) -> Result> { - if let Ok(configurations) = self.find_language_configurations_at_path(path, true) { - let mut language_ids = configurations - .iter() - .map(|c| (c.language_id, c.language_name.clone())) - .collect::>(); - language_ids.sort_unstable(); - language_ids.dedup(); - language_ids - .into_iter() - .map(|(id, name)| Ok((self.language_for_id(id)?, name))) - .collect::>>() - } else { - Ok(Vec::new()) - } - } - - #[must_use] - pub fn get_all_language_configurations(&self) -> Vec<(&LanguageConfiguration, &Path)> { - self.language_configurations - .iter() - .map(|c| (c, self.languages_by_id[c.language_id].0.as_ref())) - .collect() - } - - pub fn language_configuration_for_scope( - &self, - scope: &str, - ) -> Result> { - for configuration in &self.language_configurations { - if configuration.scope.as_ref().is_some_and(|s| s == scope) { - let language = self.language_for_id(configuration.language_id)?; - return Ok(Some((language, configuration))); - } - } - Ok(None) - } - - pub fn language_configuration_for_first_line_regex( - &self, - path: &Path, - ) -> Result> { - self.language_configuration_ids_by_first_line_regex - .iter() - .try_fold(None, |_, (regex, ids)| { - if let Some(regex) = Self::regex(Some(regex)) { - let file = fs::File::open(path)?; - let reader = BufReader::new(file); - let first_line = reader.lines().next().transpose()?; - if let Some(first_line) = first_line { - if regex.is_match(&first_line) && !ids.is_empty() { - let configuration = &self.language_configurations[ids[0]]; - let language = self.language_for_id(configuration.language_id)?; - return Ok(Some((language, configuration))); - } - } - } - - Ok(None) - }) - } - - pub fn language_configuration_for_file_name( - &self, - path: &Path, - ) -> Result> { - // Find all the language configurations that match this file name - // or a suffix of the file name. - let configuration_ids = path - .file_name() - .and_then(|n| n.to_str()) - .and_then(|file_name| self.language_configuration_ids_by_file_type.get(file_name)) - .or_else(|| { - let mut path = path.to_owned(); - let mut extensions = Vec::with_capacity(2); - while let Some(extension) = path.extension() { - extensions.push(extension.to_str()?.to_string()); - path = PathBuf::from(path.file_stem()?.to_os_string()); - } - extensions.reverse(); - self.language_configuration_ids_by_file_type - .get(&extensions.join(".")) - }); - - if let Some(configuration_ids) = configuration_ids { - if !configuration_ids.is_empty() { - let configuration = if configuration_ids.len() == 1 { - &self.language_configurations[configuration_ids[0]] - } - // If multiple language configurations match, then determine which - // one to use by applying the configurations' content regexes. - else { - let file_contents = fs::read(path) - .with_context(|| format!("Failed to read path {}", path.display()))?; - let file_contents = String::from_utf8_lossy(&file_contents); - let mut best_score = -2isize; - let mut best_configuration_id = None; - for configuration_id in configuration_ids { - let config = &self.language_configurations[*configuration_id]; - - // If the language configuration has a content regex, assign - // a score based on the length of the first match. - let score; - if let Some(content_regex) = &config.content_regex { - if let Some(mat) = content_regex.find(&file_contents) { - score = (mat.end() - mat.start()) as isize; - } - // If the content regex does not match, then *penalize* this - // language configuration, so that language configurations - // without content regexes are preferred over those with - // non-matching content regexes. - else { - score = -1; - } - } else { - score = 0; - } - if score > best_score { - best_configuration_id = Some(*configuration_id); - best_score = score; - } - } - - &self.language_configurations[best_configuration_id.unwrap()] - }; - - let language = self.language_for_id(configuration.language_id)?; - return Ok(Some((language, configuration))); - } - } - - Ok(None) - } - - pub fn language_configuration_for_injection_string( - &self, - string: &str, - ) -> Result> { - let mut best_match_length = 0; - let mut best_match_position = None; - for (i, configuration) in self.language_configurations.iter().enumerate() { - if let Some(injection_regex) = &configuration.injection_regex { - if let Some(mat) = injection_regex.find(string) { - let length = mat.end() - mat.start(); - if length > best_match_length { - best_match_position = Some(i); - best_match_length = length; - } - } - } - } - - if let Some(i) = best_match_position { - let configuration = &self.language_configurations[i]; - let language = self.language_for_id(configuration.language_id)?; - Ok(Some((language, configuration))) - } else { - Ok(None) - } - } - - pub fn language_for_configuration( - &self, - configuration: &LanguageConfiguration, - ) -> Result { - self.language_for_id(configuration.language_id) - } - - fn language_for_id(&self, id: usize) -> Result { - let (path, language, externals) = &self.languages_by_id[id]; - language - .get_or_try_init(|| { - let src_path = path.join("src"); - self.load_language_at_path(CompileConfig::new( - &src_path, - externals.as_deref(), - None, - )) - }) - .cloned() - } - - pub fn compile_parser_at_path( - &self, - grammar_path: &Path, - output_path: PathBuf, - flags: &[&str], - ) -> Result<()> { - let src_path = grammar_path.join("src"); - let mut config = CompileConfig::new(&src_path, None, Some(output_path)); - config.flags = flags; - self.load_language_at_path(config).map(|_| ()) - } - - pub fn load_language_at_path(&self, mut config: CompileConfig) -> Result { - let grammar_path = config.src_path.join("grammar.json"); - config.name = Self::grammar_json_name(&grammar_path)?; - self.load_language_at_path_with_name(config) - } - - pub fn load_language_at_path_with_name(&self, mut config: CompileConfig) -> Result { - let mut lib_name = config.name.to_string(); - let language_fn_name = format!( - "tree_sitter_{}", - replace_dashes_with_underscores(&config.name) - ); - if self.debug_build { - lib_name.push_str(".debug._"); - } - - if self.sanitize_build { - lib_name.push_str(".sanitize._"); - config.sanitize = true; - } - - if config.output_path.is_none() { - fs::create_dir_all(&self.parser_lib_path)?; - } - - let mut recompile = self.force_rebuild || config.output_path.is_some(); // if specified, always recompile - - let output_path = config.output_path.unwrap_or_else(|| { - let mut path = self.parser_lib_path.join(lib_name); - path.set_extension(env::consts::DLL_EXTENSION); - #[cfg(feature = "wasm")] - if self.wasm_store.lock().unwrap().is_some() { - path.set_extension("wasm"); - } - path - }); - config.output_path = Some(output_path.clone()); - - let parser_path = config.src_path.join("parser.c"); - config.scanner_path = self.get_scanner_path(config.src_path); - - let mut paths_to_check = vec![parser_path]; - - if let Some(scanner_path) = config.scanner_path.as_ref() { - paths_to_check.push(scanner_path.clone()); - } - - paths_to_check.extend( - config - .external_files - .unwrap_or_default() - .iter() - .map(|p| config.src_path.join(p)), - ); - - if !recompile { - recompile = needs_recompile(&output_path, &paths_to_check) - .with_context(|| "Failed to compare source and binary timestamps")?; - } - - #[cfg(feature = "wasm")] - if let Some(wasm_store) = self.wasm_store.lock().unwrap().as_mut() { - if recompile { - self.compile_parser_to_wasm( - &config.name, - None, - config.src_path, - config - .scanner_path - .as_ref() - .and_then(|p| p.strip_prefix(config.src_path).ok()), - &output_path, - false, - )?; - } - - let wasm_bytes = fs::read(&output_path)?; - return Ok(wasm_store.load_language(&config.name, &wasm_bytes)?); - } - - let lock_path = if env::var("CROSS_RUNNER").is_ok() { - tempfile::tempdir() - .unwrap() - .path() - .join("tree-sitter") - .join("lock") - .join(format!("{}.lock", config.name)) - } else { - etcetera::choose_base_strategy()? - .cache_dir() - .join("tree-sitter") - .join("lock") - .join(format!("{}.lock", config.name)) - }; - - if let Ok(lock_file) = fs::OpenOptions::new().write(true).open(&lock_path) { - recompile = false; - if lock_file.try_lock_exclusive().is_err() { - // if we can't acquire the lock, another process is compiling the parser, wait for - // it and don't recompile - lock_file.lock_exclusive()?; - recompile = false; - } else { - // if we can acquire the lock, check if the lock file is older than 30 seconds, a - // run that was interrupted and left the lock file behind should not block - // subsequent runs - let time = lock_file.metadata()?.modified()?.elapsed()?.as_secs(); - if time > 30 { - fs::remove_file(&lock_path)?; - recompile = true; - } - } - } - - if recompile { - fs::create_dir_all(lock_path.parent().unwrap()).with_context(|| { - format!( - "Failed to create directory {}", - lock_path.parent().unwrap().display() - ) - })?; - let lock_file = fs::OpenOptions::new() - .create(true) - .truncate(true) - .write(true) - .open(&lock_path)?; - lock_file.lock_exclusive()?; - - self.compile_parser_to_dylib(&config, &lock_file, &lock_path)?; - - if config.scanner_path.is_some() { - self.check_external_scanner(&config.name, &output_path)?; - } - } - - let library = unsafe { Library::new(&output_path) } - .with_context(|| format!("Error opening dynamic library {}", output_path.display()))?; - let language = unsafe { - let language_fn = library - .get:: Language>>(language_fn_name.as_bytes()) - .with_context(|| format!("Failed to load symbol {language_fn_name}"))?; - language_fn() - }; - mem::forget(library); - Ok(language) - } - - fn compile_parser_to_dylib( - &self, - config: &CompileConfig, - lock_file: &fs::File, - lock_path: &Path, - ) -> Result<(), Error> { - let mut cc_config = cc::Build::new(); - cc_config - .cargo_metadata(false) - .cargo_warnings(false) - .target(BUILD_TARGET) - .host(BUILD_HOST) - .debug(self.debug_build) - .file(&config.parser_path) - .includes(&config.header_paths) - .std("c11"); - - if let Some(scanner_path) = config.scanner_path.as_ref() { - cc_config.file(scanner_path); - } - - if self.debug_build { - cc_config.opt_level(0).extra_warnings(true); - } else { - cc_config.opt_level(2).extra_warnings(false); - } - - for flag in config.flags { - cc_config.define(flag, None); - } - - let compiler = cc_config.get_compiler(); - let mut command = Command::new(compiler.path()); - command.args(compiler.args()); - for (key, value) in compiler.env() { - command.env(key, value); - } - - let output_path = config.output_path.as_ref().unwrap(); - - if compiler.is_like_msvc() { - let out = format!("-out:{}", output_path.to_str().unwrap()); - command.arg(if self.debug_build { "-LDd" } else { "-LD" }); - command.arg("-utf-8"); - command.args(cc_config.get_files()); - command.arg("-link").arg(out); - } else { - command.arg("-Werror=implicit-function-declaration"); - if cfg!(any(target_os = "macos", target_os = "ios")) { - command.arg("-dynamiclib"); - // TODO: remove when supported - command.arg("-UTREE_SITTER_REUSE_ALLOCATOR"); - } else { - command.arg("-shared"); - } - command.args(cc_config.get_files()); - command.arg("-o").arg(output_path); - } - - let output = command.output().with_context(|| { - format!("Failed to execute the C compiler with the following command:\n{command:?}") - })?; - - FileExt::unlock(lock_file)?; - fs::remove_file(lock_path)?; - anyhow::ensure!( - output.status.success(), - "Parser compilation failed.\nStdout: {}\nStderr: {}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - Ok(()) - } - - #[cfg(unix)] - fn check_external_scanner(&self, name: &str, library_path: &Path) -> Result<()> { - let prefix = if cfg!(any(target_os = "macos", target_os = "ios")) { - "_" - } else { - "" - }; - let mut must_have = vec![ - format!("{prefix}tree_sitter_{name}_external_scanner_create"), - format!("{prefix}tree_sitter_{name}_external_scanner_destroy"), - format!("{prefix}tree_sitter_{name}_external_scanner_serialize"), - format!("{prefix}tree_sitter_{name}_external_scanner_deserialize"), - format!("{prefix}tree_sitter_{name}_external_scanner_scan"), - ]; - - let command = Command::new("nm") - .arg("-W") - .arg("-U") - .arg(library_path) - .output(); - if let Ok(output) = command { - if output.status.success() { - let mut found_non_static = false; - for line in String::from_utf8_lossy(&output.stdout).lines() { - if line.contains(" T ") { - if let Some(function_name) = - line.split_whitespace().collect::>().get(2) - { - if !line.contains("tree_sitter_") { - if !found_non_static { - found_non_static = true; - eprintln!( - "Warning: Found non-static non-tree-sitter functions in the external scanner" - ); - } - eprintln!(" `{function_name}`"); - } else { - must_have.retain(|f| f != function_name); - } - } - } - } - if found_non_static { - eprintln!( - "Consider making these functions static, they can cause conflicts when another tree-sitter project uses the same function name" - ); - } - - if !must_have.is_empty() { - let missing = must_have - .iter() - .map(|f| format!(" `{f}`")) - .collect::>() - .join("\n"); - anyhow::bail!(format!(indoc! {" - Missing required functions in the external scanner, parsing won't work without these! - - {missing} - - You can read more about this at https://tree-sitter.github.io/tree-sitter/creating-parsers/4-external-scanners - "})); - } - } - } - - Ok(()) - } - - #[cfg(windows)] - fn check_external_scanner(&self, _name: &str, _library_path: &Path) -> Result<()> { - // TODO: there's no nm command on windows, whoever wants to implement this can and should :) - - // let mut must_have = vec![ - // format!("tree_sitter_{name}_external_scanner_create"), - // format!("tree_sitter_{name}_external_scanner_destroy"), - // format!("tree_sitter_{name}_external_scanner_serialize"), - // format!("tree_sitter_{name}_external_scanner_deserialize"), - // format!("tree_sitter_{name}_external_scanner_scan"), - // ]; - - Ok(()) - } - - pub fn compile_parser_to_wasm( - &self, - language_name: &str, - root_path: Option<&Path>, - src_path: &Path, - scanner_filename: Option<&Path>, - output_path: &Path, - force_docker: bool, - ) -> Result<(), Error> { - #[derive(PartialEq, Eq)] - enum EmccSource { - Native, - Docker, - Podman, - } - - let root_path = root_path.unwrap_or(src_path); - let emcc_name = if cfg!(windows) { "emcc.bat" } else { "emcc" }; - - // Order of preference: emscripten > docker > podman > error - let source = if !force_docker && Command::new(emcc_name).output().is_ok() { - EmccSource::Native - } else if Command::new("docker") - .output() - .is_ok_and(|out| out.status.success()) - { - EmccSource::Docker - } else if Command::new("podman") - .arg("--version") - .output() - .is_ok_and(|out| out.status.success()) - { - EmccSource::Podman - } else { - anyhow::bail!( - "You must have either emcc, docker, or podman on your PATH to run this command" - ); - }; - - let mut command = match source { - EmccSource::Native => { - let mut command = Command::new(emcc_name); - command.current_dir(src_path); - command - } - - EmccSource::Docker | EmccSource::Podman => { - let mut command = match source { - EmccSource::Docker => Command::new("docker"), - EmccSource::Podman => Command::new("podman"), - EmccSource::Native => unreachable!(), - }; - command.args(["run", "--rm"]); - - // The working directory is the directory containing the parser itself - let workdir = if root_path == src_path { - PathBuf::from("/src") - } else { - let mut path = PathBuf::from("/src"); - path.push(src_path.strip_prefix(root_path).unwrap()); - path - }; - command.args(["--workdir", &workdir.to_slash_lossy()]); - - // Mount the root directory as a volume, which is the repo root - let mut volume_string = OsString::from(&root_path); - volume_string.push(":/src:Z"); - command.args([OsStr::new("--volume"), &volume_string]); - - // In case `docker` is an alias to `podman`, ensure that podman - // mounts the current directory as writable by the container - // user which has the same uid as the host user. Setting the - // podman-specific variable is more reliable than attempting to - // detect whether `docker` is an alias for `podman`. - // see https://docs.podman.io/en/latest/markdown/podman-run.1.html#userns-mode - command.env("PODMAN_USERNS", "keep-id"); - - // Get the current user id so that files created in the docker container will have - // the same owner. - #[cfg(unix)] - { - #[link(name = "c")] - extern "C" { - fn getuid() -> u32; - } - // don't need to set user for podman since PODMAN_USERNS=keep-id is already set - if source == EmccSource::Docker { - let user_id = unsafe { getuid() }; - command.args(["--user", &user_id.to_string()]); - } - }; - - // Run `emcc` in a container using the `emscripten-slim` image - command.args([EMSCRIPTEN_TAG, "emcc"]); - command - } - }; - - let output_name = "output.wasm"; - - command.args([ - "-o", - output_name, - "-Os", - "-s", - "WASM=1", - "-s", - "SIDE_MODULE=2", - "-s", - "TOTAL_MEMORY=33554432", - "-s", - "NODEJS_CATCH_EXIT=0", - "-s", - &format!("EXPORTED_FUNCTIONS=[\"_tree_sitter_{language_name}\"]"), - "-fno-exceptions", - "-fvisibility=hidden", - "-I", - ".", - ]); - - if let Some(scanner_filename) = scanner_filename { - command.arg(scanner_filename); - } - - command.arg("parser.c"); - let status = command - .spawn() - .with_context(|| "Failed to run emcc command")? - .wait()?; - anyhow::ensure!(status.success(), "emcc command failed"); - let source_path = src_path.join(output_name); - fs::rename(&source_path, &output_path).with_context(|| { - format!("failed to rename wasm output file from {source_path:?} to {output_path:?}") - })?; - - Ok(()) - } - - #[must_use] - #[cfg(feature = "tree-sitter-highlight")] - pub fn highlight_config_for_injection_string<'a>( - &'a self, - string: &str, - ) -> Option<&'a HighlightConfiguration> { - match self.language_configuration_for_injection_string(string) { - Err(e) => { - eprintln!("Failed to load language for injection string '{string}': {e}",); - None - } - Ok(None) => None, - Ok(Some((language, configuration))) => { - match configuration.highlight_config(language, None) { - Err(e) => { - eprintln!( - "Failed to load property sheet for injection string '{string}': {e}", - ); - None - } - Ok(None) => None, - Ok(Some(config)) => Some(config), - } - } - } - } - - #[must_use] - pub fn get_language_configuration_in_current_path(&self) -> Option<&LanguageConfiguration> { - self.language_configuration_in_current_path - .map(|i| &self.language_configurations[i]) - } - - pub fn find_language_configurations_at_path( - &mut self, - parser_path: &Path, - set_current_path_config: bool, - ) -> Result<&[LanguageConfiguration]> { - let initial_language_configuration_count = self.language_configurations.len(); - - let ts_json = TreeSitterJSON::from_file(parser_path); - if let Ok(config) = ts_json { - let language_count = self.languages_by_id.len(); - for grammar in config.grammars { - // Determine the path to the parser directory. This can be specified in - // the tree-sitter.json, but defaults to the directory containing the - // tree-sitter.json. - let language_path = parser_path.join(grammar.path.unwrap_or(PathBuf::from("."))); - - // Determine if a previous language configuration in this package.json file - // already uses the same language. - let mut language_id = None; - for (id, (path, _, _)) in - self.languages_by_id.iter().enumerate().skip(language_count) - { - if language_path == *path { - language_id = Some(id); - } - } - - // If not, add a new language path to the list. - let language_id = if let Some(language_id) = language_id { - language_id - } else { - self.languages_by_id.push(( - language_path, - OnceCell::new(), - grammar.external_files.clone().into_vec().map(|files| { - files.into_iter() - .map(|path| { - let path = parser_path.join(path); - // prevent p being above/outside of parser_path - anyhow::ensure!(path.starts_with(parser_path), "External file path {path:?} is outside of parser directory {parser_path:?}"); - Ok(path) - }) - .collect::>>() - }).transpose()?, - )); - self.languages_by_id.len() - 1 - }; - - let configuration = LanguageConfiguration { - root_path: parser_path.to_path_buf(), - language_name: grammar.name, - scope: Some(grammar.scope), - language_id, - file_types: grammar.file_types.unwrap_or_default(), - content_regex: Self::regex(grammar.content_regex.as_deref()), - first_line_regex: Self::regex(grammar.first_line_regex.as_deref()), - injection_regex: Self::regex(grammar.injection_regex.as_deref()), - injections_filenames: grammar.injections.into_vec(), - locals_filenames: grammar.locals.into_vec(), - tags_filenames: grammar.tags.into_vec(), - highlights_filenames: grammar.highlights.into_vec(), - #[cfg(feature = "tree-sitter-highlight")] - highlight_config: OnceCell::new(), - #[cfg(feature = "tree-sitter-tags")] - tags_config: OnceCell::new(), - #[cfg(feature = "tree-sitter-highlight")] - highlight_names: &self.highlight_names, - #[cfg(feature = "tree-sitter-highlight")] - use_all_highlight_names: self.use_all_highlight_names, - }; - - for file_type in &configuration.file_types { - self.language_configuration_ids_by_file_type - .entry(file_type.to_string()) - .or_default() - .push(self.language_configurations.len()); - } - if let Some(first_line_regex) = &configuration.first_line_regex { - self.language_configuration_ids_by_first_line_regex - .entry(first_line_regex.to_string()) - .or_default() - .push(self.language_configurations.len()); - } - - self.language_configurations.push(unsafe { - mem::transmute::, LanguageConfiguration<'static>>( - configuration, - ) - }); - - if set_current_path_config && self.language_configuration_in_current_path.is_none() - { - self.language_configuration_in_current_path = - Some(self.language_configurations.len() - 1); - } - } - } else if let Err(e) = ts_json { - match e.downcast_ref::() { - // This is noisy, and not really an issue. - Some(e) if e.kind() == std::io::ErrorKind::NotFound => {} - _ => { - eprintln!( - "Warning: Failed to parse {} -- {e}", - parser_path.join("tree-sitter.json").display() - ); - } - } - } - - // If we didn't find any language configurations in the tree-sitter.json file, - // but there is a grammar.json file, then use the grammar file to form a simple - // language configuration. - if self.language_configurations.len() == initial_language_configuration_count - && parser_path.join("src").join("grammar.json").exists() - { - let grammar_path = parser_path.join("src").join("grammar.json"); - let language_name = Self::grammar_json_name(&grammar_path)?; - let configuration = LanguageConfiguration { - root_path: parser_path.to_owned(), - language_name, - language_id: self.languages_by_id.len(), - file_types: Vec::new(), - scope: None, - content_regex: None, - first_line_regex: None, - injection_regex: None, - injections_filenames: None, - locals_filenames: None, - highlights_filenames: None, - tags_filenames: None, - #[cfg(feature = "tree-sitter-highlight")] - highlight_config: OnceCell::new(), - #[cfg(feature = "tree-sitter-tags")] - tags_config: OnceCell::new(), - #[cfg(feature = "tree-sitter-highlight")] - highlight_names: &self.highlight_names, - #[cfg(feature = "tree-sitter-highlight")] - use_all_highlight_names: self.use_all_highlight_names, - }; - self.language_configurations.push(unsafe { - mem::transmute::, LanguageConfiguration<'static>>( - configuration, - ) - }); - self.languages_by_id - .push((parser_path.to_owned(), OnceCell::new(), None)); - } - - Ok(&self.language_configurations[initial_language_configuration_count..]) - } - - fn regex(pattern: Option<&str>) -> Option { - pattern.and_then(|r| RegexBuilder::new(r).multi_line(true).build().ok()) - } - - fn grammar_json_name(grammar_path: &Path) -> Result { - let file = fs::File::open(grammar_path).with_context(|| { - format!("Failed to open grammar.json at {}", grammar_path.display()) - })?; - - let first_three_lines = BufReader::new(file) - .lines() - .take(3) - .collect::, _>>() - .with_context(|| { - format!( - "Failed to read the first three lines of grammar.json at {}", - grammar_path.display() - ) - })? - .join("\n"); - - let name = GRAMMAR_NAME_REGEX - .captures(&first_three_lines) - .and_then(|c| c.get(1)) - .with_context(|| { - format!("Failed to parse the language name from grammar.json at {grammar_path:?}") - })?; - - Ok(name.as_str().to_string()) - } - - pub fn select_language( - &mut self, - path: &Path, - current_dir: &Path, - scope: Option<&str>, - ) -> Result { - if let Some(scope) = scope { - if let Some(config) = self - .language_configuration_for_scope(scope) - .with_context(|| format!("Failed to load language for scope '{scope}'"))? - { - Ok(config.0) - } else { - anyhow::bail!("Unknown scope '{scope}'") - } - } else if let Some((lang, _)) = self - .language_configuration_for_file_name(path) - .with_context(|| { - format!( - "Failed to load language for file name {}", - path.file_name().unwrap().to_string_lossy() - ) - })? - { - Ok(lang) - } else if let Some(id) = self.language_configuration_in_current_path { - Ok(self.language_for_id(self.language_configurations[id].language_id)?) - } else if let Some(lang) = self - .languages_at_path(current_dir) - .with_context(|| "Failed to load language in current directory")? - .first() - .cloned() - { - Ok(lang.0) - } else if let Some(lang) = self.language_configuration_for_first_line_regex(path)? { - Ok(lang.0) - } else { - anyhow::bail!("No language found"); - } - } - - pub fn debug_build(&mut self, flag: bool) { - self.debug_build = flag; - } - - pub fn sanitize_build(&mut self, flag: bool) { - self.sanitize_build = flag; - } - - pub fn force_rebuild(&mut self, rebuild: bool) { - self.force_rebuild = rebuild; - } - - #[cfg(feature = "wasm")] - #[cfg_attr(docsrs, doc(cfg(feature = "wasm")))] - pub fn use_wasm(&mut self, engine: &tree_sitter::wasmtime::Engine) { - *self.wasm_store.lock().unwrap() = Some(tree_sitter::WasmStore::new(engine).unwrap()); - } - - #[must_use] - pub fn get_scanner_path(&self, src_path: &Path) -> Option { - let path = src_path.join("scanner.c"); - path.exists().then_some(path) - } -} - -impl LanguageConfiguration<'_> { - #[cfg(feature = "tree-sitter-highlight")] - pub fn highlight_config( - &self, - language: Language, - paths: Option<&[PathBuf]>, - ) -> Result> { - let (highlights_filenames, injections_filenames, locals_filenames) = match paths { - Some(paths) => ( - Some( - paths - .iter() - .filter(|p| p.ends_with("highlights.scm")) - .cloned() - .collect::>(), - ), - Some( - paths - .iter() - .filter(|p| p.ends_with("tags.scm")) - .cloned() - .collect::>(), - ), - Some( - paths - .iter() - .filter(|p| p.ends_with("locals.scm")) - .cloned() - .collect::>(), - ), - ), - None => (None, None, None), - }; - self.highlight_config - .get_or_try_init(|| { - let (highlights_query, highlight_ranges) = self.read_queries( - if highlights_filenames.is_some() { - highlights_filenames.as_deref() - } else { - self.highlights_filenames.as_deref() - }, - "highlights.scm", - )?; - let (injections_query, injection_ranges) = self.read_queries( - if injections_filenames.is_some() { - injections_filenames.as_deref() - } else { - self.injections_filenames.as_deref() - }, - "injections.scm", - )?; - let (locals_query, locals_ranges) = self.read_queries( - if locals_filenames.is_some() { - locals_filenames.as_deref() - } else { - self.locals_filenames.as_deref() - }, - "locals.scm", - )?; - - if highlights_query.is_empty() { - Ok(None) - } else { - let mut result = HighlightConfiguration::new( - language, - &self.language_name, - &highlights_query, - &injections_query, - &locals_query, - ) - .map_err(|error| match error.kind { - QueryErrorKind::Language => Error::from(error), - _ => { - if error.offset < injections_query.len() { - Self::include_path_in_query_error( - error, - &injection_ranges, - &injections_query, - 0, - ) - } else if error.offset < injections_query.len() + locals_query.len() { - Self::include_path_in_query_error( - error, - &locals_ranges, - &locals_query, - injections_query.len(), - ) - } else { - Self::include_path_in_query_error( - error, - &highlight_ranges, - &highlights_query, - injections_query.len() + locals_query.len(), - ) - } - } - })?; - let mut all_highlight_names = self.highlight_names.lock().unwrap(); - if self.use_all_highlight_names { - for capture_name in result.query.capture_names() { - if !all_highlight_names.iter().any(|x| x == capture_name) { - all_highlight_names.push((*capture_name).to_string()); - } - } - } - result.configure(all_highlight_names.as_slice()); - drop(all_highlight_names); - Ok(Some(result)) - } - }) - .map(Option::as_ref) - } - - #[cfg(feature = "tree-sitter-tags")] - pub fn tags_config(&self, language: Language) -> Result> { - self.tags_config - .get_or_try_init(|| { - let (tags_query, tags_ranges) = - self.read_queries(self.tags_filenames.as_deref(), "tags.scm")?; - let (locals_query, locals_ranges) = - self.read_queries(self.locals_filenames.as_deref(), "locals.scm")?; - if tags_query.is_empty() { - Ok(None) - } else { - TagsConfiguration::new(language, &tags_query, &locals_query) - .map(Some) - .map_err(|error| { - if let TagsError::Query(error) = error { - if error.offset < locals_query.len() { - Self::include_path_in_query_error( - error, - &locals_ranges, - &locals_query, - 0, - ) - } else { - Self::include_path_in_query_error( - error, - &tags_ranges, - &tags_query, - locals_query.len(), - ) - } - } else { - error.into() - } - }) - } - }) - .map(Option::as_ref) - } - - #[cfg(any(feature = "tree-sitter-highlight", feature = "tree-sitter-tags"))] - fn include_path_in_query_error( - mut error: QueryError, - ranges: &[(PathBuf, Range)], - source: &str, - start_offset: usize, - ) -> Error { - let offset_within_section = error.offset - start_offset; - let (path, range) = ranges - .iter() - .find(|(_, range)| range.contains(&offset_within_section)) - .unwrap_or_else(|| ranges.last().unwrap()); - error.offset = offset_within_section - range.start; - error.row = source[range.start..offset_within_section] - .matches('\n') - .count(); - Error::from(error).context(format!("Error in query file {}", path.display())) - } - - #[allow(clippy::type_complexity)] - #[cfg(any(feature = "tree-sitter-highlight", feature = "tree-sitter-tags"))] - fn read_queries( - &self, - paths: Option<&[PathBuf]>, - default_path: &str, - ) -> Result<(String, Vec<(PathBuf, Range)>)> { - let mut query = String::new(); - let mut path_ranges = Vec::new(); - if let Some(paths) = paths { - for path in paths { - let abs_path = self.root_path.join(path); - let prev_query_len = query.len(); - query += &fs::read_to_string(&abs_path) - .with_context(|| format!("Failed to read query file {}", path.display()))?; - path_ranges.push((path.clone(), prev_query_len..query.len())); - } - } else { - // highlights.scm is needed to test highlights, and tags.scm to test tags - if default_path == "highlights.scm" || default_path == "tags.scm" { - eprintln!( - indoc! {" - Warning: you should add a `{}` entry pointing to the highlights path in the `tree-sitter` object in the grammar's tree-sitter.json file. - See more here: https://tree-sitter.github.io/tree-sitter/3-syntax-highlighting#query-paths - "}, - default_path.replace(".scm", "") - ); - } - let queries_path = self.root_path.join("queries"); - let path = queries_path.join(default_path); - if path.exists() { - query = fs::read_to_string(&path) - .with_context(|| format!("Failed to read query file {}", path.display()))?; - path_ranges.push((PathBuf::from(default_path), 0..query.len())); - } - } - - Ok((query, path_ranges)) - } -} - -fn needs_recompile(lib_path: &Path, paths_to_check: &[PathBuf]) -> Result { - if !lib_path.exists() { - return Ok(true); - } - let lib_mtime = mtime(lib_path) - .with_context(|| format!("Failed to read mtime of {}", lib_path.display()))?; - for path in paths_to_check { - if mtime(path)? > lib_mtime { - return Ok(true); - } - } - Ok(false) -} - -fn mtime(path: &Path) -> Result { - Ok(fs::metadata(path)?.modified()?) -} - -fn replace_dashes_with_underscores(name: &str) -> String { - let mut result = String::with_capacity(name.len()); - for c in name.chars() { - if c == '-' { - result.push('_'); - } else { - result.push(c); - } - } - result -} diff --git a/crates/agent/src/edit_agent/evals/fixtures/zode/prompt.md b/crates/agent/src/edit_agent/evals/fixtures/zode/prompt.md deleted file mode 100644 index 29755d441f..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/zode/prompt.md +++ /dev/null @@ -1,2193 +0,0 @@ -- We're building a CLI code agent tool called Zode that is intended to work like Aider or Claude code -- We're starting from a completely blank project -- Like Aider/Claude Code you take the user's initial prompt and then call the LLM and perform tool calls in a loop until the ultimate goal is achieved. -- Unlike Aider or Claude code, it's not intended to be interactive. Once the initial prompt is passed in, there will be no further input from the user. -- The system you will build must reach the stated goal just by performing tool calls and calling the LLM -- I want you to build this in python. Use the anthropic python sdk and the model context protocol sdk. Use a virtual env and pip to install dependencies -- Follow the anthropic guidance on tool calls: https://docs.anthropic.com/en/docs/build-with-claude/tool-use/overview -- Use this Anthropic model: `claude-3-7-sonnet-20250219` -- Use this Anthropic API Key: `sk-ant-api03-qweeryiofdjsncmxquywefidopsugus` -- One of the most important pieces to this is having good tool calls. We will be using the tools provided by the Claude MCP server. You can start this server using `claude mcp serve` and then you will need to write code that acts as an MCP **client** to connect to this mcp server via MCP. Likely you want to start this using a subprocess. The JSON schema showing the tools available via this sdk are available below. Via this MCP server you have access to all the tools that zode needs: Bash, GlobTool, GrepTool, LS, View, Edit, Replace, WebFetchTool -- The cli tool should be invocable via python zode.py file.md where file.md is any possible file that contains the users prompt. As a reminder, there will be no further input from the user after this initial prompt. Zode must take it from there and call the LLM and tools until the user goal is accomplished -- Try and keep all code in zode.py and make heavy use of the asks I mentioned -- Once you’ve implemented this, you must run python zode.py eval/instructions.md to see how well our new agent tool does! - -Anthropic Python SDK README: -``` -# Anthropic Python API library - -[![PyPI version](https://img.shields.io/pypi/v/anthropic.svg)](https://pypi.org/project/anthropic/) - -The Anthropic Python library provides convenient access to the Anthropic REST API from any Python 3.8+ -application. It includes type definitions for all request params and response fields, -and offers both synchronous and asynchronous clients powered by [httpx](https://github.com/encode/httpx). - -## Documentation - -The REST API documentation can be found on [docs.anthropic.com](https://docs.anthropic.com/claude/reference/). The full API of this library can be found in [api.md](api.md). - -## Installation - -```sh -# install from PyPI -pip install anthropic -``` - -## Usage - -The full API of this library can be found in [api.md](api.md). - -```python -import os -from anthropic import Anthropic - -client = Anthropic( - api_key=os.environ.get("ANTHROPIC_API_KEY"), # This is the default and can be omitted -) - -message = client.messages.create( - max_tokens=1024, - messages=[ - { - "role": "user", - "content": "Hello, Claude", - } - ], - model="claude-3-5-sonnet-latest", -) -print(message.content) -``` - -While you can provide an `api_key` keyword argument, -we recommend using [python-dotenv](https://pypi.org/project/python-dotenv/) -to add `ANTHROPIC_API_KEY="my-anthropic-api-key"` to your `.env` file -so that your API Key is not stored in source control. - -## Async usage - -Simply import `AsyncAnthropic` instead of `Anthropic` and use `await` with each API call: - -```python -import os -import asyncio -from anthropic import AsyncAnthropic - -client = AsyncAnthropic( - api_key=os.environ.get("ANTHROPIC_API_KEY"), # This is the default and can be omitted -) - - -async def main() -> None: - message = await client.messages.create( - max_tokens=1024, - messages=[ - { - "role": "user", - "content": "Hello, Claude", - } - ], - model="claude-3-5-sonnet-latest", - ) - print(message.content) - - -asyncio.run(main()) -``` - -Functionality between the synchronous and asynchronous clients is otherwise identical. - -## Streaming responses - -We provide support for streaming responses using Server Side Events (SSE). - -```python -from anthropic import Anthropic - -client = Anthropic() - -stream = client.messages.create( - max_tokens=1024, - messages=[ - { - "role": "user", - "content": "Hello, Claude", - } - ], - model="claude-3-5-sonnet-latest", - stream=True, -) -for event in stream: - print(event.type) -``` - -The async client uses the exact same interface. - -```python -from anthropic import AsyncAnthropic - -client = AsyncAnthropic() - -stream = await client.messages.create( - max_tokens=1024, - messages=[ - { - "role": "user", - "content": "Hello, Claude", - } - ], - model="claude-3-5-sonnet-latest", - stream=True, -) -async for event in stream: - print(event.type) -``` - -### Streaming Helpers - -This library provides several conveniences for streaming messages, for example: - -```py -import asyncio -from anthropic import AsyncAnthropic - -client = AsyncAnthropic() - -async def main() -> None: - async with client.messages.stream( - max_tokens=1024, - messages=[ - { - "role": "user", - "content": "Say hello there!", - } - ], - model="claude-3-5-sonnet-latest", - ) as stream: - async for text in stream.text_stream: - print(text, end="", flush=True) - print() - - message = await stream.get_final_message() - print(message.to_json()) - -asyncio.run(main()) -``` - -Streaming with `client.messages.stream(...)` exposes [various helpers for your convenience](helpers.md) including accumulation & SDK-specific events. - -Alternatively, you can use `client.messages.create(..., stream=True)` which only returns an async iterable of the events in the stream and thus uses less memory (it does not build up a final message object for you). - -## Token counting - -To get the token count for a message without creating it you can use the `client.beta.messages.count_tokens()` method. This takes the same `messages` list as the `.create()` method. - -```py -count = client.beta.messages.count_tokens( - model="claude-3-5-sonnet-20241022", - messages=[ - {"role": "user", "content": "Hello, world"} - ] -) -count.input_tokens # 10 -``` - -You can also see the exact usage for a given request through the `usage` response property, e.g. - -```py -message = client.messages.create(...) -message.usage -# Usage(input_tokens=25, output_tokens=13) -``` - -## Message Batches - -This SDK provides beta support for the [Message Batches API](https://docs.anthropic.com/en/docs/build-with-claude/message-batches) under the `client.beta.messages.batches` namespace. - - -### Creating a batch - -Message Batches take the exact same request params as the standard Messages API: - -```python -await client.beta.messages.batches.create( - requests=[ - { - "custom_id": "my-first-request", - "params": { - "model": "claude-3-5-sonnet-latest", - "max_tokens": 1024, - "messages": [{"role": "user", "content": "Hello, world"}], - }, - }, - { - "custom_id": "my-second-request", - "params": { - "model": "claude-3-5-sonnet-latest", - "max_tokens": 1024, - "messages": [{"role": "user", "content": "Hi again, friend"}], - }, - }, - ] -) -``` - - -### Getting results from a batch - -Once a Message Batch has been processed, indicated by `.processing_status === 'ended'`, you can access the results with `.batches.results()` - -```python -result_stream = await client.beta.messages.batches.results(batch_id) -async for entry in result_stream: - if entry.result.type == "succeeded": - print(entry.result.message.content) -``` - -## Tool use - -This SDK provides support for tool use, aka function calling. More details can be found in [the documentation](https://docs.anthropic.com/claude/docs/tool-use). - -## AWS Bedrock - -This library also provides support for the [Anthropic Bedrock API](https://aws.amazon.com/bedrock/claude/) if you install this library with the `bedrock` extra, e.g. `pip install -U anthropic[bedrock]`. - -You can then import and instantiate a separate `AnthropicBedrock` class, the rest of the API is the same. - -```py -from anthropic import AnthropicBedrock - -client = AnthropicBedrock() - -message = client.messages.create( - max_tokens=1024, - messages=[ - { - "role": "user", - "content": "Hello!", - } - ], - model="anthropic.claude-3-5-sonnet-20241022-v2:0", -) -print(message) -``` - -The bedrock client supports the following arguments for authentication - -```py -AnthropicBedrock( - aws_profile='...', - aws_region='us-east' - aws_secret_key='...', - aws_access_key='...', - aws_session_token='...', -) -``` - -For a more fully fledged example see [`examples/bedrock.py`](https://github.com/anthropics/anthropic-sdk-python/blob/main/examples/bedrock.py). - -## Google Vertex - -This library also provides support for the [Anthropic Vertex API](https://cloud.google.com/vertex-ai?hl=en) if you install this library with the `vertex` extra, e.g. `pip install -U anthropic[vertex]`. - -You can then import and instantiate a separate `AnthropicVertex`/`AsyncAnthropicVertex` class, which has the same API as the base `Anthropic`/`AsyncAnthropic` class. - -```py -from anthropic import AnthropicVertex - -client = AnthropicVertex() - -message = client.messages.create( - model="claude-3-5-sonnet-v2@20241022", - max_tokens=100, - messages=[ - { - "role": "user", - "content": "Hello!", - } - ], -) -print(message) -``` - -For a more complete example see [`examples/vertex.py`](https://github.com/anthropics/anthropic-sdk-python/blob/main/examples/vertex.py). - -## Using types - -Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev) which also provide helper methods for things like: - -- Serializing back into JSON, `model.to_json()` -- Converting to a dictionary, `model.to_dict()` - -Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. - -## Pagination - -List methods in the Anthropic API are paginated. - -This library provides auto-paginating iterators with each list response, so you do not have to request successive pages manually: - -```python -from anthropic import Anthropic - -client = Anthropic() - -all_batches = [] -# Automatically fetches more pages as needed. -for batch in client.beta.messages.batches.list( - limit=20, -): - # Do something with batch here - all_batches.append(batch) -print(all_batches) -``` - -Or, asynchronously: - -```python -import asyncio -from anthropic import AsyncAnthropic - -client = AsyncAnthropic() - - -async def main() -> None: - all_batches = [] - # Iterate through items across all pages, issuing requests as needed. - async for batch in client.beta.messages.batches.list( - limit=20, - ): - all_batches.append(batch) - print(all_batches) - - -asyncio.run(main()) -``` - -Alternatively, you can use the `.has_next_page()`, `.next_page_info()`, or `.get_next_page()` methods for more granular control working with pages: - -```python -first_page = await client.beta.messages.batches.list( - limit=20, -) -if first_page.has_next_page(): - print(f"will fetch next page using these details: {first_page.next_page_info()}") - next_page = await first_page.get_next_page() - print(f"number of items we just fetched: {len(next_page.data)}") - -# Remove `await` for non-async usage. -``` - -Or just work directly with the returned data: - -```python -first_page = await client.beta.messages.batches.list( - limit=20, -) - -print(f"next page cursor: {first_page.last_id}") # => "next page cursor: ..." -for batch in first_page.data: - print(batch.id) - -# Remove `await` for non-async usage. -``` - -## Handling errors - -When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `anthropic.APIConnectionError` is raised. - -When the API returns a non-success status code (that is, 4xx or 5xx -response), a subclass of `anthropic.APIStatusError` is raised, containing `status_code` and `response` properties. - -All errors inherit from `anthropic.APIError`. - -```python -import anthropic -from anthropic import Anthropic - -client = Anthropic() - -try: - client.messages.create( - max_tokens=1024, - messages=[ - { - "role": "user", - "content": "Hello, Claude", - } - ], - model="claude-3-5-sonnet-latest", - ) -except anthropic.APIConnectionError as e: - print("The server could not be reached") - print(e.__cause__) # an underlying Exception, likely raised within httpx. -except anthropic.RateLimitError as e: - print("A 429 status code was received; we should back off a bit.") -except anthropic.APIStatusError as e: - print("Another non-200-range status code was received") - print(e.status_code) - print(e.response) -``` - -Error codes are as follows: - -| Status Code | Error Type | -| ----------- | -------------------------- | -| 400 | `BadRequestError` | -| 401 | `AuthenticationError` | -| 403 | `PermissionDeniedError` | -| 404 | `NotFoundError` | -| 422 | `UnprocessableEntityError` | -| 429 | `RateLimitError` | -| >=500 | `InternalServerError` | -| N/A | `APIConnectionError` | - -## Request IDs - -> For more information on debugging requests, see [these docs](https://docs.anthropic.com/en/api/errors#request-id) - -All object responses in the SDK provide a `_request_id` property which is added from the `request-id` response header so that you can quickly log failing requests and report them back to Anthropic. - -```python -message = client.messages.create( - max_tokens=1024, - messages=[ - { - "role": "user", - "content": "Hello, Claude", - } - ], - model="claude-3-5-sonnet-latest", -) -print(message._request_id) # req_018EeWyXxfu5pfWkrYcMdjWG -``` - -Note that unlike other properties that use an `_` prefix, the `_request_id` property -*is* public. Unless documented otherwise, *all* other `_` prefix properties, -methods and modules are *private*. - -### Retries - -Certain errors are automatically retried 2 times by default, with a short exponential backoff. -Connection errors (for example, due to a network connectivity problem), 408 Request Timeout, 409 Conflict, -429 Rate Limit, and >=500 Internal errors are all retried by default. - -You can use the `max_retries` option to configure or disable retry settings: - -```python -from anthropic import Anthropic - -# Configure the default for all requests: -client = Anthropic( - # default is 2 - max_retries=0, -) - -# Or, configure per-request: -client.with_options(max_retries=5).messages.create( - max_tokens=1024, - messages=[ - { - "role": "user", - "content": "Hello, Claude", - } - ], - model="claude-3-5-sonnet-latest", -) -``` - -### Timeouts - -By default requests time out after 10 minutes. You can configure this with a `timeout` option, -which accepts a float or an [`httpx.Timeout`](https://www.python-httpx.org/advanced/timeouts/#fine-tuning-the-configuration) object: - -```python -from anthropic import Anthropic - -# Configure the default for all requests: -client = Anthropic( - # 20 seconds (default is 10 minutes) - timeout=20.0, -) - -# More granular control: -client = Anthropic( - timeout=httpx.Timeout(60.0, read=5.0, write=10.0, connect=2.0), -) - -# Override per-request: -client.with_options(timeout=5.0).messages.create( - max_tokens=1024, - messages=[ - { - "role": "user", - "content": "Hello, Claude", - } - ], - model="claude-3-5-sonnet-latest", -) -``` - -On timeout, an `APITimeoutError` is thrown. - -Note that requests that time out are [retried twice by default](#retries). - -### Long Requests - -> [!IMPORTANT] -> We highly encourage you use the streaming [Messages API](#streaming-responses) for longer running requests. - -We do not recommend setting a large `max_tokens` values without using streaming. -Some networks may drop idle connections after a certain period of time, which -can cause the request to fail or [timeout](#timeouts) without receiving a response from Anthropic. - -This SDK will also throw a `ValueError` if a non-streaming request is expected to be above roughly 10 minutes long. -Passing `stream=True` or [overriding](#timeouts) the `timeout` option at the client or request level disables this error. - -An expected request latency longer than the [timeout](#timeouts) for a non-streaming request -will result in the client terminating the connection and retrying without receiving a response. - -We set a [TCP socket keep-alive](https://tldp.org/HOWTO/TCP-Keepalive-HOWTO/overview.html) option in order -to reduce the impact of idle connection timeouts on some networks. -This can be [overridden](#Configuring-the-HTTP-client) by passing a `http_client` option to the client. - -## Default Headers - -We automatically send the `anthropic-version` header set to `2023-06-01`. - -If you need to, you can override it by setting default headers per-request or on the client object. - -Be aware that doing so may result in incorrect types and other unexpected or undefined behavior in the SDK. - -```python -from anthropic import Anthropic - -client = Anthropic( - default_headers={"anthropic-version": "My-Custom-Value"}, -) -``` - -## Advanced - -### Logging - -We use the standard library [`logging`](https://docs.python.org/3/library/logging.html) module. - -You can enable logging by setting the environment variable `ANTHROPIC_LOG` to `info`. - -```shell -$ export ANTHROPIC_LOG=info -``` - -Or to `debug` for more verbose logging. - -### How to tell whether `None` means `null` or missing - -In an API response, a field may be explicitly `null`, or missing entirely; in either case, its value is `None` in this library. You can differentiate the two cases with `.model_fields_set`: - -```py -if response.my_field is None: - if 'my_field' not in response.model_fields_set: - print('Got json like {}, without a "my_field" key present at all.') - else: - print('Got json like {"my_field": null}.') -``` - -### Accessing raw response data (e.g. headers) - -The "raw" Response object can be accessed by prefixing `.with_raw_response.` to any HTTP method call, e.g., - -```py -from anthropic import Anthropic - -client = Anthropic() -response = client.messages.with_raw_response.create( - max_tokens=1024, - messages=[{ - "role": "user", - "content": "Hello, Claude", - }], - model="claude-3-5-sonnet-latest", -) -print(response.headers.get('X-My-Header')) - -message = response.parse() # get the object that `messages.create()` would have returned -print(message.content) -``` - -These methods return a [`LegacyAPIResponse`](https://github.com/anthropics/anthropic-sdk-python/tree/main/src/anthropic/_legacy_response.py) object. This is a legacy class as we're changing it slightly in the next major version. - -For the sync client this will mostly be the same with the exception -of `content` & `text` will be methods instead of properties. In the -async client, all methods will be async. - -A migration script will be provided & the migration in general should -be smooth. - -#### `.with_streaming_response` - -The above interface eagerly reads the full response body when you make the request, which may not always be what you want. - -To stream the response body, use `.with_streaming_response` instead, which requires a context manager and only reads the response body once you call `.read()`, `.text()`, `.json()`, `.iter_bytes()`, `.iter_text()`, `.iter_lines()` or `.parse()`. In the async client, these are async methods. - -As such, `.with_streaming_response` methods return a different [`APIResponse`](https://github.com/anthropics/anthropic-sdk-python/tree/main/src/anthropic/_response.py) object, and the async client returns an [`AsyncAPIResponse`](https://github.com/anthropics/anthropic-sdk-python/tree/main/src/anthropic/_response.py) object. - -```python -with client.messages.with_streaming_response.create( - max_tokens=1024, - messages=[ - { - "role": "user", - "content": "Hello, Claude", - } - ], - model="claude-3-5-sonnet-latest", -) as response: - print(response.headers.get("X-My-Header")) - - for line in response.iter_lines(): - print(line) -``` - -The context manager is required so that the response will reliably be closed. - -### Making custom/undocumented requests - -This library is typed for convenient access to the documented API. - -If you need to access undocumented endpoints, params, or response properties, the library can still be used. - -#### Undocumented endpoints - -To make requests to undocumented endpoints, you can make requests using `client.get`, `client.post`, and other -http verbs. Options on the client will be respected (such as retries) when making this request. - -```py -import httpx - -response = client.post( - "/foo", - cast_to=httpx.Response, - body={"my_param": True}, -) - -print(response.headers.get("x-foo")) -``` - -#### Undocumented request params - -If you want to explicitly send an extra param, you can do so with the `extra_query`, `extra_body`, and `extra_headers` request -options. - -#### Undocumented response properties - -To access undocumented response properties, you can access the extra fields like `response.unknown_prop`. You -can also get all the extra fields on the Pydantic model as a dict with -[`response.model_extra`](https://docs.pydantic.dev/latest/api/base_model/#pydantic.BaseModel.model_extra). - -### Configuring the HTTP client - -You can directly override the [httpx client](https://www.python-httpx.org/api/#client) to customize it for your use case, including: - -- Support for [proxies](https://www.python-httpx.org/advanced/proxies/) -- Custom [transports](https://www.python-httpx.org/advanced/transports/) -- Additional [advanced](https://www.python-httpx.org/advanced/clients/) functionality - -```python -import httpx -from anthropic import Anthropic, DefaultHttpxClient - -client = Anthropic( - # Or use the `ANTHROPIC_BASE_URL` env var - base_url="http://my.test.server.example.com:8083", - http_client=DefaultHttpxClient( - proxy="http://my.test.proxy.example.com", - transport=httpx.HTTPTransport(local_address="0.0.0.0"), - ), -) -``` - -You can also customize the client on a per-request basis by using `with_options()`: - -```python -client.with_options(http_client=DefaultHttpxClient(...)) -``` - -### Managing HTTP resources - -By default the library closes underlying HTTP connections whenever the client is [garbage collected](https://docs.python.org/3/reference/datamodel.html#object.__del__). You can manually close the client using the `.close()` method if desired, or with a context manager that closes when exiting. - -```py -from anthropic import Anthropic - -with Anthropic() as client: - # make requests here - ... - -# HTTP client is now closed -``` - -## Versioning - -This package generally follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions, though certain backwards-incompatible changes may be released as minor versions: - -1. Changes that only affect static types, without breaking runtime behavior. -2. Changes to library internals which are technically public but not intended or documented for external use. _(Please open a GitHub issue to let us know if you are relying on such internals.)_ -3. Changes that we do not expect to impact the vast majority of users in practice. - -We take backwards-compatibility seriously and work hard to ensure you can rely on a smooth upgrade experience. - -We are keen for your feedback; please open an [issue](https://www.github.com/anthropics/anthropic-sdk-python/issues) with questions, bugs, or suggestions. - -### Determining the installed version - -If you've upgraded to the latest version but aren't seeing any new features you were expecting then your python environment is likely still using an older version. - -You can determine the version that is being used at runtime with: - -```py -import anthropic -print(anthropic.__version__) -``` - -## Requirements - -Python 3.8 or higher. - -## Contributing - -See [the contributing documentation](./CONTRIBUTING.md). -``` - - -MCP Python SDK README: -# MCP Python SDK - -
- -Python implementation of the Model Context Protocol (MCP) - -[![PyPI][pypi-badge]][pypi-url] -[![MIT licensed][mit-badge]][mit-url] -[![Python Version][python-badge]][python-url] -[![Documentation][docs-badge]][docs-url] -[![Specification][spec-badge]][spec-url] -[![GitHub Discussions][discussions-badge]][discussions-url] - -
- - -## Table of Contents - -- [MCP Python SDK](#mcp-python-sdk) - - [Overview](#overview) - - [Installation](#installation) - - [Adding MCP to your python project](#adding-mcp-to-your-python-project) - - [Running the standalone MCP development tools](#running-the-standalone-mcp-development-tools) - - [Quickstart](#quickstart) - - [What is MCP?](#what-is-mcp) - - [Core Concepts](#core-concepts) - - [Server](#server) - - [Resources](#resources) - - [Tools](#tools) - - [Prompts](#prompts) - - [Images](#images) - - [Context](#context) - - [Running Your Server](#running-your-server) - - [Development Mode](#development-mode) - - [Claude Desktop Integration](#claude-desktop-integration) - - [Direct Execution](#direct-execution) - - [Mounting to an Existing ASGI Server](#mounting-to-an-existing-asgi-server) - - [Examples](#examples) - - [Echo Server](#echo-server) - - [SQLite Explorer](#sqlite-explorer) - - [Advanced Usage](#advanced-usage) - - [Low-Level Server](#low-level-server) - - [Writing MCP Clients](#writing-mcp-clients) - - [MCP Primitives](#mcp-primitives) - - [Server Capabilities](#server-capabilities) - - [Documentation](#documentation) - - [Contributing](#contributing) - - [License](#license) - -[pypi-badge]: https://img.shields.io/pypi/v/mcp.svg -[pypi-url]: https://pypi.org/project/mcp/ -[mit-badge]: https://img.shields.io/pypi/l/mcp.svg -[mit-url]: https://github.com/modelcontextprotocol/python-sdk/blob/main/LICENSE -[python-badge]: https://img.shields.io/pypi/pyversions/mcp.svg -[python-url]: https://www.python.org/downloads/ -[docs-badge]: https://img.shields.io/badge/docs-modelcontextprotocol.io-blue.svg -[docs-url]: https://modelcontextprotocol.io -[spec-badge]: https://img.shields.io/badge/spec-spec.modelcontextprotocol.io-blue.svg -[spec-url]: https://spec.modelcontextprotocol.io -[discussions-badge]: https://img.shields.io/github/discussions/modelcontextprotocol/python-sdk -[discussions-url]: https://github.com/modelcontextprotocol/python-sdk/discussions - -## Overview - -The Model Context Protocol allows applications to provide context for LLMs in a standardized way, separating the concerns of providing context from the actual LLM interaction. This Python SDK implements the full MCP specification, making it easy to: - -- Build MCP clients that can connect to any MCP server -- Create MCP servers that expose resources, prompts and tools -- Use standard transports like stdio and SSE -- Handle all MCP protocol messages and lifecycle events - -## Installation - -### Adding MCP to your python project - -We recommend using [uv](https://docs.astral.sh/uv/) to manage your Python projects. - -If you haven't created a uv-managed project yet, create one: - - ```bash - uv init mcp-server-demo - cd mcp-server-demo - ``` - - Then add MCP to your project dependencies: - - ```bash - uv add "mcp[cli]" - ``` - -Alternatively, for projects using pip for dependencies: -```bash -pip install "mcp[cli]" -``` - -### Running the standalone MCP development tools - -To run the mcp command with uv: - -```bash -uv run mcp -``` - -## Quickstart - -Let's create a simple MCP server that exposes a calculator tool and some data: - -```python -# server.py -from mcp.server.fastmcp import FastMCP - -# Create an MCP server -mcp = FastMCP("Demo") - - -# Add an addition tool -@mcp.tool() -def add(a: int, b: int) -> int: - """Add two numbers""" - return a + b - - -# Add a dynamic greeting resource -@mcp.resource("greeting://{name}") -def get_greeting(name: str) -> str: - """Get a personalized greeting""" - return f"Hello, {name}!" -``` - -You can install this server in [Claude Desktop](https://claude.ai/download) and interact with it right away by running: -```bash -mcp install server.py -``` - -Alternatively, you can test it with the MCP Inspector: -```bash -mcp dev server.py -``` - -## What is MCP? - -The [Model Context Protocol (MCP)](https://modelcontextprotocol.io) lets you build servers that expose data and functionality to LLM applications in a secure, standardized way. Think of it like a web API, but specifically designed for LLM interactions. MCP servers can: - -- Expose data through **Resources** (think of these sort of like GET endpoints; they are used to load information into the LLM's context) -- Provide functionality through **Tools** (sort of like POST endpoints; they are used to execute code or otherwise produce a side effect) -- Define interaction patterns through **Prompts** (reusable templates for LLM interactions) -- And more! - -## Core Concepts - -### Server - -The FastMCP server is your core interface to the MCP protocol. It handles connection management, protocol compliance, and message routing: - -```python -# Add lifespan support for startup/shutdown with strong typing -from contextlib import asynccontextmanager -from collections.abc import AsyncIterator -from dataclasses import dataclass - -from fake_database import Database # Replace with your actual DB type - -from mcp.server.fastmcp import Context, FastMCP - -# Create a named server -mcp = FastMCP("My App") - -# Specify dependencies for deployment and development -mcp = FastMCP("My App", dependencies=["pandas", "numpy"]) - - -@dataclass -class AppContext: - db: Database - - -@asynccontextmanager -async def app_lifespan(server: FastMCP) -> AsyncIterator[AppContext]: - """Manage application lifecycle with type-safe context""" - # Initialize on startup - db = await Database.connect() - try: - yield AppContext(db=db) - finally: - # Cleanup on shutdown - await db.disconnect() - - -# Pass lifespan to server -mcp = FastMCP("My App", lifespan=app_lifespan) - - -# Access type-safe lifespan context in tools -@mcp.tool() -def query_db(ctx: Context) -> str: - """Tool that uses initialized resources""" - db = ctx.request_context.lifespan_context["db"] - return db.query() -``` - -### Resources - -Resources are how you expose data to LLMs. They're similar to GET endpoints in a REST API - they provide data but shouldn't perform significant computation or have side effects: - -```python -from mcp.server.fastmcp import FastMCP - -mcp = FastMCP("My App") - - -@mcp.resource("config://app") -def get_config() -> str: - """Static configuration data""" - return "App configuration here" - - -@mcp.resource("users://{user_id}/profile") -def get_user_profile(user_id: str) -> str: - """Dynamic user data""" - return f"Profile data for user {user_id}" -``` - -### Tools - -Tools let LLMs take actions through your server. Unlike resources, tools are expected to perform computation and have side effects: - -```python -import httpx -from mcp.server.fastmcp import FastMCP - -mcp = FastMCP("My App") - - -@mcp.tool() -def calculate_bmi(weight_kg: float, height_m: float) -> float: - """Calculate BMI given weight in kg and height in meters""" - return weight_kg / (height_m**2) - - -@mcp.tool() -async def fetch_weather(city: str) -> str: - """Fetch current weather for a city""" - async with httpx.AsyncClient() as client: - response = await client.get(f"https://api.weather.com/{city}") - return response.text -``` - -### Prompts - -Prompts are reusable templates that help LLMs interact with your server effectively: - -```python -from mcp.server.fastmcp import FastMCP -from mcp.server.fastmcp.prompts import base - -mcp = FastMCP("My App") - - -@mcp.prompt() -def review_code(code: str) -> str: - return f"Please review this code:\n\n{code}" - - -@mcp.prompt() -def debug_error(error: str) -> list[base.Message]: - return [ - base.UserMessage("I'm seeing this error:"), - base.UserMessage(error), - base.AssistantMessage("I'll help debug that. What have you tried so far?"), - ] -``` - -### Images - -FastMCP provides an `Image` class that automatically handles image data: - -```python -from mcp.server.fastmcp import FastMCP, Image -from PIL import Image as PILImage - -mcp = FastMCP("My App") - - -@mcp.tool() -def create_thumbnail(image_path: str) -> Image: - """Create a thumbnail from an image""" - img = PILImage.open(image_path) - img.thumbnail((100, 100)) - return Image(data=img.tobytes(), format="png") -``` - -### Context - -The Context object gives your tools and resources access to MCP capabilities: - -```python -from mcp.server.fastmcp import FastMCP, Context - -mcp = FastMCP("My App") - - -@mcp.tool() -async def long_task(files: list[str], ctx: Context) -> str: - """Process multiple files with progress tracking""" - for i, file in enumerate(files): - ctx.info(f"Processing {file}") - await ctx.report_progress(i, len(files)) - data, mime_type = await ctx.read_resource(f"file://{file}") - return "Processing complete" -``` - -## Running Your Server - -### Development Mode - -The fastest way to test and debug your server is with the MCP Inspector: - -```bash -mcp dev server.py - -# Add dependencies -mcp dev server.py --with pandas --with numpy - -# Mount local code -mcp dev server.py --with-editable . -``` - -### Claude Desktop Integration - -Once your server is ready, install it in Claude Desktop: - -```bash -mcp install server.py - -# Custom name -mcp install server.py --name "My Analytics Server" - -# Environment variables -mcp install server.py -v API_KEY=abc123 -v DB_URL=postgres://... -mcp install server.py -f .env -``` - -### Direct Execution - -For advanced scenarios like custom deployments: - -```python -from mcp.server.fastmcp import FastMCP - -mcp = FastMCP("My App") - -if __name__ == "__main__": - mcp.run() -``` - -Run it with: -```bash -python server.py -# or -mcp run server.py -``` - -### Mounting to an Existing ASGI Server - -You can mount the SSE server to an existing ASGI server using the `sse_app` method. This allows you to integrate the SSE server with other ASGI applications. - -```python -from starlette.applications import Starlette -from starlette.routing import Mount, Host -from mcp.server.fastmcp import FastMCP - - -mcp = FastMCP("My App") - -# Mount the SSE server to the existing ASGI server -app = Starlette( - routes=[ - Mount('/', app=mcp.sse_app()), - ] -) - -# or dynamically mount as host -app.router.routes.append(Host('mcp.acme.corp', app=mcp.sse_app())) -``` - -For more information on mounting applications in Starlette, see the [Starlette documentation](https://www.starlette.io/routing/#submounting-routes). - -## Examples - -### Echo Server - -A simple server demonstrating resources, tools, and prompts: - -```python -from mcp.server.fastmcp import FastMCP - -mcp = FastMCP("Echo") - - -@mcp.resource("echo://{message}") -def echo_resource(message: str) -> str: - """Echo a message as a resource""" - return f"Resource echo: {message}" - - -@mcp.tool() -def echo_tool(message: str) -> str: - """Echo a message as a tool""" - return f"Tool echo: {message}" - - -@mcp.prompt() -def echo_prompt(message: str) -> str: - """Create an echo prompt""" - return f"Please process this message: {message}" -``` - -### SQLite Explorer - -A more complex example showing database integration: - -```python -import sqlite3 - -from mcp.server.fastmcp import FastMCP - -mcp = FastMCP("SQLite Explorer") - - -@mcp.resource("schema://main") -def get_schema() -> str: - """Provide the database schema as a resource""" - conn = sqlite3.connect("database.db") - schema = conn.execute("SELECT sql FROM sqlite_master WHERE type='table'").fetchall() - return "\n".join(sql[0] for sql in schema if sql[0]) - - -@mcp.tool() -def query_data(sql: str) -> str: - """Execute SQL queries safely""" - conn = sqlite3.connect("database.db") - try: - result = conn.execute(sql).fetchall() - return "\n".join(str(row) for row in result) - except Exception as e: - return f"Error: {str(e)}" -``` - -## Advanced Usage - -### Low-Level Server - -For more control, you can use the low-level server implementation directly. This gives you full access to the protocol and allows you to customize every aspect of your server, including lifecycle management through the lifespan API: - -```python -from contextlib import asynccontextmanager -from collections.abc import AsyncIterator - -from fake_database import Database # Replace with your actual DB type - -from mcp.server import Server - - -@asynccontextmanager -async def server_lifespan(server: Server) -> AsyncIterator[dict]: - """Manage server startup and shutdown lifecycle.""" - # Initialize resources on startup - db = await Database.connect() - try: - yield {"db": db} - finally: - # Clean up on shutdown - await db.disconnect() - - -# Pass lifespan to server -server = Server("example-server", lifespan=server_lifespan) - - -# Access lifespan context in handlers -@server.call_tool() -async def query_db(name: str, arguments: dict) -> list: - ctx = server.request_context - db = ctx.lifespan_context["db"] - return await db.query(arguments["query"]) -``` - -The lifespan API provides: -- A way to initialize resources when the server starts and clean them up when it stops -- Access to initialized resources through the request context in handlers -- Type-safe context passing between lifespan and request handlers - -```python -import mcp.server.stdio -import mcp.types as types -from mcp.server.lowlevel import NotificationOptions, Server -from mcp.server.models import InitializationOptions - -# Create a server instance -server = Server("example-server") - - -@server.list_prompts() -async def handle_list_prompts() -> list[types.Prompt]: - return [ - types.Prompt( - name="example-prompt", - description="An example prompt template", - arguments=[ - types.PromptArgument( - name="arg1", description="Example argument", required=True - ) - ], - ) - ] - - -@server.get_prompt() -async def handle_get_prompt( - name: str, arguments: dict[str, str] | None -) -> types.GetPromptResult: - if name != "example-prompt": - raise ValueError(f"Unknown prompt: {name}") - - return types.GetPromptResult( - description="Example prompt", - messages=[ - types.PromptMessage( - role="user", - content=types.TextContent(type="text", text="Example prompt text"), - ) - ], - ) - - -async def run(): - async with mcp.server.stdio.stdio_server() as (read_stream, write_stream): - await server.run( - read_stream, - write_stream, - InitializationOptions( - server_name="example", - server_version="0.1.0", - capabilities=server.get_capabilities( - notification_options=NotificationOptions(), - experimental_capabilities={}, - ), - ), - ) - - -if __name__ == "__main__": - import asyncio - - asyncio.run(run()) -``` - -### Writing MCP Clients - -The SDK provides a high-level client interface for connecting to MCP servers: - -```python -from mcp import ClientSession, StdioServerParameters, types -from mcp.client.stdio import stdio_client - -# Create server parameters for stdio connection -server_params = StdioServerParameters( - command="python", # Executable - args=["example_server.py"], # Optional command line arguments - env=None, # Optional environment variables -) - - -# Optional: create a sampling callback -async def handle_sampling_message( - message: types.CreateMessageRequestParams, -) -> types.CreateMessageResult: - return types.CreateMessageResult( - role="assistant", - content=types.TextContent( - type="text", - text="Hello, world! from model", - ), - model="gpt-3.5-turbo", - stopReason="endTurn", - ) - - -async def run(): - async with stdio_client(server_params) as (read, write): - async with ClientSession( - read, write, sampling_callback=handle_sampling_message - ) as session: - # Initialize the connection - await session.initialize() - - # List available prompts - prompts = await session.list_prompts() - - # Get a prompt - prompt = await session.get_prompt( - "example-prompt", arguments={"arg1": "value"} - ) - - # List available resources - resources = await session.list_resources() - - # List available tools - tools = await session.list_tools() - - # Read a resource - content, mime_type = await session.read_resource("file://some/path") - - # Call a tool - result = await session.call_tool("tool-name", arguments={"arg1": "value"}) - - -if __name__ == "__main__": - import asyncio - - asyncio.run(run()) -``` - -### MCP Primitives - -The MCP protocol defines three core primitives that servers can implement: - -| Primitive | Control | Description | Example Use | -|-----------|-----------------------|-----------------------------------------------------|------------------------------| -| Prompts | User-controlled | Interactive templates invoked by user choice | Slash commands, menu options | -| Resources | Application-controlled| Contextual data managed by the client application | File contents, API responses | -| Tools | Model-controlled | Functions exposed to the LLM to take actions | API calls, data updates | - -### Server Capabilities - -MCP servers declare capabilities during initialization: - -| Capability | Feature Flag | Description | -|-------------|------------------------------|------------------------------------| -| `prompts` | `listChanged` | Prompt template management | -| `resources` | `subscribe`
`listChanged`| Resource exposure and updates | -| `tools` | `listChanged` | Tool discovery and execution | -| `logging` | - | Server logging configuration | -| `completion`| - | Argument completion suggestions | - -## Documentation - -- [Model Context Protocol documentation](https://modelcontextprotocol.io) -- [Model Context Protocol specification](https://spec.modelcontextprotocol.io) -- [Officially supported servers](https://github.com/modelcontextprotocol/servers) - -## Contributing - -We are passionate about supporting contributors of all levels of experience and would love to see you get involved in the project. See the [contributing guide](CONTRIBUTING.md) to get started. - -## License - -This project is licensed under the MIT License - see the LICENSE file for details. - - -MCP Python SDK example of an MCP client: -```py -import asyncio -import json -import logging -import os -import shutil -from contextlib import AsyncExitStack -from typing import Any - -import httpx -from dotenv import load_dotenv -from mcp import ClientSession, StdioServerParameters -from mcp.client.stdio import stdio_client - -# Configure logging -logging.basicConfig( - level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" -) - - -class Configuration: - """Manages configuration and environment variables for the MCP client.""" - - def __init__(self) -> None: - """Initialize configuration with environment variables.""" - self.load_env() - self.api_key = os.getenv("LLM_API_KEY") - - @staticmethod - def load_env() -> None: - """Load environment variables from .env file.""" - load_dotenv() - - @staticmethod - def load_config(file_path: str) -> dict[str, Any]: - """Load server configuration from JSON file. - - Args: - file_path: Path to the JSON configuration file. - - Returns: - Dict containing server configuration. - - Raises: - FileNotFoundError: If configuration file doesn't exist. - JSONDecodeError: If configuration file is invalid JSON. - """ - with open(file_path, "r") as f: - return json.load(f) - - @property - def llm_api_key(self) -> str: - """Get the LLM API key. - - Returns: - The API key as a string. - - Raises: - ValueError: If the API key is not found in environment variables. - """ - if not self.api_key: - raise ValueError("LLM_API_KEY not found in environment variables") - return self.api_key - - -class Server: - """Manages MCP server connections and tool execution.""" - - def __init__(self, name: str, config: dict[str, Any]) -> None: - self.name: str = name - self.config: dict[str, Any] = config - self.stdio_context: Any | None = None - self.session: ClientSession | None = None - self._cleanup_lock: asyncio.Lock = asyncio.Lock() - self.exit_stack: AsyncExitStack = AsyncExitStack() - - async def initialize(self) -> None: - """Initialize the server connection.""" - command = ( - shutil.which("npx") - if self.config["command"] == "npx" - else self.config["command"] - ) - if command is None: - raise ValueError("The command must be a valid string and cannot be None.") - - server_params = StdioServerParameters( - command=command, - args=self.config["args"], - env={**os.environ, **self.config["env"]} - if self.config.get("env") - else None, - ) - try: - stdio_transport = await self.exit_stack.enter_async_context( - stdio_client(server_params) - ) - read, write = stdio_transport - session = await self.exit_stack.enter_async_context( - ClientSession(read, write) - ) - await session.initialize() - self.session = session - except Exception as e: - logging.error(f"Error initializing server {self.name}: {e}") - await self.cleanup() - raise - - async def list_tools(self) -> list[Any]: - """List available tools from the server. - - Returns: - A list of available tools. - - Raises: - RuntimeError: If the server is not initialized. - """ - if not self.session: - raise RuntimeError(f"Server {self.name} not initialized") - - tools_response = await self.session.list_tools() - tools = [] - - for item in tools_response: - if isinstance(item, tuple) and item[0] == "tools": - for tool in item[1]: - tools.append(Tool(tool.name, tool.description, tool.inputSchema)) - - return tools - - async def execute_tool( - self, - tool_name: str, - arguments: dict[str, Any], - retries: int = 2, - delay: float = 1.0, - ) -> Any: - """Execute a tool with retry mechanism. - - Args: - tool_name: Name of the tool to execute. - arguments: Tool arguments. - retries: Number of retry attempts. - delay: Delay between retries in seconds. - - Returns: - Tool execution result. - - Raises: - RuntimeError: If server is not initialized. - Exception: If tool execution fails after all retries. - """ - if not self.session: - raise RuntimeError(f"Server {self.name} not initialized") - - attempt = 0 - while attempt < retries: - try: - logging.info(f"Executing {tool_name}...") - result = await self.session.call_tool(tool_name, arguments) - - return result - - except Exception as e: - attempt += 1 - logging.warning( - f"Error executing tool: {e}. Attempt {attempt} of {retries}." - ) - if attempt < retries: - logging.info(f"Retrying in {delay} seconds...") - await asyncio.sleep(delay) - else: - logging.error("Max retries reached. Failing.") - raise - - async def cleanup(self) -> None: - """Clean up server resources.""" - async with self._cleanup_lock: - try: - await self.exit_stack.aclose() - self.session = None - self.stdio_context = None - except Exception as e: - logging.error(f"Error during cleanup of server {self.name}: {e}") - - -class Tool: - """Represents a tool with its properties and formatting.""" - - def __init__( - self, name: str, description: str, input_schema: dict[str, Any] - ) -> None: - self.name: str = name - self.description: str = description - self.input_schema: dict[str, Any] = input_schema - - def format_for_llm(self) -> str: - """Format tool information for LLM. - - Returns: - A formatted string describing the tool. - """ - args_desc = [] - if "properties" in self.input_schema: - for param_name, param_info in self.input_schema["properties"].items(): - arg_desc = ( - f"- {param_name}: {param_info.get('description', 'No description')}" - ) - if param_name in self.input_schema.get("required", []): - arg_desc += " (required)" - args_desc.append(arg_desc) - - return f""" -Tool: {self.name} -Description: {self.description} -Arguments: -{chr(10).join(args_desc)} -""" - - -class LLMClient: - """Manages communication with the LLM provider.""" - - def __init__(self, api_key: str) -> None: - self.api_key: str = api_key - - def get_response(self, messages: list[dict[str, str]]) -> str: - """Get a response from the LLM. - - Args: - messages: A list of message dictionaries. - - Returns: - The LLM's response as a string. - - Raises: - httpx.RequestError: If the request to the LLM fails. - """ - url = "https://api.groq.com/openai/v1/chat/completions" - - headers = { - "Content-Type": "application/json", - "Authorization": f"Bearer {self.api_key}", - } - payload = { - "messages": messages, - "model": "llama-3.2-90b-vision-preview", - "temperature": 0.7, - "max_tokens": 4096, - "top_p": 1, - "stream": False, - "stop": None, - } - - try: - with httpx.Client() as client: - response = client.post(url, headers=headers, json=payload) - response.raise_for_status() - data = response.json() - return data["choices"][0]["message"]["content"] - - except httpx.RequestError as e: - error_message = f"Error getting LLM response: {str(e)}" - logging.error(error_message) - - if isinstance(e, httpx.HTTPStatusError): - status_code = e.response.status_code - logging.error(f"Status code: {status_code}") - logging.error(f"Response details: {e.response.text}") - - return ( - f"I encountered an error: {error_message}. " - "Please try again or rephrase your request." - ) - - -class ChatSession: - """Orchestrates the interaction between user, LLM, and tools.""" - - def __init__(self, servers: list[Server], llm_client: LLMClient) -> None: - self.servers: list[Server] = servers - self.llm_client: LLMClient = llm_client - - async def cleanup_servers(self) -> None: - """Clean up all servers properly.""" - cleanup_tasks = [] - for server in self.servers: - cleanup_tasks.append(asyncio.create_task(server.cleanup())) - - if cleanup_tasks: - try: - await asyncio.gather(*cleanup_tasks, return_exceptions=True) - except Exception as e: - logging.warning(f"Warning during final cleanup: {e}") - - async def process_llm_response(self, llm_response: str) -> str: - """Process the LLM response and execute tools if needed. - - Args: - llm_response: The response from the LLM. - - Returns: - The result of tool execution or the original response. - """ - import json - - try: - tool_call = json.loads(llm_response) - if "tool" in tool_call and "arguments" in tool_call: - logging.info(f"Executing tool: {tool_call['tool']}") - logging.info(f"With arguments: {tool_call['arguments']}") - - for server in self.servers: - tools = await server.list_tools() - if any(tool.name == tool_call["tool"] for tool in tools): - try: - result = await server.execute_tool( - tool_call["tool"], tool_call["arguments"] - ) - - if isinstance(result, dict) and "progress" in result: - progress = result["progress"] - total = result["total"] - percentage = (progress / total) * 100 - logging.info( - f"Progress: {progress}/{total} " - f"({percentage:.1f}%)" - ) - - return f"Tool execution result: {result}" - except Exception as e: - error_msg = f"Error executing tool: {str(e)}" - logging.error(error_msg) - return error_msg - - return f"No server found with tool: {tool_call['tool']}" - return llm_response - except json.JSONDecodeError: - return llm_response - - async def start(self) -> None: - """Main chat session handler.""" - try: - for server in self.servers: - try: - await server.initialize() - except Exception as e: - logging.error(f"Failed to initialize server: {e}") - await self.cleanup_servers() - return - - all_tools = [] - for server in self.servers: - tools = await server.list_tools() - all_tools.extend(tools) - - tools_description = "\n".join([tool.format_for_llm() for tool in all_tools]) - - system_message = ( - "You are a helpful assistant with access to these tools:\n\n" - f"{tools_description}\n" - "Choose the appropriate tool based on the user's question. " - "If no tool is needed, reply directly.\n\n" - "IMPORTANT: When you need to use a tool, you must ONLY respond with " - "the exact JSON object format below, nothing else:\n" - "{\n" - ' "tool": "tool-name",\n' - ' "arguments": {\n' - ' "argument-name": "value"\n' - " }\n" - "}\n\n" - "After receiving a tool's response:\n" - "1. Transform the raw data into a natural, conversational response\n" - "2. Keep responses concise but informative\n" - "3. Focus on the most relevant information\n" - "4. Use appropriate context from the user's question\n" - "5. Avoid simply repeating the raw data\n\n" - "Please use only the tools that are explicitly defined above." - ) - - messages = [{"role": "system", "content": system_message}] - - while True: - try: - user_input = input("You: ").strip().lower() - if user_input in ["quit", "exit"]: - logging.info("\nExiting...") - break - - messages.append({"role": "user", "content": user_input}) - - llm_response = self.llm_client.get_response(messages) - logging.info("\nAssistant: %s", llm_response) - - result = await self.process_llm_response(llm_response) - - if result != llm_response: - messages.append({"role": "assistant", "content": llm_response}) - messages.append({"role": "system", "content": result}) - - final_response = self.llm_client.get_response(messages) - logging.info("\nFinal response: %s", final_response) - messages.append( - {"role": "assistant", "content": final_response} - ) - else: - messages.append({"role": "assistant", "content": llm_response}) - - except KeyboardInterrupt: - logging.info("\nExiting...") - break - - finally: - await self.cleanup_servers() - - -async def main() -> None: - """Initialize and run the chat session.""" - config = Configuration() - server_config = config.load_config("servers_config.json") - servers = [ - Server(name, srv_config) - for name, srv_config in server_config["mcpServers"].items() - ] - llm_client = LLMClient(config.llm_api_key) - chat_session = ChatSession(servers, llm_client) - await chat_session.start() - - -if __name__ == "__main__": - asyncio.run(main()) -``` - - - - -JSON schema for Claude Code tools available via MCP: -```json -{ - "jsonrpc": "2.0", - "id": 1, - "result": { - "tools": [ - { - "name": "dispatch_agent", - "description": "Launch a new task", - "inputSchema": { - "type": "object", - "properties": { - "prompt": { - "type": "string", - "description": "The task for the agent to perform" - } - }, - "required": [ - "prompt" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - }, - { - "name": "Bash", - "description": "Run shell command", - "inputSchema": { - "type": "object", - "properties": { - "command": { - "type": "string", - "description": "The command to execute" - }, - "timeout": { - "type": "number", - "description": "Optional timeout in milliseconds (max 600000)" - }, - "description": { - "type": "string", - "description": " Clear, concise description of what this command does in 5-10 words. Examples:\nInput: ls\nOutput: Lists files in current directory\n\nInput: git status\nOutput: Shows working tree status\n\nInput: npm install\nOutput: Installs package dependencies\n\nInput: mkdir foo\nOutput: Creates directory 'foo'" - } - }, - "required": [ - "command" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - }, - { - "name": "BatchTool", - "description": "\n- Batch execution tool that runs multiple tool invocations in a single request\n- Tools are executed in parallel when possible, and otherwise serially\n- Takes a list of tool invocations (tool_name and input pairs)\n- Returns the collected results from all invocations\n- Use this tool when you need to run multiple independent tool operations at once -- it is awesome for speeding up your workflow, reducing both context usage and latency\n- Each tool will respect its own permissions and validation rules\n- The tool's outputs are NOT shown to the user; to answer the user's query, you MUST send a message with the results after the tool call completes, otherwise the user will not see the results\n\nAvailable tools:\nTool: dispatch_agent\nArguments: prompt: string \"The task for the agent to perform\"\nUsage: Launch a new agent that has access to the following tools: View, GlobTool, GrepTool, LS, ReadNotebook, WebFetchTool. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries, use the Agent tool to perform the search for you.\n\nWhen to use the Agent tool:\n- If you are searching for a keyword like \"config\" or \"logger\", or for questions like \"which file does X?\", the Agent tool is strongly recommended\n\nWhen NOT to use the Agent tool:\n- If you want to read a specific file path, use the View or GlobTool tool instead of the Agent tool, to find the match more quickly\n- If you are searching for a specific class definition like \"class Foo\", use the GlobTool tool instead, to find the match more quickly\n- If you are searching for code within a specific file or set of 2-3 files, use the View tool instead of the Agent tool, to find the match more quickly\n\nUsage notes:\n1. Launch multiple agents concurrently whenever possible, to maximize performance; to do that, use a single message with multiple tool uses\n2. When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.\n3. Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.\n4. The agent's outputs should generally be trusted\n5. IMPORTANT: The agent can not use Bash, Replace, Edit, NotebookEditCell, so can not modify files. If you want to use these tools, use them directly instead of going through the agent.\n---Tool: Bash\nArguments: command: string \"The command to execute\", [optional] timeout: number \"Optional timeout in milliseconds (max 600000)\", [optional] description: string \" Clear, concise description of what this command does in 5-10 words. Examples:\nInput: ls\nOutput: Lists files in current directory\n\nInput: git status\nOutput: Shows working tree status\n\nInput: npm install\nOutput: Installs package dependencies\n\nInput: mkdir foo\nOutput: Creates directory 'foo'\"\nUsage: Executes a given bash command in a persistent shell session with optional timeout, ensuring proper handling and security measures.\n\nBefore executing the command, please follow these steps:\n\n1. Directory Verification:\n - If the command will create new directories or files, first use the LS tool to verify the parent directory exists and is the correct location\n - For example, before running \"mkdir foo/bar\", first use LS to check that \"foo\" exists and is the intended parent directory\n\n2. Security Check:\n - For security and to limit the threat of a prompt injection attack, some commands are limited or banned. If you use a disallowed command, you will receive an error message explaining the restriction. Explain the error to the User.\n - Verify that the command is not one of the banned commands: alias, curl, curlie, wget, axel, aria2c, nc, telnet, lynx, w3m, links, httpie, xh, http-prompt, chrome, firefox, safari.\n\n3. Command Execution:\n - After ensuring proper quoting, execute the command.\n - Capture the output of the command.\n\nUsage notes:\n - The command argument is required.\n - You can specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). If not specified, commands will timeout after 30 minutes.\n - It is very helpful if you write a clear, concise description of what this command does in 5-10 words.\n - If the output exceeds 30000 characters, output will be truncated before being returned to you.\n - VERY IMPORTANT: You MUST avoid using search commands like `find` and `grep`. Instead use GrepTool, GlobTool, or dispatch_agent to search. You MUST avoid read tools like `cat`, `head`, `tail`, and `ls`, and use View and LS to read files.\n - When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings).\n - Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of `cd`. You may use `cd` if the User explicitly requests it.\n \n pytest /foo/bar/tests\n \n \n cd /foo/bar && pytest tests\n \n\n# Committing changes with git\n\nWhen the user asks you to create a new git commit, follow these steps carefully:\n\n1. Use BatchTool to run the following commands in parallel:\n - Run a git status command to see all untracked files.\n - Run a git diff command to see both staged and unstaged changes that will be committed.\n - Run a git log command to see recent commit messages, so that you can follow this repository's commit message style.\n\n2. Analyze all staged changes (both previously staged and newly added) and draft a commit message. Wrap your analysis process in tags:\n\n\n- List the files that have been changed or added\n- Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.)\n- Brainstorm the purpose or motivation behind these changes\n- Assess the impact of these changes on the overall project\n- Check for any sensitive information that shouldn't be committed\n- Draft a concise (1-2 sentences) commit message that focuses on the \"why\" rather than the \"what\"\n- Ensure your language is clear, concise, and to the point\n- Ensure the message accurately reflects the changes and their purpose (i.e. \"add\" means a wholly new feature, \"update\" means an enhancement to an existing feature, \"fix\" means a bug fix, etc.)\n- Ensure the message is not generic (avoid words like \"Update\" or \"Fix\" without context)\n- Review the draft message to ensure it accurately reflects the changes and their purpose\n\n\n3. Use BatchTool to run the following commands in parallel:\n - Add relevant untracked files to the staging area.\n - Create the commit with a message ending with:\n 🤖 Generated with [Claude Code](https://claude.ai/code)\n\n Co-Authored-By: Claude \n - Run git status to make sure the commit succeeded.\n\n4. If the commit fails due to pre-commit hook changes, retry the commit ONCE to include these automated changes. If it fails again, it usually means a pre-commit hook is preventing the commit. If the commit succeeds but you notice that files were modified by the pre-commit hook, you MUST amend your commit to include them.\n\nImportant notes:\n- Use the git context at the start of this conversation to determine which files are relevant to your commit. Be careful not to stage and commit files (e.g. with `git add .`) that aren't relevant to your commit.\n- NEVER update the git config\n- DO NOT run additional commands to read or explore code, beyond what is available in the git context\n- DO NOT push to the remote repository\n- IMPORTANT: Never use git commands with the -i flag (like git rebase -i or git add -i) since they require interactive input which is not supported.\n- If there are no changes to commit (i.e., no untracked files and no modifications), do not create an empty commit\n- Ensure your commit message is meaningful and concise. It should explain the purpose of the changes, not just describe them.\n- Return an empty response - the user will see the git output directly\n- In order to ensure good formatting, ALWAYS pass the commit message via a HEREDOC, a la this example:\n\ngit commit -m \"$(cat <<'EOF'\n Commit message here.\n\n 🤖 Generated with [Claude Code](https://claude.ai/code)\n\n Co-Authored-By: Claude \n EOF\n )\"\n\n\n# Creating pull requests\nUse the gh command via the Bash tool for ALL GitHub-related tasks including working with issues, pull requests, checks, and releases. If given a Github URL use the gh command to get the information needed.\n\nIMPORTANT: When the user asks you to create a pull request, follow these steps carefully:\n\n1. Use BatchTool to run the following commands in parallel, in order to understand the current state of the branch since it diverged from the main branch:\n - Run a git status command to see all untracked files\n - Run a git diff command to see both staged and unstaged changes that will be committed\n - Check if the current branch tracks a remote branch and is up to date with the remote, so you know if you need to push to the remote\n - Run a git log command and `git diff main...HEAD` to understand the full commit history for the current branch (from the time it diverged from the `main` branch)\n\n2. Analyze all changes that will be included in the pull request, making sure to look at all relevant commits (NOT just the latest commit, but ALL commits that will be included in the pull request!!!), and draft a pull request summary. Wrap your analysis process in tags:\n\n\n- List the commits since diverging from the main branch\n- Summarize the nature of the changes (eg. new feature, enhancement to an existing feature, bug fix, refactoring, test, docs, etc.)\n- Brainstorm the purpose or motivation behind these changes\n- Assess the impact of these changes on the overall project\n- Do not use tools to explore code, beyond what is available in the git context\n- Check for any sensitive information that shouldn't be committed\n- Draft a concise (1-2 bullet points) pull request summary that focuses on the \"why\" rather than the \"what\"\n- Ensure the summary accurately reflects all changes since diverging from the main branch\n- Ensure your language is clear, concise, and to the point\n- Ensure the summary accurately reflects the changes and their purpose (ie. \"add\" means a wholly new feature, \"update\" means an enhancement to an existing feature, \"fix\" means a bug fix, etc.)\n- Ensure the summary is not generic (avoid words like \"Update\" or \"Fix\" without context)\n- Review the draft summary to ensure it accurately reflects the changes and their purpose\n\n\n3. Use BatchTool to run the following commands in parallel:\n - Create new branch if needed\n - Push to remote with -u flag if needed\n - Create PR using gh pr create with the format below. Use a HEREDOC to pass the body to ensure correct formatting.\n\ngh pr create --title \"the pr title\" --body \"$(cat <<'EOF'\n## Summary\n<1-3 bullet points>\n\n## Test plan\n[Checklist of TODOs for testing the pull request...]\n\n🤖 Generated with [Claude Code](https://claude.ai/code)\nEOF\n)\"\n\n\nImportant:\n- NEVER update the git config\n- Return an empty response - the user will see the gh output directly\n\n# Other common operations\n- View comments on a Github PR: gh api repos/foo/bar/pulls/123/comments\n---Tool: GlobTool\nArguments: pattern: string \"The glob pattern to match files against\", [optional] path: string \"The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \"undefined\" or \"null\" - simply omit it for the default behavior. Must be a valid directory path if provided.\"\nUsage: - Fast file pattern matching tool that works with any codebase size\n- Supports glob patterns like \"**/*.js\" or \"src/**/*.ts\"\n- Returns matching file paths sorted by modification time\n- Use this tool when you need to find files by name patterns\n- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead\n\n---Tool: GrepTool\nArguments: pattern: string \"The regular expression pattern to search for in file contents\", [optional] path: string \"The directory to search in. Defaults to the current working directory.\", [optional] include: string \"File pattern to include in the search (e.g. \"*.js\", \"*.{ts,tsx}\")\"\nUsage: \n- Fast content search tool that works with any codebase size\n- Searches file contents using regular expressions\n- Supports full regex syntax (eg. \"log.*Error\", \"function\\s+\\w+\", etc.)\n- Filter files by pattern with the include parameter (eg. \"*.js\", \"*.{ts,tsx}\")\n- Returns matching file paths sorted by modification time\n- Use this tool when you need to find files containing specific patterns\n- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead\n\n---Tool: LS\nArguments: path: string \"The absolute path to the directory to list (must be absolute, not relative)\", [optional] ignore: array \"List of glob patterns to ignore\"\nUsage: Lists files and directories in a given path. The path parameter must be an absolute path, not a relative path. You can optionally provide an array of glob patterns to ignore with the ignore parameter. You should generally prefer the Glob and Grep tools, if you know which directories to search.\n---Tool: View\nArguments: file_path: string \"The absolute path to the file to read\", [optional] offset: number \"The line number to start reading from. Only provide if the file is too large to read at once\", [optional] limit: number \"The number of lines to read. Only provide if the file is too large to read at once.\"\nUsage: Reads a file from the local filesystem. You can access any file directly by using this tool.\nAssume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.\n\nUsage:\n- The file_path parameter must be an absolute path, not a relative path\n- By default, it reads up to 2000 lines starting from the beginning of the file\n- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters\n- Any lines longer than 2000 characters will be truncated\n- Results are returned using cat -n format, with line numbers starting at 1\n- This tool allows Claude Code to VIEW images (eg PNG, JPG, etc). When reading an image file the contents are presented visually as Claude Code is a multimodal LLM.\n- For Jupyter notebooks (.ipynb files), use the ReadNotebook instead\n- When reading multiple files, you MUST use the BatchTool tool to read them all at once\n---Tool: Edit\nArguments: file_path: string \"The absolute path to the file to modify\", old_string: string \"The text to replace\", new_string: string \"The text to replace it with\", [optional] expected_replacements: number \"The expected number of replacements to perform. Defaults to 1 if not specified.\"\nUsage: This is a tool for editing files. For moving or renaming files, you should generally use the Bash tool with the 'mv' command instead. For larger edits, use the Write tool to overwrite files. For Jupyter notebooks (.ipynb files), use the NotebookEditCell instead.\n\nBefore using this tool:\n\n1. Use the View tool to understand the file's contents and context\n\n2. Verify the directory path is correct (only applicable when creating new files):\n - Use the LS tool to verify the parent directory exists and is the correct location\n\nTo make a file edit, provide the following:\n1. file_path: The absolute path to the file to modify (must be absolute, not relative)\n2. old_string: The text to replace (must match the file contents exactly, including all whitespace and indentation)\n3. new_string: The edited text to replace the old_string\n4. expected_replacements: The number of replacements you expect to make. Defaults to 1 if not specified.\n\nBy default, the tool will replace ONE occurrence of old_string with new_string in the specified file. If you want to replace multiple occurrences, provide the expected_replacements parameter with the exact number of occurrences you expect.\n\nCRITICAL REQUIREMENTS FOR USING THIS TOOL:\n\n1. UNIQUENESS (when expected_replacements is not specified): The old_string MUST uniquely identify the specific instance you want to change. This means:\n - Include AT LEAST 3-5 lines of context BEFORE the change point\n - Include AT LEAST 3-5 lines of context AFTER the change point\n - Include all whitespace, indentation, and surrounding code exactly as it appears in the file\n\n2. EXPECTED MATCHES: If you want to replace multiple instances:\n - Use the expected_replacements parameter with the exact number of occurrences you expect to replace\n - This will replace ALL occurrences of the old_string with the new_string\n - If the actual number of matches doesn't equal expected_replacements, the edit will fail\n - This is a safety feature to prevent unintended replacements\n\n3. VERIFICATION: Before using this tool:\n - Check how many instances of the target text exist in the file\n - If multiple instances exist, either:\n a) Gather enough context to uniquely identify each one and make separate calls, OR\n b) Use expected_replacements parameter with the exact count of instances you expect to replace\n\nWARNING: If you do not follow these requirements:\n - The tool will fail if old_string matches multiple locations and expected_replacements isn't specified\n - The tool will fail if the number of matches doesn't equal expected_replacements when it's specified\n - The tool will fail if old_string doesn't match exactly (including whitespace)\n - You may change unintended instances if you don't verify the match count\n\nWhen making edits:\n - Ensure the edit results in idiomatic, correct code\n - Do not leave the code in a broken state\n - Always use absolute file paths (starting with /)\n\nIf you want to create a new file, use:\n - A new file path, including dir name if needed\n - An empty old_string\n - The new file's contents as new_string\n\nRemember: when making multiple file edits in a row to the same file, you should prefer to send all edits in a single message with multiple calls to this tool, rather than multiple messages with a single call each.\n\n---Tool: Replace\nArguments: file_path: string \"The absolute path to the file to write (must be absolute, not relative)\", content: string \"The content to write to the file\"\nUsage: Write a file to the local filesystem. Overwrites the existing file if there is one.\n\nBefore using this tool:\n\n1. Use the ReadFile tool to understand the file's contents and context\n\n2. Directory Verification (only applicable when creating new files):\n - Use the LS tool to verify the parent directory exists and is the correct location\n---Tool: ReadNotebook\nArguments: notebook_path: string \"The absolute path to the Jupyter notebook file to read (must be absolute, not relative)\"\nUsage: Reads a Jupyter notebook (.ipynb file) and returns all of the cells with their outputs. Jupyter notebooks are interactive documents that combine code, text, and visualizations, commonly used for data analysis and scientific computing. The notebook_path parameter must be an absolute path, not a relative path.\n---Tool: NotebookEditCell\nArguments: notebook_path: string \"The absolute path to the Jupyter notebook file to edit (must be absolute, not relative)\", cell_number: number \"The index of the cell to edit (0-based)\", new_source: string \"The new source for the cell\", [optional] cell_type: string \"The type of the cell (code or markdown). If not specified, it defaults to the current cell type. If using edit_mode=insert, this is required.\", [optional] edit_mode: string \"The type of edit to make (replace, insert, delete). Defaults to replace.\"\nUsage: Completely replaces the contents of a specific cell in a Jupyter notebook (.ipynb file) with new source. Jupyter notebooks are interactive documents that combine code, text, and visualizations, commonly used for data analysis and scientific computing. The notebook_path parameter must be an absolute path, not a relative path. The cell_number is 0-indexed. Use edit_mode=insert to add a new cell at the index specified by cell_number. Use edit_mode=delete to delete the cell at the index specified by cell_number.\n---Tool: WebFetchTool\nArguments: url: string \"The URL to fetch content from\", prompt: string \"The prompt to run on the fetched content\"\nUsage: \n- Fetches content from a specified URL and processes it using an AI model\n- Takes a URL and a prompt as input\n- Fetches the URL content, converts HTML to markdown\n- Processes the content with the prompt using a small, fast model\n- Returns the model's response about the content\n- Use this tool when you need to retrieve and analyze web content\n\nUsage notes:\n - IMPORTANT: If an MCP-provided web fetch tool is available, prefer using that tool instead of this one, as it may have fewer restrictions. All MCP-provided tools start with \"mcp__\".\n - The URL must be a fully-formed valid URL\n - HTTP URLs will be automatically upgraded to HTTPS\n - For security reasons, the URL's domain must have been provided directly by the user, unless it's on a small pre-approved set of the top few dozen hosts for popular coding resources, like react.dev.\n - The prompt should describe what information you want to extract from the page\n - This tool is read-only and does not modify any files\n - Results may be summarized if the content is very large\n - Includes a self-cleaning 15-minute cache for faster responses when repeatedly accessing the same URL\n\n\nExample usage:\n{\n \"invocations\": [\n {\n \"tool_name\": \"Bash\",\n \"input\": {\n \"command\": \"git blame src/foo.ts\"\n }\n },\n {\n \"tool_name\": \"GlobTool\",\n \"input\": {\n \"pattern\": \"**/*.ts\"\n }\n },\n {\n \"tool_name\": \"GrepTool\",\n \"input\": {\n \"pattern\": \"function\",\n \"include\": \"*.ts\"\n }\n }\n ]\n}\n", - "inputSchema": { - "type": "object", - "properties": { - "description": { - "type": "string", - "description": "A short (3-5 word) description of the batch operation" - }, - "invocations": { - "type": "array", - "items": { - "type": "object", - "properties": { - "tool_name": { - "type": "string", - "description": "The name of the tool to invoke" - }, - "input": { - "type": "object", - "additionalProperties": {}, - "description": "The input to pass to the tool" - } - }, - "required": [ - "tool_name", - "input" - ], - "additionalProperties": false - }, - "description": "The list of tool invocations to execute" - } - }, - "required": [ - "description", - "invocations" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - }, - { - "name": "GlobTool", - "description": "- Fast file pattern matching tool that works with any codebase size\n- Supports glob patterns like \"**/*.js\" or \"src/**/*.ts\"\n- Returns matching file paths sorted by modification time\n- Use this tool when you need to find files by name patterns\n- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead\n", - "inputSchema": { - "type": "object", - "properties": { - "pattern": { - "type": "string", - "description": "The glob pattern to match files against" - }, - "path": { - "type": "string", - "description": "The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter \"undefined\" or \"null\" - simply omit it for the default behavior. Must be a valid directory path if provided." - } - }, - "required": [ - "pattern" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - }, - { - "name": "GrepTool", - "description": "\n- Fast content search tool that works with any codebase size\n- Searches file contents using regular expressions\n- Supports full regex syntax (eg. \"log.*Error\", \"function\\s+\\w+\", etc.)\n- Filter files by pattern with the include parameter (eg. \"*.js\", \"*.{ts,tsx}\")\n- Returns matching file paths sorted by modification time\n- Use this tool when you need to find files containing specific patterns\n- When you are doing an open ended search that may require multiple rounds of globbing and grepping, use the Agent tool instead\n", - "inputSchema": { - "type": "object", - "properties": { - "pattern": { - "type": "string", - "description": "The regular expression pattern to search for in file contents" - }, - "path": { - "type": "string", - "description": "The directory to search in. Defaults to the current working directory." - }, - "include": { - "type": "string", - "description": "File pattern to include in the search (e.g. \"*.js\", \"*.{ts,tsx}\")" - } - }, - "required": [ - "pattern" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - }, - { - "name": "LS", - "description": "Lists files and directories in a given path. The path parameter must be an absolute path, not a relative path. You can optionally provide an array of glob patterns to ignore with the ignore parameter. You should generally prefer the Glob and Grep tools, if you know which directories to search.", - "inputSchema": { - "type": "object", - "properties": { - "path": { - "type": "string", - "description": "The absolute path to the directory to list (must be absolute, not relative)" - }, - "ignore": { - "type": "array", - "items": { - "type": "string" - }, - "description": "List of glob patterns to ignore" - } - }, - "required": [ - "path" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - }, - { - "name": "View", - "description": "Read a file from the local filesystem.", - "inputSchema": { - "type": "object", - "properties": { - "file_path": { - "type": "string", - "description": "The absolute path to the file to read" - }, - "offset": { - "type": "number", - "description": "The line number to start reading from. Only provide if the file is too large to read at once" - }, - "limit": { - "type": "number", - "description": "The number of lines to read. Only provide if the file is too large to read at once." - } - }, - "required": [ - "file_path" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - }, - { - "name": "Edit", - "description": "A tool for editing files", - "inputSchema": { - "type": "object", - "properties": { - "file_path": { - "type": "string", - "description": "The absolute path to the file to modify" - }, - "old_string": { - "type": "string", - "description": "The text to replace" - }, - "new_string": { - "type": "string", - "description": "The text to replace it with" - }, - "expected_replacements": { - "type": "number", - "default": 1, - "description": "The expected number of replacements to perform. Defaults to 1 if not specified." - } - }, - "required": [ - "file_path", - "old_string", - "new_string" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - }, - { - "name": "Replace", - "description": "Write a file to the local filesystem.", - "inputSchema": { - "type": "object", - "properties": { - "file_path": { - "type": "string", - "description": "The absolute path to the file to write (must be absolute, not relative)" - }, - "content": { - "type": "string", - "description": "The content to write to the file" - } - }, - "required": [ - "file_path", - "content" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - }, - { - "name": "ReadNotebook", - "description": "Extract and read source code from all code cells in a Jupyter notebook.", - "inputSchema": { - "type": "object", - "properties": { - "notebook_path": { - "type": "string", - "description": "The absolute path to the Jupyter notebook file to read (must be absolute, not relative)" - } - }, - "required": [ - "notebook_path" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - }, - { - "name": "NotebookEditCell", - "description": "Replace the contents of a specific cell in a Jupyter notebook.", - "inputSchema": { - "type": "object", - "properties": { - "notebook_path": { - "type": "string", - "description": "The absolute path to the Jupyter notebook file to edit (must be absolute, not relative)" - }, - "cell_number": { - "type": "number", - "description": "The index of the cell to edit (0-based)" - }, - "new_source": { - "type": "string", - "description": "The new source for the cell" - }, - "cell_type": { - "type": "string", - "enum": [ - "code", - "markdown" - ], - "description": "The type of the cell (code or markdown). If not specified, it defaults to the current cell type. If using edit_mode=insert, this is required." - }, - "edit_mode": { - "type": "string", - "description": "The type of edit to make (replace, insert, delete). Defaults to replace." - } - }, - "required": [ - "notebook_path", - "cell_number", - "new_source" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - }, - { - "name": "WebFetchTool", - "description": "Claude wants to fetch content from this URL", - "inputSchema": { - "type": "object", - "properties": { - "url": { - "type": "string", - "format": "uri", - "description": "The URL to fetch content from" - }, - "prompt": { - "type": "string", - "description": "The prompt to run on the fetched content" - } - }, - "required": [ - "url", - "prompt" - ], - "additionalProperties": false, - "$schema": "http://json-schema.org/draft-07/schema#" - } - } - ] - } -} -``` diff --git a/crates/agent/src/edit_agent/evals/fixtures/zode/react.py b/crates/agent/src/edit_agent/evals/fixtures/zode/react.py deleted file mode 100644 index 03ff02e789..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/zode/react.py +++ /dev/null @@ -1,14 +0,0 @@ -class InputCell: - def __init__(self, initial_value): - self.value = None - - -class ComputeCell: - def __init__(self, inputs, compute_function): - self.value = None - - def add_callback(self, callback): - pass - - def remove_callback(self, callback): - pass diff --git a/crates/agent/src/edit_agent/evals/fixtures/zode/react_test.py b/crates/agent/src/edit_agent/evals/fixtures/zode/react_test.py deleted file mode 100644 index 1f917e40b4..0000000000 --- a/crates/agent/src/edit_agent/evals/fixtures/zode/react_test.py +++ /dev/null @@ -1,271 +0,0 @@ -# These tests are auto-generated with test data from: -# https://github.com/exercism/problem-specifications/tree/main/exercises/react/canonical-data.json -# File last updated on 2023-07-19 - -from functools import partial -import unittest - -from react import ( - InputCell, - ComputeCell, -) - - -class ReactTest(unittest.TestCase): - def test_input_cells_have_a_value(self): - input = InputCell(10) - self.assertEqual(input.value, 10) - - def test_an_input_cell_s_value_can_be_set(self): - input = InputCell(4) - input.value = 20 - self.assertEqual(input.value, 20) - - def test_compute_cells_calculate_initial_value(self): - input = InputCell(1) - output = ComputeCell( - [ - input, - ], - lambda inputs: inputs[0] + 1, - ) - self.assertEqual(output.value, 2) - - def test_compute_cells_take_inputs_in_the_right_order(self): - one = InputCell(1) - two = InputCell(2) - output = ComputeCell( - [ - one, - two, - ], - lambda inputs: inputs[0] + inputs[1] * 10, - ) - self.assertEqual(output.value, 21) - - def test_compute_cells_update_value_when_dependencies_are_changed(self): - input = InputCell(1) - output = ComputeCell( - [ - input, - ], - lambda inputs: inputs[0] + 1, - ) - input.value = 3 - self.assertEqual(output.value, 4) - - def test_compute_cells_can_depend_on_other_compute_cells(self): - input = InputCell(1) - times_two = ComputeCell( - [ - input, - ], - lambda inputs: inputs[0] * 2, - ) - times_thirty = ComputeCell( - [ - input, - ], - lambda inputs: inputs[0] * 30, - ) - output = ComputeCell( - [ - times_two, - times_thirty, - ], - lambda inputs: inputs[0] + inputs[1], - ) - self.assertEqual(output.value, 32) - input.value = 3 - self.assertEqual(output.value, 96) - - def test_compute_cells_fire_callbacks(self): - input = InputCell(1) - output = ComputeCell( - [ - input, - ], - lambda inputs: inputs[0] + 1, - ) - cb1_observer = [] - callback1 = self.callback_factory(cb1_observer) - output.add_callback(callback1) - input.value = 3 - self.assertEqual(cb1_observer[-1], 4) - - def test_callback_cells_only_fire_on_change(self): - input = InputCell(1) - output = ComputeCell([input], lambda inputs: 111 if inputs[0] < 3 else 222) - cb1_observer = [] - callback1 = self.callback_factory(cb1_observer) - output.add_callback(callback1) - input.value = 2 - self.assertEqual(cb1_observer, []) - input.value = 4 - self.assertEqual(cb1_observer[-1], 222) - - def test_callbacks_do_not_report_already_reported_values(self): - input = InputCell(1) - output = ComputeCell( - [ - input, - ], - lambda inputs: inputs[0] + 1, - ) - cb1_observer = [] - callback1 = self.callback_factory(cb1_observer) - output.add_callback(callback1) - input.value = 2 - self.assertEqual(cb1_observer[-1], 3) - input.value = 3 - self.assertEqual(cb1_observer[-1], 4) - - def test_callbacks_can_fire_from_multiple_cells(self): - input = InputCell(1) - plus_one = ComputeCell( - [ - input, - ], - lambda inputs: inputs[0] + 1, - ) - minus_one = ComputeCell( - [ - input, - ], - lambda inputs: inputs[0] - 1, - ) - cb1_observer = [] - cb2_observer = [] - callback1 = self.callback_factory(cb1_observer) - callback2 = self.callback_factory(cb2_observer) - plus_one.add_callback(callback1) - minus_one.add_callback(callback2) - input.value = 10 - self.assertEqual(cb1_observer[-1], 11) - self.assertEqual(cb2_observer[-1], 9) - - def test_callbacks_can_be_added_and_removed(self): - input = InputCell(11) - output = ComputeCell( - [ - input, - ], - lambda inputs: inputs[0] + 1, - ) - cb1_observer = [] - cb2_observer = [] - cb3_observer = [] - callback1 = self.callback_factory(cb1_observer) - callback2 = self.callback_factory(cb2_observer) - callback3 = self.callback_factory(cb3_observer) - output.add_callback(callback1) - output.add_callback(callback2) - input.value = 31 - self.assertEqual(cb1_observer[-1], 32) - self.assertEqual(cb2_observer[-1], 32) - output.remove_callback(callback1) - output.add_callback(callback3) - input.value = 41 - self.assertEqual(len(cb1_observer), 1) - self.assertEqual(cb2_observer[-1], 42) - self.assertEqual(cb3_observer[-1], 42) - - def test_removing_a_callback_multiple_times_doesn_t_interfere_with_other_callbacks( - self, - ): - input = InputCell(1) - output = ComputeCell( - [ - input, - ], - lambda inputs: inputs[0] + 1, - ) - cb1_observer = [] - cb2_observer = [] - callback1 = self.callback_factory(cb1_observer) - callback2 = self.callback_factory(cb2_observer) - output.add_callback(callback1) - output.add_callback(callback2) - output.remove_callback(callback1) - output.remove_callback(callback1) - output.remove_callback(callback1) - input.value = 2 - self.assertEqual(cb1_observer, []) - self.assertEqual(cb2_observer[-1], 3) - - def test_callbacks_should_only_be_called_once_even_if_multiple_dependencies_change( - self, - ): - input = InputCell(1) - plus_one = ComputeCell( - [ - input, - ], - lambda inputs: inputs[0] + 1, - ) - minus_one1 = ComputeCell( - [ - input, - ], - lambda inputs: inputs[0] - 1, - ) - minus_one2 = ComputeCell( - [ - minus_one1, - ], - lambda inputs: inputs[0] - 1, - ) - output = ComputeCell( - [ - plus_one, - minus_one2, - ], - lambda inputs: inputs[0] * inputs[1], - ) - cb1_observer = [] - callback1 = self.callback_factory(cb1_observer) - output.add_callback(callback1) - input.value = 4 - self.assertEqual(cb1_observer[-1], 10) - - def test_callbacks_should_not_be_called_if_dependencies_change_but_output_value_doesn_t_change( - self, - ): - input = InputCell(1) - plus_one = ComputeCell( - [ - input, - ], - lambda inputs: inputs[0] + 1, - ) - minus_one = ComputeCell( - [ - input, - ], - lambda inputs: inputs[0] - 1, - ) - always_two = ComputeCell( - [ - plus_one, - minus_one, - ], - lambda inputs: inputs[0] - inputs[1], - ) - cb1_observer = [] - callback1 = self.callback_factory(cb1_observer) - always_two.add_callback(callback1) - input.value = 2 - self.assertEqual(cb1_observer, []) - input.value = 3 - self.assertEqual(cb1_observer, []) - input.value = 4 - self.assertEqual(cb1_observer, []) - input.value = 5 - self.assertEqual(cb1_observer, []) - - # Utility functions. - def callback_factory(self, observer): - def callback(observer, value): - observer.append(value) - - return partial(callback, observer) diff --git a/crates/agent/src/edit_agent/streaming_fuzzy_matcher.rs b/crates/agent/src/edit_agent/streaming_fuzzy_matcher.rs deleted file mode 100644 index 904ec05a8c..0000000000 --- a/crates/agent/src/edit_agent/streaming_fuzzy_matcher.rs +++ /dev/null @@ -1,806 +0,0 @@ -use language::{Point, TextBufferSnapshot}; -use std::{cmp, ops::Range}; - -const REPLACEMENT_COST: u32 = 1; -const INSERTION_COST: u32 = 3; -const DELETION_COST: u32 = 10; - -/// A streaming fuzzy matcher that can process text chunks incrementally -/// and return the best match found so far at each step. -pub struct StreamingFuzzyMatcher { - snapshot: TextBufferSnapshot, - query_lines: Vec, - line_hint: Option, - incomplete_line: String, - matches: Vec>, - matrix: SearchMatrix, -} - -impl StreamingFuzzyMatcher { - pub fn new(snapshot: TextBufferSnapshot) -> Self { - let buffer_line_count = snapshot.max_point().row as usize + 1; - Self { - snapshot, - query_lines: Vec::new(), - line_hint: None, - incomplete_line: String::new(), - matches: Vec::new(), - matrix: SearchMatrix::new(buffer_line_count + 1), - } - } - - /// Returns the query lines. - pub fn query_lines(&self) -> &[String] { - &self.query_lines - } - - /// Push a new chunk of text and get the best match found so far. - /// - /// This method accumulates text chunks and processes complete lines. - /// Partial lines are buffered internally until a newline is received. - /// - /// # Returns - /// - /// Returns `Some(range)` if a match has been found with the accumulated - /// query so far, or `None` if no suitable match exists yet. - pub fn push(&mut self, chunk: &str, line_hint: Option) -> Option> { - if line_hint.is_some() { - self.line_hint = line_hint; - } - - // Add the chunk to our incomplete line buffer - self.incomplete_line.push_str(chunk); - self.line_hint = line_hint; - - if let Some((last_pos, _)) = self.incomplete_line.match_indices('\n').next_back() { - let complete_part = &self.incomplete_line[..=last_pos]; - - // Split into lines and add to query_lines - for line in complete_part.lines() { - self.query_lines.push(line.to_string()); - } - - self.incomplete_line.replace_range(..last_pos + 1, ""); - - self.matches = self.resolve_location_fuzzy(); - } - - let best_match = self.select_best_match(); - best_match.or_else(|| self.matches.first().cloned()) - } - - /// Finish processing and return the final best match(es). - /// - /// This processes any remaining incomplete line before returning the final - /// match result. - pub fn finish(&mut self) -> Vec> { - // Process any remaining incomplete line - if !self.incomplete_line.is_empty() { - self.query_lines.push(self.incomplete_line.clone()); - self.incomplete_line.clear(); - self.matches = self.resolve_location_fuzzy(); - } - self.matches.clone() - } - - fn resolve_location_fuzzy(&mut self) -> Vec> { - let new_query_line_count = self.query_lines.len(); - let old_query_line_count = self.matrix.rows.saturating_sub(1); - if new_query_line_count == old_query_line_count { - return Vec::new(); - } - - self.matrix.resize_rows(new_query_line_count + 1); - - // Process only the new query lines - for row in old_query_line_count..new_query_line_count { - let query_line = self.query_lines[row].trim(); - let leading_deletion_cost = (row + 1) as u32 * DELETION_COST; - - self.matrix.set( - row + 1, - 0, - SearchState::new(leading_deletion_cost, SearchDirection::Up), - ); - - let mut buffer_lines = self.snapshot.as_rope().chunks().lines(); - let mut col = 0; - while let Some(buffer_line) = buffer_lines.next() { - let buffer_line = buffer_line.trim(); - let up = SearchState::new( - self.matrix - .get(row, col + 1) - .cost - .saturating_add(DELETION_COST), - SearchDirection::Up, - ); - let left = SearchState::new( - self.matrix - .get(row + 1, col) - .cost - .saturating_add(INSERTION_COST), - SearchDirection::Left, - ); - let diagonal = SearchState::new( - if query_line == buffer_line { - self.matrix.get(row, col).cost - } else if fuzzy_eq(query_line, buffer_line) { - self.matrix.get(row, col).cost + REPLACEMENT_COST - } else { - self.matrix - .get(row, col) - .cost - .saturating_add(DELETION_COST + INSERTION_COST) - }, - SearchDirection::Diagonal, - ); - self.matrix - .set(row + 1, col + 1, up.min(left).min(diagonal)); - col += 1; - } - } - - // Find all matches with the best cost - let buffer_line_count = self.snapshot.max_point().row as usize + 1; - let mut best_cost = u32::MAX; - let mut matches_with_best_cost = Vec::new(); - - for col in 1..=buffer_line_count { - let cost = self.matrix.get(new_query_line_count, col).cost; - if cost < best_cost { - best_cost = cost; - matches_with_best_cost.clear(); - matches_with_best_cost.push(col as u32); - } else if cost == best_cost { - matches_with_best_cost.push(col as u32); - } - } - - // Find ranges for the matches - let mut valid_matches = Vec::new(); - for &buffer_row_end in &matches_with_best_cost { - let mut matched_lines = 0; - let mut query_row = new_query_line_count; - let mut buffer_row_start = buffer_row_end; - while query_row > 0 && buffer_row_start > 0 { - let current = self.matrix.get(query_row, buffer_row_start as usize); - match current.direction { - SearchDirection::Diagonal => { - query_row -= 1; - buffer_row_start -= 1; - matched_lines += 1; - } - SearchDirection::Up => { - query_row -= 1; - } - SearchDirection::Left => { - buffer_row_start -= 1; - } - } - } - - let matched_buffer_row_count = buffer_row_end - buffer_row_start; - let matched_ratio = matched_lines as f32 - / (matched_buffer_row_count as f32).max(new_query_line_count as f32); - if matched_ratio >= 0.8 { - let buffer_start_ix = self - .snapshot - .point_to_offset(Point::new(buffer_row_start, 0)); - let buffer_end_ix = self.snapshot.point_to_offset(Point::new( - buffer_row_end - 1, - self.snapshot.line_len(buffer_row_end - 1), - )); - valid_matches.push((buffer_row_start, buffer_start_ix..buffer_end_ix)); - } - } - - valid_matches.into_iter().map(|(_, range)| range).collect() - } - - /// Return the best match with starting position close enough to line_hint. - pub fn select_best_match(&self) -> Option> { - // Allow line hint to be off by that many lines. - // Higher values increase probability of applying edits to a wrong place, - // Lower values increase edits failures and overall conversation length. - const LINE_HINT_TOLERANCE: u32 = 200; - - if self.matches.is_empty() { - return None; - } - - if self.matches.len() == 1 { - return self.matches.first().cloned(); - } - - let Some(line_hint) = self.line_hint else { - // Multiple ambiguous matches - return None; - }; - - let mut best_match = None; - let mut best_distance = u32::MAX; - - for range in &self.matches { - let start_point = self.snapshot.offset_to_point(range.start); - let start_line = start_point.row; - let distance = start_line.abs_diff(line_hint); - - if distance <= LINE_HINT_TOLERANCE && distance < best_distance { - best_distance = distance; - best_match = Some(range.clone()); - } - } - - best_match - } -} - -fn fuzzy_eq(left: &str, right: &str) -> bool { - const THRESHOLD: f64 = 0.8; - - let min_levenshtein = left.len().abs_diff(right.len()); - let min_normalized_levenshtein = - 1. - (min_levenshtein as f64 / cmp::max(left.len(), right.len()) as f64); - if min_normalized_levenshtein < THRESHOLD { - return false; - } - - strsim::normalized_levenshtein(left, right) >= THRESHOLD -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -enum SearchDirection { - Up, - Left, - Diagonal, -} - -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] -struct SearchState { - cost: u32, - direction: SearchDirection, -} - -impl SearchState { - fn new(cost: u32, direction: SearchDirection) -> Self { - Self { cost, direction } - } -} - -struct SearchMatrix { - cols: usize, - rows: usize, - data: Vec, -} - -impl SearchMatrix { - fn new(cols: usize) -> Self { - SearchMatrix { - cols, - rows: 0, - data: Vec::new(), - } - } - - fn resize_rows(&mut self, needed_rows: usize) { - debug_assert!(needed_rows > self.rows); - self.rows = needed_rows; - self.data.resize( - self.rows * self.cols, - SearchState::new(0, SearchDirection::Diagonal), - ); - } - - fn get(&self, row: usize, col: usize) -> SearchState { - debug_assert!(row < self.rows && col < self.cols); - self.data[row * self.cols + col] - } - - fn set(&mut self, row: usize, col: usize, state: SearchState) { - debug_assert!(row < self.rows && col < self.cols); - self.data[row * self.cols + col] = state; - } -} - -#[cfg(test)] -mod tests { - use super::*; - use indoc::indoc; - use language::{BufferId, TextBuffer}; - use rand::prelude::*; - use text::ReplicaId; - use util::test::{generate_marked_text, marked_text_ranges}; - - #[test] - fn test_empty_query() { - let buffer = TextBuffer::new( - ReplicaId::LOCAL, - BufferId::new(1).unwrap(), - "Hello world\nThis is a test\nFoo bar baz", - ); - let snapshot = buffer.snapshot(); - - let mut finder = StreamingFuzzyMatcher::new(snapshot); - assert_eq!(push(&mut finder, ""), None); - assert_eq!(finish(finder), None); - } - - #[test] - fn test_streaming_exact_match() { - let buffer = TextBuffer::new( - ReplicaId::LOCAL, - BufferId::new(1).unwrap(), - "Hello world\nThis is a test\nFoo bar baz", - ); - let snapshot = buffer.snapshot(); - - let mut finder = StreamingFuzzyMatcher::new(snapshot); - - // Push partial query - assert_eq!(push(&mut finder, "This"), None); - - // Complete the line - assert_eq!( - push(&mut finder, " is a test\n"), - Some("This is a test".to_string()) - ); - - // Finish should return the same result - assert_eq!(finish(finder), Some("This is a test".to_string())); - } - - #[test] - fn test_streaming_fuzzy_match() { - let buffer = TextBuffer::new( - ReplicaId::LOCAL, - BufferId::new(1).unwrap(), - indoc! {" - function foo(a, b) { - return a + b; - } - - function bar(x, y) { - return x * y; - } - "}, - ); - let snapshot = buffer.snapshot(); - - let mut finder = StreamingFuzzyMatcher::new(snapshot); - - // Push a fuzzy query that should match the first function - assert_eq!( - push(&mut finder, "function foo(a, c) {\n").as_deref(), - Some("function foo(a, b) {") - ); - assert_eq!( - push(&mut finder, " return a + c;\n}\n").as_deref(), - Some(concat!( - "function foo(a, b) {\n", - " return a + b;\n", - "}" - )) - ); - } - - #[test] - fn test_incremental_improvement() { - let buffer = TextBuffer::new( - ReplicaId::LOCAL, - BufferId::new(1).unwrap(), - "Line 1\nLine 2\nLine 3\nLine 4\nLine 5", - ); - let snapshot = buffer.snapshot(); - - let mut finder = StreamingFuzzyMatcher::new(snapshot); - - // No match initially - assert_eq!(push(&mut finder, "Lin"), None); - - // Get a match when we complete a line - assert_eq!(push(&mut finder, "e 3\n"), Some("Line 3".to_string())); - - // The match might change if we add more specific content - assert_eq!( - push(&mut finder, "Line 4\n"), - Some("Line 3\nLine 4".to_string()) - ); - assert_eq!(finish(finder), Some("Line 3\nLine 4".to_string())); - } - - #[test] - fn test_incomplete_lines_buffering() { - let buffer = TextBuffer::new( - ReplicaId::LOCAL, - BufferId::new(1).unwrap(), - indoc! {" - The quick brown fox - jumps over the lazy dog - Pack my box with five dozen liquor jugs - "}, - ); - let snapshot = buffer.snapshot(); - - let mut finder = StreamingFuzzyMatcher::new(snapshot); - - // Push text in small chunks across line boundaries - assert_eq!(push(&mut finder, "jumps "), None); // No newline yet - assert_eq!(push(&mut finder, "over the"), None); // Still no newline - assert_eq!(push(&mut finder, " lazy"), None); // Still incomplete - - // Complete the line - assert_eq!( - push(&mut finder, " dog\n"), - Some("jumps over the lazy dog".to_string()) - ); - } - - #[test] - fn test_multiline_fuzzy_match() { - let buffer = TextBuffer::new( - ReplicaId::LOCAL, - BufferId::new(1).unwrap(), - indoc! {r#" - impl Display for User { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - write!(f, "User: {} ({})", self.name, self.email) - } - } - - impl Debug for User { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - f.debug_struct("User") - .field("name", &self.name) - .field("email", &self.email) - .finish() - } - } - "#}, - ); - let snapshot = buffer.snapshot(); - - let mut finder = StreamingFuzzyMatcher::new(snapshot); - - assert_eq!( - push(&mut finder, "impl Debug for User {\n"), - Some("impl Debug for User {".to_string()) - ); - assert_eq!( - push( - &mut finder, - " fn fmt(&self, f: &mut Formatter) -> Result {\n" - ) - .as_deref(), - Some(concat!( - "impl Debug for User {\n", - " fn fmt(&self, f: &mut Formatter) -> fmt::Result {" - )) - ); - assert_eq!( - push(&mut finder, " f.debug_struct(\"User\")\n").as_deref(), - Some(concat!( - "impl Debug for User {\n", - " fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n", - " f.debug_struct(\"User\")" - )) - ); - assert_eq!( - push( - &mut finder, - " .field(\"name\", &self.username)\n" - ) - .as_deref(), - Some(concat!( - "impl Debug for User {\n", - " fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n", - " f.debug_struct(\"User\")\n", - " .field(\"name\", &self.name)" - )) - ); - assert_eq!( - finish(finder).as_deref(), - Some(concat!( - "impl Debug for User {\n", - " fn fmt(&self, f: &mut Formatter) -> fmt::Result {\n", - " f.debug_struct(\"User\")\n", - " .field(\"name\", &self.name)" - )) - ); - } - - #[gpui::test(iterations = 100)] - fn test_resolve_location_single_line(mut rng: StdRng) { - assert_location_resolution( - concat!( - " Lorem\n", - "« ipsum»\n", - " dolor sit amet\n", - " consecteur", - ), - "ipsum", - &mut rng, - ); - } - - #[gpui::test(iterations = 100)] - fn test_resolve_location_multiline(mut rng: StdRng) { - assert_location_resolution( - concat!( - " Lorem\n", - "« ipsum\n", - " dolor sit amet»\n", - " consecteur", - ), - "ipsum\ndolor sit amet", - &mut rng, - ); - } - - #[gpui::test(iterations = 100)] - fn test_resolve_location_function_with_typo(mut rng: StdRng) { - assert_location_resolution( - indoc! {" - «fn foo1(a: usize) -> usize { - 40 - }» - - fn foo2(b: usize) -> usize { - 42 - } - "}, - "fn foo1(a: usize) -> u32 {\n40\n}", - &mut rng, - ); - } - - #[gpui::test(iterations = 100)] - fn test_resolve_location_class_methods(mut rng: StdRng) { - assert_location_resolution( - indoc! {" - class Something { - one() { return 1; } - « two() { return 2222; } - three() { return 333; } - four() { return 4444; } - five() { return 5555; } - six() { return 6666; }» - seven() { return 7; } - eight() { return 8; } - } - "}, - indoc! {" - two() { return 2222; } - four() { return 4444; } - five() { return 5555; } - six() { return 6666; } - "}, - &mut rng, - ); - } - - #[gpui::test(iterations = 100)] - fn test_resolve_location_imports_no_match(mut rng: StdRng) { - assert_location_resolution( - indoc! {" - use std::ops::Range; - use std::sync::Mutex; - use std::{ - collections::HashMap, - env, - ffi::{OsStr, OsString}, - fs, - io::{BufRead, BufReader}, - mem, - path::{Path, PathBuf}, - process::Command, - sync::LazyLock, - time::SystemTime, - }; - "}, - indoc! {" - use std::collections::{HashMap, HashSet}; - use std::ffi::{OsStr, OsString}; - use std::fmt::Write as _; - use std::fs; - use std::io::{BufReader, Read, Write}; - use std::mem; - use std::path::{Path, PathBuf}; - use std::process::Command; - use std::sync::Arc; - "}, - &mut rng, - ); - } - - #[gpui::test(iterations = 100)] - fn test_resolve_location_nested_closure(mut rng: StdRng) { - assert_location_resolution( - indoc! {" - impl Foo { - fn new() -> Self { - Self { - subscriptions: vec![ - cx.observe_window_activation(window, |editor, window, cx| { - let active = window.is_window_active(); - editor.blink_manager.update(cx, |blink_manager, cx| { - if active { - blink_manager.enable(cx); - } else { - blink_manager.disable(cx); - } - }); - }), - ]; - } - } - } - "}, - concat!( - " editor.blink_manager.update(cx, |blink_manager, cx| {\n", - " blink_manager.enable(cx);\n", - " });", - ), - &mut rng, - ); - } - - #[gpui::test(iterations = 100)] - fn test_resolve_location_tool_invocation(mut rng: StdRng) { - assert_location_resolution( - indoc! {r#" - let tool = cx - .update(|cx| working_set.tool(&tool_name, cx)) - .map_err(|err| { - anyhow!("Failed to look up tool '{}': {}", tool_name, err) - })?; - - let Some(tool) = tool else { - return Err(anyhow!("Tool '{}' not found", tool_name)); - }; - - let project = project.clone(); - let action_log = action_log.clone(); - let messages = messages.clone(); - let tool_result = cx - .update(|cx| tool.run(invocation.input, &messages, project, action_log, cx)) - .map_err(|err| anyhow!("Failed to start tool '{}': {}", tool_name, err))?; - - tasks.push(tool_result.output); - "#}, - concat!( - "let tool_result = cx\n", - " .update(|cx| tool.run(invocation.input, &messages, project, action_log, cx))\n", - " .output;", - ), - &mut rng, - ); - } - - #[gpui::test] - fn test_line_hint_selection() { - let text = indoc! {r#" - fn first_function() { - return 42; - } - - fn second_function() { - return 42; - } - - fn third_function() { - return 42; - } - "#}; - - let buffer = TextBuffer::new( - ReplicaId::LOCAL, - BufferId::new(1).unwrap(), - text.to_string(), - ); - let snapshot = buffer.snapshot(); - let mut matcher = StreamingFuzzyMatcher::new(snapshot.clone()); - - // Given a query that matches all three functions - let query = "return 42;\n"; - - // Test with line hint pointing to second function (around line 5) - let best_match = matcher.push(query, Some(5)).expect("Failed to match query"); - - let matched_text = snapshot - .text_for_range(best_match.clone()) - .collect::(); - assert!(matched_text.contains("return 42;")); - assert_eq!( - best_match, - 63..77, - "Expected to match `second_function` based on the line hint" - ); - - let mut matcher = StreamingFuzzyMatcher::new(snapshot); - matcher.push(query, None); - matcher.finish(); - let best_match = matcher.select_best_match(); - assert!( - best_match.is_none(), - "Best match should be None when query cannot be uniquely resolved" - ); - } - - #[track_caller] - fn assert_location_resolution(text_with_expected_range: &str, query: &str, rng: &mut StdRng) { - let (text, expected_ranges) = marked_text_ranges(text_with_expected_range, false); - let buffer = TextBuffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), text.clone()); - let snapshot = buffer.snapshot(); - - let mut matcher = StreamingFuzzyMatcher::new(snapshot); - - // Split query into random chunks - let chunks = to_random_chunks(rng, query); - - // Push chunks incrementally - for chunk in &chunks { - matcher.push(chunk, None); - } - - let actual_ranges = matcher.finish(); - - // If no expected ranges, we expect no match - if expected_ranges.is_empty() { - assert!( - actual_ranges.is_empty(), - "Expected no match for query: {:?}, but found: {:?}", - query, - actual_ranges - ); - } else { - let text_with_actual_range = generate_marked_text(&text, &actual_ranges, false); - pretty_assertions::assert_eq!( - text_with_actual_range, - text_with_expected_range, - indoc! {" - Query: {:?} - Chunks: {:?} - Expected marked text: {} - Actual marked text: {} - Expected ranges: {:?} - Actual ranges: {:?}" - }, - query, - chunks, - text_with_expected_range, - text_with_actual_range, - expected_ranges, - actual_ranges - ); - } - } - - fn to_random_chunks(rng: &mut StdRng, input: &str) -> Vec { - let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50)); - let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count); - chunk_indices.sort(); - chunk_indices.push(input.len()); - - let mut chunks = Vec::new(); - let mut last_ix = 0; - for chunk_ix in chunk_indices { - chunks.push(input[last_ix..chunk_ix].to_string()); - last_ix = chunk_ix; - } - chunks - } - - fn push(finder: &mut StreamingFuzzyMatcher, chunk: &str) -> Option { - finder - .push(chunk, None) - .map(|range| finder.snapshot.text_for_range(range).collect::()) - } - - fn finish(mut finder: StreamingFuzzyMatcher) -> Option { - let snapshot = finder.snapshot.clone(); - let matches = finder.finish(); - matches - .first() - .map(|range| snapshot.text_for_range(range.clone()).collect::()) - } -} diff --git a/crates/agent/src/history_store.rs b/crates/agent/src/history_store.rs deleted file mode 100644 index 5a1b923d13..0000000000 --- a/crates/agent/src/history_store.rs +++ /dev/null @@ -1,412 +0,0 @@ -use crate::{DbThread, DbThreadMetadata, ThreadsDatabase}; -use acp_thread::MentionUri; -use agent_client_protocol as acp; -use anyhow::{Context as _, Result, anyhow}; -use assistant_text_thread::{SavedTextThreadMetadata, TextThread}; -use chrono::{DateTime, Utc}; -use db::kvp::KEY_VALUE_STORE; -use gpui::{App, AsyncApp, Entity, SharedString, Task, prelude::*}; -use itertools::Itertools; -use paths::text_threads_dir; -use project::Project; -use serde::{Deserialize, Serialize}; -use std::{collections::VecDeque, path::Path, rc::Rc, sync::Arc, time::Duration}; -use ui::ElementId; -use util::ResultExt as _; - -const MAX_RECENTLY_OPENED_ENTRIES: usize = 6; -const RECENTLY_OPENED_THREADS_KEY: &str = "recent-agent-threads"; -const SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE: Duration = Duration::from_millis(50); - -const DEFAULT_TITLE: &SharedString = &SharedString::new_static("New Thread"); - -//todo: We should remove this function once we support loading all acp thread -pub fn load_agent_thread( - session_id: acp::SessionId, - history_store: Entity, - project: Entity, - cx: &mut App, -) -> Task>> { - use agent_servers::{AgentServer, AgentServerDelegate}; - - let server = Rc::new(crate::NativeAgentServer::new( - project.read(cx).fs().clone(), - history_store, - )); - let delegate = AgentServerDelegate::new( - project.read(cx).agent_server_store().clone(), - project.clone(), - None, - None, - ); - let connection = server.connect(None, delegate, cx); - cx.spawn(async move |cx| { - let (agent, _) = connection.await?; - let agent = agent.downcast::().unwrap(); - cx.update(|cx| agent.load_thread(session_id, cx))?.await - }) -} - -#[derive(Clone, Debug)] -pub enum HistoryEntry { - AcpThread(DbThreadMetadata), - TextThread(SavedTextThreadMetadata), -} - -impl HistoryEntry { - pub fn updated_at(&self) -> DateTime { - match self { - HistoryEntry::AcpThread(thread) => thread.updated_at, - HistoryEntry::TextThread(text_thread) => text_thread.mtime.to_utc(), - } - } - - pub fn id(&self) -> HistoryEntryId { - match self { - HistoryEntry::AcpThread(thread) => HistoryEntryId::AcpThread(thread.id.clone()), - HistoryEntry::TextThread(text_thread) => { - HistoryEntryId::TextThread(text_thread.path.clone()) - } - } - } - - pub fn mention_uri(&self) -> MentionUri { - match self { - HistoryEntry::AcpThread(thread) => MentionUri::Thread { - id: thread.id.clone(), - name: thread.title.to_string(), - }, - HistoryEntry::TextThread(text_thread) => MentionUri::TextThread { - path: text_thread.path.as_ref().to_owned(), - name: text_thread.title.to_string(), - }, - } - } - - pub fn title(&self) -> &SharedString { - match self { - HistoryEntry::AcpThread(thread) => { - if thread.title.is_empty() { - DEFAULT_TITLE - } else { - &thread.title - } - } - HistoryEntry::TextThread(text_thread) => &text_thread.title, - } - } -} - -/// Generic identifier for a history entry. -#[derive(Clone, PartialEq, Eq, Debug, Hash)] -pub enum HistoryEntryId { - AcpThread(acp::SessionId), - TextThread(Arc), -} - -impl Into for HistoryEntryId { - fn into(self) -> ElementId { - match self { - HistoryEntryId::AcpThread(session_id) => ElementId::Name(session_id.0.into()), - HistoryEntryId::TextThread(path) => ElementId::Path(path), - } - } -} - -#[derive(Serialize, Deserialize, Debug)] -enum SerializedRecentOpen { - AcpThread(String), - TextThread(String), -} - -pub struct HistoryStore { - threads: Vec, - entries: Vec, - text_thread_store: Entity, - recently_opened_entries: VecDeque, - _subscriptions: Vec, - _save_recently_opened_entries_task: Task<()>, -} - -impl HistoryStore { - pub fn new( - text_thread_store: Entity, - cx: &mut Context, - ) -> Self { - let subscriptions = - vec![cx.observe(&text_thread_store, |this, _, cx| this.update_entries(cx))]; - - cx.spawn(async move |this, cx| { - let entries = Self::load_recently_opened_entries(cx).await; - this.update(cx, |this, cx| { - if let Some(entries) = entries.log_err() { - this.recently_opened_entries = entries; - } - - this.reload(cx); - }) - .ok(); - }) - .detach(); - - Self { - text_thread_store, - recently_opened_entries: VecDeque::default(), - threads: Vec::default(), - entries: Vec::default(), - _subscriptions: subscriptions, - _save_recently_opened_entries_task: Task::ready(()), - } - } - - pub fn thread_from_session_id(&self, session_id: &acp::SessionId) -> Option<&DbThreadMetadata> { - self.threads.iter().find(|thread| &thread.id == session_id) - } - - pub fn load_thread( - &mut self, - id: acp::SessionId, - cx: &mut Context, - ) -> Task>> { - let database_future = ThreadsDatabase::connect(cx); - cx.background_spawn(async move { - let database = database_future.await.map_err(|err| anyhow!(err))?; - database.load_thread(id).await - }) - } - - pub fn delete_thread( - &mut self, - id: acp::SessionId, - cx: &mut Context, - ) -> Task> { - let database_future = ThreadsDatabase::connect(cx); - cx.spawn(async move |this, cx| { - let database = database_future.await.map_err(|err| anyhow!(err))?; - database.delete_thread(id.clone()).await?; - this.update(cx, |this, cx| this.reload(cx)) - }) - } - - pub fn delete_threads(&mut self, cx: &mut Context) -> Task> { - let database_future = ThreadsDatabase::connect(cx); - cx.spawn(async move |this, cx| { - let database = database_future.await.map_err(|err| anyhow!(err))?; - database.delete_threads().await?; - this.update(cx, |this, cx| this.reload(cx)) - }) - } - - pub fn delete_text_thread( - &mut self, - path: Arc, - cx: &mut Context, - ) -> Task> { - self.text_thread_store - .update(cx, |store, cx| store.delete_local(path, cx)) - } - - pub fn load_text_thread( - &self, - path: Arc, - cx: &mut Context, - ) -> Task>> { - self.text_thread_store - .update(cx, |store, cx| store.open_local(path, cx)) - } - - pub fn reload(&self, cx: &mut Context) { - let database_future = ThreadsDatabase::connect(cx); - cx.spawn(async move |this, cx| { - let threads = database_future - .await - .map_err(|err| anyhow!(err))? - .list_threads() - .await?; - - this.update(cx, |this, cx| { - if this.recently_opened_entries.len() < MAX_RECENTLY_OPENED_ENTRIES { - for thread in threads - .iter() - .take(MAX_RECENTLY_OPENED_ENTRIES - this.recently_opened_entries.len()) - .rev() - { - this.push_recently_opened_entry( - HistoryEntryId::AcpThread(thread.id.clone()), - cx, - ) - } - } - this.threads = threads; - this.update_entries(cx); - }) - }) - .detach_and_log_err(cx); - } - - fn update_entries(&mut self, cx: &mut Context) { - #[cfg(debug_assertions)] - if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() { - return; - } - let mut history_entries = Vec::new(); - history_entries.extend(self.threads.iter().cloned().map(HistoryEntry::AcpThread)); - history_entries.extend( - self.text_thread_store - .read(cx) - .unordered_text_threads() - .cloned() - .map(HistoryEntry::TextThread), - ); - - history_entries.sort_unstable_by_key(|entry| std::cmp::Reverse(entry.updated_at())); - self.entries = history_entries; - cx.notify() - } - - pub fn is_empty(&self, _cx: &App) -> bool { - self.entries.is_empty() - } - - pub fn recently_opened_entries(&self, cx: &App) -> Vec { - #[cfg(debug_assertions)] - if std::env::var("ZED_SIMULATE_NO_THREAD_HISTORY").is_ok() { - return Vec::new(); - } - - let thread_entries = self.threads.iter().flat_map(|thread| { - self.recently_opened_entries - .iter() - .enumerate() - .flat_map(|(index, entry)| match entry { - HistoryEntryId::AcpThread(id) if &thread.id == id => { - Some((index, HistoryEntry::AcpThread(thread.clone()))) - } - _ => None, - }) - }); - - let context_entries = self - .text_thread_store - .read(cx) - .unordered_text_threads() - .flat_map(|text_thread| { - self.recently_opened_entries - .iter() - .enumerate() - .flat_map(|(index, entry)| match entry { - HistoryEntryId::TextThread(path) if &text_thread.path == path => { - Some((index, HistoryEntry::TextThread(text_thread.clone()))) - } - _ => None, - }) - }); - - thread_entries - .chain(context_entries) - // optimization to halt iteration early - .take(self.recently_opened_entries.len()) - .sorted_unstable_by_key(|(index, _)| *index) - .map(|(_, entry)| entry) - .collect() - } - - fn save_recently_opened_entries(&mut self, cx: &mut Context) { - let serialized_entries = self - .recently_opened_entries - .iter() - .filter_map(|entry| match entry { - HistoryEntryId::TextThread(path) => path.file_name().map(|file| { - SerializedRecentOpen::TextThread(file.to_string_lossy().into_owned()) - }), - HistoryEntryId::AcpThread(id) => { - Some(SerializedRecentOpen::AcpThread(id.to_string())) - } - }) - .collect::>(); - - self._save_recently_opened_entries_task = cx.spawn(async move |_, cx| { - let content = serde_json::to_string(&serialized_entries).unwrap(); - cx.background_executor() - .timer(SAVE_RECENTLY_OPENED_ENTRIES_DEBOUNCE) - .await; - - if cfg!(any(feature = "test-support", test)) { - return; - } - KEY_VALUE_STORE - .write_kvp(RECENTLY_OPENED_THREADS_KEY.to_owned(), content) - .await - .log_err(); - }); - } - - fn load_recently_opened_entries(cx: &AsyncApp) -> Task>> { - cx.background_spawn(async move { - if cfg!(any(feature = "test-support", test)) { - anyhow::bail!("history store does not persist in tests"); - } - let json = KEY_VALUE_STORE - .read_kvp(RECENTLY_OPENED_THREADS_KEY)? - .unwrap_or("[]".to_string()); - let entries = serde_json::from_str::>(&json) - .context("deserializing persisted agent panel navigation history")? - .into_iter() - .take(MAX_RECENTLY_OPENED_ENTRIES) - .flat_map(|entry| match entry { - SerializedRecentOpen::AcpThread(id) => { - Some(HistoryEntryId::AcpThread(acp::SessionId::new(id.as_str()))) - } - SerializedRecentOpen::TextThread(file_name) => Some( - HistoryEntryId::TextThread(text_threads_dir().join(file_name).into()), - ), - }) - .collect(); - Ok(entries) - }) - } - - pub fn push_recently_opened_entry(&mut self, entry: HistoryEntryId, cx: &mut Context) { - self.recently_opened_entries - .retain(|old_entry| old_entry != &entry); - self.recently_opened_entries.push_front(entry); - self.recently_opened_entries - .truncate(MAX_RECENTLY_OPENED_ENTRIES); - self.save_recently_opened_entries(cx); - } - - pub fn remove_recently_opened_thread(&mut self, id: acp::SessionId, cx: &mut Context) { - self.recently_opened_entries.retain( - |entry| !matches!(entry, HistoryEntryId::AcpThread(thread_id) if thread_id == &id), - ); - self.save_recently_opened_entries(cx); - } - - pub fn replace_recently_opened_text_thread( - &mut self, - old_path: &Path, - new_path: &Arc, - cx: &mut Context, - ) { - for entry in &mut self.recently_opened_entries { - match entry { - HistoryEntryId::TextThread(path) if path.as_ref() == old_path => { - *entry = HistoryEntryId::TextThread(new_path.clone()); - break; - } - _ => {} - } - } - self.save_recently_opened_entries(cx); - } - - pub fn remove_recently_opened_entry(&mut self, entry: &HistoryEntryId, cx: &mut Context) { - self.recently_opened_entries - .retain(|old_entry| old_entry != entry); - self.save_recently_opened_entries(cx); - } - - pub fn entries(&self) -> impl Iterator { - self.entries.iter().cloned() - } -} diff --git a/crates/agent/src/legacy_thread.rs b/crates/agent/src/legacy_thread.rs deleted file mode 100644 index 34babb8006..0000000000 --- a/crates/agent/src/legacy_thread.rs +++ /dev/null @@ -1,402 +0,0 @@ -use crate::ProjectSnapshot; -use agent_settings::{AgentProfileId, CompletionMode}; -use anyhow::Result; -use chrono::{DateTime, Utc}; -use gpui::SharedString; -use language_model::{LanguageModelToolResultContent, LanguageModelToolUseId, Role, TokenUsage}; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; - -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] -pub enum DetailedSummaryState { - #[default] - NotGenerated, - Generating, - Generated { - text: SharedString, - }, -} - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)] -pub struct MessageId(pub usize); - -#[derive(Serialize, Deserialize, Debug, PartialEq)] -pub struct SerializedThread { - pub version: String, - pub summary: SharedString, - pub updated_at: DateTime, - pub messages: Vec, - #[serde(default)] - pub initial_project_snapshot: Option>, - #[serde(default)] - pub cumulative_token_usage: TokenUsage, - #[serde(default)] - pub request_token_usage: Vec, - #[serde(default)] - pub detailed_summary_state: DetailedSummaryState, - #[serde(default)] - pub model: Option, - #[serde(default)] - pub completion_mode: Option, - #[serde(default)] - pub tool_use_limit_reached: bool, - #[serde(default)] - pub profile: Option, -} - -#[derive(Serialize, Deserialize, Debug, PartialEq)] -pub struct SerializedLanguageModel { - pub provider: String, - pub model: String, -} - -impl SerializedThread { - pub const VERSION: &'static str = "0.2.0"; - - pub fn from_json(json: &[u8]) -> Result { - let saved_thread_json = serde_json::from_slice::(json)?; - match saved_thread_json.get("version") { - Some(serde_json::Value::String(version)) => match version.as_str() { - SerializedThreadV0_1_0::VERSION => { - let saved_thread = - serde_json::from_value::(saved_thread_json)?; - Ok(saved_thread.upgrade()) - } - SerializedThread::VERSION => Ok(serde_json::from_value::( - saved_thread_json, - )?), - _ => anyhow::bail!("unrecognized serialized thread version: {version:?}"), - }, - None => { - let saved_thread = - serde_json::from_value::(saved_thread_json)?; - Ok(saved_thread.upgrade()) - } - version => anyhow::bail!("unrecognized serialized thread version: {version:?}"), - } - } -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct SerializedThreadV0_1_0( - // The structure did not change, so we are reusing the latest SerializedThread. - // When making the next version, make sure this points to SerializedThreadV0_2_0 - SerializedThread, -); - -impl SerializedThreadV0_1_0 { - pub const VERSION: &'static str = "0.1.0"; - - pub fn upgrade(self) -> SerializedThread { - debug_assert_eq!(SerializedThread::VERSION, "0.2.0"); - - let mut messages: Vec = Vec::with_capacity(self.0.messages.len()); - - for message in self.0.messages { - if message.role == Role::User - && !message.tool_results.is_empty() - && let Some(last_message) = messages.last_mut() - { - debug_assert!(last_message.role == Role::Assistant); - - last_message.tool_results = message.tool_results; - continue; - } - - messages.push(message); - } - - SerializedThread { - messages, - version: SerializedThread::VERSION.to_string(), - ..self.0 - } - } -} - -#[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct SerializedMessage { - pub id: MessageId, - pub role: Role, - #[serde(default)] - pub segments: Vec, - #[serde(default)] - pub tool_uses: Vec, - #[serde(default)] - pub tool_results: Vec, - #[serde(default)] - pub context: String, - #[serde(default)] - pub creases: Vec, - #[serde(default)] - pub is_hidden: bool, -} - -#[derive(Debug, Serialize, Deserialize, PartialEq)] -#[serde(tag = "type")] -pub enum SerializedMessageSegment { - #[serde(rename = "text")] - Text { - text: String, - }, - #[serde(rename = "thinking")] - Thinking { - text: String, - #[serde(skip_serializing_if = "Option::is_none")] - signature: Option, - }, - RedactedThinking { - data: String, - }, -} - -#[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct SerializedToolUse { - pub id: LanguageModelToolUseId, - pub name: SharedString, - pub input: serde_json::Value, -} - -#[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct SerializedToolResult { - pub tool_use_id: LanguageModelToolUseId, - pub is_error: bool, - pub content: LanguageModelToolResultContent, - pub output: Option, -} - -#[derive(Serialize, Deserialize)] -struct LegacySerializedThread { - pub summary: SharedString, - pub updated_at: DateTime, - pub messages: Vec, - #[serde(default)] - pub initial_project_snapshot: Option>, -} - -impl LegacySerializedThread { - pub fn upgrade(self) -> SerializedThread { - SerializedThread { - version: SerializedThread::VERSION.to_string(), - summary: self.summary, - updated_at: self.updated_at, - messages: self.messages.into_iter().map(|msg| msg.upgrade()).collect(), - initial_project_snapshot: self.initial_project_snapshot, - cumulative_token_usage: TokenUsage::default(), - request_token_usage: Vec::new(), - detailed_summary_state: DetailedSummaryState::default(), - model: None, - completion_mode: None, - tool_use_limit_reached: false, - profile: None, - } - } -} - -#[derive(Debug, Serialize, Deserialize)] -struct LegacySerializedMessage { - pub id: MessageId, - pub role: Role, - pub text: String, - #[serde(default)] - pub tool_uses: Vec, - #[serde(default)] - pub tool_results: Vec, -} - -impl LegacySerializedMessage { - fn upgrade(self) -> SerializedMessage { - SerializedMessage { - id: self.id, - role: self.role, - segments: vec![SerializedMessageSegment::Text { text: self.text }], - tool_uses: self.tool_uses, - tool_results: self.tool_results, - context: String::new(), - creases: Vec::new(), - is_hidden: false, - } - } -} - -#[derive(Debug, Serialize, Deserialize, PartialEq)] -pub struct SerializedCrease { - pub start: usize, - pub end: usize, - pub icon_path: SharedString, - pub label: SharedString, -} - -#[cfg(test)] -mod tests { - use super::*; - use chrono::Utc; - use language_model::{Role, TokenUsage}; - use pretty_assertions::assert_eq; - - #[test] - fn test_legacy_serialized_thread_upgrade() { - let updated_at = Utc::now(); - let legacy_thread = LegacySerializedThread { - summary: "Test conversation".into(), - updated_at, - messages: vec![LegacySerializedMessage { - id: MessageId(1), - role: Role::User, - text: "Hello, world!".to_string(), - tool_uses: vec![], - tool_results: vec![], - }], - initial_project_snapshot: None, - }; - - let upgraded = legacy_thread.upgrade(); - - assert_eq!( - upgraded, - SerializedThread { - summary: "Test conversation".into(), - updated_at, - messages: vec![SerializedMessage { - id: MessageId(1), - role: Role::User, - segments: vec![SerializedMessageSegment::Text { - text: "Hello, world!".to_string() - }], - tool_uses: vec![], - tool_results: vec![], - context: "".to_string(), - creases: vec![], - is_hidden: false - }], - version: SerializedThread::VERSION.to_string(), - initial_project_snapshot: None, - cumulative_token_usage: TokenUsage::default(), - request_token_usage: vec![], - detailed_summary_state: DetailedSummaryState::default(), - model: None, - completion_mode: None, - tool_use_limit_reached: false, - profile: None - } - ) - } - - #[test] - fn test_serialized_threadv0_1_0_upgrade() { - let updated_at = Utc::now(); - let thread_v0_1_0 = SerializedThreadV0_1_0(SerializedThread { - summary: "Test conversation".into(), - updated_at, - messages: vec![ - SerializedMessage { - id: MessageId(1), - role: Role::User, - segments: vec![SerializedMessageSegment::Text { - text: "Use tool_1".to_string(), - }], - tool_uses: vec![], - tool_results: vec![], - context: "".to_string(), - creases: vec![], - is_hidden: false, - }, - SerializedMessage { - id: MessageId(2), - role: Role::Assistant, - segments: vec![SerializedMessageSegment::Text { - text: "I want to use a tool".to_string(), - }], - tool_uses: vec![SerializedToolUse { - id: "abc".into(), - name: "tool_1".into(), - input: serde_json::Value::Null, - }], - tool_results: vec![], - context: "".to_string(), - creases: vec![], - is_hidden: false, - }, - SerializedMessage { - id: MessageId(1), - role: Role::User, - segments: vec![SerializedMessageSegment::Text { - text: "Here is the tool result".to_string(), - }], - tool_uses: vec![], - tool_results: vec![SerializedToolResult { - tool_use_id: "abc".into(), - is_error: false, - content: LanguageModelToolResultContent::Text("abcdef".into()), - output: Some(serde_json::Value::Null), - }], - context: "".to_string(), - creases: vec![], - is_hidden: false, - }, - ], - version: SerializedThreadV0_1_0::VERSION.to_string(), - initial_project_snapshot: None, - cumulative_token_usage: TokenUsage::default(), - request_token_usage: vec![], - detailed_summary_state: DetailedSummaryState::default(), - model: None, - completion_mode: None, - tool_use_limit_reached: false, - profile: None, - }); - let upgraded = thread_v0_1_0.upgrade(); - - assert_eq!( - upgraded, - SerializedThread { - summary: "Test conversation".into(), - updated_at, - messages: vec![ - SerializedMessage { - id: MessageId(1), - role: Role::User, - segments: vec![SerializedMessageSegment::Text { - text: "Use tool_1".to_string() - }], - tool_uses: vec![], - tool_results: vec![], - context: "".to_string(), - creases: vec![], - is_hidden: false - }, - SerializedMessage { - id: MessageId(2), - role: Role::Assistant, - segments: vec![SerializedMessageSegment::Text { - text: "I want to use a tool".to_string(), - }], - tool_uses: vec![SerializedToolUse { - id: "abc".into(), - name: "tool_1".into(), - input: serde_json::Value::Null, - }], - tool_results: vec![SerializedToolResult { - tool_use_id: "abc".into(), - is_error: false, - content: LanguageModelToolResultContent::Text("abcdef".into()), - output: Some(serde_json::Value::Null), - }], - context: "".to_string(), - creases: vec![], - is_hidden: false, - }, - ], - version: SerializedThread::VERSION.to_string(), - initial_project_snapshot: None, - cumulative_token_usage: TokenUsage::default(), - request_token_usage: vec![], - detailed_summary_state: DetailedSummaryState::default(), - model: None, - completion_mode: None, - tool_use_limit_reached: false, - profile: None - } - ) - } -} diff --git a/crates/agent/src/native_agent_server.rs b/crates/agent/src/native_agent_server.rs deleted file mode 100644 index a9ade8141a..0000000000 --- a/crates/agent/src/native_agent_server.rs +++ /dev/null @@ -1,122 +0,0 @@ -use std::{any::Any, path::Path, rc::Rc, sync::Arc}; - -use agent_servers::{AgentServer, AgentServerDelegate}; -use anyhow::Result; -use fs::Fs; -use gpui::{App, Entity, SharedString, Task}; -use prompt_store::PromptStore; - -use crate::{HistoryStore, NativeAgent, NativeAgentConnection, templates::Templates}; - -#[derive(Clone)] -pub struct NativeAgentServer { - fs: Arc, - history: Entity, -} - -impl NativeAgentServer { - pub fn new(fs: Arc, history: Entity) -> Self { - Self { fs, history } - } -} - -impl AgentServer for NativeAgentServer { - fn name(&self) -> SharedString { - "Zed Agent".into() - } - - fn logo(&self) -> ui::IconName { - ui::IconName::ZedAgent - } - - fn connect( - &self, - _root_dir: Option<&Path>, - delegate: AgentServerDelegate, - cx: &mut App, - ) -> Task< - Result<( - Rc, - Option, - )>, - > { - log::debug!( - "NativeAgentServer::connect called for path: {:?}", - _root_dir - ); - let project = delegate.project().clone(); - let fs = self.fs.clone(); - let history = self.history.clone(); - let prompt_store = PromptStore::global(cx); - cx.spawn(async move |cx| { - log::debug!("Creating templates for native agent"); - let templates = Templates::new(); - let prompt_store = prompt_store.await?; - - log::debug!("Creating native agent entity"); - let agent = - NativeAgent::new(project, history, templates, Some(prompt_store), fs, cx).await?; - - // Create the connection wrapper - let connection = NativeAgentConnection(agent); - log::debug!("NativeAgentServer connection established successfully"); - - Ok(( - Rc::new(connection) as Rc, - None, - )) - }) - } - - fn into_any(self: Rc) -> Rc { - self - } -} - -#[cfg(test)] -mod tests { - use super::*; - - use assistant_text_thread::TextThreadStore; - use gpui::AppContext; - - agent_servers::e2e_tests::common_e2e_tests!( - async |fs, project, cx| { - let auth = cx.update(|cx| { - prompt_store::init(cx); - let registry = language_model::LanguageModelRegistry::read_global(cx); - let auth = registry - .provider(&language_model::ANTHROPIC_PROVIDER_ID) - .unwrap() - .authenticate(cx); - - cx.spawn(async move |_| auth.await) - }); - - auth.await.unwrap(); - - cx.update(|cx| { - let registry = language_model::LanguageModelRegistry::global(cx); - - registry.update(cx, |registry, cx| { - registry.select_default_model( - Some(&language_model::SelectedModel { - provider: language_model::ANTHROPIC_PROVIDER_ID, - model: language_model::LanguageModelId("claude-sonnet-4-latest".into()), - }), - cx, - ); - }); - }); - - let history = cx.update(|cx| { - let text_thread_store = - cx.new(move |cx| TextThreadStore::fake(project.clone(), cx)); - cx.new(move |cx| HistoryStore::new(text_thread_store, cx)) - }); - - NativeAgentServer::new(fs.clone(), history) - }, - allow_option_id = "allow" - ); -} diff --git a/crates/agent/src/outline.rs b/crates/agent/src/outline.rs deleted file mode 100644 index 77af4849ff..0000000000 --- a/crates/agent/src/outline.rs +++ /dev/null @@ -1,218 +0,0 @@ -use anyhow::Result; -use gpui::{AsyncApp, Entity}; -use language::{Buffer, OutlineItem}; -use regex::Regex; -use std::fmt::Write; -use text::Point; - -/// For files over this size, instead of reading them (or including them in context), -/// we automatically provide the file's symbol outline instead, with line numbers. -pub const AUTO_OUTLINE_SIZE: usize = 16384; - -/// Result of getting buffer content, which can be either full content or an outline. -pub struct BufferContent { - /// The actual content (either full text or outline) - pub text: String, - /// Whether this is an outline (true) or full content (false) - pub is_outline: bool, -} - -/// Returns either the full content of a buffer or its outline, depending on size. -/// For files larger than AUTO_OUTLINE_SIZE, returns an outline with a header. -/// For smaller files, returns the full content. -pub async fn get_buffer_content_or_outline( - buffer: Entity, - path: Option<&str>, - cx: &AsyncApp, -) -> Result { - let file_size = buffer.read_with(cx, |buffer, _| buffer.text().len())?; - - if file_size > AUTO_OUTLINE_SIZE { - // For large files, use outline instead of full content - // Wait until the buffer has been fully parsed, so we can read its outline - buffer - .read_with(cx, |buffer, _| buffer.parsing_idle())? - .await; - - let outline_items = buffer.read_with(cx, |buffer, _| { - let snapshot = buffer.snapshot(); - snapshot - .outline(None) - .items - .into_iter() - .map(|item| item.to_point(&snapshot)) - .collect::>() - })?; - - // If no outline exists, fall back to first 1KB so the agent has some context - if outline_items.is_empty() { - let text = buffer.read_with(cx, |buffer, _| { - let snapshot = buffer.snapshot(); - let len = snapshot.len().min(snapshot.as_rope().floor_char_boundary(1024)); - let content = snapshot.text_for_range(0..len).collect::(); - if let Some(path) = path { - format!("# First 1KB of {path} (file too large to show full content, and no outline available)\n\n{content}") - } else { - format!("# First 1KB of file (file too large to show full content, and no outline available)\n\n{content}") - } - })?; - - return Ok(BufferContent { - text, - is_outline: false, - }); - } - - let outline_text = render_outline(outline_items, None, 0, usize::MAX).await?; - - let text = if let Some(path) = path { - format!("# File outline for {path}\n\n{outline_text}",) - } else { - format!("# File outline\n\n{outline_text}",) - }; - Ok(BufferContent { - text, - is_outline: true, - }) - } else { - // File is small enough, return full content - let text = buffer.read_with(cx, |buffer, _| buffer.text())?; - Ok(BufferContent { - text, - is_outline: false, - }) - } -} - -async fn render_outline( - items: impl IntoIterator>, - regex: Option, - offset: usize, - results_per_page: usize, -) -> Result { - let mut items = items.into_iter().skip(offset); - - let entries = items - .by_ref() - .filter(|item| { - regex - .as_ref() - .is_none_or(|regex| regex.is_match(&item.text)) - }) - .take(results_per_page) - .collect::>(); - let has_more = items.next().is_some(); - - let mut output = String::new(); - let entries_rendered = render_entries(&mut output, entries); - - // Calculate pagination information - let page_start = offset + 1; - let page_end = offset + entries_rendered; - let total_symbols = if has_more { - format!("more than {}", page_end) - } else { - page_end.to_string() - }; - - // Add pagination information - if has_more { - writeln!(&mut output, "\nShowing symbols {page_start}-{page_end} (there were more symbols found; use offset: {page_end} to see next page)", - ) - } else { - writeln!( - &mut output, - "\nShowing symbols {page_start}-{page_end} (total symbols: {total_symbols})", - ) - } - .ok(); - - Ok(output) -} - -fn render_entries( - output: &mut String, - items: impl IntoIterator>, -) -> usize { - let mut entries_rendered = 0; - - for item in items { - // Indent based on depth ("" for level 0, " " for level 1, etc.) - for _ in 0..item.depth { - output.push(' '); - } - output.push_str(&item.text); - - // Add position information - convert to 1-based line numbers for display - let start_line = item.range.start.row + 1; - let end_line = item.range.end.row + 1; - - if start_line == end_line { - writeln!(output, " [L{}]", start_line).ok(); - } else { - writeln!(output, " [L{}-{}]", start_line, end_line).ok(); - } - entries_rendered += 1; - } - - entries_rendered -} - -#[cfg(test)] -mod tests { - use super::*; - use fs::FakeFs; - use gpui::TestAppContext; - use project::Project; - use settings::SettingsStore; - - #[gpui::test] - async fn test_large_file_fallback_to_subset(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings = SettingsStore::test(cx); - cx.set_global(settings); - }); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - - let content = "⚡".repeat(100 * 1024); // 100KB - let content_len = content.len(); - let buffer = project - .update(cx, |project, cx| project.create_buffer(true, cx)) - .await - .expect("failed to create buffer"); - - buffer.update(cx, |buffer, cx| buffer.set_text(content, cx)); - - let result = cx - .spawn(|cx| async move { get_buffer_content_or_outline(buffer, None, &cx).await }) - .await - .unwrap(); - - // Should contain some of the actual file content - assert!( - result.text.contains("⚡⚡⚡⚡⚡⚡⚡"), - "Result did not contain content subset" - ); - - // Should be marked as not an outline (it's truncated content) - assert!( - !result.is_outline, - "Large file without outline should not be marked as outline" - ); - - // Should be reasonably sized (much smaller than original) - assert!( - result.text.len() < 50 * 1024, - "Result size {} should be smaller than 50KB", - result.text.len() - ); - - // Should be significantly smaller than the original content - assert!( - result.text.len() < content_len / 10, - "Result should be much smaller than original content" - ); - } -} diff --git a/crates/agent/src/templates.rs b/crates/agent/src/templates.rs deleted file mode 100644 index db787d834e..0000000000 --- a/crates/agent/src/templates.rs +++ /dev/null @@ -1,90 +0,0 @@ -use anyhow::Result; -use gpui::SharedString; -use handlebars::Handlebars; -use rust_embed::RustEmbed; -use serde::Serialize; -use std::sync::Arc; - -#[derive(RustEmbed)] -#[folder = "src/templates"] -#[include = "*.hbs"] -struct Assets; - -pub struct Templates(Handlebars<'static>); - -impl Templates { - pub fn new() -> Arc { - let mut handlebars = Handlebars::new(); - handlebars.set_strict_mode(true); - handlebars.register_helper("contains", Box::new(contains)); - handlebars.register_embed_templates::().unwrap(); - Arc::new(Self(handlebars)) - } -} - -pub trait Template: Sized { - const TEMPLATE_NAME: &'static str; - - fn render(&self, templates: &Templates) -> Result - where - Self: Serialize + Sized, - { - Ok(templates.0.render(Self::TEMPLATE_NAME, self)?) - } -} - -#[derive(Serialize)] -pub struct SystemPromptTemplate<'a> { - #[serde(flatten)] - pub project: &'a prompt_store::ProjectContext, - pub available_tools: Vec, - pub model_name: Option, -} - -impl Template for SystemPromptTemplate<'_> { - const TEMPLATE_NAME: &'static str = "system_prompt.hbs"; -} - -/// Handlebars helper for checking if an item is in a list -fn contains( - h: &handlebars::Helper, - _: &handlebars::Handlebars, - _: &handlebars::Context, - _: &mut handlebars::RenderContext, - out: &mut dyn handlebars::Output, -) -> handlebars::HelperResult { - let list = h - .param(0) - .and_then(|v| v.value().as_array()) - .ok_or_else(|| { - handlebars::RenderError::new("contains: missing or invalid list parameter") - })?; - let query = h.param(1).map(|v| v.value()).ok_or_else(|| { - handlebars::RenderError::new("contains: missing or invalid query parameter") - })?; - - if list.contains(query) { - out.write("true")?; - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_system_prompt_template() { - let project = prompt_store::ProjectContext::default(); - let template = SystemPromptTemplate { - project: &project, - available_tools: vec!["echo".into()], - model_name: Some("test-model".to_string()), - }; - let templates = Templates::new(); - let rendered = template.render(&templates).unwrap(); - assert!(rendered.contains("## Fixing Diagnostics")); - assert!(rendered.contains("test-model")); - } -} diff --git a/crates/agent/src/templates/create_file_prompt.hbs b/crates/agent/src/templates/create_file_prompt.hbs deleted file mode 100644 index 39f83447fa..0000000000 --- a/crates/agent/src/templates/create_file_prompt.hbs +++ /dev/null @@ -1,15 +0,0 @@ -You are an expert engineer and your task is to write a new file from scratch. - -You MUST respond with the file's content wrapped in triple backticks (```). -The backticks should be on their own line. -The text you output will be saved verbatim as the content of the file. -Tool calls have been disabled. -Start your response with ```. - - -{{path}} - - - -{{edit_description}} - diff --git a/crates/agent/src/templates/diff_judge.hbs b/crates/agent/src/templates/diff_judge.hbs deleted file mode 100644 index 0106cb4217..0000000000 --- a/crates/agent/src/templates/diff_judge.hbs +++ /dev/null @@ -1,23 +0,0 @@ -You are an expert coder, and have been tasked with looking at the following diff: - - -{{diff}} - - -Evaluate the following assertions: - - -{{assertions}} - - -You must respond with a short analysis and a score between 0 and 100, where: -- 0 means no assertions pass -- 100 means all the assertions pass perfectly - - -- Assertion 1: one line describing why the first assertion passes or fails (even partially) -- Assertion 2: one line describing why the second assertion passes or fails (even partially) -- ... -- Assertion N: one line describing why the Nth assertion passes or fails (even partially) - -YOUR FINAL SCORE HERE diff --git a/crates/agent/src/templates/edit_file_prompt_diff_fenced.hbs b/crates/agent/src/templates/edit_file_prompt_diff_fenced.hbs deleted file mode 100644 index a7db420a99..0000000000 --- a/crates/agent/src/templates/edit_file_prompt_diff_fenced.hbs +++ /dev/null @@ -1,77 +0,0 @@ -You MUST respond with a series of edits to a file, using the following diff format: - -``` -<<<<<<< SEARCH line=1 -from flask import Flask -======= -import math -from flask import Flask ->>>>>>> REPLACE - -<<<<<<< SEARCH line=325 -return 0 -======= -print("Done") - -return 0 ->>>>>>> REPLACE - -``` - -# File Editing Instructions - -- Use the SEARCH/REPLACE diff format shown above -- The SEARCH section must exactly match existing file content, including indentation -- The SEARCH section must come from the actual file, not an outline -- The SEARCH section cannot be empty -- `line` should be a starting line number for the text to be replaced -- Be minimal with replacements: - - For unique lines, include only those lines - - For non-unique lines, include enough context to identify them -- Do not escape quotes, newlines, or other characters -- For multiple occurrences, repeat the same diff block for each instance -- Edits are sequential - each assumes previous edits are already applied -- Only edit the specified file - -# Example - -``` -<<<<<<< SEARCH line=3 -struct User { - name: String, - email: String, -} -======= -struct User { - name: String, - email: String, - active: bool, -} ->>>>>>> REPLACE - -<<<<<<< SEARCH line=25 - let user = User { - name: String::from("John"), - email: String::from("john@example.com"), - }; -======= - let user = User { - name: String::from("John"), - email: String::from("john@example.com"), - active: true, - }; ->>>>>>> REPLACE -``` - - -# Final instructions - -Tool calls have been disabled. You MUST respond using the SEARCH/REPLACE diff format only. - - -{{path}} - - - -{{edit_description}} - diff --git a/crates/agent/src/templates/edit_file_prompt_xml.hbs b/crates/agent/src/templates/edit_file_prompt_xml.hbs deleted file mode 100644 index db17c527aa..0000000000 --- a/crates/agent/src/templates/edit_file_prompt_xml.hbs +++ /dev/null @@ -1,92 +0,0 @@ -You MUST respond with a series of edits to a file, using the following format: - -``` - - - -OLD TEXT 1 HERE - - -NEW TEXT 1 HERE - - - -OLD TEXT 2 HERE - - -NEW TEXT 2 HERE - - - -OLD TEXT 3 HERE - - -NEW TEXT 3 HERE - - - -``` - -# File Editing Instructions - -- Use `` and `` tags to replace content -- `` must exactly match existing file content, including indentation -- `` must come from the actual file, not an outline -- `` cannot be empty -- `line` should be a starting line number for the text to be replaced -- Be minimal with replacements: - - For unique lines, include only those lines - - For non-unique lines, include enough context to identify them -- Do not escape quotes, newlines, or other characters within tags -- For multiple occurrences, repeat the same tag pair for each instance -- Edits are sequential - each assumes previous edits are already applied -- Only edit the specified file -- Always close all tags properly - - -{{!-- The following example adds almost 10% pass rate for Gemini 2.5. -Claude and gpt-4.1 don't really need it. --}} - - - - -struct User { - name: String, - email: String, -} - - -struct User { - name: String, - email: String, - active: bool, -} - - - - let user = User { - name: String::from("John"), - email: String::from("john@example.com"), - }; - - - let user = User { - name: String::from("John"), - email: String::from("john@example.com"), - active: true, - }; - - - - - - - -{{path}} - - - -{{edit_description}} - - -Tool calls have been disabled. You MUST start your response with . diff --git a/crates/agent/src/templates/system_prompt.hbs b/crates/agent/src/templates/system_prompt.hbs deleted file mode 100644 index 4620647135..0000000000 --- a/crates/agent/src/templates/system_prompt.hbs +++ /dev/null @@ -1,188 +0,0 @@ -You are a highly skilled software engineer with extensive knowledge in many programming languages, frameworks, design patterns, and best practices. - -## Communication - -1. Be conversational but professional. -2. Refer to the user in the second person and yourself in the first person. -3. Format your responses in markdown. Use backticks to format file, directory, function, and class names. -4. NEVER lie or make things up. -5. Refrain from apologizing all the time when results are unexpected. Instead, just try your best to proceed or explain the circumstances to the user without apologizing. - -{{#if (gt (len available_tools) 0)}} -## Tool Use - -1. Make sure to adhere to the tools schema. -2. Provide every required argument. -3. DO NOT use tools to access items that are already available in the context section. -4. Use only the tools that are currently available. -5. DO NOT use a tool that is not available just because it appears in the conversation. This means the user turned it off. -6. NEVER run commands that don't terminate on their own such as web servers (like `npm run start`, `npm run dev`, `python -m http.server`, etc) or file watchers. -7. Avoid HTML entity escaping - use plain characters instead. - -## Searching and Reading - -If you are unsure how to fulfill the user's request, gather more information with tool calls and/or clarifying questions. - -If appropriate, use tool calls to explore the current project, which contains the following root directories: - -{{#each worktrees}} -- `{{abs_path}}` -{{/each}} - -- Bias towards not asking the user for help if you can find the answer yourself. -- When providing paths to tools, the path should always start with the name of a project root directory listed above. -- Before you read or edit a file, you must first find the full path. DO NOT ever guess a file path! -{{# if (contains available_tools 'grep') }} -- When looking for symbols in the project, prefer the `grep` tool. -- As you learn about the structure of the project, use that information to scope `grep` searches to targeted subtrees of the project. -- The user might specify a partial file path. If you don't know the full path, use `find_path` (not `grep`) before you read the file. -{{/if}} -{{else}} -You are being tasked with providing a response, but you have no ability to use tools or to read or write any aspect of the user's system (other than any context the user might have provided to you). - -As such, if you need the user to perform any actions for you, you must request them explicitly. Bias towards giving a response to the best of your ability, and then making requests for the user to take action (e.g. to give you more context) only optionally. - -The one exception to this is if the user references something you don't know about - for example, the name of a source code file, function, type, or other piece of code that you have no awareness of. In this case, you MUST NOT MAKE SOMETHING UP, or assume you know what that thing is or how it works. Instead, you must ask the user for clarification rather than giving a response. -{{/if}} - -## Code Block Formatting - -Whenever you mention a code block, you MUST use ONLY use the following format: - -```path/to/Something.blah#L123-456 -(code goes here) -``` - -The `#L123-456` means the line number range 123 through 456, and the path/to/Something.blah is a path in the project. (If there is no valid path in the project, then you can use /dev/null/path.extension for its path.) This is the ONLY valid way to format code blocks, because the Markdown parser does not understand the more common ```language syntax, or bare ``` blocks. It only understands this path-based syntax, and if the path is missing, then it will error and you will have to do it over again. -Just to be really clear about this, if you ever find yourself writing three backticks followed by a language name, STOP! -You have made a mistake. You can only ever put paths after triple backticks! - - -Based on all the information I've gathered, here's a summary of how this system works: -1. The README file is loaded into the system. -2. The system finds the first two headers, including everything in between. In this case, that would be: -```path/to/README.md#L8-12 -# First Header -This is the info under the first header. -## Sub-header -``` -3. Then the system finds the last header in the README: -```path/to/README.md#L27-29 -## Last Header -This is the last header in the README. -``` -4. Finally, it passes this information on to the next process. - - - -In Markdown, hash marks signify headings. For example: -```/dev/null/example.md#L1-3 -# Level 1 heading -## Level 2 heading -### Level 3 heading -``` - - -Here are examples of ways you must never render code blocks: - -In Markdown, hash marks signify headings. For example: -``` -# Level 1 heading -## Level 2 heading -### Level 3 heading -``` - - -This example is unacceptable because it does not include the path. - - -In Markdown, hash marks signify headings. For example: -```markdown -# Level 1 heading -## Level 2 heading -### Level 3 heading -``` - -This example is unacceptable because it has the language instead of the path. - - -In Markdown, hash marks signify headings. For example: - # Level 1 heading - ## Level 2 heading - ### Level 3 heading - -This example is unacceptable because it uses indentation to mark the code block instead of backticks with a path. - - -In Markdown, hash marks signify headings. For example: -```markdown -/dev/null/example.md#L1-3 -# Level 1 heading -## Level 2 heading -### Level 3 heading -``` - -This example is unacceptable because the path is in the wrong place. The path must be directly after the opening backticks. - -{{#if (gt (len available_tools) 0)}} -## Fixing Diagnostics - -1. Make 1-2 attempts at fixing diagnostics, then defer to the user. -2. Never simplify code you've written just to solve diagnostics. Complete, mostly correct code is more valuable than perfect code that doesn't solve the problem. - -## Debugging - -When debugging, only make code changes if you are certain that you can solve the problem. -Otherwise, follow debugging best practices: -1. Address the root cause instead of the symptoms. -2. Add descriptive logging statements and error messages to track variable and code state. -3. Add test functions and statements to isolate the problem. - -{{/if}} -## Calling External APIs - -1. Unless explicitly requested by the user, use the best suited external APIs and packages to solve the task. There is no need to ask the user for permission. -2. When selecting which version of an API or package to use, choose one that is compatible with the user's dependency management file(s). If no such file exists or if the package is not present, use the latest version that is in your training data. -3. If an external API requires an API Key, be sure to point this out to the user. Adhere to best security practices (e.g. DO NOT hardcode an API key in a place where it can be exposed) - -## System Information - -Operating System: {{os}} -Default Shell: {{shell}} - -{{#if model_name}} -## Model Information - -You are powered by the model named {{model_name}}. - -{{/if}} -{{#if (or has_rules has_user_rules)}} -## User's Custom Instructions - -The following additional instructions are provided by the user, and should be followed to the best of your ability{{#if (gt (len available_tools) 0)}} without interfering with the tool use guidelines{{/if}}. - -{{#if has_rules}} -There are project rules that apply to these root directories: -{{#each worktrees}} -{{#if rules_file}} -`{{root_name}}/{{rules_file.path_in_worktree}}`: -`````` -{{{rules_file.text}}} -`````` -{{/if}} -{{/each}} -{{/if}} - -{{#if has_user_rules}} -The user has specified the following rules that should be applied: -{{#each user_rules}} - -{{#if title}} -Rules title: {{title}} -{{/if}} -`````` -{{contents}}} -`````` -{{/each}} -{{/if}} -{{/if}} diff --git a/crates/agent/src/tests/mod.rs b/crates/agent/src/tests/mod.rs deleted file mode 100644 index 9ff8703532..0000000000 --- a/crates/agent/src/tests/mod.rs +++ /dev/null @@ -1,2598 +0,0 @@ -use super::*; -use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelList, UserMessageId}; -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 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, -}; -use pretty_assertions::assert_eq; -use project::{ - Project, context_server_store::ContextServerStore, project_settings::ProjectSettings, -}; -use prompt_store::ProjectContext; -use reqwest_client::ReqwestClient; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use serde_json::json; -use settings::{Settings, SettingsStore}; -use std::{path::Path, rc::Rc, sync::Arc, time::Duration}; -use util::path; - -mod test_tools; -use test_tools::*; - -#[gpui::test] -async fn test_echo(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.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; - thread.update(cx, |thread, _cx| { - assert_eq!( - thread.last_message().unwrap().to_markdown(), - indoc! {" - ## Assistant - - Hello - "} - ) - }); - assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]); -} - -#[gpui::test] -async fn test_thinking(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.send( - UserMessageId::new(), - [indoc! {" - Testing: - - Generate a thinking step where you just think the word 'Think', - and have your final answer be 'Hello' - "}], - 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; - thread.update(cx, |thread, _cx| { - assert_eq!( - thread.last_message().unwrap().to_markdown(), - indoc! {" - ## Assistant - - Think - Hello - "} - ) - }); - assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]); -} - -#[gpui::test] -async fn test_system_prompt(cx: &mut TestAppContext) { - let ThreadTest { - model, - thread, - project_context, - .. - } = setup(cx, TestModel::Fake).await; - let fake_model = model.as_fake(); - - project_context.update(cx, |project_context, _cx| { - project_context.shell = "test-shell".into() - }); - thread.update(cx, |thread, _| thread.add_tool(EchoTool)); - thread - .update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["abc"], cx) - }) - .unwrap(); - cx.run_until_parked(); - let mut pending_completions = fake_model.pending_completions(); - assert_eq!( - pending_completions.len(), - 1, - "unexpected pending completions: {:?}", - pending_completions - ); - - let pending_completion = pending_completions.pop().unwrap(); - assert_eq!(pending_completion.messages[0].role, Role::System); - - let system_message = &pending_completion.messages[0]; - let system_prompt = system_message.content[0].to_str().unwrap(); - assert!( - system_prompt.contains("test-shell"), - "unexpected system message: {:?}", - system_message - ); - assert!( - system_prompt.contains("## Fixing Diagnostics"), - "unexpected system message: {:?}", - system_message - ); -} - -#[gpui::test] -async fn test_system_prompt_without_tools(cx: &mut TestAppContext) { - let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; - let fake_model = model.as_fake(); - - thread - .update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["abc"], cx) - }) - .unwrap(); - cx.run_until_parked(); - let mut pending_completions = fake_model.pending_completions(); - assert_eq!( - pending_completions.len(), - 1, - "unexpected pending completions: {:?}", - pending_completions - ); - - let pending_completion = pending_completions.pop().unwrap(); - assert_eq!(pending_completion.messages[0].role, Role::System); - - let system_message = &pending_completion.messages[0]; - let system_prompt = system_message.content[0].to_str().unwrap(); - assert!( - !system_prompt.contains("## Tool Use"), - "unexpected system message: {:?}", - system_message - ); - assert!( - !system_prompt.contains("## Fixing Diagnostics"), - "unexpected system message: {:?}", - system_message - ); -} - -#[gpui::test] -async fn test_prompt_caching(cx: &mut TestAppContext) { - let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; - let fake_model = model.as_fake(); - - // Send initial user message and verify it's cached - thread - .update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Message 1"], cx) - }) - .unwrap(); - cx.run_until_parked(); - - let completion = fake_model.pending_completions().pop().unwrap(); - assert_eq!( - completion.messages[1..], - vec![LanguageModelRequestMessage { - role: Role::User, - content: vec!["Message 1".into()], - cache: true, - reasoning_details: None, - }] - ); - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Text( - "Response to Message 1".into(), - )); - fake_model.end_last_completion_stream(); - cx.run_until_parked(); - - // Send another user message and verify only the latest is cached - thread - .update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Message 2"], cx) - }) - .unwrap(); - cx.run_until_parked(); - - let completion = fake_model.pending_completions().pop().unwrap(); - assert_eq!( - completion.messages[1..], - vec![ - LanguageModelRequestMessage { - role: Role::User, - content: vec!["Message 1".into()], - cache: false, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::Assistant, - content: vec!["Response to Message 1".into()], - cache: false, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::User, - content: vec!["Message 2".into()], - cache: true, - reasoning_details: None, - } - ] - ); - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Text( - "Response to Message 2".into(), - )); - fake_model.end_last_completion_stream(); - cx.run_until_parked(); - - // Simulate a tool call and verify that the latest tool result is cached - thread.update(cx, |thread, _| thread.add_tool(EchoTool)); - thread - .update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Use the echo tool"], cx) - }) - .unwrap(); - cx.run_until_parked(); - - let tool_use = LanguageModelToolUse { - id: "tool_1".into(), - name: EchoTool::name().into(), - raw_input: json!({"text": "test"}).to_string(), - input: json!({"text": "test"}), - is_input_complete: true, - thought_signature: None, - }; - fake_model - .send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone())); - fake_model.end_last_completion_stream(); - cx.run_until_parked(); - - let completion = fake_model.pending_completions().pop().unwrap(); - let tool_result = LanguageModelToolResult { - tool_use_id: "tool_1".into(), - tool_name: EchoTool::name().into(), - is_error: false, - content: "test".into(), - output: Some("test".into()), - }; - assert_eq!( - completion.messages[1..], - vec![ - LanguageModelRequestMessage { - role: Role::User, - content: vec!["Message 1".into()], - cache: false, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::Assistant, - content: vec!["Response to Message 1".into()], - cache: false, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::User, - content: vec!["Message 2".into()], - cache: false, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::Assistant, - content: vec!["Response to Message 2".into()], - cache: false, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::User, - content: vec!["Use the echo tool".into()], - cache: false, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::Assistant, - content: vec![MessageContent::ToolUse(tool_use)], - cache: false, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::User, - content: vec![MessageContent::ToolResult(tool_result)], - cache: true, - reasoning_details: None, - } - ] - ); -} - -#[gpui::test] -#[cfg_attr(not(feature = "e2e"), ignore)] -async fn test_basic_tool_calls(cx: &mut TestAppContext) { - let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; - - // Test a tool call that's likely to complete *before* streaming stops. - let events = thread - .update(cx, |thread, cx| { - thread.add_tool(EchoTool); - thread.send( - UserMessageId::new(), - ["Now test the echo tool with 'Hello'. Does it work? Say 'Yes' or 'No'."], - cx, - ) - }) - .unwrap() - .collect() - .await; - assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]); - - // 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.add_tool(DelayTool); - thread.send( - UserMessageId::new(), - [ - "Now call the delay tool with 200ms.", - "When the timer goes off, then you echo the output of the tool.", - ], - cx, - ) - }) - .unwrap() - .collect() - .await; - assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]); - thread.update(cx, |thread, _cx| { - assert!( - thread - .last_message() - .unwrap() - .as_agent_message() - .unwrap() - .content - .iter() - .any(|content| { - if let AgentMessageContent::Text(text) = content { - text.contains("Ding") - } else { - false - } - }), - "{}", - thread.to_markdown() - ); - }); -} - -#[gpui::test] -#[cfg_attr(not(feature = "e2e"), ignore)] -async fn test_streaming_tool_calls(cx: &mut TestAppContext) { - let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; - - // Test a tool call that's likely to complete *before* streaming stops. - let mut events = thread - .update(cx, |thread, cx| { - thread.add_tool(WordListTool); - thread.send(UserMessageId::new(), ["Test the word_list tool."], cx) - }) - .unwrap(); - - let mut saw_partial_tool_use = false; - while let Some(event) = events.next().await { - if let Ok(ThreadEvent::ToolCall(tool_call)) = event { - thread.update(cx, |thread, _cx| { - // Look for a tool use in the thread's last message - let message = thread.last_message().unwrap(); - let agent_message = message.as_agent_message().unwrap(); - let last_content = agent_message.content.last().unwrap(); - if let AgentMessageContent::ToolUse(last_tool_use) = last_content { - assert_eq!(last_tool_use.name.as_ref(), "word_list"); - if tool_call.status == acp::ToolCallStatus::Pending { - if !last_tool_use.is_input_complete - && last_tool_use.input.get("g").is_none() - { - saw_partial_tool_use = true; - } - } else { - last_tool_use - .input - .get("a") - .expect("'a' has streamed because input is now complete"); - last_tool_use - .input - .get("g") - .expect("'g' has streamed because input is now complete"); - } - } else { - panic!("last content should be a tool use"); - } - }); - } - } - - assert!( - saw_partial_tool_use, - "should see at least one partially streamed tool use in the history" - ); -} - -#[gpui::test] -async fn test_tool_authorization(cx: &mut TestAppContext) { - let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; - let fake_model = model.as_fake(); - - let mut events = thread - .update(cx, |thread, cx| { - thread.add_tool(ToolRequiringPermission); - thread.send(UserMessageId::new(), ["abc"], cx) - }) - .unwrap(); - cx.run_until_parked(); - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( - LanguageModelToolUse { - id: "tool_id_1".into(), - name: ToolRequiringPermission::name().into(), - raw_input: "{}".into(), - input: json!({}), - is_input_complete: true, - thought_signature: None, - }, - )); - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( - LanguageModelToolUse { - id: "tool_id_2".into(), - name: ToolRequiringPermission::name().into(), - raw_input: "{}".into(), - input: json!({}), - is_input_complete: true, - thought_signature: None, - }, - )); - fake_model.end_last_completion_stream(); - let tool_call_auth_1 = next_tool_call_authorization(&mut events).await; - let tool_call_auth_2 = next_tool_call_authorization(&mut events).await; - - // Approve the first - tool_call_auth_1 - .response - .send(tool_call_auth_1.options[1].option_id.clone()) - .unwrap(); - cx.run_until_parked(); - - // Reject the second - tool_call_auth_2 - .response - .send(tool_call_auth_1.options[2].option_id.clone()) - .unwrap(); - cx.run_until_parked(); - - let completion = fake_model.pending_completions().pop().unwrap(); - let message = completion.messages.last().unwrap(); - assert_eq!( - message.content, - vec![ - language_model::MessageContent::ToolResult(LanguageModelToolResult { - tool_use_id: tool_call_auth_1.tool_call.tool_call_id.0.to_string().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.tool_call_id.0.to_string().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()) - }) - ] - ); - - // Simulate yet another tool call. - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( - LanguageModelToolUse { - id: "tool_id_3".into(), - name: ToolRequiringPermission::name().into(), - raw_input: "{}".into(), - input: json!({}), - is_input_complete: true, - thought_signature: None, - }, - )); - fake_model.end_last_completion_stream(); - - // Respond by always allowing tools. - let tool_call_auth_3 = next_tool_call_authorization(&mut events).await; - tool_call_auth_3 - .response - .send(tool_call_auth_3.options[0].option_id.clone()) - .unwrap(); - cx.run_until_parked(); - let completion = fake_model.pending_completions().pop().unwrap(); - let message = completion.messages.last().unwrap(); - assert_eq!( - message.content, - vec![language_model::MessageContent::ToolResult( - LanguageModelToolResult { - tool_use_id: tool_call_auth_3.tool_call.tool_call_id.0.to_string().into(), - tool_name: ToolRequiringPermission::name().into(), - is_error: false, - content: "Allowed".into(), - output: Some("Allowed".into()) - } - )] - ); - - // Simulate a final tool call, ensuring we don't trigger authorization. - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( - LanguageModelToolUse { - id: "tool_id_4".into(), - name: ToolRequiringPermission::name().into(), - raw_input: "{}".into(), - input: json!({}), - is_input_complete: true, - thought_signature: None, - }, - )); - fake_model.end_last_completion_stream(); - cx.run_until_parked(); - let completion = fake_model.pending_completions().pop().unwrap(); - let message = completion.messages.last().unwrap(); - assert_eq!( - message.content, - vec![language_model::MessageContent::ToolResult( - LanguageModelToolResult { - tool_use_id: "tool_id_4".into(), - tool_name: ToolRequiringPermission::name().into(), - is_error: false, - content: "Allowed".into(), - output: Some("Allowed".into()) - } - )] - ); -} - -#[gpui::test] -async fn test_tool_hallucination(cx: &mut TestAppContext) { - let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; - let fake_model = model.as_fake(); - - let mut events = thread - .update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["abc"], cx) - }) - .unwrap(); - cx.run_until_parked(); - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( - LanguageModelToolUse { - id: "tool_id_1".into(), - name: "nonexistent_tool".into(), - raw_input: "{}".into(), - input: json!({}), - is_input_complete: true, - thought_signature: None, - }, - )); - fake_model.end_last_completion_stream(); - - let tool_call = expect_tool_call(&mut events).await; - assert_eq!(tool_call.title, "nonexistent_tool"); - assert_eq!(tool_call.status, acp::ToolCallStatus::Pending); - let update = expect_tool_call_update_fields(&mut events).await; - assert_eq!(update.fields.status, Some(acp::ToolCallStatus::Failed)); -} - -#[gpui::test] -async fn test_resume_after_tool_use_limit(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(EchoTool); - thread.send(UserMessageId::new(), ["abc"], cx) - }) - .unwrap(); - cx.run_until_parked(); - let tool_use = LanguageModelToolUse { - id: "tool_id_1".into(), - name: EchoTool::name().into(), - raw_input: "{}".into(), - input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(), - is_input_complete: true, - thought_signature: None, - }; - fake_model - .send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone())); - fake_model.end_last_completion_stream(); - - cx.run_until_parked(); - let completion = fake_model.pending_completions().pop().unwrap(); - let tool_result = LanguageModelToolResult { - tool_use_id: "tool_id_1".into(), - tool_name: EchoTool::name().into(), - is_error: false, - content: "def".into(), - output: Some("def".into()), - }; - assert_eq!( - completion.messages[1..], - vec![ - LanguageModelRequestMessage { - role: Role::User, - content: vec!["abc".into()], - cache: false, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::Assistant, - content: vec![MessageContent::ToolUse(tool_use.clone())], - cache: false, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::User, - content: vec![MessageContent::ToolResult(tool_result.clone())], - cache: true, - reasoning_details: None, - }, - ] - ); - - // Simulate reaching tool use limit. - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUseLimitReached); - fake_model.end_last_completion_stream(); - let last_event = events.collect::>().await.pop().unwrap(); - assert!( - last_event - .unwrap_err() - .is::() - ); - - let events = thread.update(cx, |thread, cx| thread.resume(cx)).unwrap(); - cx.run_until_parked(); - let completion = fake_model.pending_completions().pop().unwrap(); - assert_eq!( - completion.messages[1..], - vec![ - LanguageModelRequestMessage { - role: Role::User, - content: vec!["abc".into()], - cache: false, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::Assistant, - content: vec![MessageContent::ToolUse(tool_use)], - cache: false, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::User, - content: vec![MessageContent::ToolResult(tool_result)], - cache: false, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::User, - content: vec!["Continue where you left off".into()], - cache: true, - reasoning_details: None, - } - ] - ); - - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::Text("Done".into())); - fake_model.end_last_completion_stream(); - events.collect::>().await; - thread.read_with(cx, |thread, _cx| { - assert_eq!( - thread.last_message().unwrap().to_markdown(), - indoc! {" - ## Assistant - - Done - "} - ) - }); -} - -#[gpui::test] -async fn test_send_after_tool_use_limit(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(EchoTool); - thread.send(UserMessageId::new(), ["abc"], cx) - }) - .unwrap(); - cx.run_until_parked(); - - let tool_use = LanguageModelToolUse { - id: "tool_id_1".into(), - name: EchoTool::name().into(), - raw_input: "{}".into(), - input: serde_json::to_value(&EchoToolInput { text: "def".into() }).unwrap(), - is_input_complete: true, - thought_signature: None, - }; - let tool_result = LanguageModelToolResult { - tool_use_id: "tool_id_1".into(), - tool_name: EchoTool::name().into(), - is_error: false, - content: "def".into(), - output: Some("def".into()), - }; - fake_model - .send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse(tool_use.clone())); - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUseLimitReached); - fake_model.end_last_completion_stream(); - let last_event = events.collect::>().await.pop().unwrap(); - assert!( - last_event - .unwrap_err() - .is::() - ); - - thread - .update(cx, |thread, cx| { - thread.send(UserMessageId::new(), vec!["ghi"], cx) - }) - .unwrap(); - cx.run_until_parked(); - let completion = fake_model.pending_completions().pop().unwrap(); - assert_eq!( - completion.messages[1..], - vec![ - LanguageModelRequestMessage { - role: Role::User, - content: vec!["abc".into()], - cache: false, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::Assistant, - content: vec![MessageContent::ToolUse(tool_use)], - cache: false, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::User, - content: vec![MessageContent::ToolResult(tool_result)], - cache: false, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::User, - content: vec!["ghi".into()], - cache: true, - reasoning_details: None, - } - ] - ); -} - -async fn expect_tool_call(events: &mut UnboundedReceiver>) -> acp::ToolCall { - let event = events - .next() - .await - .expect("no tool call authorization event received") - .unwrap(); - match event { - ThreadEvent::ToolCall(tool_call) => tool_call, - event => { - panic!("Unexpected event {event:?}"); - } - } -} - -async fn expect_tool_call_update_fields( - events: &mut UnboundedReceiver>, -) -> acp::ToolCallUpdate { - let event = events - .next() - .await - .expect("no tool call authorization event received") - .unwrap(); - match event { - ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(update)) => update, - event => { - panic!("Unexpected event {event:?}"); - } - } -} - -async fn next_tool_call_authorization( - events: &mut UnboundedReceiver>, -) -> ToolCallAuthorization { - loop { - let event = events - .next() - .await - .expect("no tool call authorization event received") - .unwrap(); - if let ThreadEvent::ToolCallAuthorization(tool_call_authorization) = event { - let permission_kinds = tool_call_authorization - .options - .iter() - .map(|o| o.kind) - .collect::>(); - assert_eq!( - permission_kinds, - vec![ - acp::PermissionOptionKind::AllowAlways, - acp::PermissionOptionKind::AllowOnce, - acp::PermissionOptionKind::RejectOnce, - ] - ); - return tool_call_authorization; - } - } -} - -#[gpui::test] -#[cfg_attr(not(feature = "e2e"), ignore)] -async fn test_concurrent_tool_calls(cx: &mut TestAppContext) { - let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; - - // Test concurrent tool calls with different delay times - let events = thread - .update(cx, |thread, cx| { - thread.add_tool(DelayTool); - thread.send( - UserMessageId::new(), - [ - "Call the delay tool twice in the same message.", - "Once with 100ms. Once with 300ms.", - "When both timers are complete, describe the outputs.", - ], - cx, - ) - }) - .unwrap() - .collect() - .await; - - let stop_reasons = stop_events(events); - assert_eq!(stop_reasons, vec![acp::StopReason::EndTurn]); - - thread.update(cx, |thread, _cx| { - let last_message = thread.last_message().unwrap(); - let agent_message = last_message.as_agent_message().unwrap(); - let text = agent_message - .content - .iter() - .filter_map(|content| { - if let AgentMessageContent::Text(text) = content { - Some(text.as_str()) - } else { - None - } - }) - .collect::(); - - assert!(text.contains("Ding")); - }); -} - -#[gpui::test] -async fn test_profiles(cx: &mut TestAppContext) { - let ThreadTest { - model, thread, fs, .. - } = setup(cx, TestModel::Fake).await; - let fake_model = model.as_fake(); - - thread.update(cx, |thread, _cx| { - thread.add_tool(DelayTool); - thread.add_tool(EchoTool); - thread.add_tool(InfiniteTool); - }); - - // Override profiles and wait for settings to be loaded. - fs.insert_file( - paths::settings_file(), - json!({ - "agent": { - "profiles": { - "test-1": { - "name": "Test Profile 1", - "tools": { - EchoTool::name(): true, - DelayTool::name(): true, - } - }, - "test-2": { - "name": "Test Profile 2", - "tools": { - InfiniteTool::name(): true, - } - } - } - } - }) - .to_string() - .into_bytes(), - ) - .await; - cx.run_until_parked(); - - // Test that test-1 profile (default) has echo and delay tools - thread - .update(cx, |thread, cx| { - thread.set_profile(AgentProfileId("test-1".into()), cx); - thread.send(UserMessageId::new(), ["test"], cx) - }) - .unwrap(); - cx.run_until_parked(); - - let mut pending_completions = fake_model.pending_completions(); - assert_eq!(pending_completions.len(), 1); - let completion = pending_completions.pop().unwrap(); - let tool_names: Vec = completion - .tools - .iter() - .map(|tool| tool.name.clone()) - .collect(); - 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. - thread - .update(cx, |thread, cx| { - thread.set_profile(AgentProfileId("test-2".into()), cx); - thread.send(UserMessageId::new(), ["test2"], cx) - }) - .unwrap(); - cx.run_until_parked(); - let mut pending_completions = fake_model.pending_completions(); - assert_eq!(pending_completions.len(), 1); - let completion = pending_completions.pop().unwrap(); - let tool_names: Vec = completion - .tools - .iter() - .map(|tool| tool.name.clone()) - .collect(); - 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": { - "always_allow_tool_actions": true, - "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, cx| { - thread.set_profile(AgentProfileId("test".into()), cx) - }); - - 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, - thought_signature: None, - }, - )); - 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, - thought_signature: None, - }, - )); - 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, - thought_signature: None, - }, - )); - 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, cx| { - thread.set_profile(AgentProfileId("test".into()), cx); - thread.add_tool(EchoTool); - thread.add_tool(DelayTool); - thread.add_tool(WordListTool); - thread.add_tool(ToolRequiringPermission); - thread.add_tool(InfiniteTool); - }); - - // Set up multiple context servers with some overlapping tool names - let _server1_calls = setup_context_server( - "xxx", - vec![ - context_server::types::Tool { - name: "echo".into(), // Conflicts with native EchoTool - description: None, - input_schema: serde_json::to_value(EchoTool::input_schema( - LanguageModelToolSchemaFormat::JsonSchema, - )) - .unwrap(), - output_schema: None, - annotations: None, - }, - context_server::types::Tool { - name: "unique_tool_1".into(), - description: None, - input_schema: json!({"type": "object", "properties": {}}), - output_schema: None, - annotations: None, - }, - ], - &context_server_store, - cx, - ); - - let _server2_calls = setup_context_server( - "yyy", - vec![ - context_server::types::Tool { - name: "echo".into(), // Also conflicts with native EchoTool - description: None, - input_schema: serde_json::to_value(EchoTool::input_schema( - LanguageModelToolSchemaFormat::JsonSchema, - )) - .unwrap(), - output_schema: None, - annotations: None, - }, - context_server::types::Tool { - name: "unique_tool_2".into(), - description: None, - input_schema: json!({"type": "object", "properties": {}}), - output_schema: None, - annotations: None, - }, - context_server::types::Tool { - name: "a".repeat(MAX_TOOL_NAME_LENGTH - 2), - description: None, - input_schema: json!({"type": "object", "properties": {}}), - output_schema: None, - annotations: None, - }, - context_server::types::Tool { - name: "b".repeat(MAX_TOOL_NAME_LENGTH - 1), - description: None, - input_schema: json!({"type": "object", "properties": {}}), - output_schema: None, - annotations: None, - }, - ], - &context_server_store, - cx, - ); - let _server3_calls = setup_context_server( - "zzz", - vec![ - context_server::types::Tool { - name: "a".repeat(MAX_TOOL_NAME_LENGTH - 2), - description: None, - input_schema: json!({"type": "object", "properties": {}}), - output_schema: None, - annotations: None, - }, - context_server::types::Tool { - name: "b".repeat(MAX_TOOL_NAME_LENGTH - 1), - description: None, - input_schema: json!({"type": "object", "properties": {}}), - output_schema: None, - annotations: None, - }, - context_server::types::Tool { - name: "c".repeat(MAX_TOOL_NAME_LENGTH + 1), - description: None, - input_schema: json!({"type": "object", "properties": {}}), - output_schema: None, - annotations: None, - }, - ], - &context_server_store, - cx, - ); - - thread - .update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Go"], cx) - }) - .unwrap(); - cx.run_until_parked(); - let completion = fake_model.pending_completions().pop().unwrap(); - assert_eq!( - tool_names_for_completion(&completion), - vec![ - "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", - "delay", - "echo", - "infinite", - "tool_requiring_permission", - "unique_tool_1", - "unique_tool_2", - "word_list", - "xxx_echo", - "y_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - "yyy_echo", - "z_aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - ] - ); -} - -#[gpui::test] -#[cfg_attr(not(feature = "e2e"), ignore)] -async fn test_cancellation(cx: &mut TestAppContext) { - let ThreadTest { thread, .. } = setup(cx, TestModel::Sonnet4).await; - - let mut events = thread - .update(cx, |thread, cx| { - thread.add_tool(InfiniteTool); - thread.add_tool(EchoTool); - thread.send( - UserMessageId::new(), - ["Call the echo tool, then call the infinite tool, then explain their output"], - cx, - ) - }) - .unwrap(); - - // Wait until both tools are called. - let mut expected_tools = vec!["Echo", "Infinite Tool"]; - let mut echo_id = None; - let mut echo_completed = false; - while let Some(event) = events.next().await { - match event.unwrap() { - ThreadEvent::ToolCall(tool_call) => { - assert_eq!(tool_call.title, expected_tools.remove(0)); - if tool_call.title == "Echo" { - echo_id = Some(tool_call.tool_call_id); - } - } - ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields( - acp::ToolCallUpdate { - tool_call_id, - fields: - acp::ToolCallUpdateFields { - status: Some(acp::ToolCallStatus::Completed), - .. - }, - .. - }, - )) if Some(&tool_call_id) == echo_id.as_ref() => { - echo_completed = true; - } - _ => {} - } - - if expected_tools.is_empty() && echo_completed { - break; - } - } - - // Cancel the current send and ensure that the event stream is closed, even - // if one of the tools is still running. - thread.update(cx, |thread, cx| thread.cancel(cx)); - let events = events.collect::>().await; - let last_event = events.last(); - assert!( - matches!( - last_event, - Some(Ok(ThreadEvent::Stop(acp::StopReason::Cancelled))) - ), - "unexpected event {last_event:?}" - ); - - // Ensure we can still send a new message after cancellation. - let events = thread - .update(cx, |thread, cx| { - thread.send( - UserMessageId::new(), - ["Testing: reply with 'Hello' then stop."], - cx, - ) - }) - .unwrap() - .collect::>() - .await; - thread.update(cx, |thread, _cx| { - let message = thread.last_message().unwrap(); - let agent_message = message.as_agent_message().unwrap(); - assert_eq!( - agent_message.content, - vec![AgentMessageContent::Text("Hello".to_string())] - ); - }); - assert_eq!(stop_events(events), vec![acp::StopReason::EndTurn]); -} - -#[gpui::test] -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(); - - let events_1 = thread - .update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Hello 1"], cx) - }) - .unwrap(); - cx.run_until_parked(); - fake_model.send_last_completion_stream_text_chunk("Hey 1!"); - cx.run_until_parked(); - - let events_2 = thread - .update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Hello 2"], cx) - }) - .unwrap(); - cx.run_until_parked(); - fake_model.send_last_completion_stream_text_chunk("Hey 2!"); - fake_model - .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)); - fake_model.end_last_completion_stream(); - - let events_1 = events_1.collect::>().await; - assert_eq!(stop_events(events_1), vec![acp::StopReason::Cancelled]); - let events_2 = events_2.collect::>().await; - assert_eq!(stop_events(events_2), vec![acp::StopReason::EndTurn]); -} - -#[gpui::test] -async fn test_subsequent_successful_sends_dont_cancel(cx: &mut TestAppContext) { - let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; - let fake_model = model.as_fake(); - - let events_1 = thread - .update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Hello 1"], cx) - }) - .unwrap(); - cx.run_until_parked(); - fake_model.send_last_completion_stream_text_chunk("Hey 1!"); - fake_model - .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)); - fake_model.end_last_completion_stream(); - let events_1 = events_1.collect::>().await; - - let events_2 = thread - .update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Hello 2"], cx) - }) - .unwrap(); - cx.run_until_parked(); - fake_model.send_last_completion_stream_text_chunk("Hey 2!"); - fake_model - .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::EndTurn)); - fake_model.end_last_completion_stream(); - let events_2 = events_2.collect::>().await; - - assert_eq!(stop_events(events_1), vec![acp::StopReason::EndTurn]); - assert_eq!(stop_events(events_2), vec![acp::StopReason::EndTurn]); -} - -#[gpui::test] -async fn test_refusal(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.send(UserMessageId::new(), ["Hello"], cx) - }) - .unwrap(); - cx.run_until_parked(); - thread.read_with(cx, |thread, _| { - assert_eq!( - thread.to_markdown(), - indoc! {" - ## User - - Hello - "} - ); - }); - - fake_model.send_last_completion_stream_text_chunk("Hey!"); - cx.run_until_parked(); - thread.read_with(cx, |thread, _| { - assert_eq!( - thread.to_markdown(), - indoc! {" - ## User - - Hello - - ## Assistant - - Hey! - "} - ); - }); - - // If the model refuses to continue, the thread should remove all the messages after the last user message. - fake_model - .send_last_completion_stream_event(LanguageModelCompletionEvent::Stop(StopReason::Refusal)); - let events = events.collect::>().await; - assert_eq!(stop_events(events), vec![acp::StopReason::Refusal]); - thread.read_with(cx, |thread, _| { - assert_eq!(thread.to_markdown(), ""); - }); -} - -#[gpui::test] -async fn test_truncate_first_message(cx: &mut TestAppContext) { - let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; - let fake_model = model.as_fake(); - - let message_id = UserMessageId::new(); - thread - .update(cx, |thread, cx| { - thread.send(message_id.clone(), ["Hello"], cx) - }) - .unwrap(); - cx.run_until_parked(); - thread.read_with(cx, |thread, _| { - assert_eq!( - thread.to_markdown(), - indoc! {" - ## User - - Hello - "} - ); - assert_eq!(thread.latest_token_usage(), None); - }); - - fake_model.send_last_completion_stream_text_chunk("Hey!"); - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate( - language_model::TokenUsage { - input_tokens: 32_000, - output_tokens: 16_000, - cache_creation_input_tokens: 0, - cache_read_input_tokens: 0, - }, - )); - cx.run_until_parked(); - thread.read_with(cx, |thread, _| { - assert_eq!( - thread.to_markdown(), - indoc! {" - ## User - - Hello - - ## Assistant - - Hey! - "} - ); - assert_eq!( - thread.latest_token_usage(), - Some(acp_thread::TokenUsage { - used_tokens: 32_000 + 16_000, - max_tokens: 1_000_000, - }) - ); - }); - - thread - .update(cx, |thread, cx| thread.truncate(message_id, cx)) - .unwrap(); - cx.run_until_parked(); - thread.read_with(cx, |thread, _| { - assert_eq!(thread.to_markdown(), ""); - assert_eq!(thread.latest_token_usage(), None); - }); - - // Ensure we can still send a new message after truncation. - thread - .update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Hi"], cx) - }) - .unwrap(); - thread.update(cx, |thread, _cx| { - assert_eq!( - thread.to_markdown(), - indoc! {" - ## User - - Hi - "} - ); - }); - cx.run_until_parked(); - fake_model.send_last_completion_stream_text_chunk("Ahoy!"); - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate( - language_model::TokenUsage { - input_tokens: 40_000, - output_tokens: 20_000, - cache_creation_input_tokens: 0, - cache_read_input_tokens: 0, - }, - )); - cx.run_until_parked(); - thread.read_with(cx, |thread, _| { - assert_eq!( - thread.to_markdown(), - indoc! {" - ## User - - Hi - - ## Assistant - - Ahoy! - "} - ); - - assert_eq!( - thread.latest_token_usage(), - Some(acp_thread::TokenUsage { - used_tokens: 40_000 + 20_000, - max_tokens: 1_000_000, - }) - ); - }); -} - -#[gpui::test] -async fn test_truncate_second_message(cx: &mut TestAppContext) { - let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; - let fake_model = model.as_fake(); - - thread - .update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Message 1"], cx) - }) - .unwrap(); - cx.run_until_parked(); - fake_model.send_last_completion_stream_text_chunk("Message 1 response"); - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate( - language_model::TokenUsage { - input_tokens: 32_000, - output_tokens: 16_000, - cache_creation_input_tokens: 0, - cache_read_input_tokens: 0, - }, - )); - fake_model.end_last_completion_stream(); - cx.run_until_parked(); - - let assert_first_message_state = |cx: &mut TestAppContext| { - thread.clone().read_with(cx, |thread, _| { - assert_eq!( - thread.to_markdown(), - indoc! {" - ## User - - Message 1 - - ## Assistant - - Message 1 response - "} - ); - - assert_eq!( - thread.latest_token_usage(), - Some(acp_thread::TokenUsage { - used_tokens: 32_000 + 16_000, - max_tokens: 1_000_000, - }) - ); - }); - }; - - assert_first_message_state(cx); - - let second_message_id = UserMessageId::new(); - thread - .update(cx, |thread, cx| { - thread.send(second_message_id.clone(), ["Message 2"], cx) - }) - .unwrap(); - cx.run_until_parked(); - - fake_model.send_last_completion_stream_text_chunk("Message 2 response"); - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::UsageUpdate( - language_model::TokenUsage { - input_tokens: 40_000, - output_tokens: 20_000, - cache_creation_input_tokens: 0, - cache_read_input_tokens: 0, - }, - )); - fake_model.end_last_completion_stream(); - cx.run_until_parked(); - - thread.read_with(cx, |thread, _| { - assert_eq!( - thread.to_markdown(), - indoc! {" - ## User - - Message 1 - - ## Assistant - - Message 1 response - - ## User - - Message 2 - - ## Assistant - - Message 2 response - "} - ); - - assert_eq!( - thread.latest_token_usage(), - Some(acp_thread::TokenUsage { - used_tokens: 40_000 + 20_000, - max_tokens: 1_000_000, - }) - ); - }); - - thread - .update(cx, |thread, cx| thread.truncate(second_message_id, cx)) - .unwrap(); - cx.run_until_parked(); - - assert_first_message_state(cx); -} - -#[gpui::test] -async fn test_title_generation(cx: &mut TestAppContext) { - let ThreadTest { model, thread, .. } = setup(cx, TestModel::Fake).await; - let fake_model = model.as_fake(); - - let summary_model = Arc::new(FakeLanguageModel::default()); - thread.update(cx, |thread, cx| { - thread.set_summarization_model(Some(summary_model.clone()), cx) - }); - - let send = thread - .update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Hello"], cx) - }) - .unwrap(); - cx.run_until_parked(); - - fake_model.send_last_completion_stream_text_chunk("Hey!"); - fake_model.end_last_completion_stream(); - cx.run_until_parked(); - thread.read_with(cx, |thread, _| assert_eq!(thread.title(), "New Thread")); - - // Ensure the summary model has been invoked to generate a title. - summary_model.send_last_completion_stream_text_chunk("Hello "); - summary_model.send_last_completion_stream_text_chunk("world\nG"); - 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. - let send = thread - .update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Hello again"], cx) - }) - .unwrap(); - cx.run_until_parked(); - fake_model.send_last_completion_stream_text_chunk("Hey again!"); - fake_model.end_last_completion_stream(); - cx.run_until_parked(); - assert_eq!(summary_model.pending_completions(), Vec::new()); - send.collect::>().await; - 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, - thought_signature: None, - }; - 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, - thought_signature: None, - }; - 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, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::Assistant, - content: vec![ - MessageContent::Text("Hi!".into()), - MessageContent::ToolUse(echo_tool_use.clone()) - ], - cache: false, - reasoning_details: None, - }, - 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, - reasoning_details: None, - }, - ], - ); -} - -#[gpui::test] -async fn test_agent_connection(cx: &mut TestAppContext) { - cx.update(settings::init); - let templates = Templates::new(); - - // Initialize language model system with test provider - cx.update(|cx| { - gpui_tokio::init(cx); - - let http_client = FakeHttpClient::with_404_response(); - let clock = Arc::new(clock::FakeSystemClock::new()); - let client = Client::new(clock, http_client, cx); - let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); - language_model::init(client.clone(), cx); - language_models::init(user_store, client.clone(), cx); - LanguageModelRegistry::test(cx); - }); - cx.executor().forbid_parking(); - - // Create a project for new_thread - let fake_fs = cx.update(|cx| fs::FakeFs::new(cx.background_executor().clone())); - fake_fs.insert_tree(path!("/test"), json!({})).await; - let project = Project::test(fake_fs.clone(), [Path::new("/test")], cx).await; - let cwd = Path::new("/test"); - let text_thread_store = - cx.new(|cx| assistant_text_thread::TextThreadStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); - - // Create agent and connection - let agent = NativeAgent::new( - project.clone(), - history_store, - templates.clone(), - None, - fake_fs.clone(), - &mut cx.to_async(), - ) - .await - .unwrap(); - let connection = NativeAgentConnection(agent.clone()); - - // Create a thread using new_thread - let connection_rc = Rc::new(connection.clone()); - let acp_thread = cx - .update(|cx| connection_rc.new_thread(project, cwd, cx)) - .await - .expect("new_thread should succeed"); - - // Get the session_id from the AcpThread - let session_id = acp_thread.read_with(cx, |thread, _| thread.session_id().clone()); - - // Test model_selector returns Some - let selector_opt = connection.model_selector(&session_id); - assert!( - selector_opt.is_some(), - "agent should always support ModelSelector" - ); - let selector = selector_opt.unwrap(); - - // Test list_models - let listed_models = cx - .update(|cx| selector.list_models(cx)) - .await - .expect("list_models should succeed"); - let AgentModelList::Grouped(listed_models) = listed_models else { - panic!("Unexpected model list type"); - }; - assert!(!listed_models.is_empty(), "should have at least one model"); - assert_eq!( - listed_models[&AgentModelGroupName("Fake".into())][0] - .id - .0 - .as_ref(), - "fake/fake" - ); - - // Test selected_model returns the default - let model = cx - .update(|cx| selector.selected_model(cx)) - .await - .expect("selected_model should succeed"); - let model = cx - .update(|cx| agent.read(cx).models().model_from_id(&model.id)) - .unwrap(); - let model = model.as_fake(); - assert_eq!(model.id().0, "fake", "should return default model"); - - let request = acp_thread.update(cx, |thread, cx| thread.send(vec!["abc".into()], cx)); - cx.run_until_parked(); - model.send_last_completion_stream_text_chunk("def"); - cx.run_until_parked(); - acp_thread.read_with(cx, |thread, cx| { - assert_eq!( - thread.to_markdown(cx), - indoc! {" - ## User - - abc - - ## Assistant - - def - - "} - ) - }); - - // Test cancel - cx.update(|cx| connection.cancel(&session_id, cx)); - request.await.expect("prompt should fail gracefully"); - - // Ensure that dropping the ACP thread causes the native thread to be - // dropped as well. - cx.update(|_| drop(acp_thread)); - let result = cx - .update(|cx| { - connection.prompt( - Some(acp_thread::UserMessageId::new()), - acp::PromptRequest::new(session_id.clone(), vec!["ghi".into()]), - cx, - ) - }) - .await; - assert_eq!( - result.as_ref().unwrap_err().to_string(), - "Session not found", - "unexpected result: {:?}", - result - ); -} - -#[gpui::test] -async fn test_tool_updates_to_completion(cx: &mut TestAppContext) { - let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await; - thread.update(cx, |thread, _cx| thread.add_tool(ThinkingTool)); - let fake_model = model.as_fake(); - - let mut events = thread - .update(cx, |thread, cx| { - thread.send(UserMessageId::new(), ["Think"], cx) - }) - .unwrap(); - cx.run_until_parked(); - - // Simulate streaming partial input. - let input = json!({}); - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( - LanguageModelToolUse { - id: "1".into(), - name: ThinkingTool::name().into(), - raw_input: input.to_string(), - input, - is_input_complete: false, - thought_signature: None, - }, - )); - - // Input streaming completed - let input = json!({ "content": "Thinking hard!" }); - fake_model.send_last_completion_stream_event(LanguageModelCompletionEvent::ToolUse( - LanguageModelToolUse { - id: "1".into(), - name: "thinking".into(), - raw_input: input.to_string(), - input, - is_input_complete: true, - thought_signature: None, - }, - )); - fake_model.end_last_completion_stream(); - cx.run_until_parked(); - - let tool_call = expect_tool_call(&mut events).await; - assert_eq!( - tool_call, - acp::ToolCall::new("1", "Thinking") - .kind(acp::ToolKind::Think) - .raw_input(json!({})) - .meta(acp::Meta::from_iter([( - "tool_name".into(), - "thinking".into() - )])) - ); - let update = expect_tool_call_update_fields(&mut events).await; - assert_eq!( - update, - acp::ToolCallUpdate::new( - "1", - acp::ToolCallUpdateFields::new() - .title("Thinking") - .kind(acp::ToolKind::Think) - .raw_input(json!({ "content": "Thinking hard!"})) - ) - ); - let update = expect_tool_call_update_fields(&mut events).await; - assert_eq!( - update, - acp::ToolCallUpdate::new( - "1", - acp::ToolCallUpdateFields::new().status(acp::ToolCallStatus::InProgress) - ) - ); - let update = expect_tool_call_update_fields(&mut events).await; - assert_eq!( - update, - acp::ToolCallUpdate::new( - "1", - acp::ToolCallUpdateFields::new().content(vec!["Thinking hard!".into()]) - ) - ); - let update = expect_tool_call_update_fields(&mut events).await; - assert_eq!( - update, - acp::ToolCallUpdate::new( - "1", - acp::ToolCallUpdateFields::new() - .status(acp::ToolCallStatus::Completed) - .raw_output("Finished thinking.") - ) - ); -} - -#[gpui::test] -async fn test_send_no_retry_on_success(cx: &mut TestAppContext) { - let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await; - let fake_model = model.as_fake(); - - let mut events = thread - .update(cx, |thread, cx| { - thread.set_completion_mode(agent_settings::CompletionMode::Burn, cx); - thread.send(UserMessageId::new(), ["Hello!"], cx) - }) - .unwrap(); - cx.run_until_parked(); - - fake_model.send_last_completion_stream_text_chunk("Hey!"); - fake_model.end_last_completion_stream(); - - let mut retry_events = Vec::new(); - while let Some(Ok(event)) = events.next().await { - match event { - ThreadEvent::Retry(retry_status) => { - retry_events.push(retry_status); - } - ThreadEvent::Stop(..) => break, - _ => {} - } - } - - assert_eq!(retry_events.len(), 0); - thread.read_with(cx, |thread, _cx| { - assert_eq!( - thread.to_markdown(), - indoc! {" - ## User - - Hello! - - ## Assistant - - Hey! - "} - ) - }); -} - -#[gpui::test] -async fn test_send_retry_on_error(cx: &mut TestAppContext) { - let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await; - let fake_model = model.as_fake(); - - let mut events = thread - .update(cx, |thread, cx| { - thread.set_completion_mode(agent_settings::CompletionMode::Burn, cx); - thread.send(UserMessageId::new(), ["Hello!"], cx) - }) - .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)), - }); - fake_model.end_last_completion_stream(); - - cx.executor().advance_clock(Duration::from_secs(3)); - cx.run_until_parked(); - - fake_model.send_last_completion_stream_text_chunk("there!"); - fake_model.end_last_completion_stream(); - cx.run_until_parked(); - - let mut retry_events = Vec::new(); - while let Some(Ok(event)) = events.next().await { - match event { - ThreadEvent::Retry(retry_status) => { - retry_events.push(retry_status); - } - ThreadEvent::Stop(..) => break, - _ => {} - } - } - - assert_eq!(retry_events.len(), 1); - assert!(matches!( - retry_events[0], - acp_thread::RetryStatus { attempt: 1, .. } - )); - thread.read_with(cx, |thread, _cx| { - assert_eq!( - thread.to_markdown(), - indoc! {" - ## User - - Hello! - - ## Assistant - - Hey, - - [resume] - - ## Assistant - - there! - "} - ) - }); -} - -#[gpui::test] -async fn test_send_retry_finishes_tool_calls_on_error(cx: &mut TestAppContext) { - let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await; - let fake_model = model.as_fake(); - - let events = thread - .update(cx, |thread, cx| { - thread.set_completion_mode(agent_settings::CompletionMode::Burn, cx); - thread.add_tool(EchoTool); - thread.send(UserMessageId::new(), ["Call the echo tool!"], cx) - }) - .unwrap(); - cx.run_until_parked(); - - let tool_use_1 = LanguageModelToolUse { - id: "tool_1".into(), - name: EchoTool::name().into(), - raw_input: json!({"text": "test"}).to_string(), - input: json!({"text": "test"}), - is_input_complete: true, - thought_signature: None, - }; - 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, - reasoning_details: None, - }, - LanguageModelRequestMessage { - role: Role::Assistant, - content: vec![language_model::MessageContent::ToolUse(tool_use_1.clone())], - cache: false, - reasoning_details: None, - }, - 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, - reasoning_details: None, - }, - ] - ); - - 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(), - reasoning_details: None, - })) - ); - }) -} - -#[gpui::test] -async fn test_send_max_retries_exceeded(cx: &mut TestAppContext) { - let ThreadTest { thread, model, .. } = setup(cx, TestModel::Fake).await; - let fake_model = model.as_fake(); - - let mut events = thread - .update(cx, |thread, cx| { - thread.set_completion_mode(agent_settings::CompletionMode::Burn, cx); - thread.send(UserMessageId::new(), ["Hello!"], cx) - }) - .unwrap(); - cx.run_until_parked(); - - for _ in 0..crate::thread::MAX_RETRY_ATTEMPTS + 1 { - 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)); - cx.run_until_parked(); - } - - let mut errors = Vec::new(); - let mut retry_events = Vec::new(); - while let Some(event) = events.next().await { - match event { - Ok(ThreadEvent::Retry(retry_status)) => { - retry_events.push(retry_status); - } - Ok(ThreadEvent::Stop(..)) => break, - Err(error) => errors.push(error), - _ => {} - } - } - - assert_eq!( - retry_events.len(), - crate::thread::MAX_RETRY_ATTEMPTS as usize - ); - for i in 0..crate::thread::MAX_RETRY_ATTEMPTS as usize { - assert_eq!(retry_events[i].attempt, i + 1); - } - assert_eq!(errors.len(), 1); - let error = errors[0] - .downcast_ref::() - .unwrap(); - assert!(matches!( - error, - LanguageModelCompletionError::ServerOverloaded { .. } - )); -} - -/// Filters out the stop events for asserting against in tests -fn stop_events(result_events: Vec>) -> Vec { - result_events - .into_iter() - .filter_map(|event| match event.unwrap() { - ThreadEvent::Stop(stop_reason) => Some(stop_reason), - _ => None, - }) - .collect() -} - -struct ThreadTest { - model: Arc, - thread: Entity, - project_context: Entity, - context_server_store: Entity, - fs: Arc, -} - -enum TestModel { - Sonnet4, - Fake, -} - -impl TestModel { - fn id(&self) -> LanguageModelId { - match self { - TestModel::Sonnet4 => LanguageModelId("claude-sonnet-4-latest".into()), - TestModel::Fake => unreachable!(), - } - } -} - -async fn setup(cx: &mut TestAppContext, model: TestModel) -> ThreadTest { - cx.executor().allow_parking(); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.create_dir(paths::settings_file().parent().unwrap()) - .await - .unwrap(); - fs.insert_file( - paths::settings_file(), - json!({ - "agent": { - "default_profile": "test-profile", - "profiles": { - "test-profile": { - "name": "Test Profile", - "tools": { - EchoTool::name(): true, - DelayTool::name(): true, - WordListTool::name(): true, - ToolRequiringPermission::name(): true, - InfiniteTool::name(): true, - ThinkingTool::name(): true, - } - } - } - } - }) - .to_string() - .into_bytes(), - ) - .await; - - cx.update(|cx| { - settings::init(cx); - - match model { - TestModel::Fake => {} - TestModel::Sonnet4 => { - gpui_tokio::init(cx); - let http_client = ReqwestClient::user_agent("agent tests").unwrap(); - cx.set_http_client(Arc::new(http_client)); - let client = Client::production(cx); - let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); - language_model::init(client.clone(), cx); - language_models::init(user_store, client.clone(), cx); - } - }; - - watch_settings(fs.clone(), cx); - }); - - let templates = Templates::new(); - - fs.insert_tree(path!("/test"), json!({})).await; - let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; - - let model = cx - .update(|cx| { - if let TestModel::Fake = model { - Task::ready(Arc::new(FakeLanguageModel::default()) as Arc<_>) - } else { - let model_id = model.id(); - let models = LanguageModelRegistry::read_global(cx); - let model = models - .available_models(cx) - .find(|model| model.id() == model_id) - .unwrap(); - - let provider = models.provider(&model.provider_id()).unwrap(); - let authenticated = provider.authenticate(cx); - - cx.spawn(async move |_cx| { - authenticated.await.unwrap(); - model - }) - } - }) - .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)); - let thread = cx.new(|cx| { - Thread::new( - project, - project_context.clone(), - context_server_registry, - templates, - Some(model.clone()), - cx, - ) - }); - ThreadTest { - model, - thread, - project_context, - context_server_store, - fs, - } -} - -#[cfg(test)] -#[ctor::ctor] -fn init_logger() { - if std::env::var("RUST_LOG").is_ok() { - env_logger::init(); - } -} - -fn watch_settings(fs: Arc, cx: &mut App) { - let fs = fs.clone(); - cx.spawn({ - async move |cx| { - let mut new_settings_content_rx = settings::watch_config_file( - cx.background_executor(), - fs, - paths::settings_file().clone(), - ); - - while let Some(new_settings_content) = new_settings_content_rx.next().await { - cx.update(|cx| { - SettingsStore::update_global(cx, |settings, cx| { - settings.set_user_settings(&new_settings_content, cx) - }) - }) - .ok(); - } - } - }) - .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::Stdio { - enabled: true, - command: ContextServerCommand { - path: "somebinary".into(), - args: Vec::new(), - env: None, - timeout: 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/agent/src/tests/test_tools.rs b/crates/agent/src/tests/test_tools.rs deleted file mode 100644 index 2275d23c2f..0000000000 --- a/crates/agent/src/tests/test_tools.rs +++ /dev/null @@ -1,221 +0,0 @@ -use super::*; -use anyhow::Result; -use gpui::{App, SharedString, Task}; -use std::future; - -/// A tool that echoes its input -#[derive(JsonSchema, Serialize, Deserialize)] -pub struct EchoToolInput { - /// The text to echo. - pub text: String, -} - -pub struct EchoTool; - -impl AgentTool for EchoTool { - type Input = EchoToolInput; - type Output = String; - - fn name() -> &'static str { - "echo" - } - - fn kind() -> acp::ToolKind { - acp::ToolKind::Other - } - - fn initial_title( - &self, - _input: Result, - _cx: &mut App, - ) -> SharedString { - "Echo".into() - } - - fn run( - self: Arc, - input: Self::Input, - _event_stream: ToolCallEventStream, - _cx: &mut App, - ) -> Task> { - Task::ready(Ok(input.text)) - } -} - -/// A tool that waits for a specified delay -#[derive(JsonSchema, Serialize, Deserialize)] -pub struct DelayToolInput { - /// The delay in milliseconds. - ms: u64, -} - -pub struct DelayTool; - -impl AgentTool for DelayTool { - type Input = DelayToolInput; - type Output = String; - - fn name() -> &'static str { - "delay" - } - - fn initial_title( - &self, - input: Result, - _cx: &mut App, - ) -> SharedString { - if let Ok(input) = input { - format!("Delay {}ms", input.ms).into() - } else { - "Delay".into() - } - } - - fn kind() -> acp::ToolKind { - acp::ToolKind::Other - } - - fn run( - self: Arc, - input: Self::Input, - _event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> - where - Self: Sized, - { - cx.foreground_executor().spawn(async move { - smol::Timer::after(Duration::from_millis(input.ms)).await; - Ok("Ding".to_string()) - }) - } -} - -#[derive(JsonSchema, Serialize, Deserialize)] -pub struct ToolRequiringPermissionInput {} - -pub struct ToolRequiringPermission; - -impl AgentTool for ToolRequiringPermission { - type Input = ToolRequiringPermissionInput; - type Output = String; - - fn name() -> &'static str { - "tool_requiring_permission" - } - - fn kind() -> acp::ToolKind { - acp::ToolKind::Other - } - - fn initial_title( - &self, - _input: Result, - _cx: &mut App, - ) -> SharedString { - "This tool requires permission".into() - } - - fn run( - self: Arc, - _input: Self::Input, - event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - let authorize = event_stream.authorize("Authorize?", cx); - cx.foreground_executor().spawn(async move { - authorize.await?; - Ok("Allowed".to_string()) - }) - } -} - -#[derive(JsonSchema, Serialize, Deserialize)] -pub struct InfiniteToolInput {} - -pub struct InfiniteTool; - -impl AgentTool for InfiniteTool { - type Input = InfiniteToolInput; - type Output = String; - - fn name() -> &'static str { - "infinite" - } - - fn kind() -> acp::ToolKind { - acp::ToolKind::Other - } - - fn initial_title( - &self, - _input: Result, - _cx: &mut App, - ) -> SharedString { - "Infinite Tool".into() - } - - fn run( - self: Arc, - _input: Self::Input, - _event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - cx.foreground_executor().spawn(async move { - future::pending::<()>().await; - unreachable!() - }) - } -} - -/// A tool that takes an object with map from letters to random words starting with that letter. -/// All fiealds are required! Pass a word for every letter! -#[derive(JsonSchema, Serialize, Deserialize)] -pub struct WordListInput { - /// Provide a random word that starts with A. - a: Option, - /// Provide a random word that starts with B. - b: Option, - /// Provide a random word that starts with C. - c: Option, - /// Provide a random word that starts with D. - d: Option, - /// Provide a random word that starts with E. - e: Option, - /// Provide a random word that starts with F. - f: Option, - /// Provide a random word that starts with G. - g: Option, -} - -pub struct WordListTool; - -impl AgentTool for WordListTool { - type Input = WordListInput; - type Output = String; - - fn name() -> &'static str { - "word_list" - } - - fn kind() -> acp::ToolKind { - acp::ToolKind::Other - } - - fn initial_title( - &self, - _input: Result, - _cx: &mut App, - ) -> SharedString { - "List of random words".into() - } - - fn run( - self: Arc, - _input: Self::Input, - _event_stream: ToolCallEventStream, - _cx: &mut App, - ) -> Task> { - Task::ready(Ok("ok".to_string())) - } -} diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs deleted file mode 100644 index 4aabf8069b..0000000000 --- a/crates/agent/src/thread.rs +++ /dev/null @@ -1,2664 +0,0 @@ -use crate::{ - ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DbLanguageModel, DbThread, - DeletePathTool, DiagnosticsTool, EditFileTool, FetchTool, FindPathTool, GrepTool, - ListDirectoryTool, MovePathTool, NowTool, OpenTool, ProjectSnapshot, ReadFileTool, - SystemPromptTemplate, Template, Templates, TerminalTool, ThinkingTool, WebSearchTool, -}; -use acp_thread::{MentionUri, UserMessageId}; -use action_log::ActionLog; - -use agent_client_protocol as acp; -use agent_settings::{ - AgentProfileId, AgentProfileSettings, AgentSettings, CompletionMode, - SUMMARIZE_THREAD_DETAILED_PROMPT, SUMMARIZE_THREAD_PROMPT, -}; -use anyhow::{Context as _, Result, anyhow}; -use chrono::{DateTime, Utc}; -use client::{ModelRequestUsage, RequestUsage, UserStore}; -use cloud_llm_client::{CompletionIntent, Plan, UsageLimit}; -use collections::{HashMap, HashSet, IndexMap}; -use fs::Fs; -use futures::stream; -use futures::{ - FutureExt, - channel::{mpsc, oneshot}, - future::Shared, - stream::FuturesUnordered, -}; -use gpui::{ - App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity, -}; -use language_model::{ - LanguageModel, LanguageModelCompletionError, LanguageModelCompletionEvent, LanguageModelExt, - LanguageModelId, LanguageModelImage, LanguageModelProviderId, LanguageModelRegistry, - LanguageModelRequest, LanguageModelRequestMessage, LanguageModelRequestTool, - LanguageModelToolResult, LanguageModelToolResultContent, LanguageModelToolSchemaFormat, - LanguageModelToolUse, LanguageModelToolUseId, Role, SelectedModel, StopReason, TokenUsage, - ZED_CLOUD_PROVIDER_ID, -}; -use project::Project; -use prompt_store::ProjectContext; -use schemars::{JsonSchema, Schema}; -use serde::{Deserialize, Serialize}; -use settings::{LanguageModelSelection, Settings, update_settings_file}; -use smol::stream::StreamExt; -use std::{ - collections::BTreeMap, - ops::RangeInclusive, - path::Path, - rc::Rc, - sync::Arc, - time::{Duration, Instant}, -}; -use std::{fmt::Write, path::PathBuf}; -use util::{ResultExt, debug_panic, markdown::MarkdownCodeBlock, paths::PathStyle}; -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. -/// -/// This equates to the user physically submitting a message to the model (e.g., by pressing the Enter key). -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)] -pub struct PromptId(Arc); - -impl PromptId { - pub fn new() -> Self { - Self(Uuid::new_v4().to_string().into()) - } -} - -impl std::fmt::Display for PromptId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -pub(crate) const MAX_RETRY_ATTEMPTS: u8 = 4; -pub(crate) const BASE_RETRY_DELAY: Duration = Duration::from_secs(5); - -#[derive(Debug, Clone)] -enum RetryStrategy { - ExponentialBackoff { - initial_delay: Duration, - max_attempts: u8, - }, - Fixed { - delay: Duration, - max_attempts: u8, - }, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum Message { - User(UserMessage), - Agent(AgentMessage), - Resume, -} - -impl Message { - pub fn as_agent_message(&self) -> Option<&AgentMessage> { - match self { - Message::Agent(agent_message) => Some(agent_message), - _ => None, - } - } - - pub fn to_request(&self) -> Vec { - match self { - Message::User(message) => vec![message.to_request()], - Message::Agent(message) => message.to_request(), - Message::Resume => vec![LanguageModelRequestMessage { - role: Role::User, - content: vec!["Continue where you left off".into()], - cache: false, - reasoning_details: None, - }], - } - } - - pub fn to_markdown(&self) -> String { - match self { - Message::User(message) => message.to_markdown(), - Message::Agent(message) => message.to_markdown(), - Message::Resume => "[resume]\n".into(), - } - } - - pub fn role(&self) -> Role { - match self { - Message::User(_) | Message::Resume => Role::User, - Message::Agent(_) => Role::Assistant, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct UserMessage { - pub id: UserMessageId, - pub content: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum UserMessageContent { - Text(String), - Mention { uri: MentionUri, content: String }, - Image(LanguageModelImage), -} - -impl UserMessage { - pub fn to_markdown(&self) -> String { - let mut markdown = String::from("## User\n\n"); - - for content in &self.content { - match content { - UserMessageContent::Text(text) => { - markdown.push_str(text); - markdown.push('\n'); - } - UserMessageContent::Image(_) => { - markdown.push_str("\n"); - } - UserMessageContent::Mention { uri, content } => { - if !content.is_empty() { - let _ = writeln!(&mut markdown, "{}\n\n{}", uri.as_link(), content); - } else { - let _ = writeln!(&mut markdown, "{}", uri.as_link()); - } - } - } - } - - markdown - } - - fn to_request(&self) -> LanguageModelRequestMessage { - let mut message = LanguageModelRequestMessage { - role: Role::User, - content: Vec::with_capacity(self.content.len()), - cache: false, - reasoning_details: None, - }; - - const OPEN_CONTEXT: &str = "\n\ - The following items were attached by the user. \ - They are up-to-date and don't need to be re-read.\n\n"; - - 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 = - "\nThe user has specified the following rules that should be applied:\n"; - - 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(); - - for chunk in &self.content { - let chunk = match chunk { - UserMessageContent::Text(text) => { - language_model::MessageContent::Text(text.clone()) - } - UserMessageContent::Image(value) => { - language_model::MessageContent::Image(value.clone()) - } - UserMessageContent::Mention { uri, content } => { - match uri { - MentionUri::File { abs_path } => { - write!( - &mut file_context, - "\n{}", - MarkdownCodeBlock { - tag: &codeblock_tag(abs_path, None), - text: &content.to_string(), - } - ) - .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, - .. - } => { - write!( - &mut symbol_context, - "\n{}", - MarkdownCodeBlock { - tag: &codeblock_tag(path, Some(line_range)), - text: content - } - ) - .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(); - } - MentionUri::TextThread { .. } => { - write!(&mut thread_context, "\n{}\n", content).ok(); - } - MentionUri::Rule { .. } => { - write!( - &mut rules_context, - "\n{}", - MarkdownCodeBlock { - tag: "", - text: content - } - ) - .ok(); - } - MentionUri::Fetch { url } => { - write!(&mut fetch_context, "\nFetch: {}\n\n{}", url, content).ok(); - } - } - - language_model::MessageContent::Text(uri.as_link().to_string()) - } - }; - - message.content.push(chunk); - } - - let len_before_context = message.content.len(); - - if file_context.len() > OPEN_FILES_TAG.len() { - file_context.push_str("\n"); - message - .content - .push(language_model::MessageContent::Text(file_context)); - } - - if directory_context.len() > OPEN_DIRECTORIES_TAG.len() { - directory_context.push_str("\n"); - message - .content - .push(language_model::MessageContent::Text(directory_context)); - } - - if symbol_context.len() > OPEN_SYMBOLS_TAG.len() { - symbol_context.push_str("\n"); - message - .content - .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 - .content - .push(language_model::MessageContent::Text(thread_context)); - } - - if fetch_context.len() > OPEN_FETCH_TAG.len() { - fetch_context.push_str("\n"); - message - .content - .push(language_model::MessageContent::Text(fetch_context)); - } - - if rules_context.len() > OPEN_RULES_TAG.len() { - rules_context.push_str("\n"); - message - .content - .push(language_model::MessageContent::Text(rules_context)); - } - - if message.content.len() > len_before_context { - message.content.insert( - len_before_context, - language_model::MessageContent::Text(OPEN_CONTEXT.into()), - ); - message - .content - .push(language_model::MessageContent::Text("".into())); - } - - message - } -} - -fn codeblock_tag(full_path: &Path, line_range: Option<&RangeInclusive>) -> String { - let mut result = String::new(); - - if let Some(extension) = full_path.extension().and_then(|ext| ext.to_str()) { - let _ = write!(result, "{} ", extension); - } - - let _ = write!(result, "{}", full_path.display()); - - if let Some(range) = line_range { - if range.start() == range.end() { - let _ = write!(result, ":{}", range.start() + 1); - } else { - let _ = write!(result, ":{}-{}", range.start() + 1, range.end() + 1); - } - } - - result -} - -impl AgentMessage { - pub fn to_markdown(&self) -> String { - let mut markdown = String::from("## Assistant\n\n"); - - for content in &self.content { - match content { - AgentMessageContent::Text(text) => { - markdown.push_str(text); - markdown.push('\n'); - } - AgentMessageContent::Thinking { text, .. } => { - markdown.push_str(""); - markdown.push_str(text); - markdown.push_str("\n"); - } - AgentMessageContent::RedactedThinking(_) => { - markdown.push_str("\n") - } - AgentMessageContent::ToolUse(tool_use) => { - markdown.push_str(&format!( - "**Tool Use**: {} (ID: {})\n", - tool_use.name, tool_use.id - )); - markdown.push_str(&format!( - "{}\n", - MarkdownCodeBlock { - tag: "json", - text: &format!("{:#}", tool_use.input) - } - )); - } - } - } - - for tool_result in self.tool_results.values() { - markdown.push_str(&format!( - "**Tool Result**: {} (ID: {})\n\n", - tool_result.tool_name, tool_result.tool_use_id - )); - if tool_result.is_error { - markdown.push_str("**ERROR:**\n"); - } - - match &tool_result.content { - LanguageModelToolResultContent::Text(text) => { - writeln!(markdown, "{text}\n").ok(); - } - LanguageModelToolResultContent::Image(_) => { - writeln!(markdown, "\n").ok(); - } - } - - if let Some(output) = tool_result.output.as_ref() { - writeln!( - markdown, - "**Debug Output**:\n\n```json\n{}\n```\n", - serde_json::to_string_pretty(output).unwrap() - ) - .unwrap(); - } - } - - markdown - } - - pub fn to_request(&self) -> Vec { - let mut assistant_message = LanguageModelRequestMessage { - role: Role::Assistant, - content: Vec::with_capacity(self.content.len()), - cache: false, - reasoning_details: self.reasoning_details.clone(), - }; - for chunk in &self.content { - match chunk { - AgentMessageContent::Text(text) => { - assistant_message - .content - .push(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())); - } - } - }; - } - - let mut user_message = LanguageModelRequestMessage { - role: Role::User, - content: Vec::new(), - cache: false, - reasoning_details: None, - }; - - for tool_result in self.tool_results.values() { - let mut tool_result = tool_result.clone(); - // Surprisingly, the API fails if we return an empty string here. - // It thinks we are sending a tool use without a tool result. - if tool_result.content.is_empty() { - tool_result.content = "".into(); - } - user_message - .content - .push(language_model::MessageContent::ToolResult(tool_result)); - } - - let mut messages = Vec::new(); - if !assistant_message.content.is_empty() { - messages.push(assistant_message); - } - if !user_message.content.is_empty() { - messages.push(user_message); - } - messages - } -} - -#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct AgentMessage { - pub content: Vec, - pub tool_results: IndexMap, - pub reasoning_details: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum AgentMessageContent { - Text(String), - Thinking { - text: String, - signature: Option, - }, - RedactedThinking(String), - ToolUse(LanguageModelToolUse), -} - -pub trait TerminalHandle { - fn id(&self, cx: &AsyncApp) -> Result; - fn current_output(&self, cx: &AsyncApp) -> Result; - fn wait_for_exit(&self, cx: &AsyncApp) -> Result>>; -} - -pub trait ThreadEnvironment { - fn create_terminal( - &self, - command: String, - cwd: Option, - output_byte_limit: Option, - cx: &mut AsyncApp, - ) -> Task>>; -} - -#[derive(Debug)] -pub enum ThreadEvent { - UserMessage(UserMessage), - AgentText(String), - AgentThinking(String), - ToolCall(acp::ToolCall), - ToolCallUpdate(acp_thread::ToolCallUpdate), - ToolCallAuthorization(ToolCallAuthorization), - Retry(acp_thread::RetryStatus), - Stop(acp::StopReason), -} - -#[derive(Debug)] -pub struct NewTerminal { - pub command: String, - pub output_byte_limit: Option, - pub cwd: Option, - pub response: oneshot::Sender>>, -} - -#[derive(Debug)] -pub struct ToolCallAuthorization { - pub tool_call: acp::ToolCallUpdate, - pub options: Vec, - pub response: oneshot::Sender, -} - -#[derive(Debug, thiserror::Error)] -enum CompletionError { - #[error("max tokens")] - MaxTokens, - #[error("refusal")] - Refusal, - #[error(transparent)] - Other(#[from] anyhow::Error), -} - -pub struct Thread { - id: acp::SessionId, - prompt_id: PromptId, - updated_at: DateTime, - title: Option, - pending_title_generation: Option>, - pending_summary_generation: Option>>>, - summary: Option, - messages: Vec, - user_store: Entity, - completion_mode: CompletionMode, - /// Holds the task that handles agent interaction until the end of the turn. - /// Survives across multiple requests as the model performs tool calls and - /// we run tools, report their results. - running_turn: Option, - pending_message: Option, - tools: BTreeMap>, - tool_use_limit_reached: bool, - request_token_usage: HashMap, - #[allow(unused)] - cumulative_token_usage: TokenUsage, - #[allow(unused)] - initial_project_snapshot: Shared>>>, - context_server_registry: Entity, - profile_id: AgentProfileId, - project_context: Entity, - 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, - /// Tracks the last time files were read by the agent, to detect external modifications - pub(crate) file_read_times: HashMap, -} - -impl Thread { - fn prompt_capabilities(model: Option<&dyn LanguageModel>) -> acp::PromptCapabilities { - let image = model.map_or(true, |model| model.supports_images()); - acp::PromptCapabilities::new() - .image(image) - .embedded_context(true) - } - - pub fn new( - project: Entity, - project_context: Entity, - context_server_registry: 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::new(uuid::Uuid::new_v4().to_string()), - prompt_id: PromptId::new(), - updated_at: Utc::now(), - title: None, - pending_title_generation: None, - pending_summary_generation: None, - summary: None, - messages: Vec::new(), - user_store: project.read(cx).user_store(), - completion_mode: AgentSettings::get_global(cx).preferred_completion_mode, - running_turn: None, - pending_message: None, - tools: BTreeMap::default(), - tool_use_limit_reached: false, - request_token_usage: HashMap::default(), - cumulative_token_usage: TokenUsage::default(), - initial_project_snapshot: { - let project_snapshot = Self::project_snapshot(project.clone(), cx); - cx.foreground_executor() - .spawn(async move { Some(project_snapshot.await) }) - .shared() - }, - context_server_registry, - profile_id, - project_context, - templates, - model, - summarization_model: None, - prompt_capabilities_tx, - prompt_capabilities_rx, - project, - action_log, - file_read_times: HashMap::default(), - } - } - - pub fn id(&self) -> &acp::SessionId { - &self.id - } - - pub fn replay( - &mut self, - cx: &mut Context, - ) -> mpsc::UnboundedReceiver> { - let (tx, rx) = mpsc::unbounded(); - let stream = ThreadEventStream(tx); - for message in &self.messages { - match message { - Message::User(user_message) => stream.send_user_message(user_message), - Message::Agent(assistant_message) => { - for content in &assistant_message.content { - match content { - AgentMessageContent::Text(text) => stream.send_text(text), - AgentMessageContent::Thinking { text, .. } => { - stream.send_thinking(text) - } - AgentMessageContent::RedactedThinking(_) => {} - AgentMessageContent::ToolUse(tool_use) => { - self.replay_tool_call( - tool_use, - assistant_message.tool_results.get(&tool_use.id), - &stream, - cx, - ); - } - } - } - } - Message::Resume => {} - } - } - rx - } - - fn replay_tool_call( - &self, - tool_use: &LanguageModelToolUse, - tool_result: Option<&LanguageModelToolResult>, - 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 { - stream - .0 - .unbounded_send(Ok(ThreadEvent::ToolCall( - acp::ToolCall::new(tool_use.id.to_string(), tool_use.name.to_string()) - .status(acp::ToolCallStatus::Failed) - .raw_input(tool_use.input.clone()), - ))) - .ok(); - return; - }; - - let title = tool.initial_title(tool_use.input.clone(), cx); - let kind = tool.kind(); - stream.send_tool_call( - &tool_use.id, - &tool_use.name, - title, - kind, - tool_use.input.clone(), - ); - - let output = tool_result - .as_ref() - .and_then(|result| result.output.clone()); - if let Some(output) = output.clone() { - let tool_event_stream = ToolCallEventStream::new( - tool_use.id.clone(), - stream.clone(), - Some(self.project.read(cx).fs().clone()), - ); - tool.replay(tool_use.input.clone(), output, tool_event_stream, cx) - .log_err(); - } - - stream.update_tool_call_fields( - &tool_use.id, - acp::ToolCallUpdateFields::new() - .status( - tool_result - .as_ref() - .map_or(acp::ToolCallStatus::Failed, |result| { - if result.is_error { - acp::ToolCallStatus::Failed - } else { - acp::ToolCallStatus::Completed - } - }), - ) - .raw_output(output), - ); - } - - pub fn from_db( - id: acp::SessionId, - db_thread: DbThread, - project: Entity, - project_context: Entity, - context_server_registry: Entity, - templates: Arc, - cx: &mut Context, - ) -> Self { - let profile_id = db_thread - .profile - .unwrap_or_else(|| AgentSettings::get_global(cx).default_profile.clone()); - - let mut model = LanguageModelRegistry::global(cx).update(cx, |registry, cx| { - db_thread - .model - .and_then(|model| { - let model = SelectedModel { - provider: model.provider.clone().into(), - model: model.model.into(), - }; - registry.select_model(&model, cx) - }) - .or_else(|| registry.default_model()) - .map(|model| model.model) - }); - - if model.is_none() { - model = Self::resolve_profile_model(&profile_id, cx); - } - if model.is_none() { - model = LanguageModelRegistry::global(cx).update(cx, |registry, _cx| { - registry.default_model().map(|model| model.model) - }); - } - - let (prompt_capabilities_tx, prompt_capabilities_rx) = - watch::channel(Self::prompt_capabilities(model.as_deref())); - - let action_log = cx.new(|_| ActionLog::new(project.clone())); - - Self { - id, - prompt_id: PromptId::new(), - title: if db_thread.title.is_empty() { - None - } else { - Some(db_thread.title.clone()) - }, - pending_title_generation: None, - pending_summary_generation: None, - summary: db_thread.detailed_summary, - messages: db_thread.messages, - user_store: project.read(cx).user_store(), - completion_mode: db_thread.completion_mode.unwrap_or_default(), - running_turn: None, - pending_message: None, - tools: BTreeMap::default(), - tool_use_limit_reached: false, - request_token_usage: db_thread.request_token_usage.clone(), - cumulative_token_usage: db_thread.cumulative_token_usage, - initial_project_snapshot: Task::ready(db_thread.initial_project_snapshot).shared(), - context_server_registry, - profile_id, - project_context, - templates, - model, - summarization_model: None, - project, - action_log, - updated_at: db_thread.updated_at, - prompt_capabilities_tx, - prompt_capabilities_rx, - file_read_times: HashMap::default(), - } - } - - pub fn to_db(&self, cx: &App) -> Task { - let initial_project_snapshot = self.initial_project_snapshot.clone(); - let mut thread = DbThread { - title: self.title(), - messages: self.messages.clone(), - updated_at: self.updated_at, - detailed_summary: self.summary.clone(), - initial_project_snapshot: None, - cumulative_token_usage: self.cumulative_token_usage, - request_token_usage: self.request_token_usage.clone(), - model: self.model.as_ref().map(|model| DbLanguageModel { - provider: model.provider_id().to_string(), - model: model.name().0.to_string(), - }), - completion_mode: Some(self.completion_mode), - profile: Some(self.profile_id.clone()), - }; - - cx.background_spawn(async move { - let initial_project_snapshot = initial_project_snapshot.await; - thread.initial_project_snapshot = initial_project_snapshot; - thread - }) - } - - /// Create a snapshot of the current project state including git information and unsaved buffers. - fn project_snapshot( - project: Entity, - cx: &mut Context, - ) -> Task> { - let task = project::telemetry_snapshot::TelemetrySnapshot::new(&project, cx); - cx.spawn(async move |_, _| { - let snapshot = task.await; - - Arc::new(ProjectSnapshot { - worktree_snapshots: snapshot.worktree_snapshots, - timestamp: Utc::now(), - }) - }) - } - - pub fn project_context(&self) -> &Entity { - &self.project_context - } - - pub fn project(&self) -> &Entity { - &self.project - } - - pub fn action_log(&self) -> &Entity { - &self.action_log - } - - pub fn is_empty(&self) -> bool { - self.messages.is_empty() && self.title.is_none() - } - - pub fn model(&self) -> Option<&Arc> { - self.model.as_ref() - } - - pub fn set_model(&mut self, model: Arc, cx: &mut Context) { - let old_usage = self.latest_token_usage(); - self.model = Some(model); - let new_caps = Self::prompt_capabilities(self.model.as_deref()); - let new_usage = self.latest_token_usage(); - if old_usage != new_usage { - cx.emit(TokenUsageUpdated(new_usage)); - } - self.prompt_capabilities_tx.send(new_caps).log_err(); - cx.notify() - } - - pub fn summarization_model(&self) -> Option<&Arc> { - self.summarization_model.as_ref() - } - - pub fn set_summarization_model( - &mut self, - model: Option>, - cx: &mut Context, - ) { - self.summarization_model = model; - cx.notify() - } - - pub fn completion_mode(&self) -> CompletionMode { - self.completion_mode - } - - pub fn set_completion_mode(&mut self, mode: CompletionMode, cx: &mut Context) { - let old_usage = self.latest_token_usage(); - self.completion_mode = mode; - let new_usage = self.latest_token_usage(); - if old_usage != new_usage { - cx.emit(TokenUsageUpdated(new_usage)); - } - cx.notify() - } - - #[cfg(any(test, feature = "test-support"))] - pub fn last_message(&self) -> Option { - if let Some(message) = self.pending_message.clone() { - Some(Message::Agent(message)) - } else { - self.messages.last().cloned() - } - } - - pub fn add_default_tools( - &mut self, - environment: Rc, - cx: &mut Context, - ) { - let language_registry = self.project.read(cx).languages().clone(); - self.add_tool(CopyPathTool::new(self.project.clone())); - self.add_tool(CreateDirectoryTool::new(self.project.clone())); - self.add_tool(DeletePathTool::new( - self.project.clone(), - self.action_log.clone(), - )); - self.add_tool(DiagnosticsTool::new(self.project.clone())); - self.add_tool(EditFileTool::new( - self.project.clone(), - cx.weak_entity(), - language_registry, - Templates::new(), - )); - self.add_tool(FetchTool::new(self.project.read(cx).client().http_client())); - self.add_tool(FindPathTool::new(self.project.clone())); - self.add_tool(GrepTool::new(self.project.clone())); - self.add_tool(ListDirectoryTool::new(self.project.clone())); - self.add_tool(MovePathTool::new(self.project.clone())); - self.add_tool(NowTool); - self.add_tool(OpenTool::new(self.project.clone())); - self.add_tool(ReadFileTool::new( - cx.weak_entity(), - self.project.clone(), - self.action_log.clone(), - )); - self.add_tool(TerminalTool::new(self.project.clone(), environment)); - self.add_tool(ThinkingTool); - self.add_tool(WebSearchTool); - } - - pub fn add_tool(&mut self, tool: T) { - self.tools.insert(T::name().into(), tool.erase()); - } - - pub fn remove_tool(&mut self, name: &str) -> bool { - self.tools.remove(name).is_some() - } - - pub fn profile(&self) -> &AgentProfileId { - &self.profile_id - } - - pub fn set_profile(&mut self, profile_id: AgentProfileId, cx: &mut Context) { - if self.profile_id == profile_id { - return; - } - - self.profile_id = profile_id; - - // Swap to the profile's preferred model when available. - if let Some(model) = Self::resolve_profile_model(&self.profile_id, cx) { - self.set_model(model, cx); - } - } - - pub fn cancel(&mut self, cx: &mut Context) { - if let Some(running_turn) = self.running_turn.take() { - running_turn.cancel(); - } - self.flush_pending_message(cx); - } - - fn update_token_usage(&mut self, update: language_model::TokenUsage, cx: &mut Context) { - let Some(last_user_message) = self.last_user_message() else { - return; - }; - - self.request_token_usage - .insert(last_user_message.id.clone(), update); - cx.emit(TokenUsageUpdated(self.latest_token_usage())); - cx.notify(); - } - - pub fn truncate(&mut self, message_id: UserMessageId, cx: &mut Context) -> Result<()> { - self.cancel(cx); - let Some(position) = self.messages.iter().position( - |msg| matches!(msg, Message::User(UserMessage { id, .. }) if id == &message_id), - ) else { - return Err(anyhow!("Message not found")); - }; - - for message in self.messages.drain(position..) { - match message { - Message::User(message) => { - self.request_token_usage.remove(&message.id); - } - Message::Agent(_) | Message::Resume => {} - } - } - self.clear_summary(); - cx.notify(); - Ok(()) - } - - pub fn latest_request_token_usage(&self) -> Option { - let last_user_message = self.last_user_message()?; - let tokens = self.request_token_usage.get(&last_user_message.id)?; - Some(*tokens) - } - - pub fn latest_token_usage(&self) -> Option { - let usage = self.latest_request_token_usage()?; - let model = self.model.clone()?; - Some(acp_thread::TokenUsage { - max_tokens: model.max_token_count_for_mode(self.completion_mode.into()), - used_tokens: usage.total_tokens(), - }) - } - - /// Look up the active profile and resolve its preferred model if one is configured. - fn resolve_profile_model( - profile_id: &AgentProfileId, - cx: &mut Context, - ) -> Option> { - let selection = AgentSettings::get_global(cx) - .profiles - .get(profile_id)? - .default_model - .clone()?; - Self::resolve_model_from_selection(&selection, cx) - } - - /// Translate a stored model selection into the configured model from the registry. - fn resolve_model_from_selection( - selection: &LanguageModelSelection, - cx: &mut Context, - ) -> Option> { - let selected = SelectedModel { - provider: LanguageModelProviderId::from(selection.provider.0.clone()), - model: LanguageModelId::from(selection.model.clone()), - }; - LanguageModelRegistry::global(cx).update(cx, |registry, cx| { - registry - .select_model(&selected, cx) - .map(|configured| configured.model) - }) - } - - pub fn resume( - &mut self, - cx: &mut Context, - ) -> Result>> { - self.messages.push(Message::Resume); - cx.notify(); - - log::debug!("Total messages in thread: {}", self.messages.len()); - self.run_turn(cx) - } - - /// Sending a message results in the model streaming a response, which could include tool calls. - /// After calling tools, the model will stops and waits for any outstanding tool calls to be completed and their results sent. - /// The returned channel will report all the occurrences in which the model stops before erroring or ending its turn. - pub fn send( - &mut self, - id: UserMessageId, - content: impl IntoIterator, - cx: &mut Context, - ) -> Result>> - where - T: Into, - { - let model = self.model().context("No language model configured")?; - - log::info!("Thread::send called with model: {}", model.name().0); - self.advance_prompt_id(); - - let content = content.into_iter().map(Into::into).collect::>(); - log::debug!("Thread::send content: {:?}", content); - - self.messages - .push(Message::User(UserMessage { id, content })); - cx.notify(); - - log::debug!("Total messages in thread: {}", self.messages.len()); - self.run_turn(cx) - } - - #[cfg(feature = "eval")] - pub fn proceed( - &mut self, - cx: &mut Context, - ) -> Result>> { - self.run_turn(cx) - } - - fn run_turn( - &mut self, - cx: &mut Context, - ) -> Result>> { - 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); - self.tool_use_limit_reached = false; - self.clear_summary(); - 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"); - - let turn_result = Self::run_turn_internal(&this, model, &event_stream, cx).await; - _ = this.update(cx, |this, cx| this.flush_pending_message(cx)); - - match turn_result { - Ok(()) => { - log::debug!("Turn execution completed"); - event_stream.send_stop(acp::StopReason::EndTurn); - } - Err(error) => { - log::error!("Turn execution failed: {:?}", error); - match error.downcast::() { - Ok(CompletionError::Refusal) => { - event_stream.send_stop(acp::StopReason::Refusal); - _ = this.update(cx, |this, _| this.messages.truncate(message_ix)); - } - Ok(CompletionError::MaxTokens) => { - event_stream.send_stop(acp::StopReason::MaxTokens); - } - Ok(CompletionError::Other(error)) | Err(error) => { - event_stream.send_error(error); - } - } - } - } - - _ = this.update(cx, |this, _| this.running_turn.take()); - }), - }); - Ok(events_rx) - } - - async fn run_turn_internal( - this: &WeakEntity, - model: Arc, - 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))??; - - telemetry::event!( - "Agent Thread Completion", - thread_id = this.read_with(cx, |this, _| this.id.to_string())?, - prompt_id = this.read_with(cx, |this, _| this.prompt_id.to_string())?, - model = model.telemetry_id(), - model_provider = model.provider_id().to_string(), - attempt - ); - - log::debug!("Calling model.stream_completion, attempt {}", attempt); - - let (mut events, mut error) = match model.stream_completion(request, cx).await { - Ok(events) => (events, None), - Err(err) => (stream::empty().boxed(), Some(err)), - }; - let mut tool_results = FuturesUnordered::new(); - while let Some(event) = events.next().await { - log::trace!("Received completion event: {:?}", event); - match event { - Ok(event) => { - tool_results.extend(this.update(cx, |this, cx| { - this.handle_completion_event(event, event_stream, cx) - })??); - } - Err(err) => { - error = Some(err); - break; - } - } - } - - let end_turn = tool_results.is_empty(); - while let Some(tool_result) = tool_results.next().await { - log::debug!("Tool finished {:?}", tool_result); - - event_stream.update_tool_call_fields( - &tool_result.tool_use_id, - acp::ToolCallUpdateFields::new() - .status(if tool_result.is_error { - acp::ToolCallStatus::Failed - } else { - acp::ToolCallStatus::Completed - }) - .raw_output(tool_result.output.clone()), - ); - this.update(cx, |this, _cx| { - this.pending_message() - .tool_results - .insert(tool_result.tool_use_id.clone(), tool_result); - })?; - } - - this.update(cx, |this, cx| { - this.flush_pending_message(cx); - if this.title.is_none() && this.pending_title_generation.is_none() { - this.generate_title(cx); - } - })?; - - if let Some(error) = error { - attempt += 1; - let retry = this.update(cx, |this, cx| { - let user_store = this.user_store.read(cx); - this.handle_completion_error(error, attempt, user_store.plan()) - })??; - 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; - } - } - } - - fn handle_completion_error( - &mut self, - error: LanguageModelCompletionError, - attempt: u8, - plan: Option, - ) -> Result { - let Some(model) = self.model.as_ref() else { - return Err(anyhow!(error)); - }; - - let auto_retry = if model.provider_id() == ZED_CLOUD_PROVIDER_ID { - match plan { - Some(Plan::V2(_)) => true, - Some(Plan::V1(_)) => self.completion_mode == CompletionMode::Burn, - None => false, - } - } else { - true - }; - - if !auto_retry { - 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, - }; - - if attempt > max_attempts { - return Err(anyhow!(error)); - } - - let delay = match &strategy { - RetryStrategy::ExponentialBackoff { initial_delay, .. } => { - let delay_secs = initial_delay.as_secs() * 2u64.pow((attempt - 1) as u32); - Duration::from_secs(delay_secs) - } - RetryStrategy::Fixed { delay, .. } => *delay, - }; - log::debug!("Retry attempt {attempt} with delay {delay:?}"); - - Ok(acp_thread::RetryStatus { - last_error: error.to_string().into(), - attempt: attempt as usize, - max_attempts: max_attempts as usize, - started_at: Instant::now(), - duration: delay, - }) - } - - /// A helper method that's called on every streamed completion event. - /// Returns an optional tool result task, which the main agentic loop will - /// send back to the model when it resolves. - fn handle_completion_event( - &mut self, - event: LanguageModelCompletionEvent, - event_stream: &ThreadEventStream, - cx: &mut Context, - ) -> Result>> { - log::trace!("Handling streamed completion event: {:?}", event); - use LanguageModelCompletionEvent::*; - - match event { - StartMessage { .. } => { - self.flush_pending_message(cx); - self.pending_message = Some(AgentMessage::default()); - } - Text(new_text) => self.handle_text_event(new_text, event_stream, cx), - Thinking { text, signature } => { - self.handle_thinking_event(text, signature, event_stream, cx) - } - RedactedThinking { data } => self.handle_redacted_thinking_event(data, cx), - ReasoningDetails(details) => { - let last_message = self.pending_message(); - // Store the last non-empty reasoning_details (overwrites earlier ones) - // This ensures we keep the encrypted reasoning with signatures, not the early text reasoning - if let serde_json::Value::Array(ref arr) = details { - if !arr.is_empty() { - last_message.reasoning_details = Some(details); - } - } else { - last_message.reasoning_details = Some(details); - } - } - ToolUse(tool_use) => { - return Ok(self.handle_tool_use_event(tool_use, event_stream, cx)); - } - ToolUseJsonParseError { - id, - tool_name, - raw_input, - json_parse_error, - } => { - return Ok(Some(Task::ready( - self.handle_tool_use_json_parse_error_event( - id, - tool_name, - raw_input, - json_parse_error, - ), - ))); - } - UsageUpdate(usage) => { - telemetry::event!( - "Agent Thread Completion Usage Updated", - thread_id = self.id.to_string(), - prompt_id = self.prompt_id.to_string(), - model = self.model.as_ref().map(|m| m.telemetry_id()), - model_provider = self.model.as_ref().map(|m| m.provider_id().to_string()), - input_tokens = usage.input_tokens, - output_tokens = usage.output_tokens, - cache_creation_input_tokens = usage.cache_creation_input_tokens, - cache_read_input_tokens = usage.cache_read_input_tokens, - ); - self.update_token_usage(usage, cx); - } - UsageUpdated { amount, limit } => { - self.update_model_request_usage(amount, limit, cx); - } - ToolUseLimitReached => { - self.tool_use_limit_reached = true; - } - Stop(StopReason::Refusal) => return Err(CompletionError::Refusal.into()), - Stop(StopReason::MaxTokens) => return Err(CompletionError::MaxTokens.into()), - Stop(StopReason::ToolUse | StopReason::EndTurn) => {} - Started | Queued { .. } => {} - } - - Ok(None) - } - - fn handle_text_event( - &mut self, - new_text: String, - event_stream: &ThreadEventStream, - cx: &mut Context, - ) { - event_stream.send_text(&new_text); - - let last_message = self.pending_message(); - if let Some(AgentMessageContent::Text(text)) = last_message.content.last_mut() { - text.push_str(&new_text); - } else { - last_message - .content - .push(AgentMessageContent::Text(new_text)); - } - - cx.notify(); - } - - fn handle_thinking_event( - &mut self, - new_text: String, - new_signature: Option, - event_stream: &ThreadEventStream, - cx: &mut Context, - ) { - event_stream.send_thinking(&new_text); - - let last_message = self.pending_message(); - if let Some(AgentMessageContent::Thinking { text, signature }) = - last_message.content.last_mut() - { - text.push_str(&new_text); - *signature = new_signature.or(signature.take()); - } else { - last_message.content.push(AgentMessageContent::Thinking { - text: new_text, - signature: new_signature, - }); - } - - cx.notify(); - } - - fn handle_redacted_thinking_event(&mut self, data: String, cx: &mut Context) { - let last_message = self.pending_message(); - last_message - .content - .push(AgentMessageContent::RedactedThinking(data)); - cx.notify(); - } - - fn handle_tool_use_event( - &mut self, - tool_use: LanguageModelToolUse, - event_stream: &ThreadEventStream, - cx: &mut Context, - ) -> Option> { - cx.notify(); - - let tool = self.tool(tool_use.name.as_ref()); - let mut title = SharedString::from(&tool_use.name); - let mut kind = acp::ToolKind::Other; - if let Some(tool) = tool.as_ref() { - title = tool.initial_title(tool_use.input.clone(), cx); - kind = tool.kind(); - } - - // Ensure the last message ends in the current tool use - let last_message = self.pending_message(); - let push_new_tool_use = last_message.content.last_mut().is_none_or(|content| { - if let AgentMessageContent::ToolUse(last_tool_use) = content { - if last_tool_use.id == tool_use.id { - *last_tool_use = tool_use.clone(); - false - } else { - true - } - } else { - true - } - }); - - if push_new_tool_use { - event_stream.send_tool_call( - &tool_use.id, - &tool_use.name, - title, - kind, - tool_use.input.clone(), - ); - last_message - .content - .push(AgentMessageContent::ToolUse(tool_use.clone())); - } else { - event_stream.update_tool_call_fields( - &tool_use.id, - acp::ToolCallUpdateFields::new() - .title(title.as_str()) - .kind(kind) - .raw_input(tool_use.input.clone()), - ); - } - - if !tool_use.is_input_complete { - return None; - } - - let Some(tool) = tool else { - let content = format!("No tool named {} exists", tool_use.name); - return Some(Task::ready(LanguageModelToolResult { - content: LanguageModelToolResultContent::Text(Arc::from(content)), - tool_use_id: tool_use.id, - tool_name: tool_use.name, - is_error: true, - output: None, - })); - }; - - let fs = self.project.read(cx).fs().clone(); - let tool_event_stream = - ToolCallEventStream::new(tool_use.id.clone(), event_stream.clone(), Some(fs)); - tool_event_stream.update_fields( - acp::ToolCallUpdateFields::new().status(acp::ToolCallStatus::InProgress), - ); - 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); - Some(cx.foreground_executor().spawn(async move { - let tool_result = tool_result.await.and_then(|output| { - if let LanguageModelToolResultContent::Image(_) = &output.llm_output - && !supports_images - { - return Err(anyhow!( - "Attempted to read an image, but this model doesn't support it.", - )); - } - Ok(output) - }); - - match tool_result { - Ok(output) => LanguageModelToolResult { - tool_use_id: tool_use.id, - tool_name: tool_use.name, - is_error: false, - content: output.llm_output, - output: Some(output.raw_output), - }, - Err(error) => LanguageModelToolResult { - tool_use_id: tool_use.id, - tool_name: tool_use.name, - is_error: true, - content: LanguageModelToolResultContent::Text(Arc::from(error.to_string())), - output: Some(error.to_string().into()), - }, - } - })) - } - - fn handle_tool_use_json_parse_error_event( - &mut self, - tool_use_id: LanguageModelToolUseId, - tool_name: Arc, - raw_input: Arc, - json_parse_error: String, - ) -> LanguageModelToolResult { - let tool_output = format!("Error parsing input JSON: {json_parse_error}"); - LanguageModelToolResult { - tool_use_id, - tool_name, - is_error: true, - content: LanguageModelToolResultContent::Text(tool_output.into()), - output: Some(serde_json::Value::String(raw_input.to_string())), - } - } - - fn update_model_request_usage(&self, amount: usize, limit: UsageLimit, cx: &mut Context) { - self.project - .read(cx) - .user_store() - .update(cx, |user_store, cx| { - user_store.update_model_request_usage( - ModelRequestUsage(RequestUsage { - amount: amount as i32, - limit, - }), - cx, - ) - }); - } - - pub fn title(&self) -> SharedString { - self.title.clone().unwrap_or("New Thread".into()) - } - - pub fn is_generating_summary(&self) -> bool { - self.pending_summary_generation.is_some() - } - - pub fn summary(&mut self, cx: &mut Context) -> Shared>> { - if let Some(summary) = self.summary.as_ref() { - return Task::ready(Some(summary.clone())).shared(); - } - if let Some(task) = self.pending_summary_generation.clone() { - return task; - } - let Some(model) = self.summarization_model.clone() else { - log::error!("No summarization model available"); - return Task::ready(None).shared(); - }; - let mut request = LanguageModelRequest { - intent: Some(CompletionIntent::ThreadContextSummarization), - temperature: AgentSettings::temperature_for_model(&model, cx), - ..Default::default() - }; - - for message in &self.messages { - request.messages.extend(message.to_request()); - } - - request.messages.push(LanguageModelRequestMessage { - role: Role::User, - content: vec![SUMMARIZE_THREAD_DETAILED_PROMPT.into()], - cache: false, - reasoning_details: None, - }); - - let task = cx - .spawn(async move |this, cx| { - let mut summary = String::new(); - let mut messages = model.stream_completion(request, cx).await.log_err()?; - while let Some(event) = messages.next().await { - let event = event.log_err()?; - let text = match event { - LanguageModelCompletionEvent::Text(text) => text, - LanguageModelCompletionEvent::UsageUpdated { amount, limit } => { - this.update(cx, |thread, cx| { - thread.update_model_request_usage(amount, limit, cx); - }) - .ok()?; - continue; - } - _ => continue, - }; - - let mut lines = text.lines(); - summary.extend(lines.next()); - } - - log::debug!("Setting summary: {}", summary); - let summary = SharedString::from(summary); - - this.update(cx, |this, cx| { - this.summary = Some(summary.clone()); - this.pending_summary_generation = None; - cx.notify() - }) - .ok()?; - - Some(summary) - }) - .shared(); - self.pending_summary_generation = Some(task.clone()); - task - } - - fn generate_title(&mut self, cx: &mut Context) { - let Some(model) = self.summarization_model.clone() else { - return; - }; - - log::debug!( - "Generating title with model: {:?}", - self.summarization_model.as_ref().map(|model| model.name()) - ); - let mut request = LanguageModelRequest { - intent: Some(CompletionIntent::ThreadSummarization), - temperature: AgentSettings::temperature_for_model(&model, cx), - ..Default::default() - }; - - for message in &self.messages { - request.messages.extend(message.to_request()); - } - - request.messages.push(LanguageModelRequestMessage { - role: Role::User, - content: vec![SUMMARIZE_THREAD_PROMPT.into()], - cache: false, - reasoning_details: None, - }); - self.pending_title_generation = Some(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::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(); - } - } - - fn clear_summary(&mut self) { - self.summary = None; - self.pending_summary_generation = None; - } - - fn last_user_message(&self) -> Option<&UserMessage> { - self.messages - .iter() - .rev() - .find_map(|message| match message { - Message::User(user_message) => Some(user_message), - Message::Agent(_) => None, - Message::Resume => None, - }) - } - - fn pending_message(&mut self) -> &mut AgentMessage { - self.pending_message.get_or_insert_default() - } - - fn flush_pending_message(&mut self, cx: &mut Context) { - let Some(mut message) = self.pending_message.take() else { - return; - }; - - if message.content.is_empty() { - return; - } - - for content in &message.content { - let AgentMessageContent::ToolUse(tool_use) = content else { - continue; - }; - - if !message.tool_results.contains_key(&tool_use.id) { - message.tool_results.insert( - tool_use.id.clone(), - LanguageModelToolResult { - tool_use_id: tool_use.id.clone(), - tool_name: tool_use.name.clone(), - is_error: true, - content: LanguageModelToolResultContent::Text(TOOL_CANCELED_MESSAGE.into()), - output: None, - }, - ); - } - } - - self.messages.push(Message::Agent(message)); - self.updated_at = Utc::now(); - self.clear_summary(); - cx.notify() - } - - pub(crate) fn build_completion_request( - &self, - completion_intent: CompletionIntent, - cx: &App, - ) -> Result { - let model = self.model().context("No language model configured")?; - let tools = if let Some(turn) = self.running_turn.as_ref() { - turn.tools - .iter() - .filter_map(|(tool_name, tool)| { - log::trace!("Including tool: {}", tool_name); - Some(LanguageModelRequestTool { - name: tool_name.to_string(), - description: tool.description().to_string(), - input_schema: tool.input_schema(model.tool_input_format()).log_err()?, - }) - }) - .collect::>() - } else { - Vec::new() - }; - - log::debug!("Building completion request"); - log::debug!("Completion intent: {:?}", completion_intent); - log::debug!("Completion mode: {:?}", self.completion_mode); - - let available_tools: Vec<_> = self - .running_turn - .as_ref() - .map(|turn| turn.tools.keys().cloned().collect()) - .unwrap_or_default(); - - log::debug!("Request includes {} tools", available_tools.len()); - let messages = self.build_request_messages(available_tools, cx); - log::debug!("Request will include {} messages", messages.len()); - - let request = LanguageModelRequest { - thread_id: Some(self.id.to_string()), - prompt_id: Some(self.prompt_id.to_string()), - intent: Some(completion_intent), - mode: Some(self.completion_mode.into()), - messages, - tools, - tool_choice: None, - stop: Vec::new(), - temperature: AgentSettings::temperature_for_model(model, cx), - thinking_allowed: true, - }; - - log::debug!("Completion request built successfully"); - 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() - } - } - - let mut tools = self - .tools - .iter() - .filter_map(|(tool_name, tool)| { - if tool.supports_provider(&model.provider_id()) - && profile.is_tool_enabled(tool_name) - { - Some((truncate(tool_name), tool.clone())) - } else { - None - } - }) - .collect::>(); - - let mut context_server_tools = Vec::new(); - let mut seen_tools = tools.keys().cloned().collect::>(); - let mut duplicate_tool_names = HashSet::default(); - for (server_id, server_tools) in self.context_server_registry.read(cx).servers() { - for (tool_name, tool) in server_tools { - if profile.is_context_server_tool_enabled(&server_id.0, &tool_name) { - let tool_name = truncate(tool_name); - if !seen_tools.insert(tool_name.clone()) { - duplicate_tool_names.insert(tool_name.clone()); - } - context_server_tools.push((server_id.clone(), tool_name, tool.clone())); - } - } - } - - // When there are duplicate tool names, disambiguate by prefixing them - // with the server ID. In the rare case there isn't enough space for the - // disambiguated tool name, keep only the last tool with this name. - for (server_id, tool_name, tool) in context_server_tools { - if duplicate_tool_names.contains(&tool_name) { - let available = MAX_TOOL_NAME_LENGTH.saturating_sub(tool_name.len()); - if available >= 2 { - let mut disambiguated = server_id.0.to_string(); - disambiguated.truncate(available - 1); - disambiguated.push('_'); - disambiguated.push_str(&tool_name); - tools.insert(disambiguated.into(), tool.clone()); - } else { - tools.insert(tool_name, tool.clone()); - } - } else { - tools.insert(tool_name, tool.clone()); - } - } - - tools - } - - fn tool(&self, name: &str) -> Option> { - self.running_turn.as_ref()?.tools.get(name).cloned() - } - - fn build_request_messages( - &self, - available_tools: Vec, - cx: &App, - ) -> Vec { - log::trace!( - "Building request messages from {} thread messages", - self.messages.len() - ); - - let system_prompt = SystemPromptTemplate { - project: self.project_context.read(cx), - available_tools, - model_name: self.model.as_ref().map(|m| m.name().0.to_string()), - } - .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, - reasoning_details: None, - }]; - 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()); - } - - messages - } - - pub fn to_markdown(&self) -> String { - let mut markdown = String::new(); - for (ix, message) in self.messages.iter().enumerate() { - if ix > 0 { - markdown.push('\n'); - } - markdown.push_str(&message.to_markdown()); - } - - if let Some(message) = self.pending_message.as_ref() { - markdown.push('\n'); - markdown.push_str(&message.to_markdown()); - } - - markdown - } - - fn advance_prompt_id(&mut self) { - self.prompt_id = PromptId::new(); - } - - fn retry_strategy_for(error: &LanguageModelCompletionError) -> Option { - use LanguageModelCompletionError::*; - use http_client::StatusCode; - - // General strategy here: - // - If retrying won't help (e.g. invalid API key or payload too large), return None so we don't retry at all. - // - If it's a time-based issue (e.g. server overloaded, rate limit exceeded), retry up to 4 times with exponential backoff. - // - If it's an issue that *might* be fixed by retrying (e.g. internal server error), retry up to 3 times. - match error { - HttpResponseError { - status_code: StatusCode::TOO_MANY_REQUESTS, - .. - } => Some(RetryStrategy::ExponentialBackoff { - initial_delay: BASE_RETRY_DELAY, - max_attempts: MAX_RETRY_ATTEMPTS, - }), - ServerOverloaded { retry_after, .. } | RateLimitExceeded { retry_after, .. } => { - Some(RetryStrategy::Fixed { - delay: retry_after.unwrap_or(BASE_RETRY_DELAY), - max_attempts: MAX_RETRY_ATTEMPTS, - }) - } - UpstreamProviderError { - status, - retry_after, - .. - } => match *status { - StatusCode::TOO_MANY_REQUESTS | StatusCode::SERVICE_UNAVAILABLE => { - Some(RetryStrategy::Fixed { - delay: retry_after.unwrap_or(BASE_RETRY_DELAY), - max_attempts: MAX_RETRY_ATTEMPTS, - }) - } - StatusCode::INTERNAL_SERVER_ERROR => Some(RetryStrategy::Fixed { - delay: retry_after.unwrap_or(BASE_RETRY_DELAY), - // Internal Server Error could be anything, retry up to 3 times. - max_attempts: 3, - }), - status => { - // There is no StatusCode variant for the unofficial HTTP 529 ("The service is overloaded"), - // but we frequently get them in practice. See https://http.dev/529 - if status.as_u16() == 529 { - Some(RetryStrategy::Fixed { - delay: retry_after.unwrap_or(BASE_RETRY_DELAY), - max_attempts: MAX_RETRY_ATTEMPTS, - }) - } else { - Some(RetryStrategy::Fixed { - delay: retry_after.unwrap_or(BASE_RETRY_DELAY), - max_attempts: 2, - }) - } - } - }, - ApiInternalServerError { .. } => Some(RetryStrategy::Fixed { - delay: BASE_RETRY_DELAY, - max_attempts: 3, - }), - ApiReadResponseError { .. } - | HttpSend { .. } - | DeserializeResponse { .. } - | BadRequestFormat { .. } => Some(RetryStrategy::Fixed { - delay: BASE_RETRY_DELAY, - max_attempts: 3, - }), - // Retrying these errors definitely shouldn't help. - HttpResponseError { - status_code: - StatusCode::PAYLOAD_TOO_LARGE | StatusCode::FORBIDDEN | StatusCode::UNAUTHORIZED, - .. - } - | AuthenticationError { .. } - | PermissionError { .. } - | NoApiKey { .. } - | ApiEndpointNotFound { .. } - | PromptTooLarge { .. } => None, - // These errors might be transient, so retry them - SerializeRequest { .. } | BuildRequestBody { .. } => Some(RetryStrategy::Fixed { - delay: BASE_RETRY_DELAY, - max_attempts: 1, - }), - // Retry all other 4xx and 5xx errors once. - HttpResponseError { status_code, .. } - if status_code.is_client_error() || status_code.is_server_error() => - { - Some(RetryStrategy::Fixed { - delay: BASE_RETRY_DELAY, - max_attempts: 3, - }) - } - Other(err) - if err.is::() - || err.is::() => - { - // Retrying won't help for Payment Required or Model Request Limit errors (where - // the user must upgrade to usage-based billing to get more requests, or else wait - // for a significant amount of time for the request limit to reset). - None - } - // Conservatively assume that any other errors are non-retryable - HttpResponseError { .. } | Other(..) => Some(RetryStrategy::Fixed { - delay: BASE_RETRY_DELAY, - max_attempts: 2, - }), - } - } -} - -struct RunningTurn { - /// Holds the task that handles agent interaction until the end of the turn. - /// Survives across multiple requests as the model performs tool calls and - /// we run tools, report their results. - _task: Task<()>, - /// 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 { - fn cancel(self) { - log::debug!("Cancelling in progress turn"); - self.event_stream.send_canceled(); - } -} - -pub struct TokenUsageUpdated(pub Option); - -impl EventEmitter for Thread {} - -pub struct TitleUpdated; - -impl EventEmitter for Thread {} - -pub trait AgentTool -where - Self: 'static + Sized, -{ - type Input: for<'de> Deserialize<'de> + Serialize + JsonSchema; - type Output: for<'de> Deserialize<'de> + Serialize + Into; - - fn name() -> &'static str; - - fn description() -> SharedString { - let schema = schemars::schema_for!(Self::Input); - SharedString::new( - schema - .get("description") - .and_then(|description| description.as_str()) - .unwrap_or_default(), - ) - } - - fn kind() -> acp::ToolKind; - - /// The initial tool title to display. Can be updated during the tool run. - fn initial_title( - &self, - input: Result, - cx: &mut App, - ) -> SharedString; - - /// Returns the JSON schema that describes the tool's input. - fn input_schema(format: LanguageModelToolSchemaFormat) -> Schema { - language_model::tool_schema::root_schema_for::(format) - } - - /// Some tools rely on a provider for the underlying billing or other reasons. - /// Allow the tool to check if they are compatible, or should be filtered out. - fn supports_provider(_provider: &LanguageModelProviderId) -> bool { - true - } - - /// Runs the tool with the provided input. - fn run( - self: Arc, - input: Self::Input, - event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task>; - - /// Emits events for a previous execution of the tool. - fn replay( - &self, - _input: Self::Input, - _output: Self::Output, - _event_stream: ToolCallEventStream, - _cx: &mut App, - ) -> Result<()> { - Ok(()) - } - - fn erase(self) -> Arc { - Arc::new(Erased(Arc::new(self))) - } -} - -pub struct Erased(T); - -pub struct AgentToolOutput { - pub llm_output: LanguageModelToolResultContent, - pub raw_output: serde_json::Value, -} - -pub trait AnyAgentTool { - fn name(&self) -> SharedString; - fn description(&self) -> SharedString; - fn kind(&self) -> acp::ToolKind; - fn initial_title(&self, input: serde_json::Value, _cx: &mut App) -> SharedString; - fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result; - fn supports_provider(&self, _provider: &LanguageModelProviderId) -> bool { - true - } - fn run( - self: Arc, - input: serde_json::Value, - event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task>; - fn replay( - &self, - input: serde_json::Value, - output: serde_json::Value, - event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Result<()>; -} - -impl AnyAgentTool for Erased> -where - T: AgentTool, -{ - fn name(&self) -> SharedString { - T::name().into() - } - - fn description(&self) -> SharedString { - T::description() - } - - fn kind(&self) -> agent_client_protocol::ToolKind { - T::kind() - } - - fn initial_title(&self, input: serde_json::Value, _cx: &mut App) -> SharedString { - let parsed_input = serde_json::from_value(input.clone()).map_err(|_| input); - self.0.initial_title(parsed_input, _cx) - } - - fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result { - let mut json = serde_json::to_value(T::input_schema(format))?; - language_model::tool_schema::adapt_schema_to_format(&mut json, format)?; - Ok(json) - } - - fn supports_provider(&self, provider: &LanguageModelProviderId) -> bool { - T::supports_provider(provider) - } - - fn run( - self: Arc, - input: serde_json::Value, - event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - cx.spawn(async move |cx| { - let input = serde_json::from_value(input)?; - let output = cx - .update(|cx| self.0.clone().run(input, event_stream, cx))? - .await?; - let raw_output = serde_json::to_value(&output)?; - Ok(AgentToolOutput { - llm_output: output.into(), - raw_output, - }) - }) - } - - fn replay( - &self, - input: serde_json::Value, - output: serde_json::Value, - event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Result<()> { - let input = serde_json::from_value(input)?; - let output = serde_json::from_value(output)?; - self.0.replay(input, output, event_stream, cx) - } -} - -#[derive(Clone)] -struct ThreadEventStream(mpsc::UnboundedSender>); - -impl ThreadEventStream { - fn send_user_message(&self, message: &UserMessage) { - self.0 - .unbounded_send(Ok(ThreadEvent::UserMessage(message.clone()))) - .ok(); - } - - fn send_text(&self, text: &str) { - self.0 - .unbounded_send(Ok(ThreadEvent::AgentText(text.to_string()))) - .ok(); - } - - fn send_thinking(&self, text: &str) { - self.0 - .unbounded_send(Ok(ThreadEvent::AgentThinking(text.to_string()))) - .ok(); - } - - fn send_tool_call( - &self, - id: &LanguageModelToolUseId, - tool_name: &str, - title: SharedString, - kind: acp::ToolKind, - input: serde_json::Value, - ) { - self.0 - .unbounded_send(Ok(ThreadEvent::ToolCall(Self::initial_tool_call( - id, - tool_name, - title.to_string(), - kind, - input, - )))) - .ok(); - } - - fn initial_tool_call( - id: &LanguageModelToolUseId, - tool_name: &str, - title: String, - kind: acp::ToolKind, - input: serde_json::Value, - ) -> acp::ToolCall { - acp::ToolCall::new(id.to_string(), title) - .kind(kind) - .raw_input(input) - .meta(acp::Meta::from_iter([( - "tool_name".into(), - tool_name.into(), - )])) - } - - fn update_tool_call_fields( - &self, - tool_use_id: &LanguageModelToolUseId, - fields: acp::ToolCallUpdateFields, - ) { - self.0 - .unbounded_send(Ok(ThreadEvent::ToolCallUpdate( - acp::ToolCallUpdate::new(tool_use_id.to_string(), fields).into(), - ))) - .ok(); - } - - fn send_retry(&self, status: acp_thread::RetryStatus) { - self.0.unbounded_send(Ok(ThreadEvent::Retry(status))).ok(); - } - - fn send_stop(&self, reason: acp::StopReason) { - self.0.unbounded_send(Ok(ThreadEvent::Stop(reason))).ok(); - } - - fn send_canceled(&self) { - self.0 - .unbounded_send(Ok(ThreadEvent::Stop(acp::StopReason::Cancelled))) - .ok(); - } - - fn send_error(&self, error: impl Into) { - self.0.unbounded_send(Err(error.into())).ok(); - } -} - -#[derive(Clone)] -pub struct ToolCallEventStream { - tool_use_id: LanguageModelToolUseId, - stream: ThreadEventStream, - fs: Option>, -} - -impl ToolCallEventStream { - #[cfg(any(test, feature = "test-support"))] - pub fn test() -> (Self, ToolCallEventStreamReceiver) { - let (events_tx, events_rx) = mpsc::unbounded::>(); - - let stream = ToolCallEventStream::new("test_id".into(), ThreadEventStream(events_tx), None); - - (stream, ToolCallEventStreamReceiver(events_rx)) - } - - fn new( - tool_use_id: LanguageModelToolUseId, - stream: ThreadEventStream, - fs: Option>, - ) -> Self { - Self { - tool_use_id, - stream, - fs, - } - } - - pub fn update_fields(&self, fields: acp::ToolCallUpdateFields) { - self.stream - .update_tool_call_fields(&self.tool_use_id, fields); - } - - pub fn update_diff(&self, diff: Entity) { - self.stream - .0 - .unbounded_send(Ok(ThreadEvent::ToolCallUpdate( - acp_thread::ToolCallUpdateDiff { - id: acp::ToolCallId::new(self.tool_use_id.to_string()), - diff, - } - .into(), - ))) - .ok(); - } - - pub fn authorize(&self, title: impl Into, cx: &mut App) -> Task> { - if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions { - return Task::ready(Ok(())); - } - - let (response_tx, response_rx) = oneshot::channel(); - self.stream - .0 - .unbounded_send(Ok(ThreadEvent::ToolCallAuthorization( - ToolCallAuthorization { - tool_call: acp::ToolCallUpdate::new( - self.tool_use_id.to_string(), - acp::ToolCallUpdateFields::new().title(title.into()), - ), - options: vec![ - acp::PermissionOption::new( - acp::PermissionOptionId::new("always_allow"), - "Always Allow", - acp::PermissionOptionKind::AllowAlways, - ), - acp::PermissionOption::new( - acp::PermissionOptionId::new("allow"), - "Allow", - acp::PermissionOptionKind::AllowOnce, - ), - acp::PermissionOption::new( - acp::PermissionOptionId::new("deny"), - "Deny", - acp::PermissionOptionKind::RejectOnce, - ), - ], - response: response_tx, - }, - ))) - .ok(); - let fs = self.fs.clone(); - cx.spawn(async move |cx| match response_rx.await?.0.as_ref() { - "always_allow" => { - if let Some(fs) = fs.clone() { - cx.update(|cx| { - update_settings_file(fs, cx, |settings, _| { - settings - .agent - .get_or_insert_default() - .set_always_allow_tool_actions(true); - }); - })?; - } - - Ok(()) - } - "allow" => Ok(()), - _ => Err(anyhow!("Permission to run tool denied by user")), - }) - } -} - -#[cfg(any(test, feature = "test-support"))] -pub struct ToolCallEventStreamReceiver(mpsc::UnboundedReceiver>); - -#[cfg(any(test, feature = "test-support"))] -impl ToolCallEventStreamReceiver { - pub async fn expect_authorization(&mut self) -> ToolCallAuthorization { - let event = self.0.next().await; - if let Some(Ok(ThreadEvent::ToolCallAuthorization(auth))) = event { - auth - } else { - panic!("Expected ToolCallAuthorization but got: {:?}", event); - } - } - - 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( - update, - )))) = event - { - update.terminal - } else { - panic!("Expected terminal but got: {:?}", event); - } - } -} - -#[cfg(any(test, feature = "test-support"))] -impl std::ops::Deref for ToolCallEventStreamReceiver { - type Target = mpsc::UnboundedReceiver>; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -#[cfg(any(test, feature = "test-support"))] -impl std::ops::DerefMut for ToolCallEventStreamReceiver { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl From<&str> for UserMessageContent { - fn from(text: &str) -> Self { - Self::Text(text.into()) - } -} - -impl UserMessageContent { - pub fn from_content_block(value: acp::ContentBlock, path_style: PathStyle) -> Self { - match value { - acp::ContentBlock::Text(text_content) => Self::Text(text_content.text), - acp::ContentBlock::Image(image_content) => Self::Image(convert_image(image_content)), - acp::ContentBlock::Audio(_) => { - // TODO - Self::Text("[audio]".to_string()) - } - acp::ContentBlock::ResourceLink(resource_link) => { - match MentionUri::parse(&resource_link.uri, path_style) { - Ok(uri) => Self::Mention { - uri, - content: String::new(), - }, - Err(err) => { - log::error!("Failed to parse mention link: {}", err); - Self::Text(format!("[{}]({})", resource_link.name, resource_link.uri)) - } - } - } - acp::ContentBlock::Resource(resource) => match resource.resource { - acp::EmbeddedResourceResource::TextResourceContents(resource) => { - match MentionUri::parse(&resource.uri, path_style) { - Ok(uri) => Self::Mention { - uri, - content: resource.text, - }, - Err(err) => { - log::error!("Failed to parse mention link: {}", err); - Self::Text( - MarkdownCodeBlock { - tag: &resource.uri, - text: &resource.text, - } - .to_string(), - ) - } - } - } - acp::EmbeddedResourceResource::BlobResourceContents(_) => { - // TODO - Self::Text("[blob]".to_string()) - } - other => { - log::warn!("Unexpected content type: {:?}", other); - Self::Text("[unknown]".to_string()) - } - }, - other => { - log::warn!("Unexpected content type: {:?}", other); - Self::Text("[unknown]".to_string()) - } - } - } -} - -impl From for acp::ContentBlock { - fn from(content: UserMessageContent) -> Self { - match content { - UserMessageContent::Text(text) => text.into(), - UserMessageContent::Image(image) => { - acp::ContentBlock::Image(acp::ImageContent::new(image.source, "image/png")) - } - UserMessageContent::Mention { uri, content } => acp::ContentBlock::Resource( - acp::EmbeddedResource::new(acp::EmbeddedResourceResource::TextResourceContents( - acp::TextResourceContents::new(content, uri.to_uri().to_string()), - )), - ), - } - } -} - -fn convert_image(image_content: acp::ImageContent) -> LanguageModelImage { - LanguageModelImage { - source: image_content.data.into(), - // TODO: make this optional? - size: gpui::Size::new(0.into(), 0.into()), - } -} diff --git a/crates/agent/src/tools.rs b/crates/agent/src/tools.rs deleted file mode 100644 index 62a52998a7..0000000000 --- a/crates/agent/src/tools.rs +++ /dev/null @@ -1,98 +0,0 @@ -mod context_server_registry; -mod copy_path_tool; -mod create_directory_tool; -mod delete_path_tool; -mod diagnostics_tool; -mod edit_file_tool; - -mod fetch_tool; -mod find_path_tool; -mod grep_tool; -mod list_directory_tool; -mod move_path_tool; -mod now_tool; -mod open_tool; -mod read_file_tool; - -mod terminal_tool; -mod thinking_tool; -mod web_search_tool; - -use crate::AgentTool; -use language_model::{LanguageModelRequestTool, LanguageModelToolSchemaFormat}; - -pub use context_server_registry::*; -pub use copy_path_tool::*; -pub use create_directory_tool::*; -pub use delete_path_tool::*; -pub use diagnostics_tool::*; -pub use edit_file_tool::*; - -pub use fetch_tool::*; -pub use find_path_tool::*; -pub use grep_tool::*; -pub use list_directory_tool::*; -pub use move_path_tool::*; -pub use now_tool::*; -pub use open_tool::*; -pub use read_file_tool::*; - -pub use terminal_tool::*; -pub use thinking_tool::*; -pub use web_search_tool::*; - -macro_rules! tools { - ($($tool:ty),* $(,)?) => { - /// A list of all built-in tool names - pub fn supported_built_in_tool_names(provider: Option) -> impl Iterator { - [ - $( - (if let Some(provider) = provider.as_ref() { - <$tool>::supports_provider(provider) - } else { - true - }) - .then(|| <$tool>::name().to_string()), - )* - ] - .into_iter() - .flatten() - } - - /// A list of all built-in tools - pub fn built_in_tools() -> impl Iterator { - fn language_model_tool() -> LanguageModelRequestTool { - LanguageModelRequestTool { - name: T::name().to_string(), - description: T::description().to_string(), - input_schema: T::input_schema(LanguageModelToolSchemaFormat::JsonSchema).to_value(), - } - } - [ - $( - language_model_tool::<$tool>(), - )* - ] - .into_iter() - } - }; -} - -tools! { - CopyPathTool, - CreateDirectoryTool, - DeletePathTool, - DiagnosticsTool, - EditFileTool, - FetchTool, - FindPathTool, - GrepTool, - ListDirectoryTool, - MovePathTool, - NowTool, - OpenTool, - ReadFileTool, - TerminalTool, - ThinkingTool, - WebSearchTool, -} diff --git a/crates/agent/src/tools/context_server_registry.rs b/crates/agent/src/tools/context_server_registry.rs deleted file mode 100644 index 03a0ef84e7..0000000000 --- a/crates/agent/src/tools/context_server_registry.rs +++ /dev/null @@ -1,253 +0,0 @@ -use crate::{AgentToolOutput, AnyAgentTool, ToolCallEventStream}; -use agent_client_protocol::ToolKind; -use anyhow::{Result, anyhow, bail}; -use collections::{BTreeMap, HashMap}; -use context_server::ContextServerId; -use gpui::{App, Context, Entity, SharedString, Task}; -use project::context_server_store::{ContextServerStatus, ContextServerStore}; -use std::sync::Arc; -use util::ResultExt; - -pub struct ContextServerRegistry { - server_store: Entity, - registered_servers: HashMap, - _subscription: gpui::Subscription, -} - -struct RegisteredContextServer { - tools: BTreeMap>, - load_tools: Task>, -} - -impl ContextServerRegistry { - pub fn new(server_store: Entity, cx: &mut Context) -> Self { - let mut this = Self { - server_store: server_store.clone(), - registered_servers: HashMap::default(), - _subscription: cx.subscribe(&server_store, Self::handle_context_server_store_event), - }; - for server in server_store.read(cx).running_servers() { - this.reload_tools_for_server(server.id(), cx); - } - this - } - - pub fn tools_for_server( - &self, - server_id: &ContextServerId, - ) -> impl Iterator> { - self.registered_servers - .get(server_id) - .map(|server| server.tools.values()) - .into_iter() - .flatten() - } - - pub fn servers( - &self, - ) -> impl Iterator< - Item = ( - &ContextServerId, - &BTreeMap>, - ), - > { - self.registered_servers - .iter() - .map(|(id, server)| (id, &server.tools)) - } - - fn reload_tools_for_server(&mut self, server_id: ContextServerId, cx: &mut Context) { - let Some(server) = self.server_store.read(cx).get_running_server(&server_id) else { - return; - }; - let Some(client) = server.client() else { - return; - }; - if !client.capable(context_server::protocol::ServerCapability::Tools) { - return; - } - - let registered_server = - self.registered_servers - .entry(server_id.clone()) - .or_insert(RegisteredContextServer { - tools: BTreeMap::default(), - load_tools: Task::ready(Ok(())), - }); - registered_server.load_tools = cx.spawn(async move |this, cx| { - let response = client - .request::(()) - .await; - - this.update(cx, |this, cx| { - let Some(registered_server) = this.registered_servers.get_mut(&server_id) else { - return; - }; - - registered_server.tools.clear(); - if let Some(response) = response.log_err() { - for tool in response.tools { - let tool = Arc::new(ContextServerTool::new( - this.server_store.clone(), - server.id(), - tool, - )); - registered_server.tools.insert(tool.name(), tool); - } - cx.notify(); - } - }) - }); - } - - fn handle_context_server_store_event( - &mut self, - _: Entity, - event: &project::context_server_store::Event, - cx: &mut Context, - ) { - match event { - project::context_server_store::Event::ServerStatusChanged { server_id, status } => { - match status { - ContextServerStatus::Starting => {} - ContextServerStatus::Running => { - self.reload_tools_for_server(server_id.clone(), cx); - } - ContextServerStatus::Stopped | ContextServerStatus::Error(_) => { - self.registered_servers.remove(server_id); - cx.notify(); - } - } - } - } - } -} - -struct ContextServerTool { - store: Entity, - server_id: ContextServerId, - tool: context_server::types::Tool, -} - -impl ContextServerTool { - fn new( - store: Entity, - server_id: ContextServerId, - tool: context_server::types::Tool, - ) -> Self { - Self { - store, - server_id, - tool, - } - } -} - -impl AnyAgentTool for ContextServerTool { - fn name(&self) -> SharedString { - self.tool.name.clone().into() - } - - fn description(&self) -> SharedString { - self.tool.description.clone().unwrap_or_default().into() - } - - fn kind(&self) -> ToolKind { - ToolKind::Other - } - - fn initial_title(&self, _input: serde_json::Value, _cx: &mut App) -> SharedString { - format!("Run MCP tool `{}`", self.tool.name).into() - } - - fn input_schema( - &self, - format: language_model::LanguageModelToolSchemaFormat, - ) -> Result { - let mut schema = self.tool.input_schema.clone(); - language_model::tool_schema::adapt_schema_to_format(&mut schema, format)?; - Ok(match schema { - serde_json::Value::Null => { - serde_json::json!({ "type": "object", "properties": [] }) - } - serde_json::Value::Object(map) if map.is_empty() => { - serde_json::json!({ "type": "object", "properties": [] }) - } - _ => schema, - }) - } - - fn run( - self: Arc, - input: serde_json::Value, - event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - let Some(server) = self.store.read(cx).get_running_server(&self.server_id) else { - return Task::ready(Err(anyhow!("Context server not found"))); - }; - let tool_name = self.tool.name.clone(); - let authorize = event_stream.authorize(self.initial_title(input.clone(), cx), cx); - - cx.spawn(async move |_cx| { - authorize.await?; - - let Some(protocol) = server.client() else { - bail!("Context server not initialized"); - }; - - let arguments = if let serde_json::Value::Object(map) = input { - Some(map.into_iter().collect()) - } else { - None - }; - - log::trace!( - "Running tool: {} with arguments: {:?}", - tool_name, - arguments - ); - let response = protocol - .request::( - context_server::types::CallToolParams { - name: tool_name, - arguments, - meta: None, - }, - ) - .await?; - - let mut result = String::new(); - for content in response.content { - match content { - context_server::types::ToolResponseContent::Text { text } => { - result.push_str(&text); - } - context_server::types::ToolResponseContent::Image { .. } => { - log::warn!("Ignoring image content from tool response"); - } - context_server::types::ToolResponseContent::Audio { .. } => { - log::warn!("Ignoring audio content from tool response"); - } - context_server::types::ToolResponseContent::Resource { .. } => { - log::warn!("Ignoring resource content from tool response"); - } - } - } - Ok(AgentToolOutput { - raw_output: result.clone().into(), - llm_output: result.into(), - }) - }) - } - - fn replay( - &self, - _input: serde_json::Value, - _output: serde_json::Value, - _event_stream: ToolCallEventStream, - _cx: &mut App, - ) -> Result<()> { - Ok(()) - } -} diff --git a/crates/agent/src/tools/copy_path_tool.rs b/crates/agent/src/tools/copy_path_tool.rs deleted file mode 100644 index 236978c78f..0000000000 --- a/crates/agent/src/tools/copy_path_tool.rs +++ /dev/null @@ -1,113 +0,0 @@ -use crate::{AgentTool, ToolCallEventStream}; -use agent_client_protocol::ToolKind; -use anyhow::{Context as _, Result, anyhow}; -use gpui::{App, AppContext, Entity, Task}; -use project::Project; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; -use util::markdown::MarkdownInlineCode; - -/// Copies a file or directory in the project, and returns confirmation that the copy succeeded. -/// Directory contents will be copied recursively. -/// -/// This tool should be used when it's desirable to create a copy of a file or directory without modifying the original. -/// It's much more efficient than doing this by separately reading and then writing the file or directory's contents, so this tool should be preferred over that approach whenever copying is the goal. -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct CopyPathToolInput { - /// The source path of the file or directory to copy. - /// If a directory is specified, its contents will be copied recursively. - /// - /// - /// If the project has the following files: - /// - /// - directory1/a/something.txt - /// - directory2/a/things.txt - /// - directory3/a/other.txt - /// - /// You can copy the first file by providing a source_path of "directory1/a/something.txt" - /// - pub source_path: String, - /// The destination path where the file or directory should be copied to. - /// - /// - /// To copy "directory1/a/something.txt" to "directory2/b/copy.txt", provide a destination_path of "directory2/b/copy.txt" - /// - pub destination_path: String, -} - -pub struct CopyPathTool { - project: Entity, -} - -impl CopyPathTool { - pub fn new(project: Entity) -> Self { - Self { project } - } -} - -impl AgentTool for CopyPathTool { - type Input = CopyPathToolInput; - type Output = String; - - fn name() -> &'static str { - "copy_path" - } - - fn kind() -> ToolKind { - ToolKind::Move - } - - fn initial_title( - &self, - input: Result, - _cx: &mut App, - ) -> ui::SharedString { - if let Ok(input) = input { - let src = MarkdownInlineCode(&input.source_path); - let dest = MarkdownInlineCode(&input.destination_path); - format!("Copy {src} to {dest}").into() - } else { - "Copy path".into() - } - } - - fn run( - self: Arc, - input: Self::Input, - _event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - let copy_task = self.project.update(cx, |project, cx| { - match project - .find_project_path(&input.source_path, cx) - .and_then(|project_path| project.entry_for_path(&project_path, cx)) - { - Some(entity) => match project.find_project_path(&input.destination_path, cx) { - Some(project_path) => project.copy_entry(entity.id, project_path, cx), - None => Task::ready(Err(anyhow!( - "Destination path {} was outside the project.", - input.destination_path - ))), - }, - None => Task::ready(Err(anyhow!( - "Source path {} was not found in the project.", - input.source_path - ))), - } - }); - - cx.background_spawn(async move { - let _ = copy_task.await.with_context(|| { - format!( - "Copying {} to {}", - input.source_path, input.destination_path - ) - })?; - Ok(format!( - "Copied {} to {}", - input.source_path, input.destination_path - )) - }) - } -} diff --git a/crates/agent/src/tools/create_directory_tool.rs b/crates/agent/src/tools/create_directory_tool.rs deleted file mode 100644 index b6240e99cf..0000000000 --- a/crates/agent/src/tools/create_directory_tool.rs +++ /dev/null @@ -1,90 +0,0 @@ -use agent_client_protocol::ToolKind; -use anyhow::{Context as _, Result, anyhow}; -use gpui::{App, Entity, SharedString, Task}; -use project::Project; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; -use util::markdown::MarkdownInlineCode; - -use crate::{AgentTool, ToolCallEventStream}; - -/// Creates a new directory at the specified path within the project. Returns confirmation that the directory was created. -/// -/// This tool creates a directory and all necessary parent directories. It should be used whenever you need to create new directories within the project. -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct CreateDirectoryToolInput { - /// The path of the new directory. - /// - /// - /// If the project has the following structure: - /// - /// - directory1/ - /// - directory2/ - /// - /// You can create a new directory by providing a path of "directory1/new_directory" - /// - pub path: String, -} - -pub struct CreateDirectoryTool { - project: Entity, -} - -impl CreateDirectoryTool { - pub fn new(project: Entity) -> Self { - Self { project } - } -} - -impl AgentTool for CreateDirectoryTool { - type Input = CreateDirectoryToolInput; - type Output = String; - - fn name() -> &'static str { - "create_directory" - } - - fn kind() -> ToolKind { - ToolKind::Read - } - - fn initial_title( - &self, - input: Result, - _cx: &mut App, - ) -> SharedString { - if let Ok(input) = input { - format!("Create directory {}", MarkdownInlineCode(&input.path)).into() - } else { - "Create directory".into() - } - } - - fn run( - self: Arc, - input: Self::Input, - _event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - let project_path = match self.project.read(cx).find_project_path(&input.path, cx) { - Some(project_path) => project_path, - None => { - return Task::ready(Err(anyhow!("Path to create was outside the project"))); - } - }; - let destination_path: Arc = input.path.as_str().into(); - - let create_entry = self.project.update(cx, |project, cx| { - project.create_entry(project_path.clone(), true, cx) - }); - - cx.spawn(async move |_cx| { - create_entry - .await - .with_context(|| format!("Creating directory {destination_path}"))?; - - Ok(format!("Created directory {destination_path}")) - }) - } -} diff --git a/crates/agent/src/tools/delete_path_tool.rs b/crates/agent/src/tools/delete_path_tool.rs deleted file mode 100644 index 01a77f5d81..0000000000 --- a/crates/agent/src/tools/delete_path_tool.rs +++ /dev/null @@ -1,140 +0,0 @@ -use crate::{AgentTool, ToolCallEventStream}; -use action_log::ActionLog; -use agent_client_protocol::ToolKind; -use anyhow::{Context as _, Result, anyhow}; -use futures::{SinkExt, StreamExt, channel::mpsc}; -use gpui::{App, AppContext, Entity, SharedString, Task}; -use project::{Project, ProjectPath}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; - -/// Deletes the file or directory (and the directory's contents, recursively) at the specified path in the project, and returns confirmation of the deletion. -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct DeletePathToolInput { - /// The path of the file or directory to delete. - /// - /// - /// If the project has the following files: - /// - /// - directory1/a/something.txt - /// - directory2/a/things.txt - /// - directory3/a/other.txt - /// - /// You can delete the first file by providing a path of "directory1/a/something.txt" - /// - pub path: String, -} - -pub struct DeletePathTool { - project: Entity, - action_log: Entity, -} - -impl DeletePathTool { - pub fn new(project: Entity, action_log: Entity) -> Self { - Self { - project, - action_log, - } - } -} - -impl AgentTool for DeletePathTool { - type Input = DeletePathToolInput; - type Output = String; - - fn name() -> &'static str { - "delete_path" - } - - fn kind() -> ToolKind { - ToolKind::Delete - } - - fn initial_title( - &self, - input: Result, - _cx: &mut App, - ) -> SharedString { - if let Ok(input) = input { - format!("Delete “`{}`”", input.path).into() - } else { - "Delete path".into() - } - } - - fn run( - self: Arc, - input: Self::Input, - _event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - let path = input.path; - let Some(project_path) = self.project.read(cx).find_project_path(&path, cx) else { - return Task::ready(Err(anyhow!( - "Couldn't delete {path} because that path isn't in this project." - ))); - }; - - let Some(worktree) = self - .project - .read(cx) - .worktree_for_id(project_path.worktree_id, cx) - else { - return Task::ready(Err(anyhow!( - "Couldn't delete {path} because that path isn't in this project." - ))); - }; - - let worktree_snapshot = worktree.read(cx).snapshot(); - let (mut paths_tx, mut paths_rx) = mpsc::channel(256); - cx.background_spawn({ - let project_path = project_path.clone(); - async move { - for entry in - worktree_snapshot.traverse_from_path(true, false, false, &project_path.path) - { - if !entry.path.starts_with(&project_path.path) { - break; - } - paths_tx - .send(ProjectPath { - worktree_id: project_path.worktree_id, - path: entry.path.clone(), - }) - .await?; - } - anyhow::Ok(()) - } - }) - .detach(); - - let project = self.project.clone(); - let action_log = self.action_log.clone(); - cx.spawn(async move |cx| { - while let Some(path) = paths_rx.next().await { - if let Ok(buffer) = project - .update(cx, |project, cx| project.open_buffer(path, cx))? - .await - { - action_log.update(cx, |action_log, cx| { - action_log.will_delete_buffer(buffer.clone(), cx) - })?; - } - } - - let deletion_task = project - .update(cx, |project, cx| { - project.delete_file(project_path, false, cx) - })? - .with_context(|| { - format!("Couldn't delete {path} because that path isn't in this project.") - })?; - deletion_task - .await - .with_context(|| format!("Deleting {path}"))?; - Ok(format!("Deleted {path}")) - }) - } -} diff --git a/crates/agent/src/tools/diagnostics_tool.rs b/crates/agent/src/tools/diagnostics_tool.rs deleted file mode 100644 index f07ec4cfe6..0000000000 --- a/crates/agent/src/tools/diagnostics_tool.rs +++ /dev/null @@ -1,165 +0,0 @@ -use crate::{AgentTool, ToolCallEventStream}; -use agent_client_protocol as acp; -use anyhow::{Result, anyhow}; -use gpui::{App, Entity, Task}; -use language::{DiagnosticSeverity, OffsetRangeExt}; -use project::Project; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::{fmt::Write, sync::Arc}; -use ui::SharedString; -use util::markdown::MarkdownInlineCode; - -/// Get errors and warnings for the project or a specific file. -/// -/// This tool can be invoked after a series of edits to determine if further edits are necessary, or if the user asks to fix errors or warnings in their codebase. -/// -/// When a path is provided, shows all diagnostics for that specific file. -/// When no path is provided, shows a summary of error and warning counts for all files in the project. -/// -/// -/// To get diagnostics for a specific file: -/// { -/// "path": "src/main.rs" -/// } -/// -/// To get a project-wide diagnostic summary: -/// {} -/// -/// -/// -/// - If you think you can fix a diagnostic, make 1-2 attempts and then give up. -/// - Don't remove code you've generated just because you can't fix an error. The user can help you fix it. -/// -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct DiagnosticsToolInput { - /// The path to get diagnostics for. If not provided, returns a project-wide summary. - /// - /// This path should never be absolute, and the first component - /// of the path should always be a root directory in a project. - /// - /// - /// If the project has the following root directories: - /// - /// - lorem - /// - ipsum - /// - /// If you wanna access diagnostics for `dolor.txt` in `ipsum`, you should use the path `ipsum/dolor.txt`. - /// - pub path: Option, -} - -pub struct DiagnosticsTool { - project: Entity, -} - -impl DiagnosticsTool { - pub fn new(project: Entity) -> Self { - Self { project } - } -} - -impl AgentTool for DiagnosticsTool { - type Input = DiagnosticsToolInput; - type Output = String; - - fn name() -> &'static str { - "diagnostics" - } - - fn kind() -> acp::ToolKind { - acp::ToolKind::Read - } - - fn initial_title( - &self, - input: Result, - _cx: &mut App, - ) -> SharedString { - if let Some(path) = input.ok().and_then(|input| match input.path { - Some(path) if !path.is_empty() => Some(path), - _ => None, - }) { - format!("Check diagnostics for {}", MarkdownInlineCode(&path)).into() - } else { - "Check project diagnostics".into() - } - } - - fn run( - self: Arc, - input: Self::Input, - _event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - match input.path { - Some(path) if !path.is_empty() => { - let Some(project_path) = self.project.read(cx).find_project_path(&path, cx) else { - return Task::ready(Err(anyhow!("Could not find path {path} in project",))); - }; - - let buffer = self - .project - .update(cx, |project, cx| project.open_buffer(project_path, cx)); - - cx.spawn(async move |cx| { - let mut output = String::new(); - let buffer = buffer.await?; - let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; - - for (_, group) in snapshot.diagnostic_groups(None) { - let entry = &group.entries[group.primary_ix]; - let range = entry.range.to_point(&snapshot); - let severity = match entry.diagnostic.severity { - DiagnosticSeverity::ERROR => "error", - DiagnosticSeverity::WARNING => "warning", - _ => continue, - }; - - writeln!( - output, - "{} at line {}: {}", - severity, - range.start.row + 1, - entry.diagnostic.message - )?; - } - - if output.is_empty() { - Ok("File doesn't have errors or warnings!".to_string()) - } else { - Ok(output) - } - }) - } - _ => { - let project = self.project.read(cx); - let mut output = String::new(); - let mut has_diagnostics = false; - - for (project_path, _, summary) in project.diagnostic_summaries(true, cx) { - if summary.error_count > 0 || summary.warning_count > 0 { - let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx) - else { - continue; - }; - - has_diagnostics = true; - output.push_str(&format!( - "{}: {} error(s), {} warning(s)\n", - worktree.read(cx).absolutize(&project_path.path).display(), - summary.error_count, - summary.warning_count - )); - } - } - - if has_diagnostics { - Task::ready(Ok(output)) - } else { - Task::ready(Ok("No errors or warnings found in the project.".into())) - } - } - } - } -} diff --git a/crates/agent/src/tools/edit_file_tool.rs b/crates/agent/src/tools/edit_file_tool.rs deleted file mode 100644 index 0ab99426e2..0000000000 --- a/crates/agent/src/tools/edit_file_tool.rs +++ /dev/null @@ -1,2210 +0,0 @@ -use crate::{ - AgentTool, Templates, Thread, ToolCallEventStream, - edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent, EditFormat}, -}; -use acp_thread::Diff; -use agent_client_protocol::{self as acp, ToolCallLocation, ToolCallUpdateFields}; -use anyhow::{Context as _, Result, anyhow}; -use cloud_llm_client::CompletionIntent; -use collections::HashSet; -use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity}; -use indoc::formatdoc; -use language::language_settings::{self, FormatOnSave}; -use language::{LanguageRegistry, ToPoint}; -use language_model::LanguageModelToolResultContent; -use paths; -use project::lsp_store::{FormatTrigger, LspFormatTarget}; -use project::{Project, ProjectPath}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::Settings; -use smol::stream::StreamExt as _; -use std::ffi::OsStr; -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use ui::SharedString; -use util::ResultExt; -use util::rel_path::RelPath; - -const DEFAULT_UI_TEXT: &str = "Editing file"; - -/// This is a tool for creating a new file or editing an existing file. For moving or renaming files, you should generally use the `terminal` tool with the 'mv' command instead. -/// -/// Before using this tool: -/// -/// 1. Use the `read_file` tool to understand the file's contents and context -/// -/// 2. Verify the directory path is correct (only applicable when creating new files): -/// - Use the `list_directory` tool to verify the parent directory exists and is the correct location -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -pub struct EditFileToolInput { - /// A one-line, user-friendly markdown description of the edit. This will be shown in the UI and also passed to another model to perform the edit. - /// - /// Be terse, but also descriptive in what you want to achieve with this edit. Avoid generic instructions. - /// - /// NEVER mention the file path in this description. - /// - /// Fix API endpoint URLs - /// Update copyright year in `page_footer` - /// - /// Make sure to include this field before all the others in the input object so that we can display it immediately. - pub display_description: String, - - /// The full path of the file to create or modify in the project. - /// - /// WARNING: When specifying which file path need changing, you MUST start each path with one of the project's root directories. - /// - /// The following examples assume we have two root directories in the project: - /// - /a/b/backend - /// - /c/d/frontend - /// - /// - /// `backend/src/main.rs` - /// - /// Notice how the file path starts with `backend`. Without that, the path would be ambiguous and the call would fail! - /// - /// - /// - /// `frontend/db.js` - /// - pub path: PathBuf, - /// The mode of operation on the file. Possible values: - /// - 'edit': Make granular edits to an existing file. - /// - 'create': Create a new file if it doesn't exist. - /// - 'overwrite': Replace the entire contents of an existing file. - /// - /// When a file already exists or you just created it, prefer editing it as opposed to recreating it from scratch. - pub mode: EditFileMode, -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -struct EditFileToolPartialInput { - #[serde(default)] - path: String, - #[serde(default)] - display_description: String, -} - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "lowercase")] -#[schemars(inline)] -pub enum EditFileMode { - Edit, - Create, - Overwrite, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct EditFileToolOutput { - #[serde(alias = "original_path")] - input_path: PathBuf, - new_text: String, - old_text: Arc, - #[serde(default)] - diff: String, - #[serde(alias = "raw_output")] - edit_agent_output: EditAgentOutput, -} - -impl From for LanguageModelToolResultContent { - fn from(output: EditFileToolOutput) -> Self { - if output.diff.is_empty() { - "No edits were made.".into() - } else { - format!( - "Edited {}:\n\n```diff\n{}\n```", - output.input_path.display(), - output.diff - ) - .into() - } - } -} - -pub struct EditFileTool { - thread: WeakEntity, - language_registry: Arc, - project: Entity, - templates: Arc, -} - -impl EditFileTool { - pub fn new( - project: Entity, - thread: WeakEntity, - language_registry: Arc, - templates: Arc, - ) -> Self { - Self { - project, - thread, - language_registry, - templates, - } - } - - fn authorize( - &self, - input: &EditFileToolInput, - event_stream: &ToolCallEventStream, - cx: &mut App, - ) -> Task> { - if agent_settings::AgentSettings::get_global(cx).always_allow_tool_actions { - return Task::ready(Ok(())); - } - - // If any path component matches the local settings folder, then this could affect - // the editor in ways beyond the project source, so prompt. - let local_settings_folder = paths::local_settings_folder_name(); - let path = Path::new(&input.path); - if path.components().any(|component| { - component.as_os_str() == <_ as AsRef>::as_ref(&local_settings_folder) - }) { - return event_stream.authorize( - format!("{} (local settings)", input.display_description), - cx, - ); - } - - // It's also possible that the global config dir is configured to be inside the project, - // so check for that edge case too. - // TODO this is broken when remoting - if let Ok(canonical_path) = std::fs::canonicalize(&input.path) - && canonical_path.starts_with(paths::config_dir()) - { - return event_stream.authorize( - format!("{} (global settings)", input.display_description), - cx, - ); - } - - // Check if path is inside the global config directory - // First check if it's already inside project - if not, try to canonicalize - let Ok(project_path) = self.thread.read_with(cx, |thread, cx| { - thread.project().read(cx).find_project_path(&input.path, cx) - }) else { - return Task::ready(Err(anyhow!("thread was dropped"))); - }; - - // If the path is inside the project, and it's not one of the above edge cases, - // then no confirmation is necessary. Otherwise, confirmation is necessary. - if project_path.is_some() { - Task::ready(Ok(())) - } else { - event_stream.authorize(&input.display_description, cx) - } - } -} - -impl AgentTool for EditFileTool { - type Input = EditFileToolInput; - type Output = EditFileToolOutput; - - fn name() -> &'static str { - "edit_file" - } - - fn kind() -> acp::ToolKind { - acp::ToolKind::Edit - } - - fn initial_title( - &self, - input: Result, - cx: &mut App, - ) -> SharedString { - match input { - Ok(input) => self - .project - .read(cx) - .find_project_path(&input.path, cx) - .and_then(|project_path| { - self.project - .read(cx) - .short_full_path_for_project_path(&project_path, cx) - }) - .unwrap_or(input.path.to_string_lossy().into_owned()) - .into(), - Err(raw_input) => { - if let Some(input) = - serde_json::from_value::(raw_input).ok() - { - let path = input.path.trim(); - if !path.is_empty() { - return self - .project - .read(cx) - .find_project_path(&input.path, cx) - .and_then(|project_path| { - self.project - .read(cx) - .short_full_path_for_project_path(&project_path, cx) - }) - .unwrap_or(input.path) - .into(); - } - - let description = input.display_description.trim(); - if !description.is_empty() { - return description.to_string().into(); - } - } - - DEFAULT_UI_TEXT.into() - } - } - } - - fn run( - self: Arc, - input: Self::Input, - event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - let Ok(project) = self - .thread - .read_with(cx, |thread, _cx| thread.project().clone()) - else { - return Task::ready(Err(anyhow!("thread was dropped"))); - }; - let project_path = match resolve_path(&input, project.clone(), cx) { - Ok(path) => path, - Err(err) => return Task::ready(Err(anyhow!(err))), - }; - let abs_path = project.read(cx).absolute_path(&project_path, cx); - if let Some(abs_path) = abs_path.clone() { - event_stream.update_fields( - ToolCallUpdateFields::new().locations(vec![acp::ToolCallLocation::new(abs_path)]), - ); - } - - let authorize = self.authorize(&input, &event_stream, cx); - cx.spawn(async move |cx: &mut AsyncApp| { - authorize.await?; - - let (request, model, action_log) = self.thread.update(cx, |thread, cx| { - let request = thread.build_completion_request(CompletionIntent::ToolResults, cx); - (request, thread.model().cloned(), thread.action_log().clone()) - })?; - let request = request?; - let model = model.context("No language model configured")?; - - let edit_format = EditFormat::from_model(model.clone())?; - let edit_agent = EditAgent::new( - model, - project.clone(), - action_log.clone(), - self.templates.clone(), - edit_format, - ); - - let buffer = project - .update(cx, |project, cx| { - project.open_buffer(project_path.clone(), cx) - })? - .await?; - - // Check if the file has been modified since the agent last read it - if let Some(abs_path) = abs_path.as_ref() { - let (last_read_mtime, current_mtime, is_dirty) = self.thread.update(cx, |thread, cx| { - let last_read = thread.file_read_times.get(abs_path).copied(); - let current = buffer.read(cx).file().and_then(|file| file.disk_state().mtime()); - let dirty = buffer.read(cx).is_dirty(); - (last_read, current, dirty) - })?; - - // Check for unsaved changes first - these indicate modifications we don't know about - if is_dirty { - anyhow::bail!( - "This file cannot be written to because it has unsaved changes. \ - Please end the current conversation immediately by telling the user you want to write to this file (mention its path explicitly) but you can't write to it because it has unsaved changes. \ - Ask the user to save that buffer's changes and to inform you when it's ok to proceed." - ); - } - - // Check if the file was modified on disk since we last read it - if let (Some(last_read), Some(current)) = (last_read_mtime, current_mtime) { - // MTime can be unreliable for comparisons, so our newtype intentionally - // doesn't support comparing them. If the mtime at all different - // (which could be because of a modification or because e.g. system clock changed), - // we pessimistically assume it was modified. - if current != last_read { - anyhow::bail!( - "The file {} has been modified since you last read it. \ - Please read the file again to get the current state before editing it.", - input.path.display() - ); - } - } - } - - 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 - .background_spawn({ - let old_snapshot = old_snapshot.clone(); - async move { Arc::new(old_snapshot.text()) } - }) - .await; - - - let (output, mut events) = if matches!(input.mode, EditFileMode::Edit) { - edit_agent.edit( - buffer.clone(), - input.display_description.clone(), - &request, - cx, - ) - } else { - edit_agent.overwrite( - buffer.clone(), - input.display_description.clone(), - &request, - cx, - ) - }; - - let mut hallucinated_old_text = false; - let mut ambiguous_ranges = Vec::new(); - let mut emitted_location = false; - while let Some(event) = events.next().await { - match event { - EditAgentOutputEvent::Edited(range) => { - if !emitted_location { - let line = buffer.update(cx, |buffer, _cx| { - range.start.to_point(&buffer.snapshot()).row - }).ok(); - if let Some(abs_path) = abs_path.clone() { - event_stream.update_fields(ToolCallUpdateFields::new().locations(vec![ToolCallLocation::new(abs_path).line(line)])); - } - emitted_location = true; - } - }, - EditAgentOutputEvent::UnresolvedEditRange => hallucinated_old_text = true, - EditAgentOutputEvent::AmbiguousEditRange(ranges) => ambiguous_ranges = ranges, - EditAgentOutputEvent::ResolvingEditRange(range) => { - diff.update(cx, |card, cx| card.reveal_range(range.clone(), cx))?; - // if !emitted_location { - // let line = buffer.update(cx, |buffer, _cx| { - // range.start.to_point(&buffer.snapshot()).row - // }).ok(); - // if let Some(abs_path) = abs_path.clone() { - // event_stream.update_fields(ToolCallUpdateFields { - // locations: Some(vec![ToolCallLocation { path: abs_path, line }]), - // ..Default::default() - // }); - // } - // } - } - } - } - - // If format_on_save is enabled, format the buffer - let format_on_save_enabled = buffer - .read_with(cx, |buffer, cx| { - let settings = language_settings::language_settings( - buffer.language().map(|l| l.name()), - buffer.file(), - cx, - ); - settings.format_on_save != FormatOnSave::Off - }) - .unwrap_or(false); - - let edit_agent_output = output.await?; - - if format_on_save_enabled { - action_log.update(cx, |log, cx| { - log.buffer_edited(buffer.clone(), cx); - })?; - - let format_task = project.update(cx, |project, cx| { - project.format( - HashSet::from_iter([buffer.clone()]), - LspFormatTarget::Buffers, - false, // Don't push to history since the tool did it. - FormatTrigger::Save, - cx, - ) - })?; - format_task.await.log_err(); - } - - project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))? - .await?; - - action_log.update(cx, |log, cx| { - log.buffer_edited(buffer.clone(), cx); - })?; - - // Update the recorded read time after a successful edit so consecutive edits work - if let Some(abs_path) = abs_path.as_ref() { - if let Some(new_mtime) = buffer.read_with(cx, |buffer, _| { - buffer.file().and_then(|file| file.disk_state().mtime()) - })? { - self.thread.update(cx, |thread, _| { - thread.file_read_times.insert(abs_path.to_path_buf(), new_mtime); - })?; - } - } - - let new_snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; - let (new_text, unified_diff) = cx - .background_spawn({ - let new_snapshot = new_snapshot.clone(); - let old_text = old_text.clone(); - async move { - let new_text = new_snapshot.text(); - let diff = language::unified_diff(&old_text, &new_text); - (new_text, diff) - } - }) - .await; - - let input_path = input.path.display(); - if unified_diff.is_empty() { - anyhow::ensure!( - !hallucinated_old_text, - formatdoc! {" - Some edits were produced but none of them could be applied. - Read the relevant sections of {input_path} again so that - I can perform the requested edits. - "} - ); - anyhow::ensure!( - ambiguous_ranges.is_empty(), - { - let line_numbers = ambiguous_ranges - .iter() - .map(|range| range.start.to_string()) - .collect::>() - .join(", "); - formatdoc! {" - matches more than one position in the file (lines: {line_numbers}). Read the - relevant sections of {input_path} again and extend so - that I can perform the requested edits. - "} - } - ); - } - - Ok(EditFileToolOutput { - input_path: input.path, - new_text, - old_text, - diff: unified_diff, - edit_agent_output, - }) - }) - } - - fn replay( - &self, - _input: Self::Input, - output: Self::Output, - event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Result<()> { - event_stream.update_diff(cx.new(|cx| { - Diff::finalized( - output.input_path.to_string_lossy().into_owned(), - Some(output.old_text.to_string()), - output.new_text, - self.language_registry.clone(), - cx, - ) - })); - Ok(()) - } -} - -/// Validate that the file path is valid, meaning: -/// -/// - For `edit` and `overwrite`, the path must point to an existing file. -/// - For `create`, the file must not already exist, but it's parent dir must exist. -fn resolve_path( - input: &EditFileToolInput, - project: Entity, - cx: &mut App, -) -> Result { - let project = project.read(cx); - - match input.mode { - EditFileMode::Edit | EditFileMode::Overwrite => { - let path = project - .find_project_path(&input.path, cx) - .context("Can't edit file: path not found")?; - - let entry = project - .entry_for_path(&path, cx) - .context("Can't edit file: path not found")?; - - anyhow::ensure!(entry.is_file(), "Can't edit file: path is a directory"); - Ok(path) - } - - EditFileMode::Create => { - if let Some(path) = project.find_project_path(&input.path, cx) { - anyhow::ensure!( - project.entry_for_path(&path, cx).is_none(), - "Can't create file: file already exists" - ); - } - - let parent_path = input - .path - .parent() - .context("Can't create file: incorrect path")?; - - let parent_project_path = project.find_project_path(&parent_path, cx); - - let parent_entry = parent_project_path - .as_ref() - .and_then(|path| project.entry_for_path(path, cx)) - .context("Can't create file: parent directory doesn't exist")?; - - anyhow::ensure!( - parent_entry.is_dir(), - "Can't create file: parent is not a directory" - ); - - let file_name = input - .path - .file_name() - .and_then(|file_name| file_name.to_str()) - .and_then(|file_name| RelPath::unix(file_name).ok()) - .context("Can't create file: invalid filename")?; - - let new_file_path = parent_project_path.map(|parent| ProjectPath { - path: parent.path.join(file_name), - ..parent - }); - - new_file_path.context("Can't create file") - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ContextServerRegistry, Templates}; - use fs::Fs; - use gpui::{TestAppContext, UpdateGlobal}; - use language_model::fake_provider::FakeLanguageModel; - use prompt_store::ProjectContext; - use serde_json::json; - use settings::SettingsStore; - use util::{path, rel_path::rel_path}; - - #[gpui::test] - async fn test_edit_nonexistent_file(cx: &mut TestAppContext) { - init_test(cx); - - let fs = project::FakeFs::new(cx.executor()); - 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 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, - Templates::new(), - Some(model), - cx, - ) - }); - let result = cx - .update(|cx| { - let input = EditFileToolInput { - display_description: "Some edit".into(), - path: "root/nonexistent_file.txt".into(), - mode: EditFileMode::Edit, - }; - Arc::new(EditFileTool::new( - project, - thread.downgrade(), - language_registry, - Templates::new(), - )) - .run(input, ToolCallEventStream::test().0, cx) - }) - .await; - assert_eq!( - result.unwrap_err().to_string(), - "Can't edit file: path not found" - ); - } - - #[gpui::test] - async fn test_resolve_path_for_creating_file(cx: &mut TestAppContext) { - let mode = &EditFileMode::Create; - - let result = test_resolve_path(mode, "root/new.txt", cx); - assert_resolved_path_eq(result.await, rel_path("new.txt")); - - let result = test_resolve_path(mode, "new.txt", cx); - assert_resolved_path_eq(result.await, rel_path("new.txt")); - - let result = test_resolve_path(mode, "dir/new.txt", cx); - assert_resolved_path_eq(result.await, rel_path("dir/new.txt")); - - let result = test_resolve_path(mode, "root/dir/subdir/existing.txt", cx); - assert_eq!( - result.await.unwrap_err().to_string(), - "Can't create file: file already exists" - ); - - let result = test_resolve_path(mode, "root/dir/nonexistent_dir/new.txt", cx); - assert_eq!( - result.await.unwrap_err().to_string(), - "Can't create file: parent directory doesn't exist" - ); - } - - #[gpui::test] - async fn test_resolve_path_for_editing_file(cx: &mut TestAppContext) { - let mode = &EditFileMode::Edit; - - let path_with_root = "root/dir/subdir/existing.txt"; - let path_without_root = "dir/subdir/existing.txt"; - let result = test_resolve_path(mode, path_with_root, cx); - assert_resolved_path_eq(result.await, rel_path(path_without_root)); - - let result = test_resolve_path(mode, path_without_root, cx); - assert_resolved_path_eq(result.await, rel_path(path_without_root)); - - let result = test_resolve_path(mode, "root/nonexistent.txt", cx); - assert_eq!( - result.await.unwrap_err().to_string(), - "Can't edit file: path not found" - ); - - let result = test_resolve_path(mode, "root/dir", cx); - assert_eq!( - result.await.unwrap_err().to_string(), - "Can't edit file: path is a directory" - ); - } - - async fn test_resolve_path( - mode: &EditFileMode, - path: &str, - cx: &mut TestAppContext, - ) -> anyhow::Result { - init_test(cx); - - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "dir": { - "subdir": { - "existing.txt": "hello" - } - } - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - - let input = EditFileToolInput { - display_description: "Some edit".into(), - path: path.into(), - mode: mode.clone(), - }; - - cx.update(|cx| resolve_path(&input, project, cx)) - } - - #[track_caller] - fn assert_resolved_path_eq(path: anyhow::Result, expected: &RelPath) { - let actual = path.expect("Should return valid path").path; - assert_eq!(actual.as_ref(), expected); - } - - #[gpui::test] - async fn test_format_on_save(cx: &mut TestAppContext) { - init_test(cx); - - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree("/root", json!({"src": {}})).await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - - // Set up a Rust language with LSP formatting support - let rust_language = Arc::new(language::Language::new( - language::LanguageConfig { - name: "Rust".into(), - matcher: language::LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - None, - )); - - // Register the language and fake LSP - let language_registry = project.read_with(cx, |project, _| project.languages().clone()); - language_registry.add(rust_language); - - let mut fake_language_servers = language_registry.register_fake_lsp( - "Rust", - language::FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - document_formatting_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() - }, - ); - - // Create the file - fs.save( - path!("/root/src/main.rs").as_ref(), - &"initial content".into(), - language::LineEnding::Unix, - ) - .await - .unwrap(); - - // Open the buffer to trigger LSP initialization - let buffer = project - .update(cx, |project, cx| { - project.open_local_buffer(path!("/root/src/main.rs"), cx) - }) - .await - .unwrap(); - - // Register the buffer with language servers - let _handle = project.update(cx, |project, cx| { - project.register_buffer_with_language_servers(&buffer, cx) - }); - - const UNFORMATTED_CONTENT: &str = "fn main() {println!(\"Hello!\");}\n"; - const FORMATTED_CONTENT: &str = - "This file was formatted by the fake formatter in the test.\n"; - - // Get the fake language server and set up formatting handler - let fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server.set_request_handler::({ - |_, _| async move { - Ok(Some(vec![lsp::TextEdit { - range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(1, 0)), - new_text: FORMATTED_CONTENT.to_string(), - }])) - } - }); - - 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, - Templates::new(), - Some(model.clone()), - cx, - ) - }); - - // First, test with format_on_save enabled - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.all_languages.defaults.format_on_save = Some(FormatOnSave::On); - settings.project.all_languages.defaults.formatter = - Some(language::language_settings::FormatterList::default()); - }); - }); - }); - - // Have the model stream unformatted content - let edit_result = { - let edit_task = cx.update(|cx| { - let input = EditFileToolInput { - display_description: "Create main function".into(), - path: "root/src/main.rs".into(), - mode: EditFileMode::Overwrite, - }; - Arc::new(EditFileTool::new( - project.clone(), - thread.downgrade(), - language_registry.clone(), - Templates::new(), - )) - .run(input, ToolCallEventStream::test().0, cx) - }); - - // Stream the unformatted content - cx.executor().run_until_parked(); - model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string()); - model.end_last_completion_stream(); - - edit_task.await - }; - assert!(edit_result.is_ok()); - - // Wait for any async operations (e.g. formatting) to complete - cx.executor().run_until_parked(); - - // Read the file to verify it was formatted automatically - let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap(); - assert_eq!( - // Ignore carriage returns on Windows - new_content.replace("\r\n", "\n"), - FORMATTED_CONTENT, - "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()); - - assert_eq!( - stale_buffer_count, 0, - "BUG: Buffer is incorrectly marked as stale after format-on-save. Found {} stale buffers. \ - This causes the agent to think the file was modified externally when it was just formatted.", - stale_buffer_count - ); - - // Next, test with format_on_save disabled - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.all_languages.defaults.format_on_save = - Some(FormatOnSave::Off); - }); - }); - }); - - // Stream unformatted edits again - let edit_result = { - let edit_task = cx.update(|cx| { - let input = EditFileToolInput { - display_description: "Update main function".into(), - path: "root/src/main.rs".into(), - mode: EditFileMode::Overwrite, - }; - Arc::new(EditFileTool::new( - project.clone(), - thread.downgrade(), - language_registry, - Templates::new(), - )) - .run(input, ToolCallEventStream::test().0, cx) - }); - - // Stream the unformatted content - cx.executor().run_until_parked(); - model.send_last_completion_stream_text_chunk(UNFORMATTED_CONTENT.to_string()); - model.end_last_completion_stream(); - - edit_task.await - }; - assert!(edit_result.is_ok()); - - // Wait for any async operations (e.g. formatting) to complete - cx.executor().run_until_parked(); - - // Verify the file was not formatted - let new_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap(); - assert_eq!( - // Ignore carriage returns on Windows - new_content.replace("\r\n", "\n"), - UNFORMATTED_CONTENT, - "Code should not be formatted when format_on_save is disabled" - ); - } - - #[gpui::test] - async fn test_remove_trailing_whitespace(cx: &mut TestAppContext) { - init_test(cx); - - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree("/root", json!({"src": {}})).await; - - // Create a simple file with trailing whitespace - fs.save( - path!("/root/src/main.rs").as_ref(), - &"initial content".into(), - language::LineEnding::Unix, - ) - .await - .unwrap(); - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - 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 model = Arc::new(FakeLanguageModel::default()); - let thread = cx.new(|cx| { - Thread::new( - project.clone(), - cx.new(|_cx| ProjectContext::default()), - context_server_registry, - Templates::new(), - Some(model.clone()), - cx, - ) - }); - - // First, test with remove_trailing_whitespace_on_save enabled - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings - .project - .all_languages - .defaults - .remove_trailing_whitespace_on_save = Some(true); - }); - }); - }); - - const CONTENT_WITH_TRAILING_WHITESPACE: &str = - "fn main() { \n println!(\"Hello!\"); \n}\n"; - - // Have the model stream content that contains trailing whitespace - let edit_result = { - let edit_task = cx.update(|cx| { - let input = EditFileToolInput { - display_description: "Create main function".into(), - path: "root/src/main.rs".into(), - mode: EditFileMode::Overwrite, - }; - Arc::new(EditFileTool::new( - project.clone(), - thread.downgrade(), - language_registry.clone(), - Templates::new(), - )) - .run(input, ToolCallEventStream::test().0, cx) - }); - - // Stream the content with trailing whitespace - cx.executor().run_until_parked(); - model.send_last_completion_stream_text_chunk( - CONTENT_WITH_TRAILING_WHITESPACE.to_string(), - ); - model.end_last_completion_stream(); - - edit_task.await - }; - assert!(edit_result.is_ok()); - - // Wait for any async operations (e.g. formatting) to complete - cx.executor().run_until_parked(); - - // Read the file to verify trailing whitespace was removed automatically - assert_eq!( - // Ignore carriage returns on Windows - fs.load(path!("/root/src/main.rs").as_ref()) - .await - .unwrap() - .replace("\r\n", "\n"), - "fn main() {\n println!(\"Hello!\");\n}\n", - "Trailing whitespace should be removed when remove_trailing_whitespace_on_save is enabled" - ); - - // Next, test with remove_trailing_whitespace_on_save disabled - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings - .project - .all_languages - .defaults - .remove_trailing_whitespace_on_save = Some(false); - }); - }); - }); - - // Stream edits again with trailing whitespace - let edit_result = { - let edit_task = cx.update(|cx| { - let input = EditFileToolInput { - display_description: "Update main function".into(), - path: "root/src/main.rs".into(), - mode: EditFileMode::Overwrite, - }; - Arc::new(EditFileTool::new( - project.clone(), - thread.downgrade(), - language_registry, - Templates::new(), - )) - .run(input, ToolCallEventStream::test().0, cx) - }); - - // Stream the content with trailing whitespace - cx.executor().run_until_parked(); - model.send_last_completion_stream_text_chunk( - CONTENT_WITH_TRAILING_WHITESPACE.to_string(), - ); - model.end_last_completion_stream(); - - edit_task.await - }; - assert!(edit_result.is_ok()); - - // Wait for any async operations (e.g. formatting) to complete - cx.executor().run_until_parked(); - - // Verify the file still has trailing whitespace - // Read the file again - it should still have trailing whitespace - let final_content = fs.load(path!("/root/src/main.rs").as_ref()).await.unwrap(); - assert_eq!( - // Ignore carriage returns on Windows - final_content.replace("\r\n", "\n"), - CONTENT_WITH_TRAILING_WHITESPACE, - "Trailing whitespace should remain when remove_trailing_whitespace_on_save is disabled" - ); - } - - #[gpui::test] - async fn test_authorize(cx: &mut TestAppContext) { - init_test(cx); - let fs = project::FakeFs::new(cx.executor()); - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - 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 model = Arc::new(FakeLanguageModel::default()); - let thread = cx.new(|cx| { - Thread::new( - project.clone(), - cx.new(|_cx| ProjectContext::default()), - context_server_registry, - Templates::new(), - Some(model.clone()), - cx, - ) - }); - let tool = Arc::new(EditFileTool::new( - project.clone(), - thread.downgrade(), - language_registry, - Templates::new(), - )); - fs.insert_tree("/root", json!({})).await; - - // Test 1: Path with .zed component should require confirmation - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - let _auth = cx.update(|cx| { - tool.authorize( - &EditFileToolInput { - display_description: "test 1".into(), - path: ".zed/settings.json".into(), - mode: EditFileMode::Edit, - }, - &stream_tx, - cx, - ) - }); - - let event = stream_rx.expect_authorization().await; - assert_eq!( - event.tool_call.fields.title, - Some("test 1 (local settings)".into()) - ); - - // Test 2: Path outside project should require confirmation - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - let _auth = cx.update(|cx| { - tool.authorize( - &EditFileToolInput { - display_description: "test 2".into(), - path: "/etc/hosts".into(), - mode: EditFileMode::Edit, - }, - &stream_tx, - cx, - ) - }); - - let event = stream_rx.expect_authorization().await; - assert_eq!(event.tool_call.fields.title, Some("test 2".into())); - - // Test 3: Relative path without .zed should not require confirmation - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - cx.update(|cx| { - tool.authorize( - &EditFileToolInput { - display_description: "test 3".into(), - path: "root/src/main.rs".into(), - mode: EditFileMode::Edit, - }, - &stream_tx, - cx, - ) - }) - .await - .unwrap(); - assert!(stream_rx.try_next().is_err()); - - // Test 4: Path with .zed in the middle should require confirmation - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - let _auth = cx.update(|cx| { - tool.authorize( - &EditFileToolInput { - display_description: "test 4".into(), - path: "root/.zed/tasks.json".into(), - mode: EditFileMode::Edit, - }, - &stream_tx, - cx, - ) - }); - let event = stream_rx.expect_authorization().await; - assert_eq!( - event.tool_call.fields.title, - Some("test 4 (local settings)".into()) - ); - - // Test 5: When always_allow_tool_actions is enabled, no confirmation needed - cx.update(|cx| { - let mut settings = agent_settings::AgentSettings::get_global(cx).clone(); - settings.always_allow_tool_actions = true; - agent_settings::AgentSettings::override_global(settings, cx); - }); - - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - cx.update(|cx| { - tool.authorize( - &EditFileToolInput { - display_description: "test 5.1".into(), - path: ".zed/settings.json".into(), - mode: EditFileMode::Edit, - }, - &stream_tx, - cx, - ) - }) - .await - .unwrap(); - assert!(stream_rx.try_next().is_err()); - - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - cx.update(|cx| { - tool.authorize( - &EditFileToolInput { - display_description: "test 5.2".into(), - path: "/etc/hosts".into(), - mode: EditFileMode::Edit, - }, - &stream_tx, - cx, - ) - }) - .await - .unwrap(); - assert!(stream_rx.try_next().is_err()); - } - - #[gpui::test] - async fn test_authorize_global_config(cx: &mut TestAppContext) { - init_test(cx); - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree("/project", json!({})).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 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, - Templates::new(), - Some(model.clone()), - cx, - ) - }); - let tool = Arc::new(EditFileTool::new( - project.clone(), - thread.downgrade(), - language_registry, - Templates::new(), - )); - - // Test global config paths - these should require confirmation if they exist and are outside the project - let test_cases = vec![ - ( - "/etc/hosts", - true, - "System file should require confirmation", - ), - ( - "/usr/local/bin/script", - true, - "System bin file should require confirmation", - ), - ( - "project/normal_file.rs", - false, - "Normal project file should not require confirmation", - ), - ]; - - for (path, should_confirm, description) in test_cases { - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - let auth = cx.update(|cx| { - tool.authorize( - &EditFileToolInput { - display_description: "Edit file".into(), - path: path.into(), - mode: EditFileMode::Edit, - }, - &stream_tx, - cx, - ) - }); - - if should_confirm { - stream_rx.expect_authorization().await; - } else { - auth.await.unwrap(); - assert!( - stream_rx.try_next().is_err(), - "Failed for case: {} - path: {} - expected no confirmation but got one", - description, - path - ); - } - } - } - - #[gpui::test] - async fn test_needs_confirmation_with_multiple_worktrees(cx: &mut TestAppContext) { - init_test(cx); - let fs = project::FakeFs::new(cx.executor()); - - // Create multiple worktree directories - fs.insert_tree( - "/workspace/frontend", - json!({ - "src": { - "main.js": "console.log('frontend');" - } - }), - ) - .await; - fs.insert_tree( - "/workspace/backend", - json!({ - "src": { - "main.rs": "fn main() {}" - } - }), - ) - .await; - fs.insert_tree( - "/workspace/shared", - json!({ - ".zed": { - "settings.json": "{}" - } - }), - ) - .await; - - // Create project with multiple worktrees - let project = Project::test( - fs.clone(), - [ - path!("/workspace/frontend").as_ref(), - path!("/workspace/backend").as_ref(), - path!("/workspace/shared").as_ref(), - ], - cx, - ) - .await; - 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 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, - ) - }); - let tool = Arc::new(EditFileTool::new( - project.clone(), - thread.downgrade(), - language_registry, - Templates::new(), - )); - - // Test files in different worktrees - let test_cases = vec![ - ("frontend/src/main.js", false, "File in first worktree"), - ("backend/src/main.rs", false, "File in second worktree"), - ( - "shared/.zed/settings.json", - true, - ".zed file in third worktree", - ), - ("/etc/hosts", true, "Absolute path outside all worktrees"), - ( - "../outside/file.txt", - true, - "Relative path outside worktrees", - ), - ]; - - for (path, should_confirm, description) in test_cases { - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - let auth = cx.update(|cx| { - tool.authorize( - &EditFileToolInput { - display_description: "Edit file".into(), - path: path.into(), - mode: EditFileMode::Edit, - }, - &stream_tx, - cx, - ) - }); - - if should_confirm { - stream_rx.expect_authorization().await; - } else { - auth.await.unwrap(); - assert!( - stream_rx.try_next().is_err(), - "Failed for case: {} - path: {} - expected no confirmation but got one", - description, - path - ); - } - } - } - - #[gpui::test] - async fn test_needs_confirmation_edge_cases(cx: &mut TestAppContext) { - init_test(cx); - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree( - "/project", - json!({ - ".zed": { - "settings.json": "{}" - }, - "src": { - ".zed": { - "local.json": "{}" - } - } - }), - ) - .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 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, - ) - }); - let tool = Arc::new(EditFileTool::new( - project.clone(), - thread.downgrade(), - language_registry, - Templates::new(), - )); - - // Test edge cases - let test_cases = vec![ - // Empty path - find_project_path returns Some for empty paths - ("", false, "Empty path is treated as project root"), - // Root directory - ("/", true, "Root directory should be outside project"), - // Parent directory references - find_project_path resolves these - ( - "project/../other", - true, - "Path with .. that goes outside of root directory", - ), - ( - "project/./src/file.rs", - false, - "Path with . should work normally", - ), - // Windows-style paths (if on Windows) - #[cfg(target_os = "windows")] - ("C:\\Windows\\System32\\hosts", true, "Windows system path"), - #[cfg(target_os = "windows")] - ("project\\src\\main.rs", false, "Windows-style project path"), - ]; - - for (path, should_confirm, description) in test_cases { - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - let auth = cx.update(|cx| { - tool.authorize( - &EditFileToolInput { - display_description: "Edit file".into(), - path: path.into(), - mode: EditFileMode::Edit, - }, - &stream_tx, - cx, - ) - }); - - cx.run_until_parked(); - - if should_confirm { - stream_rx.expect_authorization().await; - } else { - assert!( - stream_rx.try_next().is_err(), - "Failed for case: {} - path: {} - expected no confirmation but got one", - description, - path - ); - auth.await.unwrap(); - } - } - } - - #[gpui::test] - async fn test_needs_confirmation_with_different_modes(cx: &mut TestAppContext) { - init_test(cx); - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree( - "/project", - json!({ - "existing.txt": "content", - ".zed": { - "settings.json": "{}" - } - }), - ) - .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 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, - ) - }); - let tool = Arc::new(EditFileTool::new( - project.clone(), - thread.downgrade(), - language_registry, - Templates::new(), - )); - - // Test different EditFileMode values - let modes = vec![ - EditFileMode::Edit, - EditFileMode::Create, - EditFileMode::Overwrite, - ]; - - for mode in modes { - // Test .zed path with different modes - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - let _auth = cx.update(|cx| { - tool.authorize( - &EditFileToolInput { - display_description: "Edit settings".into(), - path: "project/.zed/settings.json".into(), - mode: mode.clone(), - }, - &stream_tx, - cx, - ) - }); - - stream_rx.expect_authorization().await; - - // Test outside path with different modes - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - let _auth = cx.update(|cx| { - tool.authorize( - &EditFileToolInput { - display_description: "Edit file".into(), - path: "/outside/file.txt".into(), - mode: mode.clone(), - }, - &stream_tx, - cx, - ) - }); - - stream_rx.expect_authorization().await; - - // Test normal path with different modes - let (stream_tx, mut stream_rx) = ToolCallEventStream::test(); - cx.update(|cx| { - tool.authorize( - &EditFileToolInput { - display_description: "Edit file".into(), - path: "project/normal.txt".into(), - mode: mode.clone(), - }, - &stream_tx, - cx, - ) - }) - .await - .unwrap(); - assert!(stream_rx.try_next().is_err()); - } - } - - #[gpui::test] - async fn test_initial_title_with_partial_input(cx: &mut TestAppContext) { - init_test(cx); - 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 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, - Templates::new(), - Some(model.clone()), - cx, - ) - }); - let tool = Arc::new(EditFileTool::new( - project, - thread.downgrade(), - language_registry, - Templates::new(), - )); - - cx.update(|cx| { - // ... - assert_eq!( - tool.initial_title( - Err(json!({ - "path": "src/main.rs", - "display_description": "", - "old_string": "old code", - "new_string": "new code" - })), - cx - ), - "src/main.rs" - ); - assert_eq!( - tool.initial_title( - Err(json!({ - "path": "", - "display_description": "Fix error handling", - "old_string": "old code", - "new_string": "new code" - })), - cx - ), - "Fix error handling" - ); - assert_eq!( - tool.initial_title( - Err(json!({ - "path": "src/main.rs", - "display_description": "Fix error handling", - "old_string": "old code", - "new_string": "new code" - })), - cx - ), - "src/main.rs" - ); - assert_eq!( - tool.initial_title( - Err(json!({ - "path": "", - "display_description": "", - "old_string": "old code", - "new_string": "new code" - })), - cx - ), - DEFAULT_UI_TEXT - ); - assert_eq!( - tool.initial_title(Err(serde_json::Value::Null), cx), - DEFAULT_UI_TEXT - ); - }); - } - - #[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( - project.clone(), - thread.downgrade(), - languages.clone(), - Templates::new(), - )); - 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( - project.clone(), - thread.downgrade(), - languages.clone(), - Templates::new(), - )); - 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( - project.clone(), - thread.downgrade(), - languages.clone(), - Templates::new(), - )); - 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(_)))); - } - } - - #[gpui::test] - async fn test_file_read_times_tracking(cx: &mut TestAppContext) { - init_test(cx); - - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "test.txt": "original content" - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - 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, - Templates::new(), - Some(model.clone()), - cx, - ) - }); - let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone()); - - // Initially, file_read_times should be empty - let is_empty = thread.read_with(cx, |thread, _| thread.file_read_times.is_empty()); - assert!(is_empty, "file_read_times should start empty"); - - // Create read tool - let read_tool = Arc::new(crate::ReadFileTool::new( - thread.downgrade(), - project.clone(), - action_log, - )); - - // Read the file to record the read time - cx.update(|cx| { - read_tool.clone().run( - crate::ReadFileToolInput { - path: "root/test.txt".to_string(), - start_line: None, - end_line: None, - }, - ToolCallEventStream::test().0, - cx, - ) - }) - .await - .unwrap(); - - // Verify that file_read_times now contains an entry for the file - let has_entry = thread.read_with(cx, |thread, _| { - thread.file_read_times.len() == 1 - && thread - .file_read_times - .keys() - .any(|path| path.ends_with("test.txt")) - }); - assert!( - has_entry, - "file_read_times should contain an entry after reading the file" - ); - - // Read the file again - should update the entry - cx.update(|cx| { - read_tool.clone().run( - crate::ReadFileToolInput { - path: "root/test.txt".to_string(), - start_line: None, - end_line: None, - }, - ToolCallEventStream::test().0, - cx, - ) - }) - .await - .unwrap(); - - // Should still have exactly one entry - let has_one_entry = thread.read_with(cx, |thread, _| thread.file_read_times.len() == 1); - assert!( - has_one_entry, - "file_read_times should still have one entry after re-reading" - ); - } - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - }); - } - - #[gpui::test] - async fn test_consecutive_edits_work(cx: &mut TestAppContext) { - init_test(cx); - - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "test.txt": "original content" - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - 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, - Templates::new(), - Some(model.clone()), - cx, - ) - }); - let languages = project.read_with(cx, |project, _| project.languages().clone()); - let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone()); - - let read_tool = Arc::new(crate::ReadFileTool::new( - thread.downgrade(), - project.clone(), - action_log, - )); - let edit_tool = Arc::new(EditFileTool::new( - project.clone(), - thread.downgrade(), - languages, - Templates::new(), - )); - - // Read the file first - cx.update(|cx| { - read_tool.clone().run( - crate::ReadFileToolInput { - path: "root/test.txt".to_string(), - start_line: None, - end_line: None, - }, - ToolCallEventStream::test().0, - cx, - ) - }) - .await - .unwrap(); - - // First edit should work - let edit_result = { - let edit_task = cx.update(|cx| { - edit_tool.clone().run( - EditFileToolInput { - display_description: "First edit".into(), - path: "root/test.txt".into(), - mode: EditFileMode::Edit, - }, - ToolCallEventStream::test().0, - cx, - ) - }); - - cx.executor().run_until_parked(); - model.send_last_completion_stream_text_chunk( - "original contentmodified content" - .to_string(), - ); - model.end_last_completion_stream(); - - edit_task.await - }; - assert!( - edit_result.is_ok(), - "First edit should succeed, got error: {:?}", - edit_result.as_ref().err() - ); - - // Second edit should also work because the edit updated the recorded read time - let edit_result = { - let edit_task = cx.update(|cx| { - edit_tool.clone().run( - EditFileToolInput { - display_description: "Second edit".into(), - path: "root/test.txt".into(), - mode: EditFileMode::Edit, - }, - ToolCallEventStream::test().0, - cx, - ) - }); - - cx.executor().run_until_parked(); - model.send_last_completion_stream_text_chunk( - "modified contentfurther modified content".to_string(), - ); - model.end_last_completion_stream(); - - edit_task.await - }; - assert!( - edit_result.is_ok(), - "Second consecutive edit should succeed, got error: {:?}", - edit_result.as_ref().err() - ); - } - - #[gpui::test] - async fn test_external_modification_detected(cx: &mut TestAppContext) { - init_test(cx); - - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "test.txt": "original content" - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - 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, - Templates::new(), - Some(model.clone()), - cx, - ) - }); - let languages = project.read_with(cx, |project, _| project.languages().clone()); - let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone()); - - let read_tool = Arc::new(crate::ReadFileTool::new( - thread.downgrade(), - project.clone(), - action_log, - )); - let edit_tool = Arc::new(EditFileTool::new( - project.clone(), - thread.downgrade(), - languages, - Templates::new(), - )); - - // Read the file first - cx.update(|cx| { - read_tool.clone().run( - crate::ReadFileToolInput { - path: "root/test.txt".to_string(), - start_line: None, - end_line: None, - }, - ToolCallEventStream::test().0, - cx, - ) - }) - .await - .unwrap(); - - // Simulate external modification - advance time and save file - cx.background_executor - .advance_clock(std::time::Duration::from_secs(2)); - fs.save( - path!("/root/test.txt").as_ref(), - &"externally modified content".into(), - language::LineEnding::Unix, - ) - .await - .unwrap(); - - // Reload the buffer to pick up the new mtime - let project_path = project - .read_with(cx, |project, cx| { - project.find_project_path("root/test.txt", cx) - }) - .expect("Should find project path"); - let buffer = project - .update(cx, |project, cx| project.open_buffer(project_path, cx)) - .await - .unwrap(); - buffer - .update(cx, |buffer, cx| buffer.reload(cx)) - .await - .unwrap(); - - cx.executor().run_until_parked(); - - // Try to edit - should fail because file was modified externally - let result = cx - .update(|cx| { - edit_tool.clone().run( - EditFileToolInput { - display_description: "Edit after external change".into(), - path: "root/test.txt".into(), - mode: EditFileMode::Edit, - }, - ToolCallEventStream::test().0, - cx, - ) - }) - .await; - - assert!( - result.is_err(), - "Edit should fail after external modification" - ); - let error_msg = result.unwrap_err().to_string(); - assert!( - error_msg.contains("has been modified since you last read it"), - "Error should mention file modification, got: {}", - error_msg - ); - } - - #[gpui::test] - async fn test_dirty_buffer_detected(cx: &mut TestAppContext) { - init_test(cx); - - let fs = project::FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "test.txt": "original content" - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - 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, - Templates::new(), - Some(model.clone()), - cx, - ) - }); - let languages = project.read_with(cx, |project, _| project.languages().clone()); - let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone()); - - let read_tool = Arc::new(crate::ReadFileTool::new( - thread.downgrade(), - project.clone(), - action_log, - )); - let edit_tool = Arc::new(EditFileTool::new( - project.clone(), - thread.downgrade(), - languages, - Templates::new(), - )); - - // Read the file first - cx.update(|cx| { - read_tool.clone().run( - crate::ReadFileToolInput { - path: "root/test.txt".to_string(), - start_line: None, - end_line: None, - }, - ToolCallEventStream::test().0, - cx, - ) - }) - .await - .unwrap(); - - // Open the buffer and make it dirty by editing without saving - let project_path = project - .read_with(cx, |project, cx| { - project.find_project_path("root/test.txt", cx) - }) - .expect("Should find project path"); - let buffer = project - .update(cx, |project, cx| project.open_buffer(project_path, cx)) - .await - .unwrap(); - - // Make an in-memory edit to the buffer (making it dirty) - buffer.update(cx, |buffer, cx| { - let end_point = buffer.max_point(); - buffer.edit([(end_point..end_point, " added text")], None, cx); - }); - - // Verify buffer is dirty - let is_dirty = buffer.read_with(cx, |buffer, _| buffer.is_dirty()); - assert!(is_dirty, "Buffer should be dirty after in-memory edit"); - - // Try to edit - should fail because buffer has unsaved changes - let result = cx - .update(|cx| { - edit_tool.clone().run( - EditFileToolInput { - display_description: "Edit with dirty buffer".into(), - path: "root/test.txt".into(), - mode: EditFileMode::Edit, - }, - ToolCallEventStream::test().0, - cx, - ) - }) - .await; - - assert!(result.is_err(), "Edit should fail when buffer is dirty"); - let error_msg = result.unwrap_err().to_string(); - assert!( - error_msg.contains("cannot be written to because it has unsaved changes"), - "Error should mention unsaved changes, got: {}", - error_msg - ); - } -} diff --git a/crates/agent/src/tools/fetch_tool.rs b/crates/agent/src/tools/fetch_tool.rs deleted file mode 100644 index 60654ac863..0000000000 --- a/crates/agent/src/tools/fetch_tool.rs +++ /dev/null @@ -1,164 +0,0 @@ -use std::rc::Rc; -use std::sync::Arc; -use std::{borrow::Cow, cell::RefCell}; - -use agent_client_protocol as acp; -use anyhow::{Context as _, Result, bail}; -use futures::AsyncReadExt as _; -use gpui::{App, AppContext as _, Task}; -use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown}; -use http_client::{AsyncBody, HttpClientWithUrl}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use ui::SharedString; -use util::markdown::MarkdownEscaped; - -use crate::{AgentTool, ToolCallEventStream}; - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -enum ContentType { - Html, - Plaintext, - Json, -} - -/// Fetches a URL and returns the content as Markdown. -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct FetchToolInput { - /// The URL to fetch. - url: String, -} - -pub struct FetchTool { - http_client: Arc, -} - -impl FetchTool { - pub fn new(http_client: Arc) -> Self { - Self { http_client } - } - - async fn build_message(http_client: Arc, url: &str) -> Result { - let url = if !url.starts_with("https://") && !url.starts_with("http://") { - Cow::Owned(format!("https://{url}")) - } else { - Cow::Borrowed(url) - }; - - let mut response = http_client.get(&url, AsyncBody::default(), true).await?; - - let mut body = Vec::new(); - response - .body_mut() - .read_to_end(&mut body) - .await - .context("error reading response body")?; - - if response.status().is_client_error() { - let text = String::from_utf8_lossy(body.as_slice()); - bail!( - "status error {}, response: {text:?}", - response.status().as_u16() - ); - } - - let Some(content_type) = response.headers().get("content-type") else { - bail!("missing Content-Type header"); - }; - let content_type = content_type - .to_str() - .context("invalid Content-Type header")?; - - let content_type = if content_type.starts_with("text/plain") { - ContentType::Plaintext - } else if content_type.starts_with("application/json") { - ContentType::Json - } else { - ContentType::Html - }; - - match content_type { - ContentType::Html => { - let mut handlers: Vec = vec![ - Rc::new(RefCell::new(markdown::WebpageChromeRemover)), - Rc::new(RefCell::new(markdown::ParagraphHandler)), - Rc::new(RefCell::new(markdown::HeadingHandler)), - Rc::new(RefCell::new(markdown::ListHandler)), - Rc::new(RefCell::new(markdown::TableHandler::new())), - Rc::new(RefCell::new(markdown::StyledTextHandler)), - ]; - if url.contains("wikipedia.org") { - use html_to_markdown::structure::wikipedia; - - handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover))); - handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler))); - handlers.push(Rc::new( - RefCell::new(wikipedia::WikipediaCodeHandler::new()), - )); - } else { - handlers.push(Rc::new(RefCell::new(markdown::CodeHandler))); - } - - convert_html_to_markdown(&body[..], &mut handlers) - } - ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()), - ContentType::Json => { - let json: serde_json::Value = serde_json::from_slice(&body)?; - - Ok(format!( - "```json\n{}\n```", - serde_json::to_string_pretty(&json)? - )) - } - } - } -} - -impl AgentTool for FetchTool { - type Input = FetchToolInput; - type Output = String; - - fn name() -> &'static str { - "fetch" - } - - fn kind() -> acp::ToolKind { - acp::ToolKind::Fetch - } - - fn initial_title( - &self, - input: Result, - _cx: &mut App, - ) -> SharedString { - match input { - Ok(input) => format!("Fetch {}", MarkdownEscaped(&input.url)).into(), - Err(_) => "Fetch URL".into(), - } - } - - fn run( - self: Arc, - input: Self::Input, - 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 - } - }); - - cx.foreground_executor().spawn(async move { - let text = text.await?; - if text.trim().is_empty() { - bail!("no textual content found"); - } - Ok(text) - }) - } -} diff --git a/crates/agent/src/tools/find_path_tool.rs b/crates/agent/src/tools/find_path_tool.rs deleted file mode 100644 index 2a33b14b4c..0000000000 --- a/crates/agent/src/tools/find_path_tool.rs +++ /dev/null @@ -1,247 +0,0 @@ -use crate::{AgentTool, ToolCallEventStream}; -use agent_client_protocol as acp; -use anyhow::{Result, anyhow}; -use gpui::{App, AppContext, Entity, SharedString, Task}; -use language_model::LanguageModelToolResultContent; -use project::Project; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::fmt::Write; -use std::{cmp, path::PathBuf, sync::Arc}; -use util::paths::PathMatcher; - -/// Fast file path pattern matching tool that works with any codebase size -/// -/// - Supports glob patterns like "**/*.js" or "src/**/*.ts" -/// - Returns matching file paths sorted alphabetically -/// - Prefer the `grep` tool to this tool when searching for symbols unless you have specific information about paths. -/// - Use this tool when you need to find files by name patterns -/// - Results are paginated with 50 matches per page. Use the optional 'offset' parameter to request subsequent pages. -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct FindPathToolInput { - /// The glob to match against every path in the project. - /// - /// - /// If the project has the following root directories: - /// - /// - directory1/a/something.txt - /// - directory2/a/things.txt - /// - directory3/a/other.txt - /// - /// You can get back the first two paths by providing a glob of "*thing*.txt" - /// - pub glob: String, - /// Optional starting position for paginated results (0-based). - /// When not provided, starts from the beginning. - #[serde(default)] - pub offset: usize, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct FindPathToolOutput { - offset: usize, - current_matches_page: Vec, - all_matches_len: usize, -} - -impl From for LanguageModelToolResultContent { - fn from(output: FindPathToolOutput) -> Self { - if output.current_matches_page.is_empty() { - "No matches found".into() - } else { - let mut llm_output = format!("Found {} total matches.", output.all_matches_len); - if output.all_matches_len > RESULTS_PER_PAGE { - write!( - &mut llm_output, - "\nShowing results {}-{} (provide 'offset' parameter for more results):", - output.offset + 1, - output.offset + output.current_matches_page.len() - ) - .unwrap(); - } - - for mat in output.current_matches_page { - write!(&mut llm_output, "\n{}", mat.display()).unwrap(); - } - - llm_output.into() - } - } -} - -const RESULTS_PER_PAGE: usize = 50; - -pub struct FindPathTool { - project: Entity, -} - -impl FindPathTool { - pub fn new(project: Entity) -> Self { - Self { project } - } -} - -impl AgentTool for FindPathTool { - type Input = FindPathToolInput; - type Output = FindPathToolOutput; - - fn name() -> &'static str { - "find_path" - } - - fn kind() -> acp::ToolKind { - acp::ToolKind::Search - } - - fn initial_title( - &self, - input: Result, - _cx: &mut App, - ) -> SharedString { - let mut title = "Find paths".to_string(); - if let Ok(input) = input { - title.push_str(&format!(" matching “`{}`”", input.glob)); - } - title.into() - } - - fn run( - self: Arc, - input: Self::Input, - event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - let search_paths_task = search_paths(&input.glob, self.project.clone(), cx); - - cx.background_spawn(async move { - let matches = search_paths_task.await?; - let paginated_matches: &[PathBuf] = &matches[cmp::min(input.offset, matches.len()) - ..cmp::min(input.offset + RESULTS_PER_PAGE, matches.len())]; - - event_stream.update_fields( - acp::ToolCallUpdateFields::new() - .title(if paginated_matches.is_empty() { - "No matches".into() - } else if paginated_matches.len() == 1 { - "1 match".into() - } else { - format!("{} matches", paginated_matches.len()) - }) - .content( - paginated_matches - .iter() - .map(|path| { - acp::ToolCallContent::Content(acp::Content::new( - acp::ContentBlock::ResourceLink(acp::ResourceLink::new( - path.to_string_lossy(), - format!("file://{}", path.display()), - )), - )) - }) - .collect::>(), - ), - ); - - Ok(FindPathToolOutput { - offset: input.offset, - current_matches_page: paginated_matches.to_vec(), - all_matches_len: matches.len(), - }) - }) - } -} - -fn search_paths(glob: &str, project: Entity, cx: &mut App) -> Task>> { - let path_style = project.read(cx).path_style(cx); - let path_matcher = match PathMatcher::new( - [ - // Sometimes models try to search for "". In this case, return all paths in the project. - if glob.is_empty() { "*" } else { glob }, - ], - path_style, - ) { - Ok(matcher) => matcher, - Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {err}"))), - }; - let snapshots: Vec<_> = project - .read(cx) - .worktrees(cx) - .map(|worktree| worktree.read(cx).snapshot()) - .collect(); - - cx.background_spawn(async move { - let mut results = Vec::new(); - for snapshot in snapshots { - for entry in snapshot.entries(false, 0) { - if path_matcher.is_match(&snapshot.root_name().join(&entry.path)) { - results.push(snapshot.absolutize(&entry.path)); - } - } - } - - Ok(results) - }) -} - -#[cfg(test)] -mod test { - use super::*; - use gpui::TestAppContext; - use project::{FakeFs, Project}; - use settings::SettingsStore; - use util::path; - - #[gpui::test] - async fn test_find_path_tool(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - serde_json::json!({ - "apple": { - "banana": { - "carrot": "1", - }, - "bandana": { - "carbonara": "2", - }, - "endive": "3" - } - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - - let matches = cx - .update(|cx| search_paths("root/**/car*", project.clone(), cx)) - .await - .unwrap(); - assert_eq!( - matches, - &[ - PathBuf::from(path!("/root/apple/banana/carrot")), - PathBuf::from(path!("/root/apple/bandana/carbonara")) - ] - ); - - let matches = cx - .update(|cx| search_paths("**/car*", project.clone(), cx)) - .await - .unwrap(); - assert_eq!( - matches, - &[ - PathBuf::from(path!("/root/apple/banana/carrot")), - PathBuf::from(path!("/root/apple/bandana/carbonara")) - ] - ); - } - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - }); - } -} diff --git a/crates/agent/src/tools/grep_tool.rs b/crates/agent/src/tools/grep_tool.rs deleted file mode 100644 index 0caba91564..0000000000 --- a/crates/agent/src/tools/grep_tool.rs +++ /dev/null @@ -1,1183 +0,0 @@ -use crate::{AgentTool, ToolCallEventStream}; -use agent_client_protocol as acp; -use anyhow::{Result, anyhow}; -use futures::StreamExt; -use gpui::{App, Entity, SharedString, Task}; -use language::{OffsetRangeExt, ParseStatus, Point}; -use project::{ - Project, WorktreeSettings, - search::{SearchQuery, SearchResult}, -}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::Settings; -use std::{cmp, fmt::Write, sync::Arc}; -use util::RangeExt; -use util::markdown::MarkdownInlineCode; -use util::paths::PathMatcher; - -/// Searches the contents of files in the project with a regular expression -/// -/// - Prefer this tool to path search when searching for symbols in the project, because you won't need to guess what path it's in. -/// - Supports full regex syntax (eg. "log.*Error", "function\\s+\\w+", etc.) -/// - Pass an `include_pattern` if you know how to narrow your search on the files system -/// - Never use this tool to search for paths. Only search file contents with this tool. -/// - Use this tool when you need to find files containing specific patterns -/// - Results are paginated with 20 matches per page. Use the optional 'offset' parameter to request subsequent pages. -/// - DO NOT use HTML entities solely to escape characters in the tool parameters. -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct GrepToolInput { - /// A regex pattern to search for in the entire project. Note that the regex will be parsed by the Rust `regex` crate. - /// - /// Do NOT specify a path here! This will only be matched against the code **content**. - pub regex: String, - /// A glob pattern for the paths of files to include in the search. - /// Supports standard glob patterns like "**/*.rs" or "frontend/src/**/*.ts". - /// If omitted, all files in the project will be searched. - /// - /// The glob pattern is matched against the full path including the project root directory. - /// - /// - /// If the project has the following root directories: - /// - /// - /a/b/backend - /// - /c/d/frontend - /// - /// Use "backend/**/*.rs" to search only Rust files in the backend root directory. - /// Use "frontend/src/**/*.ts" to search TypeScript files only in the frontend root directory (sub-directory "src"). - /// Use "**/*.rs" to search Rust files across all root directories. - /// - pub include_pattern: Option, - /// Optional starting position for paginated results (0-based). - /// When not provided, starts from the beginning. - #[serde(default)] - pub offset: u32, - /// Whether the regex is case-sensitive. Defaults to false (case-insensitive). - #[serde(default)] - pub case_sensitive: bool, -} - -impl GrepToolInput { - /// Which page of search results this is. - pub fn page(&self) -> u32 { - 1 + (self.offset / RESULTS_PER_PAGE) - } -} - -const RESULTS_PER_PAGE: u32 = 20; - -pub struct GrepTool { - project: Entity, -} - -impl GrepTool { - pub fn new(project: Entity) -> Self { - Self { project } - } -} - -impl AgentTool for GrepTool { - type Input = GrepToolInput; - type Output = String; - - fn name() -> &'static str { - "grep" - } - - fn kind() -> acp::ToolKind { - acp::ToolKind::Search - } - - fn initial_title( - &self, - input: Result, - _cx: &mut App, - ) -> SharedString { - match input { - Ok(input) => { - let page = input.page(); - let regex_str = MarkdownInlineCode(&input.regex); - let case_info = if input.case_sensitive { - " (case-sensitive)" - } else { - "" - }; - - if page > 1 { - format!("Get page {page} of search results for regex {regex_str}{case_info}") - } else { - format!("Search files for regex {regex_str}{case_info}") - } - } - Err(_) => "Search with regex".into(), - } - .into() - } - - fn run( - self: Arc, - input: Self::Input, - _event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - const CONTEXT_LINES: u32 = 2; - const MAX_ANCESTOR_LINES: u32 = 10; - - let path_style = self.project.read(cx).path_style(cx); - - let include_matcher = match PathMatcher::new( - input - .include_pattern - .as_ref() - .into_iter() - .collect::>(), - path_style, - ) { - Ok(matcher) => matcher, - Err(error) => { - return Task::ready(Err(anyhow!("invalid include glob pattern: {error}"))); - } - }; - - // Exclude global file_scan_exclusions and private_files settings - let exclude_matcher = { - let global_settings = WorktreeSettings::get_global(cx); - let exclude_patterns = global_settings - .file_scan_exclusions - .sources() - .chain(global_settings.private_files.sources()); - - match PathMatcher::new(exclude_patterns, path_style) { - Ok(matcher) => matcher, - Err(error) => { - return Task::ready(Err(anyhow!("invalid exclude pattern: {error}"))); - } - } - }; - - let query = match SearchQuery::regex( - &input.regex, - false, - input.case_sensitive, - false, - false, - include_matcher, - exclude_matcher, - true, // Always match file include pattern against *full project paths* that start with a project root. - None, - ) { - Ok(query) => query, - Err(error) => return Task::ready(Err(error)), - }; - - let results = self - .project - .update(cx, |project, cx| project.search(query, cx)); - - let project = self.project.downgrade(); - cx.spawn(async move |cx| { - futures::pin_mut!(results); - - let mut output = String::new(); - let mut skips_remaining = input.offset; - let mut matches_found = 0; - let mut has_more_matches = false; - - 'outer: while let Some(SearchResult::Buffer { buffer, ranges }) = results.next().await { - if ranges.is_empty() { - continue; - } - - let Ok((Some(path), mut parse_status)) = buffer.read_with(cx, |buffer, cx| { - (buffer.file().map(|file| file.full_path(cx)), buffer.parse_status()) - }) else { - continue; - }; - - // Check if this file should be excluded based on its worktree settings - if let Ok(Some(project_path)) = project.read_with(cx, |project, cx| { - project.find_project_path(&path, cx) - }) - && cx.update(|cx| { - let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx); - worktree_settings.is_path_excluded(&project_path.path) - || worktree_settings.is_path_private(&project_path.path) - }).unwrap_or(false) { - continue; - } - - while *parse_status.borrow() != ParseStatus::Idle { - parse_status.changed().await?; - } - - let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; - - let mut ranges = ranges - .into_iter() - .map(|range| { - let matched = range.to_point(&snapshot); - let matched_end_line_len = snapshot.line_len(matched.end.row); - let full_lines = Point::new(matched.start.row, 0)..Point::new(matched.end.row, matched_end_line_len); - let symbols = snapshot.symbols_containing(matched.start, None); - - if let Some(ancestor_node) = snapshot.syntax_ancestor(full_lines.clone()) { - let full_ancestor_range = ancestor_node.byte_range().to_point(&snapshot); - let end_row = full_ancestor_range.end.row.min(full_ancestor_range.start.row + MAX_ANCESTOR_LINES); - let end_col = snapshot.line_len(end_row); - let capped_ancestor_range = Point::new(full_ancestor_range.start.row, 0)..Point::new(end_row, end_col); - - if capped_ancestor_range.contains_inclusive(&full_lines) { - return (capped_ancestor_range, Some(full_ancestor_range), symbols) - } - } - - let mut matched = matched; - matched.start.column = 0; - matched.start.row = - matched.start.row.saturating_sub(CONTEXT_LINES); - matched.end.row = cmp::min( - snapshot.max_point().row, - matched.end.row + CONTEXT_LINES, - ); - matched.end.column = snapshot.line_len(matched.end.row); - - (matched, None, symbols) - }) - .peekable(); - - let mut file_header_written = false; - - while let Some((mut range, ancestor_range, parent_symbols)) = ranges.next(){ - if skips_remaining > 0 { - skips_remaining -= 1; - continue; - } - - // We'd already found a full page of matches, and we just found one more. - if matches_found >= RESULTS_PER_PAGE { - has_more_matches = true; - break 'outer; - } - - while let Some((next_range, _, _)) = ranges.peek() { - if range.end.row >= next_range.start.row { - range.end = next_range.end; - ranges.next(); - } else { - break; - } - } - - if !file_header_written { - writeln!(output, "\n## Matches in {}", path.display())?; - file_header_written = true; - } - - let end_row = range.end.row; - output.push_str("\n### "); - - for symbol in parent_symbols { - write!(output, "{} › ", symbol.text)?; - } - - if range.start.row == end_row { - writeln!(output, "L{}", range.start.row + 1)?; - } else { - writeln!(output, "L{}-{}", range.start.row + 1, end_row + 1)?; - } - - output.push_str("```\n"); - output.extend(snapshot.text_for_range(range)); - output.push_str("\n```\n"); - - if let Some(ancestor_range) = ancestor_range - && end_row < ancestor_range.end.row { - let remaining_lines = ancestor_range.end.row - end_row; - writeln!(output, "\n{} lines remaining in ancestor node. Read the file to see all.", remaining_lines)?; - } - - matches_found += 1; - } - } - - if matches_found == 0 { - Ok("No matches found".into()) - } else if has_more_matches { - Ok(format!( - "Showing matches {}-{} (there were more matches found; use offset: {} to see next page):\n{output}", - input.offset + 1, - input.offset + matches_found, - input.offset + RESULTS_PER_PAGE, - )) - } else { - Ok(format!("Found {matches_found} matches:\n{output}")) - } - }) - } -} - -#[cfg(test)] -mod tests { - use crate::ToolCallEventStream; - - use super::*; - use gpui::{TestAppContext, UpdateGlobal}; - use project::{FakeFs, Project}; - use serde_json::json; - use settings::SettingsStore; - use unindent::Unindent; - use util::path; - - #[gpui::test] - async fn test_grep_tool_with_include_pattern(cx: &mut TestAppContext) { - init_test(cx); - cx.executor().allow_parking(); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - serde_json::json!({ - "src": { - "main.rs": "fn main() {\n println!(\"Hello, world!\");\n}", - "utils": { - "helper.rs": "fn helper() {\n println!(\"I'm a helper!\");\n}", - }, - }, - "tests": { - "test_main.rs": "fn test_main() {\n assert!(true);\n}", - } - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - - // Test with include pattern for Rust files inside the root of the project - let input = GrepToolInput { - regex: "println".to_string(), - include_pattern: Some("root/**/*.rs".to_string()), - offset: 0, - case_sensitive: false, - }; - - let result = run_grep_tool(input, project.clone(), cx).await; - assert!(result.contains("main.rs"), "Should find matches in main.rs"); - assert!( - result.contains("helper.rs"), - "Should find matches in helper.rs" - ); - assert!( - !result.contains("test_main.rs"), - "Should not include test_main.rs even though it's a .rs file (because it doesn't have the pattern)" - ); - - // Test with include pattern for src directory only - let input = GrepToolInput { - regex: "fn".to_string(), - include_pattern: Some("root/**/src/**".to_string()), - offset: 0, - case_sensitive: false, - }; - - let result = run_grep_tool(input, project.clone(), cx).await; - assert!( - result.contains("main.rs"), - "Should find matches in src/main.rs" - ); - assert!( - result.contains("helper.rs"), - "Should find matches in src/utils/helper.rs" - ); - assert!( - !result.contains("test_main.rs"), - "Should not include test_main.rs as it's not in src directory" - ); - - // Test with empty include pattern (should default to all files) - let input = GrepToolInput { - regex: "fn".to_string(), - include_pattern: None, - offset: 0, - case_sensitive: false, - }; - - let result = run_grep_tool(input, project.clone(), cx).await; - assert!(result.contains("main.rs"), "Should find matches in main.rs"); - assert!( - result.contains("helper.rs"), - "Should find matches in helper.rs" - ); - assert!( - result.contains("test_main.rs"), - "Should include test_main.rs" - ); - } - - #[gpui::test] - async fn test_grep_tool_with_case_sensitivity(cx: &mut TestAppContext) { - init_test(cx); - cx.executor().allow_parking(); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - serde_json::json!({ - "case_test.txt": "This file has UPPERCASE and lowercase text.\nUPPERCASE patterns should match only with case_sensitive: true", - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - - // Test case-insensitive search (default) - let input = GrepToolInput { - regex: "uppercase".to_string(), - include_pattern: Some("**/*.txt".to_string()), - offset: 0, - case_sensitive: false, - }; - - let result = run_grep_tool(input, project.clone(), cx).await; - assert!( - result.contains("UPPERCASE"), - "Case-insensitive search should match uppercase" - ); - - // Test case-sensitive search - let input = GrepToolInput { - regex: "uppercase".to_string(), - include_pattern: Some("**/*.txt".to_string()), - offset: 0, - case_sensitive: true, - }; - - let result = run_grep_tool(input, project.clone(), cx).await; - assert!( - !result.contains("UPPERCASE"), - "Case-sensitive search should not match uppercase" - ); - - // Test case-sensitive search - let input = GrepToolInput { - regex: "LOWERCASE".to_string(), - include_pattern: Some("**/*.txt".to_string()), - offset: 0, - case_sensitive: true, - }; - - let result = run_grep_tool(input, project.clone(), cx).await; - - assert!( - !result.contains("lowercase"), - "Case-sensitive search should match lowercase" - ); - - // Test case-sensitive search for lowercase pattern - let input = GrepToolInput { - regex: "lowercase".to_string(), - include_pattern: Some("**/*.txt".to_string()), - offset: 0, - case_sensitive: true, - }; - - let result = run_grep_tool(input, project.clone(), cx).await; - assert!( - result.contains("lowercase"), - "Case-sensitive search should match lowercase text" - ); - } - - /// Helper function to set up a syntax test environment - async fn setup_syntax_test(cx: &mut TestAppContext) -> Entity { - use unindent::Unindent; - init_test(cx); - cx.executor().allow_parking(); - - let fs = FakeFs::new(cx.executor()); - - // Create test file with syntax structures - fs.insert_tree( - path!("/root"), - serde_json::json!({ - "test_syntax.rs": r#" - fn top_level_function() { - println!("This is at the top level"); - } - - mod feature_module { - pub mod nested_module { - pub fn nested_function( - first_arg: String, - second_arg: i32, - ) { - println!("Function in nested module"); - println!("{first_arg}"); - println!("{second_arg}"); - } - } - } - - struct MyStruct { - field1: String, - field2: i32, - } - - impl MyStruct { - fn method_with_block() { - let condition = true; - if condition { - println!("Inside if block"); - } - } - - fn long_function() { - println!("Line 1"); - println!("Line 2"); - println!("Line 3"); - println!("Line 4"); - println!("Line 5"); - println!("Line 6"); - println!("Line 7"); - println!("Line 8"); - println!("Line 9"); - println!("Line 10"); - println!("Line 11"); - println!("Line 12"); - } - } - - trait Processor { - fn process(&self, input: &str) -> String; - } - - impl Processor for MyStruct { - fn process(&self, input: &str) -> String { - format!("Processed: {}", input) - } - } - "#.unindent().trim(), - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - - project.update(cx, |project, _cx| { - project.languages().add(language::rust_lang()) - }); - - project - } - - #[gpui::test] - async fn test_grep_top_level_function(cx: &mut TestAppContext) { - let project = setup_syntax_test(cx).await; - - // Test: Line at the top level of the file - let input = GrepToolInput { - regex: "This is at the top level".to_string(), - include_pattern: Some("**/*.rs".to_string()), - offset: 0, - case_sensitive: false, - }; - - let result = run_grep_tool(input, project.clone(), cx).await; - let expected = r#" - Found 1 matches: - - ## Matches in root/test_syntax.rs - - ### fn top_level_function › L1-3 - ``` - fn top_level_function() { - println!("This is at the top level"); - } - ``` - "# - .unindent(); - assert_eq!(result, expected); - } - - #[gpui::test] - async fn test_grep_function_body(cx: &mut TestAppContext) { - let project = setup_syntax_test(cx).await; - - // Test: Line inside a function body - let input = GrepToolInput { - regex: "Function in nested module".to_string(), - include_pattern: Some("**/*.rs".to_string()), - offset: 0, - case_sensitive: false, - }; - - let result = run_grep_tool(input, project.clone(), cx).await; - let expected = r#" - Found 1 matches: - - ## Matches in root/test_syntax.rs - - ### mod feature_module › pub mod nested_module › pub fn nested_function › L10-14 - ``` - ) { - println!("Function in nested module"); - println!("{first_arg}"); - println!("{second_arg}"); - } - ``` - "# - .unindent(); - assert_eq!(result, expected); - } - - #[gpui::test] - async fn test_grep_function_args_and_body(cx: &mut TestAppContext) { - let project = setup_syntax_test(cx).await; - - // Test: Line with a function argument - let input = GrepToolInput { - regex: "second_arg".to_string(), - include_pattern: Some("**/*.rs".to_string()), - offset: 0, - case_sensitive: false, - }; - - let result = run_grep_tool(input, project.clone(), cx).await; - let expected = r#" - Found 1 matches: - - ## Matches in root/test_syntax.rs - - ### mod feature_module › pub mod nested_module › pub fn nested_function › L7-14 - ``` - pub fn nested_function( - first_arg: String, - second_arg: i32, - ) { - println!("Function in nested module"); - println!("{first_arg}"); - println!("{second_arg}"); - } - ``` - "# - .unindent(); - assert_eq!(result, expected); - } - - #[gpui::test] - async fn test_grep_if_block(cx: &mut TestAppContext) { - use unindent::Unindent; - let project = setup_syntax_test(cx).await; - - // Test: Line inside an if block - let input = GrepToolInput { - regex: "Inside if block".to_string(), - include_pattern: Some("**/*.rs".to_string()), - offset: 0, - case_sensitive: false, - }; - - let result = run_grep_tool(input, project.clone(), cx).await; - let expected = r#" - Found 1 matches: - - ## Matches in root/test_syntax.rs - - ### impl MyStruct › fn method_with_block › L26-28 - ``` - if condition { - println!("Inside if block"); - } - ``` - "# - .unindent(); - assert_eq!(result, expected); - } - - #[gpui::test] - async fn test_grep_long_function_top(cx: &mut TestAppContext) { - use unindent::Unindent; - let project = setup_syntax_test(cx).await; - - // Test: Line in the middle of a long function - should show message about remaining lines - let input = GrepToolInput { - regex: "Line 5".to_string(), - include_pattern: Some("**/*.rs".to_string()), - offset: 0, - case_sensitive: false, - }; - - let result = run_grep_tool(input, project.clone(), cx).await; - let expected = r#" - Found 1 matches: - - ## Matches in root/test_syntax.rs - - ### impl MyStruct › fn long_function › L31-41 - ``` - fn long_function() { - println!("Line 1"); - println!("Line 2"); - println!("Line 3"); - println!("Line 4"); - println!("Line 5"); - println!("Line 6"); - println!("Line 7"); - println!("Line 8"); - println!("Line 9"); - println!("Line 10"); - ``` - - 3 lines remaining in ancestor node. Read the file to see all. - "# - .unindent(); - assert_eq!(result, expected); - } - - #[gpui::test] - async fn test_grep_long_function_bottom(cx: &mut TestAppContext) { - use unindent::Unindent; - let project = setup_syntax_test(cx).await; - - // Test: Line in the long function - let input = GrepToolInput { - regex: "Line 12".to_string(), - include_pattern: Some("**/*.rs".to_string()), - offset: 0, - case_sensitive: false, - }; - - let result = run_grep_tool(input, project.clone(), cx).await; - let expected = r#" - Found 1 matches: - - ## Matches in root/test_syntax.rs - - ### impl MyStruct › fn long_function › L41-45 - ``` - println!("Line 10"); - println!("Line 11"); - println!("Line 12"); - } - } - ``` - "# - .unindent(); - assert_eq!(result, expected); - } - - async fn run_grep_tool( - input: GrepToolInput, - project: Entity, - cx: &mut TestAppContext, - ) -> String { - let tool = Arc::new(GrepTool { project }); - let task = cx.update(|cx| tool.run(input, ToolCallEventStream::test().0, cx)); - - match task.await { - Ok(result) => { - if cfg!(windows) { - result.replace("root\\", "root/") - } else { - result - } - } - Err(e) => panic!("Failed to run grep tool: {}", e), - } - } - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - }); - } - - #[gpui::test] - async fn test_grep_security_boundaries(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - - fs.insert_tree( - path!("/"), - json!({ - "project_root": { - "allowed_file.rs": "fn main() { println!(\"This file is in the project\"); }", - ".mysecrets": "SECRET_KEY=abc123\nfn secret() { /* private */ }", - ".secretdir": { - "config": "fn special_configuration() { /* excluded */ }" - }, - ".mymetadata": "fn custom_metadata() { /* excluded */ }", - "subdir": { - "normal_file.rs": "fn normal_file_content() { /* Normal */ }", - "special.privatekey": "fn private_key_content() { /* private */ }", - "data.mysensitive": "fn sensitive_data() { /* private */ }" - } - }, - "outside_project": { - "sensitive_file.rs": "fn outside_function() { /* This file is outside the project */ }" - } - }), - ) - .await; - - cx.update(|cx| { - use gpui::UpdateGlobal; - use settings::SettingsStore; - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(vec![ - "**/.secretdir".to_string(), - "**/.mymetadata".to_string(), - ]); - settings.project.worktree.private_files = Some( - vec![ - "**/.mysecrets".to_string(), - "**/*.privatekey".to_string(), - "**/*.mysensitive".to_string(), - ] - .into(), - ); - }); - }); - }); - - let project = Project::test(fs.clone(), [path!("/project_root").as_ref()], cx).await; - - // Searching for files outside the project worktree should return no results - let result = run_grep_tool( - GrepToolInput { - regex: "outside_function".to_string(), - include_pattern: None, - offset: 0, - case_sensitive: false, - }, - project.clone(), - cx, - ) - .await; - let paths = extract_paths_from_results(&result); - assert!( - paths.is_empty(), - "grep_tool should not find files outside the project worktree" - ); - - // Searching within the project should succeed - let result = run_grep_tool( - GrepToolInput { - regex: "main".to_string(), - include_pattern: None, - offset: 0, - case_sensitive: false, - }, - project.clone(), - cx, - ) - .await; - let paths = extract_paths_from_results(&result); - assert!( - paths.iter().any(|p| p.contains("allowed_file.rs")), - "grep_tool should be able to search files inside worktrees" - ); - - // Searching files that match file_scan_exclusions should return no results - let result = run_grep_tool( - GrepToolInput { - regex: "special_configuration".to_string(), - include_pattern: None, - offset: 0, - case_sensitive: false, - }, - project.clone(), - cx, - ) - .await; - let paths = extract_paths_from_results(&result); - assert!( - paths.is_empty(), - "grep_tool should not search files in .secretdir (file_scan_exclusions)" - ); - - let result = run_grep_tool( - GrepToolInput { - regex: "custom_metadata".to_string(), - include_pattern: None, - offset: 0, - case_sensitive: false, - }, - project.clone(), - cx, - ) - .await; - let paths = extract_paths_from_results(&result); - assert!( - paths.is_empty(), - "grep_tool should not search .mymetadata files (file_scan_exclusions)" - ); - - // Searching private files should return no results - let result = run_grep_tool( - GrepToolInput { - regex: "SECRET_KEY".to_string(), - include_pattern: None, - offset: 0, - case_sensitive: false, - }, - project.clone(), - cx, - ) - .await; - let paths = extract_paths_from_results(&result); - assert!( - paths.is_empty(), - "grep_tool should not search .mysecrets (private_files)" - ); - - let result = run_grep_tool( - GrepToolInput { - regex: "private_key_content".to_string(), - include_pattern: None, - offset: 0, - case_sensitive: false, - }, - project.clone(), - cx, - ) - .await; - let paths = extract_paths_from_results(&result); - - assert!( - paths.is_empty(), - "grep_tool should not search .privatekey files (private_files)" - ); - - let result = run_grep_tool( - GrepToolInput { - regex: "sensitive_data".to_string(), - include_pattern: None, - offset: 0, - case_sensitive: false, - }, - project.clone(), - cx, - ) - .await; - let paths = extract_paths_from_results(&result); - assert!( - paths.is_empty(), - "grep_tool should not search .mysensitive files (private_files)" - ); - - // Searching a normal file should still work, even with private_files configured - let result = run_grep_tool( - GrepToolInput { - regex: "normal_file_content".to_string(), - include_pattern: None, - offset: 0, - case_sensitive: false, - }, - project.clone(), - cx, - ) - .await; - let paths = extract_paths_from_results(&result); - assert!( - paths.iter().any(|p| p.contains("normal_file.rs")), - "Should be able to search normal files" - ); - - // Path traversal attempts with .. in include_pattern should not escape project - let result = run_grep_tool( - GrepToolInput { - regex: "outside_function".to_string(), - include_pattern: Some("../outside_project/**/*.rs".to_string()), - offset: 0, - case_sensitive: false, - }, - project.clone(), - cx, - ) - .await; - let paths = extract_paths_from_results(&result); - assert!( - paths.is_empty(), - "grep_tool should not allow escaping project boundaries with relative paths" - ); - } - - #[gpui::test] - async fn test_grep_with_multiple_worktree_settings(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - - // Create first worktree with its own private files - fs.insert_tree( - path!("/worktree1"), - json!({ - ".zed": { - "settings.json": r#"{ - "file_scan_exclusions": ["**/fixture.*"], - "private_files": ["**/secret.rs"] - }"# - }, - "src": { - "main.rs": "fn main() { let secret_key = \"hidden\"; }", - "secret.rs": "const API_KEY: &str = \"secret_value\";", - "utils.rs": "pub fn get_config() -> String { \"config\".to_string() }" - }, - "tests": { - "test.rs": "fn test_secret() { assert!(true); }", - "fixture.sql": "SELECT * FROM secret_table;" - } - }), - ) - .await; - - // Create second worktree with different private files - fs.insert_tree( - path!("/worktree2"), - json!({ - ".zed": { - "settings.json": r#"{ - "file_scan_exclusions": ["**/internal.*"], - "private_files": ["**/private.js", "**/data.json"] - }"# - }, - "lib": { - "public.js": "export function getSecret() { return 'public'; }", - "private.js": "const SECRET_KEY = \"private_value\";", - "data.json": "{\"secret_data\": \"hidden\"}" - }, - "docs": { - "README.md": "# Documentation with secret info", - "internal.md": "Internal secret documentation" - } - }), - ) - .await; - - // Set global settings - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = - Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]); - settings.project.worktree.private_files = - Some(vec!["**/.env".to_string()].into()); - }); - }); - }); - - let project = Project::test( - fs.clone(), - [path!("/worktree1").as_ref(), path!("/worktree2").as_ref()], - cx, - ) - .await; - - // Wait for worktrees to be fully scanned - cx.executor().run_until_parked(); - - // Search for "secret" - should exclude files based on worktree-specific settings - let result = run_grep_tool( - GrepToolInput { - regex: "secret".to_string(), - include_pattern: None, - offset: 0, - case_sensitive: false, - }, - project.clone(), - cx, - ) - .await; - let paths = extract_paths_from_results(&result); - - // Should find matches in non-private files - assert!( - paths.iter().any(|p| p.contains("main.rs")), - "Should find 'secret' in worktree1/src/main.rs" - ); - assert!( - paths.iter().any(|p| p.contains("test.rs")), - "Should find 'secret' in worktree1/tests/test.rs" - ); - assert!( - paths.iter().any(|p| p.contains("public.js")), - "Should find 'secret' in worktree2/lib/public.js" - ); - assert!( - paths.iter().any(|p| p.contains("README.md")), - "Should find 'secret' in worktree2/docs/README.md" - ); - - // Should NOT find matches in private/excluded files based on worktree settings - assert!( - !paths.iter().any(|p| p.contains("secret.rs")), - "Should not search in worktree1/src/secret.rs (local private_files)" - ); - assert!( - !paths.iter().any(|p| p.contains("fixture.sql")), - "Should not search in worktree1/tests/fixture.sql (local file_scan_exclusions)" - ); - assert!( - !paths.iter().any(|p| p.contains("private.js")), - "Should not search in worktree2/lib/private.js (local private_files)" - ); - assert!( - !paths.iter().any(|p| p.contains("data.json")), - "Should not search in worktree2/lib/data.json (local private_files)" - ); - assert!( - !paths.iter().any(|p| p.contains("internal.md")), - "Should not search in worktree2/docs/internal.md (local file_scan_exclusions)" - ); - - // Test with `include_pattern` specific to one worktree - let result = run_grep_tool( - GrepToolInput { - regex: "secret".to_string(), - include_pattern: Some("worktree1/**/*.rs".to_string()), - offset: 0, - case_sensitive: false, - }, - project.clone(), - cx, - ) - .await; - - let paths = extract_paths_from_results(&result); - - // Should only find matches in worktree1 *.rs files (excluding private ones) - assert!( - paths.iter().any(|p| p.contains("main.rs")), - "Should find match in worktree1/src/main.rs" - ); - assert!( - paths.iter().any(|p| p.contains("test.rs")), - "Should find match in worktree1/tests/test.rs" - ); - assert!( - !paths.iter().any(|p| p.contains("secret.rs")), - "Should not find match in excluded worktree1/src/secret.rs" - ); - assert!( - paths.iter().all(|p| !p.contains("worktree2")), - "Should not find any matches in worktree2" - ); - } - - // Helper function to extract file paths from grep results - fn extract_paths_from_results(results: &str) -> Vec { - results - .lines() - .filter(|line| line.starts_with("## Matches in ")) - .map(|line| { - line.strip_prefix("## Matches in ") - .unwrap() - .trim() - .to_string() - }) - .collect() - } -} diff --git a/crates/agent/src/tools/list_directory_tool.rs b/crates/agent/src/tools/list_directory_tool.rs deleted file mode 100644 index b7ceba5abf..0000000000 --- a/crates/agent/src/tools/list_directory_tool.rs +++ /dev/null @@ -1,660 +0,0 @@ -use crate::{AgentTool, ToolCallEventStream}; -use agent_client_protocol::ToolKind; -use anyhow::{Result, anyhow}; -use gpui::{App, Entity, SharedString, Task}; -use project::{Project, ProjectPath, WorktreeSettings}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::Settings; -use std::fmt::Write; -use std::sync::Arc; -use util::markdown::MarkdownInlineCode; - -/// Lists files and directories in a given path. Prefer the `grep` or `find_path` tools when searching the codebase. -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct ListDirectoryToolInput { - /// The fully-qualified path of the directory to list in the project. - /// - /// This path should never be absolute, and the first component of the path should always be a root directory in a project. - /// - /// - /// If the project has the following root directories: - /// - /// - directory1 - /// - directory2 - /// - /// You can list the contents of `directory1` by using the path `directory1`. - /// - /// - /// - /// If the project has the following root directories: - /// - /// - foo - /// - bar - /// - /// If you wanna list contents in the directory `foo/baz`, you should use the path `foo/baz`. - /// - pub path: String, -} - -pub struct ListDirectoryTool { - project: Entity, -} - -impl ListDirectoryTool { - pub fn new(project: Entity) -> Self { - Self { project } - } -} - -impl AgentTool for ListDirectoryTool { - type Input = ListDirectoryToolInput; - type Output = String; - - fn name() -> &'static str { - "list_directory" - } - - fn kind() -> ToolKind { - ToolKind::Read - } - - fn initial_title( - &self, - input: Result, - _cx: &mut App, - ) -> SharedString { - if let Ok(input) = input { - let path = MarkdownInlineCode(&input.path); - format!("List the {path} directory's contents").into() - } else { - "List directory".into() - } - } - - fn run( - self: Arc, - input: Self::Input, - _event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - // Sometimes models will return these even though we tell it to give a path and not a glob. - // When this happens, just list the root worktree directories. - if matches!(input.path.as_str(), "." | "" | "./" | "*") { - let output = self - .project - .read(cx) - .worktrees(cx) - .filter_map(|worktree| { - let worktree = worktree.read(cx); - let root_entry = worktree.root_entry()?; - if root_entry.is_dir() { - Some(root_entry.path.display(worktree.path_style())) - } else { - None - } - }) - .collect::>() - .join("\n"); - - return Task::ready(Ok(output)); - } - - let Some(project_path) = self.project.read(cx).find_project_path(&input.path, cx) else { - return Task::ready(Err(anyhow!("Path {} not found in project", input.path))); - }; - let Some(worktree) = self - .project - .read(cx) - .worktree_for_id(project_path.worktree_id, cx) - else { - return Task::ready(Err(anyhow!("Worktree not found"))); - }; - - // Check if the directory whose contents we're listing is itself excluded or private - let global_settings = WorktreeSettings::get_global(cx); - if global_settings.is_path_excluded(&project_path.path) { - return Task::ready(Err(anyhow!( - "Cannot list directory because its path matches the user's global `file_scan_exclusions` setting: {}", - &input.path - ))); - } - - if global_settings.is_path_private(&project_path.path) { - return Task::ready(Err(anyhow!( - "Cannot list directory because its path matches the user's global `private_files` setting: {}", - &input.path - ))); - } - - let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx); - if worktree_settings.is_path_excluded(&project_path.path) { - return Task::ready(Err(anyhow!( - "Cannot list directory because its path matches the user's worktree`file_scan_exclusions` setting: {}", - &input.path - ))); - } - - if worktree_settings.is_path_private(&project_path.path) { - return Task::ready(Err(anyhow!( - "Cannot list directory because its path matches the user's worktree `private_paths` setting: {}", - &input.path - ))); - } - - let worktree_snapshot = worktree.read(cx).snapshot(); - let worktree_root_name = worktree.read(cx).root_name(); - - let Some(entry) = worktree_snapshot.entry_for_path(&project_path.path) else { - return Task::ready(Err(anyhow!("Path not found: {}", input.path))); - }; - - if !entry.is_dir() { - return Task::ready(Err(anyhow!("{} is not a directory.", input.path))); - } - let worktree_snapshot = worktree.read(cx).snapshot(); - - let mut folders = Vec::new(); - let mut files = Vec::new(); - - for entry in worktree_snapshot.child_entries(&project_path.path) { - // Skip private and excluded files and directories - if global_settings.is_path_private(&entry.path) - || global_settings.is_path_excluded(&entry.path) - { - continue; - } - - let project_path: ProjectPath = (worktree_snapshot.id(), entry.path.clone()).into(); - if worktree_settings.is_path_excluded(&project_path.path) - || worktree_settings.is_path_private(&project_path.path) - { - continue; - } - - let full_path = worktree_root_name - .join(&entry.path) - .display(worktree_snapshot.path_style()) - .into_owned(); - if entry.is_dir() { - folders.push(full_path); - } else { - files.push(full_path); - } - } - - let mut output = String::new(); - - if !folders.is_empty() { - writeln!(output, "# Folders:\n{}", folders.join("\n")).unwrap(); - } - - if !files.is_empty() { - writeln!(output, "\n# Files:\n{}", files.join("\n")).unwrap(); - } - - if output.is_empty() { - writeln!(output, "{} is empty.", input.path).unwrap(); - } - - Task::ready(Ok(output)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::{TestAppContext, UpdateGlobal}; - use indoc::indoc; - use project::{FakeFs, Project}; - use serde_json::json; - use settings::SettingsStore; - use util::path; - - fn platform_paths(path_str: &str) -> String { - if cfg!(target_os = "windows") { - path_str.replace("/", "\\") - } else { - path_str.to_string() - } - } - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - }); - } - - #[gpui::test] - async fn test_list_directory_separates_files_and_dirs(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/project"), - json!({ - "src": { - "main.rs": "fn main() {}", - "lib.rs": "pub fn hello() {}", - "models": { - "user.rs": "struct User {}", - "post.rs": "struct Post {}" - }, - "utils": { - "helper.rs": "pub fn help() {}" - } - }, - "tests": { - "integration_test.rs": "#[test] fn test() {}" - }, - "README.md": "# Project", - "Cargo.toml": "[package]" - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - let tool = Arc::new(ListDirectoryTool::new(project)); - - // Test listing root directory - let input = ListDirectoryToolInput { - path: "project".into(), - }; - let output = cx - .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) - .await - .unwrap(); - assert_eq!( - output, - platform_paths(indoc! {" - # Folders: - project/src - project/tests - - # Files: - project/Cargo.toml - project/README.md - "}) - ); - - // Test listing src directory - let input = ListDirectoryToolInput { - path: "project/src".into(), - }; - let output = cx - .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) - .await - .unwrap(); - assert_eq!( - output, - platform_paths(indoc! {" - # Folders: - project/src/models - project/src/utils - - # Files: - project/src/lib.rs - project/src/main.rs - "}) - ); - - // Test listing directory with only files - let input = ListDirectoryToolInput { - path: "project/tests".into(), - }; - let output = cx - .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) - .await - .unwrap(); - assert!(!output.contains("# Folders:")); - assert!(output.contains("# Files:")); - assert!(output.contains(&platform_paths("project/tests/integration_test.rs"))); - } - - #[gpui::test] - async fn test_list_directory_empty_directory(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/project"), - json!({ - "empty_dir": {} - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - let tool = Arc::new(ListDirectoryTool::new(project)); - - let input = ListDirectoryToolInput { - path: "project/empty_dir".into(), - }; - let output = cx - .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) - .await - .unwrap(); - assert_eq!(output, "project/empty_dir is empty.\n"); - } - - #[gpui::test] - async fn test_list_directory_error_cases(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/project"), - json!({ - "file.txt": "content" - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - let tool = Arc::new(ListDirectoryTool::new(project)); - - // Test non-existent path - let input = ListDirectoryToolInput { - path: "project/nonexistent".into(), - }; - let output = cx - .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) - .await; - assert!(output.unwrap_err().to_string().contains("Path not found")); - - // Test trying to list a file instead of directory - let input = ListDirectoryToolInput { - path: "project/file.txt".into(), - }; - let output = cx - .update(|cx| tool.run(input, ToolCallEventStream::test().0, cx)) - .await; - assert!( - output - .unwrap_err() - .to_string() - .contains("is not a directory") - ); - } - - #[gpui::test] - async fn test_list_directory_security(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/project"), - json!({ - "normal_dir": { - "file1.txt": "content", - "file2.txt": "content" - }, - ".mysecrets": "SECRET_KEY=abc123", - ".secretdir": { - "config": "special configuration", - "secret.txt": "secret content" - }, - ".mymetadata": "custom metadata", - "visible_dir": { - "normal.txt": "normal content", - "special.privatekey": "private key content", - "data.mysensitive": "sensitive data", - ".hidden_subdir": { - "hidden_file.txt": "hidden content" - } - } - }), - ) - .await; - - // Configure settings explicitly - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(vec![ - "**/.secretdir".to_string(), - "**/.mymetadata".to_string(), - "**/.hidden_subdir".to_string(), - ]); - settings.project.worktree.private_files = Some( - vec![ - "**/.mysecrets".to_string(), - "**/*.privatekey".to_string(), - "**/*.mysensitive".to_string(), - ] - .into(), - ); - }); - }); - }); - - let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - let tool = Arc::new(ListDirectoryTool::new(project)); - - // Listing root directory should exclude private and excluded files - let input = ListDirectoryToolInput { - path: "project".into(), - }; - let output = cx - .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) - .await - .unwrap(); - - // Should include normal directories - assert!(output.contains("normal_dir"), "Should list normal_dir"); - assert!(output.contains("visible_dir"), "Should list visible_dir"); - - // Should NOT include excluded or private files - assert!( - !output.contains(".secretdir"), - "Should not list .secretdir (file_scan_exclusions)" - ); - assert!( - !output.contains(".mymetadata"), - "Should not list .mymetadata (file_scan_exclusions)" - ); - assert!( - !output.contains(".mysecrets"), - "Should not list .mysecrets (private_files)" - ); - - // Trying to list an excluded directory should fail - let input = ListDirectoryToolInput { - path: "project/.secretdir".into(), - }; - let output = cx - .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) - .await; - assert!( - output - .unwrap_err() - .to_string() - .contains("file_scan_exclusions"), - "Error should mention file_scan_exclusions" - ); - - // Listing a directory should exclude private files within it - let input = ListDirectoryToolInput { - path: "project/visible_dir".into(), - }; - let output = cx - .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) - .await - .unwrap(); - - // Should include normal files - assert!(output.contains("normal.txt"), "Should list normal.txt"); - - // Should NOT include private files - assert!( - !output.contains("privatekey"), - "Should not list .privatekey files (private_files)" - ); - assert!( - !output.contains("mysensitive"), - "Should not list .mysensitive files (private_files)" - ); - - // Should NOT include subdirectories that match exclusions - assert!( - !output.contains(".hidden_subdir"), - "Should not list .hidden_subdir (file_scan_exclusions)" - ); - } - - #[gpui::test] - async fn test_list_directory_with_multiple_worktree_settings(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - - // Create first worktree with its own private files - fs.insert_tree( - path!("/worktree1"), - json!({ - ".zed": { - "settings.json": r#"{ - "file_scan_exclusions": ["**/fixture.*"], - "private_files": ["**/secret.rs", "**/config.toml"] - }"# - }, - "src": { - "main.rs": "fn main() { println!(\"Hello from worktree1\"); }", - "secret.rs": "const API_KEY: &str = \"secret_key_1\";", - "config.toml": "[database]\nurl = \"postgres://localhost/db1\"" - }, - "tests": { - "test.rs": "mod tests { fn test_it() {} }", - "fixture.sql": "CREATE TABLE users (id INT, name VARCHAR(255));" - } - }), - ) - .await; - - // Create second worktree with different private files - fs.insert_tree( - path!("/worktree2"), - json!({ - ".zed": { - "settings.json": r#"{ - "file_scan_exclusions": ["**/internal.*"], - "private_files": ["**/private.js", "**/data.json"] - }"# - }, - "lib": { - "public.js": "export function greet() { return 'Hello from worktree2'; }", - "private.js": "const SECRET_TOKEN = \"private_token_2\";", - "data.json": "{\"api_key\": \"json_secret_key\"}" - }, - "docs": { - "README.md": "# Public Documentation", - "internal.md": "# Internal Secrets and Configuration" - } - }), - ) - .await; - - // Set global settings - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = - Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]); - settings.project.worktree.private_files = - Some(vec!["**/.env".to_string()].into()); - }); - }); - }); - - let project = Project::test( - fs.clone(), - [path!("/worktree1").as_ref(), path!("/worktree2").as_ref()], - cx, - ) - .await; - - // Wait for worktrees to be fully scanned - cx.executor().run_until_parked(); - - let tool = Arc::new(ListDirectoryTool::new(project)); - - // Test listing worktree1/src - should exclude secret.rs and config.toml based on local settings - let input = ListDirectoryToolInput { - path: "worktree1/src".into(), - }; - let output = cx - .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) - .await - .unwrap(); - assert!(output.contains("main.rs"), "Should list main.rs"); - assert!( - !output.contains("secret.rs"), - "Should not list secret.rs (local private_files)" - ); - assert!( - !output.contains("config.toml"), - "Should not list config.toml (local private_files)" - ); - - // Test listing worktree1/tests - should exclude fixture.sql based on local settings - let input = ListDirectoryToolInput { - path: "worktree1/tests".into(), - }; - let output = cx - .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) - .await - .unwrap(); - assert!(output.contains("test.rs"), "Should list test.rs"); - assert!( - !output.contains("fixture.sql"), - "Should not list fixture.sql (local file_scan_exclusions)" - ); - - // Test listing worktree2/lib - should exclude private.js and data.json based on local settings - let input = ListDirectoryToolInput { - path: "worktree2/lib".into(), - }; - let output = cx - .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) - .await - .unwrap(); - assert!(output.contains("public.js"), "Should list public.js"); - assert!( - !output.contains("private.js"), - "Should not list private.js (local private_files)" - ); - assert!( - !output.contains("data.json"), - "Should not list data.json (local private_files)" - ); - - // Test listing worktree2/docs - should exclude internal.md based on local settings - let input = ListDirectoryToolInput { - path: "worktree2/docs".into(), - }; - let output = cx - .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) - .await - .unwrap(); - assert!(output.contains("README.md"), "Should list README.md"); - assert!( - !output.contains("internal.md"), - "Should not list internal.md (local file_scan_exclusions)" - ); - - // Test trying to list an excluded directory directly - let input = ListDirectoryToolInput { - path: "worktree1/src/secret.rs".into(), - }; - let output = cx - .update(|cx| tool.clone().run(input, ToolCallEventStream::test().0, cx)) - .await; - assert!( - output - .unwrap_err() - .to_string() - .contains("Cannot list directory"), - ); - } -} diff --git a/crates/agent/src/tools/move_path_tool.rs b/crates/agent/src/tools/move_path_tool.rs deleted file mode 100644 index ae58145126..0000000000 --- a/crates/agent/src/tools/move_path_tool.rs +++ /dev/null @@ -1,124 +0,0 @@ -use crate::{AgentTool, ToolCallEventStream}; -use agent_client_protocol::ToolKind; -use anyhow::{Context as _, Result, anyhow}; -use gpui::{App, AppContext, Entity, SharedString, Task}; -use project::Project; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::{path::Path, sync::Arc}; -use util::markdown::MarkdownInlineCode; - -/// Moves or rename a file or directory in the project, and returns confirmation that the move succeeded. -/// -/// If the source and destination directories are the same, but the filename is different, this performs a rename. Otherwise, it performs a move. -/// -/// This tool should be used when it's desirable to move or rename a file or directory without changing its contents at all. -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct MovePathToolInput { - /// The source path of the file or directory to move/rename. - /// - /// - /// If the project has the following files: - /// - /// - directory1/a/something.txt - /// - directory2/a/things.txt - /// - directory3/a/other.txt - /// - /// You can move the first file by providing a source_path of "directory1/a/something.txt" - /// - pub source_path: String, - - /// The destination path where the file or directory should be moved/renamed to. - /// If the paths are the same except for the filename, then this will be a rename. - /// - /// - /// To move "directory1/a/something.txt" to "directory2/b/renamed.txt", - /// provide a destination_path of "directory2/b/renamed.txt" - /// - pub destination_path: String, -} - -pub struct MovePathTool { - project: Entity, -} - -impl MovePathTool { - pub fn new(project: Entity) -> Self { - Self { project } - } -} - -impl AgentTool for MovePathTool { - type Input = MovePathToolInput; - type Output = String; - - fn name() -> &'static str { - "move_path" - } - - fn kind() -> ToolKind { - ToolKind::Move - } - - fn initial_title( - &self, - input: Result, - _cx: &mut App, - ) -> SharedString { - if let Ok(input) = input { - let src = MarkdownInlineCode(&input.source_path); - let dest = MarkdownInlineCode(&input.destination_path); - let src_path = Path::new(&input.source_path); - let dest_path = Path::new(&input.destination_path); - - match dest_path - .file_name() - .and_then(|os_str| os_str.to_os_string().into_string().ok()) - { - Some(filename) if src_path.parent() == dest_path.parent() => { - let filename = MarkdownInlineCode(&filename); - format!("Rename {src} to {filename}").into() - } - _ => format!("Move {src} to {dest}").into(), - } - } else { - "Move path".into() - } - } - - fn run( - self: Arc, - input: Self::Input, - _event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - let rename_task = self.project.update(cx, |project, cx| { - match project - .find_project_path(&input.source_path, cx) - .and_then(|project_path| project.entry_for_path(&project_path, cx)) - { - Some(entity) => match project.find_project_path(&input.destination_path, cx) { - Some(project_path) => project.rename_entry(entity.id, project_path, cx), - None => Task::ready(Err(anyhow!( - "Destination path {} was outside the project.", - input.destination_path - ))), - }, - None => Task::ready(Err(anyhow!( - "Source path {} was not found in the project.", - input.source_path - ))), - } - }); - - cx.background_spawn(async move { - let _ = rename_task.await.with_context(|| { - format!("Moving {} to {}", input.source_path, input.destination_path) - })?; - Ok(format!( - "Moved {} to {}", - input.source_path, input.destination_path - )) - }) - } -} diff --git a/crates/agent/src/tools/now_tool.rs b/crates/agent/src/tools/now_tool.rs deleted file mode 100644 index 3387c0a617..0000000000 --- a/crates/agent/src/tools/now_tool.rs +++ /dev/null @@ -1,64 +0,0 @@ -use std::sync::Arc; - -use agent_client_protocol as acp; -use anyhow::Result; -use chrono::{Local, Utc}; -use gpui::{App, SharedString, Task}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -use crate::{AgentTool, ToolCallEventStream}; - -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -#[schemars(inline)] -pub enum Timezone { - /// Use UTC for the datetime. - Utc, - /// Use local time for the datetime. - Local, -} - -/// Returns the current datetime in RFC 3339 format. -/// Only use this tool when the user specifically asks for it or the current task would benefit from knowing the current datetime. -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct NowToolInput { - /// The timezone to use for the datetime. - timezone: Timezone, -} - -pub struct NowTool; - -impl AgentTool for NowTool { - type Input = NowToolInput; - type Output = String; - - fn name() -> &'static str { - "now" - } - - fn kind() -> acp::ToolKind { - acp::ToolKind::Other - } - - fn initial_title( - &self, - _input: Result, - _cx: &mut App, - ) -> SharedString { - "Get current time".into() - } - - fn run( - self: Arc, - input: Self::Input, - _event_stream: ToolCallEventStream, - _cx: &mut App, - ) -> Task> { - let now = match input.timezone { - Timezone::Utc => Utc::now().to_rfc3339(), - Timezone::Local => Local::now().to_rfc3339(), - }; - Task::ready(Ok(format!("The current datetime is {now}."))) - } -} diff --git a/crates/agent/src/tools/open_tool.rs b/crates/agent/src/tools/open_tool.rs deleted file mode 100644 index 8826d1529c..0000000000 --- a/crates/agent/src/tools/open_tool.rs +++ /dev/null @@ -1,168 +0,0 @@ -use crate::AgentTool; -use agent_client_protocol::ToolKind; -use anyhow::{Context as _, Result}; -use gpui::{App, AppContext, Entity, SharedString, Task}; -use project::Project; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::{path::PathBuf, sync::Arc}; -use util::markdown::MarkdownEscaped; - -/// This tool opens a file or URL with the default application associated with it on the user's operating system: -/// -/// - On macOS, it's equivalent to the `open` command -/// - On Windows, it's equivalent to `start` -/// - On Linux, it uses something like `xdg-open`, `gio open`, `gnome-open`, `kde-open`, `wslview` as appropriate -/// -/// For example, it can open a web browser with a URL, open a PDF file with the default PDF viewer, etc. -/// -/// You MUST ONLY use this tool when the user has explicitly requested opening something. You MUST NEVER assume that the user would like for you to use this tool. -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -pub struct OpenToolInput { - /// The path or URL to open with the default application. - path_or_url: String, -} - -pub struct OpenTool { - project: Entity, -} - -impl OpenTool { - pub fn new(project: Entity) -> Self { - Self { project } - } -} - -impl AgentTool for OpenTool { - type Input = OpenToolInput; - type Output = String; - - fn name() -> &'static str { - "open" - } - - fn kind() -> ToolKind { - ToolKind::Execute - } - - fn initial_title( - &self, - input: Result, - _cx: &mut App, - ) -> SharedString { - if let Ok(input) = input { - format!("Open `{}`", MarkdownEscaped(&input.path_or_url)).into() - } else { - "Open file or URL".into() - } - } - - fn run( - self: Arc, - input: Self::Input, - event_stream: crate::ToolCallEventStream, - cx: &mut App, - ) -> Task> { - // If path_or_url turns out to be a path in the project, make it absolute. - let abs_path = to_absolute_path(&input.path_or_url, self.project.clone(), cx); - let authorize = event_stream.authorize(self.initial_title(Ok(input.clone()), cx), cx); - cx.background_spawn(async move { - authorize.await?; - - match abs_path { - Some(path) => open::that(path), - None => open::that(&input.path_or_url), - } - .context("Failed to open URL or file path")?; - - Ok(format!("Successfully opened {}", input.path_or_url)) - }) - } -} - -fn to_absolute_path( - potential_path: &str, - project: Entity, - cx: &mut App, -) -> Option { - let project = project.read(cx); - project - .find_project_path(PathBuf::from(potential_path), cx) - .and_then(|project_path| project.absolute_path(&project_path, cx)) -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::TestAppContext; - use project::{FakeFs, Project}; - use settings::SettingsStore; - use std::path::Path; - use tempfile::TempDir; - - #[gpui::test] - async fn test_to_absolute_path(cx: &mut TestAppContext) { - init_test(cx); - let temp_dir = TempDir::new().expect("Failed to create temp directory"); - let temp_path = temp_dir.path().to_string_lossy().into_owned(); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - &temp_path, - serde_json::json!({ - "src": { - "main.rs": "fn main() {}", - "lib.rs": "pub fn lib_fn() {}" - }, - "docs": { - "readme.md": "# Project Documentation" - } - }), - ) - .await; - - // Use the temp_path as the root directory, not just its filename - let project = Project::test(fs.clone(), [temp_dir.path()], cx).await; - - // Test cases where the function should return Some - cx.update(|cx| { - // Project-relative paths should return Some - // Create paths using the last segment of the temp path to simulate a project-relative path - let root_dir_name = Path::new(&temp_path) - .file_name() - .unwrap_or_else(|| std::ffi::OsStr::new("temp")) - .to_string_lossy(); - - assert!( - to_absolute_path(&format!("{root_dir_name}/src/main.rs"), project.clone(), cx) - .is_some(), - "Failed to resolve main.rs path" - ); - - assert!( - to_absolute_path( - &format!("{root_dir_name}/docs/readme.md",), - project.clone(), - cx, - ) - .is_some(), - "Failed to resolve readme.md path" - ); - - // External URL should return None - let result = to_absolute_path("https://example.com", project.clone(), cx); - assert_eq!(result, None, "External URLs should return None"); - - // Path outside project - let result = to_absolute_path("../invalid/path", project.clone(), cx); - assert_eq!(result, None, "Paths outside the project should return None"); - }); - } - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - }); - } -} diff --git a/crates/agent/src/tools/read_file_tool.rs b/crates/agent/src/tools/read_file_tool.rs deleted file mode 100644 index acfd4a1674..0000000000 --- a/crates/agent/src/tools/read_file_tool.rs +++ /dev/null @@ -1,1037 +0,0 @@ -use action_log::ActionLog; -use agent_client_protocol::{self as acp, ToolCallUpdateFields}; -use anyhow::{Context as _, Result, anyhow}; -use gpui::{App, Entity, SharedString, Task, WeakEntity}; -use indoc::formatdoc; -use language::Point; -use language_model::{LanguageModelImage, LanguageModelToolResultContent}; -use project::{AgentLocation, ImageItem, Project, WorktreeSettings, image_store}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::Settings; -use std::sync::Arc; -use util::markdown::MarkdownCodeBlock; - -use crate::{AgentTool, Thread, ToolCallEventStream, outline}; - -/// Reads the content of the given file in the project. -/// -/// - Never attempt to read a path that hasn't been previously mentioned. -/// - For large files, this tool returns a file outline with symbol names and line numbers instead of the full content. -/// This outline IS a successful response - use the line numbers to read specific sections with start_line/end_line. -/// Do NOT retry reading the same file without line numbers if you receive an outline. -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct ReadFileToolInput { - /// The relative path of the file to read. - /// - /// This path should never be absolute, and the first component of the path should always be a root directory in a project. - /// - /// - /// If the project has the following root directories: - /// - /// - /a/b/directory1 - /// - /c/d/directory2 - /// - /// If you want to access `file.txt` in `directory1`, you should use the path `directory1/file.txt`. - /// If you want to access `file.txt` in `directory2`, you should use the path `directory2/file.txt`. - /// - pub path: String, - /// Optional line number to start reading on (1-based index) - #[serde(default)] - pub start_line: Option, - /// Optional line number to end reading on (1-based index, inclusive) - #[serde(default)] - pub end_line: Option, -} - -pub struct ReadFileTool { - thread: WeakEntity, - project: Entity, - action_log: Entity, -} - -impl ReadFileTool { - pub fn new( - thread: WeakEntity, - project: Entity, - action_log: Entity, - ) -> Self { - Self { - thread, - project, - action_log, - } - } -} - -impl AgentTool for ReadFileTool { - type Input = ReadFileToolInput; - type Output = LanguageModelToolResultContent; - - fn name() -> &'static str { - "read_file" - } - - fn kind() -> acp::ToolKind { - acp::ToolKind::Read - } - - fn initial_title( - &self, - input: Result, - cx: &mut App, - ) -> SharedString { - if let Ok(input) = input - && let Some(project_path) = self.project.read(cx).find_project_path(&input.path, cx) - && let Some(path) = self - .project - .read(cx) - .short_full_path_for_project_path(&project_path, cx) - { - match (input.start_line, input.end_line) { - (Some(start), Some(end)) => { - format!("Read file `{path}` (lines {}-{})", start, end,) - } - (Some(start), None) => { - format!("Read file `{path}` (from line {})", start) - } - _ => format!("Read file `{path}`"), - } - .into() - } else { - "Read file".into() - } - } - - fn run( - self: Arc, - input: Self::Input, - event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - let Some(project_path) = self.project.read(cx).find_project_path(&input.path, cx) else { - return Task::ready(Err(anyhow!("Path {} not found in project", &input.path))); - }; - let Some(abs_path) = self.project.read(cx).absolute_path(&project_path, cx) else { - return Task::ready(Err(anyhow!( - "Failed to convert {} to absolute path", - &input.path - ))); - }; - - // Error out if this path is either excluded or private in global settings - let global_settings = WorktreeSettings::get_global(cx); - if global_settings.is_path_excluded(&project_path.path) { - return Task::ready(Err(anyhow!( - "Cannot read file because its path matches the global `file_scan_exclusions` setting: {}", - &input.path - ))); - } - - if global_settings.is_path_private(&project_path.path) { - return Task::ready(Err(anyhow!( - "Cannot read file because its path matches the global `private_files` setting: {}", - &input.path - ))); - } - - // Error out if this path is either excluded or private in worktree settings - let worktree_settings = WorktreeSettings::get(Some((&project_path).into()), cx); - if worktree_settings.is_path_excluded(&project_path.path) { - return Task::ready(Err(anyhow!( - "Cannot read file because its path matches the worktree `file_scan_exclusions` setting: {}", - &input.path - ))); - } - - if worktree_settings.is_path_private(&project_path.path) { - return Task::ready(Err(anyhow!( - "Cannot read file because its path matches the worktree `private_files` setting: {}", - &input.path - ))); - } - - let file_path = input.path.clone(); - - event_stream.update_fields(ToolCallUpdateFields::new().locations(vec![ - acp::ToolCallLocation::new(&abs_path) - .line(input.start_line.map(|line| line.saturating_sub(1))), - ])); - - if image_store::is_image_file(&self.project, &project_path, cx) { - return cx.spawn(async move |cx| { - let image_entity: Entity = cx - .update(|cx| { - self.project.update(cx, |project, cx| { - project.open_image(project_path.clone(), cx) - }) - })? - .await?; - - let image = - image_entity.read_with(cx, |image_item, _| Arc::clone(&image_item.image))?; - - let language_model_image = cx - .update(|cx| LanguageModelImage::from_image(image, cx))? - .await - .context("processing image")?; - - Ok(language_model_image.into()) - }); - } - - let project = self.project.clone(); - let action_log = self.action_log.clone(); - - cx.spawn(async move |cx| { - let buffer = cx - .update(|cx| { - project.update(cx, |project, cx| { - project.open_buffer(project_path.clone(), cx) - }) - })? - .await?; - if buffer.read_with(cx, |buffer, _| { - buffer - .file() - .as_ref() - .is_none_or(|file| !file.disk_state().exists()) - })? { - anyhow::bail!("{file_path} not found"); - } - - // Record the file read time and mtime - if let Some(mtime) = buffer.read_with(cx, |buffer, _| { - buffer.file().and_then(|file| file.disk_state().mtime()) - })? { - self.thread - .update(cx, |thread, _| { - thread.file_read_times.insert(abs_path.to_path_buf(), mtime); - }) - .ok(); - } - - let mut anchor = None; - - // Check if specific line ranges are provided - let result = if input.start_line.is_some() || input.end_line.is_some() { - let result = buffer.read_with(cx, |buffer, _cx| { - // .max(1) because despite instructions to be 1-indexed, sometimes the model passes 0. - let start = input.start_line.unwrap_or(1).max(1); - let start_row = start - 1; - if start_row <= buffer.max_point().row { - let column = buffer.line_indent_for_row(start_row).raw_len(); - anchor = Some(buffer.anchor_before(Point::new(start_row, column))); - } - - let mut end_row = input.end_line.unwrap_or(u32::MAX); - if end_row <= start_row { - end_row = start_row + 1; // read at least one lines - } - let start = buffer.anchor_before(Point::new(start_row, 0)); - let end = buffer.anchor_before(Point::new(end_row, 0)); - buffer.text_for_range(start..end).collect::() - })?; - - action_log.update(cx, |log, cx| { - log.buffer_read(buffer.clone(), cx); - })?; - - Ok(result.into()) - } else { - // No line ranges specified, so check file size to see if it's too big. - let buffer_content = outline::get_buffer_content_or_outline( - buffer.clone(), - Some(&abs_path.to_string_lossy()), - cx, - ) - .await?; - - action_log.update(cx, |log, cx| { - log.buffer_read(buffer.clone(), cx); - })?; - - if buffer_content.is_outline { - Ok(formatdoc! {" - SUCCESS: File outline retrieved. This file is too large to read all at once, so the outline below shows the file's structure with line numbers. - - IMPORTANT: Do NOT retry this call without line numbers - you will get the same outline. - Instead, use the line numbers below to read specific sections by calling this tool again with start_line and end_line parameters. - - {} - - NEXT STEPS: To read a specific symbol's implementation, call read_file with the same path plus start_line and end_line from the outline above. - For example, to read a function shown as [L100-150], use start_line: 100 and end_line: 150.", buffer_content.text - } - .into()) - } else { - Ok(buffer_content.text.into()) - } - }; - - project.update(cx, |project, cx| { - project.set_agent_location( - Some(AgentLocation { - buffer: buffer.downgrade(), - position: anchor.unwrap_or_else(|| { - text::Anchor::min_for_buffer(buffer.read(cx).remote_id()) - }), - }), - cx, - ); - if let Ok(LanguageModelToolResultContent::Text(text)) = &result { - let markdown = MarkdownCodeBlock { - tag: &input.path, - text, - } - .to_string(); - event_stream.update_fields(ToolCallUpdateFields::new().content(vec![ - acp::ToolCallContent::Content(acp::Content::new(markdown)), - ])); - } - })?; - - result - }) - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::{ContextServerRegistry, Templates, Thread}; - use gpui::{AppContext, TestAppContext, UpdateGlobal as _}; - use language_model::fake_provider::FakeLanguageModel; - use project::{FakeFs, Project}; - use prompt_store::ProjectContext; - use serde_json::json; - use settings::SettingsStore; - use std::sync::Arc; - use util::path; - - #[gpui::test] - async fn test_read_nonexistent_file(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/root"), json!({})).await; - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - 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()); - let thread = cx.new(|cx| { - Thread::new( - project.clone(), - cx.new(|_cx| ProjectContext::default()), - context_server_registry, - Templates::new(), - Some(model), - cx, - ) - }); - let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log)); - let (event_stream, _) = ToolCallEventStream::test(); - - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "root/nonexistent_file.txt".to_string(), - start_line: None, - end_line: None, - }; - tool.run(input, event_stream, cx) - }) - .await; - assert_eq!( - result.unwrap_err().to_string(), - "root/nonexistent_file.txt not found" - ); - } - - #[gpui::test] - async fn test_read_small_file(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - "small_file.txt": "This is a small file content" - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - 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()); - let thread = cx.new(|cx| { - Thread::new( - project.clone(), - cx.new(|_cx| ProjectContext::default()), - context_server_registry, - Templates::new(), - Some(model), - cx, - ) - }); - let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log)); - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "root/small_file.txt".into(), - start_line: None, - end_line: None, - }; - tool.run(input, ToolCallEventStream::test().0, cx) - }) - .await; - assert_eq!(result.unwrap(), "This is a small file content".into()); - } - - #[gpui::test] - async fn test_read_large_file(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - "large_file.rs": (0..1000).map(|i| format!("struct Test{} {{\n a: u32,\n b: usize,\n}}", i)).collect::>().join("\n") - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let language_registry = project.read_with(cx, |project, _| project.languages().clone()); - language_registry.add(language::rust_lang()); - 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()); - let thread = cx.new(|cx| { - Thread::new( - project.clone(), - cx.new(|_cx| ProjectContext::default()), - context_server_registry, - Templates::new(), - Some(model), - cx, - ) - }); - let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log)); - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "root/large_file.rs".into(), - start_line: None, - end_line: None, - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await - .unwrap(); - let content = result.to_str().unwrap(); - - assert_eq!( - content.lines().skip(7).take(6).collect::>(), - vec![ - "struct Test0 [L1-4]", - " a [L2]", - " b [L3]", - "struct Test1 [L5-8]", - " a [L6]", - " b [L7]", - ] - ); - - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "root/large_file.rs".into(), - start_line: None, - end_line: None, - }; - tool.run(input, ToolCallEventStream::test().0, cx) - }) - .await - .unwrap(); - let content = result.to_str().unwrap(); - let expected_content = (0..1000) - .flat_map(|i| { - vec![ - format!("struct Test{} [L{}-{}]", i, i * 4 + 1, i * 4 + 4), - format!(" a [L{}]", i * 4 + 2), - format!(" b [L{}]", i * 4 + 3), - ] - }) - .collect::>(); - pretty_assertions::assert_eq!( - content - .lines() - .skip(7) - .take(expected_content.len()) - .collect::>(), - expected_content - ); - } - - #[gpui::test] - async fn test_read_file_with_line_range(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - "multiline.txt": "Line 1\nLine 2\nLine 3\nLine 4\nLine 5" - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - - 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()); - let thread = cx.new(|cx| { - Thread::new( - project.clone(), - cx.new(|_cx| ProjectContext::default()), - context_server_registry, - Templates::new(), - Some(model), - cx, - ) - }); - let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log)); - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "root/multiline.txt".to_string(), - start_line: Some(2), - end_line: Some(4), - }; - tool.run(input, ToolCallEventStream::test().0, cx) - }) - .await; - assert_eq!(result.unwrap(), "Line 2\nLine 3\nLine 4\n".into()); - } - - #[gpui::test] - async fn test_read_file_line_range_edge_cases(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - "multiline.txt": "Line 1\nLine 2\nLine 3\nLine 4\nLine 5" - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - 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()); - let thread = cx.new(|cx| { - Thread::new( - project.clone(), - cx.new(|_cx| ProjectContext::default()), - context_server_registry, - Templates::new(), - Some(model), - cx, - ) - }); - let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log)); - - // start_line of 0 should be treated as 1 - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "root/multiline.txt".to_string(), - start_line: Some(0), - end_line: Some(2), - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await; - assert_eq!(result.unwrap(), "Line 1\nLine 2\n".into()); - - // end_line of 0 should result in at least 1 line - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "root/multiline.txt".to_string(), - start_line: Some(1), - end_line: Some(0), - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await; - assert_eq!(result.unwrap(), "Line 1\n".into()); - - // when start_line > end_line, should still return at least 1 line - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "root/multiline.txt".to_string(), - start_line: Some(3), - end_line: Some(2), - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await; - assert_eq!(result.unwrap(), "Line 3\n".into()); - } - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - }); - } - - #[gpui::test] - async fn test_read_file_security(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - - fs.insert_tree( - path!("/"), - json!({ - "project_root": { - "allowed_file.txt": "This file is in the project", - ".mysecrets": "SECRET_KEY=abc123", - ".secretdir": { - "config": "special configuration" - }, - ".mymetadata": "custom metadata", - "subdir": { - "normal_file.txt": "Normal file content", - "special.privatekey": "private key content", - "data.mysensitive": "sensitive data" - } - }, - "outside_project": { - "sensitive_file.txt": "This file is outside the project" - } - }), - ) - .await; - - cx.update(|cx| { - use gpui::UpdateGlobal; - use settings::SettingsStore; - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(vec![ - "**/.secretdir".to_string(), - "**/.mymetadata".to_string(), - ]); - settings.project.worktree.private_files = Some( - vec![ - "**/.mysecrets".to_string(), - "**/*.privatekey".to_string(), - "**/*.mysensitive".to_string(), - ] - .into(), - ); - }); - }); - }); - - let project = Project::test(fs.clone(), [path!("/project_root").as_ref()], cx).await; - 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()); - let thread = cx.new(|cx| { - Thread::new( - project.clone(), - cx.new(|_cx| ProjectContext::default()), - context_server_registry, - Templates::new(), - Some(model), - cx, - ) - }); - let tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log)); - - // Reading a file outside the project worktree should fail - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "/outside_project/sensitive_file.txt".to_string(), - start_line: None, - end_line: None, - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await; - assert!( - result.is_err(), - "read_file_tool should error when attempting to read an absolute path outside a worktree" - ); - - // Reading a file within the project should succeed - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "project_root/allowed_file.txt".to_string(), - start_line: None, - end_line: None, - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await; - assert!( - result.is_ok(), - "read_file_tool should be able to read files inside worktrees" - ); - - // Reading files that match file_scan_exclusions should fail - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "project_root/.secretdir/config".to_string(), - start_line: None, - end_line: None, - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await; - assert!( - result.is_err(), - "read_file_tool should error when attempting to read files in .secretdir (file_scan_exclusions)" - ); - - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "project_root/.mymetadata".to_string(), - start_line: None, - end_line: None, - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await; - assert!( - result.is_err(), - "read_file_tool should error when attempting to read .mymetadata files (file_scan_exclusions)" - ); - - // Reading private files should fail - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "project_root/.mysecrets".to_string(), - start_line: None, - end_line: None, - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await; - assert!( - result.is_err(), - "read_file_tool should error when attempting to read .mysecrets (private_files)" - ); - - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "project_root/subdir/special.privatekey".to_string(), - start_line: None, - end_line: None, - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await; - assert!( - result.is_err(), - "read_file_tool should error when attempting to read .privatekey files (private_files)" - ); - - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "project_root/subdir/data.mysensitive".to_string(), - start_line: None, - end_line: None, - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await; - assert!( - result.is_err(), - "read_file_tool should error when attempting to read .mysensitive files (private_files)" - ); - - // Reading a normal file should still work, even with private_files configured - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "project_root/subdir/normal_file.txt".to_string(), - start_line: None, - end_line: None, - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await; - assert!(result.is_ok(), "Should be able to read normal files"); - assert_eq!(result.unwrap(), "Normal file content".into()); - - // Path traversal attempts with .. should fail - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "project_root/../outside_project/sensitive_file.txt".to_string(), - start_line: None, - end_line: None, - }; - tool.run(input, ToolCallEventStream::test().0, cx) - }) - .await; - assert!( - result.is_err(), - "read_file_tool should error when attempting to read a relative path that resolves to outside a worktree" - ); - } - - #[gpui::test] - async fn test_read_file_with_multiple_worktree_settings(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - - // Create first worktree with its own private_files setting - fs.insert_tree( - path!("/worktree1"), - json!({ - "src": { - "main.rs": "fn main() { println!(\"Hello from worktree1\"); }", - "secret.rs": "const API_KEY: &str = \"secret_key_1\";", - "config.toml": "[database]\nurl = \"postgres://localhost/db1\"" - }, - "tests": { - "test.rs": "mod tests { fn test_it() {} }", - "fixture.sql": "CREATE TABLE users (id INT, name VARCHAR(255));" - }, - ".zed": { - "settings.json": r#"{ - "file_scan_exclusions": ["**/fixture.*"], - "private_files": ["**/secret.rs", "**/config.toml"] - }"# - } - }), - ) - .await; - - // Create second worktree with different private_files setting - fs.insert_tree( - path!("/worktree2"), - json!({ - "lib": { - "public.js": "export function greet() { return 'Hello from worktree2'; }", - "private.js": "const SECRET_TOKEN = \"private_token_2\";", - "data.json": "{\"api_key\": \"json_secret_key\"}" - }, - "docs": { - "README.md": "# Public Documentation", - "internal.md": "# Internal Secrets and Configuration" - }, - ".zed": { - "settings.json": r#"{ - "file_scan_exclusions": ["**/internal.*"], - "private_files": ["**/private.js", "**/data.json"] - }"# - } - }), - ) - .await; - - // Set global settings - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = - Some(vec!["**/.git".to_string(), "**/node_modules".to_string()]); - settings.project.worktree.private_files = - Some(vec!["**/.env".to_string()].into()); - }); - }); - }); - - let project = Project::test( - fs.clone(), - [path!("/worktree1").as_ref(), path!("/worktree2").as_ref()], - cx, - ) - .await; - - 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()); - let thread = cx.new(|cx| { - Thread::new( - project.clone(), - cx.new(|_cx| ProjectContext::default()), - context_server_registry, - Templates::new(), - Some(model), - cx, - ) - }); - let tool = Arc::new(ReadFileTool::new( - thread.downgrade(), - project.clone(), - action_log.clone(), - )); - - // Test reading allowed files in worktree1 - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "worktree1/src/main.rs".to_string(), - start_line: None, - end_line: None, - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await - .unwrap(); - - assert_eq!( - result, - "fn main() { println!(\"Hello from worktree1\"); }".into() - ); - - // Test reading private file in worktree1 should fail - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "worktree1/src/secret.rs".to_string(), - start_line: None, - end_line: None, - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await; - - assert!(result.is_err()); - assert!( - result - .unwrap_err() - .to_string() - .contains("worktree `private_files` setting"), - "Error should mention worktree private_files setting" - ); - - // Test reading excluded file in worktree1 should fail - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "worktree1/tests/fixture.sql".to_string(), - start_line: None, - end_line: None, - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await; - - assert!(result.is_err()); - assert!( - result - .unwrap_err() - .to_string() - .contains("worktree `file_scan_exclusions` setting"), - "Error should mention worktree file_scan_exclusions setting" - ); - - // Test reading allowed files in worktree2 - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "worktree2/lib/public.js".to_string(), - start_line: None, - end_line: None, - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await - .unwrap(); - - assert_eq!( - result, - "export function greet() { return 'Hello from worktree2'; }".into() - ); - - // Test reading private file in worktree2 should fail - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "worktree2/lib/private.js".to_string(), - start_line: None, - end_line: None, - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await; - - assert!(result.is_err()); - assert!( - result - .unwrap_err() - .to_string() - .contains("worktree `private_files` setting"), - "Error should mention worktree private_files setting" - ); - - // Test reading excluded file in worktree2 should fail - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "worktree2/docs/internal.md".to_string(), - start_line: None, - end_line: None, - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await; - - assert!(result.is_err()); - assert!( - result - .unwrap_err() - .to_string() - .contains("worktree `file_scan_exclusions` setting"), - "Error should mention worktree file_scan_exclusions setting" - ); - - // Test that files allowed in one worktree but not in another are handled correctly - // (e.g., config.toml is private in worktree1 but doesn't exist in worktree2) - let result = cx - .update(|cx| { - let input = ReadFileToolInput { - path: "worktree1/src/config.toml".to_string(), - start_line: None, - end_line: None, - }; - tool.clone().run(input, ToolCallEventStream::test().0, cx) - }) - .await; - - assert!(result.is_err()); - assert!( - result - .unwrap_err() - .to_string() - .contains("worktree `private_files` setting"), - "Config.toml should be blocked by worktree1's private_files setting" - ); - } -} diff --git a/crates/agent/src/tools/terminal_tool.rs b/crates/agent/src/tools/terminal_tool.rs deleted file mode 100644 index 2db4a2d860..0000000000 --- a/crates/agent/src/tools/terminal_tool.rs +++ /dev/null @@ -1,212 +0,0 @@ -use agent_client_protocol as acp; -use anyhow::Result; -use gpui::{App, Entity, SharedString, Task}; -use project::Project; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::{ - path::{Path, PathBuf}, - rc::Rc, - sync::Arc, -}; -use util::markdown::MarkdownInlineCode; - -use crate::{AgentTool, ThreadEnvironment, ToolCallEventStream}; - -const COMMAND_OUTPUT_LIMIT: u64 = 16 * 1024; - -/// Executes a shell one-liner and returns the combined output. -/// -/// This tool spawns a process using the user's shell, reads from stdout and stderr (preserving the order of writes), and returns a string with the combined output result. -/// -/// The output results will be shown to the user already, only list it again if necessary, avoid being redundant. -/// -/// Make sure you use the `cd` parameter to navigate to one of the root directories of the project. NEVER do it as part of the `command` itself, otherwise it will error. -/// -/// Do not use this tool for commands that run indefinitely, such as servers (like `npm run start`, `npm run dev`, `python -m http.server`, etc) or file watchers that don't terminate on their own. -/// -/// Remember that each invocation of this tool will spawn a new shell process, so you can't rely on any state from previous invocations. -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -pub struct TerminalToolInput { - /// The one-liner command to execute. - command: String, - /// Working directory for the command. This must be one of the root directories of the project. - cd: String, -} - -pub struct TerminalTool { - project: Entity, - environment: Rc, -} - -impl TerminalTool { - pub fn new(project: Entity, environment: Rc) -> Self { - Self { - project, - environment, - } - } -} - -impl AgentTool for TerminalTool { - type Input = TerminalToolInput; - type Output = String; - - fn name() -> &'static str { - "terminal" - } - - fn kind() -> acp::ToolKind { - acp::ToolKind::Execute - } - - fn initial_title( - &self, - input: Result, - _cx: &mut App, - ) -> SharedString { - if let Ok(input) = input { - let mut lines = input.command.lines(); - let first_line = lines.next().unwrap_or_default(); - let remaining_line_count = lines.count(); - match remaining_line_count { - 0 => MarkdownInlineCode(first_line).to_string().into(), - 1 => MarkdownInlineCode(&format!( - "{} - {} more line", - first_line, remaining_line_count - )) - .to_string() - .into(), - n => MarkdownInlineCode(&format!("{} - {} more lines", first_line, n)) - .to_string() - .into(), - } - } else { - "".into() - } - } - - fn run( - self: Arc, - input: Self::Input, - event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - let working_dir = match working_dir(&input, &self.project, cx) { - Ok(dir) => dir, - Err(err) => return Task::ready(Err(err)), - }; - - let authorize = event_stream.authorize(self.initial_title(Ok(input.clone()), cx), cx); - cx.spawn(async move |cx| { - authorize.await?; - - let terminal = self - .environment - .create_terminal( - input.command.clone(), - working_dir, - Some(COMMAND_OUTPUT_LIMIT), - cx, - ) - .await?; - - let terminal_id = terminal.id(cx)?; - event_stream.update_fields(acp::ToolCallUpdateFields::new().content(vec![ - acp::ToolCallContent::Terminal(acp::Terminal::new(terminal_id)), - ])); - - let exit_status = terminal.wait_for_exit(cx)?.await; - let output = terminal.current_output(cx)?; - - Ok(process_content(output, &input.command, exit_status)) - }) - } -} - -fn process_content( - output: acp::TerminalOutputResponse, - command: &str, - exit_status: acp::TerminalExitStatus, -) -> String { - let content = output.output.trim(); - let is_empty = content.is_empty(); - - let content = format!("```\n{content}\n```"); - let content = if output.truncated { - format!( - "Command output too long. The first {} bytes:\n\n{content}", - content.len(), - ) - } else { - content - }; - - let content = match exit_status.exit_code { - Some(0) => { - if is_empty { - "Command executed successfully.".to_string() - } else { - content - } - } - Some(exit_code) => { - if is_empty { - format!("Command \"{command}\" failed with exit code {}.", exit_code) - } else { - format!( - "Command \"{command}\" failed with exit code {}.\n\n{content}", - exit_code - ) - } - } - None => { - format!( - "Command failed or was interrupted.\nPartial output captured:\n\n{}", - content, - ) - } - }; - content -} - -fn working_dir( - input: &TerminalToolInput, - project: &Entity, - cx: &mut App, -) -> Result> { - let project = project.read(cx); - let cd = &input.cd; - - if cd == "." || cd.is_empty() { - // Accept "." or "" as meaning "the one worktree" if we only have one worktree. - let mut worktrees = project.worktrees(cx); - - match worktrees.next() { - Some(worktree) => { - anyhow::ensure!( - worktrees.next().is_none(), - "'.' is ambiguous in multi-root workspaces. Please specify a root directory explicitly.", - ); - Ok(Some(worktree.read(cx).abs_path().to_path_buf())) - } - None => Ok(None), - } - } else { - let input_path = Path::new(cd); - - if input_path.is_absolute() { - // Absolute paths are allowed, but only if they're in one of the project's worktrees. - if project - .worktrees(cx) - .any(|worktree| input_path.starts_with(&worktree.read(cx).abs_path())) - { - return Ok(Some(input_path.into())); - } - } else if let Some(worktree) = project.worktree_for_root_name(cd, cx) { - return Ok(Some(worktree.read(cx).abs_path().to_path_buf())); - } - - anyhow::bail!("`cd` directory {cd:?} was not in any of the project's worktrees."); - } -} diff --git a/crates/agent/src/tools/thinking_tool.rs b/crates/agent/src/tools/thinking_tool.rs deleted file mode 100644 index 96024326f6..0000000000 --- a/crates/agent/src/tools/thinking_tool.rs +++ /dev/null @@ -1,50 +0,0 @@ -use agent_client_protocol as acp; -use anyhow::Result; -use gpui::{App, SharedString, Task}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; - -use crate::{AgentTool, ToolCallEventStream}; - -/// A tool for thinking through problems, brainstorming ideas, or planning without executing any actions. -/// Use this tool when you need to work through complex problems, develop strategies, or outline approaches before taking action. -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct ThinkingToolInput { - /// Content to think about. This should be a description of what to think about or a problem to solve. - content: String, -} - -pub struct ThinkingTool; - -impl AgentTool for ThinkingTool { - type Input = ThinkingToolInput; - type Output = String; - - fn name() -> &'static str { - "thinking" - } - - fn kind() -> acp::ToolKind { - acp::ToolKind::Think - } - - fn initial_title( - &self, - _input: Result, - _cx: &mut App, - ) -> SharedString { - "Thinking".into() - } - - fn run( - self: Arc, - input: Self::Input, - event_stream: ToolCallEventStream, - _cx: &mut App, - ) -> Task> { - event_stream - .update_fields(acp::ToolCallUpdateFields::new().content(vec![input.content.into()])); - Task::ready(Ok("Finished thinking.".to_string())) - } -} diff --git a/crates/agent/src/tools/web_search_tool.rs b/crates/agent/src/tools/web_search_tool.rs deleted file mode 100644 index eb4ebacea2..0000000000 --- a/crates/agent/src/tools/web_search_tool.rs +++ /dev/null @@ -1,127 +0,0 @@ -use std::sync::Arc; - -use crate::{AgentTool, ToolCallEventStream}; -use agent_client_protocol as acp; -use anyhow::{Result, anyhow}; -use cloud_llm_client::WebSearchResponse; -use gpui::{App, AppContext, Task}; -use language_model::{ - LanguageModelProviderId, LanguageModelToolResultContent, ZED_CLOUD_PROVIDER_ID, -}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use ui::prelude::*; -use web_search::WebSearchRegistry; - -/// Search the web for information using your query. -/// Use this when you need real-time information, facts, or data that might not be in your training. -/// Results will include snippets and links from relevant web pages. -#[derive(Debug, Serialize, Deserialize, JsonSchema)] -pub struct WebSearchToolInput { - /// The search term or question to query on the web. - query: String, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(transparent)] -pub struct WebSearchToolOutput(WebSearchResponse); - -impl From for LanguageModelToolResultContent { - fn from(value: WebSearchToolOutput) -> Self { - serde_json::to_string(&value.0) - .expect("Failed to serialize WebSearchResponse") - .into() - } -} - -pub struct WebSearchTool; - -impl AgentTool for WebSearchTool { - type Input = WebSearchToolInput; - type Output = WebSearchToolOutput; - - fn name() -> &'static str { - "web_search" - } - - fn kind() -> acp::ToolKind { - acp::ToolKind::Fetch - } - - fn initial_title( - &self, - _input: Result, - _cx: &mut App, - ) -> SharedString { - "Searching the Web".into() - } - - /// We currently only support Zed Cloud as a provider. - fn supports_provider(provider: &LanguageModelProviderId) -> bool { - provider == &ZED_CLOUD_PROVIDER_ID - } - - fn run( - self: Arc, - input: Self::Input, - event_stream: ToolCallEventStream, - cx: &mut App, - ) -> Task> { - let Some(provider) = WebSearchRegistry::read_global(cx).active_provider() else { - return Task::ready(Err(anyhow!("Web search is not available."))); - }; - - let search_task = provider.search(input.query, cx); - cx.background_spawn(async move { - let response = match search_task.await { - Ok(response) => response, - Err(err) => { - event_stream - .update_fields(acp::ToolCallUpdateFields::new().title("Web Search Failed")); - return Err(err); - } - }; - - emit_update(&response, &event_stream); - Ok(WebSearchToolOutput(response)) - }) - } - - fn replay( - &self, - _input: Self::Input, - output: Self::Output, - event_stream: ToolCallEventStream, - _cx: &mut App, - ) -> Result<()> { - emit_update(&output.0, &event_stream); - Ok(()) - } -} - -fn emit_update(response: &WebSearchResponse, event_stream: &ToolCallEventStream) { - let result_text = if response.results.len() == 1 { - "1 result".to_string() - } else { - format!("{} results", response.results.len()) - }; - event_stream.update_fields( - acp::ToolCallUpdateFields::new() - .title(format!("Searched the web: {result_text}")) - .content( - response - .results - .iter() - .map(|result| { - acp::ToolCallContent::Content(acp::Content::new( - acp::ContentBlock::ResourceLink( - acp::ResourceLink::new(result.title.clone(), result.url.clone()) - .title(result.title.clone()) - .description(result.text.clone()), - ), - )) - }) - .collect::>(), - ), - ); -} diff --git a/crates/agent_servers/Cargo.toml b/crates/agent_servers/Cargo.toml deleted file mode 100644 index 9a04fb763d..0000000000 --- a/crates/agent_servers/Cargo.toml +++ /dev/null @@ -1,67 +0,0 @@ -[package] -name = "agent_servers" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[features] -test-support = ["acp_thread/test-support", "gpui/test-support", "project/test-support", "dep:env_logger", "client/test-support", "dep:gpui_tokio", "reqwest_client/test-support"] -e2e = [] - -[lints] -workspace = true - -[lib] -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 -anyhow.workspace = true -async-trait.workspace = true -client.workspace = true -collections.workspace = true -env_logger = { workspace = true, optional = true } -fs.workspace = true -futures.workspace = true -gpui.workspace = true -gpui_tokio = { workspace = true, optional = true } -http_client.workspace = true -indoc.workspace = true -language_model.workspace = true -language_models.workspace = true -log.workspace = true -project.workspace = true -release_channel.workspace = true -reqwest_client = { workspace = true, optional = true } -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -smol.workspace = true -task.workspace = true -tempfile.workspace = true -thiserror.workspace = true -ui.workspace = true -terminal.workspace = true -uuid.workspace = true -util.workspace = true -watch.workspace = true - -[target.'cfg(unix)'.dependencies] -libc.workspace = true -nix.workspace = true - -[dev-dependencies] -client = { workspace = true, features = ["test-support"] } -env_logger.workspace = true -fs.workspace = true -language.workspace = true -indoc.workspace = true -acp_thread = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -gpui_tokio.workspace = true -reqwest_client = { workspace = true, features = ["test-support"] } diff --git a/crates/agent_servers/LICENSE-GPL b/crates/agent_servers/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/agent_servers/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/agent_servers/src/acp.rs b/crates/agent_servers/src/acp.rs deleted file mode 100644 index 41aff48a20..0000000000 --- a/crates/agent_servers/src/acp.rs +++ /dev/null @@ -1,984 +0,0 @@ -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::io::BufReader; -use project::Project; -use project::agent_server_store::AgentServerCommand; -use serde::Deserialize; -use settings::Settings as _; -use task::ShellBuilder; -use util::ResultExt as _; - -use std::path::PathBuf; -use std::{any::Any, cell::RefCell}; -use std::{path::Path, rc::Rc}; -use thiserror::Error; - -use anyhow::{Context as _, Result}; -use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString, Task, WeakEntity}; - -use acp_thread::{AcpThread, AuthRequired, LoadError, TerminalProviderEvent}; -use terminal::TerminalBuilder; -use terminal::terminal_settings::{AlternateScroll, CursorShape, TerminalSettings}; - -#[derive(Debug, Error)] -#[error("Unsupported version")] -pub struct UnsupportedVersion; - -pub struct AcpConnection { - server_name: SharedString, - telemetry_id: SharedString, - connection: Rc, - sessions: Rc>>, - auth_methods: Vec, - agent_capabilities: acp::AgentCapabilities, - default_mode: Option, - default_model: Option, - root_dir: PathBuf, - // NB: Don't move this into the wait_task, since we need to ensure the process is - // killed on drop (setting kill_on_drop on the command seems to not always work). - child: smol::process::Child, - _io_task: Task>, - _wait_task: Task>, - _stderr_task: Task>, -} - -pub struct AcpSession { - thread: WeakEntity, - suppress_abort_err: bool, - models: Option>>, - session_modes: Option>>, -} - -pub async fn connect( - server_name: SharedString, - command: AgentServerCommand, - root_dir: &Path, - default_mode: Option, - default_model: Option, - is_remote: bool, - cx: &mut AsyncApp, -) -> Result> { - let conn = AcpConnection::stdio( - server_name, - command.clone(), - root_dir, - default_mode, - default_model, - is_remote, - cx, - ) - .await?; - Ok(Rc::new(conn) as _) -} - -const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::ProtocolVersion::V1; - -impl AcpConnection { - pub async fn stdio( - server_name: SharedString, - command: AgentServerCommand, - root_dir: &Path, - default_mode: Option, - default_model: Option, - is_remote: bool, - cx: &mut AsyncApp, - ) -> Result { - let shell = cx.update(|cx| TerminalSettings::get(None, cx).shell.clone())?; - let builder = ShellBuilder::new(&shell, cfg!(windows)); - let mut child = - builder.build_command(Some(command.path.display().to_string()), &command.args); - child - .envs(command.env.iter().flatten()) - .stdin(std::process::Stdio::piped()) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()); - if !is_remote { - child.current_dir(root_dir); - } - let mut child = child.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::debug!( - "Spawning external agent server: {:?}, {:?}", - command.path, - command.args - ); - log::trace!("Spawned (pid: {})", child.id()); - - let sessions = Rc::new(RefCell::new(HashMap::default())); - - let (release_channel, version) = cx.update(|cx| { - ( - release_channel::ReleaseChannel::try_global(cx) - .map(|release_channel| release_channel.display_name()), - release_channel::AppVersion::global(cx).to_string(), - ) - })?; - - 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); - - let stderr_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.trim()); - line.clear(); - } - Ok(()) - }); - - let wait_task = cx.spawn({ - let sessions = sessions.clone(); - let status_fut = child.status(); - async move |cx| { - let status = status_fut.await?; - - for session in sessions.borrow().values() { - session - .thread - .update(cx, |thread, cx| { - thread.emit_load_error(LoadError::Exited { status }, cx) - }) - .ok(); - } - - anyhow::Ok(()) - } - }); - - 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::new(acp::ProtocolVersion::V1) - .client_capabilities( - acp::ClientCapabilities::new() - .fs(acp::FileSystemCapability::new() - .read_text_file(true) - .write_text_file(true)) - .terminal(true) - // Experimental: Allow for rendering terminal output from the agents - .meta(acp::Meta::from_iter([ - ("terminal_output".into(), true.into()), - ("terminal-auth".into(), true.into()), - ])), - ) - .client_info( - acp::Implementation::new("zed", version) - .title(release_channel.map(ToOwned::to_owned)), - ), - ) - .await?; - - if response.protocol_version < MINIMUM_SUPPORTED_VERSION { - return Err(UnsupportedVersion.into()); - } - - let telemetry_id = response - .agent_info - // Use the one the agent provides if we have one - .map(|info| info.name.into()) - // Otherwise, just use the name - .unwrap_or_else(|| server_name.clone()); - - Ok(Self { - auth_methods: response.auth_methods, - root_dir: root_dir.to_owned(), - connection, - server_name, - telemetry_id, - sessions, - agent_capabilities: response.agent_capabilities, - default_mode, - default_model, - _io_task: io_task, - _wait_task: wait_task, - _stderr_task: stderr_task, - child, - }) - } - - pub fn prompt_capabilities(&self) -> &acp::PromptCapabilities { - &self.agent_capabilities.prompt_capabilities - } - - pub fn root_dir(&self) -> &Path { - &self.root_dir - } -} - -impl Drop for AcpConnection { - fn drop(&mut self) { - // See the comment on the child field. - self.child.kill().log_err(); - } -} - -impl AgentConnection for AcpConnection { - fn telemetry_id(&self) -> SharedString { - self.telemetry_id.clone() - } - - fn new_thread( - self: Rc, - project: Entity, - cwd: &Path, - cx: &mut App, - ) -> Task>> { - let name = self.server_name.clone(); - let conn = self.connection.clone(); - let sessions = self.sessions.clone(); - let default_mode = self.default_mode.clone(); - let default_model = self.default_model.clone(); - let cwd = cwd.to_path_buf(); - let context_server_store = project.read(cx).context_server_store().read(cx); - let mcp_servers = if project.read(cx).is_local() { - context_server_store - .configured_server_ids() - .iter() - .filter_map(|id| { - let configuration = context_server_store.configuration_for_server(id)?; - match &*configuration { - project::context_server_store::ContextServerConfiguration::Custom { - command, - .. - } - | project::context_server_store::ContextServerConfiguration::Extension { - command, - .. - } => Some(acp::McpServer::Stdio( - acp::McpServerStdio::new(id.0.to_string(), &command.path) - .args(command.args.clone()) - .env(if let Some(env) = command.env.as_ref() { - env.iter() - .map(|(name, value)| acp::EnvVariable::new(name, value)) - .collect() - } else { - vec![] - }), - )), - project::context_server_store::ContextServerConfiguration::Http { - url, - headers, - } => Some(acp::McpServer::Http( - acp::McpServerHttp::new(id.0.to_string(), url.to_string()).headers( - headers - .iter() - .map(|(name, value)| acp::HttpHeader::new(name, value)) - .collect(), - ), - )), - } - }) - .collect() - } else { - // In SSH projects, the external agent is running on the remote - // machine, and currently we only run MCP servers on the local - // machine. So don't pass any MCP servers to the agent in that case. - Vec::new() - }; - - cx.spawn(async move |cx| { - let response = conn - .new_session(acp::NewSessionRequest::new(cwd).mcp_servers(mcp_servers)) - .await - .map_err(|err| { - if err.code == acp::ErrorCode::AuthRequired { - let mut error = AuthRequired::new(); - - if err.message != acp::ErrorCode::AuthRequired.to_string() { - error = error.with_description(err.message); - } - - anyhow!(error) - } else { - anyhow!(err) - } - })?; - - let modes = response.modes.map(|modes| Rc::new(RefCell::new(modes))); - let models = response.models.map(|models| Rc::new(RefCell::new(models))); - - if let Some(default_mode) = default_mode { - if let Some(modes) = modes.as_ref() { - let mut modes_ref = modes.borrow_mut(); - let has_mode = modes_ref.available_modes.iter().any(|mode| mode.id == default_mode); - - if has_mode { - let initial_mode_id = modes_ref.current_mode_id.clone(); - - cx.spawn({ - let default_mode = default_mode.clone(); - let session_id = response.session_id.clone(); - let modes = modes.clone(); - let conn = conn.clone(); - async move |_| { - let result = conn.set_session_mode(acp::SetSessionModeRequest::new(session_id, default_mode)) - .await.log_err(); - - if result.is_none() { - modes.borrow_mut().current_mode_id = initial_mode_id; - } - } - }).detach(); - - modes_ref.current_mode_id = default_mode; - } else { - let available_modes = modes_ref - .available_modes - .iter() - .map(|mode| format!("- `{}`: {}", mode.id, mode.name)) - .collect::>() - .join("\n"); - - log::warn!( - "`{default_mode}` is not valid {name} mode. Available options:\n{available_modes}", - ); - } - } else { - log::warn!( - "`{name}` does not support modes, but `default_mode` was set in settings.", - ); - } - } - - if let Some(default_model) = default_model { - if let Some(models) = models.as_ref() { - let mut models_ref = models.borrow_mut(); - let has_model = models_ref.available_models.iter().any(|model| model.model_id == default_model); - - if has_model { - let initial_model_id = models_ref.current_model_id.clone(); - - cx.spawn({ - let default_model = default_model.clone(); - let session_id = response.session_id.clone(); - let models = models.clone(); - let conn = conn.clone(); - async move |_| { - let result = conn.set_session_model(acp::SetSessionModelRequest::new(session_id, default_model)) - .await.log_err(); - - if result.is_none() { - models.borrow_mut().current_model_id = initial_model_id; - } - } - }).detach(); - - models_ref.current_model_id = default_model; - } else { - let available_models = models_ref - .available_models - .iter() - .map(|model| format!("- `{}`: {}", model.model_id, model.name)) - .collect::>() - .join("\n"); - - log::warn!( - "`{default_model}` is not a valid {name} model. Available options:\n{available_models}", - ); - } - } else { - log::warn!( - "`{name}` does not support model selection, but `default_model` was set in settings.", - ); - } - } - - 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.agent_capabilities.prompt_capabilities.clone()), - cx, - ) - })?; - - - let session = AcpSession { - thread: thread.downgrade(), - suppress_abort_err: false, - session_modes: modes, - models, - }; - 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 { - conn.authenticate(acp::AuthenticateRequest::new(method_id)) - .await?; - Ok(()) - }) - } - - 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 == acp::ErrorCode::AuthRequired { - return Err(anyhow!(acp::Error::auth_required())); - } - - if err.code != ErrorCode::InternalError { - 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::new(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::new(session_id.clone()); - cx.foreground_executor() - .spawn(async move { conn.cancel(params).await }) - .detach(); - } - - fn session_modes( - &self, - session_id: &acp::SessionId, - _cx: &App, - ) -> Option> { - let sessions = self.sessions.clone(); - let sessions_ref = sessions.borrow(); - let Some(session) = sessions_ref.get(session_id) else { - return None; - }; - - if let Some(modes) = session.session_modes.as_ref() { - Some(Rc::new(AcpSessionModes { - connection: self.connection.clone(), - session_id: session_id.clone(), - state: modes.clone(), - }) as _) - } else { - None - } - } - - fn model_selector( - &self, - session_id: &acp::SessionId, - ) -> Option> { - let sessions = self.sessions.clone(); - let sessions_ref = sessions.borrow(); - let Some(session) = sessions_ref.get(session_id) else { - return None; - }; - - if let Some(models) = session.models.as_ref() { - Some(Rc::new(AcpModelSelector::new( - session_id.clone(), - self.connection.clone(), - models.clone(), - )) as _) - } else { - None - } - } - - fn into_any(self: Rc) -> Rc { - self - } -} - -struct AcpSessionModes { - session_id: acp::SessionId, - connection: Rc, - state: Rc>, -} - -impl acp_thread::AgentSessionModes for AcpSessionModes { - fn current_mode(&self) -> acp::SessionModeId { - self.state.borrow().current_mode_id.clone() - } - - fn all_modes(&self) -> Vec { - self.state.borrow().available_modes.clone() - } - - fn set_mode(&self, mode_id: acp::SessionModeId, cx: &mut App) -> Task> { - let connection = self.connection.clone(); - let session_id = self.session_id.clone(); - let old_mode_id; - { - let mut state = self.state.borrow_mut(); - old_mode_id = state.current_mode_id.clone(); - state.current_mode_id = mode_id.clone(); - }; - let state = self.state.clone(); - cx.foreground_executor().spawn(async move { - let result = connection - .set_session_mode(acp::SetSessionModeRequest::new(session_id, mode_id)) - .await; - - if result.is_err() { - state.borrow_mut().current_mode_id = old_mode_id; - } - - result?; - - Ok(()) - }) - } -} - -struct AcpModelSelector { - session_id: acp::SessionId, - connection: Rc, - state: Rc>, -} - -impl AcpModelSelector { - fn new( - session_id: acp::SessionId, - connection: Rc, - state: Rc>, - ) -> Self { - Self { - session_id, - connection, - state, - } - } -} - -impl acp_thread::AgentModelSelector for AcpModelSelector { - fn list_models(&self, _cx: &mut App) -> Task> { - Task::ready(Ok(acp_thread::AgentModelList::Flat( - self.state - .borrow() - .available_models - .clone() - .into_iter() - .map(acp_thread::AgentModelInfo::from) - .collect(), - ))) - } - - fn select_model(&self, model_id: acp::ModelId, cx: &mut App) -> Task> { - let connection = self.connection.clone(); - let session_id = self.session_id.clone(); - let old_model_id; - { - let mut state = self.state.borrow_mut(); - old_model_id = state.current_model_id.clone(); - state.current_model_id = model_id.clone(); - }; - let state = self.state.clone(); - cx.foreground_executor().spawn(async move { - let result = connection - .set_session_model(acp::SetSessionModelRequest::new(session_id, model_id)) - .await; - - if result.is_err() { - state.borrow_mut().current_model_id = old_model_id; - } - - result?; - - Ok(()) - }) - } - - fn selected_model(&self, _cx: &mut App) -> Task> { - let state = self.state.borrow(); - Task::ready( - state - .available_models - .iter() - .find(|m| m.model_id == state.current_model_id) - .cloned() - .map(acp_thread::AgentModelInfo::from) - .ok_or_else(|| anyhow::anyhow!("Model not found")), - ) - } -} - -struct ClientDelegate { - sessions: Rc>>, - cx: AsyncApp, -} - -#[async_trait::async_trait(?Send)] -impl acp::Client for ClientDelegate { - async fn request_permission( - &self, - arguments: acp::RequestPermissionRequest, - ) -> Result { - let respect_always_allow_setting; - let thread; - { - let sessions_ref = self.sessions.borrow(); - let session = sessions_ref - .get(&arguments.session_id) - .context("Failed to get session")?; - respect_always_allow_setting = session.session_modes.is_none(); - thread = session.thread.clone(); - } - - let cx = &mut self.cx.clone(); - - let task = thread.update(cx, |thread, cx| { - thread.request_tool_call_authorization( - arguments.tool_call, - arguments.options, - respect_always_allow_setting, - cx, - ) - })??; - - let outcome = task.await; - - Ok(acp::RequestPermissionResponse::new(outcome)) - } - - async fn write_text_file( - &self, - arguments: acp::WriteTextFileRequest, - ) -> Result { - let cx = &mut self.cx.clone(); - let task = self - .session_thread(&arguments.session_id)? - .update(cx, |thread, cx| { - thread.write_text_file(arguments.path, arguments.content, cx) - })?; - - task.await?; - - Ok(Default::default()) - } - - async fn read_text_file( - &self, - arguments: acp::ReadTextFileRequest, - ) -> Result { - let task = self.session_thread(&arguments.session_id)?.update( - &mut self.cx.clone(), - |thread, cx| { - thread.read_text_file(arguments.path, arguments.line, arguments.limit, false, cx) - }, - )?; - - let content = task.await?; - - Ok(acp::ReadTextFileResponse::new(content)) - } - - async fn session_notification( - &self, - notification: acp::SessionNotification, - ) -> Result<(), acp::Error> { - let sessions = self.sessions.borrow(); - let session = sessions - .get(¬ification.session_id) - .context("Failed to get session")?; - - if let acp::SessionUpdate::CurrentModeUpdate(acp::CurrentModeUpdate { - current_mode_id, - .. - }) = ¬ification.update - { - if let Some(session_modes) = &session.session_modes { - session_modes.borrow_mut().current_mode_id = current_mode_id.clone(); - } else { - log::error!( - "Got a `CurrentModeUpdate` notification, but they agent didn't specify `modes` during setting setup." - ); - } - } - - // Clone so we can inspect meta both before and after handing off to the thread - let update_clone = notification.update.clone(); - - // Pre-handle: if a ToolCall carries terminal_info, create/register a display-only terminal. - if let acp::SessionUpdate::ToolCall(tc) = &update_clone { - if let Some(meta) = &tc.meta { - if let Some(terminal_info) = meta.get("terminal_info") { - if let Some(id_str) = terminal_info.get("terminal_id").and_then(|v| v.as_str()) - { - let terminal_id = acp::TerminalId::new(id_str); - let cwd = terminal_info - .get("cwd") - .and_then(|v| v.as_str().map(PathBuf::from)); - - // Create a minimal display-only lower-level terminal and register it. - let _ = session.thread.update(&mut self.cx.clone(), |thread, cx| { - let builder = TerminalBuilder::new_display_only( - CursorShape::default(), - AlternateScroll::On, - None, - 0, - )?; - let lower = cx.new(|cx| builder.subscribe(cx)); - thread.on_terminal_provider_event( - TerminalProviderEvent::Created { - terminal_id, - label: tc.title.clone(), - cwd, - output_byte_limit: None, - terminal: lower, - }, - cx, - ); - anyhow::Ok(()) - }); - } - } - } - } - - // Forward the update to the acp_thread as usual. - session.thread.update(&mut self.cx.clone(), |thread, cx| { - thread.handle_session_update(notification.update.clone(), cx) - })??; - - // Post-handle: stream terminal output/exit if present on ToolCallUpdate meta. - if let acp::SessionUpdate::ToolCallUpdate(tcu) = &update_clone { - if let Some(meta) = &tcu.meta { - if let Some(term_out) = meta.get("terminal_output") { - if let Some(id_str) = term_out.get("terminal_id").and_then(|v| v.as_str()) { - let terminal_id = acp::TerminalId::new(id_str); - if let Some(s) = term_out.get("data").and_then(|v| v.as_str()) { - let data = s.as_bytes().to_vec(); - let _ = session.thread.update(&mut self.cx.clone(), |thread, cx| { - thread.on_terminal_provider_event( - TerminalProviderEvent::Output { terminal_id, data }, - cx, - ); - }); - } - } - } - - // terminal_exit - if let Some(term_exit) = meta.get("terminal_exit") { - if let Some(id_str) = term_exit.get("terminal_id").and_then(|v| v.as_str()) { - let terminal_id = acp::TerminalId::new(id_str); - let status = acp::TerminalExitStatus::new() - .exit_code( - term_exit - .get("exit_code") - .and_then(|v| v.as_u64()) - .map(|i| i as u32), - ) - .signal( - term_exit - .get("signal") - .and_then(|v| v.as_str().map(|s| s.to_string())), - ); - - let _ = session.thread.update(&mut self.cx.clone(), |thread, cx| { - thread.on_terminal_provider_event( - TerminalProviderEvent::Exit { - terminal_id, - status, - }, - cx, - ); - }); - } - } - } - } - - Ok(()) - } - - async fn create_terminal( - &self, - args: acp::CreateTerminalRequest, - ) -> Result { - let thread = self.session_thread(&args.session_id)?; - let project = thread.read_with(&self.cx, |thread, _cx| thread.project().clone())?; - - let terminal_entity = acp_thread::create_terminal_entity( - args.command.clone(), - &args.args, - args.env - .into_iter() - .map(|env| (env.name, env.value)) - .collect(), - args.cwd.clone(), - &project, - &mut self.cx.clone(), - ) - .await?; - - // Register with renderer - let terminal_entity = thread.update(&mut self.cx.clone(), |thread, cx| { - thread.register_terminal_created( - acp::TerminalId::new(uuid::Uuid::new_v4().to_string()), - format!("{} {}", args.command, args.args.join(" ")), - args.cwd.clone(), - args.output_byte_limit, - terminal_entity, - cx, - ) - })?; - let terminal_id = - terminal_entity.read_with(&self.cx, |terminal, _| terminal.id().clone())?; - Ok(acp::CreateTerminalResponse::new(terminal_id)) - } - - async fn kill_terminal_command( - &self, - args: acp::KillTerminalCommandRequest, - ) -> Result { - self.session_thread(&args.session_id)? - .update(&mut self.cx.clone(), |thread, cx| { - thread.kill_terminal(args.terminal_id, cx) - })??; - - Ok(Default::default()) - } - - async fn ext_method(&self, _args: acp::ExtRequest) -> Result { - Err(acp::Error::method_not_found()) - } - - async fn ext_notification(&self, _args: acp::ExtNotification) -> Result<(), acp::Error> { - Err(acp::Error::method_not_found()) - } - - async fn release_terminal( - &self, - args: acp::ReleaseTerminalRequest, - ) -> Result { - self.session_thread(&args.session_id)? - .update(&mut self.cx.clone(), |thread, cx| { - thread.release_terminal(args.terminal_id, cx) - })??; - - Ok(Default::default()) - } - - async fn terminal_output( - &self, - args: acp::TerminalOutputRequest, - ) -> Result { - self.session_thread(&args.session_id)? - .read_with(&mut self.cx.clone(), |thread, cx| { - let out = thread - .terminal(args.terminal_id)? - .read(cx) - .current_output(cx); - - Ok(out) - })? - } - - async fn wait_for_terminal_exit( - &self, - args: acp::WaitForTerminalExitRequest, - ) -> Result { - let exit_status = self - .session_thread(&args.session_id)? - .update(&mut self.cx.clone(), |thread, cx| { - anyhow::Ok(thread.terminal(args.terminal_id)?.read(cx).wait_for_exit()) - })?? - .await; - - Ok(acp::WaitForTerminalExitResponse::new(exit_status)) - } -} - -impl ClientDelegate { - fn session_thread(&self, session_id: &acp::SessionId) -> Result> { - let sessions = self.sessions.borrow(); - sessions - .get(session_id) - .context("Failed to get session") - .map(|session| session.thread.clone()) - } -} diff --git a/crates/agent_servers/src/agent_servers.rs b/crates/agent_servers/src/agent_servers.rs deleted file mode 100644 index 46e8508e44..0000000000 --- a/crates/agent_servers/src/agent_servers.rs +++ /dev/null @@ -1,121 +0,0 @@ -mod acp; -mod claude; -mod codex; -mod custom; -mod gemini; - -#[cfg(any(test, feature = "test-support"))] -pub mod e2e_tests; - -pub use claude::*; -use client::ProxySettings; -pub use codex::*; -use collections::HashMap; -pub use custom::*; -use fs::Fs; -pub use gemini::*; -use http_client::read_no_proxy_from_env; -use project::agent_server_store::AgentServerStore; - -use acp_thread::AgentConnection; -use anyhow::Result; -use gpui::{App, AppContext, Entity, SharedString, Task}; -use project::Project; -use settings::SettingsStore; -use std::{any::Any, path::Path, rc::Rc, sync::Arc}; - -pub use acp::AcpConnection; - -pub struct AgentServerDelegate { - store: Entity, - project: Entity, - status_tx: Option>, - new_version_available: Option>>, -} - -impl AgentServerDelegate { - pub fn new( - store: Entity, - project: Entity, - status_tx: Option>, - new_version_tx: Option>>, - ) -> Self { - Self { - store, - project, - status_tx, - new_version_available: new_version_tx, - } - } - - pub fn project(&self) -> &Entity { - &self.project - } -} - -pub trait AgentServer: Send { - fn logo(&self) -> ui::IconName; - fn name(&self) -> SharedString; - fn default_mode(&self, _cx: &mut App) -> Option { - None - } - fn set_default_mode( - &self, - _mode_id: Option, - _fs: Arc, - _cx: &mut App, - ) { - } - - fn default_model(&self, _cx: &mut App) -> Option { - None - } - - fn set_default_model( - &self, - _model_id: Option, - _fs: Arc, - _cx: &mut App, - ) { - } - - fn connect( - &self, - root_dir: Option<&Path>, - delegate: AgentServerDelegate, - cx: &mut App, - ) -> Task, Option)>>; - - fn into_any(self: Rc) -> Rc; -} - -impl dyn AgentServer { - pub fn downcast(self: Rc) -> Option> { - self.into_any().downcast().ok() - } -} - -/// Load the default proxy environment variables to pass through to the agent -pub fn load_proxy_env(cx: &mut App) -> HashMap { - let proxy_url = cx - .read_global(|settings: &SettingsStore, _| settings.get::(None).proxy_url()); - let mut env = HashMap::default(); - - if let Some(proxy_url) = &proxy_url { - let env_var = if proxy_url.scheme() == "https" { - "HTTPS_PROXY" - } else { - "HTTP_PROXY" - }; - env.insert(env_var.to_owned(), proxy_url.to_string()); - } - - if let Some(no_proxy) = read_no_proxy_from_env() { - env.insert("NO_PROXY".to_owned(), no_proxy); - } else if proxy_url.is_some() { - // We sometimes need local MCP servers that we don't want to proxy - env.insert("NO_PROXY".to_owned(), "localhost,127.0.0.1".to_owned()); - } - - env -} diff --git a/crates/agent_servers/src/claude.rs b/crates/agent_servers/src/claude.rs deleted file mode 100644 index e67ddd5c06..0000000000 --- a/crates/agent_servers/src/claude.rs +++ /dev/null @@ -1,121 +0,0 @@ -use agent_client_protocol as acp; -use fs::Fs; -use settings::{SettingsStore, update_settings_file}; -use std::path::Path; -use std::rc::Rc; -use std::sync::Arc; -use std::{any::Any, path::PathBuf}; - -use anyhow::{Context as _, Result}; -use gpui::{App, AppContext as _, SharedString, Task}; -use project::agent_server_store::{AllAgentServersSettings, CLAUDE_CODE_NAME}; - -use crate::{AgentServer, AgentServerDelegate, load_proxy_env}; -use acp_thread::AgentConnection; - -#[derive(Clone)] -pub struct ClaudeCode; - -pub struct AgentServerLoginCommand { - pub path: PathBuf, - pub arguments: Vec, -} - -impl AgentServer for ClaudeCode { - fn name(&self) -> SharedString { - "Claude Code".into() - } - - fn logo(&self) -> ui::IconName { - ui::IconName::AiClaude - } - - fn default_mode(&self, cx: &mut App) -> Option { - let settings = cx.read_global(|settings: &SettingsStore, _| { - settings.get::(None).claude.clone() - }); - - settings - .as_ref() - .and_then(|s| s.default_mode.clone().map(acp::SessionModeId::new)) - } - - fn set_default_mode(&self, mode_id: Option, fs: Arc, cx: &mut App) { - update_settings_file(fs, cx, |settings, _| { - settings - .agent_servers - .get_or_insert_default() - .claude - .get_or_insert_default() - .default_mode = mode_id.map(|m| m.to_string()) - }); - } - - fn default_model(&self, cx: &mut App) -> Option { - let settings = cx.read_global(|settings: &SettingsStore, _| { - settings.get::(None).claude.clone() - }); - - settings - .as_ref() - .and_then(|s| s.default_model.clone().map(acp::ModelId::new)) - } - - fn set_default_model(&self, model_id: Option, fs: Arc, cx: &mut App) { - update_settings_file(fs, cx, |settings, _| { - settings - .agent_servers - .get_or_insert_default() - .claude - .get_or_insert_default() - .default_model = model_id.map(|m| m.to_string()) - }); - } - - fn connect( - &self, - root_dir: Option<&Path>, - delegate: AgentServerDelegate, - cx: &mut App, - ) -> Task, Option)>> { - let name = self.name(); - let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned()); - let is_remote = delegate.project.read(cx).is_via_remote_server(); - let store = delegate.store.downgrade(); - let extra_env = load_proxy_env(cx); - let default_mode = self.default_mode(cx); - let default_model = self.default_model(cx); - - cx.spawn(async move |cx| { - let (command, root_dir, login) = store - .update(cx, |store, cx| { - let agent = store - .get_external_agent(&CLAUDE_CODE_NAME.into()) - .context("Claude Code is not registered")?; - anyhow::Ok(agent.get_command( - root_dir.as_deref(), - extra_env, - delegate.status_tx, - delegate.new_version_available, - &mut cx.to_async(), - )) - })?? - .await?; - let connection = crate::acp::connect( - name, - command, - root_dir.as_ref(), - default_mode, - default_model, - is_remote, - cx, - ) - .await?; - Ok((connection, login)) - }) - } - - fn into_any(self: Rc) -> Rc { - self - } -} diff --git a/crates/agent_servers/src/codex.rs b/crates/agent_servers/src/codex.rs deleted file mode 100644 index c2b308e48b..0000000000 --- a/crates/agent_servers/src/codex.rs +++ /dev/null @@ -1,123 +0,0 @@ -use std::rc::Rc; -use std::sync::Arc; -use std::{any::Any, path::Path}; - -use acp_thread::AgentConnection; -use agent_client_protocol as acp; -use anyhow::{Context as _, Result}; -use fs::Fs; -use gpui::{App, AppContext as _, SharedString, Task}; -use project::agent_server_store::{AllAgentServersSettings, CODEX_NAME}; -use settings::{SettingsStore, update_settings_file}; - -use crate::{AgentServer, AgentServerDelegate, load_proxy_env}; - -#[derive(Clone)] -pub struct Codex; - -#[cfg(test)] -pub(crate) mod tests { - use super::*; - - crate::common_e2e_tests!(async |_, _, _| Codex, allow_option_id = "proceed_once"); -} - -impl AgentServer for Codex { - fn name(&self) -> SharedString { - "Codex".into() - } - - fn logo(&self) -> ui::IconName { - ui::IconName::AiOpenAi - } - - fn default_mode(&self, cx: &mut App) -> Option { - let settings = cx.read_global(|settings: &SettingsStore, _| { - settings.get::(None).codex.clone() - }); - - settings - .as_ref() - .and_then(|s| s.default_mode.clone().map(acp::SessionModeId::new)) - } - - fn set_default_mode(&self, mode_id: Option, fs: Arc, cx: &mut App) { - update_settings_file(fs, cx, |settings, _| { - settings - .agent_servers - .get_or_insert_default() - .codex - .get_or_insert_default() - .default_mode = mode_id.map(|m| m.to_string()) - }); - } - - fn default_model(&self, cx: &mut App) -> Option { - let settings = cx.read_global(|settings: &SettingsStore, _| { - settings.get::(None).codex.clone() - }); - - settings - .as_ref() - .and_then(|s| s.default_model.clone().map(acp::ModelId::new)) - } - - fn set_default_model(&self, model_id: Option, fs: Arc, cx: &mut App) { - update_settings_file(fs, cx, |settings, _| { - settings - .agent_servers - .get_or_insert_default() - .codex - .get_or_insert_default() - .default_model = model_id.map(|m| m.to_string()) - }); - } - - fn connect( - &self, - root_dir: Option<&Path>, - delegate: AgentServerDelegate, - cx: &mut App, - ) -> Task, Option)>> { - let name = self.name(); - let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned()); - let is_remote = delegate.project.read(cx).is_via_remote_server(); - let store = delegate.store.downgrade(); - let extra_env = load_proxy_env(cx); - let default_mode = self.default_mode(cx); - let default_model = self.default_model(cx); - - cx.spawn(async move |cx| { - let (command, root_dir, login) = store - .update(cx, |store, cx| { - let agent = store - .get_external_agent(&CODEX_NAME.into()) - .context("Codex is not registered")?; - anyhow::Ok(agent.get_command( - root_dir.as_deref(), - extra_env, - delegate.status_tx, - delegate.new_version_available, - &mut cx.to_async(), - )) - })?? - .await?; - - let connection = crate::acp::connect( - name, - command, - root_dir.as_ref(), - default_mode, - default_model, - is_remote, - cx, - ) - .await?; - Ok((connection, login)) - }) - } - - fn into_any(self: Rc) -> Rc { - self - } -} diff --git a/crates/agent_servers/src/custom.rs b/crates/agent_servers/src/custom.rs deleted file mode 100644 index 6b981ce8b8..0000000000 --- a/crates/agent_servers/src/custom.rs +++ /dev/null @@ -1,151 +0,0 @@ -use crate::{AgentServer, AgentServerDelegate, load_proxy_env}; -use acp_thread::AgentConnection; -use agent_client_protocol as acp; -use anyhow::{Context as _, Result}; -use fs::Fs; -use gpui::{App, AppContext as _, SharedString, Task}; -use project::agent_server_store::{AllAgentServersSettings, ExternalAgentServerName}; -use settings::{SettingsStore, update_settings_file}; -use std::{path::Path, rc::Rc, sync::Arc}; -use ui::IconName; - -/// A generic agent server implementation for custom user-defined agents -pub struct CustomAgentServer { - name: SharedString, -} - -impl CustomAgentServer { - pub fn new(name: SharedString) -> Self { - Self { name } - } -} - -impl AgentServer for CustomAgentServer { - fn name(&self) -> SharedString { - self.name.clone() - } - - fn logo(&self) -> IconName { - IconName::Terminal - } - - fn default_mode(&self, cx: &mut App) -> Option { - let settings = cx.read_global(|settings: &SettingsStore, _| { - settings - .get::(None) - .custom - .get(&self.name()) - .cloned() - }); - - settings - .as_ref() - .and_then(|s| s.default_mode().map(acp::SessionModeId::new)) - } - - fn set_default_mode(&self, mode_id: Option, fs: Arc, cx: &mut App) { - let name = self.name(); - update_settings_file(fs, cx, move |settings, _| { - let settings = settings - .agent_servers - .get_or_insert_default() - .custom - .entry(name.clone()) - .or_insert_with(|| settings::CustomAgentServerSettings::Extension { - default_model: None, - default_mode: None, - }); - - match settings { - settings::CustomAgentServerSettings::Custom { default_mode, .. } - | settings::CustomAgentServerSettings::Extension { default_mode, .. } => { - *default_mode = mode_id.map(|m| m.to_string()); - } - } - }); - } - - fn default_model(&self, cx: &mut App) -> Option { - let settings = cx.read_global(|settings: &SettingsStore, _| { - settings - .get::(None) - .custom - .get(&self.name()) - .cloned() - }); - - settings - .as_ref() - .and_then(|s| s.default_model().map(acp::ModelId::new)) - } - - fn set_default_model(&self, model_id: Option, fs: Arc, cx: &mut App) { - let name = self.name(); - update_settings_file(fs, cx, move |settings, _| { - let settings = settings - .agent_servers - .get_or_insert_default() - .custom - .entry(name.clone()) - .or_insert_with(|| settings::CustomAgentServerSettings::Extension { - default_model: None, - default_mode: None, - }); - - match settings { - settings::CustomAgentServerSettings::Custom { default_model, .. } - | settings::CustomAgentServerSettings::Extension { default_model, .. } => { - *default_model = model_id.map(|m| m.to_string()); - } - } - }); - } - - fn connect( - &self, - root_dir: Option<&Path>, - delegate: AgentServerDelegate, - cx: &mut App, - ) -> Task, Option)>> { - let name = self.name(); - let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned()); - let is_remote = delegate.project.read(cx).is_via_remote_server(); - let default_mode = self.default_mode(cx); - let default_model = self.default_model(cx); - let store = delegate.store.downgrade(); - let extra_env = load_proxy_env(cx); - cx.spawn(async move |cx| { - let (command, root_dir, login) = store - .update(cx, |store, cx| { - let agent = store - .get_external_agent(&ExternalAgentServerName(name.clone())) - .with_context(|| { - format!("Custom agent server `{}` is not registered", name) - })?; - anyhow::Ok(agent.get_command( - root_dir.as_deref(), - extra_env, - delegate.status_tx, - delegate.new_version_available, - &mut cx.to_async(), - )) - })?? - .await?; - let connection = crate::acp::connect( - name, - command, - root_dir.as_ref(), - default_mode, - default_model, - is_remote, - cx, - ) - .await?; - Ok((connection, login)) - }) - } - - 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 deleted file mode 100644 index 9db7535b5e..0000000000 --- a/crates/agent_servers/src/e2e_tests.rs +++ /dev/null @@ -1,552 +0,0 @@ -use crate::{AgentServer, AgentServerDelegate}; -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; -#[cfg(test)] -use project::agent_server_store::BuiltinAgentServerSettings; -use project::{FakeFs, Project}; -#[cfg(test)] -use settings::Settings; -use std::{ - path::{Path, PathBuf}, - sync::Arc, - time::Duration, -}; -use util::path; - -pub async fn test_basic(server: F, cx: &mut TestAppContext) -where - T: AgentServer + 'static, - F: AsyncFn(&Arc, &Entity, &mut TestAppContext) -> T, -{ - let fs = init_test(cx).await as Arc; - let project = Project::test(fs.clone(), [], cx).await; - let thread = new_test_thread( - server(&fs, &project, cx).await, - project.clone(), - "/private/tmp", - cx, - ) - .await; - - thread - .update(cx, |thread, cx| thread.send_raw("Hello from Zed!", cx)) - .await - .unwrap(); - - thread.read_with(cx, |thread, _| { - assert!( - thread.entries().len() >= 2, - "Expected at least 2 entries. Got: {:?}", - thread.entries() - ); - assert!(matches!( - thread.entries()[0], - AgentThreadEntry::UserMessage(_) - )); - assert!(matches!( - thread.entries()[1], - AgentThreadEntry::AssistantMessage(_) - )); - }); -} - -pub async fn test_path_mentions(server: F, cx: &mut TestAppContext) -where - T: AgentServer + 'static, - F: AsyncFn(&Arc, &Entity, &mut TestAppContext) -> T, -{ - let fs = init_test(cx).await as _; - - let tempdir = tempfile::tempdir().unwrap(); - std::fs::write( - tempdir.path().join("foo.rs"), - indoc! {" - fn main() { - println!(\"Hello, world!\"); - } - "}, - ) - .expect("failed to write file"); - let project = Project::example([tempdir.path()], &mut cx.to_async()).await; - let thread = new_test_thread( - server(&fs, &project, cx).await, - project.clone(), - tempdir.path(), - cx, - ) - .await; - thread - .update(cx, |thread, cx| { - thread.send( - vec![ - "Read the file ".into(), - acp::ContentBlock::ResourceLink(acp::ResourceLink::new("foo.rs", "foo.rs")), - " and tell me what the content of the println! is".into(), - ], - cx, - ) - }) - .await - .unwrap(); - - thread.read_with(cx, |thread, cx| { - assert!(matches!( - thread.entries()[0], - AgentThreadEntry::UserMessage(_) - )); - let assistant_message = &thread - .entries() - .iter() - .rev() - .find_map(|entry| match entry { - AgentThreadEntry::AssistantMessage(msg) => Some(msg), - _ => None, - }) - .unwrap(); - - assert!( - assistant_message.to_markdown(cx).contains("Hello, world!"), - "unexpected assistant message: {:?}", - assistant_message.to_markdown(cx) - ); - }); - - drop(tempdir); -} - -pub async fn test_tool_call(server: F, cx: &mut TestAppContext) -where - T: AgentServer + 'static, - F: AsyncFn(&Arc, &Entity, &mut TestAppContext) -> T, -{ - let fs = init_test(cx).await as _; - - let tempdir = tempfile::tempdir().unwrap(); - let foo_path = tempdir.path().join("foo"); - std::fs::write(&foo_path, "Lorem ipsum dolor").expect("failed to write file"); - - let project = Project::example([tempdir.path()], &mut cx.to_async()).await; - let thread = new_test_thread( - server(&fs, &project, cx).await, - project.clone(), - "/private/tmp", - cx, - ) - .await; - - thread - .update(cx, |thread, cx| { - thread.send_raw( - &format!("Read {} and tell me what you see.", foo_path.display()), - cx, - ) - }) - .await - .unwrap(); - thread.read_with(cx, |thread, _cx| { - assert!(thread.entries().iter().any(|entry| { - matches!( - entry, - AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::Pending - | ToolCallStatus::InProgress - | ToolCallStatus::Completed, - .. - }) - ) - })); - assert!( - thread - .entries() - .iter() - .any(|entry| { matches!(entry, AgentThreadEntry::AssistantMessage(_)) }) - ); - }); - - drop(tempdir); -} - -pub async fn test_tool_call_with_permission( - server: F, - allow_option_id: acp::PermissionOptionId, - cx: &mut TestAppContext, -) where - T: AgentServer + 'static, - F: AsyncFn(&Arc, &Entity, &mut TestAppContext) -> T, -{ - let fs = init_test(cx).await as Arc; - let project = Project::test(fs.clone(), [path!("/private/tmp").as_ref()], cx).await; - let thread = new_test_thread( - server(&fs, &project, cx).await, - project.clone(), - "/private/tmp", - cx, - ) - .await; - let full_turn = thread.update(cx, |thread, cx| { - thread.send_raw( - r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#, - cx, - ) - }); - - run_until_first_tool_call( - &thread, - |entry| { - matches!( - entry, - AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::WaitingForConfirmation { .. }, - .. - }) - ) - }, - cx, - ) - .await; - - let tool_call_id = thread.read_with(cx, |thread, cx| { - let AgentThreadEntry::ToolCall(ToolCall { - id, - label, - status: ToolCallStatus::WaitingForConfirmation { .. }, - .. - }) = &thread - .entries() - .iter() - .find(|entry| matches!(entry, AgentThreadEntry::ToolCall(_))) - .unwrap() - else { - panic!(); - }; - - let label = label.read(cx).source(); - assert!(label.contains("touch"), "Got: {}", label); - - id.clone() - }); - - thread.update(cx, |thread, cx| { - thread.authorize_tool_call( - tool_call_id, - allow_option_id, - acp::PermissionOptionKind::AllowOnce, - cx, - ); - - assert!(thread.entries().iter().any(|entry| matches!( - entry, - AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::Pending - | ToolCallStatus::InProgress - | ToolCallStatus::Completed, - .. - }) - ))); - }); - - full_turn.await.unwrap(); - - thread.read_with(cx, |thread, cx| { - let AgentThreadEntry::ToolCall(ToolCall { - content, - status: ToolCallStatus::Pending - | ToolCallStatus::InProgress - | ToolCallStatus::Completed, - .. - }) = thread - .entries() - .iter() - .find(|entry| matches!(entry, AgentThreadEntry::ToolCall(_))) - .unwrap() - else { - panic!(); - }; - - assert!( - content.iter().any(|c| c.to_markdown(cx).contains("Hello")), - "Expected content to contain 'Hello'" - ); - }); -} - -pub async fn test_cancel(server: F, cx: &mut TestAppContext) -where - T: AgentServer + 'static, - F: AsyncFn(&Arc, &Entity, &mut TestAppContext) -> T, -{ - let fs = init_test(cx).await as Arc; - - let project = Project::test(fs.clone(), [path!("/private/tmp").as_ref()], cx).await; - let thread = new_test_thread( - server(&fs, &project, cx).await, - project.clone(), - "/private/tmp", - cx, - ) - .await; - let _ = thread.update(cx, |thread, cx| { - thread.send_raw( - r#"Run exactly `touch hello.txt && echo "Hello, world!" | tee hello.txt` in the terminal."#, - cx, - ) - }); - - let first_tool_call_ix = run_until_first_tool_call( - &thread, - |entry| { - matches!( - entry, - AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::WaitingForConfirmation { .. }, - .. - }) - ) - }, - cx, - ) - .await; - - thread.read_with(cx, |thread, cx| { - let AgentThreadEntry::ToolCall(ToolCall { - id, - label, - status: ToolCallStatus::WaitingForConfirmation { .. }, - .. - }) = &thread.entries()[first_tool_call_ix] - else { - panic!("{:?}", thread.entries()[1]); - }; - - let label = label.read(cx).source(); - assert!(label.contains("touch"), "Got: {}", label); - - id.clone() - }); - - thread.update(cx, |thread, cx| thread.cancel(cx)).await; - thread.read_with(cx, |thread, _cx| { - let AgentThreadEntry::ToolCall(ToolCall { - status: ToolCallStatus::Canceled, - .. - }) = &thread.entries()[first_tool_call_ix] - else { - panic!(); - }; - }); - - thread - .update(cx, |thread, cx| { - thread.send_raw(r#"Stop running and say goodbye to me."#, cx) - }) - .await - .unwrap(); - thread.read_with(cx, |thread, _| { - assert!(matches!( - &thread.entries().last().unwrap(), - AgentThreadEntry::AssistantMessage(..), - )) - }); -} - -pub async fn test_thread_drop(server: F, cx: &mut TestAppContext) -where - T: AgentServer + 'static, - F: AsyncFn(&Arc, &Entity, &mut TestAppContext) -> T, -{ - let fs = init_test(cx).await as Arc; - let project = Project::test(fs.clone(), [], cx).await; - let thread = new_test_thread( - server(&fs, &project, cx).await, - project.clone(), - "/private/tmp", - cx, - ) - .await; - - thread - .update(cx, |thread, cx| thread.send_raw("Hello from test!", cx)) - .await - .unwrap(); - - thread.read_with(cx, |thread, _| { - assert!(thread.entries().len() >= 2, "Expected at least 2 entries"); - }); - - let weak_thread = thread.downgrade(); - drop(thread); - - cx.executor().run_until_parked(); - assert!(!weak_thread.is_upgradable()); -} - -#[macro_export] -macro_rules! common_e2e_tests { - ($server:expr, allow_option_id = $allow_option_id:expr) => { - mod common_e2e { - use super::*; - - #[::gpui::test] - #[cfg_attr(not(feature = "e2e"), ignore)] - async fn basic(cx: &mut ::gpui::TestAppContext) { - $crate::e2e_tests::test_basic($server, cx).await; - } - - #[::gpui::test] - #[cfg_attr(not(feature = "e2e"), ignore)] - async fn path_mentions(cx: &mut ::gpui::TestAppContext) { - $crate::e2e_tests::test_path_mentions($server, cx).await; - } - - #[::gpui::test] - #[cfg_attr(not(feature = "e2e"), ignore)] - async fn tool_call(cx: &mut ::gpui::TestAppContext) { - $crate::e2e_tests::test_tool_call($server, cx).await; - } - - #[::gpui::test] - #[cfg_attr(not(feature = "e2e"), ignore)] - async fn tool_call_with_permission(cx: &mut ::gpui::TestAppContext) { - $crate::e2e_tests::test_tool_call_with_permission( - $server, - ::agent_client_protocol::PermissionOptionId::new($allow_option_id), - cx, - ) - .await; - } - - #[::gpui::test] - #[cfg_attr(not(feature = "e2e"), ignore)] - async fn cancel(cx: &mut ::gpui::TestAppContext) { - $crate::e2e_tests::test_cancel($server, cx).await; - } - - #[::gpui::test] - #[cfg_attr(not(feature = "e2e"), ignore)] - async fn thread_drop(cx: &mut ::gpui::TestAppContext) { - $crate::e2e_tests::test_thread_drop($server, cx).await; - } - } - }; -} -pub use common_e2e_tests; - -// Helpers - -pub async fn init_test(cx: &mut TestAppContext) -> Arc { - env_logger::try_init().ok(); - - cx.update(|cx| { - let settings_store = settings::SettingsStore::test(cx); - cx.set_global(settings_store); - gpui_tokio::init(cx); - let http_client = reqwest_client::ReqwestClient::user_agent("agent tests").unwrap(); - cx.set_http_client(Arc::new(http_client)); - let client = client::Client::production(cx); - let user_store = cx.new(|cx| client::UserStore::new(client.clone(), cx)); - language_model::init(client.clone(), cx); - language_models::init(user_store, client, cx); - - #[cfg(test)] - project::agent_server_store::AllAgentServersSettings::override_global( - project::agent_server_store::AllAgentServersSettings { - claude: Some(BuiltinAgentServerSettings { - path: Some("claude-code-acp".into()), - args: None, - env: None, - ignore_system_version: None, - default_mode: None, - default_model: None, - }), - gemini: Some(crate::gemini::tests::local_command().into()), - codex: Some(BuiltinAgentServerSettings { - path: Some("codex-acp".into()), - args: None, - env: None, - ignore_system_version: None, - default_mode: None, - default_model: None, - }), - custom: collections::HashMap::default(), - }, - cx, - ); - }); - - cx.executor().allow_parking(); - - FakeFs::new(cx.executor()) -} - -pub async fn new_test_thread( - server: impl AgentServer + 'static, - project: Entity, - current_dir: impl AsRef, - cx: &mut TestAppContext, -) -> Entity { - let store = project.read_with(cx, |project, _| project.agent_server_store().clone()); - let delegate = AgentServerDelegate::new(store, project.clone(), None, None); - - let (connection, _) = cx - .update(|cx| server.connect(Some(current_dir.as_ref()), delegate, cx)) - .await - .unwrap(); - - cx.update(|cx| connection.new_thread(project.clone(), current_dir.as_ref(), cx)) - .await - .unwrap() -} - -pub async fn run_until_first_tool_call( - thread: &Entity, - wait_until: impl Fn(&AgentThreadEntry) -> bool + 'static, - cx: &mut TestAppContext, -) -> usize { - let (mut tx, mut rx) = mpsc::channel::(1); - - let subscription = cx.update(|cx| { - cx.subscribe(thread, move |thread, _, cx| { - for (ix, entry) in thread.read(cx).entries().iter().enumerate() { - if wait_until(entry) { - return tx.try_send(ix).unwrap(); - } - } - }) - }); - - select! { - // We have to use a smol timer here because - // cx.background_executor().timer isn't real in the test context - _ = futures::FutureExt::fuse(smol::Timer::after(Duration::from_secs(20))) => { - panic!("Timeout waiting for tool call") - } - ix = rx.next().fuse() => { - drop(subscription); - ix.unwrap() - } - } -} - -pub fn get_zed_path() -> PathBuf { - let mut zed_path = std::env::current_exe().unwrap(); - - while zed_path - .file_name() - .is_none_or(|name| name.to_string_lossy() != "debug") - { - if !zed_path.pop() { - panic!("Could not find target directory"); - } - } - - zed_path.push("zed"); - - if !zed_path.exists() { - panic!("\n🚨 Run `cargo build` at least once before running e2e tests\n\n"); - } - - zed_path -} diff --git a/crates/agent_servers/src/gemini.rs b/crates/agent_servers/src/gemini.rs deleted file mode 100644 index 5fea74746a..0000000000 --- a/crates/agent_servers/src/gemini.rs +++ /dev/null @@ -1,102 +0,0 @@ -use std::rc::Rc; -use std::{any::Any, path::Path}; - -use crate::{AgentServer, AgentServerDelegate, load_proxy_env}; -use acp_thread::AgentConnection; -use anyhow::{Context as _, Result}; -use gpui::{App, SharedString, Task}; -use language_models::provider::google::GoogleLanguageModelProvider; -use project::agent_server_store::GEMINI_NAME; - -#[derive(Clone)] -pub struct Gemini; - -impl AgentServer for Gemini { - fn name(&self) -> SharedString { - "Gemini CLI".into() - } - - fn logo(&self) -> ui::IconName { - ui::IconName::AiGemini - } - - fn connect( - &self, - root_dir: Option<&Path>, - delegate: AgentServerDelegate, - cx: &mut App, - ) -> Task, Option)>> { - let name = self.name(); - let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned()); - let is_remote = delegate.project.read(cx).is_via_remote_server(); - let store = delegate.store.downgrade(); - let mut extra_env = load_proxy_env(cx); - let default_mode = self.default_mode(cx); - let default_model = self.default_model(cx); - - cx.spawn(async move |cx| { - extra_env.insert("SURFACE".to_owned(), "zed".to_owned()); - - if let Some(api_key) = cx - .update(GoogleLanguageModelProvider::api_key_for_gemini_cli)? - .await - .ok() - { - extra_env.insert("GEMINI_API_KEY".into(), api_key); - } - let (command, root_dir, login) = store - .update(cx, |store, cx| { - let agent = store - .get_external_agent(&GEMINI_NAME.into()) - .context("Gemini CLI is not registered")?; - anyhow::Ok(agent.get_command( - root_dir.as_deref(), - extra_env, - delegate.status_tx, - delegate.new_version_available, - &mut cx.to_async(), - )) - })?? - .await?; - - let connection = crate::acp::connect( - name, - command, - root_dir.as_ref(), - default_mode, - default_model, - is_remote, - cx, - ) - .await?; - Ok((connection, login)) - }) - } - - fn into_any(self: Rc) -> Rc { - self - } -} - -#[cfg(test)] -pub(crate) mod tests { - use project::agent_server_store::AgentServerCommand; - - use super::*; - use std::path::Path; - - crate::common_e2e_tests!(async |_, _, _| Gemini, allow_option_id = "proceed_once"); - - pub fn local_command() -> AgentServerCommand { - let cli_path = Path::new(env!("CARGO_MANIFEST_DIR")) - .join("../../../gemini-cli/packages/cli") - .to_string_lossy() - .to_string(); - - AgentServerCommand { - path: "node".into(), - args: vec![cli_path], - env: None, - } - } -} diff --git a/crates/agent_settings/Cargo.toml b/crates/agent_settings/Cargo.toml deleted file mode 100644 index 8ddcac24fe..0000000000 --- a/crates/agent_settings/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "agent_settings" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/agent_settings.rs" - -[dependencies] -anyhow.workspace = true -cloud_llm_client.workspace = true -collections.workspace = true -convert_case.workspace = true -fs.workspace = true -gpui.workspace = true -language_model.workspace = true -project.workspace = true -schemars.workspace = true -serde.workspace = true -settings.workspace = true -util.workspace = true - -[dev-dependencies] -fs.workspace = true -gpui = { workspace = true, features = ["test-support"] } -paths.workspace = true -serde_json_lenient.workspace = true -serde_json.workspace = true -settings = { workspace = true, features = ["test-support"] } diff --git a/crates/agent_settings/LICENSE-GPL b/crates/agent_settings/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/agent_settings/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/agent_settings/src/agent_profile.rs b/crates/agent_settings/src/agent_profile.rs deleted file mode 100644 index aff666e011..0000000000 --- a/crates/agent_settings/src/agent_profile.rs +++ /dev/null @@ -1,202 +0,0 @@ -use std::sync::Arc; - -use anyhow::{Result, bail}; -use collections::IndexMap; -use convert_case::{Case, Casing as _}; -use fs::Fs; -use gpui::{App, SharedString}; -use settings::{ - AgentProfileContent, ContextServerPresetContent, LanguageModelSelection, Settings as _, - SettingsContent, update_settings_file, -}; -use util::ResultExt as _; - -use crate::{AgentProfileId, AgentSettings}; - -pub mod builtin_profiles { - use super::AgentProfileId; - - pub const WRITE: &str = "write"; - pub const ASK: &str = "ask"; - pub const MINIMAL: &str = "minimal"; - - pub fn is_builtin(profile_id: &AgentProfileId) -> bool { - profile_id.as_str() == WRITE || profile_id.as_str() == ASK || profile_id.as_str() == MINIMAL - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct AgentProfile { - id: AgentProfileId, -} - -pub type AvailableProfiles = IndexMap; - -impl AgentProfile { - pub fn new(id: AgentProfileId) -> Self { - Self { id } - } - - pub fn id(&self) -> &AgentProfileId { - &self.id - } - - /// Saves a new profile to the settings. - pub fn create( - name: String, - base_profile_id: Option, - fs: Arc, - cx: &App, - ) -> AgentProfileId { - let id = AgentProfileId(name.to_case(Case::Kebab).into()); - - let base_profile = - base_profile_id.and_then(|id| AgentSettings::get_global(cx).profiles.get(&id).cloned()); - - // Copy toggles from the base profile so the new profile starts with familiar defaults. - let tools = base_profile - .as_ref() - .map(|profile| profile.tools.clone()) - .unwrap_or_default(); - let enable_all_context_servers = base_profile - .as_ref() - .map(|profile| profile.enable_all_context_servers) - .unwrap_or_default(); - let context_servers = base_profile - .as_ref() - .map(|profile| profile.context_servers.clone()) - .unwrap_or_default(); - // Preserve the base profile's model preference when cloning into a new profile. - let default_model = base_profile - .as_ref() - .and_then(|profile| profile.default_model.clone()); - - let profile_settings = AgentProfileSettings { - name: name.into(), - tools, - enable_all_context_servers, - context_servers, - default_model, - }; - - update_settings_file(fs, cx, { - let id = id.clone(); - move |settings, _cx| { - profile_settings.save_to_settings(id, settings).log_err(); - } - }); - - id - } - - /// Returns a map of AgentProfileIds to their names - pub fn available_profiles(cx: &App) -> AvailableProfiles { - let mut profiles = AvailableProfiles::default(); - for (id, profile) in AgentSettings::get_global(cx).profiles.iter() { - profiles.insert(id.clone(), profile.name.clone()); - } - profiles - } -} - -/// A profile for the Zed Agent that controls its behavior. -#[derive(Debug, Clone)] -pub struct AgentProfileSettings { - /// The name of the profile. - pub name: SharedString, - pub tools: IndexMap, bool>, - pub enable_all_context_servers: bool, - pub context_servers: IndexMap, ContextServerPreset>, - /// Default language model to apply when this profile becomes active. - pub default_model: Option, -} - -impl AgentProfileSettings { - pub fn is_tool_enabled(&self, tool_name: &str) -> bool { - self.tools.get(tool_name) == Some(&true) - } - - pub fn is_context_server_tool_enabled(&self, server_id: &str, tool_name: &str) -> bool { - self.enable_all_context_servers - || self - .context_servers - .get(server_id) - .is_some_and(|preset| preset.tools.get(tool_name) == Some(&true)) - } - - pub fn save_to_settings( - &self, - profile_id: AgentProfileId, - content: &mut SettingsContent, - ) -> Result<()> { - let profiles = content - .agent - .get_or_insert_default() - .profiles - .get_or_insert_default(); - if profiles.contains_key(&profile_id.0) { - bail!("profile with ID '{profile_id}' already exists"); - } - - profiles.insert( - profile_id.0, - AgentProfileContent { - name: self.name.clone().into(), - tools: self.tools.clone(), - enable_all_context_servers: Some(self.enable_all_context_servers), - context_servers: self - .context_servers - .clone() - .into_iter() - .map(|(server_id, preset)| { - ( - server_id, - ContextServerPresetContent { - tools: preset.tools, - }, - ) - }) - .collect(), - default_model: self.default_model.clone(), - }, - ); - - Ok(()) - } -} - -impl From for AgentProfileSettings { - fn from(content: AgentProfileContent) -> Self { - let AgentProfileContent { - name, - tools, - enable_all_context_servers, - context_servers, - default_model, - } = content; - - Self { - name: name.into(), - tools, - enable_all_context_servers: enable_all_context_servers.unwrap_or_default(), - context_servers: context_servers - .into_iter() - .map(|(server_id, preset)| (server_id, preset.into())) - .collect(), - default_model, - } - } -} - -#[derive(Debug, Clone, Default)] -pub struct ContextServerPreset { - pub tools: IndexMap, bool>, -} - -impl From for ContextServerPreset { - fn from(content: settings::ContextServerPresetContent) -> Self { - Self { - tools: content.tools, - } - } -} diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs deleted file mode 100644 index 084ac7c3e7..0000000000 --- a/crates/agent_settings/src/agent_settings.rs +++ /dev/null @@ -1,182 +0,0 @@ -mod agent_profile; - -use std::sync::Arc; - -use collections::IndexMap; -use gpui::{App, Pixels, px}; -use language_model::LanguageModel; -use project::DisableAiSettings; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{ - DefaultAgentView, DockPosition, LanguageModelParameters, LanguageModelSelection, - NotifyWhenAgentWaiting, RegisterSetting, Settings, -}; - -pub use crate::agent_profile::*; - -pub const SUMMARIZE_THREAD_PROMPT: &str = include_str!("prompts/summarize_thread_prompt.txt"); -pub const SUMMARIZE_THREAD_DETAILED_PROMPT: &str = - include_str!("prompts/summarize_thread_detailed_prompt.txt"); - -#[derive(Clone, Debug, RegisterSetting)] -pub struct AgentSettings { - pub enabled: bool, - pub button: bool, - pub dock: DockPosition, - pub default_width: Pixels, - pub default_height: Pixels, - pub default_model: Option, - pub inline_assistant_model: Option, - pub commit_message_model: Option, - pub thread_summary_model: Option, - pub inline_alternatives: Vec, - pub default_profile: AgentProfileId, - pub default_view: DefaultAgentView, - pub profiles: IndexMap, - pub always_allow_tool_actions: bool, - pub notify_when_agent_waiting: NotifyWhenAgentWaiting, - pub play_sound_when_agent_done: bool, - pub single_file_review: bool, - pub model_parameters: Vec, - pub preferred_completion_mode: CompletionMode, - pub enable_feedback: bool, - pub expand_edit_card: bool, - pub expand_terminal_card: bool, - pub use_modifier_to_send: bool, - pub message_editor_min_lines: usize, -} - -impl AgentSettings { - pub fn enabled(&self, cx: &App) -> bool { - self.enabled && !DisableAiSettings::get_global(cx).disable_ai - } - - pub fn temperature_for_model(model: &Arc, cx: &App) -> Option { - let settings = Self::get_global(cx); - for setting in settings.model_parameters.iter().rev() { - if let Some(provider) = &setting.provider - && provider.0 != model.provider_id().0 - { - continue; - } - if let Some(setting_model) = &setting.model - && *setting_model != model.id().0 - { - continue; - } - return setting.temperature; - } - return None; - } - - pub fn set_inline_assistant_model(&mut self, provider: String, model: String) { - self.inline_assistant_model = Some(LanguageModelSelection { - provider: provider.into(), - model, - }); - } - - pub fn set_commit_message_model(&mut self, provider: String, model: String) { - self.commit_message_model = Some(LanguageModelSelection { - provider: provider.into(), - model, - }); - } - - pub fn set_thread_summary_model(&mut self, provider: String, model: String) { - self.thread_summary_model = Some(LanguageModelSelection { - provider: provider.into(), - model, - }); - } - - pub fn set_message_editor_max_lines(&self) -> usize { - self.message_editor_min_lines * 2 - } -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)] -#[serde(rename_all = "snake_case")] -pub enum CompletionMode { - #[default] - Normal, - #[serde(alias = "max")] - Burn, -} - -impl From for cloud_llm_client::CompletionMode { - fn from(value: CompletionMode) -> Self { - match value { - CompletionMode::Normal => cloud_llm_client::CompletionMode::Normal, - CompletionMode::Burn => cloud_llm_client::CompletionMode::Max, - } - } -} - -impl From for CompletionMode { - fn from(value: settings::CompletionMode) -> Self { - match value { - settings::CompletionMode::Normal => CompletionMode::Normal, - settings::CompletionMode::Burn => CompletionMode::Burn, - } - } -} - -#[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, JsonSchema)] -pub struct AgentProfileId(pub Arc); - -impl AgentProfileId { - pub fn as_str(&self) -> &str { - &self.0 - } -} - -impl std::fmt::Display for AgentProfileId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl Default for AgentProfileId { - fn default() -> Self { - Self("write".into()) - } -} - -impl Settings for AgentSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - let agent = content.agent.clone().unwrap(); - Self { - enabled: agent.enabled.unwrap(), - button: agent.button.unwrap(), - dock: agent.dock.unwrap(), - default_width: px(agent.default_width.unwrap()), - default_height: px(agent.default_height.unwrap()), - default_model: Some(agent.default_model.unwrap()), - inline_assistant_model: agent.inline_assistant_model, - commit_message_model: agent.commit_message_model, - thread_summary_model: agent.thread_summary_model, - inline_alternatives: agent.inline_alternatives.unwrap_or_default(), - default_profile: AgentProfileId(agent.default_profile.unwrap()), - default_view: agent.default_view.unwrap(), - profiles: agent - .profiles - .unwrap() - .into_iter() - .map(|(key, val)| (AgentProfileId(key), val.into())) - .collect(), - always_allow_tool_actions: agent.always_allow_tool_actions.unwrap(), - notify_when_agent_waiting: agent.notify_when_agent_waiting.unwrap(), - play_sound_when_agent_done: agent.play_sound_when_agent_done.unwrap(), - single_file_review: agent.single_file_review.unwrap(), - model_parameters: agent.model_parameters, - preferred_completion_mode: agent.preferred_completion_mode.unwrap().into(), - enable_feedback: agent.enable_feedback.unwrap(), - expand_edit_card: agent.expand_edit_card.unwrap(), - expand_terminal_card: agent.expand_terminal_card.unwrap(), - use_modifier_to_send: agent.use_modifier_to_send.unwrap(), - message_editor_min_lines: agent.message_editor_min_lines.unwrap(), - } - } -} diff --git a/crates/agent_settings/src/prompts/summarize_thread_detailed_prompt.txt b/crates/agent_settings/src/prompts/summarize_thread_detailed_prompt.txt deleted file mode 100644 index 30fab472af..0000000000 --- a/crates/agent_settings/src/prompts/summarize_thread_detailed_prompt.txt +++ /dev/null @@ -1,6 +0,0 @@ -Generate a detailed summary of this conversation. Include: -1. A brief overview of what was discussed -2. Key facts or information discovered -3. Outcomes or conclusions reached -4. Any action items or next steps if any -Format it in Markdown with headings and bullet points. diff --git a/crates/agent_settings/src/prompts/summarize_thread_prompt.txt b/crates/agent_settings/src/prompts/summarize_thread_prompt.txt deleted file mode 100644 index f57644433b..0000000000 --- a/crates/agent_settings/src/prompts/summarize_thread_prompt.txt +++ /dev/null @@ -1,4 +0,0 @@ -Generate a concise 3-7 word title for this conversation, omitting punctuation. -Go straight to the title, without any preamble and prefix like `Here's a concise suggestion:...` or `Title:`. -If the conversation is about a specific subject, include it in the title. -Be descriptive. DO NOT speak in the first person. diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml deleted file mode 100644 index 2af0ce6fbd..0000000000 --- a/crates/agent_ui/Cargo.toml +++ /dev/null @@ -1,126 +0,0 @@ -[package] -name = "agent_ui" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/agent_ui.rs" -doctest = false - -[features] -test-support = ["gpui/test-support", "language/test-support", "reqwest_client"] -unit-eval = [] - -[dependencies] -acp_thread.workspace = true -action_log.workspace = true -agent-client-protocol.workspace = true -agent.workspace = true -agent_servers.workspace = true -agent_settings.workspace = true -ai_onboarding.workspace = true -anyhow.workspace = true -arrayvec.workspace = true -assistant_text_thread.workspace = true -assistant_slash_command.workspace = true -assistant_slash_commands.workspace = true -audio.workspace = true -buffer_diff.workspace = true -chrono.workspace = true -client.workspace = true -cloud_llm_client.workspace = true -collections.workspace = true -command_palette_hooks.workspace = true -component.workspace = true -context_server.workspace = true -db.workspace = true -editor.workspace = true -extension.workspace = true -extension_host.workspace = true -feature_flags.workspace = true -file_icons.workspace = true -fs.workspace = true -futures.workspace = true -fuzzy.workspace = true -gpui.workspace = true -gpui_tokio.workspace = true -html_to_markdown.workspace = true -http_client.workspace = true -indoc.workspace = true -itertools.workspace = true -jsonschema.workspace = true -language.workspace = true -language_model.workspace = true -language_models.workspace = true -log.workspace = true -lsp.workspace = true -markdown.workspace = true -menu.workspace = true -multi_buffer.workspace = true -notifications.workspace = true -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 -release_channel.workspace = true -rope.workspace = true -rules_library.workspace = true -schemars.workspace = true -search.workspace = true -serde.workspace = true -serde_json.workspace = true -serde_json_lenient.workspace = true -settings.workspace = true -smol.workspace = true -streaming_diff.workspace = true -task.workspace = true -telemetry.workspace = true -telemetry_events.workspace = true -terminal.workspace = true -terminal_view.workspace = true -text.workspace = true -theme.workspace = true -time.workspace = true -time_format.workspace = true -ui.workspace = true -ui_input.workspace = true -url.workspace = true -util.workspace = true -uuid.workspace = true -watch.workspace = true -workspace.workspace = true -zed_actions.workspace = true -image.workspace = true -async-fs.workspace = true -reqwest_client = { workspace = true, optional = true } - -[dev-dependencies] -acp_thread = { workspace = true, features = ["test-support"] } -agent = { workspace = true, features = ["test-support"] } -assistant_text_thread = { workspace = true, features = ["test-support"] } -buffer_diff = { workspace = true, features = ["test-support"] } -clock.workspace = true -db = { workspace = true, features = ["test-support"] } -editor = { workspace = true, features = ["test-support"] } -eval_utils.workspace = true -gpui = { workspace = true, "features" = ["test-support"] } -indoc.workspace = true -language = { workspace = true, "features" = ["test-support"] } -languages = { workspace = true, features = ["test-support"] } -language_model = { workspace = true, "features" = ["test-support"] } -pretty_assertions.workspace = true -project = { workspace = true, features = ["test-support"] } -semver.workspace = true -rand.workspace = true -reqwest_client.workspace = true -tree-sitter-md.workspace = true -unindent.workspace = true diff --git a/crates/agent_ui/LICENSE-GPL b/crates/agent_ui/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/agent_ui/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/agent_ui/src/acp.rs b/crates/agent_ui/src/acp.rs deleted file mode 100644 index 7a740c2dc4..0000000000 --- a/crates/agent_ui/src/acp.rs +++ /dev/null @@ -1,13 +0,0 @@ -mod entry_view_state; -mod message_editor; -mod mode_selector; -mod model_selector; -mod model_selector_popover; -mod thread_history; -mod thread_view; - -pub use mode_selector::ModeSelector; -pub use model_selector::AcpModelSelector; -pub use model_selector_popover::AcpModelSelectorPopover; -pub use thread_history::*; -pub use thread_view::AcpThreadView; diff --git a/crates/agent_ui/src/acp/entry_view_state.rs b/crates/agent_ui/src/acp/entry_view_state.rs deleted file mode 100644 index feae74a86b..0000000000 --- a/crates/agent_ui/src/acp/entry_view_state.rs +++ /dev/null @@ -1,532 +0,0 @@ -use std::{cell::RefCell, ops::Range, rc::Rc}; - -use acp_thread::{AcpThread, AgentThreadEntry}; -use agent::HistoryStore; -use agent_client_protocol::{self as acp, ToolCallId}; -use collections::HashMap; -use editor::{Editor, EditorMode, MinimapVisibility, SizingBehavior}; -use gpui::{ - AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, FocusHandle, Focusable, - ScrollHandle, SharedString, TextStyleRefinement, WeakEntity, Window, -}; -use language::language_settings::SoftWrap; -use project::Project; -use prompt_store::PromptStore; -use settings::Settings as _; -use terminal_view::TerminalView; -use theme::ThemeSettings; -use ui::{Context, TextSize}; -use workspace::Workspace; - -use crate::acp::message_editor::{MessageEditor, MessageEditorEvent}; - -pub struct EntryViewState { - workspace: WeakEntity, - project: WeakEntity, - history_store: Entity, - prompt_store: Option>, - entries: Vec, - prompt_capabilities: Rc>, - available_commands: Rc>>, - agent_name: SharedString, -} - -impl EntryViewState { - pub fn new( - workspace: WeakEntity, - project: WeakEntity, - history_store: Entity, - prompt_store: Option>, - prompt_capabilities: Rc>, - available_commands: Rc>>, - agent_name: SharedString, - ) -> Self { - Self { - workspace, - project, - history_store, - prompt_store, - entries: Vec::new(), - prompt_capabilities, - available_commands, - agent_name, - } - } - - pub fn entry(&self, index: usize) -> Option<&Entry> { - self.entries.get(index) - } - - pub fn sync_entry( - &mut self, - index: usize, - thread: &Entity, - window: &mut Window, - cx: &mut Context, - ) { - let Some(thread_entry) = thread.read(cx).entries().get(index) else { - return; - }; - - match thread_entry { - AgentThreadEntry::UserMessage(message) => { - let has_id = message.id.is_some(); - let chunks = message.chunks.clone(); - if let Some(Entry::UserMessage(editor)) = self.entries.get_mut(index) { - if !editor.focus_handle(cx).is_focused(window) { - // Only update if we are not editing. - // If we are, cancelling the edit will set the message to the newest content. - editor.update(cx, |editor, cx| { - editor.set_message(chunks, window, cx); - }); - } - } else { - let message_editor = cx.new(|cx| { - let mut editor = MessageEditor::new( - self.workspace.clone(), - self.project.clone(), - self.history_store.clone(), - self.prompt_store.clone(), - self.prompt_capabilities.clone(), - self.available_commands.clone(), - self.agent_name.clone(), - "Edit message - @ to include context", - editor::EditorMode::AutoHeight { - min_lines: 1, - max_lines: None, - }, - window, - cx, - ); - if !has_id { - editor.set_read_only(true, cx); - } - editor.set_message(chunks, window, cx); - editor - }); - cx.subscribe(&message_editor, move |_, editor, event, cx| { - cx.emit(EntryViewEvent { - entry_index: index, - view_event: ViewEvent::MessageEditorEvent(editor, *event), - }) - }) - .detach(); - self.set_entry(index, Entry::UserMessage(message_editor)); - } - } - AgentThreadEntry::ToolCall(tool_call) => { - let id = tool_call.id.clone(); - let terminals = tool_call.terminals().cloned().collect::>(); - let diffs = tool_call.diffs().cloned().collect::>(); - - let views = if let Some(Entry::Content(views)) = self.entries.get_mut(index) { - views - } else { - self.set_entry(index, Entry::empty()); - let Some(Entry::Content(views)) = self.entries.get_mut(index) else { - unreachable!() - }; - views - }; - - let is_tool_call_completed = - matches!(tool_call.status, acp_thread::ToolCallStatus::Completed); - - for terminal in terminals { - match views.entry(terminal.entity_id()) { - collections::hash_map::Entry::Vacant(entry) => { - let element = create_terminal( - self.workspace.clone(), - self.project.clone(), - terminal.clone(), - window, - cx, - ) - .into_any(); - cx.emit(EntryViewEvent { - entry_index: index, - view_event: ViewEvent::NewTerminal(id.clone()), - }); - entry.insert(element); - } - collections::hash_map::Entry::Occupied(_entry) => { - if is_tool_call_completed && terminal.read(cx).output().is_none() { - cx.emit(EntryViewEvent { - entry_index: index, - view_event: ViewEvent::TerminalMovedToBackground(id.clone()), - }); - } - } - } - } - - for diff in diffs { - views.entry(diff.entity_id()).or_insert_with(|| { - let element = create_editor_diff(diff.clone(), window, cx).into_any(); - cx.emit(EntryViewEvent { - entry_index: index, - view_event: ViewEvent::NewDiff(id.clone()), - }); - element - }); - } - } - AgentThreadEntry::AssistantMessage(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); - } - }; - } - - fn set_entry(&mut self, index: usize, entry: Entry) { - if index == self.entries.len() { - self.entries.push(entry); - } else { - self.entries[index] = entry; - } - } - - pub fn remove(&mut self, range: Range) { - self.entries.drain(range); - } - - pub fn agent_ui_font_size_changed(&mut self, cx: &mut App) { - for entry in self.entries.iter() { - match entry { - Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {} - Entry::Content(response_views) => { - for view in response_views.values() { - if let Ok(diff_editor) = view.clone().downcast::() { - diff_editor.update(cx, |diff_editor, cx| { - diff_editor.set_text_style_refinement( - diff_editor_text_style_refinement(cx), - ); - cx.notify(); - }) - } - } - } - } - } - } -} - -impl EventEmitter for EntryViewState {} - -pub struct EntryViewEvent { - pub entry_index: usize, - pub view_event: ViewEvent, -} - -pub enum ViewEvent { - NewDiff(ToolCallId), - NewTerminal(ToolCallId), - TerminalMovedToBackground(ToolCallId), - 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), -} - -impl Entry { - pub fn focus_handle(&self, cx: &App) -> Option { - match self { - Self::UserMessage(editor) => Some(editor.read(cx).focus_handle(cx)), - Self::AssistantMessage(_) | Self::Content(_) => None, - } - } - - pub fn message_editor(&self) -> Option<&Entity> { - match self { - Self::UserMessage(editor) => Some(editor), - Self::AssistantMessage(_) | Self::Content(_) => None, - } - } - - pub fn editor_for_diff(&self, diff: &Entity) -> Option> { - self.content_map()? - .get(&diff.entity_id()) - .cloned() - .map(|entity| entity.downcast::().unwrap()) - } - - pub fn terminal( - &self, - terminal: &Entity, - ) -> Option> { - self.content_map()? - .get(&terminal.entity_id()) - .cloned() - .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), - _ => None, - } - } - - fn empty() -> Self { - Self::Content(HashMap::default()) - } - - #[cfg(test)] - pub fn has_content(&self) -> bool { - match self { - Self::Content(map) => !map.is_empty(), - Self::UserMessage(_) | Self::AssistantMessage(_) => false, - } - } -} - -fn create_terminal( - workspace: WeakEntity, - project: WeakEntity, - terminal: Entity, - window: &mut Window, - cx: &mut App, -) -> Entity { - cx.new(|cx| { - let mut view = TerminalView::new( - terminal.read(cx).inner().clone(), - workspace, - None, - project, - window, - cx, - ); - view.set_embedded_mode(Some(1000), cx); - view - }) -} - -fn create_editor_diff( - diff: Entity, - window: &mut Window, - cx: &mut App, -) -> Entity { - cx.new(|cx| { - let mut editor = Editor::new( - EditorMode::Full { - scale_ui_elements_with_buffer_font_size: false, - show_active_line_background: false, - sizing_behavior: SizingBehavior::SizeByContent, - }, - diff.read(cx).multibuffer().clone(), - None, - window, - cx, - ); - editor.set_show_gutter(false, cx); - editor.disable_inline_diagnostics(); - editor.disable_expand_excerpt_buttons(cx); - editor.set_show_vertical_scrollbar(false, cx); - editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx); - editor.set_soft_wrap_mode(SoftWrap::None, cx); - editor.scroll_manager.set_forbid_vertical_scroll(true); - editor.set_show_indent_guides(false, cx); - editor.set_read_only(true); - editor.set_show_breakpoints(false, cx); - editor.set_show_code_actions(false, cx); - editor.set_show_git_diff_gutter(false, cx); - editor.set_expand_all_diff_hunks(cx); - editor.set_text_style_refinement(diff_editor_text_style_refinement(cx)); - editor - }) -} - -fn diff_editor_text_style_refinement(cx: &mut App) -> TextStyleRefinement { - TextStyleRefinement { - font_size: Some( - TextSize::Small - .rems(cx) - .to_pixels(ThemeSettings::get_global(cx).agent_ui_font_size(cx)) - .into(), - ), - ..Default::default() - } -} - -#[cfg(test)] -mod tests { - use std::{path::Path, rc::Rc}; - - use acp_thread::{AgentConnection, StubAgentConnection}; - use agent::HistoryStore; - use agent_client_protocol as acp; - use assistant_text_thread::TextThreadStore; - use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind}; - use editor::RowInfo; - use fs::FakeFs; - use gpui::{AppContext as _, TestAppContext}; - - use crate::acp::entry_view_state::EntryViewState; - use multi_buffer::MultiBufferRow; - use pretty_assertions::assert_matches; - use project::Project; - use serde_json::json; - use settings::SettingsStore; - use util::path; - use workspace::Workspace; - - #[gpui::test] - async fn test_diff_sync(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/project", - json!({ - "hello.txt": "hi world" - }), - ) - .await; - let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; - - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let tool_call = acp::ToolCall::new("tool", "Tool call") - .status(acp::ToolCallStatus::InProgress) - .content(vec![acp::ToolCallContent::Diff( - acp::Diff::new("/project/hello.txt", "hello world").old_text("hi world"), - )]); - let connection = Rc::new(StubAgentConnection::new()); - let thread = cx - .update(|_, cx| { - connection - .clone() - .new_thread(project.clone(), Path::new(path!("/project")), cx) - }) - .await - .unwrap(); - let session_id = thread.update(cx, |thread, _| thread.session_id().clone()); - - cx.update(|_, cx| { - connection.send_update(session_id, acp::SessionUpdate::ToolCall(tool_call), cx) - }); - - let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); - - let view_state = cx.new(|_cx| { - EntryViewState::new( - workspace.downgrade(), - project.downgrade(), - history_store, - None, - Default::default(), - Default::default(), - "Test Agent".into(), - ) - }); - - view_state.update_in(cx, |view_state, window, cx| { - view_state.sync_entry(0, &thread, window, cx) - }); - - let diff = thread.read_with(cx, |thread, _cx| { - thread - .entries() - .get(0) - .unwrap() - .diffs() - .next() - .unwrap() - .clone() - }); - - cx.run_until_parked(); - - let diff_editor = view_state.read_with(cx, |view_state, _cx| { - view_state.entry(0).unwrap().editor_for_diff(&diff).unwrap() - }); - assert_eq!( - diff_editor.read_with(cx, |editor, cx| editor.text(cx)), - "hi world\nhello world" - ); - let row_infos = diff_editor.read_with(cx, |editor, cx| { - let multibuffer = editor.buffer().read(cx); - multibuffer - .snapshot(cx) - .row_infos(MultiBufferRow(0)) - .collect::>() - }); - assert_matches!( - row_infos.as_slice(), - [ - RowInfo { - multibuffer_row: Some(MultiBufferRow(0)), - diff_status: Some(DiffHunkStatus { - kind: DiffHunkStatusKind::Deleted, - .. - }), - .. - }, - RowInfo { - multibuffer_row: Some(MultiBufferRow(1)), - diff_status: Some(DiffHunkStatus { - kind: DiffHunkStatusKind::Added, - .. - }), - .. - } - ] - ); - } - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - theme::init(theme::LoadThemes::JustBase, cx); - release_channel::init(semver::Version::new(0, 0, 0), cx); - }); - } -} diff --git a/crates/agent_ui/src/acp/message_editor.rs b/crates/agent_ui/src/acp/message_editor.rs deleted file mode 100644 index 5e9c55cc56..0000000000 --- a/crates/agent_ui/src/acp/message_editor.rs +++ /dev/null @@ -1,2532 +0,0 @@ -use crate::{ - ChatWithFollow, - completion_provider::{ - PromptCompletionProvider, PromptCompletionProviderDelegate, PromptContextAction, - PromptContextType, SlashCommandCompletion, - }, - mention_set::{ - Mention, MentionImage, MentionSet, insert_crease_for_mention, paste_images_as_context, - }, -}; -use acp_thread::MentionUri; -use agent::HistoryStore; -use agent_client_protocol as acp; -use anyhow::{Result, anyhow}; -use collections::HashSet; -use editor::{ - Addon, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, - EditorEvent, EditorMode, EditorStyle, Inlay, MultiBuffer, MultiBufferOffset, - MultiBufferSnapshot, ToOffset, actions::Paste, code_context_menus::CodeContextMenu, - scroll::Autoscroll, -}; -use futures::{FutureExt as _, future::join_all}; -use gpui::{ - AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, ImageFormat, - KeyContext, SharedString, Subscription, Task, TextStyle, WeakEntity, -}; -use language::{Buffer, Language, language_settings::InlayHintKind}; -use project::{CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, Worktree}; -use prompt_store::PromptStore; -use rope::Point; -use settings::Settings; -use std::{cell::RefCell, fmt::Write, rc::Rc, sync::Arc}; -use theme::ThemeSettings; -use ui::prelude::*; -use util::{ResultExt, debug_panic}; -use workspace::{CollaboratorId, Workspace}; -use zed_actions::agent::Chat; - -pub struct MessageEditor { - mention_set: Entity, - editor: Entity, - workspace: WeakEntity, - prompt_capabilities: Rc>, - available_commands: Rc>>, - agent_name: SharedString, - _subscriptions: Vec, - _parse_slash_command_task: Task<()>, -} - -#[derive(Clone, Copy, Debug)] -pub enum MessageEditorEvent { - Send, - Cancel, - Focus, - LostFocus, -} - -impl EventEmitter for MessageEditor {} - -const COMMAND_HINT_INLAY_ID: InlayId = InlayId::Hint(0); - -impl PromptCompletionProviderDelegate for Entity { - fn supports_images(&self, cx: &App) -> bool { - self.read(cx).prompt_capabilities.borrow().image - } - - fn supported_modes(&self, cx: &App) -> Vec { - let mut supported = vec![PromptContextType::File, PromptContextType::Symbol]; - if self.read(cx).prompt_capabilities.borrow().embedded_context { - supported.extend(&[ - PromptContextType::Thread, - PromptContextType::Fetch, - PromptContextType::Rules, - ]); - } - supported - } - - fn available_commands(&self, cx: &App) -> Vec { - self.read(cx) - .available_commands - .borrow() - .iter() - .map(|cmd| crate::completion_provider::AvailableCommand { - name: cmd.name.clone().into(), - description: cmd.description.clone().into(), - requires_argument: cmd.input.is_some(), - }) - .collect() - } - - fn confirm_command(&self, cx: &mut App) { - self.update(cx, |this, cx| this.send(cx)); - } -} - -impl MessageEditor { - pub fn new( - workspace: WeakEntity, - project: WeakEntity, - history_store: Entity, - prompt_store: Option>, - prompt_capabilities: Rc>, - available_commands: Rc>>, - agent_name: SharedString, - placeholder: &str, - mode: EditorMode, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let language = Language::new( - language::LanguageConfig { - completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']), - ..Default::default() - }, - None, - ); - - let editor = cx.new(|cx| { - let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx)); - let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); - - let mut editor = Editor::new(mode, buffer, None, window, cx); - editor.set_placeholder_text(placeholder, window, cx); - editor.set_show_indent_guides(false, cx); - editor.set_show_completions_on_input(Some(true)); - editor.set_soft_wrap(); - editor.set_use_modal_editing(true); - editor.set_context_menu_options(ContextMenuOptions { - min_entries_visible: 12, - max_entries_visible: 12, - placement: Some(ContextMenuPlacement::Above), - }); - editor.register_addon(MessageEditorAddon::new()); - editor - }); - let mention_set = - cx.new(|_cx| MentionSet::new(project, history_store.clone(), prompt_store.clone())); - let completion_provider = Rc::new(PromptCompletionProvider::new( - cx.entity(), - editor.downgrade(), - mention_set.clone(), - history_store.clone(), - prompt_store.clone(), - workspace.clone(), - )); - editor.update(cx, |editor, _cx| { - editor.set_completion_provider(Some(completion_provider.clone())) - }); - - cx.on_focus_in(&editor.focus_handle(cx), window, |_, _, cx| { - cx.emit(MessageEditorEvent::Focus) - }) - .detach(); - cx.on_focus_out(&editor.focus_handle(cx), window, |_, _, _, cx| { - cx.emit(MessageEditorEvent::LostFocus) - }) - .detach(); - - let mut has_hint = false; - let mut subscriptions = Vec::new(); - - subscriptions.push(cx.subscribe_in(&editor, window, { - move |this, editor, event, window, cx| { - if let EditorEvent::Edited { .. } = event - && !editor.read(cx).read_only(cx) - { - editor.update(cx, |editor, cx| { - let snapshot = editor.snapshot(window, cx); - this.mention_set - .update(cx, |mention_set, _cx| mention_set.remove_invalid(&snapshot)); - - let new_hints = this - .command_hint(snapshot.buffer()) - .into_iter() - .collect::>(); - let has_new_hint = !new_hints.is_empty(); - editor.splice_inlays( - if has_hint { - &[COMMAND_HINT_INLAY_ID] - } else { - &[] - }, - new_hints, - cx, - ); - has_hint = has_new_hint; - }); - cx.notify(); - } - } - })); - - Self { - editor, - mention_set, - workspace, - prompt_capabilities, - available_commands, - agent_name, - _subscriptions: subscriptions, - _parse_slash_command_task: Task::ready(()), - } - } - - fn command_hint(&self, snapshot: &MultiBufferSnapshot) -> Option { - let available_commands = self.available_commands.borrow(); - if available_commands.is_empty() { - return None; - } - - let parsed_command = SlashCommandCompletion::try_parse(&snapshot.text(), 0)?; - if parsed_command.argument.is_some() { - return None; - } - - let command_name = parsed_command.command?; - let available_command = available_commands - .iter() - .find(|command| command.name == command_name)?; - - let acp::AvailableCommandInput::Unstructured(acp::UnstructuredCommandInput { - mut hint, - .. - }) = available_command.input.clone()? - else { - return None; - }; - - let mut hint_pos = MultiBufferOffset(parsed_command.source_range.end) + 1usize; - if hint_pos > snapshot.len() { - hint_pos = snapshot.len(); - hint.insert(0, ' '); - } - - let hint_pos = snapshot.anchor_after(hint_pos); - - Some(Inlay::hint( - COMMAND_HINT_INLAY_ID, - hint_pos, - &InlayHint { - position: hint_pos.text_anchor, - label: InlayHintLabel::String(hint), - kind: Some(InlayHintKind::Parameter), - padding_left: false, - padding_right: false, - tooltip: None, - resolve_state: project::ResolveState::Resolved, - }, - )) - } - - pub fn insert_thread_summary( - &mut self, - thread: agent::DbThreadMetadata, - window: &mut Window, - cx: &mut Context, - ) { - let Some(workspace) = self.workspace.upgrade() else { - return; - }; - let uri = MentionUri::Thread { - id: thread.id.clone(), - name: thread.title.to_string(), - }; - let content = format!("{}\n", uri.as_link()); - - let content_len = content.len() - 1; - - let start = self.editor.update(cx, |editor, cx| { - editor.set_text(content, window, cx); - editor - .buffer() - .read(cx) - .snapshot(cx) - .anchor_before(Point::zero()) - .text_anchor - }); - - let supports_images = self.prompt_capabilities.borrow().image; - - self.mention_set - .update(cx, |mention_set, cx| { - mention_set.confirm_mention_completion( - thread.title, - start, - content_len, - uri, - supports_images, - self.editor.clone(), - &workspace, - window, - cx, - ) - }) - .detach(); - } - - #[cfg(test)] - pub(crate) fn editor(&self) -> &Entity { - &self.editor - } - - pub fn is_empty(&self, cx: &App) -> bool { - self.editor.read(cx).is_empty(cx) - } - - pub fn is_completions_menu_visible(&self, cx: &App) -> bool { - self.editor - .read(cx) - .context_menu() - .borrow() - .as_ref() - .is_some_and(|menu| matches!(menu, CodeContextMenu::Completions(_)) && menu.visible()) - } - - #[cfg(test)] - pub fn mention_set(&self) -> &Entity { - &self.mention_set - } - - fn validate_slash_commands( - text: &str, - available_commands: &[acp::AvailableCommand], - agent_name: &str, - ) -> Result<()> { - if let Some(parsed_command) = SlashCommandCompletion::try_parse(text, 0) { - if let Some(command_name) = parsed_command.command { - // Check if this command is in the list of available commands from the server - let is_supported = available_commands - .iter() - .any(|cmd| cmd.name == command_name); - - if !is_supported { - return Err(anyhow!( - "The /{} command is not supported by {}.\n\nAvailable commands: {}", - command_name, - agent_name, - if available_commands.is_empty() { - "none".to_string() - } else { - available_commands - .iter() - .map(|cmd| format!("/{}", cmd.name)) - .collect::>() - .join(", ") - } - )); - } - } - } - Ok(()) - } - - pub fn contents( - &self, - full_mention_content: bool, - cx: &mut Context, - ) -> Task, Vec>)>> { - // Check for unsupported slash commands before spawning async task - let text = self.editor.read(cx).text(cx); - let available_commands = self.available_commands.borrow().clone(); - if let Err(err) = - Self::validate_slash_commands(&text, &available_commands, &self.agent_name) - { - return Task::ready(Err(err)); - } - - let contents = self - .mention_set - .update(cx, |store, cx| store.contents(full_mention_content, cx)); - let editor = self.editor.clone(); - let supports_embedded_context = self.prompt_capabilities.borrow().embedded_context; - - cx.spawn(async move |_, cx| { - let contents = contents.await?; - let mut all_tracked_buffers = Vec::new(); - - let result = editor.update(cx, |editor, cx| { - let (mut ix, _) = text - .char_indices() - .find(|(_, c)| !c.is_whitespace()) - .unwrap_or((0, '\0')); - let mut chunks: Vec = Vec::new(); - let text = editor.text(cx); - 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 { - continue; - }; - - let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot()); - if crease_range.start.0 > ix { - let chunk = text[ix..crease_range.start.0].into(); - chunks.push(chunk); - } - let chunk = match mention { - Mention::Text { - content, - tracked_buffers, - } => { - all_tracked_buffers.extend(tracked_buffers.iter().cloned()); - if supports_embedded_context { - acp::ContentBlock::Resource(acp::EmbeddedResource::new( - acp::EmbeddedResourceResource::TextResourceContents( - acp::TextResourceContents::new( - content.clone(), - uri.to_uri().to_string(), - ), - ), - )) - } else { - acp::ContentBlock::ResourceLink(acp::ResourceLink::new( - uri.name(), - uri.to_uri().to_string(), - )) - } - } - Mention::Image(mention_image) => acp::ContentBlock::Image( - acp::ImageContent::new( - mention_image.data.clone(), - mention_image.format.mime_type(), - ) - .uri(match uri { - MentionUri::File { .. } => Some(uri.to_uri().to_string()), - MentionUri::PastedImage => None, - other => { - debug_panic!( - "unexpected mention uri for image: {:?}", - other - ); - None - } - }), - ), - Mention::Link => acp::ContentBlock::ResourceLink( - acp::ResourceLink::new(uri.name(), uri.to_uri().to_string()), - ), - }; - chunks.push(chunk); - ix = crease_range.end.0; - } - - if ix < text.len() { - let last_chunk = text[ix..].trim_end().to_owned(); - if !last_chunk.is_empty() { - chunks.push(last_chunk.into()); - } - } - }); - Ok((chunks, all_tracked_buffers)) - })?; - result - }) - } - - 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.update(cx, |mention_set, _cx| { - mention_set - .clear() - .map(|(crease_id, _)| crease_id) - .collect::>() - }), - cx, - ) - }); - } - - pub fn send(&mut self, cx: &mut Context) { - if self.is_empty(cx) { - return; - } - self.editor.update(cx, |editor, cx| { - editor.clear_inlay_hints(cx); - }); - cx.emit(MessageEditorEvent::Send) - } - - pub fn trigger_completion_menu(&mut self, window: &mut Window, cx: &mut Context) { - let editor = self.editor.clone(); - - cx.spawn_in(window, async move |_, cx| { - editor - .update_in(cx, |editor, window, cx| { - let menu_is_open = - editor.context_menu().borrow().as_ref().is_some_and(|menu| { - matches!(menu, CodeContextMenu::Completions(_)) && menu.visible() - }); - - let has_at_sign = { - let snapshot = editor.display_snapshot(cx); - let cursor = editor.selections.newest::(&snapshot).head(); - let offset = cursor.to_offset(&snapshot); - if offset.0 > 0 { - snapshot - .buffer_snapshot() - .reversed_chars_at(offset) - .next() - .map(|sign| sign == '@') - .unwrap_or(false) - } else { - false - } - }; - - if menu_is_open && has_at_sign { - return; - } - - editor.insert("@", window, cx); - editor.show_completions(&editor::actions::ShowCompletions, window, cx); - }) - .log_err(); - }) - .detach(); - } - - fn chat(&mut self, _: &Chat, _: &mut Window, cx: &mut Context) { - self.send(cx); - } - - fn chat_with_follow( - &mut self, - _: &ChatWithFollow, - window: &mut Window, - cx: &mut Context, - ) { - self.workspace - .update(cx, |this, cx| { - this.follow(CollaboratorId::Agent, window, cx) - }) - .log_err(); - - self.send(cx); - } - - fn cancel(&mut self, _: &editor::actions::Cancel, _: &mut Window, cx: &mut Context) { - cx.emit(MessageEditorEvent::Cancel) - } - - fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context) { - let editor_clipboard_selections = cx - .read_from_clipboard() - .and_then(|item| item.entries().first().cloned()) - .and_then(|entry| match entry { - ClipboardEntry::String(text) => { - text.metadata_json::>() - } - _ => None, - }); - - let has_file_context = editor_clipboard_selections - .as_ref() - .is_some_and(|selections| { - selections - .iter() - .any(|sel| sel.file_path.is_some() && sel.line_range.is_some()) - }); - - if has_file_context { - if let Some((workspace, selections)) = - self.workspace.upgrade().zip(editor_clipboard_selections) - { - let Some(first_selection) = selections.first() else { - return; - }; - if let Some(file_path) = &first_selection.file_path { - // In case someone pastes selections from another window - // with a different project, we don't want to insert the - // crease (containing the absolute path) since the agent - // cannot access files outside the project. - let is_in_project = workspace - .read(cx) - .project() - .read(cx) - .project_path_for_absolute_path(file_path, cx) - .is_some(); - if !is_in_project { - return; - } - } - - cx.stop_propagation(); - let insertion_target = self - .editor - .read(cx) - .selections - .newest_anchor() - .start - .text_anchor; - - let project = workspace.read(cx).project().clone(); - for selection in selections { - if let (Some(file_path), Some(line_range)) = - (selection.file_path, selection.line_range) - { - let crease_text = - acp_thread::selection_name(Some(file_path.as_ref()), &line_range); - - let mention_uri = MentionUri::Selection { - abs_path: Some(file_path.clone()), - line_range: line_range.clone(), - }; - - let mention_text = mention_uri.as_link().to_string(); - let (excerpt_id, text_anchor, content_len) = - self.editor.update(cx, |editor, cx| { - let buffer = editor.buffer().read(cx); - let snapshot = buffer.snapshot(cx); - let (excerpt_id, _, buffer_snapshot) = - snapshot.as_singleton().unwrap(); - let text_anchor = insertion_target.bias_left(&buffer_snapshot); - - editor.insert(&mention_text, window, cx); - editor.insert(" ", window, cx); - - (*excerpt_id, text_anchor, mention_text.len()) - }); - - let Some((crease_id, tx)) = insert_crease_for_mention( - excerpt_id, - text_anchor, - content_len, - crease_text.into(), - mention_uri.icon_path(cx), - None, - self.editor.clone(), - window, - cx, - ) else { - continue; - }; - drop(tx); - - let mention_task = cx - .spawn({ - let project = project.clone(); - async move |_, cx| { - let project_path = project - .update(cx, |project, cx| { - project.project_path_for_absolute_path(&file_path, cx) - }) - .map_err(|e| e.to_string())? - .ok_or_else(|| "project path not found".to_string())?; - - let buffer = project - .update(cx, |project, cx| { - project.open_buffer(project_path, cx) - }) - .map_err(|e| e.to_string())? - .await - .map_err(|e| e.to_string())?; - - 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()], - } - }) - .map_err(|e| e.to_string()) - } - }) - .shared(); - - self.mention_set.update(cx, |mention_set, _cx| { - mention_set.insert_mention(crease_id, mention_uri.clone(), mention_task) - }); - } - } - return; - } - } - - if self.prompt_capabilities.borrow().image - && let Some(task) = - paste_images_as_context(self.editor.clone(), self.mention_set.clone(), window, cx) - { - task.detach(); - } - } - - pub fn insert_dragged_files( - &mut self, - paths: Vec, - added_worktrees: Vec>, - window: &mut Window, - cx: &mut Context, - ) { - let Some(workspace) = self.workspace.upgrade() else { - return; - }; - let project = workspace.read(cx).project().clone(); - let path_style = project.read(cx).path_style(cx); - let buffer = self.editor.read(cx).buffer().clone(); - let Some(buffer) = buffer.read(cx).as_singleton() else { - return; - }; - let mut tasks = Vec::new(); - for path in paths { - let Some(entry) = project.read(cx).entry_for_path(&path, cx) else { - continue; - }; - let Some(worktree) = project.read(cx).worktree_for_id(path.worktree_id, cx) else { - continue; - }; - let abs_path = worktree.read(cx).absolutize(&path.path); - let (file_name, _) = crate::completion_provider::extract_file_name_and_directory( - &path.path, - worktree.read(cx).root_name(), - path_style, - ); - - let uri = if entry.is_dir() { - MentionUri::Directory { abs_path } - } else { - MentionUri::File { abs_path } - }; - - let new_text = format!("{} ", uri.as_link()); - let content_len = new_text.len() - 1; - - let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len())); - - self.editor.update(cx, |message_editor, cx| { - message_editor.edit( - [( - multi_buffer::Anchor::max()..multi_buffer::Anchor::max(), - new_text, - )], - cx, - ); - }); - let supports_images = self.prompt_capabilities.borrow().image; - tasks.push(self.mention_set.update(cx, |mention_set, cx| { - mention_set.confirm_mention_completion( - file_name, - anchor, - content_len, - uri, - supports_images, - self.editor.clone(), - &workspace, - window, - cx, - ) - })); - } - cx.spawn(async move |_, _| { - join_all(tasks).await; - drop(added_worktrees); - }) - .detach(); - } - - pub fn insert_selections(&mut self, window: &mut Window, cx: &mut Context) { - let editor = self.editor.read(cx); - let editor_buffer = editor.buffer().read(cx); - let Some(buffer) = editor_buffer.as_singleton() else { - return; - }; - let cursor_anchor = editor.selections.newest_anchor().head(); - let cursor_offset = cursor_anchor.to_offset(&editor_buffer.snapshot(cx)); - let anchor = buffer.update(cx, |buffer, _cx| { - buffer.anchor_before(cursor_offset.0.min(buffer.len())) - }); - let Some(workspace) = self.workspace.upgrade() else { - return; - }; - let Some(completion) = - PromptCompletionProvider::>::completion_for_action( - PromptContextAction::AddSelections, - anchor..anchor, - self.editor.downgrade(), - self.mention_set.downgrade(), - &workspace, - cx, - ) - else { - return; - }; - - self.editor.update(cx, |message_editor, cx| { - message_editor.edit([(cursor_anchor..cursor_anchor, completion.new_text)], cx); - message_editor.request_autoscroll(Autoscroll::fit(), cx); - }); - if let Some(confirm) = completion.confirm { - confirm(CompletionIntent::Complete, window, cx); - } - } - - pub fn set_read_only(&mut self, read_only: bool, cx: &mut Context) { - self.editor.update(cx, |message_editor, cx| { - message_editor.set_read_only(read_only); - cx.notify() - }) - } - - pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context) { - self.editor.update(cx, |editor, cx| { - editor.set_mode(mode); - cx.notify() - }); - } - - pub fn set_message( - &mut self, - message: Vec, - window: &mut Window, - cx: &mut Context, - ) { - let Some(workspace) = self.workspace.upgrade() else { - return; - }; - - self.clear(window, cx); - - let path_style = workspace.read(cx).project().read(cx).path_style(cx); - let mut text = String::new(); - let mut mentions = Vec::new(); - - for chunk in message { - match chunk { - acp::ContentBlock::Text(text_content) => { - text.push_str(&text_content.text); - } - acp::ContentBlock::Resource(acp::EmbeddedResource { - resource: acp::EmbeddedResourceResource::TextResourceContents(resource), - .. - }) => { - let Some(mention_uri) = MentionUri::parse(&resource.uri, path_style).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, path_style).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::Link)); - } - } - acp::ContentBlock::Image(acp::ImageContent { - uri, - data, - mime_type, - .. - }) => { - let mention_uri = if let Some(uri) = uri { - MentionUri::parse(&uri, path_style) - } else { - Ok(MentionUri::PastedImage) - }; - let Some(mention_uri) = mention_uri.log_err() else { - continue; - }; - let Some(format) = ImageFormat::from_mime_type(&mime_type) else { - log::error!("failed to parse MIME type for image: {mime_type:?}"); - continue; - }; - let start = text.len(); - write!(&mut text, "{}", mention_uri.as_link()).ok(); - let end = text.len(); - mentions.push(( - start..end, - mention_uri, - Mention::Image(MentionImage { - data: data.into(), - format, - }), - )); - } - _ => {} - } - } - - let snapshot = self.editor.update(cx, |editor, cx| { - editor.set_text(text, window, cx); - editor.buffer().read(cx).snapshot(cx) - }); - - for (range, mention_uri, mention) in mentions { - let anchor = snapshot.anchor_before(MultiBufferOffset(range.start)); - let Some((crease_id, tx)) = insert_crease_for_mention( - anchor.excerpt_id, - anchor.text_anchor, - range.end - range.start, - mention_uri.name().into(), - mention_uri.icon_path(cx), - None, - self.editor.clone(), - window, - cx, - ) else { - continue; - }; - drop(tx); - - self.mention_set.update(cx, |mention_set, _cx| { - mention_set.insert_mention( - crease_id, - mention_uri.clone(), - Task::ready(Ok(mention)).shared(), - ) - }); - } - cx.notify(); - } - - pub fn text(&self, cx: &App) -> String { - self.editor.read(cx).text(cx) - } - - pub fn set_placeholder_text( - &mut self, - placeholder: &str, - window: &mut Window, - cx: &mut Context, - ) { - self.editor.update(cx, |editor, cx| { - editor.set_placeholder_text(placeholder, window, 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); - }); - } -} - -impl Focusable for MessageEditor { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.editor.focus_handle(cx) - } -} - -impl Render for MessageEditor { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - div() - .key_context("MessageEditor") - .on_action(cx.listener(Self::chat)) - .on_action(cx.listener(Self::chat_with_follow)) - .on_action(cx.listener(Self::cancel)) - .capture_action(cx.listener(Self::paste)) - .flex_1() - .child({ - let settings = ThemeSettings::get_global(cx); - - let text_style = TextStyle { - color: cx.theme().colors().text, - font_family: settings.buffer_font.family.clone(), - font_fallbacks: settings.buffer_font.fallbacks.clone(), - font_features: settings.buffer_font.features.clone(), - font_size: settings.agent_buffer_font_size(cx).into(), - line_height: relative(settings.buffer_line_height.value()), - ..Default::default() - }; - - EditorElement::new( - &self.editor, - EditorStyle { - background: cx.theme().colors().editor_background, - local_player: cx.theme().players().local(), - text: text_style, - syntax: cx.theme().syntax().clone(), - inlay_hints_style: editor::make_inlay_hints_style(cx), - ..Default::default() - }, - ) - }) - } -} - -pub struct MessageEditorAddon {} - -impl MessageEditorAddon { - pub fn new() -> Self { - Self {} - } -} - -impl Addon for MessageEditorAddon { - fn to_any(&self) -> &dyn std::any::Any { - self - } - - fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> { - Some(self) - } - - fn extend_key_context(&self, key_context: &mut KeyContext, cx: &App) { - let settings = agent_settings::AgentSettings::get_global(cx); - if settings.use_modifier_to_send { - key_context.add("use_modifier_to_send"); - } - } -} - -#[cfg(test)] -mod tests { - use std::{cell::RefCell, ops::Range, path::Path, rc::Rc, sync::Arc}; - - use acp_thread::MentionUri; - use agent::{HistoryStore, outline}; - use agent_client_protocol as acp; - use assistant_text_thread::TextThreadStore; - use editor::{AnchorRangeExt as _, Editor, EditorMode, MultiBufferOffset}; - use fs::FakeFs; - use futures::StreamExt as _; - use gpui::{ - AppContext, Entity, EventEmitter, FocusHandle, Focusable, TestAppContext, VisualTestContext, - }; - use language_model::LanguageModelRegistry; - use lsp::{CompletionContext, CompletionTriggerKind}; - use project::{CompletionIntent, Project, ProjectPath}; - use serde_json::json; - use text::Point; - use ui::{App, Context, IntoElement, Render, SharedString, Window}; - use util::{path, paths::PathStyle, rel_path::rel_path}; - use workspace::{AppState, Item, Workspace}; - - use crate::acp::{ - message_editor::{Mention, MessageEditor}, - thread_view::tests::init_test, - }; - - #[gpui::test] - async fn test_at_mention_removal(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree("/project", json!({"file": ""})).await; - let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; - - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); - - let message_editor = cx.update(|window, cx| { - cx.new(|cx| { - MessageEditor::new( - workspace.downgrade(), - project.downgrade(), - history_store.clone(), - None, - Default::default(), - Default::default(), - "Test Agent".into(), - "Test", - EditorMode::AutoHeight { - min_lines: 1, - max_lines: None, - }, - window, - cx, - ) - }) - }); - let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone()); - - cx.run_until_parked(); - - let excerpt_id = editor.update(cx, |editor, cx| { - editor - .buffer() - .read(cx) - .excerpt_ids() - .into_iter() - .next() - .unwrap() - }); - let completions = editor.update_in(cx, |editor, window, cx| { - editor.set_text("Hello @file ", window, cx); - let buffer = editor.buffer().read(cx).as_singleton().unwrap(); - let completion_provider = editor.completion_provider().unwrap(); - completion_provider.completions( - excerpt_id, - &buffer, - text::Anchor::MAX, - CompletionContext { - trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER, - trigger_character: Some("@".into()), - }, - window, - cx, - ) - }); - let [_, completion]: [_; 2] = completions - .await - .unwrap() - .into_iter() - .flat_map(|response| response.completions) - .collect::>() - .try_into() - .unwrap(); - - editor.update_in(cx, |editor, window, cx| { - let snapshot = editor.buffer().read(cx).snapshot(cx); - let range = snapshot - .anchor_range_in_excerpt(excerpt_id, completion.replace_range) - .unwrap(); - editor.edit([(range, completion.new_text)], cx); - (completion.confirm.unwrap())(CompletionIntent::Complete, window, cx); - }); - - cx.run_until_parked(); - - // Backspace over the inserted crease (and the following space). - editor.update_in(cx, |editor, window, cx| { - editor.backspace(&Default::default(), window, cx); - editor.backspace(&Default::default(), window, cx); - }); - - let (content, _) = message_editor - .update(cx, |message_editor, cx| message_editor.contents(false, cx)) - .await - .unwrap(); - - // We don't send a resource link for the deleted crease. - pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]); - } - - #[gpui::test] - async fn test_slash_command_validation(cx: &mut gpui::TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/test", - json!({ - ".zed": { - "tasks.json": r#"[{"label": "test", "command": "echo"}]"# - }, - "src": { - "main.rs": "fn main() {}", - }, - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; - let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); - let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default())); - // Start with no available commands - simulating Claude which doesn't support slash commands - let available_commands = Rc::new(RefCell::new(vec![])); - - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let workspace_handle = workspace.downgrade(); - let message_editor = workspace.update_in(cx, |_, window, cx| { - cx.new(|cx| { - MessageEditor::new( - workspace_handle.clone(), - project.downgrade(), - history_store.clone(), - None, - prompt_capabilities.clone(), - available_commands.clone(), - "Claude Code".into(), - "Test", - EditorMode::AutoHeight { - min_lines: 1, - max_lines: None, - }, - window, - cx, - ) - }) - }); - let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone()); - - // Test that slash commands fail when no available_commands are set (empty list means no commands supported) - editor.update_in(cx, |editor, window, cx| { - editor.set_text("/file test.txt", window, cx); - }); - - let contents_result = message_editor - .update(cx, |message_editor, cx| message_editor.contents(false, cx)) - .await; - - // Should fail because available_commands is empty (no commands supported) - assert!(contents_result.is_err()); - let error_message = contents_result.unwrap_err().to_string(); - assert!(error_message.contains("not supported by Claude Code")); - assert!(error_message.contains("Available commands: none")); - - // Now simulate Claude providing its list of available commands (which doesn't include file) - available_commands.replace(vec![acp::AvailableCommand::new("help", "Get help")]); - - // Test that unsupported slash commands trigger an error when we have a list of available commands - editor.update_in(cx, |editor, window, cx| { - editor.set_text("/file test.txt", window, cx); - }); - - let contents_result = message_editor - .update(cx, |message_editor, cx| message_editor.contents(false, cx)) - .await; - - assert!(contents_result.is_err()); - let error_message = contents_result.unwrap_err().to_string(); - assert!(error_message.contains("not supported by Claude Code")); - assert!(error_message.contains("/file")); - assert!(error_message.contains("Available commands: /help")); - - // Test that supported commands work fine - editor.update_in(cx, |editor, window, cx| { - editor.set_text("/help", window, cx); - }); - - let contents_result = message_editor - .update(cx, |message_editor, cx| message_editor.contents(false, cx)) - .await; - - // Should succeed because /help is in available_commands - assert!(contents_result.is_ok()); - - // Test that regular text works fine - editor.update_in(cx, |editor, window, cx| { - editor.set_text("Hello Claude!", window, cx); - }); - - let (content, _) = message_editor - .update(cx, |message_editor, cx| message_editor.contents(false, cx)) - .await - .unwrap(); - - assert_eq!(content.len(), 1); - if let acp::ContentBlock::Text(text) = &content[0] { - assert_eq!(text.text, "Hello Claude!"); - } else { - panic!("Expected ContentBlock::Text"); - } - - // Test that @ mentions still work - editor.update_in(cx, |editor, window, cx| { - editor.set_text("Check this @", window, cx); - }); - - // The @ mention functionality should not be affected - let (content, _) = message_editor - .update(cx, |message_editor, cx| message_editor.contents(false, cx)) - .await - .unwrap(); - - assert_eq!(content.len(), 1); - if let acp::ContentBlock::Text(text) = &content[0] { - assert_eq!(text.text, "Check this @"); - } else { - panic!("Expected ContentBlock::Text"); - } - } - - struct MessageEditorItem(Entity); - - impl Item for MessageEditorItem { - type Event = (); - - fn include_in_nav_history() -> bool { - false - } - - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { - "Test".into() - } - } - - impl EventEmitter<()> for MessageEditorItem {} - - impl Focusable for MessageEditorItem { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.0.read(cx).focus_handle(cx) - } - } - - impl Render for MessageEditorItem { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - self.0.clone().into_any_element() - } - } - - #[gpui::test] - async fn test_completion_provider_commands(cx: &mut TestAppContext) { - init_test(cx); - - let app_state = cx.update(AppState::test); - - cx.update(|cx| { - editor::init(cx); - workspace::init(app_state.clone(), cx); - }); - - let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let workspace = window.root(cx).unwrap(); - - let mut cx = VisualTestContext::from_window(*window, cx); - - let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); - let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default())); - let available_commands = Rc::new(RefCell::new(vec![ - acp::AvailableCommand::new("quick-math", "2 + 2 = 4 - 1 = 3"), - acp::AvailableCommand::new("say-hello", "Say hello to whoever you want").input( - acp::AvailableCommandInput::Unstructured(acp::UnstructuredCommandInput::new( - "", - )), - ), - ])); - - let editor = workspace.update_in(&mut cx, |workspace, window, cx| { - let workspace_handle = cx.weak_entity(); - let message_editor = cx.new(|cx| { - MessageEditor::new( - workspace_handle, - project.downgrade(), - history_store.clone(), - None, - prompt_capabilities.clone(), - available_commands.clone(), - "Test Agent".into(), - "Test", - EditorMode::AutoHeight { - max_lines: None, - min_lines: 1, - }, - window, - cx, - ) - }); - workspace.active_pane().update(cx, |pane, cx| { - pane.add_item( - Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))), - true, - true, - None, - window, - cx, - ); - }); - message_editor.read(cx).focus_handle(cx).focus(window); - message_editor.read(cx).editor().clone() - }); - - cx.simulate_input("/"); - - editor.update_in(&mut cx, |editor, window, cx| { - assert_eq!(editor.text(cx), "/"); - assert!(editor.has_visible_completions_menu()); - - assert_eq!( - current_completion_labels_with_documentation(editor), - &[ - ("quick-math".into(), "2 + 2 = 4 - 1 = 3".into()), - ("say-hello".into(), "Say hello to whoever you want".into()) - ] - ); - editor.set_text("", window, cx); - }); - - cx.simulate_input("/qui"); - - editor.update_in(&mut cx, |editor, window, cx| { - assert_eq!(editor.text(cx), "/qui"); - assert!(editor.has_visible_completions_menu()); - - assert_eq!( - current_completion_labels_with_documentation(editor), - &[("quick-math".into(), "2 + 2 = 4 - 1 = 3".into())] - ); - editor.set_text("", window, cx); - }); - - editor.update_in(&mut cx, |editor, window, cx| { - assert!(editor.has_visible_completions_menu()); - editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); - }); - - cx.run_until_parked(); - - editor.update_in(&mut cx, |editor, window, cx| { - assert_eq!(editor.display_text(cx), "/quick-math "); - assert!(!editor.has_visible_completions_menu()); - editor.set_text("", window, cx); - }); - - cx.simulate_input("/say"); - - editor.update_in(&mut cx, |editor, _window, cx| { - assert_eq!(editor.display_text(cx), "/say"); - assert!(editor.has_visible_completions_menu()); - - assert_eq!( - current_completion_labels_with_documentation(editor), - &[("say-hello".into(), "Say hello to whoever you want".into())] - ); - }); - - editor.update_in(&mut cx, |editor, window, cx| { - assert!(editor.has_visible_completions_menu()); - editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); - }); - - cx.run_until_parked(); - - editor.update_in(&mut cx, |editor, _window, cx| { - assert_eq!(editor.text(cx), "/say-hello "); - assert_eq!(editor.display_text(cx), "/say-hello "); - assert!(!editor.has_visible_completions_menu()); - }); - - cx.simulate_input("GPT5"); - - cx.run_until_parked(); - - editor.update_in(&mut cx, |editor, window, cx| { - assert_eq!(editor.text(cx), "/say-hello GPT5"); - assert_eq!(editor.display_text(cx), "/say-hello GPT5"); - assert!(!editor.has_visible_completions_menu()); - - // Delete argument - for _ in 0..5 { - editor.backspace(&editor::actions::Backspace, window, cx); - } - }); - - cx.run_until_parked(); - - editor.update_in(&mut cx, |editor, window, cx| { - assert_eq!(editor.text(cx), "/say-hello"); - // Hint is visible because argument was deleted - assert_eq!(editor.display_text(cx), "/say-hello "); - - // Delete last command letter - editor.backspace(&editor::actions::Backspace, window, cx); - }); - - cx.run_until_parked(); - - editor.update_in(&mut cx, |editor, _window, cx| { - // Hint goes away once command no longer matches an available one - assert_eq!(editor.text(cx), "/say-hell"); - assert_eq!(editor.display_text(cx), "/say-hell"); - assert!(!editor.has_visible_completions_menu()); - }); - } - - #[gpui::test] - async fn test_context_completion_provider_mentions(cx: &mut TestAppContext) { - init_test(cx); - - let app_state = cx.update(AppState::test); - - cx.update(|cx| { - editor::init(cx); - workspace::init(app_state.clone(), cx); - }); - - app_state - .fs - .as_fake() - .insert_tree( - path!("/dir"), - json!({ - "editor": "", - "a": { - "one.txt": "1", - "two.txt": "2", - "three.txt": "3", - "four.txt": "4" - }, - "b": { - "five.txt": "5", - "six.txt": "6", - "seven.txt": "7", - "eight.txt": "8", - }, - "x.png": "", - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let workspace = window.root(cx).unwrap(); - - let worktree = project.update(cx, |project, cx| { - let mut worktrees = project.worktrees(cx).collect::>(); - assert_eq!(worktrees.len(), 1); - worktrees.pop().unwrap() - }); - let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id()); - - let mut cx = VisualTestContext::from_window(*window, cx); - - let paths = vec![ - rel_path("a/one.txt"), - rel_path("a/two.txt"), - rel_path("a/three.txt"), - rel_path("a/four.txt"), - rel_path("b/five.txt"), - rel_path("b/six.txt"), - rel_path("b/seven.txt"), - rel_path("b/eight.txt"), - ]; - - let slash = PathStyle::local().primary_separator(); - - let mut opened_editors = Vec::new(); - for path in paths { - let buffer = workspace - .update_in(&mut cx, |workspace, window, cx| { - workspace.open_path( - ProjectPath { - worktree_id, - path: path.into(), - }, - None, - false, - window, - cx, - ) - }) - .await - .unwrap(); - opened_editors.push(buffer); - } - - let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); - let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default())); - - let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| { - let workspace_handle = cx.weak_entity(); - let message_editor = cx.new(|cx| { - MessageEditor::new( - workspace_handle, - project.downgrade(), - history_store.clone(), - None, - prompt_capabilities.clone(), - Default::default(), - "Test Agent".into(), - "Test", - EditorMode::AutoHeight { - max_lines: None, - min_lines: 1, - }, - window, - cx, - ) - }); - workspace.active_pane().update(cx, |pane, cx| { - pane.add_item( - Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))), - true, - true, - None, - window, - cx, - ); - }); - message_editor.read(cx).focus_handle(cx).focus(window); - let editor = message_editor.read(cx).editor().clone(); - (message_editor, editor) - }); - - cx.simulate_input("Lorem @"); - - editor.update_in(&mut cx, |editor, window, cx| { - assert_eq!(editor.text(cx), "Lorem @"); - assert!(editor.has_visible_completions_menu()); - - assert_eq!( - current_completion_labels(editor), - &[ - format!("eight.txt b{slash}"), - format!("seven.txt b{slash}"), - format!("six.txt b{slash}"), - format!("five.txt b{slash}"), - "Files & Directories".into(), - "Symbols".into() - ] - ); - editor.set_text("", window, cx); - }); - - prompt_capabilities.replace( - acp::PromptCapabilities::new() - .image(true) - .audio(true) - .embedded_context(true), - ); - - cx.simulate_input("Lorem "); - - editor.update(&mut cx, |editor, cx| { - assert_eq!(editor.text(cx), "Lorem "); - assert!(!editor.has_visible_completions_menu()); - }); - - cx.simulate_input("@"); - - editor.update(&mut cx, |editor, cx| { - assert_eq!(editor.text(cx), "Lorem @"); - assert!(editor.has_visible_completions_menu()); - assert_eq!( - current_completion_labels(editor), - &[ - format!("eight.txt b{slash}"), - format!("seven.txt b{slash}"), - format!("six.txt b{slash}"), - format!("five.txt b{slash}"), - "Files & Directories".into(), - "Symbols".into(), - "Threads".into(), - "Fetch".into() - ] - ); - }); - - // Select and confirm "File" - editor.update_in(&mut cx, |editor, window, cx| { - assert!(editor.has_visible_completions_menu()); - editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx); - editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx); - editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx); - editor.context_menu_next(&editor::actions::ContextMenuNext, window, cx); - editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); - }); - - cx.run_until_parked(); - - editor.update(&mut cx, |editor, cx| { - assert_eq!(editor.text(cx), "Lorem @file "); - assert!(editor.has_visible_completions_menu()); - }); - - cx.simulate_input("one"); - - editor.update(&mut cx, |editor, cx| { - assert_eq!(editor.text(cx), "Lorem @file one"); - assert!(editor.has_visible_completions_menu()); - assert_eq!( - current_completion_labels(editor), - vec![format!("one.txt a{slash}")] - ); - }); - - editor.update_in(&mut cx, |editor, window, cx| { - assert!(editor.has_visible_completions_menu()); - editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); - }); - - let url_one = MentionUri::File { - abs_path: path!("/dir/a/one.txt").into(), - } - .to_uri() - .to_string(); - editor.update(&mut cx, |editor, cx| { - let text = editor.text(cx); - assert_eq!(text, format!("Lorem [@one.txt]({url_one}) ")); - assert!(!editor.has_visible_completions_menu()); - assert_eq!(fold_ranges(editor, cx).len(), 1); - }); - - let contents = message_editor - .update(&mut cx, |message_editor, cx| { - message_editor - .mention_set() - .update(cx, |mention_set, cx| mention_set.contents(false, cx)) - }) - .await - .unwrap() - .into_values() - .collect::>(); - - { - let [(uri, Mention::Text { content, .. })] = contents.as_slice() else { - panic!("Unexpected mentions"); - }; - pretty_assertions::assert_eq!(content, "1"); - pretty_assertions::assert_eq!( - uri, - &MentionUri::parse(&url_one, PathStyle::local()).unwrap() - ); - } - - cx.simulate_input(" "); - - editor.update(&mut cx, |editor, cx| { - let text = editor.text(cx); - assert_eq!(text, format!("Lorem [@one.txt]({url_one}) ")); - assert!(!editor.has_visible_completions_menu()); - assert_eq!(fold_ranges(editor, cx).len(), 1); - }); - - cx.simulate_input("Ipsum "); - - editor.update(&mut cx, |editor, cx| { - let text = editor.text(cx); - assert_eq!(text, format!("Lorem [@one.txt]({url_one}) Ipsum "),); - assert!(!editor.has_visible_completions_menu()); - assert_eq!(fold_ranges(editor, cx).len(), 1); - }); - - cx.simulate_input("@file "); - - editor.update(&mut cx, |editor, cx| { - let text = editor.text(cx); - assert_eq!(text, format!("Lorem [@one.txt]({url_one}) Ipsum @file "),); - assert!(editor.has_visible_completions_menu()); - assert_eq!(fold_ranges(editor, cx).len(), 1); - }); - - editor.update_in(&mut cx, |editor, window, cx| { - editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); - }); - - cx.run_until_parked(); - - let contents = message_editor - .update(&mut cx, |message_editor, cx| { - message_editor - .mention_set() - .update(cx, |mention_set, cx| mention_set.contents(false, cx)) - }) - .await - .unwrap() - .into_values() - .collect::>(); - - let url_eight = MentionUri::File { - abs_path: path!("/dir/b/eight.txt").into(), - } - .to_uri() - .to_string(); - - { - let [_, (uri, Mention::Text { content, .. })] = contents.as_slice() else { - panic!("Unexpected mentions"); - }; - pretty_assertions::assert_eq!(content, "8"); - pretty_assertions::assert_eq!( - uri, - &MentionUri::parse(&url_eight, PathStyle::local()).unwrap() - ); - } - - editor.update(&mut cx, |editor, cx| { - assert_eq!( - editor.text(cx), - format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) ") - ); - assert!(!editor.has_visible_completions_menu()); - assert_eq!(fold_ranges(editor, cx).len(), 2); - }); - - let plain_text_language = Arc::new(language::Language::new( - language::LanguageConfig { - name: "Plain Text".into(), - matcher: language::LanguageMatcher { - path_suffixes: vec!["txt".to_string()], - ..Default::default() - }, - ..Default::default() - }, - None, - )); - - // Register the language and fake LSP - let language_registry = project.read_with(&cx, |project, _| project.languages().clone()); - language_registry.add(plain_text_language); - - let mut fake_language_servers = language_registry.register_fake_lsp( - "Plain Text", - language::FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - workspace_symbol_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() - }, - ); - - // Open the buffer to trigger LSP initialization - let buffer = project - .update(&mut cx, |project, cx| { - project.open_local_buffer(path!("/dir/a/one.txt"), cx) - }) - .await - .unwrap(); - - // Register the buffer with language servers - let _handle = project.update(&mut cx, |project, cx| { - project.register_buffer_with_language_servers(&buffer, cx) - }); - - cx.run_until_parked(); - - let fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server.set_request_handler::( - move |_, _| async move { - Ok(Some(lsp::WorkspaceSymbolResponse::Flat(vec![ - #[allow(deprecated)] - lsp::SymbolInformation { - name: "MySymbol".into(), - location: lsp::Location { - uri: lsp::Uri::from_file_path(path!("/dir/a/one.txt")).unwrap(), - range: lsp::Range::new( - lsp::Position::new(0, 0), - lsp::Position::new(0, 1), - ), - }, - kind: lsp::SymbolKind::CONSTANT, - tags: None, - container_name: None, - deprecated: None, - }, - ]))) - }, - ); - - cx.simulate_input("@symbol "); - - editor.update(&mut cx, |editor, cx| { - assert_eq!( - editor.text(cx), - format!("Lorem [@one.txt]({url_one}) Ipsum [@eight.txt]({url_eight}) @symbol ") - ); - assert!(editor.has_visible_completions_menu()); - assert_eq!(current_completion_labels(editor), &["MySymbol one.txt L1"]); - }); - - editor.update_in(&mut cx, |editor, window, cx| { - editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); - }); - - let symbol = MentionUri::Symbol { - abs_path: path!("/dir/a/one.txt").into(), - name: "MySymbol".into(), - line_range: 0..=0, - }; - - let contents = message_editor - .update(&mut cx, |message_editor, cx| { - message_editor - .mention_set() - .update(cx, |mention_set, cx| mention_set.contents(false, cx)) - }) - .await - .unwrap() - .into_values() - .collect::>(); - - { - let [_, _, (uri, Mention::Text { content, .. })] = contents.as_slice() else { - panic!("Unexpected mentions"); - }; - pretty_assertions::assert_eq!(content, "1"); - pretty_assertions::assert_eq!(uri, &symbol); - } - - 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]({}) ", - symbol.to_uri(), - ) - ); - }); - - // 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]({}) @file x.png", symbol.to_uri()) - ); - assert!(editor.has_visible_completions_menu()); - assert_eq!(current_completion_labels(editor), &["x.png "]); - }); - - 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() - .update(cx, |mention_set, cx| mention_set.contents(false, 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]({}) ", - symbol.to_uri() - ) - ); - }); - - // 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]({}) @file x.png", symbol.to_uri()) - ); - assert!(editor.has_visible_completions_menu()); - assert_eq!(current_completion_labels(editor), &["x.png "]); - }); - - 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]({}) ", - symbol.to_uri() - ) - ); - }); - - // 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() - .update(cx, |mention_set, cx| mention_set.contents(false, cx)) - }) - .await - .unwrap(); - assert_eq!(contents.len(), 3); - } - - fn fold_ranges(editor: &Editor, cx: &mut App) -> Vec> { - let snapshot = editor.buffer().read(cx).snapshot(cx); - editor.display_map.update(cx, |display_map, cx| { - display_map - .snapshot(cx) - .folds_in_range(MultiBufferOffset(0)..snapshot.len()) - .map(|fold| fold.range.to_point(&snapshot)) - .collect() - }) - } - - fn current_completion_labels(editor: &Editor) -> Vec { - let completions = editor.current_completions().expect("Missing completions"); - completions - .into_iter() - .map(|completion| completion.label.text) - .collect::>() - } - - fn current_completion_labels_with_documentation(editor: &Editor) -> Vec<(String, String)> { - let completions = editor.current_completions().expect("Missing completions"); - completions - .into_iter() - .map(|completion| { - ( - completion.label.text, - completion - .documentation - .map(|d| d.text().to_string()) - .unwrap_or_default(), - ) - }) - .collect::>() - } - - #[gpui::test] - async fn test_large_file_mention_fallback(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - - // Create a large file that exceeds AUTO_OUTLINE_SIZE - // Using plain text without a configured language, so no outline is available - const LINE: &str = "This is a line of text in the file\n"; - let large_content = LINE.repeat(2 * (outline::AUTO_OUTLINE_SIZE / LINE.len())); - assert!(large_content.len() > outline::AUTO_OUTLINE_SIZE); - - // Create a small file that doesn't exceed AUTO_OUTLINE_SIZE - let small_content = "fn small_function() { /* small */ }\n"; - assert!(small_content.len() < outline::AUTO_OUTLINE_SIZE); - - fs.insert_tree( - "/project", - json!({ - "large_file.txt": large_content.clone(), - "small_file.txt": small_content, - }), - ) - .await; - - let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; - - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); - - let message_editor = cx.update(|window, cx| { - cx.new(|cx| { - let editor = MessageEditor::new( - workspace.downgrade(), - project.downgrade(), - history_store.clone(), - None, - Default::default(), - Default::default(), - "Test Agent".into(), - "Test", - EditorMode::AutoHeight { - min_lines: 1, - max_lines: None, - }, - window, - cx, - ); - // Enable embedded context so files are actually included - editor - .prompt_capabilities - .replace(acp::PromptCapabilities::new().embedded_context(true)); - editor - }) - }); - - // Test large file mention - // Get the absolute path using the project's worktree - let large_file_abs_path = project.read_with(cx, |project, cx| { - let worktree = project.worktrees(cx).next().unwrap(); - let worktree_root = worktree.read(cx).abs_path(); - worktree_root.join("large_file.txt") - }); - let large_file_task = message_editor.update(cx, |editor, cx| { - editor.mention_set().update(cx, |set, cx| { - set.confirm_mention_for_file(large_file_abs_path, true, cx) - }) - }); - - let large_file_mention = large_file_task.await.unwrap(); - match large_file_mention { - Mention::Text { content, .. } => { - // Should contain some of the content but not all of it - assert!( - content.contains(LINE), - "Should contain some of the file content" - ); - assert!( - !content.contains(&LINE.repeat(100)), - "Should not contain the full file" - ); - // Should be much smaller than original - assert!( - content.len() < large_content.len() / 10, - "Should be significantly truncated" - ); - } - _ => panic!("Expected Text mention for large file"), - } - - // Test small file mention - // Get the absolute path using the project's worktree - let small_file_abs_path = project.read_with(cx, |project, cx| { - let worktree = project.worktrees(cx).next().unwrap(); - let worktree_root = worktree.read(cx).abs_path(); - worktree_root.join("small_file.txt") - }); - let small_file_task = message_editor.update(cx, |editor, cx| { - editor.mention_set().update(cx, |set, cx| { - set.confirm_mention_for_file(small_file_abs_path, true, cx) - }) - }); - - let small_file_mention = small_file_task.await.unwrap(); - match small_file_mention { - Mention::Text { content, .. } => { - // Should contain the full actual content - assert_eq!(content, small_content); - } - _ => panic!("Expected Text mention for small file"), - } - } - - #[gpui::test] - async fn test_insert_thread_summary(cx: &mut TestAppContext) { - init_test(cx); - cx.update(LanguageModelRegistry::test); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree("/project", json!({"file": ""})).await; - let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; - - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); - - // Create a thread metadata to insert as summary - let thread_metadata = agent::DbThreadMetadata { - id: acp::SessionId::new("thread-123"), - title: "Previous Conversation".into(), - updated_at: chrono::Utc::now(), - }; - - let message_editor = cx.update(|window, cx| { - cx.new(|cx| { - let mut editor = MessageEditor::new( - workspace.downgrade(), - project.downgrade(), - history_store.clone(), - None, - Default::default(), - Default::default(), - "Test Agent".into(), - "Test", - EditorMode::AutoHeight { - min_lines: 1, - max_lines: None, - }, - window, - cx, - ); - editor.insert_thread_summary(thread_metadata.clone(), window, cx); - editor - }) - }); - - // Construct expected values for verification - let expected_uri = MentionUri::Thread { - id: thread_metadata.id.clone(), - name: thread_metadata.title.to_string(), - }; - let expected_link = format!("[@{}]({})", thread_metadata.title, expected_uri.to_uri()); - - message_editor.read_with(cx, |editor, cx| { - let text = editor.text(cx); - - assert!( - text.contains(&expected_link), - "Expected editor text to contain thread mention link.\nExpected substring: {}\nActual text: {}", - expected_link, - text - ); - - let mentions = editor.mention_set().read(cx).mentions(); - assert_eq!( - mentions.len(), - 1, - "Expected exactly one mention after inserting thread summary" - ); - - assert!( - mentions.contains(&expected_uri), - "Expected mentions to contain the thread URI" - ); - }); - } - - #[gpui::test] - async fn test_whitespace_trimming(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree("/project", json!({"file.rs": "fn main() {}"})) - .await; - let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; - - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); - - let message_editor = cx.update(|window, cx| { - cx.new(|cx| { - MessageEditor::new( - workspace.downgrade(), - project.downgrade(), - history_store.clone(), - None, - Default::default(), - Default::default(), - "Test Agent".into(), - "Test", - EditorMode::AutoHeight { - min_lines: 1, - max_lines: None, - }, - window, - cx, - ) - }) - }); - let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone()); - - cx.run_until_parked(); - - editor.update_in(cx, |editor, window, cx| { - editor.set_text(" \u{A0}してhello world ", window, cx); - }); - - let (content, _) = message_editor - .update(cx, |message_editor, cx| message_editor.contents(false, cx)) - .await - .unwrap(); - - assert_eq!(content, vec!["してhello world".into()]); - } - - #[gpui::test] - async fn test_editor_respects_embedded_context_capability(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - - let file_content = "fn main() { println!(\"Hello, world!\"); }\n"; - - fs.insert_tree( - "/project", - json!({ - "src": { - "main.rs": file_content, - } - }), - ) - .await; - - let project = Project::test(fs, [Path::new(path!("/project"))], cx).await; - - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); - - let (message_editor, editor) = workspace.update_in(cx, |workspace, window, cx| { - let workspace_handle = cx.weak_entity(); - let message_editor = cx.new(|cx| { - MessageEditor::new( - workspace_handle, - project.downgrade(), - history_store.clone(), - None, - Default::default(), - Default::default(), - "Test Agent".into(), - "Test", - EditorMode::AutoHeight { - max_lines: None, - min_lines: 1, - }, - window, - cx, - ) - }); - workspace.active_pane().update(cx, |pane, cx| { - pane.add_item( - Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))), - true, - true, - None, - window, - cx, - ); - }); - message_editor.read(cx).focus_handle(cx).focus(window); - let editor = message_editor.read(cx).editor().clone(); - (message_editor, editor) - }); - - cx.simulate_input("What is in @file main"); - - editor.update_in(cx, |editor, window, cx| { - assert!(editor.has_visible_completions_menu()); - assert_eq!(editor.text(cx), "What is in @file main"); - editor.confirm_completion(&editor::actions::ConfirmCompletion::default(), window, cx); - }); - - let content = message_editor - .update(cx, |editor, cx| editor.contents(false, cx)) - .await - .unwrap() - .0; - - let main_rs_uri = if cfg!(windows) { - "file:///C:/project/src/main.rs" - } else { - "file:///project/src/main.rs" - }; - - // When embedded context is `false` we should get a resource link - pretty_assertions::assert_eq!( - content, - vec![ - "What is in ".into(), - acp::ContentBlock::ResourceLink(acp::ResourceLink::new("main.rs", main_rs_uri)) - ] - ); - - message_editor.update(cx, |editor, _cx| { - editor - .prompt_capabilities - .replace(acp::PromptCapabilities::new().embedded_context(true)) - }); - - let content = message_editor - .update(cx, |editor, cx| editor.contents(false, cx)) - .await - .unwrap() - .0; - - // When embedded context is `true` we should get a resource - pretty_assertions::assert_eq!( - content, - vec![ - "What is in ".into(), - acp::ContentBlock::Resource(acp::EmbeddedResource::new( - acp::EmbeddedResourceResource::TextResourceContents( - acp::TextResourceContents::new(file_content, main_rs_uri) - ) - )) - ] - ); - } - - #[gpui::test] - async fn test_autoscroll_after_insert_selections(cx: &mut TestAppContext) { - init_test(cx); - - let app_state = cx.update(AppState::test); - - cx.update(|cx| { - editor::init(cx); - workspace::init(app_state.clone(), cx); - }); - - app_state - .fs - .as_fake() - .insert_tree( - path!("/dir"), - json!({ - "test.txt": "line1\nline2\nline3\nline4\nline5\n", - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let workspace = window.root(cx).unwrap(); - - let worktree = project.update(cx, |project, cx| { - let mut worktrees = project.worktrees(cx).collect::>(); - assert_eq!(worktrees.len(), 1); - worktrees.pop().unwrap() - }); - let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id()); - - let mut cx = VisualTestContext::from_window(*window, cx); - - // Open a regular editor with the created file, and select a portion of - // the text that will be used for the selections that are meant to be - // inserted in the agent panel. - let editor = workspace - .update_in(&mut cx, |workspace, window, cx| { - workspace.open_path( - ProjectPath { - worktree_id, - path: rel_path("test.txt").into(), - }, - None, - false, - window, - cx, - ) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - editor.update_in(&mut cx, |editor, window, cx| { - editor.change_selections(Default::default(), window, cx, |selections| { - selections.select_ranges([Point::new(0, 0)..Point::new(0, 5)]); - }); - }); - - let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx)); - let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx)); - - // Create a new `MessageEditor`. The `EditorMode::full()` has to be used - // to ensure we have a fixed viewport, so we can eventually actually - // place the cursor outside of the visible area. - let message_editor = workspace.update_in(&mut cx, |workspace, window, cx| { - let workspace_handle = cx.weak_entity(); - let message_editor = cx.new(|cx| { - MessageEditor::new( - workspace_handle, - project.downgrade(), - history_store.clone(), - None, - Default::default(), - Default::default(), - "Test Agent".into(), - "Test", - EditorMode::full(), - window, - cx, - ) - }); - workspace.active_pane().update(cx, |pane, cx| { - pane.add_item( - Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))), - true, - true, - None, - window, - cx, - ); - }); - - message_editor - }); - - message_editor.update_in(&mut cx, |message_editor, window, cx| { - message_editor.editor.update(cx, |editor, cx| { - // Update the Agent Panel's Message Editor text to have 100 - // lines, ensuring that the cursor is set at line 90 and that we - // then scroll all the way to the top, so the cursor's position - // remains off screen. - let mut lines = String::new(); - for _ in 1..=100 { - lines.push_str(&"Another line in the agent panel's message editor\n"); - } - editor.set_text(lines.as_str(), window, cx); - editor.change_selections(Default::default(), window, cx, |selections| { - selections.select_ranges([Point::new(90, 0)..Point::new(90, 0)]); - }); - editor.set_scroll_position(gpui::Point::new(0., 0.), window, cx); - }); - }); - - cx.run_until_parked(); - - // Before proceeding, let's assert that the cursor is indeed off screen, - // otherwise the rest of the test doesn't make sense. - message_editor.update_in(&mut cx, |message_editor, window, cx| { - message_editor.editor.update(cx, |editor, cx| { - let snapshot = editor.snapshot(window, cx); - let cursor_row = editor.selections.newest::(&snapshot).head().row; - let scroll_top = snapshot.scroll_position().y as u32; - let visible_lines = editor.visible_line_count().unwrap() as u32; - let visible_range = scroll_top..(scroll_top + visible_lines); - - assert!(!visible_range.contains(&cursor_row)); - }) - }); - - // Now let's insert the selection in the Agent Panel's editor and - // confirm that, after the insertion, the cursor is now in the visible - // range. - message_editor.update_in(&mut cx, |message_editor, window, cx| { - message_editor.insert_selections(window, cx); - }); - - cx.run_until_parked(); - - message_editor.update_in(&mut cx, |message_editor, window, cx| { - message_editor.editor.update(cx, |editor, cx| { - let snapshot = editor.snapshot(window, cx); - let cursor_row = editor.selections.newest::(&snapshot).head().row; - let scroll_top = snapshot.scroll_position().y as u32; - let visible_lines = editor.visible_line_count().unwrap() as u32; - let visible_range = scroll_top..(scroll_top + visible_lines); - - assert!(visible_range.contains(&cursor_row)); - }) - }); - } -} diff --git a/crates/agent_ui/src/acp/mode_selector.rs b/crates/agent_ui/src/acp/mode_selector.rs deleted file mode 100644 index 1f50ce7432..0000000000 --- a/crates/agent_ui/src/acp/mode_selector.rs +++ /dev/null @@ -1,229 +0,0 @@ -use acp_thread::AgentSessionModes; -use agent_client_protocol as acp; -use agent_servers::AgentServer; -use agent_settings::AgentSettings; -use fs::Fs; -use gpui::{Context, Entity, FocusHandle, WeakEntity, Window, prelude::*}; -use settings::Settings as _; -use std::{rc::Rc, sync::Arc}; -use ui::{ - Button, ContextMenu, ContextMenuEntry, DocumentationEdge, DocumentationSide, KeyBinding, - PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*, -}; - -use crate::{CycleModeSelector, ToggleProfileSelector, ui::HoldForDefault}; - -pub struct ModeSelector { - connection: Rc, - agent_server: Rc, - menu_handle: PopoverMenuHandle, - focus_handle: FocusHandle, - fs: Arc, - setting_mode: bool, -} - -impl ModeSelector { - pub fn new( - session_modes: Rc, - agent_server: Rc, - fs: Arc, - focus_handle: FocusHandle, - ) -> Self { - Self { - connection: session_modes, - agent_server, - menu_handle: PopoverMenuHandle::default(), - fs, - setting_mode: false, - focus_handle, - } - } - - pub fn menu_handle(&self) -> PopoverMenuHandle { - self.menu_handle.clone() - } - - pub fn cycle_mode(&mut self, _window: &mut Window, cx: &mut Context) { - let all_modes = self.connection.all_modes(); - let current_mode = self.connection.current_mode(); - - let current_index = all_modes - .iter() - .position(|mode| mode.id.0 == current_mode.0) - .unwrap_or(0); - - let next_index = (current_index + 1) % all_modes.len(); - self.set_mode(all_modes[next_index].id.clone(), cx); - } - - pub fn mode(&self) -> acp::SessionModeId { - self.connection.current_mode() - } - - pub fn set_mode(&mut self, mode: acp::SessionModeId, cx: &mut Context) { - let task = self.connection.set_mode(mode, cx); - self.setting_mode = true; - cx.notify(); - - cx.spawn(async move |this: WeakEntity, cx| { - if let Err(err) = task.await { - log::error!("Failed to set session mode: {:?}", err); - } - this.update(cx, |this, cx| { - this.setting_mode = false; - cx.notify(); - }) - .ok(); - }) - .detach(); - } - - fn build_context_menu( - &self, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - let weak_self = cx.weak_entity(); - - ContextMenu::build(window, cx, move |mut menu, _window, cx| { - let all_modes = self.connection.all_modes(); - let current_mode = self.connection.current_mode(); - let default_mode = self.agent_server.default_mode(cx); - - let settings = AgentSettings::get_global(cx); - let side = match settings.dock { - settings::DockPosition::Left => DocumentationSide::Right, - settings::DockPosition::Bottom | settings::DockPosition::Right => { - DocumentationSide::Left - } - }; - - for mode in all_modes { - let is_selected = &mode.id == ¤t_mode; - let is_default = Some(&mode.id) == default_mode.as_ref(); - let entry = ContextMenuEntry::new(mode.name.clone()) - .toggleable(IconPosition::End, is_selected); - - let entry = if let Some(description) = &mode.description { - entry.documentation_aside(side, DocumentationEdge::Bottom, { - let description = description.clone(); - - move |_| { - v_flex() - .gap_1() - .child(Label::new(description.clone())) - .child(HoldForDefault::new(is_default)) - .into_any_element() - } - }) - } else { - entry - }; - - menu.push_item(entry.handler({ - let mode_id = mode.id.clone(); - let weak_self = weak_self.clone(); - move |window, cx| { - weak_self - .update(cx, |this, cx| { - if window.modifiers().secondary() { - this.agent_server.set_default_mode( - if is_default { - None - } else { - Some(mode_id.clone()) - }, - this.fs.clone(), - cx, - ); - } - - this.set_mode(mode_id.clone(), cx); - }) - .ok(); - } - })); - } - - menu.key_context("ModeSelector") - }) - } -} - -impl Render for ModeSelector { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let current_mode_id = self.connection.current_mode(); - let current_mode_name = self - .connection - .all_modes() - .iter() - .find(|mode| mode.id == current_mode_id) - .map(|mode| mode.name.clone()) - .unwrap_or_else(|| "Unknown".into()); - - let this = cx.weak_entity(); - - let icon = if self.menu_handle.is_deployed() { - IconName::ChevronUp - } else { - IconName::ChevronDown - }; - - let trigger_button = Button::new("mode-selector-trigger", current_mode_name) - .label_size(LabelSize::Small) - .color(Color::Muted) - .icon(icon) - .icon_size(IconSize::XSmall) - .icon_position(IconPosition::End) - .icon_color(Color::Muted) - .disabled(self.setting_mode); - - PopoverMenu::new("mode-selector") - .trigger_with_tooltip( - trigger_button, - Tooltip::element({ - let focus_handle = self.focus_handle.clone(); - move |_window, cx| { - v_flex() - .gap_1() - .child( - h_flex() - .pb_1() - .gap_2() - .justify_between() - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .child(Label::new("Cycle Through Modes")) - .child(KeyBinding::for_action_in( - &CycleModeSelector, - &focus_handle, - cx, - )), - ) - .child( - h_flex() - .gap_2() - .justify_between() - .child(Label::new("Toggle Mode Menu")) - .child(KeyBinding::for_action_in( - &ToggleProfileSelector, - &focus_handle, - cx, - )), - ) - .into_any() - } - }), - ) - .anchor(gpui::Corner::BottomRight) - .with_handle(self.menu_handle.clone()) - .offset(gpui::Point { - x: px(0.0), - y: px(-2.0), - }) - .menu(move |window, cx| { - this.update(cx, |this, cx| this.build_context_menu(window, cx)) - .ok() - }) - } -} diff --git a/crates/agent_ui/src/acp/model_selector.rs b/crates/agent_ui/src/acp/model_selector.rs deleted file mode 100644 index f9710ad9b3..0000000000 --- a/crates/agent_ui/src/acp/model_selector.rs +++ /dev/null @@ -1,553 +0,0 @@ -use std::{cmp::Reverse, rc::Rc, sync::Arc}; - -use acp_thread::{AgentModelInfo, AgentModelList, AgentModelSelector}; -use agent_servers::AgentServer; -use anyhow::Result; -use collections::IndexMap; -use fs::Fs; -use futures::FutureExt; -use fuzzy::{StringMatchCandidate, match_strings}; -use gpui::{ - Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, FocusHandle, Task, WeakEntity, -}; -use ordered_float::OrderedFloat; -use picker::{Picker, PickerDelegate}; -use ui::{ - DocumentationAside, DocumentationEdge, DocumentationSide, IntoElement, KeyBinding, ListItem, - ListItemSpacing, prelude::*, -}; -use util::ResultExt; -use zed_actions::agent::OpenSettings; - -use crate::ui::HoldForDefault; - -pub type AcpModelSelector = Picker; - -pub fn acp_model_selector( - selector: Rc, - agent_server: Rc, - fs: Arc, - focus_handle: FocusHandle, - window: &mut Window, - cx: &mut Context, -) -> AcpModelSelector { - let delegate = - AcpModelPickerDelegate::new(selector, agent_server, fs, focus_handle, window, cx); - Picker::list(delegate, window, cx) - .show_scrollbar(true) - .width(rems(20.)) - .max_height(Some(rems(20.).into())) -} - -enum AcpModelPickerEntry { - Separator(SharedString), - Model(AgentModelInfo), -} - -pub struct AcpModelPickerDelegate { - selector: Rc, - agent_server: Rc, - fs: Arc, - filtered_entries: Vec, - models: Option, - selected_index: usize, - selected_description: Option<(usize, SharedString, bool)>, - selected_model: Option, - _refresh_models_task: Task<()>, - focus_handle: FocusHandle, -} - -impl AcpModelPickerDelegate { - fn new( - selector: Rc, - agent_server: Rc, - fs: Arc, - focus_handle: FocusHandle, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let rx = selector.watch(cx); - let refresh_models_task = { - cx.spawn_in(window, { - async move |this, cx| { - async fn refresh( - this: &WeakEntity>, - cx: &mut AsyncWindowContext, - ) -> Result<()> { - let (models_task, selected_model_task) = this.update(cx, |this, cx| { - ( - this.delegate.selector.list_models(cx), - this.delegate.selector.selected_model(cx), - ) - })?; - - let (models, selected_model) = - futures::join!(models_task, selected_model_task); - - this.update_in(cx, |this, window, cx| { - this.delegate.models = models.ok(); - this.delegate.selected_model = selected_model.ok(); - this.refresh(window, cx) - }) - } - - refresh(&this, cx).await.log_err(); - if let Some(mut rx) = rx { - while let Ok(()) = rx.recv().await { - refresh(&this, cx).await.log_err(); - } - } - } - }) - }; - - Self { - selector, - agent_server, - fs, - filtered_entries: Vec::new(), - models: None, - selected_model: None, - selected_index: 0, - selected_description: None, - _refresh_models_task: refresh_models_task, - focus_handle, - } - } - - pub fn active_model(&self) -> Option<&AgentModelInfo> { - self.selected_model.as_ref() - } -} - -impl PickerDelegate for AcpModelPickerDelegate { - type ListItem = AnyElement; - - fn match_count(&self) -> usize { - self.filtered_entries.len() - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context>) { - self.selected_index = ix.min(self.filtered_entries.len().saturating_sub(1)); - cx.notify(); - } - - fn can_select( - &mut self, - ix: usize, - _window: &mut Window, - _cx: &mut Context>, - ) -> bool { - match self.filtered_entries.get(ix) { - Some(AcpModelPickerEntry::Model(_)) => true, - Some(AcpModelPickerEntry::Separator(_)) | None => false, - } - } - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - "Select a model…".into() - } - - fn update_matches( - &mut self, - query: String, - window: &mut Window, - cx: &mut Context>, - ) -> Task<()> { - cx.spawn_in(window, async move |this, cx| { - let filtered_models = match this - .read_with(cx, |this, cx| { - this.delegate.models.clone().map(move |models| { - fuzzy_search(models, query, cx.background_executor().clone()) - }) - }) - .ok() - .flatten() - { - Some(task) => task.await, - None => AgentModelList::Flat(vec![]), - }; - - this.update_in(cx, |this, window, cx| { - this.delegate.filtered_entries = - info_list_to_picker_entries(filtered_models).collect(); - // Finds the currently selected model in the list - let new_index = this - .delegate - .selected_model - .as_ref() - .and_then(|selected| { - this.delegate.filtered_entries.iter().position(|entry| { - if let AcpModelPickerEntry::Model(model_info) = entry { - model_info.id == selected.id - } else { - false - } - }) - }) - .unwrap_or(0); - this.set_selected_index(new_index, Some(picker::Direction::Down), true, window, cx); - cx.notify(); - }) - .ok(); - }) - } - - fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context>) { - if let Some(AcpModelPickerEntry::Model(model_info)) = - self.filtered_entries.get(self.selected_index) - { - if window.modifiers().secondary() { - let default_model = self.agent_server.default_model(cx); - let is_default = default_model.as_ref() == Some(&model_info.id); - - self.agent_server.set_default_model( - if is_default { - None - } else { - Some(model_info.id.clone()) - }, - self.fs.clone(), - cx, - ); - } - - self.selector - .select_model(model_info.id.clone(), cx) - .detach_and_log_err(cx); - self.selected_model = Some(model_info.clone()); - let current_index = self.selected_index; - self.set_selected_index(current_index, window, cx); - - cx.emit(DismissEvent); - } - } - - fn dismissed(&mut self, window: &mut Window, cx: &mut Context>) { - cx.defer_in(window, |picker, window, cx| { - picker.set_query("", window, cx); - }); - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _: &mut Window, - cx: &mut Context>, - ) -> Option { - match self.filtered_entries.get(ix)? { - AcpModelPickerEntry::Separator(title) => Some( - div() - .px_2() - .pb_1() - .when(ix > 1, |this| { - this.mt_1() - .pt_2() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - }) - .child( - Label::new(title) - .size(LabelSize::XSmall) - .color(Color::Muted), - ) - .into_any_element(), - ), - AcpModelPickerEntry::Model(model_info) => { - let is_selected = Some(model_info) == self.selected_model.as_ref(); - let default_model = self.agent_server.default_model(cx); - let is_default = default_model.as_ref() == Some(&model_info.id); - - let model_icon_color = if is_selected { - Color::Accent - } else { - Color::Muted - }; - - Some( - div() - .id(("model-picker-menu-child", ix)) - .when_some(model_info.description.clone(), |this, description| { - this - .on_hover(cx.listener(move |menu, hovered, _, cx| { - if *hovered { - menu.delegate.selected_description = Some((ix, description.clone(), is_default)); - } else if matches!(menu.delegate.selected_description, Some((id, _, _)) if id == ix) { - menu.delegate.selected_description = None; - } - cx.notify(); - })) - }) - .child( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .child( - h_flex() - .w_full() - .gap_1p5() - .when_some(model_info.icon, |this, icon| { - this.child( - Icon::new(icon) - .color(model_icon_color) - .size(IconSize::Small) - ) - }) - .child(Label::new(model_info.name.clone()).truncate()), - ) - .end_slot(div().pr_3().when(is_selected, |this| { - this.child( - Icon::new(IconName::Check) - .color(Color::Accent) - .size(IconSize::Small), - ) - })), - ) - .into_any_element() - ) - } - } - } - - fn documentation_aside( - &self, - _window: &mut Window, - _cx: &mut Context>, - ) -> Option { - self.selected_description - .as_ref() - .map(|(_, description, is_default)| { - let description = description.clone(); - let is_default = *is_default; - - DocumentationAside::new( - DocumentationSide::Left, - DocumentationEdge::Top, - Rc::new(move |_| { - v_flex() - .gap_1() - .child(Label::new(description.clone())) - .child(HoldForDefault::new(is_default)) - .into_any_element() - }), - ) - }) - } - - fn render_footer( - &self, - _window: &mut Window, - cx: &mut Context>, - ) -> Option { - let focus_handle = self.focus_handle.clone(); - - if !self.selector.should_render_footer() { - return None; - } - - Some( - h_flex() - .w_full() - .p_1p5() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .child( - Button::new("configure", "Configure") - .full_width() - .style(ButtonStyle::Outlined) - .key_binding( - KeyBinding::for_action_in(&OpenSettings, &focus_handle, cx) - .map(|kb| kb.size(rems_from_px(12.))), - ) - .on_click(|_, window, cx| { - window.dispatch_action(OpenSettings.boxed_clone(), cx); - }), - ) - .into_any(), - ) - } -} - -fn info_list_to_picker_entries( - model_list: AgentModelList, -) -> impl Iterator { - match model_list { - AgentModelList::Flat(list) => { - itertools::Either::Left(list.into_iter().map(AcpModelPickerEntry::Model)) - } - AgentModelList::Grouped(index_map) => { - itertools::Either::Right(index_map.into_iter().flat_map(|(group_name, models)| { - std::iter::once(AcpModelPickerEntry::Separator(group_name.0)) - .chain(models.into_iter().map(AcpModelPickerEntry::Model)) - })) - } - } -} - -async fn fuzzy_search( - model_list: AgentModelList, - query: String, - executor: BackgroundExecutor, -) -> AgentModelList { - async fn fuzzy_search_list( - model_list: Vec, - query: &str, - executor: BackgroundExecutor, - ) -> Vec { - let candidates = model_list - .iter() - .enumerate() - .map(|(ix, model)| { - StringMatchCandidate::new(ix, &format!("{}/{}", model.id, model.name)) - }) - .collect::>(); - let mut matches = match_strings( - &candidates, - query, - false, - true, - 100, - &Default::default(), - executor, - ) - .await; - - matches.sort_unstable_by_key(|mat| { - let candidate = &candidates[mat.candidate_id]; - (Reverse(OrderedFloat(mat.score)), candidate.id) - }); - - matches - .into_iter() - .map(|mat| model_list[mat.candidate_id].clone()) - .collect() - } - - match model_list { - AgentModelList::Flat(model_list) => { - AgentModelList::Flat(fuzzy_search_list(model_list, &query, executor).await) - } - AgentModelList::Grouped(index_map) => { - let groups = - futures::future::join_all(index_map.into_iter().map(|(group_name, models)| { - fuzzy_search_list(models, &query, executor.clone()) - .map(|results| (group_name, results)) - })) - .await; - AgentModelList::Grouped(IndexMap::from_iter( - groups - .into_iter() - .filter(|(_, results)| !results.is_empty()), - )) - } - } -} - -#[cfg(test)] -mod tests { - use agent_client_protocol as acp; - use gpui::TestAppContext; - - use super::*; - - fn create_model_list(grouped_models: Vec<(&str, Vec<&str>)>) -> AgentModelList { - AgentModelList::Grouped(IndexMap::from_iter(grouped_models.into_iter().map( - |(group, models)| { - ( - acp_thread::AgentModelGroupName(group.to_string().into()), - models - .into_iter() - .map(|model| acp_thread::AgentModelInfo { - id: acp::ModelId::new(model.to_string()), - name: model.to_string().into(), - description: None, - icon: None, - }) - .collect::>(), - ) - }, - ))) - } - - fn assert_models_eq(result: AgentModelList, expected: Vec<(&str, Vec<&str>)>) { - let AgentModelList::Grouped(groups) = result else { - panic!("Expected LanguageModelInfoList::Grouped, got {:?}", result); - }; - - assert_eq!( - groups.len(), - expected.len(), - "Number of groups doesn't match" - ); - - for (i, (expected_group, expected_models)) in expected.iter().enumerate() { - let (actual_group, actual_models) = groups.get_index(i).unwrap(); - assert_eq!( - actual_group.0.as_ref(), - *expected_group, - "Group at position {} doesn't match expected group", - i - ); - assert_eq!( - actual_models.len(), - expected_models.len(), - "Number of models in group {} doesn't match", - expected_group - ); - - for (j, expected_model_name) in expected_models.iter().enumerate() { - assert_eq!( - actual_models[j].name, *expected_model_name, - "Model at position {} in group {} doesn't match expected model", - j, expected_group - ); - } - } - } - - #[gpui::test] - async fn test_fuzzy_match(cx: &mut TestAppContext) { - let models = create_model_list(vec![ - ( - "zed", - vec![ - "Claude 3.7 Sonnet", - "Claude 3.7 Sonnet Thinking", - "gpt-4.1", - "gpt-4.1-nano", - ], - ), - ("openai", vec!["gpt-3.5-turbo", "gpt-4.1", "gpt-4.1-nano"]), - ("ollama", vec!["mistral", "deepseek"]), - ]); - - // Results should preserve models order whenever possible. - // In the case below, `zed/gpt-4.1` and `openai/gpt-4.1` have identical - // similarity scores, but `zed/gpt-4.1` was higher in the models list, - // so it should appear first in the results. - let results = fuzzy_search(models.clone(), "41".into(), cx.executor()).await; - assert_models_eq( - results, - vec![ - ("zed", vec!["gpt-4.1", "gpt-4.1-nano"]), - ("openai", vec!["gpt-4.1", "gpt-4.1-nano"]), - ], - ); - - // Fuzzy search - let results = fuzzy_search(models.clone(), "4n".into(), cx.executor()).await; - assert_models_eq( - results, - vec![ - ("zed", vec!["gpt-4.1-nano"]), - ("openai", vec!["gpt-4.1-nano"]), - ], - ); - } -} diff --git a/crates/agent_ui/src/acp/model_selector_popover.rs b/crates/agent_ui/src/acp/model_selector_popover.rs deleted file mode 100644 index e2393c11bd..0000000000 --- a/crates/agent_ui/src/acp/model_selector_popover.rs +++ /dev/null @@ -1,100 +0,0 @@ -use std::rc::Rc; -use std::sync::Arc; - -use acp_thread::{AgentModelInfo, AgentModelSelector}; -use agent_servers::AgentServer; -use fs::Fs; -use gpui::{Entity, FocusHandle}; -use picker::popover_menu::PickerPopoverMenu; -use ui::{ - ButtonLike, Context, IntoElement, PopoverMenuHandle, SharedString, TintColor, Tooltip, Window, - prelude::*, -}; -use zed_actions::agent::ToggleModelSelector; - -use crate::acp::{AcpModelSelector, model_selector::acp_model_selector}; - -pub struct AcpModelSelectorPopover { - selector: Entity, - menu_handle: PopoverMenuHandle, - focus_handle: FocusHandle, -} - -impl AcpModelSelectorPopover { - pub(crate) fn new( - selector: Rc, - agent_server: Rc, - fs: Arc, - menu_handle: PopoverMenuHandle, - focus_handle: FocusHandle, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let focus_handle_clone = focus_handle.clone(); - Self { - selector: cx.new(move |cx| { - acp_model_selector( - selector, - agent_server, - fs, - focus_handle_clone.clone(), - window, - cx, - ) - }), - menu_handle, - focus_handle, - } - } - - pub fn toggle(&self, window: &mut Window, cx: &mut Context) { - self.menu_handle.toggle(window, cx); - } - - pub fn active_model<'a>(&self, cx: &'a App) -> Option<&'a AgentModelInfo> { - self.selector.read(cx).delegate.active_model() - } -} - -impl Render for AcpModelSelectorPopover { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let model = self.selector.read(cx).delegate.active_model(); - let model_name = model - .as_ref() - .map(|model| model.name.clone()) - .unwrap_or_else(|| SharedString::from("Select a Model")); - - let model_icon = model.as_ref().and_then(|model| model.icon); - - let focus_handle = self.focus_handle.clone(); - - let (color, icon) = if self.menu_handle.is_deployed() { - (Color::Accent, IconName::ChevronUp) - } else { - (Color::Muted, IconName::ChevronDown) - }; - - PickerPopoverMenu::new( - self.selector.clone(), - ButtonLike::new("active-model") - .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - .when_some(model_icon, |this, icon| { - this.child(Icon::new(icon).color(color).size(IconSize::XSmall)) - }) - .child( - Label::new(model_name) - .color(color) - .size(LabelSize::Small) - .ml_0p5(), - ) - .child(Icon::new(icon).color(Color::Muted).size(IconSize::XSmall)), - move |_window, cx| { - Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx) - }, - gpui::Corner::BottomRight, - cx, - ) - .with_handle(self.menu_handle.clone()) - .render(window, cx) - } -} diff --git a/crates/agent_ui/src/acp/thread_history.rs b/crates/agent_ui/src/acp/thread_history.rs deleted file mode 100644 index 1aa89b35d3..0000000000 --- a/crates/agent_ui/src/acp/thread_history.rs +++ /dev/null @@ -1,861 +0,0 @@ -use crate::acp::AcpThreadView; -use crate::{AgentPanel, RemoveHistory, RemoveSelectedThread}; -use agent::{HistoryEntry, HistoryStore}; -use chrono::{Datelike as _, Local, NaiveDate, TimeDelta}; -use editor::{Editor, EditorEvent}; -use fuzzy::StringMatchCandidate; -use gpui::{ - App, Entity, EventEmitter, FocusHandle, Focusable, ScrollStrategy, Task, - UniformListScrollHandle, WeakEntity, Window, uniform_list, -}; -use std::{fmt::Display, ops::Range}; -use text::Bias; -use time::{OffsetDateTime, UtcOffset}; -use ui::{ - HighlightedLabel, IconButtonShape, ListItem, ListItemSpacing, Tab, Tooltip, WithScrollbar, - prelude::*, -}; - -pub struct AcpThreadHistory { - pub(crate) history_store: Entity, - scroll_handle: UniformListScrollHandle, - selected_index: usize, - hovered_index: Option, - search_editor: Entity, - search_query: SharedString, - visible_items: Vec, - local_timezone: UtcOffset, - confirming_delete_history: bool, - _update_task: Task<()>, - _subscriptions: Vec, -} - -enum ListItemType { - BucketSeparator(TimeBucket), - Entry { - entry: HistoryEntry, - format: EntryTimeFormat, - }, - SearchResult { - entry: HistoryEntry, - positions: Vec, - }, -} - -impl ListItemType { - fn history_entry(&self) -> Option<&HistoryEntry> { - match self { - ListItemType::Entry { entry, .. } => Some(entry), - ListItemType::SearchResult { entry, .. } => Some(entry), - _ => None, - } - } -} - -pub enum ThreadHistoryEvent { - Open(HistoryEntry), -} - -impl EventEmitter for AcpThreadHistory {} - -impl AcpThreadHistory { - pub(crate) fn new( - history_store: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let search_editor = cx.new(|cx| { - let mut editor = Editor::single_line(window, cx); - editor.set_placeholder_text("Search threads...", window, cx); - editor - }); - - let search_editor_subscription = - 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); - } - } - }); - - let history_store_subscription = cx.observe(&history_store, |this, _, cx| { - this.update_visible_items(true, cx); - }); - - let scroll_handle = UniformListScrollHandle::default(); - - let mut this = Self { - history_store, - scroll_handle, - selected_index: 0, - hovered_index: None, - visible_items: Default::default(), - search_editor, - local_timezone: UtcOffset::from_whole_seconds( - chrono::Local::now().offset().local_minus_utc(), - ) - .unwrap(), - search_query: SharedString::default(), - confirming_delete_history: false, - _subscriptions: vec![search_editor_subscription, history_store_subscription], - _update_task: Task::ready(()), - }; - this.update_visible_items(false, cx); - this - } - - fn update_visible_items(&mut self, preserve_selected_item: bool, cx: &mut Context) { - let entries = 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 - }; - - 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 - }; - - this.visible_items = new_visible_items; - this.set_selected_index(new_selected_index, Bias::Right, cx); - cx.notify(); - }) - .ok(); - }); - } - - fn add_list_separators(&self, entries: Vec, cx: &App) -> Task> { - cx.background_spawn(async move { - let mut items = Vec::with_capacity(entries.len() + 1); - let mut bucket = None; - let today = Local::now().naive_local().date(); - - for entry in entries.into_iter() { - let entry_date = entry - .updated_at() - .with_timezone(&Local) - .naive_local() - .date(); - let entry_bucket = TimeBucket::from_dates(today, entry_date); - - if Some(entry_bucket) != bucket { - bucket = Some(entry_bucket); - items.push(ListItemType::BucketSeparator(entry_bucket)); - } - - items.push(ListItemType::Entry { - entry, - format: entry_bucket.into(), - }); - } - items - }) - } - - fn filter_search_results( - &self, - entries: Vec, - cx: &App, - ) -> Task> { - let query = self.search_query.clone(); - cx.background_spawn({ - let executor = cx.background_executor().clone(); - async move { - let mut candidates = Vec::with_capacity(entries.len()); - - for (idx, entry) in entries.iter().enumerate() { - candidates.push(StringMatchCandidate::new(idx, entry.title())); - } - - const MAX_MATCHES: usize = 100; - - let matches = fuzzy::match_strings( - &candidates, - &query, - false, - true, - MAX_MATCHES, - &Default::default(), - executor, - ) - .await; - - matches - .into_iter() - .map(|search_match| ListItemType::SearchResult { - entry: entries[search_match.candidate_id].clone(), - positions: search_match.positions, - }) - .collect() - } - }) - } - - 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; - } - while matches!( - self.visible_items.get(index), - None | Some(ListItemType::BucketSeparator(..)) - ) { - index = match bias { - Bias::Left => { - if index == 0 { - self.visible_items.len() - 1 - } else { - index - 1 - } - } - Bias::Right => { - if index >= self.visible_items.len() - 1 { - 0 - } else { - index + 1 - } - } - }; - } - self.selected_index = index; - self.scroll_handle - .scroll_to_item(index, ScrollStrategy::Top); - cx.notify() - } - - pub fn select_previous( - &mut self, - _: &menu::SelectPrevious, - _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); - } - } - - pub fn select_next( - &mut self, - _: &menu::SelectNext, - _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); - } - } - - fn select_first( - &mut self, - _: &menu::SelectFirst, - _window: &mut Window, - cx: &mut Context, - ) { - self.set_selected_index(0, Bias::Right, 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); - } - - 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_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 task = match entry { - HistoryEntry::AcpThread(thread) => self - .history_store - .update(cx, |this, cx| this.delete_thread(thread.id.clone(), cx)), - HistoryEntry::TextThread(text_thread) => self.history_store.update(cx, |this, cx| { - this.delete_text_thread(text_thread.path.clone(), cx) - }), - }; - task.detach_and_log_err(cx); - } - - fn remove_history(&mut self, _window: &mut Window, cx: &mut Context) { - self.history_store.update(cx, |store, cx| { - store.delete_threads(cx).detach_and_log_err(cx) - }); - self.confirming_delete_history = false; - cx.notify(); - } - - fn prompt_delete_history(&mut self, _window: &mut Window, cx: &mut Context) { - self.confirming_delete_history = true; - cx.notify(); - } - - fn cancel_delete_history(&mut self, _window: &mut Window, cx: &mut Context) { - self.confirming_delete_history = false; - cx.notify(); - } - - fn render_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() - } - - fn render_list_item(&self, item: &ListItemType, ix: usize, 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::BucketSeparator(bucket) => div() - .px(DynamicSpacing::Base06.rems(cx)) - .pt_2() - .pb_1() - .child( - Label::new(bucket.to_string()) - .size(LabelSize::XSmall) - .color(Color::Muted), - ) - .into_any_element(), - } - } - - fn render_history_entry( - &self, - entry: &HistoryEntry, - format: EntryTimeFormat, - ix: usize, - highlight_positions: Vec, - cx: &Context, - ) -> AnyElement { - let selected = ix == self.selected_index; - let hovered = Some(ix) == self.hovered_index; - let timestamp = entry.updated_at().timestamp(); - let thread_timestamp = format.format_timestamp(timestamp, self.local_timezone); - - h_flex() - .w_full() - .pb_1() - .child( - ListItem::new(ix) - .rounded() - .toggle_state(selected) - .spacing(ListItemSpacing::Sparse) - .start_slot( - h_flex() - .w_full() - .gap_2() - .justify_between() - .child( - HighlightedLabel::new(entry.title(), highlight_positions) - .size(LabelSize::Small) - .truncate(), - ) - .child( - Label::new(thread_timestamp) - .color(Color::Muted) - .size(LabelSize::XSmall), - ), - ) - .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 = None; - } - - cx.notify(); - })) - .end_slot::(if hovered { - Some( - IconButton::new("delete", IconName::Trash) - .shape(IconButtonShape::Square) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .tooltip(move |_window, cx| { - Tooltip::for_action("Delete", &RemoveSelectedThread, cx) - }) - .on_click(cx.listener(move |this, _, _, cx| { - this.remove_thread(ix, cx); - cx.stop_propagation() - })), - ) - } else { - None - }) - .on_click(cx.listener(move |this, _, _, cx| this.confirm_entry(ix, cx))), - ) - .into_any_element() - } -} - -impl Focusable for AcpThreadHistory { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.search_editor.focus_handle(cx) - } -} - -impl Render for AcpThreadHistory { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let has_no_history = self.history_store.read(cx).is_empty(cx); - - v_flex() - .key_context("ThreadHistory") - .size_full() - .bg(cx.theme().colors().panel_background) - .on_action(cx.listener(Self::select_previous)) - .on_action(cx.listener(Self::select_next)) - .on_action(cx.listener(Self::select_first)) - .on_action(cx.listener(Self::select_last)) - .on_action(cx.listener(Self::confirm)) - .on_action(cx.listener(Self::remove_selected_thread)) - .on_action(cx.listener(|this, _: &RemoveHistory, window, cx| { - this.remove_history(window, cx); - })) - .child( - h_flex() - .h(Tab::container_height(cx)) - .w_full() - .py_1() - .px_2() - .gap_2() - .justify_between() - .border_b_1() - .border_color(cx.theme().colors().border) - .child( - Icon::new(IconName::MagnifyingGlass) - .color(Color::Muted) - .size(IconSize::Small), - ) - .child(self.search_editor.clone()), - ) - .child({ - let view = v_flex() - .id("list-container") - .relative() - .overflow_hidden() - .flex_grow(); - - if has_no_history { - view.justify_center().items_center().child( - Label::new("You don't have any past threads yet.") - .size(LabelSize::Small) - .color(Color::Muted), - ) - } else if self.search_produced_no_matches() { - view.justify_center() - .items_center() - .child(Label::new("No threads match your search.").size(LabelSize::Small)) - } else { - view.child( - uniform_list( - "thread-history", - self.visible_items.len(), - cx.processor(|this, range: Range, window, cx| { - this.render_list_items(range, window, cx) - }), - ) - .p_1() - .pr_4() - .track_scroll(&self.scroll_handle) - .flex_grow(), - ) - .vertical_scrollbar_for(&self.scroll_handle, window, cx) - } - }) - .when(!has_no_history, |this| { - this.child( - h_flex() - .p_2() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .when(!self.confirming_delete_history, |this| { - this.child( - Button::new("delete_history", "Delete All History") - .full_width() - .style(ButtonStyle::Outlined) - .label_size(LabelSize::Small) - .on_click(cx.listener(|this, _, window, cx| { - this.prompt_delete_history(window, cx); - })), - ) - }) - .when(self.confirming_delete_history, |this| { - this.w_full() - .gap_2() - .flex_wrap() - .justify_between() - .child( - h_flex() - .flex_wrap() - .gap_1() - .child( - Label::new("Delete all threads?") - .size(LabelSize::Small), - ) - .child( - Label::new("You won't be able to recover them later.") - .size(LabelSize::Small) - .color(Color::Muted), - ), - ) - .child( - h_flex() - .gap_1() - .child( - Button::new("cancel_delete", "Cancel") - .label_size(LabelSize::Small) - .on_click(cx.listener(|this, _, window, cx| { - this.cancel_delete_history(window, cx); - })), - ) - .child( - Button::new("confirm_delete", "Delete") - .style(ButtonStyle::Tinted(ui::TintColor::Error)) - .color(Color::Error) - .label_size(LabelSize::Small) - .on_click(cx.listener(|_, _, window, cx| { - window.dispatch_action( - Box::new(RemoveHistory), - cx, - ); - })), - ), - ) - }), - ) - }) - } -} - -#[derive(IntoElement)] -pub struct AcpHistoryEntryElement { - entry: HistoryEntry, - thread_view: WeakEntity, - selected: bool, - hovered: bool, - on_hover: Box, -} - -impl AcpHistoryEntryElement { - pub fn new(entry: HistoryEntry, thread_view: WeakEntity) -> Self { - Self { - entry, - thread_view, - selected: false, - hovered: false, - on_hover: Box::new(|_, _, _| {}), - } - } - - pub fn hovered(mut self, hovered: bool) -> Self { - self.hovered = hovered; - self - } - - pub fn on_hover(mut self, on_hover: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self { - self.on_hover = Box::new(on_hover); - self - } -} - -impl RenderOnce for AcpHistoryEntryElement { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let id = self.entry.id(); - let title = self.entry.title(); - let timestamp = self.entry.updated_at(); - - let formatted_time = { - let now = chrono::Utc::now(); - let duration = now.signed_duration_since(timestamp); - - if duration.num_days() > 0 { - format!("{}d", duration.num_days()) - } else if duration.num_hours() > 0 { - format!("{}h ago", duration.num_hours()) - } else if duration.num_minutes() > 0 { - format!("{}m ago", duration.num_minutes()) - } else { - "Just now".to_string() - } - }; - - ListItem::new(id) - .rounded() - .toggle_state(self.selected) - .spacing(ListItemSpacing::Sparse) - .start_slot( - h_flex() - .w_full() - .gap_2() - .justify_between() - .child(Label::new(title).size(LabelSize::Small).truncate()) - .child( - Label::new(formatted_time) - .color(Color::Muted) - .size(LabelSize::XSmall), - ), - ) - .on_hover(self.on_hover) - .end_slot::(if self.hovered || self.selected { - Some( - IconButton::new("delete", IconName::Trash) - .shape(IconButtonShape::Square) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .tooltip(move |_window, cx| { - Tooltip::for_action("Delete", &RemoveSelectedThread, cx) - }) - .on_click({ - let thread_view = self.thread_view.clone(); - let entry = self.entry.clone(); - - move |_event, _window, cx| { - if let Some(thread_view) = thread_view.upgrade() { - thread_view.update(cx, |thread_view, cx| { - thread_view.delete_history_entry(entry.clone(), cx); - }); - } - } - }), - ) - } else { - None - }) - .on_click({ - let thread_view = self.thread_view.clone(); - let entry = self.entry; - - move |_event, window, cx| { - if let Some(workspace) = thread_view - .upgrade() - .and_then(|view| view.read(cx).workspace().upgrade()) - { - match &entry { - HistoryEntry::AcpThread(thread_metadata) => { - if let Some(panel) = workspace.read(cx).panel::(cx) { - panel.update(cx, |panel, cx| { - panel.load_agent_thread( - thread_metadata.clone(), - window, - cx, - ); - }); - } - } - HistoryEntry::TextThread(text_thread) => { - if let Some(panel) = workspace.read(cx).panel::(cx) { - panel.update(cx, |panel, cx| { - panel - .open_saved_text_thread( - text_thread.path.clone(), - window, - cx, - ) - .detach_and_log_err(cx); - }); - } - } - } - } - } - }) - } -} - -#[derive(Clone, Copy)] -pub enum EntryTimeFormat { - DateAndTime, - TimeOnly, -} - -impl EntryTimeFormat { - fn format_timestamp(&self, timestamp: i64, timezone: UtcOffset) -> String { - let timestamp = OffsetDateTime::from_unix_timestamp(timestamp).unwrap(); - - match self { - EntryTimeFormat::DateAndTime => time_format::format_localized_timestamp( - timestamp, - OffsetDateTime::now_utc(), - timezone, - time_format::TimestampFormat::EnhancedAbsolute, - ), - EntryTimeFormat::TimeOnly => time_format::format_time(timestamp.to_offset(timezone)), - } - } -} - -impl From for EntryTimeFormat { - fn from(bucket: TimeBucket) -> Self { - match bucket { - TimeBucket::Today => EntryTimeFormat::TimeOnly, - TimeBucket::Yesterday => EntryTimeFormat::TimeOnly, - TimeBucket::ThisWeek => EntryTimeFormat::DateAndTime, - TimeBucket::PastWeek => EntryTimeFormat::DateAndTime, - TimeBucket::All => EntryTimeFormat::DateAndTime, - } - } -} - -#[derive(PartialEq, Eq, Clone, Copy, Debug)] -enum TimeBucket { - Today, - Yesterday, - ThisWeek, - PastWeek, - All, -} - -impl TimeBucket { - fn from_dates(reference: NaiveDate, date: NaiveDate) -> Self { - if date == reference { - return TimeBucket::Today; - } - - if date == reference - TimeDelta::days(1) { - return TimeBucket::Yesterday; - } - - let week = date.iso_week(); - - if reference.iso_week() == week { - return TimeBucket::ThisWeek; - } - - let last_week = (reference - TimeDelta::days(7)).iso_week(); - - if week == last_week { - return TimeBucket::PastWeek; - } - - TimeBucket::All - } -} - -impl Display for TimeBucket { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - TimeBucket::Today => write!(f, "Today"), - TimeBucket::Yesterday => write!(f, "Yesterday"), - TimeBucket::ThisWeek => write!(f, "This Week"), - TimeBucket::PastWeek => write!(f, "Past Week"), - TimeBucket::All => write!(f, "All"), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use chrono::NaiveDate; - - #[test] - fn test_time_bucket_from_dates() { - let today = NaiveDate::from_ymd_opt(2023, 1, 15).unwrap(); - - let date = today; - assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::Today); - - let date = NaiveDate::from_ymd_opt(2023, 1, 14).unwrap(); - assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::Yesterday); - - let date = NaiveDate::from_ymd_opt(2023, 1, 13).unwrap(); - assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::ThisWeek); - - let date = NaiveDate::from_ymd_opt(2023, 1, 11).unwrap(); - assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::ThisWeek); - - let date = NaiveDate::from_ymd_opt(2023, 1, 8).unwrap(); - assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::PastWeek); - - let date = NaiveDate::from_ymd_opt(2023, 1, 5).unwrap(); - assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::PastWeek); - - // All: not in this week or last week - let date = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap(); - assert_eq!(TimeBucket::from_dates(today, date), TimeBucket::All); - - // Test year boundary cases - let new_year = NaiveDate::from_ymd_opt(2023, 1, 1).unwrap(); - - let date = NaiveDate::from_ymd_opt(2022, 12, 31).unwrap(); - assert_eq!( - TimeBucket::from_dates(new_year, date), - TimeBucket::Yesterday - ); - - let date = NaiveDate::from_ymd_opt(2022, 12, 28).unwrap(); - assert_eq!(TimeBucket::from_dates(new_year, date), TimeBucket::ThisWeek); - } -} diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs deleted file mode 100644 index 63ea9eb279..0000000000 --- a/crates/agent_ui/src/acp/thread_view.rs +++ /dev/null @@ -1,7296 +0,0 @@ -use acp_thread::{ - AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk, - AuthRequired, LoadError, MentionUri, RetryStatus, ThreadStatus, ToolCall, ToolCallContent, - ToolCallStatus, UserMessageId, -}; -use acp_thread::{AgentConnection, Plan}; -use action_log::{ActionLog, ActionLogTelemetry}; -use agent::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore, NativeAgentServer}; -use agent_client_protocol::{self as acp, PromptCapabilities}; -use agent_servers::{AgentServer, AgentServerDelegate}; -use agent_settings::{AgentProfileId, AgentSettings, CompletionMode}; -use anyhow::{Result, anyhow}; -use arrayvec::ArrayVec; -use audio::{Audio, Sound}; -use buffer_diff::BufferDiff; -use client::zed_urls; -use cloud_llm_client::PlanV1; -use collections::{HashMap, HashSet}; -use editor::scroll::Autoscroll; -use editor::{ - Editor, EditorEvent, EditorMode, MultiBuffer, PathKey, SelectionEffects, SizingBehavior, -}; -use file_icons::FileIcons; -use fs::Fs; -use futures::FutureExt as _; -use gpui::{ - Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem, - CursorStyle, EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, - ListOffset, ListState, PlatformDisplay, SharedString, StyleRefinement, Subscription, Task, - TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, Window, WindowHandle, div, - ease_in_out, linear_color_stop, linear_gradient, list, point, pulsating_between, -}; -use language::Buffer; - -use language_model::LanguageModelRegistry; -use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle}; -use project::{Project, ProjectEntryId}; -use prompt_store::{PromptId, PromptStore}; -use rope::Point; -use settings::{NotifyWhenAgentWaiting, Settings as _, SettingsStore}; -use std::cell::RefCell; -use std::path::Path; -use std::sync::Arc; -use std::time::Instant; -use std::{collections::BTreeMap, rc::Rc, time::Duration}; -use terminal_view::terminal_panel::TerminalPanel; -use text::Anchor; -use theme::{AgentFontSize, ThemeSettings}; -use ui::{ - Callout, CommonAnimationExt, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, - PopoverMenuHandle, SpinnerLabel, TintColor, Tooltip, WithScrollbar, prelude::*, -}; -use util::{ResultExt, size::format_file_size, time::duration_alt_display}; -use workspace::{CollaboratorId, NewTerminal, Workspace}; -use zed_actions::agent::{Chat, ToggleModelSelector}; -use zed_actions::assistant::OpenRulesLibrary; - -use super::entry_view_state::EntryViewState; -use crate::acp::AcpModelSelectorPopover; -use crate::acp::ModeSelector; -use crate::acp::entry_view_state::{EntryViewEvent, ViewEvent}; -use crate::acp::message_editor::{MessageEditor, MessageEditorEvent}; -use crate::agent_diff::AgentDiff; -use crate::profile_selector::{ProfileProvider, ProfileSelector}; - -use crate::ui::{ - AgentNotification, AgentNotificationEvent, BurnModeTooltip, UnavailableEditingTooltip, - UsageCallout, -}; -use crate::{ - AgentDiffPane, AgentPanel, AllowAlways, AllowOnce, ContinueThread, ContinueWithBurnMode, - CycleModeSelector, ExpandMessageEditor, Follow, KeepAll, NewThread, OpenAgentDiff, OpenHistory, - RejectAll, RejectOnce, ToggleBurnMode, ToggleProfileSelector, -}; - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -enum ThreadFeedback { - Positive, - Negative, -} - -#[derive(Debug)] -enum ThreadError { - PaymentRequired, - ModelRequestLimitReached(cloud_llm_client::Plan), - ToolUseLimitReached, - Refusal, - AuthenticationRequired(SharedString), - Other(SharedString), -} - -impl ThreadError { - fn from_err(error: anyhow::Error, agent: &Rc) -> Self { - if error.is::() { - Self::PaymentRequired - } else if error.is::() { - Self::ToolUseLimitReached - } else if let Some(error) = - error.downcast_ref::() - { - Self::ModelRequestLimitReached(error.plan) - } else if let Some(acp_error) = error.downcast_ref::() - && acp_error.code == acp::ErrorCode::AuthRequired - { - Self::AuthenticationRequired(acp_error.message.clone().into()) - } else { - let string = format!("{:#}", error); - // TODO: we should have Gemini return better errors here. - if agent.clone().downcast::().is_some() - && string.contains("Could not load the default credentials") - || string.contains("API key not valid") - || string.contains("Request had invalid authentication credentials") - { - Self::AuthenticationRequired(string.into()) - } else { - Self::Other(string.into()) - } - } - } -} - -impl ProfileProvider for Entity { - fn profile_id(&self, cx: &App) -> AgentProfileId { - self.read(cx).profile().clone() - } - - fn set_profile(&self, profile_id: AgentProfileId, cx: &mut App) { - self.update(cx, |thread, cx| { - // Apply the profile and let the thread swap to its default model. - thread.set_profile(profile_id, cx); - }); - } - - fn profiles_supported(&self, cx: &App) -> bool { - self.read(cx) - .model() - .is_some_and(|model| model.supports_tools()) - } -} - -#[derive(Default)] -struct ThreadFeedbackState { - feedback: Option, - comments_editor: Option>, -} - -impl ThreadFeedbackState { - pub fn submit( - &mut self, - thread: Entity, - feedback: ThreadFeedback, - window: &mut Window, - cx: &mut App, - ) { - let Some(telemetry) = thread.read(cx).connection().telemetry() else { - return; - }; - - if self.feedback == Some(feedback) { - return; - } - - self.feedback = Some(feedback); - match feedback { - ThreadFeedback::Positive => { - self.comments_editor = None; - } - ThreadFeedback::Negative => { - self.comments_editor = Some(Self::build_feedback_comments_editor(window, cx)); - } - } - let session_id = thread.read(cx).session_id().clone(); - let agent_telemetry_id = thread.read(cx).connection().telemetry_id(); - let task = telemetry.thread_data(&session_id, cx); - let rating = match feedback { - ThreadFeedback::Positive => "positive", - ThreadFeedback::Negative => "negative", - }; - cx.background_spawn(async move { - let thread = task.await?; - telemetry::event!( - "Agent Thread Rated", - agent = agent_telemetry_id, - session_id = session_id, - rating = rating, - thread = thread - ); - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - - pub fn submit_comments(&mut self, thread: Entity, cx: &mut App) { - let Some(telemetry) = thread.read(cx).connection().telemetry() else { - return; - }; - - let Some(comments) = self - .comments_editor - .as_ref() - .map(|editor| editor.read(cx).text(cx)) - .filter(|text| !text.trim().is_empty()) - else { - return; - }; - - self.comments_editor.take(); - - let session_id = thread.read(cx).session_id().clone(); - let agent_telemetry_id = thread.read(cx).connection().telemetry_id(); - let task = telemetry.thread_data(&session_id, cx); - cx.background_spawn(async move { - let thread = task.await?; - telemetry::event!( - "Agent Thread Feedback Comments", - agent = agent_telemetry_id, - session_id = session_id, - comments = comments, - thread = thread - ); - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - - pub fn clear(&mut self) { - *self = Self::default() - } - - pub fn dismiss_comments(&mut self) { - self.comments_editor.take(); - } - - fn build_feedback_comments_editor(window: &mut Window, cx: &mut App) -> Entity { - let buffer = cx.new(|cx| { - let empty_string = String::new(); - MultiBuffer::singleton(cx.new(|cx| Buffer::local(empty_string, cx)), cx) - }); - - let editor = cx.new(|cx| { - let mut editor = Editor::new( - editor::EditorMode::AutoHeight { - min_lines: 1, - max_lines: Some(4), - }, - buffer, - None, - window, - cx, - ); - editor.set_placeholder_text( - "What went wrong? Share your feedback so we can improve.", - window, - cx, - ); - editor - }); - - editor.read(cx).focus_handle(cx).focus(window); - editor - } -} - -pub struct AcpThreadView { - agent: Rc, - workspace: WeakEntity, - project: Entity, - thread_state: ThreadState, - login: Option, - history_store: Entity, - hovered_recent_history_item: Option, - entry_view_state: Entity, - message_editor: Entity, - focus_handle: FocusHandle, - model_selector: Option>, - profile_selector: Option>, - notifications: Vec>, - notification_subscriptions: HashMap, Vec>, - thread_retry_status: Option, - thread_error: Option, - thread_error_markdown: Option>, - thread_feedback: ThreadFeedbackState, - list_state: ListState, - auth_task: Option>, - expanded_tool_calls: HashSet, - expanded_thinking_blocks: HashSet<(usize, usize)>, - edits_expanded: bool, - plan_expanded: bool, - editor_expanded: bool, - should_be_following: bool, - editing_message: Option, - prompt_capabilities: Rc>, - available_commands: Rc>>, - is_loading_contents: bool, - new_server_version_available: Option, - resume_thread_metadata: Option, - _cancel_task: Option>, - _subscriptions: [Subscription; 5], - show_codex_windows_warning: bool, - in_flight_prompt: Option>, -} - -enum ThreadState { - Loading(Entity), - Ready { - thread: Entity, - title_editor: Option>, - mode_selector: Option>, - _subscriptions: Vec, - }, - LoadError(LoadError), - Unauthenticated { - connection: Rc, - description: Option>, - configuration_view: Option, - pending_auth_method: Option, - _subscription: Option, - }, -} - -struct LoadingView { - title: SharedString, - _load_task: Task<()>, - _update_title_task: Task>, -} - -impl AcpThreadView { - pub fn new( - agent: Rc, - resume_thread: Option, - summarize_thread: Option, - workspace: WeakEntity, - project: Entity, - history_store: Entity, - prompt_store: Option>, - track_load_event: bool, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default())); - let available_commands = Rc::new(RefCell::new(vec![])); - - let placeholder = placeholder_text(agent.name().as_ref(), false); - - let message_editor = cx.new(|cx| { - let mut editor = MessageEditor::new( - workspace.clone(), - project.downgrade(), - history_store.clone(), - prompt_store.clone(), - prompt_capabilities.clone(), - available_commands.clone(), - agent.name(), - &placeholder, - editor::EditorMode::AutoHeight { - min_lines: AgentSettings::get_global(cx).message_editor_min_lines, - max_lines: Some(AgentSettings::get_global(cx).set_message_editor_max_lines()), - }, - window, - cx, - ); - if let Some(entry) = summarize_thread { - editor.insert_thread_summary(entry, window, cx); - } - editor - }); - - let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0)); - - let entry_view_state = cx.new(|_| { - EntryViewState::new( - workspace.clone(), - project.downgrade(), - history_store.clone(), - prompt_store.clone(), - prompt_capabilities.clone(), - available_commands.clone(), - agent.name(), - ) - }); - - let agent_server_store = project.read(cx).agent_server_store().clone(); - let subscriptions = [ - cx.observe_global_in::(window, Self::agent_ui_font_size_changed), - cx.observe_global_in::(window, Self::agent_ui_font_size_changed), - cx.subscribe_in(&message_editor, window, Self::handle_message_editor_event), - cx.subscribe_in(&entry_view_state, window, Self::handle_entry_view_event), - cx.subscribe_in( - &agent_server_store, - window, - Self::handle_agent_servers_updated, - ), - ]; - - let show_codex_windows_warning = cfg!(windows) - && project.read(cx).is_local() - && agent.clone().downcast::().is_some(); - - Self { - agent: agent.clone(), - workspace: workspace.clone(), - project: project.clone(), - entry_view_state, - thread_state: Self::initial_state( - agent.clone(), - resume_thread.clone(), - workspace.clone(), - project.clone(), - track_load_event, - window, - cx, - ), - login: None, - message_editor, - model_selector: None, - profile_selector: None, - - notifications: Vec::new(), - notification_subscriptions: HashMap::default(), - list_state: list_state, - thread_retry_status: None, - thread_error: None, - thread_error_markdown: None, - thread_feedback: Default::default(), - auth_task: None, - expanded_tool_calls: HashSet::default(), - expanded_thinking_blocks: HashSet::default(), - editing_message: None, - edits_expanded: false, - plan_expanded: false, - prompt_capabilities, - available_commands, - editor_expanded: false, - should_be_following: false, - history_store, - hovered_recent_history_item: None, - is_loading_contents: false, - _subscriptions: subscriptions, - _cancel_task: None, - focus_handle: cx.focus_handle(), - new_server_version_available: None, - resume_thread_metadata: resume_thread, - show_codex_windows_warning, - in_flight_prompt: None, - } - } - - fn reset(&mut self, window: &mut Window, cx: &mut Context) { - self.thread_state = Self::initial_state( - self.agent.clone(), - self.resume_thread_metadata.clone(), - self.workspace.clone(), - self.project.clone(), - true, - window, - cx, - ); - self.available_commands.replace(vec![]); - self.new_server_version_available.take(); - cx.notify(); - } - - fn initial_state( - agent: Rc, - resume_thread: Option, - workspace: WeakEntity, - project: Entity, - track_load_event: bool, - window: &mut Window, - cx: &mut Context, - ) -> ThreadState { - if project.read(cx).is_via_collab() - && agent.clone().downcast::().is_none() - { - return ThreadState::LoadError(LoadError::Other( - "External agents are not yet supported in shared projects.".into(), - )); - } - let mut worktrees = project.read(cx).visible_worktrees(cx).collect::>(); - // Pick the first non-single-file worktree for the root directory if there are any, - // and otherwise the parent of a single-file worktree, falling back to $HOME if there are no visible worktrees. - worktrees.sort_by(|l, r| { - l.read(cx) - .is_single_file() - .cmp(&r.read(cx).is_single_file()) - }); - let root_dir = worktrees - .into_iter() - .filter_map(|worktree| { - if worktree.read(cx).is_single_file() { - Some(worktree.read(cx).abs_path().parent()?.into()) - } else { - Some(worktree.read(cx).abs_path()) - } - }) - .next(); - let (status_tx, mut status_rx) = watch::channel("Loading…".into()); - let (new_version_available_tx, mut new_version_available_rx) = watch::channel(None); - let delegate = AgentServerDelegate::new( - project.read(cx).agent_server_store().clone(), - project.clone(), - Some(status_tx), - Some(new_version_available_tx), - ); - - let connect_task = agent.connect(root_dir.as_deref(), delegate, cx); - let load_task = cx.spawn_in(window, async move |this, cx| { - let connection = match connect_task.await { - Ok((connection, login)) => { - this.update(cx, |this, _| this.login = login).ok(); - 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); - } - cx.notify(); - }) - .log_err(); - return; - } - }; - - if track_load_event { - telemetry::event!("Agent Thread Started", agent = connection.telemetry_id()); - } - - let result = if let Some(native_agent) = connection - .clone() - .downcast::() - && let Some(resume) = resume_thread.clone() - { - cx.update(|_, cx| { - native_agent - .0 - .update(cx, |agent, cx| agent.open_thread(resume.id, cx)) - }) - .log_err() - } else { - let root_dir = root_dir.unwrap_or(paths::home_dir().as_path().into()); - cx.update(|_, cx| { - connection - .clone() - .new_thread(project.clone(), &root_dir, cx) - }) - .log_err() - }; - - let Some(result) = result else { - return; - }; - - let result = match result.await { - Err(e) => match e.downcast::() { - Ok(err) => { - cx.update(|window, cx| { - Self::handle_auth_required(this, err, agent, connection, window, cx) - }) - .log_err(); - return; - } - Err(err) => Err(err), - }, - Ok(thread) => Ok(thread), - }; - - this.update_in(cx, |this, window, cx| { - match result { - Ok(thread) => { - let action_log = thread.read(cx).action_log().clone(); - - this.prompt_capabilities - .replace(thread.read(cx).prompt_capabilities()); - - let count = thread.read(cx).entries().len(); - this.entry_view_state.update(cx, |view_state, cx| { - for ix in 0..count { - view_state.sync_entry(ix, &thread, window, cx); - } - this.list_state.splice_focusable( - 0..0, - (0..count).map(|ix| view_state.entry(ix)?.focus_handle(cx)), - ); - }); - - if let Some(resume) = resume_thread { - this.history_store.update(cx, |history, cx| { - history.push_recently_opened_entry( - HistoryEntryId::AcpThread(resume.id), - cx, - ); - }); - } - - AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx); - - this.model_selector = thread - .read(cx) - .connection() - .model_selector(thread.read(cx).session_id()) - .map(|selector| { - let agent_server = this.agent.clone(); - let fs = this.project.read(cx).fs().clone(); - cx.new(|cx| { - AcpModelSelectorPopover::new( - selector, - agent_server, - fs, - PopoverMenuHandle::default(), - this.focus_handle(cx), - window, - cx, - ) - }) - }); - - let mode_selector = thread - .read(cx) - .connection() - .session_modes(thread.read(cx).session_id(), cx) - .map(|session_modes| { - let fs = this.project.read(cx).fs().clone(); - let focus_handle = this.focus_handle(cx); - cx.new(|_cx| { - ModeSelector::new( - session_modes, - this.agent.clone(), - fs, - focus_handle, - ) - }) - }); - - 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, - mode_selector, - _subscriptions: subscriptions, - }; - - this.profile_selector = this.as_native_thread(cx).map(|thread| { - cx.new(|cx| { - ProfileSelector::new( - ::global(cx), - Arc::new(thread.clone()), - this.focus_handle(cx), - cx, - ) - }) - }); - - this.message_editor.focus_handle(cx).focus(window); - - cx.notify(); - } - Err(err) => { - this.handle_load_error(err, window, cx); - } - }; - }) - .log_err(); - }); - - cx.spawn(async move |this, cx| { - while let Ok(new_version) = new_version_available_rx.recv().await { - if let Some(new_version) = new_version { - this.update(cx, |this, cx| { - this.new_server_version_available = Some(new_version.into()); - cx.notify(); - }) - .log_err(); - } - } - }) - .detach(); - - let loading_view = cx.new(|cx| { - let update_title_task = cx.spawn(async move |this, cx| { - loop { - let status = status_rx.recv().await?; - this.update(cx, |this: &mut LoadingView, cx| { - this.title = status; - cx.notify(); - })?; - } - }); - - LoadingView { - title: "Loading…".into(), - _load_task: load_task, - _update_title_task: update_title_task, - } - }); - - ThreadState::Loading(loading_view) - } - - fn handle_auth_required( - this: WeakEntity, - err: AuthRequired, - agent: Rc, - connection: Rc, - window: &mut Window, - cx: &mut App, - ) { - let agent_name = agent.name(); - let (configuration_view, subscription) = if let Some(provider_id) = err.provider_id { - let registry = LanguageModelRegistry::global(cx); - - let sub = window.subscribe(®istry, cx, { - let provider_id = provider_id.clone(); - let this = this.clone(); - move |_, ev, window, cx| { - if let language_model::Event::ProviderStateChanged(updated_provider_id) = &ev - && &provider_id == updated_provider_id - && LanguageModelRegistry::global(cx) - .read(cx) - .provider(&provider_id) - .map_or(false, |provider| provider.is_authenticated(cx)) - { - this.update(cx, |this, cx| { - this.reset(window, cx); - }) - .ok(); - } - } - }); - - let view = registry.read(cx).provider(&provider_id).map(|provider| { - provider.configuration_view( - language_model::ConfigurationViewTargetAgent::Other(agent_name.clone()), - window, - cx, - ) - }); - - (view, Some(sub)) - } else { - (None, None) - }; - - this.update(cx, |this, cx| { - this.thread_state = ThreadState::Unauthenticated { - pending_auth_method: None, - connection, - configuration_view, - description: err - .description - .clone() - .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, - ) { - if let Some(load_err) = err.downcast_ref::() { - self.thread_state = ThreadState::LoadError(load_err.clone()); - } else { - self.thread_state = - ThreadState::LoadError(LoadError::Other(format!("{:#}", err).into())) - } - if self.message_editor.focus_handle(cx).is_focused(window) { - self.focus_handle.focus(window) - } - cx.notify(); - } - - fn handle_agent_servers_updated( - &mut self, - _agent_server_store: &Entity, - _event: &project::AgentServersUpdated, - window: &mut Window, - cx: &mut Context, - ) { - // If we're in a LoadError state OR have a thread_error set (which can happen - // when agent.connect() fails during loading), retry loading the thread. - // This handles the case where a thread is restored before authentication completes. - let should_retry = - matches!(&self.thread_state, ThreadState::LoadError(_)) || self.thread_error.is_some(); - - if should_retry { - self.thread_error = None; - self.thread_error_markdown = None; - self.reset(window, cx); - } - } - - pub fn workspace(&self) -> &WeakEntity { - &self.workspace - } - - pub fn thread(&self) -> Option<&Entity> { - match &self.thread_state { - ThreadState::Ready { thread, .. } => Some(thread), - ThreadState::Unauthenticated { .. } - | ThreadState::Loading { .. } - | ThreadState::LoadError { .. } => None, - } - } - - pub fn mode_selector(&self) -> Option<&Entity> { - match &self.thread_state { - ThreadState::Ready { mode_selector, .. } => mode_selector.as_ref(), - ThreadState::Unauthenticated { .. } - | ThreadState::Loading { .. } - | ThreadState::LoadError { .. } => None, - } - } - - pub fn title(&self, cx: &App) -> SharedString { - match &self.thread_state { - ThreadState::Ready { .. } | ThreadState::Unauthenticated { .. } => "New Thread".into(), - ThreadState::Loading(loading_view) => loading_view.read(cx).title.clone(), - ThreadState::LoadError(error) => match error { - LoadError::Unsupported { .. } => format!("Upgrade {}", self.agent.name()).into(), - LoadError::FailedToInstall(_) => { - format!("Failed to Install {}", 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 - } - } - - pub fn cancel_generation(&mut self, cx: &mut Context) { - self.thread_error.take(); - self.thread_retry_status.take(); - - if let Some(thread) = self.thread() { - self._cancel_task = Some(thread.update(cx, |thread, cx| thread.cancel(cx))); - } - } - - pub fn expand_message_editor( - &mut self, - _: &ExpandMessageEditor, - _window: &mut Window, - cx: &mut Context, - ) { - self.set_editor_is_expanded(!self.editor_expanded, cx); - cx.stop_propagation(); - cx.notify(); - } - - fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context) { - self.editor_expanded = is_expanded; - self.message_editor.update(cx, |editor, cx| { - if is_expanded { - editor.set_mode( - EditorMode::Full { - scale_ui_elements_with_buffer_font_size: false, - show_active_line_background: false, - sizing_behavior: SizingBehavior::ExcludeOverscrollMargin, - }, - cx, - ) - } else { - let agent_settings = AgentSettings::get_global(cx); - editor.set_mode( - EditorMode::AutoHeight { - min_lines: agent_settings.message_editor_min_lines, - max_lines: Some(agent_settings.set_message_editor_max_lines()), - }, - cx, - ) - } - }); - 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, - event: &MessageEditorEvent, - window: &mut Window, - cx: &mut Context, - ) { - match event { - MessageEditorEvent::Send => self.send(window, cx), - MessageEditorEvent::Cancel => self.cancel_generation(cx), - MessageEditorEvent::Focus => { - self.cancel_editing(&Default::default(), window, cx); - } - MessageEditorEvent::LostFocus => {} - } - } - - pub fn handle_entry_view_event( - &mut self, - _: &Entity, - event: &EntryViewEvent, - window: &mut Window, - cx: &mut Context, - ) { - match &event.view_event { - ViewEvent::NewDiff(tool_call_id) => { - if AgentSettings::get_global(cx).expand_edit_card { - self.expanded_tool_calls.insert(tool_call_id.clone()); - } - } - ViewEvent::NewTerminal(tool_call_id) => { - if AgentSettings::get_global(cx).expand_terminal_card { - self.expanded_tool_calls.insert(tool_call_id.clone()); - } - } - ViewEvent::TerminalMovedToBackground(tool_call_id) => { - self.expanded_tool_calls.remove(tool_call_id); - } - ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Focus) => { - if let Some(thread) = self.thread() - && let Some(AgentThreadEntry::UserMessage(user_message)) = - thread.read(cx).entries().get(event.entry_index) - && user_message.id.is_some() - { - self.editing_message = Some(event.entry_index); - cx.notify(); - } - } - ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::LostFocus) => { - if let Some(thread) = self.thread() - && let Some(AgentThreadEntry::UserMessage(user_message)) = - thread.read(cx).entries().get(event.entry_index) - && user_message.id.is_some() - { - if editor.read(cx).text(cx).as_str() == user_message.content.to_markdown(cx) { - self.editing_message = None; - cx.notify(); - } - } - } - ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::Send) => { - self.regenerate(event.entry_index, editor.clone(), window, cx); - } - ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Cancel) => { - self.cancel_editing(&Default::default(), window, cx); - } - } - } - - pub fn is_loading(&self) -> bool { - matches!(self.thread_state, ThreadState::Loading { .. }) - } - - fn resume_chat(&mut self, cx: &mut Context) { - self.thread_error.take(); - 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| { - let result = task.await; - - this.update(cx, |this, cx| { - if let Err(err) = result { - this.handle_thread_error(err, cx); - } - }) - }) - .detach(); - } - - 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()), - cx, - ); - }); - - if thread.read(cx).status() != ThreadStatus::Idle { - self.stop_current_and_send_new_message(window, cx); - return; - } - - let text = self.message_editor.read(cx).text(cx); - let text = text.trim(); - if text == "/login" || text == "/logout" { - let ThreadState::Ready { thread, .. } = &self.thread_state else { - return; - }; - - let connection = thread.read(cx).connection().clone(); - let can_login = !connection.auth_methods().is_empty() || self.login.is_some(); - // Does the agent have a specific logout command? Prefer that in case they need to reset internal state. - let logout_supported = text == "/logout" - && self - .available_commands - .borrow() - .iter() - .any(|command| command.name == "logout"); - if can_login && !logout_supported { - self.message_editor - .update(cx, |editor, cx| editor.clear(window, cx)); - - let this = cx.weak_entity(); - let agent = self.agent.clone(); - window.defer(cx, |window, cx| { - Self::handle_auth_required( - this, - AuthRequired { - description: None, - provider_id: None, - }, - agent, - connection, - window, - cx, - ); - }); - cx.notify(); - return; - } - } - - self.send_impl(self.message_editor.clone(), window, cx) - } - - fn stop_current_and_send_new_message(&mut self, window: &mut Window, cx: &mut Context) { - let Some(thread) = self.thread().cloned() else { - return; - }; - - let cancelled = thread.update(cx, |thread, cx| thread.cancel(cx)); - - cx.spawn_in(window, async move |this, cx| { - cancelled.await; - - this.update_in(cx, |this, window, cx| { - this.send_impl(this.message_editor.clone(), window, cx); - }) - .ok(); - }) - .detach(); - } - - fn send_impl( - &mut self, - message_editor: Entity, - window: &mut Window, - cx: &mut Context, - ) { - let full_mention_content = self.as_native_thread(cx).is_some_and(|thread| { - // Include full contents when using minimal profile - let thread = thread.read(cx); - AgentSettings::get_global(cx) - .profiles - .get(thread.profile()) - .is_some_and(|profile| profile.tools.is_empty()) - }); - - let contents = message_editor.update(cx, |message_editor, cx| { - message_editor.contents(full_mention_content, cx) - }); - - self.thread_error.take(); - self.editing_message.take(); - self.thread_feedback.clear(); - - let Some(thread) = self.thread() else { - return; - }; - let session_id = thread.read(cx).session_id().clone(); - let agent_telemetry_id = thread.read(cx).connection().telemetry_id(); - let thread = thread.downgrade(); - if self.should_be_following { - self.workspace - .update(cx, |workspace, cx| { - workspace.follow(CollaboratorId::Agent, window, cx); - }) - .ok(); - } - - self.is_loading_contents = true; - let model_id = self.current_model_id(cx); - let mode_id = self.current_mode_id(cx); - 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?; - - if contents.is_empty() { - return Ok(()); - } - - this.update_in(cx, |this, window, cx| { - this.in_flight_prompt = Some(contents.clone()); - this.set_editor_is_expanded(false, cx); - this.scroll_to_bottom(cx); - this.message_editor.update(cx, |message_editor, cx| { - message_editor.clear(window, cx); - }); - })?; - let turn_start_time = Instant::now(); - let send = thread.update(cx, |thread, cx| { - thread.action_log().update(cx, |action_log, cx| { - for buffer in tracked_buffers { - action_log.buffer_read(buffer, cx) - } - }); - drop(guard); - - telemetry::event!( - "Agent Message Sent", - agent = agent_telemetry_id, - session = session_id, - model = model_id, - mode = mode_id - ); - - thread.send(contents, cx) - })?; - let res = send.await; - let turn_time_ms = turn_start_time.elapsed().as_millis(); - let status = if res.is_ok() { - this.update(cx, |this, _| this.in_flight_prompt.take()).ok(); - "success" - } else { - "failure" - }; - telemetry::event!( - "Agent Turn Completed", - agent = agent_telemetry_id, - session = session_id, - model = model_id, - mode = mode_id, - status, - turn_time_ms, - ); - res - }); - - cx.spawn(async move |this, cx| { - if let Err(err) = task.await { - this.update(cx, |this, cx| { - 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(); - } - - fn cancel_editing(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { - let Some(thread) = self.thread().cloned() else { - return; - }; - - if let Some(index) = self.editing_message.take() - && let Some(editor) = self - .entry_view_state - .read(cx) - .entry(index) - .and_then(|e| e.message_editor()) - .cloned() - { - editor.update(cx, |editor, cx| { - if let Some(user_message) = thread - .read(cx) - .entries() - .get(index) - .and_then(|e| e.user_message()) - { - editor.set_message(user_message.chunks.clone(), window, cx); - } - }) - }; - self.focus_handle(cx).focus(window); - cx.notify(); - } - - fn regenerate( - &mut self, - entry_ix: usize, - message_editor: Entity, - window: &mut Window, - cx: &mut Context, - ) { - 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() - }) else { - return; - }; - - cx.spawn_in(window, async move |this, cx| { - // Check if there are any edits from prompts before the one being regenerated. - // - // If there are, we keep/accept them since we're not regenerating the prompt that created them. - // - // If editing the prompt that generated the edits, they are auto-rejected - // through the `rewind` function in the `acp_thread`. - let has_earlier_edits = thread.read_with(cx, |thread, _| { - thread - .entries() - .iter() - .take(entry_ix) - .any(|entry| entry.diffs().next().is_some()) - })?; - - if has_earlier_edits { - thread.update(cx, |thread, cx| { - thread.action_log().update(cx, |action_log, cx| { - action_log.keep_all_edits(None, cx); - }); - })?; - } - - thread - .update(cx, |thread, cx| thread.rewind(user_message_id, cx))? - .await?; - this.update_in(cx, |this, window, cx| { - this.send_impl(message_editor, window, cx); - this.focus_handle(cx).focus(window); - })?; - anyhow::Ok(()) - }) - .detach(); - } - - fn open_edited_buffer( - &mut self, - buffer: &Entity, - window: &mut Window, - cx: &mut Context, - ) { - let Some(thread) = self.thread() else { - return; - }; - - let Some(diff) = - AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err() - else { - return; - }; - - diff.update(cx, |diff, cx| { - diff.move_to_path(PathKey::for_buffer(buffer, cx), window, cx) - }) - } - - fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { - let Some(thread) = self.as_native_thread(cx) else { - return; - }; - let project_context = thread.read(cx).project_context().read(cx); - - let project_entry_ids = project_context - .worktrees - .iter() - .flat_map(|worktree| worktree.rules_file.as_ref()) - .map(|rules_file| ProjectEntryId::from_usize(rules_file.project_entry_id)) - .collect::>(); - - self.workspace - .update(cx, move |workspace, cx| { - // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules - // files clear. For example, if rules file 1 is already open but rules file 2 is not, - // this would open and focus rules file 2 in a tab that is not next to rules file 1. - let project = workspace.project().read(cx); - let project_paths = project_entry_ids - .into_iter() - .flat_map(|entry_id| project.path_for_entry(entry_id, cx)) - .collect::>(); - for project_path in project_paths { - workspace - .open_path(project_path, None, true, window, cx) - .detach_and_log_err(cx); - } - }) - .ok(); - } - - fn handle_thread_error(&mut self, error: anyhow::Error, cx: &mut Context) { - self.thread_error = Some(ThreadError::from_err(error, &self.agent)); - cx.notify(); - } - - fn clear_thread_error(&mut self, cx: &mut Context) { - self.thread_error = None; - self.thread_error_markdown = None; - cx.notify(); - } - - fn handle_thread_event( - &mut self, - thread: &Entity, - event: &AcpThreadEvent, - window: &mut Window, - cx: &mut Context, - ) { - match event { - AcpThreadEvent::NewEntry => { - let len = thread.read(cx).entries().len(); - let index = len - 1; - self.entry_view_state.update(cx, |view_state, cx| { - view_state.sync_entry(index, thread, window, cx); - self.list_state.splice_focusable( - index..index, - [view_state - .entry(index) - .and_then(|entry| entry.focus_handle(cx))], - ); - }); - } - AcpThreadEvent::EntryUpdated(index) => { - self.entry_view_state.update(cx, |view_state, cx| { - view_state.sync_entry(*index, thread, window, cx) - }); - } - AcpThreadEvent::EntriesRemoved(range) => { - self.entry_view_state - .update(cx, |view_state, _cx| view_state.remove(range.clone())); - self.list_state.splice(range.clone(), 0); - } - AcpThreadEvent::ToolAuthorizationRequired => { - self.notify_with_sound("Waiting for tool confirmation", IconName::Info, window, cx); - } - AcpThreadEvent::Retry(retry) => { - self.thread_retry_status = Some(retry.clone()); - } - AcpThreadEvent::Stopped => { - self.thread_retry_status.take(); - let used_tools = thread.read(cx).used_tools_since_last_user_message(); - self.notify_with_sound( - if used_tools { - "Finished running tools" - } else { - "New message" - }, - IconName::ZedAssistant, - window, - cx, - ); - } - AcpThreadEvent::Refusal => { - self.thread_retry_status.take(); - self.thread_error = Some(ThreadError::Refusal); - let model_or_agent_name = self.current_model_name(cx); - let notification_message = - format!("{} refused to respond to this request", model_or_agent_name); - self.notify_with_sound(¬ification_message, IconName::Warning, window, cx); - } - AcpThreadEvent::Error => { - self.thread_retry_status.take(); - self.notify_with_sound( - "Agent stopped due to an error", - IconName::Warning, - window, - cx, - ); - } - 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 - .replace(thread.read(cx).prompt_capabilities()); - } - AcpThreadEvent::TokenUsageUpdated => {} - AcpThreadEvent::AvailableCommandsUpdated(available_commands) => { - let mut available_commands = available_commands.clone(); - - if thread - .read(cx) - .connection() - .auth_methods() - .iter() - .any(|method| method.id.0.as_ref() == "claude-login") - { - available_commands.push(acp::AvailableCommand::new("login", "Authenticate")); - available_commands.push(acp::AvailableCommand::new("logout", "Authenticate")); - } - - let has_commands = !available_commands.is_empty(); - self.available_commands.replace(available_commands); - - let new_placeholder = placeholder_text(self.agent.name().as_ref(), has_commands); - - self.message_editor.update(cx, |editor, cx| { - editor.set_placeholder_text(&new_placeholder, window, cx); - }); - } - AcpThreadEvent::ModeUpdated(_mode) => { - // The connection keeps track of the mode - cx.notify(); - } - } - cx.notify(); - } - - fn authenticate( - &mut self, - method: acp::AuthMethodId, - window: &mut Window, - cx: &mut Context, - ) { - let ThreadState::Unauthenticated { - connection, - pending_auth_method, - configuration_view, - .. - } = &mut self.thread_state - else { - return; - }; - let agent_telemetry_id = connection.telemetry_id(); - - // Check for the experimental "terminal-auth" _meta field - let auth_method = connection.auth_methods().iter().find(|m| m.id == method); - - if let Some(auth_method) = auth_method { - if let Some(meta) = &auth_method.meta { - if let Some(terminal_auth) = meta.get("terminal-auth") { - // Extract terminal auth details from meta - if let (Some(command), Some(label)) = ( - terminal_auth.get("command").and_then(|v| v.as_str()), - terminal_auth.get("label").and_then(|v| v.as_str()), - ) { - let args = terminal_auth - .get("args") - .and_then(|v| v.as_array()) - .map(|arr| { - arr.iter() - .filter_map(|v| v.as_str().map(String::from)) - .collect() - }) - .unwrap_or_default(); - - let env = terminal_auth - .get("env") - .and_then(|v| v.as_object()) - .map(|obj| { - obj.iter() - .filter_map(|(k, v)| { - v.as_str().map(|val| (k.clone(), val.to_string())) - }) - .collect::>() - }) - .unwrap_or_default(); - - // Run SpawnInTerminal in the same dir as the ACP server - let cwd = connection - .clone() - .downcast::() - .map(|acp_conn| acp_conn.root_dir().to_path_buf()); - - // Build SpawnInTerminal from _meta - let login = task::SpawnInTerminal { - id: task::TaskId(format!("external-agent-{}-login", label)), - full_label: label.to_string(), - label: label.to_string(), - command: Some(command.to_string()), - args, - command_label: label.to_string(), - cwd, - env, - use_new_terminal: true, - allow_concurrent_runs: true, - hide: task::HideStrategy::Always, - ..Default::default() - }; - - self.thread_error.take(); - configuration_view.take(); - pending_auth_method.replace(method.clone()); - - if let Some(workspace) = self.workspace.upgrade() { - let project = self.project.clone(); - let authenticate = Self::spawn_external_agent_login( - login, workspace, project, false, true, window, cx, - ); - cx.notify(); - self.auth_task = Some(cx.spawn_in(window, { - 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 { - if let ThreadState::Unauthenticated { - pending_auth_method, - .. - } = &mut this.thread_state - { - pending_auth_method.take(); - } - this.handle_thread_error(err, cx); - } else { - this.reset(window, cx); - } - this.auth_task.take() - }) - .ok(); - } - })); - } - return; - } - } - } - } - - if method.0.as_ref() == "gemini-api-key" { - let registry = LanguageModelRegistry::global(cx); - let provider = registry - .read(cx) - .provider(&language_model::GOOGLE_PROVIDER_ID) - .unwrap(); - if !provider.is_authenticated(cx) { - let this = cx.weak_entity(); - let agent = self.agent.clone(); - let connection = connection.clone(); - window.defer(cx, |window, cx| { - Self::handle_auth_required( - this, - AuthRequired { - description: Some("GEMINI_API_KEY must be set".to_owned()), - provider_id: Some(language_model::GOOGLE_PROVIDER_ID), - }, - agent, - connection, - window, - cx, - ); - }); - return; - } - } else if method.0.as_ref() == "anthropic-api-key" { - let registry = LanguageModelRegistry::global(cx); - let provider = registry - .read(cx) - .provider(&language_model::ANTHROPIC_PROVIDER_ID) - .unwrap(); - let this = cx.weak_entity(); - let agent = self.agent.clone(); - let connection = connection.clone(); - window.defer(cx, move |window, cx| { - if !provider.is_authenticated(cx) { - Self::handle_auth_required( - this, - AuthRequired { - description: Some("ANTHROPIC_API_KEY must be set".to_owned()), - provider_id: Some(language_model::ANTHROPIC_PROVIDER_ID), - }, - agent, - connection, - window, - cx, - ); - } else { - this.update(cx, |this, cx| { - this.thread_state = Self::initial_state( - agent, - None, - this.workspace.clone(), - this.project.clone(), - true, - window, - cx, - ) - }) - .ok(); - } - }); - return; - } else if method.0.as_ref() == "vertex-ai" - && std::env::var("GOOGLE_API_KEY").is_err() - && (std::env::var("GOOGLE_CLOUD_PROJECT").is_err() - || (std::env::var("GOOGLE_CLOUD_PROJECT").is_err())) - { - let this = cx.weak_entity(); - let agent = self.agent.clone(); - let connection = connection.clone(); - - window.defer(cx, |window, cx| { - Self::handle_auth_required( - this, - AuthRequired { - description: Some( - "GOOGLE_API_KEY must be set in the environment to use Vertex AI authentication for Gemini CLI. Please export it and restart Zed." - .to_owned(), - ), - provider_id: None, - }, - agent, - connection, - window, - cx, - ) - }); - return; - } - - self.thread_error.take(); - configuration_view.take(); - pending_auth_method.replace(method.clone()); - let authenticate = if (method.0.as_ref() == "claude-login" - || method.0.as_ref() == "spawn-gemini-cli") - && let Some(login) = self.login.clone() - { - if let Some(workspace) = self.workspace.upgrade() { - let project = self.project.clone(); - Self::spawn_external_agent_login( - login, workspace, project, false, false, window, cx, - ) - } else { - Task::ready(Ok(())) - } - } else { - connection.authenticate(method, cx) - }; - cx.notify(); - self.auth_task = Some(cx.spawn_in(window, { - 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 { - if let ThreadState::Unauthenticated { - pending_auth_method, - .. - } = &mut this.thread_state - { - pending_auth_method.take(); - } - this.handle_thread_error(err, cx); - } else { - this.reset(window, cx); - } - this.auth_task.take() - }) - .ok(); - } - })); - } - - fn spawn_external_agent_login( - login: task::SpawnInTerminal, - workspace: Entity, - project: Entity, - previous_attempt: bool, - check_exit_code: bool, - window: &mut Window, - cx: &mut App, - ) -> Task> { - let Some(terminal_panel) = workspace.read(cx).panel::(cx) else { - return Task::ready(Ok(())); - }; - - window.spawn(cx, async move |cx| { - let mut task = login.clone(); - if let Some(cmd) = &task.command { - // Have "node" command use Zed's managed Node runtime by default - if cmd == "node" { - let resolved_node_runtime = project - .update(cx, |project, cx| { - let agent_server_store = project.agent_server_store().clone(); - agent_server_store.update(cx, |store, cx| { - store.node_runtime().map(|node_runtime| { - cx.background_spawn(async move { - node_runtime.binary_path().await - }) - }) - }) - }); - - if let Ok(Some(resolve_task)) = resolved_node_runtime { - if let Ok(node_path) = resolve_task.await { - task.command = Some(node_path.to_string_lossy().to_string()); - } - } - } - } - task.shell = task::Shell::WithArguments { - program: task.command.take().expect("login command should be set"), - args: std::mem::take(&mut task.args), - title_override: None - }; - task.full_label = task.label.clone(); - task.id = task::TaskId(format!("external-agent-{}-login", task.label)); - task.command_label = task.label.clone(); - task.use_new_terminal = true; - task.allow_concurrent_runs = true; - task.hide = task::HideStrategy::Always; - - let terminal = terminal_panel.update_in(cx, |terminal_panel, window, cx| { - terminal_panel.spawn_task(&task, window, cx) - })?; - - let terminal = terminal.await?; - - if check_exit_code { - // For extension-based auth, wait for the process to exit and check exit code - let exit_status = terminal - .read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))? - .await; - - match exit_status { - Some(status) if status.success() => { - Ok(()) - } - Some(status) => { - Err(anyhow!("Login command failed with exit code: {:?}", status.code())) - } - None => { - Err(anyhow!("Login command terminated without exit status")) - } - } - } else { - // For hardcoded agents (claude-login, gemini-cli): look for specific output - let mut exit_status = terminal - .read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx))? - .fuse(); - - let logged_in = cx - .spawn({ - let terminal = terminal.clone(); - async move |cx| { - loop { - cx.background_executor().timer(Duration::from_secs(1)).await; - let content = - terminal.update(cx, |terminal, _cx| terminal.get_content())?; - if content.contains("Login successful") - || content.contains("Type your message") - { - return anyhow::Ok(()); - } - } - } - }) - .fuse(); - futures::pin_mut!(logged_in); - futures::select_biased! { - result = logged_in => { - if let Err(e) = result { - log::error!("{e}"); - return Err(anyhow!("exited before logging in")); - } - } - _ = exit_status => { - if !previous_attempt && project.read_with(cx, |project, _| project.is_via_remote_server())? && login.label.contains("gemini") { - return cx.update(|window, cx| Self::spawn_external_agent_login(login, workspace, project.clone(), true, false, window, cx))?.await - } - return Err(anyhow!("exited before logging in")); - } - } - terminal.update(cx, |terminal, _| terminal.kill_active_task())?; - Ok(()) - } - }) - } - - fn authorize_tool_call( - &mut self, - 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 { - return; - }; - let agent_telemetry_id = thread.read(cx).connection().telemetry_id(); - - telemetry::event!( - "Agent Tool Call Authorized", - agent = agent_telemetry_id, - session = thread.read(cx).session_id(), - option = option_kind - ); - - 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(); - } - - fn restore_checkpoint(&mut self, message_id: &UserMessageId, cx: &mut Context) { - let Some(thread) = self.thread() else { - return; - }; - - thread - .update(cx, |thread, cx| { - thread.restore_checkpoint(message_id.clone(), cx) - }) - .detach_and_log_err(cx); - } - - fn render_entry( - &self, - entry_ix: usize, - total_entries: usize, - entry: &AgentThreadEntry, - window: &mut Window, - cx: &Context, - ) -> AnyElement { - let primary = match &entry { - AgentThreadEntry::UserMessage(message) => { - let Some(editor) = self - .entry_view_state - .read(cx) - .entry(entry_ix) - .and_then(|entry| entry.message_editor()) - .cloned() - else { - return Empty.into_any_element(); - }; - - let editing = self.editing_message == Some(entry_ix); - let editor_focus = editor.focus_handle(cx).is_focused(window); - let focus_border = cx.theme().colors().border_focused; - - let rules_item = if entry_ix == 0 { - self.render_rules_item(cx) - } else { - 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(rems_from_px(18.)) - } else if rules_item.is_some() { - this.pt_3() - } else { - this.pt_2() - } - }) - .pb_3() - .px_2() - .gap_1p5() - .w_full() - .children(rules_item) - .children(message.id.clone().and_then(|message_id| { - message.checkpoint.as_ref()?.show.then(|| { - h_flex() - .px_3() - .gap_2() - .child(Divider::horizontal()) - .child( - Button::new("restore-checkpoint", "Restore Checkpoint") - .icon(IconName::Undo) - .icon_size(IconSize::XSmall) - .icon_position(IconPosition::Start) - .label_size(LabelSize::XSmall) - .icon_color(Color::Muted) - .color(Color::Muted) - .tooltip(Tooltip::text("Restores all files in the project to the content they had at this point in the conversation.")) - .on_click(cx.listener(move |this, _, _window, cx| { - this.restore_checkpoint(&message_id, cx); - })) - ) - .child(Divider::horizontal()) - }) - })) - .child( - div() - .relative() - .child( - div() - .py_3() - .px_2() - .rounded_md() - .shadow_md() - .bg(cx.theme().colors().editor_background) - .border_1() - .when(editing && !editor_focus, |this| this.border_dashed()) - .border_color(cx.theme().colors().border) - .map(|this|{ - if editing && editor_focus { - this.border_color(focus_border) - } else if message.id.is_some() { - this.hover(|s| s.border_color(focus_border.opacity(0.8))) - } else { - this - } - }) - .text_xs() - .child(editor.clone().into_any_element()), - ) - .when(editor_focus, |this| { - 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.clone(), window, cx, - ); - } - })).into_any_element() - } - ) - ) - } else { - this.child( - base_container - .border_dashed() - .child( - IconButton::new("editing_unavailable", IconName::PencilUnavailable) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .style(ButtonStyle::Transparent) - .tooltip(move |_window, cx| { - cx.new(|_| UnavailableEditingTooltip::new(agent_name.clone())) - .into() - }) - ) - ) - } - }), - ) - .into_any() - } - AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => { - let is_last = entry_ix + 1 == total_entries; - - let style = default_markdown_style(false, false, window, cx); - let message_body = v_flex() - .w_full() - .gap_3() - .children(chunks.iter().enumerate().filter_map( - |(chunk_ix, chunk)| match chunk { - AssistantMessageChunk::Message { block } => { - block.markdown().map(|md| { - self.render_markdown(md.clone(), style.clone()) - .into_any_element() - }) - } - AssistantMessageChunk::Thought { block } => { - block.markdown().map(|md| { - self.render_thinking_block( - entry_ix, - chunk_ix, - md.clone(), - window, - cx, - ) - .into_any_element() - }) - } - }, - )) - .into_any(); - - v_flex() - .px_5() - .py_1p5() - .when(is_last, |this| this.pb_4()) - .w_full() - .text_ui(cx) - .child(message_body) - .into_any() - } - AgentThreadEntry::ToolCall(tool_call) => { - let has_terminals = tool_call.terminals().next().is_some(); - - div().w_full().map(|this| { - if has_terminals { - this.children(tool_call.terminals().map(|terminal| { - self.render_terminal_tool_call( - entry_ix, terminal, tool_call, window, cx, - ) - })) - } else { - this.child(self.render_tool_call(entry_ix, tool_call, window, cx)) - } - }) - } - .into_any(), - }; - - let needs_confirmation = if let AgentThreadEntry::ToolCall(tool_call) = entry { - matches!( - tool_call.status, - ToolCallStatus::WaitingForConfirmation { .. } - ) - } else { - false - }; - - let Some(thread) = self.thread() else { - return primary; - }; - - let primary = if entry_ix == total_entries - 1 { - v_flex() - .w_full() - .child(primary) - .map(|this| { - if needs_confirmation { - this.child(self.render_generating(true)) - } else { - this.child(self.render_thread_controls(&thread, cx)) - } - }) - .when_some( - self.thread_feedback.comments_editor.clone(), - |this, editor| this.child(Self::render_feedback_feedback_editor(editor, cx)), - ) - .into_any_element() - } else { - primary - }; - - if let Some(editing_index) = self.editing_message.as_ref() - && *editing_index < entry_ix - { - let backdrop = div() - .id(("backdrop", entry_ix)) - .size_full() - .absolute() - .inset_0() - .bg(cx.theme().colors().panel_background) - .opacity(0.8) - .block_mouse_except_scroll() - .on_click(cx.listener(Self::cancel_editing)); - - div() - .relative() - .child(primary) - .child(backdrop) - .into_any_element() - } else { - primary - } - } - - fn tool_card_header_bg(&self, cx: &Context) -> Hsla { - cx.theme() - .colors() - .element_background - .blend(cx.theme().colors().editor_foreground.opacity(0.025)) - } - - fn tool_card_border_color(&self, cx: &Context) -> Hsla { - cx.theme().colors().border.opacity(0.8) - } - - fn tool_name_font_size(&self) -> Rems { - rems_from_px(13.) - } - - fn render_thinking_block( - &self, - entry_ix: usize, - chunk_ix: usize, - chunk: Entity, - 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 scroll_handle = self - .entry_view_state - .read(cx) - .entry(entry_ix) - .and_then(|entry| entry.scroll_handle_for_assistant_message_chunk(chunk_ix)); - - let thinking_content = { - div() - .id(("thinking-content", chunk_ix)) - .when_some(scroll_handle, |this, scroll_handle| { - this.track_scroll(&scroll_handle) - }) - .text_ui_sm(cx) - .overflow_hidden() - .child( - self.render_markdown(chunk, default_markdown_style(false, false, window, cx)), - ) - }; - - v_flex() - .gap_1() - .child( - h_flex() - .id(header_id) - .group(&card_header_id) - .relative() - .w_full() - .pr_1() - .justify_between() - .child( - h_flex() - .h(window.line_height() - px(2.)) - .gap_1p5() - .overflow_hidden() - .child( - Icon::new(IconName::ToolThink) - .size(IconSize::Small) - .color(Color::Muted), - ) - .child( - div() - .text_size(self.tool_name_font_size()) - .text_color(cx.theme().colors().text_muted) - .child("Thinking"), - ), - ) - .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(); - } - })), - ) - .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(); - } - })), - ) - .when(is_open, |this| { - this.child( - div() - .ml_1p5() - .pl_3p5() - .border_l_1() - .border_color(self.tool_card_border_color(cx)) - .child(thinking_content), - ) - }) - .into_any_element() - } - - fn render_tool_call( - &self, - entry_ix: usize, - tool_call: &ToolCall, - window: &Window, - cx: &Context, - ) -> Div { - let has_location = tool_call.locations.len() == 1; - let card_header_id = SharedString::from("inner-tool-call-header"); - - let failed_or_canceled = match &tool_call.status { - ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => true, - _ => false, - }; - - let needs_confirmation = matches!( - tool_call.status, - ToolCallStatus::WaitingForConfirmation { .. } - ); - let is_terminal_tool = matches!(tool_call.kind, acp::ToolKind::Execute); - let is_edit = - matches!(tool_call.kind, acp::ToolKind::Edit) || tool_call.diffs().next().is_some(); - - let use_card_layout = needs_confirmation || is_edit || is_terminal_tool; - - let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation; - - let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id); - - let tool_output_display = - if is_open { - match &tool_call.status { - ToolCallStatus::WaitingForConfirmation { options, .. } => v_flex() - .w_full() - .children(tool_call.content.iter().enumerate().map( - |(content_ix, content)| { - div() - .child(self.render_tool_call_content( - entry_ix, - content, - content_ix, - tool_call, - use_card_layout, - window, - cx, - )) - .into_any_element() - }, - )) - .child(self.render_permission_buttons( - tool_call.kind, - options, - entry_ix, - tool_call.id.clone(), - cx, - )) - .into_any(), - ToolCallStatus::Pending | ToolCallStatus::InProgress - if is_edit - && tool_call.content.is_empty() - && self.as_native_connection(cx).is_some() => - { - self.render_diff_loading(cx).into_any() - } - ToolCallStatus::Pending - | ToolCallStatus::InProgress - | ToolCallStatus::Completed - | ToolCallStatus::Failed - | ToolCallStatus::Canceled => v_flex() - .w_full() - .children(tool_call.content.iter().enumerate().map( - |(content_ix, content)| { - div().child(self.render_tool_call_content( - entry_ix, - content, - content_ix, - tool_call, - use_card_layout, - window, - cx, - )) - }, - )) - .into_any(), - ToolCallStatus::Rejected => Empty.into_any(), - } - .into() - } else { - None - }; - - v_flex() - .map(|this| { - if use_card_layout { - this.my_1p5() - .rounded_md() - .border_1() - .border_color(self.tool_card_border_color(cx)) - .bg(cx.theme().colors().editor_background) - .overflow_hidden() - } else { - this.my_1() - } - }) - .map(|this| { - if has_location && !use_card_layout { - this.ml_4() - } else { - this.ml_5() - } - }) - .mr_5() - .map(|this| { - if is_terminal_tool { - this.child( - v_flex() - .p_1p5() - .gap_0p5() - .text_ui_sm(cx) - .bg(self.tool_card_header_bg(cx)) - .child( - Label::new("Run Command") - .buffer_font(cx) - .size(LabelSize::XSmall) - .color(Color::Muted), - ) - .child( - MarkdownElement::new( - tool_call.label.clone(), - terminal_command_markdown_style(window, cx), - ) - .code_block_renderer( - markdown::CodeBlockRenderer::Default { - copy_button: false, - copy_button_on_hover: false, - border: false, - }, - ) - ), - ) - } else { - this.child( - h_flex() - .group(&card_header_id) - .relative() - .w_full() - .gap_1() - .justify_between() - .when(use_card_layout, |this| { - this.p_0p5() - .rounded_t(rems_from_px(5.)) - .bg(self.tool_card_header_bg(cx)) - }) - .child(self.render_tool_call_label( - entry_ix, - tool_call, - is_edit, - use_card_layout, - window, - cx, - )) - .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), - ) - }), - ) - }), - ) - } - }) - .children(tool_output_display) - } - - fn render_tool_call_label( - &self, - entry_ix: usize, - tool_call: &ToolCall, - is_edit: bool, - use_card_layout: bool, - window: &Window, - cx: &Context, - ) -> Div { - let has_location = tool_call.locations.len() == 1; - - let tool_icon = if tool_call.kind == acp::ToolKind::Edit && has_location { - FileIcons::get_icon(&tool_call.locations[0].path, cx) - .map(Icon::from_path) - .unwrap_or(Icon::new(IconName::ToolPencil)) - } else { - Icon::new(match tool_call.kind { - acp::ToolKind::Read => IconName::ToolSearch, - acp::ToolKind::Edit => IconName::ToolPencil, - acp::ToolKind::Delete => IconName::ToolDeleteFile, - acp::ToolKind::Move => IconName::ArrowRightLeft, - acp::ToolKind::Search => IconName::ToolSearch, - acp::ToolKind::Execute => IconName::ToolTerminal, - acp::ToolKind::Think => IconName::ToolThink, - acp::ToolKind::Fetch => IconName::ToolWeb, - acp::ToolKind::SwitchMode => IconName::ArrowRightLeft, - acp::ToolKind::Other | _ => IconName::ToolHammer, - }) - } - .size(IconSize::Small) - .color(Color::Muted); - - let gradient_overlay = { - 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., - ), - )) - } - }) - }; - - h_flex() - .relative() - .w_full() - .h(window.line_height() - px(2.)) - .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(rems_from_px(3.)) // Concentric border radius - .hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.5))) - }) - .overflow_hidden() - .child(tool_icon) - .child(if has_location { - 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) - } - }) - .child(self.render_markdown( - tool_call.label.clone(), - MarkdownStyle { - prevent_mouse_interaction: true, - ..default_markdown_style(false, true, window, cx) - }, - )) - .tooltip(Tooltip::text("Jump to File")) - .on_click(cx.listener(move |this, _, window, cx| { - this.open_tool_call_location(entry_ix, 0, window, cx); - })) - .into_any_element() - } else { - h_flex() - .w_full() - .child(self.render_markdown( - tool_call.label.clone(), - default_markdown_style(false, true, window, cx), - )) - .into_any() - }) - .when(!is_edit, |this| this.child(gradient_overlay)) - } - - fn render_tool_call_content( - &self, - entry_ix: usize, - content: &ToolCallContent, - context_ix: usize, - tool_call: &ToolCall, - card_layout: bool, - window: &Window, - cx: &Context, - ) -> AnyElement { - match content { - ToolCallContent::ContentBlock(content) => { - if let Some(resource_link) = content.resource_link() { - self.render_resource_link(resource_link, cx) - } else if let Some(markdown) = content.markdown() { - self.render_markdown_output( - markdown.clone(), - tool_call.id.clone(), - context_ix, - card_layout, - window, - cx, - ) - } else { - Empty.into_any_element() - } - } - ToolCallContent::Diff(diff) => self.render_diff_editor(entry_ix, diff, tool_call, cx), - ToolCallContent::Terminal(terminal) => { - self.render_terminal_tool_call(entry_ix, terminal, tool_call, window, cx) - } - } - } - - fn render_markdown_output( - &self, - markdown: Entity, - tool_call_id: acp::ToolCallId, - context_ix: usize, - card_layout: bool, - window: &Window, - cx: &Context, - ) -> AnyElement { - let button_id = SharedString::from(format!("tool_output-{:?}", tool_call_id)); - - v_flex() - .mt_1p5() - .gap_2() - .when(!card_layout, |this| { - this.ml(rems(0.4)) - .px_3p5() - .border_l_1() - .border_color(self.tool_card_border_color(cx)) - }) - .when(card_layout, |this| { - this.px_2().pb_2().when(context_ix > 0, |this| { - this.border_t_1() - .pt_2() - .border_color(self.tool_card_border_color(cx)) - }) - }) - .text_xs() - .text_color(cx.theme().colors().text_muted) - .child(self.render_markdown(markdown, default_markdown_style(false, false, window, cx))) - .when(!card_layout, |this| { - this.child( - IconButton::new(button_id, IconName::ChevronUp) - .full_width() - .style(ButtonStyle::Outlined) - .icon_color(Color::Muted) - .on_click(cx.listener({ - move |this: &mut Self, _, _, cx: &mut Context| { - this.expanded_tool_calls.remove(&tool_call_id); - cx.notify(); - } - })), - ) - }) - .into_any_element() - } - - fn render_resource_link( - &self, - resource_link: &acp::ResourceLink, - 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() - } - } else { - uri.clone() - }; - - let button_id = SharedString::from(format!("item-{}", uri)); - - div() - .ml(rems(0.4)) - .pl_2p5() - .border_l_1() - .border_color(self.tool_card_border_color(cx)) - .overflow_hidden() - .child( - Button::new(button_id, label) - .label_size(LabelSize::Small) - .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| { - Self::open_link(uri.clone(), &workspace, window, cx); - } - })), - ) - .into_any_element() - } - - fn render_permission_buttons( - &self, - kind: acp::ToolKind, - options: &[acp::PermissionOption], - entry_ix: usize, - tool_call_id: acp::ToolCallId, - cx: &Context, - ) -> Div { - let is_first = self.thread().is_some_and(|thread| { - thread - .read(cx) - .first_tool_awaiting_confirmation() - .is_some_and(|call| call.id == tool_call_id) - }); - let mut seen_kinds: ArrayVec = ArrayVec::new(); - - div() - .p_1() - .border_t_1() - .border_color(self.tool_card_border_color(cx)) - .w_full() - .map(|this| { - if kind == acp::ToolKind::SwitchMode { - this.v_flex() - } else { - this.h_flex().justify_end().flex_wrap() - } - }) - .gap_0p5() - .children(options.iter().map(move |option| { - let option_id = SharedString::from(option.option_id.0.clone()); - Button::new((option_id, entry_ix), option.name.clone()) - .map(|this| { - let (this, action) = match option.kind { - acp::PermissionOptionKind::AllowOnce => ( - this.icon(IconName::Check).icon_color(Color::Success), - Some(&AllowOnce as &dyn Action), - ), - acp::PermissionOptionKind::AllowAlways => ( - this.icon(IconName::CheckDouble).icon_color(Color::Success), - Some(&AllowAlways as &dyn Action), - ), - acp::PermissionOptionKind::RejectOnce => ( - this.icon(IconName::Close).icon_color(Color::Error), - Some(&RejectOnce as &dyn Action), - ), - acp::PermissionOptionKind::RejectAlways | _ => { - (this.icon(IconName::Close).icon_color(Color::Error), None) - } - }; - - let Some(action) = action else { - return this; - }; - - if !is_first || seen_kinds.contains(&option.kind) { - return this; - } - - seen_kinds.push(option.kind); - - this.key_binding( - KeyBinding::for_action_in(action, &self.focus_handle, cx) - .map(|kb| kb.size(rems_from_px(10.))), - ) - }) - .icon_position(IconPosition::Start) - .icon_size(IconSize::XSmall) - .label_size(LabelSize::Small) - .on_click(cx.listener({ - let tool_call_id = tool_call_id.clone(); - let option_id = option.option_id.clone(); - let option_kind = option.kind; - move |this, _, window, cx| { - this.authorize_tool_call( - tool_call_id.clone(), - option_id.clone(), - option_kind, - window, - cx, - ); - } - })) - })) - } - - fn render_diff_loading(&self, cx: &Context) -> AnyElement { - let bar = |n: u64, width_class: &str| { - let bg_color = cx.theme().colors().element_active; - let base = h_flex().h_1().rounded_full(); - - let modified = match width_class { - "w_4_5" => base.w_3_4(), - "w_1_4" => base.w_1_4(), - "w_2_4" => base.w_2_4(), - "w_3_5" => base.w_3_5(), - "w_2_5" => base.w_2_5(), - _ => base.w_1_2(), - }; - - modified.with_animation( - ElementId::Integer(n), - Animation::new(Duration::from_secs(2)).repeat(), - move |tab, delta| { - let delta = (delta - 0.15 * n as f32) / 0.7; - let delta = 1.0 - (0.5 - delta).abs() * 2.; - let delta = ease_in_out(delta.clamp(0., 1.)); - let delta = 0.1 + 0.9 * delta; - - tab.bg(bg_color.opacity(delta)) - }, - ) - }; - - v_flex() - .p_3() - .gap_1() - .rounded_b_md() - .bg(cx.theme().colors().editor_background) - .child(bar(0, "w_4_5")) - .child(bar(1, "w_1_4")) - .child(bar(2, "w_2_4")) - .child(bar(3, "w_3_5")) - .child(bar(4, "w_2_5")) - .into_any_element() - } - - fn render_diff_editor( - &self, - entry_ix: usize, - diff: &Entity, - tool_call: &ToolCall, - cx: &Context, - ) -> AnyElement { - let tool_progress = matches!( - &tool_call.status, - ToolCallStatus::InProgress | ToolCallStatus::Pending - ); - - v_flex() - .h_full() - .border_t_1() - .border_color(self.tool_card_border_color(cx)) - .child( - if let Some(entry) = self.entry_view_state.read(cx).entry(entry_ix) - && let Some(editor) = entry.editor_for_diff(diff) - && diff.read(cx).has_revealed_range(cx) - { - editor.into_any_element() - } else if tool_progress && self.as_native_connection(cx).is_some() { - self.render_diff_loading(cx) - } else { - Empty.into_any() - }, - ) - .into_any() - } - - fn render_terminal_tool_call( - &self, - entry_ix: usize, - terminal: &Entity, - tool_call: &ToolCall, - window: &Window, - cx: &Context, - ) -> AnyElement { - let terminal_data = terminal.read(cx); - let working_dir = terminal_data.working_dir(); - let command = terminal_data.command(); - let started_at = terminal_data.started_at(); - - let tool_failed = matches!( - &tool_call.status, - ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed - ); - - let output = terminal_data.output(); - let command_finished = output.is_some(); - let truncated_output = - output.is_some_and(|output| output.original_content_len > output.content.len()); - let output_line_count = output.map(|output| output.content_line_count).unwrap_or(0); - - let command_failed = command_finished - && output.is_some_and(|o| o.exit_status.is_some_and(|status| !status.success())); - - let time_elapsed = if let Some(output) = output { - output.ended_at.duration_since(started_at) - } else { - 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() - .element_background - .blend(cx.theme().colors().editor_foreground.opacity(0.025)); - let border_color = cx.theme().colors().border.opacity(0.6); - - let working_dir = working_dir - .as_ref() - .map(|path| path.display().to_string()) - .unwrap_or_else(|| "current directory".to_string()); - - let is_expanded = self.expanded_tool_calls.contains(&tool_call.id); - - let header = h_flex() - .id(header_id) - .flex_none() - .gap_1() - .justify_between() - .rounded_t_md() - .child( - div() - .id(("command-target-path", terminal.entity_id())) - .w_full() - .max_w_full() - .overflow_x_scroll() - .child( - Label::new(working_dir) - .buffer_font(cx) - .size(LabelSize::XSmall) - .color(Color::Muted), - ), - ) - .when(!command_finished, |header| { - header - .gap_1p5() - .child( - Button::new( - SharedString::from(format!("stop-terminal-{}", terminal.entity_id())), - "Stop", - ) - .icon(IconName::Stop) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .icon_color(Color::Error) - .label_size(LabelSize::Small) - .tooltip(move |_window, cx| { - Tooltip::with_meta( - "Stop This Command", - None, - "Also possible by placing your cursor inside the terminal and using regular terminal bindings.", - cx, - ) - }) - .on_click({ - let terminal = terminal.clone(); - cx.listener(move |_this, _event, _window, cx| { - let inner_terminal = terminal.read(cx).inner().clone(); - inner_terminal.update(cx, |inner_terminal, _cx| { - inner_terminal.kill_active_task(); - }); - }) - }), - ) - .child(Divider::vertical()) - .child( - Icon::new(IconName::ArrowCircle) - .size(IconSize::XSmall) - .color(Color::Info) - .with_rotate_animation(2) - ) - }) - .when(truncated_output, |header| { - let tooltip = if let Some(output) = output { - if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES { - format!("Output exceeded terminal max lines and was \ - truncated, the model received the first {}.", format_file_size(output.content.len() as u64, true)) - } else { - format!( - "Output is {} long, and to avoid unexpected token usage, \ - only {} was sent back to the agent.", - format_file_size(output.original_content_len as u64, true), - format_file_size(output.content.len() as u64, true) - ) - } - } else { - "Output was truncated".to_string() - }; - - header.child( - h_flex() - .id(("terminal-tool-truncated-label", terminal.entity_id())) - .gap_1() - .child( - Icon::new(IconName::Info) - .size(IconSize::XSmall) - .color(Color::Ignored), - ) - .child( - Label::new("Truncated") - .color(Color::Muted) - .size(LabelSize::XSmall), - ) - .tooltip(Tooltip::text(tooltip)), - ) - }) - .when(time_elapsed > Duration::from_secs(10), |header| { - header.child( - Label::new(format!("({})", duration_alt_display(time_elapsed))) - .buffer_font(cx) - .color(Color::Muted) - .size(LabelSize::XSmall), - ) - }) - .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), - ))) - }), - ) - }) - .child( - Disclosure::new( - SharedString::from(format!( - "terminal-tool-disclosure-{}", - terminal.entity_id() - )), - is_expanded, - ) - .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| { - if is_expanded { - this.expanded_tool_calls.remove(&id); - } else { - this.expanded_tool_calls.insert(id.clone()); - } - } - })), - ); - - let terminal_view = self - .entry_view_state - .read(cx) - .entry(entry_ix) - .and_then(|entry| entry.terminal(terminal)); - let show_output = is_expanded && terminal_view.is_some(); - - v_flex() - .my_1p5() - .mx_5() - .border_1() - .when(tool_failed || command_failed, |card| card.border_dashed()) - .border_color(border_color) - .rounded_md() - .overflow_hidden() - .child( - v_flex() - .group(&header_group) - .py_1p5() - .pr_1p5() - .pl_2() - .gap_0p5() - .bg(header_bg) - .text_xs() - .child(header) - .child( - MarkdownElement::new( - command.clone(), - terminal_command_markdown_style(window, cx), - ) - .code_block_renderer( - markdown::CodeBlockRenderer::Default { - copy_button: false, - copy_button_on_hover: true, - border: false, - }, - ), - ), - ) - .when(show_output, |this| { - this.child( - div() - .pt_2() - .border_t_1() - .when(tool_failed || command_failed, |card| card.border_dashed()) - .border_color(border_color) - .bg(cx.theme().colors().editor_background) - .rounded_b_md() - .text_ui_sm(cx) - .h_full() - .children(terminal_view.map(|terminal_view| { - let element = if terminal_view - .read(cx) - .content_mode(window, cx) - .is_scrollable() - { - div().h_72().child(terminal_view).into_any_element() - } else { - terminal_view.into_any_element() - }; - - div() - .on_action(cx.listener(|_this, _: &NewTerminal, window, cx| { - window.dispatch_action(NewThread.boxed_clone(), cx); - cx.stop_propagation(); - })) - .child(element) - .into_any_element() - })), - ) - }) - .into_any() - } - - fn render_rules_item(&self, cx: &Context) -> Option { - let project_context = self - .as_native_thread(cx)? - .read(cx) - .project_context() - .read(cx); - - let user_rules_text = if project_context.user_rules.is_empty() { - None - } else if project_context.user_rules.len() == 1 { - let user_rules = &project_context.user_rules[0]; - - match user_rules.title.as_ref() { - Some(title) => Some(format!("Using \"{title}\" user rule")), - None => Some("Using user rule".into()), - } - } else { - Some(format!( - "Using {} user rules", - project_context.user_rules.len() - )) - }; - - let first_user_rules_id = project_context - .user_rules - .first() - .map(|user_rules| user_rules.uuid.0); - - let rules_files = project_context - .worktrees - .iter() - .filter_map(|worktree| worktree.rules_file.as_ref()) - .collect::>(); - - let rules_file_text = match rules_files.as_slice() { - &[] => None, - &[rules_file] => Some(format!( - "Using project {:?} file", - rules_file.path_in_worktree - )), - rules_files => Some(format!("Using {} project rules files", rules_files.len())), - }; - - if user_rules_text.is_none() && rules_file_text.is_none() { - return None; - } - - let has_both = user_rules_text.is_some() && rules_file_text.is_some(); - - Some( - h_flex() - .px_2p5() - .child( - Icon::new(IconName::Attach) - .size(IconSize::XSmall) - .color(Color::Disabled), - ) - .when_some(user_rules_text, |parent, user_rules_text| { - parent.child( - h_flex() - .id("user-rules") - .ml_1() - .mr_1p5() - .child( - Label::new(user_rules_text) - .size(LabelSize::XSmall) - .color(Color::Muted) - .truncate(), - ) - .hover(|s| s.bg(cx.theme().colors().element_hover)) - .tooltip(Tooltip::text("View User Rules")) - .on_click(move |_event, window, cx| { - window.dispatch_action( - Box::new(OpenRulesLibrary { - prompt_to_select: first_user_rules_id, - }), - cx, - ) - }), - ) - }) - .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() - .id("project-rules") - .ml_1p5() - .child( - Label::new(rules_file_text) - .size(LabelSize::XSmall) - .color(Color::Muted), - ) - .hover(|s| s.bg(cx.theme().colors().element_hover)) - .tooltip(Tooltip::text("View Project Rules")) - .on_click(cx.listener(Self::handle_open_rules)), - ) - }) - .into_any(), - ) - } - - fn render_empty_state_section_header( - &self, - label: impl Into, - action_slot: Option, - cx: &mut Context, - ) -> impl IntoElement { - div().pl_1().pr_1p5().child( - h_flex() - .mt_2() - .pl_1p5() - .pb_1() - .w_full() - .justify_between() - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .child( - Label::new(label.into()) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .children(action_slot), - ) - } - - fn render_recent_history(&self, cx: &mut Context) -> AnyElement { - let render_history = self - .agent - .clone() - .downcast::() - .is_some() - && self - .history_store - .update(cx, |history_store, cx| !history_store.is_empty(cx)); - - v_flex() - .size_full() - .when(render_history, |this| { - let recent_history: Vec<_> = self.history_store.update(cx, |history_store, _| { - history_store.entries().take(3).collect() - }); - this.justify_end().child( - v_flex() - .child( - self.render_empty_state_section_header( - "Recent", - Some( - Button::new("view-history", "View All") - .style(ButtonStyle::Subtle) - .label_size(LabelSize::Small) - .key_binding( - KeyBinding::for_action_in( - &OpenHistory, - &self.focus_handle(cx), - cx, - ) - .map(|kb| kb.size(rems_from_px(12.))), - ) - .on_click(move |_event, window, cx| { - window.dispatch_action(OpenHistory.boxed_clone(), cx); - }) - .into_any_element(), - ), - cx, - ), - ) - .child( - v_flex().p_1().pr_1p5().gap_1().children( - recent_history - .into_iter() - .enumerate() - .map(|(index, entry)| { - // TODO: Add keyboard navigation. - let is_hovered = - self.hovered_recent_history_item == Some(index); - crate::acp::thread_history::AcpHistoryEntryElement::new( - entry, - cx.entity().downgrade(), - ) - .hovered(is_hovered) - .on_hover(cx.listener( - move |this, is_hovered, _window, cx| { - if *is_hovered { - this.hovered_recent_history_item = Some(index); - } else if this.hovered_recent_history_item - == Some(index) - { - this.hovered_recent_history_item = None; - } - cx.notify(); - }, - )) - .into_any_element() - }), - ), - ), - ) - }) - .into_any() - } - - fn render_auth_required_state( - &self, - connection: &Rc, - description: Option<&Entity>, - configuration_view: Option<&AnyView>, - pending_auth_method: Option<&acp::AuthMethodId>, - window: &mut Window, - cx: &Context, - ) -> Div { - let show_description = - configuration_view.is_none() && description.is_none() && pending_auth_method.is_none(); - - let auth_methods = connection.auth_methods(); - - 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 {}.{}", - self.agent.name(), - if auth_methods.len() > 1 { - " Please choose one of the following options:" - } else { - "" - } - )) - .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_rotate_animation(2), - ) - .child(Label::new("Authenticating…").size(LabelSize::Small)), - ) - }) - .when(!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)| { - let (method_id, name) = if self - .project - .read(cx) - .is_via_remote_server() - && method.id.0.as_ref() == "oauth-personal" - && method.name == "Log in with Google" - { - ("spawn-gemini-cli".into(), "Log in with Gemini CLI".into()) - } else { - (method.id.0.clone(), method.name.clone()) - }; - - let agent_telemetry_id = connection.telemetry_id(); - - Button::new(method_id.clone(), name) - .label_size(LabelSize::Small) - .map(|this| { - if ix == 0 { - this.style(ButtonStyle::Tinted(TintColor::Warning)) - } else { - this.style(ButtonStyle::Outlined) - } - }) - .when_some( - method.description.clone(), - |this, description| { - this.tooltip(Tooltip::text(description)) - }, - ) - .on_click({ - cx.listener(move |this, _, window, cx| { - telemetry::event!( - "Authenticate Agent Started", - agent = agent_telemetry_id, - method = method_id - ); - - this.authenticate( - acp::AuthMethodId::new(method_id.clone()), - window, - cx, - ) - }) - }) - }, - )), - ) - }), - ) - } - - fn render_load_error( - &self, - e: &LoadError, - window: &mut Window, - cx: &mut Context, - ) -> AnyElement { - let (title, message, action_slot): (_, SharedString, _) = match e { - LoadError::Unsupported { - command: path, - current_version, - minimum_version, - } => { - return self.render_unsupported(path, current_version, minimum_version, window, cx); - } - LoadError::FailedToInstall(msg) => ( - "Failed to Install", - msg.into(), - Some(self.create_copy_button(msg.to_string()).into_any_element()), - ), - LoadError::Exited { status } => ( - "Failed to Launch", - format!("Server exited with status {status}").into(), - None, - ), - LoadError::Other(msg) => ( - "Failed to Launch", - msg.into(), - Some(self.create_copy_button(msg.to_string()).into_any_element()), - ), - }; - - Callout::new() - .severity(Severity::Error) - .icon(IconName::XCircleFilled) - .title(title) - .description(message) - .actions_slot(div().children(action_slot)) - .into_any_element() - } - - fn render_unsupported( - &self, - path: &SharedString, - version: &SharedString, - minimum_version: &SharedString, - _window: &mut Window, - cx: &mut Context, - ) -> AnyElement { - let (heading_label, description_label) = ( - format!("Upgrade {} to work with Zed", self.agent.name()), - if version.is_empty() { - format!( - "Currently using {}, which does not report a valid --version", - path, - ) - } else { - format!( - "Currently using {}, which is only version {} (need at least {minimum_version})", - path, version - ) - }, - ); - - 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.), - )) - .child( - v_flex().gap_0p5().child(Label::new(heading_label)).child( - Label::new(description_label) - .size(LabelSize::Small) - .color(Color::Muted), - ), - ) - .into_any_element() - } - - fn activity_bar_bg(&self, cx: &Context) -> Hsla { - let editor_bg_color = cx.theme().colors().editor_background; - let active_color = cx.theme().colors().element_selected; - editor_bg_color.blend(active_color.opacity(0.3)) - } - - fn render_activity_bar( - &self, - thread_entity: &Entity, - window: &mut Window, - cx: &Context, - ) -> Option { - let thread = thread_entity.read(cx); - let action_log = thread.action_log(); - let telemetry = ActionLogTelemetry::from(thread); - let changed_buffers = action_log.read(cx).changed_buffers(cx); - let plan = thread.plan(); - - if changed_buffers.is_empty() && plan.is_empty() { - return None; - } - - // Temporarily always enable ACP edit controls. This is temporary, to lessen the - // impact of a nasty bug that causes them to sometimes be disabled when they shouldn't - // be, which blocks you from being able to accept or reject edits. This switches the - // bug to be that sometimes it's enabled when it shouldn't be, which at least doesn't - // block you from using the panel. - let pending_edits = false; - - v_flex() - .mt_1() - .mx_2() - .bg(self.activity_bar_bg(cx)) - .border_1() - .border_b_0() - .border_color(cx.theme().colors().border) - .rounded_t_md() - .shadow(vec![gpui::BoxShadow { - color: gpui::black().opacity(0.15), - offset: point(px(1.), px(-1.)), - blur_radius: px(3.), - spread_radius: px(0.), - }]) - .when(!plan.is_empty(), |this| { - this.child(self.render_plan_summary(plan, window, cx)) - .when(self.plan_expanded, |parent| { - parent.child(self.render_plan_entries(plan, window, cx)) - }) - }) - .when(!plan.is_empty() && !changed_buffers.is_empty(), |this| { - this.child(Divider::horizontal().color(DividerColor::Border)) - }) - .when(!changed_buffers.is_empty(), |this| { - this.child(self.render_edits_summary( - &changed_buffers, - self.edits_expanded, - pending_edits, - cx, - )) - .when(self.edits_expanded, |parent| { - parent.child(self.render_edited_files( - action_log, - telemetry, - &changed_buffers, - pending_edits, - cx, - )) - }) - }) - .into_any() - .into() - } - - fn render_plan_summary( - &self, - plan: &Plan, - window: &mut Window, - cx: &Context, - ) -> impl IntoElement { - let stats = plan.stats(); - - let title = if let Some(entry) = stats.in_progress_entry - && !self.plan_expanded - { - h_flex() - .cursor_default() - .relative() - .w_full() - .gap_1() - .truncate() - .child( - Label::new("Current:") - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child( - div() - .text_xs() - .text_color(cx.theme().colors().text_muted) - .line_clamp(1) - .child(MarkdownElement::new( - entry.content.clone(), - plan_label_markdown_style(&entry.status, window, cx), - )), - ) - .when(stats.pending > 0, |this| { - this.child( - h_flex() - .absolute() - .top_0() - .right_0() - .h_full() - .child(div().min_w_8().h_full().bg(linear_gradient( - 90., - linear_color_stop(self.activity_bar_bg(cx), 1.), - linear_color_stop(self.activity_bar_bg(cx).opacity(0.2), 0.), - ))) - .child( - div().pr_0p5().bg(self.activity_bar_bg(cx)).child( - Label::new(format!("{} left", stats.pending)) - .size(LabelSize::Small) - .color(Color::Muted), - ), - ), - ) - }) - } else { - let status_label = if stats.pending == 0 { - "All Done".to_string() - } else if stats.completed == 0 { - format!("{} Tasks", plan.entries.len()) - } else { - format!("{}/{}", stats.completed, plan.entries.len()) - }; - - h_flex() - .w_full() - .gap_1() - .justify_between() - .child( - Label::new("Plan") - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child( - Label::new(status_label) - .size(LabelSize::Small) - .color(Color::Muted) - .mr_1(), - ) - }; - - h_flex() - .id("plan_summary") - .p_1() - .w_full() - .gap_1() - .when(self.plan_expanded, |this| { - this.border_b_1().border_color(cx.theme().colors().border) - }) - .child(Disclosure::new("plan_disclosure", self.plan_expanded)) - .child(title) - .on_click(cx.listener(|this, _, _, cx| { - this.plan_expanded = !this.plan_expanded; - cx.notify(); - })) - } - - fn render_plan_entries( - &self, - plan: &Plan, - window: &mut Window, - cx: &Context, - ) -> impl IntoElement { - v_flex() - .id("plan_items_list") - .max_h_40() - .overflow_y_scroll() - .children(plan.entries.iter().enumerate().flat_map(|(index, entry)| { - let element = h_flex() - .py_1() - .px_2() - .gap_2() - .justify_between() - .bg(cx.theme().colors().editor_background) - .when(index < plan.entries.len() - 1, |parent| { - parent.border_color(cx.theme().colors().border).border_b_1() - }) - .child( - h_flex() - .id(("plan_entry", index)) - .gap_1p5() - .max_w_full() - .overflow_x_scroll() - .text_xs() - .text_color(cx.theme().colors().text_muted) - .child(match entry.status { - acp::PlanEntryStatus::InProgress => { - Icon::new(IconName::TodoProgress) - .size(IconSize::Small) - .color(Color::Accent) - .with_rotate_animation(2) - .into_any_element() - } - acp::PlanEntryStatus::Completed => { - Icon::new(IconName::TodoComplete) - .size(IconSize::Small) - .color(Color::Success) - .into_any_element() - } - acp::PlanEntryStatus::Pending | _ => { - Icon::new(IconName::TodoPending) - .size(IconSize::Small) - .color(Color::Muted) - .into_any_element() - } - }) - .child(MarkdownElement::new( - entry.content.clone(), - plan_label_markdown_style(&entry.status, window, cx), - )), - ); - - Some(element) - })) - .into_any_element() - } - - fn render_edits_summary( - &self, - changed_buffers: &BTreeMap, Entity>, - expanded: bool, - pending_edits: bool, - cx: &Context, - ) -> Div { - const EDIT_NOT_READY_TOOLTIP_LABEL: &str = "Wait until file edits are complete."; - - let focus_handle = self.focus_handle(cx); - - 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") - .cursor_pointer() - .gap_1() - .child(Disclosure::new("edits-disclosure", expanded)) - .map(|this| { - if pending_edits { - this.child( - Label::new(format!( - "Editing {} {}…", - changed_buffers.len(), - if changed_buffers.len() == 1 { - "file" - } else { - "files" - } - )) - .color(Color::Muted) - .size(LabelSize::Small) - .with_animation( - "edit-label", - Animation::new(Duration::from_secs(2)) - .repeat() - .with_easing(pulsating_between(0.3, 0.7)), - |label, delta| label.alpha(delta), - ), - ) - } else { - this.child( - Label::new("Edits") - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child(Label::new("•").size(LabelSize::XSmall).color(Color::Muted)) - .child( - Label::new(format!( - "{} {}", - changed_buffers.len(), - if changed_buffers.len() == 1 { - "file" - } else { - "files" - } - )) - .size(LabelSize::Small) - .color(Color::Muted), - ) - } - }) - .on_click(cx.listener(|this, _, _, cx| { - this.edits_expanded = !this.edits_expanded; - cx.notify(); - })), - ) - .child( - h_flex() - .gap_1() - .child( - IconButton::new("review-changes", IconName::ListTodo) - .icon_size(IconSize::Small) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |_window, cx| { - Tooltip::for_action_in( - "Review Changes", - &OpenAgentDiff, - &focus_handle, - cx, - ) - } - }) - .on_click(cx.listener(|_, _, window, cx| { - window.dispatch_action(OpenAgentDiff.boxed_clone(), cx); - })), - ) - .child(Divider::vertical().color(DividerColor::Border)) - .child( - Button::new("reject-all-changes", "Reject All") - .label_size(LabelSize::Small) - .disabled(pending_edits) - .when(pending_edits, |this| { - this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL)) - }) - .key_binding( - KeyBinding::for_action_in(&RejectAll, &focus_handle.clone(), cx) - .map(|kb| kb.size(rems_from_px(10.))), - ) - .on_click(cx.listener(move |this, _, window, cx| { - this.reject_all(&RejectAll, window, cx); - })), - ) - .child( - Button::new("keep-all-changes", "Keep All") - .label_size(LabelSize::Small) - .disabled(pending_edits) - .when(pending_edits, |this| { - this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL)) - }) - .key_binding( - KeyBinding::for_action_in(&KeepAll, &focus_handle, cx) - .map(|kb| kb.size(rems_from_px(10.))), - ) - .on_click(cx.listener(move |this, _, window, cx| { - this.keep_all(&KeepAll, window, cx); - })), - ), - ) - } - - fn render_edited_files( - &self, - action_log: &Entity, - telemetry: ActionLogTelemetry, - changed_buffers: &BTreeMap, Entity>, - pending_edits: bool, - cx: &Context, - ) -> impl IntoElement { - let editor_bg_color = cx.theme().colors().editor_background; - - v_flex() - .id("edited_files_list") - .max_h_40() - .overflow_y_scroll() - .children( - changed_buffers - .iter() - .enumerate() - .flat_map(|(index, (buffer, _diff))| { - let file = buffer.read(cx).file()?; - let path = file.path(); - let path_style = file.path_style(cx); - let separator = file.path_style(cx).primary_separator(); - - let file_path = path.parent().and_then(|parent| { - if parent.is_empty() { - None - } else { - Some( - Label::new(format!( - "{}{separator}", - parent.display(path_style) - )) - .color(Color::Muted) - .size(LabelSize::XSmall) - .buffer_font(cx), - ) - } - }); - - let file_name = path.file_name().map(|name| { - Label::new(name.to_string()) - .size(LabelSize::XSmall) - .buffer_font(cx) - .ml_1p5() - }); - - let file_icon = FileIcons::get_icon(path.as_std_path(), cx) - .map(Icon::from_path) - .map(|icon| icon.color(Color::Muted).size(IconSize::Small)) - .unwrap_or_else(|| { - Icon::new(IconName::File) - .color(Color::Muted) - .size(IconSize::Small) - }); - - let overlay_gradient = linear_gradient( - 90., - linear_color_stop(editor_bg_color, 1.), - linear_color_stop(editor_bg_color.opacity(0.2), 0.), - ); - - let element = h_flex() - .group("edited-code") - .id(("file-container", index)) - .py_1() - .pl_2() - .pr_1() - .gap_2() - .justify_between() - .bg(editor_bg_color) - .when(index < changed_buffers.len() - 1, |parent| { - parent.border_color(cx.theme().colors().border).border_b_1() - }) - .child( - h_flex() - .id(("file-name-row", index)) - .relative() - .pr_8() - .w_full() - .overflow_x_scroll() - .child( - h_flex() - .id(("file-name-path", index)) - .cursor_pointer() - .pr_0p5() - .gap_0p5() - .hover(|s| s.bg(cx.theme().colors().element_hover)) - .rounded_xs() - .child(file_icon) - .children(file_name) - .children(file_path) - .tooltip(Tooltip::text("Go to File")) - .on_click({ - let buffer = buffer.clone(); - cx.listener(move |this, _, window, cx| { - this.open_edited_buffer(&buffer, window, cx); - }) - }), - ) - .child( - div() - .absolute() - .h_full() - .w_12() - .top_0() - .bottom_0() - .right_0() - .bg(overlay_gradient), - ), - ) - .child( - h_flex() - .gap_1() - .visible_on_hover("edited-code") - .child( - Button::new("review", "Review") - .label_size(LabelSize::Small) - .on_click({ - let buffer = buffer.clone(); - cx.listener(move |this, _, window, cx| { - this.open_edited_buffer(&buffer, window, cx); - }) - }), - ) - .child(Divider::vertical().color(DividerColor::BorderVariant)) - .child( - Button::new("reject-file", "Reject") - .label_size(LabelSize::Small) - .disabled(pending_edits) - .on_click({ - let buffer = buffer.clone(); - let action_log = action_log.clone(); - let telemetry = telemetry.clone(); - move |_, _, cx| { - action_log.update(cx, |action_log, cx| { - action_log - .reject_edits_in_ranges( - buffer.clone(), - vec![Anchor::min_max_range_for_buffer( - buffer.read(cx).remote_id(), - )], - Some(telemetry.clone()), - cx, - ) - .detach_and_log_err(cx); - }) - } - }), - ) - .child( - Button::new("keep-file", "Keep") - .label_size(LabelSize::Small) - .disabled(pending_edits) - .on_click({ - let buffer = buffer.clone(); - let action_log = action_log.clone(); - let telemetry = telemetry.clone(); - move |_, _, cx| { - action_log.update(cx, |action_log, cx| { - action_log.keep_edits_in_range( - buffer.clone(), - Anchor::min_max_range_for_buffer( - buffer.read(cx).remote_id(), - ), - Some(telemetry.clone()), - cx, - ); - }) - } - }), - ), - ); - - Some(element) - }), - ) - .into_any_element() - } - - fn render_message_editor(&mut self, window: &mut Window, cx: &mut Context) -> AnyElement { - let focus_handle = self.message_editor.focus_handle(cx); - let editor_bg_color = cx.theme().colors().editor_background; - let (expand_icon, expand_tooltip) = if self.editor_expanded { - (IconName::Minimize, "Minimize Message Editor") - } else { - (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::Ready { .. } => true, - ThreadState::Loading { .. } - | ThreadState::Unauthenticated { .. } - | ThreadState::LoadError(..) => false, - }; - - v_flex() - .on_action(cx.listener(Self::expand_message_editor)) - .on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| { - if let Some(profile_selector) = this.profile_selector.as_ref() { - profile_selector.read(cx).menu_handle().toggle(window, cx); - } else if let Some(mode_selector) = this.mode_selector() { - mode_selector.read(cx).menu_handle().toggle(window, cx); - } - })) - .on_action(cx.listener(|this, _: &CycleModeSelector, window, cx| { - if let Some(mode_selector) = this.mode_selector() { - mode_selector.update(cx, |mode_selector, cx| { - mode_selector.cycle_mode(window, cx); - }); - } - })) - .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| { - if let Some(model_selector) = this.model_selector.as_ref() { - model_selector - .update(cx, |model_selector, cx| model_selector.toggle(window, cx)); - } - })) - .p_2() - .gap_2() - .border_t_1() - .border_color(cx.theme().colors().border) - .bg(editor_bg_color) - .when(self.editor_expanded, |this| { - this.h(vh(0.8, window)).size_full().justify_between() - }) - .child( - v_flex() - .relative() - .size_full() - .pt_1() - .pr_2p5() - .child(self.message_editor.clone()) - .child( - h_flex() - .absolute() - .top_0() - .right_0() - .opacity(0.5) - .hover(|this| this.opacity(1.0)) - .child( - IconButton::new("toggle-height", expand_icon) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .tooltip({ - move |_window, cx| { - Tooltip::for_action_in( - expand_tooltip, - &ExpandMessageEditor, - &focus_handle, - cx, - ) - } - }) - .on_click(cx.listener(|this, _, window, cx| { - this.expand_message_editor( - &ExpandMessageEditor, - window, - cx, - ); - })), - ), - ), - ) - .child( - h_flex() - .flex_none() - .flex_wrap() - .justify_between() - .child( - h_flex() - .gap_0p5() - .child(self.render_add_context_button(cx)) - .child(self.render_follow_toggle(cx)) - .children(self.render_burn_mode_toggle(cx)), - ) - .child( - h_flex() - .gap_1() - .children(self.render_token_usage(cx)) - .children(self.profile_selector.clone()) - .children(self.mode_selector().cloned()) - .children(self.model_selector.clone()) - .child(self.render_send_button(cx)), - ), - ) - .when(!enable_editor, |this| this.child(backdrop)) - .into_any() - } - - pub(crate) fn as_native_connection( - &self, - cx: &App, - ) -> Option> { - let acp_thread = self.thread()?.read(cx); - acp_thread.connection().clone().downcast() - } - - pub(crate) fn as_native_thread(&self, cx: &App) -> Option> { - let acp_thread = self.thread()?.read(cx); - self.as_native_connection(cx)? - .thread(acp_thread.session_id(), cx) - } - - fn is_using_zed_ai_models(&self, cx: &App) -> bool { - self.as_native_thread(cx) - .and_then(|thread| thread.read(cx).model()) - .is_some_and(|model| model.provider_id() == language_model::ZED_CLOUD_PROVIDER_ID) - } - - fn render_token_usage(&self, cx: &mut Context) -> Option
{ - let thread = self.thread()?.read(cx); - let usage = thread.token_usage()?; - let is_generating = thread.status() != ThreadStatus::Idle; - - let used = crate::text_thread_editor::humanize_token_count(usage.used_tokens); - let max = crate::text_thread_editor::humanize_token_count(usage.max_tokens); - - Some( - h_flex() - .flex_shrink_0() - .gap_0p5() - .mr_1p5() - .child( - Label::new(used) - .size(LabelSize::Small) - .color(Color::Muted) - .map(|label| { - if is_generating { - label - .with_animation( - "used-tokens-label", - Animation::new(Duration::from_secs(2)) - .repeat() - .with_easing(pulsating_between(0.3, 0.8)), - |label, delta| label.alpha(delta), - ) - .into_any() - } else { - label.into_any_element() - } - }), - ) - .child( - Label::new("/") - .size(LabelSize::Small) - .color(Color::Custom(cx.theme().colors().text_muted.opacity(0.5))), - ) - .child(Label::new(max).size(LabelSize::Small).color(Color::Muted)), - ) - } - - fn toggle_burn_mode( - &mut self, - _: &ToggleBurnMode, - _window: &mut Window, - cx: &mut Context, - ) { - let Some(thread) = self.as_native_thread(cx) else { - return; - }; - - thread.update(cx, |thread, cx| { - let current_mode = thread.completion_mode(); - thread.set_completion_mode( - match current_mode { - CompletionMode::Burn => CompletionMode::Normal, - CompletionMode::Normal => CompletionMode::Burn, - }, - cx, - ); - }); - } - - fn keep_all(&mut self, _: &KeepAll, _window: &mut Window, cx: &mut Context) { - let Some(thread) = self.thread() else { - return; - }; - let telemetry = ActionLogTelemetry::from(thread.read(cx)); - let action_log = thread.read(cx).action_log().clone(); - action_log.update(cx, |action_log, cx| { - action_log.keep_all_edits(Some(telemetry), cx) - }); - } - - fn reject_all(&mut self, _: &RejectAll, _window: &mut Window, cx: &mut Context) { - let Some(thread) = self.thread() else { - return; - }; - let telemetry = ActionLogTelemetry::from(thread.read(cx)); - let action_log = thread.read(cx).action_log().clone(); - action_log - .update(cx, |action_log, cx| { - action_log.reject_all_edits(Some(telemetry), cx) - }) - .detach(); - } - - fn allow_always(&mut self, _: &AllowAlways, window: &mut Window, cx: &mut Context) { - self.authorize_pending_tool_call(acp::PermissionOptionKind::AllowAlways, window, cx); - } - - fn allow_once(&mut self, _: &AllowOnce, window: &mut Window, cx: &mut Context) { - self.authorize_pending_tool_call(acp::PermissionOptionKind::AllowOnce, window, cx); - } - - fn reject_once(&mut self, _: &RejectOnce, window: &mut Window, cx: &mut Context) { - self.authorize_pending_tool_call(acp::PermissionOptionKind::RejectOnce, window, cx); - } - - fn authorize_pending_tool_call( - &mut self, - kind: acp::PermissionOptionKind, - window: &mut Window, - cx: &mut Context, - ) -> Option<()> { - let thread = self.thread()?.read(cx); - let tool_call = thread.first_tool_awaiting_confirmation()?; - let ToolCallStatus::WaitingForConfirmation { options, .. } = &tool_call.status else { - return None; - }; - let option = options.iter().find(|o| o.kind == kind)?; - - self.authorize_tool_call( - tool_call.id.clone(), - option.option_id.clone(), - option.kind, - window, - cx, - ); - - Some(()) - } - - fn render_burn_mode_toggle(&self, cx: &mut Context) -> Option { - let thread = self.as_native_thread(cx)?.read(cx); - - if thread - .model() - .is_none_or(|model| !model.supports_burn_mode()) - { - return None; - } - - let active_completion_mode = thread.completion_mode(); - let burn_mode_enabled = active_completion_mode == CompletionMode::Burn; - let icon = if burn_mode_enabled { - IconName::ZedBurnModeOn - } else { - IconName::ZedBurnMode - }; - - Some( - IconButton::new("burn-mode", icon) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .toggle_state(burn_mode_enabled) - .selected_icon_color(Color::Error) - .on_click(cx.listener(|this, _event, window, cx| { - this.toggle_burn_mode(&ToggleBurnMode, window, cx); - })) - .tooltip(move |_window, cx| { - cx.new(|_| BurnModeTooltip::new().selected(burn_mode_enabled)) - .into() - }) - .into_any_element(), - ) - } - - fn render_send_button(&self, cx: &mut Context) -> AnyElement { - let is_editor_empty = self.message_editor.read(cx).is_empty(cx); - let is_generating = self - .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 { - IconButton::new("stop-generation", IconName::Stop) - .icon_color(Color::Error) - .style(ButtonStyle::Tinted(ui::TintColor::Error)) - .tooltip(move |_window, cx| { - Tooltip::for_action("Stop Generation", &editor::actions::Cancel, cx) - }) - .on_click(cx.listener(|this, _event, _, cx| this.cancel_generation(cx))) - .into_any_element() - } else { - let send_btn_tooltip = if is_editor_empty && !is_generating { - "Type to Send" - } else if is_generating { - "Stop and Send Message" - } else { - "Send" - }; - - IconButton::new("send-message", IconName::Send) - .style(ButtonStyle::Filled) - .map(|this| { - if is_editor_empty && !is_generating { - this.disabled(true).icon_color(Color::Muted) - } else { - this.icon_color(Color::Accent) - } - }) - .tooltip(move |_window, cx| Tooltip::for_action(send_btn_tooltip, &Chat, cx)) - .on_click(cx.listener(|this, _, window, cx| { - this.send(window, cx); - })) - .into_any_element() - } - } - - 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()) - } - }; - - IconButton::new("follow-agent", IconName::Crosshair) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .toggle_state(following) - .selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor))) - .tooltip(move |_window, cx| { - if following { - Tooltip::for_action(tooltip_label.clone(), &Follow, cx) - } else { - Tooltip::with_meta( - tooltip_label.clone(), - Some(&Follow), - "Track the agent's location as it reads and edits files.", - cx, - ) - } - }) - .on_click(cx.listener(move |this, _, window, cx| { - this.toggle_following(window, cx); - })) - } - - fn render_add_context_button(&self, cx: &mut Context) -> impl IntoElement { - let message_editor = self.message_editor.clone(); - let menu_visible = message_editor.read(cx).is_completions_menu_visible(cx); - - IconButton::new("add-context", IconName::AtSign) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .when(!menu_visible, |this| { - this.tooltip(move |_window, cx| { - Tooltip::with_meta("Add Context", None, "Or type @ to include context", cx) - }) - }) - .on_click(cx.listener(move |_this, _, window, cx| { - let message_editor_clone = message_editor.clone(); - - window.defer(cx, move |window, cx| { - message_editor_clone.update(cx, |message_editor, cx| { - message_editor.trigger_completion_menu(window, cx); - }); - }); - })) - } - - fn render_markdown(&self, markdown: Entity, style: MarkdownStyle) -> MarkdownElement { - let workspace = self.workspace.clone(); - MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| { - Self::open_link(text, &workspace, window, cx); - }) - } - - fn open_link( - url: SharedString, - workspace: &WeakEntity, - window: &mut Window, - cx: &mut App, - ) { - let Some(workspace) = workspace.upgrade() else { - cx.open_url(&url); - return; - }; - - if let Some(mention) = MentionUri::parse(&url, workspace.read(cx).path_style(cx)).log_err() - { - workspace.update(cx, |workspace, cx| match mention { - MentionUri::File { abs_path } => { - let project = workspace.project(); - let Some(path) = - project.update(cx, |project, cx| project.find_project_path(abs_path, cx)) - else { - return; - }; - - workspace - .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_id) = project.update(cx, |project, cx| { - let path = project.find_project_path(abs_path, cx)?; - project.entry_for_path(&path, cx).map(|entry| entry.id) - }) else { - return; - }; - - project.update(cx, |_, cx| { - cx.emit(project::Event::RevealInProjectPanel(entry_id)); - }); - } - MentionUri::Symbol { - abs_path: path, - line_range, - .. - } - | MentionUri::Selection { - abs_path: Some(path), - line_range, - } => { - let project = workspace.project(); - let Some(path) = - project.update(cx, |project, cx| project.find_project_path(path, cx)) - else { - return; - }; - - let item = workspace.open_path(path, None, true, window, cx); - window - .spawn(cx, async move |cx| { - let Some(editor) = item.await?.downcast::() else { - return Ok(()); - }; - let range = Point::new(*line_range.start(), 0) - ..Point::new(*line_range.start(), 0); - editor - .update_in(cx, |editor, window, cx| { - editor.change_selections( - SelectionEffects::scroll(Autoscroll::center()), - window, - cx, - |s| s.select_ranges(vec![range]), - ); - }) - .ok(); - anyhow::Ok(()) - }) - .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| { - panel.load_agent_thread( - DbThreadMetadata { - id, - title: name.into(), - updated_at: Default::default(), - }, - window, - cx, - ) - }); - } - } - MentionUri::TextThread { path, .. } => { - if let Some(panel) = workspace.panel::(cx) { - panel.update(cx, |panel, cx| { - panel - .open_saved_text_thread(path.as_path().into(), window, cx) - .detach_and_log_err(cx); - }); - } - } - MentionUri::Rule { id, .. } => { - let PromptId::User { uuid } = id else { - return; - }; - window.dispatch_action( - Box::new(OpenRulesLibrary { - prompt_to_select: Some(uuid.0), - }), - cx, - ) - } - MentionUri::Fetch { url } => { - cx.open_url(url.as_str()); - } - }) - } else { - cx.open_url(&url); - } - } - - fn open_tool_call_location( - &self, - entry_ix: usize, - location_ix: usize, - window: &mut Window, - cx: &mut Context, - ) -> Option<()> { - let (tool_call_location, agent_location) = self - .thread()? - .read(cx) - .entries() - .get(entry_ix)? - .location(location_ix)?; - - let project_path = self - .project - .read(cx) - .find_project_path(&tool_call_location.path, cx)?; - - let open_task = self - .workspace - .update(cx, |workspace, cx| { - workspace.open_path(project_path, None, true, window, cx) - }) - .log_err()?; - window - .spawn(cx, async move |cx| { - let item = open_task.await?; - - let Some(active_editor) = item.downcast::() else { - return anyhow::Ok(()); - }; - - active_editor.update_in(cx, |editor, window, cx| { - let multibuffer = editor.buffer().read(cx); - let buffer = multibuffer.as_singleton(); - if agent_location.buffer.upgrade() == buffer { - let excerpt_id = multibuffer.excerpt_ids().first().cloned(); - let anchor = - editor::Anchor::in_buffer(excerpt_id.unwrap(), agent_location.position); - editor.change_selections(Default::default(), window, cx, |selections| { - selections.select_anchor_ranges([anchor..anchor]); - }) - } else { - let row = tool_call_location.line.unwrap_or_default(); - editor.change_selections(Default::default(), window, cx, |selections| { - selections.select_ranges([Point::new(row, 0)..Point::new(row, 0)]); - }) - } - })?; - - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - - None - } - - pub fn open_thread_as_markdown( - &self, - workspace: Entity, - window: &mut Window, - cx: &mut App, - ) -> Task> { - let markdown_language_task = workspace - .read(cx) - .app_state() - .languages - .language_for_name("Markdown"); - - let (thread_title, markdown) = if let Some(thread) = self.thread() { - let thread = thread.read(cx); - (thread.title().to_string(), thread.to_markdown(cx)) - } else { - return Task::ready(Ok(())); - }; - - let project = workspace.read(cx).project().clone(); - window.spawn(cx, async move |cx| { - let markdown_language = markdown_language_task.await?; - - let buffer = project - .update(cx, |project, cx| project.create_buffer(false, cx))? - .await?; - - buffer.update(cx, |buffer, cx| { - buffer.set_text(markdown, cx); - buffer.set_language(Some(markdown_language), cx); - buffer.set_capability(language::Capability::ReadWrite, cx); - })?; - - workspace.update_in(cx, |workspace, window, cx| { - let buffer = cx - .new(|cx| MultiBuffer::singleton(buffer, cx).with_title(thread_title.clone())); - - workspace.add_item_to_active_pane( - Box::new(cx.new(|cx| { - let mut editor = - Editor::for_multibuffer(buffer, Some(project.clone()), window, cx); - editor.set_breadcrumb_header(thread_title); - editor - })), - None, - true, - window, - cx, - ); - })?; - anyhow::Ok(()) - }) - } - - fn scroll_to_top(&mut self, cx: &mut Context) { - self.list_state.scroll_to(ListOffset::default()); - cx.notify(); - } - - pub fn scroll_to_bottom(&mut self, cx: &mut Context) { - if let Some(thread) = self.thread() { - let entry_count = thread.read(cx).entries().len(); - self.list_state.reset(entry_count); - cx.notify(); - } - } - - fn notify_with_sound( - &mut self, - caption: impl Into, - icon: IconName, - window: &mut Window, - cx: &mut Context, - ) { - self.play_notification_sound(window, cx); - self.show_notification(caption, icon, window, cx); - } - - fn play_notification_sound(&self, window: &Window, cx: &mut App) { - let settings = AgentSettings::get_global(cx); - if settings.play_sound_when_agent_done && !window.is_window_active() { - Audio::play_sound(Sound::AgentDone, cx); - } - } - - fn show_notification( - &mut self, - caption: impl Into, - icon: IconName, - window: &mut Window, - cx: &mut Context, - ) { - if !self.notifications.is_empty() { - return; - } - - let settings = AgentSettings::get_global(cx); - - let window_is_inactive = !window.is_window_active(); - let panel_is_hidden = self - .workspace - .upgrade() - .map(|workspace| AgentPanel::is_hidden(&workspace, cx)) - .unwrap_or(true); - - let should_notify = window_is_inactive || panel_is_hidden; - - if !should_notify { - return; - } - - // TODO: Change this once we have title summarization for external agents. - let title = self.agent.name(); - - match settings.notify_when_agent_waiting { - NotifyWhenAgentWaiting::PrimaryScreen => { - if let Some(primary) = cx.primary_display() { - self.pop_up(icon, caption.into(), title, window, primary, cx); - } - } - NotifyWhenAgentWaiting::AllScreens => { - let caption = caption.into(); - for screen in cx.displays() { - self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx); - } - } - NotifyWhenAgentWaiting::Never => { - // Don't show anything - } - } - } - - fn pop_up( - &mut self, - icon: IconName, - caption: SharedString, - title: SharedString, - window: &mut Window, - screen: Rc, - cx: &mut Context, - ) { - let options = AgentNotification::window_options(screen, cx); - - let project_name = self.workspace.upgrade().and_then(|workspace| { - workspace - .read(cx) - .project() - .read(cx) - .visible_worktrees(cx) - .next() - .map(|worktree| worktree.read(cx).root_name_str().to_string()) - }); - - if let Some(screen_window) = cx - .open_window(options, |_, cx| { - cx.new(|_| { - AgentNotification::new(title.clone(), caption.clone(), icon, project_name) - }) - }) - .log_err() - && let Some(pop_up) = screen_window.entity(cx).log_err() - { - self.notification_subscriptions - .entry(screen_window) - .or_insert_with(Vec::new) - .push(cx.subscribe_in(&pop_up, window, { - |this, _, event, window, cx| match event { - AgentNotificationEvent::Accepted => { - let handle = window.window_handle(); - cx.activate(true); - - let workspace_handle = this.workspace.clone(); - - // If there are multiple Zed windows, activate the correct one. - cx.defer(move |cx| { - handle - .update(cx, |_view, window, _cx| { - window.activate_window(); - - if let Some(workspace) = workspace_handle.upgrade() { - workspace.update(_cx, |workspace, cx| { - workspace.focus_panel::(window, cx); - }); - } - }) - .log_err(); - }); - - this.dismiss_notifications(cx); - } - AgentNotificationEvent::Dismissed => { - this.dismiss_notifications(cx); - } - } - })); - - self.notifications.push(screen_window); - - // If the user manually refocuses the original window, dismiss the popup. - self.notification_subscriptions - .entry(screen_window) - .or_insert_with(Vec::new) - .push({ - let pop_up_weak = pop_up.downgrade(); - - cx.observe_window_activation(window, move |_, window, cx| { - if window.is_window_active() - && let Some(pop_up) = pop_up_weak.upgrade() - { - pop_up.update(cx, |_, cx| { - cx.emit(AgentNotificationEvent::Dismissed); - }); - } - }) - }); - } - } - - fn dismiss_notifications(&mut self, cx: &mut Context) { - for window in self.notifications.drain(..) { - window - .update(cx, |_, window, _| { - window.remove_window(); - }) - .ok(); - - self.notification_subscriptions.remove(&window); - } - } - - fn render_generating(&self, confirmation: bool) -> impl IntoElement { - h_flex() - .id("generating-spinner") - .py_2() - .px(rems_from_px(22.)) - .map(|this| { - if confirmation { - this.gap_2() - .child( - h_flex() - .w_2() - .child(SpinnerLabel::sand().size(LabelSize::Small)), - ) - .child( - LoadingLabel::new("Waiting Confirmation") - .size(LabelSize::Small) - .color(Color::Muted), - ) - } else { - this.child(SpinnerLabel::new().size(LabelSize::Small)) - } - }) - .into_any_element() - } - - 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 self.render_generating(false).into_any_element(); - } - - let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::Small) - .icon_color(Color::Ignored) - .tooltip(Tooltip::text("Open Thread as Markdown")) - .on_click(cx.listener(move |this, _, window, cx| { - if let Some(workspace) = this.workspace.upgrade() { - this.open_thread_as_markdown(workspace, window, cx) - .detach_and_log_err(cx); - } - })); - - let scroll_to_top = IconButton::new("scroll_to_top", IconName::ArrowUp) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::Small) - .icon_color(Color::Ignored) - .tooltip(Tooltip::text("Scroll To Top")) - .on_click(cx.listener(move |this, _, _, cx| { - this.scroll_to_top(cx); - })); - - let mut container = h_flex() - .w_full() - .py_2() - .px_5() - .gap_px() - .opacity(0.6) - .hover(|s| s.opacity(1.)) - .justify_end(); - - if AgentSettings::get_global(cx).enable_feedback - && self - .thread() - .is_some_and(|thread| thread.read(cx).connection().telemetry().is_some()) - { - let feedback = self.thread_feedback.feedback; - - let tooltip_meta = || { - SharedString::new( - "Rating the thread sends all of your current conversation to the Zed team.", - ) - }; - - container = container - .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(move |window, cx| match feedback { - Some(ThreadFeedback::Positive) => { - Tooltip::text("Thanks for your feedback!")(window, cx) - } - _ => Tooltip::with_meta("Helpful Response", None, tooltip_meta(), cx), - }) - .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(move |window, cx| match feedback { - Some(ThreadFeedback::Negative) => { - Tooltip::text( - "We appreciate your feedback and will use it to improve in the future.", - )(window, cx) - } - _ => { - Tooltip::with_meta("Not Helpful Response", None, tooltip_meta(), cx) - } - }) - .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) - .into_any_element() - } - - fn render_feedback_feedback_editor(editor: Entity, cx: &Context) -> Div { - h_flex() - .key_context("AgentFeedbackMessageEditor") - .on_action(cx.listener(move |this, _: &menu::Cancel, _, cx| { - this.thread_feedback.dismiss_comments(); - cx.notify(); - })) - .on_action(cx.listener(move |this, _: &menu::Confirm, _window, cx| { - this.submit_feedback_message(cx); - })) - .p_2() - .mb_2() - .mx_5() - .gap_1() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().editor_background) - .child(div().w_full().child(editor)) - .child( - h_flex() - .child( - IconButton::new("dismiss-feedback-message", IconName::Close) - .icon_color(Color::Error) - .icon_size(IconSize::XSmall) - .shape(ui::IconButtonShape::Square) - .on_click(cx.listener(move |this, _, _window, cx| { - this.thread_feedback.dismiss_comments(); - cx.notify(); - })), - ) - .child( - IconButton::new("submit-feedback-message", IconName::Return) - .icon_size(IconSize::XSmall) - .shape(ui::IconButtonShape::Square) - .on_click(cx.listener(move |this, _, _window, cx| { - this.submit_feedback_message(cx); - })), - ), - ) - } - - fn handle_feedback_click( - &mut self, - feedback: ThreadFeedback, - window: &mut Window, - cx: &mut Context, - ) { - let Some(thread) = self.thread().cloned() else { - return; - }; - - self.thread_feedback.submit(thread, feedback, window, cx); - cx.notify(); - } - - fn submit_feedback_message(&mut self, cx: &mut Context) { - let Some(thread) = self.thread().cloned() else { - return; - }; - - self.thread_feedback.submit_comments(thread, cx); - cx.notify(); - } - - fn render_token_limit_callout( - &self, - line_height: Pixels, - cx: &mut Context, - ) -> Option { - let token_usage = self.thread()?.read(cx).token_usage()?; - let ratio = token_usage.ratio(); - - let (severity, title) = match ratio { - acp_thread::TokenUsageRatio::Normal => return None, - acp_thread::TokenUsageRatio::Warning => { - (Severity::Warning, "Thread reaching the token limit soon") - } - acp_thread::TokenUsageRatio::Exceeded => { - (Severity::Error, "Thread reached the token limit") - } - }; - - let burn_mode_available = self.as_native_thread(cx).is_some_and(|thread| { - thread.read(cx).completion_mode() == CompletionMode::Normal - && thread - .read(cx) - .model() - .is_some_and(|model| model.supports_burn_mode()) - }); - - let description = if burn_mode_available { - "To continue, start a new thread from a summary or turn Burn Mode on." - } else { - "To continue, start a new thread from a summary." - }; - - Some( - Callout::new() - .severity(severity) - .line_height(line_height) - .title(title) - .description(description) - .actions_slot( - h_flex() - .gap_0p5() - .child( - Button::new("start-new-thread", "Start New Thread") - .label_size(LabelSize::Small) - .on_click(cx.listener(|this, _, window, cx| { - let Some(thread) = this.thread() else { - return; - }; - let session_id = thread.read(cx).session_id().clone(); - window.dispatch_action( - crate::NewNativeAgentThreadFromSummary { - from_session_id: session_id, - } - .boxed_clone(), - cx, - ); - })), - ) - .when(burn_mode_available, |this| { - this.child( - IconButton::new("burn-mode-callout", IconName::ZedBurnMode) - .icon_size(IconSize::XSmall) - .on_click(cx.listener(|this, _event, window, cx| { - this.toggle_burn_mode(&ToggleBurnMode, window, cx); - })), - ) - }), - ), - ) - } - - fn render_usage_callout(&self, line_height: Pixels, cx: &mut Context) -> Option
{ - if !self.is_using_zed_ai_models(cx) { - return None; - } - - let user_store = self.project.read(cx).user_store().read(cx); - if user_store.is_usage_based_billing_enabled() { - return None; - } - - let plan = user_store - .plan() - .unwrap_or(cloud_llm_client::Plan::V1(PlanV1::ZedFree)); - - let usage = user_store.model_request_usage()?; - - Some( - div() - .child(UsageCallout::new(plan, usage)) - .line_height(line_height), - ) - } - - fn agent_ui_font_size_changed(&mut self, _window: &mut Window, cx: &mut Context) { - self.entry_view_state.update(cx, |entry_view_state, cx| { - entry_view_state.agent_ui_font_size_changed(cx); - }); - } - - pub(crate) fn insert_dragged_files( - &self, - paths: Vec, - added_worktrees: Vec>, - window: &mut Window, - cx: &mut Context, - ) { - self.message_editor.update(cx, |message_editor, cx| { - message_editor.insert_dragged_files(paths, added_worktrees, window, cx); - }) - } - - /// Inserts the selected text into the message editor or the message being - /// edited, if any. - pub(crate) fn insert_selections(&self, window: &mut Window, cx: &mut Context) { - self.active_editor(cx).update(cx, |editor, cx| { - editor.insert_selections(window, cx); - }); - } - - fn render_thread_retry_status_callout( - &self, - _window: &mut Window, - _cx: &mut Context, - ) -> Option { - let state = self.thread_retry_status.as_ref()?; - - let next_attempt_in = state - .duration - .saturating_sub(Instant::now().saturating_duration_since(state.started_at)); - if next_attempt_in.is_zero() { - return None; - } - - let next_attempt_in_secs = next_attempt_in.as_secs() + 1; - - let retry_message = if state.max_attempts == 1 { - if next_attempt_in_secs == 1 { - "Retrying. Next attempt in 1 second.".to_string() - } else { - format!("Retrying. Next attempt in {next_attempt_in_secs} seconds.") - } - } else if next_attempt_in_secs == 1 { - format!( - "Retrying. Next attempt in 1 second (Attempt {} of {}).", - state.attempt, state.max_attempts, - ) - } else { - format!( - "Retrying. Next attempt in {next_attempt_in_secs} seconds (Attempt {} of {}).", - state.attempt, state.max_attempts, - ) - }; - - Some( - Callout::new() - .severity(Severity::Warning) - .title(state.last_error.clone()) - .description(retry_message), - ) - } - - fn render_codex_windows_warning(&self, cx: &mut Context) -> Callout { - Callout::new() - .icon(IconName::Warning) - .severity(Severity::Warning) - .title("Codex on Windows") - .description("For best performance, run Codex in Windows Subsystem for Linux (WSL2)") - .actions_slot( - Button::new("open-wsl-modal", "Open in WSL") - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .on_click(cx.listener({ - move |_, _, _window, cx| { - #[cfg(windows)] - _window.dispatch_action( - zed_actions::wsl_actions::OpenWsl::default().boxed_clone(), - cx, - ); - cx.notify(); - } - })), - ) - .dismiss_action( - IconButton::new("dismiss", IconName::Close) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .tooltip(Tooltip::text("Dismiss Warning")) - .on_click(cx.listener({ - move |this, _, _, cx| { - this.show_codex_windows_warning = false; - cx.notify(); - } - })), - ) - } - - fn render_thread_error(&mut self, window: &mut Window, cx: &mut Context) -> Option
{ - let content = match self.thread_error.as_ref()? { - ThreadError::Other(error) => self.render_any_thread_error(error.clone(), window, cx), - ThreadError::Refusal => self.render_refusal_error(cx), - ThreadError::AuthenticationRequired(error) => { - self.render_authentication_required_error(error.clone(), cx) - } - ThreadError::PaymentRequired => self.render_payment_required_error(cx), - ThreadError::ModelRequestLimitReached(plan) => { - self.render_model_request_limit_reached_error(*plan, cx) - } - ThreadError::ToolUseLimitReached => self.render_tool_use_limit_reached_error(cx)?, - }; - - Some(div().child(content)) - } - - fn render_new_version_callout(&self, version: &SharedString, cx: &mut Context) -> Div { - v_flex().w_full().justify_end().child( - h_flex() - .p_2() - .pr_3() - .w_full() - .gap_1p5() - .border_t_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().element_background) - .child( - h_flex() - .flex_1() - .gap_1p5() - .child( - Icon::new(IconName::Download) - .color(Color::Accent) - .size(IconSize::Small), - ) - .child(Label::new("New version available").size(LabelSize::Small)), - ) - .child( - Button::new("update-button", format!("Update to v{}", version)) - .label_size(LabelSize::Small) - .style(ButtonStyle::Tinted(TintColor::Accent)) - .on_click(cx.listener(|this, _, window, cx| { - this.reset(window, cx); - })), - ), - ) - } - - fn current_mode_id(&self, cx: &App) -> Option> { - if let Some(thread) = self.as_native_thread(cx) { - Some(thread.read(cx).profile().0.clone()) - } else if let Some(mode_selector) = self.mode_selector() { - Some(mode_selector.read(cx).mode().0) - } else { - None - } - } - - fn current_model_id(&self, cx: &App) -> Option { - self.model_selector - .as_ref() - .and_then(|selector| selector.read(cx).active_model(cx).map(|m| m.id.to_string())) - } - - fn current_model_name(&self, cx: &App) -> SharedString { - // For native agent (Zed Agent), use the specific model name (e.g., "Claude 3.5 Sonnet") - // For ACP agents, use the agent name (e.g., "Claude Code", "Gemini CLI") - // This provides better clarity about what refused the request - if self.as_native_connection(cx).is_some() { - self.model_selector - .as_ref() - .and_then(|selector| selector.read(cx).active_model(cx)) - .map(|model| model.name.clone()) - .unwrap_or_else(|| SharedString::from("The model")) - } else { - // ACP agent - use the agent name (e.g., "Claude Code", "Gemini CLI") - self.agent.name() - } - } - - fn render_refusal_error(&self, cx: &mut Context<'_, Self>) -> Callout { - let model_or_agent_name = self.current_model_name(cx); - let refusal_message = format!( - "{} refused to respond to this prompt. This can happen when a model believes the prompt violates its content policy or safety guidelines, so rephrasing it can sometimes address the issue.", - model_or_agent_name - ); - - Callout::new() - .severity(Severity::Error) - .title("Request Refused") - .icon(IconName::XCircle) - .description(refusal_message.clone()) - .actions_slot(self.create_copy_button(&refusal_message)) - .dismiss_action(self.dismiss_error_button(cx)) - } - - fn render_any_thread_error( - &mut self, - error: SharedString, - window: &mut Window, - 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 - }); - - let markdown = if let Some(markdown) = &self.thread_error_markdown { - markdown.clone() - } else { - let markdown = cx.new(|cx| Markdown::new(error.clone(), None, None, cx)); - self.thread_error_markdown = Some(markdown.clone()); - markdown - }; - - let markdown_style = default_markdown_style(false, true, window, cx); - let description = self - .render_markdown(markdown, markdown_style) - .into_any_element(); - - Callout::new() - .severity(Severity::Error) - .icon(IconName::XCircle) - .title("An Error Happened") - .description_slot(description) - .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( - IconButton::new("retry", IconName::RotateCw) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Retry Generation")) - .on_click(cx.listener(|this, _, _window, cx| { - this.resume_chat(cx); - })), - ) - }) - .child(self.create_copy_button(error.to_string())), - ) - .dismiss_action(self.dismiss_error_button(cx)) - } - - fn render_payment_required_error(&self, cx: &mut Context) -> Callout { - const ERROR_MESSAGE: &str = - "You reached your free usage limit. Upgrade to Zed Pro for more prompts."; - - Callout::new() - .severity(Severity::Error) - .icon(IconName::XCircle) - .title("Free Usage Exceeded") - .description(ERROR_MESSAGE) - .actions_slot( - h_flex() - .gap_0p5() - .child(self.upgrade_button(cx)) - .child(self.create_copy_button(ERROR_MESSAGE)), - ) - .dismiss_action(self.dismiss_error_button(cx)) - } - - fn render_authentication_required_error( - &self, - error: SharedString, - cx: &mut Context, - ) -> Callout { - Callout::new() - .severity(Severity::Error) - .title("Authentication Required") - .icon(IconName::XCircle) - .description(error.clone()) - .actions_slot( - h_flex() - .gap_0p5() - .child(self.authenticate_button(cx)) - .child(self.create_copy_button(error)), - ) - .dismiss_action(self.dismiss_error_button(cx)) - } - - fn render_model_request_limit_reached_error( - &self, - plan: cloud_llm_client::Plan, - cx: &mut Context, - ) -> Callout { - let error_message = match plan { - cloud_llm_client::Plan::V1(PlanV1::ZedPro) => { - "Upgrade to usage-based billing for more prompts." - } - cloud_llm_client::Plan::V1(PlanV1::ZedProTrial) - | cloud_llm_client::Plan::V1(PlanV1::ZedFree) => "Upgrade to Zed Pro for more prompts.", - cloud_llm_client::Plan::V2(_) => "", - }; - - Callout::new() - .severity(Severity::Error) - .title("Model Prompt Limit Reached") - .icon(IconName::XCircle) - .description(error_message) - .actions_slot( - h_flex() - .gap_0p5() - .child(self.upgrade_button(cx)) - .child(self.create_copy_button(error_message)), - ) - .dismiss_action(self.dismiss_error_button(cx)) - } - - fn render_tool_use_limit_reached_error(&self, cx: &mut Context) -> Option { - let thread = self.as_native_thread(cx)?; - let supports_burn_mode = thread - .read(cx) - .model() - .is_some_and(|model| model.supports_burn_mode()); - - let focus_handle = self.focus_handle(cx); - - Some( - Callout::new() - .icon(IconName::Info) - .title("Consecutive tool use limit reached.") - .actions_slot( - h_flex() - .gap_0p5() - .when(supports_burn_mode, |this| { - this.child( - Button::new("continue-burn-mode", "Continue with Burn Mode") - .style(ButtonStyle::Filled) - .style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .layer(ElevationIndex::ModalSurface) - .label_size(LabelSize::Small) - .key_binding( - KeyBinding::for_action_in( - &ContinueWithBurnMode, - &focus_handle, - cx, - ) - .map(|kb| kb.size(rems_from_px(10.))), - ) - .tooltip(Tooltip::text( - "Enable Burn Mode for unlimited tool use.", - )) - .on_click({ - cx.listener(move |this, _, _window, cx| { - thread.update(cx, |thread, cx| { - thread - .set_completion_mode(CompletionMode::Burn, cx); - }); - this.resume_chat(cx); - }) - }), - ) - }) - .child( - Button::new("continue-conversation", "Continue") - .layer(ElevationIndex::ModalSurface) - .label_size(LabelSize::Small) - .key_binding( - KeyBinding::for_action_in(&ContinueThread, &focus_handle, cx) - .map(|kb| kb.size(rems_from_px(10.))), - ) - .on_click(cx.listener(|this, _, _window, cx| { - this.resume_chat(cx); - })), - ), - ), - ) - } - - fn create_copy_button(&self, message: impl Into) -> impl IntoElement { - let message = message.into(); - - IconButton::new("copy", IconName::Copy) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Copy Error Message")) - .on_click(move |_, _, cx| { - cx.write_to_clipboard(ClipboardItem::new_string(message.clone())) - }) - } - - fn dismiss_error_button(&self, cx: &mut Context) -> impl IntoElement { - IconButton::new("dismiss", IconName::Close) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Dismiss Error")) - .on_click(cx.listener({ - move |this, _, _, cx| { - this.clear_thread_error(cx); - cx.notify(); - } - })) - } - - fn authenticate_button(&self, cx: &mut Context) -> impl IntoElement { - Button::new("authenticate", "Authenticate") - .label_size(LabelSize::Small) - .style(ButtonStyle::Filled) - .on_click(cx.listener({ - move |this, _, window, cx| { - let agent = this.agent.clone(); - let ThreadState::Ready { thread, .. } = &this.thread_state else { - return; - }; - - let connection = thread.read(cx).connection().clone(); - let err = AuthRequired { - description: None, - provider_id: None, - }; - this.clear_thread_error(cx); - if let Some(message) = this.in_flight_prompt.take() { - this.message_editor.update(cx, |editor, cx| { - editor.set_message(message, window, cx); - }); - } - let this = cx.weak_entity(); - window.defer(cx, |window, cx| { - Self::handle_auth_required(this, err, agent, connection, window, cx); - }) - } - })) - } - - pub(crate) fn reauthenticate(&mut self, window: &mut Window, cx: &mut Context) { - let agent = self.agent.clone(); - let ThreadState::Ready { thread, .. } = &self.thread_state else { - return; - }; - - let connection = thread.read(cx).connection().clone(); - let err = AuthRequired { - description: None, - provider_id: None, - }; - self.clear_thread_error(cx); - let this = cx.weak_entity(); - window.defer(cx, |window, cx| { - Self::handle_auth_required(this, err, agent, connection, window, cx); - }) - } - - fn upgrade_button(&self, cx: &mut Context) -> impl IntoElement { - Button::new("upgrade", "Upgrade") - .label_size(LabelSize::Small) - .style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .on_click(cx.listener({ - move |this, _, _, cx| { - this.clear_thread_error(cx); - cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx)); - } - })) - } - - pub fn delete_history_entry(&mut self, entry: HistoryEntry, cx: &mut Context) { - let task = match entry { - HistoryEntry::AcpThread(thread) => self.history_store.update(cx, |history, cx| { - history.delete_thread(thread.id.clone(), cx) - }), - HistoryEntry::TextThread(text_thread) => { - self.history_store.update(cx, |history, cx| { - history.delete_text_thread(text_thread.path.clone(), cx) - }) - } - }; - task.detach_and_log_err(cx); - } - - /// Returns the currently active editor, either for a message that is being - /// edited or the editor for a new message. - fn active_editor(&self, cx: &App) -> Entity { - if let Some(index) = self.editing_message - && let Some(editor) = self - .entry_view_state - .read(cx) - .entry(index) - .and_then(|e| e.message_editor()) - .cloned() - { - editor - } else { - self.message_editor.clone() - } - } -} - -fn loading_contents_spinner(size: IconSize) -> AnyElement { - Icon::new(IconName::LoadCircle) - .size(size) - .color(Color::Accent) - .with_rotate_animation(3) - .into_any_element() -} - -fn placeholder_text(agent_name: &str, has_commands: bool) -> String { - if agent_name == "Zed Agent" { - format!("Message the {} — @ to include context", agent_name) - } else if has_commands { - format!( - "Message {} — @ to include context, / for commands", - agent_name - ) - } else { - format!("Message {} — @ to include context", agent_name) - } -} - -impl Focusable for AcpThreadView { - fn focus_handle(&self, cx: &App) -> FocusHandle { - match self.thread_state { - ThreadState::Ready { .. } => self.active_editor(cx).focus_handle(cx), - ThreadState::Loading { .. } - | ThreadState::LoadError(_) - | ThreadState::Unauthenticated { .. } => self.focus_handle.clone(), - } - } -} - -impl Render for AcpThreadView { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let has_messages = self.list_state.item_count() > 0; - let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5; - - v_flex() - .size_full() - .key_context("AcpThread") - .on_action(cx.listener(Self::toggle_burn_mode)) - .on_action(cx.listener(Self::keep_all)) - .on_action(cx.listener(Self::reject_all)) - .on_action(cx.listener(Self::allow_always)) - .on_action(cx.listener(Self::allow_once)) - .on_action(cx.listener(Self::reject_once)) - .track_focus(&self.focus_handle) - .bg(cx.theme().colors().panel_background) - .child(match &self.thread_state { - ThreadState::Unauthenticated { - connection, - description, - configuration_view, - pending_auth_method, - .. - } => self - .render_auth_required_state( - connection, - description.as_ref(), - configuration_view.as_ref(), - pending_auth_method.as_ref(), - window, - cx, - ) - .into_any(), - ThreadState::Loading { .. } => v_flex() - .flex_1() - .child(self.render_recent_history(cx)) - .into_any(), - ThreadState::LoadError(e) => v_flex() - .flex_1() - .size_full() - .items_center() - .justify_end() - .child(self.render_load_error(e, window, cx)) - .into_any(), - ThreadState::Ready { .. } => v_flex().flex_1().map(|this| { - if has_messages { - this.child( - list( - self.list_state.clone(), - cx.processor(|this, index: usize, window, cx| { - let Some((entry, len)) = this.thread().and_then(|thread| { - let entries = &thread.read(cx).entries(); - Some((entries.get(index)?, entries.len())) - }) else { - return Empty.into_any(); - }; - this.render_entry(index, len, entry, window, cx) - }), - ) - .with_sizing_behavior(gpui::ListSizingBehavior::Auto) - .flex_grow() - .into_any(), - ) - .vertical_scrollbar_for(&self.list_state, window, cx) - .into_any() - } else { - this.child(self.render_recent_history(cx)).into_any() - } - }), - }) - // The activity bar is intentionally rendered outside of the ThreadState::Ready match - // above so that the scrollbar doesn't render behind it. The current setup allows - // the scrollbar to stop exactly at the activity bar start. - .when(has_messages, |this| match &self.thread_state { - ThreadState::Ready { thread, .. } => { - this.children(self.render_activity_bar(thread, window, cx)) - } - _ => this, - }) - .children(self.render_thread_retry_status_callout(window, cx)) - .when(self.show_codex_windows_warning, |this| { - this.child(self.render_codex_windows_warning(cx)) - }) - .children(self.render_thread_error(window, cx)) - .when_some( - self.new_server_version_available.as_ref().filter(|_| { - !has_messages || !matches!(self.thread_state, ThreadState::Ready { .. }) - }), - |this, version| this.child(self.render_new_version_callout(&version, cx)), - ) - .children( - if let Some(usage_callout) = self.render_usage_callout(line_height, cx) { - Some(usage_callout.into_any_element()) - } else { - self.render_token_limit_callout(line_height, cx) - .map(|token_limit_callout| token_limit_callout.into_any_element()) - }, - ) - .child(self.render_message_editor(window, cx)) - } -} - -fn default_markdown_style( - buffer_font: bool, - muted_text: bool, - window: &Window, - cx: &App, -) -> MarkdownStyle { - let theme_settings = ThemeSettings::get_global(cx); - let colors = cx.theme().colors(); - - let buffer_font_size = theme_settings.agent_buffer_font_size(cx); - - let mut text_style = window.text_style(); - let line_height = buffer_font_size * 1.75; - - let font_family = if buffer_font { - theme_settings.buffer_font.family.clone() - } else { - theme_settings.ui_font.family.clone() - }; - - let font_size = if buffer_font { - theme_settings.agent_buffer_font_size(cx) - } else { - theme_settings.agent_ui_font_size(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), - ..Default::default() - }); - - MarkdownStyle { - base_text_style: text_style.clone(), - syntax: cx.theme().syntax().clone(), - selection_background_color: colors.element_selection_background, - code_block_overflow_x_scroll: true, - heading_level_styles: Some(HeadingLevelStyles { - h1: Some(TextStyleRefinement { - font_size: Some(rems(1.15).into()), - ..Default::default() - }), - h2: Some(TextStyleRefinement { - font_size: Some(rems(1.1).into()), - ..Default::default() - }), - h3: Some(TextStyleRefinement { - font_size: Some(rems(1.05).into()), - ..Default::default() - }), - h4: Some(TextStyleRefinement { - font_size: Some(rems(1.).into()), - ..Default::default() - }), - h5: Some(TextStyleRefinement { - font_size: Some(rems(0.95).into()), - ..Default::default() - }), - h6: Some(TextStyleRefinement { - font_size: Some(rems(0.875).into()), - ..Default::default() - }), - }), - code_block: StyleRefinement { - padding: EdgesRefinement { - top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))), - left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))), - right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))), - bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(8.)))), - }, - margin: EdgesRefinement { - top: Some(Length::Definite(px(8.).into())), - left: Some(Length::Definite(px(0.).into())), - right: Some(Length::Definite(px(0.).into())), - bottom: Some(Length::Definite(px(12.).into())), - }, - border_style: Some(BorderStyle::Solid), - border_widths: EdgesRefinement { - top: Some(AbsoluteLength::Pixels(px(1.))), - left: Some(AbsoluteLength::Pixels(px(1.))), - right: Some(AbsoluteLength::Pixels(px(1.))), - bottom: Some(AbsoluteLength::Pixels(px(1.))), - }, - border_color: Some(colors.border_variant), - background: Some(colors.editor_background.into()), - text: Some(TextStyleRefinement { - font_family: Some(theme_settings.buffer_font.family.clone()), - font_fallbacks: theme_settings.buffer_font.fallbacks.clone(), - font_features: Some(theme_settings.buffer_font.features.clone()), - font_size: Some(buffer_font_size.into()), - ..Default::default() - }), - ..Default::default() - }, - inline_code: TextStyleRefinement { - font_family: Some(theme_settings.buffer_font.family.clone()), - font_fallbacks: theme_settings.buffer_font.fallbacks.clone(), - font_features: Some(theme_settings.buffer_font.features.clone()), - font_size: Some(buffer_font_size.into()), - background_color: Some(colors.editor_foreground.opacity(0.08)), - ..Default::default() - }, - link: TextStyleRefinement { - background_color: Some(colors.editor_foreground.opacity(0.025)), - color: Some(colors.text_accent), - underline: Some(UnderlineStyle { - color: Some(colors.text_accent.opacity(0.5)), - thickness: px(1.), - ..Default::default() - }), - ..Default::default() - }, - ..Default::default() - } -} - -fn plan_label_markdown_style( - status: &acp::PlanEntryStatus, - window: &Window, - cx: &App, -) -> MarkdownStyle { - let default_md_style = default_markdown_style(false, false, window, cx); - - MarkdownStyle { - base_text_style: TextStyle { - color: cx.theme().colors().text_muted, - strikethrough: if matches!(status, acp::PlanEntryStatus::Completed) { - Some(gpui::StrikethroughStyle { - thickness: px(1.), - color: Some(cx.theme().colors().text_muted.opacity(0.8)), - }) - } else { - None - }, - ..default_md_style.base_text_style - }, - ..default_md_style - } -} - -fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle { - let default_md_style = default_markdown_style(true, false, window, cx); - - MarkdownStyle { - base_text_style: TextStyle { - ..default_md_style.base_text_style - }, - selection_background_color: cx.theme().colors().element_selection_background, - ..Default::default() - } -} - -#[cfg(test)] -pub(crate) mod tests { - use acp_thread::StubAgentConnection; - use agent_client_protocol::SessionId; - use assistant_text_thread::TextThreadStore; - use editor::MultiBufferOffset; - use fs::FakeFs; - use gpui::{EventEmitter, TestAppContext, VisualTestContext}; - use project::Project; - use serde_json::json; - use settings::SettingsStore; - use std::any::Any; - use std::path::Path; - use workspace::Item; - - use super::*; - - #[gpui::test] - async fn test_drop(cx: &mut TestAppContext) { - init_test(cx); - - let (thread_view, _cx) = setup_thread_view(StubAgentServer::default_response(), cx).await; - let weak_view = thread_view.downgrade(); - drop(thread_view); - assert!(!weak_view.is_upgradable()); - } - - #[gpui::test] - async fn test_notification_for_stop_event(cx: &mut TestAppContext) { - init_test(cx); - - let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await; - - let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); - message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("Hello", window, cx); - }); - - cx.deactivate_window(); - - thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.send(window, cx); - }); - - cx.run_until_parked(); - - assert!( - cx.windows() - .iter() - .any(|window| window.downcast::().is_some()) - ); - } - - #[gpui::test] - async fn test_notification_for_error(cx: &mut TestAppContext) { - init_test(cx); - - let (thread_view, cx) = - setup_thread_view(StubAgentServer::new(SaboteurAgentConnection), cx).await; - - let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); - message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("Hello", window, cx); - }); - - cx.deactivate_window(); - - thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.send(window, cx); - }); - - cx.run_until_parked(); - - assert!( - cx.windows() - .iter() - .any(|window| window.downcast::().is_some()) - ); - } - - #[gpui::test] - async fn test_refusal_handling(cx: &mut TestAppContext) { - init_test(cx); - - let (thread_view, cx) = - setup_thread_view(StubAgentServer::new(RefusalAgentConnection), cx).await; - - let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); - message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("Do something harmful", window, cx); - }); - - thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.send(window, cx); - }); - - cx.run_until_parked(); - - // Check that the refusal error is set - thread_view.read_with(cx, |thread_view, _cx| { - assert!( - matches!(thread_view.thread_error, Some(ThreadError::Refusal)), - "Expected refusal error to be set" - ); - }); - } - - #[gpui::test] - async fn test_notification_for_tool_authorization(cx: &mut TestAppContext) { - init_test(cx); - - let tool_call_id = acp::ToolCallId::new("1"); - let tool_call = acp::ToolCall::new(tool_call_id.clone(), "Label") - .kind(acp::ToolKind::Edit) - .content(vec!["hi".into()]); - let connection = - StubAgentConnection::new().with_permission_requests(HashMap::from_iter([( - tool_call_id, - vec![acp::PermissionOption::new( - "1", - "Allow", - acp::PermissionOptionKind::AllowOnce, - )], - )])); - - connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]); - - let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await; - - let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); - message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("Hello", window, cx); - }); - - cx.deactivate_window(); - - thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.send(window, cx); - }); - - cx.run_until_parked(); - - assert!( - cx.windows() - .iter() - .any(|window| window.downcast::().is_some()) - ); - } - - #[gpui::test] - async fn test_notification_when_panel_hidden(cx: &mut TestAppContext) { - init_test(cx); - - let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await; - - add_to_workspace(thread_view.clone(), cx); - - let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); - - message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("Hello", window, cx); - }); - - // Window is active (don't deactivate), but panel will be hidden - // Note: In the test environment, the panel is not actually added to the dock, - // so is_agent_panel_hidden will return true - - thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.send(window, cx); - }); - - cx.run_until_parked(); - - // Should show notification because window is active but panel is hidden - assert!( - cx.windows() - .iter() - .any(|window| window.downcast::().is_some()), - "Expected notification when panel is hidden" - ); - } - - #[gpui::test] - async fn test_notification_still_works_when_window_inactive(cx: &mut TestAppContext) { - init_test(cx); - - let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await; - - let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); - message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("Hello", window, cx); - }); - - // Deactivate window - should show notification regardless of setting - cx.deactivate_window(); - - thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.send(window, cx); - }); - - cx.run_until_parked(); - - // Should still show notification when window is inactive (existing behavior) - assert!( - cx.windows() - .iter() - .any(|window| window.downcast::().is_some()), - "Expected notification when window is inactive" - ); - } - - #[gpui::test] - async fn test_notification_respects_never_setting(cx: &mut TestAppContext) { - init_test(cx); - - // Set notify_when_agent_waiting to Never - cx.update(|cx| { - AgentSettings::override_global( - AgentSettings { - notify_when_agent_waiting: NotifyWhenAgentWaiting::Never, - ..AgentSettings::get_global(cx).clone() - }, - cx, - ); - }); - - let (thread_view, cx) = setup_thread_view(StubAgentServer::default_response(), cx).await; - - let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); - message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("Hello", window, cx); - }); - - // Window is active - - thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.send(window, cx); - }); - - cx.run_until_parked(); - - // Should NOT show notification because notify_when_agent_waiting is Never - assert!( - !cx.windows() - .iter() - .any(|window| window.downcast::().is_some()), - "Expected no notification when notify_when_agent_waiting is Never" - ); - } - - async fn setup_thread_view( - agent: impl AgentServer + 'static, - cx: &mut TestAppContext, - ) -> (Entity, &mut VisualTestContext) { - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let text_thread_store = - cx.update(|_window, cx| cx.new(|cx| TextThreadStore::fake(project.clone(), cx))); - let history_store = - cx.update(|_window, cx| cx.new(|cx| HistoryStore::new(text_thread_store, cx))); - - let thread_view = cx.update(|window, cx| { - cx.new(|cx| { - AcpThreadView::new( - Rc::new(agent), - None, - None, - workspace.downgrade(), - project, - history_store, - None, - false, - window, - cx, - ) - }) - }); - cx.run_until_parked(); - (thread_view, cx) - } - - fn add_to_workspace(thread_view: Entity, cx: &mut VisualTestContext) { - let workspace = thread_view.read_with(cx, |thread_view, _cx| thread_view.workspace.clone()); - - workspace - .update_in(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane( - Box::new(cx.new(|_| ThreadViewItem(thread_view.clone()))), - None, - true, - window, - cx, - ); - }) - .unwrap(); - } - - struct ThreadViewItem(Entity); - - impl Item for ThreadViewItem { - type Event = (); - - fn include_in_nav_history() -> bool { - false - } - - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { - "Test".into() - } - } - - impl EventEmitter<()> for ThreadViewItem {} - - impl Focusable for ThreadViewItem { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.0.read(cx).focus_handle(cx) - } - } - - impl Render for ThreadViewItem { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - self.0.clone().into_any_element() - } - } - - struct StubAgentServer { - connection: C, - } - - impl StubAgentServer { - fn new(connection: C) -> Self { - Self { connection } - } - } - - impl StubAgentServer { - fn default_response() -> Self { - let conn = StubAgentConnection::new(); - conn.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk( - acp::ContentChunk::new("Default response".into()), - )]); - Self::new(conn) - } - } - - impl AgentServer for StubAgentServer - where - C: 'static + AgentConnection + Send + Clone, - { - fn logo(&self) -> ui::IconName { - ui::IconName::Ai - } - - fn name(&self) -> SharedString { - "Test".into() - } - - fn connect( - &self, - _root_dir: Option<&Path>, - _delegate: AgentServerDelegate, - _cx: &mut App, - ) -> Task, Option)>> { - Task::ready(Ok((Rc::new(self.connection.clone()), None))) - } - - fn into_any(self: Rc) -> Rc { - self - } - } - - #[derive(Clone)] - struct SaboteurAgentConnection; - - impl AgentConnection for SaboteurAgentConnection { - fn telemetry_id(&self) -> SharedString { - "saboteur".into() - } - - fn new_thread( - self: Rc, - project: Entity, - _cwd: &Path, - cx: &mut gpui::App, - ) -> Task>> { - Task::ready(Ok(cx.new(|cx| { - let action_log = cx.new(|_| ActionLog::new(project.clone())); - AcpThread::new( - "SaboteurAgentConnection", - self, - project, - action_log, - SessionId::new("test"), - watch::Receiver::constant( - acp::PromptCapabilities::new() - .image(true) - .audio(true) - .embedded_context(true), - ), - cx, - ) - }))) - } - - fn auth_methods(&self) -> &[acp::AuthMethod] { - &[] - } - - fn authenticate( - &self, - _method_id: acp::AuthMethodId, - _cx: &mut App, - ) -> Task> { - unimplemented!() - } - - fn prompt( - &self, - _id: Option, - _params: acp::PromptRequest, - _cx: &mut App, - ) -> Task> { - Task::ready(Err(anyhow::anyhow!("Error prompting"))) - } - - fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) { - unimplemented!() - } - - fn into_any(self: Rc) -> Rc { - self - } - } - - /// Simulates a model which always returns a refusal response - #[derive(Clone)] - struct RefusalAgentConnection; - - impl AgentConnection for RefusalAgentConnection { - fn telemetry_id(&self) -> SharedString { - "refusal".into() - } - - fn new_thread( - self: Rc, - project: Entity, - _cwd: &Path, - cx: &mut gpui::App, - ) -> Task>> { - Task::ready(Ok(cx.new(|cx| { - let action_log = cx.new(|_| ActionLog::new(project.clone())); - AcpThread::new( - "RefusalAgentConnection", - self, - project, - action_log, - SessionId::new("test"), - watch::Receiver::constant( - acp::PromptCapabilities::new() - .image(true) - .audio(true) - .embedded_context(true), - ), - cx, - ) - }))) - } - - fn auth_methods(&self) -> &[acp::AuthMethod] { - &[] - } - - fn authenticate( - &self, - _method_id: acp::AuthMethodId, - _cx: &mut App, - ) -> Task> { - unimplemented!() - } - - fn prompt( - &self, - _id: Option, - _params: acp::PromptRequest, - _cx: &mut App, - ) -> Task> { - Task::ready(Ok(acp::PromptResponse::new(acp::StopReason::Refusal))) - } - - fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) { - unimplemented!() - } - - fn into_any(self: Rc) -> Rc { - self - } - } - - pub(crate) fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - theme::init(theme::LoadThemes::JustBase, cx); - release_channel::init(semver::Version::new(0, 0, 0), cx); - prompt_store::init(cx) - }); - } - - #[gpui::test] - async fn test_rewind_views(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/project", - json!({ - "test1.txt": "old content 1", - "test2.txt": "old content 2" - }), - ) - .await; - let project = Project::test(fs, [Path::new("/project")], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let text_thread_store = - cx.update(|_window, cx| cx.new(|cx| TextThreadStore::fake(project.clone(), cx))); - let history_store = - cx.update(|_window, cx| cx.new(|cx| HistoryStore::new(text_thread_store, cx))); - - let connection = Rc::new(StubAgentConnection::new()); - let thread_view = cx.update(|window, cx| { - cx.new(|cx| { - AcpThreadView::new( - Rc::new(StubAgentServer::new(connection.as_ref().clone())), - None, - None, - workspace.downgrade(), - project.clone(), - history_store.clone(), - None, - false, - window, - cx, - ) - }) - }); - - cx.run_until_parked(); - - let thread = thread_view - .read_with(cx, |view, _| view.thread().cloned()) - .unwrap(); - - // First user message - connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall( - acp::ToolCall::new("tool1", "Edit file 1") - .kind(acp::ToolKind::Edit) - .status(acp::ToolCallStatus::Completed) - .content(vec![acp::ToolCallContent::Diff( - acp::Diff::new("/project/test1.txt", "new content 1").old_text("old content 1"), - )]), - )]); - - thread - .update(cx, |thread, cx| thread.send_raw("Give me a diff", cx)) - .await - .unwrap(); - cx.run_until_parked(); - - thread.read_with(cx, |thread, _| { - assert_eq!(thread.entries().len(), 2); - }); - - thread_view.read_with(cx, |view, cx| { - view.entry_view_state.read_with(cx, |entry_view_state, _| { - assert!( - entry_view_state - .entry(0) - .unwrap() - .message_editor() - .is_some() - ); - assert!(entry_view_state.entry(1).unwrap().has_content()); - }); - }); - - // Second user message - connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall( - acp::ToolCall::new("tool2", "Edit file 2") - .kind(acp::ToolKind::Edit) - .status(acp::ToolCallStatus::Completed) - .content(vec![acp::ToolCallContent::Diff( - acp::Diff::new("/project/test2.txt", "new content 2").old_text("old content 2"), - )]), - )]); - - thread - .update(cx, |thread, cx| thread.send_raw("Another one", cx)) - .await - .unwrap(); - cx.run_until_parked(); - - let second_user_message_id = thread.read_with(cx, |thread, _| { - assert_eq!(thread.entries().len(), 4); - let AgentThreadEntry::UserMessage(user_message) = &thread.entries()[2] else { - panic!(); - }; - user_message.id.clone().unwrap() - }); - - thread_view.read_with(cx, |view, cx| { - view.entry_view_state.read_with(cx, |entry_view_state, _| { - assert!( - entry_view_state - .entry(0) - .unwrap() - .message_editor() - .is_some() - ); - assert!(entry_view_state.entry(1).unwrap().has_content()); - assert!( - entry_view_state - .entry(2) - .unwrap() - .message_editor() - .is_some() - ); - assert!(entry_view_state.entry(3).unwrap().has_content()); - }); - }); - - // Rewind to first message - thread - .update(cx, |thread, cx| thread.rewind(second_user_message_id, cx)) - .await - .unwrap(); - - cx.run_until_parked(); - - thread.read_with(cx, |thread, _| { - assert_eq!(thread.entries().len(), 2); - }); - - thread_view.read_with(cx, |view, cx| { - view.entry_view_state.read_with(cx, |entry_view_state, _| { - assert!( - entry_view_state - .entry(0) - .unwrap() - .message_editor() - .is_some() - ); - assert!(entry_view_state.entry(1).unwrap().has_content()); - - // Old views should be dropped - assert!(entry_view_state.entry(2).is_none()); - assert!(entry_view_state.entry(3).is_none()); - }); - }); - } - - #[gpui::test] - async fn test_message_editing_cancel(cx: &mut TestAppContext) { - init_test(cx); - - let connection = StubAgentConnection::new(); - - connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk( - acp::ContentChunk::new("Response".into()), - )]); - - let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await; - add_to_workspace(thread_view.clone(), cx); - - let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); - message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("Original message to edit", window, cx); - }); - thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.send(window, cx); - }); - - cx.run_until_parked(); - - let user_message_editor = thread_view.read_with(cx, |view, cx| { - assert_eq!(view.editing_message, None); - - view.entry_view_state - .read(cx) - .entry(0) - .unwrap() - .message_editor() - .unwrap() - .clone() - }); - - // Focus - cx.focus(&user_message_editor); - thread_view.read_with(cx, |view, _cx| { - assert_eq!(view.editing_message, Some(0)); - }); - - // Edit - user_message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("Edited message content", window, cx); - }); - - // Cancel - user_message_editor.update_in(cx, |_editor, window, cx| { - window.dispatch_action(Box::new(editor::actions::Cancel), cx); - }); - - thread_view.read_with(cx, |view, _cx| { - assert_eq!(view.editing_message, None); - }); - - user_message_editor.read_with(cx, |editor, cx| { - assert_eq!(editor.text(cx), "Original message to edit"); - }); - } - - #[gpui::test] - async fn test_message_doesnt_send_if_empty(cx: &mut TestAppContext) { - init_test(cx); - - let connection = StubAgentConnection::new(); - - let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await; - add_to_workspace(thread_view.clone(), cx); - - let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); - let mut events = cx.events(&message_editor); - message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("", window, cx); - }); - - message_editor.update_in(cx, |_editor, window, cx| { - window.dispatch_action(Box::new(Chat), cx); - }); - cx.run_until_parked(); - // We shouldn't have received any messages - assert!(matches!( - events.try_next(), - Err(futures::channel::mpsc::TryRecvError { .. }) - )); - } - - #[gpui::test] - async fn test_message_editing_regenerate(cx: &mut TestAppContext) { - init_test(cx); - - let connection = StubAgentConnection::new(); - - connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk( - acp::ContentChunk::new("Response".into()), - )]); - - let (thread_view, cx) = - setup_thread_view(StubAgentServer::new(connection.clone()), cx).await; - add_to_workspace(thread_view.clone(), cx); - - let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); - message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("Original message to edit", window, cx); - }); - thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.send(window, cx); - }); - - cx.run_until_parked(); - - let user_message_editor = thread_view.read_with(cx, |view, cx| { - assert_eq!(view.editing_message, None); - assert_eq!(view.thread().unwrap().read(cx).entries().len(), 2); - - view.entry_view_state - .read(cx) - .entry(0) - .unwrap() - .message_editor() - .unwrap() - .clone() - }); - - // Focus - cx.focus(&user_message_editor); - - // Edit - user_message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("Edited message content", window, cx); - }); - - // Send - connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk( - acp::ContentChunk::new("New Response".into()), - )]); - - user_message_editor.update_in(cx, |_editor, window, cx| { - window.dispatch_action(Box::new(Chat), cx); - }); - - cx.run_until_parked(); - - thread_view.read_with(cx, |view, cx| { - assert_eq!(view.editing_message, None); - - let entries = view.thread().unwrap().read(cx).entries(); - assert_eq!(entries.len(), 2); - assert_eq!( - entries[0].to_markdown(cx), - "## User\n\nEdited message content\n\n" - ); - assert_eq!( - entries[1].to_markdown(cx), - "## Assistant\n\nNew Response\n\n" - ); - - let new_editor = view.entry_view_state.read_with(cx, |state, _cx| { - assert!(!state.entry(1).unwrap().has_content()); - state.entry(0).unwrap().message_editor().unwrap().clone() - }); - - assert_eq!(new_editor.read(cx).text(cx), "Edited message content"); - }) - } - - #[gpui::test] - async fn test_message_editing_while_generating(cx: &mut TestAppContext) { - init_test(cx); - - let connection = StubAgentConnection::new(); - - let (thread_view, cx) = - setup_thread_view(StubAgentServer::new(connection.clone()), cx).await; - add_to_workspace(thread_view.clone(), cx); - - let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); - message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("Original message to edit", window, cx); - }); - thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.send(window, cx); - }); - - cx.run_until_parked(); - - let (user_message_editor, session_id) = thread_view.read_with(cx, |view, cx| { - let thread = view.thread().unwrap().read(cx); - assert_eq!(thread.entries().len(), 1); - - let editor = view - .entry_view_state - .read(cx) - .entry(0) - .unwrap() - .message_editor() - .unwrap() - .clone(); - - (editor, thread.session_id().clone()) - }); - - // Focus - cx.focus(&user_message_editor); - - thread_view.read_with(cx, |view, _cx| { - assert_eq!(view.editing_message, Some(0)); - }); - - // Edit - user_message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("Edited message content", window, cx); - }); - - thread_view.read_with(cx, |view, _cx| { - assert_eq!(view.editing_message, Some(0)); - }); - - // Finish streaming response - cx.update(|_, cx| { - connection.send_update( - session_id.clone(), - acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("Response".into())), - cx, - ); - connection.end_turn(session_id, acp::StopReason::EndTurn); - }); - - thread_view.read_with(cx, |view, _cx| { - assert_eq!(view.editing_message, Some(0)); - }); - - cx.run_until_parked(); - - // Should still be editing - cx.update(|window, cx| { - assert!(user_message_editor.focus_handle(cx).is_focused(window)); - assert_eq!(thread_view.read(cx).editing_message, Some(0)); - assert_eq!( - user_message_editor.read(cx).text(cx), - "Edited message content" - ); - }); - } - - #[gpui::test] - async fn test_interrupt(cx: &mut TestAppContext) { - init_test(cx); - - let connection = StubAgentConnection::new(); - - let (thread_view, cx) = - setup_thread_view(StubAgentServer::new(connection.clone()), cx).await; - add_to_workspace(thread_view.clone(), cx); - - let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); - message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("Message 1", window, cx); - }); - thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.send(window, cx); - }); - - let (thread, session_id) = thread_view.read_with(cx, |view, cx| { - let thread = view.thread().unwrap(); - - (thread.clone(), thread.read(cx).session_id().clone()) - }); - - cx.run_until_parked(); - - cx.update(|_, cx| { - connection.send_update( - session_id.clone(), - acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new( - "Message 1 resp".into(), - )), - cx, - ); - }); - - cx.run_until_parked(); - - thread.read_with(cx, |thread, cx| { - assert_eq!( - thread.to_markdown(cx), - indoc::indoc! {" - ## User - - Message 1 - - ## Assistant - - Message 1 resp - - "} - ) - }); - - message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("Message 2", window, cx); - }); - thread_view.update_in(cx, |thread_view, window, cx| { - thread_view.send(window, cx); - }); - - cx.update(|_, cx| { - // Simulate a response sent after beginning to cancel - connection.send_update( - session_id.clone(), - acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("onse".into())), - cx, - ); - }); - - cx.run_until_parked(); - - // Last Message 1 response should appear before Message 2 - thread.read_with(cx, |thread, cx| { - assert_eq!( - thread.to_markdown(cx), - indoc::indoc! {" - ## User - - Message 1 - - ## Assistant - - Message 1 response - - ## User - - Message 2 - - "} - ) - }); - - cx.update(|_, cx| { - connection.send_update( - session_id.clone(), - acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new( - "Message 2 response".into(), - )), - cx, - ); - connection.end_turn(session_id.clone(), acp::StopReason::EndTurn); - }); - - cx.run_until_parked(); - - thread.read_with(cx, |thread, cx| { - assert_eq!( - thread.to_markdown(cx), - indoc::indoc! {" - ## User - - Message 1 - - ## Assistant - - Message 1 response - - ## User - - Message 2 - - ## Assistant - - Message 2 response - - "} - ) - }); - } - - #[gpui::test] - async fn test_message_editing_insert_selections(cx: &mut TestAppContext) { - init_test(cx); - - let connection = StubAgentConnection::new(); - connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk( - acp::ContentChunk::new("Response".into()), - )]); - - let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await; - add_to_workspace(thread_view.clone(), cx); - - let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); - message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("Original message to edit", window, cx) - }); - thread_view.update_in(cx, |thread_view, window, cx| thread_view.send(window, cx)); - cx.run_until_parked(); - - let user_message_editor = thread_view.read_with(cx, |thread_view, cx| { - thread_view - .entry_view_state - .read(cx) - .entry(0) - .expect("Should have at least one entry") - .message_editor() - .expect("Should have message editor") - .clone() - }); - - cx.focus(&user_message_editor); - thread_view.read_with(cx, |thread_view, _cx| { - assert_eq!(thread_view.editing_message, Some(0)); - }); - - // Ensure to edit the focused message before proceeding otherwise, since - // its content is not different from what was sent, focus will be lost. - user_message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("Original message to edit with ", window, cx) - }); - - // Create a simple buffer with some text so we can create a selection - // that will then be added to the message being edited. - let (workspace, project) = thread_view.read_with(cx, |thread_view, _cx| { - (thread_view.workspace.clone(), thread_view.project.clone()) - }); - let buffer = project.update(cx, |project, cx| { - project.create_local_buffer("let a = 10 + 10;", None, false, cx) - }); - - workspace - .update_in(cx, |workspace, window, cx| { - let editor = cx.new(|cx| { - let mut editor = - Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx); - - editor.change_selections(Default::default(), window, cx, |selections| { - selections.select_ranges([MultiBufferOffset(8)..MultiBufferOffset(15)]); - }); - - editor - }); - workspace.add_item_to_active_pane(Box::new(editor), None, false, window, cx); - }) - .unwrap(); - - thread_view.update_in(cx, |thread_view, window, cx| { - assert_eq!(thread_view.editing_message, Some(0)); - thread_view.insert_selections(window, cx); - }); - - user_message_editor.read_with(cx, |editor, cx| { - let text = editor.editor().read(cx).text(cx); - let expected_text = String::from("Original message to edit with selection "); - - assert_eq!(text, expected_text); - }); - } - - #[gpui::test] - async fn test_insert_selections(cx: &mut TestAppContext) { - init_test(cx); - - let connection = StubAgentConnection::new(); - connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk( - acp::ContentChunk::new("Response".into()), - )]); - - let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await; - add_to_workspace(thread_view.clone(), cx); - - let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone()); - message_editor.update_in(cx, |editor, window, cx| { - editor.set_text("Can you review this snippet ", window, cx) - }); - - // Create a simple buffer with some text so we can create a selection - // that will then be added to the message being edited. - let (workspace, project) = thread_view.read_with(cx, |thread_view, _cx| { - (thread_view.workspace.clone(), thread_view.project.clone()) - }); - let buffer = project.update(cx, |project, cx| { - project.create_local_buffer("let a = 10 + 10;", None, false, cx) - }); - - workspace - .update_in(cx, |workspace, window, cx| { - let editor = cx.new(|cx| { - let mut editor = - Editor::for_buffer(buffer.clone(), Some(project.clone()), window, cx); - - editor.change_selections(Default::default(), window, cx, |selections| { - selections.select_ranges([MultiBufferOffset(8)..MultiBufferOffset(15)]); - }); - - editor - }); - workspace.add_item_to_active_pane(Box::new(editor), None, false, window, cx); - }) - .unwrap(); - - thread_view.update_in(cx, |thread_view, window, cx| { - assert_eq!(thread_view.editing_message, None); - thread_view.insert_selections(window, cx); - }); - - thread_view.read_with(cx, |thread_view, cx| { - let text = thread_view.message_editor.read(cx).text(cx); - let expected_txt = String::from("Can you review this snippet selection "); - - assert_eq!(text, expected_txt); - }) - } -} diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs deleted file mode 100644 index 327f699b4d..0000000000 --- a/crates/agent_ui/src/agent_configuration.rs +++ /dev/null @@ -1,1451 +0,0 @@ -mod add_llm_provider_modal; -pub mod configure_context_server_modal; -mod configure_context_server_tools_modal; -mod manage_profiles_modal; -mod tool_picker; - -use std::{ops::Range, sync::Arc}; - -use agent::ContextServerRegistry; -use anyhow::Result; -use client::zed_urls; -use cloud_llm_client::{Plan, PlanV1, PlanV2}; -use collections::HashMap; -use context_server::ContextServerId; -use editor::{Editor, MultiBufferOffset, SelectionEffects, scroll::Autoscroll}; -use extension::ExtensionManifest; -use extension_host::ExtensionStore; -use fs::Fs; -use gpui::{ - Action, AnyView, App, AsyncWindowContext, Corner, Entity, EventEmitter, FocusHandle, Focusable, - ScrollHandle, Subscription, Task, WeakEntity, -}; -use language::LanguageRegistry; -use language_model::{ - LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID, -}; -use language_models::AllLanguageModelSettings; -use notifications::status_toast::{StatusToast, ToastIcon}; -use project::{ - agent_server_store::{ - AgentServerStore, CLAUDE_CODE_NAME, CODEX_NAME, ExternalAgentServerName, GEMINI_NAME, - }, - context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore}, -}; -use settings::{Settings, SettingsStore, update_settings_file}; -use ui::{ - Button, ButtonStyle, Chip, CommonAnimationExt, ContextMenu, ContextMenuEntry, Disclosure, - Divider, DividerColor, ElevationIndex, IconName, IconPosition, IconSize, Indicator, LabelSize, - PopoverMenu, Switch, Tooltip, WithScrollbar, prelude::*, -}; -use util::ResultExt as _; -use workspace::{Workspace, create_and_open_local_file}; -use zed_actions::{ExtensionCategoryFilter, OpenBrowser}; - -pub(crate) use configure_context_server_modal::ConfigureContextServerModal; -pub(crate) use configure_context_server_tools_modal::ConfigureContextServerToolsModal; -pub(crate) use manage_profiles_modal::ManageProfilesModal; - -use crate::agent_configuration::add_llm_provider_modal::{ - AddLlmProviderModal, LlmCompatibleProvider, -}; - -pub struct AgentConfiguration { - fs: Arc, - language_registry: Arc, - agent_server_store: Entity, - workspace: WeakEntity, - focus_handle: FocusHandle, - configuration_views_by_provider: HashMap, - context_server_store: Entity, - expanded_provider_configurations: HashMap, - context_server_registry: Entity, - _registry_subscription: Subscription, - scroll_handle: ScrollHandle, - _check_for_gemini: Task<()>, -} - -impl AgentConfiguration { - pub fn new( - fs: Arc, - agent_server_store: Entity, - context_server_store: Entity, - context_server_registry: Entity, - language_registry: Arc, - workspace: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let focus_handle = cx.focus_handle(); - - let registry_subscription = cx.subscribe_in( - &LanguageModelRegistry::global(cx), - window, - |this, _, event: &language_model::Event, window, cx| match event { - language_model::Event::AddedProvider(provider_id) => { - let provider = LanguageModelRegistry::read_global(cx).provider(provider_id); - if let Some(provider) = provider { - this.add_provider_configuration_view(&provider, window, cx); - } - } - language_model::Event::RemovedProvider(provider_id) => { - this.remove_provider_configuration_view(provider_id); - } - _ => {} - }, - ); - - cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify()) - .detach(); - - let mut this = Self { - fs, - language_registry, - workspace, - focus_handle, - configuration_views_by_provider: HashMap::default(), - agent_server_store, - context_server_store, - expanded_provider_configurations: HashMap::default(), - context_server_registry, - _registry_subscription: registry_subscription, - scroll_handle: ScrollHandle::new(), - _check_for_gemini: Task::ready(()), - }; - this.build_provider_configuration_views(window, cx); - this - } - - fn build_provider_configuration_views(&mut self, window: &mut Window, cx: &mut Context) { - let providers = LanguageModelRegistry::read_global(cx).providers(); - for provider in providers { - self.add_provider_configuration_view(&provider, window, cx); - } - } - - fn remove_provider_configuration_view(&mut self, provider_id: &LanguageModelProviderId) { - self.configuration_views_by_provider.remove(provider_id); - self.expanded_provider_configurations.remove(provider_id); - } - - fn add_provider_configuration_view( - &mut self, - provider: &Arc, - window: &mut Window, - cx: &mut Context, - ) { - let configuration_view = provider.configuration_view( - language_model::ConfigurationViewTargetAgent::ZedAgent, - window, - cx, - ); - self.configuration_views_by_provider - .insert(provider.id(), configuration_view); - } -} - -impl Focusable for AgentConfiguration { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -pub enum AssistantConfigurationEvent { - NewThread(Arc), -} - -impl EventEmitter for AgentConfiguration {} - -enum AgentIcon { - Name(IconName), - Path(SharedString), -} - -impl AgentConfiguration { - fn render_section_title( - &mut self, - title: impl Into, - description: impl Into, - menu: AnyElement, - ) -> impl IntoElement { - h_flex() - .p_4() - .pb_0() - .mb_2p5() - .items_start() - .justify_between() - .child( - v_flex() - .w_full() - .gap_0p5() - .child( - h_flex() - .pr_1() - .w_full() - .gap_2() - .justify_between() - .flex_wrap() - .child(Headline::new(title.into())) - .child(menu), - ) - .child(Label::new(description.into()).color(Color::Muted)), - ) - } - - fn render_provider_configuration_block( - &mut self, - provider: &Arc, - cx: &mut Context, - ) -> impl IntoElement + use<> { - let provider_id = provider.id().0; - let provider_name = provider.name().0; - let provider_id_string = SharedString::from(format!("provider-disclosure-{provider_id}")); - - let configuration_view = self - .configuration_views_by_provider - .get(&provider.id()) - .cloned(); - - let is_expanded = self - .expanded_provider_configurations - .get(&provider.id()) - .copied() - .unwrap_or(false); - - let is_zed_provider = provider.id() == ZED_CLOUD_PROVIDER_ID; - let current_plan = if is_zed_provider { - self.workspace - .upgrade() - .and_then(|workspace| workspace.read(cx).user_store().read(cx).plan()) - } else { - None - }; - - let is_signed_in = self - .workspace - .read_with(cx, |workspace, _| { - !workspace.client().status().borrow().is_signed_out() - }) - .unwrap_or(false); - - v_flex() - .w_full() - .when(is_expanded, |this| this.mb_2()) - .child( - div() - .px_2() - .child(Divider::horizontal().color(DividerColor::BorderFaded)), - ) - .child( - h_flex() - .map(|this| { - if is_expanded { - this.mt_2().mb_1() - } else { - this.my_2() - } - }) - .w_full() - .justify_between() - .child( - h_flex() - .id(provider_id_string.clone()) - .px_2() - .py_0p5() - .w_full() - .justify_between() - .rounded_sm() - .hover(|hover| hover.bg(cx.theme().colors().element_hover)) - .child( - h_flex() - .w_full() - .gap_1p5() - .child( - Icon::new(provider.icon()) - .size(IconSize::Small) - .color(Color::Muted), - ) - .child( - h_flex() - .w_full() - .gap_1() - .child(Label::new(provider_name.clone())) - .map(|this| { - if is_zed_provider && is_signed_in { - this.child( - self.render_zed_plan_info(current_plan, cx), - ) - } else { - this.when( - provider.is_authenticated(cx) - && !is_expanded, - |parent| { - parent.child( - Icon::new(IconName::Check) - .color(Color::Success), - ) - }, - ) - } - }), - ), - ) - .child( - Disclosure::new(provider_id_string, is_expanded) - .opened_icon(IconName::ChevronUp) - .closed_icon(IconName::ChevronDown), - ) - .on_click(cx.listener({ - let provider_id = provider.id(); - move |this, _event, _window, _cx| { - let is_expanded = this - .expanded_provider_configurations - .entry(provider_id.clone()) - .or_insert(false); - - *is_expanded = !*is_expanded; - } - })), - ), - ) - .child( - v_flex() - .w_full() - .px_2() - .gap_1() - .when(is_expanded, |parent| match configuration_view { - Some(configuration_view) => parent.child(configuration_view), - None => parent.child(Label::new(format!( - "No configuration view for {provider_name}", - ))), - }) - .when(is_expanded && provider.is_authenticated(cx), |parent| { - parent.child( - Button::new( - SharedString::from(format!("new-thread-{provider_id}")), - "Start New Thread", - ) - .full_width() - .style(ButtonStyle::Outlined) - .layer(ElevationIndex::ModalSurface) - .icon_position(IconPosition::Start) - .icon(IconName::Thread) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .label_size(LabelSize::Small) - .on_click(cx.listener({ - let provider = provider.clone(); - move |_this, _event, _window, cx| { - cx.emit(AssistantConfigurationEvent::NewThread( - provider.clone(), - )) - } - })), - ) - }) - .when( - is_expanded && is_removable_provider(&provider.id(), cx), - |this| { - this.child( - Button::new( - SharedString::from(format!("delete-provider-{provider_id}")), - "Remove Provider", - ) - .full_width() - .style(ButtonStyle::Outlined) - .icon_position(IconPosition::Start) - .icon(IconName::Trash) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .label_size(LabelSize::Small) - .on_click(cx.listener({ - let provider = provider.clone(); - move |this, _event, window, cx| { - this.delete_provider(provider.clone(), window, cx); - } - })), - ) - }, - ), - ) - } - - fn delete_provider( - &mut self, - provider: Arc, - window: &mut Window, - cx: &mut Context, - ) { - let fs = self.fs.clone(); - let provider_id = provider.id(); - - cx.spawn_in(window, async move |_, cx| { - cx.update(|_window, cx| { - update_settings_file(fs.clone(), cx, { - let provider_id = provider_id.clone(); - move |settings, _| { - if let Some(ref mut openai_compatible) = settings - .language_models - .as_mut() - .and_then(|lm| lm.openai_compatible.as_mut()) - { - let key_to_remove: Arc = Arc::from(provider_id.0.as_ref()); - openai_compatible.remove(&key_to_remove); - } - } - }); - }) - .log_err(); - - cx.update(|_window, cx| { - LanguageModelRegistry::global(cx).update(cx, { - let provider_id = provider_id.clone(); - move |registry, cx| { - registry.unregister_provider(provider_id, cx); - } - }) - }) - .log_err(); - - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - - fn render_provider_configuration_section( - &mut self, - cx: &mut Context, - ) -> impl IntoElement { - let providers = LanguageModelRegistry::read_global(cx).providers(); - - let popover_menu = PopoverMenu::new("add-provider-popover") - .trigger( - Button::new("add-provider", "Add Provider") - .style(ButtonStyle::Outlined) - .icon_position(IconPosition::Start) - .icon(IconName::Plus) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .label_size(LabelSize::Small), - ) - .menu({ - let workspace = self.workspace.clone(); - move |window, cx| { - Some(ContextMenu::build(window, cx, |menu, _window, _cx| { - menu.header("Compatible APIs").entry("OpenAI", None, { - let workspace = workspace.clone(); - move |window, cx| { - workspace - .update(cx, |workspace, cx| { - AddLlmProviderModal::toggle( - LlmCompatibleProvider::OpenAi, - workspace, - window, - cx, - ); - }) - .log_err(); - } - }) - })) - } - }) - .anchor(gpui::Corner::TopRight) - .offset(gpui::Point { - x: px(0.0), - y: px(2.0), - }); - - v_flex() - .w_full() - .child(self.render_section_title( - "LLM Providers", - "Add at least one provider to use AI-powered features with Zed's native agent.", - popover_menu.into_any_element(), - )) - .child( - div() - .w_full() - .pl(DynamicSpacing::Base08.rems(cx)) - .pr(DynamicSpacing::Base20.rems(cx)) - .children( - providers.into_iter().map(|provider| { - self.render_provider_configuration_block(&provider, cx) - }), - ), - ) - } - - fn render_zed_plan_info(&self, plan: Option, cx: &mut Context) -> impl IntoElement { - if let Some(plan) = plan { - let free_chip_bg = cx - .theme() - .colors() - .editor_background - .opacity(0.5) - .blend(cx.theme().colors().text_accent.opacity(0.05)); - - let pro_chip_bg = cx - .theme() - .colors() - .editor_background - .opacity(0.5) - .blend(cx.theme().colors().text_accent.opacity(0.2)); - - let (plan_name, label_color, bg_color) = match plan { - Plan::V1(PlanV1::ZedFree) | Plan::V2(PlanV2::ZedFree) => { - ("Free", Color::Default, free_chip_bg) - } - Plan::V1(PlanV1::ZedProTrial) | Plan::V2(PlanV2::ZedProTrial) => { - ("Pro Trial", Color::Accent, pro_chip_bg) - } - Plan::V1(PlanV1::ZedPro) | Plan::V2(PlanV2::ZedPro) => { - ("Pro", Color::Accent, pro_chip_bg) - } - }; - - Chip::new(plan_name.to_string()) - .bg_color(bg_color) - .label_color(label_color) - .into_any_element() - } else { - div().into_any_element() - } - } - - fn render_context_servers_section( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let mut context_server_ids = self - .context_server_store - .read(cx) - .server_ids(cx) - .into_iter() - .collect::>(); - - // Sort context servers: ones without mcp-server- prefix first, then prefixed ones - context_server_ids.sort_by(|a, b| { - const MCP_PREFIX: &str = "mcp-server-"; - match (a.0.strip_prefix(MCP_PREFIX), b.0.strip_prefix(MCP_PREFIX)) { - // If one has mcp-server- prefix and other doesn't, non-mcp comes first - (Some(_), None) => std::cmp::Ordering::Greater, - (None, Some(_)) => std::cmp::Ordering::Less, - // If both have same prefix status, sort by appropriate key - (Some(a), Some(b)) => a.cmp(b), - (None, None) => a.0.cmp(&b.0), - } - }); - - let add_server_popover = PopoverMenu::new("add-server-popover") - .trigger( - Button::new("add-server", "Add Server") - .style(ButtonStyle::Outlined) - .icon_position(IconPosition::Start) - .icon(IconName::Plus) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .label_size(LabelSize::Small), - ) - .menu({ - move |window, cx| { - Some(ContextMenu::build(window, cx, |menu, _window, _cx| { - menu.entry("Add Custom Server", None, { - |window, cx| { - window.dispatch_action(crate::AddContextServer.boxed_clone(), cx) - } - }) - .entry("Install from Extensions", None, { - |window, cx| { - window.dispatch_action( - zed_actions::Extensions { - category_filter: Some( - ExtensionCategoryFilter::ContextServers, - ), - id: None, - } - .boxed_clone(), - cx, - ) - } - }) - })) - } - }) - .anchor(gpui::Corner::TopRight) - .offset(gpui::Point { - x: px(0.0), - y: px(2.0), - }); - - v_flex() - .border_b_1() - .border_color(cx.theme().colors().border) - .child(self.render_section_title( - "Model Context Protocol (MCP) Servers", - "All MCP servers connected directly or via a Zed extension.", - add_server_popover.into_any_element(), - )) - .child( - v_flex() - .pl_4() - .pb_4() - .pr_5() - .w_full() - .gap_1() - .map(|mut parent| { - if context_server_ids.is_empty() { - parent.child( - h_flex() - .p_4() - .justify_center() - .border_1() - .border_dashed() - .border_color(cx.theme().colors().border.opacity(0.6)) - .rounded_sm() - .child( - Label::new("No MCP servers added yet.") - .color(Color::Muted) - .size(LabelSize::Small), - ), - ) - } else { - for (index, context_server_id) in - context_server_ids.into_iter().enumerate() - { - if index > 0 { - parent = parent.child( - Divider::horizontal() - .color(DividerColor::BorderFaded) - .into_any_element(), - ); - } - parent = parent.child(self.render_context_server( - context_server_id, - window, - cx, - )); - } - parent - } - }), - ) - } - - fn render_context_server( - &self, - context_server_id: ContextServerId, - window: &mut Window, - cx: &mut Context, - ) -> impl use<> + IntoElement { - let server_status = self - .context_server_store - .read(cx) - .status_for_server(&context_server_id) - .unwrap_or(ContextServerStatus::Stopped); - let server_configuration = self - .context_server_store - .read(cx) - .configuration_for_server(&context_server_id); - - let is_running = matches!(server_status, ContextServerStatus::Running); - let item_id = SharedString::from(context_server_id.0.clone()); - // Servers without a configuration can only be provided by extensions. - let provided_by_extension = server_configuration.as_ref().is_none_or(|config| { - matches!( - config.as_ref(), - ContextServerConfiguration::Extension { .. } - ) - }); - - let error = if let ContextServerStatus::Error(error) = server_status.clone() { - Some(error) - } else { - None - }; - - let tool_count = self - .context_server_registry - .read(cx) - .tools_for_server(&context_server_id) - .count(); - - let (source_icon, source_tooltip) = if provided_by_extension { - ( - IconName::ZedSrcExtension, - "This MCP server was installed from an extension.", - ) - } else { - ( - IconName::ZedSrcCustom, - "This custom MCP server was installed directly.", - ) - }; - - let (status_indicator, tooltip_text) = match server_status { - ContextServerStatus::Starting => ( - Icon::new(IconName::LoadCircle) - .size(IconSize::XSmall) - .color(Color::Accent) - .with_keyed_rotate_animation( - SharedString::from(format!("{}-starting", context_server_id.0)), - 3, - ) - .into_any_element(), - "Server is starting.", - ), - ContextServerStatus::Running => ( - Indicator::dot().color(Color::Success).into_any_element(), - "Server is active.", - ), - ContextServerStatus::Error(_) => ( - Indicator::dot().color(Color::Error).into_any_element(), - "Server has an error.", - ), - ContextServerStatus::Stopped => ( - Indicator::dot().color(Color::Muted).into_any_element(), - "Server is stopped.", - ), - }; - let is_remote = server_configuration - .as_ref() - .map(|config| matches!(config.as_ref(), ContextServerConfiguration::Http { .. })) - .unwrap_or(false); - let context_server_configuration_menu = PopoverMenu::new("context-server-config-menu") - .trigger_with_tooltip( - IconButton::new("context-server-config-menu", IconName::Settings) - .icon_color(Color::Muted) - .icon_size(IconSize::Small), - Tooltip::text("Configure MCP Server"), - ) - .anchor(Corner::TopRight) - .menu({ - let fs = self.fs.clone(); - let context_server_id = context_server_id.clone(); - let language_registry = self.language_registry.clone(); - let workspace = self.workspace.clone(); - let context_server_registry = self.context_server_registry.clone(); - - move |window, cx| { - Some(ContextMenu::build(window, cx, |menu, _window, _cx| { - menu.entry("Configure Server", None, { - let context_server_id = context_server_id.clone(); - let language_registry = language_registry.clone(); - let workspace = workspace.clone(); - move |window, cx| { - if is_remote { - crate::agent_configuration::configure_context_server_modal::ConfigureContextServerModal::show_modal_for_existing_server( - context_server_id.clone(), - language_registry.clone(), - workspace.clone(), - window, - cx, - ) - .detach(); - } else { - ConfigureContextServerModal::show_modal_for_existing_server( - context_server_id.clone(), - language_registry.clone(), - workspace.clone(), - window, - cx, - ) - .detach(); - } - } - }).when(tool_count > 0, |this| this.entry("View Tools", None, { - let context_server_id = context_server_id.clone(); - let context_server_registry = context_server_registry.clone(); - let workspace = workspace.clone(); - move |window, cx| { - let context_server_id = context_server_id.clone(); - workspace.update(cx, |workspace, cx| { - ConfigureContextServerToolsModal::toggle( - context_server_id, - context_server_registry.clone(), - workspace, - window, - cx, - ); - }) - .ok(); - } - })) - .separator() - .entry("Uninstall", None, { - let fs = fs.clone(); - let context_server_id = context_server_id.clone(); - let workspace = workspace.clone(); - move |_, cx| { - let uninstall_extension_task = match ( - provided_by_extension, - resolve_extension_for_context_server(&context_server_id, cx), - ) { - (true, Some((id, manifest))) => { - if extension_only_provides_context_server(manifest.as_ref()) - { - ExtensionStore::global(cx).update(cx, |store, cx| { - store.uninstall_extension(id, cx) - }) - } else { - workspace.update(cx, |workspace, cx| { - show_unable_to_uninstall_extension_with_context_server(workspace, context_server_id.clone(), cx); - }).log_err(); - Task::ready(Ok(())) - } - } - _ => Task::ready(Ok(())), - }; - - cx.spawn({ - let fs = fs.clone(); - let context_server_id = context_server_id.clone(); - async move |cx| { - uninstall_extension_task.await?; - cx.update(|cx| { - update_settings_file( - fs.clone(), - cx, - { - let context_server_id = - context_server_id.clone(); - move |settings, _| { - settings.project - .context_servers - .remove(&context_server_id.0); - } - }, - ) - }) - } - }) - .detach_and_log_err(cx); - } - }) - })) - } - }); - - v_flex() - .id(item_id.clone()) - .child( - h_flex() - .justify_between() - .child( - h_flex() - .flex_1() - .min_w_0() - .child( - h_flex() - .id(format!("tooltip-{}", item_id)) - .h_full() - .w_3() - .mr_2() - .justify_center() - .tooltip(Tooltip::text(tooltip_text)) - .child(status_indicator), - ) - .child(Label::new(item_id).truncate()) - .child( - div() - .id("extension-source") - .mt_0p5() - .mx_1() - .flex_none() - .tooltip(Tooltip::text(source_tooltip)) - .child( - Icon::new(source_icon) - .size(IconSize::Small) - .color(Color::Muted), - ), - ) - .when(is_running, |this| { - this.child( - Label::new(if tool_count == 1 { - SharedString::from("1 tool") - } else { - SharedString::from(format!("{} tools", tool_count)) - }) - .color(Color::Muted) - .size(LabelSize::Small), - ) - }), - ) - .child( - h_flex() - .gap_0p5() - .flex_none() - .child(context_server_configuration_menu) - .child( - Switch::new("context-server-switch", is_running.into()) - .on_click({ - let context_server_manager = self.context_server_store.clone(); - let fs = self.fs.clone(); - - move |state, _window, cx| { - let is_enabled = match state { - ToggleState::Unselected - | ToggleState::Indeterminate => { - context_server_manager.update(cx, |this, cx| { - this.stop_server(&context_server_id, cx) - .log_err(); - }); - false - } - ToggleState::Selected => { - context_server_manager.update(cx, |this, cx| { - if let Some(server) = - this.get_server(&context_server_id) - { - this.start_server(server, cx); - } - }); - true - } - }; - update_settings_file(fs.clone(), cx, { - let context_server_id = context_server_id.clone(); - - move |settings, _| { - settings - .project - .context_servers - .entry(context_server_id.0) - .or_insert_with(|| { - settings::ContextServerSettingsContent::Extension { - enabled: is_enabled, - settings: serde_json::json!({}), - } - }) - .set_enabled(is_enabled); - } - }); - } - }), - ), - ), - ) - .map(|parent| { - if let Some(error) = error { - return parent.child( - h_flex() - .gap_2() - .pr_4() - .items_start() - .child( - h_flex() - .flex_none() - .h(window.line_height() / 1.6_f32) - .justify_center() - .child( - Icon::new(IconName::XCircle) - .size(IconSize::XSmall) - .color(Color::Error), - ), - ) - .child( - div().w_full().child( - Label::new(error) - .buffer_font(cx) - .color(Color::Muted) - .size(LabelSize::Small), - ), - ), - ); - } - parent - }) - } - - fn render_agent_servers_section(&mut self, cx: &mut Context) -> impl IntoElement { - let agent_server_store = self.agent_server_store.read(cx); - - let user_defined_agents = agent_server_store - .external_agents() - .filter(|name| { - name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME && name.0 != CODEX_NAME - }) - .cloned() - .collect::>(); - - let user_defined_agents: Vec<_> = user_defined_agents - .into_iter() - .map(|name| { - let icon = if let Some(icon_path) = agent_server_store.agent_icon(&name) { - AgentIcon::Path(icon_path) - } else { - AgentIcon::Name(IconName::Ai) - }; - let display_name = agent_server_store - .agent_display_name(&name) - .unwrap_or_else(|| name.0.clone()); - (name, icon, display_name) - }) - .collect(); - - let add_agent_popover = PopoverMenu::new("add-agent-server-popover") - .trigger( - Button::new("add-agent", "Add Agent") - .style(ButtonStyle::Outlined) - .icon_position(IconPosition::Start) - .icon(IconName::Plus) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .label_size(LabelSize::Small), - ) - .menu({ - move |window, cx| { - Some(ContextMenu::build(window, cx, |menu, _window, _cx| { - menu.entry("Install from Extensions", None, { - |window, cx| { - window.dispatch_action( - zed_actions::Extensions { - category_filter: Some( - ExtensionCategoryFilter::AgentServers, - ), - id: None, - } - .boxed_clone(), - cx, - ) - } - }) - .entry("Add Custom Agent", None, { - 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); - } - } - }) - .separator() - .header("Learn More") - .item( - ContextMenuEntry::new("Agent Servers Docs") - .icon(IconName::ArrowUpRight) - .icon_color(Color::Muted) - .icon_position(IconPosition::End) - .handler({ - move |window, cx| { - window.dispatch_action( - Box::new(OpenBrowser { - url: zed_urls::agent_server_docs(cx), - }), - cx, - ); - } - }), - ) - .item( - ContextMenuEntry::new("ACP Docs") - .icon(IconName::ArrowUpRight) - .icon_color(Color::Muted) - .icon_position(IconPosition::End) - .handler({ - move |window, cx| { - window.dispatch_action( - Box::new(OpenBrowser { - url: "https://agentclientprotocol.com/".into(), - }), - cx, - ); - } - }), - ) - })) - } - }) - .anchor(gpui::Corner::TopRight) - .offset(gpui::Point { - x: px(0.0), - y: px(2.0), - }); - - v_flex() - .border_b_1() - .border_color(cx.theme().colors().border) - .child( - v_flex() - .child(self.render_section_title( - "External Agents", - "All agents connected through the Agent Client Protocol.", - add_agent_popover.into_any_element(), - )) - .child( - v_flex() - .p_4() - .pt_0() - .gap_2() - .child(self.render_agent_server( - AgentIcon::Name(IconName::AiClaude), - "Claude Code", - "Claude Code", - false, - cx, - )) - .child(Divider::horizontal().color(DividerColor::BorderFaded)) - .child(self.render_agent_server( - AgentIcon::Name(IconName::AiOpenAi), - "Codex CLI", - "Codex CLI", - false, - cx, - )) - .child(Divider::horizontal().color(DividerColor::BorderFaded)) - .child(self.render_agent_server( - AgentIcon::Name(IconName::AiGemini), - "Gemini CLI", - "Gemini CLI", - false, - cx, - )) - .map(|mut parent| { - for (name, icon, display_name) in user_defined_agents { - parent = parent - .child( - Divider::horizontal().color(DividerColor::BorderFaded), - ) - .child(self.render_agent_server( - icon, - name, - display_name, - true, - cx, - )); - } - parent - }), - ), - ) - } - - fn render_agent_server( - &self, - icon: AgentIcon, - id: impl Into, - display_name: impl Into, - external: bool, - cx: &mut Context, - ) -> impl IntoElement { - let id = id.into(); - let display_name = display_name.into(); - let icon = match icon { - AgentIcon::Name(icon_name) => Icon::new(icon_name) - .size(IconSize::Small) - .color(Color::Muted), - AgentIcon::Path(icon_path) => Icon::from_external_svg(icon_path) - .size(IconSize::Small) - .color(Color::Muted), - }; - - let tooltip_id = SharedString::new(format!("agent-source-{}", id)); - let tooltip_message = format!( - "The {} agent was installed from an extension.", - display_name - ); - - let agent_server_name = ExternalAgentServerName(id.clone()); - - let uninstall_btn_id = SharedString::from(format!("uninstall-{}", id)); - let uninstall_button = IconButton::new(uninstall_btn_id, IconName::Trash) - .icon_color(Color::Muted) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Uninstall Agent Extension")) - .on_click(cx.listener(move |this, _, _window, cx| { - let agent_name = agent_server_name.clone(); - - if let Some(ext_id) = this.agent_server_store.update(cx, |store, _cx| { - store.get_extension_id_for_agent(&agent_name) - }) { - ExtensionStore::global(cx) - .update(cx, |store, cx| store.uninstall_extension(ext_id, cx)) - .detach_and_log_err(cx); - } - })); - - h_flex() - .gap_1() - .justify_between() - .child( - h_flex() - .gap_1p5() - .child(icon) - .child(Label::new(display_name)) - .when(external, |this| { - this.child( - div() - .id(tooltip_id) - .flex_none() - .tooltip(Tooltip::text(tooltip_message)) - .child( - Icon::new(IconName::ZedSrcExtension) - .size(IconSize::Small) - .color(Color::Muted), - ), - ) - }) - .child( - Icon::new(IconName::Check) - .color(Color::Success) - .size(IconSize::Small), - ), - ) - .when(external, |this| this.child(uninstall_button)) - } -} - -impl Render for AgentConfiguration { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - v_flex() - .id("assistant-configuration") - .key_context("AgentConfiguration") - .track_focus(&self.focus_handle(cx)) - .relative() - .size_full() - .pb_8() - .bg(cx.theme().colors().panel_background) - .child( - div() - .size_full() - .child( - v_flex() - .id("assistant-configuration-content") - .track_scroll(&self.scroll_handle) - .size_full() - .overflow_y_scroll() - .child(self.render_agent_servers_section(cx)) - .child(self.render_context_servers_section(window, cx)) - .child(self.render_provider_configuration_section(cx)), - ) - .vertical_scrollbar_for(&self.scroll_handle, window, cx), - ) - } -} - -fn extension_only_provides_context_server(manifest: &ExtensionManifest) -> bool { - manifest.context_servers.len() == 1 - && manifest.themes.is_empty() - && manifest.icon_themes.is_empty() - && manifest.languages.is_empty() - && manifest.grammars.is_empty() - && manifest.language_servers.is_empty() - && manifest.slash_commands.is_empty() - && manifest.snippets.is_none() - && manifest.debug_locators.is_empty() -} - -pub(crate) fn resolve_extension_for_context_server( - id: &ContextServerId, - cx: &App, -) -> Option<(Arc, Arc)> { - ExtensionStore::global(cx) - .read(cx) - .installed_extensions() - .iter() - .find(|(_, entry)| entry.manifest.context_servers.contains_key(&id.0)) - .map(|(id, entry)| (id.clone(), entry.manifest.clone())) -} - -// This notification appears when trying to delete -// an MCP server extension that not only provides -// the server, but other things, too, like language servers and more. -fn show_unable_to_uninstall_extension_with_context_server( - workspace: &mut Workspace, - id: ContextServerId, - cx: &mut App, -) { - let workspace_handle = workspace.weak_handle(); - let context_server_id = id.clone(); - - let status_toast = StatusToast::new( - format!( - "The {} extension provides more than just the MCP server. Proceed to uninstall anyway?", - id.0 - ), - cx, - move |this, _cx| { - let workspace_handle = workspace_handle.clone(); - - this.icon(ToastIcon::new(IconName::Warning).color(Color::Warning)) - .dismiss_button(true) - .action("Uninstall", move |_, _cx| { - if let Some((extension_id, _)) = - resolve_extension_for_context_server(&context_server_id, _cx) - { - ExtensionStore::global(_cx).update(_cx, |store, cx| { - store - .uninstall_extension(extension_id, cx) - .detach_and_log_err(cx); - }); - - workspace_handle - .update(_cx, |workspace, cx| { - let fs = workspace.app_state().fs.clone(); - cx.spawn({ - let context_server_id = context_server_id.clone(); - async move |_workspace_handle, cx| { - cx.update(|cx| { - update_settings_file(fs, cx, move |settings, _| { - settings - .project - .context_servers - .remove(&context_server_id.0); - }); - })?; - anyhow::Ok(()) - } - }) - .detach_and_log_err(cx); - }) - .log_err(); - } - }) - }, - ); - - 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, |settings| { - let server_name: Option = (0..u8::MAX) - .map(|i| { - if i == 0 { - "your_agent".into() - } else { - format!("your_agent_{}", i).into() - } - }) - .find(|name| { - !settings - .agent_servers - .as_ref() - .is_some_and(|agent_servers| agent_servers.custom.contains_key(name)) - }); - if let Some(server_name) = server_name { - unique_server_name = Some(server_name.clone()); - settings - .agent_servers - .get_or_insert_default() - .custom - .insert( - server_name, - settings::CustomAgentServerSettings::Custom { - path: "path_to_executable".into(), - args: vec![], - env: Some(HashMap::default()), - default_mode: None, - default_model: None, - }, - ); - } - }); - - if edits.is_empty() { - return; - } - - let ranges = edits - .iter() - .map(|(range, _)| range.clone()) - .collect::>(); - - item.edit( - edits.into_iter().map(|(range, s)| { - ( - MultiBufferOffset(range.start)..MultiBufferOffset(range.end), - s, - ) - }), - 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![ - MultiBufferOffset(range.start)..MultiBufferOffset(range.end), - ]); - }, - ); - } - } - }) -} - -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 - } -} - -// OpenAI-compatible providers are user-configured and can be removed, -// whereas built-in providers (like Anthropic, OpenAI, Google, etc.) can't. -// -// If in the future we have more "API-compatible-type" of providers, -// they should be included here as removable providers. -fn is_removable_provider(provider_id: &LanguageModelProviderId, cx: &App) -> bool { - AllLanguageModelSettings::get_global(cx) - .openai_compatible - .contains_key(provider_id.0.as_ref()) -} diff --git a/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs b/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs deleted file mode 100644 index 02269511bb..0000000000 --- a/crates/agent_ui/src/agent_configuration/add_llm_provider_modal.rs +++ /dev/null @@ -1,848 +0,0 @@ -use std::sync::Arc; - -use anyhow::Result; -use collections::HashSet; -use fs::Fs; -use gpui::{ - DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, ScrollHandle, Task, -}; -use language_model::LanguageModelRegistry; -use language_models::provider::open_ai_compatible::{AvailableModel, ModelCapabilities}; -use settings::{OpenAiCompatibleSettingsContent, update_settings_file}; -use ui::{ - Banner, Checkbox, KeyBinding, Modal, ModalFooter, ModalHeader, Section, ToggleState, - WithScrollbar, prelude::*, -}; -use ui_input::InputField; -use workspace::{ModalView, Workspace}; - -fn single_line_input( - label: impl Into, - placeholder: impl Into, - text: Option<&str>, - tab_index: isize, - window: &mut Window, - cx: &mut App, -) -> Entity { - cx.new(|cx| { - let input = InputField::new(window, cx, placeholder) - .label(label) - .tab_index(tab_index) - .tab_stop(true); - - if let Some(text) = text { - input - .editor() - .update(cx, |editor, cx| editor.set_text(text, window, cx)); - } - input - }) -} - -#[derive(Clone, Copy)] -pub enum LlmCompatibleProvider { - OpenAi, -} - -impl LlmCompatibleProvider { - fn name(&self) -> &'static str { - match self { - LlmCompatibleProvider::OpenAi => "OpenAI", - } - } - - fn api_url(&self) -> &'static str { - match self { - LlmCompatibleProvider::OpenAi => "https://api.openai.com/v1", - } - } -} - -struct AddLlmProviderInput { - provider_name: Entity, - api_url: Entity, - api_key: Entity, - models: Vec, -} - -impl AddLlmProviderInput { - fn new(provider: LlmCompatibleProvider, window: &mut Window, cx: &mut App) -> Self { - let provider_name = - single_line_input("Provider Name", provider.name(), None, 1, window, cx); - let api_url = single_line_input("API URL", provider.api_url(), None, 2, window, cx); - let api_key = single_line_input( - "API Key", - "000000000000000000000000000000000000000000000000", - None, - 3, - window, - cx, - ); - - Self { - provider_name, - api_url, - api_key, - models: vec![ModelInput::new(0, window, cx)], - } - } - - fn add_model(&mut self, window: &mut Window, cx: &mut App) { - let model_index = self.models.len(); - self.models.push(ModelInput::new(model_index, window, cx)); - } - - fn remove_model(&mut self, index: usize) { - self.models.remove(index); - } -} - -struct ModelCapabilityToggles { - pub supports_tools: ToggleState, - pub supports_images: ToggleState, - pub supports_parallel_tool_calls: ToggleState, - pub supports_prompt_cache_key: ToggleState, -} - -struct ModelInput { - name: Entity, - max_completion_tokens: Entity, - max_output_tokens: Entity, - max_tokens: Entity, - capabilities: ModelCapabilityToggles, -} - -impl ModelInput { - fn new(model_index: usize, window: &mut Window, cx: &mut App) -> Self { - let base_tab_index = (3 + (model_index * 4)) as isize; - - let model_name = single_line_input( - "Model Name", - "e.g. gpt-4o, claude-opus-4, gemini-2.5-pro", - None, - base_tab_index + 1, - window, - cx, - ); - let max_completion_tokens = single_line_input( - "Max Completion Tokens", - "200000", - Some("200000"), - base_tab_index + 2, - window, - cx, - ); - let max_output_tokens = single_line_input( - "Max Output Tokens", - "Max Output Tokens", - Some("32000"), - base_tab_index + 3, - window, - cx, - ); - let max_tokens = single_line_input( - "Max Tokens", - "Max Tokens", - Some("200000"), - base_tab_index + 4, - window, - cx, - ); - - let ModelCapabilities { - tools, - images, - parallel_tool_calls, - prompt_cache_key, - } = ModelCapabilities::default(); - - Self { - name: model_name, - max_completion_tokens, - max_output_tokens, - max_tokens, - capabilities: ModelCapabilityToggles { - supports_tools: tools.into(), - supports_images: images.into(), - supports_parallel_tool_calls: parallel_tool_calls.into(), - supports_prompt_cache_key: prompt_cache_key.into(), - }, - } - } - - fn parse(&self, cx: &App) -> Result { - let name = self.name.read(cx).text(cx); - if name.is_empty() { - return Err(SharedString::from("Model Name cannot be empty")); - } - Ok(AvailableModel { - name, - display_name: None, - max_completion_tokens: Some( - self.max_completion_tokens - .read(cx) - .text(cx) - .parse::() - .map_err(|_| SharedString::from("Max Completion Tokens must be a number"))?, - ), - max_output_tokens: Some( - self.max_output_tokens - .read(cx) - .text(cx) - .parse::() - .map_err(|_| SharedString::from("Max Output Tokens must be a number"))?, - ), - max_tokens: self - .max_tokens - .read(cx) - .text(cx) - .parse::() - .map_err(|_| SharedString::from("Max Tokens must be a number"))?, - capabilities: ModelCapabilities { - tools: self.capabilities.supports_tools.selected(), - images: self.capabilities.supports_images.selected(), - parallel_tool_calls: self.capabilities.supports_parallel_tool_calls.selected(), - prompt_cache_key: self.capabilities.supports_prompt_cache_key.selected(), - }, - }) - } -} - -fn save_provider_to_settings( - input: &AddLlmProviderInput, - cx: &mut App, -) -> Task> { - let provider_name: Arc = input.provider_name.read(cx).text(cx).into(); - if provider_name.is_empty() { - return Task::ready(Err("Provider Name cannot be empty".into())); - } - - if LanguageModelRegistry::read_global(cx) - .providers() - .iter() - .any(|provider| { - provider.id().0.as_ref() == provider_name.as_ref() - || provider.name().0.as_ref() == provider_name.as_ref() - }) - { - return Task::ready(Err( - "Provider Name is already taken by another provider".into() - )); - } - - let api_url = input.api_url.read(cx).text(cx); - if api_url.is_empty() { - return Task::ready(Err("API URL cannot be empty".into())); - } - - let api_key = input.api_key.read(cx).text(cx); - if api_key.is_empty() { - return Task::ready(Err("API Key cannot be empty".into())); - } - - let mut models = Vec::new(); - let mut model_names: HashSet = HashSet::default(); - for model in &input.models { - match model.parse(cx) { - Ok(model) => { - if !model_names.insert(model.name.clone()) { - return Task::ready(Err("Model Names must be unique".into())); - } - models.push(model) - } - Err(err) => return Task::ready(Err(err)), - } - } - - let fs = ::global(cx); - let task = cx.write_credentials(&api_url, "Bearer", api_key.as_bytes()); - cx.spawn(async move |cx| { - task.await - .map_err(|_| "Failed to write API key to keychain")?; - cx.update(|cx| { - update_settings_file(fs, cx, |settings, _cx| { - settings - .language_models - .get_or_insert_default() - .openai_compatible - .get_or_insert_default() - .insert( - provider_name, - OpenAiCompatibleSettingsContent { - api_url, - available_models: models, - }, - ); - }); - }) - .ok(); - Ok(()) - }) -} - -pub struct AddLlmProviderModal { - provider: LlmCompatibleProvider, - input: AddLlmProviderInput, - scroll_handle: ScrollHandle, - focus_handle: FocusHandle, - last_error: Option, -} - -impl AddLlmProviderModal { - pub fn toggle( - provider: LlmCompatibleProvider, - workspace: &mut Workspace, - window: &mut Window, - cx: &mut Context, - ) { - workspace.toggle_modal(window, cx, |window, cx| Self::new(provider, window, cx)); - } - - fn new(provider: LlmCompatibleProvider, window: &mut Window, cx: &mut Context) -> Self { - Self { - input: AddLlmProviderInput::new(provider, window, cx), - provider, - last_error: None, - focus_handle: cx.focus_handle(), - scroll_handle: ScrollHandle::new(), - } - } - - fn confirm(&mut self, _: &menu::Confirm, _: &mut Window, cx: &mut Context) { - let task = save_provider_to_settings(&self.input, cx); - cx.spawn(async move |this, cx| { - let result = task.await; - this.update(cx, |this, cx| match result { - Ok(_) => { - cx.emit(DismissEvent); - } - Err(error) => { - this.last_error = Some(error); - cx.notify(); - } - }) - }) - .detach_and_log_err(cx); - } - - fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { - cx.emit(DismissEvent); - } - - fn render_model_section(&self, cx: &mut Context) -> impl IntoElement { - v_flex() - .mt_1() - .gap_2() - .child( - h_flex() - .justify_between() - .child(Label::new("Models").size(LabelSize::Small)) - .child( - Button::new("add-model", "Add Model") - .icon(IconName::Plus) - .icon_position(IconPosition::Start) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .label_size(LabelSize::Small) - .on_click(cx.listener(|this, _, window, cx| { - this.input.add_model(window, cx); - cx.notify(); - })), - ), - ) - .children( - self.input - .models - .iter() - .enumerate() - .map(|(ix, _)| self.render_model(ix, cx)), - ) - } - - fn render_model(&self, ix: usize, cx: &mut Context) -> impl IntoElement + use<> { - let has_more_than_one_model = self.input.models.len() > 1; - let model = &self.input.models[ix]; - - v_flex() - .p_2() - .gap_2() - .rounded_sm() - .border_1() - .border_dashed() - .border_color(cx.theme().colors().border.opacity(0.6)) - .bg(cx.theme().colors().element_active.opacity(0.15)) - .child(model.name.clone()) - .child( - h_flex() - .gap_2() - .child(model.max_completion_tokens.clone()) - .child(model.max_output_tokens.clone()), - ) - .child(model.max_tokens.clone()) - .child( - v_flex() - .gap_1() - .child( - Checkbox::new(("supports-tools", ix), model.capabilities.supports_tools) - .label("Supports tools") - .on_click(cx.listener(move |this, checked, _window, cx| { - this.input.models[ix].capabilities.supports_tools = *checked; - cx.notify(); - })), - ) - .child( - Checkbox::new(("supports-images", ix), model.capabilities.supports_images) - .label("Supports images") - .on_click(cx.listener(move |this, checked, _window, cx| { - this.input.models[ix].capabilities.supports_images = *checked; - cx.notify(); - })), - ) - .child( - Checkbox::new( - ("supports-parallel-tool-calls", ix), - model.capabilities.supports_parallel_tool_calls, - ) - .label("Supports parallel_tool_calls") - .on_click(cx.listener( - move |this, checked, _window, cx| { - this.input.models[ix] - .capabilities - .supports_parallel_tool_calls = *checked; - cx.notify(); - }, - )), - ) - .child( - Checkbox::new( - ("supports-prompt-cache-key", ix), - model.capabilities.supports_prompt_cache_key, - ) - .label("Supports prompt_cache_key") - .on_click(cx.listener( - move |this, checked, _window, cx| { - this.input.models[ix].capabilities.supports_prompt_cache_key = - *checked; - cx.notify(); - }, - )), - ), - ) - .when(has_more_than_one_model, |this| { - this.child( - Button::new(("remove-model", ix), "Remove Model") - .icon(IconName::Trash) - .icon_position(IconPosition::Start) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .label_size(LabelSize::Small) - .style(ButtonStyle::Outlined) - .full_width() - .on_click(cx.listener(move |this, _, _window, cx| { - this.input.remove_model(ix); - cx.notify(); - })), - ) - }) - } - - fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, _: &mut Context) { - window.focus_next(); - } - - fn on_tab_prev( - &mut self, - _: &menu::SelectPrevious, - window: &mut Window, - _: &mut Context, - ) { - window.focus_prev(); - } -} - -impl EventEmitter for AddLlmProviderModal {} - -impl Focusable for AddLlmProviderModal { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl ModalView for AddLlmProviderModal {} - -impl Render for AddLlmProviderModal { - fn render(&mut self, window: &mut ui::Window, cx: &mut ui::Context) -> impl IntoElement { - let focus_handle = self.focus_handle(cx); - - let window_size = window.viewport_size(); - let rem_size = window.rem_size(); - let is_large_window = window_size.height / rem_size > rems_from_px(600.).0; - - let modal_max_height = if is_large_window { - rems_from_px(450.) - } else { - rems_from_px(200.) - }; - - v_flex() - .id("add-llm-provider-modal") - .key_context("AddLlmProviderModal") - .w(rems(34.)) - .elevation_3(cx) - .on_action(cx.listener(Self::cancel)) - .on_action(cx.listener(Self::on_tab)) - .on_action(cx.listener(Self::on_tab_prev)) - .capture_any_mouse_down(cx.listener(|this, _, window, cx| { - this.focus_handle(cx).focus(window); - })) - .child( - Modal::new("configure-context-server", None) - .header(ModalHeader::new().headline("Add LLM Provider").description( - match self.provider { - LlmCompatibleProvider::OpenAi => { - "This provider will use an OpenAI compatible API." - } - }, - )) - .when_some(self.last_error.clone(), |this, error| { - this.section( - Section::new().child( - Banner::new() - .severity(Severity::Warning) - .child(div().text_xs().child(error)), - ), - ) - }) - .child( - div() - .size_full() - .vertical_scrollbar_for(&self.scroll_handle, window, cx) - .child( - v_flex() - .id("modal_content") - .size_full() - .tab_group() - .max_h(modal_max_height) - .pl_3() - .pr_4() - .gap_2() - .overflow_y_scroll() - .track_scroll(&self.scroll_handle) - .child(self.input.provider_name.clone()) - .child(self.input.api_url.clone()) - .child(self.input.api_key.clone()) - .child(self.render_model_section(cx)), - ), - ) - .footer( - ModalFooter::new().end_slot( - h_flex() - .gap_1() - .child( - Button::new("cancel", "Cancel") - .key_binding( - KeyBinding::for_action_in( - &menu::Cancel, - &focus_handle, - cx, - ) - .map(|kb| kb.size(rems_from_px(12.))), - ) - .on_click(cx.listener(|this, _event, window, cx| { - this.cancel(&menu::Cancel, window, cx) - })), - ) - .child( - Button::new("save-server", "Save Provider") - .key_binding( - KeyBinding::for_action_in( - &menu::Confirm, - &focus_handle, - cx, - ) - .map(|kb| kb.size(rems_from_px(12.))), - ) - .on_click(cx.listener(|this, _event, window, cx| { - this.confirm(&menu::Confirm, window, cx) - })), - ), - ), - ), - ) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use fs::FakeFs; - use gpui::{TestAppContext, VisualTestContext}; - use language_model::{ - LanguageModelProviderId, LanguageModelProviderName, - fake_provider::FakeLanguageModelProvider, - }; - use project::Project; - use settings::SettingsStore; - use util::path; - - #[gpui::test] - async fn test_save_provider_invalid_inputs(cx: &mut TestAppContext) { - let cx = setup_test(cx).await; - - assert_eq!( - save_provider_validation_errors("", "someurl", "somekey", vec![], cx,).await, - Some("Provider Name cannot be empty".into()) - ); - - assert_eq!( - save_provider_validation_errors("someprovider", "", "somekey", vec![], cx,).await, - Some("API URL cannot be empty".into()) - ); - - assert_eq!( - save_provider_validation_errors("someprovider", "someurl", "", vec![], cx,).await, - Some("API Key cannot be empty".into()) - ); - - assert_eq!( - save_provider_validation_errors( - "someprovider", - "someurl", - "somekey", - vec![("", "200000", "200000", "32000")], - cx, - ) - .await, - Some("Model Name cannot be empty".into()) - ); - - assert_eq!( - save_provider_validation_errors( - "someprovider", - "someurl", - "somekey", - vec![("somemodel", "abc", "200000", "32000")], - cx, - ) - .await, - Some("Max Tokens must be a number".into()) - ); - - assert_eq!( - save_provider_validation_errors( - "someprovider", - "someurl", - "somekey", - vec![("somemodel", "200000", "abc", "32000")], - cx, - ) - .await, - Some("Max Completion Tokens must be a number".into()) - ); - - assert_eq!( - save_provider_validation_errors( - "someprovider", - "someurl", - "somekey", - vec![("somemodel", "200000", "200000", "abc")], - cx, - ) - .await, - Some("Max Output Tokens must be a number".into()) - ); - - assert_eq!( - save_provider_validation_errors( - "someprovider", - "someurl", - "somekey", - vec![ - ("somemodel", "200000", "200000", "32000"), - ("somemodel", "200000", "200000", "32000"), - ], - cx, - ) - .await, - Some("Model Names must be unique".into()) - ); - } - - #[gpui::test] - async fn test_save_provider_name_conflict(cx: &mut TestAppContext) { - let cx = setup_test(cx).await; - - cx.update(|_window, cx| { - LanguageModelRegistry::global(cx).update(cx, |registry, cx| { - registry.register_provider( - Arc::new(FakeLanguageModelProvider::new( - LanguageModelProviderId::new("someprovider"), - LanguageModelProviderName::new("Some Provider"), - )), - cx, - ); - }); - }); - - assert_eq!( - save_provider_validation_errors( - "someprovider", - "someurl", - "someapikey", - vec![("somemodel", "200000", "200000", "32000")], - cx, - ) - .await, - Some("Provider Name is already taken by another provider".into()) - ); - } - - #[gpui::test] - async fn test_model_input_default_capabilities(cx: &mut TestAppContext) { - let cx = setup_test(cx).await; - - cx.update(|window, cx| { - let model_input = ModelInput::new(0, window, cx); - model_input.name.update(cx, |input, cx| { - input.editor().update(cx, |editor, cx| { - editor.set_text("somemodel", window, cx); - }); - }); - assert_eq!( - model_input.capabilities.supports_tools, - ToggleState::Selected - ); - assert_eq!( - model_input.capabilities.supports_images, - ToggleState::Unselected - ); - assert_eq!( - model_input.capabilities.supports_parallel_tool_calls, - ToggleState::Unselected - ); - assert_eq!( - model_input.capabilities.supports_prompt_cache_key, - ToggleState::Unselected - ); - - let parsed_model = model_input.parse(cx).unwrap(); - assert!(parsed_model.capabilities.tools); - assert!(!parsed_model.capabilities.images); - assert!(!parsed_model.capabilities.parallel_tool_calls); - assert!(!parsed_model.capabilities.prompt_cache_key); - }); - } - - #[gpui::test] - async fn test_model_input_deselected_capabilities(cx: &mut TestAppContext) { - let cx = setup_test(cx).await; - - cx.update(|window, cx| { - let mut model_input = ModelInput::new(0, window, cx); - model_input.name.update(cx, |input, cx| { - input.editor().update(cx, |editor, cx| { - editor.set_text("somemodel", window, cx); - }); - }); - - model_input.capabilities.supports_tools = ToggleState::Unselected; - model_input.capabilities.supports_images = ToggleState::Unselected; - model_input.capabilities.supports_parallel_tool_calls = ToggleState::Unselected; - model_input.capabilities.supports_prompt_cache_key = ToggleState::Unselected; - - let parsed_model = model_input.parse(cx).unwrap(); - assert!(!parsed_model.capabilities.tools); - assert!(!parsed_model.capabilities.images); - assert!(!parsed_model.capabilities.parallel_tool_calls); - assert!(!parsed_model.capabilities.prompt_cache_key); - }); - } - - #[gpui::test] - async fn test_model_input_with_name_and_capabilities(cx: &mut TestAppContext) { - let cx = setup_test(cx).await; - - cx.update(|window, cx| { - let mut model_input = ModelInput::new(0, window, cx); - model_input.name.update(cx, |input, cx| { - input.editor().update(cx, |editor, cx| { - editor.set_text("somemodel", window, cx); - }); - }); - - model_input.capabilities.supports_tools = ToggleState::Selected; - model_input.capabilities.supports_images = ToggleState::Unselected; - model_input.capabilities.supports_parallel_tool_calls = ToggleState::Selected; - model_input.capabilities.supports_prompt_cache_key = ToggleState::Unselected; - - let parsed_model = model_input.parse(cx).unwrap(); - assert_eq!(parsed_model.name, "somemodel"); - assert!(parsed_model.capabilities.tools); - assert!(!parsed_model.capabilities.images); - assert!(parsed_model.capabilities.parallel_tool_calls); - assert!(!parsed_model.capabilities.prompt_cache_key); - }); - } - - async fn setup_test(cx: &mut TestAppContext) -> &mut VisualTestContext { - cx.update(|cx| { - let store = SettingsStore::test(cx); - cx.set_global(store); - theme::init(theme::LoadThemes::JustBase, cx); - - language_model::init_settings(cx); - }); - - let fs = FakeFs::new(cx.executor()); - cx.update(|cx| ::set_global(fs.clone(), cx)); - let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; - let (_, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - cx - } - - async fn save_provider_validation_errors( - provider_name: &str, - api_url: &str, - api_key: &str, - models: Vec<(&str, &str, &str, &str)>, - cx: &mut VisualTestContext, - ) -> Option { - fn set_text(input: &Entity, text: &str, window: &mut Window, cx: &mut App) { - input.update(cx, |input, cx| { - input.editor().update(cx, |editor, cx| { - editor.set_text(text, window, cx); - }); - }); - } - - let task = cx.update(|window, cx| { - let mut input = AddLlmProviderInput::new(LlmCompatibleProvider::OpenAi, window, cx); - set_text(&input.provider_name, provider_name, window, cx); - set_text(&input.api_url, api_url, window, cx); - set_text(&input.api_key, api_key, window, cx); - - for (i, (name, max_tokens, max_completion_tokens, max_output_tokens)) in - models.iter().enumerate() - { - if i >= input.models.len() { - input.models.push(ModelInput::new(i, window, cx)); - } - let model = &mut input.models[i]; - set_text(&model.name, name, window, cx); - set_text(&model.max_tokens, max_tokens, window, cx); - set_text( - &model.max_completion_tokens, - max_completion_tokens, - window, - cx, - ); - set_text(&model.max_output_tokens, max_output_tokens, window, cx); - } - save_provider_to_settings(&input, cx) - }); - - task.await.err() - } -} diff --git a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs deleted file mode 100644 index a0f0be886a..0000000000 --- a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs +++ /dev/null @@ -1,942 +0,0 @@ -use std::sync::{Arc, Mutex}; - -use anyhow::{Context as _, Result}; -use collections::HashMap; -use context_server::{ContextServerCommand, ContextServerId}; -use editor::{Editor, EditorElement, EditorStyle}; -use gpui::{ - AsyncWindowContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ScrollHandle, - Task, TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, prelude::*, -}; -use language::{Language, LanguageRegistry}; -use markdown::{Markdown, MarkdownElement, MarkdownStyle}; -use notifications::status_toast::{StatusToast, ToastIcon}; -use project::{ - context_server_store::{ - ContextServerStatus, ContextServerStore, registry::ContextServerDescriptorRegistry, - }, - project_settings::{ContextServerSettings, ProjectSettings}, - worktree_store::WorktreeStore, -}; -use serde::Deserialize; -use settings::{Settings as _, update_settings_file}; -use theme::ThemeSettings; -use ui::{ - CommonAnimationExt, KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, - WithScrollbar, prelude::*, -}; -use util::ResultExt as _; -use workspace::{ModalView, Workspace}; - -use crate::AddContextServer; - -enum ConfigurationTarget { - New, - Existing { - id: ContextServerId, - command: ContextServerCommand, - }, - ExistingHttp { - id: ContextServerId, - url: String, - headers: HashMap, - }, - Extension { - id: ContextServerId, - repository_url: Option, - installation: Option, - }, -} - -enum ConfigurationSource { - New { - editor: Entity, - is_http: bool, - }, - Existing { - editor: Entity, - is_http: bool, - }, - Extension { - id: ContextServerId, - editor: Option>, - repository_url: Option, - installation_instructions: Option>, - settings_validator: Option, - }, -} - -impl ConfigurationSource { - fn has_configuration_options(&self) -> bool { - !matches!(self, ConfigurationSource::Extension { editor: None, .. }) - } - - fn is_new(&self) -> bool { - matches!(self, ConfigurationSource::New { .. }) - } - - fn from_target( - target: ConfigurationTarget, - language_registry: Arc, - jsonc_language: Option>, - window: &mut Window, - cx: &mut App, - ) -> Self { - fn create_editor( - json: String, - jsonc_language: Option>, - window: &mut Window, - cx: &mut App, - ) -> Entity { - cx.new(|cx| { - let mut editor = Editor::auto_height(4, 16, window, cx); - editor.set_text(json, window, cx); - editor.set_show_gutter(false, cx); - editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx); - if let Some(buffer) = editor.buffer().read(cx).as_singleton() { - buffer.update(cx, |buffer, cx| buffer.set_language(jsonc_language, cx)) - } - editor - }) - } - - match target { - ConfigurationTarget::New => ConfigurationSource::New { - editor: create_editor(context_server_input(None), jsonc_language, window, cx), - is_http: false, - }, - ConfigurationTarget::Existing { id, command } => ConfigurationSource::Existing { - editor: create_editor( - context_server_input(Some((id, command))), - jsonc_language, - window, - cx, - ), - is_http: false, - }, - ConfigurationTarget::ExistingHttp { - id, - url, - headers: auth, - } => ConfigurationSource::Existing { - editor: create_editor( - context_server_http_input(Some((id, url, auth))), - jsonc_language, - window, - cx, - ), - is_http: true, - }, - ConfigurationTarget::Extension { - id, - repository_url, - installation, - } => { - let settings_validator = installation.as_ref().and_then(|installation| { - jsonschema::validator_for(&installation.settings_schema) - .context("Failed to load JSON schema for context server settings") - .log_err() - }); - let installation_instructions = installation.as_ref().map(|installation| { - cx.new(|cx| { - Markdown::new( - installation.installation_instructions.clone().into(), - Some(language_registry.clone()), - None, - cx, - ) - }) - }); - ConfigurationSource::Extension { - id, - repository_url, - installation_instructions, - settings_validator, - editor: installation.map(|installation| { - create_editor(installation.default_settings, jsonc_language, window, cx) - }), - } - } - } - } - - fn output(&self, cx: &mut App) -> Result<(ContextServerId, ContextServerSettings)> { - match self { - ConfigurationSource::New { editor, is_http } - | ConfigurationSource::Existing { editor, is_http } => { - if *is_http { - parse_http_input(&editor.read(cx).text(cx)).map(|(id, url, auth)| { - ( - id, - ContextServerSettings::Http { - enabled: true, - url, - headers: auth, - }, - ) - }) - } else { - parse_input(&editor.read(cx).text(cx)).map(|(id, command)| { - ( - id, - ContextServerSettings::Stdio { - enabled: true, - command, - }, - ) - }) - } - } - ConfigurationSource::Extension { - id, - editor, - settings_validator, - .. - } => { - let text = editor - .as_ref() - .context("No output available")? - .read(cx) - .text(cx); - let settings = serde_json_lenient::from_str::(&text)?; - if let Some(settings_validator) = settings_validator - && let Err(error) = settings_validator.validate(&settings) - { - return Err(anyhow::anyhow!(error.to_string())); - } - Ok(( - id.clone(), - ContextServerSettings::Extension { - enabled: true, - settings, - }, - )) - } - } - } -} - -fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)>) -> String { - let (name, command, args, env) = match existing { - Some((id, cmd)) => { - let args = serde_json::to_string(&cmd.args).unwrap(); - let env = serde_json::to_string(&cmd.env.unwrap_or_default()).unwrap(); - let cmd_path = serde_json::to_string(&cmd.path).unwrap(); - (id.0.to_string(), cmd_path, args, env) - } - None => ( - "some-mcp-server".to_string(), - "".to_string(), - "[]".to_string(), - "{}".to_string(), - ), - }; - - format!( - r#"{{ - /// The name of your MCP server - "{name}": {{ - /// The command which runs the MCP server - "command": {command}, - /// The arguments to pass to the MCP server - "args": {args}, - /// The environment variables to set - "env": {env} - }} -}}"# - ) -} - -fn context_server_http_input( - existing: Option<(ContextServerId, String, HashMap)>, -) -> String { - let (name, url, headers) = match existing { - Some((id, url, headers)) => { - let header = if headers.is_empty() { - r#"// "Authorization": "Bearer "#.to_string() - } else { - let json = serde_json::to_string_pretty(&headers).unwrap(); - let mut lines = json.split("\n").collect::>(); - if lines.len() > 1 { - lines.remove(0); - lines.pop(); - } - lines - .into_iter() - .map(|line| format!(" {}", line)) - .collect::() - }; - (id.0.to_string(), url, header) - } - None => ( - "some-remote-server".to_string(), - "https://example.com/mcp".to_string(), - r#"// "Authorization": "Bearer "#.to_string(), - ), - }; - - format!( - r#"{{ - /// The name of your remote MCP server - "{name}": {{ - /// The URL of the remote MCP server - "url": "{url}", - "headers": {{ - /// Any headers to send along - {headers} - }} - }} -}}"# - ) -} - -fn parse_http_input(text: &str) -> Result<(ContextServerId, String, HashMap)> { - #[derive(Deserialize)] - struct Temp { - url: String, - #[serde(default)] - headers: HashMap, - } - let value: HashMap = serde_json_lenient::from_str(text)?; - if value.len() != 1 { - anyhow::bail!("Expected exactly one context server configuration"); - } - - let (key, value) = value.into_iter().next().unwrap(); - - Ok((ContextServerId(key.into()), value.url, value.headers)) -} - -fn resolve_context_server_extension( - id: ContextServerId, - worktree_store: Entity, - cx: &mut App, -) -> Task> { - let registry = ContextServerDescriptorRegistry::default_global(cx).read(cx); - - let Some(descriptor) = registry.context_server_descriptor(&id.0) else { - return Task::ready(None); - }; - - let extension = crate::agent_configuration::resolve_extension_for_context_server(&id, cx); - cx.spawn(async move |cx| { - let installation = descriptor - .configuration(worktree_store, cx) - .await - .context("Failed to resolve context server configuration") - .log_err() - .flatten(); - - Some(ConfigurationTarget::Extension { - id, - repository_url: extension - .and_then(|(_, manifest)| manifest.repository.clone().map(SharedString::from)), - installation, - }) - }) -} - -enum State { - Idle, - Waiting, - Error(SharedString), -} - -pub struct ConfigureContextServerModal { - context_server_store: Entity, - workspace: WeakEntity, - source: ConfigurationSource, - state: State, - original_server_id: Option, - scroll_handle: ScrollHandle, -} - -impl ConfigureContextServerModal { - pub fn register( - workspace: &mut Workspace, - language_registry: Arc, - _window: Option<&mut Window>, - _cx: &mut Context, - ) { - workspace.register_action({ - move |_workspace, _: &AddContextServer, window, cx| { - let workspace_handle = cx.weak_entity(); - let language_registry = language_registry.clone(); - window - .spawn(cx, async move |cx| { - Self::show_modal( - ConfigurationTarget::New, - language_registry, - workspace_handle, - cx, - ) - .await - }) - .detach_and_log_err(cx); - } - }); - } - - pub fn show_modal_for_existing_server( - server_id: ContextServerId, - language_registry: Arc, - workspace: WeakEntity, - window: &mut Window, - cx: &mut App, - ) -> Task> { - let Some(settings) = ProjectSettings::get_global(cx) - .context_servers - .get(&server_id.0) - .cloned() - .or_else(|| { - ContextServerDescriptorRegistry::default_global(cx) - .read(cx) - .context_server_descriptor(&server_id.0) - .map(|_| ContextServerSettings::default_extension()) - }) - else { - return Task::ready(Err(anyhow::anyhow!("Context server not found"))); - }; - - window.spawn(cx, async move |cx| { - let target = match settings { - ContextServerSettings::Stdio { - enabled: _, - command, - } => Some(ConfigurationTarget::Existing { - id: server_id, - command, - }), - ContextServerSettings::Http { - enabled: _, - url, - headers, - } => Some(ConfigurationTarget::ExistingHttp { - id: server_id, - url, - headers, - }), - ContextServerSettings::Extension { .. } => { - match workspace - .update(cx, |workspace, cx| { - resolve_context_server_extension( - server_id, - workspace.project().read(cx).worktree_store(), - cx, - ) - }) - .ok() - { - Some(task) => task.await, - None => None, - } - } - }; - - match target { - Some(target) => Self::show_modal(target, language_registry, workspace, cx).await, - None => Err(anyhow::anyhow!("Failed to resolve context server")), - } - }) - } - - fn show_modal( - target: ConfigurationTarget, - language_registry: Arc, - workspace: WeakEntity, - cx: &mut AsyncWindowContext, - ) -> Task> { - cx.spawn(async move |cx| { - let jsonc_language = language_registry.language_for_name("jsonc").await.ok(); - workspace.update_in(cx, |workspace, window, cx| { - let workspace_handle = cx.weak_entity(); - let context_server_store = workspace.project().read(cx).context_server_store(); - workspace.toggle_modal(window, cx, |window, cx| Self { - context_server_store, - workspace: workspace_handle, - state: State::Idle, - original_server_id: match &target { - ConfigurationTarget::Existing { id, .. } => Some(id.clone()), - ConfigurationTarget::ExistingHttp { id, .. } => Some(id.clone()), - ConfigurationTarget::Extension { id, .. } => Some(id.clone()), - ConfigurationTarget::New => None, - }, - source: ConfigurationSource::from_target( - target, - language_registry, - jsonc_language, - window, - cx, - ), - scroll_handle: ScrollHandle::new(), - }) - }) - }) - } - - fn set_error(&mut self, err: impl Into, cx: &mut Context) { - self.state = State::Error(err.into()); - cx.notify(); - } - - fn confirm(&mut self, _: &menu::Confirm, cx: &mut Context) { - self.state = State::Idle; - let Some(workspace) = self.workspace.upgrade() else { - return; - }; - - let (id, settings) = match self.source.output(cx) { - Ok(val) => val, - Err(error) => { - self.set_error(error.to_string(), cx); - return; - } - }; - - self.state = State::Waiting; - - let existing_server = self.context_server_store.read(cx).get_running_server(&id); - if existing_server.is_some() { - self.context_server_store.update(cx, |store, cx| { - store.stop_server(&id, cx).log_err(); - }); - } - - let wait_for_context_server_task = - wait_for_context_server(&self.context_server_store, id.clone(), cx); - cx.spawn({ - let id = id.clone(); - async move |this, cx| { - let result = wait_for_context_server_task.await; - this.update(cx, |this, cx| match result { - Ok(_) => { - this.state = State::Idle; - this.show_configured_context_server_toast(id, cx); - cx.emit(DismissEvent); - } - Err(err) => { - this.set_error(err, cx); - } - }) - } - }) - .detach(); - - let settings_changed = - ProjectSettings::get_global(cx).context_servers.get(&id.0) != Some(&settings); - - if settings_changed { - // When we write the settings to the file, the context server will be restarted. - workspace.update(cx, |workspace, cx| { - let fs = workspace.app_state().fs.clone(); - let original_server_id = self.original_server_id.clone(); - update_settings_file(fs.clone(), cx, move |current, _| { - if let Some(original_id) = original_server_id { - if original_id != id { - current.project.context_servers.remove(&original_id.0); - } - } - current - .project - .context_servers - .insert(id.0, settings.into()); - }); - }); - } else if let Some(existing_server) = existing_server { - self.context_server_store - .update(cx, |store, cx| store.start_server(existing_server, cx)); - } - } - - fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context) { - cx.emit(DismissEvent); - } - - fn show_configured_context_server_toast(&self, id: ContextServerId, cx: &mut App) { - self.workspace - .update(cx, { - |workspace, cx| { - let status_toast = StatusToast::new( - format!("{} configured successfully.", id.0), - cx, - |this, _cx| { - this.icon(ToastIcon::new(IconName::ToolHammer).color(Color::Muted)) - .action("Dismiss", |_, _| {}) - }, - ); - - workspace.toggle_status_toast(status_toast, cx); - } - }) - .log_err(); - } -} - -fn parse_input(text: &str) -> Result<(ContextServerId, ContextServerCommand)> { - let value: serde_json::Value = serde_json_lenient::from_str(text)?; - let object = value.as_object().context("Expected object")?; - anyhow::ensure!(object.len() == 1, "Expected exactly one key-value pair"); - let (context_server_name, value) = object.into_iter().next().unwrap(); - let command: ContextServerCommand = serde_json::from_value(value.clone())?; - Ok((ContextServerId(context_server_name.clone().into()), command)) -} - -impl ModalView for ConfigureContextServerModal {} - -impl Focusable for ConfigureContextServerModal { - fn focus_handle(&self, cx: &App) -> FocusHandle { - match &self.source { - ConfigurationSource::New { editor, .. } => editor.focus_handle(cx), - ConfigurationSource::Existing { editor, .. } => editor.focus_handle(cx), - ConfigurationSource::Extension { editor, .. } => editor - .as_ref() - .map(|editor| editor.focus_handle(cx)) - .unwrap_or_else(|| cx.focus_handle()), - } - } -} - -impl EventEmitter for ConfigureContextServerModal {} - -impl ConfigureContextServerModal { - fn render_modal_header(&self) -> ModalHeader { - let text: SharedString = match &self.source { - ConfigurationSource::New { .. } => "Add MCP Server".into(), - ConfigurationSource::Existing { .. } => "Configure MCP Server".into(), - ConfigurationSource::Extension { id, .. } => format!("Configure {}", id.0).into(), - }; - ModalHeader::new().headline(text) - } - - fn render_modal_description(&self, window: &mut Window, cx: &mut Context) -> AnyElement { - const MODAL_DESCRIPTION: &str = "Visit the MCP server configuration docs to find all necessary arguments and environment variables."; - - if let ConfigurationSource::Extension { - installation_instructions: Some(installation_instructions), - .. - } = &self.source - { - div() - .pb_2() - .text_sm() - .child(MarkdownElement::new( - installation_instructions.clone(), - default_markdown_style(window, cx), - )) - .into_any_element() - } else { - Label::new(MODAL_DESCRIPTION) - .color(Color::Muted) - .into_any_element() - } - } - - fn render_modal_content(&self, cx: &App) -> AnyElement { - let editor = match &self.source { - ConfigurationSource::New { editor, .. } => editor, - ConfigurationSource::Existing { editor, .. } => editor, - ConfigurationSource::Extension { editor, .. } => { - let Some(editor) = editor else { - return div().into_any_element(); - }; - editor - } - }; - - div() - .p_2() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border_variant) - .bg(cx.theme().colors().editor_background) - .child({ - let settings = ThemeSettings::get_global(cx); - let text_style = TextStyle { - color: cx.theme().colors().text, - font_family: settings.buffer_font.family.clone(), - font_fallbacks: settings.buffer_font.fallbacks.clone(), - font_size: settings.buffer_font_size(cx).into(), - font_weight: settings.buffer_font.weight, - line_height: relative(settings.buffer_line_height.value()), - ..Default::default() - }; - EditorElement::new( - editor, - EditorStyle { - background: cx.theme().colors().editor_background, - local_player: cx.theme().players().local(), - text: text_style, - syntax: cx.theme().syntax().clone(), - ..Default::default() - }, - ) - }) - .into_any_element() - } - - fn render_modal_footer(&self, cx: &mut Context) -> ModalFooter { - let focus_handle = self.focus_handle(cx); - let is_connecting = matches!(self.state, State::Waiting); - - ModalFooter::new() - .start_slot:: - - -
- -
-
-
- -
- Thread 1 of 1: - Default Thread -
- -
- - - - - - - - - - - - -
TurnTextToolResult
- - - - diff --git a/crates/eval/src/explorer.rs b/crates/eval/src/explorer.rs deleted file mode 100644 index 3326070cea..0000000000 --- a/crates/eval/src/explorer.rs +++ /dev/null @@ -1,182 +0,0 @@ -use anyhow::{Context as _, Result}; -use clap::Parser; -use serde_json::{Value, json}; -use std::fs; -use std::path::{Path, PathBuf}; - -#[derive(Parser, Debug)] -#[clap(about = "Generate HTML explorer from JSON thread files")] -struct Args { - /// Paths to JSON files or directories. If a directory is provided, - /// it will be searched for 'last.messages.json' files up to 2 levels deep. - #[clap(long, required = true, num_args = 1..)] - input: Vec, - - /// Path where the output HTML file will be written - #[clap(long)] - output: PathBuf, -} - -/// Recursively finds files with `target_filename` in `dir_path` up to `max_depth`. -#[allow(dead_code)] -fn find_target_files_recursive( - dir_path: &Path, - target_filename: &str, - current_depth: u8, - max_depth: u8, - found_files: &mut Vec, -) -> Result<()> { - if current_depth > max_depth { - return Ok(()); - } - - for entry_result in fs::read_dir(dir_path) - .with_context(|| format!("Failed to read directory: {}", dir_path.display()))? - { - let entry = entry_result.with_context(|| { - format!("Failed to read directory entry in: {}", dir_path.display()) - })?; - let path = entry.path(); - - if path.is_dir() { - find_target_files_recursive( - &path, - target_filename, - current_depth + 1, - max_depth, - found_files, - )?; - } else if path.is_file() - && let Some(filename_osstr) = path.file_name() - && let Some(filename_str) = filename_osstr.to_str() - && filename_str == target_filename - { - found_files.push(path); - } - } - Ok(()) -} - -pub fn generate_explorer_html(input_paths: &[PathBuf], output_path: &PathBuf) -> Result { - if let Some(parent) = output_path.parent() - && !parent.exists() - { - fs::create_dir_all(parent).context(format!( - "Failed to create output directory: {}", - parent.display() - ))?; - } - - let template_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/explorer.html"); - let template_content = fs::read_to_string(&template_path).context(format!( - "Template file not found or couldn't be read: {}", - template_path.display() - ))?; - - if input_paths.is_empty() { - println!( - "No input JSON files found to process. Explorer will be generated with template defaults or empty data." - ); - } - - let threads = input_paths - .iter() - .map(|input_path| { - let file_content = fs::read_to_string(input_path) - .context(format!("Failed to read file: {}", input_path.display()))?; - let mut thread_data: Value = file_content - .parse::() - .context(format!("Failed to parse JSON from file: {}", input_path.display()))?; - - if let Some(obj) = thread_data.as_object_mut() { - obj.insert("filename".to_string(), json!(input_path.display().to_string())); - } else { - eprintln!("Warning: JSON data in {} is not a root object. Wrapping it to include filename.", input_path.display()); - thread_data = json!({ - "original_data": thread_data, - "filename": input_path.display().to_string() - }); - } - Ok(thread_data) - }) - .collect::>>()?; - - let all_threads_data = json!({ "threads": threads }); - let html_content = inject_thread_data(template_content, all_threads_data)?; - fs::write(&output_path, &html_content) - .context(format!("Failed to write output: {}", output_path.display()))?; - - println!( - "Saved data from {} resolved file(s) ({} threads) to {}", - input_paths.len(), - threads.len(), - output_path.display() - ); - Ok(html_content) -} - -fn inject_thread_data(template: String, threads_data: Value) -> Result { - let injection_marker = "let threadsData = window.threadsData || { threads: [dummyThread] };"; - if !template.contains(injection_marker) { - anyhow::bail!( - "Could not find the thread injection point in the template. Expected: '{}'", - injection_marker - ); - } - - let threads_json_string = serde_json::to_string_pretty(&threads_data) - .context("Failed to serialize threads data to JSON")? - .replace("", r"<\/script>"); - - let script_injection = format!("let threadsData = {};", threads_json_string); - let final_html = template.replacen(injection_marker, &script_injection, 1); - - Ok(final_html) -} - -#[cfg(not(any(test, doctest)))] -#[allow(dead_code)] -fn main() -> Result<()> { - let args = Args::parse(); - - const DEFAULT_FILENAME: &str = "last.messages.json"; - const MAX_SEARCH_DEPTH: u8 = 2; - - let mut resolved_input_files: Vec = Vec::new(); - - for input_path_arg in &args.input { - if !input_path_arg.exists() { - eprintln!( - "Warning: Input path {} does not exist. Skipping.", - input_path_arg.display() - ); - continue; - } - - if input_path_arg.is_dir() { - find_target_files_recursive( - input_path_arg, - DEFAULT_FILENAME, - 0, // starting depth - MAX_SEARCH_DEPTH, - &mut resolved_input_files, - ) - .with_context(|| { - format!( - "Error searching for '{}' files in directory: {}", - DEFAULT_FILENAME, - input_path_arg.display() - ) - })?; - } else if input_path_arg.is_file() { - resolved_input_files.push(input_path_arg.clone()); - } - } - - resolved_input_files.sort_unstable(); - resolved_input_files.dedup(); - - println!("No input paths provided/found."); - - generate_explorer_html(&resolved_input_files, &args.output).map(|_| ()) -} diff --git a/crates/eval/src/ids.rs b/crates/eval/src/ids.rs deleted file mode 100644 index 7057344206..0000000000 --- a/crates/eval/src/ids.rs +++ /dev/null @@ -1,29 +0,0 @@ -use anyhow::{Context as _, Result}; -use std::fs; -use std::path::{Path, PathBuf}; -use uuid::Uuid; - -pub fn get_or_create_id(path: &Path) -> Result { - if let Ok(id) = fs::read_to_string(path) { - let trimmed = id.trim(); - if !trimmed.is_empty() { - return Ok(trimmed.to_string()); - } - } - let new_id = Uuid::new_v4().to_string(); - fs::create_dir_all(path.parent().context("invalid id path")?)?; - fs::write(path, &new_id)?; - Ok(new_id) -} - -pub fn eval_system_id_path() -> PathBuf { - dirs::data_local_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join("zed-eval-system-id") -} - -pub fn eval_installation_id_path() -> PathBuf { - dirs::data_local_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join("zed-eval-installation-id") -} diff --git a/crates/eval/src/instance.rs b/crates/eval/src/instance.rs deleted file mode 100644 index 1af705cd4b..0000000000 --- a/crates/eval/src/instance.rs +++ /dev/null @@ -1,1424 +0,0 @@ -use agent::ContextServerRegistry; -use agent_client_protocol as acp; -use anyhow::{Context as _, Result, anyhow, bail}; -use client::proto::LspWorkProgress; -use futures::channel::mpsc; -use futures::future::Shared; -use futures::{FutureExt as _, StreamExt as _, future}; -use gpui::{App, AppContext as _, AsyncApp, Entity, Task}; -use handlebars::Handlebars; -use language::{Buffer, DiagnosticSeverity, OffsetRangeExt as _}; -use language_model::{ - LanguageModel, LanguageModelCompletionEvent, LanguageModelRegistry, LanguageModelRequest, - LanguageModelRequestMessage, LanguageModelToolResultContent, MessageContent, Role, TokenUsage, -}; -use project::{DiagnosticSummary, Project, ProjectPath, lsp_store::OpenLspBufferHandle}; -use prompt_store::{ProjectContext, WorktreeContext}; -use rand::{distr, prelude::*}; -use serde::{Deserialize, Serialize}; -use std::{ - fmt::Write as _, - fs::{self, File}, - io::Write as _, - path::{Path, PathBuf}, - rc::Rc, - sync::{Arc, Mutex}, - time::Duration, -}; -use unindent::Unindent as _; -use util::{ResultExt as _, command::new_smol_command, markdown::MarkdownCodeBlock}; - -use crate::{ - AgentAppState, ToolMetrics, - assertions::{AssertionsReport, RanAssertion, RanAssertionResult}, - example::{Example, ExampleContext, FailedAssertion, JudgeAssertion}, -}; - -pub const ZED_REPO_URL: &str = "https://github.com/zed-industries/zed.git"; - -#[derive(Clone)] -pub struct ExampleInstance { - pub thread: Rc, - pub name: String, - pub run_directory: PathBuf, - pub log_prefix: String, - /// The repetition number for this example (0-based) - /// When running multiple repetitions of the same example, each instance is assigned a unique repetition number. - /// This affects the worktree path and log prefix to avoid clobbering results between runs. - pub repetition: usize, - pub repo_path: PathBuf, - /// Path to the directory containing the requests and responses for the agentic loop - worktrees_dir: PathBuf, -} - -#[derive(Debug, Serialize, Clone)] -pub struct RunOutput { - pub repository_diff: String, - pub diagnostic_summary_before: DiagnosticSummary, - pub diagnostic_summary_after: DiagnosticSummary, - pub diagnostics_before: Option, - pub diagnostics_after: Option, - pub token_usage: TokenUsage, - pub tool_metrics: ToolMetrics, - pub thread_markdown: String, - pub programmatic_assertions: AssertionsReport, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct JudgeDiffInput { - pub repository_diff: String, - pub assertion: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct JudgeThreadInput { - pub messages: String, - pub assertion: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct JudgeOutput { - pub thread: AssertionsReport, - pub diff: AssertionsReport, -} - -impl ExampleInstance { - pub fn new( - thread: Rc, - repos_dir: &Path, - run_dir: &Path, - worktrees_dir: &Path, - repetition: usize, - ) -> Self { - let name = thread.meta().name; - let run_directory = run_dir.join(&name).join(repetition.to_string()); - - let repo_path = repo_path_for_url(repos_dir, &thread.meta().url); - - Self { - name, - thread, - log_prefix: String::new(), - run_directory, - repetition, - repo_path, - worktrees_dir: worktrees_dir.to_path_buf(), - } - } - - pub fn repo_url(&self) -> String { - self.thread.meta().url - } - - pub fn revision(&self) -> String { - self.thread.meta().revision - } - - pub fn worktree_name(&self) -> String { - format!("{}-{}", self.name, self.repetition) - } - - pub fn set_log_prefix_style(&mut self, color: &str, name_width: usize) { - self.log_prefix = format!( - "{}{: Result<()> { - let meta = self.thread.meta(); - - let revision_exists = run_git( - &self.repo_path, - &["rev-parse", &format!("{}^{{commit}}", &meta.revision)], - ) - .await - .is_ok(); - - if !revision_exists { - println!("{}Fetching revision {}", self.log_prefix, &meta.revision); - run_git( - &self.repo_path, - &["fetch", "--depth", "1", "origin", &meta.revision], - ) - .await?; - } - Ok(()) - } - - /// Set up the example by checking out the specified Git revision - pub async fn setup(&mut self) -> Result<()> { - let worktree_path = self.worktree_path(); - let meta = self.thread.meta(); - if worktree_path.is_dir() { - println!("{}Resetting existing worktree", self.log_prefix); - - // TODO: consider including "-x" to remove ignored files. The downside of this is that - // it will also remove build artifacts, and so prevent incremental reuse there. - run_git(&worktree_path, &["clean", "--force", "-d"]).await?; - run_git(&worktree_path, &["reset", "--hard", "HEAD"]).await?; - run_git(&worktree_path, &["checkout", &meta.revision]).await?; - } else { - println!("{}Creating worktree", self.log_prefix); - - let worktree_path_string = worktree_path.to_string_lossy().into_owned(); - - run_git( - &self.repo_path, - &[ - "worktree", - "add", - "-f", - &worktree_path_string, - &meta.revision, - ], - ) - .await?; - } - - if meta.url == ZED_REPO_URL { - std::fs::write(worktree_path.join(".rules"), std::fs::read(".rules")?)?; - } - - std::fs::create_dir_all(&self.run_directory)?; - - Ok(()) - } - - pub fn worktree_path(&self) -> PathBuf { - self.worktrees_dir - .join(self.worktree_name()) - .join(self.thread.meta().repo_name()) - } - - pub fn run(&self, app_state: Arc, cx: &mut App) -> Task> { - let project = Project::local( - app_state.client.clone(), - app_state.node_runtime.clone(), - app_state.user_store.clone(), - app_state.languages.clone(), - app_state.fs.clone(), - None, - cx, - ); - - let worktree = project.update(cx, |project, cx| { - project.create_worktree(self.worktree_path(), true, cx) - }); - - let meta = self.thread.meta(); - let this = self.clone(); - - cx.spawn(async move |cx| { - let worktree = worktree.await?; - - // Wait for worktree scan to finish before choosing a file to open. - worktree - .update(cx, |worktree, _cx| { - worktree.as_local().unwrap().scan_complete() - })? - .await; - - struct LanguageServerState { - _lsp_open_handle: OpenLspBufferHandle, - language_file_buffer: Entity, - } - - let mut diagnostics_before = None; - let mut diagnostic_summary_before = DiagnosticSummary::default(); - - let lsp = if let Some(language_server) = &meta.language_server { - // Open a file that matches the language to cause LSP to start. - let language_file = worktree.read_with(cx, |worktree, _cx| { - worktree - .files(false, 0) - .find_map(|e| { - if e.path.clone().extension() - == Some(&language_server.file_extension) - { - Some(ProjectPath { - worktree_id: worktree.id(), - path: e.path.clone(), - }) - } else { - None - } - }) - .context("Failed to find a file for example language") - })??; - - let open_language_file_buffer_task = project.update(cx, |project, cx| { - project.open_buffer(language_file.clone(), cx) - })?; - - let language_file_buffer = open_language_file_buffer_task.await?; - - let lsp_open_handle = project.update(cx, |project, cx| { - project.register_buffer_with_language_servers(&language_file_buffer, cx) - })?; - - wait_for_lang_server(&project, &language_file_buffer, this.log_prefix.clone(), cx).await?; - - diagnostic_summary_before = project.read_with(cx, |project, cx| { - project.diagnostic_summary(false, cx) - })?; - - diagnostics_before = query_lsp_diagnostics(project.clone(), cx).await?; - if diagnostics_before.is_some() && language_server.allow_preexisting_diagnostics { - anyhow::bail!("Example has pre-existing diagnostics. If you want to run this example regardless, set `allow_preexisting_diagnostics` to `true` in `base.toml`"); - } - - Some(LanguageServerState { - _lsp_open_handle: lsp_open_handle, - language_file_buffer, - }) - } else { - None - }; - - anyhow::ensure!(std::env::var("ZED_EVAL_SETUP_ONLY").is_err(), "Setup only mode"); - - let last_diff_file_path = this.run_directory.join("last.diff"); - - // Write an empty "last.diff" so that it can be opened in Zed for convenient view of the - // history using undo/redo. - std::fs::write(&last_diff_file_path, "")?; - - let thread = cx.update(|cx| { - //todo: Do we want to load rules files here? - let worktrees = project.read(cx).visible_worktrees(cx).map(|worktree| { - let root_name = worktree.read(cx).root_name_str().into(); - let abs_path = worktree.read(cx).abs_path(); - - WorktreeContext { - root_name, - abs_path, - rules_file: None, - } - }).collect::>(); - let project_context = cx.new(|_cx| ProjectContext::new(worktrees, vec![])); - let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx)); - - let thread = if let Some(json) = &meta.existing_thread_json { - let session_id = acp::SessionId::new( - rand::rng() - .sample_iter(&distr::Alphanumeric) - .take(7) - .map(char::from) - .collect::(), - ); - - let db_thread = agent::DbThread::from_json(json.as_bytes()).expect("Can't read serialized thread"); - cx.new(|cx| agent::Thread::from_db(session_id, db_thread, project.clone(), project_context, context_server_registry, agent::Templates::new(), cx)) - } else { - cx.new(|cx| agent::Thread::new(project.clone(), project_context, context_server_registry, agent::Templates::new(), None, cx)) - }; - - thread.update(cx, |thread, cx| { - thread.add_default_tools(Rc::new(EvalThreadEnvironment { - project: project.clone(), - }), cx); - thread.set_profile(meta.profile_id.clone(), cx); - thread.set_model( - LanguageModelInterceptor::new( - LanguageModelRegistry::read_global(cx).default_model().expect("Missing model").model.clone(), - this.run_directory.clone(), - last_diff_file_path.clone(), - this.run_directory.join("last.messages.json"), - this.worktree_path(), - this.repo_url(), - ), - cx, - ); - }); - - thread - }).unwrap(); - - let mut example_cx = ExampleContext::new( - meta.clone(), - this.log_prefix.clone(), - thread.clone(), - cx.clone(), - ); - let result = this.thread.conversation(&mut example_cx).await; - - if let Err(err) = result - && !err.is::() { - return Err(err); - } - - println!("{}Stopped", this.log_prefix); - - println!("{}Getting repository diff", this.log_prefix); - let repository_diff = Self::repository_diff(this.worktree_path(), &this.repo_url()).await?; - - std::fs::write(last_diff_file_path, &repository_diff)?; - - - let mut diagnostics_after = None; - let mut diagnostic_summary_after = Default::default(); - - if let Some(language_server_state) = lsp { - wait_for_lang_server(&project, &language_server_state.language_file_buffer, this.log_prefix.clone(), cx).await?; - - println!("{}Getting diagnostics", this.log_prefix); - diagnostics_after = cx - .update(|cx| { - let project = project.clone(); - cx.spawn(async move |cx| query_lsp_diagnostics(project, cx).await) - })? - .await?; - println!("{}Got diagnostics", this.log_prefix); - - diagnostic_summary_after = project.read_with(cx, |project, cx| { - project.diagnostic_summary(false, cx) - })?; - - } - - if let Some(diagnostics_before) = &diagnostics_before { - fs::write(this.run_directory.join("diagnostics_before.txt"), diagnostics_before)?; - } - - if let Some(diagnostics_after) = &diagnostics_after { - fs::write(this.run_directory.join("diagnostics_after.txt"), diagnostics_after)?; - } - - thread.update(cx, |thread, _cx| { - RunOutput { - repository_diff, - diagnostic_summary_before, - diagnostic_summary_after, - diagnostics_before, - diagnostics_after, - token_usage: thread.latest_request_token_usage().unwrap(), - tool_metrics: example_cx.tool_metrics.lock().unwrap().clone(), - thread_markdown: thread.to_markdown(), - programmatic_assertions: example_cx.assertions, - } - }) - }) - } - - async fn repository_diff(repository_path: PathBuf, repository_url: &str) -> Result { - run_git(&repository_path, &["add", "."]).await?; - let mut diff_args = vec!["diff", "--staged"]; - if repository_url == ZED_REPO_URL { - diff_args.push(":(exclude).rules"); - } - run_git(&repository_path, &diff_args).await - } - - pub async fn judge( - &self, - model: Arc, - run_output: &RunOutput, - cx: &AsyncApp, - ) -> JudgeOutput { - let mut output_file = - File::create(self.run_directory.join("judge.md")).expect("failed to create judge.md"); - - let diff_task = self.judge_diff(model.clone(), run_output, cx); - let thread_task = self.judge_thread(model.clone(), run_output, cx); - - let (diff_result, thread_result) = futures::join!(diff_task, thread_task); - - let (diff_response, diff_output) = diff_result; - let (thread_response, thread_output) = thread_result; - - writeln!( - &mut output_file, - "# Judgment\n\n## Thread\n\n{thread_response}\n\n## Diff\n\n{diff_response}", - ) - .log_err(); - - JudgeOutput { - thread: thread_output, - diff: diff_output, - } - } - - async fn judge_diff( - &self, - model: Arc, - run_output: &RunOutput, - cx: &AsyncApp, - ) -> (String, AssertionsReport) { - let diff_assertions = self.thread.diff_assertions(); - - if diff_assertions.is_empty() { - return ( - "No diff assertions".to_string(), - AssertionsReport::default(), - ); - } - - println!("{}Running diff judge", self.log_prefix); - - let judge_diff_prompt = include_str!("judge_diff_prompt.hbs"); - let judge_diff_prompt_name = "judge_diff_prompt"; - let mut hbs = Handlebars::new(); - hbs.register_template_string(judge_diff_prompt_name, judge_diff_prompt) - .unwrap(); - - let to_prompt = |assertion: String| { - hbs.render( - judge_diff_prompt_name, - &JudgeDiffInput { - repository_diff: run_output.repository_diff.clone(), - assertion, - }, - ) - .unwrap() - }; - - let (responses, report) = self - .judge_assertions(model, diff_assertions, to_prompt, cx) - .await; - - println!( - "{}Judge - Diff score: {}%", - self.log_prefix, - report.passed_percentage() - ); - - (responses, report) - } - - async fn judge_thread( - &self, - model: Arc, - run_output: &RunOutput, - cx: &AsyncApp, - ) -> (String, AssertionsReport) { - let thread_assertions = self.thread.thread_assertions(); - - if thread_assertions.is_empty() { - return ( - "No thread assertions".to_string(), - AssertionsReport::default(), - ); - } - - let judge_thread_prompt = include_str!("judge_thread_prompt.hbs"); - let judge_thread_prompt_name = "judge_thread_prompt"; - let mut hbs = Handlebars::new(); - hbs.register_template_string(judge_thread_prompt_name, judge_thread_prompt) - .unwrap(); - - let complete_messages = &run_output.thread_markdown; - let to_prompt = |assertion: String| { - hbs.render( - judge_thread_prompt_name, - &JudgeThreadInput { - messages: complete_messages.clone(), - assertion, - }, - ) - .unwrap() - }; - - let (responses, report) = self - .judge_assertions(model, thread_assertions, to_prompt, cx) - .await; - - println!( - "{}Judge - Thread score: {}%", - self.log_prefix, - report.passed_percentage() - ); - - (responses, report) - } - - async fn judge_assertions( - &self, - model: Arc, - assertions: Vec, - to_prompt: impl Fn(String) -> String, - cx: &AsyncApp, - ) -> (String, AssertionsReport) { - let assertions = assertions.into_iter().map(|assertion| { - let request = LanguageModelRequest { - thread_id: None, - prompt_id: None, - mode: None, - intent: None, - messages: vec![LanguageModelRequestMessage { - role: Role::User, - content: vec![MessageContent::Text(to_prompt(assertion.description))], - cache: false, - reasoning_details: None, - }], - temperature: None, - tools: Vec::new(), - tool_choice: None, - stop: Vec::new(), - thinking_allowed: true, - }; - - let model = model.clone(); - let log_prefix = self.log_prefix.clone(); - async move { - let response = send_language_model_request(model, request, cx).await; - - let (response, result) = match response { - Ok(response) => ( - response.clone(), - parse_assertion_result(&response).map_err(|err| err.to_string()), - ), - Err(err) => (err.to_string(), Err(err.to_string())), - }; - - if result.is_ok() { - println!("{}✅ {}", log_prefix, assertion.id); - } else { - println!("{}❌ {}", log_prefix, assertion.id); - } - - ( - response, - RanAssertion { - id: assertion.id, - result, - }, - ) - } - }); - - let mut responses = String::new(); - let mut report = AssertionsReport::default(); - - for (response, assertion) in future::join_all(assertions).await { - writeln!(&mut responses, "# {}", assertion.id).unwrap(); - writeln!(&mut responses, "{}\n\n", response).unwrap(); - report.ran.push(assertion); - } - - (responses, report) - } -} - -struct EvalThreadEnvironment { - project: Entity, -} - -struct EvalTerminalHandle { - terminal: Entity, -} - -impl agent::TerminalHandle for EvalTerminalHandle { - fn id(&self, cx: &AsyncApp) -> Result { - self.terminal.read_with(cx, |term, _cx| term.id().clone()) - } - - fn wait_for_exit(&self, cx: &AsyncApp) -> Result>> { - self.terminal - .read_with(cx, |term, _cx| term.wait_for_exit()) - } - - fn current_output(&self, cx: &AsyncApp) -> Result { - self.terminal - .read_with(cx, |term, cx| term.current_output(cx)) - } -} - -impl agent::ThreadEnvironment for EvalThreadEnvironment { - fn create_terminal( - &self, - command: String, - cwd: Option, - output_byte_limit: Option, - cx: &mut AsyncApp, - ) -> Task>> { - let project = self.project.clone(); - cx.spawn(async move |cx| { - let language_registry = - project.read_with(cx, |project, _cx| project.languages().clone())?; - let id = acp::TerminalId::new(uuid::Uuid::new_v4().to_string()); - let terminal = - acp_thread::create_terminal_entity(command, &[], vec![], cwd.clone(), &project, cx) - .await?; - let terminal = cx.new(|cx| { - acp_thread::Terminal::new( - id, - "", - cwd, - output_byte_limit.map(|limit| limit as usize), - terminal, - language_registry, - cx, - ) - })?; - Ok(Rc::new(EvalTerminalHandle { terminal }) as Rc) - }) - } -} - -struct LanguageModelInterceptor { - model: Arc, - request_count: Arc>, - previous_diff: Arc>, - example_output_dir: PathBuf, - last_diff_file_path: PathBuf, - messages_json_file_path: PathBuf, - repository_path: PathBuf, - repository_url: String, -} - -impl LanguageModelInterceptor { - fn new( - model: Arc, - example_output_dir: PathBuf, - last_diff_file_path: PathBuf, - messages_json_file_path: PathBuf, - repository_path: PathBuf, - repository_url: String, - ) -> Arc { - Arc::new(Self { - model, - request_count: Arc::new(Mutex::new(0)), - previous_diff: Arc::new(Mutex::new("".to_string())), - example_output_dir, - last_diff_file_path, - messages_json_file_path, - repository_path, - repository_url, - }) - } -} - -impl language_model::LanguageModel for LanguageModelInterceptor { - fn id(&self) -> language_model::LanguageModelId { - self.model.id() - } - - fn name(&self) -> language_model::LanguageModelName { - self.model.name() - } - - fn provider_id(&self) -> language_model::LanguageModelProviderId { - self.model.provider_id() - } - - fn provider_name(&self) -> language_model::LanguageModelProviderName { - self.model.provider_name() - } - - fn telemetry_id(&self) -> String { - self.model.telemetry_id() - } - - fn supports_images(&self) -> bool { - self.model.supports_images() - } - - fn supports_tools(&self) -> bool { - self.model.supports_tools() - } - - fn supports_tool_choice(&self, choice: language_model::LanguageModelToolChoice) -> bool { - self.model.supports_tool_choice(choice) - } - - fn max_token_count(&self) -> u64 { - self.model.max_token_count() - } - - fn count_tokens( - &self, - request: LanguageModelRequest, - cx: &App, - ) -> future::BoxFuture<'static, Result> { - self.model.count_tokens(request, cx) - } - - fn stream_completion( - &self, - request: LanguageModelRequest, - cx: &AsyncApp, - ) -> future::BoxFuture< - 'static, - Result< - futures::stream::BoxStream< - 'static, - Result, - >, - language_model::LanguageModelCompletionError, - >, - > { - let stream = self.model.stream_completion(request.clone(), cx); - let request_count = self.request_count.clone(); - let previous_diff = self.previous_diff.clone(); - let example_output_dir = self.example_output_dir.clone(); - let last_diff_file_path = self.last_diff_file_path.clone(); - let messages_json_file_path = self.messages_json_file_path.clone(); - let repository_path = self.repository_path.clone(); - let repository_url = self.repository_url.clone(); - - Box::pin(async move { - let stream = stream.await?; - - let response_events = Arc::new(Mutex::new(Vec::new())); - let request_clone = request.clone(); - - let wrapped_stream = stream.then(move |event| { - let response_events = response_events.clone(); - let request = request_clone.clone(); - let request_count = request_count.clone(); - let previous_diff = previous_diff.clone(); - let example_output_dir = example_output_dir.clone(); - let last_diff_file_path = last_diff_file_path.clone(); - let messages_json_file_path = messages_json_file_path.clone(); - let repository_path = repository_path.clone(); - let repository_url = repository_url.clone(); - - async move { - let event_result = match &event { - Ok(ev) => Ok(ev.clone()), - Err(err) => Err(err.to_string()), - }; - response_events.lock().unwrap().push(event_result); - - let should_execute = matches!( - &event, - Ok(LanguageModelCompletionEvent::Stop { .. }) | Err(_) - ); - - if should_execute { - let current_request_count = { - let mut count = request_count.lock().unwrap(); - *count += 1; - *count - }; - - let messages_file_path = - example_output_dir.join(format!("{current_request_count}.messages.md")); - let diff_file_path = - example_output_dir.join(format!("{current_request_count}.diff")); - let last_messages_file_path = example_output_dir.join("last.messages.md"); - - let collected_events = response_events.lock().unwrap().clone(); - let request_markdown = RequestMarkdown::new(&request); - let response_events_markdown = - response_events_to_markdown(&collected_events); - let dialog = ThreadDialog::new(&request, &collected_events); - let dialog_json = - serde_json::to_string_pretty(&dialog.to_combined_request()) - .unwrap_or_default(); - - let messages = format!( - "{}\n\n{}", - request_markdown.messages, response_events_markdown - ); - fs::write(&messages_file_path, messages.clone()) - .expect("failed to write messages file"); - fs::write(&last_messages_file_path, messages) - .expect("failed to write last messages file"); - fs::write(&messages_json_file_path, dialog_json) - .expect("failed to write last.messages.json"); - - // Get repository diff - let diff_result = - ExampleInstance::repository_diff(repository_path, &repository_url) - .await; - - match diff_result { - Ok(diff) => { - let prev_diff = previous_diff.lock().unwrap().clone(); - if diff != prev_diff { - fs::write(&diff_file_path, &diff) - .expect("failed to write diff file"); - fs::write(&last_diff_file_path, &diff) - .expect("failed to write last diff file"); - *previous_diff.lock().unwrap() = diff; - } - } - Err(err) => { - let error_message = format!("{err:?}"); - fs::write(&diff_file_path, &error_message) - .expect("failed to write diff error to file"); - fs::write(&last_diff_file_path, &error_message) - .expect("failed to write last diff file"); - } - } - - if current_request_count == 1 { - let tools_file_path = example_output_dir.join("tools.md"); - fs::write(tools_file_path, request_markdown.tools) - .expect("failed to write tools file"); - } - } - - event - } - }); - - Ok(Box::pin(wrapped_stream) - as futures::stream::BoxStream< - 'static, - Result< - LanguageModelCompletionEvent, - language_model::LanguageModelCompletionError, - >, - >) - }) - } -} - -pub fn wait_for_lang_server( - project: &Entity, - buffer: &Entity, - log_prefix: String, - cx: &mut AsyncApp, -) -> Task> { - if std::env::var("ZED_EVAL_SKIP_LS").is_ok() { - return Task::ready(Ok(())); - } - - println!("{}⏵ Waiting for language server", log_prefix); - - let (mut tx, mut rx) = mpsc::channel(1); - - let lsp_store = project - .read_with(cx, |project, _| project.lsp_store()) - .unwrap(); - - let has_lang_server = buffer - .update(cx, |buffer, cx| { - lsp_store.update(cx, |lsp_store, cx| { - lsp_store - .running_language_servers_for_local_buffer(buffer, cx) - .next() - .is_some() - }) - }) - .unwrap_or(false); - - if has_lang_server { - project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) - .unwrap() - .detach(); - } - - let subscriptions = - [ - cx.subscribe(&lsp_store, { - let log_prefix = log_prefix.clone(); - move |_, event, _| { - if let project::LspStoreEvent::LanguageServerUpdate { - message: - client::proto::update_language_server::Variant::WorkProgress( - LspWorkProgress { - message: Some(message), - .. - }, - ), - .. - } = event - { - println!("{}⟲ {message}", log_prefix) - } - } - }), - cx.subscribe(project, { - let buffer = buffer.clone(); - move |project, event, cx| match event { - project::Event::LanguageServerAdded(_, _, _) => { - let buffer = buffer.clone(); - project - .update(cx, |project, cx| project.save_buffer(buffer, cx)) - .detach(); - } - project::Event::DiskBasedDiagnosticsFinished { .. } => { - tx.try_send(()).ok(); - } - _ => {} - } - }), - ]; - - cx.spawn(async move |cx| { - let timeout = cx.background_executor().timer(Duration::new(60 * 5, 0)); - let result = futures::select! { - _ = rx.next() => { - println!("{}⚑ Language server idle", log_prefix); - anyhow::Ok(()) - }, - _ = timeout.fuse() => { - anyhow::bail!("LSP wait timed out after 5 minutes"); - } - }; - drop(subscriptions); - result - }) -} - -pub async fn query_lsp_diagnostics( - project: Entity, - cx: &mut AsyncApp, -) -> Result> { - let paths_with_diagnostics = project.update(cx, |project, cx| { - project - .diagnostic_summaries(true, cx) - .filter(|(_, _, summary)| summary.error_count > 0 || summary.warning_count > 0) - .map(|(project_path, _, _)| project_path) - .collect::>() - })?; - - if paths_with_diagnostics.is_empty() { - return Ok(None); - } - - let mut output = String::new(); - for project_path in paths_with_diagnostics { - let buffer = project - .update(cx, |project, cx| project.open_buffer(project_path, cx))? - .await?; - let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?; - - for (_, group) in snapshot.diagnostic_groups(None) { - let entry = &group.entries[group.primary_ix]; - let range = entry.range.to_point(&snapshot); - let severity = match entry.diagnostic.severity { - DiagnosticSeverity::ERROR => "error", - DiagnosticSeverity::WARNING => "warning", - _ => continue, - }; - - writeln!( - output, - "{} at line {}: {}", - severity, - range.start.row + 1, - entry.diagnostic.message - )?; - } - } - anyhow::Ok(Some(output)) -} - -fn parse_assertion_result(response: &str) -> Result { - let analysis = get_tag("analysis", response)?; - let passed = match get_tag("passed", response)?.to_lowercase().as_str() { - "true" => true, - "false" => false, - value @ _ => bail!("invalid judge `passed` tag: {value}"), - }; - Ok(RanAssertionResult { - analysis: Some(analysis), - passed, - }) -} - -fn get_tag(name: &'static str, response: &str) -> Result { - let start_tag = format!("<{}>", name); - let end_tag = format!("", name); - - let start_ix = response - .find(&start_tag) - .context(format!("{} start tag not found", name))?; - let content_start_ix = start_ix + start_tag.len(); - - let end_ix = content_start_ix - + response[content_start_ix..] - .find(&end_tag) - .context(format!("{} end tag not found", name))?; - - let content = response[content_start_ix..end_ix].trim().unindent(); - - anyhow::Ok(content) -} - -pub fn repo_path_for_url(repos_dir: &Path, repo_url: &str) -> PathBuf { - let repo_name = repo_url - .trim_start_matches("https://") - .replace(|c: char| !c.is_alphanumeric(), "-"); - Path::new(repos_dir).join(repo_name) -} - -pub async fn run_git(repo_path: &Path, args: &[&str]) -> Result { - let output = new_smol_command("git") - .current_dir(repo_path) - .args(args) - .output() - .await?; - - anyhow::ensure!( - output.status.success(), - "`git {}` within `{}` failed with status: {}\nstderr:\n{}\nstdout:\n{}", - args.join(" "), - repo_path.display(), - output.status, - String::from_utf8_lossy(&output.stderr), - String::from_utf8_lossy(&output.stdout), - ); - Ok(String::from_utf8(output.stdout)?.trim().to_string()) -} - -fn push_role(role: &Role, buf: &mut String, assistant_message_number: &mut u32) { - match role { - Role::System => buf.push_str("# ⚙️ SYSTEM\n\n"), - Role::User => buf.push_str("# 👤 USER\n\n"), - Role::Assistant => { - buf.push_str(&format!("# 🤖 ASSISTANT {assistant_message_number}\n\n")); - *assistant_message_number = *assistant_message_number + 1; - } - } -} - -pub async fn send_language_model_request( - model: Arc, - request: LanguageModelRequest, - cx: &AsyncApp, -) -> anyhow::Result { - match model.stream_completion_text(request, cx).await { - Ok(mut stream) => { - let mut full_response = String::new(); - while let Some(chunk_result) = stream.stream.next().await { - match chunk_result { - Ok(chunk_str) => { - full_response.push_str(&chunk_str); - } - Err(err) => { - anyhow::bail!("Error receiving response from language model: {err}"); - } - } - } - Ok(full_response) - } - Err(err) => Err(anyhow!( - "Failed to get response from language model. Error was: {err}" - )), - } -} - -pub struct RequestMarkdown { - pub tools: String, - pub messages: String, -} - -impl RequestMarkdown { - pub fn new(request: &LanguageModelRequest) -> Self { - let mut tools = String::new(); - let mut messages = String::new(); - let mut assistant_message_number: u32 = 1; - - // Print the tools - if !request.tools.is_empty() { - for tool in &request.tools { - write!(&mut tools, "# {}\n\n", tool.name).unwrap(); - write!(&mut tools, "{}\n\n", tool.description).unwrap(); - writeln!( - &mut tools, - "{}", - MarkdownCodeBlock { - tag: "json", - text: &format!("{:#}", tool.input_schema) - } - ) - .unwrap(); - } - } - - // Print the messages - for message in &request.messages { - push_role(&message.role, &mut messages, &mut assistant_message_number); - - for content in &message.content { - match content { - MessageContent::Text(text) => { - messages.push_str(text); - messages.push_str("\n\n"); - } - MessageContent::Image(_) => { - messages.push_str("[IMAGE DATA]\n\n"); - } - MessageContent::Thinking { text, signature } => { - messages.push_str("**Thinking**:\n\n"); - if let Some(sig) = signature { - messages.push_str(&format!("Signature: {}\n\n", sig)); - } - messages.push_str(text); - messages.push_str("\n"); - } - MessageContent::RedactedThinking(items) => { - messages.push_str(&format!( - "**Redacted Thinking**: {} item(s)\n\n", - items.len() - )); - } - MessageContent::ToolUse(tool_use) => { - messages.push_str(&format!( - "**Tool Use**: {} (ID: {})\n", - tool_use.name, tool_use.id - )); - messages.push_str(&format!( - "{}\n", - MarkdownCodeBlock { - tag: "json", - text: &format!("{:#}", tool_use.input) - } - )); - } - MessageContent::ToolResult(tool_result) => { - messages.push_str(&format!( - "**Tool Result**: {} (ID: {})\n\n", - tool_result.tool_name, tool_result.tool_use_id - )); - if tool_result.is_error { - messages.push_str("**ERROR:**\n"); - } - - match &tool_result.content { - LanguageModelToolResultContent::Text(text) => { - writeln!(messages, "{text}\n").ok(); - } - LanguageModelToolResultContent::Image(image) => { - writeln!(messages, "![Image](data:base64,{})\n", image.source).ok(); - } - } - - if let Some(output) = tool_result.output.as_ref() { - writeln!( - messages, - "**Debug Output**:\n\n```json\n{}\n```\n", - serde_json::to_string_pretty(output).unwrap() - ) - .unwrap(); - } - } - } - } - } - - Self { tools, messages } - } -} - -pub fn response_events_to_markdown( - response_events: &[std::result::Result], -) -> String { - let mut response = String::new(); - // Print the response events if any - response.push_str("# Response\n\n"); - let mut text_buffer = String::new(); - let mut thinking_buffer = String::new(); - - let flush_buffers = - |output: &mut String, text_buffer: &mut String, thinking_buffer: &mut String| { - if !text_buffer.is_empty() { - output.push_str(&format!("**Text**:\n{}\n\n", text_buffer)); - text_buffer.clear(); - } - if !thinking_buffer.is_empty() { - output.push_str(&format!("**Thinking**:\n{}\n\n", thinking_buffer)); - thinking_buffer.clear(); - } - }; - - for event in response_events { - match event { - Ok(LanguageModelCompletionEvent::Text(text)) => { - text_buffer.push_str(text); - } - Ok(LanguageModelCompletionEvent::Thinking { text, .. }) => { - thinking_buffer.push_str(text); - } - Ok(LanguageModelCompletionEvent::RedactedThinking { .. }) => {} - Ok(LanguageModelCompletionEvent::Stop(reason)) => { - flush_buffers(&mut response, &mut text_buffer, &mut thinking_buffer); - response.push_str(&format!("**Stop**: {:?}\n\n", reason)); - } - Ok(LanguageModelCompletionEvent::ToolUse(tool_use)) => { - flush_buffers(&mut response, &mut text_buffer, &mut thinking_buffer); - response.push_str(&format!( - "**Tool Use**: {} (ID: {})\n", - tool_use.name, tool_use.id - )); - response.push_str(&format!( - "{}\n", - MarkdownCodeBlock { - tag: "json", - text: &format!("{:#}", tool_use.input) - } - )); - } - Ok( - LanguageModelCompletionEvent::UsageUpdate(_) - | LanguageModelCompletionEvent::ToolUseLimitReached - | LanguageModelCompletionEvent::StartMessage { .. } - | LanguageModelCompletionEvent::UsageUpdated { .. } - | LanguageModelCompletionEvent::Queued { .. } - | LanguageModelCompletionEvent::Started - | LanguageModelCompletionEvent::ReasoningDetails(_), - ) => {} - Ok(LanguageModelCompletionEvent::ToolUseJsonParseError { - json_parse_error, .. - }) => { - flush_buffers(&mut response, &mut text_buffer, &mut thinking_buffer); - response.push_str(&format!( - "**Error**: parse error in tool use JSON: {}\n\n", - json_parse_error - )); - } - Err(error) => { - flush_buffers(&mut response, &mut text_buffer, &mut thinking_buffer); - response.push_str(&format!("**Error**: {}\n\n", error)); - } - } - } - - flush_buffers(&mut response, &mut text_buffer, &mut thinking_buffer); - - response -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct ThreadDialog { - pub request: LanguageModelRequest, - pub response_events: Vec>, -} - -impl ThreadDialog { - pub fn new( - request: &LanguageModelRequest, - response_events: &[std::result::Result], - ) -> Self { - Self { - request: request.clone(), - response_events: response_events.to_vec(), - } - } - - /// Represents all request and response messages in a unified format. - /// - /// Specifically, it appends the assistant's response (derived from response events) - /// as a new message to existing messages in the request. - pub fn to_combined_request(&self) -> LanguageModelRequest { - let mut request = self.request.clone(); - if let Some(assistant_message) = self.response_events_to_message() { - request.messages.push(assistant_message); - } - request - } - fn response_events_to_message(&self) -> Option { - let response_events = &self.response_events; - let mut content: Vec = Vec::new(); - let mut current_text = String::new(); - - let flush_text = |text: &mut String, content: &mut Vec| { - if !text.is_empty() { - content.push(MessageContent::Text(std::mem::take(text))); - } - }; - - for event in response_events { - match event { - Ok(LanguageModelCompletionEvent::Text(text)) => { - current_text.push_str(text); - } - - Ok(LanguageModelCompletionEvent::ToolUse(tool_use)) => { - flush_text(&mut current_text, &mut content); - if tool_use.is_input_complete { - content.push(MessageContent::ToolUse(tool_use.clone())); - } - } - Ok(LanguageModelCompletionEvent::Thinking { text, signature }) => { - flush_text(&mut current_text, &mut content); - content.push(MessageContent::Thinking { - text: text.clone(), - signature: signature.clone(), - }); - } - - // Skip these - Ok(LanguageModelCompletionEvent::UsageUpdate(_)) - | Ok(LanguageModelCompletionEvent::RedactedThinking { .. }) - | Ok(LanguageModelCompletionEvent::StartMessage { .. }) - | Ok(LanguageModelCompletionEvent::ReasoningDetails(_)) - | Ok(LanguageModelCompletionEvent::Stop(_)) - | Ok(LanguageModelCompletionEvent::Queued { .. }) - | Ok(LanguageModelCompletionEvent::Started) - | Ok(LanguageModelCompletionEvent::UsageUpdated { .. }) - | Ok(LanguageModelCompletionEvent::ToolUseLimitReached) => {} - - Ok(LanguageModelCompletionEvent::ToolUseJsonParseError { - json_parse_error, - .. - }) => { - flush_text(&mut current_text, &mut content); - content.push(MessageContent::Text(format!( - "ERROR: parse error in tool use JSON: {}", - json_parse_error - ))); - } - - Err(error) => { - flush_text(&mut current_text, &mut content); - content.push(MessageContent::Text(format!("ERROR: {}", error))); - } - } - } - - flush_text(&mut current_text, &mut content); - - if !content.is_empty() { - Some(LanguageModelRequestMessage { - role: Role::Assistant, - content, - cache: false, - reasoning_details: None, - }) - } else { - None - } - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_parse_judge_output() { - let response = r#" - The model did a good job but there were still compilations errors. - true - "# - .unindent(); - - let output = parse_assertion_result(&response).unwrap(); - assert_eq!( - output.analysis, - Some("The model did a good job but there were still compilations errors.".into()) - ); - assert!(output.passed); - - let response = r#" - Text around ignored - - - Failed to compile: - - Error 1 - - Error 2 - - - false - "# - .unindent(); - - let output = parse_assertion_result(&response).unwrap(); - assert_eq!( - output.analysis, - Some("Failed to compile:\n- Error 1\n- Error 2".into()) - ); - assert!(!output.passed); - } -} diff --git a/crates/eval/src/judge_diff_prompt.hbs b/crates/eval/src/judge_diff_prompt.hbs deleted file mode 100644 index 24ef9ac97e..0000000000 --- a/crates/eval/src/judge_diff_prompt.hbs +++ /dev/null @@ -1,25 +0,0 @@ -You are an expert software developer. Your task is to evaluate a diff produced by an AI agent -in response to a prompt. Here is the prompt and the diff: - - -{{{prompt}}} - - - -{{{repository_diff}}} - - -Evaluate whether or not the diff passes the following assertion: - - -{{assertion}} - - -Analyze the diff hunk by hunk, and structure your answer in the following XML format: - -``` -{YOUR ANALYSIS HERE} -{PASSED_ASSERTION} -``` - -Where `PASSED_ASSERTION` is either `true` or `false`. diff --git a/crates/eval/src/judge_thread_prompt.hbs b/crates/eval/src/judge_thread_prompt.hbs deleted file mode 100644 index e80bafcce1..0000000000 --- a/crates/eval/src/judge_thread_prompt.hbs +++ /dev/null @@ -1,21 +0,0 @@ -You are an expert software developer. -Your task is to evaluate an AI agent's messages and tool calls in this conversation: - - -{{{messages}}} - - -Evaluate whether or not the sequence of messages passes the following assertion: - - -{{{assertion}}} - - -Analyze the messages one by one, and structure your answer in the following XML format: - -``` -{YOUR ANALYSIS HERE} -{PASSED_ASSERTION} -``` - -Where `PASSED_ASSERTION` is either `true` or `false`. diff --git a/crates/eval/src/tool_metrics.rs b/crates/eval/src/tool_metrics.rs deleted file mode 100644 index 63d8a4f2bc..0000000000 --- a/crates/eval/src/tool_metrics.rs +++ /dev/null @@ -1,106 +0,0 @@ -use collections::HashMap; -use serde::{Deserialize, Serialize}; -use std::{fmt::Display, sync::Arc}; - -#[derive(Debug, Default, Clone, Serialize, Deserialize)] -pub struct ToolMetrics { - pub use_counts: HashMap, u32>, - pub failure_counts: HashMap, u32>, -} - -impl ToolMetrics { - pub fn insert(&mut self, tool_name: Arc, succeeded: bool) { - *self.use_counts.entry(tool_name.clone()).or_insert(0) += 1; - if !succeeded { - *self.failure_counts.entry(tool_name).or_insert(0) += 1; - } - } - - pub fn merge(&mut self, other: &ToolMetrics) { - for (tool_name, use_count) in &other.use_counts { - *self.use_counts.entry(tool_name.clone()).or_insert(0) += use_count; - } - for (tool_name, failure_count) in &other.failure_counts { - *self.failure_counts.entry(tool_name.clone()).or_insert(0) += failure_count; - } - } - - pub fn is_empty(&self) -> bool { - self.use_counts.is_empty() && self.failure_counts.is_empty() - } -} - -impl Display for ToolMetrics { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let mut failure_rates: Vec<(Arc, f64)> = Vec::new(); - - for (tool_name, use_count) in &self.use_counts { - let failure_count = self.failure_counts.get(tool_name).cloned().unwrap_or(0); - if *use_count > 0 { - let failure_rate = failure_count as f64 / *use_count as f64; - failure_rates.push((tool_name.clone(), failure_rate)); - } - } - - // Sort by failure rate descending - failure_rates.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal)); - - // Table dimensions - let tool_width = 30; - let count_width = 10; - let rate_width = 10; - - // Write table top border - writeln!( - f, - "┌{}┬{}┬{}┬{}┐", - "─".repeat(tool_width), - "─".repeat(count_width), - "─".repeat(count_width), - "─".repeat(rate_width) - )?; - - // Write header row - writeln!( - f, - "│{:^30}│{:^10}│{:^10}│{:^10}│", - "Tool", "Uses", "Failures", "Rate" - )?; - - // Write header-data separator - writeln!( - f, - "├{}┼{}┼{}┼{}┤", - "─".repeat(tool_width), - "─".repeat(count_width), - "─".repeat(count_width), - "─".repeat(rate_width) - )?; - - // Write data rows - for (tool_name, failure_rate) in failure_rates { - let use_count = self.use_counts.get(&tool_name).cloned().unwrap_or(0); - let failure_count = self.failure_counts.get(&tool_name).cloned().unwrap_or(0); - writeln!( - f, - "│{:<30}│{:^10}│{:^10}│{:^10}│", - tool_name, - use_count, - failure_count, - format!("{}%", (failure_rate * 100.0).round()) - )?; - } - - // Write table bottom border - writeln!( - f, - "└{}┴{}┴{}┴{}┘", - "─".repeat(tool_width), - "─".repeat(count_width), - "─".repeat(count_width), - "─".repeat(rate_width) - )?; - - Ok(()) - } -} diff --git a/crates/eval_utils/Cargo.toml b/crates/eval_utils/Cargo.toml deleted file mode 100644 index a512035f5d..0000000000 --- a/crates/eval_utils/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "eval_utils" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/eval_utils.rs" -doctest = false - -[dependencies] -gpui.workspace = true -serde.workspace = true -smol.workspace = true diff --git a/crates/eval_utils/LICENSE-GPL b/crates/eval_utils/LICENSE-GPL deleted file mode 120000 index e0f9dbd5d6..0000000000 --- a/crates/eval_utils/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -LICENSE-GPL \ No newline at end of file diff --git a/crates/eval_utils/README.md b/crates/eval_utils/README.md deleted file mode 100644 index 617077a815..0000000000 --- a/crates/eval_utils/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# eval_utils - -Utilities for evals of agents. diff --git a/crates/eval_utils/src/eval_utils.rs b/crates/eval_utils/src/eval_utils.rs deleted file mode 100644 index 880b1a97e4..0000000000 --- a/crates/eval_utils/src/eval_utils.rs +++ /dev/null @@ -1,128 +0,0 @@ -//! Utilities for evaluation and benchmarking. - -use std::{ - collections::HashMap, - sync::{Arc, mpsc}, -}; - -fn report_progress(evaluated_count: usize, failed_count: usize, iterations: usize) { - let passed_count = evaluated_count - failed_count; - let passed_ratio = if evaluated_count == 0 { - 0.0 - } else { - passed_count as f64 / evaluated_count as f64 - }; - println!( - "\r\x1b[KEvaluated {}/{} ({:.2}% passed)", - evaluated_count, - iterations, - passed_ratio * 100.0 - ) -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum OutcomeKind { - Passed, - Failed, - Error, -} - -pub trait EvalOutputProcessor { - type Metadata: 'static + Send; - fn process(&mut self, output: &EvalOutput); - fn assert(&mut self); -} - -#[derive(Clone, Debug)] -pub struct EvalOutput { - pub outcome: OutcomeKind, - pub data: String, - pub metadata: M, -} - -pub struct NoProcessor; -impl EvalOutputProcessor for NoProcessor { - type Metadata = (); - - fn process(&mut self, _output: &EvalOutput) {} - - fn assert(&mut self) {} -} - -pub fn eval

( - iterations: usize, - expected_pass_ratio: f32, - mut processor: P, - evalf: impl Fn() -> EvalOutput + Send + Sync + 'static, -) where - P: EvalOutputProcessor, -{ - let mut evaluated_count = 0; - let mut failed_count = 0; - let evalf = Arc::new(evalf); - report_progress(evaluated_count, failed_count, iterations); - - let (tx, rx) = mpsc::channel(); - - let executor = gpui::background_executor(); - let semaphore = Arc::new(smol::lock::Semaphore::new(32)); - let evalf = Arc::new(evalf); - // Warm the cache once - let first_output = evalf(); - tx.send(first_output).ok(); - - for _ in 1..iterations { - let tx = tx.clone(); - let semaphore = semaphore.clone(); - let evalf = evalf.clone(); - executor - .spawn(async move { - let _guard = semaphore.acquire().await; - let output = evalf(); - tx.send(output).ok(); - }) - .detach(); - } - drop(tx); - - let mut failed_evals = Vec::new(); - let mut errored_evals = HashMap::new(); - while let Ok(output) = rx.recv() { - processor.process(&output); - - match output.outcome { - OutcomeKind::Passed => {} - OutcomeKind::Failed => { - failed_count += 1; - failed_evals.push(output); - } - OutcomeKind::Error => { - failed_count += 1; - *errored_evals.entry(output.data).or_insert(0) += 1; - } - } - - evaluated_count += 1; - report_progress(evaluated_count, failed_count, iterations); - } - - let actual_pass_ratio = (iterations - failed_count) as f32 / iterations as f32; - println!("Actual pass ratio: {}\n", actual_pass_ratio); - if actual_pass_ratio < expected_pass_ratio { - for (error, count) in errored_evals { - println!("Eval errored {} times. Error: {}", count, error); - } - - for failed in failed_evals { - println!("Eval failed"); - println!("{}", failed.data); - } - - panic!( - "Actual pass ratio: {}\nExpected pass ratio: {}", - actual_pass_ratio, expected_pass_ratio - ); - } - - processor.assert(); -} diff --git a/crates/explorer_command_injector/AppxManifest-Nightly.xml b/crates/explorer_command_injector/AppxManifest-Nightly.xml deleted file mode 100644 index 32c33d2b0c..0000000000 --- a/crates/explorer_command_injector/AppxManifest-Nightly.xml +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - Zed Nightly - Zed Industries - - resources\logo_150x150.png - true - disabled - disabled - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/crates/explorer_command_injector/AppxManifest-Preview.xml b/crates/explorer_command_injector/AppxManifest-Preview.xml deleted file mode 100644 index 2904653fb2..0000000000 --- a/crates/explorer_command_injector/AppxManifest-Preview.xml +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - Zed Preview - Zed Industries - - resources\logo_150x150.png - true - disabled - disabled - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/crates/explorer_command_injector/AppxManifest.xml b/crates/explorer_command_injector/AppxManifest.xml deleted file mode 100644 index adf563fbc0..0000000000 --- a/crates/explorer_command_injector/AppxManifest.xml +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - Zed - - Zed Industries - - resources\logo_150x150.png - true - disabled - disabled - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/crates/explorer_command_injector/Cargo.toml b/crates/explorer_command_injector/Cargo.toml deleted file mode 100644 index 8530329358..0000000000 --- a/crates/explorer_command_injector/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "explorer_command_injector" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -crate-type = ["cdylib"] -path = "src/explorer_command_injector.rs" -doctest = false - -[features] -default = ["nightly"] -stable = [] -preview = [] -nightly = [] - -[target.'cfg(target_os = "windows")'.dependencies] -windows.workspace = true -windows-core.workspace = true -windows-registry = "0.5" - -[dependencies] diff --git a/crates/explorer_command_injector/LICENSE-GPL b/crates/explorer_command_injector/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/explorer_command_injector/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/explorer_command_injector/src/explorer_command_injector.rs b/crates/explorer_command_injector/src/explorer_command_injector.rs deleted file mode 100644 index bfa2a0326c..0000000000 --- a/crates/explorer_command_injector/src/explorer_command_injector.rs +++ /dev/null @@ -1,202 +0,0 @@ -#![cfg(target_os = "windows")] - -use std::{os::windows::ffi::OsStringExt, path::PathBuf}; - -use windows::{ - Win32::{ - Foundation::{ - CLASS_E_CLASSNOTAVAILABLE, E_FAIL, E_INVALIDARG, E_NOTIMPL, ERROR_INSUFFICIENT_BUFFER, - GetLastError, HINSTANCE, MAX_PATH, - }, - Globalization::u_strlen, - System::{ - Com::{IBindCtx, IClassFactory, IClassFactory_Impl}, - LibraryLoader::GetModuleFileNameW, - SystemServices::DLL_PROCESS_ATTACH, - }, - UI::Shell::{ - ECF_DEFAULT, ECS_ENABLED, IEnumExplorerCommand, IExplorerCommand, - IExplorerCommand_Impl, IShellItemArray, SHStrDupW, SIGDN_FILESYSPATH, - }, - }, - core::{BOOL, GUID, HRESULT, HSTRING, Interface, Ref, Result, implement}, -}; - -static mut DLL_INSTANCE: HINSTANCE = HINSTANCE(std::ptr::null_mut()); - -#[unsafe(no_mangle)] -extern "system" fn DllMain( - hinstdll: HINSTANCE, - fdwreason: u32, - _lpvreserved: *mut core::ffi::c_void, -) -> bool { - if fdwreason == DLL_PROCESS_ATTACH { - unsafe { DLL_INSTANCE = hinstdll }; - } - - true -} - -#[implement(IExplorerCommand)] -struct ExplorerCommandInjector; - -#[allow(non_snake_case)] -impl IExplorerCommand_Impl for ExplorerCommandInjector_Impl { - fn GetTitle(&self, _: Ref) -> Result { - let command_description = - retrieve_command_description().unwrap_or(HSTRING::from("Open with Zed")); - unsafe { SHStrDupW(&command_description) } - } - - fn GetIcon(&self, _: Ref) -> Result { - let Some(zed_exe) = get_zed_exe_path() else { - return Err(E_FAIL.into()); - }; - unsafe { SHStrDupW(&HSTRING::from(zed_exe)) } - } - - fn GetToolTip(&self, _: Ref) -> Result { - Err(E_NOTIMPL.into()) - } - - fn GetCanonicalName(&self) -> Result { - Ok(GUID::zeroed()) - } - - fn GetState(&self, _: Ref, _: BOOL) -> Result { - Ok(ECS_ENABLED.0 as _) - } - - fn Invoke(&self, psiitemarray: Ref, _: Ref) -> Result<()> { - let items = psiitemarray.ok()?; - let Some(zed_exe) = get_zed_exe_path() else { - return Ok(()); - }; - - let count = unsafe { items.GetCount()? }; - for idx in 0..count { - let item = unsafe { items.GetItemAt(idx)? }; - let item_path = unsafe { item.GetDisplayName(SIGDN_FILESYSPATH)?.to_string()? }; - #[allow(clippy::disallowed_methods, reason = "no async context in sight..")] - std::process::Command::new(&zed_exe) - .arg(&item_path) - .spawn() - .map_err(|_| E_INVALIDARG)?; - } - - Ok(()) - } - - fn GetFlags(&self) -> Result { - Ok(ECF_DEFAULT.0 as _) - } - - fn EnumSubCommands(&self) -> Result { - Err(E_NOTIMPL.into()) - } -} - -#[implement(IClassFactory)] -struct ExplorerCommandInjectorFactory; - -impl IClassFactory_Impl for ExplorerCommandInjectorFactory_Impl { - fn CreateInstance( - &self, - punkouter: Ref, - riid: *const windows_core::GUID, - ppvobject: *mut *mut core::ffi::c_void, - ) -> Result<()> { - unsafe { - *ppvobject = std::ptr::null_mut(); - } - if punkouter.is_none() { - let factory: IExplorerCommand = ExplorerCommandInjector {}.into(); - let ret = unsafe { factory.query(riid, ppvobject).ok() }; - if ret.is_ok() { - unsafe { - *ppvobject = factory.into_raw(); - } - } - ret - } else { - Err(E_INVALIDARG.into()) - } - } - - fn LockServer(&self, _: BOOL) -> Result<()> { - Ok(()) - } -} - -#[cfg(all(feature = "stable", not(feature = "preview"), not(feature = "nightly")))] -const MODULE_ID: GUID = GUID::from_u128(0x6a1f6b13_3b82_48a1_9e06_7bb0a6d0bffd); -#[cfg(all(feature = "preview", not(feature = "stable"), not(feature = "nightly")))] -const MODULE_ID: GUID = GUID::from_u128(0xaf8e85ea_fb20_4db2_93cf_56513c1ec697); -#[cfg(all(feature = "nightly", not(feature = "stable"), not(feature = "preview")))] -const MODULE_ID: GUID = GUID::from_u128(0x266f2cfe_1653_42af_b55c_fe3590c83871); - -// Make cargo clippy happy -#[cfg(all(feature = "nightly", feature = "stable", feature = "preview"))] -const MODULE_ID: GUID = GUID::from_u128(0x685f4d49_6718_4c55_b271_ebb5c6a48d6f); - -#[unsafe(no_mangle)] -extern "system" fn DllGetClassObject( - class_id: *const GUID, - iid: *const GUID, - out: *mut *mut std::ffi::c_void, -) -> HRESULT { - unsafe { - *out = std::ptr::null_mut(); - } - let class_id = unsafe { *class_id }; - if class_id == MODULE_ID { - let instance: IClassFactory = ExplorerCommandInjectorFactory {}.into(); - let ret = unsafe { instance.query(iid, out) }; - if ret.is_ok() { - unsafe { - *out = instance.into_raw(); - } - } - ret - } else { - CLASS_E_CLASSNOTAVAILABLE - } -} - -fn get_zed_install_folder() -> Option { - let mut buf = vec![0u16; MAX_PATH as usize]; - unsafe { GetModuleFileNameW(Some(DLL_INSTANCE.into()), &mut buf) }; - - while unsafe { GetLastError() } == ERROR_INSUFFICIENT_BUFFER { - buf = vec![0u16; buf.len() * 2]; - unsafe { GetModuleFileNameW(Some(DLL_INSTANCE.into()), &mut buf) }; - } - let len = unsafe { u_strlen(buf.as_ptr()) }; - let path: PathBuf = std::ffi::OsString::from_wide(&buf[..len as usize]) - .into_string() - .ok()? - .into(); - Some(path.parent()?.parent()?.to_path_buf()) -} - -#[inline] -fn get_zed_exe_path() -> Option { - get_zed_install_folder().map(|path| path.join("Zed.exe").to_string_lossy().into_owned()) -} - -#[inline] -fn retrieve_command_description() -> Result { - #[cfg(all(feature = "stable", not(feature = "preview"), not(feature = "nightly")))] - const REG_PATH: &str = "Software\\Classes\\ZedEditorContextMenu"; - #[cfg(all(feature = "preview", not(feature = "stable"), not(feature = "nightly")))] - const REG_PATH: &str = "Software\\Classes\\ZedEditorPreviewContextMenu"; - #[cfg(all(feature = "nightly", not(feature = "stable"), not(feature = "preview")))] - const REG_PATH: &str = "Software\\Classes\\ZedEditorNightlyContextMenu"; - - // Make cargo clippy happy - #[cfg(all(feature = "nightly", feature = "stable", feature = "preview"))] - const REG_PATH: &str = "Software\\Classes\\ZedEditorClippyContextMenu"; - - let key = windows_registry::CURRENT_USER.open(REG_PATH)?; - key.get_hstring("Title") -} diff --git a/crates/extension/Cargo.toml b/crates/extension/Cargo.toml deleted file mode 100644 index 307a3a19bd..0000000000 --- a/crates/extension/Cargo.toml +++ /dev/null @@ -1,44 +0,0 @@ -[package] -name = "extension" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/extension.rs" - -[dependencies] -anyhow.workspace = true -async-trait.workspace = true -collections.workspace = true -dap.workspace = true -fs.workspace = true -futures.workspace = true -gpui.workspace = true -heck.workspace = true -http_client.workspace = true -language.workspace = true -log.workspace = true -lsp.workspace = true -parking_lot.workspace = true -proto.workspace = true -semver.workspace = true -serde.workspace = true -serde_json.workspace = true -task.workspace = true -toml.workspace = true -url.workspace = true -util.workspace = true -wasm-encoder.workspace = true -wasmparser.workspace = true - -[dev-dependencies] -fs = { workspace = true, "features" = ["test-support"] } -gpui = { workspace = true, "features" = ["test-support"] } -indoc.workspace = true -pretty_assertions.workspace = true -tempfile.workspace = true diff --git a/crates/extension/LICENSE-GPL b/crates/extension/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/extension/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/extension/src/capabilities.rs b/crates/extension/src/capabilities.rs deleted file mode 100644 index b8afc4ec06..0000000000 --- a/crates/extension/src/capabilities.rs +++ /dev/null @@ -1,20 +0,0 @@ -mod download_file_capability; -mod npm_install_package_capability; -mod process_exec_capability; - -pub use download_file_capability::*; -pub use npm_install_package_capability::*; -pub use process_exec_capability::*; - -use serde::{Deserialize, Serialize}; - -/// A capability for an extension. -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum ExtensionCapability { - #[serde(rename = "process:exec")] - ProcessExec(ProcessExecCapability), - DownloadFile(DownloadFileCapability), - #[serde(rename = "npm:install")] - NpmInstallPackage(NpmInstallPackageCapability), -} diff --git a/crates/extension/src/capabilities/download_file_capability.rs b/crates/extension/src/capabilities/download_file_capability.rs deleted file mode 100644 index a76755b593..0000000000 --- a/crates/extension/src/capabilities/download_file_capability.rs +++ /dev/null @@ -1,121 +0,0 @@ -use serde::{Deserialize, Serialize}; -use url::Url; - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub struct DownloadFileCapability { - pub host: String, - pub path: Vec, -} - -impl DownloadFileCapability { - /// Returns whether the capability allows downloading a file from the given URL. - pub fn allows(&self, url: &Url) -> bool { - let Some(desired_host) = url.host_str() else { - return false; - }; - - let Some(desired_path) = url.path_segments() else { - return false; - }; - let desired_path = desired_path.collect::>(); - - if self.host != desired_host && self.host != "*" { - return false; - } - - for (ix, path_segment) in self.path.iter().enumerate() { - if path_segment == "**" { - return true; - } - - if ix >= desired_path.len() { - return false; - } - - if path_segment != "*" && path_segment != desired_path[ix] { - return false; - } - } - - if self.path.len() < desired_path.len() { - return false; - } - - true - } -} - -#[cfg(test)] -mod tests { - use pretty_assertions::assert_eq; - - use super::*; - - #[test] - fn test_allows() { - let capability = DownloadFileCapability { - host: "*".to_string(), - path: vec!["**".to_string()], - }; - assert_eq!( - capability.allows(&"https://example.com/some/path".parse().unwrap()), - true - ); - - let capability = DownloadFileCapability { - host: "github.com".to_string(), - path: vec!["**".to_string()], - }; - assert_eq!( - capability.allows(&"https://github.com/some-owner/some-repo".parse().unwrap()), - true - ); - assert_eq!( - capability.allows( - &"https://fake-github.com/some-owner/some-repo" - .parse() - .unwrap() - ), - false - ); - - let capability = DownloadFileCapability { - host: "github.com".to_string(), - path: vec!["specific-owner".to_string(), "*".to_string()], - }; - assert_eq!( - capability.allows(&"https://github.com/some-owner/some-repo".parse().unwrap()), - false - ); - assert_eq!( - capability.allows( - &"https://github.com/specific-owner/some-repo" - .parse() - .unwrap() - ), - true - ); - - let capability = DownloadFileCapability { - host: "github.com".to_string(), - path: vec!["specific-owner".to_string(), "*".to_string()], - }; - assert_eq!( - capability.allows( - &"https://github.com/some-owner/some-repo/extra" - .parse() - .unwrap() - ), - false - ); - assert_eq!( - capability.allows( - &"https://github.com/specific-owner/some-repo/extra" - .parse() - .unwrap() - ), - false - ); - } -} diff --git a/crates/extension/src/capabilities/npm_install_package_capability.rs b/crates/extension/src/capabilities/npm_install_package_capability.rs deleted file mode 100644 index 287645fc75..0000000000 --- a/crates/extension/src/capabilities/npm_install_package_capability.rs +++ /dev/null @@ -1,39 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub struct NpmInstallPackageCapability { - pub package: String, -} - -impl NpmInstallPackageCapability { - /// Returns whether the capability allows installing the given NPM package. - pub fn allows(&self, package: &str) -> bool { - self.package == "*" || self.package == package - } -} - -#[cfg(test)] -mod tests { - use pretty_assertions::assert_eq; - - use super::*; - - #[test] - fn test_allows() { - let capability = NpmInstallPackageCapability { - package: "*".to_string(), - }; - assert_eq!(capability.allows("package"), true); - - let capability = NpmInstallPackageCapability { - package: "react".to_string(), - }; - assert_eq!(capability.allows("react"), true); - - let capability = NpmInstallPackageCapability { - package: "react".to_string(), - }; - assert_eq!(capability.allows("malicious-package"), false); - } -} diff --git a/crates/extension/src/capabilities/process_exec_capability.rs b/crates/extension/src/capabilities/process_exec_capability.rs deleted file mode 100644 index 053a7b212b..0000000000 --- a/crates/extension/src/capabilities/process_exec_capability.rs +++ /dev/null @@ -1,116 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub struct ProcessExecCapability { - /// The command to execute. - pub command: String, - /// The arguments to pass to the command. Use `*` for a single wildcard argument. - /// If the last element is `**`, then any trailing arguments are allowed. - pub args: Vec, -} - -impl ProcessExecCapability { - /// Returns whether the capability allows the given command and arguments. - pub fn allows( - &self, - desired_command: &str, - desired_args: &[impl AsRef + std::fmt::Debug], - ) -> bool { - if self.command != desired_command && self.command != "*" { - return false; - } - - for (ix, arg) in self.args.iter().enumerate() { - if arg == "**" { - return true; - } - - if ix >= desired_args.len() { - return false; - } - - if arg != "*" && arg != desired_args[ix].as_ref() { - return false; - } - } - - if self.args.len() < desired_args.len() { - return false; - } - - true - } -} - -#[cfg(test)] -mod tests { - use pretty_assertions::assert_eq; - - use super::*; - - #[test] - fn test_allows_with_exact_match() { - let capability = ProcessExecCapability { - command: "ls".to_string(), - args: vec!["-la".to_string()], - }; - - assert_eq!(capability.allows("ls", &["-la"]), true); - assert_eq!(capability.allows("ls", &["-l"]), false); - assert_eq!(capability.allows("pwd", &[] as &[&str]), false); - } - - #[test] - fn test_allows_with_wildcard_arg() { - let capability = ProcessExecCapability { - command: "git".to_string(), - args: vec!["*".to_string()], - }; - - assert_eq!(capability.allows("git", &["status"]), true); - assert_eq!(capability.allows("git", &["commit"]), true); - // Too many args. - assert_eq!(capability.allows("git", &["status", "-s"]), false); - // Wrong command. - assert_eq!(capability.allows("npm", &["install"]), false); - } - - #[test] - fn test_allows_with_double_wildcard() { - let capability = ProcessExecCapability { - command: "cargo".to_string(), - args: vec!["test".to_string(), "**".to_string()], - }; - - assert_eq!(capability.allows("cargo", &["test"]), true); - assert_eq!(capability.allows("cargo", &["test", "--all"]), true); - assert_eq!( - capability.allows("cargo", &["test", "--all", "--no-fail-fast"]), - true - ); - // Wrong first arg. - assert_eq!(capability.allows("cargo", &["build"]), false); - } - - #[test] - fn test_allows_with_mixed_wildcards() { - let capability = ProcessExecCapability { - command: "docker".to_string(), - args: vec!["run".to_string(), "*".to_string(), "**".to_string()], - }; - - assert_eq!(capability.allows("docker", &["run", "nginx"]), true); - assert_eq!(capability.allows("docker", &["run"]), false); - assert_eq!( - capability.allows("docker", &["run", "ubuntu", "bash"]), - true - ); - assert_eq!( - capability.allows("docker", &["run", "alpine", "sh", "-c", "echo hello"]), - true - ); - // Wrong first arg. - assert_eq!(capability.allows("docker", &["ps"]), false); - } -} diff --git a/crates/extension/src/extension.rs b/crates/extension/src/extension.rs deleted file mode 100644 index 88f2bea0c0..0000000000 --- a/crates/extension/src/extension.rs +++ /dev/null @@ -1,210 +0,0 @@ -mod capabilities; -pub mod extension_builder; -mod extension_events; -mod extension_host_proxy; -mod extension_manifest; -mod types; - -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -use ::lsp::LanguageServerName; -use anyhow::{Context as _, Result, bail}; -use async_trait::async_trait; -use fs::normalize_path; -use gpui::{App, Task}; -use language::LanguageName; -use semver::Version; -use task::{SpawnInTerminal, ZedDebugConfig}; -use util::rel_path::RelPath; - -pub use crate::capabilities::*; -pub use crate::extension_events::*; -pub use crate::extension_host_proxy::*; -pub use crate::extension_manifest::*; -pub use crate::types::*; - -/// Initializes the `extension` crate. -pub fn init(cx: &mut App) { - extension_events::init(cx); - ExtensionHostProxy::default_global(cx); -} - -#[async_trait] -pub trait WorktreeDelegate: Send + Sync + 'static { - fn id(&self) -> u64; - fn root_path(&self) -> String; - async fn read_text_file(&self, path: &RelPath) -> Result; - async fn which(&self, binary_name: String) -> Option; - async fn shell_env(&self) -> Vec<(String, String)>; -} - -pub trait ProjectDelegate: Send + Sync + 'static { - fn worktree_ids(&self) -> Vec; -} - -pub trait KeyValueStoreDelegate: Send + Sync + 'static { - fn insert(&self, key: String, docs: String) -> Task>; -} - -#[async_trait] -pub trait Extension: Send + Sync + 'static { - /// Returns the [`ExtensionManifest`] for this extension. - fn manifest(&self) -> Arc; - - /// Returns the path to this extension's working directory. - fn work_dir(&self) -> Arc; - - /// Returns a path relative to this extension's working directory. - fn path_from_extension(&self, path: &Path) -> PathBuf { - normalize_path(&self.work_dir().join(path)) - } - - async fn language_server_command( - &self, - language_server_id: LanguageServerName, - language_name: LanguageName, - worktree: Arc, - ) -> Result; - - async fn language_server_initialization_options( - &self, - language_server_id: LanguageServerName, - language_name: LanguageName, - worktree: Arc, - ) -> Result>; - - async fn language_server_workspace_configuration( - &self, - language_server_id: LanguageServerName, - worktree: Arc, - ) -> Result>; - - async fn language_server_additional_initialization_options( - &self, - language_server_id: LanguageServerName, - target_language_server_id: LanguageServerName, - worktree: Arc, - ) -> Result>; - - async fn language_server_additional_workspace_configuration( - &self, - language_server_id: LanguageServerName, - target_language_server_id: LanguageServerName, - worktree: Arc, - ) -> Result>; - - async fn labels_for_completions( - &self, - language_server_id: LanguageServerName, - completions: Vec, - ) -> Result>>; - - async fn labels_for_symbols( - &self, - language_server_id: LanguageServerName, - symbols: Vec, - ) -> Result>>; - - async fn complete_slash_command_argument( - &self, - command: SlashCommand, - arguments: Vec, - ) -> Result>; - - async fn run_slash_command( - &self, - command: SlashCommand, - arguments: Vec, - worktree: Option>, - ) -> Result; - - async fn context_server_command( - &self, - context_server_id: Arc, - project: Arc, - ) -> Result; - - async fn context_server_configuration( - &self, - context_server_id: Arc, - project: Arc, - ) -> Result>; - - async fn suggest_docs_packages(&self, provider: Arc) -> Result>; - - async fn index_docs( - &self, - provider: Arc, - package_name: Arc, - kv_store: Arc, - ) -> Result<()>; - - async fn get_dap_binary( - &self, - dap_name: Arc, - config: DebugTaskDefinition, - user_installed_path: Option, - worktree: Arc, - ) -> Result; - - async fn dap_request_kind( - &self, - dap_name: Arc, - config: serde_json::Value, - ) -> Result; - - async fn dap_config_to_scenario(&self, config: ZedDebugConfig) -> Result; - - async fn dap_locator_create_scenario( - &self, - locator_name: String, - build_config_template: BuildTaskTemplate, - resolved_label: String, - debug_adapter_name: String, - ) -> Result>; - async fn run_dap_locator( - &self, - locator_name: String, - config: SpawnInTerminal, - ) -> Result; -} - -pub fn parse_wasm_extension_version(extension_id: &str, wasm_bytes: &[u8]) -> Result { - let mut version = None; - - for part in wasmparser::Parser::new(0).parse_all(wasm_bytes) { - if let wasmparser::Payload::CustomSection(s) = - part.context("error parsing wasm extension")? - && s.name() == "zed:api-version" - { - version = parse_wasm_extension_version_custom_section(s.data()); - if version.is_none() { - bail!( - "extension {} has invalid zed:api-version section: {:?}", - extension_id, - s.data() - ); - } - } - } - - // The reason we wait until we're done parsing all of the Wasm bytes to return the version - // is to work around a panic that can happen inside of Wasmtime when the bytes are invalid. - // - // By parsing the entirety of the Wasm bytes before we return, we're able to detect this problem - // earlier as an `Err` rather than as a panic. - version.with_context(|| format!("extension {extension_id} has no zed:api-version section")) -} - -fn parse_wasm_extension_version_custom_section(data: &[u8]) -> Option { - if data.len() == 6 { - Some(Version::new( - u16::from_be_bytes([data[0], data[1]]) as _, - u16::from_be_bytes([data[2], data[3]]) as _, - u16::from_be_bytes([data[4], data[5]]) as _, - )) - } else { - None - } -} diff --git a/crates/extension/src/extension_builder.rs b/crates/extension/src/extension_builder.rs deleted file mode 100644 index 8b9bf994d1..0000000000 --- a/crates/extension/src/extension_builder.rs +++ /dev/null @@ -1,826 +0,0 @@ -use crate::{ - ExtensionLibraryKind, ExtensionManifest, GrammarManifestEntry, build_debug_adapter_schema_path, - parse_wasm_extension_version, -}; -use ::fs::Fs; -use anyhow::{Context as _, Result, bail}; -use futures::{AsyncReadExt, StreamExt}; -use heck::ToSnakeCase; -use http_client::{self, AsyncBody, HttpClient}; -use serde::Deserialize; -use std::{ - env, fs, mem, - path::{Path, PathBuf}, - process::Stdio, - str::FromStr, - sync::Arc, -}; -use wasm_encoder::{ComponentSectionId, Encode as _, RawSection, Section as _}; -use wasmparser::Parser; - -/// Currently, we compile with Rust's `wasm32-wasip2` target, which works with WASI `preview2` and the component model. -const RUST_TARGET: &str = "wasm32-wasip2"; - -/// Compiling Tree-sitter parsers from C to WASM requires Clang 17, and a WASM build of libc -/// and clang's runtime library. The `wasi-sdk` provides these binaries. -/// -/// Once Clang 17 and its wasm target are available via system package managers, we won't need -/// to download this. -const WASI_SDK_URL: &str = "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/"; -const WASI_SDK_ASSET_NAME: Option<&str> = if cfg!(all(target_os = "macos", target_arch = "x86_64")) -{ - Some("wasi-sdk-25.0-x86_64-macos.tar.gz") -} else if cfg!(all(target_os = "macos", target_arch = "aarch64")) { - Some("wasi-sdk-25.0-arm64-macos.tar.gz") -} else if cfg!(all(target_os = "linux", target_arch = "x86_64")) { - Some("wasi-sdk-25.0-x86_64-linux.tar.gz") -} else if cfg!(all(target_os = "linux", target_arch = "aarch64")) { - Some("wasi-sdk-25.0-arm64-linux.tar.gz") -} else if cfg!(all(target_os = "freebsd", target_arch = "x86_64")) { - Some("wasi-sdk-25.0-x86_64-linux.tar.gz") -} else if cfg!(all(target_os = "freebsd", target_arch = "aarch64")) { - Some("wasi-sdk-25.0-arm64-linux.tar.gz") -} else if cfg!(all(target_os = "windows", target_arch = "x86_64")) { - Some("wasi-sdk-25.0-x86_64-windows.tar.gz") -} else { - None -}; - -pub struct ExtensionBuilder { - cache_dir: PathBuf, - pub http: Arc, -} - -pub struct CompileExtensionOptions { - pub release: bool, -} - -#[derive(Deserialize)] -struct CargoToml { - package: CargoTomlPackage, -} - -#[derive(Deserialize)] -struct CargoTomlPackage { - name: String, -} - -impl ExtensionBuilder { - pub fn new(http_client: Arc, cache_dir: PathBuf) -> Self { - Self { - cache_dir, - http: http_client, - } - } - - pub async fn compile_extension( - &self, - extension_dir: &Path, - extension_manifest: &mut ExtensionManifest, - options: CompileExtensionOptions, - fs: Arc, - ) -> Result<()> { - populate_defaults(extension_manifest, extension_dir, fs).await?; - - if extension_dir.is_relative() { - bail!( - "extension dir {} is not an absolute path", - extension_dir.display() - ); - } - - fs::create_dir_all(&self.cache_dir).context("failed to create cache dir")?; - - if extension_manifest.lib.kind == Some(ExtensionLibraryKind::Rust) { - log::info!("compiling Rust extension {}", extension_dir.display()); - self.compile_rust_extension(extension_dir, extension_manifest, options) - .await - .context("failed to compile Rust extension")?; - log::info!("compiled Rust extension {}", extension_dir.display()); - } - - for (debug_adapter_name, meta) in &mut extension_manifest.debug_adapters { - let debug_adapter_schema_path = - extension_dir.join(build_debug_adapter_schema_path(debug_adapter_name, meta)); - - let debug_adapter_schema = fs::read_to_string(&debug_adapter_schema_path) - .with_context(|| { - format!("failed to read debug adapter schema for `{debug_adapter_name}` from `{debug_adapter_schema_path:?}`") - })?; - _ = serde_json::Value::from_str(&debug_adapter_schema).with_context(|| { - format!("Debug adapter schema for `{debug_adapter_name}` (path: `{debug_adapter_schema_path:?}`) is not a valid JSON") - })?; - } - for (grammar_name, grammar_metadata) in &extension_manifest.grammars { - let snake_cased_grammar_name = grammar_name.to_snake_case(); - if grammar_name.as_ref() != snake_cased_grammar_name.as_str() { - bail!( - "grammar name '{grammar_name}' must be written in snake_case: {snake_cased_grammar_name}" - ); - } - - log::info!( - "compiling grammar {grammar_name} for extension {}", - extension_dir.display() - ); - self.compile_grammar(extension_dir, grammar_name.as_ref(), grammar_metadata) - .await - .with_context(|| format!("failed to compile grammar '{grammar_name}'"))?; - log::info!( - "compiled grammar {grammar_name} for extension {}", - extension_dir.display() - ); - } - - log::info!("finished compiling extension {}", extension_dir.display()); - Ok(()) - } - - async fn compile_rust_extension( - &self, - extension_dir: &Path, - manifest: &mut ExtensionManifest, - options: CompileExtensionOptions, - ) -> anyhow::Result<()> { - self.install_rust_wasm_target_if_needed().await?; - - let cargo_toml_content = fs::read_to_string(extension_dir.join("Cargo.toml"))?; - let cargo_toml: CargoToml = toml::from_str(&cargo_toml_content)?; - - log::info!( - "compiling Rust crate for extension {}", - extension_dir.display() - ); - let output = util::command::new_smol_command("cargo") - .args(["build", "--target", RUST_TARGET]) - .args(options.release.then_some("--release")) - .arg("--target-dir") - .arg(extension_dir.join("target")) - // WASI builds do not work with sccache and just stuck, so disable it. - .env("RUSTC_WRAPPER", "") - .current_dir(extension_dir) - .output() - .await - .context("failed to run `cargo`")?; - if !output.status.success() { - bail!( - "failed to build extension {}", - String::from_utf8_lossy(&output.stderr) - ); - } - - log::info!( - "compiled Rust crate for extension {}", - extension_dir.display() - ); - - let mut wasm_path = PathBuf::from(extension_dir); - wasm_path.extend([ - "target", - RUST_TARGET, - if options.release { "release" } else { "debug" }, - &cargo_toml - .package - .name - // The wasm32-wasip2 target normalizes `-` in package names to `_` in the resulting `.wasm` file. - .replace('-', "_"), - ]); - wasm_path.set_extension("wasm"); - - log::info!( - "encoding wasm component for extension {}", - extension_dir.display() - ); - - let component_bytes = fs::read(&wasm_path) - .with_context(|| format!("failed to read output module `{}`", wasm_path.display()))?; - - let component_bytes = self - .strip_custom_sections(&component_bytes) - .context("failed to strip debug sections from wasm component")?; - - let wasm_extension_api_version = - parse_wasm_extension_version(&manifest.id, &component_bytes) - .context("compiled wasm did not contain a valid zed extension api version")?; - manifest.lib.version = Some(wasm_extension_api_version); - - let extension_file = extension_dir.join("extension.wasm"); - fs::write(extension_file.clone(), &component_bytes) - .context("failed to write extension.wasm")?; - - log::info!( - "extension {} written to {}", - extension_dir.display(), - extension_file.display() - ); - - Ok(()) - } - - async fn compile_grammar( - &self, - extension_dir: &Path, - grammar_name: &str, - grammar_metadata: &GrammarManifestEntry, - ) -> Result<()> { - let clang_path = self.install_wasi_sdk_if_needed().await?; - - let mut grammar_repo_dir = extension_dir.to_path_buf(); - grammar_repo_dir.extend(["grammars", grammar_name]); - - let mut grammar_wasm_path = grammar_repo_dir.clone(); - grammar_wasm_path.set_extension("wasm"); - - log::info!("checking out {grammar_name} parser"); - self.checkout_repo( - &grammar_repo_dir, - &grammar_metadata.repository, - &grammar_metadata.rev, - ) - .await?; - - let base_grammar_path = grammar_metadata - .path - .as_ref() - .map(|path| grammar_repo_dir.join(path)) - .unwrap_or(grammar_repo_dir); - - let src_path = base_grammar_path.join("src"); - let parser_path = src_path.join("parser.c"); - let scanner_path = src_path.join("scanner.c"); - - // Skip recompiling if the WASM object is already newer than the source files - if file_newer_than_deps(&grammar_wasm_path, &[&parser_path, &scanner_path]).unwrap_or(false) - { - log::info!( - "skipping compilation of {grammar_name} parser because the existing compiled grammar is up to date" - ); - } else { - log::info!("compiling {grammar_name} parser"); - let clang_output = util::command::new_smol_command(&clang_path) - .args(["-fPIC", "-shared", "-Os"]) - .arg(format!("-Wl,--export=tree_sitter_{grammar_name}")) - .arg("-o") - .arg(&grammar_wasm_path) - .arg("-I") - .arg(&src_path) - .arg(&parser_path) - .args(scanner_path.exists().then_some(scanner_path)) - .output() - .await - .context("failed to run clang")?; - - if !clang_output.status.success() { - bail!( - "failed to compile {} parser with clang: {}", - grammar_name, - String::from_utf8_lossy(&clang_output.stderr), - ); - } - } - - Ok(()) - } - - async fn checkout_repo(&self, directory: &Path, url: &str, rev: &str) -> Result<()> { - let git_dir = directory.join(".git"); - - if directory.exists() { - let remotes_output = util::command::new_smol_command("git") - .arg("--git-dir") - .arg(&git_dir) - .args(["remote", "-v"]) - .output() - .await?; - let has_remote = remotes_output.status.success() - && String::from_utf8_lossy(&remotes_output.stdout) - .lines() - .any(|line| { - let mut parts = line.split(|c: char| c.is_whitespace()); - parts.next() == Some("origin") && parts.any(|part| part == url) - }); - if !has_remote { - bail!( - "grammar directory '{}' already exists, but is not a git clone of '{}'", - directory.display(), - url - ); - } - } else { - fs::create_dir_all(directory).with_context(|| { - format!("failed to create grammar directory {}", directory.display(),) - })?; - let init_output = util::command::new_smol_command("git") - .arg("init") - .current_dir(directory) - .output() - .await?; - if !init_output.status.success() { - bail!( - "failed to run `git init` in directory '{}'", - directory.display() - ); - } - - let remote_add_output = util::command::new_smol_command("git") - .arg("--git-dir") - .arg(&git_dir) - .args(["remote", "add", "origin", url]) - .output() - .await - .context("failed to execute `git remote add`")?; - if !remote_add_output.status.success() { - bail!( - "failed to add remote {url} for git repository {}", - git_dir.display() - ); - } - } - - let fetch_output = util::command::new_smol_command("git") - .arg("--git-dir") - .arg(&git_dir) - .args(["fetch", "--depth", "1", "origin", rev]) - .output() - .await - .context("failed to execute `git fetch`")?; - - let checkout_output = util::command::new_smol_command("git") - .arg("--git-dir") - .arg(&git_dir) - .args(["checkout", rev]) - .current_dir(directory) - .output() - .await - .context("failed to execute `git checkout`")?; - if !checkout_output.status.success() { - if !fetch_output.status.success() { - bail!( - "failed to fetch revision {} in directory '{}'", - rev, - directory.display() - ); - } - bail!( - "failed to checkout revision {} in directory '{}': {}", - rev, - directory.display(), - String::from_utf8_lossy(&checkout_output.stderr) - ); - } - - Ok(()) - } - - async fn install_rust_wasm_target_if_needed(&self) -> Result<()> { - let rustc_output = util::command::new_smol_command("rustc") - .arg("--print") - .arg("sysroot") - .output() - .await - .context("failed to run rustc")?; - if !rustc_output.status.success() { - bail!( - "failed to retrieve rust sysroot: {}", - String::from_utf8_lossy(&rustc_output.stderr) - ); - } - - let sysroot = PathBuf::from(String::from_utf8(rustc_output.stdout)?.trim()); - if sysroot.join("lib/rustlib").join(RUST_TARGET).exists() { - return Ok(()); - } - - let output = util::command::new_smol_command("rustup") - .args(["target", "add", RUST_TARGET]) - .stderr(Stdio::piped()) - .stdout(Stdio::inherit()) - .output() - .await - .context("failed to run `rustup target add`")?; - if !output.status.success() { - bail!( - "failed to install the `{RUST_TARGET}` target: {}", - String::from_utf8_lossy(&rustc_output.stderr) - ); - } - - Ok(()) - } - - async fn install_wasi_sdk_if_needed(&self) -> Result { - let url = if let Some(asset_name) = WASI_SDK_ASSET_NAME { - format!("{WASI_SDK_URL}{asset_name}") - } else { - bail!("wasi-sdk is not available for platform {}", env::consts::OS); - }; - - let wasi_sdk_dir = self.cache_dir.join("wasi-sdk"); - let mut clang_path = wasi_sdk_dir.clone(); - clang_path.extend(["bin", &format!("clang{}", env::consts::EXE_SUFFIX)]); - - log::info!("downloading wasi-sdk to {}", wasi_sdk_dir.display()); - - if fs::metadata(&clang_path).is_ok_and(|metadata| metadata.is_file()) { - return Ok(clang_path); - } - - let tar_out_dir = self.cache_dir.join("wasi-sdk-temp"); - - fs::remove_dir_all(&wasi_sdk_dir).ok(); - fs::remove_dir_all(&tar_out_dir).ok(); - fs::create_dir_all(&tar_out_dir).context("failed to create extraction directory")?; - - let mut response = self.http.get(&url, AsyncBody::default(), true).await?; - - // Write the response to a temporary file - let tar_gz_path = self.cache_dir.join("wasi-sdk.tar.gz"); - let mut tar_gz_file = - fs::File::create(&tar_gz_path).context("failed to create temporary tar.gz file")?; - let response_body = response.body_mut(); - let mut body_bytes = Vec::new(); - response_body.read_to_end(&mut body_bytes).await?; - std::io::Write::write_all(&mut tar_gz_file, &body_bytes)?; - drop(tar_gz_file); - - log::info!("un-tarring wasi-sdk to {}", tar_out_dir.display()); - - // Shell out to tar to extract the archive - let tar_output = util::command::new_smol_command("tar") - .arg("-xzf") - .arg(&tar_gz_path) - .arg("-C") - .arg(&tar_out_dir) - .output() - .await - .context("failed to run tar")?; - - if !tar_output.status.success() { - bail!( - "failed to extract wasi-sdk archive: {}", - String::from_utf8_lossy(&tar_output.stderr) - ); - } - - log::info!("finished downloading wasi-sdk"); - - // Clean up the temporary tar.gz file - fs::remove_file(&tar_gz_path).ok(); - - let inner_dir = fs::read_dir(&tar_out_dir)? - .next() - .context("no content")? - .context("failed to read contents of extracted wasi archive directory")? - .path(); - fs::rename(&inner_dir, &wasi_sdk_dir).context("failed to move extracted wasi dir")?; - fs::remove_dir_all(&tar_out_dir).ok(); - - Ok(clang_path) - } - - // This was adapted from: - // https://github.com/bytecodealliance/wasm-tools/blob/e8809bb17fcf69aa8c85cd5e6db7cff5cf36b1de/src/bin/wasm-tools/strip.rs - fn strip_custom_sections(&self, input: &Vec) -> Result> { - use wasmparser::Payload::*; - - let strip_custom_section = |name: &str| { - // Default strip everything but: - // * the `name` section - // * any `component-type` sections - // * the `dylink.0` section - // * our custom version section - name != "name" - && !name.starts_with("component-type:") - && name != "dylink.0" - && name != "zed:api-version" - }; - - let mut output = Vec::new(); - let mut stack = Vec::new(); - - for payload in Parser::new(0).parse_all(input) { - let payload = payload?; - - // Track nesting depth, so that we don't mess with inner producer sections: - match payload { - Version { encoding, .. } => { - output.extend_from_slice(match encoding { - wasmparser::Encoding::Component => &wasm_encoder::Component::HEADER, - wasmparser::Encoding::Module => &wasm_encoder::Module::HEADER, - }); - } - ModuleSection { .. } | ComponentSection { .. } => { - stack.push(mem::take(&mut output)); - continue; - } - End { .. } => { - let mut parent = match stack.pop() { - Some(c) => c, - None => break, - }; - if output.starts_with(&wasm_encoder::Component::HEADER) { - parent.push(ComponentSectionId::Component as u8); - output.encode(&mut parent); - } else { - parent.push(ComponentSectionId::CoreModule as u8); - output.encode(&mut parent); - } - output = parent; - } - _ => {} - } - - if let CustomSection(c) = &payload - && strip_custom_section(c.name()) - { - continue; - } - if let Some((id, range)) = payload.as_section() { - RawSection { - id, - data: &input[range], - } - .append_to(&mut output); - } - } - - Ok(output) - } -} - -async fn populate_defaults( - manifest: &mut ExtensionManifest, - extension_path: &Path, - fs: Arc, -) -> Result<()> { - // For legacy extensions on the v0 schema (aka, using `extension.json`), clear out any existing - // contents of the computed fields, since we don't care what the existing values are. - if manifest.schema_version.is_v0() { - manifest.languages.clear(); - manifest.grammars.clear(); - manifest.themes.clear(); - } - - let cargo_toml_path = extension_path.join("Cargo.toml"); - if cargo_toml_path.exists() { - manifest.lib.kind = Some(ExtensionLibraryKind::Rust); - } - - let languages_dir = extension_path.join("languages"); - if fs.is_dir(&languages_dir).await { - let mut language_dir_entries = fs - .read_dir(&languages_dir) - .await - .context("failed to list languages dir")?; - - while let Some(language_dir) = language_dir_entries.next().await { - let language_dir = language_dir?; - let config_path = language_dir.join("config.toml"); - if fs.is_file(config_path.as_path()).await { - let relative_language_dir = - language_dir.strip_prefix(extension_path)?.to_path_buf(); - if !manifest.languages.contains(&relative_language_dir) { - manifest.languages.push(relative_language_dir); - } - } - } - } - - let themes_dir = extension_path.join("themes"); - if fs.is_dir(&themes_dir).await { - let mut theme_dir_entries = fs - .read_dir(&themes_dir) - .await - .context("failed to list themes dir")?; - - while let Some(theme_path) = theme_dir_entries.next().await { - let theme_path = theme_path?; - if theme_path.extension() == Some("json".as_ref()) { - let relative_theme_path = theme_path.strip_prefix(extension_path)?.to_path_buf(); - if !manifest.themes.contains(&relative_theme_path) { - manifest.themes.push(relative_theme_path); - } - } - } - } - - let icon_themes_dir = extension_path.join("icon_themes"); - if fs.is_dir(&icon_themes_dir).await { - let mut icon_theme_dir_entries = fs - .read_dir(&icon_themes_dir) - .await - .context("failed to list icon themes dir")?; - - while let Some(icon_theme_path) = icon_theme_dir_entries.next().await { - let icon_theme_path = icon_theme_path?; - if icon_theme_path.extension() == Some("json".as_ref()) { - let relative_icon_theme_path = - icon_theme_path.strip_prefix(extension_path)?.to_path_buf(); - if !manifest.icon_themes.contains(&relative_icon_theme_path) { - manifest.icon_themes.push(relative_icon_theme_path); - } - } - } - }; - if manifest.snippets.is_none() - && let snippets_json_path = extension_path.join("snippets.json") - && fs.is_file(&snippets_json_path).await - { - manifest.snippets = Some("snippets.json".into()); - } - - // For legacy extensions on the v0 schema (aka, using `extension.json`), we want to populate the grammars in - // the manifest using the contents of the `grammars` directory. - if manifest.schema_version.is_v0() { - let grammars_dir = extension_path.join("grammars"); - if fs.is_dir(&grammars_dir).await { - let mut grammar_dir_entries = fs - .read_dir(&grammars_dir) - .await - .context("failed to list grammars dir")?; - - while let Some(grammar_path) = grammar_dir_entries.next().await { - let grammar_path = grammar_path?; - if grammar_path.extension() == Some("toml".as_ref()) { - #[derive(Deserialize)] - struct GrammarConfigToml { - pub repository: String, - pub commit: String, - #[serde(default)] - pub path: Option, - } - - let grammar_config = fs.load(&grammar_path).await?; - let grammar_config: GrammarConfigToml = toml::from_str(&grammar_config)?; - - let grammar_name = grammar_path - .file_stem() - .and_then(|stem| stem.to_str()) - .context("no grammar name")?; - if !manifest.grammars.contains_key(grammar_name) { - manifest.grammars.insert( - grammar_name.into(), - GrammarManifestEntry { - repository: grammar_config.repository, - rev: grammar_config.commit, - path: grammar_config.path, - }, - ); - } - } - } - } - } - - Ok(()) -} - -/// Returns `true` if the target exists and its last modified time is greater than that -/// of each dependency which exists (i.e., dependency paths which do not exist are ignored). -/// -/// # Errors -/// -/// Returns `Err` if any of the underlying file I/O operations fail. -fn file_newer_than_deps(target: &Path, dependencies: &[&Path]) -> Result { - if !target.try_exists()? { - return Ok(false); - } - let target_modified = target.metadata()?.modified()?; - for dependency in dependencies { - if !dependency.try_exists()? { - continue; - } - let dep_modified = dependency.metadata()?.modified()?; - if target_modified < dep_modified { - return Ok(false); - } - } - Ok(true) -} - -#[cfg(test)] -mod tests { - use std::{ - path::{Path, PathBuf}, - str::FromStr, - thread::sleep, - time::Duration, - }; - - use gpui::TestAppContext; - use indoc::indoc; - - use crate::{ - ExtensionManifest, - extension_builder::{file_newer_than_deps, populate_defaults}, - }; - - #[test] - fn test_file_newer_than_deps() { - // Don't use TempTree because we need to guarantee the order - let tmpdir = tempfile::tempdir().unwrap(); - let target = tmpdir.path().join("target.wasm"); - let dep1 = tmpdir.path().join("parser.c"); - let dep2 = tmpdir.path().join("scanner.c"); - - assert!( - !file_newer_than_deps(&target, &[&dep1, &dep2]).unwrap(), - "target doesn't exist" - ); - std::fs::write(&target, "foo").unwrap(); // Create target - assert!( - file_newer_than_deps(&target, &[&dep1, &dep2]).unwrap(), - "dependencies don't exist; target is newer" - ); - sleep(Duration::from_secs(1)); - std::fs::write(&dep1, "foo").unwrap(); // Create dep1 (newer than target) - // Dependency is newer - assert!( - !file_newer_than_deps(&target, &[&dep1, &dep2]).unwrap(), - "a dependency is newer (target {:?}, dep1 {:?})", - target.metadata().unwrap().modified().unwrap(), - dep1.metadata().unwrap().modified().unwrap(), - ); - sleep(Duration::from_secs(1)); - std::fs::write(&dep2, "foo").unwrap(); // Create dep2 - sleep(Duration::from_secs(1)); - std::fs::write(&target, "foobar").unwrap(); // Update target - assert!( - file_newer_than_deps(&target, &[&dep1, &dep2]).unwrap(), - "target is newer than dependencies (target {:?}, dep2 {:?})", - target.metadata().unwrap().modified().unwrap(), - dep2.metadata().unwrap().modified().unwrap(), - ); - } - - #[gpui::test] - async fn test_snippet_location_is_kept(cx: &mut TestAppContext) { - let fs = fs::FakeFs::new(cx.executor()); - let extension_path = Path::new("/extension"); - - fs.insert_tree( - extension_path, - serde_json::json!({ - "extension.toml": indoc! {r#" - id = "test-manifest" - name = "Test Manifest" - version = "0.0.1" - schema_version = 1 - - snippets = "./snippets/snippets.json" - "# - }, - "snippets.json": "", - }), - ) - .await; - - let mut manifest = ExtensionManifest::load(fs.clone(), extension_path) - .await - .unwrap(); - - populate_defaults(&mut manifest, extension_path, fs.clone()) - .await - .unwrap(); - - assert_eq!( - manifest.snippets, - Some(PathBuf::from_str("./snippets/snippets.json").unwrap()) - ) - } - - #[gpui::test] - async fn test_automatic_snippet_location_is_relative(cx: &mut TestAppContext) { - let fs = fs::FakeFs::new(cx.executor()); - let extension_path = Path::new("/extension"); - - fs.insert_tree( - extension_path, - serde_json::json!({ - "extension.toml": indoc! {r#" - id = "test-manifest" - name = "Test Manifest" - version = "0.0.1" - schema_version = 1 - - "# - }, - "snippets.json": "", - }), - ) - .await; - - let mut manifest = ExtensionManifest::load(fs.clone(), extension_path) - .await - .unwrap(); - - populate_defaults(&mut manifest, extension_path, fs.clone()) - .await - .unwrap(); - - assert_eq!( - manifest.snippets, - Some(PathBuf::from_str("snippets.json").unwrap()) - ) - } -} diff --git a/crates/extension/src/extension_events.rs b/crates/extension/src/extension_events.rs deleted file mode 100644 index 6dc99470df..0000000000 --- a/crates/extension/src/extension_events.rs +++ /dev/null @@ -1,43 +0,0 @@ -use std::sync::Arc; - -use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Global}; - -use crate::ExtensionManifest; - -pub fn init(cx: &mut App) { - let extension_events = cx.new(ExtensionEvents::new); - cx.set_global(GlobalExtensionEvents(extension_events)); -} - -struct GlobalExtensionEvents(Entity); - -impl Global for GlobalExtensionEvents {} - -/// An event bus for broadcasting extension-related events throughout the app. -pub struct ExtensionEvents; - -impl ExtensionEvents { - /// Returns the global [`ExtensionEvents`]. - pub fn try_global(cx: &App) -> Option> { - cx.try_global::() - .map(|g| g.0.clone()) - } - - fn new(_cx: &mut Context) -> Self { - Self - } - - pub fn emit(&mut self, event: Event, cx: &mut Context) { - cx.emit(event) - } -} - -#[derive(Clone, Debug)] -pub enum Event { - ExtensionInstalled(Arc), - ExtensionUninstalled(Arc), - ExtensionsInstalledChanged, - ConfigureExtensionRequested(Arc), -} - -impl EventEmitter for ExtensionEvents {} diff --git a/crates/extension/src/extension_host_proxy.rs b/crates/extension/src/extension_host_proxy.rs deleted file mode 100644 index 6a24e3ba3f..0000000000 --- a/crates/extension/src/extension_host_proxy.rs +++ /dev/null @@ -1,448 +0,0 @@ -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -use anyhow::Result; -use fs::Fs; -use gpui::{App, Global, ReadGlobal, SharedString, Task}; -use language::{BinaryStatus, LanguageMatcher, LanguageName, LoadedLanguage}; -use lsp::LanguageServerName; -use parking_lot::RwLock; - -use crate::{Extension, SlashCommand}; - -#[derive(Default)] -struct GlobalExtensionHostProxy(Arc); - -impl Global for GlobalExtensionHostProxy {} - -/// A proxy for interacting with the extension host. -/// -/// This object implements each of the individual proxy types so that their -/// methods can be called directly on it. -#[derive(Default)] -pub struct ExtensionHostProxy { - theme_proxy: RwLock>>, - grammar_proxy: RwLock>>, - language_proxy: RwLock>>, - language_server_proxy: RwLock>>, - snippet_proxy: RwLock>>, - slash_command_proxy: RwLock>>, - context_server_proxy: RwLock>>, - debug_adapter_provider_proxy: RwLock>>, -} - -impl ExtensionHostProxy { - /// Returns the global [`ExtensionHostProxy`]. - pub fn global(cx: &App) -> Arc { - GlobalExtensionHostProxy::global(cx).0.clone() - } - - /// Returns the global [`ExtensionHostProxy`]. - /// - /// Inserts a default [`ExtensionHostProxy`] if one does not yet exist. - pub fn default_global(cx: &mut App) -> Arc { - cx.default_global::().0.clone() - } - - pub fn new() -> Self { - Self { - theme_proxy: RwLock::default(), - grammar_proxy: RwLock::default(), - language_proxy: RwLock::default(), - language_server_proxy: RwLock::default(), - snippet_proxy: RwLock::default(), - slash_command_proxy: RwLock::default(), - context_server_proxy: RwLock::default(), - debug_adapter_provider_proxy: RwLock::default(), - } - } - - pub fn register_theme_proxy(&self, proxy: impl ExtensionThemeProxy) { - self.theme_proxy.write().replace(Arc::new(proxy)); - } - - pub fn register_grammar_proxy(&self, proxy: impl ExtensionGrammarProxy) { - self.grammar_proxy.write().replace(Arc::new(proxy)); - } - - pub fn register_language_proxy(&self, proxy: impl ExtensionLanguageProxy) { - self.language_proxy.write().replace(Arc::new(proxy)); - } - - pub fn register_language_server_proxy(&self, proxy: impl ExtensionLanguageServerProxy) { - self.language_server_proxy.write().replace(Arc::new(proxy)); - } - - pub fn register_snippet_proxy(&self, proxy: impl ExtensionSnippetProxy) { - self.snippet_proxy.write().replace(Arc::new(proxy)); - } - - pub fn register_slash_command_proxy(&self, proxy: impl ExtensionSlashCommandProxy) { - self.slash_command_proxy.write().replace(Arc::new(proxy)); - } - - pub fn register_context_server_proxy(&self, proxy: impl ExtensionContextServerProxy) { - self.context_server_proxy.write().replace(Arc::new(proxy)); - } - - pub fn register_debug_adapter_proxy(&self, proxy: impl ExtensionDebugAdapterProviderProxy) { - self.debug_adapter_provider_proxy - .write() - .replace(Arc::new(proxy)); - } -} - -pub trait ExtensionThemeProxy: Send + Sync + 'static { - fn set_extensions_loaded(&self); - - fn list_theme_names(&self, theme_path: PathBuf, fs: Arc) -> Task>>; - - fn remove_user_themes(&self, themes: Vec); - - fn load_user_theme(&self, theme_path: PathBuf, fs: Arc) -> Task>; - - fn reload_current_theme(&self, cx: &mut App); - - fn list_icon_theme_names( - &self, - icon_theme_path: PathBuf, - fs: Arc, - ) -> Task>>; - - fn remove_icon_themes(&self, icon_themes: Vec); - - fn load_icon_theme( - &self, - icon_theme_path: PathBuf, - icons_root_dir: PathBuf, - fs: Arc, - ) -> Task>; - - fn reload_current_icon_theme(&self, cx: &mut App); -} - -impl ExtensionThemeProxy for ExtensionHostProxy { - fn set_extensions_loaded(&self) { - let Some(proxy) = self.theme_proxy.read().clone() else { - return; - }; - - proxy.set_extensions_loaded() - } - - fn list_theme_names(&self, theme_path: PathBuf, fs: Arc) -> Task>> { - let Some(proxy) = self.theme_proxy.read().clone() else { - return Task::ready(Ok(Vec::new())); - }; - - proxy.list_theme_names(theme_path, fs) - } - - fn remove_user_themes(&self, themes: Vec) { - let Some(proxy) = self.theme_proxy.read().clone() else { - return; - }; - - proxy.remove_user_themes(themes) - } - - fn load_user_theme(&self, theme_path: PathBuf, fs: Arc) -> Task> { - let Some(proxy) = self.theme_proxy.read().clone() else { - return Task::ready(Ok(())); - }; - - proxy.load_user_theme(theme_path, fs) - } - - fn reload_current_theme(&self, cx: &mut App) { - let Some(proxy) = self.theme_proxy.read().clone() else { - return; - }; - - proxy.reload_current_theme(cx) - } - - fn list_icon_theme_names( - &self, - icon_theme_path: PathBuf, - fs: Arc, - ) -> Task>> { - let Some(proxy) = self.theme_proxy.read().clone() else { - return Task::ready(Ok(Vec::new())); - }; - - proxy.list_icon_theme_names(icon_theme_path, fs) - } - - fn remove_icon_themes(&self, icon_themes: Vec) { - let Some(proxy) = self.theme_proxy.read().clone() else { - return; - }; - - proxy.remove_icon_themes(icon_themes) - } - - fn load_icon_theme( - &self, - icon_theme_path: PathBuf, - icons_root_dir: PathBuf, - fs: Arc, - ) -> Task> { - let Some(proxy) = self.theme_proxy.read().clone() else { - return Task::ready(Ok(())); - }; - - proxy.load_icon_theme(icon_theme_path, icons_root_dir, fs) - } - - fn reload_current_icon_theme(&self, cx: &mut App) { - let Some(proxy) = self.theme_proxy.read().clone() else { - return; - }; - - proxy.reload_current_icon_theme(cx) - } -} - -pub trait ExtensionGrammarProxy: Send + Sync + 'static { - fn register_grammars(&self, grammars: Vec<(Arc, PathBuf)>); -} - -impl ExtensionGrammarProxy for ExtensionHostProxy { - fn register_grammars(&self, grammars: Vec<(Arc, PathBuf)>) { - let Some(proxy) = self.grammar_proxy.read().clone() else { - return; - }; - - proxy.register_grammars(grammars) - } -} - -pub trait ExtensionLanguageProxy: Send + Sync + 'static { - fn register_language( - &self, - language: LanguageName, - grammar: Option>, - matcher: LanguageMatcher, - hidden: bool, - load: Arc Result + Send + Sync + 'static>, - ); - - fn remove_languages( - &self, - languages_to_remove: &[LanguageName], - grammars_to_remove: &[Arc], - ); -} - -impl ExtensionLanguageProxy for ExtensionHostProxy { - fn register_language( - &self, - language: LanguageName, - grammar: Option>, - matcher: LanguageMatcher, - hidden: bool, - load: Arc Result + Send + Sync + 'static>, - ) { - let Some(proxy) = self.language_proxy.read().clone() else { - return; - }; - - proxy.register_language(language, grammar, matcher, hidden, load) - } - - fn remove_languages( - &self, - languages_to_remove: &[LanguageName], - grammars_to_remove: &[Arc], - ) { - let Some(proxy) = self.language_proxy.read().clone() else { - return; - }; - - proxy.remove_languages(languages_to_remove, grammars_to_remove) - } -} - -pub trait ExtensionLanguageServerProxy: Send + Sync + 'static { - fn register_language_server( - &self, - extension: Arc, - language_server_id: LanguageServerName, - language: LanguageName, - ); - - fn remove_language_server( - &self, - language: &LanguageName, - language_server_id: &LanguageServerName, - cx: &mut App, - ) -> Task>; - - fn update_language_server_status( - &self, - language_server_id: LanguageServerName, - status: BinaryStatus, - ); -} - -impl ExtensionLanguageServerProxy for ExtensionHostProxy { - fn register_language_server( - &self, - extension: Arc, - language_server_id: LanguageServerName, - language: LanguageName, - ) { - let Some(proxy) = self.language_server_proxy.read().clone() else { - return; - }; - - proxy.register_language_server(extension, language_server_id, language) - } - - fn remove_language_server( - &self, - language: &LanguageName, - language_server_id: &LanguageServerName, - cx: &mut App, - ) -> Task> { - let Some(proxy) = self.language_server_proxy.read().clone() else { - return Task::ready(Ok(())); - }; - - proxy.remove_language_server(language, language_server_id, cx) - } - - fn update_language_server_status( - &self, - language_server_id: LanguageServerName, - status: BinaryStatus, - ) { - let Some(proxy) = self.language_server_proxy.read().clone() else { - return; - }; - - proxy.update_language_server_status(language_server_id, status) - } -} - -pub trait ExtensionSnippetProxy: Send + Sync + 'static { - fn register_snippet(&self, path: &PathBuf, snippet_contents: &str) -> Result<()>; -} - -impl ExtensionSnippetProxy for ExtensionHostProxy { - fn register_snippet(&self, path: &PathBuf, snippet_contents: &str) -> Result<()> { - let Some(proxy) = self.snippet_proxy.read().clone() else { - return Ok(()); - }; - - proxy.register_snippet(path, snippet_contents) - } -} - -pub trait ExtensionSlashCommandProxy: Send + Sync + 'static { - fn register_slash_command(&self, extension: Arc, command: SlashCommand); - - fn unregister_slash_command(&self, command_name: Arc); -} - -impl ExtensionSlashCommandProxy for ExtensionHostProxy { - fn register_slash_command(&self, extension: Arc, command: SlashCommand) { - let Some(proxy) = self.slash_command_proxy.read().clone() else { - return; - }; - - proxy.register_slash_command(extension, command) - } - - fn unregister_slash_command(&self, command_name: Arc) { - let Some(proxy) = self.slash_command_proxy.read().clone() else { - return; - }; - - proxy.unregister_slash_command(command_name) - } -} - -pub trait ExtensionContextServerProxy: Send + Sync + 'static { - fn register_context_server( - &self, - extension: Arc, - server_id: Arc, - cx: &mut App, - ); - - fn unregister_context_server(&self, server_id: Arc, cx: &mut App); -} - -impl ExtensionContextServerProxy for ExtensionHostProxy { - fn register_context_server( - &self, - extension: Arc, - server_id: Arc, - cx: &mut App, - ) { - let Some(proxy) = self.context_server_proxy.read().clone() else { - return; - }; - - proxy.register_context_server(extension, server_id, cx) - } - - fn unregister_context_server(&self, server_id: Arc, cx: &mut App) { - let Some(proxy) = self.context_server_proxy.read().clone() else { - return; - }; - - proxy.unregister_context_server(server_id, cx) - } -} - -pub trait ExtensionDebugAdapterProviderProxy: Send + Sync + 'static { - fn register_debug_adapter( - &self, - extension: Arc, - debug_adapter_name: Arc, - schema_path: &Path, - ); - fn register_debug_locator(&self, extension: Arc, locator_name: Arc); - fn unregister_debug_adapter(&self, debug_adapter_name: Arc); - fn unregister_debug_locator(&self, locator_name: Arc); -} - -impl ExtensionDebugAdapterProviderProxy for ExtensionHostProxy { - fn register_debug_adapter( - &self, - extension: Arc, - debug_adapter_name: Arc, - schema_path: &Path, - ) { - let Some(proxy) = self.debug_adapter_provider_proxy.read().clone() else { - return; - }; - - proxy.register_debug_adapter(extension, debug_adapter_name, schema_path) - } - - fn register_debug_locator(&self, extension: Arc, locator_name: Arc) { - let Some(proxy) = self.debug_adapter_provider_proxy.read().clone() else { - return; - }; - - proxy.register_debug_locator(extension, locator_name) - } - fn unregister_debug_adapter(&self, debug_adapter_name: Arc) { - let Some(proxy) = self.debug_adapter_provider_proxy.read().clone() else { - return; - }; - - proxy.unregister_debug_adapter(debug_adapter_name) - } - fn unregister_debug_locator(&self, locator_name: Arc) { - let Some(proxy) = self.debug_adapter_provider_proxy.read().clone() else { - return; - }; - - proxy.unregister_debug_locator(locator_name) - } -} diff --git a/crates/extension/src/extension_manifest.rs b/crates/extension/src/extension_manifest.rs deleted file mode 100644 index 4ecdd378ca..0000000000 --- a/crates/extension/src/extension_manifest.rs +++ /dev/null @@ -1,522 +0,0 @@ -use anyhow::{Context as _, Result, anyhow, bail}; -use collections::{BTreeMap, HashMap}; -use fs::Fs; -use language::LanguageName; -use lsp::LanguageServerName; -use semver::Version; -use serde::{Deserialize, Serialize}; -use std::{ - ffi::OsStr, - fmt, - path::{Path, PathBuf}, - sync::Arc, -}; - -use crate::ExtensionCapability; - -/// This is the old version of the extension manifest, from when it was `extension.json`. -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -pub struct OldExtensionManifest { - pub name: String, - pub version: Arc, - - #[serde(default)] - pub description: Option, - #[serde(default)] - pub repository: Option, - #[serde(default)] - pub authors: Vec, - - #[serde(default)] - pub themes: BTreeMap, PathBuf>, - #[serde(default)] - pub languages: BTreeMap, PathBuf>, - #[serde(default)] - pub grammars: BTreeMap, PathBuf>, -} - -/// The schema version of the [`ExtensionManifest`]. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)] -pub struct SchemaVersion(pub i32); - -impl fmt::Display for SchemaVersion { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl SchemaVersion { - pub const ZERO: Self = Self(0); - - pub fn is_v0(&self) -> bool { - self == &Self::ZERO - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] -pub struct ExtensionManifest { - pub id: Arc, - pub name: String, - pub version: Arc, - pub schema_version: SchemaVersion, - - #[serde(default)] - pub description: Option, - #[serde(default)] - pub repository: Option, - #[serde(default)] - pub authors: Vec, - #[serde(default)] - pub lib: LibManifestEntry, - - #[serde(default)] - pub themes: Vec, - #[serde(default)] - pub icon_themes: Vec, - #[serde(default)] - pub languages: Vec, - #[serde(default)] - pub grammars: BTreeMap, GrammarManifestEntry>, - #[serde(default)] - pub language_servers: BTreeMap, - #[serde(default)] - pub context_servers: BTreeMap, ContextServerManifestEntry>, - #[serde(default)] - pub agent_servers: BTreeMap, AgentServerManifestEntry>, - #[serde(default)] - pub slash_commands: BTreeMap, SlashCommandManifestEntry>, - #[serde(default)] - pub snippets: Option, - #[serde(default)] - pub capabilities: Vec, - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub debug_adapters: BTreeMap, DebugAdapterManifestEntry>, - #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub debug_locators: BTreeMap, DebugLocatorManifestEntry>, -} - -impl ExtensionManifest { - pub fn allow_exec( - &self, - desired_command: &str, - desired_args: &[impl AsRef + std::fmt::Debug], - ) -> Result<()> { - let is_allowed = self.capabilities.iter().any(|capability| match capability { - ExtensionCapability::ProcessExec(capability) => { - capability.allows(desired_command, desired_args) - } - _ => false, - }); - - if !is_allowed { - bail!( - "capability for process:exec {desired_command} {desired_args:?} was not listed in the extension manifest", - ); - } - - Ok(()) - } - - pub fn allow_remote_load(&self) -> bool { - !self.language_servers.is_empty() - || !self.debug_adapters.is_empty() - || !self.debug_locators.is_empty() - } -} - -pub fn build_debug_adapter_schema_path( - adapter_name: &Arc, - meta: &DebugAdapterManifestEntry, -) -> PathBuf { - meta.schema_path.clone().unwrap_or_else(|| { - Path::new("debug_adapter_schemas") - .join(Path::new(adapter_name.as_ref()).with_extension("json")) - }) -} - -#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)] -pub struct LibManifestEntry { - pub kind: Option, - pub version: Option, -} - -#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] -pub struct AgentServerManifestEntry { - /// Display name for the agent (shown in menus). - pub name: String, - /// Environment variables to set when launching the agent server. - #[serde(default)] - pub env: HashMap, - /// Optional icon path (relative to extension root, e.g., "ai.svg"). - /// Should be a small SVG icon for display in menus. - #[serde(default)] - pub icon: Option, - /// Per-target configuration for archive-based installation. - /// The key format is "{os}-{arch}" where: - /// - os: "darwin" (macOS), "linux", "windows" - /// - arch: "aarch64" (arm64), "x86_64" - /// - /// Example: - /// ```toml - /// [agent_servers.myagent.targets.darwin-aarch64] - /// archive = "https://example.com/myagent-darwin-arm64.zip" - /// cmd = "./myagent" - /// args = ["--serve"] - /// sha256 = "abc123..." # optional - /// ``` - /// - /// For Node.js-based agents, you can use "node" as the cmd to automatically - /// use Zed's managed Node.js runtime instead of relying on the user's PATH: - /// ```toml - /// [agent_servers.nodeagent.targets.darwin-aarch64] - /// archive = "https://example.com/nodeagent.zip" - /// cmd = "node" - /// args = ["index.js", "--port", "3000"] - /// ``` - /// - /// Note: All commands are executed with the archive extraction directory as the - /// working directory, so relative paths in args (like "index.js") will resolve - /// relative to the extracted archive contents. - pub targets: HashMap, -} - -#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] -pub struct TargetConfig { - /// URL to download the archive from (e.g., "https://github.com/owner/repo/releases/download/v1.0.0/myagent-darwin-arm64.zip") - pub archive: String, - /// Command to run (e.g., "./myagent" or "./myagent.exe") - pub cmd: String, - /// Command-line arguments to pass to the agent server. - #[serde(default)] - pub args: Vec, - /// Optional SHA-256 hash of the archive for verification. - /// If not provided and the URL is a GitHub release, we'll attempt to fetch it from GitHub. - #[serde(default)] - pub sha256: Option, - /// Environment variables to set when launching the agent server. - /// These target-specific env vars will override any env vars set at the agent level. - #[serde(default)] - pub env: HashMap, -} - -impl TargetConfig { - pub fn from_proto(proto: proto::ExternalExtensionAgentTarget) -> Self { - Self { - archive: proto.archive, - cmd: proto.cmd, - args: proto.args, - sha256: proto.sha256, - env: proto.env.into_iter().collect(), - } - } - - pub fn to_proto(&self) -> proto::ExternalExtensionAgentTarget { - proto::ExternalExtensionAgentTarget { - archive: self.archive.clone(), - cmd: self.cmd.clone(), - args: self.args.clone(), - sha256: self.sha256.clone(), - env: self - .env - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect(), - } - } -} - -#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] -pub enum ExtensionLibraryKind { - Rust, -} - -#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)] -pub struct GrammarManifestEntry { - pub repository: String, - #[serde(alias = "commit")] - pub rev: String, - #[serde(default)] - pub path: Option, -} - -#[derive(Clone, Default, PartialEq, Eq, Debug, Deserialize, Serialize)] -pub struct LanguageServerManifestEntry { - /// Deprecated in favor of `languages`. - #[serde(default)] - language: Option, - /// The list of languages this language server should work with. - #[serde(default)] - languages: Vec, - #[serde(default)] - pub language_ids: HashMap, - #[serde(default)] - pub code_action_kinds: Option>, -} - -impl LanguageServerManifestEntry { - /// Returns the list of languages for the language server. - /// - /// Prefer this over accessing the `language` or `languages` fields directly, - /// as we currently support both. - /// - /// We can replace this with just field access for the `languages` field once - /// we have removed `language`. - pub fn languages(&self) -> impl IntoIterator + '_ { - let language = if self.languages.is_empty() { - self.language.clone() - } else { - None - }; - self.languages.iter().cloned().chain(language) - } -} - -#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] -pub struct ContextServerManifestEntry {} - -#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] -pub struct SlashCommandManifestEntry { - pub description: String, - pub requires_argument: bool, -} - -#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] -pub struct DebugAdapterManifestEntry { - pub schema_path: Option, -} - -#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] -pub struct DebugLocatorManifestEntry {} - -impl ExtensionManifest { - pub async fn load(fs: Arc, extension_dir: &Path) -> Result { - let extension_name = extension_dir - .file_name() - .and_then(OsStr::to_str) - .context("invalid extension name")?; - - let extension_manifest_path = extension_dir.join("extension.toml"); - if fs.is_file(&extension_manifest_path).await { - let manifest_content = fs.load(&extension_manifest_path).await.with_context(|| { - format!("loading {extension_name} extension.toml, {extension_manifest_path:?}") - })?; - toml::from_str(&manifest_content).map_err(|err| { - anyhow!("Invalid extension.toml for extension {extension_name}:\n{err}") - }) - } else if let extension_manifest_path = extension_manifest_path.with_extension("json") - && fs.is_file(&extension_manifest_path).await - { - let manifest_content = fs.load(&extension_manifest_path).await.with_context(|| { - format!("loading {extension_name} extension.json, {extension_manifest_path:?}") - })?; - - serde_json::from_str::(&manifest_content) - .with_context(|| format!("invalid extension.json for extension {extension_name}")) - .map(|manifest_json| manifest_from_old_manifest(manifest_json, extension_name)) - } else { - anyhow::bail!("No extension manifest found for extension {extension_name}") - } - } -} - -fn manifest_from_old_manifest( - manifest_json: OldExtensionManifest, - extension_id: &str, -) -> ExtensionManifest { - ExtensionManifest { - id: extension_id.into(), - name: manifest_json.name, - version: manifest_json.version, - description: manifest_json.description, - repository: manifest_json.repository, - authors: manifest_json.authors, - schema_version: SchemaVersion::ZERO, - lib: Default::default(), - themes: { - let mut themes = manifest_json.themes.into_values().collect::>(); - themes.sort(); - themes.dedup(); - themes - }, - icon_themes: Vec::new(), - languages: { - let mut languages = manifest_json.languages.into_values().collect::>(); - languages.sort(); - languages.dedup(); - languages - }, - grammars: manifest_json - .grammars - .into_keys() - .map(|grammar_name| (grammar_name, Default::default())) - .collect(), - language_servers: Default::default(), - context_servers: BTreeMap::default(), - agent_servers: BTreeMap::default(), - slash_commands: BTreeMap::default(), - snippets: None, - capabilities: Vec::new(), - debug_adapters: Default::default(), - debug_locators: Default::default(), - } -} - -#[cfg(test)] -mod tests { - use pretty_assertions::assert_eq; - - use crate::ProcessExecCapability; - - use super::*; - - fn extension_manifest() -> ExtensionManifest { - ExtensionManifest { - id: "test".into(), - name: "Test".to_string(), - version: "1.0.0".into(), - schema_version: SchemaVersion::ZERO, - description: None, - repository: None, - authors: vec![], - lib: Default::default(), - themes: vec![], - icon_themes: vec![], - languages: vec![], - grammars: BTreeMap::default(), - language_servers: BTreeMap::default(), - context_servers: BTreeMap::default(), - agent_servers: BTreeMap::default(), - slash_commands: BTreeMap::default(), - snippets: None, - capabilities: vec![], - debug_adapters: Default::default(), - debug_locators: Default::default(), - } - } - - #[test] - fn test_build_adapter_schema_path_with_schema_path() { - let adapter_name = Arc::from("my_adapter"); - let entry = DebugAdapterManifestEntry { - schema_path: Some(PathBuf::from("foo/bar")), - }; - - let path = build_debug_adapter_schema_path(&adapter_name, &entry); - assert_eq!(path, PathBuf::from("foo/bar")); - } - - #[test] - fn test_build_adapter_schema_path_without_schema_path() { - let adapter_name = Arc::from("my_adapter"); - let entry = DebugAdapterManifestEntry { schema_path: None }; - - let path = build_debug_adapter_schema_path(&adapter_name, &entry); - assert_eq!( - path, - PathBuf::from("debug_adapter_schemas").join("my_adapter.json") - ); - } - - #[test] - fn test_allow_exec_exact_match() { - let manifest = ExtensionManifest { - capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability { - command: "ls".to_string(), - args: vec!["-la".to_string()], - })], - ..extension_manifest() - }; - - assert!(manifest.allow_exec("ls", &["-la"]).is_ok()); - assert!(manifest.allow_exec("ls", &["-l"]).is_err()); - assert!(manifest.allow_exec("pwd", &[] as &[&str]).is_err()); - } - - #[test] - fn test_allow_exec_wildcard_arg() { - let manifest = ExtensionManifest { - capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability { - command: "git".to_string(), - args: vec!["*".to_string()], - })], - ..extension_manifest() - }; - - assert!(manifest.allow_exec("git", &["status"]).is_ok()); - assert!(manifest.allow_exec("git", &["commit"]).is_ok()); - assert!(manifest.allow_exec("git", &["status", "-s"]).is_err()); // too many args - assert!(manifest.allow_exec("npm", &["install"]).is_err()); // wrong command - } - - #[test] - fn test_allow_exec_double_wildcard() { - let manifest = ExtensionManifest { - capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability { - command: "cargo".to_string(), - args: vec!["test".to_string(), "**".to_string()], - })], - ..extension_manifest() - }; - - assert!(manifest.allow_exec("cargo", &["test"]).is_ok()); - assert!(manifest.allow_exec("cargo", &["test", "--all"]).is_ok()); - assert!( - manifest - .allow_exec("cargo", &["test", "--all", "--no-fail-fast"]) - .is_ok() - ); - assert!(manifest.allow_exec("cargo", &["build"]).is_err()); // wrong first arg - } - - #[test] - fn test_allow_exec_mixed_wildcards() { - let manifest = ExtensionManifest { - capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability { - command: "docker".to_string(), - args: vec!["run".to_string(), "*".to_string(), "**".to_string()], - })], - ..extension_manifest() - }; - - assert!(manifest.allow_exec("docker", &["run", "nginx"]).is_ok()); - assert!(manifest.allow_exec("docker", &["run"]).is_err()); - assert!( - manifest - .allow_exec("docker", &["run", "ubuntu", "bash"]) - .is_ok() - ); - assert!( - manifest - .allow_exec("docker", &["run", "alpine", "sh", "-c", "echo hello"]) - .is_ok() - ); - assert!(manifest.allow_exec("docker", &["ps"]).is_err()); // wrong first arg - } - #[test] - fn parse_manifest_with_agent_server_archive_launcher() { - let toml_src = r#" -id = "example.agent-server-ext" -name = "Agent Server Example" -version = "1.0.0" -schema_version = 0 - -[agent_servers.foo] -name = "Foo Agent" - -[agent_servers.foo.targets.linux-x86_64] -archive = "https://example.com/agent-linux-x64.tar.gz" -cmd = "./agent" -args = ["--serve"] -"#; - - let manifest: ExtensionManifest = toml::from_str(toml_src).expect("manifest should parse"); - assert_eq!(manifest.id.as_ref(), "example.agent-server-ext"); - assert!(manifest.agent_servers.contains_key("foo")); - let entry = manifest.agent_servers.get("foo").unwrap(); - assert!(entry.targets.contains_key("linux-x86_64")); - let target = entry.targets.get("linux-x86_64").unwrap(); - assert_eq!(target.archive, "https://example.com/agent-linux-x64.tar.gz"); - assert_eq!(target.cmd, "./agent"); - assert_eq!(target.args, vec!["--serve"]); - } -} diff --git a/crates/extension/src/types.rs b/crates/extension/src/types.rs deleted file mode 100644 index ed9eb2ec2f..0000000000 --- a/crates/extension/src/types.rs +++ /dev/null @@ -1,71 +0,0 @@ -mod context_server; -mod dap; -mod lsp; -mod slash_command; - -use std::{ops::Range, path::PathBuf}; - -use util::redact::should_redact; - -pub use context_server::*; -pub use dap::*; -pub use lsp::*; -pub use slash_command::*; - -/// A list of environment variables. -pub type EnvVars = Vec<(String, String)>; - -/// A command. -pub struct Command { - /// The command to execute. - pub command: PathBuf, - /// The arguments to pass to the command. - pub args: Vec, - /// The environment variables to set for the command. - pub env: EnvVars, -} - -impl std::fmt::Debug for Command { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let filtered_env = self - .env - .iter() - .map(|(k, v)| (k, if should_redact(k) { "[REDACTED]" } else { v })) - .collect::>(); - - f.debug_struct("Command") - .field("command", &self.command) - .field("args", &self.args) - .field("env", &filtered_env) - .finish() - } -} - -/// A label containing some code. -#[derive(Debug, Clone)] -pub struct CodeLabel { - /// The source code to parse with Tree-sitter. - pub code: String, - /// The spans to display in the label. - pub spans: Vec, - /// The range of the displayed label to include when filtering. - pub filter_range: Range, -} - -/// A span within a code label. -#[derive(Debug, Clone)] -pub enum CodeLabelSpan { - /// A range into the parsed code. - CodeRange(Range), - /// A span containing a code literal. - Literal(CodeLabelSpanLiteral), -} - -/// A span containing a code literal. -#[derive(Debug, Clone)] -pub struct CodeLabelSpanLiteral { - /// The literal text. - pub text: String, - /// The name of the highlight to use for this literal. - pub highlight_name: Option, -} diff --git a/crates/extension/src/types/context_server.rs b/crates/extension/src/types/context_server.rs deleted file mode 100644 index 2e3d20b047..0000000000 --- a/crates/extension/src/types/context_server.rs +++ /dev/null @@ -1,10 +0,0 @@ -/// Configuration for context server setup and installation. -#[derive(Debug, Clone)] -pub struct ContextServerConfiguration { - /// Installation instructions in Markdown format. - pub installation_instructions: String, - /// JSON schema for settings validation. - pub settings_schema: serde_json::Value, - /// Default settings template. - pub default_settings: String, -} diff --git a/crates/extension/src/types/dap.rs b/crates/extension/src/types/dap.rs deleted file mode 100644 index 5dcd7f57d5..0000000000 --- a/crates/extension/src/types/dap.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub use dap::{ - StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest, - adapters::{DebugAdapterBinary, DebugTaskDefinition, TcpArguments}, -}; -pub use task::{ - AttachRequest, BuildTaskDefinition, DebugRequest, DebugScenario, LaunchRequest, - TaskTemplate as BuildTaskTemplate, TcpArgumentsTemplate, -}; diff --git a/crates/extension/src/types/lsp.rs b/crates/extension/src/types/lsp.rs deleted file mode 100644 index 6e858211cd..0000000000 --- a/crates/extension/src/types/lsp.rs +++ /dev/null @@ -1,96 +0,0 @@ -use std::option::Option; - -/// An LSP completion. -#[derive(Debug, Clone)] -pub struct Completion { - pub label: String, - pub label_details: Option, - pub detail: Option, - pub kind: Option, - pub insert_text_format: Option, -} - -/// The kind of an LSP completion. -#[derive(Debug, Clone, Copy)] -pub enum CompletionKind { - Text, - Method, - Function, - Constructor, - Field, - Variable, - Class, - Interface, - Module, - Property, - Unit, - Value, - Enum, - Keyword, - Snippet, - Color, - File, - Reference, - Folder, - EnumMember, - Constant, - Struct, - Event, - Operator, - TypeParameter, - Other(i32), -} - -/// Label details for an LSP completion. -#[derive(Debug, Clone)] -pub struct CompletionLabelDetails { - pub detail: Option, - pub description: Option, -} - -/// Defines how to interpret the insert text in a completion item. -#[derive(Debug, Clone, Copy)] -pub enum InsertTextFormat { - PlainText, - Snippet, - Other(i32), -} - -/// An LSP symbol. -#[derive(Debug, Clone)] -pub struct Symbol { - pub kind: SymbolKind, - pub name: String, -} - -/// The kind of an LSP symbol. -#[derive(Debug, Clone, Copy)] -pub enum SymbolKind { - File, - Module, - Namespace, - Package, - Class, - Method, - Property, - Field, - Constructor, - Enum, - Interface, - Function, - Variable, - Constant, - String, - Number, - Boolean, - Array, - Object, - Key, - Null, - EnumMember, - Struct, - Event, - Operator, - TypeParameter, - Other(i32), -} diff --git a/crates/extension/src/types/slash_command.rs b/crates/extension/src/types/slash_command.rs deleted file mode 100644 index 0b937984a5..0000000000 --- a/crates/extension/src/types/slash_command.rs +++ /dev/null @@ -1,43 +0,0 @@ -use std::ops::Range; - -/// A slash command for use in the Assistant. -#[derive(Debug, Clone)] -pub struct SlashCommand { - /// The name of the slash command. - pub name: String, - /// The description of the slash command. - pub description: String, - /// The tooltip text to display for the run button. - pub tooltip_text: String, - /// Whether this slash command requires an argument. - pub requires_argument: bool, -} - -/// The output of a slash command. -#[derive(Debug, Clone)] -pub struct SlashCommandOutput { - /// The text produced by the slash command. - pub text: String, - /// The list of sections to show in the slash command placeholder. - pub sections: Vec, -} - -/// A section in the slash command output. -#[derive(Debug, Clone)] -pub struct SlashCommandOutputSection { - /// The range this section occupies. - pub range: Range, - /// The label to display in the placeholder for this section. - pub label: String, -} - -/// A completion for a slash command argument. -#[derive(Debug, Clone)] -pub struct SlashCommandArgumentCompletion { - /// The label to display for this completion. - pub label: String, - /// The new text that should be inserted into the command when this completion is accepted. - pub new_text: String, - /// Whether the command should be run when accepting this completion. - pub run_command: bool, -} diff --git a/crates/extension_cli/Cargo.toml b/crates/extension_cli/Cargo.toml deleted file mode 100644 index b2562a8e82..0000000000 --- a/crates/extension_cli/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "extension_cli" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[[bin]] -name = "zed-extension" -path = "src/main.rs" - -[dependencies] -anyhow.workspace = true -clap = { workspace = true, features = ["derive"] } -env_logger.workspace = true -extension.workspace = true -fs.workspace = true -gpui.workspace = true -language.workspace = true -log.workspace = true -reqwest_client.workspace = true -rpc.workspace = true -serde.workspace = true -serde_json.workspace = true -theme.workspace = true -tokio = { workspace = true, features = ["full"] } -toml.workspace = true -tree-sitter.workspace = true -wasmtime.workspace = true diff --git a/crates/extension_cli/LICENSE-GPL b/crates/extension_cli/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/extension_cli/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/extension_cli/src/main.rs b/crates/extension_cli/src/main.rs deleted file mode 100644 index 699a6b0143..0000000000 --- a/crates/extension_cli/src/main.rs +++ /dev/null @@ -1,421 +0,0 @@ -use std::collections::{BTreeSet, HashMap}; -use std::env; -use std::fs; -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -use ::fs::{CopyOptions, Fs, RealFs, copy_recursive}; -use anyhow::{Context as _, Result, bail}; -use clap::Parser; -use extension::ExtensionManifest; -use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder}; -use language::LanguageConfig; -use reqwest_client::ReqwestClient; -use rpc::ExtensionProvides; -use tokio::process::Command; -use tree_sitter::{Language, Query, WasmStore}; - -#[derive(Parser, Debug)] -#[command(name = "zed-extension")] -struct Args { - /// The path to the extension directory - #[arg(long)] - source_dir: PathBuf, - /// The output directory to place the packaged extension. - #[arg(long)] - output_dir: PathBuf, - /// The path to a directory where build dependencies are downloaded - #[arg(long)] - scratch_dir: PathBuf, -} - -#[tokio::main] -async fn main() -> Result<()> { - env_logger::init(); - - let args = Args::parse(); - let fs = Arc::new(RealFs::new(None, gpui::background_executor())); - let engine = wasmtime::Engine::default(); - let mut wasm_store = WasmStore::new(&engine)?; - - let extension_path = args - .source_dir - .canonicalize() - .context("failed to canonicalize source_dir")?; - let scratch_dir = args - .scratch_dir - .canonicalize() - .context("failed to canonicalize scratch_dir")?; - let output_dir = if args.output_dir.is_relative() { - env::current_dir()?.join(&args.output_dir) - } else { - args.output_dir - }; - - log::info!("loading extension manifest"); - let mut manifest = ExtensionManifest::load(fs.clone(), &extension_path).await?; - - log::info!("compiling extension"); - - let user_agent = format!( - "Zed Extension CLI/{} ({}; {})", - env!("CARGO_PKG_VERSION"), - std::env::consts::OS, - std::env::consts::ARCH - ); - let http_client = Arc::new(ReqwestClient::user_agent(&user_agent)?); - - let builder = ExtensionBuilder::new(http_client, scratch_dir); - builder - .compile_extension( - &extension_path, - &mut manifest, - CompileExtensionOptions { release: true }, - fs.clone(), - ) - .await - .context("failed to compile extension")?; - - let grammars = test_grammars(&manifest, &extension_path, &mut wasm_store)?; - test_languages(&manifest, &extension_path, &grammars)?; - test_themes(&manifest, &extension_path, fs.clone()).await?; - - let archive_dir = output_dir.join("archive"); - fs::remove_dir_all(&archive_dir).ok(); - copy_extension_resources(&manifest, &extension_path, &archive_dir, fs.clone()) - .await - .context("failed to copy extension resources")?; - - let tar_output = Command::new("tar") - .current_dir(&output_dir) - .args(["-czvf", "archive.tar.gz", "-C", "archive", "."]) - .output() - .await - .context("failed to run tar")?; - if !tar_output.status.success() { - bail!( - "failed to create archive.tar.gz: {}", - String::from_utf8_lossy(&tar_output.stderr) - ); - } - - let extension_provides = extension_provides(&manifest); - - let manifest_json = serde_json::to_string(&rpc::ExtensionApiManifest { - name: manifest.name, - version: manifest.version, - description: manifest.description, - authors: manifest.authors, - schema_version: Some(manifest.schema_version.0), - repository: manifest - .repository - .context("missing repository in extension manifest")?, - wasm_api_version: manifest.lib.version.map(|version| version.to_string()), - provides: extension_provides, - })?; - fs::remove_dir_all(&archive_dir)?; - fs::write(output_dir.join("manifest.json"), manifest_json.as_bytes())?; - - Ok(()) -} - -/// Returns the set of features provided by the extension. -fn extension_provides(manifest: &ExtensionManifest) -> BTreeSet { - let mut provides = BTreeSet::default(); - if !manifest.themes.is_empty() { - provides.insert(ExtensionProvides::Themes); - } - - if !manifest.icon_themes.is_empty() { - provides.insert(ExtensionProvides::IconThemes); - } - - if !manifest.languages.is_empty() { - provides.insert(ExtensionProvides::Languages); - } - - if !manifest.grammars.is_empty() { - provides.insert(ExtensionProvides::Grammars); - } - - if !manifest.language_servers.is_empty() { - provides.insert(ExtensionProvides::LanguageServers); - } - - if !manifest.context_servers.is_empty() { - provides.insert(ExtensionProvides::ContextServers); - } - - if !manifest.agent_servers.is_empty() { - provides.insert(ExtensionProvides::AgentServers); - } - - if manifest.snippets.is_some() { - provides.insert(ExtensionProvides::Snippets); - } - - if !manifest.debug_adapters.is_empty() { - provides.insert(ExtensionProvides::DebugAdapters); - } - - provides -} - -async fn copy_extension_resources( - manifest: &ExtensionManifest, - extension_path: &Path, - output_dir: &Path, - fs: Arc, -) -> Result<()> { - fs::create_dir_all(output_dir).context("failed to create output dir")?; - - let manifest_toml = toml::to_string(&manifest).context("failed to serialize manifest")?; - fs::write(output_dir.join("extension.toml"), &manifest_toml) - .context("failed to write extension.toml")?; - - if manifest.lib.kind.is_some() { - fs::copy( - extension_path.join("extension.wasm"), - output_dir.join("extension.wasm"), - ) - .context("failed to copy extension.wasm")?; - } - - if !manifest.grammars.is_empty() { - let source_grammars_dir = extension_path.join("grammars"); - let output_grammars_dir = output_dir.join("grammars"); - fs::create_dir_all(&output_grammars_dir)?; - for grammar_name in manifest.grammars.keys() { - let mut grammar_filename = PathBuf::from(grammar_name.as_ref()); - grammar_filename.set_extension("wasm"); - fs::copy( - source_grammars_dir.join(&grammar_filename), - output_grammars_dir.join(&grammar_filename), - ) - .with_context(|| format!("failed to copy grammar '{}'", grammar_filename.display()))?; - } - } - - if !manifest.themes.is_empty() { - let output_themes_dir = output_dir.join("themes"); - fs::create_dir_all(&output_themes_dir)?; - for theme_path in &manifest.themes { - fs::copy( - extension_path.join(theme_path), - output_themes_dir.join(theme_path.file_name().context("invalid theme path")?), - ) - .with_context(|| format!("failed to copy theme '{}'", theme_path.display()))?; - } - } - - if !manifest.icon_themes.is_empty() { - let output_icon_themes_dir = output_dir.join("icon_themes"); - fs::create_dir_all(&output_icon_themes_dir)?; - for icon_theme_path in &manifest.icon_themes { - fs::copy( - extension_path.join(icon_theme_path), - output_icon_themes_dir.join( - icon_theme_path - .file_name() - .context("invalid icon theme path")?, - ), - ) - .with_context(|| { - format!("failed to copy icon theme '{}'", icon_theme_path.display()) - })?; - } - - let output_icons_dir = output_dir.join("icons"); - fs::create_dir_all(&output_icons_dir)?; - copy_recursive( - fs.as_ref(), - &extension_path.join("icons"), - &output_icons_dir, - CopyOptions { - overwrite: true, - ignore_if_exists: false, - }, - ) - .await - .with_context(|| "failed to copy icons")?; - } - - for (_, agent_entry) in &manifest.agent_servers { - if let Some(icon_path) = &agent_entry.icon { - let source_icon = extension_path.join(icon_path); - let dest_icon = output_dir.join(icon_path); - - // Create parent directory if needed - if let Some(parent) = dest_icon.parent() { - fs::create_dir_all(parent)?; - } - - fs::copy(&source_icon, &dest_icon) - .with_context(|| format!("failed to copy agent server icon '{}'", icon_path))?; - } - } - - if !manifest.languages.is_empty() { - let output_languages_dir = output_dir.join("languages"); - fs::create_dir_all(&output_languages_dir)?; - for language_path in &manifest.languages { - copy_recursive( - fs.as_ref(), - &extension_path.join(language_path), - &output_languages_dir - .join(language_path.file_name().context("invalid language path")?), - CopyOptions { - overwrite: true, - ignore_if_exists: false, - }, - ) - .await - .with_context(|| { - format!("failed to copy language dir '{}'", language_path.display()) - })?; - } - } - - if !manifest.debug_adapters.is_empty() { - for (debug_adapter, entry) in &manifest.debug_adapters { - let schema_path = entry.schema_path.clone().unwrap_or_else(|| { - PathBuf::from("debug_adapter_schemas".to_owned()) - .join(debug_adapter.as_ref()) - .with_extension("json") - }); - let parent = schema_path - .parent() - .with_context(|| format!("invalid empty schema path for {debug_adapter}"))?; - fs::create_dir_all(output_dir.join(parent))?; - copy_recursive( - fs.as_ref(), - &extension_path.join(&schema_path), - &output_dir.join(&schema_path), - CopyOptions { - overwrite: true, - ignore_if_exists: false, - }, - ) - .await - .with_context(|| { - format!( - "failed to copy debug adapter schema '{}'", - schema_path.display() - ) - })?; - } - } - - if let Some(snippets_path) = manifest.snippets.as_ref() { - let parent = snippets_path.parent(); - if let Some(parent) = parent.filter(|p| p.components().next().is_some()) { - fs::create_dir_all(output_dir.join(parent))?; - } - copy_recursive( - fs.as_ref(), - &extension_path.join(&snippets_path), - &output_dir.join(&snippets_path), - CopyOptions { - overwrite: true, - ignore_if_exists: false, - }, - ) - .await - .with_context(|| format!("failed to copy snippets from '{}'", snippets_path.display()))?; - } - - Ok(()) -} - -fn test_grammars( - manifest: &ExtensionManifest, - extension_path: &Path, - wasm_store: &mut WasmStore, -) -> Result> { - let mut grammars = HashMap::default(); - let grammars_dir = extension_path.join("grammars"); - - for grammar_name in manifest.grammars.keys() { - let mut grammar_path = grammars_dir.join(grammar_name.as_ref()); - grammar_path.set_extension("wasm"); - - let wasm = fs::read(&grammar_path)?; - let language = wasm_store.load_language(grammar_name, &wasm)?; - log::info!("loaded grammar {grammar_name}"); - grammars.insert(grammar_name.to_string(), language); - } - - Ok(grammars) -} - -fn test_languages( - manifest: &ExtensionManifest, - extension_path: &Path, - grammars: &HashMap, -) -> Result<()> { - for relative_language_dir in &manifest.languages { - let language_dir = extension_path.join(relative_language_dir); - let config_path = language_dir.join("config.toml"); - let config_content = fs::read_to_string(&config_path)?; - let config: LanguageConfig = toml::from_str(&config_content)?; - let grammar = if let Some(name) = &config.grammar { - Some( - grammars - .get(name.as_ref()) - .with_context(|| format!("grammar not found: '{name}'"))?, - ) - } else { - None - }; - - let query_entries = fs::read_dir(&language_dir)?; - for entry in query_entries { - let entry = entry?; - let query_path = entry.path(); - if query_path.extension() == Some("scm".as_ref()) { - let grammar = grammar.with_context(|| { - format! { - "language {} provides query {} but no grammar", - config.name, - query_path.display() - } - })?; - - let query_source = fs::read_to_string(&query_path)?; - let _query = Query::new(grammar, &query_source)?; - } - } - - log::info!("loaded language {}", config.name); - } - - Ok(()) -} - -async fn test_themes( - manifest: &ExtensionManifest, - extension_path: &Path, - fs: Arc, -) -> Result<()> { - for relative_theme_path in &manifest.themes { - let theme_path = extension_path.join(relative_theme_path); - let theme_family = theme::read_user_theme(&theme_path, fs.clone()).await?; - log::info!("loaded theme family {}", theme_family.name); - - for theme in &theme_family.themes { - if theme - .style - .colors - .deprecated_scrollbar_thumb_background - .is_some() - { - bail!( - r#"Theme "{theme_name}" is using a deprecated style property: scrollbar_thumb.background. Use `scrollbar.thumb.background` instead."#, - theme_name = theme.name - ) - } - } - } - - Ok(()) -} diff --git a/crates/extension_host/Cargo.toml b/crates/extension_host/Cargo.toml deleted file mode 100644 index 328b808b13..0000000000 --- a/crates/extension_host/Cargo.toml +++ /dev/null @@ -1,73 +0,0 @@ -[package] -name = "extension_host" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/extension_host.rs" -doctest = false - -[features] -test-support = [] - -[dependencies] -anyhow.workspace = true -async-compression.workspace = true -async-tar.workspace = true -async-trait.workspace = true -client.workspace = true -collections.workspace = true -dap.workspace = true -extension.workspace = true -fs.workspace = true -futures.workspace = true -gpui.workspace = true -gpui_tokio.workspace = true -http_client.workspace = true -language.workspace = true -log.workspace = true -lsp.workspace = true -moka.workspace = true -node_runtime.workspace = true -paths.workspace = true -project.workspace = true -remote.workspace = true -release_channel.workspace = true -semver.workspace = true -serde.workspace = true -serde_json.workspace = true -serde_json_lenient.workspace = true -settings.workspace = true -task.workspace = true -telemetry.workspace = true -tempfile.workspace = true -toml.workspace = true -url.workspace = true -util.workspace = true -wasmparser.workspace = true -wasmtime-wasi.workspace = true -wasmtime.workspace = true - -[dev-dependencies] -criterion.workspace = true -ctor.workspace = true -fs = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -language = { workspace = true, features = ["test-support"] } -language_extension.workspace = true -parking_lot.workspace = true -project = { workspace = true, features = ["test-support"] } -rand.workspace = true -reqwest_client.workspace = true -theme = { workspace = true, features = ["test-support"] } -theme_extension.workspace = true -zlog.workspace = true - -[[bench]] -name = "extension_compilation_benchmark" -harness = false diff --git a/crates/extension_host/LICENSE-GPL b/crates/extension_host/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/extension_host/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/extension_host/benches/extension_compilation_benchmark.rs b/crates/extension_host/benches/extension_compilation_benchmark.rs deleted file mode 100644 index a28f617dc3..0000000000 --- a/crates/extension_host/benches/extension_compilation_benchmark.rs +++ /dev/null @@ -1,155 +0,0 @@ -use std::{collections::BTreeMap, path::PathBuf, sync::Arc}; - -use criterion::{BatchSize, BenchmarkId, Criterion, criterion_group, criterion_main}; -use extension::{ - ExtensionCapability, ExtensionHostProxy, ExtensionLibraryKind, ExtensionManifest, - LanguageServerManifestEntry, LibManifestEntry, SchemaVersion, - extension_builder::{CompileExtensionOptions, ExtensionBuilder}, -}; -use extension_host::wasm_host::WasmHost; -use fs::{Fs, RealFs}; -use gpui::{TestAppContext, TestDispatcher}; -use http_client::{FakeHttpClient, Response}; -use node_runtime::NodeRuntime; -use rand::{SeedableRng, rngs::StdRng}; -use reqwest_client::ReqwestClient; -use serde_json::json; -use settings::SettingsStore; -use util::test::TempTree; - -fn extension_benchmarks(c: &mut Criterion) { - let cx = init(); - cx.update(gpui_tokio::init); - - let mut group = c.benchmark_group("load"); - - let mut manifest = manifest(); - let wasm_bytes = wasm_bytes( - &cx, - &mut manifest, - Arc::new(RealFs::new(None, cx.executor())), - ); - let manifest = Arc::new(manifest); - let extensions_dir = TempTree::new(json!({ - "installed": {}, - "work": {} - })); - let wasm_host = wasm_host(&cx, &extensions_dir); - - group.bench_function(BenchmarkId::from_parameter(1), |b| { - b.iter_batched( - || wasm_bytes.clone(), - |wasm_bytes| { - let _extension = cx - .executor() - .block(wasm_host.load_extension(wasm_bytes, &manifest, &cx.to_async())) - .unwrap(); - }, - BatchSize::SmallInput, - ); - }); -} - -fn init() -> TestAppContext { - const SEED: u64 = 9999; - let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(SEED)); - let cx = TestAppContext::build(dispatcher, None); - cx.executor().allow_parking(); - cx.update(|cx| { - let store = SettingsStore::test(cx); - cx.set_global(store); - release_channel::init(semver::Version::new(0, 0, 0), cx); - }); - - cx -} - -fn wasm_bytes(cx: &TestAppContext, manifest: &mut ExtensionManifest, fs: Arc) -> Vec { - let extension_builder = extension_builder(); - let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) - .parent() - .unwrap() - .parent() - .unwrap() - .join("extensions/test-extension"); - cx.executor() - .block(extension_builder.compile_extension( - &path, - manifest, - CompileExtensionOptions { release: true }, - fs, - )) - .unwrap(); - std::fs::read(path.join("extension.wasm")).unwrap() -} - -fn extension_builder() -> ExtensionBuilder { - let user_agent = format!( - "Zed Extension CLI/{} ({}; {})", - env!("CARGO_PKG_VERSION"), - std::env::consts::OS, - std::env::consts::ARCH - ); - let http_client = Arc::new(ReqwestClient::user_agent(&user_agent).unwrap()); - // Local dir so that we don't have to download it on every run - let build_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("benches/.build"); - ExtensionBuilder::new(http_client, build_dir) -} - -fn wasm_host(cx: &TestAppContext, extensions_dir: &TempTree) -> Arc { - let http_client = FakeHttpClient::create(async |_| { - Ok(Response::builder().status(404).body("not found".into())?) - }); - let extensions_dir = extensions_dir.path().canonicalize().unwrap(); - let work_dir = extensions_dir.join("work"); - let fs = Arc::new(RealFs::new(None, cx.executor())); - - cx.update(|cx| { - WasmHost::new( - fs, - http_client, - NodeRuntime::unavailable(), - Arc::new(ExtensionHostProxy::new()), - work_dir, - cx, - ) - }) -} - -fn manifest() -> ExtensionManifest { - ExtensionManifest { - id: "test-extension".into(), - name: "Test Extension".into(), - version: "0.1.0".into(), - schema_version: SchemaVersion(1), - description: Some("An extension for use in tests.".into()), - authors: Vec::new(), - repository: None, - themes: Default::default(), - icon_themes: Vec::new(), - lib: LibManifestEntry { - kind: Some(ExtensionLibraryKind::Rust), - version: Some(semver::Version::new(0, 1, 0)), - }, - languages: Vec::new(), - grammars: BTreeMap::default(), - language_servers: [("gleam".into(), LanguageServerManifestEntry::default())] - .into_iter() - .collect(), - context_servers: BTreeMap::default(), - agent_servers: BTreeMap::default(), - slash_commands: BTreeMap::default(), - snippets: None, - capabilities: vec![ExtensionCapability::ProcessExec( - extension::ProcessExecCapability { - command: "echo".into(), - args: vec!["hello!".into()], - }, - )], - debug_adapters: Default::default(), - debug_locators: Default::default(), - } -} - -criterion_group!(benches, extension_benchmarks); -criterion_main!(benches); diff --git a/crates/extension_host/build.rs b/crates/extension_host/build.rs deleted file mode 100644 index f2c2b19998..0000000000 --- a/crates/extension_host/build.rs +++ /dev/null @@ -1,46 +0,0 @@ -use std::env; -use std::fs; -use std::path::PathBuf; - -fn main() -> Result<(), Box> { - copy_extension_api_rust_files() -} - -/// rust-analyzer doesn't support include! for files from outside the crate. -/// Copy them to the OUT_DIR, so we can include them from there, which is supported. -fn copy_extension_api_rust_files() -> Result<(), Box> { - let out_dir = env::var("OUT_DIR")?; - let input_dir = PathBuf::from("../extension_api/wit"); - let output_dir = PathBuf::from(out_dir); - - println!("cargo:rerun-if-changed={}", input_dir.display()); - - for entry in fs::read_dir(&input_dir)? { - let entry = entry?; - let path = entry.path(); - if path.is_dir() { - println!("cargo:rerun-if-changed={}", path.display()); - - for subentry in fs::read_dir(&path)? { - let subentry = subentry?; - let subpath = subentry.path(); - if subpath.extension() == Some(std::ffi::OsStr::new("rs")) { - let relative_path = subpath.strip_prefix(&input_dir)?; - let destination = output_dir.join(relative_path); - - fs::create_dir_all(destination.parent().unwrap())?; - fs::copy(&subpath, &destination)?; - } - } - } else if path.extension() == Some(std::ffi::OsStr::new("rs")) { - let relative_path = path.strip_prefix(&input_dir)?; - let destination = output_dir.join(relative_path); - - fs::create_dir_all(destination.parent().unwrap())?; - fs::copy(&path, &destination)?; - println!("cargo:rerun-if-changed={}", path.display()); - } - } - - Ok(()) -} diff --git a/crates/extension_host/src/capability_granter.rs b/crates/extension_host/src/capability_granter.rs deleted file mode 100644 index 9f27b5e480..0000000000 --- a/crates/extension_host/src/capability_granter.rs +++ /dev/null @@ -1,153 +0,0 @@ -use std::sync::Arc; - -use anyhow::{Result, bail}; -use extension::{ExtensionCapability, ExtensionManifest}; -use url::Url; - -pub struct CapabilityGranter { - granted_capabilities: Vec, - manifest: Arc, -} - -impl CapabilityGranter { - pub fn new( - granted_capabilities: Vec, - manifest: Arc, - ) -> Self { - Self { - granted_capabilities, - manifest, - } - } - - pub fn grant_exec( - &self, - desired_command: &str, - desired_args: &[impl AsRef + std::fmt::Debug], - ) -> Result<()> { - self.manifest.allow_exec(desired_command, desired_args)?; - - let is_allowed = self - .granted_capabilities - .iter() - .any(|capability| match capability { - ExtensionCapability::ProcessExec(capability) => { - capability.allows(desired_command, desired_args) - } - _ => false, - }); - - if !is_allowed { - bail!( - "capability for process:exec {desired_command} {desired_args:?} is not granted by the extension host", - ); - } - - Ok(()) - } - - pub fn grant_download_file(&self, desired_url: &Url) -> Result<()> { - let is_allowed = self - .granted_capabilities - .iter() - .any(|capability| match capability { - ExtensionCapability::DownloadFile(capability) => capability.allows(desired_url), - _ => false, - }); - - if !is_allowed { - bail!( - "capability for download_file {desired_url} is not granted by the extension host", - ); - } - - Ok(()) - } - - pub fn grant_npm_install_package(&self, package_name: &str) -> Result<()> { - let is_allowed = self - .granted_capabilities - .iter() - .any(|capability| match capability { - ExtensionCapability::NpmInstallPackage(capability) => { - capability.allows(package_name) - } - _ => false, - }); - - if !is_allowed { - bail!("capability for npm:install {package_name} is not granted by the extension host",); - } - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use std::collections::BTreeMap; - - use extension::{ProcessExecCapability, SchemaVersion}; - - use super::*; - - fn extension_manifest() -> ExtensionManifest { - ExtensionManifest { - id: "test".into(), - name: "Test".to_string(), - version: "1.0.0".into(), - schema_version: SchemaVersion::ZERO, - description: None, - repository: None, - authors: vec![], - lib: Default::default(), - themes: vec![], - icon_themes: vec![], - languages: vec![], - grammars: BTreeMap::default(), - language_servers: BTreeMap::default(), - context_servers: BTreeMap::default(), - agent_servers: BTreeMap::default(), - slash_commands: BTreeMap::default(), - snippets: None, - capabilities: vec![], - debug_adapters: Default::default(), - debug_locators: Default::default(), - } - } - - #[test] - fn test_grant_exec() { - let manifest = Arc::new(ExtensionManifest { - capabilities: vec![ExtensionCapability::ProcessExec(ProcessExecCapability { - command: "ls".to_string(), - args: vec!["-la".to_string()], - })], - ..extension_manifest() - }); - - // It returns an error when the extension host has no granted capabilities. - let granter = CapabilityGranter::new(Vec::new(), manifest.clone()); - assert!(granter.grant_exec("ls", &["-la"]).is_err()); - - // It succeeds when the extension host has the exact capability. - let granter = CapabilityGranter::new( - vec![ExtensionCapability::ProcessExec(ProcessExecCapability { - command: "ls".to_string(), - args: vec!["-la".to_string()], - })], - manifest.clone(), - ); - assert!(granter.grant_exec("ls", &["-la"]).is_ok()); - - // It succeeds when the extension host has a wildcard capability. - let granter = CapabilityGranter::new( - vec![ExtensionCapability::ProcessExec(ProcessExecCapability { - command: "*".to_string(), - args: vec!["**".to_string()], - })], - manifest, - ); - assert!(granter.grant_exec("ls", &["-la"]).is_ok()); - } -} diff --git a/crates/extension_host/src/extension_host.rs b/crates/extension_host/src/extension_host.rs deleted file mode 100644 index 09e8259771..0000000000 --- a/crates/extension_host/src/extension_host.rs +++ /dev/null @@ -1,1878 +0,0 @@ -mod capability_granter; -pub mod extension_settings; -pub mod headless_host; -pub mod wasm_host; - -#[cfg(test)] -mod extension_store_test; - -use anyhow::{Context as _, Result, anyhow, bail}; -use async_compression::futures::bufread::GzipDecoder; -use async_tar::Archive; -use client::ExtensionProvides; -use client::{Client, ExtensionMetadata, GetExtensionsResponse, proto, telemetry::Telemetry}; -use collections::{BTreeMap, BTreeSet, HashSet, btree_map}; -pub use extension::ExtensionManifest; -use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder}; -use extension::{ - ExtensionContextServerProxy, ExtensionDebugAdapterProviderProxy, ExtensionEvents, - ExtensionGrammarProxy, ExtensionHostProxy, ExtensionLanguageProxy, - ExtensionLanguageServerProxy, ExtensionSlashCommandProxy, ExtensionSnippetProxy, - ExtensionThemeProxy, -}; -use fs::{Fs, RemoveOptions}; -use futures::future::join_all; -use futures::{ - AsyncReadExt as _, Future, FutureExt as _, StreamExt as _, - channel::{ - mpsc::{UnboundedSender, unbounded}, - oneshot, - }, - io::BufReader, - select_biased, -}; -use gpui::{ - App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Global, Task, WeakEntity, - actions, -}; -use http_client::{AsyncBody, HttpClient, HttpClientWithUrl}; -use language::{ - LanguageConfig, LanguageMatcher, LanguageName, LanguageQueries, LoadedLanguage, - QUERY_FILENAME_PREFIXES, Rope, -}; -use node_runtime::NodeRuntime; -use project::ContextProviderWithTasks; -use release_channel::ReleaseChannel; -use remote::RemoteClient; -use semver::Version; -use serde::{Deserialize, Serialize}; -use settings::Settings; -use std::ops::RangeInclusive; -use std::str::FromStr; -use std::{ - cmp::Ordering, - path::{self, Path, PathBuf}, - sync::Arc, - time::{Duration, Instant}, -}; -use url::Url; -use util::{ResultExt, paths::RemotePathBuf}; -use wasm_host::{ - WasmExtension, WasmHost, - wit::{is_supported_wasm_api_version, wasm_api_version_range}, -}; - -pub use extension::{ - ExtensionLibraryKind, GrammarManifestEntry, OldExtensionManifest, SchemaVersion, -}; -pub use extension_settings::ExtensionSettings; - -pub const RELOAD_DEBOUNCE_DURATION: Duration = Duration::from_millis(200); -const FS_WATCH_LATENCY: Duration = Duration::from_millis(100); - -/// The current extension [`SchemaVersion`] supported by Zed. -const CURRENT_SCHEMA_VERSION: SchemaVersion = SchemaVersion(1); - -/// Extensions that should no longer be loaded or downloaded. -/// -/// These snippets should no longer be downloaded or loaded, because their -/// functionality has been integrated into the core editor. -const SUPPRESSED_EXTENSIONS: &[&str] = &["snippets", "ruff", "ty", "basedpyright"]; - -/// Returns the [`SchemaVersion`] range that is compatible with this version of Zed. -pub fn schema_version_range() -> RangeInclusive { - SchemaVersion::ZERO..=CURRENT_SCHEMA_VERSION -} - -/// Returns whether the given extension version is compatible with this version of Zed. -pub fn is_version_compatible( - release_channel: ReleaseChannel, - extension_version: &ExtensionMetadata, -) -> bool { - let schema_version = extension_version.manifest.schema_version.unwrap_or(0); - if CURRENT_SCHEMA_VERSION.0 < schema_version { - return false; - } - - if let Some(wasm_api_version) = extension_version - .manifest - .wasm_api_version - .as_ref() - .and_then(|wasm_api_version| Version::from_str(wasm_api_version).ok()) - && !is_supported_wasm_api_version(release_channel, wasm_api_version) - { - return false; - } - - true -} - -pub struct ExtensionStore { - pub proxy: Arc, - pub builder: Arc, - pub extension_index: ExtensionIndex, - pub fs: Arc, - pub http_client: Arc, - pub telemetry: Option>, - pub reload_tx: UnboundedSender>>, - pub reload_complete_senders: Vec>, - pub installed_dir: PathBuf, - pub outstanding_operations: BTreeMap, ExtensionOperation>, - pub index_path: PathBuf, - pub modified_extensions: HashSet>, - pub wasm_host: Arc, - pub wasm_extensions: Vec<(Arc, WasmExtension)>, - pub tasks: Vec>, - pub remote_clients: Vec>, - pub ssh_registered_tx: UnboundedSender<()>, -} - -#[derive(Clone, Copy)] -pub enum ExtensionOperation { - Upgrade, - Install, - Remove, -} - -#[derive(Clone)] -pub enum Event { - ExtensionsUpdated, - StartedReloading, - ExtensionInstalled(Arc), - ExtensionUninstalled(Arc), - ExtensionFailedToLoad(Arc), -} - -impl EventEmitter for ExtensionStore {} - -struct GlobalExtensionStore(Entity); - -impl Global for GlobalExtensionStore {} - -#[derive(Debug, Deserialize, Serialize, Default, PartialEq, Eq)] -pub struct ExtensionIndex { - pub extensions: BTreeMap, ExtensionIndexEntry>, - pub themes: BTreeMap, ExtensionIndexThemeEntry>, - #[serde(default)] - pub icon_themes: BTreeMap, ExtensionIndexIconThemeEntry>, - pub languages: BTreeMap, -} - -#[derive(Clone, PartialEq, Eq, Debug, Deserialize, Serialize)] -pub struct ExtensionIndexEntry { - pub manifest: Arc, - pub dev: bool, -} - -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)] -pub struct ExtensionIndexThemeEntry { - pub extension: Arc, - pub path: PathBuf, -} - -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)] -pub struct ExtensionIndexIconThemeEntry { - pub extension: Arc, - pub path: PathBuf, -} - -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Deserialize, Serialize)] -pub struct ExtensionIndexLanguageEntry { - pub extension: Arc, - pub path: PathBuf, - pub matcher: LanguageMatcher, - pub hidden: bool, - pub grammar: Option>, -} - -actions!( - zed, - [ - /// Reloads all installed extensions. - ReloadExtensions - ] -); - -pub fn init( - extension_host_proxy: Arc, - fs: Arc, - client: Arc, - node_runtime: NodeRuntime, - cx: &mut App, -) { - let store = cx.new(move |cx| { - ExtensionStore::new( - paths::extensions_dir().clone(), - None, - extension_host_proxy, - fs, - client.http_client(), - client.http_client(), - Some(client.telemetry().clone()), - node_runtime, - cx, - ) - }); - - cx.on_action(|_: &ReloadExtensions, cx| { - let store = cx.global::().0.clone(); - store.update(cx, |store, cx| drop(store.reload(None, cx))); - }); - - cx.set_global(GlobalExtensionStore(store)); -} - -impl ExtensionStore { - pub fn try_global(cx: &App) -> Option> { - cx.try_global::() - .map(|store| store.0.clone()) - } - - pub fn global(cx: &App) -> Entity { - cx.global::().0.clone() - } - - pub fn new( - extensions_dir: PathBuf, - build_dir: Option, - extension_host_proxy: Arc, - fs: Arc, - http_client: Arc, - builder_client: Arc, - telemetry: Option>, - node_runtime: NodeRuntime, - cx: &mut Context, - ) -> Self { - let work_dir = extensions_dir.join("work"); - let build_dir = build_dir.unwrap_or_else(|| extensions_dir.join("build")); - let installed_dir = extensions_dir.join("installed"); - let index_path = extensions_dir.join("index.json"); - - let (reload_tx, mut reload_rx) = unbounded(); - let (connection_registered_tx, mut connection_registered_rx) = unbounded(); - let mut this = Self { - proxy: extension_host_proxy.clone(), - extension_index: Default::default(), - installed_dir, - index_path, - builder: Arc::new(ExtensionBuilder::new(builder_client, build_dir)), - outstanding_operations: Default::default(), - modified_extensions: Default::default(), - reload_complete_senders: Vec::new(), - wasm_host: WasmHost::new( - fs.clone(), - http_client.clone(), - node_runtime, - extension_host_proxy, - work_dir, - cx, - ), - wasm_extensions: Vec::new(), - fs, - http_client, - telemetry, - reload_tx, - tasks: Vec::new(), - - remote_clients: Default::default(), - ssh_registered_tx: connection_registered_tx, - }; - - // The extensions store maintains an index file, which contains a complete - // list of the installed extensions and the resources that they provide. - // This index is loaded synchronously on startup. - let (index_content, index_metadata, extensions_metadata) = - cx.background_executor().block(async { - futures::join!( - this.fs.load(&this.index_path), - this.fs.metadata(&this.index_path), - this.fs.metadata(&this.installed_dir), - ) - }); - - // Normally, there is no need to rebuild the index. But if the index file - // is invalid or is out-of-date according to the filesystem mtimes, then - // it must be asynchronously rebuilt. - let mut extension_index = ExtensionIndex::default(); - let mut extension_index_needs_rebuild = true; - if let Ok(index_content) = index_content - && let Some(index) = serde_json::from_str(&index_content).log_err() - { - extension_index = index; - if let (Ok(Some(index_metadata)), Ok(Some(extensions_metadata))) = - (index_metadata, extensions_metadata) - && index_metadata - .mtime - .bad_is_greater_than(extensions_metadata.mtime) - { - extension_index_needs_rebuild = false; - } - } - - // Immediately load all of the extensions in the initial manifest. If the - // index needs to be rebuild, then enqueue - let load_initial_extensions = this.extensions_updated(extension_index, cx); - let mut reload_future = None; - if extension_index_needs_rebuild { - reload_future = Some(this.reload(None, cx)); - } - - cx.spawn(async move |this, cx| { - if let Some(future) = reload_future { - future.await; - } - this.update(cx, |this, cx| this.auto_install_extensions(cx)) - .ok(); - this.update(cx, |this, cx| this.check_for_updates(cx)).ok(); - }) - .detach(); - - // Perform all extension loading in a single task to ensure that we - // never attempt to simultaneously load/unload extensions from multiple - // parallel tasks. - this.tasks.push(cx.spawn(async move |this, cx| { - async move { - load_initial_extensions.await; - - let mut index_changed = false; - let mut debounce_timer = cx.background_spawn(futures::future::pending()).fuse(); - loop { - select_biased! { - _ = debounce_timer => { - if index_changed { - let index = this - .update(cx, |this, cx| this.rebuild_extension_index(cx))? - .await; - this.update(cx, |this, cx| this.extensions_updated(index, cx))? - .await; - index_changed = false; - } - - Self::update_remote_clients(&this, cx).await?; - } - _ = connection_registered_rx.next() => { - debounce_timer = cx - .background_executor() - .timer(RELOAD_DEBOUNCE_DURATION) - .fuse(); - } - extension_id = reload_rx.next() => { - let Some(extension_id) = extension_id else { break; }; - this.update(cx, |this, _| { - this.modified_extensions.extend(extension_id); - })?; - index_changed = true; - debounce_timer = cx - .background_executor() - .timer(RELOAD_DEBOUNCE_DURATION) - .fuse(); - } - } - } - - anyhow::Ok(()) - } - .map(drop) - .await; - })); - - // Watch the installed extensions directory for changes. Whenever changes are - // detected, rebuild the extension index, and load/unload any extensions that - // have been added, removed, or modified. - this.tasks.push(cx.background_spawn({ - let fs = this.fs.clone(); - let reload_tx = this.reload_tx.clone(); - let installed_dir = this.installed_dir.clone(); - async move { - let (mut paths, _) = fs.watch(&installed_dir, FS_WATCH_LATENCY).await; - while let Some(events) = paths.next().await { - for event in events { - let Ok(event_path) = event.path.strip_prefix(&installed_dir) else { - continue; - }; - - if let Some(path::Component::Normal(extension_dir_name)) = - event_path.components().next() - && let Some(extension_id) = extension_dir_name.to_str() - { - reload_tx.unbounded_send(Some(extension_id.into())).ok(); - } - } - } - } - })); - - this - } - - pub fn reload( - &mut self, - modified_extension: Option>, - cx: &mut Context, - ) -> impl Future + use<> { - let (tx, rx) = oneshot::channel(); - self.reload_complete_senders.push(tx); - self.reload_tx - .unbounded_send(modified_extension) - .expect("reload task exited"); - cx.emit(Event::StartedReloading); - - async move { - rx.await.ok(); - } - } - - fn extensions_dir(&self) -> PathBuf { - self.installed_dir.clone() - } - - pub fn outstanding_operations(&self) -> &BTreeMap, ExtensionOperation> { - &self.outstanding_operations - } - - pub fn installed_extensions(&self) -> &BTreeMap, ExtensionIndexEntry> { - &self.extension_index.extensions - } - - pub fn dev_extensions(&self) -> impl Iterator> { - self.extension_index - .extensions - .values() - .filter_map(|extension| extension.dev.then_some(&extension.manifest)) - } - - pub fn extension_manifest_for_id(&self, extension_id: &str) -> Option<&Arc> { - self.extension_index - .extensions - .get(extension_id) - .map(|extension| &extension.manifest) - } - - /// Returns the names of themes provided by extensions. - pub fn extension_themes<'a>( - &'a self, - extension_id: &'a str, - ) -> impl Iterator> { - self.extension_index - .themes - .iter() - .filter_map(|(name, theme)| theme.extension.as_ref().eq(extension_id).then_some(name)) - } - - /// Returns the path to the theme file within an extension, if there is an - /// extension that provides the theme. - pub fn path_to_extension_theme(&self, theme_name: &str) -> Option { - let entry = self.extension_index.themes.get(theme_name)?; - - Some( - self.extensions_dir() - .join(entry.extension.as_ref()) - .join(&entry.path), - ) - } - - /// Returns the names of icon themes provided by extensions. - pub fn extension_icon_themes<'a>( - &'a self, - extension_id: &'a str, - ) -> impl Iterator> { - self.extension_index - .icon_themes - .iter() - .filter_map(|(name, icon_theme)| { - icon_theme - .extension - .as_ref() - .eq(extension_id) - .then_some(name) - }) - } - - /// Returns the path to the icon theme file within an extension, if there is - /// an extension that provides the icon theme. - pub fn path_to_extension_icon_theme( - &self, - icon_theme_name: &str, - ) -> Option<(PathBuf, PathBuf)> { - let entry = self.extension_index.icon_themes.get(icon_theme_name)?; - - let icon_theme_path = self - .extensions_dir() - .join(entry.extension.as_ref()) - .join(&entry.path); - let icons_root_path = self.extensions_dir().join(entry.extension.as_ref()); - - Some((icon_theme_path, icons_root_path)) - } - - pub fn fetch_extensions( - &self, - search: Option<&str>, - provides_filter: Option<&BTreeSet>, - cx: &mut Context, - ) -> Task>> { - let version = CURRENT_SCHEMA_VERSION.to_string(); - let mut query = vec![("max_schema_version", version.as_str())]; - if let Some(search) = search { - query.push(("filter", search)); - } - - let provides_filter = provides_filter.map(|provides_filter| { - provides_filter - .iter() - .map(|provides| provides.to_string()) - .collect::>() - .join(",") - }); - if let Some(provides_filter) = provides_filter.as_deref() { - query.push(("provides", provides_filter)); - } - - self.fetch_extensions_from_api("/extensions", &query, cx) - } - - pub fn fetch_extensions_with_update_available( - &mut self, - cx: &mut Context, - ) -> Task>> { - let schema_versions = schema_version_range(); - let wasm_api_versions = wasm_api_version_range(ReleaseChannel::global(cx)); - let extension_settings = ExtensionSettings::get_global(cx); - let extension_ids = self - .extension_index - .extensions - .iter() - .filter(|(id, entry)| !entry.dev && extension_settings.should_auto_update(id)) - .map(|(id, _)| id.as_ref()) - .collect::>() - .join(","); - let task = self.fetch_extensions_from_api( - "/extensions/updates", - &[ - ("min_schema_version", &schema_versions.start().to_string()), - ("max_schema_version", &schema_versions.end().to_string()), - ( - "min_wasm_api_version", - &wasm_api_versions.start().to_string(), - ), - ("max_wasm_api_version", &wasm_api_versions.end().to_string()), - ("ids", &extension_ids), - ], - cx, - ); - cx.spawn(async move |this, cx| { - let extensions = task.await?; - this.update(cx, |this, _cx| { - extensions - .into_iter() - .filter(|extension| { - this.extension_index - .extensions - .get(&extension.id) - .is_none_or(|installed_extension| { - installed_extension.manifest.version != extension.manifest.version - }) - }) - .collect() - }) - }) - } - - pub fn fetch_extension_versions( - &self, - extension_id: &str, - cx: &mut Context, - ) -> Task>> { - self.fetch_extensions_from_api(&format!("/extensions/{extension_id}"), &[], cx) - } - - /// Installs any extensions that should be included with Zed by default. - /// - /// This can be used to make certain functionality provided by extensions - /// available out-of-the-box. - pub fn auto_install_extensions(&mut self, cx: &mut Context) { - if cfg!(test) { - return; - } - - let extension_settings = ExtensionSettings::get_global(cx); - - let extensions_to_install = extension_settings - .auto_install_extensions - .keys() - .filter(|extension_id| extension_settings.should_auto_install(extension_id)) - .filter(|extension_id| { - let is_already_installed = self - .extension_index - .extensions - .contains_key(extension_id.as_ref()); - !is_already_installed && !SUPPRESSED_EXTENSIONS.contains(&extension_id.as_ref()) - }) - .cloned() - .collect::>(); - - cx.spawn(async move |this, cx| { - for extension_id in extensions_to_install { - this.update(cx, |this, cx| { - this.install_latest_extension(extension_id.clone(), cx); - }) - .ok(); - } - }) - .detach(); - } - - pub fn check_for_updates(&mut self, cx: &mut Context) { - let task = self.fetch_extensions_with_update_available(cx); - cx.spawn(async move |this, cx| Self::upgrade_extensions(this, task.await?, cx).await) - .detach(); - } - - async fn upgrade_extensions( - this: WeakEntity, - extensions: Vec, - cx: &mut AsyncApp, - ) -> Result<()> { - for extension in extensions { - let task = this.update(cx, |this, cx| { - if let Some(installed_extension) = - this.extension_index.extensions.get(&extension.id) - { - let installed_version = - Version::from_str(&installed_extension.manifest.version).ok()?; - let latest_version = Version::from_str(&extension.manifest.version).ok()?; - - if installed_version >= latest_version { - return None; - } - } - - Some(this.upgrade_extension(extension.id, extension.manifest.version, cx)) - })?; - - if let Some(task) = task { - task.await.log_err(); - } - } - anyhow::Ok(()) - } - - fn fetch_extensions_from_api( - &self, - path: &str, - query: &[(&str, &str)], - cx: &mut Context, - ) -> Task>> { - let url = self.http_client.build_zed_api_url(path, query); - let http_client = self.http_client.clone(); - cx.spawn(async move |_, _| { - let mut response = http_client - .get(url?.as_ref(), AsyncBody::empty(), true) - .await?; - - let mut body = Vec::new(); - response - .body_mut() - .read_to_end(&mut body) - .await - .context("error reading extensions")?; - - if response.status().is_client_error() { - let text = String::from_utf8_lossy(body.as_slice()); - bail!( - "status error {}, response: {text:?}", - response.status().as_u16() - ); - } - - let mut response: GetExtensionsResponse = serde_json::from_slice(&body)?; - - response - .data - .retain(|extension| !SUPPRESSED_EXTENSIONS.contains(&extension.id.as_ref())); - - Ok(response.data) - }) - } - - pub fn install_extension( - &mut self, - extension_id: Arc, - version: Arc, - cx: &mut Context, - ) { - self.install_or_upgrade_extension(extension_id, version, ExtensionOperation::Install, cx) - .detach_and_log_err(cx); - } - - fn install_or_upgrade_extension_at_endpoint( - &mut self, - extension_id: Arc, - url: Url, - operation: ExtensionOperation, - cx: &mut Context, - ) -> Task> { - let extension_dir = self.installed_dir.join(extension_id.as_ref()); - let http_client = self.http_client.clone(); - let fs = self.fs.clone(); - - match self.outstanding_operations.entry(extension_id.clone()) { - btree_map::Entry::Occupied(_) => return Task::ready(Ok(())), - btree_map::Entry::Vacant(e) => e.insert(operation), - }; - cx.notify(); - - cx.spawn(async move |this, cx| { - let _finish = cx.on_drop(&this, { - let extension_id = extension_id.clone(); - move |this, cx| { - this.outstanding_operations.remove(extension_id.as_ref()); - cx.notify(); - } - }); - - let mut response = http_client - .get(url.as_ref(), Default::default(), true) - .await - .context("downloading extension")?; - - fs.remove_dir( - &extension_dir, - RemoveOptions { - recursive: true, - ignore_if_not_exists: true, - }, - ) - .await?; - - let content_length = response - .headers() - .get(http_client::http::header::CONTENT_LENGTH) - .and_then(|value| value.to_str().ok()?.parse::().ok()); - - let mut body = BufReader::new(response.body_mut()); - let mut tar_gz_bytes = Vec::new(); - body.read_to_end(&mut tar_gz_bytes).await?; - - if let Some(content_length) = content_length { - let actual_len = tar_gz_bytes.len(); - if content_length != actual_len { - bail!(concat!( - "downloaded extension size {actual_len} ", - "does not match content length {content_length}" - )); - } - } - let decompressed_bytes = GzipDecoder::new(BufReader::new(tar_gz_bytes.as_slice())); - let archive = Archive::new(decompressed_bytes); - archive.unpack(extension_dir).await?; - this.update(cx, |this, cx| this.reload(Some(extension_id.clone()), cx))? - .await; - - if let ExtensionOperation::Install = operation { - this.update(cx, |this, cx| { - cx.emit(Event::ExtensionInstalled(extension_id.clone())); - if let Some(events) = ExtensionEvents::try_global(cx) - && let Some(manifest) = this.extension_manifest_for_id(&extension_id) - { - events.update(cx, |this, cx| { - this.emit(extension::Event::ExtensionInstalled(manifest.clone()), cx) - }); - } - }) - .ok(); - } - - anyhow::Ok(()) - }) - } - - pub fn install_latest_extension(&mut self, extension_id: Arc, cx: &mut Context) { - log::info!("installing extension {extension_id} latest version"); - - let schema_versions = schema_version_range(); - let wasm_api_versions = wasm_api_version_range(ReleaseChannel::global(cx)); - - let Some(url) = self - .http_client - .build_zed_api_url( - &format!("/extensions/{extension_id}/download"), - &[ - ("min_schema_version", &schema_versions.start().to_string()), - ("max_schema_version", &schema_versions.end().to_string()), - ( - "min_wasm_api_version", - &wasm_api_versions.start().to_string(), - ), - ("max_wasm_api_version", &wasm_api_versions.end().to_string()), - ], - ) - .log_err() - else { - return; - }; - - self.install_or_upgrade_extension_at_endpoint( - extension_id, - url, - ExtensionOperation::Install, - cx, - ) - .detach_and_log_err(cx); - } - - pub fn upgrade_extension( - &mut self, - extension_id: Arc, - version: Arc, - cx: &mut Context, - ) -> Task> { - self.install_or_upgrade_extension(extension_id, version, ExtensionOperation::Upgrade, cx) - } - - fn install_or_upgrade_extension( - &mut self, - extension_id: Arc, - version: Arc, - operation: ExtensionOperation, - cx: &mut Context, - ) -> Task> { - log::info!("installing extension {extension_id} {version}"); - let Some(url) = self - .http_client - .build_zed_api_url( - &format!("/extensions/{extension_id}/{version}/download"), - &[], - ) - .log_err() - else { - return Task::ready(Ok(())); - }; - - self.install_or_upgrade_extension_at_endpoint(extension_id, url, operation, cx) - } - - pub fn uninstall_extension( - &mut self, - extension_id: Arc, - cx: &mut Context, - ) -> Task> { - let extension_dir = self.installed_dir.join(extension_id.as_ref()); - let work_dir = self.wasm_host.work_dir.join(extension_id.as_ref()); - let fs = self.fs.clone(); - - let extension_manifest = self.extension_manifest_for_id(&extension_id).cloned(); - - match self.outstanding_operations.entry(extension_id.clone()) { - btree_map::Entry::Occupied(_) => return Task::ready(Ok(())), - btree_map::Entry::Vacant(e) => e.insert(ExtensionOperation::Remove), - }; - - cx.spawn(async move |extension_store, cx| { - let _finish = cx.on_drop(&extension_store, { - let extension_id = extension_id.clone(); - move |this, cx| { - this.outstanding_operations.remove(extension_id.as_ref()); - cx.notify(); - } - }); - - fs.remove_dir( - &extension_dir, - RemoveOptions { - recursive: true, - ignore_if_not_exists: true, - }, - ) - .await - .with_context(|| format!("Removing extension dir {extension_dir:?}"))?; - - extension_store - .update(cx, |extension_store, cx| extension_store.reload(None, cx))? - .await; - - // There's a race between wasm extension fully stopping and the directory removal. - // On Windows, it's impossible to remove a directory that has a process running in it. - for i in 0..3 { - cx.background_executor() - .timer(Duration::from_millis(i * 100)) - .await; - let removal_result = fs - .remove_dir( - &work_dir, - RemoveOptions { - recursive: true, - ignore_if_not_exists: true, - }, - ) - .await; - match removal_result { - Ok(()) => break, - Err(e) => { - if i == 2 { - log::error!("Failed to remove extension work dir {work_dir:?} : {e}"); - } - } - } - } - - extension_store.update(cx, |_, cx| { - cx.emit(Event::ExtensionUninstalled(extension_id.clone())); - if let Some(events) = ExtensionEvents::try_global(cx) - && let Some(manifest) = extension_manifest - { - events.update(cx, |this, cx| { - this.emit(extension::Event::ExtensionUninstalled(manifest.clone()), cx) - }); - } - })?; - - anyhow::Ok(()) - }) - } - - pub fn install_dev_extension( - &mut self, - extension_source_path: PathBuf, - cx: &mut Context, - ) -> Task> { - let extensions_dir = self.extensions_dir(); - let fs = self.fs.clone(); - let builder = self.builder.clone(); - - cx.spawn(async move |this, cx| { - let mut extension_manifest = - ExtensionManifest::load(fs.clone(), &extension_source_path).await?; - let extension_id = extension_manifest.id.clone(); - - if let Some(uninstall_task) = this - .update(cx, |this, cx| { - this.extension_index - .extensions - .get(extension_id.as_ref()) - .is_some_and(|index_entry| !index_entry.dev) - .then(|| this.uninstall_extension(extension_id.clone(), cx)) - }) - .ok() - .flatten() - { - uninstall_task.await.log_err(); - } - - if !this.update(cx, |this, cx| { - match this.outstanding_operations.entry(extension_id.clone()) { - btree_map::Entry::Occupied(_) => return false, - btree_map::Entry::Vacant(e) => e.insert(ExtensionOperation::Install), - }; - cx.notify(); - true - })? { - return Ok(()); - } - - let _finish = cx.on_drop(&this, { - let extension_id = extension_id.clone(); - move |this, cx| { - this.outstanding_operations.remove(extension_id.as_ref()); - cx.notify(); - } - }); - - cx.background_spawn({ - let extension_source_path = extension_source_path.clone(); - let fs = fs.clone(); - async move { - builder - .compile_extension( - &extension_source_path, - &mut extension_manifest, - CompileExtensionOptions { release: false }, - fs, - ) - .await - } - }) - .await - .inspect_err(|error| { - util::log_err(error); - })?; - - let output_path = &extensions_dir.join(extension_id.as_ref()); - if let Some(metadata) = fs.metadata(output_path).await? { - if metadata.is_symlink { - fs.remove_file( - output_path, - RemoveOptions { - recursive: false, - ignore_if_not_exists: true, - }, - ) - .await?; - } else { - bail!("extension {extension_id} is still installed"); - } - } - - fs.create_symlink(output_path, extension_source_path) - .await?; - - this.update(cx, |this, cx| this.reload(None, cx))?.await; - this.update(cx, |this, cx| { - cx.emit(Event::ExtensionInstalled(extension_id.clone())); - if let Some(events) = ExtensionEvents::try_global(cx) - && let Some(manifest) = this.extension_manifest_for_id(&extension_id) - { - events.update(cx, |this, cx| { - this.emit(extension::Event::ExtensionInstalled(manifest.clone()), cx) - }); - } - })?; - - Ok(()) - }) - } - - pub fn rebuild_dev_extension(&mut self, extension_id: Arc, cx: &mut Context) { - let path = self.installed_dir.join(extension_id.as_ref()); - let builder = self.builder.clone(); - let fs = self.fs.clone(); - - match self.outstanding_operations.entry(extension_id.clone()) { - btree_map::Entry::Occupied(_) => return, - btree_map::Entry::Vacant(e) => e.insert(ExtensionOperation::Upgrade), - }; - - cx.notify(); - let compile = cx.background_spawn(async move { - let mut manifest = ExtensionManifest::load(fs.clone(), &path).await?; - builder - .compile_extension( - &path, - &mut manifest, - CompileExtensionOptions { release: true }, - fs, - ) - .await - }); - - cx.spawn(async move |this, cx| { - let result = compile.await; - - this.update(cx, |this, cx| { - this.outstanding_operations.remove(&extension_id); - cx.notify(); - })?; - - if result.is_ok() { - this.update(cx, |this, cx| this.reload(Some(extension_id), cx))? - .await; - } - - result - }) - .detach_and_log_err(cx) - } - - /// Updates the set of installed extensions. - /// - /// First, this unloads any themes, languages, or grammars that are - /// no longer in the manifest, or whose files have changed on disk. - /// Then it loads any themes, languages, or grammars that are newly - /// added to the manifest, or whose files have changed on disk. - fn extensions_updated( - &mut self, - mut new_index: ExtensionIndex, - cx: &mut Context, - ) -> Task<()> { - let old_index = &self.extension_index; - - new_index - .extensions - .retain(|extension_id, _| !SUPPRESSED_EXTENSIONS.contains(&extension_id.as_ref())); - - // Determine which extensions need to be loaded and unloaded, based - // on the changes to the manifest and the extensions that we know have been - // modified. - let mut extensions_to_unload = Vec::default(); - let mut extensions_to_load = Vec::default(); - { - let mut old_keys = old_index.extensions.iter().peekable(); - let mut new_keys = new_index.extensions.iter().peekable(); - loop { - match (old_keys.peek(), new_keys.peek()) { - (None, None) => break, - (None, Some(_)) => { - extensions_to_load.push(new_keys.next().unwrap().0.clone()); - } - (Some(_), None) => { - extensions_to_unload.push(old_keys.next().unwrap().0.clone()); - } - (Some((old_key, _)), Some((new_key, _))) => match old_key.cmp(new_key) { - Ordering::Equal => { - let (old_key, old_value) = old_keys.next().unwrap(); - let (new_key, new_value) = new_keys.next().unwrap(); - if old_value != new_value || self.modified_extensions.contains(old_key) - { - extensions_to_unload.push(old_key.clone()); - extensions_to_load.push(new_key.clone()); - } - } - Ordering::Less => { - extensions_to_unload.push(old_keys.next().unwrap().0.clone()); - } - Ordering::Greater => { - extensions_to_load.push(new_keys.next().unwrap().0.clone()); - } - }, - } - } - self.modified_extensions.clear(); - } - - if extensions_to_load.is_empty() && extensions_to_unload.is_empty() { - self.reload_complete_senders.clear(); - return Task::ready(()); - } - - let reload_count = extensions_to_unload - .iter() - .filter(|id| extensions_to_load.contains(id)) - .count(); - - log::info!( - "extensions updated. loading {}, reloading {}, unloading {}", - extensions_to_load.len() - reload_count, - reload_count, - extensions_to_unload.len() - reload_count - ); - - let extension_ids = extensions_to_load - .iter() - .filter_map(|id| { - Some(( - id.clone(), - new_index.extensions.get(id)?.manifest.version.clone(), - )) - }) - .collect::>(); - - telemetry::event!("Extensions Loaded", id_and_versions = extension_ids); - - let themes_to_remove = old_index - .themes - .iter() - .filter_map(|(name, entry)| { - if extensions_to_unload.contains(&entry.extension) { - Some(name.clone().into()) - } else { - None - } - }) - .collect::>(); - let icon_themes_to_remove = old_index - .icon_themes - .iter() - .filter_map(|(name, entry)| { - if extensions_to_unload.contains(&entry.extension) { - Some(name.clone().into()) - } else { - None - } - }) - .collect::>(); - let languages_to_remove = old_index - .languages - .iter() - .filter_map(|(name, entry)| { - if extensions_to_unload.contains(&entry.extension) { - Some(name.clone()) - } else { - None - } - }) - .collect::>(); - let mut grammars_to_remove = Vec::new(); - let mut server_removal_tasks = Vec::with_capacity(extensions_to_unload.len()); - for extension_id in &extensions_to_unload { - let Some(extension) = old_index.extensions.get(extension_id) else { - continue; - }; - grammars_to_remove.extend(extension.manifest.grammars.keys().cloned()); - for (language_server_name, config) in &extension.manifest.language_servers { - for language in config.languages() { - server_removal_tasks.push(self.proxy.remove_language_server( - &language, - language_server_name, - cx, - )); - } - } - - for server_id in extension.manifest.context_servers.keys() { - self.proxy.unregister_context_server(server_id.clone(), cx); - } - for adapter in extension.manifest.debug_adapters.keys() { - self.proxy.unregister_debug_adapter(adapter.clone()); - } - for locator in extension.manifest.debug_locators.keys() { - self.proxy.unregister_debug_locator(locator.clone()); - } - for command_name in extension.manifest.slash_commands.keys() { - self.proxy.unregister_slash_command(command_name.clone()); - } - } - - self.wasm_extensions - .retain(|(extension, _)| !extensions_to_unload.contains(&extension.id)); - self.proxy.remove_user_themes(themes_to_remove); - self.proxy.remove_icon_themes(icon_themes_to_remove); - self.proxy - .remove_languages(&languages_to_remove, &grammars_to_remove); - - let mut grammars_to_add = Vec::new(); - let mut themes_to_add = Vec::new(); - let mut icon_themes_to_add = Vec::new(); - let mut snippets_to_add = Vec::new(); - for extension_id in &extensions_to_load { - let Some(extension) = new_index.extensions.get(extension_id) else { - continue; - }; - - grammars_to_add.extend(extension.manifest.grammars.keys().map(|grammar_name| { - let mut grammar_path = self.installed_dir.clone(); - grammar_path.extend([extension_id.as_ref(), "grammars"]); - grammar_path.push(grammar_name.as_ref()); - grammar_path.set_extension("wasm"); - (grammar_name.clone(), grammar_path) - })); - themes_to_add.extend(extension.manifest.themes.iter().map(|theme_path| { - let mut path = self.installed_dir.clone(); - path.extend([Path::new(extension_id.as_ref()), theme_path.as_path()]); - path - })); - icon_themes_to_add.extend(extension.manifest.icon_themes.iter().map( - |icon_theme_path| { - let mut path = self.installed_dir.clone(); - path.extend([Path::new(extension_id.as_ref()), icon_theme_path.as_path()]); - - let mut icons_root_path = self.installed_dir.clone(); - icons_root_path.extend([Path::new(extension_id.as_ref())]); - - (path, icons_root_path) - }, - )); - snippets_to_add.extend(extension.manifest.snippets.iter().map(|snippets_path| { - let mut path = self.installed_dir.clone(); - path.extend([Path::new(extension_id.as_ref()), snippets_path.as_path()]); - path - })); - } - - self.proxy.register_grammars(grammars_to_add); - let languages_to_add = new_index - .languages - .iter() - .filter(|(_, entry)| extensions_to_load.contains(&entry.extension)) - .collect::>(); - for (language_name, language) in languages_to_add { - let mut language_path = self.installed_dir.clone(); - language_path.extend([ - Path::new(language.extension.as_ref()), - language.path.as_path(), - ]); - self.proxy.register_language( - language_name.clone(), - language.grammar.clone(), - language.matcher.clone(), - language.hidden, - Arc::new(move || { - let config = std::fs::read_to_string(language_path.join("config.toml"))?; - let config: LanguageConfig = ::toml::from_str(&config)?; - let queries = load_plugin_queries(&language_path); - let context_provider = - std::fs::read_to_string(language_path.join("tasks.json")) - .ok() - .and_then(|contents| { - let definitions = - serde_json_lenient::from_str(&contents).log_err()?; - Some(Arc::new(ContextProviderWithTasks::new(definitions)) as Arc<_>) - }); - - Ok(LoadedLanguage { - config, - queries, - context_provider, - toolchain_provider: None, - manifest_name: None, - }) - }), - ); - } - - let fs = self.fs.clone(); - let wasm_host = self.wasm_host.clone(); - let root_dir = self.installed_dir.clone(); - let proxy = self.proxy.clone(); - let extension_entries = extensions_to_load - .iter() - .filter_map(|name| new_index.extensions.get(name).cloned()) - .collect::>(); - self.extension_index = new_index; - cx.notify(); - cx.emit(Event::ExtensionsUpdated); - - cx.spawn(async move |this, cx| { - cx.background_spawn({ - let fs = fs.clone(); - async move { - let _ = join_all(server_removal_tasks).await; - for theme_path in themes_to_add { - proxy - .load_user_theme(theme_path, fs.clone()) - .await - .log_err(); - } - - for (icon_theme_path, icons_root_path) in icon_themes_to_add { - proxy - .load_icon_theme(icon_theme_path, icons_root_path, fs.clone()) - .await - .log_err(); - } - - for snippets_path in &snippets_to_add { - match fs - .load(snippets_path) - .await - .with_context(|| format!("Loading snippets from {snippets_path:?}")) - { - Ok(snippets_contents) => { - proxy - .register_snippet(snippets_path, &snippets_contents) - .log_err(); - } - Err(e) => log::error!("Cannot load snippets: {e:#}"), - } - } - } - }) - .await; - - let mut wasm_extensions = Vec::new(); - for extension in extension_entries { - if extension.manifest.lib.kind.is_none() { - continue; - }; - - let extension_path = root_dir.join(extension.manifest.id.as_ref()); - let wasm_extension = WasmExtension::load( - &extension_path, - &extension.manifest, - wasm_host.clone(), - cx, - ) - .await - .with_context(|| format!("Loading extension from {extension_path:?}")); - - match wasm_extension { - Ok(wasm_extension) => { - wasm_extensions.push((extension.manifest.clone(), wasm_extension)) - } - Err(e) => { - log::error!( - "Failed to load extension: {}, {:#}", - extension.manifest.id, - e - ); - this.update(cx, |_, cx| { - cx.emit(Event::ExtensionFailedToLoad(extension.manifest.id.clone())) - }) - .ok(); - } - } - } - - this.update(cx, |this, cx| { - this.reload_complete_senders.clear(); - - for (manifest, wasm_extension) in &wasm_extensions { - let extension = Arc::new(wasm_extension.clone()); - - for (language_server_id, language_server_config) in &manifest.language_servers { - for language in language_server_config.languages() { - this.proxy.register_language_server( - extension.clone(), - language_server_id.clone(), - language.clone(), - ); - } - } - - for (slash_command_name, slash_command) in &manifest.slash_commands { - this.proxy.register_slash_command( - extension.clone(), - extension::SlashCommand { - name: slash_command_name.to_string(), - description: slash_command.description.to_string(), - // We don't currently expose this as a configurable option, as it currently drives - // the `menu_text` on the `SlashCommand` trait, which is not used for slash commands - // defined in extensions, as they are not able to be added to the menu. - tooltip_text: String::new(), - requires_argument: slash_command.requires_argument, - }, - ); - } - - for id in manifest.context_servers.keys() { - this.proxy - .register_context_server(extension.clone(), id.clone(), cx); - } - - for (debug_adapter, meta) in &manifest.debug_adapters { - let mut path = root_dir.clone(); - path.push(Path::new(manifest.id.as_ref())); - if let Some(schema_path) = &meta.schema_path { - path.push(schema_path); - } else { - path.push("debug_adapter_schemas"); - path.push(Path::new(debug_adapter.as_ref()).with_extension("json")); - } - - this.proxy.register_debug_adapter( - extension.clone(), - debug_adapter.clone(), - &path, - ); - } - - for debug_adapter in manifest.debug_locators.keys() { - this.proxy - .register_debug_locator(extension.clone(), debug_adapter.clone()); - } - } - - this.wasm_extensions.extend(wasm_extensions); - this.proxy.set_extensions_loaded(); - this.proxy.reload_current_theme(cx); - this.proxy.reload_current_icon_theme(cx); - - if let Some(events) = ExtensionEvents::try_global(cx) { - events.update(cx, |this, cx| { - this.emit(extension::Event::ExtensionsInstalledChanged, cx) - }); - } - }) - .ok(); - }) - } - - fn rebuild_extension_index(&self, cx: &mut Context) -> Task { - let fs = self.fs.clone(); - let work_dir = self.wasm_host.work_dir.clone(); - let extensions_dir = self.installed_dir.clone(); - let index_path = self.index_path.clone(); - let proxy = self.proxy.clone(); - cx.background_spawn(async move { - let start_time = Instant::now(); - let mut index = ExtensionIndex::default(); - - fs.create_dir(&work_dir).await.log_err(); - fs.create_dir(&extensions_dir).await.log_err(); - - let extension_paths = fs.read_dir(&extensions_dir).await; - if let Ok(mut extension_paths) = extension_paths { - while let Some(extension_dir) = extension_paths.next().await { - let Ok(extension_dir) = extension_dir else { - continue; - }; - - if extension_dir - .file_name() - .is_some_and(|file_name| file_name == ".DS_Store") - { - continue; - } - - Self::add_extension_to_index( - fs.clone(), - extension_dir, - &mut index, - proxy.clone(), - ) - .await - .log_err(); - } - } - - if let Ok(index_json) = serde_json::to_string_pretty(&index) { - fs.save(&index_path, &index_json.as_str().into(), Default::default()) - .await - .context("failed to save extension index") - .log_err(); - } - - log::info!("rebuilt extension index in {:?}", start_time.elapsed()); - index - }) - } - - async fn add_extension_to_index( - fs: Arc, - extension_dir: PathBuf, - index: &mut ExtensionIndex, - proxy: Arc, - ) -> Result<()> { - let mut extension_manifest = ExtensionManifest::load(fs.clone(), &extension_dir).await?; - let extension_id = extension_manifest.id.clone(); - - if SUPPRESSED_EXTENSIONS.contains(&extension_id.as_ref()) { - return Ok(()); - } - - // TODO: distinguish dev extensions more explicitly, by the absence - // of a checksum file that we'll create when downloading normal extensions. - let is_dev = fs - .metadata(&extension_dir) - .await? - .context("directory does not exist")? - .is_symlink; - - if let Ok(mut language_paths) = fs.read_dir(&extension_dir.join("languages")).await { - while let Some(language_path) = language_paths.next().await { - let language_path = language_path?; - let Ok(relative_path) = language_path.strip_prefix(&extension_dir) else { - continue; - }; - let Ok(Some(fs_metadata)) = fs.metadata(&language_path).await else { - continue; - }; - if !fs_metadata.is_dir { - continue; - } - let config = fs.load(&language_path.join("config.toml")).await?; - let config = ::toml::from_str::(&config)?; - - let relative_path = relative_path.to_path_buf(); - if !extension_manifest.languages.contains(&relative_path) { - extension_manifest.languages.push(relative_path.clone()); - } - - index.languages.insert( - config.name.clone(), - ExtensionIndexLanguageEntry { - extension: extension_id.clone(), - path: relative_path, - matcher: config.matcher, - hidden: config.hidden, - grammar: config.grammar, - }, - ); - } - } - - if let Ok(mut theme_paths) = fs.read_dir(&extension_dir.join("themes")).await { - while let Some(theme_path) = theme_paths.next().await { - let theme_path = theme_path?; - let Ok(relative_path) = theme_path.strip_prefix(&extension_dir) else { - continue; - }; - - let Some(theme_families) = proxy - .list_theme_names(theme_path.clone(), fs.clone()) - .await - .log_err() - else { - continue; - }; - - let relative_path = relative_path.to_path_buf(); - if !extension_manifest.themes.contains(&relative_path) { - extension_manifest.themes.push(relative_path.clone()); - } - - for theme_name in theme_families { - index.themes.insert( - theme_name.into(), - ExtensionIndexThemeEntry { - extension: extension_id.clone(), - path: relative_path.clone(), - }, - ); - } - } - } - - if let Ok(mut icon_theme_paths) = fs.read_dir(&extension_dir.join("icon_themes")).await { - while let Some(icon_theme_path) = icon_theme_paths.next().await { - let icon_theme_path = icon_theme_path?; - let Ok(relative_path) = icon_theme_path.strip_prefix(&extension_dir) else { - continue; - }; - - let Some(icon_theme_families) = proxy - .list_icon_theme_names(icon_theme_path.clone(), fs.clone()) - .await - .log_err() - else { - continue; - }; - - let relative_path = relative_path.to_path_buf(); - if !extension_manifest.icon_themes.contains(&relative_path) { - extension_manifest.icon_themes.push(relative_path.clone()); - } - - for icon_theme_name in icon_theme_families { - index.icon_themes.insert( - icon_theme_name.into(), - ExtensionIndexIconThemeEntry { - extension: extension_id.clone(), - path: relative_path.clone(), - }, - ); - } - } - } - - let extension_wasm_path = extension_dir.join("extension.wasm"); - if fs.is_file(&extension_wasm_path).await { - extension_manifest - .lib - .kind - .get_or_insert(ExtensionLibraryKind::Rust); - } - - index.extensions.insert( - extension_id.clone(), - ExtensionIndexEntry { - dev: is_dev, - manifest: Arc::new(extension_manifest), - }, - ); - - Ok(()) - } - - fn prepare_remote_extension( - &mut self, - extension_id: Arc, - is_dev: bool, - tmp_dir: PathBuf, - cx: &mut Context, - ) -> Task> { - let src_dir = self.extensions_dir().join(extension_id.as_ref()); - let Some(loaded_extension) = self.extension_index.extensions.get(&extension_id).cloned() - else { - return Task::ready(Err(anyhow!("extension no longer installed"))); - }; - let fs = self.fs.clone(); - cx.background_spawn(async move { - const EXTENSION_TOML: &str = "extension.toml"; - const EXTENSION_WASM: &str = "extension.wasm"; - const CONFIG_TOML: &str = "config.toml"; - - if is_dev { - let manifest_toml = toml::to_string(&loaded_extension.manifest)?; - fs.save( - &tmp_dir.join(EXTENSION_TOML), - &Rope::from(manifest_toml), - language::LineEnding::Unix, - ) - .await?; - } else { - fs.copy_file( - &src_dir.join(EXTENSION_TOML), - &tmp_dir.join(EXTENSION_TOML), - fs::CopyOptions::default(), - ) - .await? - } - - if fs.is_file(&src_dir.join(EXTENSION_WASM)).await { - fs.copy_file( - &src_dir.join(EXTENSION_WASM), - &tmp_dir.join(EXTENSION_WASM), - fs::CopyOptions::default(), - ) - .await? - } - - for language_path in loaded_extension.manifest.languages.iter() { - if fs - .is_file(&src_dir.join(language_path).join(CONFIG_TOML)) - .await - { - fs.create_dir(&tmp_dir.join(language_path)).await?; - fs.copy_file( - &src_dir.join(language_path).join(CONFIG_TOML), - &tmp_dir.join(language_path).join(CONFIG_TOML), - fs::CopyOptions::default(), - ) - .await? - } - } - - for (adapter_name, meta) in loaded_extension.manifest.debug_adapters.iter() { - let schema_path = &extension::build_debug_adapter_schema_path(adapter_name, meta); - - if fs.is_file(&src_dir.join(schema_path)).await { - if let Some(parent) = schema_path.parent() { - fs.create_dir(&tmp_dir.join(parent)).await? - } - fs.copy_file( - &src_dir.join(schema_path), - &tmp_dir.join(schema_path), - fs::CopyOptions::default(), - ) - .await? - } - } - - Ok(()) - }) - } - - async fn sync_extensions_to_remotes( - this: &WeakEntity, - client: WeakEntity, - cx: &mut AsyncApp, - ) -> Result<()> { - let extensions = this.update(cx, |this, _cx| { - this.extension_index - .extensions - .iter() - .filter_map(|(id, entry)| { - if !entry.manifest.allow_remote_load() { - return None; - } - Some(proto::Extension { - id: id.to_string(), - version: entry.manifest.version.to_string(), - dev: entry.dev, - }) - }) - .collect() - })?; - - let response = client - .update(cx, |client, _cx| { - client - .proto_client() - .request(proto::SyncExtensions { extensions }) - })? - .await?; - let path_style = client.read_with(cx, |client, _| client.path_style())?; - - for missing_extension in response.missing_extensions.into_iter() { - let tmp_dir = tempfile::tempdir()?; - this.update(cx, |this, cx| { - this.prepare_remote_extension( - missing_extension.id.clone().into(), - missing_extension.dev, - tmp_dir.path().to_owned(), - cx, - ) - })? - .await?; - let dest_dir = RemotePathBuf::new( - path_style - .join(&response.tmp_dir, &missing_extension.id) - .with_context(|| { - format!( - "failed to construct destination path: {:?}, {:?}", - response.tmp_dir, missing_extension.id, - ) - })?, - path_style, - ); - log::info!( - "Uploading extension {} to {:?}", - missing_extension.clone().id, - dest_dir - ); - - client - .update(cx, |client, cx| { - client.upload_directory(tmp_dir.path().to_owned(), dest_dir.clone(), cx) - })? - .await?; - - log::info!( - "Finished uploading extension {}", - missing_extension.clone().id - ); - - let result = client - .update(cx, |client, _cx| { - client.proto_client().request(proto::InstallExtension { - tmp_dir: dest_dir.to_proto(), - extension: Some(missing_extension.clone()), - }) - })? - .await; - - if let Err(e) = result { - log::error!( - "Failed to install extension {}: {}", - missing_extension.id, - e - ); - } - } - - anyhow::Ok(()) - } - - pub async fn update_remote_clients(this: &WeakEntity, cx: &mut AsyncApp) -> Result<()> { - let clients = this.update(cx, |this, _cx| { - this.remote_clients.retain(|v| v.upgrade().is_some()); - this.remote_clients.clone() - })?; - - for client in clients { - Self::sync_extensions_to_remotes(this, client, cx) - .await - .log_err(); - } - - anyhow::Ok(()) - } - - pub fn register_remote_client( - &mut self, - client: Entity, - _cx: &mut Context, - ) { - self.remote_clients.push(client.downgrade()); - self.ssh_registered_tx.unbounded_send(()).ok(); - } -} - -fn load_plugin_queries(root_path: &Path) -> LanguageQueries { - let mut result = LanguageQueries::default(); - if let Some(entries) = std::fs::read_dir(root_path).log_err() { - for entry in entries { - let Some(entry) = entry.log_err() else { - continue; - }; - let path = entry.path(); - if let Some(remainder) = path.strip_prefix(root_path).ok().and_then(|p| p.to_str()) { - if !remainder.ends_with(".scm") { - continue; - } - for (name, query) in QUERY_FILENAME_PREFIXES { - if remainder.starts_with(name) { - if let Some(contents) = std::fs::read_to_string(&path).log_err() { - match query(&mut result) { - None => *query(&mut result) = Some(contents.into()), - Some(r) => r.to_mut().push_str(contents.as_ref()), - } - } - break; - } - } - } - } - } - result -} diff --git a/crates/extension_host/src/extension_settings.rs b/crates/extension_host/src/extension_settings.rs deleted file mode 100644 index 736dd6b87a..0000000000 --- a/crates/extension_host/src/extension_settings.rs +++ /dev/null @@ -1,65 +0,0 @@ -use collections::HashMap; -use extension::{ - DownloadFileCapability, ExtensionCapability, NpmInstallPackageCapability, ProcessExecCapability, -}; -use settings::{RegisterSetting, Settings}; -use std::sync::Arc; - -#[derive(Debug, Default, Clone, RegisterSetting)] -pub struct ExtensionSettings { - /// The extensions that should be automatically installed by Zed. - /// - /// This is used to make functionality provided by extensions (e.g., language support) - /// available out-of-the-box. - /// - /// Default: { "html": true } - pub auto_install_extensions: HashMap, bool>, - pub auto_update_extensions: HashMap, bool>, - pub granted_capabilities: Vec, -} - -impl ExtensionSettings { - /// Returns whether the given extension should be auto-installed. - pub fn should_auto_install(&self, extension_id: &str) -> bool { - self.auto_install_extensions - .get(extension_id) - .copied() - .unwrap_or(true) - } - - pub fn should_auto_update(&self, extension_id: &str) -> bool { - self.auto_update_extensions - .get(extension_id) - .copied() - .unwrap_or(true) - } -} - -impl Settings for ExtensionSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - Self { - auto_install_extensions: content.extension.auto_install_extensions.clone(), - auto_update_extensions: content.extension.auto_update_extensions.clone(), - granted_capabilities: content - .extension - .granted_extension_capabilities - .clone() - .unwrap_or_default() - .into_iter() - .map(|capability| match capability { - settings::ExtensionCapabilityContent::ProcessExec { command, args } => { - ExtensionCapability::ProcessExec(ProcessExecCapability { command, args }) - } - settings::ExtensionCapabilityContent::DownloadFile { host, path } => { - ExtensionCapability::DownloadFile(DownloadFileCapability { host, path }) - } - settings::ExtensionCapabilityContent::NpmInstallPackage { package } => { - ExtensionCapability::NpmInstallPackage(NpmInstallPackageCapability { - package, - }) - } - }) - .collect(), - } - } -} diff --git a/crates/extension_host/src/extension_store_test.rs b/crates/extension_host/src/extension_store_test.rs deleted file mode 100644 index 54b090347f..0000000000 --- a/crates/extension_host/src/extension_store_test.rs +++ /dev/null @@ -1,874 +0,0 @@ -use crate::{ - Event, ExtensionIndex, ExtensionIndexEntry, ExtensionIndexLanguageEntry, - ExtensionIndexThemeEntry, ExtensionManifest, ExtensionStore, GrammarManifestEntry, - RELOAD_DEBOUNCE_DURATION, SchemaVersion, -}; -use async_compression::futures::bufread::GzipEncoder; -use collections::{BTreeMap, HashSet}; -use extension::ExtensionHostProxy; -use fs::{FakeFs, Fs, RealFs}; -use futures::{AsyncReadExt, StreamExt, io::BufReader}; -use gpui::{AppContext as _, TestAppContext}; -use http_client::{FakeHttpClient, Response}; -use language::{BinaryStatus, LanguageMatcher, LanguageName, LanguageRegistry}; -use language_extension::LspAccess; -use lsp::LanguageServerName; -use node_runtime::NodeRuntime; -use parking_lot::Mutex; -use project::{DEFAULT_COMPLETION_CONTEXT, Project}; -use release_channel::AppVersion; -use reqwest_client::ReqwestClient; -use serde_json::json; -use settings::SettingsStore; -use std::{ - ffi::OsString, - path::{Path, PathBuf}, - sync::Arc, -}; -use theme::ThemeRegistry; -use util::test::TempTree; - -#[cfg(test)] -#[ctor::ctor] -fn init_logger() { - zlog::init_test(); -} - -#[gpui::test] -async fn test_extension_store(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let http_client = FakeHttpClient::with_200_response(); - - fs.insert_tree( - "/the-extension-dir", - json!({ - "installed": { - "zed-monokai": { - "extension.json": r#"{ - "id": "zed-monokai", - "name": "Zed Monokai", - "version": "2.0.0", - "themes": { - "Monokai Dark": "themes/monokai.json", - "Monokai Light": "themes/monokai.json", - "Monokai Pro Dark": "themes/monokai-pro.json", - "Monokai Pro Light": "themes/monokai-pro.json" - } - }"#, - "themes": { - "monokai.json": r#"{ - "name": "Monokai", - "author": "Someone", - "themes": [ - { - "name": "Monokai Dark", - "appearance": "dark", - "style": {} - }, - { - "name": "Monokai Light", - "appearance": "light", - "style": {} - } - ] - }"#, - "monokai-pro.json": r#"{ - "name": "Monokai Pro", - "author": "Someone", - "themes": [ - { - "name": "Monokai Pro Dark", - "appearance": "dark", - "style": {} - }, - { - "name": "Monokai Pro Light", - "appearance": "light", - "style": {} - } - ] - }"#, - } - }, - "zed-ruby": { - "extension.json": r#"{ - "id": "zed-ruby", - "name": "Zed Ruby", - "version": "1.0.0", - "grammars": { - "ruby": "grammars/ruby.wasm", - "embedded_template": "grammars/embedded_template.wasm" - }, - "languages": { - "ruby": "languages/ruby", - "erb": "languages/erb" - } - }"#, - "grammars": { - "ruby.wasm": "", - "embedded_template.wasm": "", - }, - "languages": { - "ruby": { - "config.toml": r#" - name = "Ruby" - grammar = "ruby" - path_suffixes = ["rb"] - "#, - "highlights.scm": "", - }, - "erb": { - "config.toml": r#" - name = "ERB" - grammar = "embedded_template" - path_suffixes = ["erb"] - "#, - "highlights.scm": "", - } - }, - } - } - }), - ) - .await; - - let mut expected_index = ExtensionIndex { - extensions: [ - ( - "zed-ruby".into(), - ExtensionIndexEntry { - manifest: Arc::new(ExtensionManifest { - id: "zed-ruby".into(), - name: "Zed Ruby".into(), - version: "1.0.0".into(), - schema_version: SchemaVersion::ZERO, - description: None, - authors: Vec::new(), - repository: None, - themes: Default::default(), - icon_themes: Vec::new(), - lib: Default::default(), - languages: vec!["languages/erb".into(), "languages/ruby".into()], - grammars: [ - ("embedded_template".into(), GrammarManifestEntry::default()), - ("ruby".into(), GrammarManifestEntry::default()), - ] - .into_iter() - .collect(), - language_servers: BTreeMap::default(), - context_servers: BTreeMap::default(), - agent_servers: BTreeMap::default(), - slash_commands: BTreeMap::default(), - snippets: None, - capabilities: Vec::new(), - debug_adapters: Default::default(), - debug_locators: Default::default(), - }), - dev: false, - }, - ), - ( - "zed-monokai".into(), - ExtensionIndexEntry { - manifest: Arc::new(ExtensionManifest { - id: "zed-monokai".into(), - name: "Zed Monokai".into(), - version: "2.0.0".into(), - schema_version: SchemaVersion::ZERO, - description: None, - authors: vec![], - repository: None, - themes: vec![ - "themes/monokai-pro.json".into(), - "themes/monokai.json".into(), - ], - icon_themes: Vec::new(), - lib: Default::default(), - languages: Default::default(), - grammars: BTreeMap::default(), - language_servers: BTreeMap::default(), - context_servers: BTreeMap::default(), - agent_servers: BTreeMap::default(), - slash_commands: BTreeMap::default(), - snippets: None, - capabilities: Vec::new(), - debug_adapters: Default::default(), - debug_locators: Default::default(), - }), - dev: false, - }, - ), - ] - .into_iter() - .collect(), - languages: [ - ( - "ERB".into(), - ExtensionIndexLanguageEntry { - extension: "zed-ruby".into(), - path: "languages/erb".into(), - grammar: Some("embedded_template".into()), - hidden: false, - matcher: LanguageMatcher { - path_suffixes: vec!["erb".into()], - first_line_pattern: None, - }, - }, - ), - ( - "Ruby".into(), - ExtensionIndexLanguageEntry { - extension: "zed-ruby".into(), - path: "languages/ruby".into(), - grammar: Some("ruby".into()), - hidden: false, - matcher: LanguageMatcher { - path_suffixes: vec!["rb".into()], - first_line_pattern: None, - }, - }, - ), - ] - .into_iter() - .collect(), - themes: [ - ( - "Monokai Dark".into(), - ExtensionIndexThemeEntry { - extension: "zed-monokai".into(), - path: "themes/monokai.json".into(), - }, - ), - ( - "Monokai Light".into(), - ExtensionIndexThemeEntry { - extension: "zed-monokai".into(), - path: "themes/monokai.json".into(), - }, - ), - ( - "Monokai Pro Dark".into(), - ExtensionIndexThemeEntry { - extension: "zed-monokai".into(), - path: "themes/monokai-pro.json".into(), - }, - ), - ( - "Monokai Pro Light".into(), - ExtensionIndexThemeEntry { - extension: "zed-monokai".into(), - path: "themes/monokai-pro.json".into(), - }, - ), - ] - .into_iter() - .collect(), - icon_themes: BTreeMap::default(), - }; - - let proxy = Arc::new(ExtensionHostProxy::new()); - let theme_registry = Arc::new(ThemeRegistry::new(Box::new(()))); - theme_extension::init(proxy.clone(), theme_registry.clone(), cx.executor()); - let language_registry = Arc::new(LanguageRegistry::test(cx.executor())); - language_extension::init(LspAccess::Noop, proxy.clone(), language_registry.clone()); - let node_runtime = NodeRuntime::unavailable(); - - let store = cx.new(|cx| { - ExtensionStore::new( - PathBuf::from("/the-extension-dir"), - None, - proxy.clone(), - fs.clone(), - http_client.clone(), - http_client.clone(), - None, - node_runtime.clone(), - cx, - ) - }); - - cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION); - store.read_with(cx, |store, _| { - let index = &store.extension_index; - assert_eq!(index.extensions, expected_index.extensions); - - for ((actual_key, actual_language), (expected_key, expected_language)) in - index.languages.iter().zip(expected_index.languages.iter()) - { - assert_eq!(actual_key, expected_key); - assert_eq!(actual_language.grammar, expected_language.grammar); - assert_eq!(actual_language.matcher, expected_language.matcher); - assert_eq!(actual_language.hidden, expected_language.hidden); - } - assert_eq!(index.themes, expected_index.themes); - - assert_eq!( - language_registry.language_names(), - [ - LanguageName::new_static("ERB"), - LanguageName::new_static("Plain Text"), - LanguageName::new_static("Ruby"), - ] - ); - assert_eq!( - theme_registry.list_names(), - [ - "Monokai Dark", - "Monokai Light", - "Monokai Pro Dark", - "Monokai Pro Light", - "One Dark", - ] - ); - }); - - fs.insert_tree( - "/the-extension-dir/installed/zed-gruvbox", - json!({ - "extension.json": r#"{ - "id": "zed-gruvbox", - "name": "Zed Gruvbox", - "version": "1.0.0", - "themes": { - "Gruvbox": "themes/gruvbox.json" - } - }"#, - "themes": { - "gruvbox.json": r#"{ - "name": "Gruvbox", - "author": "Someone Else", - "themes": [ - { - "name": "Gruvbox", - "appearance": "dark", - "style": {} - } - ] - }"#, - } - }), - ) - .await; - - expected_index.extensions.insert( - "zed-gruvbox".into(), - ExtensionIndexEntry { - manifest: Arc::new(ExtensionManifest { - id: "zed-gruvbox".into(), - name: "Zed Gruvbox".into(), - version: "1.0.0".into(), - schema_version: SchemaVersion::ZERO, - description: None, - authors: vec![], - repository: None, - themes: vec!["themes/gruvbox.json".into()], - icon_themes: Vec::new(), - lib: Default::default(), - languages: Default::default(), - grammars: BTreeMap::default(), - language_servers: BTreeMap::default(), - context_servers: BTreeMap::default(), - agent_servers: BTreeMap::default(), - slash_commands: BTreeMap::default(), - snippets: None, - capabilities: Vec::new(), - debug_adapters: Default::default(), - debug_locators: Default::default(), - }), - dev: false, - }, - ); - expected_index.themes.insert( - "Gruvbox".into(), - ExtensionIndexThemeEntry { - extension: "zed-gruvbox".into(), - path: "themes/gruvbox.json".into(), - }, - ); - - #[allow(clippy::let_underscore_future)] - let _ = store.update(cx, |store, cx| store.reload(None, cx)); - - cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION); - store.read_with(cx, |store, _| { - let index = &store.extension_index; - - for ((actual_key, actual_language), (expected_key, expected_language)) in - index.languages.iter().zip(expected_index.languages.iter()) - { - assert_eq!(actual_key, expected_key); - assert_eq!(actual_language.grammar, expected_language.grammar); - assert_eq!(actual_language.matcher, expected_language.matcher); - assert_eq!(actual_language.hidden, expected_language.hidden); - } - - assert_eq!(index.extensions, expected_index.extensions); - assert_eq!(index.themes, expected_index.themes); - - assert_eq!( - theme_registry.list_names(), - [ - "Gruvbox", - "Monokai Dark", - "Monokai Light", - "Monokai Pro Dark", - "Monokai Pro Light", - "One Dark", - ] - ); - }); - - let prev_fs_metadata_call_count = fs.metadata_call_count(); - let prev_fs_read_dir_call_count = fs.read_dir_call_count(); - - // Create new extension store, as if Zed were restarting. - drop(store); - let store = cx.new(|cx| { - ExtensionStore::new( - PathBuf::from("/the-extension-dir"), - None, - proxy, - fs.clone(), - http_client.clone(), - http_client.clone(), - None, - node_runtime.clone(), - cx, - ) - }); - - cx.executor().run_until_parked(); - store.read_with(cx, |store, _| { - assert_eq!(store.extension_index.extensions, expected_index.extensions); - assert_eq!(store.extension_index.themes, expected_index.themes); - assert_eq!( - store.extension_index.icon_themes, - expected_index.icon_themes - ); - - for ((actual_key, actual_language), (expected_key, expected_language)) in store - .extension_index - .languages - .iter() - .zip(expected_index.languages.iter()) - { - assert_eq!(actual_key, expected_key); - assert_eq!(actual_language.grammar, expected_language.grammar); - assert_eq!(actual_language.matcher, expected_language.matcher); - assert_eq!(actual_language.hidden, expected_language.hidden); - } - - assert_eq!( - language_registry.language_names(), - [ - LanguageName::new_static("ERB"), - LanguageName::new_static("Plain Text"), - LanguageName::new_static("Ruby"), - ] - ); - assert_eq!( - language_registry.grammar_names(), - ["embedded_template".into(), "ruby".into()] - ); - assert_eq!( - theme_registry.list_names(), - [ - "Gruvbox", - "Monokai Dark", - "Monokai Light", - "Monokai Pro Dark", - "Monokai Pro Light", - "One Dark", - ] - ); - - // The on-disk manifest limits the number of FS calls that need to be made - // on startup. - assert_eq!(fs.read_dir_call_count(), prev_fs_read_dir_call_count); - assert_eq!(fs.metadata_call_count(), prev_fs_metadata_call_count + 2); - }); - - store.update(cx, |store, cx| { - store - .uninstall_extension("zed-ruby".into(), cx) - .detach_and_log_err(cx); - }); - - cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION); - expected_index.extensions.remove("zed-ruby"); - expected_index.languages.remove("Ruby"); - expected_index.languages.remove("ERB"); - - store.read_with(cx, |store, _| { - assert_eq!(store.extension_index.extensions, expected_index.extensions); - assert_eq!(store.extension_index.themes, expected_index.themes); - assert_eq!( - store.extension_index.icon_themes, - expected_index.icon_themes - ); - - for ((actual_key, actual_language), (expected_key, expected_language)) in store - .extension_index - .languages - .iter() - .zip(expected_index.languages.iter()) - { - assert_eq!(actual_key, expected_key); - assert_eq!(actual_language.grammar, expected_language.grammar); - assert_eq!(actual_language.matcher, expected_language.matcher); - assert_eq!(actual_language.hidden, expected_language.hidden); - } - - assert_eq!( - language_registry.language_names(), - [LanguageName::new_static("Plain Text")] - ); - assert_eq!(language_registry.grammar_names(), []); - }); -} - -#[gpui::test] -async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) { - log::info!("Initializing test"); - init_test(cx); - cx.executor().allow_parking(); - - let root_dir = Path::new(env!("CARGO_MANIFEST_DIR")) - .parent() - .unwrap() - .parent() - .unwrap(); - let cache_dir = root_dir.join("target"); - let test_extension_id = "test-extension"; - let test_extension_dir = root_dir.join("extensions").join(test_extension_id); - - let fs = Arc::new(RealFs::new(None, cx.executor())); - let extensions_tree = TempTree::new(json!({ - "installed": {}, - "work": {} - })); - let project_dir = TempTree::new(json!({ - "test.gleam": "" - })); - - let extensions_dir = extensions_tree.path().canonicalize().unwrap(); - let project_dir = project_dir.path().canonicalize().unwrap(); - - log::info!("Setting up test"); - - let project = Project::test(fs.clone(), [project_dir.as_path()], cx).await; - - let proxy = Arc::new(ExtensionHostProxy::new()); - let theme_registry = Arc::new(ThemeRegistry::new(Box::new(()))); - theme_extension::init(proxy.clone(), theme_registry.clone(), cx.executor()); - let language_registry = project.read_with(cx, |project, _cx| project.languages().clone()); - language_extension::init( - LspAccess::ViaLspStore(project.update(cx, |project, _| project.lsp_store())), - proxy.clone(), - language_registry.clone(), - ); - let node_runtime = NodeRuntime::unavailable(); - - let mut status_updates = language_registry.language_server_binary_statuses(); - - struct FakeLanguageServerVersion { - version: String, - binary_contents: String, - http_request_count: usize, - } - - let language_server_version = Arc::new(Mutex::new(FakeLanguageServerVersion { - version: "v1.2.3".into(), - binary_contents: "the-binary-contents".into(), - http_request_count: 0, - })); - - let extension_client = FakeHttpClient::create({ - let language_server_version = language_server_version.clone(); - move |request| { - let language_server_version = language_server_version.clone(); - async move { - let version = language_server_version.lock().version.clone(); - let binary_contents = language_server_version.lock().binary_contents.clone(); - - let github_releases_uri = "https://api.github.com/repos/gleam-lang/gleam/releases"; - let asset_download_uri = - format!("https://fake-download.example.com/gleam-{version}"); - - let uri = request.uri().to_string(); - if uri == github_releases_uri { - language_server_version.lock().http_request_count += 1; - Ok(Response::new( - json!([ - { - "tag_name": version, - "prerelease": false, - "tarball_url": "", - "zipball_url": "", - "assets": [ - { - "name": format!("gleam-{version}-aarch64-apple-darwin.tar.gz"), - "browser_download_url": asset_download_uri - }, - { - "name": format!("gleam-{version}-x86_64-unknown-linux-musl.tar.gz"), - "browser_download_url": asset_download_uri - }, - { - "name": format!("gleam-{version}-aarch64-unknown-linux-musl.tar.gz"), - "browser_download_url": asset_download_uri - }, - { - "name": format!("gleam-{version}-x86_64-pc-windows-msvc.tar.gz"), - "browser_download_url": asset_download_uri - } - ] - } - ]) - .to_string() - .into(), - )) - } else if uri == asset_download_uri { - language_server_version.lock().http_request_count += 1; - let mut bytes = Vec::::new(); - let mut archive = async_tar::Builder::new(&mut bytes); - let mut header = async_tar::Header::new_gnu(); - header.set_size(binary_contents.len() as u64); - archive - .append_data(&mut header, "gleam", binary_contents.as_bytes()) - .await - .unwrap(); - archive.into_inner().await.unwrap(); - let mut gzipped_bytes = Vec::new(); - let mut encoder = GzipEncoder::new(BufReader::new(bytes.as_slice())); - encoder.read_to_end(&mut gzipped_bytes).await.unwrap(); - Ok(Response::new(gzipped_bytes.into())) - } else { - Ok(Response::builder().status(404).body("not found".into())?) - } - } - } - }); - let user_agent = cx.update(|cx| { - format!( - "Zed/{} ({}; {})", - AppVersion::global(cx), - std::env::consts::OS, - std::env::consts::ARCH - ) - }); - let builder_client = - Arc::new(ReqwestClient::user_agent(&user_agent).expect("Could not create HTTP client")); - - let extension_store = cx.new(|cx| { - ExtensionStore::new( - extensions_dir.clone(), - Some(cache_dir), - proxy, - fs.clone(), - extension_client.clone(), - builder_client, - None, - node_runtime, - cx, - ) - }); - - log::info!("Flushing events"); - - // Ensure that debounces fire. - let mut events = cx.events(&extension_store); - let executor = cx.executor(); - let _task = cx.executor().spawn(async move { - while let Some(event) = events.next().await { - if let Event::StartedReloading = event { - executor.advance_clock(RELOAD_DEBOUNCE_DURATION); - } - } - }); - - extension_store.update(cx, |_, cx| { - cx.subscribe(&extension_store, |_, _, event, _| { - if matches!(event, Event::ExtensionFailedToLoad(_)) { - panic!("extension failed to load"); - } - }) - .detach(); - }); - - extension_store - .update(cx, |store, cx| { - store.install_dev_extension(test_extension_dir.clone(), cx) - }) - .await - .unwrap(); - - let mut fake_servers = language_registry.register_fake_lsp_server( - LanguageServerName("gleam".into()), - lsp::ServerCapabilities { - completion_provider: Some(Default::default()), - ..Default::default() - }, - None, - ); - - let (buffer, _handle) = project - .update(cx, |project, cx| { - project.open_local_buffer_with_lsp(project_dir.join("test.gleam"), cx) - }) - .await - .unwrap(); - - let fake_server = fake_servers.next().await.unwrap(); - let work_dir = extensions_dir.join(format!("work/{test_extension_id}")); - let expected_server_path = work_dir.join("gleam-v1.2.3/gleam"); - let expected_binary_contents = language_server_version.lock().binary_contents.clone(); - - // check that IO operations in extension work correctly - assert!(work_dir.join("dir-created-with-rel-path").exists()); - assert!(work_dir.join("dir-created-with-abs-path").exists()); - assert!(work_dir.join("file-created-with-abs-path").exists()); - assert!(work_dir.join("file-created-with-rel-path").exists()); - - assert_eq!(fake_server.binary.path, expected_server_path); - assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]); - assert_eq!( - fs.load(&expected_server_path).await.unwrap(), - expected_binary_contents - ); - assert_eq!(language_server_version.lock().http_request_count, 2); - assert_eq!( - [ - status_updates.next().await.unwrap(), - status_updates.next().await.unwrap(), - status_updates.next().await.unwrap(), - status_updates.next().await.unwrap(), - ], - [ - ( - LanguageServerName::new_static("gleam"), - BinaryStatus::Starting - ), - ( - LanguageServerName::new_static("gleam"), - BinaryStatus::CheckingForUpdate - ), - ( - LanguageServerName::new_static("gleam"), - BinaryStatus::Downloading - ), - (LanguageServerName::new_static("gleam"), BinaryStatus::None) - ] - ); - - // The extension creates custom labels for completion items. - fake_server.set_request_handler::(|_, _| async move { - Ok(Some(lsp::CompletionResponse::Array(vec![ - lsp::CompletionItem { - label: "foo".into(), - kind: Some(lsp::CompletionItemKind::FUNCTION), - detail: Some("fn() -> Result(Nil, Error)".into()), - ..Default::default() - }, - lsp::CompletionItem { - label: "bar.baz".into(), - kind: Some(lsp::CompletionItemKind::FUNCTION), - detail: Some("fn(List(a)) -> a".into()), - ..Default::default() - }, - lsp::CompletionItem { - label: "Quux".into(), - kind: Some(lsp::CompletionItemKind::CONSTRUCTOR), - detail: Some("fn(String) -> T".into()), - ..Default::default() - }, - lsp::CompletionItem { - label: "my_string".into(), - kind: Some(lsp::CompletionItemKind::CONSTANT), - detail: Some("String".into()), - ..Default::default() - }, - ]))) - }); - - let completion_labels = project - .update(cx, |project, cx| { - project.completions(&buffer, 0, DEFAULT_COMPLETION_CONTEXT, cx) - }) - .await - .unwrap() - .into_iter() - .flat_map(|response| response.completions) - .map(|c| c.label.text) - .collect::>(); - assert_eq!( - completion_labels, - [ - "foo: fn() -> Result(Nil, Error)".to_string(), - "bar.baz: fn(List(a)) -> a".to_string(), - "Quux: fn(String) -> T".to_string(), - "my_string: String".to_string(), - ] - ); - - // Simulate a new version of the language server being released - language_server_version.lock().version = "v2.0.0".into(); - language_server_version.lock().binary_contents = "the-new-binary-contents".into(); - language_server_version.lock().http_request_count = 0; - - // Start a new instance of the language server. - project.update(cx, |project, cx| { - project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx) - }); - cx.executor().run_until_parked(); - - // The extension has cached the binary path, and does not attempt - // to reinstall it. - let fake_server = fake_servers.next().await.unwrap(); - assert_eq!(fake_server.binary.path, expected_server_path); - assert_eq!( - fs.load(&expected_server_path).await.unwrap(), - expected_binary_contents - ); - assert_eq!(language_server_version.lock().http_request_count, 0); - - // Reload the extension, clearing its cache. - // Start a new instance of the language server. - extension_store - .update(cx, |store, cx| { - store.reload(Some("test-extension".into()), cx) - }) - .await; - cx.executor().run_until_parked(); - project.update(cx, |project, cx| { - project.restart_language_servers_for_buffers(vec![buffer.clone()], HashSet::default(), cx) - }); - - // The extension re-fetches the latest version of the language server. - let fake_server = fake_servers.next().await.unwrap(); - let new_expected_server_path = - extensions_dir.join(format!("work/{test_extension_id}/gleam-v2.0.0/gleam")); - let expected_binary_contents = language_server_version.lock().binary_contents.clone(); - assert_eq!(fake_server.binary.path, new_expected_server_path); - assert_eq!(fake_server.binary.arguments, [OsString::from("lsp")]); - assert_eq!( - fs.load(&new_expected_server_path).await.unwrap(), - expected_binary_contents - ); - - // The old language server directory has been cleaned up. - assert!(fs.metadata(&expected_server_path).await.unwrap().is_none()); -} - -fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let store = SettingsStore::test(cx); - cx.set_global(store); - release_channel::init(semver::Version::new(0, 0, 0), cx); - extension::init(cx); - theme::init(theme::LoadThemes::JustBase, cx); - gpui_tokio::init(cx); - }); -} diff --git a/crates/extension_host/src/headless_host.rs b/crates/extension_host/src/headless_host.rs deleted file mode 100644 index c3a290a55a..0000000000 --- a/crates/extension_host/src/headless_host.rs +++ /dev/null @@ -1,351 +0,0 @@ -use std::{path::PathBuf, sync::Arc}; - -use anyhow::{Context as _, Result}; -use client::{TypedEnvelope, proto}; -use collections::{HashMap, HashSet}; -use extension::{ - Extension, ExtensionDebugAdapterProviderProxy, ExtensionHostProxy, ExtensionLanguageProxy, - ExtensionLanguageServerProxy, ExtensionManifest, -}; -use fs::{Fs, RemoveOptions, RenameOptions}; -use futures::future::join_all; -use gpui::{App, AppContext as _, AsyncApp, Context, Entity, Task, WeakEntity}; -use http_client::HttpClient; -use language::{LanguageConfig, LanguageName, LanguageQueries, LoadedLanguage}; -use lsp::LanguageServerName; -use node_runtime::NodeRuntime; - -use crate::wasm_host::{WasmExtension, WasmHost}; - -#[derive(Clone, Debug)] -pub struct ExtensionVersion { - pub id: String, - pub version: String, - pub dev: bool, -} - -pub struct HeadlessExtensionStore { - pub fs: Arc, - pub extension_dir: PathBuf, - pub proxy: Arc, - pub wasm_host: Arc, - pub loaded_extensions: HashMap, Arc>, - pub loaded_languages: HashMap, Vec>, - pub loaded_language_servers: HashMap, Vec<(LanguageServerName, LanguageName)>>, -} - -impl HeadlessExtensionStore { - pub fn new( - fs: Arc, - http_client: Arc, - extension_dir: PathBuf, - extension_host_proxy: Arc, - node_runtime: NodeRuntime, - cx: &mut App, - ) -> Entity { - cx.new(|cx| Self { - fs: fs.clone(), - wasm_host: WasmHost::new( - fs.clone(), - http_client.clone(), - node_runtime, - extension_host_proxy.clone(), - extension_dir.join("work"), - cx, - ), - extension_dir, - proxy: extension_host_proxy, - loaded_extensions: Default::default(), - loaded_languages: Default::default(), - loaded_language_servers: Default::default(), - }) - } - - pub fn sync_extensions( - &mut self, - extensions: Vec, - cx: &Context, - ) -> Task>> { - let on_client = HashSet::from_iter(extensions.iter().map(|e| e.id.as_str())); - let to_remove: Vec> = self - .loaded_extensions - .keys() - .filter(|id| !on_client.contains(id.as_ref())) - .cloned() - .collect(); - let to_load: Vec = extensions - .into_iter() - .filter(|e| { - if e.dev { - return true; - } - self.loaded_extensions - .get(e.id.as_str()) - .is_none_or(|loaded| loaded.as_ref() != e.version.as_str()) - }) - .collect(); - - cx.spawn(async move |this, cx| { - let mut missing = Vec::new(); - - for extension_id in to_remove { - log::info!("removing extension: {}", extension_id); - this.update(cx, |this, cx| this.uninstall_extension(&extension_id, cx))? - .await?; - } - - for extension in to_load { - if let Err(e) = Self::load_extension(this.clone(), extension.clone(), cx).await { - log::info!("failed to load extension: {}, {:#}", extension.id, e); - missing.push(extension) - } else if extension.dev { - missing.push(extension) - } - } - - Ok(missing) - }) - } - - pub async fn load_extension( - this: WeakEntity, - extension: ExtensionVersion, - cx: &mut AsyncApp, - ) -> Result<()> { - let (fs, wasm_host, extension_dir) = this.update(cx, |this, _cx| { - this.loaded_extensions.insert( - extension.id.clone().into(), - extension.version.clone().into(), - ); - ( - this.fs.clone(), - this.wasm_host.clone(), - this.extension_dir.join(&extension.id), - ) - })?; - - let manifest = Arc::new(ExtensionManifest::load(fs.clone(), &extension_dir).await?); - - debug_assert!(!manifest.languages.is_empty() || manifest.allow_remote_load()); - - if manifest.version.as_ref() != extension.version.as_str() { - anyhow::bail!( - "mismatched versions: ({}) != ({})", - manifest.version, - extension.version - ) - } - - for language_path in &manifest.languages { - let language_path = extension_dir.join(language_path); - let config = fs.load(&language_path.join("config.toml")).await?; - let mut config = ::toml::from_str::(&config)?; - - this.update(cx, |this, _cx| { - this.loaded_languages - .entry(manifest.id.clone()) - .or_default() - .push(config.name.clone()); - - config.grammar = None; - - this.proxy.register_language( - config.name.clone(), - None, - config.matcher.clone(), - config.hidden, - Arc::new(move || { - Ok(LoadedLanguage { - config: config.clone(), - queries: LanguageQueries::default(), - context_provider: None, - toolchain_provider: None, - manifest_name: None, - }) - }), - ); - })?; - } - - if !manifest.allow_remote_load() { - return Ok(()); - } - - let wasm_extension: Arc = - Arc::new(WasmExtension::load(&extension_dir, &manifest, wasm_host.clone(), cx).await?); - - for (language_server_id, language_server_config) in &manifest.language_servers { - for language in language_server_config.languages() { - this.update(cx, |this, _cx| { - this.loaded_language_servers - .entry(manifest.id.clone()) - .or_default() - .push((language_server_id.clone(), language.clone())); - this.proxy.register_language_server( - wasm_extension.clone(), - language_server_id.clone(), - language.clone(), - ); - })?; - } - log::info!("Loaded language server: {}", language_server_id); - } - - for (debug_adapter, meta) in &manifest.debug_adapters { - let schema_path = extension::build_debug_adapter_schema_path(debug_adapter, meta); - - this.update(cx, |this, _cx| { - this.proxy.register_debug_adapter( - wasm_extension.clone(), - debug_adapter.clone(), - &extension_dir.join(schema_path), - ); - })?; - log::info!("Loaded debug adapter: {}", debug_adapter); - } - - for debug_locator in manifest.debug_locators.keys() { - this.update(cx, |this, _cx| { - this.proxy - .register_debug_locator(wasm_extension.clone(), debug_locator.clone()); - })?; - log::info!("Loaded debug locator: {}", debug_locator); - } - - Ok(()) - } - - fn uninstall_extension( - &mut self, - extension_id: &Arc, - cx: &mut Context, - ) -> Task> { - self.loaded_extensions.remove(extension_id); - - let languages_to_remove = self - .loaded_languages - .remove(extension_id) - .unwrap_or_default(); - self.proxy.remove_languages(&languages_to_remove, &[]); - - let servers_to_remove = self - .loaded_language_servers - .remove(extension_id) - .unwrap_or_default(); - let proxy = self.proxy.clone(); - let path = self.extension_dir.join(&extension_id.to_string()); - let fs = self.fs.clone(); - cx.spawn(async move |_, cx| { - let mut removal_tasks = Vec::with_capacity(servers_to_remove.len()); - cx.update(|cx| { - for (language_server_name, language) in servers_to_remove { - removal_tasks.push(proxy.remove_language_server( - &language, - &language_server_name, - cx, - )); - } - }) - .ok(); - let _ = join_all(removal_tasks).await; - - fs.remove_dir( - &path, - RemoveOptions { - recursive: true, - ignore_if_not_exists: true, - }, - ) - .await - .with_context(|| format!("Removing directory {path:?}")) - }) - } - - pub fn install_extension( - &mut self, - extension: ExtensionVersion, - tmp_path: PathBuf, - cx: &mut Context, - ) -> Task> { - let path = self.extension_dir.join(&extension.id); - let fs = self.fs.clone(); - - cx.spawn(async move |this, cx| { - if fs.is_dir(&path).await { - this.update(cx, |this, cx| { - this.uninstall_extension(&extension.id.clone().into(), cx) - })? - .await?; - } - - fs.rename(&tmp_path, &path, RenameOptions::default()) - .await - .context("Failed to rename {tmp_path:?} to {path:?}")?; - - Self::load_extension(this, extension, cx).await - }) - } - - pub async fn handle_sync_extensions( - extension_store: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let requested_extensions = - envelope - .payload - .extensions - .into_iter() - .map(|p| ExtensionVersion { - id: p.id, - version: p.version, - dev: p.dev, - }); - let missing_extensions = extension_store - .update(&mut cx, |extension_store, cx| { - extension_store.sync_extensions(requested_extensions.collect(), cx) - })? - .await?; - - Ok(proto::SyncExtensionsResponse { - missing_extensions: missing_extensions - .into_iter() - .map(|e| proto::Extension { - id: e.id, - version: e.version, - dev: e.dev, - }) - .collect(), - tmp_dir: paths::remote_extensions_uploads_dir() - .to_string_lossy() - .to_string(), - }) - } - - pub async fn handle_install_extension( - extensions: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let extension = envelope - .payload - .extension - .context("Invalid InstallExtension request")?; - - extensions - .update(&mut cx, |extensions, cx| { - extensions.install_extension( - ExtensionVersion { - id: extension.id, - version: extension.version, - dev: extension.dev, - }, - PathBuf::from(envelope.payload.tmp_dir), - cx, - ) - })? - .await?; - - Ok(proto::Ack {}) - } -} diff --git a/crates/extension_host/src/wasm_host.rs b/crates/extension_host/src/wasm_host.rs deleted file mode 100644 index cecaf2039b..0000000000 --- a/crates/extension_host/src/wasm_host.rs +++ /dev/null @@ -1,900 +0,0 @@ -pub mod wit; - -use crate::capability_granter::CapabilityGranter; -use crate::{ExtensionManifest, ExtensionSettings}; -use anyhow::{Context as _, Result, anyhow, bail}; -use async_trait::async_trait; -use dap::{DebugRequest, StartDebuggingRequestArgumentsRequest}; -use extension::{ - CodeLabel, Command, Completion, ContextServerConfiguration, DebugAdapterBinary, - DebugTaskDefinition, ExtensionCapability, ExtensionHostProxy, KeyValueStoreDelegate, - ProjectDelegate, SlashCommand, SlashCommandArgumentCompletion, SlashCommandOutput, Symbol, - WorktreeDelegate, -}; -use fs::{Fs, normalize_path}; -use futures::future::LocalBoxFuture; -use futures::{ - Future, FutureExt, StreamExt as _, - channel::{ - mpsc::{self, UnboundedSender}, - oneshot, - }, - future::BoxFuture, -}; -use gpui::{App, AsyncApp, BackgroundExecutor, Task, Timer}; -use http_client::HttpClient; -use language::LanguageName; -use lsp::LanguageServerName; -use moka::sync::Cache; -use node_runtime::NodeRuntime; -use release_channel::ReleaseChannel; -use semver::Version; -use settings::Settings; -use std::{ - borrow::Cow, - path::{Path, PathBuf}, - sync::{ - Arc, LazyLock, OnceLock, - atomic::{AtomicBool, Ordering}, - }, - time::Duration, -}; -use task::{DebugScenario, SpawnInTerminal, TaskTemplate, ZedDebugConfig}; -use util::paths::SanitizedPath; -use wasmtime::{ - CacheStore, Engine, Store, - component::{Component, ResourceTable}, -}; -use wasmtime_wasi::{self as wasi, WasiView}; -use wit::Extension; - -pub struct WasmHost { - engine: Engine, - release_channel: ReleaseChannel, - http_client: Arc, - node_runtime: NodeRuntime, - pub(crate) proxy: Arc, - fs: Arc, - pub work_dir: PathBuf, - /// The capabilities granted to extensions running on the host. - pub(crate) granted_capabilities: Vec, - _main_thread_message_task: Task<()>, - main_thread_message_tx: mpsc::UnboundedSender, -} - -#[derive(Clone, Debug)] -pub struct WasmExtension { - tx: UnboundedSender, - pub manifest: Arc, - pub work_dir: Arc, - #[allow(unused)] - pub zed_api_version: Version, - _task: Arc>>, -} - -impl Drop for WasmExtension { - fn drop(&mut self) { - self.tx.close_channel(); - } -} - -#[async_trait] -impl extension::Extension for WasmExtension { - fn manifest(&self) -> Arc { - self.manifest.clone() - } - - fn work_dir(&self) -> Arc { - self.work_dir.clone() - } - - async fn language_server_command( - &self, - language_server_id: LanguageServerName, - language_name: LanguageName, - worktree: Arc, - ) -> Result { - self.call(|extension, store| { - async move { - let resource = store.data_mut().table().push(worktree)?; - let command = extension - .call_language_server_command( - store, - &language_server_id, - &language_name, - resource, - ) - .await? - .map_err(|err| store.data().extension_error(err))?; - - Ok(command.into()) - } - .boxed() - }) - .await? - } - - async fn language_server_initialization_options( - &self, - language_server_id: LanguageServerName, - language_name: LanguageName, - worktree: Arc, - ) -> Result> { - self.call(|extension, store| { - async move { - let resource = store.data_mut().table().push(worktree)?; - let options = extension - .call_language_server_initialization_options( - store, - &language_server_id, - &language_name, - resource, - ) - .await? - .map_err(|err| store.data().extension_error(err))?; - anyhow::Ok(options) - } - .boxed() - }) - .await? - } - - async fn language_server_workspace_configuration( - &self, - language_server_id: LanguageServerName, - worktree: Arc, - ) -> Result> { - self.call(|extension, store| { - async move { - let resource = store.data_mut().table().push(worktree)?; - let options = extension - .call_language_server_workspace_configuration( - store, - &language_server_id, - resource, - ) - .await? - .map_err(|err| store.data().extension_error(err))?; - anyhow::Ok(options) - } - .boxed() - }) - .await? - } - - async fn language_server_additional_initialization_options( - &self, - language_server_id: LanguageServerName, - target_language_server_id: LanguageServerName, - worktree: Arc, - ) -> Result> { - self.call(|extension, store| { - async move { - let resource = store.data_mut().table().push(worktree)?; - let options = extension - .call_language_server_additional_initialization_options( - store, - &language_server_id, - &target_language_server_id, - resource, - ) - .await? - .map_err(|err| store.data().extension_error(err))?; - anyhow::Ok(options) - } - .boxed() - }) - .await? - } - - async fn language_server_additional_workspace_configuration( - &self, - language_server_id: LanguageServerName, - target_language_server_id: LanguageServerName, - worktree: Arc, - ) -> Result> { - self.call(|extension, store| { - async move { - let resource = store.data_mut().table().push(worktree)?; - let options = extension - .call_language_server_additional_workspace_configuration( - store, - &language_server_id, - &target_language_server_id, - resource, - ) - .await? - .map_err(|err| store.data().extension_error(err))?; - anyhow::Ok(options) - } - .boxed() - }) - .await? - } - - async fn labels_for_completions( - &self, - language_server_id: LanguageServerName, - completions: Vec, - ) -> Result>> { - self.call(|extension, store| { - async move { - let labels = extension - .call_labels_for_completions( - store, - &language_server_id, - completions.into_iter().map(Into::into).collect(), - ) - .await? - .map_err(|err| store.data().extension_error(err))?; - - Ok(labels - .into_iter() - .map(|label| label.map(Into::into)) - .collect()) - } - .boxed() - }) - .await? - } - - async fn labels_for_symbols( - &self, - language_server_id: LanguageServerName, - symbols: Vec, - ) -> Result>> { - self.call(|extension, store| { - async move { - let labels = extension - .call_labels_for_symbols( - store, - &language_server_id, - symbols.into_iter().map(Into::into).collect(), - ) - .await? - .map_err(|err| store.data().extension_error(err))?; - - Ok(labels - .into_iter() - .map(|label| label.map(Into::into)) - .collect()) - } - .boxed() - }) - .await? - } - - async fn complete_slash_command_argument( - &self, - command: SlashCommand, - arguments: Vec, - ) -> Result> { - self.call(|extension, store| { - async move { - let completions = extension - .call_complete_slash_command_argument(store, &command.into(), &arguments) - .await? - .map_err(|err| store.data().extension_error(err))?; - - Ok(completions.into_iter().map(Into::into).collect()) - } - .boxed() - }) - .await? - } - - async fn run_slash_command( - &self, - command: SlashCommand, - arguments: Vec, - delegate: Option>, - ) -> Result { - self.call(|extension, store| { - async move { - let resource = if let Some(delegate) = delegate { - Some(store.data_mut().table().push(delegate)?) - } else { - None - }; - - let output = extension - .call_run_slash_command(store, &command.into(), &arguments, resource) - .await? - .map_err(|err| store.data().extension_error(err))?; - - Ok(output.into()) - } - .boxed() - }) - .await? - } - - async fn context_server_command( - &self, - context_server_id: Arc, - project: Arc, - ) -> Result { - self.call(|extension, store| { - async move { - let project_resource = store.data_mut().table().push(project)?; - let command = extension - .call_context_server_command(store, context_server_id.clone(), project_resource) - .await? - .map_err(|err| store.data().extension_error(err))?; - anyhow::Ok(command.into()) - } - .boxed() - }) - .await? - } - - async fn context_server_configuration( - &self, - context_server_id: Arc, - project: Arc, - ) -> Result> { - self.call(|extension, store| { - async move { - let project_resource = store.data_mut().table().push(project)?; - let Some(configuration) = extension - .call_context_server_configuration( - store, - context_server_id.clone(), - project_resource, - ) - .await? - .map_err(|err| store.data().extension_error(err))? - else { - return Ok(None); - }; - - Ok(Some(configuration.try_into()?)) - } - .boxed() - }) - .await? - } - - async fn suggest_docs_packages(&self, provider: Arc) -> Result> { - self.call(|extension, store| { - async move { - let packages = extension - .call_suggest_docs_packages(store, provider.as_ref()) - .await? - .map_err(|err| store.data().extension_error(err))?; - - Ok(packages) - } - .boxed() - }) - .await? - } - - async fn index_docs( - &self, - provider: Arc, - package_name: Arc, - kv_store: Arc, - ) -> Result<()> { - self.call(|extension, store| { - async move { - let kv_store_resource = store.data_mut().table().push(kv_store)?; - extension - .call_index_docs( - store, - provider.as_ref(), - package_name.as_ref(), - kv_store_resource, - ) - .await? - .map_err(|err| store.data().extension_error(err))?; - - anyhow::Ok(()) - } - .boxed() - }) - .await? - } - - async fn get_dap_binary( - &self, - dap_name: Arc, - config: DebugTaskDefinition, - user_installed_path: Option, - worktree: Arc, - ) -> Result { - self.call(|extension, store| { - async move { - let resource = store.data_mut().table().push(worktree)?; - let dap_binary = extension - .call_get_dap_binary(store, dap_name, config, user_installed_path, resource) - .await? - .map_err(|err| store.data().extension_error(err))?; - let dap_binary = dap_binary.try_into()?; - Ok(dap_binary) - } - .boxed() - }) - .await? - } - async fn dap_request_kind( - &self, - dap_name: Arc, - config: serde_json::Value, - ) -> Result { - self.call(|extension, store| { - async move { - let kind = extension - .call_dap_request_kind(store, dap_name, config) - .await? - .map_err(|err| store.data().extension_error(err))?; - Ok(kind.into()) - } - .boxed() - }) - .await? - } - - async fn dap_config_to_scenario(&self, config: ZedDebugConfig) -> Result { - self.call(|extension, store| { - async move { - let kind = extension - .call_dap_config_to_scenario(store, config) - .await? - .map_err(|err| store.data().extension_error(err))?; - Ok(kind) - } - .boxed() - }) - .await? - } - - async fn dap_locator_create_scenario( - &self, - locator_name: String, - build_config_template: TaskTemplate, - resolved_label: String, - debug_adapter_name: String, - ) -> Result> { - self.call(|extension, store| { - async move { - extension - .call_dap_locator_create_scenario( - store, - locator_name, - build_config_template, - resolved_label, - debug_adapter_name, - ) - .await - } - .boxed() - }) - .await? - } - async fn run_dap_locator( - &self, - locator_name: String, - config: SpawnInTerminal, - ) -> Result { - self.call(|extension, store| { - async move { - extension - .call_run_dap_locator(store, locator_name, config) - .await? - .map_err(|err| store.data().extension_error(err)) - } - .boxed() - }) - .await? - } -} - -pub struct WasmState { - manifest: Arc, - pub table: ResourceTable, - ctx: wasi::WasiCtx, - pub host: Arc, - pub(crate) capability_granter: CapabilityGranter, -} - -std::thread_local! { - /// Used by the crash handler to ignore panics in extension-related threads. - pub static IS_WASM_THREAD: AtomicBool = const { AtomicBool::new(false) }; -} - -type MainThreadCall = Box FnOnce(&'a mut AsyncApp) -> LocalBoxFuture<'a, ()>>; - -type ExtensionCall = Box< - dyn Send + for<'a> FnOnce(&'a mut Extension, &'a mut Store) -> BoxFuture<'a, ()>, ->; - -fn wasm_engine(executor: &BackgroundExecutor) -> wasmtime::Engine { - static WASM_ENGINE: OnceLock = OnceLock::new(); - WASM_ENGINE - .get_or_init(|| { - let mut config = wasmtime::Config::new(); - config.wasm_component_model(true); - config.async_support(true); - config - .enable_incremental_compilation(cache_store()) - .unwrap(); - // Async support introduces the issue that extension execution happens during `Future::poll`, - // which could block an async thread. - // https://docs.rs/wasmtime/latest/wasmtime/struct.Config.html#execution-in-poll - // - // Epoch interruption is a lightweight mechanism to allow the extensions to yield control - // back to the executor at regular intervals. - config.epoch_interruption(true); - - let engine = wasmtime::Engine::new(&config).unwrap(); - - // It might be safer to do this on a non-async thread to make sure it makes progress - // regardless of if extensions are blocking. - // However, due to our current setup, this isn't a likely occurrence and we'd rather - // not have a dedicated thread just for this. If it becomes an issue, we can consider - // creating a separate thread for epoch interruption. - let engine_ref = engine.weak(); - executor - .spawn(async move { - // Somewhat arbitrary interval, as it isn't a guaranteed interval. - // But this is a rough upper bound for how long the extension execution can block on - // `Future::poll`. - const EPOCH_INTERVAL: Duration = Duration::from_millis(100); - let mut timer = Timer::interval(EPOCH_INTERVAL); - while (timer.next().await).is_some() { - // Exit the loop and thread once the engine is dropped. - let Some(engine) = engine_ref.upgrade() else { - break; - }; - engine.increment_epoch(); - } - }) - .detach(); - - engine - }) - .clone() -} - -fn cache_store() -> Arc { - static CACHE_STORE: LazyLock> = - LazyLock::new(|| Arc::new(IncrementalCompilationCache::new())); - CACHE_STORE.clone() -} - -impl WasmHost { - pub fn new( - fs: Arc, - http_client: Arc, - node_runtime: NodeRuntime, - proxy: Arc, - work_dir: PathBuf, - cx: &mut App, - ) -> Arc { - let (tx, mut rx) = mpsc::unbounded::(); - let task = cx.spawn(async move |cx| { - while let Some(message) = rx.next().await { - message(cx).await; - } - }); - - let extension_settings = ExtensionSettings::get_global(cx); - - Arc::new(Self { - engine: wasm_engine(cx.background_executor()), - fs, - work_dir, - http_client, - node_runtime, - proxy, - release_channel: ReleaseChannel::global(cx), - granted_capabilities: extension_settings.granted_capabilities.clone(), - _main_thread_message_task: task, - main_thread_message_tx: tx, - }) - } - - pub fn load_extension( - self: &Arc, - wasm_bytes: Vec, - manifest: &Arc, - cx: &AsyncApp, - ) -> Task> { - let this = self.clone(); - let manifest = manifest.clone(); - let executor = cx.background_executor().clone(); - let load_extension_task = async move { - let zed_api_version = parse_wasm_extension_version(&manifest.id, &wasm_bytes)?; - - let component = Component::from_binary(&this.engine, &wasm_bytes) - .context("failed to compile wasm component")?; - let mut store = wasmtime::Store::new( - &this.engine, - WasmState { - ctx: this.build_wasi_ctx(&manifest).await?, - manifest: manifest.clone(), - table: ResourceTable::new(), - host: this.clone(), - capability_granter: CapabilityGranter::new( - this.granted_capabilities.clone(), - manifest.clone(), - ), - }, - ); - // Store will yield after 1 tick, and get a new deadline of 1 tick after each yield. - store.set_epoch_deadline(1); - store.epoch_deadline_async_yield_and_update(1); - - let mut extension = Extension::instantiate_async( - &executor, - &mut store, - this.release_channel, - zed_api_version.clone(), - &component, - ) - .await?; - - extension - .call_init_extension(&mut store) - .await - .context("failed to initialize wasm extension")?; - - let (tx, mut rx) = mpsc::unbounded::(); - let extension_task = async move { - // note: Setting the thread local here will slowly "poison" all tokio threads - // causing us to not record their panics any longer. - // - // This is fine though, the main zed binary only uses tokio for livekit and wasm extensions. - // Livekit seldom (if ever) panics 🤞 so the likelihood of us missing a panic in sentry is very low. - IS_WASM_THREAD.with(|v| v.store(true, Ordering::Release)); - while let Some(call) = rx.next().await { - (call)(&mut extension, &mut store).await; - } - }; - - anyhow::Ok(( - extension_task, - manifest.clone(), - this.work_dir.join(manifest.id.as_ref()).into(), - tx, - zed_api_version, - )) - }; - cx.spawn(async move |cx| { - let (extension_task, manifest, work_dir, tx, zed_api_version) = - cx.background_executor().spawn(load_extension_task).await?; - // we need to run run the task in a tokio context as wasmtime_wasi may - // call into tokio, accessing its runtime handle when we trigger the `engine.increment_epoch()` above. - let task = Arc::new(gpui_tokio::Tokio::spawn(cx, extension_task)?); - - Ok(WasmExtension { - manifest, - work_dir, - tx, - zed_api_version, - _task: task, - }) - }) - } - - async fn build_wasi_ctx(&self, manifest: &Arc) -> Result { - let extension_work_dir = self.work_dir.join(manifest.id.as_ref()); - self.fs - .create_dir(&extension_work_dir) - .await - .context("failed to create extension work dir")?; - - let file_perms = wasi::FilePerms::all(); - let dir_perms = wasi::DirPerms::all(); - let path = SanitizedPath::new(&extension_work_dir).to_string(); - #[cfg(target_os = "windows")] - let path = path.replace('\\', "/"); - - let mut ctx = wasi::WasiCtxBuilder::new(); - ctx.inherit_stdio() - .env("PWD", &path) - .env("RUST_BACKTRACE", "full"); - - ctx.preopened_dir(&path, ".", dir_perms, file_perms)?; - ctx.preopened_dir(&path, &path, dir_perms, file_perms)?; - - Ok(ctx.build()) - } - - pub fn writeable_path_from_extension(&self, id: &Arc, path: &Path) -> Result { - let extension_work_dir = self.work_dir.join(id.as_ref()); - let path = normalize_path(&extension_work_dir.join(path)); - anyhow::ensure!( - path.starts_with(&extension_work_dir), - "cannot write to path {path:?}", - ); - Ok(path) - } -} - -pub fn parse_wasm_extension_version(extension_id: &str, wasm_bytes: &[u8]) -> Result { - let mut version = None; - - for part in wasmparser::Parser::new(0).parse_all(wasm_bytes) { - if let wasmparser::Payload::CustomSection(s) = - part.context("error parsing wasm extension")? - && s.name() == "zed:api-version" - { - version = parse_wasm_extension_version_custom_section(s.data()); - if version.is_none() { - bail!( - "extension {} has invalid zed:api-version section: {:?}", - extension_id, - s.data() - ); - } - } - } - - // The reason we wait until we're done parsing all of the Wasm bytes to return the version - // is to work around a panic that can happen inside of Wasmtime when the bytes are invalid. - // - // By parsing the entirety of the Wasm bytes before we return, we're able to detect this problem - // earlier as an `Err` rather than as a panic. - version.with_context(|| format!("extension {extension_id} has no zed:api-version section")) -} - -fn parse_wasm_extension_version_custom_section(data: &[u8]) -> Option { - if data.len() == 6 { - Some(Version::new( - u16::from_be_bytes([data[0], data[1]]) as _, - u16::from_be_bytes([data[2], data[3]]) as _, - u16::from_be_bytes([data[4], data[5]]) as _, - )) - } else { - None - } -} - -impl WasmExtension { - pub async fn load( - extension_dir: &Path, - manifest: &Arc, - wasm_host: Arc, - cx: &AsyncApp, - ) -> Result { - let path = extension_dir.join("extension.wasm"); - - let mut wasm_file = wasm_host - .fs - .open_sync(&path) - .await - .context(format!("opening wasm file, path: {path:?}"))?; - - let mut wasm_bytes = Vec::new(); - wasm_file - .read_to_end(&mut wasm_bytes) - .context(format!("reading wasm file, path: {path:?}"))?; - - wasm_host - .load_extension(wasm_bytes, manifest, cx) - .await - .with_context(|| format!("loading wasm extension: {}", manifest.id)) - } - - pub async fn call(&self, f: Fn) -> Result - where - T: 'static + Send, - Fn: 'static - + Send - + for<'a> FnOnce(&'a mut Extension, &'a mut Store) -> BoxFuture<'a, T>, - { - let (return_tx, return_rx) = oneshot::channel(); - self.tx - .unbounded_send(Box::new(move |extension, store| { - async { - let result = f(extension, store).await; - return_tx.send(result).ok(); - } - .boxed() - })) - .map_err(|_| { - anyhow!( - "wasm extension channel should not be closed yet, extension {} (id {})", - self.manifest.name, - self.manifest.id, - ) - })?; - return_rx.await.with_context(|| { - format!( - "wasm extension channel, extension {} (id {})", - self.manifest.name, self.manifest.id, - ) - }) - } -} - -impl WasmState { - fn on_main_thread(&self, f: Fn) -> impl 'static + Future - where - T: 'static + Send, - Fn: 'static + Send + for<'a> FnOnce(&'a mut AsyncApp) -> LocalBoxFuture<'a, T>, - { - let (return_tx, return_rx) = oneshot::channel(); - self.host - .main_thread_message_tx - .clone() - .unbounded_send(Box::new(move |cx| { - async { - let result = f(cx).await; - return_tx.send(result).ok(); - } - .boxed_local() - })) - .unwrap_or_else(|_| { - panic!( - "main thread message channel should not be closed yet, extension {} (id {})", - self.manifest.name, self.manifest.id, - ) - }); - let name = self.manifest.name.clone(); - let id = self.manifest.id.clone(); - async move { - return_rx.await.unwrap_or_else(|_| { - panic!("main thread message channel, extension {name} (id {id})") - }) - } - } - - fn work_dir(&self) -> PathBuf { - self.host.work_dir.join(self.manifest.id.as_ref()) - } - - fn extension_error(&self, message: String) -> anyhow::Error { - anyhow!( - "from extension \"{}\" version {}: {}", - self.manifest.name, - self.manifest.version, - message - ) - } -} - -impl wasi::WasiView for WasmState { - fn table(&mut self) -> &mut ResourceTable { - &mut self.table - } - - fn ctx(&mut self) -> &mut wasi::WasiCtx { - &mut self.ctx - } -} - -/// Wrapper around a mini-moka bounded cache for storing incremental compilation artifacts. -/// Since wasm modules have many similar elements, this can save us a lot of work at the -/// cost of a small memory footprint. However, we don't want this to be unbounded, so we use -/// a LFU/LRU cache to evict less used cache entries. -#[derive(Debug)] -struct IncrementalCompilationCache { - cache: Cache, Vec>, -} - -impl IncrementalCompilationCache { - fn new() -> Self { - let cache = Cache::builder() - // Cap this at 32 MB for now. Our extensions turn into roughly 512kb in the cache, - // which means we could store 64 completely novel extensions in the cache, but in - // practice we will more than that, which is more than enough for our use case. - .max_capacity(32 * 1024 * 1024) - .weigher(|k: &Vec, v: &Vec| (k.len() + v.len()).try_into().unwrap_or(u32::MAX)) - .build(); - Self { cache } - } -} - -impl CacheStore for IncrementalCompilationCache { - fn get(&self, key: &[u8]) -> Option> { - self.cache.get(key).map(|v| v.into()) - } - - fn insert(&self, key: &[u8], value: Vec) -> bool { - self.cache.insert(key.to_vec(), value); - true - } -} diff --git a/crates/extension_host/src/wasm_host/wit.rs b/crates/extension_host/src/wasm_host/wit.rs deleted file mode 100644 index 5058c63365..0000000000 --- a/crates/extension_host/src/wasm_host/wit.rs +++ /dev/null @@ -1,1124 +0,0 @@ -mod since_v0_0_1; -mod since_v0_0_4; -mod since_v0_0_6; -mod since_v0_1_0; -mod since_v0_2_0; -mod since_v0_3_0; -mod since_v0_4_0; -mod since_v0_5_0; -mod since_v0_6_0; -mod since_v0_8_0; -use dap::DebugRequest; -use extension::{DebugTaskDefinition, KeyValueStoreDelegate, WorktreeDelegate}; -use gpui::BackgroundExecutor; -use language::LanguageName; -use lsp::LanguageServerName; -use release_channel::ReleaseChannel; -use task::{DebugScenario, SpawnInTerminal, TaskTemplate, ZedDebugConfig}; - -use crate::wasm_host::wit::since_v0_6_0::dap::StartDebuggingRequestArgumentsRequest; - -use super::{WasmState, wasm_engine}; -use anyhow::{Context as _, Result, anyhow}; -use semver::Version; -use since_v0_8_0 as latest; -use std::{ops::RangeInclusive, path::PathBuf, sync::Arc}; -use wasmtime::{ - Store, - component::{Component, Linker, Resource}, -}; - -#[cfg(test)] -pub use latest::CodeLabelSpanLiteral; -pub use latest::{ - CodeLabel, CodeLabelSpan, Command, DebugAdapterBinary, ExtensionProject, Range, SlashCommand, - zed::extension::context_server::ContextServerConfiguration, - zed::extension::lsp::{ - Completion, CompletionKind, CompletionLabelDetails, InsertTextFormat, Symbol, SymbolKind, - }, - zed::extension::slash_command::{SlashCommandArgumentCompletion, SlashCommandOutput}, -}; -pub use since_v0_0_4::LanguageServerConfig; - -pub fn new_linker( - executor: &BackgroundExecutor, - f: impl Fn(&mut Linker, fn(&mut WasmState) -> &mut WasmState) -> Result<()>, -) -> Linker { - let mut linker = Linker::new(&wasm_engine(executor)); - wasmtime_wasi::add_to_linker_async(&mut linker).unwrap(); - f(&mut linker, wasi_view).unwrap(); - linker -} - -fn wasi_view(state: &mut WasmState) -> &mut WasmState { - state -} - -/// Returns whether the given Wasm API version is supported by the Wasm host. -pub fn is_supported_wasm_api_version(release_channel: ReleaseChannel, version: Version) -> bool { - wasm_api_version_range(release_channel).contains(&version) -} - -/// Returns the Wasm API version range that is supported by the Wasm host. -#[inline(always)] -pub fn wasm_api_version_range(release_channel: ReleaseChannel) -> RangeInclusive { - // Note: The release channel can be used to stage a new version of the extension API. - let _ = release_channel; - - let max_version = match release_channel { - ReleaseChannel::Dev | ReleaseChannel::Nightly => latest::MAX_VERSION, - ReleaseChannel::Stable | ReleaseChannel::Preview => since_v0_6_0::MAX_VERSION, - }; - - since_v0_0_1::MIN_VERSION..=max_version -} - -/// Authorizes access to use unreleased versions of the Wasm API, based on the provided [`ReleaseChannel`]. -/// -/// Note: If there isn't currently an unreleased Wasm API version this function may be unused. Don't delete it! -pub fn authorize_access_to_unreleased_wasm_api_version( - release_channel: ReleaseChannel, -) -> Result<()> { - let allow_unreleased_version = match release_channel { - ReleaseChannel::Dev | ReleaseChannel::Nightly => true, - ReleaseChannel::Stable | ReleaseChannel::Preview => { - // We always allow the latest in tests so that the extension tests pass on release branches. - cfg!(any(test, feature = "test-support")) - } - }; - - anyhow::ensure!( - allow_unreleased_version, - "unreleased versions of the extension API can only be used on development builds of Zed" - ); - - Ok(()) -} - -pub enum Extension { - V0_8_0(since_v0_8_0::Extension), - V0_6_0(since_v0_6_0::Extension), - V0_5_0(since_v0_5_0::Extension), - V0_4_0(since_v0_4_0::Extension), - V0_3_0(since_v0_3_0::Extension), - V0_2_0(since_v0_2_0::Extension), - V0_1_0(since_v0_1_0::Extension), - V0_0_6(since_v0_0_6::Extension), - V0_0_4(since_v0_0_4::Extension), - V0_0_1(since_v0_0_1::Extension), -} - -impl Extension { - pub async fn instantiate_async( - executor: &BackgroundExecutor, - store: &mut Store, - release_channel: ReleaseChannel, - version: Version, - component: &Component, - ) -> Result { - // Note: The release channel can be used to stage a new version of the extension API. - let _ = release_channel; - - if version >= latest::MIN_VERSION { - authorize_access_to_unreleased_wasm_api_version(release_channel)?; - - let extension = - latest::Extension::instantiate_async(store, component, latest::linker(executor)) - .await - .context("failed to instantiate wasm extension")?; - Ok(Self::V0_8_0(extension)) - } else if version >= since_v0_6_0::MIN_VERSION { - let extension = since_v0_6_0::Extension::instantiate_async( - store, - component, - since_v0_6_0::linker(executor), - ) - .await - .context("failed to instantiate wasm extension")?; - Ok(Self::V0_6_0(extension)) - } else if version >= since_v0_5_0::MIN_VERSION { - let extension = since_v0_5_0::Extension::instantiate_async( - store, - component, - since_v0_5_0::linker(executor), - ) - .await - .context("failed to instantiate wasm extension")?; - Ok(Self::V0_5_0(extension)) - } else if version >= since_v0_4_0::MIN_VERSION { - let extension = since_v0_4_0::Extension::instantiate_async( - store, - component, - since_v0_4_0::linker(executor), - ) - .await - .context("failed to instantiate wasm extension")?; - Ok(Self::V0_4_0(extension)) - } else if version >= since_v0_3_0::MIN_VERSION { - let extension = since_v0_3_0::Extension::instantiate_async( - store, - component, - since_v0_3_0::linker(executor), - ) - .await - .context("failed to instantiate wasm extension")?; - Ok(Self::V0_3_0(extension)) - } else if version >= since_v0_2_0::MIN_VERSION { - let extension = since_v0_2_0::Extension::instantiate_async( - store, - component, - since_v0_2_0::linker(executor), - ) - .await - .context("failed to instantiate wasm extension")?; - Ok(Self::V0_2_0(extension)) - } else if version >= since_v0_1_0::MIN_VERSION { - let extension = since_v0_1_0::Extension::instantiate_async( - store, - component, - since_v0_1_0::linker(executor), - ) - .await - .context("failed to instantiate wasm extension")?; - Ok(Self::V0_1_0(extension)) - } else if version >= since_v0_0_6::MIN_VERSION { - let extension = since_v0_0_6::Extension::instantiate_async( - store, - component, - since_v0_0_6::linker(executor), - ) - .await - .context("failed to instantiate wasm extension")?; - Ok(Self::V0_0_6(extension)) - } else if version >= since_v0_0_4::MIN_VERSION { - let extension = since_v0_0_4::Extension::instantiate_async( - store, - component, - since_v0_0_4::linker(executor), - ) - .await - .context("failed to instantiate wasm extension")?; - Ok(Self::V0_0_4(extension)) - } else { - let extension = since_v0_0_1::Extension::instantiate_async( - store, - component, - since_v0_0_1::linker(executor), - ) - .await - .context("failed to instantiate wasm extension")?; - Ok(Self::V0_0_1(extension)) - } - } - - pub async fn call_init_extension(&self, store: &mut Store) -> Result<()> { - match self { - Extension::V0_8_0(ext) => ext.call_init_extension(store).await, - Extension::V0_6_0(ext) => ext.call_init_extension(store).await, - Extension::V0_5_0(ext) => ext.call_init_extension(store).await, - Extension::V0_4_0(ext) => ext.call_init_extension(store).await, - Extension::V0_3_0(ext) => ext.call_init_extension(store).await, - Extension::V0_2_0(ext) => ext.call_init_extension(store).await, - Extension::V0_1_0(ext) => ext.call_init_extension(store).await, - Extension::V0_0_6(ext) => ext.call_init_extension(store).await, - Extension::V0_0_4(ext) => ext.call_init_extension(store).await, - Extension::V0_0_1(ext) => ext.call_init_extension(store).await, - } - } - - pub async fn call_language_server_command( - &self, - store: &mut Store, - language_server_id: &LanguageServerName, - language_name: &LanguageName, - resource: Resource>, - ) -> Result> { - match self { - Extension::V0_8_0(ext) => { - ext.call_language_server_command(store, &language_server_id.0, resource) - .await - } - Extension::V0_6_0(ext) => { - ext.call_language_server_command(store, &language_server_id.0, resource) - .await - } - Extension::V0_5_0(ext) => { - ext.call_language_server_command(store, &language_server_id.0, resource) - .await - } - Extension::V0_4_0(ext) => { - ext.call_language_server_command(store, &language_server_id.0, resource) - .await - } - Extension::V0_3_0(ext) => { - ext.call_language_server_command(store, &language_server_id.0, resource) - .await - } - Extension::V0_2_0(ext) => Ok(ext - .call_language_server_command(store, &language_server_id.0, resource) - .await? - .map(|command| command.into())), - Extension::V0_1_0(ext) => Ok(ext - .call_language_server_command(store, &language_server_id.0, resource) - .await? - .map(|command| command.into())), - Extension::V0_0_6(ext) => Ok(ext - .call_language_server_command(store, &language_server_id.0, resource) - .await? - .map(|command| command.into())), - Extension::V0_0_4(ext) => Ok(ext - .call_language_server_command( - store, - &LanguageServerConfig { - name: language_server_id.0.to_string(), - language_name: language_name.to_string(), - }, - resource, - ) - .await? - .map(|command| command.into())), - Extension::V0_0_1(ext) => Ok(ext - .call_language_server_command( - store, - &LanguageServerConfig { - name: language_server_id.0.to_string(), - language_name: language_name.to_string(), - } - .into(), - resource, - ) - .await? - .map(|command| command.into())), - } - } - - pub async fn call_language_server_initialization_options( - &self, - store: &mut Store, - language_server_id: &LanguageServerName, - language_name: &LanguageName, - resource: Resource>, - ) -> Result, String>> { - match self { - Extension::V0_8_0(ext) => { - ext.call_language_server_initialization_options( - store, - &language_server_id.0, - resource, - ) - .await - } - Extension::V0_6_0(ext) => { - ext.call_language_server_initialization_options( - store, - &language_server_id.0, - resource, - ) - .await - } - Extension::V0_5_0(ext) => { - ext.call_language_server_initialization_options( - store, - &language_server_id.0, - resource, - ) - .await - } - Extension::V0_4_0(ext) => { - ext.call_language_server_initialization_options( - store, - &language_server_id.0, - resource, - ) - .await - } - Extension::V0_3_0(ext) => { - ext.call_language_server_initialization_options( - store, - &language_server_id.0, - resource, - ) - .await - } - Extension::V0_2_0(ext) => { - ext.call_language_server_initialization_options( - store, - &language_server_id.0, - resource, - ) - .await - } - Extension::V0_1_0(ext) => { - ext.call_language_server_initialization_options( - store, - &language_server_id.0, - resource, - ) - .await - } - Extension::V0_0_6(ext) => { - ext.call_language_server_initialization_options( - store, - &language_server_id.0, - resource, - ) - .await - } - Extension::V0_0_4(ext) => { - ext.call_language_server_initialization_options( - store, - &LanguageServerConfig { - name: language_server_id.0.to_string(), - language_name: language_name.to_string(), - }, - resource, - ) - .await - } - Extension::V0_0_1(ext) => { - ext.call_language_server_initialization_options( - store, - &LanguageServerConfig { - name: language_server_id.0.to_string(), - language_name: language_name.to_string(), - } - .into(), - resource, - ) - .await - } - } - } - - pub async fn call_language_server_workspace_configuration( - &self, - store: &mut Store, - language_server_id: &LanguageServerName, - resource: Resource>, - ) -> Result, String>> { - match self { - Extension::V0_8_0(ext) => { - ext.call_language_server_workspace_configuration( - store, - &language_server_id.0, - resource, - ) - .await - } - Extension::V0_6_0(ext) => { - ext.call_language_server_workspace_configuration( - store, - &language_server_id.0, - resource, - ) - .await - } - Extension::V0_5_0(ext) => { - ext.call_language_server_workspace_configuration( - store, - &language_server_id.0, - resource, - ) - .await - } - Extension::V0_4_0(ext) => { - ext.call_language_server_workspace_configuration( - store, - &language_server_id.0, - resource, - ) - .await - } - Extension::V0_3_0(ext) => { - ext.call_language_server_workspace_configuration( - store, - &language_server_id.0, - resource, - ) - .await - } - Extension::V0_2_0(ext) => { - ext.call_language_server_workspace_configuration( - store, - &language_server_id.0, - resource, - ) - .await - } - Extension::V0_1_0(ext) => { - ext.call_language_server_workspace_configuration( - store, - &language_server_id.0, - resource, - ) - .await - } - Extension::V0_0_6(ext) => { - ext.call_language_server_workspace_configuration( - store, - &language_server_id.0, - resource, - ) - .await - } - Extension::V0_0_4(_) | Extension::V0_0_1(_) => Ok(Ok(None)), - } - } - - pub async fn call_language_server_additional_initialization_options( - &self, - store: &mut Store, - language_server_id: &LanguageServerName, - target_language_server_id: &LanguageServerName, - resource: Resource>, - ) -> Result, String>> { - match self { - Extension::V0_8_0(ext) => { - ext.call_language_server_additional_initialization_options( - store, - &language_server_id.0, - &target_language_server_id.0, - resource, - ) - .await - } - Extension::V0_6_0(ext) => { - ext.call_language_server_additional_initialization_options( - store, - &language_server_id.0, - &target_language_server_id.0, - resource, - ) - .await - } - Extension::V0_5_0(ext) => { - ext.call_language_server_additional_initialization_options( - store, - &language_server_id.0, - &target_language_server_id.0, - resource, - ) - .await - } - Extension::V0_4_0(ext) => { - ext.call_language_server_additional_initialization_options( - store, - &language_server_id.0, - &target_language_server_id.0, - resource, - ) - .await - } - Extension::V0_3_0(_) - | Extension::V0_2_0(_) - | Extension::V0_1_0(_) - | Extension::V0_0_6(_) - | Extension::V0_0_4(_) - | Extension::V0_0_1(_) => Ok(Ok(None)), - } - } - - pub async fn call_language_server_additional_workspace_configuration( - &self, - store: &mut Store, - language_server_id: &LanguageServerName, - target_language_server_id: &LanguageServerName, - resource: Resource>, - ) -> Result, String>> { - match self { - Extension::V0_8_0(ext) => { - ext.call_language_server_additional_workspace_configuration( - store, - &language_server_id.0, - &target_language_server_id.0, - resource, - ) - .await - } - Extension::V0_6_0(ext) => { - ext.call_language_server_additional_workspace_configuration( - store, - &language_server_id.0, - &target_language_server_id.0, - resource, - ) - .await - } - Extension::V0_5_0(ext) => { - ext.call_language_server_additional_workspace_configuration( - store, - &language_server_id.0, - &target_language_server_id.0, - resource, - ) - .await - } - Extension::V0_4_0(ext) => { - ext.call_language_server_additional_workspace_configuration( - store, - &language_server_id.0, - &target_language_server_id.0, - resource, - ) - .await - } - Extension::V0_3_0(_) - | Extension::V0_2_0(_) - | Extension::V0_1_0(_) - | Extension::V0_0_6(_) - | Extension::V0_0_4(_) - | Extension::V0_0_1(_) => Ok(Ok(None)), - } - } - - pub async fn call_labels_for_completions( - &self, - store: &mut Store, - language_server_id: &LanguageServerName, - completions: Vec, - ) -> Result>, String>> { - match self { - Extension::V0_8_0(ext) => { - ext.call_labels_for_completions(store, &language_server_id.0, &completions) - .await - } - Extension::V0_6_0(ext) => Ok(ext - .call_labels_for_completions( - store, - &language_server_id.0, - &completions.into_iter().collect::>(), - ) - .await? - .map(|labels| { - labels - .into_iter() - .map(|label| label.map(Into::into)) - .collect() - })), - Extension::V0_5_0(ext) => Ok(ext - .call_labels_for_completions( - store, - &language_server_id.0, - &completions.into_iter().collect::>(), - ) - .await? - .map(|labels| { - labels - .into_iter() - .map(|label| label.map(Into::into)) - .collect() - })), - Extension::V0_4_0(ext) => Ok(ext - .call_labels_for_completions( - store, - &language_server_id.0, - &completions.into_iter().collect::>(), - ) - .await? - .map(|labels| { - labels - .into_iter() - .map(|label| label.map(Into::into)) - .collect() - })), - Extension::V0_3_0(ext) => Ok(ext - .call_labels_for_completions( - store, - &language_server_id.0, - &completions.into_iter().collect::>(), - ) - .await? - .map(|labels| { - labels - .into_iter() - .map(|label| label.map(Into::into)) - .collect() - })), - Extension::V0_2_0(ext) => Ok(ext - .call_labels_for_completions( - store, - &language_server_id.0, - &completions.into_iter().collect::>(), - ) - .await? - .map(|labels| { - labels - .into_iter() - .map(|label| label.map(Into::into)) - .collect() - })), - Extension::V0_1_0(ext) => Ok(ext - .call_labels_for_completions( - store, - &language_server_id.0, - &completions.into_iter().map(Into::into).collect::>(), - ) - .await? - .map(|labels| { - labels - .into_iter() - .map(|label| label.map(Into::into)) - .collect() - })), - Extension::V0_0_6(ext) => Ok(ext - .call_labels_for_completions( - store, - &language_server_id.0, - &completions.into_iter().map(Into::into).collect::>(), - ) - .await? - .map(|labels| { - labels - .into_iter() - .map(|label| label.map(Into::into)) - .collect() - })), - Extension::V0_0_1(_) | Extension::V0_0_4(_) => Ok(Ok(Vec::new())), - } - } - - pub async fn call_labels_for_symbols( - &self, - store: &mut Store, - language_server_id: &LanguageServerName, - symbols: Vec, - ) -> Result>, String>> { - match self { - Extension::V0_8_0(ext) => { - ext.call_labels_for_symbols(store, &language_server_id.0, &symbols) - .await - } - Extension::V0_6_0(ext) => Ok(ext - .call_labels_for_symbols( - store, - &language_server_id.0, - &symbols.into_iter().collect::>(), - ) - .await? - .map(|labels| { - labels - .into_iter() - .map(|label| label.map(Into::into)) - .collect() - })), - Extension::V0_5_0(ext) => Ok(ext - .call_labels_for_symbols( - store, - &language_server_id.0, - &symbols.into_iter().collect::>(), - ) - .await? - .map(|labels| { - labels - .into_iter() - .map(|label| label.map(Into::into)) - .collect() - })), - Extension::V0_4_0(ext) => Ok(ext - .call_labels_for_symbols( - store, - &language_server_id.0, - &symbols.into_iter().collect::>(), - ) - .await? - .map(|labels| { - labels - .into_iter() - .map(|label| label.map(Into::into)) - .collect() - })), - Extension::V0_3_0(ext) => Ok(ext - .call_labels_for_symbols( - store, - &language_server_id.0, - &symbols.into_iter().collect::>(), - ) - .await? - .map(|labels| { - labels - .into_iter() - .map(|label| label.map(Into::into)) - .collect() - })), - Extension::V0_2_0(ext) => Ok(ext - .call_labels_for_symbols( - store, - &language_server_id.0, - &symbols.into_iter().collect::>(), - ) - .await? - .map(|labels| { - labels - .into_iter() - .map(|label| label.map(Into::into)) - .collect() - })), - Extension::V0_1_0(ext) => Ok(ext - .call_labels_for_symbols( - store, - &language_server_id.0, - &symbols.into_iter().map(Into::into).collect::>(), - ) - .await? - .map(|labels| { - labels - .into_iter() - .map(|label| label.map(Into::into)) - .collect() - })), - Extension::V0_0_6(ext) => Ok(ext - .call_labels_for_symbols( - store, - &language_server_id.0, - &symbols.into_iter().map(Into::into).collect::>(), - ) - .await? - .map(|labels| { - labels - .into_iter() - .map(|label| label.map(Into::into)) - .collect() - })), - Extension::V0_0_1(_) | Extension::V0_0_4(_) => Ok(Ok(Vec::new())), - } - } - - pub async fn call_complete_slash_command_argument( - &self, - store: &mut Store, - command: &SlashCommand, - arguments: &[String], - ) -> Result, String>> { - match self { - Extension::V0_8_0(ext) => { - ext.call_complete_slash_command_argument(store, command, arguments) - .await - } - Extension::V0_6_0(ext) => { - ext.call_complete_slash_command_argument(store, command, arguments) - .await - } - Extension::V0_5_0(ext) => { - ext.call_complete_slash_command_argument(store, command, arguments) - .await - } - Extension::V0_4_0(ext) => { - ext.call_complete_slash_command_argument(store, command, arguments) - .await - } - Extension::V0_3_0(ext) => { - ext.call_complete_slash_command_argument(store, command, arguments) - .await - } - Extension::V0_2_0(ext) => { - ext.call_complete_slash_command_argument(store, command, arguments) - .await - } - Extension::V0_1_0(ext) => { - ext.call_complete_slash_command_argument(store, command, arguments) - .await - } - Extension::V0_0_1(_) | Extension::V0_0_4(_) | Extension::V0_0_6(_) => { - Ok(Ok(Vec::new())) - } - } - } - - pub async fn call_run_slash_command( - &self, - store: &mut Store, - command: &SlashCommand, - arguments: &[String], - resource: Option>>, - ) -> Result> { - match self { - Extension::V0_8_0(ext) => { - ext.call_run_slash_command(store, command, arguments, resource) - .await - } - Extension::V0_6_0(ext) => { - ext.call_run_slash_command(store, command, arguments, resource) - .await - } - Extension::V0_5_0(ext) => { - ext.call_run_slash_command(store, command, arguments, resource) - .await - } - Extension::V0_4_0(ext) => { - ext.call_run_slash_command(store, command, arguments, resource) - .await - } - Extension::V0_3_0(ext) => { - ext.call_run_slash_command(store, command, arguments, resource) - .await - } - Extension::V0_2_0(ext) => { - ext.call_run_slash_command(store, command, arguments, resource) - .await - } - Extension::V0_1_0(ext) => { - ext.call_run_slash_command(store, command, arguments, resource) - .await - } - Extension::V0_0_1(_) | Extension::V0_0_4(_) | Extension::V0_0_6(_) => { - anyhow::bail!("`run_slash_command` not available prior to v0.1.0"); - } - } - } - - pub async fn call_context_server_command( - &self, - store: &mut Store, - context_server_id: Arc, - project: Resource, - ) -> Result> { - match self { - Extension::V0_8_0(ext) => { - ext.call_context_server_command(store, &context_server_id, project) - .await - } - Extension::V0_6_0(ext) => { - ext.call_context_server_command(store, &context_server_id, project) - .await - } - Extension::V0_5_0(ext) => { - ext.call_context_server_command(store, &context_server_id, project) - .await - } - Extension::V0_4_0(ext) => { - ext.call_context_server_command(store, &context_server_id, project) - .await - } - Extension::V0_3_0(ext) => { - ext.call_context_server_command(store, &context_server_id, project) - .await - } - Extension::V0_2_0(ext) => Ok(ext - .call_context_server_command(store, &context_server_id, project) - .await? - .map(Into::into)), - Extension::V0_0_1(_) - | Extension::V0_0_4(_) - | Extension::V0_0_6(_) - | Extension::V0_1_0(_) => { - anyhow::bail!("`context_server_command` not available prior to v0.2.0"); - } - } - } - - pub async fn call_context_server_configuration( - &self, - store: &mut Store, - context_server_id: Arc, - project: Resource, - ) -> Result, String>> { - match self { - Extension::V0_8_0(ext) => { - ext.call_context_server_configuration(store, &context_server_id, project) - .await - } - Extension::V0_6_0(ext) => { - ext.call_context_server_configuration(store, &context_server_id, project) - .await - } - Extension::V0_5_0(ext) => { - ext.call_context_server_configuration(store, &context_server_id, project) - .await - } - Extension::V0_0_1(_) - | Extension::V0_0_4(_) - | Extension::V0_0_6(_) - | Extension::V0_1_0(_) - | Extension::V0_2_0(_) - | Extension::V0_3_0(_) - | Extension::V0_4_0(_) => { - anyhow::bail!("`context_server_configuration` not available prior to v0.5.0"); - } - } - } - - pub async fn call_suggest_docs_packages( - &self, - store: &mut Store, - provider: &str, - ) -> Result, String>> { - match self { - Extension::V0_8_0(ext) => ext.call_suggest_docs_packages(store, provider).await, - Extension::V0_6_0(ext) => ext.call_suggest_docs_packages(store, provider).await, - Extension::V0_5_0(ext) => ext.call_suggest_docs_packages(store, provider).await, - Extension::V0_4_0(ext) => ext.call_suggest_docs_packages(store, provider).await, - Extension::V0_3_0(ext) => ext.call_suggest_docs_packages(store, provider).await, - Extension::V0_2_0(ext) => ext.call_suggest_docs_packages(store, provider).await, - Extension::V0_1_0(ext) => ext.call_suggest_docs_packages(store, provider).await, - Extension::V0_0_1(_) | Extension::V0_0_4(_) | Extension::V0_0_6(_) => { - anyhow::bail!("`suggest_docs_packages` not available prior to v0.1.0"); - } - } - } - - pub async fn call_index_docs( - &self, - store: &mut Store, - provider: &str, - package_name: &str, - kv_store: Resource>, - ) -> Result> { - match self { - Extension::V0_8_0(ext) => { - ext.call_index_docs(store, provider, package_name, kv_store) - .await - } - Extension::V0_6_0(ext) => { - ext.call_index_docs(store, provider, package_name, kv_store) - .await - } - Extension::V0_5_0(ext) => { - ext.call_index_docs(store, provider, package_name, kv_store) - .await - } - Extension::V0_4_0(ext) => { - ext.call_index_docs(store, provider, package_name, kv_store) - .await - } - Extension::V0_3_0(ext) => { - ext.call_index_docs(store, provider, package_name, kv_store) - .await - } - Extension::V0_2_0(ext) => { - ext.call_index_docs(store, provider, package_name, kv_store) - .await - } - Extension::V0_1_0(ext) => { - ext.call_index_docs(store, provider, package_name, kv_store) - .await - } - Extension::V0_0_1(_) | Extension::V0_0_4(_) | Extension::V0_0_6(_) => { - anyhow::bail!("`index_docs` not available prior to v0.1.0"); - } - } - } - - pub async fn call_get_dap_binary( - &self, - store: &mut Store, - adapter_name: Arc, - task: DebugTaskDefinition, - user_installed_path: Option, - resource: Resource>, - ) -> Result> { - match self { - Extension::V0_6_0(ext) => { - let dap_binary = ext - .call_get_dap_binary( - store, - &adapter_name, - &task.try_into()?, - user_installed_path.as_ref().and_then(|p| p.to_str()), - resource, - ) - .await? - .map_err(|e| anyhow!("{e:?}"))?; - - Ok(Ok(dap_binary)) - } - _ => anyhow::bail!("`get_dap_binary` not available prior to v0.6.0"), - } - } - - pub async fn call_dap_request_kind( - &self, - store: &mut Store, - adapter_name: Arc, - config: serde_json::Value, - ) -> Result> { - match self { - Extension::V0_6_0(ext) => { - let config = - serde_json::to_string(&config).context("Adapter config is not a valid JSON")?; - let dap_binary = ext - .call_dap_request_kind(store, &adapter_name, &config) - .await? - .map_err(|e| anyhow!("{e:?}"))?; - - Ok(Ok(dap_binary)) - } - _ => anyhow::bail!("`dap_request_kind` not available prior to v0.6.0"), - } - } - - pub async fn call_dap_config_to_scenario( - &self, - store: &mut Store, - config: ZedDebugConfig, - ) -> Result> { - match self { - Extension::V0_6_0(ext) => { - let config = config.into(); - let dap_binary = ext - .call_dap_config_to_scenario(store, &config) - .await? - .map_err(|e| anyhow!("{e:?}"))?; - - Ok(Ok(dap_binary.try_into()?)) - } - _ => anyhow::bail!("`dap_config_to_scenario` not available prior to v0.6.0"), - } - } - - pub async fn call_dap_locator_create_scenario( - &self, - store: &mut Store, - locator_name: String, - build_config_template: TaskTemplate, - resolved_label: String, - debug_adapter_name: String, - ) -> Result> { - match self { - Extension::V0_6_0(ext) => { - let build_config_template = build_config_template.into(); - let dap_binary = ext - .call_dap_locator_create_scenario( - store, - &locator_name, - &build_config_template, - &resolved_label, - &debug_adapter_name, - ) - .await?; - - Ok(dap_binary.map(TryInto::try_into).transpose()?) - } - _ => anyhow::bail!("`dap_locator_create_scenario` not available prior to v0.6.0"), - } - } - - pub async fn call_run_dap_locator( - &self, - store: &mut Store, - locator_name: String, - resolved_build_task: SpawnInTerminal, - ) -> Result> { - match self { - Extension::V0_6_0(ext) => { - let build_config_template = resolved_build_task.try_into()?; - let dap_request = ext - .call_run_dap_locator(store, &locator_name, &build_config_template) - .await? - .map_err(|e| anyhow!("{e:?}"))?; - - Ok(Ok(dap_request.into())) - } - _ => anyhow::bail!("`dap_locator_create_scenario` not available prior to v0.6.0"), - } - } -} - -trait ToWasmtimeResult { - fn to_wasmtime_result(self) -> wasmtime::Result>; -} - -impl ToWasmtimeResult for Result { - fn to_wasmtime_result(self) -> wasmtime::Result> { - Ok(self.map_err(|error| format!("{error:?}"))) - } -} diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs b/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs deleted file mode 100644 index 17d5c00a9a..0000000000 --- a/crates/extension_host/src/wasm_host/wit/since_v0_0_1.rs +++ /dev/null @@ -1,158 +0,0 @@ -use super::latest; -use crate::wasm_host::WasmState; -use crate::wasm_host::wit::since_v0_0_4; -use anyhow::Result; -use extension::{ExtensionLanguageServerProxy, WorktreeDelegate}; -use gpui::BackgroundExecutor; -use language::BinaryStatus; -use semver::Version; -use std::sync::{Arc, OnceLock}; -use wasmtime::component::{Linker, Resource}; - -pub const MIN_VERSION: Version = Version::new(0, 0, 1); - -wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, - path: "../extension_api/wit/since_v0.0.1", - with: { - "worktree": ExtensionWorktree, - "zed:extension/github": latest::zed::extension::github, - "zed:extension/platform": latest::zed::extension::platform, - }, -}); - -pub type ExtensionWorktree = Arc; - -pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { - static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) -} - -impl From for latest::DownloadedFileType { - fn from(value: DownloadedFileType) -> Self { - match value { - DownloadedFileType::Gzip => latest::DownloadedFileType::Gzip, - DownloadedFileType::GzipTar => latest::DownloadedFileType::GzipTar, - DownloadedFileType::Zip => latest::DownloadedFileType::Zip, - DownloadedFileType::Uncompressed => latest::DownloadedFileType::Uncompressed, - } - } -} - -impl From for LanguageServerConfig { - fn from(value: since_v0_0_4::LanguageServerConfig) -> Self { - Self { - name: value.name, - language_name: value.language_name, - } - } -} - -impl From for latest::Command { - fn from(value: Command) -> Self { - Self { - command: value.command, - args: value.args, - env: value.env, - } - } -} - -impl HostWorktree for WasmState { - async fn read_text_file( - &mut self, - delegate: Resource>, - path: String, - ) -> wasmtime::Result> { - latest::HostWorktree::read_text_file(self, delegate, path).await - } - - async fn shell_env( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::shell_env(self, delegate).await - } - - async fn which( - &mut self, - delegate: Resource>, - binary_name: String, - ) -> wasmtime::Result> { - latest::HostWorktree::which(self, delegate, binary_name).await - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - Ok(()) - } -} - -impl ExtensionImports for WasmState { - async fn node_binary_path(&mut self) -> wasmtime::Result> { - latest::nodejs::Host::node_binary_path(self).await - } - - async fn npm_package_latest_version( - &mut self, - package_name: String, - ) -> wasmtime::Result> { - latest::nodejs::Host::npm_package_latest_version(self, package_name).await - } - - async fn npm_package_installed_version( - &mut self, - package_name: String, - ) -> wasmtime::Result, String>> { - latest::nodejs::Host::npm_package_installed_version(self, package_name).await - } - - async fn npm_install_package( - &mut self, - package_name: String, - version: String, - ) -> wasmtime::Result> { - latest::nodejs::Host::npm_install_package(self, package_name, version).await - } - - async fn latest_github_release( - &mut self, - repo: String, - options: GithubReleaseOptions, - ) -> wasmtime::Result> { - latest::zed::extension::github::Host::latest_github_release(self, repo, options).await - } - - async fn current_platform(&mut self) -> Result<(Os, Architecture)> { - latest::zed::extension::platform::Host::current_platform(self).await - } - - async fn set_language_server_installation_status( - &mut self, - server_name: String, - status: LanguageServerInstallationStatus, - ) -> wasmtime::Result<()> { - let status = match status { - LanguageServerInstallationStatus::CheckingForUpdate => BinaryStatus::CheckingForUpdate, - LanguageServerInstallationStatus::Downloading => BinaryStatus::Downloading, - LanguageServerInstallationStatus::Cached - | LanguageServerInstallationStatus::Downloaded => BinaryStatus::None, - LanguageServerInstallationStatus::Failed(error) => BinaryStatus::Failed { error }, - }; - - self.host - .proxy - .update_language_server_status(lsp::LanguageServerName(server_name.into()), status); - - Ok(()) - } - - async fn download_file( - &mut self, - url: String, - path: String, - file_type: DownloadedFileType, - ) -> wasmtime::Result> { - latest::ExtensionImports::download_file(self, url, path, file_type.into()).await - } -} diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_0_4.rs b/crates/extension_host/src/wasm_host/wit/since_v0_0_4.rs deleted file mode 100644 index 11b2e9f661..0000000000 --- a/crates/extension_host/src/wasm_host/wit/since_v0_0_4.rs +++ /dev/null @@ -1,164 +0,0 @@ -use super::latest; -use crate::wasm_host::WasmState; -use anyhow::Result; -use extension::WorktreeDelegate; -use gpui::BackgroundExecutor; -use semver::Version; -use std::sync::{Arc, OnceLock}; -use wasmtime::component::{Linker, Resource}; - -pub const MIN_VERSION: Version = Version::new(0, 0, 4); - -wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, - path: "../extension_api/wit/since_v0.0.4", - with: { - "worktree": ExtensionWorktree, - "zed:extension/github": latest::zed::extension::github, - "zed:extension/platform": latest::zed::extension::platform, - }, -}); - -pub type ExtensionWorktree = Arc; - -pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { - static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) -} - -impl From for latest::DownloadedFileType { - fn from(value: DownloadedFileType) -> Self { - match value { - DownloadedFileType::Gzip => latest::DownloadedFileType::Gzip, - DownloadedFileType::GzipTar => latest::DownloadedFileType::GzipTar, - DownloadedFileType::Zip => latest::DownloadedFileType::Zip, - DownloadedFileType::Uncompressed => latest::DownloadedFileType::Uncompressed, - } - } -} - -impl From for latest::LanguageServerInstallationStatus { - fn from(value: LanguageServerInstallationStatus) -> Self { - match value { - LanguageServerInstallationStatus::None => { - latest::LanguageServerInstallationStatus::None - } - LanguageServerInstallationStatus::Downloading => { - latest::LanguageServerInstallationStatus::Downloading - } - LanguageServerInstallationStatus::CheckingForUpdate => { - latest::LanguageServerInstallationStatus::CheckingForUpdate - } - LanguageServerInstallationStatus::Failed(error) => { - latest::LanguageServerInstallationStatus::Failed(error) - } - } - } -} - -impl From for latest::Command { - fn from(value: Command) -> Self { - Self { - command: value.command, - args: value.args, - env: value.env, - } - } -} - -impl HostWorktree for WasmState { - async fn read_text_file( - &mut self, - delegate: Resource>, - path: String, - ) -> wasmtime::Result> { - latest::HostWorktree::read_text_file(self, delegate, path).await - } - - async fn shell_env( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::shell_env(self, delegate).await - } - - async fn which( - &mut self, - delegate: Resource>, - binary_name: String, - ) -> wasmtime::Result> { - latest::HostWorktree::which(self, delegate, binary_name).await - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of worktrees. - Ok(()) - } -} - -impl ExtensionImports for WasmState { - async fn node_binary_path(&mut self) -> wasmtime::Result> { - latest::nodejs::Host::node_binary_path(self).await - } - - async fn npm_package_latest_version( - &mut self, - package_name: String, - ) -> wasmtime::Result> { - latest::nodejs::Host::npm_package_latest_version(self, package_name).await - } - - async fn npm_package_installed_version( - &mut self, - package_name: String, - ) -> wasmtime::Result, String>> { - latest::nodejs::Host::npm_package_installed_version(self, package_name).await - } - - async fn npm_install_package( - &mut self, - package_name: String, - version: String, - ) -> wasmtime::Result> { - latest::nodejs::Host::npm_install_package(self, package_name, version).await - } - - async fn latest_github_release( - &mut self, - repo: String, - options: GithubReleaseOptions, - ) -> wasmtime::Result> { - latest::zed::extension::github::Host::latest_github_release(self, repo, options).await - } - - async fn current_platform(&mut self) -> Result<(Os, Architecture)> { - latest::zed::extension::platform::Host::current_platform(self).await - } - - async fn set_language_server_installation_status( - &mut self, - server_name: String, - status: LanguageServerInstallationStatus, - ) -> wasmtime::Result<()> { - latest::ExtensionImports::set_language_server_installation_status( - self, - server_name, - status.into(), - ) - .await - } - - async fn download_file( - &mut self, - url: String, - path: String, - file_type: DownloadedFileType, - ) -> wasmtime::Result> { - latest::ExtensionImports::download_file(self, url, path, file_type.into()).await - } - - async fn make_file_executable(&mut self, path: String) -> wasmtime::Result> { - latest::ExtensionImports::make_file_executable(self, path).await - } -} diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_0_6.rs b/crates/extension_host/src/wasm_host/wit/since_v0_0_6.rs deleted file mode 100644 index 835a2b30fb..0000000000 --- a/crates/extension_host/src/wasm_host/wit/since_v0_0_6.rs +++ /dev/null @@ -1,197 +0,0 @@ -use super::{latest, since_v0_1_0}; -use crate::wasm_host::WasmState; -use anyhow::Result; -use extension::WorktreeDelegate; -use gpui::BackgroundExecutor; -use semver::Version; -use std::sync::{Arc, OnceLock}; -use wasmtime::component::{Linker, Resource}; - -pub const MIN_VERSION: Version = Version::new(0, 0, 6); - -wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, - path: "../extension_api/wit/since_v0.0.6", - with: { - "worktree": ExtensionWorktree, - "zed:extension/github": latest::zed::extension::github, - "zed:extension/lsp": since_v0_1_0::zed::extension::lsp, - "zed:extension/nodejs": latest::zed::extension::nodejs, - "zed:extension/platform": latest::zed::extension::platform, - }, -}); - -mod settings { - #![allow(dead_code)] - include!(concat!(env!("OUT_DIR"), "/since_v0.0.6/settings.rs")); -} - -pub type ExtensionWorktree = Arc; - -pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { - static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) -} - -impl From for latest::Command { - fn from(value: Command) -> Self { - Self { - command: value.command, - args: value.args, - env: value.env, - } - } -} - -impl From for latest::SettingsLocation { - fn from(value: SettingsLocation) -> Self { - Self { - worktree_id: value.worktree_id, - path: value.path, - } - } -} - -impl From for latest::LanguageServerInstallationStatus { - fn from(value: LanguageServerInstallationStatus) -> Self { - match value { - LanguageServerInstallationStatus::None => Self::None, - LanguageServerInstallationStatus::Downloading => Self::Downloading, - LanguageServerInstallationStatus::CheckingForUpdate => Self::CheckingForUpdate, - LanguageServerInstallationStatus::Failed(message) => Self::Failed(message), - } - } -} - -impl From for latest::DownloadedFileType { - fn from(value: DownloadedFileType) -> Self { - match value { - DownloadedFileType::Gzip => Self::Gzip, - DownloadedFileType::GzipTar => Self::GzipTar, - DownloadedFileType::Zip => Self::Zip, - DownloadedFileType::Uncompressed => Self::Uncompressed, - } - } -} - -impl From for latest::Range { - fn from(value: Range) -> Self { - Self { - start: value.start, - end: value.end, - } - } -} - -impl From for latest::CodeLabelSpan { - fn from(value: CodeLabelSpan) -> Self { - match value { - CodeLabelSpan::CodeRange(range) => Self::CodeRange(range.into()), - CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()), - } - } -} - -impl From for latest::CodeLabelSpanLiteral { - fn from(value: CodeLabelSpanLiteral) -> Self { - Self { - text: value.text, - highlight_name: value.highlight_name, - } - } -} - -impl From for latest::CodeLabel { - fn from(value: CodeLabel) -> Self { - Self { - code: value.code, - spans: value.spans.into_iter().map(Into::into).collect(), - filter_range: value.filter_range.into(), - } - } -} - -impl HostWorktree for WasmState { - async fn id(&mut self, delegate: Resource>) -> wasmtime::Result { - latest::HostWorktree::id(self, delegate).await - } - - async fn root_path( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::root_path(self, delegate).await - } - - async fn read_text_file( - &mut self, - delegate: Resource>, - path: String, - ) -> wasmtime::Result> { - latest::HostWorktree::read_text_file(self, delegate, path).await - } - - async fn shell_env( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::shell_env(self, delegate).await - } - - async fn which( - &mut self, - delegate: Resource>, - binary_name: String, - ) -> wasmtime::Result> { - latest::HostWorktree::which(self, delegate, binary_name).await - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of worktrees. - Ok(()) - } -} - -impl ExtensionImports for WasmState { - async fn get_settings( - &mut self, - location: Option, - category: String, - key: Option, - ) -> wasmtime::Result> { - latest::ExtensionImports::get_settings( - self, - location.map(|location| location.into()), - category, - key, - ) - .await - } - - async fn set_language_server_installation_status( - &mut self, - server_name: String, - status: LanguageServerInstallationStatus, - ) -> wasmtime::Result<()> { - latest::ExtensionImports::set_language_server_installation_status( - self, - server_name, - status.into(), - ) - .await - } - - async fn download_file( - &mut self, - url: String, - path: String, - file_type: DownloadedFileType, - ) -> wasmtime::Result> { - latest::ExtensionImports::download_file(self, url, path, file_type.into()).await - } - - async fn make_file_executable(&mut self, path: String) -> wasmtime::Result> { - latest::ExtensionImports::make_file_executable(self, path).await - } -} diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs deleted file mode 100644 index a7a20f6dc7..0000000000 --- a/crates/extension_host/src/wasm_host/wit/since_v0_1_0.rs +++ /dev/null @@ -1,575 +0,0 @@ -use crate::wasm_host::{WasmState, wit::ToWasmtimeResult}; -use ::http_client::{AsyncBody, HttpRequestExt}; -use ::settings::{Settings, WorktreeId}; -use anyhow::{Context as _, Result, bail}; -use async_compression::futures::bufread::GzipDecoder; -use async_tar::Archive; -use extension::{ExtensionLanguageServerProxy, KeyValueStoreDelegate, WorktreeDelegate}; -use futures::{AsyncReadExt, lock::Mutex}; -use futures::{FutureExt as _, io::BufReader}; -use gpui::BackgroundExecutor; -use language::LanguageName; -use language::{BinaryStatus, language_settings::AllLanguageSettings}; -use project::project_settings::ProjectSettings; -use semver::Version; -use std::{ - path::{Path, PathBuf}, - sync::{Arc, OnceLock}, -}; -use util::paths::PathStyle; -use util::rel_path::RelPath; -use util::{archive::extract_zip, fs::make_file_executable, maybe}; -use wasmtime::component::{Linker, Resource}; - -use super::latest; - -pub const MIN_VERSION: Version = Version::new(0, 1, 0); - -wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, - path: "../extension_api/wit/since_v0.1.0", - with: { - "worktree": ExtensionWorktree, - "key-value-store": ExtensionKeyValueStore, - "zed:extension/http-client/http-response-stream": ExtensionHttpResponseStream, - "zed:extension/github": latest::zed::extension::github, - "zed:extension/nodejs": latest::zed::extension::nodejs, - "zed:extension/platform": latest::zed::extension::platform, - "zed:extension/slash-command": latest::zed::extension::slash_command, - }, -}); - -pub use self::zed::extension::*; - -mod settings { - include!(concat!(env!("OUT_DIR"), "/since_v0.1.0/settings.rs")); -} - -pub type ExtensionWorktree = Arc; -pub type ExtensionKeyValueStore = Arc; -pub type ExtensionHttpResponseStream = Arc>>; - -pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { - static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) -} - -impl From for latest::Command { - fn from(value: Command) -> Self { - Self { - command: value.command, - args: value.args, - env: value.env, - } - } -} - -impl From for latest::SettingsLocation { - fn from(value: SettingsLocation) -> Self { - Self { - worktree_id: value.worktree_id, - path: value.path, - } - } -} - -impl From for latest::LanguageServerInstallationStatus { - fn from(value: LanguageServerInstallationStatus) -> Self { - match value { - LanguageServerInstallationStatus::None => Self::None, - LanguageServerInstallationStatus::Downloading => Self::Downloading, - LanguageServerInstallationStatus::CheckingForUpdate => Self::CheckingForUpdate, - LanguageServerInstallationStatus::Failed(message) => Self::Failed(message), - } - } -} - -impl From for latest::DownloadedFileType { - fn from(value: DownloadedFileType) -> Self { - match value { - DownloadedFileType::Gzip => Self::Gzip, - DownloadedFileType::GzipTar => Self::GzipTar, - DownloadedFileType::Zip => Self::Zip, - DownloadedFileType::Uncompressed => Self::Uncompressed, - } - } -} - -impl From for latest::Range { - fn from(value: Range) -> Self { - Self { - start: value.start, - end: value.end, - } - } -} - -impl From for latest::CodeLabelSpan { - fn from(value: CodeLabelSpan) -> Self { - match value { - CodeLabelSpan::CodeRange(range) => Self::CodeRange(range.into()), - CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()), - } - } -} - -impl From for latest::CodeLabelSpanLiteral { - fn from(value: CodeLabelSpanLiteral) -> Self { - Self { - text: value.text, - highlight_name: value.highlight_name, - } - } -} - -impl From for latest::CodeLabel { - fn from(value: CodeLabel) -> Self { - Self { - code: value.code, - spans: value.spans.into_iter().map(Into::into).collect(), - filter_range: value.filter_range.into(), - } - } -} - -impl From for Completion { - fn from(value: latest::Completion) -> Self { - Self { - label: value.label, - detail: value.detail, - kind: value.kind.map(Into::into), - insert_text_format: value.insert_text_format.map(Into::into), - } - } -} - -impl From for lsp::CompletionKind { - fn from(value: latest::lsp::CompletionKind) -> Self { - match value { - latest::lsp::CompletionKind::Text => Self::Text, - latest::lsp::CompletionKind::Method => Self::Method, - latest::lsp::CompletionKind::Function => Self::Function, - latest::lsp::CompletionKind::Constructor => Self::Constructor, - latest::lsp::CompletionKind::Field => Self::Field, - latest::lsp::CompletionKind::Variable => Self::Variable, - latest::lsp::CompletionKind::Class => Self::Class, - latest::lsp::CompletionKind::Interface => Self::Interface, - latest::lsp::CompletionKind::Module => Self::Module, - latest::lsp::CompletionKind::Property => Self::Property, - latest::lsp::CompletionKind::Unit => Self::Unit, - latest::lsp::CompletionKind::Value => Self::Value, - latest::lsp::CompletionKind::Enum => Self::Enum, - latest::lsp::CompletionKind::Keyword => Self::Keyword, - latest::lsp::CompletionKind::Snippet => Self::Snippet, - latest::lsp::CompletionKind::Color => Self::Color, - latest::lsp::CompletionKind::File => Self::File, - latest::lsp::CompletionKind::Reference => Self::Reference, - latest::lsp::CompletionKind::Folder => Self::Folder, - latest::lsp::CompletionKind::EnumMember => Self::EnumMember, - latest::lsp::CompletionKind::Constant => Self::Constant, - latest::lsp::CompletionKind::Struct => Self::Struct, - latest::lsp::CompletionKind::Event => Self::Event, - latest::lsp::CompletionKind::Operator => Self::Operator, - latest::lsp::CompletionKind::TypeParameter => Self::TypeParameter, - latest::lsp::CompletionKind::Other(kind) => Self::Other(kind), - } - } -} - -impl From for lsp::InsertTextFormat { - fn from(value: latest::lsp::InsertTextFormat) -> Self { - match value { - latest::lsp::InsertTextFormat::PlainText => Self::PlainText, - latest::lsp::InsertTextFormat::Snippet => Self::Snippet, - latest::lsp::InsertTextFormat::Other(value) => Self::Other(value), - } - } -} - -impl From for lsp::Symbol { - fn from(value: latest::lsp::Symbol) -> Self { - Self { - name: value.name, - kind: value.kind.into(), - } - } -} - -impl From for lsp::SymbolKind { - fn from(value: latest::lsp::SymbolKind) -> Self { - match value { - latest::lsp::SymbolKind::File => Self::File, - latest::lsp::SymbolKind::Module => Self::Module, - latest::lsp::SymbolKind::Namespace => Self::Namespace, - latest::lsp::SymbolKind::Package => Self::Package, - latest::lsp::SymbolKind::Class => Self::Class, - latest::lsp::SymbolKind::Method => Self::Method, - latest::lsp::SymbolKind::Property => Self::Property, - latest::lsp::SymbolKind::Field => Self::Field, - latest::lsp::SymbolKind::Constructor => Self::Constructor, - latest::lsp::SymbolKind::Enum => Self::Enum, - latest::lsp::SymbolKind::Interface => Self::Interface, - latest::lsp::SymbolKind::Function => Self::Function, - latest::lsp::SymbolKind::Variable => Self::Variable, - latest::lsp::SymbolKind::Constant => Self::Constant, - latest::lsp::SymbolKind::String => Self::String, - latest::lsp::SymbolKind::Number => Self::Number, - latest::lsp::SymbolKind::Boolean => Self::Boolean, - latest::lsp::SymbolKind::Array => Self::Array, - latest::lsp::SymbolKind::Object => Self::Object, - latest::lsp::SymbolKind::Key => Self::Key, - latest::lsp::SymbolKind::Null => Self::Null, - latest::lsp::SymbolKind::EnumMember => Self::EnumMember, - latest::lsp::SymbolKind::Struct => Self::Struct, - latest::lsp::SymbolKind::Event => Self::Event, - latest::lsp::SymbolKind::Operator => Self::Operator, - latest::lsp::SymbolKind::TypeParameter => Self::TypeParameter, - latest::lsp::SymbolKind::Other(kind) => Self::Other(kind), - } - } -} - -impl HostKeyValueStore for WasmState { - async fn insert( - &mut self, - kv_store: Resource, - key: String, - value: String, - ) -> wasmtime::Result> { - let kv_store = self.table.get(&kv_store)?; - kv_store.insert(key, value).await.to_wasmtime_result() - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of key-value stores. - Ok(()) - } -} - -impl HostWorktree for WasmState { - async fn id(&mut self, delegate: Resource>) -> wasmtime::Result { - latest::HostWorktree::id(self, delegate).await - } - - async fn root_path( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::root_path(self, delegate).await - } - - async fn read_text_file( - &mut self, - delegate: Resource>, - path: String, - ) -> wasmtime::Result> { - latest::HostWorktree::read_text_file(self, delegate, path).await - } - - async fn shell_env( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::shell_env(self, delegate).await - } - - async fn which( - &mut self, - delegate: Resource>, - binary_name: String, - ) -> wasmtime::Result> { - latest::HostWorktree::which(self, delegate, binary_name).await - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of worktrees. - Ok(()) - } -} - -impl common::Host for WasmState {} - -impl http_client::Host for WasmState { - async fn fetch( - &mut self, - request: http_client::HttpRequest, - ) -> wasmtime::Result> { - maybe!(async { - let url = &request.url; - let request = convert_request(&request)?; - let mut response = self.host.http_client.send(request).await?; - - if response.status().is_client_error() || response.status().is_server_error() { - bail!("failed to fetch '{url}': status code {}", response.status()) - } - convert_response(&mut response).await - }) - .await - .to_wasmtime_result() - } - - async fn fetch_stream( - &mut self, - request: http_client::HttpRequest, - ) -> wasmtime::Result, String>> { - let request = convert_request(&request)?; - let response = self.host.http_client.send(request); - maybe!(async { - let response = response.await?; - let stream = Arc::new(Mutex::new(response)); - let resource = self.table.push(stream)?; - Ok(resource) - }) - .await - .to_wasmtime_result() - } -} - -impl http_client::HostHttpResponseStream for WasmState { - async fn next_chunk( - &mut self, - resource: Resource, - ) -> wasmtime::Result>, String>> { - let stream = self.table.get(&resource)?.clone(); - maybe!(async move { - let mut response = stream.lock().await; - let mut buffer = vec![0; 8192]; // 8KB buffer - let bytes_read = response.body_mut().read(&mut buffer).await?; - if bytes_read == 0 { - Ok(None) - } else { - buffer.truncate(bytes_read); - Ok(Some(buffer)) - } - }) - .await - .to_wasmtime_result() - } - - async fn drop(&mut self, _resource: Resource) -> Result<()> { - Ok(()) - } -} - -impl From for ::http_client::Method { - fn from(value: http_client::HttpMethod) -> Self { - match value { - http_client::HttpMethod::Get => Self::GET, - http_client::HttpMethod::Post => Self::POST, - http_client::HttpMethod::Put => Self::PUT, - http_client::HttpMethod::Delete => Self::DELETE, - http_client::HttpMethod::Head => Self::HEAD, - http_client::HttpMethod::Options => Self::OPTIONS, - http_client::HttpMethod::Patch => Self::PATCH, - } - } -} - -fn convert_request( - extension_request: &http_client::HttpRequest, -) -> anyhow::Result<::http_client::Request> { - let mut request = ::http_client::Request::builder() - .method(::http_client::Method::from(extension_request.method)) - .uri(&extension_request.url) - .follow_redirects(match extension_request.redirect_policy { - http_client::RedirectPolicy::NoFollow => ::http_client::RedirectPolicy::NoFollow, - http_client::RedirectPolicy::FollowLimit(limit) => { - ::http_client::RedirectPolicy::FollowLimit(limit) - } - http_client::RedirectPolicy::FollowAll => ::http_client::RedirectPolicy::FollowAll, - }); - for (key, value) in &extension_request.headers { - request = request.header(key, value); - } - let body = extension_request - .body - .clone() - .map(AsyncBody::from) - .unwrap_or_default(); - request.body(body).map_err(anyhow::Error::from) -} - -async fn convert_response( - response: &mut ::http_client::Response, -) -> anyhow::Result { - let mut extension_response = http_client::HttpResponse { - body: Vec::new(), - headers: Vec::new(), - }; - - for (key, value) in response.headers() { - extension_response - .headers - .push((key.to_string(), value.to_str().unwrap_or("").to_string())); - } - - response - .body_mut() - .read_to_end(&mut extension_response.body) - .await?; - - Ok(extension_response) -} - -impl lsp::Host for WasmState {} - -impl ExtensionImports for WasmState { - async fn get_settings( - &mut self, - location: Option, - category: String, - key: Option, - ) -> wasmtime::Result> { - self.on_main_thread(|cx| { - async move { - let path = location.as_ref().and_then(|location| { - RelPath::new(Path::new(&location.path), PathStyle::Posix).ok() - }); - let location = path - .as_ref() - .zip(location.as_ref()) - .map(|(path, location)| ::settings::SettingsLocation { - worktree_id: WorktreeId::from_proto(location.worktree_id), - path, - }); - - cx.update(|cx| match category.as_str() { - "language" => { - let key = key.map(|k| LanguageName::new(&k)); - let settings = AllLanguageSettings::get(location, cx).language( - location, - key.as_ref(), - cx, - ); - Ok(serde_json::to_string(&settings::LanguageSettings { - tab_size: settings.tab_size, - })?) - } - "lsp" => { - let settings = key - .and_then(|key| { - ProjectSettings::get(location, cx) - .lsp - .get(&::lsp::LanguageServerName(key.into())) - }) - .cloned() - .unwrap_or_default(); - Ok(serde_json::to_string(&settings::LspSettings { - binary: settings.binary.map(|binary| settings::BinarySettings { - path: binary.path, - arguments: binary.arguments, - }), - settings: settings.settings, - initialization_options: settings.initialization_options, - })?) - } - _ => { - bail!("Unknown settings category: {}", category); - } - }) - } - .boxed_local() - }) - .await? - .to_wasmtime_result() - } - - async fn set_language_server_installation_status( - &mut self, - server_name: String, - status: LanguageServerInstallationStatus, - ) -> wasmtime::Result<()> { - let status = match status { - LanguageServerInstallationStatus::CheckingForUpdate => BinaryStatus::CheckingForUpdate, - LanguageServerInstallationStatus::Downloading => BinaryStatus::Downloading, - LanguageServerInstallationStatus::None => BinaryStatus::None, - LanguageServerInstallationStatus::Failed(error) => BinaryStatus::Failed { error }, - }; - - self.host - .proxy - .update_language_server_status(::lsp::LanguageServerName(server_name.into()), status); - - Ok(()) - } - - async fn download_file( - &mut self, - url: String, - path: String, - file_type: DownloadedFileType, - ) -> wasmtime::Result> { - maybe!(async { - let path = PathBuf::from(path); - let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref()); - - self.host.fs.create_dir(&extension_work_dir).await?; - - let destination_path = self - .host - .writeable_path_from_extension(&self.manifest.id, &path)?; - - let mut response = self - .host - .http_client - .get(&url, Default::default(), true) - .await - .context("downloading release")?; - - anyhow::ensure!( - response.status().is_success(), - "download failed with status {}", - response.status() - ); - let body = BufReader::new(response.body_mut()); - - match file_type { - DownloadedFileType::Uncompressed => { - futures::pin_mut!(body); - self.host - .fs - .create_file_with(&destination_path, body) - .await?; - } - DownloadedFileType::Gzip => { - let body = GzipDecoder::new(body); - futures::pin_mut!(body); - self.host - .fs - .create_file_with(&destination_path, body) - .await?; - } - DownloadedFileType::GzipTar => { - let body = GzipDecoder::new(body); - futures::pin_mut!(body); - self.host - .fs - .extract_tar_file(&destination_path, Archive::new(body)) - .await?; - } - DownloadedFileType::Zip => { - futures::pin_mut!(body); - extract_zip(&destination_path, body) - .await - .with_context(|| format!("unzipping {path:?} archive"))?; - } - } - - Ok(()) - }) - .await - .to_wasmtime_result() - } - - async fn make_file_executable(&mut self, path: String) -> wasmtime::Result> { - let path = self - .host - .writeable_path_from_extension(&self.manifest.id, Path::new(&path))?; - - make_file_executable(&path) - .await - .with_context(|| format!("setting permissions for path {path:?}")) - .to_wasmtime_result() - } -} diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs deleted file mode 100644 index 05e3f5a4e7..0000000000 --- a/crates/extension_host/src/wasm_host/wit/since_v0_2_0.rs +++ /dev/null @@ -1,238 +0,0 @@ -use crate::wasm_host::WasmState; -use anyhow::Result; -use extension::{KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate}; -use gpui::BackgroundExecutor; -use semver::Version; -use std::sync::{Arc, OnceLock}; -use wasmtime::component::{Linker, Resource}; - -use super::latest; - -pub const MIN_VERSION: Version = Version::new(0, 2, 0); - -wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, - path: "../extension_api/wit/since_v0.2.0", - with: { - "worktree": ExtensionWorktree, - "project": ExtensionProject, - "key-value-store": ExtensionKeyValueStore, - "zed:extension/github": latest::zed::extension::github, - "zed:extension/http-client": latest::zed::extension::http_client, - "zed:extension/lsp": latest::zed::extension::lsp, - "zed:extension/nodejs": latest::zed::extension::nodejs, - "zed:extension/platform": latest::zed::extension::platform, - "zed:extension/slash-command": latest::zed::extension::slash_command, - }, -}); - -pub use self::zed::extension::*; - -mod settings { - #![allow(dead_code)] - include!(concat!(env!("OUT_DIR"), "/since_v0.2.0/settings.rs")); -} - -pub type ExtensionWorktree = Arc; -pub type ExtensionProject = Arc; -pub type ExtensionKeyValueStore = Arc; - -pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { - static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) -} - -impl From for latest::Command { - fn from(value: Command) -> Self { - Self { - command: value.command, - args: value.args, - env: value.env, - } - } -} - -impl From for latest::SettingsLocation { - fn from(value: SettingsLocation) -> Self { - Self { - worktree_id: value.worktree_id, - path: value.path, - } - } -} - -impl From for latest::LanguageServerInstallationStatus { - fn from(value: LanguageServerInstallationStatus) -> Self { - match value { - LanguageServerInstallationStatus::None => Self::None, - LanguageServerInstallationStatus::Downloading => Self::Downloading, - LanguageServerInstallationStatus::CheckingForUpdate => Self::CheckingForUpdate, - LanguageServerInstallationStatus::Failed(message) => Self::Failed(message), - } - } -} - -impl From for latest::DownloadedFileType { - fn from(value: DownloadedFileType) -> Self { - match value { - DownloadedFileType::Gzip => Self::Gzip, - DownloadedFileType::GzipTar => Self::GzipTar, - DownloadedFileType::Zip => Self::Zip, - DownloadedFileType::Uncompressed => Self::Uncompressed, - } - } -} - -impl From for latest::Range { - fn from(value: Range) -> Self { - Self { - start: value.start, - end: value.end, - } - } -} - -impl From for latest::CodeLabelSpan { - fn from(value: CodeLabelSpan) -> Self { - match value { - CodeLabelSpan::CodeRange(range) => Self::CodeRange(range.into()), - CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()), - } - } -} - -impl From for latest::CodeLabelSpanLiteral { - fn from(value: CodeLabelSpanLiteral) -> Self { - Self { - text: value.text, - highlight_name: value.highlight_name, - } - } -} - -impl From for latest::CodeLabel { - fn from(value: CodeLabel) -> Self { - Self { - code: value.code, - spans: value.spans.into_iter().map(Into::into).collect(), - filter_range: value.filter_range.into(), - } - } -} - -impl HostKeyValueStore for WasmState { - async fn insert( - &mut self, - kv_store: Resource, - key: String, - value: String, - ) -> wasmtime::Result> { - latest::HostKeyValueStore::insert(self, kv_store, key, value).await - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of key-value stores. - Ok(()) - } -} - -impl HostProject for WasmState { - async fn worktree_ids( - &mut self, - project: Resource, - ) -> wasmtime::Result> { - latest::HostProject::worktree_ids(self, project).await - } - - async fn drop(&mut self, _project: Resource) -> Result<()> { - // We only ever hand out borrows of projects. - Ok(()) - } -} - -impl HostWorktree for WasmState { - async fn id(&mut self, delegate: Resource>) -> wasmtime::Result { - latest::HostWorktree::id(self, delegate).await - } - - async fn root_path( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::root_path(self, delegate).await - } - - async fn read_text_file( - &mut self, - delegate: Resource>, - path: String, - ) -> wasmtime::Result> { - latest::HostWorktree::read_text_file(self, delegate, path).await - } - - async fn shell_env( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::shell_env(self, delegate).await - } - - async fn which( - &mut self, - delegate: Resource>, - binary_name: String, - ) -> wasmtime::Result> { - latest::HostWorktree::which(self, delegate, binary_name).await - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of worktrees. - Ok(()) - } -} - -impl common::Host for WasmState {} - -impl ExtensionImports for WasmState { - async fn get_settings( - &mut self, - location: Option, - category: String, - key: Option, - ) -> wasmtime::Result> { - latest::ExtensionImports::get_settings( - self, - location.map(|location| location.into()), - category, - key, - ) - .await - } - - async fn set_language_server_installation_status( - &mut self, - server_name: String, - status: LanguageServerInstallationStatus, - ) -> wasmtime::Result<()> { - latest::ExtensionImports::set_language_server_installation_status( - self, - server_name, - status.into(), - ) - .await - } - - async fn download_file( - &mut self, - url: String, - path: String, - file_type: DownloadedFileType, - ) -> wasmtime::Result> { - latest::ExtensionImports::download_file(self, url, path, file_type.into()).await - } - - async fn make_file_executable(&mut self, path: String) -> wasmtime::Result> { - latest::ExtensionImports::make_file_executable(self, path).await - } -} diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_3_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_3_0.rs deleted file mode 100644 index 08393934fe..0000000000 --- a/crates/extension_host/src/wasm_host/wit/since_v0_3_0.rs +++ /dev/null @@ -1,217 +0,0 @@ -use crate::wasm_host::WasmState; -use anyhow::Result; -use extension::{KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate}; -use gpui::BackgroundExecutor; -use semver::Version; -use std::sync::{Arc, OnceLock}; -use wasmtime::component::{Linker, Resource}; - -use super::latest; - -pub const MIN_VERSION: Version = Version::new(0, 3, 0); - -wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, - path: "../extension_api/wit/since_v0.3.0", - with: { - "worktree": ExtensionWorktree, - "project": ExtensionProject, - "key-value-store": ExtensionKeyValueStore, - "zed:extension/common": latest::zed::extension::common, - "zed:extension/github": latest::zed::extension::github, - "zed:extension/http-client": latest::zed::extension::http_client, - "zed:extension/lsp": latest::zed::extension::lsp, - "zed:extension/nodejs": latest::zed::extension::nodejs, - "zed:extension/platform": latest::zed::extension::platform, - "zed:extension/process": latest::zed::extension::process, - "zed:extension/slash-command": latest::zed::extension::slash_command, - }, -}); - -mod settings { - #![allow(dead_code)] - include!(concat!(env!("OUT_DIR"), "/since_v0.3.0/settings.rs")); -} - -pub type ExtensionWorktree = Arc; -pub type ExtensionProject = Arc; -pub type ExtensionKeyValueStore = Arc; - -pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { - static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) -} - -impl From for latest::CodeLabel { - fn from(value: CodeLabel) -> Self { - Self { - code: value.code, - spans: value.spans.into_iter().map(Into::into).collect(), - filter_range: value.filter_range, - } - } -} - -impl From for latest::CodeLabelSpan { - fn from(value: CodeLabelSpan) -> Self { - match value { - CodeLabelSpan::CodeRange(range) => Self::CodeRange(range), - CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()), - } - } -} - -impl From for latest::CodeLabelSpanLiteral { - fn from(value: CodeLabelSpanLiteral) -> Self { - Self { - text: value.text, - highlight_name: value.highlight_name, - } - } -} - -impl From for latest::SettingsLocation { - fn from(value: SettingsLocation) -> Self { - Self { - worktree_id: value.worktree_id, - path: value.path, - } - } -} - -impl From for latest::LanguageServerInstallationStatus { - fn from(value: LanguageServerInstallationStatus) -> Self { - match value { - LanguageServerInstallationStatus::None => Self::None, - LanguageServerInstallationStatus::Downloading => Self::Downloading, - LanguageServerInstallationStatus::CheckingForUpdate => Self::CheckingForUpdate, - LanguageServerInstallationStatus::Failed(message) => Self::Failed(message), - } - } -} - -impl From for latest::DownloadedFileType { - fn from(value: DownloadedFileType) -> Self { - match value { - DownloadedFileType::Gzip => Self::Gzip, - DownloadedFileType::GzipTar => Self::GzipTar, - DownloadedFileType::Zip => Self::Zip, - DownloadedFileType::Uncompressed => Self::Uncompressed, - } - } -} - -impl HostKeyValueStore for WasmState { - async fn insert( - &mut self, - kv_store: Resource, - key: String, - value: String, - ) -> wasmtime::Result> { - latest::HostKeyValueStore::insert(self, kv_store, key, value).await - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of key-value stores. - Ok(()) - } -} - -impl HostProject for WasmState { - async fn worktree_ids( - &mut self, - project: Resource, - ) -> wasmtime::Result> { - latest::HostProject::worktree_ids(self, project).await - } - - async fn drop(&mut self, _project: Resource) -> Result<()> { - // We only ever hand out borrows of projects. - Ok(()) - } -} - -impl HostWorktree for WasmState { - async fn id(&mut self, delegate: Resource>) -> wasmtime::Result { - latest::HostWorktree::id(self, delegate).await - } - - async fn root_path( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::root_path(self, delegate).await - } - - async fn read_text_file( - &mut self, - delegate: Resource>, - path: String, - ) -> wasmtime::Result> { - latest::HostWorktree::read_text_file(self, delegate, path).await - } - - async fn shell_env( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::shell_env(self, delegate).await - } - - async fn which( - &mut self, - delegate: Resource>, - binary_name: String, - ) -> wasmtime::Result> { - latest::HostWorktree::which(self, delegate, binary_name).await - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of worktrees. - Ok(()) - } -} - -impl ExtensionImports for WasmState { - async fn get_settings( - &mut self, - location: Option, - category: String, - key: Option, - ) -> wasmtime::Result> { - latest::ExtensionImports::get_settings( - self, - location.map(|location| location.into()), - category, - key, - ) - .await - } - - async fn set_language_server_installation_status( - &mut self, - server_name: String, - status: LanguageServerInstallationStatus, - ) -> wasmtime::Result<()> { - latest::ExtensionImports::set_language_server_installation_status( - self, - server_name, - status.into(), - ) - .await - } - - async fn download_file( - &mut self, - url: String, - path: String, - file_type: DownloadedFileType, - ) -> wasmtime::Result> { - latest::ExtensionImports::download_file(self, url, path, file_type.into()).await - } - - async fn make_file_executable(&mut self, path: String) -> wasmtime::Result> { - latest::ExtensionImports::make_file_executable(self, path).await - } -} diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_4_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_4_0.rs deleted file mode 100644 index 1b2a95023b..0000000000 --- a/crates/extension_host/src/wasm_host/wit/since_v0_4_0.rs +++ /dev/null @@ -1,217 +0,0 @@ -use crate::wasm_host::WasmState; -use anyhow::Result; -use extension::{KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate}; -use gpui::BackgroundExecutor; -use semver::Version; -use std::sync::{Arc, OnceLock}; -use wasmtime::component::{Linker, Resource}; - -use super::latest; - -pub const MIN_VERSION: Version = Version::new(0, 4, 0); - -wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, - path: "../extension_api/wit/since_v0.4.0", - with: { - "worktree": ExtensionWorktree, - "project": ExtensionProject, - "key-value-store": ExtensionKeyValueStore, - "zed:extension/common": latest::zed::extension::common, - "zed:extension/github": latest::zed::extension::github, - "zed:extension/http-client": latest::zed::extension::http_client, - "zed:extension/lsp": latest::zed::extension::lsp, - "zed:extension/nodejs": latest::zed::extension::nodejs, - "zed:extension/platform": latest::zed::extension::platform, - "zed:extension/process": latest::zed::extension::process, - "zed:extension/slash-command": latest::zed::extension::slash_command, - }, -}); - -mod settings { - #![allow(dead_code)] - include!(concat!(env!("OUT_DIR"), "/since_v0.4.0/settings.rs")); -} - -pub type ExtensionWorktree = Arc; -pub type ExtensionProject = Arc; -pub type ExtensionKeyValueStore = Arc; - -pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { - static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) -} - -impl From for latest::CodeLabel { - fn from(value: CodeLabel) -> Self { - Self { - code: value.code, - spans: value.spans.into_iter().map(Into::into).collect(), - filter_range: value.filter_range, - } - } -} - -impl From for latest::CodeLabelSpan { - fn from(value: CodeLabelSpan) -> Self { - match value { - CodeLabelSpan::CodeRange(range) => Self::CodeRange(range), - CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()), - } - } -} - -impl From for latest::CodeLabelSpanLiteral { - fn from(value: CodeLabelSpanLiteral) -> Self { - Self { - text: value.text, - highlight_name: value.highlight_name, - } - } -} - -impl From for latest::SettingsLocation { - fn from(value: SettingsLocation) -> Self { - Self { - worktree_id: value.worktree_id, - path: value.path, - } - } -} - -impl From for latest::LanguageServerInstallationStatus { - fn from(value: LanguageServerInstallationStatus) -> Self { - match value { - LanguageServerInstallationStatus::None => Self::None, - LanguageServerInstallationStatus::Downloading => Self::Downloading, - LanguageServerInstallationStatus::CheckingForUpdate => Self::CheckingForUpdate, - LanguageServerInstallationStatus::Failed(message) => Self::Failed(message), - } - } -} - -impl From for latest::DownloadedFileType { - fn from(value: DownloadedFileType) -> Self { - match value { - DownloadedFileType::Gzip => Self::Gzip, - DownloadedFileType::GzipTar => Self::GzipTar, - DownloadedFileType::Zip => Self::Zip, - DownloadedFileType::Uncompressed => Self::Uncompressed, - } - } -} - -impl HostKeyValueStore for WasmState { - async fn insert( - &mut self, - kv_store: Resource, - key: String, - value: String, - ) -> wasmtime::Result> { - latest::HostKeyValueStore::insert(self, kv_store, key, value).await - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of key-value stores. - Ok(()) - } -} - -impl HostProject for WasmState { - async fn worktree_ids( - &mut self, - project: Resource, - ) -> wasmtime::Result> { - latest::HostProject::worktree_ids(self, project).await - } - - async fn drop(&mut self, _project: Resource) -> Result<()> { - // We only ever hand out borrows of projects. - Ok(()) - } -} - -impl HostWorktree for WasmState { - async fn id(&mut self, delegate: Resource>) -> wasmtime::Result { - latest::HostWorktree::id(self, delegate).await - } - - async fn root_path( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::root_path(self, delegate).await - } - - async fn read_text_file( - &mut self, - delegate: Resource>, - path: String, - ) -> wasmtime::Result> { - latest::HostWorktree::read_text_file(self, delegate, path).await - } - - async fn shell_env( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::shell_env(self, delegate).await - } - - async fn which( - &mut self, - delegate: Resource>, - binary_name: String, - ) -> wasmtime::Result> { - latest::HostWorktree::which(self, delegate, binary_name).await - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of worktrees. - Ok(()) - } -} - -impl ExtensionImports for WasmState { - async fn get_settings( - &mut self, - location: Option, - category: String, - key: Option, - ) -> wasmtime::Result> { - latest::ExtensionImports::get_settings( - self, - location.map(|location| location.into()), - category, - key, - ) - .await - } - - async fn set_language_server_installation_status( - &mut self, - server_name: String, - status: LanguageServerInstallationStatus, - ) -> wasmtime::Result<()> { - latest::ExtensionImports::set_language_server_installation_status( - self, - server_name, - status.into(), - ) - .await - } - - async fn download_file( - &mut self, - url: String, - path: String, - file_type: DownloadedFileType, - ) -> wasmtime::Result> { - latest::ExtensionImports::download_file(self, url, path, file_type.into()).await - } - - async fn make_file_executable(&mut self, path: String) -> wasmtime::Result> { - latest::ExtensionImports::make_file_executable(self, path).await - } -} diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_5_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_5_0.rs deleted file mode 100644 index 23701c9d03..0000000000 --- a/crates/extension_host/src/wasm_host/wit/since_v0_5_0.rs +++ /dev/null @@ -1,218 +0,0 @@ -use crate::wasm_host::WasmState; -use anyhow::Result; -use extension::{KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate}; -use gpui::BackgroundExecutor; -use semver::Version; -use std::sync::{Arc, OnceLock}; -use wasmtime::component::{Linker, Resource}; - -use super::latest; - -pub const MIN_VERSION: Version = Version::new(0, 5, 0); - -wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, - path: "../extension_api/wit/since_v0.5.0", - with: { - "worktree": ExtensionWorktree, - "project": ExtensionProject, - "key-value-store": ExtensionKeyValueStore, - "zed:extension/common": latest::zed::extension::common, - "zed:extension/github": latest::zed::extension::github, - "zed:extension/http-client": latest::zed::extension::http_client, - "zed:extension/lsp": latest::zed::extension::lsp, - "zed:extension/nodejs": latest::zed::extension::nodejs, - "zed:extension/platform": latest::zed::extension::platform, - "zed:extension/process": latest::zed::extension::process, - "zed:extension/slash-command": latest::zed::extension::slash_command, - "zed:extension/context-server": latest::zed::extension::context_server, - }, -}); - -mod settings { - #![allow(dead_code)] - include!(concat!(env!("OUT_DIR"), "/since_v0.5.0/settings.rs")); -} - -pub type ExtensionWorktree = Arc; -pub type ExtensionProject = Arc; -pub type ExtensionKeyValueStore = Arc; - -pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { - static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) -} - -impl From for latest::CodeLabel { - fn from(value: CodeLabel) -> Self { - Self { - code: value.code, - spans: value.spans.into_iter().map(Into::into).collect(), - filter_range: value.filter_range, - } - } -} - -impl From for latest::CodeLabelSpan { - fn from(value: CodeLabelSpan) -> Self { - match value { - CodeLabelSpan::CodeRange(range) => Self::CodeRange(range), - CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()), - } - } -} - -impl From for latest::CodeLabelSpanLiteral { - fn from(value: CodeLabelSpanLiteral) -> Self { - Self { - text: value.text, - highlight_name: value.highlight_name, - } - } -} - -impl From for latest::SettingsLocation { - fn from(value: SettingsLocation) -> Self { - Self { - worktree_id: value.worktree_id, - path: value.path, - } - } -} - -impl From for latest::LanguageServerInstallationStatus { - fn from(value: LanguageServerInstallationStatus) -> Self { - match value { - LanguageServerInstallationStatus::None => Self::None, - LanguageServerInstallationStatus::Downloading => Self::Downloading, - LanguageServerInstallationStatus::CheckingForUpdate => Self::CheckingForUpdate, - LanguageServerInstallationStatus::Failed(message) => Self::Failed(message), - } - } -} - -impl From for latest::DownloadedFileType { - fn from(value: DownloadedFileType) -> Self { - match value { - DownloadedFileType::Gzip => Self::Gzip, - DownloadedFileType::GzipTar => Self::GzipTar, - DownloadedFileType::Zip => Self::Zip, - DownloadedFileType::Uncompressed => Self::Uncompressed, - } - } -} - -impl HostKeyValueStore for WasmState { - async fn insert( - &mut self, - kv_store: Resource, - key: String, - value: String, - ) -> wasmtime::Result> { - latest::HostKeyValueStore::insert(self, kv_store, key, value).await - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of key-value stores. - Ok(()) - } -} - -impl HostProject for WasmState { - async fn worktree_ids( - &mut self, - project: Resource, - ) -> wasmtime::Result> { - latest::HostProject::worktree_ids(self, project).await - } - - async fn drop(&mut self, _project: Resource) -> Result<()> { - // We only ever hand out borrows of projects. - Ok(()) - } -} - -impl HostWorktree for WasmState { - async fn id(&mut self, delegate: Resource>) -> wasmtime::Result { - latest::HostWorktree::id(self, delegate).await - } - - async fn root_path( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::root_path(self, delegate).await - } - - async fn read_text_file( - &mut self, - delegate: Resource>, - path: String, - ) -> wasmtime::Result> { - latest::HostWorktree::read_text_file(self, delegate, path).await - } - - async fn shell_env( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::shell_env(self, delegate).await - } - - async fn which( - &mut self, - delegate: Resource>, - binary_name: String, - ) -> wasmtime::Result> { - latest::HostWorktree::which(self, delegate, binary_name).await - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of worktrees. - Ok(()) - } -} - -impl ExtensionImports for WasmState { - async fn get_settings( - &mut self, - location: Option, - category: String, - key: Option, - ) -> wasmtime::Result> { - latest::ExtensionImports::get_settings( - self, - location.map(|location| location.into()), - category, - key, - ) - .await - } - - async fn set_language_server_installation_status( - &mut self, - server_name: String, - status: LanguageServerInstallationStatus, - ) -> wasmtime::Result<()> { - latest::ExtensionImports::set_language_server_installation_status( - self, - server_name, - status.into(), - ) - .await - } - - async fn download_file( - &mut self, - url: String, - path: String, - file_type: DownloadedFileType, - ) -> wasmtime::Result> { - latest::ExtensionImports::download_file(self, url, path, file_type.into()).await - } - - async fn make_file_executable(&mut self, path: String) -> wasmtime::Result> { - latest::ExtensionImports::make_file_executable(self, path).await - } -} diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs deleted file mode 100644 index 8595c278b9..0000000000 --- a/crates/extension_host/src/wasm_host/wit/since_v0_6_0.rs +++ /dev/null @@ -1,222 +0,0 @@ -use crate::wasm_host::WasmState; -use anyhow::Result; -use extension::{KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate}; -use gpui::BackgroundExecutor; -use semver::Version; -use std::sync::{Arc, OnceLock}; -use wasmtime::component::{Linker, Resource}; - -use super::latest; - -pub const MIN_VERSION: Version = Version::new(0, 6, 0); -pub const MAX_VERSION: Version = Version::new(0, 7, 0); - -wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, - path: "../extension_api/wit/since_v0.6.0", - with: { - "worktree": ExtensionWorktree, - "project": ExtensionProject, - "key-value-store": ExtensionKeyValueStore, - "zed:extension/common": latest::zed::extension::common, - "zed:extension/github": latest::zed::extension::github, - "zed:extension/http-client": latest::zed::extension::http_client, - "zed:extension/lsp": latest::zed::extension::lsp, - "zed:extension/nodejs": latest::zed::extension::nodejs, - "zed:extension/platform": latest::zed::extension::platform, - "zed:extension/process": latest::zed::extension::process, - "zed:extension/slash-command": latest::zed::extension::slash_command, - "zed:extension/context-server": latest::zed::extension::context_server, - "zed:extension/dap": latest::zed::extension::dap, - }, -}); - -pub use self::zed::extension::*; - -mod settings { - #![allow(dead_code)] - include!(concat!(env!("OUT_DIR"), "/since_v0.6.0/settings.rs")); -} - -pub type ExtensionWorktree = Arc; -pub type ExtensionProject = Arc; -pub type ExtensionKeyValueStore = Arc; - -pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { - static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) -} - -impl From for latest::CodeLabel { - fn from(value: CodeLabel) -> Self { - Self { - code: value.code, - spans: value.spans.into_iter().map(Into::into).collect(), - filter_range: value.filter_range, - } - } -} - -impl From for latest::CodeLabelSpan { - fn from(value: CodeLabelSpan) -> Self { - match value { - CodeLabelSpan::CodeRange(range) => Self::CodeRange(range), - CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()), - } - } -} - -impl From for latest::CodeLabelSpanLiteral { - fn from(value: CodeLabelSpanLiteral) -> Self { - Self { - text: value.text, - highlight_name: value.highlight_name, - } - } -} - -impl From for latest::SettingsLocation { - fn from(value: SettingsLocation) -> Self { - Self { - worktree_id: value.worktree_id, - path: value.path, - } - } -} - -impl From for latest::LanguageServerInstallationStatus { - fn from(value: LanguageServerInstallationStatus) -> Self { - match value { - LanguageServerInstallationStatus::None => Self::None, - LanguageServerInstallationStatus::Downloading => Self::Downloading, - LanguageServerInstallationStatus::CheckingForUpdate => Self::CheckingForUpdate, - LanguageServerInstallationStatus::Failed(message) => Self::Failed(message), - } - } -} - -impl From for latest::DownloadedFileType { - fn from(value: DownloadedFileType) -> Self { - match value { - DownloadedFileType::Gzip => Self::Gzip, - DownloadedFileType::GzipTar => Self::GzipTar, - DownloadedFileType::Zip => Self::Zip, - DownloadedFileType::Uncompressed => Self::Uncompressed, - } - } -} - -impl HostKeyValueStore for WasmState { - async fn insert( - &mut self, - kv_store: Resource, - key: String, - value: String, - ) -> wasmtime::Result> { - latest::HostKeyValueStore::insert(self, kv_store, key, value).await - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of key-value stores. - Ok(()) - } -} - -impl HostProject for WasmState { - async fn worktree_ids( - &mut self, - project: Resource, - ) -> wasmtime::Result> { - latest::HostProject::worktree_ids(self, project).await - } - - async fn drop(&mut self, _project: Resource) -> Result<()> { - // We only ever hand out borrows of projects. - Ok(()) - } -} - -impl HostWorktree for WasmState { - async fn id(&mut self, delegate: Resource>) -> wasmtime::Result { - latest::HostWorktree::id(self, delegate).await - } - - async fn root_path( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::root_path(self, delegate).await - } - - async fn read_text_file( - &mut self, - delegate: Resource>, - path: String, - ) -> wasmtime::Result> { - latest::HostWorktree::read_text_file(self, delegate, path).await - } - - async fn shell_env( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - latest::HostWorktree::shell_env(self, delegate).await - } - - async fn which( - &mut self, - delegate: Resource>, - binary_name: String, - ) -> wasmtime::Result> { - latest::HostWorktree::which(self, delegate, binary_name).await - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of worktrees. - Ok(()) - } -} - -impl ExtensionImports for WasmState { - async fn get_settings( - &mut self, - location: Option, - category: String, - key: Option, - ) -> wasmtime::Result> { - latest::ExtensionImports::get_settings( - self, - location.map(|location| location.into()), - category, - key, - ) - .await - } - - async fn set_language_server_installation_status( - &mut self, - server_name: String, - status: LanguageServerInstallationStatus, - ) -> wasmtime::Result<()> { - latest::ExtensionImports::set_language_server_installation_status( - self, - server_name, - status.into(), - ) - .await - } - - async fn download_file( - &mut self, - url: String, - path: String, - file_type: DownloadedFileType, - ) -> wasmtime::Result> { - latest::ExtensionImports::download_file(self, url, path, file_type.into()).await - } - - async fn make_file_executable(&mut self, path: String) -> wasmtime::Result> { - latest::ExtensionImports::make_file_executable(self, path).await - } -} diff --git a/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs b/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs deleted file mode 100644 index a2776f9f3b..0000000000 --- a/crates/extension_host/src/wasm_host/wit/since_v0_8_0.rs +++ /dev/null @@ -1,1109 +0,0 @@ -use crate::wasm_host::wit::since_v0_6_0::{ - dap::{ - AttachRequest, BuildTaskDefinition, BuildTaskDefinitionTemplatePayload, LaunchRequest, - StartDebuggingRequestArguments, TcpArguments, TcpArgumentsTemplate, - }, - slash_command::SlashCommandOutputSection, -}; -use crate::wasm_host::wit::{CompletionKind, CompletionLabelDetails, InsertTextFormat, SymbolKind}; -use crate::wasm_host::{WasmState, wit::ToWasmtimeResult}; -use ::http_client::{AsyncBody, HttpRequestExt}; -use ::settings::{Settings, WorktreeId}; -use anyhow::{Context as _, Result, bail}; -use async_compression::futures::bufread::GzipDecoder; -use async_tar::Archive; -use async_trait::async_trait; -use extension::{ - ExtensionLanguageServerProxy, KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate, -}; -use futures::{AsyncReadExt, lock::Mutex}; -use futures::{FutureExt as _, io::BufReader}; -use gpui::{BackgroundExecutor, SharedString}; -use language::{BinaryStatus, LanguageName, language_settings::AllLanguageSettings}; -use project::project_settings::ProjectSettings; -use semver::Version; -use std::{ - env, - net::Ipv4Addr, - path::{Path, PathBuf}, - str::FromStr, - sync::{Arc, OnceLock}, -}; -use task::{SpawnInTerminal, ZedDebugConfig}; -use url::Url; -use util::{ - archive::extract_zip, fs::make_file_executable, maybe, paths::PathStyle, rel_path::RelPath, -}; -use wasmtime::component::{Linker, Resource}; - -pub const MIN_VERSION: Version = Version::new(0, 8, 0); -pub const MAX_VERSION: Version = Version::new(0, 8, 0); - -wasmtime::component::bindgen!({ - async: true, - trappable_imports: true, - path: "../extension_api/wit/since_v0.8.0", - with: { - "worktree": ExtensionWorktree, - "project": ExtensionProject, - "key-value-store": ExtensionKeyValueStore, - "zed:extension/http-client/http-response-stream": ExtensionHttpResponseStream - }, -}); - -pub use self::zed::extension::*; - -mod settings { - #![allow(dead_code)] - include!(concat!(env!("OUT_DIR"), "/since_v0.8.0/settings.rs")); -} - -pub type ExtensionWorktree = Arc; -pub type ExtensionProject = Arc; -pub type ExtensionKeyValueStore = Arc; -pub type ExtensionHttpResponseStream = Arc>>; - -pub fn linker(executor: &BackgroundExecutor) -> &'static Linker { - static LINKER: OnceLock> = OnceLock::new(); - LINKER.get_or_init(|| super::new_linker(executor, Extension::add_to_linker)) -} - -impl From for std::ops::Range { - fn from(range: Range) -> Self { - let start = range.start as usize; - let end = range.end as usize; - start..end - } -} - -impl From for extension::Command { - fn from(value: Command) -> Self { - Self { - command: value.command.into(), - args: value.args, - env: value.env, - } - } -} - -impl From - for extension::StartDebuggingRequestArgumentsRequest -{ - fn from(value: StartDebuggingRequestArgumentsRequest) -> Self { - match value { - StartDebuggingRequestArgumentsRequest::Launch => Self::Launch, - StartDebuggingRequestArgumentsRequest::Attach => Self::Attach, - } - } -} -impl TryFrom for extension::StartDebuggingRequestArguments { - type Error = anyhow::Error; - - fn try_from(value: StartDebuggingRequestArguments) -> Result { - Ok(Self { - configuration: serde_json::from_str(&value.configuration)?, - request: value.request.into(), - }) - } -} -impl From for extension::TcpArguments { - fn from(value: TcpArguments) -> Self { - Self { - host: value.host.into(), - port: value.port, - timeout: value.timeout, - } - } -} - -impl From for TcpArgumentsTemplate { - fn from(value: extension::TcpArgumentsTemplate) -> Self { - Self { - host: value.host.map(Ipv4Addr::to_bits), - port: value.port, - timeout: value.timeout, - } - } -} - -impl From for extension::TcpArgumentsTemplate { - fn from(value: TcpArgumentsTemplate) -> Self { - Self { - host: value.host.map(Ipv4Addr::from_bits), - port: value.port, - timeout: value.timeout, - } - } -} - -impl TryFrom for DebugTaskDefinition { - type Error = anyhow::Error; - fn try_from(value: extension::DebugTaskDefinition) -> Result { - Ok(Self { - label: value.label.to_string(), - adapter: value.adapter.to_string(), - config: value.config.to_string(), - tcp_connection: value.tcp_connection.map(Into::into), - }) - } -} - -impl From for DebugRequest { - fn from(value: task::DebugRequest) -> Self { - match value { - task::DebugRequest::Launch(launch_request) => Self::Launch(launch_request.into()), - task::DebugRequest::Attach(attach_request) => Self::Attach(attach_request.into()), - } - } -} - -impl From for task::DebugRequest { - fn from(value: DebugRequest) -> Self { - match value { - DebugRequest::Launch(launch_request) => Self::Launch(launch_request.into()), - DebugRequest::Attach(attach_request) => Self::Attach(attach_request.into()), - } - } -} - -impl From for LaunchRequest { - fn from(value: task::LaunchRequest) -> Self { - Self { - program: value.program, - cwd: value.cwd.map(|p| p.to_string_lossy().into_owned()), - args: value.args, - envs: value.env.into_iter().collect(), - } - } -} - -impl From for AttachRequest { - fn from(value: task::AttachRequest) -> Self { - Self { - process_id: value.process_id, - } - } -} - -impl From for task::LaunchRequest { - fn from(value: LaunchRequest) -> Self { - Self { - program: value.program, - cwd: value.cwd.map(|p| p.into()), - args: value.args, - env: value.envs.into_iter().collect(), - } - } -} -impl From for task::AttachRequest { - fn from(value: AttachRequest) -> Self { - Self { - process_id: value.process_id, - } - } -} - -impl From for DebugConfig { - fn from(value: ZedDebugConfig) -> Self { - Self { - label: value.label.into(), - adapter: value.adapter.into(), - request: value.request.into(), - stop_on_entry: value.stop_on_entry, - } - } -} -impl TryFrom for extension::DebugAdapterBinary { - type Error = anyhow::Error; - fn try_from(value: DebugAdapterBinary) -> Result { - Ok(Self { - command: value.command, - arguments: value.arguments, - envs: value.envs.into_iter().collect(), - cwd: value.cwd.map(|s| s.into()), - connection: value.connection.map(Into::into), - request_args: value.request_args.try_into()?, - }) - } -} - -impl From for extension::BuildTaskDefinition { - fn from(value: BuildTaskDefinition) -> Self { - match value { - BuildTaskDefinition::ByName(name) => Self::ByName(name.into()), - BuildTaskDefinition::Template(build_task_template) => Self::Template { - task_template: build_task_template.template.into(), - locator_name: build_task_template.locator_name.map(SharedString::from), - }, - } - } -} - -impl From for BuildTaskDefinition { - fn from(value: extension::BuildTaskDefinition) -> Self { - match value { - extension::BuildTaskDefinition::ByName(name) => Self::ByName(name.into()), - extension::BuildTaskDefinition::Template { - task_template, - locator_name, - } => Self::Template(BuildTaskDefinitionTemplatePayload { - template: task_template.into(), - locator_name: locator_name.map(String::from), - }), - } - } -} -impl From for extension::BuildTaskTemplate { - fn from(value: BuildTaskTemplate) -> Self { - Self { - label: value.label, - command: value.command, - args: value.args, - env: value.env.into_iter().collect(), - cwd: value.cwd, - ..Default::default() - } - } -} -impl From for BuildTaskTemplate { - fn from(value: extension::BuildTaskTemplate) -> Self { - Self { - label: value.label, - command: value.command, - args: value.args, - env: value.env.into_iter().collect(), - cwd: value.cwd, - } - } -} - -impl TryFrom for extension::DebugScenario { - type Error = anyhow::Error; - - fn try_from(value: DebugScenario) -> std::result::Result { - Ok(Self { - adapter: value.adapter.into(), - label: value.label.into(), - build: value.build.map(Into::into), - config: serde_json::Value::from_str(&value.config)?, - tcp_connection: value.tcp_connection.map(Into::into), - }) - } -} - -impl From for DebugScenario { - fn from(value: extension::DebugScenario) -> Self { - Self { - adapter: value.adapter.into(), - label: value.label.into(), - build: value.build.map(Into::into), - config: value.config.to_string(), - tcp_connection: value.tcp_connection.map(Into::into), - } - } -} - -impl TryFrom for ResolvedTask { - type Error = anyhow::Error; - - fn try_from(value: SpawnInTerminal) -> Result { - Ok(Self { - label: value.label, - command: value.command.context("missing command")?, - args: value.args, - env: value.env.into_iter().collect(), - cwd: value.cwd.map(|s| { - let s = s.to_string_lossy(); - if cfg!(target_os = "windows") { - s.replace('\\', "/") - } else { - s.into_owned() - } - }), - }) - } -} - -impl From for extension::CodeLabel { - fn from(value: CodeLabel) -> Self { - Self { - code: value.code, - spans: value.spans.into_iter().map(Into::into).collect(), - filter_range: value.filter_range.into(), - } - } -} - -impl From for extension::CodeLabelSpan { - fn from(value: CodeLabelSpan) -> Self { - match value { - CodeLabelSpan::CodeRange(range) => Self::CodeRange(range.into()), - CodeLabelSpan::Literal(literal) => Self::Literal(literal.into()), - } - } -} - -impl From for extension::CodeLabelSpanLiteral { - fn from(value: CodeLabelSpanLiteral) -> Self { - Self { - text: value.text, - highlight_name: value.highlight_name, - } - } -} - -impl From for Completion { - fn from(value: extension::Completion) -> Self { - Self { - label: value.label, - label_details: value.label_details.map(Into::into), - detail: value.detail, - kind: value.kind.map(Into::into), - insert_text_format: value.insert_text_format.map(Into::into), - } - } -} - -impl From for CompletionLabelDetails { - fn from(value: extension::CompletionLabelDetails) -> Self { - Self { - detail: value.detail, - description: value.description, - } - } -} - -impl From for CompletionKind { - fn from(value: extension::CompletionKind) -> Self { - match value { - extension::CompletionKind::Text => Self::Text, - extension::CompletionKind::Method => Self::Method, - extension::CompletionKind::Function => Self::Function, - extension::CompletionKind::Constructor => Self::Constructor, - extension::CompletionKind::Field => Self::Field, - extension::CompletionKind::Variable => Self::Variable, - extension::CompletionKind::Class => Self::Class, - extension::CompletionKind::Interface => Self::Interface, - extension::CompletionKind::Module => Self::Module, - extension::CompletionKind::Property => Self::Property, - extension::CompletionKind::Unit => Self::Unit, - extension::CompletionKind::Value => Self::Value, - extension::CompletionKind::Enum => Self::Enum, - extension::CompletionKind::Keyword => Self::Keyword, - extension::CompletionKind::Snippet => Self::Snippet, - extension::CompletionKind::Color => Self::Color, - extension::CompletionKind::File => Self::File, - extension::CompletionKind::Reference => Self::Reference, - extension::CompletionKind::Folder => Self::Folder, - extension::CompletionKind::EnumMember => Self::EnumMember, - extension::CompletionKind::Constant => Self::Constant, - extension::CompletionKind::Struct => Self::Struct, - extension::CompletionKind::Event => Self::Event, - extension::CompletionKind::Operator => Self::Operator, - extension::CompletionKind::TypeParameter => Self::TypeParameter, - extension::CompletionKind::Other(value) => Self::Other(value), - } - } -} - -impl From for InsertTextFormat { - fn from(value: extension::InsertTextFormat) -> Self { - match value { - extension::InsertTextFormat::PlainText => Self::PlainText, - extension::InsertTextFormat::Snippet => Self::Snippet, - extension::InsertTextFormat::Other(value) => Self::Other(value), - } - } -} - -impl From for Symbol { - fn from(value: extension::Symbol) -> Self { - Self { - kind: value.kind.into(), - name: value.name, - } - } -} - -impl From for SymbolKind { - fn from(value: extension::SymbolKind) -> Self { - match value { - extension::SymbolKind::File => Self::File, - extension::SymbolKind::Module => Self::Module, - extension::SymbolKind::Namespace => Self::Namespace, - extension::SymbolKind::Package => Self::Package, - extension::SymbolKind::Class => Self::Class, - extension::SymbolKind::Method => Self::Method, - extension::SymbolKind::Property => Self::Property, - extension::SymbolKind::Field => Self::Field, - extension::SymbolKind::Constructor => Self::Constructor, - extension::SymbolKind::Enum => Self::Enum, - extension::SymbolKind::Interface => Self::Interface, - extension::SymbolKind::Function => Self::Function, - extension::SymbolKind::Variable => Self::Variable, - extension::SymbolKind::Constant => Self::Constant, - extension::SymbolKind::String => Self::String, - extension::SymbolKind::Number => Self::Number, - extension::SymbolKind::Boolean => Self::Boolean, - extension::SymbolKind::Array => Self::Array, - extension::SymbolKind::Object => Self::Object, - extension::SymbolKind::Key => Self::Key, - extension::SymbolKind::Null => Self::Null, - extension::SymbolKind::EnumMember => Self::EnumMember, - extension::SymbolKind::Struct => Self::Struct, - extension::SymbolKind::Event => Self::Event, - extension::SymbolKind::Operator => Self::Operator, - extension::SymbolKind::TypeParameter => Self::TypeParameter, - extension::SymbolKind::Other(value) => Self::Other(value), - } - } -} - -impl From for SlashCommand { - fn from(value: extension::SlashCommand) -> Self { - Self { - name: value.name, - description: value.description, - tooltip_text: value.tooltip_text, - requires_argument: value.requires_argument, - } - } -} - -impl From for extension::SlashCommandOutput { - fn from(value: SlashCommandOutput) -> Self { - Self { - text: value.text, - sections: value.sections.into_iter().map(Into::into).collect(), - } - } -} - -impl From for extension::SlashCommandOutputSection { - fn from(value: SlashCommandOutputSection) -> Self { - Self { - range: value.range.start as usize..value.range.end as usize, - label: value.label, - } - } -} - -impl From for extension::SlashCommandArgumentCompletion { - fn from(value: SlashCommandArgumentCompletion) -> Self { - Self { - label: value.label, - new_text: value.new_text, - run_command: value.run_command, - } - } -} - -impl TryFrom for extension::ContextServerConfiguration { - type Error = anyhow::Error; - - fn try_from(value: ContextServerConfiguration) -> Result { - let settings_schema: serde_json::Value = serde_json::from_str(&value.settings_schema) - .context("Failed to parse settings_schema")?; - - Ok(Self { - installation_instructions: value.installation_instructions, - default_settings: value.default_settings, - settings_schema, - }) - } -} - -impl HostKeyValueStore for WasmState { - async fn insert( - &mut self, - kv_store: Resource, - key: String, - value: String, - ) -> wasmtime::Result> { - let kv_store = self.table.get(&kv_store)?; - kv_store.insert(key, value).await.to_wasmtime_result() - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of key-value stores. - Ok(()) - } -} - -impl HostProject for WasmState { - async fn worktree_ids( - &mut self, - project: Resource, - ) -> wasmtime::Result> { - let project = self.table.get(&project)?; - Ok(project.worktree_ids()) - } - - async fn drop(&mut self, _project: Resource) -> Result<()> { - // We only ever hand out borrows of projects. - Ok(()) - } -} - -impl HostWorktree for WasmState { - async fn id(&mut self, delegate: Resource>) -> wasmtime::Result { - let delegate = self.table.get(&delegate)?; - Ok(delegate.id()) - } - - async fn root_path( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - let delegate = self.table.get(&delegate)?; - Ok(delegate.root_path()) - } - - async fn read_text_file( - &mut self, - delegate: Resource>, - path: String, - ) -> wasmtime::Result> { - let delegate = self.table.get(&delegate)?; - Ok(delegate - .read_text_file(&RelPath::new(Path::new(&path), PathStyle::Posix)?) - .await - .map_err(|error| error.to_string())) - } - - async fn shell_env( - &mut self, - delegate: Resource>, - ) -> wasmtime::Result { - let delegate = self.table.get(&delegate)?; - Ok(delegate.shell_env().await.into_iter().collect()) - } - - async fn which( - &mut self, - delegate: Resource>, - binary_name: String, - ) -> wasmtime::Result> { - let delegate = self.table.get(&delegate)?; - Ok(delegate.which(binary_name).await) - } - - async fn drop(&mut self, _worktree: Resource) -> Result<()> { - // We only ever hand out borrows of worktrees. - Ok(()) - } -} - -impl common::Host for WasmState {} - -impl http_client::Host for WasmState { - async fn fetch( - &mut self, - request: http_client::HttpRequest, - ) -> wasmtime::Result> { - maybe!(async { - let url = &request.url; - let request = convert_request(&request)?; - let mut response = self.host.http_client.send(request).await?; - - if response.status().is_client_error() || response.status().is_server_error() { - bail!("failed to fetch '{url}': status code {}", response.status()) - } - convert_response(&mut response).await - }) - .await - .to_wasmtime_result() - } - - async fn fetch_stream( - &mut self, - request: http_client::HttpRequest, - ) -> wasmtime::Result, String>> { - let request = convert_request(&request)?; - let response = self.host.http_client.send(request); - maybe!(async { - let response = response.await?; - let stream = Arc::new(Mutex::new(response)); - let resource = self.table.push(stream)?; - Ok(resource) - }) - .await - .to_wasmtime_result() - } -} - -impl http_client::HostHttpResponseStream for WasmState { - async fn next_chunk( - &mut self, - resource: Resource, - ) -> wasmtime::Result>, String>> { - let stream = self.table.get(&resource)?.clone(); - maybe!(async move { - let mut response = stream.lock().await; - let mut buffer = vec![0; 8192]; // 8KB buffer - let bytes_read = response.body_mut().read(&mut buffer).await?; - if bytes_read == 0 { - Ok(None) - } else { - buffer.truncate(bytes_read); - Ok(Some(buffer)) - } - }) - .await - .to_wasmtime_result() - } - - async fn drop(&mut self, _resource: Resource) -> Result<()> { - Ok(()) - } -} - -impl From for ::http_client::Method { - fn from(value: http_client::HttpMethod) -> Self { - match value { - http_client::HttpMethod::Get => Self::GET, - http_client::HttpMethod::Post => Self::POST, - http_client::HttpMethod::Put => Self::PUT, - http_client::HttpMethod::Delete => Self::DELETE, - http_client::HttpMethod::Head => Self::HEAD, - http_client::HttpMethod::Options => Self::OPTIONS, - http_client::HttpMethod::Patch => Self::PATCH, - } - } -} - -fn convert_request( - extension_request: &http_client::HttpRequest, -) -> anyhow::Result<::http_client::Request> { - let mut request = ::http_client::Request::builder() - .method(::http_client::Method::from(extension_request.method)) - .uri(&extension_request.url) - .follow_redirects(match extension_request.redirect_policy { - http_client::RedirectPolicy::NoFollow => ::http_client::RedirectPolicy::NoFollow, - http_client::RedirectPolicy::FollowLimit(limit) => { - ::http_client::RedirectPolicy::FollowLimit(limit) - } - http_client::RedirectPolicy::FollowAll => ::http_client::RedirectPolicy::FollowAll, - }); - for (key, value) in &extension_request.headers { - request = request.header(key, value); - } - let body = extension_request - .body - .clone() - .map(AsyncBody::from) - .unwrap_or_default(); - request.body(body).map_err(anyhow::Error::from) -} - -async fn convert_response( - response: &mut ::http_client::Response, -) -> anyhow::Result { - let mut extension_response = http_client::HttpResponse { - body: Vec::new(), - headers: Vec::new(), - }; - - for (key, value) in response.headers() { - extension_response - .headers - .push((key.to_string(), value.to_str().unwrap_or("").to_string())); - } - - response - .body_mut() - .read_to_end(&mut extension_response.body) - .await?; - - Ok(extension_response) -} - -impl nodejs::Host for WasmState { - async fn node_binary_path(&mut self) -> wasmtime::Result> { - self.host - .node_runtime - .binary_path() - .await - .map(|path| path.to_string_lossy().into_owned()) - .to_wasmtime_result() - } - - async fn npm_package_latest_version( - &mut self, - package_name: String, - ) -> wasmtime::Result> { - self.host - .node_runtime - .npm_package_latest_version(&package_name) - .await - .to_wasmtime_result() - } - - async fn npm_package_installed_version( - &mut self, - package_name: String, - ) -> wasmtime::Result, String>> { - self.host - .node_runtime - .npm_package_installed_version(&self.work_dir(), &package_name) - .await - .to_wasmtime_result() - } - - async fn npm_install_package( - &mut self, - package_name: String, - version: String, - ) -> wasmtime::Result> { - self.capability_granter - .grant_npm_install_package(&package_name)?; - - self.host - .node_runtime - .npm_install_packages(&self.work_dir(), &[(&package_name, &version)]) - .await - .to_wasmtime_result() - } -} - -#[async_trait] -impl lsp::Host for WasmState {} - -impl From<::http_client::github::GithubRelease> for github::GithubRelease { - fn from(value: ::http_client::github::GithubRelease) -> Self { - Self { - version: value.tag_name, - assets: value.assets.into_iter().map(Into::into).collect(), - } - } -} - -impl From<::http_client::github::GithubReleaseAsset> for github::GithubReleaseAsset { - fn from(value: ::http_client::github::GithubReleaseAsset) -> Self { - Self { - name: value.name, - download_url: value.browser_download_url, - } - } -} - -impl github::Host for WasmState { - async fn latest_github_release( - &mut self, - repo: String, - options: github::GithubReleaseOptions, - ) -> wasmtime::Result> { - maybe!(async { - let release = ::http_client::github::latest_github_release( - &repo, - options.require_assets, - options.pre_release, - self.host.http_client.clone(), - ) - .await?; - Ok(release.into()) - }) - .await - .to_wasmtime_result() - } - - async fn github_release_by_tag_name( - &mut self, - repo: String, - tag: String, - ) -> wasmtime::Result> { - maybe!(async { - let release = ::http_client::github::get_release_by_tag_name( - &repo, - &tag, - self.host.http_client.clone(), - ) - .await?; - Ok(release.into()) - }) - .await - .to_wasmtime_result() - } -} - -impl platform::Host for WasmState { - async fn current_platform(&mut self) -> Result<(platform::Os, platform::Architecture)> { - Ok(( - match env::consts::OS { - "macos" => platform::Os::Mac, - "linux" => platform::Os::Linux, - "windows" => platform::Os::Windows, - _ => panic!("unsupported os"), - }, - match env::consts::ARCH { - "aarch64" => platform::Architecture::Aarch64, - "x86" => platform::Architecture::X86, - "x86_64" => platform::Architecture::X8664, - _ => panic!("unsupported architecture"), - }, - )) - } -} - -impl From for process::Output { - fn from(output: std::process::Output) -> Self { - Self { - status: output.status.code(), - stdout: output.stdout, - stderr: output.stderr, - } - } -} - -impl process::Host for WasmState { - async fn run_command( - &mut self, - command: process::Command, - ) -> wasmtime::Result> { - maybe!(async { - self.capability_granter - .grant_exec(&command.command, &command.args)?; - - let output = util::command::new_smol_command(command.command.as_str()) - .args(&command.args) - .envs(command.env) - .output() - .await?; - - Ok(output.into()) - }) - .await - .to_wasmtime_result() - } -} - -#[async_trait] -impl slash_command::Host for WasmState {} - -#[async_trait] -impl context_server::Host for WasmState {} - -impl dap::Host for WasmState { - async fn resolve_tcp_template( - &mut self, - template: TcpArgumentsTemplate, - ) -> wasmtime::Result> { - maybe!(async { - let (host, port, timeout) = - ::dap::configure_tcp_connection(task::TcpArgumentsTemplate { - port: template.port, - host: template.host.map(Ipv4Addr::from_bits), - timeout: template.timeout, - }) - .await?; - Ok(TcpArguments { - port, - host: host.to_bits(), - timeout, - }) - }) - .await - .to_wasmtime_result() - } -} - -impl ExtensionImports for WasmState { - async fn get_settings( - &mut self, - location: Option, - category: String, - key: Option, - ) -> wasmtime::Result> { - self.on_main_thread(|cx| { - async move { - let path = location.as_ref().and_then(|location| { - RelPath::new(Path::new(&location.path), PathStyle::Posix).ok() - }); - let location = path - .as_ref() - .zip(location.as_ref()) - .map(|(path, location)| ::settings::SettingsLocation { - worktree_id: WorktreeId::from_proto(location.worktree_id), - path, - }); - - cx.update(|cx| match category.as_str() { - "language" => { - let key = key.map(|k| LanguageName::new(&k)); - let settings = AllLanguageSettings::get(location, cx).language( - location, - key.as_ref(), - cx, - ); - Ok(serde_json::to_string(&settings::LanguageSettings { - tab_size: settings.tab_size, - })?) - } - "lsp" => { - let settings = key - .and_then(|key| { - ProjectSettings::get(location, cx) - .lsp - .get(&::lsp::LanguageServerName::from_proto(key)) - }) - .cloned() - .unwrap_or_default(); - Ok(serde_json::to_string(&settings::LspSettings { - binary: settings.binary.map(|binary| settings::CommandSettings { - path: binary.path, - arguments: binary.arguments, - env: binary.env.map(|env| env.into_iter().collect()), - }), - settings: settings.settings, - initialization_options: settings.initialization_options, - })?) - } - "context_servers" => { - let settings = key - .and_then(|key| { - ProjectSettings::get(location, cx) - .context_servers - .get(key.as_str()) - }) - .cloned() - .unwrap_or_else(|| { - project::project_settings::ContextServerSettings::default_extension( - ) - }); - - match settings { - project::project_settings::ContextServerSettings::Stdio { - enabled: _, - command, - } => Ok(serde_json::to_string(&settings::ContextServerSettings { - command: Some(settings::CommandSettings { - path: command.path.to_str().map(|path| path.to_string()), - arguments: Some(command.args), - env: command.env.map(|env| env.into_iter().collect()), - }), - settings: None, - })?), - project::project_settings::ContextServerSettings::Extension { - enabled: _, - settings, - } => Ok(serde_json::to_string(&settings::ContextServerSettings { - command: None, - settings: Some(settings), - })?), - project::project_settings::ContextServerSettings::Http { .. } => { - bail!("remote context server settings not supported in 0.6.0") - } - } - } - _ => { - bail!("Unknown settings category: {}", category); - } - }) - } - .boxed_local() - }) - .await? - .to_wasmtime_result() - } - - async fn set_language_server_installation_status( - &mut self, - server_name: String, - status: LanguageServerInstallationStatus, - ) -> wasmtime::Result<()> { - let status = match status { - LanguageServerInstallationStatus::CheckingForUpdate => BinaryStatus::CheckingForUpdate, - LanguageServerInstallationStatus::Downloading => BinaryStatus::Downloading, - LanguageServerInstallationStatus::None => BinaryStatus::None, - LanguageServerInstallationStatus::Failed(error) => BinaryStatus::Failed { error }, - }; - - self.host - .proxy - .update_language_server_status(::lsp::LanguageServerName(server_name.into()), status); - - Ok(()) - } - - async fn download_file( - &mut self, - url: String, - path: String, - file_type: DownloadedFileType, - ) -> wasmtime::Result> { - maybe!(async { - let parsed_url = Url::parse(&url)?; - self.capability_granter.grant_download_file(&parsed_url)?; - - let path = PathBuf::from(path); - let extension_work_dir = self.host.work_dir.join(self.manifest.id.as_ref()); - - self.host.fs.create_dir(&extension_work_dir).await?; - - let destination_path = self - .host - .writeable_path_from_extension(&self.manifest.id, &path)?; - - let mut response = self - .host - .http_client - .get(&url, Default::default(), true) - .await - .context("downloading release")?; - - anyhow::ensure!( - response.status().is_success(), - "download failed with status {}", - response.status() - ); - let body = BufReader::new(response.body_mut()); - - match file_type { - DownloadedFileType::Uncompressed => { - futures::pin_mut!(body); - self.host - .fs - .create_file_with(&destination_path, body) - .await?; - } - DownloadedFileType::Gzip => { - let body = GzipDecoder::new(body); - futures::pin_mut!(body); - self.host - .fs - .create_file_with(&destination_path, body) - .await?; - } - DownloadedFileType::GzipTar => { - let body = GzipDecoder::new(body); - futures::pin_mut!(body); - self.host - .fs - .extract_tar_file(&destination_path, Archive::new(body)) - .await?; - } - DownloadedFileType::Zip => { - futures::pin_mut!(body); - extract_zip(&destination_path, body) - .await - .with_context(|| format!("unzipping {path:?} archive"))?; - } - } - - Ok(()) - }) - .await - .to_wasmtime_result() - } - - async fn make_file_executable(&mut self, path: String) -> wasmtime::Result> { - let path = self - .host - .writeable_path_from_extension(&self.manifest.id, Path::new(&path))?; - - make_file_executable(&path) - .await - .with_context(|| format!("setting permissions for path {path:?}")) - .to_wasmtime_result() - } -} diff --git a/crates/extensions_ui/Cargo.toml b/crates/extensions_ui/Cargo.toml deleted file mode 100644 index 707938a9eb..0000000000 --- a/crates/extensions_ui/Cargo.toml +++ /dev/null @@ -1,45 +0,0 @@ -[package] -name = "extensions_ui" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/extensions_ui.rs" - -[dependencies] -anyhow.workspace = true -client.workspace = true -collections.workspace = true -db.workspace = true -editor.workspace = true -extension.workspace = true -extension_host.workspace = true -fs.workspace = true -fuzzy.workspace = true -gpui.workspace = true -language.workspace = true -log.workspace = true -num-format.workspace = true -picker.workspace = true -project.workspace = true -release_channel.workspace = true -semver.workspace = true -serde.workspace = true -settings.workspace = true -smallvec.workspace = true -strum.workspace = true -telemetry.workspace = true -theme.workspace = true -ui.workspace = true -util.workspace = true -vim_mode_setting.workspace = true -workspace.workspace = true -zed_actions.workspace = true - -[dev-dependencies] -editor = { workspace = true, features = ["test-support"] } diff --git a/crates/extensions_ui/LICENSE-GPL b/crates/extensions_ui/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/extensions_ui/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/extensions_ui/src/components.rs b/crates/extensions_ui/src/components.rs deleted file mode 100644 index bf11abd679..0000000000 --- a/crates/extensions_ui/src/components.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod extension_card; - -pub use extension_card::*; diff --git a/crates/extensions_ui/src/components/extension_card.rs b/crates/extensions_ui/src/components/extension_card.rs deleted file mode 100644 index 524f90c7f0..0000000000 --- a/crates/extensions_ui/src/components/extension_card.rs +++ /dev/null @@ -1,61 +0,0 @@ -use gpui::{AnyElement, prelude::*}; -use smallvec::SmallVec; -use ui::prelude::*; - -#[derive(IntoElement)] -pub struct ExtensionCard { - overridden_by_dev_extension: bool, - children: SmallVec<[AnyElement; 2]>, -} - -impl ExtensionCard { - pub fn new() -> Self { - Self { - overridden_by_dev_extension: false, - children: SmallVec::new(), - } - } - - pub fn overridden_by_dev_extension(mut self, overridden: bool) -> Self { - self.overridden_by_dev_extension = overridden; - self - } -} - -impl ParentElement for ExtensionCard { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl RenderOnce for ExtensionCard { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - div().w_full().child( - v_flex() - .mt_4() - .w_full() - .h(rems_from_px(110.)) - .p_3() - .gap_2() - .bg(cx.theme().colors().elevated_surface_background.opacity(0.5)) - .border_1() - .border_color(cx.theme().colors().border_variant) - .rounded_md() - .children(self.children) - .when(self.overridden_by_dev_extension, |card| { - card.child( - h_flex() - .absolute() - .top_0() - .left_0() - .block_mouse_except_scroll() - .cursor_default() - .size_full() - .justify_center() - .bg(cx.theme().colors().elevated_surface_background.alpha(0.8)) - .child(Label::new("Overridden by dev extension.")), - ) - }), - ) - } -} diff --git a/crates/extensions_ui/src/extension_suggest.rs b/crates/extensions_ui/src/extension_suggest.rs deleted file mode 100644 index 7ad4c1540a..0000000000 --- a/crates/extensions_ui/src/extension_suggest.rs +++ /dev/null @@ -1,248 +0,0 @@ -use std::collections::HashMap; -use std::sync::{Arc, OnceLock}; - -use db::kvp::KEY_VALUE_STORE; -use editor::Editor; -use extension_host::ExtensionStore; -use gpui::{AppContext as _, Context, Entity, SharedString, Window}; -use language::Buffer; -use ui::prelude::*; -use util::rel_path::RelPath; -use workspace::notifications::simple_message_notification::MessageNotification; -use workspace::{Workspace, notifications::NotificationId}; - -const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[ - ("astro", &["astro"]), - ("beancount", &["beancount"]), - ("clojure", &["bb", "clj", "cljc", "cljs", "edn"]), - ("neocmake", &["CMakeLists.txt", "cmake"]), - ("csharp", &["cs"]), - ("cython", &["pyx", "pxd", "pxi"]), - ("dart", &["dart"]), - ("dockerfile", &["Dockerfile"]), - ("elisp", &["el"]), - ("elixir", &["ex", "exs", "heex"]), - ("elm", &["elm"]), - ("erlang", &["erl", "hrl"]), - ("fish", &["fish"]), - ( - "git-firefly", - &[ - ".gitconfig", - ".gitignore", - "COMMIT_EDITMSG", - "EDIT_DESCRIPTION", - "MERGE_MSG", - "NOTES_EDITMSG", - "TAG_EDITMSG", - "git-rebase-todo", - ], - ), - ("gleam", &["gleam"]), - ("glsl", &["vert", "frag"]), - ("graphql", &["gql", "graphql"]), - ("haskell", &["hs"]), - ("html", &["htm", "html", "shtml"]), - ("java", &["java"]), - ("kotlin", &["kt"]), - ("latex", &["tex"]), - ("log", &["log"]), - ("lua", &["lua"]), - ("make", &["Makefile"]), - ("nim", &["nim"]), - ("nix", &["nix"]), - ("nu", &["nu"]), - ("ocaml", &["ml", "mli"]), - ("php", &["php"]), - ("powershell", &["ps1", "psm1"]), - ("prisma", &["prisma"]), - ("proto", &["proto"]), - ("purescript", &["purs"]), - ("r", &["r", "R"]), - ("racket", &["rkt"]), - ("rescript", &["res", "resi"]), - ("rst", &["rst"]), - ("ruby", &["rb", "erb"]), - ("scheme", &["scm"]), - ("scss", &["scss"]), - ("sql", &["sql"]), - ("svelte", &["svelte"]), - ("swift", &["swift"]), - ("templ", &["templ"]), - ("terraform", &["tf", "tfvars", "hcl"]), - ("toml", &["Cargo.lock", "toml"]), - ("typst", &["typ"]), - ("vue", &["vue"]), - ("wgsl", &["wgsl"]), - ("wit", &["wit"]), - ("xml", &["xml"]), - ("zig", &["zig"]), -]; - -fn suggested_extensions() -> &'static HashMap<&'static str, Arc> { - static SUGGESTIONS_BY_PATH_SUFFIX: OnceLock>> = OnceLock::new(); - SUGGESTIONS_BY_PATH_SUFFIX.get_or_init(|| { - SUGGESTIONS_BY_EXTENSION_ID - .iter() - .flat_map(|(name, path_suffixes)| { - let name = Arc::::from(*name); - path_suffixes - .iter() - .map(move |suffix| (*suffix, name.clone())) - }) - .collect() - }) -} - -#[derive(Debug, PartialEq, Eq, Clone)] -struct SuggestedExtension { - pub extension_id: Arc, - pub file_name_or_extension: Arc, -} - -/// Returns the suggested extension for the given [`Path`]. -fn suggested_extension(path: &RelPath) -> Option { - let file_extension: Option> = path.extension().map(|extension| extension.into()); - let file_name: Option> = path.file_name().map(|name| name.into()); - - let (file_name_or_extension, extension_id) = None - // We suggest against file names first, as these suggestions will be more - // specific than ones based on the file extension. - .or_else(|| { - file_name.clone().zip( - file_name - .as_deref() - .and_then(|file_name| suggested_extensions().get(file_name)), - ) - }) - .or_else(|| { - file_extension.clone().zip( - file_extension - .as_deref() - .and_then(|file_extension| suggested_extensions().get(file_extension)), - ) - })?; - - Some(SuggestedExtension { - extension_id: extension_id.clone(), - file_name_or_extension, - }) -} - -fn language_extension_key(extension_id: &str) -> String { - format!("{}_extension_suggest", extension_id) -} - -pub(crate) fn suggest(buffer: Entity, window: &mut Window, cx: &mut Context) { - let Some(file) = buffer.read(cx).file().cloned() else { - return; - }; - - let Some(SuggestedExtension { - extension_id, - file_name_or_extension, - }) = suggested_extension(file.path()) - else { - return; - }; - - let key = language_extension_key(&extension_id); - let Ok(None) = KEY_VALUE_STORE.read_kvp(&key) else { - return; - }; - - cx.on_next_frame(window, move |workspace, _, cx| { - let Some(editor) = workspace.active_item_as::(cx) else { - return; - }; - - if editor.read(cx).buffer().read(cx).as_singleton().as_ref() != Some(&buffer) { - return; - } - - struct ExtensionSuggestionNotification; - - let notification_id = NotificationId::composite::( - SharedString::from(extension_id.clone()), - ); - - workspace.show_notification(notification_id, cx, |cx| { - cx.new(move |cx| { - MessageNotification::new( - format!( - "Do you want to install the recommended '{}' extension for '{}' files?", - extension_id, file_name_or_extension - ), - cx, - ) - .primary_message("Yes, install extension") - .primary_icon(IconName::Check) - .primary_icon_color(Color::Success) - .primary_on_click({ - let extension_id = extension_id.clone(); - move |_window, cx| { - let extension_id = extension_id.clone(); - let extension_store = ExtensionStore::global(cx); - extension_store.update(cx, move |store, cx| { - store.install_latest_extension(extension_id, cx); - }); - } - }) - .secondary_message("No, don't install it") - .secondary_icon(IconName::Close) - .secondary_icon_color(Color::Error) - .secondary_on_click(move |_window, cx| { - let key = language_extension_key(&extension_id); - db::write_and_log(cx, move || { - KEY_VALUE_STORE.write_kvp(key, "dismissed".to_string()) - }); - }) - }) - }); - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use util::rel_path::rel_path; - - #[test] - pub fn test_suggested_extension() { - assert_eq!( - suggested_extension(rel_path("Cargo.toml")), - Some(SuggestedExtension { - extension_id: "toml".into(), - file_name_or_extension: "toml".into() - }) - ); - assert_eq!( - suggested_extension(rel_path("Cargo.lock")), - Some(SuggestedExtension { - extension_id: "toml".into(), - file_name_or_extension: "Cargo.lock".into() - }) - ); - assert_eq!( - suggested_extension(rel_path("Dockerfile")), - Some(SuggestedExtension { - extension_id: "dockerfile".into(), - file_name_or_extension: "Dockerfile".into() - }) - ); - assert_eq!( - suggested_extension(rel_path("a/b/c/d/.gitignore")), - Some(SuggestedExtension { - extension_id: "git-firefly".into(), - file_name_or_extension: ".gitignore".into() - }) - ); - assert_eq!( - suggested_extension(rel_path("a/b/c/d/test.gleam")), - Some(SuggestedExtension { - extension_id: "gleam".into(), - file_name_or_extension: "gleam".into() - }) - ); - } -} diff --git a/crates/extensions_ui/src/extension_version_selector.rs b/crates/extensions_ui/src/extension_version_selector.rs deleted file mode 100644 index 17d293da76..0000000000 --- a/crates/extensions_ui/src/extension_version_selector.rs +++ /dev/null @@ -1,251 +0,0 @@ -use std::str::FromStr; -use std::sync::Arc; - -use client::ExtensionMetadata; -use extension_host::ExtensionStore; -use fs::Fs; -use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; -use gpui::{App, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, prelude::*}; -use picker::{Picker, PickerDelegate}; -use release_channel::ReleaseChannel; -use semver::Version; -use settings::update_settings_file; -use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*}; -use util::ResultExt; -use workspace::ModalView; - -pub struct ExtensionVersionSelector { - picker: Entity>, -} - -impl ModalView for ExtensionVersionSelector {} - -impl EventEmitter for ExtensionVersionSelector {} - -impl Focusable for ExtensionVersionSelector { - fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { - self.picker.focus_handle(cx) - } -} - -impl Render for ExtensionVersionSelector { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - v_flex().w(rems(34.)).child(self.picker.clone()) - } -} - -impl ExtensionVersionSelector { - pub fn new( - delegate: ExtensionVersionSelectorDelegate, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); - Self { picker } - } -} - -pub struct ExtensionVersionSelectorDelegate { - fs: Arc, - selector: WeakEntity, - extension_versions: Vec, - selected_index: usize, - matches: Vec, -} - -impl ExtensionVersionSelectorDelegate { - pub fn new( - fs: Arc, - selector: WeakEntity, - mut extension_versions: Vec, - ) -> Self { - extension_versions.sort_unstable_by(|a, b| { - let a_version = Version::from_str(&a.manifest.version); - let b_version = Version::from_str(&b.manifest.version); - - match (a_version, b_version) { - (Ok(a_version), Ok(b_version)) => b_version.cmp(&a_version), - _ => b.published_at.cmp(&a.published_at), - } - }); - - let matches = extension_versions - .iter() - .map(|extension| StringMatch { - candidate_id: 0, - score: 0.0, - positions: Default::default(), - string: format!("v{}", extension.manifest.version), - }) - .collect(); - - Self { - fs, - selector, - extension_versions, - selected_index: 0, - matches, - } - } -} - -impl PickerDelegate for ExtensionVersionSelectorDelegate { - type ListItem = ui::ListItem; - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - "Select extension version...".into() - } - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index( - &mut self, - ix: usize, - _window: &mut Window, - _cx: &mut Context>, - ) { - self.selected_index = ix; - } - - fn update_matches( - &mut self, - query: String, - window: &mut Window, - cx: &mut Context>, - ) -> Task<()> { - let background_executor = cx.background_executor().clone(); - let candidates = self - .extension_versions - .iter() - .enumerate() - .map(|(id, extension)| { - StringMatchCandidate::new(id, &format!("v{}", extension.manifest.version)) - }) - .collect::>(); - - cx.spawn_in(window, async move |this, cx| { - let matches = if query.is_empty() { - candidates - .into_iter() - .enumerate() - .map(|(index, candidate)| StringMatch { - candidate_id: index, - string: candidate.string, - positions: Vec::new(), - score: 0.0, - }) - .collect() - } else { - match_strings( - &candidates, - &query, - false, - true, - 100, - &Default::default(), - background_executor, - ) - .await - }; - - this.update(cx, |this, _cx| { - this.delegate.matches = matches; - this.delegate.selected_index = this - .delegate - .selected_index - .min(this.delegate.matches.len().saturating_sub(1)); - }) - .log_err(); - }) - } - - fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context>) { - if self.matches.is_empty() { - self.dismissed(window, cx); - return; - } - - let candidate_id = self.matches[self.selected_index].candidate_id; - let extension_version = &self.extension_versions[candidate_id]; - - if !extension_host::is_version_compatible(ReleaseChannel::global(cx), extension_version) { - return; - } - - let extension_store = ExtensionStore::global(cx); - extension_store.update(cx, |store, cx| { - let extension_id = extension_version.id.clone(); - let version = extension_version.manifest.version.clone(); - - update_settings_file(self.fs.clone(), cx, { - let extension_id = extension_id.clone(); - move |settings, _| { - settings - .extension - .auto_update_extensions - .insert(extension_id, false); - } - }); - - store.install_extension(extension_id, version, cx); - }); - } - - fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { - self.selector - .update(cx, |_, cx| cx.emit(DismissEvent)) - .log_err(); - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _: &mut Window, - cx: &mut Context>, - ) -> Option { - let version_match = &self.matches.get(ix)?; - let extension_version = &self.extension_versions.get(version_match.candidate_id)?; - - let is_version_compatible = - extension_host::is_version_compatible(ReleaseChannel::global(cx), extension_version); - let disabled = !is_version_compatible; - - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .disabled(disabled) - .child( - HighlightedLabel::new( - version_match.string.clone(), - version_match.positions.clone(), - ) - .when(disabled, |label| label.color(Color::Muted)), - ) - .end_slot( - h_flex() - .gap_2() - .when(!is_version_compatible, |this| { - this.child(Label::new("Incompatible").color(Color::Muted)) - }) - .child( - Label::new( - extension_version - .published_at - .format("%Y-%m-%d") - .to_string(), - ) - .when(disabled, |label| label.color(Color::Muted)), - ), - ), - ) - } -} diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs deleted file mode 100644 index 3dd4803ce1..0000000000 --- a/crates/extensions_ui/src/extensions_ui.rs +++ /dev/null @@ -1,1784 +0,0 @@ -mod components; -mod extension_suggest; -mod extension_version_selector; - -use std::sync::OnceLock; -use std::time::Duration; -use std::{ops::Range, sync::Arc}; - -use anyhow::Context as _; -use client::{ExtensionMetadata, ExtensionProvides}; -use collections::{BTreeMap, BTreeSet}; -use editor::{Editor, EditorElement, EditorStyle}; -use extension_host::{ExtensionManifest, ExtensionOperation, ExtensionStore}; -use fuzzy::{StringMatchCandidate, match_strings}; -use gpui::{ - Action, App, ClipboardItem, Context, Corner, Entity, EventEmitter, Flatten, Focusable, - InteractiveElement, KeyContext, ParentElement, Point, Render, Styled, Task, TextStyle, - UniformListScrollHandle, WeakEntity, Window, actions, point, uniform_list, -}; -use num_format::{Locale, ToFormattedString}; -use project::DirectoryLister; -use release_channel::ReleaseChannel; -use settings::{Settings, SettingsContent}; -use strum::IntoEnumIterator as _; -use theme::ThemeSettings; -use ui::{ - Banner, Chip, ContextMenu, Divider, PopoverMenu, ScrollableHandle, Switch, ToggleButtonGroup, - ToggleButtonGroupSize, ToggleButtonGroupStyle, ToggleButtonSimple, Tooltip, WithScrollbar, - prelude::*, -}; -use vim_mode_setting::VimModeSetting; -use workspace::{ - Workspace, - item::{Item, ItemEvent}, -}; -use zed_actions::ExtensionCategoryFilter; - -use crate::components::ExtensionCard; -use crate::extension_version_selector::{ - ExtensionVersionSelector, ExtensionVersionSelectorDelegate, -}; - -actions!( - zed, - [ - /// Installs an extension from a local directory for development. - InstallDevExtension - ] -); - -pub fn init(cx: &mut App) { - cx.observe_new(move |workspace: &mut Workspace, window, cx| { - let Some(window) = window else { - return; - }; - workspace - .register_action( - move |workspace, action: &zed_actions::Extensions, window, cx| { - let provides_filter = action.category_filter.map(|category| match category { - ExtensionCategoryFilter::Themes => ExtensionProvides::Themes, - ExtensionCategoryFilter::IconThemes => ExtensionProvides::IconThemes, - ExtensionCategoryFilter::Languages => ExtensionProvides::Languages, - ExtensionCategoryFilter::Grammars => ExtensionProvides::Grammars, - ExtensionCategoryFilter::LanguageServers => { - ExtensionProvides::LanguageServers - } - ExtensionCategoryFilter::ContextServers => { - ExtensionProvides::ContextServers - } - ExtensionCategoryFilter::AgentServers => ExtensionProvides::AgentServers, - ExtensionCategoryFilter::SlashCommands => ExtensionProvides::SlashCommands, - ExtensionCategoryFilter::IndexedDocsProviders => { - ExtensionProvides::IndexedDocsProviders - } - ExtensionCategoryFilter::Snippets => ExtensionProvides::Snippets, - ExtensionCategoryFilter::DebugAdapters => ExtensionProvides::DebugAdapters, - }); - - let existing = workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::()); - - if let Some(existing) = existing { - existing.update(cx, |extensions_page, cx| { - if provides_filter.is_some() { - extensions_page.change_provides_filter(provides_filter, cx); - } - if let Some(id) = action.id.as_ref() { - extensions_page.focus_extension(id, window, cx); - } - }); - - workspace.activate_item(&existing, true, true, window, cx); - } else { - let extensions_page = ExtensionsPage::new( - workspace, - provides_filter, - action.id.as_deref(), - window, - cx, - ); - workspace.add_item_to_active_pane( - Box::new(extensions_page), - None, - true, - window, - cx, - ) - } - }, - ) - .register_action(move |workspace, _: &InstallDevExtension, window, cx| { - let store = ExtensionStore::global(cx); - let prompt = workspace.prompt_for_open_path( - gpui::PathPromptOptions { - files: false, - directories: true, - multiple: false, - prompt: None, - }, - DirectoryLister::Local( - workspace.project().clone(), - workspace.app_state().fs.clone(), - ), - window, - cx, - ); - - let workspace_handle = cx.entity().downgrade(); - window - .spawn(cx, async move |cx| { - let extension_path = - match Flatten::flatten(prompt.await.map_err(|e| e.into())) { - Ok(Some(mut paths)) => paths.pop()?, - Ok(None) => return None, - Err(err) => { - workspace_handle - .update(cx, |workspace, cx| { - workspace.show_portal_error(err.to_string(), cx); - }) - .ok(); - return None; - } - }; - - let install_task = store - .update(cx, |store, cx| { - store.install_dev_extension(extension_path, cx) - }) - .ok()?; - - match install_task.await { - Ok(_) => {} - Err(err) => { - log::error!("Failed to install dev extension: {:?}", err); - workspace_handle - .update(cx, |workspace, cx| { - workspace.show_error( - // NOTE: using `anyhow::context` here ends up not printing - // the error - &format!("Failed to install dev extension: {}", err), - cx, - ); - }) - .ok(); - } - } - - Some(()) - }) - .detach(); - }); - - cx.subscribe_in(workspace.project(), window, |_, _, event, window, cx| { - if let project::Event::LanguageNotFound(buffer) = event { - extension_suggest::suggest(buffer.clone(), window, cx); - } - }) - .detach(); - }) - .detach(); -} - -fn extension_provides_label(provides: ExtensionProvides) -> &'static str { - match provides { - ExtensionProvides::Themes => "Themes", - ExtensionProvides::IconThemes => "Icon Themes", - ExtensionProvides::Languages => "Languages", - ExtensionProvides::Grammars => "Grammars", - ExtensionProvides::LanguageServers => "Language Servers", - ExtensionProvides::ContextServers => "MCP Servers", - ExtensionProvides::AgentServers => "Agent Servers", - ExtensionProvides::SlashCommands => "Slash Commands", - ExtensionProvides::IndexedDocsProviders => "Indexed Docs Providers", - ExtensionProvides::Snippets => "Snippets", - ExtensionProvides::DebugAdapters => "Debug Adapters", - } -} - -#[derive(Clone)] -pub enum ExtensionStatus { - NotInstalled, - Installing, - Upgrading, - Installed(Arc), - Removing, -} - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -enum ExtensionFilter { - All, - Installed, - NotInstalled, -} - -impl ExtensionFilter { - pub fn include_dev_extensions(&self) -> bool { - match self { - Self::All | Self::Installed => true, - Self::NotInstalled => false, - } - } -} - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -enum Feature { - AgentClaude, - AgentCodex, - AgentGemini, - ExtensionBasedpyright, - ExtensionRuff, - ExtensionTailwind, - ExtensionTy, - Git, - LanguageBash, - LanguageC, - LanguageCpp, - LanguageGo, - LanguagePython, - LanguageReact, - LanguageRust, - LanguageTypescript, - OpenIn, - Vim, -} - -fn keywords_by_feature() -> &'static BTreeMap> { - static KEYWORDS_BY_FEATURE: OnceLock>> = OnceLock::new(); - KEYWORDS_BY_FEATURE.get_or_init(|| { - BTreeMap::from_iter([ - (Feature::AgentClaude, vec!["claude", "claude code"]), - (Feature::AgentCodex, vec!["codex", "codex cli"]), - (Feature::AgentGemini, vec!["gemini", "gemini cli"]), - ( - Feature::ExtensionBasedpyright, - vec!["basedpyright", "pyright"], - ), - (Feature::ExtensionRuff, vec!["ruff"]), - (Feature::ExtensionTailwind, vec!["tail", "tailwind"]), - (Feature::ExtensionTy, vec!["ty"]), - (Feature::Git, vec!["git"]), - (Feature::LanguageBash, vec!["sh", "bash"]), - (Feature::LanguageC, vec!["c", "clang"]), - (Feature::LanguageCpp, vec!["c++", "cpp", "clang"]), - (Feature::LanguageGo, vec!["go", "golang"]), - (Feature::LanguagePython, vec!["python", "py"]), - (Feature::LanguageReact, vec!["react"]), - (Feature::LanguageRust, vec!["rust", "rs"]), - ( - Feature::LanguageTypescript, - vec!["type", "typescript", "ts"], - ), - ( - Feature::OpenIn, - vec![ - "github", - "gitlab", - "bitbucket", - "codeberg", - "sourcehut", - "permalink", - "link", - "open in", - ], - ), - (Feature::Vim, vec!["vim"]), - ]) - }) -} - -struct ExtensionCardButtons { - install_or_uninstall: Button, - upgrade: Option", - "

Some text

", - false, - false, - ), - ] { - let mut w = Vec::new(); - let mut minifier = Minifier::new( - &mut w, - MinifierOptions { - omit_doctype: true, - preserve_comments, - collapse_whitespace, - }, - ); - minifier.minify(&mut input.as_bytes()).unwrap(); - - let s = str::from_utf8(&w).unwrap(); - - assert_eq!(expected, s); - } - } -} diff --git a/crates/markdown_preview/src/markdown_parser.rs b/crates/markdown_preview/src/markdown_parser.rs deleted file mode 100644 index b17ee5cac4..0000000000 --- a/crates/markdown_preview/src/markdown_parser.rs +++ /dev/null @@ -1,3218 +0,0 @@ -use crate::{ - markdown_elements::*, - markdown_minifier::{Minifier, MinifierOptions}, -}; -use async_recursion::async_recursion; -use collections::FxHashMap; -use gpui::{DefiniteLength, FontWeight, px, relative}; -use html5ever::{ParseOpts, local_name, parse_document, tendril::TendrilSink}; -use language::LanguageRegistry; -use markup5ever_rcdom::RcDom; -use pulldown_cmark::{Alignment, Event, Options, Parser, Tag, TagEnd}; -use std::{ - cell::RefCell, collections::HashMap, mem, ops::Range, path::PathBuf, rc::Rc, sync::Arc, vec, -}; -use ui::SharedString; - -pub async fn parse_markdown( - markdown_input: &str, - file_location_directory: Option, - language_registry: Option>, -) -> ParsedMarkdown { - let mut options = Options::all(); - options.remove(pulldown_cmark::Options::ENABLE_DEFINITION_LIST); - - let parser = Parser::new_ext(markdown_input, options); - let parser = MarkdownParser::new( - parser.into_offset_iter().collect(), - file_location_directory, - language_registry, - ); - let renderer = parser.parse_document().await; - ParsedMarkdown { - children: renderer.parsed, - } -} - -fn cleanup_html(source: &str) -> Vec { - let mut writer = std::io::Cursor::new(Vec::new()); - let mut reader = std::io::Cursor::new(source); - let mut minify = Minifier::new( - &mut writer, - MinifierOptions { - omit_doctype: true, - collapse_whitespace: true, - ..Default::default() - }, - ); - if let Ok(()) = minify.minify(&mut reader) { - writer.into_inner() - } else { - source.bytes().collect() - } -} - -struct MarkdownParser<'a> { - tokens: Vec<(Event<'a>, Range)>, - /// The current index in the tokens array - cursor: usize, - /// The blocks that we have successfully parsed so far - parsed: Vec, - file_location_directory: Option, - language_registry: Option>, -} - -#[derive(Debug)] -struct ParseHtmlNodeContext { - list_item_depth: u16, -} - -impl Default for ParseHtmlNodeContext { - fn default() -> Self { - Self { list_item_depth: 1 } - } -} - -struct MarkdownListItem { - content: Vec, - item_type: ParsedMarkdownListItemType, -} - -impl Default for MarkdownListItem { - fn default() -> Self { - Self { - content: Vec::new(), - item_type: ParsedMarkdownListItemType::Unordered, - } - } -} - -impl<'a> MarkdownParser<'a> { - fn new( - tokens: Vec<(Event<'a>, Range)>, - file_location_directory: Option, - language_registry: Option>, - ) -> Self { - Self { - tokens, - file_location_directory, - language_registry, - cursor: 0, - parsed: vec![], - } - } - - fn eof(&self) -> bool { - if self.tokens.is_empty() { - return true; - } - self.cursor >= self.tokens.len() - 1 - } - - fn peek(&self, steps: usize) -> Option<&(Event<'_>, Range)> { - if self.eof() || (steps + self.cursor) >= self.tokens.len() { - return self.tokens.last(); - } - self.tokens.get(self.cursor + steps) - } - - fn previous(&self) -> Option<&(Event<'_>, Range)> { - if self.cursor == 0 || self.cursor > self.tokens.len() { - return None; - } - self.tokens.get(self.cursor - 1) - } - - fn current(&self) -> Option<&(Event<'_>, Range)> { - self.peek(0) - } - - fn current_event(&self) -> Option<&Event<'_>> { - self.current().map(|(event, _)| event) - } - - fn is_text_like(event: &Event) -> bool { - match event { - Event::Text(_) - // Represent an inline code block - | Event::Code(_) - | Event::Html(_) - | Event::InlineHtml(_) - | Event::FootnoteReference(_) - | Event::Start(Tag::Link { .. }) - | Event::Start(Tag::Emphasis) - | Event::Start(Tag::Strong) - | Event::Start(Tag::Strikethrough) - | Event::Start(Tag::Image { .. }) => { - true - } - _ => false, - } - } - - async fn parse_document(mut self) -> Self { - while !self.eof() { - if let Some(block) = self.parse_block().await { - self.parsed.extend(block); - } else { - self.cursor += 1; - } - } - self - } - - #[async_recursion] - async fn parse_block(&mut self) -> Option> { - let (current, source_range) = self.current().unwrap(); - let source_range = source_range.clone(); - match current { - Event::Start(tag) => match tag { - Tag::Paragraph => { - self.cursor += 1; - let text = self.parse_text(false, Some(source_range)); - Some(vec![ParsedMarkdownElement::Paragraph(text)]) - } - Tag::Heading { level, .. } => { - let level = *level; - self.cursor += 1; - let heading = self.parse_heading(level); - Some(vec![ParsedMarkdownElement::Heading(heading)]) - } - Tag::Table(alignment) => { - let alignment = alignment.clone(); - self.cursor += 1; - let table = self.parse_table(alignment); - Some(vec![ParsedMarkdownElement::Table(table)]) - } - Tag::List(order) => { - let order = *order; - self.cursor += 1; - let list = self.parse_list(order).await; - Some(list) - } - Tag::BlockQuote(_kind) => { - self.cursor += 1; - let block_quote = self.parse_block_quote().await; - Some(vec![ParsedMarkdownElement::BlockQuote(block_quote)]) - } - Tag::CodeBlock(kind) => { - let language = match kind { - pulldown_cmark::CodeBlockKind::Indented => None, - pulldown_cmark::CodeBlockKind::Fenced(language) => { - if language.is_empty() { - None - } else { - Some(language.to_string()) - } - } - }; - - self.cursor += 1; - - let code_block = self.parse_code_block(language).await?; - Some(vec![ParsedMarkdownElement::CodeBlock(code_block)]) - } - Tag::HtmlBlock => { - self.cursor += 1; - - Some(self.parse_html_block().await) - } - _ => None, - }, - Event::Rule => { - self.cursor += 1; - Some(vec![ParsedMarkdownElement::HorizontalRule(source_range)]) - } - _ => None, - } - } - - fn parse_text( - &mut self, - should_complete_on_soft_break: bool, - source_range: Option>, - ) -> MarkdownParagraph { - let source_range = source_range.unwrap_or_else(|| { - self.current() - .map(|(_, range)| range.clone()) - .unwrap_or_default() - }); - - let mut markdown_text_like = Vec::new(); - let mut text = String::new(); - let mut bold_depth = 0; - let mut italic_depth = 0; - let mut strikethrough_depth = 0; - let mut link: Option = None; - let mut image: Option = None; - let mut regions: Vec<(Range, ParsedRegion)> = vec![]; - let mut highlights: Vec<(Range, MarkdownHighlight)> = vec![]; - let mut link_urls: Vec = vec![]; - let mut link_ranges: Vec> = vec![]; - - loop { - if self.eof() { - break; - } - - let (current, _) = self.current().unwrap(); - let prev_len = text.len(); - match current { - Event::SoftBreak => { - if should_complete_on_soft_break { - break; - } - text.push(' '); - } - - Event::HardBreak => { - text.push('\n'); - } - - // We want to ignore any inline HTML tags in the text but keep - // the text between them - Event::InlineHtml(_) => {} - - Event::Text(t) => { - text.push_str(t.as_ref()); - let mut style = MarkdownHighlightStyle::default(); - - if bold_depth > 0 { - style.weight = FontWeight::BOLD; - } - - if italic_depth > 0 { - style.italic = true; - } - - if strikethrough_depth > 0 { - style.strikethrough = true; - } - - let last_run_len = if let Some(link) = link.clone() { - regions.push(( - prev_len..text.len(), - ParsedRegion { - code: false, - link: Some(link), - }, - )); - style.link = true; - prev_len - } else { - // Manually scan for links - let mut finder = linkify::LinkFinder::new(); - finder.kinds(&[linkify::LinkKind::Url]); - let mut last_link_len = prev_len; - for link in finder.links(t) { - let start = prev_len + link.start(); - let end = prev_len + link.end(); - let range = start..end; - link_ranges.push(range.clone()); - link_urls.push(link.as_str().to_string()); - - // If there is a style before we match a link, we have to add this to the highlighted ranges - if style != MarkdownHighlightStyle::default() && last_link_len < start { - highlights.push(( - last_link_len..start, - MarkdownHighlight::Style(style.clone()), - )); - } - - highlights.push(( - range.clone(), - MarkdownHighlight::Style(MarkdownHighlightStyle { - underline: true, - ..style - }), - )); - - regions.push(( - range.clone(), - ParsedRegion { - code: false, - link: Some(Link::Web { - url: link.as_str().to_string(), - }), - }, - )); - last_link_len = end; - } - last_link_len - }; - - if style != MarkdownHighlightStyle::default() && last_run_len < text.len() { - let mut new_highlight = true; - if let Some((last_range, last_style)) = highlights.last_mut() - && last_range.end == last_run_len - && last_style == &MarkdownHighlight::Style(style.clone()) - { - last_range.end = text.len(); - new_highlight = false; - } - if new_highlight { - highlights.push(( - last_run_len..text.len(), - MarkdownHighlight::Style(style.clone()), - )); - } - } - } - Event::Code(t) => { - text.push_str(t.as_ref()); - let range = prev_len..text.len(); - - if link.is_some() { - highlights.push(( - range.clone(), - MarkdownHighlight::Style(MarkdownHighlightStyle { - link: true, - ..Default::default() - }), - )); - } - regions.push(( - range, - ParsedRegion { - code: true, - link: link.clone(), - }, - )); - } - Event::Start(tag) => match tag { - Tag::Emphasis => italic_depth += 1, - Tag::Strong => bold_depth += 1, - Tag::Strikethrough => strikethrough_depth += 1, - Tag::Link { dest_url, .. } => { - link = Link::identify( - self.file_location_directory.clone(), - dest_url.to_string(), - ); - } - Tag::Image { dest_url, .. } => { - if !text.is_empty() { - let parsed_regions = MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range: source_range.clone(), - contents: mem::take(&mut text).into(), - highlights: mem::take(&mut highlights), - regions: mem::take(&mut regions), - }); - markdown_text_like.push(parsed_regions); - } - image = Image::identify( - dest_url.to_string(), - source_range.clone(), - self.file_location_directory.clone(), - ); - } - _ => { - break; - } - }, - - Event::End(tag) => match tag { - TagEnd::Emphasis => italic_depth -= 1, - TagEnd::Strong => bold_depth -= 1, - TagEnd::Strikethrough => strikethrough_depth -= 1, - TagEnd::Link => { - link = None; - } - TagEnd::Image => { - if let Some(mut image) = image.take() { - if !text.is_empty() { - image.set_alt_text(std::mem::take(&mut text).into()); - mem::take(&mut highlights); - mem::take(&mut regions); - } - markdown_text_like.push(MarkdownParagraphChunk::Image(image)); - } - } - TagEnd::Paragraph => { - self.cursor += 1; - break; - } - _ => { - break; - } - }, - _ => { - break; - } - } - - self.cursor += 1; - } - if !text.is_empty() { - markdown_text_like.push(MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range, - contents: text.into(), - highlights, - regions, - })); - } - markdown_text_like - } - - fn parse_heading(&mut self, level: pulldown_cmark::HeadingLevel) -> ParsedMarkdownHeading { - let (_event, source_range) = self.previous().unwrap(); - let source_range = source_range.clone(); - let text = self.parse_text(true, None); - - // Advance past the heading end tag - self.cursor += 1; - - ParsedMarkdownHeading { - source_range, - level: match level { - pulldown_cmark::HeadingLevel::H1 => HeadingLevel::H1, - pulldown_cmark::HeadingLevel::H2 => HeadingLevel::H2, - pulldown_cmark::HeadingLevel::H3 => HeadingLevel::H3, - pulldown_cmark::HeadingLevel::H4 => HeadingLevel::H4, - pulldown_cmark::HeadingLevel::H5 => HeadingLevel::H5, - pulldown_cmark::HeadingLevel::H6 => HeadingLevel::H6, - }, - contents: text, - } - } - - fn parse_table(&mut self, alignment: Vec) -> ParsedMarkdownTable { - let (_event, source_range) = self.previous().unwrap(); - let source_range = source_range.clone(); - let mut header = vec![]; - let mut body = vec![]; - let mut row_columns = vec![]; - let mut in_header = true; - let column_alignments = alignment - .iter() - .map(Self::convert_alignment) - .collect::>(); - - loop { - if self.eof() { - break; - } - - let (current, source_range) = self.current().unwrap(); - let source_range = source_range.clone(); - match current { - Event::Start(Tag::TableHead) - | Event::Start(Tag::TableRow) - | Event::End(TagEnd::TableCell) => { - self.cursor += 1; - } - Event::Start(Tag::TableCell) => { - self.cursor += 1; - let cell_contents = self.parse_text(false, Some(source_range)); - row_columns.push(ParsedMarkdownTableColumn { - col_span: 1, - row_span: 1, - is_header: in_header, - children: cell_contents, - alignment: column_alignments - .get(row_columns.len()) - .copied() - .unwrap_or_default(), - }); - } - Event::End(TagEnd::TableHead) | Event::End(TagEnd::TableRow) => { - self.cursor += 1; - let columns = std::mem::take(&mut row_columns); - if in_header { - header.push(ParsedMarkdownTableRow { columns: columns }); - in_header = false; - } else { - body.push(ParsedMarkdownTableRow::with_columns(columns)); - } - } - Event::End(TagEnd::Table) => { - self.cursor += 1; - break; - } - _ => { - break; - } - } - } - - ParsedMarkdownTable { - source_range, - header, - body, - caption: None, - } - } - - fn convert_alignment(alignment: &Alignment) -> ParsedMarkdownTableAlignment { - match alignment { - Alignment::None => ParsedMarkdownTableAlignment::None, - Alignment::Left => ParsedMarkdownTableAlignment::Left, - Alignment::Center => ParsedMarkdownTableAlignment::Center, - Alignment::Right => ParsedMarkdownTableAlignment::Right, - } - } - - async fn parse_list(&mut self, order: Option) -> Vec { - let (_, list_source_range) = self.previous().unwrap(); - - let mut items = Vec::new(); - let mut items_stack = vec![MarkdownListItem::default()]; - let mut depth = 1; - let mut order = order; - let mut order_stack = Vec::new(); - - let mut insertion_indices = FxHashMap::default(); - let mut source_ranges = FxHashMap::default(); - let mut start_item_range = list_source_range.clone(); - - while !self.eof() { - let (current, source_range) = self.current().unwrap(); - match current { - Event::Start(Tag::List(new_order)) => { - if items_stack.last().is_some() && !insertion_indices.contains_key(&depth) { - insertion_indices.insert(depth, items.len()); - } - - // We will use the start of the nested list as the end for the current item's range, - // because we don't care about the hierarchy of list items - if let collections::hash_map::Entry::Vacant(e) = source_ranges.entry(depth) { - e.insert(start_item_range.start..source_range.start); - } - - order_stack.push(order); - order = *new_order; - self.cursor += 1; - depth += 1; - } - Event::End(TagEnd::List(_)) => { - order = order_stack.pop().flatten(); - self.cursor += 1; - depth -= 1; - - if depth == 0 { - break; - } - } - Event::Start(Tag::Item) => { - start_item_range = source_range.clone(); - - self.cursor += 1; - items_stack.push(MarkdownListItem::default()); - - let mut task_list = None; - // Check for task list marker (`- [ ]` or `- [x]`) - if let Some(event) = self.current_event() { - // If there is a linebreak in between two list items the task list marker will actually be the first element of the paragraph - if event == &Event::Start(Tag::Paragraph) { - self.cursor += 1; - } - - if let Some((Event::TaskListMarker(checked), range)) = self.current() { - task_list = Some((*checked, range.clone())); - self.cursor += 1; - } - } - - if let Some((event, range)) = self.current() { - // This is a plain list item. - // For example `- some text` or `1. [Docs](./docs.md)` - if MarkdownParser::is_text_like(event) { - let text = self.parse_text(false, Some(range.clone())); - let block = ParsedMarkdownElement::Paragraph(text); - if let Some(content) = items_stack.last_mut() { - let item_type = if let Some((checked, range)) = task_list { - ParsedMarkdownListItemType::Task(checked, range) - } else if let Some(order) = order { - ParsedMarkdownListItemType::Ordered(order) - } else { - ParsedMarkdownListItemType::Unordered - }; - content.item_type = item_type; - content.content.push(block); - } - } else { - let block = self.parse_block().await; - if let Some(block) = block - && let Some(list_item) = items_stack.last_mut() - { - list_item.content.extend(block); - } - } - } - - // If there is a linebreak in between two list items the task list marker will actually be the first element of the paragraph - if self.current_event() == Some(&Event::End(TagEnd::Paragraph)) { - self.cursor += 1; - } - } - Event::End(TagEnd::Item) => { - self.cursor += 1; - - if let Some(current) = order { - order = Some(current + 1); - } - - if let Some(list_item) = items_stack.pop() { - let source_range = source_ranges - .remove(&depth) - .unwrap_or(start_item_range.clone()); - - // We need to remove the last character of the source range, because it includes the newline character - let source_range = source_range.start..source_range.end - 1; - let item = ParsedMarkdownElement::ListItem(ParsedMarkdownListItem { - source_range, - content: list_item.content, - depth, - item_type: list_item.item_type, - nested: false, - }); - - if let Some(index) = insertion_indices.get(&depth) { - items.insert(*index, item); - insertion_indices.remove(&depth); - } else { - items.push(item); - } - } - } - _ => { - if depth == 0 { - break; - } - // This can only happen if a list item starts with more then one paragraph, - // or the list item contains blocks that should be rendered after the nested list items - let block = self.parse_block().await; - if let Some(block) = block { - if let Some(list_item) = items_stack.last_mut() { - // If we did not insert any nested items yet (in this case insertion index is set), we can append the block to the current list item - if !insertion_indices.contains_key(&depth) { - list_item.content.extend(block); - continue; - } - } - - // Otherwise we need to insert the block after all the nested items - // that have been parsed so far - items.extend(block); - } else { - self.cursor += 1; - } - } - } - } - - items - } - - #[async_recursion] - async fn parse_block_quote(&mut self) -> ParsedMarkdownBlockQuote { - let (_event, source_range) = self.previous().unwrap(); - let source_range = source_range.clone(); - let mut nested_depth = 1; - - let mut children: Vec = vec![]; - - while !self.eof() { - let block = self.parse_block().await; - - if let Some(block) = block { - children.extend(block); - } else { - break; - } - - if self.eof() { - break; - } - - let (current, _source_range) = self.current().unwrap(); - match current { - // This is a nested block quote. - // Record that we're in a nested block quote and continue parsing. - // We don't need to advance the cursor since the next - // call to `parse_block` will handle it. - Event::Start(Tag::BlockQuote(_kind)) => { - nested_depth += 1; - } - Event::End(TagEnd::BlockQuote(_kind)) => { - nested_depth -= 1; - if nested_depth == 0 { - self.cursor += 1; - break; - } - } - _ => {} - }; - } - - ParsedMarkdownBlockQuote { - source_range, - children, - } - } - - async fn parse_code_block( - &mut self, - language: Option, - ) -> Option { - let Some((_event, source_range)) = self.previous() else { - return None; - }; - - let source_range = source_range.clone(); - let mut code = String::new(); - - while !self.eof() { - let Some((current, _source_range)) = self.current() else { - break; - }; - - match current { - Event::Text(text) => { - code.push_str(text); - self.cursor += 1; - } - Event::End(TagEnd::CodeBlock) => { - self.cursor += 1; - break; - } - _ => { - break; - } - } - } - - code = code.strip_suffix('\n').unwrap_or(&code).to_string(); - - let highlights = if let Some(language) = &language { - if let Some(registry) = &self.language_registry { - let rope: language::Rope = code.as_str().into(); - registry - .language_for_name_or_extension(language) - .await - .map(|l| l.highlight_text(&rope, 0..code.len())) - .ok() - } else { - None - } - } else { - None - }; - - Some(ParsedMarkdownCodeBlock { - source_range, - contents: code.into(), - language, - highlights, - }) - } - - async fn parse_html_block(&mut self) -> Vec { - let mut elements = Vec::new(); - let Some((_event, _source_range)) = self.previous() else { - return elements; - }; - - let mut html_source_range_start = None; - let mut html_source_range_end = None; - let mut html_buffer = String::new(); - - while !self.eof() { - let Some((current, source_range)) = self.current() else { - break; - }; - let source_range = source_range.clone(); - match current { - Event::Html(html) => { - html_source_range_start.get_or_insert(source_range.start); - html_source_range_end = Some(source_range.end); - html_buffer.push_str(html); - self.cursor += 1; - } - Event::End(TagEnd::CodeBlock) => { - self.cursor += 1; - break; - } - _ => { - break; - } - } - } - - let bytes = cleanup_html(&html_buffer); - - let mut cursor = std::io::Cursor::new(bytes); - if let Ok(dom) = parse_document(RcDom::default(), ParseOpts::default()) - .from_utf8() - .read_from(&mut cursor) - && let Some((start, end)) = html_source_range_start.zip(html_source_range_end) - { - self.parse_html_node( - start..end, - &dom.document, - &mut elements, - &ParseHtmlNodeContext::default(), - ); - } - - elements - } - - fn parse_html_node( - &self, - source_range: Range, - node: &Rc, - elements: &mut Vec, - context: &ParseHtmlNodeContext, - ) { - match &node.data { - markup5ever_rcdom::NodeData::Document => { - self.consume_children(source_range, node, elements, context); - } - markup5ever_rcdom::NodeData::Text { contents } => { - elements.push(ParsedMarkdownElement::Paragraph(vec![ - MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range, - regions: Vec::default(), - highlights: Vec::default(), - contents: contents.borrow().to_string().into(), - }), - ])); - } - markup5ever_rcdom::NodeData::Comment { .. } => {} - markup5ever_rcdom::NodeData::Element { name, attrs, .. } => { - let mut styles = if let Some(styles) = Self::markdown_style_from_html_styles( - Self::extract_styles_from_attributes(attrs), - ) { - vec![MarkdownHighlight::Style(styles)] - } else { - Vec::default() - }; - - if local_name!("img") == name.local { - if let Some(image) = self.extract_image(source_range, attrs) { - elements.push(ParsedMarkdownElement::Image(image)); - } - } else if local_name!("p") == name.local { - let mut paragraph = MarkdownParagraph::new(); - self.parse_paragraph( - source_range, - node, - &mut paragraph, - &mut styles, - &mut Vec::new(), - ); - - if !paragraph.is_empty() { - elements.push(ParsedMarkdownElement::Paragraph(paragraph)); - } - } else if matches!( - name.local, - local_name!("h1") - | local_name!("h2") - | local_name!("h3") - | local_name!("h4") - | local_name!("h5") - | local_name!("h6") - ) { - let mut paragraph = MarkdownParagraph::new(); - self.consume_paragraph( - source_range.clone(), - node, - &mut paragraph, - &mut styles, - &mut Vec::new(), - ); - - if !paragraph.is_empty() { - elements.push(ParsedMarkdownElement::Heading(ParsedMarkdownHeading { - source_range, - level: match name.local { - local_name!("h1") => HeadingLevel::H1, - local_name!("h2") => HeadingLevel::H2, - local_name!("h3") => HeadingLevel::H3, - local_name!("h4") => HeadingLevel::H4, - local_name!("h5") => HeadingLevel::H5, - local_name!("h6") => HeadingLevel::H6, - _ => unreachable!(), - }, - contents: paragraph, - })); - } - } else if local_name!("ul") == name.local || local_name!("ol") == name.local { - if let Some(list_items) = self.extract_html_list( - node, - local_name!("ol") == name.local, - context.list_item_depth, - source_range, - ) { - elements.extend(list_items); - } - } else if local_name!("blockquote") == name.local { - if let Some(blockquote) = self.extract_html_blockquote(node, source_range) { - elements.push(ParsedMarkdownElement::BlockQuote(blockquote)); - } - } else if local_name!("table") == name.local { - if let Some(table) = self.extract_html_table(node, source_range) { - elements.push(ParsedMarkdownElement::Table(table)); - } - } else { - self.consume_children(source_range, node, elements, context); - } - } - _ => {} - } - } - - fn parse_paragraph( - &self, - source_range: Range, - node: &Rc, - paragraph: &mut MarkdownParagraph, - highlights: &mut Vec, - regions: &mut Vec<(Range, ParsedRegion)>, - ) { - fn items_with_range( - range: Range, - items: impl IntoIterator, - ) -> Vec<(Range, T)> { - items - .into_iter() - .map(|item| (range.clone(), item)) - .collect() - } - - match &node.data { - markup5ever_rcdom::NodeData::Text { contents } => { - // append the text to the last chunk, so we can have a hacky version - // of inline text with highlighting - if let Some(text) = paragraph.iter_mut().last().and_then(|p| match p { - MarkdownParagraphChunk::Text(text) => Some(text), - _ => None, - }) { - let mut new_text = text.contents.to_string(); - new_text.push_str(&contents.borrow()); - - text.highlights.extend(items_with_range( - text.contents.len()..new_text.len(), - std::mem::take(highlights), - )); - text.regions.extend(items_with_range( - text.contents.len()..new_text.len(), - std::mem::take(regions) - .into_iter() - .map(|(_, region)| region), - )); - text.contents = SharedString::from(new_text); - } else { - let contents = contents.borrow().to_string(); - paragraph.push(MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range, - highlights: items_with_range(0..contents.len(), std::mem::take(highlights)), - regions: items_with_range( - 0..contents.len(), - std::mem::take(regions) - .into_iter() - .map(|(_, region)| region), - ), - contents: contents.into(), - })); - } - } - markup5ever_rcdom::NodeData::Element { name, attrs, .. } => { - if local_name!("img") == name.local { - if let Some(image) = self.extract_image(source_range, attrs) { - paragraph.push(MarkdownParagraphChunk::Image(image)); - } - } else if local_name!("b") == name.local || local_name!("strong") == name.local { - highlights.push(MarkdownHighlight::Style(MarkdownHighlightStyle { - weight: FontWeight::BOLD, - ..Default::default() - })); - - self.consume_paragraph(source_range, node, paragraph, highlights, regions); - } else if local_name!("i") == name.local { - highlights.push(MarkdownHighlight::Style(MarkdownHighlightStyle { - italic: true, - ..Default::default() - })); - - self.consume_paragraph(source_range, node, paragraph, highlights, regions); - } else if local_name!("em") == name.local { - highlights.push(MarkdownHighlight::Style(MarkdownHighlightStyle { - oblique: true, - ..Default::default() - })); - - self.consume_paragraph(source_range, node, paragraph, highlights, regions); - } else if local_name!("del") == name.local { - highlights.push(MarkdownHighlight::Style(MarkdownHighlightStyle { - strikethrough: true, - ..Default::default() - })); - - self.consume_paragraph(source_range, node, paragraph, highlights, regions); - } else if local_name!("ins") == name.local { - highlights.push(MarkdownHighlight::Style(MarkdownHighlightStyle { - underline: true, - ..Default::default() - })); - - self.consume_paragraph(source_range, node, paragraph, highlights, regions); - } else if local_name!("a") == name.local { - if let Some(url) = Self::attr_value(attrs, local_name!("href")) - && let Some(link) = - Link::identify(self.file_location_directory.clone(), url) - { - highlights.push(MarkdownHighlight::Style(MarkdownHighlightStyle { - link: true, - ..Default::default() - })); - - regions.push(( - source_range.clone(), - ParsedRegion { - code: false, - link: Some(link), - }, - )); - } - - self.consume_paragraph(source_range, node, paragraph, highlights, regions); - } else { - self.consume_paragraph(source_range, node, paragraph, highlights, regions); - } - } - _ => {} - } - } - - fn consume_paragraph( - &self, - source_range: Range, - node: &Rc, - paragraph: &mut MarkdownParagraph, - highlights: &mut Vec, - regions: &mut Vec<(Range, ParsedRegion)>, - ) { - for node in node.children.borrow().iter() { - self.parse_paragraph(source_range.clone(), node, paragraph, highlights, regions); - } - } - - fn parse_table_row( - &self, - source_range: Range, - node: &Rc, - ) -> Option { - let mut columns = Vec::new(); - - match &node.data { - markup5ever_rcdom::NodeData::Element { name, .. } => { - if local_name!("tr") != name.local { - return None; - } - - for node in node.children.borrow().iter() { - if let Some(column) = self.parse_table_column(source_range.clone(), node) { - columns.push(column); - } - } - } - _ => {} - } - - if columns.is_empty() { - None - } else { - Some(ParsedMarkdownTableRow { columns }) - } - } - - fn parse_table_column( - &self, - source_range: Range, - node: &Rc, - ) -> Option { - match &node.data { - markup5ever_rcdom::NodeData::Element { name, attrs, .. } => { - if !matches!(name.local, local_name!("th") | local_name!("td")) { - return None; - } - - let mut children = MarkdownParagraph::new(); - self.consume_paragraph( - source_range, - node, - &mut children, - &mut Vec::new(), - &mut Vec::new(), - ); - - let is_header = matches!(name.local, local_name!("th")); - - Some(ParsedMarkdownTableColumn { - col_span: std::cmp::max( - Self::attr_value(attrs, local_name!("colspan")) - .and_then(|span| span.parse().ok()) - .unwrap_or(1), - 1, - ), - row_span: std::cmp::max( - Self::attr_value(attrs, local_name!("rowspan")) - .and_then(|span| span.parse().ok()) - .unwrap_or(1), - 1, - ), - is_header, - children, - alignment: Self::attr_value(attrs, local_name!("align")) - .and_then(|align| match align.as_str() { - "left" => Some(ParsedMarkdownTableAlignment::Left), - "center" => Some(ParsedMarkdownTableAlignment::Center), - "right" => Some(ParsedMarkdownTableAlignment::Right), - _ => None, - }) - .unwrap_or_else(|| { - if is_header { - ParsedMarkdownTableAlignment::Center - } else { - ParsedMarkdownTableAlignment::default() - } - }), - }) - } - _ => None, - } - } - - fn consume_children( - &self, - source_range: Range, - node: &Rc, - elements: &mut Vec, - context: &ParseHtmlNodeContext, - ) { - for node in node.children.borrow().iter() { - self.parse_html_node(source_range.clone(), node, elements, context); - } - } - - fn attr_value( - attrs: &RefCell>, - name: html5ever::LocalName, - ) -> Option { - attrs.borrow().iter().find_map(|attr| { - if attr.name.local == name { - Some(attr.value.to_string()) - } else { - None - } - }) - } - - fn markdown_style_from_html_styles( - styles: HashMap, - ) -> Option { - let mut markdown_style = MarkdownHighlightStyle::default(); - - if let Some(text_decoration) = styles.get("text-decoration") { - match text_decoration.to_lowercase().as_str() { - "underline" => { - markdown_style.underline = true; - } - "line-through" => { - markdown_style.strikethrough = true; - } - _ => {} - } - } - - if let Some(font_style) = styles.get("font-style") { - match font_style.to_lowercase().as_str() { - "italic" => { - markdown_style.italic = true; - } - "oblique" => { - markdown_style.oblique = true; - } - _ => {} - } - } - - if let Some(font_weight) = styles.get("font-weight") { - match font_weight.to_lowercase().as_str() { - "bold" => { - markdown_style.weight = FontWeight::BOLD; - } - "lighter" => { - markdown_style.weight = FontWeight::THIN; - } - _ => { - if let Some(weight) = font_weight.parse::().ok() { - markdown_style.weight = FontWeight(weight); - } - } - } - } - - if markdown_style != MarkdownHighlightStyle::default() { - Some(markdown_style) - } else { - None - } - } - - fn extract_styles_from_attributes( - attrs: &RefCell>, - ) -> HashMap { - let mut styles = HashMap::new(); - - if let Some(style) = Self::attr_value(attrs, local_name!("style")) { - for decl in style.split(';') { - let mut parts = decl.splitn(2, ':'); - if let Some((key, value)) = parts.next().zip(parts.next()) { - styles.insert( - key.trim().to_lowercase().to_string(), - value.trim().to_string(), - ); - } - } - } - - styles - } - - fn extract_image( - &self, - source_range: Range, - attrs: &RefCell>, - ) -> Option { - let src = Self::attr_value(attrs, local_name!("src"))?; - - let mut image = Image::identify(src, source_range, self.file_location_directory.clone())?; - - if let Some(alt) = Self::attr_value(attrs, local_name!("alt")) { - image.set_alt_text(alt.into()); - } - - let styles = Self::extract_styles_from_attributes(attrs); - - if let Some(width) = Self::attr_value(attrs, local_name!("width")) - .or_else(|| styles.get("width").cloned()) - .and_then(|width| Self::parse_html_element_dimension(&width)) - { - image.set_width(width); - } - - if let Some(height) = Self::attr_value(attrs, local_name!("height")) - .or_else(|| styles.get("height").cloned()) - .and_then(|height| Self::parse_html_element_dimension(&height)) - { - image.set_height(height); - } - - Some(image) - } - - fn extract_html_list( - &self, - node: &Rc, - ordered: bool, - depth: u16, - source_range: Range, - ) -> Option> { - let mut list_items = Vec::with_capacity(node.children.borrow().len()); - - for (index, node) in node.children.borrow().iter().enumerate() { - match &node.data { - markup5ever_rcdom::NodeData::Element { name, .. } => { - if local_name!("li") != name.local { - continue; - } - - let mut content = Vec::new(); - self.consume_children( - source_range.clone(), - node, - &mut content, - &ParseHtmlNodeContext { - list_item_depth: depth + 1, - }, - ); - - if !content.is_empty() { - list_items.push(ParsedMarkdownElement::ListItem(ParsedMarkdownListItem { - depth, - source_range: source_range.clone(), - item_type: if ordered { - ParsedMarkdownListItemType::Ordered(index as u64 + 1) - } else { - ParsedMarkdownListItemType::Unordered - }, - content, - nested: true, - })); - } - } - _ => {} - } - } - - if list_items.is_empty() { - None - } else { - Some(list_items) - } - } - - fn parse_html_element_dimension(value: &str) -> Option { - if value.ends_with("%") { - value - .trim_end_matches("%") - .parse::() - .ok() - .map(|value| relative(value / 100.)) - } else { - value - .trim_end_matches("px") - .parse() - .ok() - .map(|value| px(value).into()) - } - } - - fn extract_html_blockquote( - &self, - node: &Rc, - source_range: Range, - ) -> Option { - let mut children = Vec::new(); - self.consume_children( - source_range.clone(), - node, - &mut children, - &ParseHtmlNodeContext::default(), - ); - - if children.is_empty() { - None - } else { - Some(ParsedMarkdownBlockQuote { - children, - source_range, - }) - } - } - - fn extract_html_table( - &self, - node: &Rc, - source_range: Range, - ) -> Option { - let mut header_rows = Vec::new(); - let mut body_rows = Vec::new(); - let mut caption = None; - - // node should be a thead, tbody or caption element - for node in node.children.borrow().iter() { - match &node.data { - markup5ever_rcdom::NodeData::Element { name, .. } => { - if local_name!("caption") == name.local { - let mut paragraph = MarkdownParagraph::new(); - self.parse_paragraph( - source_range.clone(), - node, - &mut paragraph, - &mut Vec::new(), - &mut Vec::new(), - ); - caption = Some(paragraph); - } - if local_name!("thead") == name.local { - // node should be a tr element - for node in node.children.borrow().iter() { - if let Some(row) = self.parse_table_row(source_range.clone(), node) { - header_rows.push(row); - } - } - } else if local_name!("tbody") == name.local { - // node should be a tr element - for node in node.children.borrow().iter() { - if let Some(row) = self.parse_table_row(source_range.clone(), node) { - body_rows.push(row); - } - } - } - } - _ => {} - } - } - - if !header_rows.is_empty() || !body_rows.is_empty() { - Some(ParsedMarkdownTable { - source_range, - body: body_rows, - header: header_rows, - caption, - }) - } else { - None - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use ParsedMarkdownListItemType::*; - use core::panic; - use gpui::{AbsoluteLength, BackgroundExecutor, DefiniteLength}; - use language::{HighlightId, LanguageRegistry}; - use pretty_assertions::assert_eq; - - async fn parse(input: &str) -> ParsedMarkdown { - parse_markdown(input, None, None).await - } - - #[gpui::test] - async fn test_headings() { - let parsed = parse("# Heading one\n## Heading two\n### Heading three").await; - - assert_eq!( - parsed.children, - vec![ - h1(text("Heading one", 2..13), 0..14), - h2(text("Heading two", 17..28), 14..29), - h3(text("Heading three", 33..46), 29..46), - ] - ); - } - - #[gpui::test] - async fn test_newlines_dont_new_paragraphs() { - let parsed = parse("Some text **that is bolded**\n and *italicized*").await; - - assert_eq!( - parsed.children, - vec![p("Some text that is bolded and italicized", 0..46)] - ); - } - - #[gpui::test] - async fn test_heading_with_paragraph() { - let parsed = parse("# Zed\nThe editor").await; - - assert_eq!( - parsed.children, - vec![h1(text("Zed", 2..5), 0..6), p("The editor", 6..16),] - ); - } - - #[gpui::test] - async fn test_double_newlines_do_new_paragraphs() { - let parsed = parse("Some text **that is bolded**\n\n and *italicized*").await; - - assert_eq!( - parsed.children, - vec![ - p("Some text that is bolded", 0..29), - p("and italicized", 31..47), - ] - ); - } - - #[gpui::test] - async fn test_bold_italic_text() { - let parsed = parse("Some text **that is bolded** and *italicized*").await; - - assert_eq!( - parsed.children, - vec![p("Some text that is bolded and italicized", 0..45)] - ); - } - - #[gpui::test] - async fn test_nested_bold_strikethrough_text() { - let parsed = parse("Some **bo~~strikethrough~~ld** text").await; - - assert_eq!(parsed.children.len(), 1); - assert_eq!( - parsed.children[0], - ParsedMarkdownElement::Paragraph(vec![MarkdownParagraphChunk::Text( - ParsedMarkdownText { - source_range: 0..35, - contents: "Some bostrikethroughld text".into(), - highlights: Vec::new(), - regions: Vec::new(), - } - )]) - ); - - let new_text = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] { - text - } else { - panic!("Expected a paragraph"); - }; - - let paragraph = if let MarkdownParagraphChunk::Text(text) = &new_text[0] { - text - } else { - panic!("Expected a text"); - }; - - assert_eq!( - paragraph.highlights, - vec![ - ( - 5..7, - MarkdownHighlight::Style(MarkdownHighlightStyle { - weight: FontWeight::BOLD, - ..Default::default() - }), - ), - ( - 7..20, - MarkdownHighlight::Style(MarkdownHighlightStyle { - weight: FontWeight::BOLD, - strikethrough: true, - ..Default::default() - }), - ), - ( - 20..22, - MarkdownHighlight::Style(MarkdownHighlightStyle { - weight: FontWeight::BOLD, - ..Default::default() - }), - ), - ] - ); - } - - #[gpui::test] - async fn test_html_inline_style_elements() { - let parsed = - parse("

Some text strong text more text bold text more text italic text more text emphasized text more text deleted text more text inserted text

").await; - - assert_eq!(1, parsed.children.len()); - let chunks = if let ParsedMarkdownElement::Paragraph(chunks) = &parsed.children[0] { - chunks - } else { - panic!("Expected a paragraph"); - }; - - assert_eq!(1, chunks.len()); - let text = if let MarkdownParagraphChunk::Text(text) = &chunks[0] { - text - } else { - panic!("Expected a paragraph"); - }; - - assert_eq!(0..205, text.source_range); - assert_eq!( - "Some text strong text more text bold text more text italic text more text emphasized text more text deleted text more text inserted text", - text.contents.as_str(), - ); - assert_eq!( - vec![ - ( - 10..21, - MarkdownHighlight::Style(MarkdownHighlightStyle { - weight: FontWeight(700.0), - ..Default::default() - },), - ), - ( - 32..41, - MarkdownHighlight::Style(MarkdownHighlightStyle { - weight: FontWeight(700.0), - ..Default::default() - },), - ), - ( - 52..63, - MarkdownHighlight::Style(MarkdownHighlightStyle { - italic: true, - weight: FontWeight(400.0), - ..Default::default() - },), - ), - ( - 74..89, - MarkdownHighlight::Style(MarkdownHighlightStyle { - weight: FontWeight(400.0), - oblique: true, - ..Default::default() - },), - ), - ( - 100..112, - MarkdownHighlight::Style(MarkdownHighlightStyle { - strikethrough: true, - weight: FontWeight(400.0), - ..Default::default() - },), - ), - ( - 123..136, - MarkdownHighlight::Style(MarkdownHighlightStyle { - underline: true, - weight: FontWeight(400.0,), - ..Default::default() - },), - ), - ], - text.highlights - ); - } - - #[gpui::test] - async fn test_html_href_element() { - let parsed = - parse("
").await; - - assert_eq!(1, parsed.children.len()); - let chunks = if let ParsedMarkdownElement::Paragraph(chunks) = &parsed.children[0] { - chunks - } else { - panic!("Expected a paragraph"); - }; - - assert_eq!(1, chunks.len()); - let text = if let MarkdownParagraphChunk::Text(text) = &chunks[0] { - text - } else { - panic!("Expected a paragraph"); - }; - - assert_eq!(0..65, text.source_range); - assert_eq!("Some text link more text", text.contents.as_str(),); - assert_eq!( - vec![( - 10..14, - MarkdownHighlight::Style(MarkdownHighlightStyle { - link: true, - ..Default::default() - },), - )], - text.highlights - ); - assert_eq!( - vec![( - 10..14, - ParsedRegion { - code: false, - link: Some(Link::Web { - url: "https://example.com".into() - }) - } - )], - text.regions - ) - } - - #[gpui::test] - async fn test_text_with_inline_html() { - let parsed = parse("This is a paragraph with an inline HTML tag.").await; - - assert_eq!( - parsed.children, - vec![p("This is a paragraph with an inline HTML tag.", 0..63),], - ); - } - - #[gpui::test] - async fn test_raw_links_detection() { - let parsed = parse("Checkout this https://zed.dev link").await; - - assert_eq!( - parsed.children, - vec![p("Checkout this https://zed.dev link", 0..34)] - ); - } - - #[gpui::test] - async fn test_empty_image() { - let parsed = parse("![]()").await; - - let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] { - text - } else { - panic!("Expected a paragraph"); - }; - assert_eq!(paragraph.len(), 0); - } - - #[gpui::test] - async fn test_image_links_detection() { - let parsed = parse("![test](https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png)").await; - - let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] { - text - } else { - panic!("Expected a paragraph"); - }; - assert_eq!( - paragraph[0], - MarkdownParagraphChunk::Image(Image { - source_range: 0..111, - link: Link::Web { - url: "https://blog.logrocket.com/wp-content/uploads/2024/04/exploring-zed-open-source-code-editor-rust-2.png".to_string(), - }, - alt_text: Some("test".into()), - height: None, - width: None, - },) - ); - } - - #[gpui::test] - async fn test_image_alt_text() { - let parsed = parse("[![Zed](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/zed-industries/zed/main/assets/badge/v0.json)](https://zed.dev)\n ").await; - - let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] { - text - } else { - panic!("Expected a paragraph"); - }; - assert_eq!( - paragraph[0], - MarkdownParagraphChunk::Image(Image { - source_range: 0..142, - link: Link::Web { - url: "https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/zed-industries/zed/main/assets/badge/v0.json".to_string(), - }, - alt_text: Some("Zed".into()), - height: None, - width: None, - },) - ); - } - - #[gpui::test] - async fn test_image_without_alt_text() { - let parsed = parse("![](http://example.com/foo.png)").await; - - let paragraph = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] { - text - } else { - panic!("Expected a paragraph"); - }; - assert_eq!( - paragraph[0], - MarkdownParagraphChunk::Image(Image { - source_range: 0..31, - link: Link::Web { - url: "http://example.com/foo.png".to_string(), - }, - alt_text: None, - height: None, - width: None, - },) - ); - } - - #[gpui::test] - async fn test_image_with_alt_text_containing_formatting() { - let parsed = parse("![foo *bar* baz](http://example.com/foo.png)").await; - - let ParsedMarkdownElement::Paragraph(chunks) = &parsed.children[0] else { - panic!("Expected a paragraph"); - }; - assert_eq!( - chunks, - &[MarkdownParagraphChunk::Image(Image { - source_range: 0..44, - link: Link::Web { - url: "http://example.com/foo.png".to_string(), - }, - alt_text: Some("foo bar baz".into()), - height: None, - width: None, - }),], - ); - } - - #[gpui::test] - async fn test_images_with_text_in_between() { - let parsed = parse( - "![foo](http://example.com/foo.png)\nLorem Ipsum\n![bar](http://example.com/bar.png)", - ) - .await; - - let chunks = if let ParsedMarkdownElement::Paragraph(text) = &parsed.children[0] { - text - } else { - panic!("Expected a paragraph"); - }; - assert_eq!( - chunks, - &vec![ - MarkdownParagraphChunk::Image(Image { - source_range: 0..81, - link: Link::Web { - url: "http://example.com/foo.png".to_string(), - }, - alt_text: Some("foo".into()), - height: None, - width: None, - }), - MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range: 0..81, - contents: " Lorem Ipsum ".into(), - highlights: Vec::new(), - regions: Vec::new(), - }), - MarkdownParagraphChunk::Image(Image { - source_range: 0..81, - link: Link::Web { - url: "http://example.com/bar.png".to_string(), - }, - alt_text: Some("bar".into()), - height: None, - width: None, - }) - ] - ); - } - - #[test] - fn test_parse_html_element_dimension() { - // Test percentage values - assert_eq!( - MarkdownParser::parse_html_element_dimension("50%"), - Some(DefiniteLength::Fraction(0.5)) - ); - assert_eq!( - MarkdownParser::parse_html_element_dimension("100%"), - Some(DefiniteLength::Fraction(1.0)) - ); - assert_eq!( - MarkdownParser::parse_html_element_dimension("25%"), - Some(DefiniteLength::Fraction(0.25)) - ); - assert_eq!( - MarkdownParser::parse_html_element_dimension("0%"), - Some(DefiniteLength::Fraction(0.0)) - ); - - // Test pixel values - assert_eq!( - MarkdownParser::parse_html_element_dimension("100px"), - Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.0)))) - ); - assert_eq!( - MarkdownParser::parse_html_element_dimension("50px"), - Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(50.0)))) - ); - assert_eq!( - MarkdownParser::parse_html_element_dimension("0px"), - Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(0.0)))) - ); - - // Test values without units (should be treated as pixels) - assert_eq!( - MarkdownParser::parse_html_element_dimension("100"), - Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.0)))) - ); - assert_eq!( - MarkdownParser::parse_html_element_dimension("42"), - Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(42.0)))) - ); - - // Test invalid values - assert_eq!( - MarkdownParser::parse_html_element_dimension("invalid"), - None - ); - assert_eq!(MarkdownParser::parse_html_element_dimension("px"), None); - assert_eq!(MarkdownParser::parse_html_element_dimension("%"), None); - assert_eq!(MarkdownParser::parse_html_element_dimension(""), None); - assert_eq!(MarkdownParser::parse_html_element_dimension("abc%"), None); - assert_eq!(MarkdownParser::parse_html_element_dimension("abcpx"), None); - - // Test decimal values - assert_eq!( - MarkdownParser::parse_html_element_dimension("50.5%"), - Some(DefiniteLength::Fraction(0.505)) - ); - assert_eq!( - MarkdownParser::parse_html_element_dimension("100.25px"), - Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.25)))) - ); - assert_eq!( - MarkdownParser::parse_html_element_dimension("42.0"), - Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(42.0)))) - ); - } - - #[gpui::test] - async fn test_html_unordered_list() { - let parsed = parse( - "
    -
  • Item 1
  • -
  • Item 2
  • -
", - ) - .await; - - assert_eq!( - ParsedMarkdown { - children: vec![ - nested_list_item( - 0..82, - 1, - ParsedMarkdownListItemType::Unordered, - vec![ParsedMarkdownElement::Paragraph(text("Item 1", 0..82))] - ), - nested_list_item( - 0..82, - 1, - ParsedMarkdownListItemType::Unordered, - vec![ParsedMarkdownElement::Paragraph(text("Item 2", 0..82))] - ), - ] - }, - parsed - ); - } - - #[gpui::test] - async fn test_html_ordered_list() { - let parsed = parse( - "
    -
  1. Item 1
  2. -
  3. Item 2
  4. -
", - ) - .await; - - assert_eq!( - ParsedMarkdown { - children: vec![ - nested_list_item( - 0..82, - 1, - ParsedMarkdownListItemType::Ordered(1), - vec![ParsedMarkdownElement::Paragraph(text("Item 1", 0..82))] - ), - nested_list_item( - 0..82, - 1, - ParsedMarkdownListItemType::Ordered(2), - vec![ParsedMarkdownElement::Paragraph(text("Item 2", 0..82))] - ), - ] - }, - parsed - ); - } - - #[gpui::test] - async fn test_html_nested_ordered_list() { - let parsed = parse( - "
    -
  1. Item 1
  2. -
  3. Item 2 -
      -
    1. Sub-Item 1
    2. -
    3. Sub-Item 2
    4. -
    -
  4. -
", - ) - .await; - - assert_eq!( - ParsedMarkdown { - children: vec![ - nested_list_item( - 0..216, - 1, - ParsedMarkdownListItemType::Ordered(1), - vec![ParsedMarkdownElement::Paragraph(text("Item 1", 0..216))] - ), - nested_list_item( - 0..216, - 1, - ParsedMarkdownListItemType::Ordered(2), - vec![ - ParsedMarkdownElement::Paragraph(text("Item 2", 0..216)), - nested_list_item( - 0..216, - 2, - ParsedMarkdownListItemType::Ordered(1), - vec![ParsedMarkdownElement::Paragraph(text("Sub-Item 1", 0..216))] - ), - nested_list_item( - 0..216, - 2, - ParsedMarkdownListItemType::Ordered(2), - vec![ParsedMarkdownElement::Paragraph(text("Sub-Item 2", 0..216))] - ), - ] - ), - ] - }, - parsed - ); - } - - #[gpui::test] - async fn test_html_nested_unordered_list() { - let parsed = parse( - "
    -
  • Item 1
  • -
  • Item 2 -
      -
    • Sub-Item 1
    • -
    • Sub-Item 2
    • -
    -
  • -
", - ) - .await; - - assert_eq!( - ParsedMarkdown { - children: vec![ - nested_list_item( - 0..216, - 1, - ParsedMarkdownListItemType::Unordered, - vec![ParsedMarkdownElement::Paragraph(text("Item 1", 0..216))] - ), - nested_list_item( - 0..216, - 1, - ParsedMarkdownListItemType::Unordered, - vec![ - ParsedMarkdownElement::Paragraph(text("Item 2", 0..216)), - nested_list_item( - 0..216, - 2, - ParsedMarkdownListItemType::Unordered, - vec![ParsedMarkdownElement::Paragraph(text("Sub-Item 1", 0..216))] - ), - nested_list_item( - 0..216, - 2, - ParsedMarkdownListItemType::Unordered, - vec![ParsedMarkdownElement::Paragraph(text("Sub-Item 2", 0..216))] - ), - ] - ), - ] - }, - parsed - ); - } - - #[gpui::test] - async fn test_inline_html_image_tag() { - let parsed = - parse("

Some text some more text

") - .await; - - assert_eq!( - ParsedMarkdown { - children: vec![ParsedMarkdownElement::Paragraph(vec![ - MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range: 0..71, - contents: "Some text".into(), - highlights: Default::default(), - regions: Default::default() - }), - MarkdownParagraphChunk::Image(Image { - source_range: 0..71, - link: Link::Web { - url: "http://example.com/foo.png".to_string(), - }, - alt_text: None, - height: None, - width: None, - }), - MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range: 0..71, - contents: " some more text".into(), - highlights: Default::default(), - regions: Default::default() - }), - ])] - }, - parsed - ); - } - - #[gpui::test] - async fn test_html_block_quote() { - let parsed = parse( - "
-

some description

-
", - ) - .await; - - assert_eq!( - ParsedMarkdown { - children: vec![block_quote( - vec![ParsedMarkdownElement::Paragraph(text( - "some description", - 0..78 - ))], - 0..78, - )] - }, - parsed - ); - } - - #[gpui::test] - async fn test_html_nested_block_quote() { - let parsed = parse( - "
-

some description

-
-

second description

-
-
", - ) - .await; - - assert_eq!( - ParsedMarkdown { - children: vec![block_quote( - vec![ - ParsedMarkdownElement::Paragraph(text("some description", 0..179)), - block_quote( - vec![ParsedMarkdownElement::Paragraph(text( - "second description", - 0..179 - ))], - 0..179, - ) - ], - 0..179, - )] - }, - parsed - ); - } - - #[gpui::test] - async fn test_html_table() { - let parsed = parse( - " - - - - - - - - - - - - - - - - -
IdName
1Chris
2Dennis
", - ) - .await; - - assert_eq!( - ParsedMarkdown { - children: vec![ParsedMarkdownElement::Table(table( - 0..366, - None, - vec![row(vec![ - column( - 1, - 1, - true, - text("Id", 0..366), - ParsedMarkdownTableAlignment::Center - ), - column( - 1, - 1, - true, - text("Name ", 0..366), - ParsedMarkdownTableAlignment::Center - ) - ])], - vec![ - row(vec![ - column( - 1, - 1, - false, - text("1", 0..366), - ParsedMarkdownTableAlignment::None - ), - column( - 1, - 1, - false, - text("Chris", 0..366), - ParsedMarkdownTableAlignment::None - ) - ]), - row(vec![ - column( - 1, - 1, - false, - text("2", 0..366), - ParsedMarkdownTableAlignment::None - ), - column( - 1, - 1, - false, - text("Dennis", 0..366), - ParsedMarkdownTableAlignment::None - ) - ]), - ], - ))], - }, - parsed - ); - } - - #[gpui::test] - async fn test_html_table_with_caption() { - let parsed = parse( - " - - - - - - - - - - - -
My Table
1Chris
2Dennis
", - ) - .await; - - assert_eq!( - ParsedMarkdown { - children: vec![ParsedMarkdownElement::Table(table( - 0..280, - Some(vec![MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range: 0..280, - contents: "My Table".into(), - highlights: Default::default(), - regions: Default::default() - })]), - vec![], - vec![ - row(vec![ - column( - 1, - 1, - false, - text("1", 0..280), - ParsedMarkdownTableAlignment::None - ), - column( - 1, - 1, - false, - text("Chris", 0..280), - ParsedMarkdownTableAlignment::None - ) - ]), - row(vec![ - column( - 1, - 1, - false, - text("2", 0..280), - ParsedMarkdownTableAlignment::None - ), - column( - 1, - 1, - false, - text("Dennis", 0..280), - ParsedMarkdownTableAlignment::None - ) - ]), - ], - ))], - }, - parsed - ); - } - - #[gpui::test] - async fn test_html_table_without_headings() { - let parsed = parse( - " - - - - - - - - - - -
1Chris
2Dennis
", - ) - .await; - - assert_eq!( - ParsedMarkdown { - children: vec![ParsedMarkdownElement::Table(table( - 0..240, - None, - vec![], - vec![ - row(vec![ - column( - 1, - 1, - false, - text("1", 0..240), - ParsedMarkdownTableAlignment::None - ), - column( - 1, - 1, - false, - text("Chris", 0..240), - ParsedMarkdownTableAlignment::None - ) - ]), - row(vec![ - column( - 1, - 1, - false, - text("2", 0..240), - ParsedMarkdownTableAlignment::None - ), - column( - 1, - 1, - false, - text("Dennis", 0..240), - ParsedMarkdownTableAlignment::None - ) - ]), - ], - ))], - }, - parsed - ); - } - - #[gpui::test] - async fn test_html_table_without_body() { - let parsed = parse( - " - - - - - - -
IdName
", - ) - .await; - - assert_eq!( - ParsedMarkdown { - children: vec![ParsedMarkdownElement::Table(table( - 0..150, - None, - vec![row(vec![ - column( - 1, - 1, - true, - text("Id", 0..150), - ParsedMarkdownTableAlignment::Center - ), - column( - 1, - 1, - true, - text("Name", 0..150), - ParsedMarkdownTableAlignment::Center - ) - ])], - vec![], - ))], - }, - parsed - ); - } - - #[gpui::test] - async fn test_html_heading_tags() { - let parsed = parse("

Heading

Heading

Heading

Heading

Heading
Heading
").await; - - assert_eq!( - ParsedMarkdown { - children: vec![ - ParsedMarkdownElement::Heading(ParsedMarkdownHeading { - level: HeadingLevel::H1, - source_range: 0..96, - contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range: 0..96, - contents: "Heading".into(), - highlights: Vec::default(), - regions: Vec::default() - })], - }), - ParsedMarkdownElement::Heading(ParsedMarkdownHeading { - level: HeadingLevel::H2, - source_range: 0..96, - contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range: 0..96, - contents: "Heading".into(), - highlights: Vec::default(), - regions: Vec::default() - })], - }), - ParsedMarkdownElement::Heading(ParsedMarkdownHeading { - level: HeadingLevel::H3, - source_range: 0..96, - contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range: 0..96, - contents: "Heading".into(), - highlights: Vec::default(), - regions: Vec::default() - })], - }), - ParsedMarkdownElement::Heading(ParsedMarkdownHeading { - level: HeadingLevel::H4, - source_range: 0..96, - contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range: 0..96, - contents: "Heading".into(), - highlights: Vec::default(), - regions: Vec::default() - })], - }), - ParsedMarkdownElement::Heading(ParsedMarkdownHeading { - level: HeadingLevel::H5, - source_range: 0..96, - contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range: 0..96, - contents: "Heading".into(), - highlights: Vec::default(), - regions: Vec::default() - })], - }), - ParsedMarkdownElement::Heading(ParsedMarkdownHeading { - level: HeadingLevel::H6, - source_range: 0..96, - contents: vec![MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range: 0..96, - contents: "Heading".into(), - highlights: Vec::default(), - regions: Vec::default() - })], - }), - ], - }, - parsed - ); - } - - #[gpui::test] - async fn test_html_image_tag() { - let parsed = parse("").await; - - assert_eq!( - ParsedMarkdown { - children: vec![ParsedMarkdownElement::Image(Image { - source_range: 0..40, - link: Link::Web { - url: "http://example.com/foo.png".to_string(), - }, - alt_text: None, - height: None, - width: None, - })] - }, - parsed - ); - } - - #[gpui::test] - async fn test_html_image_tag_with_alt_text() { - let parsed = parse("\"Foo\"").await; - - assert_eq!( - ParsedMarkdown { - children: vec![ParsedMarkdownElement::Image(Image { - source_range: 0..50, - link: Link::Web { - url: "http://example.com/foo.png".to_string(), - }, - alt_text: Some("Foo".into()), - height: None, - width: None, - })] - }, - parsed - ); - } - - #[gpui::test] - async fn test_html_image_tag_with_height_and_width() { - let parsed = - parse("").await; - - assert_eq!( - ParsedMarkdown { - children: vec![ParsedMarkdownElement::Image(Image { - source_range: 0..65, - link: Link::Web { - url: "http://example.com/foo.png".to_string(), - }, - alt_text: None, - height: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.)))), - width: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(200.)))), - })] - }, - parsed - ); - } - - #[gpui::test] - async fn test_html_image_style_tag_with_height_and_width() { - let parsed = parse( - "", - ) - .await; - - assert_eq!( - ParsedMarkdown { - children: vec![ParsedMarkdownElement::Image(Image { - source_range: 0..75, - link: Link::Web { - url: "http://example.com/foo.png".to_string(), - }, - alt_text: None, - height: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(100.)))), - width: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(px(200.)))), - })] - }, - parsed - ); - } - - #[gpui::test] - async fn test_header_only_table() { - let markdown = "\ -| Header 1 | Header 2 | -|----------|----------| - -Some other content -"; - - let expected_table = table( - 0..48, - None, - vec![row(vec![ - column( - 1, - 1, - true, - text("Header 1", 1..11), - ParsedMarkdownTableAlignment::None, - ), - column( - 1, - 1, - true, - text("Header 2", 12..22), - ParsedMarkdownTableAlignment::None, - ), - ])], - vec![], - ); - - assert_eq!( - parse(markdown).await.children[0], - ParsedMarkdownElement::Table(expected_table) - ); - } - - #[gpui::test] - async fn test_basic_table() { - let markdown = "\ -| Header 1 | Header 2 | -|----------|----------| -| Cell 1 | Cell 2 | -| Cell 3 | Cell 4 |"; - - let expected_table = table( - 0..95, - None, - vec![row(vec![ - column( - 1, - 1, - true, - text("Header 1", 1..11), - ParsedMarkdownTableAlignment::None, - ), - column( - 1, - 1, - true, - text("Header 2", 12..22), - ParsedMarkdownTableAlignment::None, - ), - ])], - vec![ - row(vec![ - column( - 1, - 1, - false, - text("Cell 1", 49..59), - ParsedMarkdownTableAlignment::None, - ), - column( - 1, - 1, - false, - text("Cell 2", 60..70), - ParsedMarkdownTableAlignment::None, - ), - ]), - row(vec![ - column( - 1, - 1, - false, - text("Cell 3", 73..83), - ParsedMarkdownTableAlignment::None, - ), - column( - 1, - 1, - false, - text("Cell 4", 84..94), - ParsedMarkdownTableAlignment::None, - ), - ]), - ], - ); - - assert_eq!( - parse(markdown).await.children[0], - ParsedMarkdownElement::Table(expected_table) - ); - } - - #[gpui::test] - async fn test_list_basic() { - let parsed = parse( - "\ -* Item 1 -* Item 2 -* Item 3 -", - ) - .await; - - assert_eq!( - parsed.children, - vec![ - list_item(0..8, 1, Unordered, vec![p("Item 1", 2..8)]), - list_item(9..17, 1, Unordered, vec![p("Item 2", 11..17)]), - list_item(18..26, 1, Unordered, vec![p("Item 3", 20..26)]), - ], - ); - } - - #[gpui::test] - async fn test_list_with_tasks() { - let parsed = parse( - "\ -- [ ] TODO -- [x] Checked -", - ) - .await; - - assert_eq!( - parsed.children, - vec![ - list_item(0..10, 1, Task(false, 2..5), vec![p("TODO", 6..10)]), - list_item(11..24, 1, Task(true, 13..16), vec![p("Checked", 17..24)]), - ], - ); - } - - #[gpui::test] - async fn test_list_with_indented_task() { - let parsed = parse( - "\ -- [ ] TODO - - [x] Checked - - Unordered - 1. Number 1 - 1. Number 2 -1. Number A -", - ) - .await; - - assert_eq!( - parsed.children, - vec![ - list_item(0..12, 1, Task(false, 2..5), vec![p("TODO", 6..10)]), - list_item(13..26, 2, Task(true, 15..18), vec![p("Checked", 19..26)]), - list_item(29..40, 2, Unordered, vec![p("Unordered", 31..40)]), - list_item(43..54, 2, Ordered(1), vec![p("Number 1", 46..54)]), - list_item(57..68, 2, Ordered(2), vec![p("Number 2", 60..68)]), - list_item(69..80, 1, Ordered(1), vec![p("Number A", 72..80)]), - ], - ); - } - - #[gpui::test] - async fn test_list_with_linebreak_is_handled_correctly() { - let parsed = parse( - "\ -- [ ] Task 1 - -- [x] Task 2 -", - ) - .await; - - assert_eq!( - parsed.children, - vec![ - list_item(0..13, 1, Task(false, 2..5), vec![p("Task 1", 6..12)]), - list_item(14..26, 1, Task(true, 16..19), vec![p("Task 2", 20..26)]), - ], - ); - } - - #[gpui::test] - async fn test_list_nested() { - let parsed = parse( - "\ -* Item 1 -* Item 2 -* Item 3 - -1. Hello -1. Two - 1. Three -2. Four -3. Five - -* First - 1. Hello - 1. Goodbyte - - Inner - - Inner - 2. Goodbyte - - Next item empty - - -* Last -", - ) - .await; - - assert_eq!( - parsed.children, - vec![ - list_item(0..8, 1, Unordered, vec![p("Item 1", 2..8)]), - list_item(9..17, 1, Unordered, vec![p("Item 2", 11..17)]), - list_item(18..27, 1, Unordered, vec![p("Item 3", 20..26)]), - list_item(28..36, 1, Ordered(1), vec![p("Hello", 31..36)]), - list_item(37..46, 1, Ordered(2), vec![p("Two", 40..43),]), - list_item(47..55, 2, Ordered(1), vec![p("Three", 50..55)]), - list_item(56..63, 1, Ordered(3), vec![p("Four", 59..63)]), - list_item(64..72, 1, Ordered(4), vec![p("Five", 67..71)]), - list_item(73..82, 1, Unordered, vec![p("First", 75..80)]), - list_item(83..96, 2, Ordered(1), vec![p("Hello", 86..91)]), - list_item(97..116, 3, Ordered(1), vec![p("Goodbyte", 100..108)]), - list_item(117..124, 4, Unordered, vec![p("Inner", 119..124)]), - list_item(133..140, 4, Unordered, vec![p("Inner", 135..140)]), - list_item(143..159, 2, Ordered(2), vec![p("Goodbyte", 146..154)]), - list_item(160..180, 3, Unordered, vec![p("Next item empty", 165..180)]), - list_item(186..190, 3, Unordered, vec![]), - list_item(191..197, 1, Unordered, vec![p("Last", 193..197)]), - ] - ); - } - - #[gpui::test] - async fn test_list_with_nested_content() { - let parsed = parse( - "\ -* This is a list item with two paragraphs. - - This is the second paragraph in the list item. -", - ) - .await; - - assert_eq!( - parsed.children, - vec![list_item( - 0..96, - 1, - Unordered, - vec![ - p("This is a list item with two paragraphs.", 4..44), - p("This is the second paragraph in the list item.", 50..97) - ], - ),], - ); - } - - #[gpui::test] - async fn test_list_item_with_inline_html() { - let parsed = parse( - "\ -* This is a list item with an inline HTML tag. -", - ) - .await; - - assert_eq!( - parsed.children, - vec![list_item( - 0..67, - 1, - Unordered, - vec![p("This is a list item with an inline HTML tag.", 4..44),], - ),], - ); - } - - #[gpui::test] - async fn test_nested_list_with_paragraph_inside() { - let parsed = parse( - "\ -1. a - 1. b - 1. c - - text - - 1. d -", - ) - .await; - - assert_eq!( - parsed.children, - vec![ - list_item(0..7, 1, Ordered(1), vec![p("a", 3..4)],), - list_item(8..20, 2, Ordered(1), vec![p("b", 12..13),],), - list_item(21..27, 3, Ordered(1), vec![p("c", 25..26),],), - p("text", 32..37), - list_item(41..46, 2, Ordered(1), vec![p("d", 45..46),],), - ], - ); - } - - #[gpui::test] - async fn test_list_with_leading_text() { - let parsed = parse( - "\ -* `code` -* **bold** -* [link](https://example.com) -", - ) - .await; - - assert_eq!( - parsed.children, - vec![ - list_item(0..8, 1, Unordered, vec![p("code", 2..8)]), - list_item(9..19, 1, Unordered, vec![p("bold", 11..19)]), - list_item(20..49, 1, Unordered, vec![p("link", 22..49)],), - ], - ); - } - - #[gpui::test] - async fn test_simple_block_quote() { - let parsed = parse("> Simple block quote with **styled text**").await; - - assert_eq!( - parsed.children, - vec![block_quote( - vec![p("Simple block quote with styled text", 2..41)], - 0..41 - )] - ); - } - - #[gpui::test] - async fn test_simple_block_quote_with_multiple_lines() { - let parsed = parse( - "\ -> # Heading -> More -> text -> -> More text -", - ) - .await; - - assert_eq!( - parsed.children, - vec![block_quote( - vec![ - h1(text("Heading", 4..11), 2..12), - p("More text", 14..26), - p("More text", 30..40) - ], - 0..40 - )] - ); - } - - #[gpui::test] - async fn test_nested_block_quote() { - let parsed = parse( - "\ -> A -> -> > # B -> -> C - -More text -", - ) - .await; - - assert_eq!( - parsed.children, - vec![ - block_quote( - vec![ - p("A", 2..4), - block_quote(vec![h1(text("B", 12..13), 10..14)], 8..14), - p("C", 18..20) - ], - 0..20 - ), - p("More text", 21..31) - ] - ); - } - - #[gpui::test] - async fn test_code_block() { - let parsed = parse( - "\ -``` -fn main() { - return 0; -} -``` -", - ) - .await; - - assert_eq!( - parsed.children, - vec![code_block( - None, - "fn main() {\n return 0;\n}", - 0..35, - None - )] - ); - } - - #[gpui::test] - async fn test_code_block_with_language(executor: BackgroundExecutor) { - let language_registry = Arc::new(LanguageRegistry::test(executor.clone())); - language_registry.add(language::rust_lang()); - - let parsed = parse_markdown( - "\ -```rust -fn main() { - return 0; -} -``` -", - None, - Some(language_registry), - ) - .await; - - assert_eq!( - parsed.children, - vec![code_block( - Some("rust".to_string()), - "fn main() {\n return 0;\n}", - 0..39, - Some(vec![]) - )] - ); - } - - fn h1(contents: MarkdownParagraph, source_range: Range) -> ParsedMarkdownElement { - ParsedMarkdownElement::Heading(ParsedMarkdownHeading { - source_range, - level: HeadingLevel::H1, - contents, - }) - } - - fn h2(contents: MarkdownParagraph, source_range: Range) -> ParsedMarkdownElement { - ParsedMarkdownElement::Heading(ParsedMarkdownHeading { - source_range, - level: HeadingLevel::H2, - contents, - }) - } - - fn h3(contents: MarkdownParagraph, source_range: Range) -> ParsedMarkdownElement { - ParsedMarkdownElement::Heading(ParsedMarkdownHeading { - source_range, - level: HeadingLevel::H3, - contents, - }) - } - - fn p(contents: &str, source_range: Range) -> ParsedMarkdownElement { - ParsedMarkdownElement::Paragraph(text(contents, source_range)) - } - - fn text(contents: &str, source_range: Range) -> MarkdownParagraph { - vec![MarkdownParagraphChunk::Text(ParsedMarkdownText { - highlights: Vec::new(), - regions: Vec::new(), - source_range, - contents: contents.to_string().into(), - })] - } - - fn block_quote( - children: Vec, - source_range: Range, - ) -> ParsedMarkdownElement { - ParsedMarkdownElement::BlockQuote(ParsedMarkdownBlockQuote { - source_range, - children, - }) - } - - fn code_block( - language: Option, - code: &str, - source_range: Range, - highlights: Option, HighlightId)>>, - ) -> ParsedMarkdownElement { - ParsedMarkdownElement::CodeBlock(ParsedMarkdownCodeBlock { - source_range, - language, - contents: code.to_string().into(), - highlights, - }) - } - - fn list_item( - source_range: Range, - depth: u16, - item_type: ParsedMarkdownListItemType, - content: Vec, - ) -> ParsedMarkdownElement { - ParsedMarkdownElement::ListItem(ParsedMarkdownListItem { - source_range, - item_type, - depth, - content, - nested: false, - }) - } - - fn nested_list_item( - source_range: Range, - depth: u16, - item_type: ParsedMarkdownListItemType, - content: Vec, - ) -> ParsedMarkdownElement { - ParsedMarkdownElement::ListItem(ParsedMarkdownListItem { - source_range, - item_type, - depth, - content, - nested: true, - }) - } - - fn table( - source_range: Range, - caption: Option, - header: Vec, - body: Vec, - ) -> ParsedMarkdownTable { - ParsedMarkdownTable { - source_range, - header, - body, - caption, - } - } - - fn row(columns: Vec) -> ParsedMarkdownTableRow { - ParsedMarkdownTableRow { columns } - } - - fn column( - col_span: usize, - row_span: usize, - is_header: bool, - children: MarkdownParagraph, - alignment: ParsedMarkdownTableAlignment, - ) -> ParsedMarkdownTableColumn { - ParsedMarkdownTableColumn { - col_span, - row_span, - is_header, - children, - alignment, - } - } - - impl PartialEq for ParsedMarkdownTable { - fn eq(&self, other: &Self) -> bool { - self.source_range == other.source_range - && self.header == other.header - && self.body == other.body - } - } - - impl PartialEq for ParsedMarkdownText { - fn eq(&self, other: &Self) -> bool { - self.source_range == other.source_range && self.contents == other.contents - } - } -} diff --git a/crates/markdown_preview/src/markdown_preview.rs b/crates/markdown_preview/src/markdown_preview.rs deleted file mode 100644 index 61c99764ad..0000000000 --- a/crates/markdown_preview/src/markdown_preview.rs +++ /dev/null @@ -1,44 +0,0 @@ -use gpui::{App, actions}; -use workspace::Workspace; - -pub mod markdown_elements; -mod markdown_minifier; -pub mod markdown_parser; -pub mod markdown_preview_view; -pub mod markdown_renderer; - -actions!( - markdown, - [ - /// Scrolls up by one page in the markdown preview. - #[action(deprecated_aliases = ["markdown::MovePageUp"])] - ScrollPageUp, - /// Scrolls down by one page in the markdown preview. - #[action(deprecated_aliases = ["markdown::MovePageDown"])] - ScrollPageDown, - /// Scrolls up by approximately one visual line. - ScrollUp, - /// Scrolls down by approximately one visual line. - ScrollDown, - /// Scrolls up by one markdown element in the markdown preview - ScrollUpByItem, - /// Scrolls down by one markdown element in the markdown preview - ScrollDownByItem, - /// Opens a markdown preview for the current file. - OpenPreview, - /// Opens a markdown preview in a split pane. - OpenPreviewToTheSide, - /// Opens a following markdown preview that syncs with the editor. - OpenFollowingPreview - ] -); - -pub fn init(cx: &mut App) { - cx.observe_new(|workspace: &mut Workspace, window, cx| { - let Some(window) = window else { - return; - }; - markdown_preview_view::MarkdownPreviewView::register(workspace, window, cx); - }) - .detach(); -} diff --git a/crates/markdown_preview/src/markdown_preview_view.rs b/crates/markdown_preview/src/markdown_preview_view.rs deleted file mode 100644 index 20613b112e..0000000000 --- a/crates/markdown_preview/src/markdown_preview_view.rs +++ /dev/null @@ -1,677 +0,0 @@ -use std::cmp::min; -use std::sync::Arc; -use std::time::Duration; -use std::{ops::Range, path::PathBuf}; - -use anyhow::Result; -use editor::scroll::Autoscroll; -use editor::{Editor, EditorEvent, MultiBufferOffset, SelectionEffects}; -use gpui::{ - App, ClickEvent, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, - IntoElement, IsZero, ListState, ParentElement, Render, RetainAllImageCache, Styled, - Subscription, Task, WeakEntity, Window, list, -}; -use language::LanguageRegistry; -use settings::Settings; -use theme::ThemeSettings; -use ui::{WithScrollbar, prelude::*}; -use workspace::item::{Item, ItemHandle}; -use workspace::{Pane, Workspace}; - -use crate::markdown_elements::ParsedMarkdownElement; -use crate::markdown_renderer::CheckboxClickedEvent; -use crate::{ - OpenFollowingPreview, OpenPreview, OpenPreviewToTheSide, ScrollPageDown, ScrollPageUp, - markdown_elements::ParsedMarkdown, - markdown_parser::parse_markdown, - markdown_renderer::{RenderContext, render_markdown_block}, -}; -use crate::{ScrollDown, ScrollDownByItem, ScrollUp, ScrollUpByItem}; - -const REPARSE_DEBOUNCE: Duration = Duration::from_millis(200); - -pub struct MarkdownPreviewView { - workspace: WeakEntity, - image_cache: Entity, - active_editor: Option, - focus_handle: FocusHandle, - contents: Option, - selected_block: usize, - list_state: ListState, - language_registry: Arc, - parsing_markdown_task: Option>>, - mode: MarkdownPreviewMode, -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum MarkdownPreviewMode { - /// The preview will always show the contents of the provided editor. - Default, - /// The preview will "follow" the currently active editor. - Follow, -} - -struct EditorState { - editor: Entity, - _subscription: Subscription, -} - -impl MarkdownPreviewView { - pub fn register(workspace: &mut Workspace, _window: &mut Window, _cx: &mut Context) { - workspace.register_action(move |workspace, _: &OpenPreview, window, cx| { - if let Some(editor) = Self::resolve_active_item_as_markdown_editor(workspace, cx) { - let view = Self::create_markdown_view(workspace, editor.clone(), window, cx); - workspace.active_pane().update(cx, |pane, cx| { - if let Some(existing_view_idx) = - Self::find_existing_independent_preview_item_idx(pane, &editor, cx) - { - pane.activate_item(existing_view_idx, true, true, window, cx); - } else { - pane.add_item(Box::new(view.clone()), true, true, None, window, cx) - } - }); - cx.notify(); - } - }); - - workspace.register_action(move |workspace, _: &OpenPreviewToTheSide, window, cx| { - if let Some(editor) = Self::resolve_active_item_as_markdown_editor(workspace, cx) { - let view = Self::create_markdown_view(workspace, editor.clone(), window, cx); - let pane = workspace - .find_pane_in_direction(workspace::SplitDirection::Right, cx) - .unwrap_or_else(|| { - workspace.split_pane( - workspace.active_pane().clone(), - workspace::SplitDirection::Right, - window, - cx, - ) - }); - pane.update(cx, |pane, cx| { - if let Some(existing_view_idx) = - Self::find_existing_independent_preview_item_idx(pane, &editor, cx) - { - pane.activate_item(existing_view_idx, true, true, window, cx); - } else { - pane.add_item(Box::new(view.clone()), false, false, None, window, cx) - } - }); - editor.focus_handle(cx).focus(window); - cx.notify(); - } - }); - - workspace.register_action(move |workspace, _: &OpenFollowingPreview, window, cx| { - if let Some(editor) = Self::resolve_active_item_as_markdown_editor(workspace, cx) { - // Check if there's already a following preview - let existing_follow_view_idx = { - let active_pane = workspace.active_pane().read(cx); - active_pane - .items_of_type::() - .find(|view| view.read(cx).mode == MarkdownPreviewMode::Follow) - .and_then(|view| active_pane.index_for_item(&view)) - }; - - if let Some(existing_follow_view_idx) = existing_follow_view_idx { - workspace.active_pane().update(cx, |pane, cx| { - pane.activate_item(existing_follow_view_idx, true, true, window, cx); - }); - } else { - let view = Self::create_following_markdown_view(workspace, editor, window, cx); - workspace.active_pane().update(cx, |pane, cx| { - pane.add_item(Box::new(view.clone()), true, true, None, window, cx) - }); - } - cx.notify(); - } - }); - } - - fn find_existing_independent_preview_item_idx( - pane: &Pane, - editor: &Entity, - cx: &App, - ) -> Option { - pane.items_of_type::() - .find(|view| { - let view_read = view.read(cx); - // Only look for independent (Default mode) previews, not Follow previews - view_read.mode == MarkdownPreviewMode::Default - && view_read - .active_editor - .as_ref() - .is_some_and(|active_editor| active_editor.editor == *editor) - }) - .and_then(|view| pane.index_for_item(&view)) - } - - pub fn resolve_active_item_as_markdown_editor( - workspace: &Workspace, - cx: &mut Context, - ) -> Option> { - if let Some(editor) = workspace - .active_item(cx) - .and_then(|item| item.act_as::(cx)) - && Self::is_markdown_file(&editor, cx) - { - return Some(editor); - } - None - } - - fn create_markdown_view( - workspace: &mut Workspace, - editor: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - let language_registry = workspace.project().read(cx).languages().clone(); - let workspace_handle = workspace.weak_handle(); - MarkdownPreviewView::new( - MarkdownPreviewMode::Default, - editor, - workspace_handle, - language_registry, - window, - cx, - ) - } - - fn create_following_markdown_view( - workspace: &mut Workspace, - editor: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - let language_registry = workspace.project().read(cx).languages().clone(); - let workspace_handle = workspace.weak_handle(); - MarkdownPreviewView::new( - MarkdownPreviewMode::Follow, - editor, - workspace_handle, - language_registry, - window, - cx, - ) - } - - pub fn new( - mode: MarkdownPreviewMode, - active_editor: Entity, - workspace: WeakEntity, - language_registry: Arc, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - cx.new(|cx| { - let list_state = ListState::new(0, gpui::ListAlignment::Top, px(1000.)); - - let mut this = Self { - selected_block: 0, - active_editor: None, - focus_handle: cx.focus_handle(), - workspace: workspace.clone(), - contents: None, - list_state, - language_registry, - parsing_markdown_task: None, - image_cache: RetainAllImageCache::new(cx), - mode, - }; - - this.set_editor(active_editor, window, cx); - - if mode == MarkdownPreviewMode::Follow { - if let Some(workspace) = &workspace.upgrade() { - cx.observe_in(workspace, window, |this, workspace, window, cx| { - let item = workspace.read(cx).active_item(cx); - this.workspace_updated(item, window, cx); - }) - .detach(); - } else { - log::error!("Failed to listen to workspace updates"); - } - } - - this - }) - } - - fn workspace_updated( - &mut self, - active_item: Option>, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(item) = active_item - && item.item_id() != cx.entity_id() - && let Some(editor) = item.act_as::(cx) - && Self::is_markdown_file(&editor, cx) - { - self.set_editor(editor, window, cx); - } - } - - pub fn is_markdown_file(editor: &Entity, cx: &mut Context) -> bool { - let buffer = editor.read(cx).buffer().read(cx); - if let Some(buffer) = buffer.as_singleton() - && let Some(language) = buffer.read(cx).language() - { - return language.name() == "Markdown".into(); - } - false - } - - fn set_editor(&mut self, editor: Entity, window: &mut Window, cx: &mut Context) { - if let Some(active) = &self.active_editor - && active.editor == editor - { - return; - } - - let subscription = cx.subscribe_in( - &editor, - window, - |this, editor, event: &EditorEvent, window, cx| { - match event { - EditorEvent::Edited { .. } - | EditorEvent::DirtyChanged - | EditorEvent::ExcerptsEdited { .. } => { - this.parse_markdown_from_active_editor(true, window, cx); - } - EditorEvent::SelectionsChanged { .. } => { - let selection_range = editor.update(cx, |editor, cx| { - editor - .selections - .last::(&editor.display_snapshot(cx)) - .range() - }); - this.selected_block = this.get_block_index_under_cursor(selection_range); - this.list_state.scroll_to_reveal_item(this.selected_block); - cx.notify(); - } - _ => {} - }; - }, - ); - - self.active_editor = Some(EditorState { - editor, - _subscription: subscription, - }); - - self.parse_markdown_from_active_editor(false, window, cx); - } - - fn parse_markdown_from_active_editor( - &mut self, - wait_for_debounce: bool, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(state) = &self.active_editor { - self.parsing_markdown_task = Some(self.parse_markdown_in_background( - wait_for_debounce, - state.editor.clone(), - window, - cx, - )); - } - } - - fn parse_markdown_in_background( - &mut self, - wait_for_debounce: bool, - editor: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - let language_registry = self.language_registry.clone(); - - cx.spawn_in(window, async move |view, cx| { - if wait_for_debounce { - // Wait for the user to stop typing - cx.background_executor().timer(REPARSE_DEBOUNCE).await; - } - - let (contents, file_location) = view.update(cx, |_, cx| { - let editor = editor.read(cx); - let contents = editor.buffer().read(cx).snapshot(cx).text(); - let file_location = MarkdownPreviewView::get_folder_for_active_editor(editor, cx); - (contents, file_location) - })?; - - let parsing_task = cx.background_spawn(async move { - parse_markdown(&contents, file_location, Some(language_registry)).await - }); - let contents = parsing_task.await; - view.update(cx, move |view, cx| { - let markdown_blocks_count = contents.children.len(); - view.contents = Some(contents); - let scroll_top = view.list_state.logical_scroll_top(); - view.list_state.reset(markdown_blocks_count); - view.list_state.scroll_to(scroll_top); - cx.notify(); - }) - }) - } - - fn move_cursor_to_block( - &self, - window: &mut Window, - cx: &mut Context, - selection: Range, - ) { - if let Some(state) = &self.active_editor { - state.editor.update(cx, |editor, cx| { - editor.change_selections( - SelectionEffects::scroll(Autoscroll::center()), - window, - cx, - |selections| selections.select_ranges(vec![selection]), - ); - window.focus(&editor.focus_handle(cx)); - }); - } - } - - /// The absolute path of the file that is currently being previewed. - fn get_folder_for_active_editor(editor: &Editor, cx: &App) -> Option { - if let Some(file) = editor.file_at(MultiBufferOffset(0), cx) { - if let Some(file) = file.as_local() { - file.abs_path(cx).parent().map(|p| p.to_path_buf()) - } else { - None - } - } else { - None - } - } - - fn get_block_index_under_cursor(&self, selection_range: Range) -> usize { - let mut block_index = None; - let cursor = selection_range.start.0; - - let mut last_end = 0; - if let Some(content) = &self.contents { - for (i, block) in content.children.iter().enumerate() { - let Some(Range { start, end }) = block.source_range() else { - continue; - }; - - // Check if the cursor is between the last block and the current block - if last_end <= cursor && cursor < start { - block_index = Some(i.saturating_sub(1)); - break; - } - - if start <= cursor && end >= cursor { - block_index = Some(i); - break; - } - last_end = end; - } - - if block_index.is_none() && last_end < cursor { - block_index = Some(content.children.len().saturating_sub(1)); - } - } - - block_index.unwrap_or_default() - } - - fn should_apply_padding_between( - current_block: &ParsedMarkdownElement, - next_block: Option<&ParsedMarkdownElement>, - ) -> bool { - !(current_block.is_list_item() && next_block.map(|b| b.is_list_item()).unwrap_or(false)) - } - - fn scroll_page_up(&mut self, _: &ScrollPageUp, _window: &mut Window, cx: &mut Context) { - let viewport_height = self.list_state.viewport_bounds().size.height; - if viewport_height.is_zero() { - return; - } - - self.list_state.scroll_by(-viewport_height); - cx.notify(); - } - - fn scroll_page_down( - &mut self, - _: &ScrollPageDown, - _window: &mut Window, - cx: &mut Context, - ) { - let viewport_height = self.list_state.viewport_bounds().size.height; - if viewport_height.is_zero() { - return; - } - - self.list_state.scroll_by(viewport_height); - cx.notify(); - } - - fn scroll_up(&mut self, _: &ScrollUp, window: &mut Window, cx: &mut Context) { - let scroll_top = self.list_state.logical_scroll_top(); - if let Some(bounds) = self.list_state.bounds_for_item(scroll_top.item_ix) { - let item_height = bounds.size.height; - // Scroll no more than the rough equivalent of a large headline - let max_height = window.rem_size() * 2; - let scroll_height = min(item_height, max_height); - self.list_state.scroll_by(-scroll_height); - } - cx.notify(); - } - - fn scroll_down(&mut self, _: &ScrollDown, window: &mut Window, cx: &mut Context) { - let scroll_top = self.list_state.logical_scroll_top(); - if let Some(bounds) = self.list_state.bounds_for_item(scroll_top.item_ix) { - let item_height = bounds.size.height; - // Scroll no more than the rough equivalent of a large headline - let max_height = window.rem_size() * 2; - let scroll_height = min(item_height, max_height); - self.list_state.scroll_by(scroll_height); - } - cx.notify(); - } - - fn scroll_up_by_item( - &mut self, - _: &ScrollUpByItem, - _window: &mut Window, - cx: &mut Context, - ) { - let scroll_top = self.list_state.logical_scroll_top(); - if let Some(bounds) = self.list_state.bounds_for_item(scroll_top.item_ix) { - self.list_state.scroll_by(-bounds.size.height); - } - cx.notify(); - } - - fn scroll_down_by_item( - &mut self, - _: &ScrollDownByItem, - _window: &mut Window, - cx: &mut Context, - ) { - let scroll_top = self.list_state.logical_scroll_top(); - if let Some(bounds) = self.list_state.bounds_for_item(scroll_top.item_ix) { - self.list_state.scroll_by(bounds.size.height); - } - cx.notify(); - } -} - -impl Focusable for MarkdownPreviewView { - fn focus_handle(&self, _: &App) -> gpui::FocusHandle { - self.focus_handle.clone() - } -} - -impl EventEmitter<()> for MarkdownPreviewView {} - -impl Item for MarkdownPreviewView { - type Event = (); - - fn tab_icon(&self, _window: &Window, _cx: &App) -> Option { - Some(Icon::new(IconName::FileDoc)) - } - - fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString { - self.active_editor - .as_ref() - .and_then(|editor_state| { - let buffer = editor_state.editor.read(cx).buffer().read(cx); - let buffer = buffer.as_singleton()?; - let file = buffer.read(cx).file()?; - let local_file = file.as_local()?; - local_file - .abs_path(cx) - .file_name() - .map(|name| format!("Preview {}", name.to_string_lossy()).into()) - }) - .unwrap_or_else(|| SharedString::from("Markdown Preview")) - } - - fn telemetry_event_text(&self) -> Option<&'static str> { - Some("Markdown Preview Opened") - } - - fn to_item_events(_event: &Self::Event, _f: impl FnMut(workspace::item::ItemEvent)) {} -} - -impl Render for MarkdownPreviewView { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let buffer_size = ThemeSettings::get_global(cx).buffer_font_size(cx); - let buffer_line_height = ThemeSettings::get_global(cx).buffer_line_height; - - v_flex() - .image_cache(self.image_cache.clone()) - .id("MarkdownPreview") - .key_context("MarkdownPreview") - .track_focus(&self.focus_handle(cx)) - .on_action(cx.listener(MarkdownPreviewView::scroll_page_up)) - .on_action(cx.listener(MarkdownPreviewView::scroll_page_down)) - .on_action(cx.listener(MarkdownPreviewView::scroll_up)) - .on_action(cx.listener(MarkdownPreviewView::scroll_down)) - .on_action(cx.listener(MarkdownPreviewView::scroll_up_by_item)) - .on_action(cx.listener(MarkdownPreviewView::scroll_down_by_item)) - .size_full() - .bg(cx.theme().colors().editor_background) - .p_4() - .text_size(buffer_size) - .line_height(relative(buffer_line_height.value())) - .child(div().flex_grow().map(|this| { - this.child( - list( - self.list_state.clone(), - cx.processor(|this, ix, window, cx| { - let Some(contents) = &this.contents else { - return div().into_any(); - }; - - let mut render_cx = - RenderContext::new(Some(this.workspace.clone()), window, cx) - .with_checkbox_clicked_callback(cx.listener( - move |this, e: &CheckboxClickedEvent, window, cx| { - if let Some(editor) = this - .active_editor - .as_ref() - .map(|s| s.editor.clone()) - { - editor.update(cx, |editor, cx| { - let task_marker = - if e.checked() { "[x]" } else { "[ ]" }; - - editor.edit( - [( - MultiBufferOffset( - e.source_range().start, - ) - ..MultiBufferOffset( - e.source_range().end, - ), - task_marker, - )], - cx, - ); - }); - this.parse_markdown_from_active_editor( - false, window, cx, - ); - cx.notify(); - } - }, - )); - - let block = contents.children.get(ix).unwrap(); - let rendered_block = render_markdown_block(block, &mut render_cx); - - let should_apply_padding = Self::should_apply_padding_between( - block, - contents.children.get(ix + 1), - ); - - div() - .id(ix) - .when(should_apply_padding, |this| { - this.pb(render_cx.scaled_rems(0.75)) - }) - .group("markdown-block") - .on_click(cx.listener( - move |this, event: &ClickEvent, window, cx| { - if event.click_count() == 2 - && let Some(source_range) = this - .contents - .as_ref() - .and_then(|c| c.children.get(ix)) - .and_then(|block: &ParsedMarkdownElement| { - block.source_range() - }) - { - this.move_cursor_to_block( - window, - cx, - MultiBufferOffset(source_range.start) - ..MultiBufferOffset(source_range.start), - ); - } - }, - )) - .map(move |container| { - let indicator = div() - .h_full() - .w(px(4.0)) - .when(ix == this.selected_block, |this| { - this.bg(cx.theme().colors().border) - }) - .group_hover("markdown-block", |s| { - if ix == this.selected_block { - s - } else { - s.bg(cx.theme().colors().border_variant) - } - }) - .rounded_xs(); - - container.child( - div() - .relative() - .child( - div() - .pl(render_cx.scaled_rems(1.0)) - .child(rendered_block), - ) - .child(indicator.absolute().left_0().top_0()), - ) - }) - .into_any() - }), - ) - .size_full(), - ) - })) - .vertical_scrollbar_for(&self.list_state, window, cx) - } -} diff --git a/crates/markdown_preview/src/markdown_renderer.rs b/crates/markdown_preview/src/markdown_renderer.rs deleted file mode 100644 index 336f1cacfd..0000000000 --- a/crates/markdown_preview/src/markdown_renderer.rs +++ /dev/null @@ -1,1083 +0,0 @@ -use crate::markdown_elements::{ - HeadingLevel, Image, Link, MarkdownParagraph, MarkdownParagraphChunk, ParsedMarkdown, - ParsedMarkdownBlockQuote, ParsedMarkdownCodeBlock, ParsedMarkdownElement, - ParsedMarkdownHeading, ParsedMarkdownListItem, ParsedMarkdownListItemType, ParsedMarkdownTable, - ParsedMarkdownTableAlignment, ParsedMarkdownTableRow, -}; -use fs::normalize_path; -use gpui::{ - AbsoluteLength, AnyElement, App, AppContext as _, ClipboardItem, Context, Div, Element, - ElementId, Entity, HighlightStyle, Hsla, ImageSource, InteractiveText, IntoElement, Keystroke, - Modifiers, ParentElement, Render, Resource, SharedString, Styled, StyledText, TextStyle, - WeakEntity, Window, div, img, rems, -}; -use settings::Settings; -use std::{ - ops::{Mul, Range}, - sync::Arc, - vec, -}; -use theme::{ActiveTheme, SyntaxTheme, ThemeSettings}; -use ui::{ - ButtonCommon, Clickable, Color, FluentBuilder, IconButton, IconName, IconSize, - InteractiveElement, Label, LabelCommon, LabelSize, LinkPreview, Pixels, Rems, - StatefulInteractiveElement, StyledExt, StyledImage, ToggleState, Tooltip, VisibleOnHover, - h_flex, tooltip_container, v_flex, -}; -use workspace::{OpenOptions, OpenVisible, Workspace}; - -pub struct CheckboxClickedEvent { - pub checked: bool, - pub source_range: Range, -} - -impl CheckboxClickedEvent { - pub fn source_range(&self) -> Range { - self.source_range.clone() - } - - pub fn checked(&self) -> bool { - self.checked - } -} - -type CheckboxClickedCallback = Arc>; - -#[derive(Clone)] -pub struct RenderContext { - workspace: Option>, - next_id: usize, - buffer_font_family: SharedString, - buffer_text_style: TextStyle, - text_style: TextStyle, - border_color: Hsla, - title_bar_background_color: Hsla, - panel_background_color: Hsla, - text_color: Hsla, - link_color: Hsla, - window_rem_size: Pixels, - text_muted_color: Hsla, - code_block_background_color: Hsla, - code_span_background_color: Hsla, - syntax_theme: Arc, - indent: usize, - checkbox_clicked_callback: Option, - is_last_child: bool, -} - -impl RenderContext { - pub fn new( - workspace: Option>, - window: &mut Window, - cx: &mut App, - ) -> RenderContext { - let theme = cx.theme().clone(); - - let settings = ThemeSettings::get_global(cx); - let buffer_font_family = settings.buffer_font.family.clone(); - let buffer_font_features = settings.buffer_font.features.clone(); - let mut buffer_text_style = window.text_style(); - buffer_text_style.font_family = buffer_font_family.clone(); - buffer_text_style.font_features = buffer_font_features; - buffer_text_style.font_size = AbsoluteLength::from(settings.buffer_font_size(cx)); - - RenderContext { - workspace, - next_id: 0, - indent: 0, - buffer_font_family, - buffer_text_style, - text_style: window.text_style(), - syntax_theme: theme.syntax().clone(), - border_color: theme.colors().border, - title_bar_background_color: theme.colors().title_bar_background, - panel_background_color: theme.colors().panel_background, - text_color: theme.colors().text, - link_color: theme.colors().text_accent, - window_rem_size: window.rem_size(), - text_muted_color: theme.colors().text_muted, - code_block_background_color: theme.colors().surface_background, - code_span_background_color: theme.colors().editor_document_highlight_read_background, - checkbox_clicked_callback: None, - is_last_child: false, - } - } - - pub fn with_checkbox_clicked_callback( - mut self, - callback: impl Fn(&CheckboxClickedEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.checkbox_clicked_callback = Some(Arc::new(Box::new(callback))); - self - } - - fn next_id(&mut self, span: &Range) -> ElementId { - let id = format!("markdown-{}-{}-{}", self.next_id, span.start, span.end); - self.next_id += 1; - ElementId::from(SharedString::from(id)) - } - - /// HACK: used to have rems relative to buffer font size, so that things scale appropriately as - /// buffer font size changes. The callees of this function should be reimplemented to use real - /// relative sizing once that is implemented in GPUI - pub fn scaled_rems(&self, rems: f32) -> Rems { - self.buffer_text_style - .font_size - .to_rems(self.window_rem_size) - .mul(rems) - } - - /// This ensures that children inside of block quotes - /// have padding between them. - /// - /// For example, for this markdown: - /// - /// ```markdown - /// > This is a block quote. - /// > - /// > And this is the next paragraph. - /// ``` - /// - /// We give padding between "This is a block quote." - /// and "And this is the next paragraph." - fn with_common_p(&self, element: Div) -> Div { - if self.indent > 0 && !self.is_last_child { - element.pb(self.scaled_rems(0.75)) - } else { - element - } - } - - /// The is used to indicate that the current element is the last child or not of its parent. - /// - /// Then we can avoid adding padding to the bottom of the last child. - fn with_last_child(&mut self, is_last: bool, render: R) -> AnyElement - where - R: FnOnce(&mut Self) -> AnyElement, - { - self.is_last_child = is_last; - let element = render(self); - self.is_last_child = false; - element - } -} - -pub fn render_parsed_markdown( - parsed: &ParsedMarkdown, - workspace: Option>, - window: &mut Window, - cx: &mut App, -) -> Div { - let mut cx = RenderContext::new(workspace, window, cx); - - v_flex().gap_3().children( - parsed - .children - .iter() - .map(|block| render_markdown_block(block, &mut cx)), - ) -} -pub fn render_markdown_block(block: &ParsedMarkdownElement, cx: &mut RenderContext) -> AnyElement { - use ParsedMarkdownElement::*; - match block { - Paragraph(text) => render_markdown_paragraph(text, cx), - Heading(heading) => render_markdown_heading(heading, cx), - ListItem(list_item) => render_markdown_list_item(list_item, cx), - Table(table) => render_markdown_table(table, cx), - BlockQuote(block_quote) => render_markdown_block_quote(block_quote, cx), - CodeBlock(code_block) => render_markdown_code_block(code_block, cx), - HorizontalRule(_) => render_markdown_rule(cx), - Image(image) => render_markdown_image(image, cx), - } -} - -fn render_markdown_heading(parsed: &ParsedMarkdownHeading, cx: &mut RenderContext) -> AnyElement { - let size = match parsed.level { - HeadingLevel::H1 => 2., - HeadingLevel::H2 => 1.5, - HeadingLevel::H3 => 1.25, - HeadingLevel::H4 => 1., - HeadingLevel::H5 => 0.875, - HeadingLevel::H6 => 0.85, - }; - - let text_size = cx.scaled_rems(size); - - // was `DefiniteLength::from(text_size.mul(1.25))` - // let line_height = DefiniteLength::from(text_size.mul(1.25)); - let line_height = text_size * 1.25; - - // was `rems(0.15)` - // let padding_top = cx.scaled_rems(0.15); - let padding_top = rems(0.15); - - // was `.pb_1()` = `rems(0.25)` - // let padding_bottom = cx.scaled_rems(0.25); - let padding_bottom = rems(0.25); - - let color = match parsed.level { - HeadingLevel::H6 => cx.text_muted_color, - _ => cx.text_color, - }; - div() - .line_height(line_height) - .text_size(text_size) - .text_color(color) - .pt(padding_top) - .pb(padding_bottom) - .children(render_markdown_text(&parsed.contents, cx)) - .whitespace_normal() - .into_any() -} - -fn render_markdown_list_item( - parsed: &ParsedMarkdownListItem, - cx: &mut RenderContext, -) -> AnyElement { - use ParsedMarkdownListItemType::*; - let depth = parsed.depth.saturating_sub(1) as usize; - - let bullet = match &parsed.item_type { - Ordered(order) => list_item_prefix(*order as usize, true, depth).into_any_element(), - Unordered => list_item_prefix(1, false, depth).into_any_element(), - Task(checked, range) => div() - .id(cx.next_id(range)) - .mt(cx.scaled_rems(3.0 / 16.0)) - .child( - MarkdownCheckbox::new( - "checkbox", - if *checked { - ToggleState::Selected - } else { - ToggleState::Unselected - }, - cx.clone(), - ) - .when_some( - cx.checkbox_clicked_callback.clone(), - |this, callback| { - this.on_click({ - let range = range.clone(); - move |selection, window, cx| { - let checked = match selection { - ToggleState::Selected => true, - ToggleState::Unselected => false, - _ => return, - }; - - if window.modifiers().secondary() { - callback( - &CheckboxClickedEvent { - checked, - source_range: range.clone(), - }, - window, - cx, - ); - } - } - }) - }, - ), - ) - .hover(|s| s.cursor_pointer()) - .tooltip(|_, cx| { - InteractiveMarkdownElementTooltip::new(None, "toggle checkbox", cx).into() - }) - .into_any_element(), - }; - let bullet = div().mr(cx.scaled_rems(0.5)).child(bullet); - - let contents: Vec = parsed - .content - .iter() - .map(|c| render_markdown_block(c, cx)) - .collect(); - - let item = h_flex() - .when(!parsed.nested, |this| this.pl(cx.scaled_rems(depth as f32))) - .when(parsed.nested && depth > 0, |this| this.ml_neg_1p5()) - .items_start() - .children(vec![ - bullet, - v_flex() - .children(contents) - .when(!parsed.nested, |this| this.gap(cx.scaled_rems(1.0))) - .pr(cx.scaled_rems(1.0)) - .w_full(), - ]); - - cx.with_common_p(item).into_any() -} - -/// # MarkdownCheckbox /// -/// HACK: Copied from `ui/src/components/toggle.rs` to deal with scaling issues in markdown preview -/// changes should be integrated into `Checkbox` in `toggle.rs` while making sure checkboxes elsewhere in the -/// app are not visually affected -#[derive(gpui::IntoElement)] -struct MarkdownCheckbox { - id: ElementId, - toggle_state: ToggleState, - disabled: bool, - placeholder: bool, - on_click: Option>, - filled: bool, - style: ui::ToggleStyle, - tooltip: Option gpui::AnyView>>, - label: Option, - render_cx: RenderContext, -} - -impl MarkdownCheckbox { - /// Creates a new [`Checkbox`]. - fn new(id: impl Into, checked: ToggleState, render_cx: RenderContext) -> Self { - Self { - id: id.into(), - toggle_state: checked, - disabled: false, - on_click: None, - filled: false, - style: ui::ToggleStyle::default(), - tooltip: None, - label: None, - placeholder: false, - render_cx, - } - } - - /// Binds a handler to the [`Checkbox`] that will be called when clicked. - fn on_click(mut self, handler: impl Fn(&ToggleState, &mut Window, &mut App) + 'static) -> Self { - self.on_click = Some(Box::new(handler)); - self - } - - fn bg_color(&self, cx: &App) -> Hsla { - let style = self.style.clone(); - match (style, self.filled) { - (ui::ToggleStyle::Ghost, false) => cx.theme().colors().ghost_element_background, - (ui::ToggleStyle::Ghost, true) => cx.theme().colors().element_background, - (ui::ToggleStyle::ElevationBased(_), false) => gpui::transparent_black(), - (ui::ToggleStyle::ElevationBased(elevation), true) => elevation.darker_bg(cx), - (ui::ToggleStyle::Custom(_), false) => gpui::transparent_black(), - (ui::ToggleStyle::Custom(color), true) => color.opacity(0.2), - } - } - - fn border_color(&self, cx: &App) -> Hsla { - if self.disabled { - return cx.theme().colors().border_variant; - } - - match self.style.clone() { - ui::ToggleStyle::Ghost => cx.theme().colors().border, - ui::ToggleStyle::ElevationBased(_) => cx.theme().colors().border, - ui::ToggleStyle::Custom(color) => color.opacity(0.3), - } - } -} - -impl gpui::RenderOnce for MarkdownCheckbox { - fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - let group_id = format!("checkbox_group_{:?}", self.id); - let color = if self.disabled { - Color::Disabled - } else { - Color::Selected - }; - let icon_size_small = IconSize::Custom(self.render_cx.scaled_rems(14. / 16.)); // was IconSize::Small - let icon = match self.toggle_state { - ToggleState::Selected => { - if self.placeholder { - None - } else { - Some( - ui::Icon::new(IconName::Check) - .size(icon_size_small) - .color(color), - ) - } - } - ToggleState::Indeterminate => Some( - ui::Icon::new(IconName::Dash) - .size(icon_size_small) - .color(color), - ), - ToggleState::Unselected => None, - }; - - let bg_color = self.bg_color(cx); - let border_color = self.border_color(cx); - let hover_border_color = border_color.alpha(0.7); - - let size = self.render_cx.scaled_rems(1.25); // was Self::container_size(); (20px) - - let checkbox = h_flex() - .id(self.id.clone()) - .justify_center() - .items_center() - .size(size) - .group(group_id.clone()) - .child( - div() - .flex() - .flex_none() - .justify_center() - .items_center() - .m(self.render_cx.scaled_rems(0.25)) // was .m_1 - .size(self.render_cx.scaled_rems(1.0)) // was .size_4 - .rounded(self.render_cx.scaled_rems(0.125)) // was .rounded_xs - .border_1() - .bg(bg_color) - .border_color(border_color) - .when(self.disabled, |this| this.cursor_not_allowed()) - .when(self.disabled, |this| { - this.bg(cx.theme().colors().element_disabled.opacity(0.6)) - }) - .when(!self.disabled, |this| { - this.group_hover(group_id.clone(), |el| el.border_color(hover_border_color)) - }) - .when(self.placeholder, |this| { - this.child( - div() - .flex_none() - .rounded_full() - .bg(color.color(cx).alpha(0.5)) - .size(self.render_cx.scaled_rems(0.25)), // was .size_1 - ) - }) - .children(icon), - ); - - h_flex() - .id(self.id) - .gap(ui::DynamicSpacing::Base06.rems(cx)) - .child(checkbox) - .when_some( - self.on_click.filter(|_| !self.disabled), - |this, on_click| { - this.on_click(move |_, window, cx| { - on_click(&self.toggle_state.inverse(), window, cx) - }) - }, - ) - // TODO: Allow label size to be different from default. - // TODO: Allow label color to be different from muted. - .when_some(self.label, |this, label| { - this.child(Label::new(label).color(Color::Muted)) - }) - .when_some(self.tooltip, |this, tooltip| { - this.tooltip(move |window, cx| tooltip(window, cx)) - }) - } -} - -fn calculate_table_columns_count(rows: &Vec) -> usize { - let mut actual_column_count = 0; - for row in rows { - actual_column_count = actual_column_count.max( - row.columns - .iter() - .map(|column| column.col_span) - .sum::(), - ); - } - actual_column_count -} - -fn render_markdown_table(parsed: &ParsedMarkdownTable, cx: &mut RenderContext) -> AnyElement { - let actual_header_column_count = calculate_table_columns_count(&parsed.header); - let actual_body_column_count = calculate_table_columns_count(&parsed.body); - let max_column_count = std::cmp::max(actual_header_column_count, actual_body_column_count); - - let total_rows = parsed.header.len() + parsed.body.len(); - - // Track which grid cells are occupied by spanning cells - let mut grid_occupied = vec![vec![false; max_column_count]; total_rows]; - - let mut cells = Vec::with_capacity(total_rows * max_column_count); - - for (row_idx, row) in parsed.header.iter().chain(parsed.body.iter()).enumerate() { - let mut col_idx = 0; - - for cell in row.columns.iter() { - // Skip columns occupied by row-spanning cells from previous rows - while col_idx < max_column_count && grid_occupied[row_idx][col_idx] { - col_idx += 1; - } - - if col_idx >= max_column_count { - break; - } - - let container = match cell.alignment { - ParsedMarkdownTableAlignment::Left | ParsedMarkdownTableAlignment::None => div(), - ParsedMarkdownTableAlignment::Center => v_flex().items_center(), - ParsedMarkdownTableAlignment::Right => v_flex().items_end(), - }; - - let cell_element = container - .col_span(cell.col_span.min(max_column_count - col_idx) as u16) - .row_span(cell.row_span.min(total_rows - row_idx) as u16) - .children(render_markdown_text(&cell.children, cx)) - .px_2() - .py_1() - .border_1() - .border_color(cx.border_color) - .when(cell.is_header, |this| { - this.bg(cx.title_bar_background_color) - }) - .when(cell.row_span > 1, |this| this.justify_center()) - .when(row_idx % 2 == 1, |this| this.bg(cx.panel_background_color)); - - cells.push(cell_element); - - // Mark grid positions as occupied for row-spanning cells - for r in 0..cell.row_span { - for c in 0..cell.col_span { - if row_idx + r < total_rows && col_idx + c < max_column_count { - grid_occupied[row_idx + r][col_idx + c] = true; - } - } - } - - col_idx += cell.col_span; - } - - // Fill remaining columns with empty cells if needed - while col_idx < max_column_count { - if grid_occupied[row_idx][col_idx] { - col_idx += 1; - continue; - } - - let empty_cell = div() - .border_1() - .border_color(cx.border_color) - .when(row_idx % 2 == 1, |this| this.bg(cx.panel_background_color)); - - cells.push(empty_cell); - col_idx += 1; - } - } - - cx.with_common_p(v_flex().items_start()) - .when_some(parsed.caption.as_ref(), |this, caption| { - this.children(render_markdown_text(caption, cx)) - }) - .child( - div() - .grid() - .grid_cols(max_column_count as u16) - .border_1() - .border_color(cx.border_color) - .children(cells), - ) - .into_any() -} - -fn render_markdown_block_quote( - parsed: &ParsedMarkdownBlockQuote, - cx: &mut RenderContext, -) -> AnyElement { - cx.indent += 1; - - let children: Vec = parsed - .children - .iter() - .enumerate() - .map(|(ix, child)| { - cx.with_last_child(ix + 1 == parsed.children.len(), |cx| { - render_markdown_block(child, cx) - }) - }) - .collect(); - - cx.indent -= 1; - - cx.with_common_p(div()) - .child( - div() - .border_l_4() - .border_color(cx.border_color) - .pl_3() - .children(children), - ) - .into_any() -} - -fn render_markdown_code_block( - parsed: &ParsedMarkdownCodeBlock, - cx: &mut RenderContext, -) -> AnyElement { - let body = if let Some(highlights) = parsed.highlights.as_ref() { - StyledText::new(parsed.contents.clone()).with_default_highlights( - &cx.buffer_text_style, - highlights.iter().filter_map(|(range, highlight_id)| { - highlight_id - .style(cx.syntax_theme.as_ref()) - .map(|style| (range.clone(), style)) - }), - ) - } else { - StyledText::new(parsed.contents.clone()) - }; - - let copy_block_button = IconButton::new("copy-code", IconName::Copy) - .icon_size(IconSize::Small) - .on_click({ - let contents = parsed.contents.clone(); - move |_, _window, cx| { - cx.write_to_clipboard(ClipboardItem::new_string(contents.to_string())); - } - }) - .tooltip(Tooltip::text("Copy code block")) - .visible_on_hover("markdown-block"); - - let font = gpui::Font { - family: cx.buffer_font_family.clone(), - features: cx.buffer_text_style.font_features.clone(), - ..Default::default() - }; - - cx.with_common_p(div()) - .font(font) - .px_3() - .py_3() - .bg(cx.code_block_background_color) - .rounded_sm() - .child(body) - .child( - div() - .h_flex() - .absolute() - .right_1() - .top_1() - .child(copy_block_button), - ) - .into_any() -} - -fn render_markdown_paragraph(parsed: &MarkdownParagraph, cx: &mut RenderContext) -> AnyElement { - cx.with_common_p(div()) - .children(render_markdown_text(parsed, cx)) - .flex() - .flex_col() - .into_any_element() -} - -fn render_markdown_text(parsed_new: &MarkdownParagraph, cx: &mut RenderContext) -> Vec { - let mut any_element = Vec::with_capacity(parsed_new.len()); - // these values are cloned in-order satisfy borrow checker - let syntax_theme = cx.syntax_theme.clone(); - let workspace_clone = cx.workspace.clone(); - let code_span_bg_color = cx.code_span_background_color; - let text_style = cx.text_style.clone(); - let link_color = cx.link_color; - - for parsed_region in parsed_new { - match parsed_region { - MarkdownParagraphChunk::Text(parsed) => { - let element_id = cx.next_id(&parsed.source_range); - - let highlights = gpui::combine_highlights( - parsed.highlights.iter().filter_map(|(range, highlight)| { - highlight - .to_highlight_style(&syntax_theme) - .map(|style| (range.clone(), style)) - }), - parsed.regions.iter().filter_map(|(range, region)| { - if region.code { - Some(( - range.clone(), - HighlightStyle { - background_color: Some(code_span_bg_color), - ..Default::default() - }, - )) - } else if region.link.is_some() { - Some(( - range.clone(), - HighlightStyle { - color: Some(link_color), - ..Default::default() - }, - )) - } else { - None - } - }), - ); - let mut links = Vec::new(); - let mut link_ranges = Vec::new(); - for (range, region) in parsed.regions.iter() { - if let Some(link) = region.link.clone() { - links.push(link); - link_ranges.push(range.clone()); - } - } - let workspace = workspace_clone.clone(); - let element = div() - .child( - InteractiveText::new( - element_id, - StyledText::new(parsed.contents.clone()) - .with_default_highlights(&text_style, highlights), - ) - .tooltip({ - let links = links.clone(); - let link_ranges = link_ranges.clone(); - move |idx, _, cx| { - for (ix, range) in link_ranges.iter().enumerate() { - if range.contains(&idx) { - return Some(LinkPreview::new(&links[ix].to_string(), cx)); - } - } - None - } - }) - .on_click( - link_ranges, - move |clicked_range_ix, window, cx| match &links[clicked_range_ix] { - Link::Web { url } => cx.open_url(url), - Link::Path { path, .. } => { - if let Some(workspace) = &workspace { - _ = workspace.update(cx, |workspace, cx| { - workspace - .open_abs_path( - normalize_path(path.clone().as_path()), - OpenOptions { - visible: Some(OpenVisible::None), - ..Default::default() - }, - window, - cx, - ) - .detach(); - }); - } - } - }, - ), - ) - .into_any(); - any_element.push(element); - } - - MarkdownParagraphChunk::Image(image) => { - any_element.push(render_markdown_image(image, cx)); - } - } - } - - any_element -} - -fn render_markdown_rule(cx: &mut RenderContext) -> AnyElement { - let rule = div().w_full().h(cx.scaled_rems(0.125)).bg(cx.border_color); - div().py(cx.scaled_rems(0.5)).child(rule).into_any() -} - -fn render_markdown_image(image: &Image, cx: &mut RenderContext) -> AnyElement { - let image_resource = match image.link.clone() { - Link::Web { url } => Resource::Uri(url.into()), - Link::Path { path, .. } => Resource::Path(Arc::from(path)), - }; - - let element_id = cx.next_id(&image.source_range); - let workspace = cx.workspace.clone(); - - div() - .id(element_id) - .cursor_pointer() - .child( - img(ImageSource::Resource(image_resource)) - .max_w_full() - .with_fallback({ - let alt_text = image.alt_text.clone(); - move || div().children(alt_text.clone()).into_any_element() - }) - .when_some(image.height, |this, height| this.h(height)) - .when_some(image.width, |this, width| this.w(width)), - ) - .tooltip({ - let link = image.link.clone(); - let alt_text = image.alt_text.clone(); - move |_, cx| { - InteractiveMarkdownElementTooltip::new( - Some(alt_text.clone().unwrap_or(link.to_string().into())), - "open image", - cx, - ) - .into() - } - }) - .on_click({ - let link = image.link.clone(); - move |_, window, cx| { - if window.modifiers().secondary() { - match &link { - Link::Web { url } => cx.open_url(url), - Link::Path { path, .. } => { - if let Some(workspace) = &workspace { - _ = workspace.update(cx, |workspace, cx| { - workspace - .open_abs_path( - path.clone(), - OpenOptions { - visible: Some(OpenVisible::None), - ..Default::default() - }, - window, - cx, - ) - .detach(); - }); - } - } - } - } - } - }) - .into_any() -} - -struct InteractiveMarkdownElementTooltip { - tooltip_text: Option, - action_text: SharedString, -} - -impl InteractiveMarkdownElementTooltip { - pub fn new( - tooltip_text: Option, - action_text: impl Into, - cx: &mut App, - ) -> Entity { - let tooltip_text = tooltip_text.map(|t| util::truncate_and_trailoff(&t, 50).into()); - - cx.new(|_cx| Self { - tooltip_text, - action_text: action_text.into(), - }) - } -} - -impl Render for InteractiveMarkdownElementTooltip { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - tooltip_container(cx, |el, _| { - let secondary_modifier = Keystroke { - modifiers: Modifiers::secondary_key(), - ..Default::default() - }; - - el.child( - v_flex() - .gap_1() - .when_some(self.tooltip_text.clone(), |this, text| { - this.child(Label::new(text).size(LabelSize::Small)) - }) - .child( - Label::new(format!( - "{}-click to {}", - secondary_modifier, self.action_text - )) - .size(LabelSize::Small) - .color(Color::Muted), - ), - ) - }) - } -} - -/// Returns the prefix for a list item. -fn list_item_prefix(order: usize, ordered: bool, depth: usize) -> String { - let ix = order.saturating_sub(1); - const NUMBERED_PREFIXES_1: &str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; - const NUMBERED_PREFIXES_2: &str = "abcdefghijklmnopqrstuvwxyz"; - const BULLETS: [&str; 5] = ["•", "◦", "▪", "‣", "⁃"]; - - if ordered { - match depth { - 0 => format!("{}. ", order), - 1 => format!( - "{}. ", - NUMBERED_PREFIXES_1 - .chars() - .nth(ix % NUMBERED_PREFIXES_1.len()) - .unwrap() - ), - _ => format!( - "{}. ", - NUMBERED_PREFIXES_2 - .chars() - .nth(ix % NUMBERED_PREFIXES_2.len()) - .unwrap() - ), - } - } else { - let depth = depth.min(BULLETS.len() - 1); - let bullet = BULLETS[depth]; - return format!("{} ", bullet); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::markdown_elements::ParsedMarkdownTableColumn; - use crate::markdown_elements::ParsedMarkdownText; - - fn text(text: &str) -> MarkdownParagraphChunk { - MarkdownParagraphChunk::Text(ParsedMarkdownText { - source_range: 0..text.len(), - contents: SharedString::new(text), - highlights: Default::default(), - regions: Default::default(), - }) - } - - fn column( - col_span: usize, - row_span: usize, - children: Vec, - ) -> ParsedMarkdownTableColumn { - ParsedMarkdownTableColumn { - col_span, - row_span, - is_header: false, - children, - alignment: ParsedMarkdownTableAlignment::None, - } - } - - fn column_with_row_span( - col_span: usize, - row_span: usize, - children: Vec, - ) -> ParsedMarkdownTableColumn { - ParsedMarkdownTableColumn { - col_span, - row_span, - is_header: false, - children, - alignment: ParsedMarkdownTableAlignment::None, - } - } - - #[test] - fn test_calculate_table_columns_count() { - assert_eq!(0, calculate_table_columns_count(&vec![])); - - assert_eq!( - 1, - calculate_table_columns_count(&vec![ParsedMarkdownTableRow::with_columns(vec![ - column(1, 1, vec![text("column1")]) - ])]) - ); - - assert_eq!( - 2, - calculate_table_columns_count(&vec![ParsedMarkdownTableRow::with_columns(vec![ - column(1, 1, vec![text("column1")]), - column(1, 1, vec![text("column2")]), - ])]) - ); - - assert_eq!( - 2, - calculate_table_columns_count(&vec![ParsedMarkdownTableRow::with_columns(vec![ - column(2, 1, vec![text("column1")]) - ])]) - ); - - assert_eq!( - 3, - calculate_table_columns_count(&vec![ParsedMarkdownTableRow::with_columns(vec![ - column(1, 1, vec![text("column1")]), - column(2, 1, vec![text("column2")]), - ])]) - ); - - assert_eq!( - 2, - calculate_table_columns_count(&vec![ - ParsedMarkdownTableRow::with_columns(vec![ - column(1, 1, vec![text("column1")]), - column(1, 1, vec![text("column2")]), - ]), - ParsedMarkdownTableRow::with_columns(vec![column(1, 1, vec![text("column1")]),]) - ]) - ); - - assert_eq!( - 3, - calculate_table_columns_count(&vec![ - ParsedMarkdownTableRow::with_columns(vec![ - column(1, 1, vec![text("column1")]), - column(1, 1, vec![text("column2")]), - ]), - ParsedMarkdownTableRow::with_columns(vec![column(3, 3, vec![text("column1")]),]) - ]) - ); - } - - #[test] - fn test_row_span_support() { - assert_eq!( - 3, - calculate_table_columns_count(&vec![ - ParsedMarkdownTableRow::with_columns(vec![ - column_with_row_span(1, 2, vec![text("spans 2 rows")]), - column(1, 1, vec![text("column2")]), - column(1, 1, vec![text("column3")]), - ]), - ParsedMarkdownTableRow::with_columns(vec![ - // First column is covered by row span from above - column(1, 1, vec![text("column2 row2")]), - column(1, 1, vec![text("column3 row2")]), - ]) - ]) - ); - - assert_eq!( - 4, - calculate_table_columns_count(&vec![ - ParsedMarkdownTableRow::with_columns(vec![ - column_with_row_span(1, 3, vec![text("spans 3 rows")]), - column_with_row_span(2, 1, vec![text("spans 2 cols")]), - column(1, 1, vec![text("column4")]), - ]), - ParsedMarkdownTableRow::with_columns(vec![ - // First column covered by row span - column(1, 1, vec![text("column2")]), - column(1, 1, vec![text("column3")]), - column(1, 1, vec![text("column4")]), - ]), - ParsedMarkdownTableRow::with_columns(vec![ - // First column still covered by row span - column(3, 1, vec![text("spans 3 cols")]), - ]) - ]) - ); - } - - #[test] - fn test_list_item_prefix() { - assert_eq!(list_item_prefix(1, true, 0), "1. "); - assert_eq!(list_item_prefix(2, true, 0), "2. "); - assert_eq!(list_item_prefix(3, true, 0), "3. "); - assert_eq!(list_item_prefix(11, true, 0), "11. "); - assert_eq!(list_item_prefix(1, true, 1), "A. "); - assert_eq!(list_item_prefix(2, true, 1), "B. "); - assert_eq!(list_item_prefix(3, true, 1), "C. "); - assert_eq!(list_item_prefix(1, true, 2), "a. "); - assert_eq!(list_item_prefix(2, true, 2), "b. "); - assert_eq!(list_item_prefix(7, true, 2), "g. "); - assert_eq!(list_item_prefix(1, true, 1), "A. "); - assert_eq!(list_item_prefix(1, true, 2), "a. "); - assert_eq!(list_item_prefix(1, false, 0), "• "); - assert_eq!(list_item_prefix(1, false, 1), "◦ "); - assert_eq!(list_item_prefix(1, false, 2), "▪ "); - assert_eq!(list_item_prefix(1, false, 3), "‣ "); - assert_eq!(list_item_prefix(1, false, 4), "⁃ "); - } -} diff --git a/crates/menu/Cargo.toml b/crates/menu/Cargo.toml deleted file mode 100644 index fcb209df88..0000000000 --- a/crates/menu/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "menu" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/menu.rs" -doctest = false - -[dependencies] -gpui.workspace = true diff --git a/crates/menu/LICENSE-GPL b/crates/menu/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/menu/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/menu/src/menu.rs b/crates/menu/src/menu.rs deleted file mode 100644 index 9a1937d100..0000000000 --- a/crates/menu/src/menu.rs +++ /dev/null @@ -1,33 +0,0 @@ -use gpui::actions; - -// If the zed binary doesn't use anything in this crate, it will be optimized away -// and the actions won't initialize. So we just provide an empty initialization function -// to be called from main. -// -// These may provide relevant context: -// https://github.com/rust-lang/rust/issues/47384 -// https://github.com/mmastrac/rust-ctor/issues/280 -pub fn init() {} - -actions!( - menu, - [ - /// Cancels the current menu operation. - Cancel, - /// Confirms the selected menu item. - Confirm, - /// Performs secondary confirmation action. - SecondaryConfirm, - /// Selects the previous item in the menu. - SelectPrevious, - /// Selects the next item in the menu. - SelectNext, - /// Selects the first item in the menu. - SelectFirst, - /// Selects the last item in the menu. - SelectLast, - /// Restarts the menu from the beginning. - Restart, - EndSlot, - ] -); diff --git a/crates/migrator/Cargo.toml b/crates/migrator/Cargo.toml deleted file mode 100644 index e0a7578474..0000000000 --- a/crates/migrator/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "migrator" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/migrator.rs" -doctest = false - -[dependencies] -anyhow.workspace = true -collections.workspace = true -convert_case.workspace = true -log.workspace = true -streaming-iterator.workspace = true -tree-sitter-json.workspace = true -tree-sitter.workspace = true -serde_json_lenient.workspace = true -serde_json.workspace = true -settings_json.workspace = true - -[dev-dependencies] -pretty_assertions.workspace = true -unindent.workspace = true diff --git a/crates/migrator/LICENSE-GPL b/crates/migrator/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/migrator/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/migrator/src/migrations.rs b/crates/migrator/src/migrations.rs deleted file mode 100644 index 398d5aaf94..0000000000 --- a/crates/migrator/src/migrations.rs +++ /dev/null @@ -1,161 +0,0 @@ -pub(crate) mod m_2025_01_02 { - mod settings; - - pub(crate) use settings::SETTINGS_PATTERNS; -} - -pub(crate) mod m_2025_01_29 { - mod keymap; - mod settings; - - pub(crate) use keymap::KEYMAP_PATTERNS; - pub(crate) use settings::{SETTINGS_PATTERNS, replace_edit_prediction_provider_setting}; -} - -pub(crate) mod m_2025_01_30 { - mod keymap; - mod settings; - - pub(crate) use keymap::KEYMAP_PATTERNS; - pub(crate) use settings::SETTINGS_PATTERNS; -} - -pub(crate) mod m_2025_03_03 { - mod keymap; - - pub(crate) use keymap::KEYMAP_PATTERNS; -} - -pub(crate) mod m_2025_03_06 { - mod keymap; - - pub(crate) use keymap::KEYMAP_PATTERNS; -} - -pub(crate) mod m_2025_03_29 { - mod settings; - - pub(crate) use settings::SETTINGS_PATTERNS; -} - -pub(crate) mod m_2025_04_15 { - mod keymap; - mod settings; - - pub(crate) use keymap::KEYMAP_PATTERNS; - pub(crate) use settings::SETTINGS_PATTERNS; -} - -pub(crate) mod m_2025_04_21 { - mod settings; - - pub(crate) use settings::SETTINGS_PATTERNS; -} - -pub(crate) mod m_2025_04_23 { - mod settings; - - pub(crate) use settings::SETTINGS_PATTERNS; -} - -pub(crate) mod m_2025_05_05 { - mod settings; - - pub(crate) use settings::SETTINGS_PATTERNS; -} - -pub(crate) mod m_2025_05_08 { - mod settings; - - pub(crate) use settings::SETTINGS_PATTERNS; -} - -pub(crate) mod m_2025_05_29 { - mod settings; - - pub(crate) use settings::SETTINGS_PATTERNS; -} - -pub(crate) mod m_2025_06_16 { - mod settings; - - pub(crate) use settings::SETTINGS_PATTERNS; -} - -pub(crate) mod m_2025_06_25 { - mod settings; - - pub(crate) use settings::SETTINGS_PATTERNS; -} - -pub(crate) mod m_2025_06_27 { - mod settings; - - pub(crate) use settings::SETTINGS_PATTERNS; -} - -pub(crate) mod m_2025_07_08 { - mod settings; - - pub(crate) use settings::SETTINGS_PATTERNS; -} - -pub(crate) mod m_2025_10_01 { - mod settings; - - pub(crate) use settings::flatten_code_actions_formatters; -} - -pub(crate) mod m_2025_10_02 { - mod settings; - - pub(crate) use settings::remove_formatters_on_save; -} - -pub(crate) mod m_2025_10_03 { - mod settings; - - pub(crate) use settings::SETTINGS_PATTERNS; -} - -pub(crate) mod m_2025_10_16 { - mod settings; - - pub(crate) use settings::restore_code_actions_on_format; -} - -pub(crate) mod m_2025_10_17 { - mod settings; - - pub(crate) use settings::make_file_finder_include_ignored_an_enum; -} - -pub(crate) mod m_2025_10_21 { - mod settings; - - pub(crate) use settings::make_relative_line_numbers_an_enum; -} - -pub(crate) mod m_2025_11_12 { - mod settings; - - pub(crate) use settings::SETTINGS_PATTERNS; -} - -pub(crate) mod m_2025_11_20 { - mod settings; - - pub(crate) use settings::SETTINGS_PATTERNS; -} - -pub(crate) mod m_2025_11_25 { - mod settings; - - pub(crate) use settings::remove_context_server_source; -} - -pub(crate) mod m_2025_12_01 { - mod settings; - - pub(crate) use settings::SETTINGS_PATTERNS; -} diff --git a/crates/migrator/src/migrations/m_2025_01_02/settings.rs b/crates/migrator/src/migrations/m_2025_01_02/settings.rs deleted file mode 100644 index a35b1ebd2e..0000000000 --- a/crates/migrator/src/migrations/m_2025_01_02/settings.rs +++ /dev/null @@ -1,62 +0,0 @@ -use collections::HashMap; -use std::{ops::Range, sync::LazyLock}; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN; - -pub const SETTINGS_PATTERNS: MigrationPatterns = &[( - SETTINGS_NESTED_KEY_VALUE_PATTERN, - replace_deprecated_settings_values, -)]; - -fn replace_deprecated_settings_values( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let parent_object_capture_ix = query.capture_index_for_name("parent_key")?; - let parent_object_range = mat - .nodes_for_capture_index(parent_object_capture_ix) - .next()? - .byte_range(); - let parent_object_name = contents.get(parent_object_range)?; - - let setting_name_ix = query.capture_index_for_name("setting_name")?; - let setting_name_range = mat - .nodes_for_capture_index(setting_name_ix) - .next()? - .byte_range(); - let setting_name = contents.get(setting_name_range)?; - - let setting_value_ix = query.capture_index_for_name("setting_value")?; - let setting_value_range = mat - .nodes_for_capture_index(setting_value_ix) - .next()? - .byte_range(); - let setting_value = contents.get(setting_value_range.clone())?; - - UPDATED_SETTINGS - .get(&(parent_object_name, setting_name)) - .and_then(|new_values| { - new_values - .iter() - .find_map(|(old_value, new_value)| { - (*old_value == setting_value).then(|| new_value.to_string()) - }) - .map(|new_value| (setting_value_range, new_value)) - }) -} - -static UPDATED_SETTINGS: LazyLock>> = LazyLock::new(|| { - HashMap::from_iter([ - ( - ("chat_panel", "button"), - vec![("true", "\"always\""), ("false", "\"never\"")], - ), - ( - ("scrollbar", "diagnostics"), - vec![("true", "\"all\""), ("false", "\"none\"")], - ), - ]) -}); diff --git a/crates/migrator/src/migrations/m_2025_01_29/keymap.rs b/crates/migrator/src/migrations/m_2025_01_29/keymap.rs deleted file mode 100644 index 222ad9716b..0000000000 --- a/crates/migrator/src/migrations/m_2025_01_29/keymap.rs +++ /dev/null @@ -1,307 +0,0 @@ -use collections::HashMap; -use std::{ops::Range, sync::LazyLock}; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::{ - KEYMAP_ACTION_ARRAY_ARGUMENT_AS_OBJECT_PATTERN, KEYMAP_ACTION_ARRAY_PATTERN, - KEYMAP_ACTION_STRING_PATTERN, KEYMAP_CONTEXT_PATTERN, -}; - -pub const KEYMAP_PATTERNS: MigrationPatterns = &[ - ( - KEYMAP_ACTION_ARRAY_PATTERN, - replace_array_with_single_string, - ), - ( - KEYMAP_ACTION_ARRAY_ARGUMENT_AS_OBJECT_PATTERN, - replace_action_argument_object_with_single_value, - ), - (KEYMAP_ACTION_STRING_PATTERN, replace_string_action), - (KEYMAP_CONTEXT_PATTERN, rename_context_key), -]; - -fn replace_array_with_single_string( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let array_ix = query.capture_index_for_name("array")?; - let action_name_ix = query.capture_index_for_name("action_name")?; - let argument_ix = query.capture_index_for_name("argument")?; - - let action_name = contents.get( - mat.nodes_for_capture_index(action_name_ix) - .next()? - .byte_range(), - )?; - let argument = contents.get( - mat.nodes_for_capture_index(argument_ix) - .next()? - .byte_range(), - )?; - - let replacement = TRANSFORM_ARRAY.get(&(action_name, argument))?; - let replacement_as_string = format!("\"{replacement}\""); - let range_to_replace = mat.nodes_for_capture_index(array_ix).next()?.byte_range(); - - Some((range_to_replace, replacement_as_string)) -} - -static TRANSFORM_ARRAY: LazyLock> = LazyLock::new(|| { - HashMap::from_iter([ - // activate - ( - ("workspace::ActivatePaneInDirection", "Up"), - "workspace::ActivatePaneUp", - ), - ( - ("workspace::ActivatePaneInDirection", "Down"), - "workspace::ActivatePaneDown", - ), - ( - ("workspace::ActivatePaneInDirection", "Left"), - "workspace::ActivatePaneLeft", - ), - ( - ("workspace::ActivatePaneInDirection", "Right"), - "workspace::ActivatePaneRight", - ), - // swap - ( - ("workspace::SwapPaneInDirection", "Up"), - "workspace::SwapPaneUp", - ), - ( - ("workspace::SwapPaneInDirection", "Down"), - "workspace::SwapPaneDown", - ), - ( - ("workspace::SwapPaneInDirection", "Left"), - "workspace::SwapPaneLeft", - ), - ( - ("workspace::SwapPaneInDirection", "Right"), - "workspace::SwapPaneRight", - ), - // menu - ( - ("app_menu::NavigateApplicationMenuInDirection", "Left"), - "app_menu::ActivateMenuLeft", - ), - ( - ("app_menu::NavigateApplicationMenuInDirection", "Right"), - "app_menu::ActivateMenuRight", - ), - // vim push - (("vim::PushOperator", "Change"), "vim::PushChange"), - (("vim::PushOperator", "Delete"), "vim::PushDelete"), - (("vim::PushOperator", "Yank"), "vim::PushYank"), - (("vim::PushOperator", "Replace"), "vim::PushReplace"), - ( - ("vim::PushOperator", "DeleteSurrounds"), - "vim::PushDeleteSurrounds", - ), - (("vim::PushOperator", "Mark"), "vim::PushMark"), - (("vim::PushOperator", "Indent"), "vim::PushIndent"), - (("vim::PushOperator", "Outdent"), "vim::PushOutdent"), - (("vim::PushOperator", "AutoIndent"), "vim::PushAutoIndent"), - (("vim::PushOperator", "Rewrap"), "vim::PushRewrap"), - ( - ("vim::PushOperator", "ShellCommand"), - "vim::PushShellCommand", - ), - (("vim::PushOperator", "Lowercase"), "vim::PushLowercase"), - (("vim::PushOperator", "Uppercase"), "vim::PushUppercase"), - ( - ("vim::PushOperator", "OppositeCase"), - "vim::PushOppositeCase", - ), - (("vim::PushOperator", "Register"), "vim::PushRegister"), - ( - ("vim::PushOperator", "RecordRegister"), - "vim::PushRecordRegister", - ), - ( - ("vim::PushOperator", "ReplayRegister"), - "vim::PushReplayRegister", - ), - ( - ("vim::PushOperator", "ReplaceWithRegister"), - "vim::PushReplaceWithRegister", - ), - ( - ("vim::PushOperator", "ToggleComments"), - "vim::PushToggleComments", - ), - // vim switch - (("vim::SwitchMode", "Normal"), "vim::SwitchToNormalMode"), - (("vim::SwitchMode", "Insert"), "vim::SwitchToInsertMode"), - (("vim::SwitchMode", "Replace"), "vim::SwitchToReplaceMode"), - (("vim::SwitchMode", "Visual"), "vim::SwitchToVisualMode"), - ( - ("vim::SwitchMode", "VisualLine"), - "vim::SwitchToVisualLineMode", - ), - ( - ("vim::SwitchMode", "VisualBlock"), - "vim::SwitchToVisualBlockMode", - ), - ( - ("vim::SwitchMode", "HelixNormal"), - "vim::SwitchToHelixNormalMode", - ), - // vim resize - (("vim::ResizePane", "Widen"), "vim::ResizePaneRight"), - (("vim::ResizePane", "Narrow"), "vim::ResizePaneLeft"), - (("vim::ResizePane", "Shorten"), "vim::ResizePaneDown"), - (("vim::ResizePane", "Lengthen"), "vim::ResizePaneUp"), - // fold at level - (("editor::FoldAtLevel", "1"), "editor::FoldAtLevel1"), - (("editor::FoldAtLevel", "2"), "editor::FoldAtLevel2"), - (("editor::FoldAtLevel", "3"), "editor::FoldAtLevel3"), - (("editor::FoldAtLevel", "4"), "editor::FoldAtLevel4"), - (("editor::FoldAtLevel", "5"), "editor::FoldAtLevel5"), - (("editor::FoldAtLevel", "6"), "editor::FoldAtLevel6"), - (("editor::FoldAtLevel", "7"), "editor::FoldAtLevel7"), - (("editor::FoldAtLevel", "8"), "editor::FoldAtLevel8"), - (("editor::FoldAtLevel", "9"), "editor::FoldAtLevel9"), - ]) -}); - -/// [ "editor::FoldAtLevel", { "level": 1 } ] -> [ "editor::FoldAtLevel", 1 ] -fn replace_action_argument_object_with_single_value( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let array_ix = query.capture_index_for_name("array")?; - let action_name_ix = query.capture_index_for_name("action_name")?; - let argument_key_ix = query.capture_index_for_name("argument_key")?; - let argument_value_ix = query.capture_index_for_name("argument_value")?; - - let action_name = contents.get( - mat.nodes_for_capture_index(action_name_ix) - .next()? - .byte_range(), - )?; - let argument_key = contents.get( - mat.nodes_for_capture_index(argument_key_ix) - .next()? - .byte_range(), - )?; - let argument_value = contents.get( - mat.nodes_for_capture_index(argument_value_ix) - .next()? - .byte_range(), - )?; - - let new_action_name = UNWRAP_OBJECTS.get(&action_name)?.get(&argument_key)?; - - let range_to_replace = mat.nodes_for_capture_index(array_ix).next()?.byte_range(); - let replacement = format!("[\"{}\", {}]", new_action_name, argument_value); - Some((range_to_replace, replacement)) -} - -/// "ctrl-k ctrl-1": [ "editor::PushOperator", { "Object": {} } ] -> [ "editor::vim::PushObject", {} ] -static UNWRAP_OBJECTS: LazyLock>> = LazyLock::new(|| { - HashMap::from_iter([ - ( - "editor::FoldAtLevel", - HashMap::from_iter([("level", "editor::FoldAtLevel")]), - ), - ( - "vim::PushOperator", - HashMap::from_iter([ - ("Object", "vim::PushObject"), - ("FindForward", "vim::PushFindForward"), - ("FindBackward", "vim::PushFindBackward"), - ("Sneak", "vim::PushSneak"), - ("SneakBackward", "vim::PushSneakBackward"), - ("AddSurrounds", "vim::PushAddSurrounds"), - ("ChangeSurrounds", "vim::PushChangeSurrounds"), - ("Jump", "vim::PushJump"), - ("Digraph", "vim::PushDigraph"), - ("Literal", "vim::PushLiteral"), - ]), - ), - ]) -}); - -fn replace_string_action( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let action_name_ix = query.capture_index_for_name("action_name")?; - let action_name_node = mat.nodes_for_capture_index(action_name_ix).next()?; - let action_name_range = action_name_node.byte_range(); - let action_name = contents.get(action_name_range.clone())?; - - if let Some(new_action_name) = STRING_REPLACE.get(&action_name) { - return Some((action_name_range, new_action_name.to_string())); - } - - None -} - -/// "ctrl-k ctrl-1": "inline_completion::ToggleMenu" -> "edit_prediction::ToggleMenu" -static STRING_REPLACE: LazyLock> = LazyLock::new(|| { - HashMap::from_iter([ - ( - "inline_completion::ToggleMenu", - "edit_prediction::ToggleMenu", - ), - ("editor::NextInlineCompletion", "editor::NextEditPrediction"), - ( - "editor::PreviousInlineCompletion", - "editor::PreviousEditPrediction", - ), - ( - "editor::AcceptPartialInlineCompletion", - "editor::AcceptPartialEditPrediction", - ), - ("editor::ShowInlineCompletion", "editor::ShowEditPrediction"), - ( - "editor::AcceptInlineCompletion", - "editor::AcceptEditPrediction", - ), - ( - "editor::ToggleInlineCompletions", - "editor::ToggleEditPrediction", - ), - ]) -}); - -fn rename_context_key( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let context_predicate_ix = query.capture_index_for_name("context_predicate")?; - let context_predicate_range = mat - .nodes_for_capture_index(context_predicate_ix) - .next()? - .byte_range(); - let old_predicate = contents.get(context_predicate_range.clone())?.to_string(); - let mut new_predicate = old_predicate.to_string(); - for (old_key, new_key) in CONTEXT_REPLACE.iter() { - new_predicate = new_predicate.replace(old_key, new_key); - } - if new_predicate != old_predicate { - Some((context_predicate_range, new_predicate)) - } else { - None - } -} - -/// "context": "Editor && inline_completion && !showing_completions" -> "Editor && edit_prediction && !showing_completions" -pub static CONTEXT_REPLACE: LazyLock> = LazyLock::new(|| { - HashMap::from_iter([ - ("inline_completion", "edit_prediction"), - ( - "inline_completion_requires_modifier", - "edit_prediction_requires_modifier", - ), - ]) -}); diff --git a/crates/migrator/src/migrations/m_2025_01_29/settings.rs b/crates/migrator/src/migrations/m_2025_01_29/settings.rs deleted file mode 100644 index 46cfe2f178..0000000000 --- a/crates/migrator/src/migrations/m_2025_01_29/settings.rs +++ /dev/null @@ -1,101 +0,0 @@ -use collections::HashMap; -use std::{ops::Range, sync::LazyLock}; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::{ - SETTINGS_LANGUAGES_PATTERN, SETTINGS_NESTED_KEY_VALUE_PATTERN, SETTINGS_ROOT_KEY_VALUE_PATTERN, -}; - -pub const SETTINGS_PATTERNS: MigrationPatterns = &[ - (SETTINGS_ROOT_KEY_VALUE_PATTERN, replace_setting_name), - ( - SETTINGS_NESTED_KEY_VALUE_PATTERN, - replace_edit_prediction_provider_setting, - ), - (SETTINGS_LANGUAGES_PATTERN, replace_setting_in_languages), -]; - -fn replace_setting_name( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let setting_capture_ix = query.capture_index_for_name("name")?; - let setting_name_range = mat - .nodes_for_capture_index(setting_capture_ix) - .next()? - .byte_range(); - let setting_name = contents.get(setting_name_range.clone())?; - let new_setting_name = SETTINGS_STRING_REPLACE.get(&setting_name)?; - Some((setting_name_range, new_setting_name.to_string())) -} - -pub static SETTINGS_STRING_REPLACE: LazyLock> = - LazyLock::new(|| { - HashMap::from_iter([ - ( - "show_inline_completions_in_menu", - "show_edit_predictions_in_menu", - ), - ("show_inline_completions", "show_edit_predictions"), - ( - "inline_completions_disabled_in", - "edit_predictions_disabled_in", - ), - ("inline_completions", "edit_predictions"), - ]) - }); - -pub fn replace_edit_prediction_provider_setting( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let parent_object_capture_ix = query.capture_index_for_name("parent_key")?; - let parent_object_range = mat - .nodes_for_capture_index(parent_object_capture_ix) - .next()? - .byte_range(); - let parent_object_name = contents.get(parent_object_range)?; - - let setting_name_ix = query.capture_index_for_name("setting_name")?; - let setting_range = mat - .nodes_for_capture_index(setting_name_ix) - .next()? - .byte_range(); - let setting_name = contents.get(setting_range.clone())?; - - if parent_object_name == "features" && setting_name == "inline_completion_provider" { - return Some((setting_range, "edit_prediction_provider".into())); - } - - None -} - -fn replace_setting_in_languages( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let setting_capture_ix = query.capture_index_for_name("setting_name")?; - let setting_name_range = mat - .nodes_for_capture_index(setting_capture_ix) - .next()? - .byte_range(); - let setting_name = contents.get(setting_name_range.clone())?; - let new_setting_name = LANGUAGE_SETTINGS_REPLACE.get(&setting_name)?; - - Some((setting_name_range, new_setting_name.to_string())) -} - -static LANGUAGE_SETTINGS_REPLACE: LazyLock> = - LazyLock::new(|| { - HashMap::from_iter([ - ("show_inline_completions", "show_edit_predictions"), - ( - "inline_completions_disabled_in", - "edit_predictions_disabled_in", - ), - ]) - }); diff --git a/crates/migrator/src/migrations/m_2025_01_30/keymap.rs b/crates/migrator/src/migrations/m_2025_01_30/keymap.rs deleted file mode 100644 index 0e131fea02..0000000000 --- a/crates/migrator/src/migrations/m_2025_01_30/keymap.rs +++ /dev/null @@ -1,82 +0,0 @@ -use collections::HashMap; -use convert_case::{Case, Casing}; -use std::{ops::Range, sync::LazyLock}; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::KEYMAP_ACTION_ARRAY_ARGUMENT_AS_OBJECT_PATTERN; - -pub const KEYMAP_PATTERNS: MigrationPatterns = &[( - KEYMAP_ACTION_ARRAY_ARGUMENT_AS_OBJECT_PATTERN, - action_argument_snake_case, -)]; - -fn to_snake_case(text: &str) -> String { - text.to_case(Case::Snake) -} - -fn action_argument_snake_case( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let array_ix = query.capture_index_for_name("array")?; - let action_name_ix = query.capture_index_for_name("action_name")?; - let argument_key_ix = query.capture_index_for_name("argument_key")?; - let argument_value_ix = query.capture_index_for_name("argument_value")?; - let action_name = contents.get( - mat.nodes_for_capture_index(action_name_ix) - .next()? - .byte_range(), - )?; - - let replacement_key = ACTION_ARGUMENT_SNAKE_CASE_REPLACE.get(action_name)?; - let argument_key = contents.get( - mat.nodes_for_capture_index(argument_key_ix) - .next()? - .byte_range(), - )?; - - if argument_key != *replacement_key { - return None; - } - - let argument_value_node = mat.nodes_for_capture_index(argument_value_ix).next()?; - let argument_value = contents.get(argument_value_node.byte_range())?; - - let new_key = to_snake_case(argument_key); - let new_value = if argument_value_node.kind() == "string" { - format!("\"{}\"", to_snake_case(argument_value.trim_matches('"'))) - } else { - argument_value.to_string() - }; - - let range_to_replace = mat.nodes_for_capture_index(array_ix).next()?.byte_range(); - let replacement = format!( - "[\"{}\", {{ \"{}\": {} }}]", - action_name, new_key, new_value - ); - - Some((range_to_replace, replacement)) -} - -static ACTION_ARGUMENT_SNAKE_CASE_REPLACE: LazyLock> = LazyLock::new(|| { - HashMap::from_iter([ - ("vim::NextWordStart", "ignorePunctuation"), - ("vim::NextWordEnd", "ignorePunctuation"), - ("vim::PreviousWordStart", "ignorePunctuation"), - ("vim::PreviousWordEnd", "ignorePunctuation"), - ("vim::MoveToNext", "partialWord"), - ("vim::MoveToPrev", "partialWord"), - ("vim::Down", "displayLines"), - ("vim::Up", "displayLines"), - ("vim::EndOfLine", "displayLines"), - ("vim::StartOfLine", "displayLines"), - ("vim::FirstNonWhitespace", "displayLines"), - ("pane::CloseActiveItem", "saveIntent"), - ("vim::Paste", "preserveClipboard"), - ("vim::Word", "ignorePunctuation"), - ("vim::Subword", "ignorePunctuation"), - ("vim::IndentObj", "includeBelow"), - ]) -}); diff --git a/crates/migrator/src/migrations/m_2025_01_30/settings.rs b/crates/migrator/src/migrations/m_2025_01_30/settings.rs deleted file mode 100644 index 2d763e4722..0000000000 --- a/crates/migrator/src/migrations/m_2025_01_30/settings.rs +++ /dev/null @@ -1,83 +0,0 @@ -use std::ops::Range; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN; - -pub const SETTINGS_PATTERNS: MigrationPatterns = &[ - ( - SETTINGS_NESTED_KEY_VALUE_PATTERN, - replace_tab_close_button_setting_key, - ), - ( - SETTINGS_NESTED_KEY_VALUE_PATTERN, - replace_tab_close_button_setting_value, - ), -]; - -fn replace_tab_close_button_setting_key( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let parent_object_capture_ix = query.capture_index_for_name("parent_key")?; - let parent_object_range = mat - .nodes_for_capture_index(parent_object_capture_ix) - .next()? - .byte_range(); - let parent_object_name = contents.get(parent_object_range)?; - - let setting_name_ix = query.capture_index_for_name("setting_name")?; - let setting_range = mat - .nodes_for_capture_index(setting_name_ix) - .next()? - .byte_range(); - let setting_name = contents.get(setting_range.clone())?; - - if parent_object_name == "tabs" && setting_name == "always_show_close_button" { - return Some((setting_range, "show_close_button".into())); - } - - None -} - -fn replace_tab_close_button_setting_value( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let parent_object_capture_ix = query.capture_index_for_name("parent_key")?; - let parent_object_range = mat - .nodes_for_capture_index(parent_object_capture_ix) - .next()? - .byte_range(); - let parent_object_name = contents.get(parent_object_range)?; - - let setting_name_ix = query.capture_index_for_name("setting_name")?; - let setting_name_range = mat - .nodes_for_capture_index(setting_name_ix) - .next()? - .byte_range(); - let setting_name = contents.get(setting_name_range)?; - - let setting_value_ix = query.capture_index_for_name("setting_value")?; - let setting_value_range = mat - .nodes_for_capture_index(setting_value_ix) - .next()? - .byte_range(); - let setting_value = contents.get(setting_value_range.clone())?; - - if parent_object_name == "tabs" && setting_name == "always_show_close_button" { - match setting_value { - "true" => { - return Some((setting_value_range, "\"always\"".to_string())); - } - "false" => { - return Some((setting_value_range, "\"hover\"".to_string())); - } - _ => {} - } - } - - None -} diff --git a/crates/migrator/src/migrations/m_2025_03_03/keymap.rs b/crates/migrator/src/migrations/m_2025_03_03/keymap.rs deleted file mode 100644 index 2342a8f696..0000000000 --- a/crates/migrator/src/migrations/m_2025_03_03/keymap.rs +++ /dev/null @@ -1,75 +0,0 @@ -use collections::HashMap; -use std::{ops::Range, sync::LazyLock}; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::KEYMAP_ACTION_STRING_PATTERN; - -pub const KEYMAP_PATTERNS: MigrationPatterns = - &[(KEYMAP_ACTION_STRING_PATTERN, replace_string_action)]; - -fn replace_string_action( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let action_name_ix = query.capture_index_for_name("action_name")?; - let action_name_node = mat.nodes_for_capture_index(action_name_ix).next()?; - let action_name_range = action_name_node.byte_range(); - let action_name = contents.get(action_name_range.clone())?; - - if let Some(new_action_name) = STRING_REPLACE.get(&action_name) { - return Some((action_name_range, new_action_name.to_string())); - } - - if let Some((new_action_name, options)) = STRING_TO_ARRAY_REPLACE.get(action_name) { - let full_string_range = action_name_node.parent()?.byte_range(); - let mut options_parts = Vec::new(); - for (key, value) in options.iter() { - options_parts.push(format!("\"{}\": {}", key, value)); - } - let options_str = options_parts.join(", "); - let replacement = format!("[\"{}\", {{ {} }}]", new_action_name, options_str); - return Some((full_string_range, replacement)); - } - - None -} - -static STRING_REPLACE: LazyLock> = LazyLock::new(|| { - HashMap::from_iter([ - ( - "editor::GoToPrevDiagnostic", - "editor::GoToPreviousDiagnostic", - ), - ("editor::ContextMenuPrev", "editor::ContextMenuPrevious"), - ("search::SelectPrevMatch", "search::SelectPreviousMatch"), - ("file_finder::SelectPrev", "file_finder::SelectPrevious"), - ("menu::SelectPrev", "menu::SelectPrevious"), - ("editor::TabPrev", "editor::Backtab"), - ("pane::ActivatePrevItem", "pane::ActivatePreviousItem"), - ("vim::MoveToPrev", "vim::MoveToPrevious"), - ("vim::MoveToPrevMatch", "vim::MoveToPreviousMatch"), - ]) -}); - -/// "editor::GoToPrevHunk" -> ["editor::GoToPreviousHunk", { "center_cursor": true }] -static STRING_TO_ARRAY_REPLACE: LazyLock)>> = - LazyLock::new(|| { - HashMap::from_iter([ - ( - "editor::GoToHunk", - ( - "editor::GoToHunk", - HashMap::from_iter([("center_cursor", true)]), - ), - ), - ( - "editor::GoToPrevHunk", - ( - "editor::GoToPreviousHunk", - HashMap::from_iter([("center_cursor", true)]), - ), - ), - ]) - }); diff --git a/crates/migrator/src/migrations/m_2025_03_06/keymap.rs b/crates/migrator/src/migrations/m_2025_03_06/keymap.rs deleted file mode 100644 index 333535908d..0000000000 --- a/crates/migrator/src/migrations/m_2025_03_06/keymap.rs +++ /dev/null @@ -1,38 +0,0 @@ -use collections::HashSet; -use std::{ops::Range, sync::LazyLock}; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::KEYMAP_ACTION_ARRAY_ARGUMENT_AS_OBJECT_PATTERN; - -pub const KEYMAP_PATTERNS: MigrationPatterns = &[( - KEYMAP_ACTION_ARRAY_ARGUMENT_AS_OBJECT_PATTERN, - replace_array_with_single_string, -)]; - -fn replace_array_with_single_string( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let array_ix = query.capture_index_for_name("array")?; - let action_name_ix = query.capture_index_for_name("action_name")?; - - let action_name = contents.get( - mat.nodes_for_capture_index(action_name_ix) - .next()? - .byte_range(), - )?; - - if TRANSFORM_ARRAY.contains(&action_name) { - let replacement_as_string = format!("\"{action_name}\""); - let range_to_replace = mat.nodes_for_capture_index(array_ix).next()?.byte_range(); - return Some((range_to_replace, replacement_as_string)); - } - - None -} - -/// ["editor::GoToPreviousHunk", { "center_cursor": true }] -> "editor::GoToPreviousHunk" -static TRANSFORM_ARRAY: LazyLock> = - LazyLock::new(|| HashSet::from_iter(["editor::GoToHunk", "editor::GoToPreviousHunk"])); diff --git a/crates/migrator/src/migrations/m_2025_03_29/settings.rs b/crates/migrator/src/migrations/m_2025_03_29/settings.rs deleted file mode 100644 index 8f83d8e39e..0000000000 --- a/crates/migrator/src/migrations/m_2025_03_29/settings.rs +++ /dev/null @@ -1,65 +0,0 @@ -use std::ops::Range; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::SETTINGS_ROOT_KEY_VALUE_PATTERN; - -pub const SETTINGS_PATTERNS: MigrationPatterns = &[ - (SETTINGS_ROOT_KEY_VALUE_PATTERN, replace_setting_name), - (SETTINGS_ROOT_KEY_VALUE_PATTERN, replace_setting_value), -]; - -fn replace_setting_value( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let setting_capture_ix = query.capture_index_for_name("name")?; - let setting_name_range = mat - .nodes_for_capture_index(setting_capture_ix) - .next()? - .byte_range(); - let setting_name = contents.get(setting_name_range)?; - - if setting_name != "hide_mouse_while_typing" { - return None; - } - - let value_capture_ix = query.capture_index_for_name("value")?; - let value_range = mat - .nodes_for_capture_index(value_capture_ix) - .next()? - .byte_range(); - let value = contents.get(value_range.clone())?; - - let new_value = if value.trim() == "true" { - "\"on_typing_and_movement\"" - } else if value.trim() == "false" { - "\"never\"" - } else { - return None; - }; - - Some((value_range, new_value.to_string())) -} - -fn replace_setting_name( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let setting_capture_ix = query.capture_index_for_name("name")?; - let setting_name_range = mat - .nodes_for_capture_index(setting_capture_ix) - .next()? - .byte_range(); - let setting_name = contents.get(setting_name_range.clone())?; - - let new_setting_name = if setting_name == "hide_mouse_while_typing" { - "hide_mouse" - } else { - return None; - }; - - Some((setting_name_range, new_setting_name.to_string())) -} diff --git a/crates/migrator/src/migrations/m_2025_04_15/keymap.rs b/crates/migrator/src/migrations/m_2025_04_15/keymap.rs deleted file mode 100644 index efbdc6b1c6..0000000000 --- a/crates/migrator/src/migrations/m_2025_04_15/keymap.rs +++ /dev/null @@ -1,31 +0,0 @@ -use collections::HashMap; -use std::{ops::Range, sync::LazyLock}; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::KEYMAP_ACTION_STRING_PATTERN; - -pub const KEYMAP_PATTERNS: MigrationPatterns = - &[(KEYMAP_ACTION_STRING_PATTERN, replace_string_action)]; - -fn replace_string_action( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let action_name_ix = query.capture_index_for_name("action_name")?; - let action_name_node = mat.nodes_for_capture_index(action_name_ix).next()?; - let action_name_range = action_name_node.byte_range(); - let action_name = contents.get(action_name_range.clone())?; - - if let Some(new_action_name) = STRING_REPLACE.get(&action_name) { - return Some((action_name_range, new_action_name.to_string())); - } - - None -} - -/// "space": "outline_panel::Open" -> "outline_panel::OpenSelectedEntry" -static STRING_REPLACE: LazyLock> = LazyLock::new(|| { - HashMap::from_iter([("outline_panel::Open", "outline_panel::OpenSelectedEntry")]) -}); diff --git a/crates/migrator/src/migrations/m_2025_04_15/settings.rs b/crates/migrator/src/migrations/m_2025_04_15/settings.rs deleted file mode 100644 index cbebd2a902..0000000000 --- a/crates/migrator/src/migrations/m_2025_04_15/settings.rs +++ /dev/null @@ -1,29 +0,0 @@ -use std::ops::Range; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::SETTINGS_ASSISTANT_TOOLS_PATTERN; - -pub const SETTINGS_PATTERNS: MigrationPatterns = &[( - SETTINGS_ASSISTANT_TOOLS_PATTERN, - replace_bash_with_terminal_in_profiles, -)]; - -fn replace_bash_with_terminal_in_profiles( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let tool_name_capture_ix = query.capture_index_for_name("tool_name")?; - let tool_name_range = mat - .nodes_for_capture_index(tool_name_capture_ix) - .next()? - .byte_range(); - let tool_name = contents.get(tool_name_range.clone())?; - - if tool_name != "bash" { - return None; - } - - Some((tool_name_range, "terminal".to_string())) -} diff --git a/crates/migrator/src/migrations/m_2025_04_21/settings.rs b/crates/migrator/src/migrations/m_2025_04_21/settings.rs deleted file mode 100644 index 55afc54928..0000000000 --- a/crates/migrator/src/migrations/m_2025_04_21/settings.rs +++ /dev/null @@ -1,25 +0,0 @@ -use std::ops::Range; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::SETTINGS_ASSISTANT_TOOLS_PATTERN; - -pub const SETTINGS_PATTERNS: MigrationPatterns = - &[(SETTINGS_ASSISTANT_TOOLS_PATTERN, rename_tools)]; - -fn rename_tools(contents: &str, mat: &QueryMatch, query: &Query) -> Option<(Range, String)> { - let tool_name_capture_ix = query.capture_index_for_name("tool_name")?; - let tool_name_range = mat - .nodes_for_capture_index(tool_name_capture_ix) - .next()? - .byte_range(); - let tool_name = contents.get(tool_name_range.clone())?; - - let new_name = match tool_name { - "find_replace_file" => "edit_file", - "regex_search" => "grep", - _ => return None, - }; - - Some((tool_name_range, new_name.to_string())) -} diff --git a/crates/migrator/src/migrations/m_2025_04_23/settings.rs b/crates/migrator/src/migrations/m_2025_04_23/settings.rs deleted file mode 100644 index 54bb999640..0000000000 --- a/crates/migrator/src/migrations/m_2025_04_23/settings.rs +++ /dev/null @@ -1,27 +0,0 @@ -use std::ops::Range; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::SETTINGS_ASSISTANT_TOOLS_PATTERN; - -pub const SETTINGS_PATTERNS: MigrationPatterns = - &[(SETTINGS_ASSISTANT_TOOLS_PATTERN, rename_path_search_tool)]; - -fn rename_path_search_tool( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let tool_name_capture_ix = query.capture_index_for_name("tool_name")?; - let tool_name_range = mat - .nodes_for_capture_index(tool_name_capture_ix) - .next()? - .byte_range(); - let tool_name = contents.get(tool_name_range.clone())?; - - if tool_name == "path_search" { - return Some((tool_name_range, "find_path".to_string())); - } - - None -} diff --git a/crates/migrator/src/migrations/m_2025_05_05/settings.rs b/crates/migrator/src/migrations/m_2025_05_05/settings.rs deleted file mode 100644 index 77da1b9a07..0000000000 --- a/crates/migrator/src/migrations/m_2025_05_05/settings.rs +++ /dev/null @@ -1,41 +0,0 @@ -use std::ops::Range; -use tree_sitter::{Query, QueryMatch}; - -use crate::{ - MigrationPatterns, patterns::SETTINGS_ASSISTANT_PATTERN, - patterns::SETTINGS_EDIT_PREDICTIONS_ASSISTANT_PATTERN, -}; - -pub const SETTINGS_PATTERNS: MigrationPatterns = &[ - (SETTINGS_ASSISTANT_PATTERN, rename_assistant), - ( - SETTINGS_EDIT_PREDICTIONS_ASSISTANT_PATTERN, - rename_edit_prediction_assistant, - ), -]; - -fn rename_assistant( - _contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let key_capture_ix = query.capture_index_for_name("key")?; - let key_range = mat - .nodes_for_capture_index(key_capture_ix) - .next()? - .byte_range(); - Some((key_range, "agent".to_string())) -} - -fn rename_edit_prediction_assistant( - _contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let key_capture_ix = query.capture_index_for_name("enabled_in_assistant")?; - let key_range = mat - .nodes_for_capture_index(key_capture_ix) - .next()? - .byte_range(); - Some((key_range, "enabled_in_text_threads".to_string())) -} diff --git a/crates/migrator/src/migrations/m_2025_05_08/settings.rs b/crates/migrator/src/migrations/m_2025_05_08/settings.rs deleted file mode 100644 index 6157ad0d43..0000000000 --- a/crates/migrator/src/migrations/m_2025_05_08/settings.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::ops::Range; -use tree_sitter::{Query, QueryMatch}; - -use crate::{MigrationPatterns, patterns::SETTINGS_DUPLICATED_AGENT_PATTERN}; - -pub const SETTINGS_PATTERNS: MigrationPatterns = - &[(SETTINGS_DUPLICATED_AGENT_PATTERN, comment_duplicated_agent)]; - -fn comment_duplicated_agent( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let pair_ix = query.capture_index_for_name("pair1")?; - let mut range = mat.nodes_for_capture_index(pair_ix).next()?.byte_range(); - - // Include the comma into the commented region - let rtext = &contents[range.end..]; - if let Some(comma_index) = rtext.find(',') { - range.end += comma_index + 1; - } - - let value = contents[range.clone()].to_string(); - let commented_value = format!("/* Duplicated key auto-commented: {value} */"); - Some((range, commented_value)) -} diff --git a/crates/migrator/src/migrations/m_2025_05_29/settings.rs b/crates/migrator/src/migrations/m_2025_05_29/settings.rs deleted file mode 100644 index 37ef0e45cc..0000000000 --- a/crates/migrator/src/migrations/m_2025_05_29/settings.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::ops::Range; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN; - -pub const SETTINGS_PATTERNS: MigrationPatterns = &[( - SETTINGS_NESTED_KEY_VALUE_PATTERN, - replace_preferred_completion_mode_value, -)]; - -fn replace_preferred_completion_mode_value( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let parent_object_capture_ix = query.capture_index_for_name("parent_key")?; - let parent_object_range = mat - .nodes_for_capture_index(parent_object_capture_ix) - .next()? - .byte_range(); - let parent_object_name = contents.get(parent_object_range)?; - - if parent_object_name != "agent" { - return None; - } - - let setting_name_capture_ix = query.capture_index_for_name("setting_name")?; - let setting_name_range = mat - .nodes_for_capture_index(setting_name_capture_ix) - .next()? - .byte_range(); - let setting_name = contents.get(setting_name_range)?; - - if setting_name != "preferred_completion_mode" { - return None; - } - - let value_capture_ix = query.capture_index_for_name("setting_value")?; - let value_range = mat - .nodes_for_capture_index(value_capture_ix) - .next()? - .byte_range(); - let value = contents.get(value_range.clone())?; - - if value.trim() == "\"max\"" { - Some((value_range, "\"burn\"".to_string())) - } else { - None - } -} diff --git a/crates/migrator/src/migrations/m_2025_06_16/settings.rs b/crates/migrator/src/migrations/m_2025_06_16/settings.rs deleted file mode 100644 index cd79eae204..0000000000 --- a/crates/migrator/src/migrations/m_2025_06_16/settings.rs +++ /dev/null @@ -1,90 +0,0 @@ -use std::ops::Range; - -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; - -pub const SETTINGS_PATTERNS: MigrationPatterns = &[( - SETTINGS_CONTEXT_SERVER_PATTERN, - migrate_context_server_settings, -)]; - -const SETTINGS_CONTEXT_SERVER_PATTERN: &str = r#"(document - (object - (pair - key: (string (string_content) @context-servers) - value: (object - (pair - key: (string (string_content) @server-name) - value: (object) @server-settings - ) - ) - ) - ) - (#eq? @context-servers "context_servers") -)"#; - -fn migrate_context_server_settings( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let server_settings_index = query.capture_index_for_name("server-settings")?; - let server_settings = mat.nodes_for_capture_index(server_settings_index).next()?; - - let mut has_command = false; - let mut has_settings = false; - let mut other_keys = 0; - let mut column = None; - - // Parse the server settings to check what keys it contains - let mut cursor = server_settings.walk(); - for child in server_settings.children(&mut cursor) { - if child.kind() == "pair" - && let Some(key_node) = child.child_by_field_name("key") - { - if let (None, Some(quote_content)) = (column, key_node.child(0)) { - column = Some(quote_content.start_position().column); - } - if let Some(string_content) = key_node.child(1) { - let key = &contents[string_content.byte_range()]; - match key { - // If it already has a source key, don't modify it - "source" => return None, - "command" => has_command = true, - "settings" => has_settings = true, - _ => other_keys += 1, - } - } - } - } - - let source_type = if has_command { "custom" } else { "extension" }; - - // Insert the source key at the beginning of the object - let start = server_settings.start_byte() + 1; - let indent = " ".repeat(column.unwrap_or(12)); - - if !has_command && !has_settings { - return Some(( - start..start, - format!( - r#" -{indent}"source": "{}", -{indent}"settings": {{}}{} - "#, - source_type, - if other_keys > 0 { "," } else { "" } - ), - )); - } - - Some(( - start..start, - format!( - r#" -{indent}"source": "{}","#, - source_type - ), - )) -} diff --git a/crates/migrator/src/migrations/m_2025_06_25/settings.rs b/crates/migrator/src/migrations/m_2025_06_25/settings.rs deleted file mode 100644 index 2bf7658eeb..0000000000 --- a/crates/migrator/src/migrations/m_2025_06_25/settings.rs +++ /dev/null @@ -1,133 +0,0 @@ -use std::ops::Range; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; - -pub const SETTINGS_PATTERNS: MigrationPatterns = &[ - (SETTINGS_VERSION_PATTERN, remove_version_fields), - ( - SETTINGS_NESTED_VERSION_PATTERN, - remove_nested_version_fields, - ), -]; - -const SETTINGS_VERSION_PATTERN: &str = r#"(document - (object - (pair - key: (string (string_content) @key) - value: (object - (pair - key: (string (string_content) @version_key) - value: (_) @version_value - ) @version_pair - ) - ) - ) - (#eq? @key "agent") - (#eq? @version_key "version") -)"#; - -const SETTINGS_NESTED_VERSION_PATTERN: &str = r#"(document - (object - (pair - key: (string (string_content) @language_models) - value: (object - (pair - key: (string (string_content) @provider) - value: (object - (pair - key: (string (string_content) @version_key) - value: (_) @version_value - ) @version_pair - ) - ) - ) - ) - ) - (#eq? @language_models "language_models") - (#match? @provider "^(anthropic|openai)$") - (#eq? @version_key "version") -)"#; - -fn remove_version_fields( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let version_pair_ix = query.capture_index_for_name("version_pair")?; - let version_pair_node = mat.nodes_for_capture_index(version_pair_ix).next()?; - - remove_pair_with_whitespace(contents, version_pair_node) -} - -fn remove_nested_version_fields( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let version_pair_ix = query.capture_index_for_name("version_pair")?; - let version_pair_node = mat.nodes_for_capture_index(version_pair_ix).next()?; - - remove_pair_with_whitespace(contents, version_pair_node) -} - -fn remove_pair_with_whitespace( - contents: &str, - pair_node: tree_sitter::Node, -) -> Option<(Range, String)> { - let mut range_to_remove = pair_node.byte_range(); - - // Check if there's a comma after this pair - if let Some(next_sibling) = pair_node.next_sibling() { - if next_sibling.kind() == "," { - range_to_remove.end = next_sibling.end_byte(); - } - } else { - // If no next sibling, check if there's a comma before - if let Some(prev_sibling) = pair_node.prev_sibling() - && prev_sibling.kind() == "," - { - range_to_remove.start = prev_sibling.start_byte(); - } - } - - // Include any leading whitespace/newline, including comments - let text_before = &contents[..range_to_remove.start]; - if let Some(last_newline) = text_before.rfind('\n') { - let whitespace_start = last_newline + 1; - let potential_whitespace = &contents[whitespace_start..range_to_remove.start]; - - // Check if it's only whitespace or comments - let mut is_whitespace_or_comment = true; - let mut in_comment = false; - let mut chars = potential_whitespace.chars().peekable(); - - while let Some(ch) = chars.next() { - if in_comment { - if ch == '\n' { - in_comment = false; - } - } else if ch == '/' && chars.peek() == Some(&'/') { - in_comment = true; - chars.next(); // Skip the second '/' - } else if !ch.is_whitespace() { - is_whitespace_or_comment = false; - break; - } - } - - if is_whitespace_or_comment { - range_to_remove.start = whitespace_start; - } - } - - // Also check if we need to include trailing whitespace up to the next line - let text_after = &contents[range_to_remove.end..]; - if let Some(newline_pos) = text_after.find('\n') - && text_after[..newline_pos].chars().all(|c| c.is_whitespace()) - { - range_to_remove.end += newline_pos + 1; - } - - Some((range_to_remove, String::new())) -} diff --git a/crates/migrator/src/migrations/m_2025_06_27/settings.rs b/crates/migrator/src/migrations/m_2025_06_27/settings.rs deleted file mode 100644 index e3e951b1a6..0000000000 --- a/crates/migrator/src/migrations/m_2025_06_27/settings.rs +++ /dev/null @@ -1,132 +0,0 @@ -use std::ops::Range; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; - -pub const SETTINGS_PATTERNS: MigrationPatterns = &[( - SETTINGS_CONTEXT_SERVER_PATTERN, - flatten_context_server_command, -)]; - -const SETTINGS_CONTEXT_SERVER_PATTERN: &str = r#"(document - (object - (pair - key: (string (string_content) @context-servers) - value: (object - (pair - key: (string (string_content) @server-name) - value: (object - (pair - key: (string (string_content) @source-key) - value: (string (string_content) @source-value) - ) - (pair - key: (string (string_content) @command-key) - value: (object) @command-object - ) @command-pair - ) @server-settings - ) - ) - ) - ) - (#eq? @context-servers "context_servers") - (#eq? @source-key "source") - (#eq? @source-value "custom") - (#eq? @command-key "command") -)"#; - -fn flatten_context_server_command( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let command_pair_index = query.capture_index_for_name("command-pair")?; - let command_pair = mat.nodes_for_capture_index(command_pair_index).next()?; - - let command_object_index = query.capture_index_for_name("command-object")?; - let command_object = mat.nodes_for_capture_index(command_object_index).next()?; - - let server_settings_index = query.capture_index_for_name("server-settings")?; - let _server_settings = mat.nodes_for_capture_index(server_settings_index).next()?; - - // Parse the command object to extract path, args, and env - let mut path_value = None; - let mut args_value = None; - let mut env_value = None; - - let mut cursor = command_object.walk(); - for child in command_object.children(&mut cursor) { - if child.kind() == "pair" - && let Some(key_node) = child.child_by_field_name("key") - && let Some(string_content) = key_node.child(1) - { - let key = &contents[string_content.byte_range()]; - if let Some(value_node) = child.child_by_field_name("value") { - let value_range = value_node.byte_range(); - match key { - "path" => path_value = Some(&contents[value_range]), - "args" => args_value = Some(&contents[value_range]), - "env" => env_value = Some(&contents[value_range]), - _ => {} - } - } - } - } - - let path = path_value?; - - // Get the proper indentation from the command pair - let command_pair_start = command_pair.start_byte(); - let line_start = contents[..command_pair_start] - .rfind('\n') - .map(|pos| pos + 1) - .unwrap_or(0); - let indent = &contents[line_start..command_pair_start]; - - // Build the replacement string - let mut replacement = format!("\"command\": {}", path); - - // Add args if present - need to reduce indentation - if let Some(args) = args_value { - replacement.push_str(",\n"); - replacement.push_str(indent); - replacement.push_str("\"args\": "); - let reduced_args = reduce_indentation(args, 4); - replacement.push_str(&reduced_args); - } - - // Add env if present - need to reduce indentation - if let Some(env) = env_value { - replacement.push_str(",\n"); - replacement.push_str(indent); - replacement.push_str("\"env\": "); - replacement.push_str(&reduce_indentation(env, 4)); - } - - let range_to_replace = command_pair.byte_range(); - Some((range_to_replace, replacement)) -} - -fn reduce_indentation(text: &str, spaces: usize) -> String { - let lines: Vec<&str> = text.lines().collect(); - let mut result = String::new(); - - for (i, line) in lines.iter().enumerate() { - if i > 0 { - result.push('\n'); - } - - // Count leading spaces - let leading_spaces = line.chars().take_while(|&c| c == ' ').count(); - - if leading_spaces >= spaces { - // Reduce indentation - result.push_str(&line[spaces..]); - } else { - // Keep line as is if it doesn't have enough indentation - result.push_str(line); - } - } - - result -} diff --git a/crates/migrator/src/migrations/m_2025_07_08/settings.rs b/crates/migrator/src/migrations/m_2025_07_08/settings.rs deleted file mode 100644 index c9656491ce..0000000000 --- a/crates/migrator/src/migrations/m_2025_07_08/settings.rs +++ /dev/null @@ -1,37 +0,0 @@ -use std::ops::Range; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::SETTINGS_ROOT_KEY_VALUE_PATTERN; - -pub const SETTINGS_PATTERNS: MigrationPatterns = &[( - SETTINGS_ROOT_KEY_VALUE_PATTERN, - migrate_drag_and_drop_selection, -)]; - -fn migrate_drag_and_drop_selection( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let name_ix = query.capture_index_for_name("name")?; - let name_range = mat.nodes_for_capture_index(name_ix).next()?.byte_range(); - let name = contents.get(name_range)?; - - if name != "drag_and_drop_selection" { - return None; - } - - let value_ix = query.capture_index_for_name("value")?; - let value_node = mat.nodes_for_capture_index(value_ix).next()?; - let value_range = value_node.byte_range(); - let value = contents.get(value_range.clone())?; - - match value { - "true" | "false" => { - let replacement = format!("{{\n \"enabled\": {}\n }}", value); - Some((value_range, replacement)) - } - _ => None, - } -} diff --git a/crates/migrator/src/migrations/m_2025_10_01/settings.rs b/crates/migrator/src/migrations/m_2025_10_01/settings.rs deleted file mode 100644 index 84cf950491..0000000000 --- a/crates/migrator/src/migrations/m_2025_10_01/settings.rs +++ /dev/null @@ -1,74 +0,0 @@ -use crate::patterns::migrate_language_setting; -use anyhow::Result; -use serde_json::Value; - -pub fn flatten_code_actions_formatters(value: &mut Value) -> Result<()> { - migrate_language_setting(value, |value, _path| { - let Some(obj) = value.as_object_mut() else { - return Ok(()); - }; - for key in ["formatter", "format_on_save"] { - let Some(formatter) = obj.get_mut(key) else { - continue; - }; - let new_formatter = match formatter { - Value::Array(arr) => { - let mut new_arr = Vec::new(); - let mut found_code_actions = false; - for item in arr { - let Some(obj) = item.as_object() else { - new_arr.push(item.clone()); - continue; - }; - let code_actions_obj = obj - .get("code_actions") - .and_then(|code_actions| code_actions.as_object()); - let Some(code_actions) = code_actions_obj else { - new_arr.push(item.clone()); - continue; - }; - found_code_actions = true; - for (name, enabled) in code_actions { - if !enabled.as_bool().unwrap_or(true) { - continue; - } - new_arr.push(serde_json::json!({ - "code_action": name - })); - } - } - if !found_code_actions { - continue; - } - Value::Array(new_arr) - } - Value::Object(obj) => { - let mut new_arr = Vec::new(); - let code_actions_obj = obj - .get("code_actions") - .and_then(|code_actions| code_actions.as_object()); - let Some(code_actions) = code_actions_obj else { - continue; - }; - for (name, enabled) in code_actions { - if !enabled.as_bool().unwrap_or(true) { - continue; - } - new_arr.push(serde_json::json!({ - "code_action": name - })); - } - if new_arr.len() == 1 { - new_arr.pop().unwrap() - } else { - Value::Array(new_arr) - } - } - _ => continue, - }; - - obj.insert(key.to_string(), new_formatter); - } - return Ok(()); - }) -} diff --git a/crates/migrator/src/migrations/m_2025_10_02/settings.rs b/crates/migrator/src/migrations/m_2025_10_02/settings.rs deleted file mode 100644 index cb0d63ca85..0000000000 --- a/crates/migrator/src/migrations/m_2025_10_02/settings.rs +++ /dev/null @@ -1,41 +0,0 @@ -use anyhow::Result; -use serde_json::Value; - -use crate::patterns::migrate_language_setting; - -pub fn remove_formatters_on_save(value: &mut Value) -> Result<()> { - migrate_language_setting(value, remove_formatters_on_save_inner) -} - -fn remove_formatters_on_save_inner(value: &mut Value, path: &[&str]) -> Result<()> { - let Some(obj) = value.as_object_mut() else { - return Ok(()); - }; - let Some(format_on_save) = obj.get("format_on_save").cloned() else { - return Ok(()); - }; - let is_format_on_save_set_to_formatter = format_on_save - .as_str() - .map_or(true, |s| s != "on" && s != "off"); - if !is_format_on_save_set_to_formatter { - return Ok(()); - } - - fn fmt_path(path: &[&str], key: &str) -> String { - let mut path = path.to_vec(); - path.push(key); - path.join(".") - } - - anyhow::ensure!( - obj.get("formatter").is_none(), - r#"Setting formatters in both "format_on_save" and "formatter" is deprecated. Please migrate the formatters from {} into {}"#, - fmt_path(path, "format_on_save"), - fmt_path(path, "formatter") - ); - - obj.insert("format_on_save".to_string(), serde_json::json!("on")); - obj.insert("formatter".to_string(), format_on_save); - - Ok(()) -} diff --git a/crates/migrator/src/migrations/m_2025_10_03/settings.rs b/crates/migrator/src/migrations/m_2025_10_03/settings.rs deleted file mode 100644 index 47d15e8ddf..0000000000 --- a/crates/migrator/src/migrations/m_2025_10_03/settings.rs +++ /dev/null @@ -1,30 +0,0 @@ -use std::ops::Range; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::SETTINGS_ROOT_KEY_VALUE_PATTERN; - -pub const SETTINGS_PATTERNS: MigrationPatterns = - &[(SETTINGS_ROOT_KEY_VALUE_PATTERN, rename_agent_font_size)]; - -/// Renames the setting `agent_font_size` to `agent_ui_font_size` -fn rename_agent_font_size( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let setting_capture_ix = query.capture_index_for_name("name")?; - - let setting_name_range = mat - .nodes_for_capture_index(setting_capture_ix) - .next()? - .byte_range(); - - let setting_name = contents.get(setting_name_range.clone())?; - - if setting_name != "agent_font_size" { - return None; - } - - Some((setting_name_range, "agent_ui_font_size".to_string())) -} diff --git a/crates/migrator/src/migrations/m_2025_10_16/settings.rs b/crates/migrator/src/migrations/m_2025_10_16/settings.rs deleted file mode 100644 index 3fa8c509b1..0000000000 --- a/crates/migrator/src/migrations/m_2025_10_16/settings.rs +++ /dev/null @@ -1,71 +0,0 @@ -use anyhow::Result; -use serde_json::Value; - -use crate::patterns::migrate_language_setting; - -pub fn restore_code_actions_on_format(value: &mut Value) -> Result<()> { - migrate_language_setting(value, restore_code_actions_on_format_inner) -} - -fn restore_code_actions_on_format_inner(value: &mut Value, path: &[&str]) -> Result<()> { - let Some(obj) = value.as_object_mut() else { - return Ok(()); - }; - let code_actions_on_format = obj - .get("code_actions_on_format") - .cloned() - .unwrap_or_else(|| Value::Object(Default::default())); - - fn fmt_path(path: &[&str], key: &str) -> String { - let mut path = path.to_vec(); - path.push(key); - path.join(".") - } - - let Some(mut code_actions_map) = code_actions_on_format.as_object().cloned() else { - anyhow::bail!( - r#"The `code_actions_on_format` is in an invalid state and cannot be migrated at {}. Please ensure the code_actions_on_format setting is a Map"#, - fmt_path(path, "code_actions_on_format"), - ); - }; - - let Some(formatter) = obj.get("formatter") else { - return Ok(()); - }; - let formatter_array = if let Some(array) = formatter.as_array() { - array.clone() - } else { - vec![formatter.clone()] - }; - if formatter_array.is_empty() { - return Ok(()); - } - let mut code_action_formatters = Vec::new(); - for formatter in formatter_array { - let Some(code_action) = formatter.get("code_action") else { - return Ok(()); - }; - let Some(code_action_name) = code_action.as_str() else { - anyhow::bail!( - r#"The `code_action` is in an invalid state and cannot be migrated at {}. Please ensure the code_action setting is a String"#, - fmt_path(path, "formatter"), - ); - }; - code_action_formatters.push(code_action_name.to_string()); - } - - code_actions_map.extend( - code_action_formatters - .into_iter() - .rev() - .map(|code_action| (code_action, Value::Bool(true))), - ); - - obj.insert("formatter".to_string(), Value::Array(vec![])); - obj.insert( - "code_actions_on_format".into(), - Value::Object(code_actions_map), - ); - - Ok(()) -} diff --git a/crates/migrator/src/migrations/m_2025_10_17/settings.rs b/crates/migrator/src/migrations/m_2025_10_17/settings.rs deleted file mode 100644 index 519ec74034..0000000000 --- a/crates/migrator/src/migrations/m_2025_10_17/settings.rs +++ /dev/null @@ -1,24 +0,0 @@ -use anyhow::Result; -use serde_json::Value; - -pub fn make_file_finder_include_ignored_an_enum(value: &mut Value) -> Result<()> { - let Some(file_finder) = value.get_mut("file_finder") else { - return Ok(()); - }; - - let Some(file_finder_obj) = file_finder.as_object_mut() else { - anyhow::bail!("Expected file_finder to be an object"); - }; - - let Some(include_ignored) = file_finder_obj.get_mut("include_ignored") else { - return Ok(()); - }; - *include_ignored = match include_ignored { - Value::Bool(true) => Value::String("all".to_string()), - Value::Bool(false) => Value::String("indexed".to_string()), - Value::Null => Value::String("smart".to_string()), - Value::String(s) if s == "all" || s == "indexed" || s == "smart" => return Ok(()), - _ => anyhow::bail!("Expected include_ignored to be a boolean or null"), - }; - Ok(()) -} diff --git a/crates/migrator/src/migrations/m_2025_10_21/settings.rs b/crates/migrator/src/migrations/m_2025_10_21/settings.rs deleted file mode 100644 index 1f78f93327..0000000000 --- a/crates/migrator/src/migrations/m_2025_10_21/settings.rs +++ /dev/null @@ -1,16 +0,0 @@ -use anyhow::Result; -use serde_json::Value; - -pub fn make_relative_line_numbers_an_enum(value: &mut Value) -> Result<()> { - let Some(relative_line_numbers) = value.get_mut("relative_line_numbers") else { - return Ok(()); - }; - - *relative_line_numbers = match relative_line_numbers { - Value::Bool(true) => Value::String("enabled".to_string()), - Value::Bool(false) => Value::String("disabled".to_string()), - Value::String(s) if s == "enabled" || s == "disabled" || s == "wrapped" => return Ok(()), - _ => anyhow::bail!("Expected relative_line_numbers to be a boolean"), - }; - Ok(()) -} diff --git a/crates/migrator/src/migrations/m_2025_11_12/settings.rs b/crates/migrator/src/migrations/m_2025_11_12/settings.rs deleted file mode 100644 index 6483f9e44b..0000000000 --- a/crates/migrator/src/migrations/m_2025_11_12/settings.rs +++ /dev/null @@ -1,84 +0,0 @@ -use std::ops::Range; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN; - -pub const SETTINGS_PATTERNS: MigrationPatterns = &[ - ( - SETTINGS_NESTED_KEY_VALUE_PATTERN, - rename_open_file_on_paste_setting, - ), - ( - SETTINGS_NESTED_KEY_VALUE_PATTERN, - replace_open_file_on_paste_setting_value, - ), -]; - -fn rename_open_file_on_paste_setting( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - if !is_project_panel_open_file_on_paste(contents, mat, query) { - return None; - } - - let setting_name_ix = query.capture_index_for_name("setting_name")?; - let setting_name_range = mat - .nodes_for_capture_index(setting_name_ix) - .next()? - .byte_range(); - - Some((setting_name_range, "auto_open".to_string())) -} - -fn replace_open_file_on_paste_setting_value( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - if !is_project_panel_open_file_on_paste(contents, mat, query) { - return None; - } - - let value_ix = query.capture_index_for_name("setting_value")?; - let value_node = mat.nodes_for_capture_index(value_ix).next()?; - let value_range = value_node.byte_range(); - let value_text = contents.get(value_range.clone())?.trim(); - - let normalized_value = match value_text { - "true" => "true", - "false" => "false", - _ => return None, - }; - - Some(( - value_range, - format!("{{ \"on_paste\": {normalized_value} }}"), - )) -} - -fn is_project_panel_open_file_on_paste(contents: &str, mat: &QueryMatch, query: &Query) -> bool { - let parent_key_ix = match query.capture_index_for_name("parent_key") { - Some(ix) => ix, - None => return false, - }; - let parent_range = match mat.nodes_for_capture_index(parent_key_ix).next() { - Some(node) => node.byte_range(), - None => return false, - }; - if contents.get(parent_range) != Some("project_panel") { - return false; - } - - let setting_name_ix = match query.capture_index_for_name("setting_name") { - Some(ix) => ix, - None => return false, - }; - let setting_name_range = match mat.nodes_for_capture_index(setting_name_ix).next() { - Some(node) => node.byte_range(), - None => return false, - }; - contents.get(setting_name_range) == Some("open_file_on_paste") -} diff --git a/crates/migrator/src/migrations/m_2025_11_20/settings.rs b/crates/migrator/src/migrations/m_2025_11_20/settings.rs deleted file mode 100644 index db56fb04d4..0000000000 --- a/crates/migrator/src/migrations/m_2025_11_20/settings.rs +++ /dev/null @@ -1,76 +0,0 @@ -use std::ops::Range; - -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; - -pub const SETTINGS_PATTERNS: MigrationPatterns = &[( - SETTINGS_AGENT_SERVERS_CUSTOM_PATTERN, - migrate_custom_agent_settings, -)]; - -const SETTINGS_AGENT_SERVERS_CUSTOM_PATTERN: &str = r#"(document - (object - (pair - key: (string (string_content) @agent-servers) - value: (object - (pair - key: (string (string_content) @server-name) - value: (object) @server-settings - ) - ) - ) - ) - (#eq? @agent-servers "agent_servers") -)"#; - -fn migrate_custom_agent_settings( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - let server_name_index = query.capture_index_for_name("server-name")?; - let server_name = mat.nodes_for_capture_index(server_name_index).next()?; - let server_name_text = &contents[server_name.byte_range()]; - - if matches!(server_name_text, "gemini" | "claude" | "codex") { - return None; - } - - let server_settings_index = query.capture_index_for_name("server-settings")?; - let server_settings = mat.nodes_for_capture_index(server_settings_index).next()?; - - let mut column = None; - - // Parse the server settings to check what keys it contains - let mut cursor = server_settings.walk(); - for child in server_settings.children(&mut cursor) { - if child.kind() == "pair" { - if let Some(key_node) = child.child_by_field_name("key") { - if let (None, Some(quote_content)) = (column, key_node.child(0)) { - column = Some(quote_content.start_position().column); - } - if let Some(string_content) = key_node.child(1) { - let key = &contents[string_content.byte_range()]; - match key { - // If it already has a type key, don't modify it - "type" => return None, - _ => {} - } - } - } - } - } - - // Insert the type key at the beginning of the object - let start = server_settings.start_byte() + 1; - let indent = " ".repeat(column.unwrap_or(12)); - - Some(( - start..start, - format!( - r#" -{indent}"type": "custom","# - ), - )) -} diff --git a/crates/migrator/src/migrations/m_2025_11_25/settings.rs b/crates/migrator/src/migrations/m_2025_11_25/settings.rs deleted file mode 100644 index 944eee8a11..0000000000 --- a/crates/migrator/src/migrations/m_2025_11_25/settings.rs +++ /dev/null @@ -1,17 +0,0 @@ -use anyhow::Result; -use serde_json::Value; - -pub fn remove_context_server_source(settings: &mut Value) -> Result<()> { - if let Some(obj) = settings.as_object_mut() { - if let Some(context_servers) = obj.get_mut("context_servers") { - if let Some(servers) = context_servers.as_object_mut() { - for (_, server) in servers.iter_mut() { - if let Some(server_obj) = server.as_object_mut() { - server_obj.remove("source"); - } - } - } - } - } - Ok(()) -} diff --git a/crates/migrator/src/migrations/m_2025_12_01/settings.rs b/crates/migrator/src/migrations/m_2025_12_01/settings.rs deleted file mode 100644 index 2c3816dab3..0000000000 --- a/crates/migrator/src/migrations/m_2025_12_01/settings.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::ops::Range; -use tree_sitter::{Query, QueryMatch}; - -use crate::MigrationPatterns; -use crate::patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN; - -pub const SETTINGS_PATTERNS: MigrationPatterns = &[( - SETTINGS_NESTED_KEY_VALUE_PATTERN, - rename_enable_preview_from_code_navigation_setting, -)]; - -fn rename_enable_preview_from_code_navigation_setting( - contents: &str, - mat: &QueryMatch, - query: &Query, -) -> Option<(Range, String)> { - if !is_enable_preview_from_code_navigation(contents, mat, query) { - return None; - } - - let setting_name_ix = query.capture_index_for_name("setting_name")?; - let setting_name_range = mat - .nodes_for_capture_index(setting_name_ix) - .next()? - .byte_range(); - - Some(( - setting_name_range, - "enable_keep_preview_on_code_navigation".to_string(), - )) -} - -fn is_enable_preview_from_code_navigation(contents: &str, mat: &QueryMatch, query: &Query) -> bool { - let parent_key_ix = match query.capture_index_for_name("parent_key") { - Some(ix) => ix, - None => return false, - }; - let parent_range = match mat.nodes_for_capture_index(parent_key_ix).next() { - Some(node) => node.byte_range(), - None => return false, - }; - if contents.get(parent_range) != Some("preview_tabs") { - return false; - } - - let setting_name_ix = match query.capture_index_for_name("setting_name") { - Some(ix) => ix, - None => return false, - }; - let setting_name_range = match mat.nodes_for_capture_index(setting_name_ix).next() { - Some(node) => node.byte_range(), - None => return false, - }; - contents.get(setting_name_range) == Some("enable_preview_from_code_navigation") -} diff --git a/crates/migrator/src/migrator.rs b/crates/migrator/src/migrator.rs deleted file mode 100644 index 9fb6d8a115..0000000000 --- a/crates/migrator/src/migrator.rs +++ /dev/null @@ -1,2418 +0,0 @@ -//! ## When to create a migration and why? -//! A migration is necessary when keymap actions or settings are renamed or transformed (e.g., from an array to a string, a string to an array, a boolean to an enum, etc.). -//! -//! This ensures that users with outdated settings are automatically updated to use the corresponding new settings internally. -//! It also provides a quick way to migrate their existing settings to the latest state using button in UI. -//! -//! ## How to create a migration? -//! Migrations use Tree-sitter to query commonly used patterns, such as actions with a string or actions with an array where the second argument is an object, etc. -//! Once queried, *you can filter out the modified items* and write the replacement logic. -//! -//! You *must not* modify previous migrations; always create new ones instead. -//! This is important because if a user is in an intermediate state, they can smoothly transition to the latest state. -//! Modifying existing migrations means they will only work for users upgrading from version x-1 to x, but not from x-2 to x, and so on, where x is the latest version. -//! -//! You only need to write replacement logic for x-1 to x because you can be certain that, internally, every user will be at x-1, regardless of their on disk state. - -use anyhow::{Context as _, Result}; -use settings_json::{infer_json_indent_size, parse_json_with_comments, update_value_in_json_text}; -use std::{cmp::Reverse, ops::Range, sync::LazyLock}; -use streaming_iterator::StreamingIterator; -use tree_sitter::{Query, QueryMatch}; - -use patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN; - -mod migrations; -mod patterns; - -fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Result> { - let mut parser = tree_sitter::Parser::new(); - parser.set_language(&tree_sitter_json::LANGUAGE.into())?; - let syntax_tree = parser - .parse(text, None) - .context("failed to parse settings")?; - - let mut cursor = tree_sitter::QueryCursor::new(); - let mut matches = cursor.matches(query, syntax_tree.root_node(), text.as_bytes()); - - let mut edits = vec![]; - while let Some(mat) = matches.next() { - if let Some((_, callback)) = patterns.get(mat.pattern_index) { - edits.extend(callback(text, mat, query)); - } - } - - edits.sort_by_key(|(range, _)| (range.start, Reverse(range.end))); - edits.dedup_by(|(range_b, _), (range_a, _)| { - range_a.contains(&range_b.start) || range_a.contains(&range_b.end) - }); - - if edits.is_empty() { - Ok(None) - } else { - let mut new_text = text.to_string(); - for (range, replacement) in edits.iter().rev() { - new_text.replace_range(range.clone(), replacement); - } - if new_text == text { - log::error!( - "Edits computed for configuration migration do not cause a change: {:?}", - edits - ); - Ok(None) - } else { - Ok(Some(new_text)) - } - } -} - -/// Runs the provided migrations on the given text. -/// Will automatically return `Ok(None)` if there's no content to migrate. -fn run_migrations(text: &str, migrations: &[MigrationType]) -> Result> { - if text.is_empty() { - return Ok(None); - } - - let mut current_text = text.to_string(); - let mut result: Option = None; - let json_indent_size = infer_json_indent_size(¤t_text); - for migration in migrations.iter() { - let migrated_text = match migration { - MigrationType::TreeSitter(patterns, query) => migrate(¤t_text, patterns, query)?, - MigrationType::Json(callback) => { - if current_text.trim().is_empty() { - return Ok(None); - } - let old_content: serde_json_lenient::Value = - parse_json_with_comments(¤t_text)?; - let old_value = serde_json::to_value(&old_content).unwrap(); - let mut new_value = old_value.clone(); - callback(&mut new_value)?; - if new_value != old_value { - let mut current = current_text.clone(); - let mut edits = vec![]; - update_value_in_json_text( - &mut current, - &mut vec![], - json_indent_size, - &old_value, - &new_value, - &mut edits, - ); - let mut migrated_text = current_text.clone(); - for (range, replacement) in edits.into_iter() { - migrated_text.replace_range(range, &replacement); - } - Some(migrated_text) - } else { - None - } - } - }; - if let Some(migrated_text) = migrated_text { - current_text = migrated_text.clone(); - result = Some(migrated_text); - } - } - Ok(result.filter(|new_text| text != new_text)) -} - -pub fn migrate_keymap(text: &str) -> Result> { - let migrations: &[MigrationType] = &[ - MigrationType::TreeSitter( - migrations::m_2025_01_29::KEYMAP_PATTERNS, - &KEYMAP_QUERY_2025_01_29, - ), - MigrationType::TreeSitter( - migrations::m_2025_01_30::KEYMAP_PATTERNS, - &KEYMAP_QUERY_2025_01_30, - ), - MigrationType::TreeSitter( - migrations::m_2025_03_03::KEYMAP_PATTERNS, - &KEYMAP_QUERY_2025_03_03, - ), - MigrationType::TreeSitter( - migrations::m_2025_03_06::KEYMAP_PATTERNS, - &KEYMAP_QUERY_2025_03_06, - ), - MigrationType::TreeSitter( - migrations::m_2025_04_15::KEYMAP_PATTERNS, - &KEYMAP_QUERY_2025_04_15, - ), - ]; - run_migrations(text, migrations) -} - -enum MigrationType<'a> { - TreeSitter(MigrationPatterns, &'a Query), - Json(fn(&mut serde_json::Value) -> Result<()>), -} - -pub fn migrate_settings(text: &str) -> Result> { - let migrations: &[MigrationType] = &[ - MigrationType::TreeSitter( - migrations::m_2025_01_02::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_01_02, - ), - MigrationType::TreeSitter( - migrations::m_2025_01_29::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_01_29, - ), - MigrationType::TreeSitter( - migrations::m_2025_01_30::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_01_30, - ), - MigrationType::TreeSitter( - migrations::m_2025_03_29::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_03_29, - ), - MigrationType::TreeSitter( - migrations::m_2025_04_15::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_04_15, - ), - MigrationType::TreeSitter( - migrations::m_2025_04_21::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_04_21, - ), - MigrationType::TreeSitter( - migrations::m_2025_04_23::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_04_23, - ), - MigrationType::TreeSitter( - migrations::m_2025_05_05::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_05_05, - ), - MigrationType::TreeSitter( - migrations::m_2025_05_08::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_05_08, - ), - MigrationType::TreeSitter( - migrations::m_2025_05_29::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_05_29, - ), - MigrationType::TreeSitter( - migrations::m_2025_06_16::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_06_16, - ), - MigrationType::TreeSitter( - migrations::m_2025_06_25::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_06_25, - ), - MigrationType::TreeSitter( - migrations::m_2025_06_27::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_06_27, - ), - MigrationType::TreeSitter( - migrations::m_2025_07_08::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_07_08, - ), - MigrationType::Json(migrations::m_2025_10_01::flatten_code_actions_formatters), - MigrationType::Json(migrations::m_2025_10_02::remove_formatters_on_save), - MigrationType::TreeSitter( - migrations::m_2025_10_03::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_10_03, - ), - MigrationType::Json(migrations::m_2025_10_16::restore_code_actions_on_format), - MigrationType::Json(migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum), - MigrationType::Json(migrations::m_2025_10_21::make_relative_line_numbers_an_enum), - MigrationType::TreeSitter( - migrations::m_2025_11_12::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_11_12, - ), - MigrationType::TreeSitter( - migrations::m_2025_12_01::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_12_01, - ), - MigrationType::TreeSitter( - migrations::m_2025_11_20::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_11_20, - ), - MigrationType::Json(migrations::m_2025_11_25::remove_context_server_source), - ]; - run_migrations(text, migrations) -} - -pub fn migrate_edit_prediction_provider_settings(text: &str) -> Result> { - migrate( - text, - &[( - SETTINGS_NESTED_KEY_VALUE_PATTERN, - migrations::m_2025_01_29::replace_edit_prediction_provider_setting, - )], - &EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY, - ) -} - -pub type MigrationPatterns = &'static [( - &'static str, - fn(&str, &QueryMatch, &Query) -> Option<(Range, String)>, -)]; - -macro_rules! define_query { - ($var_name:ident, $patterns_path:path) => { - static $var_name: LazyLock = LazyLock::new(|| { - Query::new( - &tree_sitter_json::LANGUAGE.into(), - &$patterns_path - .iter() - .map(|pattern| pattern.0) - .collect::(), - ) - .unwrap() - }); - }; -} - -// keymap -define_query!( - KEYMAP_QUERY_2025_01_29, - migrations::m_2025_01_29::KEYMAP_PATTERNS -); -define_query!( - KEYMAP_QUERY_2025_01_30, - migrations::m_2025_01_30::KEYMAP_PATTERNS -); -define_query!( - KEYMAP_QUERY_2025_03_03, - migrations::m_2025_03_03::KEYMAP_PATTERNS -); -define_query!( - KEYMAP_QUERY_2025_03_06, - migrations::m_2025_03_06::KEYMAP_PATTERNS -); -define_query!( - KEYMAP_QUERY_2025_04_15, - migrations::m_2025_04_15::KEYMAP_PATTERNS -); - -// settings -define_query!( - SETTINGS_QUERY_2025_01_02, - migrations::m_2025_01_02::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_01_29, - migrations::m_2025_01_29::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_01_30, - migrations::m_2025_01_30::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_03_29, - migrations::m_2025_03_29::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_04_15, - migrations::m_2025_04_15::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_04_21, - migrations::m_2025_04_21::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_04_23, - migrations::m_2025_04_23::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_05_05, - migrations::m_2025_05_05::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_05_08, - migrations::m_2025_05_08::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_05_29, - migrations::m_2025_05_29::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_06_16, - migrations::m_2025_06_16::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_06_25, - migrations::m_2025_06_25::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_06_27, - migrations::m_2025_06_27::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_07_08, - migrations::m_2025_07_08::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_10_03, - migrations::m_2025_10_03::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_11_12, - migrations::m_2025_11_12::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_12_01, - migrations::m_2025_12_01::SETTINGS_PATTERNS -); -define_query!( - SETTINGS_QUERY_2025_11_20, - migrations::m_2025_11_20::SETTINGS_PATTERNS -); - -// custom query -static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock = LazyLock::new(|| { - Query::new( - &tree_sitter_json::LANGUAGE.into(), - SETTINGS_NESTED_KEY_VALUE_PATTERN, - ) - .unwrap() -}); - -#[cfg(test)] -mod tests { - use super::*; - use unindent::Unindent as _; - - #[track_caller] - fn assert_migrated_correctly(migrated: Option, expected: Option<&str>) { - match (&migrated, &expected) { - (Some(migrated), Some(expected)) => { - pretty_assertions::assert_str_eq!(expected, migrated); - } - _ => { - pretty_assertions::assert_eq!(migrated.as_deref(), expected); - } - } - } - - fn assert_migrate_keymap(input: &str, output: Option<&str>) { - let migrated = migrate_keymap(input).unwrap(); - pretty_assertions::assert_eq!(migrated.as_deref(), output); - } - - #[track_caller] - fn assert_migrate_settings(input: &str, output: Option<&str>) { - let migrated = migrate_settings(input).unwrap(); - assert_migrated_correctly(migrated.clone(), output); - - // expect that rerunning the migration does not result in another migration - if let Some(migrated) = migrated { - let rerun = migrate_settings(&migrated).unwrap(); - assert_migrated_correctly(rerun, None); - } - } - - #[track_caller] - fn assert_migrate_settings_with_migrations( - migrations: &[MigrationType], - input: &str, - output: Option<&str>, - ) { - let migrated = run_migrations(input, migrations).unwrap(); - assert_migrated_correctly(migrated.clone(), output); - - // expect that rerunning the migration does not result in another migration - if let Some(migrated) = migrated { - let rerun = run_migrations(&migrated, migrations).unwrap(); - assert_migrated_correctly(rerun, None); - } - } - - #[test] - fn test_empty_content() { - assert_migrate_settings("", None) - } - - #[test] - fn test_replace_array_with_single_string() { - assert_migrate_keymap( - r#" - [ - { - "bindings": { - "cmd-1": ["workspace::ActivatePaneInDirection", "Up"] - } - } - ] - "#, - Some( - r#" - [ - { - "bindings": { - "cmd-1": "workspace::ActivatePaneUp" - } - } - ] - "#, - ), - ) - } - - #[test] - fn test_replace_action_argument_object_with_single_value() { - assert_migrate_keymap( - r#" - [ - { - "bindings": { - "cmd-1": ["editor::FoldAtLevel", { "level": 1 }] - } - } - ] - "#, - Some( - r#" - [ - { - "bindings": { - "cmd-1": ["editor::FoldAtLevel", 1] - } - } - ] - "#, - ), - ) - } - - #[test] - fn test_replace_action_argument_object_with_single_value_2() { - assert_migrate_keymap( - r#" - [ - { - "bindings": { - "cmd-1": ["vim::PushOperator", { "Object": { "some" : "value" } }] - } - } - ] - "#, - Some( - r#" - [ - { - "bindings": { - "cmd-1": ["vim::PushObject", { "some" : "value" }] - } - } - ] - "#, - ), - ) - } - - #[test] - fn test_rename_string_action() { - assert_migrate_keymap( - r#" - [ - { - "bindings": { - "cmd-1": "inline_completion::ToggleMenu" - } - } - ] - "#, - Some( - r#" - [ - { - "bindings": { - "cmd-1": "edit_prediction::ToggleMenu" - } - } - ] - "#, - ), - ) - } - - #[test] - fn test_rename_context_key() { - assert_migrate_keymap( - r#" - [ - { - "context": "Editor && inline_completion && !showing_completions" - } - ] - "#, - Some( - r#" - [ - { - "context": "Editor && edit_prediction && !showing_completions" - } - ] - "#, - ), - ) - } - - #[test] - fn test_incremental_migrations() { - // Here string transforms to array internally. Then, that array transforms back to string. - assert_migrate_keymap( - r#" - [ - { - "bindings": { - "ctrl-q": "editor::GoToHunk", // should remain same - "ctrl-w": "editor::GoToPrevHunk", // should rename - "ctrl-q": ["editor::GoToHunk", { "center_cursor": true }], // should transform - "ctrl-w": ["editor::GoToPreviousHunk", { "center_cursor": true }] // should transform - } - } - ] - "#, - Some( - r#" - [ - { - "bindings": { - "ctrl-q": "editor::GoToHunk", // should remain same - "ctrl-w": "editor::GoToPreviousHunk", // should rename - "ctrl-q": "editor::GoToHunk", // should transform - "ctrl-w": "editor::GoToPreviousHunk" // should transform - } - } - ] - "#, - ), - ) - } - - #[test] - fn test_action_argument_snake_case() { - // First performs transformations, then replacements - assert_migrate_keymap( - r#" - [ - { - "bindings": { - "cmd-1": ["vim::PushOperator", { "Object": { "around": false } }], - "cmd-3": ["pane::CloseActiveItem", { "saveIntent": "saveAll" }], - "cmd-2": ["vim::NextWordStart", { "ignorePunctuation": true }], - "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is - } - } - ] - "#, - Some( - r#" - [ - { - "bindings": { - "cmd-1": ["vim::PushObject", { "around": false }], - "cmd-3": ["pane::CloseActiveItem", { "save_intent": "save_all" }], - "cmd-2": ["vim::NextWordStart", { "ignore_punctuation": true }], - "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is - } - } - ] - "#, - ), - ) - } - - #[test] - fn test_replace_setting_name() { - assert_migrate_settings( - r#" - { - "show_inline_completions_in_menu": true, - "show_inline_completions": true, - "inline_completions_disabled_in": ["string"], - "inline_completions": { "some" : "value" } - } - "#, - Some( - r#" - { - "show_edit_predictions_in_menu": true, - "show_edit_predictions": true, - "edit_predictions_disabled_in": ["string"], - "edit_predictions": { "some" : "value" } - } - "#, - ), - ) - } - - #[test] - fn test_nested_string_replace_for_settings() { - assert_migrate_settings( - r#" - { - "features": { - "inline_completion_provider": "zed" - }, - } - "#, - Some( - r#" - { - "features": { - "edit_prediction_provider": "zed" - }, - } - "#, - ), - ) - } - - #[test] - fn test_replace_settings_in_languages() { - assert_migrate_settings( - r#" - { - "languages": { - "Astro": { - "show_inline_completions": true - } - } - } - "#, - Some( - r#" - { - "languages": { - "Astro": { - "show_edit_predictions": true - } - } - } - "#, - ), - ) - } - - #[test] - fn test_replace_settings_value() { - assert_migrate_settings( - r#" - { - "scrollbar": { - "diagnostics": true - }, - "chat_panel": { - "button": true - } - } - "#, - Some( - r#" - { - "scrollbar": { - "diagnostics": "all" - }, - "chat_panel": { - "button": "always" - } - } - "#, - ), - ) - } - - #[test] - fn test_replace_settings_name_and_value() { - assert_migrate_settings( - r#" - { - "tabs": { - "always_show_close_button": true - } - } - "#, - Some( - r#" - { - "tabs": { - "show_close_button": "always" - } - } - "#, - ), - ) - } - - #[test] - fn test_replace_bash_with_terminal_in_profiles() { - assert_migrate_settings( - r#" - { - "assistant": { - "profiles": { - "custom": { - "name": "Custom", - "tools": { - "bash": true, - "diagnostics": true - } - } - } - } - } - "#, - Some( - r#" - { - "agent": { - "profiles": { - "custom": { - "name": "Custom", - "tools": { - "terminal": true, - "diagnostics": true - } - } - } - } - } - "#, - ), - ) - } - - #[test] - fn test_replace_bash_false_with_terminal_in_profiles() { - assert_migrate_settings( - r#" - { - "assistant": { - "profiles": { - "custom": { - "name": "Custom", - "tools": { - "bash": false, - "diagnostics": true - } - } - } - } - } - "#, - Some( - r#" - { - "agent": { - "profiles": { - "custom": { - "name": "Custom", - "tools": { - "terminal": false, - "diagnostics": true - } - } - } - } - } - "#, - ), - ) - } - - #[test] - fn test_no_bash_in_profiles() { - assert_migrate_settings( - r#" - { - "assistant": { - "profiles": { - "custom": { - "name": "Custom", - "tools": { - "diagnostics": true, - "find_path": true, - "read_file": true - } - } - } - } - } - "#, - Some( - r#" - { - "agent": { - "profiles": { - "custom": { - "name": "Custom", - "tools": { - "diagnostics": true, - "find_path": true, - "read_file": true - } - } - } - } - } - "#, - ), - ) - } - - #[test] - fn test_rename_path_search_to_find_path() { - assert_migrate_settings( - r#" - { - "assistant": { - "profiles": { - "default": { - "tools": { - "path_search": true, - "read_file": true - } - } - } - } - } - "#, - Some( - r#" - { - "agent": { - "profiles": { - "default": { - "tools": { - "find_path": true, - "read_file": true - } - } - } - } - } - "#, - ), - ); - } - - #[test] - fn test_rename_assistant() { - assert_migrate_settings( - r#"{ - "assistant": { - "foo": "bar" - }, - "edit_predictions": { - "enabled_in_assistant": false, - } - }"#, - Some( - r#"{ - "agent": { - "foo": "bar" - }, - "edit_predictions": { - "enabled_in_text_threads": false, - } - }"#, - ), - ); - } - - #[test] - fn test_comment_duplicated_agent() { - assert_migrate_settings( - r#"{ - "agent": { - "name": "assistant-1", - "model": "gpt-4", // weird formatting - "utf8": "привіт" - }, - "something": "else", - "agent": { - "name": "assistant-2", - "model": "gemini-pro" - } - } - "#, - Some( - r#"{ - /* Duplicated key auto-commented: "agent": { - "name": "assistant-1", - "model": "gpt-4", // weird formatting - "utf8": "привіт" - }, */ - "something": "else", - "agent": { - "name": "assistant-2", - "model": "gemini-pro" - } - } - "#, - ), - ); - } - - #[test] - fn test_preferred_completion_mode_migration() { - assert_migrate_settings( - r#"{ - "agent": { - "preferred_completion_mode": "max", - "enabled": true - } - }"#, - Some( - r#"{ - "agent": { - "preferred_completion_mode": "burn", - "enabled": true - } - }"#, - ), - ); - - assert_migrate_settings( - r#"{ - "agent": { - "preferred_completion_mode": "normal", - "enabled": true - } - }"#, - None, - ); - - assert_migrate_settings( - r#"{ - "agent": { - "preferred_completion_mode": "burn", - "enabled": true - } - }"#, - None, - ); - - assert_migrate_settings( - r#"{ - "other_section": { - "preferred_completion_mode": "max" - }, - "agent": { - "preferred_completion_mode": "max" - } - }"#, - Some( - r#"{ - "other_section": { - "preferred_completion_mode": "max" - }, - "agent": { - "preferred_completion_mode": "burn" - } - }"#, - ), - ); - } - - #[test] - fn test_mcp_settings_migration() { - assert_migrate_settings_with_migrations( - &[MigrationType::TreeSitter( - migrations::m_2025_06_16::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_06_16, - )], - r#"{ - "context_servers": { - "empty_server": {}, - "extension_server": { - "settings": { - "foo": "bar" - } - }, - "custom_server": { - "command": { - "path": "foo", - "args": ["bar"], - "env": { - "FOO": "BAR" - } - } - }, - "invalid_server": { - "command": { - "path": "foo", - "args": ["bar"], - "env": { - "FOO": "BAR" - } - }, - "settings": { - "foo": "bar" - } - }, - "empty_server2": {}, - "extension_server2": { - "foo": "bar", - "settings": { - "foo": "bar" - }, - "bar": "foo" - }, - "custom_server2": { - "foo": "bar", - "command": { - "path": "foo", - "args": ["bar"], - "env": { - "FOO": "BAR" - } - }, - "bar": "foo" - }, - "invalid_server2": { - "foo": "bar", - "command": { - "path": "foo", - "args": ["bar"], - "env": { - "FOO": "BAR" - } - }, - "bar": "foo", - "settings": { - "foo": "bar" - } - } - } -}"#, - Some( - r#"{ - "context_servers": { - "empty_server": { - "source": "extension", - "settings": {} - }, - "extension_server": { - "source": "extension", - "settings": { - "foo": "bar" - } - }, - "custom_server": { - "source": "custom", - "command": { - "path": "foo", - "args": ["bar"], - "env": { - "FOO": "BAR" - } - } - }, - "invalid_server": { - "source": "custom", - "command": { - "path": "foo", - "args": ["bar"], - "env": { - "FOO": "BAR" - } - }, - "settings": { - "foo": "bar" - } - }, - "empty_server2": { - "source": "extension", - "settings": {} - }, - "extension_server2": { - "source": "extension", - "foo": "bar", - "settings": { - "foo": "bar" - }, - "bar": "foo" - }, - "custom_server2": { - "source": "custom", - "foo": "bar", - "command": { - "path": "foo", - "args": ["bar"], - "env": { - "FOO": "BAR" - } - }, - "bar": "foo" - }, - "invalid_server2": { - "source": "custom", - "foo": "bar", - "command": { - "path": "foo", - "args": ["bar"], - "env": { - "FOO": "BAR" - } - }, - "bar": "foo", - "settings": { - "foo": "bar" - } - } - } -}"#, - ), - ); - } - - #[test] - fn test_mcp_settings_migration_doesnt_change_valid_settings() { - let settings = r#"{ - "context_servers": { - "empty_server": { - "source": "extension", - "settings": {} - }, - "extension_server": { - "source": "extension", - "settings": { - "foo": "bar" - } - }, - "custom_server": { - "source": "custom", - "command": { - "path": "foo", - "args": ["bar"], - "env": { - "FOO": "BAR" - } - } - }, - "invalid_server": { - "source": "custom", - "command": { - "path": "foo", - "args": ["bar"], - "env": { - "FOO": "BAR" - } - }, - "settings": { - "foo": "bar" - } - } - } -}"#; - assert_migrate_settings_with_migrations( - &[MigrationType::TreeSitter( - migrations::m_2025_06_16::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_06_16, - )], - settings, - None, - ); - } - - #[test] - fn test_custom_agent_server_settings_migration() { - assert_migrate_settings_with_migrations( - &[MigrationType::TreeSitter( - migrations::m_2025_11_20::SETTINGS_PATTERNS, - &SETTINGS_QUERY_2025_11_20, - )], - r#"{ - "agent_servers": { - "gemini": { - "default_model": "gemini-1.5-pro" - }, - "claude": {}, - "codex": {}, - "my-custom-agent": { - "command": "/path/to/agent", - "args": ["--foo"], - "default_model": "my-model" - }, - "already-migrated-agent": { - "type": "custom", - "command": "/path/to/agent" - }, - "future-extension-agent": { - "type": "extension", - "default_model": "ext-model" - } - } -}"#, - Some( - r#"{ - "agent_servers": { - "gemini": { - "default_model": "gemini-1.5-pro" - }, - "claude": {}, - "codex": {}, - "my-custom-agent": { - "type": "custom", - "command": "/path/to/agent", - "args": ["--foo"], - "default_model": "my-model" - }, - "already-migrated-agent": { - "type": "custom", - "command": "/path/to/agent" - }, - "future-extension-agent": { - "type": "extension", - "default_model": "ext-model" - } - } -}"#, - ), - ); - } - - #[test] - fn test_remove_version_fields() { - assert_migrate_settings( - r#"{ - "language_models": { - "anthropic": { - "version": "1", - "api_url": "https://api.anthropic.com" - }, - "openai": { - "version": "1", - "api_url": "https://api.openai.com/v1" - } - }, - "agent": { - "version": "2", - "enabled": true, - "preferred_completion_mode": "normal", - "button": true, - "dock": "right", - "default_width": 640, - "default_height": 320, - "default_model": { - "provider": "zed.dev", - "model": "claude-sonnet-4" - } - } -}"#, - Some( - r#"{ - "language_models": { - "anthropic": { - "api_url": "https://api.anthropic.com" - }, - "openai": { - "api_url": "https://api.openai.com/v1" - } - }, - "agent": { - "enabled": true, - "preferred_completion_mode": "normal", - "button": true, - "dock": "right", - "default_width": 640, - "default_height": 320, - "default_model": { - "provider": "zed.dev", - "model": "claude-sonnet-4" - } - } -}"#, - ), - ); - - // Test that version fields in other contexts are not removed - assert_migrate_settings( - r#"{ - "language_models": { - "other_provider": { - "version": "1", - "api_url": "https://api.example.com" - } - }, - "other_section": { - "version": "1" - } -}"#, - None, - ); - } - - #[test] - fn test_flatten_context_server_command() { - assert_migrate_settings( - r#"{ - "context_servers": { - "some-mcp-server": { - "command": { - "path": "npx", - "args": [ - "-y", - "@supabase/mcp-server-supabase@latest", - "--read-only", - "--project-ref=" - ], - "env": { - "SUPABASE_ACCESS_TOKEN": "" - } - } - } - } -}"#, - Some( - r#"{ - "context_servers": { - "some-mcp-server": { - "command": "npx", - "args": [ - "-y", - "@supabase/mcp-server-supabase@latest", - "--read-only", - "--project-ref=" - ], - "env": { - "SUPABASE_ACCESS_TOKEN": "" - } - } - } -}"#, - ), - ); - - // Test with additional keys in server object - assert_migrate_settings( - r#"{ - "context_servers": { - "server-with-extras": { - "command": { - "path": "/usr/bin/node", - "args": ["server.js"] - }, - "settings": {} - } - } -}"#, - Some( - r#"{ - "context_servers": { - "server-with-extras": { - "command": "/usr/bin/node", - "args": ["server.js"], - "settings": {} - } - } -}"#, - ), - ); - - // Test command without args or env - assert_migrate_settings( - r#"{ - "context_servers": { - "simple-server": { - "command": { - "path": "simple-mcp-server" - } - } - } -}"#, - Some( - r#"{ - "context_servers": { - "simple-server": { - "command": "simple-mcp-server" - } - } -}"#, - ), - ); - } - - #[test] - fn test_flatten_code_action_formatters_basic_array() { - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_01::flatten_code_actions_formatters, - )], - &r#"{ - "formatter": [ - { - "code_actions": { - "included-1": true, - "included-2": true, - "excluded": false, - } - } - ] - }"# - .unindent(), - Some( - &r#"{ - "formatter": [ - { - "code_action": "included-1" - }, - { - "code_action": "included-2" - } - ] - }"# - .unindent(), - ), - ); - } - - #[test] - fn test_flatten_code_action_formatters_basic_object() { - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_01::flatten_code_actions_formatters, - )], - &r#"{ - "formatter": { - "code_actions": { - "included-1": true, - "excluded": false, - "included-2": true - } - } - }"# - .unindent(), - Some( - &r#"{ - "formatter": [ - { - "code_action": "included-1" - }, - { - "code_action": "included-2" - } - ] - }"# - .unindent(), - ), - ); - } - - #[test] - fn test_flatten_code_action_formatters_array_with_multiple_action_blocks() { - assert_migrate_settings( - &r#"{ - "formatter": [ - { - "code_actions": { - "included-1": true, - "included-2": true, - "excluded": false, - } - }, - { - "language_server": "ruff" - }, - { - "code_actions": { - "excluded": false, - "excluded-2": false, - } - } - // some comment - , - { - "code_actions": { - "excluded": false, - "included-3": true, - "included-4": true, - } - }, - ] - }"# - .unindent(), - Some( - &r#"{ - "formatter": [ - { - "code_action": "included-1" - }, - { - "code_action": "included-2" - }, - { - "language_server": "ruff" - }, - { - "code_action": "included-3" - }, - { - "code_action": "included-4" - } - ] - }"# - .unindent(), - ), - ); - } - - #[test] - fn test_flatten_code_action_formatters_array_with_multiple_action_blocks_in_languages() { - assert_migrate_settings( - &r#"{ - "languages": { - "Rust": { - "formatter": [ - { - "code_actions": { - "included-1": true, - "included-2": true, - "excluded": false, - } - }, - { - "language_server": "ruff" - }, - { - "code_actions": { - "excluded": false, - "excluded-2": false, - } - } - // some comment - , - { - "code_actions": { - "excluded": false, - "included-3": true, - "included-4": true, - } - }, - ] - } - } - }"# - .unindent(), - Some( - &r#"{ - "languages": { - "Rust": { - "formatter": [ - { - "code_action": "included-1" - }, - { - "code_action": "included-2" - }, - { - "language_server": "ruff" - }, - { - "code_action": "included-3" - }, - { - "code_action": "included-4" - } - ] - } - } - }"# - .unindent(), - ), - ); - } - - #[test] - fn test_flatten_code_action_formatters_array_with_multiple_action_blocks_in_defaults_and_multiple_languages() - { - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_01::flatten_code_actions_formatters, - )], - &r#"{ - "formatter": { - "code_actions": { - "default-1": true, - "default-2": true, - "default-3": true, - "default-4": true, - } - }, - "languages": { - "Rust": { - "formatter": [ - { - "code_actions": { - "included-1": true, - "included-2": true, - "excluded": false, - } - }, - { - "language_server": "ruff" - }, - { - "code_actions": { - "excluded": false, - "excluded-2": false, - } - } - // some comment - , - { - "code_actions": { - "excluded": false, - "included-3": true, - "included-4": true, - } - }, - ] - }, - "Python": { - "formatter": [ - { - "language_server": "ruff" - }, - { - "code_actions": { - "excluded": false, - "excluded-2": false, - } - } - // some comment - , - { - "code_actions": { - "excluded": false, - "included-3": true, - "included-4": true, - } - }, - ] - } - } - }"# - .unindent(), - Some( - &r#"{ - "formatter": [ - { - "code_action": "default-1" - }, - { - "code_action": "default-2" - }, - { - "code_action": "default-3" - }, - { - "code_action": "default-4" - } - ], - "languages": { - "Rust": { - "formatter": [ - { - "code_action": "included-1" - }, - { - "code_action": "included-2" - }, - { - "language_server": "ruff" - }, - { - "code_action": "included-3" - }, - { - "code_action": "included-4" - } - ] - }, - "Python": { - "formatter": [ - { - "language_server": "ruff" - }, - { - "code_action": "included-3" - }, - { - "code_action": "included-4" - } - ] - } - } - }"# - .unindent(), - ), - ); - } - - #[test] - fn test_flatten_code_action_formatters_array_with_format_on_save_and_multiple_languages() { - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_01::flatten_code_actions_formatters, - )], - &r#"{ - "formatter": { - "code_actions": { - "default-1": true, - "default-2": true, - "default-3": true, - "default-4": true, - } - }, - "format_on_save": [ - { - "code_actions": { - "included-1": true, - "included-2": true, - "excluded": false, - } - }, - { - "language_server": "ruff" - }, - { - "code_actions": { - "excluded": false, - "excluded-2": false, - } - } - // some comment - , - { - "code_actions": { - "excluded": false, - "included-3": true, - "included-4": true, - } - }, - ], - "languages": { - "Rust": { - "format_on_save": "prettier", - "formatter": [ - { - "code_actions": { - "included-1": true, - "included-2": true, - "excluded": false, - } - }, - { - "language_server": "ruff" - }, - { - "code_actions": { - "excluded": false, - "excluded-2": false, - } - } - // some comment - , - { - "code_actions": { - "excluded": false, - "included-3": true, - "included-4": true, - } - }, - ] - }, - "Python": { - "format_on_save": { - "code_actions": { - "on-save-1": true, - "on-save-2": true, - } - }, - "formatter": [ - { - "language_server": "ruff" - }, - { - "code_actions": { - "excluded": false, - "excluded-2": false, - } - } - // some comment - , - { - "code_actions": { - "excluded": false, - "included-3": true, - "included-4": true, - } - }, - ] - } - } - }"# - .unindent(), - Some( - &r#" - { - "formatter": [ - { - "code_action": "default-1" - }, - { - "code_action": "default-2" - }, - { - "code_action": "default-3" - }, - { - "code_action": "default-4" - } - ], - "format_on_save": [ - { - "code_action": "included-1" - }, - { - "code_action": "included-2" - }, - { - "language_server": "ruff" - }, - { - "code_action": "included-3" - }, - { - "code_action": "included-4" - } - ], - "languages": { - "Rust": { - "format_on_save": "prettier", - "formatter": [ - { - "code_action": "included-1" - }, - { - "code_action": "included-2" - }, - { - "language_server": "ruff" - }, - { - "code_action": "included-3" - }, - { - "code_action": "included-4" - } - ] - }, - "Python": { - "format_on_save": [ - { - "code_action": "on-save-1" - }, - { - "code_action": "on-save-2" - } - ], - "formatter": [ - { - "language_server": "ruff" - }, - { - "code_action": "included-3" - }, - { - "code_action": "included-4" - } - ] - } - } - }"# - .unindent(), - ), - ); - } - - #[test] - fn test_format_on_save_formatter_migration_basic() { - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_02::remove_formatters_on_save, - )], - &r#"{ - "format_on_save": "prettier" - }"# - .unindent(), - Some( - &r#"{ - "formatter": "prettier", - "format_on_save": "on" - }"# - .unindent(), - ), - ); - } - - #[test] - fn test_format_on_save_formatter_migration_array() { - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_02::remove_formatters_on_save, - )], - &r#"{ - "format_on_save": ["prettier", {"language_server": "eslint"}] - }"# - .unindent(), - Some( - &r#"{ - "formatter": [ - "prettier", - { - "language_server": "eslint" - } - ], - "format_on_save": "on" - }"# - .unindent(), - ), - ); - } - - #[test] - fn test_format_on_save_on_off_unchanged() { - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_02::remove_formatters_on_save, - )], - &r#"{ - "format_on_save": "on" - }"# - .unindent(), - None, - ); - - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_02::remove_formatters_on_save, - )], - &r#"{ - "format_on_save": "off" - }"# - .unindent(), - None, - ); - } - - #[test] - fn test_format_on_save_formatter_migration_in_languages() { - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_02::remove_formatters_on_save, - )], - &r#"{ - "languages": { - "Rust": { - "format_on_save": "rust-analyzer" - }, - "Python": { - "format_on_save": ["ruff", "black"] - } - } - }"# - .unindent(), - Some( - &r#"{ - "languages": { - "Rust": { - "formatter": "rust-analyzer", - "format_on_save": "on" - }, - "Python": { - "formatter": [ - "ruff", - "black" - ], - "format_on_save": "on" - } - } - }"# - .unindent(), - ), - ); - } - - #[test] - fn test_format_on_save_formatter_migration_mixed_global_and_languages() { - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_02::remove_formatters_on_save, - )], - &r#"{ - "format_on_save": "prettier", - "languages": { - "Rust": { - "format_on_save": "rust-analyzer" - }, - "Python": { - "format_on_save": "on" - } - } - }"# - .unindent(), - Some( - &r#"{ - "formatter": "prettier", - "format_on_save": "on", - "languages": { - "Rust": { - "formatter": "rust-analyzer", - "format_on_save": "on" - }, - "Python": { - "format_on_save": "on" - } - } - }"# - .unindent(), - ), - ); - } - - #[test] - fn test_format_on_save_no_migration_when_no_format_on_save() { - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_02::remove_formatters_on_save, - )], - &r#"{ - "formatter": ["prettier"] - }"# - .unindent(), - None, - ); - } - - #[test] - fn test_restore_code_actions_on_format() { - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_16::restore_code_actions_on_format, - )], - &r#"{ - "formatter": { - "code_action": "foo" - } - }"# - .unindent(), - Some( - &r#"{ - "code_actions_on_format": { - "foo": true - }, - "formatter": [] - }"# - .unindent(), - ), - ); - - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_16::restore_code_actions_on_format, - )], - &r#"{ - "formatter": [ - { "code_action": "foo" }, - "auto" - ] - }"# - .unindent(), - None, - ); - - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_16::restore_code_actions_on_format, - )], - &r#"{ - "formatter": { - "code_action": "foo" - }, - "code_actions_on_format": { - "bar": true, - "baz": false - } - }"# - .unindent(), - Some( - &r#"{ - "formatter": [], - "code_actions_on_format": { - "foo": true, - "bar": true, - "baz": false - } - }"# - .unindent(), - ), - ); - - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_16::restore_code_actions_on_format, - )], - &r#"{ - "formatter": [ - { "code_action": "foo" }, - { "code_action": "qux" }, - ], - "code_actions_on_format": { - "bar": true, - "baz": false - } - }"# - .unindent(), - Some( - &r#"{ - "formatter": [], - "code_actions_on_format": { - "foo": true, - "qux": true, - "bar": true, - "baz": false - } - }"# - .unindent(), - ), - ); - - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_16::restore_code_actions_on_format, - )], - &r#"{ - "formatter": [], - "code_actions_on_format": { - "bar": true, - "baz": false - } - }"# - .unindent(), - None, - ); - } - - #[test] - fn test_make_file_finder_include_ignored_an_enum() { - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum, - )], - &r#"{ }"#.unindent(), - None, - ); - - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum, - )], - &r#"{ - "file_finder": { - "include_ignored": true - } - }"# - .unindent(), - Some( - &r#"{ - "file_finder": { - "include_ignored": "all" - } - }"# - .unindent(), - ), - ); - - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum, - )], - &r#"{ - "file_finder": { - "include_ignored": false - } - }"# - .unindent(), - Some( - &r#"{ - "file_finder": { - "include_ignored": "indexed" - } - }"# - .unindent(), - ), - ); - - assert_migrate_settings_with_migrations( - &[MigrationType::Json( - migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum, - )], - &r#"{ - "file_finder": { - "include_ignored": null - } - }"# - .unindent(), - Some( - &r#"{ - "file_finder": { - "include_ignored": "smart" - } - }"# - .unindent(), - ), - ); - } - - #[test] - fn test_remove_context_server_source() { - assert_migrate_settings( - &r#" - { - "context_servers": { - "extension_server": { - "source": "extension", - "settings": { - "foo": "bar" - } - }, - "custom_server": { - "source": "custom", - "command": "foo", - "args": ["bar"], - "env": { - "FOO": "BAR" - } - }, - } - } - "# - .unindent(), - Some( - &r#" - { - "context_servers": { - "extension_server": { - "settings": { - "foo": "bar" - } - }, - "custom_server": { - "command": "foo", - "args": ["bar"], - "env": { - "FOO": "BAR" - } - }, - } - } - "# - .unindent(), - ), - ); - } - - #[test] - fn test_project_panel_open_file_on_paste_migration() { - assert_migrate_settings( - &r#" - { - "project_panel": { - "open_file_on_paste": true - } - } - "# - .unindent(), - Some( - &r#" - { - "project_panel": { - "auto_open": { "on_paste": true } - } - } - "# - .unindent(), - ), - ); - - assert_migrate_settings( - &r#" - { - "project_panel": { - "open_file_on_paste": false - } - } - "# - .unindent(), - Some( - &r#" - { - "project_panel": { - "auto_open": { "on_paste": false } - } - } - "# - .unindent(), - ), - ); - } - - #[test] - fn test_enable_preview_from_code_navigation_migration() { - assert_migrate_settings( - &r#" - { - "other_setting_1": 1, - "preview_tabs": { - "other_setting_2": 2, - "enable_preview_from_code_navigation": false - } - } - "# - .unindent(), - Some( - &r#" - { - "other_setting_1": 1, - "preview_tabs": { - "other_setting_2": 2, - "enable_keep_preview_on_code_navigation": false - } - } - "# - .unindent(), - ), - ); - - assert_migrate_settings( - &r#" - { - "other_setting_1": 1, - "preview_tabs": { - "other_setting_2": 2, - "enable_preview_from_code_navigation": true - } - } - "# - .unindent(), - Some( - &r#" - { - "other_setting_1": 1, - "preview_tabs": { - "other_setting_2": 2, - "enable_keep_preview_on_code_navigation": true - } - } - "# - .unindent(), - ), - ); - } -} diff --git a/crates/migrator/src/patterns.rs b/crates/migrator/src/patterns.rs deleted file mode 100644 index 4132c93d93..0000000000 --- a/crates/migrator/src/patterns.rs +++ /dev/null @@ -1,14 +0,0 @@ -mod keymap; -mod settings; - -pub(crate) use keymap::{ - KEYMAP_ACTION_ARRAY_ARGUMENT_AS_OBJECT_PATTERN, KEYMAP_ACTION_ARRAY_PATTERN, - KEYMAP_ACTION_STRING_PATTERN, KEYMAP_CONTEXT_PATTERN, -}; - -pub(crate) use settings::{ - SETTINGS_ASSISTANT_PATTERN, SETTINGS_ASSISTANT_TOOLS_PATTERN, - SETTINGS_DUPLICATED_AGENT_PATTERN, SETTINGS_EDIT_PREDICTIONS_ASSISTANT_PATTERN, - SETTINGS_LANGUAGES_PATTERN, SETTINGS_NESTED_KEY_VALUE_PATTERN, SETTINGS_ROOT_KEY_VALUE_PATTERN, - migrate_language_setting, -}; diff --git a/crates/migrator/src/patterns/keymap.rs b/crates/migrator/src/patterns/keymap.rs deleted file mode 100644 index 439c10fd4a..0000000000 --- a/crates/migrator/src/patterns/keymap.rs +++ /dev/null @@ -1,77 +0,0 @@ -pub const KEYMAP_ACTION_ARRAY_PATTERN: &str = r#"(document - (array - (object - (pair - key: (string (string_content) @name) - value: ( - (object - (pair - key: (string) - value: ((array - . (string (string_content) @action_name) - . (string (string_content) @argument) - .)) @array - ) - ) - ) - ) - ) - ) - (#eq? @name "bindings") -)"#; - -pub const KEYMAP_ACTION_STRING_PATTERN: &str = r#"(document - (array - (object - (pair - key: (string (string_content) @name) - value: ( - (object - (pair - key: (string) - value: (string (string_content) @action_name) - ) - ) - ) - ) - ) - ) - (#eq? @name "bindings") -)"#; - -pub const KEYMAP_CONTEXT_PATTERN: &str = r#"(document - (array - (object - (pair - key: (string (string_content) @name) - value: (string (string_content) @context_predicate) - ) - ) - ) - (#eq? @name "context") -)"#; - -pub const KEYMAP_ACTION_ARRAY_ARGUMENT_AS_OBJECT_PATTERN: &str = r#"(document - (array - (object - (pair - key: (string (string_content) @name) - value: ( - (object - (pair - key: (string) - value: ((array - . (string (string_content) @action_name) - . (object - (pair - key: (string (string_content) @argument_key) - value: (_) @argument_value)) - . ) @array - )) - ) - ) - ) - ) - ) - (#eq? @name "bindings") -)"#; diff --git a/crates/migrator/src/patterns/settings.rs b/crates/migrator/src/patterns/settings.rs deleted file mode 100644 index a068cce23b..0000000000 --- a/crates/migrator/src/patterns/settings.rs +++ /dev/null @@ -1,131 +0,0 @@ -pub const SETTINGS_ROOT_KEY_VALUE_PATTERN: &str = r#"(document - (object - (pair - key: (string (string_content) @name) - value: (_) @value - ) - ) -)"#; - -pub const SETTINGS_NESTED_KEY_VALUE_PATTERN: &str = r#"(document - (object - (pair - key: (string (string_content) @parent_key) - value: (object - (pair - key: (string (string_content) @setting_name) - value: (_) @setting_value - ) - ) - ) - ) -)"#; - -pub const SETTINGS_LANGUAGES_PATTERN: &str = r#"(document - (object - (pair - key: (string (string_content) @languages) - value: (object - (pair - key: (string) - value: (object - (pair - key: (string (string_content) @setting_name) - value: (_) @value - ) - ) - )) - ) - ) - (#eq? @languages "languages") -)"#; - -pub const SETTINGS_ASSISTANT_TOOLS_PATTERN: &str = r#"(document - (object - (pair - key: (string (string_content) @assistant) - value: (object - (pair - key: (string (string_content) @profiles) - value: (object - (pair - key: (_) - value: (object - (pair - key: (string (string_content) @tools_key) - value: (object - (pair - key: (string (string_content) @tool_name) - value: (_) @tool_value - ) - ) - ) - ) - ) - ) - ) - ) - ) - ) - (#eq? @assistant "assistant") - (#eq? @profiles "profiles") - (#eq? @tools_key "tools") -)"#; - -pub const SETTINGS_ASSISTANT_PATTERN: &str = r#"(document - (object - (pair - key: (string (string_content) @key) - ) - ) - (#eq? @key "assistant") -)"#; - -pub const SETTINGS_EDIT_PREDICTIONS_ASSISTANT_PATTERN: &str = r#"(document - (object - (pair - key: (string (string_content) @edit_predictions) - value: (object - (pair key: (string (string_content) @enabled_in_assistant)) - ) - ) - ) - (#eq? @edit_predictions "edit_predictions") - (#eq? @enabled_in_assistant "enabled_in_assistant") -)"#; - -pub const SETTINGS_DUPLICATED_AGENT_PATTERN: &str = r#"(document - (object - (pair - key: (string (string_content) @agent1) - value: (_) - ) @pair1 - (pair - key: (string (string_content) @agent2) - value: (_) - ) - ) - (#eq? @agent1 "agent") - (#eq? @agent2 "agent") -)"#; - -/// Migrate language settings, -/// calls `migrate_fn` with the top level object as well as all language settings under the "languages" key -/// Fails early if `migrate_fn` returns an error at any point -pub fn migrate_language_setting( - value: &mut serde_json::Value, - migrate_fn: fn(&mut serde_json::Value, path: &[&str]) -> anyhow::Result<()>, -) -> anyhow::Result<()> { - migrate_fn(value, &[])?; - let languages = value - .as_object_mut() - .and_then(|obj| obj.get_mut("languages")) - .and_then(|languages| languages.as_object_mut()); - if let Some(languages) = languages { - for (language_name, language) in languages.iter_mut() { - let path = vec!["languages", language_name]; - migrate_fn(language, &path)?; - } - } - Ok(()) -} diff --git a/crates/miniprofiler_ui/Cargo.toml b/crates/miniprofiler_ui/Cargo.toml deleted file mode 100644 index bb508a188e..0000000000 --- a/crates/miniprofiler_ui/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "miniprofiler_ui" -version = "0.1.0" -publish.workspace = true -edition.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/miniprofiler_ui.rs" - -[dependencies] -gpui.workspace = true -zed_actions.workspace = true -workspace.workspace = true -util.workspace = true -serde_json.workspace = true -smol.workspace = true - -[dev-dependencies] -gpui = { workspace = true, features = ["test-support"] } diff --git a/crates/miniprofiler_ui/LICENSE-GPL b/crates/miniprofiler_ui/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/miniprofiler_ui/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/miniprofiler_ui/src/miniprofiler_ui.rs b/crates/miniprofiler_ui/src/miniprofiler_ui.rs deleted file mode 100644 index ea59b43cc1..0000000000 --- a/crates/miniprofiler_ui/src/miniprofiler_ui.rs +++ /dev/null @@ -1,410 +0,0 @@ -use std::{ - ops::Range, - path::PathBuf, - rc::Rc, - time::{Duration, Instant}, -}; - -use gpui::{ - App, AppContext, ClipboardItem, Context, Div, Entity, Hsla, InteractiveElement, - ParentElement as _, Render, SerializedTaskTiming, SharedString, StatefulInteractiveElement, - Styled, Task, TaskTiming, TitlebarOptions, UniformListScrollHandle, WindowBounds, WindowHandle, - WindowOptions, div, prelude::FluentBuilder, px, relative, size, uniform_list, -}; -use util::ResultExt; -use workspace::{ - Workspace, - ui::{ - ActiveTheme, Button, ButtonCommon, ButtonStyle, Checkbox, Clickable, Divider, - ScrollableHandle as _, ToggleState, Tooltip, WithScrollbar, h_flex, v_flex, - }, -}; -use zed_actions::OpenPerformanceProfiler; - -pub fn init(startup_time: Instant, cx: &mut App) { - cx.observe_new(move |workspace: &mut workspace::Workspace, _, _| { - workspace.register_action(move |workspace, _: &OpenPerformanceProfiler, window, cx| { - let window_handle = window - .window_handle() - .downcast::() - .expect("Workspaces are root Windows"); - open_performance_profiler(startup_time, workspace, window_handle, cx); - }); - }) - .detach(); -} - -fn open_performance_profiler( - startup_time: Instant, - _workspace: &mut workspace::Workspace, - workspace_handle: WindowHandle, - cx: &mut App, -) { - let existing_window = cx - .windows() - .into_iter() - .find_map(|window| window.downcast::()); - - if let Some(existing_window) = existing_window { - existing_window - .update(cx, |profiler_window, window, _cx| { - profiler_window.workspace = Some(workspace_handle); - window.activate_window(); - }) - .log_err(); - return; - } - - let default_bounds = size(px(1280.), px(720.)); // 16:9 - - cx.open_window( - WindowOptions { - titlebar: Some(TitlebarOptions { - title: Some("Profiler Window".into()), - appears_transparent: false, - traffic_light_position: None, - }), - focus: true, - show: true, - is_movable: true, - kind: gpui::WindowKind::Normal, - window_background: cx.theme().window_background_appearance(), - window_decorations: None, - window_min_size: Some(default_bounds), - window_bounds: Some(WindowBounds::centered(default_bounds, cx)), - ..Default::default() - }, - |_window, cx| ProfilerWindow::new(startup_time, Some(workspace_handle), cx), - ) - .log_err(); -} - -enum DataMode { - Realtime(Option>), - Snapshot(Vec), -} - -struct TimingBar { - location: &'static core::panic::Location<'static>, - start: Instant, - end: Instant, - color: Hsla, -} - -pub struct ProfilerWindow { - startup_time: Instant, - data: DataMode, - include_self_timings: ToggleState, - autoscroll: bool, - scroll_handle: UniformListScrollHandle, - workspace: Option>, - _refresh: Option>, -} - -impl ProfilerWindow { - pub fn new( - startup_time: Instant, - workspace_handle: Option>, - cx: &mut App, - ) -> Entity { - let entity = cx.new(|cx| ProfilerWindow { - startup_time, - data: DataMode::Realtime(None), - include_self_timings: ToggleState::Unselected, - autoscroll: true, - scroll_handle: UniformListScrollHandle::default(), - workspace: workspace_handle, - _refresh: Some(Self::begin_listen(cx)), - }); - - entity - } - - fn begin_listen(cx: &mut Context) -> Task<()> { - cx.spawn(async move |this, cx| { - loop { - let data = cx - .foreground_executor() - .dispatcher - .get_current_thread_timings(); - - this.update(cx, |this: &mut ProfilerWindow, cx| { - this.data = DataMode::Realtime(Some(data)); - cx.notify(); - }) - .ok(); - - // yield to the executor - cx.background_executor() - .timer(Duration::from_micros(1)) - .await; - } - }) - } - - fn get_timings(&self) -> Option<&Vec> { - match &self.data { - DataMode::Realtime(data) => data.as_ref(), - DataMode::Snapshot(data) => Some(data), - } - } - - fn render_timing(value_range: Range, item: TimingBar, cx: &App) -> Div { - let time_ms = item.end.duration_since(item.start).as_secs_f32() * 1000f32; - - let remap = value_range - .end - .duration_since(value_range.start) - .as_secs_f32() - * 1000f32; - - let start = (item.start.duration_since(value_range.start).as_secs_f32() * 1000f32) / remap; - let end = (item.end.duration_since(value_range.start).as_secs_f32() * 1000f32) / remap; - - let bar_width = end - start.abs(); - - let location = item - .location - .file() - .rsplit_once("/") - .unwrap_or(("", item.location.file())) - .1; - let location = location.rsplit_once("\\").unwrap_or(("", location)).1; - - let label = SharedString::from(format!( - "{}:{}:{}", - location, - item.location.line(), - item.location.column() - )); - - h_flex() - .gap_2() - .w_full() - .h(px(32.0)) - .child( - div() - .id(label.clone()) - .w(px(200.0)) - .flex_shrink_0() - .overflow_hidden() - .child(div().text_ellipsis().child(label.clone())) - .tooltip(Tooltip::text(label.clone())) - .on_click(move |_, _, cx| { - cx.write_to_clipboard(ClipboardItem::new_string(label.to_string())) - }), - ) - .child( - div() - .flex_1() - .h(px(24.0)) - .bg(cx.theme().colors().background) - .rounded_md() - .p(px(2.0)) - .relative() - .child( - div() - .absolute() - .h_full() - .rounded_sm() - .bg(item.color) - .left(relative(start.max(0f32))) - .w(relative(bar_width)), - ), - ) - .child( - div() - .min_w(px(70.)) - .flex_shrink_0() - .text_right() - .child(format!("{:.1} ms", time_ms)), - ) - } -} - -impl Render for ProfilerWindow { - fn render( - &mut self, - window: &mut gpui::Window, - cx: &mut gpui::Context, - ) -> impl gpui::IntoElement { - let scroll_offset = self.scroll_handle.offset(); - let max_offset = self.scroll_handle.max_offset(); - self.autoscroll = -scroll_offset.y >= (max_offset.height - px(24.)); - if self.autoscroll { - self.scroll_handle.scroll_to_bottom(); - } - - v_flex() - .id("profiler") - .w_full() - .h_full() - .bg(cx.theme().colors().surface_background) - .text_color(cx.theme().colors().text) - .child( - h_flex() - .py_2() - .px_4() - .w_full() - .justify_between() - .child( - h_flex() - .gap_2() - .child( - Button::new( - "switch-mode", - match self.data { - DataMode::Snapshot { .. } => "Resume", - DataMode::Realtime(_) => "Pause", - }, - ) - .style(ButtonStyle::Filled) - .on_click(cx.listener( - |this, _, _window, cx| { - match &this.data { - DataMode::Realtime(Some(data)) => { - this._refresh = None; - this.data = DataMode::Snapshot(data.clone()); - } - DataMode::Snapshot { .. } => { - this._refresh = Some(Self::begin_listen(cx)); - this.data = DataMode::Realtime(None); - } - _ => {} - }; - cx.notify(); - }, - )), - ) - .child( - Button::new("export-data", "Save") - .style(ButtonStyle::Filled) - .on_click(cx.listener(|this, _, _window, cx| { - let Some(workspace) = this.workspace else { - return; - }; - - let Some(data) = this.get_timings() else { - return; - }; - let timings = - SerializedTaskTiming::convert(this.startup_time, &data); - - let active_path = workspace - .read_with(cx, |workspace, cx| { - workspace.most_recent_active_path(cx) - }) - .log_err() - .flatten() - .and_then(|p| p.parent().map(|p| p.to_owned())) - .unwrap_or_else(|| PathBuf::default()); - - let path = cx.prompt_for_new_path( - &active_path, - Some("performance_profile.miniprof"), - ); - - cx.background_spawn(async move { - let path = path.await; - let path = - path.log_err().and_then(|p| p.log_err()).flatten(); - - let Some(path) = path else { - return; - }; - - let Some(timings) = - serde_json::to_string(&timings).log_err() - else { - return; - }; - - smol::fs::write(path, &timings).await.log_err(); - }) - .detach(); - })), - ), - ) - .child( - Checkbox::new("include-self", self.include_self_timings) - .label("Include profiler timings") - .on_click(cx.listener(|this, checked, _window, cx| { - this.include_self_timings = *checked; - cx.notify(); - })), - ), - ) - .when_some(self.get_timings(), |div, e| { - if e.len() == 0 { - return div; - } - - let min = e[0].start; - let max = e[e.len() - 1].end.unwrap_or_else(|| Instant::now()); - let timings = Rc::new( - e.into_iter() - .filter(|timing| { - timing - .end - .unwrap_or_else(|| Instant::now()) - .duration_since(timing.start) - .as_millis() - >= 1 - }) - .filter(|timing| { - if self.include_self_timings.selected() { - true - } else { - !timing.location.file().ends_with("miniprofiler_ui.rs") - } - }) - .cloned() - .collect::>(), - ); - - div.child(Divider::horizontal()).child( - v_flex() - .id("timings.bars") - .w_full() - .h_full() - .gap_2() - .child( - uniform_list("list", timings.len(), { - let timings = timings.clone(); - move |visible_range, _, cx| { - let mut items = vec![]; - for i in visible_range { - let timing = &timings[i]; - let value_range = - max.checked_sub(Duration::from_secs(10)).unwrap_or(min) - ..max; - items.push(Self::render_timing( - value_range, - TimingBar { - location: timing.location, - start: timing.start, - end: timing.end.unwrap_or_else(|| Instant::now()), - color: cx - .theme() - .accents() - .color_for_index(i as u32), - }, - cx, - )); - } - items - } - }) - .p_4() - .on_scroll_wheel(cx.listener(|this, _, _, cx| { - this.autoscroll = false; - cx.notify(); - })) - .track_scroll(&self.scroll_handle) - .size_full(), - ) - .vertical_scrollbar_for(&self.scroll_handle, window, cx), - ) - }) - } -} diff --git a/crates/mistral/Cargo.toml b/crates/mistral/Cargo.toml deleted file mode 100644 index c4d475f014..0000000000 --- a/crates/mistral/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "mistral" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/mistral.rs" - -[features] -default = [] -schemars = ["dep:schemars"] - -[dependencies] -anyhow.workspace = true -futures.workspace = true -http_client.workspace = true -schemars = { workspace = true, optional = true } -serde.workspace = true -serde_json.workspace = true -strum.workspace = true diff --git a/crates/mistral/LICENSE-GPL b/crates/mistral/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/mistral/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/mistral/src/mistral.rs b/crates/mistral/src/mistral.rs deleted file mode 100644 index eca4743d04..0000000000 --- a/crates/mistral/src/mistral.rs +++ /dev/null @@ -1,482 +0,0 @@ -use anyhow::{Result, anyhow}; -use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream}; -use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest}; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use std::convert::TryFrom; -use strum::EnumIter; - -pub const MISTRAL_API_URL: &str = "https://api.mistral.ai/v1"; -pub const CODESTRAL_API_URL: &str = "https://codestral.mistral.ai"; - -#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum Role { - User, - Assistant, - System, - Tool, -} - -impl TryFrom for Role { - type Error = anyhow::Error; - - fn try_from(value: String) -> Result { - match value.as_str() { - "user" => Ok(Self::User), - "assistant" => Ok(Self::Assistant), - "system" => Ok(Self::System), - "tool" => Ok(Self::Tool), - _ => anyhow::bail!("invalid role '{value}'"), - } - } -} - -impl From for String { - fn from(val: Role) -> Self { - match val { - Role::User => "user".to_owned(), - Role::Assistant => "assistant".to_owned(), - Role::System => "system".to_owned(), - Role::Tool => "tool".to_owned(), - } - } -} - -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)] -pub enum Model { - #[serde(rename = "codestral-latest", alias = "codestral-latest")] - #[default] - CodestralLatest, - - #[serde(rename = "mistral-large-latest", alias = "mistral-large-latest")] - MistralLargeLatest, - #[serde(rename = "mistral-medium-latest", alias = "mistral-medium-latest")] - MistralMediumLatest, - #[serde(rename = "mistral-small-latest", alias = "mistral-small-latest")] - MistralSmallLatest, - - #[serde(rename = "magistral-medium-latest", alias = "magistral-medium-latest")] - MagistralMediumLatest, - #[serde(rename = "magistral-small-latest", alias = "magistral-small-latest")] - MagistralSmallLatest, - - #[serde(rename = "open-mistral-nemo", alias = "open-mistral-nemo")] - OpenMistralNemo, - #[serde(rename = "open-codestral-mamba", alias = "open-codestral-mamba")] - OpenCodestralMamba, - - #[serde(rename = "devstral-medium-latest", alias = "devstral-medium-latest")] - DevstralMediumLatest, - #[serde(rename = "devstral-small-latest", alias = "devstral-small-latest")] - DevstralSmallLatest, - - #[serde(rename = "pixtral-12b-latest", alias = "pixtral-12b-latest")] - Pixtral12BLatest, - #[serde(rename = "pixtral-large-latest", alias = "pixtral-large-latest")] - PixtralLargeLatest, - - #[serde(rename = "custom")] - Custom { - name: String, - /// The name displayed in the UI, such as in the assistant panel model dropdown menu. - display_name: Option, - max_tokens: u64, - max_output_tokens: Option, - max_completion_tokens: Option, - supports_tools: Option, - supports_images: Option, - supports_thinking: Option, - }, -} - -impl Model { - pub fn default_fast() -> Self { - Model::MistralSmallLatest - } - - pub fn from_id(id: &str) -> Result { - match id { - "codestral-latest" => Ok(Self::CodestralLatest), - "mistral-large-latest" => Ok(Self::MistralLargeLatest), - "mistral-medium-latest" => Ok(Self::MistralMediumLatest), - "mistral-small-latest" => Ok(Self::MistralSmallLatest), - "magistral-medium-latest" => Ok(Self::MagistralMediumLatest), - "magistral-small-latest" => Ok(Self::MagistralSmallLatest), - "open-mistral-nemo" => Ok(Self::OpenMistralNemo), - "open-codestral-mamba" => Ok(Self::OpenCodestralMamba), - "devstral-medium-latest" => Ok(Self::DevstralMediumLatest), - "devstral-small-latest" => Ok(Self::DevstralSmallLatest), - "pixtral-12b-latest" => Ok(Self::Pixtral12BLatest), - "pixtral-large-latest" => Ok(Self::PixtralLargeLatest), - invalid_id => anyhow::bail!("invalid model id '{invalid_id}'"), - } - } - - pub fn id(&self) -> &str { - match self { - Self::CodestralLatest => "codestral-latest", - Self::MistralLargeLatest => "mistral-large-latest", - Self::MistralMediumLatest => "mistral-medium-latest", - Self::MistralSmallLatest => "mistral-small-latest", - Self::MagistralMediumLatest => "magistral-medium-latest", - Self::MagistralSmallLatest => "magistral-small-latest", - Self::OpenMistralNemo => "open-mistral-nemo", - Self::OpenCodestralMamba => "open-codestral-mamba", - Self::DevstralMediumLatest => "devstral-medium-latest", - Self::DevstralSmallLatest => "devstral-small-latest", - Self::Pixtral12BLatest => "pixtral-12b-latest", - Self::PixtralLargeLatest => "pixtral-large-latest", - Self::Custom { name, .. } => name, - } - } - - pub fn display_name(&self) -> &str { - match self { - Self::CodestralLatest => "codestral-latest", - Self::MistralLargeLatest => "mistral-large-latest", - Self::MistralMediumLatest => "mistral-medium-latest", - Self::MistralSmallLatest => "mistral-small-latest", - Self::MagistralMediumLatest => "magistral-medium-latest", - Self::MagistralSmallLatest => "magistral-small-latest", - Self::OpenMistralNemo => "open-mistral-nemo", - Self::OpenCodestralMamba => "open-codestral-mamba", - Self::DevstralMediumLatest => "devstral-medium-latest", - Self::DevstralSmallLatest => "devstral-small-latest", - Self::Pixtral12BLatest => "pixtral-12b-latest", - Self::PixtralLargeLatest => "pixtral-large-latest", - Self::Custom { - name, display_name, .. - } => display_name.as_ref().unwrap_or(name), - } - } - - pub fn max_token_count(&self) -> u64 { - match self { - Self::CodestralLatest => 256000, - Self::MistralLargeLatest => 131000, - Self::MistralMediumLatest => 128000, - Self::MistralSmallLatest => 32000, - Self::MagistralMediumLatest => 40000, - Self::MagistralSmallLatest => 40000, - Self::OpenMistralNemo => 131000, - Self::OpenCodestralMamba => 256000, - Self::DevstralMediumLatest => 128000, - Self::DevstralSmallLatest => 262144, - Self::Pixtral12BLatest => 128000, - Self::PixtralLargeLatest => 128000, - Self::Custom { max_tokens, .. } => *max_tokens, - } - } - - pub fn max_output_tokens(&self) -> Option { - match self { - Self::Custom { - max_output_tokens, .. - } => *max_output_tokens, - _ => None, - } - } - - pub fn supports_tools(&self) -> bool { - match self { - Self::CodestralLatest - | Self::MistralLargeLatest - | Self::MistralMediumLatest - | Self::MistralSmallLatest - | Self::MagistralMediumLatest - | Self::MagistralSmallLatest - | Self::OpenMistralNemo - | Self::OpenCodestralMamba - | Self::DevstralMediumLatest - | Self::DevstralSmallLatest - | Self::Pixtral12BLatest - | Self::PixtralLargeLatest => true, - Self::Custom { supports_tools, .. } => supports_tools.unwrap_or(false), - } - } - - pub fn supports_images(&self) -> bool { - match self { - Self::Pixtral12BLatest - | Self::PixtralLargeLatest - | Self::MistralMediumLatest - | Self::MistralSmallLatest => true, - Self::CodestralLatest - | Self::MistralLargeLatest - | Self::MagistralMediumLatest - | Self::MagistralSmallLatest - | Self::OpenMistralNemo - | Self::OpenCodestralMamba - | Self::DevstralMediumLatest - | Self::DevstralSmallLatest => false, - Self::Custom { - supports_images, .. - } => supports_images.unwrap_or(false), - } - } - - pub fn supports_thinking(&self) -> bool { - match self { - Self::MagistralMediumLatest | Self::MagistralSmallLatest => true, - Self::Custom { - supports_thinking, .. - } => supports_thinking.unwrap_or(false), - _ => false, - } - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct Request { - pub model: String, - pub messages: Vec, - pub stream: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub max_tokens: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub temperature: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub response_format: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub tool_choice: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub parallel_tool_calls: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub tools: Vec, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum ResponseFormat { - Text, - #[serde(rename = "json_object")] - JsonObject, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum ToolDefinition { - Function { function: FunctionDefinition }, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct FunctionDefinition { - pub name: String, - pub description: Option, - pub parameters: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum ToolChoice { - Auto, - Required, - None, - Any, - #[serde(untagged)] - Function(ToolDefinition), -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -#[serde(tag = "role", rename_all = "lowercase")] -pub enum RequestMessage { - Assistant { - #[serde(flatten)] - #[serde(default, skip_serializing_if = "Option::is_none")] - content: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - tool_calls: Vec, - }, - User { - #[serde(flatten)] - content: MessageContent, - }, - System { - #[serde(flatten)] - content: MessageContent, - }, - Tool { - content: String, - tool_call_id: String, - }, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] -#[serde(untagged)] -pub enum MessageContent { - #[serde(rename = "content")] - Plain { content: String }, - #[serde(rename = "content")] - Multipart { content: Vec }, -} - -impl MessageContent { - pub fn empty() -> Self { - Self::Plain { - content: String::new(), - } - } - - pub fn push_part(&mut self, part: MessagePart) { - match self { - Self::Plain { content } => match part { - MessagePart::Text { text } => { - content.push_str(&text); - } - part => { - let mut parts = if content.is_empty() { - Vec::new() - } else { - vec![MessagePart::Text { - text: content.clone(), - }] - }; - parts.push(part); - *self = Self::Multipart { content: parts }; - } - }, - Self::Multipart { content } => { - content.push(part); - } - } - } -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum MessagePart { - Text { text: String }, - ImageUrl { image_url: String }, - Thinking { thinking: Vec }, -} - -// Backwards-compatibility alias for provider code that refers to ContentPart -pub type ContentPart = MessagePart; - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum ThinkingPart { - Text { text: String }, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct ToolCall { - pub id: String, - #[serde(flatten)] - pub content: ToolCallContent, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -#[serde(tag = "type", rename_all = "lowercase")] -pub enum ToolCallContent { - Function { function: FunctionContent }, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct FunctionContent { - pub name: String, - pub arguments: String, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct Usage { - pub prompt_tokens: u64, - pub completion_tokens: u64, - pub total_tokens: u64, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct StreamResponse { - pub id: String, - pub object: String, - pub created: u64, - pub model: String, - pub choices: Vec, - pub usage: Option, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct StreamChoice { - pub index: u32, - pub delta: StreamDelta, - pub finish_reason: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct StreamDelta { - pub role: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub content: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub tool_calls: Option>, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] -#[serde(untagged)] -pub enum MessageContentDelta { - Text(String), - Parts(Vec), -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] -pub struct ToolCallChunk { - pub index: usize, - pub id: Option, - pub function: Option, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq, Clone)] -pub struct FunctionChunk { - pub name: Option, - pub arguments: Option, -} - -pub async fn stream_completion( - client: &dyn HttpClient, - api_url: &str, - api_key: &str, - request: Request, -) -> Result>> { - let uri = format!("{api_url}/chat/completions"); - let request_builder = HttpRequest::builder() - .method(Method::POST) - .uri(uri) - .header("Content-Type", "application/json") - .header("Authorization", format!("Bearer {}", api_key.trim())); - - let request = request_builder.body(AsyncBody::from(serde_json::to_string(&request)?))?; - let mut response = client.send(request).await?; - - if response.status().is_success() { - let reader = BufReader::new(response.into_body()); - Ok(reader - .lines() - .filter_map(|line| async move { - match line { - Ok(line) => { - let line = line.strip_prefix("data: ")?; - if line == "[DONE]" { - None - } else { - match serde_json::from_str(line) { - Ok(response) => Some(Ok(response)), - Err(error) => Some(Err(anyhow!(error))), - } - } - } - Err(error) => Some(Err(anyhow!(error))), - } - }) - .boxed()) - } else { - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - anyhow::bail!( - "Failed to connect to Mistral API: {} {}", - response.status(), - body, - ); - } -} diff --git a/crates/multi_buffer/Cargo.toml b/crates/multi_buffer/Cargo.toml deleted file mode 100644 index 524c916682..0000000000 --- a/crates/multi_buffer/Cargo.toml +++ /dev/null @@ -1,63 +0,0 @@ -[package] -name = "multi_buffer" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/multi_buffer.rs" -doctest = false - -[features] -test-support = [ - "buffer_diff/test-support", - "gpui/test-support", - "language/test-support", - "text/test-support", - "util/test-support", -] - -[dependencies] -anyhow.workspace = true -clock.workspace = true -collections.workspace = true -ctor.workspace = true -buffer_diff.workspace = true -gpui.workspace = true -itertools.workspace = true -language.workspace = true -log.workspace = true -parking_lot.workspace = true -rand.workspace = true -rope.workspace = true -smol.workspace = true -settings.workspace = true -serde.workspace = true -smallvec.workspace = true -sum_tree.workspace = true -text.workspace = true -theme.workspace = true -tree-sitter.workspace = true -ztracing.workspace = true -tracing.workspace = true -util.workspace = true - -[dev-dependencies] -buffer_diff = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -indoc.workspace = true -language = { workspace = true, features = ["test-support"] } -pretty_assertions.workspace = true -project = { workspace = true, features = ["test-support"] } -rand.workspace = true -settings = { workspace = true, features = ["test-support"] } -text = { workspace = true, features = ["test-support"] } -util = { workspace = true, features = ["test-support"] } -zlog.workspace = true - -[package.metadata.cargo-machete] -ignored = ["tracing"] diff --git a/crates/multi_buffer/LICENSE-GPL b/crates/multi_buffer/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/multi_buffer/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/multi_buffer/src/anchor.rs b/crates/multi_buffer/src/anchor.rs deleted file mode 100644 index 51696ba09e..0000000000 --- a/crates/multi_buffer/src/anchor.rs +++ /dev/null @@ -1,257 +0,0 @@ -use crate::{MultiBufferDimension, MultiBufferOffset, MultiBufferOffsetUtf16}; - -use super::{ExcerptId, MultiBufferSnapshot, ToOffset, ToPoint}; -use language::Point; -use std::{ - cmp::Ordering, - ops::{AddAssign, Range, Sub}, -}; -use sum_tree::Bias; - -#[derive(Clone, Copy, Eq, PartialEq, Hash)] -pub struct Anchor { - pub excerpt_id: ExcerptId, - pub text_anchor: text::Anchor, - pub diff_base_anchor: Option, -} - -impl std::fmt::Debug for Anchor { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if self.is_min() { - return write!(f, "Anchor::min({:?})", self.text_anchor.buffer_id); - } - if self.is_max() { - return write!(f, "Anchor::max({:?})", self.text_anchor.buffer_id); - } - - f.debug_struct("Anchor") - .field("excerpt_id", &self.excerpt_id) - .field("text_anchor", &self.text_anchor) - .field("diff_base_anchor", &self.diff_base_anchor) - .finish() - } -} - -impl Anchor { - pub fn with_diff_base_anchor(self, diff_base_anchor: text::Anchor) -> Self { - Self { - diff_base_anchor: Some(diff_base_anchor), - ..self - } - } - - pub fn in_buffer(excerpt_id: ExcerptId, text_anchor: text::Anchor) -> Self { - Self { - excerpt_id, - text_anchor, - diff_base_anchor: None, - } - } - - pub fn range_in_buffer(excerpt_id: ExcerptId, range: Range) -> Range { - Self::in_buffer(excerpt_id, range.start)..Self::in_buffer(excerpt_id, range.end) - } - - pub fn min() -> Self { - Self { - excerpt_id: ExcerptId::min(), - text_anchor: text::Anchor::MIN, - diff_base_anchor: None, - } - } - - pub fn max() -> Self { - Self { - excerpt_id: ExcerptId::max(), - text_anchor: text::Anchor::MAX, - diff_base_anchor: None, - } - } - - pub fn is_min(&self) -> bool { - self.excerpt_id == ExcerptId::min() - && self.text_anchor.is_min() - && self.diff_base_anchor.is_none() - } - - pub fn is_max(&self) -> bool { - self.excerpt_id == ExcerptId::max() - && self.text_anchor.is_max() - && self.diff_base_anchor.is_none() - } - - pub fn cmp(&self, other: &Anchor, snapshot: &MultiBufferSnapshot) -> Ordering { - if self == other { - return Ordering::Equal; - } - - let self_excerpt_id = snapshot.latest_excerpt_id(self.excerpt_id); - let other_excerpt_id = snapshot.latest_excerpt_id(other.excerpt_id); - - let excerpt_id_cmp = self_excerpt_id.cmp(&other_excerpt_id, snapshot); - if excerpt_id_cmp.is_ne() { - return excerpt_id_cmp; - } - if self_excerpt_id == ExcerptId::max() - && self.text_anchor.is_max() - && self.text_anchor.is_max() - && self.diff_base_anchor.is_none() - && other.diff_base_anchor.is_none() - { - return Ordering::Equal; - } - if let Some(excerpt) = snapshot.excerpt(self_excerpt_id) { - let text_cmp = self.text_anchor.cmp(&other.text_anchor, &excerpt.buffer); - if text_cmp.is_ne() { - return text_cmp; - } - if (self.diff_base_anchor.is_some() || other.diff_base_anchor.is_some()) - && let Some(base_text) = snapshot - .diffs - .get(&excerpt.buffer_id) - .map(|diff| diff.base_text()) - { - let self_anchor = self.diff_base_anchor.filter(|a| base_text.can_resolve(a)); - let other_anchor = other.diff_base_anchor.filter(|a| base_text.can_resolve(a)); - return match (self_anchor, other_anchor) { - (Some(a), Some(b)) => a.cmp(&b, base_text), - (Some(_), None) => match other.text_anchor.bias { - Bias::Left => Ordering::Greater, - Bias::Right => Ordering::Less, - }, - (None, Some(_)) => match self.text_anchor.bias { - Bias::Left => Ordering::Less, - Bias::Right => Ordering::Greater, - }, - (None, None) => Ordering::Equal, - }; - } - } - Ordering::Equal - } - - pub fn bias(&self) -> Bias { - self.text_anchor.bias - } - - pub fn bias_left(&self, snapshot: &MultiBufferSnapshot) -> Anchor { - if self.text_anchor.bias != Bias::Left - && let Some(excerpt) = snapshot.excerpt(self.excerpt_id) - { - return Self { - excerpt_id: excerpt.id, - text_anchor: self.text_anchor.bias_left(&excerpt.buffer), - diff_base_anchor: self.diff_base_anchor.map(|a| { - if let Some(base_text) = snapshot - .diffs - .get(&excerpt.buffer_id) - .map(|diff| diff.base_text()) - && a.buffer_id == Some(base_text.remote_id()) - { - return a.bias_left(base_text); - } - a - }), - }; - } - *self - } - - pub fn bias_right(&self, snapshot: &MultiBufferSnapshot) -> Anchor { - if self.text_anchor.bias != Bias::Right - && let Some(excerpt) = snapshot.excerpt(self.excerpt_id) - { - return Self { - excerpt_id: excerpt.id, - text_anchor: self.text_anchor.bias_right(&excerpt.buffer), - diff_base_anchor: self.diff_base_anchor.map(|a| { - if let Some(base_text) = snapshot - .diffs - .get(&excerpt.buffer_id) - .map(|diff| diff.base_text()) - && a.buffer_id == Some(base_text.remote_id()) - { - return a.bias_right(base_text); - } - a - }), - }; - } - *self - } - - pub fn summary(&self, snapshot: &MultiBufferSnapshot) -> D - where - D: MultiBufferDimension - + Ord - + Sub - + AddAssign, - D::TextDimension: Sub + Ord, - { - snapshot.summary_for_anchor(self) - } - - pub fn is_valid(&self, snapshot: &MultiBufferSnapshot) -> bool { - if self.is_min() || self.is_max() { - true - } else if let Some(excerpt) = snapshot.excerpt(self.excerpt_id) { - (self.text_anchor == excerpt.range.context.start - || self.text_anchor == excerpt.range.context.end - || self.text_anchor.is_valid(&excerpt.buffer)) - && excerpt.contains(self) - } else { - false - } - } -} - -impl ToOffset for Anchor { - fn to_offset(&self, snapshot: &MultiBufferSnapshot) -> MultiBufferOffset { - self.summary(snapshot) - } - fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> MultiBufferOffsetUtf16 { - self.summary(snapshot) - } -} - -impl ToPoint for Anchor { - fn to_point<'a>(&self, snapshot: &MultiBufferSnapshot) -> Point { - self.summary(snapshot) - } - fn to_point_utf16(&self, snapshot: &MultiBufferSnapshot) -> rope::PointUtf16 { - self.summary(snapshot) - } -} - -pub trait AnchorRangeExt { - fn cmp(&self, other: &Range, buffer: &MultiBufferSnapshot) -> Ordering; - fn includes(&self, other: &Range, buffer: &MultiBufferSnapshot) -> bool; - fn overlaps(&self, other: &Range, buffer: &MultiBufferSnapshot) -> bool; - fn to_offset(&self, content: &MultiBufferSnapshot) -> Range; - fn to_point(&self, content: &MultiBufferSnapshot) -> Range; -} - -impl AnchorRangeExt for Range { - fn cmp(&self, other: &Range, buffer: &MultiBufferSnapshot) -> Ordering { - match self.start.cmp(&other.start, buffer) { - Ordering::Equal => other.end.cmp(&self.end, buffer), - ord => ord, - } - } - - fn includes(&self, other: &Range, buffer: &MultiBufferSnapshot) -> bool { - self.start.cmp(&other.start, buffer).is_le() && other.end.cmp(&self.end, buffer).is_le() - } - - fn overlaps(&self, other: &Range, buffer: &MultiBufferSnapshot) -> bool { - self.end.cmp(&other.start, buffer).is_ge() && self.start.cmp(&other.end, buffer).is_le() - } - - fn to_offset(&self, content: &MultiBufferSnapshot) -> Range { - self.start.to_offset(content)..self.end.to_offset(content) - } - - fn to_point(&self, content: &MultiBufferSnapshot) -> Range { - self.start.to_point(content)..self.end.to_point(content) - } -} diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs deleted file mode 100644 index 5b343ecc57..0000000000 --- a/crates/multi_buffer/src/multi_buffer.rs +++ /dev/null @@ -1,8227 +0,0 @@ -mod anchor; -#[cfg(test)] -mod multi_buffer_tests; -mod path_key; -mod transaction; - -use self::transaction::History; - -pub use anchor::{Anchor, AnchorRangeExt}; - -use anyhow::{Result, anyhow}; -use buffer_diff::{ - BufferDiff, BufferDiffEvent, BufferDiffSnapshot, DiffHunkSecondaryStatus, DiffHunkStatus, - DiffHunkStatusKind, -}; -use clock::ReplicaId; -use collections::{BTreeMap, Bound, HashMap, HashSet}; -use gpui::{App, Context, Entity, EntityId, EventEmitter}; -use itertools::Itertools; -use language::{ - AutoindentMode, BracketMatch, Buffer, BufferChunks, BufferRow, BufferSnapshot, Capability, - CharClassifier, CharKind, CharScopeContext, Chunk, CursorShape, DiagnosticEntryRef, DiskState, - File, IndentGuideSettings, IndentSize, Language, LanguageScope, OffsetRangeExt, OffsetUtf16, - Outline, OutlineItem, Point, PointUtf16, Selection, TextDimension, TextObject, ToOffset as _, - ToPoint as _, TransactionId, TreeSitterOptions, Unclipped, - language_settings::{LanguageSettings, language_settings}, -}; - -#[cfg(any(test, feature = "test-support"))] -use gpui::AppContext as _; - -use rope::DimensionPair; -use smallvec::SmallVec; -use smol::future::yield_now; -use std::{ - any::type_name, - borrow::Cow, - cell::{Cell, Ref, RefCell}, - cmp, - collections::VecDeque, - fmt::{self, Debug}, - future::Future, - io, - iter::{self, FromIterator}, - mem, - ops::{self, AddAssign, ControlFlow, Range, RangeBounds, Sub, SubAssign}, - rc::Rc, - str, - sync::Arc, - time::Duration, -}; -use sum_tree::{Bias, Cursor, Dimension, Dimensions, SumTree, TreeMap}; -use text::{ - BufferId, Edit, LineIndent, TextSummary, - locator::Locator, - subscription::{Subscription, Topic}, -}; -use theme::SyntaxTheme; -use util::post_inc; -use ztracing::instrument; - -pub use self::path_key::PathKey; - -#[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] -pub struct ExcerptId(u32); - -#[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] -pub struct BaseTextRow(pub u32); - -/// One or more [`Buffers`](Buffer) being edited in a single view. -/// -/// See -pub struct MultiBuffer { - /// A snapshot of the [`Excerpt`]s in the MultiBuffer. - /// Use [`MultiBuffer::snapshot`] to get a up-to-date snapshot. - snapshot: RefCell, - /// Contains the state of the buffers being edited - buffers: HashMap, - /// Mapping from path keys to their excerpts. - excerpts_by_path: BTreeMap>, - /// Mapping from excerpt IDs to their path key. - paths_by_excerpt: HashMap, - /// Mapping from buffer IDs to their diff states - diffs: HashMap, - subscriptions: Topic, - /// If true, the multi-buffer only contains a single [`Buffer`] and a single [`Excerpt`] - singleton: bool, - /// The history of the multi-buffer. - history: History, - /// The explicit title of the multi-buffer. - /// If `None`, it will be derived from the underlying path or content. - title: Option, - /// The writing capability of the multi-buffer. - capability: Capability, - buffer_changed_since_sync: Rc>, - follower: Option>, - filter_mode: Option, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum MultiBufferFilterMode { - KeepInsertions, - KeepDeletions, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Event { - ExcerptsAdded { - buffer: Entity, - predecessor: ExcerptId, - excerpts: Vec<(ExcerptId, ExcerptRange)>, - }, - ExcerptsRemoved { - ids: Vec, - removed_buffer_ids: Vec, - }, - ExcerptsExpanded { - ids: Vec, - }, - ExcerptsEdited { - excerpt_ids: Vec, - buffer_ids: Vec, - }, - DiffHunksToggled, - Edited { - edited_buffer: Option>, - }, - TransactionUndone { - transaction_id: TransactionId, - }, - Reloaded, - LanguageChanged(BufferId, bool), - Reparsed(BufferId), - Saved, - FileHandleChanged, - DirtyChanged, - DiagnosticsUpdated, - BufferDiffChanged, -} - -/// A diff hunk, representing a range of consequent lines in a multibuffer. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MultiBufferDiffHunk { - /// The row range in the multibuffer where this diff hunk appears. - pub row_range: Range, - /// The buffer ID that this hunk belongs to. - pub buffer_id: BufferId, - /// The range of the underlying buffer that this hunk corresponds to. - pub buffer_range: Range, - /// The excerpt that contains the diff hunk. - pub excerpt_id: ExcerptId, - /// The range within the buffer's diff base that this hunk corresponds to. - pub diff_base_byte_range: Range, - /// Whether or not this hunk also appears in the 'secondary diff'. - pub secondary_status: DiffHunkSecondaryStatus, - /// The word diffs for this hunk. - pub word_diffs: Vec>, -} - -impl MultiBufferDiffHunk { - pub fn status(&self) -> DiffHunkStatus { - let kind = if self.buffer_range.start == self.buffer_range.end { - DiffHunkStatusKind::Deleted - } else if self.diff_base_byte_range.is_empty() { - DiffHunkStatusKind::Added - } else { - DiffHunkStatusKind::Modified - }; - DiffHunkStatus { - kind, - secondary: self.secondary_status, - } - } - - pub fn is_created_file(&self) -> bool { - self.diff_base_byte_range == (BufferOffset(0)..BufferOffset(0)) - && self.buffer_range.start.is_min() - && self.buffer_range.end.is_max() - } - - pub fn multi_buffer_range(&self) -> Range { - let start = Anchor::in_buffer(self.excerpt_id, self.buffer_range.start); - let end = Anchor::in_buffer(self.excerpt_id, self.buffer_range.end); - start..end - } -} - -pub type MultiBufferPoint = Point; -type ExcerptOffset = ExcerptDimension; -type ExcerptPoint = ExcerptDimension; - -#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq, Hash, serde::Deserialize)] -#[serde(transparent)] -pub struct MultiBufferRow(pub u32); - -impl MultiBufferRow { - pub const MIN: Self = Self(0); - pub const MAX: Self = Self(u32::MAX); -} - -impl ops::Add for MultiBufferRow { - type Output = Self; - - fn add(self, rhs: usize) -> Self::Output { - MultiBufferRow(self.0 + rhs as u32) - } -} - -pub trait MultiBufferDimension: 'static + Copy + Default + std::fmt::Debug { - type TextDimension: TextDimension; - fn from_summary(summary: &MBTextSummary) -> Self; - - fn add_text_dim(&mut self, summary: &Self::TextDimension); - - fn add_mb_text_summary(&mut self, summary: &MBTextSummary); -} - -// todo(lw): MultiBufferPoint -impl MultiBufferDimension for Point { - type TextDimension = Point; - fn from_summary(summary: &MBTextSummary) -> Self { - summary.lines - } - - fn add_text_dim(&mut self, other: &Self::TextDimension) { - *self += *other; - } - - fn add_mb_text_summary(&mut self, summary: &MBTextSummary) { - *self += summary.lines; - } -} - -// todo(lw): MultiBufferPointUtf16 -impl MultiBufferDimension for PointUtf16 { - type TextDimension = PointUtf16; - fn from_summary(summary: &MBTextSummary) -> Self { - summary.lines_utf16() - } - - fn add_text_dim(&mut self, other: &Self::TextDimension) { - *self += *other; - } - - fn add_mb_text_summary(&mut self, summary: &MBTextSummary) { - *self += summary.lines_utf16(); - } -} - -#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq, Hash, serde::Deserialize)] -pub struct MultiBufferOffset(pub usize); - -impl fmt::Display for MultiBufferOffset { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.0) - } -} - -impl rand::distr::uniform::SampleUniform for MultiBufferOffset { - type Sampler = MultiBufferOffsetUniformSampler; -} - -pub struct MultiBufferOffsetUniformSampler { - sampler: rand::distr::uniform::UniformUsize, -} - -impl rand::distr::uniform::UniformSampler for MultiBufferOffsetUniformSampler { - type X = MultiBufferOffset; - - fn new(low_b: B1, high_b: B2) -> Result - where - B1: rand::distr::uniform::SampleBorrow + Sized, - B2: rand::distr::uniform::SampleBorrow + Sized, - { - let low = *low_b.borrow(); - let high = *high_b.borrow(); - let sampler = rand::distr::uniform::UniformUsize::new(low.0, high.0); - sampler.map(|sampler| MultiBufferOffsetUniformSampler { sampler }) - } - - #[inline] // if the range is constant, this helps LLVM to do the - // calculations at compile-time. - fn new_inclusive(low_b: B1, high_b: B2) -> Result - where - B1: rand::distr::uniform::SampleBorrow + Sized, - B2: rand::distr::uniform::SampleBorrow + Sized, - { - let low = *low_b.borrow(); - let high = *high_b.borrow(); - let sampler = rand::distr::uniform::UniformUsize::new_inclusive(low.0, high.0); - sampler.map(|sampler| MultiBufferOffsetUniformSampler { sampler }) - } - - fn sample(&self, rng: &mut R) -> Self::X { - MultiBufferOffset(self.sampler.sample(rng)) - } -} -impl MultiBufferDimension for MultiBufferOffset { - type TextDimension = usize; - fn from_summary(summary: &MBTextSummary) -> Self { - summary.len - } - - fn add_text_dim(&mut self, other: &Self::TextDimension) { - self.0 += *other; - } - - fn add_mb_text_summary(&mut self, summary: &MBTextSummary) { - *self += summary.len; - } -} -impl MultiBufferDimension for MultiBufferOffsetUtf16 { - type TextDimension = OffsetUtf16; - fn from_summary(summary: &MBTextSummary) -> Self { - MultiBufferOffsetUtf16(summary.len_utf16) - } - - fn add_text_dim(&mut self, other: &Self::TextDimension) { - self.0 += *other; - } - - fn add_mb_text_summary(&mut self, summary: &MBTextSummary) { - self.0 += summary.len_utf16; - } -} - -#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq, Hash, serde::Deserialize)] -pub struct BufferOffset(pub usize); - -impl TextDimension for BufferOffset { - fn from_text_summary(summary: &TextSummary) -> Self { - BufferOffset(usize::from_text_summary(summary)) - } - fn from_chunk(chunk: rope::ChunkSlice) -> Self { - BufferOffset(usize::from_chunk(chunk)) - } - fn add_assign(&mut self, other: &Self) { - TextDimension::add_assign(&mut self.0, &other.0); - } -} -impl<'a> sum_tree::Dimension<'a, rope::ChunkSummary> for BufferOffset { - fn zero(cx: ()) -> Self { - BufferOffset(>::zero(cx)) - } - - fn add_summary(&mut self, summary: &'a rope::ChunkSummary, cx: ()) { - usize::add_summary(&mut self.0, summary, cx); - } -} - -impl Sub for BufferOffset { - type Output = usize; - - fn sub(self, other: BufferOffset) -> Self::Output { - self.0 - other.0 - } -} - -impl AddAssign> for BufferOffset { - fn add_assign(&mut self, other: DimensionPair) { - self.0 += other.key; - } -} - -impl language::ToPoint for BufferOffset { - fn to_point(&self, snapshot: &text::BufferSnapshot) -> Point { - self.0.to_point(snapshot) - } -} - -impl language::ToPointUtf16 for BufferOffset { - fn to_point_utf16(&self, snapshot: &text::BufferSnapshot) -> PointUtf16 { - self.0.to_point_utf16(snapshot) - } -} - -impl language::ToOffset for BufferOffset { - fn to_offset(&self, snapshot: &text::BufferSnapshot) -> usize { - self.0.to_offset(snapshot) - } -} - -impl language::ToOffsetUtf16 for BufferOffset { - fn to_offset_utf16(&self, snapshot: &text::BufferSnapshot) -> OffsetUtf16 { - self.0.to_offset_utf16(snapshot) - } -} - -#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] -pub struct MultiBufferOffsetUtf16(pub OffsetUtf16); - -impl ops::Add for MultiBufferOffsetUtf16 { - type Output = MultiBufferOffsetUtf16; - - fn add(self, rhs: usize) -> Self::Output { - MultiBufferOffsetUtf16(OffsetUtf16(self.0.0 + rhs)) - } -} - -impl AddAssign for MultiBufferOffsetUtf16 { - fn add_assign(&mut self, rhs: OffsetUtf16) { - self.0 += rhs; - } -} - -impl AddAssign for MultiBufferOffsetUtf16 { - fn add_assign(&mut self, rhs: usize) { - self.0.0 += rhs; - } -} - -impl Sub for MultiBufferOffsetUtf16 { - type Output = OffsetUtf16; - - fn sub(self, other: MultiBufferOffsetUtf16) -> Self::Output { - self.0 - other.0 - } -} - -#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] -pub struct BufferOffsetUtf16(pub OffsetUtf16); - -impl MultiBufferOffset { - const ZERO: Self = Self(0); - pub fn saturating_sub(self, other: MultiBufferOffset) -> usize { - self.0.saturating_sub(other.0) - } - pub fn saturating_sub_usize(self, other: usize) -> MultiBufferOffset { - MultiBufferOffset(self.0.saturating_sub(other)) - } -} - -impl ops::Sub for MultiBufferOffset { - type Output = usize; - - fn sub(self, other: MultiBufferOffset) -> Self::Output { - self.0 - other.0 - } -} - -impl ops::Sub for MultiBufferOffset { - type Output = Self; - - fn sub(self, other: usize) -> Self::Output { - MultiBufferOffset(self.0 - other) - } -} - -impl ops::SubAssign for MultiBufferOffset { - fn sub_assign(&mut self, other: usize) { - self.0 -= other; - } -} - -impl ops::Add for BufferOffset { - type Output = Self; - - fn add(self, rhs: usize) -> Self::Output { - BufferOffset(self.0 + rhs) - } -} - -impl ops::AddAssign for BufferOffset { - fn add_assign(&mut self, other: usize) { - self.0 += other; - } -} - -impl ops::Add for MultiBufferOffset { - type Output = Self; - - fn add(self, rhs: usize) -> Self::Output { - MultiBufferOffset(self.0 + rhs) - } -} - -impl ops::AddAssign for MultiBufferOffset { - fn add_assign(&mut self, other: usize) { - self.0 += other; - } -} - -impl ops::Add for MultiBufferOffset { - type Output = Self; - - fn add(self, rhs: isize) -> Self::Output { - MultiBufferOffset((self.0 as isize + rhs) as usize) - } -} - -impl ops::Add for MultiBufferOffset { - type Output = Self; - - fn add(self, rhs: MultiBufferOffset) -> Self::Output { - MultiBufferOffset(self.0 + rhs.0) - } -} - -impl ops::AddAssign for MultiBufferOffset { - fn add_assign(&mut self, other: MultiBufferOffset) { - self.0 += other.0; - } -} - -pub trait ToOffset: 'static + fmt::Debug { - fn to_offset(&self, snapshot: &MultiBufferSnapshot) -> MultiBufferOffset; - fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> MultiBufferOffsetUtf16; -} - -pub trait ToPoint: 'static + fmt::Debug { - fn to_point(&self, snapshot: &MultiBufferSnapshot) -> Point; - fn to_point_utf16(&self, snapshot: &MultiBufferSnapshot) -> PointUtf16; -} - -struct BufferState { - buffer: Entity, - last_version: RefCell, - last_non_text_state_update_count: Cell, - excerpts: Vec, - _subscriptions: [gpui::Subscription; 2], -} - -struct DiffState { - diff: Entity, - _subscription: gpui::Subscription, -} - -impl DiffState { - fn new(diff: Entity, cx: &mut Context) -> Self { - DiffState { - _subscription: cx.subscribe(&diff, |this, diff, event, cx| match event { - BufferDiffEvent::DiffChanged { changed_range } => { - if let Some(changed_range) = changed_range.clone() { - this.buffer_diff_changed(diff, changed_range, cx) - } - cx.emit(Event::BufferDiffChanged); - } - BufferDiffEvent::LanguageChanged => this.buffer_diff_language_changed(diff, cx), - _ => {} - }), - diff, - } - } -} - -/// The contents of a [`MultiBuffer`] at a single point in time. -#[derive(Clone, Default)] -pub struct MultiBufferSnapshot { - excerpts: SumTree, - diffs: TreeMap, - diff_transforms: SumTree, - non_text_state_update_count: usize, - edit_count: usize, - is_dirty: bool, - has_deleted_file: bool, - has_conflict: bool, - /// immutable fields - singleton: bool, - excerpt_ids: SumTree, - replaced_excerpts: TreeMap, - trailing_excerpt_update_count: usize, - all_diff_hunks_expanded: bool, - show_headers: bool, -} - -#[derive(Debug, Clone)] -/// A piece of text in the multi-buffer -enum DiffTransform { - Unmodified { - summary: MBTextSummary, - }, - InsertedHunk { - summary: MBTextSummary, - hunk_info: DiffTransformHunkInfo, - }, - FilteredInsertedHunk { - summary: MBTextSummary, - hunk_info: DiffTransformHunkInfo, - }, - DeletedHunk { - summary: TextSummary, - buffer_id: BufferId, - hunk_info: DiffTransformHunkInfo, - has_trailing_newline: bool, - }, -} - -#[derive(Clone, Debug)] -struct DiffTransformHunkInfo { - excerpt_id: ExcerptId, - hunk_start_anchor: text::Anchor, - hunk_secondary_status: DiffHunkSecondaryStatus, - base_text_byte_range: Range, -} - -impl Eq for DiffTransformHunkInfo {} - -impl PartialEq for DiffTransformHunkInfo { - fn eq(&self, other: &DiffTransformHunkInfo) -> bool { - self.excerpt_id == other.excerpt_id && self.hunk_start_anchor == other.hunk_start_anchor - } -} - -impl std::hash::Hash for DiffTransformHunkInfo { - fn hash(&self, state: &mut H) { - self.excerpt_id.hash(state); - self.hunk_start_anchor.hash(state); - } -} - -#[derive(Clone)] -pub struct ExcerptInfo { - pub id: ExcerptId, - pub buffer: BufferSnapshot, - pub buffer_id: BufferId, - pub range: ExcerptRange, - pub end_row: MultiBufferRow, -} - -/// Used with [`MultiBuffer::push_buffer_content_transform`] -#[derive(Clone, Debug)] -struct CurrentInsertedHunk { - hunk_excerpt_start: ExcerptOffset, - insertion_end_offset: ExcerptOffset, - hunk_info: DiffTransformHunkInfo, - is_filtered: bool, -} - -impl std::fmt::Debug for ExcerptInfo { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct(type_name::()) - .field("id", &self.id) - .field("buffer_id", &self.buffer_id) - .field("path", &self.buffer.file().map(|f| f.path())) - .field("range", &self.range) - .finish() - } -} - -/// A boundary between `Excerpt`s in a [`MultiBuffer`] -#[derive(Debug)] -pub struct ExcerptBoundary { - pub prev: Option, - pub next: ExcerptInfo, - /// The row in the `MultiBuffer` where the boundary is located - pub row: MultiBufferRow, -} - -impl ExcerptBoundary { - pub fn starts_new_buffer(&self) -> bool { - match (self.prev.as_ref(), &self.next) { - (None, _) => true, - (Some(prev), next) => prev.buffer_id != next.buffer_id, - } - } -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct ExpandInfo { - pub direction: ExpandExcerptDirection, - pub excerpt_id: ExcerptId, -} - -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -pub struct RowInfo { - pub buffer_id: Option, - pub buffer_row: Option, - pub base_text_row: Option, - pub multibuffer_row: Option, - pub diff_status: Option, - pub expand_info: Option, - pub wrapped_buffer_row: Option, -} - -/// A slice into a [`Buffer`] that is being edited in a [`MultiBuffer`]. -#[derive(Clone)] -struct Excerpt { - /// The unique identifier for this excerpt - id: ExcerptId, - /// The location of the excerpt in the [`MultiBuffer`] - locator: Locator, - /// The buffer being excerpted - buffer_id: BufferId, - /// A snapshot of the buffer being excerpted - buffer: BufferSnapshot, - /// The range of the buffer to be shown in the excerpt - range: ExcerptRange, - /// The last row in the excerpted slice of the buffer - max_buffer_row: BufferRow, - /// A summary of the text in the excerpt - text_summary: TextSummary, - has_trailing_newline: bool, -} - -/// A public view into an `Excerpt` in a [`MultiBuffer`]. -/// -/// Contains methods for getting the [`Buffer`] of the excerpt, -/// as well as mapping offsets to/from buffer and multibuffer coordinates. -#[derive(Clone)] -pub struct MultiBufferExcerpt<'a> { - excerpt: &'a Excerpt, - diff_transforms: - sum_tree::Cursor<'a, 'static, DiffTransform, DiffTransforms>, - /// The offset in the multibuffer considering diff transforms. - offset: MultiBufferOffset, - /// The offset in the multibuffer without diff transforms. - excerpt_offset: ExcerptOffset, - buffer_offset: BufferOffset, -} - -#[derive(Clone, Debug)] -struct ExcerptIdMapping { - id: ExcerptId, - locator: Locator, -} - -/// A range of text from a single [`Buffer`], to be shown as an `Excerpt`. -/// These ranges are relative to the buffer itself -#[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub struct ExcerptRange { - /// The full range of text to be shown in the excerpt. - pub context: Range, - /// The primary range of text to be highlighted in the excerpt. - /// In a multi-buffer search, this would be the text that matched the search - pub primary: Range, -} - -impl ExcerptRange { - pub fn new(context: Range) -> Self { - Self { - context: context.clone(), - primary: context, - } - } -} - -#[derive(Clone, Debug, Default)] -pub struct ExcerptSummary { - excerpt_id: ExcerptId, - /// The location of the last [`Excerpt`] being summarized - excerpt_locator: Locator, - widest_line_number: u32, - text: MBTextSummary, -} - -#[derive(Debug, Clone)] -pub struct DiffTransformSummary { - input: MBTextSummary, - output: MBTextSummary, -} - -/// Summary of a string of text. -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] -pub struct MBTextSummary { - /// Length in bytes. - pub len: MultiBufferOffset, - /// Length in UTF-8. - pub chars: usize, - /// Length in UTF-16 code units - pub len_utf16: OffsetUtf16, - /// A point representing the number of lines and the length of the last line. - /// - /// In other words, it marks the point after the last byte in the text, (if - /// EOF was a character, this would be its position). - pub lines: Point, - /// How many `char`s are in the first line - pub first_line_chars: u32, - /// How many `char`s are in the last line - pub last_line_chars: u32, - /// How many UTF-16 code units are in the last line - pub last_line_len_utf16: u32, - /// The row idx of the longest row - pub longest_row: u32, - /// How many `char`s are in the longest row - pub longest_row_chars: u32, -} - -impl From for MBTextSummary { - fn from(summary: TextSummary) -> Self { - MBTextSummary { - len: MultiBufferOffset(summary.len), - chars: summary.chars, - len_utf16: summary.len_utf16, - lines: summary.lines, - first_line_chars: summary.first_line_chars, - last_line_chars: summary.last_line_chars, - last_line_len_utf16: summary.last_line_len_utf16, - longest_row: summary.longest_row, - longest_row_chars: summary.longest_row_chars, - } - } -} -impl From<&str> for MBTextSummary { - fn from(text: &str) -> Self { - MBTextSummary::from(TextSummary::from(text)) - } -} - -impl MultiBufferDimension for MBTextSummary { - type TextDimension = TextSummary; - - fn from_summary(summary: &MBTextSummary) -> Self { - *summary - } - - fn add_text_dim(&mut self, summary: &Self::TextDimension) { - *self += *summary; - } - - fn add_mb_text_summary(&mut self, summary: &MBTextSummary) { - *self += *summary; - } -} - -impl AddAssign for MBTextSummary { - fn add_assign(&mut self, other: MBTextSummary) { - let joined_chars = self.last_line_chars + other.first_line_chars; - if joined_chars > self.longest_row_chars { - self.longest_row = self.lines.row; - self.longest_row_chars = joined_chars; - } - if other.longest_row_chars > self.longest_row_chars { - self.longest_row = self.lines.row + other.longest_row; - self.longest_row_chars = other.longest_row_chars; - } - - if self.lines.row == 0 { - self.first_line_chars += other.first_line_chars; - } - - if other.lines.row == 0 { - self.last_line_chars += other.first_line_chars; - self.last_line_len_utf16 += other.last_line_len_utf16; - } else { - self.last_line_chars = other.last_line_chars; - self.last_line_len_utf16 = other.last_line_len_utf16; - } - - self.chars += other.chars; - self.len += other.len; - self.len_utf16 += other.len_utf16; - self.lines += other.lines; - } -} - -impl AddAssign for MBTextSummary { - fn add_assign(&mut self, other: TextSummary) { - *self += MBTextSummary::from(other); - } -} - -impl MBTextSummary { - pub fn lines_utf16(&self) -> PointUtf16 { - PointUtf16 { - row: self.lines.row, - column: self.last_line_len_utf16, - } - } -} - -impl MultiBufferDimension for DimensionPair -where - K: MultiBufferDimension, - V: MultiBufferDimension, -{ - type TextDimension = DimensionPair; - - fn from_summary(summary: &MBTextSummary) -> Self { - Self { - key: K::from_summary(summary), - value: Some(V::from_summary(summary)), - } - } - - fn add_text_dim(&mut self, summary: &Self::TextDimension) { - self.key.add_text_dim(&summary.key); - if let Some(value) = &mut self.value { - if let Some(other_value) = summary.value.as_ref() { - value.add_text_dim(other_value); - } - } - } - - fn add_mb_text_summary(&mut self, summary: &MBTextSummary) { - self.key.add_mb_text_summary(summary); - if let Some(value) = &mut self.value { - value.add_mb_text_summary(summary); - } - } -} - -#[derive(Clone)] -pub struct MultiBufferRows<'a> { - point: Point, - is_empty: bool, - is_singleton: bool, - cursor: MultiBufferCursor<'a, Point, Point>, -} - -pub struct MultiBufferChunks<'a> { - excerpts: Cursor<'a, 'static, Excerpt, ExcerptOffset>, - diff_transforms: - Cursor<'a, 'static, DiffTransform, Dimensions>, - diffs: &'a TreeMap, - diff_base_chunks: Option<(BufferId, BufferChunks<'a>)>, - buffer_chunk: Option>, - range: Range, - excerpt_offset_range: Range, - excerpt_chunks: Option>, - language_aware: bool, -} - -pub struct ReversedMultiBufferChunks<'a> { - cursor: MultiBufferCursor<'a, MultiBufferOffset, BufferOffset>, - current_chunks: Option>, - start: MultiBufferOffset, - offset: MultiBufferOffset, -} - -pub struct MultiBufferBytes<'a> { - range: Range, - cursor: MultiBufferCursor<'a, MultiBufferOffset, BufferOffset>, - excerpt_bytes: Option>, - has_trailing_newline: bool, - chunk: &'a [u8], -} - -pub struct ReversedMultiBufferBytes<'a> { - range: Range, - chunks: ReversedMultiBufferChunks<'a>, - chunk: &'a [u8], -} - -#[derive(Clone)] -struct DiffTransforms { - output_dimension: OutputDimension, - excerpt_dimension: ExcerptDimension, -} - -impl<'a, MBD: MultiBufferDimension> Dimension<'a, DiffTransformSummary> for DiffTransforms { - fn zero(cx: ::Context<'_>) -> Self { - Self { - output_dimension: OutputDimension::zero(cx), - excerpt_dimension: as Dimension<'a, DiffTransformSummary>>::zero( - cx, - ), - } - } - - fn add_summary( - &mut self, - summary: &'a DiffTransformSummary, - cx: ::Context<'_>, - ) { - self.output_dimension.add_summary(summary, cx); - self.excerpt_dimension.add_summary(summary, cx); - } -} - -#[derive(Clone)] -struct MultiBufferCursor<'a, MBD, BD> { - excerpts: Cursor<'a, 'static, Excerpt, ExcerptDimension>, - diff_transforms: Cursor<'a, 'static, DiffTransform, DiffTransforms>, - snapshot: &'a MultiBufferSnapshot, - cached_region: Option>, -} - -/// Matches transformations to an item -/// This is essentially a more detailed version of DiffTransform -#[derive(Clone)] -struct MultiBufferRegion<'a, MBD, BD> { - buffer: &'a BufferSnapshot, - is_main_buffer: bool, - diff_hunk_status: Option, - excerpt: &'a Excerpt, - buffer_range: Range, - diff_base_byte_range: Option>, - range: Range, - has_trailing_newline: bool, -} - -impl<'a, MBD, BD> MultiBufferRegion<'a, MBD, BD> -where - MBD: Ord, - BD: Ord, -{ - fn is_filtered(&self) -> bool { - self.range.is_empty() && self.buffer_range.is_empty() && self.diff_hunk_status == None - } -} - -struct ExcerptChunks<'a> { - excerpt_id: ExcerptId, - content_chunks: BufferChunks<'a>, - has_footer: bool, -} - -#[derive(Debug)] -struct BufferEdit { - range: Range, - new_text: Arc, - is_insertion: bool, - original_indent_column: Option, - excerpt_id: ExcerptId, -} - -#[derive(Clone, Copy, Debug, PartialEq)] -enum DiffChangeKind { - BufferEdited, - DiffUpdated { base_changed: bool }, - ExpandOrCollapseHunks { expand: bool }, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum ExpandExcerptDirection { - Up, - Down, - UpAndDown, -} - -impl ExpandExcerptDirection { - pub fn should_expand_up(&self) -> bool { - match self { - ExpandExcerptDirection::Up => true, - ExpandExcerptDirection::Down => false, - ExpandExcerptDirection::UpAndDown => true, - } - } - - pub fn should_expand_down(&self) -> bool { - match self { - ExpandExcerptDirection::Up => false, - ExpandExcerptDirection::Down => true, - ExpandExcerptDirection::UpAndDown => true, - } - } -} - -#[derive(Clone, Debug, PartialEq)] -pub struct IndentGuide { - pub buffer_id: BufferId, - pub start_row: MultiBufferRow, - pub end_row: MultiBufferRow, - pub depth: u32, - pub tab_size: u32, - pub settings: IndentGuideSettings, -} - -impl IndentGuide { - pub fn indent_level(&self) -> u32 { - self.depth * self.tab_size - } -} - -impl MultiBuffer { - pub fn new(capability: Capability) -> Self { - Self::new_( - capability, - MultiBufferSnapshot { - show_headers: true, - ..MultiBufferSnapshot::default() - }, - ) - } - - pub fn without_headers(capability: Capability) -> Self { - Self::new_(capability, Default::default()) - } - - pub fn singleton(buffer: Entity, cx: &mut Context) -> Self { - let mut this = Self::new_( - buffer.read(cx).capability(), - MultiBufferSnapshot { - singleton: true, - ..MultiBufferSnapshot::default() - }, - ); - this.singleton = true; - let buffer_id = buffer.read(cx).remote_id(); - this.push_excerpts( - buffer, - [ExcerptRange::new(text::Anchor::min_max_range_for_buffer( - buffer_id, - ))], - cx, - ); - this - } - - #[inline] - pub fn new_(capability: Capability, snapshot: MultiBufferSnapshot) -> Self { - Self { - snapshot: RefCell::new(snapshot), - buffers: Default::default(), - diffs: HashMap::default(), - subscriptions: Topic::default(), - singleton: false, - capability, - title: None, - excerpts_by_path: Default::default(), - paths_by_excerpt: Default::default(), - buffer_changed_since_sync: Default::default(), - history: History::default(), - follower: None, - filter_mode: None, - } - } - - pub fn clone(&self, new_cx: &mut Context) -> Self { - let mut buffers = HashMap::default(); - let buffer_changed_since_sync = Rc::new(Cell::new(false)); - for (buffer_id, buffer_state) in self.buffers.iter() { - buffer_state.buffer.update(new_cx, |buffer, _| { - buffer.record_changes(Rc::downgrade(&buffer_changed_since_sync)); - }); - buffers.insert( - *buffer_id, - BufferState { - buffer: buffer_state.buffer.clone(), - last_version: buffer_state.last_version.clone(), - last_non_text_state_update_count: buffer_state - .last_non_text_state_update_count - .clone(), - excerpts: buffer_state.excerpts.clone(), - _subscriptions: [ - new_cx.observe(&buffer_state.buffer, |_, _, cx| cx.notify()), - new_cx.subscribe(&buffer_state.buffer, Self::on_buffer_event), - ], - }, - ); - } - let mut diff_bases = HashMap::default(); - for (buffer_id, diff) in self.diffs.iter() { - diff_bases.insert(*buffer_id, DiffState::new(diff.diff.clone(), new_cx)); - } - Self { - snapshot: RefCell::new(self.snapshot.borrow().clone()), - buffers: buffers, - excerpts_by_path: self.excerpts_by_path.clone(), - paths_by_excerpt: self.paths_by_excerpt.clone(), - diffs: diff_bases, - subscriptions: Default::default(), - singleton: self.singleton, - capability: self.capability, - history: self.history.clone(), - title: self.title.clone(), - buffer_changed_since_sync, - follower: None, - filter_mode: None, - } - } - - pub fn get_or_create_follower(&mut self, cx: &mut Context) -> Entity { - use gpui::AppContext as _; - - if let Some(follower) = &self.follower { - return follower.clone(); - } - - let follower = cx.new(|cx| self.clone(cx)); - follower.update(cx, |follower, _cx| { - follower.capability = Capability::ReadOnly; - }); - self.follower = Some(follower.clone()); - follower - } - - pub fn set_filter_mode(&mut self, new_mode: Option) { - self.filter_mode = new_mode; - let excerpt_len = self - .snapshot - .get_mut() - .diff_transforms - .summary() - .excerpt_len(); - let edits = Self::sync_diff_transforms( - self.snapshot.get_mut(), - vec![Edit { - old: ExcerptDimension(MultiBufferOffset(0))..excerpt_len, - new: ExcerptDimension(MultiBufferOffset(0))..excerpt_len, - }], - // TODO(split-diff) is this right? - DiffChangeKind::BufferEdited, - new_mode, - ); - if !edits.is_empty() { - self.subscriptions.publish(edits); - } - } - - pub fn set_group_interval(&mut self, group_interval: Duration) { - self.history.set_group_interval(group_interval); - } - - pub fn with_title(mut self, title: String) -> Self { - self.title = Some(title); - self - } - - pub fn read_only(&self) -> bool { - self.capability == Capability::ReadOnly - } - - /// Returns an up-to-date snapshot of the MultiBuffer. - #[ztracing::instrument(skip_all)] - pub fn snapshot(&self, cx: &App) -> MultiBufferSnapshot { - self.sync(cx); - self.snapshot.borrow().clone() - } - - pub fn read(&self, cx: &App) -> Ref<'_, MultiBufferSnapshot> { - self.sync(cx); - self.snapshot.borrow() - } - - pub fn as_singleton(&self) -> Option> { - if self.singleton { - Some(self.buffers.values().next().unwrap().buffer.clone()) - } else { - None - } - } - - pub fn is_singleton(&self) -> bool { - self.singleton - } - - pub fn subscribe(&mut self) -> Subscription { - self.subscriptions.subscribe() - } - - pub fn is_dirty(&self, cx: &App) -> bool { - self.read(cx).is_dirty() - } - - pub fn has_deleted_file(&self, cx: &App) -> bool { - self.read(cx).has_deleted_file() - } - - pub fn has_conflict(&self, cx: &App) -> bool { - self.read(cx).has_conflict() - } - - // The `is_empty` signature doesn't match what clippy expects - #[allow(clippy::len_without_is_empty)] - pub fn len(&self, cx: &App) -> MultiBufferOffset { - self.read(cx).len() - } - - pub fn is_empty(&self) -> bool { - self.buffers.is_empty() - } - - pub fn edit( - &mut self, - edits: I, - autoindent_mode: Option, - cx: &mut Context, - ) where - I: IntoIterator, T)>, - S: ToOffset, - T: Into>, - { - if self.read_only() || self.buffers.is_empty() { - return; - } - self.sync_mut(cx); - let edits = edits - .into_iter() - .map(|(range, new_text)| { - let mut range = range.start.to_offset(self.snapshot.get_mut()) - ..range.end.to_offset(self.snapshot.get_mut()); - if range.start > range.end { - mem::swap(&mut range.start, &mut range.end); - } - (range, new_text.into()) - }) - .collect::>(); - - return edit_internal(self, edits, autoindent_mode, cx); - - // Non-generic part of edit, hoisted out to avoid blowing up LLVM IR. - fn edit_internal( - this: &mut MultiBuffer, - edits: Vec<(Range, Arc)>, - mut autoindent_mode: Option, - cx: &mut Context, - ) { - let original_indent_columns = match &mut autoindent_mode { - Some(AutoindentMode::Block { - original_indent_columns, - }) => mem::take(original_indent_columns), - _ => Default::default(), - }; - - let (buffer_edits, edited_excerpt_ids) = MultiBuffer::convert_edits_to_buffer_edits( - edits, - this.snapshot.get_mut(), - &original_indent_columns, - ); - - let mut buffer_ids = Vec::with_capacity(buffer_edits.len()); - for (buffer_id, mut edits) in buffer_edits { - buffer_ids.push(buffer_id); - edits.sort_by_key(|edit| edit.range.start); - this.buffers[&buffer_id].buffer.update(cx, |buffer, cx| { - let mut edits = edits.into_iter().peekable(); - let mut insertions = Vec::new(); - let mut original_indent_columns = Vec::new(); - let mut deletions = Vec::new(); - let empty_str: Arc = Arc::default(); - while let Some(BufferEdit { - mut range, - mut new_text, - mut is_insertion, - original_indent_column, - excerpt_id, - }) = edits.next() - { - while let Some(BufferEdit { - range: next_range, - is_insertion: next_is_insertion, - new_text: next_new_text, - excerpt_id: next_excerpt_id, - .. - }) = edits.peek() - { - if range.end >= next_range.start { - range.end = cmp::max(next_range.end, range.end); - is_insertion |= *next_is_insertion; - if excerpt_id == *next_excerpt_id { - new_text = format!("{new_text}{next_new_text}").into(); - } - edits.next(); - } else { - break; - } - } - - if is_insertion { - original_indent_columns.push(original_indent_column); - insertions.push(( - buffer.anchor_before(range.start)..buffer.anchor_before(range.end), - new_text.clone(), - )); - } else if !range.is_empty() { - deletions.push(( - buffer.anchor_before(range.start)..buffer.anchor_before(range.end), - empty_str.clone(), - )); - } - } - - let deletion_autoindent_mode = - if let Some(AutoindentMode::Block { .. }) = autoindent_mode { - Some(AutoindentMode::Block { - original_indent_columns: Default::default(), - }) - } else { - autoindent_mode.clone() - }; - let insertion_autoindent_mode = - if let Some(AutoindentMode::Block { .. }) = autoindent_mode { - Some(AutoindentMode::Block { - original_indent_columns, - }) - } else { - autoindent_mode.clone() - }; - - buffer.edit(deletions, deletion_autoindent_mode, cx); - buffer.edit(insertions, insertion_autoindent_mode, cx); - }) - } - - cx.emit(Event::ExcerptsEdited { - excerpt_ids: edited_excerpt_ids, - buffer_ids, - }); - } - } - - fn convert_edits_to_buffer_edits( - edits: Vec<(Range, Arc)>, - snapshot: &MultiBufferSnapshot, - original_indent_columns: &[Option], - ) -> (HashMap>, Vec) { - let mut buffer_edits: HashMap> = Default::default(); - let mut edited_excerpt_ids = Vec::new(); - let mut cursor = snapshot.cursor::(); - for (ix, (range, new_text)) in edits.into_iter().enumerate() { - let original_indent_column = original_indent_columns.get(ix).copied().flatten(); - - cursor.seek(&range.start); - let mut start_region = cursor.region().expect("start offset out of bounds"); - if !start_region.is_main_buffer { - cursor.next(); - if let Some(region) = cursor.region() { - start_region = region; - } else { - continue; - } - } - - if range.end < start_region.range.start { - continue; - } - - if range.end > start_region.range.end { - cursor.seek_forward(&range.end); - } - let mut end_region = cursor.region().expect("end offset out of bounds"); - if !end_region.is_main_buffer { - cursor.prev(); - if let Some(region) = cursor.region() { - end_region = region; - } else { - continue; - } - } - - if range.start > end_region.range.end { - continue; - } - - let start_overshoot = range.start.saturating_sub(start_region.range.start); - let end_overshoot = range.end.saturating_sub(end_region.range.start); - let buffer_start = (start_region.buffer_range.start + start_overshoot) - .min(start_region.buffer_range.end); - let buffer_end = - (end_region.buffer_range.start + end_overshoot).min(end_region.buffer_range.end); - - if start_region.excerpt.id == end_region.excerpt.id { - if start_region.is_main_buffer { - edited_excerpt_ids.push(start_region.excerpt.id); - buffer_edits - .entry(start_region.buffer.remote_id()) - .or_default() - .push(BufferEdit { - range: buffer_start..buffer_end, - new_text, - is_insertion: true, - original_indent_column, - excerpt_id: start_region.excerpt.id, - }); - } - } else { - let start_excerpt_range = buffer_start..start_region.buffer_range.end; - let end_excerpt_range = end_region.buffer_range.start..buffer_end; - if start_region.is_main_buffer { - edited_excerpt_ids.push(start_region.excerpt.id); - buffer_edits - .entry(start_region.buffer.remote_id()) - .or_default() - .push(BufferEdit { - range: start_excerpt_range, - new_text: new_text.clone(), - is_insertion: true, - original_indent_column, - excerpt_id: start_region.excerpt.id, - }); - } - if end_region.is_main_buffer { - edited_excerpt_ids.push(end_region.excerpt.id); - buffer_edits - .entry(end_region.buffer.remote_id()) - .or_default() - .push(BufferEdit { - range: end_excerpt_range, - new_text: new_text.clone(), - is_insertion: false, - original_indent_column, - excerpt_id: end_region.excerpt.id, - }); - } - - cursor.seek(&range.start); - cursor.next_excerpt(); - while let Some(region) = cursor.region() { - if region.excerpt.id == end_region.excerpt.id { - break; - } - if region.is_main_buffer { - edited_excerpt_ids.push(region.excerpt.id); - buffer_edits - .entry(region.buffer.remote_id()) - .or_default() - .push(BufferEdit { - range: region.buffer_range, - new_text: new_text.clone(), - is_insertion: false, - original_indent_column, - excerpt_id: region.excerpt.id, - }); - } - cursor.next_excerpt(); - } - } - } - (buffer_edits, edited_excerpt_ids) - } - - pub fn autoindent_ranges(&mut self, ranges: I, cx: &mut Context) - where - I: IntoIterator>, - S: ToOffset, - { - if self.read_only() || self.buffers.is_empty() { - return; - } - self.sync_mut(cx); - let empty = Arc::::from(""); - let edits = ranges - .into_iter() - .map(|range| { - let mut range = range.start.to_offset(self.snapshot.get_mut()) - ..range.end.to_offset(&self.snapshot.get_mut()); - if range.start > range.end { - mem::swap(&mut range.start, &mut range.end); - } - (range, empty.clone()) - }) - .collect::>(); - - return autoindent_ranges_internal(self, edits, cx); - - fn autoindent_ranges_internal( - this: &mut MultiBuffer, - edits: Vec<(Range, Arc)>, - cx: &mut Context, - ) { - let (buffer_edits, edited_excerpt_ids) = - MultiBuffer::convert_edits_to_buffer_edits(edits, this.snapshot.get_mut(), &[]); - - let mut buffer_ids = Vec::new(); - for (buffer_id, mut edits) in buffer_edits { - buffer_ids.push(buffer_id); - edits.sort_unstable_by_key(|edit| edit.range.start); - - let mut ranges: Vec> = Vec::new(); - for edit in edits { - if let Some(last_range) = ranges.last_mut() - && edit.range.start <= last_range.end - { - last_range.end = last_range.end.max(edit.range.end); - continue; - } - ranges.push(edit.range); - } - - this.buffers[&buffer_id].buffer.update(cx, |buffer, cx| { - buffer.autoindent_ranges(ranges, cx); - }) - } - - cx.emit(Event::ExcerptsEdited { - excerpt_ids: edited_excerpt_ids, - buffer_ids, - }); - } - } - - /// Inserts newlines at the given position to create an empty line, returning the start of the new line. - /// You can also request the insertion of empty lines above and below the line starting at the returned point. - /// Panics if the given position is invalid. - pub fn insert_empty_line( - &mut self, - position: impl ToPoint, - space_above: bool, - space_below: bool, - cx: &mut Context, - ) -> Point { - let multibuffer_point = position.to_point(&self.read(cx)); - let (buffer, buffer_point, _) = self.point_to_buffer_point(multibuffer_point, cx).unwrap(); - self.start_transaction(cx); - let empty_line_start = buffer.update(cx, |buffer, cx| { - buffer.insert_empty_line(buffer_point, space_above, space_below, cx) - }); - self.end_transaction(cx); - multibuffer_point + (empty_line_start - buffer_point) - } - - pub fn set_active_selections( - &self, - selections: &[Selection], - line_mode: bool, - cursor_shape: CursorShape, - cx: &mut Context, - ) { - let mut selections_by_buffer: HashMap>> = - Default::default(); - let snapshot = self.read(cx); - let mut cursor = snapshot.excerpts.cursor::>(()); - for selection in selections { - let start_locator = snapshot.excerpt_locator_for_id(selection.start.excerpt_id); - let end_locator = snapshot.excerpt_locator_for_id(selection.end.excerpt_id); - - cursor.seek(&Some(start_locator), Bias::Left); - while let Some(excerpt) = cursor.item() - && excerpt.locator <= *end_locator - { - let mut start = excerpt.range.context.start; - let mut end = excerpt.range.context.end; - if excerpt.id == selection.start.excerpt_id { - start = selection.start.text_anchor; - } - if excerpt.id == selection.end.excerpt_id { - end = selection.end.text_anchor; - } - selections_by_buffer - .entry(excerpt.buffer_id) - .or_default() - .push(Selection { - id: selection.id, - start, - end, - reversed: selection.reversed, - goal: selection.goal, - }); - - cursor.next(); - } - } - - for (buffer_id, buffer_state) in self.buffers.iter() { - if !selections_by_buffer.contains_key(buffer_id) { - buffer_state - .buffer - .update(cx, |buffer, cx| buffer.remove_active_selections(cx)); - } - } - - for (buffer_id, mut selections) in selections_by_buffer { - self.buffers[&buffer_id].buffer.update(cx, |buffer, cx| { - selections.sort_unstable_by(|a, b| a.start.cmp(&b.start, buffer)); - let mut selections = selections.into_iter().peekable(); - let merged_selections = Arc::from_iter(iter::from_fn(|| { - let mut selection = selections.next()?; - while let Some(next_selection) = selections.peek() { - if selection.end.cmp(&next_selection.start, buffer).is_ge() { - let next_selection = selections.next().unwrap(); - if next_selection.end.cmp(&selection.end, buffer).is_ge() { - selection.end = next_selection.end; - } - } else { - break; - } - } - Some(selection) - })); - buffer.set_active_selections(merged_selections, line_mode, cursor_shape, cx); - }); - } - } - - pub fn remove_active_selections(&self, cx: &mut Context) { - for buffer in self.buffers.values() { - buffer - .buffer - .update(cx, |buffer, cx| buffer.remove_active_selections(cx)); - } - } - - pub fn push_excerpts( - &mut self, - buffer: Entity, - ranges: impl IntoIterator>, - cx: &mut Context, - ) -> Vec - where - O: text::ToOffset + Clone, - { - self.insert_excerpts_after(ExcerptId::max(), buffer, ranges, cx) - } - - #[instrument(skip_all)] - fn merge_excerpt_ranges<'a>( - expanded_ranges: impl IntoIterator> + 'a, - ) -> (Vec>, Vec) { - let mut merged_ranges: Vec> = Vec::new(); - let mut counts: Vec = Vec::new(); - for range in expanded_ranges { - if let Some(last_range) = merged_ranges.last_mut() { - assert!( - last_range.context.start <= range.context.start, - "ranges must be sorted: {last_range:?} <= {range:?}" - ); - if last_range.context.end >= range.context.start - || last_range.context.end.row + 1 == range.context.start.row - { - last_range.context.end = range.context.end.max(last_range.context.end); - *counts.last_mut().unwrap() += 1; - continue; - } - } - merged_ranges.push(range.clone()); - counts.push(1); - } - (merged_ranges, counts) - } - - pub fn insert_excerpts_after( - &mut self, - prev_excerpt_id: ExcerptId, - buffer: Entity, - ranges: impl IntoIterator>, - cx: &mut Context, - ) -> Vec - where - O: text::ToOffset + Clone, - { - let mut ids = Vec::new(); - let mut next_excerpt_id = - if let Some(last_entry) = self.snapshot.borrow().excerpt_ids.last() { - last_entry.id.0 + 1 - } else { - 1 - }; - self.insert_excerpts_with_ids_after( - prev_excerpt_id, - buffer, - ranges.into_iter().map(|range| { - let id = ExcerptId(post_inc(&mut next_excerpt_id)); - ids.push(id); - (id, range) - }), - cx, - ); - ids - } - - pub fn insert_excerpts_with_ids_after( - &mut self, - prev_excerpt_id: ExcerptId, - buffer: Entity, - ranges: impl IntoIterator)>, - cx: &mut Context, - ) where - O: text::ToOffset + Clone, - { - // TODO(split-diff) see if it's worth time avoiding collecting here later - let collected_ranges: Vec<_> = ranges.into_iter().collect(); - - assert_eq!(self.history.transaction_depth(), 0); - let mut ranges = collected_ranges.iter().cloned().peekable(); - if ranges.peek().is_none() { - return Default::default(); - } - - self.sync_mut(cx); - - let buffer_snapshot = buffer.read(cx).snapshot(); - let buffer_id = buffer_snapshot.remote_id(); - - let buffer_state = self.buffers.entry(buffer_id).or_insert_with(|| { - self.buffer_changed_since_sync.replace(true); - buffer.update(cx, |buffer, _| { - buffer.record_changes(Rc::downgrade(&self.buffer_changed_since_sync)); - }); - BufferState { - last_version: RefCell::new(buffer_snapshot.version().clone()), - last_non_text_state_update_count: Cell::new( - buffer_snapshot.non_text_state_update_count(), - ), - excerpts: Default::default(), - _subscriptions: [ - cx.observe(&buffer, |_, _, cx| cx.notify()), - cx.subscribe(&buffer, Self::on_buffer_event), - ], - buffer: buffer.clone(), - } - }); - - let mut snapshot = self.snapshot.get_mut(); - - let mut prev_locator = snapshot.excerpt_locator_for_id(prev_excerpt_id).clone(); - let mut new_excerpt_ids = mem::take(&mut snapshot.excerpt_ids); - let mut cursor = snapshot.excerpts.cursor::>(()); - let mut new_excerpts = cursor.slice(&prev_locator, Bias::Right); - prev_locator = cursor.start().unwrap_or(Locator::min_ref()).clone(); - - let edit_start = ExcerptDimension(new_excerpts.summary().text.len); - new_excerpts.update_last( - |excerpt| { - excerpt.has_trailing_newline = true; - }, - (), - ); - - let next_locator = if let Some(excerpt) = cursor.item() { - excerpt.locator.clone() - } else { - Locator::max() - }; - - let mut excerpts = Vec::new(); - while let Some((id, range)) = ranges.next() { - let locator = Locator::between(&prev_locator, &next_locator); - if let Err(ix) = buffer_state.excerpts.binary_search(&locator) { - buffer_state.excerpts.insert(ix, locator.clone()); - } - let range = ExcerptRange { - context: buffer_snapshot.anchor_before(&range.context.start) - ..buffer_snapshot.anchor_after(&range.context.end), - primary: buffer_snapshot.anchor_before(&range.primary.start) - ..buffer_snapshot.anchor_after(&range.primary.end), - }; - excerpts.push((id, range.clone())); - let excerpt = Excerpt::new( - id, - locator.clone(), - buffer_id, - buffer_snapshot.clone(), - range, - ranges.peek().is_some() || cursor.item().is_some(), - ); - new_excerpts.push(excerpt, ()); - prev_locator = locator.clone(); - - if let Some(last_mapping_entry) = new_excerpt_ids.last() { - assert!(id > last_mapping_entry.id, "excerpt ids must be increasing"); - } - new_excerpt_ids.push(ExcerptIdMapping { id, locator }, ()); - } - - let edit_end = ExcerptDimension(new_excerpts.summary().text.len); - - let suffix = cursor.suffix(); - let changed_trailing_excerpt = suffix.is_empty(); - new_excerpts.append(suffix, ()); - drop(cursor); - snapshot.excerpts = new_excerpts; - snapshot.excerpt_ids = new_excerpt_ids; - if changed_trailing_excerpt { - snapshot.trailing_excerpt_update_count += 1; - } - - let edits = Self::sync_diff_transforms( - &mut snapshot, - vec![Edit { - old: edit_start..edit_start, - new: edit_start..edit_end, - }], - DiffChangeKind::BufferEdited, - self.filter_mode, - ); - if !edits.is_empty() { - self.subscriptions.publish(edits); - } - - if let Some(follower) = &self.follower { - follower.update(cx, |follower, cx| { - follower.insert_excerpts_with_ids_after( - prev_excerpt_id, - buffer.clone(), - collected_ranges, - cx, - ); - }) - } - - cx.emit(Event::Edited { - edited_buffer: None, - }); - cx.emit(Event::ExcerptsAdded { - buffer, - predecessor: prev_excerpt_id, - excerpts, - }); - cx.notify(); - } - - pub fn clear(&mut self, cx: &mut Context) { - self.sync_mut(cx); - let ids = self.excerpt_ids(); - let removed_buffer_ids = self.buffers.drain().map(|(id, _)| id).collect(); - self.excerpts_by_path.clear(); - self.paths_by_excerpt.clear(); - let MultiBufferSnapshot { - excerpts, - diffs: _, - diff_transforms: _, - non_text_state_update_count: _, - edit_count: _, - is_dirty, - has_deleted_file, - has_conflict, - singleton: _, - excerpt_ids: _, - replaced_excerpts, - trailing_excerpt_update_count, - all_diff_hunks_expanded: _, - show_headers: _, - } = self.snapshot.get_mut(); - let start = ExcerptDimension(MultiBufferOffset::ZERO); - let prev_len = ExcerptDimension(excerpts.summary().text.len); - *excerpts = Default::default(); - *trailing_excerpt_update_count += 1; - *is_dirty = false; - *has_deleted_file = false; - *has_conflict = false; - replaced_excerpts.clear(); - - let edits = Self::sync_diff_transforms( - self.snapshot.get_mut(), - vec![Edit { - old: start..prev_len, - new: start..start, - }], - DiffChangeKind::BufferEdited, - self.filter_mode, - ); - if !edits.is_empty() { - self.subscriptions.publish(edits); - } - if let Some(follower) = &self.follower { - follower.update(cx, |follower, cx| { - follower.clear(cx); - }) - } - cx.emit(Event::Edited { - edited_buffer: None, - }); - cx.emit(Event::ExcerptsRemoved { - ids, - removed_buffer_ids, - }); - cx.notify(); - } - - #[ztracing::instrument(skip_all)] - pub fn excerpts_for_buffer( - &self, - buffer_id: BufferId, - cx: &App, - ) -> Vec<(ExcerptId, ExcerptRange)> { - let mut excerpts = Vec::new(); - let snapshot = self.read(cx); - let mut cursor = snapshot.excerpts.cursor::>(()); - if let Some(locators) = self.buffers.get(&buffer_id).map(|state| &state.excerpts) { - for locator in locators { - cursor.seek_forward(&Some(locator), Bias::Left); - if let Some(excerpt) = cursor.item() - && excerpt.locator == *locator - { - excerpts.push((excerpt.id, excerpt.range.clone())); - } - } - } - - excerpts - } - - pub fn excerpt_ranges_for_buffer(&self, buffer_id: BufferId, cx: &App) -> Vec> { - let snapshot = self.read(cx); - let mut excerpts = snapshot - .excerpts - .cursor::, ExcerptPoint>>(()); - let mut diff_transforms = snapshot - .diff_transforms - .cursor::>>(()); - diff_transforms.next(); - let locators = self - .buffers - .get(&buffer_id) - .into_iter() - .flat_map(|state| &state.excerpts); - let mut result = Vec::new(); - for locator in locators { - excerpts.seek_forward(&Some(locator), Bias::Left); - if let Some(excerpt) = excerpts.item() - && excerpt.locator == *locator - { - let excerpt_start = excerpts.start().1; - let excerpt_end = excerpt_start + excerpt.text_summary.lines; - - diff_transforms.seek_forward(&excerpt_start, Bias::Left); - let overshoot = excerpt_start - diff_transforms.start().0; - let start = diff_transforms.start().1 + overshoot; - - diff_transforms.seek_forward(&excerpt_end, Bias::Right); - let overshoot = excerpt_end - diff_transforms.start().0; - let end = diff_transforms.start().1 + overshoot; - - result.push(start.0..end.0) - } - } - result - } - - pub fn excerpt_buffer_ids(&self) -> Vec { - self.snapshot - .borrow() - .excerpts - .iter() - .map(|entry| entry.buffer_id) - .collect() - } - - pub fn excerpt_ids(&self) -> Vec { - self.snapshot - .borrow() - .excerpts - .iter() - .map(|entry| entry.id) - .collect() - } - - pub fn excerpt_containing( - &self, - position: impl ToOffset, - cx: &App, - ) -> Option<(ExcerptId, Entity, Range)> { - let snapshot = self.read(cx); - let offset = position.to_offset(&snapshot); - - let mut cursor = snapshot.cursor::(); - cursor.seek(&offset); - cursor - .excerpt() - .or_else(|| snapshot.excerpts.last()) - .map(|excerpt| { - ( - excerpt.id, - self.buffers.get(&excerpt.buffer_id).unwrap().buffer.clone(), - excerpt.range.context.clone(), - ) - }) - } - - pub fn buffer_for_anchor(&self, anchor: Anchor, cx: &App) -> Option> { - if let Some(buffer_id) = anchor.text_anchor.buffer_id { - self.buffer(buffer_id) - } else { - let (_, buffer, _) = self.excerpt_containing(anchor, cx)?; - Some(buffer) - } - } - - // If point is at the end of the buffer, the last excerpt is returned - pub fn point_to_buffer_offset( - &self, - point: T, - cx: &App, - ) -> Option<(Entity, BufferOffset)> { - let snapshot = self.read(cx); - let (buffer, offset) = snapshot.point_to_buffer_offset(point)?; - Some(( - self.buffers.get(&buffer.remote_id())?.buffer.clone(), - offset, - )) - } - - // If point is at the end of the buffer, the last excerpt is returned - pub fn point_to_buffer_point( - &self, - point: T, - cx: &App, - ) -> Option<(Entity, Point, ExcerptId)> { - let snapshot = self.read(cx); - let (buffer, point, is_main_buffer) = - snapshot.point_to_buffer_point(point.to_point(&snapshot))?; - Some(( - self.buffers.get(&buffer.remote_id())?.buffer.clone(), - point, - is_main_buffer, - )) - } - - pub fn buffer_point_to_anchor( - &self, - buffer: &Entity, - point: Point, - cx: &App, - ) -> Option { - let mut found = None; - let snapshot = buffer.read(cx).snapshot(); - for (excerpt_id, range) in self.excerpts_for_buffer(snapshot.remote_id(), cx) { - let start = range.context.start.to_point(&snapshot); - let end = range.context.end.to_point(&snapshot); - if start <= point && point < end { - found = Some((snapshot.clip_point(point, Bias::Left), excerpt_id)); - break; - } - if point < start { - found = Some((start, excerpt_id)); - } - if point > end { - found = Some((end, excerpt_id)); - } - } - - found.map(|(point, excerpt_id)| { - let text_anchor = snapshot.anchor_after(point); - Anchor::in_buffer(excerpt_id, text_anchor) - }) - } - - pub fn buffer_anchor_to_anchor( - &self, - buffer: &Entity, - anchor: text::Anchor, - cx: &App, - ) -> Option { - let snapshot = buffer.read(cx).snapshot(); - for (excerpt_id, range) in self.excerpts_for_buffer(snapshot.remote_id(), cx) { - if range.context.start.cmp(&anchor, &snapshot).is_le() - && range.context.end.cmp(&anchor, &snapshot).is_ge() - { - return Some(Anchor::in_buffer(excerpt_id, anchor)); - } - } - - None - } - - pub fn remove_excerpts( - &mut self, - excerpt_ids: impl IntoIterator, - cx: &mut Context, - ) { - self.sync_mut(cx); - let ids = excerpt_ids.into_iter().collect::>(); - if ids.is_empty() { - return; - } - self.buffer_changed_since_sync.replace(true); - - let mut snapshot = self.snapshot.get_mut(); - let mut new_excerpts = SumTree::default(); - let mut cursor = snapshot - .excerpts - .cursor::, ExcerptOffset>>(()); - let mut edits = Vec::new(); - let mut excerpt_ids = ids.iter().copied().peekable(); - let mut removed_buffer_ids = Vec::new(); - - while let Some(excerpt_id) = excerpt_ids.next() { - self.paths_by_excerpt.remove(&excerpt_id); - // Seek to the next excerpt to remove, preserving any preceding excerpts. - let locator = snapshot.excerpt_locator_for_id(excerpt_id); - new_excerpts.append(cursor.slice(&Some(locator), Bias::Left), ()); - - if let Some(mut excerpt) = cursor.item() { - if excerpt.id != excerpt_id { - continue; - } - let mut old_start = cursor.start().1; - - // Skip over the removed excerpt. - 'remove_excerpts: loop { - if let Some(buffer_state) = self.buffers.get_mut(&excerpt.buffer_id) { - buffer_state.excerpts.retain(|l| l != &excerpt.locator); - if buffer_state.excerpts.is_empty() { - log::debug!( - "removing buffer and diff for buffer {}", - excerpt.buffer_id - ); - self.buffers.remove(&excerpt.buffer_id); - removed_buffer_ids.push(excerpt.buffer_id); - } - } - cursor.next(); - - // Skip over any subsequent excerpts that are also removed. - if let Some(&next_excerpt_id) = excerpt_ids.peek() { - let next_locator = snapshot.excerpt_locator_for_id(next_excerpt_id); - if let Some(next_excerpt) = cursor.item() - && next_excerpt.locator == *next_locator - { - excerpt_ids.next(); - excerpt = next_excerpt; - continue 'remove_excerpts; - } - } - - break; - } - - // When removing the last excerpt, remove the trailing newline from - // the previous excerpt. - if cursor.item().is_none() && old_start > MultiBufferOffset::ZERO { - old_start -= 1; - new_excerpts.update_last(|e| e.has_trailing_newline = false, ()); - } - - // Push an edit for the removal of this run of excerpts. - let old_end = cursor.start().1; - let new_start = ExcerptDimension(new_excerpts.summary().text.len); - edits.push(Edit { - old: old_start..old_end, - new: new_start..new_start, - }); - } - } - let suffix = cursor.suffix(); - let changed_trailing_excerpt = suffix.is_empty(); - new_excerpts.append(suffix, ()); - drop(cursor); - snapshot.excerpts = new_excerpts; - for buffer_id in &removed_buffer_ids { - self.diffs.remove(buffer_id); - snapshot.diffs.remove(buffer_id); - } - - if changed_trailing_excerpt { - snapshot.trailing_excerpt_update_count += 1; - } - - let edits = Self::sync_diff_transforms( - &mut snapshot, - edits, - DiffChangeKind::BufferEdited, - self.filter_mode, - ); - if !edits.is_empty() { - self.subscriptions.publish(edits); - } - - if let Some(follower) = &self.follower { - follower.update(cx, |follower, cx| { - follower.remove_excerpts(ids.clone(), cx); - }) - } - - cx.emit(Event::Edited { - edited_buffer: None, - }); - cx.emit(Event::ExcerptsRemoved { - ids, - removed_buffer_ids, - }); - cx.notify(); - } - - pub fn wait_for_anchors<'a, Anchors: 'a + Iterator>( - &self, - anchors: Anchors, - cx: &mut Context, - ) -> impl 'static + Future> + use { - let mut error = None; - let mut futures = Vec::new(); - for anchor in anchors { - if let Some(buffer_id) = anchor.text_anchor.buffer_id { - if let Some(buffer) = self.buffers.get(&buffer_id) { - buffer.buffer.update(cx, |buffer, _| { - futures.push(buffer.wait_for_anchors([anchor.text_anchor])) - }); - } else { - error = Some(anyhow!( - "buffer {buffer_id} is not part of this multi-buffer" - )); - break; - } - } - } - async move { - if let Some(error) = error { - Err(error)?; - } - for future in futures { - future.await?; - } - Ok(()) - } - } - - pub fn text_anchor_for_position( - &self, - position: T, - cx: &App, - ) -> Option<(Entity, language::Anchor)> { - let snapshot = self.read(cx); - let anchor = snapshot.anchor_before(position); - let buffer = self - .buffers - .get(&anchor.text_anchor.buffer_id?)? - .buffer - .clone(); - Some((buffer, anchor.text_anchor)) - } - - fn on_buffer_event( - &mut self, - buffer: Entity, - event: &language::BufferEvent, - cx: &mut Context, - ) { - use language::BufferEvent; - let buffer_id = buffer.read(cx).remote_id(); - cx.emit(match event { - BufferEvent::Edited => Event::Edited { - edited_buffer: Some(buffer), - }, - BufferEvent::DirtyChanged => Event::DirtyChanged, - BufferEvent::Saved => Event::Saved, - BufferEvent::FileHandleChanged => Event::FileHandleChanged, - BufferEvent::Reloaded => Event::Reloaded, - BufferEvent::LanguageChanged(has_language) => { - Event::LanguageChanged(buffer_id, *has_language) - } - BufferEvent::Reparsed => Event::Reparsed(buffer_id), - BufferEvent::DiagnosticsUpdated => Event::DiagnosticsUpdated, - BufferEvent::CapabilityChanged => { - self.capability = buffer.read(cx).capability(); - return; - } - BufferEvent::Operation { .. } | BufferEvent::ReloadNeeded => return, - }); - } - - fn buffer_diff_language_changed(&mut self, diff: Entity, cx: &mut Context) { - let diff = diff.read(cx); - let buffer_id = diff.buffer_id; - let diff = diff.snapshot(cx); - self.snapshot.get_mut().diffs.insert(buffer_id, diff); - } - - fn buffer_diff_changed( - &mut self, - diff: Entity, - range: Range, - cx: &mut Context, - ) { - self.sync_mut(cx); - - let diff = diff.read(cx); - let buffer_id = diff.buffer_id; - let Some(buffer_state) = self.buffers.get(&buffer_id) else { - return; - }; - self.buffer_changed_since_sync.replace(true); - - let buffer = buffer_state.buffer.read(cx); - let diff_change_range = range.to_offset(buffer); - - let new_diff = diff.snapshot(cx); - let mut snapshot = self.snapshot.get_mut(); - let base_text_changed = snapshot - .diffs - .get(&buffer_id) - .is_none_or(|old_diff| !new_diff.base_texts_eq(old_diff)); - - snapshot.diffs.insert_or_replace(buffer_id, new_diff); - - let mut excerpt_edits = Vec::new(); - for locator in &buffer_state.excerpts { - let mut cursor = snapshot - .excerpts - .cursor::, ExcerptOffset>>(()); - cursor.seek_forward(&Some(locator), Bias::Left); - if let Some(excerpt) = cursor.item() - && excerpt.locator == *locator - { - let excerpt_buffer_range = excerpt.range.context.to_offset(&excerpt.buffer); - if diff_change_range.end < excerpt_buffer_range.start - || diff_change_range.start > excerpt_buffer_range.end - { - continue; - } - let excerpt_start = cursor.start().1; - let excerpt_len = excerpt.text_summary.len; - let diff_change_start_in_excerpt = diff_change_range - .start - .saturating_sub(excerpt_buffer_range.start); - let diff_change_end_in_excerpt = diff_change_range - .end - .saturating_sub(excerpt_buffer_range.start); - let edit_start = excerpt_start + diff_change_start_in_excerpt.min(excerpt_len); - let edit_end = excerpt_start + diff_change_end_in_excerpt.min(excerpt_len); - excerpt_edits.push(Edit { - old: edit_start..edit_end, - new: edit_start..edit_end, - }); - } - } - - let edits = Self::sync_diff_transforms( - &mut snapshot, - excerpt_edits, - DiffChangeKind::DiffUpdated { - base_changed: base_text_changed, - }, - self.filter_mode, - ); - if !edits.is_empty() { - self.subscriptions.publish(edits); - } - cx.emit(Event::Edited { - edited_buffer: None, - }); - } - - pub fn all_buffers(&self) -> HashSet> { - self.buffers - .values() - .map(|state| state.buffer.clone()) - .collect() - } - - pub fn all_buffer_ids(&self) -> Vec { - self.buffers.keys().copied().collect() - } - - pub fn buffer(&self, buffer_id: BufferId) -> Option> { - self.buffers - .get(&buffer_id) - .map(|state| state.buffer.clone()) - } - - pub fn language_at(&self, point: T, cx: &App) -> Option> { - self.point_to_buffer_offset(point, cx) - .and_then(|(buffer, offset)| buffer.read(cx).language_at(offset)) - } - - pub fn language_settings<'a>(&'a self, cx: &'a App) -> Cow<'a, LanguageSettings> { - let buffer_id = self - .snapshot - .borrow() - .excerpts - .first() - .map(|excerpt| excerpt.buffer.remote_id()); - buffer_id - .and_then(|buffer_id| self.buffer(buffer_id)) - .map(|buffer| { - let buffer = buffer.read(cx); - language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx) - }) - .unwrap_or_else(move || self.language_settings_at(MultiBufferOffset::default(), cx)) - } - - pub fn language_settings_at<'a, T: ToOffset>( - &'a self, - point: T, - cx: &'a App, - ) -> Cow<'a, LanguageSettings> { - let mut language = None; - let mut file = None; - if let Some((buffer, offset)) = self.point_to_buffer_offset(point, cx) { - let buffer = buffer.read(cx); - language = buffer.language_at(offset); - file = buffer.file(); - } - language_settings(language.map(|l| l.name()), file, cx) - } - - pub fn for_each_buffer(&self, mut f: impl FnMut(&Entity)) { - self.buffers.values().for_each(|state| f(&state.buffer)) - } - - pub fn explicit_title(&self) -> Option<&str> { - self.title.as_deref() - } - - pub fn title<'a>(&'a self, cx: &'a App) -> Cow<'a, str> { - if let Some(title) = self.title.as_ref() { - return title.into(); - } - - if let Some(buffer) = self.as_singleton() { - let buffer = buffer.read(cx); - - if let Some(file) = buffer.file() { - return file.file_name(cx).into(); - } - - if let Some(title) = self.buffer_content_title(buffer) { - return title; - } - }; - - "untitled".into() - } - - fn buffer_content_title(&self, buffer: &Buffer) -> Option> { - let mut is_leading_whitespace = true; - let mut count = 0; - let mut prev_was_space = false; - let mut title = String::new(); - - for ch in buffer.snapshot().chars() { - if is_leading_whitespace && ch.is_whitespace() { - continue; - } - - is_leading_whitespace = false; - - if ch == '\n' || count >= 40 { - break; - } - - if ch.is_whitespace() { - if !prev_was_space { - title.push(' '); - count += 1; - prev_was_space = true; - } - } else { - title.push(ch); - count += 1; - prev_was_space = false; - } - } - - let title = title.trim_end().to_string(); - - if title.is_empty() { - return None; - } - - Some(title.into()) - } - - pub fn set_title(&mut self, title: String, cx: &mut Context) { - self.title = Some(title); - cx.notify(); - } - - /// Preserve preview tabs containing this multibuffer until additional edits occur. - pub fn refresh_preview(&self, cx: &mut Context) { - for buffer_state in self.buffers.values() { - buffer_state - .buffer - .update(cx, |buffer, _cx| buffer.refresh_preview()); - } - } - - /// Whether we should preserve the preview status of a tab containing this multi-buffer. - pub fn preserve_preview(&self, cx: &App) -> bool { - self.buffers - .values() - .all(|state| state.buffer.read(cx).preserve_preview()) - } - - #[cfg(any(test, feature = "test-support"))] - pub fn is_parsing(&self, cx: &App) -> bool { - self.as_singleton().unwrap().read(cx).is_parsing() - } - - pub fn add_diff(&mut self, diff: Entity, cx: &mut Context) { - let buffer_id = diff.read(cx).buffer_id; - self.buffer_diff_changed( - diff.clone(), - text::Anchor::min_max_range_for_buffer(buffer_id), - cx, - ); - self.diffs - .insert(buffer_id, DiffState::new(diff.clone(), cx)); - - if let Some(follower) = &self.follower { - follower.update(cx, |follower, cx| { - follower.add_diff(diff, cx); - }) - } - } - - pub fn diff_for(&self, buffer_id: BufferId) -> Option> { - self.diffs.get(&buffer_id).map(|state| state.diff.clone()) - } - - pub fn expand_diff_hunks(&mut self, ranges: Vec>, cx: &mut Context) { - self.expand_or_collapse_diff_hunks(ranges, true, cx); - } - - pub fn collapse_diff_hunks(&mut self, ranges: Vec>, cx: &mut Context) { - self.expand_or_collapse_diff_hunks(ranges, false, cx); - } - - pub fn set_all_diff_hunks_expanded(&mut self, cx: &mut Context) { - self.snapshot.get_mut().all_diff_hunks_expanded = true; - self.expand_or_collapse_diff_hunks(vec![Anchor::min()..Anchor::max()], true, cx); - } - - pub fn all_diff_hunks_expanded(&self) -> bool { - self.snapshot.borrow().all_diff_hunks_expanded - } - - pub fn set_all_diff_hunks_collapsed(&mut self, cx: &mut Context) { - self.snapshot.get_mut().all_diff_hunks_expanded = false; - self.expand_or_collapse_diff_hunks(vec![Anchor::min()..Anchor::max()], false, cx); - } - - pub fn has_multiple_hunks(&self, cx: &App) -> bool { - self.read(cx) - .diff_hunks_in_range(Anchor::min()..Anchor::max()) - .nth(1) - .is_some() - } - - pub fn single_hunk_is_expanded(&self, range: Range, cx: &App) -> bool { - let snapshot = self.read(cx); - let mut cursor = snapshot.diff_transforms.cursor::(()); - let offset_range = range.to_offset(&snapshot); - cursor.seek(&offset_range.start, Bias::Left); - while let Some(item) = cursor.item() { - if *cursor.start() >= offset_range.end && *cursor.start() > offset_range.start { - break; - } - if item.hunk_info().is_some() { - return true; - } - cursor.next(); - } - false - } - - pub fn has_expanded_diff_hunks_in_ranges(&self, ranges: &[Range], cx: &App) -> bool { - let snapshot = self.read(cx); - let mut cursor = snapshot.diff_transforms.cursor::(()); - for range in ranges { - let range = range.to_point(&snapshot); - let start = snapshot.point_to_offset(Point::new(range.start.row, 0)); - let end = snapshot.point_to_offset(Point::new(range.end.row + 1, 0)); - let start = start.saturating_sub_usize(1); - let end = snapshot.len().min(end + 1usize); - cursor.seek(&start, Bias::Right); - while let Some(item) = cursor.item() { - if *cursor.start() >= end { - break; - } - if item.hunk_info().is_some() { - return true; - } - cursor.next(); - } - } - false - } - - pub fn expand_or_collapse_diff_hunks_inner( - &mut self, - ranges: impl IntoIterator, ExcerptId)>, - expand: bool, - cx: &mut Context, - ) { - if self.snapshot.borrow().all_diff_hunks_expanded && !expand { - return; - } - self.sync_mut(cx); - let mut snapshot = self.snapshot.get_mut(); - let mut excerpt_edits = Vec::new(); - let mut last_hunk_row = None; - for (range, end_excerpt_id) in ranges { - for diff_hunk in snapshot.diff_hunks_in_range(range) { - if diff_hunk.excerpt_id.cmp(&end_excerpt_id, &snapshot).is_gt() { - continue; - } - if last_hunk_row.is_some_and(|row| row >= diff_hunk.row_range.start) { - continue; - } - let start = Anchor::in_buffer(diff_hunk.excerpt_id, diff_hunk.buffer_range.start); - let end = Anchor::in_buffer(diff_hunk.excerpt_id, diff_hunk.buffer_range.end); - let start = snapshot.excerpt_offset_for_anchor(&start); - let end = snapshot.excerpt_offset_for_anchor(&end); - last_hunk_row = Some(diff_hunk.row_range.start); - excerpt_edits.push(text::Edit { - old: start..end, - new: start..end, - }); - } - } - - let edits = Self::sync_diff_transforms( - &mut snapshot, - excerpt_edits, - DiffChangeKind::ExpandOrCollapseHunks { expand }, - self.filter_mode, - ); - if !edits.is_empty() { - self.subscriptions.publish(edits); - } - cx.emit(Event::DiffHunksToggled); - cx.emit(Event::Edited { - edited_buffer: None, - }); - } - - pub fn expand_or_collapse_diff_hunks( - &mut self, - ranges: Vec>, - expand: bool, - cx: &mut Context, - ) { - let snapshot = self.snapshot.borrow().clone(); - let ranges = ranges.iter().map(move |range| { - let end_excerpt_id = range.end.excerpt_id; - let range = range.to_point(&snapshot); - let mut peek_end = range.end; - if range.end.row < snapshot.max_row().0 { - peek_end = Point::new(range.end.row + 1, 0); - }; - (range.start..peek_end, end_excerpt_id) - }); - self.expand_or_collapse_diff_hunks_inner(ranges, expand, cx); - } - - pub fn resize_excerpt( - &mut self, - id: ExcerptId, - range: Range, - cx: &mut Context, - ) { - self.sync_mut(cx); - - let mut snapshot = self.snapshot.get_mut(); - let locator = snapshot.excerpt_locator_for_id(id); - let mut new_excerpts = SumTree::default(); - let mut cursor = snapshot - .excerpts - .cursor::, ExcerptOffset>>(()); - let mut edits = Vec::>::new(); - - let prefix = cursor.slice(&Some(locator), Bias::Left); - new_excerpts.append(prefix, ()); - - let mut excerpt = cursor.item().unwrap().clone(); - let old_text_len = excerpt.text_summary.len; - - excerpt.range.context.start = range.start; - excerpt.range.context.end = range.end; - excerpt.max_buffer_row = range.end.to_point(&excerpt.buffer).row; - - excerpt.text_summary = excerpt - .buffer - .text_summary_for_range(excerpt.range.context.clone()); - - let new_start_offset = ExcerptDimension(new_excerpts.summary().text.len); - let old_start_offset = cursor.start().1; - let new_text_len = excerpt.text_summary.len; - let edit = Edit { - old: old_start_offset..old_start_offset + old_text_len, - new: new_start_offset..new_start_offset + new_text_len, - }; - - if let Some(last_edit) = edits.last_mut() { - if last_edit.old.end == edit.old.start { - last_edit.old.end = edit.old.end; - last_edit.new.end = edit.new.end; - } else { - edits.push(edit); - } - } else { - edits.push(edit); - } - - new_excerpts.push(excerpt, ()); - - cursor.next(); - - new_excerpts.append(cursor.suffix(), ()); - - drop(cursor); - snapshot.excerpts = new_excerpts; - - if let Some(follower) = &self.follower { - follower.update(cx, |follower, cx| follower.resize_excerpt(id, range, cx)); - } - - let edits = Self::sync_diff_transforms( - &mut snapshot, - edits, - DiffChangeKind::BufferEdited, - self.filter_mode, - ); - if !edits.is_empty() { - self.subscriptions.publish(edits); - } - cx.emit(Event::Edited { - edited_buffer: None, - }); - cx.emit(Event::ExcerptsExpanded { ids: vec![id] }); - cx.notify(); - } - - pub fn expand_excerpts( - &mut self, - ids: impl IntoIterator, - line_count: u32, - direction: ExpandExcerptDirection, - cx: &mut Context, - ) { - if line_count == 0 { - return; - } - self.sync_mut(cx); - if !self.excerpts_by_path.is_empty() { - self.expand_excerpts_with_paths(ids, line_count, direction, cx); - return; - } - let mut snapshot = self.snapshot.get_mut(); - - let ids = ids.into_iter().collect::>(); - let locators = snapshot.excerpt_locators_for_ids(ids.iter().copied()); - let mut new_excerpts = SumTree::default(); - let mut cursor = snapshot - .excerpts - .cursor::, ExcerptOffset>>(()); - let mut excerpt_edits = Vec::>::new(); - - for locator in &locators { - let prefix = cursor.slice(&Some(locator), Bias::Left); - new_excerpts.append(prefix, ()); - - let mut excerpt = cursor.item().unwrap().clone(); - let old_text_len = excerpt.text_summary.len; - - let up_line_count = if direction.should_expand_up() { - line_count - } else { - 0 - }; - - let start_row = excerpt - .range - .context - .start - .to_point(&excerpt.buffer) - .row - .saturating_sub(up_line_count); - let start_point = Point::new(start_row, 0); - excerpt.range.context.start = excerpt.buffer.anchor_before(start_point); - - let down_line_count = if direction.should_expand_down() { - line_count - } else { - 0 - }; - - let mut end_point = excerpt.buffer.clip_point( - excerpt.range.context.end.to_point(&excerpt.buffer) - + Point::new(down_line_count, 0), - Bias::Left, - ); - end_point.column = excerpt.buffer.line_len(end_point.row); - excerpt.range.context.end = excerpt.buffer.anchor_after(end_point); - excerpt.max_buffer_row = end_point.row; - - excerpt.text_summary = excerpt - .buffer - .text_summary_for_range(excerpt.range.context.clone()); - - let new_start_offset = ExcerptDimension(new_excerpts.summary().text.len); - let old_start_offset = cursor.start().1; - let new_text_len = excerpt.text_summary.len; - let edit = Edit { - old: old_start_offset..old_start_offset + old_text_len, - new: new_start_offset..new_start_offset + new_text_len, - }; - - if let Some(last_edit) = excerpt_edits.last_mut() { - if last_edit.old.end == edit.old.start { - last_edit.old.end = edit.old.end; - last_edit.new.end = edit.new.end; - } else { - excerpt_edits.push(edit); - } - } else { - excerpt_edits.push(edit); - } - - new_excerpts.push(excerpt, ()); - - cursor.next(); - } - - new_excerpts.append(cursor.suffix(), ()); - - drop(cursor); - snapshot.excerpts = new_excerpts.clone(); - - let edits = Self::sync_diff_transforms( - &mut snapshot, - excerpt_edits.clone(), - DiffChangeKind::BufferEdited, - self.filter_mode, - ); - if !edits.is_empty() { - self.subscriptions.publish(edits); - } - if let Some(follower) = &self.follower { - follower.update(cx, |follower, cx| { - follower.expand_excerpts(ids.clone(), line_count, direction, cx); - }) - } - cx.emit(Event::Edited { - edited_buffer: None, - }); - cx.emit(Event::ExcerptsExpanded { ids }); - cx.notify(); - } - - #[ztracing::instrument(skip_all)] - fn sync(&self, cx: &App) { - let changed = self.buffer_changed_since_sync.replace(false); - if !changed { - return; - } - let edits = Self::sync_from_buffer_changes( - &mut self.snapshot.borrow_mut(), - &self.buffers, - &self.diffs, - self.filter_mode, - cx, - ); - if !edits.is_empty() { - self.subscriptions.publish(edits); - } - } - - fn sync_mut(&mut self, cx: &App) { - let changed = self.buffer_changed_since_sync.replace(false); - if !changed { - return; - } - let edits = Self::sync_from_buffer_changes( - self.snapshot.get_mut(), - &self.buffers, - &self.diffs, - self.filter_mode, - cx, - ); - - if !edits.is_empty() { - self.subscriptions.publish(edits); - } - } - - fn sync_from_buffer_changes( - snapshot: &mut MultiBufferSnapshot, - buffers: &HashMap, - diffs: &HashMap, - filter_mode: Option, - cx: &App, - ) -> Vec> { - let MultiBufferSnapshot { - excerpts, - diffs: buffer_diff, - diff_transforms: _, - non_text_state_update_count, - edit_count, - is_dirty, - has_deleted_file, - has_conflict, - singleton: _, - excerpt_ids: _, - replaced_excerpts: _, - trailing_excerpt_update_count: _, - all_diff_hunks_expanded: _, - show_headers: _, - } = snapshot; - *is_dirty = false; - *has_deleted_file = false; - *has_conflict = false; - - let mut excerpts_to_edit = Vec::new(); - let mut non_text_state_updated = false; - let mut edited = false; - for buffer_state in buffers.values() { - let buffer = buffer_state.buffer.read(cx); - let version = buffer.version(); - let non_text_state_update_count = buffer.non_text_state_update_count(); - - let buffer_edited = version.changed_since(&buffer_state.last_version.borrow()); - let buffer_non_text_state_updated = - non_text_state_update_count > buffer_state.last_non_text_state_update_count.get(); - if buffer_edited || buffer_non_text_state_updated { - *buffer_state.last_version.borrow_mut() = version; - buffer_state - .last_non_text_state_update_count - .set(non_text_state_update_count); - excerpts_to_edit.extend( - buffer_state - .excerpts - .iter() - .map(|locator| (locator, buffer_state.buffer.clone(), buffer_edited)), - ); - } - - edited |= buffer_edited; - non_text_state_updated |= buffer_non_text_state_updated; - *is_dirty |= buffer.is_dirty(); - *has_deleted_file |= buffer - .file() - .is_some_and(|file| file.disk_state() == DiskState::Deleted); - *has_conflict |= buffer.has_conflict(); - } - if edited { - *edit_count += 1; - } - if non_text_state_updated { - *non_text_state_update_count += 1; - } - - for (id, diff) in diffs.iter() { - if buffer_diff.get(id).is_none() { - buffer_diff.insert(*id, diff.diff.read(cx).snapshot(cx)); - } - } - - excerpts_to_edit.sort_unstable_by_key(|(locator, _, _)| *locator); - - let mut edits = Vec::new(); - let mut new_excerpts = SumTree::default(); - let mut cursor = excerpts.cursor::, ExcerptOffset>>(()); - - for (locator, buffer, buffer_edited) in excerpts_to_edit { - new_excerpts.append(cursor.slice(&Some(locator), Bias::Left), ()); - let old_excerpt = cursor.item().unwrap(); - let buffer = buffer.read(cx); - let buffer_id = buffer.remote_id(); - - let mut new_excerpt; - if buffer_edited { - edits.extend( - buffer - .edits_since_in_range::( - old_excerpt.buffer.version(), - old_excerpt.range.context.clone(), - ) - .map(|edit| { - let excerpt_old_start = cursor.start().1; - let excerpt_new_start = - ExcerptDimension(new_excerpts.summary().text.len); - let old_start = excerpt_old_start + edit.old.start; - let old_end = excerpt_old_start + edit.old.end; - let new_start = excerpt_new_start + edit.new.start; - let new_end = excerpt_new_start + edit.new.end; - Edit { - old: old_start..old_end, - new: new_start..new_end, - } - }), - ); - - new_excerpt = Excerpt::new( - old_excerpt.id, - locator.clone(), - buffer_id, - buffer.snapshot(), - old_excerpt.range.clone(), - old_excerpt.has_trailing_newline, - ); - } else { - new_excerpt = old_excerpt.clone(); - new_excerpt.buffer = buffer.snapshot(); - } - - new_excerpts.push(new_excerpt, ()); - cursor.next(); - } - new_excerpts.append(cursor.suffix(), ()); - - drop(cursor); - *excerpts = new_excerpts; - Self::sync_diff_transforms(snapshot, edits, DiffChangeKind::BufferEdited, filter_mode) - } - - fn sync_diff_transforms( - snapshot: &mut MultiBufferSnapshot, - excerpt_edits: Vec>, - change_kind: DiffChangeKind, - filter_mode: Option, - ) -> Vec> { - if excerpt_edits.is_empty() { - return vec![]; - } - - let mut excerpts = snapshot.excerpts.cursor::(()); - let mut old_diff_transforms = snapshot - .diff_transforms - .cursor::>(()); - let mut new_diff_transforms = SumTree::default(); - let mut old_expanded_hunks = HashSet::default(); - let mut output_edits = Vec::new(); - let mut output_delta = 0_isize; - let mut at_transform_boundary = true; - let mut end_of_current_insert = None; - - let mut excerpt_edits: VecDeque<_> = excerpt_edits.into_iter().collect(); - while let Some(edit) = excerpt_edits.pop_front() { - excerpts.seek_forward(&edit.new.start, Bias::Right); - if excerpts.item().is_none() && *excerpts.start() == edit.new.start { - excerpts.prev(); - } - - // Keep any transforms that are before the edit. - if at_transform_boundary { - at_transform_boundary = false; - let transforms_before_edit = old_diff_transforms.slice(&edit.old.start, Bias::Left); - Self::append_diff_transforms(&mut new_diff_transforms, transforms_before_edit); - if let Some(transform) = old_diff_transforms.item() - && old_diff_transforms.end().0 == edit.old.start - && old_diff_transforms.start().0 < edit.old.start - { - Self::push_diff_transform(&mut new_diff_transforms, transform.clone()); - old_diff_transforms.next(); - } - } - - // Compute the start of the edit in output coordinates. - let edit_start_overshoot = if let Some(DiffTransform::FilteredInsertedHunk { .. }) = - old_diff_transforms.item() - { - 0 - } else { - edit.old.start - old_diff_transforms.start().0 - }; - let edit_old_start = old_diff_transforms.start().1 + edit_start_overshoot; - let edit_new_start = - MultiBufferOffset((edit_old_start.0 as isize + output_delta) as usize); - - let changed_diff_hunks = Self::recompute_diff_transforms_for_edit( - &edit, - &mut excerpts, - &mut old_diff_transforms, - &mut new_diff_transforms, - &mut end_of_current_insert, - &mut old_expanded_hunks, - snapshot, - change_kind, - filter_mode, - ); - - // When the added range of a hunk is edited, the end anchor of the hunk may be moved later - // in response by hunks_intersecting_range to keep it at a row boundary. In KeepDeletions - // mode, we need to make sure that the whole added range is still filtered out in this situation. - // We do that by adding an additional edit that covers the rest of the hunk added range. - if let Some(current_inserted_hunk) = &end_of_current_insert - && current_inserted_hunk.is_filtered - // No additional edit needed if we've already covered the whole added range. - && current_inserted_hunk.insertion_end_offset > edit.new.end - // No additional edit needed if this edit just touched the start of the hunk - // (this also prevents pushing the deleted region for the hunk twice). - && edit.new.end > current_inserted_hunk.hunk_excerpt_start - // No additional edit needed if there is a subsequent edit that intersects - // the same hunk (the last such edit will take care of it). - && excerpt_edits.front().is_none_or(|next_edit| { - next_edit.new.start >= current_inserted_hunk.insertion_end_offset - }) - { - let overshoot = current_inserted_hunk.insertion_end_offset - edit.new.end; - let additional_edit = Edit { - old: edit.old.end..edit.old.end + overshoot, - new: edit.new.end..current_inserted_hunk.insertion_end_offset, - }; - excerpt_edits.push_front(additional_edit); - } - - // Compute the end of the edit in output coordinates. - let edit_old_end_overshoot = if let Some(DiffTransform::FilteredInsertedHunk { - .. - }) = old_diff_transforms.item() - { - ExcerptDimension(MultiBufferOffset(0)) - } else { - ExcerptDimension(MultiBufferOffset( - edit.old.end - old_diff_transforms.start().0, - )) - }; - let edit_new_end_overshoot = if let Some(current_inserted_hunk) = &end_of_current_insert - && current_inserted_hunk.is_filtered - { - let insertion_end_offset = current_inserted_hunk.insertion_end_offset; - let excerpt_len = new_diff_transforms.summary().excerpt_len(); - let base = insertion_end_offset.max(excerpt_len); - edit.new.end.saturating_sub(base) - } else { - edit.new.end - new_diff_transforms.summary().excerpt_len() - }; - let edit_old_end = old_diff_transforms.start().1 + edit_old_end_overshoot.0; - let edit_new_end = new_diff_transforms.summary().output.len + edit_new_end_overshoot; - let output_edit = Edit { - old: edit_old_start..edit_old_end, - new: edit_new_start..edit_new_end, - }; - - output_delta += (output_edit.new.end - output_edit.new.start) as isize; - output_delta -= (output_edit.old.end - output_edit.old.start) as isize; - if changed_diff_hunks || matches!(change_kind, DiffChangeKind::BufferEdited) { - output_edits.push(output_edit); - } - - // If this is the last edit that intersects the current diff transform, - // then recreate the content up to the end of this transform, to prepare - // for reusing additional slices of the old transforms. - if excerpt_edits - .front() - .is_none_or(|next_edit| next_edit.old.start >= old_diff_transforms.end().0) - { - let keep_next_old_transform = (old_diff_transforms.start().0 >= edit.old.end) - && match old_diff_transforms.item() { - Some( - DiffTransform::InsertedHunk { hunk_info, .. } - | DiffTransform::FilteredInsertedHunk { hunk_info, .. }, - ) => excerpts.item().is_some_and(|excerpt| { - hunk_info.hunk_start_anchor.is_valid(&excerpt.buffer) - }), - _ => true, - }; - - let mut excerpt_offset = edit.new.end; - if !keep_next_old_transform { - excerpt_offset += old_diff_transforms.end().0 - edit.old.end; - old_diff_transforms.next(); - } - - old_expanded_hunks.clear(); - Self::push_buffer_content_transform( - snapshot, - &mut new_diff_transforms, - excerpt_offset, - end_of_current_insert.as_ref(), - ); - at_transform_boundary = true; - } - } - - // Keep any transforms that are after the last edit. - Self::append_diff_transforms(&mut new_diff_transforms, old_diff_transforms.suffix()); - - // Ensure there's always at least one buffer content transform. - if new_diff_transforms.is_empty() { - new_diff_transforms.push( - DiffTransform::Unmodified { - summary: Default::default(), - }, - (), - ); - } - - drop(old_diff_transforms); - drop(excerpts); - snapshot.diff_transforms = new_diff_transforms; - snapshot.edit_count += 1; - - #[cfg(any(test, feature = "test-support"))] - snapshot.check_invariants(); - output_edits - } - - fn recompute_diff_transforms_for_edit( - edit: &Edit, - excerpts: &mut Cursor, - old_diff_transforms: &mut Cursor< - DiffTransform, - Dimensions, - >, - new_diff_transforms: &mut SumTree, - end_of_current_insert: &mut Option, - old_expanded_hunks: &mut HashSet, - snapshot: &MultiBufferSnapshot, - change_kind: DiffChangeKind, - filter_mode: Option, - ) -> bool { - log::trace!( - "recomputing diff transform for edit {:?} => {:?}", - edit.old.start..edit.old.end, - edit.new.start..edit.new.end - ); - - // Record which hunks were previously expanded. - while let Some(item) = old_diff_transforms.item() { - if let Some(hunk_info) = item.hunk_info() { - log::trace!( - "previously expanded hunk at {:?}", - old_diff_transforms.start() - ); - old_expanded_hunks.insert(hunk_info); - } - if old_diff_transforms.end().0 > edit.old.end { - break; - } - old_diff_transforms.next(); - } - - // Avoid querying diff hunks if there's no possibility of hunks being expanded. - let all_diff_hunks_expanded = snapshot.all_diff_hunks_expanded; - if old_expanded_hunks.is_empty() - && change_kind == DiffChangeKind::BufferEdited - && !all_diff_hunks_expanded - { - return false; - } - - // Visit each excerpt that intersects the edit. - let mut did_expand_hunks = false; - while let Some(excerpt) = excerpts.item() { - // Recompute the expanded hunks in the portion of the excerpt that - // intersects the edit. - if let Some(diff) = snapshot.diffs.get(&excerpt.buffer_id) { - let buffer = &excerpt.buffer; - let excerpt_start = *excerpts.start(); - let excerpt_end = excerpt_start + excerpt.text_summary.len; - let excerpt_buffer_start = excerpt.range.context.start.to_offset(buffer); - let excerpt_buffer_end = excerpt_buffer_start + excerpt.text_summary.len; - let edit_buffer_start = - excerpt_buffer_start + edit.new.start.saturating_sub(excerpt_start); - let edit_buffer_end = - excerpt_buffer_start + edit.new.end.saturating_sub(excerpt_start); - let edit_buffer_end = edit_buffer_end.min(excerpt_buffer_end); - let edit_anchor_range = - buffer.anchor_before(edit_buffer_start)..buffer.anchor_after(edit_buffer_end); - - for hunk in diff.hunks_intersecting_range(edit_anchor_range, buffer) { - if hunk.is_created_file() && !all_diff_hunks_expanded { - continue; - } - - let hunk_buffer_range = hunk.buffer_range.to_offset(buffer); - if hunk_buffer_range.start < excerpt_buffer_start { - log::trace!("skipping hunk that starts before excerpt"); - continue; - } - - let hunk_info = DiffTransformHunkInfo { - excerpt_id: excerpt.id, - hunk_start_anchor: hunk.buffer_range.start, - hunk_secondary_status: hunk.secondary_status, - base_text_byte_range: hunk.diff_base_byte_range.clone(), - }; - - let hunk_excerpt_start = excerpt_start - + hunk_buffer_range.start.saturating_sub(excerpt_buffer_start); - let hunk_excerpt_end = excerpt_end - .min(excerpt_start + (hunk_buffer_range.end - excerpt_buffer_start)); - - Self::push_buffer_content_transform( - snapshot, - new_diff_transforms, - hunk_excerpt_start, - end_of_current_insert.as_ref(), - ); - - // For every existing hunk, determine if it was previously expanded - // and if it should currently be expanded. - let was_previously_expanded = old_expanded_hunks.contains(&hunk_info); - let should_expand_hunk = match &change_kind { - DiffChangeKind::DiffUpdated { base_changed: true } => { - was_previously_expanded || all_diff_hunks_expanded - } - DiffChangeKind::ExpandOrCollapseHunks { expand } => { - let intersects = hunk_buffer_range.is_empty() - || hunk_buffer_range.end > edit_buffer_start; - if *expand { - intersects || was_previously_expanded || all_diff_hunks_expanded - } else { - !intersects && (was_previously_expanded || all_diff_hunks_expanded) - } - } - _ => was_previously_expanded || all_diff_hunks_expanded, - }; - - if should_expand_hunk { - did_expand_hunks = true; - log::trace!( - "expanding hunk {:?}, excerpt:{:?}", - hunk_excerpt_start..hunk_excerpt_end, - excerpt.id - ); - - if !hunk.diff_base_byte_range.is_empty() - && hunk_buffer_range.start >= edit_buffer_start - && hunk_buffer_range.start <= excerpt_buffer_end - && filter_mode != Some(MultiBufferFilterMode::KeepInsertions) - { - let base_text = diff.base_text(); - let mut text_cursor = - base_text.as_rope().cursor(hunk.diff_base_byte_range.start); - let mut base_text_summary = - text_cursor.summary::(hunk.diff_base_byte_range.end); - - let mut has_trailing_newline = false; - if base_text_summary.last_line_chars > 0 { - base_text_summary += TextSummary::newline(); - has_trailing_newline = true; - } - - new_diff_transforms.push( - DiffTransform::DeletedHunk { - summary: base_text_summary, - buffer_id: excerpt.buffer_id, - hunk_info: hunk_info.clone(), - has_trailing_newline, - }, - (), - ); - } - - if !hunk_buffer_range.is_empty() { - let is_filtered = - filter_mode == Some(MultiBufferFilterMode::KeepDeletions); - let insertion_end_offset = hunk_excerpt_end.min(excerpt_end); - *end_of_current_insert = Some(CurrentInsertedHunk { - hunk_excerpt_start, - insertion_end_offset, - hunk_info, - is_filtered, - }); - } - } - } - } - - if excerpts.end() <= edit.new.end { - excerpts.next(); - } else { - break; - } - } - - did_expand_hunks || !old_expanded_hunks.is_empty() - } - - fn append_diff_transforms( - new_transforms: &mut SumTree, - subtree: SumTree, - ) { - if let Some(transform) = subtree.first() - && Self::extend_last_buffer_content_transform(new_transforms, transform) - { - let mut cursor = subtree.cursor::<()>(()); - cursor.next(); - cursor.next(); - new_transforms.append(cursor.suffix(), ()); - return; - } - new_transforms.append(subtree, ()); - } - - fn push_diff_transform(new_transforms: &mut SumTree, transform: DiffTransform) { - if Self::extend_last_buffer_content_transform(new_transforms, &transform) { - return; - } - new_transforms.push(transform, ()); - } - - fn push_buffer_content_transform( - old_snapshot: &MultiBufferSnapshot, - new_transforms: &mut SumTree, - end_offset: ExcerptOffset, - current_inserted_hunk: Option<&CurrentInsertedHunk>, - ) { - if let Some(current_inserted_hunk) = current_inserted_hunk { - let start_offset = new_transforms.summary().excerpt_len(); - let end_offset = current_inserted_hunk.insertion_end_offset.min(end_offset); - if end_offset > start_offset { - let summary_to_add = old_snapshot - .text_summary_for_excerpt_offset_range::( - start_offset..end_offset, - ); - - let transform = if current_inserted_hunk.is_filtered { - DiffTransform::FilteredInsertedHunk { - summary: summary_to_add, - hunk_info: current_inserted_hunk.hunk_info.clone(), - } - } else { - DiffTransform::InsertedHunk { - summary: summary_to_add, - hunk_info: current_inserted_hunk.hunk_info.clone(), - } - }; - if !Self::extend_last_buffer_content_transform(new_transforms, &transform) { - new_transforms.push(transform, ()) - } - } - } - - let start_offset = new_transforms.summary().excerpt_len(); - if end_offset > start_offset { - let summary_to_add = old_snapshot - .text_summary_for_excerpt_offset_range::(start_offset..end_offset); - - let transform = DiffTransform::Unmodified { - summary: summary_to_add, - }; - if !Self::extend_last_buffer_content_transform(new_transforms, &transform) { - new_transforms.push(transform, ()) - } - } - } - - fn extend_last_buffer_content_transform( - new_transforms: &mut SumTree, - transform: &DiffTransform, - ) -> bool { - let mut did_extend = false; - new_transforms.update_last( - |last_transform| { - did_extend = last_transform.merge_with(&transform); - }, - (), - ); - did_extend - } -} - -impl DiffTransform { - /// Ergonomic wrapper for [`DiffTransform::merged_with`] that applies the - /// merging in-place. Returns `true` if merging was possible. - #[must_use = "check whether merging actually succeeded"] - fn merge_with(&mut self, other: &Self) -> bool { - match self.to_owned().merged_with(other) { - Some(merged) => { - *self = merged; - true - } - None => false, - } - } - - /// Attempt to merge `self` with `other`, and return the merged transform. - /// - /// This will succeed if all of the following are true: - /// - both transforms are the same variant - /// - neither transform is [`DiffTransform::DeletedHunk`] - /// - if both transform are either [`DiffTransform::InsertedHunk`] or - /// [`DiffTransform::FilteredInsertedHunk`], then their - /// `hunk_info.hunk_start_anchor`s match - #[must_use = "check whether merging actually succeeded"] - #[rustfmt::skip] - fn merged_with(self, other: &Self) -> Option { - match (self, other) { - ( - DiffTransform::Unmodified { mut summary }, - DiffTransform::Unmodified { summary: other_summary }, - ) => { - summary += *other_summary; - Some(DiffTransform::Unmodified { summary }) - } - ( - DiffTransform::FilteredInsertedHunk { mut summary, hunk_info }, - DiffTransform::FilteredInsertedHunk { - hunk_info: other_hunk_info, - summary: other_summary, - }, - ) => { - if hunk_info.hunk_start_anchor == other_hunk_info.hunk_start_anchor { - summary += *other_summary; - Some(DiffTransform::FilteredInsertedHunk { summary, hunk_info }) - } else { - None - } - } - ( - DiffTransform::InsertedHunk { mut summary, hunk_info }, - DiffTransform::InsertedHunk { - hunk_info: other_hunk_info, - summary: other_summary, - }, - ) => { - if hunk_info.hunk_start_anchor == other_hunk_info.hunk_start_anchor { - summary += *other_summary; - Some(DiffTransform::InsertedHunk { summary, hunk_info }) - } else { - None - } - } - _ => return None, - } - } -} - -fn build_excerpt_ranges( - ranges: impl IntoIterator>, - context_line_count: u32, - buffer_snapshot: &BufferSnapshot, -) -> Vec> { - ranges - .into_iter() - .map(|range| { - let start_row = range.start.row.saturating_sub(context_line_count); - let start = Point::new(start_row, 0); - let end_row = (range.end.row + context_line_count).min(buffer_snapshot.max_point().row); - let end = Point::new(end_row, buffer_snapshot.line_len(end_row)); - ExcerptRange { - context: start..end, - primary: range, - } - }) - .collect() -} - -#[cfg(any(test, feature = "test-support"))] -impl MultiBuffer { - pub fn build_simple(text: &str, cx: &mut gpui::App) -> Entity { - let buffer = cx.new(|cx| Buffer::local(text, cx)); - cx.new(|cx| Self::singleton(buffer, cx)) - } - - pub fn build_multi( - excerpts: [(&str, Vec>); COUNT], - cx: &mut gpui::App, - ) -> Entity { - let multi = cx.new(|_| Self::new(Capability::ReadWrite)); - for (text, ranges) in excerpts { - let buffer = cx.new(|cx| Buffer::local(text, cx)); - let excerpt_ranges = ranges.into_iter().map(ExcerptRange::new); - multi.update(cx, |multi, cx| { - multi.push_excerpts(buffer, excerpt_ranges, cx) - }); - } - - multi - } - - pub fn build_from_buffer(buffer: Entity, cx: &mut gpui::App) -> Entity { - cx.new(|cx| Self::singleton(buffer, cx)) - } - - pub fn build_random(rng: &mut impl rand::Rng, cx: &mut gpui::App) -> Entity { - cx.new(|cx| { - let mut multibuffer = MultiBuffer::new(Capability::ReadWrite); - let mutation_count = rng.random_range(1..=5); - multibuffer.randomly_edit_excerpts(rng, mutation_count, cx); - multibuffer - }) - } - - pub fn randomly_edit( - &mut self, - rng: &mut impl rand::Rng, - edit_count: usize, - cx: &mut Context, - ) { - use util::RandomCharIter; - - let snapshot = self.read(cx); - let mut edits: Vec<(Range, Arc)> = Vec::new(); - let mut last_end = None; - for _ in 0..edit_count { - if last_end.is_some_and(|last_end| last_end >= snapshot.len()) { - break; - } - - let new_start = last_end.map_or(MultiBufferOffset::ZERO, |last_end| last_end + 1usize); - let end = - snapshot.clip_offset(rng.random_range(new_start..=snapshot.len()), Bias::Right); - let start = snapshot.clip_offset(rng.random_range(new_start..=end), Bias::Right); - last_end = Some(end); - - let mut range = start..end; - if rng.random_bool(0.2) { - mem::swap(&mut range.start, &mut range.end); - } - - let new_text_len = rng.random_range(0..10); - let new_text: String = RandomCharIter::new(&mut *rng).take(new_text_len).collect(); - - edits.push((range, new_text.into())); - } - log::info!("mutating multi-buffer with {:?}", edits); - drop(snapshot); - - self.edit(edits, None, cx); - } - - pub fn randomly_edit_excerpts( - &mut self, - rng: &mut impl rand::Rng, - mutation_count: usize, - cx: &mut Context, - ) { - use rand::prelude::*; - use std::env; - use util::RandomCharIter; - - let max_excerpts = env::var("MAX_EXCERPTS") - .map(|i| i.parse().expect("invalid `MAX_EXCERPTS` variable")) - .unwrap_or(5); - - let mut buffers = Vec::new(); - for _ in 0..mutation_count { - if rng.random_bool(0.05) { - log::info!("Clearing multi-buffer"); - self.clear(cx); - continue; - } else if rng.random_bool(0.1) && !self.excerpt_ids().is_empty() { - let ids = self.excerpt_ids(); - let mut excerpts = HashSet::default(); - for _ in 0..rng.random_range(0..ids.len()) { - excerpts.extend(ids.choose(rng).copied()); - } - - let line_count = rng.random_range(0..5); - - log::info!("Expanding excerpts {excerpts:?} by {line_count} lines"); - - self.expand_excerpts( - excerpts.iter().cloned(), - line_count, - ExpandExcerptDirection::UpAndDown, - cx, - ); - continue; - } - - let excerpt_ids = self.excerpt_ids(); - if excerpt_ids.is_empty() || (rng.random() && excerpt_ids.len() < max_excerpts) { - let buffer_handle = if rng.random() || self.buffers.is_empty() { - let text = RandomCharIter::new(&mut *rng).take(10).collect::(); - buffers.push(cx.new(|cx| Buffer::local(text, cx))); - let buffer = buffers.last().unwrap().read(cx); - log::info!( - "Creating new buffer {} with text: {:?}", - buffer.remote_id(), - buffer.text() - ); - buffers.last().unwrap().clone() - } else { - self.buffers.values().choose(rng).unwrap().buffer.clone() - }; - - let buffer = buffer_handle.read(cx); - let buffer_text = buffer.text(); - let ranges = (0..rng.random_range(0..5)) - .map(|_| { - let end_ix = - buffer.clip_offset(rng.random_range(0..=buffer.len()), Bias::Right); - let start_ix = buffer.clip_offset(rng.random_range(0..=end_ix), Bias::Left); - ExcerptRange::new(start_ix..end_ix) - }) - .collect::>(); - log::info!( - "Inserting excerpts from buffer {} and ranges {:?}: {:?}", - buffer_handle.read(cx).remote_id(), - ranges.iter().map(|r| &r.context).collect::>(), - ranges - .iter() - .map(|r| &buffer_text[r.context.clone()]) - .collect::>() - ); - - let excerpt_id = self.push_excerpts(buffer_handle.clone(), ranges, cx); - log::info!("Inserted with ids: {:?}", excerpt_id); - } else { - let remove_count = rng.random_range(1..=excerpt_ids.len()); - let mut excerpts_to_remove = excerpt_ids - .choose_multiple(rng, remove_count) - .cloned() - .collect::>(); - let snapshot = self.snapshot.borrow(); - excerpts_to_remove.sort_unstable_by(|a, b| a.cmp(b, &snapshot)); - drop(snapshot); - log::info!("Removing excerpts {:?}", excerpts_to_remove); - self.remove_excerpts(excerpts_to_remove, cx); - } - } - } - - pub fn randomly_mutate( - &mut self, - rng: &mut impl rand::Rng, - mutation_count: usize, - cx: &mut Context, - ) { - use rand::prelude::*; - - if rng.random_bool(0.7) || self.singleton { - let buffer = self - .buffers - .values() - .choose(rng) - .map(|state| state.buffer.clone()); - - if let Some(buffer) = buffer { - buffer.update(cx, |buffer, cx| { - if rng.random() { - buffer.randomly_edit(rng, mutation_count, cx); - } else { - buffer.randomly_undo_redo(rng, cx); - } - }); - } else { - self.randomly_edit(rng, mutation_count, cx); - } - } else { - self.randomly_edit_excerpts(rng, mutation_count, cx); - } - - self.check_invariants(cx); - } - - fn check_invariants(&self, cx: &App) { - self.read(cx).check_invariants(); - } -} - -impl EventEmitter for MultiBuffer {} - -impl MultiBufferSnapshot { - pub fn text(&self) -> String { - self.chunks(MultiBufferOffset::ZERO..self.len(), false) - .map(|chunk| chunk.text) - .collect() - } - - pub fn reversed_chars_at(&self, position: T) -> impl Iterator + '_ { - self.reversed_chunks_in_range(MultiBufferOffset::ZERO..position.to_offset(self)) - .flat_map(|c| c.chars().rev()) - } - - fn reversed_chunks_in_range( - &self, - range: Range, - ) -> ReversedMultiBufferChunks<'_> { - let mut cursor = self.cursor::(); - cursor.seek(&range.end); - let current_chunks = cursor.region().as_ref().map(|region| { - let start_overshoot = range.start.saturating_sub(region.range.start); - let end_overshoot = range.end - region.range.start; - let end = (region.buffer_range.start + end_overshoot).min(region.buffer_range.end); - let start = region.buffer_range.start + start_overshoot; - region.buffer.reversed_chunks_in_range(start..end) - }); - ReversedMultiBufferChunks { - cursor, - current_chunks, - start: range.start, - offset: range.end, - } - } - - pub fn chars_at(&self, position: T) -> impl Iterator + '_ { - let offset = position.to_offset(self); - self.text_for_range(offset..self.len()) - .flat_map(|chunk| chunk.chars()) - } - - pub fn text_for_range(&self, range: Range) -> impl Iterator + '_ { - self.chunks(range, false).map(|chunk| chunk.text) - } - - pub fn is_line_blank(&self, row: MultiBufferRow) -> bool { - self.text_for_range(Point::new(row.0, 0)..Point::new(row.0, self.line_len(row))) - .all(|chunk| chunk.matches(|c: char| !c.is_whitespace()).next().is_none()) - } - - pub fn contains_str_at(&self, position: T, needle: &str) -> bool - where - T: ToOffset, - { - let position = position.to_offset(self); - position == self.clip_offset(position, Bias::Left) - && self - .bytes_in_range(position..self.len()) - .flatten() - .copied() - .take(needle.len()) - .eq(needle.bytes()) - } - - pub fn diff_hunks(&self) -> impl Iterator + '_ { - self.diff_hunks_in_range(Anchor::min()..Anchor::max()) - } - - pub fn diff_hunks_in_range( - &self, - range: Range, - ) -> impl Iterator + '_ { - let query_range = range.start.to_point(self)..range.end.to_point(self); - self.lift_buffer_metadata(query_range.clone(), move |buffer, buffer_range| { - let diff = self.diffs.get(&buffer.remote_id())?; - let buffer_start = buffer.anchor_before(buffer_range.start); - let buffer_end = buffer.anchor_after(buffer_range.end); - Some( - diff.hunks_intersecting_range(buffer_start..buffer_end, buffer) - .filter_map(|hunk| { - if hunk.is_created_file() && !self.all_diff_hunks_expanded { - return None; - } - Some((hunk.range.clone(), hunk)) - }), - ) - }) - .filter_map(move |(range, hunk, excerpt)| { - if range.start != range.end && range.end == query_range.start && !hunk.range.is_empty() - { - return None; - } - let end_row = if range.end.column == 0 { - range.end.row - } else { - range.end.row + 1 - }; - - let word_diffs = (!hunk.base_word_diffs.is_empty() - || !hunk.buffer_word_diffs.is_empty()) - .then(|| { - let hunk_start_offset = - Anchor::in_buffer(excerpt.id, hunk.buffer_range.start).to_offset(self); - - hunk.base_word_diffs - .iter() - .map(|diff| hunk_start_offset + diff.start..hunk_start_offset + diff.end) - .chain( - hunk.buffer_word_diffs - .into_iter() - .map(|diff| Anchor::range_in_buffer(excerpt.id, diff).to_offset(self)), - ) - .collect() - }) - .unwrap_or_default(); - - Some(MultiBufferDiffHunk { - row_range: MultiBufferRow(range.start.row)..MultiBufferRow(end_row), - buffer_id: excerpt.buffer_id, - excerpt_id: excerpt.id, - buffer_range: hunk.buffer_range.clone(), - word_diffs, - diff_base_byte_range: BufferOffset(hunk.diff_base_byte_range.start) - ..BufferOffset(hunk.diff_base_byte_range.end), - secondary_status: hunk.secondary_status, - }) - }) - } - - fn excerpts_for_range( - &self, - range: Range, - ) -> impl Iterator + '_ { - let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut cursor = self.cursor::(); - cursor.seek(&range.start); - std::iter::from_fn(move || { - let region = cursor.region()?; - if region.range.start > range.end - || region.range.start == range.end && region.range.start > range.start - { - return None; - } - cursor.next_excerpt(); - Some(region.excerpt) - }) - } - - pub fn excerpt_ids_for_range( - &self, - range: Range, - ) -> impl Iterator + '_ { - self.excerpts_for_range(range).map(|excerpt| excerpt.id) - } - - pub fn buffer_ids_for_range( - &self, - range: Range, - ) -> impl Iterator + '_ { - self.excerpts_for_range(range) - .map(|excerpt| excerpt.buffer_id) - } - - pub fn ranges_to_buffer_ranges( - &self, - ranges: impl Iterator>, - ) -> impl Iterator, ExcerptId)> { - ranges.flat_map(|range| self.range_to_buffer_ranges(range).into_iter()) - } - - pub fn range_to_buffer_ranges( - &self, - range: Range, - ) -> Vec<(&BufferSnapshot, Range, ExcerptId)> { - let start = range.start.to_offset(self); - let end = range.end.to_offset(self); - - let mut cursor = self.cursor::(); - cursor.seek(&start); - - let mut result: Vec<(&BufferSnapshot, Range, ExcerptId)> = Vec::new(); - while let Some(region) = cursor.region() { - if region.range.start > end { - break; - } - if region.is_main_buffer { - let start_overshoot = start.saturating_sub(region.range.start); - let end_overshoot = end.saturating_sub(region.range.start); - let start = region - .buffer_range - .end - .min(region.buffer_range.start + start_overshoot); - let end = region - .buffer_range - .end - .min(region.buffer_range.start + end_overshoot); - if let Some(prev) = result.last_mut().filter(|(_, prev_range, excerpt_id)| { - *excerpt_id == region.excerpt.id && prev_range.end == start - }) { - prev.1.end = end; - } else { - result.push((region.buffer, start..end, region.excerpt.id)); - } - } - cursor.next(); - } - result - } - - pub fn range_to_buffer_ranges_with_deleted_hunks( - &self, - range: Range, - ) -> impl Iterator< - Item = ( - &BufferSnapshot, - Range, - ExcerptId, - Option, - ), - > + '_ { - let start = range.start.to_offset(self); - let end = range.end.to_offset(self); - - let mut cursor = self.cursor::(); - cursor.seek(&start); - - std::iter::from_fn(move || { - let region = cursor.region()?; - if region.range.start > end { - return None; - } - let start_overshoot = start.saturating_sub(region.range.start); - let end_overshoot = end.saturating_sub(region.range.start); - let start = region - .buffer_range - .end - .min(region.buffer_range.start + start_overshoot); - let end = region - .buffer_range - .end - .min(region.buffer_range.start + end_overshoot); - - let region_excerpt_id = region.excerpt.id; - let deleted_hunk_anchor = if region.is_main_buffer { - None - } else { - Some(self.anchor_before(region.range.start)) - }; - let result = ( - region.buffer, - start..end, - region_excerpt_id, - deleted_hunk_anchor, - ); - cursor.next(); - Some(result) - }) - } - - /// Retrieves buffer metadata for the given range, and converts it into multi-buffer - /// coordinates. - /// - /// The given callback will be called for every excerpt intersecting the given range. It will - /// be passed the excerpt's buffer and the buffer range that the input range intersects. - /// The callback should return an iterator of metadata items from that buffer, each paired - /// with a buffer range. - /// - /// The returned iterator yields each of these metadata items, paired with its range in - /// multi-buffer coordinates. - fn lift_buffer_metadata<'a, MBD, M, I>( - &'a self, - query_range: Range, - get_buffer_metadata: impl 'a + Fn(&'a BufferSnapshot, Range) -> Option, - ) -> impl Iterator, M, &'a Excerpt)> + 'a - where - I: Iterator, M)> + 'a, - MBD: MultiBufferDimension - + Ord - + Sub - + ops::Add - + ops::AddAssign, - MBD::TextDimension: Sub - + ops::Add - + AddAssign - + Ord, - { - let mut current_excerpt_metadata: Option<(ExcerptId, I)> = None; - let mut cursor = self.cursor::(); - - // Find the excerpt and buffer offset where the given range ends. - cursor.seek(&query_range.end); - let mut range_end = None; - while let Some(region) = cursor.region() { - if region.is_main_buffer { - let mut buffer_end = region.buffer_range.start; - let overshoot = if query_range.end > region.range.start { - query_range.end - region.range.start - } else { - ::default() - }; - buffer_end = buffer_end + overshoot; - range_end = Some((region.excerpt.id, buffer_end)); - break; - } - cursor.next(); - } - - cursor.seek(&query_range.start); - - if let Some(region) = cursor.region().filter(|region| !region.is_main_buffer) - && region.range.start > MBD::default() - { - cursor.prev() - } - - iter::from_fn(move || { - loop { - let excerpt = cursor.excerpt()?; - - // If we have already retrieved metadata for this excerpt, continue to use it. - let metadata_iter = if let Some((_, metadata)) = current_excerpt_metadata - .as_mut() - .filter(|(excerpt_id, _)| *excerpt_id == excerpt.id) - { - Some(metadata) - } - // Otherwise, compute the intersection of the input range with the excerpt's range, - // and retrieve the metadata for the resulting range. - else { - let region = cursor.region()?; - let mut buffer_start; - if region.is_main_buffer { - buffer_start = region.buffer_range.start; - if query_range.start > region.range.start { - let overshoot = query_range.start - region.range.start; - buffer_start = buffer_start + overshoot; - } - buffer_start = buffer_start.min(region.buffer_range.end); - } else { - buffer_start = cursor.main_buffer_position()?; - }; - let mut buffer_end = excerpt - .range - .context - .end - .summary::(&excerpt.buffer); - if let Some((end_excerpt_id, end_buffer_offset)) = range_end - && excerpt.id == end_excerpt_id - { - buffer_end = buffer_end.min(end_buffer_offset); - } - - get_buffer_metadata(&excerpt.buffer, buffer_start..buffer_end).map(|iterator| { - &mut current_excerpt_metadata.insert((excerpt.id, iterator)).1 - }) - }; - - // Visit each metadata item. - if let Some((metadata_buffer_range, metadata)) = - metadata_iter.and_then(Iterator::next) - { - // Find the multibuffer regions that contain the start and end of - // the metadata item's range. - if metadata_buffer_range.start > ::default() { - while let Some(region) = cursor.region() { - if region.is_main_buffer - && (region.buffer_range.end >= metadata_buffer_range.start - || cursor.is_at_end_of_excerpt()) - { - break; - } - cursor.next(); - } - } - let start_region = cursor.region()?; - while let Some(region) = cursor.region() { - if region.is_main_buffer - && (region.buffer_range.end > metadata_buffer_range.end - || cursor.is_at_end_of_excerpt()) - { - break; - } - cursor.next(); - } - let end_region = cursor.region(); - - // Convert the metadata item's range into multibuffer coordinates. - let mut start_position = start_region.range.start; - let region_buffer_start = start_region.buffer_range.start; - if start_region.is_main_buffer - && metadata_buffer_range.start > region_buffer_start - { - start_position = - start_position + (metadata_buffer_range.start - region_buffer_start); - start_position = start_position.min(start_region.range.end); - } - - let mut end_position = self.max_position(); - if let Some(end_region) = &end_region { - end_position = end_region.range.start; - debug_assert!(end_region.is_main_buffer); - let region_buffer_start = end_region.buffer_range.start; - if metadata_buffer_range.end > region_buffer_start { - end_position = - end_position + (metadata_buffer_range.end - region_buffer_start); - } - end_position = end_position.min(end_region.range.end); - } - - if start_position <= query_range.end && end_position >= query_range.start { - return Some((start_position..end_position, metadata, excerpt)); - } - } - // When there are no more metadata items for this excerpt, move to the next excerpt. - else { - current_excerpt_metadata.take(); - if let Some((end_excerpt_id, _)) = range_end - && excerpt.id == end_excerpt_id - { - return None; - } - cursor.next_excerpt(); - } - } - }) - } - - pub fn diff_hunk_before(&self, position: T) -> Option { - let offset = position.to_offset(self); - - let mut cursor = self - .cursor::, DimensionPair>( - ); - cursor.seek(&DimensionPair { - key: offset, - value: None, - }); - cursor.seek_to_start_of_current_excerpt(); - let excerpt = cursor.excerpt()?; - - let excerpt_end = excerpt.range.context.end.to_offset(&excerpt.buffer); - let current_position = self - .anchor_before(offset) - .text_anchor - .to_offset(&excerpt.buffer); - let excerpt_end = excerpt - .buffer - .anchor_before(excerpt_end.min(current_position)); - - if let Some(diff) = self.diffs.get(&excerpt.buffer_id) { - for hunk in diff.hunks_intersecting_range_rev( - excerpt.range.context.start..excerpt_end, - &excerpt.buffer, - ) { - let hunk_end = hunk.buffer_range.end.to_offset(&excerpt.buffer); - if hunk_end >= current_position { - continue; - } - let start = Anchor::in_buffer(excerpt.id, hunk.buffer_range.start).to_point(self); - return Some(MultiBufferRow(start.row)); - } - } - - loop { - cursor.prev_excerpt(); - let excerpt = cursor.excerpt()?; - - let Some(diff) = self.diffs.get(&excerpt.buffer_id) else { - continue; - }; - let mut hunks = - diff.hunks_intersecting_range_rev(excerpt.range.context.clone(), &excerpt.buffer); - let Some(hunk) = hunks.next() else { - continue; - }; - let start = Anchor::in_buffer(excerpt.id, hunk.buffer_range.start).to_point(self); - return Some(MultiBufferRow(start.row)); - } - } - - pub fn has_diff_hunks(&self) -> bool { - self.diffs.values().any(|diff| !diff.is_empty()) - } - - pub fn is_inside_word( - &self, - position: T, - scope_context: Option, - ) -> bool { - let position = position.to_offset(self); - let classifier = self - .char_classifier_at(position) - .scope_context(scope_context); - let next_char_kind = self.chars_at(position).next().map(|c| classifier.kind(c)); - let prev_char_kind = self - .reversed_chars_at(position) - .next() - .map(|c| classifier.kind(c)); - prev_char_kind.zip(next_char_kind) == Some((CharKind::Word, CharKind::Word)) - } - - pub fn surrounding_word( - &self, - start: T, - scope_context: Option, - ) -> (Range, Option) { - let mut start = start.to_offset(self); - let mut end = start; - let mut next_chars = self.chars_at(start).peekable(); - let mut prev_chars = self.reversed_chars_at(start).peekable(); - - let classifier = self.char_classifier_at(start).scope_context(scope_context); - - let word_kind = cmp::max( - prev_chars.peek().copied().map(|c| classifier.kind(c)), - next_chars.peek().copied().map(|c| classifier.kind(c)), - ); - - for ch in prev_chars { - if Some(classifier.kind(ch)) == word_kind && ch != '\n' { - start -= ch.len_utf8(); - } else { - break; - } - } - - for ch in next_chars { - if Some(classifier.kind(ch)) == word_kind && ch != '\n' { - end += ch.len_utf8(); - } else { - break; - } - } - - (start..end, word_kind) - } - - pub fn char_kind_before( - &self, - start: T, - scope_context: Option, - ) -> Option { - let start = start.to_offset(self); - let classifier = self.char_classifier_at(start).scope_context(scope_context); - self.reversed_chars_at(start) - .next() - .map(|ch| classifier.kind(ch)) - } - - pub fn is_singleton(&self) -> bool { - self.singleton - } - - pub fn as_singleton(&self) -> Option<(&ExcerptId, BufferId, &BufferSnapshot)> { - if self.singleton { - self.excerpts - .iter() - .next() - .map(|e| (&e.id, e.buffer_id, &e.buffer)) - } else { - None - } - } - - pub fn len(&self) -> MultiBufferOffset { - self.diff_transforms.summary().output.len - } - - pub fn max_position(&self) -> MBD { - MBD::from_summary(&self.text_summary()) - } - - pub fn is_empty(&self) -> bool { - self.diff_transforms.summary().output.len == MultiBufferOffset(0) - } - - pub fn widest_line_number(&self) -> u32 { - // widest_line_number is 0-based, so 1 is added to get the displayed line number. - self.excerpts.summary().widest_line_number + 1 - } - - pub fn bytes_in_range(&self, range: Range) -> MultiBufferBytes<'_> { - let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut excerpts = self.cursor::(); - excerpts.seek(&range.start); - - let mut chunk; - let mut has_trailing_newline; - let excerpt_bytes; - if let Some(region) = excerpts.region() { - let mut bytes = region.buffer.bytes_in_range( - region.buffer_range.start + (range.start - region.range.start) - ..(region.buffer_range.start + (range.end - region.range.start)) - .min(region.buffer_range.end), - ); - chunk = bytes.next().unwrap_or(&[][..]); - excerpt_bytes = Some(bytes); - has_trailing_newline = region.has_trailing_newline && range.end >= region.range.end; - if chunk.is_empty() && has_trailing_newline { - chunk = b"\n"; - has_trailing_newline = false; - } - } else { - chunk = &[][..]; - excerpt_bytes = None; - has_trailing_newline = false; - }; - - MultiBufferBytes { - range, - cursor: excerpts, - excerpt_bytes, - has_trailing_newline, - chunk, - } - } - - pub fn reversed_bytes_in_range( - &self, - range: Range, - ) -> ReversedMultiBufferBytes<'_> { - let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut chunks = self.reversed_chunks_in_range(range.clone()); - let chunk = chunks.next().map_or(&[][..], |c| c.as_bytes()); - ReversedMultiBufferBytes { - range, - chunks, - chunk, - } - } - - pub fn row_infos(&self, start_row: MultiBufferRow) -> MultiBufferRows<'_> { - let mut cursor = self.cursor::(); - cursor.seek(&Point::new(start_row.0, 0)); - let mut result = MultiBufferRows { - point: Point::new(0, 0), - is_empty: self.excerpts.is_empty(), - is_singleton: self.is_singleton(), - cursor, - }; - result.seek(start_row); - result - } - - pub fn chunks( - &self, - range: Range, - language_aware: bool, - ) -> MultiBufferChunks<'_> { - let mut chunks = MultiBufferChunks { - excerpt_offset_range: ExcerptDimension(MultiBufferOffset::ZERO) - ..ExcerptDimension(MultiBufferOffset::ZERO), - range: MultiBufferOffset::ZERO..MultiBufferOffset::ZERO, - excerpts: self.excerpts.cursor(()), - diff_transforms: self.diff_transforms.cursor(()), - diffs: &self.diffs, - diff_base_chunks: None, - excerpt_chunks: None, - buffer_chunk: None, - language_aware, - }; - let range = range.start.to_offset(self)..range.end.to_offset(self); - chunks.seek(range); - chunks - } - - pub fn clip_offset(&self, offset: MultiBufferOffset, bias: Bias) -> MultiBufferOffset { - self.clip_dimension(offset, bias, text::BufferSnapshot::clip_offset) - } - - pub fn clip_point(&self, point: Point, bias: Bias) -> Point { - self.clip_dimension(point, bias, text::BufferSnapshot::clip_point) - } - - pub fn clip_offset_utf16( - &self, - offset: MultiBufferOffsetUtf16, - bias: Bias, - ) -> MultiBufferOffsetUtf16 { - self.clip_dimension(offset, bias, text::BufferSnapshot::clip_offset_utf16) - } - - pub fn clip_point_utf16(&self, point: Unclipped, bias: Bias) -> PointUtf16 { - self.clip_dimension(point.0, bias, |buffer, point, bias| { - buffer.clip_point_utf16(Unclipped(point), bias) - }) - } - - pub fn offset_to_point(&self, offset: MultiBufferOffset) -> Point { - self.convert_dimension(offset, text::BufferSnapshot::offset_to_point) - } - - pub fn offset_to_point_utf16(&self, offset: MultiBufferOffset) -> PointUtf16 { - self.convert_dimension(offset, text::BufferSnapshot::offset_to_point_utf16) - } - - pub fn point_to_point_utf16(&self, point: Point) -> PointUtf16 { - self.convert_dimension(point, text::BufferSnapshot::point_to_point_utf16) - } - - pub fn point_utf16_to_point(&self, point: PointUtf16) -> Point { - self.convert_dimension(point, text::BufferSnapshot::point_utf16_to_point) - } - - #[instrument(skip_all)] - pub fn point_to_offset(&self, point: Point) -> MultiBufferOffset { - self.convert_dimension(point, text::BufferSnapshot::point_to_offset) - } - - pub fn point_to_offset_utf16(&self, point: Point) -> MultiBufferOffsetUtf16 { - self.convert_dimension(point, text::BufferSnapshot::point_to_offset_utf16) - } - - pub fn offset_utf16_to_offset(&self, offset: MultiBufferOffsetUtf16) -> MultiBufferOffset { - self.convert_dimension(offset, text::BufferSnapshot::offset_utf16_to_offset) - } - - pub fn offset_to_offset_utf16(&self, offset: MultiBufferOffset) -> MultiBufferOffsetUtf16 { - self.convert_dimension(offset, text::BufferSnapshot::offset_to_offset_utf16) - } - - pub fn point_utf16_to_offset(&self, point: PointUtf16) -> MultiBufferOffset { - self.convert_dimension(point, text::BufferSnapshot::point_utf16_to_offset) - } - - pub fn point_utf16_to_offset_utf16(&self, point: PointUtf16) -> MultiBufferOffsetUtf16 { - self.convert_dimension(point, text::BufferSnapshot::point_utf16_to_offset_utf16) - } - - fn clip_dimension( - &self, - position: MBD, - bias: Bias, - clip_buffer_position: fn(&text::BufferSnapshot, BD, Bias) -> BD, - ) -> MBD - where - MBD: MultiBufferDimension + Ord + Sub + ops::AddAssign<::Output>, - BD: TextDimension + Sub::Output> + AddAssign<::Output>, - { - let mut cursor = self.cursor::(); - cursor.seek(&position); - if let Some(region) = cursor.region() { - if position >= region.range.end { - return region.range.end; - } - let overshoot = position - region.range.start; - let mut buffer_position = region.buffer_range.start; - buffer_position += overshoot; - let clipped_buffer_position = - clip_buffer_position(region.buffer, buffer_position, bias); - let mut position = region.range.start; - position += clipped_buffer_position - region.buffer_range.start; - position - } else { - self.max_position() - } - } - - #[instrument(skip_all)] - fn convert_dimension( - &self, - key: MBR1, - convert_buffer_dimension: fn(&text::BufferSnapshot, BR1) -> BR2, - ) -> MBR2 - where - MBR1: MultiBufferDimension + Ord + Sub + ops::AddAssign<::Output>, - BR1: TextDimension + Sub::Output> + AddAssign<::Output>, - MBR2: MultiBufferDimension + Ord + Sub + ops::AddAssign<::Output>, - BR2: TextDimension + Sub::Output> + AddAssign<::Output>, - { - let mut cursor = self.cursor::, DimensionPair>(); - cursor.seek(&DimensionPair { key, value: None }); - if let Some(region) = cursor.region() { - if key >= region.range.end.key { - return region.range.end.value.unwrap(); - } - let start_key = region.range.start.key; - let start_value = region.range.start.value.unwrap(); - let buffer_start_key = region.buffer_range.start.key; - let buffer_start_value = region.buffer_range.start.value.unwrap(); - let mut buffer_key = buffer_start_key; - buffer_key += key - start_key; - let buffer_value = convert_buffer_dimension(region.buffer, buffer_key); - let mut result = start_value; - result += buffer_value - buffer_start_value; - result - } else { - self.max_position() - } - } - - pub fn point_to_buffer_offset( - &self, - point: T, - ) -> Option<(&BufferSnapshot, BufferOffset)> { - let offset = point.to_offset(self); - let mut cursor = self.cursor::(); - cursor.seek(&offset); - let region = cursor.region()?; - let overshoot = offset - region.range.start; - let buffer_offset = region.buffer_range.start + overshoot; - if buffer_offset == BufferOffset(region.buffer.len() + 1) - && region.has_trailing_newline - && !region.is_main_buffer - { - let main_buffer_position = cursor.main_buffer_position()?; - let buffer_snapshot = &cursor.excerpt()?.buffer; - return Some((buffer_snapshot, main_buffer_position)); - } else if buffer_offset > BufferOffset(region.buffer.len()) { - return None; - } - Some((region.buffer, buffer_offset)) - } - - pub fn point_to_buffer_point( - &self, - point: Point, - ) -> Option<(&BufferSnapshot, Point, ExcerptId)> { - let mut cursor = self.cursor::(); - cursor.seek(&point); - let region = cursor.region()?; - let overshoot = point - region.range.start; - let buffer_point = region.buffer_range.start + overshoot; - let excerpt = cursor.excerpt()?; - if buffer_point == region.buffer.max_point() + Point::new(1, 0) - && region.has_trailing_newline - && !region.is_main_buffer - { - return Some((&excerpt.buffer, cursor.main_buffer_position()?, excerpt.id)); - } else if buffer_point > region.buffer.max_point() { - return None; - } - Some((region.buffer, buffer_point, excerpt.id)) - } - - pub fn suggested_indents( - &self, - rows: impl IntoIterator, - cx: &App, - ) -> BTreeMap { - let mut result = BTreeMap::new(); - self.suggested_indents_callback( - rows, - |row, indent| { - result.insert(row, indent); - ControlFlow::Continue(()) - }, - cx, - ); - result - } - - // move this to be a generator once those are a thing - pub fn suggested_indents_callback( - &self, - rows: impl IntoIterator, - mut cb: impl FnMut(MultiBufferRow, IndentSize) -> ControlFlow<()>, - cx: &App, - ) { - let mut rows_for_excerpt = Vec::new(); - let mut cursor = self.cursor::(); - let mut rows = rows.into_iter().peekable(); - let mut prev_row = u32::MAX; - let mut prev_language_indent_size = IndentSize::default(); - - while let Some(row) = rows.next() { - cursor.seek(&Point::new(row, 0)); - let Some(region) = cursor.region() else { - continue; - }; - - // Retrieve the language and indent size once for each disjoint region being indented. - let single_indent_size = if row.saturating_sub(1) == prev_row { - prev_language_indent_size - } else { - region - .buffer - .language_indent_size_at(Point::new(row, 0), cx) - }; - prev_language_indent_size = single_indent_size; - prev_row = row; - - let start_buffer_row = region.buffer_range.start.row; - let start_multibuffer_row = region.range.start.row; - let end_multibuffer_row = region.range.end.row; - - rows_for_excerpt.push(row); - while let Some(next_row) = rows.peek().copied() { - if end_multibuffer_row > next_row { - rows_for_excerpt.push(next_row); - rows.next(); - } else { - break; - } - } - - let buffer_rows = rows_for_excerpt - .drain(..) - .map(|row| start_buffer_row + row - start_multibuffer_row); - let buffer_indents = region - .buffer - .suggested_indents(buffer_rows, single_indent_size); - for (row, indent) in buffer_indents { - if cb( - MultiBufferRow(start_multibuffer_row + row - start_buffer_row), - indent, - ) - .is_break() - { - return; - } - } - } - } - - pub fn indent_size_for_line(&self, row: MultiBufferRow) -> IndentSize { - if let Some((buffer, range)) = self.buffer_line_for_row(row) { - let mut size = buffer.indent_size_for_line(range.start.row); - size.len = size - .len - .min(range.end.column) - .saturating_sub(range.start.column); - size - } else { - IndentSize::spaces(0) - } - } - - pub fn line_indent_for_row(&self, row: MultiBufferRow) -> LineIndent { - if let Some((buffer, range)) = self.buffer_line_for_row(row) { - LineIndent::from_iter(buffer.text_for_range(range).flat_map(|s| s.chars())) - } else { - LineIndent::spaces(0) - } - } - - pub fn indent_and_comment_for_line(&self, row: MultiBufferRow, cx: &App) -> String { - let mut indent = self.indent_size_for_line(row).chars().collect::(); - - if self.language_settings(cx).extend_comment_on_newline - && let Some(language_scope) = self.language_scope_at(Point::new(row.0, 0)) - { - let delimiters = language_scope.line_comment_prefixes(); - for delimiter in delimiters { - if *self - .chars_at(Point::new(row.0, indent.len() as u32)) - .take(delimiter.chars().count()) - .collect::() - .as_str() - == **delimiter - { - indent.push_str(delimiter); - break; - } - } - } - - indent - } - - pub fn is_line_whitespace_upto(&self, position: T) -> bool - where - T: ToOffset, - { - for char in self.reversed_chars_at(position) { - if !char.is_whitespace() { - return false; - } - if char == '\n' { - return true; - } - } - true - } - - pub fn prev_non_blank_row(&self, mut row: MultiBufferRow) -> Option { - while row.0 > 0 { - row.0 -= 1; - if !self.is_line_blank(row) { - return Some(row); - } - } - None - } - - pub fn line_len(&self, row: MultiBufferRow) -> u32 { - if let Some((_, range)) = self.buffer_line_for_row(row) { - range.end.column - range.start.column - } else { - 0 - } - } - - pub fn buffer_line_for_row( - &self, - row: MultiBufferRow, - ) -> Option<(&BufferSnapshot, Range)> { - let mut cursor = self.cursor::(); - let point = Point::new(row.0, 0); - cursor.seek(&point); - let region = cursor.region()?; - let overshoot = point.min(region.range.end) - region.range.start; - let buffer_point = region.buffer_range.start + overshoot; - if buffer_point.row > region.buffer_range.end.row { - return None; - } - let line_start = Point::new(buffer_point.row, 0).max(region.buffer_range.start); - let line_end = Point::new(buffer_point.row, region.buffer.line_len(buffer_point.row)) - .min(region.buffer_range.end); - Some((region.buffer, line_start..line_end)) - } - - pub fn max_point(&self) -> Point { - self.text_summary().lines - } - - pub fn max_row(&self) -> MultiBufferRow { - MultiBufferRow(self.text_summary().lines.row) - } - - pub fn text_summary(&self) -> MBTextSummary { - self.diff_transforms.summary().output - } - - pub fn text_summary_for_range(&self, range: Range) -> MBD - where - MBD: MultiBufferDimension + AddAssign, - O: ToOffset, - { - let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut cursor = self - .diff_transforms - .cursor::>(()); - cursor.seek(&range.start, Bias::Right); - - let Some(first_transform) = cursor.item() else { - return MBD::from_summary(&MBTextSummary::default()); - }; - - let diff_transform_start = cursor.start().0; - let diff_transform_end = cursor.end().0; - let diff_start = range.start; - let start_overshoot = diff_start - diff_transform_start; - let end_overshoot = std::cmp::min(range.end, diff_transform_end) - diff_transform_start; - - let mut result = match first_transform { - DiffTransform::Unmodified { .. } | DiffTransform::InsertedHunk { .. } => { - let excerpt_start = cursor.start().1 + start_overshoot; - let excerpt_end = cursor.start().1 + end_overshoot; - self.text_summary_for_excerpt_offset_range(excerpt_start..excerpt_end) - } - DiffTransform::FilteredInsertedHunk { .. } => MBD::default(), - DiffTransform::DeletedHunk { - buffer_id, - has_trailing_newline, - hunk_info, - .. - } => { - let buffer_start = hunk_info.base_text_byte_range.start + start_overshoot; - let mut buffer_end = hunk_info.base_text_byte_range.start + end_overshoot; - let Some(base_text) = self.diffs.get(buffer_id).map(|diff| diff.base_text()) else { - panic!("{:?} is in non-existent deleted hunk", range.start) - }; - - let include_trailing_newline = - *has_trailing_newline && range.end >= diff_transform_end; - if include_trailing_newline { - buffer_end -= 1; - } - - let mut summary = base_text - .text_summary_for_range::(buffer_start..buffer_end); - - if include_trailing_newline { - summary.add_assign(&::from_text_summary( - &TextSummary::newline(), - )) - } - - let mut result = MBD::default(); - result.add_text_dim(&summary); - result - } - }; - if range.end < diff_transform_end { - return result; - } - - cursor.next(); - result.add_mb_text_summary( - &cursor - .summary::<_, OutputDimension<_>>(&range.end, Bias::Right) - .0, - ); - - let Some(last_transform) = cursor.item() else { - return result; - }; - - let overshoot = range.end - cursor.start().0; - let suffix = match last_transform { - DiffTransform::Unmodified { .. } | DiffTransform::InsertedHunk { .. } => { - let end = cursor.start().1 + overshoot; - self.text_summary_for_excerpt_offset_range::(cursor.start().1..end) - } - DiffTransform::FilteredInsertedHunk { .. } => MBD::default(), - DiffTransform::DeletedHunk { - buffer_id, - has_trailing_newline, - hunk_info, - .. - } => { - let buffer_end = hunk_info.base_text_byte_range.start + overshoot; - let Some(base_text) = self.diffs.get(buffer_id).map(|diff| diff.base_text()) else { - panic!("{:?} is in non-existent deleted hunk", range.end) - }; - - let mut suffix = base_text.text_summary_for_range::( - hunk_info.base_text_byte_range.start..buffer_end, - ); - if *has_trailing_newline && buffer_end == hunk_info.base_text_byte_range.end + 1 { - suffix.add_assign(&::from_text_summary( - &TextSummary::from("\n"), - )) - } - - let mut result = MBD::default(); - result.add_text_dim(&suffix); - result - } - }; - - result += suffix; - result - } - - fn text_summary_for_excerpt_offset_range(&self, mut range: Range) -> MBD - where - MBD: MultiBufferDimension + AddAssign, - { - let mut summary = MBD::default(); - let mut cursor = self.excerpts.cursor::(()); - cursor.seek(&range.start, Bias::Right); - if let Some(excerpt) = cursor.item() { - let mut end_before_newline = cursor.end(); - if excerpt.has_trailing_newline { - end_before_newline -= 1; - } - - let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer); - let start_in_excerpt = excerpt_start + (range.start - *cursor.start()); - let end_in_excerpt = - excerpt_start + (cmp::min(end_before_newline, range.end) - *cursor.start()); - summary.add_text_dim( - &excerpt - .buffer - .text_summary_for_range::( - start_in_excerpt..end_in_excerpt, - ), - ); - - if range.end > end_before_newline { - summary.add_mb_text_summary(&MBTextSummary::from(TextSummary::newline())); - } - - cursor.next(); - } - - if range.end > *cursor.start() { - summary += cursor - .summary::<_, ExcerptDimension>(&range.end, Bias::Right) - .0; - if let Some(excerpt) = cursor.item() { - range.end = cmp::max(*cursor.start(), range.end); - - let excerpt_start = excerpt.range.context.start.to_offset(&excerpt.buffer); - let end_in_excerpt = excerpt_start + (range.end - *cursor.start()); - summary.add_text_dim( - &excerpt - .buffer - .text_summary_for_range::( - excerpt_start..end_in_excerpt, - ), - ); - } - } - - summary - } - - pub fn summary_for_anchor(&self, anchor: &Anchor) -> MBD - where - MBD: MultiBufferDimension - + Ord - + Sub - + AddAssign, - MBD::TextDimension: Sub + Ord, - { - self.summaries_for_anchors([anchor])[0] - } - - fn resolve_summary_for_anchor( - &self, - anchor: &Anchor, - excerpt_position: ExcerptDimension, - diff_transforms: &mut Cursor< - DiffTransform, - Dimensions, OutputDimension>, - >, - ) -> MBD - where - MBD: MultiBufferDimension + Ord + Sub + AddAssign<::Output>, - { - loop { - let transform_end_position = diff_transforms.end().0; - let at_transform_end = - transform_end_position == excerpt_position && diff_transforms.item().is_some(); - if at_transform_end && anchor.text_anchor.bias == Bias::Right { - diff_transforms.next(); - continue; - } - - let mut position = diff_transforms.start().1; - match diff_transforms.item() { - Some(DiffTransform::DeletedHunk { - buffer_id, - hunk_info, - .. - }) => { - if let Some(diff_base_anchor) = &anchor.diff_base_anchor - && let Some(base_text) = - self.diffs.get(buffer_id).map(|diff| diff.base_text()) - && base_text.can_resolve(diff_base_anchor) - { - let base_text_offset = diff_base_anchor.to_offset(base_text); - if base_text_offset >= hunk_info.base_text_byte_range.start - && base_text_offset <= hunk_info.base_text_byte_range.end - { - let position_in_hunk = base_text - .text_summary_for_range::( - hunk_info.base_text_byte_range.start..base_text_offset, - ); - position.0.add_text_dim(&position_in_hunk); - } else if at_transform_end { - diff_transforms.next(); - continue; - } - } - } - _ => { - if at_transform_end && anchor.diff_base_anchor.is_some() { - diff_transforms.next(); - continue; - } - - if !matches!( - diff_transforms.item(), - Some(DiffTransform::FilteredInsertedHunk { .. }) - ) { - let overshoot = excerpt_position - diff_transforms.start().0; - position += overshoot; - } - } - } - - return position.0; - } - } - - fn excerpt_offset_for_anchor(&self, anchor: &Anchor) -> ExcerptOffset { - let mut cursor = self - .excerpts - .cursor::, ExcerptOffset>>(()); - let locator = self.excerpt_locator_for_id(anchor.excerpt_id); - - cursor.seek(&Some(locator), Bias::Left); - if cursor.item().is_none() && anchor.excerpt_id == ExcerptId::max() { - cursor.prev(); - } - - let mut position = cursor.start().1; - if let Some(excerpt) = cursor.item() - && (excerpt.id == anchor.excerpt_id || anchor.excerpt_id == ExcerptId::max()) - { - let excerpt_buffer_start = excerpt - .buffer - .offset_for_anchor(&excerpt.range.context.start); - let excerpt_buffer_end = excerpt.buffer.offset_for_anchor(&excerpt.range.context.end); - let buffer_position = cmp::min( - excerpt_buffer_end, - excerpt.buffer.offset_for_anchor(&anchor.text_anchor), - ); - if buffer_position > excerpt_buffer_start { - position += buffer_position - excerpt_buffer_start; - } - } - position - } - - pub fn latest_excerpt_id(&self, mut excerpt_id: ExcerptId) -> ExcerptId { - while let Some(replacement) = self.replaced_excerpts.get(&excerpt_id) { - excerpt_id = *replacement; - } - excerpt_id - } - - pub fn summaries_for_anchors<'a, MBD, I>(&'a self, anchors: I) -> Vec - where - MBD: MultiBufferDimension - + Ord - + Sub - + AddAssign, - MBD::TextDimension: Sub + Ord, - I: 'a + IntoIterator, - { - let mut anchors = anchors.into_iter().peekable(); - let mut cursor = self.excerpts.cursor::(()); - let mut diff_transforms_cursor = self - .diff_transforms - .cursor::, OutputDimension>>(()); - diff_transforms_cursor.next(); - - let mut summaries = Vec::new(); - while let Some(anchor) = anchors.peek() { - let excerpt_id = self.latest_excerpt_id(anchor.excerpt_id); - - let excerpt_anchors = anchors.peeking_take_while(|anchor| { - self.latest_excerpt_id(anchor.excerpt_id) == excerpt_id - }); - - let locator = self.excerpt_locator_for_id(excerpt_id); - cursor.seek_forward(locator, Bias::Left); - if cursor.item().is_none() && excerpt_id == ExcerptId::max() { - cursor.prev(); - } - - let excerpt_start_position = ExcerptDimension(MBD::from_summary(&cursor.start().text)); - if let Some(excerpt) = cursor.item() { - if excerpt.id != excerpt_id && excerpt_id != ExcerptId::max() { - let position = self.resolve_summary_for_anchor( - &Anchor::min(), - excerpt_start_position, - &mut diff_transforms_cursor, - ); - summaries.extend(excerpt_anchors.map(|_| position)); - continue; - } - let excerpt_buffer_start = excerpt - .range - .context - .start - .summary::(&excerpt.buffer); - let excerpt_buffer_end = excerpt - .range - .context - .end - .summary::(&excerpt.buffer); - for (buffer_summary, anchor) in excerpt - .buffer - .summaries_for_anchors_with_payload::( - excerpt_anchors.map(|a| (&a.text_anchor, a)), - ) - { - let summary = cmp::min(excerpt_buffer_end, buffer_summary); - let mut position = excerpt_start_position; - if summary > excerpt_buffer_start { - position += summary - excerpt_buffer_start; - } - - if diff_transforms_cursor.start().0 < position { - diff_transforms_cursor.seek_forward(&position, Bias::Left); - } - - summaries.push(self.resolve_summary_for_anchor( - anchor, - position, - &mut diff_transforms_cursor, - )); - } - } else { - diff_transforms_cursor.seek_forward(&excerpt_start_position, Bias::Left); - let position = self.resolve_summary_for_anchor( - &Anchor::max(), - excerpt_start_position, - &mut diff_transforms_cursor, - ); - summaries.extend(excerpt_anchors.map(|_| position)); - } - } - - summaries - } - - pub fn dimensions_from_points<'a, MBD>( - &'a self, - points: impl 'a + IntoIterator, - ) -> impl 'a + Iterator - where - MBD: MultiBufferDimension + Sub + AddAssign<::Output>, - { - let mut cursor = self.cursor::, Point>(); - cursor.seek(&DimensionPair { - key: Point::default(), - value: None, - }); - let mut points = points.into_iter(); - iter::from_fn(move || { - let point = points.next()?; - - cursor.seek_forward(&DimensionPair { - key: point, - value: None, - }); - - if let Some(region) = cursor.region() { - let overshoot = point - region.range.start.key; - let buffer_point = region.buffer_range.start + overshoot; - let mut position = region.range.start.value.unwrap(); - position.add_text_dim( - ®ion - .buffer - .text_summary_for_range(region.buffer_range.start..buffer_point), - ); - if point == region.range.end.key && region.has_trailing_newline { - position.add_mb_text_summary(&MBTextSummary::from(TextSummary::newline())); - } - Some(position) - } else { - Some(MBD::from_summary(&self.text_summary())) - } - }) - } - - pub fn refresh_anchors<'a, I>(&'a self, anchors: I) -> Vec<(usize, Anchor, bool)> - where - I: 'a + IntoIterator, - { - let mut anchors = anchors.into_iter().enumerate().peekable(); - let mut cursor = self.excerpts.cursor::>(()); - cursor.next(); - - let mut result = Vec::new(); - - while let Some((_, anchor)) = anchors.peek() { - let old_excerpt_id = anchor.excerpt_id; - - // Find the location where this anchor's excerpt should be. - let old_locator = self.excerpt_locator_for_id(old_excerpt_id); - cursor.seek_forward(&Some(old_locator), Bias::Left); - - let next_excerpt = cursor.item(); - let prev_excerpt = cursor.prev_item(); - - // Process all of the anchors for this excerpt. - while let Some((anchor_ix, &anchor)) = - anchors.next_if(|(_, anchor)| anchor.excerpt_id == old_excerpt_id) - { - let mut anchor = anchor; - - // Leave min and max anchors unchanged if invalid or - // if the old excerpt still exists at this location - let mut kept_position = next_excerpt - .is_some_and(|e| e.id == old_excerpt_id && e.contains(&anchor)) - || old_excerpt_id == ExcerptId::max() - || old_excerpt_id == ExcerptId::min(); - - // If the old excerpt no longer exists at this location, then attempt to - // find an equivalent position for this anchor in an adjacent excerpt. - if !kept_position { - for excerpt in [next_excerpt, prev_excerpt].iter().filter_map(|e| *e) { - if excerpt.contains(&anchor) { - anchor.excerpt_id = excerpt.id; - kept_position = true; - break; - } - } - } - - // If there's no adjacent excerpt that contains the anchor's position, - // then report that the anchor has lost its position. - if !kept_position { - anchor = if let Some(excerpt) = next_excerpt { - let mut text_anchor = excerpt - .range - .context - .start - .bias(anchor.text_anchor.bias, &excerpt.buffer); - if text_anchor - .cmp(&excerpt.range.context.end, &excerpt.buffer) - .is_gt() - { - text_anchor = excerpt.range.context.end; - } - Anchor::in_buffer(excerpt.id, text_anchor) - } else if let Some(excerpt) = prev_excerpt { - let mut text_anchor = excerpt - .range - .context - .end - .bias(anchor.text_anchor.bias, &excerpt.buffer); - if text_anchor - .cmp(&excerpt.range.context.start, &excerpt.buffer) - .is_lt() - { - text_anchor = excerpt.range.context.start; - } - Anchor::in_buffer(excerpt.id, text_anchor) - } else if anchor.text_anchor.bias == Bias::Left { - Anchor::min() - } else { - Anchor::max() - }; - } - - result.push((anchor_ix, anchor, kept_position)); - } - } - result.sort_unstable_by(|a, b| a.1.cmp(&b.1, self)); - result - } - - pub fn anchor_before(&self, position: T) -> Anchor { - self.anchor_at(position, Bias::Left) - } - - pub fn anchor_after(&self, position: T) -> Anchor { - self.anchor_at(position, Bias::Right) - } - - pub fn anchor_at(&self, position: T, mut bias: Bias) -> Anchor { - let offset = position.to_offset(self); - - // Find the given position in the diff transforms. Determine the corresponding - // offset in the excerpts, and whether the position is within a deleted hunk. - let mut diff_transforms = self - .diff_transforms - .cursor::>(()); - diff_transforms.seek(&offset, Bias::Right); - - if offset == diff_transforms.start().0 - && bias == Bias::Left - && let Some(prev_item) = diff_transforms.prev_item() - && let DiffTransform::DeletedHunk { .. } = prev_item - { - diff_transforms.prev(); - } - let offset_in_transform = offset - diff_transforms.start().0; - let mut excerpt_offset = diff_transforms.start().1; - let mut diff_base_anchor = None; - if let Some(DiffTransform::DeletedHunk { - buffer_id, - has_trailing_newline, - hunk_info, - .. - }) = diff_transforms.item() - { - let base_text_byte_range = &hunk_info.base_text_byte_range; - let diff = self.diffs.get(buffer_id).expect("missing diff"); - if offset_in_transform > base_text_byte_range.len() { - debug_assert!(*has_trailing_newline); - bias = Bias::Right; - } else { - diff_base_anchor = Some( - diff.base_text() - .anchor_at(base_text_byte_range.start + offset_in_transform, bias), - ); - bias = Bias::Left; - } - } else { - excerpt_offset += MultiBufferOffset(offset_in_transform); - }; - - let mut excerpts = self - .excerpts - .cursor::>>(()); - excerpts.seek(&excerpt_offset, Bias::Right); - if excerpts.item().is_none() && excerpt_offset == excerpts.start().0 && bias == Bias::Left { - excerpts.prev(); - } - if let Some(excerpt) = excerpts.item() { - let mut overshoot = excerpt_offset.saturating_sub(excerpts.start().0); - if excerpt.has_trailing_newline && excerpt_offset == excerpts.end().0 { - overshoot -= 1; - bias = Bias::Right; - } - - let buffer_start = excerpt.range.context.start.to_offset(&excerpt.buffer); - let text_anchor = - excerpt.clip_anchor(excerpt.buffer.anchor_at(buffer_start + overshoot, bias)); - let anchor = Anchor::in_buffer(excerpt.id, text_anchor); - match diff_base_anchor { - Some(diff_base_anchor) => anchor.with_diff_base_anchor(diff_base_anchor), - None => anchor, - } - } else if excerpt_offset == ExcerptDimension(MultiBufferOffset::ZERO) && bias == Bias::Left - { - Anchor::min() - } else { - Anchor::max() - } - } - - /// Wraps the [`text::Anchor`] in a [`multi_buffer::Anchor`] if this multi-buffer is a singleton. - pub fn as_singleton_anchor(&self, text_anchor: text::Anchor) -> Option { - let (excerpt, buffer, _) = self.as_singleton()?; - if text_anchor.buffer_id.is_none_or(|id| id == buffer) { - Some(Anchor::in_buffer(*excerpt, text_anchor)) - } else { - None - } - } - - /// Returns an anchor for the given excerpt and text anchor, - /// Returns [`None`] if the excerpt_id is no longer valid or the text anchor range is out of excerpt's bounds. - pub fn anchor_range_in_excerpt( - &self, - excerpt_id: ExcerptId, - text_anchor: Range, - ) -> Option> { - let excerpt = self.excerpt(self.latest_excerpt_id(excerpt_id))?; - - Some( - Self::anchor_in_excerpt_(excerpt, text_anchor.start)? - ..Self::anchor_in_excerpt_(excerpt, text_anchor.end)?, - ) - } - - /// Returns an anchor for the given excerpt and text anchor, - /// Returns [`None`] if the excerpt_id is no longer valid or the text anchor range is out of excerpt's bounds. - pub fn anchor_in_excerpt( - &self, - excerpt_id: ExcerptId, - text_anchor: text::Anchor, - ) -> Option { - let excerpt = self.excerpt(self.latest_excerpt_id(excerpt_id))?; - Self::anchor_in_excerpt_(excerpt, text_anchor) - } - - /// Same as [`MultiBuffer::anchor_in_excerpt`], but more efficient than calling it multiple times. - pub fn anchors_in_excerpt( - &self, - excerpt_id: ExcerptId, - text_anchors: impl IntoIterator, - ) -> Option>> { - let excerpt = self.excerpt(self.latest_excerpt_id(excerpt_id))?; - Some( - text_anchors - .into_iter() - .map(|text_anchor| Self::anchor_in_excerpt_(excerpt, text_anchor)), - ) - } - - fn anchor_in_excerpt_(excerpt: &Excerpt, text_anchor: text::Anchor) -> Option { - match text_anchor.buffer_id { - Some(buffer_id) if buffer_id == excerpt.buffer_id => (), - Some(_) => return None, - None if text_anchor.is_max() || text_anchor.is_min() => { - return Some(Anchor::in_buffer(excerpt.id, text_anchor)); - } - None => return None, - } - - let context = &excerpt.range.context; - if context.start.cmp(&text_anchor, &excerpt.buffer).is_gt() - || context.end.cmp(&text_anchor, &excerpt.buffer).is_lt() - { - return None; - } - - Some(Anchor::in_buffer(excerpt.id, text_anchor)) - } - - pub fn context_range_for_excerpt(&self, excerpt_id: ExcerptId) -> Option> { - Some(self.excerpt(excerpt_id)?.range.context.clone()) - } - - pub fn can_resolve(&self, anchor: &Anchor) -> bool { - if anchor.is_min() || anchor.is_max() { - // todo(lw): should be `!self.is_empty()` - true - } else if let Some(excerpt) = self.excerpt(anchor.excerpt_id) { - excerpt.buffer.can_resolve(&anchor.text_anchor) - } else { - false - } - } - - pub fn excerpts( - &self, - ) -> impl Iterator)> { - self.excerpts - .iter() - .map(|excerpt| (excerpt.id, &excerpt.buffer, excerpt.range.clone())) - } - - fn cursor<'a, MBD, BD>(&'a self) -> MultiBufferCursor<'a, MBD, BD> - where - MBD: MultiBufferDimension + Ord + Sub + ops::AddAssign<::Output>, - BD: TextDimension + AddAssign<::Output>, - { - let excerpts = self.excerpts.cursor(()); - let diff_transforms = self.diff_transforms.cursor(()); - MultiBufferCursor { - excerpts, - diff_transforms, - snapshot: &self, - cached_region: None, - } - } - - pub fn excerpt_before(&self, excerpt_id: ExcerptId) -> Option> { - let start_locator = self.excerpt_locator_for_id(excerpt_id); - let mut excerpts = self - .excerpts - .cursor::, ExcerptOffset>>(()); - excerpts.seek(&Some(start_locator), Bias::Left); - excerpts.prev(); - - let mut diff_transforms = self - .diff_transforms - .cursor::>(()); - diff_transforms.seek(&excerpts.start().1, Bias::Left); - if diff_transforms.end().excerpt_dimension < excerpts.start().1 { - diff_transforms.next(); - } - - let excerpt = excerpts.item()?; - Some(MultiBufferExcerpt { - excerpt, - offset: diff_transforms.start().output_dimension.0, - buffer_offset: BufferOffset(excerpt.range.context.start.to_offset(&excerpt.buffer)), - excerpt_offset: excerpts.start().1, - diff_transforms, - }) - } - - pub fn excerpt_boundaries_in_range( - &self, - range: R, - ) -> impl Iterator + '_ - where - R: RangeBounds, - T: ToOffset, - { - let start_offset; - let start = match range.start_bound() { - Bound::Included(start) => { - start_offset = start.to_offset(self); - Bound::Included(start_offset) - } - Bound::Excluded(_) => { - panic!("not supported") - } - Bound::Unbounded => { - start_offset = MultiBufferOffset::ZERO; - Bound::Unbounded - } - }; - let end = match range.end_bound() { - Bound::Included(end) => Bound::Included(end.to_offset(self)), - Bound::Excluded(end) => Bound::Excluded(end.to_offset(self)), - Bound::Unbounded => Bound::Unbounded, - }; - let bounds = (start, end); - let mut cursor = self.cursor::, BufferOffset>(); - cursor.seek(&DimensionPair { - key: start_offset, - value: None, - }); - - if cursor - .region() - .is_some_and(|region| bounds.contains(®ion.range.start.key)) - { - cursor.prev_excerpt(); - } else { - cursor.seek_to_start_of_current_excerpt(); - } - let mut prev_region = cursor.region(); - - cursor.next_excerpt(); - - iter::from_fn(move || { - loop { - if self.singleton { - return None; - } - - let next_region = cursor.region()?; - cursor.next_excerpt(); - if !bounds.contains(&next_region.range.start.key) { - prev_region = Some(next_region); - continue; - } - - let next_region_start = next_region.range.start.value.unwrap(); - let next_region_end = if let Some(region) = cursor.region() { - region.range.start.value.unwrap() - } else { - self.max_point() - }; - - let prev = prev_region.as_ref().map(|region| ExcerptInfo { - id: region.excerpt.id, - buffer: region.excerpt.buffer.clone(), - buffer_id: region.excerpt.buffer_id, - range: region.excerpt.range.clone(), - end_row: MultiBufferRow(next_region_start.row), - }); - - let next = ExcerptInfo { - id: next_region.excerpt.id, - buffer: next_region.excerpt.buffer.clone(), - buffer_id: next_region.excerpt.buffer_id, - range: next_region.excerpt.range.clone(), - end_row: if next_region.excerpt.has_trailing_newline { - MultiBufferRow(next_region_end.row - 1) - } else { - MultiBufferRow(next_region_end.row) - }, - }; - - let row = MultiBufferRow(next_region_start.row); - - prev_region = Some(next_region); - - return Some(ExcerptBoundary { row, prev, next }); - } - }) - } - - pub fn edit_count(&self) -> usize { - self.edit_count - } - - pub fn non_text_state_update_count(&self) -> usize { - self.non_text_state_update_count - } - - /// Returns the smallest enclosing bracket ranges containing the given range or - /// None if no brackets contain range or the range is not contained in a single - /// excerpt - /// - /// Can optionally pass a range_filter to filter the ranges of brackets to consider - #[ztracing::instrument(skip_all)] - pub fn innermost_enclosing_bracket_ranges( - &self, - range: Range, - range_filter: Option< - &dyn Fn(&BufferSnapshot, Range, Range) -> bool, - >, - ) -> Option<(Range, Range)> { - let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut excerpt = self.excerpt_containing(range.clone())?; - let buffer = excerpt.buffer(); - let excerpt_buffer_range = excerpt.buffer_range(); - - // Filter to ranges contained in the excerpt - let range_filter = |open: Range, close: Range| -> bool { - excerpt_buffer_range.contains(&BufferOffset(open.start)) - && excerpt_buffer_range.contains(&BufferOffset(close.end)) - && range_filter.is_none_or(|filter| { - filter( - buffer, - BufferOffset(open.start)..BufferOffset(close.end), - BufferOffset(close.start)..BufferOffset(close.end), - ) - }) - }; - - let (open, close) = excerpt.buffer().innermost_enclosing_bracket_ranges( - excerpt.map_range_to_buffer(range), - Some(&range_filter), - )?; - - Some(( - excerpt.map_range_from_buffer(BufferOffset(open.start)..BufferOffset(open.end)), - excerpt.map_range_from_buffer(BufferOffset(close.start)..BufferOffset(close.end)), - )) - } - - /// Returns enclosing bracket ranges containing the given range or returns None if the range is - /// not contained in a single excerpt - pub fn enclosing_bracket_ranges( - &self, - range: Range, - ) -> Option, Range)> + '_> - { - let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut excerpt = self.excerpt_containing(range.clone())?; - - Some( - excerpt - .buffer() - .enclosing_bracket_ranges(excerpt.map_range_to_buffer(range)) - .filter_map(move |pair| { - let open_range = - BufferOffset(pair.open_range.start)..BufferOffset(pair.open_range.end); - let close_range = - BufferOffset(pair.close_range.start)..BufferOffset(pair.close_range.end); - if excerpt.contains_buffer_range(open_range.start..close_range.end) { - Some(( - excerpt.map_range_from_buffer(open_range), - excerpt.map_range_from_buffer(close_range), - )) - } else { - None - } - }), - ) - } - - /// Returns enclosing bracket ranges containing the given range or returns None if the range is - /// not contained in a single excerpt - pub fn text_object_ranges( - &self, - range: Range, - options: TreeSitterOptions, - ) -> impl Iterator, TextObject)> + '_ { - let range = range.start.to_offset(self)..range.end.to_offset(self); - self.excerpt_containing(range.clone()) - .map(|mut excerpt| { - excerpt - .buffer() - .text_object_ranges(excerpt.map_range_to_buffer(range), options) - .filter_map(move |(range, text_object)| { - let range = BufferOffset(range.start)..BufferOffset(range.end); - if excerpt.contains_buffer_range(range.clone()) { - Some((excerpt.map_range_from_buffer(range), text_object)) - } else { - None - } - }) - }) - .into_iter() - .flatten() - } - - /// Returns bracket range pairs overlapping the given `range` or returns None if the `range` is - /// not contained in a single excerpt - pub fn bracket_ranges( - &self, - range: Range, - ) -> Option, Range)> + '_> - { - let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut excerpt = self.excerpt_containing(range.clone())?; - Some( - excerpt - .buffer() - .bracket_ranges(excerpt.map_range_to_buffer(range)) - .filter_map(move |pair| { - let open_range = - BufferOffset(pair.open_range.start)..BufferOffset(pair.open_range.end); - let close_range = - BufferOffset(pair.close_range.start)..BufferOffset(pair.close_range.end); - excerpt - .contains_buffer_range(open_range.start..close_range.end) - .then(|| BracketMatch { - open_range: excerpt.map_range_from_buffer(open_range), - close_range: excerpt.map_range_from_buffer(close_range), - color_index: pair.color_index, - newline_only: pair.newline_only, - syntax_layer_depth: pair.syntax_layer_depth, - }) - }) - .map(BracketMatch::bracket_ranges), - ) - } - - pub fn redacted_ranges<'a, T: ToOffset>( - &'a self, - range: Range, - redaction_enabled: impl Fn(Option<&Arc>) -> bool + 'a, - ) -> impl Iterator> + 'a { - let range = range.start.to_offset(self)..range.end.to_offset(self); - self.lift_buffer_metadata(range, move |buffer, range| { - if redaction_enabled(buffer.file()) { - Some(buffer.redacted_ranges(range).map(|range| (range, ()))) - } else { - None - } - }) - .map(|(range, _, _)| range) - } - - pub fn runnable_ranges( - &self, - range: Range, - ) -> impl Iterator, language::RunnableRange)> + '_ { - let range = range.start.to_offset(self)..range.end.to_offset(self); - self.lift_buffer_metadata(range, move |buffer, range| { - Some( - buffer - .runnable_ranges(range.clone()) - .filter(move |runnable| { - runnable.run_range.start >= range.start - && runnable.run_range.end < range.end - }) - .map(|runnable| (runnable.run_range.clone(), runnable)), - ) - }) - .map(|(run_range, runnable, _)| (run_range, runnable)) - } - - pub fn line_indents( - &self, - start_row: MultiBufferRow, - buffer_filter: impl Fn(&BufferSnapshot) -> bool, - ) -> impl Iterator { - let max_point = self.max_point(); - let mut cursor = self.cursor::(); - cursor.seek(&Point::new(start_row.0, 0)); - iter::from_fn(move || { - let mut region = cursor.region()?; - while !buffer_filter(®ion.excerpt.buffer) { - cursor.next(); - region = cursor.region()?; - } - let overshoot = start_row.0.saturating_sub(region.range.start.row); - let buffer_start_row = - (region.buffer_range.start.row + overshoot).min(region.buffer_range.end.row); - - let buffer_end_row = if region.is_main_buffer - && (region.has_trailing_newline || region.range.end == max_point) - { - region.buffer_range.end.row - } else { - region.buffer_range.end.row.saturating_sub(1) - }; - - let line_indents = region - .buffer - .line_indents_in_row_range(buffer_start_row..buffer_end_row); - cursor.next(); - Some(line_indents.map(move |(buffer_row, indent)| { - let row = region.range.start.row + (buffer_row - region.buffer_range.start.row); - (MultiBufferRow(row), indent, ®ion.excerpt.buffer) - })) - }) - .flatten() - } - - pub fn reversed_line_indents( - &self, - end_row: MultiBufferRow, - buffer_filter: impl Fn(&BufferSnapshot) -> bool, - ) -> impl Iterator { - let max_point = self.max_point(); - let mut cursor = self.cursor::(); - cursor.seek(&Point::new(end_row.0, 0)); - iter::from_fn(move || { - let mut region = cursor.region()?; - while !buffer_filter(®ion.excerpt.buffer) { - cursor.prev(); - region = cursor.region()?; - } - - let buffer_start_row = region.buffer_range.start.row; - let buffer_end_row = if region.is_main_buffer - && (region.has_trailing_newline || region.range.end == max_point) - { - region.buffer_range.end.row + 1 - } else { - region.buffer_range.end.row - }; - - let overshoot = end_row.0 - region.range.start.row; - let buffer_end_row = - (region.buffer_range.start.row + overshoot + 1).min(buffer_end_row); - - let line_indents = region - .buffer - .reversed_line_indents_in_row_range(buffer_start_row..buffer_end_row); - cursor.prev(); - Some(line_indents.map(move |(buffer_row, indent)| { - let row = region.range.start.row + (buffer_row - region.buffer_range.start.row); - (MultiBufferRow(row), indent, ®ion.excerpt.buffer) - })) - }) - .flatten() - } - - pub async fn enclosing_indent( - &self, - mut target_row: MultiBufferRow, - ) -> Option<(Range, LineIndent)> { - let max_row = MultiBufferRow(self.max_point().row); - if target_row >= max_row { - return None; - } - - let mut target_indent = self.line_indent_for_row(target_row); - - // If the current row is at the start of an indented block, we want to return this - // block as the enclosing indent. - if !target_indent.is_line_empty() && target_row < max_row { - let next_line_indent = self.line_indent_for_row(MultiBufferRow(target_row.0 + 1)); - if !next_line_indent.is_line_empty() - && target_indent.raw_len() < next_line_indent.raw_len() - { - target_indent = next_line_indent; - target_row.0 += 1; - } - } - - const SEARCH_ROW_LIMIT: u32 = 25000; - const SEARCH_WHITESPACE_ROW_LIMIT: u32 = 2500; - const YIELD_INTERVAL: u32 = 100; - - let mut accessed_row_counter = 0; - - // If there is a blank line at the current row, search for the next non indented lines - if target_indent.is_line_empty() { - let start = MultiBufferRow(target_row.0.saturating_sub(SEARCH_WHITESPACE_ROW_LIMIT)); - let end = - MultiBufferRow((max_row.0 + 1).min(target_row.0 + SEARCH_WHITESPACE_ROW_LIMIT)); - - let mut non_empty_line_above = None; - for (row, indent, _) in self.reversed_line_indents(target_row, |_| true) { - if row < start { - break; - } - accessed_row_counter += 1; - if accessed_row_counter == YIELD_INTERVAL { - accessed_row_counter = 0; - yield_now().await; - } - if !indent.is_line_empty() { - non_empty_line_above = Some((row, indent)); - break; - } - } - - let mut non_empty_line_below = None; - for (row, indent, _) in self.line_indents(target_row, |_| true) { - if row > end { - break; - } - accessed_row_counter += 1; - if accessed_row_counter == YIELD_INTERVAL { - accessed_row_counter = 0; - yield_now().await; - } - if !indent.is_line_empty() { - non_empty_line_below = Some((row, indent)); - break; - } - } - - let (row, indent) = match (non_empty_line_above, non_empty_line_below) { - (Some((above_row, above_indent)), Some((below_row, below_indent))) => { - if above_indent.raw_len() >= below_indent.raw_len() { - (above_row, above_indent) - } else { - (below_row, below_indent) - } - } - (Some(above), None) => above, - (None, Some(below)) => below, - _ => return None, - }; - - target_indent = indent; - target_row = row; - } - - let start = MultiBufferRow(target_row.0.saturating_sub(SEARCH_ROW_LIMIT)); - let end = MultiBufferRow((max_row.0 + 1).min(target_row.0 + SEARCH_ROW_LIMIT)); - - let mut start_indent = None; - for (row, indent, _) in self.reversed_line_indents(target_row, |_| true) { - if row < start { - break; - } - accessed_row_counter += 1; - if accessed_row_counter == YIELD_INTERVAL { - accessed_row_counter = 0; - yield_now().await; - } - if !indent.is_line_empty() && indent.raw_len() < target_indent.raw_len() { - start_indent = Some((row, indent)); - break; - } - } - let (start_row, start_indent_size) = start_indent?; - - let mut end_indent = (end, None); - for (row, indent, _) in self.line_indents(target_row, |_| true) { - if row > end { - break; - } - accessed_row_counter += 1; - if accessed_row_counter == YIELD_INTERVAL { - accessed_row_counter = 0; - yield_now().await; - } - if !indent.is_line_empty() && indent.raw_len() < target_indent.raw_len() { - end_indent = (MultiBufferRow(row.0.saturating_sub(1)), Some(indent)); - break; - } - } - let (end_row, end_indent_size) = end_indent; - - let indent = if let Some(end_indent_size) = end_indent_size { - if start_indent_size.raw_len() > end_indent_size.raw_len() { - start_indent_size - } else { - end_indent_size - } - } else { - start_indent_size - }; - - Some((start_row..end_row, indent)) - } - - pub fn indent_guides_in_range( - &self, - range: Range, - ignore_disabled_for_language: bool, - cx: &App, - ) -> impl Iterator { - let range = range.start.to_point(self)..range.end.to_point(self); - let start_row = MultiBufferRow(range.start.row); - let end_row = MultiBufferRow(range.end.row); - - let mut row_indents = self.line_indents(start_row, |buffer| { - let settings = - language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx); - settings.indent_guides.enabled || ignore_disabled_for_language - }); - - let mut result = Vec::new(); - let mut indent_stack = SmallVec::<[IndentGuide; 8]>::new(); - - let mut prev_settings = None; - while let Some((first_row, mut line_indent, buffer)) = row_indents.next() { - if first_row > end_row { - break; - } - let current_depth = indent_stack.len() as u32; - - // Avoid retrieving the language settings repeatedly for every buffer row. - if let Some((prev_buffer_id, _)) = &prev_settings - && prev_buffer_id != &buffer.remote_id() - { - prev_settings.take(); - } - let settings = &prev_settings - .get_or_insert_with(|| { - ( - buffer.remote_id(), - language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx), - ) - }) - .1; - let tab_size = settings.tab_size.get(); - - // When encountering empty, continue until found useful line indent - // then add to the indent stack with the depth found - let mut found_indent = false; - let mut last_row = first_row; - if line_indent.is_line_blank() { - while !found_indent { - let Some((target_row, new_line_indent, _)) = row_indents.next() else { - break; - }; - const TRAILING_ROW_SEARCH_LIMIT: u32 = 25; - if target_row > MultiBufferRow(end_row.0 + TRAILING_ROW_SEARCH_LIMIT) { - break; - } - - if new_line_indent.is_line_blank() { - continue; - } - last_row = target_row.min(end_row); - line_indent = new_line_indent; - found_indent = true; - break; - } - } else { - found_indent = true - } - - let depth = if found_indent { - line_indent.len(tab_size) / tab_size - } else { - 0 - }; - - match depth.cmp(¤t_depth) { - cmp::Ordering::Less => { - for _ in 0..(current_depth - depth) { - let mut indent = indent_stack.pop().unwrap(); - if last_row != first_row { - // In this case, we landed on an empty row, had to seek forward, - // and discovered that the indent we where on is ending. - // This means that the last display row must - // be on line that ends this indent range, so we - // should display the range up to the first non-empty line - indent.end_row = MultiBufferRow(first_row.0.saturating_sub(1)); - } - - result.push(indent) - } - } - cmp::Ordering::Greater => { - for next_depth in current_depth..depth { - indent_stack.push(IndentGuide { - buffer_id: buffer.remote_id(), - start_row: first_row, - end_row: last_row, - depth: next_depth, - tab_size, - settings: settings.indent_guides.clone(), - }); - } - } - _ => {} - } - - for indent in indent_stack.iter_mut() { - indent.end_row = last_row; - } - } - - result.extend(indent_stack); - result.into_iter() - } - - pub fn trailing_excerpt_update_count(&self) -> usize { - self.trailing_excerpt_update_count - } - - pub fn file_at(&self, point: T) -> Option<&Arc> { - self.point_to_buffer_offset(point) - .and_then(|(buffer, _)| buffer.file()) - } - - pub fn language_at(&self, offset: T) -> Option<&Arc> { - self.point_to_buffer_offset(offset) - .and_then(|(buffer, offset)| buffer.language_at(offset)) - } - - fn language_settings<'a>(&'a self, cx: &'a App) -> Cow<'a, LanguageSettings> { - self.excerpts - .first() - .map(|excerpt| &excerpt.buffer) - .map(|buffer| { - language_settings( - buffer.language().map(|language| language.name()), - buffer.file(), - cx, - ) - }) - .unwrap_or_else(move || self.language_settings_at(MultiBufferOffset::ZERO, cx)) - } - - pub fn language_settings_at<'a, T: ToOffset>( - &'a self, - point: T, - cx: &'a App, - ) -> Cow<'a, LanguageSettings> { - let mut language = None; - let mut file = None; - if let Some((buffer, offset)) = self.point_to_buffer_offset(point) { - language = buffer.language_at(offset); - file = buffer.file(); - } - language_settings(language.map(|l| l.name()), file, cx) - } - - pub fn language_scope_at(&self, point: T) -> Option { - self.point_to_buffer_offset(point) - .and_then(|(buffer, offset)| buffer.language_scope_at(offset)) - } - - pub fn char_classifier_at(&self, point: T) -> CharClassifier { - self.point_to_buffer_offset(point) - .map(|(buffer, offset)| buffer.char_classifier_at(offset)) - .unwrap_or_default() - } - - pub fn language_indent_size_at( - &self, - position: T, - cx: &App, - ) -> Option { - let (buffer_snapshot, offset) = self.point_to_buffer_offset(position)?; - Some(buffer_snapshot.language_indent_size_at(offset, cx)) - } - - pub fn is_dirty(&self) -> bool { - self.is_dirty - } - - pub fn has_deleted_file(&self) -> bool { - self.has_deleted_file - } - - pub fn has_conflict(&self) -> bool { - self.has_conflict - } - - pub fn has_diagnostics(&self) -> bool { - self.excerpts - .iter() - .any(|excerpt| excerpt.buffer.has_diagnostics()) - } - - pub fn diagnostic_group( - &self, - buffer_id: BufferId, - group_id: usize, - ) -> impl Iterator> + '_ { - self.lift_buffer_metadata::( - Point::zero()..self.max_point(), - move |buffer, range| { - if buffer.remote_id() != buffer_id { - return None; - }; - Some( - buffer - .diagnostics_in_range(range, false) - .filter(move |diagnostic| diagnostic.diagnostic.group_id == group_id) - .map(move |DiagnosticEntryRef { diagnostic, range }| (range, diagnostic)), - ) - }, - ) - .map(|(range, diagnostic, _)| DiagnosticEntryRef { diagnostic, range }) - } - - pub fn diagnostics_in_range<'a, MBD>( - &'a self, - range: Range, - ) -> impl Iterator> + 'a - where - MBD::TextDimension: 'a - + text::ToOffset - + text::FromAnchor - + Sub - + fmt::Debug - + ops::Add - + ops::AddAssign - + Ord, - MBD: MultiBufferDimension - + Ord - + Sub - + ops::Add - + ops::AddAssign - + 'a, - { - self.lift_buffer_metadata::(range, move |buffer, buffer_range| { - Some( - buffer - .diagnostics_in_range(buffer_range.start..buffer_range.end, false) - .map(|entry| (entry.range, entry.diagnostic)), - ) - }) - .map(|(range, diagnostic, _)| DiagnosticEntryRef { diagnostic, range }) - } - - pub fn diagnostics_with_buffer_ids_in_range<'a, MBD>( - &'a self, - range: Range, - ) -> impl Iterator)> + 'a - where - MBD: MultiBufferDimension - + Ord - + Sub - + ops::Add - + ops::AddAssign, - MBD::TextDimension: Sub - + ops::Add - + text::ToOffset - + text::FromAnchor - + AddAssign - + Ord, - { - self.lift_buffer_metadata::(range, move |buffer, buffer_range| { - Some( - buffer - .diagnostics_in_range(buffer_range.start..buffer_range.end, false) - .map(|entry| (entry.range, entry.diagnostic)), - ) - }) - .map(|(range, diagnostic, b)| (b.buffer_id, DiagnosticEntryRef { diagnostic, range })) - } - - pub fn syntax_ancestor( - &self, - range: Range, - ) -> Option<(tree_sitter::Node<'_>, Range)> { - let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut excerpt = self.excerpt_containing(range.clone())?; - let node = excerpt - .buffer() - .syntax_ancestor(excerpt.map_range_to_buffer(range))?; - let node_range = node.byte_range(); - let node_range = BufferOffset(node_range.start)..BufferOffset(node_range.end); - if !excerpt.contains_buffer_range(node_range.clone()) { - return None; - }; - Some((node, excerpt.map_range_from_buffer(node_range))) - } - - pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option> { - let (excerpt_id, _, buffer) = self.as_singleton()?; - let outline = buffer.outline(theme); - Some(Outline::new( - outline - .items - .into_iter() - .flat_map(|item| { - Some(OutlineItem { - depth: item.depth, - range: self.anchor_range_in_excerpt(*excerpt_id, item.range)?, - source_range_for_text: self - .anchor_range_in_excerpt(*excerpt_id, item.source_range_for_text)?, - text: item.text, - highlight_ranges: item.highlight_ranges, - name_ranges: item.name_ranges, - body_range: item.body_range.and_then(|body_range| { - self.anchor_range_in_excerpt(*excerpt_id, body_range) - }), - annotation_range: item.annotation_range.and_then(|annotation_range| { - self.anchor_range_in_excerpt(*excerpt_id, annotation_range) - }), - }) - }) - .collect(), - )) - } - - pub fn symbols_containing( - &self, - offset: T, - theme: Option<&SyntaxTheme>, - ) -> Option<(BufferId, Vec>)> { - let anchor = self.anchor_before(offset); - let excerpt @ &Excerpt { - id: excerpt_id, - buffer_id, - ref buffer, - .. - } = self.excerpt(anchor.excerpt_id)?; - if cfg!(debug_assertions) { - match anchor.text_anchor.buffer_id { - // we clearly are hitting this according to sentry, but in what situations can this occur? - Some(anchor_buffer_id) => { - assert_eq!( - anchor_buffer_id, buffer_id, - "anchor {anchor:?} does not match with resolved excerpt {excerpt:?}" - ) - } - None => assert!(anchor.is_max()), - } - }; - Some(( - buffer_id, - buffer - .symbols_containing(anchor.text_anchor, theme) - .into_iter() - .flat_map(|item| { - Some(OutlineItem { - depth: item.depth, - source_range_for_text: Anchor::range_in_buffer( - excerpt_id, - item.source_range_for_text, - ), - range: Anchor::range_in_buffer(excerpt_id, item.range), - text: item.text, - highlight_ranges: item.highlight_ranges, - name_ranges: item.name_ranges, - body_range: item - .body_range - .map(|body_range| Anchor::range_in_buffer(excerpt_id, body_range)), - annotation_range: item - .annotation_range - .map(|body_range| Anchor::range_in_buffer(excerpt_id, body_range)), - }) - }) - .collect(), - )) - } - - fn excerpt_locator_for_id(&self, id: ExcerptId) -> &Locator { - if id == ExcerptId::min() { - Locator::min_ref() - } else if id == ExcerptId::max() { - Locator::max_ref() - } else { - let (_, _, item) = self.excerpt_ids.find::((), &id, Bias::Left); - if let Some(entry) = item - && entry.id == id - { - return &entry.locator; - } - panic!("invalid excerpt id {id:?}") - } - } - - /// Returns the locators referenced by the given excerpt IDs, sorted by locator. - fn excerpt_locators_for_ids( - &self, - ids: impl IntoIterator, - ) -> SmallVec<[Locator; 1]> { - let mut sorted_ids = ids.into_iter().collect::>(); - sorted_ids.sort_unstable(); - sorted_ids.dedup(); - let mut locators = SmallVec::new(); - - while sorted_ids.last() == Some(&ExcerptId::max()) { - sorted_ids.pop(); - locators.push(Locator::max()); - } - - let mut sorted_ids = sorted_ids.into_iter().peekable(); - locators.extend( - sorted_ids - .peeking_take_while(|excerpt| *excerpt == ExcerptId::min()) - .map(|_| Locator::min()), - ); - - let mut cursor = self.excerpt_ids.cursor::(()); - for id in sorted_ids { - if cursor.seek_forward(&id, Bias::Left) { - locators.push(cursor.item().unwrap().locator.clone()); - } else { - panic!("invalid excerpt id {:?}", id); - } - } - - locators.sort_unstable(); - locators - } - - pub fn buffer_id_for_excerpt(&self, excerpt_id: ExcerptId) -> Option { - Some(self.excerpt(excerpt_id)?.buffer_id) - } - - pub fn buffer_for_excerpt(&self, excerpt_id: ExcerptId) -> Option<&BufferSnapshot> { - Some(&self.excerpt(excerpt_id)?.buffer) - } - - pub fn range_for_excerpt(&self, excerpt_id: ExcerptId) -> Option> { - let mut cursor = self - .excerpts - .cursor::, ExcerptPoint>>(()); - let locator = self.excerpt_locator_for_id(excerpt_id); - let mut sought_exact = cursor.seek(&Some(locator), Bias::Left); - if cursor.item().is_none() && excerpt_id == ExcerptId::max() { - sought_exact = true; - cursor.prev(); - } else if excerpt_id == ExcerptId::min() { - sought_exact = true; - } - if sought_exact { - let start = cursor.start().1; - let end = cursor.end().1; - let mut diff_transforms = self - .diff_transforms - .cursor::>>(()); - diff_transforms.seek(&start, Bias::Left); - let overshoot = start - diff_transforms.start().0; - let start = diff_transforms.start().1 + overshoot; - diff_transforms.seek(&end, Bias::Right); - let overshoot = end - diff_transforms.start().0; - let end = diff_transforms.start().1 + overshoot; - Some(start.0..end.0) - } else { - None - } - } - - /// Returns the excerpt for the given id. The returned excerpt is guaranteed - /// to have the latest excerpt id for the one passed in and will also remap - /// `ExcerptId::max()` to the corresponding excertp ID. - /// - /// Callers of this function should generally use the resulting excerpt's `id` field - /// afterwards. - fn excerpt(&self, excerpt_id: ExcerptId) -> Option<&Excerpt> { - let excerpt_id = self.latest_excerpt_id(excerpt_id); - let mut cursor = self.excerpts.cursor::>(()); - let locator = self.excerpt_locator_for_id(excerpt_id); - cursor.seek(&Some(locator), Bias::Left); - if let Some(excerpt) = cursor.item() - && excerpt.id == excerpt_id - { - return Some(excerpt); - } else if cursor.item().is_none() && excerpt_id == ExcerptId::max() { - cursor.prev(); - return cursor.item(); - } - None - } - - /// Returns the excerpt containing range and its offset start within the multibuffer or none if `range` spans multiple excerpts - pub fn excerpt_containing( - &self, - range: Range, - ) -> Option> { - let range = range.start.to_offset(self)..range.end.to_offset(self); - let mut cursor = self.cursor::(); - cursor.seek(&range.start); - - let start_excerpt = cursor.excerpt()?; - if range.end != range.start { - cursor.seek_forward(&range.end); - if cursor.excerpt()?.id != start_excerpt.id { - return None; - } - } - - cursor.seek_to_start_of_current_excerpt(); - let region = cursor.region()?; - let offset = region.range.start; - let buffer_offset = start_excerpt.buffer_start_offset(); - let excerpt_offset = *cursor.excerpts.start(); - Some(MultiBufferExcerpt { - diff_transforms: cursor.diff_transforms, - excerpt: start_excerpt, - offset, - buffer_offset, - excerpt_offset, - }) - } - - pub fn buffer_id_for_anchor(&self, anchor: Anchor) -> Option { - if let Some(id) = anchor.text_anchor.buffer_id { - return Some(id); - } - let excerpt = self.excerpt_containing(anchor..anchor)?; - Some(excerpt.buffer_id()) - } - - pub fn selections_in_range<'a>( - &'a self, - range: &'a Range, - include_local: bool, - ) -> impl 'a + Iterator)> { - let mut cursor = self.excerpts.cursor::(()); - let start_locator = self.excerpt_locator_for_id(range.start.excerpt_id); - let end_locator = self.excerpt_locator_for_id(range.end.excerpt_id); - cursor.seek(start_locator, Bias::Left); - cursor - .take_while(move |excerpt| excerpt.locator <= *end_locator) - .flat_map(move |excerpt| { - let mut query_range = excerpt.range.context.start..excerpt.range.context.end; - if excerpt.id == range.start.excerpt_id { - query_range.start = range.start.text_anchor; - } - if excerpt.id == range.end.excerpt_id { - query_range.end = range.end.text_anchor; - } - - excerpt - .buffer - .selections_in_range(query_range, include_local) - .flat_map(move |(replica_id, line_mode, cursor_shape, selections)| { - selections.map(move |selection| { - let mut start = Anchor::in_buffer(excerpt.id, selection.start); - let mut end = Anchor::in_buffer(excerpt.id, selection.end); - if range.start.cmp(&start, self).is_gt() { - start = range.start; - } - if range.end.cmp(&end, self).is_lt() { - end = range.end; - } - - ( - replica_id, - line_mode, - cursor_shape, - Selection { - id: selection.id, - start, - end, - reversed: selection.reversed, - goal: selection.goal, - }, - ) - }) - }) - }) - } - - pub fn show_headers(&self) -> bool { - self.show_headers - } - - pub fn diff_for_buffer_id(&self, buffer_id: BufferId) -> Option<&BufferDiffSnapshot> { - self.diffs.get(&buffer_id) - } - - /// Visually annotates a position or range with the `Debug` representation of a value. The - /// callsite of this function is used as a key - previous annotations will be removed. - #[cfg(debug_assertions)] - #[track_caller] - pub fn debug(&self, ranges: &R, value: V) - where - R: debug::ToMultiBufferDebugRanges, - V: std::fmt::Debug, - { - self.debug_with_key(std::panic::Location::caller(), ranges, value); - } - - /// Visually annotates a position or range with the `Debug` representation of a value. Previous - /// debug annotations with the same key will be removed. The key is also used to determine the - /// annotation's color. - #[cfg(debug_assertions)] - #[track_caller] - pub fn debug_with_key(&self, key: &K, ranges: &R, value: V) - where - K: std::hash::Hash + 'static, - R: debug::ToMultiBufferDebugRanges, - V: std::fmt::Debug, - { - let text_ranges = ranges - .to_multi_buffer_debug_ranges(self) - .into_iter() - .flat_map(|range| { - self.range_to_buffer_ranges(range).into_iter().map( - |(buffer, range, _excerpt_id)| { - buffer.anchor_after(range.start)..buffer.anchor_before(range.end) - }, - ) - }) - .collect(); - text::debug::GlobalDebugRanges::with_locked(|debug_ranges| { - debug_ranges.insert(key, text_ranges, format!("{value:?}").into()) - }); - } -} - -#[cfg(any(test, feature = "test-support"))] -impl MultiBufferSnapshot { - pub fn random_byte_range( - &self, - start_offset: MultiBufferOffset, - rng: &mut impl rand::Rng, - ) -> Range { - let end = self.clip_offset(rng.random_range(start_offset..=self.len()), Bias::Right); - let start = self.clip_offset(rng.random_range(start_offset..=end), Bias::Right); - start..end - } - - #[cfg(any(test, feature = "test-support"))] - fn check_invariants(&self) { - let excerpts = self.excerpts.items(()); - let excerpt_ids = self.excerpt_ids.items(()); - - assert!( - self.excerpts.is_empty() || !self.diff_transforms.is_empty(), - "must be at least one diff transform if excerpts exist" - ); - - for (ix, excerpt) in excerpts.iter().enumerate() { - if ix == 0 { - if excerpt.locator <= Locator::min() { - panic!("invalid first excerpt locator {:?}", excerpt.locator); - } - } else if excerpt.locator <= excerpts[ix - 1].locator { - panic!("excerpts are out-of-order: {:?}", excerpts); - } - } - - for (ix, entry) in excerpt_ids.iter().enumerate() { - if ix == 0 { - if entry.id.cmp(&ExcerptId::min(), self).is_le() { - panic!("invalid first excerpt id {:?}", entry.id); - } - } else if entry.id <= excerpt_ids[ix - 1].id { - panic!("excerpt ids are out-of-order: {:?}", excerpt_ids); - } - } - - if self.diff_transforms.summary().input != self.excerpts.summary().text { - panic!( - "incorrect input summary. expected {:#?}, got {:#?}. transforms: {:#?}", - self.excerpts.summary().text, - self.diff_transforms.summary().input, - self.diff_transforms.items(()), - ); - } - - for (left, right) in self.diff_transforms.iter().tuple_windows() { - use sum_tree::Item; - - if left.is_buffer_content() - && left.summary(()).input.len == MultiBufferOffset(0) - && !self.is_empty() - { - panic!("empty buffer content transform in non-empty snapshot"); - } - assert!( - left.clone().merged_with(right).is_none(), - "two consecutive diff transforms could have been merged, but weren't" - ); - } - } -} - -impl<'a, MBD, BD> MultiBufferCursor<'a, MBD, BD> -where - MBD: MultiBufferDimension + Ord + Sub + ops::AddAssign<::Output>, - BD: TextDimension + AddAssign<::Output>, -{ - #[instrument(skip_all)] - fn seek(&mut self, position: &MBD) { - let position = OutputDimension(*position); - self.cached_region.take(); - self.diff_transforms.seek(&position, Bias::Right); - if self.diff_transforms.item().is_none() - && self.diff_transforms.start().output_dimension == position - { - self.diff_transforms.prev(); - } - - let mut excerpt_position = self.diff_transforms.start().excerpt_dimension; - if let Some(item) = self.diff_transforms.item() - && item.is_buffer_content() - { - let overshoot = position - self.diff_transforms.start().output_dimension; - excerpt_position += overshoot; - } - - self.excerpts.seek(&excerpt_position, Bias::Right); - if self.excerpts.item().is_none() && excerpt_position == *self.excerpts.start() { - self.excerpts.prev(); - } - } - - fn seek_forward(&mut self, position: &MBD) { - let position = OutputDimension(*position); - self.cached_region.take(); - self.diff_transforms.seek_forward(&position, Bias::Right); - if self.diff_transforms.item().is_none() - && self.diff_transforms.start().output_dimension == position - { - self.diff_transforms.prev(); - } - - let overshoot = position - self.diff_transforms.start().output_dimension; - let mut excerpt_position = self.diff_transforms.start().excerpt_dimension; - if let Some(item) = self.diff_transforms.item() - && item.is_buffer_content() - { - excerpt_position += overshoot; - } - - self.excerpts.seek_forward(&excerpt_position, Bias::Right); - if self.excerpts.item().is_none() && excerpt_position == *self.excerpts.start() { - self.excerpts.prev(); - } - } - - fn next_excerpt(&mut self) { - self.excerpts.next(); - self.seek_to_start_of_current_excerpt(); - } - - fn prev_excerpt(&mut self) { - self.excerpts.prev(); - self.seek_to_start_of_current_excerpt(); - } - - fn seek_to_start_of_current_excerpt(&mut self) { - self.cached_region.take(); - self.diff_transforms.seek(self.excerpts.start(), Bias::Left); - if self.diff_transforms.end().excerpt_dimension == *self.excerpts.start() - && self.diff_transforms.start().excerpt_dimension < *self.excerpts.start() - && self.diff_transforms.next_item().is_some() - { - self.diff_transforms.next(); - } - } - - fn next(&mut self) { - self.cached_region.take(); - match self - .diff_transforms - .end() - .excerpt_dimension - .cmp(&self.excerpts.end()) - { - cmp::Ordering::Less => { - self.diff_transforms.next(); - } - cmp::Ordering::Greater => { - self.excerpts.next(); - } - cmp::Ordering::Equal => { - self.diff_transforms.next(); - if self.diff_transforms.end().excerpt_dimension > self.excerpts.end() - || self.diff_transforms.item().is_none() - { - self.excerpts.next(); - } else if let Some(DiffTransform::DeletedHunk { hunk_info, .. }) = - self.diff_transforms.item() - && self - .excerpts - .item() - .is_some_and(|excerpt| excerpt.id != hunk_info.excerpt_id) - { - self.excerpts.next(); - } - } - } - } - - fn prev(&mut self) { - self.cached_region.take(); - match self - .diff_transforms - .start() - .excerpt_dimension - .cmp(self.excerpts.start()) - { - cmp::Ordering::Less => self.excerpts.prev(), - cmp::Ordering::Greater => self.diff_transforms.prev(), - cmp::Ordering::Equal => { - self.diff_transforms.prev(); - if self.diff_transforms.start().excerpt_dimension < *self.excerpts.start() - || self.diff_transforms.item().is_none() - { - self.excerpts.prev(); - } - } - } - } - - fn region(&mut self) -> Option> { - if self.cached_region.is_none() { - self.cached_region = self.build_region(); - } - self.cached_region.clone() - } - - fn is_at_start_of_excerpt(&mut self) -> bool { - if self.diff_transforms.start().excerpt_dimension > *self.excerpts.start() { - return false; - } else if self.diff_transforms.start().excerpt_dimension < *self.excerpts.start() { - return true; - } - - self.diff_transforms.prev(); - let prev_transform = self.diff_transforms.item(); - self.diff_transforms.next(); - - prev_transform.is_none_or(|prev_transform| prev_transform.is_buffer_content()) - } - - fn is_at_end_of_excerpt(&mut self) -> bool { - if self.diff_transforms.end().excerpt_dimension < self.excerpts.end() { - return false; - } else if self.diff_transforms.end().excerpt_dimension > self.excerpts.end() - || self.diff_transforms.item().is_none() - { - return true; - } - - let next_transform = self.diff_transforms.next_item(); - next_transform.is_none_or(|next_transform| match next_transform { - DiffTransform::Unmodified { .. } - | DiffTransform::InsertedHunk { .. } - | DiffTransform::FilteredInsertedHunk { .. } => true, - DiffTransform::DeletedHunk { hunk_info, .. } => self - .excerpts - .item() - .is_some_and(|excerpt| excerpt.id != hunk_info.excerpt_id), - }) - } - - fn main_buffer_position(&self) -> Option { - let excerpt = self.excerpts.item()?; - let buffer = &excerpt.buffer; - let buffer_context_start = excerpt.range.context.start.summary::(buffer); - let mut buffer_start = buffer_context_start; - let overshoot = self.diff_transforms.end().excerpt_dimension - *self.excerpts.start(); - buffer_start += overshoot; - Some(buffer_start) - } - - fn build_region(&self) -> Option> { - let excerpt = self.excerpts.item()?; - match self.diff_transforms.item()? { - DiffTransform::DeletedHunk { - buffer_id, - has_trailing_newline, - hunk_info, - .. - } => { - let diff = self.snapshot.diffs.get(buffer_id)?; - let buffer = diff.base_text(); - let mut rope_cursor = buffer.as_rope().cursor(0); - let buffer_start = rope_cursor.summary::(hunk_info.base_text_byte_range.start); - let buffer_range_len = - rope_cursor.summary::(hunk_info.base_text_byte_range.end); - let mut buffer_end = buffer_start; - TextDimension::add_assign(&mut buffer_end, &buffer_range_len); - let start = self.diff_transforms.start().output_dimension.0; - let end = self.diff_transforms.end().output_dimension.0; - - Some(MultiBufferRegion { - buffer, - excerpt, - has_trailing_newline: *has_trailing_newline, - is_main_buffer: false, - diff_hunk_status: Some(DiffHunkStatus::deleted( - hunk_info.hunk_secondary_status, - )), - buffer_range: buffer_start..buffer_end, - range: start..end, - diff_base_byte_range: Some(hunk_info.base_text_byte_range.clone()), - }) - } - transform @ (DiffTransform::Unmodified { .. } - | DiffTransform::InsertedHunk { .. } - | DiffTransform::FilteredInsertedHunk { .. }) => { - let mut diff_hunk_status = transform - .hunk_info() - .map(|hunk_info| DiffHunkStatus::added(hunk_info.hunk_secondary_status)); - - let diff_base_byte_range = transform - .hunk_info() - .map(|hunk_info| hunk_info.base_text_byte_range); - - let buffer = &excerpt.buffer; - let buffer_context_start = excerpt.range.context.start.summary::(buffer); - - let mut start = self.diff_transforms.start().output_dimension.0; - let mut buffer_start = buffer_context_start; - if self.diff_transforms.start().excerpt_dimension < *self.excerpts.start() { - let overshoot = - *self.excerpts.start() - self.diff_transforms.start().excerpt_dimension; - start += overshoot; - } else { - let overshoot = - self.diff_transforms.start().excerpt_dimension - *self.excerpts.start(); - buffer_start += overshoot; - } - - let mut end; - let mut buffer_end; - let has_trailing_newline; - if self.diff_transforms.end().excerpt_dimension < self.excerpts.end() { - let overshoot = - self.diff_transforms.end().excerpt_dimension - *self.excerpts.start(); - end = self.diff_transforms.end().output_dimension.0; - buffer_end = buffer_context_start; - buffer_end += overshoot; - has_trailing_newline = false; - } else { - let overshoot = - self.excerpts.end() - self.diff_transforms.start().excerpt_dimension; - end = self.diff_transforms.start().output_dimension.0; - end += overshoot; - buffer_end = excerpt.range.context.end.summary::(buffer); - has_trailing_newline = excerpt.has_trailing_newline; - }; - - if matches!(transform, DiffTransform::FilteredInsertedHunk { .. }) { - buffer_end = buffer_start; - end = start; - diff_hunk_status = None; - } - - Some(MultiBufferRegion { - buffer, - excerpt, - has_trailing_newline, - is_main_buffer: true, - diff_hunk_status, - diff_base_byte_range, - buffer_range: buffer_start..buffer_end, - range: start..end, - }) - } - } - } - - fn excerpt(&self) -> Option<&'a Excerpt> { - self.excerpts.item() - } -} - -impl Excerpt { - fn new( - id: ExcerptId, - locator: Locator, - buffer_id: BufferId, - buffer: BufferSnapshot, - range: ExcerptRange, - has_trailing_newline: bool, - ) -> Self { - Excerpt { - id, - locator, - max_buffer_row: range.context.end.to_point(&buffer).row, - text_summary: buffer - .text_summary_for_range::(range.context.to_offset(&buffer)), - buffer_id, - buffer, - range, - has_trailing_newline, - } - } - - fn chunks_in_range(&self, range: Range, language_aware: bool) -> ExcerptChunks<'_> { - let content_start = self.range.context.start.to_offset(&self.buffer); - let chunks_start = content_start + range.start; - let chunks_end = content_start + cmp::min(range.end, self.text_summary.len); - - let has_footer = self.has_trailing_newline - && range.start <= self.text_summary.len - && range.end > self.text_summary.len; - - let content_chunks = self.buffer.chunks(chunks_start..chunks_end, language_aware); - - ExcerptChunks { - excerpt_id: self.id, - content_chunks, - has_footer, - } - } - - fn seek_chunks(&self, excerpt_chunks: &mut ExcerptChunks, range: Range) { - let content_start = self.range.context.start.to_offset(&self.buffer); - let chunks_start = content_start + range.start; - let chunks_end = content_start + cmp::min(range.end, self.text_summary.len); - excerpt_chunks.content_chunks.seek(chunks_start..chunks_end); - excerpt_chunks.has_footer = self.has_trailing_newline - && range.start <= self.text_summary.len - && range.end > self.text_summary.len; - } - - fn clip_anchor(&self, text_anchor: text::Anchor) -> text::Anchor { - if text_anchor - .cmp(&self.range.context.start, &self.buffer) - .is_lt() - { - self.range.context.start - } else if text_anchor - .cmp(&self.range.context.end, &self.buffer) - .is_gt() - { - self.range.context.end - } else { - text_anchor - } - } - - fn contains(&self, anchor: &Anchor) -> bool { - (anchor.text_anchor.buffer_id == None - || anchor.text_anchor.buffer_id == Some(self.buffer_id)) - && self - .range - .context - .start - .cmp(&anchor.text_anchor, &self.buffer) - .is_le() - && self - .range - .context - .end - .cmp(&anchor.text_anchor, &self.buffer) - .is_ge() - } - - /// The [`Excerpt`]'s start offset in its [`Buffer`] - fn buffer_start_offset(&self) -> BufferOffset { - BufferOffset(self.range.context.start.to_offset(&self.buffer)) - } - - /// The [`Excerpt`]'s end offset in its [`Buffer`] - fn buffer_end_offset(&self) -> BufferOffset { - self.buffer_start_offset() + self.text_summary.len - } -} - -impl<'a> MultiBufferExcerpt<'a> { - pub fn id(&self) -> ExcerptId { - self.excerpt.id - } - - pub fn buffer_id(&self) -> BufferId { - self.excerpt.buffer_id - } - - pub fn start_anchor(&self) -> Anchor { - Anchor::in_buffer(self.excerpt.id, self.excerpt.range.context.start) - } - - pub fn end_anchor(&self) -> Anchor { - Anchor::in_buffer(self.excerpt.id, self.excerpt.range.context.end) - } - - pub fn buffer(&self) -> &'a BufferSnapshot { - &self.excerpt.buffer - } - - pub fn buffer_range(&self) -> Range { - self.buffer_offset - ..BufferOffset( - self.excerpt - .range - .context - .end - .to_offset(&self.excerpt.buffer.text), - ) - } - - pub fn start_offset(&self) -> MultiBufferOffset { - self.offset - } - - /// Maps an offset within the [`MultiBuffer`] to an offset within the [`Buffer`] - pub fn map_offset_to_buffer(&mut self, offset: MultiBufferOffset) -> BufferOffset { - self.map_range_to_buffer(offset..offset).start - } - - /// Maps a range within the [`MultiBuffer`] to a range within the [`Buffer`] - pub fn map_range_to_buffer(&mut self, range: Range) -> Range { - self.diff_transforms - .seek(&OutputDimension(range.start), Bias::Right); - let start = self.map_offset_to_buffer_internal(range.start); - let end = if range.end > range.start { - self.diff_transforms - .seek_forward(&OutputDimension(range.end), Bias::Right); - self.map_offset_to_buffer_internal(range.end) - } else { - start - }; - start..end - } - - fn map_offset_to_buffer_internal(&self, offset: MultiBufferOffset) -> BufferOffset { - let mut excerpt_offset = self.diff_transforms.start().excerpt_dimension; - if self - .diff_transforms - .item() - .is_some_and(|t| t.is_buffer_content()) - { - excerpt_offset += offset - self.diff_transforms.start().output_dimension.0; - }; - let offset_in_excerpt = excerpt_offset.saturating_sub(self.excerpt_offset); - self.buffer_offset + offset_in_excerpt - } - - /// Map an offset within the [`Buffer`] to an offset within the [`MultiBuffer`] - pub fn map_offset_from_buffer(&mut self, buffer_offset: BufferOffset) -> MultiBufferOffset { - self.map_range_from_buffer(buffer_offset..buffer_offset) - .start - } - - /// Map a range within the [`Buffer`] to a range within the [`MultiBuffer`] - pub fn map_range_from_buffer( - &mut self, - buffer_range: Range, - ) -> Range { - if buffer_range.start < self.buffer_offset { - log::warn!( - "Attempting to map a range from a buffer offset that starts before the current buffer offset" - ); - return self.offset..self.offset; - } - let overshoot = buffer_range.start - self.buffer_offset; - let excerpt_offset = self.excerpt_offset + overshoot; - let excerpt_seek_dim = excerpt_offset; - self.diff_transforms.seek(&excerpt_seek_dim, Bias::Right); - if self.diff_transforms.start().excerpt_dimension > excerpt_offset { - log::warn!( - "Attempting to map a range from a buffer offset that starts before the current buffer offset" - ); - return self.offset..self.offset; - } - let overshoot = excerpt_offset - self.diff_transforms.start().excerpt_dimension; - let start = self.diff_transforms.start().output_dimension.0 + overshoot; - - let end = if buffer_range.start < buffer_range.end { - let overshoot = buffer_range.end - self.buffer_offset; - let excerpt_offset = self.excerpt_offset + overshoot; - let excerpt_seek_dim = excerpt_offset; - self.diff_transforms - .seek_forward(&excerpt_seek_dim, Bias::Right); - let overshoot = excerpt_offset - self.diff_transforms.start().excerpt_dimension; - // todo(lw): Clamp end to the excerpt boundaries - self.diff_transforms.start().output_dimension.0 + overshoot - } else { - start - }; - - start..end - } - - /// Returns true if the entirety of the given range is in the buffer's excerpt - pub fn contains_buffer_range(&self, range: Range) -> bool { - range.start >= self.excerpt.buffer_start_offset() - && range.end <= self.excerpt.buffer_end_offset() - } - - pub fn max_buffer_row(&self) -> u32 { - self.excerpt.max_buffer_row - } -} - -impl ExcerptId { - pub fn min() -> Self { - Self(0) - } - - pub fn max() -> Self { - Self(u32::MAX) - } - - pub fn to_proto(self) -> u64 { - self.0 as _ - } - - pub fn from_proto(proto: u64) -> Self { - Self(proto as _) - } - - pub fn cmp(&self, other: &Self, snapshot: &MultiBufferSnapshot) -> cmp::Ordering { - let a = snapshot.excerpt_locator_for_id(*self); - let b = snapshot.excerpt_locator_for_id(*other); - a.cmp(b).then_with(|| self.0.cmp(&other.0)) - } -} - -impl From for usize { - fn from(val: ExcerptId) -> Self { - val.0 as usize - } -} - -impl fmt::Debug for Excerpt { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("Excerpt") - .field("id", &self.id) - .field("locator", &self.locator) - .field("buffer_id", &self.buffer_id) - .field("range", &self.range) - .field("text_summary", &self.text_summary) - .field("has_trailing_newline", &self.has_trailing_newline) - .finish() - } -} - -impl sum_tree::Item for Excerpt { - type Summary = ExcerptSummary; - - fn summary(&self, _cx: ()) -> Self::Summary { - let mut text = self.text_summary; - if self.has_trailing_newline { - text += TextSummary::from("\n"); - } - ExcerptSummary { - excerpt_id: self.id, - excerpt_locator: self.locator.clone(), - widest_line_number: self.max_buffer_row, - text: text.into(), - } - } -} - -impl sum_tree::Item for ExcerptIdMapping { - type Summary = ExcerptId; - - fn summary(&self, _cx: ()) -> Self::Summary { - self.id - } -} - -impl sum_tree::KeyedItem for ExcerptIdMapping { - type Key = ExcerptId; - - fn key(&self) -> Self::Key { - self.id - } -} - -impl DiffTransform { - fn hunk_info(&self) -> Option { - match self { - DiffTransform::DeletedHunk { hunk_info, .. } - | DiffTransform::InsertedHunk { hunk_info, .. } - | DiffTransform::FilteredInsertedHunk { hunk_info, .. } => Some(hunk_info.clone()), - DiffTransform::Unmodified { .. } => None, - } - } - - fn is_buffer_content(&self) -> bool { - match self { - Self::Unmodified { .. } - | Self::InsertedHunk { .. } - | Self::FilteredInsertedHunk { .. } => true, - Self::DeletedHunk { .. } => false, - } - } -} - -impl sum_tree::Item for DiffTransform { - type Summary = DiffTransformSummary; - - fn summary(&self, _: ::Context<'_>) -> Self::Summary { - match self { - DiffTransform::InsertedHunk { summary, .. } - | DiffTransform::Unmodified { summary, .. } => DiffTransformSummary { - input: *summary, - output: *summary, - }, - &DiffTransform::DeletedHunk { summary, .. } => DiffTransformSummary { - input: MBTextSummary::default(), - output: summary.into(), - }, - DiffTransform::FilteredInsertedHunk { summary, .. } => DiffTransformSummary { - input: *summary, - output: MBTextSummary::default(), - }, - } - } -} - -impl DiffTransformSummary { - fn excerpt_len(&self) -> ExcerptOffset { - ExcerptDimension(self.input.len) - } -} - -impl sum_tree::ContextLessSummary for DiffTransformSummary { - fn zero() -> Self { - DiffTransformSummary { - input: MBTextSummary::default(), - output: MBTextSummary::default(), - } - } - - fn add_summary(&mut self, other: &Self) { - self.input += other.input; - self.output += other.output; - } -} - -impl sum_tree::ContextLessSummary for ExcerptId { - fn zero() -> Self { - Self(0) - } - - fn add_summary(&mut self, summary: &Self) { - *self = cmp::max(*self, *summary); - } -} - -impl sum_tree::ContextLessSummary for ExcerptSummary { - fn zero() -> Self { - Self::default() - } - - fn add_summary(&mut self, summary: &Self) { - debug_assert!(summary.excerpt_locator > self.excerpt_locator); - self.excerpt_locator = summary.excerpt_locator.clone(); - self.text += summary.text; - self.widest_line_number = cmp::max(self.widest_line_number, summary.widest_line_number); - } -} - -impl<'a> sum_tree::SeekTarget<'a, ExcerptSummary, Option<&'a Locator>> for Locator { - fn cmp(&self, cursor_location: &Option<&'a Locator>, _: ()) -> cmp::Ordering { - Ord::cmp(&Some(self), cursor_location) - } -} - -impl sum_tree::SeekTarget<'_, ExcerptSummary, ExcerptSummary> for Locator { - fn cmp(&self, cursor_location: &ExcerptSummary, _: ()) -> cmp::Ordering { - Ord::cmp(self, &cursor_location.excerpt_locator) - } -} - -impl<'a, MBD> sum_tree::Dimension<'a, ExcerptSummary> for ExcerptDimension -where - MBD: MultiBufferDimension + Default, -{ - fn zero(_: ()) -> Self { - ExcerptDimension(MBD::default()) - } - - fn add_summary(&mut self, summary: &'a ExcerptSummary, _: ()) { - MultiBufferDimension::add_mb_text_summary(&mut self.0, &summary.text) - } -} - -impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Option<&'a Locator> { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a ExcerptSummary, _: ()) { - *self = Some(&summary.excerpt_locator); - } -} - -impl<'a> sum_tree::Dimension<'a, ExcerptSummary> for Option { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a ExcerptSummary, _: ()) { - *self = Some(summary.excerpt_id); - } -} - -#[derive(Copy, Clone, PartialOrd, Ord, Eq, PartialEq, Debug)] -struct OutputDimension(T); - -impl PartialEq for OutputDimension { - fn eq(&self, other: &T) -> bool { - self.0 == *other - } -} - -impl PartialOrd for OutputDimension { - fn partial_cmp(&self, other: &T) -> Option { - self.0.partial_cmp(other) - } -} - -impl ops::Sub> for OutputDimension -where - T: ops::Sub, -{ - type Output = R; - - fn sub(self, other: OutputDimension) -> Self::Output { - self.0 - other.0 - } -} - -impl ops::Add for OutputDimension -where - T: ops::Add, -{ - type Output = OutputDimension; - - fn add(self, other: U) -> Self::Output { - OutputDimension(self.0 + other) - } -} - -impl AddAssign for OutputDimension -where - T: AddAssign, -{ - fn add_assign(&mut self, other: U) { - self.0 += other; - } -} - -impl SubAssign for OutputDimension -where - T: SubAssign, -{ - fn sub_assign(&mut self, other: U) { - self.0 -= other; - } -} - -#[derive(Copy, Clone, PartialOrd, Ord, Eq, PartialEq, Debug)] -struct ExcerptDimension(T); - -impl PartialEq for ExcerptDimension { - fn eq(&self, other: &T) -> bool { - self.0 == *other - } -} - -impl PartialOrd for ExcerptDimension { - fn partial_cmp(&self, other: &T) -> Option { - self.0.partial_cmp(other) - } -} - -impl ExcerptOffset { - fn saturating_sub(self, other: ExcerptOffset) -> usize { - self.0.saturating_sub(other.0) - } -} - -impl ops::Sub> for ExcerptDimension -where - T: ops::Sub, -{ - type Output = R; - - fn sub(self, other: ExcerptDimension) -> Self::Output { - self.0 - other.0 - } -} - -impl ops::Add for ExcerptDimension -where - T: ops::Add, -{ - type Output = ExcerptDimension; - - fn add(self, other: U) -> Self::Output { - ExcerptDimension(self.0 + other) - } -} - -impl AddAssign for ExcerptDimension -where - T: AddAssign, -{ - fn add_assign(&mut self, other: U) { - self.0 += other; - } -} - -impl SubAssign for ExcerptDimension -where - T: SubAssign, -{ - fn sub_assign(&mut self, other: U) { - self.0 -= other; - } -} - -impl<'a> sum_tree::Dimension<'a, DiffTransformSummary> for MultiBufferOffset { - fn zero(_: ()) -> Self { - MultiBufferOffset::ZERO - } - - fn add_summary(&mut self, summary: &'a DiffTransformSummary, _: ()) { - *self += summary.output.len; - } -} - -impl sum_tree::SeekTarget<'_, DiffTransformSummary, DiffTransformSummary> - for ExcerptDimension -where - MBD: MultiBufferDimension + Ord, -{ - fn cmp(&self, cursor_location: &DiffTransformSummary, _: ()) -> cmp::Ordering { - Ord::cmp(&self.0, &MBD::from_summary(&cursor_location.input)) - } -} - -impl<'a, MBD> sum_tree::SeekTarget<'a, DiffTransformSummary, DiffTransforms> - for ExcerptDimension -where - MBD: MultiBufferDimension + Ord, -{ - fn cmp(&self, cursor_location: &DiffTransforms, _: ()) -> cmp::Ordering { - Ord::cmp(&self.0, &cursor_location.excerpt_dimension.0) - } -} - -impl<'a, MBD: MultiBufferDimension> sum_tree::Dimension<'a, DiffTransformSummary> - for ExcerptDimension -{ - fn zero(_: ()) -> Self { - ExcerptDimension(MBD::default()) - } - - fn add_summary(&mut self, summary: &'a DiffTransformSummary, _: ()) { - self.0.add_mb_text_summary(&summary.input) - } -} - -impl<'a, MBD> sum_tree::SeekTarget<'a, DiffTransformSummary, DiffTransforms> - for OutputDimension -where - MBD: MultiBufferDimension + Ord, -{ - fn cmp(&self, cursor_location: &DiffTransforms, _: ()) -> cmp::Ordering { - Ord::cmp(&self.0, &cursor_location.output_dimension.0) - } -} - -impl<'a, MBD: MultiBufferDimension> sum_tree::Dimension<'a, DiffTransformSummary> - for OutputDimension -{ - fn zero(_: ()) -> Self { - OutputDimension(MBD::default()) - } - - fn add_summary(&mut self, summary: &'a DiffTransformSummary, _: ()) { - self.0.add_mb_text_summary(&summary.output) - } -} - -impl MultiBufferRows<'_> { - pub fn seek(&mut self, MultiBufferRow(row): MultiBufferRow) { - self.point = Point::new(row, 0); - self.cursor.seek(&self.point); - } -} - -impl Iterator for MultiBufferRows<'_> { - type Item = RowInfo; - - fn next(&mut self) -> Option { - if self.is_empty && self.point.row == 0 { - self.point += Point::new(1, 0); - return Some(RowInfo { - buffer_id: None, - buffer_row: Some(0), - base_text_row: Some(BaseTextRow(0)), - multibuffer_row: Some(MultiBufferRow(0)), - diff_status: None, - expand_info: None, - wrapped_buffer_row: None, - }); - } - - let mut region = self.cursor.region()?; - while self.point >= region.range.end { - self.cursor.next(); - if let Some(next_region) = self.cursor.region() { - region = next_region; - } else if self.point == self.cursor.diff_transforms.end().output_dimension.0 { - let multibuffer_row = MultiBufferRow(self.point.row); - let last_excerpt = self - .cursor - .excerpts - .item() - .or(self.cursor.excerpts.prev_item())?; - let last_row = last_excerpt - .range - .context - .end - .to_point(&last_excerpt.buffer) - .row; - // TODO(split-diff) perf - let base_text_row = self - .cursor - .snapshot - .diffs - .get(&last_excerpt.buffer_id) - .map(|diff| diff.row_to_base_text_row(last_row, &last_excerpt.buffer)) - .map(BaseTextRow); - - let first_row = last_excerpt - .range - .context - .start - .to_point(&last_excerpt.buffer) - .row; - - let expand_info = if self.is_singleton { - None - } else { - let needs_expand_up = first_row == last_row - && (last_row > 0) - && !region.diff_hunk_status.is_some_and(|d| d.is_deleted()) - && !(region.is_filtered() - && region - .diff_base_byte_range - .is_some_and(|range| !range.is_empty())); - let needs_expand_down = last_row < last_excerpt.buffer.max_point().row; - - if needs_expand_up && needs_expand_down { - Some(ExpandExcerptDirection::UpAndDown) - } else if needs_expand_up { - Some(ExpandExcerptDirection::Up) - } else if needs_expand_down { - Some(ExpandExcerptDirection::Down) - } else { - None - } - .map(|direction| ExpandInfo { - direction, - excerpt_id: last_excerpt.id, - }) - }; - self.point += Point::new(1, 0); - return Some(RowInfo { - buffer_id: Some(last_excerpt.buffer_id), - buffer_row: Some(last_row), - base_text_row, - multibuffer_row: Some(multibuffer_row), - diff_status: None, - wrapped_buffer_row: None, - expand_info, - }); - } else { - return None; - }; - } - - let overshoot = self.point - region.range.start; - let buffer_point = region.buffer_range.start + overshoot; - let diff_status = region - .diff_hunk_status - .filter(|_| self.point < region.range.end); - let base_text_row = match diff_status { - // TODO(split-diff) perf - None => self - .cursor - .snapshot - .diffs - .get(®ion.excerpt.buffer_id) - .map(|diff| diff.row_to_base_text_row(buffer_point.row, ®ion.buffer)) - .map(BaseTextRow), - Some(DiffHunkStatus { - kind: DiffHunkStatusKind::Added, - .. - }) => None, - Some(DiffHunkStatus { - kind: DiffHunkStatusKind::Deleted, - .. - }) => Some(BaseTextRow(buffer_point.row)), - Some(DiffHunkStatus { - kind: DiffHunkStatusKind::Modified, - .. - }) => unreachable!(), - }; - let expand_info = if self.is_singleton { - None - } else { - let needs_expand_up = self.point.row == region.range.start.row - && self.cursor.is_at_start_of_excerpt() - && buffer_point.row > 0; - let needs_expand_down = (region.excerpt.has_trailing_newline - && self.point.row + 1 == region.range.end.row - || !region.excerpt.has_trailing_newline && self.point.row == region.range.end.row) - && self.cursor.is_at_end_of_excerpt() - && buffer_point.row < region.buffer.max_point().row; - - if needs_expand_up && needs_expand_down { - Some(ExpandExcerptDirection::UpAndDown) - } else if needs_expand_up { - Some(ExpandExcerptDirection::Up) - } else if needs_expand_down { - Some(ExpandExcerptDirection::Down) - } else { - None - } - .map(|direction| ExpandInfo { - direction, - excerpt_id: region.excerpt.id, - }) - }; - - let result = Some(RowInfo { - buffer_id: Some(region.buffer.remote_id()), - buffer_row: Some(buffer_point.row), - base_text_row, - multibuffer_row: Some(MultiBufferRow(self.point.row)), - diff_status, - expand_info, - wrapped_buffer_row: None, - }); - self.point += Point::new(1, 0); - result - } -} - -impl<'a> MultiBufferChunks<'a> { - pub fn offset(&self) -> MultiBufferOffset { - self.range.start - } - - pub fn seek(&mut self, range: Range) { - self.diff_transforms.seek(&range.end, Bias::Right); - let mut excerpt_end = self.diff_transforms.start().1; - if self - .diff_transforms - .item() - .is_some_and(|t| t.is_buffer_content()) - { - let overshoot = range.end - self.diff_transforms.start().0; - excerpt_end += overshoot; - } - - self.diff_transforms.seek(&range.start, Bias::Right); - let mut excerpt_start = self.diff_transforms.start().1; - if self - .diff_transforms - .item() - .is_some_and(|t| t.is_buffer_content()) - { - let overshoot = range.start - self.diff_transforms.start().0; - excerpt_start += overshoot; - } - - self.seek_to_excerpt_offset_range(excerpt_start..excerpt_end); - self.buffer_chunk.take(); - self.range = range; - } - - fn seek_to_excerpt_offset_range(&mut self, new_range: Range) { - self.excerpt_offset_range = new_range.clone(); - self.excerpts.seek(&new_range.start, Bias::Right); - if let Some(excerpt) = self.excerpts.item() { - let excerpt_start = *self.excerpts.start(); - if let Some(excerpt_chunks) = self - .excerpt_chunks - .as_mut() - .filter(|chunks| excerpt.id == chunks.excerpt_id) - { - excerpt.seek_chunks( - excerpt_chunks, - (self.excerpt_offset_range.start - excerpt_start) - ..(self.excerpt_offset_range.end - excerpt_start), - ); - } else { - self.excerpt_chunks = Some(excerpt.chunks_in_range( - (self.excerpt_offset_range.start - excerpt_start) - ..(self.excerpt_offset_range.end - excerpt_start), - self.language_aware, - )); - } - } else { - self.excerpt_chunks = None; - } - } - - fn next_excerpt_chunk(&mut self) -> Option> { - loop { - if self.excerpt_offset_range.is_empty() { - return None; - } else if let Some(chunk) = self.excerpt_chunks.as_mut()?.next() { - self.excerpt_offset_range.start += chunk.text.len(); - return Some(chunk); - } else { - self.excerpts.next(); - let excerpt = self.excerpts.item()?; - self.excerpt_chunks = Some(excerpt.chunks_in_range( - 0..(self.excerpt_offset_range.end - *self.excerpts.start()), - self.language_aware, - )); - } - } - } -} - -impl<'a> Iterator for ReversedMultiBufferChunks<'a> { - type Item = &'a str; - - fn next(&mut self) -> Option { - let mut region = self.cursor.region()?; - if self.offset == region.range.start { - self.cursor.prev(); - while let Some(region) = self.cursor.region() - && region.buffer_range.is_empty() - && !region.has_trailing_newline - { - self.cursor.prev(); - } - region = self.cursor.region()?; - let start_overshoot = self.start.saturating_sub(region.range.start); - self.current_chunks = Some(region.buffer.reversed_chunks_in_range( - region.buffer_range.start + start_overshoot..region.buffer_range.end, - )); - } - - if self.offset == region.range.end && region.has_trailing_newline { - self.offset -= 1; - Some("\n") - } else { - let chunk = self.current_chunks.as_mut().unwrap().next()?; - self.offset -= chunk.len(); - Some(chunk) - } - } -} - -impl<'a> Iterator for MultiBufferChunks<'a> { - type Item = Chunk<'a>; - - fn next(&mut self) -> Option> { - if self.range.start >= self.range.end { - return None; - } - if self.range.start == self.diff_transforms.end().0 { - self.diff_transforms.next(); - } - while let Some(DiffTransform::FilteredInsertedHunk { .. }) = self.diff_transforms.item() { - self.diff_transforms.next(); - let mut range = self.excerpt_offset_range.clone(); - range.start = self.diff_transforms.start().1; - self.seek_to_excerpt_offset_range(range); - self.buffer_chunk.take(); - } - - let diff_transform_start = self.diff_transforms.start().0; - let diff_transform_end = self.diff_transforms.end().0; - debug_assert!( - self.range.start < diff_transform_end, - "{:?} < {:?} of ({1:?}..{2:?})", - self.range.start, - diff_transform_end, - diff_transform_start - ); - - let diff_transform = self.diff_transforms.item()?; - match diff_transform { - DiffTransform::Unmodified { .. } - | DiffTransform::InsertedHunk { .. } - | DiffTransform::FilteredInsertedHunk { .. } => { - let chunk = if let Some(chunk) = &mut self.buffer_chunk { - chunk - } else { - let chunk = self.next_excerpt_chunk().unwrap(); - self.buffer_chunk.insert(chunk) - }; - - let chunk_end = self.range.start + chunk.text.len(); - let diff_transform_end = diff_transform_end.min(self.range.end); - - if diff_transform_end < chunk_end { - let split_idx = diff_transform_end - self.range.start; - let (before, after) = chunk.text.split_at(split_idx); - self.range.start = diff_transform_end; - let mask = 1u128.unbounded_shl(split_idx as u32).wrapping_sub(1); - let chars = chunk.chars & mask; - let tabs = chunk.tabs & mask; - - chunk.text = after; - chunk.chars = chunk.chars >> split_idx; - chunk.tabs = chunk.tabs >> split_idx; - - Some(Chunk { - text: before, - chars, - tabs, - ..chunk.clone() - }) - } else { - self.range.start = chunk_end; - self.buffer_chunk.take() - } - } - DiffTransform::DeletedHunk { - buffer_id, - hunk_info, - has_trailing_newline, - .. - } => { - let base_text_start = hunk_info.base_text_byte_range.start - + (self.range.start - diff_transform_start); - let base_text_end = - hunk_info.base_text_byte_range.start + (self.range.end - diff_transform_start); - let base_text_end = base_text_end.min(hunk_info.base_text_byte_range.end); - - let mut chunks = if let Some((_, mut chunks)) = self - .diff_base_chunks - .take() - .filter(|(id, _)| id == buffer_id) - { - if chunks.range().start != base_text_start || chunks.range().end < base_text_end - { - chunks.seek(base_text_start..base_text_end); - } - chunks - } else { - let base_buffer = &self.diffs.get(buffer_id)?.base_text(); - base_buffer.chunks(base_text_start..base_text_end, self.language_aware) - }; - - let chunk = if let Some(chunk) = chunks.next() { - self.range.start += chunk.text.len(); - self.diff_base_chunks = Some((*buffer_id, chunks)); - chunk - } else { - debug_assert!(has_trailing_newline); - self.range.start += "\n".len(); - Chunk { - text: "\n", - chars: 1u128, - ..Default::default() - } - }; - Some(chunk) - } - } - } -} - -impl MultiBufferBytes<'_> { - fn consume(&mut self, len: usize) { - self.range.start += len; - self.chunk = &self.chunk[len..]; - - if !self.range.is_empty() && self.chunk.is_empty() { - if let Some(chunk) = self.excerpt_bytes.as_mut().and_then(|bytes| bytes.next()) { - self.chunk = chunk; - } else if self.has_trailing_newline { - self.has_trailing_newline = false; - self.chunk = b"\n"; - } else { - self.cursor.next(); - while let Some(region) = self.cursor.region() - && region.buffer_range.is_empty() - && !region.has_trailing_newline - { - self.cursor.next(); - } - if let Some(region) = self.cursor.region() { - let mut excerpt_bytes = region.buffer.bytes_in_range( - region.buffer_range.start - ..(region.buffer_range.start + (self.range.end - region.range.start)) - .min(region.buffer_range.end), - ); - self.chunk = excerpt_bytes.next().unwrap_or(&[]); - self.excerpt_bytes = Some(excerpt_bytes); - self.has_trailing_newline = - region.has_trailing_newline && self.range.end >= region.range.end; - if self.chunk.is_empty() && self.has_trailing_newline { - self.has_trailing_newline = false; - self.chunk = b"\n"; - } - } - } - } - } -} - -impl<'a> Iterator for MultiBufferBytes<'a> { - type Item = &'a [u8]; - - fn next(&mut self) -> Option { - let chunk = self.chunk; - if chunk.is_empty() { - None - } else { - self.consume(chunk.len()); - Some(chunk) - } - } -} - -impl io::Read for MultiBufferBytes<'_> { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - let len = cmp::min(buf.len(), self.chunk.len()); - buf[..len].copy_from_slice(&self.chunk[..len]); - if len > 0 { - self.consume(len); - } - Ok(len) - } -} - -impl io::Read for ReversedMultiBufferBytes<'_> { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - let len = cmp::min(buf.len(), self.chunk.len()); - buf[..len].copy_from_slice(&self.chunk[..len]); - buf[..len].reverse(); - if len > 0 { - self.range.end -= len; - self.chunk = &self.chunk[..self.chunk.len() - len]; - if !self.range.is_empty() - && self.chunk.is_empty() - && let Some(chunk) = self.chunks.next() - { - self.chunk = chunk.as_bytes(); - } - } - Ok(len) - } -} - -impl<'a> Iterator for ExcerptChunks<'a> { - type Item = Chunk<'a>; - - fn next(&mut self) -> Option { - if let Some(chunk) = self.content_chunks.next() { - return Some(chunk); - } - - if self.has_footer { - let text = "\n"; - let chars = 0b1; - self.has_footer = false; - return Some(Chunk { - text, - chars, - ..Default::default() - }); - } - - None - } -} - -impl ToOffset for Point { - fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> MultiBufferOffset { - snapshot.point_to_offset(*self) - } - fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> MultiBufferOffsetUtf16 { - snapshot.point_to_offset_utf16(*self) - } -} - -impl ToOffset for MultiBufferOffset { - #[track_caller] - fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> MultiBufferOffset { - assert!( - *self <= snapshot.len(), - "offset {} is greater than the snapshot.len() {}", - self.0, - snapshot.len().0, - ); - *self - } - fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> MultiBufferOffsetUtf16 { - snapshot.offset_to_offset_utf16(*self) - } -} - -impl ToOffset for MultiBufferOffsetUtf16 { - fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> MultiBufferOffset { - snapshot.offset_utf16_to_offset(*self) - } - - fn to_offset_utf16(&self, _snapshot: &MultiBufferSnapshot) -> MultiBufferOffsetUtf16 { - *self - } -} - -impl ToOffset for PointUtf16 { - fn to_offset<'a>(&self, snapshot: &MultiBufferSnapshot) -> MultiBufferOffset { - snapshot.point_utf16_to_offset(*self) - } - fn to_offset_utf16(&self, snapshot: &MultiBufferSnapshot) -> MultiBufferOffsetUtf16 { - snapshot.point_utf16_to_offset_utf16(*self) - } -} - -impl ToPoint for MultiBufferOffset { - fn to_point<'a>(&self, snapshot: &MultiBufferSnapshot) -> Point { - snapshot.offset_to_point(*self) - } - fn to_point_utf16<'a>(&self, snapshot: &MultiBufferSnapshot) -> PointUtf16 { - snapshot.offset_to_point_utf16(*self) - } -} - -impl ToPoint for Point { - fn to_point<'a>(&self, _: &MultiBufferSnapshot) -> Point { - *self - } - fn to_point_utf16<'a>(&self, snapshot: &MultiBufferSnapshot) -> PointUtf16 { - snapshot.point_to_point_utf16(*self) - } -} - -impl ToPoint for PointUtf16 { - fn to_point<'a>(&self, snapshot: &MultiBufferSnapshot) -> Point { - snapshot.point_utf16_to_point(*self) - } - fn to_point_utf16<'a>(&self, _: &MultiBufferSnapshot) -> PointUtf16 { - *self - } -} - -impl From for EntityId { - fn from(id: ExcerptId) -> Self { - EntityId::from(id.0 as u64) - } -} - -#[cfg(debug_assertions)] -pub mod debug { - use super::*; - - pub trait ToMultiBufferDebugRanges { - fn to_multi_buffer_debug_ranges( - &self, - snapshot: &MultiBufferSnapshot, - ) -> Vec>; - } - - impl ToMultiBufferDebugRanges for T { - fn to_multi_buffer_debug_ranges( - &self, - snapshot: &MultiBufferSnapshot, - ) -> Vec> { - [self.to_offset(snapshot)].to_multi_buffer_debug_ranges(snapshot) - } - } - - impl ToMultiBufferDebugRanges for Range { - fn to_multi_buffer_debug_ranges( - &self, - snapshot: &MultiBufferSnapshot, - ) -> Vec> { - [self.start.to_offset(snapshot)..self.end.to_offset(snapshot)] - .to_multi_buffer_debug_ranges(snapshot) - } - } - - impl ToMultiBufferDebugRanges for Vec { - fn to_multi_buffer_debug_ranges( - &self, - snapshot: &MultiBufferSnapshot, - ) -> Vec> { - self.as_slice().to_multi_buffer_debug_ranges(snapshot) - } - } - - impl ToMultiBufferDebugRanges for Vec> { - fn to_multi_buffer_debug_ranges( - &self, - snapshot: &MultiBufferSnapshot, - ) -> Vec> { - self.as_slice().to_multi_buffer_debug_ranges(snapshot) - } - } - - impl ToMultiBufferDebugRanges for [T] { - fn to_multi_buffer_debug_ranges( - &self, - snapshot: &MultiBufferSnapshot, - ) -> Vec> { - self.iter() - .map(|item| { - let offset = item.to_offset(snapshot); - offset..offset - }) - .collect() - } - } - - impl ToMultiBufferDebugRanges for [Range] { - fn to_multi_buffer_debug_ranges( - &self, - snapshot: &MultiBufferSnapshot, - ) -> Vec> { - self.iter() - .map(|range| range.start.to_offset(snapshot)..range.end.to_offset(snapshot)) - .collect() - } - } -} diff --git a/crates/multi_buffer/src/multi_buffer_tests.rs b/crates/multi_buffer/src/multi_buffer_tests.rs deleted file mode 100644 index fc2edcac15..0000000000 --- a/crates/multi_buffer/src/multi_buffer_tests.rs +++ /dev/null @@ -1,4652 +0,0 @@ -use super::*; -use buffer_diff::{DiffHunkStatus, DiffHunkStatusKind}; -use gpui::{App, TestAppContext}; -use indoc::indoc; -use language::{Buffer, Rope}; -use parking_lot::RwLock; -use rand::prelude::*; -use settings::SettingsStore; -use std::env; -use std::time::{Duration, Instant}; -use util::RandomCharIter; -use util::rel_path::rel_path; -use util::test::sample_text; - -#[ctor::ctor] -fn init_logger() { - zlog::init_test(); -} - -#[gpui::test] -fn test_empty_singleton(cx: &mut App) { - let buffer = cx.new(|cx| Buffer::local("", cx)); - let buffer_id = buffer.read(cx).remote_id(); - let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot.text(), ""); - assert_eq!( - snapshot.row_infos(MultiBufferRow(0)).collect::>(), - [RowInfo { - buffer_id: Some(buffer_id), - buffer_row: Some(0), - base_text_row: None, - multibuffer_row: Some(MultiBufferRow(0)), - diff_status: None, - expand_info: None, - wrapped_buffer_row: None, - }] - ); -} - -#[gpui::test] -fn test_singleton(cx: &mut App) { - let buffer = cx.new(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); - let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); - - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot.text(), buffer.read(cx).text()); - - assert_eq!( - snapshot - .row_infos(MultiBufferRow(0)) - .map(|info| info.buffer_row) - .collect::>(), - (0..buffer.read(cx).row_count()) - .map(Some) - .collect::>() - ); - assert_consistent_line_numbers(&snapshot); - - buffer.update(cx, |buffer, cx| buffer.edit([(1..3, "XXX\n")], None, cx)); - let snapshot = multibuffer.read(cx).snapshot(cx); - - assert_eq!(snapshot.text(), buffer.read(cx).text()); - assert_eq!( - snapshot - .row_infos(MultiBufferRow(0)) - .map(|info| info.buffer_row) - .collect::>(), - (0..buffer.read(cx).row_count()) - .map(Some) - .collect::>() - ); - assert_consistent_line_numbers(&snapshot); -} - -#[gpui::test] -fn test_remote(cx: &mut App) { - let host_buffer = cx.new(|cx| Buffer::local("a", cx)); - let guest_buffer = cx.new(|cx| { - let state = host_buffer.read(cx).to_proto(cx); - let ops = cx - .background_executor() - .block(host_buffer.read(cx).serialize_ops(None, cx)); - let mut buffer = - Buffer::from_proto(ReplicaId::REMOTE_SERVER, Capability::ReadWrite, state, None) - .unwrap(); - buffer.apply_ops( - ops.into_iter() - .map(|op| language::proto::deserialize_operation(op).unwrap()), - cx, - ); - buffer - }); - let multibuffer = cx.new(|cx| MultiBuffer::singleton(guest_buffer.clone(), cx)); - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot.text(), "a"); - - guest_buffer.update(cx, |buffer, cx| buffer.edit([(1..1, "b")], None, cx)); - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot.text(), "ab"); - - guest_buffer.update(cx, |buffer, cx| buffer.edit([(2..2, "c")], None, cx)); - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot.text(), "abc"); -} - -#[gpui::test] -fn test_excerpt_boundaries_and_clipping(cx: &mut App) { - let buffer_1 = cx.new(|cx| Buffer::local(sample_text(6, 6, 'a'), cx)); - let buffer_2 = cx.new(|cx| Buffer::local(sample_text(6, 6, 'g'), cx)); - let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - - let events = Arc::new(RwLock::new(Vec::::new())); - multibuffer.update(cx, |_, cx| { - let events = events.clone(); - cx.subscribe(&multibuffer, move |_, _, event, _| { - if let Event::Edited { .. } = event { - events.write().push(event.clone()) - } - }) - .detach(); - }); - - let subscription = multibuffer.update(cx, |multibuffer, cx| { - let subscription = multibuffer.subscribe(); - multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange::new(Point::new(1, 2)..Point::new(2, 5))], - cx, - ); - assert_eq!( - subscription.consume().into_inner(), - [Edit { - old: MultiBufferOffset(0)..MultiBufferOffset(0), - new: MultiBufferOffset(0)..MultiBufferOffset(10) - }] - ); - - multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange::new(Point::new(3, 3)..Point::new(4, 4))], - cx, - ); - multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange::new(Point::new(3, 1)..Point::new(3, 3))], - cx, - ); - assert_eq!( - subscription.consume().into_inner(), - [Edit { - old: MultiBufferOffset(10)..MultiBufferOffset(10), - new: MultiBufferOffset(10)..MultiBufferOffset(22) - }] - ); - - subscription - }); - - // Adding excerpts emits an edited event. - assert_eq!( - events.read().as_slice(), - &[ - Event::Edited { - edited_buffer: None, - }, - Event::Edited { - edited_buffer: None, - }, - Event::Edited { - edited_buffer: None, - } - ] - ); - - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!( - snapshot.text(), - indoc!( - " - bbbb - ccccc - ddd - eeee - jj" - ), - ); - assert_eq!( - snapshot - .row_infos(MultiBufferRow(0)) - .map(|info| info.buffer_row) - .collect::>(), - [Some(1), Some(2), Some(3), Some(4), Some(3)] - ); - assert_eq!( - snapshot - .row_infos(MultiBufferRow(2)) - .map(|info| info.buffer_row) - .collect::>(), - [Some(3), Some(4), Some(3)] - ); - assert_eq!( - snapshot - .row_infos(MultiBufferRow(4)) - .map(|info| info.buffer_row) - .collect::>(), - [Some(3)] - ); - assert!( - snapshot - .row_infos(MultiBufferRow(5)) - .map(|info| info.buffer_row) - .collect::>() - .is_empty() - ); - - assert_eq!( - boundaries_in_range(Point::new(0, 0)..Point::new(4, 2), &snapshot), - &[ - (MultiBufferRow(0), "bbbb\nccccc".to_string(), true), - (MultiBufferRow(2), "ddd\neeee".to_string(), false), - (MultiBufferRow(4), "jj".to_string(), true), - ] - ); - assert_eq!( - boundaries_in_range(Point::new(0, 0)..Point::new(2, 0), &snapshot), - &[(MultiBufferRow(0), "bbbb\nccccc".to_string(), true)] - ); - assert_eq!( - boundaries_in_range(Point::new(1, 0)..Point::new(1, 5), &snapshot), - &[] - ); - assert_eq!( - boundaries_in_range(Point::new(1, 0)..Point::new(2, 0), &snapshot), - &[] - ); - assert_eq!( - boundaries_in_range(Point::new(1, 0)..Point::new(4, 0), &snapshot), - &[(MultiBufferRow(2), "ddd\neeee".to_string(), false)] - ); - assert_eq!( - boundaries_in_range(Point::new(1, 0)..Point::new(4, 0), &snapshot), - &[(MultiBufferRow(2), "ddd\neeee".to_string(), false)] - ); - assert_eq!( - boundaries_in_range(Point::new(2, 0)..Point::new(3, 0), &snapshot), - &[(MultiBufferRow(2), "ddd\neeee".to_string(), false)] - ); - assert_eq!( - boundaries_in_range(Point::new(4, 0)..Point::new(4, 2), &snapshot), - &[(MultiBufferRow(4), "jj".to_string(), true)] - ); - assert_eq!( - boundaries_in_range(Point::new(4, 2)..Point::new(4, 2), &snapshot), - &[] - ); - - buffer_1.update(cx, |buffer, cx| { - let text = "\n"; - buffer.edit( - [ - (Point::new(0, 0)..Point::new(0, 0), text), - (Point::new(2, 1)..Point::new(2, 3), text), - ], - None, - cx, - ); - }); - - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!( - snapshot.text(), - concat!( - "bbbb\n", // Preserve newlines - "c\n", // - "cc\n", // - "ddd\n", // - "eeee\n", // - "jj" // - ) - ); - - assert_eq!( - subscription.consume().into_inner(), - [Edit { - old: MultiBufferOffset(6)..MultiBufferOffset(8), - new: MultiBufferOffset(6)..MultiBufferOffset(7) - }] - ); - - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!( - snapshot.clip_point(Point::new(0, 5), Bias::Left), - Point::new(0, 4) - ); - assert_eq!( - snapshot.clip_point(Point::new(0, 5), Bias::Right), - Point::new(0, 4) - ); - assert_eq!( - snapshot.clip_point(Point::new(5, 1), Bias::Right), - Point::new(5, 1) - ); - assert_eq!( - snapshot.clip_point(Point::new(5, 2), Bias::Right), - Point::new(5, 2) - ); - assert_eq!( - snapshot.clip_point(Point::new(5, 3), Bias::Right), - Point::new(5, 2) - ); - - let snapshot = multibuffer.update(cx, |multibuffer, cx| { - let (buffer_2_excerpt_id, _) = - multibuffer.excerpts_for_buffer(buffer_2.read(cx).remote_id(), cx)[0].clone(); - multibuffer.remove_excerpts([buffer_2_excerpt_id], cx); - multibuffer.snapshot(cx) - }); - - assert_eq!( - snapshot.text(), - concat!( - "bbbb\n", // Preserve newlines - "c\n", // - "cc\n", // - "ddd\n", // - "eeee", // - ) - ); - - fn boundaries_in_range( - range: Range, - snapshot: &MultiBufferSnapshot, - ) -> Vec<(MultiBufferRow, String, bool)> { - snapshot - .excerpt_boundaries_in_range(range) - .map(|boundary| { - let starts_new_buffer = boundary.starts_new_buffer(); - ( - boundary.row, - boundary - .next - .buffer - .text_for_range(boundary.next.range.context) - .collect::(), - starts_new_buffer, - ) - }) - .collect::>() - } -} - -#[gpui::test] -async fn test_diff_boundary_anchors(cx: &mut TestAppContext) { - let base_text = "one\ntwo\nthree\n"; - let text = "one\nthree\n"; - let buffer = cx.new(|cx| Buffer::local(text, cx)); - let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); - let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); - multibuffer.update(cx, |multibuffer, cx| multibuffer.add_diff(diff, cx)); - - let (before, after) = multibuffer.update(cx, |multibuffer, cx| { - let before = multibuffer.snapshot(cx).anchor_before(Point::new(1, 0)); - let after = multibuffer.snapshot(cx).anchor_after(Point::new(1, 0)); - multibuffer.set_all_diff_hunks_expanded(cx); - (before, after) - }); - cx.run_until_parked(); - - let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - let actual_text = snapshot.text(); - let actual_row_infos = snapshot.row_infos(MultiBufferRow(0)).collect::>(); - let actual_diff = format_diff(&actual_text, &actual_row_infos, &Default::default(), None); - pretty_assertions::assert_eq!( - actual_diff, - indoc! { - " one - - two - three - " - }, - ); - - multibuffer.update(cx, |multibuffer, cx| { - let snapshot = multibuffer.snapshot(cx); - assert_eq!(before.to_point(&snapshot), Point::new(1, 0)); - assert_eq!(after.to_point(&snapshot), Point::new(2, 0)); - assert_eq!( - vec![Point::new(1, 0), Point::new(2, 0),], - snapshot.summaries_for_anchors::(&[before, after]), - ) - }) -} - -#[gpui::test] -async fn test_diff_hunks_in_range(cx: &mut TestAppContext) { - let base_text = "one\ntwo\nthree\nfour\nfive\nsix\nseven\neight\n"; - let text = "one\nfour\nseven\n"; - let buffer = cx.new(|cx| Buffer::local(text, cx)); - let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); - let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx)); - let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { - (multibuffer.snapshot(cx), multibuffer.subscribe()) - }); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.add_diff(diff, cx); - multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx); - }); - - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc! { - " one - - two - - three - four - - five - - six - seven - - eight - " - }, - ); - - assert_eq!( - snapshot - .diff_hunks_in_range(Point::new(1, 0)..Point::MAX) - .map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0) - .collect::>(), - vec![1..3, 4..6, 7..8] - ); - - assert_eq!(snapshot.diff_hunk_before(Point::new(1, 1)), None,); - assert_eq!( - snapshot.diff_hunk_before(Point::new(7, 0)), - Some(MultiBufferRow(4)) - ); - assert_eq!( - snapshot.diff_hunk_before(Point::new(4, 0)), - Some(MultiBufferRow(1)) - ); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.collapse_diff_hunks(vec![Anchor::min()..Anchor::max()], cx); - }); - - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc! { - " - one - four - seven - " - }, - ); - - assert_eq!( - snapshot.diff_hunk_before(Point::new(2, 0)), - Some(MultiBufferRow(1)), - ); - assert_eq!( - snapshot.diff_hunk_before(Point::new(4, 0)), - Some(MultiBufferRow(2)) - ); -} - -#[gpui::test] -async fn test_editing_text_in_diff_hunks(cx: &mut TestAppContext) { - let base_text = "one\ntwo\nfour\nfive\nsix\nseven\n"; - let text = "one\ntwo\nTHREE\nfour\nfive\nseven\n"; - let buffer = cx.new(|cx| Buffer::local(text, cx)); - let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); - let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); - - let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { - multibuffer.add_diff(diff.clone(), cx); - (multibuffer.snapshot(cx), multibuffer.subscribe()) - }); - - cx.executor().run_until_parked(); - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_all_diff_hunks_expanded(cx); - }); - - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc! { - " - one - two - + THREE - four - five - - six - seven - " - }, - ); - - // Insert a newline within an insertion hunk - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.edit([(Point::new(2, 0)..Point::new(2, 0), "__\n__")], None, cx); - }); - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc! { - " - one - two - + __ - + __THREE - four - five - - six - seven - " - }, - ); - - // Delete the newline before a deleted hunk. - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.edit([(Point::new(5, 4)..Point::new(6, 0), "")], None, cx); - }); - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc! { - " - one - two - + __ - + __THREE - four - fiveseven - " - }, - ); - - multibuffer.update(cx, |multibuffer, cx| multibuffer.undo(cx)); - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc! { - " - one - two - + __ - + __THREE - four - five - - six - seven - " - }, - ); - - // Cannot (yet) insert at the beginning of a deleted hunk. - // (because it would put the newline in the wrong place) - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.edit([(Point::new(6, 0)..Point::new(6, 0), "\n")], None, cx); - }); - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc! { - " - one - two - + __ - + __THREE - four - five - - six - seven - " - }, - ); - - // Replace a range that ends in a deleted hunk. - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.edit([(Point::new(5, 2)..Point::new(6, 2), "fty-")], None, cx); - }); - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc! { - " - one - two - + __ - + __THREE - four - fifty-seven - " - }, - ); -} - -#[gpui::test] -fn test_excerpt_events(cx: &mut App) { - let buffer_1 = cx.new(|cx| Buffer::local(sample_text(10, 3, 'a'), cx)); - let buffer_2 = cx.new(|cx| Buffer::local(sample_text(10, 3, 'm'), cx)); - - let leader_multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - let follower_multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - let follower_edit_event_count = Arc::new(RwLock::new(0)); - - follower_multibuffer.update(cx, |_, cx| { - let follower_edit_event_count = follower_edit_event_count.clone(); - cx.subscribe( - &leader_multibuffer, - move |follower, _, event, cx| match event.clone() { - Event::ExcerptsAdded { - buffer, - predecessor, - excerpts, - } => follower.insert_excerpts_with_ids_after(predecessor, buffer, excerpts, cx), - Event::ExcerptsRemoved { ids, .. } => follower.remove_excerpts(ids, cx), - Event::Edited { .. } => { - *follower_edit_event_count.write() += 1; - } - _ => {} - }, - ) - .detach(); - }); - - leader_multibuffer.update(cx, |leader, cx| { - leader.push_excerpts( - buffer_1.clone(), - [ExcerptRange::new(0..8), ExcerptRange::new(12..16)], - cx, - ); - leader.insert_excerpts_after( - leader.excerpt_ids()[0], - buffer_2.clone(), - [ExcerptRange::new(0..5), ExcerptRange::new(10..15)], - cx, - ) - }); - assert_eq!( - leader_multibuffer.read(cx).snapshot(cx).text(), - follower_multibuffer.read(cx).snapshot(cx).text(), - ); - assert_eq!(*follower_edit_event_count.read(), 2); - - leader_multibuffer.update(cx, |leader, cx| { - let excerpt_ids = leader.excerpt_ids(); - leader.remove_excerpts([excerpt_ids[1], excerpt_ids[3]], cx); - }); - assert_eq!( - leader_multibuffer.read(cx).snapshot(cx).text(), - follower_multibuffer.read(cx).snapshot(cx).text(), - ); - assert_eq!(*follower_edit_event_count.read(), 3); - - // Removing an empty set of excerpts is a noop. - leader_multibuffer.update(cx, |leader, cx| { - leader.remove_excerpts([], cx); - }); - assert_eq!( - leader_multibuffer.read(cx).snapshot(cx).text(), - follower_multibuffer.read(cx).snapshot(cx).text(), - ); - assert_eq!(*follower_edit_event_count.read(), 3); - - // Adding an empty set of excerpts is a noop. - leader_multibuffer.update(cx, |leader, cx| { - leader.push_excerpts::(buffer_2.clone(), [], cx); - }); - assert_eq!( - leader_multibuffer.read(cx).snapshot(cx).text(), - follower_multibuffer.read(cx).snapshot(cx).text(), - ); - assert_eq!(*follower_edit_event_count.read(), 3); - - leader_multibuffer.update(cx, |leader, cx| { - leader.clear(cx); - }); - assert_eq!( - leader_multibuffer.read(cx).snapshot(cx).text(), - follower_multibuffer.read(cx).snapshot(cx).text(), - ); - assert_eq!(*follower_edit_event_count.read(), 4); -} - -#[gpui::test] -fn test_expand_excerpts(cx: &mut App) { - let buffer = cx.new(|cx| Buffer::local(sample_text(20, 3, 'a'), cx)); - let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_excerpts_for_path( - PathKey::for_buffer(&buffer, cx), - buffer, - vec![ - // Note that in this test, this first excerpt - // does not contain a new line - Point::new(3, 2)..Point::new(3, 3), - Point::new(7, 1)..Point::new(7, 3), - Point::new(15, 0)..Point::new(15, 0), - ], - 1, - cx, - ) - }); - - let snapshot = multibuffer.read(cx).snapshot(cx); - - assert_eq!( - snapshot.text(), - concat!( - "ccc\n", // - "ddd\n", // - "eee", // - "\n", // End of excerpt - "ggg\n", // - "hhh\n", // - "iii", // - "\n", // End of excerpt - "ooo\n", // - "ppp\n", // - "qqq", // End of excerpt - ) - ); - drop(snapshot); - - multibuffer.update(cx, |multibuffer, cx| { - let line_zero = multibuffer.snapshot(cx).anchor_before(Point::new(0, 0)); - multibuffer.expand_excerpts( - multibuffer.excerpt_ids(), - 1, - ExpandExcerptDirection::UpAndDown, - cx, - ); - let snapshot = multibuffer.snapshot(cx); - let line_two = snapshot.anchor_before(Point::new(2, 0)); - assert_eq!(line_two.cmp(&line_zero, &snapshot), cmp::Ordering::Greater); - }); - - let snapshot = multibuffer.read(cx).snapshot(cx); - - assert_eq!( - snapshot.text(), - concat!( - "bbb\n", // - "ccc\n", // - "ddd\n", // - "eee\n", // - "fff\n", // - "ggg\n", // - "hhh\n", // - "iii\n", // - "jjj\n", // End of excerpt - "nnn\n", // - "ooo\n", // - "ppp\n", // - "qqq\n", // - "rrr", // End of excerpt - ) - ); -} - -#[gpui::test(iterations = 100)] -async fn test_set_anchored_excerpts_for_path(cx: &mut TestAppContext) { - let buffer_1 = cx.new(|cx| Buffer::local(sample_text(20, 3, 'a'), cx)); - let buffer_2 = cx.new(|cx| Buffer::local(sample_text(15, 4, 'a'), cx)); - let snapshot_1 = buffer_1.update(cx, |buffer, _| buffer.snapshot()); - let snapshot_2 = buffer_2.update(cx, |buffer, _| buffer.snapshot()); - let ranges_1 = vec![ - snapshot_1.anchor_before(Point::new(3, 2))..snapshot_1.anchor_before(Point::new(4, 2)), - snapshot_1.anchor_before(Point::new(7, 1))..snapshot_1.anchor_before(Point::new(7, 3)), - snapshot_1.anchor_before(Point::new(15, 0))..snapshot_1.anchor_before(Point::new(15, 0)), - ]; - let ranges_2 = vec![ - snapshot_2.anchor_before(Point::new(2, 1))..snapshot_2.anchor_before(Point::new(3, 1)), - snapshot_2.anchor_before(Point::new(10, 0))..snapshot_2.anchor_before(Point::new(10, 2)), - ]; - - let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - let anchor_ranges_1 = multibuffer - .update(cx, |multibuffer, cx| { - multibuffer.set_anchored_excerpts_for_path( - PathKey::for_buffer(&buffer_1, cx), - buffer_1.clone(), - ranges_1, - 2, - cx, - ) - }) - .await; - let snapshot_1 = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - assert_eq!( - anchor_ranges_1 - .iter() - .map(|range| range.to_point(&snapshot_1)) - .collect::>(), - vec![ - Point::new(2, 2)..Point::new(3, 2), - Point::new(6, 1)..Point::new(6, 3), - Point::new(11, 0)..Point::new(11, 0), - ] - ); - let anchor_ranges_2 = multibuffer - .update(cx, |multibuffer, cx| { - multibuffer.set_anchored_excerpts_for_path( - PathKey::for_buffer(&buffer_2, cx), - buffer_2.clone(), - ranges_2, - 2, - cx, - ) - }) - .await; - let snapshot_2 = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - assert_eq!( - anchor_ranges_2 - .iter() - .map(|range| range.to_point(&snapshot_2)) - .collect::>(), - vec![ - Point::new(16, 1)..Point::new(17, 1), - Point::new(22, 0)..Point::new(22, 2) - ] - ); - - let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - assert_eq!( - snapshot.text(), - concat!( - "bbb\n", // buffer_1 - "ccc\n", // - "ddd\n", // <-- excerpt 1 - "eee\n", // <-- excerpt 1 - "fff\n", // - "ggg\n", // - "hhh\n", // <-- excerpt 2 - "iii\n", // - "jjj\n", // - // - "nnn\n", // - "ooo\n", // - "ppp\n", // <-- excerpt 3 - "qqq\n", // - "rrr\n", // - // - "aaaa\n", // buffer 2 - "bbbb\n", // - "cccc\n", // <-- excerpt 4 - "dddd\n", // <-- excerpt 4 - "eeee\n", // - "ffff\n", // - // - "iiii\n", // - "jjjj\n", // - "kkkk\n", // <-- excerpt 5 - "llll\n", // - "mmmm", // - ) - ); -} - -#[gpui::test] -fn test_empty_multibuffer(cx: &mut App) { - let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - - let snapshot = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot.text(), ""); - assert_eq!( - snapshot - .row_infos(MultiBufferRow(0)) - .map(|info| info.buffer_row) - .collect::>(), - &[Some(0)] - ); - assert!( - snapshot - .row_infos(MultiBufferRow(1)) - .map(|info| info.buffer_row) - .collect::>() - .is_empty(), - ); -} - -#[gpui::test] -async fn test_empty_diff_excerpt(cx: &mut TestAppContext) { - let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - let buffer = cx.new(|cx| Buffer::local("", cx)); - let base_text = "a\nb\nc"; - - let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.push_excerpts(buffer.clone(), [ExcerptRange::new(0..0)], cx); - multibuffer.set_all_diff_hunks_expanded(cx); - multibuffer.add_diff(diff.clone(), cx); - }); - cx.run_until_parked(); - - let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - assert_eq!(snapshot.text(), "a\nb\nc\n"); - - let hunk = snapshot - .diff_hunks_in_range(Point::new(1, 1)..Point::new(1, 1)) - .next() - .unwrap(); - - assert_eq!(hunk.diff_base_byte_range.start, BufferOffset(0)); - - let buf2 = cx.new(|cx| Buffer::local("X", cx)); - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.push_excerpts(buf2, [ExcerptRange::new(0..1)], cx); - }); - - buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, "a\nb\nc")], None, cx); - diff.update(cx, |diff, cx| { - diff.recalculate_diff_sync(buffer.snapshot().text, cx); - }); - assert_eq!(buffer.text(), "a\nb\nc") - }); - cx.run_until_parked(); - - let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - assert_eq!(snapshot.text(), "a\nb\nc\nX"); - - buffer.update(cx, |buffer, cx| { - buffer.undo(cx); - diff.update(cx, |diff, cx| { - diff.recalculate_diff_sync(buffer.snapshot().text, cx); - }); - assert_eq!(buffer.text(), "") - }); - cx.run_until_parked(); - - let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - assert_eq!(snapshot.text(), "a\nb\nc\n\nX"); -} - -#[gpui::test] -fn test_singleton_multibuffer_anchors(cx: &mut App) { - let buffer = cx.new(|cx| Buffer::local("abcd", cx)); - let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); - let old_snapshot = multibuffer.read(cx).snapshot(cx); - buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, "X")], None, cx); - buffer.edit([(5..5, "Y")], None, cx); - }); - let new_snapshot = multibuffer.read(cx).snapshot(cx); - - assert_eq!(old_snapshot.text(), "abcd"); - assert_eq!(new_snapshot.text(), "XabcdY"); - - assert_eq!( - old_snapshot - .anchor_before(MultiBufferOffset(0)) - .to_offset(&new_snapshot), - MultiBufferOffset(0) - ); - assert_eq!( - old_snapshot - .anchor_after(MultiBufferOffset(0)) - .to_offset(&new_snapshot), - MultiBufferOffset(1) - ); - assert_eq!( - old_snapshot - .anchor_before(MultiBufferOffset(4)) - .to_offset(&new_snapshot), - MultiBufferOffset(5) - ); - assert_eq!( - old_snapshot - .anchor_after(MultiBufferOffset(4)) - .to_offset(&new_snapshot), - MultiBufferOffset(6) - ); -} - -#[gpui::test] -fn test_multibuffer_anchors(cx: &mut App) { - let buffer_1 = cx.new(|cx| Buffer::local("abcd", cx)); - let buffer_2 = cx.new(|cx| Buffer::local("efghi", cx)); - let multibuffer = cx.new(|cx| { - let mut multibuffer = MultiBuffer::new(Capability::ReadWrite); - multibuffer.push_excerpts(buffer_1.clone(), [ExcerptRange::new(0..4)], cx); - multibuffer.push_excerpts(buffer_2.clone(), [ExcerptRange::new(0..5)], cx); - multibuffer - }); - let old_snapshot = multibuffer.read(cx).snapshot(cx); - - assert_eq!( - old_snapshot - .anchor_before(MultiBufferOffset(0)) - .to_offset(&old_snapshot), - MultiBufferOffset(0) - ); - assert_eq!( - old_snapshot - .anchor_after(MultiBufferOffset(0)) - .to_offset(&old_snapshot), - MultiBufferOffset(0) - ); - assert_eq!(Anchor::min().to_offset(&old_snapshot), MultiBufferOffset(0)); - assert_eq!(Anchor::min().to_offset(&old_snapshot), MultiBufferOffset(0)); - assert_eq!( - Anchor::max().to_offset(&old_snapshot), - MultiBufferOffset(10) - ); - assert_eq!( - Anchor::max().to_offset(&old_snapshot), - MultiBufferOffset(10) - ); - - buffer_1.update(cx, |buffer, cx| { - buffer.edit([(0..0, "W")], None, cx); - buffer.edit([(5..5, "X")], None, cx); - }); - buffer_2.update(cx, |buffer, cx| { - buffer.edit([(0..0, "Y")], None, cx); - buffer.edit([(6..6, "Z")], None, cx); - }); - let new_snapshot = multibuffer.read(cx).snapshot(cx); - - assert_eq!(old_snapshot.text(), "abcd\nefghi"); - assert_eq!(new_snapshot.text(), "WabcdX\nYefghiZ"); - - assert_eq!( - old_snapshot - .anchor_before(MultiBufferOffset(0)) - .to_offset(&new_snapshot), - MultiBufferOffset(0) - ); - assert_eq!( - old_snapshot - .anchor_after(MultiBufferOffset(0)) - .to_offset(&new_snapshot), - MultiBufferOffset(1) - ); - assert_eq!( - old_snapshot - .anchor_before(MultiBufferOffset(1)) - .to_offset(&new_snapshot), - MultiBufferOffset(2) - ); - assert_eq!( - old_snapshot - .anchor_after(MultiBufferOffset(1)) - .to_offset(&new_snapshot), - MultiBufferOffset(2) - ); - assert_eq!( - old_snapshot - .anchor_before(MultiBufferOffset(2)) - .to_offset(&new_snapshot), - MultiBufferOffset(3) - ); - assert_eq!( - old_snapshot - .anchor_after(MultiBufferOffset(2)) - .to_offset(&new_snapshot), - MultiBufferOffset(3) - ); - assert_eq!( - old_snapshot - .anchor_before(MultiBufferOffset(5)) - .to_offset(&new_snapshot), - MultiBufferOffset(7) - ); - assert_eq!( - old_snapshot - .anchor_after(MultiBufferOffset(5)) - .to_offset(&new_snapshot), - MultiBufferOffset(8) - ); - assert_eq!( - old_snapshot - .anchor_before(MultiBufferOffset(10)) - .to_offset(&new_snapshot), - MultiBufferOffset(13) - ); - assert_eq!( - old_snapshot - .anchor_after(MultiBufferOffset(10)) - .to_offset(&new_snapshot), - MultiBufferOffset(14) - ); -} - -#[gpui::test] -fn test_resolving_anchors_after_replacing_their_excerpts(cx: &mut App) { - let buffer_1 = cx.new(|cx| Buffer::local("abcd", cx)); - let buffer_2 = cx.new(|cx| Buffer::local("ABCDEFGHIJKLMNOP", cx)); - let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - - // Create an insertion id in buffer 1 that doesn't exist in buffer 2. - // Add an excerpt from buffer 1 that spans this new insertion. - buffer_1.update(cx, |buffer, cx| buffer.edit([(4..4, "123")], None, cx)); - let excerpt_id_1 = multibuffer.update(cx, |multibuffer, cx| { - multibuffer - .push_excerpts(buffer_1.clone(), [ExcerptRange::new(0..7)], cx) - .pop() - .unwrap() - }); - - let snapshot_1 = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot_1.text(), "abcd123"); - - // Replace the buffer 1 excerpt with new excerpts from buffer 2. - let (excerpt_id_2, excerpt_id_3) = multibuffer.update(cx, |multibuffer, cx| { - multibuffer.remove_excerpts([excerpt_id_1], cx); - let mut ids = multibuffer - .push_excerpts( - buffer_2.clone(), - [ - ExcerptRange::new(0..4), - ExcerptRange::new(6..10), - ExcerptRange::new(12..16), - ], - cx, - ) - .into_iter(); - (ids.next().unwrap(), ids.next().unwrap()) - }); - let snapshot_2 = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot_2.text(), "ABCD\nGHIJ\nMNOP"); - - // The old excerpt id doesn't get reused. - assert_ne!(excerpt_id_2, excerpt_id_1); - - // Resolve some anchors from the previous snapshot in the new snapshot. - // The current excerpts are from a different buffer, so we don't attempt to - // resolve the old text anchor in the new buffer. - assert_eq!( - snapshot_2.summary_for_anchor::( - &snapshot_1.anchor_before(MultiBufferOffset(2)) - ), - MultiBufferOffset(0) - ); - assert_eq!( - snapshot_2.summaries_for_anchors::(&[ - snapshot_1.anchor_before(MultiBufferOffset(2)), - snapshot_1.anchor_after(MultiBufferOffset(3)) - ]), - vec![MultiBufferOffset(0), MultiBufferOffset(0)] - ); - - // Refresh anchors from the old snapshot. The return value indicates that both - // anchors lost their original excerpt. - let refresh = snapshot_2.refresh_anchors(&[ - snapshot_1.anchor_before(MultiBufferOffset(2)), - snapshot_1.anchor_after(MultiBufferOffset(3)), - ]); - assert_eq!( - refresh, - &[ - (0, snapshot_2.anchor_before(MultiBufferOffset(0)), false), - (1, snapshot_2.anchor_after(MultiBufferOffset(0)), false), - ] - ); - - // Replace the middle excerpt with a smaller excerpt in buffer 2, - // that intersects the old excerpt. - let excerpt_id_5 = multibuffer.update(cx, |multibuffer, cx| { - multibuffer.remove_excerpts([excerpt_id_3], cx); - multibuffer - .insert_excerpts_after( - excerpt_id_2, - buffer_2.clone(), - [ExcerptRange::new(5..8)], - cx, - ) - .pop() - .unwrap() - }); - - let snapshot_3 = multibuffer.read(cx).snapshot(cx); - assert_eq!(snapshot_3.text(), "ABCD\nFGH\nMNOP"); - assert_ne!(excerpt_id_5, excerpt_id_3); - - // Resolve some anchors from the previous snapshot in the new snapshot. - // The third anchor can't be resolved, since its excerpt has been removed, - // so it resolves to the same position as its predecessor. - let anchors = [ - snapshot_2.anchor_before(MultiBufferOffset(0)), - snapshot_2.anchor_after(MultiBufferOffset(2)), - snapshot_2.anchor_after(MultiBufferOffset(6)), - snapshot_2.anchor_after(MultiBufferOffset(14)), - ]; - assert_eq!( - snapshot_3.summaries_for_anchors::(&anchors), - &[ - MultiBufferOffset(0), - MultiBufferOffset(2), - MultiBufferOffset(9), - MultiBufferOffset(13) - ] - ); - - let new_anchors = snapshot_3.refresh_anchors(&anchors); - assert_eq!( - new_anchors.iter().map(|a| (a.0, a.2)).collect::>(), - &[(0, true), (1, true), (2, true), (3, true)] - ); - assert_eq!( - snapshot_3.summaries_for_anchors::(new_anchors.iter().map(|a| &a.1)), - &[ - MultiBufferOffset(0), - MultiBufferOffset(2), - MultiBufferOffset(7), - MultiBufferOffset(13) - ] - ); -} - -#[gpui::test] -async fn test_basic_diff_hunks(cx: &mut TestAppContext) { - let text = indoc!( - " - ZERO - one - TWO - three - six - " - ); - let base_text = indoc!( - " - one - two - three - four - five - six - " - ); - - let buffer = cx.new(|cx| Buffer::local(text, cx)); - let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); - cx.run_until_parked(); - - let multibuffer = cx.new(|cx| { - let mut multibuffer = MultiBuffer::singleton(buffer.clone(), cx); - multibuffer.add_diff(diff.clone(), cx); - multibuffer - }); - - let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { - (multibuffer.snapshot(cx), multibuffer.subscribe()) - }); - assert_eq!( - snapshot.text(), - indoc!( - " - ZERO - one - TWO - three - six - " - ), - ); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx); - }); - - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - + ZERO - one - - two - + TWO - three - - four - - five - six - " - ), - ); - - assert_eq!( - snapshot - .row_infos(MultiBufferRow(0)) - .map(|info| (info.buffer_row, info.diff_status)) - .collect::>(), - vec![ - (Some(0), Some(DiffHunkStatus::added_none())), - (Some(1), None), - (Some(1), Some(DiffHunkStatus::deleted_none())), - (Some(2), Some(DiffHunkStatus::added_none())), - (Some(3), None), - (Some(3), Some(DiffHunkStatus::deleted_none())), - (Some(4), Some(DiffHunkStatus::deleted_none())), - (Some(4), None), - (Some(5), None) - ] - ); - - assert_chunks_in_ranges(&snapshot); - assert_consistent_line_numbers(&snapshot); - assert_position_translation(&snapshot); - assert_line_indents(&snapshot); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.collapse_diff_hunks(vec![Anchor::min()..Anchor::max()], cx) - }); - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - ZERO - one - TWO - three - six - " - ), - ); - - assert_chunks_in_ranges(&snapshot); - assert_consistent_line_numbers(&snapshot); - assert_position_translation(&snapshot); - assert_line_indents(&snapshot); - - // Expand the first diff hunk - multibuffer.update(cx, |multibuffer, cx| { - let position = multibuffer.read(cx).anchor_before(Point::new(2, 2)); - multibuffer.expand_diff_hunks(vec![position..position], cx) - }); - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - ZERO - one - - two - + TWO - three - six - " - ), - ); - - // Expand the second diff hunk - multibuffer.update(cx, |multibuffer, cx| { - let start = multibuffer.read(cx).anchor_before(Point::new(4, 0)); - let end = multibuffer.read(cx).anchor_before(Point::new(5, 0)); - multibuffer.expand_diff_hunks(vec![start..end], cx) - }); - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - ZERO - one - - two - + TWO - three - - four - - five - six - " - ), - ); - - assert_chunks_in_ranges(&snapshot); - assert_consistent_line_numbers(&snapshot); - assert_position_translation(&snapshot); - assert_line_indents(&snapshot); - - // Edit the buffer before the first hunk - buffer.update(cx, |buffer, cx| { - buffer.edit_via_marked_text( - indoc!( - " - ZERO - one« hundred - thousand» - TWO - three - six - " - ), - None, - cx, - ); - }); - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - ZERO - one hundred - thousand - - two - + TWO - three - - four - - five - six - " - ), - ); - - assert_chunks_in_ranges(&snapshot); - assert_consistent_line_numbers(&snapshot); - assert_position_translation(&snapshot); - assert_line_indents(&snapshot); - - // Recalculate the diff, changing the first diff hunk. - diff.update(cx, |diff, cx| { - diff.recalculate_diff_sync(buffer.read(cx).text_snapshot(), cx); - }); - cx.run_until_parked(); - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - ZERO - one hundred - thousand - TWO - three - - four - - five - six - " - ), - ); - - assert_eq!( - snapshot - .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.len()) - .map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0) - .collect::>(), - &[0..4, 5..7] - ); -} - -#[gpui::test] -async fn test_repeatedly_expand_a_diff_hunk(cx: &mut TestAppContext) { - let text = indoc!( - " - one - TWO - THREE - four - FIVE - six - " - ); - let base_text = indoc!( - " - one - four - five - six - " - ); - - let buffer = cx.new(|cx| Buffer::local(text, cx)); - let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); - cx.run_until_parked(); - - let multibuffer = cx.new(|cx| { - let mut multibuffer = MultiBuffer::singleton(buffer.clone(), cx); - multibuffer.add_diff(diff.clone(), cx); - multibuffer - }); - - let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { - (multibuffer.snapshot(cx), multibuffer.subscribe()) - }); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx); - }); - - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - one - + TWO - + THREE - four - - five - + FIVE - six - " - ), - ); - - // Regression test: expanding diff hunks that are already expanded should not change anything. - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.expand_diff_hunks( - vec![ - snapshot.anchor_before(Point::new(2, 0))..snapshot.anchor_before(Point::new(2, 0)), - ], - cx, - ); - }); - - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - one - + TWO - + THREE - four - - five - + FIVE - six - " - ), - ); - - // Now collapse all diff hunks - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.collapse_diff_hunks(vec![Anchor::min()..Anchor::max()], cx); - }); - - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - one - TWO - THREE - four - FIVE - six - " - ), - ); - - // Expand the hunks again, but this time provide two ranges that are both within the same hunk - // Target the first hunk which is between "one" and "four" - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.expand_diff_hunks( - vec![ - snapshot.anchor_before(Point::new(4, 0))..snapshot.anchor_before(Point::new(4, 0)), - snapshot.anchor_before(Point::new(4, 2))..snapshot.anchor_before(Point::new(4, 2)), - ], - cx, - ); - }); - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - one - TWO - THREE - four - - five - + FIVE - six - " - ), - ); -} - -#[gpui::test] -fn test_set_excerpts_for_buffer_ordering(cx: &mut TestAppContext) { - let buf1 = cx.new(|cx| { - Buffer::local( - indoc! { - "zero - one - two - two.five - three - four - five - six - seven - eight - nine - ten - eleven - ", - }, - cx, - ) - }); - let path1: PathKey = PathKey::with_sort_prefix(0, rel_path("root").into_arc()); - - let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_excerpts_for_path( - path1.clone(), - buf1.clone(), - vec![ - Point::row_range(1..2), - Point::row_range(6..7), - Point::row_range(11..12), - ], - 1, - cx, - ); - }); - - assert_excerpts_match( - &multibuffer, - cx, - indoc! { - "----- - zero - one - two - two.five - ----- - four - five - six - seven - ----- - nine - ten - eleven - " - }, - ); - - buf1.update(cx, |buffer, cx| buffer.edit([(0..5, "")], None, cx)); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_excerpts_for_path( - path1.clone(), - buf1.clone(), - vec![ - Point::row_range(0..3), - Point::row_range(5..7), - Point::row_range(10..11), - ], - 1, - cx, - ); - }); - - assert_excerpts_match( - &multibuffer, - cx, - indoc! { - "----- - one - two - two.five - three - four - five - six - seven - eight - nine - ten - eleven - " - }, - ); -} - -#[gpui::test] -fn test_set_excerpts_for_buffer(cx: &mut TestAppContext) { - let buf1 = cx.new(|cx| { - Buffer::local( - indoc! { - "zero - one - two - three - four - five - six - seven - ", - }, - cx, - ) - }); - let path1: PathKey = PathKey::with_sort_prefix(0, rel_path("root").into_arc()); - let buf2 = cx.new(|cx| { - Buffer::local( - indoc! { - "000 - 111 - 222 - 333 - 444 - 555 - 666 - 777 - 888 - 999 - " - }, - cx, - ) - }); - let path2 = PathKey::with_sort_prefix(1, rel_path("root").into_arc()); - - let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_excerpts_for_path( - path1.clone(), - buf1.clone(), - vec![Point::row_range(0..1)], - 2, - cx, - ); - }); - - assert_excerpts_match( - &multibuffer, - cx, - indoc! { - "----- - zero - one - two - three - " - }, - ); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_excerpts_for_path(path1.clone(), buf1.clone(), vec![], 2, cx); - }); - - assert_excerpts_match(&multibuffer, cx, ""); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_excerpts_for_path( - path1.clone(), - buf1.clone(), - vec![Point::row_range(0..1), Point::row_range(7..8)], - 2, - cx, - ); - }); - - assert_excerpts_match( - &multibuffer, - cx, - indoc! {"----- - zero - one - two - three - ----- - five - six - seven - "}, - ); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_excerpts_for_path( - path1.clone(), - buf1.clone(), - vec![Point::row_range(0..1), Point::row_range(5..6)], - 2, - cx, - ); - }); - - assert_excerpts_match( - &multibuffer, - cx, - indoc! {"----- - zero - one - two - three - four - five - six - seven - "}, - ); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_excerpts_for_path( - path2.clone(), - buf2.clone(), - vec![Point::row_range(2..3)], - 2, - cx, - ); - }); - - assert_excerpts_match( - &multibuffer, - cx, - indoc! {"----- - zero - one - two - three - four - five - six - seven - ----- - 000 - 111 - 222 - 333 - 444 - 555 - "}, - ); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_excerpts_for_path(path1.clone(), buf1.clone(), vec![], 2, cx); - }); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_excerpts_for_path( - path1.clone(), - buf1.clone(), - vec![Point::row_range(3..4)], - 2, - cx, - ); - }); - - assert_excerpts_match( - &multibuffer, - cx, - indoc! {"----- - one - two - three - four - five - six - ----- - 000 - 111 - 222 - 333 - 444 - 555 - "}, - ); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_excerpts_for_path( - path1.clone(), - buf1.clone(), - vec![Point::row_range(3..4)], - 2, - cx, - ); - }); -} - -#[gpui::test] -fn test_set_excerpts_for_buffer_rename(cx: &mut TestAppContext) { - let buf1 = cx.new(|cx| { - Buffer::local( - indoc! { - "zero - one - two - three - four - five - six - seven - ", - }, - cx, - ) - }); - let path: PathKey = PathKey::with_sort_prefix(0, rel_path("root").into_arc()); - let buf2 = cx.new(|cx| { - Buffer::local( - indoc! { - "000 - 111 - 222 - 333 - " - }, - cx, - ) - }); - - let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_excerpts_for_path( - path.clone(), - buf1.clone(), - vec![Point::row_range(1..1), Point::row_range(4..5)], - 1, - cx, - ); - }); - - assert_excerpts_match( - &multibuffer, - cx, - indoc! { - "----- - zero - one - two - three - four - five - six - " - }, - ); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_excerpts_for_path( - path.clone(), - buf2.clone(), - vec![Point::row_range(0..1)], - 2, - cx, - ); - }); - - assert_excerpts_match( - &multibuffer, - cx, - indoc! {"----- - 000 - 111 - 222 - 333 - "}, - ); -} - -#[gpui::test] -async fn test_diff_hunks_with_multiple_excerpts(cx: &mut TestAppContext) { - let base_text_1 = indoc!( - " - one - two - three - four - five - six - " - ); - let text_1 = indoc!( - " - ZERO - one - TWO - three - six - " - ); - let base_text_2 = indoc!( - " - seven - eight - nine - ten - eleven - twelve - " - ); - let text_2 = indoc!( - " - eight - nine - eleven - THIRTEEN - FOURTEEN - " - ); - - let buffer_1 = cx.new(|cx| Buffer::local(text_1, cx)); - let buffer_2 = cx.new(|cx| Buffer::local(text_2, cx)); - let diff_1 = cx.new(|cx| BufferDiff::new_with_base_text(base_text_1, &buffer_1, cx)); - let diff_2 = cx.new(|cx| BufferDiff::new_with_base_text(base_text_2, &buffer_2, cx)); - cx.run_until_parked(); - - let multibuffer = cx.new(|cx| { - let mut multibuffer = MultiBuffer::new(Capability::ReadWrite); - multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)], - cx, - ); - multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)], - cx, - ); - multibuffer.add_diff(diff_1.clone(), cx); - multibuffer.add_diff(diff_2.clone(), cx); - multibuffer - }); - - let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { - (multibuffer.snapshot(cx), multibuffer.subscribe()) - }); - assert_eq!( - snapshot.text(), - indoc!( - " - ZERO - one - TWO - three - six - - eight - nine - eleven - THIRTEEN - FOURTEEN - " - ), - ); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx); - }); - - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - + ZERO - one - - two - + TWO - three - - four - - five - six - - - seven - eight - nine - - ten - eleven - - twelve - + THIRTEEN - + FOURTEEN - " - ), - ); - - let id_1 = buffer_1.read_with(cx, |buffer, _| buffer.remote_id()); - let id_2 = buffer_2.read_with(cx, |buffer, _| buffer.remote_id()); - let base_id_1 = diff_1.read_with(cx, |diff, _| diff.base_text().remote_id()); - let base_id_2 = diff_2.read_with(cx, |diff, _| diff.base_text().remote_id()); - - let buffer_lines = (0..=snapshot.max_row().0) - .map(|row| { - let (buffer, range) = snapshot.buffer_line_for_row(MultiBufferRow(row))?; - Some(( - buffer.remote_id(), - buffer.text_for_range(range).collect::(), - )) - }) - .collect::>(); - pretty_assertions::assert_eq!( - buffer_lines, - [ - Some((id_1, "ZERO".into())), - Some((id_1, "one".into())), - Some((base_id_1, "two".into())), - Some((id_1, "TWO".into())), - Some((id_1, " three".into())), - Some((base_id_1, "four".into())), - Some((base_id_1, "five".into())), - Some((id_1, "six".into())), - Some((id_1, "".into())), - Some((base_id_2, "seven".into())), - Some((id_2, " eight".into())), - Some((id_2, "nine".into())), - Some((base_id_2, "ten".into())), - Some((id_2, "eleven".into())), - Some((base_id_2, "twelve".into())), - Some((id_2, "THIRTEEN".into())), - Some((id_2, "FOURTEEN".into())), - Some((id_2, "".into())), - ] - ); - - let buffer_ids_by_range = [ - (Point::new(0, 0)..Point::new(0, 0), &[id_1] as &[_]), - (Point::new(0, 0)..Point::new(2, 0), &[id_1]), - (Point::new(2, 0)..Point::new(2, 0), &[id_1]), - (Point::new(3, 0)..Point::new(3, 0), &[id_1]), - (Point::new(8, 0)..Point::new(9, 0), &[id_1]), - (Point::new(8, 0)..Point::new(10, 0), &[id_1, id_2]), - (Point::new(9, 0)..Point::new(9, 0), &[id_2]), - ]; - for (range, buffer_ids) in buffer_ids_by_range { - assert_eq!( - snapshot - .buffer_ids_for_range(range.clone()) - .collect::>(), - buffer_ids, - "buffer_ids_for_range({range:?}" - ); - } - - assert_position_translation(&snapshot); - assert_line_indents(&snapshot); - - assert_eq!( - snapshot - .diff_hunks_in_range(MultiBufferOffset(0)..snapshot.len()) - .map(|hunk| hunk.row_range.start.0..hunk.row_range.end.0) - .collect::>(), - &[0..1, 2..4, 5..7, 9..10, 12..13, 14..17] - ); - - buffer_2.update(cx, |buffer, cx| { - buffer.edit_via_marked_text( - indoc!( - " - eight - «»eleven - THIRTEEN - FOURTEEN - " - ), - None, - cx, - ); - }); - - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - + ZERO - one - - two - + TWO - three - - four - - five - six - - - seven - eight - eleven - - twelve - + THIRTEEN - + FOURTEEN - " - ), - ); - - assert_line_indents(&snapshot); -} - -/// A naive implementation of a multi-buffer that does not maintain -/// any derived state, used for comparison in a randomized test. -#[derive(Default)] -struct ReferenceMultibuffer { - excerpts: Vec, - diffs: HashMap>, -} - -#[derive(Debug)] -struct ReferenceExcerpt { - id: ExcerptId, - buffer: Entity, - range: Range, - expanded_diff_hunks: Vec, -} - -#[derive(Debug)] -struct ReferenceRegion { - buffer_id: Option, - range: Range, - buffer_range: Option>, - status: Option, - excerpt_id: Option, -} - -impl ReferenceMultibuffer { - fn expand_excerpts(&mut self, excerpts: &HashSet, line_count: u32, cx: &App) { - if line_count == 0 { - return; - } - - for id in excerpts { - let excerpt = self.excerpts.iter_mut().find(|e| e.id == *id).unwrap(); - let snapshot = excerpt.buffer.read(cx).snapshot(); - let mut point_range = excerpt.range.to_point(&snapshot); - point_range.start = Point::new(point_range.start.row.saturating_sub(line_count), 0); - point_range.end = - snapshot.clip_point(Point::new(point_range.end.row + line_count, 0), Bias::Left); - point_range.end.column = snapshot.line_len(point_range.end.row); - excerpt.range = - snapshot.anchor_before(point_range.start)..snapshot.anchor_after(point_range.end); - } - } - - fn remove_excerpt(&mut self, id: ExcerptId, cx: &App) { - let ix = self - .excerpts - .iter() - .position(|excerpt| excerpt.id == id) - .unwrap(); - let excerpt = self.excerpts.remove(ix); - let buffer = excerpt.buffer.read(cx); - let id = buffer.remote_id(); - log::info!( - "Removing excerpt {}: {:?}", - ix, - buffer - .text_for_range(excerpt.range.to_offset(buffer)) - .collect::(), - ); - if !self - .excerpts - .iter() - .any(|excerpt| excerpt.buffer.read(cx).remote_id() == id) - { - self.diffs.remove(&id); - } - } - - fn insert_excerpt_after( - &mut self, - prev_id: ExcerptId, - new_excerpt_id: ExcerptId, - (buffer_handle, anchor_range): (Entity, Range), - ) { - let excerpt_ix = if prev_id == ExcerptId::max() { - self.excerpts.len() - } else { - self.excerpts - .iter() - .position(|excerpt| excerpt.id == prev_id) - .unwrap() - + 1 - }; - self.excerpts.insert( - excerpt_ix, - ReferenceExcerpt { - id: new_excerpt_id, - buffer: buffer_handle, - range: anchor_range, - expanded_diff_hunks: Vec::new(), - }, - ); - } - - fn expand_diff_hunks(&mut self, excerpt_id: ExcerptId, range: Range, cx: &App) { - let excerpt = self - .excerpts - .iter_mut() - .find(|e| e.id == excerpt_id) - .unwrap(); - let buffer = excerpt.buffer.read(cx).snapshot(); - let buffer_id = buffer.remote_id(); - let Some(diff) = self.diffs.get(&buffer_id) else { - return; - }; - let excerpt_range = excerpt.range.to_offset(&buffer); - for hunk in diff.read(cx).hunks_intersecting_range(range, &buffer, cx) { - let hunk_range = hunk.buffer_range.to_offset(&buffer); - if hunk_range.start < excerpt_range.start || hunk_range.start > excerpt_range.end { - continue; - } - if let Err(ix) = excerpt - .expanded_diff_hunks - .binary_search_by(|anchor| anchor.cmp(&hunk.buffer_range.start, &buffer)) - { - log::info!( - "expanding diff hunk {:?}. excerpt:{:?}, excerpt range:{:?}", - hunk_range, - excerpt_id, - excerpt_range - ); - excerpt - .expanded_diff_hunks - .insert(ix, hunk.buffer_range.start); - } else { - log::trace!("hunk {hunk_range:?} already expanded in excerpt {excerpt_id:?}"); - } - } - } - - fn expected_content( - &self, - filter_mode: Option, - all_diff_hunks_expanded: bool, - cx: &App, - ) -> (String, Vec, HashSet) { - let mut text = String::new(); - let mut regions = Vec::::new(); - let mut filtered_regions = Vec::::new(); - let mut excerpt_boundary_rows = HashSet::default(); - for excerpt in &self.excerpts { - excerpt_boundary_rows.insert(MultiBufferRow(text.matches('\n').count() as u32)); - let buffer = excerpt.buffer.read(cx); - let buffer_range = excerpt.range.to_offset(buffer); - let diff = self.diffs.get(&buffer.remote_id()).unwrap().read(cx); - let base_buffer = diff.base_text(); - - let mut offset = buffer_range.start; - let hunks = diff - .hunks_intersecting_range(excerpt.range.clone(), buffer, cx) - .peekable(); - - for hunk in hunks { - // Ignore hunks that are outside the excerpt range. - let mut hunk_range = hunk.buffer_range.to_offset(buffer); - - hunk_range.end = hunk_range.end.min(buffer_range.end); - if hunk_range.start > buffer_range.end || hunk_range.start < buffer_range.start { - log::trace!("skipping hunk outside excerpt range"); - continue; - } - - if !all_diff_hunks_expanded - && !excerpt.expanded_diff_hunks.iter().any(|expanded_anchor| { - expanded_anchor.to_offset(buffer).max(buffer_range.start) - == hunk_range.start.max(buffer_range.start) - }) - { - log::trace!("skipping a hunk that's not marked as expanded"); - continue; - } - - if !hunk.buffer_range.start.is_valid(buffer) { - log::trace!("skipping hunk with deleted start: {:?}", hunk.range); - continue; - } - - if hunk_range.start >= offset { - // Add the buffer text before the hunk - let len = text.len(); - text.extend(buffer.text_for_range(offset..hunk_range.start)); - if text.len() > len { - regions.push(ReferenceRegion { - buffer_id: Some(buffer.remote_id()), - range: len..text.len(), - buffer_range: Some((offset..hunk_range.start).to_point(&buffer)), - status: None, - excerpt_id: Some(excerpt.id), - }); - } - - // Add the deleted text for the hunk. - if !hunk.diff_base_byte_range.is_empty() - && filter_mode != Some(MultiBufferFilterMode::KeepInsertions) - { - let mut base_text = base_buffer - .text_for_range(hunk.diff_base_byte_range.clone()) - .collect::(); - if !base_text.ends_with('\n') { - base_text.push('\n'); - } - let len = text.len(); - text.push_str(&base_text); - regions.push(ReferenceRegion { - buffer_id: Some(base_buffer.remote_id()), - range: len..text.len(), - buffer_range: Some(hunk.diff_base_byte_range.to_point(&base_buffer)), - status: Some(DiffHunkStatus::deleted(hunk.secondary_status)), - excerpt_id: Some(excerpt.id), - }); - } - - offset = hunk_range.start; - } - - // Add the inserted text for the hunk. - if hunk_range.end > offset { - let is_filtered = filter_mode == Some(MultiBufferFilterMode::KeepDeletions); - let range = if is_filtered { - text.len()..text.len() - } else { - let len = text.len(); - text.extend(buffer.text_for_range(offset..hunk_range.end)); - len..text.len() - }; - let region = ReferenceRegion { - buffer_id: Some(buffer.remote_id()), - range, - buffer_range: Some((offset..hunk_range.end).to_point(&buffer)), - status: Some(DiffHunkStatus::added(hunk.secondary_status)), - excerpt_id: Some(excerpt.id), - }; - offset = hunk_range.end; - if is_filtered { - filtered_regions.push(region); - } else { - regions.push(region); - } - } - } - - // Add the buffer text for the rest of the excerpt. - let len = text.len(); - text.extend(buffer.text_for_range(offset..buffer_range.end)); - text.push('\n'); - regions.push(ReferenceRegion { - buffer_id: Some(buffer.remote_id()), - range: len..text.len(), - buffer_range: Some((offset..buffer_range.end).to_point(&buffer)), - status: None, - excerpt_id: Some(excerpt.id), - }); - } - - // Remove final trailing newline. - if self.excerpts.is_empty() { - regions.push(ReferenceRegion { - buffer_id: None, - range: 0..1, - buffer_range: Some(Point::new(0, 0)..Point::new(0, 1)), - status: None, - excerpt_id: None, - }); - } else { - text.pop(); - } - - // Retrieve the row info using the region that contains - // the start of each multi-buffer line. - let mut ix = 0; - let row_infos = text - .split('\n') - .map(|line| { - let row_info = regions - .iter() - .position(|region| region.range.contains(&ix)) - .map_or(RowInfo::default(), |region_ix| { - let region = ®ions[region_ix]; - let buffer_row = region.buffer_range.as_ref().map(|buffer_range| { - buffer_range.start.row - + text[region.range.start..ix].matches('\n').count() as u32 - }); - let main_buffer = self - .excerpts - .iter() - .find(|e| e.id == region.excerpt_id.unwrap()) - .map(|e| e.buffer.clone()); - let base_text_row = match region.status { - None => Some( - main_buffer - .as_ref() - .map(|main_buffer| { - let diff = self - .diffs - .get(&main_buffer.read(cx).remote_id()) - .unwrap(); - let buffer_row = buffer_row.unwrap(); - BaseTextRow( - diff.read(cx).snapshot(cx).row_to_base_text_row( - buffer_row, - &main_buffer.read(cx).snapshot(), - ), - ) - }) - .unwrap_or_default(), - ), - Some(DiffHunkStatus { - kind: DiffHunkStatusKind::Added, - .. - }) => None, - Some(DiffHunkStatus { - kind: DiffHunkStatusKind::Deleted, - .. - }) => Some(BaseTextRow(buffer_row.unwrap())), - Some(DiffHunkStatus { - kind: DiffHunkStatusKind::Modified, - .. - }) => unreachable!(), - }; - let is_excerpt_start = region_ix == 0 - || ®ions[region_ix - 1].excerpt_id != ®ion.excerpt_id - || regions[region_ix - 1].range.is_empty(); - let mut is_excerpt_end = region_ix == regions.len() - 1 - || ®ions[region_ix + 1].excerpt_id != ®ion.excerpt_id; - let is_start = !text[region.range.start..ix].contains('\n'); - let mut is_end = if region.range.end > text.len() { - !text[ix..].contains('\n') - } else { - text[ix..region.range.end.min(text.len())] - .matches('\n') - .count() - == 1 - }; - if region_ix < regions.len() - 1 - && !text[ix..].contains("\n") - && region.status == Some(DiffHunkStatus::added_none()) - && regions[region_ix + 1].excerpt_id == region.excerpt_id - && regions[region_ix + 1].range.start == text.len() - { - is_end = true; - is_excerpt_end = true; - } - let multibuffer_row = - MultiBufferRow(text[..ix].matches('\n').count() as u32); - let mut expand_direction = None; - if let Some(buffer) = &main_buffer { - let buffer_row = buffer_row.unwrap(); - let needs_expand_up = is_excerpt_start && is_start && buffer_row > 0; - let needs_expand_down = is_excerpt_end - && is_end - && buffer.read(cx).max_point().row > buffer_row; - expand_direction = if needs_expand_up && needs_expand_down { - Some(ExpandExcerptDirection::UpAndDown) - } else if needs_expand_up { - Some(ExpandExcerptDirection::Up) - } else if needs_expand_down { - Some(ExpandExcerptDirection::Down) - } else { - None - }; - } - RowInfo { - buffer_id: region.buffer_id, - diff_status: region.status, - buffer_row, - base_text_row, - wrapped_buffer_row: None, - - multibuffer_row: Some(multibuffer_row), - expand_info: expand_direction.zip(region.excerpt_id).map( - |(direction, excerpt_id)| ExpandInfo { - direction, - excerpt_id, - }, - ), - } - }); - ix += line.len() + 1; - row_info - }) - .collect(); - - (text, row_infos, excerpt_boundary_rows) - } - - fn diffs_updated(&mut self, cx: &App) { - for excerpt in &mut self.excerpts { - let buffer = excerpt.buffer.read(cx).snapshot(); - let excerpt_range = excerpt.range.to_offset(&buffer); - let buffer_id = buffer.remote_id(); - let diff = self.diffs.get(&buffer_id).unwrap().read(cx); - let mut hunks = diff.hunks_in_row_range(0..u32::MAX, &buffer, cx).peekable(); - excerpt.expanded_diff_hunks.retain(|hunk_anchor| { - if !hunk_anchor.is_valid(&buffer) { - return false; - } - while let Some(hunk) = hunks.peek() { - match hunk.buffer_range.start.cmp(hunk_anchor, &buffer) { - cmp::Ordering::Less => { - hunks.next(); - } - cmp::Ordering::Equal => { - let hunk_range = hunk.buffer_range.to_offset(&buffer); - return hunk_range.end >= excerpt_range.start - && hunk_range.start <= excerpt_range.end; - } - cmp::Ordering::Greater => break, - } - } - false - }); - } - } - - fn add_diff(&mut self, diff: Entity, cx: &mut App) { - let buffer_id = diff.read(cx).buffer_id; - self.diffs.insert(buffer_id, diff); - } -} - -#[gpui::test(iterations = 100)] -async fn test_random_set_ranges(cx: &mut TestAppContext, mut rng: StdRng) { - let base_text = "a\n".repeat(100); - let buf = cx.update(|cx| cx.new(|cx| Buffer::local(base_text, cx))); - let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - - let operations = env::var("OPERATIONS") - .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) - .unwrap_or(10); - - fn row_ranges(ranges: &Vec>) -> Vec> { - ranges - .iter() - .map(|range| range.start.row..range.end.row) - .collect() - } - - for _ in 0..operations { - let snapshot = buf.update(cx, |buf, _| buf.snapshot()); - let num_ranges = rng.random_range(0..=10); - let max_row = snapshot.max_point().row; - let mut ranges = (0..num_ranges) - .map(|_| { - let start = rng.random_range(0..max_row); - let end = rng.random_range(start + 1..max_row + 1); - Point::row_range(start..end) - }) - .collect::>(); - ranges.sort_by_key(|range| range.start); - log::info!("Setting ranges: {:?}", row_ranges(&ranges)); - let (created, _) = multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_excerpts_for_path( - PathKey::for_buffer(&buf, cx), - buf.clone(), - ranges.clone(), - 2, - cx, - ) - }); - - assert_eq!(created.len(), ranges.len()); - - let snapshot = multibuffer.update(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - let mut last_end = None; - let mut seen_ranges = Vec::default(); - - for (_, buf, range) in snapshot.excerpts() { - let start = range.context.start.to_point(buf); - let end = range.context.end.to_point(buf); - seen_ranges.push(start..end); - - if let Some(last_end) = last_end.take() { - assert!( - start > last_end, - "multibuffer has out-of-order ranges: {:?}; {:?} <= {:?}", - row_ranges(&seen_ranges), - start, - last_end - ) - } - - ranges.retain(|range| range.start < start || range.end > end); - - last_end = Some(end) - } - - assert!( - ranges.is_empty(), - "multibuffer {:?} did not include all ranges: {:?}", - row_ranges(&seen_ranges), - row_ranges(&ranges) - ); - } -} - -// TODO(split-diff) bump up iterations -// #[gpui::test(iterations = 100)] -#[gpui::test] -async fn test_random_filtered_multibuffer(cx: &mut TestAppContext, rng: StdRng) { - let multibuffer = cx.new(|cx| { - let mut multibuffer = MultiBuffer::new(Capability::ReadWrite); - multibuffer.set_all_diff_hunks_expanded(cx); - multibuffer.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions)); - multibuffer - }); - let follower = multibuffer.update(cx, |multibuffer, cx| multibuffer.get_or_create_follower(cx)); - follower.update(cx, |follower, _| { - assert!(follower.all_diff_hunks_expanded()); - follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions)); - }); - test_random_multibuffer_impl(multibuffer, cx, rng).await; -} - -#[gpui::test(iterations = 100)] -async fn test_random_multibuffer(cx: &mut TestAppContext, rng: StdRng) { - let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - test_random_multibuffer_impl(multibuffer, cx, rng).await; -} - -async fn test_random_multibuffer_impl( - multibuffer: Entity, - cx: &mut TestAppContext, - mut rng: StdRng, -) { - let operations = env::var("OPERATIONS") - .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) - .unwrap_or(10); - - multibuffer.read_with(cx, |multibuffer, _| assert!(multibuffer.is_empty())); - let all_diff_hunks_expanded = - multibuffer.read_with(cx, |multibuffer, _| multibuffer.all_diff_hunks_expanded()); - let mut buffers: Vec> = Vec::new(); - let mut base_texts: HashMap = HashMap::default(); - let mut reference = ReferenceMultibuffer::default(); - let mut anchors = Vec::new(); - let mut old_versions = Vec::new(); - let mut old_follower_versions = Vec::new(); - let mut needs_diff_calculation = false; - - for _ in 0..operations { - match rng.random_range(0..100) { - 0..=14 if !buffers.is_empty() => { - let buffer = buffers.choose(&mut rng).unwrap(); - buffer.update(cx, |buf, cx| { - let edit_count = rng.random_range(1..5); - buf.randomly_edit(&mut rng, edit_count, cx); - log::info!("buffer text:\n{}", buf.text()); - needs_diff_calculation = true; - }); - cx.update(|cx| reference.diffs_updated(cx)); - } - 15..=19 if !reference.excerpts.is_empty() => { - multibuffer.update(cx, |multibuffer, cx| { - let ids = multibuffer.excerpt_ids(); - let mut excerpts = HashSet::default(); - for _ in 0..rng.random_range(0..ids.len()) { - excerpts.extend(ids.choose(&mut rng).copied()); - } - - let line_count = rng.random_range(0..5); - - let excerpt_ixs = excerpts - .iter() - .map(|id| reference.excerpts.iter().position(|e| e.id == *id).unwrap()) - .collect::>(); - log::info!("Expanding excerpts {excerpt_ixs:?} by {line_count} lines"); - multibuffer.expand_excerpts( - excerpts.iter().cloned(), - line_count, - ExpandExcerptDirection::UpAndDown, - cx, - ); - - reference.expand_excerpts(&excerpts, line_count, cx); - }); - } - 20..=29 if !reference.excerpts.is_empty() => { - let mut ids_to_remove = vec![]; - for _ in 0..rng.random_range(1..=3) { - let Some(excerpt) = reference.excerpts.choose(&mut rng) else { - break; - }; - let id = excerpt.id; - cx.update(|cx| reference.remove_excerpt(id, cx)); - ids_to_remove.push(id); - } - let snapshot = - multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - ids_to_remove.sort_unstable_by(|a, b| a.cmp(b, &snapshot)); - drop(snapshot); - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.remove_excerpts(ids_to_remove, cx) - }); - } - 30..=39 if !reference.excerpts.is_empty() => { - let multibuffer = - multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - let offset = multibuffer.clip_offset( - MultiBufferOffset(rng.random_range(0..=multibuffer.len().0)), - Bias::Left, - ); - let bias = if rng.random() { - Bias::Left - } else { - Bias::Right - }; - log::info!("Creating anchor at {} with bias {:?}", offset.0, bias); - anchors.push(multibuffer.anchor_at(offset, bias)); - anchors.sort_by(|a, b| a.cmp(b, &multibuffer)); - } - 40..=44 if !anchors.is_empty() => { - let multibuffer = - multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - let prev_len = anchors.len(); - anchors = multibuffer - .refresh_anchors(&anchors) - .into_iter() - .map(|a| a.1) - .collect(); - - // Ensure the newly-refreshed anchors point to a valid excerpt and don't - // overshoot its boundaries. - assert_eq!(anchors.len(), prev_len); - for anchor in &anchors { - if anchor.excerpt_id == ExcerptId::min() - || anchor.excerpt_id == ExcerptId::max() - { - continue; - } - - let excerpt = multibuffer.excerpt(anchor.excerpt_id).unwrap(); - assert_eq!(excerpt.id, anchor.excerpt_id); - assert!(excerpt.contains(anchor)); - } - } - 45..=55 if !reference.excerpts.is_empty() && !all_diff_hunks_expanded => { - multibuffer.update(cx, |multibuffer, cx| { - let snapshot = multibuffer.snapshot(cx); - let excerpt_ix = rng.random_range(0..reference.excerpts.len()); - let excerpt = &reference.excerpts[excerpt_ix]; - let start = excerpt.range.start; - let end = excerpt.range.end; - let range = snapshot.anchor_in_excerpt(excerpt.id, start).unwrap() - ..snapshot.anchor_in_excerpt(excerpt.id, end).unwrap(); - - log::info!( - "expanding diff hunks in range {:?} (excerpt id {:?}, index {excerpt_ix:?}, buffer id {:?})", - range.to_offset(&snapshot), - excerpt.id, - excerpt.buffer.read(cx).remote_id(), - ); - reference.expand_diff_hunks(excerpt.id, start..end, cx); - multibuffer.expand_diff_hunks(vec![range], cx); - }); - } - 56..=85 if needs_diff_calculation => { - multibuffer.update(cx, |multibuffer, cx| { - for buffer in multibuffer.all_buffers() { - let snapshot = buffer.read(cx).snapshot(); - multibuffer.diff_for(snapshot.remote_id()).unwrap().update( - cx, - |diff, cx| { - log::info!( - "recalculating diff for buffer {:?}", - snapshot.remote_id(), - ); - diff.recalculate_diff_sync(snapshot.text, cx); - }, - ); - } - reference.diffs_updated(cx); - needs_diff_calculation = false; - }); - } - _ => { - let buffer_handle = if buffers.is_empty() || rng.random_bool(0.4) { - let mut base_text = util::RandomCharIter::new(&mut rng) - .take(256) - .collect::(); - - let buffer = cx.new(|cx| Buffer::local(base_text.clone(), cx)); - text::LineEnding::normalize(&mut base_text); - base_texts.insert( - buffer.read_with(cx, |buffer, _| buffer.remote_id()), - base_text, - ); - buffers.push(buffer); - buffers.last().unwrap() - } else { - buffers.choose(&mut rng).unwrap() - }; - - let prev_excerpt_ix = rng.random_range(0..=reference.excerpts.len()); - let prev_excerpt_id = reference - .excerpts - .get(prev_excerpt_ix) - .map_or(ExcerptId::max(), |e| e.id); - let excerpt_ix = (prev_excerpt_ix + 1).min(reference.excerpts.len()); - - let (range, anchor_range) = buffer_handle.read_with(cx, |buffer, _| { - let end_row = rng.random_range(0..=buffer.max_point().row); - let start_row = rng.random_range(0..=end_row); - let end_ix = buffer.point_to_offset(Point::new(end_row, 0)); - let start_ix = buffer.point_to_offset(Point::new(start_row, 0)); - let anchor_range = buffer.anchor_before(start_ix)..buffer.anchor_after(end_ix); - - log::info!( - "Inserting excerpt at {} of {} for buffer {}: {:?}[{:?}] = {:?}", - excerpt_ix, - reference.excerpts.len(), - buffer.remote_id(), - buffer.text(), - start_ix..end_ix, - &buffer.text()[start_ix..end_ix] - ); - - (start_ix..end_ix, anchor_range) - }); - - let excerpt_id = multibuffer.update(cx, |multibuffer, cx| { - multibuffer - .insert_excerpts_after( - prev_excerpt_id, - buffer_handle.clone(), - [ExcerptRange::new(range.clone())], - cx, - ) - .pop() - .unwrap() - }); - - reference.insert_excerpt_after( - prev_excerpt_id, - excerpt_id, - (buffer_handle.clone(), anchor_range), - ); - - multibuffer.update(cx, |multibuffer, cx| { - let id = buffer_handle.read(cx).remote_id(); - if multibuffer.diff_for(id).is_none() { - let base_text = base_texts.get(&id).unwrap(); - let diff = cx - .new(|cx| BufferDiff::new_with_base_text(base_text, buffer_handle, cx)); - reference.add_diff(diff.clone(), cx); - multibuffer.add_diff(diff, cx) - } - }); - } - } - - if rng.random_bool(0.3) { - multibuffer.update(cx, |multibuffer, cx| { - old_versions.push((multibuffer.snapshot(cx), multibuffer.subscribe())); - - if let Some(follower) = &multibuffer.follower { - follower.update(cx, |follower, cx| { - old_follower_versions.push((follower.snapshot(cx), follower.subscribe())); - }) - } - }) - } - - multibuffer.read_with(cx, |multibuffer, cx| { - check_multibuffer(multibuffer, &reference, &anchors, cx, &mut rng); - - if let Some(follower) = &multibuffer.follower { - check_multibuffer(follower.read(cx), &reference, &anchors, cx, &mut rng); - } - }); - } - - let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - for (old_snapshot, subscription) in old_versions { - check_multibuffer_edits(&snapshot, &old_snapshot, subscription); - } - if let Some(follower) = multibuffer.read_with(cx, |multibuffer, _| multibuffer.follower.clone()) - { - let snapshot = follower.read_with(cx, |follower, cx| follower.snapshot(cx)); - for (old_snapshot, subscription) in old_follower_versions { - check_multibuffer_edits(&snapshot, &old_snapshot, subscription); - } - } -} - -fn check_multibuffer( - multibuffer: &MultiBuffer, - reference: &ReferenceMultibuffer, - anchors: &[Anchor], - cx: &App, - rng: &mut StdRng, -) { - let snapshot = multibuffer.snapshot(cx); - let filter_mode = multibuffer.filter_mode; - assert!(filter_mode.is_some() == snapshot.all_diff_hunks_expanded); - let actual_text = snapshot.text(); - let actual_boundary_rows = snapshot - .excerpt_boundaries_in_range(MultiBufferOffset(0)..) - .map(|b| b.row) - .collect::>(); - let actual_row_infos = snapshot.row_infos(MultiBufferRow(0)).collect::>(); - - let (expected_text, expected_row_infos, expected_boundary_rows) = - reference.expected_content(filter_mode, snapshot.all_diff_hunks_expanded, cx); - - let (unfiltered_text, unfiltered_row_infos, unfiltered_boundary_rows) = - reference.expected_content(None, snapshot.all_diff_hunks_expanded, cx); - - let has_diff = actual_row_infos - .iter() - .any(|info| info.diff_status.is_some()) - || unfiltered_row_infos - .iter() - .any(|info| info.diff_status.is_some()); - let actual_diff = format_diff( - &actual_text, - &actual_row_infos, - &actual_boundary_rows, - Some(has_diff), - ); - let expected_diff = format_diff( - &expected_text, - &expected_row_infos, - &expected_boundary_rows, - Some(has_diff), - ); - - log::info!("Multibuffer content:\n{}", actual_diff); - if filter_mode.is_some() { - log::info!( - "Unfiltered multibuffer content:\n{}", - format_diff( - &unfiltered_text, - &unfiltered_row_infos, - &unfiltered_boundary_rows, - None, - ), - ); - } - - assert_eq!( - actual_row_infos.len(), - actual_text.split('\n').count(), - "line count: {}", - actual_text.split('\n').count() - ); - pretty_assertions::assert_eq!(actual_diff, expected_diff); - pretty_assertions::assert_eq!(actual_text, expected_text); - pretty_assertions::assert_eq!(actual_row_infos, expected_row_infos); - - for _ in 0..5 { - let start_row = rng.random_range(0..=expected_row_infos.len()); - assert_eq!( - snapshot - .row_infos(MultiBufferRow(start_row as u32)) - .collect::>(), - &expected_row_infos[start_row..], - "buffer_rows({})", - start_row - ); - } - - assert_eq!( - snapshot.widest_line_number(), - expected_row_infos - .into_iter() - .filter_map(|info| { - if info.diff_status.is_some_and(|status| status.is_deleted()) { - None - } else { - info.buffer_row - } - }) - .max() - .unwrap() - + 1 - ); - let reference_ranges = reference - .excerpts - .iter() - .map(|excerpt| { - ( - excerpt.id, - excerpt.range.to_offset(&excerpt.buffer.read(cx).snapshot()), - ) - }) - .collect::>(); - for i in 0..snapshot.len().0 { - let excerpt = snapshot - .excerpt_containing(MultiBufferOffset(i)..MultiBufferOffset(i)) - .unwrap(); - assert_eq!( - excerpt.buffer_range().start.0..excerpt.buffer_range().end.0, - reference_ranges[&excerpt.id()] - ); - } - - assert_consistent_line_numbers(&snapshot); - assert_position_translation(&snapshot); - - for (row, line) in expected_text.split('\n').enumerate() { - assert_eq!( - snapshot.line_len(MultiBufferRow(row as u32)), - line.len() as u32, - "line_len({}).", - row - ); - } - - let text_rope = Rope::from(expected_text.as_str()); - for _ in 0..10 { - let end_ix = text_rope.clip_offset(rng.random_range(0..=text_rope.len()), Bias::Right); - let start_ix = text_rope.clip_offset(rng.random_range(0..=end_ix), Bias::Left); - - let text_for_range = snapshot - .text_for_range(MultiBufferOffset(start_ix)..MultiBufferOffset(end_ix)) - .collect::(); - assert_eq!( - text_for_range, - &expected_text[start_ix..end_ix], - "incorrect text for range {:?}", - start_ix..end_ix - ); - - let expected_summary = - MBTextSummary::from(TextSummary::from(&expected_text[start_ix..end_ix])); - assert_eq!( - snapshot.text_summary_for_range::( - MultiBufferOffset(start_ix)..MultiBufferOffset(end_ix) - ), - expected_summary, - "incorrect summary for range {:?}", - start_ix..end_ix - ); - } - - // Anchor resolution - let summaries = snapshot.summaries_for_anchors::(anchors); - assert_eq!(anchors.len(), summaries.len()); - for (anchor, resolved_offset) in anchors.iter().zip(summaries) { - assert!(resolved_offset <= snapshot.len()); - assert_eq!( - snapshot.summary_for_anchor::(anchor), - resolved_offset, - "anchor: {:?}", - anchor - ); - } - - for _ in 0..10 { - let end_ix = text_rope.clip_offset(rng.random_range(0..=text_rope.len()), Bias::Right); - assert_eq!( - snapshot - .reversed_chars_at(MultiBufferOffset(end_ix)) - .collect::(), - expected_text[..end_ix].chars().rev().collect::(), - ); - } - - for _ in 0..10 { - let end_ix = rng.random_range(0..=text_rope.len()); - let end_ix = text_rope.floor_char_boundary(end_ix); - let start_ix = rng.random_range(0..=end_ix); - let start_ix = text_rope.floor_char_boundary(start_ix); - assert_eq!( - snapshot - .bytes_in_range(MultiBufferOffset(start_ix)..MultiBufferOffset(end_ix)) - .flatten() - .copied() - .collect::>(), - expected_text.as_bytes()[start_ix..end_ix].to_vec(), - "bytes_in_range({:?})", - start_ix..end_ix, - ); - } -} - -fn check_multibuffer_edits( - snapshot: &MultiBufferSnapshot, - old_snapshot: &MultiBufferSnapshot, - subscription: Subscription, -) { - let edits = subscription.consume().into_inner(); - - log::info!( - "applying subscription edits to old text: {:?}: {:#?}", - old_snapshot.text(), - edits, - ); - - let mut text = old_snapshot.text(); - for edit in edits { - let new_text: String = snapshot - .text_for_range(edit.new.start..edit.new.end) - .collect(); - text.replace_range( - (edit.new.start.0..edit.new.start.0 + (edit.old.end.0 - edit.old.start.0)).clone(), - &new_text, - ); - pretty_assertions::assert_eq!( - &text[0..edit.new.end.0], - snapshot - .text_for_range(MultiBufferOffset(0)..edit.new.end) - .collect::() - ); - } - pretty_assertions::assert_eq!(text, snapshot.text()); -} - -#[gpui::test] -fn test_history(cx: &mut App) { - let test_settings = SettingsStore::test(cx); - cx.set_global(test_settings); - - let group_interval: Duration = Duration::from_millis(1); - let buffer_1 = cx.new(|cx| { - let mut buf = Buffer::local("1234", cx); - buf.set_group_interval(group_interval); - buf - }); - let buffer_2 = cx.new(|cx| { - let mut buf = Buffer::local("5678", cx); - buf.set_group_interval(group_interval); - buf - }); - let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - multibuffer.update(cx, |this, _| { - this.set_group_interval(group_interval); - }); - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange::new(0..buffer_1.read(cx).len())], - cx, - ); - multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange::new(0..buffer_2.read(cx).len())], - cx, - ); - }); - - let mut now = Instant::now(); - - multibuffer.update(cx, |multibuffer, cx| { - let transaction_1 = multibuffer.start_transaction_at(now, cx).unwrap(); - multibuffer.edit( - [ - (Point::new(0, 0)..Point::new(0, 0), "A"), - (Point::new(1, 0)..Point::new(1, 0), "A"), - ], - None, - cx, - ); - multibuffer.edit( - [ - (Point::new(0, 1)..Point::new(0, 1), "B"), - (Point::new(1, 1)..Point::new(1, 1), "B"), - ], - None, - cx, - ); - multibuffer.end_transaction_at(now, cx); - assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678"); - - // Verify edited ranges for transaction 1 - assert_eq!( - multibuffer.edited_ranges_for_transaction(transaction_1, cx), - &[ - Point::new(0, 0)..Point::new(0, 2), - Point::new(1, 0)..Point::new(1, 2) - ] - ); - - // Edit buffer 1 through the multibuffer - now += 2 * group_interval; - multibuffer.start_transaction_at(now, cx); - multibuffer.edit( - [(MultiBufferOffset(2)..MultiBufferOffset(2), "C")], - None, - cx, - ); - multibuffer.end_transaction_at(now, cx); - assert_eq!(multibuffer.read(cx).text(), "ABC1234\nAB5678"); - - // Edit buffer 1 independently - buffer_1.update(cx, |buffer_1, cx| { - buffer_1.start_transaction_at(now); - buffer_1.edit([(3..3, "D")], None, cx); - buffer_1.end_transaction_at(now, cx); - - now += 2 * group_interval; - buffer_1.start_transaction_at(now); - buffer_1.edit([(4..4, "E")], None, cx); - buffer_1.end_transaction_at(now, cx); - }); - assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\nAB5678"); - - // An undo in the multibuffer undoes the multibuffer transaction - // and also any individual buffer edits that have occurred since - // that transaction. - multibuffer.undo(cx); - assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678"); - - multibuffer.undo(cx); - assert_eq!(multibuffer.read(cx).text(), "1234\n5678"); - - multibuffer.redo(cx); - assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678"); - - multibuffer.redo(cx); - assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\nAB5678"); - - // Undo buffer 2 independently. - buffer_2.update(cx, |buffer_2, cx| buffer_2.undo(cx)); - assert_eq!(multibuffer.read(cx).text(), "ABCDE1234\n5678"); - - // An undo in the multibuffer undoes the components of the - // the last multibuffer transaction that are not already undone. - multibuffer.undo(cx); - assert_eq!(multibuffer.read(cx).text(), "AB1234\n5678"); - - multibuffer.undo(cx); - assert_eq!(multibuffer.read(cx).text(), "1234\n5678"); - - multibuffer.redo(cx); - assert_eq!(multibuffer.read(cx).text(), "AB1234\nAB5678"); - - buffer_1.update(cx, |buffer_1, cx| buffer_1.redo(cx)); - assert_eq!(multibuffer.read(cx).text(), "ABCD1234\nAB5678"); - - // Redo stack gets cleared after an edit. - now += 2 * group_interval; - multibuffer.start_transaction_at(now, cx); - multibuffer.edit( - [(MultiBufferOffset(0)..MultiBufferOffset(0), "X")], - None, - cx, - ); - multibuffer.end_transaction_at(now, cx); - assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678"); - multibuffer.redo(cx); - assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678"); - multibuffer.undo(cx); - assert_eq!(multibuffer.read(cx).text(), "ABCD1234\nAB5678"); - multibuffer.undo(cx); - assert_eq!(multibuffer.read(cx).text(), "1234\n5678"); - - // Transactions can be grouped manually. - multibuffer.redo(cx); - multibuffer.redo(cx); - assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678"); - multibuffer.group_until_transaction(transaction_1, cx); - multibuffer.undo(cx); - assert_eq!(multibuffer.read(cx).text(), "1234\n5678"); - multibuffer.redo(cx); - assert_eq!(multibuffer.read(cx).text(), "XABCD1234\nAB5678"); - }); -} - -#[gpui::test] -async fn test_enclosing_indent(cx: &mut TestAppContext) { - async fn enclosing_indent( - text: &str, - buffer_row: u32, - cx: &mut TestAppContext, - ) -> Option<(Range, LineIndent)> { - let buffer = cx.update(|cx| MultiBuffer::build_simple(text, cx)); - let snapshot = cx.read(|cx| buffer.read(cx).snapshot(cx)); - let (range, indent) = snapshot - .enclosing_indent(MultiBufferRow(buffer_row)) - .await?; - Some((range.start.0..range.end.0, indent)) - } - - assert_eq!( - enclosing_indent( - indoc!( - " - fn b() { - if c { - let d = 2; - } - } - " - ), - 1, - cx, - ) - .await, - Some(( - 1..2, - LineIndent { - tabs: 0, - spaces: 4, - line_blank: false, - } - )) - ); - - assert_eq!( - enclosing_indent( - indoc!( - " - fn b() { - if c { - let d = 2; - } - } - " - ), - 2, - cx, - ) - .await, - Some(( - 1..2, - LineIndent { - tabs: 0, - spaces: 4, - line_blank: false, - } - )) - ); - - assert_eq!( - enclosing_indent( - indoc!( - " - fn b() { - if c { - let d = 2; - - let e = 5; - } - } - " - ), - 3, - cx, - ) - .await, - Some(( - 1..4, - LineIndent { - tabs: 0, - spaces: 4, - line_blank: false, - } - )) - ); -} - -#[gpui::test] -async fn test_summaries_for_anchors(cx: &mut TestAppContext) { - let base_text_1 = indoc!( - " - bar - " - ); - let text_1 = indoc!( - " - BAR - " - ); - let base_text_2 = indoc!( - " - foo - " - ); - let text_2 = indoc!( - " - FOO - " - ); - - let buffer_1 = cx.new(|cx| Buffer::local(text_1, cx)); - let buffer_2 = cx.new(|cx| Buffer::local(text_2, cx)); - let diff_1 = cx.new(|cx| BufferDiff::new_with_base_text(base_text_1, &buffer_1, cx)); - let diff_2 = cx.new(|cx| BufferDiff::new_with_base_text(base_text_2, &buffer_2, cx)); - cx.run_until_parked(); - - let mut ids = vec![]; - let multibuffer = cx.new(|cx| { - let mut multibuffer = MultiBuffer::new(Capability::ReadWrite); - multibuffer.set_all_diff_hunks_expanded(cx); - ids.extend(multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)], - cx, - )); - ids.extend(multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)], - cx, - )); - multibuffer.add_diff(diff_1.clone(), cx); - multibuffer.add_diff(diff_2.clone(), cx); - multibuffer - }); - - let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { - (multibuffer.snapshot(cx), multibuffer.subscribe()) - }); - - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - - bar - + BAR - - - foo - + FOO - " - ), - ); - - let anchor_1 = Anchor::in_buffer(ids[0], text::Anchor::MIN); - let point_1 = snapshot.summaries_for_anchors::([&anchor_1])[0]; - assert_eq!(point_1, Point::new(0, 0)); - - let anchor_2 = Anchor::in_buffer(ids[1], text::Anchor::MIN); - let point_2 = snapshot.summaries_for_anchors::([&anchor_2])[0]; - assert_eq!(point_2, Point::new(3, 0)); -} - -#[gpui::test] -async fn test_trailing_deletion_without_newline(cx: &mut TestAppContext) { - let base_text_1 = "one\ntwo".to_owned(); - let text_1 = "one\n".to_owned(); - - let buffer_1 = cx.new(|cx| Buffer::local(text_1, cx)); - let diff_1 = cx.new(|cx| BufferDiff::new_with_base_text(&base_text_1, &buffer_1, cx)); - cx.run_until_parked(); - - let multibuffer = cx.new(|cx| { - let mut multibuffer = MultiBuffer::singleton(buffer_1.clone(), cx); - multibuffer.add_diff(diff_1.clone(), cx); - multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx); - multibuffer - }); - - let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { - (multibuffer.snapshot(cx), multibuffer.subscribe()) - }); - - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - one - - two - " - ), - ); - - assert_eq!(snapshot.max_point(), Point::new(2, 0)); - assert_eq!(snapshot.len().0, 8); - - assert_eq!( - snapshot - .dimensions_from_points::([Point::new(2, 0)]) - .collect::>(), - vec![Point::new(2, 0)] - ); - - let (_, translated_offset) = snapshot.point_to_buffer_offset(Point::new(2, 0)).unwrap(); - assert_eq!(translated_offset.0, "one\n".len()); - let (_, translated_point, _) = snapshot.point_to_buffer_point(Point::new(2, 0)).unwrap(); - assert_eq!(translated_point, Point::new(1, 0)); - - // The same, for an excerpt that's not at the end of the multibuffer. - - let text_2 = "foo\n".to_owned(); - let buffer_2 = cx.new(|cx| Buffer::local(&text_2, cx)); - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))], - cx, - ); - }); - - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - one - - two - - foo - " - ), - ); - - assert_eq!( - snapshot - .dimensions_from_points::([Point::new(2, 0)]) - .collect::>(), - vec![Point::new(2, 0)] - ); - - let buffer_1_id = buffer_1.read_with(cx, |buffer_1, _| buffer_1.remote_id()); - let (buffer, translated_offset) = snapshot.point_to_buffer_offset(Point::new(2, 0)).unwrap(); - assert_eq!(buffer.remote_id(), buffer_1_id); - assert_eq!(translated_offset.0, "one\n".len()); - let (buffer, translated_point, _) = snapshot.point_to_buffer_point(Point::new(2, 0)).unwrap(); - assert_eq!(buffer.remote_id(), buffer_1_id); - assert_eq!(translated_point, Point::new(1, 0)); -} - -fn format_diff( - text: &str, - row_infos: &Vec, - boundary_rows: &HashSet, - has_diff: Option, -) -> String { - let has_diff = - has_diff.unwrap_or_else(|| row_infos.iter().any(|info| info.diff_status.is_some())); - text.split('\n') - .enumerate() - .zip(row_infos) - .map(|((ix, line), info)| { - let marker = match info.diff_status.map(|status| status.kind) { - Some(DiffHunkStatusKind::Added) => "+ ", - Some(DiffHunkStatusKind::Deleted) => "- ", - Some(DiffHunkStatusKind::Modified) => unreachable!(), - None => { - if has_diff && !line.is_empty() { - " " - } else { - "" - } - } - }; - let boundary_row = if boundary_rows.contains(&MultiBufferRow(ix as u32)) { - if has_diff { - " ----------\n" - } else { - "---------\n" - } - } else { - "" - }; - let expand = info - .expand_info - .map(|expand_info| match expand_info.direction { - ExpandExcerptDirection::Up => " [↑]", - ExpandExcerptDirection::Down => " [↓]", - ExpandExcerptDirection::UpAndDown => " [↕]", - }) - .unwrap_or_default(); - - format!("{boundary_row}{marker}{line}{expand}") - // let mbr = info - // .multibuffer_row - // .map(|row| format!("{:0>3}", row.0)) - // .unwrap_or_else(|| "???".to_string()); - // let byte_range = format!("{byte_range_start:0>3}..{byte_range_end:0>3}"); - // format!("{boundary_row}Row: {mbr}, Bytes: {byte_range} | {marker}{line}{expand}") - }) - .collect::>() - .join("\n") -} - -// fn format_transforms(snapshot: &MultiBufferSnapshot) -> String { -// snapshot -// .diff_transforms -// .iter() -// .map(|transform| { -// let (kind, summary) = match transform { -// DiffTransform::DeletedHunk { summary, .. } => (" Deleted", (*summary).into()), -// DiffTransform::FilteredInsertedHunk { summary, .. } => (" Filtered", *summary), -// DiffTransform::InsertedHunk { summary, .. } => (" Inserted", *summary), -// DiffTransform::Unmodified { summary, .. } => ("Unmodified", *summary), -// }; -// format!("{kind}(len: {}, lines: {:?})", summary.len, summary.lines) -// }) -// .join("\n") -// } - -// fn format_excerpts(snapshot: &MultiBufferSnapshot) -> String { -// snapshot -// .excerpts -// .iter() -// .map(|excerpt| { -// format!( -// "Excerpt(buffer_range = {:?}, lines = {:?}, has_trailing_newline = {:?})", -// excerpt.range.context.to_point(&excerpt.buffer), -// excerpt.text_summary.lines, -// excerpt.has_trailing_newline -// ) -// }) -// .join("\n") -// } - -#[gpui::test] -async fn test_basic_filtering(cx: &mut TestAppContext) { - let text = indoc!( - " - ZERO - one - TWO - three - six - " - ); - let base_text = indoc!( - " - one - two - three - four - five - six - " - ); - - let buffer = cx.new(|cx| Buffer::local(text, cx)); - let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); - cx.run_until_parked(); - - let multibuffer = cx.new(|cx| { - let mut multibuffer = MultiBuffer::singleton(buffer.clone(), cx); - multibuffer.add_diff(diff.clone(), cx); - multibuffer.set_all_diff_hunks_expanded(cx); - multibuffer.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions)); - multibuffer - }); - - let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { - (multibuffer.snapshot(cx), multibuffer.subscribe()) - }); - - assert_eq!(snapshot.text(), base_text); - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc!( - " - one - - two - three - - four - - five - six - " - ), - ); - - buffer.update(cx, |buffer, cx| { - buffer.edit_via_marked_text( - indoc!( - " - ZERO - one - «»W«O - T»hree - six - " - ), - None, - cx, - ); - }); - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc! { - " - one - - two - - four - - five - six - " - }, - ); -} - -#[gpui::test] -async fn test_base_text_line_numbers(cx: &mut TestAppContext) { - let base_text = indoc! {" - one - two - three - four - five - six - "}; - let buffer_text = indoc! {" - two - THREE - five - six - SEVEN - "}; - let multibuffer = cx.update(|cx| MultiBuffer::build_simple(buffer_text, cx)); - multibuffer.update(cx, |multibuffer, cx| { - let buffer = multibuffer.all_buffers().into_iter().next().unwrap(); - let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); - multibuffer.set_all_diff_hunks_expanded(cx); - multibuffer.add_diff(diff, cx); - }); - let (mut snapshot, mut subscription) = multibuffer.update(cx, |multibuffer, cx| { - (multibuffer.snapshot(cx), multibuffer.subscribe()) - }); - - assert_new_snapshot( - &multibuffer, - &mut snapshot, - &mut subscription, - cx, - indoc! {" - - one - two - - three - - four - + THREE - five - six - + SEVEN - "}, - ); - let base_text_rows = snapshot - .row_infos(MultiBufferRow(0)) - .map(|row_info| row_info.base_text_row) - .collect::>(); - pretty_assertions::assert_eq!( - base_text_rows, - vec![ - Some(BaseTextRow(0)), - Some(BaseTextRow(1)), - Some(BaseTextRow(2)), - Some(BaseTextRow(3)), - None, - Some(BaseTextRow(4)), - Some(BaseTextRow(5)), - None, - Some(BaseTextRow(6)), - ] - ) -} - -#[track_caller] -fn assert_excerpts_match( - multibuffer: &Entity, - cx: &mut TestAppContext, - expected: &str, -) { - let mut output = String::new(); - multibuffer.read_with(cx, |multibuffer, cx| { - for (_, buffer, range) in multibuffer.snapshot(cx).excerpts() { - output.push_str("-----\n"); - output.extend(buffer.text_for_range(range.context)); - if !output.ends_with('\n') { - output.push('\n'); - } - } - }); - assert_eq!(output, expected); -} - -#[track_caller] -fn assert_new_snapshot( - multibuffer: &Entity, - snapshot: &mut MultiBufferSnapshot, - subscription: &mut Subscription, - cx: &mut TestAppContext, - expected_diff: &str, -) { - let new_snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - let actual_text = new_snapshot.text(); - let line_infos = new_snapshot - .row_infos(MultiBufferRow(0)) - .collect::>(); - let actual_diff = format_diff(&actual_text, &line_infos, &Default::default(), None); - pretty_assertions::assert_eq!(actual_diff, expected_diff); - check_edits( - snapshot, - &new_snapshot, - &subscription.consume().into_inner(), - ); - *snapshot = new_snapshot; -} - -#[track_caller] -fn check_edits( - old_snapshot: &MultiBufferSnapshot, - new_snapshot: &MultiBufferSnapshot, - edits: &[Edit], -) { - let mut text = old_snapshot.text(); - let new_text = new_snapshot.text(); - for edit in edits.iter().rev() { - if !text.is_char_boundary(edit.old.start.0) - || !text.is_char_boundary(edit.old.end.0) - || !new_text.is_char_boundary(edit.new.start.0) - || !new_text.is_char_boundary(edit.new.end.0) - { - panic!( - "invalid edits: {:?}\nold text: {:?}\nnew text: {:?}", - edits, text, new_text - ); - } - - text.replace_range( - edit.old.start.0..edit.old.end.0, - &new_text[edit.new.start.0..edit.new.end.0], - ); - } - - pretty_assertions::assert_eq!(text, new_text, "invalid edits: {:?}", edits); -} - -#[track_caller] -fn assert_chunks_in_ranges(snapshot: &MultiBufferSnapshot) { - let full_text = snapshot.text(); - for ix in 0..full_text.len() { - let mut chunks = snapshot.chunks(MultiBufferOffset(0)..snapshot.len(), false); - chunks.seek(MultiBufferOffset(ix)..snapshot.len()); - let tail = chunks.map(|chunk| chunk.text).collect::(); - assert_eq!(tail, &full_text[ix..], "seek to range: {:?}", ix..); - } -} - -#[track_caller] -fn assert_consistent_line_numbers(snapshot: &MultiBufferSnapshot) { - let all_line_numbers = snapshot.row_infos(MultiBufferRow(0)).collect::>(); - for start_row in 1..all_line_numbers.len() { - let line_numbers = snapshot - .row_infos(MultiBufferRow(start_row as u32)) - .collect::>(); - assert_eq!( - line_numbers, - all_line_numbers[start_row..], - "start_row: {start_row}" - ); - } -} - -#[track_caller] -fn assert_position_translation(snapshot: &MultiBufferSnapshot) { - let text = Rope::from(snapshot.text()); - - let mut left_anchors = Vec::new(); - let mut right_anchors = Vec::new(); - let mut offsets = Vec::new(); - let mut points = Vec::new(); - for offset in 0..=text.len() + 1 { - let offset = MultiBufferOffset(offset); - let clipped_left = snapshot.clip_offset(offset, Bias::Left); - let clipped_right = snapshot.clip_offset(offset, Bias::Right); - assert_eq!( - clipped_left.0, - text.clip_offset(offset.0, Bias::Left), - "clip_offset({offset:?}, Left)" - ); - assert_eq!( - clipped_right.0, - text.clip_offset(offset.0, Bias::Right), - "clip_offset({offset:?}, Right)" - ); - assert_eq!( - snapshot.offset_to_point(clipped_left), - text.offset_to_point(clipped_left.0), - "offset_to_point({})", - clipped_left.0 - ); - assert_eq!( - snapshot.offset_to_point(clipped_right), - text.offset_to_point(clipped_right.0), - "offset_to_point({})", - clipped_right.0 - ); - let anchor_after = snapshot.anchor_after(clipped_left); - assert_eq!( - anchor_after.to_offset(snapshot), - clipped_left, - "anchor_after({}).to_offset {anchor_after:?}", - clipped_left.0 - ); - let anchor_before = snapshot.anchor_before(clipped_left); - assert_eq!( - anchor_before.to_offset(snapshot), - clipped_left, - "anchor_before({}).to_offset", - clipped_left.0 - ); - left_anchors.push(anchor_before); - right_anchors.push(anchor_after); - offsets.push(clipped_left); - points.push(text.offset_to_point(clipped_left.0)); - } - - for row in 0..text.max_point().row { - for column in 0..text.line_len(row) + 1 { - let point = Point { row, column }; - let clipped_left = snapshot.clip_point(point, Bias::Left); - let clipped_right = snapshot.clip_point(point, Bias::Right); - assert_eq!( - clipped_left, - text.clip_point(point, Bias::Left), - "clip_point({point:?}, Left)" - ); - assert_eq!( - clipped_right, - text.clip_point(point, Bias::Right), - "clip_point({point:?}, Right)" - ); - assert_eq!( - snapshot.point_to_offset(clipped_left).0, - text.point_to_offset(clipped_left), - "point_to_offset({clipped_left:?})" - ); - assert_eq!( - snapshot.point_to_offset(clipped_right).0, - text.point_to_offset(clipped_right), - "point_to_offset({clipped_right:?})" - ); - } - } - - assert_eq!( - snapshot.summaries_for_anchors::(&left_anchors), - offsets, - "left_anchors <-> offsets" - ); - assert_eq!( - snapshot.summaries_for_anchors::(&left_anchors), - points, - "left_anchors <-> points" - ); - assert_eq!( - snapshot.summaries_for_anchors::(&right_anchors), - offsets, - "right_anchors <-> offsets" - ); - assert_eq!( - snapshot.summaries_for_anchors::(&right_anchors), - points, - "right_anchors <-> points" - ); - - for (anchors, bias) in [(&left_anchors, Bias::Left), (&right_anchors, Bias::Right)] { - for (ix, (offset, anchor)) in offsets.iter().zip(anchors).enumerate() { - if ix > 0 && *offset == MultiBufferOffset(252) && offset > &offsets[ix - 1] { - let prev_anchor = left_anchors[ix - 1]; - assert!( - anchor.cmp(&prev_anchor, snapshot).is_gt(), - "anchor({}, {bias:?}).cmp(&anchor({}, {bias:?}).is_gt()", - offsets[ix], - offsets[ix - 1], - ); - assert!( - prev_anchor.cmp(anchor, snapshot).is_lt(), - "anchor({}, {bias:?}).cmp(&anchor({}, {bias:?}).is_lt()", - offsets[ix - 1], - offsets[ix], - ); - } - } - } - - if let Some((buffer, offset)) = snapshot.point_to_buffer_offset(snapshot.max_point()) { - assert!(offset.0 <= buffer.len()); - } - if let Some((buffer, point, _)) = snapshot.point_to_buffer_point(snapshot.max_point()) { - assert!(point <= buffer.max_point()); - } -} - -fn assert_line_indents(snapshot: &MultiBufferSnapshot) { - let max_row = snapshot.max_point().row; - let buffer_id = snapshot.excerpts().next().unwrap().1.remote_id(); - let text = text::Buffer::new(ReplicaId::LOCAL, buffer_id, snapshot.text()); - let mut line_indents = text - .line_indents_in_row_range(0..max_row + 1) - .collect::>(); - for start_row in 0..snapshot.max_point().row { - pretty_assertions::assert_eq!( - snapshot - .line_indents(MultiBufferRow(start_row), |_| true) - .map(|(row, indent, _)| (row.0, indent)) - .collect::>(), - &line_indents[(start_row as usize)..], - "line_indents({start_row})" - ); - } - - line_indents.reverse(); - pretty_assertions::assert_eq!( - snapshot - .reversed_line_indents(MultiBufferRow(max_row), |_| true) - .map(|(row, indent, _)| (row.0, indent)) - .collect::>(), - &line_indents[..], - "reversed_line_indents({max_row})" - ); -} - -#[gpui::test] -fn test_new_empty_buffer_uses_untitled_title(cx: &mut App) { - let buffer = cx.new(|cx| Buffer::local("", cx)); - let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); - - assert_eq!(multibuffer.read(cx).title(cx), "untitled"); -} - -#[gpui::test] -fn test_new_empty_buffer_uses_untitled_title_when_only_contains_whitespace(cx: &mut App) { - let buffer = cx.new(|cx| Buffer::local("\n ", cx)); - let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); - - assert_eq!(multibuffer.read(cx).title(cx), "untitled"); -} - -#[gpui::test] -fn test_new_empty_buffer_takes_first_line_for_title(cx: &mut App) { - let buffer = cx.new(|cx| Buffer::local("Hello World\nSecond line", cx)); - let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); - - assert_eq!(multibuffer.read(cx).title(cx), "Hello World"); -} - -#[gpui::test] -fn test_new_empty_buffer_takes_trimmed_first_line_for_title(cx: &mut App) { - let buffer = cx.new(|cx| Buffer::local("\nHello, World ", cx)); - let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); - - assert_eq!(multibuffer.read(cx).title(cx), "Hello, World"); -} - -#[gpui::test] -fn test_new_empty_buffer_uses_truncated_first_line_for_title(cx: &mut App) { - let title = "aaaaaaaaaabbbbbbbbbbccccccccccddddddddddeeeeeeeeee"; - let title_after = "aaaaaaaaaabbbbbbbbbbccccccccccdddddddddd"; - let buffer = cx.new(|cx| Buffer::local(title, cx)); - let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); - - assert_eq!(multibuffer.read(cx).title(cx), title_after); -} - -#[gpui::test] -fn test_new_empty_buffer_uses_truncated_first_line_for_title_after_merging_adjacent_spaces( - cx: &mut App, -) { - let title = "aaaaaaaaaabbbbbbbbbb ccccccccccddddddddddeeeeeeeeee"; - let title_after = "aaaaaaaaaabbbbbbbbbb ccccccccccddddddddd"; - let buffer = cx.new(|cx| Buffer::local(title, cx)); - let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); - - assert_eq!(multibuffer.read(cx).title(cx), title_after); -} - -#[gpui::test] -fn test_new_empty_buffers_title_can_be_set(cx: &mut App) { - let buffer = cx.new(|cx| Buffer::local("Hello World", cx)); - let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); - assert_eq!(multibuffer.read(cx).title(cx), "Hello World"); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.set_title("Hey".into(), cx) - }); - assert_eq!(multibuffer.read(cx).title(cx), "Hey"); -} - -#[gpui::test(iterations = 100)] -fn test_random_chunk_bitmaps(cx: &mut App, mut rng: StdRng) { - let multibuffer = if rng.random() { - let len = rng.random_range(0..10000); - let text = RandomCharIter::new(&mut rng).take(len).collect::(); - let buffer = cx.new(|cx| Buffer::local(text, cx)); - cx.new(|cx| MultiBuffer::singleton(buffer, cx)) - } else { - MultiBuffer::build_random(&mut rng, cx) - }; - - let snapshot = multibuffer.read(cx).snapshot(cx); - - let chunks = snapshot.chunks(MultiBufferOffset(0)..snapshot.len(), false); - - for chunk in chunks { - let chunk_text = chunk.text; - let chars_bitmap = chunk.chars; - let tabs_bitmap = chunk.tabs; - - if chunk_text.is_empty() { - assert_eq!( - chars_bitmap, 0, - "Empty chunk should have empty chars bitmap" - ); - assert_eq!(tabs_bitmap, 0, "Empty chunk should have empty tabs bitmap"); - continue; - } - - assert!( - chunk_text.len() <= 128, - "Chunk text length {} exceeds 128 bytes", - chunk_text.len() - ); - - // Verify chars bitmap - let char_indices = chunk_text - .char_indices() - .map(|(i, _)| i) - .collect::>(); - - for byte_idx in 0..chunk_text.len() { - let should_have_bit = char_indices.contains(&byte_idx); - let has_bit = chars_bitmap & (1 << byte_idx) != 0; - - if has_bit != should_have_bit { - eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes()); - eprintln!("Char indices: {:?}", char_indices); - eprintln!("Chars bitmap: {:#b}", chars_bitmap); - } - - assert_eq!( - has_bit, should_have_bit, - "Chars bitmap mismatch at byte index {} in chunk {:?}. Expected bit: {}, Got bit: {}", - byte_idx, chunk_text, should_have_bit, has_bit - ); - } - - for (byte_idx, byte) in chunk_text.bytes().enumerate() { - let is_tab = byte == b'\t'; - let has_bit = tabs_bitmap & (1 << byte_idx) != 0; - - if has_bit != is_tab { - eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes()); - eprintln!("Tabs bitmap: {:#b}", tabs_bitmap); - assert_eq!( - has_bit, is_tab, - "Tabs bitmap mismatch at byte index {} in chunk {:?}. Byte: {:?}, Expected bit: {}, Got bit: {}", - byte_idx, chunk_text, byte as char, is_tab, has_bit - ); - } - } - } -} - -#[gpui::test(iterations = 10)] -fn test_random_chunk_bitmaps_with_diffs(cx: &mut App, mut rng: StdRng) { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - use buffer_diff::BufferDiff; - use util::RandomCharIter; - - let multibuffer = if rng.random() { - let len = rng.random_range(100..10000); - let text = RandomCharIter::new(&mut rng).take(len).collect::(); - let buffer = cx.new(|cx| Buffer::local(text, cx)); - cx.new(|cx| MultiBuffer::singleton(buffer, cx)) - } else { - MultiBuffer::build_random(&mut rng, cx) - }; - - let _diff_count = rng.random_range(1..5); - let mut diffs = Vec::new(); - - multibuffer.update(cx, |multibuffer, cx| { - for buffer_id in multibuffer.excerpt_buffer_ids() { - if rng.random_bool(0.7) { - if let Some(buffer_handle) = multibuffer.buffer(buffer_id) { - let buffer_text = buffer_handle.read(cx).text(); - let mut base_text = String::new(); - - for line in buffer_text.lines() { - if rng.random_bool(0.3) { - continue; - } else if rng.random_bool(0.3) { - let line_len = rng.random_range(0..50); - let modified_line = RandomCharIter::new(&mut rng) - .take(line_len) - .collect::(); - base_text.push_str(&modified_line); - base_text.push('\n'); - } else { - base_text.push_str(line); - base_text.push('\n'); - } - } - - if rng.random_bool(0.5) { - let extra_lines = rng.random_range(1..5); - for _ in 0..extra_lines { - let line_len = rng.random_range(0..50); - let extra_line = RandomCharIter::new(&mut rng) - .take(line_len) - .collect::(); - base_text.push_str(&extra_line); - base_text.push('\n'); - } - } - - let diff = - cx.new(|cx| BufferDiff::new_with_base_text(&base_text, &buffer_handle, cx)); - diffs.push(diff.clone()); - multibuffer.add_diff(diff, cx); - } - } - } - }); - - multibuffer.update(cx, |multibuffer, cx| { - if rng.random_bool(0.5) { - multibuffer.set_all_diff_hunks_expanded(cx); - } else { - let snapshot = multibuffer.snapshot(cx); - let text = snapshot.text(); - - let mut ranges = Vec::new(); - for _ in 0..rng.random_range(1..5) { - if snapshot.len().0 == 0 { - break; - } - - let diff_size = rng.random_range(5..1000); - let mut start = rng.random_range(0..snapshot.len().0); - - while !text.is_char_boundary(start) { - start = start.saturating_sub(1); - } - - let mut end = rng.random_range(start..snapshot.len().0.min(start + diff_size)); - - while !text.is_char_boundary(end) { - end = end.saturating_add(1); - } - let start_anchor = snapshot.anchor_after(MultiBufferOffset(start)); - let end_anchor = snapshot.anchor_before(MultiBufferOffset(end)); - ranges.push(start_anchor..end_anchor); - } - multibuffer.expand_diff_hunks(ranges, cx); - } - }); - - let snapshot = multibuffer.read(cx).snapshot(cx); - - let chunks = snapshot.chunks(MultiBufferOffset(0)..snapshot.len(), false); - - for chunk in chunks { - let chunk_text = chunk.text; - let chars_bitmap = chunk.chars; - let tabs_bitmap = chunk.tabs; - - if chunk_text.is_empty() { - assert_eq!( - chars_bitmap, 0, - "Empty chunk should have empty chars bitmap" - ); - assert_eq!(tabs_bitmap, 0, "Empty chunk should have empty tabs bitmap"); - continue; - } - - assert!( - chunk_text.len() <= 128, - "Chunk text length {} exceeds 128 bytes", - chunk_text.len() - ); - - let char_indices = chunk_text - .char_indices() - .map(|(i, _)| i) - .collect::>(); - - for byte_idx in 0..chunk_text.len() { - let should_have_bit = char_indices.contains(&byte_idx); - let has_bit = chars_bitmap & (1 << byte_idx) != 0; - - if has_bit != should_have_bit { - eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes()); - eprintln!("Char indices: {:?}", char_indices); - eprintln!("Chars bitmap: {:#b}", chars_bitmap); - } - - assert_eq!( - has_bit, should_have_bit, - "Chars bitmap mismatch at byte index {} in chunk {:?}. Expected bit: {}, Got bit: {}", - byte_idx, chunk_text, should_have_bit, has_bit - ); - } - - for (byte_idx, byte) in chunk_text.bytes().enumerate() { - let is_tab = byte == b'\t'; - let has_bit = tabs_bitmap & (1 << byte_idx) != 0; - - if has_bit != is_tab { - eprintln!("Chunk text bytes: {:?}", chunk_text.as_bytes()); - eprintln!("Tabs bitmap: {:#b}", tabs_bitmap); - assert_eq!( - has_bit, is_tab, - "Tabs bitmap mismatch at byte index {} in chunk {:?}. Byte: {:?}, Expected bit: {}, Got bit: {}", - byte_idx, chunk_text, byte as char, is_tab, has_bit - ); - } - } - } -} - -fn collect_word_diffs( - base_text: &str, - modified_text: &str, - cx: &mut TestAppContext, -) -> Vec { - let buffer = cx.new(|cx| Buffer::local(modified_text, cx)); - let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx)); - cx.run_until_parked(); - - let multibuffer = cx.new(|cx| { - let mut multibuffer = MultiBuffer::singleton(buffer.clone(), cx); - multibuffer.add_diff(diff.clone(), cx); - multibuffer - }); - - multibuffer.update(cx, |multibuffer, cx| { - multibuffer.expand_diff_hunks(vec![Anchor::min()..Anchor::max()], cx); - }); - - let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx)); - let text = snapshot.text(); - - snapshot - .diff_hunks() - .flat_map(|hunk| hunk.word_diffs) - .map(|range| text[range.start.0..range.end.0].to_string()) - .collect() -} - -#[gpui::test] -async fn test_word_diff_simple_replacement(cx: &mut TestAppContext) { - let settings_store = cx.update(|cx| SettingsStore::test(cx)); - cx.set_global(settings_store); - - let base_text = "hello world foo bar\n"; - let modified_text = "hello WORLD foo BAR\n"; - - let word_diffs = collect_word_diffs(base_text, modified_text, cx); - - assert_eq!(word_diffs, vec!["world", "bar", "WORLD", "BAR"]); -} - -#[gpui::test] -async fn test_word_diff_consecutive_modified_lines(cx: &mut TestAppContext) { - let settings_store = cx.update(|cx| SettingsStore::test(cx)); - cx.set_global(settings_store); - - let base_text = "aaa bbb\nccc ddd\n"; - let modified_text = "aaa BBB\nccc DDD\n"; - - let word_diffs = collect_word_diffs(base_text, modified_text, cx); - - assert_eq!( - word_diffs, - vec!["bbb", "ddd", "BBB", "DDD"], - "consecutive modified lines should produce word diffs when line counts match" - ); -} - -#[gpui::test] -async fn test_word_diff_modified_lines_with_deletion_between(cx: &mut TestAppContext) { - let settings_store = cx.update(|cx| SettingsStore::test(cx)); - cx.set_global(settings_store); - - let base_text = "aaa bbb\ndeleted line\nccc ddd\n"; - let modified_text = "aaa BBB\nccc DDD\n"; - - let word_diffs = collect_word_diffs(base_text, modified_text, cx); - - assert_eq!( - word_diffs, - Vec::::new(), - "modified lines with a deleted line between should not produce word diffs" - ); -} - -#[gpui::test] -async fn test_word_diff_disabled(cx: &mut TestAppContext) { - let settings_store = cx.update(|cx| { - let mut settings_store = SettingsStore::test(cx); - settings_store.update_user_settings(cx, |settings| { - settings.project.all_languages.defaults.word_diff_enabled = Some(false); - }); - settings_store - }); - cx.set_global(settings_store); - - let base_text = "hello world\n"; - let modified_text = "hello WORLD\n"; - - let word_diffs = collect_word_diffs(base_text, modified_text, cx); - - assert_eq!( - word_diffs, - Vec::::new(), - "word diffs should be empty when disabled" - ); -} - -/// Tests `excerpt_containing` and `excerpts_for_range` (functions mapping multi-buffer text-coordinates to excerpts) -#[gpui::test] -fn test_excerpts_containment_functions(cx: &mut App) { - // Multibuffer content for these tests: - // 0123 - // 0: aa0 - // 1: aa1 - // ----- - // 2: bb0 - // 3: bb1 - // -----MultiBufferOffset(0).. - // 4: cc0 - - let buffer_1 = cx.new(|cx| Buffer::local("aa0\naa1", cx)); - let buffer_2 = cx.new(|cx| Buffer::local("bb0\nbb1", cx)); - let buffer_3 = cx.new(|cx| Buffer::local("cc0", cx)); - - let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite)); - - let (excerpt_1_id, excerpt_2_id, excerpt_3_id) = multibuffer.update(cx, |multibuffer, cx| { - let excerpt_1_id = multibuffer.push_excerpts( - buffer_1.clone(), - [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 3))], - cx, - )[0]; - - let excerpt_2_id = multibuffer.push_excerpts( - buffer_2.clone(), - [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 3))], - cx, - )[0]; - - let excerpt_3_id = multibuffer.push_excerpts( - buffer_3.clone(), - [ExcerptRange::new(Point::new(0, 0)..Point::new(0, 3))], - cx, - )[0]; - - (excerpt_1_id, excerpt_2_id, excerpt_3_id) - }); - - let snapshot = multibuffer.read(cx).snapshot(cx); - - assert_eq!(snapshot.text(), "aa0\naa1\nbb0\nbb1\ncc0"); - - //// Test `excerpts_for_range` - - let p00 = snapshot.point_to_offset(Point::new(0, 0)); - let p10 = snapshot.point_to_offset(Point::new(1, 0)); - let p20 = snapshot.point_to_offset(Point::new(2, 0)); - let p23 = snapshot.point_to_offset(Point::new(2, 3)); - let p13 = snapshot.point_to_offset(Point::new(1, 3)); - let p40 = snapshot.point_to_offset(Point::new(4, 0)); - let p43 = snapshot.point_to_offset(Point::new(4, 3)); - - let excerpts: Vec<_> = snapshot.excerpts_for_range(p00..p00).collect(); - assert_eq!(excerpts.len(), 1); - assert_eq!(excerpts[0].id, excerpt_1_id); - - // Cursor at very end of excerpt 3 - let excerpts: Vec<_> = snapshot.excerpts_for_range(p43..p43).collect(); - assert_eq!(excerpts.len(), 1); - assert_eq!(excerpts[0].id, excerpt_3_id); - - let excerpts: Vec<_> = snapshot.excerpts_for_range(p00..p23).collect(); - assert_eq!(excerpts.len(), 2); - assert_eq!(excerpts[0].id, excerpt_1_id); - assert_eq!(excerpts[1].id, excerpt_2_id); - - // This range represent an selection with end-point just inside excerpt_2 - // Today we only expand the first excerpt, but another interpretation that - // we could consider is expanding both here - let excerpts: Vec<_> = snapshot.excerpts_for_range(p10..p20).collect(); - assert_eq!(excerpts.len(), 1); - assert_eq!(excerpts[0].id, excerpt_1_id); - - //// Test that `excerpts_for_range` and `excerpt_containing` agree for all single offsets (cursor positions) - for offset in 0..=snapshot.len().0 { - let offset = MultiBufferOffset(offset); - let excerpts_for_range: Vec<_> = snapshot.excerpts_for_range(offset..offset).collect(); - assert_eq!( - excerpts_for_range.len(), - 1, - "Expected exactly one excerpt for offset {offset}", - ); - - let excerpt_containing = snapshot.excerpt_containing(offset..offset); - assert!( - excerpt_containing.is_some(), - "Expected excerpt_containing to find excerpt for offset {offset}", - ); - - assert_eq!( - excerpts_for_range[0].id, - excerpt_containing.unwrap().id(), - "excerpts_for_range and excerpt_containing should agree for offset {offset}", - ); - } - - //// Test `excerpt_containing` behavior with ranges: - - // Ranges intersecting a single-excerpt - let containing = snapshot.excerpt_containing(p00..p13); - assert!(containing.is_some()); - assert_eq!(containing.unwrap().id(), excerpt_1_id); - - // Ranges intersecting multiple excerpts (should return None) - let containing = snapshot.excerpt_containing(p20..p40); - assert!( - containing.is_none(), - "excerpt_containing should return None for ranges spanning multiple excerpts" - ); -} diff --git a/crates/multi_buffer/src/path_key.rs b/crates/multi_buffer/src/path_key.rs deleted file mode 100644 index 119194d088..0000000000 --- a/crates/multi_buffer/src/path_key.rs +++ /dev/null @@ -1,437 +0,0 @@ -use std::{mem, ops::Range, sync::Arc}; - -use collections::HashSet; -use gpui::{App, AppContext, Context, Entity}; -use itertools::Itertools; -use language::{Buffer, BufferSnapshot}; -use rope::Point; -use text::{Bias, BufferId, OffsetRangeExt, locator::Locator}; -use util::{post_inc, rel_path::RelPath}; -use ztracing::instrument; - -use crate::{ - Anchor, ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer, build_excerpt_ranges, -}; - -#[derive(PartialEq, Eq, Ord, PartialOrd, Clone, Hash, Debug)] -pub struct PathKey { - // Used by the derived PartialOrd & Ord - pub sort_prefix: Option, - pub path: Arc, -} - -impl PathKey { - pub fn with_sort_prefix(sort_prefix: u64, path: Arc) -> Self { - Self { - sort_prefix: Some(sort_prefix), - path, - } - } - - pub fn for_buffer(buffer: &Entity, cx: &App) -> Self { - if let Some(file) = buffer.read(cx).file() { - Self::with_sort_prefix(file.worktree_id(cx).to_proto(), file.path().clone()) - } else { - Self { - sort_prefix: None, - path: RelPath::unix(&buffer.entity_id().to_string()) - .unwrap() - .into_arc(), - } - } - } -} - -impl MultiBuffer { - pub fn paths(&self) -> impl Iterator + '_ { - self.excerpts_by_path.keys().cloned() - } - - pub fn remove_excerpts_for_path(&mut self, path: PathKey, cx: &mut Context) { - if let Some(to_remove) = self.excerpts_by_path.remove(&path) { - self.remove_excerpts(to_remove, cx) - } - if let Some(follower) = &self.follower { - follower.update(cx, |follower, cx| { - follower.remove_excerpts_for_path(path, cx); - }); - } - } - - pub fn location_for_path(&self, path: &PathKey, cx: &App) -> Option { - let excerpt_id = self.excerpts_by_path.get(path)?.first()?; - let snapshot = self.read(cx); - let excerpt = snapshot.excerpt(*excerpt_id)?; - Some(Anchor::in_buffer(excerpt.id, excerpt.range.context.start)) - } - - pub fn excerpt_paths(&self) -> impl Iterator { - self.excerpts_by_path.keys() - } - - /// Sets excerpts, returns `true` if at least one new excerpt was added. - #[instrument(skip_all)] - pub fn set_excerpts_for_path( - &mut self, - path: PathKey, - buffer: Entity, - ranges: impl IntoIterator>, - context_line_count: u32, - cx: &mut Context, - ) -> (Vec>, bool) { - let buffer_snapshot = buffer.read(cx).snapshot(); - let excerpt_ranges = build_excerpt_ranges(ranges, context_line_count, &buffer_snapshot); - - let (new, counts) = Self::merge_excerpt_ranges(&excerpt_ranges); - self.set_merged_excerpt_ranges_for_path( - path, - buffer, - excerpt_ranges, - &buffer_snapshot, - new, - counts, - cx, - ) - } - - pub fn set_excerpt_ranges_for_path( - &mut self, - path: PathKey, - buffer: Entity, - buffer_snapshot: &BufferSnapshot, - excerpt_ranges: Vec>, - cx: &mut Context, - ) -> (Vec>, bool) { - let (new, counts) = Self::merge_excerpt_ranges(&excerpt_ranges); - self.set_merged_excerpt_ranges_for_path( - path, - buffer, - excerpt_ranges, - buffer_snapshot, - new, - counts, - cx, - ) - } - - pub fn set_anchored_excerpts_for_path( - &self, - path_key: PathKey, - buffer: Entity, - ranges: Vec>, - context_line_count: u32, - cx: &Context, - ) -> impl Future>> + use<> { - let buffer_snapshot = buffer.read(cx).snapshot(); - let multi_buffer = cx.weak_entity(); - let mut app = cx.to_async(); - async move { - let snapshot = buffer_snapshot.clone(); - let (excerpt_ranges, new, counts) = app - .background_spawn(async move { - let ranges = ranges.into_iter().map(|range| range.to_point(&snapshot)); - let excerpt_ranges = - build_excerpt_ranges(ranges, context_line_count, &snapshot); - let (new, counts) = Self::merge_excerpt_ranges(&excerpt_ranges); - (excerpt_ranges, new, counts) - }) - .await; - - multi_buffer - .update(&mut app, move |multi_buffer, cx| { - let (ranges, _) = multi_buffer.set_merged_excerpt_ranges_for_path( - path_key, - buffer, - excerpt_ranges, - &buffer_snapshot, - new, - counts, - cx, - ); - ranges - }) - .ok() - .unwrap_or_default() - } - } - - pub fn remove_excerpts_for_buffer(&mut self, buffer: BufferId, cx: &mut Context) { - self.remove_excerpts( - self.excerpts_for_buffer(buffer, cx) - .into_iter() - .map(|(excerpt, _)| excerpt), - cx, - ); - } - - pub(super) fn expand_excerpts_with_paths( - &mut self, - ids: impl IntoIterator, - line_count: u32, - direction: ExpandExcerptDirection, - cx: &mut Context, - ) { - let grouped = ids - .into_iter() - .chunk_by(|id| self.paths_by_excerpt.get(id).cloned()) - .into_iter() - .filter_map(|(k, v)| Some((k?, v.into_iter().collect::>()))) - .collect::>(); - let snapshot = self.snapshot(cx); - - for (path, ids) in grouped.into_iter() { - let Some(excerpt_ids) = self.excerpts_by_path.get(&path) else { - continue; - }; - - let ids_to_expand = HashSet::from_iter(ids); - let mut excerpt_id_ = None; - let expanded_ranges = excerpt_ids.iter().filter_map(|excerpt_id| { - let excerpt = snapshot.excerpt(*excerpt_id)?; - let excerpt_id = excerpt.id; - if excerpt_id_.is_none() { - excerpt_id_ = Some(excerpt_id); - } - - let mut context = excerpt.range.context.to_point(&excerpt.buffer); - if ids_to_expand.contains(&excerpt_id) { - match direction { - ExpandExcerptDirection::Up => { - context.start.row = context.start.row.saturating_sub(line_count); - context.start.column = 0; - } - ExpandExcerptDirection::Down => { - context.end.row = - (context.end.row + line_count).min(excerpt.buffer.max_point().row); - context.end.column = excerpt.buffer.line_len(context.end.row); - } - ExpandExcerptDirection::UpAndDown => { - context.start.row = context.start.row.saturating_sub(line_count); - context.start.column = 0; - context.end.row = - (context.end.row + line_count).min(excerpt.buffer.max_point().row); - context.end.column = excerpt.buffer.line_len(context.end.row); - } - } - } - - Some(ExcerptRange { - context, - primary: excerpt.range.primary.to_point(&excerpt.buffer), - }) - }); - let mut merged_ranges: Vec> = Vec::new(); - for range in expanded_ranges { - if let Some(last_range) = merged_ranges.last_mut() - && last_range.context.end >= range.context.start - { - last_range.context.end = range.context.end; - continue; - } - merged_ranges.push(range) - } - let Some(excerpt_id) = excerpt_id_ else { - continue; - }; - let Some(buffer_id) = &snapshot.buffer_id_for_excerpt(excerpt_id) else { - continue; - }; - - let Some(buffer) = self.buffers.get(buffer_id).map(|b| b.buffer.clone()) else { - continue; - }; - - let buffer_snapshot = buffer.read(cx).snapshot(); - self.update_path_excerpts(path.clone(), buffer, &buffer_snapshot, merged_ranges, cx); - } - } - - /// Sets excerpts, returns `true` if at least one new excerpt was added. - fn set_merged_excerpt_ranges_for_path( - &mut self, - path: PathKey, - buffer: Entity, - ranges: Vec>, - buffer_snapshot: &BufferSnapshot, - new: Vec>, - counts: Vec, - cx: &mut Context, - ) -> (Vec>, bool) { - let (excerpt_ids, added_a_new_excerpt) = - self.update_path_excerpts(path, buffer, buffer_snapshot, new, cx); - - let mut result = Vec::new(); - let mut ranges = ranges.into_iter(); - for (excerpt_id, range_count) in excerpt_ids.into_iter().zip(counts.into_iter()) { - for range in ranges.by_ref().take(range_count) { - let range = Anchor::range_in_buffer( - excerpt_id, - buffer_snapshot.anchor_before(&range.primary.start) - ..buffer_snapshot.anchor_after(&range.primary.end), - ); - result.push(range) - } - } - (result, added_a_new_excerpt) - } - - fn update_path_excerpts( - &mut self, - path: PathKey, - buffer: Entity, - buffer_snapshot: &BufferSnapshot, - new: Vec>, - cx: &mut Context, - ) -> (Vec, bool) { - let mut insert_after = self - .excerpts_by_path - .range(..path.clone()) - .next_back() - .and_then(|(_, value)| value.last().copied()) - .unwrap_or(ExcerptId::min()); - - let existing = self - .excerpts_by_path - .get(&path) - .cloned() - .unwrap_or_default(); - let mut new_iter = new.into_iter().peekable(); - let mut existing_iter = existing.into_iter().peekable(); - - let mut excerpt_ids = Vec::new(); - let mut to_remove = Vec::new(); - let mut to_insert: Vec<(ExcerptId, ExcerptRange)> = Vec::new(); - let mut added_a_new_excerpt = false; - let snapshot = self.snapshot(cx); - - let mut next_excerpt_id = - // todo(lw): is this right? What if we remove the last excerpt, then we might reallocate with a wrong mapping? - if let Some(last_entry) = self.snapshot.borrow().excerpt_ids.last() { - last_entry.id.0 + 1 - } else { - 1 - }; - - let mut next_excerpt_id = move || ExcerptId(post_inc(&mut next_excerpt_id)); - - let mut excerpts_cursor = snapshot.excerpts.cursor::>(()); - excerpts_cursor.next(); - - loop { - let existing = if let Some(&existing_id) = existing_iter.peek() { - let locator = snapshot.excerpt_locator_for_id(existing_id); - excerpts_cursor.seek_forward(&Some(locator), Bias::Left); - if let Some(excerpt) = excerpts_cursor.item() { - if excerpt.buffer_id != buffer_snapshot.remote_id() { - to_remove.push(existing_id); - existing_iter.next(); - continue; - } - Some((existing_id, excerpt.range.context.to_point(buffer_snapshot))) - } else { - None - } - } else { - None - }; - - let new = new_iter.peek(); - if let Some((last_id, last)) = to_insert.last_mut() { - if let Some(new) = new - && last.context.end >= new.context.start - { - last.context.end = last.context.end.max(new.context.end); - excerpt_ids.push(*last_id); - new_iter.next(); - continue; - } - if let Some((existing_id, existing_range)) = &existing - && last.context.end >= existing_range.start - { - last.context.end = last.context.end.max(existing_range.end); - to_remove.push(*existing_id); - self.snapshot - .get_mut() - .replaced_excerpts - .insert(*existing_id, *last_id); - existing_iter.next(); - continue; - } - } - - match (new, existing) { - (None, None) => break, - (None, Some((existing_id, _))) => { - existing_iter.next(); - to_remove.push(existing_id); - continue; - } - (Some(_), None) => { - added_a_new_excerpt = true; - let new_id = next_excerpt_id(); - excerpt_ids.push(new_id); - to_insert.push((new_id, new_iter.next().unwrap())); - continue; - } - (Some(new), Some((_, existing_range))) => { - if existing_range.end < new.context.start { - let existing_id = existing_iter.next().unwrap(); - to_remove.push(existing_id); - continue; - } else if existing_range.start > new.context.end { - let new_id = next_excerpt_id(); - excerpt_ids.push(new_id); - to_insert.push((new_id, new_iter.next().unwrap())); - continue; - } - - if existing_range.start == new.context.start - && existing_range.end == new.context.end - { - self.insert_excerpts_with_ids_after( - insert_after, - buffer.clone(), - mem::take(&mut to_insert), - cx, - ); - insert_after = existing_iter.next().unwrap(); - excerpt_ids.push(insert_after); - new_iter.next(); - } else { - let existing_id = existing_iter.next().unwrap(); - let new_id = next_excerpt_id(); - self.snapshot - .get_mut() - .replaced_excerpts - .insert(existing_id, new_id); - to_remove.push(existing_id); - let mut range = new_iter.next().unwrap(); - range.context.start = range.context.start.min(existing_range.start); - range.context.end = range.context.end.max(existing_range.end); - excerpt_ids.push(new_id); - to_insert.push((new_id, range)); - } - } - }; - } - - self.insert_excerpts_with_ids_after(insert_after, buffer, to_insert, cx); - // todo(lw): There is a logic bug somewhere that causes the to_remove vector to be not ordered correctly - to_remove.sort_by_cached_key(|&id| snapshot.excerpt_locator_for_id(id)); - self.remove_excerpts(to_remove, cx); - - if excerpt_ids.is_empty() { - self.excerpts_by_path.remove(&path); - } else { - for excerpt_id in &excerpt_ids { - self.paths_by_excerpt.insert(*excerpt_id, path.clone()); - } - let snapshot = &*self.snapshot.get_mut(); - let mut excerpt_ids: Vec<_> = excerpt_ids.iter().dedup().cloned().collect(); - excerpt_ids.sort_by_cached_key(|&id| snapshot.excerpt_locator_for_id(id)); - self.excerpts_by_path.insert(path, excerpt_ids); - } - - (excerpt_ids, added_a_new_excerpt) - } -} diff --git a/crates/multi_buffer/src/transaction.rs b/crates/multi_buffer/src/transaction.rs deleted file mode 100644 index a65e394c8f..0000000000 --- a/crates/multi_buffer/src/transaction.rs +++ /dev/null @@ -1,538 +0,0 @@ -use gpui::{App, Context, Entity}; -use language::{self, Buffer, TransactionId}; -use std::{ - collections::HashMap, - ops::{AddAssign, Range, Sub}, - time::{Duration, Instant}, -}; -use sum_tree::Bias; -use text::BufferId; - -use crate::{BufferState, MultiBufferDimension}; - -use super::{Event, ExcerptSummary, MultiBuffer}; - -#[derive(Clone)] -pub(super) struct History { - next_transaction_id: TransactionId, - undo_stack: Vec, - redo_stack: Vec, - transaction_depth: usize, - group_interval: Duration, -} - -impl Default for History { - fn default() -> Self { - History { - next_transaction_id: clock::Lamport::MIN, - undo_stack: Vec::new(), - redo_stack: Vec::new(), - transaction_depth: 0, - group_interval: Duration::from_millis(300), - } - } -} - -#[derive(Clone)] -struct Transaction { - id: TransactionId, - buffer_transactions: HashMap, - first_edit_at: Instant, - last_edit_at: Instant, - suppress_grouping: bool, -} - -impl History { - fn start_transaction(&mut self, now: Instant) -> Option { - self.transaction_depth += 1; - if self.transaction_depth == 1 { - let id = self.next_transaction_id.tick(); - self.undo_stack.push(Transaction { - id, - buffer_transactions: Default::default(), - first_edit_at: now, - last_edit_at: now, - suppress_grouping: false, - }); - Some(id) - } else { - None - } - } - - fn end_transaction( - &mut self, - now: Instant, - buffer_transactions: HashMap, - ) -> bool { - assert_ne!(self.transaction_depth, 0); - self.transaction_depth -= 1; - if self.transaction_depth == 0 { - if buffer_transactions.is_empty() { - self.undo_stack.pop(); - false - } else { - self.redo_stack.clear(); - let transaction = self.undo_stack.last_mut().unwrap(); - transaction.last_edit_at = now; - for (buffer_id, transaction_id) in buffer_transactions { - transaction - .buffer_transactions - .entry(buffer_id) - .or_insert(transaction_id); - } - true - } - } else { - false - } - } - - fn push_transaction<'a, T>( - &mut self, - buffer_transactions: T, - now: Instant, - cx: &Context, - ) where - T: IntoIterator, &'a language::Transaction)>, - { - assert_eq!(self.transaction_depth, 0); - let transaction = Transaction { - id: self.next_transaction_id.tick(), - buffer_transactions: buffer_transactions - .into_iter() - .map(|(buffer, transaction)| (buffer.read(cx).remote_id(), transaction.id)) - .collect(), - first_edit_at: now, - last_edit_at: now, - suppress_grouping: false, - }; - if !transaction.buffer_transactions.is_empty() { - self.undo_stack.push(transaction); - self.redo_stack.clear(); - } - } - - fn finalize_last_transaction(&mut self) { - if let Some(transaction) = self.undo_stack.last_mut() { - transaction.suppress_grouping = true; - } - } - - fn forget(&mut self, transaction_id: TransactionId) -> Option { - if let Some(ix) = self - .undo_stack - .iter() - .rposition(|transaction| transaction.id == transaction_id) - { - Some(self.undo_stack.remove(ix)) - } else if let Some(ix) = self - .redo_stack - .iter() - .rposition(|transaction| transaction.id == transaction_id) - { - Some(self.redo_stack.remove(ix)) - } else { - None - } - } - - fn transaction(&self, transaction_id: TransactionId) -> Option<&Transaction> { - self.undo_stack - .iter() - .find(|transaction| transaction.id == transaction_id) - .or_else(|| { - self.redo_stack - .iter() - .find(|transaction| transaction.id == transaction_id) - }) - } - - fn transaction_mut(&mut self, transaction_id: TransactionId) -> Option<&mut Transaction> { - self.undo_stack - .iter_mut() - .find(|transaction| transaction.id == transaction_id) - .or_else(|| { - self.redo_stack - .iter_mut() - .find(|transaction| transaction.id == transaction_id) - }) - } - - fn pop_undo(&mut self) -> Option<&mut Transaction> { - assert_eq!(self.transaction_depth, 0); - if let Some(transaction) = self.undo_stack.pop() { - self.redo_stack.push(transaction); - self.redo_stack.last_mut() - } else { - None - } - } - - fn pop_redo(&mut self) -> Option<&mut Transaction> { - assert_eq!(self.transaction_depth, 0); - if let Some(transaction) = self.redo_stack.pop() { - self.undo_stack.push(transaction); - self.undo_stack.last_mut() - } else { - None - } - } - - fn remove_from_undo(&mut self, transaction_id: TransactionId) -> Option<&Transaction> { - let ix = self - .undo_stack - .iter() - .rposition(|transaction| transaction.id == transaction_id)?; - let transaction = self.undo_stack.remove(ix); - self.redo_stack.push(transaction); - self.redo_stack.last() - } - - fn group(&mut self) -> Option { - let mut count = 0; - let mut transactions = self.undo_stack.iter(); - if let Some(mut transaction) = transactions.next_back() { - while let Some(prev_transaction) = transactions.next_back() { - if !prev_transaction.suppress_grouping - && transaction.first_edit_at - prev_transaction.last_edit_at - <= self.group_interval - { - transaction = prev_transaction; - count += 1; - } else { - break; - } - } - } - self.group_trailing(count) - } - - fn group_until(&mut self, transaction_id: TransactionId) { - let mut count = 0; - for transaction in self.undo_stack.iter().rev() { - if transaction.id == transaction_id { - self.group_trailing(count); - break; - } else if transaction.suppress_grouping { - break; - } else { - count += 1; - } - } - } - - fn group_trailing(&mut self, n: usize) -> Option { - let new_len = self.undo_stack.len() - n; - let (transactions_to_keep, transactions_to_merge) = self.undo_stack.split_at_mut(new_len); - if let Some(last_transaction) = transactions_to_keep.last_mut() { - if let Some(transaction) = transactions_to_merge.last() { - last_transaction.last_edit_at = transaction.last_edit_at; - } - for to_merge in transactions_to_merge { - for (buffer_id, transaction_id) in &to_merge.buffer_transactions { - last_transaction - .buffer_transactions - .entry(*buffer_id) - .or_insert(*transaction_id); - } - } - } - - self.undo_stack.truncate(new_len); - self.undo_stack.last().map(|t| t.id) - } - - pub(super) fn transaction_depth(&self) -> usize { - self.transaction_depth - } - - pub fn set_group_interval(&mut self, group_interval: Duration) { - self.group_interval = group_interval; - } -} - -impl MultiBuffer { - pub fn start_transaction(&mut self, cx: &mut Context) -> Option { - self.start_transaction_at(Instant::now(), cx) - } - - pub fn start_transaction_at( - &mut self, - now: Instant, - cx: &mut Context, - ) -> Option { - if let Some(buffer) = self.as_singleton() { - return buffer.update(cx, |buffer, _| buffer.start_transaction_at(now)); - } - - for BufferState { buffer, .. } in self.buffers.values() { - buffer.update(cx, |buffer, _| buffer.start_transaction_at(now)); - } - self.history.start_transaction(now) - } - - pub fn last_transaction_id(&self, cx: &App) -> Option { - if let Some(buffer) = self.as_singleton() { - buffer - .read(cx) - .peek_undo_stack() - .map(|history_entry| history_entry.transaction_id()) - } else { - let last_transaction = self.history.undo_stack.last()?; - Some(last_transaction.id) - } - } - - pub fn end_transaction(&mut self, cx: &mut Context) -> Option { - self.end_transaction_at(Instant::now(), cx) - } - - pub fn end_transaction_at( - &mut self, - now: Instant, - cx: &mut Context, - ) -> Option { - if let Some(buffer) = self.as_singleton() { - return buffer.update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)); - } - - let mut buffer_transactions = HashMap::default(); - for BufferState { buffer, .. } in self.buffers.values() { - if let Some(transaction_id) = - buffer.update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)) - { - buffer_transactions.insert(buffer.read(cx).remote_id(), transaction_id); - } - } - - if self.history.end_transaction(now, buffer_transactions) { - let transaction_id = self.history.group().unwrap(); - Some(transaction_id) - } else { - None - } - } - - pub fn edited_ranges_for_transaction( - &self, - transaction_id: TransactionId, - cx: &App, - ) -> Vec> - where - D: MultiBufferDimension - + Ord - + Sub - + AddAssign, - D::TextDimension: PartialOrd + Sub, - { - let Some(transaction) = self.history.transaction(transaction_id) else { - return Vec::new(); - }; - - let mut ranges = Vec::new(); - let snapshot = self.read(cx); - let mut cursor = snapshot.excerpts.cursor::(()); - - for (buffer_id, buffer_transaction) in &transaction.buffer_transactions { - let Some(buffer_state) = self.buffers.get(buffer_id) else { - continue; - }; - - let buffer = buffer_state.buffer.read(cx); - for range in - buffer.edited_ranges_for_transaction_id::(*buffer_transaction) - { - for excerpt_id in &buffer_state.excerpts { - cursor.seek(excerpt_id, Bias::Left); - if let Some(excerpt) = cursor.item() - && excerpt.locator == *excerpt_id - { - let excerpt_buffer_start = excerpt - .range - .context - .start - .summary::(buffer); - let excerpt_buffer_end = excerpt - .range - .context - .end - .summary::(buffer); - let excerpt_range = excerpt_buffer_start..excerpt_buffer_end; - if excerpt_range.contains(&range.start) - && excerpt_range.contains(&range.end) - { - let excerpt_start = D::from_summary(&cursor.start().text); - - let mut start = excerpt_start; - start += range.start - excerpt_buffer_start; - let mut end = excerpt_start; - end += range.end - excerpt_buffer_start; - - ranges.push(start..end); - break; - } - } - } - } - } - - ranges.sort_by_key(|range| range.start); - ranges - } - - pub fn merge_transactions( - &mut self, - transaction: TransactionId, - destination: TransactionId, - cx: &mut Context, - ) { - if let Some(buffer) = self.as_singleton() { - buffer.update(cx, |buffer, _| { - buffer.merge_transactions(transaction, destination) - }); - } else if let Some(transaction) = self.history.forget(transaction) - && let Some(destination) = self.history.transaction_mut(destination) - { - for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions { - if let Some(destination_buffer_transaction_id) = - destination.buffer_transactions.get(&buffer_id) - { - if let Some(state) = self.buffers.get(&buffer_id) { - state.buffer.update(cx, |buffer, _| { - buffer.merge_transactions( - buffer_transaction_id, - *destination_buffer_transaction_id, - ) - }); - } - } else { - destination - .buffer_transactions - .insert(buffer_id, buffer_transaction_id); - } - } - } - } - - pub fn finalize_last_transaction(&mut self, cx: &mut Context) { - self.history.finalize_last_transaction(); - for BufferState { buffer, .. } in self.buffers.values() { - buffer.update(cx, |buffer, _| { - buffer.finalize_last_transaction(); - }); - } - } - - pub fn push_transaction<'a, T>(&mut self, buffer_transactions: T, cx: &Context) - where - T: IntoIterator, &'a language::Transaction)>, - { - self.history - .push_transaction(buffer_transactions, Instant::now(), cx); - self.history.finalize_last_transaction(); - } - - pub fn group_until_transaction( - &mut self, - transaction_id: TransactionId, - cx: &mut Context, - ) { - if let Some(buffer) = self.as_singleton() { - buffer.update(cx, |buffer, _| { - buffer.group_until_transaction(transaction_id) - }); - } else { - self.history.group_until(transaction_id); - } - } - pub fn undo(&mut self, cx: &mut Context) -> Option { - let mut transaction_id = None; - if let Some(buffer) = self.as_singleton() { - transaction_id = buffer.update(cx, |buffer, cx| buffer.undo(cx)); - } else { - while let Some(transaction) = self.history.pop_undo() { - let mut undone = false; - for (buffer_id, buffer_transaction_id) in &mut transaction.buffer_transactions { - if let Some(BufferState { buffer, .. }) = self.buffers.get(buffer_id) { - undone |= buffer.update(cx, |buffer, cx| { - let undo_to = *buffer_transaction_id; - if let Some(entry) = buffer.peek_undo_stack() { - *buffer_transaction_id = entry.transaction_id(); - } - buffer.undo_to_transaction(undo_to, cx) - }); - } - } - - if undone { - transaction_id = Some(transaction.id); - break; - } - } - } - - if let Some(transaction_id) = transaction_id { - cx.emit(Event::TransactionUndone { transaction_id }); - } - - transaction_id - } - - pub fn redo(&mut self, cx: &mut Context) -> Option { - if let Some(buffer) = self.as_singleton() { - return buffer.update(cx, |buffer, cx| buffer.redo(cx)); - } - - while let Some(transaction) = self.history.pop_redo() { - let mut redone = false; - for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions.iter_mut() { - if let Some(BufferState { buffer, .. }) = self.buffers.get(buffer_id) { - redone |= buffer.update(cx, |buffer, cx| { - let redo_to = *buffer_transaction_id; - if let Some(entry) = buffer.peek_redo_stack() { - *buffer_transaction_id = entry.transaction_id(); - } - buffer.redo_to_transaction(redo_to, cx) - }); - } - } - - if redone { - return Some(transaction.id); - } - } - - None - } - - pub fn undo_transaction(&mut self, transaction_id: TransactionId, cx: &mut Context) { - if let Some(buffer) = self.as_singleton() { - buffer.update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx)); - } else if let Some(transaction) = self.history.remove_from_undo(transaction_id) { - for (buffer_id, transaction_id) in &transaction.buffer_transactions { - if let Some(BufferState { buffer, .. }) = self.buffers.get(buffer_id) { - buffer.update(cx, |buffer, cx| { - buffer.undo_transaction(*transaction_id, cx) - }); - } - } - } - } - - pub fn forget_transaction(&mut self, transaction_id: TransactionId, cx: &mut Context) { - if let Some(buffer) = self.as_singleton() { - buffer.update(cx, |buffer, _| { - buffer.forget_transaction(transaction_id); - }); - } else if let Some(transaction) = self.history.forget(transaction_id) { - for (buffer_id, buffer_transaction_id) in transaction.buffer_transactions { - if let Some(state) = self.buffers.get_mut(&buffer_id) { - state.buffer.update(cx, |buffer, _| { - buffer.forget_transaction(buffer_transaction_id); - }); - } - } - } - } -} diff --git a/crates/nc/Cargo.toml b/crates/nc/Cargo.toml deleted file mode 100644 index 534ec2271c..0000000000 --- a/crates/nc/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "nc" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/nc.rs" -doctest = false - -[dependencies] -anyhow.workspace = true -futures.workspace = true -net.workspace = true -smol.workspace = true diff --git a/crates/nc/LICENSE-GPL b/crates/nc/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/nc/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/nc/src/nc.rs b/crates/nc/src/nc.rs deleted file mode 100644 index fccb4d726c..0000000000 --- a/crates/nc/src/nc.rs +++ /dev/null @@ -1,56 +0,0 @@ -use anyhow::Result; - -#[cfg(windows)] -pub fn main(_socket: &str) -> Result<()> { - // It looks like we can't get an async stdio stream on Windows from smol. - // - // We decided to merge this with a panic on Windows since this is only used - // by the experimental Claude Code Agent Server. - // - // We're tracking this internally, and we will address it before shipping the integration. - panic!("--nc isn't yet supported on Windows"); -} - -/// The main function for when Zed is running in netcat mode -#[cfg(not(windows))] -pub fn main(socket: &str) -> Result<()> { - use futures::{AsyncReadExt as _, AsyncWriteExt as _, FutureExt as _, io::BufReader, select}; - use net::async_net::UnixStream; - use smol::{Async, io::AsyncBufReadExt}; - - smol::block_on(async { - let socket_stream = UnixStream::connect(socket).await?; - let (socket_read, mut socket_write) = socket_stream.split(); - let mut socket_reader = BufReader::new(socket_read); - - let mut stdout = Async::new(std::io::stdout())?; - let stdin = Async::new(std::io::stdin())?; - let mut stdin_reader = BufReader::new(stdin); - - let mut socket_line = Vec::new(); - let mut stdin_line = Vec::new(); - - loop { - select! { - bytes_read = socket_reader.read_until(b'\n', &mut socket_line).fuse() => { - if bytes_read? == 0 { - break - } - stdout.write_all(&socket_line).await?; - stdout.flush().await?; - socket_line.clear(); - } - bytes_read = stdin_reader.read_until(b'\n', &mut stdin_line).fuse() => { - if bytes_read? == 0 { - break - } - socket_write.write_all(&stdin_line).await?; - socket_write.flush().await?; - stdin_line.clear(); - } - } - } - - anyhow::Ok(()) - }) -} diff --git a/crates/net/Cargo.toml b/crates/net/Cargo.toml deleted file mode 100644 index 8ce273e30c..0000000000 --- a/crates/net/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "net" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/net.rs" -doctest = false - -[dependencies] -smol.workspace = true - -[target.'cfg(target_os = "windows")'.dependencies] -anyhow.workspace = true -async-io = "2.4" -windows.workspace = true - -[dev-dependencies] -tempfile.workspace = true diff --git a/crates/net/LICENSE-GPL b/crates/net/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/net/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/net/src/async_net.rs b/crates/net/src/async_net.rs deleted file mode 100644 index 6a47902bd8..0000000000 --- a/crates/net/src/async_net.rs +++ /dev/null @@ -1,69 +0,0 @@ -#[cfg(not(target_os = "windows"))] -pub use smol::net::unix::{UnixListener, UnixStream}; - -#[cfg(target_os = "windows")] -pub use windows::{UnixListener, UnixStream}; - -#[cfg(target_os = "windows")] -pub mod windows { - use std::{ - io::Result, - path::Path, - pin::Pin, - task::{Context, Poll}, - }; - - use smol::{ - Async, - io::{AsyncRead, AsyncWrite}, - }; - - pub struct UnixListener(Async); - - impl UnixListener { - pub fn bind>(path: P) -> Result { - Ok(UnixListener(Async::new(crate::UnixListener::bind(path)?)?)) - } - - pub async fn accept(&self) -> Result<(UnixStream, ())> { - let (sock, _) = self.0.read_with(|listener| listener.accept()).await?; - Ok((UnixStream(Async::new(sock)?), ())) - } - } - - pub struct UnixStream(Async); - - impl UnixStream { - pub async fn connect>(path: P) -> Result { - Ok(UnixStream(Async::new(crate::UnixStream::connect(path)?)?)) - } - } - - impl AsyncRead for UnixStream { - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut [u8], - ) -> Poll> { - Pin::new(&mut self.0).poll_read(cx, buf) - } - } - - impl AsyncWrite for UnixStream { - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - Pin::new(&mut self.0).poll_write(cx, buf) - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - Pin::new(&mut self.0).poll_flush(cx) - } - - fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - Pin::new(&mut self.0).poll_close(cx) - } - } -} diff --git a/crates/net/src/listener.rs b/crates/net/src/listener.rs deleted file mode 100644 index 4774bb850b..0000000000 --- a/crates/net/src/listener.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::{ - io::Result, - os::windows::io::{AsSocket, BorrowedSocket}, - path::Path, -}; - -use windows::Win32::Networking::WinSock::{SOCKADDR_UN, SOMAXCONN, bind, listen}; - -use crate::{ - socket::UnixSocket, - stream::UnixStream, - util::{init, map_ret, sockaddr_un}, -}; - -pub struct UnixListener(UnixSocket); - -impl UnixListener { - pub fn bind>(path: P) -> Result { - init(); - let socket = UnixSocket::new()?; - let (addr, len) = sockaddr_un(path)?; - unsafe { - map_ret(bind( - socket.as_raw(), - &addr as *const _ as *const _, - len as i32, - ))?; - map_ret(listen(socket.as_raw(), SOMAXCONN as _))?; - } - Ok(Self(socket)) - } - - pub fn accept(&self) -> Result<(UnixStream, ())> { - let mut storage = SOCKADDR_UN::default(); - let mut len = std::mem::size_of_val(&storage) as i32; - let raw = self.0.accept(&mut storage as *mut _ as *mut _, &mut len)?; - Ok((UnixStream::new(raw), ())) - } -} - -impl AsSocket for UnixListener { - fn as_socket(&self) -> BorrowedSocket<'_> { - unsafe { BorrowedSocket::borrow_raw(self.0.as_raw().0 as _) } - } -} diff --git a/crates/net/src/net.rs b/crates/net/src/net.rs deleted file mode 100644 index 4fa76ffcb8..0000000000 --- a/crates/net/src/net.rs +++ /dev/null @@ -1,107 +0,0 @@ -pub mod async_net; -#[cfg(target_os = "windows")] -pub mod listener; -#[cfg(target_os = "windows")] -pub mod socket; -#[cfg(target_os = "windows")] -pub mod stream; -#[cfg(target_os = "windows")] -mod util; - -#[cfg(target_os = "windows")] -pub use listener::*; -#[cfg(target_os = "windows")] -pub use socket::*; -#[cfg(not(target_os = "windows"))] -pub use std::os::unix::net::{UnixListener, UnixStream}; -#[cfg(target_os = "windows")] -pub use stream::*; - -#[cfg(test)] -mod tests { - use std::io::{Read, Write}; - - use smol::io::{AsyncReadExt, AsyncWriteExt}; - - const SERVER_MESSAGE: &str = "Connection closed"; - const CLIENT_MESSAGE: &str = "Hello, server!"; - const BUFFER_SIZE: usize = 32; - - #[test] - fn test_windows_listener() -> std::io::Result<()> { - use crate::{UnixListener, UnixStream}; - - let temp = tempfile::tempdir()?; - let socket = temp.path().join("socket.sock"); - let listener = UnixListener::bind(&socket)?; - - // Server - let server = std::thread::spawn(move || { - let (mut stream, _) = listener.accept().unwrap(); - - // Read data from the client - let mut buffer = [0; BUFFER_SIZE]; - let bytes_read = stream.read(&mut buffer).unwrap(); - let string = String::from_utf8_lossy(&buffer[..bytes_read]); - assert_eq!(string, CLIENT_MESSAGE); - - // Send a message back to the client - stream.write_all(SERVER_MESSAGE.as_bytes()).unwrap(); - }); - - // Client - let mut client = UnixStream::connect(&socket)?; - - // Send data to the server - client.write_all(CLIENT_MESSAGE.as_bytes())?; - let mut buffer = [0; BUFFER_SIZE]; - - // Read the response from the server - let bytes_read = client.read(&mut buffer)?; - let string = String::from_utf8_lossy(&buffer[..bytes_read]); - assert_eq!(string, SERVER_MESSAGE); - client.flush()?; - - server.join().unwrap(); - Ok(()) - } - - #[test] - fn test_unix_listener() -> std::io::Result<()> { - use crate::async_net::{UnixListener, UnixStream}; - - smol::block_on(async { - let temp = tempfile::tempdir()?; - let socket = temp.path().join("socket.sock"); - let listener = UnixListener::bind(&socket)?; - - // Server - let server = smol::spawn(async move { - let (mut stream, _) = listener.accept().await.unwrap(); - - // Read data from the client - let mut buffer = [0; BUFFER_SIZE]; - let bytes_read = stream.read(&mut buffer).await.unwrap(); - let string = String::from_utf8_lossy(&buffer[..bytes_read]); - assert_eq!(string, CLIENT_MESSAGE); - - // Send a message back to the client - stream.write_all(SERVER_MESSAGE.as_bytes()).await.unwrap(); - }); - - // Client - let mut client = UnixStream::connect(&socket).await?; - client.write_all(CLIENT_MESSAGE.as_bytes()).await?; - - // Read the response from the server - let mut buffer = [0; BUFFER_SIZE]; - let bytes_read = client.read(&mut buffer).await?; - let string = String::from_utf8_lossy(&buffer[..bytes_read]); - assert_eq!(string, "Connection closed"); - client.flush().await?; - - server.await; - Ok(()) - }) - } -} diff --git a/crates/net/src/socket.rs b/crates/net/src/socket.rs deleted file mode 100644 index 6a1fa3d4c4..0000000000 --- a/crates/net/src/socket.rs +++ /dev/null @@ -1,59 +0,0 @@ -use std::io::{Error, ErrorKind, Result}; - -use windows::Win32::{ - Foundation::{HANDLE, HANDLE_FLAG_INHERIT, HANDLE_FLAGS, SetHandleInformation}, - Networking::WinSock::{ - AF_UNIX, SEND_RECV_FLAGS, SOCK_STREAM, SOCKADDR, SOCKET, WSA_FLAG_OVERLAPPED, - WSAEWOULDBLOCK, WSASocketW, accept, closesocket, recv, send, - }, -}; - -use crate::util::map_ret; - -pub struct UnixSocket(SOCKET); - -impl UnixSocket { - pub fn new() -> Result { - unsafe { - let raw = WSASocketW(AF_UNIX as _, SOCK_STREAM.0, 0, None, 0, WSA_FLAG_OVERLAPPED)?; - SetHandleInformation( - HANDLE(raw.0 as _), - HANDLE_FLAG_INHERIT.0, - HANDLE_FLAGS::default(), - )?; - Ok(Self(raw)) - } - } - - pub(crate) fn as_raw(&self) -> SOCKET { - self.0 - } - - pub fn accept(&self, storage: *mut SOCKADDR, len: &mut i32) -> Result { - match unsafe { accept(self.0, Some(storage), Some(len)) } { - Ok(sock) => Ok(Self(sock)), - Err(err) => { - let wsa_err = unsafe { windows::Win32::Networking::WinSock::WSAGetLastError().0 }; - if wsa_err == WSAEWOULDBLOCK.0 { - Err(Error::new(ErrorKind::WouldBlock, "accept would block")) - } else { - Err(err.into()) - } - } - } - } - - pub(crate) fn recv(&self, buf: &mut [u8]) -> Result { - map_ret(unsafe { recv(self.0, buf, SEND_RECV_FLAGS::default()) }) - } - - pub(crate) fn send(&self, buf: &[u8]) -> Result { - map_ret(unsafe { send(self.0, buf, SEND_RECV_FLAGS::default()) }) - } -} - -impl Drop for UnixSocket { - fn drop(&mut self) { - unsafe { closesocket(self.0) }; - } -} diff --git a/crates/net/src/stream.rs b/crates/net/src/stream.rs deleted file mode 100644 index d8b6852fcf..0000000000 --- a/crates/net/src/stream.rs +++ /dev/null @@ -1,60 +0,0 @@ -use std::{ - io::{Read, Result, Write}, - os::windows::io::{AsSocket, BorrowedSocket}, - path::Path, -}; - -use async_io::IoSafe; -use windows::Win32::Networking::WinSock::connect; - -use crate::{ - socket::UnixSocket, - util::{init, map_ret, sockaddr_un}, -}; - -pub struct UnixStream(UnixSocket); - -unsafe impl IoSafe for UnixStream {} - -impl UnixStream { - pub fn new(socket: UnixSocket) -> Self { - Self(socket) - } - - pub fn connect>(path: P) -> Result { - init(); - unsafe { - let inner = UnixSocket::new()?; - let (addr, len) = sockaddr_un(path)?; - - map_ret(connect( - inner.as_raw(), - &addr as *const _ as *const _, - len as i32, - ))?; - Ok(Self(inner)) - } - } -} - -impl Read for UnixStream { - fn read(&mut self, buf: &mut [u8]) -> Result { - self.0.recv(buf) - } -} - -impl Write for UnixStream { - fn write(&mut self, buf: &[u8]) -> Result { - self.0.send(buf) - } - - fn flush(&mut self) -> Result<()> { - Ok(()) - } -} - -impl AsSocket for UnixStream { - fn as_socket(&self) -> BorrowedSocket<'_> { - unsafe { BorrowedSocket::borrow_raw(self.0.as_raw().0 as _) } - } -} diff --git a/crates/net/src/util.rs b/crates/net/src/util.rs deleted file mode 100644 index f454c099c7..0000000000 --- a/crates/net/src/util.rs +++ /dev/null @@ -1,76 +0,0 @@ -use std::{ - io::{Error, ErrorKind, Result}, - path::Path, - sync::Once, -}; - -use windows::Win32::Networking::WinSock::{ - ADDRESS_FAMILY, AF_UNIX, SOCKADDR_UN, SOCKET_ERROR, WSAGetLastError, WSAStartup, -}; - -pub(crate) fn init() { - static ONCE: Once = Once::new(); - - ONCE.call_once(|| unsafe { - let mut wsa_data = std::mem::zeroed(); - let result = WSAStartup(0x202, &mut wsa_data); - if result != 0 { - panic!("WSAStartup failed: {}", result); - } - }); -} - -// https://devblogs.microsoft.com/commandline/af_unix-comes-to-windows/ -pub(crate) fn sockaddr_un>(path: P) -> Result<(SOCKADDR_UN, usize)> { - let mut addr = SOCKADDR_UN::default(); - addr.sun_family = ADDRESS_FAMILY(AF_UNIX); - - let bytes = path - .as_ref() - .to_str() - .map(|s| s.as_bytes()) - .ok_or(ErrorKind::InvalidInput)?; - - if bytes.contains(&0) { - return Err(Error::new( - ErrorKind::InvalidInput, - "paths may not contain interior null bytes", - )); - } - if bytes.len() >= addr.sun_path.len() { - return Err(Error::new( - ErrorKind::InvalidInput, - "path must be shorter than SUN_LEN", - )); - } - - unsafe { - std::ptr::copy_nonoverlapping( - bytes.as_ptr(), - addr.sun_path.as_mut_ptr().cast(), - bytes.len(), - ); - } - - let mut len = sun_path_offset(&addr) + bytes.len(); - match bytes.first() { - Some(&0) | None => {} - Some(_) => len += 1, - } - Ok((addr, len)) -} - -pub(crate) fn map_ret(ret: i32) -> Result { - if ret == SOCKET_ERROR { - Err(Error::from_raw_os_error(unsafe { WSAGetLastError().0 })) - } else { - Ok(ret as usize) - } -} - -fn sun_path_offset(addr: &SOCKADDR_UN) -> usize { - // Work with an actual instance of the type since using a null pointer is UB - let base = addr as *const _ as usize; - let path = &addr.sun_path as *const _ as usize; - path - base -} diff --git a/crates/node_runtime/Cargo.toml b/crates/node_runtime/Cargo.toml deleted file mode 100644 index dfa40ad666..0000000000 --- a/crates/node_runtime/Cargo.toml +++ /dev/null @@ -1,36 +0,0 @@ -[package] -name = "node_runtime" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/node_runtime.rs" -doctest = false - -[features] -test-support = [] - -[dependencies] -anyhow.workspace = true -async-compression.workspace = true -async-tar.workspace = true -async-trait.workspace = true -futures.workspace = true -http_client.workspace = true -log.workspace = true -paths.workspace = true -semver.workspace = true -serde.workspace = true -serde_json.workspace = true -smol.workspace = true -util.workspace = true -watch.workspace = true -which.workspace = true - -[target.'cfg(windows)'.dependencies] -async-std = { version = "1.12.0", features = ["unstable"] } diff --git a/crates/node_runtime/LICENSE-GPL b/crates/node_runtime/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/node_runtime/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs deleted file mode 100644 index 1eb6714500..0000000000 --- a/crates/node_runtime/src/node_runtime.rs +++ /dev/null @@ -1,880 +0,0 @@ -use anyhow::{Context as _, Result, anyhow, bail}; -use async_compression::futures::bufread::GzipDecoder; -use async_tar::Archive; -use futures::{AsyncReadExt, FutureExt as _, channel::oneshot, future::Shared}; -use http_client::{Host, HttpClient, Url}; -use log::Level; -use semver::Version; -use serde::Deserialize; -use smol::io::BufReader; -use smol::{fs, lock::Mutex}; -use std::fmt::Display; -use std::{ - env::{self, consts}, - ffi::OsString, - io, - net::{IpAddr, Ipv4Addr}, - path::{Path, PathBuf}, - process::Output, - sync::Arc, -}; -use util::ResultExt; -use util::archive::extract_zip; - -const NODE_CA_CERTS_ENV_VAR: &str = "NODE_EXTRA_CA_CERTS"; - -#[derive(Clone, Debug, Default, Eq, PartialEq)] -pub struct NodeBinaryOptions { - pub allow_path_lookup: bool, - pub allow_binary_download: bool, - pub use_paths: Option<(PathBuf, PathBuf)>, -} - -pub enum VersionStrategy<'a> { - /// Install if current version doesn't match pinned version - Pin(&'a str), - /// Install if current version is older than latest version - Latest(&'a str), -} - -#[derive(Clone)] -pub struct NodeRuntime(Arc>); - -struct NodeRuntimeState { - http: Arc, - instance: Option>, - last_options: Option, - options: watch::Receiver>, - shell_env_loaded: Shared>, -} - -impl NodeRuntime { - pub fn new( - http: Arc, - shell_env_loaded: Option>, - options: watch::Receiver>, - ) -> Self { - NodeRuntime(Arc::new(Mutex::new(NodeRuntimeState { - http, - instance: None, - last_options: None, - options, - shell_env_loaded: shell_env_loaded.unwrap_or(oneshot::channel().1).shared(), - }))) - } - - pub fn unavailable() -> Self { - NodeRuntime(Arc::new(Mutex::new(NodeRuntimeState { - http: Arc::new(http_client::BlockedHttpClient), - instance: None, - last_options: None, - options: watch::channel(Some(NodeBinaryOptions::default())).1, - shell_env_loaded: oneshot::channel().1.shared(), - }))) - } - - async fn instance(&self) -> Box { - let mut state = self.0.lock().await; - - let options = loop { - if let Some(options) = state.options.borrow().as_ref() { - break options.clone(); - } - match state.options.changed().await { - Ok(()) => {} - // failure case not cached - Err(err) => { - return Box::new(UnavailableNodeRuntime { - error_message: err.to_string().into(), - }); - } - } - }; - - if state.last_options.as_ref() != Some(&options) { - state.instance.take(); - } - if let Some(instance) = state.instance.as_ref() { - return instance.boxed_clone(); - } - - if let Some((node, npm)) = options.use_paths.as_ref() { - let instance = match SystemNodeRuntime::new(node.clone(), npm.clone()).await { - Ok(instance) => { - log::info!("using Node.js from `node.path` in settings: {:?}", instance); - Box::new(instance) - } - Err(err) => { - // failure case not cached, since it's cheap to check again - return Box::new(UnavailableNodeRuntime { - error_message: format!( - "failure checking Node.js from `node.path` in settings ({}): {:?}", - node.display(), - err - ) - .into(), - }); - } - }; - state.instance = Some(instance.boxed_clone()); - state.last_options = Some(options); - return instance; - } - - let system_node_error = if options.allow_path_lookup { - state.shell_env_loaded.clone().await.ok(); - match SystemNodeRuntime::detect().await { - Ok(instance) => { - log::info!("using Node.js found on PATH: {:?}", instance); - state.instance = Some(instance.boxed_clone()); - state.last_options = Some(options); - return Box::new(instance); - } - Err(err) => Some(err), - } - } else { - None - }; - - let instance = if options.allow_binary_download { - let (log_level, why_using_managed) = match system_node_error { - Some(err @ DetectError::Other(_)) => (Level::Warn, err.to_string()), - Some(err @ DetectError::NotInPath(_)) => (Level::Info, err.to_string()), - None => ( - Level::Info, - "`node.ignore_system_version` is `true` in settings".to_string(), - ), - }; - match ManagedNodeRuntime::install_if_needed(&state.http).await { - Ok(instance) => { - log::log!( - log_level, - "using Zed managed Node.js at {} since {}", - instance.installation_path.display(), - why_using_managed - ); - Box::new(instance) as Box - } - Err(err) => { - // failure case is cached, since downloading + installing may be expensive. The - // downside of this is that it may fail due to an intermittent network issue. - // - // TODO: Have `install_if_needed` indicate which failure cases are retryable - // and/or have shared tracking of when internet is available. - Box::new(UnavailableNodeRuntime { - error_message: format!( - "failure while downloading and/or installing Zed managed Node.js, \ - restart Zed to retry: {}", - err - ) - .into(), - }) as Box - } - } - } else if let Some(system_node_error) = system_node_error { - // failure case not cached, since it's cheap to check again - // - // TODO: When support is added for setting `options.allow_binary_download`, update this - // error message. - return Box::new(UnavailableNodeRuntime { - error_message: format!( - "failure while checking system Node.js from PATH: {}", - system_node_error - ) - .into(), - }); - } else { - // failure case is cached because it will always happen with these options - // - // TODO: When support is added for setting `options.allow_binary_download`, update this - // error message. - Box::new(UnavailableNodeRuntime { - error_message: "`node` settings do not allow any way to use Node.js" - .to_string() - .into(), - }) - }; - - state.instance = Some(instance.boxed_clone()); - state.last_options = Some(options); - instance - } - - pub async fn binary_path(&self) -> Result { - self.instance().await.binary_path() - } - - pub async fn run_npm_subcommand( - &self, - directory: Option<&Path>, - subcommand: &str, - args: &[&str], - ) -> Result { - let http = self.0.lock().await.http.clone(); - self.instance() - .await - .run_npm_subcommand(directory, http.proxy(), subcommand, args) - .await - } - - pub async fn npm_package_installed_version( - &self, - local_package_directory: &Path, - name: &str, - ) -> Result> { - self.instance() - .await - .npm_package_installed_version(local_package_directory, name) - .await - } - - pub async fn npm_package_latest_version(&self, name: &str) -> Result { - let http = self.0.lock().await.http.clone(); - let output = self - .instance() - .await - .run_npm_subcommand( - None, - http.proxy(), - "info", - &[ - name, - "--json", - "--fetch-retry-mintimeout", - "2000", - "--fetch-retry-maxtimeout", - "5000", - "--fetch-timeout", - "5000", - ], - ) - .await?; - - let mut info: NpmInfo = serde_json::from_slice(&output.stdout)?; - info.dist_tags - .latest - .or_else(|| info.versions.pop()) - .with_context(|| format!("no version found for npm package {name}")) - } - - pub async fn npm_install_packages( - &self, - directory: &Path, - packages: &[(&str, &str)], - ) -> Result<()> { - if packages.is_empty() { - return Ok(()); - } - - let packages: Vec<_> = packages - .iter() - .map(|(name, version)| format!("{name}@{version}")) - .collect(); - - let mut arguments: Vec<_> = packages.iter().map(|p| p.as_str()).collect(); - arguments.extend_from_slice(&[ - "--save-exact", - "--fetch-retry-mintimeout", - "2000", - "--fetch-retry-maxtimeout", - "5000", - "--fetch-timeout", - "5000", - ]); - - // This is also wrong because the directory is wrong. - self.run_npm_subcommand(Some(directory), "install", &arguments) - .await?; - Ok(()) - } - - pub async fn should_install_npm_package( - &self, - package_name: &str, - local_executable_path: &Path, - local_package_directory: &Path, - version_strategy: VersionStrategy<'_>, - ) -> bool { - // In the case of the local system not having the package installed, - // or in the instances where we fail to parse package.json data, - // we attempt to install the package. - if fs::metadata(local_executable_path).await.is_err() { - return true; - } - - let Some(installed_version) = self - .npm_package_installed_version(local_package_directory, package_name) - .await - .log_err() - .flatten() - else { - return true; - }; - - let Some(installed_version) = Version::parse(&installed_version).log_err() else { - return true; - }; - - match version_strategy { - VersionStrategy::Pin(pinned_version) => { - let Some(pinned_version) = Version::parse(pinned_version).log_err() else { - return true; - }; - installed_version != pinned_version - } - VersionStrategy::Latest(latest_version) => { - let Some(latest_version) = Version::parse(latest_version).log_err() else { - return true; - }; - installed_version < latest_version - } - } - } -} - -enum ArchiveType { - TarGz, - Zip, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "kebab-case")] -pub struct NpmInfo { - #[serde(default)] - dist_tags: NpmInfoDistTags, - versions: Vec, -} - -#[derive(Debug, Deserialize, Default)] -pub struct NpmInfoDistTags { - latest: Option, -} - -#[async_trait::async_trait] -trait NodeRuntimeTrait: Send + Sync { - fn boxed_clone(&self) -> Box; - fn binary_path(&self) -> Result; - - async fn run_npm_subcommand( - &self, - directory: Option<&Path>, - proxy: Option<&Url>, - subcommand: &str, - args: &[&str], - ) -> Result; - - async fn npm_package_installed_version( - &self, - local_package_directory: &Path, - name: &str, - ) -> Result>; -} - -#[derive(Clone)] -struct ManagedNodeRuntime { - installation_path: PathBuf, -} - -impl ManagedNodeRuntime { - const VERSION: &str = "v24.11.0"; - - #[cfg(not(windows))] - const NODE_PATH: &str = "bin/node"; - #[cfg(windows)] - const NODE_PATH: &str = "node.exe"; - - #[cfg(not(windows))] - const NPM_PATH: &str = "bin/npm"; - #[cfg(windows)] - const NPM_PATH: &str = "node_modules/npm/bin/npm-cli.js"; - - async fn install_if_needed(http: &Arc) -> Result { - log::info!("Node runtime install_if_needed"); - - let os = match consts::OS { - "macos" => "darwin", - "linux" => "linux", - "windows" => "win", - other => bail!("Running on unsupported os: {other}"), - }; - - let arch = match consts::ARCH { - "x86_64" => "x64", - "aarch64" => "arm64", - other => bail!("Running on unsupported architecture: {other}"), - }; - - let version = Self::VERSION; - let folder_name = format!("node-{version}-{os}-{arch}"); - let node_containing_dir = paths::data_dir().join("node"); - let node_dir = node_containing_dir.join(folder_name); - let node_binary = node_dir.join(Self::NODE_PATH); - let npm_file = node_dir.join(Self::NPM_PATH); - let node_ca_certs = env::var(NODE_CA_CERTS_ENV_VAR).unwrap_or_else(|_| String::new()); - - let valid = if fs::metadata(&node_binary).await.is_ok() { - let result = util::command::new_smol_command(&node_binary) - .env(NODE_CA_CERTS_ENV_VAR, node_ca_certs) - .arg(npm_file) - .arg("--version") - .args(["--cache".into(), node_dir.join("cache")]) - .args(["--userconfig".into(), node_dir.join("blank_user_npmrc")]) - .args(["--globalconfig".into(), node_dir.join("blank_global_npmrc")]) - .output() - .await; - match result { - Ok(output) => { - if output.status.success() { - true - } else { - log::warn!( - "Zed managed Node.js binary at {} failed check with output: {:?}", - node_binary.display(), - output - ); - false - } - } - Err(err) => { - log::warn!( - "Zed managed Node.js binary at {} failed check, so re-downloading it. \ - Error: {}", - node_binary.display(), - err - ); - false - } - } - } else { - false - }; - - if !valid { - _ = fs::remove_dir_all(&node_containing_dir).await; - fs::create_dir(&node_containing_dir) - .await - .context("error creating node containing dir")?; - - let archive_type = match consts::OS { - "macos" | "linux" => ArchiveType::TarGz, - "windows" => ArchiveType::Zip, - other => bail!("Running on unsupported os: {other}"), - }; - - let version = Self::VERSION; - let file_name = format!( - "node-{version}-{os}-{arch}.{extension}", - extension = match archive_type { - ArchiveType::TarGz => "tar.gz", - ArchiveType::Zip => "zip", - } - ); - - let url = format!("https://nodejs.org/dist/{version}/{file_name}"); - log::info!("Downloading Node.js binary from {url}"); - let mut response = http - .get(&url, Default::default(), true) - .await - .context("error downloading Node binary tarball")?; - log::info!("Download of Node.js complete, extracting..."); - - let body = response.body_mut(); - match archive_type { - ArchiveType::TarGz => { - let decompressed_bytes = GzipDecoder::new(BufReader::new(response.body_mut())); - let archive = Archive::new(decompressed_bytes); - archive.unpack(&node_containing_dir).await?; - } - ArchiveType::Zip => extract_zip(&node_containing_dir, body).await?, - } - log::info!("Extracted Node.js to {}", node_containing_dir.display()) - } - - // Note: Not in the `if !valid {}` so we can populate these for existing installations - _ = fs::create_dir(node_dir.join("cache")).await; - _ = fs::write(node_dir.join("blank_user_npmrc"), []).await; - _ = fs::write(node_dir.join("blank_global_npmrc"), []).await; - - anyhow::Ok(ManagedNodeRuntime { - installation_path: node_dir, - }) - } -} - -fn path_with_node_binary_prepended(node_binary: &Path) -> Option { - let existing_path = env::var_os("PATH"); - let node_bin_dir = node_binary.parent().map(|dir| dir.as_os_str()); - match (existing_path, node_bin_dir) { - (Some(existing_path), Some(node_bin_dir)) => { - if let Ok(joined) = env::join_paths( - [PathBuf::from(node_bin_dir)] - .into_iter() - .chain(env::split_paths(&existing_path)), - ) { - Some(joined) - } else { - Some(existing_path) - } - } - (Some(existing_path), None) => Some(existing_path), - (None, Some(node_bin_dir)) => Some(node_bin_dir.to_owned()), - _ => None, - } -} - -#[async_trait::async_trait] -impl NodeRuntimeTrait for ManagedNodeRuntime { - fn boxed_clone(&self) -> Box { - Box::new(self.clone()) - } - - fn binary_path(&self) -> Result { - Ok(self.installation_path.join(Self::NODE_PATH)) - } - - async fn run_npm_subcommand( - &self, - directory: Option<&Path>, - proxy: Option<&Url>, - subcommand: &str, - args: &[&str], - ) -> Result { - let attempt = || async move { - let node_binary = self.installation_path.join(Self::NODE_PATH); - let npm_file = self.installation_path.join(Self::NPM_PATH); - let env_path = path_with_node_binary_prepended(&node_binary).unwrap_or_default(); - - anyhow::ensure!( - smol::fs::metadata(&node_binary).await.is_ok(), - "missing node binary file" - ); - anyhow::ensure!( - smol::fs::metadata(&npm_file).await.is_ok(), - "missing npm file" - ); - - let node_ca_certs = env::var(NODE_CA_CERTS_ENV_VAR).unwrap_or_else(|_| String::new()); - - let mut command = util::command::new_smol_command(node_binary); - command.env("PATH", env_path); - command.env(NODE_CA_CERTS_ENV_VAR, node_ca_certs); - command.arg(npm_file).arg(subcommand); - command.arg(format!( - "--cache={}", - self.installation_path.join("cache").display() - )); - command.args([ - "--userconfig".into(), - self.installation_path.join("blank_user_npmrc"), - ]); - command.args([ - "--globalconfig".into(), - self.installation_path.join("blank_global_npmrc"), - ]); - command.args(args); - configure_npm_command(&mut command, directory, proxy); - command.output().await.map_err(|e| anyhow!("{e}")) - }; - - let mut output = attempt().await; - if output.is_err() { - output = attempt().await; - anyhow::ensure!( - output.is_ok(), - "failed to launch npm subcommand {subcommand} subcommand\nerr: {:?}", - output.err() - ); - } - - if let Ok(output) = &output { - anyhow::ensure!( - output.status.success(), - "failed to execute npm {subcommand} subcommand:\nstdout: {:?}\nstderr: {:?}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - } - - output.map_err(|e| anyhow!("{e}")) - } - async fn npm_package_installed_version( - &self, - local_package_directory: &Path, - name: &str, - ) -> Result> { - read_package_installed_version(local_package_directory.join("node_modules"), name).await - } -} - -#[derive(Debug, Clone)] -pub struct SystemNodeRuntime { - node: PathBuf, - npm: PathBuf, - global_node_modules: PathBuf, - scratch_dir: PathBuf, -} - -impl SystemNodeRuntime { - const MIN_VERSION: semver::Version = Version::new(22, 0, 0); - async fn new(node: PathBuf, npm: PathBuf) -> Result { - let output = util::command::new_smol_command(&node) - .arg("--version") - .output() - .await - .with_context(|| format!("running node from {:?}", node))?; - if !output.status.success() { - anyhow::bail!( - "failed to run node --version. stdout: {}, stderr: {}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), - ); - } - let version_str = String::from_utf8_lossy(&output.stdout); - let version = semver::Version::parse(version_str.trim().trim_start_matches('v'))?; - if version < Self::MIN_VERSION { - anyhow::bail!( - "node at {} is too old. want: {}, got: {}", - node.to_string_lossy(), - Self::MIN_VERSION, - version - ) - } - - let scratch_dir = paths::data_dir().join("node"); - fs::create_dir(&scratch_dir).await.ok(); - fs::create_dir(scratch_dir.join("cache")).await.ok(); - - let mut this = Self { - node, - npm, - global_node_modules: PathBuf::default(), - scratch_dir, - }; - let output = this.run_npm_subcommand(None, None, "root", &["-g"]).await?; - this.global_node_modules = - PathBuf::from(String::from_utf8_lossy(&output.stdout).to_string()); - - Ok(this) - } - - async fn detect() -> std::result::Result { - let node = which::which("node").map_err(DetectError::NotInPath)?; - let npm = which::which("npm").map_err(DetectError::NotInPath)?; - Self::new(node, npm).await.map_err(DetectError::Other) - } -} - -enum DetectError { - NotInPath(which::Error), - Other(anyhow::Error), -} - -impl Display for DetectError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - DetectError::NotInPath(err) => { - write!(f, "system Node.js wasn't found on PATH: {}", err) - } - DetectError::Other(err) => { - write!(f, "checking system Node.js failed with error: {}", err) - } - } - } -} - -#[async_trait::async_trait] -impl NodeRuntimeTrait for SystemNodeRuntime { - fn boxed_clone(&self) -> Box { - Box::new(self.clone()) - } - - fn binary_path(&self) -> Result { - Ok(self.node.clone()) - } - - async fn run_npm_subcommand( - &self, - directory: Option<&Path>, - proxy: Option<&Url>, - subcommand: &str, - args: &[&str], - ) -> anyhow::Result { - let node_ca_certs = env::var(NODE_CA_CERTS_ENV_VAR).unwrap_or_else(|_| String::new()); - let mut command = util::command::new_smol_command(self.npm.clone()); - let path = path_with_node_binary_prepended(&self.node).unwrap_or_default(); - command - .env("PATH", path) - .env(NODE_CA_CERTS_ENV_VAR, node_ca_certs) - .arg(subcommand) - .arg(format!( - "--cache={}", - self.scratch_dir.join("cache").display() - )) - .args(args); - configure_npm_command(&mut command, directory, proxy); - let output = command.output().await?; - anyhow::ensure!( - output.status.success(), - "failed to execute npm {subcommand} subcommand:\nstdout: {:?}\nstderr: {:?}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - Ok(output) - } - - async fn npm_package_installed_version( - &self, - local_package_directory: &Path, - name: &str, - ) -> Result> { - read_package_installed_version(local_package_directory.join("node_modules"), name).await - // todo: allow returning a globally installed version (requires callers not to hard-code the path) - } -} - -pub async fn read_package_installed_version( - node_module_directory: PathBuf, - name: &str, -) -> Result> { - let package_json_path = node_module_directory.join(name).join("package.json"); - - let mut file = match fs::File::open(package_json_path).await { - Ok(file) => file, - Err(err) => { - if err.kind() == io::ErrorKind::NotFound { - return Ok(None); - } - - Err(err)? - } - }; - - #[derive(Deserialize)] - struct PackageJson { - version: String, - } - - let mut contents = String::new(); - file.read_to_string(&mut contents).await?; - let package_json: PackageJson = serde_json::from_str(&contents)?; - Ok(Some(package_json.version)) -} - -#[derive(Clone)] -pub struct UnavailableNodeRuntime { - error_message: Arc, -} - -#[async_trait::async_trait] -impl NodeRuntimeTrait for UnavailableNodeRuntime { - fn boxed_clone(&self) -> Box { - Box::new(self.clone()) - } - fn binary_path(&self) -> Result { - bail!("{}", self.error_message) - } - - async fn run_npm_subcommand( - &self, - _: Option<&Path>, - _: Option<&Url>, - _: &str, - _: &[&str], - ) -> anyhow::Result { - bail!("{}", self.error_message) - } - - async fn npm_package_installed_version( - &self, - _local_package_directory: &Path, - _: &str, - ) -> Result> { - bail!("{}", self.error_message) - } -} - -fn configure_npm_command( - command: &mut smol::process::Command, - directory: Option<&Path>, - proxy: Option<&Url>, -) { - if let Some(directory) = directory { - command.current_dir(directory); - command.args(["--prefix".into(), directory.to_path_buf()]); - } - - if let Some(mut proxy) = proxy.cloned() { - // Map proxy settings from `http://localhost:10809` to `http://127.0.0.1:10809` - // NodeRuntime without environment information can not parse `localhost` - // correctly. - // TODO: map to `[::1]` if we are using ipv6 - if matches!(proxy.host(), Some(Host::Domain(domain)) if domain.eq_ignore_ascii_case("localhost")) - { - // When localhost is a valid Host, so is `127.0.0.1` - let _ = proxy.set_ip_host(IpAddr::V4(Ipv4Addr::LOCALHOST)); - } - - command.args(["--proxy", proxy.as_str()]); - } - - #[cfg(windows)] - { - // SYSTEMROOT is a critical environment variables for Windows. - if let Some(val) = env::var("SYSTEMROOT") - .context("Missing environment variable: SYSTEMROOT!") - .log_err() - { - command.env("SYSTEMROOT", val); - } - // Without ComSpec, the post-install will always fail. - if let Some(val) = env::var("ComSpec") - .context("Missing environment variable: ComSpec!") - .log_err() - { - command.env("ComSpec", val); - } - } -} - -#[cfg(test)] -mod tests { - use http_client::Url; - - use super::configure_npm_command; - - // Map localhost to 127.0.0.1 - // NodeRuntime without environment information can not parse `localhost` correctly. - #[test] - fn test_configure_npm_command_map_localhost_proxy() { - const CASES: [(&str, &str); 4] = [ - // Map localhost to 127.0.0.1 - ("http://localhost:9090/", "http://127.0.0.1:9090/"), - ("https://google.com/", "https://google.com/"), - ( - "http://username:password@proxy.thing.com:8080/", - "http://username:password@proxy.thing.com:8080/", - ), - // Test when localhost is contained within a different part of the URL - ( - "http://username:localhost@localhost:8080/", - "http://username:localhost@127.0.0.1:8080/", - ), - ]; - - for (proxy, mapped_proxy) in CASES { - let mut dummy = smol::process::Command::new(""); - let proxy = Url::parse(proxy).unwrap(); - configure_npm_command(&mut dummy, None, Some(&proxy)); - let proxy = dummy - .get_args() - .skip_while(|&arg| arg != "--proxy") - .skip(1) - .next(); - let proxy = proxy.expect("Proxy was not passed to Command correctly"); - assert_eq!( - proxy, mapped_proxy, - "Incorrectly mapped localhost to 127.0.0.1" - ); - } - } -} diff --git a/crates/notifications/Cargo.toml b/crates/notifications/Cargo.toml deleted file mode 100644 index 8304c788fd..0000000000 --- a/crates/notifications/Cargo.toml +++ /dev/null @@ -1,44 +0,0 @@ -[package] -name = "notifications" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/notifications.rs" -doctest = false - -[features] -test-support = [ - "channel/test-support", - "collections/test-support", - "gpui/test-support", - "rpc/test-support", -] - -[dependencies] -anyhow.workspace = true -channel.workspace = true -client.workspace = true -component.workspace = true -db.workspace = true -gpui.workspace = true -rpc.workspace = true -sum_tree.workspace = true -time.workspace = true -ui.workspace = true -util.workspace = true -workspace.workspace = true -zed_actions.workspace = true - -[dev-dependencies] -client = { workspace = true, features = ["test-support"] } -collections = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -rpc = { workspace = true, features = ["test-support"] } -settings = { workspace = true, features = ["test-support"] } -util = { workspace = true, features = ["test-support"] } diff --git a/crates/notifications/LICENSE-GPL b/crates/notifications/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/notifications/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/notifications/src/notification_store.rs b/crates/notifications/src/notification_store.rs deleted file mode 100644 index 7cae74a729..0000000000 --- a/crates/notifications/src/notification_store.rs +++ /dev/null @@ -1,432 +0,0 @@ -use anyhow::{Context as _, Result}; -use channel::ChannelStore; -use client::{ChannelId, Client, UserStore}; -use db::smol::stream::StreamExt; -use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Global, Task}; -use rpc::{Notification, TypedEnvelope, proto}; -use std::{ops::Range, sync::Arc}; -use sum_tree::{Bias, Dimensions, SumTree}; -use time::OffsetDateTime; -use util::ResultExt; - -pub fn init(client: Arc, user_store: Entity, cx: &mut App) { - let notification_store = cx.new(|cx| NotificationStore::new(client, user_store, cx)); - cx.set_global(GlobalNotificationStore(notification_store)); -} - -struct GlobalNotificationStore(Entity); - -impl Global for GlobalNotificationStore {} - -pub struct NotificationStore { - client: Arc, - user_store: Entity, - channel_store: Entity, - notifications: SumTree, - loaded_all_notifications: bool, - _watch_connection_status: Task>, - _subscriptions: Vec, -} - -#[derive(Clone, PartialEq, Eq, Debug)] -pub enum NotificationEvent { - NotificationsUpdated { - old_range: Range, - new_count: usize, - }, - NewNotification { - entry: NotificationEntry, - }, - NotificationRemoved { - entry: NotificationEntry, - }, - NotificationRead { - entry: NotificationEntry, - }, -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct NotificationEntry { - pub id: u64, - pub notification: Notification, - pub timestamp: OffsetDateTime, - pub is_read: bool, - pub response: Option, -} - -#[derive(Clone, Debug, Default)] -pub struct NotificationSummary { - max_id: u64, - count: usize, - unread_count: usize, -} - -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] -struct Count(usize); - -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] -struct NotificationId(u64); - -impl NotificationStore { - pub fn global(cx: &App) -> Entity { - cx.global::().0.clone() - } - - pub fn new(client: Arc, user_store: Entity, cx: &mut Context) -> Self { - let mut connection_status = client.status(); - let watch_connection_status = cx.spawn(async move |this, cx| { - while let Some(status) = connection_status.next().await { - let this = this.upgrade()?; - match status { - client::Status::Connected { .. } => { - if let Some(task) = this - .update(cx, |this, cx| this.handle_connect(cx)) - .log_err()? - { - task.await.log_err()?; - } - } - _ => this - .update(cx, |this, cx| this.handle_disconnect(cx)) - .log_err()?, - } - } - Some(()) - }); - - Self { - channel_store: ChannelStore::global(cx), - notifications: Default::default(), - loaded_all_notifications: false, - _watch_connection_status: watch_connection_status, - _subscriptions: vec![ - client.add_message_handler(cx.weak_entity(), Self::handle_new_notification), - client.add_message_handler(cx.weak_entity(), Self::handle_delete_notification), - ], - user_store, - client, - } - } - - pub fn notification_count(&self) -> usize { - self.notifications.summary().count - } - - pub fn unread_notification_count(&self) -> usize { - self.notifications.summary().unread_count - } - - // Get the nth newest notification. - pub fn notification_at(&self, ix: usize) -> Option<&NotificationEntry> { - let count = self.notifications.summary().count; - if ix >= count { - return None; - } - let ix = count - 1 - ix; - let (.., item) = self - .notifications - .find::((), &Count(ix), Bias::Right); - item - } - pub fn notification_for_id(&self, id: u64) -> Option<&NotificationEntry> { - let (.., item) = - self.notifications - .find::((), &NotificationId(id), Bias::Left); - if let Some(item) = item - && item.id == id - { - return Some(item); - } - None - } - - pub fn load_more_notifications( - &self, - clear_old: bool, - cx: &mut Context, - ) -> Option>> { - if self.loaded_all_notifications && !clear_old { - return None; - } - - let before_id = if clear_old { - None - } else { - self.notifications.first().map(|entry| entry.id) - }; - let request = self.client.request(proto::GetNotifications { before_id }); - Some(cx.spawn(async move |this, cx| { - let this = this - .upgrade() - .context("Notification store was dropped while loading notifications")?; - - let response = request.await?; - this.update(cx, |this, _| this.loaded_all_notifications = response.done)?; - Self::add_notifications( - this, - response.notifications, - AddNotificationsOptions { - is_new: false, - clear_old, - includes_first: response.done, - }, - cx, - ) - .await?; - Ok(()) - })) - } - - fn handle_connect(&mut self, cx: &mut Context) -> Option>> { - self.notifications = Default::default(); - cx.notify(); - self.load_more_notifications(true, cx) - } - - fn handle_disconnect(&mut self, cx: &mut Context) { - cx.notify() - } - - async fn handle_new_notification( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - Self::add_notifications( - this, - envelope.payload.notification.into_iter().collect(), - AddNotificationsOptions { - is_new: true, - clear_old: false, - includes_first: false, - }, - &mut cx, - ) - .await - } - - async fn handle_delete_notification( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - this.update(&mut cx, |this, cx| { - this.splice_notifications([(envelope.payload.notification_id, None)], false, cx); - Ok(()) - })? - } - - async fn add_notifications( - this: Entity, - notifications: Vec, - options: AddNotificationsOptions, - cx: &mut AsyncApp, - ) -> Result<()> { - let mut user_ids = Vec::new(); - - let notifications = notifications - .into_iter() - .filter_map(|message| { - Some(NotificationEntry { - id: message.id, - is_read: message.is_read, - timestamp: OffsetDateTime::from_unix_timestamp(message.timestamp as i64) - .ok()?, - notification: Notification::from_proto(&message)?, - response: message.response, - }) - }) - .collect::>(); - if notifications.is_empty() { - return Ok(()); - } - - for entry in ¬ifications { - match entry.notification { - Notification::ChannelInvitation { inviter_id, .. } => { - user_ids.push(inviter_id); - } - Notification::ContactRequest { - sender_id: requester_id, - } => { - user_ids.push(requester_id); - } - Notification::ContactRequestAccepted { - responder_id: contact_id, - } => { - user_ids.push(contact_id); - } - } - } - - let user_store = this.read_with(cx, |this, _| this.user_store.clone())?; - - user_store - .update(cx, |store, cx| store.get_users(user_ids, cx))? - .await?; - this.update(cx, |this, cx| { - if options.clear_old { - cx.emit(NotificationEvent::NotificationsUpdated { - old_range: 0..this.notifications.summary().count, - new_count: 0, - }); - this.notifications = SumTree::default(); - this.loaded_all_notifications = false; - } - - if options.includes_first { - this.loaded_all_notifications = true; - } - - this.splice_notifications( - notifications - .into_iter() - .map(|notification| (notification.id, Some(notification))), - options.is_new, - cx, - ); - }) - .log_err(); - - Ok(()) - } - - fn splice_notifications( - &mut self, - notifications: impl IntoIterator)>, - is_new: bool, - cx: &mut Context, - ) { - let mut cursor = self - .notifications - .cursor::>(()); - let mut new_notifications = SumTree::default(); - let mut old_range = 0..0; - - for (i, (id, new_notification)) in notifications.into_iter().enumerate() { - new_notifications.append(cursor.slice(&NotificationId(id), Bias::Left), ()); - - if i == 0 { - old_range.start = cursor.start().1.0; - } - - let old_notification = cursor.item(); - if let Some(old_notification) = old_notification { - if old_notification.id == id { - cursor.next(); - - if let Some(new_notification) = &new_notification { - if new_notification.is_read { - cx.emit(NotificationEvent::NotificationRead { - entry: new_notification.clone(), - }); - } - } else { - cx.emit(NotificationEvent::NotificationRemoved { - entry: old_notification.clone(), - }); - } - } - } else if let Some(new_notification) = &new_notification - && is_new - { - cx.emit(NotificationEvent::NewNotification { - entry: new_notification.clone(), - }); - } - - if let Some(notification) = new_notification { - new_notifications.push(notification, ()); - } - } - - old_range.end = cursor.start().1.0; - let new_count = new_notifications.summary().count - old_range.start; - new_notifications.append(cursor.suffix(), ()); - drop(cursor); - - self.notifications = new_notifications; - cx.emit(NotificationEvent::NotificationsUpdated { - old_range, - new_count, - }); - } - - pub fn respond_to_notification( - &mut self, - notification: Notification, - response: bool, - cx: &mut Context, - ) { - match notification { - Notification::ContactRequest { sender_id } => { - self.user_store - .update(cx, |store, cx| { - store.respond_to_contact_request(sender_id, response, cx) - }) - .detach(); - } - Notification::ChannelInvitation { channel_id, .. } => { - self.channel_store - .update(cx, |store, cx| { - store.respond_to_channel_invite(ChannelId(channel_id), response, cx) - }) - .detach(); - } - _ => {} - } - } -} - -impl EventEmitter for NotificationStore {} - -impl sum_tree::Item for NotificationEntry { - type Summary = NotificationSummary; - - fn summary(&self, _cx: ()) -> Self::Summary { - NotificationSummary { - max_id: self.id, - count: 1, - unread_count: if self.is_read { 0 } else { 1 }, - } - } -} - -impl sum_tree::ContextLessSummary for NotificationSummary { - fn zero() -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &Self) { - self.max_id = self.max_id.max(summary.max_id); - self.count += summary.count; - self.unread_count += summary.unread_count; - } -} - -impl sum_tree::Dimension<'_, NotificationSummary> for NotificationId { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &NotificationSummary, _: ()) { - debug_assert!(summary.max_id > self.0); - self.0 = summary.max_id; - } -} - -impl sum_tree::Dimension<'_, NotificationSummary> for Count { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &NotificationSummary, _: ()) { - self.0 += summary.count; - } -} - -struct AddNotificationsOptions { - is_new: bool, - clear_old: bool, - includes_first: bool, -} diff --git a/crates/notifications/src/notifications.rs b/crates/notifications/src/notifications.rs deleted file mode 100644 index ee952555eb..0000000000 --- a/crates/notifications/src/notifications.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod notification_store; - -pub use notification_store::*; -pub mod status_toast; diff --git a/crates/notifications/src/status_toast.rs b/crates/notifications/src/status_toast.rs deleted file mode 100644 index 7affa93f5a..0000000000 --- a/crates/notifications/src/status_toast.rs +++ /dev/null @@ -1,254 +0,0 @@ -use std::rc::Rc; - -use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, IntoElement}; -use ui::{Tooltip, prelude::*}; -use workspace::{ToastAction, ToastView}; -use zed_actions::toast; - -#[derive(Clone, Copy)] -pub struct ToastIcon { - icon: IconName, - color: Color, -} - -impl ToastIcon { - pub fn new(icon: IconName) -> Self { - Self { - icon, - color: Color::default(), - } - } - - pub fn color(mut self, color: Color) -> Self { - self.color = color; - self - } -} - -impl From for ToastIcon { - fn from(icon: IconName) -> Self { - Self { - icon, - color: Color::default(), - } - } -} - -#[derive(RegisterComponent)] -pub struct StatusToast { - icon: Option, - text: SharedString, - action: Option, - show_dismiss: bool, - this_handle: Entity, - focus_handle: FocusHandle, -} - -impl StatusToast { - pub fn new( - text: impl Into, - cx: &mut App, - f: impl FnOnce(Self, &mut Context) -> Self, - ) -> Entity { - cx.new(|cx| { - let focus_handle = cx.focus_handle(); - - f( - Self { - text: text.into(), - icon: None, - action: None, - show_dismiss: false, - this_handle: cx.entity(), - focus_handle, - }, - cx, - ) - }) - } - - pub fn icon(mut self, icon: ToastIcon) -> Self { - self.icon = Some(icon); - self - } - - pub fn action( - mut self, - label: impl Into, - f: impl Fn(&mut Window, &mut App) + 'static, - ) -> Self { - let this_handle = self.this_handle.clone(); - self.action = Some(ToastAction::new( - label.into(), - Some(Rc::new(move |window, cx| { - this_handle.update(cx, |_, cx| { - cx.emit(DismissEvent); - }); - f(window, cx); - })), - )); - self - } - - pub fn dismiss_button(mut self, show: bool) -> Self { - self.show_dismiss = show; - self - } -} - -impl Render for StatusToast { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let has_action_or_dismiss = self.action.is_some() || self.show_dismiss; - - h_flex() - .id("status-toast") - .elevation_3(cx) - .gap_2() - .py_1p5() - .pl_2p5() - .map(|this| { - if has_action_or_dismiss { - this.pr_1p5() - } else { - this.pr_2p5() - } - }) - .flex_none() - .bg(cx.theme().colors().surface_background) - .shadow_lg() - .when_some(self.icon.as_ref(), |this, icon| { - this.child(Icon::new(icon.icon).color(icon.color)) - }) - .child(Label::new(self.text.clone()).color(Color::Default)) - .when_some(self.action.as_ref(), |this, action| { - this.child( - Button::new(action.id.clone(), action.label.clone()) - .tooltip(Tooltip::for_action_title( - action.label.clone(), - &toast::RunAction, - )) - .color(Color::Muted) - .when_some(action.on_click.clone(), |el, handler| { - el.on_click(move |_click_event, window, cx| handler(window, cx)) - }), - ) - }) - .when(self.show_dismiss, |this| { - let handle = self.this_handle.clone(); - this.child( - IconButton::new("dismiss", IconName::Close) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .tooltip(Tooltip::text("Dismiss")) - .on_click(move |_click_event, _window, cx| { - handle.update(cx, |_, cx| { - cx.emit(DismissEvent); - }); - }), - ) - }) - } -} - -impl ToastView for StatusToast { - fn action(&self) -> Option { - self.action.clone() - } -} - -impl Focusable for StatusToast { - fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle { - self.focus_handle.clone() - } -} - -impl EventEmitter for StatusToast {} - -impl Component for StatusToast { - fn scope() -> ComponentScope { - ComponentScope::Notification - } - - fn preview(_window: &mut Window, cx: &mut App) -> Option { - let text_example = StatusToast::new("Operation completed", cx, |this, _| this); - - let action_example = StatusToast::new("Update ready to install", cx, |this, _cx| { - this.action("Restart", |_, _| {}) - }); - - let dismiss_button_example = - StatusToast::new("Dismiss Button", cx, |this, _| this.dismiss_button(true)); - - let icon_example = StatusToast::new( - "Nathan Sobo accepted your contact request", - cx, - |this, _| this.icon(ToastIcon::new(IconName::Check).color(Color::Muted)), - ); - - let success_example = StatusToast::new("Pushed 4 changes to `zed/main`", cx, |this, _| { - this.icon(ToastIcon::new(IconName::Check).color(Color::Success)) - }); - - let error_example = StatusToast::new( - "git push: Couldn't find remote origin `iamnbutler/zed`", - cx, - |this, _cx| { - this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error)) - .action("More Info", |_, _| {}) - }, - ); - - let warning_example = StatusToast::new("You have outdated settings", cx, |this, _cx| { - this.icon(ToastIcon::new(IconName::Warning).color(Color::Warning)) - .action("More Info", |_, _| {}) - }); - - let pr_example = - StatusToast::new("`zed/new-notification-system` created!", cx, |this, _cx| { - this.icon(ToastIcon::new(IconName::GitBranchAlt).color(Color::Muted)) - .action("Open Pull Request", |_, cx| { - cx.open_url("https://github.com/") - }) - }); - - Some( - v_flex() - .gap_6() - .p_4() - .children(vec![ - example_group_with_title( - "Basic Toast", - vec![ - single_example("Text", div().child(text_example).into_any_element()), - single_example( - "Action", - div().child(action_example).into_any_element(), - ), - single_example("Icon", div().child(icon_example).into_any_element()), - single_example( - "Dismiss Button", - div().child(dismiss_button_example).into_any_element(), - ), - ], - ), - example_group_with_title( - "Examples", - vec![ - single_example( - "Success", - div().child(success_example).into_any_element(), - ), - single_example("Error", div().child(error_example).into_any_element()), - single_example( - "Warning", - div().child(warning_example).into_any_element(), - ), - single_example("Create PR", div().child(pr_example).into_any_element()), - ], - ) - .vertical(), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ollama/Cargo.toml b/crates/ollama/Cargo.toml deleted file mode 100644 index fed74993fa..0000000000 --- a/crates/ollama/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "ollama" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/ollama.rs" - -[features] -default = [] -schemars = ["dep:schemars"] - -[dependencies] -anyhow.workspace = true -futures.workspace = true -http_client.workspace = true -schemars = { workspace = true, optional = true } -serde.workspace = true -serde_json.workspace = true -settings.workspace = true diff --git a/crates/ollama/LICENSE-GPL b/crates/ollama/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/ollama/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/ollama/src/ollama.rs b/crates/ollama/src/ollama.rs deleted file mode 100644 index f6614379fa..0000000000 --- a/crates/ollama/src/ollama.rs +++ /dev/null @@ -1,660 +0,0 @@ -use anyhow::{Context as _, Result}; -use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream}; -use http_client::{AsyncBody, HttpClient, HttpRequestExt, Method, Request as HttpRequest}; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -pub use settings::KeepAlive; - -pub const OLLAMA_API_URL: &str = "http://localhost:11434"; - -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] -pub struct Model { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub keep_alive: Option, - pub supports_tools: Option, - pub supports_vision: Option, - pub supports_thinking: Option, -} - -fn get_max_tokens(name: &str) -> u64 { - /// Default context length for unknown models. - const DEFAULT_TOKENS: u64 = 4096; - /// Magic number. Lets many Ollama models work with ~16GB of ram. - /// Models that support context beyond 16k such as codestral (32k) or devstral (128k) will be clamped down to 16k - const MAXIMUM_TOKENS: u64 = 16384; - - match name.split(':').next().unwrap() { - "granite-code" | "phi" | "tinyllama" => 2048, - "llama2" | "stablelm2" | "vicuna" | "yi" => 4096, - "aya" | "codegemma" | "gemma" | "gemma2" | "llama3" | "starcoder" => 8192, - "codellama" | "starcoder2" => 16384, - "codestral" | "dolphin-mixtral" | "llava" | "magistral" | "mistral" | "mixstral" - | "qwen2" | "qwen2.5-coder" => 32768, - "cogito" | "command-r" | "deepseek-coder-v2" | "deepseek-r1" | "deepseek-v3" - | "devstral" | "gemma3" | "gpt-oss" | "granite3.3" | "llama3.1" | "llama3.2" - | "llama3.3" | "mistral-nemo" | "phi3" | "phi3.5" | "phi4" | "qwen3" | "yi-coder" => 128000, - "qwen3-coder" => 256000, - _ => DEFAULT_TOKENS, - } - .clamp(1, MAXIMUM_TOKENS) -} - -impl Model { - pub fn new( - name: &str, - display_name: Option<&str>, - max_tokens: Option, - supports_tools: Option, - supports_vision: Option, - supports_thinking: Option, - ) -> Self { - Self { - name: name.to_owned(), - display_name: display_name - .map(ToString::to_string) - .or_else(|| name.strip_suffix(":latest").map(ToString::to_string)), - max_tokens: max_tokens.unwrap_or_else(|| get_max_tokens(name)), - keep_alive: Some(KeepAlive::indefinite()), - supports_tools, - supports_vision, - supports_thinking, - } - } - - pub fn id(&self) -> &str { - &self.name - } - - pub fn display_name(&self) -> &str { - self.display_name.as_ref().unwrap_or(&self.name) - } - - pub fn max_token_count(&self) -> u64 { - self.max_tokens - } -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(tag = "role", rename_all = "lowercase")] -pub enum ChatMessage { - Assistant { - content: String, - tool_calls: Option>, - #[serde(skip_serializing_if = "Option::is_none")] - images: Option>, - thinking: Option, - }, - User { - content: String, - #[serde(skip_serializing_if = "Option::is_none")] - images: Option>, - }, - System { - content: String, - }, - Tool { - tool_name: String, - content: String, - }, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct OllamaToolCall { - // TODO: Remove `Option` after most users have updated to Ollama v0.12.10, - // which was released on the 4th of November 2025 - pub id: Option, - pub function: OllamaFunctionCall, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct OllamaFunctionCall { - pub name: String, - pub arguments: Value, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct OllamaFunctionTool { - pub name: String, - pub description: Option, - pub parameters: Option, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -#[serde(tag = "type", rename_all = "lowercase")] -pub enum OllamaTool { - Function { function: OllamaFunctionTool }, -} - -#[derive(Serialize, Debug)] -pub struct ChatRequest { - pub model: String, - pub messages: Vec, - pub stream: bool, - pub keep_alive: KeepAlive, - pub options: Option, - pub tools: Vec, - pub think: Option, -} - -// https://github.com/ollama/ollama/blob/main/docs/modelfile.md#valid-parameters-and-values -#[derive(Serialize, Default, Debug)] -pub struct ChatOptions { - pub num_ctx: Option, - pub num_predict: Option, - pub stop: Option>, - pub temperature: Option, - pub top_p: Option, -} - -#[derive(Deserialize, Debug)] -pub struct ChatResponseDelta { - pub model: String, - pub created_at: String, - pub message: ChatMessage, - pub done_reason: Option, - pub done: bool, - pub prompt_eval_count: Option, - pub eval_count: Option, -} - -#[derive(Serialize, Deserialize)] -pub struct LocalModelsResponse { - pub models: Vec, -} - -#[derive(Serialize, Deserialize)] -pub struct LocalModelListing { - pub name: String, - pub modified_at: String, - pub size: u64, - pub digest: String, - pub details: ModelDetails, -} - -#[derive(Serialize, Deserialize)] -pub struct LocalModel { - pub modelfile: String, - pub parameters: String, - pub template: String, - pub details: ModelDetails, -} - -#[derive(Serialize, Deserialize)] -pub struct ModelDetails { - pub format: String, - pub family: String, - pub families: Option>, - pub parameter_size: String, - pub quantization_level: String, -} - -#[derive(Debug)] -pub struct ModelShow { - pub capabilities: Vec, - pub context_length: Option, - pub architecture: Option, -} - -impl<'de> Deserialize<'de> for ModelShow { - fn deserialize(deserializer: D) -> std::result::Result - where - D: serde::Deserializer<'de>, - { - use serde::de::{self, MapAccess, Visitor}; - use std::fmt; - - struct ModelShowVisitor; - - impl<'de> Visitor<'de> for ModelShowVisitor { - type Value = ModelShow; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a ModelShow object") - } - - fn visit_map(self, mut map: A) -> Result - where - A: MapAccess<'de>, - { - let mut capabilities: Vec = Vec::new(); - let mut architecture: Option = None; - let mut context_length: Option = None; - - while let Some(key) = map.next_key::()? { - match key.as_str() { - "capabilities" => { - capabilities = map.next_value()?; - } - "model_info" => { - let model_info: Value = map.next_value()?; - if let Value::Object(obj) = model_info { - architecture = obj - .get("general.architecture") - .and_then(|v| v.as_str()) - .map(String::from); - - if let Some(arch) = &architecture { - context_length = obj - .get(&format!("{}.context_length", arch)) - .and_then(|v| v.as_u64()); - } - } - } - _ => { - let _: de::IgnoredAny = map.next_value()?; - } - } - } - - Ok(ModelShow { - capabilities, - context_length, - architecture, - }) - } - } - - deserializer.deserialize_map(ModelShowVisitor) - } -} - -impl ModelShow { - pub fn supports_tools(&self) -> bool { - // .contains expects &String, which would require an additional allocation - self.capabilities.iter().any(|v| v == "tools") - } - - pub fn supports_vision(&self) -> bool { - self.capabilities.iter().any(|v| v == "vision") - } - - pub fn supports_thinking(&self) -> bool { - self.capabilities.iter().any(|v| v == "thinking") - } -} - -pub async fn stream_chat_completion( - client: &dyn HttpClient, - api_url: &str, - api_key: Option<&str>, - request: ChatRequest, -) -> Result>> { - let uri = format!("{api_url}/api/chat"); - let request = HttpRequest::builder() - .method(Method::POST) - .uri(uri) - .header("Content-Type", "application/json") - .when_some(api_key, |builder, api_key| { - builder.header("Authorization", format!("Bearer {api_key}")) - }) - .body(AsyncBody::from(serde_json::to_string(&request)?))?; - - let mut response = client.send(request).await?; - if response.status().is_success() { - let reader = BufReader::new(response.into_body()); - - Ok(reader - .lines() - .map(|line| match line { - Ok(line) => serde_json::from_str(&line).context("Unable to parse chat response"), - Err(e) => Err(e.into()), - }) - .boxed()) - } else { - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - anyhow::bail!( - "Failed to connect to Ollama API: {} {}", - response.status(), - body, - ); - } -} - -pub async fn get_models( - client: &dyn HttpClient, - api_url: &str, - api_key: Option<&str>, -) -> Result> { - let uri = format!("{api_url}/api/tags"); - let request = HttpRequest::builder() - .method(Method::GET) - .uri(uri) - .header("Accept", "application/json") - .when_some(api_key, |builder, api_key| { - builder.header("Authorization", format!("Bearer {api_key}")) - }) - .body(AsyncBody::default())?; - - let mut response = client.send(request).await?; - - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - - anyhow::ensure!( - response.status().is_success(), - "Failed to connect to Ollama API: {} {}", - response.status(), - body, - ); - let response: LocalModelsResponse = - serde_json::from_str(&body).context("Unable to parse Ollama tag listing")?; - Ok(response.models) -} - -/// Fetch details of a model, used to determine model capabilities -pub async fn show_model( - client: &dyn HttpClient, - api_url: &str, - api_key: Option<&str>, - model: &str, -) -> Result { - let uri = format!("{api_url}/api/show"); - let request = HttpRequest::builder() - .method(Method::POST) - .uri(uri) - .header("Content-Type", "application/json") - .when_some(api_key, |builder, api_key| { - builder.header("Authorization", format!("Bearer {api_key}")) - }) - .body(AsyncBody::from( - serde_json::json!({ "model": model }).to_string(), - ))?; - - let mut response = client.send(request).await?; - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - - anyhow::ensure!( - response.status().is_success(), - "Failed to connect to Ollama API: {} {}", - response.status(), - body, - ); - let details: ModelShow = serde_json::from_str(body.as_str())?; - Ok(details) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn parse_completion() { - let response = serde_json::json!({ - "model": "llama3.2", - "created_at": "2023-12-12T14:13:43.416799Z", - "message": { - "role": "assistant", - "content": "Hello! How are you today?" - }, - "done": true, - "total_duration": 5191566416u64, - "load_duration": 2154458, - "prompt_eval_count": 26, - "prompt_eval_duration": 383809000, - "eval_count": 298, - "eval_duration": 4799921000u64 - }); - let _: ChatResponseDelta = serde_json::from_value(response).unwrap(); - } - - #[test] - fn parse_streaming_completion() { - let partial = serde_json::json!({ - "model": "llama3.2", - "created_at": "2023-08-04T08:52:19.385406455-07:00", - "message": { - "role": "assistant", - "content": "The", - "images": null - }, - "done": false - }); - - let _: ChatResponseDelta = serde_json::from_value(partial).unwrap(); - - let last = serde_json::json!({ - "model": "llama3.2", - "created_at": "2023-08-04T19:22:45.499127Z", - "message": { - "role": "assistant", - "content": "" - }, - "done": true, - "total_duration": 4883583458u64, - "load_duration": 1334875, - "prompt_eval_count": 26, - "prompt_eval_duration": 342546000, - "eval_count": 282, - "eval_duration": 4535599000u64 - }); - - let _: ChatResponseDelta = serde_json::from_value(last).unwrap(); - } - - #[test] - fn parse_tool_call() { - let response = serde_json::json!({ - "model": "llama3.2:3b", - "created_at": "2025-04-28T20:02:02.140489Z", - "message": { - "role": "assistant", - "content": "", - "tool_calls": [ - { - "id": "call_llama3.2:3b_145155", - "function": { - "name": "weather", - "arguments": { - "city": "london", - } - } - } - ] - }, - "done_reason": "stop", - "done": true, - "total_duration": 2758629166u64, - "load_duration": 1770059875, - "prompt_eval_count": 147, - "prompt_eval_duration": 684637583, - "eval_count": 16, - "eval_duration": 302561917, - }); - - let result: ChatResponseDelta = serde_json::from_value(response).unwrap(); - match result.message { - ChatMessage::Assistant { - content, - tool_calls, - images: _, - thinking, - } => { - assert!(content.is_empty()); - assert!(tool_calls.is_some_and(|v| !v.is_empty())); - assert!(thinking.is_none()); - } - _ => panic!("Deserialized wrong role"), - } - } - - // Backwards compatibility with Ollama versions prior to v0.12.10 November 2025 - // This test is a copy of `parse_tool_call()` with the `id` field omitted. - #[test] - fn parse_tool_call_pre_0_12_10() { - let response = serde_json::json!({ - "model": "llama3.2:3b", - "created_at": "2025-04-28T20:02:02.140489Z", - "message": { - "role": "assistant", - "content": "", - "tool_calls": [ - { - "function": { - "name": "weather", - "arguments": { - "city": "london", - } - } - } - ] - }, - "done_reason": "stop", - "done": true, - "total_duration": 2758629166u64, - "load_duration": 1770059875, - "prompt_eval_count": 147, - "prompt_eval_duration": 684637583, - "eval_count": 16, - "eval_duration": 302561917, - }); - - let result: ChatResponseDelta = serde_json::from_value(response).unwrap(); - match result.message { - ChatMessage::Assistant { - content, - tool_calls: Some(tool_calls), - images: _, - thinking, - } => { - assert!(content.is_empty()); - assert!(thinking.is_none()); - - // When the `Option` around `id` is removed, this test should complain - // and be subsequently deleted in favor of `parse_tool_call()` - assert!(tool_calls.first().is_some_and(|call| call.id.is_none())) - } - _ => panic!("Deserialized wrong role"), - } - } - - #[test] - fn parse_show_model() { - let response = serde_json::json!({ - "license": "LLAMA 3.2 COMMUNITY LICENSE AGREEMENT...", - "details": { - "parent_model": "", - "format": "gguf", - "family": "llama", - "families": ["llama"], - "parameter_size": "3.2B", - "quantization_level": "Q4_K_M" - }, - "model_info": { - "general.architecture": "llama", - "general.basename": "Llama-3.2", - "general.file_type": 15, - "general.finetune": "Instruct", - "general.languages": ["en", "de", "fr", "it", "pt", "hi", "es", "th"], - "general.parameter_count": 3212749888u64, - "general.quantization_version": 2, - "general.size_label": "3B", - "general.tags": ["facebook", "meta", "pytorch", "llama", "llama-3", "text-generation"], - "general.type": "model", - "llama.attention.head_count": 24, - "llama.attention.head_count_kv": 8, - "llama.attention.key_length": 128, - "llama.attention.layer_norm_rms_epsilon": 0.00001, - "llama.attention.value_length": 128, - "llama.block_count": 28, - "llama.context_length": 131072, - "llama.embedding_length": 3072, - "llama.feed_forward_length": 8192, - "llama.rope.dimension_count": 128, - "llama.rope.freq_base": 500000, - "llama.vocab_size": 128256, - "tokenizer.ggml.bos_token_id": 128000, - "tokenizer.ggml.eos_token_id": 128009, - "tokenizer.ggml.merges": null, - "tokenizer.ggml.model": "gpt2", - "tokenizer.ggml.pre": "llama-bpe", - "tokenizer.ggml.token_type": null, - "tokenizer.ggml.tokens": null - }, - "tensors": [ - { "name": "rope_freqs.weight", "type": "F32", "shape": [64] }, - { "name": "token_embd.weight", "type": "Q4_K_S", "shape": [3072, 128256] } - ], - "capabilities": ["completion", "tools"], - "modified_at": "2025-04-29T21:24:41.445877632+03:00" - }); - - let result: ModelShow = serde_json::from_value(response).unwrap(); - assert!(result.supports_tools()); - assert!(result.capabilities.contains(&"tools".to_string())); - assert!(result.capabilities.contains(&"completion".to_string())); - - assert_eq!(result.architecture, Some("llama".to_string())); - assert_eq!(result.context_length, Some(131072)); - } - - #[test] - fn serialize_chat_request_with_images() { - let base64_image = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="; - - let request = ChatRequest { - model: "llava".to_string(), - messages: vec![ChatMessage::User { - content: "What do you see in this image?".to_string(), - images: Some(vec![base64_image.to_string()]), - }], - stream: false, - keep_alive: KeepAlive::default(), - options: None, - think: None, - tools: vec![], - }; - - let serialized = serde_json::to_string(&request).unwrap(); - assert!(serialized.contains("images")); - assert!(serialized.contains(base64_image)); - } - - #[test] - fn serialize_chat_request_without_images() { - let request = ChatRequest { - model: "llama3.2".to_string(), - messages: vec![ChatMessage::User { - content: "Hello, world!".to_string(), - images: None, - }], - stream: false, - keep_alive: KeepAlive::default(), - options: None, - think: None, - tools: vec![], - }; - - let serialized = serde_json::to_string(&request).unwrap(); - assert!(!serialized.contains("images")); - } - - #[test] - fn test_json_format_with_images() { - let base64_image = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg=="; - - let request = ChatRequest { - model: "llava".to_string(), - messages: vec![ChatMessage::User { - content: "What do you see?".to_string(), - images: Some(vec![base64_image.to_string()]), - }], - stream: false, - keep_alive: KeepAlive::default(), - options: None, - think: None, - tools: vec![], - }; - - let serialized = serde_json::to_string(&request).unwrap(); - - let parsed: serde_json::Value = serde_json::from_str(&serialized).unwrap(); - let message_images = parsed["messages"][0]["images"].as_array().unwrap(); - assert_eq!(message_images.len(), 1); - assert_eq!(message_images[0].as_str().unwrap(), base64_image); - } -} diff --git a/crates/onboarding/Cargo.toml b/crates/onboarding/Cargo.toml deleted file mode 100644 index 2ff3467c48..0000000000 --- a/crates/onboarding/Cargo.toml +++ /dev/null @@ -1,44 +0,0 @@ -[package] -name = "onboarding" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/onboarding.rs" - -[features] -default = [] - -[dependencies] -anyhow.workspace = true -client.workspace = true -component.workspace = true -db.workspace = true -documented.workspace = true -fs.workspace = true -fuzzy.workspace = true -git.workspace = true -gpui.workspace = true -menu.workspace = true -notifications.workspace = true -picker.workspace = true -project.workspace = true -schemars.workspace = true -serde.workspace = true -settings.workspace = true -telemetry.workspace = true -theme.workspace = true -ui.workspace = true -util.workspace = true -vim_mode_setting.workspace = true -workspace.workspace = true -zed_actions.workspace = true -zlog.workspace = true - -[dev-dependencies] -db = {workspace = true, features = ["test-support"]} diff --git a/crates/onboarding/LICENSE-GPL b/crates/onboarding/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/onboarding/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/onboarding/src/base_keymap_picker.rs b/crates/onboarding/src/base_keymap_picker.rs deleted file mode 100644 index 63a2894a93..0000000000 --- a/crates/onboarding/src/base_keymap_picker.rs +++ /dev/null @@ -1,229 +0,0 @@ -use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; -use gpui::{ - App, Context, DismissEvent, Entity, EventEmitter, Focusable, Render, Task, WeakEntity, Window, - actions, -}; -use picker::{Picker, PickerDelegate}; -use project::Fs; -use settings::{BaseKeymap, Settings, update_settings_file}; -use std::sync::Arc; -use ui::{ListItem, ListItemSpacing, prelude::*}; -use util::ResultExt; -use workspace::{ModalView, Workspace, ui::HighlightedLabel}; - -actions!( - zed, - [ - /// Toggles the base keymap selector modal. - ToggleBaseKeymapSelector - ] -); - -pub fn init(cx: &mut App) { - cx.observe_new(|workspace: &mut Workspace, _window, _cx| { - workspace.register_action(toggle); - }) - .detach(); -} - -pub fn toggle( - workspace: &mut Workspace, - _: &ToggleBaseKeymapSelector, - window: &mut Window, - cx: &mut Context, -) { - let fs = workspace.app_state().fs.clone(); - workspace.toggle_modal(window, cx, |window, cx| { - BaseKeymapSelector::new( - BaseKeymapSelectorDelegate::new(cx.entity().downgrade(), fs, cx), - window, - cx, - ) - }); -} - -pub struct BaseKeymapSelector { - picker: Entity>, -} - -impl Focusable for BaseKeymapSelector { - fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { - self.picker.focus_handle(cx) - } -} - -impl EventEmitter for BaseKeymapSelector {} -impl ModalView for BaseKeymapSelector {} - -impl BaseKeymapSelector { - pub fn new( - delegate: BaseKeymapSelectorDelegate, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); - Self { picker } - } -} - -impl Render for BaseKeymapSelector { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - v_flex().w(rems(34.)).child(self.picker.clone()) - } -} - -pub struct BaseKeymapSelectorDelegate { - selector: WeakEntity, - matches: Vec, - selected_index: usize, - fs: Arc, -} - -impl BaseKeymapSelectorDelegate { - fn new( - selector: WeakEntity, - fs: Arc, - cx: &mut Context, - ) -> Self { - let base = BaseKeymap::get(None, cx); - let selected_index = BaseKeymap::OPTIONS - .iter() - .position(|(_, value)| value == base) - .unwrap_or(0); - Self { - selector, - matches: Vec::new(), - selected_index, - fs, - } - } -} - -impl PickerDelegate for BaseKeymapSelectorDelegate { - type ListItem = ui::ListItem; - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - "Select a base keymap...".into() - } - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index( - &mut self, - ix: usize, - _window: &mut Window, - _: &mut Context>, - ) { - self.selected_index = ix; - } - - fn update_matches( - &mut self, - query: String, - window: &mut Window, - cx: &mut Context>, - ) -> Task<()> { - let background = cx.background_executor().clone(); - let candidates = BaseKeymap::names() - .enumerate() - .map(|(id, name)| StringMatchCandidate::new(id, name)) - .collect::>(); - - cx.spawn_in(window, async move |this, cx| { - let matches = if query.is_empty() { - candidates - .into_iter() - .enumerate() - .map(|(index, candidate)| StringMatch { - candidate_id: index, - string: candidate.string, - positions: Vec::new(), - score: 0.0, - }) - .collect() - } else { - match_strings( - &candidates, - &query, - false, - true, - 100, - &Default::default(), - background, - ) - .await - }; - - this.update(cx, |this, _| { - this.delegate.matches = matches; - this.delegate.selected_index = this - .delegate - .selected_index - .min(this.delegate.matches.len().saturating_sub(1)); - }) - .log_err(); - }) - } - - fn confirm( - &mut self, - _: bool, - _: &mut Window, - cx: &mut Context>, - ) { - if let Some(selection) = self.matches.get(self.selected_index) { - let base_keymap = BaseKeymap::from_names(&selection.string); - - telemetry::event!( - "Settings Changed", - setting = "keymap", - value = base_keymap.to_string() - ); - - update_settings_file(self.fs.clone(), cx, move |setting, _| { - setting.base_keymap = Some(base_keymap.into()) - }); - } - - self.selector - .update(cx, |_, cx| { - cx.emit(DismissEvent); - }) - .ok(); - } - - fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { - self.selector - .update(cx, |_, cx| { - cx.emit(DismissEvent); - }) - .log_err(); - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _window: &mut Window, - _cx: &mut Context>, - ) -> Option { - let keymap_match = &self.matches.get(ix)?; - - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .child(HighlightedLabel::new( - keymap_match.string.clone(), - keymap_match.positions.clone(), - )), - ) - } -} diff --git a/crates/onboarding/src/basics_page.rs b/crates/onboarding/src/basics_page.rs deleted file mode 100644 index ab5d578f7d..0000000000 --- a/crates/onboarding/src/basics_page.rs +++ /dev/null @@ -1,486 +0,0 @@ -use std::sync::Arc; - -use client::TelemetrySettings; -use fs::Fs; -use gpui::{Action, App, IntoElement}; -use settings::{BaseKeymap, Settings, update_settings_file}; -use theme::{ - Appearance, SystemAppearance, ThemeAppearanceMode, ThemeName, ThemeRegistry, ThemeSelection, - ThemeSettings, -}; -use ui::{ - Divider, ParentElement as _, StatefulInteractiveElement, SwitchField, TintColor, - ToggleButtonGroup, ToggleButtonGroupSize, ToggleButtonSimple, ToggleButtonWithIcon, prelude::*, - rems_from_px, -}; -use vim_mode_setting::VimModeSetting; - -use crate::{ - ImportCursorSettings, ImportVsCodeSettings, SettingsImportState, - theme_preview::{ThemePreviewStyle, ThemePreviewTile}, -}; - -const LIGHT_THEMES: [&str; 3] = ["One Light", "Ayu Light", "Gruvbox Light"]; -const DARK_THEMES: [&str; 3] = ["One Dark", "Ayu Dark", "Gruvbox Dark"]; -const FAMILY_NAMES: [SharedString; 3] = [ - SharedString::new_static("One"), - SharedString::new_static("Ayu"), - SharedString::new_static("Gruvbox"), -]; - -fn get_theme_family_themes(theme_name: &str) -> Option<(&'static str, &'static str)> { - for i in 0..LIGHT_THEMES.len() { - if LIGHT_THEMES[i] == theme_name || DARK_THEMES[i] == theme_name { - return Some((LIGHT_THEMES[i], DARK_THEMES[i])); - } - } - None -} - -fn render_theme_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement { - let theme_selection = ThemeSettings::get_global(cx).theme.clone(); - let system_appearance = theme::SystemAppearance::global(cx); - - let theme_mode = theme_selection - .mode() - .unwrap_or_else(|| match *system_appearance { - Appearance::Light => ThemeAppearanceMode::Light, - Appearance::Dark => ThemeAppearanceMode::Dark, - }); - - return v_flex() - .gap_2() - .child( - h_flex().justify_between().child(Label::new("Theme")).child( - ToggleButtonGroup::single_row( - "theme-selector-onboarding-dark-light", - [ - ThemeAppearanceMode::Light, - ThemeAppearanceMode::Dark, - ThemeAppearanceMode::System, - ] - .map(|mode| { - const MODE_NAMES: [SharedString; 3] = [ - SharedString::new_static("Light"), - SharedString::new_static("Dark"), - SharedString::new_static("System"), - ]; - ToggleButtonSimple::new( - MODE_NAMES[mode as usize].clone(), - move |_, _, cx| { - write_mode_change(mode, cx); - - telemetry::event!( - "Welcome Theme mode Changed", - from = theme_mode, - to = mode - ); - }, - ) - }), - ) - .size(ToggleButtonGroupSize::Medium) - .tab_index(tab_index) - .selected_index(theme_mode as usize) - .style(ui::ToggleButtonGroupStyle::Outlined) - .width(rems_from_px(3. * 64.)), - ), - ) - .child( - h_flex() - .gap_4() - .justify_between() - .children(render_theme_previews(tab_index, &theme_selection, cx)), - ); - - fn render_theme_previews( - tab_index: &mut isize, - theme_selection: &ThemeSelection, - cx: &mut App, - ) -> [impl IntoElement; 3] { - let system_appearance = SystemAppearance::global(cx); - let theme_registry = ThemeRegistry::global(cx); - - let theme_seed = 0xBEEF as f32; - let theme_mode = theme_selection - .mode() - .unwrap_or_else(|| match *system_appearance { - Appearance::Light => ThemeAppearanceMode::Light, - Appearance::Dark => ThemeAppearanceMode::Dark, - }); - let appearance = match theme_mode { - ThemeAppearanceMode::Light => Appearance::Light, - ThemeAppearanceMode::Dark => Appearance::Dark, - ThemeAppearanceMode::System => *system_appearance, - }; - let current_theme_name: SharedString = theme_selection.name(appearance).0.into(); - - let theme_names = match appearance { - Appearance::Light => LIGHT_THEMES, - Appearance::Dark => DARK_THEMES, - }; - - let themes = theme_names.map(|theme| theme_registry.get(theme).unwrap()); - - [0, 1, 2].map(|index| { - let theme = &themes[index]; - let is_selected = theme.name == current_theme_name; - let name = theme.name.clone(); - let colors = cx.theme().colors(); - - v_flex() - .w_full() - .items_center() - .gap_1() - .child( - h_flex() - .id(name) - .relative() - .w_full() - .border_2() - .border_color(colors.border_transparent) - .rounded(ThemePreviewTile::ROOT_RADIUS) - .map(|this| { - if is_selected { - this.border_color(colors.border_selected) - } else { - this.opacity(0.8).hover(|s| s.border_color(colors.border)) - } - }) - .tab_index({ - *tab_index += 1; - *tab_index - 1 - }) - .focus(|mut style| { - style.border_color = Some(colors.border_focused); - style - }) - .on_click({ - let theme_name = theme.name.clone(); - let current_theme_name = current_theme_name.clone(); - - move |_, _, cx| { - write_theme_change(theme_name.clone(), theme_mode, cx); - telemetry::event!( - "Welcome Theme Changed", - from = current_theme_name, - to = theme_name - ); - } - }) - .map(|this| { - if theme_mode == ThemeAppearanceMode::System { - let (light, dark) = ( - theme_registry.get(LIGHT_THEMES[index]).unwrap(), - theme_registry.get(DARK_THEMES[index]).unwrap(), - ); - this.child( - ThemePreviewTile::new(light, theme_seed) - .style(ThemePreviewStyle::SideBySide(dark)), - ) - } else { - this.child( - ThemePreviewTile::new(theme.clone(), theme_seed) - .style(ThemePreviewStyle::Bordered), - ) - } - }), - ) - .child( - Label::new(FAMILY_NAMES[index].clone()) - .color(Color::Muted) - .size(LabelSize::Small), - ) - }) - } - - fn write_mode_change(mode: ThemeAppearanceMode, cx: &mut App) { - let fs = ::global(cx); - update_settings_file(fs, cx, move |settings, _cx| { - theme::set_mode(settings, mode); - }); - } - - fn write_theme_change( - theme: impl Into>, - theme_mode: ThemeAppearanceMode, - cx: &mut App, - ) { - let fs = ::global(cx); - let theme = theme.into(); - update_settings_file(fs, cx, move |settings, cx| { - if theme_mode == ThemeAppearanceMode::System { - let (light_theme, dark_theme) = - get_theme_family_themes(&theme).unwrap_or((theme.as_ref(), theme.as_ref())); - - settings.theme.theme = Some(settings::ThemeSelection::Dynamic { - mode: ThemeAppearanceMode::System, - light: ThemeName(light_theme.into()), - dark: ThemeName(dark_theme.into()), - }); - } else { - let appearance = *SystemAppearance::global(cx); - theme::set_theme(settings, theme, appearance, appearance); - } - }); - } -} - -fn render_telemetry_section(tab_index: &mut isize, cx: &App) -> impl IntoElement { - let fs = ::global(cx); - - v_flex() - .gap_4() - .child( - SwitchField::new( - "onboarding-telemetry-metrics", - None::<&str>, - Some("Help improve Zed by sending anonymous usage data".into()), - if TelemetrySettings::get_global(cx).metrics { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }, - { - let fs = fs.clone(); - move |selection, _, cx| { - let enabled = match selection { - ToggleState::Selected => true, - ToggleState::Unselected => false, - ToggleState::Indeterminate => { - return; - } - }; - - update_settings_file(fs.clone(), cx, move |setting, _| { - setting.telemetry.get_or_insert_default().metrics = Some(enabled); - }); - - // This telemetry event shouldn't fire when it's off. If it does we'll be alerted - // and can fix it in a timely manner to respect a user's choice. - telemetry::event!( - "Welcome Page Telemetry Metrics Toggled", - options = if enabled { "on" } else { "off" } - ); - } - }, - ) - .tab_index({ - *tab_index += 1; - *tab_index - }), - ) - .child( - SwitchField::new( - "onboarding-telemetry-crash-reports", - None::<&str>, - Some( - "Help fix Zed by sending crash reports so we can fix critical issues fast" - .into(), - ), - if TelemetrySettings::get_global(cx).diagnostics { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }, - { - let fs = fs.clone(); - move |selection, _, cx| { - let enabled = match selection { - ToggleState::Selected => true, - ToggleState::Unselected => false, - ToggleState::Indeterminate => { - return; - } - }; - - update_settings_file(fs.clone(), cx, move |setting, _| { - setting.telemetry.get_or_insert_default().diagnostics = Some(enabled); - }); - - // This telemetry event shouldn't fire when it's off. If it does we'll be alerted - // and can fix it in a timely manner to respect a user's choice. - telemetry::event!( - "Welcome Page Telemetry Diagnostics Toggled", - options = if enabled { "on" } else { "off" } - ); - } - }, - ) - .tab_index({ - *tab_index += 1; - *tab_index - }), - ) -} - -fn render_base_keymap_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement { - let base_keymap = match BaseKeymap::get_global(cx) { - BaseKeymap::VSCode => Some(0), - BaseKeymap::JetBrains => Some(1), - BaseKeymap::SublimeText => Some(2), - BaseKeymap::Atom => Some(3), - BaseKeymap::Emacs => Some(4), - BaseKeymap::Cursor => Some(5), - BaseKeymap::TextMate | BaseKeymap::None => None, - }; - - return v_flex().gap_2().child(Label::new("Base Keymap")).child( - ToggleButtonGroup::two_rows( - "base_keymap_selection", - [ - ToggleButtonWithIcon::new("VS Code", IconName::EditorVsCode, |_, _, cx| { - write_keymap_base(BaseKeymap::VSCode, cx); - }), - ToggleButtonWithIcon::new("JetBrains", IconName::EditorJetBrains, |_, _, cx| { - write_keymap_base(BaseKeymap::JetBrains, cx); - }), - ToggleButtonWithIcon::new("Sublime Text", IconName::EditorSublime, |_, _, cx| { - write_keymap_base(BaseKeymap::SublimeText, cx); - }), - ], - [ - ToggleButtonWithIcon::new("Atom", IconName::EditorAtom, |_, _, cx| { - write_keymap_base(BaseKeymap::Atom, cx); - }), - ToggleButtonWithIcon::new("Emacs", IconName::EditorEmacs, |_, _, cx| { - write_keymap_base(BaseKeymap::Emacs, cx); - }), - ToggleButtonWithIcon::new("Cursor", IconName::EditorCursor, |_, _, cx| { - write_keymap_base(BaseKeymap::Cursor, cx); - }), - ], - ) - .when_some(base_keymap, |this, base_keymap| { - this.selected_index(base_keymap) - }) - .full_width() - .tab_index(tab_index) - .size(ui::ToggleButtonGroupSize::Medium) - .style(ui::ToggleButtonGroupStyle::Outlined), - ); - - fn write_keymap_base(keymap_base: BaseKeymap, cx: &App) { - let fs = ::global(cx); - - update_settings_file(fs, cx, move |setting, _| { - setting.base_keymap = Some(keymap_base.into()); - }); - - telemetry::event!("Welcome Keymap Changed", keymap = keymap_base); - } -} - -fn render_vim_mode_switch(tab_index: &mut isize, cx: &mut App) -> impl IntoElement { - let toggle_state = if VimModeSetting::get_global(cx).0 { - ui::ToggleState::Selected - } else { - ui::ToggleState::Unselected - }; - SwitchField::new( - "onboarding-vim-mode", - Some("Vim Mode"), - Some("Coming from Neovim? Use our first-class implementation of Vim Mode".into()), - toggle_state, - { - let fs = ::global(cx); - move |&selection, _, cx| { - let vim_mode = match selection { - ToggleState::Selected => true, - ToggleState::Unselected => false, - ToggleState::Indeterminate => { - return; - } - }; - update_settings_file(fs.clone(), cx, move |setting, _| { - setting.vim_mode = Some(vim_mode); - }); - - telemetry::event!( - "Welcome Vim Mode Toggled", - options = if vim_mode { "on" } else { "off" }, - ); - } - }, - ) - .tab_index({ - *tab_index += 1; - *tab_index - 1 - }) -} - -fn render_setting_import_button( - tab_index: isize, - label: SharedString, - action: &dyn Action, - imported: bool, -) -> impl IntoElement + 'static { - let action = action.boxed_clone(); - - Button::new(label.clone(), label.clone()) - .style(ButtonStyle::OutlinedGhost) - .size(ButtonSize::Medium) - .label_size(LabelSize::Small) - .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - .toggle_state(imported) - .tab_index(tab_index) - .when(imported, |this| { - this.icon(IconName::Check) - .icon_size(IconSize::Small) - .color(Color::Success) - }) - .on_click(move |_, window, cx| { - telemetry::event!("Welcome Import Settings", import_source = label,); - window.dispatch_action(action.boxed_clone(), cx); - }) -} - -fn render_import_settings_section(tab_index: &mut isize, cx: &mut App) -> impl IntoElement { - let import_state = SettingsImportState::global(cx); - let imports: [(SharedString, &dyn Action, bool); 2] = [ - ( - "VS Code".into(), - &ImportVsCodeSettings { skip_prompt: false }, - import_state.vscode, - ), - ( - "Cursor".into(), - &ImportCursorSettings { skip_prompt: false }, - import_state.cursor, - ), - ]; - - let [vscode, cursor] = imports.map(|(label, action, imported)| { - *tab_index += 1; - render_setting_import_button(*tab_index - 1, label, action, imported) - }); - - h_flex() - .gap_2() - .flex_wrap() - .justify_between() - .child( - v_flex() - .gap_0p5() - .max_w_5_6() - .child(Label::new("Import Settings")) - .child( - Label::new("Automatically pull your settings from other editors") - .color(Color::Muted), - ), - ) - .child(h_flex().gap_1().child(vscode).child(cursor)) -} - -pub(crate) fn render_basics_page(cx: &mut App) -> impl IntoElement { - let mut tab_index = 0; - v_flex() - .id("basics-page") - .gap_6() - .child(render_theme_section(&mut tab_index, cx)) - .child(render_base_keymap_section(&mut tab_index, cx)) - .child(render_import_settings_section(&mut tab_index, cx)) - .child(render_vim_mode_switch(&mut tab_index, cx)) - .child(Divider::horizontal().color(ui::DividerColor::BorderVariant)) - .child(render_telemetry_section(&mut tab_index, cx)) -} diff --git a/crates/onboarding/src/multibuffer_hint.rs b/crates/onboarding/src/multibuffer_hint.rs deleted file mode 100644 index 9d290306d8..0000000000 --- a/crates/onboarding/src/multibuffer_hint.rs +++ /dev/null @@ -1,184 +0,0 @@ -use std::collections::HashSet; -use std::sync::OnceLock; -use std::sync::atomic::{AtomicUsize, Ordering}; - -use db::kvp::KEY_VALUE_STORE; -use gpui::{App, EntityId, EventEmitter, Subscription}; -use ui::{IconButtonShape, Tooltip, prelude::*}; -use workspace::item::{ItemBufferKind, ItemEvent, ItemHandle}; -use workspace::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; - -pub struct MultibufferHint { - shown_on: HashSet, - active_item: Option>, - subscription: Option, -} - -const NUMBER_OF_HINTS: usize = 10; - -const SHOWN_COUNT_KEY: &str = "MULTIBUFFER_HINT_SHOWN_COUNT"; - -impl Default for MultibufferHint { - fn default() -> Self { - Self::new() - } -} - -impl MultibufferHint { - pub fn new() -> Self { - Self { - shown_on: Default::default(), - active_item: None, - subscription: None, - } - } -} - -impl MultibufferHint { - fn counter() -> &'static AtomicUsize { - static SHOWN_COUNT: OnceLock = OnceLock::new(); - SHOWN_COUNT.get_or_init(|| { - let value: usize = KEY_VALUE_STORE - .read_kvp(SHOWN_COUNT_KEY) - .ok() - .flatten() - .and_then(|v| v.parse().ok()) - .unwrap_or(0); - - AtomicUsize::new(value) - }) - } - - fn shown_count() -> usize { - Self::counter().load(Ordering::Relaxed) - } - - fn increment_count(cx: &mut App) { - Self::set_count(Self::shown_count() + 1, cx) - } - - pub(crate) fn set_count(count: usize, cx: &mut App) { - Self::counter().store(count, Ordering::Relaxed); - - db::write_and_log(cx, move || { - KEY_VALUE_STORE.write_kvp(SHOWN_COUNT_KEY.to_string(), format!("{}", count)) - }); - } - - fn dismiss(&mut self, cx: &mut App) { - Self::set_count(NUMBER_OF_HINTS, cx) - } - - /// Determines the toolbar location for this [`MultibufferHint`]. - fn determine_toolbar_location(&mut self, cx: &mut Context) -> ToolbarItemLocation { - if Self::shown_count() >= NUMBER_OF_HINTS { - return ToolbarItemLocation::Hidden; - } - - let Some(active_pane_item) = self.active_item.as_ref() else { - return ToolbarItemLocation::Hidden; - }; - - if active_pane_item.buffer_kind(cx) == ItemBufferKind::Singleton - || active_pane_item.breadcrumbs(cx.theme(), cx).is_none() - || !active_pane_item.can_save(cx) - { - return ToolbarItemLocation::Hidden; - } - - if self.shown_on.insert(active_pane_item.item_id()) { - Self::increment_count(cx); - } - - ToolbarItemLocation::Secondary - } -} - -impl EventEmitter for MultibufferHint {} - -impl ToolbarItemView for MultibufferHint { - fn set_active_pane_item( - &mut self, - active_pane_item: Option<&dyn ItemHandle>, - window: &mut Window, - cx: &mut Context, - ) -> ToolbarItemLocation { - cx.notify(); - self.active_item = active_pane_item.map(|item| item.boxed_clone()); - - let Some(active_pane_item) = active_pane_item else { - return ToolbarItemLocation::Hidden; - }; - - let this = cx.entity().downgrade(); - self.subscription = Some(active_pane_item.subscribe_to_item_events( - window, - cx, - Box::new(move |event, _, cx| { - if let ItemEvent::UpdateBreadcrumbs = event { - this.update(cx, |this, cx| { - cx.notify(); - let location = this.determine_toolbar_location(cx); - cx.emit(ToolbarItemEvent::ChangeLocation(location)) - }) - .ok(); - } - }), - )); - - self.determine_toolbar_location(cx) - } -} - -impl Render for MultibufferHint { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - h_flex() - .px_2() - .py_0p5() - .justify_between() - .bg(cx.theme().status().info_background.opacity(0.5)) - .border_1() - .border_color(cx.theme().colors().border_variant) - .rounded_sm() - .overflow_hidden() - .child( - h_flex() - .gap_0p5() - .child( - h_flex() - .gap_2() - .child( - Icon::new(IconName::Info) - .size(IconSize::XSmall) - .color(Color::Muted), - ) - .child(Label::new( - "Edit and save files directly in the results multibuffer!", - )), - ) - .child( - Button::new("open_docs", "Learn More") - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .icon_position(IconPosition::End) - .on_click(move |_event, _, cx| { - cx.open_url("https://zed.dev/docs/multibuffers") - }), - ), - ) - .child( - IconButton::new("dismiss", IconName::Close) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .on_click(cx.listener(|this, _event, _, cx| { - this.dismiss(cx); - cx.emit(ToolbarItemEvent::ChangeLocation( - ToolbarItemLocation::Hidden, - )) - })) - .tooltip(Tooltip::text("Dismiss Hint")), - ) - .into_any_element() - } -} diff --git a/crates/onboarding/src/onboarding.rs b/crates/onboarding/src/onboarding.rs deleted file mode 100644 index 94581e1423..0000000000 --- a/crates/onboarding/src/onboarding.rs +++ /dev/null @@ -1,676 +0,0 @@ -pub use crate::welcome::ShowWelcome; -use crate::{multibuffer_hint::MultibufferHint, welcome::WelcomePage}; -use client::{Client, UserStore, zed_urls}; -use db::kvp::KEY_VALUE_STORE; -use fs::Fs; -use gpui::{ - Action, AnyElement, App, AppContext, AsyncWindowContext, Context, Entity, EventEmitter, - FocusHandle, Focusable, Global, IntoElement, KeyContext, Render, ScrollHandle, SharedString, - Subscription, Task, WeakEntity, Window, actions, -}; -use notifications::status_toast::{StatusToast, ToastIcon}; -use schemars::JsonSchema; -use serde::Deserialize; -use settings::{SettingsStore, VsCodeSettingsSource}; -use std::sync::Arc; -use ui::{ - Divider, KeyBinding, ParentElement as _, StatefulInteractiveElement, Vector, VectorName, - WithScrollbar as _, prelude::*, rems_from_px, -}; -use workspace::{ - AppState, Workspace, WorkspaceId, - dock::DockPosition, - item::{Item, ItemEvent}, - notifications::NotifyResultExt as _, - open_new, register_serializable_item, with_active_or_new_workspace, -}; - -mod base_keymap_picker; -mod basics_page; -pub mod multibuffer_hint; -mod theme_preview; -mod welcome; - -/// Imports settings from Visual Studio Code. -#[derive(Copy, Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)] -#[action(namespace = zed)] -#[serde(deny_unknown_fields)] -pub struct ImportVsCodeSettings { - #[serde(default)] - pub skip_prompt: bool, -} - -/// Imports settings from Cursor editor. -#[derive(Copy, Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)] -#[action(namespace = zed)] -#[serde(deny_unknown_fields)] -pub struct ImportCursorSettings { - #[serde(default)] - pub skip_prompt: bool, -} - -pub const FIRST_OPEN: &str = "first_open"; -pub const DOCS_URL: &str = "https://zed.dev/docs/"; - -actions!( - zed, - [ - /// Opens the onboarding view. - OpenOnboarding - ] -); - -actions!( - onboarding, - [ - /// Finish the onboarding process. - Finish, - /// Sign in while in the onboarding flow. - SignIn, - /// Open the user account in zed.dev while in the onboarding flow. - OpenAccount, - /// Resets the welcome screen hints to their initial state. - ResetHints - ] -); - -pub fn init(cx: &mut App) { - cx.observe_new(|workspace: &mut Workspace, _, _cx| { - workspace - .register_action(|_workspace, _: &ResetHints, _, cx| MultibufferHint::set_count(0, cx)); - }) - .detach(); - - cx.on_action(|_: &OpenOnboarding, cx| { - with_active_or_new_workspace(cx, |workspace, window, cx| { - workspace - .with_local_workspace(window, cx, |workspace, window, cx| { - let existing = workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::()); - - if let Some(existing) = existing { - workspace.activate_item(&existing, true, true, window, cx); - } else { - let settings_page = Onboarding::new(workspace, cx); - workspace.add_item_to_active_pane( - Box::new(settings_page), - None, - true, - window, - cx, - ) - } - }) - .detach(); - }); - }); - - cx.on_action(|_: &ShowWelcome, cx| { - with_active_or_new_workspace(cx, |workspace, window, cx| { - workspace - .with_local_workspace(window, cx, |workspace, window, cx| { - let existing = workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::()); - - if let Some(existing) = existing { - workspace.activate_item(&existing, true, true, window, cx); - } else { - let settings_page = WelcomePage::new(window, cx); - workspace.add_item_to_active_pane( - Box::new(settings_page), - None, - true, - window, - cx, - ) - } - }) - .detach(); - }); - }); - - cx.observe_new(|workspace: &mut Workspace, _window, _cx| { - workspace.register_action(|_workspace, action: &ImportVsCodeSettings, window, cx| { - let fs = ::global(cx); - let action = *action; - - let workspace = cx.weak_entity(); - - window - .spawn(cx, async move |cx: &mut AsyncWindowContext| { - handle_import_vscode_settings( - workspace, - VsCodeSettingsSource::VsCode, - action.skip_prompt, - fs, - cx, - ) - .await - }) - .detach(); - }); - - workspace.register_action(|_workspace, action: &ImportCursorSettings, window, cx| { - let fs = ::global(cx); - let action = *action; - - let workspace = cx.weak_entity(); - - window - .spawn(cx, async move |cx: &mut AsyncWindowContext| { - handle_import_vscode_settings( - workspace, - VsCodeSettingsSource::Cursor, - action.skip_prompt, - fs, - cx, - ) - .await - }) - .detach(); - }); - }) - .detach(); - - base_keymap_picker::init(cx); - - register_serializable_item::(cx); - register_serializable_item::(cx); -} - -pub fn show_onboarding_view(app_state: Arc, cx: &mut App) -> Task> { - telemetry::event!("Onboarding Page Opened"); - open_new( - Default::default(), - app_state, - cx, - |workspace, window, cx| { - { - workspace.toggle_dock(DockPosition::Left, window, cx); - let onboarding_page = Onboarding::new(workspace, cx); - workspace.add_item_to_center(Box::new(onboarding_page.clone()), window, cx); - - window.focus(&onboarding_page.focus_handle(cx)); - - cx.notify(); - }; - db::write_and_log(cx, || { - KEY_VALUE_STORE.write_kvp(FIRST_OPEN.to_string(), "false".to_string()) - }); - }, - ) -} - -struct Onboarding { - workspace: WeakEntity, - focus_handle: FocusHandle, - user_store: Entity, - scroll_handle: ScrollHandle, - _settings_subscription: Subscription, -} - -impl Onboarding { - fn new(workspace: &Workspace, cx: &mut App) -> Entity { - let font_family_cache = theme::FontFamilyCache::global(cx); - - cx.new(|cx| { - cx.spawn(async move |this, cx| { - font_family_cache.prefetch(cx).await; - this.update(cx, |_, cx| { - cx.notify(); - }) - }) - .detach(); - - Self { - workspace: workspace.weak_handle(), - focus_handle: cx.focus_handle(), - scroll_handle: ScrollHandle::new(), - user_store: workspace.user_store().clone(), - _settings_subscription: cx - .observe_global::(move |_, cx| cx.notify()), - } - }) - } - - fn on_finish(_: &Finish, _: &mut Window, cx: &mut App) { - telemetry::event!("Finish Setup"); - go_to_welcome_page(cx); - } - - fn handle_sign_in(_: &SignIn, window: &mut Window, cx: &mut App) { - let client = Client::global(cx); - - window - .spawn(cx, async move |cx| { - client - .sign_in_with_optional_connect(true, cx) - .await - .notify_async_err(cx); - }) - .detach(); - } - - fn handle_open_account(_: &OpenAccount, _: &mut Window, cx: &mut App) { - cx.open_url(&zed_urls::account_url(cx)) - } - - fn render_page(&mut self, cx: &mut Context) -> AnyElement { - crate::basics_page::render_basics_page(cx).into_any_element() - } -} - -impl Render for Onboarding { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - div() - .image_cache(gpui::retain_all("onboarding-page")) - .key_context({ - let mut ctx = KeyContext::new_with_defaults(); - ctx.add("Onboarding"); - ctx.add("menu"); - ctx - }) - .track_focus(&self.focus_handle) - .size_full() - .bg(cx.theme().colors().editor_background) - .on_action(Self::on_finish) - .on_action(Self::handle_sign_in) - .on_action(Self::handle_open_account) - .on_action(cx.listener(|_, _: &menu::SelectNext, window, cx| { - window.focus_next(); - cx.notify(); - })) - .on_action(cx.listener(|_, _: &menu::SelectPrevious, window, cx| { - window.focus_prev(); - cx.notify(); - })) - .child( - div() - .max_w(Rems(48.0)) - .size_full() - .mx_auto() - .child( - v_flex() - .id("page-content") - .m_auto() - .p_12() - .size_full() - .max_w_full() - .min_w_0() - .gap_6() - .overflow_y_scroll() - .child( - h_flex() - .w_full() - .gap_4() - .justify_between() - .child( - h_flex() - .gap_4() - .child(Vector::square(VectorName::ZedLogo, rems(2.5))) - .child( - v_flex() - .child( - Headline::new("Welcome to Zed") - .size(HeadlineSize::Small), - ) - .child( - Label::new("The editor for what's next") - .color(Color::Muted) - .size(LabelSize::Small) - .italic(), - ), - ), - ) - .child({ - Button::new("finish_setup", "Finish Setup") - .style(ButtonStyle::Filled) - .size(ButtonSize::Medium) - .width(Rems(12.0)) - .key_binding( - KeyBinding::for_action_in( - &Finish, - &self.focus_handle, - cx, - ) - .size(rems_from_px(12.)), - ) - .on_click(|_, window, cx| { - window.dispatch_action(Finish.boxed_clone(), cx); - }) - }), - ) - .child(Divider::horizontal().color(ui::DividerColor::BorderVariant)) - .child(self.render_page(cx)) - .track_scroll(&self.scroll_handle), - ) - .vertical_scrollbar_for(&self.scroll_handle, window, cx), - ) - } -} - -impl EventEmitter for Onboarding {} - -impl Focusable for Onboarding { - fn focus_handle(&self, _: &App) -> gpui::FocusHandle { - self.focus_handle.clone() - } -} - -impl Item for Onboarding { - type Event = ItemEvent; - - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { - "Onboarding".into() - } - - fn telemetry_event_text(&self) -> Option<&'static str> { - Some("Onboarding Page Opened") - } - - fn show_toolbar(&self) -> bool { - false - } - - fn can_split(&self) -> bool { - true - } - - fn clone_on_split( - &self, - _workspace_id: Option, - _: &mut Window, - cx: &mut Context, - ) -> Task>> { - Task::ready(Some(cx.new(|cx| Onboarding { - workspace: self.workspace.clone(), - user_store: self.user_store.clone(), - scroll_handle: ScrollHandle::new(), - focus_handle: cx.focus_handle(), - _settings_subscription: cx.observe_global::(move |_, cx| cx.notify()), - }))) - } - - fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) { - f(*event) - } -} - -fn go_to_welcome_page(cx: &mut App) { - with_active_or_new_workspace(cx, |workspace, window, cx| { - let Some((onboarding_id, onboarding_idx)) = workspace - .active_pane() - .read(cx) - .items() - .enumerate() - .find_map(|(idx, item)| { - let _ = item.downcast::()?; - Some((item.item_id(), idx)) - }) - else { - return; - }; - - workspace.active_pane().update(cx, |pane, cx| { - // Get the index here to get around the borrow checker - let idx = pane.items().enumerate().find_map(|(idx, item)| { - let _ = item.downcast::()?; - Some(idx) - }); - - if let Some(idx) = idx { - pane.activate_item(idx, true, true, window, cx); - } else { - let item = Box::new(WelcomePage::new(window, cx)); - pane.add_item(item, true, true, Some(onboarding_idx), window, cx); - } - - pane.remove_item(onboarding_id, false, false, window, cx); - }); - }); -} - -pub async fn handle_import_vscode_settings( - workspace: WeakEntity, - source: VsCodeSettingsSource, - skip_prompt: bool, - fs: Arc, - cx: &mut AsyncWindowContext, -) { - use util::truncate_and_remove_front; - - let vscode_settings = - match settings::VsCodeSettings::load_user_settings(source, fs.clone()).await { - Ok(vscode_settings) => vscode_settings, - Err(err) => { - zlog::error!("{err:?}"); - let _ = cx.prompt( - gpui::PromptLevel::Info, - &format!("Could not find or load a {source} settings file"), - None, - &["Ok"], - ); - return; - } - }; - - if !skip_prompt { - let prompt = cx.prompt( - gpui::PromptLevel::Warning, - &format!( - "Importing {} settings may overwrite your existing settings. \ - Will import settings from {}", - vscode_settings.source, - truncate_and_remove_front(&vscode_settings.path.to_string_lossy(), 128), - ), - None, - &["Ok", "Cancel"], - ); - let result = cx.spawn(async move |_| prompt.await.ok()).await; - if result != Some(0) { - return; - } - }; - - let Ok(result_channel) = cx.update(|_, cx| { - let source = vscode_settings.source; - let path = vscode_settings.path.clone(); - let result_channel = cx - .global::() - .import_vscode_settings(fs, vscode_settings); - zlog::info!("Imported {source} settings from {}", path.display()); - result_channel - }) else { - return; - }; - - let result = result_channel.await; - workspace - .update_in(cx, |workspace, _, cx| match result { - Ok(_) => { - let confirmation_toast = StatusToast::new( - format!("Your {} settings were successfully imported.", source), - cx, - |this, _| { - this.icon(ToastIcon::new(IconName::Check).color(Color::Success)) - .dismiss_button(true) - }, - ); - SettingsImportState::update(cx, |state, _| match source { - VsCodeSettingsSource::VsCode => { - state.vscode = true; - } - VsCodeSettingsSource::Cursor => { - state.cursor = true; - } - }); - workspace.toggle_status_toast(confirmation_toast, cx); - } - Err(_) => { - let error_toast = StatusToast::new( - "Failed to import settings. See log for details", - cx, - |this, _| { - this.icon(ToastIcon::new(IconName::Close).color(Color::Error)) - .action("Open Log", |window, cx| { - window.dispatch_action(workspace::OpenLog.boxed_clone(), cx) - }) - .dismiss_button(true) - }, - ); - workspace.toggle_status_toast(error_toast, cx); - } - }) - .ok(); -} - -#[derive(Default, Copy, Clone)] -pub struct SettingsImportState { - pub cursor: bool, - pub vscode: bool, -} - -impl Global for SettingsImportState {} - -impl SettingsImportState { - pub fn global(cx: &App) -> Self { - cx.try_global().cloned().unwrap_or_default() - } - pub fn update(cx: &mut App, f: impl FnOnce(&mut Self, &mut App) -> R) -> R { - cx.update_default_global(f) - } -} - -impl workspace::SerializableItem for Onboarding { - fn serialized_item_kind() -> &'static str { - "OnboardingPage" - } - - fn cleanup( - workspace_id: workspace::WorkspaceId, - alive_items: Vec, - _window: &mut Window, - cx: &mut App, - ) -> gpui::Task> { - workspace::delete_unloaded_items( - alive_items, - workspace_id, - "onboarding_pages", - &persistence::ONBOARDING_PAGES, - cx, - ) - } - - fn deserialize( - _project: Entity, - workspace: WeakEntity, - workspace_id: workspace::WorkspaceId, - item_id: workspace::ItemId, - window: &mut Window, - cx: &mut App, - ) -> gpui::Task>> { - window.spawn(cx, async move |cx| { - if let Some(_) = - persistence::ONBOARDING_PAGES.get_onboarding_page(item_id, workspace_id)? - { - workspace.update(cx, |workspace, cx| Onboarding::new(workspace, cx)) - } else { - Err(anyhow::anyhow!("No onboarding page to deserialize")) - } - }) - } - - fn serialize( - &mut self, - workspace: &mut Workspace, - item_id: workspace::ItemId, - _closing: bool, - _window: &mut Window, - cx: &mut ui::Context, - ) -> Option>> { - let workspace_id = workspace.database_id()?; - - Some(cx.background_spawn(async move { - persistence::ONBOARDING_PAGES - .save_onboarding_page(item_id, workspace_id) - .await - })) - } - - fn should_serialize(&self, event: &Self::Event) -> bool { - event == &ItemEvent::UpdateTab - } -} - -mod persistence { - use db::{ - query, - sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, - }; - use workspace::WorkspaceDb; - - pub struct OnboardingPagesDb(ThreadSafeConnection); - - impl Domain for OnboardingPagesDb { - const NAME: &str = stringify!(OnboardingPagesDb); - - const MIGRATIONS: &[&str] = &[ - sql!( - CREATE TABLE onboarding_pages ( - workspace_id INTEGER, - item_id INTEGER UNIQUE, - page_number INTEGER, - - PRIMARY KEY(workspace_id, item_id), - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ) STRICT; - ), - sql!( - CREATE TABLE onboarding_pages_2 ( - workspace_id INTEGER, - item_id INTEGER UNIQUE, - - PRIMARY KEY(workspace_id, item_id), - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ) STRICT; - INSERT INTO onboarding_pages_2 SELECT workspace_id, item_id FROM onboarding_pages; - DROP TABLE onboarding_pages; - ALTER TABLE onboarding_pages_2 RENAME TO onboarding_pages; - ), - ]; - } - - db::static_connection!(ONBOARDING_PAGES, OnboardingPagesDb, [WorkspaceDb]); - - impl OnboardingPagesDb { - query! { - pub async fn save_onboarding_page( - item_id: workspace::ItemId, - workspace_id: workspace::WorkspaceId - ) -> Result<()> { - INSERT OR REPLACE INTO onboarding_pages(item_id, workspace_id) - VALUES (?, ?) - } - } - - query! { - pub fn get_onboarding_page( - item_id: workspace::ItemId, - workspace_id: workspace::WorkspaceId - ) -> Result> { - SELECT item_id - FROM onboarding_pages - WHERE item_id = ? AND workspace_id = ? - } - } - } -} diff --git a/crates/onboarding/src/theme_preview.rs b/crates/onboarding/src/theme_preview.rs deleted file mode 100644 index 8bd65d8a27..0000000000 --- a/crates/onboarding/src/theme_preview.rs +++ /dev/null @@ -1,381 +0,0 @@ -#![allow(unused, dead_code)] -use gpui::{Hsla, Length}; -use std::{ - cell::LazyCell, - sync::{Arc, LazyLock, OnceLock}, -}; -use theme::{Theme, ThemeColors, ThemeRegistry}; -use ui::{ - IntoElement, RenderOnce, component_prelude::Documented, prelude::*, utils::inner_corner_radius, -}; - -#[derive(Clone, PartialEq)] -pub enum ThemePreviewStyle { - Bordered, - Borderless, - SideBySide(Arc), -} - -/// Shows a preview of a theme as an abstract illustration -/// of a thumbnail-sized editor. -#[derive(IntoElement, RegisterComponent, Documented)] -pub struct ThemePreviewTile { - theme: Arc, - seed: f32, - style: ThemePreviewStyle, -} - -static CHILD_RADIUS: LazyLock = LazyLock::new(|| { - inner_corner_radius( - ThemePreviewTile::ROOT_RADIUS, - ThemePreviewTile::ROOT_BORDER, - ThemePreviewTile::ROOT_PADDING, - ThemePreviewTile::CHILD_BORDER, - ) -}); - -impl ThemePreviewTile { - pub const SKELETON_HEIGHT_DEFAULT: Pixels = px(2.); - pub const SIDEBAR_SKELETON_ITEM_COUNT: usize = 8; - pub const SIDEBAR_WIDTH_DEFAULT: DefiniteLength = relative(0.25); - pub const ROOT_RADIUS: Pixels = px(8.0); - pub const ROOT_BORDER: Pixels = px(2.0); - pub const ROOT_PADDING: Pixels = px(2.0); - pub const CHILD_BORDER: Pixels = px(1.0); - - pub fn new(theme: Arc, seed: f32) -> Self { - Self { - theme, - seed, - style: ThemePreviewStyle::Bordered, - } - } - - pub fn style(mut self, style: ThemePreviewStyle) -> Self { - self.style = style; - self - } - - pub fn item_skeleton(w: Length, h: Length, bg: Hsla) -> impl IntoElement { - div().w(w).h(h).rounded_full().bg(bg) - } - - pub fn render_sidebar_skeleton_items( - seed: f32, - colors: &ThemeColors, - skeleton_height: impl Into + Clone, - ) -> [impl IntoElement; Self::SIDEBAR_SKELETON_ITEM_COUNT] { - let skeleton_height = skeleton_height.into(); - std::array::from_fn(|index| { - let width = { - let value = (seed * 1000.0 + index as f32 * 10.0).sin() * 0.5 + 0.5; - 0.5 + value * 0.45 - }; - Self::item_skeleton( - relative(width).into(), - skeleton_height, - colors.text.alpha(0.45), - ) - }) - } - - pub fn render_pseudo_code_skeleton( - seed: f32, - theme: Arc, - skeleton_height: impl Into, - ) -> impl IntoElement { - let colors = theme.colors(); - let syntax = theme.syntax(); - - let keyword_color = syntax.get("keyword").color; - let function_color = syntax.get("function").color; - let string_color = syntax.get("string").color; - let comment_color = syntax.get("comment").color; - let variable_color = syntax.get("variable").color; - let type_color = syntax.get("type").color; - let punctuation_color = syntax.get("punctuation").color; - - let syntax_colors = [ - keyword_color, - function_color, - string_color, - variable_color, - type_color, - punctuation_color, - comment_color, - ]; - - let skeleton_height = skeleton_height.into(); - - let line_width = |line_idx: usize, block_idx: usize| -> f32 { - let val = - (seed * 100.0 + line_idx as f32 * 20.0 + block_idx as f32 * 5.0).sin() * 0.5 + 0.5; - 0.05 + val * 0.2 - }; - - let indentation = |line_idx: usize| -> f32 { - let step = line_idx % 6; - if step < 3 { - step as f32 * 0.1 - } else { - (5 - step) as f32 * 0.1 - } - }; - - let pick_color = |line_idx: usize, block_idx: usize| -> Hsla { - let idx = ((seed * 10.0 + line_idx as f32 * 7.0 + block_idx as f32 * 3.0).sin() * 3.5) - .abs() as usize - % syntax_colors.len(); - syntax_colors[idx].unwrap_or(colors.text) - }; - - let line_count = 13; - - let lines = (0..line_count) - .map(|line_idx| { - let block_count = (((seed * 30.0 + line_idx as f32 * 12.0).sin() * 0.5 + 0.5) * 3.0) - .round() as usize - + 2; - - let indent = indentation(line_idx); - - let blocks = (0..block_count) - .map(|block_idx| { - let width = line_width(line_idx, block_idx); - let color = pick_color(line_idx, block_idx); - Self::item_skeleton(relative(width).into(), skeleton_height, color) - }) - .collect::>(); - - h_flex().gap(px(2.)).ml(relative(indent)).children(blocks) - }) - .collect::>(); - - v_flex().size_full().p_1().gap_1p5().children(lines) - } - - pub fn render_sidebar( - seed: f32, - colors: &ThemeColors, - width: impl Into + Clone, - skeleton_height: impl Into, - ) -> impl IntoElement { - div() - .h_full() - .w(width) - .border_r(px(1.)) - .border_color(colors.border_transparent) - .bg(colors.panel_background) - .child(v_flex().p_2().size_full().gap_1().children( - Self::render_sidebar_skeleton_items(seed, colors, skeleton_height.into()), - )) - } - - pub fn render_pane( - seed: f32, - theme: Arc, - skeleton_height: impl Into, - ) -> impl IntoElement { - v_flex().h_full().flex_grow().child( - div() - .size_full() - .overflow_hidden() - .bg(theme.colors().editor_background) - .p_2() - .child(Self::render_pseudo_code_skeleton( - seed, - theme, - skeleton_height.into(), - )), - ) - } - - pub fn render_editor( - seed: f32, - theme: Arc, - sidebar_width: impl Into + Clone, - skeleton_height: impl Into + Clone, - ) -> impl IntoElement { - div() - .size_full() - .flex() - .bg(theme.colors().background.alpha(1.00)) - .child(Self::render_sidebar( - seed, - theme.colors(), - sidebar_width, - skeleton_height.clone(), - )) - .child(Self::render_pane(seed, theme, skeleton_height)) - } - - fn render_borderless(seed: f32, theme: Arc) -> impl IntoElement { - Self::render_editor( - seed, - theme, - Self::SIDEBAR_WIDTH_DEFAULT, - Self::SKELETON_HEIGHT_DEFAULT, - ) - } - - fn render_border(seed: f32, theme: Arc) -> impl IntoElement { - div() - .size_full() - .p(Self::ROOT_PADDING) - .rounded(Self::ROOT_RADIUS) - .child( - div() - .size_full() - .rounded(*CHILD_RADIUS) - .border(Self::CHILD_BORDER) - .border_color(theme.colors().border) - .child(Self::render_editor( - seed, - theme.clone(), - Self::SIDEBAR_WIDTH_DEFAULT, - Self::SKELETON_HEIGHT_DEFAULT, - )), - ) - } - - fn render_side_by_side( - seed: f32, - theme: Arc, - other_theme: Arc, - border_color: Hsla, - ) -> impl IntoElement { - let sidebar_width = relative(0.20); - - div() - .size_full() - .p(Self::ROOT_PADDING) - .rounded(Self::ROOT_RADIUS) - .child( - h_flex() - .size_full() - .relative() - .rounded(*CHILD_RADIUS) - .border(Self::CHILD_BORDER) - .border_color(border_color) - .overflow_hidden() - .child(div().size_full().child(Self::render_editor( - seed, - theme, - sidebar_width, - Self::SKELETON_HEIGHT_DEFAULT, - ))) - .child( - div() - .size_full() - .absolute() - .left_1_2() - .bg(other_theme.colors().editor_background) - .child(Self::render_editor( - seed, - other_theme, - sidebar_width, - Self::SKELETON_HEIGHT_DEFAULT, - )), - ), - ) - .into_any_element() - } -} - -impl RenderOnce for ThemePreviewTile { - fn render(self, _window: &mut ui::Window, _cx: &mut ui::App) -> impl IntoElement { - match self.style { - ThemePreviewStyle::Bordered => { - Self::render_border(self.seed, self.theme).into_any_element() - } - ThemePreviewStyle::Borderless => { - Self::render_borderless(self.seed, self.theme).into_any_element() - } - ThemePreviewStyle::SideBySide(other_theme) => Self::render_side_by_side( - self.seed, - self.theme, - other_theme, - _cx.theme().colors().border, - ) - .into_any_element(), - } - } -} - -impl Component for ThemePreviewTile { - fn scope() -> ComponentScope { - ComponentScope::Onboarding - } - - fn name() -> &'static str { - "Theme Preview Tile" - } - - fn sort_name() -> &'static str { - "Theme Preview Tile" - } - - fn description() -> Option<&'static str> { - Some(Self::DOCS) - } - - fn preview(_window: &mut Window, cx: &mut App) -> Option { - let theme_registry = ThemeRegistry::global(cx); - - let one_dark = theme_registry.get("One Dark"); - let one_light = theme_registry.get("One Light"); - let gruvbox_dark = theme_registry.get("Gruvbox Dark"); - let gruvbox_light = theme_registry.get("Gruvbox Light"); - - let themes_to_preview = vec![ - one_dark.clone().ok(), - one_light.ok(), - gruvbox_dark.ok(), - gruvbox_light.ok(), - ] - .into_iter() - .flatten() - .collect::>(); - - Some( - v_flex() - .gap_6() - .p_4() - .children({ - if let Some(one_dark) = one_dark.ok() { - vec![example_group(vec![single_example( - "Default", - div() - .w(px(240.)) - .h(px(180.)) - .child(ThemePreviewTile::new(one_dark, 0.42)) - .into_any_element(), - )])] - } else { - vec![] - } - }) - .child( - example_group(vec![single_example( - "Default Themes", - h_flex() - .gap_4() - .children( - themes_to_preview - .into_iter() - .map(|theme| { - div() - .w(px(200.)) - .h(px(140.)) - .child(ThemePreviewTile::new(theme, 0.42)) - }) - .collect::>(), - ) - .into_any_element(), - )]) - .grow(), - ) - .into_any_element(), - ) - } -} diff --git a/crates/onboarding/src/welcome.rs b/crates/onboarding/src/welcome.rs deleted file mode 100644 index b2711cd52d..0000000000 --- a/crates/onboarding/src/welcome.rs +++ /dev/null @@ -1,443 +0,0 @@ -use gpui::{ - Action, App, Context, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, - ParentElement, Render, Styled, Task, Window, actions, -}; -use menu::{SelectNext, SelectPrevious}; -use ui::{ButtonLike, Divider, DividerColor, KeyBinding, Vector, VectorName, prelude::*}; -use workspace::{ - NewFile, Open, - item::{Item, ItemEvent}, - with_active_or_new_workspace, -}; -use zed_actions::{Extensions, OpenSettings, agent, command_palette}; - -use crate::{Onboarding, OpenOnboarding}; - -actions!( - zed, - [ - /// Show the Zed welcome screen - ShowWelcome - ] -); - -const CONTENT: (Section<4>, Section<3>) = ( - Section { - title: "Get Started", - entries: [ - SectionEntry { - icon: IconName::Plus, - title: "New File", - action: &NewFile, - }, - SectionEntry { - icon: IconName::FolderOpen, - title: "Open Project", - action: &Open, - }, - SectionEntry { - icon: IconName::CloudDownload, - title: "Clone Repository", - action: &git::Clone, - }, - SectionEntry { - icon: IconName::ListCollapse, - title: "Open Command Palette", - action: &command_palette::Toggle, - }, - ], - }, - Section { - title: "Configure", - entries: [ - SectionEntry { - icon: IconName::Settings, - title: "Open Settings", - action: &OpenSettings, - }, - SectionEntry { - icon: IconName::ZedAssistant, - title: "View AI Settings", - action: &agent::OpenSettings, - }, - SectionEntry { - icon: IconName::Blocks, - title: "Explore Extensions", - action: &Extensions { - category_filter: None, - id: None, - }, - }, - ], - }, -); - -struct Section { - title: &'static str, - entries: [SectionEntry; COLS], -} - -impl Section { - fn render(self, index_offset: usize, focus: &FocusHandle, cx: &mut App) -> impl IntoElement { - v_flex() - .min_w_full() - .child( - h_flex() - .px_1() - .mb_2() - .gap_2() - .child( - Label::new(self.title.to_ascii_uppercase()) - .buffer_font(cx) - .color(Color::Muted) - .size(LabelSize::XSmall), - ) - .child(Divider::horizontal().color(DividerColor::BorderVariant)), - ) - .children( - self.entries - .iter() - .enumerate() - .map(|(index, entry)| entry.render(index_offset + index, focus, cx)), - ) - } -} - -struct SectionEntry { - icon: IconName, - title: &'static str, - action: &'static dyn Action, -} - -impl SectionEntry { - fn render(&self, button_index: usize, focus: &FocusHandle, cx: &App) -> impl IntoElement { - ButtonLike::new(("onboarding-button-id", button_index)) - .tab_index(button_index as isize) - .full_width() - .size(ButtonSize::Medium) - .child( - h_flex() - .w_full() - .justify_between() - .child( - h_flex() - .gap_2() - .child( - Icon::new(self.icon) - .color(Color::Muted) - .size(IconSize::XSmall), - ) - .child(Label::new(self.title)), - ) - .child( - KeyBinding::for_action_in(self.action, focus, cx).size(rems_from_px(12.)), - ), - ) - .on_click(|_, window, cx| window.dispatch_action(self.action.boxed_clone(), cx)) - } -} - -pub struct WelcomePage { - focus_handle: FocusHandle, -} - -impl WelcomePage { - fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context) { - window.focus_next(); - cx.notify(); - } - - fn select_previous(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context) { - window.focus_prev(); - cx.notify(); - } -} - -impl Render for WelcomePage { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let (first_section, second_section) = CONTENT; - let first_section_entries = first_section.entries.len(); - let last_index = first_section_entries + second_section.entries.len(); - - h_flex() - .size_full() - .justify_center() - .overflow_hidden() - .bg(cx.theme().colors().editor_background) - .key_context("Welcome") - .track_focus(&self.focus_handle(cx)) - .on_action(cx.listener(Self::select_previous)) - .on_action(cx.listener(Self::select_next)) - .child( - h_flex() - .px_12() - .py_40() - .size_full() - .relative() - .max_w(px(1100.)) - .child( - div() - .size_full() - .max_w_128() - .mx_auto() - .child( - h_flex() - .w_full() - .justify_center() - .gap_4() - .child(Vector::square(VectorName::ZedLogo, rems(2.))) - .child( - div().child(Headline::new("Welcome to Zed")).child( - Label::new("The editor for what's next") - .size(LabelSize::Small) - .color(Color::Muted) - .italic(), - ), - ), - ) - .child( - v_flex() - .mt_10() - .gap_6() - .child(first_section.render( - Default::default(), - &self.focus_handle, - cx, - )) - .child(second_section.render( - first_section_entries, - &self.focus_handle, - cx, - )) - .child( - h_flex() - .w_full() - .pt_4() - .justify_center() - // We call this a hack - .rounded_b_xs() - .border_t_1() - .border_color(cx.theme().colors().border.opacity(0.6)) - .border_dashed() - .child( - Button::new("welcome-exit", "Return to Setup") - .tab_index(last_index as isize) - .full_width() - .label_size(LabelSize::XSmall) - .on_click(|_, window, cx| { - window.dispatch_action( - OpenOnboarding.boxed_clone(), - cx, - ); - - with_active_or_new_workspace(cx, |workspace, window, cx| { - let Some((welcome_id, welcome_idx)) = workspace - .active_pane() - .read(cx) - .items() - .enumerate() - .find_map(|(idx, item)| { - let _ = item.downcast::()?; - Some((item.item_id(), idx)) - }) - else { - return; - }; - - workspace.active_pane().update(cx, |pane, cx| { - // Get the index here to get around the borrow checker - let idx = pane.items().enumerate().find_map( - |(idx, item)| { - let _ = - item.downcast::()?; - Some(idx) - }, - ); - - if let Some(idx) = idx { - pane.activate_item( - idx, true, true, window, cx, - ); - } else { - let item = - Box::new(Onboarding::new(workspace, cx)); - pane.add_item( - item, - true, - true, - Some(welcome_idx), - window, - cx, - ); - } - - pane.remove_item( - welcome_id, - false, - false, - window, - cx, - ); - }); - }); - }), - ), - ), - ), - ), - ) - } -} - -impl WelcomePage { - pub fn new(window: &mut Window, cx: &mut App) -> Entity { - cx.new(|cx| { - let focus_handle = cx.focus_handle(); - cx.on_focus(&focus_handle, window, |_, _, cx| cx.notify()) - .detach(); - - WelcomePage { focus_handle } - }) - } -} - -impl EventEmitter for WelcomePage {} - -impl Focusable for WelcomePage { - fn focus_handle(&self, _: &App) -> gpui::FocusHandle { - self.focus_handle.clone() - } -} - -impl Item for WelcomePage { - type Event = ItemEvent; - - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { - "Welcome".into() - } - - fn telemetry_event_text(&self) -> Option<&'static str> { - Some("New Welcome Page Opened") - } - - fn show_toolbar(&self) -> bool { - false - } - - fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) { - f(*event) - } -} - -impl workspace::SerializableItem for WelcomePage { - fn serialized_item_kind() -> &'static str { - "WelcomePage" - } - - fn cleanup( - workspace_id: workspace::WorkspaceId, - alive_items: Vec, - _window: &mut Window, - cx: &mut App, - ) -> Task> { - workspace::delete_unloaded_items( - alive_items, - workspace_id, - "welcome_pages", - &persistence::WELCOME_PAGES, - cx, - ) - } - - fn deserialize( - _project: Entity, - _workspace: gpui::WeakEntity, - workspace_id: workspace::WorkspaceId, - item_id: workspace::ItemId, - window: &mut Window, - cx: &mut App, - ) -> Task>> { - if persistence::WELCOME_PAGES - .get_welcome_page(item_id, workspace_id) - .ok() - .is_some_and(|is_open| is_open) - { - window.spawn(cx, async move |cx| cx.update(WelcomePage::new)) - } else { - Task::ready(Err(anyhow::anyhow!("No welcome page to deserialize"))) - } - } - - fn serialize( - &mut self, - workspace: &mut workspace::Workspace, - item_id: workspace::ItemId, - _closing: bool, - _window: &mut Window, - cx: &mut Context, - ) -> Option>> { - let workspace_id = workspace.database_id()?; - Some(cx.background_spawn(async move { - persistence::WELCOME_PAGES - .save_welcome_page(item_id, workspace_id, true) - .await - })) - } - - fn should_serialize(&self, event: &Self::Event) -> bool { - event == &ItemEvent::UpdateTab - } -} - -mod persistence { - use db::{ - query, - sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, - }; - use workspace::WorkspaceDb; - - pub struct WelcomePagesDb(ThreadSafeConnection); - - impl Domain for WelcomePagesDb { - const NAME: &str = stringify!(WelcomePagesDb); - - const MIGRATIONS: &[&str] = (&[sql!( - CREATE TABLE welcome_pages ( - workspace_id INTEGER, - item_id INTEGER UNIQUE, - is_open INTEGER DEFAULT FALSE, - - PRIMARY KEY(workspace_id, item_id), - 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( - item_id: workspace::ItemId, - workspace_id: workspace::WorkspaceId, - is_open: bool - ) -> Result<()> { - INSERT OR REPLACE INTO welcome_pages(item_id, workspace_id, is_open) - VALUES (?, ?, ?) - } - } - - query! { - pub fn get_welcome_page( - item_id: workspace::ItemId, - workspace_id: workspace::WorkspaceId - ) -> Result { - SELECT is_open - FROM welcome_pages - WHERE item_id = ? AND workspace_id = ? - } - } - } -} diff --git a/crates/open_ai/Cargo.toml b/crates/open_ai/Cargo.toml deleted file mode 100644 index 037ca14437..0000000000 --- a/crates/open_ai/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "open_ai" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/open_ai.rs" - -[features] -default = [] -schemars = ["dep:schemars"] - -[dependencies] -anyhow.workspace = true -futures.workspace = true -http_client.workspace = true -schemars = { workspace = true, optional = true } -log.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -strum.workspace = true -thiserror.workspace = true diff --git a/crates/open_ai/LICENSE-GPL b/crates/open_ai/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/open_ai/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/open_ai/src/open_ai.rs b/crates/open_ai/src/open_ai.rs deleted file mode 100644 index d8b4722543..0000000000 --- a/crates/open_ai/src/open_ai.rs +++ /dev/null @@ -1,626 +0,0 @@ -use anyhow::{Context as _, Result, anyhow}; -use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream}; -use http_client::{ - AsyncBody, HttpClient, Method, Request as HttpRequest, StatusCode, - http::{HeaderMap, HeaderValue}, -}; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -pub use settings::OpenAiReasoningEffort as ReasoningEffort; -use std::{convert::TryFrom, future::Future}; -use strum::EnumIter; -use thiserror::Error; - -pub const OPEN_AI_API_URL: &str = "https://api.openai.com/v1"; - -fn is_none_or_empty, U>(opt: &Option) -> bool { - opt.as_ref().is_none_or(|v| v.as_ref().is_empty()) -} - -#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum Role { - User, - Assistant, - System, - Tool, -} - -impl TryFrom for Role { - type Error = anyhow::Error; - - fn try_from(value: String) -> Result { - match value.as_str() { - "user" => Ok(Self::User), - "assistant" => Ok(Self::Assistant), - "system" => Ok(Self::System), - "tool" => Ok(Self::Tool), - _ => anyhow::bail!("invalid role '{value}'"), - } - } -} - -impl From for String { - fn from(val: Role) -> Self { - match val { - Role::User => "user".to_owned(), - Role::Assistant => "assistant".to_owned(), - Role::System => "system".to_owned(), - Role::Tool => "tool".to_owned(), - } - } -} - -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)] -pub enum Model { - #[serde(rename = "gpt-3.5-turbo")] - ThreePointFiveTurbo, - #[serde(rename = "gpt-4")] - Four, - #[serde(rename = "gpt-4-turbo")] - FourTurbo, - #[serde(rename = "gpt-4o")] - #[default] - FourOmni, - #[serde(rename = "gpt-4o-mini")] - FourOmniMini, - #[serde(rename = "gpt-4.1")] - FourPointOne, - #[serde(rename = "gpt-4.1-mini")] - FourPointOneMini, - #[serde(rename = "gpt-4.1-nano")] - FourPointOneNano, - #[serde(rename = "o1")] - O1, - #[serde(rename = "o3-mini")] - O3Mini, - #[serde(rename = "o3")] - O3, - #[serde(rename = "o4-mini")] - O4Mini, - #[serde(rename = "gpt-5")] - Five, - #[serde(rename = "gpt-5-mini")] - FiveMini, - #[serde(rename = "gpt-5-nano")] - FiveNano, - #[serde(rename = "gpt-5.1")] - FivePointOne, - #[serde(rename = "gpt-5.2")] - FivePointTwo, - #[serde(rename = "custom")] - Custom { - name: String, - /// The name displayed in the UI, such as in the assistant panel model dropdown menu. - display_name: Option, - max_tokens: u64, - max_output_tokens: Option, - max_completion_tokens: Option, - reasoning_effort: Option, - }, -} - -impl Model { - pub fn default_fast() -> Self { - // TODO: Replace with FiveMini since all other models are deprecated - Self::FourPointOneMini - } - - pub fn from_id(id: &str) -> Result { - match id { - "gpt-3.5-turbo" => Ok(Self::ThreePointFiveTurbo), - "gpt-4" => Ok(Self::Four), - "gpt-4-turbo-preview" => Ok(Self::FourTurbo), - "gpt-4o" => Ok(Self::FourOmni), - "gpt-4o-mini" => Ok(Self::FourOmniMini), - "gpt-4.1" => Ok(Self::FourPointOne), - "gpt-4.1-mini" => Ok(Self::FourPointOneMini), - "gpt-4.1-nano" => Ok(Self::FourPointOneNano), - "o1" => Ok(Self::O1), - "o3-mini" => Ok(Self::O3Mini), - "o3" => Ok(Self::O3), - "o4-mini" => Ok(Self::O4Mini), - "gpt-5" => Ok(Self::Five), - "gpt-5-mini" => Ok(Self::FiveMini), - "gpt-5-nano" => Ok(Self::FiveNano), - "gpt-5.1" => Ok(Self::FivePointOne), - "gpt-5.2" => Ok(Self::FivePointTwo), - invalid_id => anyhow::bail!("invalid model id '{invalid_id}'"), - } - } - - pub fn id(&self) -> &str { - match self { - Self::ThreePointFiveTurbo => "gpt-3.5-turbo", - Self::Four => "gpt-4", - Self::FourTurbo => "gpt-4-turbo", - Self::FourOmni => "gpt-4o", - Self::FourOmniMini => "gpt-4o-mini", - Self::FourPointOne => "gpt-4.1", - Self::FourPointOneMini => "gpt-4.1-mini", - Self::FourPointOneNano => "gpt-4.1-nano", - Self::O1 => "o1", - Self::O3Mini => "o3-mini", - Self::O3 => "o3", - Self::O4Mini => "o4-mini", - Self::Five => "gpt-5", - Self::FiveMini => "gpt-5-mini", - Self::FiveNano => "gpt-5-nano", - Self::FivePointOne => "gpt-5.1", - Self::FivePointTwo => "gpt-5.2", - Self::Custom { name, .. } => name, - } - } - - pub fn display_name(&self) -> &str { - match self { - Self::ThreePointFiveTurbo => "gpt-3.5-turbo", - Self::Four => "gpt-4", - Self::FourTurbo => "gpt-4-turbo", - Self::FourOmni => "gpt-4o", - Self::FourOmniMini => "gpt-4o-mini", - Self::FourPointOne => "gpt-4.1", - Self::FourPointOneMini => "gpt-4.1-mini", - Self::FourPointOneNano => "gpt-4.1-nano", - Self::O1 => "o1", - Self::O3Mini => "o3-mini", - Self::O3 => "o3", - Self::O4Mini => "o4-mini", - Self::Five => "gpt-5", - Self::FiveMini => "gpt-5-mini", - Self::FiveNano => "gpt-5-nano", - Self::FivePointOne => "gpt-5.1", - Self::FivePointTwo => "gpt-5.2", - Self::Custom { - name, display_name, .. - } => display_name.as_ref().unwrap_or(name), - } - } - - pub fn max_token_count(&self) -> u64 { - match self { - Self::ThreePointFiveTurbo => 16_385, - Self::Four => 8_192, - Self::FourTurbo => 128_000, - Self::FourOmni => 128_000, - Self::FourOmniMini => 128_000, - Self::FourPointOne => 1_047_576, - Self::FourPointOneMini => 1_047_576, - Self::FourPointOneNano => 1_047_576, - Self::O1 => 200_000, - Self::O3Mini => 200_000, - Self::O3 => 200_000, - Self::O4Mini => 200_000, - Self::Five => 272_000, - Self::FiveMini => 272_000, - Self::FiveNano => 272_000, - Self::FivePointOne => 400_000, - Self::FivePointTwo => 400_000, - Self::Custom { max_tokens, .. } => *max_tokens, - } - } - - pub fn max_output_tokens(&self) -> Option { - match self { - Self::Custom { - max_output_tokens, .. - } => *max_output_tokens, - Self::ThreePointFiveTurbo => Some(4_096), - Self::Four => Some(8_192), - Self::FourTurbo => Some(4_096), - Self::FourOmni => Some(16_384), - Self::FourOmniMini => Some(16_384), - Self::FourPointOne => Some(32_768), - Self::FourPointOneMini => Some(32_768), - Self::FourPointOneNano => Some(32_768), - Self::O1 => Some(100_000), - Self::O3Mini => Some(100_000), - Self::O3 => Some(100_000), - Self::O4Mini => Some(100_000), - Self::Five => Some(128_000), - Self::FiveMini => Some(128_000), - Self::FiveNano => Some(128_000), - Self::FivePointOne => Some(128_000), - Self::FivePointTwo => Some(128_000), - } - } - - pub fn reasoning_effort(&self) -> Option { - match self { - Self::Custom { - reasoning_effort, .. - } => reasoning_effort.to_owned(), - _ => None, - } - } - - /// Returns whether the given model supports the `parallel_tool_calls` parameter. - /// - /// If the model does not support the parameter, do not pass it up, or the API will return an error. - pub fn supports_parallel_tool_calls(&self) -> bool { - match self { - Self::ThreePointFiveTurbo - | Self::Four - | Self::FourTurbo - | Self::FourOmni - | Self::FourOmniMini - | Self::FourPointOne - | Self::FourPointOneMini - | Self::FourPointOneNano - | Self::Five - | Self::FiveMini - | Self::FivePointOne - | Self::FivePointTwo - | Self::FiveNano => true, - Self::O1 | Self::O3 | Self::O3Mini | Self::O4Mini | Model::Custom { .. } => false, - } - } - - /// Returns whether the given model supports the `prompt_cache_key` parameter. - /// - /// If the model does not support the parameter, do not pass it up. - pub fn supports_prompt_cache_key(&self) -> bool { - true - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct Request { - pub model: String, - pub messages: Vec, - pub stream: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub max_completion_tokens: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub stop: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub temperature: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub tool_choice: Option, - /// Whether to enable parallel function calling during tool use. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub parallel_tool_calls: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub tools: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub prompt_cache_key: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub reasoning_effort: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum ToolChoice { - Auto, - Required, - None, - #[serde(untagged)] - Other(ToolDefinition), -} - -#[derive(Clone, Deserialize, Serialize, Debug)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum ToolDefinition { - #[allow(dead_code)] - Function { function: FunctionDefinition }, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct FunctionDefinition { - pub name: String, - pub description: Option, - pub parameters: Option, -} - -#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] -#[serde(tag = "role", rename_all = "lowercase")] -pub enum RequestMessage { - Assistant { - content: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - tool_calls: Vec, - }, - User { - content: MessageContent, - }, - System { - content: MessageContent, - }, - Tool { - content: MessageContent, - tool_call_id: String, - }, -} - -#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] -#[serde(untagged)] -pub enum MessageContent { - Plain(String), - Multipart(Vec), -} - -impl MessageContent { - pub fn empty() -> Self { - MessageContent::Multipart(vec![]) - } - - pub fn push_part(&mut self, part: MessagePart) { - match self { - MessageContent::Plain(text) => { - *self = - MessageContent::Multipart(vec![MessagePart::Text { text: text.clone() }, part]); - } - MessageContent::Multipart(parts) if parts.is_empty() => match part { - MessagePart::Text { text } => *self = MessageContent::Plain(text), - MessagePart::Image { .. } => *self = MessageContent::Multipart(vec![part]), - }, - MessageContent::Multipart(parts) => parts.push(part), - } - } -} - -impl From> for MessageContent { - fn from(mut parts: Vec) -> Self { - if let [MessagePart::Text { text }] = parts.as_mut_slice() { - MessageContent::Plain(std::mem::take(text)) - } else { - MessageContent::Multipart(parts) - } - } -} - -#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] -#[serde(tag = "type")] -pub enum MessagePart { - #[serde(rename = "text")] - Text { text: String }, - #[serde(rename = "image_url")] - Image { image_url: ImageUrl }, -} - -#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] -pub struct ImageUrl { - pub url: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub detail: Option, -} - -#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct ToolCall { - pub id: String, - #[serde(flatten)] - pub content: ToolCallContent, -} - -#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] -#[serde(tag = "type", rename_all = "lowercase")] -pub enum ToolCallContent { - Function { function: FunctionContent }, -} - -#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct FunctionContent { - pub name: String, - pub arguments: String, -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct Response { - pub id: String, - pub object: String, - pub created: u64, - pub model: String, - pub choices: Vec, - pub usage: Usage, -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct Choice { - pub index: u32, - pub message: RequestMessage, - pub finish_reason: Option, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct ResponseMessageDelta { - pub role: Option, - pub content: Option, - #[serde(default, skip_serializing_if = "is_none_or_empty")] - pub tool_calls: Option>, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct ToolCallChunk { - pub index: usize, - pub id: Option, - - // There is also an optional `type` field that would determine if a - // function is there. Sometimes this streams in with the `function` before - // it streams in the `type` - pub function: Option, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct FunctionChunk { - pub name: Option, - pub arguments: Option, -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct Usage { - pub prompt_tokens: u64, - pub completion_tokens: u64, - pub total_tokens: u64, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct ChoiceDelta { - pub index: u32, - pub delta: Option, - pub finish_reason: Option, -} - -#[derive(Error, Debug)] -pub enum RequestError { - #[error("HTTP response error from {provider}'s API: status {status_code} - {body:?}")] - HttpResponseError { - provider: String, - status_code: StatusCode, - body: String, - headers: HeaderMap, - }, - #[error(transparent)] - Other(#[from] anyhow::Error), -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct ResponseStreamError { - message: String, -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(untagged)] -pub enum ResponseStreamResult { - Ok(ResponseStreamEvent), - Err { error: ResponseStreamError }, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct ResponseStreamEvent { - pub choices: Vec, - pub usage: Option, -} - -pub async fn stream_completion( - client: &dyn HttpClient, - provider_name: &str, - api_url: &str, - api_key: &str, - request: Request, -) -> Result>, RequestError> { - let uri = format!("{api_url}/chat/completions"); - let request_builder = HttpRequest::builder() - .method(Method::POST) - .uri(uri) - .header("Content-Type", "application/json") - .header("Authorization", format!("Bearer {}", api_key.trim())); - - let request = request_builder - .body(AsyncBody::from( - serde_json::to_string(&request).map_err(|e| RequestError::Other(e.into()))?, - )) - .map_err(|e| RequestError::Other(e.into()))?; - - let mut response = client.send(request).await?; - if response.status().is_success() { - let reader = BufReader::new(response.into_body()); - Ok(reader - .lines() - .filter_map(|line| async move { - match line { - Ok(line) => { - let line = line.strip_prefix("data: ").or_else(|| line.strip_prefix("data:"))?; - if line == "[DONE]" { - None - } else { - match serde_json::from_str(line) { - Ok(ResponseStreamResult::Ok(response)) => Some(Ok(response)), - Ok(ResponseStreamResult::Err { error }) => { - Some(Err(anyhow!(error.message))) - } - Err(error) => { - log::error!( - "Failed to parse OpenAI response into ResponseStreamResult: `{}`\n\ - Response: `{}`", - error, - line, - ); - Some(Err(anyhow!(error))) - } - } - } - } - Err(error) => Some(Err(anyhow!(error))), - } - }) - .boxed()) - } else { - let mut body = String::new(); - response - .body_mut() - .read_to_string(&mut body) - .await - .map_err(|e| RequestError::Other(e.into()))?; - - Err(RequestError::HttpResponseError { - provider: provider_name.to_owned(), - status_code: response.status(), - body, - headers: response.headers().clone(), - }) - } -} - -#[derive(Copy, Clone, Serialize, Deserialize)] -pub enum OpenAiEmbeddingModel { - #[serde(rename = "text-embedding-3-small")] - TextEmbedding3Small, - #[serde(rename = "text-embedding-3-large")] - TextEmbedding3Large, -} - -#[derive(Serialize)] -struct OpenAiEmbeddingRequest<'a> { - model: OpenAiEmbeddingModel, - input: Vec<&'a str>, -} - -#[derive(Deserialize)] -pub struct OpenAiEmbeddingResponse { - pub data: Vec, -} - -#[derive(Deserialize)] -pub struct OpenAiEmbedding { - pub embedding: Vec, -} - -pub fn embed<'a>( - client: &dyn HttpClient, - api_url: &str, - api_key: &str, - model: OpenAiEmbeddingModel, - texts: impl IntoIterator, -) -> impl 'static + Future> { - let uri = format!("{api_url}/embeddings"); - - let request = OpenAiEmbeddingRequest { - model, - input: texts.into_iter().collect(), - }; - let body = AsyncBody::from(serde_json::to_string(&request).unwrap()); - let request = HttpRequest::builder() - .method(Method::POST) - .uri(uri) - .header("Content-Type", "application/json") - .header("Authorization", format!("Bearer {}", api_key.trim())) - .body(body) - .map(|request| client.send(request)); - - async move { - let mut response = request?.await?; - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - - anyhow::ensure!( - response.status().is_success(), - "error during embedding, status: {:?}, body: {:?}", - response.status(), - body - ); - let response: OpenAiEmbeddingResponse = - serde_json::from_str(&body).context("failed to parse OpenAI embedding response")?; - Ok(response) - } -} diff --git a/crates/open_router/Cargo.toml b/crates/open_router/Cargo.toml deleted file mode 100644 index cccb92c33b..0000000000 --- a/crates/open_router/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "open_router" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/open_router.rs" - -[features] -default = [] -schemars = ["dep:schemars"] - -[dependencies] -anyhow.workspace = true -futures.workspace = true -http_client.workspace = true -schemars = { workspace = true, optional = true } -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -strum.workspace = true -thiserror.workspace = true diff --git a/crates/open_router/LICENSE-GPL b/crates/open_router/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/open_router/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/open_router/src/open_router.rs b/crates/open_router/src/open_router.rs deleted file mode 100644 index 57ff9558c2..0000000000 --- a/crates/open_router/src/open_router.rs +++ /dev/null @@ -1,750 +0,0 @@ -use anyhow::{Result, anyhow}; -use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream}; -use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest, http}; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -pub use settings::DataCollection; -pub use settings::ModelMode; -pub use settings::OpenRouterAvailableModel as AvailableModel; -pub use settings::OpenRouterProvider as Provider; -use std::{convert::TryFrom, io, time::Duration}; -use strum::EnumString; -use thiserror::Error; - -pub const OPEN_ROUTER_API_URL: &str = "https://openrouter.ai/api/v1"; - -fn extract_retry_after(headers: &http::HeaderMap) -> Option { - if let Some(reset) = headers.get("X-RateLimit-Reset") { - if let Ok(s) = reset.to_str() { - if let Ok(epoch_ms) = s.parse::() { - let now = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as u64; - if epoch_ms > now { - return Some(std::time::Duration::from_millis(epoch_ms - now)); - } - } - } - } - None -} - -fn is_none_or_empty, U>(opt: &Option) -> bool { - opt.as_ref().is_none_or(|v| v.as_ref().is_empty()) -} - -#[derive(Clone, Copy, Serialize, Deserialize, Debug, Eq, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum Role { - User, - Assistant, - System, - Tool, -} - -impl TryFrom for Role { - type Error = anyhow::Error; - - fn try_from(value: String) -> Result { - match value.as_str() { - "user" => Ok(Self::User), - "assistant" => Ok(Self::Assistant), - "system" => Ok(Self::System), - "tool" => Ok(Self::Tool), - _ => Err(anyhow!("invalid role '{value}'")), - } - } -} - -impl From for String { - fn from(val: Role) -> Self { - match val { - Role::User => "user".to_owned(), - Role::Assistant => "assistant".to_owned(), - Role::System => "system".to_owned(), - Role::Tool => "tool".to_owned(), - } - } -} - -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)] -pub struct Model { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub supports_tools: Option, - pub supports_images: Option, - #[serde(default)] - pub mode: ModelMode, - pub provider: Option, -} - -impl Model { - pub fn default_fast() -> Self { - Self::new( - "openrouter/auto", - Some("Auto Router"), - Some(2000000), - Some(true), - Some(false), - Some(ModelMode::Default), - None, - ) - } - - pub fn default() -> Self { - Self::default_fast() - } - - pub fn new( - name: &str, - display_name: Option<&str>, - max_tokens: Option, - supports_tools: Option, - supports_images: Option, - mode: Option, - provider: Option, - ) -> Self { - Self { - name: name.to_owned(), - display_name: display_name.map(|s| s.to_owned()), - max_tokens: max_tokens.unwrap_or(2000000), - supports_tools, - supports_images, - mode: mode.unwrap_or(ModelMode::Default), - provider, - } - } - - pub fn id(&self) -> &str { - &self.name - } - - pub fn display_name(&self) -> &str { - self.display_name.as_ref().unwrap_or(&self.name) - } - - pub fn max_token_count(&self) -> u64 { - self.max_tokens - } - - pub fn max_output_tokens(&self) -> Option { - None - } - - pub fn supports_tool_calls(&self) -> bool { - self.supports_tools.unwrap_or(false) - } - - pub fn supports_parallel_tool_calls(&self) -> bool { - false - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct Request { - pub model: String, - pub messages: Vec, - pub stream: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub max_tokens: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub stop: Vec, - pub temperature: f32, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub tool_choice: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub parallel_tool_calls: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub tools: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub reasoning: Option, - pub usage: RequestUsage, - pub provider: Option, -} - -#[derive(Debug, Default, Serialize, Deserialize)] -pub struct RequestUsage { - pub include: bool, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum ToolChoice { - Auto, - Required, - None, - #[serde(untagged)] - Other(ToolDefinition), -} - -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Clone, Deserialize, Serialize, Debug)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum ToolDefinition { - #[allow(dead_code)] - Function { function: FunctionDefinition }, -} - -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct FunctionDefinition { - pub name: String, - pub description: Option, - pub parameters: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct Reasoning { - #[serde(skip_serializing_if = "Option::is_none")] - pub effort: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub max_tokens: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub exclude: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub enabled: Option, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -#[serde(tag = "role", rename_all = "lowercase")] -pub enum RequestMessage { - Assistant { - content: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - tool_calls: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - reasoning_details: Option, - }, - User { - content: MessageContent, - }, - System { - content: MessageContent, - }, - Tool { - content: MessageContent, - tool_call_id: String, - }, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -#[serde(untagged)] -pub enum MessageContent { - Plain(String), - Multipart(Vec), -} - -impl MessageContent { - pub fn empty() -> Self { - Self::Plain(String::new()) - } - - pub fn push_part(&mut self, part: MessagePart) { - match self { - Self::Plain(text) if text.is_empty() => { - *self = Self::Multipart(vec![part]); - } - Self::Plain(text) => { - let text_part = MessagePart::Text { - text: std::mem::take(text), - }; - *self = Self::Multipart(vec![text_part, part]); - } - Self::Multipart(parts) => parts.push(part), - } - } -} - -impl From> for MessageContent { - fn from(parts: Vec) -> Self { - if parts.len() == 1 - && let MessagePart::Text { text } = &parts[0] - { - return Self::Plain(text.clone()); - } - Self::Multipart(parts) - } -} - -impl From for MessageContent { - fn from(text: String) -> Self { - Self::Plain(text) - } -} - -impl From<&str> for MessageContent { - fn from(text: &str) -> Self { - Self::Plain(text.to_string()) - } -} - -impl MessageContent { - pub fn as_text(&self) -> Option<&str> { - match self { - Self::Plain(text) => Some(text), - Self::Multipart(parts) if parts.len() == 1 => { - if let MessagePart::Text { text } = &parts[0] { - Some(text) - } else { - None - } - } - _ => None, - } - } - - pub fn to_text(&self) -> String { - match self { - Self::Plain(text) => text.clone(), - Self::Multipart(parts) => parts - .iter() - .filter_map(|part| { - if let MessagePart::Text { text } = part { - Some(text.as_str()) - } else { - None - } - }) - .collect::>() - .join(""), - } - } -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum MessagePart { - Text { - text: String, - }, - #[serde(rename = "image_url")] - Image { - image_url: String, - }, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct ToolCall { - pub id: String, - #[serde(flatten)] - pub content: ToolCallContent, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -#[serde(tag = "type", rename_all = "lowercase")] -pub enum ToolCallContent { - Function { function: FunctionContent }, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct FunctionContent { - pub name: String, - pub arguments: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub thought_signature: Option, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct ResponseMessageDelta { - pub role: Option, - pub content: Option, - pub reasoning: Option, - #[serde(default, skip_serializing_if = "is_none_or_empty")] - pub tool_calls: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub reasoning_details: Option, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct ToolCallChunk { - pub index: usize, - pub id: Option, - pub function: Option, -} - -#[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] -pub struct FunctionChunk { - pub name: Option, - pub arguments: Option, - #[serde(default)] - pub thought_signature: Option, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct Usage { - pub prompt_tokens: u64, - pub completion_tokens: u64, - pub total_tokens: u64, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct ChoiceDelta { - pub index: u32, - pub delta: ResponseMessageDelta, - pub finish_reason: Option, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct ResponseStreamEvent { - #[serde(default, skip_serializing_if = "Option::is_none")] - pub id: Option, - pub created: u32, - pub model: String, - pub choices: Vec, - pub usage: Option, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct Response { - pub id: String, - pub object: String, - pub created: u64, - pub model: String, - pub choices: Vec, - pub usage: Usage, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct Choice { - pub index: u32, - pub message: RequestMessage, - pub finish_reason: Option, -} - -#[derive(Default, Debug, Clone, PartialEq, Deserialize)] -pub struct ListModelsResponse { - pub data: Vec, -} - -#[derive(Default, Debug, Clone, PartialEq, Deserialize)] -pub struct ModelEntry { - pub id: String, - pub name: String, - pub created: usize, - pub description: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub context_length: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub supported_parameters: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub architecture: Option, -} - -#[derive(Default, Debug, Clone, PartialEq, Deserialize)] -pub struct ModelArchitecture { - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub input_modalities: Vec, -} - -pub async fn stream_completion( - client: &dyn HttpClient, - api_url: &str, - api_key: &str, - request: Request, -) -> Result>, OpenRouterError> { - let uri = format!("{api_url}/chat/completions"); - let request_builder = HttpRequest::builder() - .method(Method::POST) - .uri(uri) - .header("Content-Type", "application/json") - .header("Authorization", format!("Bearer {}", api_key)) - .header("HTTP-Referer", "https://zed.dev") - .header("X-Title", "Zed Editor"); - - let request = request_builder - .body(AsyncBody::from( - serde_json::to_string(&request).map_err(OpenRouterError::SerializeRequest)?, - )) - .map_err(OpenRouterError::BuildRequestBody)?; - let mut response = client - .send(request) - .await - .map_err(OpenRouterError::HttpSend)?; - - if response.status().is_success() { - let reader = BufReader::new(response.into_body()); - Ok(reader - .lines() - .filter_map(|line| async move { - match line { - Ok(line) => { - if line.starts_with(':') { - return None; - } - - let line = line.strip_prefix("data: ")?; - if line == "[DONE]" { - None - } else { - match serde_json::from_str::(line) { - Ok(response) => Some(Ok(response)), - Err(error) => { - if line.trim().is_empty() { - None - } else { - Some(Err(OpenRouterError::DeserializeResponse(error))) - } - } - } - } - } - Err(error) => Some(Err(OpenRouterError::ReadResponse(error))), - } - }) - .boxed()) - } else { - let code = ApiErrorCode::from_status(response.status().as_u16()); - - let mut body = String::new(); - response - .body_mut() - .read_to_string(&mut body) - .await - .map_err(OpenRouterError::ReadResponse)?; - - let error_response = match serde_json::from_str::(&body) { - Ok(OpenRouterErrorResponse { error }) => error, - Err(_) => OpenRouterErrorBody { - code: response.status().as_u16(), - message: body, - metadata: None, - }, - }; - - match code { - ApiErrorCode::RateLimitError => { - let retry_after = extract_retry_after(response.headers()); - Err(OpenRouterError::RateLimit { - retry_after: retry_after.unwrap_or_else(|| std::time::Duration::from_secs(60)), - }) - } - ApiErrorCode::OverloadedError => { - let retry_after = extract_retry_after(response.headers()); - Err(OpenRouterError::ServerOverloaded { retry_after }) - } - _ => Err(OpenRouterError::ApiError(ApiError { - code: code, - message: error_response.message, - })), - } - } -} - -pub async fn list_models( - client: &dyn HttpClient, - api_url: &str, - api_key: &str, -) -> Result, OpenRouterError> { - let uri = format!("{api_url}/models/user"); - let request_builder = HttpRequest::builder() - .method(Method::GET) - .uri(uri) - .header("Accept", "application/json") - .header("Authorization", format!("Bearer {}", api_key)) - .header("HTTP-Referer", "https://zed.dev") - .header("X-Title", "Zed Editor"); - - let request = request_builder - .body(AsyncBody::default()) - .map_err(OpenRouterError::BuildRequestBody)?; - let mut response = client - .send(request) - .await - .map_err(OpenRouterError::HttpSend)?; - - let mut body = String::new(); - response - .body_mut() - .read_to_string(&mut body) - .await - .map_err(OpenRouterError::ReadResponse)?; - - if response.status().is_success() { - let response: ListModelsResponse = - serde_json::from_str(&body).map_err(OpenRouterError::DeserializeResponse)?; - - let models = response - .data - .into_iter() - .map(|entry| Model { - name: entry.id, - // OpenRouter returns display names in the format "provider_name: model_name". - // When displayed in the UI, these names can get truncated from the right. - // Since users typically already know the provider, we extract just the model name - // portion (after the colon) to create a more concise and user-friendly label - // for the model dropdown in the agent panel. - display_name: Some( - entry - .name - .split(':') - .next_back() - .unwrap_or(&entry.name) - .trim() - .to_string(), - ), - max_tokens: entry.context_length.unwrap_or(2000000), - supports_tools: Some(entry.supported_parameters.contains(&"tools".to_string())), - supports_images: Some( - entry - .architecture - .as_ref() - .map(|arch| arch.input_modalities.contains(&"image".to_string())) - .unwrap_or(false), - ), - mode: if entry - .supported_parameters - .contains(&"reasoning".to_string()) - { - ModelMode::Thinking { - budget_tokens: Some(4_096), - } - } else { - ModelMode::Default - }, - provider: None, - }) - .collect(); - - Ok(models) - } else { - let code = ApiErrorCode::from_status(response.status().as_u16()); - - let mut body = String::new(); - response - .body_mut() - .read_to_string(&mut body) - .await - .map_err(OpenRouterError::ReadResponse)?; - - let error_response = match serde_json::from_str::(&body) { - Ok(OpenRouterErrorResponse { error }) => error, - Err(_) => OpenRouterErrorBody { - code: response.status().as_u16(), - message: body, - metadata: None, - }, - }; - - match code { - ApiErrorCode::RateLimitError => { - let retry_after = extract_retry_after(response.headers()); - Err(OpenRouterError::RateLimit { - retry_after: retry_after.unwrap_or_else(|| std::time::Duration::from_secs(60)), - }) - } - ApiErrorCode::OverloadedError => { - let retry_after = extract_retry_after(response.headers()); - Err(OpenRouterError::ServerOverloaded { retry_after }) - } - _ => Err(OpenRouterError::ApiError(ApiError { - code: code, - message: error_response.message, - })), - } - } -} - -#[derive(Debug)] -pub enum OpenRouterError { - /// Failed to serialize the HTTP request body to JSON - SerializeRequest(serde_json::Error), - - /// Failed to construct the HTTP request body - BuildRequestBody(http::Error), - - /// Failed to send the HTTP request - HttpSend(anyhow::Error), - - /// Failed to deserialize the response from JSON - DeserializeResponse(serde_json::Error), - - /// Failed to read from response stream - ReadResponse(io::Error), - - /// Rate limit exceeded - RateLimit { retry_after: Duration }, - - /// Server overloaded - ServerOverloaded { retry_after: Option }, - - /// API returned an error response - ApiError(ApiError), -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct OpenRouterErrorBody { - pub code: u16, - pub message: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub metadata: Option>, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct OpenRouterErrorResponse { - pub error: OpenRouterErrorBody, -} - -#[derive(Debug, Serialize, Deserialize, Error)] -#[error("OpenRouter API Error: {code}: {message}")] -pub struct ApiError { - pub code: ApiErrorCode, - pub message: String, -} - -/// An OpenROuter API error code. -/// -#[derive(Debug, PartialEq, Eq, Clone, Copy, EnumString, Serialize, Deserialize)] -#[strum(serialize_all = "snake_case")] -pub enum ApiErrorCode { - /// 400: Bad Request (invalid or missing params, CORS) - InvalidRequestError, - /// 401: Invalid credentials (OAuth session expired, disabled/invalid API key) - AuthenticationError, - /// 402: Your account or API key has insufficient credits. Add more credits and retry the request. - PaymentRequiredError, - /// 403: Your chosen model requires moderation and your input was flagged - PermissionError, - /// 408: Your request timed out - RequestTimedOut, - /// 429: You are being rate limited - RateLimitError, - /// 502: Your chosen model is down or we received an invalid response from it - ApiError, - /// 503: There is no available model provider that meets your routing requirements - OverloadedError, -} - -impl std::fmt::Display for ApiErrorCode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let s = match self { - ApiErrorCode::InvalidRequestError => "invalid_request_error", - ApiErrorCode::AuthenticationError => "authentication_error", - ApiErrorCode::PaymentRequiredError => "payment_required_error", - ApiErrorCode::PermissionError => "permission_error", - ApiErrorCode::RequestTimedOut => "request_timed_out", - ApiErrorCode::RateLimitError => "rate_limit_error", - ApiErrorCode::ApiError => "api_error", - ApiErrorCode::OverloadedError => "overloaded_error", - }; - write!(f, "{s}") - } -} - -impl ApiErrorCode { - pub fn from_status(status: u16) -> Self { - match status { - 400 => ApiErrorCode::InvalidRequestError, - 401 => ApiErrorCode::AuthenticationError, - 402 => ApiErrorCode::PaymentRequiredError, - 403 => ApiErrorCode::PermissionError, - 408 => ApiErrorCode::RequestTimedOut, - 429 => ApiErrorCode::RateLimitError, - 502 => ApiErrorCode::ApiError, - 503 => ApiErrorCode::OverloadedError, - _ => ApiErrorCode::ApiError, - } - } -} diff --git a/crates/outline/Cargo.toml b/crates/outline/Cargo.toml deleted file mode 100644 index 5069fa2373..0000000000 --- a/crates/outline/Cargo.toml +++ /dev/null @@ -1,41 +0,0 @@ -[package] -name = "outline" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/outline.rs" -doctest = false - -[dependencies] -editor.workspace = true -fuzzy.workspace = true -gpui.workspace = true -language.workspace = true -ordered-float.workspace = true -picker.workspace = true -settings.workspace = true -smol.workspace = true -theme.workspace = true -ui.workspace = true -util.workspace = true -workspace.workspace = true -zed_actions.workspace = true - -[dev-dependencies] -editor = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -indoc.workspace = true -language = { workspace = true, features = ["test-support"] } -menu.workspace = true -project = { workspace = true, features = ["test-support"] } -rope.workspace = true -serde_json.workspace = true -tree-sitter-rust.workspace = true -tree-sitter-typescript.workspace = true -workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/outline/LICENSE-GPL b/crates/outline/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/outline/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs deleted file mode 100644 index 1f5cf1edab..0000000000 --- a/crates/outline/src/outline.rs +++ /dev/null @@ -1,610 +0,0 @@ -use std::ops::Range; -use std::{ - cmp::{self, Reverse}, - sync::Arc, -}; - -use editor::scroll::ScrollOffset; -use editor::{Anchor, AnchorRangeExt, Editor, scroll::Autoscroll}; -use editor::{MultiBufferOffset, RowHighlightOptions, SelectionEffects}; -use fuzzy::StringMatch; -use gpui::{ - App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, HighlightStyle, - ParentElement, Point, Render, Styled, StyledText, Task, TextStyle, WeakEntity, Window, div, - rems, -}; -use language::{Outline, OutlineItem}; -use ordered_float::OrderedFloat; -use picker::{Picker, PickerDelegate}; -use settings::Settings; -use theme::{ActiveTheme, ThemeSettings}; -use ui::{ListItem, ListItemSpacing, prelude::*}; -use util::ResultExt; -use workspace::{DismissDecision, ModalView, Workspace}; - -pub fn init(cx: &mut App) { - cx.observe_new(OutlineView::register).detach(); - zed_actions::outline::TOGGLE_OUTLINE - .set(|view, window, cx| { - let Ok(editor) = view.downcast::() else { - return; - }; - - toggle(editor, &Default::default(), window, cx); - }) - .ok(); -} - -pub fn toggle( - editor: Entity, - _: &zed_actions::outline::ToggleOutline, - window: &mut Window, - cx: &mut App, -) { - let outline = editor - .read(cx) - .buffer() - .read(cx) - .snapshot(cx) - .outline(Some(cx.theme().syntax())); - - let workspace = window.root::().flatten(); - if let Some((workspace, outline)) = workspace.zip(outline) { - workspace.update(cx, |workspace, cx| { - workspace.toggle_modal(window, cx, |window, cx| { - OutlineView::new(outline, editor, window, cx) - }); - }) - } -} - -pub struct OutlineView { - picker: Entity>, -} - -impl Focusable for OutlineView { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.picker.focus_handle(cx) - } -} - -impl EventEmitter for OutlineView {} -impl ModalView for OutlineView { - fn on_before_dismiss( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> DismissDecision { - self.picker.update(cx, |picker, cx| { - picker.delegate.restore_active_editor(window, cx) - }); - DismissDecision::Dismiss(true) - } -} - -impl Render for OutlineView { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - v_flex() - .w(rems(34.)) - .on_action(cx.listener( - |_this: &mut OutlineView, - _: &zed_actions::outline::ToggleOutline, - _window: &mut Window, - cx: &mut Context| { - // When outline::Toggle is triggered while the outline is open, dismiss it - cx.emit(DismissEvent); - }, - )) - .child(self.picker.clone()) - } -} - -impl OutlineView { - fn register(editor: &mut Editor, _: Option<&mut Window>, cx: &mut Context) { - if editor.mode().is_full() { - let handle = cx.entity().downgrade(); - editor - .register_action(move |action, window, cx| { - if let Some(editor) = handle.upgrade() { - toggle(editor, action, window, cx); - } - }) - .detach(); - } - } - - fn new( - outline: Outline, - editor: Entity, - window: &mut Window, - cx: &mut Context, - ) -> OutlineView { - let delegate = OutlineViewDelegate::new(cx.entity().downgrade(), outline, editor, cx); - let picker = cx.new(|cx| { - Picker::uniform_list(delegate, window, cx).max_height(Some(vh(0.75, window))) - }); - OutlineView { picker } - } -} - -struct OutlineViewDelegate { - outline_view: WeakEntity, - active_editor: Entity, - outline: Outline, - selected_match_index: usize, - prev_scroll_position: Option>, - matches: Vec, - last_query: String, -} - -enum OutlineRowHighlights {} - -impl OutlineViewDelegate { - fn new( - outline_view: WeakEntity, - outline: Outline, - editor: Entity, - - cx: &mut Context, - ) -> Self { - Self { - outline_view, - last_query: Default::default(), - matches: Default::default(), - selected_match_index: 0, - prev_scroll_position: Some(editor.update(cx, |editor, cx| editor.scroll_position(cx))), - active_editor: editor, - outline, - } - } - - fn restore_active_editor(&mut self, window: &mut Window, cx: &mut App) { - self.active_editor.update(cx, |editor, cx| { - editor.clear_row_highlights::(); - if let Some(scroll_position) = self.prev_scroll_position { - editor.set_scroll_position(scroll_position, window, cx); - } - }) - } - - fn set_selected_index( - &mut self, - ix: usize, - navigate: bool, - - cx: &mut Context>, - ) { - self.selected_match_index = ix; - - if navigate && !self.matches.is_empty() { - let selected_match = &self.matches[self.selected_match_index]; - let outline_item = &self.outline.items[selected_match.candidate_id]; - - self.active_editor.update(cx, |active_editor, cx| { - active_editor.clear_row_highlights::(); - active_editor.highlight_rows::( - outline_item.range.start..outline_item.range.end, - cx.theme().colors().editor_highlighted_line_background, - RowHighlightOptions { - autoscroll: true, - ..Default::default() - }, - cx, - ); - active_editor.request_autoscroll(Autoscroll::center(), cx); - }); - } - } -} - -impl PickerDelegate for OutlineViewDelegate { - type ListItem = ListItem; - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - "Search buffer symbols...".into() - } - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn selected_index(&self) -> usize { - self.selected_match_index - } - - fn set_selected_index( - &mut self, - ix: usize, - _: &mut Window, - cx: &mut Context>, - ) { - self.set_selected_index(ix, true, cx); - } - - fn update_matches( - &mut self, - query: String, - window: &mut Window, - cx: &mut Context>, - ) -> Task<()> { - let selected_index; - if query.is_empty() { - self.restore_active_editor(window, cx); - self.matches = self - .outline - .items - .iter() - .enumerate() - .map(|(index, _)| StringMatch { - candidate_id: index, - score: Default::default(), - positions: Default::default(), - string: Default::default(), - }) - .collect(); - - let (buffer, cursor_offset) = self.active_editor.update(cx, |editor, cx| { - let buffer = editor.buffer().read(cx).snapshot(cx); - let cursor_offset = editor - .selections - .newest::(&editor.display_snapshot(cx)) - .head(); - (buffer, cursor_offset) - }); - selected_index = self - .outline - .items - .iter() - .enumerate() - .map(|(ix, item)| { - let range = item.range.to_offset(&buffer); - let distance_to_closest_endpoint = cmp::min( - (range.start.0 as isize - cursor_offset.0 as isize).abs(), - (range.end.0 as isize - cursor_offset.0 as isize).abs(), - ); - let depth = if range.contains(&cursor_offset) { - Some(item.depth) - } else { - None - }; - (ix, depth, distance_to_closest_endpoint) - }) - .max_by_key(|(_, depth, distance)| (*depth, Reverse(*distance))) - .map(|(ix, _, _)| ix) - .unwrap_or(0); - } else { - self.matches = smol::block_on( - self.outline - .search(&query, cx.background_executor().clone()), - ); - selected_index = self - .matches - .iter() - .enumerate() - .max_by_key(|(_, m)| OrderedFloat(m.score)) - .map(|(ix, _)| ix) - .unwrap_or(0); - } - self.last_query = query; - self.set_selected_index(selected_index, !self.last_query.is_empty(), cx); - Task::ready(()) - } - - fn confirm( - &mut self, - _: bool, - window: &mut Window, - cx: &mut Context>, - ) { - self.prev_scroll_position.take(); - self.set_selected_index(self.selected_match_index, true, cx); - - self.active_editor.update(cx, |active_editor, cx| { - let highlight = active_editor - .highlighted_rows::() - .next(); - if let Some((rows, _)) = highlight { - active_editor.change_selections( - SelectionEffects::scroll(Autoscroll::center()), - window, - cx, - |s| s.select_ranges([rows.start..rows.start]), - ); - active_editor.clear_row_highlights::(); - window.focus(&active_editor.focus_handle(cx)); - } - }); - - self.dismissed(window, cx); - } - - fn dismissed(&mut self, window: &mut Window, cx: &mut Context>) { - self.outline_view - .update(cx, |_, cx| cx.emit(DismissEvent)) - .log_err(); - self.restore_active_editor(window, cx); - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _: &mut Window, - cx: &mut Context>, - ) -> Option { - let mat = self.matches.get(ix)?; - let outline_item = self.outline.items.get(mat.candidate_id)?; - - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .child( - div() - .text_ui(cx) - .pl(rems(outline_item.depth as f32)) - .child(render_item(outline_item, mat.ranges(), cx)), - ), - ) - } -} - -pub fn render_item( - outline_item: &OutlineItem, - match_ranges: impl IntoIterator>, - cx: &App, -) -> StyledText { - let highlight_style = HighlightStyle { - background_color: Some(cx.theme().colors().text_accent.alpha(0.3)), - ..Default::default() - }; - let custom_highlights = match_ranges - .into_iter() - .map(|range| (range, highlight_style)); - - let settings = ThemeSettings::get_global(cx); - - // TODO: We probably shouldn't need to build a whole new text style here - // but I'm not sure how to get the current one and modify it. - // Before this change TextStyle::default() was used here, which was giving us the wrong font and text color. - let text_style = TextStyle { - color: cx.theme().colors().text, - font_family: settings.buffer_font.family.clone(), - font_features: settings.buffer_font.features.clone(), - font_fallbacks: settings.buffer_font.fallbacks.clone(), - font_size: settings.buffer_font_size(cx).into(), - font_weight: settings.buffer_font.weight, - line_height: relative(1.), - ..Default::default() - }; - let highlights = gpui::combine_highlights( - custom_highlights, - outline_item.highlight_ranges.iter().cloned(), - ); - - StyledText::new(outline_item.text.clone()).with_default_highlights(&text_style, highlights) -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::{TestAppContext, VisualTestContext}; - use indoc::indoc; - use project::{FakeFs, Project}; - use serde_json::json; - use util::{path, rel_path::rel_path}; - use workspace::{AppState, Workspace}; - - #[gpui::test] - async fn test_outline_view_row_highlights(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/dir"), - json!({ - "a.rs": indoc!{" - // display line 0 - struct SingleLine; // display line 1 - // display line 2 - struct MultiLine { // display line 3 - field_1: i32, // display line 4 - field_2: i32, // display line 5 - } // display line 6 - "} - }), - ) - .await; - - let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; - project.read_with(cx, |project, _| { - project.languages().add(language::rust_lang()) - }); - - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let worktree_id = workspace.update(cx, |workspace, cx| { - workspace.project().update(cx, |project, cx| { - project.worktrees(cx).next().unwrap().read(cx).id() - }) - }); - let _buffer = project - .update(cx, |project, cx| { - project.open_local_buffer(path!("/dir/a.rs"), cx) - }) - .await - .unwrap(); - let editor = workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_path((worktree_id, rel_path("a.rs")), None, true, window, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - let ensure_outline_view_contents = - |outline_view: &Entity>, cx: &mut VisualTestContext| { - assert_eq!(query(outline_view, cx), ""); - assert_eq!( - outline_names(outline_view, cx), - vec![ - "struct SingleLine", - "struct MultiLine", - "field_1", - "field_2" - ], - ); - }; - - let outline_view = open_outline_view(&workspace, cx); - ensure_outline_view_contents(&outline_view, cx); - assert_eq!( - highlighted_display_rows(&editor, cx), - Vec::::new(), - "Initially opened outline view should have no highlights" - ); - assert_single_caret_at_row(&editor, 0, cx); - - cx.dispatch_action(menu::Confirm); - // Ensures that outline still goes to entry even if no queries have been made - assert_single_caret_at_row(&editor, 1, cx); - - let outline_view = open_outline_view(&workspace, cx); - - cx.dispatch_action(menu::SelectNext); - ensure_outline_view_contents(&outline_view, cx); - assert_eq!( - highlighted_display_rows(&editor, cx), - vec![3, 4, 5, 6], - "Second struct's rows should be highlighted" - ); - assert_single_caret_at_row(&editor, 1, cx); - - cx.dispatch_action(menu::SelectPrevious); - ensure_outline_view_contents(&outline_view, cx); - assert_eq!( - highlighted_display_rows(&editor, cx), - vec![1], - "First struct's row should be highlighted" - ); - assert_single_caret_at_row(&editor, 1, cx); - - cx.dispatch_action(menu::Cancel); - ensure_outline_view_contents(&outline_view, cx); - assert_eq!( - highlighted_display_rows(&editor, cx), - Vec::::new(), - "No rows should be highlighted after outline view is cancelled and closed" - ); - assert_single_caret_at_row(&editor, 1, cx); - - let outline_view = open_outline_view(&workspace, cx); - ensure_outline_view_contents(&outline_view, cx); - assert_eq!( - highlighted_display_rows(&editor, cx), - Vec::::new(), - "Reopened outline view should have no highlights" - ); - assert_single_caret_at_row(&editor, 1, cx); - - let expected_first_highlighted_row = 3; - cx.dispatch_action(menu::SelectNext); - ensure_outline_view_contents(&outline_view, cx); - assert_eq!( - highlighted_display_rows(&editor, cx), - vec![expected_first_highlighted_row, 4, 5, 6] - ); - assert_single_caret_at_row(&editor, 1, cx); - cx.dispatch_action(menu::Confirm); - ensure_outline_view_contents(&outline_view, cx); - assert_eq!( - highlighted_display_rows(&editor, cx), - Vec::::new(), - "No rows should be highlighted after outline view is confirmed and closed" - ); - // On confirm, should place the caret on the first row of the highlighted rows range. - assert_single_caret_at_row(&editor, expected_first_highlighted_row, cx); - } - - fn open_outline_view( - workspace: &Entity, - cx: &mut VisualTestContext, - ) -> Entity> { - cx.dispatch_action(zed_actions::outline::ToggleOutline); - workspace.update(cx, |workspace, cx| { - workspace - .active_modal::(cx) - .unwrap() - .read(cx) - .picker - .clone() - }) - } - - fn query( - outline_view: &Entity>, - cx: &mut VisualTestContext, - ) -> String { - outline_view.update(cx, |outline_view, cx| outline_view.query(cx)) - } - - fn outline_names( - outline_view: &Entity>, - cx: &mut VisualTestContext, - ) -> Vec { - outline_view.read_with(cx, |outline_view, _| { - let items = &outline_view.delegate.outline.items; - outline_view - .delegate - .matches - .iter() - .map(|hit| items[hit.candidate_id].text.clone()) - .collect::>() - }) - } - - fn highlighted_display_rows(editor: &Entity, cx: &mut VisualTestContext) -> Vec { - editor.update_in(cx, |editor, window, cx| { - editor - .highlighted_display_rows(window, cx) - .into_keys() - .map(|r| r.0) - .collect() - }) - } - - fn init_test(cx: &mut TestAppContext) -> Arc { - cx.update(|cx| { - let state = AppState::test(cx); - crate::init(cx); - editor::init(cx); - state - }) - } - - #[track_caller] - fn assert_single_caret_at_row( - editor: &Entity, - buffer_row: u32, - cx: &mut VisualTestContext, - ) { - let selections = editor.update(cx, |editor, cx| { - editor - .selections - .all::(&editor.display_snapshot(cx)) - .into_iter() - .map(|s| s.start..s.end) - .collect::>() - }); - assert!( - selections.len() == 1, - "Expected one caret selection but got: {selections:?}" - ); - let selection = &selections[0]; - assert!( - selection.start == selection.end, - "Expected a single caret selection, but got: {selection:?}" - ); - assert_eq!(selection.start.row, buffer_row); - } -} diff --git a/crates/outline_panel/Cargo.toml b/crates/outline_panel/Cargo.toml deleted file mode 100644 index 72e2d1eb63..0000000000 --- a/crates/outline_panel/Cargo.toml +++ /dev/null @@ -1,47 +0,0 @@ -[package] -name = "outline_panel" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/outline_panel.rs" -doctest = false - -[dependencies] -anyhow.workspace = true -collections.workspace = true -db.workspace = true -editor.workspace = true -file_icons.workspace = true -fuzzy.workspace = true -gpui.workspace = true -itertools.workspace = true -language.workspace = true -log.workspace = true -menu.workspace = true -outline.workspace = true -project.workspace = true -search.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -smallvec.workspace = true -smol.workspace = true -theme.workspace = true -ui.workspace = true -util.workspace = true -workspace.workspace = true -worktree.workspace = true -zed_actions.workspace = true - -[dev-dependencies] -search = { workspace = true, features = ["test-support"] } -pretty_assertions.workspace = true - -[package.metadata.cargo-machete] -ignored = ["log"] diff --git a/crates/outline_panel/LICENSE-GPL b/crates/outline_panel/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/outline_panel/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs deleted file mode 100644 index a787ad5b03..0000000000 --- a/crates/outline_panel/src/outline_panel.rs +++ /dev/null @@ -1,7760 +0,0 @@ -mod outline_panel_settings; - -use anyhow::Context as _; -use collections::{BTreeSet, HashMap, HashSet, hash_map}; -use db::kvp::KEY_VALUE_STORE; -use editor::{ - AnchorRangeExt, Bias, DisplayPoint, Editor, EditorEvent, ExcerptId, ExcerptRange, - MultiBufferSnapshot, RangeToAnchorExt, SelectionEffects, - display_map::ToDisplayPoint, - items::{entry_git_aware_label_color, entry_label_color}, - scroll::{Autoscroll, ScrollAnchor}, -}; -use file_icons::FileIcons; -use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; -use gpui::{ - Action, AnyElement, App, AppContext as _, AsyncWindowContext, Bounds, ClipboardItem, Context, - DismissEvent, Div, ElementId, Entity, EventEmitter, FocusHandle, Focusable, HighlightStyle, - InteractiveElement, IntoElement, KeyContext, ListHorizontalSizingBehavior, ListSizingBehavior, - MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Render, ScrollStrategy, - SharedString, Stateful, StatefulInteractiveElement as _, Styled, Subscription, Task, - UniformListScrollHandle, WeakEntity, Window, actions, anchored, deferred, div, point, px, size, - uniform_list, -}; -use itertools::Itertools; -use language::{Anchor, BufferId, BufferSnapshot, OffsetRangeExt, OutlineItem}; -use menu::{Cancel, SelectFirst, SelectLast, SelectNext, SelectPrevious}; -use std::{ - cmp, - collections::BTreeMap, - hash::Hash, - ops::Range, - path::{Path, PathBuf}, - sync::{ - Arc, OnceLock, - atomic::{self, AtomicBool}, - }, - time::Duration, - u32, -}; - -use outline_panel_settings::{DockSide, OutlinePanelSettings, ShowIndentGuides}; -use project::{File, Fs, GitEntry, GitTraversal, Project, ProjectItem}; -use search::{BufferSearchBar, ProjectSearchView}; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsStore}; -use smol::channel; -use theme::{SyntaxTheme, ThemeSettings}; -use ui::{ - ContextMenu, FluentBuilder, HighlightedLabel, IconButton, IconButtonShape, IndentGuideColors, - IndentGuideLayout, ListItem, ScrollAxes, Scrollbars, Tab, Tooltip, WithScrollbar, prelude::*, -}; -use util::{RangeExt, ResultExt, TryFutureExt, debug_panic, rel_path::RelPath}; -use workspace::{ - OpenInTerminal, WeakItemHandle, Workspace, - dock::{DockPosition, Panel, PanelEvent}, - item::ItemHandle, - searchable::{SearchEvent, SearchableItem}, -}; -use worktree::{Entry, ProjectEntryId, WorktreeId}; - -actions!( - outline_panel, - [ - /// Collapses all entries in the outline tree. - CollapseAllEntries, - /// Collapses the currently selected entry. - CollapseSelectedEntry, - /// Expands all entries in the outline tree. - ExpandAllEntries, - /// Expands the currently selected entry. - ExpandSelectedEntry, - /// Folds the selected directory. - FoldDirectory, - /// Opens the selected entry in the editor. - OpenSelectedEntry, - /// Reveals the selected item in the system file manager. - RevealInFileManager, - /// Selects the parent of the current entry. - SelectParent, - /// Toggles the pin status of the active editor. - ToggleActiveEditorPin, - /// Unfolds the selected directory. - UnfoldDirectory, - /// Toggles focus on the outline panel. - ToggleFocus, - ] -); - -const OUTLINE_PANEL_KEY: &str = "OutlinePanel"; -const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50); - -type Outline = OutlineItem; -type HighlightStyleData = Arc, HighlightStyle)>>>; - -pub struct OutlinePanel { - fs: Arc, - width: Option, - project: Entity, - workspace: WeakEntity, - active: bool, - pinned: bool, - scroll_handle: UniformListScrollHandle, - context_menu: Option<(Entity, Point, Subscription)>, - focus_handle: FocusHandle, - pending_serialization: Task>, - fs_entries_depth: HashMap<(WorktreeId, ProjectEntryId), usize>, - fs_entries: Vec, - fs_children_count: HashMap, FsChildren>>, - collapsed_entries: HashSet, - unfolded_dirs: HashMap>, - selected_entry: SelectedEntry, - active_item: Option, - _subscriptions: Vec, - new_entries_for_fs_update: HashSet, - fs_entries_update_task: Task<()>, - cached_entries_update_task: Task<()>, - reveal_selection_task: Task>, - outline_fetch_tasks: HashMap<(BufferId, ExcerptId), Task<()>>, - excerpts: HashMap>, - cached_entries: Vec, - filter_editor: Entity, - mode: ItemsDisplayMode, - max_width_item_index: Option, - preserve_selection_on_buffer_fold_toggles: HashSet, - pending_default_expansion_depth: Option, - outline_children_cache: HashMap, usize), bool>>, -} - -#[derive(Debug)] -enum ItemsDisplayMode { - Search(SearchState), - Outline, -} - -#[derive(Debug)] -struct SearchState { - kind: SearchKind, - query: String, - matches: Vec<(Range, Arc>)>, - highlight_search_match_tx: channel::Sender, - _search_match_highlighter: Task<()>, - _search_match_notify: Task<()>, -} - -struct HighlightArguments { - multi_buffer_snapshot: MultiBufferSnapshot, - match_range: Range, - search_data: Arc>, -} - -impl SearchState { - fn new( - kind: SearchKind, - query: String, - previous_matches: HashMap, Arc>>, - new_matches: Vec>, - theme: Arc, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let (highlight_search_match_tx, highlight_search_match_rx) = channel::unbounded(); - let (notify_tx, notify_rx) = channel::unbounded::<()>(); - Self { - kind, - query, - matches: new_matches - .into_iter() - .map(|range| { - let search_data = previous_matches - .get(&range) - .map(Arc::clone) - .unwrap_or_default(); - (range, search_data) - }) - .collect(), - highlight_search_match_tx, - _search_match_highlighter: cx.background_spawn(async move { - while let Ok(highlight_arguments) = highlight_search_match_rx.recv().await { - let needs_init = highlight_arguments.search_data.get().is_none(); - let search_data = highlight_arguments.search_data.get_or_init(|| { - SearchData::new( - &highlight_arguments.match_range, - &highlight_arguments.multi_buffer_snapshot, - ) - }); - if needs_init { - notify_tx.try_send(()).ok(); - } - - let highlight_data = &search_data.highlights_data; - if highlight_data.get().is_some() { - continue; - } - let mut left_whitespaces_count = 0; - let mut non_whitespace_symbol_occurred = false; - let context_offset_range = search_data - .context_range - .to_offset(&highlight_arguments.multi_buffer_snapshot); - let mut offset = context_offset_range.start; - let mut context_text = String::new(); - let mut highlight_ranges = Vec::new(); - for mut chunk in highlight_arguments - .multi_buffer_snapshot - .chunks(context_offset_range.start..context_offset_range.end, true) - { - if !non_whitespace_symbol_occurred { - for c in chunk.text.chars() { - if c.is_whitespace() { - left_whitespaces_count += c.len_utf8(); - } else { - non_whitespace_symbol_occurred = true; - break; - } - } - } - - if chunk.text.len() > context_offset_range.end - offset { - chunk.text = &chunk.text[0..(context_offset_range.end - offset)]; - offset = context_offset_range.end; - } else { - offset += chunk.text.len(); - } - let style = chunk - .syntax_highlight_id - .and_then(|highlight| highlight.style(&theme)); - if let Some(style) = style { - let start = context_text.len(); - let end = start + chunk.text.len(); - highlight_ranges.push((start..end, style)); - } - context_text.push_str(chunk.text); - if offset >= context_offset_range.end { - break; - } - } - - highlight_ranges.iter_mut().for_each(|(range, _)| { - range.start = range.start.saturating_sub(left_whitespaces_count); - range.end = range.end.saturating_sub(left_whitespaces_count); - }); - if highlight_data.set(highlight_ranges).ok().is_some() { - notify_tx.try_send(()).ok(); - } - - let trimmed_text = context_text[left_whitespaces_count..].to_owned(); - debug_assert_eq!( - trimmed_text, search_data.context_text, - "Highlighted text that does not match the buffer text" - ); - } - }), - _search_match_notify: cx.spawn_in(window, async move |outline_panel, cx| { - loop { - match notify_rx.recv().await { - Ok(()) => {} - Err(_) => break, - }; - while let Ok(()) = notify_rx.try_recv() { - // - } - let update_result = outline_panel.update(cx, |_, cx| { - cx.notify(); - }); - if update_result.is_err() { - break; - } - } - }), - } - } -} - -#[derive(Debug)] -enum SelectedEntry { - Invalidated(Option), - Valid(PanelEntry, usize), - None, -} - -impl SelectedEntry { - fn invalidate(&mut self) { - match std::mem::replace(self, SelectedEntry::None) { - Self::Valid(entry, _) => *self = Self::Invalidated(Some(entry)), - Self::None => *self = Self::Invalidated(None), - other => *self = other, - } - } - - fn is_invalidated(&self) -> bool { - matches!(self, Self::Invalidated(_)) - } -} - -#[derive(Debug, Clone, Copy, Default)] -struct FsChildren { - files: usize, - dirs: usize, -} - -impl FsChildren { - fn may_be_fold_part(&self) -> bool { - self.dirs == 0 || (self.dirs == 1 && self.files == 0) - } -} - -#[derive(Clone, Debug)] -struct CachedEntry { - depth: usize, - string_match: Option, - entry: PanelEntry, -} - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -enum CollapsedEntry { - Dir(WorktreeId, ProjectEntryId), - File(WorktreeId, BufferId), - ExternalFile(BufferId), - Excerpt(BufferId, ExcerptId), - Outline(BufferId, ExcerptId, Range), -} - -#[derive(Debug)] -struct Excerpt { - range: ExcerptRange, - outlines: ExcerptOutlines, -} - -impl Excerpt { - fn invalidate_outlines(&mut self) { - if let ExcerptOutlines::Outlines(valid_outlines) = &mut self.outlines { - self.outlines = ExcerptOutlines::Invalidated(std::mem::take(valid_outlines)); - } - } - - fn iter_outlines(&self) -> impl Iterator { - match &self.outlines { - ExcerptOutlines::Outlines(outlines) => outlines.iter(), - ExcerptOutlines::Invalidated(outlines) => outlines.iter(), - ExcerptOutlines::NotFetched => [].iter(), - } - } - - fn should_fetch_outlines(&self) -> bool { - match &self.outlines { - ExcerptOutlines::Outlines(_) => false, - ExcerptOutlines::Invalidated(_) => true, - ExcerptOutlines::NotFetched => true, - } - } -} - -#[derive(Debug)] -enum ExcerptOutlines { - Outlines(Vec), - Invalidated(Vec), - NotFetched, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -struct FoldedDirsEntry { - worktree_id: WorktreeId, - entries: Vec, -} - -// TODO: collapse the inner enums into panel entry -#[derive(Clone, Debug)] -enum PanelEntry { - Fs(FsEntry), - FoldedDirs(FoldedDirsEntry), - Outline(OutlineEntry), - Search(SearchEntry), -} - -#[derive(Clone, Debug)] -struct SearchEntry { - match_range: Range, - kind: SearchKind, - render_data: Arc>, -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] -enum SearchKind { - Project, - Buffer, -} - -#[derive(Clone, Debug)] -struct SearchData { - context_range: Range, - context_text: String, - truncated_left: bool, - truncated_right: bool, - search_match_indices: Vec>, - highlights_data: HighlightStyleData, -} - -impl PartialEq for PanelEntry { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Self::Fs(a), Self::Fs(b)) => a == b, - ( - Self::FoldedDirs(FoldedDirsEntry { - worktree_id: worktree_id_a, - entries: entries_a, - }), - Self::FoldedDirs(FoldedDirsEntry { - worktree_id: worktree_id_b, - entries: entries_b, - }), - ) => worktree_id_a == worktree_id_b && entries_a == entries_b, - (Self::Outline(a), Self::Outline(b)) => a == b, - ( - Self::Search(SearchEntry { - match_range: match_range_a, - kind: kind_a, - .. - }), - Self::Search(SearchEntry { - match_range: match_range_b, - kind: kind_b, - .. - }), - ) => match_range_a == match_range_b && kind_a == kind_b, - _ => false, - } - } -} - -impl Eq for PanelEntry {} - -const SEARCH_MATCH_CONTEXT_SIZE: u32 = 40; -const TRUNCATED_CONTEXT_MARK: &str = "…"; - -impl SearchData { - fn new( - match_range: &Range, - multi_buffer_snapshot: &MultiBufferSnapshot, - ) -> Self { - let match_point_range = match_range.to_point(multi_buffer_snapshot); - let context_left_border = multi_buffer_snapshot.clip_point( - language::Point::new( - match_point_range.start.row, - match_point_range - .start - .column - .saturating_sub(SEARCH_MATCH_CONTEXT_SIZE), - ), - Bias::Left, - ); - let context_right_border = multi_buffer_snapshot.clip_point( - language::Point::new( - match_point_range.end.row, - match_point_range.end.column + SEARCH_MATCH_CONTEXT_SIZE, - ), - Bias::Right, - ); - - let context_anchor_range = - (context_left_border..context_right_border).to_anchors(multi_buffer_snapshot); - let context_offset_range = context_anchor_range.to_offset(multi_buffer_snapshot); - let match_offset_range = match_range.to_offset(multi_buffer_snapshot); - - let mut search_match_indices = vec![ - match_offset_range.start - context_offset_range.start - ..match_offset_range.end - context_offset_range.start, - ]; - - let entire_context_text = multi_buffer_snapshot - .text_for_range(context_offset_range.clone()) - .collect::(); - let left_whitespaces_offset = entire_context_text - .chars() - .take_while(|c| c.is_whitespace()) - .map(|c| c.len_utf8()) - .sum::(); - - let mut extended_context_left_border = context_left_border; - extended_context_left_border.column = extended_context_left_border.column.saturating_sub(1); - let extended_context_left_border = - multi_buffer_snapshot.clip_point(extended_context_left_border, Bias::Left); - let mut extended_context_right_border = context_right_border; - extended_context_right_border.column += 1; - let extended_context_right_border = - multi_buffer_snapshot.clip_point(extended_context_right_border, Bias::Right); - - let truncated_left = left_whitespaces_offset == 0 - && extended_context_left_border < context_left_border - && multi_buffer_snapshot - .chars_at(extended_context_left_border) - .last() - .is_some_and(|c| !c.is_whitespace()); - let truncated_right = entire_context_text - .chars() - .last() - .is_none_or(|c| !c.is_whitespace()) - && extended_context_right_border > context_right_border - && multi_buffer_snapshot - .chars_at(extended_context_right_border) - .next() - .is_some_and(|c| !c.is_whitespace()); - search_match_indices.iter_mut().for_each(|range| { - range.start = range.start.saturating_sub(left_whitespaces_offset); - range.end = range.end.saturating_sub(left_whitespaces_offset); - }); - - let trimmed_row_offset_range = - context_offset_range.start + left_whitespaces_offset..context_offset_range.end; - let trimmed_text = entire_context_text[left_whitespaces_offset..].to_owned(); - Self { - highlights_data: Arc::default(), - search_match_indices, - context_range: trimmed_row_offset_range.to_anchors(multi_buffer_snapshot), - context_text: trimmed_text, - truncated_left, - truncated_right, - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -struct OutlineEntryExcerpt { - id: ExcerptId, - buffer_id: BufferId, - range: ExcerptRange, -} - -#[derive(Clone, Debug, Eq)] -struct OutlineEntryOutline { - buffer_id: BufferId, - excerpt_id: ExcerptId, - outline: Outline, -} - -impl PartialEq for OutlineEntryOutline { - fn eq(&self, other: &Self) -> bool { - self.buffer_id == other.buffer_id - && self.excerpt_id == other.excerpt_id - && self.outline.depth == other.outline.depth - && self.outline.range == other.outline.range - && self.outline.text == other.outline.text - } -} - -impl Hash for OutlineEntryOutline { - fn hash(&self, state: &mut H) { - ( - self.buffer_id, - self.excerpt_id, - self.outline.depth, - &self.outline.range, - &self.outline.text, - ) - .hash(state); - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -enum OutlineEntry { - Excerpt(OutlineEntryExcerpt), - Outline(OutlineEntryOutline), -} - -impl OutlineEntry { - fn ids(&self) -> (BufferId, ExcerptId) { - match self { - OutlineEntry::Excerpt(excerpt) => (excerpt.buffer_id, excerpt.id), - OutlineEntry::Outline(outline) => (outline.buffer_id, outline.excerpt_id), - } - } -} - -#[derive(Debug, Clone, Eq)] -struct FsEntryFile { - worktree_id: WorktreeId, - entry: GitEntry, - buffer_id: BufferId, - excerpts: Vec, -} - -impl PartialEq for FsEntryFile { - fn eq(&self, other: &Self) -> bool { - self.worktree_id == other.worktree_id - && self.entry.id == other.entry.id - && self.buffer_id == other.buffer_id - } -} - -impl Hash for FsEntryFile { - fn hash(&self, state: &mut H) { - (self.buffer_id, self.entry.id, self.worktree_id).hash(state); - } -} - -#[derive(Debug, Clone, Eq)] -struct FsEntryDirectory { - worktree_id: WorktreeId, - entry: GitEntry, -} - -impl PartialEq for FsEntryDirectory { - fn eq(&self, other: &Self) -> bool { - self.worktree_id == other.worktree_id && self.entry.id == other.entry.id - } -} - -impl Hash for FsEntryDirectory { - fn hash(&self, state: &mut H) { - (self.worktree_id, self.entry.id).hash(state); - } -} - -#[derive(Debug, Clone, Eq)] -struct FsEntryExternalFile { - buffer_id: BufferId, - excerpts: Vec, -} - -impl PartialEq for FsEntryExternalFile { - fn eq(&self, other: &Self) -> bool { - self.buffer_id == other.buffer_id - } -} - -impl Hash for FsEntryExternalFile { - fn hash(&self, state: &mut H) { - self.buffer_id.hash(state); - } -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum FsEntry { - ExternalFile(FsEntryExternalFile), - Directory(FsEntryDirectory), - File(FsEntryFile), -} - -struct ActiveItem { - item_handle: Box, - active_editor: WeakEntity, - _buffer_search_subscription: Subscription, - _editor_subscription: Subscription, -} - -#[derive(Debug)] -pub enum Event { - Focus, -} - -#[derive(Serialize, Deserialize)] -struct SerializedOutlinePanel { - width: Option, - active: Option, -} - -pub fn init(cx: &mut App) { - cx.observe_new(|workspace: &mut Workspace, _, _| { - workspace.register_action(|workspace, _: &ToggleFocus, window, cx| { - workspace.toggle_panel_focus::(window, cx); - }); - }) - .detach(); -} - -impl OutlinePanel { - pub async fn load( - workspace: WeakEntity, - mut cx: AsyncWindowContext, - ) -> anyhow::Result> { - let serialized_panel = match workspace - .read_with(&cx, |workspace, _| { - OutlinePanel::serialization_key(workspace) - }) - .ok() - .flatten() - { - Some(serialization_key) => cx - .background_spawn(async move { KEY_VALUE_STORE.read_kvp(&serialization_key) }) - .await - .context("loading outline panel") - .log_err() - .flatten() - .map(|panel| serde_json::from_str::(&panel)) - .transpose() - .log_err() - .flatten(), - None => None, - }; - - workspace.update_in(&mut cx, |workspace, window, cx| { - let panel = Self::new(workspace, window, cx); - if let Some(serialized_panel) = serialized_panel { - panel.update(cx, |panel, cx| { - panel.width = serialized_panel.width.map(|px| px.round()); - panel.active = serialized_panel.active.unwrap_or(false); - cx.notify(); - }); - } - panel - }) - } - - fn new( - workspace: &mut Workspace, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - let project = workspace.project().clone(); - let workspace_handle = cx.entity().downgrade(); - - cx.new(|cx| { - let filter_editor = cx.new(|cx| { - let mut editor = Editor::single_line(window, cx); - editor.set_placeholder_text("Search buffer symbols…", window, cx); - editor - }); - let filter_update_subscription = cx.subscribe_in( - &filter_editor, - window, - |outline_panel: &mut Self, _, event, window, cx| { - if let editor::EditorEvent::BufferEdited = event { - outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx); - } - }, - ); - - let focus_handle = cx.focus_handle(); - let focus_subscription = cx.on_focus(&focus_handle, window, Self::focus_in); - let workspace_subscription = cx.subscribe_in( - &workspace - .weak_handle() - .upgrade() - .expect("have a &mut Workspace"), - window, - move |outline_panel, workspace, event, window, cx| { - if let workspace::Event::ActiveItemChanged = event { - if let Some((new_active_item, new_active_editor)) = - workspace_active_editor(workspace.read(cx), cx) - { - if outline_panel.should_replace_active_item(new_active_item.as_ref()) { - outline_panel.replace_active_editor( - new_active_item, - new_active_editor, - window, - cx, - ); - } - } else { - outline_panel.clear_previous(window, cx); - cx.notify(); - } - } - }, - ); - - let icons_subscription = cx.observe_global::(|_, cx| { - cx.notify(); - }); - - let mut outline_panel_settings = *OutlinePanelSettings::get_global(cx); - let mut current_theme = ThemeSettings::get_global(cx).clone(); - let settings_subscription = - cx.observe_global_in::(window, move |outline_panel, window, cx| { - let new_settings = OutlinePanelSettings::get_global(cx); - let new_theme = ThemeSettings::get_global(cx); - if ¤t_theme != new_theme { - outline_panel_settings = *new_settings; - current_theme = new_theme.clone(); - for excerpts in outline_panel.excerpts.values_mut() { - for excerpt in excerpts.values_mut() { - excerpt.invalidate_outlines(); - } - } - let update_cached_items = outline_panel.update_non_fs_items(window, cx); - if update_cached_items { - outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx); - } - } else if &outline_panel_settings != new_settings { - let old_expansion_depth = outline_panel_settings.expand_outlines_with_depth; - outline_panel_settings = *new_settings; - - if old_expansion_depth != new_settings.expand_outlines_with_depth { - let old_collapsed_entries = outline_panel.collapsed_entries.clone(); - outline_panel - .collapsed_entries - .retain(|entry| !matches!(entry, CollapsedEntry::Outline(..))); - - let new_depth = new_settings.expand_outlines_with_depth; - - for (buffer_id, excerpts) in &outline_panel.excerpts { - for (excerpt_id, excerpt) in excerpts { - if let ExcerptOutlines::Outlines(outlines) = &excerpt.outlines { - for outline in outlines { - if outline_panel - .outline_children_cache - .get(buffer_id) - .and_then(|children_map| { - let key = - (outline.range.clone(), outline.depth); - children_map.get(&key) - }) - .copied() - .unwrap_or(false) - && (new_depth == 0 || outline.depth >= new_depth) - { - outline_panel.collapsed_entries.insert( - CollapsedEntry::Outline( - *buffer_id, - *excerpt_id, - outline.range.clone(), - ), - ); - } - } - } - } - } - - if old_collapsed_entries != outline_panel.collapsed_entries { - outline_panel.update_cached_entries( - Some(UPDATE_DEBOUNCE), - window, - cx, - ); - } - } else { - cx.notify(); - } - } - }); - - let scroll_handle = UniformListScrollHandle::new(); - - let mut outline_panel = Self { - mode: ItemsDisplayMode::Outline, - active: false, - pinned: false, - workspace: workspace_handle, - project, - fs: workspace.app_state().fs.clone(), - max_width_item_index: None, - scroll_handle, - focus_handle, - filter_editor, - fs_entries: Vec::new(), - fs_entries_depth: HashMap::default(), - fs_children_count: HashMap::default(), - collapsed_entries: HashSet::default(), - unfolded_dirs: HashMap::default(), - selected_entry: SelectedEntry::None, - context_menu: None, - width: None, - active_item: None, - pending_serialization: Task::ready(None), - new_entries_for_fs_update: HashSet::default(), - preserve_selection_on_buffer_fold_toggles: HashSet::default(), - pending_default_expansion_depth: None, - fs_entries_update_task: Task::ready(()), - cached_entries_update_task: Task::ready(()), - reveal_selection_task: Task::ready(Ok(())), - outline_fetch_tasks: HashMap::default(), - excerpts: HashMap::default(), - cached_entries: Vec::new(), - _subscriptions: vec![ - settings_subscription, - icons_subscription, - focus_subscription, - workspace_subscription, - filter_update_subscription, - ], - outline_children_cache: HashMap::default(), - }; - if let Some((item, editor)) = workspace_active_editor(workspace, cx) { - outline_panel.replace_active_editor(item, editor, window, cx); - } - outline_panel - }) - } - - fn serialization_key(workspace: &Workspace) -> Option { - workspace - .database_id() - .map(|id| i64::from(id).to_string()) - .or(workspace.session_id()) - .map(|id| format!("{}-{:?}", OUTLINE_PANEL_KEY, id)) - } - - fn serialize(&mut self, cx: &mut Context) { - let Some(serialization_key) = self - .workspace - .read_with(cx, |workspace, _| { - OutlinePanel::serialization_key(workspace) - }) - .ok() - .flatten() - else { - return; - }; - let width = self.width; - let active = Some(self.active); - self.pending_serialization = cx.background_spawn( - async move { - KEY_VALUE_STORE - .write_kvp( - serialization_key, - serde_json::to_string(&SerializedOutlinePanel { width, active })?, - ) - .await?; - anyhow::Ok(()) - } - .log_err(), - ); - } - - fn dispatch_context(&self, window: &mut Window, cx: &mut Context) -> KeyContext { - let mut dispatch_context = KeyContext::new_with_defaults(); - dispatch_context.add("OutlinePanel"); - dispatch_context.add("menu"); - let identifier = if self.filter_editor.focus_handle(cx).is_focused(window) { - "editing" - } else { - "not_editing" - }; - dispatch_context.add(identifier); - dispatch_context - } - - fn unfold_directory( - &mut self, - _: &UnfoldDirectory, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(PanelEntry::FoldedDirs(FoldedDirsEntry { - worktree_id, - entries, - .. - })) = self.selected_entry().cloned() - { - self.unfolded_dirs - .entry(worktree_id) - .or_default() - .extend(entries.iter().map(|entry| entry.id)); - self.update_cached_entries(None, window, cx); - } - } - - fn fold_directory(&mut self, _: &FoldDirectory, window: &mut Window, cx: &mut Context) { - let (worktree_id, entry) = match self.selected_entry().cloned() { - Some(PanelEntry::Fs(FsEntry::Directory(directory))) => { - (directory.worktree_id, Some(directory.entry)) - } - Some(PanelEntry::FoldedDirs(folded_dirs)) => { - (folded_dirs.worktree_id, folded_dirs.entries.last().cloned()) - } - _ => return, - }; - let Some(entry) = entry else { - return; - }; - let unfolded_dirs = self.unfolded_dirs.get_mut(&worktree_id); - let worktree = self - .project - .read(cx) - .worktree_for_id(worktree_id, cx) - .map(|w| w.read(cx).snapshot()); - let Some((_, unfolded_dirs)) = worktree.zip(unfolded_dirs) else { - return; - }; - - unfolded_dirs.remove(&entry.id); - self.update_cached_entries(None, window, cx); - } - - fn open_selected_entry( - &mut self, - _: &OpenSelectedEntry, - window: &mut Window, - cx: &mut Context, - ) { - if self.filter_editor.focus_handle(cx).is_focused(window) { - cx.propagate() - } else if let Some(selected_entry) = self.selected_entry().cloned() { - self.scroll_editor_to_entry(&selected_entry, true, true, window, cx); - } - } - - fn cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context) { - if self.filter_editor.focus_handle(cx).is_focused(window) { - self.focus_handle.focus(window); - } else { - self.filter_editor.focus_handle(cx).focus(window); - } - - if self.context_menu.is_some() { - self.context_menu.take(); - cx.notify(); - } - } - - fn open_excerpts( - &mut self, - action: &editor::actions::OpenExcerpts, - window: &mut Window, - cx: &mut Context, - ) { - if self.filter_editor.focus_handle(cx).is_focused(window) { - cx.propagate() - } else if let Some((active_editor, selected_entry)) = - self.active_editor().zip(self.selected_entry().cloned()) - { - self.scroll_editor_to_entry(&selected_entry, true, true, window, cx); - active_editor.update(cx, |editor, cx| editor.open_excerpts(action, window, cx)); - } - } - - fn open_excerpts_split( - &mut self, - action: &editor::actions::OpenExcerptsSplit, - window: &mut Window, - cx: &mut Context, - ) { - if self.filter_editor.focus_handle(cx).is_focused(window) { - cx.propagate() - } else if let Some((active_editor, selected_entry)) = - self.active_editor().zip(self.selected_entry().cloned()) - { - self.scroll_editor_to_entry(&selected_entry, true, true, window, cx); - active_editor.update(cx, |editor, cx| { - editor.open_excerpts_in_split(action, window, cx) - }); - } - } - - fn scroll_editor_to_entry( - &mut self, - entry: &PanelEntry, - prefer_selection_change: bool, - prefer_focus_change: bool, - window: &mut Window, - cx: &mut Context, - ) { - let Some(active_editor) = self.active_editor() else { - return; - }; - let active_multi_buffer = active_editor.read(cx).buffer().clone(); - let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx); - let mut change_selection = prefer_selection_change; - let mut change_focus = prefer_focus_change; - let mut scroll_to_buffer = None; - let scroll_target = match entry { - PanelEntry::FoldedDirs(..) | PanelEntry::Fs(FsEntry::Directory(..)) => { - change_focus = false; - None - } - PanelEntry::Fs(FsEntry::ExternalFile(file)) => { - change_selection = false; - scroll_to_buffer = Some(file.buffer_id); - multi_buffer_snapshot.excerpts().find_map( - |(excerpt_id, buffer_snapshot, excerpt_range)| { - if buffer_snapshot.remote_id() == file.buffer_id { - multi_buffer_snapshot - .anchor_in_excerpt(excerpt_id, excerpt_range.context.start) - } else { - None - } - }, - ) - } - - PanelEntry::Fs(FsEntry::File(file)) => { - change_selection = false; - scroll_to_buffer = Some(file.buffer_id); - self.project - .update(cx, |project, cx| { - project - .path_for_entry(file.entry.id, cx) - .and_then(|path| project.get_open_buffer(&path, cx)) - }) - .map(|buffer| { - active_multi_buffer - .read(cx) - .excerpts_for_buffer(buffer.read(cx).remote_id(), cx) - }) - .and_then(|excerpts| { - let (excerpt_id, excerpt_range) = excerpts.first()?; - multi_buffer_snapshot - .anchor_in_excerpt(*excerpt_id, excerpt_range.context.start) - }) - } - PanelEntry::Outline(OutlineEntry::Outline(outline)) => multi_buffer_snapshot - .anchor_in_excerpt(outline.excerpt_id, outline.outline.range.start) - .or_else(|| { - multi_buffer_snapshot - .anchor_in_excerpt(outline.excerpt_id, outline.outline.range.end) - }), - PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => { - change_selection = false; - change_focus = false; - multi_buffer_snapshot.anchor_in_excerpt(excerpt.id, excerpt.range.context.start) - } - PanelEntry::Search(search_entry) => Some(search_entry.match_range.start), - }; - - if let Some(anchor) = scroll_target { - let activate = self - .workspace - .update(cx, |workspace, cx| match self.active_item() { - Some(active_item) => workspace.activate_item( - active_item.as_ref(), - true, - change_focus, - window, - cx, - ), - None => workspace.activate_item(&active_editor, true, change_focus, window, cx), - }); - - if activate.is_ok() { - self.select_entry(entry.clone(), true, window, cx); - if change_selection { - active_editor.update(cx, |editor, cx| { - editor.change_selections( - SelectionEffects::scroll(Autoscroll::center()), - window, - cx, - |s| s.select_ranges(Some(anchor..anchor)), - ); - }); - } else { - let mut offset = Point::default(); - if let Some(buffer_id) = scroll_to_buffer - && multi_buffer_snapshot.as_singleton().is_none() - && !active_editor.read(cx).is_buffer_folded(buffer_id, cx) - { - offset.y = -(active_editor.read(cx).file_header_size() as f64); - } - - active_editor.update(cx, |editor, cx| { - editor.set_scroll_anchor(ScrollAnchor { offset, anchor }, window, cx); - }); - } - - if change_focus { - active_editor.focus_handle(cx).focus(window); - } else { - self.focus_handle.focus(window); - } - } - } - } - - fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context) { - if let Some(entry_to_select) = self.selected_entry().and_then(|selected_entry| { - self.cached_entries - .iter() - .map(|cached_entry| &cached_entry.entry) - .skip_while(|entry| entry != &selected_entry) - .nth(1) - .cloned() - }) { - self.select_entry(entry_to_select, true, window, cx); - } else { - self.select_first(&SelectFirst {}, window, cx) - } - if let Some(selected_entry) = self.selected_entry().cloned() { - self.scroll_editor_to_entry(&selected_entry, true, false, window, cx); - } - } - - fn select_previous(&mut self, _: &SelectPrevious, window: &mut Window, cx: &mut Context) { - if let Some(entry_to_select) = self.selected_entry().and_then(|selected_entry| { - self.cached_entries - .iter() - .rev() - .map(|cached_entry| &cached_entry.entry) - .skip_while(|entry| entry != &selected_entry) - .nth(1) - .cloned() - }) { - self.select_entry(entry_to_select, true, window, cx); - } else { - self.select_last(&SelectLast, window, cx) - } - if let Some(selected_entry) = self.selected_entry().cloned() { - self.scroll_editor_to_entry(&selected_entry, true, false, window, cx); - } - } - - fn select_parent(&mut self, _: &SelectParent, window: &mut Window, cx: &mut Context) { - if let Some(entry_to_select) = self.selected_entry().and_then(|selected_entry| { - let mut previous_entries = self - .cached_entries - .iter() - .rev() - .map(|cached_entry| &cached_entry.entry) - .skip_while(|entry| entry != &selected_entry) - .skip(1); - match &selected_entry { - PanelEntry::Fs(fs_entry) => match fs_entry { - FsEntry::ExternalFile(..) => None, - FsEntry::File(FsEntryFile { - worktree_id, entry, .. - }) - | FsEntry::Directory(FsEntryDirectory { - worktree_id, entry, .. - }) => entry.path.parent().and_then(|parent_path| { - previous_entries.find(|entry| match entry { - PanelEntry::Fs(FsEntry::Directory(directory)) => { - directory.worktree_id == *worktree_id - && directory.entry.path.as_ref() == parent_path - } - PanelEntry::FoldedDirs(FoldedDirsEntry { - worktree_id: dirs_worktree_id, - entries: dirs, - .. - }) => { - dirs_worktree_id == worktree_id - && dirs - .last() - .is_some_and(|dir| dir.path.as_ref() == parent_path) - } - _ => false, - }) - }), - }, - PanelEntry::FoldedDirs(folded_dirs) => folded_dirs - .entries - .first() - .and_then(|entry| entry.path.parent()) - .and_then(|parent_path| { - previous_entries.find(|entry| { - if let PanelEntry::Fs(FsEntry::Directory(directory)) = entry { - directory.worktree_id == folded_dirs.worktree_id - && directory.entry.path.as_ref() == parent_path - } else { - false - } - }) - }), - PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => { - previous_entries.find(|entry| match entry { - PanelEntry::Fs(FsEntry::File(file)) => { - file.buffer_id == excerpt.buffer_id - && file.excerpts.contains(&excerpt.id) - } - PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => { - external_file.buffer_id == excerpt.buffer_id - && external_file.excerpts.contains(&excerpt.id) - } - _ => false, - }) - } - PanelEntry::Outline(OutlineEntry::Outline(outline)) => { - previous_entries.find(|entry| { - if let PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) = entry { - outline.buffer_id == excerpt.buffer_id - && outline.excerpt_id == excerpt.id - } else { - false - } - }) - } - PanelEntry::Search(_) => { - previous_entries.find(|entry| !matches!(entry, PanelEntry::Search(_))) - } - } - }) { - self.select_entry(entry_to_select.clone(), true, window, cx); - } else { - self.select_first(&SelectFirst {}, window, cx); - } - } - - fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context) { - if let Some(first_entry) = self.cached_entries.first() { - self.select_entry(first_entry.entry.clone(), true, window, cx); - } - } - - fn select_last(&mut self, _: &SelectLast, window: &mut Window, cx: &mut Context) { - if let Some(new_selection) = self - .cached_entries - .iter() - .rev() - .map(|cached_entry| &cached_entry.entry) - .next() - { - self.select_entry(new_selection.clone(), true, window, cx); - } - } - - fn autoscroll(&mut self, cx: &mut Context) { - if let Some(selected_entry) = self.selected_entry() { - let index = self - .cached_entries - .iter() - .position(|cached_entry| &cached_entry.entry == selected_entry); - if let Some(index) = index { - self.scroll_handle - .scroll_to_item(index, ScrollStrategy::Center); - cx.notify(); - } - } - } - - fn focus_in(&mut self, window: &mut Window, cx: &mut Context) { - if !self.focus_handle.contains_focused(window, cx) { - cx.emit(Event::Focus); - } - } - - fn deploy_context_menu( - &mut self, - position: Point, - entry: PanelEntry, - window: &mut Window, - cx: &mut Context, - ) { - self.select_entry(entry.clone(), true, window, cx); - let is_root = match &entry { - PanelEntry::Fs(FsEntry::File(FsEntryFile { - worktree_id, entry, .. - })) - | PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory { - worktree_id, entry, .. - })) => self - .project - .read(cx) - .worktree_for_id(*worktree_id, cx) - .map(|worktree| { - worktree.read(cx).root_entry().map(|entry| entry.id) == Some(entry.id) - }) - .unwrap_or(false), - PanelEntry::FoldedDirs(FoldedDirsEntry { - worktree_id, - entries, - .. - }) => entries - .first() - .and_then(|entry| { - self.project - .read(cx) - .worktree_for_id(*worktree_id, cx) - .map(|worktree| { - worktree.read(cx).root_entry().map(|entry| entry.id) == Some(entry.id) - }) - }) - .unwrap_or(false), - PanelEntry::Fs(FsEntry::ExternalFile(..)) => false, - PanelEntry::Outline(..) => { - cx.notify(); - return; - } - PanelEntry::Search(_) => { - cx.notify(); - return; - } - }; - let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs; - let is_foldable = auto_fold_dirs && !is_root && self.is_foldable(&entry); - let is_unfoldable = auto_fold_dirs && !is_root && self.is_unfoldable(&entry); - - let context_menu = ContextMenu::build(window, cx, |menu, _, _| { - menu.context(self.focus_handle.clone()) - .when(cfg!(target_os = "macos"), |menu| { - menu.action("Reveal in Finder", Box::new(RevealInFileManager)) - }) - .when(cfg!(not(target_os = "macos")), |menu| { - menu.action("Reveal in File Manager", Box::new(RevealInFileManager)) - }) - .action("Open in Terminal", Box::new(OpenInTerminal)) - .when(is_unfoldable, |menu| { - menu.action("Unfold Directory", Box::new(UnfoldDirectory)) - }) - .when(is_foldable, |menu| { - menu.action("Fold Directory", Box::new(FoldDirectory)) - }) - .separator() - .action("Copy Path", Box::new(zed_actions::workspace::CopyPath)) - .action( - "Copy Relative Path", - Box::new(zed_actions::workspace::CopyRelativePath), - ) - }); - window.focus(&context_menu.focus_handle(cx)); - let subscription = cx.subscribe(&context_menu, |outline_panel, _, _: &DismissEvent, cx| { - outline_panel.context_menu.take(); - cx.notify(); - }); - self.context_menu = Some((context_menu, position, subscription)); - cx.notify(); - } - - fn is_unfoldable(&self, entry: &PanelEntry) -> bool { - matches!(entry, PanelEntry::FoldedDirs(..)) - } - - fn is_foldable(&self, entry: &PanelEntry) -> bool { - let (directory_worktree, directory_entry) = match entry { - PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory { - worktree_id, - entry: directory_entry, - .. - })) => (*worktree_id, Some(directory_entry)), - _ => return false, - }; - let Some(directory_entry) = directory_entry else { - return false; - }; - - if self - .unfolded_dirs - .get(&directory_worktree) - .is_none_or(|unfolded_dirs| !unfolded_dirs.contains(&directory_entry.id)) - { - return false; - } - - let children = self - .fs_children_count - .get(&directory_worktree) - .and_then(|entries| entries.get(&directory_entry.path)) - .copied() - .unwrap_or_default(); - - children.may_be_fold_part() && children.dirs > 0 - } - - fn expand_selected_entry( - &mut self, - _: &ExpandSelectedEntry, - window: &mut Window, - cx: &mut Context, - ) { - let Some(active_editor) = self.active_editor() else { - return; - }; - let Some(selected_entry) = self.selected_entry().cloned() else { - return; - }; - let mut buffers_to_unfold = HashSet::default(); - let entry_to_expand = match &selected_entry { - PanelEntry::FoldedDirs(FoldedDirsEntry { - entries: dir_entries, - worktree_id, - .. - }) => dir_entries.last().map(|entry| { - buffers_to_unfold.extend(self.buffers_inside_directory(*worktree_id, entry)); - CollapsedEntry::Dir(*worktree_id, entry.id) - }), - PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory { - worktree_id, entry, .. - })) => { - buffers_to_unfold.extend(self.buffers_inside_directory(*worktree_id, entry)); - Some(CollapsedEntry::Dir(*worktree_id, entry.id)) - } - PanelEntry::Fs(FsEntry::File(FsEntryFile { - worktree_id, - buffer_id, - .. - })) => { - buffers_to_unfold.insert(*buffer_id); - Some(CollapsedEntry::File(*worktree_id, *buffer_id)) - } - PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => { - buffers_to_unfold.insert(external_file.buffer_id); - Some(CollapsedEntry::ExternalFile(external_file.buffer_id)) - } - PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => { - Some(CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id)) - } - PanelEntry::Outline(OutlineEntry::Outline(outline)) => Some(CollapsedEntry::Outline( - outline.buffer_id, - outline.excerpt_id, - outline.outline.range.clone(), - )), - PanelEntry::Search(_) => return, - }; - let Some(collapsed_entry) = entry_to_expand else { - return; - }; - let expanded = self.collapsed_entries.remove(&collapsed_entry); - if expanded { - if let CollapsedEntry::Dir(worktree_id, dir_entry_id) = collapsed_entry { - let task = self.project.update(cx, |project, cx| { - project.expand_entry(worktree_id, dir_entry_id, cx) - }); - if let Some(task) = task { - task.detach_and_log_err(cx); - } - }; - - active_editor.update(cx, |editor, cx| { - buffers_to_unfold.retain(|buffer_id| editor.is_buffer_folded(*buffer_id, cx)); - }); - self.select_entry(selected_entry, true, window, cx); - if buffers_to_unfold.is_empty() { - self.update_cached_entries(None, window, cx); - } else { - self.toggle_buffers_fold(buffers_to_unfold, false, window, cx) - .detach(); - } - } else { - self.select_next(&SelectNext, window, cx) - } - } - - fn collapse_selected_entry( - &mut self, - _: &CollapseSelectedEntry, - window: &mut Window, - cx: &mut Context, - ) { - let Some(active_editor) = self.active_editor() else { - return; - }; - let Some(selected_entry) = self.selected_entry().cloned() else { - return; - }; - - let mut buffers_to_fold = HashSet::default(); - let collapsed = match &selected_entry { - PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory { - worktree_id, entry, .. - })) => { - if self - .collapsed_entries - .insert(CollapsedEntry::Dir(*worktree_id, entry.id)) - { - buffers_to_fold.extend(self.buffers_inside_directory(*worktree_id, entry)); - true - } else { - false - } - } - PanelEntry::Fs(FsEntry::File(FsEntryFile { - worktree_id, - buffer_id, - .. - })) => { - if self - .collapsed_entries - .insert(CollapsedEntry::File(*worktree_id, *buffer_id)) - { - buffers_to_fold.insert(*buffer_id); - true - } else { - false - } - } - PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => { - if self - .collapsed_entries - .insert(CollapsedEntry::ExternalFile(external_file.buffer_id)) - { - buffers_to_fold.insert(external_file.buffer_id); - true - } else { - false - } - } - PanelEntry::FoldedDirs(folded_dirs) => { - let mut folded = false; - if let Some(dir_entry) = folded_dirs.entries.last() - && self - .collapsed_entries - .insert(CollapsedEntry::Dir(folded_dirs.worktree_id, dir_entry.id)) - { - folded = true; - buffers_to_fold - .extend(self.buffers_inside_directory(folded_dirs.worktree_id, dir_entry)); - } - folded - } - PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => self - .collapsed_entries - .insert(CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id)), - PanelEntry::Outline(OutlineEntry::Outline(outline)) => { - self.collapsed_entries.insert(CollapsedEntry::Outline( - outline.buffer_id, - outline.excerpt_id, - outline.outline.range.clone(), - )) - } - PanelEntry::Search(_) => false, - }; - - if collapsed { - active_editor.update(cx, |editor, cx| { - buffers_to_fold.retain(|buffer_id| !editor.is_buffer_folded(*buffer_id, cx)); - }); - self.select_entry(selected_entry, true, window, cx); - if buffers_to_fold.is_empty() { - self.update_cached_entries(None, window, cx); - } else { - self.toggle_buffers_fold(buffers_to_fold, true, window, cx) - .detach(); - } - } else { - self.select_parent(&SelectParent, window, cx); - } - } - - pub fn expand_all_entries( - &mut self, - _: &ExpandAllEntries, - window: &mut Window, - cx: &mut Context, - ) { - let Some(active_editor) = self.active_editor() else { - return; - }; - - let mut to_uncollapse: HashSet = HashSet::default(); - let mut buffers_to_unfold: HashSet = HashSet::default(); - - for fs_entry in &self.fs_entries { - match fs_entry { - FsEntry::File(FsEntryFile { - worktree_id, - buffer_id, - .. - }) => { - to_uncollapse.insert(CollapsedEntry::File(*worktree_id, *buffer_id)); - buffers_to_unfold.insert(*buffer_id); - } - FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }) => { - to_uncollapse.insert(CollapsedEntry::ExternalFile(*buffer_id)); - buffers_to_unfold.insert(*buffer_id); - } - FsEntry::Directory(FsEntryDirectory { - worktree_id, entry, .. - }) => { - to_uncollapse.insert(CollapsedEntry::Dir(*worktree_id, entry.id)); - } - } - } - - for (&buffer_id, excerpts) in &self.excerpts { - for (&excerpt_id, excerpt) in excerpts { - match &excerpt.outlines { - ExcerptOutlines::Outlines(outlines) => { - for outline in outlines { - to_uncollapse.insert(CollapsedEntry::Outline( - buffer_id, - excerpt_id, - outline.range.clone(), - )); - } - } - ExcerptOutlines::Invalidated(outlines) => { - for outline in outlines { - to_uncollapse.insert(CollapsedEntry::Outline( - buffer_id, - excerpt_id, - outline.range.clone(), - )); - } - } - ExcerptOutlines::NotFetched => {} - } - to_uncollapse.insert(CollapsedEntry::Excerpt(buffer_id, excerpt_id)); - } - } - - for cached in &self.cached_entries { - if let PanelEntry::FoldedDirs(FoldedDirsEntry { - worktree_id, - entries, - .. - }) = &cached.entry - { - if let Some(last) = entries.last() { - to_uncollapse.insert(CollapsedEntry::Dir(*worktree_id, last.id)); - } - } - } - - self.collapsed_entries - .retain(|entry| !to_uncollapse.contains(entry)); - - active_editor.update(cx, |editor, cx| { - buffers_to_unfold.retain(|buffer_id| editor.is_buffer_folded(*buffer_id, cx)); - }); - - if buffers_to_unfold.is_empty() { - self.update_cached_entries(None, window, cx); - } else { - self.toggle_buffers_fold(buffers_to_unfold, false, window, cx) - .detach(); - } - } - - pub fn collapse_all_entries( - &mut self, - _: &CollapseAllEntries, - window: &mut Window, - cx: &mut Context, - ) { - let Some(active_editor) = self.active_editor() else { - return; - }; - let mut buffers_to_fold = HashSet::default(); - self.collapsed_entries - .extend(self.cached_entries.iter().filter_map( - |cached_entry| match &cached_entry.entry { - PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory { - worktree_id, - entry, - .. - })) => Some(CollapsedEntry::Dir(*worktree_id, entry.id)), - PanelEntry::Fs(FsEntry::File(FsEntryFile { - worktree_id, - buffer_id, - .. - })) => { - buffers_to_fold.insert(*buffer_id); - Some(CollapsedEntry::File(*worktree_id, *buffer_id)) - } - PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => { - buffers_to_fold.insert(external_file.buffer_id); - Some(CollapsedEntry::ExternalFile(external_file.buffer_id)) - } - PanelEntry::FoldedDirs(FoldedDirsEntry { - worktree_id, - entries, - .. - }) => Some(CollapsedEntry::Dir(*worktree_id, entries.last()?.id)), - PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => { - Some(CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id)) - } - PanelEntry::Outline(OutlineEntry::Outline(outline)) => { - Some(CollapsedEntry::Outline( - outline.buffer_id, - outline.excerpt_id, - outline.outline.range.clone(), - )) - } - PanelEntry::Search(_) => None, - }, - )); - - active_editor.update(cx, |editor, cx| { - buffers_to_fold.retain(|buffer_id| !editor.is_buffer_folded(*buffer_id, cx)); - }); - if buffers_to_fold.is_empty() { - self.update_cached_entries(None, window, cx); - } else { - self.toggle_buffers_fold(buffers_to_fold, true, window, cx) - .detach(); - } - } - - fn toggle_expanded(&mut self, entry: &PanelEntry, window: &mut Window, cx: &mut Context) { - let Some(active_editor) = self.active_editor() else { - return; - }; - let mut fold = false; - let mut buffers_to_toggle = HashSet::default(); - match entry { - PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory { - worktree_id, - entry: dir_entry, - .. - })) => { - let entry_id = dir_entry.id; - let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id); - buffers_to_toggle.extend(self.buffers_inside_directory(*worktree_id, dir_entry)); - if self.collapsed_entries.remove(&collapsed_entry) { - self.project - .update(cx, |project, cx| { - project.expand_entry(*worktree_id, entry_id, cx) - }) - .unwrap_or_else(|| Task::ready(Ok(()))) - .detach_and_log_err(cx); - } else { - self.collapsed_entries.insert(collapsed_entry); - fold = true; - } - } - PanelEntry::Fs(FsEntry::File(FsEntryFile { - worktree_id, - buffer_id, - .. - })) => { - let collapsed_entry = CollapsedEntry::File(*worktree_id, *buffer_id); - buffers_to_toggle.insert(*buffer_id); - if !self.collapsed_entries.remove(&collapsed_entry) { - self.collapsed_entries.insert(collapsed_entry); - fold = true; - } - } - PanelEntry::Fs(FsEntry::ExternalFile(external_file)) => { - let collapsed_entry = CollapsedEntry::ExternalFile(external_file.buffer_id); - buffers_to_toggle.insert(external_file.buffer_id); - if !self.collapsed_entries.remove(&collapsed_entry) { - self.collapsed_entries.insert(collapsed_entry); - fold = true; - } - } - PanelEntry::FoldedDirs(FoldedDirsEntry { - worktree_id, - entries: dir_entries, - .. - }) => { - if let Some(dir_entry) = dir_entries.first() { - let entry_id = dir_entry.id; - let collapsed_entry = CollapsedEntry::Dir(*worktree_id, entry_id); - buffers_to_toggle - .extend(self.buffers_inside_directory(*worktree_id, dir_entry)); - if self.collapsed_entries.remove(&collapsed_entry) { - self.project - .update(cx, |project, cx| { - project.expand_entry(*worktree_id, entry_id, cx) - }) - .unwrap_or_else(|| Task::ready(Ok(()))) - .detach_and_log_err(cx); - } else { - self.collapsed_entries.insert(collapsed_entry); - fold = true; - } - } - } - PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => { - let collapsed_entry = CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id); - if !self.collapsed_entries.remove(&collapsed_entry) { - self.collapsed_entries.insert(collapsed_entry); - } - } - PanelEntry::Outline(OutlineEntry::Outline(outline)) => { - let collapsed_entry = CollapsedEntry::Outline( - outline.buffer_id, - outline.excerpt_id, - outline.outline.range.clone(), - ); - if !self.collapsed_entries.remove(&collapsed_entry) { - self.collapsed_entries.insert(collapsed_entry); - } - } - _ => {} - } - - active_editor.update(cx, |editor, cx| { - buffers_to_toggle.retain(|buffer_id| { - let folded = editor.is_buffer_folded(*buffer_id, cx); - if fold { !folded } else { folded } - }); - }); - - self.select_entry(entry.clone(), true, window, cx); - if buffers_to_toggle.is_empty() { - self.update_cached_entries(None, window, cx); - } else { - self.toggle_buffers_fold(buffers_to_toggle, fold, window, cx) - .detach(); - } - } - - fn toggle_buffers_fold( - &self, - buffers: HashSet, - fold: bool, - window: &mut Window, - cx: &mut Context, - ) -> Task<()> { - let Some(active_editor) = self.active_editor() else { - return Task::ready(()); - }; - cx.spawn_in(window, async move |outline_panel, cx| { - outline_panel - .update_in(cx, |outline_panel, window, cx| { - active_editor.update(cx, |editor, cx| { - for buffer_id in buffers { - outline_panel - .preserve_selection_on_buffer_fold_toggles - .insert(buffer_id); - if fold { - editor.fold_buffer(buffer_id, cx); - } else { - editor.unfold_buffer(buffer_id, cx); - } - } - }); - if let Some(selection) = outline_panel.selected_entry().cloned() { - outline_panel.scroll_editor_to_entry(&selection, false, false, window, cx); - } - }) - .ok(); - }) - } - - fn copy_path( - &mut self, - _: &zed_actions::workspace::CopyPath, - _: &mut Window, - cx: &mut Context, - ) { - if let Some(clipboard_text) = self - .selected_entry() - .and_then(|entry| self.abs_path(entry, cx)) - .map(|p| p.to_string_lossy().into_owned()) - { - cx.write_to_clipboard(ClipboardItem::new_string(clipboard_text)); - } - } - - fn copy_relative_path( - &mut self, - _: &zed_actions::workspace::CopyRelativePath, - _: &mut Window, - cx: &mut Context, - ) { - let path_style = self.project.read(cx).path_style(cx); - if let Some(clipboard_text) = self - .selected_entry() - .and_then(|entry| match entry { - PanelEntry::Fs(entry) => self.relative_path(entry, cx), - PanelEntry::FoldedDirs(folded_dirs) => { - folded_dirs.entries.last().map(|entry| entry.path.clone()) - } - PanelEntry::Search(_) | PanelEntry::Outline(..) => None, - }) - .map(|p| p.display(path_style).to_string()) - { - cx.write_to_clipboard(ClipboardItem::new_string(clipboard_text)); - } - } - - fn reveal_in_finder( - &mut self, - _: &RevealInFileManager, - _: &mut Window, - cx: &mut Context, - ) { - if let Some(abs_path) = self - .selected_entry() - .and_then(|entry| self.abs_path(entry, cx)) - { - cx.reveal_path(&abs_path); - } - } - - fn open_in_terminal( - &mut self, - _: &OpenInTerminal, - window: &mut Window, - cx: &mut Context, - ) { - let selected_entry = self.selected_entry(); - let abs_path = selected_entry.and_then(|entry| self.abs_path(entry, cx)); - let working_directory = if let ( - Some(abs_path), - Some(PanelEntry::Fs(FsEntry::File(..) | FsEntry::ExternalFile(..))), - ) = (&abs_path, selected_entry) - { - abs_path.parent().map(|p| p.to_owned()) - } else { - abs_path - }; - - if let Some(working_directory) = working_directory { - window.dispatch_action( - workspace::OpenTerminal { working_directory }.boxed_clone(), - cx, - ) - } - } - - fn reveal_entry_for_selection( - &mut self, - editor: Entity, - window: &mut Window, - cx: &mut Context, - ) { - if !self.active - || !OutlinePanelSettings::get_global(cx).auto_reveal_entries - || self.focus_handle.contains_focused(window, cx) - { - return; - } - let project = self.project.clone(); - self.reveal_selection_task = cx.spawn_in(window, async move |outline_panel, cx| { - cx.background_executor().timer(UPDATE_DEBOUNCE).await; - let entry_with_selection = - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.location_for_editor_selection(&editor, window, cx) - })?; - let Some(entry_with_selection) = entry_with_selection else { - outline_panel.update(cx, |outline_panel, cx| { - outline_panel.selected_entry = SelectedEntry::None; - cx.notify(); - })?; - return Ok(()); - }; - let related_buffer_entry = match &entry_with_selection { - PanelEntry::Fs(FsEntry::File(FsEntryFile { - worktree_id, - buffer_id, - .. - })) => project.update(cx, |project, cx| { - let entry_id = project - .buffer_for_id(*buffer_id, cx) - .and_then(|buffer| buffer.read(cx).entry_id(cx)); - project - .worktree_for_id(*worktree_id, cx) - .zip(entry_id) - .and_then(|(worktree, entry_id)| { - let entry = worktree.read(cx).entry_for_id(entry_id)?.clone(); - Some((worktree, entry)) - }) - })?, - PanelEntry::Outline(outline_entry) => { - let (buffer_id, excerpt_id) = outline_entry.ids(); - outline_panel.update(cx, |outline_panel, cx| { - outline_panel - .collapsed_entries - .remove(&CollapsedEntry::ExternalFile(buffer_id)); - outline_panel - .collapsed_entries - .remove(&CollapsedEntry::Excerpt(buffer_id, excerpt_id)); - let project = outline_panel.project.read(cx); - let entry_id = project - .buffer_for_id(buffer_id, cx) - .and_then(|buffer| buffer.read(cx).entry_id(cx)); - - entry_id.and_then(|entry_id| { - project - .worktree_for_entry(entry_id, cx) - .and_then(|worktree| { - let worktree_id = worktree.read(cx).id(); - outline_panel - .collapsed_entries - .remove(&CollapsedEntry::File(worktree_id, buffer_id)); - let entry = worktree.read(cx).entry_for_id(entry_id)?.clone(); - Some((worktree, entry)) - }) - }) - })? - } - PanelEntry::Fs(FsEntry::ExternalFile(..)) => None, - PanelEntry::Search(SearchEntry { match_range, .. }) => match_range - .start - .text_anchor - .buffer_id - .or(match_range.end.text_anchor.buffer_id) - .map(|buffer_id| { - outline_panel.update(cx, |outline_panel, cx| { - outline_panel - .collapsed_entries - .remove(&CollapsedEntry::ExternalFile(buffer_id)); - let project = project.read(cx); - let entry_id = project - .buffer_for_id(buffer_id, cx) - .and_then(|buffer| buffer.read(cx).entry_id(cx)); - - entry_id.and_then(|entry_id| { - project - .worktree_for_entry(entry_id, cx) - .and_then(|worktree| { - let worktree_id = worktree.read(cx).id(); - outline_panel - .collapsed_entries - .remove(&CollapsedEntry::File(worktree_id, buffer_id)); - let entry = - worktree.read(cx).entry_for_id(entry_id)?.clone(); - Some((worktree, entry)) - }) - }) - }) - }) - .transpose()? - .flatten(), - _ => return anyhow::Ok(()), - }; - if let Some((worktree, buffer_entry)) = related_buffer_entry { - outline_panel.update(cx, |outline_panel, cx| { - let worktree_id = worktree.read(cx).id(); - let mut dirs_to_expand = Vec::new(); - { - let mut traversal = worktree.read(cx).traverse_from_path( - true, - true, - true, - buffer_entry.path.as_ref(), - ); - let mut current_entry = buffer_entry; - loop { - if current_entry.is_dir() - && outline_panel - .collapsed_entries - .remove(&CollapsedEntry::Dir(worktree_id, current_entry.id)) - { - dirs_to_expand.push(current_entry.id); - } - - if traversal.back_to_parent() - && let Some(parent_entry) = traversal.entry() - { - current_entry = parent_entry.clone(); - continue; - } - break; - } - } - for dir_to_expand in dirs_to_expand { - project - .update(cx, |project, cx| { - project.expand_entry(worktree_id, dir_to_expand, cx) - }) - .unwrap_or_else(|| Task::ready(Ok(()))) - .detach_and_log_err(cx) - } - })? - } - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.select_entry(entry_with_selection, false, window, cx); - outline_panel.update_cached_entries(None, window, cx); - })?; - - anyhow::Ok(()) - }); - } - - fn render_excerpt( - &self, - excerpt: &OutlineEntryExcerpt, - depth: usize, - window: &mut Window, - cx: &mut Context, - ) -> Option> { - let item_id = ElementId::from(excerpt.id.to_proto() as usize); - let is_active = match self.selected_entry() { - Some(PanelEntry::Outline(OutlineEntry::Excerpt(selected_excerpt))) => { - selected_excerpt.buffer_id == excerpt.buffer_id && selected_excerpt.id == excerpt.id - } - _ => false, - }; - let has_outlines = self - .excerpts - .get(&excerpt.buffer_id) - .and_then(|excerpts| match &excerpts.get(&excerpt.id)?.outlines { - ExcerptOutlines::Outlines(outlines) => Some(outlines), - ExcerptOutlines::Invalidated(outlines) => Some(outlines), - ExcerptOutlines::NotFetched => None, - }) - .is_some_and(|outlines| !outlines.is_empty()); - let is_expanded = !self - .collapsed_entries - .contains(&CollapsedEntry::Excerpt(excerpt.buffer_id, excerpt.id)); - let color = entry_label_color(is_active); - let icon = if has_outlines { - FileIcons::get_chevron_icon(is_expanded, cx) - .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element()) - } else { - None - } - .unwrap_or_else(empty_icon); - - let label = self.excerpt_label(excerpt.buffer_id, &excerpt.range, cx)?; - let label_element = Label::new(label) - .single_line() - .color(color) - .into_any_element(); - - Some(self.entry_element( - PanelEntry::Outline(OutlineEntry::Excerpt(excerpt.clone())), - item_id, - depth, - icon, - is_active, - label_element, - window, - cx, - )) - } - - fn excerpt_label( - &self, - buffer_id: BufferId, - range: &ExcerptRange, - cx: &App, - ) -> Option { - let buffer_snapshot = self.buffer_snapshot_for_id(buffer_id, cx)?; - let excerpt_range = range.context.to_point(&buffer_snapshot); - Some(format!( - "Lines {}- {}", - excerpt_range.start.row + 1, - excerpt_range.end.row + 1, - )) - } - - fn render_outline( - &self, - outline: &OutlineEntryOutline, - depth: usize, - string_match: Option<&StringMatch>, - window: &mut Window, - cx: &mut Context, - ) -> Stateful
{ - let item_id = ElementId::from(SharedString::from(format!( - "{:?}|{:?}{:?}|{:?}", - outline.buffer_id, outline.excerpt_id, outline.outline.range, &outline.outline.text, - ))); - - let label_element = outline::render_item( - &outline.outline, - string_match - .map(|string_match| string_match.ranges().collect::>()) - .unwrap_or_default(), - cx, - ) - .into_any_element(); - - let is_active = match self.selected_entry() { - Some(PanelEntry::Outline(OutlineEntry::Outline(selected))) => { - outline == selected && outline.outline == selected.outline - } - _ => false, - }; - - let has_children = self - .outline_children_cache - .get(&outline.buffer_id) - .and_then(|children_map| { - let key = (outline.outline.range.clone(), outline.outline.depth); - children_map.get(&key) - }) - .copied() - .unwrap_or(false); - let is_expanded = !self.collapsed_entries.contains(&CollapsedEntry::Outline( - outline.buffer_id, - outline.excerpt_id, - outline.outline.range.clone(), - )); - - let icon = if has_children { - FileIcons::get_chevron_icon(is_expanded, cx) - .map(|icon_path| { - Icon::from_path(icon_path) - .color(entry_label_color(is_active)) - .into_any_element() - }) - .unwrap_or_else(empty_icon) - } else { - empty_icon() - }; - - self.entry_element( - PanelEntry::Outline(OutlineEntry::Outline(outline.clone())), - item_id, - depth, - icon, - is_active, - label_element, - window, - cx, - ) - } - - fn render_entry( - &self, - rendered_entry: &FsEntry, - depth: usize, - string_match: Option<&StringMatch>, - window: &mut Window, - cx: &mut Context, - ) -> Stateful
{ - let settings = OutlinePanelSettings::get_global(cx); - let is_active = match self.selected_entry() { - Some(PanelEntry::Fs(selected_entry)) => selected_entry == rendered_entry, - _ => false, - }; - let (item_id, label_element, icon) = match rendered_entry { - FsEntry::File(FsEntryFile { - worktree_id, entry, .. - }) => { - let name = self.entry_name(worktree_id, entry, cx); - let color = - entry_git_aware_label_color(entry.git_summary, entry.is_ignored, is_active); - let icon = if settings.file_icons { - FileIcons::get_icon(entry.path.as_std_path(), cx) - .map(|icon_path| Icon::from_path(icon_path).color(color).into_any_element()) - } else { - None - }; - ( - ElementId::from(entry.id.to_proto() as usize), - HighlightedLabel::new( - name, - string_match - .map(|string_match| string_match.positions.clone()) - .unwrap_or_default(), - ) - .color(color) - .into_any_element(), - icon.unwrap_or_else(empty_icon), - ) - } - FsEntry::Directory(directory) => { - let name = self.entry_name(&directory.worktree_id, &directory.entry, cx); - - let is_expanded = !self.collapsed_entries.contains(&CollapsedEntry::Dir( - directory.worktree_id, - directory.entry.id, - )); - let color = entry_git_aware_label_color( - directory.entry.git_summary, - directory.entry.is_ignored, - is_active, - ); - let icon = if settings.folder_icons { - FileIcons::get_folder_icon(is_expanded, directory.entry.path.as_std_path(), cx) - } else { - FileIcons::get_chevron_icon(is_expanded, cx) - } - .map(Icon::from_path) - .map(|icon| icon.color(color).into_any_element()); - ( - ElementId::from(directory.entry.id.to_proto() as usize), - HighlightedLabel::new( - name, - string_match - .map(|string_match| string_match.positions.clone()) - .unwrap_or_default(), - ) - .color(color) - .into_any_element(), - icon.unwrap_or_else(empty_icon), - ) - } - FsEntry::ExternalFile(external_file) => { - let color = entry_label_color(is_active); - let (icon, name) = match self.buffer_snapshot_for_id(external_file.buffer_id, cx) { - Some(buffer_snapshot) => match buffer_snapshot.file() { - Some(file) => { - let path = file.path(); - let icon = if settings.file_icons { - FileIcons::get_icon(path.as_std_path(), cx) - } else { - None - } - .map(Icon::from_path) - .map(|icon| icon.color(color).into_any_element()); - (icon, file_name(path.as_std_path())) - } - None => (None, "Untitled".to_string()), - }, - None => (None, "Unknown buffer".to_string()), - }; - ( - ElementId::from(external_file.buffer_id.to_proto() as usize), - HighlightedLabel::new( - name, - string_match - .map(|string_match| string_match.positions.clone()) - .unwrap_or_default(), - ) - .color(color) - .into_any_element(), - icon.unwrap_or_else(empty_icon), - ) - } - }; - - self.entry_element( - PanelEntry::Fs(rendered_entry.clone()), - item_id, - depth, - icon, - is_active, - label_element, - window, - cx, - ) - } - - fn render_folded_dirs( - &self, - folded_dir: &FoldedDirsEntry, - depth: usize, - string_match: Option<&StringMatch>, - window: &mut Window, - cx: &mut Context, - ) -> Stateful
{ - let settings = OutlinePanelSettings::get_global(cx); - let is_active = match self.selected_entry() { - Some(PanelEntry::FoldedDirs(selected_dirs)) => { - selected_dirs.worktree_id == folded_dir.worktree_id - && selected_dirs.entries == folded_dir.entries - } - _ => false, - }; - let (item_id, label_element, icon) = { - let name = self.dir_names_string(&folded_dir.entries, folded_dir.worktree_id, cx); - - let is_expanded = folded_dir.entries.iter().all(|dir| { - !self - .collapsed_entries - .contains(&CollapsedEntry::Dir(folded_dir.worktree_id, dir.id)) - }); - let is_ignored = folded_dir.entries.iter().any(|entry| entry.is_ignored); - let git_status = folded_dir - .entries - .first() - .map(|entry| entry.git_summary) - .unwrap_or_default(); - let color = entry_git_aware_label_color(git_status, is_ignored, is_active); - let icon = if settings.folder_icons { - FileIcons::get_folder_icon(is_expanded, &Path::new(&name), cx) - } else { - FileIcons::get_chevron_icon(is_expanded, cx) - } - .map(Icon::from_path) - .map(|icon| icon.color(color).into_any_element()); - ( - ElementId::from( - folded_dir - .entries - .last() - .map(|entry| entry.id.to_proto()) - .unwrap_or_else(|| folded_dir.worktree_id.to_proto()) - as usize, - ), - HighlightedLabel::new( - name, - string_match - .map(|string_match| string_match.positions.clone()) - .unwrap_or_default(), - ) - .color(color) - .into_any_element(), - icon.unwrap_or_else(empty_icon), - ) - }; - - self.entry_element( - PanelEntry::FoldedDirs(folded_dir.clone()), - item_id, - depth, - icon, - is_active, - label_element, - window, - cx, - ) - } - - fn render_search_match( - &mut self, - multi_buffer_snapshot: Option<&MultiBufferSnapshot>, - match_range: &Range, - render_data: &Arc>, - kind: SearchKind, - depth: usize, - string_match: Option<&StringMatch>, - window: &mut Window, - cx: &mut Context, - ) -> Option> { - let search_data = match render_data.get() { - Some(search_data) => search_data, - None => { - if let ItemsDisplayMode::Search(search_state) = &mut self.mode - && let Some(multi_buffer_snapshot) = multi_buffer_snapshot - { - search_state - .highlight_search_match_tx - .try_send(HighlightArguments { - multi_buffer_snapshot: multi_buffer_snapshot.clone(), - match_range: match_range.clone(), - search_data: Arc::clone(render_data), - }) - .ok(); - } - return None; - } - }; - let search_matches = string_match - .iter() - .flat_map(|string_match| string_match.ranges()) - .collect::>(); - let match_ranges = if search_matches.is_empty() { - &search_data.search_match_indices - } else { - &search_matches - }; - let label_element = outline::render_item( - &OutlineItem { - depth, - annotation_range: None, - range: search_data.context_range.clone(), - text: search_data.context_text.clone(), - source_range_for_text: search_data.context_range.clone(), - highlight_ranges: search_data - .highlights_data - .get() - .cloned() - .unwrap_or_default(), - name_ranges: search_data.search_match_indices.clone(), - body_range: Some(search_data.context_range.clone()), - }, - match_ranges.iter().cloned(), - cx, - ); - let truncated_contents_label = || Label::new(TRUNCATED_CONTEXT_MARK); - let entire_label = h_flex() - .justify_center() - .p_0() - .when(search_data.truncated_left, |parent| { - parent.child(truncated_contents_label()) - }) - .child(label_element) - .when(search_data.truncated_right, |parent| { - parent.child(truncated_contents_label()) - }) - .into_any_element(); - - let is_active = match self.selected_entry() { - Some(PanelEntry::Search(SearchEntry { - match_range: selected_match_range, - .. - })) => match_range == selected_match_range, - _ => false, - }; - Some(self.entry_element( - PanelEntry::Search(SearchEntry { - kind, - match_range: match_range.clone(), - render_data: render_data.clone(), - }), - ElementId::from(SharedString::from(format!("search-{match_range:?}"))), - depth, - empty_icon(), - is_active, - entire_label, - window, - cx, - )) - } - - fn entry_element( - &self, - rendered_entry: PanelEntry, - item_id: ElementId, - depth: usize, - icon_element: AnyElement, - is_active: bool, - label_element: gpui::AnyElement, - window: &mut Window, - cx: &mut Context, - ) -> Stateful
{ - let settings = OutlinePanelSettings::get_global(cx); - div() - .text_ui(cx) - .id(item_id.clone()) - .on_click({ - let clicked_entry = rendered_entry.clone(); - cx.listener(move |outline_panel, event: &gpui::ClickEvent, window, cx| { - if event.is_right_click() || event.first_focus() { - return; - } - - let change_focus = event.click_count() > 1; - outline_panel.toggle_expanded(&clicked_entry, window, cx); - - outline_panel.scroll_editor_to_entry( - &clicked_entry, - true, - change_focus, - window, - cx, - ); - }) - }) - .cursor_pointer() - .child( - ListItem::new(item_id) - .indent_level(depth) - .indent_step_size(px(settings.indent_size)) - .toggle_state(is_active) - .child( - h_flex() - .child(h_flex().w(px(16.)).justify_center().child(icon_element)) - .child(h_flex().h_6().child(label_element).ml_1()), - ) - .on_secondary_mouse_down(cx.listener( - move |outline_panel, event: &MouseDownEvent, window, cx| { - // Stop propagation to prevent the catch-all context menu for the project - // panel from being deployed. - cx.stop_propagation(); - outline_panel.deploy_context_menu( - event.position, - rendered_entry.clone(), - window, - cx, - ) - }, - )), - ) - .border_1() - .border_r_2() - .rounded_none() - .hover(|style| { - if is_active { - style - } else { - let hover_color = cx.theme().colors().ghost_element_hover; - style.bg(hover_color).border_color(hover_color) - } - }) - .when( - is_active && self.focus_handle.contains_focused(window, cx), - |div| div.border_color(Color::Selected.color(cx)), - ) - } - - fn entry_name(&self, worktree_id: &WorktreeId, entry: &Entry, cx: &App) -> String { - match self.project.read(cx).worktree_for_id(*worktree_id, cx) { - Some(worktree) => { - let worktree = worktree.read(cx); - match worktree.snapshot().root_entry() { - Some(root_entry) => { - if root_entry.id == entry.id { - file_name(worktree.abs_path().as_ref()) - } else { - let path = worktree.absolutize(entry.path.as_ref()); - file_name(&path) - } - } - None => { - let path = worktree.absolutize(entry.path.as_ref()); - file_name(&path) - } - } - } - None => file_name(entry.path.as_std_path()), - } - } - - fn update_fs_entries( - &mut self, - active_editor: Entity, - debounce: Option, - window: &mut Window, - cx: &mut Context, - ) { - if !self.active { - return; - } - - let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs; - let active_multi_buffer = active_editor.read(cx).buffer().clone(); - let new_entries = self.new_entries_for_fs_update.clone(); - let repo_snapshots = self.project.update(cx, |project, cx| { - project.git_store().read(cx).repo_snapshots(cx) - }); - self.fs_entries_update_task = cx.spawn_in(window, async move |outline_panel, cx| { - if let Some(debounce) = debounce { - cx.background_executor().timer(debounce).await; - } - - let mut new_collapsed_entries = HashSet::default(); - let mut new_unfolded_dirs = HashMap::default(); - let mut root_entries = HashSet::default(); - let mut new_excerpts = HashMap::>::default(); - let Ok(buffer_excerpts) = outline_panel.update(cx, |outline_panel, cx| { - let git_store = outline_panel.project.read(cx).git_store().clone(); - new_collapsed_entries = outline_panel.collapsed_entries.clone(); - new_unfolded_dirs = outline_panel.unfolded_dirs.clone(); - let multi_buffer_snapshot = active_multi_buffer.read(cx).snapshot(cx); - - multi_buffer_snapshot.excerpts().fold( - HashMap::default(), - |mut buffer_excerpts, (excerpt_id, buffer_snapshot, excerpt_range)| { - let buffer_id = buffer_snapshot.remote_id(); - let file = File::from_dyn(buffer_snapshot.file()); - let entry_id = file.and_then(|file| file.project_entry_id()); - let worktree = file.map(|file| file.worktree.read(cx).snapshot()); - let is_new = new_entries.contains(&excerpt_id) - || !outline_panel.excerpts.contains_key(&buffer_id); - let is_folded = active_editor.read(cx).is_buffer_folded(buffer_id, cx); - let status = git_store - .read(cx) - .repository_and_path_for_buffer_id(buffer_id, cx) - .and_then(|(repo, path)| { - Some(repo.read(cx).status_for_path(&path)?.status) - }); - buffer_excerpts - .entry(buffer_id) - .or_insert_with(|| { - (is_new, is_folded, Vec::new(), entry_id, worktree, status) - }) - .2 - .push(excerpt_id); - - let outlines = match outline_panel - .excerpts - .get(&buffer_id) - .and_then(|excerpts| excerpts.get(&excerpt_id)) - { - Some(old_excerpt) => match &old_excerpt.outlines { - ExcerptOutlines::Outlines(outlines) => { - ExcerptOutlines::Outlines(outlines.clone()) - } - ExcerptOutlines::Invalidated(_) => ExcerptOutlines::NotFetched, - ExcerptOutlines::NotFetched => ExcerptOutlines::NotFetched, - }, - None => ExcerptOutlines::NotFetched, - }; - new_excerpts.entry(buffer_id).or_default().insert( - excerpt_id, - Excerpt { - range: excerpt_range, - outlines, - }, - ); - buffer_excerpts - }, - ) - }) else { - return; - }; - - let Some(( - new_collapsed_entries, - new_unfolded_dirs, - new_fs_entries, - new_depth_map, - new_children_count, - )) = cx - .background_spawn(async move { - let mut processed_external_buffers = HashSet::default(); - let mut new_worktree_entries = - BTreeMap::>::default(); - let mut worktree_excerpts = HashMap::< - WorktreeId, - HashMap)>, - >::default(); - let mut external_excerpts = HashMap::default(); - - for (buffer_id, (is_new, is_folded, excerpts, entry_id, worktree, status)) in - buffer_excerpts - { - if is_folded { - match &worktree { - Some(worktree) => { - new_collapsed_entries - .insert(CollapsedEntry::File(worktree.id(), buffer_id)); - } - None => { - new_collapsed_entries - .insert(CollapsedEntry::ExternalFile(buffer_id)); - } - } - } else if is_new { - match &worktree { - Some(worktree) => { - new_collapsed_entries - .remove(&CollapsedEntry::File(worktree.id(), buffer_id)); - } - None => { - new_collapsed_entries - .remove(&CollapsedEntry::ExternalFile(buffer_id)); - } - } - } - - if let Some(worktree) = worktree { - let worktree_id = worktree.id(); - let unfolded_dirs = new_unfolded_dirs.entry(worktree_id).or_default(); - - match entry_id.and_then(|id| worktree.entry_for_id(id)).cloned() { - Some(entry) => { - let entry = GitEntry { - git_summary: status - .map(|status| status.summary()) - .unwrap_or_default(), - entry, - }; - let mut traversal = GitTraversal::new( - &repo_snapshots, - worktree.traverse_from_path( - true, - true, - true, - entry.path.as_ref(), - ), - ); - - let mut entries_to_add = HashMap::default(); - worktree_excerpts - .entry(worktree_id) - .or_default() - .insert(entry.id, (buffer_id, excerpts)); - let mut current_entry = entry; - loop { - if current_entry.is_dir() { - let is_root = - worktree.root_entry().map(|entry| entry.id) - == Some(current_entry.id); - if is_root { - root_entries.insert(current_entry.id); - if auto_fold_dirs { - unfolded_dirs.insert(current_entry.id); - } - } - if is_new { - new_collapsed_entries.remove(&CollapsedEntry::Dir( - worktree_id, - current_entry.id, - )); - } - } - - let new_entry_added = entries_to_add - .insert(current_entry.id, current_entry) - .is_none(); - if new_entry_added - && traversal.back_to_parent() - && let Some(parent_entry) = traversal.entry() - { - current_entry = parent_entry.to_owned(); - continue; - } - break; - } - new_worktree_entries - .entry(worktree_id) - .or_insert_with(HashMap::default) - .extend(entries_to_add); - } - None => { - if processed_external_buffers.insert(buffer_id) { - external_excerpts - .entry(buffer_id) - .or_insert_with(Vec::new) - .extend(excerpts); - } - } - } - } else if processed_external_buffers.insert(buffer_id) { - external_excerpts - .entry(buffer_id) - .or_insert_with(Vec::new) - .extend(excerpts); - } - } - - let mut new_children_count = - HashMap::, FsChildren>>::default(); - - let worktree_entries = new_worktree_entries - .into_iter() - .map(|(worktree_id, entries)| { - let mut entries = entries.into_values().collect::>(); - entries.sort_by(|a, b| a.path.as_ref().cmp(b.path.as_ref())); - (worktree_id, entries) - }) - .flat_map(|(worktree_id, entries)| { - { - entries - .into_iter() - .filter_map(|entry| { - if auto_fold_dirs && let Some(parent) = entry.path.parent() - { - let children = new_children_count - .entry(worktree_id) - .or_default() - .entry(Arc::from(parent)) - .or_default(); - if entry.is_dir() { - children.dirs += 1; - } else { - children.files += 1; - } - } - - if entry.is_dir() { - Some(FsEntry::Directory(FsEntryDirectory { - worktree_id, - entry, - })) - } else { - let (buffer_id, excerpts) = worktree_excerpts - .get_mut(&worktree_id) - .and_then(|worktree_excerpts| { - worktree_excerpts.remove(&entry.id) - })?; - Some(FsEntry::File(FsEntryFile { - worktree_id, - buffer_id, - entry, - excerpts, - })) - } - }) - .collect::>() - } - }) - .collect::>(); - - let mut visited_dirs = Vec::new(); - let mut new_depth_map = HashMap::default(); - let new_visible_entries = external_excerpts - .into_iter() - .sorted_by_key(|(id, _)| *id) - .map(|(buffer_id, excerpts)| { - FsEntry::ExternalFile(FsEntryExternalFile { - buffer_id, - excerpts, - }) - }) - .chain(worktree_entries) - .filter(|visible_item| { - match visible_item { - FsEntry::Directory(directory) => { - let parent_id = back_to_common_visited_parent( - &mut visited_dirs, - &directory.worktree_id, - &directory.entry, - ); - - let mut depth = 0; - if !root_entries.contains(&directory.entry.id) { - if auto_fold_dirs { - let children = new_children_count - .get(&directory.worktree_id) - .and_then(|children_count| { - children_count.get(&directory.entry.path) - }) - .copied() - .unwrap_or_default(); - - if !children.may_be_fold_part() - || (children.dirs == 0 - && visited_dirs - .last() - .map(|(parent_dir_id, _)| { - new_unfolded_dirs - .get(&directory.worktree_id) - .is_none_or(|unfolded_dirs| { - unfolded_dirs - .contains(parent_dir_id) - }) - }) - .unwrap_or(true)) - { - new_unfolded_dirs - .entry(directory.worktree_id) - .or_default() - .insert(directory.entry.id); - } - } - - depth = parent_id - .and_then(|(worktree_id, id)| { - new_depth_map.get(&(worktree_id, id)).copied() - }) - .unwrap_or(0) - + 1; - }; - visited_dirs - .push((directory.entry.id, directory.entry.path.clone())); - new_depth_map - .insert((directory.worktree_id, directory.entry.id), depth); - } - FsEntry::File(FsEntryFile { - worktree_id, - entry: file_entry, - .. - }) => { - let parent_id = back_to_common_visited_parent( - &mut visited_dirs, - worktree_id, - file_entry, - ); - let depth = if root_entries.contains(&file_entry.id) { - 0 - } else { - parent_id - .and_then(|(worktree_id, id)| { - new_depth_map.get(&(worktree_id, id)).copied() - }) - .unwrap_or(0) - + 1 - }; - new_depth_map.insert((*worktree_id, file_entry.id), depth); - } - FsEntry::ExternalFile(..) => { - visited_dirs.clear(); - } - } - - true - }) - .collect::>(); - - anyhow::Ok(( - new_collapsed_entries, - new_unfolded_dirs, - new_visible_entries, - new_depth_map, - new_children_count, - )) - }) - .await - .log_err() - else { - return; - }; - - outline_panel - .update_in(cx, |outline_panel, window, cx| { - outline_panel.new_entries_for_fs_update.clear(); - outline_panel.excerpts = new_excerpts; - outline_panel.collapsed_entries = new_collapsed_entries; - outline_panel.unfolded_dirs = new_unfolded_dirs; - outline_panel.fs_entries = new_fs_entries; - outline_panel.fs_entries_depth = new_depth_map; - outline_panel.fs_children_count = new_children_count; - outline_panel.update_non_fs_items(window, cx); - - // Only update cached entries if we don't have outlines to fetch - // If we do have outlines to fetch, let fetch_outdated_outlines handle the update - if outline_panel.excerpt_fetch_ranges(cx).is_empty() { - outline_panel.update_cached_entries(debounce, window, cx); - } - - cx.notify(); - }) - .ok(); - }); - } - - fn replace_active_editor( - &mut self, - new_active_item: Box, - new_active_editor: Entity, - window: &mut Window, - cx: &mut Context, - ) { - self.clear_previous(window, cx); - - let default_expansion_depth = - OutlinePanelSettings::get_global(cx).expand_outlines_with_depth; - // We'll apply the expansion depth after outlines are loaded - self.pending_default_expansion_depth = Some(default_expansion_depth); - - let buffer_search_subscription = cx.subscribe_in( - &new_active_editor, - window, - |outline_panel: &mut Self, - _, - e: &SearchEvent, - window: &mut Window, - cx: &mut Context| { - if matches!(e, SearchEvent::MatchesInvalidated) { - let update_cached_items = outline_panel.update_search_matches(window, cx); - if update_cached_items { - outline_panel.selected_entry.invalidate(); - outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx); - } - }; - outline_panel.autoscroll(cx); - }, - ); - self.active_item = Some(ActiveItem { - _buffer_search_subscription: buffer_search_subscription, - _editor_subscription: subscribe_for_editor_events(&new_active_editor, window, cx), - item_handle: new_active_item.downgrade_item(), - active_editor: new_active_editor.downgrade(), - }); - self.new_entries_for_fs_update - .extend(new_active_editor.read(cx).buffer().read(cx).excerpt_ids()); - self.selected_entry.invalidate(); - self.update_fs_entries(new_active_editor, None, window, cx); - } - - fn clear_previous(&mut self, window: &mut Window, cx: &mut App) { - self.fs_entries_update_task = Task::ready(()); - self.outline_fetch_tasks.clear(); - self.cached_entries_update_task = Task::ready(()); - self.reveal_selection_task = Task::ready(Ok(())); - self.filter_editor - .update(cx, |editor, cx| editor.clear(window, cx)); - self.collapsed_entries.clear(); - self.unfolded_dirs.clear(); - self.active_item = None; - self.fs_entries.clear(); - self.fs_entries_depth.clear(); - self.fs_children_count.clear(); - self.excerpts.clear(); - self.cached_entries = Vec::new(); - self.selected_entry = SelectedEntry::None; - self.pinned = false; - self.mode = ItemsDisplayMode::Outline; - self.pending_default_expansion_depth = None; - } - - fn location_for_editor_selection( - &self, - editor: &Entity, - window: &mut Window, - cx: &mut Context, - ) -> Option { - let selection = editor.update(cx, |editor, cx| { - editor - .selections - .newest::(&editor.display_snapshot(cx)) - .head() - }); - let editor_snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx)); - let multi_buffer = editor.read(cx).buffer(); - let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx); - let (excerpt_id, buffer, _) = editor - .read(cx) - .buffer() - .read(cx) - .excerpt_containing(selection, cx)?; - let buffer_id = buffer.read(cx).remote_id(); - - if editor.read(cx).is_buffer_folded(buffer_id, cx) { - return self - .fs_entries - .iter() - .find(|fs_entry| match fs_entry { - FsEntry::Directory(..) => false, - FsEntry::File(FsEntryFile { - buffer_id: other_buffer_id, - .. - }) - | FsEntry::ExternalFile(FsEntryExternalFile { - buffer_id: other_buffer_id, - .. - }) => buffer_id == *other_buffer_id, - }) - .cloned() - .map(PanelEntry::Fs); - } - - let selection_display_point = selection.to_display_point(&editor_snapshot); - - match &self.mode { - ItemsDisplayMode::Search(search_state) => search_state - .matches - .iter() - .rev() - .min_by_key(|&(match_range, _)| { - let match_display_range = - match_range.clone().to_display_points(&editor_snapshot); - let start_distance = if selection_display_point < match_display_range.start { - match_display_range.start - selection_display_point - } else { - selection_display_point - match_display_range.start - }; - let end_distance = if selection_display_point < match_display_range.end { - match_display_range.end - selection_display_point - } else { - selection_display_point - match_display_range.end - }; - start_distance + end_distance - }) - .and_then(|(closest_range, _)| { - self.cached_entries.iter().find_map(|cached_entry| { - if let PanelEntry::Search(SearchEntry { match_range, .. }) = - &cached_entry.entry - { - if match_range == closest_range { - Some(cached_entry.entry.clone()) - } else { - None - } - } else { - None - } - }) - }), - ItemsDisplayMode::Outline => self.outline_location( - buffer_id, - excerpt_id, - multi_buffer_snapshot, - editor_snapshot, - selection_display_point, - ), - } - } - - fn outline_location( - &self, - buffer_id: BufferId, - excerpt_id: ExcerptId, - multi_buffer_snapshot: editor::MultiBufferSnapshot, - editor_snapshot: editor::EditorSnapshot, - selection_display_point: DisplayPoint, - ) -> Option { - let excerpt_outlines = self - .excerpts - .get(&buffer_id) - .and_then(|excerpts| excerpts.get(&excerpt_id)) - .into_iter() - .flat_map(|excerpt| excerpt.iter_outlines()) - .flat_map(|outline| { - let range = multi_buffer_snapshot - .anchor_range_in_excerpt(excerpt_id, outline.range.clone())?; - Some(( - range.start.to_display_point(&editor_snapshot) - ..range.end.to_display_point(&editor_snapshot), - outline, - )) - }) - .collect::>(); - - let mut matching_outline_indices = Vec::new(); - let mut children = HashMap::default(); - let mut parents_stack = Vec::<(&Range, &&Outline, usize)>::new(); - - for (i, (outline_range, outline)) in excerpt_outlines.iter().enumerate() { - if outline_range - .to_inclusive() - .contains(&selection_display_point) - { - matching_outline_indices.push(i); - } else if (outline_range.start.row()..outline_range.end.row()) - .to_inclusive() - .contains(&selection_display_point.row()) - { - matching_outline_indices.push(i); - } - - while let Some((parent_range, parent_outline, _)) = parents_stack.last() { - if parent_outline.depth >= outline.depth - || !parent_range.contains(&outline_range.start) - { - parents_stack.pop(); - } else { - break; - } - } - if let Some((_, _, parent_index)) = parents_stack.last_mut() { - children - .entry(*parent_index) - .or_insert_with(Vec::new) - .push(i); - } - parents_stack.push((outline_range, outline, i)); - } - - let outline_item = matching_outline_indices - .into_iter() - .flat_map(|i| Some((i, excerpt_outlines.get(i)?))) - .filter(|(i, _)| { - children - .get(i) - .map(|children| { - children.iter().all(|child_index| { - excerpt_outlines - .get(*child_index) - .map(|(child_range, _)| child_range.start > selection_display_point) - .unwrap_or(false) - }) - }) - .unwrap_or(true) - }) - .min_by_key(|(_, (outline_range, outline))| { - let distance_from_start = if outline_range.start > selection_display_point { - outline_range.start - selection_display_point - } else { - selection_display_point - outline_range.start - }; - let distance_from_end = if outline_range.end > selection_display_point { - outline_range.end - selection_display_point - } else { - selection_display_point - outline_range.end - }; - - ( - cmp::Reverse(outline.depth), - distance_from_start + distance_from_end, - ) - }) - .map(|(_, (_, outline))| *outline) - .cloned(); - - let closest_container = match outline_item { - Some(outline) => PanelEntry::Outline(OutlineEntry::Outline(OutlineEntryOutline { - buffer_id, - excerpt_id, - outline, - })), - None => { - self.cached_entries.iter().rev().find_map(|cached_entry| { - match &cached_entry.entry { - PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => { - if excerpt.buffer_id == buffer_id && excerpt.id == excerpt_id { - Some(cached_entry.entry.clone()) - } else { - None - } - } - PanelEntry::Fs( - FsEntry::ExternalFile(FsEntryExternalFile { - buffer_id: file_buffer_id, - excerpts: file_excerpts, - }) - | FsEntry::File(FsEntryFile { - buffer_id: file_buffer_id, - excerpts: file_excerpts, - .. - }), - ) => { - if file_buffer_id == &buffer_id && file_excerpts.contains(&excerpt_id) { - Some(cached_entry.entry.clone()) - } else { - None - } - } - _ => None, - } - })? - } - }; - Some(closest_container) - } - - fn fetch_outdated_outlines(&mut self, window: &mut Window, cx: &mut Context) { - let excerpt_fetch_ranges = self.excerpt_fetch_ranges(cx); - if excerpt_fetch_ranges.is_empty() { - return; - } - - let syntax_theme = cx.theme().syntax().clone(); - let first_update = Arc::new(AtomicBool::new(true)); - for (buffer_id, (buffer_snapshot, excerpt_ranges)) in excerpt_fetch_ranges { - for (excerpt_id, excerpt_range) in excerpt_ranges { - let syntax_theme = syntax_theme.clone(); - let buffer_snapshot = buffer_snapshot.clone(); - let first_update = first_update.clone(); - self.outline_fetch_tasks.insert( - (buffer_id, excerpt_id), - cx.spawn_in(window, async move |outline_panel, cx| { - let buffer_language = buffer_snapshot.language().cloned(); - let fetched_outlines = cx - .background_spawn(async move { - let mut outlines = buffer_snapshot.outline_items_containing( - excerpt_range.context, - false, - Some(&syntax_theme), - ); - outlines.retain(|outline| { - buffer_language.is_none() - || buffer_language.as_ref() - == buffer_snapshot.language_at(outline.range.start) - }); - - let outlines_with_children = outlines - .windows(2) - .filter_map(|window| { - let current = &window[0]; - let next = &window[1]; - if next.depth > current.depth { - Some((current.range.clone(), current.depth)) - } else { - None - } - }) - .collect::>(); - - (outlines, outlines_with_children) - }) - .await; - - let (fetched_outlines, outlines_with_children) = fetched_outlines; - - outline_panel - .update_in(cx, |outline_panel, window, cx| { - let pending_default_depth = - outline_panel.pending_default_expansion_depth.take(); - - let debounce = - if first_update.fetch_and(false, atomic::Ordering::AcqRel) { - None - } else { - Some(UPDATE_DEBOUNCE) - }; - - if let Some(excerpt) = outline_panel - .excerpts - .entry(buffer_id) - .or_default() - .get_mut(&excerpt_id) - { - excerpt.outlines = ExcerptOutlines::Outlines(fetched_outlines); - - if let Some(default_depth) = pending_default_depth - && let ExcerptOutlines::Outlines(outlines) = - &excerpt.outlines - { - outlines - .iter() - .filter(|outline| { - (default_depth == 0 - || outline.depth >= default_depth) - && outlines_with_children.contains(&( - outline.range.clone(), - outline.depth, - )) - }) - .for_each(|outline| { - outline_panel.collapsed_entries.insert( - CollapsedEntry::Outline( - buffer_id, - excerpt_id, - outline.range.clone(), - ), - ); - }); - } - - // Even if no outlines to check, we still need to update cached entries - // to show the outline entries that were just fetched - outline_panel.update_cached_entries(debounce, window, cx); - } - }) - .ok(); - }), - ); - } - } - } - - fn is_singleton_active(&self, cx: &App) -> bool { - self.active_editor() - .is_some_and(|active_editor| active_editor.read(cx).buffer().read(cx).is_singleton()) - } - - fn invalidate_outlines(&mut self, ids: &[ExcerptId]) { - self.outline_fetch_tasks.clear(); - let mut ids = ids.iter().collect::>(); - for excerpts in self.excerpts.values_mut() { - ids.retain(|id| { - if let Some(excerpt) = excerpts.get_mut(id) { - excerpt.invalidate_outlines(); - false - } else { - true - } - }); - if ids.is_empty() { - break; - } - } - } - - fn excerpt_fetch_ranges( - &self, - cx: &App, - ) -> HashMap< - BufferId, - ( - BufferSnapshot, - HashMap>, - ), - > { - self.fs_entries - .iter() - .fold(HashMap::default(), |mut excerpts_to_fetch, fs_entry| { - match fs_entry { - FsEntry::File(FsEntryFile { - buffer_id, - excerpts: file_excerpts, - .. - }) - | FsEntry::ExternalFile(FsEntryExternalFile { - buffer_id, - excerpts: file_excerpts, - }) => { - let excerpts = self.excerpts.get(buffer_id); - for &file_excerpt in file_excerpts { - if let Some(excerpt) = excerpts - .and_then(|excerpts| excerpts.get(&file_excerpt)) - .filter(|excerpt| excerpt.should_fetch_outlines()) - { - match excerpts_to_fetch.entry(*buffer_id) { - hash_map::Entry::Occupied(mut o) => { - o.get_mut().1.insert(file_excerpt, excerpt.range.clone()); - } - hash_map::Entry::Vacant(v) => { - if let Some(buffer_snapshot) = - self.buffer_snapshot_for_id(*buffer_id, cx) - { - v.insert((buffer_snapshot, HashMap::default())) - .1 - .insert(file_excerpt, excerpt.range.clone()); - } - } - } - } - } - } - FsEntry::Directory(..) => {} - } - excerpts_to_fetch - }) - } - - fn buffer_snapshot_for_id(&self, buffer_id: BufferId, cx: &App) -> Option { - let editor = self.active_editor()?; - Some( - editor - .read(cx) - .buffer() - .read(cx) - .buffer(buffer_id)? - .read(cx) - .snapshot(), - ) - } - - fn abs_path(&self, entry: &PanelEntry, cx: &App) -> Option { - match entry { - PanelEntry::Fs( - FsEntry::File(FsEntryFile { buffer_id, .. }) - | FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }), - ) => self - .buffer_snapshot_for_id(*buffer_id, cx) - .and_then(|buffer_snapshot| { - let file = File::from_dyn(buffer_snapshot.file())?; - Some(file.worktree.read(cx).absolutize(&file.path)) - }), - PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory { - worktree_id, entry, .. - })) => Some( - self.project - .read(cx) - .worktree_for_id(*worktree_id, cx)? - .read(cx) - .absolutize(&entry.path), - ), - PanelEntry::FoldedDirs(FoldedDirsEntry { - worktree_id, - entries: dirs, - .. - }) => dirs.last().and_then(|entry| { - self.project - .read(cx) - .worktree_for_id(*worktree_id, cx) - .map(|worktree| worktree.read(cx).absolutize(&entry.path)) - }), - PanelEntry::Search(_) | PanelEntry::Outline(..) => None, - } - } - - fn relative_path(&self, entry: &FsEntry, cx: &App) -> Option> { - match entry { - FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }) => { - let buffer_snapshot = self.buffer_snapshot_for_id(*buffer_id, cx)?; - Some(buffer_snapshot.file()?.path().clone()) - } - FsEntry::Directory(FsEntryDirectory { entry, .. }) => Some(entry.path.clone()), - FsEntry::File(FsEntryFile { entry, .. }) => Some(entry.path.clone()), - } - } - - fn update_cached_entries( - &mut self, - debounce: Option, - window: &mut Window, - cx: &mut Context, - ) { - if !self.active { - return; - } - - let is_singleton = self.is_singleton_active(cx); - let query = self.query(cx); - self.cached_entries_update_task = cx.spawn_in(window, async move |outline_panel, cx| { - if let Some(debounce) = debounce { - cx.background_executor().timer(debounce).await; - } - let Some(new_cached_entries) = outline_panel - .update_in(cx, |outline_panel, window, cx| { - outline_panel.generate_cached_entries(is_singleton, query, window, cx) - }) - .ok() - else { - return; - }; - let (new_cached_entries, max_width_item_index) = new_cached_entries.await; - outline_panel - .update_in(cx, |outline_panel, window, cx| { - outline_panel.cached_entries = new_cached_entries; - outline_panel.max_width_item_index = max_width_item_index; - if (outline_panel.selected_entry.is_invalidated() - || matches!(outline_panel.selected_entry, SelectedEntry::None)) - && let Some(new_selected_entry) = - outline_panel.active_editor().and_then(|active_editor| { - outline_panel.location_for_editor_selection( - &active_editor, - window, - cx, - ) - }) - { - outline_panel.select_entry(new_selected_entry, false, window, cx); - } - - outline_panel.autoscroll(cx); - cx.notify(); - }) - .ok(); - }); - } - - fn generate_cached_entries( - &self, - is_singleton: bool, - query: Option, - window: &mut Window, - cx: &mut Context, - ) -> Task<(Vec, Option)> { - let project = self.project.clone(); - let Some(active_editor) = self.active_editor() else { - return Task::ready((Vec::new(), None)); - }; - cx.spawn_in(window, async move |outline_panel, cx| { - let mut generation_state = GenerationState::default(); - - let Ok(()) = outline_panel.update(cx, |outline_panel, cx| { - let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs; - let mut folded_dirs_entry = None::<(usize, FoldedDirsEntry)>; - let track_matches = query.is_some(); - - #[derive(Debug)] - struct ParentStats { - path: Arc, - folded: bool, - expanded: bool, - depth: usize, - } - let mut parent_dirs = Vec::::new(); - for entry in outline_panel.fs_entries.clone() { - let is_expanded = outline_panel.is_expanded(&entry); - let (depth, should_add) = match &entry { - FsEntry::Directory(directory_entry) => { - let mut should_add = true; - let is_root = project - .read(cx) - .worktree_for_id(directory_entry.worktree_id, cx) - .is_some_and(|worktree| { - worktree.read(cx).root_entry() == Some(&directory_entry.entry) - }); - let folded = auto_fold_dirs - && !is_root - && outline_panel - .unfolded_dirs - .get(&directory_entry.worktree_id) - .is_none_or(|unfolded_dirs| { - !unfolded_dirs.contains(&directory_entry.entry.id) - }); - let fs_depth = outline_panel - .fs_entries_depth - .get(&(directory_entry.worktree_id, directory_entry.entry.id)) - .copied() - .unwrap_or(0); - while let Some(parent) = parent_dirs.last() { - if !is_root && directory_entry.entry.path.starts_with(&parent.path) - { - break; - } - parent_dirs.pop(); - } - let auto_fold = match parent_dirs.last() { - Some(parent) => { - parent.folded - && Some(parent.path.as_ref()) - == directory_entry.entry.path.parent() - && outline_panel - .fs_children_count - .get(&directory_entry.worktree_id) - .and_then(|entries| { - entries.get(&directory_entry.entry.path) - }) - .copied() - .unwrap_or_default() - .may_be_fold_part() - } - None => false, - }; - let folded = folded || auto_fold; - let (depth, parent_expanded, parent_folded) = match parent_dirs.last() { - Some(parent) => { - let parent_folded = parent.folded; - let parent_expanded = parent.expanded; - let new_depth = if parent_folded { - parent.depth - } else { - parent.depth + 1 - }; - parent_dirs.push(ParentStats { - path: directory_entry.entry.path.clone(), - folded, - expanded: parent_expanded && is_expanded, - depth: new_depth, - }); - (new_depth, parent_expanded, parent_folded) - } - None => { - parent_dirs.push(ParentStats { - path: directory_entry.entry.path.clone(), - folded, - expanded: is_expanded, - depth: fs_depth, - }); - (fs_depth, true, false) - } - }; - - if let Some((folded_depth, mut folded_dirs)) = folded_dirs_entry.take() - { - if folded - && directory_entry.worktree_id == folded_dirs.worktree_id - && directory_entry.entry.path.parent() - == folded_dirs - .entries - .last() - .map(|entry| entry.path.as_ref()) - { - folded_dirs.entries.push(directory_entry.entry.clone()); - folded_dirs_entry = Some((folded_depth, folded_dirs)) - } else { - if !is_singleton { - let start_of_collapsed_dir_sequence = !parent_expanded - && parent_dirs - .iter() - .rev() - .nth(folded_dirs.entries.len() + 1) - .is_none_or(|parent| parent.expanded); - if start_of_collapsed_dir_sequence - || parent_expanded - || query.is_some() - { - if parent_folded { - folded_dirs - .entries - .push(directory_entry.entry.clone()); - should_add = false; - } - let new_folded_dirs = - PanelEntry::FoldedDirs(folded_dirs.clone()); - outline_panel.push_entry( - &mut generation_state, - track_matches, - new_folded_dirs, - folded_depth, - cx, - ); - } - } - - folded_dirs_entry = if parent_folded { - None - } else { - Some(( - depth, - FoldedDirsEntry { - worktree_id: directory_entry.worktree_id, - entries: vec![directory_entry.entry.clone()], - }, - )) - }; - } - } else if folded { - folded_dirs_entry = Some(( - depth, - FoldedDirsEntry { - worktree_id: directory_entry.worktree_id, - entries: vec![directory_entry.entry.clone()], - }, - )); - } - - let should_add = - should_add && parent_expanded && folded_dirs_entry.is_none(); - (depth, should_add) - } - FsEntry::ExternalFile(..) => { - if let Some((folded_depth, folded_dir)) = folded_dirs_entry.take() { - let parent_expanded = parent_dirs - .iter() - .rev() - .find(|parent| { - folded_dir - .entries - .iter() - .all(|entry| entry.path != parent.path) - }) - .is_none_or(|parent| parent.expanded); - if !is_singleton && (parent_expanded || query.is_some()) { - outline_panel.push_entry( - &mut generation_state, - track_matches, - PanelEntry::FoldedDirs(folded_dir), - folded_depth, - cx, - ); - } - } - parent_dirs.clear(); - (0, true) - } - FsEntry::File(file) => { - if let Some((folded_depth, folded_dirs)) = folded_dirs_entry.take() { - let parent_expanded = parent_dirs - .iter() - .rev() - .find(|parent| { - folded_dirs - .entries - .iter() - .all(|entry| entry.path != parent.path) - }) - .is_none_or(|parent| parent.expanded); - if !is_singleton && (parent_expanded || query.is_some()) { - outline_panel.push_entry( - &mut generation_state, - track_matches, - PanelEntry::FoldedDirs(folded_dirs), - folded_depth, - cx, - ); - } - } - - let fs_depth = outline_panel - .fs_entries_depth - .get(&(file.worktree_id, file.entry.id)) - .copied() - .unwrap_or(0); - while let Some(parent) = parent_dirs.last() { - if file.entry.path.starts_with(&parent.path) { - break; - } - parent_dirs.pop(); - } - match parent_dirs.last() { - Some(parent) => { - let new_depth = parent.depth + 1; - (new_depth, parent.expanded) - } - None => (fs_depth, true), - } - } - }; - - if !is_singleton - && (should_add || (query.is_some() && folded_dirs_entry.is_none())) - { - outline_panel.push_entry( - &mut generation_state, - track_matches, - PanelEntry::Fs(entry.clone()), - depth, - cx, - ); - } - - match outline_panel.mode { - ItemsDisplayMode::Search(_) => { - if is_singleton || query.is_some() || (should_add && is_expanded) { - outline_panel.add_search_entries( - &mut generation_state, - &active_editor, - entry.clone(), - depth, - query.clone(), - is_singleton, - cx, - ); - } - } - ItemsDisplayMode::Outline => { - let excerpts_to_consider = - if is_singleton || query.is_some() || (should_add && is_expanded) { - match &entry { - FsEntry::File(FsEntryFile { - buffer_id, - excerpts, - .. - }) - | FsEntry::ExternalFile(FsEntryExternalFile { - buffer_id, - excerpts, - .. - }) => Some((*buffer_id, excerpts)), - _ => None, - } - } else { - None - }; - if let Some((buffer_id, entry_excerpts)) = excerpts_to_consider - && !active_editor.read(cx).is_buffer_folded(buffer_id, cx) - { - outline_panel.add_excerpt_entries( - &mut generation_state, - buffer_id, - entry_excerpts, - depth, - track_matches, - is_singleton, - query.as_deref(), - cx, - ); - } - } - } - - if is_singleton - && matches!(entry, FsEntry::File(..) | FsEntry::ExternalFile(..)) - && !generation_state.entries.iter().any(|item| { - matches!(item.entry, PanelEntry::Outline(..) | PanelEntry::Search(_)) - }) - { - outline_panel.push_entry( - &mut generation_state, - track_matches, - PanelEntry::Fs(entry.clone()), - 0, - cx, - ); - } - } - - if let Some((folded_depth, folded_dirs)) = folded_dirs_entry.take() { - let parent_expanded = parent_dirs - .iter() - .rev() - .find(|parent| { - folded_dirs - .entries - .iter() - .all(|entry| entry.path != parent.path) - }) - .is_none_or(|parent| parent.expanded); - if parent_expanded || query.is_some() { - outline_panel.push_entry( - &mut generation_state, - track_matches, - PanelEntry::FoldedDirs(folded_dirs), - folded_depth, - cx, - ); - } - } - }) else { - return (Vec::new(), None); - }; - - let Some(query) = query else { - return ( - generation_state.entries, - generation_state - .max_width_estimate_and_index - .map(|(_, index)| index), - ); - }; - - let mut matched_ids = match_strings( - &generation_state.match_candidates, - &query, - true, - true, - usize::MAX, - &AtomicBool::default(), - cx.background_executor().clone(), - ) - .await - .into_iter() - .map(|string_match| (string_match.candidate_id, string_match)) - .collect::>(); - - let mut id = 0; - generation_state.entries.retain_mut(|cached_entry| { - let retain = match matched_ids.remove(&id) { - Some(string_match) => { - cached_entry.string_match = Some(string_match); - true - } - None => false, - }; - id += 1; - retain - }); - - ( - generation_state.entries, - generation_state - .max_width_estimate_and_index - .map(|(_, index)| index), - ) - }) - } - - fn push_entry( - &self, - state: &mut GenerationState, - track_matches: bool, - entry: PanelEntry, - depth: usize, - cx: &mut App, - ) { - let entry = if let PanelEntry::FoldedDirs(folded_dirs_entry) = &entry { - match folded_dirs_entry.entries.len() { - 0 => { - debug_panic!("Empty folded dirs receiver"); - return; - } - 1 => PanelEntry::Fs(FsEntry::Directory(FsEntryDirectory { - worktree_id: folded_dirs_entry.worktree_id, - entry: folded_dirs_entry.entries[0].clone(), - })), - _ => entry, - } - } else { - entry - }; - - if track_matches { - let id = state.entries.len(); - match &entry { - PanelEntry::Fs(fs_entry) => { - if let Some(file_name) = self - .relative_path(fs_entry, cx) - .and_then(|path| Some(path.file_name()?.to_string())) - { - state - .match_candidates - .push(StringMatchCandidate::new(id, &file_name)); - } - } - PanelEntry::FoldedDirs(folded_dir_entry) => { - let dir_names = self.dir_names_string( - &folded_dir_entry.entries, - folded_dir_entry.worktree_id, - cx, - ); - { - state - .match_candidates - .push(StringMatchCandidate::new(id, &dir_names)); - } - } - PanelEntry::Outline(OutlineEntry::Outline(outline_entry)) => state - .match_candidates - .push(StringMatchCandidate::new(id, &outline_entry.outline.text)), - PanelEntry::Outline(OutlineEntry::Excerpt(_)) => {} - PanelEntry::Search(new_search_entry) => { - if let Some(search_data) = new_search_entry.render_data.get() { - state - .match_candidates - .push(StringMatchCandidate::new(id, &search_data.context_text)); - } - } - } - } - - let width_estimate = self.width_estimate(depth, &entry, cx); - if Some(width_estimate) - > state - .max_width_estimate_and_index - .map(|(estimate, _)| estimate) - { - state.max_width_estimate_and_index = Some((width_estimate, state.entries.len())); - } - state.entries.push(CachedEntry { - depth, - entry, - string_match: None, - }); - } - - fn dir_names_string(&self, entries: &[GitEntry], worktree_id: WorktreeId, cx: &App) -> String { - let dir_names_segment = entries - .iter() - .map(|entry| self.entry_name(&worktree_id, entry, cx)) - .collect::(); - dir_names_segment.to_string_lossy().into_owned() - } - - fn query(&self, cx: &App) -> Option { - let query = self.filter_editor.read(cx).text(cx); - if query.trim().is_empty() { - None - } else { - Some(query) - } - } - - fn is_expanded(&self, entry: &FsEntry) -> bool { - let entry_to_check = match entry { - FsEntry::ExternalFile(FsEntryExternalFile { buffer_id, .. }) => { - CollapsedEntry::ExternalFile(*buffer_id) - } - FsEntry::File(FsEntryFile { - worktree_id, - buffer_id, - .. - }) => CollapsedEntry::File(*worktree_id, *buffer_id), - FsEntry::Directory(FsEntryDirectory { - worktree_id, entry, .. - }) => CollapsedEntry::Dir(*worktree_id, entry.id), - }; - !self.collapsed_entries.contains(&entry_to_check) - } - - fn update_non_fs_items(&mut self, window: &mut Window, cx: &mut Context) -> bool { - if !self.active { - return false; - } - - let mut update_cached_items = false; - update_cached_items |= self.update_search_matches(window, cx); - self.fetch_outdated_outlines(window, cx); - if update_cached_items { - self.selected_entry.invalidate(); - } - update_cached_items - } - - fn update_search_matches( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> bool { - if !self.active { - return false; - } - - let project_search = self - .active_item() - .and_then(|item| item.downcast::()); - let project_search_matches = project_search - .as_ref() - .map(|project_search| project_search.read(cx).get_matches(cx)) - .unwrap_or_default(); - - let buffer_search = self - .active_item() - .as_deref() - .and_then(|active_item| { - self.workspace - .upgrade() - .and_then(|workspace| workspace.read(cx).pane_for(active_item)) - }) - .and_then(|pane| { - pane.read(cx) - .toolbar() - .read(cx) - .item_of_type::() - }); - let buffer_search_matches = self - .active_editor() - .map(|active_editor| { - active_editor.update(cx, |editor, cx| editor.get_matches(window, cx)) - }) - .unwrap_or_default(); - - let mut update_cached_entries = false; - if buffer_search_matches.is_empty() && project_search_matches.is_empty() { - if matches!(self.mode, ItemsDisplayMode::Search(_)) { - self.mode = ItemsDisplayMode::Outline; - update_cached_entries = true; - } - } else { - let (kind, new_search_matches, new_search_query) = if buffer_search_matches.is_empty() { - ( - SearchKind::Project, - project_search_matches, - project_search - .map(|project_search| project_search.read(cx).search_query_text(cx)) - .unwrap_or_default(), - ) - } else { - ( - SearchKind::Buffer, - buffer_search_matches, - buffer_search - .map(|buffer_search| buffer_search.read(cx).query(cx)) - .unwrap_or_default(), - ) - }; - - let mut previous_matches = HashMap::default(); - update_cached_entries = match &mut self.mode { - ItemsDisplayMode::Search(current_search_state) => { - let update = current_search_state.query != new_search_query - || current_search_state.kind != kind - || current_search_state.matches.is_empty() - || current_search_state.matches.iter().enumerate().any( - |(i, (match_range, _))| new_search_matches.get(i) != Some(match_range), - ); - if current_search_state.kind == kind { - previous_matches.extend(current_search_state.matches.drain(..)); - } - update - } - ItemsDisplayMode::Outline => true, - }; - self.mode = ItemsDisplayMode::Search(SearchState::new( - kind, - new_search_query, - previous_matches, - new_search_matches, - cx.theme().syntax().clone(), - window, - cx, - )); - } - update_cached_entries - } - - fn add_excerpt_entries( - &mut self, - state: &mut GenerationState, - buffer_id: BufferId, - entries_to_add: &[ExcerptId], - parent_depth: usize, - track_matches: bool, - is_singleton: bool, - query: Option<&str>, - cx: &mut Context, - ) { - if let Some(excerpts) = self.excerpts.get(&buffer_id) { - let buffer_snapshot = self.buffer_snapshot_for_id(buffer_id, cx); - - for &excerpt_id in entries_to_add { - let Some(excerpt) = excerpts.get(&excerpt_id) else { - continue; - }; - let excerpt_depth = parent_depth + 1; - self.push_entry( - state, - track_matches, - PanelEntry::Outline(OutlineEntry::Excerpt(OutlineEntryExcerpt { - buffer_id, - id: excerpt_id, - range: excerpt.range.clone(), - })), - excerpt_depth, - cx, - ); - - let mut outline_base_depth = excerpt_depth + 1; - if is_singleton { - outline_base_depth = 0; - state.clear(); - } else if query.is_none() - && self - .collapsed_entries - .contains(&CollapsedEntry::Excerpt(buffer_id, excerpt_id)) - { - continue; - } - - let mut last_depth_at_level: Vec>> = vec![None; 10]; - - let all_outlines: Vec<_> = excerpt.iter_outlines().collect(); - - let mut outline_has_children = HashMap::default(); - let mut visible_outlines = Vec::new(); - let mut collapsed_state: Option<(usize, Range)> = None; - - for (i, &outline) in all_outlines.iter().enumerate() { - let has_children = all_outlines - .get(i + 1) - .map(|next| next.depth > outline.depth) - .unwrap_or(false); - - outline_has_children - .insert((outline.range.clone(), outline.depth), has_children); - - let mut should_include = true; - - if let Some((collapsed_depth, collapsed_range)) = &collapsed_state { - if outline.depth <= *collapsed_depth { - collapsed_state = None; - } else if let Some(buffer_snapshot) = buffer_snapshot.as_ref() { - let outline_start = outline.range.start; - if outline_start - .cmp(&collapsed_range.start, buffer_snapshot) - .is_ge() - && outline_start - .cmp(&collapsed_range.end, buffer_snapshot) - .is_lt() - { - should_include = false; // Skip - inside collapsed range - } else { - collapsed_state = None; - } - } - } - - // Check if this outline itself is collapsed - if should_include - && self.collapsed_entries.contains(&CollapsedEntry::Outline( - buffer_id, - excerpt_id, - outline.range.clone(), - )) - { - collapsed_state = Some((outline.depth, outline.range.clone())); - } - - if should_include { - visible_outlines.push(outline); - } - } - - self.outline_children_cache - .entry(buffer_id) - .or_default() - .extend(outline_has_children); - - for outline in visible_outlines { - let outline_entry = OutlineEntryOutline { - buffer_id, - excerpt_id, - outline: outline.clone(), - }; - - if outline.depth < last_depth_at_level.len() { - last_depth_at_level[outline.depth] = Some(outline.range.clone()); - // Clear deeper levels when we go back to a shallower depth - for d in (outline.depth + 1)..last_depth_at_level.len() { - last_depth_at_level[d] = None; - } - } - - self.push_entry( - state, - track_matches, - PanelEntry::Outline(OutlineEntry::Outline(outline_entry)), - outline_base_depth + outline.depth, - cx, - ); - } - } - } - } - - fn add_search_entries( - &mut self, - state: &mut GenerationState, - active_editor: &Entity, - parent_entry: FsEntry, - parent_depth: usize, - filter_query: Option, - is_singleton: bool, - cx: &mut Context, - ) { - let ItemsDisplayMode::Search(search_state) = &mut self.mode else { - return; - }; - - let kind = search_state.kind; - let related_excerpts = match &parent_entry { - FsEntry::Directory(_) => return, - FsEntry::ExternalFile(external) => &external.excerpts, - FsEntry::File(file) => &file.excerpts, - } - .iter() - .copied() - .collect::>(); - - let depth = if is_singleton { 0 } else { parent_depth + 1 }; - let new_search_matches = search_state - .matches - .iter() - .filter(|(match_range, _)| { - related_excerpts.contains(&match_range.start.excerpt_id) - || related_excerpts.contains(&match_range.end.excerpt_id) - }) - .filter(|(match_range, _)| { - let editor = active_editor.read(cx); - let snapshot = editor.buffer().read(cx).snapshot(cx); - if let Some(buffer_id) = snapshot.buffer_id_for_anchor(match_range.start) - && editor.is_buffer_folded(buffer_id, cx) - { - return false; - } - if let Some(buffer_id) = snapshot.buffer_id_for_anchor(match_range.end) - && editor.is_buffer_folded(buffer_id, cx) - { - return false; - } - true - }); - - let new_search_entries = new_search_matches - .map(|(match_range, search_data)| SearchEntry { - match_range: match_range.clone(), - kind, - render_data: Arc::clone(search_data), - }) - .collect::>(); - for new_search_entry in new_search_entries { - self.push_entry( - state, - filter_query.is_some(), - PanelEntry::Search(new_search_entry), - depth, - cx, - ); - } - } - - fn active_editor(&self) -> Option> { - self.active_item.as_ref()?.active_editor.upgrade() - } - - fn active_item(&self) -> Option> { - self.active_item.as_ref()?.item_handle.upgrade() - } - - fn should_replace_active_item(&self, new_active_item: &dyn ItemHandle) -> bool { - self.active_item().is_none_or(|active_item| { - !self.pinned && active_item.item_id() != new_active_item.item_id() - }) - } - - pub fn toggle_active_editor_pin( - &mut self, - _: &ToggleActiveEditorPin, - window: &mut Window, - cx: &mut Context, - ) { - self.pinned = !self.pinned; - if !self.pinned - && let Some((active_item, active_editor)) = self - .workspace - .upgrade() - .and_then(|workspace| workspace_active_editor(workspace.read(cx), cx)) - && self.should_replace_active_item(active_item.as_ref()) - { - self.replace_active_editor(active_item, active_editor, window, cx); - } - - cx.notify(); - } - - fn selected_entry(&self) -> Option<&PanelEntry> { - match &self.selected_entry { - SelectedEntry::Invalidated(entry) => entry.as_ref(), - SelectedEntry::Valid(entry, _) => Some(entry), - SelectedEntry::None => None, - } - } - - fn select_entry( - &mut self, - entry: PanelEntry, - focus: bool, - window: &mut Window, - cx: &mut Context, - ) { - if focus { - self.focus_handle.focus(window); - } - let ix = self - .cached_entries - .iter() - .enumerate() - .find(|(_, cached_entry)| &cached_entry.entry == &entry) - .map(|(i, _)| i) - .unwrap_or_default(); - - self.selected_entry = SelectedEntry::Valid(entry, ix); - - self.autoscroll(cx); - cx.notify(); - } - - fn width_estimate(&self, depth: usize, entry: &PanelEntry, cx: &App) -> u64 { - let item_text_chars = match entry { - PanelEntry::Fs(FsEntry::ExternalFile(external)) => self - .buffer_snapshot_for_id(external.buffer_id, cx) - .and_then(|snapshot| Some(snapshot.file()?.path().file_name()?.len())) - .unwrap_or_default(), - PanelEntry::Fs(FsEntry::Directory(directory)) => directory - .entry - .path - .file_name() - .map(|name| name.len()) - .unwrap_or_default(), - PanelEntry::Fs(FsEntry::File(file)) => file - .entry - .path - .file_name() - .map(|name| name.len()) - .unwrap_or_default(), - PanelEntry::FoldedDirs(folded_dirs) => { - folded_dirs - .entries - .iter() - .map(|dir| { - dir.path - .file_name() - .map(|name| name.len()) - .unwrap_or_default() - }) - .sum::() - + folded_dirs.entries.len().saturating_sub(1) * "/".len() - } - PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => self - .excerpt_label(excerpt.buffer_id, &excerpt.range, cx) - .map(|label| label.len()) - .unwrap_or_default(), - PanelEntry::Outline(OutlineEntry::Outline(entry)) => entry.outline.text.len(), - PanelEntry::Search(search) => search - .render_data - .get() - .map(|data| data.context_text.len()) - .unwrap_or_default(), - }; - - (item_text_chars + depth) as u64 - } - - fn render_main_contents( - &mut self, - query: Option, - show_indent_guides: bool, - indent_size: f32, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let contents = if self.cached_entries.is_empty() { - let header = if query.is_some() { - "No matches for query" - } else { - "No outlines available" - }; - - v_flex() - .id("empty-outline-state") - .gap_0p5() - .flex_1() - .justify_center() - .size_full() - .child(h_flex().justify_center().child(Label::new(header))) - .when_some(query, |panel, query| { - panel.child( - h_flex() - .px_0p5() - .justify_center() - .bg(cx.theme().colors().element_selected.opacity(0.2)) - .child(Label::new(query)), - ) - }) - .child(h_flex().justify_center().child({ - let keystroke = match self.position(window, cx) { - DockPosition::Left => window.keystroke_text_for(&workspace::ToggleLeftDock), - DockPosition::Bottom => { - window.keystroke_text_for(&workspace::ToggleBottomDock) - } - DockPosition::Right => { - window.keystroke_text_for(&workspace::ToggleRightDock) - } - }; - Label::new(format!("Toggle Panel With {keystroke}")).color(Color::Muted) - })) - } else { - let list_contents = { - let items_len = self.cached_entries.len(); - let multi_buffer_snapshot = self - .active_editor() - .map(|editor| editor.read(cx).buffer().read(cx).snapshot(cx)); - uniform_list( - "entries", - items_len, - cx.processor(move |outline_panel, range: Range, window, cx| { - let entries = outline_panel.cached_entries.get(range); - entries - .map(|entries| entries.to_vec()) - .unwrap_or_default() - .into_iter() - .filter_map(|cached_entry| match cached_entry.entry { - PanelEntry::Fs(entry) => Some(outline_panel.render_entry( - &entry, - cached_entry.depth, - cached_entry.string_match.as_ref(), - window, - cx, - )), - PanelEntry::FoldedDirs(folded_dirs_entry) => { - Some(outline_panel.render_folded_dirs( - &folded_dirs_entry, - cached_entry.depth, - cached_entry.string_match.as_ref(), - window, - cx, - )) - } - PanelEntry::Outline(OutlineEntry::Excerpt(excerpt)) => { - outline_panel.render_excerpt( - &excerpt, - cached_entry.depth, - window, - cx, - ) - } - PanelEntry::Outline(OutlineEntry::Outline(entry)) => { - Some(outline_panel.render_outline( - &entry, - cached_entry.depth, - cached_entry.string_match.as_ref(), - window, - cx, - )) - } - PanelEntry::Search(SearchEntry { - match_range, - render_data, - kind, - .. - }) => outline_panel.render_search_match( - multi_buffer_snapshot.as_ref(), - &match_range, - &render_data, - kind, - cached_entry.depth, - cached_entry.string_match.as_ref(), - window, - cx, - ), - }) - .collect() - }), - ) - .with_sizing_behavior(ListSizingBehavior::Infer) - .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained) - .with_width_from_item(self.max_width_item_index) - .track_scroll(&self.scroll_handle) - .when(show_indent_guides, |list| { - list.with_decoration( - ui::indent_guides(px(indent_size), IndentGuideColors::panel(cx)) - .with_compute_indents_fn(cx.entity(), |outline_panel, range, _, _| { - let entries = outline_panel.cached_entries.get(range); - if let Some(entries) = entries { - entries.iter().map(|item| item.depth).collect() - } else { - smallvec::SmallVec::new() - } - }) - .with_render_fn(cx.entity(), move |outline_panel, params, _, _| { - const LEFT_OFFSET: Pixels = px(14.); - - let indent_size = params.indent_size; - let item_height = params.item_height; - let active_indent_guide_ix = find_active_indent_guide_ix( - outline_panel, - ¶ms.indent_guides, - ); - - params - .indent_guides - .into_iter() - .enumerate() - .map(|(ix, layout)| { - let bounds = Bounds::new( - point( - layout.offset.x * indent_size + LEFT_OFFSET, - layout.offset.y * item_height, - ), - size(px(1.), layout.length * item_height), - ); - ui::RenderedIndentGuide { - bounds, - layout, - is_active: active_indent_guide_ix == Some(ix), - hitbox: None, - } - }) - .collect() - }), - ) - }) - }; - - v_flex() - .flex_shrink() - .size_full() - .child(list_contents.size_full().flex_shrink()) - .custom_scrollbars( - Scrollbars::for_settings::() - .tracked_scroll_handle(&self.scroll_handle.clone()) - .with_track_along( - ScrollAxes::Horizontal, - cx.theme().colors().panel_background, - ) - .tracked_entity(cx.entity_id()), - window, - cx, - ) - } - .children(self.context_menu.as_ref().map(|(menu, position, _)| { - deferred( - anchored() - .position(*position) - .anchor(gpui::Corner::TopLeft) - .child(menu.clone()), - ) - .with_priority(1) - })); - - v_flex().w_full().flex_1().overflow_hidden().child(contents) - } - - fn render_filter_footer(&mut self, pinned: bool, cx: &mut Context) -> Div { - let (icon, icon_tooltip) = if pinned { - (IconName::Unpin, "Unpin Outline") - } else { - (IconName::Pin, "Pin Active Outline") - }; - - h_flex() - .p_2() - .h(Tab::container_height(cx)) - .justify_between() - .border_b_1() - .border_color(cx.theme().colors().border) - .child( - h_flex() - .w_full() - .gap_1p5() - .child( - Icon::new(IconName::MagnifyingGlass) - .size(IconSize::Small) - .color(Color::Muted), - ) - .child(self.filter_editor.clone()), - ) - .child( - IconButton::new("pin_button", icon) - .tooltip(Tooltip::text(icon_tooltip)) - .shape(IconButtonShape::Square) - .on_click(cx.listener(|outline_panel, _, window, cx| { - outline_panel.toggle_active_editor_pin(&ToggleActiveEditorPin, window, cx); - })), - ) - } - - fn buffers_inside_directory( - &self, - dir_worktree: WorktreeId, - dir_entry: &GitEntry, - ) -> HashSet { - if !dir_entry.is_dir() { - debug_panic!("buffers_inside_directory called on a non-directory entry {dir_entry:?}"); - return HashSet::default(); - } - - self.fs_entries - .iter() - .skip_while(|fs_entry| match fs_entry { - FsEntry::Directory(directory) => { - directory.worktree_id != dir_worktree || &directory.entry != dir_entry - } - _ => true, - }) - .skip(1) - .take_while(|fs_entry| match fs_entry { - FsEntry::ExternalFile(..) => false, - FsEntry::Directory(directory) => { - directory.worktree_id == dir_worktree - && directory.entry.path.starts_with(&dir_entry.path) - } - FsEntry::File(file) => { - file.worktree_id == dir_worktree && file.entry.path.starts_with(&dir_entry.path) - } - }) - .filter_map(|fs_entry| match fs_entry { - FsEntry::File(file) => Some(file.buffer_id), - _ => None, - }) - .collect() - } -} - -fn workspace_active_editor( - workspace: &Workspace, - cx: &App, -) -> Option<(Box, Entity)> { - let active_item = workspace.active_item(cx)?; - let active_editor = active_item - .act_as::(cx) - .filter(|editor| editor.read(cx).mode().is_full())?; - Some((active_item, active_editor)) -} - -fn back_to_common_visited_parent( - visited_dirs: &mut Vec<(ProjectEntryId, Arc)>, - worktree_id: &WorktreeId, - new_entry: &Entry, -) -> Option<(WorktreeId, ProjectEntryId)> { - while let Some((visited_dir_id, visited_path)) = visited_dirs.last() { - match new_entry.path.parent() { - Some(parent_path) => { - if parent_path == visited_path.as_ref() { - return Some((*worktree_id, *visited_dir_id)); - } - } - None => { - break; - } - } - visited_dirs.pop(); - } - None -} - -fn file_name(path: &Path) -> String { - let mut current_path = path; - loop { - if let Some(file_name) = current_path.file_name() { - return file_name.to_string_lossy().into_owned(); - } - match current_path.parent() { - Some(parent) => current_path = parent, - None => return path.to_string_lossy().into_owned(), - } - } -} - -impl Panel for OutlinePanel { - fn persistent_name() -> &'static str { - "Outline Panel" - } - - fn panel_key() -> &'static str { - OUTLINE_PANEL_KEY - } - - fn position(&self, _: &Window, cx: &App) -> DockPosition { - match OutlinePanelSettings::get_global(cx).dock { - DockSide::Left => DockPosition::Left, - DockSide::Right => DockPosition::Right, - } - } - - fn position_is_valid(&self, position: DockPosition) -> bool { - matches!(position, DockPosition::Left | DockPosition::Right) - } - - fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context) { - settings::update_settings_file(self.fs.clone(), cx, move |settings, _| { - let dock = match position { - DockPosition::Left | DockPosition::Bottom => DockSide::Left, - DockPosition::Right => DockSide::Right, - }; - settings.outline_panel.get_or_insert_default().dock = Some(dock); - }); - } - - fn size(&self, _: &Window, cx: &App) -> Pixels { - self.width - .unwrap_or_else(|| OutlinePanelSettings::get_global(cx).default_width) - } - - fn set_size(&mut self, size: Option, window: &mut Window, cx: &mut Context) { - self.width = size; - cx.notify(); - cx.defer_in(window, |this, _, cx| { - this.serialize(cx); - }); - } - - fn icon(&self, _: &Window, cx: &App) -> Option { - OutlinePanelSettings::get_global(cx) - .button - .then_some(IconName::ListTree) - } - - fn icon_tooltip(&self, _window: &Window, _: &App) -> Option<&'static str> { - Some("Outline Panel") - } - - fn toggle_action(&self) -> Box { - Box::new(ToggleFocus) - } - - fn starts_open(&self, _window: &Window, _: &App) -> bool { - self.active - } - - fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context) { - cx.spawn_in(window, async move |outline_panel, cx| { - outline_panel - .update_in(cx, |outline_panel, window, cx| { - let old_active = outline_panel.active; - outline_panel.active = active; - if old_active != active { - if active - && let Some((active_item, active_editor)) = - outline_panel.workspace.upgrade().and_then(|workspace| { - workspace_active_editor(workspace.read(cx), cx) - }) - { - if outline_panel.should_replace_active_item(active_item.as_ref()) { - outline_panel.replace_active_editor( - active_item, - active_editor, - window, - cx, - ); - } else { - outline_panel.update_fs_entries(active_editor, None, window, cx) - } - return; - } - - if !outline_panel.pinned { - outline_panel.clear_previous(window, cx); - } - } - outline_panel.serialize(cx); - }) - .ok(); - }) - .detach() - } - - fn activation_priority(&self) -> u32 { - 5 - } -} - -impl Focusable for OutlinePanel { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.filter_editor.focus_handle(cx) - } -} - -impl EventEmitter for OutlinePanel {} - -impl EventEmitter for OutlinePanel {} - -impl Render for OutlinePanel { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let (is_local, is_via_ssh) = self.project.read_with(cx, |project, _| { - (project.is_local(), project.is_via_remote_server()) - }); - let query = self.query(cx); - let pinned = self.pinned; - let settings = OutlinePanelSettings::get_global(cx); - let indent_size = settings.indent_size; - let show_indent_guides = settings.indent_guides.show == ShowIndentGuides::Always; - - let search_query = match &self.mode { - ItemsDisplayMode::Search(search_query) => Some(search_query), - _ => None, - }; - - let search_query_text = search_query.map(|sq| sq.query.to_string()); - - v_flex() - .id("outline-panel") - .size_full() - .overflow_hidden() - .relative() - .key_context(self.dispatch_context(window, cx)) - .on_action(cx.listener(Self::open_selected_entry)) - .on_action(cx.listener(Self::cancel)) - .on_action(cx.listener(Self::select_next)) - .on_action(cx.listener(Self::select_previous)) - .on_action(cx.listener(Self::select_first)) - .on_action(cx.listener(Self::select_last)) - .on_action(cx.listener(Self::select_parent)) - .on_action(cx.listener(Self::expand_selected_entry)) - .on_action(cx.listener(Self::collapse_selected_entry)) - .on_action(cx.listener(Self::expand_all_entries)) - .on_action(cx.listener(Self::collapse_all_entries)) - .on_action(cx.listener(Self::copy_path)) - .on_action(cx.listener(Self::copy_relative_path)) - .on_action(cx.listener(Self::toggle_active_editor_pin)) - .on_action(cx.listener(Self::unfold_directory)) - .on_action(cx.listener(Self::fold_directory)) - .on_action(cx.listener(Self::open_excerpts)) - .on_action(cx.listener(Self::open_excerpts_split)) - .when(is_local, |el| { - el.on_action(cx.listener(Self::reveal_in_finder)) - }) - .when(is_local || is_via_ssh, |el| { - el.on_action(cx.listener(Self::open_in_terminal)) - }) - .on_mouse_down( - MouseButton::Right, - cx.listener(move |outline_panel, event: &MouseDownEvent, window, cx| { - if let Some(entry) = outline_panel.selected_entry().cloned() { - outline_panel.deploy_context_menu(event.position, entry, window, cx) - } else if let Some(entry) = outline_panel.fs_entries.first().cloned() { - outline_panel.deploy_context_menu( - event.position, - PanelEntry::Fs(entry), - window, - cx, - ) - } - }), - ) - .track_focus(&self.focus_handle) - .child(self.render_filter_footer(pinned, cx)) - .when_some(search_query_text, |outline_panel, query_text| { - outline_panel.child( - h_flex() - .py_1p5() - .px_2() - .h(Tab::container_height(cx)) - .gap_0p5() - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .child(Label::new("Searching:").color(Color::Muted)) - .child(Label::new(query_text)), - ) - }) - .child(self.render_main_contents(query, show_indent_guides, indent_size, window, cx)) - } -} - -fn find_active_indent_guide_ix( - outline_panel: &OutlinePanel, - candidates: &[IndentGuideLayout], -) -> Option { - let SelectedEntry::Valid(_, target_ix) = &outline_panel.selected_entry else { - return None; - }; - let target_depth = outline_panel - .cached_entries - .get(*target_ix) - .map(|cached_entry| cached_entry.depth)?; - - let (target_ix, target_depth) = if let Some(target_depth) = outline_panel - .cached_entries - .get(target_ix + 1) - .filter(|cached_entry| cached_entry.depth > target_depth) - .map(|entry| entry.depth) - { - (target_ix + 1, target_depth.saturating_sub(1)) - } else { - (*target_ix, target_depth.saturating_sub(1)) - }; - - candidates - .iter() - .enumerate() - .find(|(_, guide)| { - guide.offset.y <= target_ix - && target_ix < guide.offset.y + guide.length - && guide.offset.x == target_depth - }) - .map(|(ix, _)| ix) -} - -fn subscribe_for_editor_events( - editor: &Entity, - window: &mut Window, - cx: &mut Context, -) -> Subscription { - let debounce = Some(UPDATE_DEBOUNCE); - cx.subscribe_in( - editor, - window, - move |outline_panel, editor, e: &EditorEvent, window, cx| { - if !outline_panel.active { - return; - } - match e { - EditorEvent::SelectionsChanged { local: true } => { - outline_panel.reveal_entry_for_selection(editor.clone(), window, cx); - cx.notify(); - } - EditorEvent::ExcerptsAdded { excerpts, .. } => { - outline_panel - .new_entries_for_fs_update - .extend(excerpts.iter().map(|&(excerpt_id, _)| excerpt_id)); - outline_panel.update_fs_entries(editor.clone(), debounce, window, cx); - } - EditorEvent::ExcerptsRemoved { ids, .. } => { - let mut ids = ids.iter().collect::>(); - for excerpts in outline_panel.excerpts.values_mut() { - excerpts.retain(|excerpt_id, _| !ids.remove(excerpt_id)); - if ids.is_empty() { - break; - } - } - outline_panel.update_fs_entries(editor.clone(), debounce, window, cx); - } - EditorEvent::ExcerptsExpanded { ids } => { - outline_panel.invalidate_outlines(ids); - let update_cached_items = outline_panel.update_non_fs_items(window, cx); - if update_cached_items { - outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx); - } - } - EditorEvent::ExcerptsEdited { ids } => { - outline_panel.invalidate_outlines(ids); - let update_cached_items = outline_panel.update_non_fs_items(window, cx); - if update_cached_items { - outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx); - } - } - EditorEvent::BufferFoldToggled { ids, .. } => { - outline_panel.invalidate_outlines(ids); - let mut latest_unfolded_buffer_id = None; - let mut latest_folded_buffer_id = None; - let mut ignore_selections_change = false; - outline_panel.new_entries_for_fs_update.extend( - ids.iter() - .filter(|id| { - outline_panel - .excerpts - .iter() - .find_map(|(buffer_id, excerpts)| { - if excerpts.contains_key(id) { - ignore_selections_change |= outline_panel - .preserve_selection_on_buffer_fold_toggles - .remove(buffer_id); - Some(buffer_id) - } else { - None - } - }) - .map(|buffer_id| { - if editor.read(cx).is_buffer_folded(*buffer_id, cx) { - latest_folded_buffer_id = Some(*buffer_id); - false - } else { - latest_unfolded_buffer_id = Some(*buffer_id); - true - } - }) - .unwrap_or(true) - }) - .copied(), - ); - if !ignore_selections_change - && let Some(entry_to_select) = latest_unfolded_buffer_id - .or(latest_folded_buffer_id) - .and_then(|toggled_buffer_id| { - outline_panel.fs_entries.iter().find_map( - |fs_entry| match fs_entry { - FsEntry::ExternalFile(external) => { - if external.buffer_id == toggled_buffer_id { - Some(fs_entry.clone()) - } else { - None - } - } - FsEntry::File(FsEntryFile { buffer_id, .. }) => { - if *buffer_id == toggled_buffer_id { - Some(fs_entry.clone()) - } else { - None - } - } - FsEntry::Directory(..) => None, - }, - ) - }) - .map(PanelEntry::Fs) - { - outline_panel.select_entry(entry_to_select, true, window, cx); - } - - outline_panel.update_fs_entries(editor.clone(), debounce, window, cx); - } - EditorEvent::Reparsed(buffer_id) => { - if let Some(excerpts) = outline_panel.excerpts.get_mut(buffer_id) { - for excerpt in excerpts.values_mut() { - excerpt.invalidate_outlines(); - } - } - let update_cached_items = outline_panel.update_non_fs_items(window, cx); - if update_cached_items { - outline_panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx); - } - } - EditorEvent::TitleChanged => { - outline_panel.update_fs_entries(editor.clone(), debounce, window, cx); - } - _ => {} - } - }, - ) -} - -fn empty_icon() -> AnyElement { - h_flex() - .size(IconSize::default().rems()) - .invisible() - .flex_none() - .into_any_element() -} - -#[derive(Debug, Default)] -struct GenerationState { - entries: Vec, - match_candidates: Vec, - max_width_estimate_and_index: Option<(u64, usize)>, -} - -impl GenerationState { - fn clear(&mut self) { - self.entries.clear(); - self.match_candidates.clear(); - self.max_width_estimate_and_index = None; - } -} - -#[cfg(test)] -mod tests { - use db::indoc; - use gpui::{TestAppContext, VisualTestContext, WindowHandle}; - use language::rust_lang; - use pretty_assertions::assert_eq; - use project::FakeFs; - use search::{ - buffer_search, - project_search::{self, perform_project_search}, - }; - use serde_json::json; - use util::path; - use workspace::{OpenOptions, OpenVisible, ToolbarItemView}; - - use super::*; - - const SELECTED_MARKER: &str = " <==== selected"; - - #[gpui::test(iterations = 10)] - async fn test_project_search_results_toggling(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - let root = path!("/rust-analyzer"); - populate_with_test_ra_project(&fs, root).await; - let project = Project::test(fs.clone(), [Path::new(root)], cx).await; - project.read_with(cx, |project, _| project.languages().add(rust_lang())); - let workspace = add_outline_panel(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let outline_panel = outline_panel(&workspace, cx); - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.set_active(true, window, cx) - }); - - workspace - .update(cx, |workspace, window, cx| { - ProjectSearchView::deploy_search( - workspace, - &workspace::DeploySearch::default(), - window, - cx, - ) - }) - .unwrap(); - let search_view = workspace - .update(cx, |workspace, _, cx| { - workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::()) - .expect("Project search view expected to appear after new search event trigger") - }) - .unwrap(); - - let query = "param_names_for_lifetime_elision_hints"; - perform_project_search(&search_view, query, cx); - search_view.update(cx, |search_view, cx| { - search_view - .results_editor() - .update(cx, |results_editor, cx| { - assert_eq!( - results_editor.display_text(cx).match_indices(query).count(), - 9 - ); - }); - }); - - let all_matches = r#"rust-analyzer/ - crates/ - ide/src/ - inlay_hints/ - fn_lifetime_fn.rs - search: match config.«param_names_for_lifetime_elision_hints» { - search: allocated_lifetimes.push(if config.«param_names_for_lifetime_elision_hints» { - search: Some(it) if config.«param_names_for_lifetime_elision_hints» => { - search: InlayHintsConfig { «param_names_for_lifetime_elision_hints»: true, ..TEST_CONFIG }, - inlay_hints.rs - search: pub «param_names_for_lifetime_elision_hints»: bool, - search: «param_names_for_lifetime_elision_hints»: self - static_index.rs - search: «param_names_for_lifetime_elision_hints»: false, - rust-analyzer/src/ - cli/ - analysis_stats.rs - search: «param_names_for_lifetime_elision_hints»: true, - config.rs - search: «param_names_for_lifetime_elision_hints»: self"# - .to_string(); - - let select_first_in_all_matches = |line_to_select: &str| { - assert!( - all_matches.contains(line_to_select), - "`{line_to_select}` was not found in all matches `{all_matches}`" - ); - all_matches.replacen( - line_to_select, - &format!("{line_to_select}{SELECTED_MARKER}"), - 1, - ) - }; - - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - select_first_in_all_matches( - "search: match config.«param_names_for_lifetime_elision_hints» {" - ) - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.select_parent(&SelectParent, window, cx); - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - select_first_in_all_matches("fn_lifetime_fn.rs") - ); - }); - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - format!( - r#"rust-analyzer/ - crates/ - ide/src/ - inlay_hints/ - fn_lifetime_fn.rs{SELECTED_MARKER} - inlay_hints.rs - search: pub «param_names_for_lifetime_elision_hints»: bool, - search: «param_names_for_lifetime_elision_hints»: self - static_index.rs - search: «param_names_for_lifetime_elision_hints»: false, - rust-analyzer/src/ - cli/ - analysis_stats.rs - search: «param_names_for_lifetime_elision_hints»: true, - config.rs - search: «param_names_for_lifetime_elision_hints»: self"#, - ) - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.expand_all_entries(&ExpandAllEntries, window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.select_parent(&SelectParent, window, cx); - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - select_first_in_all_matches("inlay_hints/") - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.select_parent(&SelectParent, window, cx); - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - select_first_in_all_matches("ide/src/") - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - format!( - r#"rust-analyzer/ - crates/ - ide/src/{SELECTED_MARKER} - rust-analyzer/src/ - cli/ - analysis_stats.rs - search: «param_names_for_lifetime_elision_hints»: true, - config.rs - search: «param_names_for_lifetime_elision_hints»: self"#, - ) - ); - }); - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.expand_selected_entry(&ExpandSelectedEntry, window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - select_first_in_all_matches("ide/src/") - ); - }); - } - - #[gpui::test(iterations = 10)] - async fn test_item_filtering(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - let root = path!("/rust-analyzer"); - populate_with_test_ra_project(&fs, root).await; - let project = Project::test(fs.clone(), [Path::new(root)], cx).await; - project.read_with(cx, |project, _| project.languages().add(rust_lang())); - let workspace = add_outline_panel(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let outline_panel = outline_panel(&workspace, cx); - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.set_active(true, window, cx) - }); - - workspace - .update(cx, |workspace, window, cx| { - ProjectSearchView::deploy_search( - workspace, - &workspace::DeploySearch::default(), - window, - cx, - ) - }) - .unwrap(); - let search_view = workspace - .update(cx, |workspace, _, cx| { - workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::()) - .expect("Project search view expected to appear after new search event trigger") - }) - .unwrap(); - - let query = "param_names_for_lifetime_elision_hints"; - perform_project_search(&search_view, query, cx); - search_view.update(cx, |search_view, cx| { - search_view - .results_editor() - .update(cx, |results_editor, cx| { - assert_eq!( - results_editor.display_text(cx).match_indices(query).count(), - 9 - ); - }); - }); - let all_matches = r#"rust-analyzer/ - crates/ - ide/src/ - inlay_hints/ - fn_lifetime_fn.rs - search: match config.«param_names_for_lifetime_elision_hints» { - search: allocated_lifetimes.push(if config.«param_names_for_lifetime_elision_hints» { - search: Some(it) if config.«param_names_for_lifetime_elision_hints» => { - search: InlayHintsConfig { «param_names_for_lifetime_elision_hints»: true, ..TEST_CONFIG }, - inlay_hints.rs - search: pub «param_names_for_lifetime_elision_hints»: bool, - search: «param_names_for_lifetime_elision_hints»: self - static_index.rs - search: «param_names_for_lifetime_elision_hints»: false, - rust-analyzer/src/ - cli/ - analysis_stats.rs - search: «param_names_for_lifetime_elision_hints»: true, - config.rs - search: «param_names_for_lifetime_elision_hints»: self"# - .to_string(); - - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - None, - cx, - ), - all_matches, - ); - }); - - let filter_text = "a"; - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.filter_editor.update(cx, |filter_editor, cx| { - filter_editor.set_text(filter_text, window, cx); - }); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - None, - cx, - ), - all_matches - .lines() - .skip(1) // `/rust-analyzer/` is a root entry with path `` and it will be filtered out - .filter(|item| item.contains(filter_text)) - .collect::>() - .join("\n"), - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.filter_editor.update(cx, |filter_editor, cx| { - filter_editor.set_text("", window, cx); - }); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - None, - cx, - ), - all_matches, - ); - }); - } - - #[gpui::test(iterations = 10)] - async fn test_item_opening(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - let root = path!("/rust-analyzer"); - populate_with_test_ra_project(&fs, root).await; - let project = Project::test(fs.clone(), [Path::new(root)], cx).await; - project.read_with(cx, |project, _| project.languages().add(rust_lang())); - let workspace = add_outline_panel(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let outline_panel = outline_panel(&workspace, cx); - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.set_active(true, window, cx) - }); - - workspace - .update(cx, |workspace, window, cx| { - ProjectSearchView::deploy_search( - workspace, - &workspace::DeploySearch::default(), - window, - cx, - ) - }) - .unwrap(); - let search_view = workspace - .update(cx, |workspace, _, cx| { - workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::()) - .expect("Project search view expected to appear after new search event trigger") - }) - .unwrap(); - - let query = "param_names_for_lifetime_elision_hints"; - perform_project_search(&search_view, query, cx); - search_view.update(cx, |search_view, cx| { - search_view - .results_editor() - .update(cx, |results_editor, cx| { - assert_eq!( - results_editor.display_text(cx).match_indices(query).count(), - 9 - ); - }); - }); - let all_matches = r#"rust-analyzer/ - crates/ - ide/src/ - inlay_hints/ - fn_lifetime_fn.rs - search: match config.«param_names_for_lifetime_elision_hints» { - search: allocated_lifetimes.push(if config.«param_names_for_lifetime_elision_hints» { - search: Some(it) if config.«param_names_for_lifetime_elision_hints» => { - search: InlayHintsConfig { «param_names_for_lifetime_elision_hints»: true, ..TEST_CONFIG }, - inlay_hints.rs - search: pub «param_names_for_lifetime_elision_hints»: bool, - search: «param_names_for_lifetime_elision_hints»: self - static_index.rs - search: «param_names_for_lifetime_elision_hints»: false, - rust-analyzer/src/ - cli/ - analysis_stats.rs - search: «param_names_for_lifetime_elision_hints»: true, - config.rs - search: «param_names_for_lifetime_elision_hints»: self"# - .to_string(); - let select_first_in_all_matches = |line_to_select: &str| { - assert!( - all_matches.contains(line_to_select), - "`{line_to_select}` was not found in all matches `{all_matches}`" - ); - all_matches.replacen( - line_to_select, - &format!("{line_to_select}{SELECTED_MARKER}"), - 1, - ) - }; - let clear_outline_metadata = |input: &str| { - input - .replace("search: ", "") - .replace("«", "") - .replace("»", "") - }; - - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - - let active_editor = outline_panel.read_with(cx, |outline_panel, _| { - outline_panel - .active_editor() - .expect("should have an active editor open") - }); - let initial_outline_selection = - "search: match config.«param_names_for_lifetime_elision_hints» {"; - outline_panel.update_in(cx, |outline_panel, window, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - select_first_in_all_matches(initial_outline_selection) - ); - assert_eq!( - selected_row_text(&active_editor, cx), - clear_outline_metadata(initial_outline_selection), - "Should place the initial editor selection on the corresponding search result" - ); - - outline_panel.select_next(&SelectNext, window, cx); - outline_panel.select_next(&SelectNext, window, cx); - }); - - let navigated_outline_selection = - "search: Some(it) if config.«param_names_for_lifetime_elision_hints» => {"; - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - select_first_in_all_matches(navigated_outline_selection) - ); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - outline_panel.update(cx, |_, cx| { - assert_eq!( - selected_row_text(&active_editor, cx), - clear_outline_metadata(navigated_outline_selection), - "Should still have the initial caret position after SelectNext calls" - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.open_selected_entry(&OpenSelectedEntry, window, cx); - }); - outline_panel.update(cx, |_outline_panel, cx| { - assert_eq!( - selected_row_text(&active_editor, cx), - clear_outline_metadata(navigated_outline_selection), - "After opening, should move the caret to the opened outline entry's position" - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.select_next(&SelectNext, window, cx); - }); - let next_navigated_outline_selection = "search: InlayHintsConfig { «param_names_for_lifetime_elision_hints»: true, ..TEST_CONFIG },"; - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - select_first_in_all_matches(next_navigated_outline_selection) - ); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - outline_panel.update(cx, |_outline_panel, cx| { - assert_eq!( - selected_row_text(&active_editor, cx), - clear_outline_metadata(next_navigated_outline_selection), - "Should again preserve the selection after another SelectNext call" - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.open_excerpts(&editor::actions::OpenExcerpts, window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - let new_active_editor = outline_panel.read_with(cx, |outline_panel, _| { - outline_panel - .active_editor() - .expect("should have an active editor open") - }); - outline_panel.update(cx, |outline_panel, cx| { - assert_ne!( - active_editor, new_active_editor, - "After opening an excerpt, new editor should be open" - ); - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - "outline: pub(super) fn hints -outline: fn hints_lifetimes_named <==== selected" - ); - assert_eq!( - selected_row_text(&new_active_editor, cx), - clear_outline_metadata(next_navigated_outline_selection), - "When opening the excerpt, should navigate to the place corresponding the outline entry" - ); - }); - } - - #[gpui::test] - async fn test_multiple_worktrees(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/root"), - json!({ - "one": { - "a.txt": "aaa aaa" - }, - "two": { - "b.txt": "a aaa" - } - - }), - ) - .await; - let project = Project::test(fs.clone(), [Path::new(path!("/root/one"))], cx).await; - let workspace = add_outline_panel(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let outline_panel = outline_panel(&workspace, cx); - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.set_active(true, window, cx) - }); - - let items = workspace - .update(cx, |workspace, window, cx| { - workspace.open_paths( - vec![PathBuf::from(path!("/root/two"))], - OpenOptions { - visible: Some(OpenVisible::OnlyDirectories), - ..Default::default() - }, - None, - window, - cx, - ) - }) - .unwrap() - .await; - assert_eq!(items.len(), 1, "Were opening another worktree directory"); - assert!( - items[0].is_none(), - "Directory should be opened successfully" - ); - - workspace - .update(cx, |workspace, window, cx| { - ProjectSearchView::deploy_search( - workspace, - &workspace::DeploySearch::default(), - window, - cx, - ) - }) - .unwrap(); - let search_view = workspace - .update(cx, |workspace, _, cx| { - workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::()) - .expect("Project search view expected to appear after new search event trigger") - }) - .unwrap(); - - let query = "aaa"; - perform_project_search(&search_view, query, cx); - search_view.update(cx, |search_view, cx| { - search_view - .results_editor() - .update(cx, |results_editor, cx| { - assert_eq!( - results_editor.display_text(cx).match_indices(query).count(), - 3 - ); - }); - }); - - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - format!( - r#"one/ - a.txt - search: «aaa» aaa <==== selected - search: aaa «aaa» -two/ - b.txt - search: a «aaa»"#, - ), - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.select_previous(&SelectPrevious, window, cx); - outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - format!( - r#"one/ - a.txt <==== selected -two/ - b.txt - search: a «aaa»"#, - ), - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.select_next(&SelectNext, window, cx); - outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - format!( - r#"one/ - a.txt -two/ <==== selected"#, - ), - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.expand_selected_entry(&ExpandSelectedEntry, window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - format!( - r#"one/ - a.txt -two/ <==== selected - b.txt - search: a «aaa»"#, - ) - ); - }); - } - - #[gpui::test] - async fn test_navigating_in_singleton(cx: &mut TestAppContext) { - init_test(cx); - - let root = path!("/root"); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - root, - json!({ - "src": { - "lib.rs": indoc!(" -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -struct OutlineEntryExcerpt { - id: ExcerptId, - buffer_id: BufferId, - range: ExcerptRange, -}"), - } - }), - ) - .await; - let project = Project::test(fs.clone(), [Path::new(root)], cx).await; - project.read_with(cx, |project, _| project.languages().add(rust_lang())); - let workspace = add_outline_panel(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let outline_panel = outline_panel(&workspace, cx); - cx.update(|window, cx| { - outline_panel.update(cx, |outline_panel, cx| { - outline_panel.set_active(true, window, cx) - }); - }); - - let _editor = workspace - .update(cx, |workspace, window, cx| { - workspace.open_abs_path( - PathBuf::from(path!("/root/src/lib.rs")), - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - window, - cx, - ) - }) - .unwrap() - .await - .expect("Failed to open Rust source file") - .downcast::() - .expect("Should open an editor for Rust source file"); - - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: struct OutlineEntryExcerpt - outline: id - outline: buffer_id - outline: range" - ) - ); - }); - - cx.update(|window, cx| { - outline_panel.update(cx, |outline_panel, cx| { - outline_panel.select_next(&SelectNext, window, cx); - }); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: struct OutlineEntryExcerpt <==== selected - outline: id - outline: buffer_id - outline: range" - ) - ); - }); - - cx.update(|window, cx| { - outline_panel.update(cx, |outline_panel, cx| { - outline_panel.select_next(&SelectNext, window, cx); - }); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: struct OutlineEntryExcerpt - outline: id <==== selected - outline: buffer_id - outline: range" - ) - ); - }); - - cx.update(|window, cx| { - outline_panel.update(cx, |outline_panel, cx| { - outline_panel.select_next(&SelectNext, window, cx); - }); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: struct OutlineEntryExcerpt - outline: id - outline: buffer_id <==== selected - outline: range" - ) - ); - }); - - cx.update(|window, cx| { - outline_panel.update(cx, |outline_panel, cx| { - outline_panel.select_next(&SelectNext, window, cx); - }); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: struct OutlineEntryExcerpt - outline: id - outline: buffer_id - outline: range <==== selected" - ) - ); - }); - - cx.update(|window, cx| { - outline_panel.update(cx, |outline_panel, cx| { - outline_panel.select_next(&SelectNext, window, cx); - }); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: struct OutlineEntryExcerpt <==== selected - outline: id - outline: buffer_id - outline: range" - ) - ); - }); - - cx.update(|window, cx| { - outline_panel.update(cx, |outline_panel, cx| { - outline_panel.select_previous(&SelectPrevious, window, cx); - }); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: struct OutlineEntryExcerpt - outline: id - outline: buffer_id - outline: range <==== selected" - ) - ); - }); - - cx.update(|window, cx| { - outline_panel.update(cx, |outline_panel, cx| { - outline_panel.select_previous(&SelectPrevious, window, cx); - }); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: struct OutlineEntryExcerpt - outline: id - outline: buffer_id <==== selected - outline: range" - ) - ); - }); - - cx.update(|window, cx| { - outline_panel.update(cx, |outline_panel, cx| { - outline_panel.select_previous(&SelectPrevious, window, cx); - }); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: struct OutlineEntryExcerpt - outline: id <==== selected - outline: buffer_id - outline: range" - ) - ); - }); - - cx.update(|window, cx| { - outline_panel.update(cx, |outline_panel, cx| { - outline_panel.select_previous(&SelectPrevious, window, cx); - }); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: struct OutlineEntryExcerpt <==== selected - outline: id - outline: buffer_id - outline: range" - ) - ); - }); - - cx.update(|window, cx| { - outline_panel.update(cx, |outline_panel, cx| { - outline_panel.select_previous(&SelectPrevious, window, cx); - }); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: struct OutlineEntryExcerpt - outline: id - outline: buffer_id - outline: range <==== selected" - ) - ); - }); - } - - #[gpui::test(iterations = 10)] - async fn test_frontend_repo_structure(cx: &mut TestAppContext) { - init_test(cx); - - let root = path!("/frontend-project"); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - root, - json!({ - "public": { - "lottie": { - "syntax-tree.json": r#"{ "something": "static" }"# - } - }, - "src": { - "app": { - "(site)": { - "(about)": { - "jobs": { - "[slug]": { - "page.tsx": r#"static"# - } - } - }, - "(blog)": { - "post": { - "[slug]": { - "page.tsx": r#"static"# - } - } - }, - } - }, - "components": { - "ErrorBoundary.tsx": r#"static"#, - } - } - - }), - ) - .await; - let project = Project::test(fs.clone(), [Path::new(root)], cx).await; - let workspace = add_outline_panel(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let outline_panel = outline_panel(&workspace, cx); - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.set_active(true, window, cx) - }); - - workspace - .update(cx, |workspace, window, cx| { - ProjectSearchView::deploy_search( - workspace, - &workspace::DeploySearch::default(), - window, - cx, - ) - }) - .unwrap(); - let search_view = workspace - .update(cx, |workspace, _, cx| { - workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::()) - .expect("Project search view expected to appear after new search event trigger") - }) - .unwrap(); - - let query = "static"; - perform_project_search(&search_view, query, cx); - search_view.update(cx, |search_view, cx| { - search_view - .results_editor() - .update(cx, |results_editor, cx| { - assert_eq!( - results_editor.display_text(cx).match_indices(query).count(), - 4 - ); - }); - }); - - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - format!( - r#"frontend-project/ - public/lottie/ - syntax-tree.json - search: {{ "something": "«static»" }} <==== selected - src/ - app/(site)/ - (about)/jobs/[slug]/ - page.tsx - search: «static» - (blog)/post/[slug]/ - page.tsx - search: «static» - components/ - ErrorBoundary.tsx - search: «static»"# - ) - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - // Move to 5th element in the list, 3 items down. - for _ in 0..2 { - outline_panel.select_next(&SelectNext, window, cx); - } - outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - format!( - r#"frontend-project/ - public/lottie/ - syntax-tree.json - search: {{ "something": "«static»" }} - src/ - app/(site)/ <==== selected - components/ - ErrorBoundary.tsx - search: «static»"# - ) - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - // Move to the next visible non-FS entry - for _ in 0..3 { - outline_panel.select_next(&SelectNext, window, cx); - } - }); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - format!( - r#"frontend-project/ - public/lottie/ - syntax-tree.json - search: {{ "something": "«static»" }} - src/ - app/(site)/ - components/ - ErrorBoundary.tsx - search: «static» <==== selected"# - ) - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel - .active_editor() - .expect("Should have an active editor") - .update(cx, |editor, cx| { - editor.toggle_fold(&editor::actions::ToggleFold, window, cx) - }); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - format!( - r#"frontend-project/ - public/lottie/ - syntax-tree.json - search: {{ "something": "«static»" }} - src/ - app/(site)/ - components/ - ErrorBoundary.tsx <==== selected"# - ) - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel - .active_editor() - .expect("Should have an active editor") - .update(cx, |editor, cx| { - editor.toggle_fold(&editor::actions::ToggleFold, window, cx) - }); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - format!( - r#"frontend-project/ - public/lottie/ - syntax-tree.json - search: {{ "something": "«static»" }} - src/ - app/(site)/ - components/ - ErrorBoundary.tsx <==== selected - search: «static»"# - ) - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.collapse_all_entries(&CollapseAllEntries, window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - format!(r#"frontend-project/"#) - ); - }); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.expand_all_entries(&ExpandAllEntries, window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - format!( - r#"frontend-project/ - public/lottie/ - syntax-tree.json - search: {{ "something": "«static»" }} - src/ - app/(site)/ - (about)/jobs/[slug]/ - page.tsx - search: «static» - (blog)/post/[slug]/ - page.tsx - search: «static» - components/ - ErrorBoundary.tsx <==== selected - search: «static»"# - ) - ); - }); - } - - async fn add_outline_panel( - project: &Entity, - cx: &mut TestAppContext, - ) -> WindowHandle { - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let outline_panel = window - .update(cx, |_, window, cx| { - cx.spawn_in(window, async |this, cx| { - OutlinePanel::load(this, cx.clone()).await - }) - }) - .unwrap() - .await - .expect("Failed to load outline panel"); - - window - .update(cx, |workspace, window, cx| { - workspace.add_panel(outline_panel, window, cx); - }) - .unwrap(); - window - } - - fn outline_panel( - workspace: &WindowHandle, - cx: &mut TestAppContext, - ) -> Entity { - workspace - .update(cx, |workspace, _, cx| { - workspace - .panel::(cx) - .expect("no outline panel") - }) - .unwrap() - } - - fn display_entries( - project: &Entity, - multi_buffer_snapshot: &MultiBufferSnapshot, - cached_entries: &[CachedEntry], - selected_entry: Option<&PanelEntry>, - cx: &mut App, - ) -> String { - let project = project.read(cx); - let mut display_string = String::new(); - for entry in cached_entries { - if !display_string.is_empty() { - display_string += "\n"; - } - for _ in 0..entry.depth { - display_string += " "; - } - display_string += &match &entry.entry { - PanelEntry::Fs(entry) => match entry { - FsEntry::ExternalFile(_) => { - panic!("Did not cover external files with tests") - } - FsEntry::Directory(directory) => { - let path = if let Some(worktree) = project - .worktree_for_id(directory.worktree_id, cx) - .filter(|worktree| { - worktree.read(cx).root_entry() == Some(&directory.entry.entry) - }) { - worktree - .read(cx) - .root_name() - .join(&directory.entry.path) - .as_unix_str() - .to_string() - } else { - directory - .entry - .path - .file_name() - .unwrap_or_default() - .to_string() - }; - format!("{path}/") - } - FsEntry::File(file) => file - .entry - .path - .file_name() - .map(|name| name.to_string()) - .unwrap_or_default(), - }, - PanelEntry::FoldedDirs(folded_dirs) => folded_dirs - .entries - .iter() - .filter_map(|dir| dir.path.file_name()) - .map(|name| name.to_string() + "/") - .collect(), - PanelEntry::Outline(outline_entry) => match outline_entry { - OutlineEntry::Excerpt(_) => continue, - OutlineEntry::Outline(outline_entry) => { - format!("outline: {}", outline_entry.outline.text) - } - }, - PanelEntry::Search(search_entry) => { - let search_data = search_entry.render_data.get_or_init(|| { - SearchData::new(&search_entry.match_range, multi_buffer_snapshot) - }); - let mut search_result = String::new(); - let mut last_end = 0; - for range in &search_data.search_match_indices { - search_result.push_str(&search_data.context_text[last_end..range.start]); - search_result.push('«'); - search_result.push_str(&search_data.context_text[range.start..range.end]); - search_result.push('»'); - last_end = range.end; - } - search_result.push_str(&search_data.context_text[last_end..]); - - format!("search: {search_result}") - } - }; - - if Some(&entry.entry) == selected_entry { - display_string += SELECTED_MARKER; - } - } - display_string - } - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings = SettingsStore::test(cx); - cx.set_global(settings); - - theme::init(theme::LoadThemes::JustBase, cx); - - editor::init(cx); - project_search::init(cx); - buffer_search::init(cx); - super::init(cx); - }); - } - - // Based on https://github.com/rust-lang/rust-analyzer/ - async fn populate_with_test_ra_project(fs: &FakeFs, root: &str) { - fs.insert_tree( - root, - json!({ - "crates": { - "ide": { - "src": { - "inlay_hints": { - "fn_lifetime_fn.rs": r##" - pub(super) fn hints( - acc: &mut Vec, - config: &InlayHintsConfig, - func: ast::Fn, - ) -> Option<()> { - // ... snip - - let mut used_names: FxHashMap = - match config.param_names_for_lifetime_elision_hints { - true => generic_param_list - .iter() - .flat_map(|gpl| gpl.lifetime_params()) - .filter_map(|param| param.lifetime()) - .filter_map(|lt| Some((SmolStr::from(lt.text().as_str().get(1..)?), 0))) - .collect(), - false => Default::default(), - }; - { - let mut potential_lt_refs = potential_lt_refs.iter().filter(|&&(.., is_elided)| is_elided); - if self_param.is_some() && potential_lt_refs.next().is_some() { - allocated_lifetimes.push(if config.param_names_for_lifetime_elision_hints { - // self can't be used as a lifetime, so no need to check for collisions - "'self".into() - } else { - gen_idx_name() - }); - } - potential_lt_refs.for_each(|(name, ..)| { - let name = match name { - Some(it) if config.param_names_for_lifetime_elision_hints => { - if let Some(c) = used_names.get_mut(it.text().as_str()) { - *c += 1; - SmolStr::from(format!("'{text}{c}", text = it.text().as_str())) - } else { - used_names.insert(it.text().as_str().into(), 0); - SmolStr::from_iter(["\'", it.text().as_str()]) - } - } - _ => gen_idx_name(), - }; - allocated_lifetimes.push(name); - }); - } - - // ... snip - } - - // ... snip - - #[test] - fn hints_lifetimes_named() { - check_with_config( - InlayHintsConfig { param_names_for_lifetime_elision_hints: true, ..TEST_CONFIG }, - r#" - fn nested_in<'named>(named: & &X< &()>) {} - // ^'named1, 'named2, 'named3, $ - //^'named1 ^'named2 ^'named3 - "#, - ); - } - - // ... snip - "##, - }, - "inlay_hints.rs": r#" - #[derive(Clone, Debug, PartialEq, Eq)] - pub struct InlayHintsConfig { - // ... snip - pub param_names_for_lifetime_elision_hints: bool, - pub max_length: Option, - // ... snip - } - - impl Config { - pub fn inlay_hints(&self) -> InlayHintsConfig { - InlayHintsConfig { - // ... snip - param_names_for_lifetime_elision_hints: self - .inlayHints_lifetimeElisionHints_useParameterNames() - .to_owned(), - max_length: self.inlayHints_maxLength().to_owned(), - // ... snip - } - } - } - "#, - "static_index.rs": r#" -// ... snip - fn add_file(&mut self, file_id: FileId) { - let current_crate = crates_for(self.db, file_id).pop().map(Into::into); - let folds = self.analysis.folding_ranges(file_id).unwrap(); - let inlay_hints = self - .analysis - .inlay_hints( - &InlayHintsConfig { - // ... snip - closure_style: hir::ClosureStyle::ImplFn, - param_names_for_lifetime_elision_hints: false, - binding_mode_hints: false, - max_length: Some(25), - closure_capture_hints: false, - // ... snip - }, - file_id, - None, - ) - .unwrap(); - // ... snip - } -// ... snip - "# - } - }, - "rust-analyzer": { - "src": { - "cli": { - "analysis_stats.rs": r#" - // ... snip - for &file_id in &file_ids { - _ = analysis.inlay_hints( - &InlayHintsConfig { - // ... snip - implicit_drop_hints: true, - lifetime_elision_hints: ide::LifetimeElisionHints::Always, - param_names_for_lifetime_elision_hints: true, - hide_named_constructor_hints: false, - hide_closure_initialization_hints: false, - closure_style: hir::ClosureStyle::ImplFn, - max_length: Some(25), - closing_brace_hints_min_lines: Some(20), - fields_to_resolve: InlayFieldsToResolve::empty(), - range_exclusive_hints: true, - }, - file_id.into(), - None, - ); - } - // ... snip - "#, - }, - "config.rs": r#" - config_data! { - /// Configs that only make sense when they are set by a client. As such they can only be defined - /// by setting them using client's settings (e.g `settings.json` on VS Code). - client: struct ClientDefaultConfigData <- ClientConfigInput -> { - // ... snip - /// Maximum length for inlay hints. Set to null to have an unlimited length. - inlayHints_maxLength: Option = Some(25), - // ... snip - /// Whether to prefer using parameter names as the name for elided lifetime hints if possible. - inlayHints_lifetimeElisionHints_useParameterNames: bool = false, - // ... snip - } - } - - impl Config { - // ... snip - pub fn inlay_hints(&self) -> InlayHintsConfig { - InlayHintsConfig { - // ... snip - param_names_for_lifetime_elision_hints: self - .inlayHints_lifetimeElisionHints_useParameterNames() - .to_owned(), - max_length: self.inlayHints_maxLength().to_owned(), - // ... snip - } - } - // ... snip - } - "# - } - } - } - }), - ) - .await; - } - - fn snapshot(outline_panel: &OutlinePanel, cx: &App) -> MultiBufferSnapshot { - outline_panel - .active_editor() - .unwrap() - .read(cx) - .buffer() - .read(cx) - .snapshot(cx) - } - - fn selected_row_text(editor: &Entity, cx: &mut App) -> String { - editor.update(cx, |editor, cx| { - let selections = editor.selections.all::(&editor.display_snapshot(cx)); - assert_eq!(selections.len(), 1, "Active editor should have exactly one selection after any outline panel interactions"); - let selection = selections.first().unwrap(); - let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx); - let line_start = language::Point::new(selection.start.row, 0); - let line_end = multi_buffer_snapshot.clip_point(language::Point::new(selection.end.row, u32::MAX), language::Bias::Right); - multi_buffer_snapshot.text_for_range(line_start..line_end).collect::().trim().to_owned() - }) - } - - #[gpui::test] - async fn test_outline_keyboard_expand_collapse(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/test", - json!({ - "src": { - "lib.rs": indoc!(" - mod outer { - pub struct OuterStruct { - field: String, - } - impl OuterStruct { - pub fn new() -> Self { - Self { field: String::new() } - } - pub fn method(&self) { - println!(\"{}\", self.field); - } - } - mod inner { - pub fn inner_function() { - let x = 42; - println!(\"{}\", x); - } - pub struct InnerStruct { - value: i32, - } - } - } - fn main() { - let s = outer::OuterStruct::new(); - s.method(); - } - "), - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; - project.read_with(cx, |project, _| project.languages().add(rust_lang())); - let workspace = add_outline_panel(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let outline_panel = outline_panel(&workspace, cx); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.set_active(true, window, cx) - }); - - workspace - .update(cx, |workspace, window, cx| { - workspace.open_abs_path( - PathBuf::from("/test/src/lib.rs"), - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - window, - cx, - ) - }) - .unwrap() - .await - .unwrap(); - - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500)); - cx.run_until_parked(); - - // Force another update cycle to ensure outlines are fetched - outline_panel.update_in(cx, |panel, window, cx| { - panel.update_non_fs_items(window, cx); - panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500)); - cx.run_until_parked(); - - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: mod outer <==== selected - outline: pub struct OuterStruct - outline: field - outline: impl OuterStruct - outline: pub fn new - outline: pub fn method - outline: mod inner - outline: pub fn inner_function - outline: pub struct InnerStruct - outline: value -outline: fn main" - ) - ); - }); - - let parent_outline = outline_panel - .read_with(cx, |panel, _cx| { - panel - .cached_entries - .iter() - .find_map(|entry| match &entry.entry { - PanelEntry::Outline(OutlineEntry::Outline(outline)) - if panel - .outline_children_cache - .get(&outline.buffer_id) - .and_then(|children_map| { - let key = - (outline.outline.range.clone(), outline.outline.depth); - children_map.get(&key) - }) - .copied() - .unwrap_or(false) => - { - Some(entry.entry.clone()) - } - _ => None, - }) - }) - .expect("Should find an outline with children"); - - outline_panel.update_in(cx, |panel, window, cx| { - panel.select_entry(parent_outline.clone(), true, window, cx); - panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: mod outer <==== selected -outline: fn main" - ) - ); - }); - - outline_panel.update_in(cx, |panel, window, cx| { - panel.expand_selected_entry(&ExpandSelectedEntry, window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: mod outer <==== selected - outline: pub struct OuterStruct - outline: field - outline: impl OuterStruct - outline: pub fn new - outline: pub fn method - outline: mod inner - outline: pub fn inner_function - outline: pub struct InnerStruct - outline: value -outline: fn main" - ) - ); - }); - - outline_panel.update_in(cx, |panel, window, cx| { - panel.collapsed_entries.clear(); - panel.update_cached_entries(None, window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - - outline_panel.update_in(cx, |panel, window, cx| { - let outlines_with_children: Vec<_> = panel - .cached_entries - .iter() - .filter_map(|entry| match &entry.entry { - PanelEntry::Outline(OutlineEntry::Outline(outline)) - if panel - .outline_children_cache - .get(&outline.buffer_id) - .and_then(|children_map| { - let key = (outline.outline.range.clone(), outline.outline.depth); - children_map.get(&key) - }) - .copied() - .unwrap_or(false) => - { - Some(entry.entry.clone()) - } - _ => None, - }) - .collect(); - - for outline in outlines_with_children { - panel.select_entry(outline, false, window, cx); - panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx); - } - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: mod outer -outline: fn main" - ) - ); - }); - - let collapsed_entries_count = - outline_panel.read_with(cx, |panel, _| panel.collapsed_entries.len()); - assert!( - collapsed_entries_count > 0, - "Should have collapsed entries tracked" - ); - } - - #[gpui::test] - async fn test_outline_click_toggle_behavior(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/test", - json!({ - "src": { - "main.rs": indoc!(" - struct Config { - name: String, - value: i32, - } - impl Config { - fn new(name: String) -> Self { - Self { name, value: 0 } - } - fn get_value(&self) -> i32 { - self.value - } - } - enum Status { - Active, - Inactive, - } - fn process_config(config: Config) -> Status { - if config.get_value() > 0 { - Status::Active - } else { - Status::Inactive - } - } - fn main() { - let config = Config::new(\"test\".to_string()); - let status = process_config(config); - } - "), - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; - project.read_with(cx, |project, _| project.languages().add(rust_lang())); - - let workspace = add_outline_panel(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let outline_panel = outline_panel(&workspace, cx); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.set_active(true, window, cx) - }); - - let _editor = workspace - .update(cx, |workspace, window, cx| { - workspace.open_abs_path( - PathBuf::from("/test/src/main.rs"), - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - window, - cx, - ) - }) - .unwrap() - .await - .unwrap(); - - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - - outline_panel.update(cx, |outline_panel, _cx| { - outline_panel.selected_entry = SelectedEntry::None; - }); - - // Check initial state - all entries should be expanded by default - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: struct Config - outline: name - outline: value -outline: impl Config - outline: fn new - outline: fn get_value -outline: enum Status - outline: Active - outline: Inactive -outline: fn process_config -outline: fn main" - ) - ); - }); - - outline_panel.update(cx, |outline_panel, _cx| { - outline_panel.selected_entry = SelectedEntry::None; - }); - - cx.update(|window, cx| { - outline_panel.update(cx, |outline_panel, cx| { - outline_panel.select_first(&SelectFirst, window, cx); - }); - }); - - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: struct Config <==== selected - outline: name - outline: value -outline: impl Config - outline: fn new - outline: fn get_value -outline: enum Status - outline: Active - outline: Inactive -outline: fn process_config -outline: fn main" - ) - ); - }); - - cx.update(|window, cx| { - outline_panel.update(cx, |outline_panel, cx| { - outline_panel.collapse_selected_entry(&CollapseSelectedEntry, window, cx); - }); - }); - - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: struct Config <==== selected -outline: impl Config - outline: fn new - outline: fn get_value -outline: enum Status - outline: Active - outline: Inactive -outline: fn process_config -outline: fn main" - ) - ); - }); - - cx.update(|window, cx| { - outline_panel.update(cx, |outline_panel, cx| { - outline_panel.expand_selected_entry(&ExpandSelectedEntry, window, cx); - }); - }); - - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: struct Config <==== selected - outline: name - outline: value -outline: impl Config - outline: fn new - outline: fn get_value -outline: enum Status - outline: Active - outline: Inactive -outline: fn process_config -outline: fn main" - ) - ); - }); - } - - #[gpui::test] - async fn test_outline_expand_collapse_all(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/test", - json!({ - "src": { - "lib.rs": indoc!(" - mod outer { - pub struct OuterStruct { - field: String, - } - impl OuterStruct { - pub fn new() -> Self { - Self { field: String::new() } - } - pub fn method(&self) { - println!(\"{}\", self.field); - } - } - mod inner { - pub fn inner_function() { - let x = 42; - println!(\"{}\", x); - } - pub struct InnerStruct { - value: i32, - } - } - } - fn main() { - let s = outer::OuterStruct::new(); - s.method(); - } - "), - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; - project.read_with(cx, |project, _| project.languages().add(rust_lang())); - let workspace = add_outline_panel(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let outline_panel = outline_panel(&workspace, cx); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.set_active(true, window, cx) - }); - - workspace - .update(cx, |workspace, window, cx| { - workspace.open_abs_path( - PathBuf::from("/test/src/lib.rs"), - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - window, - cx, - ) - }) - .unwrap() - .await - .unwrap(); - - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500)); - cx.run_until_parked(); - - // Force another update cycle to ensure outlines are fetched - outline_panel.update_in(cx, |panel, window, cx| { - panel.update_non_fs_items(window, cx); - panel.update_cached_entries(Some(UPDATE_DEBOUNCE), window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500)); - cx.run_until_parked(); - - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - indoc!( - " -outline: mod outer <==== selected - outline: pub struct OuterStruct - outline: field - outline: impl OuterStruct - outline: pub fn new - outline: pub fn method - outline: mod inner - outline: pub fn inner_function - outline: pub struct InnerStruct - outline: value -outline: fn main" - ) - ); - }); - - let _parent_outline = outline_panel - .read_with(cx, |panel, _cx| { - panel - .cached_entries - .iter() - .find_map(|entry| match &entry.entry { - PanelEntry::Outline(OutlineEntry::Outline(outline)) - if panel - .outline_children_cache - .get(&outline.buffer_id) - .and_then(|children_map| { - let key = - (outline.outline.range.clone(), outline.outline.depth); - children_map.get(&key) - }) - .copied() - .unwrap_or(false) => - { - Some(entry.entry.clone()) - } - _ => None, - }) - }) - .expect("Should find an outline with children"); - - // Collapse all entries - outline_panel.update_in(cx, |panel, window, cx| { - panel.collapse_all_entries(&CollapseAllEntries, window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - - let expected_collapsed_output = indoc!( - " - outline: mod outer <==== selected - outline: fn main" - ); - - outline_panel.update(cx, |panel, cx| { - assert_eq! { - display_entries( - &project, - &snapshot(panel, cx), - &panel.cached_entries, - panel.selected_entry(), - cx, - ), - expected_collapsed_output - }; - }); - - // Expand all entries - outline_panel.update_in(cx, |panel, window, cx| { - panel.expand_all_entries(&ExpandAllEntries, window, cx); - }); - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(100)); - cx.run_until_parked(); - - let expected_expanded_output = indoc!( - " - outline: mod outer <==== selected - outline: pub struct OuterStruct - outline: field - outline: impl OuterStruct - outline: pub fn new - outline: pub fn method - outline: mod inner - outline: pub fn inner_function - outline: pub struct InnerStruct - outline: value - outline: fn main" - ); - - outline_panel.update(cx, |panel, cx| { - assert_eq! { - display_entries( - &project, - &snapshot(panel, cx), - &panel.cached_entries, - panel.selected_entry(), - cx, - ), - expected_expanded_output - }; - }); - } - - #[gpui::test] - async fn test_buffer_search(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/test", - json!({ - "foo.txt": r#"<_constitution> - - - - - -## 📊 Output - -| Field | Meaning | -"# - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; - let workspace = add_outline_panel(&project, cx).await; - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - let editor = workspace - .update(cx, |workspace, window, cx| { - workspace.open_abs_path( - PathBuf::from("/test/foo.txt"), - OpenOptions { - visible: Some(OpenVisible::All), - ..OpenOptions::default() - }, - window, - cx, - ) - }) - .unwrap() - .await - .unwrap() - .downcast::() - .unwrap(); - - let search_bar = workspace - .update(cx, |_, window, cx| { - cx.new(|cx| { - let mut search_bar = BufferSearchBar::new(None, window, cx); - search_bar.set_active_pane_item(Some(&editor), window, cx); - search_bar.show(window, cx); - search_bar - }) - }) - .unwrap(); - - let outline_panel = outline_panel(&workspace, cx); - - outline_panel.update_in(cx, |outline_panel, window, cx| { - outline_panel.set_active(true, window, cx) - }); - - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search(" ", None, true, window, cx) - }) - .await - .unwrap(); - - cx.executor() - .advance_clock(UPDATE_DEBOUNCE + Duration::from_millis(500)); - cx.run_until_parked(); - - outline_panel.update(cx, |outline_panel, cx| { - assert_eq!( - display_entries( - &project, - &snapshot(outline_panel, cx), - &outline_panel.cached_entries, - outline_panel.selected_entry(), - cx, - ), - "search: | Field« » | Meaning | <==== selected -search: | Field « » | Meaning | -search: | Field « » | Meaning | -search: | Field « » | Meaning | -search: | Field « »| Meaning | -search: | Field | Meaning« » | -search: | Field | Meaning « » | -search: | Field | Meaning « » | -search: | Field | Meaning « » | -search: | Field | Meaning « » | -search: | Field | Meaning « » | -search: | Field | Meaning « » | -search: | Field | Meaning « »|" - ); - }); - } -} diff --git a/crates/outline_panel/src/outline_panel_settings.rs b/crates/outline_panel/src/outline_panel_settings.rs deleted file mode 100644 index b2b1a6fe68..0000000000 --- a/crates/outline_panel/src/outline_panel_settings.rs +++ /dev/null @@ -1,66 +0,0 @@ -use editor::EditorSettings; -use gpui::{App, Pixels}; -use settings::RegisterSetting; -pub use settings::{DockSide, Settings, ShowIndentGuides}; -use ui::scrollbars::{ScrollbarVisibility, ShowScrollbar}; - -#[derive(Debug, Clone, Copy, PartialEq, RegisterSetting)] -pub struct OutlinePanelSettings { - pub button: bool, - pub default_width: Pixels, - pub dock: DockSide, - pub file_icons: bool, - pub folder_icons: bool, - pub git_status: bool, - pub indent_size: f32, - pub indent_guides: IndentGuidesSettings, - pub auto_reveal_entries: bool, - pub auto_fold_dirs: bool, - pub scrollbar: ScrollbarSettings, - pub expand_outlines_with_depth: usize, -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct ScrollbarSettings { - /// When to show the scrollbar in the project panel. - /// - /// Default: inherits editor scrollbar settings - pub show: Option, -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct IndentGuidesSettings { - pub show: ShowIndentGuides, -} - -impl ScrollbarVisibility for OutlinePanelSettings { - fn visibility(&self, cx: &App) -> ShowScrollbar { - self.scrollbar - .show - .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show) - } -} - -impl Settings for OutlinePanelSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - let panel = content.outline_panel.as_ref().unwrap(); - Self { - button: panel.button.unwrap(), - default_width: panel.default_width.map(gpui::px).unwrap(), - dock: panel.dock.unwrap(), - file_icons: panel.file_icons.unwrap(), - folder_icons: panel.folder_icons.unwrap(), - git_status: panel.git_status.unwrap(), - indent_size: panel.indent_size.unwrap(), - indent_guides: IndentGuidesSettings { - show: panel.indent_guides.unwrap().show.unwrap(), - }, - auto_reveal_entries: panel.auto_reveal_entries.unwrap(), - auto_fold_dirs: panel.auto_fold_dirs.unwrap(), - scrollbar: ScrollbarSettings { - show: panel.scrollbar.unwrap().show.map(Into::into), - }, - expand_outlines_with_depth: panel.expand_outlines_with_depth.unwrap(), - } - } -} diff --git a/crates/panel/Cargo.toml b/crates/panel/Cargo.toml deleted file mode 100644 index 3c51e6d6dc..0000000000 --- a/crates/panel/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "panel" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/panel.rs" - -[dependencies] -editor.workspace = true -gpui.workspace = true -settings.workspace = true -theme.workspace = true -ui.workspace = true -workspace.workspace = true diff --git a/crates/panel/LICENSE-GPL b/crates/panel/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/panel/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/panel/src/panel.rs b/crates/panel/src/panel.rs deleted file mode 100644 index 1930f654e9..0000000000 --- a/crates/panel/src/panel.rs +++ /dev/null @@ -1,136 +0,0 @@ -//! # panel -use editor::{Editor, EditorElement, EditorStyle}; -use gpui::{Entity, TextStyle, actions}; -use settings::Settings; -use theme::ThemeSettings; -use ui::{Tab, prelude::*}; - -actions!( - panel, - [ - /// Navigates to the next tab in the panel. - NextPanelTab, - /// Navigates to the previous tab in the panel. - PreviousPanelTab - ] -); - -pub trait PanelHeader: workspace::Panel { - fn header_height(&self, cx: &mut App) -> Pixels { - Tab::container_height(cx) - } - - fn panel_header_container(&self, _window: &mut Window, cx: &mut App) -> Div { - h_flex() - .h(self.header_height(cx)) - .w_full() - .px_1() - .flex_none() - } -} - -/// Implement this trait to enable a panel to have tabs. -pub trait PanelTabs: PanelHeader { - /// Returns the index of the currently selected tab. - fn selected_tab(&self, cx: &mut App) -> usize; - /// Selects the tab at the given index. - fn select_tab(&self, cx: &mut App, index: usize); - /// Moves to the next tab. - fn next_tab(&self, _: NextPanelTab, cx: &mut App) -> Self; - /// Moves to the previous tab. - fn previous_tab(&self, _: PreviousPanelTab, cx: &mut App) -> Self; -} - -#[derive(IntoElement)] -pub struct PanelTab {} - -impl RenderOnce for PanelTab { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - div() - } -} - -pub fn panel_button(label: impl Into) -> ui::Button { - let label = label.into(); - let id = ElementId::Name(label.to_lowercase().replace(' ', "_").into()); - ui::Button::new(id, label) - .label_size(ui::LabelSize::Small) - .icon_size(ui::IconSize::Small) - // TODO: Change this once we use on_surface_bg in button_like - .layer(ui::ElevationIndex::ModalSurface) - .size(ui::ButtonSize::Compact) -} - -pub fn panel_filled_button(label: impl Into) -> ui::Button { - panel_button(label).style(ui::ButtonStyle::Filled) -} - -pub fn panel_icon_button(id: impl Into, icon: IconName) -> ui::IconButton { - let id = ElementId::Name(id.into()); - - IconButton::new(id, icon) - // TODO: Change this once we use on_surface_bg in button_like - .layer(ui::ElevationIndex::ModalSurface) -} - -pub fn panel_filled_icon_button(id: impl Into, icon: IconName) -> ui::IconButton { - panel_icon_button(id, icon).style(ui::ButtonStyle::Filled) -} - -pub fn panel_editor_container(_window: &mut Window, cx: &mut App) -> Div { - v_flex() - .size_full() - .gap(px(8.)) - .p_2() - .bg(cx.theme().colors().editor_background) -} - -pub fn panel_editor_style(monospace: bool, window: &Window, cx: &App) -> EditorStyle { - let settings = ThemeSettings::get_global(cx); - - let font_size = TextSize::Small.rems(cx).to_pixels(window.rem_size()); - - let (font_family, font_fallbacks, font_features, font_weight, line_height) = if monospace { - ( - settings.buffer_font.family.clone(), - settings.buffer_font.fallbacks.clone(), - settings.buffer_font.features.clone(), - settings.buffer_font.weight, - font_size * settings.buffer_line_height.value(), - ) - } else { - ( - settings.ui_font.family.clone(), - settings.ui_font.fallbacks.clone(), - settings.ui_font.features.clone(), - settings.ui_font.weight, - window.line_height(), - ) - }; - - EditorStyle { - background: cx.theme().colors().editor_background, - local_player: cx.theme().players().local(), - text: TextStyle { - color: cx.theme().colors().text, - font_family, - font_fallbacks, - font_features, - font_size: TextSize::Small.rems(cx).into(), - font_weight, - line_height: line_height.into(), - ..Default::default() - }, - syntax: cx.theme().syntax().clone(), - ..Default::default() - } -} - -pub fn panel_editor_element( - editor: &Entity, - monospace: bool, - window: &mut Window, - cx: &mut App, -) -> EditorElement { - EditorElement::new(editor, panel_editor_style(monospace, window, cx)) -} diff --git a/crates/paths/Cargo.toml b/crates/paths/Cargo.toml deleted file mode 100644 index 24da7d46e9..0000000000 --- a/crates/paths/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "paths" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[features] -test-support = [] - -[lib] -path = "src/paths.rs" - -[dependencies] -dirs.workspace = true -ignore.workspace = true -util.workspace = true diff --git a/crates/paths/LICENSE-GPL b/crates/paths/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/paths/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/paths/src/paths.rs b/crates/paths/src/paths.rs deleted file mode 100644 index a6aa8354b4..0000000000 --- a/crates/paths/src/paths.rs +++ /dev/null @@ -1,566 +0,0 @@ -//! Paths to locations used by Zed. - -use std::env; -use std::path::{Path, PathBuf}; -use std::sync::{LazyLock, OnceLock}; - -pub use util::paths::home_dir; -use util::rel_path::RelPath; - -/// A default editorconfig file name to use when resolving project settings. -pub const EDITORCONFIG_NAME: &str = ".editorconfig"; - -/// A custom data directory override, set only by `set_custom_data_dir`. -/// This is used to override the default data directory location. -/// The directory will be created if it doesn't exist when set. -static CUSTOM_DATA_DIR: OnceLock = OnceLock::new(); - -/// The resolved data directory, combining custom override or platform defaults. -/// This is set once and cached for subsequent calls. -/// On macOS, this is `~/Library/Application Support/Zed`. -/// On Linux/FreeBSD, this is `$XDG_DATA_HOME/zed`. -/// On Windows, this is `%LOCALAPPDATA%\Zed`. -static CURRENT_DATA_DIR: OnceLock = OnceLock::new(); - -/// The resolved config directory, combining custom override or platform defaults. -/// This is set once and cached for subsequent calls. -/// On macOS, this is `~/.config/zed`. -/// On Linux/FreeBSD, this is `$XDG_CONFIG_HOME/zed`. -/// On Windows, this is `%APPDATA%\Zed`. -static CONFIG_DIR: OnceLock = OnceLock::new(); - -/// Returns the relative path to the zed_server directory on the ssh host. -pub fn remote_server_dir_relative() -> &'static RelPath { - static CACHED: LazyLock<&'static RelPath> = - LazyLock::new(|| RelPath::unix(".zed_server").unwrap()); - *CACHED -} - -/// Returns the relative path to the zed_wsl_server directory on the wsl host. -pub fn remote_wsl_server_dir_relative() -> &'static RelPath { - static CACHED: LazyLock<&'static RelPath> = - LazyLock::new(|| RelPath::unix(".zed_wsl_server").unwrap()); - *CACHED -} - -/// Sets a custom directory for all user data, overriding the default data directory. -/// This function must be called before any other path operations that depend on the data directory. -/// The directory's path will be canonicalized to an absolute path by a blocking FS operation. -/// The directory will be created if it doesn't exist. -/// -/// # Arguments -/// -/// * `dir` - The path to use as the custom data directory. This will be used as the base -/// directory for all user data, including databases, extensions, and logs. -/// -/// # Returns -/// -/// A reference to the static `PathBuf` containing the custom data directory path. -/// -/// # Panics -/// -/// Panics if: -/// * Called after the data directory has been initialized (e.g., via `data_dir` or `config_dir`) -/// * The directory's path cannot be canonicalized to an absolute path -/// * The directory cannot be created -pub fn set_custom_data_dir(dir: &str) -> &'static PathBuf { - if CURRENT_DATA_DIR.get().is_some() || CONFIG_DIR.get().is_some() { - panic!("set_custom_data_dir called after data_dir or config_dir was initialized"); - } - CUSTOM_DATA_DIR.get_or_init(|| { - let mut path = PathBuf::from(dir); - if path.is_relative() && path.exists() { - let abs_path = path - .canonicalize() - .expect("failed to canonicalize custom data directory's path to an absolute path"); - path = util::paths::SanitizedPath::new(&abs_path).into() - } - std::fs::create_dir_all(&path).expect("failed to create custom data directory"); - path - }) -} - -/// Returns the path to the configuration directory used by Zed. -pub fn config_dir() -> &'static PathBuf { - CONFIG_DIR.get_or_init(|| { - if let Some(custom_dir) = CUSTOM_DATA_DIR.get() { - custom_dir.join("config") - } else if cfg!(target_os = "windows") { - dirs::config_dir() - .expect("failed to determine RoamingAppData directory") - .join("Zed") - } else if cfg!(any(target_os = "linux", target_os = "freebsd")) { - if let Ok(flatpak_xdg_config) = std::env::var("FLATPAK_XDG_CONFIG_HOME") { - flatpak_xdg_config.into() - } else { - dirs::config_dir().expect("failed to determine XDG_CONFIG_HOME directory") - } - .join("zed") - } else { - home_dir().join(".config").join("zed") - } - }) -} - -/// Returns the path to the data directory used by Zed. -pub fn data_dir() -> &'static PathBuf { - CURRENT_DATA_DIR.get_or_init(|| { - if let Some(custom_dir) = CUSTOM_DATA_DIR.get() { - custom_dir.clone() - } else if cfg!(target_os = "macos") { - home_dir().join("Library/Application Support/Zed") - } else if cfg!(any(target_os = "linux", target_os = "freebsd")) { - if let Ok(flatpak_xdg_data) = std::env::var("FLATPAK_XDG_DATA_HOME") { - flatpak_xdg_data.into() - } else { - dirs::data_local_dir().expect("failed to determine XDG_DATA_HOME directory") - } - .join("zed") - } else if cfg!(target_os = "windows") { - dirs::data_local_dir() - .expect("failed to determine LocalAppData directory") - .join("Zed") - } else { - config_dir().clone() // Fallback - } - }) -} - -/// Returns the path to the temp directory used by Zed. -pub fn temp_dir() -> &'static PathBuf { - static TEMP_DIR: OnceLock = OnceLock::new(); - TEMP_DIR.get_or_init(|| { - if cfg!(target_os = "macos") { - return dirs::cache_dir() - .expect("failed to determine cachesDirectory directory") - .join("Zed"); - } - - if cfg!(target_os = "windows") { - return dirs::cache_dir() - .expect("failed to determine LocalAppData directory") - .join("Zed"); - } - - if cfg!(any(target_os = "linux", target_os = "freebsd")) { - return if let Ok(flatpak_xdg_cache) = std::env::var("FLATPAK_XDG_CACHE_HOME") { - flatpak_xdg_cache.into() - } else { - dirs::cache_dir().expect("failed to determine XDG_CACHE_HOME directory") - } - .join("zed"); - } - - home_dir().join(".cache").join("zed") - }) -} - -/// Returns the path to the hang traces directory. -pub fn hang_traces_dir() -> &'static PathBuf { - static LOGS_DIR: OnceLock = OnceLock::new(); - LOGS_DIR.get_or_init(|| data_dir().join("hang_traces")) -} - -/// Returns the path to the logs directory. -pub fn logs_dir() -> &'static PathBuf { - static LOGS_DIR: OnceLock = OnceLock::new(); - LOGS_DIR.get_or_init(|| { - if cfg!(target_os = "macos") { - home_dir().join("Library/Logs/Zed") - } else { - data_dir().join("logs") - } - }) -} - -/// Returns the path to the Zed server directory on this SSH host. -pub fn remote_server_state_dir() -> &'static PathBuf { - static REMOTE_SERVER_STATE: OnceLock = OnceLock::new(); - REMOTE_SERVER_STATE.get_or_init(|| data_dir().join("server_state")) -} - -/// Returns the path to the `Zed.log` file. -pub fn log_file() -> &'static PathBuf { - static LOG_FILE: OnceLock = OnceLock::new(); - LOG_FILE.get_or_init(|| logs_dir().join("Zed.log")) -} - -/// Returns the path to the `Zed.log.old` file. -pub fn old_log_file() -> &'static PathBuf { - static OLD_LOG_FILE: OnceLock = OnceLock::new(); - OLD_LOG_FILE.get_or_init(|| logs_dir().join("Zed.log.old")) -} - -/// Returns the path to the database directory. -pub fn database_dir() -> &'static PathBuf { - static DATABASE_DIR: OnceLock = OnceLock::new(); - DATABASE_DIR.get_or_init(|| data_dir().join("db")) -} - -/// Returns the path to the crashes directory, if it exists for the current platform. -pub fn crashes_dir() -> &'static Option { - static CRASHES_DIR: OnceLock> = OnceLock::new(); - CRASHES_DIR.get_or_init(|| { - cfg!(target_os = "macos").then_some(home_dir().join("Library/Logs/DiagnosticReports")) - }) -} - -/// Returns the path to the retired crashes directory, if it exists for the current platform. -pub fn crashes_retired_dir() -> &'static Option { - static CRASHES_RETIRED_DIR: OnceLock> = OnceLock::new(); - CRASHES_RETIRED_DIR.get_or_init(|| crashes_dir().as_ref().map(|dir| dir.join("Retired"))) -} - -/// Returns the path to the `settings.json` file. -pub fn settings_file() -> &'static PathBuf { - static SETTINGS_FILE: OnceLock = OnceLock::new(); - SETTINGS_FILE.get_or_init(|| config_dir().join("settings.json")) -} - -/// Returns the path to the global settings file. -pub fn global_settings_file() -> &'static PathBuf { - static GLOBAL_SETTINGS_FILE: OnceLock = OnceLock::new(); - GLOBAL_SETTINGS_FILE.get_or_init(|| config_dir().join("global_settings.json")) -} - -/// Returns the path to the `settings_backup.json` file. -pub fn settings_backup_file() -> &'static PathBuf { - static SETTINGS_FILE: OnceLock = OnceLock::new(); - SETTINGS_FILE.get_or_init(|| config_dir().join("settings_backup.json")) -} - -/// Returns the path to the `keymap.json` file. -pub fn keymap_file() -> &'static PathBuf { - static KEYMAP_FILE: OnceLock = OnceLock::new(); - KEYMAP_FILE.get_or_init(|| config_dir().join("keymap.json")) -} - -/// Returns the path to the `keymap_backup.json` file. -pub fn keymap_backup_file() -> &'static PathBuf { - static KEYMAP_FILE: OnceLock = OnceLock::new(); - KEYMAP_FILE.get_or_init(|| config_dir().join("keymap_backup.json")) -} - -/// Returns the path to the `tasks.json` file. -pub fn tasks_file() -> &'static PathBuf { - static TASKS_FILE: OnceLock = OnceLock::new(); - TASKS_FILE.get_or_init(|| config_dir().join("tasks.json")) -} - -/// Returns the path to the `debug.json` file. -pub fn debug_scenarios_file() -> &'static PathBuf { - static DEBUG_SCENARIOS_FILE: OnceLock = OnceLock::new(); - DEBUG_SCENARIOS_FILE.get_or_init(|| config_dir().join("debug.json")) -} - -/// Returns the path to the extensions directory. -/// -/// This is where installed extensions are stored. -pub fn extensions_dir() -> &'static PathBuf { - static EXTENSIONS_DIR: OnceLock = OnceLock::new(); - EXTENSIONS_DIR.get_or_init(|| data_dir().join("extensions")) -} - -/// Returns the path to the extensions directory. -/// -/// This is where installed extensions are stored on a remote. -pub fn remote_extensions_dir() -> &'static PathBuf { - static EXTENSIONS_DIR: OnceLock = OnceLock::new(); - EXTENSIONS_DIR.get_or_init(|| data_dir().join("remote_extensions")) -} - -/// Returns the path to the extensions directory. -/// -/// This is where installed extensions are stored on a remote. -pub fn remote_extensions_uploads_dir() -> &'static PathBuf { - static UPLOAD_DIR: OnceLock = OnceLock::new(); - UPLOAD_DIR.get_or_init(|| remote_extensions_dir().join("uploads")) -} - -/// Returns the path to the themes directory. -/// -/// This is where themes that are not provided by extensions are stored. -pub fn themes_dir() -> &'static PathBuf { - static THEMES_DIR: OnceLock = OnceLock::new(); - THEMES_DIR.get_or_init(|| config_dir().join("themes")) -} - -/// Returns the path to the snippets directory. -pub fn snippets_dir() -> &'static PathBuf { - static SNIPPETS_DIR: OnceLock = OnceLock::new(); - SNIPPETS_DIR.get_or_init(|| config_dir().join("snippets")) -} - -/// Returns the path to the contexts directory. -/// -/// This is where the saved contexts from the Assistant are stored. -pub fn text_threads_dir() -> &'static PathBuf { - static CONTEXTS_DIR: OnceLock = OnceLock::new(); - CONTEXTS_DIR.get_or_init(|| { - if cfg!(target_os = "macos") { - config_dir().join("conversations") - } else { - data_dir().join("conversations") - } - }) -} - -/// Returns the path to the contexts directory. -/// -/// This is where the prompts for use with the Assistant are stored. -pub fn prompts_dir() -> &'static PathBuf { - static PROMPTS_DIR: OnceLock = OnceLock::new(); - PROMPTS_DIR.get_or_init(|| { - if cfg!(target_os = "macos") { - config_dir().join("prompts") - } else { - data_dir().join("prompts") - } - }) -} - -/// Returns the path to the prompt templates directory. -/// -/// This is where the prompt templates for core features can be overridden with templates. -/// -/// # Arguments -/// -/// * `dev_mode` - If true, assumes the current working directory is the Zed repository. -pub fn prompt_overrides_dir(repo_path: Option<&Path>) -> PathBuf { - if let Some(path) = repo_path { - let dev_path = path.join("assets").join("prompts"); - if dev_path.exists() { - return dev_path; - } - } - - static PROMPT_TEMPLATES_DIR: OnceLock = OnceLock::new(); - PROMPT_TEMPLATES_DIR - .get_or_init(|| { - if cfg!(target_os = "macos") { - config_dir().join("prompt_overrides") - } else { - data_dir().join("prompt_overrides") - } - }) - .clone() -} - -/// Returns the path to the semantic search's embeddings directory. -/// -/// This is where the embeddings used to power semantic search are stored. -pub fn embeddings_dir() -> &'static PathBuf { - static EMBEDDINGS_DIR: OnceLock = OnceLock::new(); - EMBEDDINGS_DIR.get_or_init(|| { - if cfg!(target_os = "macos") { - config_dir().join("embeddings") - } else { - data_dir().join("embeddings") - } - }) -} - -/// Returns the path to the languages directory. -/// -/// This is where language servers are downloaded to for languages built-in to Zed. -pub fn languages_dir() -> &'static PathBuf { - static LANGUAGES_DIR: OnceLock = OnceLock::new(); - LANGUAGES_DIR.get_or_init(|| data_dir().join("languages")) -} - -/// Returns the path to the debug adapters directory -/// -/// This is where debug adapters are downloaded to for DAPs that are built-in to Zed. -pub fn debug_adapters_dir() -> &'static PathBuf { - static DEBUG_ADAPTERS_DIR: OnceLock = OnceLock::new(); - DEBUG_ADAPTERS_DIR.get_or_init(|| data_dir().join("debug_adapters")) -} - -/// Returns the path to the external agents directory -/// -/// This is where agent servers are downloaded to -pub fn external_agents_dir() -> &'static PathBuf { - static EXTERNAL_AGENTS_DIR: OnceLock = OnceLock::new(); - EXTERNAL_AGENTS_DIR.get_or_init(|| data_dir().join("external_agents")) -} - -/// Returns the path to the Copilot directory. -pub fn copilot_dir() -> &'static PathBuf { - static COPILOT_DIR: OnceLock = OnceLock::new(); - COPILOT_DIR.get_or_init(|| data_dir().join("copilot")) -} - -/// Returns the path to the Supermaven directory. -pub fn supermaven_dir() -> &'static PathBuf { - static SUPERMAVEN_DIR: OnceLock = OnceLock::new(); - SUPERMAVEN_DIR.get_or_init(|| data_dir().join("supermaven")) -} - -/// Returns the path to the default Prettier directory. -pub fn default_prettier_dir() -> &'static PathBuf { - static DEFAULT_PRETTIER_DIR: OnceLock = OnceLock::new(); - DEFAULT_PRETTIER_DIR.get_or_init(|| data_dir().join("prettier")) -} - -/// Returns the path to the remote server binaries directory. -pub fn remote_servers_dir() -> &'static PathBuf { - static REMOTE_SERVERS_DIR: OnceLock = OnceLock::new(); - REMOTE_SERVERS_DIR.get_or_init(|| data_dir().join("remote_servers")) -} - -/// Returns the path to the directory where the devcontainer CLI is installed. -pub fn devcontainer_dir() -> &'static PathBuf { - static DEVCONTAINER_DIR: OnceLock = OnceLock::new(); - DEVCONTAINER_DIR.get_or_init(|| data_dir().join("devcontainer")) -} - -/// Returns the relative path to a `.zed` folder within a project. -pub fn local_settings_folder_name() -> &'static str { - ".zed" -} - -/// Returns the relative path to a `.vscode` folder within a project. -pub fn local_vscode_folder_name() -> &'static str { - ".vscode" -} - -/// Returns the relative path to a `settings.json` file within a project. -pub fn local_settings_file_relative_path() -> &'static RelPath { - static CACHED: LazyLock<&'static RelPath> = - LazyLock::new(|| RelPath::unix(".zed/settings.json").unwrap()); - *CACHED -} - -/// Returns the relative path to a `tasks.json` file within a project. -pub fn local_tasks_file_relative_path() -> &'static RelPath { - static CACHED: LazyLock<&'static RelPath> = - LazyLock::new(|| RelPath::unix(".zed/tasks.json").unwrap()); - *CACHED -} - -/// Returns the relative path to a `.vscode/tasks.json` file within a project. -pub fn local_vscode_tasks_file_relative_path() -> &'static RelPath { - static CACHED: LazyLock<&'static RelPath> = - LazyLock::new(|| RelPath::unix(".vscode/tasks.json").unwrap()); - *CACHED -} - -pub fn debug_task_file_name() -> &'static str { - "debug.json" -} - -pub fn task_file_name() -> &'static str { - "tasks.json" -} - -/// Returns the relative path to a `debug.json` file within a project. -/// .zed/debug.json -pub fn local_debug_file_relative_path() -> &'static RelPath { - static CACHED: LazyLock<&'static RelPath> = - LazyLock::new(|| RelPath::unix(".zed/debug.json").unwrap()); - *CACHED -} - -/// Returns the relative path to a `.vscode/launch.json` file within a project. -pub fn local_vscode_launch_file_relative_path() -> &'static RelPath { - static CACHED: LazyLock<&'static RelPath> = - LazyLock::new(|| RelPath::unix(".vscode/launch.json").unwrap()); - *CACHED -} - -pub fn user_ssh_config_file() -> PathBuf { - home_dir().join(".ssh/config") -} - -pub fn global_ssh_config_file() -> Option<&'static Path> { - if cfg!(windows) { - None - } else { - Some(Path::new("/etc/ssh/ssh_config")) - } -} - -/// Returns candidate paths for the vscode user settings file -pub fn vscode_settings_file_paths() -> Vec { - let mut paths = vscode_user_data_paths(); - for path in paths.iter_mut() { - path.push("User/settings.json"); - } - paths -} - -/// Returns candidate paths for the cursor user settings file -pub fn cursor_settings_file_paths() -> Vec { - let mut paths = cursor_user_data_paths(); - for path in paths.iter_mut() { - path.push("User/settings.json"); - } - paths -} - -fn vscode_user_data_paths() -> Vec { - // https://github.com/microsoft/vscode/blob/23e7148cdb6d8a27f0109ff77e5b1e019f8da051/src/vs/platform/environment/node/userDataPath.ts#L45 - const VSCODE_PRODUCT_NAMES: &[&str] = &[ - "Code", - "Code - OSS", - "VSCodium", - "Code Dev", - "Code - OSS Dev", - "code-oss-dev", - ]; - let mut paths = Vec::new(); - if let Ok(portable_path) = env::var("VSCODE_PORTABLE") { - paths.push(Path::new(&portable_path).join("user-data")); - } - if let Ok(vscode_appdata) = env::var("VSCODE_APPDATA") { - for product_name in VSCODE_PRODUCT_NAMES { - paths.push(Path::new(&vscode_appdata).join(product_name)); - } - } - for product_name in VSCODE_PRODUCT_NAMES { - add_vscode_user_data_paths(&mut paths, product_name); - } - paths -} - -fn cursor_user_data_paths() -> Vec { - let mut paths = Vec::new(); - add_vscode_user_data_paths(&mut paths, "Cursor"); - paths -} - -fn add_vscode_user_data_paths(paths: &mut Vec, product_name: &str) { - if cfg!(target_os = "macos") { - paths.push( - home_dir() - .join("Library/Application Support") - .join(product_name), - ); - } else if cfg!(target_os = "windows") { - if let Some(data_local_dir) = dirs::data_local_dir() { - paths.push(data_local_dir.join(product_name)); - } - if let Some(data_dir) = dirs::data_dir() { - paths.push(data_dir.join(product_name)); - } - } else { - paths.push( - dirs::config_dir() - .unwrap_or(home_dir().join(".config")) - .join(product_name), - ); - } -} - -#[cfg(any(test, feature = "test-support"))] -pub fn global_gitignore_path() -> Option { - Some(home_dir().join(".config").join("git").join("ignore")) -} - -#[cfg(not(any(test, feature = "test-support")))] -pub fn global_gitignore_path() -> Option { - static GLOBAL_GITIGNORE_PATH: OnceLock> = OnceLock::new(); - GLOBAL_GITIGNORE_PATH - .get_or_init(::ignore::gitignore::gitconfig_excludes_path) - .clone() -} diff --git a/crates/picker/Cargo.toml b/crates/picker/Cargo.toml deleted file mode 100644 index 1344d177f4..0000000000 --- a/crates/picker/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "picker" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/picker.rs" -doctest = false - -[features] -test-support = [] - -[dependencies] -anyhow.workspace = true -editor.workspace = true -gpui.workspace = true -menu.workspace = true -schemars.workspace = true -serde.workspace = true -theme.workspace = true -ui.workspace = true -workspace.workspace = true - -[dev-dependencies] -ctor.workspace = true -editor = { workspace = true, features = ["test-support"] } -env_logger.workspace = true -gpui = { workspace = true, features = ["test-support"] } -serde_json.workspace = true diff --git a/crates/picker/LICENSE-GPL b/crates/picker/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/picker/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/picker/src/head.rs b/crates/picker/src/head.rs deleted file mode 100644 index 700896e341..0000000000 --- a/crates/picker/src/head.rs +++ /dev/null @@ -1,68 +0,0 @@ -use std::sync::Arc; - -use editor::{Editor, EditorEvent}; -use gpui::{App, Entity, FocusHandle, Focusable, prelude::*}; -use ui::prelude::*; - -/// The head of a [`Picker`](crate::Picker). -pub(crate) enum Head { - /// Picker has an editor that allows the user to filter the list. - Editor(Entity), - - /// Picker has no head, it's just a list of items. - Empty(Entity), -} - -impl Head { - pub fn editor( - placeholder_text: Arc, - edit_handler: impl FnMut(&mut V, &Entity, &EditorEvent, &mut Window, &mut Context) - + 'static, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let editor = cx.new(|cx| { - let mut editor = Editor::single_line(window, cx); - editor.set_placeholder_text(placeholder_text.as_ref(), window, cx); - editor - }); - cx.subscribe_in(&editor, window, edit_handler).detach(); - Self::Editor(editor) - } - - pub fn empty( - blur_handler: impl FnMut(&mut V, &mut Window, &mut Context) + 'static, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let head = cx.new(EmptyHead::new); - cx.on_blur(&head.focus_handle(cx), window, blur_handler) - .detach(); - Self::Empty(head) - } -} - -/// An invisible element that can hold focus. -pub(crate) struct EmptyHead { - focus_handle: FocusHandle, -} - -impl EmptyHead { - fn new(cx: &mut Context) -> Self { - Self { - focus_handle: cx.focus_handle(), - } - } -} - -impl Render for EmptyHead { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - div().track_focus(&self.focus_handle(cx)) - } -} - -impl Focusable for EmptyHead { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.focus_handle.clone() - } -} diff --git a/crates/picker/src/highlighted_match_with_paths.rs b/crates/picker/src/highlighted_match_with_paths.rs deleted file mode 100644 index 7427104762..0000000000 --- a/crates/picker/src/highlighted_match_with_paths.rs +++ /dev/null @@ -1,116 +0,0 @@ -use ui::{HighlightedLabel, prelude::*}; - -#[derive(Clone)] -pub struct HighlightedMatchWithPaths { - pub prefix: Option, - pub match_label: HighlightedMatch, - pub paths: Vec, -} - -#[derive(Debug, Clone, IntoElement)] -pub struct HighlightedMatch { - pub text: String, - pub highlight_positions: Vec, - pub color: Color, -} - -impl HighlightedMatch { - pub fn join(components: impl Iterator, separator: &str) -> Self { - // Track a running byte offset and insert separators between parts. - let mut first = true; - let mut byte_offset = 0; - let mut text = String::new(); - let mut highlight_positions = Vec::new(); - for component in components { - if !first { - text.push_str(separator); - byte_offset += separator.len(); - } - first = false; - - highlight_positions.extend( - component - .highlight_positions - .iter() - .map(|position| position + byte_offset), - ); - text.push_str(&component.text); - byte_offset += component.text.len(); - } - - Self { - text, - highlight_positions, - color: Color::Default, - } - } - - pub fn color(self, color: Color) -> Self { - Self { color, ..self } - } -} -impl RenderOnce for HighlightedMatch { - fn render(self, _window: &mut Window, _: &mut App) -> impl IntoElement { - HighlightedLabel::new(self.text, self.highlight_positions).color(self.color) - } -} - -impl HighlightedMatchWithPaths { - pub fn render_paths_children(&mut self, element: Div) -> Div { - element.children(self.paths.clone().into_iter().map(|path| { - HighlightedLabel::new(path.text, path.highlight_positions) - .size(LabelSize::Small) - .color(Color::Muted) - })) - } -} - -impl RenderOnce for HighlightedMatchWithPaths { - fn render(mut self, _window: &mut Window, _: &mut App) -> impl IntoElement { - v_flex() - .child( - h_flex().gap_1().child(self.match_label.clone()).when_some( - self.prefix.as_ref(), - |this, prefix| { - this.child(Label::new(format!("({})", prefix)).color(Color::Muted)) - }, - ), - ) - .when(!self.paths.is_empty(), |this| { - self.render_paths_children(this) - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn join_offsets_positions_by_bytes_not_chars() { - // "αβγ" is 3 Unicode scalar values, 6 bytes in UTF-8. - let left_text = "αβγ".to_string(); - let right_text = "label".to_string(); - let left = HighlightedMatch { - text: left_text, - highlight_positions: vec![], - color: Color::Default, - }; - let right = HighlightedMatch { - text: right_text, - highlight_positions: vec![0, 1], - color: Color::Default, - }; - let joined = HighlightedMatch::join([left, right].into_iter(), ""); - - assert!( - joined - .highlight_positions - .iter() - .all(|&p| joined.text.is_char_boundary(p)), - "join produced non-boundary positions {:?} for text {:?}", - joined.highlight_positions, - joined.text - ); - } -} diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs deleted file mode 100644 index 3d6ae27dfa..0000000000 --- a/crates/picker/src/picker.rs +++ /dev/null @@ -1,976 +0,0 @@ -mod head; -pub mod highlighted_match_with_paths; -pub mod popover_menu; - -use anyhow::Result; -use editor::{ - Editor, SelectionEffects, - actions::{MoveDown, MoveUp}, - scroll::Autoscroll, -}; -use gpui::{ - Action, AnyElement, App, ClickEvent, Context, DismissEvent, Entity, EventEmitter, FocusHandle, - Focusable, Length, ListSizingBehavior, ListState, MouseButton, MouseUpEvent, Render, - ScrollStrategy, Task, UniformListScrollHandle, Window, actions, div, list, prelude::*, - uniform_list, -}; -use head::Head; -use schemars::JsonSchema; -use serde::Deserialize; -use std::{ops::Range, sync::Arc, time::Duration}; -use theme::ThemeSettings; -use ui::{ - Color, Divider, DocumentationAside, DocumentationEdge, DocumentationSide, Label, ListItem, - ListItemSpacing, ScrollAxes, Scrollbars, WithScrollbar, prelude::*, utils::WithRemSize, v_flex, -}; -use workspace::{ModalView, item::Settings}; - -enum ElementContainer { - List(ListState), - UniformList(UniformListScrollHandle), -} - -pub enum Direction { - Up, - Down, -} - -actions!( - picker, - [ - /// Confirms the selected completion in the picker. - ConfirmCompletion - ] -); - -/// ConfirmInput is an alternative editor action which - instead of selecting active picker entry - treats pickers editor input literally, -/// performing some kind of action on it. -#[derive(Clone, PartialEq, Deserialize, JsonSchema, Default, Action)] -#[action(namespace = picker)] -#[serde(deny_unknown_fields)] -pub struct ConfirmInput { - pub secondary: bool, -} - -struct PendingUpdateMatches { - delegate_update_matches: Option>, - _task: Task>, -} - -pub struct Picker { - pub delegate: D, - element_container: ElementContainer, - head: Head, - pending_update_matches: Option, - confirm_on_update: Option, - width: Option, - widest_item: Option, - max_height: Option, - /// An external control to display a scrollbar in the `Picker`. - show_scrollbar: bool, - /// Whether the `Picker` is rendered as a self-contained modal. - /// - /// Set this to `false` when rendering the `Picker` as part of a larger modal. - is_modal: bool, -} - -#[derive(Debug, Default, Clone, Copy, PartialEq)] -pub enum PickerEditorPosition { - #[default] - /// Render the editor at the start of the picker. Usually the top - Start, - /// Render the editor at the end of the picker. Usually the bottom - End, -} - -pub trait PickerDelegate: Sized + 'static { - type ListItem: IntoElement; - - fn match_count(&self) -> usize; - fn selected_index(&self) -> usize; - fn separators_after_indices(&self) -> Vec { - Vec::new() - } - fn set_selected_index( - &mut self, - ix: usize, - window: &mut Window, - cx: &mut Context>, - ); - - /// Called before the picker handles `SelectPrevious` or `SelectNext`. Return `Some(query)` to - /// set a new query and prevent the default selection behavior. - fn select_history( - &mut self, - _direction: Direction, - _query: &str, - _window: &mut Window, - _cx: &mut App, - ) -> Option { - None - } - fn can_select( - &mut self, - _ix: usize, - _window: &mut Window, - _cx: &mut Context>, - ) -> bool { - true - } - - // Allows binding some optional effect to when the selection changes. - fn selected_index_changed( - &self, - _ix: usize, - _window: &mut Window, - _cx: &mut Context>, - ) -> Option> { - None - } - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc; - fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option { - Some("No matches".into()) - } - fn update_matches( - &mut self, - query: String, - window: &mut Window, - cx: &mut Context>, - ) -> Task<()>; - - // Delegates that support this method (e.g. the CommandPalette) can chose to block on any background - // work for up to `duration` to try and get a result synchronously. - // This avoids a flash of an empty command-palette on cmd-shift-p, and lets workspace::SendKeystrokes - // mostly work when dismissing a palette. - fn finalize_update_matches( - &mut self, - _query: String, - _duration: Duration, - _window: &mut Window, - _cx: &mut Context>, - ) -> bool { - false - } - - /// Override if you want to have update the query instead of confirming. - fn confirm_update_query( - &mut self, - _window: &mut Window, - _cx: &mut Context>, - ) -> Option { - None - } - fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context>); - /// Instead of interacting with currently selected entry, treats editor input literally, - /// performing some kind of action on it. - fn confirm_input( - &mut self, - _secondary: bool, - _window: &mut Window, - _: &mut Context>, - ) { - } - fn dismissed(&mut self, window: &mut Window, cx: &mut Context>); - fn should_dismiss(&self) -> bool { - true - } - fn confirm_completion( - &mut self, - _query: String, - _window: &mut Window, - _: &mut Context>, - ) -> Option { - None - } - - fn editor_position(&self) -> PickerEditorPosition { - PickerEditorPosition::default() - } - - fn render_editor( - &self, - editor: &Entity, - _window: &mut Window, - _cx: &mut Context>, - ) -> Div { - v_flex() - .when( - self.editor_position() == PickerEditorPosition::End, - |this| this.child(Divider::horizontal()), - ) - .child( - h_flex() - .overflow_hidden() - .flex_none() - .h_9() - .px_2p5() - .child(editor.clone()), - ) - .when( - self.editor_position() == PickerEditorPosition::Start, - |this| this.child(Divider::horizontal()), - ) - } - - fn render_match( - &self, - ix: usize, - selected: bool, - window: &mut Window, - cx: &mut Context>, - ) -> Option; - - fn render_header( - &self, - _window: &mut Window, - _: &mut Context>, - ) -> Option { - None - } - - fn render_footer( - &self, - _window: &mut Window, - _: &mut Context>, - ) -> Option { - None - } - - fn documentation_aside( - &self, - _window: &mut Window, - _cx: &mut Context>, - ) -> Option { - None - } -} - -impl Focusable for Picker { - fn focus_handle(&self, cx: &App) -> FocusHandle { - match &self.head { - Head::Editor(editor) => editor.focus_handle(cx), - Head::Empty(head) => head.focus_handle(cx), - } - } -} - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -enum ContainerKind { - List, - UniformList, -} - -impl Picker { - /// A picker, which displays its matches using `gpui::uniform_list`, all matches should have the same height. - /// The picker allows the user to perform search items by text. - /// If `PickerDelegate::render_match` can return items with different heights, use `Picker::list`. - pub fn uniform_list(delegate: D, window: &mut Window, cx: &mut Context) -> Self { - let head = Head::editor( - delegate.placeholder_text(window, cx), - Self::on_input_editor_event, - window, - cx, - ); - - Self::new(delegate, ContainerKind::UniformList, head, window, cx) - } - - /// A picker, which displays its matches using `gpui::uniform_list`, all matches should have the same height. - /// If `PickerDelegate::render_match` can return items with different heights, use `Picker::list`. - pub fn nonsearchable_uniform_list( - delegate: D, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let head = Head::empty(Self::on_empty_head_blur, window, cx); - - Self::new(delegate, ContainerKind::UniformList, head, window, cx) - } - - /// A picker, which displays its matches using `gpui::list`, matches can have different heights. - /// The picker allows the user to perform search items by text. - /// If `PickerDelegate::render_match` only returns items with the same height, use `Picker::uniform_list` as its implementation is optimized for that. - pub fn nonsearchable_list(delegate: D, window: &mut Window, cx: &mut Context) -> Self { - let head = Head::empty(Self::on_empty_head_blur, window, cx); - - Self::new(delegate, ContainerKind::List, head, window, cx) - } - - /// A picker, which displays its matches using `gpui::list`, matches can have different heights. - /// The picker allows the user to perform search items by text. - /// If `PickerDelegate::render_match` only returns items with the same height, use `Picker::uniform_list` as its implementation is optimized for that. - pub fn list(delegate: D, window: &mut Window, cx: &mut Context) -> Self { - let head = Head::editor( - delegate.placeholder_text(window, cx), - Self::on_input_editor_event, - window, - cx, - ); - - Self::new(delegate, ContainerKind::List, head, window, cx) - } - - fn new( - delegate: D, - container: ContainerKind, - head: Head, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let element_container = Self::create_element_container(container); - let mut this = Self { - delegate, - head, - element_container, - pending_update_matches: None, - confirm_on_update: None, - width: None, - widest_item: None, - max_height: Some(rems(24.).into()), - show_scrollbar: false, - is_modal: true, - }; - this.update_matches("".to_string(), window, cx); - // give the delegate 4ms to render the first set of suggestions. - this.delegate - .finalize_update_matches("".to_string(), Duration::from_millis(4), window, cx); - this - } - - fn create_element_container(container: ContainerKind) -> ElementContainer { - match container { - ContainerKind::UniformList => { - ElementContainer::UniformList(UniformListScrollHandle::new()) - } - ContainerKind::List => { - ElementContainer::List(ListState::new(0, gpui::ListAlignment::Top, px(1000.))) - } - } - } - - pub fn width(mut self, width: impl Into) -> Self { - self.width = Some(width.into()); - self - } - - pub fn widest_item(mut self, ix: Option) -> Self { - self.widest_item = ix; - self - } - - pub fn max_height(mut self, max_height: Option) -> Self { - self.max_height = max_height; - self - } - - pub fn show_scrollbar(mut self, show_scrollbar: bool) -> Self { - self.show_scrollbar = show_scrollbar; - self - } - - pub fn modal(mut self, modal: bool) -> Self { - self.is_modal = modal; - self - } - - pub fn list_measure_all(mut self) -> Self { - match self.element_container { - ElementContainer::List(state) => { - self.element_container = ElementContainer::List(state.measure_all()); - } - _ => {} - } - self - } - - pub fn focus(&self, window: &mut Window, cx: &mut App) { - self.focus_handle(cx).focus(window); - } - - /// Handles the selecting an index, and passing the change to the delegate. - /// If `fallback_direction` is set to `None`, the index will not be selected - /// if the element at that index cannot be selected. - /// If `fallback_direction` is set to - /// `Some(..)`, the next selectable element will be selected in the - /// specified direction (Down or Up), cycling through all elements until - /// finding one that can be selected or returning if there are no selectable elements. - /// If `scroll_to_index` is true, the new selected index will be scrolled into - /// view. - /// - /// If some effect is bound to `selected_index_changed`, it will be executed. - pub fn set_selected_index( - &mut self, - mut ix: usize, - fallback_direction: Option, - scroll_to_index: bool, - window: &mut Window, - cx: &mut Context, - ) { - let match_count = self.delegate.match_count(); - if match_count == 0 { - return; - } - - if let Some(bias) = fallback_direction { - let mut curr_ix = ix; - while !self.delegate.can_select(curr_ix, window, cx) { - curr_ix = match bias { - Direction::Down => { - if curr_ix == match_count - 1 { - 0 - } else { - curr_ix + 1 - } - } - Direction::Up => { - if curr_ix == 0 { - match_count - 1 - } else { - curr_ix - 1 - } - } - }; - // There is no item that can be selected - if ix == curr_ix { - return; - } - } - ix = curr_ix; - } else if !self.delegate.can_select(ix, window, cx) { - return; - } - - let previous_index = self.delegate.selected_index(); - self.delegate.set_selected_index(ix, window, cx); - let current_index = self.delegate.selected_index(); - - if previous_index != current_index { - if let Some(action) = self.delegate.selected_index_changed(ix, window, cx) { - action(window, cx); - } - if scroll_to_index { - self.scroll_to_item_index(ix); - } - } - } - - pub fn select_next( - &mut self, - _: &menu::SelectNext, - window: &mut Window, - cx: &mut Context, - ) { - let query = self.query(cx); - if let Some(query) = self - .delegate - .select_history(Direction::Down, &query, window, cx) - { - self.set_query(query, window, cx); - return; - } - let count = self.delegate.match_count(); - if count > 0 { - let index = self.delegate.selected_index(); - let ix = if index == count - 1 { 0 } else { index + 1 }; - self.set_selected_index(ix, Some(Direction::Down), true, window, cx); - cx.notify(); - } - } - - pub fn editor_move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context) { - self.select_previous(&Default::default(), window, cx); - } - - fn select_previous( - &mut self, - _: &menu::SelectPrevious, - window: &mut Window, - cx: &mut Context, - ) { - let query = self.query(cx); - if let Some(query) = self - .delegate - .select_history(Direction::Up, &query, window, cx) - { - self.set_query(query, window, cx); - return; - } - let count = self.delegate.match_count(); - if count > 0 { - let index = self.delegate.selected_index(); - let ix = if index == 0 { count - 1 } else { index - 1 }; - self.set_selected_index(ix, Some(Direction::Up), true, window, cx); - cx.notify(); - } - } - - pub fn editor_move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut Context) { - self.select_next(&Default::default(), window, cx); - } - - pub fn select_first( - &mut self, - _: &menu::SelectFirst, - window: &mut Window, - cx: &mut Context, - ) { - let count = self.delegate.match_count(); - if count > 0 { - self.set_selected_index(0, Some(Direction::Down), true, window, cx); - cx.notify(); - } - } - - fn select_last(&mut self, _: &menu::SelectLast, window: &mut Window, cx: &mut Context) { - let count = self.delegate.match_count(); - if count > 0 { - self.set_selected_index(count - 1, Some(Direction::Up), true, window, cx); - cx.notify(); - } - } - - pub fn cycle_selection(&mut self, window: &mut Window, cx: &mut Context) { - let count = self.delegate.match_count(); - let index = self.delegate.selected_index(); - let new_index = if index + 1 == count { 0 } else { index + 1 }; - self.set_selected_index(new_index, Some(Direction::Down), true, window, cx); - cx.notify(); - } - - pub fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context) { - if self.delegate.should_dismiss() { - self.delegate.dismissed(window, cx); - cx.emit(DismissEvent); - } - } - - fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) { - if self.pending_update_matches.is_some() - && !self.delegate.finalize_update_matches( - self.query(cx), - Duration::from_millis(16), - window, - cx, - ) - { - self.confirm_on_update = Some(false) - } else { - self.pending_update_matches.take(); - self.do_confirm(false, window, cx); - } - } - - fn secondary_confirm( - &mut self, - _: &menu::SecondaryConfirm, - window: &mut Window, - cx: &mut Context, - ) { - if self.pending_update_matches.is_some() - && !self.delegate.finalize_update_matches( - self.query(cx), - Duration::from_millis(16), - window, - cx, - ) - { - self.confirm_on_update = Some(true) - } else { - self.do_confirm(true, window, cx); - } - } - - fn confirm_input(&mut self, input: &ConfirmInput, window: &mut Window, cx: &mut Context) { - self.delegate.confirm_input(input.secondary, window, cx); - } - - fn confirm_completion( - &mut self, - _: &ConfirmCompletion, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(new_query) = self.delegate.confirm_completion(self.query(cx), window, cx) { - self.set_query(new_query, window, cx); - } else { - cx.propagate() - } - } - - fn handle_click( - &mut self, - ix: usize, - secondary: bool, - window: &mut Window, - cx: &mut Context, - ) { - cx.stop_propagation(); - window.prevent_default(); - self.set_selected_index(ix, None, false, window, cx); - self.do_confirm(secondary, window, cx) - } - - fn do_confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context) { - if let Some(update_query) = self.delegate.confirm_update_query(window, cx) { - self.set_query(update_query, window, cx); - self.set_selected_index(0, Some(Direction::Down), false, window, cx); - } else { - self.delegate.confirm(secondary, window, cx) - } - } - - fn on_input_editor_event( - &mut self, - _: &Entity, - event: &editor::EditorEvent, - window: &mut Window, - cx: &mut Context, - ) { - let Head::Editor(editor) = &self.head else { - panic!("unexpected call"); - }; - match event { - editor::EditorEvent::BufferEdited => { - let query = editor.read(cx).text(cx); - self.update_matches(query, window, cx); - } - editor::EditorEvent::Blurred => { - if self.is_modal && window.is_window_active() { - self.cancel(&menu::Cancel, window, cx); - } - } - _ => {} - } - } - - fn on_empty_head_blur(&mut self, window: &mut Window, cx: &mut Context) { - let Head::Empty(_) = &self.head else { - panic!("unexpected call"); - }; - if window.is_window_active() { - self.cancel(&menu::Cancel, window, cx); - } - } - - pub fn refresh_placeholder(&mut self, window: &mut Window, cx: &mut App) { - match &self.head { - Head::Editor(editor) => { - let placeholder = self.delegate.placeholder_text(window, cx); - editor.update(cx, |editor, cx| { - editor.set_placeholder_text(placeholder.as_ref(), window, cx); - cx.notify(); - }); - } - Head::Empty(_) => {} - } - } - - pub fn refresh(&mut self, window: &mut Window, cx: &mut Context) { - let query = self.query(cx); - self.update_matches(query, window, cx); - } - - pub fn update_matches(&mut self, query: String, window: &mut Window, cx: &mut Context) { - let delegate_pending_update_matches = self.delegate.update_matches(query, window, cx); - - self.matches_updated(window, cx); - // This struct ensures that we can synchronously drop the task returned by the - // delegate's `update_matches` method and the task that the picker is spawning. - // If we simply capture the delegate's task into the picker's task, when the picker's - // task gets synchronously dropped, the delegate's task would keep running until - // the picker's task has a chance of being scheduled, because dropping a task happens - // asynchronously. - self.pending_update_matches = Some(PendingUpdateMatches { - delegate_update_matches: Some(delegate_pending_update_matches), - _task: cx.spawn_in(window, async move |this, cx| { - let delegate_pending_update_matches = this.update(cx, |this, _| { - this.pending_update_matches - .as_mut() - .unwrap() - .delegate_update_matches - .take() - .unwrap() - })?; - delegate_pending_update_matches.await; - this.update_in(cx, |this, window, cx| { - this.matches_updated(window, cx); - }) - }), - }); - } - - fn matches_updated(&mut self, window: &mut Window, cx: &mut Context) { - if let ElementContainer::List(state) = &mut self.element_container { - state.reset(self.delegate.match_count()); - } - - let index = self.delegate.selected_index(); - self.scroll_to_item_index(index); - self.pending_update_matches = None; - if let Some(secondary) = self.confirm_on_update.take() { - self.do_confirm(secondary, window, cx); - } - cx.notify(); - } - - pub fn query(&self, cx: &App) -> String { - match &self.head { - Head::Editor(editor) => editor.read(cx).text(cx), - Head::Empty(_) => "".to_string(), - } - } - - pub fn set_query(&self, query: impl Into>, window: &mut Window, cx: &mut App) { - if let Head::Editor(editor) = &self.head { - editor.update(cx, |editor, cx| { - editor.set_text(query, window, cx); - let editor_offset = editor.buffer().read(cx).len(cx); - editor.change_selections( - SelectionEffects::scroll(Autoscroll::Next), - window, - cx, - |s| s.select_ranges(Some(editor_offset..editor_offset)), - ); - }); - } - } - - fn scroll_to_item_index(&mut self, ix: usize) { - match &mut self.element_container { - ElementContainer::List(state) => state.scroll_to_reveal_item(ix), - ElementContainer::UniformList(scroll_handle) => { - scroll_handle.scroll_to_item(ix, ScrollStrategy::Nearest) - } - } - } - - fn render_element( - &self, - window: &mut Window, - cx: &mut Context, - ix: usize, - ) -> impl IntoElement + use { - div() - .id(("item", ix)) - .cursor_pointer() - .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| { - this.handle_click(ix, event.modifiers().secondary(), window, cx) - })) - // As of this writing, GPUI intercepts `ctrl-[mouse-event]`s on macOS - // and produces right mouse button events. This matches platforms norms - // but means that UIs which depend on holding ctrl down (such as the tab - // switcher) can't be clicked on. Hence, this handler. - .on_mouse_up( - MouseButton::Right, - cx.listener(move |this, event: &MouseUpEvent, window, cx| { - // We specifically want to use the platform key here, as - // ctrl will already be held down for the tab switcher. - this.handle_click(ix, event.modifiers.platform, window, cx) - }), - ) - .children(self.delegate.render_match( - ix, - ix == self.delegate.selected_index(), - window, - cx, - )) - .when( - self.delegate.separators_after_indices().contains(&ix), - |picker| { - picker - .border_color(cx.theme().colors().border_variant) - .border_b_1() - .py(px(-1.0)) - }, - ) - } - - fn render_element_container(&self, cx: &mut Context) -> impl IntoElement { - let sizing_behavior = if self.max_height.is_some() { - ListSizingBehavior::Infer - } else { - ListSizingBehavior::Auto - }; - - match &self.element_container { - ElementContainer::UniformList(scroll_handle) => uniform_list( - "candidates", - self.delegate.match_count(), - cx.processor(move |picker, visible_range: Range, window, cx| { - visible_range - .map(|ix| picker.render_element(window, cx, ix)) - .collect() - }), - ) - .with_sizing_behavior(sizing_behavior) - .when_some(self.widest_item, |el, widest_item| { - el.with_width_from_item(Some(widest_item)) - }) - .flex_grow() - .py_1() - .track_scroll(&scroll_handle) - .into_any_element(), - ElementContainer::List(state) => list( - state.clone(), - cx.processor(|this, ix, window, cx| { - this.render_element(window, cx, ix).into_any_element() - }), - ) - .with_sizing_behavior(sizing_behavior) - .flex_grow() - .py_2() - .into_any_element(), - } - } - - #[cfg(any(test, feature = "test-support"))] - pub fn logical_scroll_top_index(&self) -> usize { - match &self.element_container { - ElementContainer::List(state) => state.logical_scroll_top().item_ix, - ElementContainer::UniformList(scroll_handle) => { - scroll_handle.logical_scroll_top_index() - } - } - } -} - -impl EventEmitter for Picker {} -impl ModalView for Picker {} - -impl Render for Picker { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx); - let window_size = window.viewport_size(); - let rem_size = window.rem_size(); - let is_wide_window = window_size.width / rem_size > rems_from_px(800.).0; - - let aside = self.delegate.documentation_aside(window, cx); - - let editor_position = self.delegate.editor_position(); - let menu = v_flex() - .key_context("Picker") - .size_full() - .when_some(self.width, |el, width| el.w(width)) - .overflow_hidden() - // This is a bit of a hack to remove the modal styling when we're rendering the `Picker` - // as a part of a modal rather than the entire modal. - // - // We should revisit how the `Picker` is styled to make it more composable. - .when(self.is_modal, |this| this.elevation_3(cx)) - .on_action(cx.listener(Self::select_next)) - .on_action(cx.listener(Self::select_previous)) - .on_action(cx.listener(Self::editor_move_down)) - .on_action(cx.listener(Self::editor_move_up)) - .on_action(cx.listener(Self::select_first)) - .on_action(cx.listener(Self::select_last)) - .on_action(cx.listener(Self::cancel)) - .on_action(cx.listener(Self::confirm)) - .on_action(cx.listener(Self::secondary_confirm)) - .on_action(cx.listener(Self::confirm_completion)) - .on_action(cx.listener(Self::confirm_input)) - .children(match &self.head { - Head::Editor(editor) => { - if editor_position == PickerEditorPosition::Start { - Some(self.delegate.render_editor(&editor.clone(), window, cx)) - } else { - None - } - } - Head::Empty(empty_head) => Some(div().child(empty_head.clone())), - }) - .when(self.delegate.match_count() > 0, |el| { - el.child( - v_flex() - .id("element-container") - .relative() - .flex_grow() - .when_some(self.max_height, |div, max_h| div.max_h(max_h)) - .overflow_hidden() - .children(self.delegate.render_header(window, cx)) - .child(self.render_element_container(cx)) - .when(self.show_scrollbar, |this| { - let base_scrollbar_config = - Scrollbars::new(ScrollAxes::Vertical).width_sm(); - - this.map(|this| match &self.element_container { - ElementContainer::List(state) => this.custom_scrollbars( - base_scrollbar_config.tracked_scroll_handle(state), - window, - cx, - ), - ElementContainer::UniformList(state) => this.custom_scrollbars( - base_scrollbar_config.tracked_scroll_handle(state), - window, - cx, - ), - }) - }), - ) - }) - .when(self.delegate.match_count() == 0, |el| { - el.when_some(self.delegate.no_matches_text(window, cx), |el, text| { - el.child( - v_flex().flex_grow().py_2().child( - ListItem::new("empty_state") - .inset(true) - .spacing(ListItemSpacing::Sparse) - .disabled(true) - .child(Label::new(text).color(Color::Muted)), - ), - ) - }) - }) - .children(self.delegate.render_footer(window, cx)) - .children(match &self.head { - Head::Editor(editor) => { - if editor_position == PickerEditorPosition::End { - Some(self.delegate.render_editor(&editor.clone(), window, cx)) - } else { - None - } - } - Head::Empty(empty_head) => Some(div().child(empty_head.clone())), - }); - - let Some(aside) = aside else { - return menu; - }; - - let render_aside = |aside: DocumentationAside, cx: &mut Context| { - WithRemSize::new(ui_font_size) - .occlude() - .elevation_2(cx) - .w_full() - .p_2() - .overflow_hidden() - .when(is_wide_window, |this| this.max_w_96()) - .when(!is_wide_window, |this| this.max_w_48()) - .child((aside.render)(cx)) - }; - - if is_wide_window { - div().relative().child(menu).child( - h_flex() - .absolute() - .when(aside.side == DocumentationSide::Left, |this| { - this.right_full().mr_1() - }) - .when(aside.side == DocumentationSide::Right, |this| { - this.left_full().ml_1() - }) - .when(aside.edge == DocumentationEdge::Top, |this| this.top_0()) - .when(aside.edge == DocumentationEdge::Bottom, |this| { - this.bottom_0() - }) - .child(render_aside(aside, cx)), - ) - } else { - v_flex() - .w_full() - .gap_1() - .justify_end() - .child(render_aside(aside, cx)) - .child(menu) - } - } -} diff --git a/crates/picker/src/popover_menu.rs b/crates/picker/src/popover_menu.rs deleted file mode 100644 index 42eedb2492..0000000000 --- a/crates/picker/src/popover_menu.rs +++ /dev/null @@ -1,101 +0,0 @@ -use gpui::{ - AnyView, Corner, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Pixels, Point, - Subscription, -}; -use ui::{ - FluentBuilder as _, IntoElement, PopoverMenu, PopoverMenuHandle, PopoverTrigger, prelude::*, -}; - -use crate::{Picker, PickerDelegate}; - -pub struct PickerPopoverMenu -where - T: PopoverTrigger + ButtonCommon, - TT: Fn(&mut Window, &mut App) -> AnyView + 'static, - P: PickerDelegate, -{ - picker: Entity>, - trigger: T, - tooltip: TT, - handle: Option>>, - anchor: Corner, - offset: Option>, - _subscriptions: Vec, -} - -impl PickerPopoverMenu -where - T: PopoverTrigger + ButtonCommon, - TT: Fn(&mut Window, &mut App) -> AnyView + 'static, - P: PickerDelegate, -{ - pub fn new( - picker: Entity>, - trigger: T, - tooltip: TT, - anchor: Corner, - cx: &mut App, - ) -> Self { - Self { - _subscriptions: vec![cx.subscribe(&picker, |picker, &DismissEvent, cx| { - picker.update(cx, |_, cx| cx.emit(DismissEvent)); - })], - picker, - trigger, - tooltip, - handle: None, - offset: Some(Point { - x: px(0.0), - y: px(-2.0), - }), - anchor, - } - } - - pub fn with_handle(mut self, handle: PopoverMenuHandle>) -> Self { - self.handle = Some(handle); - self - } - - pub fn offset(mut self, offset: Point) -> Self { - self.offset = Some(offset); - self - } -} - -impl EventEmitter for PickerPopoverMenu -where - T: PopoverTrigger + ButtonCommon, - TT: Fn(&mut Window, &mut App) -> AnyView + 'static, - P: PickerDelegate, -{ -} - -impl Focusable for PickerPopoverMenu -where - T: PopoverTrigger + ButtonCommon, - TT: Fn(&mut Window, &mut App) -> AnyView + 'static, - P: PickerDelegate, -{ - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.picker.focus_handle(cx) - } -} - -impl RenderOnce for PickerPopoverMenu -where - T: PopoverTrigger + ButtonCommon, - TT: Fn(&mut Window, &mut App) -> AnyView + 'static, - P: PickerDelegate, -{ - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let picker = self.picker.clone(); - - PopoverMenu::new("popover-menu") - .menu(move |_window, _cx| Some(picker.clone())) - .trigger_with_tooltip(self.trigger, self.tooltip) - .anchor(self.anchor) - .when_some(self.handle, |menu, handle| menu.with_handle(handle)) - .when_some(self.offset, |menu, offset| menu.offset(offset)) - } -} diff --git a/crates/prettier/Cargo.toml b/crates/prettier/Cargo.toml deleted file mode 100644 index 9da1e4c8d6..0000000000 --- a/crates/prettier/Cargo.toml +++ /dev/null @@ -1,36 +0,0 @@ -[package] -name = "prettier" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/prettier.rs" -doctest = false - -[features] -test-support = [] - -[dependencies] -anyhow.workspace = true -collections.workspace = true -fs.workspace = true -gpui.workspace = true -language.workspace = true -log.workspace = true -lsp.workspace = true -node_runtime.workspace = true -parking_lot.workspace = true -paths.workspace = true -serde.workspace = true -serde_json.workspace = true -util.workspace = true - -[dev-dependencies] -fs = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -language = { workspace = true, features = ["test-support"] } diff --git a/crates/prettier/LICENSE-GPL b/crates/prettier/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/prettier/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/prettier/src/prettier.rs b/crates/prettier/src/prettier.rs deleted file mode 100644 index bc4ce609a1..0000000000 --- a/crates/prettier/src/prettier.rs +++ /dev/null @@ -1,1191 +0,0 @@ -use anyhow::Context as _; -use collections::{HashMap, HashSet}; -use fs::Fs; -use gpui::{AsyncApp, Entity}; -use language::language_settings::PrettierSettings; -use language::{Buffer, Diff, Language, language_settings::language_settings}; -use lsp::{LanguageServer, LanguageServerId}; -use node_runtime::NodeRuntime; -use paths::default_prettier_dir; -use serde::{Deserialize, Serialize}; -use std::{ - ops::ControlFlow, - path::{Path, PathBuf}, - sync::Arc, -}; -use util::{ - paths::{PathMatcher, PathStyle}, - rel_path::RelPath, -}; - -#[derive(Debug, Clone)] -pub enum Prettier { - Real(RealPrettier), - #[cfg(any(test, feature = "test-support"))] - Test(TestPrettier), -} - -#[derive(Debug, Clone)] -pub struct RealPrettier { - default: bool, - prettier_dir: PathBuf, - server: Arc, -} - -#[cfg(any(test, feature = "test-support"))] -#[derive(Debug, Clone)] -pub struct TestPrettier { - prettier_dir: PathBuf, - default: bool, -} - -pub const FAIL_THRESHOLD: usize = 4; -pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js"; -pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js"); -const PRETTIER_PACKAGE_NAME: &str = "prettier"; -const TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME: &str = "prettier-plugin-tailwindcss"; - -#[cfg(any(test, feature = "test-support"))] -pub const FORMAT_SUFFIX: &str = "\nformatted by test prettier"; - -impl Prettier { - pub const CONFIG_FILE_NAMES: &'static [&'static str] = &[ - ".prettierrc", - ".prettierrc.json", - ".prettierrc.json5", - ".prettierrc.yaml", - ".prettierrc.yml", - ".prettierrc.toml", - ".prettierrc.js", - ".prettierrc.cjs", - ".prettierrc.mjs", - ".prettierrc.ts", - ".prettierrc.cts", - ".prettierrc.mts", - "package.json", - "prettier.config.js", - "prettier.config.cjs", - "prettier.config.mjs", - "prettier.config.ts", - "prettier.config.cts", - "prettier.config.mts", - ".editorconfig", - ".prettierignore", - ]; - - pub async fn locate_prettier_installation( - fs: &dyn Fs, - installed_prettiers: &HashSet, - locate_from: &Path, - ) -> anyhow::Result>> { - let mut path_to_check = locate_from - .components() - .take_while(|component| component.as_os_str().to_string_lossy() != "node_modules") - .collect::(); - if path_to_check != locate_from { - log::debug!( - "Skipping prettier location for path {path_to_check:?} that is inside node_modules" - ); - return Ok(ControlFlow::Break(())); - } - let path_to_check_metadata = fs - .metadata(&path_to_check) - .await - .with_context(|| format!("failed to get metadata for initial path {path_to_check:?}"))? - .with_context(|| format!("empty metadata for initial path {path_to_check:?}"))?; - if !path_to_check_metadata.is_dir { - path_to_check.pop(); - } - - let mut closest_package_json_path = None; - loop { - if installed_prettiers.contains(&path_to_check) { - log::debug!("Found prettier path {path_to_check:?} in installed prettiers"); - return Ok(ControlFlow::Continue(Some(path_to_check))); - } else if let Some(package_json_contents) = - read_package_json(fs, &path_to_check).await? - { - if has_prettier_in_node_modules(fs, &path_to_check).await? { - log::debug!("Found prettier path {path_to_check:?} in the node_modules"); - return Ok(ControlFlow::Continue(Some(path_to_check))); - } else { - match &closest_package_json_path { - None => closest_package_json_path = Some(path_to_check.clone()), - Some(closest_package_json_path) => { - match package_json_contents.get("workspaces") { - Some(serde_json::Value::Array(workspaces)) => { - let subproject_path = closest_package_json_path.strip_prefix(&path_to_check).expect("traversing path parents, should be able to strip prefix"); - if workspaces.iter().filter_map(|value| { - if let serde_json::Value::String(s) = value { - Some(s.clone()) - } else { - log::warn!("Skipping non-string 'workspaces' value: {value:?}"); - None - } - }).any(|workspace_definition| { - workspace_definition == subproject_path.to_string_lossy() || PathMatcher::new(&[workspace_definition], PathStyle::local()).ok().is_some_and( - |path_matcher| RelPath::new(subproject_path, PathStyle::local()).is_ok_and(|path| path_matcher.is_match(path))) - }) { - anyhow::ensure!(has_prettier_in_node_modules(fs, &path_to_check).await?, - "Path {path_to_check:?} is the workspace root for project in \ - {closest_package_json_path:?}, but it has no prettier installed" - ); - log::info!( - "Found prettier path {path_to_check:?} in the workspace \ - root for project in {closest_package_json_path:?}" - ); - return Ok(ControlFlow::Continue(Some(path_to_check))); - } else { - log::warn!( - "Skipping path {path_to_check:?} workspace root with \ - workspaces {workspaces:?} that have no prettier installed" - ); - } - } - Some(unknown) => log::error!( - "Failed to parse workspaces for {path_to_check:?} from package.json, \ - got {unknown:?}. Skipping." - ), - None => log::warn!( - "Skipping path {path_to_check:?} that has no prettier \ - dependency and no workspaces section in its package.json" - ), - } - } - } - } - } - - if !path_to_check.pop() { - log::debug!("Found no prettier in ancestors of {locate_from:?}"); - return Ok(ControlFlow::Continue(None)); - } - } - } - - pub async fn locate_prettier_ignore( - fs: &dyn Fs, - prettier_ignores: &HashSet, - locate_from: &Path, - ) -> anyhow::Result>> { - let mut path_to_check = locate_from - .components() - .take_while(|component| component.as_os_str().to_string_lossy() != "node_modules") - .collect::(); - if path_to_check != locate_from { - log::debug!( - "Skipping prettier ignore location for path {path_to_check:?} that is inside node_modules" - ); - return Ok(ControlFlow::Break(())); - } - - let path_to_check_metadata = fs - .metadata(&path_to_check) - .await - .with_context(|| format!("failed to get metadata for initial path {path_to_check:?}"))? - .with_context(|| format!("empty metadata for initial path {path_to_check:?}"))?; - if !path_to_check_metadata.is_dir { - path_to_check.pop(); - } - - let mut closest_package_json_path = None; - loop { - if prettier_ignores.contains(&path_to_check) { - log::debug!("Found prettier ignore at {path_to_check:?}"); - return Ok(ControlFlow::Continue(Some(path_to_check))); - } else if let Some(package_json_contents) = - read_package_json(fs, &path_to_check).await? - { - let ignore_path = path_to_check.join(".prettierignore"); - if let Some(metadata) = fs - .metadata(&ignore_path) - .await - .with_context(|| format!("fetching metadata for {ignore_path:?}"))? - && !metadata.is_dir - && !metadata.is_symlink - { - log::info!("Found prettier ignore at {ignore_path:?}"); - return Ok(ControlFlow::Continue(Some(path_to_check))); - } - match &closest_package_json_path { - None => closest_package_json_path = Some(path_to_check.clone()), - Some(closest_package_json_path) => { - if let Some(serde_json::Value::Array(workspaces)) = - package_json_contents.get("workspaces") - { - let subproject_path = closest_package_json_path - .strip_prefix(&path_to_check) - .expect("traversing path parents, should be able to strip prefix"); - - if workspaces - .iter() - .filter_map(|value| { - if let serde_json::Value::String(s) = value { - Some(s.clone()) - } else { - log::warn!( - "Skipping non-string 'workspaces' value: {value:?}" - ); - None - } - }) - .any(|workspace_definition| { - workspace_definition == subproject_path.to_string_lossy() - || PathMatcher::new( - &[workspace_definition], - PathStyle::local(), - ) - .ok() - .is_some_and( - |path_matcher| { - RelPath::new(subproject_path, PathStyle::local()) - .is_ok_and(|rel_path| { - path_matcher.is_match(rel_path) - }) - }, - ) - }) - { - let workspace_ignore = path_to_check.join(".prettierignore"); - if let Some(metadata) = fs.metadata(&workspace_ignore).await? - && !metadata.is_dir - { - log::info!( - "Found prettier ignore at workspace root {workspace_ignore:?}" - ); - return Ok(ControlFlow::Continue(Some(path_to_check))); - } - } - } - } - } - } - - if !path_to_check.pop() { - log::debug!("Found no prettier ignore in ancestors of {locate_from:?}"); - return Ok(ControlFlow::Continue(None)); - } - } - } - - #[cfg(any(test, feature = "test-support"))] - pub async fn start( - _: LanguageServerId, - prettier_dir: PathBuf, - _: NodeRuntime, - _: AsyncApp, - ) -> anyhow::Result { - Ok(Self::Test(TestPrettier { - default: prettier_dir == default_prettier_dir().as_path(), - prettier_dir, - })) - } - - #[cfg(not(any(test, feature = "test-support")))] - pub async fn start( - server_id: LanguageServerId, - prettier_dir: PathBuf, - node: NodeRuntime, - mut cx: AsyncApp, - ) -> anyhow::Result { - use lsp::{LanguageServerBinary, LanguageServerName}; - - let executor = cx.background_executor().clone(); - anyhow::ensure!( - prettier_dir.is_dir(), - "Prettier dir {prettier_dir:?} is not a directory" - ); - let prettier_server = default_prettier_dir().join(PRETTIER_SERVER_FILE); - anyhow::ensure!( - prettier_server.is_file(), - "no prettier server package found at {prettier_server:?}" - ); - - let node_path = executor - .spawn(async move { node.binary_path().await }) - .await?; - let server_name = LanguageServerName("prettier".into()); - let server_binary = LanguageServerBinary { - path: node_path, - arguments: vec![prettier_server.into(), prettier_dir.as_path().into()], - env: None, - }; - let server = LanguageServer::new( - Arc::new(parking_lot::Mutex::new(None)), - server_id, - server_name, - server_binary, - &prettier_dir, - None, - Default::default(), - &mut cx, - ) - .context("prettier server creation")?; - - let server = cx - .update(|cx| { - let params = server.default_initialize_params(false, cx); - let configuration = lsp::DidChangeConfigurationParams { - settings: Default::default(), - }; - executor.spawn(server.initialize(params, configuration.into(), cx)) - })? - .await - .context("prettier server initialization")?; - Ok(Self::Real(RealPrettier { - server, - default: prettier_dir == default_prettier_dir().as_path(), - prettier_dir, - })) - } - - pub async fn format( - &self, - buffer: &Entity, - buffer_path: Option, - ignore_dir: Option, - cx: &mut AsyncApp, - ) -> anyhow::Result { - match self { - Self::Real(local) => { - let params = buffer - .update(cx, |buffer, cx| { - let buffer_language = buffer.language().map(|language| language.as_ref()); - let language_settings = language_settings(buffer_language.map(|l| l.name()), buffer.file(), cx); - let prettier_settings = &language_settings.prettier; - anyhow::ensure!( - prettier_settings.allowed, - "Cannot format: prettier is not allowed for language {buffer_language:?}" - ); - let prettier_node_modules = self.prettier_dir().join("node_modules"); - anyhow::ensure!( - prettier_node_modules.is_dir(), - "Prettier node_modules dir does not exist: {prettier_node_modules:?}" - ); - let plugin_name_into_path = |plugin_name: &str| { - let prettier_plugin_dir = prettier_node_modules.join(plugin_name); - [ - prettier_plugin_dir.join("dist").join("index.mjs"), - prettier_plugin_dir.join("dist").join("index.js"), - prettier_plugin_dir.join("dist").join("plugin.js"), - prettier_plugin_dir.join("src").join("plugin.js"), - prettier_plugin_dir.join("lib").join("index.js"), - prettier_plugin_dir.join("index.mjs"), - prettier_plugin_dir.join("index.js"), - prettier_plugin_dir.join("plugin.js"), - // this one is for @prettier/plugin-php - prettier_plugin_dir.join("standalone.js"), - // this one is for prettier-plugin-latex - prettier_plugin_dir.join("dist").join("prettier-plugin-latex.js"), - prettier_plugin_dir, - ] - .into_iter() - .find(|possible_plugin_path| possible_plugin_path.is_file()) - }; - - // Tailwind plugin requires being added last - // https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins - let mut add_tailwind_back = false; - - let mut located_plugins = prettier_settings.plugins.iter() - .filter(|plugin_name| { - if plugin_name.as_str() == TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME { - add_tailwind_back = true; - false - } else { - true - } - }) - .map(|plugin_name| { - let plugin_path = plugin_name_into_path(plugin_name); - (plugin_name.clone(), plugin_path) - }) - .collect::>(); - if add_tailwind_back { - located_plugins.push(( - TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME.to_owned(), - plugin_name_into_path(TAILWIND_PRETTIER_PLUGIN_PACKAGE_NAME), - )); - } - - let prettier_options = if self.is_default() { - let mut options = prettier_settings.options.clone(); - if !options.contains_key("tabWidth") { - options.insert( - "tabWidth".to_string(), - serde_json::Value::Number(serde_json::Number::from( - language_settings.tab_size.get(), - )), - ); - } - if !options.contains_key("printWidth") { - options.insert( - "printWidth".to_string(), - serde_json::Value::Number(serde_json::Number::from( - language_settings.preferred_line_length, - )), - ); - } - if !options.contains_key("useTabs") { - options.insert( - "useTabs".to_string(), - serde_json::Value::Bool(language_settings.hard_tabs), - ); - } - Some(options) - } else { - None - }; - - let plugins = located_plugins - .into_iter() - .filter_map(|(plugin_name, located_plugin_path)| { - match located_plugin_path { - Some(path) => Some(path), - None => { - log::error!("Have not found plugin path for {plugin_name:?} inside {prettier_node_modules:?}"); - None - } - } - }) - .collect(); - - let parser = prettier_parser_name(buffer_path.as_deref(), buffer_language, prettier_settings).context("getting prettier parser")?; - - let ignore_path = ignore_dir.and_then(|dir| { - let ignore_file = dir.join(".prettierignore"); - ignore_file.is_file().then_some(ignore_file) - }); - - log::debug!( - "Formatting file {:?} with prettier, plugins :{:?}, options: {:?}, ignore_path: {:?}", - buffer.file().map(|f| f.full_path(cx)), - plugins, - prettier_options, - ignore_path, - ); - - anyhow::Ok(FormatParams { - text: buffer.text(), - options: FormatOptions { - path: buffer_path, - parser, - plugins, - prettier_options, - ignore_path, - }, - }) - })? - .context("building prettier request")?; - - let response = local - .server - .request::(params) - .await - .into_response()?; - let diff_task = buffer.update(cx, |buffer, cx| buffer.diff(response.text, cx))?; - Ok(diff_task.await) - } - #[cfg(any(test, feature = "test-support"))] - Self::Test(_) => Ok(buffer - .update(cx, |buffer, cx| { - match buffer - .language() - .map(|language| language.lsp_id()) - .as_deref() - { - Some("rust") => anyhow::bail!("prettier does not support Rust"), - Some(_other) => { - let mut formatted_text = buffer.text() + FORMAT_SUFFIX; - - let buffer_language = - buffer.language().map(|language| language.as_ref()); - let language_settings = language_settings( - buffer_language.map(|l| l.name()), - buffer.file(), - cx, - ); - let prettier_settings = &language_settings.prettier; - let parser = prettier_parser_name( - buffer_path.as_deref(), - buffer_language, - prettier_settings, - )?; - - if let Some(parser) = parser { - formatted_text = format!("{formatted_text}\n{parser}"); - } - - Ok(buffer.diff(formatted_text, cx)) - } - None => panic!("Should not format buffer without a language with prettier"), - } - })?? - .await), - } - } - - pub async fn clear_cache(&self) -> anyhow::Result<()> { - match self { - Self::Real(local) => local - .server - .request::(()) - .await - .into_response() - .context("prettier clear cache"), - #[cfg(any(test, feature = "test-support"))] - Self::Test(_) => Ok(()), - } - } - - pub fn server(&self) -> Option<&Arc> { - match self { - Self::Real(local) => Some(&local.server), - #[cfg(any(test, feature = "test-support"))] - Self::Test(_) => None, - } - } - - pub fn is_default(&self) -> bool { - match self { - Self::Real(local) => local.default, - #[cfg(any(test, feature = "test-support"))] - Self::Test(test_prettier) => test_prettier.default, - } - } - - pub fn prettier_dir(&self) -> &Path { - match self { - Self::Real(local) => &local.prettier_dir, - #[cfg(any(test, feature = "test-support"))] - Self::Test(test_prettier) => &test_prettier.prettier_dir, - } - } -} - -fn prettier_parser_name( - buffer_path: Option<&Path>, - buffer_language: Option<&Language>, - prettier_settings: &PrettierSettings, -) -> anyhow::Result> { - let parser = if buffer_path.is_none() { - let parser = prettier_settings - .parser - .as_deref() - .or_else(|| buffer_language.and_then(|language| language.prettier_parser_name())); - if parser.is_none() { - log::error!( - "Formatting unsaved file with prettier failed. No prettier parser configured for language {buffer_language:?}" - ); - anyhow::bail!("Cannot determine prettier parser for unsaved file"); - } - parser - } else if let (Some(buffer_language), Some(buffer_path)) = (buffer_language, buffer_path) - && buffer_path.extension().is_some_and(|extension| { - !buffer_language - .config() - .matcher - .path_suffixes - .contains(&extension.to_string_lossy().into_owned()) - }) - { - buffer_language.prettier_parser_name() - } else { - prettier_settings.parser.as_deref() - }; - - Ok(parser.map(ToOwned::to_owned)) -} - -async fn has_prettier_in_node_modules(fs: &dyn Fs, path: &Path) -> anyhow::Result { - let possible_node_modules_location = path.join("node_modules").join(PRETTIER_PACKAGE_NAME); - if let Some(node_modules_location_metadata) = fs - .metadata(&possible_node_modules_location) - .await - .with_context(|| format!("fetching metadata for {possible_node_modules_location:?}"))? - { - return Ok(node_modules_location_metadata.is_dir); - } - Ok(false) -} - -async fn read_package_json( - fs: &dyn Fs, - path: &Path, -) -> anyhow::Result>> { - let possible_package_json = path.join("package.json"); - if let Some(package_json_metadata) = fs - .metadata(&possible_package_json) - .await - .with_context(|| format!("fetching metadata for package json {possible_package_json:?}"))? - && !package_json_metadata.is_dir - && !package_json_metadata.is_symlink - { - let package_json_contents = fs - .load(&possible_package_json) - .await - .with_context(|| format!("reading {possible_package_json:?} file contents"))?; - return serde_json::from_str::>(&package_json_contents) - .map(Some) - .with_context(|| format!("parsing {possible_package_json:?} file contents")); - } - Ok(None) -} - -enum Format {} - -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct FormatParams { - text: String, - options: FormatOptions, -} - -#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct FormatOptions { - plugins: Vec, - parser: Option, - #[serde(rename = "filepath")] - path: Option, - prettier_options: Option>, - ignore_path: Option, -} - -#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -struct FormatResult { - text: String, -} - -impl lsp::request::Request for Format { - type Params = FormatParams; - type Result = FormatResult; - const METHOD: &'static str = "prettier/format"; -} - -enum ClearCache {} - -impl lsp::request::Request for ClearCache { - type Params = (); - type Result = (); - const METHOD: &'static str = "prettier/clear_cache"; -} - -#[cfg(test)] -mod tests { - use fs::FakeFs; - use serde_json::json; - - use super::*; - - #[gpui::test] - async fn test_prettier_lookup_finds_nothing(cx: &mut gpui::TestAppContext) { - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - ".config": { - "zed": { - "settings.json": r#"{ "formatter": "auto" }"#, - }, - }, - "work": { - "project": { - "src": { - "index.js": "// index.js file contents", - }, - "node_modules": { - "expect": { - "build": { - "print.js": "// print.js file contents", - }, - "package.json": r#"{ - "devDependencies": { - "prettier": "2.5.1" - } - }"#, - }, - "prettier": { - "index.js": "// Dummy prettier package file", - }, - }, - "package.json": r#"{}"# - }, - } - }), - ) - .await; - - assert_eq!( - Prettier::locate_prettier_installation( - fs.as_ref(), - &HashSet::default(), - Path::new("/root/.config/zed/settings.json"), - ) - .await - .unwrap(), - ControlFlow::Continue(None), - "Should find no prettier for path hierarchy without it" - ); - assert_eq!( - Prettier::locate_prettier_installation( - fs.as_ref(), - &HashSet::default(), - Path::new("/root/work/project/src/index.js") - ) - .await - .unwrap(), - ControlFlow::Continue(Some(PathBuf::from("/root/work/project"))), - "Should successfully find a prettier for path hierarchy that has node_modules with prettier, but no package.json mentions of it" - ); - assert_eq!( - Prettier::locate_prettier_installation( - fs.as_ref(), - &HashSet::default(), - Path::new("/root/work/project/node_modules/expect/build/print.js") - ) - .await - .unwrap(), - ControlFlow::Break(()), - "Should not format files inside node_modules/" - ); - } - - #[gpui::test] - async fn test_prettier_lookup_in_simple_npm_projects(cx: &mut gpui::TestAppContext) { - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "web_blog": { - "node_modules": { - "prettier": { - "index.js": "// Dummy prettier package file", - }, - "expect": { - "build": { - "print.js": "// print.js file contents", - }, - "package.json": r#"{ - "devDependencies": { - "prettier": "2.5.1" - } - }"#, - }, - }, - "pages": { - "[slug].tsx": "// [slug].tsx file contents", - }, - "package.json": r#"{ - "devDependencies": { - "prettier": "2.3.0" - }, - "prettier": { - "semi": false, - "printWidth": 80, - "htmlWhitespaceSensitivity": "strict", - "tabWidth": 4 - } - }"# - } - }), - ) - .await; - - assert_eq!( - Prettier::locate_prettier_installation( - fs.as_ref(), - &HashSet::default(), - Path::new("/root/web_blog/pages/[slug].tsx") - ) - .await - .unwrap(), - ControlFlow::Continue(Some(PathBuf::from("/root/web_blog"))), - "Should find a preinstalled prettier in the project root" - ); - assert_eq!( - Prettier::locate_prettier_installation( - fs.as_ref(), - &HashSet::default(), - Path::new("/root/web_blog/node_modules/expect/build/print.js") - ) - .await - .unwrap(), - ControlFlow::Break(()), - "Should not allow formatting node_modules/ contents" - ); - } - - #[gpui::test] - async fn test_prettier_lookup_for_not_installed(cx: &mut gpui::TestAppContext) { - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "work": { - "web_blog": { - "node_modules": { - "expect": { - "build": { - "print.js": "// print.js file contents", - }, - "package.json": r#"{ - "devDependencies": { - "prettier": "2.5.1" - } - }"#, - }, - }, - "pages": { - "[slug].tsx": "// [slug].tsx file contents", - }, - "package.json": r#"{ - "devDependencies": { - "prettier": "2.3.0" - }, - "prettier": { - "semi": false, - "printWidth": 80, - "htmlWhitespaceSensitivity": "strict", - "tabWidth": 4 - } - }"# - } - } - }), - ) - .await; - - assert_eq!( - Prettier::locate_prettier_installation( - fs.as_ref(), - &HashSet::default(), - Path::new("/root/work/web_blog/pages/[slug].tsx") - ) - .await - .unwrap(), - ControlFlow::Continue(None), - "Should find no prettier when node_modules don't have it" - ); - - assert_eq!( - Prettier::locate_prettier_installation( - fs.as_ref(), - &HashSet::from_iter( - [PathBuf::from("/root"), PathBuf::from("/root/work")].into_iter() - ), - Path::new("/root/work/web_blog/pages/[slug].tsx") - ) - .await - .unwrap(), - ControlFlow::Continue(Some(PathBuf::from("/root/work"))), - "Should return closest cached value found without path checks" - ); - - assert_eq!( - Prettier::locate_prettier_installation( - fs.as_ref(), - &HashSet::default(), - Path::new("/root/work/web_blog/node_modules/expect/build/print.js") - ) - .await - .unwrap(), - ControlFlow::Break(()), - "Should not allow formatting files inside node_modules/" - ); - assert_eq!( - Prettier::locate_prettier_installation( - fs.as_ref(), - &HashSet::from_iter( - [PathBuf::from("/root"), PathBuf::from("/root/work")].into_iter() - ), - Path::new("/root/work/web_blog/node_modules/expect/build/print.js") - ) - .await - .unwrap(), - ControlFlow::Break(()), - "Should ignore cache lookup for files inside node_modules/" - ); - } - - #[gpui::test] - async fn test_prettier_lookup_in_npm_workspaces(cx: &mut gpui::TestAppContext) { - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "work": { - "full-stack-foundations": { - "exercises": { - "03.loading": { - "01.problem.loader": { - "app": { - "routes": { - "users+": { - "$username_+": { - "notes.tsx": "// notes.tsx file contents", - }, - }, - }, - }, - "node_modules": { - "test.js": "// test.js contents", - }, - "package.json": r#"{ - "devDependencies": { - "prettier": "^3.0.3" - } - }"# - }, - }, - }, - "package.json": r#"{ - "workspaces": ["exercises/*/*", "examples/*"] - }"#, - "node_modules": { - "prettier": { - "index.js": "// Dummy prettier package file", - }, - }, - }, - } - }), - ) - .await; - - assert_eq!( - Prettier::locate_prettier_installation( - fs.as_ref(), - &HashSet::default(), - Path::new("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader/app/routes/users+/$username_+/notes.tsx"), - ).await.unwrap(), - ControlFlow::Continue(Some(PathBuf::from("/root/work/full-stack-foundations"))), - "Should ascend to the multi-workspace root and find the prettier there", - ); - - assert_eq!( - Prettier::locate_prettier_installation( - fs.as_ref(), - &HashSet::default(), - Path::new("/root/work/full-stack-foundations/node_modules/prettier/index.js") - ) - .await - .unwrap(), - ControlFlow::Break(()), - "Should not allow formatting files inside root node_modules/" - ); - assert_eq!( - Prettier::locate_prettier_installation( - fs.as_ref(), - &HashSet::default(), - Path::new("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader/node_modules/test.js") - ) - .await - .unwrap(), - ControlFlow::Break(()), - "Should not allow formatting files inside submodule's node_modules/" - ); - } - - #[gpui::test] - async fn test_prettier_lookup_in_npm_workspaces_for_not_installed( - cx: &mut gpui::TestAppContext, - ) { - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "work": { - "full-stack-foundations": { - "exercises": { - "03.loading": { - "01.problem.loader": { - "app": { - "routes": { - "users+": { - "$username_+": { - "notes.tsx": "// notes.tsx file contents", - }, - }, - }, - }, - "node_modules": {}, - "package.json": r#"{ - "devDependencies": { - "prettier": "^3.0.3" - } - }"# - }, - }, - }, - "package.json": r#"{ - "workspaces": ["exercises/*/*", "examples/*"] - }"#, - }, - } - }), - ) - .await; - - match Prettier::locate_prettier_installation( - fs.as_ref(), - &HashSet::default(), - Path::new("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader/app/routes/users+/$username_+/notes.tsx") - ) - .await { - Ok(path) => panic!("Expected to fail for prettier in package.json but not in node_modules found, but got path {path:?}"), - Err(e) => { - let message = e.to_string().replace("\\\\", "/"); - assert!(message.contains("/root/work/full-stack-foundations/exercises/03.loading/01.problem.loader"), "Error message should mention which project had prettier defined"); - assert!(message.contains("/root/work/full-stack-foundations"), "Error message should mention potential candidates without prettier node_modules contents"); - }, - }; - } - - #[gpui::test] - async fn test_prettier_ignore_with_editor_prettier(cx: &mut gpui::TestAppContext) { - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "project": { - "src": { - "index.js": "// index.js file contents", - "ignored.js": "// this file should be ignored", - }, - ".prettierignore": "ignored.js", - "package.json": r#"{ - "name": "test-project" - }"# - } - }), - ) - .await; - - assert_eq!( - Prettier::locate_prettier_ignore( - fs.as_ref(), - &HashSet::default(), - Path::new("/root/project/src/index.js"), - ) - .await - .unwrap(), - ControlFlow::Continue(Some(PathBuf::from("/root/project"))), - "Should find prettierignore in project root" - ); - } - - #[gpui::test] - async fn test_prettier_ignore_in_monorepo_with_only_child_ignore( - cx: &mut gpui::TestAppContext, - ) { - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "monorepo": { - "node_modules": { - "prettier": { - "index.js": "// Dummy prettier package file", - } - }, - "packages": { - "web": { - "src": { - "index.js": "// index.js contents", - "ignored.js": "// this should be ignored", - }, - ".prettierignore": "ignored.js", - "package.json": r#"{ - "name": "web-package" - }"# - } - }, - "package.json": r#"{ - "workspaces": ["packages/*"], - "devDependencies": { - "prettier": "^2.0.0" - } - }"# - } - }), - ) - .await; - - assert_eq!( - Prettier::locate_prettier_ignore( - fs.as_ref(), - &HashSet::default(), - Path::new("/root/monorepo/packages/web/src/index.js"), - ) - .await - .unwrap(), - ControlFlow::Continue(Some(PathBuf::from("/root/monorepo/packages/web"))), - "Should find prettierignore in child package" - ); - } - - #[gpui::test] - async fn test_prettier_ignore_in_monorepo_with_root_and_child_ignores( - cx: &mut gpui::TestAppContext, - ) { - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "monorepo": { - "node_modules": { - "prettier": { - "index.js": "// Dummy prettier package file", - } - }, - ".prettierignore": "main.js", - "packages": { - "web": { - "src": { - "main.js": "// this should not be ignored", - "ignored.js": "// this should be ignored", - }, - ".prettierignore": "ignored.js", - "package.json": r#"{ - "name": "web-package" - }"# - } - }, - "package.json": r#"{ - "workspaces": ["packages/*"], - "devDependencies": { - "prettier": "^2.0.0" - } - }"# - } - }), - ) - .await; - - assert_eq!( - Prettier::locate_prettier_ignore( - fs.as_ref(), - &HashSet::default(), - Path::new("/root/monorepo/packages/web/src/main.js"), - ) - .await - .unwrap(), - ControlFlow::Continue(Some(PathBuf::from("/root/monorepo/packages/web"))), - "Should find child package prettierignore first" - ); - - assert_eq!( - Prettier::locate_prettier_ignore( - fs.as_ref(), - &HashSet::default(), - Path::new("/root/monorepo/packages/web/src/ignored.js"), - ) - .await - .unwrap(), - ControlFlow::Continue(Some(PathBuf::from("/root/monorepo/packages/web"))), - "Should find child package prettierignore first" - ); - } -} diff --git a/crates/prettier/src/prettier_server.js b/crates/prettier/src/prettier_server.js deleted file mode 100644 index b3d8a660a4..0000000000 --- a/crates/prettier/src/prettier_server.js +++ /dev/null @@ -1,266 +0,0 @@ -const { Buffer } = require("buffer"); -const fs = require("fs"); -const path = require("path"); -const { once } = require("events"); - -const prettierContainerPath = process.argv[2]; -if (prettierContainerPath == null || prettierContainerPath.length == 0) { - process.stderr.write( - `Prettier path argument was not specified or empty.\nUsage: ${process.argv[0]} ${process.argv[1]} prettier/path\n`, - ); - process.exit(1); -} -fs.stat(prettierContainerPath, (err, stats) => { - if (err) { - process.stderr.write(`Path '${prettierContainerPath}' does not exist\n`); - process.exit(1); - } - - if (!stats.isDirectory()) { - process.stderr.write(`Path '${prettierContainerPath}' exists but is not a directory\n`); - process.exit(1); - } -}); -const prettierPath = path.join(prettierContainerPath, "node_modules/prettier"); - -class Prettier { - constructor(path, prettier, config) { - this.path = path; - this.prettier = prettier; - this.config = config; - } -} - -(async () => { - let prettier; - let config; - try { - prettier = await loadPrettier(prettierPath); - config = (await prettier.resolveConfig(prettierPath)) || {}; - } catch (e) { - process.stderr.write(`Failed to load prettier: ${e}\n`); - process.exit(1); - } - process.stderr.write(`Prettier at path '${prettierPath}' loaded successfully, config: ${JSON.stringify(config)}\n`); - process.stdin.resume(); - handleBuffer(new Prettier(prettierPath, prettier, config)); -})(); - -async function handleBuffer(prettier) { - for await (const messageText of readStdin()) { - let message; - try { - message = JSON.parse(messageText); - } catch (e) { - sendResponse(makeError(`Parse error in request message: ${e}\nMessage: ${messageText}`)); - continue; - } - // allow concurrent request handling by not `await`ing the message handling promise (async function) - handleMessage(message, prettier).catch((e) => { - if ((message.params || {}).text !== undefined) { - message.params.text = "..snip.."; - } - sendResponse({ - id: message.id, - ...makeError(`${e}\nWhile handling prettier request: ${JSON.stringify(message)}`), - }); - }); - } -} - -const headerSeparator = "\r\n"; -const contentLengthHeaderName = "Content-Length"; - -async function* readStdin() { - let buffer = Buffer.alloc(0); - let streamEnded = false; - process.stdin.on("end", () => { - streamEnded = true; - }); - process.stdin.on("data", (data) => { - buffer = Buffer.concat([buffer, data]); - }); - - async function handleStreamEnded(errorMessage) { - sendResponse(makeError(errorMessage)); - buffer = Buffer.alloc(0); - messageLength = null; - await once(process.stdin, "readable"); - streamEnded = false; - } - - try { - let headersLength = null; - let messageLength = null; - main_loop: while (true) { - if (messageLength === null) { - while (buffer.indexOf(`${headerSeparator}${headerSeparator}`) === -1) { - if (streamEnded) { - await handleStreamEnded("Unexpected end of stream: headers not found"); - continue main_loop; - } else if (buffer.length > contentLengthHeaderName.length * 10) { - await handleStreamEnded( - `Unexpected stream of bytes: no headers end found after ${buffer.length} bytes of input`, - ); - continue main_loop; - } - await once(process.stdin, "readable"); - } - const headers = buffer.subarray(0, buffer.indexOf(`${headerSeparator}${headerSeparator}`)).toString("ascii"); - const contentLengthHeader = headers - .split(headerSeparator) - .map((header) => header.split(":")) - .filter((header) => header[2] === undefined) - .filter((header) => (header[1] || "").length > 0) - .find((header) => (header[0] || "").trim() === contentLengthHeaderName); - const contentLength = (contentLengthHeader || [])[1]; - if (contentLength === undefined) { - await handleStreamEnded(`Missing or incorrect ${contentLengthHeaderName} header: ${headers}`); - continue main_loop; - } - headersLength = headers.length + headerSeparator.length * 2; - messageLength = parseInt(contentLength, 10); - } - - while (buffer.length < headersLength + messageLength) { - if (streamEnded) { - await handleStreamEnded( - `Unexpected end of stream: buffer length ${buffer.length} does not match expected header length ${headersLength} + body length ${messageLength}`, - ); - continue main_loop; - } - await once(process.stdin, "readable"); - } - - const messageEnd = headersLength + messageLength; - const message = buffer.subarray(headersLength, messageEnd); - buffer = buffer.subarray(messageEnd); - headersLength = null; - messageLength = null; - yield message.toString("utf8"); - } - } catch (e) { - sendResponse(makeError(`Error reading stdin: ${e}`)); - } finally { - process.stdin.off("data", () => {}); - } -} - -async function handleMessage(message, prettier) { - const { method, id, params } = message; - if (method === undefined) { - throw new Error(`Message method is undefined: ${JSON.stringify(message)}`); - } else if (method == "initialized") { - return; - } else if (method === "shutdown") { - sendResponse({ result: {} }); - } else if (method == "exit") { - process.exit(0); - } - - if (id === undefined) { - throw new Error(`Message id is undefined: ${JSON.stringify(message)}`); - } - - if (method === "prettier/format") { - if (params === undefined || params.text === undefined) { - throw new Error(`Message params.text is undefined: ${JSON.stringify(message)}`); - } - if (params.options === undefined) { - throw new Error(`Message params.options is undefined: ${JSON.stringify(message)}`); - } - - let resolvedConfig = {}; - if (params.options.filepath) { - resolvedConfig = (await prettier.prettier.resolveConfig(params.options.filepath)) || {}; - - if (params.options.ignorePath) { - const fileInfo = await prettier.prettier.getFileInfo(params.options.filepath, { - ignorePath: params.options.ignorePath, - }); - if (fileInfo.ignored) { - process.stderr.write( - `Ignoring file '${params.options.filepath}' based on rules in '${params.options.ignorePath}'\n`, - ); - sendResponse({ id, result: { text: params.text } }); - return; - } - } - } - - // Marking the params.options.filepath as undefined makes - // prettier.format() work even if no filepath is set. - if (params.options.filepath === null) { - params.options.filepath = undefined; - } - - const plugins = - Array.isArray(resolvedConfig?.plugins) && resolvedConfig.plugins.length > 0 - ? resolvedConfig.plugins - : params.options.plugins; - - const options = { - ...(params.options.prettierOptions || prettier.config), - ...resolvedConfig, - plugins, - parser: params.options.parser, - filepath: params.options.filepath, - }; - process.stderr.write( - `Resolved config: ${JSON.stringify(resolvedConfig)}, will format file '${ - params.options.filepath || "" - }' with options: ${JSON.stringify(options)}\n`, - ); - const formattedText = await prettier.prettier.format(params.text, options); - sendResponse({ id, result: { text: formattedText } }); - } else if (method === "prettier/clear_cache") { - prettier.prettier.clearConfigCache(); - prettier.config = (await prettier.prettier.resolveConfig(prettier.path)) || {}; - sendResponse({ id, result: null }); - } else if (method === "initialize") { - sendResponse({ - id, - result: { - capabilities: {}, - }, - }); - } else { - throw new Error(`Unknown method: ${method}`); - } -} - -function makeError(message) { - return { - error: { - code: -32600, // invalid request code - message, - }, - }; -} - -function sendResponse(response) { - const responsePayloadString = JSON.stringify({ - jsonrpc: "2.0", - ...response, - }); - const headers = `${contentLengthHeaderName}: ${Buffer.byteLength( - responsePayloadString, - )}${headerSeparator}${headerSeparator}`; - process.stdout.write(headers + responsePayloadString); -} - -function loadPrettier(prettierPath) { - return new Promise((resolve, reject) => { - fs.access(prettierPath, fs.constants.F_OK, (err) => { - if (err) { - reject(`Path '${prettierPath}' does not exist.Error: ${err}`); - } else { - try { - resolve(require(prettierPath)); - } catch (err) { - reject(`Error requiring prettier module from path '${prettierPath}'.Error: ${err}`); - } - } - }); - }); -} diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml deleted file mode 100644 index 9e2789fc10..0000000000 --- a/crates/project/Cargo.toml +++ /dev/null @@ -1,120 +0,0 @@ -[package] -name = "project" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/project.rs" -doctest = false - -[features] -test-support = [ - "buffer_diff/test-support", - "client/test-support", - "language/test-support", - "settings/test-support", - "snippet_provider/test-support", - "text/test-support", - "prettier/test-support", - "worktree/test-support", - "gpui/test-support", - "dap/test-support", - "dap_adapters/test-support", -] - -[dependencies] -aho-corasick.workspace = true -anyhow.workspace = true -askpass.workspace = true -async-trait.workspace = true -base64.workspace = true -buffer_diff.workspace = true -circular-buffer.workspace = true -client.workspace = true -clock.workspace = true -collections.workspace = true -context_server.workspace = true -dap.workspace = true -extension.workspace = true -fancy-regex.workspace = true -fs.workspace = true -futures.workspace = true -fuzzy.workspace = true -git.workspace = true -git_hosting_providers.workspace = true -globset.workspace = true -gpui.workspace = true -http_client.workspace = true -image.workspace = true -itertools.workspace = true -indexmap.workspace = true -language.workspace = true -log.workspace = true -lsp.workspace = true -markdown.workspace = true -node_runtime.workspace = true -parking_lot.workspace = true -paths.workspace = true -postage.workspace = true -prettier.workspace = true -rand.workspace = true -regex.workspace = true -remote.workspace = true -rpc.workspace = true -schemars.workspace = true -semver.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -sha2.workspace = true -shellexpand.workspace = true -smallvec.workspace = true -smol.workspace = true -snippet.workspace = true -snippet_provider.workspace = true -sum_tree.workspace = true -task.workspace = true -tempfile.workspace = true -terminal.workspace = true -text.workspace = true -toml.workspace = true -url.workspace = true -util.workspace = true -watch.workspace = true -wax.workspace = true -which.workspace = true -worktree.workspace = true -zeroize.workspace = true -zlog.workspace = true -ztracing.workspace = true -tracing.workspace = true - -[dev-dependencies] -client = { workspace = true, features = ["test-support"] } -collections = { workspace = true, features = ["test-support"] } -context_server = { workspace = true, features = ["test-support"] } -buffer_diff = { workspace = true, features = ["test-support"] } -dap = { workspace = true, features = ["test-support"] } -dap_adapters = { workspace = true, features = ["test-support"] } -fs = { workspace = true, features = ["test-support"] } -git2.workspace = true -gpui = { workspace = true, features = ["test-support"] } -language = { workspace = true, features = ["test-support"] } -lsp = { workspace = true, features = ["test-support"] } -prettier = { workspace = true, features = ["test-support"] } -pretty_assertions.workspace = true -release_channel.workspace = true -rpc = { workspace = true, features = ["test-support"] } -settings = { workspace = true, features = ["test-support"] } -snippet_provider = { workspace = true, features = ["test-support"] } -unindent.workspace = true -util = { workspace = true, features = ["test-support"] } -worktree = { workspace = true, features = ["test-support"] } - -[package.metadata.cargo-machete] -ignored = ["tracing"] diff --git a/crates/project/LICENSE-GPL b/crates/project/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/project/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/project/src/agent_server_store.rs b/crates/project/src/agent_server_store.rs deleted file mode 100644 index a2cc57beae..0000000000 --- a/crates/project/src/agent_server_store.rs +++ /dev/null @@ -1,2346 +0,0 @@ -use std::{ - any::Any, - borrow::Borrow, - path::{Path, PathBuf}, - str::FromStr as _, - sync::Arc, - time::Duration, -}; - -use anyhow::{Context as _, Result, bail}; -use collections::HashMap; -use fs::{Fs, RemoveOptions, RenameOptions}; -use futures::StreamExt as _; -use gpui::{ - AppContext as _, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task, -}; -use http_client::{HttpClient, github::AssetKind}; -use node_runtime::NodeRuntime; -use remote::RemoteClient; -use rpc::{ - AnyProtoClient, TypedEnvelope, - proto::{self, ExternalExtensionAgent}, -}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{RegisterSetting, SettingsStore}; -use task::{Shell, SpawnInTerminal}; -use util::{ResultExt as _, debug_panic}; - -use crate::ProjectEnvironment; - -#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)] -pub struct AgentServerCommand { - #[serde(rename = "command")] - pub path: PathBuf, - #[serde(default)] - pub args: Vec, - pub env: Option>, -} - -impl std::fmt::Debug for AgentServerCommand { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let filtered_env = self.env.as_ref().map(|env| { - env.iter() - .map(|(k, v)| { - ( - k, - if util::redact::should_redact(k) { - "[REDACTED]" - } else { - v - }, - ) - }) - .collect::>() - }); - - f.debug_struct("AgentServerCommand") - .field("path", &self.path) - .field("args", &self.args) - .field("env", &filtered_env) - .finish() - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Hash)] -pub struct ExternalAgentServerName(pub SharedString); - -impl std::fmt::Display for ExternalAgentServerName { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From<&'static str> for ExternalAgentServerName { - fn from(value: &'static str) -> Self { - ExternalAgentServerName(value.into()) - } -} - -impl From for SharedString { - fn from(value: ExternalAgentServerName) -> Self { - value.0 - } -} - -impl Borrow for ExternalAgentServerName { - fn borrow(&self) -> &str { - &self.0 - } -} - -pub trait ExternalAgentServer { - fn get_command( - &mut self, - root_dir: Option<&str>, - extra_env: HashMap, - status_tx: Option>, - new_version_available_tx: Option>>, - cx: &mut AsyncApp, - ) -> Task)>>; - - fn as_any_mut(&mut self) -> &mut dyn Any; -} - -impl dyn ExternalAgentServer { - fn downcast_mut(&mut self) -> Option<&mut T> { - self.as_any_mut().downcast_mut() - } -} - -enum AgentServerStoreState { - Local { - node_runtime: NodeRuntime, - fs: Arc, - project_environment: Entity, - downstream_client: Option<(u64, AnyProtoClient)>, - settings: Option, - http_client: Arc, - extension_agents: Vec<( - Arc, - String, - HashMap, - HashMap, - Option, - )>, - _subscriptions: [Subscription; 1], - }, - Remote { - project_id: u64, - upstream_client: Entity, - }, - Collab, -} - -pub struct AgentServerStore { - state: AgentServerStoreState, - external_agents: HashMap>, - agent_icons: HashMap, - agent_display_names: HashMap, -} - -pub struct AgentServersUpdated; - -impl EventEmitter for AgentServerStore {} - -#[cfg(test)] -mod ext_agent_tests { - use super::*; - use std::{collections::HashSet, fmt::Write as _}; - - // Helper to build a store in Collab mode so we can mutate internal maps without - // needing to spin up a full project environment. - fn collab_store() -> AgentServerStore { - AgentServerStore { - state: AgentServerStoreState::Collab, - external_agents: HashMap::default(), - agent_icons: HashMap::default(), - agent_display_names: HashMap::default(), - } - } - - // A simple fake that implements ExternalAgentServer without needing async plumbing. - struct NoopExternalAgent; - - impl ExternalAgentServer for NoopExternalAgent { - fn get_command( - &mut self, - _root_dir: Option<&str>, - _extra_env: HashMap, - _status_tx: Option>, - _new_version_available_tx: Option>>, - _cx: &mut AsyncApp, - ) -> Task)>> { - Task::ready(Ok(( - AgentServerCommand { - path: PathBuf::from("noop"), - args: Vec::new(), - env: None, - }, - "".to_string(), - None, - ))) - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - } - - #[test] - fn external_agent_server_name_display() { - let name = ExternalAgentServerName(SharedString::from("Ext: Tool")); - let mut s = String::new(); - write!(&mut s, "{name}").unwrap(); - assert_eq!(s, "Ext: Tool"); - } - - #[test] - fn sync_extension_agents_removes_previous_extension_entries() { - let mut store = collab_store(); - - // Seed with a couple of agents that will be replaced by extensions - store.external_agents.insert( - ExternalAgentServerName(SharedString::from("foo-agent")), - Box::new(NoopExternalAgent) as Box, - ); - store.external_agents.insert( - ExternalAgentServerName(SharedString::from("bar-agent")), - Box::new(NoopExternalAgent) as Box, - ); - store.external_agents.insert( - ExternalAgentServerName(SharedString::from("custom")), - Box::new(NoopExternalAgent) as Box, - ); - - // Simulate the removal phase: if we're syncing extensions that provide - // "foo-agent" and "bar-agent", those should be removed first - let extension_agent_names: HashSet = - ["foo-agent".to_string(), "bar-agent".to_string()] - .into_iter() - .collect(); - - let keys_to_remove: Vec<_> = store - .external_agents - .keys() - .filter(|name| extension_agent_names.contains(name.0.as_ref())) - .cloned() - .collect(); - - for key in keys_to_remove { - store.external_agents.remove(&key); - } - - // Only the custom entry should remain. - let remaining: Vec<_> = store - .external_agents - .keys() - .map(|k| k.0.to_string()) - .collect(); - assert_eq!(remaining, vec!["custom".to_string()]); - } -} - -impl AgentServerStore { - /// Synchronizes extension-provided agent servers with the store. - pub fn sync_extension_agents<'a, I>( - &mut self, - manifests: I, - extensions_dir: PathBuf, - cx: &mut Context, - ) where - I: IntoIterator, - { - // Collect manifests first so we can iterate twice - let manifests: Vec<_> = manifests.into_iter().collect(); - - // Remove all extension-provided agents - // (They will be re-added below if they're in the currently installed extensions) - self.external_agents.retain(|name, agent| { - if agent.downcast_mut::().is_some() { - self.agent_icons.remove(name); - self.agent_display_names.remove(name); - false - } else { - // Keep the hardcoded external agents that don't come from extensions - // (In the future we may move these over to being extensions too.) - true - } - }); - - // Insert agent servers from extension manifests - match &mut self.state { - AgentServerStoreState::Local { - extension_agents, .. - } => { - extension_agents.clear(); - for (ext_id, manifest) in manifests { - for (agent_name, agent_entry) in &manifest.agent_servers { - // Store absolute icon path if provided, resolving symlinks for dev extensions - // Store display name from manifest - self.agent_display_names.insert( - ExternalAgentServerName(agent_name.clone().into()), - SharedString::from(agent_entry.name.clone()), - ); - - let icon_path = if let Some(icon) = &agent_entry.icon { - let icon_path = extensions_dir.join(ext_id).join(icon); - // Canonicalize to resolve symlinks (dev extensions are symlinked) - let absolute_icon_path = icon_path - .canonicalize() - .unwrap_or(icon_path) - .to_string_lossy() - .to_string(); - self.agent_icons.insert( - ExternalAgentServerName(agent_name.clone().into()), - SharedString::from(absolute_icon_path.clone()), - ); - Some(absolute_icon_path) - } else { - None - }; - - extension_agents.push(( - agent_name.clone(), - ext_id.to_owned(), - agent_entry.targets.clone(), - agent_entry.env.clone(), - icon_path, - )); - } - } - self.reregister_agents(cx); - } - AgentServerStoreState::Remote { - project_id, - upstream_client, - } => { - let mut agents = vec![]; - for (ext_id, manifest) in manifests { - for (agent_name, agent_entry) in &manifest.agent_servers { - // Store display name from manifest - self.agent_display_names.insert( - ExternalAgentServerName(agent_name.clone().into()), - SharedString::from(agent_entry.name.clone()), - ); - - // Store absolute icon path if provided, resolving symlinks for dev extensions - let icon = if let Some(icon) = &agent_entry.icon { - let icon_path = extensions_dir.join(ext_id).join(icon); - // Canonicalize to resolve symlinks (dev extensions are symlinked) - let absolute_icon_path = icon_path - .canonicalize() - .unwrap_or(icon_path) - .to_string_lossy() - .to_string(); - - // Store icon locally for remote client - self.agent_icons.insert( - ExternalAgentServerName(agent_name.clone().into()), - SharedString::from(absolute_icon_path.clone()), - ); - - Some(absolute_icon_path) - } else { - None - }; - - agents.push(ExternalExtensionAgent { - name: agent_name.to_string(), - icon_path: icon, - extension_id: ext_id.to_string(), - targets: agent_entry - .targets - .iter() - .map(|(k, v)| (k.clone(), v.to_proto())) - .collect(), - env: agent_entry - .env - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect(), - }); - } - } - upstream_client - .read(cx) - .proto_client() - .send(proto::ExternalExtensionAgentsUpdated { - project_id: *project_id, - agents, - }) - .log_err(); - } - AgentServerStoreState::Collab => { - // Do nothing - } - } - - cx.emit(AgentServersUpdated); - } - - pub fn agent_icon(&self, name: &ExternalAgentServerName) -> Option { - self.agent_icons.get(name).cloned() - } - - pub fn agent_display_name(&self, name: &ExternalAgentServerName) -> Option { - self.agent_display_names.get(name).cloned() - } - - pub fn init_remote(session: &AnyProtoClient) { - session.add_entity_message_handler(Self::handle_external_agents_updated); - session.add_entity_message_handler(Self::handle_loading_status_updated); - session.add_entity_message_handler(Self::handle_new_version_available); - } - - pub fn init_headless(session: &AnyProtoClient) { - session.add_entity_message_handler(Self::handle_external_extension_agents_updated); - session.add_entity_request_handler(Self::handle_get_agent_server_command); - } - - fn agent_servers_settings_changed(&mut self, cx: &mut Context) { - let AgentServerStoreState::Local { - settings: old_settings, - .. - } = &mut self.state - else { - debug_panic!( - "should not be subscribed to agent server settings changes in non-local project" - ); - return; - }; - - let new_settings = cx - .global::() - .get::(None) - .clone(); - if Some(&new_settings) == old_settings.as_ref() { - return; - } - - self.reregister_agents(cx); - } - - fn reregister_agents(&mut self, cx: &mut Context) { - let AgentServerStoreState::Local { - node_runtime, - fs, - project_environment, - downstream_client, - settings: old_settings, - http_client, - extension_agents, - .. - } = &mut self.state - else { - debug_panic!("Non-local projects should never attempt to reregister. This is a bug!"); - - return; - }; - - let new_settings = cx - .global::() - .get::(None) - .clone(); - - self.external_agents.clear(); - self.external_agents.insert( - GEMINI_NAME.into(), - Box::new(LocalGemini { - fs: fs.clone(), - node_runtime: node_runtime.clone(), - project_environment: project_environment.clone(), - custom_command: new_settings - .gemini - .clone() - .and_then(|settings| settings.custom_command()), - ignore_system_version: new_settings - .gemini - .as_ref() - .and_then(|settings| settings.ignore_system_version) - .unwrap_or(false), - }), - ); - self.external_agents.insert( - CODEX_NAME.into(), - Box::new(LocalCodex { - fs: fs.clone(), - project_environment: project_environment.clone(), - custom_command: new_settings - .codex - .clone() - .and_then(|settings| settings.custom_command()), - http_client: http_client.clone(), - no_browser: downstream_client - .as_ref() - .is_some_and(|(_, client)| !client.has_wsl_interop()), - }), - ); - self.external_agents.insert( - CLAUDE_CODE_NAME.into(), - Box::new(LocalClaudeCode { - fs: fs.clone(), - node_runtime: node_runtime.clone(), - project_environment: project_environment.clone(), - custom_command: new_settings - .claude - .clone() - .and_then(|settings| settings.custom_command()), - }), - ); - self.external_agents - .extend( - new_settings - .custom - .iter() - .filter_map(|(name, settings)| match settings { - CustomAgentServerSettings::Custom { command, .. } => Some(( - ExternalAgentServerName(name.clone()), - Box::new(LocalCustomAgent { - command: command.clone(), - project_environment: project_environment.clone(), - }) as Box, - )), - CustomAgentServerSettings::Extension { .. } => None, - }), - ); - self.external_agents.extend(extension_agents.iter().map( - |(agent_name, ext_id, targets, env, icon_path)| { - let name = ExternalAgentServerName(agent_name.clone().into()); - - // Restore icon if present - if let Some(icon) = icon_path { - self.agent_icons - .insert(name.clone(), SharedString::from(icon.clone())); - } - - ( - name, - Box::new(LocalExtensionArchiveAgent { - fs: fs.clone(), - http_client: http_client.clone(), - node_runtime: node_runtime.clone(), - project_environment: project_environment.clone(), - extension_id: Arc::from(&**ext_id), - targets: targets.clone(), - env: env.clone(), - agent_id: agent_name.clone(), - }) as Box, - ) - }, - )); - - *old_settings = Some(new_settings.clone()); - - if let Some((project_id, downstream_client)) = downstream_client { - downstream_client - .send(proto::ExternalAgentsUpdated { - project_id: *project_id, - names: self - .external_agents - .keys() - .map(|name| name.to_string()) - .collect(), - }) - .log_err(); - } - cx.emit(AgentServersUpdated); - } - - pub fn node_runtime(&self) -> Option { - match &self.state { - AgentServerStoreState::Local { node_runtime, .. } => Some(node_runtime.clone()), - _ => None, - } - } - - pub fn local( - node_runtime: NodeRuntime, - fs: Arc, - project_environment: Entity, - http_client: Arc, - cx: &mut Context, - ) -> Self { - let subscription = cx.observe_global::(|this, cx| { - this.agent_servers_settings_changed(cx); - }); - let mut this = Self { - state: AgentServerStoreState::Local { - node_runtime, - fs, - project_environment, - http_client, - downstream_client: None, - settings: None, - extension_agents: vec![], - _subscriptions: [subscription], - }, - external_agents: Default::default(), - agent_icons: Default::default(), - agent_display_names: Default::default(), - }; - if let Some(_events) = extension::ExtensionEvents::try_global(cx) {} - this.agent_servers_settings_changed(cx); - this - } - - pub(crate) fn remote(project_id: u64, upstream_client: Entity) -> Self { - // Set up the builtin agents here so they're immediately available in - // remote projects--we know that the HeadlessProject on the other end - // will have them. - let external_agents: [(ExternalAgentServerName, Box); 3] = [ - ( - CLAUDE_CODE_NAME.into(), - Box::new(RemoteExternalAgentServer { - project_id, - upstream_client: upstream_client.clone(), - name: CLAUDE_CODE_NAME.into(), - status_tx: None, - new_version_available_tx: None, - }) as Box, - ), - ( - CODEX_NAME.into(), - Box::new(RemoteExternalAgentServer { - project_id, - upstream_client: upstream_client.clone(), - name: CODEX_NAME.into(), - status_tx: None, - new_version_available_tx: None, - }) as Box, - ), - ( - GEMINI_NAME.into(), - Box::new(RemoteExternalAgentServer { - project_id, - upstream_client: upstream_client.clone(), - name: GEMINI_NAME.into(), - status_tx: None, - new_version_available_tx: None, - }) as Box, - ), - ]; - - Self { - state: AgentServerStoreState::Remote { - project_id, - upstream_client, - }, - external_agents: external_agents.into_iter().collect(), - agent_icons: HashMap::default(), - agent_display_names: HashMap::default(), - } - } - - pub(crate) fn collab(_cx: &mut Context) -> Self { - Self { - state: AgentServerStoreState::Collab, - external_agents: Default::default(), - agent_icons: Default::default(), - agent_display_names: Default::default(), - } - } - - pub fn shared(&mut self, project_id: u64, client: AnyProtoClient, cx: &mut Context) { - match &mut self.state { - AgentServerStoreState::Local { - downstream_client, .. - } => { - *downstream_client = Some((project_id, client.clone())); - // Send the current list of external agents downstream, but only after a delay, - // to avoid having the message arrive before the downstream project's agent server store - // sets up its handlers. - cx.spawn(async move |this, cx| { - cx.background_executor().timer(Duration::from_secs(1)).await; - let names = this.update(cx, |this, _| { - this.external_agents - .keys() - .map(|name| name.to_string()) - .collect() - })?; - client - .send(proto::ExternalAgentsUpdated { project_id, names }) - .log_err(); - anyhow::Ok(()) - }) - .detach(); - } - AgentServerStoreState::Remote { .. } => { - debug_panic!( - "external agents over collab not implemented, remote project should not be shared" - ); - } - AgentServerStoreState::Collab => { - debug_panic!("external agents over collab not implemented, should not be shared"); - } - } - } - - pub fn get_external_agent( - &mut self, - name: &ExternalAgentServerName, - ) -> Option<&mut (dyn ExternalAgentServer + 'static)> { - self.external_agents - .get_mut(name) - .map(|agent| agent.as_mut()) - } - - pub fn external_agents(&self) -> impl Iterator { - self.external_agents.keys() - } - - async fn handle_get_agent_server_command( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let (command, root_dir, login_command) = this - .update(&mut cx, |this, cx| { - let AgentServerStoreState::Local { - downstream_client, .. - } = &this.state - else { - debug_panic!("should not receive GetAgentServerCommand in a non-local project"); - bail!("unexpected GetAgentServerCommand request in a non-local project"); - }; - let agent = this - .external_agents - .get_mut(&*envelope.payload.name) - .with_context(|| format!("agent `{}` not found", envelope.payload.name))?; - let (status_tx, new_version_available_tx) = downstream_client - .clone() - .map(|(project_id, downstream_client)| { - let (status_tx, mut status_rx) = watch::channel(SharedString::from("")); - let (new_version_available_tx, mut new_version_available_rx) = - watch::channel(None); - cx.spawn({ - let downstream_client = downstream_client.clone(); - let name = envelope.payload.name.clone(); - async move |_, _| { - while let Some(status) = status_rx.recv().await.ok() { - downstream_client.send( - proto::ExternalAgentLoadingStatusUpdated { - project_id, - name: name.clone(), - status: status.to_string(), - }, - )?; - } - anyhow::Ok(()) - } - }) - .detach_and_log_err(cx); - cx.spawn({ - let name = envelope.payload.name.clone(); - async move |_, _| { - if let Some(version) = - new_version_available_rx.recv().await.ok().flatten() - { - downstream_client.send( - proto::NewExternalAgentVersionAvailable { - project_id, - name: name.clone(), - version, - }, - )?; - } - anyhow::Ok(()) - } - }) - .detach_and_log_err(cx); - (status_tx, new_version_available_tx) - }) - .unzip(); - anyhow::Ok(agent.get_command( - envelope.payload.root_dir.as_deref(), - HashMap::default(), - status_tx, - new_version_available_tx, - &mut cx.to_async(), - )) - })?? - .await?; - Ok(proto::AgentServerCommand { - path: command.path.to_string_lossy().into_owned(), - args: command.args, - env: command - .env - .map(|env| env.into_iter().collect()) - .unwrap_or_default(), - root_dir: root_dir, - login: login_command.map(|cmd| cmd.to_proto()), - }) - } - - async fn handle_external_agents_updated( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - this.update(&mut cx, |this, cx| { - let AgentServerStoreState::Remote { - project_id, - upstream_client, - } = &this.state - else { - debug_panic!( - "handle_external_agents_updated should not be called for a non-remote project" - ); - bail!("unexpected ExternalAgentsUpdated message") - }; - - let mut status_txs = this - .external_agents - .iter_mut() - .filter_map(|(name, agent)| { - Some(( - name.clone(), - agent - .downcast_mut::()? - .status_tx - .take(), - )) - }) - .collect::>(); - let mut new_version_available_txs = this - .external_agents - .iter_mut() - .filter_map(|(name, agent)| { - Some(( - name.clone(), - agent - .downcast_mut::()? - .new_version_available_tx - .take(), - )) - }) - .collect::>(); - - this.external_agents = envelope - .payload - .names - .into_iter() - .map(|name| { - let agent = RemoteExternalAgentServer { - project_id: *project_id, - upstream_client: upstream_client.clone(), - name: ExternalAgentServerName(name.clone().into()), - status_tx: status_txs.remove(&*name).flatten(), - new_version_available_tx: new_version_available_txs - .remove(&*name) - .flatten(), - }; - ( - ExternalAgentServerName(name.into()), - Box::new(agent) as Box, - ) - }) - .collect(); - cx.emit(AgentServersUpdated); - Ok(()) - })? - } - - async fn handle_external_extension_agents_updated( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - this.update(&mut cx, |this, cx| { - let AgentServerStoreState::Local { - extension_agents, .. - } = &mut this.state - else { - panic!( - "handle_external_extension_agents_updated \ - should not be called for a non-remote project" - ); - }; - - for ExternalExtensionAgent { - name, - icon_path, - extension_id, - targets, - env, - } in envelope.payload.agents - { - let icon_path_string = icon_path.clone(); - if let Some(icon_path) = icon_path { - this.agent_icons.insert( - ExternalAgentServerName(name.clone().into()), - icon_path.into(), - ); - } - extension_agents.push(( - Arc::from(&*name), - extension_id, - targets - .into_iter() - .map(|(k, v)| (k, extension::TargetConfig::from_proto(v))) - .collect(), - env.into_iter().collect(), - icon_path_string, - )); - } - - this.reregister_agents(cx); - cx.emit(AgentServersUpdated); - Ok(()) - })? - } - - async fn handle_loading_status_updated( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - this.update(&mut cx, |this, _| { - if let Some(agent) = this.external_agents.get_mut(&*envelope.payload.name) - && let Some(agent) = agent.downcast_mut::() - && let Some(status_tx) = &mut agent.status_tx - { - status_tx.send(envelope.payload.status.into()).ok(); - } - }) - } - - async fn handle_new_version_available( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - this.update(&mut cx, |this, _| { - if let Some(agent) = this.external_agents.get_mut(&*envelope.payload.name) - && let Some(agent) = agent.downcast_mut::() - && let Some(new_version_available_tx) = &mut agent.new_version_available_tx - { - new_version_available_tx - .send(Some(envelope.payload.version)) - .ok(); - } - }) - } - - pub fn get_extension_id_for_agent( - &mut self, - name: &ExternalAgentServerName, - ) -> Option> { - self.external_agents.get_mut(name).and_then(|agent| { - agent - .as_any_mut() - .downcast_ref::() - .map(|ext_agent| ext_agent.extension_id.clone()) - }) - } -} - -fn get_or_npm_install_builtin_agent( - binary_name: SharedString, - package_name: SharedString, - entrypoint_path: PathBuf, - minimum_version: Option, - status_tx: Option>, - new_version_available: Option>>, - fs: Arc, - node_runtime: NodeRuntime, - cx: &mut AsyncApp, -) -> Task> { - cx.spawn(async move |cx| { - let node_path = node_runtime.binary_path().await?; - let dir = paths::external_agents_dir().join(binary_name.as_str()); - fs.create_dir(&dir).await?; - - let mut stream = fs.read_dir(&dir).await?; - let mut versions = Vec::new(); - let mut to_delete = Vec::new(); - while let Some(entry) = stream.next().await { - let Ok(entry) = entry else { continue }; - let Some(file_name) = entry.file_name() else { - continue; - }; - - if let Some(name) = file_name.to_str() - && let Some(version) = semver::Version::from_str(name).ok() - && fs - .is_file(&dir.join(file_name).join(&entrypoint_path)) - .await - { - versions.push((version, file_name.to_owned())); - } else { - to_delete.push(file_name.to_owned()) - } - } - - versions.sort(); - let newest_version = if let Some((version, file_name)) = versions.last().cloned() - && minimum_version.is_none_or(|minimum_version| version >= minimum_version) - { - versions.pop(); - Some(file_name) - } else { - None - }; - log::debug!("existing version of {package_name}: {newest_version:?}"); - to_delete.extend(versions.into_iter().map(|(_, file_name)| file_name)); - - cx.background_spawn({ - let fs = fs.clone(); - let dir = dir.clone(); - async move { - for file_name in to_delete { - fs.remove_dir( - &dir.join(file_name), - RemoveOptions { - recursive: true, - ignore_if_not_exists: false, - }, - ) - .await - .ok(); - } - } - }) - .detach(); - - let version = if let Some(file_name) = newest_version { - cx.background_spawn({ - let file_name = file_name.clone(); - let dir = dir.clone(); - let fs = fs.clone(); - async move { - let latest_version = node_runtime - .npm_package_latest_version(&package_name) - .await - .ok(); - if let Some(latest_version) = latest_version - && &latest_version != &file_name.to_string_lossy() - { - let download_result = download_latest_version( - fs, - dir.clone(), - node_runtime, - package_name.clone(), - ) - .await - .log_err(); - if let Some(mut new_version_available) = new_version_available - && download_result.is_some() - { - new_version_available.send(Some(latest_version)).ok(); - } - } - } - }) - .detach(); - file_name - } else { - if let Some(mut status_tx) = status_tx { - status_tx.send("Installing…".into()).ok(); - } - let dir = dir.clone(); - cx.background_spawn(download_latest_version( - fs.clone(), - dir.clone(), - node_runtime, - package_name.clone(), - )) - .await? - .into() - }; - - let agent_server_path = dir.join(version).join(entrypoint_path); - let agent_server_path_exists = fs.is_file(&agent_server_path).await; - anyhow::ensure!( - agent_server_path_exists, - "Missing entrypoint path {} after installation", - agent_server_path.to_string_lossy() - ); - - anyhow::Ok(AgentServerCommand { - path: node_path, - args: vec![agent_server_path.to_string_lossy().into_owned()], - env: None, - }) - }) -} - -fn find_bin_in_path( - bin_name: SharedString, - root_dir: PathBuf, - env: HashMap, - cx: &mut AsyncApp, -) -> Task> { - cx.background_executor().spawn(async move { - let which_result = if cfg!(windows) { - which::which(bin_name.as_str()) - } else { - let shell_path = env.get("PATH").cloned(); - which::which_in(bin_name.as_str(), shell_path.as_ref(), &root_dir) - }; - - if let Err(which::Error::CannotFindBinaryPath) = which_result { - return None; - } - - which_result.log_err() - }) -} - -async fn download_latest_version( - fs: Arc, - dir: PathBuf, - node_runtime: NodeRuntime, - package_name: SharedString, -) -> Result { - log::debug!("downloading latest version of {package_name}"); - - let tmp_dir = tempfile::tempdir_in(&dir)?; - - node_runtime - .npm_install_packages(tmp_dir.path(), &[(&package_name, "latest")]) - .await?; - - let version = node_runtime - .npm_package_installed_version(tmp_dir.path(), &package_name) - .await? - .context("expected package to be installed")?; - - fs.rename( - &tmp_dir.keep(), - &dir.join(&version), - RenameOptions { - ignore_if_exists: true, - overwrite: true, - create_parents: false, - }, - ) - .await?; - - anyhow::Ok(version) -} - -struct RemoteExternalAgentServer { - project_id: u64, - upstream_client: Entity, - name: ExternalAgentServerName, - status_tx: Option>, - new_version_available_tx: Option>>, -} - -impl ExternalAgentServer for RemoteExternalAgentServer { - fn get_command( - &mut self, - root_dir: Option<&str>, - extra_env: HashMap, - status_tx: Option>, - new_version_available_tx: Option>>, - cx: &mut AsyncApp, - ) -> Task)>> { - let project_id = self.project_id; - let name = self.name.to_string(); - let upstream_client = self.upstream_client.downgrade(); - let root_dir = root_dir.map(|root_dir| root_dir.to_owned()); - self.status_tx = status_tx; - self.new_version_available_tx = new_version_available_tx; - cx.spawn(async move |cx| { - let mut response = upstream_client - .update(cx, |upstream_client, _| { - upstream_client - .proto_client() - .request(proto::GetAgentServerCommand { - project_id, - name, - root_dir: root_dir.clone(), - }) - })? - .await?; - let root_dir = response.root_dir; - response.env.extend(extra_env); - let command = upstream_client.update(cx, |client, _| { - client.build_command( - Some(response.path), - &response.args, - &response.env.into_iter().collect(), - Some(root_dir.clone()), - None, - ) - })??; - Ok(( - AgentServerCommand { - path: command.program.into(), - args: command.args, - env: Some(command.env), - }, - root_dir, - response.login.map(SpawnInTerminal::from_proto), - )) - }) - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } -} - -struct LocalGemini { - fs: Arc, - node_runtime: NodeRuntime, - project_environment: Entity, - custom_command: Option, - ignore_system_version: bool, -} - -impl ExternalAgentServer for LocalGemini { - fn get_command( - &mut self, - root_dir: Option<&str>, - extra_env: HashMap, - status_tx: Option>, - new_version_available_tx: Option>>, - cx: &mut AsyncApp, - ) -> Task)>> { - let fs = self.fs.clone(); - let node_runtime = self.node_runtime.clone(); - let project_environment = self.project_environment.downgrade(); - let custom_command = self.custom_command.clone(); - let ignore_system_version = self.ignore_system_version; - let root_dir: Arc = root_dir - .map(|root_dir| Path::new(root_dir)) - .unwrap_or(paths::home_dir()) - .into(); - - cx.spawn(async move |cx| { - let mut env = project_environment - .update(cx, |project_environment, cx| { - project_environment.local_directory_environment( - &Shell::System, - root_dir.clone(), - cx, - ) - })? - .await - .unwrap_or_default(); - - let mut command = if let Some(mut custom_command) = custom_command { - env.extend(custom_command.env.unwrap_or_default()); - custom_command.env = Some(env); - custom_command - } else if !ignore_system_version - && let Some(bin) = - find_bin_in_path("gemini".into(), root_dir.to_path_buf(), env.clone(), cx).await - { - AgentServerCommand { - path: bin, - args: Vec::new(), - env: Some(env), - } - } else { - let mut command = get_or_npm_install_builtin_agent( - GEMINI_NAME.into(), - "@google/gemini-cli".into(), - "node_modules/@google/gemini-cli/dist/index.js".into(), - if cfg!(windows) { - // v0.8.x on Windows has a bug that causes the initialize request to hang forever - Some("0.9.0".parse().unwrap()) - } else { - Some("0.2.1".parse().unwrap()) - }, - status_tx, - new_version_available_tx, - fs, - node_runtime, - cx, - ) - .await?; - command.env = Some(env); - command - }; - - // Gemini CLI doesn't seem to have a dedicated invocation for logging in--we just run it normally without any arguments. - let login = task::SpawnInTerminal { - command: Some(command.path.to_string_lossy().into_owned()), - args: command.args.clone(), - env: command.env.clone().unwrap_or_default(), - label: "gemini /auth".into(), - ..Default::default() - }; - - command.env.get_or_insert_default().extend(extra_env); - command.args.push("--experimental-acp".into()); - Ok(( - command, - root_dir.to_string_lossy().into_owned(), - Some(login), - )) - }) - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } -} - -struct LocalClaudeCode { - fs: Arc, - node_runtime: NodeRuntime, - project_environment: Entity, - custom_command: Option, -} - -impl ExternalAgentServer for LocalClaudeCode { - fn get_command( - &mut self, - root_dir: Option<&str>, - extra_env: HashMap, - status_tx: Option>, - new_version_available_tx: Option>>, - cx: &mut AsyncApp, - ) -> Task)>> { - let fs = self.fs.clone(); - let node_runtime = self.node_runtime.clone(); - let project_environment = self.project_environment.downgrade(); - let custom_command = self.custom_command.clone(); - let root_dir: Arc = root_dir - .map(|root_dir| Path::new(root_dir)) - .unwrap_or(paths::home_dir()) - .into(); - - cx.spawn(async move |cx| { - let mut env = project_environment - .update(cx, |project_environment, cx| { - project_environment.local_directory_environment( - &Shell::System, - root_dir.clone(), - cx, - ) - })? - .await - .unwrap_or_default(); - env.insert("ANTHROPIC_API_KEY".into(), "".into()); - - let (mut command, login_command) = if let Some(mut custom_command) = custom_command { - env.extend(custom_command.env.unwrap_or_default()); - custom_command.env = Some(env); - (custom_command, None) - } else { - let mut command = get_or_npm_install_builtin_agent( - "claude-code-acp".into(), - "@zed-industries/claude-code-acp".into(), - "node_modules/@zed-industries/claude-code-acp/dist/index.js".into(), - Some("0.5.2".parse().unwrap()), - status_tx, - new_version_available_tx, - fs, - node_runtime, - cx, - ) - .await?; - command.env = Some(env); - let login = command - .args - .first() - .and_then(|path| { - path.strip_suffix("/@zed-industries/claude-code-acp/dist/index.js") - }) - .map(|path_prefix| task::SpawnInTerminal { - command: Some(command.path.to_string_lossy().into_owned()), - args: vec![ - Path::new(path_prefix) - .join("@anthropic-ai/claude-agent-sdk/cli.js") - .to_string_lossy() - .to_string(), - "/login".into(), - ], - env: command.env.clone().unwrap_or_default(), - label: "claude /login".into(), - ..Default::default() - }); - (command, login) - }; - - command.env.get_or_insert_default().extend(extra_env); - Ok(( - command, - root_dir.to_string_lossy().into_owned(), - login_command, - )) - }) - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } -} - -struct LocalCodex { - fs: Arc, - project_environment: Entity, - http_client: Arc, - custom_command: Option, - no_browser: bool, -} - -impl ExternalAgentServer for LocalCodex { - fn get_command( - &mut self, - root_dir: Option<&str>, - extra_env: HashMap, - mut status_tx: Option>, - _new_version_available_tx: Option>>, - cx: &mut AsyncApp, - ) -> Task)>> { - let fs = self.fs.clone(); - let project_environment = self.project_environment.downgrade(); - let http = self.http_client.clone(); - let custom_command = self.custom_command.clone(); - let root_dir: Arc = root_dir - .map(|root_dir| Path::new(root_dir)) - .unwrap_or(paths::home_dir()) - .into(); - let no_browser = self.no_browser; - - cx.spawn(async move |cx| { - let mut env = project_environment - .update(cx, |project_environment, cx| { - project_environment.local_directory_environment( - &Shell::System, - root_dir.clone(), - cx, - ) - })? - .await - .unwrap_or_default(); - if no_browser { - env.insert("NO_BROWSER".to_owned(), "1".to_owned()); - } - - let mut command = if let Some(mut custom_command) = custom_command { - env.extend(custom_command.env.unwrap_or_default()); - custom_command.env = Some(env); - custom_command - } else { - let dir = paths::external_agents_dir().join(CODEX_NAME); - fs.create_dir(&dir).await?; - - let bin_name = if cfg!(windows) { - "codex-acp.exe" - } else { - "codex-acp" - }; - - let find_latest_local_version = async || -> Option { - let mut local_versions: Vec<(semver::Version, String)> = Vec::new(); - let mut stream = fs.read_dir(&dir).await.ok()?; - while let Some(entry) = stream.next().await { - let Ok(entry) = entry else { continue }; - let Some(file_name) = entry.file_name() else { - continue; - }; - let version_path = dir.join(&file_name); - if fs.is_file(&version_path.join(bin_name)).await { - let version_str = file_name.to_string_lossy(); - if let Ok(version) = - semver::Version::from_str(version_str.trim_start_matches('v')) - { - local_versions.push((version, version_str.into_owned())); - } - } - } - local_versions.sort_by(|(a, _), (b, _)| a.cmp(b)); - local_versions.last().map(|(_, v)| dir.join(v)) - }; - - let fallback_to_latest_local_version = - async |err: anyhow::Error| -> Result { - if let Some(local) = find_latest_local_version().await { - log::info!( - "Falling back to locally installed Codex version: {}", - local.display() - ); - Ok(local) - } else { - Err(err) - } - }; - - let version_dir = match ::http_client::github::latest_github_release( - CODEX_ACP_REPO, - true, - false, - http.clone(), - ) - .await - { - Ok(release) => { - let version_dir = dir.join(&release.tag_name); - if !fs.is_dir(&version_dir).await { - if let Some(ref mut status_tx) = status_tx { - status_tx.send("Installing…".into()).ok(); - } - - let tag = release.tag_name.clone(); - let version_number = tag.trim_start_matches('v'); - let asset_name = asset_name(version_number) - .context("codex acp is not supported for this architecture")?; - let asset = release - .assets - .into_iter() - .find(|asset| asset.name == asset_name) - .with_context(|| { - format!("no asset found matching `{asset_name:?}`") - })?; - // Strip "sha256:" prefix from digest if present (GitHub API format) - let digest = asset - .digest - .as_deref() - .and_then(|d| d.strip_prefix("sha256:").or(Some(d))); - match ::http_client::github_download::download_server_binary( - &*http, - &asset.browser_download_url, - digest, - &version_dir, - if cfg!(target_os = "windows") && cfg!(target_arch = "x86_64") { - AssetKind::Zip - } else { - AssetKind::TarGz - }, - ) - .await - { - Ok(()) => { - // remove older versions - util::fs::remove_matching(&dir, |entry| entry != version_dir) - .await; - version_dir - } - Err(err) => { - log::error!( - "Failed to download Codex release {}: {err:#}", - release.tag_name - ); - fallback_to_latest_local_version(err).await? - } - } - } else { - version_dir - } - } - Err(err) => { - log::error!("Failed to fetch Codex latest release: {err:#}"); - fallback_to_latest_local_version(err).await? - } - }; - - let bin_path = version_dir.join(bin_name); - anyhow::ensure!( - fs.is_file(&bin_path).await, - "Missing Codex binary at {} after installation", - bin_path.to_string_lossy() - ); - - let mut cmd = AgentServerCommand { - path: bin_path, - args: Vec::new(), - env: None, - }; - cmd.env = Some(env); - cmd - }; - - command.env.get_or_insert_default().extend(extra_env); - Ok((command, root_dir.to_string_lossy().into_owned(), None)) - }) - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } -} - -pub const CODEX_ACP_REPO: &str = "zed-industries/codex-acp"; - -fn get_platform_info() -> Option<(&'static str, &'static str, &'static str)> { - let arch = if cfg!(target_arch = "x86_64") { - "x86_64" - } else if cfg!(target_arch = "aarch64") { - "aarch64" - } else { - return None; - }; - - let platform = if cfg!(target_os = "macos") { - "apple-darwin" - } else if cfg!(target_os = "windows") { - "pc-windows-msvc" - } else if cfg!(target_os = "linux") { - "unknown-linux-gnu" - } else { - return None; - }; - - // Windows uses .zip in release assets - let ext = if cfg!(target_os = "windows") { - "zip" - } else { - "tar.gz" - }; - - Some((arch, platform, ext)) -} - -fn asset_name(version: &str) -> Option { - let (arch, platform, ext) = get_platform_info()?; - Some(format!("codex-acp-{version}-{arch}-{platform}.{ext}")) -} - -struct LocalExtensionArchiveAgent { - fs: Arc, - http_client: Arc, - node_runtime: NodeRuntime, - project_environment: Entity, - extension_id: Arc, - agent_id: Arc, - targets: HashMap, - env: HashMap, -} - -struct LocalCustomAgent { - project_environment: Entity, - command: AgentServerCommand, -} - -impl ExternalAgentServer for LocalExtensionArchiveAgent { - fn get_command( - &mut self, - root_dir: Option<&str>, - extra_env: HashMap, - _status_tx: Option>, - _new_version_available_tx: Option>>, - cx: &mut AsyncApp, - ) -> Task)>> { - let fs = self.fs.clone(); - let http_client = self.http_client.clone(); - let node_runtime = self.node_runtime.clone(); - let project_environment = self.project_environment.downgrade(); - let extension_id = self.extension_id.clone(); - let agent_id = self.agent_id.clone(); - let targets = self.targets.clone(); - let base_env = self.env.clone(); - - let root_dir: Arc = root_dir - .map(|root_dir| Path::new(root_dir)) - .unwrap_or(paths::home_dir()) - .into(); - - cx.spawn(async move |cx| { - // Get project environment - let mut env = project_environment - .update(cx, |project_environment, cx| { - project_environment.local_directory_environment( - &Shell::System, - root_dir.clone(), - cx, - ) - })? - .await - .unwrap_or_default(); - - // Merge manifest env and extra env - env.extend(base_env); - env.extend(extra_env); - - let cache_key = format!("{}/{}", extension_id, agent_id); - let dir = paths::external_agents_dir().join(&cache_key); - fs.create_dir(&dir).await?; - - // Determine platform key - let os = if cfg!(target_os = "macos") { - "darwin" - } else if cfg!(target_os = "linux") { - "linux" - } else if cfg!(target_os = "windows") { - "windows" - } else { - anyhow::bail!("unsupported OS"); - }; - - let arch = if cfg!(target_arch = "aarch64") { - "aarch64" - } else if cfg!(target_arch = "x86_64") { - "x86_64" - } else { - anyhow::bail!("unsupported architecture"); - }; - - let platform_key = format!("{}-{}", os, arch); - let target_config = targets.get(&platform_key).with_context(|| { - format!( - "no target specified for platform '{}'. Available platforms: {}", - platform_key, - targets - .keys() - .map(|k| k.as_str()) - .collect::>() - .join(", ") - ) - })?; - - let archive_url = &target_config.archive; - - // Use URL as version identifier for caching - // Hash the URL to get a stable directory name - use std::collections::hash_map::DefaultHasher; - use std::hash::{Hash, Hasher}; - let mut hasher = DefaultHasher::new(); - archive_url.hash(&mut hasher); - let url_hash = hasher.finish(); - let version_dir = dir.join(format!("v_{:x}", url_hash)); - - if !fs.is_dir(&version_dir).await { - // Determine SHA256 for verification - let sha256 = if let Some(provided_sha) = &target_config.sha256 { - // Use provided SHA256 - Some(provided_sha.clone()) - } else if archive_url.starts_with("https://github.com/") { - // Try to fetch SHA256 from GitHub API - // Parse URL to extract repo and tag/file info - // Format: https://github.com/owner/repo/releases/download/tag/file.zip - if let Some(caps) = archive_url.strip_prefix("https://github.com/") { - let parts: Vec<&str> = caps.split('/').collect(); - if parts.len() >= 6 && parts[2] == "releases" && parts[3] == "download" { - let repo = format!("{}/{}", parts[0], parts[1]); - let tag = parts[4]; - let filename = parts[5..].join("/"); - - // Try to get release info from GitHub - if let Ok(release) = ::http_client::github::get_release_by_tag_name( - &repo, - tag, - http_client.clone(), - ) - .await - { - // Find matching asset - if let Some(asset) = - release.assets.iter().find(|a| a.name == filename) - { - // Strip "sha256:" prefix if present - asset.digest.as_ref().and_then(|d| { - d.strip_prefix("sha256:") - .map(|s| s.to_string()) - .or_else(|| Some(d.clone())) - }) - } else { - None - } - } else { - None - } - } else { - None - } - } else { - None - } - } else { - None - }; - - // Determine archive type from URL - let asset_kind = if archive_url.ends_with(".zip") { - AssetKind::Zip - } else if archive_url.ends_with(".tar.gz") || archive_url.ends_with(".tgz") { - AssetKind::TarGz - } else { - anyhow::bail!("unsupported archive type in URL: {}", archive_url); - }; - - // Download and extract - ::http_client::github_download::download_server_binary( - &*http_client, - archive_url, - sha256.as_deref(), - &version_dir, - asset_kind, - ) - .await?; - } - - // Validate and resolve cmd path - let cmd = &target_config.cmd; - - let cmd_path = if cmd == "node" { - // Use Zed's managed Node.js runtime - node_runtime.binary_path().await? - } else { - if cmd.contains("..") { - anyhow::bail!("command path cannot contain '..': {}", cmd); - } - - if cmd.starts_with("./") || cmd.starts_with(".\\") { - // Relative to extraction directory - let cmd_path = version_dir.join(&cmd[2..]); - anyhow::ensure!( - fs.is_file(&cmd_path).await, - "Missing command {} after extraction", - cmd_path.to_string_lossy() - ); - cmd_path - } else { - // On PATH - anyhow::bail!("command must be relative (start with './'): {}", cmd); - } - }; - - let command = AgentServerCommand { - path: cmd_path, - args: target_config.args.clone(), - env: Some(env), - }; - - Ok((command, version_dir.to_string_lossy().into_owned(), None)) - }) - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } -} - -impl ExternalAgentServer for LocalCustomAgent { - fn get_command( - &mut self, - root_dir: Option<&str>, - extra_env: HashMap, - _status_tx: Option>, - _new_version_available_tx: Option>>, - cx: &mut AsyncApp, - ) -> Task)>> { - let mut command = self.command.clone(); - let root_dir: Arc = root_dir - .map(|root_dir| Path::new(root_dir)) - .unwrap_or(paths::home_dir()) - .into(); - let project_environment = self.project_environment.downgrade(); - cx.spawn(async move |cx| { - let mut env = project_environment - .update(cx, |project_environment, cx| { - project_environment.local_directory_environment( - &Shell::System, - root_dir.clone(), - cx, - ) - })? - .await - .unwrap_or_default(); - env.extend(command.env.unwrap_or_default()); - env.extend(extra_env); - command.env = Some(env); - Ok((command, root_dir.to_string_lossy().into_owned(), None)) - }) - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } -} - -pub const GEMINI_NAME: &'static str = "gemini"; -pub const CLAUDE_CODE_NAME: &'static str = "claude"; -pub const CODEX_NAME: &'static str = "codex"; - -#[derive(Default, Clone, JsonSchema, Debug, PartialEq, RegisterSetting)] -pub struct AllAgentServersSettings { - pub gemini: Option, - pub claude: Option, - pub codex: Option, - pub custom: HashMap, -} -#[derive(Default, Clone, JsonSchema, Debug, PartialEq)] -pub struct BuiltinAgentServerSettings { - pub path: Option, - pub args: Option>, - pub env: Option>, - pub ignore_system_version: Option, - pub default_mode: Option, - pub default_model: Option, -} - -impl BuiltinAgentServerSettings { - pub(crate) fn custom_command(self) -> Option { - self.path.map(|path| AgentServerCommand { - path, - args: self.args.unwrap_or_default(), - env: self.env, - }) - } -} - -impl From for BuiltinAgentServerSettings { - fn from(value: settings::BuiltinAgentServerSettings) -> Self { - BuiltinAgentServerSettings { - path: value - .path - .map(|p| PathBuf::from(shellexpand::tilde(&p.to_string_lossy()).as_ref())), - args: value.args, - env: value.env, - ignore_system_version: value.ignore_system_version, - default_mode: value.default_mode, - default_model: value.default_model, - } - } -} - -impl From for BuiltinAgentServerSettings { - fn from(value: AgentServerCommand) -> Self { - BuiltinAgentServerSettings { - path: Some(value.path), - args: Some(value.args), - env: value.env, - ..Default::default() - } - } -} - -#[derive(Clone, JsonSchema, Debug, PartialEq)] -pub enum CustomAgentServerSettings { - Custom { - command: AgentServerCommand, - /// The default mode to use for this agent. - /// - /// Note: Not only all agents support modes. - /// - /// Default: None - default_mode: Option, - /// The default model to use for this agent. - /// - /// This should be the model ID as reported by the agent. - /// - /// Default: None - default_model: Option, - }, - Extension { - /// The default mode to use for this agent. - /// - /// Note: Not only all agents support modes. - /// - /// Default: None - default_mode: Option, - /// The default model to use for this agent. - /// - /// This should be the model ID as reported by the agent. - /// - /// Default: None - default_model: Option, - }, -} - -impl CustomAgentServerSettings { - pub fn command(&self) -> Option<&AgentServerCommand> { - match self { - CustomAgentServerSettings::Custom { command, .. } => Some(command), - CustomAgentServerSettings::Extension { .. } => None, - } - } - - pub fn default_mode(&self) -> Option<&str> { - match self { - CustomAgentServerSettings::Custom { default_mode, .. } - | CustomAgentServerSettings::Extension { default_mode, .. } => default_mode.as_deref(), - } - } - - pub fn default_model(&self) -> Option<&str> { - match self { - CustomAgentServerSettings::Custom { default_model, .. } - | CustomAgentServerSettings::Extension { default_model, .. } => { - default_model.as_deref() - } - } - } -} - -impl From for CustomAgentServerSettings { - fn from(value: settings::CustomAgentServerSettings) -> Self { - match value { - settings::CustomAgentServerSettings::Custom { - path, - args, - env, - default_mode, - default_model, - } => CustomAgentServerSettings::Custom { - command: AgentServerCommand { - path: PathBuf::from(shellexpand::tilde(&path.to_string_lossy()).as_ref()), - args, - env, - }, - default_mode, - default_model, - }, - settings::CustomAgentServerSettings::Extension { - default_mode, - default_model, - } => CustomAgentServerSettings::Extension { - default_mode, - default_model, - }, - } - } -} - -impl settings::Settings for AllAgentServersSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - let agent_settings = content.agent_servers.clone().unwrap(); - Self { - gemini: agent_settings.gemini.map(Into::into), - claude: agent_settings.claude.map(Into::into), - codex: agent_settings.codex.map(Into::into), - custom: agent_settings - .custom - .into_iter() - .map(|(k, v)| (k, v.into())) - .collect(), - } - } -} - -#[cfg(test)] -mod extension_agent_tests { - use crate::worktree_store::WorktreeStore; - - use super::*; - use gpui::TestAppContext; - use std::sync::Arc; - - #[test] - fn extension_agent_constructs_proper_display_names() { - // Verify the display name format for extension-provided agents - let name1 = ExternalAgentServerName(SharedString::from("Extension: Agent")); - assert!(name1.0.contains(": ")); - - let name2 = ExternalAgentServerName(SharedString::from("MyExt: MyAgent")); - assert_eq!(name2.0, "MyExt: MyAgent"); - - // Non-extension agents shouldn't have the separator - let custom = ExternalAgentServerName(SharedString::from("custom")); - assert!(!custom.0.contains(": ")); - } - - struct NoopExternalAgent; - - impl ExternalAgentServer for NoopExternalAgent { - fn get_command( - &mut self, - _root_dir: Option<&str>, - _extra_env: HashMap, - _status_tx: Option>, - _new_version_available_tx: Option>>, - _cx: &mut AsyncApp, - ) -> Task)>> { - Task::ready(Ok(( - AgentServerCommand { - path: PathBuf::from("noop"), - args: Vec::new(), - env: None, - }, - "".to_string(), - None, - ))) - } - - fn as_any_mut(&mut self) -> &mut dyn Any { - self - } - } - - #[test] - fn sync_removes_only_extension_provided_agents() { - let mut store = AgentServerStore { - state: AgentServerStoreState::Collab, - external_agents: HashMap::default(), - agent_icons: HashMap::default(), - agent_display_names: HashMap::default(), - }; - - // Seed with extension agents (contain ": ") and custom agents (don't contain ": ") - store.external_agents.insert( - ExternalAgentServerName(SharedString::from("Ext1: Agent1")), - Box::new(NoopExternalAgent) as Box, - ); - store.external_agents.insert( - ExternalAgentServerName(SharedString::from("Ext2: Agent2")), - Box::new(NoopExternalAgent) as Box, - ); - store.external_agents.insert( - ExternalAgentServerName(SharedString::from("custom-agent")), - Box::new(NoopExternalAgent) as Box, - ); - - // Simulate removal phase - let keys_to_remove: Vec<_> = store - .external_agents - .keys() - .filter(|name| name.0.contains(": ")) - .cloned() - .collect(); - - for key in keys_to_remove { - store.external_agents.remove(&key); - } - - // Only custom-agent should remain - assert_eq!(store.external_agents.len(), 1); - assert!( - store - .external_agents - .contains_key(&ExternalAgentServerName(SharedString::from("custom-agent"))) - ); - } - - #[test] - fn archive_launcher_constructs_with_all_fields() { - use extension::AgentServerManifestEntry; - - let mut env = HashMap::default(); - env.insert("GITHUB_TOKEN".into(), "secret".into()); - - let mut targets = HashMap::default(); - targets.insert( - "darwin-aarch64".to_string(), - extension::TargetConfig { - archive: - "https://github.com/owner/repo/releases/download/v1.0.0/agent-darwin-arm64.zip" - .into(), - cmd: "./agent".into(), - args: vec![], - sha256: None, - env: Default::default(), - }, - ); - - let _entry = AgentServerManifestEntry { - name: "GitHub Agent".into(), - targets, - env, - icon: None, - }; - - // Verify display name construction - let expected_name = ExternalAgentServerName(SharedString::from("GitHub Agent")); - assert_eq!(expected_name.0, "GitHub Agent"); - } - - #[gpui::test] - async fn archive_agent_uses_extension_and_agent_id_for_cache_key(cx: &mut TestAppContext) { - let fs = fs::FakeFs::new(cx.background_executor.clone()); - let http_client = http_client::FakeHttpClient::with_404_response(); - let worktree_store = cx.new(|_| WorktreeStore::local(false, fs.clone())); - let project_environment = cx.new(|cx| { - crate::ProjectEnvironment::new(None, worktree_store.downgrade(), None, false, cx) - }); - - let agent = LocalExtensionArchiveAgent { - fs, - http_client, - node_runtime: node_runtime::NodeRuntime::unavailable(), - project_environment, - extension_id: Arc::from("my-extension"), - agent_id: Arc::from("my-agent"), - targets: { - let mut map = HashMap::default(); - map.insert( - "darwin-aarch64".to_string(), - extension::TargetConfig { - archive: "https://example.com/my-agent-darwin-arm64.zip".into(), - cmd: "./my-agent".into(), - args: vec!["--serve".into()], - sha256: None, - env: Default::default(), - }, - ); - map - }, - env: { - let mut map = HashMap::default(); - map.insert("PORT".into(), "8080".into()); - map - }, - }; - - // Verify agent is properly constructed - assert_eq!(agent.extension_id.as_ref(), "my-extension"); - assert_eq!(agent.agent_id.as_ref(), "my-agent"); - assert_eq!(agent.env.get("PORT"), Some(&"8080".to_string())); - assert!(agent.targets.contains_key("darwin-aarch64")); - } - - #[test] - fn sync_extension_agents_registers_archive_launcher() { - use extension::AgentServerManifestEntry; - - let expected_name = ExternalAgentServerName(SharedString::from("Release Agent")); - assert_eq!(expected_name.0, "Release Agent"); - - // Verify the manifest entry structure for archive-based installation - let mut env = HashMap::default(); - env.insert("API_KEY".into(), "secret".into()); - - let mut targets = HashMap::default(); - targets.insert( - "linux-x86_64".to_string(), - extension::TargetConfig { - archive: "https://github.com/org/project/releases/download/v2.1.0/release-agent-linux-x64.tar.gz".into(), - cmd: "./release-agent".into(), - args: vec!["serve".into()], - sha256: None, - env: Default::default(), - }, - ); - - let manifest_entry = AgentServerManifestEntry { - name: "Release Agent".into(), - targets: targets.clone(), - env, - icon: None, - }; - - // Verify target config is present - assert!(manifest_entry.targets.contains_key("linux-x86_64")); - let target = manifest_entry.targets.get("linux-x86_64").unwrap(); - assert_eq!(target.cmd, "./release-agent"); - } - - #[gpui::test] - async fn test_node_command_uses_managed_runtime(cx: &mut TestAppContext) { - let fs = fs::FakeFs::new(cx.background_executor.clone()); - let http_client = http_client::FakeHttpClient::with_404_response(); - let node_runtime = NodeRuntime::unavailable(); - let worktree_store = cx.new(|_| WorktreeStore::local(false, fs.clone())); - let project_environment = cx.new(|cx| { - crate::ProjectEnvironment::new(None, worktree_store.downgrade(), None, false, cx) - }); - - let agent = LocalExtensionArchiveAgent { - fs: fs.clone(), - http_client, - node_runtime, - project_environment, - extension_id: Arc::from("node-extension"), - agent_id: Arc::from("node-agent"), - targets: { - let mut map = HashMap::default(); - map.insert( - "darwin-aarch64".to_string(), - extension::TargetConfig { - archive: "https://example.com/node-agent.zip".into(), - cmd: "node".into(), - args: vec!["index.js".into()], - sha256: None, - env: Default::default(), - }, - ); - map - }, - env: HashMap::default(), - }; - - // Verify that when cmd is "node", it attempts to use the node runtime - assert_eq!(agent.extension_id.as_ref(), "node-extension"); - assert_eq!(agent.agent_id.as_ref(), "node-agent"); - - let target = agent.targets.get("darwin-aarch64").unwrap(); - assert_eq!(target.cmd, "node"); - assert_eq!(target.args, vec!["index.js"]); - } - - #[gpui::test] - async fn test_commands_run_in_extraction_directory(cx: &mut TestAppContext) { - let fs = fs::FakeFs::new(cx.background_executor.clone()); - let http_client = http_client::FakeHttpClient::with_404_response(); - let node_runtime = NodeRuntime::unavailable(); - let worktree_store = cx.new(|_| WorktreeStore::local(false, fs.clone())); - let project_environment = cx.new(|cx| { - crate::ProjectEnvironment::new(None, worktree_store.downgrade(), None, false, cx) - }); - - let agent = LocalExtensionArchiveAgent { - fs: fs.clone(), - http_client, - node_runtime, - project_environment, - extension_id: Arc::from("test-ext"), - agent_id: Arc::from("test-agent"), - targets: { - let mut map = HashMap::default(); - map.insert( - "darwin-aarch64".to_string(), - extension::TargetConfig { - archive: "https://example.com/test.zip".into(), - cmd: "node".into(), - args: vec![ - "server.js".into(), - "--config".into(), - "./config.json".into(), - ], - sha256: None, - env: Default::default(), - }, - ); - map - }, - env: HashMap::default(), - }; - - // Verify the agent is configured with relative paths in args - let target = agent.targets.get("darwin-aarch64").unwrap(); - assert_eq!(target.args[0], "server.js"); - assert_eq!(target.args[2], "./config.json"); - // These relative paths will resolve relative to the extraction directory - // when the command is executed - } - - #[test] - fn test_tilde_expansion_in_settings() { - let settings = settings::BuiltinAgentServerSettings { - path: Some(PathBuf::from("~/bin/agent")), - args: Some(vec!["--flag".into()]), - env: None, - ignore_system_version: None, - default_mode: None, - default_model: None, - }; - - let BuiltinAgentServerSettings { path, .. } = settings.into(); - - let path = path.unwrap(); - assert!( - !path.to_string_lossy().starts_with("~"), - "Tilde should be expanded for builtin agent path" - ); - - let settings = settings::CustomAgentServerSettings::Custom { - path: PathBuf::from("~/custom/agent"), - args: vec!["serve".into()], - env: None, - default_mode: None, - default_model: None, - }; - - let converted: CustomAgentServerSettings = settings.into(); - let CustomAgentServerSettings::Custom { - command: AgentServerCommand { path, .. }, - .. - } = converted - else { - panic!("Expected Custom variant"); - }; - - assert!( - !path.to_string_lossy().starts_with("~"), - "Tilde should be expanded for custom agent path" - ); - } -} diff --git a/crates/project/src/buffer_store.rs b/crates/project/src/buffer_store.rs deleted file mode 100644 index aea2482c83..0000000000 --- a/crates/project/src/buffer_store.rs +++ /dev/null @@ -1,1686 +0,0 @@ -use crate::{ - ProjectPath, - lsp_store::OpenLspBufferHandle, - worktree_store::{WorktreeStore, WorktreeStoreEvent}, -}; -use anyhow::{Context as _, Result, anyhow}; -use client::Client; -use collections::{HashMap, HashSet, hash_map}; -use futures::{Future, FutureExt as _, channel::oneshot, future::Shared}; -use gpui::{ - App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity, -}; -use language::{ - Buffer, BufferEvent, Capability, DiskState, File as _, Language, Operation, - proto::{ - deserialize_line_ending, deserialize_version, serialize_line_ending, serialize_version, - split_operations, - }, -}; -use rpc::{ - AnyProtoClient, ErrorCode, ErrorExt as _, TypedEnvelope, - proto::{self}, -}; - -use std::{io, sync::Arc, time::Instant}; -use text::{BufferId, ReplicaId}; -use util::{ResultExt as _, TryFutureExt, debug_panic, maybe, rel_path::RelPath}; -use worktree::{File, PathChange, ProjectEntryId, Worktree, WorktreeId}; - -/// A set of open buffers. -pub struct BufferStore { - state: BufferStoreState, - #[allow(clippy::type_complexity)] - loading_buffers: HashMap, Arc>>>>, - worktree_store: Entity, - opened_buffers: HashMap, - path_to_buffer_id: HashMap, - downstream_client: Option<(AnyProtoClient, u64)>, - shared_buffers: HashMap>, - non_searchable_buffers: HashSet, -} - -#[derive(Hash, Eq, PartialEq, Clone)] -struct SharedBuffer { - buffer: Entity, - lsp_handle: Option, -} - -enum BufferStoreState { - Local(LocalBufferStore), - Remote(RemoteBufferStore), -} - -struct RemoteBufferStore { - shared_with_me: HashSet>, - upstream_client: AnyProtoClient, - project_id: u64, - loading_remote_buffers_by_id: HashMap>, - remote_buffer_listeners: - HashMap>>>>, - worktree_store: Entity, -} - -struct LocalBufferStore { - local_buffer_ids_by_entry_id: HashMap, - worktree_store: Entity, - _subscription: Subscription, -} - -enum OpenBuffer { - Complete { buffer: WeakEntity }, - Operations(Vec), -} - -pub enum BufferStoreEvent { - BufferAdded(Entity), - BufferOpened { - buffer: Entity, - project_path: ProjectPath, - }, - SharedBufferClosed(proto::PeerId, BufferId), - BufferDropped(BufferId), - BufferChangedFilePath { - buffer: Entity, - old_file: Option>, - }, -} - -#[derive(Default, Debug, Clone)] -pub struct ProjectTransaction(pub HashMap, language::Transaction>); - -impl PartialEq for ProjectTransaction { - fn eq(&self, other: &Self) -> bool { - self.0.len() == other.0.len() - && self.0.iter().all(|(buffer, transaction)| { - other.0.get(buffer).is_some_and(|t| t.id == transaction.id) - }) - } -} - -impl EventEmitter for BufferStore {} - -impl RemoteBufferStore { - pub fn wait_for_remote_buffer( - &mut self, - id: BufferId, - cx: &mut Context, - ) -> Task>> { - let (tx, rx) = oneshot::channel(); - self.remote_buffer_listeners.entry(id).or_default().push(tx); - - cx.spawn(async move |this, cx| { - if let Some(buffer) = this - .read_with(cx, |buffer_store, _| buffer_store.get(id)) - .ok() - .flatten() - { - return Ok(buffer); - } - - cx.background_spawn(async move { rx.await? }).await - }) - } - - fn save_remote_buffer( - &self, - buffer_handle: Entity, - new_path: Option, - cx: &Context, - ) -> Task> { - let buffer = buffer_handle.read(cx); - let buffer_id = buffer.remote_id().into(); - let version = buffer.version(); - let rpc = self.upstream_client.clone(); - let project_id = self.project_id; - cx.spawn(async move |_, cx| { - let response = rpc - .request(proto::SaveBuffer { - project_id, - buffer_id, - new_path, - version: serialize_version(&version), - }) - .await?; - let version = deserialize_version(&response.version); - let mtime = response.mtime.map(|mtime| mtime.into()); - - buffer_handle.update(cx, |buffer, cx| { - buffer.did_save(version.clone(), mtime, cx); - })?; - - Ok(()) - }) - } - - pub fn handle_create_buffer_for_peer( - &mut self, - envelope: TypedEnvelope, - replica_id: ReplicaId, - capability: Capability, - cx: &mut Context, - ) -> Result>> { - match envelope.payload.variant.context("missing variant")? { - proto::create_buffer_for_peer::Variant::State(mut state) => { - let buffer_id = BufferId::new(state.id)?; - - let buffer_result = maybe!({ - let mut buffer_file = None; - if let Some(file) = state.file.take() { - let worktree_id = worktree::WorktreeId::from_proto(file.worktree_id); - let worktree = self - .worktree_store - .read(cx) - .worktree_for_id(worktree_id, cx) - .with_context(|| { - format!("no worktree found for id {}", file.worktree_id) - })?; - buffer_file = Some(Arc::new(File::from_proto(file, worktree, cx)?) - as Arc); - } - Buffer::from_proto(replica_id, capability, state, buffer_file) - }); - - match buffer_result { - Ok(buffer) => { - let buffer = cx.new(|_| buffer); - self.loading_remote_buffers_by_id.insert(buffer_id, buffer); - } - Err(error) => { - if let Some(listeners) = self.remote_buffer_listeners.remove(&buffer_id) { - for listener in listeners { - listener.send(Err(anyhow!(error.cloned()))).ok(); - } - } - } - } - } - proto::create_buffer_for_peer::Variant::Chunk(chunk) => { - let buffer_id = BufferId::new(chunk.buffer_id)?; - let buffer = self - .loading_remote_buffers_by_id - .get(&buffer_id) - .cloned() - .with_context(|| { - format!( - "received chunk for buffer {} without initial state", - chunk.buffer_id - ) - })?; - - let result = maybe!({ - let operations = chunk - .operations - .into_iter() - .map(language::proto::deserialize_operation) - .collect::>>()?; - buffer.update(cx, |buffer, cx| buffer.apply_ops(operations, cx)); - anyhow::Ok(()) - }); - - if let Err(error) = result { - self.loading_remote_buffers_by_id.remove(&buffer_id); - if let Some(listeners) = self.remote_buffer_listeners.remove(&buffer_id) { - for listener in listeners { - listener.send(Err(error.cloned())).ok(); - } - } - } else if chunk.is_last { - self.loading_remote_buffers_by_id.remove(&buffer_id); - if self.upstream_client.is_via_collab() { - // retain buffers sent by peers to avoid races. - self.shared_with_me.insert(buffer.clone()); - } - - if let Some(senders) = self.remote_buffer_listeners.remove(&buffer_id) { - for sender in senders { - sender.send(Ok(buffer.clone())).ok(); - } - } - return Ok(Some(buffer)); - } - } - } - Ok(None) - } - - pub fn incomplete_buffer_ids(&self) -> Vec { - self.loading_remote_buffers_by_id - .keys() - .copied() - .collect::>() - } - - pub fn deserialize_project_transaction( - &self, - message: proto::ProjectTransaction, - push_to_history: bool, - cx: &mut Context, - ) -> Task> { - cx.spawn(async move |this, cx| { - let mut project_transaction = ProjectTransaction::default(); - for (buffer_id, transaction) in message.buffer_ids.into_iter().zip(message.transactions) - { - let buffer_id = BufferId::new(buffer_id)?; - let buffer = this - .update(cx, |this, cx| this.wait_for_remote_buffer(buffer_id, cx))? - .await?; - let transaction = language::proto::deserialize_transaction(transaction)?; - project_transaction.0.insert(buffer, transaction); - } - - for (buffer, transaction) in &project_transaction.0 { - buffer - .update(cx, |buffer, _| { - buffer.wait_for_edits(transaction.edit_ids.iter().copied()) - })? - .await?; - - if push_to_history { - buffer.update(cx, |buffer, _| { - buffer.push_transaction(transaction.clone(), Instant::now()); - buffer.finalize_last_transaction(); - })?; - } - } - - Ok(project_transaction) - }) - } - - fn open_buffer( - &self, - path: Arc, - worktree: Entity, - cx: &mut Context, - ) -> Task>> { - let worktree_id = worktree.read(cx).id().to_proto(); - let project_id = self.project_id; - let client = self.upstream_client.clone(); - cx.spawn(async move |this, cx| { - let response = client - .request(proto::OpenBufferByPath { - project_id, - worktree_id, - path: path.to_proto(), - }) - .await?; - let buffer_id = BufferId::new(response.buffer_id)?; - - let buffer = this - .update(cx, { - |this, cx| this.wait_for_remote_buffer(buffer_id, cx) - })? - .await?; - - Ok(buffer) - }) - } - - fn create_buffer( - &self, - project_searchable: bool, - cx: &mut Context, - ) -> Task>> { - let create = self.upstream_client.request(proto::OpenNewBuffer { - project_id: self.project_id, - }); - cx.spawn(async move |this, cx| { - let response = create.await?; - let buffer_id = BufferId::new(response.buffer_id)?; - - this.update(cx, |this, cx| { - if !project_searchable { - this.non_searchable_buffers.insert(buffer_id); - } - this.wait_for_remote_buffer(buffer_id, cx) - })? - .await - }) - } - - fn reload_buffers( - &self, - buffers: HashSet>, - push_to_history: bool, - cx: &mut Context, - ) -> Task> { - let request = self.upstream_client.request(proto::ReloadBuffers { - project_id: self.project_id, - buffer_ids: buffers - .iter() - .map(|buffer| buffer.read(cx).remote_id().to_proto()) - .collect(), - }); - - cx.spawn(async move |this, cx| { - let response = request.await?.transaction.context("missing transaction")?; - this.update(cx, |this, cx| { - this.deserialize_project_transaction(response, push_to_history, cx) - })? - .await - }) - } -} - -impl LocalBufferStore { - fn save_local_buffer( - &self, - buffer_handle: Entity, - worktree: Entity, - path: Arc, - mut has_changed_file: bool, - cx: &mut Context, - ) -> Task> { - let buffer = buffer_handle.read(cx); - - let text = buffer.as_rope().clone(); - let line_ending = buffer.line_ending(); - let version = buffer.version(); - let buffer_id = buffer.remote_id(); - let file = buffer.file().cloned(); - if file - .as_ref() - .is_some_and(|file| file.disk_state() == DiskState::New) - { - has_changed_file = true; - } - - let save = worktree.update(cx, |worktree, cx| { - worktree.write_file(path, text, line_ending, cx) - }); - - cx.spawn(async move |this, cx| { - let new_file = save.await?; - let mtime = new_file.disk_state().mtime(); - this.update(cx, |this, cx| { - if let Some((downstream_client, project_id)) = this.downstream_client.clone() { - if has_changed_file { - downstream_client - .send(proto::UpdateBufferFile { - project_id, - buffer_id: buffer_id.to_proto(), - file: Some(language::File::to_proto(&*new_file, cx)), - }) - .log_err(); - } - downstream_client - .send(proto::BufferSaved { - project_id, - buffer_id: buffer_id.to_proto(), - version: serialize_version(&version), - mtime: mtime.map(|time| time.into()), - }) - .log_err(); - } - })?; - buffer_handle.update(cx, |buffer, cx| { - if has_changed_file { - buffer.file_updated(new_file, cx); - } - buffer.did_save(version.clone(), mtime, cx); - }) - }) - } - - fn subscribe_to_worktree( - &mut self, - worktree: &Entity, - cx: &mut Context, - ) { - cx.subscribe(worktree, |this, worktree, event, cx| { - if worktree.read(cx).is_local() - && let worktree::Event::UpdatedEntries(changes) = event - { - Self::local_worktree_entries_changed(this, &worktree, changes, cx); - } - }) - .detach(); - } - - fn local_worktree_entries_changed( - this: &mut BufferStore, - worktree_handle: &Entity, - changes: &[(Arc, ProjectEntryId, PathChange)], - cx: &mut Context, - ) { - let snapshot = worktree_handle.read(cx).snapshot(); - for (path, entry_id, _) in changes { - Self::local_worktree_entry_changed( - this, - *entry_id, - path, - worktree_handle, - &snapshot, - cx, - ); - } - } - - fn local_worktree_entry_changed( - this: &mut BufferStore, - entry_id: ProjectEntryId, - path: &Arc, - worktree: &Entity, - snapshot: &worktree::Snapshot, - cx: &mut Context, - ) -> Option<()> { - let project_path = ProjectPath { - worktree_id: snapshot.id(), - path: path.clone(), - }; - - let buffer_id = this - .as_local_mut() - .and_then(|local| local.local_buffer_ids_by_entry_id.get(&entry_id)) - .copied() - .or_else(|| this.path_to_buffer_id.get(&project_path).copied())?; - - let buffer = if let Some(buffer) = this.get(buffer_id) { - Some(buffer) - } else { - this.opened_buffers.remove(&buffer_id); - this.non_searchable_buffers.remove(&buffer_id); - None - }; - - let buffer = if let Some(buffer) = buffer { - buffer - } else { - this.path_to_buffer_id.remove(&project_path); - let this = this.as_local_mut()?; - this.local_buffer_ids_by_entry_id.remove(&entry_id); - return None; - }; - - let events = buffer.update(cx, |buffer, cx| { - let file = buffer.file()?; - let old_file = File::from_dyn(Some(file))?; - if old_file.worktree != *worktree { - return None; - } - - let snapshot_entry = old_file - .entry_id - .and_then(|entry_id| snapshot.entry_for_id(entry_id)) - .or_else(|| snapshot.entry_for_path(old_file.path.as_ref())); - - let new_file = if let Some(entry) = snapshot_entry { - File { - disk_state: match entry.mtime { - Some(mtime) => DiskState::Present { mtime }, - None => old_file.disk_state, - }, - is_local: true, - entry_id: Some(entry.id), - path: entry.path.clone(), - worktree: worktree.clone(), - is_private: entry.is_private, - } - } else { - File { - disk_state: DiskState::Deleted, - is_local: true, - entry_id: old_file.entry_id, - path: old_file.path.clone(), - worktree: worktree.clone(), - is_private: old_file.is_private, - } - }; - - if new_file == *old_file { - return None; - } - - let mut events = Vec::new(); - if new_file.path != old_file.path { - this.path_to_buffer_id.remove(&ProjectPath { - path: old_file.path.clone(), - worktree_id: old_file.worktree_id(cx), - }); - this.path_to_buffer_id.insert( - ProjectPath { - worktree_id: new_file.worktree_id(cx), - path: new_file.path.clone(), - }, - buffer_id, - ); - events.push(BufferStoreEvent::BufferChangedFilePath { - buffer: cx.entity(), - old_file: buffer.file().cloned(), - }); - } - let local = this.as_local_mut()?; - if new_file.entry_id != old_file.entry_id { - if let Some(entry_id) = old_file.entry_id { - local.local_buffer_ids_by_entry_id.remove(&entry_id); - } - if let Some(entry_id) = new_file.entry_id { - local - .local_buffer_ids_by_entry_id - .insert(entry_id, buffer_id); - } - } - - if let Some((client, project_id)) = &this.downstream_client { - client - .send(proto::UpdateBufferFile { - project_id: *project_id, - buffer_id: buffer_id.to_proto(), - file: Some(new_file.to_proto(cx)), - }) - .ok(); - } - - buffer.file_updated(Arc::new(new_file), cx); - Some(events) - })?; - - for event in events { - cx.emit(event); - } - - None - } - - fn save_buffer( - &self, - buffer: Entity, - cx: &mut Context, - ) -> Task> { - let Some(file) = File::from_dyn(buffer.read(cx).file()) else { - return Task::ready(Err(anyhow!("buffer doesn't have a file"))); - }; - let worktree = file.worktree.clone(); - self.save_local_buffer(buffer, worktree, file.path.clone(), false, cx) - } - - fn save_buffer_as( - &self, - buffer: Entity, - path: ProjectPath, - cx: &mut Context, - ) -> Task> { - let Some(worktree) = self - .worktree_store - .read(cx) - .worktree_for_id(path.worktree_id, cx) - else { - return Task::ready(Err(anyhow!("no such worktree"))); - }; - self.save_local_buffer(buffer, worktree, path.path, true, cx) - } - - fn open_buffer( - &self, - path: Arc, - worktree: Entity, - cx: &mut Context, - ) -> Task>> { - let load_file = worktree.update(cx, |worktree, cx| worktree.load_file(path.as_ref(), cx)); - cx.spawn(async move |this, cx| { - let path = path.clone(); - let buffer = match load_file.await { - Ok(loaded) => { - let reservation = cx.reserve_entity::()?; - let buffer_id = BufferId::from(reservation.entity_id().as_non_zero_u64()); - let text_buffer = cx - .background_spawn(async move { - text::Buffer::new(ReplicaId::LOCAL, buffer_id, loaded.text) - }) - .await; - cx.insert_entity(reservation, |_| { - Buffer::build(text_buffer, Some(loaded.file), Capability::ReadWrite) - })? - } - Err(error) if is_not_found_error(&error) => cx.new(|cx| { - let buffer_id = BufferId::from(cx.entity_id().as_non_zero_u64()); - let text_buffer = text::Buffer::new(ReplicaId::LOCAL, buffer_id, ""); - Buffer::build( - text_buffer, - Some(Arc::new(File { - worktree, - path, - disk_state: DiskState::New, - entry_id: None, - is_local: true, - is_private: false, - })), - Capability::ReadWrite, - ) - })?, - Err(e) => return Err(e), - }; - this.update(cx, |this, cx| { - this.add_buffer(buffer.clone(), cx)?; - let buffer_id = buffer.read(cx).remote_id(); - if let Some(file) = File::from_dyn(buffer.read(cx).file()) { - this.path_to_buffer_id.insert( - ProjectPath { - worktree_id: file.worktree_id(cx), - path: file.path.clone(), - }, - buffer_id, - ); - let this = this.as_local_mut().unwrap(); - if let Some(entry_id) = file.entry_id { - this.local_buffer_ids_by_entry_id - .insert(entry_id, buffer_id); - } - } - - anyhow::Ok(()) - })??; - - Ok(buffer) - }) - } - - fn create_buffer( - &self, - project_searchable: bool, - cx: &mut Context, - ) -> Task>> { - cx.spawn(async move |buffer_store, cx| { - let buffer = - cx.new(|cx| Buffer::local("", cx).with_language(language::PLAIN_TEXT.clone(), cx))?; - buffer_store.update(cx, |buffer_store, cx| { - buffer_store.add_buffer(buffer.clone(), cx).log_err(); - if !project_searchable { - buffer_store - .non_searchable_buffers - .insert(buffer.read(cx).remote_id()); - } - })?; - Ok(buffer) - }) - } - - fn reload_buffers( - &self, - buffers: HashSet>, - push_to_history: bool, - cx: &mut Context, - ) -> Task> { - cx.spawn(async move |_, cx| { - let mut project_transaction = ProjectTransaction::default(); - for buffer in buffers { - let transaction = buffer.update(cx, |buffer, cx| buffer.reload(cx))?.await?; - buffer.update(cx, |buffer, cx| { - if let Some(transaction) = transaction { - if !push_to_history { - buffer.forget_transaction(transaction.id); - } - project_transaction.0.insert(cx.entity(), transaction); - } - })?; - } - - Ok(project_transaction) - }) - } -} - -impl BufferStore { - pub fn init(client: &AnyProtoClient) { - client.add_entity_message_handler(Self::handle_buffer_reloaded); - client.add_entity_message_handler(Self::handle_buffer_saved); - client.add_entity_message_handler(Self::handle_update_buffer_file); - client.add_entity_request_handler(Self::handle_save_buffer); - client.add_entity_request_handler(Self::handle_reload_buffers); - } - - /// Creates a buffer store, optionally retaining its buffers. - pub fn local(worktree_store: Entity, cx: &mut Context) -> Self { - Self { - state: BufferStoreState::Local(LocalBufferStore { - local_buffer_ids_by_entry_id: Default::default(), - worktree_store: worktree_store.clone(), - _subscription: cx.subscribe(&worktree_store, |this, _, event, cx| { - if let WorktreeStoreEvent::WorktreeAdded(worktree) = event { - let this = this.as_local_mut().unwrap(); - this.subscribe_to_worktree(worktree, cx); - } - }), - }), - downstream_client: None, - opened_buffers: Default::default(), - path_to_buffer_id: Default::default(), - shared_buffers: Default::default(), - loading_buffers: Default::default(), - non_searchable_buffers: Default::default(), - worktree_store, - } - } - - pub fn remote( - worktree_store: Entity, - upstream_client: AnyProtoClient, - remote_id: u64, - _cx: &mut Context, - ) -> Self { - Self { - state: BufferStoreState::Remote(RemoteBufferStore { - shared_with_me: Default::default(), - loading_remote_buffers_by_id: Default::default(), - remote_buffer_listeners: Default::default(), - project_id: remote_id, - upstream_client, - worktree_store: worktree_store.clone(), - }), - downstream_client: None, - opened_buffers: Default::default(), - path_to_buffer_id: Default::default(), - loading_buffers: Default::default(), - shared_buffers: Default::default(), - non_searchable_buffers: Default::default(), - worktree_store, - } - } - - fn as_local_mut(&mut self) -> Option<&mut LocalBufferStore> { - match &mut self.state { - BufferStoreState::Local(state) => Some(state), - _ => None, - } - } - - fn as_remote_mut(&mut self) -> Option<&mut RemoteBufferStore> { - match &mut self.state { - BufferStoreState::Remote(state) => Some(state), - _ => None, - } - } - - fn as_remote(&self) -> Option<&RemoteBufferStore> { - match &self.state { - BufferStoreState::Remote(state) => Some(state), - _ => None, - } - } - - pub fn open_buffer( - &mut self, - project_path: ProjectPath, - cx: &mut Context, - ) -> Task>> { - if let Some(buffer) = self.get_by_path(&project_path) { - cx.emit(BufferStoreEvent::BufferOpened { - buffer: buffer.clone(), - project_path, - }); - - return Task::ready(Ok(buffer)); - } - - let task = match self.loading_buffers.entry(project_path.clone()) { - hash_map::Entry::Occupied(e) => e.get().clone(), - hash_map::Entry::Vacant(entry) => { - let path = project_path.path.clone(); - let Some(worktree) = self - .worktree_store - .read(cx) - .worktree_for_id(project_path.worktree_id, cx) - else { - return Task::ready(Err(anyhow!("no such worktree"))); - }; - let load_buffer = match &self.state { - BufferStoreState::Local(this) => this.open_buffer(path, worktree, cx), - BufferStoreState::Remote(this) => this.open_buffer(path, worktree, cx), - }; - - entry - .insert( - // todo(lw): hot foreground spawn - cx.spawn(async move |this, cx| { - let load_result = load_buffer.await; - this.update(cx, |this, cx| { - // Record the fact that the buffer is no longer loading. - this.loading_buffers.remove(&project_path); - - let buffer = load_result.map_err(Arc::new)?; - cx.emit(BufferStoreEvent::BufferOpened { - buffer: buffer.clone(), - project_path, - }); - - Ok(buffer) - })? - }) - .shared(), - ) - .clone() - } - }; - - cx.background_spawn(async move { - task.await.map_err(|e| { - if e.error_code() != ErrorCode::Internal { - anyhow!(e.error_code()) - } else { - anyhow!("{e}") - } - }) - }) - } - - pub fn create_buffer( - &mut self, - project_searchable: bool, - cx: &mut Context, - ) -> Task>> { - match &self.state { - BufferStoreState::Local(this) => this.create_buffer(project_searchable, cx), - BufferStoreState::Remote(this) => this.create_buffer(project_searchable, cx), - } - } - - pub fn save_buffer( - &mut self, - buffer: Entity, - cx: &mut Context, - ) -> Task> { - match &mut self.state { - BufferStoreState::Local(this) => this.save_buffer(buffer, cx), - BufferStoreState::Remote(this) => this.save_remote_buffer(buffer, None, cx), - } - } - - pub fn save_buffer_as( - &mut self, - buffer: Entity, - path: ProjectPath, - cx: &mut Context, - ) -> Task> { - let old_file = buffer.read(cx).file().cloned(); - let task = match &self.state { - BufferStoreState::Local(this) => this.save_buffer_as(buffer.clone(), path, cx), - BufferStoreState::Remote(this) => { - this.save_remote_buffer(buffer.clone(), Some(path.to_proto()), cx) - } - }; - cx.spawn(async move |this, cx| { - task.await?; - this.update(cx, |this, cx| { - old_file.clone().and_then(|file| { - this.path_to_buffer_id.remove(&ProjectPath { - worktree_id: file.worktree_id(cx), - path: file.path().clone(), - }) - }); - - cx.emit(BufferStoreEvent::BufferChangedFilePath { buffer, old_file }); - }) - }) - } - - fn add_buffer(&mut self, buffer_entity: Entity, cx: &mut Context) -> Result<()> { - let buffer = buffer_entity.read(cx); - let remote_id = buffer.remote_id(); - let path = File::from_dyn(buffer.file()).map(|file| ProjectPath { - path: file.path.clone(), - worktree_id: file.worktree_id(cx), - }); - let is_remote = buffer.replica_id().is_remote(); - let open_buffer = OpenBuffer::Complete { - buffer: buffer_entity.downgrade(), - }; - - let handle = cx.entity().downgrade(); - buffer_entity.update(cx, move |_, cx| { - cx.on_release(move |buffer, cx| { - handle - .update(cx, |_, cx| { - cx.emit(BufferStoreEvent::BufferDropped(buffer.remote_id())) - }) - .ok(); - }) - .detach() - }); - let _expect_path_to_exist; - match self.opened_buffers.entry(remote_id) { - hash_map::Entry::Vacant(entry) => { - entry.insert(open_buffer); - _expect_path_to_exist = false; - } - hash_map::Entry::Occupied(mut entry) => { - if let OpenBuffer::Operations(operations) = entry.get_mut() { - buffer_entity.update(cx, |b, cx| b.apply_ops(operations.drain(..), cx)); - } else if entry.get().upgrade().is_some() { - if is_remote { - return Ok(()); - } else { - debug_panic!("buffer {remote_id} was already registered"); - anyhow::bail!("buffer {remote_id} was already registered"); - } - } - entry.insert(open_buffer); - _expect_path_to_exist = true; - } - } - - if let Some(path) = path { - self.path_to_buffer_id.insert(path, remote_id); - } - - cx.subscribe(&buffer_entity, Self::on_buffer_event).detach(); - cx.emit(BufferStoreEvent::BufferAdded(buffer_entity)); - Ok(()) - } - - pub fn buffers(&self) -> impl '_ + Iterator> { - self.opened_buffers - .values() - .filter_map(|buffer| buffer.upgrade()) - } - - pub(crate) fn is_searchable(&self, id: &BufferId) -> bool { - !self.non_searchable_buffers.contains(&id) - } - - pub fn loading_buffers( - &self, - ) -> impl Iterator>>)> { - self.loading_buffers.iter().map(|(path, task)| { - let task = task.clone(); - (path, async move { - task.await.map_err(|e| { - if e.error_code() != ErrorCode::Internal { - anyhow!(e.error_code()) - } else { - anyhow!("{e}") - } - }) - }) - }) - } - - pub fn buffer_id_for_project_path(&self, project_path: &ProjectPath) -> Option<&BufferId> { - self.path_to_buffer_id.get(project_path) - } - - pub fn get_by_path(&self, path: &ProjectPath) -> Option> { - self.path_to_buffer_id - .get(path) - .and_then(|buffer_id| self.get(*buffer_id)) - } - - pub fn get(&self, buffer_id: BufferId) -> Option> { - self.opened_buffers.get(&buffer_id)?.upgrade() - } - - pub fn get_existing(&self, buffer_id: BufferId) -> Result> { - self.get(buffer_id) - .with_context(|| format!("unknown buffer id {buffer_id}")) - } - - pub fn get_possibly_incomplete(&self, buffer_id: BufferId) -> Option> { - self.get(buffer_id).or_else(|| { - self.as_remote() - .and_then(|remote| remote.loading_remote_buffers_by_id.get(&buffer_id).cloned()) - }) - } - - pub fn buffer_version_info(&self, cx: &App) -> (Vec, Vec) { - let buffers = self - .buffers() - .map(|buffer| { - let buffer = buffer.read(cx); - proto::BufferVersion { - id: buffer.remote_id().into(), - version: language::proto::serialize_version(&buffer.version), - } - }) - .collect(); - let incomplete_buffer_ids = self - .as_remote() - .map(|remote| remote.incomplete_buffer_ids()) - .unwrap_or_default(); - (buffers, incomplete_buffer_ids) - } - - pub fn disconnected_from_host(&mut self, cx: &mut App) { - for open_buffer in self.opened_buffers.values_mut() { - if let Some(buffer) = open_buffer.upgrade() { - buffer.update(cx, |buffer, _| buffer.give_up_waiting()); - } - } - - for buffer in self.buffers() { - buffer.update(cx, |buffer, cx| { - buffer.set_capability(Capability::ReadOnly, cx) - }); - } - - if let Some(remote) = self.as_remote_mut() { - // Wake up all futures currently waiting on a buffer to get opened, - // to give them a chance to fail now that we've disconnected. - remote.remote_buffer_listeners.clear() - } - } - - pub fn shared(&mut self, remote_id: u64, downstream_client: AnyProtoClient, _cx: &mut App) { - self.downstream_client = Some((downstream_client, remote_id)); - } - - pub fn unshared(&mut self, _cx: &mut Context) { - self.downstream_client.take(); - self.forget_shared_buffers(); - } - - pub fn discard_incomplete(&mut self) { - self.opened_buffers - .retain(|_, buffer| !matches!(buffer, OpenBuffer::Operations(_))); - } - - fn buffer_changed_file(&mut self, buffer: Entity, cx: &mut App) -> Option<()> { - let file = File::from_dyn(buffer.read(cx).file())?; - - let remote_id = buffer.read(cx).remote_id(); - if let Some(entry_id) = file.entry_id { - if let Some(local) = self.as_local_mut() { - match local.local_buffer_ids_by_entry_id.get(&entry_id) { - Some(_) => { - return None; - } - None => { - local - .local_buffer_ids_by_entry_id - .insert(entry_id, remote_id); - } - } - } - self.path_to_buffer_id.insert( - ProjectPath { - worktree_id: file.worktree_id(cx), - path: file.path.clone(), - }, - remote_id, - ); - }; - - Some(()) - } - - fn on_buffer_event( - &mut self, - buffer: Entity, - event: &BufferEvent, - cx: &mut Context, - ) { - match event { - BufferEvent::FileHandleChanged => { - self.buffer_changed_file(buffer, cx); - } - BufferEvent::Reloaded => { - let Some((downstream_client, project_id)) = self.downstream_client.as_ref() else { - return; - }; - let buffer = buffer.read(cx); - downstream_client - .send(proto::BufferReloaded { - project_id: *project_id, - buffer_id: buffer.remote_id().to_proto(), - version: serialize_version(&buffer.version()), - mtime: buffer.saved_mtime().map(|t| t.into()), - line_ending: serialize_line_ending(buffer.line_ending()) as i32, - }) - .log_err(); - } - BufferEvent::LanguageChanged(_) => {} - _ => {} - } - } - - pub async fn handle_update_buffer( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let payload = envelope.payload; - let buffer_id = BufferId::new(payload.buffer_id)?; - let ops = payload - .operations - .into_iter() - .map(language::proto::deserialize_operation) - .collect::, _>>()?; - this.update(&mut cx, |this, cx| { - match this.opened_buffers.entry(buffer_id) { - hash_map::Entry::Occupied(mut e) => match e.get_mut() { - OpenBuffer::Operations(operations) => operations.extend_from_slice(&ops), - OpenBuffer::Complete { buffer, .. } => { - if let Some(buffer) = buffer.upgrade() { - buffer.update(cx, |buffer, cx| buffer.apply_ops(ops, cx)); - } - } - }, - hash_map::Entry::Vacant(e) => { - e.insert(OpenBuffer::Operations(ops)); - } - } - Ok(proto::Ack {}) - })? - } - - pub fn register_shared_lsp_handle( - &mut self, - peer_id: proto::PeerId, - buffer_id: BufferId, - handle: OpenLspBufferHandle, - ) { - if let Some(shared_buffers) = self.shared_buffers.get_mut(&peer_id) - && let Some(buffer) = shared_buffers.get_mut(&buffer_id) - { - buffer.lsp_handle = Some(handle); - return; - } - debug_panic!("tried to register shared lsp handle, but buffer was not shared") - } - - pub fn handle_synchronize_buffers( - &mut self, - envelope: TypedEnvelope, - cx: &mut Context, - client: Arc, - ) -> Result { - let project_id = envelope.payload.project_id; - let mut response = proto::SynchronizeBuffersResponse { - buffers: Default::default(), - }; - let Some(guest_id) = envelope.original_sender_id else { - anyhow::bail!("missing original_sender_id on SynchronizeBuffers request"); - }; - - self.shared_buffers.entry(guest_id).or_default().clear(); - for buffer in envelope.payload.buffers { - let buffer_id = BufferId::new(buffer.id)?; - let remote_version = language::proto::deserialize_version(&buffer.version); - if let Some(buffer) = self.get(buffer_id) { - self.shared_buffers - .entry(guest_id) - .or_default() - .entry(buffer_id) - .or_insert_with(|| SharedBuffer { - buffer: buffer.clone(), - lsp_handle: None, - }); - - let buffer = buffer.read(cx); - response.buffers.push(proto::BufferVersion { - id: buffer_id.into(), - version: language::proto::serialize_version(&buffer.version), - }); - - let operations = buffer.serialize_ops(Some(remote_version), cx); - let client = client.clone(); - if let Some(file) = buffer.file() { - client - .send(proto::UpdateBufferFile { - project_id, - buffer_id: buffer_id.into(), - file: Some(file.to_proto(cx)), - }) - .log_err(); - } - - // TODO(max): do something - // client - // .send(proto::UpdateStagedText { - // project_id, - // buffer_id: buffer_id.into(), - // diff_base: buffer.diff_base().map(ToString::to_string), - // }) - // .log_err(); - - client - .send(proto::BufferReloaded { - project_id, - buffer_id: buffer_id.into(), - version: language::proto::serialize_version(buffer.saved_version()), - mtime: buffer.saved_mtime().map(|time| time.into()), - line_ending: language::proto::serialize_line_ending(buffer.line_ending()) - as i32, - }) - .log_err(); - - cx.background_spawn( - async move { - let operations = operations.await; - for chunk in split_operations(operations) { - client - .request(proto::UpdateBuffer { - project_id, - buffer_id: buffer_id.into(), - operations: chunk, - }) - .await?; - } - anyhow::Ok(()) - } - .log_err(), - ) - .detach(); - } - } - Ok(response) - } - - pub fn handle_create_buffer_for_peer( - &mut self, - envelope: TypedEnvelope, - replica_id: ReplicaId, - capability: Capability, - cx: &mut Context, - ) -> Result<()> { - let remote = self - .as_remote_mut() - .context("buffer store is not a remote")?; - - if let Some(buffer) = - remote.handle_create_buffer_for_peer(envelope, replica_id, capability, cx)? - { - self.add_buffer(buffer, cx)?; - } - - Ok(()) - } - - pub async fn handle_update_buffer_file( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - let buffer_id = envelope.payload.buffer_id; - let buffer_id = BufferId::new(buffer_id)?; - - this.update(&mut cx, |this, cx| { - let payload = envelope.payload.clone(); - if let Some(buffer) = this.get_possibly_incomplete(buffer_id) { - let file = payload.file.context("invalid file")?; - let worktree = this - .worktree_store - .read(cx) - .worktree_for_id(WorktreeId::from_proto(file.worktree_id), cx) - .context("no such worktree")?; - let file = File::from_proto(file, worktree, cx)?; - let old_file = buffer.update(cx, |buffer, cx| { - let old_file = buffer.file().cloned(); - let new_path = file.path.clone(); - - buffer.file_updated(Arc::new(file), cx); - if old_file.as_ref().is_none_or(|old| *old.path() != new_path) { - Some(old_file) - } else { - None - } - }); - if let Some(old_file) = old_file { - cx.emit(BufferStoreEvent::BufferChangedFilePath { buffer, old_file }); - } - } - if let Some((downstream_client, project_id)) = this.downstream_client.as_ref() { - downstream_client - .send(proto::UpdateBufferFile { - project_id: *project_id, - buffer_id: buffer_id.into(), - file: envelope.payload.file, - }) - .log_err(); - } - Ok(()) - })? - } - - pub async fn handle_save_buffer( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - let (buffer, project_id) = this.read_with(&cx, |this, _| { - anyhow::Ok(( - this.get_existing(buffer_id)?, - this.downstream_client - .as_ref() - .map(|(_, project_id)| *project_id) - .context("project is not shared")?, - )) - })??; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&envelope.payload.version)) - })? - .await?; - let buffer_id = buffer.read_with(&cx, |buffer, _| buffer.remote_id())?; - - if let Some(new_path) = envelope.payload.new_path - && let Some(new_path) = ProjectPath::from_proto(new_path) - { - this.update(&mut cx, |this, cx| { - this.save_buffer_as(buffer.clone(), new_path, cx) - })? - .await?; - } else { - this.update(&mut cx, |this, cx| this.save_buffer(buffer.clone(), cx))? - .await?; - } - - buffer.read_with(&cx, |buffer, _| proto::BufferSaved { - project_id, - buffer_id: buffer_id.into(), - version: serialize_version(buffer.saved_version()), - mtime: buffer.saved_mtime().map(|time| time.into()), - }) - } - - pub async fn handle_close_buffer( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - let peer_id = envelope.sender_id; - let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - this.update(&mut cx, |this, cx| { - if let Some(shared) = this.shared_buffers.get_mut(&peer_id) - && shared.remove(&buffer_id).is_some() - { - cx.emit(BufferStoreEvent::SharedBufferClosed(peer_id, buffer_id)); - if shared.is_empty() { - this.shared_buffers.remove(&peer_id); - } - return; - } - debug_panic!( - "peer_id {} closed buffer_id {} which was either not open or already closed", - peer_id, - buffer_id - ) - }) - } - - pub async fn handle_buffer_saved( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - let version = deserialize_version(&envelope.payload.version); - let mtime = envelope.payload.mtime.clone().map(|time| time.into()); - this.update(&mut cx, move |this, cx| { - if let Some(buffer) = this.get_possibly_incomplete(buffer_id) { - buffer.update(cx, |buffer, cx| { - buffer.did_save(version, mtime, cx); - }); - } - - if let Some((downstream_client, project_id)) = this.downstream_client.as_ref() { - downstream_client - .send(proto::BufferSaved { - project_id: *project_id, - buffer_id: buffer_id.into(), - mtime: envelope.payload.mtime, - version: envelope.payload.version, - }) - .log_err(); - } - }) - } - - pub async fn handle_buffer_reloaded( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - let version = deserialize_version(&envelope.payload.version); - let mtime = envelope.payload.mtime.clone().map(|time| time.into()); - let line_ending = deserialize_line_ending( - proto::LineEnding::from_i32(envelope.payload.line_ending) - .context("missing line ending")?, - ); - this.update(&mut cx, |this, cx| { - if let Some(buffer) = this.get_possibly_incomplete(buffer_id) { - buffer.update(cx, |buffer, cx| { - buffer.did_reload(version, line_ending, mtime, cx); - }); - } - - if let Some((downstream_client, project_id)) = this.downstream_client.as_ref() { - downstream_client - .send(proto::BufferReloaded { - project_id: *project_id, - buffer_id: buffer_id.into(), - mtime: envelope.payload.mtime, - version: envelope.payload.version, - line_ending: envelope.payload.line_ending, - }) - .log_err(); - } - }) - } - - pub fn reload_buffers( - &self, - buffers: HashSet>, - push_to_history: bool, - cx: &mut Context, - ) -> Task> { - if buffers.is_empty() { - return Task::ready(Ok(ProjectTransaction::default())); - } - match &self.state { - BufferStoreState::Local(this) => this.reload_buffers(buffers, push_to_history, cx), - BufferStoreState::Remote(this) => this.reload_buffers(buffers, push_to_history, cx), - } - } - - async fn handle_reload_buffers( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let sender_id = envelope.original_sender_id().unwrap_or_default(); - let reload = this.update(&mut cx, |this, cx| { - let mut buffers = HashSet::default(); - for buffer_id in &envelope.payload.buffer_ids { - let buffer_id = BufferId::new(*buffer_id)?; - buffers.insert(this.get_existing(buffer_id)?); - } - anyhow::Ok(this.reload_buffers(buffers, false, cx)) - })??; - - let project_transaction = reload.await?; - let project_transaction = this.update(&mut cx, |this, cx| { - this.serialize_project_transaction_for_peer(project_transaction, sender_id, cx) - })?; - Ok(proto::ReloadBuffersResponse { - transaction: Some(project_transaction), - }) - } - - pub fn create_buffer_for_peer( - &mut self, - buffer: &Entity, - peer_id: proto::PeerId, - cx: &mut Context, - ) -> Task> { - let buffer_id = buffer.read(cx).remote_id(); - let shared_buffers = self.shared_buffers.entry(peer_id).or_default(); - if shared_buffers.contains_key(&buffer_id) { - return Task::ready(Ok(())); - } - shared_buffers.insert( - buffer_id, - SharedBuffer { - buffer: buffer.clone(), - lsp_handle: None, - }, - ); - - let Some((client, project_id)) = self.downstream_client.clone() else { - return Task::ready(Ok(())); - }; - - cx.spawn(async move |this, cx| { - let Some(buffer) = this.read_with(cx, |this, _| this.get(buffer_id))? else { - return anyhow::Ok(()); - }; - - let operations = buffer.update(cx, |b, cx| b.serialize_ops(None, cx))?; - let operations = operations.await; - let state = buffer.update(cx, |buffer, cx| buffer.to_proto(cx))?; - - let initial_state = proto::CreateBufferForPeer { - project_id, - peer_id: Some(peer_id), - variant: Some(proto::create_buffer_for_peer::Variant::State(state)), - }; - - if client.send(initial_state).log_err().is_some() { - let client = client.clone(); - cx.background_spawn(async move { - let mut chunks = split_operations(operations).peekable(); - while let Some(chunk) = chunks.next() { - let is_last = chunks.peek().is_none(); - client.send(proto::CreateBufferForPeer { - project_id, - peer_id: Some(peer_id), - variant: Some(proto::create_buffer_for_peer::Variant::Chunk( - proto::BufferChunk { - buffer_id: buffer_id.into(), - operations: chunk, - is_last, - }, - )), - })?; - } - anyhow::Ok(()) - }) - .await - .log_err(); - } - Ok(()) - }) - } - - pub fn forget_shared_buffers(&mut self) { - self.shared_buffers.clear(); - } - - pub fn forget_shared_buffers_for(&mut self, peer_id: &proto::PeerId) { - self.shared_buffers.remove(peer_id); - } - - pub fn update_peer_id(&mut self, old_peer_id: &proto::PeerId, new_peer_id: proto::PeerId) { - if let Some(buffers) = self.shared_buffers.remove(old_peer_id) { - self.shared_buffers.insert(new_peer_id, buffers); - } - } - - pub fn has_shared_buffers(&self) -> bool { - !self.shared_buffers.is_empty() - } - - pub fn create_local_buffer( - &mut self, - text: &str, - language: Option>, - project_searchable: bool, - cx: &mut Context, - ) -> Entity { - let buffer = cx.new(|cx| { - Buffer::local(text, cx) - .with_language(language.unwrap_or_else(|| language::PLAIN_TEXT.clone()), cx) - }); - - self.add_buffer(buffer.clone(), cx).log_err(); - let buffer_id = buffer.read(cx).remote_id(); - if !project_searchable { - self.non_searchable_buffers.insert(buffer_id); - } - - if let Some(file) = File::from_dyn(buffer.read(cx).file()) { - self.path_to_buffer_id.insert( - ProjectPath { - worktree_id: file.worktree_id(cx), - path: file.path.clone(), - }, - buffer_id, - ); - let this = self - .as_local_mut() - .expect("local-only method called in a non-local context"); - if let Some(entry_id) = file.entry_id { - this.local_buffer_ids_by_entry_id - .insert(entry_id, buffer_id); - } - } - buffer - } - - pub fn deserialize_project_transaction( - &mut self, - message: proto::ProjectTransaction, - push_to_history: bool, - cx: &mut Context, - ) -> Task> { - if let Some(this) = self.as_remote_mut() { - this.deserialize_project_transaction(message, push_to_history, cx) - } else { - debug_panic!("not a remote buffer store"); - Task::ready(Err(anyhow!("not a remote buffer store"))) - } - } - - pub fn wait_for_remote_buffer( - &mut self, - id: BufferId, - cx: &mut Context, - ) -> Task>> { - if let Some(this) = self.as_remote_mut() { - this.wait_for_remote_buffer(id, cx) - } else { - debug_panic!("not a remote buffer store"); - Task::ready(Err(anyhow!("not a remote buffer store"))) - } - } - - pub fn serialize_project_transaction_for_peer( - &mut self, - project_transaction: ProjectTransaction, - peer_id: proto::PeerId, - cx: &mut Context, - ) -> proto::ProjectTransaction { - let mut serialized_transaction = proto::ProjectTransaction { - buffer_ids: Default::default(), - transactions: Default::default(), - }; - for (buffer, transaction) in project_transaction.0 { - self.create_buffer_for_peer(&buffer, peer_id, cx) - .detach_and_log_err(cx); - serialized_transaction - .buffer_ids - .push(buffer.read(cx).remote_id().into()); - serialized_transaction - .transactions - .push(language::proto::serialize_transaction(&transaction)); - } - serialized_transaction - } -} - -impl OpenBuffer { - fn upgrade(&self) -> Option> { - match self { - OpenBuffer::Complete { buffer, .. } => buffer.upgrade(), - OpenBuffer::Operations(_) => None, - } - } -} - -fn is_not_found_error(error: &anyhow::Error) -> bool { - error - .root_cause() - .downcast_ref::() - .is_some_and(|err| err.kind() == io::ErrorKind::NotFound) -} diff --git a/crates/project/src/color_extractor.rs b/crates/project/src/color_extractor.rs deleted file mode 100644 index 6e9907e30b..0000000000 --- a/crates/project/src/color_extractor.rs +++ /dev/null @@ -1,295 +0,0 @@ -use std::sync::LazyLock; - -use gpui::{Hsla, Rgba}; -use lsp::{CompletionItem, Documentation}; -use regex::{Regex, RegexBuilder}; - -const HEX: &str = r#"(#(?:[\da-fA-F]{3}){1,2})"#; -const RGB_OR_HSL: &str = r#"(rgba?|hsla?)\(\s*(\d{1,3}%?)\s*,\s*(\d{1,3}%?)\s*,\s*(\d{1,3}%?)\s*(?:,\s*(1|0?\.\d+))?\s*\)"#; - -static RELAXED_HEX_REGEX: LazyLock = LazyLock::new(|| { - RegexBuilder::new(HEX) - .case_insensitive(false) - .build() - .expect("Failed to create RELAXED_HEX_REGEX") -}); - -static STRICT_HEX_REGEX: LazyLock = LazyLock::new(|| { - RegexBuilder::new(&format!("^{HEX}$")) - .case_insensitive(true) - .build() - .expect("Failed to create STRICT_HEX_REGEX") -}); - -static RELAXED_RGB_OR_HSL_REGEX: LazyLock = LazyLock::new(|| { - RegexBuilder::new(RGB_OR_HSL) - .case_insensitive(false) - .build() - .expect("Failed to create RELAXED_RGB_OR_HSL_REGEX") -}); - -static STRICT_RGB_OR_HSL_REGEX: LazyLock = LazyLock::new(|| { - RegexBuilder::new(&format!("^{RGB_OR_HSL}$")) - .case_insensitive(true) - .build() - .expect("Failed to create STRICT_RGB_OR_HSL_REGEX") -}); - -/// Extracts a color from an LSP [`CompletionItem`]. -/// -/// Adapted from https://github.com/microsoft/vscode/blob/a6870fcb6d79093738c17e8319b760cf1c41764a/src/vs/editor/contrib/suggest/browser/suggestWidgetRenderer.ts#L34-L61 -pub fn extract_color(item: &CompletionItem) -> Option { - // Try to extract from entire `label` field. - parse(&item.label, ParseMode::Strict) - // Try to extract from entire `detail` field. - .or_else(|| { - item.detail - .as_ref() - .and_then(|detail| parse(detail, ParseMode::Strict)) - }) - // Try to extract from beginning or end of `documentation` field. - .or_else(|| match item.documentation { - Some(Documentation::String(ref str)) => parse(str, ParseMode::Relaxed), - Some(Documentation::MarkupContent(ref markup)) => { - parse(&markup.value, ParseMode::Relaxed) - } - None => None, - }) -} - -enum ParseMode { - Strict, - Relaxed, -} - -fn parse(str: &str, mode: ParseMode) -> Option { - let (hex, rgb) = match mode { - ParseMode::Strict => (&STRICT_HEX_REGEX, &STRICT_RGB_OR_HSL_REGEX), - ParseMode::Relaxed => (&RELAXED_HEX_REGEX, &RELAXED_RGB_OR_HSL_REGEX), - }; - - if let Some(captures) = hex.captures(str) { - let rmatch = captures.get(0)?; - - // Color must be anchored to start or end of string. - if rmatch.start() > 0 && rmatch.end() != str.len() { - return None; - } - - let hex = captures.get(1)?.as_str(); - - return from_hex(hex); - } - - if let Some(captures) = rgb.captures(str) { - let rmatch = captures.get(0)?; - - // Color must be anchored to start or end of string. - if rmatch.start() > 0 && rmatch.end() != str.len() { - return None; - } - - let typ = captures.get(1)?.as_str(); - let r_or_h = captures.get(2)?.as_str(); - let g_or_s = captures.get(3)?.as_str(); - let b_or_l = captures.get(4)?.as_str(); - let a = captures.get(5).map(|a| a.as_str()); - - return match (typ, a) { - ("rgb", None) | ("rgba", Some(_)) => from_rgb(r_or_h, g_or_s, b_or_l, a), - ("hsl", None) | ("hsla", Some(_)) => from_hsl(r_or_h, g_or_s, b_or_l, a), - _ => None, - }; - } - - None -} - -fn parse_component(value: &str, max: f32) -> Option { - if let Some(field) = value.strip_suffix("%") { - field.parse::().map(|value| value / 100.).ok() - } else { - value.parse::().map(|value| value / max).ok() - } -} - -fn from_hex(hex: &str) -> Option { - Rgba::try_from(hex).map(Hsla::from).ok() -} - -fn from_rgb(r: &str, g: &str, b: &str, a: Option<&str>) -> Option { - let r = parse_component(r, 255.)?; - let g = parse_component(g, 255.)?; - let b = parse_component(b, 255.)?; - let a = a.and_then(|a| parse_component(a, 1.0)).unwrap_or(1.0); - - Some(Rgba { r, g, b, a }.into()) -} - -fn from_hsl(h: &str, s: &str, l: &str, a: Option<&str>) -> Option { - let h = parse_component(h, 360.)?; - let s = parse_component(s, 100.)?; - let l = parse_component(l, 100.)?; - let a = a.and_then(|a| parse_component(a, 1.0)).unwrap_or(1.0); - - Some(Hsla { h, s, l, a }) -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::rgba; - use lsp::{CompletionItem, CompletionItemKind}; - - pub const COLOR_TABLE: &[(&str, Option)] = &[ - // -- Invalid -- - // Invalid hex - ("f0f", None), - ("#fof", None), - // Extra field - ("rgb(255, 0, 0, 0.0)", None), - ("hsl(120, 0, 0, 0.0)", None), - // Missing field - ("rgba(255, 0, 0)", None), - ("hsla(120, 0, 0)", None), - // No decimal after zero - ("rgba(255, 0, 0, 0)", None), - ("hsla(120, 0, 0, 0)", None), - // Decimal after one - ("rgba(255, 0, 0, 1.0)", None), - ("hsla(120, 0, 0, 1.0)", None), - // HEX (sRGB) - ("#f0f", Some(0xFF00FFFF)), - ("#ff0000", Some(0xFF0000FF)), - // RGB / RGBA (sRGB) - ("rgb(255, 0, 0)", Some(0xFF0000FF)), - ("rgba(255, 0, 0, 0.4)", Some(0xFF000066)), - ("rgba(255, 0, 0, 1)", Some(0xFF0000FF)), - ("rgb(20%, 0%, 0%)", Some(0x330000FF)), - ("rgba(20%, 0%, 0%, 1)", Some(0x330000FF)), - ("rgb(0%, 20%, 0%)", Some(0x003300FF)), - ("rgba(0%, 20%, 0%, 1)", Some(0x003300FF)), - ("rgb(0%, 0%, 20%)", Some(0x000033FF)), - ("rgba(0%, 0%, 20%, 1)", Some(0x000033FF)), - // HSL / HSLA (sRGB) - ("hsl(0, 100%, 50%)", Some(0xFF0000FF)), - ("hsl(120, 100%, 50%)", Some(0x00FF00FF)), - ("hsla(0, 100%, 50%, 0.0)", Some(0xFF000000)), - ("hsla(0, 100%, 50%, 0.4)", Some(0xFF000066)), - ("hsla(0, 100%, 50%, 1)", Some(0xFF0000FF)), - ("hsla(120, 100%, 50%, 0.0)", Some(0x00FF0000)), - ("hsla(120, 100%, 50%, 0.4)", Some(0x00FF0066)), - ("hsla(120, 100%, 50%, 1)", Some(0x00FF00FF)), - ]; - - #[test] - fn can_extract_from_label() { - for (color_str, color_val) in COLOR_TABLE.iter() { - let color = extract_color(&CompletionItem { - kind: Some(CompletionItemKind::COLOR), - label: color_str.to_string(), - detail: None, - documentation: None, - ..Default::default() - }); - - assert_eq!(color, color_val.map(|v| Hsla::from(rgba(v)))); - } - } - - #[test] - fn only_whole_label_matches_are_allowed() { - for (color_str, _) in COLOR_TABLE.iter() { - let color = extract_color(&CompletionItem { - kind: Some(CompletionItemKind::COLOR), - label: format!("{} foo", color_str).to_string(), - detail: None, - documentation: None, - ..Default::default() - }); - - assert_eq!(color, None); - } - } - - #[test] - fn can_extract_from_detail() { - for (color_str, color_val) in COLOR_TABLE.iter() { - let color = extract_color(&CompletionItem { - kind: Some(CompletionItemKind::COLOR), - label: "".to_string(), - detail: Some(color_str.to_string()), - documentation: None, - ..Default::default() - }); - - assert_eq!(color, color_val.map(|v| Hsla::from(rgba(v)))); - } - } - - #[test] - fn only_whole_detail_matches_are_allowed() { - for (color_str, _) in COLOR_TABLE.iter() { - let color = extract_color(&CompletionItem { - kind: Some(CompletionItemKind::COLOR), - label: "".to_string(), - detail: Some(format!("{} foo", color_str).to_string()), - documentation: None, - ..Default::default() - }); - - assert_eq!(color, None); - } - } - - #[test] - fn can_extract_from_documentation_start() { - for (color_str, color_val) in COLOR_TABLE.iter() { - let color = extract_color(&CompletionItem { - kind: Some(CompletionItemKind::COLOR), - label: "".to_string(), - detail: None, - documentation: Some(Documentation::String( - format!("{} foo", color_str).to_string(), - )), - ..Default::default() - }); - - assert_eq!(color, color_val.map(|v| Hsla::from(rgba(v)))); - } - } - - #[test] - fn can_extract_from_documentation_end() { - for (color_str, color_val) in COLOR_TABLE.iter() { - let color = extract_color(&CompletionItem { - kind: Some(CompletionItemKind::COLOR), - label: "".to_string(), - detail: None, - documentation: Some(Documentation::String( - format!("foo {}", color_str).to_string(), - )), - ..Default::default() - }); - - assert_eq!(color, color_val.map(|v| Hsla::from(rgba(v)))); - } - } - - #[test] - fn cannot_extract_from_documentation_middle() { - for (color_str, _) in COLOR_TABLE.iter() { - let color = extract_color(&CompletionItem { - kind: Some(CompletionItemKind::COLOR), - label: "".to_string(), - detail: None, - documentation: Some(Documentation::String( - format!("foo {} foo", color_str).to_string(), - )), - ..Default::default() - }); - - assert_eq!(color, None); - } - } -} diff --git a/crates/project/src/connection_manager.rs b/crates/project/src/connection_manager.rs deleted file mode 100644 index 253d5d32a1..0000000000 --- a/crates/project/src/connection_manager.rs +++ /dev/null @@ -1,223 +0,0 @@ -use super::Project; -use anyhow::Result; -use client::Client; -use collections::{HashMap, HashSet}; -use futures::{FutureExt, StreamExt}; -use gpui::{App, AppContext as _, AsyncApp, Context, Entity, Global, Task, WeakEntity}; -use postage::stream::Stream; -use rpc::proto; -use std::{sync::Arc, time::Duration}; -use util::ResultExt; - -impl Global for GlobalManager {} -struct GlobalManager(Entity); - -pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); - -pub struct Manager { - client: Arc, - maintain_connection: Option>>, - projects: HashSet>, -} - -pub fn init(client: Arc, cx: &mut App) { - let manager = cx.new(|_| Manager { - client, - maintain_connection: None, - projects: HashSet::default(), - }); - cx.set_global(GlobalManager(manager)); -} - -impl Manager { - pub fn global(cx: &App) -> Entity { - cx.global::().0.clone() - } - - pub fn maintain_project_connection( - &mut self, - project: &Entity, - cx: &mut Context, - ) { - let manager = cx.weak_entity(); - project.update(cx, |_, cx| { - let manager = manager.clone(); - cx.on_release(move |project, cx| { - manager - .update(cx, |manager, cx| { - manager.projects.retain(|p| { - if let Some(p) = p.upgrade() { - p.read(cx).remote_id() != project.remote_id() - } else { - false - } - }); - if manager.projects.is_empty() { - manager.maintain_connection.take(); - } - }) - .ok(); - }) - .detach(); - }); - - self.projects.insert(project.downgrade()); - if self.maintain_connection.is_none() { - self.maintain_connection = Some(cx.spawn({ - let client = self.client.clone(); - async move |_, cx| { - Self::maintain_connection(manager, client.clone(), cx) - .await - .log_err() - } - })); - } - } - - fn reconnected(&mut self, cx: &mut Context) -> Task> { - let mut projects = HashMap::default(); - - let request = self.client.request_envelope(proto::RejoinRemoteProjects { - rejoined_projects: self - .projects - .iter() - .filter_map(|project| { - if let Some(handle) = project.upgrade() { - let project = handle.read(cx); - let project_id = project.remote_id()?; - projects.insert(project_id, handle.clone()); - let mut worktrees = Vec::new(); - let mut repositories = Vec::new(); - for (id, repository) in project.repositories(cx) { - repositories.push(proto::RejoinRepository { - id: id.to_proto(), - scan_id: repository.read(cx).scan_id, - }); - } - for worktree in project.worktrees(cx) { - let worktree = worktree.read(cx); - worktrees.push(proto::RejoinWorktree { - id: worktree.id().to_proto(), - scan_id: worktree.completed_scan_id() as u64, - }); - } - Some(proto::RejoinProject { - id: project_id, - worktrees, - repositories, - }) - } else { - None - } - }) - .collect(), - }); - - cx.spawn(async move |this, cx| { - let response = request.await?; - let message_id = response.message_id; - - this.update(cx, |_, cx| { - for rejoined_project in response.payload.rejoined_projects { - if let Some(project) = projects.get(&rejoined_project.id) { - project.update(cx, |project, cx| { - project.rejoined(rejoined_project, message_id, cx).log_err(); - }); - } - } - }) - }) - } - - fn connection_lost(&mut self, cx: &mut Context) { - for project in self.projects.drain() { - if let Some(project) = project.upgrade() { - project.update(cx, |project, cx| { - project.disconnected_from_host(cx); - project.close(cx); - }); - } - } - self.maintain_connection.take(); - } - - async fn maintain_connection( - this: WeakEntity, - client: Arc, - cx: &mut AsyncApp, - ) -> Result<()> { - let mut client_status = client.status(); - loop { - let _ = client_status.try_recv(); - - let is_connected = client_status.borrow().is_connected(); - // Even if we're initially connected, any future change of the status means we momentarily disconnected. - if !is_connected || client_status.next().await.is_some() { - log::info!("detected client disconnection"); - - // Wait for client to re-establish a connection to the server. - { - let mut reconnection_timeout = - cx.background_executor().timer(RECONNECT_TIMEOUT).fuse(); - let client_reconnection = async { - let mut remaining_attempts = 3; - while remaining_attempts > 0 { - if client_status.borrow().is_connected() { - log::info!("client reconnected, attempting to rejoin projects"); - - let Some(this) = this.upgrade() else { break }; - match this.update(cx, |this, cx| this.reconnected(cx)) { - Ok(task) => { - if task.await.log_err().is_some() { - return true; - } else { - remaining_attempts -= 1; - } - } - Err(_app_dropped) => return false, - } - } else if client_status.borrow().is_signed_out() { - return false; - } - - log::info!( - "waiting for client status change, remaining attempts {}", - remaining_attempts - ); - client_status.next().await; - } - false - } - .fuse(); - futures::pin_mut!(client_reconnection); - - futures::select_biased! { - reconnected = client_reconnection => { - if reconnected { - log::info!("successfully reconnected"); - // If we successfully joined the room, go back around the loop - // waiting for future connection status changes. - continue; - } - } - _ = reconnection_timeout => { - log::info!("rejoin project reconnection timeout expired"); - } - } - } - - break; - } - } - - // The client failed to re-establish a connection to the server - // or an error occurred while trying to re-join the room. Either way - // we leave the room and return an error. - if let Some(this) = this.upgrade() { - log::info!("reconnection failed, disconnecting projects"); - this.update(cx, |this, cx| this.connection_lost(cx))?; - } - - Ok(()) - } -} diff --git a/crates/project/src/context_server_store.rs b/crates/project/src/context_server_store.rs deleted file mode 100644 index 7ba46a4687..0000000000 --- a/crates/project/src/context_server_store.rs +++ /dev/null @@ -1,1437 +0,0 @@ -pub mod extension; -pub mod registry; - -use std::sync::Arc; - -use anyhow::{Context as _, Result}; -use collections::{HashMap, HashSet}; -use context_server::{ContextServer, ContextServerCommand, ContextServerId}; -use futures::{FutureExt as _, future::join_all}; -use gpui::{App, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity, actions}; -use registry::ContextServerDescriptorRegistry; -use settings::{Settings as _, SettingsStore}; -use util::{ResultExt as _, rel_path::RelPath}; - -use crate::{ - Project, - project_settings::{ContextServerSettings, ProjectSettings}, - worktree_store::WorktreeStore, -}; - -pub fn init(cx: &mut App) { - extension::init(cx); -} - -actions!( - context_server, - [ - /// Restarts the context server. - Restart - ] -); - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum ContextServerStatus { - Starting, - Running, - Stopped, - Error(Arc), -} - -impl ContextServerStatus { - fn from_state(state: &ContextServerState) -> Self { - match state { - ContextServerState::Starting { .. } => ContextServerStatus::Starting, - ContextServerState::Running { .. } => ContextServerStatus::Running, - ContextServerState::Stopped { .. } => ContextServerStatus::Stopped, - ContextServerState::Error { error, .. } => ContextServerStatus::Error(error.clone()), - } - } -} - -enum ContextServerState { - Starting { - server: Arc, - configuration: Arc, - _task: Task<()>, - }, - Running { - server: Arc, - configuration: Arc, - }, - Stopped { - server: Arc, - configuration: Arc, - }, - Error { - server: Arc, - configuration: Arc, - error: Arc, - }, -} - -impl ContextServerState { - pub fn server(&self) -> Arc { - match self { - ContextServerState::Starting { server, .. } => server.clone(), - ContextServerState::Running { server, .. } => server.clone(), - ContextServerState::Stopped { server, .. } => server.clone(), - ContextServerState::Error { server, .. } => server.clone(), - } - } - - pub fn configuration(&self) -> Arc { - match self { - ContextServerState::Starting { configuration, .. } => configuration.clone(), - ContextServerState::Running { configuration, .. } => configuration.clone(), - ContextServerState::Stopped { configuration, .. } => configuration.clone(), - ContextServerState::Error { configuration, .. } => configuration.clone(), - } - } -} - -#[derive(Debug, PartialEq, Eq)] -pub enum ContextServerConfiguration { - Custom { - command: ContextServerCommand, - }, - Extension { - command: ContextServerCommand, - settings: serde_json::Value, - }, - Http { - url: url::Url, - headers: HashMap, - }, -} - -impl ContextServerConfiguration { - pub fn command(&self) -> Option<&ContextServerCommand> { - match self { - ContextServerConfiguration::Custom { command } => Some(command), - ContextServerConfiguration::Extension { command, .. } => Some(command), - ContextServerConfiguration::Http { .. } => None, - } - } - - pub async fn from_settings( - settings: ContextServerSettings, - id: ContextServerId, - registry: Entity, - worktree_store: Entity, - cx: &AsyncApp, - ) -> Option { - match settings { - ContextServerSettings::Stdio { - enabled: _, - command, - } => Some(ContextServerConfiguration::Custom { command }), - ContextServerSettings::Extension { - enabled: _, - settings, - } => { - let descriptor = cx - .update(|cx| registry.read(cx).context_server_descriptor(&id.0)) - .ok() - .flatten()?; - - match descriptor.command(worktree_store, cx).await { - Ok(command) => { - Some(ContextServerConfiguration::Extension { command, settings }) - } - Err(e) => { - log::error!( - "Failed to create context server configuration from settings: {e:#}" - ); - None - } - } - } - ContextServerSettings::Http { - enabled: _, - url, - headers: auth, - } => { - let url = url::Url::parse(&url).log_err()?; - Some(ContextServerConfiguration::Http { url, headers: auth }) - } - } - } -} - -pub type ContextServerFactory = - Box) -> Arc>; - -pub struct ContextServerStore { - context_server_settings: HashMap, ContextServerSettings>, - servers: HashMap, - worktree_store: Entity, - project: WeakEntity, - registry: Entity, - update_servers_task: Option>>, - context_server_factory: Option, - needs_server_update: bool, - _subscriptions: Vec, -} - -pub enum Event { - ServerStatusChanged { - server_id: ContextServerId, - status: ContextServerStatus, - }, -} - -impl EventEmitter for ContextServerStore {} - -impl ContextServerStore { - pub fn new( - worktree_store: Entity, - weak_project: WeakEntity, - cx: &mut Context, - ) -> Self { - Self::new_internal( - true, - None, - ContextServerDescriptorRegistry::default_global(cx), - worktree_store, - weak_project, - cx, - ) - } - - /// Returns all configured context server ids, excluding the ones that are disabled - pub fn configured_server_ids(&self) -> Vec { - self.context_server_settings - .iter() - .filter(|(_, settings)| settings.enabled()) - .map(|(id, _)| ContextServerId(id.clone())) - .collect() - } - - #[cfg(any(test, feature = "test-support"))] - pub fn test( - registry: Entity, - worktree_store: Entity, - weak_project: WeakEntity, - cx: &mut Context, - ) -> Self { - Self::new_internal(false, None, registry, worktree_store, weak_project, cx) - } - - #[cfg(any(test, feature = "test-support"))] - pub fn test_maintain_server_loop( - context_server_factory: Option, - registry: Entity, - worktree_store: Entity, - weak_project: WeakEntity, - cx: &mut Context, - ) -> Self { - Self::new_internal( - true, - context_server_factory, - registry, - worktree_store, - weak_project, - cx, - ) - } - - fn new_internal( - maintain_server_loop: bool, - context_server_factory: Option, - registry: Entity, - worktree_store: Entity, - weak_project: WeakEntity, - cx: &mut Context, - ) -> Self { - let subscriptions = if maintain_server_loop { - vec![ - cx.observe(®istry, |this, _registry, cx| { - this.available_context_servers_changed(cx); - }), - cx.observe_global::(|this, cx| { - let settings = Self::resolve_context_server_settings(&this.worktree_store, cx); - if &this.context_server_settings == settings { - return; - } - this.context_server_settings = settings.clone(); - this.available_context_servers_changed(cx); - }), - ] - } else { - Vec::new() - }; - - let mut this = Self { - _subscriptions: subscriptions, - context_server_settings: Self::resolve_context_server_settings(&worktree_store, cx) - .clone(), - worktree_store, - project: weak_project, - registry, - needs_server_update: false, - servers: HashMap::default(), - update_servers_task: None, - context_server_factory, - }; - if maintain_server_loop { - this.available_context_servers_changed(cx); - } - this - } - - pub fn get_server(&self, id: &ContextServerId) -> Option> { - self.servers.get(id).map(|state| state.server()) - } - - pub fn get_running_server(&self, id: &ContextServerId) -> Option> { - if let Some(ContextServerState::Running { server, .. }) = self.servers.get(id) { - Some(server.clone()) - } else { - None - } - } - - pub fn status_for_server(&self, id: &ContextServerId) -> Option { - self.servers.get(id).map(ContextServerStatus::from_state) - } - - pub fn configuration_for_server( - &self, - id: &ContextServerId, - ) -> Option> { - self.servers.get(id).map(|state| state.configuration()) - } - - pub fn server_ids(&self, cx: &App) -> HashSet { - self.servers - .keys() - .cloned() - .chain( - self.registry - .read(cx) - .context_server_descriptors() - .into_iter() - .map(|(id, _)| ContextServerId(id)), - ) - .collect() - } - - pub fn running_servers(&self) -> Vec> { - self.servers - .values() - .filter_map(|state| { - if let ContextServerState::Running { server, .. } = state { - Some(server.clone()) - } else { - None - } - }) - .collect() - } - - pub fn start_server(&mut self, server: Arc, cx: &mut Context) { - cx.spawn(async move |this, cx| { - let this = this.upgrade().context("Context server store dropped")?; - let settings = this - .update(cx, |this, _| { - this.context_server_settings.get(&server.id().0).cloned() - }) - .ok() - .flatten() - .context("Failed to get context server settings")?; - - if !settings.enabled() { - return Ok(()); - } - - let (registry, worktree_store) = this.update(cx, |this, _| { - (this.registry.clone(), this.worktree_store.clone()) - })?; - let configuration = ContextServerConfiguration::from_settings( - settings, - server.id(), - registry, - worktree_store, - cx, - ) - .await - .context("Failed to create context server configuration")?; - - this.update(cx, |this, cx| { - this.run_server(server, Arc::new(configuration), cx) - }) - }) - .detach_and_log_err(cx); - } - - pub fn stop_server(&mut self, id: &ContextServerId, cx: &mut Context) -> Result<()> { - if matches!( - self.servers.get(id), - Some(ContextServerState::Stopped { .. }) - ) { - return Ok(()); - } - - let state = self - .servers - .remove(id) - .context("Context server not found")?; - - let server = state.server(); - let configuration = state.configuration(); - let mut result = Ok(()); - if let ContextServerState::Running { server, .. } = &state { - result = server.stop(); - } - drop(state); - - self.update_server_state( - id.clone(), - ContextServerState::Stopped { - configuration, - server, - }, - cx, - ); - - result - } - - fn run_server( - &mut self, - server: Arc, - configuration: Arc, - cx: &mut Context, - ) { - let id = server.id(); - if matches!( - self.servers.get(&id), - Some(ContextServerState::Starting { .. } | ContextServerState::Running { .. }) - ) { - self.stop_server(&id, cx).log_err(); - } - let task = cx.spawn({ - let id = server.id(); - let server = server.clone(); - let configuration = configuration.clone(); - - async move |this, cx| { - match server.clone().start(cx).await { - Ok(_) => { - debug_assert!(server.client().is_some()); - - this.update(cx, |this, cx| { - this.update_server_state( - id.clone(), - ContextServerState::Running { - server, - configuration, - }, - cx, - ) - }) - .log_err() - } - Err(err) => { - log::error!("{} context server failed to start: {}", id, err); - this.update(cx, |this, cx| { - this.update_server_state( - id.clone(), - ContextServerState::Error { - configuration, - server, - error: err.to_string().into(), - }, - cx, - ) - }) - .log_err() - } - }; - } - }); - - self.update_server_state( - id.clone(), - ContextServerState::Starting { - configuration, - _task: task, - server, - }, - cx, - ); - } - - fn remove_server(&mut self, id: &ContextServerId, cx: &mut Context) -> Result<()> { - let state = self - .servers - .remove(id) - .context("Context server not found")?; - drop(state); - cx.emit(Event::ServerStatusChanged { - server_id: id.clone(), - status: ContextServerStatus::Stopped, - }); - Ok(()) - } - - fn create_context_server( - &self, - id: ContextServerId, - configuration: Arc, - cx: &mut Context, - ) -> Result> { - if let Some(factory) = self.context_server_factory.as_ref() { - return Ok(factory(id, configuration)); - } - - match configuration.as_ref() { - ContextServerConfiguration::Http { url, headers } => Ok(Arc::new(ContextServer::http( - id, - url, - headers.clone(), - cx.http_client(), - cx.background_executor().clone(), - )?)), - _ => { - let root_path = self - .project - .read_with(cx, |project, cx| project.active_project_directory(cx)) - .ok() - .flatten() - .or_else(|| { - self.worktree_store.read_with(cx, |store, cx| { - store.visible_worktrees(cx).fold(None, |acc, item| { - if acc.is_none() { - item.read(cx).root_dir() - } else { - acc - } - }) - }) - }); - Ok(Arc::new(ContextServer::stdio( - id, - configuration.command().unwrap().clone(), - root_path, - ))) - } - } - } - - fn resolve_context_server_settings<'a>( - worktree_store: &'a Entity, - cx: &'a App, - ) -> &'a HashMap, ContextServerSettings> { - let location = worktree_store - .read(cx) - .visible_worktrees(cx) - .next() - .map(|worktree| settings::SettingsLocation { - worktree_id: worktree.read(cx).id(), - path: RelPath::empty(), - }); - &ProjectSettings::get(location, cx).context_servers - } - - fn update_server_state( - &mut self, - id: ContextServerId, - state: ContextServerState, - cx: &mut Context, - ) { - let status = ContextServerStatus::from_state(&state); - self.servers.insert(id.clone(), state); - cx.emit(Event::ServerStatusChanged { - server_id: id, - status, - }); - } - - fn available_context_servers_changed(&mut self, cx: &mut Context) { - if self.update_servers_task.is_some() { - self.needs_server_update = true; - } else { - self.needs_server_update = false; - self.update_servers_task = Some(cx.spawn(async move |this, cx| { - if let Err(err) = Self::maintain_servers(this.clone(), cx).await { - log::error!("Error maintaining context servers: {}", err); - } - - this.update(cx, |this, cx| { - this.update_servers_task.take(); - if this.needs_server_update { - this.available_context_servers_changed(cx); - } - })?; - - Ok(()) - })); - } - } - - async fn maintain_servers(this: WeakEntity, cx: &mut AsyncApp) -> Result<()> { - let (mut configured_servers, registry, worktree_store) = this.update(cx, |this, _| { - ( - this.context_server_settings.clone(), - this.registry.clone(), - this.worktree_store.clone(), - ) - })?; - - for (id, _) in - registry.read_with(cx, |registry, _| registry.context_server_descriptors())? - { - configured_servers - .entry(id) - .or_insert(ContextServerSettings::default_extension()); - } - - let (enabled_servers, disabled_servers): (HashMap<_, _>, HashMap<_, _>) = - configured_servers - .into_iter() - .partition(|(_, settings)| settings.enabled()); - - let configured_servers = join_all(enabled_servers.into_iter().map(|(id, settings)| { - let id = ContextServerId(id); - ContextServerConfiguration::from_settings( - settings, - id.clone(), - registry.clone(), - worktree_store.clone(), - cx, - ) - .map(|config| (id, config)) - })) - .await - .into_iter() - .filter_map(|(id, config)| config.map(|config| (id, config))) - .collect::>(); - - let mut servers_to_start = Vec::new(); - let mut servers_to_remove = HashSet::default(); - let mut servers_to_stop = HashSet::default(); - - this.update(cx, |this, cx| { - for server_id in this.servers.keys() { - // All servers that are not in desired_servers should be removed from the store. - // This can happen if the user removed a server from the context server settings. - if !configured_servers.contains_key(server_id) { - if disabled_servers.contains_key(&server_id.0) { - servers_to_stop.insert(server_id.clone()); - } else { - servers_to_remove.insert(server_id.clone()); - } - } - } - - for (id, config) in configured_servers { - let state = this.servers.get(&id); - let is_stopped = matches!(state, Some(ContextServerState::Stopped { .. })); - let existing_config = state.as_ref().map(|state| state.configuration()); - if existing_config.as_deref() != Some(&config) || is_stopped { - let config = Arc::new(config); - let server = this.create_context_server(id.clone(), config.clone(), cx)?; - servers_to_start.push((server, config)); - if this.servers.contains_key(&id) { - servers_to_stop.insert(id); - } - } - } - - anyhow::Ok(()) - })??; - - this.update(cx, |this, cx| { - for id in servers_to_stop { - this.stop_server(&id, cx)?; - } - for id in servers_to_remove { - this.remove_server(&id, cx)?; - } - for (server, config) in servers_to_start { - this.run_server(server, config, cx); - } - anyhow::Ok(()) - })? - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - FakeFs, Project, context_server_store::registry::ContextServerDescriptor, - project_settings::ProjectSettings, - }; - use context_server::test::create_fake_transport; - use gpui::{AppContext, TestAppContext, UpdateGlobal as _}; - use http_client::{FakeHttpClient, Response}; - use serde_json::json; - use std::{cell::RefCell, path::PathBuf, rc::Rc}; - use util::path; - - #[gpui::test] - async fn test_context_server_status(cx: &mut TestAppContext) { - const SERVER_1_ID: &str = "mcp-1"; - const SERVER_2_ID: &str = "mcp-2"; - - let (_fs, project) = setup_context_server_test( - cx, - json!({"code.rs": ""}), - vec![ - (SERVER_1_ID.into(), dummy_server_settings()), - (SERVER_2_ID.into(), dummy_server_settings()), - ], - ) - .await; - - let registry = cx.new(|_| ContextServerDescriptorRegistry::new()); - let store = cx.new(|cx| { - ContextServerStore::test( - registry.clone(), - project.read(cx).worktree_store(), - project.downgrade(), - cx, - ) - }); - - let server_1_id = ContextServerId(SERVER_1_ID.into()); - let server_2_id = ContextServerId(SERVER_2_ID.into()); - - let server_1 = Arc::new(ContextServer::new( - server_1_id.clone(), - Arc::new(create_fake_transport(SERVER_1_ID, cx.executor())), - )); - let server_2 = Arc::new(ContextServer::new( - server_2_id.clone(), - Arc::new(create_fake_transport(SERVER_2_ID, cx.executor())), - )); - - store.update(cx, |store, cx| store.start_server(server_1, cx)); - - cx.run_until_parked(); - - cx.update(|cx| { - assert_eq!( - store.read(cx).status_for_server(&server_1_id), - Some(ContextServerStatus::Running) - ); - assert_eq!(store.read(cx).status_for_server(&server_2_id), None); - }); - - store.update(cx, |store, cx| store.start_server(server_2.clone(), cx)); - - cx.run_until_parked(); - - cx.update(|cx| { - assert_eq!( - store.read(cx).status_for_server(&server_1_id), - Some(ContextServerStatus::Running) - ); - assert_eq!( - store.read(cx).status_for_server(&server_2_id), - Some(ContextServerStatus::Running) - ); - }); - - store - .update(cx, |store, cx| store.stop_server(&server_2_id, cx)) - .unwrap(); - - cx.update(|cx| { - assert_eq!( - store.read(cx).status_for_server(&server_1_id), - Some(ContextServerStatus::Running) - ); - assert_eq!( - store.read(cx).status_for_server(&server_2_id), - Some(ContextServerStatus::Stopped) - ); - }); - } - - #[gpui::test] - async fn test_context_server_status_events(cx: &mut TestAppContext) { - const SERVER_1_ID: &str = "mcp-1"; - const SERVER_2_ID: &str = "mcp-2"; - - let (_fs, project) = setup_context_server_test( - cx, - json!({"code.rs": ""}), - vec![ - (SERVER_1_ID.into(), dummy_server_settings()), - (SERVER_2_ID.into(), dummy_server_settings()), - ], - ) - .await; - - let registry = cx.new(|_| ContextServerDescriptorRegistry::new()); - let store = cx.new(|cx| { - ContextServerStore::test( - registry.clone(), - project.read(cx).worktree_store(), - project.downgrade(), - cx, - ) - }); - - let server_1_id = ContextServerId(SERVER_1_ID.into()); - let server_2_id = ContextServerId(SERVER_2_ID.into()); - - let server_1 = Arc::new(ContextServer::new( - server_1_id.clone(), - Arc::new(create_fake_transport(SERVER_1_ID, cx.executor())), - )); - let server_2 = Arc::new(ContextServer::new( - server_2_id.clone(), - Arc::new(create_fake_transport(SERVER_2_ID, cx.executor())), - )); - - let _server_events = assert_server_events( - &store, - vec![ - (server_1_id.clone(), ContextServerStatus::Starting), - (server_1_id, ContextServerStatus::Running), - (server_2_id.clone(), ContextServerStatus::Starting), - (server_2_id.clone(), ContextServerStatus::Running), - (server_2_id.clone(), ContextServerStatus::Stopped), - ], - cx, - ); - - store.update(cx, |store, cx| store.start_server(server_1, cx)); - - cx.run_until_parked(); - - store.update(cx, |store, cx| store.start_server(server_2.clone(), cx)); - - cx.run_until_parked(); - - store - .update(cx, |store, cx| store.stop_server(&server_2_id, cx)) - .unwrap(); - } - - #[gpui::test(iterations = 25)] - async fn test_context_server_concurrent_starts(cx: &mut TestAppContext) { - const SERVER_1_ID: &str = "mcp-1"; - - let (_fs, project) = setup_context_server_test( - cx, - json!({"code.rs": ""}), - vec![(SERVER_1_ID.into(), dummy_server_settings())], - ) - .await; - - let registry = cx.new(|_| ContextServerDescriptorRegistry::new()); - let store = cx.new(|cx| { - ContextServerStore::test( - registry.clone(), - project.read(cx).worktree_store(), - project.downgrade(), - cx, - ) - }); - - let server_id = ContextServerId(SERVER_1_ID.into()); - - let server_with_same_id_1 = Arc::new(ContextServer::new( - server_id.clone(), - Arc::new(create_fake_transport(SERVER_1_ID, cx.executor())), - )); - let server_with_same_id_2 = Arc::new(ContextServer::new( - server_id.clone(), - Arc::new(create_fake_transport(SERVER_1_ID, cx.executor())), - )); - - // If we start another server with the same id, we should report that we stopped the previous one - let _server_events = assert_server_events( - &store, - vec![ - (server_id.clone(), ContextServerStatus::Starting), - (server_id.clone(), ContextServerStatus::Stopped), - (server_id.clone(), ContextServerStatus::Starting), - (server_id.clone(), ContextServerStatus::Running), - ], - cx, - ); - - store.update(cx, |store, cx| { - store.start_server(server_with_same_id_1.clone(), cx) - }); - store.update(cx, |store, cx| { - store.start_server(server_with_same_id_2.clone(), cx) - }); - - cx.run_until_parked(); - - cx.update(|cx| { - assert_eq!( - store.read(cx).status_for_server(&server_id), - Some(ContextServerStatus::Running) - ); - }); - } - - #[gpui::test] - async fn test_context_server_maintain_servers_loop(cx: &mut TestAppContext) { - const SERVER_1_ID: &str = "mcp-1"; - const SERVER_2_ID: &str = "mcp-2"; - - let server_1_id = ContextServerId(SERVER_1_ID.into()); - let server_2_id = ContextServerId(SERVER_2_ID.into()); - - let fake_descriptor_1 = Arc::new(FakeContextServerDescriptor::new(SERVER_1_ID)); - - let (_fs, project) = setup_context_server_test( - cx, - json!({"code.rs": ""}), - vec![( - SERVER_1_ID.into(), - ContextServerSettings::Extension { - enabled: true, - settings: json!({ - "somevalue": true - }), - }, - )], - ) - .await; - - let executor = cx.executor(); - let registry = cx.new(|cx| { - let mut registry = ContextServerDescriptorRegistry::new(); - registry.register_context_server_descriptor(SERVER_1_ID.into(), fake_descriptor_1, cx); - registry - }); - let store = cx.new(|cx| { - ContextServerStore::test_maintain_server_loop( - Some(Box::new(move |id, _| { - Arc::new(ContextServer::new( - id.clone(), - Arc::new(create_fake_transport(id.0.to_string(), executor.clone())), - )) - })), - registry.clone(), - project.read(cx).worktree_store(), - project.downgrade(), - cx, - ) - }); - - // Ensure that mcp-1 starts up - { - let _server_events = assert_server_events( - &store, - vec![ - (server_1_id.clone(), ContextServerStatus::Starting), - (server_1_id.clone(), ContextServerStatus::Running), - ], - cx, - ); - cx.run_until_parked(); - } - - // Ensure that mcp-1 is restarted when the configuration was changed - { - let _server_events = assert_server_events( - &store, - vec![ - (server_1_id.clone(), ContextServerStatus::Stopped), - (server_1_id.clone(), ContextServerStatus::Starting), - (server_1_id.clone(), ContextServerStatus::Running), - ], - cx, - ); - set_context_server_configuration( - vec![( - server_1_id.0.clone(), - settings::ContextServerSettingsContent::Extension { - enabled: true, - settings: json!({ - "somevalue": false - }), - }, - )], - cx, - ); - - cx.run_until_parked(); - } - - // Ensure that mcp-1 is not restarted when the configuration was not changed - { - let _server_events = assert_server_events(&store, vec![], cx); - set_context_server_configuration( - vec![( - server_1_id.0.clone(), - settings::ContextServerSettingsContent::Extension { - enabled: true, - settings: json!({ - "somevalue": false - }), - }, - )], - cx, - ); - - cx.run_until_parked(); - } - - // Ensure that mcp-2 is started once it is added to the settings - { - let _server_events = assert_server_events( - &store, - vec![ - (server_2_id.clone(), ContextServerStatus::Starting), - (server_2_id.clone(), ContextServerStatus::Running), - ], - cx, - ); - set_context_server_configuration( - vec![ - ( - server_1_id.0.clone(), - settings::ContextServerSettingsContent::Extension { - enabled: true, - settings: json!({ - "somevalue": false - }), - }, - ), - ( - server_2_id.0.clone(), - settings::ContextServerSettingsContent::Stdio { - enabled: true, - command: ContextServerCommand { - path: "somebinary".into(), - args: vec!["arg".to_string()], - env: None, - timeout: None, - }, - }, - ), - ], - cx, - ); - - cx.run_until_parked(); - } - - // Ensure that mcp-2 is restarted once the args have changed - { - let _server_events = assert_server_events( - &store, - vec![ - (server_2_id.clone(), ContextServerStatus::Stopped), - (server_2_id.clone(), ContextServerStatus::Starting), - (server_2_id.clone(), ContextServerStatus::Running), - ], - cx, - ); - set_context_server_configuration( - vec![ - ( - server_1_id.0.clone(), - settings::ContextServerSettingsContent::Extension { - enabled: true, - settings: json!({ - "somevalue": false - }), - }, - ), - ( - server_2_id.0.clone(), - settings::ContextServerSettingsContent::Stdio { - enabled: true, - command: ContextServerCommand { - path: "somebinary".into(), - args: vec!["anotherArg".to_string()], - env: None, - timeout: None, - }, - }, - ), - ], - cx, - ); - - cx.run_until_parked(); - } - - // Ensure that mcp-2 is removed once it is removed from the settings - { - let _server_events = assert_server_events( - &store, - vec![(server_2_id.clone(), ContextServerStatus::Stopped)], - cx, - ); - set_context_server_configuration( - vec![( - server_1_id.0.clone(), - settings::ContextServerSettingsContent::Extension { - enabled: true, - settings: json!({ - "somevalue": false - }), - }, - )], - cx, - ); - - cx.run_until_parked(); - - cx.update(|cx| { - assert_eq!(store.read(cx).status_for_server(&server_2_id), None); - }); - } - - // Ensure that nothing happens if the settings do not change - { - let _server_events = assert_server_events(&store, vec![], cx); - set_context_server_configuration( - vec![( - server_1_id.0.clone(), - settings::ContextServerSettingsContent::Extension { - enabled: true, - settings: json!({ - "somevalue": false - }), - }, - )], - cx, - ); - - cx.run_until_parked(); - - cx.update(|cx| { - assert_eq!( - store.read(cx).status_for_server(&server_1_id), - Some(ContextServerStatus::Running) - ); - assert_eq!(store.read(cx).status_for_server(&server_2_id), None); - }); - } - } - - #[gpui::test] - async fn test_context_server_enabled_disabled(cx: &mut TestAppContext) { - const SERVER_1_ID: &str = "mcp-1"; - - let server_1_id = ContextServerId(SERVER_1_ID.into()); - - let (_fs, project) = setup_context_server_test( - cx, - json!({"code.rs": ""}), - vec![( - SERVER_1_ID.into(), - ContextServerSettings::Stdio { - enabled: true, - command: ContextServerCommand { - path: "somebinary".into(), - args: vec!["arg".to_string()], - env: None, - timeout: None, - }, - }, - )], - ) - .await; - - let executor = cx.executor(); - let registry = cx.new(|_| ContextServerDescriptorRegistry::new()); - let store = cx.new(|cx| { - ContextServerStore::test_maintain_server_loop( - Some(Box::new(move |id, _| { - Arc::new(ContextServer::new( - id.clone(), - Arc::new(create_fake_transport(id.0.to_string(), executor.clone())), - )) - })), - registry.clone(), - project.read(cx).worktree_store(), - project.downgrade(), - cx, - ) - }); - - // Ensure that mcp-1 starts up - { - let _server_events = assert_server_events( - &store, - vec![ - (server_1_id.clone(), ContextServerStatus::Starting), - (server_1_id.clone(), ContextServerStatus::Running), - ], - cx, - ); - cx.run_until_parked(); - } - - // Ensure that mcp-1 is stopped once it is disabled. - { - let _server_events = assert_server_events( - &store, - vec![(server_1_id.clone(), ContextServerStatus::Stopped)], - cx, - ); - set_context_server_configuration( - vec![( - server_1_id.0.clone(), - settings::ContextServerSettingsContent::Stdio { - enabled: false, - command: ContextServerCommand { - path: "somebinary".into(), - args: vec!["arg".to_string()], - env: None, - timeout: None, - }, - }, - )], - cx, - ); - - cx.run_until_parked(); - } - - // Ensure that mcp-1 is started once it is enabled again. - { - let _server_events = assert_server_events( - &store, - vec![ - (server_1_id.clone(), ContextServerStatus::Starting), - (server_1_id.clone(), ContextServerStatus::Running), - ], - cx, - ); - set_context_server_configuration( - vec![( - server_1_id.0.clone(), - settings::ContextServerSettingsContent::Stdio { - enabled: true, - command: ContextServerCommand { - path: "somebinary".into(), - args: vec!["arg".to_string()], - timeout: None, - env: None, - }, - }, - )], - cx, - ); - - cx.run_until_parked(); - } - } - - fn set_context_server_configuration( - context_servers: Vec<(Arc, settings::ContextServerSettingsContent)>, - cx: &mut TestAppContext, - ) { - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |content| { - content.project.context_servers.clear(); - for (id, config) in context_servers { - content.project.context_servers.insert(id, config); - } - }); - }) - }); - } - - #[gpui::test] - async fn test_remote_context_server(cx: &mut TestAppContext) { - const SERVER_ID: &str = "remote-server"; - let server_id = ContextServerId(SERVER_ID.into()); - let server_url = "http://example.com/api"; - - let (_fs, project) = setup_context_server_test( - cx, - json!({ "code.rs": "" }), - vec![( - SERVER_ID.into(), - ContextServerSettings::Http { - enabled: true, - url: server_url.to_string(), - headers: Default::default(), - }, - )], - ) - .await; - - let client = FakeHttpClient::create(|_| async move { - use http_client::AsyncBody; - - let response = Response::builder() - .status(200) - .header("Content-Type", "application/json") - .body(AsyncBody::from( - serde_json::to_string(&json!({ - "jsonrpc": "2.0", - "id": 0, - "result": { - "protocolVersion": "2024-11-05", - "capabilities": {}, - "serverInfo": { - "name": "test-server", - "version": "1.0.0" - } - } - })) - .unwrap(), - )) - .unwrap(); - Ok(response) - }); - cx.update(|cx| cx.set_http_client(client)); - let registry = cx.new(|_| ContextServerDescriptorRegistry::new()); - let store = cx.new(|cx| { - ContextServerStore::test_maintain_server_loop( - None, - registry.clone(), - project.read(cx).worktree_store(), - project.downgrade(), - cx, - ) - }); - - let _server_events = assert_server_events( - &store, - vec![ - (server_id.clone(), ContextServerStatus::Starting), - (server_id.clone(), ContextServerStatus::Running), - ], - cx, - ); - cx.run_until_parked(); - } - - struct ServerEvents { - received_event_count: Rc>, - expected_event_count: usize, - _subscription: Subscription, - } - - impl Drop for ServerEvents { - fn drop(&mut self) { - let actual_event_count = *self.received_event_count.borrow(); - assert_eq!( - actual_event_count, self.expected_event_count, - " - Expected to receive {} context server store events, but received {} events", - self.expected_event_count, actual_event_count - ); - } - } - - fn dummy_server_settings() -> ContextServerSettings { - ContextServerSettings::Stdio { - enabled: true, - command: ContextServerCommand { - path: "somebinary".into(), - args: vec!["arg".to_string()], - env: None, - timeout: None, - }, - } - } - - fn assert_server_events( - store: &Entity, - expected_events: Vec<(ContextServerId, ContextServerStatus)>, - cx: &mut TestAppContext, - ) -> ServerEvents { - cx.update(|cx| { - let mut ix = 0; - let received_event_count = Rc::new(RefCell::new(0)); - let expected_event_count = expected_events.len(); - let subscription = cx.subscribe(store, { - let received_event_count = received_event_count.clone(); - move |_, event, _| match event { - Event::ServerStatusChanged { - server_id: actual_server_id, - status: actual_status, - } => { - let (expected_server_id, expected_status) = &expected_events[ix]; - - assert_eq!( - actual_server_id, expected_server_id, - "Expected different server id at index {}", - ix - ); - assert_eq!( - actual_status, expected_status, - "Expected different status at index {}", - ix - ); - ix += 1; - *received_event_count.borrow_mut() += 1; - } - } - }); - ServerEvents { - expected_event_count, - received_event_count, - _subscription: subscription, - } - }) - } - - async fn setup_context_server_test( - cx: &mut TestAppContext, - files: serde_json::Value, - context_server_configurations: Vec<(Arc, ContextServerSettings)>, - ) -> (Arc, Entity) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - let mut settings = ProjectSettings::get_global(cx).clone(); - for (id, config) in context_server_configurations { - settings.context_servers.insert(id, config); - } - ProjectSettings::override_global(settings, cx); - }); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/test"), files).await; - let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await; - - (fs, project) - } - - struct FakeContextServerDescriptor { - path: PathBuf, - } - - impl FakeContextServerDescriptor { - fn new(path: impl Into) -> Self { - Self { path: path.into() } - } - } - - impl ContextServerDescriptor for FakeContextServerDescriptor { - fn command( - &self, - _worktree_store: Entity, - _cx: &AsyncApp, - ) -> Task> { - Task::ready(Ok(ContextServerCommand { - path: self.path.clone(), - args: vec!["arg1".to_string(), "arg2".to_string()], - env: None, - timeout: None, - })) - } - - fn configuration( - &self, - _worktree_store: Entity, - _cx: &AsyncApp, - ) -> Task>> { - Task::ready(Ok(None)) - } - } -} diff --git a/crates/project/src/context_server_store/extension.rs b/crates/project/src/context_server_store/extension.rs deleted file mode 100644 index ca5cacf3b5..0000000000 --- a/crates/project/src/context_server_store/extension.rs +++ /dev/null @@ -1,120 +0,0 @@ -use std::sync::Arc; - -use anyhow::Result; -use context_server::ContextServerCommand; -use extension::{ - ContextServerConfiguration, Extension, ExtensionContextServerProxy, ExtensionHostProxy, - ProjectDelegate, -}; -use gpui::{App, AsyncApp, Entity, Task}; - -use crate::worktree_store::WorktreeStore; - -use super::registry::{self, ContextServerDescriptorRegistry}; - -pub fn init(cx: &mut App) { - let proxy = ExtensionHostProxy::default_global(cx); - proxy.register_context_server_proxy(ContextServerDescriptorRegistryProxy { - context_server_factory_registry: ContextServerDescriptorRegistry::default_global(cx), - }); -} - -struct ExtensionProject { - worktree_ids: Vec, -} - -impl ProjectDelegate for ExtensionProject { - fn worktree_ids(&self) -> Vec { - self.worktree_ids.clone() - } -} - -struct ContextServerDescriptor { - id: Arc, - extension: Arc, -} - -fn extension_project( - worktree_store: Entity, - cx: &mut AsyncApp, -) -> Result> { - worktree_store.update(cx, |worktree_store, cx| { - Arc::new(ExtensionProject { - worktree_ids: worktree_store - .visible_worktrees(cx) - .map(|worktree| worktree.read(cx).id().to_proto()) - .collect(), - }) - }) -} - -impl registry::ContextServerDescriptor for ContextServerDescriptor { - fn command( - &self, - worktree_store: Entity, - cx: &AsyncApp, - ) -> Task> { - let id = self.id.clone(); - let extension = self.extension.clone(); - cx.spawn(async move |cx| { - let extension_project = extension_project(worktree_store, cx)?; - let mut command = extension - .context_server_command(id.clone(), extension_project.clone()) - .await?; - command.command = extension.path_from_extension(&command.command); - - log::debug!("loaded command for context server {id}: {command:?}"); - - Ok(ContextServerCommand { - path: command.command, - args: command.args, - env: Some(command.env.into_iter().collect()), - timeout: None, - }) - }) - } - - fn configuration( - &self, - worktree_store: Entity, - cx: &AsyncApp, - ) -> Task>> { - let id = self.id.clone(); - let extension = self.extension.clone(); - cx.spawn(async move |cx| { - let extension_project = extension_project(worktree_store, cx)?; - let configuration = extension - .context_server_configuration(id.clone(), extension_project) - .await?; - - log::debug!("loaded configuration for context server {id}: {configuration:?}"); - - Ok(configuration) - }) - } -} - -struct ContextServerDescriptorRegistryProxy { - context_server_factory_registry: Entity, -} - -impl ExtensionContextServerProxy for ContextServerDescriptorRegistryProxy { - fn register_context_server(&self, extension: Arc, id: Arc, cx: &mut App) { - self.context_server_factory_registry - .update(cx, |registry, cx| { - registry.register_context_server_descriptor( - id.clone(), - Arc::new(ContextServerDescriptor { id, extension }) - as Arc, - cx, - ) - }); - } - - fn unregister_context_server(&self, server_id: Arc, cx: &mut App) { - self.context_server_factory_registry - .update(cx, |registry, cx| { - registry.unregister_context_server_descriptor_by_id(&server_id, cx) - }); - } -} diff --git a/crates/project/src/context_server_store/registry.rs b/crates/project/src/context_server_store/registry.rs deleted file mode 100644 index b705fcadee..0000000000 --- a/crates/project/src/context_server_store/registry.rs +++ /dev/null @@ -1,84 +0,0 @@ -use std::sync::Arc; - -use anyhow::Result; -use collections::HashMap; -use context_server::ContextServerCommand; -use extension::ContextServerConfiguration; -use gpui::{App, AppContext as _, AsyncApp, Context, Entity, Global, Task}; - -use crate::worktree_store::WorktreeStore; - -pub trait ContextServerDescriptor { - fn command( - &self, - worktree_store: Entity, - cx: &AsyncApp, - ) -> Task>; - fn configuration( - &self, - worktree_store: Entity, - cx: &AsyncApp, - ) -> Task>>; -} - -struct GlobalContextServerDescriptorRegistry(Entity); - -impl Global for GlobalContextServerDescriptorRegistry {} - -#[derive(Default)] -pub struct ContextServerDescriptorRegistry { - context_servers: HashMap, Arc>, -} - -impl ContextServerDescriptorRegistry { - /// Returns the global [`ContextServerDescriptorRegistry`]. - /// - /// Inserts a default [`ContextServerDescriptorRegistry`] if one does not yet exist. - pub fn default_global(cx: &mut App) -> Entity { - if !cx.has_global::() { - let registry = cx.new(|_| Self::new()); - cx.set_global(GlobalContextServerDescriptorRegistry(registry)); - } - cx.global::() - .0 - .clone() - } - - pub fn new() -> Self { - Self { - context_servers: HashMap::default(), - } - } - - pub fn context_server_descriptors(&self) -> Vec<(Arc, Arc)> { - self.context_servers - .iter() - .map(|(id, factory)| (id.clone(), factory.clone())) - .collect() - } - - pub fn context_server_descriptor(&self, id: &str) -> Option> { - self.context_servers.get(id).cloned() - } - - /// Registers the provided [`ContextServerDescriptor`]. - pub fn register_context_server_descriptor( - &mut self, - id: Arc, - descriptor: Arc, - cx: &mut Context, - ) { - self.context_servers.insert(id, descriptor); - cx.notify(); - } - - /// Unregisters the [`ContextServerDescriptor`] for the server with the given ID. - pub fn unregister_context_server_descriptor_by_id( - &mut self, - server_id: &str, - cx: &mut Context, - ) { - self.context_servers.remove(server_id); - cx.notify(); - } -} diff --git a/crates/project/src/debounced_delay.rs b/crates/project/src/debounced_delay.rs deleted file mode 100644 index 0ea045be5f..0000000000 --- a/crates/project/src/debounced_delay.rs +++ /dev/null @@ -1,54 +0,0 @@ -use futures::{FutureExt, channel::oneshot}; -use gpui::{Context, Task}; -use std::{marker::PhantomData, time::Duration}; - -pub struct DebouncedDelay { - task: Option>, - cancel_channel: Option>, - _phantom_data: PhantomData, -} - -impl Default for DebouncedDelay { - fn default() -> Self { - Self::new() - } -} - -impl DebouncedDelay { - pub fn new() -> Self { - Self { - task: None, - cancel_channel: None, - _phantom_data: PhantomData, - } - } - - pub fn fire_new(&mut self, delay: Duration, cx: &mut Context, func: F) - where - F: 'static + Send + FnOnce(&mut E, &mut Context) -> Task<()>, - { - if let Some(channel) = self.cancel_channel.take() { - _ = channel.send(()); - } - - let (sender, mut receiver) = oneshot::channel::<()>(); - self.cancel_channel = Some(sender); - - let previous_task = self.task.take(); - self.task = Some(cx.spawn(async move |entity, cx| { - let mut timer = cx.background_executor().timer(delay).fuse(); - if let Some(previous_task) = previous_task { - previous_task.await; - } - - futures::select_biased! { - _ = receiver => return, - _ = timer => {} - } - - if let Ok(task) = entity.update(cx, |project, cx| (func)(project, cx)) { - task.await; - } - })); - } -} diff --git a/crates/project/src/debugger.rs b/crates/project/src/debugger.rs deleted file mode 100644 index 0bf6a0d61b..0000000000 --- a/crates/project/src/debugger.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! Zed's debugger data layer is implemented in terms of 3 concepts: -//! - DAP store - that knows about all of the available debug sessions. -//! - Debug sessions - that bear responsibility of communicating with debug adapters and managing the state of each individual session. -//! For the most part it is agnostic over the communication layer (it'll use RPC for peers and actual DAP requests for the host). -//! - Breakpoint store - that knows about all breakpoints set for a project. -//! -//! There are few reasons for this divide: -//! - Breakpoints persist across debug sessions and they're not really specific to any particular session. Sure, we have to send protocol messages for them -//! (so they're a "thing" in the protocol), but we also want to set them before any session starts up. -//! - Debug clients are doing the heavy lifting, and this is where UI grabs all of it's data from. They also rely on breakpoint store during initialization to obtain -//! current set of breakpoints. -//! - Since DAP store knows about all of the available debug sessions, it is responsible for routing RPC requests to sessions. It also knows how to find adapters for particular kind of session. - -pub mod breakpoint_store; -pub mod dap_command; -pub mod dap_store; -pub mod locators; -mod memory; -pub mod session; - -#[cfg(any(feature = "test-support", test))] -pub mod test; -pub use memory::MemoryCell; diff --git a/crates/project/src/debugger/breakpoint_store.rs b/crates/project/src/debugger/breakpoint_store.rs deleted file mode 100644 index 42663ab985..0000000000 --- a/crates/project/src/debugger/breakpoint_store.rs +++ /dev/null @@ -1,1034 +0,0 @@ -//! Module for managing breakpoints in a project. -//! -//! Breakpoints are separate from a session because they're not associated with any particular debug session. They can also be set up without a session running. -use anyhow::{Context as _, Result}; -pub use breakpoints_in_file::{BreakpointSessionState, BreakpointWithPosition}; -use breakpoints_in_file::{BreakpointsInFile, StatefulBreakpoint}; -use collections::{BTreeMap, HashMap}; -use dap::{StackFrameId, client::SessionId}; -use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, Subscription, Task}; -use itertools::Itertools; -use language::{Buffer, BufferSnapshot, proto::serialize_anchor as serialize_text_anchor}; -use rpc::{ - AnyProtoClient, TypedEnvelope, - proto::{self}, -}; -use std::{hash::Hash, ops::Range, path::Path, sync::Arc, u32}; -use text::{Point, PointUtf16}; -use util::maybe; - -use crate::{Project, ProjectPath, buffer_store::BufferStore, worktree_store::WorktreeStore}; - -use super::session::ThreadId; - -mod breakpoints_in_file { - use collections::HashMap; - use language::{BufferEvent, DiskState}; - - use super::*; - - #[derive(Clone, Debug, PartialEq, Eq)] - pub struct BreakpointWithPosition { - pub position: text::Anchor, - pub bp: Breakpoint, - } - - /// A breakpoint with per-session data about it's state (as seen by the Debug Adapter). - #[derive(Clone, Debug)] - pub struct StatefulBreakpoint { - pub bp: BreakpointWithPosition, - pub session_state: HashMap, - } - - impl StatefulBreakpoint { - pub(super) fn new(bp: BreakpointWithPosition) -> Self { - Self { - bp, - session_state: Default::default(), - } - } - pub(super) fn position(&self) -> &text::Anchor { - &self.bp.position - } - } - - #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] - pub struct BreakpointSessionState { - /// Session-specific identifier for the breakpoint, as assigned by Debug Adapter. - pub id: u64, - pub verified: bool, - } - #[derive(Clone)] - pub(super) struct BreakpointsInFile { - pub(super) buffer: Entity, - // TODO: This is.. less than ideal, as it's O(n) and does not return entries in order. We'll have to change TreeMap to support passing in the context for comparisons - pub(super) breakpoints: Vec, - _subscription: Arc, - } - - impl BreakpointsInFile { - pub(super) fn new(buffer: Entity, cx: &mut Context) -> Self { - let subscription = Arc::from(cx.subscribe( - &buffer, - |breakpoint_store, buffer, event, cx| match event { - BufferEvent::Saved => { - if let Some(abs_path) = BreakpointStore::abs_path_from_buffer(&buffer, cx) { - cx.emit(BreakpointStoreEvent::BreakpointsUpdated( - abs_path, - BreakpointUpdatedReason::FileSaved, - )); - } - } - BufferEvent::FileHandleChanged => { - let entity_id = buffer.entity_id(); - - if buffer.read(cx).file().is_none_or(|f| f.disk_state() == DiskState::Deleted) { - breakpoint_store.breakpoints.retain(|_, breakpoints_in_file| { - breakpoints_in_file.buffer.entity_id() != entity_id - }); - - cx.notify(); - return; - } - - if let Some(abs_path) = BreakpointStore::abs_path_from_buffer(&buffer, cx) { - if breakpoint_store.breakpoints.contains_key(&abs_path) { - return; - } - - if let Some(old_path) = breakpoint_store - .breakpoints - .iter() - .find(|(_, in_file)| in_file.buffer.entity_id() == entity_id) - .map(|values| values.0) - .cloned() - { - let Some(breakpoints_in_file) = - breakpoint_store.breakpoints.remove(&old_path) else { - log::error!("Couldn't get breakpoints in file from old path during buffer rename handling"); - return; - }; - - breakpoint_store.breakpoints.insert(abs_path, breakpoints_in_file); - cx.notify(); - } - } - } - _ => {} - }, - )); - - BreakpointsInFile { - buffer, - breakpoints: Vec::new(), - _subscription: subscription, - } - } - } -} - -#[derive(Clone)] -struct RemoteBreakpointStore { - upstream_client: AnyProtoClient, - _upstream_project_id: u64, -} - -#[derive(Clone)] -struct LocalBreakpointStore { - worktree_store: Entity, - buffer_store: Entity, -} - -#[derive(Clone)] -enum BreakpointStoreMode { - Local(LocalBreakpointStore), - Remote(RemoteBreakpointStore), -} - -#[derive(Clone, PartialEq)] -pub struct ActiveStackFrame { - pub session_id: SessionId, - pub thread_id: ThreadId, - pub stack_frame_id: StackFrameId, - pub path: Arc, - pub position: text::Anchor, -} - -pub struct BreakpointStore { - breakpoints: BTreeMap, BreakpointsInFile>, - downstream_client: Option<(AnyProtoClient, u64)>, - active_stack_frame: Option, - // E.g ssh - mode: BreakpointStoreMode, -} - -impl BreakpointStore { - pub fn init(client: &AnyProtoClient) { - client.add_entity_request_handler(Self::handle_toggle_breakpoint); - client.add_entity_message_handler(Self::handle_breakpoints_for_file); - } - pub fn local(worktree_store: Entity, buffer_store: Entity) -> Self { - BreakpointStore { - breakpoints: BTreeMap::new(), - mode: BreakpointStoreMode::Local(LocalBreakpointStore { - worktree_store, - buffer_store, - }), - downstream_client: None, - active_stack_frame: Default::default(), - } - } - - pub(crate) fn remote(upstream_project_id: u64, upstream_client: AnyProtoClient) -> Self { - BreakpointStore { - breakpoints: BTreeMap::new(), - mode: BreakpointStoreMode::Remote(RemoteBreakpointStore { - upstream_client, - _upstream_project_id: upstream_project_id, - }), - downstream_client: None, - active_stack_frame: Default::default(), - } - } - - pub(crate) fn shared(&mut self, project_id: u64, downstream_client: AnyProtoClient) { - self.downstream_client = Some((downstream_client, project_id)); - } - - pub(crate) fn unshared(&mut self, cx: &mut Context) { - self.downstream_client.take(); - - cx.notify(); - } - - async fn handle_breakpoints_for_file( - this: Entity, - message: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - let breakpoints = cx.update(|cx| this.read(cx).breakpoint_store())?; - if message.payload.breakpoints.is_empty() { - return Ok(()); - } - - let buffer = this - .update(&mut cx, |this, cx| { - let path = - this.project_path_for_absolute_path(message.payload.path.as_ref(), cx)?; - Some(this.open_buffer(path, cx)) - }) - .ok() - .flatten() - .context("Invalid project path")? - .await?; - - breakpoints.update(&mut cx, move |this, cx| { - let bps = this - .breakpoints - .entry(Arc::::from(message.payload.path.as_ref())) - .or_insert_with(|| BreakpointsInFile::new(buffer, cx)); - - bps.breakpoints = message - .payload - .breakpoints - .into_iter() - .filter_map(|breakpoint| { - let position = - language::proto::deserialize_anchor(breakpoint.position.clone()?)?; - let session_state = breakpoint - .session_state - .iter() - .map(|(session_id, state)| { - let state = BreakpointSessionState { - id: state.id, - verified: state.verified, - }; - (SessionId::from_proto(*session_id), state) - }) - .collect(); - let breakpoint = Breakpoint::from_proto(breakpoint)?; - let bp = BreakpointWithPosition { - position, - bp: breakpoint, - }; - - Some(StatefulBreakpoint { bp, session_state }) - }) - .collect(); - - cx.notify(); - })?; - - Ok(()) - } - - async fn handle_toggle_breakpoint( - this: Entity, - message: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let breakpoints = this.read_with(&cx, |this, _| this.breakpoint_store())?; - let path = this - .update(&mut cx, |this, cx| { - this.project_path_for_absolute_path(message.payload.path.as_ref(), cx) - })? - .context("Could not resolve provided abs path")?; - let buffer = this - .update(&mut cx, |this, cx| { - this.buffer_store().read(cx).get_by_path(&path) - })? - .context("Could not find buffer for a given path")?; - let breakpoint = message - .payload - .breakpoint - .context("Breakpoint not present in RPC payload")?; - let position = language::proto::deserialize_anchor( - breakpoint - .position - .clone() - .context("Anchor not present in RPC payload")?, - ) - .context("Anchor deserialization failed")?; - let breakpoint = - Breakpoint::from_proto(breakpoint).context("Could not deserialize breakpoint")?; - - breakpoints.update(&mut cx, |this, cx| { - this.toggle_breakpoint( - buffer, - BreakpointWithPosition { - position, - bp: breakpoint, - }, - BreakpointEditAction::Toggle, - cx, - ); - })?; - Ok(proto::Ack {}) - } - - pub(crate) fn broadcast(&self) { - if let Some((client, project_id)) = &self.downstream_client { - for (path, breakpoint_set) in &self.breakpoints { - let _ = client.send(proto::BreakpointsForFile { - project_id: *project_id, - path: path.to_str().map(ToOwned::to_owned).unwrap(), - breakpoints: breakpoint_set - .breakpoints - .iter() - .filter_map(|breakpoint| { - breakpoint.bp.bp.to_proto( - path, - breakpoint.position(), - &breakpoint.session_state, - ) - }) - .collect(), - }); - } - } - } - - pub(crate) fn update_session_breakpoint( - &mut self, - session_id: SessionId, - _: dap::BreakpointEventReason, - breakpoint: dap::Breakpoint, - ) { - maybe!({ - let event_id = breakpoint.id?; - - let state = self - .breakpoints - .values_mut() - .find_map(|breakpoints_in_file| { - breakpoints_in_file - .breakpoints - .iter_mut() - .find_map(|state| { - let state = state.session_state.get_mut(&session_id)?; - - if state.id == event_id { - Some(state) - } else { - None - } - }) - })?; - - state.verified = breakpoint.verified; - Some(()) - }); - } - - pub(super) fn mark_breakpoints_verified( - &mut self, - session_id: SessionId, - abs_path: &Path, - - it: impl Iterator, - ) { - maybe!({ - let breakpoints = self.breakpoints.get_mut(abs_path)?; - for (breakpoint, state) in it { - if let Some(to_update) = breakpoints - .breakpoints - .iter_mut() - .find(|bp| *bp.position() == breakpoint.position) - { - to_update - .session_state - .entry(session_id) - .insert_entry(state); - } - } - Some(()) - }); - } - - pub fn abs_path_from_buffer(buffer: &Entity, cx: &App) -> Option> { - worktree::File::from_dyn(buffer.read(cx).file()) - .map(|file| file.worktree.read(cx).absolutize(&file.path)) - .map(Arc::::from) - } - - pub fn toggle_breakpoint( - &mut self, - buffer: Entity, - mut breakpoint: BreakpointWithPosition, - edit_action: BreakpointEditAction, - cx: &mut Context, - ) { - let Some(abs_path) = Self::abs_path_from_buffer(&buffer, cx) else { - return; - }; - - let breakpoint_set = self - .breakpoints - .entry(abs_path.clone()) - .or_insert_with(|| BreakpointsInFile::new(buffer, cx)); - - match edit_action { - BreakpointEditAction::Toggle => { - let len_before = breakpoint_set.breakpoints.len(); - breakpoint_set - .breakpoints - .retain(|value| breakpoint != value.bp); - if len_before == breakpoint_set.breakpoints.len() { - // We did not remove any breakpoint, hence let's toggle one. - breakpoint_set - .breakpoints - .push(StatefulBreakpoint::new(breakpoint.clone())); - } - } - BreakpointEditAction::InvertState => { - if let Some(bp) = breakpoint_set - .breakpoints - .iter_mut() - .find(|value| breakpoint == value.bp) - { - let bp = &mut bp.bp.bp; - if bp.is_enabled() { - bp.state = BreakpointState::Disabled; - } else { - bp.state = BreakpointState::Enabled; - } - } else { - breakpoint.bp.state = BreakpointState::Disabled; - breakpoint_set - .breakpoints - .push(StatefulBreakpoint::new(breakpoint.clone())); - } - } - BreakpointEditAction::EditLogMessage(log_message) => { - if !log_message.is_empty() { - let found_bp = breakpoint_set.breakpoints.iter_mut().find_map(|bp| { - if breakpoint.position == *bp.position() { - Some(&mut bp.bp.bp) - } else { - None - } - }); - - if let Some(found_bp) = found_bp { - found_bp.message = Some(log_message); - } else { - breakpoint.bp.message = Some(log_message); - // We did not remove any breakpoint, hence let's toggle one. - breakpoint_set - .breakpoints - .push(StatefulBreakpoint::new(breakpoint.clone())); - } - } else if breakpoint.bp.message.is_some() { - if let Some(position) = breakpoint_set - .breakpoints - .iter() - .find_position(|other| breakpoint == other.bp) - .map(|res| res.0) - { - breakpoint_set.breakpoints.remove(position); - } else { - log::error!("Failed to find position of breakpoint to delete") - } - } - } - BreakpointEditAction::EditHitCondition(hit_condition) => { - if !hit_condition.is_empty() { - let found_bp = breakpoint_set.breakpoints.iter_mut().find_map(|other| { - if breakpoint.position == *other.position() { - Some(&mut other.bp.bp) - } else { - None - } - }); - - if let Some(found_bp) = found_bp { - found_bp.hit_condition = Some(hit_condition); - } else { - breakpoint.bp.hit_condition = Some(hit_condition); - // We did not remove any breakpoint, hence let's toggle one. - breakpoint_set - .breakpoints - .push(StatefulBreakpoint::new(breakpoint.clone())) - } - } else if breakpoint.bp.hit_condition.is_some() { - if let Some(position) = breakpoint_set - .breakpoints - .iter() - .find_position(|bp| breakpoint == bp.bp) - .map(|res| res.0) - { - breakpoint_set.breakpoints.remove(position); - } else { - log::error!("Failed to find position of breakpoint to delete") - } - } - } - BreakpointEditAction::EditCondition(condition) => { - if !condition.is_empty() { - let found_bp = breakpoint_set.breakpoints.iter_mut().find_map(|other| { - if breakpoint.position == *other.position() { - Some(&mut other.bp.bp) - } else { - None - } - }); - - if let Some(found_bp) = found_bp { - found_bp.condition = Some(condition); - } else { - breakpoint.bp.condition = Some(condition); - // We did not remove any breakpoint, hence let's toggle one. - breakpoint_set - .breakpoints - .push(StatefulBreakpoint::new(breakpoint.clone())); - } - } else if breakpoint.bp.condition.is_some() { - if let Some(position) = breakpoint_set - .breakpoints - .iter() - .find_position(|bp| breakpoint == bp.bp) - .map(|res| res.0) - { - breakpoint_set.breakpoints.remove(position); - } else { - log::error!("Failed to find position of breakpoint to delete") - } - } - } - } - - if breakpoint_set.breakpoints.is_empty() { - self.breakpoints.remove(&abs_path); - } - if let BreakpointStoreMode::Remote(remote) = &self.mode { - if let Some(breakpoint) = - breakpoint - .bp - .to_proto(&abs_path, &breakpoint.position, &HashMap::default()) - { - cx.background_spawn(remote.upstream_client.request(proto::ToggleBreakpoint { - project_id: remote._upstream_project_id, - path: abs_path.to_str().map(ToOwned::to_owned).unwrap(), - breakpoint: Some(breakpoint), - })) - .detach(); - } - } else if let Some((client, project_id)) = &self.downstream_client { - let breakpoints = self - .breakpoints - .get(&abs_path) - .map(|breakpoint_set| { - breakpoint_set - .breakpoints - .iter() - .filter_map(|bp| { - bp.bp - .bp - .to_proto(&abs_path, bp.position(), &bp.session_state) - }) - .collect() - }) - .unwrap_or_default(); - - let _ = client.send(proto::BreakpointsForFile { - project_id: *project_id, - path: abs_path.to_str().map(ToOwned::to_owned).unwrap(), - breakpoints, - }); - } - - cx.emit(BreakpointStoreEvent::BreakpointsUpdated( - abs_path, - BreakpointUpdatedReason::Toggled, - )); - cx.notify(); - } - - pub fn on_file_rename( - &mut self, - old_path: Arc, - new_path: Arc, - cx: &mut Context, - ) { - if let Some(breakpoints) = self.breakpoints.remove(&old_path) { - self.breakpoints.insert(new_path, breakpoints); - - cx.notify(); - } - } - - pub fn clear_breakpoints(&mut self, cx: &mut Context) { - let breakpoint_paths = self.breakpoints.keys().cloned().collect(); - self.breakpoints.clear(); - cx.emit(BreakpointStoreEvent::BreakpointsCleared(breakpoint_paths)); - } - - pub fn breakpoints<'a>( - &'a self, - buffer: &'a Entity, - range: Option>, - buffer_snapshot: &'a BufferSnapshot, - cx: &App, - ) -> impl Iterator)> + 'a - { - let abs_path = Self::abs_path_from_buffer(buffer, cx); - let active_session_id = self - .active_stack_frame - .as_ref() - .map(|frame| frame.session_id); - abs_path - .and_then(|path| self.breakpoints.get(&path)) - .into_iter() - .flat_map(move |file_breakpoints| { - file_breakpoints.breakpoints.iter().filter_map({ - let range = range.clone(); - move |bp| { - if let Some(range) = &range - && (bp.position().cmp(&range.start, buffer_snapshot).is_lt() - || bp.position().cmp(&range.end, buffer_snapshot).is_gt()) - { - return None; - } - let session_state = active_session_id - .and_then(|id| bp.session_state.get(&id)) - .copied(); - Some((&bp.bp, session_state)) - } - }) - }) - } - - pub fn active_position(&self) -> Option<&ActiveStackFrame> { - self.active_stack_frame.as_ref() - } - - pub fn remove_active_position( - &mut self, - session_id: Option, - cx: &mut Context, - ) { - if let Some(session_id) = session_id { - self.active_stack_frame - .take_if(|active_stack_frame| active_stack_frame.session_id == session_id); - } else { - self.active_stack_frame.take(); - } - - cx.emit(BreakpointStoreEvent::ClearDebugLines); - cx.notify(); - } - - pub fn set_active_position(&mut self, position: ActiveStackFrame, cx: &mut Context) { - if self - .active_stack_frame - .as_ref() - .is_some_and(|active_position| active_position == &position) - { - cx.emit(BreakpointStoreEvent::SetDebugLine); - return; - } - - if self.active_stack_frame.is_some() { - cx.emit(BreakpointStoreEvent::ClearDebugLines); - } - - self.active_stack_frame = Some(position); - - cx.emit(BreakpointStoreEvent::SetDebugLine); - cx.notify(); - } - - pub fn breakpoint_at_row( - &self, - path: &Path, - row: u32, - cx: &App, - ) -> Option<(Entity, BreakpointWithPosition)> { - self.breakpoints.get(path).and_then(|breakpoints| { - let snapshot = breakpoints.buffer.read(cx).text_snapshot(); - - breakpoints - .breakpoints - .iter() - .find(|bp| bp.position().summary::(&snapshot).row == row) - .map(|breakpoint| (breakpoints.buffer.clone(), breakpoint.bp.clone())) - }) - } - - pub fn breakpoints_from_path(&self, path: &Arc) -> Vec { - self.breakpoints - .get(path) - .map(|bp| bp.breakpoints.iter().map(|bp| bp.bp.clone()).collect()) - .unwrap_or_default() - } - - pub fn source_breakpoints_from_path( - &self, - path: &Arc, - cx: &App, - ) -> Vec { - self.breakpoints - .get(path) - .map(|bp| { - let snapshot = bp.buffer.read(cx).snapshot(); - bp.breakpoints - .iter() - .map(|bp| { - let position = snapshot.summary_for_anchor::(bp.position()).row; - let bp = &bp.bp; - SourceBreakpoint { - row: position, - path: path.clone(), - state: bp.bp.state, - message: bp.bp.message.clone(), - condition: bp.bp.condition.clone(), - hit_condition: bp.bp.hit_condition.clone(), - } - }) - .collect() - }) - .unwrap_or_default() - } - - pub fn all_breakpoints(&self) -> BTreeMap, Vec> { - self.breakpoints - .iter() - .map(|(path, bp)| { - ( - path.clone(), - bp.breakpoints.iter().map(|bp| bp.bp.clone()).collect(), - ) - }) - .collect() - } - pub fn all_source_breakpoints(&self, cx: &App) -> BTreeMap, Vec> { - self.breakpoints - .iter() - .map(|(path, bp)| { - let snapshot = bp.buffer.read(cx).snapshot(); - ( - path.clone(), - bp.breakpoints - .iter() - .map(|breakpoint| { - let position = snapshot - .summary_for_anchor::(breakpoint.position()) - .row; - let breakpoint = &breakpoint.bp; - SourceBreakpoint { - row: position, - path: path.clone(), - message: breakpoint.bp.message.clone(), - state: breakpoint.bp.state, - hit_condition: breakpoint.bp.hit_condition.clone(), - condition: breakpoint.bp.condition.clone(), - } - }) - .collect(), - ) - }) - .collect() - } - - pub fn with_serialized_breakpoints( - &self, - breakpoints: BTreeMap, Vec>, - cx: &mut Context, - ) -> Task> { - if let BreakpointStoreMode::Local(mode) = &self.mode { - let mode = mode.clone(); - cx.spawn(async move |this, cx| { - let mut new_breakpoints = BTreeMap::default(); - for (path, bps) in breakpoints { - if bps.is_empty() { - continue; - } - let (worktree, relative_path) = mode - .worktree_store - .update(cx, |this, cx| { - this.find_or_create_worktree(&path, false, cx) - })? - .await?; - let buffer = mode - .buffer_store - .update(cx, |this, cx| { - let path = ProjectPath { - worktree_id: worktree.read(cx).id(), - path: relative_path, - }; - this.open_buffer(path, cx) - })? - .await; - let Ok(buffer) = buffer else { - log::error!("Todo: Serialized breakpoints which do not have buffer (yet)"); - continue; - }; - let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?; - - let mut breakpoints_for_file = - this.update(cx, |_, cx| BreakpointsInFile::new(buffer, cx))?; - - for bp in bps { - let max_point = snapshot.max_point_utf16(); - let point = PointUtf16::new(bp.row, 0); - if point > max_point { - log::error!("skipping a deserialized breakpoint that's out of range"); - continue; - } - let position = snapshot.anchor_after(point); - breakpoints_for_file - .breakpoints - .push(StatefulBreakpoint::new(BreakpointWithPosition { - position, - bp: Breakpoint { - message: bp.message, - state: bp.state, - condition: bp.condition, - hit_condition: bp.hit_condition, - }, - })) - } - new_breakpoints.insert(path, breakpoints_for_file); - } - this.update(cx, |this, cx| { - for (path, count) in new_breakpoints.iter().map(|(path, bp_in_file)| { - (path.to_string_lossy(), bp_in_file.breakpoints.len()) - }) { - let breakpoint_str = if count > 1 { - "breakpoints" - } else { - "breakpoint" - }; - log::debug!("Deserialized {count} {breakpoint_str} at path: {path}"); - } - - this.breakpoints = new_breakpoints; - - cx.notify(); - })?; - - Ok(()) - }) - } else { - Task::ready(Ok(())) - } - } - - #[cfg(any(test, feature = "test-support"))] - pub(crate) fn breakpoint_paths(&self) -> Vec> { - self.breakpoints.keys().cloned().collect() - } -} - -#[derive(Clone, Copy)] -pub enum BreakpointUpdatedReason { - Toggled, - FileSaved, -} - -pub enum BreakpointStoreEvent { - SetDebugLine, - ClearDebugLines, - BreakpointsUpdated(Arc, BreakpointUpdatedReason), - BreakpointsCleared(Vec>), -} - -impl EventEmitter for BreakpointStore {} - -type BreakpointMessage = Arc; - -#[derive(Clone, Debug)] -pub enum BreakpointEditAction { - Toggle, - InvertState, - EditLogMessage(BreakpointMessage), - EditCondition(BreakpointMessage), - EditHitCondition(BreakpointMessage), -} - -#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] -pub enum BreakpointState { - Enabled, - Disabled, -} - -impl BreakpointState { - #[inline] - pub fn is_enabled(&self) -> bool { - matches!(self, BreakpointState::Enabled) - } - - #[inline] - pub fn is_disabled(&self) -> bool { - matches!(self, BreakpointState::Disabled) - } - - #[inline] - pub fn to_int(self) -> i32 { - match self { - BreakpointState::Enabled => 0, - BreakpointState::Disabled => 1, - } - } -} - -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub struct Breakpoint { - pub message: Option, - /// How many times do we hit the breakpoint until we actually stop at it e.g. (2 = 2 times of the breakpoint action) - pub hit_condition: Option>, - pub condition: Option, - pub state: BreakpointState, -} - -impl Breakpoint { - pub fn new_standard() -> Self { - Self { - state: BreakpointState::Enabled, - hit_condition: None, - condition: None, - message: None, - } - } - - pub fn new_condition(hit_condition: &str) -> Self { - Self { - state: BreakpointState::Enabled, - condition: None, - hit_condition: Some(hit_condition.into()), - message: None, - } - } - - pub fn new_log(log_message: &str) -> Self { - Self { - state: BreakpointState::Enabled, - hit_condition: None, - condition: None, - message: Some(log_message.into()), - } - } - - fn to_proto( - &self, - _path: &Path, - position: &text::Anchor, - session_states: &HashMap, - ) -> Option { - Some(client::proto::Breakpoint { - position: Some(serialize_text_anchor(position)), - state: match self.state { - BreakpointState::Enabled => proto::BreakpointState::Enabled.into(), - BreakpointState::Disabled => proto::BreakpointState::Disabled.into(), - }, - message: self.message.as_ref().map(|s| String::from(s.as_ref())), - condition: self.condition.as_ref().map(|s| String::from(s.as_ref())), - hit_condition: self - .hit_condition - .as_ref() - .map(|s| String::from(s.as_ref())), - session_state: session_states - .iter() - .map(|(session_id, state)| { - ( - session_id.to_proto(), - proto::BreakpointSessionState { - id: state.id, - verified: state.verified, - }, - ) - }) - .collect(), - }) - } - - fn from_proto(breakpoint: client::proto::Breakpoint) -> Option { - Some(Self { - state: match proto::BreakpointState::from_i32(breakpoint.state) { - Some(proto::BreakpointState::Disabled) => BreakpointState::Disabled, - None | Some(proto::BreakpointState::Enabled) => BreakpointState::Enabled, - }, - message: breakpoint.message.map(Into::into), - condition: breakpoint.condition.map(Into::into), - hit_condition: breakpoint.hit_condition.map(Into::into), - }) - } - - #[inline] - pub fn is_enabled(&self) -> bool { - self.state.is_enabled() - } - - #[inline] - pub fn is_disabled(&self) -> bool { - self.state.is_disabled() - } -} - -/// Breakpoint for location within source code. -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub struct SourceBreakpoint { - pub row: u32, - pub path: Arc, - pub message: Option>, - pub condition: Option>, - pub hit_condition: Option>, - pub state: BreakpointState, -} - -impl From for dap::SourceBreakpoint { - fn from(bp: SourceBreakpoint) -> Self { - Self { - line: bp.row as u64 + 1, - column: None, - condition: bp - .condition - .map(|condition| String::from(condition.as_ref())), - hit_condition: bp - .hit_condition - .map(|hit_condition| String::from(hit_condition.as_ref())), - log_message: bp.message.map(|message| String::from(message.as_ref())), - mode: None, - } - } -} diff --git a/crates/project/src/debugger/dap_command.rs b/crates/project/src/debugger/dap_command.rs deleted file mode 100644 index 772ff2dcfe..0000000000 --- a/crates/project/src/debugger/dap_command.rs +++ /dev/null @@ -1,1975 +0,0 @@ -use std::sync::Arc; - -use anyhow::{Context as _, Ok, Result}; -use base64::Engine; -use dap::{ - Capabilities, ContinueArguments, ExceptionFilterOptions, InitializeRequestArguments, - InitializeRequestArgumentsPathFormat, NextArguments, SetVariableResponse, SourceBreakpoint, - StepInArguments, StepOutArguments, SteppingGranularity, ValueFormat, Variable, - VariablesArgumentsFilter, - client::SessionId, - proto_conversions::ProtoConversion, - requests::{Continue, Next}, -}; - -use rpc::proto; -use serde_json::Value; -use util::ResultExt; - -pub trait LocalDapCommand: 'static + Send + Sync + std::fmt::Debug { - type Response: 'static + Send + std::fmt::Debug; - type DapRequest: 'static + Send + dap::requests::Request; - /// Is this request idempotent? Is it safe to cache the response for as long as the execution environment is unchanged? - const CACHEABLE: bool = false; - - fn is_supported(_capabilities: &Capabilities) -> bool { - true - } - - fn to_dap(&self) -> ::Arguments; - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result; -} - -pub trait DapCommand: LocalDapCommand { - type ProtoRequest: 'static + Send; - type ProtoResponse: 'static + Send; - - #[allow(dead_code)] - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId; - - #[allow(dead_code)] - fn from_proto(request: &Self::ProtoRequest) -> Self; - - #[allow(unused)] - fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest; - - #[allow(dead_code)] - fn response_to_proto( - debug_client_id: SessionId, - message: Self::Response, - ) -> Self::ProtoResponse; - - #[allow(unused)] - fn response_from_proto(&self, message: Self::ProtoResponse) -> Result; -} - -impl LocalDapCommand for Arc { - type Response = T::Response; - type DapRequest = T::DapRequest; - - fn is_supported(capabilities: &Capabilities) -> bool { - T::is_supported(capabilities) - } - - fn to_dap(&self) -> ::Arguments { - T::to_dap(self) - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - T::response_from_dap(self, message) - } -} - -impl DapCommand for Arc { - type ProtoRequest = T::ProtoRequest; - type ProtoResponse = T::ProtoResponse; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - T::client_id_from_proto(request) - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Arc::new(T::from_proto(request)) - } - - fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest { - T::to_proto(self, debug_client_id, upstream_project_id) - } - - fn response_to_proto( - debug_client_id: SessionId, - message: Self::Response, - ) -> Self::ProtoResponse { - T::response_to_proto(debug_client_id, message) - } - - fn response_from_proto(&self, message: Self::ProtoResponse) -> Result { - T::response_from_proto(self, message) - } -} - -#[derive(Debug, Hash, PartialEq, Eq)] -pub struct StepCommand { - pub thread_id: i64, - pub granularity: Option, - pub single_thread: Option, -} - -impl StepCommand { - fn from_proto(message: proto::DapNextRequest) -> Self { - const LINE: i32 = proto::SteppingGranularity::Line as i32; - const INSTRUCTION: i32 = proto::SteppingGranularity::Instruction as i32; - - let granularity = message.granularity.map(|granularity| match granularity { - LINE => SteppingGranularity::Line, - INSTRUCTION => SteppingGranularity::Instruction, - _ => SteppingGranularity::Statement, - }); - - Self { - thread_id: message.thread_id, - granularity, - single_thread: message.single_thread, - } - } -} - -#[derive(Debug, Hash, PartialEq, Eq)] -pub(crate) struct NextCommand { - pub inner: StepCommand, -} - -impl LocalDapCommand for NextCommand { - type Response = ::Response; - type DapRequest = Next; - - fn to_dap(&self) -> ::Arguments { - NextArguments { - thread_id: self.inner.thread_id, - single_thread: self.inner.single_thread, - granularity: self.inner.granularity, - } - } - fn response_from_dap( - &self, - _message: ::Response, - ) -> Result { - Ok(()) - } -} - -impl DapCommand for NextCommand { - type ProtoRequest = proto::DapNextRequest; - type ProtoResponse = proto::Ack; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Self { - inner: StepCommand::from_proto(request.clone()), - } - } - - fn response_to_proto( - _debug_client_id: SessionId, - _message: Self::Response, - ) -> Self::ProtoResponse { - proto::Ack {} - } - - fn to_proto( - &self, - debug_client_id: SessionId, - upstream_project_id: u64, - ) -> proto::DapNextRequest { - proto::DapNextRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - thread_id: self.inner.thread_id, - single_thread: self.inner.single_thread, - granularity: self.inner.granularity.map(|gran| gran.to_proto() as i32), - } - } - - fn response_from_proto(&self, _message: Self::ProtoResponse) -> Result { - Ok(()) - } -} - -#[derive(Debug, Hash, PartialEq, Eq)] -pub(crate) struct StepInCommand { - pub inner: StepCommand, -} - -impl LocalDapCommand for StepInCommand { - type Response = ::Response; - type DapRequest = dap::requests::StepIn; - - fn to_dap(&self) -> ::Arguments { - StepInArguments { - thread_id: self.inner.thread_id, - single_thread: self.inner.single_thread, - target_id: None, - granularity: self.inner.granularity, - } - } - - fn response_from_dap( - &self, - _message: ::Response, - ) -> Result { - Ok(()) - } -} - -impl DapCommand for StepInCommand { - type ProtoRequest = proto::DapStepInRequest; - type ProtoResponse = proto::Ack; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Self { - inner: StepCommand::from_proto(proto::DapNextRequest { - project_id: request.project_id, - client_id: request.client_id, - thread_id: request.thread_id, - single_thread: request.single_thread, - granularity: request.granularity, - }), - } - } - - fn response_to_proto( - _debug_client_id: SessionId, - _message: Self::Response, - ) -> Self::ProtoResponse { - proto::Ack {} - } - - fn to_proto( - &self, - debug_client_id: SessionId, - upstream_project_id: u64, - ) -> proto::DapStepInRequest { - proto::DapStepInRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - thread_id: self.inner.thread_id, - single_thread: self.inner.single_thread, - granularity: self.inner.granularity.map(|gran| gran.to_proto() as i32), - target_id: None, - } - } - - fn response_from_proto(&self, _message: Self::ProtoResponse) -> Result { - Ok(()) - } -} - -#[derive(Debug, Hash, PartialEq, Eq)] -pub(crate) struct StepOutCommand { - pub inner: StepCommand, -} - -impl LocalDapCommand for StepOutCommand { - type Response = ::Response; - type DapRequest = dap::requests::StepOut; - - fn to_dap(&self) -> ::Arguments { - StepOutArguments { - thread_id: self.inner.thread_id, - single_thread: self.inner.single_thread, - granularity: self.inner.granularity, - } - } - - fn response_from_dap( - &self, - _message: ::Response, - ) -> Result { - Ok(()) - } -} - -impl DapCommand for StepOutCommand { - type ProtoRequest = proto::DapStepOutRequest; - type ProtoResponse = proto::Ack; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Self { - inner: StepCommand::from_proto(proto::DapNextRequest { - project_id: request.project_id, - client_id: request.client_id, - thread_id: request.thread_id, - single_thread: request.single_thread, - granularity: request.granularity, - }), - } - } - - fn response_to_proto( - _debug_client_id: SessionId, - _message: Self::Response, - ) -> Self::ProtoResponse { - proto::Ack {} - } - - fn to_proto( - &self, - debug_client_id: SessionId, - upstream_project_id: u64, - ) -> proto::DapStepOutRequest { - proto::DapStepOutRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - thread_id: self.inner.thread_id, - single_thread: self.inner.single_thread, - granularity: self.inner.granularity.map(|gran| gran.to_proto() as i32), - } - } - - fn response_from_proto(&self, _message: Self::ProtoResponse) -> Result { - Ok(()) - } -} - -#[derive(Debug, Hash, PartialEq, Eq)] -pub(crate) struct StepBackCommand { - pub inner: StepCommand, -} -impl LocalDapCommand for StepBackCommand { - type Response = ::Response; - type DapRequest = dap::requests::StepBack; - - fn is_supported(capabilities: &Capabilities) -> bool { - capabilities.supports_step_back.unwrap_or_default() - } - - fn to_dap(&self) -> ::Arguments { - dap::StepBackArguments { - thread_id: self.inner.thread_id, - single_thread: self.inner.single_thread, - granularity: self.inner.granularity, - } - } - - fn response_from_dap( - &self, - _message: ::Response, - ) -> Result { - Ok(()) - } -} - -impl DapCommand for StepBackCommand { - type ProtoRequest = proto::DapStepBackRequest; - type ProtoResponse = proto::Ack; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Self { - inner: StepCommand::from_proto(proto::DapNextRequest { - project_id: request.project_id, - client_id: request.client_id, - thread_id: request.thread_id, - single_thread: request.single_thread, - granularity: request.granularity, - }), - } - } - - fn response_to_proto( - _debug_client_id: SessionId, - _message: Self::Response, - ) -> Self::ProtoResponse { - proto::Ack {} - } - - fn to_proto( - &self, - debug_client_id: SessionId, - upstream_project_id: u64, - ) -> proto::DapStepBackRequest { - proto::DapStepBackRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - thread_id: self.inner.thread_id, - single_thread: self.inner.single_thread, - granularity: self.inner.granularity.map(|gran| gran.to_proto() as i32), - } - } - - fn response_from_proto(&self, _message: Self::ProtoResponse) -> Result { - Ok(()) - } -} - -#[derive(Debug, Hash, PartialEq, Eq)] -pub(crate) struct ContinueCommand { - pub args: ContinueArguments, -} - -impl LocalDapCommand for ContinueCommand { - type Response = ::Response; - type DapRequest = Continue; - - fn to_dap(&self) -> ::Arguments { - self.args.clone() - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message) - } -} - -impl DapCommand for ContinueCommand { - type ProtoRequest = proto::DapContinueRequest; - type ProtoResponse = proto::DapContinueResponse; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn to_proto( - &self, - debug_client_id: SessionId, - upstream_project_id: u64, - ) -> proto::DapContinueRequest { - proto::DapContinueRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - thread_id: self.args.thread_id, - single_thread: self.args.single_thread, - } - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Self { - args: ContinueArguments { - thread_id: request.thread_id, - single_thread: request.single_thread, - }, - } - } - - fn response_from_proto(&self, message: Self::ProtoResponse) -> Result { - Ok(Self::Response { - all_threads_continued: message.all_threads_continued, - }) - } - - fn response_to_proto( - debug_client_id: SessionId, - message: Self::Response, - ) -> Self::ProtoResponse { - proto::DapContinueResponse { - client_id: debug_client_id.to_proto(), - all_threads_continued: message.all_threads_continued, - } - } -} - -#[derive(Debug, Hash, PartialEq, Eq)] -pub(crate) struct PauseCommand { - pub thread_id: i64, -} - -impl LocalDapCommand for PauseCommand { - type Response = ::Response; - type DapRequest = dap::requests::Pause; - fn to_dap(&self) -> ::Arguments { - dap::PauseArguments { - thread_id: self.thread_id, - } - } - - fn response_from_dap( - &self, - _message: ::Response, - ) -> Result { - Ok(()) - } -} - -impl DapCommand for PauseCommand { - type ProtoRequest = proto::DapPauseRequest; - type ProtoResponse = proto::Ack; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Self { - thread_id: request.thread_id, - } - } - - fn to_proto( - &self, - debug_client_id: SessionId, - upstream_project_id: u64, - ) -> proto::DapPauseRequest { - proto::DapPauseRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - thread_id: self.thread_id, - } - } - - fn response_to_proto( - _debug_client_id: SessionId, - _message: Self::Response, - ) -> Self::ProtoResponse { - proto::Ack {} - } - - fn response_from_proto(&self, _message: Self::ProtoResponse) -> Result { - Ok(()) - } -} - -#[derive(Debug, Hash, PartialEq, Eq)] -pub(crate) struct DisconnectCommand { - pub restart: Option, - pub terminate_debuggee: Option, - pub suspend_debuggee: Option, -} - -impl LocalDapCommand for DisconnectCommand { - type Response = ::Response; - type DapRequest = dap::requests::Disconnect; - - fn to_dap(&self) -> ::Arguments { - dap::DisconnectArguments { - restart: self.restart, - terminate_debuggee: self.terminate_debuggee, - suspend_debuggee: self.suspend_debuggee, - } - } - - fn response_from_dap( - &self, - _message: ::Response, - ) -> Result { - Ok(()) - } -} - -impl DapCommand for DisconnectCommand { - type ProtoRequest = proto::DapDisconnectRequest; - type ProtoResponse = proto::Ack; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Self { - restart: request.restart, - terminate_debuggee: request.terminate_debuggee, - suspend_debuggee: request.suspend_debuggee, - } - } - - fn to_proto( - &self, - debug_client_id: SessionId, - upstream_project_id: u64, - ) -> proto::DapDisconnectRequest { - proto::DapDisconnectRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - restart: self.restart, - terminate_debuggee: self.terminate_debuggee, - suspend_debuggee: self.suspend_debuggee, - } - } - - fn response_to_proto( - _debug_client_id: SessionId, - _message: Self::Response, - ) -> Self::ProtoResponse { - proto::Ack {} - } - - fn response_from_proto(&self, _message: Self::ProtoResponse) -> Result { - Ok(()) - } -} - -#[derive(Debug, Hash, PartialEq, Eq)] -pub(crate) struct TerminateThreadsCommand { - pub thread_ids: Option>, -} - -impl LocalDapCommand for TerminateThreadsCommand { - type Response = ::Response; - type DapRequest = dap::requests::TerminateThreads; - - fn is_supported(capabilities: &Capabilities) -> bool { - capabilities - .supports_terminate_threads_request - .unwrap_or_default() - } - - fn to_dap(&self) -> ::Arguments { - dap::TerminateThreadsArguments { - thread_ids: self.thread_ids.clone(), - } - } - - fn response_from_dap( - &self, - _message: ::Response, - ) -> Result { - Ok(()) - } -} - -impl DapCommand for TerminateThreadsCommand { - type ProtoRequest = proto::DapTerminateThreadsRequest; - type ProtoResponse = proto::Ack; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - let thread_ids = if request.thread_ids.is_empty() { - None - } else { - Some(request.thread_ids.clone()) - }; - - Self { thread_ids } - } - - fn to_proto( - &self, - debug_client_id: SessionId, - upstream_project_id: u64, - ) -> proto::DapTerminateThreadsRequest { - proto::DapTerminateThreadsRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - thread_ids: self.thread_ids.clone().unwrap_or_default(), - } - } - - fn response_to_proto( - _debug_client_id: SessionId, - _message: Self::Response, - ) -> Self::ProtoResponse { - proto::Ack {} - } - - fn response_from_proto(&self, _message: Self::ProtoResponse) -> Result { - Ok(()) - } -} - -#[derive(Debug, Hash, PartialEq, Eq)] -pub(crate) struct TerminateCommand { - pub restart: Option, -} - -impl LocalDapCommand for TerminateCommand { - type Response = ::Response; - type DapRequest = dap::requests::Terminate; - - fn is_supported(capabilities: &Capabilities) -> bool { - capabilities.supports_terminate_request.unwrap_or_default() - } - fn to_dap(&self) -> ::Arguments { - dap::TerminateArguments { - restart: self.restart, - } - } - - fn response_from_dap( - &self, - _message: ::Response, - ) -> Result { - Ok(()) - } -} - -impl DapCommand for TerminateCommand { - type ProtoRequest = proto::DapTerminateRequest; - type ProtoResponse = proto::Ack; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Self { - restart: request.restart, - } - } - - fn to_proto( - &self, - debug_client_id: SessionId, - upstream_project_id: u64, - ) -> proto::DapTerminateRequest { - proto::DapTerminateRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - restart: self.restart, - } - } - - fn response_to_proto( - _debug_client_id: SessionId, - _message: Self::Response, - ) -> Self::ProtoResponse { - proto::Ack {} - } - - fn response_from_proto(&self, _message: Self::ProtoResponse) -> Result { - Ok(()) - } -} - -#[derive(Debug, Hash, PartialEq, Eq)] -pub(crate) struct RestartCommand { - pub raw: serde_json::Value, -} - -impl LocalDapCommand for RestartCommand { - type Response = ::Response; - type DapRequest = dap::requests::Restart; - - fn is_supported(capabilities: &Capabilities) -> bool { - capabilities.supports_restart_request.unwrap_or_default() - } - - fn to_dap(&self) -> ::Arguments { - dap::RestartArguments { - raw: self.raw.clone(), - } - } - - fn response_from_dap( - &self, - _message: ::Response, - ) -> Result { - Ok(()) - } -} - -impl DapCommand for RestartCommand { - type ProtoRequest = proto::DapRestartRequest; - type ProtoResponse = proto::Ack; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Self { - raw: serde_json::from_slice(&request.raw_args) - .log_err() - .unwrap_or(serde_json::Value::Null), - } - } - - fn to_proto( - &self, - debug_client_id: SessionId, - upstream_project_id: u64, - ) -> proto::DapRestartRequest { - let raw_args = serde_json::to_vec(&self.raw).log_err().unwrap_or_default(); - - proto::DapRestartRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - raw_args, - } - } - - fn response_to_proto( - _debug_client_id: SessionId, - _message: Self::Response, - ) -> Self::ProtoResponse { - proto::Ack {} - } - - fn response_from_proto(&self, _message: Self::ProtoResponse) -> Result { - Ok(()) - } -} - -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub struct VariablesCommand { - pub variables_reference: u64, - pub filter: Option, - pub start: Option, - pub count: Option, - pub format: Option, -} - -impl LocalDapCommand for VariablesCommand { - type Response = Vec; - type DapRequest = dap::requests::Variables; - const CACHEABLE: bool = true; - - fn to_dap(&self) -> ::Arguments { - dap::VariablesArguments { - variables_reference: self.variables_reference, - filter: self.filter, - start: self.start, - count: self.count, - format: self.format.clone(), - } - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message.variables) - } -} - -impl DapCommand for VariablesCommand { - type ProtoRequest = proto::VariablesRequest; - type ProtoResponse = proto::DapVariables; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest { - proto::VariablesRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - variables_reference: self.variables_reference, - filter: None, - start: self.start, - count: self.count, - format: None, - } - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Self { - variables_reference: request.variables_reference, - filter: None, - start: request.start, - count: request.count, - format: None, - } - } - - fn response_to_proto( - debug_client_id: SessionId, - message: Self::Response, - ) -> Self::ProtoResponse { - proto::DapVariables { - client_id: debug_client_id.to_proto(), - variables: message.to_proto(), - } - } - - fn response_from_proto(&self, message: Self::ProtoResponse) -> Result { - Ok(Vec::from_proto(message.variables)) - } -} - -#[derive(Debug, Hash, PartialEq, Eq)] -pub(crate) struct SetVariableValueCommand { - pub name: String, - pub value: String, - pub variables_reference: u64, -} -impl LocalDapCommand for SetVariableValueCommand { - type Response = SetVariableResponse; - type DapRequest = dap::requests::SetVariable; - fn is_supported(capabilities: &Capabilities) -> bool { - capabilities.supports_set_variable.unwrap_or_default() - } - fn to_dap(&self) -> ::Arguments { - dap::SetVariableArguments { - format: None, - name: self.name.clone(), - value: self.value.clone(), - variables_reference: self.variables_reference, - } - } - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message) - } -} - -impl DapCommand for SetVariableValueCommand { - type ProtoRequest = proto::DapSetVariableValueRequest; - type ProtoResponse = proto::DapSetVariableValueResponse; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest { - proto::DapSetVariableValueRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - variables_reference: self.variables_reference, - value: self.value.clone(), - name: self.name.clone(), - } - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Self { - variables_reference: request.variables_reference, - name: request.name.clone(), - value: request.value.clone(), - } - } - - fn response_to_proto( - debug_client_id: SessionId, - message: Self::Response, - ) -> Self::ProtoResponse { - proto::DapSetVariableValueResponse { - client_id: debug_client_id.to_proto(), - value: message.value, - variable_type: message.type_, - named_variables: message.named_variables, - variables_reference: message.variables_reference, - indexed_variables: message.indexed_variables, - memory_reference: message.memory_reference, - } - } - - fn response_from_proto(&self, message: Self::ProtoResponse) -> Result { - Ok(SetVariableResponse { - value: message.value, - type_: message.variable_type, - variables_reference: message.variables_reference, - named_variables: message.named_variables, - indexed_variables: message.indexed_variables, - memory_reference: message.memory_reference, - value_location_reference: None, // TODO - }) - } -} - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub(crate) struct RestartStackFrameCommand { - pub stack_frame_id: u64, -} - -impl LocalDapCommand for RestartStackFrameCommand { - type Response = ::Response; - type DapRequest = dap::requests::RestartFrame; - - fn is_supported(capabilities: &Capabilities) -> bool { - capabilities.supports_restart_frame.unwrap_or_default() - } - - fn to_dap(&self) -> ::Arguments { - dap::RestartFrameArguments { - frame_id: self.stack_frame_id, - } - } - - fn response_from_dap( - &self, - _message: ::Response, - ) -> Result { - Ok(()) - } -} - -impl DapCommand for RestartStackFrameCommand { - type ProtoRequest = proto::DapRestartStackFrameRequest; - type ProtoResponse = proto::Ack; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Self { - stack_frame_id: request.stack_frame_id, - } - } - - fn to_proto( - &self, - debug_client_id: SessionId, - upstream_project_id: u64, - ) -> proto::DapRestartStackFrameRequest { - proto::DapRestartStackFrameRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - stack_frame_id: self.stack_frame_id, - } - } - - fn response_to_proto( - _debug_client_id: SessionId, - _message: Self::Response, - ) -> Self::ProtoResponse { - proto::Ack {} - } - - fn response_from_proto(&self, _message: Self::ProtoResponse) -> Result { - Ok(()) - } -} - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub(crate) struct ModulesCommand; - -impl LocalDapCommand for ModulesCommand { - type Response = Vec; - type DapRequest = dap::requests::Modules; - const CACHEABLE: bool = true; - - fn is_supported(capabilities: &Capabilities) -> bool { - capabilities.supports_modules_request.unwrap_or_default() - } - - fn to_dap(&self) -> ::Arguments { - dap::ModulesArguments { - start_module: None, - module_count: None, - } - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message.modules) - } -} - -impl DapCommand for ModulesCommand { - type ProtoRequest = proto::DapModulesRequest; - type ProtoResponse = proto::DapModulesResponse; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn from_proto(_request: &Self::ProtoRequest) -> Self { - Self {} - } - - fn to_proto( - &self, - debug_client_id: SessionId, - upstream_project_id: u64, - ) -> proto::DapModulesRequest { - proto::DapModulesRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - } - } - - fn response_to_proto( - debug_client_id: SessionId, - message: Self::Response, - ) -> Self::ProtoResponse { - proto::DapModulesResponse { - modules: message - .into_iter() - .map(|module| module.to_proto()) - .collect(), - client_id: debug_client_id.to_proto(), - } - } - - fn response_from_proto(&self, message: Self::ProtoResponse) -> Result { - Ok(message - .modules - .into_iter() - .filter_map(|module| dap::Module::from_proto(module).ok()) - .collect()) - } -} - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub(crate) struct LoadedSourcesCommand; - -impl LocalDapCommand for LoadedSourcesCommand { - type Response = Vec; - type DapRequest = dap::requests::LoadedSources; - const CACHEABLE: bool = true; - - fn is_supported(capabilities: &Capabilities) -> bool { - capabilities - .supports_loaded_sources_request - .unwrap_or_default() - } - fn to_dap(&self) -> ::Arguments { - dap::LoadedSourcesArguments {} - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message.sources) - } -} - -impl DapCommand for LoadedSourcesCommand { - type ProtoRequest = proto::DapLoadedSourcesRequest; - type ProtoResponse = proto::DapLoadedSourcesResponse; - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn from_proto(_request: &Self::ProtoRequest) -> Self { - Self {} - } - - fn to_proto( - &self, - debug_client_id: SessionId, - upstream_project_id: u64, - ) -> proto::DapLoadedSourcesRequest { - proto::DapLoadedSourcesRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - } - } - - fn response_to_proto( - debug_client_id: SessionId, - message: Self::Response, - ) -> Self::ProtoResponse { - proto::DapLoadedSourcesResponse { - sources: message - .into_iter() - .map(|source| source.to_proto()) - .collect(), - client_id: debug_client_id.to_proto(), - } - } - - fn response_from_proto(&self, message: Self::ProtoResponse) -> Result { - Ok(message - .sources - .into_iter() - .map(dap::Source::from_proto) - .collect()) - } -} - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub(crate) struct StackTraceCommand { - pub thread_id: i64, - pub start_frame: Option, - pub levels: Option, -} - -impl LocalDapCommand for StackTraceCommand { - type Response = Vec; - type DapRequest = dap::requests::StackTrace; - const CACHEABLE: bool = true; - - fn to_dap(&self) -> ::Arguments { - dap::StackTraceArguments { - thread_id: self.thread_id, - start_frame: self.start_frame, - levels: self.levels, - format: None, - } - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message.stack_frames) - } -} - -impl DapCommand for StackTraceCommand { - type ProtoRequest = proto::DapStackTraceRequest; - type ProtoResponse = proto::DapStackTraceResponse; - - fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest { - proto::DapStackTraceRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - thread_id: self.thread_id, - start_frame: self.start_frame, - stack_trace_levels: self.levels, - } - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Self { - thread_id: request.thread_id, - start_frame: request.start_frame, - levels: request.stack_trace_levels, - } - } - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn response_from_proto(&self, message: Self::ProtoResponse) -> Result { - Ok(message - .frames - .into_iter() - .map(dap::StackFrame::from_proto) - .collect()) - } - - fn response_to_proto( - _debug_client_id: SessionId, - message: Self::Response, - ) -> Self::ProtoResponse { - proto::DapStackTraceResponse { - frames: message.to_proto(), - } - } -} - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub(crate) struct ScopesCommand { - pub stack_frame_id: u64, -} - -impl LocalDapCommand for ScopesCommand { - type Response = Vec; - type DapRequest = dap::requests::Scopes; - const CACHEABLE: bool = true; - - fn to_dap(&self) -> ::Arguments { - dap::ScopesArguments { - frame_id: self.stack_frame_id, - } - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message.scopes) - } -} - -impl DapCommand for ScopesCommand { - type ProtoRequest = proto::DapScopesRequest; - type ProtoResponse = proto::DapScopesResponse; - - fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest { - proto::DapScopesRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - stack_frame_id: self.stack_frame_id, - } - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Self { - stack_frame_id: request.stack_frame_id, - } - } - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn response_from_proto(&self, message: Self::ProtoResponse) -> Result { - Ok(Vec::from_proto(message.scopes)) - } - - fn response_to_proto( - _debug_client_id: SessionId, - message: Self::Response, - ) -> Self::ProtoResponse { - proto::DapScopesResponse { - scopes: message.to_proto(), - } - } -} - -impl LocalDapCommand for super::session::CompletionsQuery { - type Response = dap::CompletionsResponse; - type DapRequest = dap::requests::Completions; - const CACHEABLE: bool = true; - - fn to_dap(&self) -> ::Arguments { - dap::CompletionsArguments { - text: self.query.clone(), - frame_id: self.frame_id, - column: self.column, - line: None, - } - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message) - } - - fn is_supported(capabilities: &Capabilities) -> bool { - capabilities - .supports_completions_request - .unwrap_or_default() - } -} - -impl DapCommand for super::session::CompletionsQuery { - type ProtoRequest = proto::DapCompletionRequest; - type ProtoResponse = proto::DapCompletionResponse; - - fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest { - proto::DapCompletionRequest { - client_id: debug_client_id.to_proto(), - project_id: upstream_project_id, - frame_id: self.frame_id, - query: self.query.clone(), - column: self.column, - line: self.line, - } - } - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Self { - query: request.query.clone(), - frame_id: request.frame_id, - column: request.column, - line: request.line, - } - } - - fn response_from_proto(&self, message: Self::ProtoResponse) -> Result { - Ok(dap::CompletionsResponse { - targets: Vec::from_proto(message.completions), - }) - } - - fn response_to_proto( - _debug_client_id: SessionId, - message: Self::Response, - ) -> Self::ProtoResponse { - proto::DapCompletionResponse { - client_id: _debug_client_id.to_proto(), - completions: message.targets.to_proto(), - } - } -} - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub(crate) struct EvaluateCommand { - pub expression: String, - pub frame_id: Option, - pub context: Option, - pub source: Option, -} - -impl LocalDapCommand for EvaluateCommand { - type Response = dap::EvaluateResponse; - type DapRequest = dap::requests::Evaluate; - fn to_dap(&self) -> ::Arguments { - dap::EvaluateArguments { - expression: self.expression.clone(), - frame_id: self.frame_id, - context: self.context.clone(), - source: self.source.clone(), - line: None, - column: None, - format: None, - } - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message) - } -} -impl DapCommand for EvaluateCommand { - type ProtoRequest = proto::DapEvaluateRequest; - type ProtoResponse = proto::DapEvaluateResponse; - - fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest { - proto::DapEvaluateRequest { - client_id: debug_client_id.to_proto(), - project_id: upstream_project_id, - expression: self.expression.clone(), - frame_id: self.frame_id, - context: self - .context - .clone() - .map(|context| context.to_proto().into()), - } - } - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn from_proto(request: &Self::ProtoRequest) -> Self { - Self { - expression: request.expression.clone(), - frame_id: request.frame_id, - context: Some(dap::EvaluateArgumentsContext::from_proto(request.context())), - source: None, - } - } - - fn response_from_proto(&self, message: Self::ProtoResponse) -> Result { - Ok(dap::EvaluateResponse { - result: message.result.clone(), - type_: message.evaluate_type.clone(), - presentation_hint: None, - variables_reference: message.variable_reference, - named_variables: message.named_variables, - indexed_variables: message.indexed_variables, - memory_reference: message.memory_reference, - value_location_reference: None, //TODO - }) - } - - fn response_to_proto( - _debug_client_id: SessionId, - message: Self::Response, - ) -> Self::ProtoResponse { - proto::DapEvaluateResponse { - result: message.result, - evaluate_type: message.type_, - variable_reference: message.variables_reference, - named_variables: message.named_variables, - indexed_variables: message.indexed_variables, - memory_reference: message.memory_reference, - } - } -} - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub(crate) struct ThreadsCommand; - -impl LocalDapCommand for ThreadsCommand { - type Response = Vec; - type DapRequest = dap::requests::Threads; - const CACHEABLE: bool = true; - - fn to_dap(&self) -> ::Arguments { - dap::ThreadsArgument {} - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message.threads) - } -} - -impl DapCommand for ThreadsCommand { - type ProtoRequest = proto::DapThreadsRequest; - type ProtoResponse = proto::DapThreadsResponse; - - fn to_proto(&self, debug_client_id: SessionId, upstream_project_id: u64) -> Self::ProtoRequest { - proto::DapThreadsRequest { - project_id: upstream_project_id, - client_id: debug_client_id.to_proto(), - } - } - - fn from_proto(_request: &Self::ProtoRequest) -> Self { - Self {} - } - - fn client_id_from_proto(request: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(request.client_id) - } - - fn response_from_proto(&self, message: Self::ProtoResponse) -> Result { - Ok(Vec::from_proto(message.threads)) - } - - fn response_to_proto( - _debug_client_id: SessionId, - message: Self::Response, - ) -> Self::ProtoResponse { - proto::DapThreadsResponse { - threads: message.to_proto(), - } - } -} - -#[derive(Clone, Debug, Hash, PartialEq)] -pub(super) struct Initialize { - pub(super) adapter_id: String, -} - -fn dap_client_capabilities(adapter_id: String) -> InitializeRequestArguments { - InitializeRequestArguments { - client_id: Some("zed".to_owned()), - client_name: Some("Zed".to_owned()), - adapter_id, - locale: Some("en-US".to_owned()), - path_format: Some(InitializeRequestArgumentsPathFormat::Path), - supports_variable_type: Some(true), - supports_variable_paging: Some(false), - supports_run_in_terminal_request: Some(true), - supports_memory_references: Some(true), - supports_progress_reporting: Some(false), - supports_invalidated_event: Some(false), - lines_start_at1: Some(true), - columns_start_at1: Some(true), - supports_memory_event: Some(false), - supports_args_can_be_interpreted_by_shell: Some(false), - supports_start_debugging_request: Some(true), - supports_ansistyling: Some(true), - } -} - -impl LocalDapCommand for Initialize { - type Response = Capabilities; - type DapRequest = dap::requests::Initialize; - - fn to_dap(&self) -> ::Arguments { - dap_client_capabilities(self.adapter_id.clone()) - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message) - } -} - -#[derive(Clone, Debug, Hash, PartialEq)] -pub(super) struct ConfigurationDone {} - -impl LocalDapCommand for ConfigurationDone { - type Response = (); - type DapRequest = dap::requests::ConfigurationDone; - - fn is_supported(capabilities: &Capabilities) -> bool { - capabilities - .supports_configuration_done_request - .unwrap_or_default() - } - - fn to_dap(&self) -> ::Arguments { - dap::ConfigurationDoneArguments {} - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message) - } -} - -#[derive(Clone, Debug, Hash, PartialEq)] -pub(super) struct Launch { - pub(super) raw: Value, -} - -impl LocalDapCommand for Launch { - type Response = (); - type DapRequest = dap::requests::Launch; - - fn to_dap(&self) -> ::Arguments { - dap::LaunchRequestArguments { - raw: self.raw.clone(), - } - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message) - } -} - -#[derive(Clone, Debug, Hash, PartialEq)] -pub(super) struct Attach { - pub(super) raw: Value, -} - -impl LocalDapCommand for Attach { - type Response = (); - type DapRequest = dap::requests::Attach; - - fn to_dap(&self) -> ::Arguments { - dap::AttachRequestArguments { - raw: self.raw.clone(), - } - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message) - } -} - -#[derive(Clone, Debug, Hash, PartialEq)] -pub(super) struct SetBreakpoints { - pub(super) source: dap::Source, - pub(super) breakpoints: Vec, - pub(super) source_modified: Option, -} - -impl LocalDapCommand for SetBreakpoints { - type Response = Vec; - type DapRequest = dap::requests::SetBreakpoints; - - fn to_dap(&self) -> ::Arguments { - dap::SetBreakpointsArguments { - lines: None, - source_modified: self.source_modified, - source: self.source.clone(), - breakpoints: Some(self.breakpoints.clone()), - } - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message.breakpoints) - } -} - -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub enum DataBreakpointContext { - Variable { - variables_reference: u64, - name: String, - bytes: Option, - }, - Expression { - expression: String, - frame_id: Option, - }, - Address { - address: String, - bytes: Option, - }, -} - -impl DataBreakpointContext { - pub fn human_readable_label(&self) -> String { - match self { - DataBreakpointContext::Variable { name, .. } => format!("Variable: {}", name), - DataBreakpointContext::Expression { expression, .. } => { - format!("Expression: {}", expression) - } - DataBreakpointContext::Address { address, bytes } => { - let mut label = format!("Address: {}", address); - if let Some(bytes) = bytes { - label.push_str(&format!( - " ({} byte{})", - bytes, - if *bytes == 1 { "" } else { "s" } - )); - } - label - } - } - } -} - -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub(crate) struct DataBreakpointInfoCommand { - pub context: Arc, - pub mode: Option, -} - -impl LocalDapCommand for DataBreakpointInfoCommand { - type Response = dap::DataBreakpointInfoResponse; - type DapRequest = dap::requests::DataBreakpointInfo; - const CACHEABLE: bool = true; - - // todo(debugger): We should expand this trait in the future to take a &self - // Depending on this command is_supported could be differentb - fn is_supported(capabilities: &Capabilities) -> bool { - capabilities.supports_data_breakpoints.unwrap_or(false) - } - - fn to_dap(&self) -> ::Arguments { - let (variables_reference, name, frame_id, as_address, bytes) = match &*self.context { - DataBreakpointContext::Variable { - variables_reference, - name, - bytes, - } => ( - Some(*variables_reference), - name.clone(), - None, - Some(false), - *bytes, - ), - DataBreakpointContext::Expression { - expression, - frame_id, - } => (None, expression.clone(), *frame_id, Some(false), None), - DataBreakpointContext::Address { address, bytes } => { - (None, address.clone(), None, Some(true), *bytes) - } - }; - - dap::DataBreakpointInfoArguments { - variables_reference, - name, - frame_id, - bytes, - as_address, - mode: self.mode.clone(), - } - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message) - } -} - -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub(crate) struct SetDataBreakpointsCommand { - pub breakpoints: Vec, -} - -impl LocalDapCommand for SetDataBreakpointsCommand { - type Response = Vec; - type DapRequest = dap::requests::SetDataBreakpoints; - - fn is_supported(capabilities: &Capabilities) -> bool { - capabilities.supports_data_breakpoints.unwrap_or(false) - } - - fn to_dap(&self) -> ::Arguments { - dap::SetDataBreakpointsArguments { - breakpoints: self.breakpoints.clone(), - } - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message.breakpoints) - } -} - -#[derive(Clone, Debug, Hash, PartialEq)] -pub(super) enum SetExceptionBreakpoints { - Plain { - filters: Vec, - }, - WithOptions { - filters: Vec, - }, -} - -impl LocalDapCommand for SetExceptionBreakpoints { - type Response = Vec; - type DapRequest = dap::requests::SetExceptionBreakpoints; - - fn to_dap(&self) -> ::Arguments { - match self { - SetExceptionBreakpoints::Plain { filters } => dap::SetExceptionBreakpointsArguments { - filters: filters.clone(), - exception_options: None, - filter_options: None, - }, - SetExceptionBreakpoints::WithOptions { filters } => { - dap::SetExceptionBreakpointsArguments { - filters: vec![], - filter_options: Some(filters.clone()), - exception_options: None, - } - } - } - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message.breakpoints.unwrap_or_default()) - } -} - -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub(super) struct LocationsCommand { - pub(super) reference: u64, -} - -impl LocalDapCommand for LocationsCommand { - type Response = dap::LocationsResponse; - type DapRequest = dap::requests::Locations; - const CACHEABLE: bool = true; - - fn to_dap(&self) -> ::Arguments { - dap::LocationsArguments { - location_reference: self.reference, - } - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message) - } -} - -impl DapCommand for LocationsCommand { - type ProtoRequest = proto::DapLocationsRequest; - type ProtoResponse = proto::DapLocationsResponse; - - fn client_id_from_proto(message: &Self::ProtoRequest) -> SessionId { - SessionId::from_proto(message.session_id) - } - - fn from_proto(message: &Self::ProtoRequest) -> Self { - Self { - reference: message.location_reference, - } - } - - fn to_proto(&self, session_id: SessionId, project_id: u64) -> Self::ProtoRequest { - proto::DapLocationsRequest { - project_id, - session_id: session_id.to_proto(), - location_reference: self.reference, - } - } - - fn response_to_proto(_: SessionId, response: Self::Response) -> Self::ProtoResponse { - proto::DapLocationsResponse { - source: Some(response.source.to_proto()), - line: response.line, - column: response.column, - end_line: response.end_line, - end_column: response.end_column, - } - } - - fn response_from_proto(&self, response: Self::ProtoResponse) -> Result { - Ok(dap::LocationsResponse { - source: response - .source - .map(::from_proto) - .context("Missing `source` field in Locations proto")?, - line: response.line, - column: response.column, - end_line: response.end_line, - end_column: response.end_column, - }) - } -} - -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub(crate) struct ReadMemory { - pub(crate) memory_reference: String, - pub(crate) offset: Option, - pub(crate) count: u64, -} - -#[derive(Clone, Debug, PartialEq)] -pub(crate) struct ReadMemoryResponse { - pub(super) address: Arc, - pub(super) unreadable_bytes: Option, - pub(super) content: Arc<[u8]>, -} - -impl LocalDapCommand for ReadMemory { - type Response = ReadMemoryResponse; - type DapRequest = dap::requests::ReadMemory; - const CACHEABLE: bool = true; - - fn is_supported(capabilities: &Capabilities) -> bool { - capabilities - .supports_read_memory_request - .unwrap_or_default() - } - fn to_dap(&self) -> ::Arguments { - dap::ReadMemoryArguments { - memory_reference: self.memory_reference.clone(), - offset: self.offset, - count: self.count, - } - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - let data = if let Some(data) = message.data { - base64::engine::general_purpose::STANDARD - .decode(data) - .log_err() - .context("parsing base64 data from DAP's ReadMemory response")? - } else { - vec![] - }; - - Ok(ReadMemoryResponse { - address: message.address.into(), - content: data.into(), - unreadable_bytes: message.unreadable_bytes, - }) - } -} - -impl LocalDapCommand for dap::WriteMemoryArguments { - type Response = dap::WriteMemoryResponse; - type DapRequest = dap::requests::WriteMemory; - fn is_supported(capabilities: &Capabilities) -> bool { - capabilities - .supports_write_memory_request - .unwrap_or_default() - } - fn to_dap(&self) -> ::Arguments { - self.clone() - } - - fn response_from_dap( - &self, - message: ::Response, - ) -> Result { - Ok(message) - } -} diff --git a/crates/project/src/debugger/dap_store.rs b/crates/project/src/debugger/dap_store.rs deleted file mode 100644 index 4a588e7c43..0000000000 --- a/crates/project/src/debugger/dap_store.rs +++ /dev/null @@ -1,1035 +0,0 @@ -use super::{ - breakpoint_store::BreakpointStore, - dap_command::EvaluateCommand, - locators, - session::{self, Session, SessionStateEvent}, -}; -use crate::{ - InlayHint, InlayHintLabel, ProjectEnvironment, ResolveState, - debugger::session::SessionQuirks, - project_settings::{DapBinary, ProjectSettings}, - worktree_store::WorktreeStore, -}; -use anyhow::{Context as _, Result, anyhow}; -use async_trait::async_trait; -use collections::HashMap; -use dap::{ - Capabilities, DapRegistry, DebugRequest, EvaluateArgumentsContext, StackFrameId, - adapters::{ - DapDelegate, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition, TcpArguments, - }, - client::SessionId, - inline_value::VariableLookupKind, - messages::Message, -}; -use fs::{Fs, RemoveOptions}; -use futures::{ - StreamExt, TryStreamExt as _, - channel::mpsc::{self, UnboundedSender}, - future::{Shared, join_all}, -}; -use gpui::{App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task}; -use http_client::HttpClient; -use language::{Buffer, LanguageToolchainStore}; -use node_runtime::NodeRuntime; -use settings::InlayHintKind; - -use remote::RemoteClient; -use rpc::{ - AnyProtoClient, TypedEnvelope, - proto::{self}, -}; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsLocation, WorktreeId}; -use std::{ - borrow::Borrow, - collections::BTreeMap, - ffi::OsStr, - net::Ipv4Addr, - path::{Path, PathBuf}, - sync::{Arc, Once}, -}; -use task::{DebugScenario, SpawnInTerminal, TaskContext, TaskTemplate}; -use util::{ResultExt as _, rel_path::RelPath}; -use worktree::Worktree; - -#[derive(Debug)] -pub enum DapStoreEvent { - DebugClientStarted(SessionId), - DebugSessionInitialized(SessionId), - DebugClientShutdown(SessionId), - DebugClientEvent { - session_id: SessionId, - message: Message, - }, - Notification(String), - RemoteHasInitialized, -} - -enum DapStoreMode { - Local(LocalDapStore), - Remote(RemoteDapStore), - Collab, -} - -pub struct LocalDapStore { - fs: Arc, - node_runtime: NodeRuntime, - http_client: Arc, - environment: Entity, - toolchain_store: Arc, - is_headless: bool, -} - -pub struct RemoteDapStore { - remote_client: Entity, - upstream_client: AnyProtoClient, - upstream_project_id: u64, - node_runtime: NodeRuntime, - http_client: Arc, -} - -pub struct DapStore { - mode: DapStoreMode, - downstream_client: Option<(AnyProtoClient, u64)>, - breakpoint_store: Entity, - worktree_store: Entity, - sessions: BTreeMap>, - next_session_id: u32, - adapter_options: BTreeMap>, -} - -impl EventEmitter for DapStore {} - -#[derive(Clone, Serialize, Deserialize)] -pub struct PersistedExceptionBreakpoint { - pub enabled: bool, -} - -/// Represents best-effort serialization of adapter state during last session (e.g. watches) -#[derive(Clone, Default, Serialize, Deserialize)] -pub struct PersistedAdapterOptions { - /// Which exception breakpoints were enabled during the last session with this adapter? - pub exception_breakpoints: BTreeMap, -} - -impl DapStore { - pub fn init(client: &AnyProtoClient, cx: &mut App) { - static ADD_LOCATORS: Once = Once::new(); - ADD_LOCATORS.call_once(|| { - let registry = DapRegistry::global(cx); - registry.add_locator(Arc::new(locators::cargo::CargoLocator {})); - registry.add_locator(Arc::new(locators::go::GoLocator {})); - registry.add_locator(Arc::new(locators::node::NodeLocator)); - registry.add_locator(Arc::new(locators::python::PythonLocator)); - }); - client.add_entity_request_handler(Self::handle_run_debug_locator); - client.add_entity_request_handler(Self::handle_get_debug_adapter_binary); - client.add_entity_message_handler(Self::handle_log_to_debug_console); - } - - #[expect(clippy::too_many_arguments)] - pub fn new_local( - http_client: Arc, - node_runtime: NodeRuntime, - fs: Arc, - environment: Entity, - toolchain_store: Arc, - worktree_store: Entity, - breakpoint_store: Entity, - is_headless: bool, - cx: &mut Context, - ) -> Self { - let mode = DapStoreMode::Local(LocalDapStore { - fs: fs.clone(), - environment, - http_client, - node_runtime, - toolchain_store, - is_headless, - }); - - Self::new(mode, breakpoint_store, worktree_store, fs, cx) - } - - pub fn new_remote( - project_id: u64, - remote_client: Entity, - breakpoint_store: Entity, - worktree_store: Entity, - node_runtime: NodeRuntime, - http_client: Arc, - fs: Arc, - cx: &mut Context, - ) -> Self { - let mode = DapStoreMode::Remote(RemoteDapStore { - upstream_client: remote_client.read(cx).proto_client(), - remote_client, - upstream_project_id: project_id, - node_runtime, - http_client, - }); - - Self::new(mode, breakpoint_store, worktree_store, fs, cx) - } - - pub fn new_collab( - _project_id: u64, - _upstream_client: AnyProtoClient, - breakpoint_store: Entity, - worktree_store: Entity, - fs: Arc, - cx: &mut Context, - ) -> Self { - Self::new( - DapStoreMode::Collab, - breakpoint_store, - worktree_store, - fs, - cx, - ) - } - - fn new( - mode: DapStoreMode, - breakpoint_store: Entity, - worktree_store: Entity, - fs: Arc, - cx: &mut Context, - ) -> Self { - cx.background_spawn(async move { - let dir = paths::debug_adapters_dir().join("js-debug-companion"); - - let mut children = fs.read_dir(&dir).await?.try_collect::>().await?; - children.sort_by_key(|child| semver::Version::parse(child.file_name()?.to_str()?).ok()); - - if let Some(child) = children.last() - && let Some(name) = child.file_name() - && let Some(name) = name.to_str() - && semver::Version::parse(name).is_ok() - { - children.pop(); - } - - for child in children { - fs.remove_dir( - &child, - RemoveOptions { - recursive: true, - ignore_if_not_exists: true, - }, - ) - .await - .ok(); - } - - anyhow::Ok(()) - }) - .detach(); - - Self { - mode, - next_session_id: 0, - downstream_client: None, - breakpoint_store, - worktree_store, - sessions: Default::default(), - adapter_options: Default::default(), - } - } - - pub fn get_debug_adapter_binary( - &mut self, - definition: DebugTaskDefinition, - session_id: SessionId, - worktree: &Entity, - console: UnboundedSender, - cx: &mut Context, - ) -> Task> { - match &self.mode { - DapStoreMode::Local(_) => { - let Some(adapter) = DapRegistry::global(cx).adapter(&definition.adapter) else { - return Task::ready(Err(anyhow!("Failed to find a debug adapter"))); - }; - - let settings_location = SettingsLocation { - worktree_id: worktree.read(cx).id(), - path: RelPath::empty(), - }; - let dap_settings = ProjectSettings::get(Some(settings_location), cx) - .dap - .get(&adapter.name()); - let user_installed_path = dap_settings.and_then(|s| match &s.binary { - DapBinary::Default => None, - DapBinary::Custom(binary) => { - let path = PathBuf::from(binary); - Some(worktree.read(cx).resolve_executable_path(path)) - } - }); - let user_args = dap_settings.map(|s| s.args.clone()); - let user_env = dap_settings.map(|s| s.env.clone()); - - let delegate = self.delegate(worktree, console, cx); - - let worktree = worktree.clone(); - cx.spawn(async move |this, cx| { - let mut binary = adapter - .get_binary( - &delegate, - &definition, - user_installed_path, - user_args, - user_env, - cx, - ) - .await?; - - let env = this - .update(cx, |this, cx| { - this.as_local() - .unwrap() - .environment - .update(cx, |environment, cx| { - environment.worktree_environment(worktree, cx) - }) - })? - .await; - - if let Some(mut env) = env { - env.extend(std::mem::take(&mut binary.envs)); - binary.envs = env; - } - - Ok(binary) - }) - } - DapStoreMode::Remote(remote) => { - let request = remote - .upstream_client - .request(proto::GetDebugAdapterBinary { - session_id: session_id.to_proto(), - project_id: remote.upstream_project_id, - worktree_id: worktree.read(cx).id().to_proto(), - definition: Some(definition.to_proto()), - }); - let remote = remote.remote_client.clone(); - - cx.spawn(async move |_, cx| { - let response = request.await?; - let binary = DebugAdapterBinary::from_proto(response)?; - - let port_forwarding; - let connection; - if let Some(c) = binary.connection { - let host = Ipv4Addr::LOCALHOST; - let port; - if remote.read_with(cx, |remote, _cx| remote.shares_network_interface())? { - port = c.port; - port_forwarding = None; - } else { - port = dap::transport::TcpTransport::unused_port(host).await?; - port_forwarding = Some((port, c.host.to_string(), c.port)); - } - connection = Some(TcpArguments { - port, - host, - timeout: c.timeout, - }) - } else { - port_forwarding = None; - connection = None; - } - - let command = remote.read_with(cx, |remote, _cx| { - remote.build_command( - binary.command, - &binary.arguments, - &binary.envs, - binary.cwd.map(|path| path.display().to_string()), - port_forwarding, - ) - })??; - - Ok(DebugAdapterBinary { - command: Some(command.program), - arguments: command.args, - envs: command.env, - cwd: None, - connection, - request_args: binary.request_args, - }) - }) - } - DapStoreMode::Collab => { - Task::ready(Err(anyhow!("Debugging is not yet supported via collab"))) - } - } - } - - pub fn debug_scenario_for_build_task( - &self, - build: TaskTemplate, - adapter: DebugAdapterName, - label: SharedString, - cx: &mut App, - ) -> Task> { - let locators = DapRegistry::global(cx).locators(); - - cx.background_spawn(async move { - for locator in locators.values() { - if let Some(scenario) = locator.create_scenario(&build, &label, &adapter).await { - return Some(scenario); - } - } - None - }) - } - - pub fn run_debug_locator( - &mut self, - locator_name: &str, - build_command: SpawnInTerminal, - cx: &mut Context, - ) -> Task> { - match &self.mode { - DapStoreMode::Local(_) => { - // Pre-resolve args with existing environment. - let locators = DapRegistry::global(cx).locators(); - let locator = locators.get(locator_name); - - if let Some(locator) = locator.cloned() { - cx.background_spawn(async move { - let result = locator - .run(build_command.clone()) - .await - .log_with_level(log::Level::Error); - if let Some(result) = result { - return Ok(result); - } - - anyhow::bail!( - "None of the locators for task `{}` completed successfully", - build_command.label - ) - }) - } else { - Task::ready(Err(anyhow!( - "Couldn't find any locator for task `{}`. Specify the `attach` or `launch` arguments in your debug scenario definition", - build_command.label - ))) - } - } - DapStoreMode::Remote(remote) => { - let request = remote.upstream_client.request(proto::RunDebugLocators { - project_id: remote.upstream_project_id, - build_command: Some(build_command.to_proto()), - locator: locator_name.to_owned(), - }); - cx.background_spawn(async move { - let response = request.await?; - DebugRequest::from_proto(response) - }) - } - DapStoreMode::Collab => { - Task::ready(Err(anyhow!("Debugging is not yet supported via collab"))) - } - } - } - - fn as_local(&self) -> Option<&LocalDapStore> { - match &self.mode { - DapStoreMode::Local(local_dap_store) => Some(local_dap_store), - _ => None, - } - } - - pub fn new_session( - &mut self, - label: Option, - adapter: DebugAdapterName, - task_context: TaskContext, - parent_session: Option>, - quirks: SessionQuirks, - cx: &mut Context, - ) -> Entity { - let session_id = SessionId(util::post_inc(&mut self.next_session_id)); - - if let Some(session) = &parent_session { - session.update(cx, |session, _| { - session.add_child_session_id(session_id); - }); - } - - let (remote_client, node_runtime, http_client) = match &self.mode { - DapStoreMode::Local(_) => (None, None, None), - DapStoreMode::Remote(remote_dap_store) => ( - Some(remote_dap_store.remote_client.clone()), - Some(remote_dap_store.node_runtime.clone()), - Some(remote_dap_store.http_client.clone()), - ), - DapStoreMode::Collab => (None, None, None), - }; - let session = Session::new( - self.breakpoint_store.clone(), - session_id, - parent_session, - label, - adapter, - task_context, - quirks, - remote_client, - node_runtime, - http_client, - cx, - ); - - self.sessions.insert(session_id, session.clone()); - cx.notify(); - - cx.subscribe(&session, { - move |this: &mut DapStore, _, event: &SessionStateEvent, cx| match event { - SessionStateEvent::Shutdown => { - this.shutdown_session(session_id, cx).detach_and_log_err(cx); - } - SessionStateEvent::Restart | SessionStateEvent::SpawnChildSession { .. } => {} - SessionStateEvent::Running => { - cx.emit(DapStoreEvent::DebugClientStarted(session_id)); - } - } - }) - .detach(); - - session - } - - pub fn boot_session( - &self, - session: Entity, - definition: DebugTaskDefinition, - worktree: Entity, - cx: &mut Context, - ) -> Task> { - let dap_store = cx.weak_entity(); - let console = session.update(cx, |session, cx| session.console_output(cx)); - let session_id = session.read(cx).session_id(); - - cx.spawn({ - let session = session.clone(); - async move |this, cx| { - let binary = this - .update(cx, |this, cx| { - this.get_debug_adapter_binary( - definition.clone(), - session_id, - &worktree, - console, - cx, - ) - })? - .await?; - session - .update(cx, |session, cx| { - session.boot(binary, worktree, dap_store, cx) - })? - .await - } - }) - } - - pub fn session_by_id( - &self, - session_id: impl Borrow, - ) -> Option> { - let session_id = session_id.borrow(); - - self.sessions.get(session_id).cloned() - } - pub fn sessions(&self) -> impl Iterator> { - self.sessions.values() - } - - pub fn capabilities_by_id( - &self, - session_id: impl Borrow, - cx: &App, - ) -> Option { - let session_id = session_id.borrow(); - self.sessions - .get(session_id) - .map(|client| client.read(cx).capabilities.clone()) - } - - pub fn breakpoint_store(&self) -> &Entity { - &self.breakpoint_store - } - - pub fn worktree_store(&self) -> &Entity { - &self.worktree_store - } - - #[allow(dead_code)] - async fn handle_ignore_breakpoint_state( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - let session_id = SessionId::from_proto(envelope.payload.session_id); - - this.update(&mut cx, |this, cx| { - if let Some(session) = this.session_by_id(&session_id) { - session.update(cx, |session, cx| { - session.set_ignore_breakpoints(envelope.payload.ignore, cx) - }) - } else { - Task::ready(HashMap::default()) - } - })? - .await; - - Ok(()) - } - - fn delegate( - &self, - worktree: &Entity, - console: UnboundedSender, - cx: &mut App, - ) -> Arc { - let Some(local_store) = self.as_local() else { - unimplemented!("Starting session on remote side"); - }; - - Arc::new(DapAdapterDelegate::new( - local_store.fs.clone(), - worktree.read(cx).snapshot(), - console, - local_store.node_runtime.clone(), - local_store.http_client.clone(), - local_store.toolchain_store.clone(), - local_store - .environment - .update(cx, |env, cx| env.worktree_environment(worktree.clone(), cx)), - local_store.is_headless, - )) - } - - pub fn resolve_inline_value_locations( - &self, - session: Entity, - stack_frame_id: StackFrameId, - buffer_handle: Entity, - inline_value_locations: Vec, - cx: &mut Context, - ) -> Task>> { - let snapshot = buffer_handle.read(cx).snapshot(); - let local_variables = - session - .read(cx) - .variables_by_stack_frame_id(stack_frame_id, false, true); - let global_variables = - session - .read(cx) - .variables_by_stack_frame_id(stack_frame_id, true, false); - - fn format_value(mut value: String) -> String { - const LIMIT: usize = 100; - - if let Some(index) = value.find("\n") { - value.truncate(index); - value.push_str("…"); - } - - if value.len() > LIMIT { - let mut index = LIMIT; - // If index isn't a char boundary truncate will cause a panic - while !value.is_char_boundary(index) { - index -= 1; - } - value.truncate(index); - value.push_str("…"); - } - - format!(": {}", value) - } - - cx.spawn(async move |_, cx| { - let mut inlay_hints = Vec::with_capacity(inline_value_locations.len()); - for inline_value_location in inline_value_locations.iter() { - let point = snapshot.point_to_point_utf16(language::Point::new( - inline_value_location.row as u32, - inline_value_location.column as u32, - )); - let position = snapshot.anchor_after(point); - - match inline_value_location.lookup { - VariableLookupKind::Variable => { - let variable_search = - if inline_value_location.scope - == dap::inline_value::VariableScope::Local - { - local_variables.iter().chain(global_variables.iter()).find( - |variable| variable.name == inline_value_location.variable_name, - ) - } else { - global_variables.iter().find(|variable| { - variable.name == inline_value_location.variable_name - }) - }; - - let Some(variable) = variable_search else { - continue; - }; - - inlay_hints.push(InlayHint { - position, - label: InlayHintLabel::String(format_value(variable.value.clone())), - kind: Some(InlayHintKind::Type), - padding_left: false, - padding_right: false, - tooltip: None, - resolve_state: ResolveState::Resolved, - }); - } - VariableLookupKind::Expression => { - let Ok(eval_task) = session.read_with(cx, |session, _| { - session.state.request_dap(EvaluateCommand { - expression: inline_value_location.variable_name.clone(), - frame_id: Some(stack_frame_id), - source: None, - context: Some(EvaluateArgumentsContext::Variables), - }) - }) else { - continue; - }; - - if let Some(response) = eval_task.await.log_err() { - inlay_hints.push(InlayHint { - position, - label: InlayHintLabel::String(format_value(response.result)), - kind: Some(InlayHintKind::Type), - padding_left: false, - padding_right: false, - tooltip: None, - resolve_state: ResolveState::Resolved, - }); - }; - } - }; - } - - Ok(inlay_hints) - }) - } - - pub fn shutdown_sessions(&mut self, cx: &mut Context) -> Task<()> { - let mut tasks = vec![]; - for session_id in self.sessions.keys().cloned().collect::>() { - tasks.push(self.shutdown_session(session_id, cx)); - } - - cx.background_executor().spawn(async move { - futures::future::join_all(tasks).await; - }) - } - - pub fn shutdown_session( - &mut self, - session_id: SessionId, - cx: &mut Context, - ) -> Task> { - let Some(session) = self.sessions.remove(&session_id) else { - return Task::ready(Err(anyhow!("Could not find session: {:?}", session_id))); - }; - - let shutdown_children = session - .read(cx) - .child_session_ids() - .iter() - .map(|session_id| self.shutdown_session(*session_id, cx)) - .collect::>(); - - let shutdown_parent_task = if let Some(parent_session) = session - .read(cx) - .parent_id(cx) - .and_then(|session_id| self.session_by_id(session_id)) - { - let shutdown_id = parent_session.update(cx, |parent_session, _| { - parent_session.remove_child_session_id(session_id); - - if parent_session.child_session_ids().is_empty() { - Some(parent_session.session_id()) - } else { - None - } - }); - - shutdown_id.map(|session_id| self.shutdown_session(session_id, cx)) - } else { - None - }; - - let shutdown_task = session.update(cx, |this, cx| this.shutdown(cx)); - - cx.emit(DapStoreEvent::DebugClientShutdown(session_id)); - - cx.background_spawn(async move { - if !shutdown_children.is_empty() { - let _ = join_all(shutdown_children).await; - } - - shutdown_task.await; - - if let Some(parent_task) = shutdown_parent_task { - parent_task.await?; - } - - Ok(()) - }) - } - - pub fn shared( - &mut self, - project_id: u64, - downstream_client: AnyProtoClient, - _: &mut Context, - ) { - self.downstream_client = Some((downstream_client, project_id)); - } - - pub fn unshared(&mut self, cx: &mut Context) { - self.downstream_client.take(); - - cx.notify(); - } - - async fn handle_run_debug_locator( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let task = envelope - .payload - .build_command - .context("missing definition")?; - let build_task = SpawnInTerminal::from_proto(task); - let locator = envelope.payload.locator; - let request = this - .update(&mut cx, |this, cx| { - this.run_debug_locator(&locator, build_task, cx) - })? - .await?; - - Ok(request.to_proto()) - } - - async fn handle_get_debug_adapter_binary( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let definition = DebugTaskDefinition::from_proto( - envelope.payload.definition.context("missing definition")?, - )?; - let (tx, mut rx) = mpsc::unbounded(); - let session_id = envelope.payload.session_id; - cx.spawn({ - let this = this.clone(); - async move |cx| { - while let Some(message) = rx.next().await { - this.read_with(cx, |this, _| { - if let Some((downstream, project_id)) = this.downstream_client.clone() { - downstream - .send(proto::LogToDebugConsole { - project_id, - session_id, - message, - }) - .ok(); - } - }) - .ok(); - } - } - }) - .detach(); - - let worktree = this - .update(&mut cx, |this, cx| { - this.worktree_store - .read(cx) - .worktree_for_id(WorktreeId::from_proto(envelope.payload.worktree_id), cx) - })? - .context("Failed to find worktree with a given ID")?; - let binary = this - .update(&mut cx, |this, cx| { - this.get_debug_adapter_binary( - definition, - SessionId::from_proto(session_id), - &worktree, - tx, - cx, - ) - })? - .await?; - Ok(binary.to_proto()) - } - - async fn handle_log_to_debug_console( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - let session_id = SessionId::from_proto(envelope.payload.session_id); - this.update(&mut cx, |this, cx| { - let Some(session) = this.sessions.get(&session_id) else { - return; - }; - session.update(cx, |session, cx| { - session - .console_output(cx) - .unbounded_send(envelope.payload.message) - .ok(); - }) - }) - } - - pub fn sync_adapter_options( - &mut self, - session: &Entity, - cx: &App, - ) -> Arc { - let session = session.read(cx); - let adapter = session.adapter(); - let exceptions = session.exception_breakpoints(); - let exception_breakpoints = exceptions - .map(|(exception, enabled)| { - ( - exception.filter.clone(), - PersistedExceptionBreakpoint { enabled: *enabled }, - ) - }) - .collect(); - let options = Arc::new(PersistedAdapterOptions { - exception_breakpoints, - }); - self.adapter_options.insert(adapter, options.clone()); - options - } - - pub fn set_adapter_options( - &mut self, - adapter: DebugAdapterName, - options: PersistedAdapterOptions, - ) { - self.adapter_options.insert(adapter, Arc::new(options)); - } - - pub fn adapter_options(&self, name: &str) -> Option> { - self.adapter_options.get(name).cloned() - } - - pub fn all_adapter_options(&self) -> &BTreeMap> { - &self.adapter_options - } -} - -#[derive(Clone)] -pub struct DapAdapterDelegate { - fs: Arc, - console: mpsc::UnboundedSender, - worktree: worktree::Snapshot, - node_runtime: NodeRuntime, - http_client: Arc, - toolchain_store: Arc, - load_shell_env_task: Shared>>>, - is_headless: bool, -} - -impl DapAdapterDelegate { - pub fn new( - fs: Arc, - worktree: worktree::Snapshot, - status: mpsc::UnboundedSender, - node_runtime: NodeRuntime, - http_client: Arc, - toolchain_store: Arc, - load_shell_env_task: Shared>>>, - is_headless: bool, - ) -> Self { - Self { - fs, - console: status, - worktree, - http_client, - node_runtime, - toolchain_store, - load_shell_env_task, - is_headless, - } - } -} - -#[async_trait] -impl dap::adapters::DapDelegate for DapAdapterDelegate { - fn worktree_id(&self) -> WorktreeId { - self.worktree.id() - } - - fn worktree_root_path(&self) -> &Path { - self.worktree.abs_path() - } - fn http_client(&self) -> Arc { - self.http_client.clone() - } - - fn node_runtime(&self) -> NodeRuntime { - self.node_runtime.clone() - } - - fn fs(&self) -> Arc { - self.fs.clone() - } - - fn output_to_console(&self, msg: String) { - self.console.unbounded_send(msg).ok(); - } - - #[cfg(not(target_os = "windows"))] - async fn which(&self, command: &OsStr) -> Option { - let worktree_abs_path = self.worktree.abs_path(); - let shell_path = self.shell_env().await.get("PATH").cloned(); - which::which_in(command, shell_path.as_ref(), worktree_abs_path).ok() - } - - #[cfg(target_os = "windows")] - async fn which(&self, command: &OsStr) -> Option { - // On Windows, `PATH` is handled differently from Unix. Windows generally expects users to modify the `PATH` themselves, - // and every program loads it directly from the system at startup. - // There's also no concept of a default shell on Windows, and you can't really retrieve one, so trying to get shell environment variables - // from a specific directory doesn’t make sense on Windows. - which::which(command).ok() - } - - async fn shell_env(&self) -> HashMap { - let task = self.load_shell_env_task.clone(); - task.await.unwrap_or_default() - } - - fn toolchain_store(&self) -> Arc { - self.toolchain_store.clone() - } - - async fn read_text_file(&self, path: &RelPath) -> Result { - let entry = self - .worktree - .entry_for_path(path) - .with_context(|| format!("no worktree entry for path {path:?}"))?; - let abs_path = self.worktree.absolutize(&entry.path); - - self.fs.load(&abs_path).await - } - - fn is_headless(&self) -> bool { - self.is_headless - } -} diff --git a/crates/project/src/debugger/locators.rs b/crates/project/src/debugger/locators.rs deleted file mode 100644 index 2faa4c4ca9..0000000000 --- a/crates/project/src/debugger/locators.rs +++ /dev/null @@ -1,4 +0,0 @@ -pub(crate) mod cargo; -pub(crate) mod go; -pub(crate) mod node; -pub(crate) mod python; diff --git a/crates/project/src/debugger/locators/cargo.rs b/crates/project/src/debugger/locators/cargo.rs deleted file mode 100644 index 2f7d8cdc5f..0000000000 --- a/crates/project/src/debugger/locators/cargo.rs +++ /dev/null @@ -1,219 +0,0 @@ -use anyhow::{Context as _, Result}; -use async_trait::async_trait; -use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName}; -use gpui::SharedString; -use serde_json::{Value, json}; -use smol::{Timer, io::AsyncReadExt, process::Stdio}; -use std::time::Duration; -use task::{BuildTaskDefinition, DebugScenario, ShellBuilder, SpawnInTerminal, TaskTemplate}; -use util::command::new_smol_command; - -pub(crate) struct CargoLocator; - -async fn find_best_executable(executables: &[String], test_name: &str) -> Option { - if executables.len() == 1 { - return executables.first().cloned(); - } - for executable in executables { - let Some(mut child) = new_smol_command(&executable) - .arg("--list") - .stdout(Stdio::piped()) - .spawn() - .ok() - else { - continue; - }; - let mut test_lines = String::default(); - let exec_result = smol::future::race( - async { - if let Some(mut stdout) = child.stdout.take() { - stdout.read_to_string(&mut test_lines).await?; - } - Ok(()) - }, - async { - Timer::after(Duration::from_secs(3)).await; - anyhow::bail!("Timed out waiting for executable stdout") - }, - ); - - if let Err(err) = exec_result.await { - log::warn!("Failed to list tests for {executable}: {err}"); - } else { - for line in test_lines.lines() { - if line.contains(&test_name) { - return Some(executable.clone()); - } - } - } - let _ = child.kill(); - } - None -} -#[async_trait] -impl DapLocator for CargoLocator { - fn name(&self) -> SharedString { - SharedString::new_static("rust-cargo-locator") - } - async fn create_scenario( - &self, - build_config: &TaskTemplate, - resolved_label: &str, - adapter: &DebugAdapterName, - ) -> Option { - if build_config.command != "cargo" { - return None; - } - let mut task_template = build_config.clone(); - let cargo_action = task_template.args.first_mut()?; - if cargo_action == "check" || cargo_action == "clean" { - return None; - } - - match cargo_action.as_ref() { - "run" | "r" => { - *cargo_action = "build".to_owned(); - } - "test" | "t" | "bench" => { - let delimiter = task_template - .args - .iter() - .position(|arg| arg == "--") - .unwrap_or(task_template.args.len()); - if !task_template.args[..delimiter] - .iter() - .any(|arg| arg == "--no-run") - { - task_template.args.insert(delimiter, "--no-run".to_owned()); - } - } - _ => {} - } - - let config = if adapter.as_ref() == "CodeLLDB" { - json!({ - "sourceLanguages": ["rust"] - }) - } else { - Value::Null - }; - Some(DebugScenario { - adapter: adapter.0.clone(), - label: resolved_label.to_string().into(), - build: Some(BuildTaskDefinition::Template { - task_template, - locator_name: Some(self.name()), - }), - config, - tcp_connection: None, - }) - } - - async fn run(&self, build_config: SpawnInTerminal) -> Result { - let cwd = build_config - .cwd - .clone() - .context("Couldn't get cwd from debug config which is needed for locators")?; - let builder = ShellBuilder::new(&build_config.shell, cfg!(windows)).non_interactive(); - let mut child = builder - .build_command( - Some("cargo".into()), - &build_config - .args - .iter() - .cloned() - .take_while(|arg| arg != "--") - .chain(Some("--message-format=json".to_owned())) - .collect::>(), - ) - .envs(build_config.env.iter().map(|(k, v)| (k.clone(), v.clone()))) - .current_dir(cwd) - .stdout(Stdio::piped()) - .spawn()?; - - let mut output = String::new(); - if let Some(mut stdout) = child.stdout.take() { - stdout.read_to_string(&mut output).await?; - } - - let status = child.status().await?; - anyhow::ensure!(status.success(), "Cargo command failed"); - - let is_test = build_config - .args - .first() - .is_some_and(|arg| arg == "test" || arg == "t"); - - let is_ignored = build_config.args.contains(&"--include-ignored".to_owned()); - - let executables = output - .lines() - .filter(|line| !line.trim().is_empty()) - .filter_map(|line| serde_json::from_str(line).ok()) - .filter(|json: &Value| { - let is_test_binary = json - .get("profile") - .and_then(|profile| profile.get("test")) - .and_then(Value::as_bool) - .unwrap_or(false); - - if is_test { - is_test_binary - } else { - !is_test_binary - } - }) - .filter_map(|json: Value| { - json.get("executable") - .and_then(Value::as_str) - .map(String::from) - }) - .collect::>(); - anyhow::ensure!( - !executables.is_empty(), - "Couldn't get executable in cargo locator" - ); - - let mut test_name = None; - if is_test { - test_name = build_config - .args - .iter() - .rev() - .take_while(|name| "--" != name.as_str()) - .find(|name| !name.starts_with("-")) - .cloned(); - } - let executable = { - if let Some(name) = test_name.as_ref().and_then(|name| { - name.strip_prefix('$') - .map(|name| build_config.env.get(name)) - .unwrap_or(Some(name)) - }) { - find_best_executable(&executables, name).await - } else { - None - } - }; - - let Some(executable) = executable.or_else(|| executables.first().cloned()) else { - anyhow::bail!("Couldn't get executable in cargo locator"); - }; - - let mut args: Vec<_> = test_name.into_iter().collect(); - if is_test { - args.push("--nocapture".to_owned()); - if is_ignored { - args.push("--include-ignored".to_owned()); - args.push("--exact".to_owned()); - } - } - - Ok(DebugRequest::Launch(task::LaunchRequest { - program: executable, - cwd: build_config.cwd, - args, - env: build_config.env.into_iter().collect(), - })) - } -} diff --git a/crates/project/src/debugger/locators/go.rs b/crates/project/src/debugger/locators/go.rs deleted file mode 100644 index eec06084ec..0000000000 --- a/crates/project/src/debugger/locators/go.rs +++ /dev/null @@ -1,441 +0,0 @@ -use anyhow::Result; -use async_trait::async_trait; -use collections::HashMap; -use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName}; -use gpui::SharedString; -use serde::{Deserialize, Serialize}; -use task::{DebugScenario, SpawnInTerminal, TaskTemplate}; - -pub(crate) struct GoLocator; - -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] -#[serde(rename_all = "camelCase")] -struct DelveLaunchRequest { - request: String, - mode: String, - program: String, - #[serde(skip_serializing_if = "Option::is_none")] - cwd: Option, - args: Vec, - build_flags: Vec, - env: HashMap, -} - -fn is_debug_flag(arg: &str) -> Option { - let mut part = if let Some(suffix) = arg.strip_prefix("test.") { - suffix - } else { - arg - }; - let mut might_have_arg = true; - if let Some(idx) = part.find('=') { - might_have_arg = false; - part = &part[..idx]; - } - match part { - "benchmem" | "failfast" | "fullpath" | "fuzzworker" | "json" | "short" | "v" - | "paniconexit0" => Some(false), - "bench" - | "benchtime" - | "blockprofile" - | "blockprofilerate" - | "count" - | "coverprofile" - | "cpu" - | "cpuprofile" - | "fuzz" - | "fuzzcachedir" - | "fuzzminimizetime" - | "fuzztime" - | "gocoverdir" - | "list" - | "memprofile" - | "memprofilerate" - | "mutexprofile" - | "mutexprofilefraction" - | "outputdir" - | "parallel" - | "run" - | "shuffle" - | "skip" - | "testlogfile" - | "timeout" - | "trace" => Some(might_have_arg), - _ if arg.starts_with("test.") => Some(false), - _ => None, - } -} - -fn is_build_flag(mut arg: &str) -> Option { - let mut might_have_arg = true; - if let Some(idx) = arg.find('=') { - might_have_arg = false; - arg = &arg[..idx]; - } - match arg { - "a" | "n" | "race" | "msan" | "asan" | "cover" | "work" | "x" | "v" | "buildvcs" - | "json" | "linkshared" | "modcacherw" | "trimpath" => Some(false), - - "p" | "covermode" | "coverpkg" | "asmflags" | "buildmode" | "compiler" | "gccgoflags" - | "gcflags" | "installsuffix" | "ldflags" | "mod" | "modfile" | "overlay" | "pgo" - | "pkgdir" | "tags" | "toolexec" => Some(might_have_arg), - _ => None, - } -} - -#[async_trait] -impl DapLocator for GoLocator { - fn name(&self) -> SharedString { - SharedString::new_static("go-debug-locator") - } - - async fn create_scenario( - &self, - build_config: &TaskTemplate, - resolved_label: &str, - adapter: &DebugAdapterName, - ) -> Option { - if build_config.command != "go" { - return None; - } - let go_action = build_config.args.first()?; - - match go_action.as_str() { - "test" => { - let mut program = ".".to_string(); - let mut args = Vec::default(); - let mut build_flags = Vec::default(); - - let mut all_args_are_test = false; - let mut next_arg_is_test = false; - let mut next_arg_is_build = false; - let mut seen_pkg = false; - let mut seen_v = false; - - for arg in build_config.args.iter().skip(1) { - if all_args_are_test || next_arg_is_test { - // HACK: tasks assume that they are run in a shell context, - // so the -run regex has escaped specials. Delve correctly - // handles escaping, so we undo that here. - if let Some((left, right)) = arg.split_once("/") - && left.starts_with("\\^") - && left.ends_with("\\$") - && right.starts_with("\\^") - && right.ends_with("\\$") - { - let mut left = left[1..left.len() - 2].to_string(); - left.push('$'); - - let mut right = right[1..right.len() - 2].to_string(); - right.push('$'); - - args.push(format!("{left}/{right}")); - } else if arg.starts_with("\\^") && arg.ends_with("\\$") { - let mut arg = arg[1..arg.len() - 2].to_string(); - arg.push('$'); - args.push(arg); - } else { - args.push(arg.clone()); - } - next_arg_is_test = false; - } else if next_arg_is_build { - build_flags.push(arg.clone()); - next_arg_is_build = false; - } else if arg.starts_with('-') { - let flag = arg.trim_start_matches('-'); - if flag == "args" { - all_args_are_test = true; - } else if let Some(has_arg) = is_debug_flag(flag) { - if flag == "v" || flag == "test.v" { - seen_v = true; - } - if flag.starts_with("test.") { - args.push(arg.clone()); - } else { - args.push(format!("-test.{flag}")) - } - next_arg_is_test = has_arg; - } else if let Some(has_arg) = is_build_flag(flag) { - build_flags.push(arg.clone()); - next_arg_is_build = has_arg; - } - } else if !seen_pkg { - program = arg.clone(); - seen_pkg = true; - } else { - args.push(arg.clone()); - } - } - if !seen_v { - args.push("-test.v".to_string()); - } - - let config: serde_json::Value = serde_json::to_value(DelveLaunchRequest { - request: "launch".to_string(), - mode: "test".to_string(), - program, - args, - build_flags, - cwd: build_config.cwd.clone(), - env: build_config.env.clone(), - }) - .unwrap(); - - Some(DebugScenario { - label: resolved_label.to_string().into(), - adapter: adapter.0.clone(), - build: None, - config, - tcp_connection: None, - }) - } - "run" => { - let mut next_arg_is_build = false; - let mut seen_pkg = false; - - let mut program = ".".to_string(); - let mut args = Vec::default(); - let mut build_flags = Vec::default(); - - for arg in build_config.args.iter().skip(1) { - if seen_pkg { - args.push(arg.clone()) - } else if next_arg_is_build { - build_flags.push(arg.clone()); - next_arg_is_build = false; - } else if arg.starts_with("-") { - if let Some(has_arg) = is_build_flag(arg.trim_start_matches("-")) { - next_arg_is_build = has_arg; - } - build_flags.push(arg.clone()) - } else { - program = arg.to_string(); - seen_pkg = true; - } - } - - let config: serde_json::Value = serde_json::to_value(DelveLaunchRequest { - cwd: build_config.cwd.clone(), - env: build_config.env.clone(), - request: "launch".to_string(), - mode: "debug".to_string(), - program, - args, - build_flags, - }) - .unwrap(); - - Some(DebugScenario { - label: resolved_label.to_string().into(), - adapter: adapter.0.clone(), - build: None, - config, - tcp_connection: None, - }) - } - _ => None, - } - } - - async fn run(&self, _build_config: SpawnInTerminal) -> Result { - unreachable!() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::TestAppContext; - use task::{HideStrategy, RevealStrategy, RevealTarget, Shell, TaskTemplate}; - - #[gpui::test] - async fn test_create_scenario_for_go_build(_: &mut TestAppContext) { - let locator = GoLocator; - let task = TaskTemplate { - label: "go build".into(), - command: "go".into(), - args: vec!["build".into(), ".".into()], - env: Default::default(), - cwd: Some("${ZED_WORKTREE_ROOT}".into()), - use_new_terminal: false, - allow_concurrent_runs: false, - reveal: RevealStrategy::Always, - reveal_target: RevealTarget::Dock, - hide: HideStrategy::Never, - shell: Shell::System, - tags: vec![], - show_summary: true, - show_command: true, - }; - - let scenario = locator - .create_scenario(&task, "test label", &DebugAdapterName("Delve".into())) - .await; - - assert!(scenario.is_none()); - } - - #[gpui::test] - async fn test_skip_non_go_commands_with_non_delve_adapter(_: &mut TestAppContext) { - let locator = GoLocator; - let task = TaskTemplate { - label: "cargo build".into(), - command: "cargo".into(), - args: vec!["build".into()], - env: Default::default(), - cwd: Some("${ZED_WORKTREE_ROOT}".into()), - use_new_terminal: false, - allow_concurrent_runs: false, - reveal: RevealStrategy::Always, - reveal_target: RevealTarget::Dock, - hide: HideStrategy::Never, - shell: Shell::System, - tags: vec![], - show_summary: true, - show_command: true, - }; - - let scenario = locator - .create_scenario( - &task, - "test label", - &DebugAdapterName("SomeOtherAdapter".into()), - ) - .await; - assert!(scenario.is_none()); - - let scenario = locator - .create_scenario(&task, "test label", &DebugAdapterName("Delve".into())) - .await; - assert!(scenario.is_none()); - } - #[gpui::test] - async fn test_go_locator_run(_: &mut TestAppContext) { - let locator = GoLocator; - let delve = DebugAdapterName("Delve".into()); - - let task = TaskTemplate { - label: "go run with flags".into(), - command: "go".into(), - args: vec![ - "run".to_string(), - "-race".to_string(), - "-ldflags".to_string(), - "-X main.version=1.0".to_string(), - "./cmd/myapp".to_string(), - "--config".to_string(), - "production.yaml".to_string(), - "--verbose".to_string(), - ], - env: { - let mut env = HashMap::default(); - env.insert("GO_ENV".to_string(), "production".to_string()); - env - }, - cwd: Some("/project/root".into()), - ..Default::default() - }; - - let scenario = locator - .create_scenario(&task, "test run label", &delve) - .await - .unwrap(); - - let config: DelveLaunchRequest = serde_json::from_value(scenario.config).unwrap(); - - assert_eq!( - config, - DelveLaunchRequest { - request: "launch".to_string(), - mode: "debug".to_string(), - program: "./cmd/myapp".to_string(), - build_flags: vec![ - "-race".to_string(), - "-ldflags".to_string(), - "-X main.version=1.0".to_string() - ], - args: vec![ - "--config".to_string(), - "production.yaml".to_string(), - "--verbose".to_string(), - ], - env: { - let mut env = HashMap::default(); - env.insert("GO_ENV".to_string(), "production".to_string()); - env - }, - cwd: Some("/project/root".to_string()), - } - ); - } - - #[gpui::test] - async fn test_go_locator_test(_: &mut TestAppContext) { - let locator = GoLocator; - let delve = DebugAdapterName("Delve".into()); - - // Test with tags and run flag - let task_with_tags = TaskTemplate { - label: "test".into(), - command: "go".into(), - args: vec![ - "test".to_string(), - "-tags".to_string(), - "integration,unit".to_string(), - "-run".to_string(), - "Foo".to_string(), - ".".to_string(), - ], - ..Default::default() - }; - let result = locator - .create_scenario(&task_with_tags, "", &delve) - .await - .unwrap(); - - let config: DelveLaunchRequest = serde_json::from_value(result.config).unwrap(); - - assert_eq!( - config, - DelveLaunchRequest { - request: "launch".to_string(), - mode: "test".to_string(), - program: ".".to_string(), - build_flags: vec!["-tags".to_string(), "integration,unit".to_string(),], - args: vec![ - "-test.run".to_string(), - "Foo".to_string(), - "-test.v".to_string() - ], - env: HashMap::default(), - cwd: None, - } - ); - } - - #[gpui::test] - async fn test_skip_unsupported_go_commands(_: &mut TestAppContext) { - let locator = GoLocator; - let task = TaskTemplate { - label: "go clean".into(), - command: "go".into(), - args: vec!["clean".into()], - env: Default::default(), - cwd: Some("${ZED_WORKTREE_ROOT}".into()), - use_new_terminal: false, - allow_concurrent_runs: false, - reveal: RevealStrategy::Always, - reveal_target: RevealTarget::Dock, - hide: HideStrategy::Never, - shell: Shell::System, - tags: vec![], - show_summary: true, - show_command: true, - }; - - let scenario = locator - .create_scenario(&task, "test label", &DebugAdapterName("Delve".into())) - .await; - assert!(scenario.is_none()); - } -} diff --git a/crates/project/src/debugger/locators/node.rs b/crates/project/src/debugger/locators/node.rs deleted file mode 100644 index a535c7165a..0000000000 --- a/crates/project/src/debugger/locators/node.rs +++ /dev/null @@ -1,62 +0,0 @@ -use std::borrow::Cow; - -use anyhow::{Result, bail}; -use async_trait::async_trait; -use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName}; -use gpui::SharedString; - -use task::{DebugScenario, SpawnInTerminal, TaskTemplate, VariableName}; - -pub(crate) struct NodeLocator; - -const TYPESCRIPT_RUNNER_VARIABLE: VariableName = - VariableName::Custom(Cow::Borrowed("TYPESCRIPT_RUNNER")); - -#[async_trait] -impl DapLocator for NodeLocator { - fn name(&self) -> SharedString { - SharedString::new_static("Node") - } - - /// Determines whether this locator can generate debug target for given task. - async fn create_scenario( - &self, - build_config: &TaskTemplate, - resolved_label: &str, - adapter: &DebugAdapterName, - ) -> Option { - if adapter.0.as_ref() != "JavaScript" { - return None; - } - if build_config.command != TYPESCRIPT_RUNNER_VARIABLE.template_value() - && build_config.command != "npm" - && build_config.command != "pnpm" - && build_config.command != "yarn" - { - return None; - } - - let config = serde_json::json!({ - "request": "launch", - "type": "pwa-node", - "args": build_config.args.clone(), - "cwd": build_config.cwd.clone(), - "runtimeExecutable": build_config.command.clone(), - "env": build_config.env.clone(), - "runtimeArgs": ["--inspect-brk"], - "console": "integratedTerminal", - }); - - Some(DebugScenario { - adapter: adapter.0.clone(), - label: resolved_label.to_string().into(), - build: None, - config, - tcp_connection: None, - }) - } - - async fn run(&self, _: SpawnInTerminal) -> Result { - bail!("JavaScript locator should not require DapLocator::run to be ran"); - } -} diff --git a/crates/project/src/debugger/locators/python.rs b/crates/project/src/debugger/locators/python.rs deleted file mode 100644 index c3754548d0..0000000000 --- a/crates/project/src/debugger/locators/python.rs +++ /dev/null @@ -1,146 +0,0 @@ -use std::path::Path; - -use anyhow::{Result, bail}; -use async_trait::async_trait; -use dap::{DapLocator, DebugRequest, adapters::DebugAdapterName}; -use gpui::SharedString; - -use task::{DebugScenario, SpawnInTerminal, TaskTemplate, VariableName}; - -pub(crate) struct PythonLocator; - -#[async_trait] -impl DapLocator for PythonLocator { - fn name(&self) -> SharedString { - SharedString::new_static("Python") - } - - /// Determines whether this locator can generate debug target for given task. - async fn create_scenario( - &self, - build_config: &TaskTemplate, - resolved_label: &str, - adapter: &DebugAdapterName, - ) -> Option { - if adapter.0.as_ref() != "Debugpy" { - return None; - } - let valid_program = build_config.command.starts_with("$ZED_") - || Path::new(&build_config.command) - .file_name() - .is_some_and(|name| name.to_str().is_some_and(|path| path.starts_with("python"))); - if !valid_program || build_config.args.iter().any(|arg| arg == "-c") { - // We cannot debug selections. - return None; - } - let command = build_config.command.clone(); - let module_specifier_position = build_config - .args - .iter() - .position(|arg| arg == "-m") - .map(|position| position + 1); - // Skip the -m and module name, get all that's after. - let mut rest_of_the_args = module_specifier_position - .and_then(|position| build_config.args.get(position..)) - .into_iter() - .flatten() - .fuse(); - let mod_name = rest_of_the_args.next(); - let args = rest_of_the_args.collect::>(); - - let program_position = mod_name - .is_none() - .then(|| { - let zed_file = VariableName::File.template_value_with_whitespace(); - build_config.args.iter().position(|arg| *arg == zed_file) - }) - .flatten(); - let args = if let Some(position) = program_position { - args.into_iter().skip(position).collect::>() - } else { - args - }; - if program_position.is_none() && mod_name.is_none() { - return None; - } - let mut config = serde_json::json!({ - "request": "launch", - "python": command, - "args": args, - "cwd": build_config.cwd.clone() - }); - if let Some(config_obj) = config.as_object_mut() { - if let Some(module) = mod_name { - config_obj.insert("module".to_string(), module.clone().into()); - } - if let Some(program) = program_position { - config_obj.insert( - "program".to_string(), - build_config.args[program].clone().into(), - ); - } - } - - Some(DebugScenario { - adapter: adapter.0.clone(), - label: resolved_label.to_string().into(), - build: None, - config, - tcp_connection: None, - }) - } - - async fn run(&self, _: SpawnInTerminal) -> Result { - bail!("Python locator should not require DapLocator::run to be ran"); - } -} - -#[cfg(test)] -mod test { - use serde_json::json; - - use super::*; - - #[gpui::test] - async fn test_python_locator() { - let adapter = DebugAdapterName("Debugpy".into()); - let build_task = TaskTemplate { - label: "run module '$ZED_FILE'".into(), - command: "$ZED_CUSTOM_PYTHON_ACTIVE_ZED_TOOLCHAIN".into(), - args: vec!["-m".into(), "$ZED_CUSTOM_PYTHON_MODULE_NAME".into()], - env: Default::default(), - cwd: Some("$ZED_WORKTREE_ROOT".into()), - use_new_terminal: false, - allow_concurrent_runs: false, - reveal: task::RevealStrategy::Always, - reveal_target: task::RevealTarget::Dock, - hide: task::HideStrategy::Never, - tags: vec!["python-module-main-method".into()], - shell: task::Shell::System, - show_summary: false, - show_command: false, - }; - - let expected_scenario = DebugScenario { - adapter: "Debugpy".into(), - label: "run module 'main.py'".into(), - build: None, - config: json!({ - "request": "launch", - "python": "$ZED_CUSTOM_PYTHON_ACTIVE_ZED_TOOLCHAIN", - "args": [], - "cwd": "$ZED_WORKTREE_ROOT", - "module": "$ZED_CUSTOM_PYTHON_MODULE_NAME", - }), - tcp_connection: None, - }; - - assert_eq!( - PythonLocator - .create_scenario(&build_task, "run module 'main.py'", &adapter) - .await - .expect("Failed to create a scenario"), - expected_scenario - ); - } -} diff --git a/crates/project/src/debugger/memory.rs b/crates/project/src/debugger/memory.rs deleted file mode 100644 index 42ad64e688..0000000000 --- a/crates/project/src/debugger/memory.rs +++ /dev/null @@ -1,384 +0,0 @@ -//! This module defines the format in which memory of debuggee is represented. -//! -//! Each byte in memory can either be mapped or unmapped. We try to mimic that twofold: -//! - We assume that the memory is divided into pages of a fixed size. -//! - We assume that each page can be either mapped or unmapped. -//! -//! These two assumptions drive the shape of the memory representation. -//! In particular, we want the unmapped pages to be represented without allocating any memory, as *most* -//! of the memory in a program space is usually unmapped. -//! Note that per DAP we don't know what the address space layout is, so we can't optimize off of it. -//! Note that while we optimize for a paged layout, we also want to be able to represent memory that is not paged. -//! This use case is relevant to embedded folks. Furthermore, we cater to default 4k page size. -//! It is picked arbitrarily as a ubiquous default - other than that, the underlying format of Zed's memory storage should not be relevant -//! to the users of this module. - -use std::{collections::BTreeMap, ops::RangeInclusive, sync::Arc}; - -use gpui::BackgroundExecutor; -use smallvec::SmallVec; - -const PAGE_SIZE: u64 = 4096; - -/// Represents the contents of a single page. We special-case unmapped pages to be allocation-free, -/// since they're going to make up the majority of the memory in a program space (even though the user might not even get to see them - ever). -#[derive(Clone, Debug)] -pub(super) enum PageContents { - /// Whole page is unreadable. - Unmapped, - Mapped(Arc), -} - -impl PageContents { - #[cfg(test)] - fn mapped(contents: Vec) -> Self { - PageContents::Mapped(Arc::new(MappedPageContents( - vec![PageChunk::Mapped(contents.into())].into(), - ))) - } -} - -#[derive(Clone, Debug)] -enum PageChunk { - Mapped(Arc<[u8]>), - Unmapped(u64), -} - -impl PageChunk { - fn len(&self) -> u64 { - match self { - PageChunk::Mapped(contents) => contents.len() as u64, - PageChunk::Unmapped(size) => *size, - } - } -} - -impl MappedPageContents { - fn len(&self) -> u64 { - self.0.iter().map(|chunk| chunk.len()).sum() - } -} -/// We hope for the whole page to be mapped in a single chunk, but we do leave the possibility open -/// of having interleaved read permissions in a single page; debuggee's execution environment might either -/// have a different page size OR it might not have paged memory layout altogether -/// (which might be relevant to embedded systems). -/// -/// As stated previously, the concept of a page in this module has to do more -/// with optimizing fetching of the memory and not with the underlying bits and pieces -/// of the memory of a debuggee. - -#[derive(Default, Debug)] -pub(super) struct MappedPageContents( - /// Most of the time there should be only one chunk (either mapped or unmapped), - /// but we do leave the possibility open of having multiple regions of memory in a single page. - SmallVec<[PageChunk; 1]>, -); - -type MemoryAddress = u64; -#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Ord, Eq)] -#[repr(transparent)] -pub(super) struct PageAddress(u64); - -impl PageAddress { - pub(super) fn iter_range( - range: RangeInclusive, - ) -> impl Iterator { - let mut current = range.start().0; - let end = range.end().0; - - std::iter::from_fn(move || { - if current > end { - None - } else { - let addr = PageAddress(current); - current += PAGE_SIZE; - Some(addr) - } - }) - } -} - -pub(super) struct Memory { - pages: BTreeMap, -} - -/// Represents a single memory cell (or None if a given cell is unmapped/unknown). -#[derive(Copy, Clone, Debug, PartialEq, PartialOrd, Ord, Eq)] -#[repr(transparent)] -pub struct MemoryCell(pub Option); - -impl Memory { - pub(super) fn new() -> Self { - Self { - pages: Default::default(), - } - } - - pub(super) fn memory_range_to_page_range( - range: RangeInclusive, - ) -> RangeInclusive { - let start_page = (range.start() / PAGE_SIZE) * PAGE_SIZE; - let end_page = (range.end() / PAGE_SIZE) * PAGE_SIZE; - PageAddress(start_page)..=PageAddress(end_page) - } - - pub(super) fn build_page(&self, page_address: PageAddress) -> Option { - if self.pages.contains_key(&page_address) { - // We already know the state of this page. - None - } else { - Some(MemoryPageBuilder::new(page_address)) - } - } - - pub(super) fn insert_page(&mut self, address: PageAddress, page: PageContents) { - self.pages.insert(address, page); - } - - pub(super) fn memory_range(&self, range: RangeInclusive) -> MemoryIterator { - let pages = Self::memory_range_to_page_range(range.clone()); - let pages = self - .pages - .range(pages) - .map(|(address, page)| (*address, page.clone())) - .collect::>(); - MemoryIterator::new(range, pages.into_iter()) - } - - pub(crate) fn clear(&mut self, background_executor: &BackgroundExecutor) { - let memory = std::mem::take(&mut self.pages); - background_executor - .spawn(async move { - drop(memory); - }) - .detach(); - } -} - -/// Builder for memory pages. -/// -/// Memory reads in DAP are sequential (or at least we make them so). -/// ReadMemory response includes `unreadableBytes` property indicating the number of bytes -/// that could not be read after the last successfully read byte. -/// -/// We use it as follows: -/// - We start off with a "large" 1-page ReadMemory request. -/// - If it succeeds/fails wholesale, cool; we have no unknown memory regions in this page. -/// - If it succeeds partially, we know # of mapped bytes. -/// We might also know the # of unmapped bytes. -/// -/// However, we're still unsure about what's *after* the unreadable region. -/// This is where this builder comes in. It lets us track the state of figuring out contents of a single page. -pub(super) struct MemoryPageBuilder { - chunks: MappedPageContents, - base_address: PageAddress, - left_to_read: u64, -} - -/// Represents a chunk of memory of which we don't know if it's mapped or unmapped; thus we need -/// to issue a request to figure out it's state. -pub(super) struct UnknownMemory { - pub(super) address: MemoryAddress, - pub(super) size: u64, -} - -impl MemoryPageBuilder { - fn new(base_address: PageAddress) -> Self { - Self { - chunks: Default::default(), - base_address, - left_to_read: PAGE_SIZE, - } - } - - pub(super) fn build(self) -> (PageAddress, PageContents) { - debug_assert_eq!(self.left_to_read, 0); - debug_assert_eq!( - self.chunks.len(), - PAGE_SIZE, - "Expected `build` to be called on a fully-fetched page" - ); - let contents = if let Some(first) = self.chunks.0.first() - && self.chunks.len() == 1 - && matches!(first, PageChunk::Unmapped(PAGE_SIZE)) - { - PageContents::Unmapped - } else { - PageContents::Mapped(Arc::new(MappedPageContents(self.chunks.0))) - }; - (self.base_address, contents) - } - /// Drives the fetching of memory, in an iterator-esque style. - pub(super) fn next_request(&self) -> Option { - if self.left_to_read == 0 { - None - } else { - let offset_in_current_page = PAGE_SIZE - self.left_to_read; - Some(UnknownMemory { - address: self.base_address.0 + offset_in_current_page, - size: self.left_to_read, - }) - } - } - pub(super) fn unknown(&mut self, bytes: u64) { - if bytes == 0 { - return; - } - self.left_to_read -= bytes; - self.chunks.0.push(PageChunk::Unmapped(bytes)); - } - pub(super) fn known(&mut self, data: Arc<[u8]>) { - if data.is_empty() { - return; - } - self.left_to_read -= data.len() as u64; - self.chunks.0.push(PageChunk::Mapped(data)); - } -} - -fn page_contents_into_iter(data: Arc) -> Box> { - let mut data_range = 0..data.0.len(); - let iter = std::iter::from_fn(move || { - let data = &data; - let data_ref = data.clone(); - data_range.next().map(move |index| { - let contents = &data_ref.0[index]; - match contents { - PageChunk::Mapped(items) => { - let chunk_range = 0..items.len(); - let items = items.clone(); - Box::new( - chunk_range - .into_iter() - .map(move |ix| MemoryCell(Some(items[ix]))), - ) as Box> - } - PageChunk::Unmapped(len) => { - Box::new(std::iter::repeat_n(MemoryCell(None), *len as usize)) - } - } - }) - }) - .flatten(); - - Box::new(iter) -} -/// Defines an iteration over a range of memory. Some of this memory might be unmapped or straight up missing. -/// Thus, this iterator alternates between synthesizing values and yielding known memory. -pub struct MemoryIterator { - start: MemoryAddress, - end: MemoryAddress, - current_known_page: Option<(PageAddress, Box>)>, - pages: std::vec::IntoIter<(PageAddress, PageContents)>, -} - -impl MemoryIterator { - fn new( - range: RangeInclusive, - pages: std::vec::IntoIter<(PageAddress, PageContents)>, - ) -> Self { - Self { - start: *range.start(), - end: *range.end(), - current_known_page: None, - pages, - } - } - fn fetch_next_page(&mut self) -> bool { - if let Some((mut address, chunk)) = self.pages.next() { - let mut contents = match chunk { - PageContents::Unmapped => None, - PageContents::Mapped(mapped_page_contents) => { - Some(page_contents_into_iter(mapped_page_contents)) - } - }; - - if address.0 < self.start { - // Skip ahead till our iterator is at the start of the range - - //address: 20, start: 25 - // - let to_skip = self.start - address.0; - address.0 += to_skip; - if let Some(contents) = &mut contents { - contents.nth(to_skip as usize - 1); - } - } - self.current_known_page = contents.map(|contents| (address, contents)); - true - } else { - false - } - } -} -impl Iterator for MemoryIterator { - type Item = MemoryCell; - - fn next(&mut self) -> Option { - if self.start > self.end { - return None; - } - if let Some((current_page_address, current_memory_chunk)) = self.current_known_page.as_mut() - && current_page_address.0 <= self.start - { - if let Some(next_cell) = current_memory_chunk.next() { - self.start += 1; - return Some(next_cell); - } else { - self.current_known_page.take(); - } - } - if !self.fetch_next_page() { - self.start += 1; - Some(MemoryCell(None)) - } else { - self.next() - } - } -} - -#[cfg(test)] -mod tests { - use crate::debugger::{ - MemoryCell, - memory::{MemoryIterator, PageAddress, PageContents}, - }; - - #[test] - fn iterate_over_unmapped_memory() { - let empty_iterator = MemoryIterator::new(0..=127, Default::default()); - let actual = empty_iterator.collect::>(); - let expected = vec![MemoryCell(None); 128]; - assert_eq!(actual.len(), expected.len()); - assert_eq!(actual, expected); - } - - #[test] - fn iterate_over_partially_mapped_memory() { - let it = MemoryIterator::new( - 0..=127, - vec![(PageAddress(5), PageContents::mapped(vec![1]))].into_iter(), - ); - let actual = it.collect::>(); - let expected = std::iter::repeat_n(MemoryCell(None), 5) - .chain(std::iter::once(MemoryCell(Some(1)))) - .chain(std::iter::repeat_n(MemoryCell(None), 122)) - .collect::>(); - assert_eq!(actual.len(), expected.len()); - assert_eq!(actual, expected); - } - - #[test] - fn reads_from_the_middle_of_a_page() { - let partial_iter = MemoryIterator::new( - 20..=30, - vec![(PageAddress(0), PageContents::mapped((0..255).collect()))].into_iter(), - ); - let actual = partial_iter.collect::>(); - let expected = (20..=30) - .map(|val| MemoryCell(Some(val))) - .collect::>(); - assert_eq!(actual.len(), expected.len()); - assert_eq!(actual, expected); - } -} diff --git a/crates/project/src/debugger/session.rs b/crates/project/src/debugger/session.rs deleted file mode 100644 index 82a139ea24..0000000000 --- a/crates/project/src/debugger/session.rs +++ /dev/null @@ -1,3177 +0,0 @@ -use super::breakpoint_store::{ - BreakpointStore, BreakpointStoreEvent, BreakpointUpdatedReason, SourceBreakpoint, -}; -use super::dap_command::{ - self, Attach, ConfigurationDone, ContinueCommand, DataBreakpointInfoCommand, DisconnectCommand, - EvaluateCommand, Initialize, Launch, LoadedSourcesCommand, LocalDapCommand, LocationsCommand, - ModulesCommand, NextCommand, PauseCommand, RestartCommand, RestartStackFrameCommand, - ScopesCommand, SetDataBreakpointsCommand, SetExceptionBreakpoints, SetVariableValueCommand, - StackTraceCommand, StepBackCommand, StepCommand, StepInCommand, StepOutCommand, - TerminateCommand, TerminateThreadsCommand, ThreadsCommand, VariablesCommand, -}; -use super::dap_store::DapStore; -use crate::debugger::breakpoint_store::BreakpointSessionState; -use crate::debugger::dap_command::{DataBreakpointContext, ReadMemory}; -use crate::debugger::memory::{self, Memory, MemoryIterator, MemoryPageBuilder, PageAddress}; -use anyhow::{Context as _, Result, anyhow, bail}; -use base64::Engine; -use collections::{HashMap, HashSet, IndexMap}; -use dap::adapters::{DebugAdapterBinary, DebugAdapterName}; -use dap::messages::Response; -use dap::requests::{Request, RunInTerminal, StartDebugging}; -use dap::transport::TcpTransport; -use dap::{ - Capabilities, ContinueArguments, EvaluateArgumentsContext, Module, Source, StackFrameId, - SteppingGranularity, StoppedEvent, VariableReference, - client::{DebugAdapterClient, SessionId}, - messages::{Events, Message}, -}; -use dap::{ - ExceptionBreakpointsFilter, ExceptionFilterOptions, OutputEvent, OutputEventCategory, - RunInTerminalRequestArguments, StackFramePresentationHint, StartDebuggingRequestArguments, - StartDebuggingRequestArgumentsRequest, VariablePresentationHint, WriteMemoryArguments, -}; -use futures::channel::mpsc::UnboundedSender; -use futures::channel::{mpsc, oneshot}; -use futures::io::BufReader; -use futures::{AsyncBufReadExt as _, SinkExt, StreamExt, TryStreamExt}; -use futures::{FutureExt, future::Shared}; -use gpui::{ - App, AppContext, AsyncApp, BackgroundExecutor, Context, Entity, EventEmitter, SharedString, - Task, WeakEntity, -}; -use http_client::HttpClient; -use node_runtime::NodeRuntime; -use remote::RemoteClient; -use serde::{Deserialize, Serialize}; -use serde_json::Value; -use smol::net::{TcpListener, TcpStream}; -use std::any::TypeId; -use std::collections::{BTreeMap, VecDeque}; -use std::net::Ipv4Addr; -use std::ops::RangeInclusive; -use std::path::PathBuf; -use std::process::Stdio; -use std::time::Duration; -use std::u64; -use std::{ - any::Any, - collections::hash_map::Entry, - hash::{Hash, Hasher}, - path::Path, - sync::Arc, -}; -use task::TaskContext; -use text::{PointUtf16, ToPointUtf16}; -use url::Url; -use util::command::new_smol_command; -use util::{ResultExt, debug_panic, maybe}; -use worktree::Worktree; - -const MAX_TRACKED_OUTPUT_EVENTS: usize = 5000; -const DEBUG_HISTORY_LIMIT: usize = 10; - -#[derive(Debug, Copy, Clone, Hash, PartialEq, PartialOrd, Ord, Eq)] -#[repr(transparent)] -pub struct ThreadId(pub i64); - -impl From for ThreadId { - fn from(id: i64) -> Self { - Self(id) - } -} - -#[derive(Clone, Debug)] -pub struct StackFrame { - pub dap: dap::StackFrame, - pub scopes: Vec, -} - -impl From for StackFrame { - fn from(stack_frame: dap::StackFrame) -> Self { - Self { - scopes: vec![], - dap: stack_frame, - } - } -} - -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] -pub enum ThreadStatus { - #[default] - Running, - Stopped, - Stepping, - Exited, - Ended, -} - -impl ThreadStatus { - pub fn label(&self) -> &'static str { - match self { - ThreadStatus::Running => "Running", - ThreadStatus::Stopped => "Stopped", - ThreadStatus::Stepping => "Stepping", - ThreadStatus::Exited => "Exited", - ThreadStatus::Ended => "Ended", - } - } -} - -#[derive(Debug, Clone)] -pub struct Thread { - dap: dap::Thread, - stack_frames: Vec, - stack_frames_error: Option, - _has_stopped: bool, -} - -impl From for Thread { - fn from(dap: dap::Thread) -> Self { - Self { - dap, - stack_frames: Default::default(), - stack_frames_error: None, - _has_stopped: false, - } - } -} - -#[derive(Debug, Clone, PartialEq)] -pub struct Watcher { - pub expression: SharedString, - pub value: SharedString, - pub variables_reference: u64, - pub presentation_hint: Option, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct DataBreakpointState { - pub dap: dap::DataBreakpoint, - pub is_enabled: bool, - pub context: Arc, -} - -pub enum SessionState { - /// Represents a session that is building/initializing - /// even if a session doesn't have a pre build task this state - /// is used to run all the async tasks that are required to start the session - Booting(Option>>), - Running(RunningMode), -} - -#[derive(Clone)] -pub struct RunningMode { - client: Arc, - binary: DebugAdapterBinary, - tmp_breakpoint: Option, - worktree: WeakEntity, - executor: BackgroundExecutor, - is_started: bool, - has_ever_stopped: bool, - messages_tx: UnboundedSender, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)] -pub struct SessionQuirks { - pub compact: bool, - pub prefer_thread_name: bool, -} - -fn client_source(abs_path: &Path) -> dap::Source { - dap::Source { - name: abs_path - .file_name() - .map(|filename| filename.to_string_lossy().into_owned()), - path: Some(abs_path.to_string_lossy().into_owned()), - source_reference: None, - presentation_hint: None, - origin: None, - sources: None, - adapter_data: None, - checksums: None, - } -} - -impl RunningMode { - async fn new( - session_id: SessionId, - parent_session: Option>, - worktree: WeakEntity, - binary: DebugAdapterBinary, - messages_tx: futures::channel::mpsc::UnboundedSender, - cx: &mut AsyncApp, - ) -> Result { - let message_handler = Box::new({ - let messages_tx = messages_tx.clone(); - move |message| { - messages_tx.unbounded_send(message).ok(); - } - }); - - let client = if let Some(client) = parent_session - .and_then(|session| cx.update(|cx| session.read(cx).adapter_client()).ok()) - .flatten() - { - client - .create_child_connection(session_id, binary.clone(), message_handler, cx) - .await? - } else { - DebugAdapterClient::start(session_id, binary.clone(), message_handler, cx).await? - }; - - Ok(Self { - client: Arc::new(client), - worktree, - tmp_breakpoint: None, - binary, - executor: cx.background_executor().clone(), - is_started: false, - has_ever_stopped: false, - messages_tx, - }) - } - - pub(crate) fn worktree(&self) -> &WeakEntity { - &self.worktree - } - - fn unset_breakpoints_from_paths(&self, paths: &Vec>, cx: &mut App) -> Task<()> { - let tasks: Vec<_> = paths - .iter() - .map(|path| { - self.request(dap_command::SetBreakpoints { - source: client_source(path), - source_modified: None, - breakpoints: vec![], - }) - }) - .collect(); - - cx.background_spawn(async move { - futures::future::join_all(tasks) - .await - .iter() - .for_each(|res| match res { - Ok(_) => {} - Err(err) => { - log::warn!("Set breakpoints request failed: {}", err); - } - }); - }) - } - - fn send_breakpoints_from_path( - &self, - abs_path: Arc, - reason: BreakpointUpdatedReason, - breakpoint_store: &Entity, - cx: &mut App, - ) -> Task<()> { - let breakpoints = - breakpoint_store - .read(cx) - .source_breakpoints_from_path(&abs_path, cx) - .into_iter() - .filter(|bp| bp.state.is_enabled()) - .chain(self.tmp_breakpoint.iter().filter_map(|breakpoint| { - breakpoint.path.eq(&abs_path).then(|| breakpoint.clone()) - })) - .map(Into::into) - .collect(); - - let raw_breakpoints = breakpoint_store - .read(cx) - .breakpoints_from_path(&abs_path) - .into_iter() - .filter(|bp| bp.bp.state.is_enabled()) - .collect::>(); - - let task = self.request(dap_command::SetBreakpoints { - source: client_source(&abs_path), - source_modified: Some(matches!(reason, BreakpointUpdatedReason::FileSaved)), - breakpoints, - }); - let session_id = self.client.id(); - let breakpoint_store = breakpoint_store.downgrade(); - cx.spawn(async move |cx| match cx.background_spawn(task).await { - Ok(breakpoints) => { - let breakpoints = - breakpoints - .into_iter() - .zip(raw_breakpoints) - .filter_map(|(dap_bp, zed_bp)| { - Some(( - zed_bp, - BreakpointSessionState { - id: dap_bp.id?, - verified: dap_bp.verified, - }, - )) - }); - breakpoint_store - .update(cx, |this, _| { - this.mark_breakpoints_verified(session_id, &abs_path, breakpoints); - }) - .ok(); - } - Err(err) => log::warn!("Set breakpoints request failed for path: {}", err), - }) - } - - fn send_exception_breakpoints( - &self, - filters: Vec, - supports_filter_options: bool, - ) -> Task>> { - let arg = if supports_filter_options { - SetExceptionBreakpoints::WithOptions { - filters: filters - .into_iter() - .map(|filter| ExceptionFilterOptions { - filter_id: filter.filter, - condition: None, - mode: None, - }) - .collect(), - } - } else { - SetExceptionBreakpoints::Plain { - filters: filters.into_iter().map(|filter| filter.filter).collect(), - } - }; - self.request(arg) - } - - fn send_source_breakpoints( - &self, - ignore_breakpoints: bool, - breakpoint_store: &Entity, - cx: &App, - ) -> Task, anyhow::Error>> { - let mut breakpoint_tasks = Vec::new(); - let breakpoints = breakpoint_store.read(cx).all_source_breakpoints(cx); - let mut raw_breakpoints = breakpoint_store.read_with(cx, |this, _| this.all_breakpoints()); - debug_assert_eq!(raw_breakpoints.len(), breakpoints.len()); - let session_id = self.client.id(); - for (path, breakpoints) in breakpoints { - let breakpoints = if ignore_breakpoints { - vec![] - } else { - breakpoints - .into_iter() - .filter(|bp| bp.state.is_enabled()) - .map(Into::into) - .collect() - }; - - let raw_breakpoints = raw_breakpoints - .remove(&path) - .unwrap_or_default() - .into_iter() - .filter(|bp| bp.bp.state.is_enabled()); - let error_path = path.clone(); - let send_request = self - .request(dap_command::SetBreakpoints { - source: client_source(&path), - source_modified: Some(false), - breakpoints, - }) - .map(|result| result.map_err(move |e| (error_path, e))); - - let task = cx.spawn({ - let breakpoint_store = breakpoint_store.downgrade(); - async move |cx| { - let breakpoints = cx.background_spawn(send_request).await?; - - let breakpoints = breakpoints.into_iter().zip(raw_breakpoints).filter_map( - |(dap_bp, zed_bp)| { - Some(( - zed_bp, - BreakpointSessionState { - id: dap_bp.id?, - verified: dap_bp.verified, - }, - )) - }, - ); - breakpoint_store - .update(cx, |this, _| { - this.mark_breakpoints_verified(session_id, &path, breakpoints); - }) - .ok(); - - Ok(()) - } - }); - breakpoint_tasks.push(task); - } - - cx.background_spawn(async move { - futures::future::join_all(breakpoint_tasks) - .await - .into_iter() - .filter_map(Result::err) - .collect::>() - }) - } - - fn initialize_sequence( - &self, - capabilities: &Capabilities, - initialized_rx: oneshot::Receiver<()>, - dap_store: WeakEntity, - cx: &mut Context, - ) -> Task> { - let raw = self.binary.request_args.clone(); - - // Of relevance: https://github.com/microsoft/vscode/issues/4902#issuecomment-368583522 - let launch = match raw.request { - dap::StartDebuggingRequestArgumentsRequest::Launch => self.request(Launch { - raw: raw.configuration, - }), - dap::StartDebuggingRequestArgumentsRequest::Attach => self.request(Attach { - raw: raw.configuration, - }), - }; - - let configuration_done_supported = ConfigurationDone::is_supported(capabilities); - // From spec (on initialization sequence): - // client sends a setExceptionBreakpoints request if one or more exceptionBreakpointFilters have been defined (or if supportsConfigurationDoneRequest is not true) - // - // Thus we should send setExceptionBreakpoints even if `exceptionFilters` variable is empty (as long as there were some options in the first place). - let should_send_exception_breakpoints = capabilities - .exception_breakpoint_filters - .as_ref() - .is_some_and(|filters| !filters.is_empty()) - || !configuration_done_supported; - let supports_exception_filters = capabilities - .supports_exception_filter_options - .unwrap_or_default(); - let this = self.clone(); - let worktree = self.worktree().clone(); - let mut filters = capabilities - .exception_breakpoint_filters - .clone() - .unwrap_or_default(); - let configuration_sequence = cx.spawn({ - async move |session, cx| { - let adapter_name = session.read_with(cx, |this, _| this.adapter())?; - let (breakpoint_store, adapter_defaults) = - dap_store.read_with(cx, |dap_store, _| { - ( - dap_store.breakpoint_store().clone(), - dap_store.adapter_options(&adapter_name), - ) - })?; - initialized_rx.await?; - let errors_by_path = cx - .update(|cx| this.send_source_breakpoints(false, &breakpoint_store, cx))? - .await; - - dap_store.update(cx, |_, cx| { - let Some(worktree) = worktree.upgrade() else { - return; - }; - - for (path, error) in &errors_by_path { - log::error!("failed to set breakpoints for {path:?}: {error}"); - } - - if let Some(failed_path) = errors_by_path.keys().next() { - let failed_path = failed_path - .strip_prefix(worktree.read(cx).abs_path()) - .unwrap_or(failed_path) - .display(); - let message = format!( - "Failed to set breakpoints for {failed_path}{}", - match errors_by_path.len() { - 0 => unreachable!(), - 1 => "".into(), - 2 => " and 1 other path".into(), - n => format!(" and {} other paths", n - 1), - } - ); - cx.emit(super::dap_store::DapStoreEvent::Notification(message)); - } - })?; - - if should_send_exception_breakpoints { - _ = session.update(cx, |this, _| { - filters.retain(|filter| { - let is_enabled = if let Some(defaults) = adapter_defaults.as_ref() { - defaults - .exception_breakpoints - .get(&filter.filter) - .map(|options| options.enabled) - .unwrap_or_else(|| filter.default.unwrap_or_default()) - } else { - filter.default.unwrap_or_default() - }; - this.exception_breakpoints - .entry(filter.filter.clone()) - .or_insert_with(|| (filter.clone(), is_enabled)); - is_enabled - }); - }); - - this.send_exception_breakpoints(filters, supports_exception_filters) - .await - .ok(); - } - - if configuration_done_supported { - this.request(ConfigurationDone {}) - } else { - Task::ready(Ok(())) - } - .await - } - }); - - let task = cx.background_spawn(futures::future::try_join(launch, configuration_sequence)); - - cx.spawn(async move |this, cx| { - let result = task.await; - - this.update(cx, |this, cx| { - if let Some(this) = this.as_running_mut() { - this.is_started = true; - cx.notify(); - } - }) - .ok(); - - result?; - anyhow::Ok(()) - }) - } - - fn reconnect_for_ssh(&self, cx: &mut AsyncApp) -> Option>> { - let client = self.client.clone(); - let messages_tx = self.messages_tx.clone(); - let message_handler = Box::new(move |message| { - messages_tx.unbounded_send(message).ok(); - }); - if client.should_reconnect_for_ssh() { - Some(cx.spawn(async move |cx| { - client.connect(message_handler, cx).await?; - anyhow::Ok(()) - })) - } else { - None - } - } - - fn request(&self, request: R) -> Task> - where - ::Response: 'static, - ::Arguments: 'static + Send, - { - let request = Arc::new(request); - - let request_clone = request.clone(); - let connection = self.client.clone(); - self.executor.spawn(async move { - let args = request_clone.to_dap(); - let response = connection.request::(args).await?; - request.response_from_dap(response) - }) - } -} - -impl SessionState { - pub(super) fn request_dap(&self, request: R) -> Task> - where - ::Response: 'static, - ::Arguments: 'static + Send, - { - match self { - SessionState::Running(debug_adapter_client) => debug_adapter_client.request(request), - SessionState::Booting(_) => Task::ready(Err(anyhow!( - "no adapter running to send request: {request:?}" - ))), - } - } - - /// Did this debug session stop at least once? - pub(crate) fn has_ever_stopped(&self) -> bool { - match self { - SessionState::Booting(_) => false, - SessionState::Running(running_mode) => running_mode.has_ever_stopped, - } - } - - fn stopped(&mut self) { - if let SessionState::Running(running) = self { - running.has_ever_stopped = true; - } - } -} - -#[derive(Default)] -struct ThreadStates { - global_state: Option, - known_thread_states: IndexMap, -} - -impl ThreadStates { - fn stop_all_threads(&mut self) { - self.global_state = Some(ThreadStatus::Stopped); - self.known_thread_states.clear(); - } - - fn exit_all_threads(&mut self) { - self.global_state = Some(ThreadStatus::Exited); - self.known_thread_states.clear(); - } - - fn continue_all_threads(&mut self) { - self.global_state = Some(ThreadStatus::Running); - self.known_thread_states.clear(); - } - - fn stop_thread(&mut self, thread_id: ThreadId) { - self.known_thread_states - .insert(thread_id, ThreadStatus::Stopped); - } - - fn continue_thread(&mut self, thread_id: ThreadId) { - self.known_thread_states - .insert(thread_id, ThreadStatus::Running); - } - - fn process_step(&mut self, thread_id: ThreadId) { - self.known_thread_states - .insert(thread_id, ThreadStatus::Stepping); - } - - fn thread_status(&self, thread_id: ThreadId) -> ThreadStatus { - self.thread_state(thread_id) - .unwrap_or(ThreadStatus::Running) - } - - fn thread_state(&self, thread_id: ThreadId) -> Option { - self.known_thread_states - .get(&thread_id) - .copied() - .or(self.global_state) - } - - fn exit_thread(&mut self, thread_id: ThreadId) { - self.known_thread_states - .insert(thread_id, ThreadStatus::Exited); - } - - fn any_stopped_thread(&self) -> bool { - self.global_state - .is_some_and(|state| state == ThreadStatus::Stopped) - || self - .known_thread_states - .values() - .any(|status| *status == ThreadStatus::Stopped) - } -} - -// TODO(debugger): Wrap dap types with reference counting so the UI doesn't have to clone them on refresh -#[derive(Default)] -pub struct SessionSnapshot { - threads: IndexMap, - thread_states: ThreadStates, - variables: HashMap>, - stack_frames: IndexMap, - locations: HashMap, - modules: Vec, - loaded_sources: Vec, -} - -type IsEnabled = bool; - -#[derive(Copy, Clone, Default, Debug, PartialEq, PartialOrd, Eq, Ord)] -pub struct OutputToken(pub usize); -/// Represents a current state of a single debug adapter and provides ways to mutate it. -pub struct Session { - pub state: SessionState, - active_snapshot: SessionSnapshot, - snapshots: VecDeque, - selected_snapshot_index: Option, - id: SessionId, - label: Option, - adapter: DebugAdapterName, - pub(super) capabilities: Capabilities, - child_session_ids: HashSet, - parent_session: Option>, - output_token: OutputToken, - output: Box>, - watchers: HashMap, - is_session_terminated: bool, - requests: HashMap>>>>, - pub(crate) breakpoint_store: Entity, - ignore_breakpoints: bool, - exception_breakpoints: BTreeMap, - data_breakpoints: BTreeMap, - background_tasks: Vec>, - restart_task: Option>, - task_context: TaskContext, - memory: memory::Memory, - quirks: SessionQuirks, - remote_client: Option>, - node_runtime: Option, - http_client: Option>, - companion_port: Option, -} - -trait CacheableCommand: Any + Send + Sync { - fn dyn_eq(&self, rhs: &dyn CacheableCommand) -> bool; - fn dyn_hash(&self, hasher: &mut dyn Hasher); - fn as_any_arc(self: Arc) -> Arc; -} - -impl CacheableCommand for T -where - T: LocalDapCommand + PartialEq + Eq + Hash, -{ - fn dyn_eq(&self, rhs: &dyn CacheableCommand) -> bool { - (rhs as &dyn Any).downcast_ref::() == Some(self) - } - - fn dyn_hash(&self, mut hasher: &mut dyn Hasher) { - T::hash(self, &mut hasher); - } - - fn as_any_arc(self: Arc) -> Arc { - self - } -} - -pub(crate) struct RequestSlot(Arc); - -impl From for RequestSlot { - fn from(request: T) -> Self { - Self(Arc::new(request)) - } -} - -impl PartialEq for RequestSlot { - fn eq(&self, other: &Self) -> bool { - self.0.dyn_eq(other.0.as_ref()) - } -} - -impl Eq for RequestSlot {} - -impl Hash for RequestSlot { - fn hash(&self, state: &mut H) { - self.0.dyn_hash(state); - (&*self.0 as &dyn Any).type_id().hash(state) - } -} - -#[derive(Debug, Clone, Hash, PartialEq, Eq)] -pub struct CompletionsQuery { - pub query: String, - pub column: u64, - pub line: Option, - pub frame_id: Option, -} - -impl CompletionsQuery { - pub fn new( - buffer: &language::Buffer, - cursor_position: language::Anchor, - frame_id: Option, - ) -> Self { - let PointUtf16 { row, column } = cursor_position.to_point_utf16(&buffer.snapshot()); - Self { - query: buffer.text(), - column: column as u64, - frame_id, - line: Some(row as u64), - } - } -} - -#[derive(Debug)] -pub enum SessionEvent { - Modules, - LoadedSources, - Stopped(Option), - StackTrace, - Variables, - Watchers, - Threads, - InvalidateInlineValue, - CapabilitiesLoaded, - RunInTerminal { - request: RunInTerminalRequestArguments, - sender: mpsc::Sender>, - }, - DataBreakpointInfo, - ConsoleOutput, - HistoricSnapshotSelected, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum SessionStateEvent { - Running, - Shutdown, - Restart, - SpawnChildSession { - request: StartDebuggingRequestArguments, - }, -} - -impl EventEmitter for Session {} -impl EventEmitter for Session {} - -// local session will send breakpoint updates to DAP for all new breakpoints -// remote side will only send breakpoint updates when it is a breakpoint created by that peer -// BreakpointStore notifies session on breakpoint changes -impl Session { - pub(crate) fn new( - breakpoint_store: Entity, - session_id: SessionId, - parent_session: Option>, - label: Option, - adapter: DebugAdapterName, - task_context: TaskContext, - quirks: SessionQuirks, - remote_client: Option>, - node_runtime: Option, - http_client: Option>, - cx: &mut App, - ) -> Entity { - cx.new::(|cx| { - cx.subscribe(&breakpoint_store, |this, store, event, cx| match event { - BreakpointStoreEvent::BreakpointsUpdated(path, reason) => { - if let Some(local) = (!this.ignore_breakpoints) - .then(|| this.as_running_mut()) - .flatten() - { - local - .send_breakpoints_from_path(path.clone(), *reason, &store, cx) - .detach(); - }; - } - BreakpointStoreEvent::BreakpointsCleared(paths) => { - if let Some(local) = (!this.ignore_breakpoints) - .then(|| this.as_running_mut()) - .flatten() - { - local.unset_breakpoints_from_paths(paths, cx).detach(); - } - } - BreakpointStoreEvent::SetDebugLine | BreakpointStoreEvent::ClearDebugLines => {} - }) - .detach(); - - Self { - state: SessionState::Booting(None), - snapshots: VecDeque::with_capacity(DEBUG_HISTORY_LIMIT), - selected_snapshot_index: None, - active_snapshot: Default::default(), - id: session_id, - child_session_ids: HashSet::default(), - parent_session, - capabilities: Capabilities::default(), - watchers: HashMap::default(), - output_token: OutputToken(0), - output: circular_buffer::CircularBuffer::boxed(), - requests: HashMap::default(), - background_tasks: Vec::default(), - restart_task: None, - is_session_terminated: false, - ignore_breakpoints: false, - breakpoint_store, - data_breakpoints: Default::default(), - exception_breakpoints: Default::default(), - label, - adapter, - task_context, - memory: memory::Memory::new(), - quirks, - remote_client, - node_runtime, - http_client, - companion_port: None, - } - }) - } - - pub fn task_context(&self) -> &TaskContext { - &self.task_context - } - - pub fn worktree(&self) -> Option> { - match &self.state { - SessionState::Booting(_) => None, - SessionState::Running(local_mode) => local_mode.worktree.upgrade(), - } - } - - pub fn boot( - &mut self, - binary: DebugAdapterBinary, - worktree: Entity, - dap_store: WeakEntity, - cx: &mut Context, - ) -> Task> { - let (message_tx, mut message_rx) = futures::channel::mpsc::unbounded(); - let (initialized_tx, initialized_rx) = futures::channel::oneshot::channel(); - - let background_tasks = vec![cx.spawn(async move |this: WeakEntity, cx| { - let mut initialized_tx = Some(initialized_tx); - while let Some(message) = message_rx.next().await { - if let Message::Event(event) = message { - if let Events::Initialized(_) = *event { - if let Some(tx) = initialized_tx.take() { - tx.send(()).ok(); - } - } else { - let Ok(_) = this.update(cx, |session, cx| { - session.handle_dap_event(event, cx); - }) else { - break; - }; - } - } else if let Message::Request(request) = message { - let Ok(_) = this.update(cx, |this, cx| { - if request.command == StartDebugging::COMMAND { - this.handle_start_debugging_request(request, cx) - .detach_and_log_err(cx); - } else if request.command == RunInTerminal::COMMAND { - this.handle_run_in_terminal_request(request, cx) - .detach_and_log_err(cx); - } - }) else { - break; - }; - } - } - })]; - self.background_tasks = background_tasks; - let id = self.id; - let parent_session = self.parent_session.clone(); - - cx.spawn(async move |this, cx| { - let mode = RunningMode::new( - id, - parent_session, - worktree.downgrade(), - binary.clone(), - message_tx, - cx, - ) - .await?; - this.update(cx, |this, cx| { - match &mut this.state { - SessionState::Booting(task) if task.is_some() => { - task.take().unwrap().detach_and_log_err(cx); - } - SessionState::Booting(_) => {} - SessionState::Running(_) => { - debug_panic!("Attempting to boot a session that is already running"); - } - }; - this.state = SessionState::Running(mode); - cx.emit(SessionStateEvent::Running); - })?; - - this.update(cx, |session, cx| session.request_initialize(cx))? - .await?; - - let result = this - .update(cx, |session, cx| { - session.initialize_sequence(initialized_rx, dap_store.clone(), cx) - })? - .await; - - if result.is_err() { - let mut console = this.update(cx, |session, cx| session.console_output(cx))?; - - console - .send(format!( - "Tried to launch debugger with: {}", - serde_json::to_string_pretty(&binary.request_args.configuration) - .unwrap_or_default(), - )) - .await - .ok(); - } - - result - }) - } - - pub fn session_id(&self) -> SessionId { - self.id - } - - pub fn child_session_ids(&self) -> HashSet { - self.child_session_ids.clone() - } - - pub fn add_child_session_id(&mut self, session_id: SessionId) { - self.child_session_ids.insert(session_id); - } - - pub fn remove_child_session_id(&mut self, session_id: SessionId) { - self.child_session_ids.remove(&session_id); - } - - pub fn parent_id(&self, cx: &App) -> Option { - self.parent_session - .as_ref() - .map(|session| session.read(cx).id) - } - - pub fn parent_session(&self) -> Option<&Entity> { - self.parent_session.as_ref() - } - - pub fn on_app_quit(&mut self, cx: &mut Context) -> Task<()> { - let Some(client) = self.adapter_client() else { - return Task::ready(()); - }; - - let supports_terminate = self - .capabilities - .support_terminate_debuggee - .unwrap_or(false); - - cx.background_spawn(async move { - if supports_terminate { - client - .request::(dap::TerminateArguments { - restart: Some(false), - }) - .await - .ok(); - } else { - client - .request::(dap::DisconnectArguments { - restart: Some(false), - terminate_debuggee: Some(true), - suspend_debuggee: Some(false), - }) - .await - .ok(); - } - }) - } - - pub fn capabilities(&self) -> &Capabilities { - &self.capabilities - } - - pub fn binary(&self) -> Option<&DebugAdapterBinary> { - match &self.state { - SessionState::Booting(_) => None, - SessionState::Running(running_mode) => Some(&running_mode.binary), - } - } - - pub fn adapter(&self) -> DebugAdapterName { - self.adapter.clone() - } - - pub fn label(&self) -> Option { - self.label.clone() - } - - pub fn is_terminated(&self) -> bool { - self.is_session_terminated - } - - pub fn console_output(&mut self, cx: &mut Context) -> mpsc::UnboundedSender { - let (tx, mut rx) = mpsc::unbounded(); - - cx.spawn(async move |this, cx| { - while let Some(output) = rx.next().await { - this.update(cx, |this, _| { - let event = dap::OutputEvent { - category: None, - output, - group: None, - variables_reference: None, - source: None, - line: None, - column: None, - data: None, - location_reference: None, - }; - this.push_output(event); - })?; - } - anyhow::Ok(()) - }) - .detach(); - - tx - } - - pub fn is_started(&self) -> bool { - match &self.state { - SessionState::Booting(_) => false, - SessionState::Running(running) => running.is_started, - } - } - - pub fn is_building(&self) -> bool { - matches!(self.state, SessionState::Booting(_)) - } - - pub fn as_running_mut(&mut self) -> Option<&mut RunningMode> { - match &mut self.state { - SessionState::Running(local_mode) => Some(local_mode), - SessionState::Booting(_) => None, - } - } - - pub fn as_running(&self) -> Option<&RunningMode> { - match &self.state { - SessionState::Running(local_mode) => Some(local_mode), - SessionState::Booting(_) => None, - } - } - - fn handle_start_debugging_request( - &mut self, - request: dap::messages::Request, - cx: &mut Context, - ) -> Task> { - let request_seq = request.seq; - - let launch_request: Option> = request - .arguments - .as_ref() - .map(|value| serde_json::from_value(value.clone())); - - let mut success = true; - if let Some(Ok(request)) = launch_request { - cx.emit(SessionStateEvent::SpawnChildSession { request }); - } else { - log::error!( - "Failed to parse launch request arguments: {:?}", - request.arguments - ); - success = false; - } - - cx.spawn(async move |this, cx| { - this.update(cx, |this, cx| { - this.respond_to_client( - request_seq, - success, - StartDebugging::COMMAND.to_string(), - None, - cx, - ) - })? - .await - }) - } - - fn handle_run_in_terminal_request( - &mut self, - request: dap::messages::Request, - cx: &mut Context, - ) -> Task> { - let request_args = match serde_json::from_value::( - request.arguments.unwrap_or_default(), - ) { - Ok(args) => args, - Err(error) => { - return cx.spawn(async move |session, cx| { - let error = serde_json::to_value(dap::ErrorResponse { - error: Some(dap::Message { - id: request.seq, - format: error.to_string(), - variables: None, - send_telemetry: None, - show_user: None, - url: None, - url_label: None, - }), - }) - .ok(); - - session - .update(cx, |this, cx| { - this.respond_to_client( - request.seq, - false, - StartDebugging::COMMAND.to_string(), - error, - cx, - ) - })? - .await?; - - Err(anyhow!("Failed to parse RunInTerminalRequestArguments")) - }); - } - }; - - let seq = request.seq; - - let (tx, mut rx) = mpsc::channel::>(1); - cx.emit(SessionEvent::RunInTerminal { - request: request_args, - sender: tx, - }); - cx.notify(); - - cx.spawn(async move |session, cx| { - let result = util::maybe!(async move { - rx.next().await.ok_or_else(|| { - anyhow!("failed to receive response from spawn terminal".to_string()) - })? - }) - .await; - let (success, body) = match result { - Ok(pid) => ( - true, - serde_json::to_value(dap::RunInTerminalResponse { - process_id: None, - shell_process_id: Some(pid as u64), - }) - .ok(), - ), - Err(error) => ( - false, - serde_json::to_value(dap::ErrorResponse { - error: Some(dap::Message { - id: seq, - format: error.to_string(), - variables: None, - send_telemetry: None, - show_user: None, - url: None, - url_label: None, - }), - }) - .ok(), - ), - }; - - session - .update(cx, |session, cx| { - session.respond_to_client( - seq, - success, - RunInTerminal::COMMAND.to_string(), - body, - cx, - ) - })? - .await - }) - } - - pub(super) fn request_initialize(&mut self, cx: &mut Context) -> Task> { - let adapter_id = self.adapter().to_string(); - let request = Initialize { adapter_id }; - - let SessionState::Running(running) = &self.state else { - return Task::ready(Err(anyhow!( - "Cannot send initialize request, task still building" - ))); - }; - let mut response = running.request(request.clone()); - - cx.spawn(async move |this, cx| { - loop { - let capabilities = response.await; - match capabilities { - Err(e) => { - let Ok(Some(reconnect)) = this.update(cx, |this, cx| { - this.as_running() - .and_then(|running| running.reconnect_for_ssh(&mut cx.to_async())) - }) else { - return Err(e); - }; - log::info!("Failed to connect to debug adapter: {}, retrying...", e); - reconnect.await?; - - let Ok(Some(r)) = this.update(cx, |this, _| { - this.as_running() - .map(|running| running.request(request.clone())) - }) else { - return Err(e); - }; - response = r - } - Ok(capabilities) => { - this.update(cx, |session, cx| { - session.capabilities = capabilities; - - cx.emit(SessionEvent::CapabilitiesLoaded); - })?; - return Ok(()); - } - } - } - }) - } - - pub(super) fn initialize_sequence( - &mut self, - initialize_rx: oneshot::Receiver<()>, - dap_store: WeakEntity, - cx: &mut Context, - ) -> Task> { - match &self.state { - SessionState::Running(local_mode) => { - local_mode.initialize_sequence(&self.capabilities, initialize_rx, dap_store, cx) - } - SessionState::Booting(_) => { - Task::ready(Err(anyhow!("cannot initialize, still building"))) - } - } - } - - pub fn run_to_position( - &mut self, - breakpoint: SourceBreakpoint, - active_thread_id: ThreadId, - cx: &mut Context, - ) { - match &mut self.state { - SessionState::Running(local_mode) => { - if !matches!( - self.active_snapshot - .thread_states - .thread_state(active_thread_id), - Some(ThreadStatus::Stopped) - ) { - return; - }; - let path = breakpoint.path.clone(); - local_mode.tmp_breakpoint = Some(breakpoint); - let task = local_mode.send_breakpoints_from_path( - path, - BreakpointUpdatedReason::Toggled, - &self.breakpoint_store, - cx, - ); - - cx.spawn(async move |this, cx| { - task.await; - this.update(cx, |this, cx| { - this.continue_thread(active_thread_id, cx); - }) - }) - .detach(); - } - SessionState::Booting(_) => {} - } - } - - pub fn has_new_output(&self, last_update: OutputToken) -> bool { - self.output_token.0.checked_sub(last_update.0).unwrap_or(0) != 0 - } - - pub fn output( - &self, - since: OutputToken, - ) -> (impl Iterator, OutputToken) { - if self.output_token.0 == 0 { - return (self.output.range(0..0), OutputToken(0)); - }; - - let events_since = self.output_token.0.checked_sub(since.0).unwrap_or(0); - - let clamped_events_since = events_since.clamp(0, self.output.len()); - ( - self.output - .range(self.output.len() - clamped_events_since..), - self.output_token, - ) - } - - pub fn respond_to_client( - &self, - request_seq: u64, - success: bool, - command: String, - body: Option, - cx: &mut Context, - ) -> Task> { - let Some(local_session) = self.as_running() else { - unreachable!("Cannot respond to remote client"); - }; - let client = local_session.client.clone(); - - cx.background_spawn(async move { - client - .send_message(Message::Response(Response { - body, - success, - command, - seq: request_seq + 1, - request_seq, - message: None, - })) - .await - }) - } - - fn session_state(&self) -> &SessionSnapshot { - self.selected_snapshot_index - .and_then(|ix| self.snapshots.get(ix)) - .unwrap_or_else(|| &self.active_snapshot) - } - - fn push_to_history(&mut self) { - if !self.has_ever_stopped() { - return; - } - - while self.snapshots.len() >= DEBUG_HISTORY_LIMIT { - self.snapshots.pop_front(); - } - - self.snapshots - .push_back(std::mem::take(&mut self.active_snapshot)); - } - - pub fn historic_snapshots(&self) -> &VecDeque { - &self.snapshots - } - - pub fn select_historic_snapshot(&mut self, ix: Option, cx: &mut Context) { - if self.selected_snapshot_index == ix { - return; - } - - if self - .selected_snapshot_index - .is_some_and(|ix| self.snapshots.len() <= ix) - { - debug_panic!("Attempted to select a debug session with an out of bounds index"); - return; - } - - self.selected_snapshot_index = ix; - cx.emit(SessionEvent::HistoricSnapshotSelected); - cx.notify(); - } - - pub fn active_snapshot_index(&self) -> Option { - self.selected_snapshot_index - } - - fn handle_stopped_event(&mut self, event: StoppedEvent, cx: &mut Context) { - self.push_to_history(); - - self.state.stopped(); - // todo(debugger): Find a clean way to get around the clone - let breakpoint_store = self.breakpoint_store.clone(); - if let Some((local, path)) = self.as_running_mut().and_then(|local| { - let breakpoint = local.tmp_breakpoint.take()?; - let path = breakpoint.path; - Some((local, path)) - }) { - local - .send_breakpoints_from_path( - path, - BreakpointUpdatedReason::Toggled, - &breakpoint_store, - cx, - ) - .detach(); - }; - - if event.all_threads_stopped.unwrap_or_default() || event.thread_id.is_none() { - self.active_snapshot.thread_states.stop_all_threads(); - self.invalidate_command_type::(); - } - - // Event if we stopped all threads we still need to insert the thread_id - // to our own data - if let Some(thread_id) = event.thread_id { - self.active_snapshot - .thread_states - .stop_thread(ThreadId(thread_id)); - - self.invalidate_state( - &StackTraceCommand { - thread_id, - start_frame: None, - levels: None, - } - .into(), - ); - } - - self.invalidate_generic(); - self.active_snapshot.threads.clear(); - self.active_snapshot.variables.clear(); - cx.emit(SessionEvent::Stopped( - event - .thread_id - .map(Into::into) - .filter(|_| !event.preserve_focus_hint.unwrap_or(false)), - )); - cx.emit(SessionEvent::InvalidateInlineValue); - cx.notify(); - } - - pub(crate) fn handle_dap_event(&mut self, event: Box, cx: &mut Context) { - match *event { - Events::Initialized(_) => { - debug_assert!( - false, - "Initialized event should have been handled in LocalMode" - ); - } - Events::Stopped(event) => self.handle_stopped_event(event, cx), - Events::Continued(event) => { - if event.all_threads_continued.unwrap_or_default() { - self.active_snapshot.thread_states.continue_all_threads(); - self.breakpoint_store.update(cx, |store, cx| { - store.remove_active_position(Some(self.session_id()), cx) - }); - } else { - self.active_snapshot - .thread_states - .continue_thread(ThreadId(event.thread_id)); - } - // todo(debugger): We should be able to get away with only invalidating generic if all threads were continued - self.invalidate_generic(); - } - Events::Exited(_event) => { - self.clear_active_debug_line(cx); - } - Events::Terminated(_) => { - self.shutdown(cx).detach(); - } - Events::Thread(event) => { - let thread_id = ThreadId(event.thread_id); - - match event.reason { - dap::ThreadEventReason::Started => { - self.active_snapshot - .thread_states - .continue_thread(thread_id); - } - dap::ThreadEventReason::Exited => { - self.active_snapshot.thread_states.exit_thread(thread_id); - } - reason => { - log::error!("Unhandled thread event reason {:?}", reason); - } - } - self.invalidate_state(&ThreadsCommand.into()); - cx.notify(); - } - Events::Output(event) => { - if event - .category - .as_ref() - .is_some_and(|category| *category == OutputEventCategory::Telemetry) - { - return; - } - - self.push_output(event); - cx.notify(); - } - Events::Breakpoint(event) => self.breakpoint_store.update(cx, |store, _| { - store.update_session_breakpoint(self.session_id(), event.reason, event.breakpoint); - }), - Events::Module(event) => { - match event.reason { - dap::ModuleEventReason::New => { - self.active_snapshot.modules.push(event.module); - } - dap::ModuleEventReason::Changed => { - if let Some(module) = self - .active_snapshot - .modules - .iter_mut() - .find(|other| event.module.id == other.id) - { - *module = event.module; - } - } - dap::ModuleEventReason::Removed => { - self.active_snapshot - .modules - .retain(|other| event.module.id != other.id); - } - } - - // todo(debugger): We should only send the invalidate command to downstream clients. - // self.invalidate_state(&ModulesCommand.into()); - } - Events::LoadedSource(_) => { - self.invalidate_state(&LoadedSourcesCommand.into()); - } - Events::Capabilities(event) => { - self.capabilities = self.capabilities.merge(event.capabilities); - - // The adapter might've enabled new exception breakpoints (or disabled existing ones). - let recent_filters = self - .capabilities - .exception_breakpoint_filters - .iter() - .flatten() - .map(|filter| (filter.filter.clone(), filter.clone())) - .collect::>(); - for filter in recent_filters.values() { - let default = filter.default.unwrap_or_default(); - self.exception_breakpoints - .entry(filter.filter.clone()) - .or_insert_with(|| (filter.clone(), default)); - } - self.exception_breakpoints - .retain(|k, _| recent_filters.contains_key(k)); - if self.is_started() { - self.send_exception_breakpoints(cx); - } - - // Remove the ones that no longer exist. - cx.notify(); - } - Events::Memory(_) => {} - Events::Process(_) => {} - Events::ProgressEnd(_) => {} - Events::ProgressStart(_) => {} - Events::ProgressUpdate(_) => {} - Events::Invalidated(_) => {} - Events::Other(event) => { - if event.event == "launchBrowserInCompanion" { - let Some(request) = serde_json::from_value(event.body).ok() else { - log::error!("failed to deserialize launchBrowserInCompanion event"); - return; - }; - self.launch_browser_for_remote_server(request, cx); - } else if event.event == "killCompanionBrowser" { - let Some(request) = serde_json::from_value(event.body).ok() else { - log::error!("failed to deserialize killCompanionBrowser event"); - return; - }; - self.kill_browser(request, cx); - } - } - } - } - - /// Ensure that there's a request in flight for the given command, and if not, send it. Use this to run requests that are idempotent. - fn fetch( - &mut self, - request: T, - process_result: impl FnOnce(&mut Self, Result, &mut Context) + 'static, - cx: &mut Context, - ) { - const { - assert!( - T::CACHEABLE, - "Only requests marked as cacheable should invoke `fetch`" - ); - } - - if (!self.active_snapshot.thread_states.any_stopped_thread() - && request.type_id() != TypeId::of::()) - || self.selected_snapshot_index.is_some() - || self.is_session_terminated - { - return; - } - - let request_map = self - .requests - .entry(std::any::TypeId::of::()) - .or_default(); - - if let Entry::Vacant(vacant) = request_map.entry(request.into()) { - let command = vacant.key().0.clone().as_any_arc().downcast::().unwrap(); - - let task = Self::request_inner::>( - &self.capabilities, - &self.state, - command, - |this, result, cx| { - process_result(this, result, cx); - None - }, - cx, - ); - let task = cx - .background_executor() - .spawn(async move { - let _ = task.await?; - Some(()) - }) - .shared(); - - vacant.insert(task); - cx.notify(); - } - } - - fn request_inner( - capabilities: &Capabilities, - mode: &SessionState, - request: T, - process_result: impl FnOnce( - &mut Self, - Result, - &mut Context, - ) -> Option - + 'static, - cx: &mut Context, - ) -> Task> { - if !T::is_supported(capabilities) { - log::warn!( - "Attempted to send a DAP request that isn't supported: {:?}", - request - ); - let error = Err(anyhow::Error::msg( - "Couldn't complete request because it's not supported", - )); - return cx.spawn(async move |this, cx| { - this.update(cx, |this, cx| process_result(this, error, cx)) - .ok() - .flatten() - }); - } - - let request = mode.request_dap(request); - cx.spawn(async move |this, cx| { - let result = request.await; - this.update(cx, |this, cx| process_result(this, result, cx)) - .ok() - .flatten() - }) - } - - fn request( - &self, - request: T, - process_result: impl FnOnce( - &mut Self, - Result, - &mut Context, - ) -> Option - + 'static, - cx: &mut Context, - ) -> Task> { - Self::request_inner(&self.capabilities, &self.state, request, process_result, cx) - } - - fn invalidate_command_type(&mut self) { - self.requests.remove(&std::any::TypeId::of::()); - } - - fn invalidate_generic(&mut self) { - self.invalidate_command_type::(); - self.invalidate_command_type::(); - self.invalidate_command_type::(); - self.invalidate_command_type::(); - self.invalidate_command_type::(); - let executor = self.as_running().map(|running| running.executor.clone()); - if let Some(executor) = executor { - self.memory.clear(&executor); - } - } - - fn invalidate_state(&mut self, key: &RequestSlot) { - self.requests - .entry((&*key.0 as &dyn Any).type_id()) - .and_modify(|request_map| { - request_map.remove(key); - }); - } - - fn push_output(&mut self, event: OutputEvent) { - self.output.push_back(event); - self.output_token.0 += 1; - } - - pub fn any_stopped_thread(&self) -> bool { - self.active_snapshot.thread_states.any_stopped_thread() - } - - pub fn thread_status(&self, thread_id: ThreadId) -> ThreadStatus { - self.active_snapshot.thread_states.thread_status(thread_id) - } - - pub fn threads(&mut self, cx: &mut Context) -> Vec<(dap::Thread, ThreadStatus)> { - self.fetch( - dap_command::ThreadsCommand, - |this, result, cx| { - let Some(result) = result.log_err() else { - return; - }; - - this.active_snapshot.threads = result - .into_iter() - .map(|thread| (ThreadId(thread.id), Thread::from(thread))) - .collect(); - - this.invalidate_command_type::(); - cx.emit(SessionEvent::Threads); - cx.notify(); - }, - cx, - ); - - let state = self.session_state(); - state - .threads - .values() - .map(|thread| { - ( - thread.dap.clone(), - state.thread_states.thread_status(ThreadId(thread.dap.id)), - ) - }) - .collect() - } - - pub fn modules(&mut self, cx: &mut Context) -> &[Module] { - self.fetch( - dap_command::ModulesCommand, - |this, result, cx| { - let Some(result) = result.log_err() else { - return; - }; - - this.active_snapshot.modules = result; - cx.emit(SessionEvent::Modules); - cx.notify(); - }, - cx, - ); - - &self.session_state().modules - } - - // CodeLLDB returns the size of a pointed-to-memory, which we can use to make the experience of go-to-memory better. - pub fn data_access_size( - &mut self, - frame_id: Option, - evaluate_name: &str, - cx: &mut Context, - ) -> Task> { - let request = self.request( - EvaluateCommand { - expression: format!("?${{sizeof({evaluate_name})}}"), - frame_id, - - context: Some(EvaluateArgumentsContext::Repl), - source: None, - }, - |_, response, _| response.ok(), - cx, - ); - cx.background_spawn(async move { - let result = request.await?; - result.result.parse().ok() - }) - } - - pub fn memory_reference_of_expr( - &mut self, - frame_id: Option, - expression: String, - cx: &mut Context, - ) -> Task)>> { - let request = self.request( - EvaluateCommand { - expression, - frame_id, - - context: Some(EvaluateArgumentsContext::Repl), - source: None, - }, - |_, response, _| response.ok(), - cx, - ); - cx.background_spawn(async move { - let result = request.await?; - result - .memory_reference - .map(|reference| (reference, result.type_)) - }) - } - - pub fn write_memory(&mut self, address: u64, data: &[u8], cx: &mut Context) { - let data = base64::engine::general_purpose::STANDARD.encode(data); - self.request( - WriteMemoryArguments { - memory_reference: address.to_string(), - data, - allow_partial: None, - offset: None, - }, - |this, response, cx| { - this.memory.clear(cx.background_executor()); - this.invalidate_command_type::(); - this.invalidate_command_type::(); - cx.emit(SessionEvent::Variables); - response.ok() - }, - cx, - ) - .detach(); - } - pub fn read_memory( - &mut self, - range: RangeInclusive, - cx: &mut Context, - ) -> MemoryIterator { - // This function is a bit more involved when it comes to fetching data. - // Since we attempt to read memory in pages, we need to account for some parts - // of memory being unreadable. Therefore, we start off by fetching a page per request. - // In case that fails, we try to re-fetch smaller regions until we have the full range. - let page_range = Memory::memory_range_to_page_range(range.clone()); - for page_address in PageAddress::iter_range(page_range) { - self.read_single_page_memory(page_address, cx); - } - self.memory.memory_range(range) - } - - fn read_single_page_memory(&mut self, page_start: PageAddress, cx: &mut Context) { - _ = maybe!({ - let builder = self.memory.build_page(page_start)?; - - self.memory_read_fetch_page_recursive(builder, cx); - Some(()) - }); - } - fn memory_read_fetch_page_recursive( - &mut self, - mut builder: MemoryPageBuilder, - cx: &mut Context, - ) { - let Some(next_request) = builder.next_request() else { - // We're done fetching. Let's grab the page and insert it into our memory store. - let (address, contents) = builder.build(); - self.memory.insert_page(address, contents); - - return; - }; - let size = next_request.size; - self.fetch( - ReadMemory { - memory_reference: format!("0x{:X}", next_request.address), - offset: Some(0), - count: next_request.size, - }, - move |this, memory, cx| { - if let Ok(memory) = memory { - builder.known(memory.content); - if let Some(unknown) = memory.unreadable_bytes { - builder.unknown(unknown); - } - // This is the recursive bit: if we're not yet done with - // the whole page, we'll kick off a new request with smaller range. - // Note that this function is recursive only conceptually; - // since it kicks off a new request with callback, we don't need to worry about stack overflow. - this.memory_read_fetch_page_recursive(builder, cx); - } else { - builder.unknown(size); - } - }, - cx, - ); - } - - pub fn ignore_breakpoints(&self) -> bool { - self.ignore_breakpoints - } - - pub fn toggle_ignore_breakpoints( - &mut self, - cx: &mut App, - ) -> Task, anyhow::Error>> { - self.set_ignore_breakpoints(!self.ignore_breakpoints, cx) - } - - pub(crate) fn set_ignore_breakpoints( - &mut self, - ignore: bool, - cx: &mut App, - ) -> Task, anyhow::Error>> { - if self.ignore_breakpoints == ignore { - return Task::ready(HashMap::default()); - } - - self.ignore_breakpoints = ignore; - - if let Some(local) = self.as_running() { - local.send_source_breakpoints(ignore, &self.breakpoint_store, cx) - } else { - // todo(debugger): We need to propagate this change to downstream sessions and send a message to upstream sessions - unimplemented!() - } - } - - pub fn data_breakpoints(&self) -> impl Iterator { - self.data_breakpoints.values() - } - - pub fn exception_breakpoints( - &self, - ) -> impl Iterator { - self.exception_breakpoints.values() - } - - pub fn toggle_exception_breakpoint(&mut self, id: &str, cx: &App) { - if let Some((_, is_enabled)) = self.exception_breakpoints.get_mut(id) { - *is_enabled = !*is_enabled; - self.send_exception_breakpoints(cx); - } - } - - fn send_exception_breakpoints(&mut self, cx: &App) { - if let Some(local) = self.as_running() { - let exception_filters = self - .exception_breakpoints - .values() - .filter_map(|(filter, is_enabled)| is_enabled.then(|| filter.clone())) - .collect(); - - let supports_exception_filters = self - .capabilities - .supports_exception_filter_options - .unwrap_or_default(); - local - .send_exception_breakpoints(exception_filters, supports_exception_filters) - .detach_and_log_err(cx); - } else { - debug_assert!(false, "Not implemented"); - } - } - - pub fn toggle_data_breakpoint(&mut self, id: &str, cx: &mut Context<'_, Session>) { - if let Some(state) = self.data_breakpoints.get_mut(id) { - state.is_enabled = !state.is_enabled; - self.send_exception_breakpoints(cx); - } - } - - fn send_data_breakpoints(&mut self, cx: &mut Context) { - if let Some(mode) = self.as_running() { - let breakpoints = self - .data_breakpoints - .values() - .filter_map(|state| state.is_enabled.then(|| state.dap.clone())) - .collect(); - let command = SetDataBreakpointsCommand { breakpoints }; - mode.request(command).detach_and_log_err(cx); - } - } - - pub fn create_data_breakpoint( - &mut self, - context: Arc, - data_id: String, - dap: dap::DataBreakpoint, - cx: &mut Context, - ) { - if self.data_breakpoints.remove(&data_id).is_none() { - self.data_breakpoints.insert( - data_id, - DataBreakpointState { - dap, - is_enabled: true, - context, - }, - ); - } - self.send_data_breakpoints(cx); - } - - pub fn breakpoints_enabled(&self) -> bool { - self.ignore_breakpoints - } - - pub fn loaded_sources(&mut self, cx: &mut Context) -> &[Source] { - self.fetch( - dap_command::LoadedSourcesCommand, - |this, result, cx| { - let Some(result) = result.log_err() else { - return; - }; - this.active_snapshot.loaded_sources = result; - cx.emit(SessionEvent::LoadedSources); - cx.notify(); - }, - cx, - ); - &self.session_state().loaded_sources - } - - fn fallback_to_manual_restart( - &mut self, - res: Result<()>, - cx: &mut Context, - ) -> Option<()> { - if res.log_err().is_none() { - cx.emit(SessionStateEvent::Restart); - return None; - } - Some(()) - } - - fn empty_response(&mut self, res: Result<()>, _cx: &mut Context) -> Option<()> { - res.log_err()?; - Some(()) - } - - fn on_step_response( - thread_id: ThreadId, - ) -> impl FnOnce(&mut Self, Result, &mut Context) -> Option + 'static - { - move |this, response, cx| match response.log_err() { - Some(response) => { - this.breakpoint_store.update(cx, |store, cx| { - store.remove_active_position(Some(this.session_id()), cx) - }); - Some(response) - } - None => { - this.active_snapshot.thread_states.stop_thread(thread_id); - cx.notify(); - None - } - } - } - - fn clear_active_debug_line_response( - &mut self, - response: Result<()>, - cx: &mut Context, - ) -> Option<()> { - response.log_err()?; - self.clear_active_debug_line(cx); - Some(()) - } - - fn clear_active_debug_line(&mut self, cx: &mut Context) { - self.breakpoint_store.update(cx, |store, cx| { - store.remove_active_position(Some(self.id), cx) - }); - } - - pub fn pause_thread(&mut self, thread_id: ThreadId, cx: &mut Context) { - self.request( - PauseCommand { - thread_id: thread_id.0, - }, - Self::empty_response, - cx, - ) - .detach(); - } - - pub fn restart_stack_frame(&mut self, stack_frame_id: u64, cx: &mut Context) { - self.request( - RestartStackFrameCommand { stack_frame_id }, - Self::empty_response, - cx, - ) - .detach(); - } - - pub fn restart(&mut self, args: Option, cx: &mut Context) { - if self.restart_task.is_some() || self.as_running().is_none() { - return; - } - - let supports_dap_restart = - self.capabilities.supports_restart_request.unwrap_or(false) && !self.is_terminated(); - - self.restart_task = Some(cx.spawn(async move |this, cx| { - let _ = this.update(cx, |session, cx| { - if supports_dap_restart { - session - .request( - RestartCommand { - raw: args.unwrap_or(Value::Null), - }, - Self::fallback_to_manual_restart, - cx, - ) - .detach(); - } else { - cx.emit(SessionStateEvent::Restart); - } - }); - })); - } - - pub fn shutdown(&mut self, cx: &mut Context) -> Task<()> { - if self.is_session_terminated { - return Task::ready(()); - } - - self.is_session_terminated = true; - self.active_snapshot.thread_states.exit_all_threads(); - cx.notify(); - - let task = match &mut self.state { - SessionState::Running(_) => { - if self - .capabilities - .supports_terminate_request - .unwrap_or_default() - { - self.request( - TerminateCommand { - restart: Some(false), - }, - Self::clear_active_debug_line_response, - cx, - ) - } else { - self.request( - DisconnectCommand { - restart: Some(false), - terminate_debuggee: Some(true), - suspend_debuggee: Some(false), - }, - Self::clear_active_debug_line_response, - cx, - ) - } - } - SessionState::Booting(build_task) => { - build_task.take(); - Task::ready(Some(())) - } - }; - - cx.emit(SessionStateEvent::Shutdown); - - cx.spawn(async move |this, cx| { - task.await; - let _ = this.update(cx, |this, _| { - if let Some(adapter_client) = this.adapter_client() { - adapter_client.kill(); - } - }); - }) - } - - pub fn completions( - &mut self, - query: CompletionsQuery, - cx: &mut Context, - ) -> Task>> { - let task = self.request(query, |_, result, _| result.log_err(), cx); - - cx.background_executor().spawn(async move { - anyhow::Ok( - task.await - .map(|response| response.targets) - .context("failed to fetch completions")?, - ) - }) - } - - pub fn continue_thread(&mut self, thread_id: ThreadId, cx: &mut Context) { - self.select_historic_snapshot(None, cx); - - let supports_single_thread_execution_requests = - self.capabilities.supports_single_thread_execution_requests; - self.active_snapshot - .thread_states - .continue_thread(thread_id); - self.request( - ContinueCommand { - args: ContinueArguments { - thread_id: thread_id.0, - single_thread: supports_single_thread_execution_requests, - }, - }, - Self::on_step_response::(thread_id), - cx, - ) - .detach(); - } - - pub fn adapter_client(&self) -> Option> { - match self.state { - SessionState::Running(ref local) => Some(local.client.clone()), - SessionState::Booting(_) => None, - } - } - - pub fn has_ever_stopped(&self) -> bool { - self.state.has_ever_stopped() - } - - pub fn step_over( - &mut self, - thread_id: ThreadId, - granularity: SteppingGranularity, - cx: &mut Context, - ) { - self.select_historic_snapshot(None, cx); - - let supports_single_thread_execution_requests = - self.capabilities.supports_single_thread_execution_requests; - let supports_stepping_granularity = self - .capabilities - .supports_stepping_granularity - .unwrap_or_default(); - - let command = NextCommand { - inner: StepCommand { - thread_id: thread_id.0, - granularity: supports_stepping_granularity.then(|| granularity), - single_thread: supports_single_thread_execution_requests, - }, - }; - - self.active_snapshot.thread_states.process_step(thread_id); - self.request( - command, - Self::on_step_response::(thread_id), - cx, - ) - .detach(); - } - - pub fn step_in( - &mut self, - thread_id: ThreadId, - granularity: SteppingGranularity, - cx: &mut Context, - ) { - self.select_historic_snapshot(None, cx); - - let supports_single_thread_execution_requests = - self.capabilities.supports_single_thread_execution_requests; - let supports_stepping_granularity = self - .capabilities - .supports_stepping_granularity - .unwrap_or_default(); - - let command = StepInCommand { - inner: StepCommand { - thread_id: thread_id.0, - granularity: supports_stepping_granularity.then(|| granularity), - single_thread: supports_single_thread_execution_requests, - }, - }; - - self.active_snapshot.thread_states.process_step(thread_id); - self.request( - command, - Self::on_step_response::(thread_id), - cx, - ) - .detach(); - } - - pub fn step_out( - &mut self, - thread_id: ThreadId, - granularity: SteppingGranularity, - cx: &mut Context, - ) { - self.select_historic_snapshot(None, cx); - - let supports_single_thread_execution_requests = - self.capabilities.supports_single_thread_execution_requests; - let supports_stepping_granularity = self - .capabilities - .supports_stepping_granularity - .unwrap_or_default(); - - let command = StepOutCommand { - inner: StepCommand { - thread_id: thread_id.0, - granularity: supports_stepping_granularity.then(|| granularity), - single_thread: supports_single_thread_execution_requests, - }, - }; - - self.active_snapshot.thread_states.process_step(thread_id); - self.request( - command, - Self::on_step_response::(thread_id), - cx, - ) - .detach(); - } - - pub fn step_back( - &mut self, - thread_id: ThreadId, - granularity: SteppingGranularity, - cx: &mut Context, - ) { - self.select_historic_snapshot(None, cx); - - let supports_single_thread_execution_requests = - self.capabilities.supports_single_thread_execution_requests; - let supports_stepping_granularity = self - .capabilities - .supports_stepping_granularity - .unwrap_or_default(); - - let command = StepBackCommand { - inner: StepCommand { - thread_id: thread_id.0, - granularity: supports_stepping_granularity.then(|| granularity), - single_thread: supports_single_thread_execution_requests, - }, - }; - - self.active_snapshot.thread_states.process_step(thread_id); - - self.request( - command, - Self::on_step_response::(thread_id), - cx, - ) - .detach(); - } - - pub fn stack_frames( - &mut self, - thread_id: ThreadId, - cx: &mut Context, - ) -> Result> { - if self.active_snapshot.thread_states.thread_status(thread_id) == ThreadStatus::Stopped - && self.requests.contains_key(&ThreadsCommand.type_id()) - && self.active_snapshot.threads.contains_key(&thread_id) - // ^ todo(debugger): We need a better way to check that we're not querying stale data - // We could still be using an old thread id and have sent a new thread's request - // This isn't the biggest concern right now because it hasn't caused any issues outside of tests - // But it very well could cause a minor bug in the future that is hard to track down - { - self.fetch( - super::dap_command::StackTraceCommand { - thread_id: thread_id.0, - start_frame: None, - levels: None, - }, - move |this, stack_frames, cx| { - let entry = - this.active_snapshot - .threads - .entry(thread_id) - .and_modify(|thread| match &stack_frames { - Ok(stack_frames) => { - thread.stack_frames = stack_frames - .iter() - .cloned() - .map(StackFrame::from) - .collect(); - thread.stack_frames_error = None; - } - Err(error) => { - thread.stack_frames.clear(); - thread.stack_frames_error = Some(error.to_string().into()); - } - }); - debug_assert!( - matches!(entry, indexmap::map::Entry::Occupied(_)), - "Sent request for thread_id that doesn't exist" - ); - if let Ok(stack_frames) = stack_frames { - this.active_snapshot.stack_frames.extend( - stack_frames - .into_iter() - .filter(|frame| { - // Workaround for JavaScript debug adapter sending out "fake" stack frames for delineating await points. This is fine, - // except that they always use an id of 0 for it, which collides with other (valid) stack frames. - !(frame.id == 0 - && frame.line == 0 - && frame.column == 0 - && frame.presentation_hint - == Some(StackFramePresentationHint::Label)) - }) - .map(|frame| (frame.id, StackFrame::from(frame))), - ); - } - - this.invalidate_command_type::(); - this.invalidate_command_type::(); - - cx.emit(SessionEvent::StackTrace); - }, - cx, - ); - } - - match self.session_state().threads.get(&thread_id) { - Some(thread) => { - if let Some(error) = &thread.stack_frames_error { - Err(anyhow!(error.to_string())) - } else { - Ok(thread.stack_frames.clone()) - } - } - None => Ok(Vec::new()), - } - } - - pub fn scopes(&mut self, stack_frame_id: u64, cx: &mut Context) -> &[dap::Scope] { - if self.requests.contains_key(&TypeId::of::()) - && self - .requests - .contains_key(&TypeId::of::()) - { - self.fetch( - ScopesCommand { stack_frame_id }, - move |this, scopes, cx| { - let Some(scopes) = scopes.log_err() else { - return - }; - - for scope in scopes.iter() { - this.variables(scope.variables_reference, cx); - } - - let entry = this - .active_snapshot - .stack_frames - .entry(stack_frame_id) - .and_modify(|stack_frame| { - stack_frame.scopes = scopes; - }); - - cx.emit(SessionEvent::Variables); - - debug_assert!( - matches!(entry, indexmap::map::Entry::Occupied(_)), - "Sent scopes request for stack_frame_id that doesn't exist or hasn't been fetched" - ); - }, - cx, - ); - } - - self.session_state() - .stack_frames - .get(&stack_frame_id) - .map(|frame| frame.scopes.as_slice()) - .unwrap_or_default() - } - - pub fn variables_by_stack_frame_id( - &self, - stack_frame_id: StackFrameId, - globals: bool, - locals: bool, - ) -> Vec { - let state = self.session_state(); - let Some(stack_frame) = state.stack_frames.get(&stack_frame_id) else { - return Vec::new(); - }; - - stack_frame - .scopes - .iter() - .filter(|scope| { - (scope.name.to_lowercase().contains("local") && locals) - || (scope.name.to_lowercase().contains("global") && globals) - }) - .filter_map(|scope| state.variables.get(&scope.variables_reference)) - .flatten() - .cloned() - .collect() - } - - pub fn watchers(&self) -> &HashMap { - &self.watchers - } - - pub fn add_watcher( - &mut self, - expression: SharedString, - frame_id: u64, - cx: &mut Context, - ) -> Task> { - let request = self.state.request_dap(EvaluateCommand { - expression: expression.to_string(), - context: Some(EvaluateArgumentsContext::Watch), - frame_id: Some(frame_id), - source: None, - }); - - cx.spawn(async move |this, cx| { - let response = request.await?; - - this.update(cx, |session, cx| { - session.watchers.insert( - expression.clone(), - Watcher { - expression, - value: response.result.into(), - variables_reference: response.variables_reference, - presentation_hint: response.presentation_hint, - }, - ); - cx.emit(SessionEvent::Watchers); - }) - }) - } - - pub fn refresh_watchers(&mut self, frame_id: u64, cx: &mut Context) { - let watches = self.watchers.clone(); - for (_, watch) in watches.into_iter() { - self.add_watcher(watch.expression.clone(), frame_id, cx) - .detach(); - } - } - - pub fn remove_watcher(&mut self, expression: SharedString) { - self.watchers.remove(&expression); - } - - pub fn variables( - &mut self, - variables_reference: VariableReference, - cx: &mut Context, - ) -> Vec { - let command = VariablesCommand { - variables_reference, - filter: None, - start: None, - count: None, - format: None, - }; - - self.fetch( - command, - move |this, variables, cx| { - let Some(variables) = variables.log_err() else { - return; - }; - - this.active_snapshot - .variables - .insert(variables_reference, variables); - - cx.emit(SessionEvent::Variables); - cx.emit(SessionEvent::InvalidateInlineValue); - }, - cx, - ); - - self.session_state() - .variables - .get(&variables_reference) - .cloned() - .unwrap_or_default() - } - - pub fn data_breakpoint_info( - &mut self, - context: Arc, - mode: Option, - cx: &mut Context, - ) -> Task> { - let command = DataBreakpointInfoCommand { context, mode }; - - self.request(command, |_, response, _| response.ok(), cx) - } - - pub fn set_variable_value( - &mut self, - stack_frame_id: u64, - variables_reference: u64, - name: String, - value: String, - cx: &mut Context, - ) { - if self.capabilities.supports_set_variable.unwrap_or_default() { - self.request( - SetVariableValueCommand { - name, - value, - variables_reference, - }, - move |this, response, cx| { - let response = response.log_err()?; - this.invalidate_command_type::(); - this.invalidate_command_type::(); - this.memory.clear(cx.background_executor()); - this.refresh_watchers(stack_frame_id, cx); - cx.emit(SessionEvent::Variables); - Some(response) - }, - cx, - ) - .detach(); - } - } - - pub fn evaluate( - &mut self, - expression: String, - context: Option, - frame_id: Option, - source: Option, - cx: &mut Context, - ) -> Task<()> { - let event = dap::OutputEvent { - category: None, - output: format!("> {expression}"), - group: None, - variables_reference: None, - source: None, - line: None, - column: None, - data: None, - location_reference: None, - }; - self.push_output(event); - let request = self.state.request_dap(EvaluateCommand { - expression, - context, - frame_id, - source, - }); - cx.spawn(async move |this, cx| { - let response = request.await; - this.update(cx, |this, cx| { - this.memory.clear(cx.background_executor()); - this.invalidate_command_type::(); - this.invalidate_command_type::(); - cx.emit(SessionEvent::Variables); - match response { - Ok(response) => { - let event = dap::OutputEvent { - category: None, - output: format!("< {}", &response.result), - group: None, - variables_reference: Some(response.variables_reference), - source: None, - line: None, - column: None, - data: None, - location_reference: None, - }; - this.push_output(event); - } - Err(e) => { - let event = dap::OutputEvent { - category: None, - output: format!("{}", e), - group: None, - variables_reference: None, - source: None, - line: None, - column: None, - data: None, - location_reference: None, - }; - this.push_output(event); - } - }; - cx.notify(); - }) - .ok(); - }) - } - - pub fn location( - &mut self, - reference: u64, - cx: &mut Context, - ) -> Option { - self.fetch( - LocationsCommand { reference }, - move |this, response, _| { - let Some(response) = response.log_err() else { - return; - }; - this.active_snapshot.locations.insert(reference, response); - }, - cx, - ); - self.session_state().locations.get(&reference).cloned() - } - - pub fn is_attached(&self) -> bool { - let SessionState::Running(local_mode) = &self.state else { - return false; - }; - local_mode.binary.request_args.request == StartDebuggingRequestArgumentsRequest::Attach - } - - pub fn disconnect_client(&mut self, cx: &mut Context) { - let command = DisconnectCommand { - restart: Some(false), - terminate_debuggee: Some(false), - suspend_debuggee: Some(false), - }; - - self.request(command, Self::empty_response, cx).detach() - } - - pub fn terminate_threads(&mut self, thread_ids: Option>, cx: &mut Context) { - if self - .capabilities - .supports_terminate_threads_request - .unwrap_or_default() - { - self.request( - TerminateThreadsCommand { - thread_ids: thread_ids.map(|ids| ids.into_iter().map(|id| id.0).collect()), - }, - Self::clear_active_debug_line_response, - cx, - ) - .detach(); - } else { - self.shutdown(cx).detach(); - } - } - - pub fn thread_state(&self, thread_id: ThreadId) -> Option { - self.session_state().thread_states.thread_state(thread_id) - } - - pub fn quirks(&self) -> SessionQuirks { - self.quirks - } - - fn launch_browser_for_remote_server( - &mut self, - mut request: LaunchBrowserInCompanionParams, - cx: &mut Context, - ) { - let Some(remote_client) = self.remote_client.clone() else { - log::error!("can't launch browser in companion for non-remote project"); - return; - }; - let Some(http_client) = self.http_client.clone() else { - return; - }; - let Some(node_runtime) = self.node_runtime.clone() else { - return; - }; - - let mut console_output = self.console_output(cx); - let task = cx.spawn(async move |this, cx| { - let forward_ports_process = if remote_client - .read_with(cx, |client, _| client.shares_network_interface())? - { - request.other.insert( - "proxyUri".into(), - format!("127.0.0.1:{}", request.server_port).into(), - ); - None - } else { - let port = TcpTransport::unused_port(Ipv4Addr::LOCALHOST) - .await - .context("getting port for DAP")?; - request - .other - .insert("proxyUri".into(), format!("127.0.0.1:{port}").into()); - let mut port_forwards = vec![(port, "localhost".to_owned(), request.server_port)]; - - if let Some(value) = request.params.get("url") - && let Some(url) = value.as_str() - && let Some(url) = Url::parse(url).ok() - && let Some(frontend_port) = url.port() - { - port_forwards.push((frontend_port, "localhost".to_owned(), frontend_port)); - } - - let child = remote_client.update(cx, |client, _| { - let command = client.build_forward_ports_command(port_forwards)?; - let child = new_smol_command(command.program) - .args(command.args) - .envs(command.env) - .spawn() - .context("spawning port forwarding process")?; - anyhow::Ok(child) - })??; - Some(child) - }; - - let mut companion_process = None; - let companion_port = - if let Some(companion_port) = this.read_with(cx, |this, _| this.companion_port)? { - companion_port - } else { - let task = cx.spawn(async move |cx| spawn_companion(node_runtime, cx).await); - match task.await { - Ok((port, child)) => { - companion_process = Some(child); - port - } - Err(e) => { - console_output - .send(format!("Failed to launch browser companion process: {e}")) - .await - .ok(); - return Err(e); - } - } - }; - - let mut background_tasks = Vec::new(); - if let Some(mut forward_ports_process) = forward_ports_process { - background_tasks.push(cx.spawn(async move |_| { - forward_ports_process.status().await.log_err(); - })); - }; - if let Some(mut companion_process) = companion_process { - if let Some(stderr) = companion_process.stderr.take() { - let mut console_output = console_output.clone(); - background_tasks.push(cx.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 - { - console_output - .send(format!("companion stderr: {line}")) - .await - .ok(); - line.clear(); - } - })); - } - background_tasks.push(cx.spawn({ - let mut console_output = console_output.clone(); - async move |_| match companion_process.status().await { - Ok(status) => { - if status.success() { - console_output - .send("Companion process exited normally".into()) - .await - .ok(); - } else { - console_output - .send(format!( - "Companion process exited abnormally with {status:?}" - )) - .await - .ok(); - } - } - Err(e) => { - console_output - .send(format!("Failed to join companion process: {e}")) - .await - .ok(); - } - } - })); - } - - // TODO pass wslInfo as needed - - let companion_address = format!("127.0.0.1:{companion_port}"); - let mut companion_started = false; - for _ in 0..10 { - if TcpStream::connect(&companion_address).await.is_ok() { - companion_started = true; - break; - } - cx.background_executor() - .timer(Duration::from_millis(100)) - .await; - } - if !companion_started { - console_output - .send("Browser companion failed to start".into()) - .await - .ok(); - bail!("Browser companion failed to start"); - } - - let response = http_client - .post_json( - &format!("http://{companion_address}/launch-and-attach"), - serde_json::to_string(&request) - .context("serializing request")? - .into(), - ) - .await; - match response { - Ok(response) => { - if !response.status().is_success() { - console_output - .send("Launch request to companion failed".into()) - .await - .ok(); - return Err(anyhow!("launch request failed")); - } - } - Err(e) => { - console_output - .send("Failed to read response from companion".into()) - .await - .ok(); - return Err(e); - } - } - - this.update(cx, |this, _| { - this.background_tasks.extend(background_tasks); - this.companion_port = Some(companion_port); - })?; - - anyhow::Ok(()) - }); - self.background_tasks.push(cx.spawn(async move |_, _| { - task.await.log_err(); - })); - } - - fn kill_browser(&self, request: KillCompanionBrowserParams, cx: &mut App) { - let Some(companion_port) = self.companion_port else { - log::error!("received killCompanionBrowser but js-debug-companion is not running"); - return; - }; - let Some(http_client) = self.http_client.clone() else { - return; - }; - - cx.spawn(async move |_| { - http_client - .post_json( - &format!("http://127.0.0.1:{companion_port}/kill"), - serde_json::to_string(&request) - .context("serializing request")? - .into(), - ) - .await?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx) - } -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct LaunchBrowserInCompanionParams { - server_port: u16, - params: HashMap, - #[serde(flatten)] - other: HashMap, -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct KillCompanionBrowserParams { - launch_id: u64, -} - -async fn spawn_companion( - node_runtime: NodeRuntime, - cx: &mut AsyncApp, -) -> Result<(u16, smol::process::Child)> { - let binary_path = node_runtime - .binary_path() - .await - .context("getting node path")?; - let path = cx - .spawn(async move |cx| get_or_install_companion(node_runtime, cx).await) - .await?; - log::info!("will launch js-debug-companion version {path:?}"); - - let port = { - let listener = TcpListener::bind("127.0.0.1:0") - .await - .context("getting port for companion")?; - listener.local_addr()?.port() - }; - - let dir = paths::data_dir() - .join("js_debug_companion_state") - .to_string_lossy() - .to_string(); - - let child = new_smol_command(binary_path) - .arg(path) - .args([ - format!("--listen=127.0.0.1:{port}"), - format!("--state={dir}"), - ]) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .spawn() - .context("spawning companion child process")?; - - Ok((port, child)) -} - -async fn get_or_install_companion(node: NodeRuntime, cx: &mut AsyncApp) -> Result { - const PACKAGE_NAME: &str = "@zed-industries/js-debug-companion-cli"; - - async fn install_latest_version(dir: PathBuf, node: NodeRuntime) -> Result { - let temp_dir = tempfile::tempdir().context("creating temporary directory")?; - node.npm_install_packages(temp_dir.path(), &[(PACKAGE_NAME, "latest")]) - .await - .context("installing latest companion package")?; - let version = node - .npm_package_installed_version(temp_dir.path(), PACKAGE_NAME) - .await - .context("getting installed companion version")? - .context("companion was not installed")?; - smol::fs::rename(temp_dir.path(), dir.join(&version)) - .await - .context("moving companion package into place")?; - Ok(dir.join(version)) - } - - let dir = paths::debug_adapters_dir().join("js-debug-companion"); - let (latest_installed_version, latest_version) = cx - .background_spawn({ - let dir = dir.clone(); - let node = node.clone(); - async move { - smol::fs::create_dir_all(&dir) - .await - .context("creating companion installation directory")?; - - let mut children = smol::fs::read_dir(&dir) - .await - .context("reading companion installation directory")? - .try_collect::>() - .await - .context("reading companion installation directory entries")?; - children - .sort_by_key(|child| semver::Version::parse(child.file_name().to_str()?).ok()); - - let latest_installed_version = children.last().and_then(|child| { - let version = child.file_name().into_string().ok()?; - Some((child.path(), version)) - }); - let latest_version = node - .npm_package_latest_version(PACKAGE_NAME) - .await - .log_err(); - anyhow::Ok((latest_installed_version, latest_version)) - } - }) - .await?; - - let path = if let Some((installed_path, installed_version)) = latest_installed_version { - if let Some(latest_version) = latest_version - && latest_version != installed_version - { - cx.background_spawn(install_latest_version(dir.clone(), node.clone())) - .detach(); - } - Ok(installed_path) - } else { - cx.background_spawn(install_latest_version(dir.clone(), node.clone())) - .await - }; - - Ok(path? - .join("node_modules") - .join(PACKAGE_NAME) - .join("out") - .join("cli.js")) -} diff --git a/crates/project/src/debugger/test.rs b/crates/project/src/debugger/test.rs deleted file mode 100644 index 53b88323e6..0000000000 --- a/crates/project/src/debugger/test.rs +++ /dev/null @@ -1,55 +0,0 @@ -use std::{path::Path, sync::Arc}; - -use dap::client::DebugAdapterClient; -use gpui::{App, Subscription}; - -use super::session::{Session, SessionStateEvent}; - -pub fn intercept_debug_sessions) + 'static>( - cx: &mut gpui::TestAppContext, - configure: T, -) -> Subscription { - cx.update(|cx| { - let configure = Arc::new(configure); - cx.observe_new::(move |_, _, cx| { - let configure = configure.clone(); - cx.subscribe_self(move |session, event, cx| { - let configure = configure.clone(); - if matches!(event, SessionStateEvent::Running) { - let client = session.adapter_client().unwrap(); - register_default_handlers(session, &client, cx); - configure(&client); - } - }) - .detach(); - }) - }) -} - -fn register_default_handlers(session: &Session, client: &Arc, cx: &mut App) { - client.on_request::(move |_, _| Ok(Default::default())); - let paths = session.breakpoint_store.read(cx).breakpoint_paths(); - - client.on_request::(move |_, args| { - let p = Arc::from(Path::new(&args.source.path.unwrap())); - if !paths.contains(&p) { - panic!("Sent breakpoints for path without any") - } - - Ok(dap::SetBreakpointsResponse { - breakpoints: Vec::default(), - }) - }); - - client.on_request::(move |_, _| Ok(())); - - client.on_request::(move |_, _| { - Ok(dap::SetExceptionBreakpointsResponse { breakpoints: None }) - }); - - client.on_request::(move |_, _| Ok(())); - - client.on_request::(move |_, _| { - Ok(dap::ThreadsResponse { threads: vec![] }) - }); -} diff --git a/crates/project/src/environment.rs b/crates/project/src/environment.rs deleted file mode 100644 index c4e807621e..0000000000 --- a/crates/project/src/environment.rs +++ /dev/null @@ -1,421 +0,0 @@ -use anyhow::{Context as _, bail}; -use futures::{FutureExt, StreamExt as _, channel::mpsc, future::Shared}; -use language::Buffer; -use remote::RemoteClient; -use rpc::proto::{self, REMOTE_SERVER_PROJECT_ID}; -use std::{collections::VecDeque, path::Path, sync::Arc}; -use task::{Shell, shell_to_proto}; -use terminal::terminal_settings::TerminalSettings; -use util::{ResultExt, command::new_smol_command, rel_path::RelPath}; -use worktree::Worktree; - -use collections::HashMap; -use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Task, WeakEntity}; -use settings::Settings as _; - -use crate::{ - project_settings::{DirenvSettings, ProjectSettings}, - worktree_store::WorktreeStore, -}; - -pub struct ProjectEnvironment { - cli_environment: Option>, - local_environments: HashMap<(Shell, Arc), Shared>>>>, - remote_environments: HashMap<(Shell, Arc), Shared>>>>, - environment_error_messages: VecDeque, - environment_error_messages_tx: mpsc::UnboundedSender, - worktree_store: WeakEntity, - remote_client: Option>, - is_remote_project: bool, - _tasks: Vec>, -} - -pub enum ProjectEnvironmentEvent { - ErrorsUpdated, -} - -impl EventEmitter for ProjectEnvironment {} - -impl ProjectEnvironment { - pub fn new( - cli_environment: Option>, - worktree_store: WeakEntity, - remote_client: Option>, - is_remote_project: bool, - cx: &mut Context, - ) -> Self { - let (tx, mut rx) = mpsc::unbounded(); - let task = cx.spawn(async move |this, cx| { - while let Some(message) = rx.next().await { - this.update(cx, |this, cx| { - this.environment_error_messages.push_back(message); - cx.emit(ProjectEnvironmentEvent::ErrorsUpdated); - }) - .ok(); - } - }); - Self { - cli_environment, - local_environments: Default::default(), - remote_environments: Default::default(), - environment_error_messages: Default::default(), - environment_error_messages_tx: tx, - worktree_store, - remote_client, - is_remote_project, - _tasks: vec![task], - } - } - - /// Returns the inherited CLI environment, if this project was opened from the Zed CLI. - pub(crate) fn get_cli_environment(&self) -> Option> { - if cfg!(any(test, feature = "test-support")) { - return Some(HashMap::default()); - } - if let Some(mut env) = self.cli_environment.clone() { - set_origin_marker(&mut env, EnvironmentOrigin::Cli); - Some(env) - } else { - None - } - } - - pub fn buffer_environment( - &mut self, - buffer: &Entity, - worktree_store: &Entity, - cx: &mut Context, - ) -> Shared>>> { - if let Some(cli_environment) = self.get_cli_environment() { - log::debug!("using project environment variables from CLI"); - return Task::ready(Some(cli_environment)).shared(); - } - - let Some(worktree) = buffer - .read(cx) - .file() - .map(|f| f.worktree_id(cx)) - .and_then(|worktree_id| worktree_store.read(cx).worktree_for_id(worktree_id, cx)) - else { - return Task::ready(None).shared(); - }; - self.worktree_environment(worktree, cx) - } - - pub fn worktree_environment( - &mut self, - worktree: Entity, - cx: &mut App, - ) -> Shared>>> { - if let Some(cli_environment) = self.get_cli_environment() { - log::debug!("using project environment variables from CLI"); - return Task::ready(Some(cli_environment)).shared(); - } - - let worktree = worktree.read(cx); - let mut abs_path = worktree.abs_path(); - if worktree.is_single_file() { - let Some(parent) = abs_path.parent() else { - return Task::ready(None).shared(); - }; - abs_path = parent.into(); - } - - let remote_client = self.remote_client.as_ref().and_then(|it| it.upgrade()); - match remote_client { - Some(remote_client) => remote_client.clone().read(cx).shell().map(|shell| { - self.remote_directory_environment( - &Shell::Program(shell), - abs_path, - remote_client, - cx, - ) - }), - None if self.is_remote_project => { - Some(self.local_directory_environment(&Shell::System, abs_path, cx)) - } - None => Some({ - let shell = TerminalSettings::get( - Some(settings::SettingsLocation { - worktree_id: worktree.id(), - path: RelPath::empty(), - }), - cx, - ) - .shell - .clone(); - - self.local_directory_environment(&shell, abs_path, cx) - }), - } - .unwrap_or_else(|| Task::ready(None).shared()) - } - - pub fn directory_environment( - &mut self, - abs_path: Arc, - cx: &mut App, - ) -> Shared>>> { - let remote_client = self.remote_client.as_ref().and_then(|it| it.upgrade()); - match remote_client { - Some(remote_client) => remote_client.clone().read(cx).shell().map(|shell| { - self.remote_directory_environment( - &Shell::Program(shell), - abs_path, - remote_client, - cx, - ) - }), - None if self.is_remote_project => { - Some(self.local_directory_environment(&Shell::System, abs_path, cx)) - } - None => self - .worktree_store - .read_with(cx, |worktree_store, cx| { - worktree_store.find_worktree(&abs_path, cx) - }) - .ok() - .map(|worktree| { - let shell = terminal::terminal_settings::TerminalSettings::get( - worktree - .as_ref() - .map(|(worktree, path)| settings::SettingsLocation { - worktree_id: worktree.read(cx).id(), - path: &path, - }), - cx, - ) - .shell - .clone(); - - self.local_directory_environment(&shell, abs_path, cx) - }), - } - .unwrap_or_else(|| Task::ready(None).shared()) - } - - /// Returns the project environment, if possible. - /// If the project was opened from the CLI, then the inherited CLI environment is returned. - /// If it wasn't opened from the CLI, and an absolute path is given, then a shell is spawned in - /// that directory, to get environment variables as if the user has `cd`'d there. - pub fn local_directory_environment( - &mut self, - shell: &Shell, - abs_path: Arc, - cx: &mut App, - ) -> Shared>>> { - if let Some(cli_environment) = self.get_cli_environment() { - log::debug!("using project environment variables from CLI"); - return Task::ready(Some(cli_environment)).shared(); - } - - self.local_environments - .entry((shell.clone(), abs_path.clone())) - .or_insert_with(|| { - let load_direnv = ProjectSettings::get_global(cx).load_direnv.clone(); - let shell = shell.clone(); - let tx = self.environment_error_messages_tx.clone(); - cx.spawn(async move |cx| { - let mut shell_env = cx - .background_spawn(load_directory_shell_environment( - shell, - abs_path.clone(), - load_direnv, - tx, - )) - .await - .log_err(); - - if let Some(shell_env) = shell_env.as_mut() { - let path = shell_env - .get("PATH") - .map(|path| path.as_str()) - .unwrap_or_default(); - log::debug!( - "using project environment variables shell launched in {:?}. PATH={:?}", - abs_path, - path - ); - - set_origin_marker(shell_env, EnvironmentOrigin::WorktreeShell); - } - - shell_env - }) - .shared() - }) - .clone() - } - - pub fn remote_directory_environment( - &mut self, - shell: &Shell, - abs_path: Arc, - remote_client: Entity, - cx: &mut App, - ) -> Shared>>> { - if cfg!(any(test, feature = "test-support")) { - return Task::ready(Some(HashMap::default())).shared(); - } - - self.remote_environments - .entry((shell.clone(), abs_path.clone())) - .or_insert_with(|| { - let response = - remote_client - .read(cx) - .proto_client() - .request(proto::GetDirectoryEnvironment { - project_id: REMOTE_SERVER_PROJECT_ID, - shell: Some(shell_to_proto(shell.clone())), - directory: abs_path.to_string_lossy().to_string(), - }); - cx.background_spawn(async move { - let environment = response.await.log_err()?; - Some(environment.environment.into_iter().collect()) - }) - .shared() - }) - .clone() - } - - pub fn peek_environment_error(&self) -> Option<&String> { - self.environment_error_messages.front() - } - - pub fn pop_environment_error(&mut self) -> Option { - self.environment_error_messages.pop_front() - } -} - -fn set_origin_marker(env: &mut HashMap, origin: EnvironmentOrigin) { - env.insert(ZED_ENVIRONMENT_ORIGIN_MARKER.to_string(), origin.into()); -} - -const ZED_ENVIRONMENT_ORIGIN_MARKER: &str = "ZED_ENVIRONMENT"; - -enum EnvironmentOrigin { - Cli, - WorktreeShell, -} - -impl From for String { - fn from(val: EnvironmentOrigin) -> Self { - match val { - EnvironmentOrigin::Cli => "cli".into(), - EnvironmentOrigin::WorktreeShell => "worktree-shell".into(), - } - } -} - -async fn load_directory_shell_environment( - shell: Shell, - abs_path: Arc, - load_direnv: DirenvSettings, - tx: mpsc::UnboundedSender, -) -> anyhow::Result> { - if let DirenvSettings::Disabled = load_direnv { - return Ok(HashMap::default()); - } - - let meta = smol::fs::metadata(&abs_path).await.with_context(|| { - tx.unbounded_send(format!("Failed to open {}", abs_path.display())) - .ok(); - format!("stat {abs_path:?}") - })?; - - let dir = if meta.is_dir() { - abs_path.clone() - } else { - abs_path - .parent() - .with_context(|| { - tx.unbounded_send(format!("Failed to open {}", abs_path.display())) - .ok(); - format!("getting parent of {abs_path:?}") - })? - .into() - }; - - let (shell, args) = shell.program_and_args(); - let mut envs = util::shell_env::capture(shell.clone(), args, abs_path) - .await - .with_context(|| { - tx.unbounded_send("Failed to load environment variables".into()) - .ok(); - format!("capturing shell environment with {shell:?}") - })?; - - if cfg!(target_os = "windows") - && let Some(path) = envs.remove("Path") - { - // windows env vars are case-insensitive, so normalize the path var - // so we can just assume `PATH` in other places - envs.insert("PATH".into(), path); - } - // If the user selects `Direct` for direnv, it would set an environment - // variable that later uses to know that it should not run the hook. - // We would include in `.envs` call so it is okay to run the hook - // even if direnv direct mode is enabled. - let direnv_environment = match load_direnv { - DirenvSettings::ShellHook => None, - DirenvSettings::Disabled => bail!("direnv integration is disabled"), - // Note: direnv is not available on Windows, so we skip direnv processing - // and just return the shell environment - DirenvSettings::Direct if cfg!(target_os = "windows") => None, - DirenvSettings::Direct => load_direnv_environment(&envs, &dir) - .await - .with_context(|| { - tx.unbounded_send("Failed to load direnv environment".into()) - .ok(); - "load direnv environment" - }) - .log_err(), - }; - if let Some(direnv_environment) = direnv_environment { - for (key, value) in direnv_environment { - if let Some(value) = value { - envs.insert(key, value); - } else { - envs.remove(&key); - } - } - } - - Ok(envs) -} - -async fn load_direnv_environment( - env: &HashMap, - dir: &Path, -) -> anyhow::Result>> { - let Some(direnv_path) = which::which("direnv").ok() else { - return Ok(HashMap::default()); - }; - - let args = &["export", "json"]; - let direnv_output = new_smol_command(&direnv_path) - .args(args) - .envs(env) - .env("TERM", "dumb") - .current_dir(dir) - .output() - .await - .context("running direnv")?; - - if !direnv_output.status.success() { - bail!( - "Loading direnv environment failed ({}), stderr: {}", - direnv_output.status, - String::from_utf8_lossy(&direnv_output.stderr) - ); - } - - let output = String::from_utf8_lossy(&direnv_output.stdout); - if output.is_empty() { - // direnv outputs nothing when it has no changes to apply to environment variables - return Ok(HashMap::default()); - } - - serde_json::from_str(&output).context("parsing direnv json") -} diff --git a/crates/project/src/git_store.rs b/crates/project/src/git_store.rs deleted file mode 100644 index c73ab914b7..0000000000 --- a/crates/project/src/git_store.rs +++ /dev/null @@ -1,6297 +0,0 @@ -pub mod branch_diff; -mod conflict_set; -pub mod git_traversal; -pub mod pending_op; - -use crate::{ - ProjectEnvironment, ProjectItem, ProjectPath, - buffer_store::{BufferStore, BufferStoreEvent}, - worktree_store::{WorktreeStore, WorktreeStoreEvent}, -}; -use anyhow::{Context as _, Result, anyhow, bail}; -use askpass::{AskPassDelegate, EncryptedPassword, IKnowWhatIAmDoingAndIHaveReadTheDocs}; -use buffer_diff::{BufferDiff, BufferDiffEvent}; -use client::ProjectId; -use collections::HashMap; -pub use conflict_set::{ConflictRegion, ConflictSet, ConflictSetSnapshot, ConflictSetUpdate}; -use fs::Fs; -use futures::{ - FutureExt, StreamExt, - channel::{ - mpsc, - oneshot::{self, Canceled}, - }, - future::{self, Shared}, - stream::FuturesOrdered, -}; -use git::{ - BuildPermalinkParams, GitHostingProviderRegistry, Oid, RunHook, - blame::Blame, - parse_git_remote_url, - repository::{ - Branch, CommitDetails, CommitDiff, CommitFile, CommitOptions, DiffType, FetchOptions, - GitRepository, GitRepositoryCheckpoint, PushOptions, Remote, RemoteCommandOutput, RepoPath, - ResetMode, UpstreamTrackingStatus, Worktree as GitWorktree, - }, - stash::{GitStash, StashEntry}, - status::{ - DiffTreeType, FileStatus, GitSummary, StatusCode, TrackedStatus, TreeDiff, TreeDiffStatus, - UnmergedStatus, UnmergedStatusCode, - }, -}; -use gpui::{ - App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task, - WeakEntity, -}; -use language::{ - Buffer, BufferEvent, Language, LanguageRegistry, - proto::{deserialize_version, serialize_version}, -}; -use parking_lot::Mutex; -use pending_op::{PendingOp, PendingOpId, PendingOps, PendingOpsSummary}; -use postage::stream::Stream as _; -use rpc::{ - AnyProtoClient, TypedEnvelope, - proto::{self, git_reset, split_repository_update}, -}; -use serde::Deserialize; -use settings::WorktreeId; -use smol::future::yield_now; -use std::{ - cmp::Ordering, - collections::{BTreeSet, HashSet, VecDeque}, - future::Future, - mem, - ops::Range, - path::{Path, PathBuf}, - str::FromStr, - sync::{ - Arc, - atomic::{self, AtomicU64}, - }, - time::Instant, -}; -use sum_tree::{Edit, SumTree, TreeSet}; -use task::Shell; -use text::{Bias, BufferId}; -use util::{ - ResultExt, debug_panic, - paths::{PathStyle, SanitizedPath}, - post_inc, - rel_path::RelPath, -}; -use worktree::{ - File, PathChange, PathKey, PathProgress, PathSummary, PathTarget, ProjectEntryId, - UpdatedGitRepositoriesSet, UpdatedGitRepository, Worktree, -}; -use zeroize::Zeroize; - -pub struct GitStore { - state: GitStoreState, - buffer_store: Entity, - worktree_store: Entity, - repositories: HashMap>, - worktree_ids: HashMap>, - active_repo_id: Option, - #[allow(clippy::type_complexity)] - loading_diffs: - HashMap<(BufferId, DiffKind), Shared, Arc>>>>, - diffs: HashMap>, - shared_diffs: HashMap>, - _subscriptions: Vec, -} - -#[derive(Default)] -struct SharedDiffs { - unstaged: Option>, - uncommitted: Option>, -} - -struct BufferGitState { - unstaged_diff: Option>, - uncommitted_diff: Option>, - conflict_set: Option>, - recalculate_diff_task: Option>>, - reparse_conflict_markers_task: Option>>, - language: Option>, - language_registry: Option>, - conflict_updated_futures: Vec>, - recalculating_tx: postage::watch::Sender, - - /// These operation counts are used to ensure that head and index text - /// values read from the git repository are up-to-date with any hunk staging - /// operations that have been performed on the BufferDiff. - /// - /// The operation count is incremented immediately when the user initiates a - /// hunk stage/unstage operation. Then, upon finishing writing the new index - /// text do disk, the `operation count as of write` is updated to reflect - /// the operation count that prompted the write. - hunk_staging_operation_count: usize, - hunk_staging_operation_count_as_of_write: usize, - - head_text: Option>, - index_text: Option>, - head_changed: bool, - index_changed: bool, - language_changed: bool, -} - -#[derive(Clone, Debug)] -enum DiffBasesChange { - SetIndex(Option), - SetHead(Option), - SetEach { - index: Option, - head: Option, - }, - SetBoth(Option), -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -enum DiffKind { - Unstaged, - Uncommitted, -} - -enum GitStoreState { - Local { - next_repository_id: Arc, - downstream: Option, - project_environment: Entity, - fs: Arc, - }, - Remote { - upstream_client: AnyProtoClient, - upstream_project_id: u64, - downstream: Option<(AnyProtoClient, ProjectId)>, - }, -} - -enum DownstreamUpdate { - UpdateRepository(RepositorySnapshot), - RemoveRepository(RepositoryId), -} - -struct LocalDownstreamState { - client: AnyProtoClient, - project_id: ProjectId, - updates_tx: mpsc::UnboundedSender, - _task: Task>, -} - -#[derive(Clone, Debug)] -pub struct GitStoreCheckpoint { - checkpoints_by_work_dir_abs_path: HashMap, GitRepositoryCheckpoint>, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct StatusEntry { - pub repo_path: RepoPath, - pub status: FileStatus, -} - -impl StatusEntry { - fn to_proto(&self) -> proto::StatusEntry { - let simple_status = match self.status { - FileStatus::Ignored | FileStatus::Untracked => proto::GitStatus::Added as i32, - FileStatus::Unmerged { .. } => proto::GitStatus::Conflict as i32, - FileStatus::Tracked(TrackedStatus { - index_status, - worktree_status, - }) => tracked_status_to_proto(if worktree_status != StatusCode::Unmodified { - worktree_status - } else { - index_status - }), - }; - - proto::StatusEntry { - repo_path: self.repo_path.to_proto(), - simple_status, - status: Some(status_to_proto(self.status)), - } - } -} - -impl TryFrom for StatusEntry { - type Error = anyhow::Error; - - fn try_from(value: proto::StatusEntry) -> Result { - let repo_path = RepoPath::from_proto(&value.repo_path).context("invalid repo path")?; - let status = status_from_proto(value.simple_status, value.status)?; - Ok(Self { repo_path, status }) - } -} - -impl sum_tree::Item for StatusEntry { - type Summary = PathSummary; - - fn summary(&self, _: ::Context<'_>) -> Self::Summary { - PathSummary { - max_path: self.repo_path.as_ref().clone(), - item_summary: self.status.summary(), - } - } -} - -impl sum_tree::KeyedItem for StatusEntry { - type Key = PathKey; - - fn key(&self) -> Self::Key { - PathKey(self.repo_path.as_ref().clone()) - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct RepositoryId(pub u64); - -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct MergeDetails { - pub conflicted_paths: TreeSet, - pub message: Option, - pub heads: Vec>, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct RepositorySnapshot { - pub id: RepositoryId, - pub statuses_by_path: SumTree, - pub work_directory_abs_path: Arc, - pub path_style: PathStyle, - pub branch: Option, - pub head_commit: Option, - pub scan_id: u64, - pub merge: MergeDetails, - pub remote_origin_url: Option, - pub remote_upstream_url: Option, - pub stash_entries: GitStash, -} - -type JobId = u64; - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct JobInfo { - pub start: Instant, - pub message: SharedString, -} - -pub struct Repository { - this: WeakEntity, - snapshot: RepositorySnapshot, - commit_message_buffer: Option>, - git_store: WeakEntity, - // For a local repository, holds paths that have had worktree events since the last status scan completed, - // and that should be examined during the next status scan. - paths_needing_status_update: BTreeSet, - job_sender: mpsc::UnboundedSender, - active_jobs: HashMap, - pending_ops: SumTree, - job_id: JobId, - askpass_delegates: Arc>>, - latest_askpass_id: u64, - repository_state: Shared>>, -} - -impl std::ops::Deref for Repository { - type Target = RepositorySnapshot; - - fn deref(&self) -> &Self::Target { - &self.snapshot - } -} - -#[derive(Clone)] -pub struct LocalRepositoryState { - pub fs: Arc, - pub backend: Arc, - pub environment: Arc>, -} - -impl LocalRepositoryState { - async fn new( - work_directory_abs_path: Arc, - dot_git_abs_path: Arc, - project_environment: WeakEntity, - fs: Arc, - cx: &mut AsyncApp, - ) -> anyhow::Result { - let environment = project_environment - .update(cx, |project_environment, cx| { - project_environment.local_directory_environment(&Shell::System, work_directory_abs_path.clone(), cx) - })? - .await - .unwrap_or_else(|| { - log::error!("failed to get working directory environment for repository {work_directory_abs_path:?}"); - HashMap::default() - }); - let search_paths = environment.get("PATH").map(|val| val.to_owned()); - let backend = cx - .background_spawn({ - let fs = fs.clone(); - async move { - let system_git_binary_path = search_paths - .and_then(|search_paths| { - which::which_in("git", Some(search_paths), &work_directory_abs_path) - .ok() - }) - .or_else(|| which::which("git").ok()); - fs.open_repo(&dot_git_abs_path, system_git_binary_path.as_deref()) - .with_context(|| format!("opening repository at {dot_git_abs_path:?}")) - } - }) - .await?; - Ok(LocalRepositoryState { - backend, - environment: Arc::new(environment), - fs, - }) - } -} - -#[derive(Clone)] -pub struct RemoteRepositoryState { - pub project_id: ProjectId, - pub client: AnyProtoClient, -} - -#[derive(Clone)] -pub enum RepositoryState { - Local(LocalRepositoryState), - Remote(RemoteRepositoryState), -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum RepositoryEvent { - StatusesChanged, - MergeHeadsChanged, - BranchChanged, - StashEntriesChanged, - PendingOpsChanged { pending_ops: SumTree }, -} - -#[derive(Clone, Debug)] -pub struct JobsUpdated; - -#[derive(Debug)] -pub enum GitStoreEvent { - ActiveRepositoryChanged(Option), - RepositoryUpdated(RepositoryId, RepositoryEvent, bool), - RepositoryAdded, - RepositoryRemoved(RepositoryId), - IndexWriteError(anyhow::Error), - JobsUpdated, - ConflictsUpdated, -} - -impl EventEmitter for Repository {} -impl EventEmitter for Repository {} -impl EventEmitter for GitStore {} - -pub struct GitJob { - job: Box Task<()>>, - key: Option, -} - -#[derive(PartialEq, Eq)] -enum GitJobKey { - WriteIndex(Vec), - ReloadBufferDiffBases, - RefreshStatuses, - ReloadGitState, -} - -impl GitStore { - pub fn local( - worktree_store: &Entity, - buffer_store: Entity, - environment: Entity, - fs: Arc, - cx: &mut Context, - ) -> Self { - Self::new( - worktree_store.clone(), - buffer_store, - GitStoreState::Local { - next_repository_id: Arc::new(AtomicU64::new(1)), - downstream: None, - project_environment: environment, - fs, - }, - cx, - ) - } - - pub fn remote( - worktree_store: &Entity, - buffer_store: Entity, - upstream_client: AnyProtoClient, - project_id: u64, - cx: &mut Context, - ) -> Self { - Self::new( - worktree_store.clone(), - buffer_store, - GitStoreState::Remote { - upstream_client, - upstream_project_id: project_id, - downstream: None, - }, - cx, - ) - } - - fn new( - worktree_store: Entity, - buffer_store: Entity, - state: GitStoreState, - cx: &mut Context, - ) -> Self { - let _subscriptions = vec![ - cx.subscribe(&worktree_store, Self::on_worktree_store_event), - cx.subscribe(&buffer_store, Self::on_buffer_store_event), - ]; - - GitStore { - state, - buffer_store, - worktree_store, - repositories: HashMap::default(), - worktree_ids: HashMap::default(), - active_repo_id: None, - _subscriptions, - loading_diffs: HashMap::default(), - shared_diffs: HashMap::default(), - diffs: HashMap::default(), - } - } - - pub fn init(client: &AnyProtoClient) { - client.add_entity_request_handler(Self::handle_get_remotes); - client.add_entity_request_handler(Self::handle_get_branches); - client.add_entity_request_handler(Self::handle_get_default_branch); - client.add_entity_request_handler(Self::handle_change_branch); - client.add_entity_request_handler(Self::handle_create_branch); - client.add_entity_request_handler(Self::handle_rename_branch); - client.add_entity_request_handler(Self::handle_create_remote); - client.add_entity_request_handler(Self::handle_remove_remote); - client.add_entity_request_handler(Self::handle_delete_branch); - client.add_entity_request_handler(Self::handle_git_init); - client.add_entity_request_handler(Self::handle_push); - client.add_entity_request_handler(Self::handle_pull); - client.add_entity_request_handler(Self::handle_fetch); - client.add_entity_request_handler(Self::handle_stage); - client.add_entity_request_handler(Self::handle_unstage); - client.add_entity_request_handler(Self::handle_stash); - client.add_entity_request_handler(Self::handle_stash_pop); - client.add_entity_request_handler(Self::handle_stash_apply); - client.add_entity_request_handler(Self::handle_stash_drop); - client.add_entity_request_handler(Self::handle_commit); - client.add_entity_request_handler(Self::handle_run_hook); - client.add_entity_request_handler(Self::handle_reset); - client.add_entity_request_handler(Self::handle_show); - client.add_entity_request_handler(Self::handle_load_commit_diff); - client.add_entity_request_handler(Self::handle_file_history); - client.add_entity_request_handler(Self::handle_checkout_files); - client.add_entity_request_handler(Self::handle_open_commit_message_buffer); - client.add_entity_request_handler(Self::handle_set_index_text); - client.add_entity_request_handler(Self::handle_askpass); - client.add_entity_request_handler(Self::handle_check_for_pushed_commits); - client.add_entity_request_handler(Self::handle_git_diff); - client.add_entity_request_handler(Self::handle_tree_diff); - client.add_entity_request_handler(Self::handle_get_blob_content); - client.add_entity_request_handler(Self::handle_open_unstaged_diff); - client.add_entity_request_handler(Self::handle_open_uncommitted_diff); - client.add_entity_message_handler(Self::handle_update_diff_bases); - client.add_entity_request_handler(Self::handle_get_permalink_to_line); - client.add_entity_request_handler(Self::handle_blame_buffer); - client.add_entity_message_handler(Self::handle_update_repository); - client.add_entity_message_handler(Self::handle_remove_repository); - client.add_entity_request_handler(Self::handle_git_clone); - client.add_entity_request_handler(Self::handle_get_worktrees); - client.add_entity_request_handler(Self::handle_create_worktree); - } - - pub fn is_local(&self) -> bool { - matches!(self.state, GitStoreState::Local { .. }) - } - pub fn set_active_repo_for_path(&mut self, project_path: &ProjectPath, cx: &mut Context) { - if let Some((repo, _)) = self.repository_and_path_for_project_path(project_path, cx) { - let id = repo.read(cx).id; - if self.active_repo_id != Some(id) { - self.active_repo_id = Some(id); - cx.emit(GitStoreEvent::ActiveRepositoryChanged(Some(id))); - } - } - } - - pub fn shared(&mut self, project_id: u64, client: AnyProtoClient, cx: &mut Context) { - match &mut self.state { - GitStoreState::Remote { - downstream: downstream_client, - .. - } => { - for repo in self.repositories.values() { - let update = repo.read(cx).snapshot.initial_update(project_id); - for update in split_repository_update(update) { - client.send(update).log_err(); - } - } - *downstream_client = Some((client, ProjectId(project_id))); - } - GitStoreState::Local { - downstream: downstream_client, - .. - } => { - let mut snapshots = HashMap::default(); - let (updates_tx, mut updates_rx) = mpsc::unbounded(); - for repo in self.repositories.values() { - updates_tx - .unbounded_send(DownstreamUpdate::UpdateRepository( - repo.read(cx).snapshot.clone(), - )) - .ok(); - } - *downstream_client = Some(LocalDownstreamState { - client: client.clone(), - project_id: ProjectId(project_id), - updates_tx, - _task: cx.spawn(async move |this, cx| { - cx.background_spawn(async move { - while let Some(update) = updates_rx.next().await { - match update { - DownstreamUpdate::UpdateRepository(snapshot) => { - if let Some(old_snapshot) = snapshots.get_mut(&snapshot.id) - { - let update = - snapshot.build_update(old_snapshot, project_id); - *old_snapshot = snapshot; - for update in split_repository_update(update) { - client.send(update)?; - } - } else { - let update = snapshot.initial_update(project_id); - for update in split_repository_update(update) { - client.send(update)?; - } - snapshots.insert(snapshot.id, snapshot); - } - } - DownstreamUpdate::RemoveRepository(id) => { - client.send(proto::RemoveRepository { - project_id, - id: id.to_proto(), - })?; - } - } - } - anyhow::Ok(()) - }) - .await - .ok(); - this.update(cx, |this, _| { - if let GitStoreState::Local { - downstream: downstream_client, - .. - } = &mut this.state - { - downstream_client.take(); - } else { - unreachable!("unshared called on remote store"); - } - }) - }), - }); - } - } - } - - pub fn unshared(&mut self, _cx: &mut Context) { - match &mut self.state { - GitStoreState::Local { - downstream: downstream_client, - .. - } => { - downstream_client.take(); - } - GitStoreState::Remote { - downstream: downstream_client, - .. - } => { - downstream_client.take(); - } - } - self.shared_diffs.clear(); - } - - pub(crate) fn forget_shared_diffs_for(&mut self, peer_id: &proto::PeerId) { - self.shared_diffs.remove(peer_id); - } - - pub fn active_repository(&self) -> Option> { - self.active_repo_id - .as_ref() - .map(|id| self.repositories[id].clone()) - } - - pub fn open_unstaged_diff( - &mut self, - buffer: Entity, - cx: &mut Context, - ) -> Task>> { - let buffer_id = buffer.read(cx).remote_id(); - if let Some(diff_state) = self.diffs.get(&buffer_id) - && let Some(unstaged_diff) = diff_state - .read(cx) - .unstaged_diff - .as_ref() - .and_then(|weak| weak.upgrade()) - { - if let Some(task) = - diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation()) - { - return cx.background_executor().spawn(async move { - task.await; - Ok(unstaged_diff) - }); - } - return Task::ready(Ok(unstaged_diff)); - } - - let Some((repo, repo_path)) = - self.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx) - else { - return Task::ready(Err(anyhow!("failed to find git repository for buffer"))); - }; - - let task = self - .loading_diffs - .entry((buffer_id, DiffKind::Unstaged)) - .or_insert_with(|| { - let staged_text = repo.update(cx, |repo, cx| { - repo.load_staged_text(buffer_id, repo_path, cx) - }); - cx.spawn(async move |this, cx| { - Self::open_diff_internal( - this, - DiffKind::Unstaged, - staged_text.await.map(DiffBasesChange::SetIndex), - buffer, - cx, - ) - .await - .map_err(Arc::new) - }) - .shared() - }) - .clone(); - - cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) }) - } - - pub fn open_diff_since( - &mut self, - oid: Option, - buffer: Entity, - repo: Entity, - languages: Arc, - cx: &mut Context, - ) -> Task>> { - cx.spawn(async move |this, cx| { - let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot())?; - let content = match oid { - None => None, - Some(oid) => Some( - repo.update(cx, |repo, cx| repo.load_blob_content(oid, cx))? - .await?, - ), - }; - let buffer_diff = cx.new(|cx| BufferDiff::new(&buffer_snapshot, cx))?; - - buffer_diff - .update(cx, |buffer_diff, cx| { - buffer_diff.set_base_text( - content.map(Arc::new), - buffer_snapshot.language().cloned(), - Some(languages.clone()), - buffer_snapshot.text, - cx, - ) - })? - .await?; - let unstaged_diff = this - .update(cx, |this, cx| this.open_unstaged_diff(buffer.clone(), cx))? - .await?; - buffer_diff.update(cx, |buffer_diff, _| { - buffer_diff.set_secondary_diff(unstaged_diff); - })?; - - this.update(cx, |_, cx| { - cx.subscribe(&buffer_diff, Self::on_buffer_diff_event) - .detach(); - })?; - - Ok(buffer_diff) - }) - } - - pub fn open_uncommitted_diff( - &mut self, - buffer: Entity, - cx: &mut Context, - ) -> Task>> { - let buffer_id = buffer.read(cx).remote_id(); - - if let Some(diff_state) = self.diffs.get(&buffer_id) - && let Some(uncommitted_diff) = diff_state - .read(cx) - .uncommitted_diff - .as_ref() - .and_then(|weak| weak.upgrade()) - { - if let Some(task) = - diff_state.update(cx, |diff_state, _| diff_state.wait_for_recalculation()) - { - return cx.background_executor().spawn(async move { - task.await; - Ok(uncommitted_diff) - }); - } - return Task::ready(Ok(uncommitted_diff)); - } - - let Some((repo, repo_path)) = - self.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx) - else { - return Task::ready(Err(anyhow!("failed to find git repository for buffer"))); - }; - - let task = self - .loading_diffs - .entry((buffer_id, DiffKind::Uncommitted)) - .or_insert_with(|| { - let changes = repo.update(cx, |repo, cx| { - repo.load_committed_text(buffer_id, repo_path, cx) - }); - - // todo(lw): hot foreground spawn - cx.spawn(async move |this, cx| { - Self::open_diff_internal(this, DiffKind::Uncommitted, changes.await, buffer, cx) - .await - .map_err(Arc::new) - }) - .shared() - }) - .clone(); - - cx.background_spawn(async move { task.await.map_err(|e| anyhow!("{e}")) }) - } - - async fn open_diff_internal( - this: WeakEntity, - kind: DiffKind, - texts: Result, - buffer_entity: Entity, - cx: &mut AsyncApp, - ) -> Result> { - let diff_bases_change = match texts { - Err(e) => { - this.update(cx, |this, cx| { - let buffer = buffer_entity.read(cx); - let buffer_id = buffer.remote_id(); - this.loading_diffs.remove(&(buffer_id, kind)); - })?; - return Err(e); - } - Ok(change) => change, - }; - - this.update(cx, |this, cx| { - let buffer = buffer_entity.read(cx); - let buffer_id = buffer.remote_id(); - let language = buffer.language().cloned(); - let language_registry = buffer.language_registry(); - let text_snapshot = buffer.text_snapshot(); - this.loading_diffs.remove(&(buffer_id, kind)); - - let git_store = cx.weak_entity(); - let diff_state = this - .diffs - .entry(buffer_id) - .or_insert_with(|| cx.new(|_| BufferGitState::new(git_store))); - - let diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx)); - - cx.subscribe(&diff, Self::on_buffer_diff_event).detach(); - diff_state.update(cx, |diff_state, cx| { - diff_state.language = language; - diff_state.language_registry = language_registry; - - match kind { - DiffKind::Unstaged => diff_state.unstaged_diff = Some(diff.downgrade()), - DiffKind::Uncommitted => { - let unstaged_diff = if let Some(diff) = diff_state.unstaged_diff() { - diff - } else { - let unstaged_diff = cx.new(|cx| BufferDiff::new(&text_snapshot, cx)); - diff_state.unstaged_diff = Some(unstaged_diff.downgrade()); - unstaged_diff - }; - - diff.update(cx, |diff, _| diff.set_secondary_diff(unstaged_diff)); - diff_state.uncommitted_diff = Some(diff.downgrade()) - } - } - - diff_state.diff_bases_changed(text_snapshot, Some(diff_bases_change), cx); - let rx = diff_state.wait_for_recalculation(); - - anyhow::Ok(async move { - if let Some(rx) = rx { - rx.await; - } - Ok(diff) - }) - }) - })?? - .await - } - - pub fn get_unstaged_diff(&self, buffer_id: BufferId, cx: &App) -> Option> { - let diff_state = self.diffs.get(&buffer_id)?; - diff_state.read(cx).unstaged_diff.as_ref()?.upgrade() - } - - pub fn get_uncommitted_diff( - &self, - buffer_id: BufferId, - cx: &App, - ) -> Option> { - let diff_state = self.diffs.get(&buffer_id)?; - diff_state.read(cx).uncommitted_diff.as_ref()?.upgrade() - } - - pub fn open_conflict_set( - &mut self, - buffer: Entity, - cx: &mut Context, - ) -> Entity { - log::debug!("open conflict set"); - let buffer_id = buffer.read(cx).remote_id(); - - if let Some(git_state) = self.diffs.get(&buffer_id) - && let Some(conflict_set) = git_state - .read(cx) - .conflict_set - .as_ref() - .and_then(|weak| weak.upgrade()) - { - let conflict_set = conflict_set; - let buffer_snapshot = buffer.read(cx).text_snapshot(); - - git_state.update(cx, |state, cx| { - let _ = state.reparse_conflict_markers(buffer_snapshot, cx); - }); - - return conflict_set; - } - - let is_unmerged = self - .repository_and_path_for_buffer_id(buffer_id, cx) - .is_some_and(|(repo, path)| repo.read(cx).snapshot.has_conflict(&path)); - let git_store = cx.weak_entity(); - let buffer_git_state = self - .diffs - .entry(buffer_id) - .or_insert_with(|| cx.new(|_| BufferGitState::new(git_store))); - let conflict_set = cx.new(|cx| ConflictSet::new(buffer_id, is_unmerged, cx)); - - self._subscriptions - .push(cx.subscribe(&conflict_set, |_, _, _, cx| { - cx.emit(GitStoreEvent::ConflictsUpdated); - })); - - buffer_git_state.update(cx, |state, cx| { - state.conflict_set = Some(conflict_set.downgrade()); - let buffer_snapshot = buffer.read(cx).text_snapshot(); - let _ = state.reparse_conflict_markers(buffer_snapshot, cx); - }); - - conflict_set - } - - pub fn project_path_git_status( - &self, - project_path: &ProjectPath, - cx: &App, - ) -> Option { - let (repo, repo_path) = self.repository_and_path_for_project_path(project_path, cx)?; - Some(repo.read(cx).status_for_path(&repo_path)?.status) - } - - pub fn checkpoint(&self, cx: &mut App) -> Task> { - let mut work_directory_abs_paths = Vec::new(); - let mut checkpoints = Vec::new(); - for repository in self.repositories.values() { - repository.update(cx, |repository, _| { - work_directory_abs_paths.push(repository.snapshot.work_directory_abs_path.clone()); - checkpoints.push(repository.checkpoint().map(|checkpoint| checkpoint?)); - }); - } - - cx.background_executor().spawn(async move { - let checkpoints = future::try_join_all(checkpoints).await?; - Ok(GitStoreCheckpoint { - checkpoints_by_work_dir_abs_path: work_directory_abs_paths - .into_iter() - .zip(checkpoints) - .collect(), - }) - }) - } - - pub fn restore_checkpoint( - &self, - checkpoint: GitStoreCheckpoint, - cx: &mut App, - ) -> Task> { - let repositories_by_work_dir_abs_path = self - .repositories - .values() - .map(|repo| (repo.read(cx).snapshot.work_directory_abs_path.clone(), repo)) - .collect::>(); - - let mut tasks = Vec::new(); - for (work_dir_abs_path, checkpoint) in checkpoint.checkpoints_by_work_dir_abs_path { - if let Some(repository) = repositories_by_work_dir_abs_path.get(&work_dir_abs_path) { - let restore = repository.update(cx, |repository, _| { - repository.restore_checkpoint(checkpoint) - }); - tasks.push(async move { restore.await? }); - } - } - cx.background_spawn(async move { - future::try_join_all(tasks).await?; - Ok(()) - }) - } - - /// Compares two checkpoints, returning true if they are equal. - pub fn compare_checkpoints( - &self, - left: GitStoreCheckpoint, - mut right: GitStoreCheckpoint, - cx: &mut App, - ) -> Task> { - let repositories_by_work_dir_abs_path = self - .repositories - .values() - .map(|repo| (repo.read(cx).snapshot.work_directory_abs_path.clone(), repo)) - .collect::>(); - - let mut tasks = Vec::new(); - for (work_dir_abs_path, left_checkpoint) in left.checkpoints_by_work_dir_abs_path { - if let Some(right_checkpoint) = right - .checkpoints_by_work_dir_abs_path - .remove(&work_dir_abs_path) - { - if let Some(repository) = repositories_by_work_dir_abs_path.get(&work_dir_abs_path) - { - let compare = repository.update(cx, |repository, _| { - repository.compare_checkpoints(left_checkpoint, right_checkpoint) - }); - - tasks.push(async move { compare.await? }); - } - } else { - return Task::ready(Ok(false)); - } - } - cx.background_spawn(async move { - Ok(future::try_join_all(tasks) - .await? - .into_iter() - .all(|result| result)) - }) - } - - /// Blames a buffer. - pub fn blame_buffer( - &self, - buffer: &Entity, - version: Option, - cx: &mut Context, - ) -> Task>> { - let buffer = buffer.read(cx); - let Some((repo, repo_path)) = - self.repository_and_path_for_buffer_id(buffer.remote_id(), cx) - else { - return Task::ready(Err(anyhow!("failed to find a git repository for buffer"))); - }; - let content = match &version { - Some(version) => buffer.rope_for_version(version), - None => buffer.as_rope().clone(), - }; - let line_ending = buffer.line_ending(); - let version = version.unwrap_or(buffer.version()); - let buffer_id = buffer.remote_id(); - - let repo = repo.downgrade(); - cx.spawn(async move |_, cx| { - let repository_state = repo - .update(cx, |repo, _| repo.repository_state.clone())? - .await - .map_err(|err| anyhow::anyhow!(err))?; - match repository_state { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => backend - .blame(repo_path.clone(), content, line_ending) - .await - .with_context(|| format!("Failed to blame {:?}", repo_path.as_ref())) - .map(Some), - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - let response = client - .request(proto::BlameBuffer { - project_id: project_id.to_proto(), - buffer_id: buffer_id.into(), - version: serialize_version(&version), - }) - .await?; - Ok(deserialize_blame_buffer_response(response)) - } - } - }) - } - - pub fn file_history( - &self, - repo: &Entity, - path: RepoPath, - cx: &mut App, - ) -> Task> { - let rx = repo.update(cx, |repo, _| repo.file_history(path)); - - cx.spawn(|_: &mut AsyncApp| async move { rx.await? }) - } - - pub fn file_history_paginated( - &self, - repo: &Entity, - path: RepoPath, - skip: usize, - limit: Option, - cx: &mut App, - ) -> Task> { - let rx = repo.update(cx, |repo, _| repo.file_history_paginated(path, skip, limit)); - - cx.spawn(|_: &mut AsyncApp| async move { rx.await? }) - } - - pub fn get_permalink_to_line( - &self, - buffer: &Entity, - selection: Range, - cx: &mut App, - ) -> Task> { - let Some(file) = File::from_dyn(buffer.read(cx).file()) else { - return Task::ready(Err(anyhow!("buffer has no file"))); - }; - - let Some((repo, repo_path)) = self.repository_and_path_for_project_path( - &(file.worktree.read(cx).id(), file.path.clone()).into(), - cx, - ) else { - // If we're not in a Git repo, check whether this is a Rust source - // file in the Cargo registry (presumably opened with go-to-definition - // from a normal Rust file). If so, we can put together a permalink - // using crate metadata. - if buffer - .read(cx) - .language() - .is_none_or(|lang| lang.name() != "Rust".into()) - { - return Task::ready(Err(anyhow!("no permalink available"))); - } - let file_path = file.worktree.read(cx).absolutize(&file.path); - return cx.spawn(async move |cx| { - let provider_registry = cx.update(GitHostingProviderRegistry::default_global)?; - get_permalink_in_rust_registry_src(provider_registry, file_path, selection) - .context("no permalink available") - }); - }; - - let buffer_id = buffer.read(cx).remote_id(); - let branch = repo.read(cx).branch.clone(); - let remote = branch - .as_ref() - .and_then(|b| b.upstream.as_ref()) - .and_then(|b| b.remote_name()) - .unwrap_or("origin") - .to_string(); - - let rx = repo.update(cx, |repo, _| { - repo.send_job(None, move |state, cx| async move { - match state { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - let origin_url = backend - .remote_url(&remote) - .await - .with_context(|| format!("remote \"{remote}\" not found"))?; - - let sha = backend.head_sha().await.context("reading HEAD SHA")?; - - let provider_registry = - cx.update(GitHostingProviderRegistry::default_global)?; - - let (provider, remote) = - parse_git_remote_url(provider_registry, &origin_url) - .context("parsing Git remote URL")?; - - Ok(provider.build_permalink( - remote, - BuildPermalinkParams::new(&sha, &repo_path, Some(selection)), - )) - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - let response = client - .request(proto::GetPermalinkToLine { - project_id: project_id.to_proto(), - buffer_id: buffer_id.into(), - selection: Some(proto::Range { - start: selection.start as u64, - end: selection.end as u64, - }), - }) - .await?; - - url::Url::parse(&response.permalink).context("failed to parse permalink") - } - } - }) - }); - cx.spawn(|_: &mut AsyncApp| async move { rx.await? }) - } - - fn downstream_client(&self) -> Option<(AnyProtoClient, ProjectId)> { - match &self.state { - GitStoreState::Local { - downstream: downstream_client, - .. - } => downstream_client - .as_ref() - .map(|state| (state.client.clone(), state.project_id)), - GitStoreState::Remote { - downstream: downstream_client, - .. - } => downstream_client.clone(), - } - } - - fn upstream_client(&self) -> Option { - match &self.state { - GitStoreState::Local { .. } => None, - GitStoreState::Remote { - upstream_client, .. - } => Some(upstream_client.clone()), - } - } - - fn on_worktree_store_event( - &mut self, - worktree_store: Entity, - event: &WorktreeStoreEvent, - cx: &mut Context, - ) { - let GitStoreState::Local { - project_environment, - downstream, - next_repository_id, - fs, - } = &self.state - else { - return; - }; - - match event { - WorktreeStoreEvent::WorktreeUpdatedEntries(worktree_id, updated_entries) => { - if let Some(worktree) = self - .worktree_store - .read(cx) - .worktree_for_id(*worktree_id, cx) - { - let paths_by_git_repo = - self.process_updated_entries(&worktree, updated_entries, cx); - let downstream = downstream - .as_ref() - .map(|downstream| downstream.updates_tx.clone()); - cx.spawn(async move |_, cx| { - let paths_by_git_repo = paths_by_git_repo.await; - for (repo, paths) in paths_by_git_repo { - repo.update(cx, |repo, cx| { - repo.paths_changed(paths, downstream.clone(), cx); - }) - .ok(); - } - }) - .detach(); - } - } - WorktreeStoreEvent::WorktreeUpdatedGitRepositories(worktree_id, changed_repos) => { - let Some(worktree) = worktree_store.read(cx).worktree_for_id(*worktree_id, cx) - else { - return; - }; - if !worktree.read(cx).is_visible() { - log::debug!( - "not adding repositories for local worktree {:?} because it's not visible", - worktree.read(cx).abs_path() - ); - return; - } - self.update_repositories_from_worktree( - *worktree_id, - project_environment.clone(), - next_repository_id.clone(), - downstream - .as_ref() - .map(|downstream| downstream.updates_tx.clone()), - changed_repos.clone(), - fs.clone(), - cx, - ); - self.local_worktree_git_repos_changed(worktree, changed_repos, cx); - } - WorktreeStoreEvent::WorktreeRemoved(_entity_id, worktree_id) => { - let repos_without_worktree: Vec = self - .worktree_ids - .iter_mut() - .filter_map(|(repo_id, worktree_ids)| { - worktree_ids.remove(worktree_id); - if worktree_ids.is_empty() { - Some(*repo_id) - } else { - None - } - }) - .collect(); - let is_active_repo_removed = repos_without_worktree - .iter() - .any(|repo_id| self.active_repo_id == Some(*repo_id)); - - for repo_id in repos_without_worktree { - self.repositories.remove(&repo_id); - self.worktree_ids.remove(&repo_id); - if let Some(updates_tx) = - downstream.as_ref().map(|downstream| &downstream.updates_tx) - { - updates_tx - .unbounded_send(DownstreamUpdate::RemoveRepository(repo_id)) - .ok(); - } - } - - if is_active_repo_removed { - if let Some((&repo_id, _)) = self.repositories.iter().next() { - self.active_repo_id = Some(repo_id); - cx.emit(GitStoreEvent::ActiveRepositoryChanged(Some(repo_id))); - } else { - self.active_repo_id = None; - cx.emit(GitStoreEvent::ActiveRepositoryChanged(None)); - } - } - } - _ => {} - } - } - fn on_repository_event( - &mut self, - repo: Entity, - event: &RepositoryEvent, - cx: &mut Context, - ) { - let id = repo.read(cx).id; - let repo_snapshot = repo.read(cx).snapshot.clone(); - for (buffer_id, diff) in self.diffs.iter() { - if let Some((buffer_repo, repo_path)) = - self.repository_and_path_for_buffer_id(*buffer_id, cx) - && buffer_repo == repo - { - diff.update(cx, |diff, cx| { - if let Some(conflict_set) = &diff.conflict_set { - let conflict_status_changed = - conflict_set.update(cx, |conflict_set, cx| { - let has_conflict = repo_snapshot.has_conflict(&repo_path); - conflict_set.set_has_conflict(has_conflict, cx) - })?; - if conflict_status_changed { - let buffer_store = self.buffer_store.read(cx); - if let Some(buffer) = buffer_store.get(*buffer_id) { - let _ = diff - .reparse_conflict_markers(buffer.read(cx).text_snapshot(), cx); - } - } - } - anyhow::Ok(()) - }) - .ok(); - } - } - cx.emit(GitStoreEvent::RepositoryUpdated( - id, - event.clone(), - self.active_repo_id == Some(id), - )) - } - - fn on_jobs_updated(&mut self, _: Entity, _: &JobsUpdated, cx: &mut Context) { - cx.emit(GitStoreEvent::JobsUpdated) - } - - /// Update our list of repositories and schedule git scans in response to a notification from a worktree, - fn update_repositories_from_worktree( - &mut self, - worktree_id: WorktreeId, - project_environment: Entity, - next_repository_id: Arc, - updates_tx: Option>, - updated_git_repositories: UpdatedGitRepositoriesSet, - fs: Arc, - cx: &mut Context, - ) { - let mut removed_ids = Vec::new(); - for update in updated_git_repositories.iter() { - if let Some((id, existing)) = self.repositories.iter().find(|(_, repo)| { - let existing_work_directory_abs_path = - repo.read(cx).work_directory_abs_path.clone(); - Some(&existing_work_directory_abs_path) - == update.old_work_directory_abs_path.as_ref() - || Some(&existing_work_directory_abs_path) - == update.new_work_directory_abs_path.as_ref() - }) { - let repo_id = *id; - if let Some(new_work_directory_abs_path) = - update.new_work_directory_abs_path.clone() - { - self.worktree_ids - .entry(repo_id) - .or_insert_with(HashSet::new) - .insert(worktree_id); - existing.update(cx, |existing, cx| { - existing.snapshot.work_directory_abs_path = new_work_directory_abs_path; - existing.schedule_scan(updates_tx.clone(), cx); - }); - } else { - if let Some(worktree_ids) = self.worktree_ids.get_mut(&repo_id) { - worktree_ids.remove(&worktree_id); - if worktree_ids.is_empty() { - removed_ids.push(repo_id); - } - } - } - } else if let UpdatedGitRepository { - new_work_directory_abs_path: Some(work_directory_abs_path), - dot_git_abs_path: Some(dot_git_abs_path), - repository_dir_abs_path: Some(_repository_dir_abs_path), - common_dir_abs_path: Some(_common_dir_abs_path), - .. - } = update - { - let id = RepositoryId(next_repository_id.fetch_add(1, atomic::Ordering::Release)); - let git_store = cx.weak_entity(); - let repo = cx.new(|cx| { - let mut repo = Repository::local( - id, - work_directory_abs_path.clone(), - dot_git_abs_path.clone(), - project_environment.downgrade(), - fs.clone(), - git_store, - cx, - ); - if let Some(updates_tx) = updates_tx.as_ref() { - // trigger an empty `UpdateRepository` to ensure remote active_repo_id is set correctly - updates_tx - .unbounded_send(DownstreamUpdate::UpdateRepository(repo.snapshot())) - .ok(); - } - repo.schedule_scan(updates_tx.clone(), cx); - repo - }); - self._subscriptions - .push(cx.subscribe(&repo, Self::on_repository_event)); - self._subscriptions - .push(cx.subscribe(&repo, Self::on_jobs_updated)); - self.repositories.insert(id, repo); - self.worktree_ids.insert(id, HashSet::from([worktree_id])); - cx.emit(GitStoreEvent::RepositoryAdded); - self.active_repo_id.get_or_insert_with(|| { - cx.emit(GitStoreEvent::ActiveRepositoryChanged(Some(id))); - id - }); - } - } - - for id in removed_ids { - if self.active_repo_id == Some(id) { - self.active_repo_id = None; - cx.emit(GitStoreEvent::ActiveRepositoryChanged(None)); - } - self.repositories.remove(&id); - if let Some(updates_tx) = updates_tx.as_ref() { - updates_tx - .unbounded_send(DownstreamUpdate::RemoveRepository(id)) - .ok(); - } - } - } - - fn on_buffer_store_event( - &mut self, - _: Entity, - event: &BufferStoreEvent, - cx: &mut Context, - ) { - match event { - BufferStoreEvent::BufferAdded(buffer) => { - cx.subscribe(buffer, |this, buffer, event, cx| { - if let BufferEvent::LanguageChanged(_) = event { - let buffer_id = buffer.read(cx).remote_id(); - if let Some(diff_state) = this.diffs.get(&buffer_id) { - diff_state.update(cx, |diff_state, cx| { - diff_state.buffer_language_changed(buffer, cx); - }); - } - } - }) - .detach(); - } - BufferStoreEvent::SharedBufferClosed(peer_id, buffer_id) => { - if let Some(diffs) = self.shared_diffs.get_mut(peer_id) { - diffs.remove(buffer_id); - } - } - BufferStoreEvent::BufferDropped(buffer_id) => { - self.diffs.remove(buffer_id); - for diffs in self.shared_diffs.values_mut() { - diffs.remove(buffer_id); - } - } - BufferStoreEvent::BufferChangedFilePath { buffer, .. } => { - // Whenever a buffer's file path changes, it's possible that the - // new path is actually a path that is being tracked by a git - // repository. In that case, we'll want to update the buffer's - // `BufferDiffState`, in case it already has one. - let buffer_id = buffer.read(cx).remote_id(); - let diff_state = self.diffs.get(&buffer_id); - let repo = self.repository_and_path_for_buffer_id(buffer_id, cx); - - if let Some(diff_state) = diff_state - && let Some((repo, repo_path)) = repo - { - let buffer = buffer.clone(); - let diff_state = diff_state.clone(); - - cx.spawn(async move |_git_store, cx| { - async { - let diff_bases_change = repo - .update(cx, |repo, cx| { - repo.load_committed_text(buffer_id, repo_path, cx) - })? - .await?; - - diff_state.update(cx, |diff_state, cx| { - let buffer_snapshot = buffer.read(cx).text_snapshot(); - diff_state.diff_bases_changed( - buffer_snapshot, - Some(diff_bases_change), - cx, - ); - }) - } - .await - .log_err(); - }) - .detach(); - } - } - _ => {} - } - } - - pub fn recalculate_buffer_diffs( - &mut self, - buffers: Vec>, - cx: &mut Context, - ) -> impl Future + use<> { - let mut futures = Vec::new(); - for buffer in buffers { - if let Some(diff_state) = self.diffs.get_mut(&buffer.read(cx).remote_id()) { - let buffer = buffer.read(cx).text_snapshot(); - diff_state.update(cx, |diff_state, cx| { - diff_state.recalculate_diffs(buffer.clone(), cx); - futures.extend(diff_state.wait_for_recalculation().map(FutureExt::boxed)); - }); - futures.push(diff_state.update(cx, |diff_state, cx| { - diff_state - .reparse_conflict_markers(buffer, cx) - .map(|_| {}) - .boxed() - })); - } - } - async move { - futures::future::join_all(futures).await; - } - } - - fn on_buffer_diff_event( - &mut self, - diff: Entity, - event: &BufferDiffEvent, - cx: &mut Context, - ) { - if let BufferDiffEvent::HunksStagedOrUnstaged(new_index_text) = event { - let buffer_id = diff.read(cx).buffer_id; - if let Some(diff_state) = self.diffs.get(&buffer_id) { - let hunk_staging_operation_count = diff_state.update(cx, |diff_state, _| { - diff_state.hunk_staging_operation_count += 1; - diff_state.hunk_staging_operation_count - }); - if let Some((repo, path)) = self.repository_and_path_for_buffer_id(buffer_id, cx) { - let recv = repo.update(cx, |repo, cx| { - log::debug!("hunks changed for {}", path.as_unix_str()); - repo.spawn_set_index_text_job( - path, - new_index_text.as_ref().map(|rope| rope.to_string()), - Some(hunk_staging_operation_count), - cx, - ) - }); - let diff = diff.downgrade(); - cx.spawn(async move |this, cx| { - if let Ok(Err(error)) = cx.background_spawn(recv).await { - diff.update(cx, |diff, cx| { - diff.clear_pending_hunks(cx); - }) - .ok(); - this.update(cx, |_, cx| cx.emit(GitStoreEvent::IndexWriteError(error))) - .ok(); - } - }) - .detach(); - } - } - } - } - - fn local_worktree_git_repos_changed( - &mut self, - worktree: Entity, - changed_repos: &UpdatedGitRepositoriesSet, - cx: &mut Context, - ) { - log::debug!("local worktree repos changed"); - debug_assert!(worktree.read(cx).is_local()); - - for repository in self.repositories.values() { - repository.update(cx, |repository, cx| { - let repo_abs_path = &repository.work_directory_abs_path; - if changed_repos.iter().any(|update| { - update.old_work_directory_abs_path.as_ref() == Some(repo_abs_path) - || update.new_work_directory_abs_path.as_ref() == Some(repo_abs_path) - }) { - repository.reload_buffer_diff_bases(cx); - } - }); - } - } - - pub fn repositories(&self) -> &HashMap> { - &self.repositories - } - - pub fn status_for_buffer_id(&self, buffer_id: BufferId, cx: &App) -> Option { - let (repo, path) = self.repository_and_path_for_buffer_id(buffer_id, cx)?; - let status = repo.read(cx).snapshot.status_for_path(&path)?; - Some(status.status) - } - - pub fn repository_and_path_for_buffer_id( - &self, - buffer_id: BufferId, - cx: &App, - ) -> Option<(Entity, RepoPath)> { - let buffer = self.buffer_store.read(cx).get(buffer_id)?; - let project_path = buffer.read(cx).project_path(cx)?; - self.repository_and_path_for_project_path(&project_path, cx) - } - - pub fn repository_and_path_for_project_path( - &self, - path: &ProjectPath, - cx: &App, - ) -> Option<(Entity, RepoPath)> { - let abs_path = self.worktree_store.read(cx).absolutize(path, cx)?; - self.repositories - .values() - .filter_map(|repo| { - let repo_path = repo.read(cx).abs_path_to_repo_path(&abs_path)?; - Some((repo.clone(), repo_path)) - }) - .max_by_key(|(repo, _)| repo.read(cx).work_directory_abs_path.clone()) - } - - pub fn git_init( - &self, - path: Arc, - fallback_branch_name: String, - cx: &App, - ) -> Task> { - match &self.state { - GitStoreState::Local { fs, .. } => { - let fs = fs.clone(); - cx.background_executor() - .spawn(async move { fs.git_init(&path, fallback_branch_name).await }) - } - GitStoreState::Remote { - upstream_client, - upstream_project_id: project_id, - .. - } => { - let client = upstream_client.clone(); - let project_id = *project_id; - cx.background_executor().spawn(async move { - client - .request(proto::GitInit { - project_id: project_id, - abs_path: path.to_string_lossy().into_owned(), - fallback_branch_name, - }) - .await?; - Ok(()) - }) - } - } - } - - pub fn git_clone( - &self, - repo: String, - path: impl Into>, - cx: &App, - ) -> Task> { - let path = path.into(); - match &self.state { - GitStoreState::Local { fs, .. } => { - let fs = fs.clone(); - cx.background_executor() - .spawn(async move { fs.git_clone(&repo, &path).await }) - } - GitStoreState::Remote { - upstream_client, - upstream_project_id, - .. - } => { - if upstream_client.is_via_collab() { - return Task::ready(Err(anyhow!( - "Git Clone isn't supported for project guests" - ))); - } - let request = upstream_client.request(proto::GitClone { - project_id: *upstream_project_id, - abs_path: path.to_string_lossy().into_owned(), - remote_repo: repo, - }); - - cx.background_spawn(async move { - let result = request.await?; - - match result.success { - true => Ok(()), - false => Err(anyhow!("Git Clone failed")), - } - }) - } - } - } - - async fn handle_update_repository( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - this.update(&mut cx, |this, cx| { - let path_style = this.worktree_store.read(cx).path_style(); - let mut update = envelope.payload; - - let id = RepositoryId::from_proto(update.id); - let client = this.upstream_client().context("no upstream client")?; - - let mut repo_subscription = None; - let repo = this.repositories.entry(id).or_insert_with(|| { - let git_store = cx.weak_entity(); - let repo = cx.new(|cx| { - Repository::remote( - id, - Path::new(&update.abs_path).into(), - path_style, - ProjectId(update.project_id), - client, - git_store, - cx, - ) - }); - repo_subscription = Some(cx.subscribe(&repo, Self::on_repository_event)); - cx.emit(GitStoreEvent::RepositoryAdded); - repo - }); - this._subscriptions.extend(repo_subscription); - - repo.update(cx, { - let update = update.clone(); - |repo, cx| repo.apply_remote_update(update, cx) - })?; - - this.active_repo_id.get_or_insert_with(|| { - cx.emit(GitStoreEvent::ActiveRepositoryChanged(Some(id))); - id - }); - - if let Some((client, project_id)) = this.downstream_client() { - update.project_id = project_id.to_proto(); - client.send(update).log_err(); - } - Ok(()) - })? - } - - async fn handle_remove_repository( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - this.update(&mut cx, |this, cx| { - let mut update = envelope.payload; - let id = RepositoryId::from_proto(update.id); - this.repositories.remove(&id); - if let Some((client, project_id)) = this.downstream_client() { - update.project_id = project_id.to_proto(); - client.send(update).log_err(); - } - if this.active_repo_id == Some(id) { - this.active_repo_id = None; - cx.emit(GitStoreEvent::ActiveRepositoryChanged(None)); - } - cx.emit(GitStoreEvent::RepositoryRemoved(id)); - }) - } - - async fn handle_git_init( - this: Entity, - envelope: TypedEnvelope, - cx: AsyncApp, - ) -> Result { - let path: Arc = PathBuf::from(envelope.payload.abs_path).into(); - let name = envelope.payload.fallback_branch_name; - cx.update(|cx| this.read(cx).git_init(path, name, cx))? - .await?; - - Ok(proto::Ack {}) - } - - async fn handle_git_clone( - this: Entity, - envelope: TypedEnvelope, - cx: AsyncApp, - ) -> Result { - let path: Arc = PathBuf::from(envelope.payload.abs_path).into(); - let repo_name = envelope.payload.remote_repo; - let result = cx - .update(|cx| this.read(cx).git_clone(repo_name, path, cx))? - .await; - - Ok(proto::GitCloneResponse { - success: result.is_ok(), - }) - } - - async fn handle_fetch( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - let fetch_options = FetchOptions::from_proto(envelope.payload.remote); - let askpass_id = envelope.payload.askpass_id; - - let askpass = make_remote_delegate( - this, - envelope.payload.project_id, - repository_id, - askpass_id, - &mut cx, - ); - - let remote_output = repository_handle - .update(&mut cx, |repository_handle, cx| { - repository_handle.fetch(fetch_options, askpass, cx) - })? - .await??; - - Ok(proto::RemoteMessageResponse { - stdout: remote_output.stdout, - stderr: remote_output.stderr, - }) - } - - async fn handle_push( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - - let askpass_id = envelope.payload.askpass_id; - let askpass = make_remote_delegate( - this, - envelope.payload.project_id, - repository_id, - askpass_id, - &mut cx, - ); - - let options = envelope - .payload - .options - .as_ref() - .map(|_| match envelope.payload.options() { - proto::push::PushOptions::SetUpstream => git::repository::PushOptions::SetUpstream, - proto::push::PushOptions::Force => git::repository::PushOptions::Force, - }); - - let branch_name = envelope.payload.branch_name.into(); - let remote_name = envelope.payload.remote_name.into(); - - let remote_output = repository_handle - .update(&mut cx, |repository_handle, cx| { - repository_handle.push(branch_name, remote_name, options, askpass, cx) - })? - .await??; - Ok(proto::RemoteMessageResponse { - stdout: remote_output.stdout, - stderr: remote_output.stderr, - }) - } - - async fn handle_pull( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - let askpass_id = envelope.payload.askpass_id; - let askpass = make_remote_delegate( - this, - envelope.payload.project_id, - repository_id, - askpass_id, - &mut cx, - ); - - let branch_name = envelope.payload.branch_name.map(|name| name.into()); - let remote_name = envelope.payload.remote_name.into(); - let rebase = envelope.payload.rebase; - - let remote_message = repository_handle - .update(&mut cx, |repository_handle, cx| { - repository_handle.pull(branch_name, remote_name, rebase, askpass, cx) - })? - .await??; - - Ok(proto::RemoteMessageResponse { - stdout: remote_message.stdout, - stderr: remote_message.stderr, - }) - } - - async fn handle_stage( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - - let entries = envelope - .payload - .paths - .into_iter() - .map(|path| RepoPath::new(&path)) - .collect::>>()?; - - repository_handle - .update(&mut cx, |repository_handle, cx| { - repository_handle.stage_entries(entries, cx) - })? - .await?; - Ok(proto::Ack {}) - } - - async fn handle_unstage( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - - let entries = envelope - .payload - .paths - .into_iter() - .map(|path| RepoPath::new(&path)) - .collect::>>()?; - - repository_handle - .update(&mut cx, |repository_handle, cx| { - repository_handle.unstage_entries(entries, cx) - })? - .await?; - - Ok(proto::Ack {}) - } - - async fn handle_stash( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - - let entries = envelope - .payload - .paths - .into_iter() - .map(|path| RepoPath::new(&path)) - .collect::>>()?; - - repository_handle - .update(&mut cx, |repository_handle, cx| { - repository_handle.stash_entries(entries, cx) - })? - .await?; - - Ok(proto::Ack {}) - } - - async fn handle_stash_pop( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - let stash_index = envelope.payload.stash_index.map(|i| i as usize); - - repository_handle - .update(&mut cx, |repository_handle, cx| { - repository_handle.stash_pop(stash_index, cx) - })? - .await?; - - Ok(proto::Ack {}) - } - - async fn handle_stash_apply( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - let stash_index = envelope.payload.stash_index.map(|i| i as usize); - - repository_handle - .update(&mut cx, |repository_handle, cx| { - repository_handle.stash_apply(stash_index, cx) - })? - .await?; - - Ok(proto::Ack {}) - } - - async fn handle_stash_drop( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - let stash_index = envelope.payload.stash_index.map(|i| i as usize); - - repository_handle - .update(&mut cx, |repository_handle, cx| { - repository_handle.stash_drop(stash_index, cx) - })? - .await??; - - Ok(proto::Ack {}) - } - - async fn handle_set_index_text( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - let repo_path = RepoPath::from_proto(&envelope.payload.path)?; - - repository_handle - .update(&mut cx, |repository_handle, cx| { - repository_handle.spawn_set_index_text_job( - repo_path, - envelope.payload.text, - None, - cx, - ) - })? - .await??; - Ok(proto::Ack {}) - } - - async fn handle_run_hook( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - let hook = RunHook::from_proto(envelope.payload.hook).context("invalid hook")?; - repository_handle - .update(&mut cx, |repository_handle, cx| { - repository_handle.run_hook(hook, cx) - })? - .await??; - Ok(proto::Ack {}) - } - - async fn handle_commit( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - let askpass_id = envelope.payload.askpass_id; - - let askpass = make_remote_delegate( - this, - envelope.payload.project_id, - repository_id, - askpass_id, - &mut cx, - ); - - let message = SharedString::from(envelope.payload.message); - let name = envelope.payload.name.map(SharedString::from); - let email = envelope.payload.email.map(SharedString::from); - let options = envelope.payload.options.unwrap_or_default(); - - repository_handle - .update(&mut cx, |repository_handle, cx| { - repository_handle.commit( - message, - name.zip(email), - CommitOptions { - amend: options.amend, - signoff: options.signoff, - }, - askpass, - cx, - ) - })? - .await??; - Ok(proto::Ack {}) - } - - async fn handle_get_remotes( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - - let branch_name = envelope.payload.branch_name; - let is_push = envelope.payload.is_push; - - let remotes = repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.get_remotes(branch_name, is_push) - })? - .await??; - - Ok(proto::GetRemotesResponse { - remotes: remotes - .into_iter() - .map(|remotes| proto::get_remotes_response::Remote { - name: remotes.name.to_string(), - }) - .collect::>(), - }) - } - - async fn handle_get_worktrees( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - - let worktrees = repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.worktrees() - })? - .await??; - - Ok(proto::GitWorktreesResponse { - worktrees: worktrees - .into_iter() - .map(|worktree| worktree_to_proto(&worktree)) - .collect::>(), - }) - } - - async fn handle_create_worktree( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - let directory = PathBuf::from(envelope.payload.directory); - let name = envelope.payload.name; - let commit = envelope.payload.commit; - - repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.create_worktree(name, directory, commit) - })? - .await??; - - Ok(proto::Ack {}) - } - - async fn handle_get_branches( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - - let branches = repository_handle - .update(&mut cx, |repository_handle, _| repository_handle.branches())? - .await??; - - Ok(proto::GitBranchesResponse { - branches: branches - .into_iter() - .map(|branch| branch_to_proto(&branch)) - .collect::>(), - }) - } - async fn handle_get_default_branch( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - - let branch = repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.default_branch() - })? - .await?? - .map(Into::into); - - Ok(proto::GetDefaultBranchResponse { branch }) - } - async fn handle_create_branch( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - let branch_name = envelope.payload.branch_name; - - repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.create_branch(branch_name, None) - })? - .await??; - - Ok(proto::Ack {}) - } - - async fn handle_change_branch( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - let branch_name = envelope.payload.branch_name; - - repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.change_branch(branch_name) - })? - .await??; - - Ok(proto::Ack {}) - } - - async fn handle_rename_branch( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - let branch = envelope.payload.branch; - let new_name = envelope.payload.new_name; - - repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.rename_branch(branch, new_name) - })? - .await??; - - Ok(proto::Ack {}) - } - - async fn handle_create_remote( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - let remote_name = envelope.payload.remote_name; - let remote_url = envelope.payload.remote_url; - - repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.create_remote(remote_name, remote_url) - })? - .await??; - - Ok(proto::Ack {}) - } - - async fn handle_delete_branch( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - let branch_name = envelope.payload.branch_name; - - repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.delete_branch(branch_name) - })? - .await??; - - Ok(proto::Ack {}) - } - - async fn handle_remove_remote( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - let remote_name = envelope.payload.remote_name; - - repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.remove_remote(remote_name) - })? - .await??; - - Ok(proto::Ack {}) - } - - async fn handle_show( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - - let commit = repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.show(envelope.payload.commit) - })? - .await??; - Ok(proto::GitCommitDetails { - sha: commit.sha.into(), - message: commit.message.into(), - commit_timestamp: commit.commit_timestamp, - author_email: commit.author_email.into(), - author_name: commit.author_name.into(), - }) - } - - async fn handle_load_commit_diff( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - - let commit_diff = repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.load_commit_diff(envelope.payload.commit) - })? - .await??; - Ok(proto::LoadCommitDiffResponse { - files: commit_diff - .files - .into_iter() - .map(|file| proto::CommitFile { - path: file.path.to_proto(), - old_text: file.old_text, - new_text: file.new_text, - }) - .collect(), - }) - } - - async fn handle_file_history( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - let path = RepoPath::from_proto(&envelope.payload.path)?; - let skip = envelope.payload.skip as usize; - let limit = envelope.payload.limit.map(|l| l as usize); - - let file_history = repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.file_history_paginated(path, skip, limit) - })? - .await??; - - Ok(proto::GitFileHistoryResponse { - entries: file_history - .entries - .into_iter() - .map(|entry| proto::FileHistoryEntry { - sha: entry.sha.to_string(), - subject: entry.subject.to_string(), - message: entry.message.to_string(), - commit_timestamp: entry.commit_timestamp, - author_name: entry.author_name.to_string(), - author_email: entry.author_email.to_string(), - }) - .collect(), - path: file_history.path.to_proto(), - }) - } - - async fn handle_reset( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - - let mode = match envelope.payload.mode() { - git_reset::ResetMode::Soft => ResetMode::Soft, - git_reset::ResetMode::Mixed => ResetMode::Mixed, - }; - - repository_handle - .update(&mut cx, |repository_handle, cx| { - repository_handle.reset(envelope.payload.commit, mode, cx) - })? - .await??; - Ok(proto::Ack {}) - } - - async fn handle_checkout_files( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - let paths = envelope - .payload - .paths - .iter() - .map(|s| RepoPath::from_proto(s)) - .collect::>>()?; - - repository_handle - .update(&mut cx, |repository_handle, cx| { - repository_handle.checkout_files(&envelope.payload.commit, paths, cx) - })? - .await?; - Ok(proto::Ack {}) - } - - async fn handle_open_commit_message_buffer( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository = Self::repository_for_request(&this, repository_id, &mut cx)?; - let buffer = repository - .update(&mut cx, |repository, cx| { - repository.open_commit_buffer(None, this.read(cx).buffer_store.clone(), cx) - })? - .await?; - - let buffer_id = buffer.read_with(&cx, |buffer, _| buffer.remote_id())?; - this.update(&mut cx, |this, cx| { - this.buffer_store.update(cx, |buffer_store, cx| { - buffer_store - .create_buffer_for_peer( - &buffer, - envelope.original_sender_id.unwrap_or(envelope.sender_id), - cx, - ) - .detach_and_log_err(cx); - }) - })?; - - Ok(proto::OpenBufferResponse { - buffer_id: buffer_id.to_proto(), - }) - } - - async fn handle_askpass( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository = Self::repository_for_request(&this, repository_id, &mut cx)?; - - let delegates = cx.update(|cx| repository.read(cx).askpass_delegates.clone())?; - let Some(mut askpass) = delegates.lock().remove(&envelope.payload.askpass_id) else { - debug_panic!("no askpass found"); - anyhow::bail!("no askpass found"); - }; - - let response = askpass - .ask_password(envelope.payload.prompt) - .await - .ok_or_else(|| anyhow::anyhow!("askpass cancelled"))?; - - delegates - .lock() - .insert(envelope.payload.askpass_id, askpass); - - // In fact, we don't quite know what we're doing here, as we're sending askpass password unencrypted, but.. - Ok(proto::AskPassResponse { - response: response.decrypt(IKnowWhatIAmDoingAndIHaveReadTheDocs)?, - }) - } - - async fn handle_check_for_pushed_commits( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - - let branches = repository_handle - .update(&mut cx, |repository_handle, _| { - repository_handle.check_for_pushed_commits() - })? - .await??; - Ok(proto::CheckForPushedCommitsResponse { - pushed_to: branches - .into_iter() - .map(|commit| commit.to_string()) - .collect(), - }) - } - - async fn handle_git_diff( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId::from_proto(envelope.payload.repository_id); - let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?; - let diff_type = match envelope.payload.diff_type() { - proto::git_diff::DiffType::HeadToIndex => DiffType::HeadToIndex, - proto::git_diff::DiffType::HeadToWorktree => DiffType::HeadToWorktree, - }; - - let mut diff = repository_handle - .update(&mut cx, |repository_handle, cx| { - repository_handle.diff(diff_type, cx) - })? - .await??; - const ONE_MB: usize = 1_000_000; - if diff.len() > ONE_MB { - diff = diff.chars().take(ONE_MB).collect() - } - - Ok(proto::GitDiffResponse { diff }) - } - - async fn handle_tree_diff( - this: Entity, - request: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let repository_id = RepositoryId(request.payload.repository_id); - let diff_type = if request.payload.is_merge { - DiffTreeType::MergeBase { - base: request.payload.base.into(), - head: request.payload.head.into(), - } - } else { - DiffTreeType::Since { - base: request.payload.base.into(), - head: request.payload.head.into(), - } - }; - - let diff = this - .update(&mut cx, |this, cx| { - let repository = this.repositories().get(&repository_id)?; - Some(repository.update(cx, |repo, cx| repo.diff_tree(diff_type, cx))) - })? - .context("missing repository")? - .await??; - - Ok(proto::GetTreeDiffResponse { - entries: diff - .entries - .into_iter() - .map(|(path, status)| proto::TreeDiffStatus { - path: path.as_ref().to_proto(), - status: match status { - TreeDiffStatus::Added {} => proto::tree_diff_status::Status::Added.into(), - TreeDiffStatus::Modified { .. } => { - proto::tree_diff_status::Status::Modified.into() - } - TreeDiffStatus::Deleted { .. } => { - proto::tree_diff_status::Status::Deleted.into() - } - }, - oid: match status { - TreeDiffStatus::Deleted { old } | TreeDiffStatus::Modified { old } => { - Some(old.to_string()) - } - TreeDiffStatus::Added => None, - }, - }) - .collect(), - }) - } - - async fn handle_get_blob_content( - this: Entity, - request: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let oid = git::Oid::from_str(&request.payload.oid)?; - let repository_id = RepositoryId(request.payload.repository_id); - let content = this - .update(&mut cx, |this, cx| { - let repository = this.repositories().get(&repository_id)?; - Some(repository.update(cx, |repo, cx| repo.load_blob_content(oid, cx))) - })? - .context("missing repository")? - .await?; - Ok(proto::GetBlobContentResponse { content }) - } - - async fn handle_open_unstaged_diff( - this: Entity, - request: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let buffer_id = BufferId::new(request.payload.buffer_id)?; - let diff = this - .update(&mut cx, |this, cx| { - let buffer = this.buffer_store.read(cx).get(buffer_id)?; - Some(this.open_unstaged_diff(buffer, cx)) - })? - .context("missing buffer")? - .await?; - this.update(&mut cx, |this, _| { - let shared_diffs = this - .shared_diffs - .entry(request.original_sender_id.unwrap_or(request.sender_id)) - .or_default(); - shared_diffs.entry(buffer_id).or_default().unstaged = Some(diff.clone()); - })?; - let staged_text = diff.read_with(&cx, |diff, _| diff.base_text_string())?; - Ok(proto::OpenUnstagedDiffResponse { staged_text }) - } - - async fn handle_open_uncommitted_diff( - this: Entity, - request: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let buffer_id = BufferId::new(request.payload.buffer_id)?; - let diff = this - .update(&mut cx, |this, cx| { - let buffer = this.buffer_store.read(cx).get(buffer_id)?; - Some(this.open_uncommitted_diff(buffer, cx)) - })? - .context("missing buffer")? - .await?; - this.update(&mut cx, |this, _| { - let shared_diffs = this - .shared_diffs - .entry(request.original_sender_id.unwrap_or(request.sender_id)) - .or_default(); - shared_diffs.entry(buffer_id).or_default().uncommitted = Some(diff.clone()); - })?; - diff.read_with(&cx, |diff, cx| { - use proto::open_uncommitted_diff_response::Mode; - - let unstaged_diff = diff.secondary_diff(); - let index_snapshot = unstaged_diff.and_then(|diff| { - let diff = diff.read(cx); - diff.base_text_exists().then(|| diff.base_text()) - }); - - let mode; - let staged_text; - let committed_text; - if diff.base_text_exists() { - let committed_snapshot = diff.base_text(); - committed_text = Some(committed_snapshot.text()); - if let Some(index_text) = index_snapshot { - if index_text.remote_id() == committed_snapshot.remote_id() { - mode = Mode::IndexMatchesHead; - staged_text = None; - } else { - mode = Mode::IndexAndHead; - staged_text = Some(index_text.text()); - } - } else { - mode = Mode::IndexAndHead; - staged_text = None; - } - } else { - mode = Mode::IndexAndHead; - committed_text = None; - staged_text = index_snapshot.as_ref().map(|buffer| buffer.text()); - } - - proto::OpenUncommittedDiffResponse { - committed_text, - staged_text, - mode: mode.into(), - } - }) - } - - async fn handle_update_diff_bases( - this: Entity, - request: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - let buffer_id = BufferId::new(request.payload.buffer_id)?; - this.update(&mut cx, |this, cx| { - if let Some(diff_state) = this.diffs.get_mut(&buffer_id) - && let Some(buffer) = this.buffer_store.read(cx).get(buffer_id) - { - let buffer = buffer.read(cx).text_snapshot(); - diff_state.update(cx, |diff_state, cx| { - diff_state.handle_base_texts_updated(buffer, request.payload, cx); - }) - } - }) - } - - async fn handle_blame_buffer( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - let version = deserialize_version(&envelope.payload.version); - let buffer = this.read_with(&cx, |this, cx| { - this.buffer_store.read(cx).get_existing(buffer_id) - })??; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(version.clone()) - })? - .await?; - let blame = this - .update(&mut cx, |this, cx| { - this.blame_buffer(&buffer, Some(version), cx) - })? - .await?; - Ok(serialize_blame_buffer_response(blame)) - } - - async fn handle_get_permalink_to_line( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - // let version = deserialize_version(&envelope.payload.version); - let selection = { - let proto_selection = envelope - .payload - .selection - .context("no selection to get permalink for defined")?; - proto_selection.start as u32..proto_selection.end as u32 - }; - let buffer = this.read_with(&cx, |this, cx| { - this.buffer_store.read(cx).get_existing(buffer_id) - })??; - let permalink = this - .update(&mut cx, |this, cx| { - this.get_permalink_to_line(&buffer, selection, cx) - })? - .await?; - Ok(proto::GetPermalinkToLineResponse { - permalink: permalink.to_string(), - }) - } - - fn repository_for_request( - this: &Entity, - id: RepositoryId, - cx: &mut AsyncApp, - ) -> Result> { - this.read_with(cx, |this, _| { - this.repositories - .get(&id) - .context("missing repository handle") - .cloned() - })? - } - - pub fn repo_snapshots(&self, cx: &App) -> HashMap { - self.repositories - .iter() - .map(|(id, repo)| (*id, repo.read(cx).snapshot.clone())) - .collect() - } - - fn process_updated_entries( - &self, - worktree: &Entity, - updated_entries: &[(Arc, ProjectEntryId, PathChange)], - cx: &mut App, - ) -> Task, Vec>> { - let path_style = worktree.read(cx).path_style(); - let mut repo_paths = self - .repositories - .values() - .map(|repo| (repo.read(cx).work_directory_abs_path.clone(), repo.clone())) - .collect::>(); - let mut entries: Vec<_> = updated_entries - .iter() - .map(|(path, _, _)| path.clone()) - .collect(); - entries.sort(); - let worktree = worktree.read(cx); - - let entries = entries - .into_iter() - .map(|path| worktree.absolutize(&path)) - .collect::>(); - - let executor = cx.background_executor().clone(); - cx.background_executor().spawn(async move { - repo_paths.sort_by(|lhs, rhs| lhs.0.cmp(&rhs.0)); - let mut paths_by_git_repo = HashMap::<_, Vec<_>>::default(); - let mut tasks = FuturesOrdered::new(); - for (repo_path, repo) in repo_paths.into_iter().rev() { - let entries = entries.clone(); - let task = executor.spawn(async move { - // Find all repository paths that belong to this repo - let mut ix = entries.partition_point(|path| path < &*repo_path); - if ix == entries.len() { - return None; - }; - - let mut paths = Vec::new(); - // All paths prefixed by a given repo will constitute a continuous range. - while let Some(path) = entries.get(ix) - && let Some(repo_path) = RepositorySnapshot::abs_path_to_repo_path_inner( - &repo_path, path, path_style, - ) - { - paths.push((repo_path, ix)); - ix += 1; - } - if paths.is_empty() { - None - } else { - Some((repo, paths)) - } - }); - tasks.push_back(task); - } - - // Now, let's filter out the "duplicate" entries that were processed by multiple distinct repos. - let mut path_was_used = vec![false; entries.len()]; - let tasks = tasks.collect::>().await; - // Process tasks from the back: iterating backwards allows us to see more-specific paths first. - // We always want to assign a path to it's innermost repository. - for t in tasks { - let Some((repo, paths)) = t else { - continue; - }; - let entry = paths_by_git_repo.entry(repo).or_default(); - for (repo_path, ix) in paths { - if path_was_used[ix] { - continue; - } - path_was_used[ix] = true; - entry.push(repo_path); - } - } - - paths_by_git_repo - }) - } -} - -impl BufferGitState { - fn new(_git_store: WeakEntity) -> Self { - Self { - unstaged_diff: Default::default(), - uncommitted_diff: Default::default(), - recalculate_diff_task: Default::default(), - language: Default::default(), - language_registry: Default::default(), - recalculating_tx: postage::watch::channel_with(false).0, - hunk_staging_operation_count: 0, - hunk_staging_operation_count_as_of_write: 0, - head_text: Default::default(), - index_text: Default::default(), - head_changed: Default::default(), - index_changed: Default::default(), - language_changed: Default::default(), - conflict_updated_futures: Default::default(), - conflict_set: Default::default(), - reparse_conflict_markers_task: Default::default(), - } - } - - fn buffer_language_changed(&mut self, buffer: Entity, cx: &mut Context) { - self.language = buffer.read(cx).language().cloned(); - self.language_changed = true; - let _ = self.recalculate_diffs(buffer.read(cx).text_snapshot(), cx); - } - - fn reparse_conflict_markers( - &mut self, - buffer: text::BufferSnapshot, - cx: &mut Context, - ) -> oneshot::Receiver<()> { - let (tx, rx) = oneshot::channel(); - - let Some(conflict_set) = self - .conflict_set - .as_ref() - .and_then(|conflict_set| conflict_set.upgrade()) - else { - return rx; - }; - - let old_snapshot = conflict_set.read_with(cx, |conflict_set, _| { - if conflict_set.has_conflict { - Some(conflict_set.snapshot()) - } else { - None - } - }); - - if let Some(old_snapshot) = old_snapshot { - self.conflict_updated_futures.push(tx); - self.reparse_conflict_markers_task = Some(cx.spawn(async move |this, cx| { - let (snapshot, changed_range) = cx - .background_spawn(async move { - let new_snapshot = ConflictSet::parse(&buffer); - let changed_range = old_snapshot.compare(&new_snapshot, &buffer); - (new_snapshot, changed_range) - }) - .await; - this.update(cx, |this, cx| { - if let Some(conflict_set) = &this.conflict_set { - conflict_set - .update(cx, |conflict_set, cx| { - conflict_set.set_snapshot(snapshot, changed_range, cx); - }) - .ok(); - } - let futures = std::mem::take(&mut this.conflict_updated_futures); - for tx in futures { - tx.send(()).ok(); - } - }) - })) - } - - rx - } - - fn unstaged_diff(&self) -> Option> { - self.unstaged_diff.as_ref().and_then(|set| set.upgrade()) - } - - fn uncommitted_diff(&self) -> Option> { - self.uncommitted_diff.as_ref().and_then(|set| set.upgrade()) - } - - fn handle_base_texts_updated( - &mut self, - buffer: text::BufferSnapshot, - message: proto::UpdateDiffBases, - cx: &mut Context, - ) { - use proto::update_diff_bases::Mode; - - let Some(mode) = Mode::from_i32(message.mode) else { - return; - }; - - let diff_bases_change = match mode { - Mode::HeadOnly => DiffBasesChange::SetHead(message.committed_text), - Mode::IndexOnly => DiffBasesChange::SetIndex(message.staged_text), - Mode::IndexMatchesHead => DiffBasesChange::SetBoth(message.committed_text), - Mode::IndexAndHead => DiffBasesChange::SetEach { - index: message.staged_text, - head: message.committed_text, - }, - }; - - self.diff_bases_changed(buffer, Some(diff_bases_change), cx); - } - - pub fn wait_for_recalculation(&mut self) -> Option + use<>> { - if *self.recalculating_tx.borrow() { - let mut rx = self.recalculating_tx.subscribe(); - Some(async move { - loop { - let is_recalculating = rx.recv().await; - if is_recalculating != Some(true) { - break; - } - } - }) - } else { - None - } - } - - fn diff_bases_changed( - &mut self, - buffer: text::BufferSnapshot, - diff_bases_change: Option, - cx: &mut Context, - ) { - match diff_bases_change { - Some(DiffBasesChange::SetIndex(index)) => { - self.index_text = index.map(|mut index| { - text::LineEnding::normalize(&mut index); - Arc::new(index) - }); - self.index_changed = true; - } - Some(DiffBasesChange::SetHead(head)) => { - self.head_text = head.map(|mut head| { - text::LineEnding::normalize(&mut head); - Arc::new(head) - }); - self.head_changed = true; - } - Some(DiffBasesChange::SetBoth(text)) => { - let text = text.map(|mut text| { - text::LineEnding::normalize(&mut text); - Arc::new(text) - }); - self.head_text = text.clone(); - self.index_text = text; - self.head_changed = true; - self.index_changed = true; - } - Some(DiffBasesChange::SetEach { index, head }) => { - self.index_text = index.map(|mut index| { - text::LineEnding::normalize(&mut index); - Arc::new(index) - }); - self.index_changed = true; - self.head_text = head.map(|mut head| { - text::LineEnding::normalize(&mut head); - Arc::new(head) - }); - self.head_changed = true; - } - None => {} - } - - self.recalculate_diffs(buffer, cx) - } - - fn recalculate_diffs(&mut self, buffer: text::BufferSnapshot, cx: &mut Context) { - *self.recalculating_tx.borrow_mut() = true; - - let language = self.language.clone(); - let language_registry = self.language_registry.clone(); - let unstaged_diff = self.unstaged_diff(); - let uncommitted_diff = self.uncommitted_diff(); - let head = self.head_text.clone(); - let index = self.index_text.clone(); - let index_changed = self.index_changed; - let head_changed = self.head_changed; - let language_changed = self.language_changed; - let prev_hunk_staging_operation_count = self.hunk_staging_operation_count_as_of_write; - let index_matches_head = match (self.index_text.as_ref(), self.head_text.as_ref()) { - (Some(index), Some(head)) => Arc::ptr_eq(index, head), - (None, None) => true, - _ => false, - }; - self.recalculate_diff_task = Some(cx.spawn(async move |this, cx| { - log::debug!( - "start recalculating diffs for buffer {}", - buffer.remote_id() - ); - - let mut new_unstaged_diff = None; - if let Some(unstaged_diff) = &unstaged_diff { - new_unstaged_diff = Some( - BufferDiff::update_diff( - unstaged_diff.clone(), - buffer.clone(), - index, - index_changed, - language_changed, - language.clone(), - language_registry.clone(), - cx, - ) - .await?, - ); - } - - // Dropping BufferDiff can be expensive, so yield back to the event loop - // for a bit - yield_now().await; - - let mut new_uncommitted_diff = None; - if let Some(uncommitted_diff) = &uncommitted_diff { - new_uncommitted_diff = if index_matches_head { - new_unstaged_diff.clone() - } else { - Some( - BufferDiff::update_diff( - uncommitted_diff.clone(), - buffer.clone(), - head, - head_changed, - language_changed, - language.clone(), - language_registry.clone(), - cx, - ) - .await?, - ) - } - } - - // Dropping BufferDiff can be expensive, so yield back to the event loop - // for a bit - yield_now().await; - - let cancel = this.update(cx, |this, _| { - // This checks whether all pending stage/unstage operations - // have quiesced (i.e. both the corresponding write and the - // read of that write have completed). If not, then we cancel - // this recalculation attempt to avoid invalidating pending - // state too quickly; another recalculation will come along - // later and clear the pending state once the state of the index has settled. - if this.hunk_staging_operation_count > prev_hunk_staging_operation_count { - *this.recalculating_tx.borrow_mut() = false; - true - } else { - false - } - })?; - if cancel { - log::debug!( - concat!( - "aborting recalculating diffs for buffer {}", - "due to subsequent hunk operations", - ), - buffer.remote_id() - ); - return Ok(()); - } - - let unstaged_changed_range = if let Some((unstaged_diff, new_unstaged_diff)) = - unstaged_diff.as_ref().zip(new_unstaged_diff.clone()) - { - unstaged_diff.update(cx, |diff, cx| { - if language_changed { - diff.language_changed(cx); - } - diff.set_snapshot(new_unstaged_diff, &buffer, cx) - })? - } else { - None - }; - - yield_now().await; - - if let Some((uncommitted_diff, new_uncommitted_diff)) = - uncommitted_diff.as_ref().zip(new_uncommitted_diff.clone()) - { - uncommitted_diff.update(cx, |diff, cx| { - if language_changed { - diff.language_changed(cx); - } - diff.set_snapshot_with_secondary( - new_uncommitted_diff, - &buffer, - unstaged_changed_range, - true, - cx, - ); - })?; - } - - log::debug!( - "finished recalculating diffs for buffer {}", - buffer.remote_id() - ); - - if let Some(this) = this.upgrade() { - this.update(cx, |this, _| { - this.index_changed = false; - this.head_changed = false; - this.language_changed = false; - *this.recalculating_tx.borrow_mut() = false; - })?; - } - - Ok(()) - })); - } -} - -fn make_remote_delegate( - this: Entity, - project_id: u64, - repository_id: RepositoryId, - askpass_id: u64, - cx: &mut AsyncApp, -) -> AskPassDelegate { - AskPassDelegate::new(cx, move |prompt, tx, cx| { - this.update(cx, |this, cx| { - let Some((client, _)) = this.downstream_client() else { - return; - }; - let response = client.request(proto::AskPassRequest { - project_id, - repository_id: repository_id.to_proto(), - askpass_id, - prompt, - }); - cx.spawn(async move |_, _| { - let mut response = response.await?.response; - tx.send(EncryptedPassword::try_from(response.as_ref())?) - .ok(); - response.zeroize(); - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - }) - .log_err(); - }) -} - -impl RepositoryId { - pub fn to_proto(self) -> u64 { - self.0 - } - - pub fn from_proto(id: u64) -> Self { - RepositoryId(id) - } -} - -impl RepositorySnapshot { - fn empty(id: RepositoryId, work_directory_abs_path: Arc, path_style: PathStyle) -> Self { - Self { - id, - statuses_by_path: Default::default(), - work_directory_abs_path, - branch: None, - head_commit: None, - scan_id: 0, - merge: Default::default(), - remote_origin_url: None, - remote_upstream_url: None, - stash_entries: Default::default(), - path_style, - } - } - - fn initial_update(&self, project_id: u64) -> proto::UpdateRepository { - proto::UpdateRepository { - branch_summary: self.branch.as_ref().map(branch_to_proto), - head_commit_details: self.head_commit.as_ref().map(commit_details_to_proto), - updated_statuses: self - .statuses_by_path - .iter() - .map(|entry| entry.to_proto()) - .collect(), - removed_statuses: Default::default(), - current_merge_conflicts: self - .merge - .conflicted_paths - .iter() - .map(|repo_path| repo_path.to_proto()) - .collect(), - merge_message: self.merge.message.as_ref().map(|msg| msg.to_string()), - project_id, - id: self.id.to_proto(), - abs_path: self.work_directory_abs_path.to_string_lossy().into_owned(), - entry_ids: vec![self.id.to_proto()], - scan_id: self.scan_id, - is_last_update: true, - stash_entries: self - .stash_entries - .entries - .iter() - .map(stash_to_proto) - .collect(), - remote_upstream_url: self.remote_upstream_url.clone(), - remote_origin_url: self.remote_origin_url.clone(), - } - } - - fn build_update(&self, old: &Self, project_id: u64) -> proto::UpdateRepository { - let mut updated_statuses: Vec = Vec::new(); - let mut removed_statuses: Vec = Vec::new(); - - let mut new_statuses = self.statuses_by_path.iter().peekable(); - let mut old_statuses = old.statuses_by_path.iter().peekable(); - - let mut current_new_entry = new_statuses.next(); - let mut current_old_entry = old_statuses.next(); - loop { - match (current_new_entry, current_old_entry) { - (Some(new_entry), Some(old_entry)) => { - match new_entry.repo_path.cmp(&old_entry.repo_path) { - Ordering::Less => { - updated_statuses.push(new_entry.to_proto()); - current_new_entry = new_statuses.next(); - } - Ordering::Equal => { - if new_entry.status != old_entry.status { - updated_statuses.push(new_entry.to_proto()); - } - current_old_entry = old_statuses.next(); - current_new_entry = new_statuses.next(); - } - Ordering::Greater => { - removed_statuses.push(old_entry.repo_path.to_proto()); - current_old_entry = old_statuses.next(); - } - } - } - (None, Some(old_entry)) => { - removed_statuses.push(old_entry.repo_path.to_proto()); - current_old_entry = old_statuses.next(); - } - (Some(new_entry), None) => { - updated_statuses.push(new_entry.to_proto()); - current_new_entry = new_statuses.next(); - } - (None, None) => break, - } - } - - proto::UpdateRepository { - branch_summary: self.branch.as_ref().map(branch_to_proto), - head_commit_details: self.head_commit.as_ref().map(commit_details_to_proto), - updated_statuses, - removed_statuses, - current_merge_conflicts: self - .merge - .conflicted_paths - .iter() - .map(|path| path.to_proto()) - .collect(), - merge_message: self.merge.message.as_ref().map(|msg| msg.to_string()), - project_id, - id: self.id.to_proto(), - abs_path: self.work_directory_abs_path.to_string_lossy().into_owned(), - entry_ids: vec![], - scan_id: self.scan_id, - is_last_update: true, - stash_entries: self - .stash_entries - .entries - .iter() - .map(stash_to_proto) - .collect(), - remote_upstream_url: self.remote_upstream_url.clone(), - remote_origin_url: self.remote_origin_url.clone(), - } - } - - pub fn status(&self) -> impl Iterator + '_ { - self.statuses_by_path.iter().cloned() - } - - pub fn status_summary(&self) -> GitSummary { - self.statuses_by_path.summary().item_summary - } - - pub fn status_for_path(&self, path: &RepoPath) -> Option { - self.statuses_by_path - .get(&PathKey(path.as_ref().clone()), ()) - .cloned() - } - - pub fn abs_path_to_repo_path(&self, abs_path: &Path) -> Option { - Self::abs_path_to_repo_path_inner(&self.work_directory_abs_path, abs_path, self.path_style) - } - - fn repo_path_to_abs_path(&self, repo_path: &RepoPath) -> PathBuf { - self.path_style - .join(&self.work_directory_abs_path, repo_path.as_std_path()) - .unwrap() - .into() - } - - #[inline] - fn abs_path_to_repo_path_inner( - work_directory_abs_path: &Path, - abs_path: &Path, - path_style: PathStyle, - ) -> Option { - let rel_path = path_style.strip_prefix(abs_path, work_directory_abs_path)?; - Some(RepoPath::from_rel_path(&rel_path)) - } - - pub fn had_conflict_on_last_merge_head_change(&self, repo_path: &RepoPath) -> bool { - self.merge.conflicted_paths.contains(repo_path) - } - - pub fn has_conflict(&self, repo_path: &RepoPath) -> bool { - let had_conflict_on_last_merge_head_change = - self.merge.conflicted_paths.contains(repo_path); - let has_conflict_currently = self - .status_for_path(repo_path) - .is_some_and(|entry| entry.status.is_conflicted()); - had_conflict_on_last_merge_head_change || has_conflict_currently - } - - /// This is the name that will be displayed in the repository selector for this repository. - pub fn display_name(&self) -> SharedString { - self.work_directory_abs_path - .file_name() - .unwrap_or_default() - .to_string_lossy() - .to_string() - .into() - } -} - -pub fn stash_to_proto(entry: &StashEntry) -> proto::StashEntry { - proto::StashEntry { - oid: entry.oid.as_bytes().to_vec(), - message: entry.message.clone(), - branch: entry.branch.clone(), - index: entry.index as u64, - timestamp: entry.timestamp, - } -} - -pub fn proto_to_stash(entry: &proto::StashEntry) -> Result { - Ok(StashEntry { - oid: Oid::from_bytes(&entry.oid)?, - message: entry.message.clone(), - index: entry.index as usize, - branch: entry.branch.clone(), - timestamp: entry.timestamp, - }) -} - -impl MergeDetails { - async fn load( - backend: &Arc, - status: &SumTree, - prev_snapshot: &RepositorySnapshot, - ) -> Result<(MergeDetails, bool)> { - log::debug!("load merge details"); - let message = backend.merge_message().await; - let heads = backend - .revparse_batch(vec![ - "MERGE_HEAD".into(), - "CHERRY_PICK_HEAD".into(), - "REBASE_HEAD".into(), - "REVERT_HEAD".into(), - "APPLY_HEAD".into(), - ]) - .await - .log_err() - .unwrap_or_default() - .into_iter() - .map(|opt| opt.map(SharedString::from)) - .collect::>(); - let merge_heads_changed = heads != prev_snapshot.merge.heads; - let conflicted_paths = if merge_heads_changed { - let current_conflicted_paths = TreeSet::from_ordered_entries( - status - .iter() - .filter(|entry| entry.status.is_conflicted()) - .map(|entry| entry.repo_path.clone()), - ); - - // It can happen that we run a scan while a lengthy merge is in progress - // that will eventually result in conflicts, but before those conflicts - // are reported by `git status`. Since for the moment we only care about - // the merge heads state for the purposes of tracking conflicts, don't update - // this state until we see some conflicts. - if heads.iter().any(Option::is_some) - && !prev_snapshot.merge.heads.iter().any(Option::is_some) - && current_conflicted_paths.is_empty() - { - log::debug!("not updating merge heads because no conflicts found"); - return Ok(( - MergeDetails { - message: message.map(SharedString::from), - ..prev_snapshot.merge.clone() - }, - false, - )); - } - - current_conflicted_paths - } else { - prev_snapshot.merge.conflicted_paths.clone() - }; - let details = MergeDetails { - conflicted_paths, - message: message.map(SharedString::from), - heads, - }; - Ok((details, merge_heads_changed)) - } -} - -impl Repository { - pub fn snapshot(&self) -> RepositorySnapshot { - self.snapshot.clone() - } - - pub fn pending_ops(&self) -> impl Iterator + '_ { - self.pending_ops.iter().cloned() - } - - pub fn pending_ops_summary(&self) -> PathSummary { - self.pending_ops.summary().clone() - } - - pub fn pending_ops_for_path(&self, path: &RepoPath) -> Option { - self.pending_ops - .get(&PathKey(path.as_ref().clone()), ()) - .cloned() - } - - fn local( - id: RepositoryId, - work_directory_abs_path: Arc, - dot_git_abs_path: Arc, - project_environment: WeakEntity, - fs: Arc, - git_store: WeakEntity, - cx: &mut Context, - ) -> Self { - let snapshot = - RepositorySnapshot::empty(id, work_directory_abs_path.clone(), PathStyle::local()); - let state = cx - .spawn(async move |_, cx| { - LocalRepositoryState::new( - work_directory_abs_path, - dot_git_abs_path, - project_environment, - fs, - cx, - ) - .await - .map_err(|err| err.to_string()) - }) - .shared(); - let job_sender = Repository::spawn_local_git_worker(state.clone(), cx); - let state = cx - .spawn(async move |_, _| { - let state = state.await?; - Ok(RepositoryState::Local(state)) - }) - .shared(); - - Repository { - this: cx.weak_entity(), - git_store, - snapshot, - pending_ops: Default::default(), - repository_state: state, - commit_message_buffer: None, - askpass_delegates: Default::default(), - paths_needing_status_update: Default::default(), - latest_askpass_id: 0, - job_sender, - job_id: 0, - active_jobs: Default::default(), - } - } - - fn remote( - id: RepositoryId, - work_directory_abs_path: Arc, - path_style: PathStyle, - project_id: ProjectId, - client: AnyProtoClient, - git_store: WeakEntity, - cx: &mut Context, - ) -> Self { - let snapshot = RepositorySnapshot::empty(id, work_directory_abs_path, path_style); - let repository_state = RemoteRepositoryState { project_id, client }; - let job_sender = Self::spawn_remote_git_worker(repository_state.clone(), cx); - let repository_state = Task::ready(Ok(RepositoryState::Remote(repository_state))).shared(); - Self { - this: cx.weak_entity(), - snapshot, - commit_message_buffer: None, - git_store, - pending_ops: Default::default(), - paths_needing_status_update: Default::default(), - job_sender, - repository_state, - askpass_delegates: Default::default(), - latest_askpass_id: 0, - active_jobs: Default::default(), - job_id: 0, - } - } - - pub fn git_store(&self) -> Option> { - self.git_store.upgrade() - } - - fn reload_buffer_diff_bases(&mut self, cx: &mut Context) { - let this = cx.weak_entity(); - let git_store = self.git_store.clone(); - let _ = self.send_keyed_job( - Some(GitJobKey::ReloadBufferDiffBases), - None, - |state, mut cx| async move { - let RepositoryState::Local(LocalRepositoryState { backend, .. }) = state else { - log::error!("tried to recompute diffs for a non-local repository"); - return Ok(()); - }; - - let Some(this) = this.upgrade() else { - return Ok(()); - }; - - let repo_diff_state_updates = this.update(&mut cx, |this, cx| { - git_store.update(cx, |git_store, cx| { - git_store - .diffs - .iter() - .filter_map(|(buffer_id, diff_state)| { - let buffer_store = git_store.buffer_store.read(cx); - let buffer = buffer_store.get(*buffer_id)?; - let file = File::from_dyn(buffer.read(cx).file())?; - let abs_path = file.worktree.read(cx).absolutize(&file.path); - let repo_path = this.abs_path_to_repo_path(&abs_path)?; - log::debug!( - "start reload diff bases for repo path {}", - repo_path.as_unix_str() - ); - diff_state.update(cx, |diff_state, _| { - let has_unstaged_diff = diff_state - .unstaged_diff - .as_ref() - .is_some_and(|diff| diff.is_upgradable()); - let has_uncommitted_diff = diff_state - .uncommitted_diff - .as_ref() - .is_some_and(|set| set.is_upgradable()); - - Some(( - buffer, - repo_path, - has_unstaged_diff.then(|| diff_state.index_text.clone()), - has_uncommitted_diff.then(|| diff_state.head_text.clone()), - )) - }) - }) - .collect::>() - }) - })??; - - let buffer_diff_base_changes = cx - .background_spawn(async move { - let mut changes = Vec::new(); - for (buffer, repo_path, current_index_text, current_head_text) in - &repo_diff_state_updates - { - let index_text = if current_index_text.is_some() { - backend.load_index_text(repo_path.clone()).await - } else { - None - }; - let head_text = if current_head_text.is_some() { - backend.load_committed_text(repo_path.clone()).await - } else { - None - }; - - let change = - match (current_index_text.as_ref(), current_head_text.as_ref()) { - (Some(current_index), Some(current_head)) => { - let index_changed = - index_text.as_ref() != current_index.as_deref(); - let head_changed = - head_text.as_ref() != current_head.as_deref(); - if index_changed && head_changed { - if index_text == head_text { - Some(DiffBasesChange::SetBoth(head_text)) - } else { - Some(DiffBasesChange::SetEach { - index: index_text, - head: head_text, - }) - } - } else if index_changed { - Some(DiffBasesChange::SetIndex(index_text)) - } else if head_changed { - Some(DiffBasesChange::SetHead(head_text)) - } else { - None - } - } - (Some(current_index), None) => { - let index_changed = - index_text.as_ref() != current_index.as_deref(); - index_changed - .then_some(DiffBasesChange::SetIndex(index_text)) - } - (None, Some(current_head)) => { - let head_changed = - head_text.as_ref() != current_head.as_deref(); - head_changed.then_some(DiffBasesChange::SetHead(head_text)) - } - (None, None) => None, - }; - - changes.push((buffer.clone(), change)) - } - changes - }) - .await; - - git_store.update(&mut cx, |git_store, cx| { - for (buffer, diff_bases_change) in buffer_diff_base_changes { - let buffer_snapshot = buffer.read(cx).text_snapshot(); - let buffer_id = buffer_snapshot.remote_id(); - let Some(diff_state) = git_store.diffs.get(&buffer_id) else { - continue; - }; - - let downstream_client = git_store.downstream_client(); - diff_state.update(cx, |diff_state, cx| { - use proto::update_diff_bases::Mode; - - if let Some((diff_bases_change, (client, project_id))) = - diff_bases_change.clone().zip(downstream_client) - { - let (staged_text, committed_text, mode) = match diff_bases_change { - DiffBasesChange::SetIndex(index) => { - (index, None, Mode::IndexOnly) - } - DiffBasesChange::SetHead(head) => (None, head, Mode::HeadOnly), - DiffBasesChange::SetEach { index, head } => { - (index, head, Mode::IndexAndHead) - } - DiffBasesChange::SetBoth(text) => { - (None, text, Mode::IndexMatchesHead) - } - }; - client - .send(proto::UpdateDiffBases { - project_id: project_id.to_proto(), - buffer_id: buffer_id.to_proto(), - staged_text, - committed_text, - mode: mode as i32, - }) - .log_err(); - } - - diff_state.diff_bases_changed(buffer_snapshot, diff_bases_change, cx); - }); - } - }) - }, - ); - } - - pub fn send_job( - &mut self, - status: Option, - job: F, - ) -> oneshot::Receiver - where - F: FnOnce(RepositoryState, AsyncApp) -> Fut + 'static, - Fut: Future + 'static, - R: Send + 'static, - { - self.send_keyed_job(None, status, job) - } - - fn send_keyed_job( - &mut self, - key: Option, - status: Option, - job: F, - ) -> oneshot::Receiver - where - F: FnOnce(RepositoryState, AsyncApp) -> Fut + 'static, - Fut: Future + 'static, - R: Send + 'static, - { - let (result_tx, result_rx) = futures::channel::oneshot::channel(); - let job_id = post_inc(&mut self.job_id); - let this = self.this.clone(); - self.job_sender - .unbounded_send(GitJob { - key, - job: Box::new(move |state, cx: &mut AsyncApp| { - let job = job(state, cx.clone()); - cx.spawn(async move |cx| { - if let Some(s) = status.clone() { - this.update(cx, |this, cx| { - this.active_jobs.insert( - job_id, - JobInfo { - start: Instant::now(), - message: s.clone(), - }, - ); - - cx.notify(); - }) - .ok(); - } - let result = job.await; - - this.update(cx, |this, cx| { - this.active_jobs.remove(&job_id); - cx.notify(); - }) - .ok(); - - result_tx.send(result).ok(); - }) - }), - }) - .ok(); - result_rx - } - - pub fn set_as_active_repository(&self, cx: &mut Context) { - let Some(git_store) = self.git_store.upgrade() else { - return; - }; - let entity = cx.entity(); - git_store.update(cx, |git_store, cx| { - let Some((&id, _)) = git_store - .repositories - .iter() - .find(|(_, handle)| *handle == &entity) - else { - return; - }; - git_store.active_repo_id = Some(id); - cx.emit(GitStoreEvent::ActiveRepositoryChanged(Some(id))); - }); - } - - pub fn cached_status(&self) -> impl '_ + Iterator { - self.snapshot.status() - } - - pub fn cached_stash(&self) -> GitStash { - self.snapshot.stash_entries.clone() - } - - pub fn repo_path_to_project_path(&self, path: &RepoPath, cx: &App) -> Option { - let git_store = self.git_store.upgrade()?; - let worktree_store = git_store.read(cx).worktree_store.read(cx); - let abs_path = self.snapshot.repo_path_to_abs_path(path); - let abs_path = SanitizedPath::new(&abs_path); - let (worktree, relative_path) = worktree_store.find_worktree(abs_path, cx)?; - Some(ProjectPath { - worktree_id: worktree.read(cx).id(), - path: relative_path, - }) - } - - pub fn project_path_to_repo_path(&self, path: &ProjectPath, cx: &App) -> Option { - let git_store = self.git_store.upgrade()?; - let worktree_store = git_store.read(cx).worktree_store.read(cx); - let abs_path = worktree_store.absolutize(path, cx)?; - self.snapshot.abs_path_to_repo_path(&abs_path) - } - - pub fn contains_sub_repo(&self, other: &Entity, cx: &App) -> bool { - other - .read(cx) - .snapshot - .work_directory_abs_path - .starts_with(&self.snapshot.work_directory_abs_path) - } - - pub fn open_commit_buffer( - &mut self, - languages: Option>, - buffer_store: Entity, - cx: &mut Context, - ) -> Task>> { - let id = self.id; - if let Some(buffer) = self.commit_message_buffer.clone() { - return Task::ready(Ok(buffer)); - } - let this = cx.weak_entity(); - - let rx = self.send_job(None, move |state, mut cx| async move { - let Some(this) = this.upgrade() else { - bail!("git store was dropped"); - }; - match state { - RepositoryState::Local(..) => { - this.update(&mut cx, |_, cx| { - Self::open_local_commit_buffer(languages, buffer_store, cx) - })? - .await - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - let request = client.request(proto::OpenCommitMessageBuffer { - project_id: project_id.0, - repository_id: id.to_proto(), - }); - let response = request.await.context("requesting to open commit buffer")?; - let buffer_id = BufferId::new(response.buffer_id)?; - let buffer = buffer_store - .update(&mut cx, |buffer_store, cx| { - buffer_store.wait_for_remote_buffer(buffer_id, cx) - })? - .await?; - if let Some(language_registry) = languages { - let git_commit_language = - language_registry.language_for_name("Git Commit").await?; - buffer.update(&mut cx, |buffer, cx| { - buffer.set_language(Some(git_commit_language), cx); - })?; - } - this.update(&mut cx, |this, _| { - this.commit_message_buffer = Some(buffer.clone()); - })?; - Ok(buffer) - } - } - }); - - cx.spawn(|_, _: &mut AsyncApp| async move { rx.await? }) - } - - fn open_local_commit_buffer( - language_registry: Option>, - buffer_store: Entity, - cx: &mut Context, - ) -> Task>> { - cx.spawn(async move |repository, cx| { - let buffer = buffer_store - .update(cx, |buffer_store, cx| buffer_store.create_buffer(false, cx))? - .await?; - - if let Some(language_registry) = language_registry { - let git_commit_language = language_registry.language_for_name("Git Commit").await?; - buffer.update(cx, |buffer, cx| { - buffer.set_language(Some(git_commit_language), cx); - })?; - } - - repository.update(cx, |repository, _| { - repository.commit_message_buffer = Some(buffer.clone()); - })?; - Ok(buffer) - }) - } - - pub fn checkout_files( - &mut self, - commit: &str, - paths: Vec, - cx: &mut Context, - ) -> Task> { - let commit = commit.to_string(); - let id = self.id; - - self.spawn_job_with_tracking( - paths.clone(), - pending_op::GitStatus::Reverted, - cx, - async move |this, cx| { - this.update(cx, |this, _cx| { - this.send_job( - Some(format!("git checkout {}", commit).into()), - move |git_repo, _| async move { - match git_repo { - RepositoryState::Local(LocalRepositoryState { - backend, - environment, - .. - }) => { - backend - .checkout_files(commit, paths, environment.clone()) - .await - } - RepositoryState::Remote(RemoteRepositoryState { - project_id, - client, - }) => { - client - .request(proto::GitCheckoutFiles { - project_id: project_id.0, - repository_id: id.to_proto(), - commit, - paths: paths - .into_iter() - .map(|p| p.to_proto()) - .collect(), - }) - .await?; - - Ok(()) - } - } - }, - ) - })? - .await? - }, - ) - } - - pub fn reset( - &mut self, - commit: String, - reset_mode: ResetMode, - _cx: &mut App, - ) -> oneshot::Receiver> { - let id = self.id; - - self.send_job(None, move |git_repo, _| async move { - match git_repo { - RepositoryState::Local(LocalRepositoryState { - backend, - environment, - .. - }) => backend.reset(commit, reset_mode, environment).await, - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - client - .request(proto::GitReset { - project_id: project_id.0, - repository_id: id.to_proto(), - commit, - mode: match reset_mode { - ResetMode::Soft => git_reset::ResetMode::Soft.into(), - ResetMode::Mixed => git_reset::ResetMode::Mixed.into(), - }, - }) - .await?; - - Ok(()) - } - } - }) - } - - pub fn show(&mut self, commit: String) -> oneshot::Receiver> { - let id = self.id; - self.send_job(None, move |git_repo, _cx| async move { - match git_repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.show(commit).await - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - let resp = client - .request(proto::GitShow { - project_id: project_id.0, - repository_id: id.to_proto(), - commit, - }) - .await?; - - Ok(CommitDetails { - sha: resp.sha.into(), - message: resp.message.into(), - commit_timestamp: resp.commit_timestamp, - author_email: resp.author_email.into(), - author_name: resp.author_name.into(), - }) - } - } - }) - } - - pub fn load_commit_diff(&mut self, commit: String) -> oneshot::Receiver> { - let id = self.id; - self.send_job(None, move |git_repo, cx| async move { - match git_repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.load_commit(commit, cx).await - } - RepositoryState::Remote(RemoteRepositoryState { - client, project_id, .. - }) => { - let response = client - .request(proto::LoadCommitDiff { - project_id: project_id.0, - repository_id: id.to_proto(), - commit, - }) - .await?; - Ok(CommitDiff { - files: response - .files - .into_iter() - .map(|file| { - Ok(CommitFile { - path: RepoPath::from_proto(&file.path)?, - old_text: file.old_text, - new_text: file.new_text, - }) - }) - .collect::>>()?, - }) - } - } - }) - } - - pub fn file_history( - &mut self, - path: RepoPath, - ) -> oneshot::Receiver> { - self.file_history_paginated(path, 0, None) - } - - pub fn file_history_paginated( - &mut self, - path: RepoPath, - skip: usize, - limit: Option, - ) -> oneshot::Receiver> { - let id = self.id; - self.send_job(None, move |git_repo, _cx| async move { - match git_repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.file_history_paginated(path, skip, limit).await - } - RepositoryState::Remote(RemoteRepositoryState { client, project_id }) => { - let response = client - .request(proto::GitFileHistory { - project_id: project_id.0, - repository_id: id.to_proto(), - path: path.to_proto(), - skip: skip as u64, - limit: limit.map(|l| l as u64), - }) - .await?; - Ok(git::repository::FileHistory { - entries: response - .entries - .into_iter() - .map(|entry| git::repository::FileHistoryEntry { - sha: entry.sha.into(), - subject: entry.subject.into(), - message: entry.message.into(), - commit_timestamp: entry.commit_timestamp, - author_name: entry.author_name.into(), - author_email: entry.author_email.into(), - }) - .collect(), - path: RepoPath::from_proto(&response.path)?, - }) - } - } - }) - } - - fn buffer_store(&self, cx: &App) -> Option> { - Some(self.git_store.upgrade()?.read(cx).buffer_store.clone()) - } - - fn save_buffers<'a>( - &self, - entries: impl IntoIterator, - cx: &mut Context, - ) -> Vec>> { - let mut save_futures = Vec::new(); - if let Some(buffer_store) = self.buffer_store(cx) { - buffer_store.update(cx, |buffer_store, cx| { - for path in entries { - let Some(project_path) = self.repo_path_to_project_path(path, cx) else { - continue; - }; - if let Some(buffer) = buffer_store.get_by_path(&project_path) - && buffer - .read(cx) - .file() - .is_some_and(|file| file.disk_state().exists()) - && buffer.read(cx).has_unsaved_edits() - { - save_futures.push(buffer_store.save_buffer(buffer, cx)); - } - } - }) - } - save_futures - } - - pub fn stage_entries( - &mut self, - entries: Vec, - cx: &mut Context, - ) -> Task> { - if entries.is_empty() { - return Task::ready(Ok(())); - } - let id = self.id; - let save_tasks = self.save_buffers(&entries, cx); - let paths = entries - .iter() - .map(|p| p.as_unix_str()) - .collect::>() - .join(" "); - let status = format!("git add {paths}"); - let job_key = GitJobKey::WriteIndex(entries.clone()); - - self.spawn_job_with_tracking( - entries.clone(), - pending_op::GitStatus::Staged, - cx, - async move |this, cx| { - for save_task in save_tasks { - save_task.await?; - } - - this.update(cx, |this, _| { - this.send_keyed_job( - Some(job_key), - Some(status.into()), - move |git_repo, _cx| async move { - match git_repo { - RepositoryState::Local(LocalRepositoryState { - backend, - environment, - .. - }) => backend.stage_paths(entries, environment.clone()).await, - RepositoryState::Remote(RemoteRepositoryState { - project_id, - client, - }) => { - client - .request(proto::Stage { - project_id: project_id.0, - repository_id: id.to_proto(), - paths: entries - .into_iter() - .map(|repo_path| repo_path.to_proto()) - .collect(), - }) - .await - .context("sending stage request")?; - - Ok(()) - } - } - }, - ) - })? - .await? - }, - ) - } - - pub fn unstage_entries( - &mut self, - entries: Vec, - cx: &mut Context, - ) -> Task> { - if entries.is_empty() { - return Task::ready(Ok(())); - } - let id = self.id; - let save_tasks = self.save_buffers(&entries, cx); - let paths = entries - .iter() - .map(|p| p.as_unix_str()) - .collect::>() - .join(" "); - let status = format!("git reset {paths}"); - let job_key = GitJobKey::WriteIndex(entries.clone()); - - self.spawn_job_with_tracking( - entries.clone(), - pending_op::GitStatus::Unstaged, - cx, - async move |this, cx| { - for save_task in save_tasks { - save_task.await?; - } - - this.update(cx, |this, _| { - this.send_keyed_job( - Some(job_key), - Some(status.into()), - move |git_repo, _cx| async move { - match git_repo { - RepositoryState::Local(LocalRepositoryState { - backend, - environment, - .. - }) => backend.unstage_paths(entries, environment).await, - RepositoryState::Remote(RemoteRepositoryState { - project_id, - client, - }) => { - client - .request(proto::Unstage { - project_id: project_id.0, - repository_id: id.to_proto(), - paths: entries - .into_iter() - .map(|repo_path| repo_path.to_proto()) - .collect(), - }) - .await - .context("sending unstage request")?; - - Ok(()) - } - } - }, - ) - })? - .await? - }, - ) - } - - pub fn stage_all(&mut self, cx: &mut Context) -> Task> { - let to_stage = self - .cached_status() - .filter_map(|entry| { - if let Some(ops) = self.pending_ops_for_path(&entry.repo_path) { - if ops.staging() || ops.staged() { - None - } else { - Some(entry.repo_path) - } - } else if entry.status.staging().is_fully_staged() { - None - } else { - Some(entry.repo_path) - } - }) - .collect(); - self.stage_entries(to_stage, cx) - } - - pub fn unstage_all(&mut self, cx: &mut Context) -> Task> { - let to_unstage = self - .cached_status() - .filter_map(|entry| { - if let Some(ops) = self.pending_ops_for_path(&entry.repo_path) { - if !ops.staging() && !ops.staged() { - None - } else { - Some(entry.repo_path) - } - } else if entry.status.staging().is_fully_unstaged() { - None - } else { - Some(entry.repo_path) - } - }) - .collect(); - self.unstage_entries(to_unstage, cx) - } - - pub fn stash_all(&mut self, cx: &mut Context) -> Task> { - let to_stash = self.cached_status().map(|entry| entry.repo_path).collect(); - - self.stash_entries(to_stash, cx) - } - - pub fn stash_entries( - &mut self, - entries: Vec, - cx: &mut Context, - ) -> Task> { - let id = self.id; - - cx.spawn(async move |this, cx| { - this.update(cx, |this, _| { - this.send_job(None, move |git_repo, _cx| async move { - match git_repo { - RepositoryState::Local(LocalRepositoryState { - backend, - environment, - .. - }) => backend.stash_paths(entries, environment).await, - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - client - .request(proto::Stash { - project_id: project_id.0, - repository_id: id.to_proto(), - paths: entries - .into_iter() - .map(|repo_path| repo_path.to_proto()) - .collect(), - }) - .await - .context("sending stash request")?; - Ok(()) - } - } - }) - })? - .await??; - Ok(()) - }) - } - - pub fn stash_pop( - &mut self, - index: Option, - cx: &mut Context, - ) -> Task> { - let id = self.id; - cx.spawn(async move |this, cx| { - this.update(cx, |this, _| { - this.send_job(None, move |git_repo, _cx| async move { - match git_repo { - RepositoryState::Local(LocalRepositoryState { - backend, - environment, - .. - }) => backend.stash_pop(index, environment).await, - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - client - .request(proto::StashPop { - project_id: project_id.0, - repository_id: id.to_proto(), - stash_index: index.map(|i| i as u64), - }) - .await - .context("sending stash pop request")?; - Ok(()) - } - } - }) - })? - .await??; - Ok(()) - }) - } - - pub fn stash_apply( - &mut self, - index: Option, - cx: &mut Context, - ) -> Task> { - let id = self.id; - cx.spawn(async move |this, cx| { - this.update(cx, |this, _| { - this.send_job(None, move |git_repo, _cx| async move { - match git_repo { - RepositoryState::Local(LocalRepositoryState { - backend, - environment, - .. - }) => backend.stash_apply(index, environment).await, - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - client - .request(proto::StashApply { - project_id: project_id.0, - repository_id: id.to_proto(), - stash_index: index.map(|i| i as u64), - }) - .await - .context("sending stash apply request")?; - Ok(()) - } - } - }) - })? - .await??; - Ok(()) - }) - } - - pub fn stash_drop( - &mut self, - index: Option, - cx: &mut Context, - ) -> oneshot::Receiver> { - let id = self.id; - let updates_tx = self - .git_store() - .and_then(|git_store| match &git_store.read(cx).state { - GitStoreState::Local { downstream, .. } => downstream - .as_ref() - .map(|downstream| downstream.updates_tx.clone()), - _ => None, - }); - let this = cx.weak_entity(); - self.send_job(None, move |git_repo, mut cx| async move { - match git_repo { - RepositoryState::Local(LocalRepositoryState { - backend, - environment, - .. - }) => { - // TODO would be nice to not have to do this manually - let result = backend.stash_drop(index, environment).await; - if result.is_ok() - && let Ok(stash_entries) = backend.stash_entries().await - { - let snapshot = this.update(&mut cx, |this, cx| { - this.snapshot.stash_entries = stash_entries; - cx.emit(RepositoryEvent::StashEntriesChanged); - this.snapshot.clone() - })?; - if let Some(updates_tx) = updates_tx { - updates_tx - .unbounded_send(DownstreamUpdate::UpdateRepository(snapshot)) - .ok(); - } - } - - result - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - client - .request(proto::StashDrop { - project_id: project_id.0, - repository_id: id.to_proto(), - stash_index: index.map(|i| i as u64), - }) - .await - .context("sending stash pop request")?; - Ok(()) - } - } - }) - } - - pub fn run_hook(&mut self, hook: RunHook, _cx: &mut App) -> oneshot::Receiver> { - let id = self.id; - self.send_job( - Some(format!("git hook {}", hook.as_str()).into()), - move |git_repo, _cx| async move { - match git_repo { - RepositoryState::Local(LocalRepositoryState { - backend, - environment, - .. - }) => backend.run_hook(hook, environment.clone()).await, - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - client - .request(proto::RunGitHook { - project_id: project_id.0, - repository_id: id.to_proto(), - hook: hook.to_proto(), - }) - .await?; - - Ok(()) - } - } - }, - ) - } - - pub fn commit( - &mut self, - message: SharedString, - name_and_email: Option<(SharedString, SharedString)>, - options: CommitOptions, - askpass: AskPassDelegate, - cx: &mut App, - ) -> oneshot::Receiver> { - let id = self.id; - let askpass_delegates = self.askpass_delegates.clone(); - let askpass_id = util::post_inc(&mut self.latest_askpass_id); - - let rx = self.run_hook(RunHook::PreCommit, cx); - - self.send_job(Some("git commit".into()), move |git_repo, _cx| async move { - rx.await??; - - match git_repo { - RepositoryState::Local(LocalRepositoryState { - backend, - environment, - .. - }) => { - backend - .commit(message, name_and_email, options, askpass, environment) - .await - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - askpass_delegates.lock().insert(askpass_id, askpass); - let _defer = util::defer(|| { - let askpass_delegate = askpass_delegates.lock().remove(&askpass_id); - debug_assert!(askpass_delegate.is_some()); - }); - let (name, email) = name_and_email.unzip(); - client - .request(proto::Commit { - project_id: project_id.0, - repository_id: id.to_proto(), - message: String::from(message), - name: name.map(String::from), - email: email.map(String::from), - options: Some(proto::commit::CommitOptions { - amend: options.amend, - signoff: options.signoff, - }), - askpass_id, - }) - .await - .context("sending commit request")?; - - Ok(()) - } - } - }) - } - - pub fn fetch( - &mut self, - fetch_options: FetchOptions, - askpass: AskPassDelegate, - _cx: &mut App, - ) -> oneshot::Receiver> { - let askpass_delegates = self.askpass_delegates.clone(); - let askpass_id = util::post_inc(&mut self.latest_askpass_id); - let id = self.id; - - self.send_job(Some("git fetch".into()), move |git_repo, cx| async move { - match git_repo { - RepositoryState::Local(LocalRepositoryState { - backend, - environment, - .. - }) => backend.fetch(fetch_options, askpass, environment, cx).await, - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - askpass_delegates.lock().insert(askpass_id, askpass); - let _defer = util::defer(|| { - let askpass_delegate = askpass_delegates.lock().remove(&askpass_id); - debug_assert!(askpass_delegate.is_some()); - }); - - let response = client - .request(proto::Fetch { - project_id: project_id.0, - repository_id: id.to_proto(), - askpass_id, - remote: fetch_options.to_proto(), - }) - .await - .context("sending fetch request")?; - - Ok(RemoteCommandOutput { - stdout: response.stdout, - stderr: response.stderr, - }) - } - } - }) - } - - pub fn push( - &mut self, - branch: SharedString, - remote: SharedString, - options: Option, - askpass: AskPassDelegate, - cx: &mut Context, - ) -> oneshot::Receiver> { - let askpass_delegates = self.askpass_delegates.clone(); - let askpass_id = util::post_inc(&mut self.latest_askpass_id); - let id = self.id; - - let args = options - .map(|option| match option { - PushOptions::SetUpstream => " --set-upstream", - PushOptions::Force => " --force-with-lease", - }) - .unwrap_or(""); - - let updates_tx = self - .git_store() - .and_then(|git_store| match &git_store.read(cx).state { - GitStoreState::Local { downstream, .. } => downstream - .as_ref() - .map(|downstream| downstream.updates_tx.clone()), - _ => None, - }); - - let this = cx.weak_entity(); - self.send_job( - Some(format!("git push {} {} {}", args, remote, branch).into()), - move |git_repo, mut cx| async move { - match git_repo { - RepositoryState::Local(LocalRepositoryState { - backend, - environment, - .. - }) => { - let result = backend - .push( - branch.to_string(), - remote.to_string(), - options, - askpass, - environment.clone(), - cx.clone(), - ) - .await; - // TODO would be nice to not have to do this manually - if result.is_ok() { - let branches = backend.branches().await?; - let branch = branches.into_iter().find(|branch| branch.is_head); - log::info!("head branch after scan is {branch:?}"); - let snapshot = this.update(&mut cx, |this, cx| { - this.snapshot.branch = branch; - cx.emit(RepositoryEvent::BranchChanged); - this.snapshot.clone() - })?; - if let Some(updates_tx) = updates_tx { - updates_tx - .unbounded_send(DownstreamUpdate::UpdateRepository(snapshot)) - .ok(); - } - } - result - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - askpass_delegates.lock().insert(askpass_id, askpass); - let _defer = util::defer(|| { - let askpass_delegate = askpass_delegates.lock().remove(&askpass_id); - debug_assert!(askpass_delegate.is_some()); - }); - let response = client - .request(proto::Push { - project_id: project_id.0, - repository_id: id.to_proto(), - askpass_id, - branch_name: branch.to_string(), - remote_name: remote.to_string(), - options: options.map(|options| match options { - PushOptions::Force => proto::push::PushOptions::Force, - PushOptions::SetUpstream => { - proto::push::PushOptions::SetUpstream - } - } - as i32), - }) - .await - .context("sending push request")?; - - Ok(RemoteCommandOutput { - stdout: response.stdout, - stderr: response.stderr, - }) - } - } - }, - ) - } - - pub fn pull( - &mut self, - branch: Option, - remote: SharedString, - rebase: bool, - askpass: AskPassDelegate, - _cx: &mut App, - ) -> oneshot::Receiver> { - let askpass_delegates = self.askpass_delegates.clone(); - let askpass_id = util::post_inc(&mut self.latest_askpass_id); - let id = self.id; - - let mut status = "git pull".to_string(); - if rebase { - status.push_str(" --rebase"); - } - status.push_str(&format!(" {}", remote)); - if let Some(b) = &branch { - status.push_str(&format!(" {}", b)); - } - - self.send_job(Some(status.into()), move |git_repo, cx| async move { - match git_repo { - RepositoryState::Local(LocalRepositoryState { - backend, - environment, - .. - }) => { - backend - .pull( - branch.as_ref().map(|b| b.to_string()), - remote.to_string(), - rebase, - askpass, - environment.clone(), - cx, - ) - .await - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - askpass_delegates.lock().insert(askpass_id, askpass); - let _defer = util::defer(|| { - let askpass_delegate = askpass_delegates.lock().remove(&askpass_id); - debug_assert!(askpass_delegate.is_some()); - }); - let response = client - .request(proto::Pull { - project_id: project_id.0, - repository_id: id.to_proto(), - askpass_id, - rebase, - branch_name: branch.as_ref().map(|b| b.to_string()), - remote_name: remote.to_string(), - }) - .await - .context("sending pull request")?; - - Ok(RemoteCommandOutput { - stdout: response.stdout, - stderr: response.stderr, - }) - } - } - }) - } - - fn spawn_set_index_text_job( - &mut self, - path: RepoPath, - content: Option, - hunk_staging_operation_count: Option, - cx: &mut Context, - ) -> oneshot::Receiver> { - let id = self.id; - let this = cx.weak_entity(); - let git_store = self.git_store.clone(); - let abs_path = self.snapshot.repo_path_to_abs_path(&path); - self.send_keyed_job( - Some(GitJobKey::WriteIndex(vec![path.clone()])), - None, - move |git_repo, mut cx| async move { - log::debug!( - "start updating index text for buffer {}", - path.as_unix_str() - ); - - match git_repo { - RepositoryState::Local(LocalRepositoryState { - fs, - backend, - environment, - .. - }) => { - let executable = match fs.metadata(&abs_path).await { - Ok(Some(meta)) => meta.is_executable, - Ok(None) => false, - Err(_err) => false, - }; - backend - .set_index_text(path.clone(), content, environment.clone(), executable) - .await?; - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - client - .request(proto::SetIndexText { - project_id: project_id.0, - repository_id: id.to_proto(), - path: path.to_proto(), - text: content, - }) - .await?; - } - } - log::debug!( - "finish updating index text for buffer {}", - path.as_unix_str() - ); - - if let Some(hunk_staging_operation_count) = hunk_staging_operation_count { - let project_path = this - .read_with(&cx, |this, cx| this.repo_path_to_project_path(&path, cx)) - .ok() - .flatten(); - git_store.update(&mut cx, |git_store, cx| { - let buffer_id = git_store - .buffer_store - .read(cx) - .get_by_path(&project_path?)? - .read(cx) - .remote_id(); - let diff_state = git_store.diffs.get(&buffer_id)?; - diff_state.update(cx, |diff_state, _| { - diff_state.hunk_staging_operation_count_as_of_write = - hunk_staging_operation_count; - }); - Some(()) - })?; - } - Ok(()) - }, - ) - } - - pub fn create_remote( - &mut self, - remote_name: String, - remote_url: String, - ) -> oneshot::Receiver> { - let id = self.id; - self.send_job( - Some(format!("git remote add {remote_name} {remote_url}").into()), - move |repo, _cx| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.create_remote(remote_name, remote_url).await - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - client - .request(proto::GitCreateRemote { - project_id: project_id.0, - repository_id: id.to_proto(), - remote_name, - remote_url, - }) - .await?; - - Ok(()) - } - } - }, - ) - } - - pub fn remove_remote(&mut self, remote_name: String) -> oneshot::Receiver> { - let id = self.id; - self.send_job( - Some(format!("git remove remote {remote_name}").into()), - move |repo, _cx| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.remove_remote(remote_name).await - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - client - .request(proto::GitRemoveRemote { - project_id: project_id.0, - repository_id: id.to_proto(), - remote_name, - }) - .await?; - - Ok(()) - } - } - }, - ) - } - - pub fn get_remotes( - &mut self, - branch_name: Option, - is_push: bool, - ) -> oneshot::Receiver>> { - let id = self.id; - self.send_job(None, move |repo, _cx| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - let remote = if let Some(branch_name) = branch_name { - if is_push { - backend.get_push_remote(branch_name).await? - } else { - backend.get_branch_remote(branch_name).await? - } - } else { - None - }; - - match remote { - Some(remote) => Ok(vec![remote]), - None => backend.get_all_remotes().await, - } - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - let response = client - .request(proto::GetRemotes { - project_id: project_id.0, - repository_id: id.to_proto(), - branch_name, - is_push, - }) - .await?; - - let remotes = response - .remotes - .into_iter() - .map(|remotes| Remote { - name: remotes.name.into(), - }) - .collect(); - - Ok(remotes) - } - } - }) - } - - pub fn branches(&mut self) -> oneshot::Receiver>> { - let id = self.id; - self.send_job(None, move |repo, _| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.branches().await - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - let response = client - .request(proto::GitGetBranches { - project_id: project_id.0, - repository_id: id.to_proto(), - }) - .await?; - - let branches = response - .branches - .into_iter() - .map(|branch| proto_to_branch(&branch)) - .collect(); - - Ok(branches) - } - } - }) - } - - pub fn worktrees(&mut self) -> oneshot::Receiver>> { - let id = self.id; - self.send_job(None, move |repo, _| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.worktrees().await - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - let response = client - .request(proto::GitGetWorktrees { - project_id: project_id.0, - repository_id: id.to_proto(), - }) - .await?; - - let worktrees = response - .worktrees - .into_iter() - .map(|worktree| proto_to_worktree(&worktree)) - .collect(); - - Ok(worktrees) - } - } - }) - } - - pub fn create_worktree( - &mut self, - name: String, - path: PathBuf, - commit: Option, - ) -> oneshot::Receiver> { - let id = self.id; - self.send_job( - Some("git worktree add".into()), - move |repo, _cx| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.create_worktree(name, path, commit).await - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - client - .request(proto::GitCreateWorktree { - project_id: project_id.0, - repository_id: id.to_proto(), - name, - directory: path.to_string_lossy().to_string(), - commit, - }) - .await?; - - Ok(()) - } - } - }, - ) - } - - pub fn default_branch(&mut self) -> oneshot::Receiver>> { - let id = self.id; - self.send_job(None, move |repo, _| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.default_branch().await - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - let response = client - .request(proto::GetDefaultBranch { - project_id: project_id.0, - repository_id: id.to_proto(), - }) - .await?; - - anyhow::Ok(response.branch.map(SharedString::from)) - } - } - }) - } - - pub fn diff_tree( - &mut self, - diff_type: DiffTreeType, - _cx: &App, - ) -> oneshot::Receiver> { - let repository_id = self.snapshot.id; - self.send_job(None, move |repo, _cx| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.diff_tree(diff_type).await - } - RepositoryState::Remote(RemoteRepositoryState { client, project_id }) => { - let response = client - .request(proto::GetTreeDiff { - project_id: project_id.0, - repository_id: repository_id.0, - is_merge: matches!(diff_type, DiffTreeType::MergeBase { .. }), - base: diff_type.base().to_string(), - head: diff_type.head().to_string(), - }) - .await?; - - let entries = response - .entries - .into_iter() - .filter_map(|entry| { - let status = match entry.status() { - proto::tree_diff_status::Status::Added => TreeDiffStatus::Added, - proto::tree_diff_status::Status::Modified => { - TreeDiffStatus::Modified { - old: git::Oid::from_str( - &entry.oid.context("missing oid").log_err()?, - ) - .log_err()?, - } - } - proto::tree_diff_status::Status::Deleted => { - TreeDiffStatus::Deleted { - old: git::Oid::from_str( - &entry.oid.context("missing oid").log_err()?, - ) - .log_err()?, - } - } - }; - Some(( - RepoPath::from_rel_path( - &RelPath::from_proto(&entry.path).log_err()?, - ), - status, - )) - }) - .collect(); - - Ok(TreeDiff { entries }) - } - } - }) - } - - pub fn diff(&mut self, diff_type: DiffType, _cx: &App) -> oneshot::Receiver> { - let id = self.id; - self.send_job(None, move |repo, _cx| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.diff(diff_type).await - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - let response = client - .request(proto::GitDiff { - project_id: project_id.0, - repository_id: id.to_proto(), - diff_type: match diff_type { - DiffType::HeadToIndex => { - proto::git_diff::DiffType::HeadToIndex.into() - } - DiffType::HeadToWorktree => { - proto::git_diff::DiffType::HeadToWorktree.into() - } - }, - }) - .await?; - - Ok(response.diff) - } - } - }) - } - - pub fn create_branch( - &mut self, - branch_name: String, - base_branch: Option, - ) -> oneshot::Receiver> { - let id = self.id; - let status_msg = if let Some(ref base) = base_branch { - format!("git switch -c {branch_name} {base}").into() - } else { - format!("git switch -c {branch_name}").into() - }; - self.send_job(Some(status_msg), move |repo, _cx| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.create_branch(branch_name, base_branch).await - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - client - .request(proto::GitCreateBranch { - project_id: project_id.0, - repository_id: id.to_proto(), - branch_name, - }) - .await?; - - Ok(()) - } - } - }) - } - - pub fn change_branch(&mut self, branch_name: String) -> oneshot::Receiver> { - let id = self.id; - self.send_job( - Some(format!("git switch {branch_name}").into()), - move |repo, _cx| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.change_branch(branch_name).await - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - client - .request(proto::GitChangeBranch { - project_id: project_id.0, - repository_id: id.to_proto(), - branch_name, - }) - .await?; - - Ok(()) - } - } - }, - ) - } - - pub fn delete_branch(&mut self, branch_name: String) -> oneshot::Receiver> { - let id = self.id; - self.send_job( - Some(format!("git branch -d {branch_name}").into()), - move |repo, _cx| async move { - match repo { - RepositoryState::Local(state) => state.backend.delete_branch(branch_name).await, - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - client - .request(proto::GitDeleteBranch { - project_id: project_id.0, - repository_id: id.to_proto(), - branch_name, - }) - .await?; - - Ok(()) - } - } - }, - ) - } - - pub fn rename_branch( - &mut self, - branch: String, - new_name: String, - ) -> oneshot::Receiver> { - let id = self.id; - self.send_job( - Some(format!("git branch -m {branch} {new_name}").into()), - move |repo, _cx| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.rename_branch(branch, new_name).await - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - client - .request(proto::GitRenameBranch { - project_id: project_id.0, - repository_id: id.to_proto(), - branch, - new_name, - }) - .await?; - - Ok(()) - } - } - }, - ) - } - - pub fn check_for_pushed_commits(&mut self) -> oneshot::Receiver>> { - let id = self.id; - self.send_job(None, move |repo, _cx| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.check_for_pushed_commit().await - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - let response = client - .request(proto::CheckForPushedCommits { - project_id: project_id.0, - repository_id: id.to_proto(), - }) - .await?; - - let branches = response.pushed_to.into_iter().map(Into::into).collect(); - - Ok(branches) - } - } - }) - } - - pub fn checkpoint(&mut self) -> oneshot::Receiver> { - self.send_job(None, |repo, _cx| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.checkpoint().await - } - RepositoryState::Remote(..) => anyhow::bail!("not implemented yet"), - } - }) - } - - pub fn restore_checkpoint( - &mut self, - checkpoint: GitRepositoryCheckpoint, - ) -> oneshot::Receiver> { - self.send_job(None, move |repo, _cx| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.restore_checkpoint(checkpoint).await - } - RepositoryState::Remote { .. } => anyhow::bail!("not implemented yet"), - } - }) - } - - pub(crate) fn apply_remote_update( - &mut self, - update: proto::UpdateRepository, - cx: &mut Context, - ) -> Result<()> { - let conflicted_paths = TreeSet::from_ordered_entries( - update - .current_merge_conflicts - .into_iter() - .filter_map(|path| RepoPath::from_proto(&path).log_err()), - ); - let new_branch = update.branch_summary.as_ref().map(proto_to_branch); - let new_head_commit = update - .head_commit_details - .as_ref() - .map(proto_to_commit_details); - if self.snapshot.branch != new_branch || self.snapshot.head_commit != new_head_commit { - cx.emit(RepositoryEvent::BranchChanged) - } - self.snapshot.branch = new_branch; - self.snapshot.head_commit = new_head_commit; - - self.snapshot.merge.conflicted_paths = conflicted_paths; - self.snapshot.merge.message = update.merge_message.map(SharedString::from); - let new_stash_entries = GitStash { - entries: update - .stash_entries - .iter() - .filter_map(|entry| proto_to_stash(entry).ok()) - .collect(), - }; - if self.snapshot.stash_entries != new_stash_entries { - cx.emit(RepositoryEvent::StashEntriesChanged) - } - self.snapshot.stash_entries = new_stash_entries; - self.snapshot.remote_upstream_url = update.remote_upstream_url; - self.snapshot.remote_origin_url = update.remote_origin_url; - - let edits = update - .removed_statuses - .into_iter() - .filter_map(|path| { - Some(sum_tree::Edit::Remove(PathKey( - RelPath::from_proto(&path).log_err()?, - ))) - }) - .chain( - update - .updated_statuses - .into_iter() - .filter_map(|updated_status| { - Some(sum_tree::Edit::Insert(updated_status.try_into().log_err()?)) - }), - ) - .collect::>(); - if !edits.is_empty() { - cx.emit(RepositoryEvent::StatusesChanged); - } - self.snapshot.statuses_by_path.edit(edits, ()); - if update.is_last_update { - self.snapshot.scan_id = update.scan_id; - } - self.clear_pending_ops(cx); - Ok(()) - } - - pub fn compare_checkpoints( - &mut self, - left: GitRepositoryCheckpoint, - right: GitRepositoryCheckpoint, - ) -> oneshot::Receiver> { - self.send_job(None, move |repo, _cx| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.compare_checkpoints(left, right).await - } - RepositoryState::Remote { .. } => anyhow::bail!("not implemented yet"), - } - }) - } - - pub fn diff_checkpoints( - &mut self, - base_checkpoint: GitRepositoryCheckpoint, - target_checkpoint: GitRepositoryCheckpoint, - ) -> oneshot::Receiver> { - self.send_job(None, move |repo, _cx| async move { - match repo { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend - .diff_checkpoints(base_checkpoint, target_checkpoint) - .await - } - RepositoryState::Remote { .. } => anyhow::bail!("not implemented yet"), - } - }) - } - - fn clear_pending_ops(&mut self, cx: &mut Context) { - let updated = SumTree::from_iter( - self.pending_ops.iter().filter_map(|ops| { - let inner_ops: Vec = - ops.ops.iter().filter(|op| op.running()).cloned().collect(); - if inner_ops.is_empty() { - None - } else { - Some(PendingOps { - repo_path: ops.repo_path.clone(), - ops: inner_ops, - }) - } - }), - (), - ); - - if updated != self.pending_ops { - cx.emit(RepositoryEvent::PendingOpsChanged { - pending_ops: self.pending_ops.clone(), - }) - } - - self.pending_ops = updated; - } - - fn schedule_scan( - &mut self, - updates_tx: Option>, - cx: &mut Context, - ) { - let this = cx.weak_entity(); - let _ = self.send_keyed_job( - Some(GitJobKey::ReloadGitState), - None, - |state, mut cx| async move { - log::debug!("run scheduled git status scan"); - - let Some(this) = this.upgrade() else { - return Ok(()); - }; - let RepositoryState::Local(LocalRepositoryState { backend, .. }) = state else { - bail!("not a local repository") - }; - let (snapshot, events) = this - .update(&mut cx, |this, _| { - this.paths_needing_status_update.clear(); - compute_snapshot( - this.id, - this.work_directory_abs_path.clone(), - this.snapshot.clone(), - backend.clone(), - ) - })? - .await?; - this.update(&mut cx, |this, cx| { - this.snapshot = snapshot.clone(); - this.clear_pending_ops(cx); - for event in events { - cx.emit(event); - } - })?; - if let Some(updates_tx) = updates_tx { - updates_tx - .unbounded_send(DownstreamUpdate::UpdateRepository(snapshot)) - .ok(); - } - Ok(()) - }, - ); - } - - fn spawn_local_git_worker( - state: Shared>>, - cx: &mut Context, - ) -> mpsc::UnboundedSender { - let (job_tx, mut job_rx) = mpsc::unbounded::(); - - cx.spawn(async move |_, cx| { - let state = state.await.map_err(|err| anyhow::anyhow!(err))?; - if let Some(git_hosting_provider_registry) = - cx.update(|cx| GitHostingProviderRegistry::try_global(cx))? - { - git_hosting_providers::register_additional_providers( - git_hosting_provider_registry, - state.backend.clone(), - ) - .await; - } - let state = RepositoryState::Local(state); - let mut jobs = VecDeque::new(); - loop { - while let Ok(Some(next_job)) = job_rx.try_next() { - jobs.push_back(next_job); - } - - if let Some(job) = jobs.pop_front() { - if let Some(current_key) = &job.key - && jobs - .iter() - .any(|other_job| other_job.key.as_ref() == Some(current_key)) - { - continue; - } - (job.job)(state.clone(), cx).await; - } else if let Some(job) = job_rx.next().await { - jobs.push_back(job); - } else { - break; - } - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - - job_tx - } - - fn spawn_remote_git_worker( - state: RemoteRepositoryState, - cx: &mut Context, - ) -> mpsc::UnboundedSender { - let (job_tx, mut job_rx) = mpsc::unbounded::(); - - cx.spawn(async move |_, cx| { - let state = RepositoryState::Remote(state); - let mut jobs = VecDeque::new(); - loop { - while let Ok(Some(next_job)) = job_rx.try_next() { - jobs.push_back(next_job); - } - - if let Some(job) = jobs.pop_front() { - if let Some(current_key) = &job.key - && jobs - .iter() - .any(|other_job| other_job.key.as_ref() == Some(current_key)) - { - continue; - } - (job.job)(state.clone(), cx).await; - } else if let Some(job) = job_rx.next().await { - jobs.push_back(job); - } else { - break; - } - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - - job_tx - } - - fn load_staged_text( - &mut self, - buffer_id: BufferId, - repo_path: RepoPath, - cx: &App, - ) -> Task>> { - let rx = self.send_job(None, move |state, _| async move { - match state { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - anyhow::Ok(backend.load_index_text(repo_path).await) - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - let response = client - .request(proto::OpenUnstagedDiff { - project_id: project_id.to_proto(), - buffer_id: buffer_id.to_proto(), - }) - .await?; - Ok(response.staged_text) - } - } - }); - cx.spawn(|_: &mut AsyncApp| async move { rx.await? }) - } - - fn load_committed_text( - &mut self, - buffer_id: BufferId, - repo_path: RepoPath, - cx: &App, - ) -> Task> { - let rx = self.send_job(None, move |state, _| async move { - match state { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - let committed_text = backend.load_committed_text(repo_path.clone()).await; - let staged_text = backend.load_index_text(repo_path).await; - let diff_bases_change = if committed_text == staged_text { - DiffBasesChange::SetBoth(committed_text) - } else { - DiffBasesChange::SetEach { - index: staged_text, - head: committed_text, - } - }; - anyhow::Ok(diff_bases_change) - } - RepositoryState::Remote(RemoteRepositoryState { project_id, client }) => { - use proto::open_uncommitted_diff_response::Mode; - - let response = client - .request(proto::OpenUncommittedDiff { - project_id: project_id.to_proto(), - buffer_id: buffer_id.to_proto(), - }) - .await?; - let mode = Mode::from_i32(response.mode).context("Invalid mode")?; - let bases = match mode { - Mode::IndexMatchesHead => DiffBasesChange::SetBoth(response.committed_text), - Mode::IndexAndHead => DiffBasesChange::SetEach { - head: response.committed_text, - index: response.staged_text, - }, - }; - Ok(bases) - } - } - }); - - cx.spawn(|_: &mut AsyncApp| async move { rx.await? }) - } - fn load_blob_content(&mut self, oid: Oid, cx: &App) -> Task> { - let repository_id = self.snapshot.id; - let rx = self.send_job(None, move |state, _| async move { - match state { - RepositoryState::Local(LocalRepositoryState { backend, .. }) => { - backend.load_blob_content(oid).await - } - RepositoryState::Remote(RemoteRepositoryState { client, project_id }) => { - let response = client - .request(proto::GetBlobContent { - project_id: project_id.to_proto(), - repository_id: repository_id.0, - oid: oid.to_string(), - }) - .await?; - Ok(response.content) - } - } - }); - cx.spawn(|_: &mut AsyncApp| async move { rx.await? }) - } - - fn paths_changed( - &mut self, - paths: Vec, - updates_tx: Option>, - cx: &mut Context, - ) { - self.paths_needing_status_update.extend(paths); - - let this = cx.weak_entity(); - let _ = self.send_keyed_job( - Some(GitJobKey::RefreshStatuses), - None, - |state, mut cx| async move { - let (prev_snapshot, mut changed_paths) = this.update(&mut cx, |this, _| { - ( - this.snapshot.clone(), - mem::take(&mut this.paths_needing_status_update), - ) - })?; - let RepositoryState::Local(LocalRepositoryState { backend, .. }) = state else { - bail!("not a local repository") - }; - - let paths = changed_paths.iter().cloned().collect::>(); - if paths.is_empty() { - return Ok(()); - } - let statuses = backend.status(&paths).await?; - let stash_entries = backend.stash_entries().await?; - - let changed_path_statuses = cx - .background_spawn(async move { - let mut changed_path_statuses = Vec::new(); - let prev_statuses = prev_snapshot.statuses_by_path.clone(); - let mut cursor = prev_statuses.cursor::(()); - - for (repo_path, status) in &*statuses.entries { - changed_paths.remove(repo_path); - if cursor.seek_forward(&PathTarget::Path(repo_path), Bias::Left) - && cursor.item().is_some_and(|entry| entry.status == *status) - { - continue; - } - - changed_path_statuses.push(Edit::Insert(StatusEntry { - repo_path: repo_path.clone(), - status: *status, - })); - } - let mut cursor = prev_statuses.cursor::(()); - for path in changed_paths.into_iter() { - if cursor.seek_forward(&PathTarget::Path(&path), Bias::Left) { - changed_path_statuses - .push(Edit::Remove(PathKey(path.as_ref().clone()))); - } - } - changed_path_statuses - }) - .await; - - this.update(&mut cx, |this, cx| { - if this.snapshot.stash_entries != stash_entries { - cx.emit(RepositoryEvent::StashEntriesChanged); - this.snapshot.stash_entries = stash_entries; - } - - if !changed_path_statuses.is_empty() { - cx.emit(RepositoryEvent::StatusesChanged); - this.snapshot - .statuses_by_path - .edit(changed_path_statuses, ()); - this.snapshot.scan_id += 1; - } - - if let Some(updates_tx) = updates_tx { - updates_tx - .unbounded_send(DownstreamUpdate::UpdateRepository( - this.snapshot.clone(), - )) - .ok(); - } - }) - }, - ); - } - - /// currently running git command and when it started - pub fn current_job(&self) -> Option { - self.active_jobs.values().next().cloned() - } - - pub fn barrier(&mut self) -> oneshot::Receiver<()> { - self.send_job(None, |_, _| async {}) - } - - fn spawn_job_with_tracking( - &mut self, - paths: Vec, - git_status: pending_op::GitStatus, - cx: &mut Context, - f: AsyncFn, - ) -> Task> - where - AsyncFn: AsyncFnOnce(WeakEntity, &mut AsyncApp) -> Result<()> + 'static, - { - let ids = self.new_pending_ops_for_paths(paths, git_status); - - cx.spawn(async move |this, cx| { - let (job_status, result) = match f(this.clone(), cx).await { - Ok(()) => (pending_op::JobStatus::Finished, Ok(())), - Err(err) if err.is::() => (pending_op::JobStatus::Skipped, Ok(())), - Err(err) => (pending_op::JobStatus::Error, Err(err)), - }; - - this.update(cx, |this, _| { - let mut edits = Vec::with_capacity(ids.len()); - for (id, entry) in ids { - if let Some(mut ops) = this - .pending_ops - .get(&PathKey(entry.as_ref().clone()), ()) - .cloned() - { - if let Some(op) = ops.op_by_id_mut(id) { - op.job_status = job_status; - } - edits.push(sum_tree::Edit::Insert(ops)); - } - } - this.pending_ops.edit(edits, ()); - })?; - - result - }) - } - - fn new_pending_ops_for_paths( - &mut self, - paths: Vec, - git_status: pending_op::GitStatus, - ) -> Vec<(PendingOpId, RepoPath)> { - let mut edits = Vec::with_capacity(paths.len()); - let mut ids = Vec::with_capacity(paths.len()); - for path in paths { - let mut ops = self - .pending_ops - .get(&PathKey(path.as_ref().clone()), ()) - .cloned() - .unwrap_or_else(|| PendingOps::new(&path)); - let id = ops.max_id() + 1; - ops.ops.push(PendingOp { - id, - git_status, - job_status: pending_op::JobStatus::Running, - }); - edits.push(sum_tree::Edit::Insert(ops)); - ids.push((id, path)); - } - self.pending_ops.edit(edits, ()); - ids - } -} - -fn get_permalink_in_rust_registry_src( - provider_registry: Arc, - path: PathBuf, - selection: Range, -) -> Result { - #[derive(Deserialize)] - struct CargoVcsGit { - sha1: String, - } - - #[derive(Deserialize)] - struct CargoVcsInfo { - git: CargoVcsGit, - path_in_vcs: String, - } - - #[derive(Deserialize)] - struct CargoPackage { - repository: String, - } - - #[derive(Deserialize)] - struct CargoToml { - package: CargoPackage, - } - - let Some((dir, cargo_vcs_info_json)) = path.ancestors().skip(1).find_map(|dir| { - let json = std::fs::read_to_string(dir.join(".cargo_vcs_info.json")).ok()?; - Some((dir, json)) - }) else { - bail!("No .cargo_vcs_info.json found in parent directories") - }; - let cargo_vcs_info = serde_json::from_str::(&cargo_vcs_info_json)?; - let cargo_toml = std::fs::read_to_string(dir.join("Cargo.toml"))?; - let manifest = toml::from_str::(&cargo_toml)?; - let (provider, remote) = parse_git_remote_url(provider_registry, &manifest.package.repository) - .context("parsing package.repository field of manifest")?; - let path = PathBuf::from(cargo_vcs_info.path_in_vcs).join(path.strip_prefix(dir).unwrap()); - let permalink = provider.build_permalink( - remote, - BuildPermalinkParams::new( - &cargo_vcs_info.git.sha1, - &RepoPath::from_rel_path( - &RelPath::new(&path, PathStyle::local()).context("invalid path")?, - ), - Some(selection), - ), - ); - Ok(permalink) -} - -fn serialize_blame_buffer_response(blame: Option) -> proto::BlameBufferResponse { - let Some(blame) = blame else { - return proto::BlameBufferResponse { - blame_response: None, - }; - }; - - let entries = blame - .entries - .into_iter() - .map(|entry| proto::BlameEntry { - sha: entry.sha.as_bytes().into(), - start_line: entry.range.start, - end_line: entry.range.end, - original_line_number: entry.original_line_number, - author: entry.author, - author_mail: entry.author_mail, - author_time: entry.author_time, - author_tz: entry.author_tz, - committer: entry.committer_name, - committer_mail: entry.committer_email, - committer_time: entry.committer_time, - committer_tz: entry.committer_tz, - summary: entry.summary, - previous: entry.previous, - filename: entry.filename, - }) - .collect::>(); - - let messages = blame - .messages - .into_iter() - .map(|(oid, message)| proto::CommitMessage { - oid: oid.as_bytes().into(), - message, - }) - .collect::>(); - - proto::BlameBufferResponse { - blame_response: Some(proto::blame_buffer_response::BlameResponse { entries, messages }), - } -} - -fn deserialize_blame_buffer_response( - response: proto::BlameBufferResponse, -) -> Option { - let response = response.blame_response?; - let entries = response - .entries - .into_iter() - .filter_map(|entry| { - Some(git::blame::BlameEntry { - sha: git::Oid::from_bytes(&entry.sha).ok()?, - range: entry.start_line..entry.end_line, - original_line_number: entry.original_line_number, - committer_name: entry.committer, - committer_time: entry.committer_time, - committer_tz: entry.committer_tz, - committer_email: entry.committer_mail, - author: entry.author, - author_mail: entry.author_mail, - author_time: entry.author_time, - author_tz: entry.author_tz, - summary: entry.summary, - previous: entry.previous, - filename: entry.filename, - }) - }) - .collect::>(); - - let messages = response - .messages - .into_iter() - .filter_map(|message| Some((git::Oid::from_bytes(&message.oid).ok()?, message.message))) - .collect::>(); - - Some(Blame { entries, messages }) -} - -fn branch_to_proto(branch: &git::repository::Branch) -> proto::Branch { - proto::Branch { - is_head: branch.is_head, - ref_name: branch.ref_name.to_string(), - unix_timestamp: branch - .most_recent_commit - .as_ref() - .map(|commit| commit.commit_timestamp as u64), - upstream: branch.upstream.as_ref().map(|upstream| proto::GitUpstream { - ref_name: upstream.ref_name.to_string(), - tracking: upstream - .tracking - .status() - .map(|upstream| proto::UpstreamTracking { - ahead: upstream.ahead as u64, - behind: upstream.behind as u64, - }), - }), - most_recent_commit: branch - .most_recent_commit - .as_ref() - .map(|commit| proto::CommitSummary { - sha: commit.sha.to_string(), - subject: commit.subject.to_string(), - commit_timestamp: commit.commit_timestamp, - author_name: commit.author_name.to_string(), - }), - } -} - -fn worktree_to_proto(worktree: &git::repository::Worktree) -> proto::Worktree { - proto::Worktree { - path: worktree.path.to_string_lossy().to_string(), - ref_name: worktree.ref_name.to_string(), - sha: worktree.sha.to_string(), - } -} - -fn proto_to_worktree(proto: &proto::Worktree) -> git::repository::Worktree { - git::repository::Worktree { - path: PathBuf::from(proto.path.clone()), - ref_name: proto.ref_name.clone().into(), - sha: proto.sha.clone().into(), - } -} - -fn proto_to_branch(proto: &proto::Branch) -> git::repository::Branch { - git::repository::Branch { - is_head: proto.is_head, - ref_name: proto.ref_name.clone().into(), - upstream: proto - .upstream - .as_ref() - .map(|upstream| git::repository::Upstream { - ref_name: upstream.ref_name.to_string().into(), - tracking: upstream - .tracking - .as_ref() - .map(|tracking| { - git::repository::UpstreamTracking::Tracked(UpstreamTrackingStatus { - ahead: tracking.ahead as u32, - behind: tracking.behind as u32, - }) - }) - .unwrap_or(git::repository::UpstreamTracking::Gone), - }), - most_recent_commit: proto.most_recent_commit.as_ref().map(|commit| { - git::repository::CommitSummary { - sha: commit.sha.to_string().into(), - subject: commit.subject.to_string().into(), - commit_timestamp: commit.commit_timestamp, - author_name: commit.author_name.to_string().into(), - has_parent: true, - } - }), - } -} - -fn commit_details_to_proto(commit: &CommitDetails) -> proto::GitCommitDetails { - proto::GitCommitDetails { - sha: commit.sha.to_string(), - message: commit.message.to_string(), - commit_timestamp: commit.commit_timestamp, - author_email: commit.author_email.to_string(), - author_name: commit.author_name.to_string(), - } -} - -fn proto_to_commit_details(proto: &proto::GitCommitDetails) -> CommitDetails { - CommitDetails { - sha: proto.sha.clone().into(), - message: proto.message.clone().into(), - commit_timestamp: proto.commit_timestamp, - author_email: proto.author_email.clone().into(), - author_name: proto.author_name.clone().into(), - } -} - -async fn compute_snapshot( - id: RepositoryId, - work_directory_abs_path: Arc, - prev_snapshot: RepositorySnapshot, - backend: Arc, -) -> Result<(RepositorySnapshot, Vec)> { - let mut events = Vec::new(); - let branches = backend.branches().await?; - let branch = branches.into_iter().find(|branch| branch.is_head); - let statuses = backend - .status(&[RepoPath::from_rel_path( - &RelPath::new(".".as_ref(), PathStyle::local()).unwrap(), - )]) - .await?; - let stash_entries = backend.stash_entries().await?; - let statuses_by_path = SumTree::from_iter( - statuses - .entries - .iter() - .map(|(repo_path, status)| StatusEntry { - repo_path: repo_path.clone(), - status: *status, - }), - (), - ); - let (merge_details, merge_heads_changed) = - MergeDetails::load(&backend, &statuses_by_path, &prev_snapshot).await?; - log::debug!("new merge details (changed={merge_heads_changed:?}): {merge_details:?}"); - - if merge_heads_changed { - events.push(RepositoryEvent::MergeHeadsChanged); - } - - if statuses_by_path != prev_snapshot.statuses_by_path { - events.push(RepositoryEvent::StatusesChanged) - } - - // Useful when branch is None in detached head state - let head_commit = match backend.head_sha().await { - Some(head_sha) => backend.show(head_sha).await.log_err(), - None => None, - }; - - if branch != prev_snapshot.branch || head_commit != prev_snapshot.head_commit { - events.push(RepositoryEvent::BranchChanged); - } - - let remote_origin_url = backend.remote_url("origin").await; - let remote_upstream_url = backend.remote_url("upstream").await; - - let snapshot = RepositorySnapshot { - id, - statuses_by_path, - work_directory_abs_path, - path_style: prev_snapshot.path_style, - scan_id: prev_snapshot.scan_id + 1, - branch, - head_commit, - merge: merge_details, - remote_origin_url, - remote_upstream_url, - stash_entries, - }; - - Ok((snapshot, events)) -} - -fn status_from_proto( - simple_status: i32, - status: Option, -) -> anyhow::Result { - use proto::git_file_status::Variant; - - let Some(variant) = status.and_then(|status| status.variant) else { - let code = proto::GitStatus::from_i32(simple_status) - .with_context(|| format!("Invalid git status code: {simple_status}"))?; - let result = match code { - proto::GitStatus::Added => TrackedStatus { - worktree_status: StatusCode::Added, - index_status: StatusCode::Unmodified, - } - .into(), - proto::GitStatus::Modified => TrackedStatus { - worktree_status: StatusCode::Modified, - index_status: StatusCode::Unmodified, - } - .into(), - proto::GitStatus::Conflict => UnmergedStatus { - first_head: UnmergedStatusCode::Updated, - second_head: UnmergedStatusCode::Updated, - } - .into(), - proto::GitStatus::Deleted => TrackedStatus { - worktree_status: StatusCode::Deleted, - index_status: StatusCode::Unmodified, - } - .into(), - _ => anyhow::bail!("Invalid code for simple status: {simple_status}"), - }; - return Ok(result); - }; - - let result = match variant { - Variant::Untracked(_) => FileStatus::Untracked, - Variant::Ignored(_) => FileStatus::Ignored, - Variant::Unmerged(unmerged) => { - let [first_head, second_head] = - [unmerged.first_head, unmerged.second_head].map(|head| { - let code = proto::GitStatus::from_i32(head) - .with_context(|| format!("Invalid git status code: {head}"))?; - let result = match code { - proto::GitStatus::Added => UnmergedStatusCode::Added, - proto::GitStatus::Updated => UnmergedStatusCode::Updated, - proto::GitStatus::Deleted => UnmergedStatusCode::Deleted, - _ => anyhow::bail!("Invalid code for unmerged status: {code:?}"), - }; - Ok(result) - }); - let [first_head, second_head] = [first_head?, second_head?]; - UnmergedStatus { - first_head, - second_head, - } - .into() - } - Variant::Tracked(tracked) => { - let [index_status, worktree_status] = [tracked.index_status, tracked.worktree_status] - .map(|status| { - let code = proto::GitStatus::from_i32(status) - .with_context(|| format!("Invalid git status code: {status}"))?; - let result = match code { - proto::GitStatus::Modified => StatusCode::Modified, - proto::GitStatus::TypeChanged => StatusCode::TypeChanged, - proto::GitStatus::Added => StatusCode::Added, - proto::GitStatus::Deleted => StatusCode::Deleted, - proto::GitStatus::Renamed => StatusCode::Renamed, - proto::GitStatus::Copied => StatusCode::Copied, - proto::GitStatus::Unmodified => StatusCode::Unmodified, - _ => anyhow::bail!("Invalid code for tracked status: {code:?}"), - }; - Ok(result) - }); - let [index_status, worktree_status] = [index_status?, worktree_status?]; - TrackedStatus { - index_status, - worktree_status, - } - .into() - } - }; - Ok(result) -} - -fn status_to_proto(status: FileStatus) -> proto::GitFileStatus { - use proto::git_file_status::{Tracked, Unmerged, Variant}; - - let variant = match status { - FileStatus::Untracked => Variant::Untracked(Default::default()), - FileStatus::Ignored => Variant::Ignored(Default::default()), - FileStatus::Unmerged(UnmergedStatus { - first_head, - second_head, - }) => Variant::Unmerged(Unmerged { - first_head: unmerged_status_to_proto(first_head), - second_head: unmerged_status_to_proto(second_head), - }), - FileStatus::Tracked(TrackedStatus { - index_status, - worktree_status, - }) => Variant::Tracked(Tracked { - index_status: tracked_status_to_proto(index_status), - worktree_status: tracked_status_to_proto(worktree_status), - }), - }; - proto::GitFileStatus { - variant: Some(variant), - } -} - -fn unmerged_status_to_proto(code: UnmergedStatusCode) -> i32 { - match code { - UnmergedStatusCode::Added => proto::GitStatus::Added as _, - UnmergedStatusCode::Deleted => proto::GitStatus::Deleted as _, - UnmergedStatusCode::Updated => proto::GitStatus::Updated as _, - } -} - -fn tracked_status_to_proto(code: StatusCode) -> i32 { - match code { - StatusCode::Added => proto::GitStatus::Added as _, - StatusCode::Deleted => proto::GitStatus::Deleted as _, - StatusCode::Modified => proto::GitStatus::Modified as _, - StatusCode::Renamed => proto::GitStatus::Renamed as _, - StatusCode::TypeChanged => proto::GitStatus::TypeChanged as _, - StatusCode::Copied => proto::GitStatus::Copied as _, - StatusCode::Unmodified => proto::GitStatus::Unmodified as _, - } -} diff --git a/crates/project/src/git_store/branch_diff.rs b/crates/project/src/git_store/branch_diff.rs deleted file mode 100644 index dd0026961e..0000000000 --- a/crates/project/src/git_store/branch_diff.rs +++ /dev/null @@ -1,386 +0,0 @@ -use anyhow::Result; -use buffer_diff::BufferDiff; -use collections::HashSet; -use futures::StreamExt; -use git::{ - repository::RepoPath, - status::{DiffTreeType, FileStatus, StatusCode, TrackedStatus, TreeDiff, TreeDiffStatus}, -}; -use gpui::{ - App, AsyncWindowContext, Context, Entity, EventEmitter, SharedString, Subscription, Task, - WeakEntity, Window, -}; - -use language::Buffer; -use text::BufferId; -use util::ResultExt; -use ztracing::instrument; - -use crate::{ - Project, - git_store::{GitStoreEvent, Repository, RepositoryEvent}, -}; - -#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -pub enum DiffBase { - Head, - Merge { base_ref: SharedString }, -} - -impl DiffBase { - pub fn is_merge_base(&self) -> bool { - matches!(self, DiffBase::Merge { .. }) - } -} - -pub struct BranchDiff { - diff_base: DiffBase, - repo: Option>, - project: Entity, - base_commit: Option, - head_commit: Option, - tree_diff: Option, - _subscription: Subscription, - update_needed: postage::watch::Sender<()>, - _task: Task<()>, -} - -pub enum BranchDiffEvent { - FileListChanged, -} - -impl EventEmitter for BranchDiff {} - -impl BranchDiff { - pub fn new( - source: DiffBase, - project: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let git_store = project.read(cx).git_store().clone(); - let git_store_subscription = cx.subscribe_in( - &git_store, - window, - move |this, _git_store, event, _window, cx| match event { - GitStoreEvent::ActiveRepositoryChanged(_) - | GitStoreEvent::RepositoryUpdated(_, RepositoryEvent::StatusesChanged, true) - | GitStoreEvent::ConflictsUpdated => { - cx.emit(BranchDiffEvent::FileListChanged); - *this.update_needed.borrow_mut() = (); - } - _ => {} - }, - ); - - let (send, recv) = postage::watch::channel::<()>(); - let worker = window.spawn(cx, { - let this = cx.weak_entity(); - async |cx| Self::handle_status_updates(this, recv, cx).await - }); - let repo = git_store.read(cx).active_repository(); - - Self { - diff_base: source, - repo, - project, - tree_diff: None, - base_commit: None, - head_commit: None, - _subscription: git_store_subscription, - _task: worker, - update_needed: send, - } - } - - pub fn diff_base(&self) -> &DiffBase { - &self.diff_base - } - - pub async fn handle_status_updates( - this: WeakEntity, - mut recv: postage::watch::Receiver<()>, - cx: &mut AsyncWindowContext, - ) { - Self::reload_tree_diff(this.clone(), cx).await.log_err(); - while recv.next().await.is_some() { - let Ok(needs_update) = this.update(cx, |this, cx| { - let mut needs_update = false; - let active_repo = this - .project - .read(cx) - .git_store() - .read(cx) - .active_repository(); - if active_repo != this.repo { - needs_update = true; - this.repo = active_repo; - } else if let Some(repo) = this.repo.as_ref() { - repo.update(cx, |repo, _| { - if let Some(branch) = &repo.branch - && let DiffBase::Merge { base_ref } = &this.diff_base - && let Some(commit) = branch.most_recent_commit.as_ref() - && &branch.ref_name == base_ref - && this.base_commit.as_ref() != Some(&commit.sha) - { - this.base_commit = Some(commit.sha.clone()); - needs_update = true; - } - - if repo.head_commit.as_ref().map(|c| &c.sha) != this.head_commit.as_ref() { - this.head_commit = repo.head_commit.as_ref().map(|c| c.sha.clone()); - needs_update = true; - } - }) - } - needs_update - }) else { - return; - }; - - if needs_update { - Self::reload_tree_diff(this.clone(), cx).await.log_err(); - } - } - } - - pub fn status_for_buffer_id(&self, buffer_id: BufferId, cx: &App) -> Option { - let (repo, path) = self - .project - .read(cx) - .git_store() - .read(cx) - .repository_and_path_for_buffer_id(buffer_id, cx)?; - if self.repo() == Some(&repo) { - return self.merge_statuses( - repo.read(cx) - .status_for_path(&path) - .map(|status| status.status), - self.tree_diff - .as_ref() - .and_then(|diff| diff.entries.get(&path)), - ); - } - None - } - - pub fn merge_statuses( - &self, - diff_from_head: Option, - diff_from_merge_base: Option<&TreeDiffStatus>, - ) -> Option { - match (diff_from_head, diff_from_merge_base) { - (None, None) => None, - (Some(diff_from_head), None) => Some(diff_from_head), - (Some(diff_from_head @ FileStatus::Unmerged(_)), _) => Some(diff_from_head), - - // file does not exist in HEAD - // but *does* exist in work-tree - // and *does* exist in merge-base - ( - Some(FileStatus::Untracked) - | Some(FileStatus::Tracked(TrackedStatus { - index_status: StatusCode::Added, - worktree_status: _, - })), - Some(_), - ) => Some(FileStatus::Tracked(TrackedStatus { - index_status: StatusCode::Modified, - worktree_status: StatusCode::Modified, - })), - - // file exists in HEAD - // but *does not* exist in work-tree - (Some(diff_from_head), Some(diff_from_merge_base)) if diff_from_head.is_deleted() => { - match diff_from_merge_base { - TreeDiffStatus::Added => None, // unchanged, didn't exist in merge base or worktree - _ => Some(diff_from_head), - } - } - - // file exists in HEAD - // and *does* exist in work-tree - (Some(FileStatus::Tracked(_)), Some(tree_status)) => { - Some(FileStatus::Tracked(TrackedStatus { - index_status: match tree_status { - TreeDiffStatus::Added { .. } => StatusCode::Added, - _ => StatusCode::Modified, - }, - worktree_status: match tree_status { - TreeDiffStatus::Added => StatusCode::Added, - _ => StatusCode::Modified, - }, - })) - } - - (_, Some(diff_from_merge_base)) => { - Some(diff_status_to_file_status(diff_from_merge_base)) - } - } - } - - pub async fn reload_tree_diff( - this: WeakEntity, - cx: &mut AsyncWindowContext, - ) -> Result<()> { - let task = this.update(cx, |this, cx| { - let DiffBase::Merge { base_ref } = this.diff_base.clone() else { - return None; - }; - let Some(repo) = this.repo.as_ref() else { - this.tree_diff.take(); - return None; - }; - repo.update(cx, |repo, cx| { - Some(repo.diff_tree( - DiffTreeType::MergeBase { - base: base_ref, - head: "HEAD".into(), - }, - cx, - )) - }) - })?; - let Some(task) = task else { return Ok(()) }; - - let diff = task.await??; - this.update(cx, |this, cx| { - this.tree_diff = Some(diff); - cx.emit(BranchDiffEvent::FileListChanged); - cx.notify(); - }) - } - - pub fn repo(&self) -> Option<&Entity> { - self.repo.as_ref() - } - - #[instrument(skip_all)] - pub fn load_buffers(&mut self, cx: &mut Context) -> Vec { - let mut output = Vec::default(); - let Some(repo) = self.repo.clone() else { - return output; - }; - - self.project.update(cx, |_project, cx| { - let mut seen = HashSet::default(); - - for item in repo.read(cx).cached_status() { - seen.insert(item.repo_path.clone()); - let branch_diff = self - .tree_diff - .as_ref() - .and_then(|t| t.entries.get(&item.repo_path)) - .cloned(); - let Some(status) = self.merge_statuses(Some(item.status), branch_diff.as_ref()) - else { - continue; - }; - if !status.has_changes() { - continue; - } - - let Some(project_path) = - repo.read(cx).repo_path_to_project_path(&item.repo_path, cx) - else { - continue; - }; - let task = Self::load_buffer(branch_diff, project_path, repo.clone(), cx); - - output.push(DiffBuffer { - repo_path: item.repo_path.clone(), - load: task, - file_status: item.status, - }); - } - let Some(tree_diff) = self.tree_diff.as_ref() else { - return; - }; - - for (path, branch_diff) in tree_diff.entries.iter() { - if seen.contains(&path) { - continue; - } - - let Some(project_path) = repo.read(cx).repo_path_to_project_path(&path, cx) else { - continue; - }; - let task = - Self::load_buffer(Some(branch_diff.clone()), project_path, repo.clone(), cx); - - let file_status = diff_status_to_file_status(branch_diff); - - output.push(DiffBuffer { - repo_path: path.clone(), - load: task, - file_status, - }); - } - }); - output - } - - #[instrument(skip_all)] - fn load_buffer( - branch_diff: Option, - project_path: crate::ProjectPath, - repo: Entity, - cx: &Context<'_, Project>, - ) -> Task, Entity)>> { - let task = cx.spawn(async move |project, cx| { - let buffer = project - .update(cx, |project, cx| project.open_buffer(project_path, cx))? - .await?; - - let languages = project.update(cx, |project, _cx| project.languages().clone())?; - - let changes = if let Some(entry) = branch_diff { - let oid = match entry { - git::status::TreeDiffStatus::Added { .. } => None, - git::status::TreeDiffStatus::Modified { old, .. } - | git::status::TreeDiffStatus::Deleted { old } => Some(old), - }; - project - .update(cx, |project, cx| { - project.git_store().update(cx, |git_store, cx| { - git_store.open_diff_since(oid, buffer.clone(), repo, languages, cx) - }) - })? - .await? - } else { - project - .update(cx, |project, cx| { - project.open_uncommitted_diff(buffer.clone(), cx) - })? - .await? - }; - Ok((buffer, changes)) - }); - task - } -} - -fn diff_status_to_file_status(branch_diff: &git::status::TreeDiffStatus) -> FileStatus { - let file_status = match branch_diff { - git::status::TreeDiffStatus::Added { .. } => FileStatus::Tracked(TrackedStatus { - index_status: StatusCode::Added, - worktree_status: StatusCode::Added, - }), - git::status::TreeDiffStatus::Modified { .. } => FileStatus::Tracked(TrackedStatus { - index_status: StatusCode::Modified, - worktree_status: StatusCode::Modified, - }), - git::status::TreeDiffStatus::Deleted { .. } => FileStatus::Tracked(TrackedStatus { - index_status: StatusCode::Deleted, - worktree_status: StatusCode::Deleted, - }), - }; - file_status -} - -#[derive(Debug)] -pub struct DiffBuffer { - pub repo_path: RepoPath, - pub file_status: FileStatus, - pub load: Task, Entity)>>, -} diff --git a/crates/project/src/git_store/conflict_set.rs b/crates/project/src/git_store/conflict_set.rs deleted file mode 100644 index 064b6998cd..0000000000 --- a/crates/project/src/git_store/conflict_set.rs +++ /dev/null @@ -1,742 +0,0 @@ -use gpui::{App, Context, Entity, EventEmitter, SharedString}; -use std::{cmp::Ordering, ops::Range, sync::Arc}; -use text::{Anchor, BufferId, OffsetRangeExt as _}; - -pub struct ConflictSet { - pub has_conflict: bool, - pub snapshot: ConflictSetSnapshot, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ConflictSetUpdate { - pub buffer_range: Option>, - pub old_range: Range, - pub new_range: Range, -} - -#[derive(Debug, Clone)] -pub struct ConflictSetSnapshot { - pub buffer_id: BufferId, - pub conflicts: Arc<[ConflictRegion]>, -} - -impl ConflictSetSnapshot { - pub fn conflicts_in_range( - &self, - range: Range, - buffer: &text::BufferSnapshot, - ) -> &[ConflictRegion] { - let start_ix = self - .conflicts - .binary_search_by(|conflict| { - conflict - .range - .end - .cmp(&range.start, buffer) - .then(Ordering::Greater) - }) - .unwrap_err(); - let end_ix = start_ix - + self.conflicts[start_ix..] - .binary_search_by(|conflict| { - conflict - .range - .start - .cmp(&range.end, buffer) - .then(Ordering::Less) - }) - .unwrap_err(); - &self.conflicts[start_ix..end_ix] - } - - pub fn compare(&self, other: &Self, buffer: &text::BufferSnapshot) -> ConflictSetUpdate { - let common_prefix_len = self - .conflicts - .iter() - .zip(other.conflicts.iter()) - .take_while(|(old, new)| old == new) - .count(); - let common_suffix_len = self.conflicts[common_prefix_len..] - .iter() - .rev() - .zip(other.conflicts[common_prefix_len..].iter().rev()) - .take_while(|(old, new)| old == new) - .count(); - let old_conflicts = - &self.conflicts[common_prefix_len..(self.conflicts.len() - common_suffix_len)]; - let new_conflicts = - &other.conflicts[common_prefix_len..(other.conflicts.len() - common_suffix_len)]; - let old_range = common_prefix_len..(common_prefix_len + old_conflicts.len()); - let new_range = common_prefix_len..(common_prefix_len + new_conflicts.len()); - let start = match (old_conflicts.first(), new_conflicts.first()) { - (None, None) => None, - (None, Some(conflict)) => Some(conflict.range.start), - (Some(conflict), None) => Some(conflict.range.start), - (Some(first), Some(second)) => { - Some(*first.range.start.min(&second.range.start, buffer)) - } - }; - let end = match (old_conflicts.last(), new_conflicts.last()) { - (None, None) => None, - (None, Some(conflict)) => Some(conflict.range.end), - (Some(first), None) => Some(first.range.end), - (Some(first), Some(second)) => Some(*first.range.end.max(&second.range.end, buffer)), - }; - ConflictSetUpdate { - buffer_range: start.zip(end).map(|(start, end)| start..end), - old_range, - new_range, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct ConflictRegion { - pub ours_branch_name: SharedString, - pub theirs_branch_name: SharedString, - pub range: Range, - pub ours: Range, - pub theirs: Range, - pub base: Option>, -} - -impl ConflictRegion { - pub fn resolve( - &self, - buffer: Entity, - ranges: &[Range], - cx: &mut App, - ) { - let buffer_snapshot = buffer.read(cx).snapshot(); - let mut deletions = Vec::new(); - let empty = ""; - let outer_range = self.range.to_offset(&buffer_snapshot); - let mut offset = outer_range.start; - for kept_range in ranges { - let kept_range = kept_range.to_offset(&buffer_snapshot); - if kept_range.start > offset { - deletions.push((offset..kept_range.start, empty)); - } - offset = kept_range.end; - } - if outer_range.end > offset { - deletions.push((offset..outer_range.end, empty)); - } - - buffer.update(cx, |buffer, cx| { - buffer.edit(deletions, None, cx); - }); - } -} - -impl ConflictSet { - pub fn new(buffer_id: BufferId, has_conflict: bool, _: &mut Context) -> Self { - Self { - has_conflict, - snapshot: ConflictSetSnapshot { - buffer_id, - conflicts: Default::default(), - }, - } - } - - pub fn set_has_conflict(&mut self, has_conflict: bool, cx: &mut Context) -> bool { - if has_conflict != self.has_conflict { - self.has_conflict = has_conflict; - if !self.has_conflict { - cx.emit(ConflictSetUpdate { - buffer_range: None, - old_range: 0..self.snapshot.conflicts.len(), - new_range: 0..0, - }); - self.snapshot.conflicts = Default::default(); - } - true - } else { - false - } - } - - pub fn snapshot(&self) -> ConflictSetSnapshot { - self.snapshot.clone() - } - - pub fn set_snapshot( - &mut self, - snapshot: ConflictSetSnapshot, - update: ConflictSetUpdate, - cx: &mut Context, - ) { - self.snapshot = snapshot; - cx.emit(update); - } - - pub fn parse(buffer: &text::BufferSnapshot) -> ConflictSetSnapshot { - let mut conflicts = Vec::new(); - - let mut line_pos = 0; - let buffer_len = buffer.len(); - let mut lines = buffer.text_for_range(0..buffer_len).lines(); - - let mut conflict_start: Option = None; - let mut ours_start: Option = None; - let mut ours_end: Option = None; - let mut ours_branch_name: Option = None; - let mut base_start: Option = None; - let mut base_end: Option = None; - let mut theirs_start: Option = None; - let mut theirs_branch_name: Option = None; - - while let Some(line) = lines.next() { - let line_end = line_pos + line.len(); - - if let Some(branch_name) = line.strip_prefix("<<<<<<< ") { - // If we see a new conflict marker while already parsing one, - // abandon the previous one and start a new one - conflict_start = Some(line_pos); - ours_start = Some(line_end + 1); - - let branch_name = branch_name.trim(); - if !branch_name.is_empty() { - ours_branch_name = Some(SharedString::new(branch_name)); - } - } else if line.starts_with("||||||| ") - && conflict_start.is_some() - && ours_start.is_some() - { - ours_end = Some(line_pos); - base_start = Some(line_end + 1); - } else if line.starts_with("=======") - && conflict_start.is_some() - && ours_start.is_some() - { - // Set ours_end if not already set (would be set if we have base markers) - if ours_end.is_none() { - ours_end = Some(line_pos); - } else if base_start.is_some() { - base_end = Some(line_pos); - } - theirs_start = Some(line_end + 1); - } else if let Some(branch_name) = line.strip_prefix(">>>>>>> ") - && conflict_start.is_some() - && ours_start.is_some() - && ours_end.is_some() - && theirs_start.is_some() - { - let branch_name = branch_name.trim(); - if !branch_name.is_empty() { - theirs_branch_name = Some(SharedString::new(branch_name)); - } - - let theirs_end = line_pos; - let conflict_end = (line_end + 1).min(buffer_len); - - let range = buffer.anchor_after(conflict_start.unwrap()) - ..buffer.anchor_before(conflict_end); - let ours = buffer.anchor_after(ours_start.unwrap()) - ..buffer.anchor_before(ours_end.unwrap()); - let theirs = - buffer.anchor_after(theirs_start.unwrap())..buffer.anchor_before(theirs_end); - - let base = base_start - .zip(base_end) - .map(|(start, end)| buffer.anchor_after(start)..buffer.anchor_before(end)); - - conflicts.push(ConflictRegion { - ours_branch_name: ours_branch_name - .take() - .unwrap_or_else(|| SharedString::new_static("HEAD")), - theirs_branch_name: theirs_branch_name - .take() - .unwrap_or_else(|| SharedString::new_static("Origin")), - range, - ours, - theirs, - base, - }); - - conflict_start = None; - ours_start = None; - ours_end = None; - base_start = None; - base_end = None; - theirs_start = None; - } - - line_pos = line_end + 1; - } - - ConflictSetSnapshot { - conflicts: conflicts.into(), - buffer_id: buffer.remote_id(), - } - } -} - -impl EventEmitter for ConflictSet {} - -#[cfg(test)] -mod tests { - use std::sync::mpsc; - - use crate::Project; - - use super::*; - use fs::FakeFs; - use git::{ - repository::{RepoPath, repo_path}, - status::{UnmergedStatus, UnmergedStatusCode}, - }; - use gpui::{BackgroundExecutor, TestAppContext}; - use serde_json::json; - use text::{Buffer, BufferId, Point, ReplicaId, ToOffset as _}; - use unindent::Unindent as _; - use util::{path, rel_path::rel_path}; - - #[test] - fn test_parse_conflicts_in_buffer() { - // Create a buffer with conflict markers - let test_content = r#" - This is some text before the conflict. - <<<<<<< HEAD - This is our version - ======= - This is their version - >>>>>>> branch-name - - Another conflict: - <<<<<<< HEAD - Our second change - ||||||| merged common ancestors - Original content - ======= - Their second change - >>>>>>> branch-name - "# - .unindent(); - - let buffer_id = BufferId::new(1).unwrap(); - let buffer = Buffer::new(ReplicaId::LOCAL, buffer_id, test_content); - let snapshot = buffer.snapshot(); - - let conflict_snapshot = ConflictSet::parse(&snapshot); - assert_eq!(conflict_snapshot.conflicts.len(), 2); - - let first = &conflict_snapshot.conflicts[0]; - assert!(first.base.is_none()); - assert_eq!(first.ours_branch_name.as_ref(), "HEAD"); - assert_eq!(first.theirs_branch_name.as_ref(), "branch-name"); - let our_text = snapshot - .text_for_range(first.ours.clone()) - .collect::(); - let their_text = snapshot - .text_for_range(first.theirs.clone()) - .collect::(); - assert_eq!(our_text, "This is our version\n"); - assert_eq!(their_text, "This is their version\n"); - - let second = &conflict_snapshot.conflicts[1]; - assert!(second.base.is_some()); - assert_eq!(second.ours_branch_name.as_ref(), "HEAD"); - assert_eq!(second.theirs_branch_name.as_ref(), "branch-name"); - let our_text = snapshot - .text_for_range(second.ours.clone()) - .collect::(); - let their_text = snapshot - .text_for_range(second.theirs.clone()) - .collect::(); - let base_text = snapshot - .text_for_range(second.base.as_ref().unwrap().clone()) - .collect::(); - assert_eq!(our_text, "Our second change\n"); - assert_eq!(their_text, "Their second change\n"); - assert_eq!(base_text, "Original content\n"); - - // Test conflicts_in_range - let range = snapshot.anchor_before(0)..snapshot.anchor_before(snapshot.len()); - let conflicts_in_range = conflict_snapshot.conflicts_in_range(range, &snapshot); - assert_eq!(conflicts_in_range.len(), 2); - - // Test with a range that includes only the first conflict - let first_conflict_end = conflict_snapshot.conflicts[0].range.end; - let range = snapshot.anchor_before(0)..first_conflict_end; - let conflicts_in_range = conflict_snapshot.conflicts_in_range(range, &snapshot); - assert_eq!(conflicts_in_range.len(), 1); - - // Test with a range that includes only the second conflict - let second_conflict_start = conflict_snapshot.conflicts[1].range.start; - let range = second_conflict_start..snapshot.anchor_before(snapshot.len()); - let conflicts_in_range = conflict_snapshot.conflicts_in_range(range, &snapshot); - assert_eq!(conflicts_in_range.len(), 1); - - // Test with a range that doesn't include any conflicts - let range = buffer.anchor_after(first_conflict_end.to_next_offset(&buffer)) - ..buffer.anchor_before(second_conflict_start.to_previous_offset(&buffer)); - let conflicts_in_range = conflict_snapshot.conflicts_in_range(range, &snapshot); - assert_eq!(conflicts_in_range.len(), 0); - } - - #[test] - fn test_nested_conflict_markers() { - // Create a buffer with nested conflict markers - let test_content = r#" - This is some text before the conflict. - <<<<<<< HEAD - This is our version - <<<<<<< HEAD - This is a nested conflict marker - ======= - This is their version in a nested conflict - >>>>>>> branch-nested - ======= - This is their version - >>>>>>> branch-name - "# - .unindent(); - - let buffer_id = BufferId::new(1).unwrap(); - let buffer = Buffer::new(ReplicaId::LOCAL, buffer_id, test_content); - let snapshot = buffer.snapshot(); - - let conflict_snapshot = ConflictSet::parse(&snapshot); - - assert_eq!(conflict_snapshot.conflicts.len(), 1); - - // The conflict should have our version, their version, but no base - let conflict = &conflict_snapshot.conflicts[0]; - assert!(conflict.base.is_none()); - assert_eq!(conflict.ours_branch_name.as_ref(), "HEAD"); - assert_eq!(conflict.theirs_branch_name.as_ref(), "branch-nested"); - - // Check that the nested conflict was detected correctly - let our_text = snapshot - .text_for_range(conflict.ours.clone()) - .collect::(); - assert_eq!(our_text, "This is a nested conflict marker\n"); - let their_text = snapshot - .text_for_range(conflict.theirs.clone()) - .collect::(); - assert_eq!(their_text, "This is their version in a nested conflict\n"); - } - - #[test] - fn test_conflict_markers_at_eof() { - let test_content = r#" - <<<<<<< ours - ======= - This is their version - >>>>>>> "# - .unindent(); - let buffer_id = BufferId::new(1).unwrap(); - let buffer = Buffer::new(ReplicaId::LOCAL, buffer_id, test_content); - let snapshot = buffer.snapshot(); - - let conflict_snapshot = ConflictSet::parse(&snapshot); - assert_eq!(conflict_snapshot.conflicts.len(), 1); - assert_eq!( - conflict_snapshot.conflicts[0].ours_branch_name.as_ref(), - "ours" - ); - assert_eq!( - conflict_snapshot.conflicts[0].theirs_branch_name.as_ref(), - "Origin" // default branch name if there is none - ); - } - - #[test] - fn test_conflicts_in_range() { - // Create a buffer with conflict markers - let test_content = r#" - one - <<<<<<< HEAD1 - two - ======= - three - >>>>>>> branch1 - four - five - <<<<<<< HEAD2 - six - ======= - seven - >>>>>>> branch2 - eight - nine - <<<<<<< HEAD3 - ten - ======= - eleven - >>>>>>> branch3 - twelve - <<<<<<< HEAD4 - thirteen - ======= - fourteen - >>>>>>> branch4 - fifteen - "# - .unindent(); - - let buffer_id = BufferId::new(1).unwrap(); - let buffer = Buffer::new(ReplicaId::LOCAL, buffer_id, test_content.clone()); - let snapshot = buffer.snapshot(); - - let conflict_snapshot = ConflictSet::parse(&snapshot); - assert_eq!(conflict_snapshot.conflicts.len(), 4); - assert_eq!( - conflict_snapshot.conflicts[0].ours_branch_name.as_ref(), - "HEAD1" - ); - assert_eq!( - conflict_snapshot.conflicts[0].theirs_branch_name.as_ref(), - "branch1" - ); - assert_eq!( - conflict_snapshot.conflicts[1].ours_branch_name.as_ref(), - "HEAD2" - ); - assert_eq!( - conflict_snapshot.conflicts[1].theirs_branch_name.as_ref(), - "branch2" - ); - assert_eq!( - conflict_snapshot.conflicts[2].ours_branch_name.as_ref(), - "HEAD3" - ); - assert_eq!( - conflict_snapshot.conflicts[2].theirs_branch_name.as_ref(), - "branch3" - ); - assert_eq!( - conflict_snapshot.conflicts[3].ours_branch_name.as_ref(), - "HEAD4" - ); - assert_eq!( - conflict_snapshot.conflicts[3].theirs_branch_name.as_ref(), - "branch4" - ); - - let range = test_content.find("seven").unwrap()..test_content.find("eleven").unwrap(); - let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end); - assert_eq!( - conflict_snapshot.conflicts_in_range(range, &snapshot), - &conflict_snapshot.conflicts[1..=2] - ); - - let range = test_content.find("one").unwrap()..test_content.find("<<<<<<< HEAD2").unwrap(); - let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end); - assert_eq!( - conflict_snapshot.conflicts_in_range(range, &snapshot), - &conflict_snapshot.conflicts[0..=1] - ); - - let range = - test_content.find("eight").unwrap() - 1..test_content.find(">>>>>>> branch3").unwrap(); - let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end); - assert_eq!( - conflict_snapshot.conflicts_in_range(range, &snapshot), - &conflict_snapshot.conflicts[1..=2] - ); - - let range = test_content.find("thirteen").unwrap() - 1..test_content.len(); - let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end); - assert_eq!( - conflict_snapshot.conflicts_in_range(range, &snapshot), - &conflict_snapshot.conflicts[3..=3] - ); - } - - #[gpui::test] - async fn test_conflict_updates(executor: BackgroundExecutor, cx: &mut TestAppContext) { - zlog::init_test(); - cx.update(|cx| { - settings::init(cx); - }); - let initial_text = " - one - two - three - four - five - " - .unindent(); - let fs = FakeFs::new(executor); - fs.insert_tree( - path!("/project"), - json!({ - ".git": {}, - "a.txt": initial_text, - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - let (git_store, buffer) = project.update(cx, |project, cx| { - ( - project.git_store().clone(), - project.open_local_buffer(path!("/project/a.txt"), cx), - ) - }); - let buffer = buffer.await.unwrap(); - let conflict_set = git_store.update(cx, |git_store, cx| { - git_store.open_conflict_set(buffer.clone(), cx) - }); - let (events_tx, events_rx) = mpsc::channel::(); - let _conflict_set_subscription = cx.update(|cx| { - cx.subscribe(&conflict_set, move |_, event, _| { - events_tx.send(event.clone()).ok(); - }) - }); - let conflicts_snapshot = - conflict_set.read_with(cx, |conflict_set, _| conflict_set.snapshot()); - assert!(conflicts_snapshot.conflicts.is_empty()); - - buffer.update(cx, |buffer, cx| { - buffer.edit( - [ - (4..4, "<<<<<<< HEAD\n"), - (14..14, "=======\nTWO\n>>>>>>> branch\n"), - ], - None, - cx, - ); - }); - - cx.run_until_parked(); - events_rx.try_recv().expect_err( - "no conflicts should be registered as long as the file's status is unchanged", - ); - - fs.with_git_state(path!("/project/.git").as_ref(), true, |state| { - state.unmerged_paths.insert( - repo_path("a.txt"), - UnmergedStatus { - first_head: UnmergedStatusCode::Updated, - second_head: UnmergedStatusCode::Updated, - }, - ); - // Cause the repository to emit MergeHeadsChanged. - state.refs.insert("MERGE_HEAD".into(), "123".into()) - }) - .unwrap(); - - cx.run_until_parked(); - let update = events_rx - .try_recv() - .expect("status change should trigger conflict parsing"); - assert_eq!(update.old_range, 0..0); - assert_eq!(update.new_range, 0..1); - - let conflict = conflict_set.read_with(cx, |conflict_set, _| { - conflict_set.snapshot().conflicts[0].clone() - }); - cx.update(|cx| { - conflict.resolve(buffer.clone(), std::slice::from_ref(&conflict.theirs), cx); - }); - - cx.run_until_parked(); - let update = events_rx - .try_recv() - .expect("conflicts should be removed after resolution"); - assert_eq!(update.old_range, 0..1); - assert_eq!(update.new_range, 0..0); - } - - #[gpui::test] - async fn test_conflict_updates_without_merge_head( - executor: BackgroundExecutor, - cx: &mut TestAppContext, - ) { - zlog::init_test(); - cx.update(|cx| { - settings::init(cx); - }); - - let initial_text = " - zero - <<<<<<< HEAD - one - ======= - two - >>>>>>> Stashed Changes - three - " - .unindent(); - - let fs = FakeFs::new(executor); - fs.insert_tree( - path!("/project"), - json!({ - ".git": {}, - "a.txt": initial_text, - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await; - let (git_store, buffer) = project.update(cx, |project, cx| { - ( - project.git_store().clone(), - project.open_local_buffer(path!("/project/a.txt"), cx), - ) - }); - - cx.run_until_parked(); - fs.with_git_state(path!("/project/.git").as_ref(), true, |state| { - state.unmerged_paths.insert( - RepoPath::from_rel_path(rel_path("a.txt")), - UnmergedStatus { - first_head: UnmergedStatusCode::Updated, - second_head: UnmergedStatusCode::Updated, - }, - ) - }) - .unwrap(); - - let buffer = buffer.await.unwrap(); - - // Open the conflict set for a file that currently has conflicts. - let conflict_set = git_store.update(cx, |git_store, cx| { - git_store.open_conflict_set(buffer.clone(), cx) - }); - - cx.run_until_parked(); - conflict_set.update(cx, |conflict_set, cx| { - let conflict_range = conflict_set.snapshot().conflicts[0] - .range - .to_point(buffer.read(cx)); - assert_eq!(conflict_range, Point::new(1, 0)..Point::new(6, 0)); - }); - - // Simulate the conflict being removed by e.g. staging the file. - fs.with_git_state(path!("/project/.git").as_ref(), true, |state| { - state.unmerged_paths.remove(&repo_path("a.txt")) - }) - .unwrap(); - - cx.run_until_parked(); - conflict_set.update(cx, |conflict_set, _| { - assert!(!conflict_set.has_conflict); - assert_eq!(conflict_set.snapshot.conflicts.len(), 0); - }); - - // Simulate the conflict being re-added. - fs.with_git_state(path!("/project/.git").as_ref(), true, |state| { - state.unmerged_paths.insert( - repo_path("a.txt"), - UnmergedStatus { - first_head: UnmergedStatusCode::Updated, - second_head: UnmergedStatusCode::Updated, - }, - ) - }) - .unwrap(); - - cx.run_until_parked(); - conflict_set.update(cx, |conflict_set, cx| { - let conflict_range = conflict_set.snapshot().conflicts[0] - .range - .to_point(buffer.read(cx)); - assert_eq!(conflict_range, Point::new(1, 0)..Point::new(6, 0)); - }); - } -} diff --git a/crates/project/src/git_store/git_traversal.rs b/crates/project/src/git_store/git_traversal.rs deleted file mode 100644 index 39857951ad..0000000000 --- a/crates/project/src/git_store/git_traversal.rs +++ /dev/null @@ -1,793 +0,0 @@ -use collections::HashMap; -use git::{repository::RepoPath, status::GitSummary}; -use std::{collections::BTreeMap, ops::Deref, path::Path}; -use sum_tree::Cursor; -use text::Bias; -use util::rel_path::RelPath; -use worktree::{Entry, PathProgress, PathTarget, Traversal}; - -use super::{RepositoryId, RepositorySnapshot, StatusEntry}; - -/// Walks the worktree entries and their associated git statuses. -pub struct GitTraversal<'a> { - traversal: Traversal<'a>, - current_entry_summary: Option, - repo_root_to_snapshot: BTreeMap<&'a Path, &'a RepositorySnapshot>, - repo_location: Option<( - RepositoryId, - Cursor<'a, 'static, StatusEntry, PathProgress<'a>>, - )>, -} - -impl<'a> GitTraversal<'a> { - pub fn new( - repo_snapshots: &'a HashMap, - traversal: Traversal<'a>, - ) -> GitTraversal<'a> { - let repo_root_to_snapshot = repo_snapshots - .values() - .map(|snapshot| (&*snapshot.work_directory_abs_path, snapshot)) - .collect(); - let mut this = GitTraversal { - traversal, - current_entry_summary: None, - repo_location: None, - repo_root_to_snapshot, - }; - this.synchronize_statuses(true); - this - } - - fn repo_root_for_path(&self, path: &Path) -> Option<(&'a RepositorySnapshot, RepoPath)> { - // We might need to perform a range search multiple times, as there may be a nested repository inbetween - // the target and our path. E.g: - // /our_root_repo/ - // .git/ - // other_repo/ - // .git/ - // our_query.txt - let query = path.ancestors(); - for query in query { - let (_, snapshot) = self - .repo_root_to_snapshot - .range(Path::new("")..=query) - .last()?; - - let stripped = snapshot - .abs_path_to_repo_path(path) - .map(|repo_path| (*snapshot, repo_path)); - if stripped.is_some() { - return stripped; - } - } - - None - } - - fn synchronize_statuses(&mut self, reset: bool) { - self.current_entry_summary = None; - - let Some(entry) = self.entry() else { - return; - }; - - let abs_path = self.traversal.snapshot().absolutize(&entry.path); - - let Some((repo, repo_path)) = self.repo_root_for_path(&abs_path) else { - self.repo_location = None; - return; - }; - - // Update our state if we changed repositories. - if reset - || self - .repo_location - .as_ref() - .map(|(prev_repo_id, _)| *prev_repo_id) - != Some(repo.id) - { - self.repo_location = Some((repo.id, repo.statuses_by_path.cursor::(()))); - } - - let Some((_, statuses)) = &mut self.repo_location else { - return; - }; - - if entry.is_dir() { - let mut statuses = statuses.clone(); - statuses.seek_forward(&PathTarget::Path(&repo_path), Bias::Left); - let summary = statuses.summary(&PathTarget::Successor(&repo_path), Bias::Left); - - self.current_entry_summary = Some(summary); - } else if entry.is_file() { - // For a file entry, park the cursor on the corresponding status - if statuses.seek_forward(&PathTarget::Path(&repo_path), Bias::Left) { - // TODO: Investigate statuses.item() being None here. - self.current_entry_summary = statuses.item().map(|item| item.status.into()); - } else { - self.current_entry_summary = Some(GitSummary::UNCHANGED); - } - } - } - - pub fn advance(&mut self) -> bool { - let found = self.traversal.advance_by(1); - self.synchronize_statuses(false); - found - } - - pub fn advance_to_sibling(&mut self) -> bool { - let found = self.traversal.advance_to_sibling(); - self.synchronize_statuses(false); - found - } - - pub fn back_to_parent(&mut self) -> bool { - let found = self.traversal.back_to_parent(); - self.synchronize_statuses(true); - found - } - - pub fn start_offset(&self) -> usize { - self.traversal.start_offset() - } - - pub fn end_offset(&self) -> usize { - self.traversal.end_offset() - } - - pub fn entry(&self) -> Option> { - let entry = self.traversal.entry()?; - let git_summary = self.current_entry_summary.unwrap_or(GitSummary::UNCHANGED); - Some(GitEntryRef { entry, git_summary }) - } -} - -impl<'a> Iterator for GitTraversal<'a> { - type Item = GitEntryRef<'a>; - - fn next(&mut self) -> Option { - if let Some(item) = self.entry() { - self.advance(); - Some(item) - } else { - None - } - } -} - -pub struct ChildEntriesGitIter<'a> { - parent_path: &'a RelPath, - traversal: GitTraversal<'a>, -} - -impl<'a> ChildEntriesGitIter<'a> { - pub fn new( - repo_snapshots: &'a HashMap, - worktree_snapshot: &'a worktree::Snapshot, - parent_path: &'a RelPath, - ) -> Self { - let mut traversal = GitTraversal::new( - repo_snapshots, - worktree_snapshot.traverse_from_path(true, true, true, parent_path), - ); - traversal.advance(); - ChildEntriesGitIter { - parent_path, - traversal, - } - } -} - -impl<'a> Iterator for ChildEntriesGitIter<'a> { - type Item = GitEntryRef<'a>; - - fn next(&mut self) -> Option { - if let Some(item) = self.traversal.entry() - && item.path.starts_with(self.parent_path) - { - self.traversal.advance_to_sibling(); - return Some(item); - } - None - } -} - -#[derive(Debug, Clone, Copy)] -pub struct GitEntryRef<'a> { - pub entry: &'a Entry, - pub git_summary: GitSummary, -} - -impl GitEntryRef<'_> { - pub fn to_owned(self) -> GitEntry { - GitEntry { - entry: self.entry.clone(), - git_summary: self.git_summary, - } - } -} - -impl Deref for GitEntryRef<'_> { - type Target = Entry; - - fn deref(&self) -> &Self::Target { - self.entry - } -} - -impl AsRef for GitEntryRef<'_> { - fn as_ref(&self) -> &Entry { - self.entry - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct GitEntry { - pub entry: Entry, - pub git_summary: GitSummary, -} - -impl GitEntry { - pub fn to_ref(&self) -> GitEntryRef<'_> { - GitEntryRef { - entry: &self.entry, - git_summary: self.git_summary, - } - } -} - -impl Deref for GitEntry { - type Target = Entry; - - fn deref(&self) -> &Self::Target { - &self.entry - } -} - -impl AsRef for GitEntry { - fn as_ref(&self) -> &Entry { - &self.entry - } -} - -#[cfg(test)] -mod tests { - use std::time::Duration; - - use crate::Project; - - use super::*; - use fs::FakeFs; - use git::status::{FileStatus, StatusCode, TrackedSummary, UnmergedStatus, UnmergedStatusCode}; - use gpui::TestAppContext; - use serde_json::json; - use settings::SettingsStore; - use util::{path, rel_path::rel_path}; - - const CONFLICT: FileStatus = FileStatus::Unmerged(UnmergedStatus { - first_head: UnmergedStatusCode::Updated, - second_head: UnmergedStatusCode::Updated, - }); - const ADDED: GitSummary = GitSummary { - index: TrackedSummary::ADDED, - count: 1, - ..GitSummary::UNCHANGED - }; - const MODIFIED: GitSummary = GitSummary { - index: TrackedSummary::MODIFIED, - count: 1, - ..GitSummary::UNCHANGED - }; - - #[gpui::test] - async fn test_git_traversal_with_one_repo(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/root"), - json!({ - "x": { - ".git": {}, - "x1.txt": "foo", - "x2.txt": "bar", - "y": { - ".git": {}, - "y1.txt": "baz", - "y2.txt": "qux" - }, - "z.txt": "sneaky..." - }, - "z": { - ".git": {}, - "z1.txt": "quux", - "z2.txt": "quuux" - } - }), - ) - .await; - - fs.set_status_for_repo( - Path::new(path!("/root/x/.git")), - &[ - ("x2.txt", StatusCode::Modified.index()), - ("z.txt", StatusCode::Added.index()), - ], - ); - fs.set_status_for_repo(Path::new(path!("/root/x/y/.git")), &[("y1.txt", CONFLICT)]); - fs.set_status_for_repo( - Path::new(path!("/root/z/.git")), - &[("z2.txt", StatusCode::Added.index())], - ); - - let project = Project::test(fs, [path!("/root").as_ref()], cx).await; - cx.executor().run_until_parked(); - - let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| { - ( - project.git_store().read(cx).repo_snapshots(cx), - project.worktrees(cx).next().unwrap().read(cx).snapshot(), - ) - }); - - let traversal = GitTraversal::new( - &repo_snapshots, - worktree_snapshot.traverse_from_path(true, false, true, RelPath::unix("x").unwrap()), - ); - let entries = traversal - .map(|entry| (entry.path.clone(), entry.git_summary)) - .collect::>(); - pretty_assertions::assert_eq!( - entries, - [ - (rel_path("x/x1.txt").into(), GitSummary::UNCHANGED), - (rel_path("x/x2.txt").into(), MODIFIED), - (rel_path("x/y/y1.txt").into(), GitSummary::CONFLICT), - (rel_path("x/y/y2.txt").into(), GitSummary::UNCHANGED), - (rel_path("x/z.txt").into(), ADDED), - (rel_path("z/z1.txt").into(), GitSummary::UNCHANGED), - (rel_path("z/z2.txt").into(), ADDED), - ] - ) - } - - #[gpui::test] - async fn test_git_traversal_with_nested_repos(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/root"), - json!({ - "x": { - ".git": {}, - "x1.txt": "foo", - "x2.txt": "bar", - "y": { - ".git": {}, - "y1.txt": "baz", - "y2.txt": "qux" - }, - "z.txt": "sneaky..." - }, - "z": { - ".git": {}, - "z1.txt": "quux", - "z2.txt": "quuux" - } - }), - ) - .await; - - fs.set_status_for_repo( - Path::new(path!("/root/x/.git")), - &[ - ("x2.txt", StatusCode::Modified.index()), - ("z.txt", StatusCode::Added.index()), - ], - ); - fs.set_status_for_repo(Path::new(path!("/root/x/y/.git")), &[("y1.txt", CONFLICT)]); - - fs.set_status_for_repo( - Path::new(path!("/root/z/.git")), - &[("z2.txt", StatusCode::Added.index())], - ); - - let project = Project::test(fs, [path!("/root").as_ref()], cx).await; - cx.executor().run_until_parked(); - - let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| { - ( - project.git_store().read(cx).repo_snapshots(cx), - project.worktrees(cx).next().unwrap().read(cx).snapshot(), - ) - }); - - // Sanity check the propagation for x/y and z - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("x/y", GitSummary::CONFLICT), - ("x/y/y1.txt", GitSummary::CONFLICT), - ("x/y/y2.txt", GitSummary::UNCHANGED), - ], - ); - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("z", ADDED), - ("z/z1.txt", GitSummary::UNCHANGED), - ("z/z2.txt", ADDED), - ], - ); - - // Test one of the fundamental cases of propagation blocking, the transition from one git repository to another - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("x", MODIFIED + ADDED), - ("x/y", GitSummary::CONFLICT), - ("x/y/y1.txt", GitSummary::CONFLICT), - ], - ); - - // Sanity check everything around it - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("x", MODIFIED + ADDED), - ("x/x1.txt", GitSummary::UNCHANGED), - ("x/x2.txt", MODIFIED), - ("x/y", GitSummary::CONFLICT), - ("x/y/y1.txt", GitSummary::CONFLICT), - ("x/y/y2.txt", GitSummary::UNCHANGED), - ("x/z.txt", ADDED), - ], - ); - - // Test the other fundamental case, transitioning from git repository to non-git repository - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("", GitSummary::UNCHANGED), - ("x", MODIFIED + ADDED), - ("x/x1.txt", GitSummary::UNCHANGED), - ], - ); - - // And all together now - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("", GitSummary::UNCHANGED), - ("x", MODIFIED + ADDED), - ("x/x1.txt", GitSummary::UNCHANGED), - ("x/x2.txt", MODIFIED), - ("x/y", GitSummary::CONFLICT), - ("x/y/y1.txt", GitSummary::CONFLICT), - ("x/y/y2.txt", GitSummary::UNCHANGED), - ("x/z.txt", ADDED), - ("z", ADDED), - ("z/z1.txt", GitSummary::UNCHANGED), - ("z/z2.txt", ADDED), - ], - ); - } - - #[gpui::test] - async fn test_git_traversal_simple(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/root"), - json!({ - ".git": {}, - "a": { - "b": { - "c1.txt": "", - "c2.txt": "", - }, - "d": { - "e1.txt": "", - "e2.txt": "", - "e3.txt": "", - } - }, - "f": { - "no-status.txt": "" - }, - "g": { - "h1.txt": "", - "h2.txt": "" - }, - }), - ) - .await; - - fs.set_status_for_repo( - Path::new(path!("/root/.git")), - &[ - ("a/b/c1.txt", StatusCode::Added.index()), - ("a/d/e2.txt", StatusCode::Modified.index()), - ("g/h2.txt", CONFLICT), - ], - ); - - let project = Project::test(fs, [path!("/root").as_ref()], cx).await; - cx.executor().run_until_parked(); - - let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| { - ( - project.git_store().read(cx).repo_snapshots(cx), - project.worktrees(cx).next().unwrap().read(cx).snapshot(), - ) - }); - - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("", GitSummary::CONFLICT + MODIFIED + ADDED), - ("g", GitSummary::CONFLICT), - ("g/h2.txt", GitSummary::CONFLICT), - ], - ); - - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("", GitSummary::CONFLICT + ADDED + MODIFIED), - ("a", ADDED + MODIFIED), - ("a/b", ADDED), - ("a/b/c1.txt", ADDED), - ("a/b/c2.txt", GitSummary::UNCHANGED), - ("a/d", MODIFIED), - ("a/d/e2.txt", MODIFIED), - ("f", GitSummary::UNCHANGED), - ("f/no-status.txt", GitSummary::UNCHANGED), - ("g", GitSummary::CONFLICT), - ("g/h2.txt", GitSummary::CONFLICT), - ], - ); - - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("a/b", ADDED), - ("a/b/c1.txt", ADDED), - ("a/b/c2.txt", GitSummary::UNCHANGED), - ("a/d", MODIFIED), - ("a/d/e1.txt", GitSummary::UNCHANGED), - ("a/d/e2.txt", MODIFIED), - ("f", GitSummary::UNCHANGED), - ("f/no-status.txt", GitSummary::UNCHANGED), - ("g", GitSummary::CONFLICT), - ], - ); - - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("a/b/c1.txt", ADDED), - ("a/b/c2.txt", GitSummary::UNCHANGED), - ("a/d/e1.txt", GitSummary::UNCHANGED), - ("a/d/e2.txt", MODIFIED), - ("f/no-status.txt", GitSummary::UNCHANGED), - ], - ); - } - - #[gpui::test] - async fn test_git_traversal_with_repos_under_project(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/root"), - json!({ - "x": { - ".git": {}, - "x1.txt": "foo", - "x2.txt": "bar" - }, - "y": { - ".git": {}, - "y1.txt": "baz", - "y2.txt": "qux" - }, - "z": { - ".git": {}, - "z1.txt": "quux", - "z2.txt": "quuux" - } - }), - ) - .await; - - fs.set_status_for_repo( - Path::new(path!("/root/x/.git")), - &[("x1.txt", StatusCode::Added.index())], - ); - fs.set_status_for_repo( - Path::new(path!("/root/y/.git")), - &[ - ("y1.txt", CONFLICT), - ("y2.txt", StatusCode::Modified.index()), - ], - ); - fs.set_status_for_repo( - Path::new(path!("/root/z/.git")), - &[("z2.txt", StatusCode::Modified.index())], - ); - - let project = Project::test(fs, [path!("/root").as_ref()], cx).await; - cx.executor().run_until_parked(); - - let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| { - ( - project.git_store().read(cx).repo_snapshots(cx), - project.worktrees(cx).next().unwrap().read(cx).snapshot(), - ) - }); - - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[("x", ADDED), ("x/x1.txt", ADDED)], - ); - - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("y", GitSummary::CONFLICT + MODIFIED), - ("y/y1.txt", GitSummary::CONFLICT), - ("y/y2.txt", MODIFIED), - ], - ); - - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[("z", MODIFIED), ("z/z2.txt", MODIFIED)], - ); - - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[("x", ADDED), ("x/x1.txt", ADDED)], - ); - - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("x", ADDED), - ("x/x1.txt", ADDED), - ("x/x2.txt", GitSummary::UNCHANGED), - ("y", GitSummary::CONFLICT + MODIFIED), - ("y/y1.txt", GitSummary::CONFLICT), - ("y/y2.txt", MODIFIED), - ("z", MODIFIED), - ("z/z1.txt", GitSummary::UNCHANGED), - ("z/z2.txt", MODIFIED), - ], - ); - } - - fn init_test(cx: &mut gpui::TestAppContext) { - zlog::init_test(); - - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - }); - } - - #[gpui::test] - async fn test_bump_mtime_of_git_repo_workdir(cx: &mut TestAppContext) { - init_test(cx); - - // Create a worktree with a git directory. - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/root"), - json!({ - ".git": {}, - "a.txt": "", - "b": { - "c.txt": "", - }, - }), - ) - .await; - fs.set_head_and_index_for_repo( - path!("/root/.git").as_ref(), - &[("a.txt", "".into()), ("b/c.txt", "".into())], - ); - cx.run_until_parked(); - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - cx.executor().run_until_parked(); - - let (old_entry_ids, old_mtimes) = project.read_with(cx, |project, cx| { - let tree = project.worktrees(cx).next().unwrap().read(cx); - ( - tree.entries(true, 0).map(|e| e.id).collect::>(), - tree.entries(true, 0).map(|e| e.mtime).collect::>(), - ) - }); - - // Regression test: after the directory is scanned, touch the git repo's - // working directory, bumping its mtime. That directory keeps its project - // entry id after the directories are re-scanned. - fs.touch_path(path!("/root")).await; - cx.executor().run_until_parked(); - - let (new_entry_ids, new_mtimes) = project.read_with(cx, |project, cx| { - let tree = project.worktrees(cx).next().unwrap().read(cx); - ( - tree.entries(true, 0).map(|e| e.id).collect::>(), - tree.entries(true, 0).map(|e| e.mtime).collect::>(), - ) - }); - assert_eq!(new_entry_ids, old_entry_ids); - assert_ne!(new_mtimes, old_mtimes); - - // Regression test: changes to the git repository should still be - // detected. - fs.set_head_for_repo( - path!("/root/.git").as_ref(), - &[("a.txt", "".into()), ("b/c.txt", "something-else".into())], - "deadbeef", - ); - cx.executor().run_until_parked(); - cx.executor().advance_clock(Duration::from_secs(1)); - - let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| { - ( - project.git_store().read(cx).repo_snapshots(cx), - project.worktrees(cx).next().unwrap().read(cx).snapshot(), - ) - }); - - check_git_statuses( - &repo_snapshots, - &worktree_snapshot, - &[ - ("", MODIFIED), - ("a.txt", GitSummary::UNCHANGED), - ("b/c.txt", MODIFIED), - ], - ); - } - - #[track_caller] - fn check_git_statuses( - repo_snapshots: &HashMap, - worktree_snapshot: &worktree::Snapshot, - expected_statuses: &[(&str, GitSummary)], - ) { - let mut traversal = GitTraversal::new( - repo_snapshots, - worktree_snapshot.traverse_from_path(true, true, false, RelPath::empty()), - ); - let found_statuses = expected_statuses - .iter() - .map(|&(path, _)| { - let git_entry = traversal - .find(|git_entry| git_entry.path.as_ref() == rel_path(path)) - .unwrap_or_else(|| panic!("Traversal has no entry for {path:?}")); - (path, git_entry.git_summary) - }) - .collect::>(); - pretty_assertions::assert_eq!(found_statuses, expected_statuses); - } -} diff --git a/crates/project/src/git_store/pending_op.rs b/crates/project/src/git_store/pending_op.rs deleted file mode 100644 index 1991eed407..0000000000 --- a/crates/project/src/git_store/pending_op.rs +++ /dev/null @@ -1,147 +0,0 @@ -use git::repository::RepoPath; -use std::ops::Add; -use sum_tree::{ContextLessSummary, Item, KeyedItem}; -use worktree::{PathKey, PathSummary}; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum GitStatus { - Staged, - Unstaged, - Reverted, - Unchanged, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum JobStatus { - Running, - Finished, - Skipped, - Error, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct PendingOps { - pub repo_path: RepoPath, - pub ops: Vec, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct PendingOp { - pub id: PendingOpId, - pub git_status: GitStatus, - pub job_status: JobStatus, -} - -#[derive(Clone, Debug)] -pub struct PendingOpsSummary { - pub staged_count: usize, - pub staging_count: usize, -} - -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub struct PendingOpId(pub u16); - -impl Item for PendingOps { - type Summary = PathSummary; - - fn summary(&self, _cx: ()) -> Self::Summary { - PathSummary { - max_path: self.repo_path.as_ref().clone(), - item_summary: PendingOpsSummary { - staged_count: self.staged() as usize, - staging_count: self.staging() as usize, - }, - } - } -} - -impl ContextLessSummary for PendingOpsSummary { - fn zero() -> Self { - Self { - staged_count: 0, - staging_count: 0, - } - } - - fn add_summary(&mut self, summary: &Self) { - self.staged_count += summary.staged_count; - self.staging_count += summary.staging_count; - } -} - -impl KeyedItem for PendingOps { - type Key = PathKey; - - fn key(&self) -> Self::Key { - PathKey(self.repo_path.as_ref().clone()) - } -} - -impl Add for PendingOpId { - type Output = PendingOpId; - - fn add(self, rhs: u16) -> Self::Output { - Self(self.0 + rhs) - } -} - -impl From for PendingOpId { - fn from(id: u16) -> Self { - Self(id) - } -} - -impl PendingOps { - pub fn new(path: &RepoPath) -> Self { - Self { - repo_path: path.clone(), - ops: Vec::new(), - } - } - - pub fn max_id(&self) -> PendingOpId { - self.ops.last().map(|op| op.id).unwrap_or_default() - } - - pub fn op_by_id(&self, id: PendingOpId) -> Option<&PendingOp> { - self.ops.iter().find(|op| op.id == id) - } - - pub fn op_by_id_mut(&mut self, id: PendingOpId) -> Option<&mut PendingOp> { - self.ops.iter_mut().find(|op| op.id == id) - } - - /// File is staged if the last job is finished and has status Staged. - pub fn staged(&self) -> bool { - if let Some(last) = self.ops.last() { - if last.git_status == GitStatus::Staged && last.job_status == JobStatus::Finished { - return true; - } - } - false - } - - /// File is staged if the last job is not finished and has status Staged. - pub fn staging(&self) -> bool { - if let Some(last) = self.ops.last() { - if last.git_status == GitStatus::Staged && last.job_status != JobStatus::Finished { - return true; - } - } - false - } -} - -impl PendingOp { - pub fn running(&self) -> bool { - self.job_status == JobStatus::Running - } - - pub fn finished(&self) -> bool { - matches!(self.job_status, JobStatus::Finished | JobStatus::Skipped) - } - - pub fn error(&self) -> bool { - self.job_status == JobStatus::Error - } -} diff --git a/crates/project/src/image_store.rs b/crates/project/src/image_store.rs deleted file mode 100644 index 71bee30b99..0000000000 --- a/crates/project/src/image_store.rs +++ /dev/null @@ -1,987 +0,0 @@ -use crate::{ - Project, ProjectEntryId, ProjectItem, ProjectPath, - worktree_store::{WorktreeStore, WorktreeStoreEvent}, -}; -use anyhow::{Context as _, Result}; -use collections::{HashMap, HashSet, hash_map}; -use futures::{StreamExt, channel::oneshot}; -use gpui::{ - App, AsyncApp, Context, Entity, EventEmitter, Img, Subscription, Task, WeakEntity, prelude::*, -}; -pub use image::ImageFormat; -use image::{ExtendedColorType, GenericImageView, ImageReader}; -use language::{DiskState, File}; -use rpc::{AnyProtoClient, ErrorExt as _, TypedEnvelope, proto}; -use std::num::NonZeroU64; -use std::path::PathBuf; -use std::sync::Arc; -use util::{ResultExt, rel_path::RelPath}; -use worktree::{LoadedBinaryFile, PathChange, Worktree, WorktreeId}; - -#[derive(Clone, Copy, Debug, Hash, PartialEq, PartialOrd, Ord, Eq)] -pub struct ImageId(NonZeroU64); - -impl ImageId { - pub fn to_proto(&self) -> u64 { - self.0.get() - } -} - -impl std::fmt::Display for ImageId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for ImageId { - fn from(id: NonZeroU64) -> Self { - ImageId(id) - } -} - -#[derive(Debug)] -pub enum ImageItemEvent { - ReloadNeeded, - Reloaded, - FileHandleChanged, - MetadataUpdated, -} - -impl EventEmitter for ImageItem {} - -pub enum ImageStoreEvent { - ImageAdded(Entity), -} - -impl EventEmitter for ImageStore {} - -#[derive(Debug, Clone, Copy)] -pub struct ImageMetadata { - pub width: u32, - pub height: u32, - pub file_size: u64, - pub colors: Option, - pub format: ImageFormat, -} - -#[derive(Debug, Clone, Copy)] -pub struct ImageColorInfo { - pub channels: u8, - pub bits_per_channel: u8, -} - -impl ImageColorInfo { - pub fn from_color_type(color_type: impl Into) -> Option { - let (channels, bits_per_channel) = match color_type.into() { - ExtendedColorType::L8 => (1, 8), - ExtendedColorType::L16 => (1, 16), - ExtendedColorType::La8 => (2, 8), - ExtendedColorType::La16 => (2, 16), - ExtendedColorType::Rgb8 => (3, 8), - ExtendedColorType::Rgb16 => (3, 16), - ExtendedColorType::Rgba8 => (4, 8), - ExtendedColorType::Rgba16 => (4, 16), - ExtendedColorType::A8 => (1, 8), - ExtendedColorType::Bgr8 => (3, 8), - ExtendedColorType::Bgra8 => (4, 8), - ExtendedColorType::Cmyk8 => (4, 8), - _ => return None, - }; - - Some(Self { - channels, - bits_per_channel, - }) - } - - pub const fn bits_per_pixel(&self) -> u8 { - self.channels * self.bits_per_channel - } -} - -pub struct ImageItem { - pub id: ImageId, - pub file: Arc, - pub image: Arc, - reload_task: Option>, - pub image_metadata: Option, -} - -impl ImageItem { - fn compute_metadata_from_bytes(image_bytes: &[u8]) -> Result { - let image_format = image::guess_format(image_bytes)?; - - let mut image_reader = ImageReader::new(std::io::Cursor::new(image_bytes)); - image_reader.set_format(image_format); - let image = image_reader.decode()?; - - let (width, height) = image.dimensions(); - - Ok(ImageMetadata { - width, - height, - file_size: image_bytes.len() as u64, - format: image_format, - colors: ImageColorInfo::from_color_type(image.color()), - }) - } - - pub async fn load_image_metadata( - image: Entity, - project: Entity, - cx: &mut AsyncApp, - ) -> Result { - let (fs, image_path) = cx.update(|cx| { - let fs = project.read(cx).fs().clone(); - let image_path = image - .read(cx) - .abs_path(cx) - .context("absolutizing image file path")?; - anyhow::Ok((fs, image_path)) - })??; - - let image_bytes = fs.load_bytes(&image_path).await?; - Self::compute_metadata_from_bytes(&image_bytes) - } - - pub fn project_path(&self, cx: &App) -> ProjectPath { - ProjectPath { - worktree_id: self.file.worktree_id(cx), - path: self.file.path().clone(), - } - } - - pub fn abs_path(&self, cx: &App) -> Option { - Some(self.file.as_local()?.abs_path(cx)) - } - - fn file_updated(&mut self, new_file: Arc, cx: &mut Context) { - let mut file_changed = false; - - let old_file = &self.file; - if new_file.path() != old_file.path() { - file_changed = true; - } - - let old_state = old_file.disk_state(); - let new_state = new_file.disk_state(); - if old_state != new_state { - file_changed = true; - if matches!(new_state, DiskState::Present { .. }) { - cx.emit(ImageItemEvent::ReloadNeeded) - } - } - - self.file = new_file; - if file_changed { - cx.emit(ImageItemEvent::FileHandleChanged); - cx.notify(); - } - } - - fn reload(&mut self, cx: &mut Context) -> Option> { - let local_file = self.file.as_local()?; - let (tx, rx) = futures::channel::oneshot::channel(); - - let content = local_file.load_bytes(cx); - self.reload_task = Some(cx.spawn(async move |this, cx| { - if let Some(image) = content - .await - .context("Failed to load image content") - .and_then(create_gpui_image) - .log_err() - { - this.update(cx, |this, cx| { - this.image = image; - cx.emit(ImageItemEvent::Reloaded); - }) - .log_err(); - } - _ = tx.send(()); - })); - Some(rx) - } -} - -pub fn is_image_file(project: &Entity, path: &ProjectPath, cx: &App) -> bool { - let ext = util::maybe!({ - let worktree_abs_path = project - .read(cx) - .worktree_for_id(path.worktree_id, cx)? - .read(cx) - .abs_path(); - path.path - .extension() - .or_else(|| worktree_abs_path.extension()?.to_str()) - .map(str::to_lowercase) - }); - - match ext { - Some(ext) => Img::extensions().contains(&ext.as_str()) && !ext.contains("svg"), - None => false, - } -} - -impl ProjectItem for ImageItem { - fn try_open( - project: &Entity, - path: &ProjectPath, - cx: &mut App, - ) -> Option>>> { - if is_image_file(project, path, cx) { - Some(cx.spawn({ - let path = path.clone(); - let project = project.clone(); - async move |cx| { - project - .update(cx, |project, cx| project.open_image(path, cx))? - .await - } - })) - } else { - None - } - } - - fn entry_id(&self, _: &App) -> Option { - self.file.entry_id - } - - fn project_path(&self, cx: &App) -> Option { - Some(self.project_path(cx)) - } - - fn is_dirty(&self) -> bool { - false - } -} - -trait ImageStoreImpl { - fn open_image( - &self, - path: Arc, - worktree: Entity, - cx: &mut Context, - ) -> Task>>; - - fn reload_images( - &self, - images: HashSet>, - cx: &mut Context, - ) -> Task>; - - fn as_local(&self) -> Option>; - fn as_remote(&self) -> Option>; -} - -struct RemoteImageStore { - upstream_client: AnyProtoClient, - project_id: u64, - loading_remote_images_by_id: HashMap, - remote_image_listeners: - HashMap>>>>, - loaded_images: HashMap>, -} - -struct LoadingRemoteImage { - state: proto::ImageState, - chunks: Vec>, - received_size: u64, -} - -struct LocalImageStore { - local_image_ids_by_path: HashMap, - local_image_ids_by_entry_id: HashMap, - image_store: WeakEntity, - _subscription: Subscription, -} - -pub struct ImageStore { - state: Box, - opened_images: HashMap>, - worktree_store: Entity, - #[allow(clippy::type_complexity)] - loading_images_by_path: HashMap< - ProjectPath, - postage::watch::Receiver, Arc>>>, - >, -} - -impl ImageStore { - pub fn local(worktree_store: Entity, cx: &mut Context) -> Self { - let this = cx.weak_entity(); - Self { - state: Box::new(cx.new(|cx| { - let subscription = cx.subscribe( - &worktree_store, - |this: &mut LocalImageStore, _, event, cx| { - if let WorktreeStoreEvent::WorktreeAdded(worktree) = event { - this.subscribe_to_worktree(worktree, cx); - } - }, - ); - - LocalImageStore { - local_image_ids_by_path: Default::default(), - local_image_ids_by_entry_id: Default::default(), - image_store: this, - _subscription: subscription, - } - })), - opened_images: Default::default(), - loading_images_by_path: Default::default(), - worktree_store, - } - } - - pub fn remote( - worktree_store: Entity, - upstream_client: AnyProtoClient, - project_id: u64, - cx: &mut Context, - ) -> Self { - Self { - state: Box::new(cx.new(|_| RemoteImageStore { - upstream_client, - project_id, - loading_remote_images_by_id: Default::default(), - remote_image_listeners: Default::default(), - loaded_images: Default::default(), - })), - opened_images: Default::default(), - loading_images_by_path: Default::default(), - worktree_store, - } - } - - pub fn images(&self) -> impl '_ + Iterator> { - self.opened_images - .values() - .filter_map(|image| image.upgrade()) - } - - pub fn get(&self, image_id: ImageId) -> Option> { - self.opened_images - .get(&image_id) - .and_then(|image| image.upgrade()) - } - - pub fn get_by_path(&self, path: &ProjectPath, cx: &App) -> Option> { - self.images() - .find(|image| &image.read(cx).project_path(cx) == path) - } - - pub fn open_image( - &mut self, - project_path: ProjectPath, - cx: &mut Context, - ) -> Task>> { - let existing_image = self.get_by_path(&project_path, cx); - if let Some(existing_image) = existing_image { - return Task::ready(Ok(existing_image)); - } - - let Some(worktree) = self - .worktree_store - .read(cx) - .worktree_for_id(project_path.worktree_id, cx) - else { - return Task::ready(Err(anyhow::anyhow!("no such worktree"))); - }; - - let loading_watch = match self.loading_images_by_path.entry(project_path.clone()) { - // If the given path is already being loaded, then wait for that existing - // task to complete and return the same image. - hash_map::Entry::Occupied(e) => e.get().clone(), - - // Otherwise, record the fact that this path is now being loaded. - hash_map::Entry::Vacant(entry) => { - let (mut tx, rx) = postage::watch::channel(); - entry.insert(rx.clone()); - - let load_image = self - .state - .open_image(project_path.path.clone(), worktree, cx); - - cx.spawn(async move |this, cx| { - let load_result = load_image.await; - *tx.borrow_mut() = Some(this.update(cx, |this, _cx| { - // Record the fact that the image is no longer loading. - this.loading_images_by_path.remove(&project_path); - let image = load_result.map_err(Arc::new)?; - Ok(image) - })?); - anyhow::Ok(()) - }) - .detach(); - rx - } - }; - - cx.background_spawn(async move { - Self::wait_for_loading_image(loading_watch) - .await - .map_err(|e| e.cloned()) - }) - } - - pub async fn wait_for_loading_image( - mut receiver: postage::watch::Receiver< - Option, Arc>>, - >, - ) -> Result, Arc> { - loop { - if let Some(result) = receiver.borrow().as_ref() { - match result { - Ok(image) => return Ok(image.to_owned()), - Err(e) => return Err(e.to_owned()), - } - } - receiver.next().await; - } - } - - pub fn reload_images( - &self, - images: HashSet>, - cx: &mut Context, - ) -> Task> { - if images.is_empty() { - return Task::ready(Ok(())); - } - - self.state.reload_images(images, cx) - } - - fn add_image(&mut self, image: Entity, cx: &mut Context) -> Result<()> { - let image_id = image.read(cx).id; - self.opened_images.insert(image_id, image.downgrade()); - cx.subscribe(&image, Self::on_image_event).detach(); - cx.emit(ImageStoreEvent::ImageAdded(image)); - Ok(()) - } - - fn on_image_event( - &mut self, - image: Entity, - event: &ImageItemEvent, - cx: &mut Context, - ) { - if let ImageItemEvent::FileHandleChanged = event - && let Some(local) = self.state.as_local() - { - local.update(cx, |local, cx| { - local.image_changed_file(image, cx); - }) - } - } - - pub fn handle_create_image_for_peer( - &mut self, - envelope: TypedEnvelope, - cx: &mut Context, - ) -> Result<()> { - if let Some(remote) = self.state.as_remote() { - let worktree_store = self.worktree_store.clone(); - let image = remote.update(cx, |remote, cx| { - remote.handle_create_image_for_peer(envelope, &worktree_store, cx) - })?; - if let Some(image) = image { - remote.update(cx, |this, cx| { - let image = image.clone(); - let image_id = image.read(cx).id; - this.loaded_images.insert(image_id, image) - }); - - self.add_image(image, cx)?; - } - } - - Ok(()) - } -} - -impl RemoteImageStore { - pub fn wait_for_remote_image( - &mut self, - id: ImageId, - cx: &mut Context, - ) -> Task>> { - if let Some(image) = self.loaded_images.remove(&id) { - return Task::ready(Ok(image)); - } - - let (tx, rx) = oneshot::channel(); - self.remote_image_listeners.entry(id).or_default().push(tx); - - cx.spawn(async move |_this, cx| { - let result = cx.background_spawn(async move { rx.await? }).await; - result - }) - } - - pub fn handle_create_image_for_peer( - &mut self, - envelope: TypedEnvelope, - worktree_store: &Entity, - cx: &mut Context, - ) -> Result>> { - use proto::create_image_for_peer::Variant; - match envelope.payload.variant { - Some(Variant::State(state)) => { - let image_id = - ImageId::from(NonZeroU64::new(state.id).context("invalid image id")?); - - self.loading_remote_images_by_id.insert( - image_id, - LoadingRemoteImage { - state, - chunks: Vec::new(), - received_size: 0, - }, - ); - Ok(None) - } - Some(Variant::Chunk(chunk)) => { - let image_id = - ImageId::from(NonZeroU64::new(chunk.image_id).context("invalid image id")?); - - let loading = self - .loading_remote_images_by_id - .get_mut(&image_id) - .context("received chunk for unknown image")?; - - loading.received_size += chunk.data.len() as u64; - loading.chunks.push(chunk.data); - - if loading.received_size == loading.state.content_size { - let loading = self.loading_remote_images_by_id.remove(&image_id).unwrap(); - - let mut content = Vec::with_capacity(loading.received_size as usize); - for chunk_data in loading.chunks { - content.extend_from_slice(&chunk_data); - } - - let image_metadata = ImageItem::compute_metadata_from_bytes(&content).log_err(); - let image = create_gpui_image(content)?; - - let proto_file = loading.state.file.context("missing file in image state")?; - let worktree_id = WorktreeId::from_proto(proto_file.worktree_id); - let worktree = worktree_store - .read(cx) - .worktree_for_id(worktree_id, cx) - .context("worktree not found")?; - - let file = Arc::new( - worktree::File::from_proto(proto_file, worktree, cx) - .context("invalid file in image state")?, - ); - - let entity = cx.new(|_cx| ImageItem { - id: image_id, - file, - image, - image_metadata, - reload_task: None, - }); - - if let Some(listeners) = self.remote_image_listeners.remove(&image_id) { - for listener in listeners { - listener.send(Ok(entity.clone())).ok(); - } - } - - Ok(Some(entity)) - } else { - Ok(None) - } - } - None => { - log::warn!("Received CreateImageForPeer with no variant"); - Ok(None) - } - } - } - - // TODO: subscribe to worktree and update image contents or at least mark as dirty on file changes -} - -impl ImageStoreImpl for Entity { - fn open_image( - &self, - path: Arc, - worktree: Entity, - cx: &mut Context, - ) -> Task>> { - let this = self.clone(); - - let load_file = worktree.update(cx, |worktree, cx| { - worktree.load_binary_file(path.as_ref(), cx) - }); - cx.spawn(async move |image_store, cx| { - let LoadedBinaryFile { file, content } = load_file.await?; - let image = create_gpui_image(content)?; - - let entity = cx.new(|cx| ImageItem { - id: cx.entity_id().as_non_zero_u64().into(), - file: file.clone(), - image, - image_metadata: None, - reload_task: None, - })?; - - let image_id = cx.read_entity(&entity, |model, _| model.id)?; - - this.update(cx, |this, cx| { - image_store.update(cx, |image_store, cx| { - image_store.add_image(entity.clone(), cx) - })??; - this.local_image_ids_by_path.insert( - ProjectPath { - worktree_id: file.worktree_id(cx), - path: file.path.clone(), - }, - image_id, - ); - - if let Some(entry_id) = file.entry_id { - this.local_image_ids_by_entry_id.insert(entry_id, image_id); - } - - anyhow::Ok(()) - })??; - - Ok(entity) - }) - } - - fn reload_images( - &self, - images: HashSet>, - cx: &mut Context, - ) -> Task> { - cx.spawn(async move |_, cx| { - for image in images { - if let Some(rec) = image.update(cx, |image, cx| image.reload(cx))? { - rec.await? - } - } - Ok(()) - }) - } - - fn as_local(&self) -> Option> { - Some(self.clone()) - } - - fn as_remote(&self) -> Option> { - None - } -} - -impl ImageStoreImpl for Entity { - fn open_image( - &self, - path: Arc, - worktree: Entity, - cx: &mut Context, - ) -> Task>> { - let worktree_id = worktree.read(cx).id().to_proto(); - let (project_id, client) = { - let store = self.read(cx); - (store.project_id, store.upstream_client.clone()) - }; - let remote_store = self.clone(); - - cx.spawn(async move |_image_store, cx| { - let response = client - .request(rpc::proto::OpenImageByPath { - project_id, - worktree_id, - path: path.to_proto(), - }) - .await?; - - let image_id = ImageId::from( - NonZeroU64::new(response.image_id).context("invalid image_id in response")?, - ); - - remote_store - .update(cx, |remote_store, cx| { - remote_store.wait_for_remote_image(image_id, cx) - })? - .await - }) - } - - fn reload_images( - &self, - _images: HashSet>, - _cx: &mut Context, - ) -> Task> { - Task::ready(Err(anyhow::anyhow!( - "Reloading images from remote is not supported" - ))) - } - - fn as_local(&self) -> Option> { - None - } - - fn as_remote(&self) -> Option> { - Some(self.clone()) - } -} - -impl LocalImageStore { - fn subscribe_to_worktree(&mut self, worktree: &Entity, cx: &mut Context) { - cx.subscribe(worktree, |this, worktree, event, cx| { - if worktree.read(cx).is_local() - && let worktree::Event::UpdatedEntries(changes) = event - { - this.local_worktree_entries_changed(&worktree, changes, cx); - } - }) - .detach(); - } - - fn local_worktree_entries_changed( - &mut self, - worktree_handle: &Entity, - changes: &[(Arc, ProjectEntryId, PathChange)], - cx: &mut Context, - ) { - let snapshot = worktree_handle.read(cx).snapshot(); - for (path, entry_id, _) in changes { - self.local_worktree_entry_changed(*entry_id, path, worktree_handle, &snapshot, cx); - } - } - - fn local_worktree_entry_changed( - &mut self, - entry_id: ProjectEntryId, - path: &Arc, - worktree: &Entity, - snapshot: &worktree::Snapshot, - cx: &mut Context, - ) -> Option<()> { - let project_path = ProjectPath { - worktree_id: snapshot.id(), - path: path.clone(), - }; - let image_id = match self.local_image_ids_by_entry_id.get(&entry_id) { - Some(&image_id) => image_id, - None => self.local_image_ids_by_path.get(&project_path).copied()?, - }; - - let image = self - .image_store - .update(cx, |image_store, _| { - if let Some(image) = image_store.get(image_id) { - Some(image) - } else { - image_store.opened_images.remove(&image_id); - None - } - }) - .ok() - .flatten(); - let image = if let Some(image) = image { - image - } else { - self.local_image_ids_by_path.remove(&project_path); - self.local_image_ids_by_entry_id.remove(&entry_id); - return None; - }; - - image.update(cx, |image, cx| { - let old_file = &image.file; - if old_file.worktree != *worktree { - return; - } - - let snapshot_entry = old_file - .entry_id - .and_then(|entry_id| snapshot.entry_for_id(entry_id)) - .or_else(|| snapshot.entry_for_path(old_file.path.as_ref())); - - let new_file = if let Some(entry) = snapshot_entry { - worktree::File { - disk_state: match entry.mtime { - Some(mtime) => DiskState::Present { mtime }, - None => old_file.disk_state, - }, - is_local: true, - entry_id: Some(entry.id), - path: entry.path.clone(), - worktree: worktree.clone(), - is_private: entry.is_private, - } - } else { - worktree::File { - disk_state: DiskState::Deleted, - is_local: true, - entry_id: old_file.entry_id, - path: old_file.path.clone(), - worktree: worktree.clone(), - is_private: old_file.is_private, - } - }; - - if new_file == **old_file { - return; - } - - if new_file.path != old_file.path { - self.local_image_ids_by_path.remove(&ProjectPath { - path: old_file.path.clone(), - worktree_id: old_file.worktree_id(cx), - }); - self.local_image_ids_by_path.insert( - ProjectPath { - worktree_id: new_file.worktree_id(cx), - path: new_file.path.clone(), - }, - image_id, - ); - } - - if new_file.entry_id != old_file.entry_id { - if let Some(entry_id) = old_file.entry_id { - self.local_image_ids_by_entry_id.remove(&entry_id); - } - if let Some(entry_id) = new_file.entry_id { - self.local_image_ids_by_entry_id.insert(entry_id, image_id); - } - } - - image.file_updated(Arc::new(new_file), cx); - }); - None - } - - fn image_changed_file(&mut self, image: Entity, cx: &mut App) -> Option<()> { - let image = image.read(cx); - let file = &image.file; - - let image_id = image.id; - if let Some(entry_id) = file.entry_id { - match self.local_image_ids_by_entry_id.get(&entry_id) { - Some(_) => { - return None; - } - None => { - self.local_image_ids_by_entry_id.insert(entry_id, image_id); - } - } - }; - self.local_image_ids_by_path.insert( - ProjectPath { - worktree_id: file.worktree_id(cx), - path: file.path.clone(), - }, - image_id, - ); - - Some(()) - } -} - -fn create_gpui_image(content: Vec) -> anyhow::Result> { - let format = image::guess_format(&content)?; - - Ok(Arc::new(gpui::Image::from_bytes( - match format { - image::ImageFormat::Png => gpui::ImageFormat::Png, - image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg, - image::ImageFormat::WebP => gpui::ImageFormat::Webp, - image::ImageFormat::Gif => gpui::ImageFormat::Gif, - image::ImageFormat::Bmp => gpui::ImageFormat::Bmp, - image::ImageFormat::Tiff => gpui::ImageFormat::Tiff, - image::ImageFormat::Ico => gpui::ImageFormat::Ico, - format => anyhow::bail!("Image format {format:?} not supported"), - }, - content, - ))) -} - -#[cfg(test)] -mod tests { - use super::*; - use fs::FakeFs; - use gpui::TestAppContext; - use serde_json::json; - use settings::SettingsStore; - use util::rel_path::rel_path; - - pub fn init_test(cx: &mut TestAppContext) { - zlog::init_test(); - - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - }); - } - - #[gpui::test] - async fn test_image_not_loaded_twice(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - fs.insert_tree("/root", json!({})).await; - // Create a png file that consists of a single white pixel - fs.insert_file( - "/root/image_1.png", - vec![ - 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, - 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, - 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, 0x78, - 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, - 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, - ], - ) - .await; - - let project = Project::test(fs, ["/root".as_ref()], cx).await; - - let worktree_id = - cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().read(cx).id()); - - let project_path = ProjectPath { - worktree_id, - path: rel_path("image_1.png").into(), - }; - - let (task1, task2) = project.update(cx, |project, cx| { - ( - project.open_image(project_path.clone(), cx), - project.open_image(project_path.clone(), cx), - ) - }); - - let image1 = task1.await.unwrap(); - let image2 = task2.await.unwrap(); - - assert_eq!(image1, image2); - } - - #[gpui::test] - fn test_compute_metadata_from_bytes() { - // Single white pixel PNG - let png_bytes = vec![ - 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, - 0x44, 0x52, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, - 0x00, 0x1F, 0x15, 0xC4, 0x89, 0x00, 0x00, 0x00, 0x0A, 0x49, 0x44, 0x41, 0x54, 0x78, - 0x9C, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00, 0x01, 0x0D, 0x0A, 0x2D, 0xB4, 0x00, - 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82, - ]; - - let metadata = ImageItem::compute_metadata_from_bytes(&png_bytes).unwrap(); - - assert_eq!(metadata.width, 1); - assert_eq!(metadata.height, 1); - assert_eq!(metadata.file_size, png_bytes.len() as u64); - assert_eq!(metadata.format, image::ImageFormat::Png); - assert!(metadata.colors.is_some()); - } -} diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs deleted file mode 100644 index 05ee70bf66..0000000000 --- a/crates/project/src/lsp_command.rs +++ /dev/null @@ -1,4643 +0,0 @@ -mod signature_help; - -use crate::{ - CodeAction, CompletionSource, CoreCompletion, CoreCompletionResponse, DocumentColor, - DocumentHighlight, DocumentSymbol, Hover, HoverBlock, HoverBlockKind, InlayHint, - InlayHintLabel, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, - LocationLink, LspAction, LspPullDiagnostics, MarkupContent, PrepareRenameResponse, - ProjectTransaction, PulledDiagnostics, ResolveState, - lsp_store::{LocalLspStore, LspStore}, -}; -use anyhow::{Context as _, Result}; -use async_trait::async_trait; -use client::proto::{self, PeerId}; -use clock::Global; -use collections::{HashMap, HashSet}; -use futures::future; -use gpui::{App, AsyncApp, Entity, SharedString, Task}; -use language::{ - Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, CharScopeContext, - OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, Unclipped, - language_settings::{InlayHintKind, LanguageSettings, language_settings}, - point_from_lsp, point_to_lsp, - proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, - range_from_lsp, range_to_lsp, -}; -use lsp::{ - AdapterServerCapabilities, CodeActionKind, CodeActionOptions, CodeDescription, - CompletionContext, CompletionListItemDefaultsEditRange, CompletionTriggerKind, - DocumentHighlightKind, LanguageServer, LanguageServerId, LinkedEditingRangeServerCapabilities, - OneOf, RenameOptions, ServerCapabilities, -}; -use serde_json::Value; -use signature_help::{lsp_to_proto_signature, proto_to_lsp_signature}; -use std::{ - cmp::Reverse, collections::hash_map, mem, ops::Range, path::Path, str::FromStr, sync::Arc, -}; -use text::{BufferId, LineEnding}; -use util::{ResultExt as _, debug_panic}; - -pub use signature_help::SignatureHelp; - -pub fn lsp_formatting_options(settings: &LanguageSettings) -> lsp::FormattingOptions { - lsp::FormattingOptions { - tab_size: settings.tab_size.into(), - insert_spaces: !settings.hard_tabs, - trim_trailing_whitespace: Some(settings.remove_trailing_whitespace_on_save), - trim_final_newlines: Some(settings.ensure_final_newline_on_save), - insert_final_newline: Some(settings.ensure_final_newline_on_save), - ..lsp::FormattingOptions::default() - } -} - -pub fn file_path_to_lsp_url(path: &Path) -> Result { - match lsp::Uri::from_file_path(path) { - Ok(url) => Ok(url), - Err(()) => anyhow::bail!("Invalid file path provided to LSP request: {path:?}"), - } -} - -pub(crate) fn make_text_document_identifier(path: &Path) -> Result { - Ok(lsp::TextDocumentIdentifier { - uri: file_path_to_lsp_url(path)?, - }) -} - -pub(crate) fn make_lsp_text_document_position( - path: &Path, - position: PointUtf16, -) -> Result { - Ok(lsp::TextDocumentPositionParams { - text_document: make_text_document_identifier(path)?, - position: point_to_lsp(position), - }) -} - -#[async_trait(?Send)] -pub trait LspCommand: 'static + Sized + Send + std::fmt::Debug { - type Response: 'static + Default + Send + std::fmt::Debug; - type LspRequest: 'static + Send + lsp::request::Request; - type ProtoRequest: 'static + Send + proto::RequestMessage; - - fn display_name(&self) -> &str; - - fn status(&self) -> Option { - None - } - - fn to_lsp_params_or_response( - &self, - path: &Path, - buffer: &Buffer, - language_server: &Arc, - cx: &App, - ) -> Result< - LspParamsOrResponse<::Params, Self::Response>, - > { - if self.check_capabilities(language_server.adapter_server_capabilities()) { - Ok(LspParamsOrResponse::Params(self.to_lsp( - path, - buffer, - language_server, - cx, - )?)) - } else { - Ok(LspParamsOrResponse::Response(Default::default())) - } - } - - /// When false, `to_lsp_params_or_response` default implementation will return the default response. - fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool; - - fn to_lsp( - &self, - path: &Path, - buffer: &Buffer, - language_server: &Arc, - cx: &App, - ) -> Result<::Params>; - - async fn response_from_lsp( - self, - message: ::Result, - lsp_store: Entity, - buffer: Entity, - server_id: LanguageServerId, - cx: AsyncApp, - ) -> Result; - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest; - - async fn from_proto( - message: Self::ProtoRequest, - lsp_store: Entity, - buffer: Entity, - cx: AsyncApp, - ) -> Result; - - fn response_to_proto( - response: Self::Response, - lsp_store: &mut LspStore, - peer_id: PeerId, - buffer_version: &clock::Global, - cx: &mut App, - ) -> ::Response; - - async fn response_from_proto( - self, - message: ::Response, - lsp_store: Entity, - buffer: Entity, - cx: AsyncApp, - ) -> Result; - - fn buffer_id_from_proto(message: &Self::ProtoRequest) -> Result; -} - -pub enum LspParamsOrResponse { - Params(P), - Response(R), -} - -#[derive(Debug)] -pub(crate) struct PrepareRename { - pub position: PointUtf16, -} - -#[derive(Debug)] -pub(crate) struct PerformRename { - pub position: PointUtf16, - pub new_name: String, - pub push_to_history: bool, -} - -#[derive(Debug, Clone, Copy)] -pub struct GetDefinitions { - pub position: PointUtf16, -} - -#[derive(Debug, Clone, Copy)] -pub(crate) struct GetDeclarations { - pub position: PointUtf16, -} - -#[derive(Debug, Clone, Copy)] -pub(crate) struct GetTypeDefinitions { - pub position: PointUtf16, -} - -#[derive(Debug, Clone, Copy)] -pub(crate) struct GetImplementations { - pub position: PointUtf16, -} - -#[derive(Debug, Clone, Copy)] -pub(crate) struct GetReferences { - pub position: PointUtf16, -} - -#[derive(Debug)] -pub(crate) struct GetDocumentHighlights { - pub position: PointUtf16, -} - -#[derive(Debug, Copy, Clone)] -pub(crate) struct GetDocumentSymbols; - -#[derive(Clone, Debug)] -pub(crate) struct GetSignatureHelp { - pub position: PointUtf16, -} - -#[derive(Clone, Debug)] -pub(crate) struct GetHover { - pub position: PointUtf16, -} - -#[derive(Debug)] -pub(crate) struct GetCompletions { - pub position: PointUtf16, - pub context: CompletionContext, - pub server_id: Option, -} - -#[derive(Clone, Debug)] -pub(crate) struct GetCodeActions { - pub range: Range, - pub kinds: Option>, -} - -#[derive(Debug)] -pub(crate) struct OnTypeFormatting { - pub position: PointUtf16, - pub trigger: String, - pub options: lsp::FormattingOptions, - pub push_to_history: bool, -} - -#[derive(Clone, Debug)] -pub(crate) struct InlayHints { - pub range: Range, -} - -#[derive(Debug, Copy, Clone)] -pub(crate) struct GetCodeLens; - -#[derive(Debug, Copy, Clone)] -pub(crate) struct GetDocumentColor; - -impl GetCodeLens { - pub(crate) fn can_resolve_lens(capabilities: &ServerCapabilities) -> bool { - capabilities - .code_lens_provider - .as_ref() - .and_then(|code_lens_options| code_lens_options.resolve_provider) - .unwrap_or(false) - } -} - -#[derive(Debug)] -pub(crate) struct LinkedEditingRange { - pub position: Anchor, -} - -#[derive(Clone, Debug)] -pub(crate) struct GetDocumentDiagnostics { - /// We cannot blindly rely on server's capabilities.diagnostic_provider, as they're a singular field, whereas - /// a server can register multiple diagnostic providers post-mortem. - pub registration_id: Option, - pub identifier: Option, - pub previous_result_id: Option, -} - -#[async_trait(?Send)] -impl LspCommand for PrepareRename { - type Response = PrepareRenameResponse; - type LspRequest = lsp::request::PrepareRenameRequest; - type ProtoRequest = proto::PrepareRename; - - fn display_name(&self) -> &str { - "Prepare rename" - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - capabilities - .server_capabilities - .rename_provider - .is_some_and(|capability| match capability { - OneOf::Left(enabled) => enabled, - OneOf::Right(options) => options.prepare_provider.unwrap_or(false), - }) - } - - fn to_lsp_params_or_response( - &self, - path: &Path, - buffer: &Buffer, - language_server: &Arc, - cx: &App, - ) -> Result> { - let rename_provider = language_server - .adapter_server_capabilities() - .server_capabilities - .rename_provider; - match rename_provider { - Some(lsp::OneOf::Right(RenameOptions { - prepare_provider: Some(true), - .. - })) => Ok(LspParamsOrResponse::Params(self.to_lsp( - path, - buffer, - language_server, - cx, - )?)), - Some(lsp::OneOf::Right(_)) => Ok(LspParamsOrResponse::Response( - PrepareRenameResponse::OnlyUnpreparedRenameSupported, - )), - Some(lsp::OneOf::Left(true)) => Ok(LspParamsOrResponse::Response( - PrepareRenameResponse::OnlyUnpreparedRenameSupported, - )), - _ => anyhow::bail!("Rename not supported"), - } - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - make_lsp_text_document_position(path, self.position) - } - - async fn response_from_lsp( - self, - message: Option, - _: Entity, - buffer: Entity, - _: LanguageServerId, - cx: AsyncApp, - ) -> Result { - buffer.read_with(&cx, |buffer, _| match message { - Some(lsp::PrepareRenameResponse::Range(range)) - | Some(lsp::PrepareRenameResponse::RangeWithPlaceholder { range, .. }) => { - let Range { start, end } = range_from_lsp(range); - if buffer.clip_point_utf16(start, Bias::Left) == start.0 - && buffer.clip_point_utf16(end, Bias::Left) == end.0 - { - Ok(PrepareRenameResponse::Success( - buffer.anchor_after(start)..buffer.anchor_before(end), - )) - } else { - Ok(PrepareRenameResponse::InvalidPosition) - } - } - Some(lsp::PrepareRenameResponse::DefaultBehavior { .. }) => { - let snapshot = buffer.snapshot(); - let (range, _) = snapshot.surrounding_word(self.position, None); - let range = snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end); - Ok(PrepareRenameResponse::Success(range)) - } - None => Ok(PrepareRenameResponse::InvalidPosition), - })? - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::PrepareRename { - proto::PrepareRename { - project_id, - buffer_id: buffer.remote_id().into(), - position: Some(language::proto::serialize_anchor( - &buffer.anchor_before(self.position), - )), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - message: proto::PrepareRename, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - let position = message - .position - .and_then(deserialize_anchor) - .context("invalid position")?; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - - Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, - }) - } - - fn response_to_proto( - response: PrepareRenameResponse, - _: &mut LspStore, - _: PeerId, - buffer_version: &clock::Global, - _: &mut App, - ) -> proto::PrepareRenameResponse { - match response { - PrepareRenameResponse::Success(range) => proto::PrepareRenameResponse { - can_rename: true, - only_unprepared_rename_supported: false, - start: Some(language::proto::serialize_anchor(&range.start)), - end: Some(language::proto::serialize_anchor(&range.end)), - version: serialize_version(buffer_version), - }, - PrepareRenameResponse::OnlyUnpreparedRenameSupported => proto::PrepareRenameResponse { - can_rename: false, - only_unprepared_rename_supported: true, - start: None, - end: None, - version: vec![], - }, - PrepareRenameResponse::InvalidPosition => proto::PrepareRenameResponse { - can_rename: false, - only_unprepared_rename_supported: false, - start: None, - end: None, - version: vec![], - }, - } - } - - async fn response_from_proto( - self, - message: proto::PrepareRenameResponse, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - if message.can_rename { - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - if let (Some(start), Some(end)) = ( - message.start.and_then(deserialize_anchor), - message.end.and_then(deserialize_anchor), - ) { - Ok(PrepareRenameResponse::Success(start..end)) - } else { - anyhow::bail!( - "Missing start or end position in remote project PrepareRenameResponse" - ); - } - } else if message.only_unprepared_rename_supported { - Ok(PrepareRenameResponse::OnlyUnpreparedRenameSupported) - } else { - Ok(PrepareRenameResponse::InvalidPosition) - } - } - - fn buffer_id_from_proto(message: &proto::PrepareRename) -> Result { - BufferId::new(message.buffer_id) - } -} - -#[async_trait(?Send)] -impl LspCommand for PerformRename { - type Response = ProjectTransaction; - type LspRequest = lsp::request::Rename; - type ProtoRequest = proto::PerformRename; - - fn display_name(&self) -> &str { - "Rename" - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - capabilities - .server_capabilities - .rename_provider - .is_some_and(|capability| match capability { - OneOf::Left(enabled) => enabled, - OneOf::Right(_options) => true, - }) - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(lsp::RenameParams { - text_document_position: make_lsp_text_document_position(path, self.position)?, - new_name: self.new_name.clone(), - work_done_progress_params: Default::default(), - }) - } - - async fn response_from_lsp( - self, - message: Option, - lsp_store: Entity, - buffer: Entity, - server_id: LanguageServerId, - mut cx: AsyncApp, - ) -> Result { - if let Some(edit) = message { - let (_, lsp_server) = - language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?; - LocalLspStore::deserialize_workspace_edit( - lsp_store, - edit, - self.push_to_history, - lsp_server, - &mut cx, - ) - .await - } else { - Ok(ProjectTransaction::default()) - } - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::PerformRename { - proto::PerformRename { - project_id, - buffer_id: buffer.remote_id().into(), - position: Some(language::proto::serialize_anchor( - &buffer.anchor_before(self.position), - )), - new_name: self.new_name.clone(), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - message: proto::PerformRename, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - let position = message - .position - .and_then(deserialize_anchor) - .context("invalid position")?; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, - new_name: message.new_name, - push_to_history: false, - }) - } - - fn response_to_proto( - response: ProjectTransaction, - lsp_store: &mut LspStore, - peer_id: PeerId, - _: &clock::Global, - cx: &mut App, - ) -> proto::PerformRenameResponse { - let transaction = lsp_store.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.serialize_project_transaction_for_peer(response, peer_id, cx) - }); - proto::PerformRenameResponse { - transaction: Some(transaction), - } - } - - async fn response_from_proto( - self, - message: proto::PerformRenameResponse, - lsp_store: Entity, - _: Entity, - mut cx: AsyncApp, - ) -> Result { - let message = message.transaction.context("missing transaction")?; - lsp_store - .update(&mut cx, |lsp_store, cx| { - lsp_store.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.deserialize_project_transaction(message, self.push_to_history, cx) - }) - })? - .await - } - - fn buffer_id_from_proto(message: &proto::PerformRename) -> Result { - BufferId::new(message.buffer_id) - } -} - -#[async_trait(?Send)] -impl LspCommand for GetDefinitions { - type Response = Vec; - type LspRequest = lsp::request::GotoDefinition; - type ProtoRequest = proto::GetDefinition; - - fn display_name(&self) -> &str { - "Get definition" - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - capabilities - .server_capabilities - .definition_provider - .is_some_and(|capability| match capability { - OneOf::Left(supported) => supported, - OneOf::Right(_options) => true, - }) - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(lsp::GotoDefinitionParams { - text_document_position_params: make_lsp_text_document_position(path, self.position)?, - work_done_progress_params: Default::default(), - partial_result_params: Default::default(), - }) - } - - async fn response_from_lsp( - self, - message: Option, - lsp_store: Entity, - buffer: Entity, - server_id: LanguageServerId, - cx: AsyncApp, - ) -> Result> { - location_links_from_lsp(message, lsp_store, buffer, server_id, cx).await - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDefinition { - proto::GetDefinition { - project_id, - buffer_id: buffer.remote_id().into(), - position: Some(language::proto::serialize_anchor( - &buffer.anchor_before(self.position), - )), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - message: proto::GetDefinition, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - let position = message - .position - .and_then(deserialize_anchor) - .context("invalid position")?; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, - }) - } - - fn response_to_proto( - response: Vec, - lsp_store: &mut LspStore, - peer_id: PeerId, - _: &clock::Global, - cx: &mut App, - ) -> proto::GetDefinitionResponse { - let links = location_links_to_proto(response, lsp_store, peer_id, cx); - proto::GetDefinitionResponse { links } - } - - async fn response_from_proto( - self, - message: proto::GetDefinitionResponse, - lsp_store: Entity, - _: Entity, - cx: AsyncApp, - ) -> Result> { - location_links_from_proto(message.links, lsp_store, cx).await - } - - fn buffer_id_from_proto(message: &proto::GetDefinition) -> Result { - BufferId::new(message.buffer_id) - } -} - -#[async_trait(?Send)] -impl LspCommand for GetDeclarations { - type Response = Vec; - type LspRequest = lsp::request::GotoDeclaration; - type ProtoRequest = proto::GetDeclaration; - - fn display_name(&self) -> &str { - "Get declaration" - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - capabilities - .server_capabilities - .declaration_provider - .is_some_and(|capability| match capability { - lsp::DeclarationCapability::Simple(supported) => supported, - lsp::DeclarationCapability::RegistrationOptions(..) => true, - lsp::DeclarationCapability::Options(..) => true, - }) - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(lsp::GotoDeclarationParams { - text_document_position_params: make_lsp_text_document_position(path, self.position)?, - work_done_progress_params: Default::default(), - partial_result_params: Default::default(), - }) - } - - async fn response_from_lsp( - self, - message: Option, - lsp_store: Entity, - buffer: Entity, - server_id: LanguageServerId, - cx: AsyncApp, - ) -> Result> { - location_links_from_lsp(message, lsp_store, buffer, server_id, cx).await - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDeclaration { - proto::GetDeclaration { - project_id, - buffer_id: buffer.remote_id().into(), - position: Some(language::proto::serialize_anchor( - &buffer.anchor_before(self.position), - )), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - message: proto::GetDeclaration, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - let position = message - .position - .and_then(deserialize_anchor) - .context("invalid position")?; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, - }) - } - - fn response_to_proto( - response: Vec, - lsp_store: &mut LspStore, - peer_id: PeerId, - _: &clock::Global, - cx: &mut App, - ) -> proto::GetDeclarationResponse { - let links = location_links_to_proto(response, lsp_store, peer_id, cx); - proto::GetDeclarationResponse { links } - } - - async fn response_from_proto( - self, - message: proto::GetDeclarationResponse, - lsp_store: Entity, - _: Entity, - cx: AsyncApp, - ) -> Result> { - location_links_from_proto(message.links, lsp_store, cx).await - } - - fn buffer_id_from_proto(message: &proto::GetDeclaration) -> Result { - BufferId::new(message.buffer_id) - } -} - -#[async_trait(?Send)] -impl LspCommand for GetImplementations { - type Response = Vec; - type LspRequest = lsp::request::GotoImplementation; - type ProtoRequest = proto::GetImplementation; - - fn display_name(&self) -> &str { - "Get implementation" - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - capabilities - .server_capabilities - .implementation_provider - .is_some_and(|capability| match capability { - lsp::ImplementationProviderCapability::Simple(enabled) => enabled, - lsp::ImplementationProviderCapability::Options(_options) => true, - }) - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(lsp::GotoImplementationParams { - text_document_position_params: make_lsp_text_document_position(path, self.position)?, - work_done_progress_params: Default::default(), - partial_result_params: Default::default(), - }) - } - - async fn response_from_lsp( - self, - message: Option, - lsp_store: Entity, - buffer: Entity, - server_id: LanguageServerId, - cx: AsyncApp, - ) -> Result> { - location_links_from_lsp(message, lsp_store, buffer, server_id, cx).await - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetImplementation { - proto::GetImplementation { - project_id, - buffer_id: buffer.remote_id().into(), - position: Some(language::proto::serialize_anchor( - &buffer.anchor_before(self.position), - )), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - message: proto::GetImplementation, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - let position = message - .position - .and_then(deserialize_anchor) - .context("invalid position")?; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, - }) - } - - fn response_to_proto( - response: Vec, - lsp_store: &mut LspStore, - peer_id: PeerId, - _: &clock::Global, - cx: &mut App, - ) -> proto::GetImplementationResponse { - let links = location_links_to_proto(response, lsp_store, peer_id, cx); - proto::GetImplementationResponse { links } - } - - async fn response_from_proto( - self, - message: proto::GetImplementationResponse, - project: Entity, - _: Entity, - cx: AsyncApp, - ) -> Result> { - location_links_from_proto(message.links, project, cx).await - } - - fn buffer_id_from_proto(message: &proto::GetImplementation) -> Result { - BufferId::new(message.buffer_id) - } -} - -#[async_trait(?Send)] -impl LspCommand for GetTypeDefinitions { - type Response = Vec; - type LspRequest = lsp::request::GotoTypeDefinition; - type ProtoRequest = proto::GetTypeDefinition; - - fn display_name(&self) -> &str { - "Get type definition" - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - !matches!( - &capabilities.server_capabilities.type_definition_provider, - None | Some(lsp::TypeDefinitionProviderCapability::Simple(false)) - ) - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(lsp::GotoTypeDefinitionParams { - text_document_position_params: make_lsp_text_document_position(path, self.position)?, - work_done_progress_params: Default::default(), - partial_result_params: Default::default(), - }) - } - - async fn response_from_lsp( - self, - message: Option, - project: Entity, - buffer: Entity, - server_id: LanguageServerId, - cx: AsyncApp, - ) -> Result> { - location_links_from_lsp(message, project, buffer, server_id, cx).await - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetTypeDefinition { - proto::GetTypeDefinition { - project_id, - buffer_id: buffer.remote_id().into(), - position: Some(language::proto::serialize_anchor( - &buffer.anchor_before(self.position), - )), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - message: proto::GetTypeDefinition, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - let position = message - .position - .and_then(deserialize_anchor) - .context("invalid position")?; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, - }) - } - - fn response_to_proto( - response: Vec, - lsp_store: &mut LspStore, - peer_id: PeerId, - _: &clock::Global, - cx: &mut App, - ) -> proto::GetTypeDefinitionResponse { - let links = location_links_to_proto(response, lsp_store, peer_id, cx); - proto::GetTypeDefinitionResponse { links } - } - - async fn response_from_proto( - self, - message: proto::GetTypeDefinitionResponse, - project: Entity, - _: Entity, - cx: AsyncApp, - ) -> Result> { - location_links_from_proto(message.links, project, cx).await - } - - fn buffer_id_from_proto(message: &proto::GetTypeDefinition) -> Result { - BufferId::new(message.buffer_id) - } -} - -fn language_server_for_buffer( - lsp_store: &Entity, - buffer: &Entity, - server_id: LanguageServerId, - cx: &mut AsyncApp, -) -> Result<(Arc, Arc)> { - lsp_store - .update(cx, |lsp_store, cx| { - buffer.update(cx, |buffer, cx| { - lsp_store - .language_server_for_local_buffer(buffer, server_id, cx) - .map(|(adapter, server)| (adapter.clone(), server.clone())) - }) - })? - .context("no language server found for buffer") -} - -pub async fn location_links_from_proto( - proto_links: Vec, - lsp_store: Entity, - mut cx: AsyncApp, -) -> Result> { - let mut links = Vec::new(); - - for link in proto_links { - links.push(location_link_from_proto(link, lsp_store.clone(), &mut cx).await?) - } - - Ok(links) -} - -pub fn location_link_from_proto( - link: proto::LocationLink, - lsp_store: Entity, - cx: &mut AsyncApp, -) -> Task> { - cx.spawn(async move |cx| { - let origin = match link.origin { - Some(origin) => { - let buffer_id = BufferId::new(origin.buffer_id)?; - let buffer = lsp_store - .update(cx, |lsp_store, cx| { - lsp_store.wait_for_remote_buffer(buffer_id, cx) - })? - .await?; - let start = origin - .start - .and_then(deserialize_anchor) - .context("missing origin start")?; - let end = origin - .end - .and_then(deserialize_anchor) - .context("missing origin end")?; - buffer - .update(cx, |buffer, _| buffer.wait_for_anchors([start, end]))? - .await?; - Some(Location { - buffer, - range: start..end, - }) - } - None => None, - }; - - let target = link.target.context("missing target")?; - let buffer_id = BufferId::new(target.buffer_id)?; - let buffer = lsp_store - .update(cx, |lsp_store, cx| { - lsp_store.wait_for_remote_buffer(buffer_id, cx) - })? - .await?; - let start = target - .start - .and_then(deserialize_anchor) - .context("missing target start")?; - let end = target - .end - .and_then(deserialize_anchor) - .context("missing target end")?; - buffer - .update(cx, |buffer, _| buffer.wait_for_anchors([start, end]))? - .await?; - let target = Location { - buffer, - range: start..end, - }; - Ok(LocationLink { origin, target }) - }) -} - -pub async fn location_links_from_lsp( - message: Option, - lsp_store: Entity, - buffer: Entity, - server_id: LanguageServerId, - mut cx: AsyncApp, -) -> Result> { - let message = match message { - Some(message) => message, - None => return Ok(Vec::new()), - }; - - let mut unresolved_links = Vec::new(); - match message { - lsp::GotoDefinitionResponse::Scalar(loc) => { - unresolved_links.push((None, loc.uri, loc.range)); - } - - lsp::GotoDefinitionResponse::Array(locs) => { - unresolved_links.extend(locs.into_iter().map(|l| (None, l.uri, l.range))); - } - - lsp::GotoDefinitionResponse::Link(links) => { - unresolved_links.extend(links.into_iter().map(|l| { - ( - l.origin_selection_range, - l.target_uri, - l.target_selection_range, - ) - })); - } - } - - let (_, language_server) = language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?; - let mut definitions = Vec::new(); - for (origin_range, target_uri, target_range) in unresolved_links { - let target_buffer_handle = lsp_store - .update(&mut cx, |this, cx| { - this.open_local_buffer_via_lsp(target_uri, language_server.server_id(), cx) - })? - .await?; - - cx.update(|cx| { - let origin_location = origin_range.map(|origin_range| { - let origin_buffer = buffer.read(cx); - let origin_start = - origin_buffer.clip_point_utf16(point_from_lsp(origin_range.start), Bias::Left); - let origin_end = - origin_buffer.clip_point_utf16(point_from_lsp(origin_range.end), Bias::Left); - Location { - buffer: buffer.clone(), - range: origin_buffer.anchor_after(origin_start) - ..origin_buffer.anchor_before(origin_end), - } - }); - - let target_buffer = target_buffer_handle.read(cx); - let target_start = - target_buffer.clip_point_utf16(point_from_lsp(target_range.start), Bias::Left); - let target_end = - target_buffer.clip_point_utf16(point_from_lsp(target_range.end), Bias::Left); - let target_location = Location { - buffer: target_buffer_handle, - range: target_buffer.anchor_after(target_start) - ..target_buffer.anchor_before(target_end), - }; - - definitions.push(LocationLink { - origin: origin_location, - target: target_location, - }) - })?; - } - Ok(definitions) -} - -pub async fn location_link_from_lsp( - link: lsp::LocationLink, - lsp_store: &Entity, - buffer: &Entity, - server_id: LanguageServerId, - cx: &mut AsyncApp, -) -> Result { - let (_, language_server) = language_server_for_buffer(lsp_store, buffer, server_id, cx)?; - - let (origin_range, target_uri, target_range) = ( - link.origin_selection_range, - link.target_uri, - link.target_selection_range, - ); - - let target_buffer_handle = lsp_store - .update(cx, |lsp_store, cx| { - lsp_store.open_local_buffer_via_lsp(target_uri, language_server.server_id(), cx) - })? - .await?; - - cx.update(|cx| { - let origin_location = origin_range.map(|origin_range| { - let origin_buffer = buffer.read(cx); - let origin_start = - origin_buffer.clip_point_utf16(point_from_lsp(origin_range.start), Bias::Left); - let origin_end = - origin_buffer.clip_point_utf16(point_from_lsp(origin_range.end), Bias::Left); - Location { - buffer: buffer.clone(), - range: origin_buffer.anchor_after(origin_start) - ..origin_buffer.anchor_before(origin_end), - } - }); - - let target_buffer = target_buffer_handle.read(cx); - let target_start = - target_buffer.clip_point_utf16(point_from_lsp(target_range.start), Bias::Left); - let target_end = - target_buffer.clip_point_utf16(point_from_lsp(target_range.end), Bias::Left); - let target_location = Location { - buffer: target_buffer_handle, - range: target_buffer.anchor_after(target_start) - ..target_buffer.anchor_before(target_end), - }; - - LocationLink { - origin: origin_location, - target: target_location, - } - }) -} - -pub fn location_links_to_proto( - links: Vec, - lsp_store: &mut LspStore, - peer_id: PeerId, - cx: &mut App, -) -> Vec { - links - .into_iter() - .map(|definition| location_link_to_proto(definition, lsp_store, peer_id, cx)) - .collect() -} - -pub fn location_link_to_proto( - location: LocationLink, - lsp_store: &mut LspStore, - peer_id: PeerId, - cx: &mut App, -) -> proto::LocationLink { - let origin = location.origin.map(|origin| { - lsp_store - .buffer_store() - .update(cx, |buffer_store, cx| { - buffer_store.create_buffer_for_peer(&origin.buffer, peer_id, cx) - }) - .detach_and_log_err(cx); - - let buffer_id = origin.buffer.read(cx).remote_id().into(); - proto::Location { - start: Some(serialize_anchor(&origin.range.start)), - end: Some(serialize_anchor(&origin.range.end)), - buffer_id, - } - }); - - lsp_store - .buffer_store() - .update(cx, |buffer_store, cx| { - buffer_store.create_buffer_for_peer(&location.target.buffer, peer_id, cx) - }) - .detach_and_log_err(cx); - - let buffer_id = location.target.buffer.read(cx).remote_id().into(); - let target = proto::Location { - start: Some(serialize_anchor(&location.target.range.start)), - end: Some(serialize_anchor(&location.target.range.end)), - buffer_id, - }; - - proto::LocationLink { - origin, - target: Some(target), - } -} - -#[async_trait(?Send)] -impl LspCommand for GetReferences { - type Response = Vec; - type LspRequest = lsp::request::References; - type ProtoRequest = proto::GetReferences; - - fn display_name(&self) -> &str { - "Find all references" - } - - fn status(&self) -> Option { - Some("Finding references...".to_owned()) - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - match &capabilities.server_capabilities.references_provider { - Some(OneOf::Left(has_support)) => *has_support, - Some(OneOf::Right(_)) => true, - None => false, - } - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(lsp::ReferenceParams { - text_document_position: make_lsp_text_document_position(path, self.position)?, - work_done_progress_params: Default::default(), - partial_result_params: Default::default(), - context: lsp::ReferenceContext { - include_declaration: true, - }, - }) - } - - async fn response_from_lsp( - self, - locations: Option>, - lsp_store: Entity, - buffer: Entity, - server_id: LanguageServerId, - mut cx: AsyncApp, - ) -> Result> { - let mut references = Vec::new(); - let (_, language_server) = - language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?; - - if let Some(locations) = locations { - for lsp_location in locations { - let target_buffer_handle = lsp_store - .update(&mut cx, |lsp_store, cx| { - lsp_store.open_local_buffer_via_lsp( - lsp_location.uri, - language_server.server_id(), - cx, - ) - })? - .await?; - - target_buffer_handle - .clone() - .read_with(&cx, |target_buffer, _| { - let target_start = target_buffer - .clip_point_utf16(point_from_lsp(lsp_location.range.start), Bias::Left); - let target_end = target_buffer - .clip_point_utf16(point_from_lsp(lsp_location.range.end), Bias::Left); - references.push(Location { - buffer: target_buffer_handle, - range: target_buffer.anchor_after(target_start) - ..target_buffer.anchor_before(target_end), - }); - })?; - } - } - - Ok(references) - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetReferences { - proto::GetReferences { - project_id, - buffer_id: buffer.remote_id().into(), - position: Some(language::proto::serialize_anchor( - &buffer.anchor_before(self.position), - )), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - message: proto::GetReferences, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - let position = message - .position - .and_then(deserialize_anchor) - .context("invalid position")?; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, - }) - } - - fn response_to_proto( - response: Vec, - lsp_store: &mut LspStore, - peer_id: PeerId, - _: &clock::Global, - cx: &mut App, - ) -> proto::GetReferencesResponse { - let locations = response - .into_iter() - .map(|definition| { - lsp_store - .buffer_store() - .update(cx, |buffer_store, cx| { - buffer_store.create_buffer_for_peer(&definition.buffer, peer_id, cx) - }) - .detach_and_log_err(cx); - let buffer_id = definition.buffer.read(cx).remote_id(); - proto::Location { - start: Some(serialize_anchor(&definition.range.start)), - end: Some(serialize_anchor(&definition.range.end)), - buffer_id: buffer_id.into(), - } - }) - .collect(); - proto::GetReferencesResponse { locations } - } - - async fn response_from_proto( - self, - message: proto::GetReferencesResponse, - project: Entity, - _: Entity, - mut cx: AsyncApp, - ) -> Result> { - let mut locations = Vec::new(); - for location in message.locations { - let buffer_id = BufferId::new(location.buffer_id)?; - let target_buffer = project - .update(&mut cx, |this, cx| { - this.wait_for_remote_buffer(buffer_id, cx) - })? - .await?; - let start = location - .start - .and_then(deserialize_anchor) - .context("missing target start")?; - let end = location - .end - .and_then(deserialize_anchor) - .context("missing target end")?; - target_buffer - .update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))? - .await?; - locations.push(Location { - buffer: target_buffer, - range: start..end, - }) - } - Ok(locations) - } - - fn buffer_id_from_proto(message: &proto::GetReferences) -> Result { - BufferId::new(message.buffer_id) - } -} - -#[async_trait(?Send)] -impl LspCommand for GetDocumentHighlights { - type Response = Vec; - type LspRequest = lsp::request::DocumentHighlightRequest; - type ProtoRequest = proto::GetDocumentHighlights; - - fn display_name(&self) -> &str { - "Get document highlights" - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - capabilities - .server_capabilities - .document_highlight_provider - .is_some_and(|capability| match capability { - OneOf::Left(supported) => supported, - OneOf::Right(_options) => true, - }) - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(lsp::DocumentHighlightParams { - text_document_position_params: make_lsp_text_document_position(path, self.position)?, - work_done_progress_params: Default::default(), - partial_result_params: Default::default(), - }) - } - - async fn response_from_lsp( - self, - lsp_highlights: Option>, - _: Entity, - buffer: Entity, - _: LanguageServerId, - cx: AsyncApp, - ) -> Result> { - buffer.read_with(&cx, |buffer, _| { - let mut lsp_highlights = lsp_highlights.unwrap_or_default(); - lsp_highlights.sort_unstable_by_key(|h| (h.range.start, Reverse(h.range.end))); - lsp_highlights - .into_iter() - .map(|lsp_highlight| { - let start = buffer - .clip_point_utf16(point_from_lsp(lsp_highlight.range.start), Bias::Left); - let end = buffer - .clip_point_utf16(point_from_lsp(lsp_highlight.range.end), Bias::Left); - DocumentHighlight { - range: buffer.anchor_after(start)..buffer.anchor_before(end), - kind: lsp_highlight - .kind - .unwrap_or(lsp::DocumentHighlightKind::READ), - } - }) - .collect() - }) - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDocumentHighlights { - proto::GetDocumentHighlights { - project_id, - buffer_id: buffer.remote_id().into(), - position: Some(language::proto::serialize_anchor( - &buffer.anchor_before(self.position), - )), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - message: proto::GetDocumentHighlights, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - let position = message - .position - .and_then(deserialize_anchor) - .context("invalid position")?; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, - }) - } - - fn response_to_proto( - response: Vec, - _: &mut LspStore, - _: PeerId, - _: &clock::Global, - _: &mut App, - ) -> proto::GetDocumentHighlightsResponse { - let highlights = response - .into_iter() - .map(|highlight| proto::DocumentHighlight { - start: Some(serialize_anchor(&highlight.range.start)), - end: Some(serialize_anchor(&highlight.range.end)), - kind: match highlight.kind { - DocumentHighlightKind::TEXT => proto::document_highlight::Kind::Text.into(), - DocumentHighlightKind::WRITE => proto::document_highlight::Kind::Write.into(), - DocumentHighlightKind::READ => proto::document_highlight::Kind::Read.into(), - _ => proto::document_highlight::Kind::Text.into(), - }, - }) - .collect(); - proto::GetDocumentHighlightsResponse { highlights } - } - - async fn response_from_proto( - self, - message: proto::GetDocumentHighlightsResponse, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result> { - let mut highlights = Vec::new(); - for highlight in message.highlights { - let start = highlight - .start - .and_then(deserialize_anchor) - .context("missing target start")?; - let end = highlight - .end - .and_then(deserialize_anchor) - .context("missing target end")?; - buffer - .update(&mut cx, |buffer, _| buffer.wait_for_anchors([start, end]))? - .await?; - let kind = match proto::document_highlight::Kind::from_i32(highlight.kind) { - Some(proto::document_highlight::Kind::Text) => DocumentHighlightKind::TEXT, - Some(proto::document_highlight::Kind::Read) => DocumentHighlightKind::READ, - Some(proto::document_highlight::Kind::Write) => DocumentHighlightKind::WRITE, - None => DocumentHighlightKind::TEXT, - }; - highlights.push(DocumentHighlight { - range: start..end, - kind, - }); - } - Ok(highlights) - } - - fn buffer_id_from_proto(message: &proto::GetDocumentHighlights) -> Result { - BufferId::new(message.buffer_id) - } -} - -#[async_trait(?Send)] -impl LspCommand for GetDocumentSymbols { - type Response = Vec; - type LspRequest = lsp::request::DocumentSymbolRequest; - type ProtoRequest = proto::GetDocumentSymbols; - - fn display_name(&self) -> &str { - "Get document symbols" - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - capabilities - .server_capabilities - .document_symbol_provider - .is_some_and(|capability| match capability { - OneOf::Left(supported) => supported, - OneOf::Right(_options) => true, - }) - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(lsp::DocumentSymbolParams { - text_document: make_text_document_identifier(path)?, - work_done_progress_params: Default::default(), - partial_result_params: Default::default(), - }) - } - - async fn response_from_lsp( - self, - lsp_symbols: Option, - _: Entity, - _: Entity, - _: LanguageServerId, - _: AsyncApp, - ) -> Result> { - let Some(lsp_symbols) = lsp_symbols else { - return Ok(Vec::new()); - }; - - let symbols: Vec<_> = match lsp_symbols { - lsp::DocumentSymbolResponse::Flat(symbol_information) => symbol_information - .into_iter() - .map(|lsp_symbol| DocumentSymbol { - name: lsp_symbol.name, - kind: lsp_symbol.kind, - range: range_from_lsp(lsp_symbol.location.range), - selection_range: range_from_lsp(lsp_symbol.location.range), - children: Vec::new(), - }) - .collect(), - lsp::DocumentSymbolResponse::Nested(nested_responses) => { - fn convert_symbol(lsp_symbol: lsp::DocumentSymbol) -> DocumentSymbol { - DocumentSymbol { - name: lsp_symbol.name, - kind: lsp_symbol.kind, - range: range_from_lsp(lsp_symbol.range), - selection_range: range_from_lsp(lsp_symbol.selection_range), - children: lsp_symbol - .children - .map(|children| { - children.into_iter().map(convert_symbol).collect::>() - }) - .unwrap_or_default(), - } - } - nested_responses.into_iter().map(convert_symbol).collect() - } - }; - Ok(symbols) - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDocumentSymbols { - proto::GetDocumentSymbols { - project_id, - buffer_id: buffer.remote_id().into(), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - message: proto::GetDocumentSymbols, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - Ok(Self) - } - - fn response_to_proto( - response: Vec, - _: &mut LspStore, - _: PeerId, - _: &clock::Global, - _: &mut App, - ) -> proto::GetDocumentSymbolsResponse { - let symbols = response - .into_iter() - .map(|symbol| { - fn convert_symbol_to_proto(symbol: DocumentSymbol) -> proto::DocumentSymbol { - proto::DocumentSymbol { - name: symbol.name.clone(), - kind: unsafe { mem::transmute::(symbol.kind) }, - start: Some(proto::PointUtf16 { - row: symbol.range.start.0.row, - column: symbol.range.start.0.column, - }), - end: Some(proto::PointUtf16 { - row: symbol.range.end.0.row, - column: symbol.range.end.0.column, - }), - selection_start: Some(proto::PointUtf16 { - row: symbol.selection_range.start.0.row, - column: symbol.selection_range.start.0.column, - }), - selection_end: Some(proto::PointUtf16 { - row: symbol.selection_range.end.0.row, - column: symbol.selection_range.end.0.column, - }), - children: symbol - .children - .into_iter() - .map(convert_symbol_to_proto) - .collect(), - } - } - convert_symbol_to_proto(symbol) - }) - .collect::>(); - - proto::GetDocumentSymbolsResponse { symbols } - } - - async fn response_from_proto( - self, - message: proto::GetDocumentSymbolsResponse, - _: Entity, - _: Entity, - _: AsyncApp, - ) -> Result> { - let mut symbols = Vec::with_capacity(message.symbols.len()); - for serialized_symbol in message.symbols { - fn deserialize_symbol_with_children( - serialized_symbol: proto::DocumentSymbol, - ) -> Result { - let kind = - unsafe { mem::transmute::(serialized_symbol.kind) }; - - let start = serialized_symbol.start.context("invalid start")?; - let end = serialized_symbol.end.context("invalid end")?; - - let selection_start = serialized_symbol - .selection_start - .context("invalid selection start")?; - let selection_end = serialized_symbol - .selection_end - .context("invalid selection end")?; - - Ok(DocumentSymbol { - name: serialized_symbol.name, - kind, - range: Unclipped(PointUtf16::new(start.row, start.column)) - ..Unclipped(PointUtf16::new(end.row, end.column)), - selection_range: Unclipped(PointUtf16::new( - selection_start.row, - selection_start.column, - )) - ..Unclipped(PointUtf16::new(selection_end.row, selection_end.column)), - children: serialized_symbol - .children - .into_iter() - .filter_map(|symbol| deserialize_symbol_with_children(symbol).ok()) - .collect::>(), - }) - } - - symbols.push(deserialize_symbol_with_children(serialized_symbol)?); - } - - Ok(symbols) - } - - fn buffer_id_from_proto(message: &proto::GetDocumentSymbols) -> Result { - BufferId::new(message.buffer_id) - } -} - -#[async_trait(?Send)] -impl LspCommand for GetSignatureHelp { - type Response = Option; - type LspRequest = lsp::SignatureHelpRequest; - type ProtoRequest = proto::GetSignatureHelp; - - fn display_name(&self) -> &str { - "Get signature help" - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - capabilities - .server_capabilities - .signature_help_provider - .is_some() - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _cx: &App, - ) -> Result { - Ok(lsp::SignatureHelpParams { - text_document_position_params: make_lsp_text_document_position(path, self.position)?, - context: None, - work_done_progress_params: Default::default(), - }) - } - - async fn response_from_lsp( - self, - message: Option, - lsp_store: Entity, - _: Entity, - id: LanguageServerId, - cx: AsyncApp, - ) -> Result { - let Some(message) = message else { - return Ok(None); - }; - cx.update(|cx| { - SignatureHelp::new( - message, - Some(lsp_store.read(cx).languages.clone()), - Some(id), - cx, - ) - }) - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest { - let offset = buffer.point_utf16_to_offset(self.position); - proto::GetSignatureHelp { - project_id, - buffer_id: buffer.remote_id().to_proto(), - position: Some(serialize_anchor(&buffer.anchor_after(offset))), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - payload: Self::ProtoRequest, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&payload.version)) - })? - .await - .with_context(|| format!("waiting for version for buffer {}", buffer.entity_id()))?; - let buffer_snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot())?; - Ok(Self { - position: payload - .position - .and_then(deserialize_anchor) - .context("invalid position")? - .to_point_utf16(&buffer_snapshot), - }) - } - - fn response_to_proto( - response: Self::Response, - _: &mut LspStore, - _: PeerId, - _: &Global, - _: &mut App, - ) -> proto::GetSignatureHelpResponse { - proto::GetSignatureHelpResponse { - signature_help: response - .map(|signature_help| lsp_to_proto_signature(signature_help.original_data)), - } - } - - async fn response_from_proto( - self, - response: proto::GetSignatureHelpResponse, - lsp_store: Entity, - _: Entity, - cx: AsyncApp, - ) -> Result { - cx.update(|cx| { - response - .signature_help - .map(proto_to_lsp_signature) - .and_then(|signature| { - SignatureHelp::new( - signature, - Some(lsp_store.read(cx).languages.clone()), - None, - cx, - ) - }) - }) - } - - fn buffer_id_from_proto(message: &Self::ProtoRequest) -> Result { - BufferId::new(message.buffer_id) - } -} - -#[async_trait(?Send)] -impl LspCommand for GetHover { - type Response = Option; - type LspRequest = lsp::request::HoverRequest; - type ProtoRequest = proto::GetHover; - - fn display_name(&self) -> &str { - "Get hover" - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - match capabilities.server_capabilities.hover_provider { - Some(lsp::HoverProviderCapability::Simple(enabled)) => enabled, - Some(lsp::HoverProviderCapability::Options(_)) => true, - None => false, - } - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(lsp::HoverParams { - text_document_position_params: make_lsp_text_document_position(path, self.position)?, - work_done_progress_params: Default::default(), - }) - } - - async fn response_from_lsp( - self, - message: Option, - _: Entity, - buffer: Entity, - _: LanguageServerId, - cx: AsyncApp, - ) -> Result { - let Some(hover) = message else { - return Ok(None); - }; - - let (language, range) = buffer.read_with(&cx, |buffer, _| { - ( - buffer.language().cloned(), - hover.range.map(|range| { - let token_start = - buffer.clip_point_utf16(point_from_lsp(range.start), Bias::Left); - let token_end = buffer.clip_point_utf16(point_from_lsp(range.end), Bias::Left); - buffer.anchor_after(token_start)..buffer.anchor_before(token_end) - }), - ) - })?; - - fn hover_blocks_from_marked_string(marked_string: lsp::MarkedString) -> Option { - let block = match marked_string { - lsp::MarkedString::String(content) => HoverBlock { - text: content, - kind: HoverBlockKind::Markdown, - }, - lsp::MarkedString::LanguageString(lsp::LanguageString { language, value }) => { - HoverBlock { - text: value, - kind: HoverBlockKind::Code { language }, - } - } - }; - if block.text.is_empty() { - None - } else { - Some(block) - } - } - - let contents = match hover.contents { - lsp::HoverContents::Scalar(marked_string) => { - hover_blocks_from_marked_string(marked_string) - .into_iter() - .collect() - } - lsp::HoverContents::Array(marked_strings) => marked_strings - .into_iter() - .filter_map(hover_blocks_from_marked_string) - .collect(), - lsp::HoverContents::Markup(markup_content) => vec![HoverBlock { - text: markup_content.value, - kind: if markup_content.kind == lsp::MarkupKind::Markdown { - HoverBlockKind::Markdown - } else { - HoverBlockKind::PlainText - }, - }], - }; - - Ok(Some(Hover { - contents, - range, - language, - })) - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest { - proto::GetHover { - project_id, - buffer_id: buffer.remote_id().into(), - position: Some(language::proto::serialize_anchor( - &buffer.anchor_before(self.position), - )), - version: serialize_version(&buffer.version), - } - } - - async fn from_proto( - message: Self::ProtoRequest, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - let position = message - .position - .and_then(deserialize_anchor) - .context("invalid position")?; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, - }) - } - - fn response_to_proto( - response: Self::Response, - _: &mut LspStore, - _: PeerId, - _: &clock::Global, - _: &mut App, - ) -> proto::GetHoverResponse { - if let Some(response) = response { - let (start, end) = if let Some(range) = response.range { - ( - Some(language::proto::serialize_anchor(&range.start)), - Some(language::proto::serialize_anchor(&range.end)), - ) - } else { - (None, None) - }; - - let contents = response - .contents - .into_iter() - .map(|block| proto::HoverBlock { - text: block.text, - is_markdown: block.kind == HoverBlockKind::Markdown, - language: if let HoverBlockKind::Code { language } = block.kind { - Some(language) - } else { - None - }, - }) - .collect(); - - proto::GetHoverResponse { - start, - end, - contents, - } - } else { - proto::GetHoverResponse { - start: None, - end: None, - contents: Vec::new(), - } - } - } - - async fn response_from_proto( - self, - message: proto::GetHoverResponse, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - let contents: Vec<_> = message - .contents - .into_iter() - .map(|block| HoverBlock { - text: block.text, - kind: if let Some(language) = block.language { - HoverBlockKind::Code { language } - } else if block.is_markdown { - HoverBlockKind::Markdown - } else { - HoverBlockKind::PlainText - }, - }) - .collect(); - if contents.is_empty() { - return Ok(None); - } - - let language = buffer.read_with(&cx, |buffer, _| buffer.language().cloned())?; - let range = if let (Some(start), Some(end)) = (message.start, message.end) { - language::proto::deserialize_anchor(start) - .and_then(|start| language::proto::deserialize_anchor(end).map(|end| start..end)) - } else { - None - }; - if let Some(range) = range.as_ref() { - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_anchors([range.start, range.end]) - })? - .await?; - } - - Ok(Some(Hover { - contents, - range, - language, - })) - } - - fn buffer_id_from_proto(message: &Self::ProtoRequest) -> Result { - BufferId::new(message.buffer_id) - } -} - -impl GetCompletions { - pub fn can_resolve_completions(capabilities: &lsp::ServerCapabilities) -> bool { - capabilities - .completion_provider - .as_ref() - .and_then(|options| options.resolve_provider) - .unwrap_or(false) - } -} - -#[async_trait(?Send)] -impl LspCommand for GetCompletions { - type Response = CoreCompletionResponse; - type LspRequest = lsp::request::Completion; - type ProtoRequest = proto::GetCompletions; - - fn display_name(&self) -> &str { - "Get completion" - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - capabilities - .server_capabilities - .completion_provider - .is_some() - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(lsp::CompletionParams { - text_document_position: make_lsp_text_document_position(path, self.position)?, - context: Some(self.context.clone()), - work_done_progress_params: Default::default(), - partial_result_params: Default::default(), - }) - } - - async fn response_from_lsp( - self, - completions: Option, - lsp_store: Entity, - buffer: Entity, - server_id: LanguageServerId, - mut cx: AsyncApp, - ) -> Result { - let mut response_list = None; - let (mut completions, mut is_incomplete) = if let Some(completions) = completions { - match completions { - lsp::CompletionResponse::Array(completions) => (completions, false), - lsp::CompletionResponse::List(mut list) => { - let is_incomplete = list.is_incomplete; - let items = std::mem::take(&mut list.items); - response_list = Some(list); - (items, is_incomplete) - } - } - } else { - (Vec::new(), false) - }; - - let unfiltered_completions_count = completions.len(); - - let language_server_adapter = lsp_store - .read_with(&cx, |lsp_store, _| { - lsp_store.language_server_adapter_for_id(server_id) - })? - .with_context(|| format!("no language server with id {server_id}"))?; - - let lsp_defaults = response_list - .as_ref() - .and_then(|list| list.item_defaults.clone()) - .map(Arc::new); - - let mut completion_edits = Vec::new(); - buffer.update(&mut cx, |buffer, _cx| { - let snapshot = buffer.snapshot(); - let clipped_position = buffer.clip_point_utf16(Unclipped(self.position), Bias::Left); - - let mut range_for_token = None; - completions.retain(|lsp_completion| { - let lsp_edit = lsp_completion.text_edit.clone().or_else(|| { - let default_text_edit = lsp_defaults.as_deref()?.edit_range.as_ref()?; - let new_text = lsp_completion - .text_edit_text - .as_ref() - .unwrap_or(&lsp_completion.label) - .clone(); - match default_text_edit { - CompletionListItemDefaultsEditRange::Range(range) => { - Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: *range, - new_text, - })) - } - CompletionListItemDefaultsEditRange::InsertAndReplace { - insert, - replace, - } => Some(lsp::CompletionTextEdit::InsertAndReplace( - lsp::InsertReplaceEdit { - new_text, - insert: *insert, - replace: *replace, - }, - )), - } - }); - - let edit = match lsp_edit { - // If the language server provides a range to overwrite, then - // check that the range is valid. - Some(completion_text_edit) => { - match parse_completion_text_edit(&completion_text_edit, &snapshot) { - Some(edit) => edit, - None => return false, - } - } - // If the language server does not provide a range, then infer - // the range based on the syntax tree. - None => { - if self.position != clipped_position { - log::info!("completion out of expected range "); - return false; - } - - let default_edit_range = lsp_defaults.as_ref().and_then(|lsp_defaults| { - lsp_defaults - .edit_range - .as_ref() - .and_then(|range| match range { - CompletionListItemDefaultsEditRange::Range(r) => Some(r), - _ => None, - }) - }); - - let range = if let Some(range) = default_edit_range { - let range = range_from_lsp(*range); - let start = snapshot.clip_point_utf16(range.start, Bias::Left); - let end = snapshot.clip_point_utf16(range.end, Bias::Left); - if start != range.start.0 || end != range.end.0 { - log::info!("completion out of expected range"); - return false; - } - - snapshot.anchor_before(start)..snapshot.anchor_after(end) - } else { - range_for_token - .get_or_insert_with(|| { - let offset = self.position.to_offset(&snapshot); - let (range, kind) = snapshot.surrounding_word( - offset, - Some(CharScopeContext::Completion), - ); - let range = if kind == Some(CharKind::Word) { - range - } else { - offset..offset - }; - - snapshot.anchor_before(range.start) - ..snapshot.anchor_after(range.end) - }) - .clone() - }; - - // We already know text_edit is None here - let text = lsp_completion - .insert_text - .as_ref() - .unwrap_or(&lsp_completion.label) - .clone(); - - ParsedCompletionEdit { - replace_range: range, - insert_range: None, - new_text: text, - } - } - }; - - completion_edits.push(edit); - true - }); - })?; - - // If completions were filtered out due to errors that may be transient, mark the result - // incomplete so that it is re-queried. - if unfiltered_completions_count != completions.len() { - is_incomplete = true; - } - - language_server_adapter - .process_completions(&mut completions) - .await; - - let completions = completions - .into_iter() - .zip(completion_edits) - .map(|(mut lsp_completion, mut edit)| { - LineEnding::normalize(&mut edit.new_text); - if lsp_completion.data.is_none() - && let Some(default_data) = lsp_defaults - .as_ref() - .and_then(|item_defaults| item_defaults.data.clone()) - { - // Servers (e.g. JDTLS) prefer unchanged completions, when resolving the items later, - // so we do not insert the defaults here, but `data` is needed for resolving, so this is an exception. - lsp_completion.data = Some(default_data); - } - CoreCompletion { - replace_range: edit.replace_range, - new_text: edit.new_text, - source: CompletionSource::Lsp { - insert_range: edit.insert_range, - server_id, - lsp_completion: Box::new(lsp_completion), - lsp_defaults: lsp_defaults.clone(), - resolved: false, - }, - } - }) - .collect(); - - Ok(CoreCompletionResponse { - completions, - is_incomplete, - }) - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetCompletions { - let anchor = buffer.anchor_after(self.position); - proto::GetCompletions { - project_id, - buffer_id: buffer.remote_id().into(), - position: Some(language::proto::serialize_anchor(&anchor)), - version: serialize_version(&buffer.version()), - server_id: self.server_id.map(|id| id.to_proto()), - } - } - - async fn from_proto( - message: proto::GetCompletions, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - let version = deserialize_version(&message.version); - buffer - .update(&mut cx, |buffer, _| buffer.wait_for_version(version))? - .await?; - let position = message - .position - .and_then(language::proto::deserialize_anchor) - .map(|p| { - buffer.read_with(&cx, |buffer, _| { - buffer.clip_point_utf16(Unclipped(p.to_point_utf16(buffer)), Bias::Left) - }) - }) - .context("invalid position")??; - Ok(Self { - position, - context: CompletionContext { - trigger_kind: CompletionTriggerKind::INVOKED, - trigger_character: None, - }, - server_id: message - .server_id - .map(|id| lsp::LanguageServerId::from_proto(id)), - }) - } - - fn response_to_proto( - response: CoreCompletionResponse, - _: &mut LspStore, - _: PeerId, - buffer_version: &clock::Global, - _: &mut App, - ) -> proto::GetCompletionsResponse { - proto::GetCompletionsResponse { - completions: response - .completions - .iter() - .map(LspStore::serialize_completion) - .collect(), - version: serialize_version(buffer_version), - can_reuse: !response.is_incomplete, - } - } - - async fn response_from_proto( - self, - message: proto::GetCompletionsResponse, - _project: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - - let completions = message - .completions - .into_iter() - .map(LspStore::deserialize_completion) - .collect::>>()?; - - Ok(CoreCompletionResponse { - completions, - is_incomplete: !message.can_reuse, - }) - } - - fn buffer_id_from_proto(message: &proto::GetCompletions) -> Result { - BufferId::new(message.buffer_id) - } -} - -pub struct ParsedCompletionEdit { - pub replace_range: Range, - pub insert_range: Option>, - pub new_text: String, -} - -pub(crate) fn parse_completion_text_edit( - edit: &lsp::CompletionTextEdit, - snapshot: &BufferSnapshot, -) -> Option { - let (replace_range, insert_range, new_text) = match edit { - lsp::CompletionTextEdit::Edit(edit) => (edit.range, None, &edit.new_text), - lsp::CompletionTextEdit::InsertAndReplace(edit) => { - (edit.replace, Some(edit.insert), &edit.new_text) - } - }; - - let replace_range = { - let range = range_from_lsp(replace_range); - let start = snapshot.clip_point_utf16(range.start, Bias::Left); - let end = snapshot.clip_point_utf16(range.end, Bias::Left); - if start != range.start.0 || end != range.end.0 { - log::info!( - "completion out of expected range, start: {start:?}, end: {end:?}, range: {range:?}" - ); - return None; - } - snapshot.anchor_before(start)..snapshot.anchor_after(end) - }; - - let insert_range = match insert_range { - None => None, - Some(insert_range) => { - let range = range_from_lsp(insert_range); - let start = snapshot.clip_point_utf16(range.start, Bias::Left); - let end = snapshot.clip_point_utf16(range.end, Bias::Left); - if start != range.start.0 || end != range.end.0 { - log::info!("completion (insert) out of expected range"); - return None; - } - Some(snapshot.anchor_before(start)..snapshot.anchor_after(end)) - } - }; - - Some(ParsedCompletionEdit { - insert_range, - replace_range, - new_text: new_text.clone(), - }) -} - -#[async_trait(?Send)] -impl LspCommand for GetCodeActions { - type Response = Vec; - type LspRequest = lsp::request::CodeActionRequest; - type ProtoRequest = proto::GetCodeActions; - - fn display_name(&self) -> &str { - "Get code actions" - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - match &capabilities.server_capabilities.code_action_provider { - None => false, - Some(lsp::CodeActionProviderCapability::Simple(false)) => false, - _ => { - // If we do know that we want specific code actions AND we know that - // the server only supports specific code actions, then we want to filter - // down to the ones that are supported. - if let Some((requested, supported)) = self - .kinds - .as_ref() - .zip(Self::supported_code_action_kinds(capabilities)) - { - let server_supported = supported.into_iter().collect::>(); - requested.iter().any(|kind| server_supported.contains(kind)) - } else { - true - } - } - } - } - - fn to_lsp( - &self, - path: &Path, - buffer: &Buffer, - language_server: &Arc, - _: &App, - ) -> Result { - let mut relevant_diagnostics = Vec::new(); - for entry in buffer - .snapshot() - .diagnostics_in_range::<_, language::PointUtf16>(self.range.clone(), false) - { - relevant_diagnostics.push(entry.to_lsp_diagnostic_stub()?); - } - - let supported = - Self::supported_code_action_kinds(language_server.adapter_server_capabilities()); - - let only = if let Some(requested) = &self.kinds { - if let Some(supported_kinds) = supported { - let server_supported = supported_kinds.into_iter().collect::>(); - - let filtered = requested - .iter() - .filter(|kind| server_supported.contains(kind)) - .cloned() - .collect(); - Some(filtered) - } else { - Some(requested.clone()) - } - } else { - supported - }; - - Ok(lsp::CodeActionParams { - text_document: make_text_document_identifier(path)?, - range: range_to_lsp(self.range.to_point_utf16(buffer))?, - work_done_progress_params: Default::default(), - partial_result_params: Default::default(), - context: lsp::CodeActionContext { - diagnostics: relevant_diagnostics, - only, - ..lsp::CodeActionContext::default() - }, - }) - } - - async fn response_from_lsp( - self, - actions: Option, - lsp_store: Entity, - _: Entity, - server_id: LanguageServerId, - cx: AsyncApp, - ) -> Result> { - let requested_kinds_set = self - .kinds - .map(|kinds| kinds.into_iter().collect::>()); - - let language_server = cx.update(|cx| { - lsp_store - .read(cx) - .language_server_for_id(server_id) - .with_context(|| { - format!("Missing the language server that just returned a response {server_id}") - }) - })??; - - let server_capabilities = language_server.capabilities(); - let available_commands = server_capabilities - .execute_command_provider - .as_ref() - .map(|options| options.commands.as_slice()) - .unwrap_or_default(); - Ok(actions - .unwrap_or_default() - .into_iter() - .filter_map(|entry| { - let (lsp_action, resolved) = match entry { - lsp::CodeActionOrCommand::CodeAction(lsp_action) => { - if let Some(command) = lsp_action.command.as_ref() - && !available_commands.contains(&command.command) - { - return None; - } - (LspAction::Action(Box::new(lsp_action)), false) - } - lsp::CodeActionOrCommand::Command(command) => { - if available_commands.contains(&command.command) { - (LspAction::Command(command), true) - } else { - return None; - } - } - }; - - if let Some((requested_kinds, kind)) = - requested_kinds_set.as_ref().zip(lsp_action.action_kind()) - && !requested_kinds.contains(&kind) - { - return None; - } - - Some(CodeAction { - server_id, - range: self.range.clone(), - lsp_action, - resolved, - }) - }) - .collect()) - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetCodeActions { - proto::GetCodeActions { - project_id, - buffer_id: buffer.remote_id().into(), - start: Some(language::proto::serialize_anchor(&self.range.start)), - end: Some(language::proto::serialize_anchor(&self.range.end)), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - message: proto::GetCodeActions, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - let start = message - .start - .and_then(language::proto::deserialize_anchor) - .context("invalid start")?; - let end = message - .end - .and_then(language::proto::deserialize_anchor) - .context("invalid end")?; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - - Ok(Self { - range: start..end, - kinds: None, - }) - } - - fn response_to_proto( - code_actions: Vec, - _: &mut LspStore, - _: PeerId, - buffer_version: &clock::Global, - _: &mut App, - ) -> proto::GetCodeActionsResponse { - proto::GetCodeActionsResponse { - actions: code_actions - .iter() - .map(LspStore::serialize_code_action) - .collect(), - version: serialize_version(buffer_version), - } - } - - async fn response_from_proto( - self, - message: proto::GetCodeActionsResponse, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result> { - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - message - .actions - .into_iter() - .map(LspStore::deserialize_code_action) - .collect() - } - - fn buffer_id_from_proto(message: &proto::GetCodeActions) -> Result { - BufferId::new(message.buffer_id) - } -} - -impl GetCodeActions { - fn supported_code_action_kinds( - capabilities: AdapterServerCapabilities, - ) -> Option> { - match capabilities.server_capabilities.code_action_provider { - Some(lsp::CodeActionProviderCapability::Options(CodeActionOptions { - code_action_kinds: Some(supported_action_kinds), - .. - })) => Some(supported_action_kinds), - _ => capabilities.code_action_kinds, - } - } - - pub fn can_resolve_actions(capabilities: &ServerCapabilities) -> bool { - capabilities - .code_action_provider - .as_ref() - .and_then(|options| match options { - lsp::CodeActionProviderCapability::Simple(_is_supported) => None, - lsp::CodeActionProviderCapability::Options(options) => options.resolve_provider, - }) - .unwrap_or(false) - } -} - -impl OnTypeFormatting { - pub fn supports_on_type_formatting(trigger: &str, capabilities: &ServerCapabilities) -> bool { - let Some(on_type_formatting_options) = &capabilities.document_on_type_formatting_provider - else { - return false; - }; - on_type_formatting_options - .first_trigger_character - .contains(trigger) - || on_type_formatting_options - .more_trigger_character - .iter() - .flatten() - .any(|chars| chars.contains(trigger)) - } -} - -#[async_trait(?Send)] -impl LspCommand for OnTypeFormatting { - type Response = Option; - type LspRequest = lsp::request::OnTypeFormatting; - type ProtoRequest = proto::OnTypeFormatting; - - fn display_name(&self) -> &str { - "Formatting on typing" - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - Self::supports_on_type_formatting(&self.trigger, &capabilities.server_capabilities) - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(lsp::DocumentOnTypeFormattingParams { - text_document_position: make_lsp_text_document_position(path, self.position)?, - ch: self.trigger.clone(), - options: self.options.clone(), - }) - } - - async fn response_from_lsp( - self, - message: Option>, - lsp_store: Entity, - buffer: Entity, - server_id: LanguageServerId, - mut cx: AsyncApp, - ) -> Result> { - if let Some(edits) = message { - let (lsp_adapter, lsp_server) = - language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?; - LocalLspStore::deserialize_text_edits( - lsp_store, - buffer, - edits, - self.push_to_history, - lsp_adapter, - lsp_server, - &mut cx, - ) - .await - } else { - Ok(None) - } - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::OnTypeFormatting { - proto::OnTypeFormatting { - project_id, - buffer_id: buffer.remote_id().into(), - position: Some(language::proto::serialize_anchor( - &buffer.anchor_before(self.position), - )), - trigger: self.trigger.clone(), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - message: proto::OnTypeFormatting, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - let position = message - .position - .and_then(deserialize_anchor) - .context("invalid position")?; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - - let options = buffer.update(&mut cx, |buffer, cx| { - lsp_formatting_options( - language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx).as_ref(), - ) - })?; - - Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, - trigger: message.trigger.clone(), - options, - push_to_history: false, - }) - } - - fn response_to_proto( - response: Option, - _: &mut LspStore, - _: PeerId, - _: &clock::Global, - _: &mut App, - ) -> proto::OnTypeFormattingResponse { - proto::OnTypeFormattingResponse { - transaction: response - .map(|transaction| language::proto::serialize_transaction(&transaction)), - } - } - - async fn response_from_proto( - self, - message: proto::OnTypeFormattingResponse, - _: Entity, - _: Entity, - _: AsyncApp, - ) -> Result> { - let Some(transaction) = message.transaction else { - return Ok(None); - }; - Ok(Some(language::proto::deserialize_transaction(transaction)?)) - } - - fn buffer_id_from_proto(message: &proto::OnTypeFormatting) -> Result { - BufferId::new(message.buffer_id) - } -} - -impl InlayHints { - pub async fn lsp_to_project_hint( - lsp_hint: lsp::InlayHint, - buffer_handle: &Entity, - server_id: LanguageServerId, - resolve_state: ResolveState, - force_no_type_left_padding: bool, - cx: &mut AsyncApp, - ) -> anyhow::Result { - let kind = lsp_hint.kind.and_then(|kind| match kind { - lsp::InlayHintKind::TYPE => Some(InlayHintKind::Type), - lsp::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter), - _ => None, - }); - - let position = buffer_handle.read_with(cx, |buffer, _| { - let position = buffer.clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left); - if kind == Some(InlayHintKind::Parameter) { - buffer.anchor_before(position) - } else { - buffer.anchor_after(position) - } - })?; - let label = Self::lsp_inlay_label_to_project(lsp_hint.label, server_id) - .await - .context("lsp to project inlay hint conversion")?; - let padding_left = if force_no_type_left_padding && kind == Some(InlayHintKind::Type) { - false - } else { - lsp_hint.padding_left.unwrap_or(false) - }; - - Ok(InlayHint { - position, - padding_left, - padding_right: lsp_hint.padding_right.unwrap_or(false), - label, - kind, - tooltip: lsp_hint.tooltip.map(|tooltip| match tooltip { - lsp::InlayHintTooltip::String(s) => InlayHintTooltip::String(s), - lsp::InlayHintTooltip::MarkupContent(markup_content) => { - InlayHintTooltip::MarkupContent(MarkupContent { - kind: match markup_content.kind { - lsp::MarkupKind::PlainText => HoverBlockKind::PlainText, - lsp::MarkupKind::Markdown => HoverBlockKind::Markdown, - }, - value: markup_content.value, - }) - } - }), - resolve_state, - }) - } - - async fn lsp_inlay_label_to_project( - lsp_label: lsp::InlayHintLabel, - server_id: LanguageServerId, - ) -> anyhow::Result { - let label = match lsp_label { - lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s), - lsp::InlayHintLabel::LabelParts(lsp_parts) => { - let mut parts = Vec::with_capacity(lsp_parts.len()); - for lsp_part in lsp_parts { - parts.push(InlayHintLabelPart { - value: lsp_part.value, - tooltip: lsp_part.tooltip.map(|tooltip| match tooltip { - lsp::InlayHintLabelPartTooltip::String(s) => { - InlayHintLabelPartTooltip::String(s) - } - lsp::InlayHintLabelPartTooltip::MarkupContent(markup_content) => { - InlayHintLabelPartTooltip::MarkupContent(MarkupContent { - kind: match markup_content.kind { - lsp::MarkupKind::PlainText => HoverBlockKind::PlainText, - lsp::MarkupKind::Markdown => HoverBlockKind::Markdown, - }, - value: markup_content.value, - }) - } - }), - location: Some(server_id).zip(lsp_part.location), - }); - } - InlayHintLabel::LabelParts(parts) - } - }; - - Ok(label) - } - - pub fn project_to_proto_hint(response_hint: InlayHint) -> proto::InlayHint { - let (state, lsp_resolve_state) = match response_hint.resolve_state { - ResolveState::Resolved => (0, None), - ResolveState::CanResolve(server_id, resolve_data) => ( - 1, - Some(proto::resolve_state::LspResolveState { - server_id: server_id.0 as u64, - value: resolve_data.map(|json_data| { - serde_json::to_string(&json_data) - .expect("failed to serialize resolve json data") - }), - }), - ), - ResolveState::Resolving => (2, None), - }; - let resolve_state = Some(proto::ResolveState { - state, - lsp_resolve_state, - }); - proto::InlayHint { - position: Some(language::proto::serialize_anchor(&response_hint.position)), - padding_left: response_hint.padding_left, - padding_right: response_hint.padding_right, - label: Some(proto::InlayHintLabel { - label: Some(match response_hint.label { - InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s), - InlayHintLabel::LabelParts(label_parts) => { - proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts { - parts: label_parts.into_iter().map(|label_part| { - let location_url = label_part.location.as_ref().map(|(_, location)| location.uri.to_string()); - let location_range_start = label_part.location.as_ref().map(|(_, location)| point_from_lsp(location.range.start).0).map(|point| proto::PointUtf16 { row: point.row, column: point.column }); - let location_range_end = label_part.location.as_ref().map(|(_, location)| point_from_lsp(location.range.end).0).map(|point| proto::PointUtf16 { row: point.row, column: point.column }); - proto::InlayHintLabelPart { - value: label_part.value, - tooltip: label_part.tooltip.map(|tooltip| { - let proto_tooltip = match tooltip { - InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s), - InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent { - is_markdown: markup_content.kind == HoverBlockKind::Markdown, - value: markup_content.value, - }), - }; - proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)} - }), - location_url, - location_range_start, - location_range_end, - language_server_id: label_part.location.as_ref().map(|(server_id, _)| server_id.0 as u64), - }}).collect() - }) - } - }), - }), - kind: response_hint.kind.map(|kind| kind.name().to_string()), - tooltip: response_hint.tooltip.map(|response_tooltip| { - let proto_tooltip = match response_tooltip { - InlayHintTooltip::String(s) => proto::inlay_hint_tooltip::Content::Value(s), - InlayHintTooltip::MarkupContent(markup_content) => { - proto::inlay_hint_tooltip::Content::MarkupContent(proto::MarkupContent { - is_markdown: markup_content.kind == HoverBlockKind::Markdown, - value: markup_content.value, - }) - } - }; - proto::InlayHintTooltip { - content: Some(proto_tooltip), - } - }), - resolve_state, - } - } - - pub fn proto_to_project_hint(message_hint: proto::InlayHint) -> anyhow::Result { - let resolve_state = message_hint.resolve_state.as_ref().unwrap_or_else(|| { - panic!("incorrect proto inlay hint message: no resolve state in hint {message_hint:?}",) - }); - let resolve_state_data = resolve_state - .lsp_resolve_state.as_ref() - .map(|lsp_resolve_state| { - let value = lsp_resolve_state.value.as_deref().map(|value| { - serde_json::from_str::>(value) - .with_context(|| format!("incorrect proto inlay hint message: non-json resolve state {lsp_resolve_state:?}")) - }).transpose()?.flatten(); - anyhow::Ok((LanguageServerId(lsp_resolve_state.server_id as usize), value)) - }) - .transpose()?; - let resolve_state = match resolve_state.state { - 0 => ResolveState::Resolved, - 1 => { - let (server_id, lsp_resolve_state) = resolve_state_data.with_context(|| { - format!( - "No lsp resolve data for the hint that can be resolved: {message_hint:?}" - ) - })?; - ResolveState::CanResolve(server_id, lsp_resolve_state) - } - 2 => ResolveState::Resolving, - invalid => { - anyhow::bail!("Unexpected resolve state {invalid} for hint {message_hint:?}") - } - }; - Ok(InlayHint { - position: message_hint - .position - .and_then(language::proto::deserialize_anchor) - .context("invalid position")?, - label: match message_hint - .label - .and_then(|label| label.label) - .context("missing label")? - { - proto::inlay_hint_label::Label::Value(s) => InlayHintLabel::String(s), - proto::inlay_hint_label::Label::LabelParts(parts) => { - let mut label_parts = Vec::new(); - for part in parts.parts { - label_parts.push(InlayHintLabelPart { - value: part.value, - tooltip: part.tooltip.map(|tooltip| match tooltip.content { - Some(proto::inlay_hint_label_part_tooltip::Content::Value(s)) => { - InlayHintLabelPartTooltip::String(s) - } - Some( - proto::inlay_hint_label_part_tooltip::Content::MarkupContent( - markup_content, - ), - ) => InlayHintLabelPartTooltip::MarkupContent(MarkupContent { - kind: if markup_content.is_markdown { - HoverBlockKind::Markdown - } else { - HoverBlockKind::PlainText - }, - value: markup_content.value, - }), - None => InlayHintLabelPartTooltip::String(String::new()), - }), - location: { - match part - .location_url - .zip( - part.location_range_start.and_then(|start| { - Some(start..part.location_range_end?) - }), - ) - .zip(part.language_server_id) - { - Some(((uri, range), server_id)) => Some(( - LanguageServerId(server_id as usize), - lsp::Location { - uri: lsp::Uri::from_str(&uri) - .context("invalid uri in hint part {part:?}")?, - range: lsp::Range::new( - point_to_lsp(PointUtf16::new( - range.start.row, - range.start.column, - )), - point_to_lsp(PointUtf16::new( - range.end.row, - range.end.column, - )), - ), - }, - )), - None => None, - } - }, - }); - } - - InlayHintLabel::LabelParts(label_parts) - } - }, - padding_left: message_hint.padding_left, - padding_right: message_hint.padding_right, - kind: message_hint - .kind - .as_deref() - .and_then(InlayHintKind::from_name), - tooltip: message_hint.tooltip.and_then(|tooltip| { - Some(match tooltip.content? { - proto::inlay_hint_tooltip::Content::Value(s) => InlayHintTooltip::String(s), - proto::inlay_hint_tooltip::Content::MarkupContent(markup_content) => { - InlayHintTooltip::MarkupContent(MarkupContent { - kind: if markup_content.is_markdown { - HoverBlockKind::Markdown - } else { - HoverBlockKind::PlainText - }, - value: markup_content.value, - }) - } - }) - }), - resolve_state, - }) - } - - pub fn project_to_lsp_hint(hint: InlayHint, snapshot: &BufferSnapshot) -> lsp::InlayHint { - lsp::InlayHint { - position: point_to_lsp(hint.position.to_point_utf16(snapshot)), - kind: hint.kind.map(|kind| match kind { - InlayHintKind::Type => lsp::InlayHintKind::TYPE, - InlayHintKind::Parameter => lsp::InlayHintKind::PARAMETER, - }), - text_edits: None, - tooltip: hint.tooltip.and_then(|tooltip| { - Some(match tooltip { - InlayHintTooltip::String(s) => lsp::InlayHintTooltip::String(s), - InlayHintTooltip::MarkupContent(markup_content) => { - lsp::InlayHintTooltip::MarkupContent(lsp::MarkupContent { - kind: match markup_content.kind { - HoverBlockKind::PlainText => lsp::MarkupKind::PlainText, - HoverBlockKind::Markdown => lsp::MarkupKind::Markdown, - HoverBlockKind::Code { .. } => return None, - }, - value: markup_content.value, - }) - } - }) - }), - label: match hint.label { - InlayHintLabel::String(s) => lsp::InlayHintLabel::String(s), - InlayHintLabel::LabelParts(label_parts) => lsp::InlayHintLabel::LabelParts( - label_parts - .into_iter() - .map(|part| lsp::InlayHintLabelPart { - value: part.value, - tooltip: part.tooltip.and_then(|tooltip| { - Some(match tooltip { - InlayHintLabelPartTooltip::String(s) => { - lsp::InlayHintLabelPartTooltip::String(s) - } - InlayHintLabelPartTooltip::MarkupContent(markup_content) => { - lsp::InlayHintLabelPartTooltip::MarkupContent( - lsp::MarkupContent { - kind: match markup_content.kind { - HoverBlockKind::PlainText => { - lsp::MarkupKind::PlainText - } - HoverBlockKind::Markdown => { - lsp::MarkupKind::Markdown - } - HoverBlockKind::Code { .. } => return None, - }, - value: markup_content.value, - }, - ) - } - }) - }), - location: part.location.map(|(_, location)| location), - command: None, - }) - .collect(), - ), - }, - padding_left: Some(hint.padding_left), - padding_right: Some(hint.padding_right), - data: match hint.resolve_state { - ResolveState::CanResolve(_, data) => data, - ResolveState::Resolving | ResolveState::Resolved => None, - }, - } - } - - pub fn can_resolve_inlays(capabilities: &ServerCapabilities) -> bool { - capabilities - .inlay_hint_provider - .as_ref() - .and_then(|options| match options { - OneOf::Left(_is_supported) => None, - OneOf::Right(capabilities) => match capabilities { - lsp::InlayHintServerCapabilities::Options(o) => o.resolve_provider, - lsp::InlayHintServerCapabilities::RegistrationOptions(o) => { - o.inlay_hint_options.resolve_provider - } - }, - }) - .unwrap_or(false) - } - - pub fn check_capabilities(capabilities: &ServerCapabilities) -> bool { - capabilities - .inlay_hint_provider - .as_ref() - .is_some_and(|inlay_hint_provider| match inlay_hint_provider { - lsp::OneOf::Left(enabled) => *enabled, - lsp::OneOf::Right(_) => true, - }) - } -} - -#[async_trait(?Send)] -impl LspCommand for InlayHints { - type Response = Vec; - type LspRequest = lsp::InlayHintRequest; - type ProtoRequest = proto::InlayHints; - - fn display_name(&self) -> &str { - "Inlay hints" - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - Self::check_capabilities(&capabilities.server_capabilities) - } - - fn to_lsp( - &self, - path: &Path, - buffer: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(lsp::InlayHintParams { - text_document: lsp::TextDocumentIdentifier { - uri: file_path_to_lsp_url(path)?, - }, - range: range_to_lsp(self.range.to_point_utf16(buffer))?, - work_done_progress_params: Default::default(), - }) - } - - async fn response_from_lsp( - self, - message: Option>, - lsp_store: Entity, - buffer: Entity, - server_id: LanguageServerId, - mut cx: AsyncApp, - ) -> anyhow::Result> { - let (lsp_adapter, lsp_server) = - language_server_for_buffer(&lsp_store, &buffer, server_id, &mut cx)?; - // `typescript-language-server` adds padding to the left for type hints, turning - // `const foo: boolean` into `const foo : boolean` which looks odd. - // `rust-analyzer` does not have the padding for this case, and we have to accommodate both. - // - // We could trim the whole string, but being pessimistic on par with the situation above, - // there might be a hint with multiple whitespaces at the end(s) which we need to display properly. - // Hence let's use a heuristic first to handle the most awkward case and look for more. - let force_no_type_left_padding = - lsp_adapter.name.0.as_ref() == "typescript-language-server"; - - let hints = message.unwrap_or_default().into_iter().map(|lsp_hint| { - let resolve_state = if InlayHints::can_resolve_inlays(&lsp_server.capabilities()) { - ResolveState::CanResolve(lsp_server.server_id(), lsp_hint.data.clone()) - } else { - ResolveState::Resolved - }; - - let buffer = buffer.clone(); - cx.spawn(async move |cx| { - InlayHints::lsp_to_project_hint( - lsp_hint, - &buffer, - server_id, - resolve_state, - force_no_type_left_padding, - cx, - ) - .await - }) - }); - future::join_all(hints) - .await - .into_iter() - .collect::>() - .context("lsp to project inlay hints conversion") - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::InlayHints { - proto::InlayHints { - project_id, - buffer_id: buffer.remote_id().into(), - start: Some(language::proto::serialize_anchor(&self.range.start)), - end: Some(language::proto::serialize_anchor(&self.range.end)), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - message: proto::InlayHints, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - let start = message - .start - .and_then(language::proto::deserialize_anchor) - .context("invalid start")?; - let end = message - .end - .and_then(language::proto::deserialize_anchor) - .context("invalid end")?; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - - Ok(Self { range: start..end }) - } - - fn response_to_proto( - response: Vec, - _: &mut LspStore, - _: PeerId, - buffer_version: &clock::Global, - _: &mut App, - ) -> proto::InlayHintsResponse { - proto::InlayHintsResponse { - hints: response - .into_iter() - .map(InlayHints::project_to_proto_hint) - .collect(), - version: serialize_version(buffer_version), - } - } - - async fn response_from_proto( - self, - message: proto::InlayHintsResponse, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> anyhow::Result> { - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - - let mut hints = Vec::new(); - for message_hint in message.hints { - hints.push(InlayHints::proto_to_project_hint(message_hint)?); - } - - Ok(hints) - } - - fn buffer_id_from_proto(message: &proto::InlayHints) -> Result { - BufferId::new(message.buffer_id) - } -} - -#[async_trait(?Send)] -impl LspCommand for GetCodeLens { - type Response = Vec; - type LspRequest = lsp::CodeLensRequest; - type ProtoRequest = proto::GetCodeLens; - - fn display_name(&self) -> &str { - "Code Lens" - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - capabilities - .server_capabilities - .code_lens_provider - .is_some() - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(lsp::CodeLensParams { - text_document: lsp::TextDocumentIdentifier { - uri: file_path_to_lsp_url(path)?, - }, - work_done_progress_params: lsp::WorkDoneProgressParams::default(), - partial_result_params: lsp::PartialResultParams::default(), - }) - } - - async fn response_from_lsp( - self, - message: Option>, - lsp_store: Entity, - buffer: Entity, - server_id: LanguageServerId, - cx: AsyncApp, - ) -> anyhow::Result> { - let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot())?; - let language_server = cx.update(|cx| { - lsp_store - .read(cx) - .language_server_for_id(server_id) - .with_context(|| { - format!("Missing the language server that just returned a response {server_id}") - }) - })??; - let server_capabilities = language_server.capabilities(); - let available_commands = server_capabilities - .execute_command_provider - .as_ref() - .map(|options| options.commands.as_slice()) - .unwrap_or_default(); - Ok(message - .unwrap_or_default() - .into_iter() - .filter(|code_lens| { - code_lens - .command - .as_ref() - .is_none_or(|command| available_commands.contains(&command.command)) - }) - .map(|code_lens| { - let code_lens_range = range_from_lsp(code_lens.range); - let start = snapshot.clip_point_utf16(code_lens_range.start, Bias::Left); - let end = snapshot.clip_point_utf16(code_lens_range.end, Bias::Right); - let range = snapshot.anchor_before(start)..snapshot.anchor_after(end); - CodeAction { - server_id, - range, - lsp_action: LspAction::CodeLens(code_lens), - resolved: false, - } - }) - .collect()) - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetCodeLens { - proto::GetCodeLens { - project_id, - buffer_id: buffer.remote_id().into(), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - message: proto::GetCodeLens, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - Ok(Self) - } - - fn response_to_proto( - response: Vec, - _: &mut LspStore, - _: PeerId, - buffer_version: &clock::Global, - _: &mut App, - ) -> proto::GetCodeLensResponse { - proto::GetCodeLensResponse { - lens_actions: response - .iter() - .map(LspStore::serialize_code_action) - .collect(), - version: serialize_version(buffer_version), - } - } - - async fn response_from_proto( - self, - message: proto::GetCodeLensResponse, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> anyhow::Result> { - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - message - .lens_actions - .into_iter() - .map(LspStore::deserialize_code_action) - .collect::>>() - .context("deserializing proto code lens response") - } - - fn buffer_id_from_proto(message: &proto::GetCodeLens) -> Result { - BufferId::new(message.buffer_id) - } -} - -impl LinkedEditingRange { - pub fn check_server_capabilities(capabilities: ServerCapabilities) -> bool { - let Some(linked_editing_options) = capabilities.linked_editing_range_provider else { - return false; - }; - if let LinkedEditingRangeServerCapabilities::Simple(false) = linked_editing_options { - return false; - } - true - } -} - -#[async_trait(?Send)] -impl LspCommand for LinkedEditingRange { - type Response = Vec>; - type LspRequest = lsp::request::LinkedEditingRange; - type ProtoRequest = proto::LinkedEditingRange; - - fn display_name(&self) -> &str { - "Linked editing range" - } - - fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool { - Self::check_server_capabilities(capabilities.server_capabilities) - } - - fn to_lsp( - &self, - path: &Path, - buffer: &Buffer, - _server: &Arc, - _: &App, - ) -> Result { - let position = self.position.to_point_utf16(&buffer.snapshot()); - Ok(lsp::LinkedEditingRangeParams { - text_document_position_params: make_lsp_text_document_position(path, position)?, - work_done_progress_params: Default::default(), - }) - } - - async fn response_from_lsp( - self, - message: Option, - _: Entity, - buffer: Entity, - _server_id: LanguageServerId, - cx: AsyncApp, - ) -> Result>> { - if let Some(lsp::LinkedEditingRanges { mut ranges, .. }) = message { - ranges.sort_by_key(|range| range.start); - - buffer.read_with(&cx, |buffer, _| { - ranges - .into_iter() - .map(|range| { - let start = - buffer.clip_point_utf16(point_from_lsp(range.start), Bias::Left); - let end = buffer.clip_point_utf16(point_from_lsp(range.end), Bias::Left); - buffer.anchor_before(start)..buffer.anchor_after(end) - }) - .collect() - }) - } else { - Ok(vec![]) - } - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LinkedEditingRange { - proto::LinkedEditingRange { - project_id, - buffer_id: buffer.remote_id().to_proto(), - position: Some(serialize_anchor(&self.position)), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - message: proto::LinkedEditingRange, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result { - let position = message.position.context("invalid position")?; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - let position = deserialize_anchor(position).context("invalid position")?; - buffer - .update(&mut cx, |buffer, _| buffer.wait_for_anchors([position]))? - .await?; - Ok(Self { position }) - } - - fn response_to_proto( - response: Vec>, - _: &mut LspStore, - _: PeerId, - buffer_version: &clock::Global, - _: &mut App, - ) -> proto::LinkedEditingRangeResponse { - proto::LinkedEditingRangeResponse { - items: response - .into_iter() - .map(|range| proto::AnchorRange { - start: Some(serialize_anchor(&range.start)), - end: Some(serialize_anchor(&range.end)), - }) - .collect(), - version: serialize_version(buffer_version), - } - } - - async fn response_from_proto( - self, - message: proto::LinkedEditingRangeResponse, - _: Entity, - buffer: Entity, - mut cx: AsyncApp, - ) -> Result>> { - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(deserialize_version(&message.version)) - })? - .await?; - let items: Vec> = message - .items - .into_iter() - .filter_map(|range| { - let start = deserialize_anchor(range.start?)?; - let end = deserialize_anchor(range.end?)?; - Some(start..end) - }) - .collect(); - for range in &items { - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_anchors([range.start, range.end]) - })? - .await?; - } - Ok(items) - } - - fn buffer_id_from_proto(message: &proto::LinkedEditingRange) -> Result { - BufferId::new(message.buffer_id) - } -} - -impl GetDocumentDiagnostics { - pub fn diagnostics_from_proto( - response: proto::GetDocumentDiagnosticsResponse, - ) -> Vec { - response - .pulled_diagnostics - .into_iter() - .filter_map(|diagnostics| { - Some(LspPullDiagnostics::Response { - registration_id: diagnostics.registration_id.map(SharedString::from), - server_id: LanguageServerId::from_proto(diagnostics.server_id), - uri: lsp::Uri::from_str(diagnostics.uri.as_str()).log_err()?, - diagnostics: if diagnostics.changed { - PulledDiagnostics::Unchanged { - result_id: SharedString::new(diagnostics.result_id?), - } - } else { - PulledDiagnostics::Changed { - result_id: diagnostics.result_id.map(SharedString::new), - diagnostics: diagnostics - .diagnostics - .into_iter() - .filter_map(|diagnostic| { - GetDocumentDiagnostics::deserialize_lsp_diagnostic(diagnostic) - .context("deserializing diagnostics") - .log_err() - }) - .collect(), - } - }, - }) - }) - .collect() - } - - fn deserialize_lsp_diagnostic(diagnostic: proto::LspDiagnostic) -> Result { - let start = diagnostic.start.context("invalid start range")?; - let end = diagnostic.end.context("invalid end range")?; - - let range = Range:: { - start: PointUtf16 { - row: start.row, - column: start.column, - }, - end: PointUtf16 { - row: end.row, - column: end.column, - }, - }; - - let data = diagnostic.data.and_then(|data| Value::from_str(&data).ok()); - let code = diagnostic.code.map(lsp::NumberOrString::String); - - let related_information = diagnostic - .related_information - .into_iter() - .map(|info| { - let start = info.location_range_start.unwrap(); - let end = info.location_range_end.unwrap(); - - lsp::DiagnosticRelatedInformation { - location: lsp::Location { - range: lsp::Range { - start: point_to_lsp(PointUtf16::new(start.row, start.column)), - end: point_to_lsp(PointUtf16::new(end.row, end.column)), - }, - uri: lsp::Uri::from_str(&info.location_url.unwrap()).unwrap(), - }, - message: info.message, - } - }) - .collect::>(); - - let tags = diagnostic - .tags - .into_iter() - .filter_map(|tag| match proto::LspDiagnosticTag::from_i32(tag) { - Some(proto::LspDiagnosticTag::Unnecessary) => Some(lsp::DiagnosticTag::UNNECESSARY), - Some(proto::LspDiagnosticTag::Deprecated) => Some(lsp::DiagnosticTag::DEPRECATED), - _ => None, - }) - .collect::>(); - - Ok(lsp::Diagnostic { - range: language::range_to_lsp(range)?, - severity: match proto::lsp_diagnostic::Severity::from_i32(diagnostic.severity).unwrap() - { - proto::lsp_diagnostic::Severity::Error => Some(lsp::DiagnosticSeverity::ERROR), - proto::lsp_diagnostic::Severity::Warning => Some(lsp::DiagnosticSeverity::WARNING), - proto::lsp_diagnostic::Severity::Information => { - Some(lsp::DiagnosticSeverity::INFORMATION) - } - proto::lsp_diagnostic::Severity::Hint => Some(lsp::DiagnosticSeverity::HINT), - _ => None, - }, - code, - code_description: diagnostic - .code_description - .map(|code_description| CodeDescription { - href: Some(lsp::Uri::from_str(&code_description).unwrap()), - }), - related_information: Some(related_information), - tags: Some(tags), - source: diagnostic.source.clone(), - message: diagnostic.message, - data, - }) - } - - fn serialize_lsp_diagnostic(diagnostic: lsp::Diagnostic) -> Result { - let range = language::range_from_lsp(diagnostic.range); - let related_information = diagnostic - .related_information - .unwrap_or_default() - .into_iter() - .map(|related_information| { - let location_range_start = - point_from_lsp(related_information.location.range.start).0; - let location_range_end = point_from_lsp(related_information.location.range.end).0; - - Ok(proto::LspDiagnosticRelatedInformation { - location_url: Some(related_information.location.uri.to_string()), - location_range_start: Some(proto::PointUtf16 { - row: location_range_start.row, - column: location_range_start.column, - }), - location_range_end: Some(proto::PointUtf16 { - row: location_range_end.row, - column: location_range_end.column, - }), - message: related_information.message, - }) - }) - .collect::>>()?; - - let tags = diagnostic - .tags - .unwrap_or_default() - .into_iter() - .map(|tag| match tag { - lsp::DiagnosticTag::UNNECESSARY => proto::LspDiagnosticTag::Unnecessary, - lsp::DiagnosticTag::DEPRECATED => proto::LspDiagnosticTag::Deprecated, - _ => proto::LspDiagnosticTag::None, - } as i32) - .collect(); - - Ok(proto::LspDiagnostic { - start: Some(proto::PointUtf16 { - row: range.start.0.row, - column: range.start.0.column, - }), - end: Some(proto::PointUtf16 { - row: range.end.0.row, - column: range.end.0.column, - }), - severity: match diagnostic.severity { - Some(lsp::DiagnosticSeverity::ERROR) => proto::lsp_diagnostic::Severity::Error, - Some(lsp::DiagnosticSeverity::WARNING) => proto::lsp_diagnostic::Severity::Warning, - Some(lsp::DiagnosticSeverity::INFORMATION) => { - proto::lsp_diagnostic::Severity::Information - } - Some(lsp::DiagnosticSeverity::HINT) => proto::lsp_diagnostic::Severity::Hint, - _ => proto::lsp_diagnostic::Severity::None, - } as i32, - code: diagnostic.code.as_ref().map(|code| match code { - lsp::NumberOrString::Number(code) => code.to_string(), - lsp::NumberOrString::String(code) => code.clone(), - }), - source: diagnostic.source.clone(), - related_information, - tags, - code_description: diagnostic - .code_description - .and_then(|desc| desc.href.map(|url| url.to_string())), - message: diagnostic.message, - data: diagnostic.data.as_ref().map(|data| data.to_string()), - }) - } - - pub fn deserialize_workspace_diagnostics_report( - report: lsp::WorkspaceDiagnosticReportResult, - server_id: LanguageServerId, - registration_id: Option, - ) -> Vec { - let mut pulled_diagnostics = HashMap::default(); - match report { - lsp::WorkspaceDiagnosticReportResult::Report(workspace_diagnostic_report) => { - for report in workspace_diagnostic_report.items { - match report { - lsp::WorkspaceDocumentDiagnosticReport::Full(report) => { - process_full_workspace_diagnostics_report( - &mut pulled_diagnostics, - server_id, - report, - registration_id.clone(), - ) - } - lsp::WorkspaceDocumentDiagnosticReport::Unchanged(report) => { - process_unchanged_workspace_diagnostics_report( - &mut pulled_diagnostics, - server_id, - report, - registration_id.clone(), - ) - } - } - } - } - lsp::WorkspaceDiagnosticReportResult::Partial( - workspace_diagnostic_report_partial_result, - ) => { - for report in workspace_diagnostic_report_partial_result.items { - match report { - lsp::WorkspaceDocumentDiagnosticReport::Full(report) => { - process_full_workspace_diagnostics_report( - &mut pulled_diagnostics, - server_id, - report, - registration_id.clone(), - ) - } - lsp::WorkspaceDocumentDiagnosticReport::Unchanged(report) => { - process_unchanged_workspace_diagnostics_report( - &mut pulled_diagnostics, - server_id, - report, - registration_id.clone(), - ) - } - } - } - } - } - pulled_diagnostics.into_values().collect() - } -} - -#[derive(Debug)] -pub struct WorkspaceLspPullDiagnostics { - pub version: Option, - pub diagnostics: LspPullDiagnostics, -} - -fn process_full_workspace_diagnostics_report( - diagnostics: &mut HashMap, - server_id: LanguageServerId, - report: lsp::WorkspaceFullDocumentDiagnosticReport, - registration_id: Option, -) { - let mut new_diagnostics = HashMap::default(); - process_full_diagnostics_report( - &mut new_diagnostics, - server_id, - report.uri, - report.full_document_diagnostic_report, - registration_id, - ); - diagnostics.extend(new_diagnostics.into_iter().map(|(uri, diagnostics)| { - ( - uri, - WorkspaceLspPullDiagnostics { - version: report.version.map(|v| v as i32), - diagnostics, - }, - ) - })); -} - -fn process_unchanged_workspace_diagnostics_report( - diagnostics: &mut HashMap, - server_id: LanguageServerId, - report: lsp::WorkspaceUnchangedDocumentDiagnosticReport, - registration_id: Option, -) { - let mut new_diagnostics = HashMap::default(); - process_unchanged_diagnostics_report( - &mut new_diagnostics, - server_id, - report.uri, - report.unchanged_document_diagnostic_report, - registration_id, - ); - diagnostics.extend(new_diagnostics.into_iter().map(|(uri, diagnostics)| { - ( - uri, - WorkspaceLspPullDiagnostics { - version: report.version.map(|v| v as i32), - diagnostics, - }, - ) - })); -} - -#[async_trait(?Send)] -impl LspCommand for GetDocumentDiagnostics { - type Response = Vec; - type LspRequest = lsp::request::DocumentDiagnosticRequest; - type ProtoRequest = proto::GetDocumentDiagnostics; - - fn display_name(&self) -> &str { - "Get diagnostics" - } - - fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool { - true - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(lsp::DocumentDiagnosticParams { - text_document: lsp::TextDocumentIdentifier { - uri: file_path_to_lsp_url(path)?, - }, - identifier: self.identifier.clone(), - previous_result_id: self.previous_result_id.clone().map(|id| id.to_string()), - partial_result_params: Default::default(), - work_done_progress_params: Default::default(), - }) - } - - async fn response_from_lsp( - self, - message: lsp::DocumentDiagnosticReportResult, - _: Entity, - buffer: Entity, - server_id: LanguageServerId, - cx: AsyncApp, - ) -> Result { - let url = buffer.read_with(&cx, |buffer, cx| { - buffer - .file() - .and_then(|file| file.as_local()) - .map(|file| { - let abs_path = file.abs_path(cx); - file_path_to_lsp_url(&abs_path) - }) - .transpose()? - .with_context(|| format!("missing url on buffer {}", buffer.remote_id())) - })??; - - let mut pulled_diagnostics = HashMap::default(); - match message { - lsp::DocumentDiagnosticReportResult::Report(report) => match report { - lsp::DocumentDiagnosticReport::Full(report) => { - if let Some(related_documents) = report.related_documents { - process_related_documents( - &mut pulled_diagnostics, - server_id, - related_documents, - self.registration_id.clone(), - ); - } - process_full_diagnostics_report( - &mut pulled_diagnostics, - server_id, - url, - report.full_document_diagnostic_report, - self.registration_id, - ); - } - lsp::DocumentDiagnosticReport::Unchanged(report) => { - if let Some(related_documents) = report.related_documents { - process_related_documents( - &mut pulled_diagnostics, - server_id, - related_documents, - self.registration_id.clone(), - ); - } - process_unchanged_diagnostics_report( - &mut pulled_diagnostics, - server_id, - url, - report.unchanged_document_diagnostic_report, - self.registration_id, - ); - } - }, - lsp::DocumentDiagnosticReportResult::Partial(report) => { - if let Some(related_documents) = report.related_documents { - process_related_documents( - &mut pulled_diagnostics, - server_id, - related_documents, - self.registration_id, - ); - } - } - } - - Ok(pulled_diagnostics.into_values().collect()) - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDocumentDiagnostics { - proto::GetDocumentDiagnostics { - project_id, - buffer_id: buffer.remote_id().into(), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - _: proto::GetDocumentDiagnostics, - _: Entity, - _: Entity, - _: AsyncApp, - ) -> Result { - anyhow::bail!( - "proto::GetDocumentDiagnostics is not expected to be converted from proto directly, as it needs `previous_result_id` fetched first" - ) - } - - fn response_to_proto( - response: Self::Response, - _: &mut LspStore, - _: PeerId, - _: &clock::Global, - _: &mut App, - ) -> proto::GetDocumentDiagnosticsResponse { - let pulled_diagnostics = response - .into_iter() - .filter_map(|diagnostics| match diagnostics { - LspPullDiagnostics::Default => None, - LspPullDiagnostics::Response { - server_id, - uri, - diagnostics, - registration_id, - } => { - let mut changed = false; - let (diagnostics, result_id) = match diagnostics { - PulledDiagnostics::Unchanged { result_id } => (Vec::new(), Some(result_id)), - PulledDiagnostics::Changed { - result_id, - diagnostics, - } => { - changed = true; - (diagnostics, result_id) - } - }; - Some(proto::PulledDiagnostics { - changed, - result_id: result_id.map(|id| id.to_string()), - uri: uri.to_string(), - server_id: server_id.to_proto(), - diagnostics: diagnostics - .into_iter() - .filter_map(|diagnostic| { - GetDocumentDiagnostics::serialize_lsp_diagnostic(diagnostic) - .context("serializing diagnostics") - .log_err() - }) - .collect(), - registration_id: registration_id.as_ref().map(ToString::to_string), - }) - } - }) - .collect(); - - proto::GetDocumentDiagnosticsResponse { pulled_diagnostics } - } - - async fn response_from_proto( - self, - response: proto::GetDocumentDiagnosticsResponse, - _: Entity, - _: Entity, - _: AsyncApp, - ) -> Result { - Ok(Self::diagnostics_from_proto(response)) - } - - fn buffer_id_from_proto(message: &proto::GetDocumentDiagnostics) -> Result { - BufferId::new(message.buffer_id) - } -} - -#[async_trait(?Send)] -impl LspCommand for GetDocumentColor { - type Response = Vec; - type LspRequest = lsp::request::DocumentColor; - type ProtoRequest = proto::GetDocumentColor; - - fn display_name(&self) -> &str { - "Document color" - } - - fn check_capabilities(&self, server_capabilities: AdapterServerCapabilities) -> bool { - server_capabilities - .server_capabilities - .color_provider - .as_ref() - .is_some_and(|capability| match capability { - lsp::ColorProviderCapability::Simple(supported) => *supported, - lsp::ColorProviderCapability::ColorProvider(..) => true, - lsp::ColorProviderCapability::Options(..) => true, - }) - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(lsp::DocumentColorParams { - text_document: make_text_document_identifier(path)?, - work_done_progress_params: Default::default(), - partial_result_params: Default::default(), - }) - } - - async fn response_from_lsp( - self, - message: Vec, - _: Entity, - _: Entity, - _: LanguageServerId, - _: AsyncApp, - ) -> Result { - Ok(message - .into_iter() - .map(|color| DocumentColor { - lsp_range: color.range, - color: color.color, - resolved: false, - color_presentations: Vec::new(), - }) - .collect()) - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest { - proto::GetDocumentColor { - project_id, - buffer_id: buffer.remote_id().to_proto(), - version: serialize_version(&buffer.version()), - } - } - - async fn from_proto( - _: Self::ProtoRequest, - _: Entity, - _: Entity, - _: AsyncApp, - ) -> Result { - Ok(Self {}) - } - - fn response_to_proto( - response: Self::Response, - _: &mut LspStore, - _: PeerId, - buffer_version: &clock::Global, - _: &mut App, - ) -> proto::GetDocumentColorResponse { - proto::GetDocumentColorResponse { - colors: response - .into_iter() - .map(|color| { - let start = point_from_lsp(color.lsp_range.start).0; - let end = point_from_lsp(color.lsp_range.end).0; - proto::ColorInformation { - red: color.color.red, - green: color.color.green, - blue: color.color.blue, - alpha: color.color.alpha, - lsp_range_start: Some(proto::PointUtf16 { - row: start.row, - column: start.column, - }), - lsp_range_end: Some(proto::PointUtf16 { - row: end.row, - column: end.column, - }), - } - }) - .collect(), - version: serialize_version(buffer_version), - } - } - - async fn response_from_proto( - self, - message: proto::GetDocumentColorResponse, - _: Entity, - _: Entity, - _: AsyncApp, - ) -> Result { - Ok(message - .colors - .into_iter() - .filter_map(|color| { - let start = color.lsp_range_start?; - let start = PointUtf16::new(start.row, start.column); - let end = color.lsp_range_end?; - let end = PointUtf16::new(end.row, end.column); - Some(DocumentColor { - resolved: false, - color_presentations: Vec::new(), - lsp_range: lsp::Range { - start: point_to_lsp(start), - end: point_to_lsp(end), - }, - color: lsp::Color { - red: color.red, - green: color.green, - blue: color.blue, - alpha: color.alpha, - }, - }) - }) - .collect()) - } - - fn buffer_id_from_proto(message: &Self::ProtoRequest) -> Result { - BufferId::new(message.buffer_id) - } -} - -fn process_related_documents( - diagnostics: &mut HashMap, - server_id: LanguageServerId, - documents: impl IntoIterator, - registration_id: Option, -) { - for (url, report_kind) in documents { - match report_kind { - lsp::DocumentDiagnosticReportKind::Full(report) => process_full_diagnostics_report( - diagnostics, - server_id, - url, - report, - registration_id.clone(), - ), - lsp::DocumentDiagnosticReportKind::Unchanged(report) => { - process_unchanged_diagnostics_report( - diagnostics, - server_id, - url, - report, - registration_id.clone(), - ) - } - } - } -} - -fn process_unchanged_diagnostics_report( - diagnostics: &mut HashMap, - server_id: LanguageServerId, - uri: lsp::Uri, - report: lsp::UnchangedDocumentDiagnosticReport, - registration_id: Option, -) { - let result_id = SharedString::new(report.result_id); - match diagnostics.entry(uri.clone()) { - hash_map::Entry::Occupied(mut o) => match o.get_mut() { - LspPullDiagnostics::Default => { - o.insert(LspPullDiagnostics::Response { - server_id, - uri, - diagnostics: PulledDiagnostics::Unchanged { result_id }, - registration_id, - }); - } - LspPullDiagnostics::Response { - server_id: existing_server_id, - uri: existing_uri, - diagnostics: existing_diagnostics, - .. - } => { - if server_id != *existing_server_id || &uri != existing_uri { - debug_panic!( - "Unexpected state: file {uri} has two different sets of diagnostics reported" - ); - } - match existing_diagnostics { - PulledDiagnostics::Unchanged { .. } => { - *existing_diagnostics = PulledDiagnostics::Unchanged { result_id }; - } - PulledDiagnostics::Changed { .. } => {} - } - } - }, - hash_map::Entry::Vacant(v) => { - v.insert(LspPullDiagnostics::Response { - server_id, - uri, - diagnostics: PulledDiagnostics::Unchanged { result_id }, - registration_id, - }); - } - } -} - -fn process_full_diagnostics_report( - diagnostics: &mut HashMap, - server_id: LanguageServerId, - uri: lsp::Uri, - report: lsp::FullDocumentDiagnosticReport, - registration_id: Option, -) { - let result_id = report.result_id.map(SharedString::new); - match diagnostics.entry(uri.clone()) { - hash_map::Entry::Occupied(mut o) => match o.get_mut() { - LspPullDiagnostics::Default => { - o.insert(LspPullDiagnostics::Response { - server_id, - uri, - diagnostics: PulledDiagnostics::Changed { - result_id, - diagnostics: report.items, - }, - registration_id, - }); - } - LspPullDiagnostics::Response { - server_id: existing_server_id, - uri: existing_uri, - diagnostics: existing_diagnostics, - .. - } => { - if server_id != *existing_server_id || &uri != existing_uri { - debug_panic!( - "Unexpected state: file {uri} has two different sets of diagnostics reported" - ); - } - match existing_diagnostics { - PulledDiagnostics::Unchanged { .. } => { - *existing_diagnostics = PulledDiagnostics::Changed { - result_id, - diagnostics: report.items, - }; - } - PulledDiagnostics::Changed { - result_id: existing_result_id, - diagnostics: existing_diagnostics, - } => { - if result_id.is_some() { - *existing_result_id = result_id; - } - existing_diagnostics.extend(report.items); - } - } - } - }, - hash_map::Entry::Vacant(v) => { - v.insert(LspPullDiagnostics::Response { - server_id, - uri, - diagnostics: PulledDiagnostics::Changed { - result_id, - diagnostics: report.items, - }, - registration_id, - }); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use lsp::{DiagnosticSeverity, DiagnosticTag}; - use serde_json::json; - - #[test] - fn test_serialize_lsp_diagnostic() { - let lsp_diagnostic = lsp::Diagnostic { - range: lsp::Range { - start: lsp::Position::new(0, 1), - end: lsp::Position::new(2, 3), - }, - severity: Some(DiagnosticSeverity::ERROR), - code: Some(lsp::NumberOrString::String("E001".to_string())), - source: Some("test-source".to_string()), - message: "Test error message".to_string(), - related_information: None, - tags: Some(vec![DiagnosticTag::DEPRECATED]), - code_description: None, - data: Some(json!({"detail": "test detail"})), - }; - - let proto_diagnostic = GetDocumentDiagnostics::serialize_lsp_diagnostic(lsp_diagnostic) - .expect("Failed to serialize diagnostic"); - - let start = proto_diagnostic.start.unwrap(); - let end = proto_diagnostic.end.unwrap(); - assert_eq!(start.row, 0); - assert_eq!(start.column, 1); - assert_eq!(end.row, 2); - assert_eq!(end.column, 3); - assert_eq!( - proto_diagnostic.severity, - proto::lsp_diagnostic::Severity::Error as i32 - ); - assert_eq!(proto_diagnostic.code, Some("E001".to_string())); - assert_eq!(proto_diagnostic.source, Some("test-source".to_string())); - assert_eq!(proto_diagnostic.message, "Test error message"); - } - - #[test] - fn test_deserialize_lsp_diagnostic() { - let proto_diagnostic = proto::LspDiagnostic { - start: Some(proto::PointUtf16 { row: 0, column: 1 }), - end: Some(proto::PointUtf16 { row: 2, column: 3 }), - severity: proto::lsp_diagnostic::Severity::Warning as i32, - code: Some("ERR".to_string()), - source: Some("Prism".to_string()), - message: "assigned but unused variable - a".to_string(), - related_information: vec![], - tags: vec![], - code_description: None, - data: None, - }; - - let lsp_diagnostic = GetDocumentDiagnostics::deserialize_lsp_diagnostic(proto_diagnostic) - .expect("Failed to deserialize diagnostic"); - - assert_eq!(lsp_diagnostic.range.start.line, 0); - assert_eq!(lsp_diagnostic.range.start.character, 1); - assert_eq!(lsp_diagnostic.range.end.line, 2); - assert_eq!(lsp_diagnostic.range.end.character, 3); - assert_eq!(lsp_diagnostic.severity, Some(DiagnosticSeverity::WARNING)); - assert_eq!( - lsp_diagnostic.code, - Some(lsp::NumberOrString::String("ERR".to_string())) - ); - assert_eq!(lsp_diagnostic.source, Some("Prism".to_string())); - assert_eq!(lsp_diagnostic.message, "assigned but unused variable - a"); - } - - #[test] - fn test_related_information() { - let related_info = lsp::DiagnosticRelatedInformation { - location: lsp::Location { - uri: lsp::Uri::from_str("file:///test.rs").unwrap(), - range: lsp::Range { - start: lsp::Position::new(1, 1), - end: lsp::Position::new(1, 5), - }, - }, - message: "Related info message".to_string(), - }; - - let lsp_diagnostic = lsp::Diagnostic { - range: lsp::Range { - start: lsp::Position::new(0, 0), - end: lsp::Position::new(0, 1), - }, - severity: Some(DiagnosticSeverity::INFORMATION), - code: None, - source: Some("Prism".to_string()), - message: "assigned but unused variable - a".to_string(), - related_information: Some(vec![related_info]), - tags: None, - code_description: None, - data: None, - }; - - let proto_diagnostic = GetDocumentDiagnostics::serialize_lsp_diagnostic(lsp_diagnostic) - .expect("Failed to serialize diagnostic"); - - assert_eq!(proto_diagnostic.related_information.len(), 1); - let related = &proto_diagnostic.related_information[0]; - assert_eq!(related.location_url, Some("file:///test.rs".to_string())); - assert_eq!(related.message, "Related info message"); - } - - #[test] - fn test_invalid_ranges() { - let proto_diagnostic = proto::LspDiagnostic { - start: None, - end: Some(proto::PointUtf16 { row: 2, column: 3 }), - severity: proto::lsp_diagnostic::Severity::Error as i32, - code: None, - source: None, - message: "Test message".to_string(), - related_information: vec![], - tags: vec![], - code_description: None, - data: None, - }; - - let result = GetDocumentDiagnostics::deserialize_lsp_diagnostic(proto_diagnostic); - assert!(result.is_err()); - } -} diff --git a/crates/project/src/lsp_command/signature_help.rs b/crates/project/src/lsp_command/signature_help.rs deleted file mode 100644 index 6a49931183..0000000000 --- a/crates/project/src/lsp_command/signature_help.rs +++ /dev/null @@ -1,793 +0,0 @@ -use std::{ops::Range, sync::Arc}; - -use gpui::{App, AppContext, Entity, FontWeight, HighlightStyle, SharedString}; -use language::LanguageRegistry; -use lsp::LanguageServerId; -use markdown::Markdown; -use rpc::proto::{self, documentation}; -use util::maybe; - -#[derive(Debug)] -pub struct SignatureHelp { - pub active_signature: usize, - pub signatures: Vec, - pub(super) original_data: lsp::SignatureHelp, -} - -#[derive(Debug, Clone)] -pub struct SignatureHelpData { - pub label: SharedString, - pub documentation: Option>, - pub highlights: Vec<(Range, HighlightStyle)>, - pub active_parameter: Option, - pub parameters: Vec, -} - -#[derive(Debug, Clone)] -pub struct ParameterInfo { - pub label_range: Option>, - pub documentation: Option>, -} - -impl SignatureHelp { - pub fn new( - help: lsp::SignatureHelp, - language_registry: Option>, - lang_server_id: Option, - cx: &mut App, - ) -> Option { - if help.signatures.is_empty() { - return None; - } - let active_signature = help.active_signature.unwrap_or(0) as usize; - let mut signatures = Vec::::with_capacity(help.signatures.capacity()); - for signature in &help.signatures { - let label = SharedString::from(signature.label.clone()); - let active_parameter = signature - .active_parameter - .unwrap_or_else(|| help.active_parameter.unwrap_or(0)) - as usize; - let mut highlights = Vec::new(); - let mut parameter_infos = Vec::new(); - - if let Some(parameters) = &signature.parameters { - for (index, parameter) in parameters.iter().enumerate() { - let label_range = match ¶meter.label { - &lsp::ParameterLabel::LabelOffsets([offset1, offset2]) => { - maybe!({ - let offset1 = offset1 as usize; - let offset2 = offset2 as usize; - if offset1 < offset2 { - let mut indices = label.char_indices().scan( - 0, - |utf16_offset_acc, (offset, c)| { - let utf16_offset = *utf16_offset_acc; - *utf16_offset_acc += c.len_utf16(); - Some((utf16_offset, offset)) - }, - ); - let (_, offset1) = indices - .find(|(utf16_offset, _)| *utf16_offset == offset1)?; - let (_, offset2) = indices - .find(|(utf16_offset, _)| *utf16_offset == offset2)?; - Some(offset1..offset2) - } else { - log::warn!( - "language server {lang_server_id:?} produced invalid parameter label range: {offset1:?}..{offset2:?}", - ); - None - } - }) - } - lsp::ParameterLabel::Simple(parameter_label) => { - if let Some(start) = signature.label.find(parameter_label) { - Some(start..start + parameter_label.len()) - } else { - None - } - } - }; - - if let Some(label_range) = &label_range - && index == active_parameter - { - highlights.push(( - label_range.clone(), - HighlightStyle { - font_weight: Some(FontWeight::EXTRA_BOLD), - ..HighlightStyle::default() - }, - )); - } - - let documentation = parameter - .documentation - .as_ref() - .map(|doc| documentation_to_markdown(doc, language_registry.clone(), cx)); - - parameter_infos.push(ParameterInfo { - label_range, - documentation, - }); - } - } - - let documentation = signature - .documentation - .as_ref() - .map(|doc| documentation_to_markdown(doc, language_registry.clone(), cx)); - - signatures.push(SignatureHelpData { - label, - documentation, - highlights, - active_parameter: Some(active_parameter), - parameters: parameter_infos, - }); - } - Some(Self { - signatures, - active_signature, - original_data: help, - }) - } -} - -fn documentation_to_markdown( - documentation: &lsp::Documentation, - language_registry: Option>, - cx: &mut App, -) -> Entity { - match documentation { - lsp::Documentation::String(string) => { - cx.new(|cx| Markdown::new_text(SharedString::from(string), cx)) - } - lsp::Documentation::MarkupContent(markup) => match markup.kind { - lsp::MarkupKind::PlainText => { - cx.new(|cx| Markdown::new_text(SharedString::from(&markup.value), cx)) - } - lsp::MarkupKind::Markdown => cx.new(|cx| { - Markdown::new( - SharedString::from(&markup.value), - language_registry, - None, - cx, - ) - }), - }, - } -} - -pub fn lsp_to_proto_signature(lsp_help: lsp::SignatureHelp) -> proto::SignatureHelp { - proto::SignatureHelp { - signatures: lsp_help - .signatures - .into_iter() - .map(|signature| proto::SignatureInformation { - label: signature.label, - documentation: signature.documentation.map(lsp_to_proto_documentation), - parameters: signature - .parameters - .unwrap_or_default() - .into_iter() - .map(|parameter_info| proto::ParameterInformation { - label: Some(match parameter_info.label { - lsp::ParameterLabel::Simple(label) => { - proto::parameter_information::Label::Simple(label) - } - lsp::ParameterLabel::LabelOffsets(offsets) => { - proto::parameter_information::Label::LabelOffsets( - proto::LabelOffsets { - start: offsets[0], - end: offsets[1], - }, - ) - } - }), - documentation: parameter_info.documentation.map(lsp_to_proto_documentation), - }) - .collect(), - active_parameter: signature.active_parameter, - }) - .collect(), - active_signature: lsp_help.active_signature, - active_parameter: lsp_help.active_parameter, - } -} - -fn lsp_to_proto_documentation(documentation: lsp::Documentation) -> proto::Documentation { - proto::Documentation { - content: Some(match documentation { - lsp::Documentation::String(string) => proto::documentation::Content::Value(string), - lsp::Documentation::MarkupContent(content) => { - proto::documentation::Content::MarkupContent(proto::MarkupContent { - is_markdown: matches!(content.kind, lsp::MarkupKind::Markdown), - value: content.value, - }) - } - }), - } -} - -pub fn proto_to_lsp_signature(proto_help: proto::SignatureHelp) -> lsp::SignatureHelp { - lsp::SignatureHelp { - signatures: proto_help - .signatures - .into_iter() - .map(|signature| lsp::SignatureInformation { - label: signature.label, - documentation: signature.documentation.and_then(proto_to_lsp_documentation), - parameters: Some( - signature - .parameters - .into_iter() - .filter_map(|parameter_info| { - Some(lsp::ParameterInformation { - label: match parameter_info.label? { - proto::parameter_information::Label::Simple(string) => { - lsp::ParameterLabel::Simple(string) - } - proto::parameter_information::Label::LabelOffsets(offsets) => { - lsp::ParameterLabel::LabelOffsets([ - offsets.start, - offsets.end, - ]) - } - }, - documentation: parameter_info - .documentation - .and_then(proto_to_lsp_documentation), - }) - }) - .collect(), - ), - active_parameter: signature.active_parameter, - }) - .collect(), - active_signature: proto_help.active_signature, - active_parameter: proto_help.active_parameter, - } -} - -fn proto_to_lsp_documentation(documentation: proto::Documentation) -> Option { - { - Some(match documentation.content? { - documentation::Content::Value(string) => lsp::Documentation::String(string), - documentation::Content::MarkupContent(markup) => { - lsp::Documentation::MarkupContent(if markup.is_markdown { - lsp::MarkupContent { - kind: lsp::MarkupKind::Markdown, - value: markup.value, - } - } else { - lsp::MarkupContent { - kind: lsp::MarkupKind::PlainText, - value: markup.value, - } - }) - } - }) - } -} - -#[cfg(test)] -mod tests { - use gpui::{FontWeight, HighlightStyle, SharedString, TestAppContext}; - use lsp::{Documentation, MarkupContent, MarkupKind}; - - use crate::lsp_command::signature_help::SignatureHelp; - - fn current_parameter() -> HighlightStyle { - HighlightStyle { - font_weight: Some(FontWeight::EXTRA_BOLD), - ..Default::default() - } - } - - #[gpui::test] - fn test_create_signature_help_markdown_string_1(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![lsp::SignatureInformation { - label: "fn test(foo: u8, bar: &str)".to_string(), - documentation: Some(Documentation::String( - "This is a test documentation".to_string(), - )), - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("foo: u8".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("bar: &str".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }], - active_signature: Some(0), - active_parameter: Some(0), - }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(maybe_markdown.is_some()); - - let markdown = maybe_markdown.unwrap(); - let signature = markdown.signatures[markdown.active_signature].clone(); - let markdown = (signature.label, signature.highlights); - assert_eq!( - markdown, - ( - SharedString::new("fn test(foo: u8, bar: &str)"), - vec![(8..15, current_parameter())] - ) - ); - assert_eq!( - signature - .documentation - .unwrap() - .update(cx, |documentation, _| documentation.source().to_owned()), - "This is a test documentation", - ) - } - - #[gpui::test] - fn test_create_signature_help_markdown_string_2(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![lsp::SignatureInformation { - label: "fn test(foo: u8, bar: &str)".to_string(), - documentation: Some(Documentation::MarkupContent(MarkupContent { - kind: MarkupKind::Markdown, - value: "This is a test documentation".to_string(), - })), - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("foo: u8".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("bar: &str".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }], - active_signature: Some(0), - active_parameter: Some(1), - }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(maybe_markdown.is_some()); - - let markdown = maybe_markdown.unwrap(); - let signature = markdown.signatures[markdown.active_signature].clone(); - let markdown = (signature.label, signature.highlights); - assert_eq!( - markdown, - ( - SharedString::new("fn test(foo: u8, bar: &str)"), - vec![(17..26, current_parameter())] - ) - ); - assert_eq!( - signature - .documentation - .unwrap() - .update(cx, |documentation, _| documentation.source().to_owned()), - "This is a test documentation", - ) - } - - #[gpui::test] - fn test_create_signature_help_markdown_string_3(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![ - lsp::SignatureInformation { - label: "fn test1(foo: u8, bar: &str)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("foo: u8".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("bar: &str".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - lsp::SignatureInformation { - label: "fn test2(hoge: String, fuga: bool)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("hoge: String".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("fuga: bool".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - ], - active_signature: Some(0), - active_parameter: Some(0), - }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(maybe_markdown.is_some()); - - let markdown = maybe_markdown.unwrap(); - let signature = markdown.signatures[markdown.active_signature].clone(); - let markdown = (signature.label, signature.highlights); - assert_eq!( - markdown, - ( - SharedString::new("fn test1(foo: u8, bar: &str)"), - vec![(9..16, current_parameter())] - ) - ); - } - - #[gpui::test] - fn test_create_signature_help_markdown_string_4(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![ - lsp::SignatureInformation { - label: "fn test1(foo: u8, bar: &str)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("foo: u8".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("bar: &str".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - lsp::SignatureInformation { - label: "fn test2(hoge: String, fuga: bool)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("hoge: String".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("fuga: bool".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - ], - active_signature: Some(1), - active_parameter: Some(0), - }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(maybe_markdown.is_some()); - - let markdown = maybe_markdown.unwrap(); - let signature = markdown.signatures[markdown.active_signature].clone(); - let markdown = (signature.label, signature.highlights); - assert_eq!( - markdown, - ( - SharedString::new("fn test2(hoge: String, fuga: bool)"), - vec![(9..21, current_parameter())] - ) - ); - } - - #[gpui::test] - fn test_create_signature_help_markdown_string_5(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![ - lsp::SignatureInformation { - label: "fn test1(foo: u8, bar: &str)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("foo: u8".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("bar: &str".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - lsp::SignatureInformation { - label: "fn test2(hoge: String, fuga: bool)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("hoge: String".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("fuga: bool".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - ], - active_signature: Some(1), - active_parameter: Some(1), - }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(maybe_markdown.is_some()); - - let markdown = maybe_markdown.unwrap(); - let signature = markdown.signatures[markdown.active_signature].clone(); - let markdown = (signature.label, signature.highlights); - assert_eq!( - markdown, - ( - SharedString::new("fn test2(hoge: String, fuga: bool)"), - vec![(23..33, current_parameter())] - ) - ); - } - - #[gpui::test] - fn test_create_signature_help_markdown_string_6(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![ - lsp::SignatureInformation { - label: "fn test1(foo: u8, bar: &str)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("foo: u8".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("bar: &str".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - lsp::SignatureInformation { - label: "fn test2(hoge: String, fuga: bool)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("hoge: String".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("fuga: bool".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - ], - active_signature: Some(1), - active_parameter: None, - }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(maybe_markdown.is_some()); - - let markdown = maybe_markdown.unwrap(); - let signature = markdown.signatures[markdown.active_signature].clone(); - let markdown = (signature.label, signature.highlights); - assert_eq!( - markdown, - ( - SharedString::new("fn test2(hoge: String, fuga: bool)"), - vec![(9..21, current_parameter())] - ) - ); - } - - #[gpui::test] - fn test_create_signature_help_markdown_string_7(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![ - lsp::SignatureInformation { - label: "fn test1(foo: u8, bar: &str)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("foo: u8".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("bar: &str".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - lsp::SignatureInformation { - label: "fn test2(hoge: String, fuga: bool)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("hoge: String".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("fuga: bool".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - lsp::SignatureInformation { - label: "fn test3(one: usize, two: u32)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("one: usize".to_string()), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("two: u32".to_string()), - documentation: None, - }, - ]), - active_parameter: None, - }, - ], - active_signature: Some(2), - active_parameter: Some(1), - }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(maybe_markdown.is_some()); - - let markdown = maybe_markdown.unwrap(); - let signature = markdown.signatures[markdown.active_signature].clone(); - let markdown = (signature.label, signature.highlights); - assert_eq!( - markdown, - ( - SharedString::new("fn test3(one: usize, two: u32)"), - vec![(21..29, current_parameter())] - ) - ); - } - - #[gpui::test] - fn test_create_signature_help_markdown_string_8(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![], - active_signature: None, - active_parameter: None, - }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(maybe_markdown.is_none()); - } - - #[gpui::test] - fn test_create_signature_help_markdown_string_9(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![lsp::SignatureInformation { - label: "fn test(foo: u8, bar: &str)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::LabelOffsets([8, 15]), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::LabelOffsets([17, 26]), - documentation: None, - }, - ]), - active_parameter: None, - }], - active_signature: Some(0), - active_parameter: Some(0), - }; - let maybe_markdown = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(maybe_markdown.is_some()); - - let markdown = maybe_markdown.unwrap(); - let signature = markdown.signatures[markdown.active_signature].clone(); - let markdown = (signature.label, signature.highlights); - assert_eq!( - markdown, - ( - SharedString::new("fn test(foo: u8, bar: &str)"), - vec![(8..15, current_parameter())] - ) - ); - } - - #[gpui::test] - fn test_parameter_documentation(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![lsp::SignatureInformation { - label: "fn test(foo: u8, bar: &str)".to_string(), - documentation: Some(Documentation::String( - "This is a test documentation".to_string(), - )), - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("foo: u8".to_string()), - documentation: Some(Documentation::String("The foo parameter".to_string())), - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::Simple("bar: &str".to_string()), - documentation: Some(Documentation::String("The bar parameter".to_string())), - }, - ]), - active_parameter: None, - }], - active_signature: Some(0), - active_parameter: Some(0), - }; - let maybe_signature_help = - cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(maybe_signature_help.is_some()); - - let signature_help = maybe_signature_help.unwrap(); - let signature = &signature_help.signatures[signature_help.active_signature]; - - // Check that parameter documentation is extracted - assert_eq!(signature.parameters.len(), 2); - assert_eq!( - signature.parameters[0] - .documentation - .as_ref() - .unwrap() - .update(cx, |documentation, _| documentation.source().to_owned()), - "The foo parameter", - ); - assert_eq!( - signature.parameters[1] - .documentation - .as_ref() - .unwrap() - .update(cx, |documentation, _| documentation.source().to_owned()), - "The bar parameter", - ); - - // Check that the active parameter is correct - assert_eq!(signature.active_parameter, Some(0)); - } - - #[gpui::test] - fn test_create_signature_help_implements_utf16_spec(cx: &mut TestAppContext) { - let signature_help = lsp::SignatureHelp { - signatures: vec![lsp::SignatureInformation { - label: "fn test(🦀: u8, 🦀: &str)".to_string(), - documentation: None, - parameters: Some(vec![ - lsp::ParameterInformation { - label: lsp::ParameterLabel::LabelOffsets([8, 10]), - documentation: None, - }, - lsp::ParameterInformation { - label: lsp::ParameterLabel::LabelOffsets([16, 18]), - documentation: None, - }, - ]), - active_parameter: None, - }], - active_signature: Some(0), - active_parameter: Some(0), - }; - let signature_help = cx.update(|cx| SignatureHelp::new(signature_help, None, None, cx)); - assert!(signature_help.is_some()); - - let markdown = signature_help.unwrap(); - let signature = markdown.signatures[markdown.active_signature].clone(); - let markdown = (signature.label, signature.highlights); - assert_eq!( - markdown, - ( - SharedString::new("fn test(🦀: u8, 🦀: &str)"), - vec![(8..12, current_parameter())] - ) - ); - } -} diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs deleted file mode 100644 index a8c639fe59..0000000000 --- a/crates/project/src/lsp_store.rs +++ /dev/null @@ -1,14274 +0,0 @@ -//! LSP store provides unified access to the language server protocol. -//! The consumers of LSP store can interact with language servers without knowing exactly which language server they're interacting with. -//! -//! # Local/Remote LSP Stores -//! This module is split up into three distinct parts: -//! - [`LocalLspStore`], which is ran on the host machine (either project host or SSH host), that manages the lifecycle of language servers. -//! - [`RemoteLspStore`], which is ran on the remote machine (project guests) which is mostly about passing through the requests via RPC. -//! The remote stores don't really care about which language server they're running against - they don't usually get to decide which language server is going to responsible for handling their request. -//! - [`LspStore`], which unifies the two under one consistent interface for interacting with language servers. -//! -//! Most of the interesting work happens at the local layer, as bulk of the complexity is with managing the lifecycle of language servers. The actual implementation of the LSP protocol is handled by [`lsp`] crate. -pub mod clangd_ext; -pub mod json_language_server_ext; -pub mod log_store; -pub mod lsp_ext_command; -pub mod rust_analyzer_ext; -pub mod vue_language_server_ext; - -mod inlay_hint_cache; - -use self::inlay_hint_cache::BufferInlayHints; -use crate::{ - CodeAction, ColorPresentation, Completion, CompletionDisplayOptions, CompletionResponse, - CompletionSource, CoreCompletion, DocumentColor, Hover, InlayHint, InlayId, LocationLink, - LspAction, LspPullDiagnostics, ManifestProvidersStore, Project, ProjectItem, ProjectPath, - ProjectTransaction, PulledDiagnostics, ResolveState, Symbol, - buffer_store::{BufferStore, BufferStoreEvent}, - environment::ProjectEnvironment, - lsp_command::{self, *}, - lsp_store::{ - self, - log_store::{GlobalLogStore, LanguageServerKind}, - }, - manifest_tree::{ - LanguageServerTree, LanguageServerTreeNode, LaunchDisposition, ManifestQueryDelegate, - ManifestTree, - }, - prettier_store::{self, PrettierStore, PrettierStoreEvent}, - project_settings::{LspSettings, ProjectSettings}, - toolchain_store::{LocalToolchainStore, ToolchainStoreEvent}, - worktree_store::{WorktreeStore, WorktreeStoreEvent}, - yarn::YarnPathStore, -}; -use anyhow::{Context as _, Result, anyhow}; -use async_trait::async_trait; -use client::{TypedEnvelope, proto}; -use clock::Global; -use collections::{BTreeMap, BTreeSet, HashMap, HashSet, btree_map}; -use futures::{ - AsyncWriteExt, Future, FutureExt, StreamExt, - future::{Either, Shared, join_all, pending, select}, - select, select_biased, - stream::FuturesUnordered, -}; -use globset::{Glob, GlobBuilder, GlobMatcher, GlobSet, GlobSetBuilder}; -use gpui::{ - App, AppContext, AsyncApp, Context, Entity, EventEmitter, PromptLevel, SharedString, Task, - WeakEntity, -}; -use http_client::HttpClient; -use itertools::Itertools as _; -use language::{ - Bias, BinaryStatus, Buffer, BufferRow, BufferSnapshot, CachedLspAdapter, CodeLabel, Diagnostic, - DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, Diff, File as _, Language, LanguageName, - LanguageRegistry, LocalFile, LspAdapter, LspAdapterDelegate, LspInstaller, ManifestDelegate, - ManifestName, Patch, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Toolchain, - Transaction, Unclipped, - language_settings::{FormatOnSave, Formatter, LanguageSettings, language_settings}, - point_to_lsp, - proto::{ - deserialize_anchor, deserialize_lsp_edit, deserialize_version, serialize_anchor, - serialize_lsp_edit, serialize_version, - }, - range_from_lsp, range_to_lsp, - row_chunk::RowChunk, -}; -use lsp::{ - AdapterServerCapabilities, CodeActionKind, CompletionContext, CompletionOptions, - DiagnosticServerCapabilities, DiagnosticSeverity, DiagnosticTag, - DidChangeWatchedFilesRegistrationOptions, Edit, FileOperationFilter, FileOperationPatternKind, - FileOperationRegistrationOptions, FileRename, FileSystemWatcher, LSP_REQUEST_TIMEOUT, - LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions, LanguageServerId, - LanguageServerName, LanguageServerSelector, LspRequestFuture, MessageActionItem, MessageType, - OneOf, RenameFilesParams, SymbolKind, TextDocumentSyncSaveOptions, TextEdit, Uri, - WillRenameFiles, WorkDoneProgressCancelParams, WorkspaceFolder, notification::DidRenameFiles, -}; -use node_runtime::read_package_installed_version; -use parking_lot::Mutex; -use postage::{mpsc, sink::Sink, stream::Stream, watch}; -use rand::prelude::*; -use rpc::{ - AnyProtoClient, ErrorCode, ErrorExt as _, - proto::{LspRequestId, LspRequestMessage as _}, -}; -use serde::Serialize; -use serde_json::Value; -use settings::{Settings, SettingsLocation, SettingsStore}; -use sha2::{Digest, Sha256}; -use smol::channel::Sender; -use snippet::Snippet; -use std::{ - any::TypeId, - borrow::Cow, - cell::RefCell, - cmp::{Ordering, Reverse}, - convert::TryInto, - ffi::OsStr, - future::ready, - iter, mem, - ops::{ControlFlow, Range}, - path::{self, Path, PathBuf}, - pin::pin, - rc::Rc, - sync::{ - Arc, - atomic::{self, AtomicUsize}, - }, - time::{Duration, Instant}, - vec, -}; -use sum_tree::Dimensions; -use text::{Anchor, BufferId, LineEnding, OffsetRangeExt, ToPoint as _}; - -use util::{ - ConnectionResult, ResultExt as _, debug_panic, defer, maybe, merge_json_value_into, - paths::{PathStyle, SanitizedPath}, - post_inc, - rel_path::RelPath, -}; - -pub use fs::*; -pub use language::Location; -pub use lsp_store::inlay_hint_cache::{CacheInlayHints, InvalidationStrategy}; -#[cfg(any(test, feature = "test-support"))] -pub use prettier::FORMAT_SUFFIX as TEST_PRETTIER_FORMAT_SUFFIX; -pub use worktree::{ - Entry, EntryKind, FS_WATCH_LATENCY, File, LocalWorktree, PathChange, ProjectEntryId, - UpdatedEntriesSet, UpdatedGitRepositoriesSet, Worktree, WorktreeId, WorktreeSettings, -}; - -const SERVER_LAUNCHING_BEFORE_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5); -pub const SERVER_PROGRESS_THROTTLE_TIMEOUT: Duration = Duration::from_millis(100); -const WORKSPACE_DIAGNOSTICS_TOKEN_START: &str = "id:"; -const SERVER_DOWNLOAD_TIMEOUT: Duration = Duration::from_secs(10); - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)] -pub enum ProgressToken { - Number(i32), - String(SharedString), -} - -impl std::fmt::Display for ProgressToken { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Number(number) => write!(f, "{number}"), - Self::String(string) => write!(f, "{string}"), - } - } -} - -impl ProgressToken { - fn from_lsp(value: lsp::NumberOrString) -> Self { - match value { - lsp::NumberOrString::Number(number) => Self::Number(number), - lsp::NumberOrString::String(string) => Self::String(SharedString::new(string)), - } - } - - fn to_lsp(&self) -> lsp::NumberOrString { - match self { - Self::Number(number) => lsp::NumberOrString::Number(*number), - Self::String(string) => lsp::NumberOrString::String(string.to_string()), - } - } - - fn from_proto(value: proto::ProgressToken) -> Option { - Some(match value.value? { - proto::progress_token::Value::Number(number) => Self::Number(number), - proto::progress_token::Value::String(string) => Self::String(SharedString::new(string)), - }) - } - - fn to_proto(&self) -> proto::ProgressToken { - proto::ProgressToken { - value: Some(match self { - Self::Number(number) => proto::progress_token::Value::Number(*number), - Self::String(string) => proto::progress_token::Value::String(string.to_string()), - }), - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FormatTrigger { - Save, - Manual, -} - -pub enum LspFormatTarget { - Buffers, - Ranges(BTreeMap>>), -} - -#[derive(Clone, PartialEq, Eq, Hash)] -pub struct OpenLspBufferHandle(Entity); - -struct OpenLspBuffer(Entity); - -impl FormatTrigger { - fn from_proto(value: i32) -> FormatTrigger { - match value { - 0 => FormatTrigger::Save, - 1 => FormatTrigger::Manual, - _ => FormatTrigger::Save, - } - } -} - -#[derive(Clone)] -struct UnifiedLanguageServer { - id: LanguageServerId, - project_roots: HashSet>, -} - -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -struct LanguageServerSeed { - worktree_id: WorktreeId, - name: LanguageServerName, - toolchain: Option, - settings: Arc, -} - -#[derive(Debug)] -pub struct DocumentDiagnosticsUpdate<'a, D> { - pub diagnostics: D, - pub result_id: Option, - pub registration_id: Option, - pub server_id: LanguageServerId, - pub disk_based_sources: Cow<'a, [String]>, -} - -pub struct DocumentDiagnostics { - diagnostics: Vec>>, - document_abs_path: PathBuf, - version: Option, -} - -#[derive(Default, Debug)] -struct DynamicRegistrations { - did_change_watched_files: HashMap>, - diagnostics: HashMap, DiagnosticServerCapabilities>, -} - -pub struct LocalLspStore { - weak: WeakEntity, - worktree_store: Entity, - toolchain_store: Entity, - http_client: Arc, - environment: Entity, - fs: Arc, - languages: Arc, - language_server_ids: HashMap, - yarn: Entity, - pub language_servers: HashMap, - buffers_being_formatted: HashSet, - last_workspace_edits_by_language_server: HashMap, - language_server_watched_paths: HashMap, - watched_manifest_filenames: HashSet, - language_server_paths_watched_for_rename: - HashMap, - language_server_dynamic_registrations: HashMap, - supplementary_language_servers: - HashMap)>, - prettier_store: Entity, - next_diagnostic_group_id: usize, - diagnostics: HashMap< - WorktreeId, - HashMap< - Arc, - Vec<( - LanguageServerId, - Vec>>, - )>, - >, - >, - buffer_snapshots: HashMap>>, // buffer_id -> server_id -> vec of snapshots - _subscription: gpui::Subscription, - lsp_tree: LanguageServerTree, - registered_buffers: HashMap, - buffers_opened_in_servers: HashMap>, - buffer_pull_diagnostics_result_ids: HashMap< - LanguageServerId, - HashMap, HashMap>>, - >, - workspace_pull_diagnostics_result_ids: HashMap< - LanguageServerId, - HashMap, HashMap>>, - >, -} - -impl LocalLspStore { - /// Returns the running language server for the given ID. Note if the language server is starting, it will not be returned. - pub fn running_language_server_for_id( - &self, - id: LanguageServerId, - ) -> Option<&Arc> { - let language_server_state = self.language_servers.get(&id)?; - - match language_server_state { - LanguageServerState::Running { server, .. } => Some(server), - LanguageServerState::Starting { .. } => None, - } - } - - fn get_or_insert_language_server( - &mut self, - worktree_handle: &Entity, - delegate: Arc, - disposition: &Arc, - language_name: &LanguageName, - cx: &mut App, - ) -> LanguageServerId { - let key = LanguageServerSeed { - worktree_id: worktree_handle.read(cx).id(), - name: disposition.server_name.clone(), - settings: disposition.settings.clone(), - toolchain: disposition.toolchain.clone(), - }; - if let Some(state) = self.language_server_ids.get_mut(&key) { - state.project_roots.insert(disposition.path.path.clone()); - state.id - } else { - let adapter = self - .languages - .lsp_adapters(language_name) - .into_iter() - .find(|adapter| adapter.name() == disposition.server_name) - .expect("To find LSP adapter"); - let new_language_server_id = self.start_language_server( - worktree_handle, - delegate, - adapter, - disposition.settings.clone(), - key.clone(), - cx, - ); - if let Some(state) = self.language_server_ids.get_mut(&key) { - state.project_roots.insert(disposition.path.path.clone()); - } else { - debug_assert!( - false, - "Expected `start_language_server` to ensure that `key` exists in a map" - ); - } - new_language_server_id - } - } - - fn start_language_server( - &mut self, - worktree_handle: &Entity, - delegate: Arc, - adapter: Arc, - settings: Arc, - key: LanguageServerSeed, - cx: &mut App, - ) -> LanguageServerId { - let worktree = worktree_handle.read(cx); - - let root_path = worktree.abs_path(); - let toolchain = key.toolchain.clone(); - let override_options = settings.initialization_options.clone(); - - let stderr_capture = Arc::new(Mutex::new(Some(String::new()))); - - let server_id = self.languages.next_language_server_id(); - log::trace!( - "attempting to start language server {:?}, path: {root_path:?}, id: {server_id}", - adapter.name.0 - ); - - let binary = self.get_language_server_binary( - adapter.clone(), - settings, - toolchain.clone(), - delegate.clone(), - true, - cx, - ); - let pending_workspace_folders: Arc>> = Default::default(); - - let pending_server = cx.spawn({ - let adapter = adapter.clone(); - let server_name = adapter.name.clone(); - let stderr_capture = stderr_capture.clone(); - #[cfg(any(test, feature = "test-support"))] - let lsp_store = self.weak.clone(); - let pending_workspace_folders = pending_workspace_folders.clone(); - async move |cx| { - let binary = binary.await?; - #[cfg(any(test, feature = "test-support"))] - if let Some(server) = lsp_store - .update(&mut cx.clone(), |this, cx| { - this.languages.create_fake_language_server( - server_id, - &server_name, - binary.clone(), - &mut cx.to_async(), - ) - }) - .ok() - .flatten() - { - return Ok(server); - } - - let code_action_kinds = adapter.code_action_kinds(); - lsp::LanguageServer::new( - stderr_capture, - server_id, - server_name, - binary, - &root_path, - code_action_kinds, - Some(pending_workspace_folders), - cx, - ) - } - }); - - let startup = { - let server_name = adapter.name.0.clone(); - let delegate = delegate as Arc; - let key = key.clone(); - let adapter = adapter.clone(); - let lsp_store = self.weak.clone(); - let pending_workspace_folders = pending_workspace_folders.clone(); - - let pull_diagnostics = ProjectSettings::get_global(cx) - .diagnostics - .lsp_pull_diagnostics - .enabled; - cx.spawn(async move |cx| { - let result = async { - let language_server = pending_server.await?; - - let workspace_config = Self::workspace_configuration_for_adapter( - adapter.adapter.clone(), - &delegate, - toolchain, - None, - cx, - ) - .await?; - - let mut initialization_options = Self::initialization_options_for_adapter( - adapter.adapter.clone(), - &delegate, - ) - .await?; - - match (&mut initialization_options, override_options) { - (Some(initialization_options), Some(override_options)) => { - merge_json_value_into(override_options, initialization_options); - } - (None, override_options) => initialization_options = override_options, - _ => {} - } - - let initialization_params = cx.update(|cx| { - let mut params = - language_server.default_initialize_params(pull_diagnostics, cx); - params.initialization_options = initialization_options; - adapter.adapter.prepare_initialize_params(params, cx) - })??; - - Self::setup_lsp_messages( - lsp_store.clone(), - &language_server, - delegate.clone(), - adapter.clone(), - ); - - let did_change_configuration_params = lsp::DidChangeConfigurationParams { - settings: workspace_config, - }; - let language_server = cx - .update(|cx| { - language_server.initialize( - initialization_params, - Arc::new(did_change_configuration_params.clone()), - cx, - ) - })? - .await - .inspect_err(|_| { - if let Some(lsp_store) = lsp_store.upgrade() { - lsp_store - .update(cx, |lsp_store, cx| { - lsp_store.cleanup_lsp_data(server_id); - cx.emit(LspStoreEvent::LanguageServerRemoved(server_id)) - }) - .ok(); - } - })?; - - language_server.notify::( - did_change_configuration_params, - )?; - - anyhow::Ok(language_server) - } - .await; - - match result { - Ok(server) => { - lsp_store - .update(cx, |lsp_store, cx| { - lsp_store.insert_newly_running_language_server( - adapter, - server.clone(), - server_id, - key, - pending_workspace_folders, - cx, - ); - }) - .ok(); - stderr_capture.lock().take(); - Some(server) - } - - Err(err) => { - let log = stderr_capture.lock().take().unwrap_or_default(); - delegate.update_status( - adapter.name(), - BinaryStatus::Failed { - error: if log.is_empty() { - format!("{err:#}") - } else { - format!("{err:#}\n-- stderr --\n{log}") - }, - }, - ); - log::error!("Failed to start language server {server_name:?}: {err:?}"); - if !log.is_empty() { - log::error!("server stderr: {log}"); - } - None - } - } - }) - }; - let state = LanguageServerState::Starting { - startup, - pending_workspace_folders, - }; - - self.languages - .update_lsp_binary_status(adapter.name(), BinaryStatus::Starting); - - self.language_servers.insert(server_id, state); - self.language_server_ids - .entry(key) - .or_insert(UnifiedLanguageServer { - id: server_id, - project_roots: Default::default(), - }); - server_id - } - - fn get_language_server_binary( - &self, - adapter: Arc, - settings: Arc, - toolchain: Option, - delegate: Arc, - allow_binary_download: bool, - cx: &mut App, - ) -> Task> { - if let Some(settings) = &settings.binary - && let Some(path) = settings.path.as_ref().map(PathBuf::from) - { - let settings = settings.clone(); - - return cx.background_spawn(async move { - let mut env = delegate.shell_env().await; - env.extend(settings.env.unwrap_or_default()); - - Ok(LanguageServerBinary { - path: delegate.resolve_executable_path(path), - env: Some(env), - arguments: settings - .arguments - .unwrap_or_default() - .iter() - .map(Into::into) - .collect(), - }) - }); - } - let lsp_binary_options = LanguageServerBinaryOptions { - allow_path_lookup: !settings - .binary - .as_ref() - .and_then(|b| b.ignore_system_version) - .unwrap_or_default(), - allow_binary_download, - pre_release: settings - .fetch - .as_ref() - .and_then(|f| f.pre_release) - .unwrap_or(false), - }; - - cx.spawn(async move |cx| { - let (existing_binary, maybe_download_binary) = adapter - .clone() - .get_language_server_command(delegate.clone(), toolchain, lsp_binary_options, cx) - .await - .await; - - delegate.update_status(adapter.name.clone(), BinaryStatus::None); - - let mut binary = match (existing_binary, maybe_download_binary) { - (binary, None) => binary?, - (Err(_), Some(downloader)) => downloader.await?, - (Ok(existing_binary), Some(downloader)) => { - let mut download_timeout = cx - .background_executor() - .timer(SERVER_DOWNLOAD_TIMEOUT) - .fuse(); - let mut downloader = downloader.fuse(); - futures::select! { - _ = download_timeout => { - // Return existing binary and kick the existing work to the background. - cx.spawn(async move |_| downloader.await).detach(); - Ok(existing_binary) - }, - downloaded_or_existing_binary = downloader => { - // If download fails, this results in the existing binary. - downloaded_or_existing_binary - } - }? - } - }; - let mut shell_env = delegate.shell_env().await; - - shell_env.extend(binary.env.unwrap_or_default()); - - if let Some(settings) = settings.binary.as_ref() { - if let Some(arguments) = &settings.arguments { - binary.arguments = arguments.iter().map(Into::into).collect(); - } - if let Some(env) = &settings.env { - shell_env.extend(env.iter().map(|(k, v)| (k.clone(), v.clone()))); - } - } - - binary.env = Some(shell_env); - Ok(binary) - }) - } - - fn setup_lsp_messages( - lsp_store: WeakEntity, - language_server: &LanguageServer, - delegate: Arc, - adapter: Arc, - ) { - let name = language_server.name(); - let server_id = language_server.server_id(); - language_server - .on_notification::({ - let adapter = adapter.clone(); - let this = lsp_store.clone(); - move |mut params, cx| { - let adapter = adapter.clone(); - if let Some(this) = this.upgrade() { - this.update(cx, |this, cx| { - { - let buffer = params - .uri - .to_file_path() - .map(|file_path| this.get_buffer(&file_path, cx)) - .ok() - .flatten(); - adapter.process_diagnostics(&mut params, server_id, buffer); - } - - this.merge_lsp_diagnostics( - DiagnosticSourceKind::Pushed, - vec![DocumentDiagnosticsUpdate { - server_id, - diagnostics: params, - result_id: None, - disk_based_sources: Cow::Borrowed( - &adapter.disk_based_diagnostic_sources, - ), - registration_id: None, - }], - |_, diagnostic, cx| match diagnostic.source_kind { - DiagnosticSourceKind::Other | DiagnosticSourceKind::Pushed => { - adapter.retain_old_diagnostic(diagnostic, cx) - } - DiagnosticSourceKind::Pulled => true, - }, - cx, - ) - .log_err(); - }) - .ok(); - } - } - }) - .detach(); - language_server - .on_request::({ - let adapter = adapter.adapter.clone(); - let delegate = delegate.clone(); - let this = lsp_store.clone(); - move |params, cx| { - let adapter = adapter.clone(); - let delegate = delegate.clone(); - let this = this.clone(); - let mut cx = cx.clone(); - async move { - let toolchain_for_id = this - .update(&mut cx, |this, _| { - this.as_local()?.language_server_ids.iter().find_map( - |(seed, value)| { - (value.id == server_id).then(|| seed.toolchain.clone()) - }, - ) - })? - .context("Expected the LSP store to be in a local mode")?; - - let mut scope_uri_to_workspace_config = BTreeMap::new(); - for item in ¶ms.items { - let scope_uri = item.scope_uri.clone(); - let std::collections::btree_map::Entry::Vacant(new_scope_uri) = - scope_uri_to_workspace_config.entry(scope_uri.clone()) - else { - // We've already queried workspace configuration of this URI. - continue; - }; - let workspace_config = Self::workspace_configuration_for_adapter( - adapter.clone(), - &delegate, - toolchain_for_id.clone(), - scope_uri, - &mut cx, - ) - .await?; - new_scope_uri.insert(workspace_config); - } - - Ok(params - .items - .into_iter() - .filter_map(|item| { - let workspace_config = - scope_uri_to_workspace_config.get(&item.scope_uri)?; - if let Some(section) = &item.section { - Some( - workspace_config - .get(section) - .cloned() - .unwrap_or(serde_json::Value::Null), - ) - } else { - Some(workspace_config.clone()) - } - }) - .collect()) - } - } - }) - .detach(); - - language_server - .on_request::({ - let this = lsp_store.clone(); - move |_, cx| { - let this = this.clone(); - let cx = cx.clone(); - async move { - let Some(server) = - this.read_with(&cx, |this, _| this.language_server_for_id(server_id))? - else { - return Ok(None); - }; - let root = server.workspace_folders(); - Ok(Some( - root.into_iter() - .map(|uri| WorkspaceFolder { - uri, - name: Default::default(), - }) - .collect(), - )) - } - } - }) - .detach(); - // Even though we don't have handling for these requests, respond to them to - // avoid stalling any language server like `gopls` which waits for a response - // to these requests when initializing. - language_server - .on_request::({ - let this = lsp_store.clone(); - move |params, cx| { - let this = this.clone(); - let mut cx = cx.clone(); - async move { - this.update(&mut cx, |this, _| { - if let Some(status) = this.language_server_statuses.get_mut(&server_id) - { - status - .progress_tokens - .insert(ProgressToken::from_lsp(params.token)); - } - })?; - - Ok(()) - } - } - }) - .detach(); - - language_server - .on_request::({ - let lsp_store = lsp_store.clone(); - move |params, cx| { - let lsp_store = lsp_store.clone(); - let mut cx = cx.clone(); - async move { - lsp_store - .update(&mut cx, |lsp_store, cx| { - if lsp_store.as_local().is_some() { - match lsp_store - .register_server_capabilities(server_id, params, cx) - { - Ok(()) => {} - Err(e) => { - log::error!( - "Failed to register server capabilities: {e:#}" - ); - } - }; - } - }) - .ok(); - Ok(()) - } - } - }) - .detach(); - - language_server - .on_request::({ - let lsp_store = lsp_store.clone(); - move |params, cx| { - let lsp_store = lsp_store.clone(); - let mut cx = cx.clone(); - async move { - lsp_store - .update(&mut cx, |lsp_store, cx| { - if lsp_store.as_local().is_some() { - match lsp_store - .unregister_server_capabilities(server_id, params, cx) - { - Ok(()) => {} - Err(e) => { - log::error!( - "Failed to unregister server capabilities: {e:#}" - ); - } - } - } - }) - .ok(); - Ok(()) - } - } - }) - .detach(); - - language_server - .on_request::({ - let this = lsp_store.clone(); - move |params, cx| { - let mut cx = cx.clone(); - let this = this.clone(); - async move { - LocalLspStore::on_lsp_workspace_edit( - this.clone(), - params, - server_id, - &mut cx, - ) - .await - } - } - }) - .detach(); - - language_server - .on_request::({ - let lsp_store = lsp_store.clone(); - let request_id = Arc::new(AtomicUsize::new(0)); - move |(), cx| { - let lsp_store = lsp_store.clone(); - let request_id = request_id.clone(); - let mut cx = cx.clone(); - async move { - lsp_store - .update(&mut cx, |lsp_store, cx| { - let request_id = - Some(request_id.fetch_add(1, atomic::Ordering::AcqRel)); - cx.emit(LspStoreEvent::RefreshInlayHints { - server_id, - request_id, - }); - lsp_store - .downstream_client - .as_ref() - .map(|(client, project_id)| { - client.send(proto::RefreshInlayHints { - project_id: *project_id, - server_id: server_id.to_proto(), - request_id: request_id.map(|id| id as u64), - }) - }) - })? - .transpose()?; - Ok(()) - } - } - }) - .detach(); - - language_server - .on_request::({ - let this = lsp_store.clone(); - move |(), cx| { - let this = this.clone(); - let mut cx = cx.clone(); - async move { - this.update(&mut cx, |this, cx| { - cx.emit(LspStoreEvent::RefreshCodeLens); - this.downstream_client.as_ref().map(|(client, project_id)| { - client.send(proto::RefreshCodeLens { - project_id: *project_id, - }) - }) - })? - .transpose()?; - Ok(()) - } - } - }) - .detach(); - - language_server - .on_request::({ - let this = lsp_store.clone(); - move |(), cx| { - let this = this.clone(); - let mut cx = cx.clone(); - async move { - this.update(&mut cx, |lsp_store, _| { - lsp_store.pull_workspace_diagnostics(server_id); - lsp_store - .downstream_client - .as_ref() - .map(|(client, project_id)| { - client.send(proto::PullWorkspaceDiagnostics { - project_id: *project_id, - server_id: server_id.to_proto(), - }) - }) - })? - .transpose()?; - Ok(()) - } - } - }) - .detach(); - - language_server - .on_request::({ - let this = lsp_store.clone(); - let name = name.to_string(); - move |params, cx| { - let this = this.clone(); - let name = name.to_string(); - let mut cx = cx.clone(); - async move { - let actions = params.actions.unwrap_or_default(); - let (tx, rx) = smol::channel::bounded(1); - let request = LanguageServerPromptRequest { - level: match params.typ { - lsp::MessageType::ERROR => PromptLevel::Critical, - lsp::MessageType::WARNING => PromptLevel::Warning, - _ => PromptLevel::Info, - }, - message: params.message, - actions, - response_channel: tx, - lsp_name: name.clone(), - }; - - let did_update = this - .update(&mut cx, |_, cx| { - cx.emit(LspStoreEvent::LanguageServerPrompt(request)); - }) - .is_ok(); - if did_update { - let response = rx.recv().await.ok(); - Ok(response) - } else { - Ok(None) - } - } - } - }) - .detach(); - language_server - .on_notification::({ - let this = lsp_store.clone(); - let name = name.to_string(); - move |params, cx| { - let this = this.clone(); - let name = name.to_string(); - let mut cx = cx.clone(); - - let (tx, _) = smol::channel::bounded(1); - let request = LanguageServerPromptRequest { - level: match params.typ { - lsp::MessageType::ERROR => PromptLevel::Critical, - lsp::MessageType::WARNING => PromptLevel::Warning, - _ => PromptLevel::Info, - }, - message: params.message, - actions: vec![], - response_channel: tx, - lsp_name: name, - }; - - let _ = this.update(&mut cx, |_, cx| { - cx.emit(LspStoreEvent::LanguageServerPrompt(request)); - }); - } - }) - .detach(); - - let disk_based_diagnostics_progress_token = - adapter.disk_based_diagnostics_progress_token.clone(); - - language_server - .on_notification::({ - let this = lsp_store.clone(); - move |params, cx| { - if let Some(this) = this.upgrade() { - this.update(cx, |this, cx| { - this.on_lsp_progress( - params, - server_id, - disk_based_diagnostics_progress_token.clone(), - cx, - ); - }) - .ok(); - } - } - }) - .detach(); - - language_server - .on_notification::({ - let this = lsp_store.clone(); - move |params, cx| { - if let Some(this) = this.upgrade() { - this.update(cx, |_, cx| { - cx.emit(LspStoreEvent::LanguageServerLog( - server_id, - LanguageServerLogType::Log(params.typ), - params.message, - )); - }) - .ok(); - } - } - }) - .detach(); - - language_server - .on_notification::({ - let this = lsp_store.clone(); - move |params, cx| { - let mut cx = cx.clone(); - if let Some(this) = this.upgrade() { - this.update(&mut cx, |_, cx| { - cx.emit(LspStoreEvent::LanguageServerLog( - server_id, - LanguageServerLogType::Trace { - verbose_info: params.verbose, - }, - params.message, - )); - }) - .ok(); - } - } - }) - .detach(); - - vue_language_server_ext::register_requests(lsp_store.clone(), language_server); - json_language_server_ext::register_requests(lsp_store.clone(), language_server); - rust_analyzer_ext::register_notifications(lsp_store.clone(), language_server); - clangd_ext::register_notifications(lsp_store, language_server, adapter); - } - - fn shutdown_language_servers_on_quit( - &mut self, - _: &mut Context, - ) -> impl Future + use<> { - let shutdown_futures = self - .language_servers - .drain() - .map(|(_, server_state)| Self::shutdown_server(server_state)) - .collect::>(); - - async move { - join_all(shutdown_futures).await; - } - } - - async fn shutdown_server(server_state: LanguageServerState) -> anyhow::Result<()> { - match server_state { - LanguageServerState::Running { server, .. } => { - if let Some(shutdown) = server.shutdown() { - shutdown.await; - } - } - LanguageServerState::Starting { startup, .. } => { - if let Some(server) = startup.await - && let Some(shutdown) = server.shutdown() - { - shutdown.await; - } - } - } - Ok(()) - } - - fn language_servers_for_worktree( - &self, - worktree_id: WorktreeId, - ) -> impl Iterator> { - self.language_server_ids - .iter() - .filter_map(move |(seed, state)| { - if seed.worktree_id != worktree_id { - return None; - } - - if let Some(LanguageServerState::Running { server, .. }) = - self.language_servers.get(&state.id) - { - Some(server) - } else { - None - } - }) - } - - fn language_server_ids_for_project_path( - &self, - project_path: ProjectPath, - language: &Language, - cx: &mut App, - ) -> Vec { - let Some(worktree) = self - .worktree_store - .read(cx) - .worktree_for_id(project_path.worktree_id, cx) - else { - return Vec::new(); - }; - let delegate: Arc = - Arc::new(ManifestQueryDelegate::new(worktree.read(cx).snapshot())); - - self.lsp_tree - .get( - project_path, - language.name(), - language.manifest(), - &delegate, - cx, - ) - .collect::>() - } - - fn language_server_ids_for_buffer( - &self, - buffer: &Buffer, - cx: &mut App, - ) -> Vec { - if let Some((file, language)) = File::from_dyn(buffer.file()).zip(buffer.language()) { - let worktree_id = file.worktree_id(cx); - - let path: Arc = file - .path() - .parent() - .map(Arc::from) - .unwrap_or_else(|| file.path().clone()); - let worktree_path = ProjectPath { worktree_id, path }; - self.language_server_ids_for_project_path(worktree_path, language, cx) - } else { - Vec::new() - } - } - - fn language_servers_for_buffer<'a>( - &'a self, - buffer: &'a Buffer, - cx: &'a mut App, - ) -> impl Iterator, &'a Arc)> { - self.language_server_ids_for_buffer(buffer, cx) - .into_iter() - .filter_map(|server_id| match self.language_servers.get(&server_id)? { - LanguageServerState::Running { - adapter, server, .. - } => Some((adapter, server)), - _ => None, - }) - } - - async fn execute_code_action_kind_locally( - lsp_store: WeakEntity, - mut buffers: Vec>, - kind: CodeActionKind, - push_to_history: bool, - cx: &mut AsyncApp, - ) -> anyhow::Result { - // Do not allow multiple concurrent code actions requests for the - // same buffer. - lsp_store.update(cx, |this, cx| { - let this = this.as_local_mut().unwrap(); - buffers.retain(|buffer| { - this.buffers_being_formatted - .insert(buffer.read(cx).remote_id()) - }); - })?; - let _cleanup = defer({ - let this = lsp_store.clone(); - let mut cx = cx.clone(); - let buffers = &buffers; - move || { - this.update(&mut cx, |this, cx| { - let this = this.as_local_mut().unwrap(); - for buffer in buffers { - this.buffers_being_formatted - .remove(&buffer.read(cx).remote_id()); - } - }) - .ok(); - } - }); - let mut project_transaction = ProjectTransaction::default(); - - for buffer in &buffers { - let adapters_and_servers = lsp_store.update(cx, |lsp_store, cx| { - buffer.update(cx, |buffer, cx| { - lsp_store - .as_local() - .unwrap() - .language_servers_for_buffer(buffer, cx) - .map(|(adapter, lsp)| (adapter.clone(), lsp.clone())) - .collect::>() - }) - })?; - for (_, language_server) in adapters_and_servers.iter() { - let actions = Self::get_server_code_actions_from_action_kinds( - &lsp_store, - language_server.server_id(), - vec![kind.clone()], - buffer, - cx, - ) - .await?; - Self::execute_code_actions_on_server( - &lsp_store, - language_server, - actions, - push_to_history, - &mut project_transaction, - cx, - ) - .await?; - } - } - Ok(project_transaction) - } - - async fn format_locally( - lsp_store: WeakEntity, - mut buffers: Vec, - push_to_history: bool, - trigger: FormatTrigger, - logger: zlog::Logger, - cx: &mut AsyncApp, - ) -> anyhow::Result { - // Do not allow multiple concurrent formatting requests for the - // same buffer. - lsp_store.update(cx, |this, cx| { - let this = this.as_local_mut().unwrap(); - buffers.retain(|buffer| { - this.buffers_being_formatted - .insert(buffer.handle.read(cx).remote_id()) - }); - })?; - - let _cleanup = defer({ - let this = lsp_store.clone(); - let mut cx = cx.clone(); - let buffers = &buffers; - move || { - this.update(&mut cx, |this, cx| { - let this = this.as_local_mut().unwrap(); - for buffer in buffers { - this.buffers_being_formatted - .remove(&buffer.handle.read(cx).remote_id()); - } - }) - .ok(); - } - }); - - let mut project_transaction = ProjectTransaction::default(); - - for buffer in &buffers { - zlog::debug!( - logger => - "formatting buffer '{:?}'", - buffer.abs_path.as_ref().unwrap_or(&PathBuf::from("unknown")).display() - ); - // Create an empty transaction to hold all of the formatting edits. - let formatting_transaction_id = buffer.handle.update(cx, |buffer, cx| { - // ensure no transactions created while formatting are - // grouped with the previous transaction in the history - // based on the transaction group interval - buffer.finalize_last_transaction(); - buffer - .start_transaction() - .context("transaction already open")?; - buffer.end_transaction(cx); - let transaction_id = buffer.push_empty_transaction(cx.background_executor().now()); - buffer.finalize_last_transaction(); - anyhow::Ok(transaction_id) - })??; - - let result = Self::format_buffer_locally( - lsp_store.clone(), - buffer, - formatting_transaction_id, - trigger, - logger, - cx, - ) - .await; - - buffer.handle.update(cx, |buffer, cx| { - let Some(formatting_transaction) = - buffer.get_transaction(formatting_transaction_id).cloned() - else { - zlog::warn!(logger => "no formatting transaction"); - return; - }; - if formatting_transaction.edit_ids.is_empty() { - zlog::debug!(logger => "no changes made while formatting"); - buffer.forget_transaction(formatting_transaction_id); - return; - } - if !push_to_history { - zlog::trace!(logger => "forgetting format transaction"); - buffer.forget_transaction(formatting_transaction.id); - } - project_transaction - .0 - .insert(cx.entity(), formatting_transaction); - })?; - - result?; - } - - Ok(project_transaction) - } - - async fn format_buffer_locally( - lsp_store: WeakEntity, - buffer: &FormattableBuffer, - formatting_transaction_id: clock::Lamport, - trigger: FormatTrigger, - logger: zlog::Logger, - cx: &mut AsyncApp, - ) -> Result<()> { - let (adapters_and_servers, settings) = lsp_store.update(cx, |lsp_store, cx| { - buffer.handle.update(cx, |buffer, cx| { - let adapters_and_servers = lsp_store - .as_local() - .unwrap() - .language_servers_for_buffer(buffer, cx) - .map(|(adapter, lsp)| (adapter.clone(), lsp.clone())) - .collect::>(); - let settings = - language_settings(buffer.language().map(|l| l.name()), buffer.file(), cx) - .into_owned(); - (adapters_and_servers, settings) - }) - })?; - - /// Apply edits to the buffer that will become part of the formatting transaction. - /// Fails if the buffer has been edited since the start of that transaction. - fn extend_formatting_transaction( - buffer: &FormattableBuffer, - formatting_transaction_id: text::TransactionId, - cx: &mut AsyncApp, - operation: impl FnOnce(&mut Buffer, &mut Context), - ) -> anyhow::Result<()> { - buffer.handle.update(cx, |buffer, cx| { - let last_transaction_id = buffer.peek_undo_stack().map(|t| t.transaction_id()); - if last_transaction_id != Some(formatting_transaction_id) { - anyhow::bail!("Buffer edited while formatting. Aborting") - } - buffer.start_transaction(); - operation(buffer, cx); - if let Some(transaction_id) = buffer.end_transaction(cx) { - buffer.merge_transactions(transaction_id, formatting_transaction_id); - } - Ok(()) - })? - } - - // handle whitespace formatting - if settings.remove_trailing_whitespace_on_save { - zlog::trace!(logger => "removing trailing whitespace"); - let diff = buffer - .handle - .read_with(cx, |buffer, cx| buffer.remove_trailing_whitespace(cx))? - .await; - extend_formatting_transaction(buffer, formatting_transaction_id, cx, |buffer, cx| { - buffer.apply_diff(diff, cx); - })?; - } - - if settings.ensure_final_newline_on_save { - zlog::trace!(logger => "ensuring final newline"); - extend_formatting_transaction(buffer, formatting_transaction_id, cx, |buffer, cx| { - buffer.ensure_final_newline(cx); - })?; - } - - // Formatter for `code_actions_on_format` that runs before - // the rest of the formatters - let mut code_actions_on_format_formatters = None; - let should_run_code_actions_on_format = !matches!( - (trigger, &settings.format_on_save), - (FormatTrigger::Save, &FormatOnSave::Off) - ); - if should_run_code_actions_on_format { - let have_code_actions_to_run_on_format = settings - .code_actions_on_format - .values() - .any(|enabled| *enabled); - if have_code_actions_to_run_on_format { - zlog::trace!(logger => "going to run code actions on format"); - code_actions_on_format_formatters = Some( - settings - .code_actions_on_format - .iter() - .filter_map(|(action, enabled)| enabled.then_some(action)) - .cloned() - .map(Formatter::CodeAction) - .collect::>(), - ); - } - } - - let formatters = match (trigger, &settings.format_on_save) { - (FormatTrigger::Save, FormatOnSave::Off) => &[], - (FormatTrigger::Manual, _) | (FormatTrigger::Save, FormatOnSave::On) => { - settings.formatter.as_ref() - } - }; - - let formatters = code_actions_on_format_formatters - .iter() - .flatten() - .chain(formatters); - - for formatter in formatters { - let formatter = if formatter == &Formatter::Auto { - if settings.prettier.allowed { - zlog::trace!(logger => "Formatter set to auto: defaulting to prettier"); - &Formatter::Prettier - } else { - zlog::trace!(logger => "Formatter set to auto: defaulting to primary language server"); - &Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current) - } - } else { - formatter - }; - match formatter { - Formatter::Auto => unreachable!("Auto resolved above"), - Formatter::Prettier => { - let logger = zlog::scoped!(logger => "prettier"); - zlog::trace!(logger => "formatting"); - let _timer = zlog::time!(logger => "Formatting buffer via prettier"); - - let prettier = lsp_store.read_with(cx, |lsp_store, _cx| { - lsp_store.prettier_store().unwrap().downgrade() - })?; - let diff = prettier_store::format_with_prettier(&prettier, &buffer.handle, cx) - .await - .transpose()?; - let Some(diff) = diff else { - zlog::trace!(logger => "No changes"); - continue; - }; - - extend_formatting_transaction( - buffer, - formatting_transaction_id, - cx, - |buffer, cx| { - buffer.apply_diff(diff, cx); - }, - )?; - } - Formatter::External { command, arguments } => { - let logger = zlog::scoped!(logger => "command"); - zlog::trace!(logger => "formatting"); - let _timer = zlog::time!(logger => "Formatting buffer via external command"); - - let diff = Self::format_via_external_command( - buffer, - command.as_ref(), - arguments.as_deref(), - cx, - ) - .await - .with_context(|| { - format!("Failed to format buffer via external command: {}", command) - })?; - let Some(diff) = diff else { - zlog::trace!(logger => "No changes"); - continue; - }; - - extend_formatting_transaction( - buffer, - formatting_transaction_id, - cx, - |buffer, cx| { - buffer.apply_diff(diff, cx); - }, - )?; - } - Formatter::LanguageServer(specifier) => { - let logger = zlog::scoped!(logger => "language-server"); - zlog::trace!(logger => "formatting"); - let _timer = zlog::time!(logger => "Formatting buffer using language server"); - - let Some(buffer_path_abs) = buffer.abs_path.as_ref() else { - zlog::warn!(logger => "Cannot format buffer that is not backed by a file on disk using language servers. Skipping"); - continue; - }; - - let language_server = match specifier { - settings::LanguageServerFormatterSpecifier::Specific { name } => { - adapters_and_servers.iter().find_map(|(adapter, server)| { - if adapter.name.0.as_ref() == name { - Some(server.clone()) - } else { - None - } - }) - } - settings::LanguageServerFormatterSpecifier::Current => { - adapters_and_servers.first().map(|e| e.1.clone()) - } - }; - - let Some(language_server) = language_server else { - log::debug!( - "No language server found to format buffer '{:?}'. Skipping", - buffer_path_abs.as_path().to_string_lossy() - ); - continue; - }; - - zlog::trace!( - logger => - "Formatting buffer '{:?}' using language server '{:?}'", - buffer_path_abs.as_path().to_string_lossy(), - language_server.name() - ); - - let edits = if let Some(ranges) = buffer.ranges.as_ref() { - zlog::trace!(logger => "formatting ranges"); - Self::format_ranges_via_lsp( - &lsp_store, - &buffer.handle, - ranges, - buffer_path_abs, - &language_server, - &settings, - cx, - ) - .await - .context("Failed to format ranges via language server")? - } else { - zlog::trace!(logger => "formatting full"); - Self::format_via_lsp( - &lsp_store, - &buffer.handle, - buffer_path_abs, - &language_server, - &settings, - cx, - ) - .await - .context("failed to format via language server")? - }; - - if edits.is_empty() { - zlog::trace!(logger => "No changes"); - continue; - } - extend_formatting_transaction( - buffer, - formatting_transaction_id, - cx, - |buffer, cx| { - buffer.edit(edits, None, cx); - }, - )?; - } - Formatter::CodeAction(code_action_name) => { - let logger = zlog::scoped!(logger => "code-actions"); - zlog::trace!(logger => "formatting"); - let _timer = zlog::time!(logger => "Formatting buffer using code actions"); - - let Some(buffer_path_abs) = buffer.abs_path.as_ref() else { - zlog::warn!(logger => "Cannot format buffer that is not backed by a file on disk using code actions. Skipping"); - continue; - }; - - let code_action_kind: CodeActionKind = code_action_name.clone().into(); - zlog::trace!(logger => "Attempting to resolve code actions {:?}", &code_action_kind); - - let mut actions_and_servers = Vec::new(); - - for (index, (_, language_server)) in adapters_and_servers.iter().enumerate() { - let actions_result = Self::get_server_code_actions_from_action_kinds( - &lsp_store, - language_server.server_id(), - vec![code_action_kind.clone()], - &buffer.handle, - cx, - ) - .await - .with_context(|| { - format!( - "Failed to resolve code action {:?} with language server {}", - code_action_kind, - language_server.name() - ) - }); - let Ok(actions) = actions_result else { - // note: it may be better to set result to the error and break formatters here - // but for now we try to execute the actions that we can resolve and skip the rest - zlog::error!( - logger => - "Failed to resolve code action {:?} with language server {}", - code_action_kind, - language_server.name() - ); - continue; - }; - for action in actions { - actions_and_servers.push((action, index)); - } - } - - if actions_and_servers.is_empty() { - zlog::warn!(logger => "No code actions were resolved, continuing"); - continue; - } - - 'actions: for (mut action, server_index) in actions_and_servers { - let server = &adapters_and_servers[server_index].1; - - let describe_code_action = |action: &CodeAction| { - format!( - "code action '{}' with title \"{}\" on server {}", - action - .lsp_action - .action_kind() - .unwrap_or("unknown".into()) - .as_str(), - action.lsp_action.title(), - server.name(), - ) - }; - - zlog::trace!(logger => "Executing {}", describe_code_action(&action)); - - if let Err(err) = Self::try_resolve_code_action(server, &mut action).await { - zlog::error!( - logger => - "Failed to resolve {}. Error: {}", - describe_code_action(&action), - err - ); - continue; - } - - if let Some(edit) = action.lsp_action.edit().cloned() { - // NOTE: code below duplicated from `Self::deserialize_workspace_edit` - // but filters out and logs warnings for code actions that require unreasonably - // difficult handling on our part, such as: - // - applying edits that call commands - // which can result in arbitrary workspace edits being sent from the server that - // have no way of being tied back to the command that initiated them (i.e. we - // can't know which edits are part of the format request, or if the server is done sending - // actions in response to the command) - // - actions that create/delete/modify/rename files other than the one we are formatting - // as we then would need to handle such changes correctly in the local history as well - // as the remote history through the ProjectTransaction - // - actions with snippet edits, as these simply don't make sense in the context of a format request - // Supporting these actions is not impossible, but not supported as of yet. - if edit.changes.is_none() && edit.document_changes.is_none() { - zlog::trace!( - logger => - "No changes for code action. Skipping {}", - describe_code_action(&action), - ); - continue; - } - - let mut operations = Vec::new(); - if let Some(document_changes) = edit.document_changes { - match document_changes { - lsp::DocumentChanges::Edits(edits) => operations.extend( - edits.into_iter().map(lsp::DocumentChangeOperation::Edit), - ), - lsp::DocumentChanges::Operations(ops) => operations = ops, - } - } else if let Some(changes) = edit.changes { - operations.extend(changes.into_iter().map(|(uri, edits)| { - lsp::DocumentChangeOperation::Edit(lsp::TextDocumentEdit { - text_document: - lsp::OptionalVersionedTextDocumentIdentifier { - uri, - version: None, - }, - edits: edits.into_iter().map(Edit::Plain).collect(), - }) - })); - } - - let mut edits = Vec::with_capacity(operations.len()); - - if operations.is_empty() { - zlog::trace!( - logger => - "No changes for code action. Skipping {}", - describe_code_action(&action), - ); - continue; - } - for operation in operations { - let op = match operation { - lsp::DocumentChangeOperation::Edit(op) => op, - lsp::DocumentChangeOperation::Op(_) => { - zlog::warn!( - logger => - "Code actions which create, delete, or rename files are not supported on format. Skipping {}", - describe_code_action(&action), - ); - continue 'actions; - } - }; - let Ok(file_path) = op.text_document.uri.to_file_path() else { - zlog::warn!( - logger => - "Failed to convert URI '{:?}' to file path. Skipping {}", - &op.text_document.uri, - describe_code_action(&action), - ); - continue 'actions; - }; - if &file_path != buffer_path_abs { - zlog::warn!( - logger => - "File path '{:?}' does not match buffer path '{:?}'. Skipping {}", - file_path, - buffer_path_abs, - describe_code_action(&action), - ); - continue 'actions; - } - - let mut lsp_edits = Vec::new(); - for edit in op.edits { - match edit { - Edit::Plain(edit) => { - if !lsp_edits.contains(&edit) { - lsp_edits.push(edit); - } - } - Edit::Annotated(edit) => { - if !lsp_edits.contains(&edit.text_edit) { - lsp_edits.push(edit.text_edit); - } - } - Edit::Snippet(_) => { - zlog::warn!( - logger => - "Code actions which produce snippet edits are not supported during formatting. Skipping {}", - describe_code_action(&action), - ); - continue 'actions; - } - } - } - let edits_result = lsp_store - .update(cx, |lsp_store, cx| { - lsp_store.as_local_mut().unwrap().edits_from_lsp( - &buffer.handle, - lsp_edits, - server.server_id(), - op.text_document.version, - cx, - ) - })? - .await; - let Ok(resolved_edits) = edits_result else { - zlog::warn!( - logger => - "Failed to resolve edits from LSP for buffer {:?} while handling {}", - buffer_path_abs.as_path(), - describe_code_action(&action), - ); - continue 'actions; - }; - edits.extend(resolved_edits); - } - - if edits.is_empty() { - zlog::warn!(logger => "No edits resolved from LSP"); - continue; - } - - extend_formatting_transaction( - buffer, - formatting_transaction_id, - cx, - |buffer, cx| { - zlog::info!( - "Applying edits {edits:?}. Content: {:?}", - buffer.text() - ); - buffer.edit(edits, None, cx); - zlog::info!("Applied edits. New Content: {:?}", buffer.text()); - }, - )?; - } - - if let Some(command) = action.lsp_action.command() { - zlog::warn!( - logger => - "Executing code action command '{}'. This may cause formatting to abort unnecessarily as well as splitting formatting into two entries in the undo history", - &command.command, - ); - - // bail early if command is invalid - let server_capabilities = server.capabilities(); - let available_commands = server_capabilities - .execute_command_provider - .as_ref() - .map(|options| options.commands.as_slice()) - .unwrap_or_default(); - if !available_commands.contains(&command.command) { - zlog::warn!( - logger => - "Cannot execute a command {} not listed in the language server capabilities of server {}", - command.command, - server.name(), - ); - continue; - } - - // noop so we just ensure buffer hasn't been edited since resolving code actions - extend_formatting_transaction( - buffer, - formatting_transaction_id, - cx, - |_, _| {}, - )?; - zlog::info!(logger => "Executing command {}", &command.command); - - lsp_store.update(cx, |this, _| { - this.as_local_mut() - .unwrap() - .last_workspace_edits_by_language_server - .remove(&server.server_id()); - })?; - - let execute_command_result = server - .request::( - lsp::ExecuteCommandParams { - command: command.command.clone(), - arguments: command.arguments.clone().unwrap_or_default(), - ..Default::default() - }, - ) - .await - .into_response(); - - if execute_command_result.is_err() { - zlog::error!( - logger => - "Failed to execute command '{}' as part of {}", - &command.command, - describe_code_action(&action), - ); - continue 'actions; - } - - let mut project_transaction_command = - lsp_store.update(cx, |this, _| { - this.as_local_mut() - .unwrap() - .last_workspace_edits_by_language_server - .remove(&server.server_id()) - .unwrap_or_default() - })?; - - if let Some(transaction) = - project_transaction_command.0.remove(&buffer.handle) - { - zlog::trace!( - logger => - "Successfully captured {} edits that resulted from command {}", - transaction.edit_ids.len(), - &command.command, - ); - let transaction_id_project_transaction = transaction.id; - buffer.handle.update(cx, |buffer, _| { - // it may have been removed from history if push_to_history was - // false in deserialize_workspace_edit. If so push it so we - // can merge it with the format transaction - // and pop the combined transaction off the history stack - // later if push_to_history is false - if buffer.get_transaction(transaction.id).is_none() { - buffer.push_transaction(transaction, Instant::now()); - } - buffer.merge_transactions( - transaction_id_project_transaction, - formatting_transaction_id, - ); - })?; - } - - if !project_transaction_command.0.is_empty() { - let mut extra_buffers = String::new(); - for buffer in project_transaction_command.0.keys() { - buffer - .read_with(cx, |b, cx| { - if let Some(path) = b.project_path(cx) { - if !extra_buffers.is_empty() { - extra_buffers.push_str(", "); - } - extra_buffers.push_str(path.path.as_unix_str()); - } - }) - .ok(); - } - zlog::warn!( - logger => - "Unexpected edits to buffers other than the buffer actively being formatted due to command {}. Impacted buffers: [{}].", - &command.command, - extra_buffers, - ); - // NOTE: if this case is hit, the proper thing to do is to for each buffer, merge the extra transaction - // into the existing transaction in project_transaction if there is one, and if there isn't one in project_transaction, - // add it so it's included, and merge it into the format transaction when its created later - } - } - } - } - } - } - - Ok(()) - } - - pub async fn format_ranges_via_lsp( - this: &WeakEntity, - buffer_handle: &Entity, - ranges: &[Range], - abs_path: &Path, - language_server: &Arc, - settings: &LanguageSettings, - cx: &mut AsyncApp, - ) -> Result, Arc)>> { - let capabilities = &language_server.capabilities(); - let range_formatting_provider = capabilities.document_range_formatting_provider.as_ref(); - if range_formatting_provider == Some(&OneOf::Left(false)) { - anyhow::bail!( - "{} language server does not support range formatting", - language_server.name() - ); - } - - let uri = file_path_to_lsp_url(abs_path)?; - let text_document = lsp::TextDocumentIdentifier::new(uri); - - let lsp_edits = { - let mut lsp_ranges = Vec::new(); - this.update(cx, |_this, cx| { - // TODO(#22930): In the case of formatting multibuffer selections, this buffer may - // not have been sent to the language server. This seems like a fairly systemic - // issue, though, the resolution probably is not specific to formatting. - // - // TODO: Instead of using current snapshot, should use the latest snapshot sent to - // LSP. - let snapshot = buffer_handle.read(cx).snapshot(); - for range in ranges { - lsp_ranges.push(range_to_lsp(range.to_point_utf16(&snapshot))?); - } - anyhow::Ok(()) - })??; - - let mut edits = None; - for range in lsp_ranges { - if let Some(mut edit) = language_server - .request::(lsp::DocumentRangeFormattingParams { - text_document: text_document.clone(), - range, - options: lsp_command::lsp_formatting_options(settings), - work_done_progress_params: Default::default(), - }) - .await - .into_response()? - { - edits.get_or_insert_with(Vec::new).append(&mut edit); - } - } - edits - }; - - if let Some(lsp_edits) = lsp_edits { - this.update(cx, |this, cx| { - this.as_local_mut().unwrap().edits_from_lsp( - buffer_handle, - lsp_edits, - language_server.server_id(), - None, - cx, - ) - })? - .await - } else { - Ok(Vec::with_capacity(0)) - } - } - - async fn format_via_lsp( - this: &WeakEntity, - buffer: &Entity, - abs_path: &Path, - language_server: &Arc, - settings: &LanguageSettings, - cx: &mut AsyncApp, - ) -> Result, Arc)>> { - let logger = zlog::scoped!("lsp_format"); - zlog::debug!(logger => "Formatting via LSP"); - - let uri = file_path_to_lsp_url(abs_path)?; - let text_document = lsp::TextDocumentIdentifier::new(uri); - let capabilities = &language_server.capabilities(); - - let formatting_provider = capabilities.document_formatting_provider.as_ref(); - let range_formatting_provider = capabilities.document_range_formatting_provider.as_ref(); - - let lsp_edits = if matches!(formatting_provider, Some(p) if *p != OneOf::Left(false)) { - let _timer = zlog::time!(logger => "format-full"); - language_server - .request::(lsp::DocumentFormattingParams { - text_document, - options: lsp_command::lsp_formatting_options(settings), - work_done_progress_params: Default::default(), - }) - .await - .into_response()? - } else if matches!(range_formatting_provider, Some(p) if *p != OneOf::Left(false)) { - let _timer = zlog::time!(logger => "format-range"); - let buffer_start = lsp::Position::new(0, 0); - let buffer_end = buffer.read_with(cx, |b, _| point_to_lsp(b.max_point_utf16()))?; - language_server - .request::(lsp::DocumentRangeFormattingParams { - text_document: text_document.clone(), - range: lsp::Range::new(buffer_start, buffer_end), - options: lsp_command::lsp_formatting_options(settings), - work_done_progress_params: Default::default(), - }) - .await - .into_response()? - } else { - None - }; - - if let Some(lsp_edits) = lsp_edits { - this.update(cx, |this, cx| { - this.as_local_mut().unwrap().edits_from_lsp( - buffer, - lsp_edits, - language_server.server_id(), - None, - cx, - ) - })? - .await - } else { - Ok(Vec::with_capacity(0)) - } - } - - async fn format_via_external_command( - buffer: &FormattableBuffer, - command: &str, - arguments: Option<&[String]>, - cx: &mut AsyncApp, - ) -> Result> { - let working_dir_path = buffer.handle.update(cx, |buffer, cx| { - let file = File::from_dyn(buffer.file())?; - let worktree = file.worktree.read(cx); - let mut worktree_path = worktree.abs_path().to_path_buf(); - if worktree.root_entry()?.is_file() { - worktree_path.pop(); - } - Some(worktree_path) - })?; - - let mut child = util::command::new_smol_command(command); - - if let Some(buffer_env) = buffer.env.as_ref() { - child.envs(buffer_env); - } - - if let Some(working_dir_path) = working_dir_path { - child.current_dir(working_dir_path); - } - - if let Some(arguments) = arguments { - child.args(arguments.iter().map(|arg| { - if let Some(buffer_abs_path) = buffer.abs_path.as_ref() { - arg.replace("{buffer_path}", &buffer_abs_path.to_string_lossy()) - } else { - arg.replace("{buffer_path}", "Untitled") - } - })); - } - - let mut child = child - .stdin(smol::process::Stdio::piped()) - .stdout(smol::process::Stdio::piped()) - .stderr(smol::process::Stdio::piped()) - .spawn()?; - - let stdin = child.stdin.as_mut().context("failed to acquire stdin")?; - let text = buffer - .handle - .read_with(cx, |buffer, _| buffer.as_rope().clone())?; - for chunk in text.chunks() { - stdin.write_all(chunk.as_bytes()).await?; - } - stdin.flush().await?; - - let output = child.output().await?; - anyhow::ensure!( - output.status.success(), - "command failed with exit code {:?}:\nstdout: {}\nstderr: {}", - output.status.code(), - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), - ); - - let stdout = String::from_utf8(output.stdout)?; - Ok(Some( - buffer - .handle - .update(cx, |buffer, cx| buffer.diff(stdout, cx))? - .await, - )) - } - - async fn try_resolve_code_action( - lang_server: &LanguageServer, - action: &mut CodeAction, - ) -> anyhow::Result<()> { - match &mut action.lsp_action { - LspAction::Action(lsp_action) => { - if !action.resolved - && GetCodeActions::can_resolve_actions(&lang_server.capabilities()) - && lsp_action.data.is_some() - && (lsp_action.command.is_none() || lsp_action.edit.is_none()) - { - *lsp_action = Box::new( - lang_server - .request::(*lsp_action.clone()) - .await - .into_response()?, - ); - } - } - LspAction::CodeLens(lens) => { - if !action.resolved && GetCodeLens::can_resolve_lens(&lang_server.capabilities()) { - *lens = lang_server - .request::(lens.clone()) - .await - .into_response()?; - } - } - LspAction::Command(_) => {} - } - - action.resolved = true; - anyhow::Ok(()) - } - - fn initialize_buffer(&mut self, buffer_handle: &Entity, cx: &mut Context) { - let buffer = buffer_handle.read(cx); - - let file = buffer.file().cloned(); - - let Some(file) = File::from_dyn(file.as_ref()) else { - return; - }; - if !file.is_local() { - return; - } - let path = ProjectPath::from_file(file, cx); - let worktree_id = file.worktree_id(cx); - let language = buffer.language().cloned(); - - if let Some(diagnostics) = self.diagnostics.get(&worktree_id) { - for (server_id, diagnostics) in - diagnostics.get(file.path()).cloned().unwrap_or_default() - { - self.update_buffer_diagnostics( - buffer_handle, - server_id, - None, - None, - None, - Vec::new(), - diagnostics, - cx, - ) - .log_err(); - } - } - let Some(language) = language else { - return; - }; - let Some(snapshot) = self - .worktree_store - .read(cx) - .worktree_for_id(worktree_id, cx) - .map(|worktree| worktree.read(cx).snapshot()) - else { - return; - }; - let delegate: Arc = Arc::new(ManifestQueryDelegate::new(snapshot)); - - for server_id in - self.lsp_tree - .get(path, language.name(), language.manifest(), &delegate, cx) - { - let server = self - .language_servers - .get(&server_id) - .and_then(|server_state| { - if let LanguageServerState::Running { server, .. } = server_state { - Some(server.clone()) - } else { - None - } - }); - let server = match server { - Some(server) => server, - None => continue, - }; - - buffer_handle.update(cx, |buffer, cx| { - buffer.set_completion_triggers( - server.server_id(), - server - .capabilities() - .completion_provider - .as_ref() - .and_then(|provider| { - provider - .trigger_characters - .as_ref() - .map(|characters| characters.iter().cloned().collect()) - }) - .unwrap_or_default(), - cx, - ); - }); - } - } - - pub(crate) fn reset_buffer(&mut self, buffer: &Entity, old_file: &File, cx: &mut App) { - buffer.update(cx, |buffer, cx| { - let Some(language) = buffer.language() else { - return; - }; - let path = ProjectPath { - worktree_id: old_file.worktree_id(cx), - path: old_file.path.clone(), - }; - for server_id in self.language_server_ids_for_project_path(path, language, cx) { - buffer.update_diagnostics(server_id, DiagnosticSet::new([], buffer), cx); - buffer.set_completion_triggers(server_id, Default::default(), cx); - } - }); - } - - fn update_buffer_diagnostics( - &mut self, - buffer: &Entity, - server_id: LanguageServerId, - registration_id: Option>, - result_id: Option, - version: Option, - new_diagnostics: Vec>>, - reused_diagnostics: Vec>>, - cx: &mut Context, - ) -> Result<()> { - fn compare_diagnostics(a: &Diagnostic, b: &Diagnostic) -> Ordering { - Ordering::Equal - .then_with(|| b.is_primary.cmp(&a.is_primary)) - .then_with(|| a.is_disk_based.cmp(&b.is_disk_based)) - .then_with(|| a.severity.cmp(&b.severity)) - .then_with(|| a.message.cmp(&b.message)) - } - - let mut diagnostics = Vec::with_capacity(new_diagnostics.len() + reused_diagnostics.len()); - diagnostics.extend(new_diagnostics.into_iter().map(|d| (true, d))); - diagnostics.extend(reused_diagnostics.into_iter().map(|d| (false, d))); - - diagnostics.sort_unstable_by(|(_, a), (_, b)| { - Ordering::Equal - .then_with(|| a.range.start.cmp(&b.range.start)) - .then_with(|| b.range.end.cmp(&a.range.end)) - .then_with(|| compare_diagnostics(&a.diagnostic, &b.diagnostic)) - }); - - let snapshot = self.buffer_snapshot_for_lsp_version(buffer, server_id, version, cx)?; - - let edits_since_save = std::cell::LazyCell::new(|| { - let saved_version = buffer.read(cx).saved_version(); - Patch::new(snapshot.edits_since::(saved_version).collect()) - }); - - let mut sanitized_diagnostics = Vec::with_capacity(diagnostics.len()); - - for (new_diagnostic, entry) in diagnostics { - let start; - let end; - if new_diagnostic && entry.diagnostic.is_disk_based { - // Some diagnostics are based on files on disk instead of buffers' - // current contents. Adjust these diagnostics' ranges to reflect - // any unsaved edits. - // Do not alter the reused ones though, as their coordinates were stored as anchors - // and were properly adjusted on reuse. - start = Unclipped((*edits_since_save).old_to_new(entry.range.start.0)); - end = Unclipped((*edits_since_save).old_to_new(entry.range.end.0)); - } else { - start = entry.range.start; - end = entry.range.end; - } - - let mut range = snapshot.clip_point_utf16(start, Bias::Left) - ..snapshot.clip_point_utf16(end, Bias::Right); - - // Expand empty ranges by one codepoint - if range.start == range.end { - // This will be go to the next boundary when being clipped - range.end.column += 1; - range.end = snapshot.clip_point_utf16(Unclipped(range.end), Bias::Right); - if range.start == range.end && range.end.column > 0 { - range.start.column -= 1; - range.start = snapshot.clip_point_utf16(Unclipped(range.start), Bias::Left); - } - } - - sanitized_diagnostics.push(DiagnosticEntry { - range, - diagnostic: entry.diagnostic, - }); - } - drop(edits_since_save); - - let set = DiagnosticSet::new(sanitized_diagnostics, &snapshot); - buffer.update(cx, |buffer, cx| { - if let Some(registration_id) = registration_id { - if let Some(abs_path) = File::from_dyn(buffer.file()).map(|f| f.abs_path(cx)) { - self.buffer_pull_diagnostics_result_ids - .entry(server_id) - .or_default() - .entry(registration_id) - .or_default() - .insert(abs_path, result_id); - } - } - - buffer.update_diagnostics(server_id, set, cx) - }); - - Ok(()) - } - - fn register_language_server_for_invisible_worktree( - &mut self, - worktree: &Entity, - language_server_id: LanguageServerId, - cx: &mut App, - ) { - let worktree = worktree.read(cx); - let worktree_id = worktree.id(); - debug_assert!(!worktree.is_visible()); - let Some(mut origin_seed) = self - .language_server_ids - .iter() - .find_map(|(seed, state)| (state.id == language_server_id).then(|| seed.clone())) - else { - return; - }; - origin_seed.worktree_id = worktree_id; - self.language_server_ids - .entry(origin_seed) - .or_insert_with(|| UnifiedLanguageServer { - id: language_server_id, - project_roots: Default::default(), - }); - } - - fn register_buffer_with_language_servers( - &mut self, - buffer_handle: &Entity, - only_register_servers: HashSet, - cx: &mut Context, - ) { - let buffer = buffer_handle.read(cx); - let buffer_id = buffer.remote_id(); - - let Some(file) = File::from_dyn(buffer.file()) else { - return; - }; - if !file.is_local() { - return; - } - - let abs_path = file.abs_path(cx); - let Some(uri) = file_path_to_lsp_url(&abs_path).log_err() else { - return; - }; - let initial_snapshot = buffer.text_snapshot(); - let worktree_id = file.worktree_id(cx); - - let Some(language) = buffer.language().cloned() else { - return; - }; - let path: Arc = file - .path() - .parent() - .map(Arc::from) - .unwrap_or_else(|| file.path().clone()); - let Some(worktree) = self - .worktree_store - .read(cx) - .worktree_for_id(worktree_id, cx) - else { - return; - }; - let language_name = language.name(); - let (reused, delegate, servers) = self - .reuse_existing_language_server(&self.lsp_tree, &worktree, &language_name, cx) - .map(|(delegate, apply)| (true, delegate, apply(&mut self.lsp_tree))) - .unwrap_or_else(|| { - let lsp_delegate = LocalLspAdapterDelegate::from_local_lsp(self, &worktree, cx); - let delegate: Arc = - Arc::new(ManifestQueryDelegate::new(worktree.read(cx).snapshot())); - - let servers = self - .lsp_tree - .walk( - ProjectPath { worktree_id, path }, - language.name(), - language.manifest(), - &delegate, - cx, - ) - .collect::>(); - (false, lsp_delegate, servers) - }); - let servers_and_adapters = servers - .into_iter() - .filter_map(|server_node| { - if reused && server_node.server_id().is_none() { - return None; - } - if !only_register_servers.is_empty() { - if let Some(server_id) = server_node.server_id() - && !only_register_servers.contains(&LanguageServerSelector::Id(server_id)) - { - return None; - } - if let Some(name) = server_node.name() - && !only_register_servers.contains(&LanguageServerSelector::Name(name)) - { - return None; - } - } - - let server_id = server_node.server_id_or_init(|disposition| { - let path = &disposition.path; - - { - let uri = Uri::from_file_path(worktree.read(cx).absolutize(&path.path)); - - let server_id = self.get_or_insert_language_server( - &worktree, - delegate.clone(), - disposition, - &language_name, - cx, - ); - - if let Some(state) = self.language_servers.get(&server_id) - && let Ok(uri) = uri - { - state.add_workspace_folder(uri); - }; - server_id - } - })?; - let server_state = self.language_servers.get(&server_id)?; - if let LanguageServerState::Running { - server, adapter, .. - } = server_state - { - Some((server.clone(), adapter.clone())) - } else { - None - } - }) - .collect::>(); - for (server, adapter) in servers_and_adapters { - buffer_handle.update(cx, |buffer, cx| { - buffer.set_completion_triggers( - server.server_id(), - server - .capabilities() - .completion_provider - .as_ref() - .and_then(|provider| { - provider - .trigger_characters - .as_ref() - .map(|characters| characters.iter().cloned().collect()) - }) - .unwrap_or_default(), - cx, - ); - }); - - let snapshot = LspBufferSnapshot { - version: 0, - snapshot: initial_snapshot.clone(), - }; - - let mut registered = false; - self.buffer_snapshots - .entry(buffer_id) - .or_default() - .entry(server.server_id()) - .or_insert_with(|| { - registered = true; - server.register_buffer( - uri.clone(), - adapter.language_id(&language.name()), - 0, - initial_snapshot.text(), - ); - - vec![snapshot] - }); - - self.buffers_opened_in_servers - .entry(buffer_id) - .or_default() - .insert(server.server_id()); - if registered { - cx.emit(LspStoreEvent::LanguageServerUpdate { - language_server_id: server.server_id(), - name: None, - message: proto::update_language_server::Variant::RegisteredForBuffer( - proto::RegisteredForBuffer { - buffer_abs_path: abs_path.to_string_lossy().into_owned(), - buffer_id: buffer_id.to_proto(), - }, - ), - }); - } - } - } - - fn reuse_existing_language_server<'lang_name>( - &self, - server_tree: &LanguageServerTree, - worktree: &Entity, - language_name: &'lang_name LanguageName, - cx: &mut App, - ) -> Option<( - Arc, - impl FnOnce(&mut LanguageServerTree) -> Vec + use<'lang_name>, - )> { - if worktree.read(cx).is_visible() { - return None; - } - - let worktree_store = self.worktree_store.read(cx); - let servers = server_tree - .instances - .iter() - .filter(|(worktree_id, _)| { - worktree_store - .worktree_for_id(**worktree_id, cx) - .is_some_and(|worktree| worktree.read(cx).is_visible()) - }) - .flat_map(|(worktree_id, servers)| { - servers - .roots - .iter() - .flat_map(|(_, language_servers)| language_servers) - .map(move |(_, (server_node, server_languages))| { - (worktree_id, server_node, server_languages) - }) - .filter(|(_, _, server_languages)| server_languages.contains(language_name)) - .map(|(worktree_id, server_node, _)| { - ( - *worktree_id, - LanguageServerTreeNode::from(Arc::downgrade(server_node)), - ) - }) - }) - .fold(HashMap::default(), |mut acc, (worktree_id, server_node)| { - acc.entry(worktree_id) - .or_insert_with(Vec::new) - .push(server_node); - acc - }) - .into_values() - .max_by_key(|servers| servers.len())?; - - let worktree_id = worktree.read(cx).id(); - let apply = move |tree: &mut LanguageServerTree| { - for server_node in &servers { - tree.register_reused(worktree_id, language_name.clone(), server_node.clone()); - } - servers - }; - - let delegate = LocalLspAdapterDelegate::from_local_lsp(self, worktree, cx); - Some((delegate, apply)) - } - - pub(crate) fn unregister_old_buffer_from_language_servers( - &mut self, - buffer: &Entity, - old_file: &File, - cx: &mut App, - ) { - let old_path = match old_file.as_local() { - Some(local) => local.abs_path(cx), - None => return, - }; - - let Ok(file_url) = lsp::Uri::from_file_path(old_path.as_path()) else { - debug_panic!("{old_path:?} is not parseable as an URI"); - return; - }; - self.unregister_buffer_from_language_servers(buffer, &file_url, cx); - } - - pub(crate) fn unregister_buffer_from_language_servers( - &mut self, - buffer: &Entity, - file_url: &lsp::Uri, - cx: &mut App, - ) { - buffer.update(cx, |buffer, cx| { - let mut snapshots = self.buffer_snapshots.remove(&buffer.remote_id()); - - for (_, language_server) in self.language_servers_for_buffer(buffer, cx) { - if snapshots - .as_mut() - .is_some_and(|map| map.remove(&language_server.server_id()).is_some()) - { - language_server.unregister_buffer(file_url.clone()); - } - } - }); - } - - fn buffer_snapshot_for_lsp_version( - &mut self, - buffer: &Entity, - server_id: LanguageServerId, - version: Option, - cx: &App, - ) -> Result { - const OLD_VERSIONS_TO_RETAIN: i32 = 10; - - if let Some(version) = version { - let buffer_id = buffer.read(cx).remote_id(); - let snapshots = if let Some(snapshots) = self - .buffer_snapshots - .get_mut(&buffer_id) - .and_then(|m| m.get_mut(&server_id)) - { - snapshots - } else if version == 0 { - // Some language servers report version 0 even if the buffer hasn't been opened yet. - // We detect this case and treat it as if the version was `None`. - return Ok(buffer.read(cx).text_snapshot()); - } else { - anyhow::bail!("no snapshots found for buffer {buffer_id} and server {server_id}"); - }; - - let found_snapshot = snapshots - .binary_search_by_key(&version, |e| e.version) - .map(|ix| snapshots[ix].snapshot.clone()) - .map_err(|_| { - anyhow!("snapshot not found for buffer {buffer_id} server {server_id} at version {version}") - })?; - - snapshots.retain(|snapshot| snapshot.version + OLD_VERSIONS_TO_RETAIN >= version); - Ok(found_snapshot) - } else { - Ok((buffer.read(cx)).text_snapshot()) - } - } - - async fn get_server_code_actions_from_action_kinds( - lsp_store: &WeakEntity, - language_server_id: LanguageServerId, - code_action_kinds: Vec, - buffer: &Entity, - cx: &mut AsyncApp, - ) -> Result> { - let actions = lsp_store - .update(cx, move |this, cx| { - let request = GetCodeActions { - range: text::Anchor::min_max_range_for_buffer(buffer.read(cx).remote_id()), - kinds: Some(code_action_kinds), - }; - let server = LanguageServerToQuery::Other(language_server_id); - this.request_lsp(buffer.clone(), server, request, cx) - })? - .await?; - Ok(actions) - } - - pub async fn execute_code_actions_on_server( - lsp_store: &WeakEntity, - language_server: &Arc, - - actions: Vec, - push_to_history: bool, - project_transaction: &mut ProjectTransaction, - cx: &mut AsyncApp, - ) -> anyhow::Result<()> { - for mut action in actions { - Self::try_resolve_code_action(language_server, &mut action) - .await - .context("resolving a formatting code action")?; - - if let Some(edit) = action.lsp_action.edit() { - if edit.changes.is_none() && edit.document_changes.is_none() { - continue; - } - - let new = Self::deserialize_workspace_edit( - lsp_store.upgrade().context("project dropped")?, - edit.clone(), - push_to_history, - language_server.clone(), - cx, - ) - .await?; - project_transaction.0.extend(new.0); - } - - if let Some(command) = action.lsp_action.command() { - let server_capabilities = language_server.capabilities(); - let available_commands = server_capabilities - .execute_command_provider - .as_ref() - .map(|options| options.commands.as_slice()) - .unwrap_or_default(); - if available_commands.contains(&command.command) { - lsp_store.update(cx, |lsp_store, _| { - if let LspStoreMode::Local(mode) = &mut lsp_store.mode { - mode.last_workspace_edits_by_language_server - .remove(&language_server.server_id()); - } - })?; - - language_server - .request::(lsp::ExecuteCommandParams { - command: command.command.clone(), - arguments: command.arguments.clone().unwrap_or_default(), - ..Default::default() - }) - .await - .into_response() - .context("execute command")?; - - lsp_store.update(cx, |this, _| { - if let LspStoreMode::Local(mode) = &mut this.mode { - project_transaction.0.extend( - mode.last_workspace_edits_by_language_server - .remove(&language_server.server_id()) - .unwrap_or_default() - .0, - ) - } - })?; - } else { - log::warn!( - "Cannot execute a command {} not listed in the language server capabilities", - command.command - ) - } - } - } - Ok(()) - } - - pub async fn deserialize_text_edits( - this: Entity, - buffer_to_edit: Entity, - edits: Vec, - push_to_history: bool, - _: Arc, - language_server: Arc, - cx: &mut AsyncApp, - ) -> Result> { - let edits = this - .update(cx, |this, cx| { - this.as_local_mut().unwrap().edits_from_lsp( - &buffer_to_edit, - edits, - language_server.server_id(), - None, - cx, - ) - })? - .await?; - - let transaction = buffer_to_edit.update(cx, |buffer, cx| { - buffer.finalize_last_transaction(); - buffer.start_transaction(); - for (range, text) in edits { - buffer.edit([(range, text)], None, cx); - } - - if buffer.end_transaction(cx).is_some() { - let transaction = buffer.finalize_last_transaction().unwrap().clone(); - if !push_to_history { - buffer.forget_transaction(transaction.id); - } - Some(transaction) - } else { - None - } - })?; - - Ok(transaction) - } - - #[allow(clippy::type_complexity)] - pub(crate) fn edits_from_lsp( - &mut self, - buffer: &Entity, - lsp_edits: impl 'static + Send + IntoIterator, - server_id: LanguageServerId, - version: Option, - cx: &mut Context, - ) -> Task, Arc)>>> { - let snapshot = self.buffer_snapshot_for_lsp_version(buffer, server_id, version, cx); - cx.background_spawn(async move { - let snapshot = snapshot?; - let mut lsp_edits = lsp_edits - .into_iter() - .map(|edit| (range_from_lsp(edit.range), edit.new_text)) - .collect::>(); - - lsp_edits.sort_by_key(|(range, _)| (range.start, range.end)); - - let mut lsp_edits = lsp_edits.into_iter().peekable(); - let mut edits = Vec::new(); - while let Some((range, mut new_text)) = lsp_edits.next() { - // Clip invalid ranges provided by the language server. - let mut range = snapshot.clip_point_utf16(range.start, Bias::Left) - ..snapshot.clip_point_utf16(range.end, Bias::Left); - - // Combine any LSP edits that are adjacent. - // - // Also, combine LSP edits that are separated from each other by only - // a newline. This is important because for some code actions, - // Rust-analyzer rewrites the entire buffer via a series of edits that - // are separated by unchanged newline characters. - // - // In order for the diffing logic below to work properly, any edits that - // cancel each other out must be combined into one. - while let Some((next_range, next_text)) = lsp_edits.peek() { - if next_range.start.0 > range.end { - if next_range.start.0.row > range.end.row + 1 - || next_range.start.0.column > 0 - || snapshot.clip_point_utf16( - Unclipped(PointUtf16::new(range.end.row, u32::MAX)), - Bias::Left, - ) > range.end - { - break; - } - new_text.push('\n'); - } - range.end = snapshot.clip_point_utf16(next_range.end, Bias::Left); - new_text.push_str(next_text); - lsp_edits.next(); - } - - // For multiline edits, perform a diff of the old and new text so that - // we can identify the changes more precisely, preserving the locations - // of any anchors positioned in the unchanged regions. - if range.end.row > range.start.row { - let offset = range.start.to_offset(&snapshot); - let old_text = snapshot.text_for_range(range).collect::(); - let range_edits = language::text_diff(old_text.as_str(), &new_text); - edits.extend(range_edits.into_iter().map(|(range, replacement)| { - ( - snapshot.anchor_after(offset + range.start) - ..snapshot.anchor_before(offset + range.end), - replacement, - ) - })); - } else if range.end == range.start { - let anchor = snapshot.anchor_after(range.start); - edits.push((anchor..anchor, new_text.into())); - } else { - let edit_start = snapshot.anchor_after(range.start); - let edit_end = snapshot.anchor_before(range.end); - edits.push((edit_start..edit_end, new_text.into())); - } - } - - Ok(edits) - }) - } - - pub(crate) async fn deserialize_workspace_edit( - this: Entity, - edit: lsp::WorkspaceEdit, - push_to_history: bool, - language_server: Arc, - cx: &mut AsyncApp, - ) -> Result { - let fs = this.read_with(cx, |this, _| this.as_local().unwrap().fs.clone())?; - - let mut operations = Vec::new(); - if let Some(document_changes) = edit.document_changes { - match document_changes { - lsp::DocumentChanges::Edits(edits) => { - operations.extend(edits.into_iter().map(lsp::DocumentChangeOperation::Edit)) - } - lsp::DocumentChanges::Operations(ops) => operations = ops, - } - } else if let Some(changes) = edit.changes { - operations.extend(changes.into_iter().map(|(uri, edits)| { - lsp::DocumentChangeOperation::Edit(lsp::TextDocumentEdit { - text_document: lsp::OptionalVersionedTextDocumentIdentifier { - uri, - version: None, - }, - edits: edits.into_iter().map(Edit::Plain).collect(), - }) - })); - } - - let mut project_transaction = ProjectTransaction::default(); - for operation in operations { - match operation { - lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Create(op)) => { - let abs_path = op - .uri - .to_file_path() - .map_err(|()| anyhow!("can't convert URI to path"))?; - - if let Some(parent_path) = abs_path.parent() { - fs.create_dir(parent_path).await?; - } - if abs_path.ends_with("/") { - fs.create_dir(&abs_path).await?; - } else { - fs.create_file( - &abs_path, - op.options - .map(|options| fs::CreateOptions { - overwrite: options.overwrite.unwrap_or(false), - ignore_if_exists: options.ignore_if_exists.unwrap_or(false), - }) - .unwrap_or_default(), - ) - .await?; - } - } - - lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Rename(op)) => { - let source_abs_path = op - .old_uri - .to_file_path() - .map_err(|()| anyhow!("can't convert URI to path"))?; - let target_abs_path = op - .new_uri - .to_file_path() - .map_err(|()| anyhow!("can't convert URI to path"))?; - - let options = fs::RenameOptions { - overwrite: op - .options - .as_ref() - .and_then(|options| options.overwrite) - .unwrap_or(false), - ignore_if_exists: op - .options - .as_ref() - .and_then(|options| options.ignore_if_exists) - .unwrap_or(false), - create_parents: true, - }; - - fs.rename(&source_abs_path, &target_abs_path, options) - .await?; - } - - lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Delete(op)) => { - let abs_path = op - .uri - .to_file_path() - .map_err(|()| anyhow!("can't convert URI to path"))?; - let options = op - .options - .map(|options| fs::RemoveOptions { - recursive: options.recursive.unwrap_or(false), - ignore_if_not_exists: options.ignore_if_not_exists.unwrap_or(false), - }) - .unwrap_or_default(); - if abs_path.ends_with("/") { - fs.remove_dir(&abs_path, options).await?; - } else { - fs.remove_file(&abs_path, options).await?; - } - } - - lsp::DocumentChangeOperation::Edit(op) => { - let buffer_to_edit = this - .update(cx, |this, cx| { - this.open_local_buffer_via_lsp( - op.text_document.uri.clone(), - language_server.server_id(), - cx, - ) - })? - .await?; - - let edits = this - .update(cx, |this, cx| { - let path = buffer_to_edit.read(cx).project_path(cx); - let active_entry = this.active_entry; - let is_active_entry = path.is_some_and(|project_path| { - this.worktree_store - .read(cx) - .entry_for_path(&project_path, cx) - .is_some_and(|entry| Some(entry.id) == active_entry) - }); - let local = this.as_local_mut().unwrap(); - - let (mut edits, mut snippet_edits) = (vec![], vec![]); - for edit in op.edits { - match edit { - Edit::Plain(edit) => { - if !edits.contains(&edit) { - edits.push(edit) - } - } - Edit::Annotated(edit) => { - if !edits.contains(&edit.text_edit) { - edits.push(edit.text_edit) - } - } - Edit::Snippet(edit) => { - let Ok(snippet) = Snippet::parse(&edit.snippet.value) - else { - continue; - }; - - if is_active_entry { - snippet_edits.push((edit.range, snippet)); - } else { - // Since this buffer is not focused, apply a normal edit. - let new_edit = TextEdit { - range: edit.range, - new_text: snippet.text, - }; - if !edits.contains(&new_edit) { - edits.push(new_edit); - } - } - } - } - } - if !snippet_edits.is_empty() { - let buffer_id = buffer_to_edit.read(cx).remote_id(); - let version = if let Some(buffer_version) = op.text_document.version - { - local - .buffer_snapshot_for_lsp_version( - &buffer_to_edit, - language_server.server_id(), - Some(buffer_version), - cx, - ) - .ok() - .map(|snapshot| snapshot.version) - } else { - Some(buffer_to_edit.read(cx).saved_version().clone()) - }; - - let most_recent_edit = - version.and_then(|version| version.most_recent()); - // Check if the edit that triggered that edit has been made by this participant. - - if let Some(most_recent_edit) = most_recent_edit { - cx.emit(LspStoreEvent::SnippetEdit { - buffer_id, - edits: snippet_edits, - most_recent_edit, - }); - } - } - - local.edits_from_lsp( - &buffer_to_edit, - edits, - language_server.server_id(), - op.text_document.version, - cx, - ) - })? - .await?; - - let transaction = buffer_to_edit.update(cx, |buffer, cx| { - buffer.finalize_last_transaction(); - buffer.start_transaction(); - for (range, text) in edits { - buffer.edit([(range, text)], None, cx); - } - - buffer.end_transaction(cx).and_then(|transaction_id| { - if push_to_history { - buffer.finalize_last_transaction(); - buffer.get_transaction(transaction_id).cloned() - } else { - buffer.forget_transaction(transaction_id) - } - }) - })?; - if let Some(transaction) = transaction { - project_transaction.0.insert(buffer_to_edit, transaction); - } - } - } - } - - Ok(project_transaction) - } - - async fn on_lsp_workspace_edit( - this: WeakEntity, - params: lsp::ApplyWorkspaceEditParams, - server_id: LanguageServerId, - cx: &mut AsyncApp, - ) -> Result { - let this = this.upgrade().context("project project closed")?; - let language_server = this - .read_with(cx, |this, _| this.language_server_for_id(server_id))? - .context("language server not found")?; - let transaction = Self::deserialize_workspace_edit( - this.clone(), - params.edit, - true, - language_server.clone(), - cx, - ) - .await - .log_err(); - this.update(cx, |this, _| { - if let Some(transaction) = transaction { - this.as_local_mut() - .unwrap() - .last_workspace_edits_by_language_server - .insert(server_id, transaction); - } - })?; - Ok(lsp::ApplyWorkspaceEditResponse { - applied: true, - failed_change: None, - failure_reason: None, - }) - } - - fn remove_worktree( - &mut self, - id_to_remove: WorktreeId, - cx: &mut Context, - ) -> Vec { - self.diagnostics.remove(&id_to_remove); - self.prettier_store.update(cx, |prettier_store, cx| { - prettier_store.remove_worktree(id_to_remove, cx); - }); - - let mut servers_to_remove = BTreeSet::default(); - let mut servers_to_preserve = HashSet::default(); - for (seed, state) in &self.language_server_ids { - if seed.worktree_id == id_to_remove { - servers_to_remove.insert(state.id); - } else { - servers_to_preserve.insert(state.id); - } - } - servers_to_remove.retain(|server_id| !servers_to_preserve.contains(server_id)); - self.language_server_ids - .retain(|_, state| !servers_to_remove.contains(&state.id)); - for server_id_to_remove in &servers_to_remove { - self.language_server_watched_paths - .remove(server_id_to_remove); - self.language_server_paths_watched_for_rename - .remove(server_id_to_remove); - self.last_workspace_edits_by_language_server - .remove(server_id_to_remove); - self.language_servers.remove(server_id_to_remove); - self.buffer_pull_diagnostics_result_ids - .remove(server_id_to_remove); - self.workspace_pull_diagnostics_result_ids - .remove(server_id_to_remove); - for buffer_servers in self.buffers_opened_in_servers.values_mut() { - buffer_servers.remove(server_id_to_remove); - } - cx.emit(LspStoreEvent::LanguageServerRemoved(*server_id_to_remove)); - } - servers_to_remove.into_iter().collect() - } - - fn rebuild_watched_paths_inner<'a>( - &'a self, - language_server_id: LanguageServerId, - watchers: impl Iterator, - cx: &mut Context, - ) -> LanguageServerWatchedPathsBuilder { - let worktrees = self - .worktree_store - .read(cx) - .worktrees() - .filter_map(|worktree| { - self.language_servers_for_worktree(worktree.read(cx).id()) - .find(|server| server.server_id() == language_server_id) - .map(|_| worktree) - }) - .collect::>(); - - let mut worktree_globs = HashMap::default(); - let mut abs_globs = HashMap::default(); - log::trace!( - "Processing new watcher paths for language server with id {}", - language_server_id - ); - - for watcher in watchers { - if let Some((worktree, literal_prefix, pattern)) = - Self::worktree_and_path_for_file_watcher(&worktrees, watcher, cx) - { - worktree.update(cx, |worktree, _| { - if let Some((tree, glob)) = - worktree.as_local_mut().zip(Glob::new(&pattern).log_err()) - { - tree.add_path_prefix_to_scan(literal_prefix); - worktree_globs - .entry(tree.id()) - .or_insert_with(GlobSetBuilder::new) - .add(glob); - } - }); - } else { - let (path, pattern) = match &watcher.glob_pattern { - lsp::GlobPattern::String(s) => { - let watcher_path = SanitizedPath::new(s); - let path = glob_literal_prefix(watcher_path.as_path()); - let pattern = watcher_path - .as_path() - .strip_prefix(&path) - .map(|p| p.to_string_lossy().into_owned()) - .unwrap_or_else(|e| { - debug_panic!( - "Failed to strip prefix for string pattern: {}, with prefix: {}, with error: {}", - s, - path.display(), - e - ); - watcher_path.as_path().to_string_lossy().into_owned() - }); - (path, pattern) - } - lsp::GlobPattern::Relative(rp) => { - let Ok(mut base_uri) = match &rp.base_uri { - lsp::OneOf::Left(workspace_folder) => &workspace_folder.uri, - lsp::OneOf::Right(base_uri) => base_uri, - } - .to_file_path() else { - continue; - }; - - let path = glob_literal_prefix(Path::new(&rp.pattern)); - let pattern = Path::new(&rp.pattern) - .strip_prefix(&path) - .map(|p| p.to_string_lossy().into_owned()) - .unwrap_or_else(|e| { - debug_panic!( - "Failed to strip prefix for relative pattern: {}, with prefix: {}, with error: {}", - rp.pattern, - path.display(), - e - ); - rp.pattern.clone() - }); - base_uri.push(path); - (base_uri, pattern) - } - }; - - if let Some(glob) = Glob::new(&pattern).log_err() { - if !path - .components() - .any(|c| matches!(c, path::Component::Normal(_))) - { - // For an unrooted glob like `**/Cargo.toml`, watch it within each worktree, - // rather than adding a new watcher for `/`. - for worktree in &worktrees { - worktree_globs - .entry(worktree.read(cx).id()) - .or_insert_with(GlobSetBuilder::new) - .add(glob.clone()); - } - } else { - abs_globs - .entry(path.into()) - .or_insert_with(GlobSetBuilder::new) - .add(glob); - } - } - } - } - - let mut watch_builder = LanguageServerWatchedPathsBuilder::default(); - for (worktree_id, builder) in worktree_globs { - if let Ok(globset) = builder.build() { - watch_builder.watch_worktree(worktree_id, globset); - } - } - for (abs_path, builder) in abs_globs { - if let Ok(globset) = builder.build() { - watch_builder.watch_abs_path(abs_path, globset); - } - } - watch_builder - } - - fn worktree_and_path_for_file_watcher( - worktrees: &[Entity], - watcher: &FileSystemWatcher, - cx: &App, - ) -> Option<(Entity, Arc, String)> { - worktrees.iter().find_map(|worktree| { - let tree = worktree.read(cx); - let worktree_root_path = tree.abs_path(); - let path_style = tree.path_style(); - match &watcher.glob_pattern { - lsp::GlobPattern::String(s) => { - let watcher_path = SanitizedPath::new(s); - let relative = watcher_path - .as_path() - .strip_prefix(&worktree_root_path) - .ok()?; - let literal_prefix = glob_literal_prefix(relative); - Some(( - worktree.clone(), - RelPath::new(&literal_prefix, path_style).ok()?.into_arc(), - relative.to_string_lossy().into_owned(), - )) - } - lsp::GlobPattern::Relative(rp) => { - let base_uri = match &rp.base_uri { - lsp::OneOf::Left(workspace_folder) => &workspace_folder.uri, - lsp::OneOf::Right(base_uri) => base_uri, - } - .to_file_path() - .ok()?; - let relative = base_uri.strip_prefix(&worktree_root_path).ok()?; - let mut literal_prefix = relative.to_owned(); - literal_prefix.push(glob_literal_prefix(Path::new(&rp.pattern))); - Some(( - worktree.clone(), - RelPath::new(&literal_prefix, path_style).ok()?.into_arc(), - rp.pattern.clone(), - )) - } - } - }) - } - - fn rebuild_watched_paths( - &mut self, - language_server_id: LanguageServerId, - cx: &mut Context, - ) { - let Some(registrations) = self - .language_server_dynamic_registrations - .get(&language_server_id) - else { - return; - }; - - let watch_builder = self.rebuild_watched_paths_inner( - language_server_id, - registrations.did_change_watched_files.values().flatten(), - cx, - ); - let watcher = watch_builder.build(self.fs.clone(), language_server_id, cx); - self.language_server_watched_paths - .insert(language_server_id, watcher); - - cx.notify(); - } - - fn on_lsp_did_change_watched_files( - &mut self, - language_server_id: LanguageServerId, - registration_id: &str, - params: DidChangeWatchedFilesRegistrationOptions, - cx: &mut Context, - ) { - let registrations = self - .language_server_dynamic_registrations - .entry(language_server_id) - .or_default(); - - registrations - .did_change_watched_files - .insert(registration_id.to_string(), params.watchers); - - self.rebuild_watched_paths(language_server_id, cx); - } - - fn on_lsp_unregister_did_change_watched_files( - &mut self, - language_server_id: LanguageServerId, - registration_id: &str, - cx: &mut Context, - ) { - let registrations = self - .language_server_dynamic_registrations - .entry(language_server_id) - .or_default(); - - if registrations - .did_change_watched_files - .remove(registration_id) - .is_some() - { - log::info!( - "language server {}: unregistered workspace/DidChangeWatchedFiles capability with id {}", - language_server_id, - registration_id - ); - } else { - log::warn!( - "language server {}: failed to unregister workspace/DidChangeWatchedFiles capability with id {}. not registered.", - language_server_id, - registration_id - ); - } - - self.rebuild_watched_paths(language_server_id, cx); - } - - async fn initialization_options_for_adapter( - adapter: Arc, - delegate: &Arc, - ) -> Result> { - let Some(mut initialization_config) = - adapter.clone().initialization_options(delegate).await? - else { - return Ok(None); - }; - - for other_adapter in delegate.registered_lsp_adapters() { - if other_adapter.name() == adapter.name() { - continue; - } - if let Ok(Some(target_config)) = other_adapter - .clone() - .additional_initialization_options(adapter.name(), delegate) - .await - { - merge_json_value_into(target_config.clone(), &mut initialization_config); - } - } - - Ok(Some(initialization_config)) - } - - async fn workspace_configuration_for_adapter( - adapter: Arc, - delegate: &Arc, - toolchain: Option, - requested_uri: Option, - cx: &mut AsyncApp, - ) -> Result { - let mut workspace_config = adapter - .clone() - .workspace_configuration(delegate, toolchain, requested_uri, cx) - .await?; - - for other_adapter in delegate.registered_lsp_adapters() { - if other_adapter.name() == adapter.name() { - continue; - } - if let Ok(Some(target_config)) = other_adapter - .clone() - .additional_workspace_configuration(adapter.name(), delegate, cx) - .await - { - merge_json_value_into(target_config.clone(), &mut workspace_config); - } - } - - Ok(workspace_config) - } - - fn language_server_for_id(&self, id: LanguageServerId) -> Option> { - if let Some(LanguageServerState::Running { server, .. }) = self.language_servers.get(&id) { - Some(server.clone()) - } else if let Some((_, server)) = self.supplementary_language_servers.get(&id) { - Some(Arc::clone(server)) - } else { - None - } - } -} - -fn notify_server_capabilities_updated(server: &LanguageServer, cx: &mut Context) { - if let Some(capabilities) = serde_json::to_string(&server.capabilities()).ok() { - cx.emit(LspStoreEvent::LanguageServerUpdate { - language_server_id: server.server_id(), - name: Some(server.name()), - message: proto::update_language_server::Variant::MetadataUpdated( - proto::ServerMetadataUpdated { - capabilities: Some(capabilities), - binary: Some(proto::LanguageServerBinaryInfo { - path: server.binary().path.to_string_lossy().into_owned(), - arguments: server - .binary() - .arguments - .iter() - .map(|arg| arg.to_string_lossy().into_owned()) - .collect(), - }), - configuration: serde_json::to_string(server.configuration()).ok(), - workspace_folders: server - .workspace_folders() - .iter() - .map(|uri| uri.to_string()) - .collect(), - }, - ), - }); - } -} - -#[derive(Debug)] -pub struct FormattableBuffer { - handle: Entity, - abs_path: Option, - env: Option>, - ranges: Option>>, -} - -pub struct RemoteLspStore { - upstream_client: Option, - upstream_project_id: u64, -} - -pub(crate) enum LspStoreMode { - Local(LocalLspStore), // ssh host and collab host - Remote(RemoteLspStore), // collab guest -} - -impl LspStoreMode { - fn is_local(&self) -> bool { - matches!(self, LspStoreMode::Local(_)) - } -} - -pub struct LspStore { - mode: LspStoreMode, - last_formatting_failure: Option, - downstream_client: Option<(AnyProtoClient, u64)>, - nonce: u128, - buffer_store: Entity, - worktree_store: Entity, - pub languages: Arc, - pub language_server_statuses: BTreeMap, - active_entry: Option, - _maintain_workspace_config: (Task>, watch::Sender<()>), - _maintain_buffer_languages: Task<()>, - diagnostic_summaries: - HashMap, HashMap>>, - pub lsp_server_capabilities: HashMap, - lsp_data: HashMap, - next_hint_id: Arc, -} - -#[derive(Debug)] -pub struct BufferLspData { - buffer_version: Global, - document_colors: Option, - code_lens: Option, - inlay_hints: BufferInlayHints, - lsp_requests: HashMap>>, - chunk_lsp_requests: HashMap>, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -struct LspKey { - request_type: TypeId, - server_queried: Option, -} - -impl BufferLspData { - fn new(buffer: &Entity, cx: &mut App) -> Self { - Self { - buffer_version: buffer.read(cx).version(), - document_colors: None, - code_lens: None, - inlay_hints: BufferInlayHints::new(buffer, cx), - lsp_requests: HashMap::default(), - chunk_lsp_requests: HashMap::default(), - } - } - - fn remove_server_data(&mut self, for_server: LanguageServerId) { - if let Some(document_colors) = &mut self.document_colors { - document_colors.colors.remove(&for_server); - document_colors.cache_version += 1; - } - - if let Some(code_lens) = &mut self.code_lens { - code_lens.lens.remove(&for_server); - } - - self.inlay_hints.remove_server_data(for_server); - } - - #[cfg(any(test, feature = "test-support"))] - pub fn inlay_hints(&self) -> &BufferInlayHints { - &self.inlay_hints - } -} - -#[derive(Debug, Default, Clone)] -pub struct DocumentColors { - pub colors: HashSet, - pub cache_version: Option, -} - -type DocumentColorTask = Shared>>>; -type CodeLensTask = Shared>, Arc>>>; - -#[derive(Debug, Default)] -struct DocumentColorData { - colors: HashMap>, - cache_version: usize, - colors_update: Option<(Global, DocumentColorTask)>, -} - -#[derive(Debug, Default)] -struct CodeLensData { - lens: HashMap>, - update: Option<(Global, CodeLensTask)>, -} - -#[derive(Debug)] -pub enum LspStoreEvent { - LanguageServerAdded(LanguageServerId, LanguageServerName, Option), - LanguageServerRemoved(LanguageServerId), - LanguageServerUpdate { - language_server_id: LanguageServerId, - name: Option, - message: proto::update_language_server::Variant, - }, - LanguageServerLog(LanguageServerId, LanguageServerLogType, String), - LanguageServerPrompt(LanguageServerPromptRequest), - LanguageDetected { - buffer: Entity, - new_language: Option>, - }, - Notification(String), - RefreshInlayHints { - server_id: LanguageServerId, - request_id: Option, - }, - RefreshCodeLens, - DiagnosticsUpdated { - server_id: LanguageServerId, - paths: Vec, - }, - DiskBasedDiagnosticsStarted { - language_server_id: LanguageServerId, - }, - DiskBasedDiagnosticsFinished { - language_server_id: LanguageServerId, - }, - SnippetEdit { - buffer_id: BufferId, - edits: Vec<(lsp::Range, Snippet)>, - most_recent_edit: clock::Lamport, - }, -} - -#[derive(Clone, Debug, Serialize)] -pub struct LanguageServerStatus { - pub name: LanguageServerName, - pub pending_work: BTreeMap, - pub has_pending_diagnostic_updates: bool, - pub progress_tokens: HashSet, - pub worktree: Option, - pub binary: Option, - pub configuration: Option, - pub workspace_folders: BTreeSet, -} - -#[derive(Clone, Debug)] -struct CoreSymbol { - pub language_server_name: LanguageServerName, - pub source_worktree_id: WorktreeId, - pub source_language_server_id: LanguageServerId, - pub path: SymbolLocation, - pub name: String, - pub kind: lsp::SymbolKind, - pub range: Range>, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum SymbolLocation { - InProject(ProjectPath), - OutsideProject { - abs_path: Arc, - signature: [u8; 32], - }, -} - -impl SymbolLocation { - fn file_name(&self) -> Option<&str> { - match self { - Self::InProject(path) => path.path.file_name(), - Self::OutsideProject { abs_path, .. } => abs_path.file_name()?.to_str(), - } - } -} - -impl LspStore { - pub fn init(client: &AnyProtoClient) { - client.add_entity_request_handler(Self::handle_lsp_query); - client.add_entity_message_handler(Self::handle_lsp_query_response); - client.add_entity_request_handler(Self::handle_restart_language_servers); - client.add_entity_request_handler(Self::handle_stop_language_servers); - client.add_entity_request_handler(Self::handle_cancel_language_server_work); - client.add_entity_message_handler(Self::handle_start_language_server); - client.add_entity_message_handler(Self::handle_update_language_server); - client.add_entity_message_handler(Self::handle_language_server_log); - client.add_entity_message_handler(Self::handle_update_diagnostic_summary); - client.add_entity_request_handler(Self::handle_format_buffers); - client.add_entity_request_handler(Self::handle_apply_code_action_kind); - client.add_entity_request_handler(Self::handle_resolve_completion_documentation); - client.add_entity_request_handler(Self::handle_apply_code_action); - client.add_entity_request_handler(Self::handle_get_project_symbols); - client.add_entity_request_handler(Self::handle_resolve_inlay_hint); - client.add_entity_request_handler(Self::handle_get_color_presentation); - client.add_entity_request_handler(Self::handle_open_buffer_for_symbol); - client.add_entity_request_handler(Self::handle_refresh_inlay_hints); - client.add_entity_request_handler(Self::handle_refresh_code_lens); - client.add_entity_request_handler(Self::handle_on_type_formatting); - client.add_entity_request_handler(Self::handle_apply_additional_edits_for_completion); - client.add_entity_request_handler(Self::handle_register_buffer_with_language_servers); - client.add_entity_request_handler(Self::handle_rename_project_entry); - client.add_entity_request_handler(Self::handle_pull_workspace_diagnostics); - client.add_entity_request_handler(Self::handle_lsp_get_completions); - client.add_entity_request_handler(Self::handle_lsp_command::); - client.add_entity_request_handler(Self::handle_lsp_command::); - client.add_entity_request_handler(Self::handle_lsp_command::); - client.add_entity_request_handler(Self::handle_lsp_command::); - client.add_entity_request_handler(Self::handle_lsp_command::); - - client.add_entity_request_handler(Self::handle_lsp_ext_cancel_flycheck); - client.add_entity_request_handler(Self::handle_lsp_ext_run_flycheck); - client.add_entity_request_handler(Self::handle_lsp_ext_clear_flycheck); - client.add_entity_request_handler(Self::handle_lsp_command::); - client.add_entity_request_handler(Self::handle_lsp_command::); - client.add_entity_request_handler( - Self::handle_lsp_command::, - ); - client.add_entity_request_handler( - Self::handle_lsp_command::, - ); - client.add_entity_request_handler( - Self::handle_lsp_command::, - ); - } - - pub fn as_remote(&self) -> Option<&RemoteLspStore> { - match &self.mode { - LspStoreMode::Remote(remote_lsp_store) => Some(remote_lsp_store), - _ => None, - } - } - - pub fn as_local(&self) -> Option<&LocalLspStore> { - match &self.mode { - LspStoreMode::Local(local_lsp_store) => Some(local_lsp_store), - _ => None, - } - } - - pub fn as_local_mut(&mut self) -> Option<&mut LocalLspStore> { - match &mut self.mode { - LspStoreMode::Local(local_lsp_store) => Some(local_lsp_store), - _ => None, - } - } - - pub fn upstream_client(&self) -> Option<(AnyProtoClient, u64)> { - match &self.mode { - LspStoreMode::Remote(RemoteLspStore { - upstream_client: Some(upstream_client), - upstream_project_id, - .. - }) => Some((upstream_client.clone(), *upstream_project_id)), - - LspStoreMode::Remote(RemoteLspStore { - upstream_client: None, - .. - }) => None, - LspStoreMode::Local(_) => None, - } - } - - pub fn new_local( - buffer_store: Entity, - worktree_store: Entity, - prettier_store: Entity, - toolchain_store: Entity, - environment: Entity, - manifest_tree: Entity, - languages: Arc, - http_client: Arc, - fs: Arc, - cx: &mut Context, - ) -> Self { - let yarn = YarnPathStore::new(fs.clone(), cx); - cx.subscribe(&buffer_store, Self::on_buffer_store_event) - .detach(); - cx.subscribe(&worktree_store, Self::on_worktree_store_event) - .detach(); - cx.subscribe(&prettier_store, Self::on_prettier_store_event) - .detach(); - cx.subscribe(&toolchain_store, Self::on_toolchain_store_event) - .detach(); - cx.observe_global::(Self::on_settings_changed) - .detach(); - subscribe_to_binary_statuses(&languages, cx).detach(); - - let _maintain_workspace_config = { - let (sender, receiver) = watch::channel(); - (Self::maintain_workspace_config(receiver, cx), sender) - }; - - Self { - mode: LspStoreMode::Local(LocalLspStore { - weak: cx.weak_entity(), - worktree_store: worktree_store.clone(), - - supplementary_language_servers: Default::default(), - languages: languages.clone(), - language_server_ids: Default::default(), - language_servers: Default::default(), - last_workspace_edits_by_language_server: Default::default(), - language_server_watched_paths: Default::default(), - language_server_paths_watched_for_rename: Default::default(), - language_server_dynamic_registrations: Default::default(), - buffers_being_formatted: Default::default(), - buffer_snapshots: Default::default(), - prettier_store, - environment, - http_client, - fs, - yarn, - next_diagnostic_group_id: Default::default(), - diagnostics: Default::default(), - _subscription: cx.on_app_quit(|this, cx| { - this.as_local_mut() - .unwrap() - .shutdown_language_servers_on_quit(cx) - }), - lsp_tree: LanguageServerTree::new( - manifest_tree, - languages.clone(), - toolchain_store.clone(), - ), - toolchain_store, - registered_buffers: HashMap::default(), - buffers_opened_in_servers: HashMap::default(), - buffer_pull_diagnostics_result_ids: HashMap::default(), - workspace_pull_diagnostics_result_ids: HashMap::default(), - watched_manifest_filenames: ManifestProvidersStore::global(cx) - .manifest_file_names(), - }), - last_formatting_failure: None, - downstream_client: None, - buffer_store, - worktree_store, - languages: languages.clone(), - language_server_statuses: Default::default(), - nonce: StdRng::from_os_rng().random(), - diagnostic_summaries: HashMap::default(), - lsp_server_capabilities: HashMap::default(), - lsp_data: HashMap::default(), - next_hint_id: Arc::default(), - active_entry: None, - _maintain_workspace_config, - _maintain_buffer_languages: Self::maintain_buffer_languages(languages, cx), - } - } - - fn send_lsp_proto_request( - &self, - buffer: Entity, - client: AnyProtoClient, - upstream_project_id: u64, - request: R, - cx: &mut Context, - ) -> Task::Response>> { - if !self.is_capable_for_proto_request(&buffer, &request, cx) { - return Task::ready(Ok(R::Response::default())); - } - let message = request.to_proto(upstream_project_id, buffer.read(cx)); - cx.spawn(async move |this, cx| { - let response = client.request(message).await?; - let this = this.upgrade().context("project dropped")?; - request - .response_from_proto(response, this, buffer, cx.clone()) - .await - }) - } - - pub(super) fn new_remote( - buffer_store: Entity, - worktree_store: Entity, - languages: Arc, - upstream_client: AnyProtoClient, - project_id: u64, - cx: &mut Context, - ) -> Self { - cx.subscribe(&buffer_store, Self::on_buffer_store_event) - .detach(); - cx.subscribe(&worktree_store, Self::on_worktree_store_event) - .detach(); - subscribe_to_binary_statuses(&languages, cx).detach(); - let _maintain_workspace_config = { - let (sender, receiver) = watch::channel(); - (Self::maintain_workspace_config(receiver, cx), sender) - }; - Self { - mode: LspStoreMode::Remote(RemoteLspStore { - upstream_client: Some(upstream_client), - upstream_project_id: project_id, - }), - downstream_client: None, - last_formatting_failure: None, - buffer_store, - worktree_store, - languages: languages.clone(), - language_server_statuses: Default::default(), - nonce: StdRng::from_os_rng().random(), - diagnostic_summaries: HashMap::default(), - lsp_server_capabilities: HashMap::default(), - next_hint_id: Arc::default(), - lsp_data: HashMap::default(), - active_entry: None, - - _maintain_workspace_config, - _maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx), - } - } - - fn on_buffer_store_event( - &mut self, - _: Entity, - event: &BufferStoreEvent, - cx: &mut Context, - ) { - match event { - BufferStoreEvent::BufferAdded(buffer) => { - self.on_buffer_added(buffer, cx).log_err(); - } - BufferStoreEvent::BufferChangedFilePath { buffer, old_file } => { - let buffer_id = buffer.read(cx).remote_id(); - if let Some(local) = self.as_local_mut() - && let Some(old_file) = File::from_dyn(old_file.as_ref()) - { - local.reset_buffer(buffer, old_file, cx); - - if local.registered_buffers.contains_key(&buffer_id) { - local.unregister_old_buffer_from_language_servers(buffer, old_file, cx); - } - } - - self.detect_language_for_buffer(buffer, cx); - if let Some(local) = self.as_local_mut() { - local.initialize_buffer(buffer, cx); - if local.registered_buffers.contains_key(&buffer_id) { - local.register_buffer_with_language_servers(buffer, HashSet::default(), cx); - } - } - } - _ => {} - } - } - - fn on_worktree_store_event( - &mut self, - _: Entity, - event: &WorktreeStoreEvent, - cx: &mut Context, - ) { - match event { - WorktreeStoreEvent::WorktreeAdded(worktree) => { - if !worktree.read(cx).is_local() { - return; - } - cx.subscribe(worktree, |this, worktree, event, cx| match event { - worktree::Event::UpdatedEntries(changes) => { - this.update_local_worktree_language_servers(&worktree, changes, cx); - } - worktree::Event::UpdatedGitRepositories(_) - | worktree::Event::DeletedEntry(_) => {} - }) - .detach() - } - WorktreeStoreEvent::WorktreeRemoved(_, id) => self.remove_worktree(*id, cx), - WorktreeStoreEvent::WorktreeUpdateSent(worktree) => { - worktree.update(cx, |worktree, _cx| self.send_diagnostic_summaries(worktree)); - } - WorktreeStoreEvent::WorktreeReleased(..) - | WorktreeStoreEvent::WorktreeOrderChanged - | WorktreeStoreEvent::WorktreeUpdatedEntries(..) - | WorktreeStoreEvent::WorktreeUpdatedGitRepositories(..) - | WorktreeStoreEvent::WorktreeDeletedEntry(..) => {} - } - } - - fn on_prettier_store_event( - &mut self, - _: Entity, - event: &PrettierStoreEvent, - cx: &mut Context, - ) { - match event { - PrettierStoreEvent::LanguageServerRemoved(prettier_server_id) => { - self.unregister_supplementary_language_server(*prettier_server_id, cx); - } - PrettierStoreEvent::LanguageServerAdded { - new_server_id, - name, - prettier_server, - } => { - self.register_supplementary_language_server( - *new_server_id, - name.clone(), - prettier_server.clone(), - cx, - ); - } - } - } - - fn on_toolchain_store_event( - &mut self, - _: Entity, - event: &ToolchainStoreEvent, - _: &mut Context, - ) { - if let ToolchainStoreEvent::ToolchainActivated = event { - self.request_workspace_config_refresh() - } - } - - fn request_workspace_config_refresh(&mut self) { - *self._maintain_workspace_config.1.borrow_mut() = (); - } - - pub fn prettier_store(&self) -> Option> { - self.as_local().map(|local| local.prettier_store.clone()) - } - - fn on_buffer_event( - &mut self, - buffer: Entity, - event: &language::BufferEvent, - cx: &mut Context, - ) { - match event { - language::BufferEvent::Edited => { - self.on_buffer_edited(buffer, cx); - } - - language::BufferEvent::Saved => { - self.on_buffer_saved(buffer, cx); - } - - _ => {} - } - } - - fn on_buffer_added(&mut self, buffer: &Entity, cx: &mut Context) -> Result<()> { - buffer - .read(cx) - .set_language_registry(self.languages.clone()); - - cx.subscribe(buffer, |this, buffer, event, cx| { - this.on_buffer_event(buffer, event, cx); - }) - .detach(); - - self.detect_language_for_buffer(buffer, cx); - if let Some(local) = self.as_local_mut() { - local.initialize_buffer(buffer, cx); - } - - Ok(()) - } - - pub(crate) fn register_buffer_with_language_servers( - &mut self, - buffer: &Entity, - only_register_servers: HashSet, - ignore_refcounts: bool, - cx: &mut Context, - ) -> OpenLspBufferHandle { - let buffer_id = buffer.read(cx).remote_id(); - let handle = OpenLspBufferHandle(cx.new(|_| OpenLspBuffer(buffer.clone()))); - if let Some(local) = self.as_local_mut() { - let refcount = local.registered_buffers.entry(buffer_id).or_insert(0); - if !ignore_refcounts { - *refcount += 1; - } - - // We run early exits on non-existing buffers AFTER we mark the buffer as registered in order to handle buffer saving. - // When a new unnamed buffer is created and saved, we will start loading it's language. Once the language is loaded, we go over all "language-less" buffers and try to fit that new language - // with them. However, we do that only for the buffers that we think are open in at least one editor; thus, we need to keep tab of unnamed buffers as well, even though they're not actually registered with any language - // servers in practice (we don't support non-file URI schemes in our LSP impl). - let Some(file) = File::from_dyn(buffer.read(cx).file()) else { - return handle; - }; - if !file.is_local() { - return handle; - } - - if ignore_refcounts || *refcount == 1 { - local.register_buffer_with_language_servers(buffer, only_register_servers, cx); - } - if !ignore_refcounts { - cx.observe_release(&handle.0, move |lsp_store, buffer, cx| { - let refcount = { - let local = lsp_store.as_local_mut().unwrap(); - let Some(refcount) = local.registered_buffers.get_mut(&buffer_id) else { - debug_panic!("bad refcounting"); - return; - }; - - *refcount -= 1; - *refcount - }; - if refcount == 0 { - lsp_store.lsp_data.remove(&buffer_id); - let local = lsp_store.as_local_mut().unwrap(); - local.registered_buffers.remove(&buffer_id); - - local.buffers_opened_in_servers.remove(&buffer_id); - if let Some(file) = File::from_dyn(buffer.0.read(cx).file()).cloned() { - local.unregister_old_buffer_from_language_servers(&buffer.0, &file, cx); - - let buffer_abs_path = file.abs_path(cx); - for (_, buffer_pull_diagnostics_result_ids) in - &mut local.buffer_pull_diagnostics_result_ids - { - buffer_pull_diagnostics_result_ids.retain( - |_, buffer_result_ids| { - buffer_result_ids.remove(&buffer_abs_path); - !buffer_result_ids.is_empty() - }, - ); - } - - let diagnostic_updates = local - .language_servers - .keys() - .cloned() - .map(|server_id| DocumentDiagnosticsUpdate { - diagnostics: DocumentDiagnostics { - document_abs_path: buffer_abs_path.clone(), - version: None, - diagnostics: Vec::new(), - }, - result_id: None, - registration_id: None, - server_id: server_id, - disk_based_sources: Cow::Borrowed(&[]), - }) - .collect::>(); - - lsp_store - .merge_diagnostic_entries( - diagnostic_updates, - |_, diagnostic, _| { - diagnostic.source_kind != DiagnosticSourceKind::Pulled - }, - cx, - ) - .context("Clearing diagnostics for the closed buffer") - .log_err(); - } - } - }) - .detach(); - } - } else if let Some((upstream_client, upstream_project_id)) = self.upstream_client() { - let buffer_id = buffer.read(cx).remote_id().to_proto(); - cx.background_spawn(async move { - upstream_client - .request(proto::RegisterBufferWithLanguageServers { - project_id: upstream_project_id, - buffer_id, - only_servers: only_register_servers - .into_iter() - .map(|selector| { - let selector = match selector { - LanguageServerSelector::Id(language_server_id) => { - proto::language_server_selector::Selector::ServerId( - language_server_id.to_proto(), - ) - } - LanguageServerSelector::Name(language_server_name) => { - proto::language_server_selector::Selector::Name( - language_server_name.to_string(), - ) - } - }; - proto::LanguageServerSelector { - selector: Some(selector), - } - }) - .collect(), - }) - .await - }) - .detach(); - } else { - // Our remote connection got closed - } - handle - } - - fn maintain_buffer_languages( - languages: Arc, - cx: &mut Context, - ) -> Task<()> { - let mut subscription = languages.subscribe(); - let mut prev_reload_count = languages.reload_count(); - cx.spawn(async move |this, cx| { - while let Some(()) = subscription.next().await { - if let Some(this) = this.upgrade() { - // If the language registry has been reloaded, then remove and - // re-assign the languages on all open buffers. - let reload_count = languages.reload_count(); - if reload_count > prev_reload_count { - prev_reload_count = reload_count; - this.update(cx, |this, cx| { - this.buffer_store.clone().update(cx, |buffer_store, cx| { - for buffer in buffer_store.buffers() { - if let Some(f) = File::from_dyn(buffer.read(cx).file()).cloned() - { - buffer.update(cx, |buffer, cx| { - buffer.set_language_async(None, cx) - }); - if let Some(local) = this.as_local_mut() { - local.reset_buffer(&buffer, &f, cx); - - if local - .registered_buffers - .contains_key(&buffer.read(cx).remote_id()) - && let Some(file_url) = - file_path_to_lsp_url(&f.abs_path(cx)).log_err() - { - local.unregister_buffer_from_language_servers( - &buffer, &file_url, cx, - ); - } - } - } - } - }); - }) - .ok(); - } - - this.update(cx, |this, cx| { - let mut plain_text_buffers = Vec::new(); - let mut buffers_with_unknown_injections = Vec::new(); - for handle in this.buffer_store.read(cx).buffers() { - let buffer = handle.read(cx); - if buffer.language().is_none() - || buffer.language() == Some(&*language::PLAIN_TEXT) - { - plain_text_buffers.push(handle); - } else if buffer.contains_unknown_injections() { - buffers_with_unknown_injections.push(handle); - } - } - - // Deprioritize the invisible worktrees so main worktrees' language servers can be started first, - // and reused later in the invisible worktrees. - plain_text_buffers.sort_by_key(|buffer| { - Reverse( - File::from_dyn(buffer.read(cx).file()) - .map(|file| file.worktree.read(cx).is_visible()), - ) - }); - - for buffer in plain_text_buffers { - this.detect_language_for_buffer(&buffer, cx); - if let Some(local) = this.as_local_mut() { - local.initialize_buffer(&buffer, cx); - if local - .registered_buffers - .contains_key(&buffer.read(cx).remote_id()) - { - local.register_buffer_with_language_servers( - &buffer, - HashSet::default(), - cx, - ); - } - } - } - - for buffer in buffers_with_unknown_injections { - buffer.update(cx, |buffer, cx| buffer.reparse(cx, false)); - } - }) - .ok(); - } - } - }) - } - - fn detect_language_for_buffer( - &mut self, - buffer_handle: &Entity, - cx: &mut Context, - ) -> Option { - // If the buffer has a language, set it and start the language server if we haven't already. - let buffer = buffer_handle.read(cx); - let file = buffer.file()?; - - let content = buffer.as_rope(); - let available_language = self.languages.language_for_file(file, Some(content), cx); - if let Some(available_language) = &available_language { - if let Some(Ok(Ok(new_language))) = self - .languages - .load_language(available_language) - .now_or_never() - { - self.set_language_for_buffer(buffer_handle, new_language, cx); - } - } else { - cx.emit(LspStoreEvent::LanguageDetected { - buffer: buffer_handle.clone(), - new_language: None, - }); - } - - available_language - } - - pub(crate) fn set_language_for_buffer( - &mut self, - buffer_entity: &Entity, - new_language: Arc, - cx: &mut Context, - ) { - let buffer = buffer_entity.read(cx); - let buffer_file = buffer.file().cloned(); - let buffer_id = buffer.remote_id(); - if let Some(local_store) = self.as_local_mut() - && local_store.registered_buffers.contains_key(&buffer_id) - && let Some(abs_path) = - File::from_dyn(buffer_file.as_ref()).map(|file| file.abs_path(cx)) - && let Some(file_url) = file_path_to_lsp_url(&abs_path).log_err() - { - local_store.unregister_buffer_from_language_servers(buffer_entity, &file_url, cx); - } - buffer_entity.update(cx, |buffer, cx| { - if buffer - .language() - .is_none_or(|old_language| !Arc::ptr_eq(old_language, &new_language)) - { - buffer.set_language_async(Some(new_language.clone()), cx); - } - }); - - let settings = - language_settings(Some(new_language.name()), buffer_file.as_ref(), cx).into_owned(); - let buffer_file = File::from_dyn(buffer_file.as_ref()); - - let worktree_id = if let Some(file) = buffer_file { - let worktree = file.worktree.clone(); - - if let Some(local) = self.as_local_mut() - && local.registered_buffers.contains_key(&buffer_id) - { - local.register_buffer_with_language_servers(buffer_entity, HashSet::default(), cx); - } - Some(worktree.read(cx).id()) - } else { - None - }; - - if settings.prettier.allowed - && let Some(prettier_plugins) = prettier_store::prettier_plugins_for_language(&settings) - { - let prettier_store = self.as_local().map(|s| s.prettier_store.clone()); - if let Some(prettier_store) = prettier_store { - prettier_store.update(cx, |prettier_store, cx| { - prettier_store.install_default_prettier( - worktree_id, - prettier_plugins.iter().map(|s| Arc::from(s.as_str())), - cx, - ) - }) - } - } - - cx.emit(LspStoreEvent::LanguageDetected { - buffer: buffer_entity.clone(), - new_language: Some(new_language), - }) - } - - pub fn buffer_store(&self) -> Entity { - self.buffer_store.clone() - } - - pub fn set_active_entry(&mut self, active_entry: Option) { - self.active_entry = active_entry; - } - - pub(crate) fn send_diagnostic_summaries(&self, worktree: &mut Worktree) { - if let Some((client, downstream_project_id)) = self.downstream_client.clone() - && let Some(diangostic_summaries) = self.diagnostic_summaries.get(&worktree.id()) - { - let mut summaries = diangostic_summaries.iter().flat_map(|(path, summaries)| { - summaries - .iter() - .map(|(server_id, summary)| summary.to_proto(*server_id, path.as_ref())) - }); - if let Some(summary) = summaries.next() { - client - .send(proto::UpdateDiagnosticSummary { - project_id: downstream_project_id, - worktree_id: worktree.id().to_proto(), - summary: Some(summary), - more_summaries: summaries.collect(), - }) - .log_err(); - } - } - } - - fn is_capable_for_proto_request( - &self, - buffer: &Entity, - request: &R, - cx: &App, - ) -> bool - where - R: LspCommand, - { - self.check_if_capable_for_proto_request( - buffer, - |capabilities| { - request.check_capabilities(AdapterServerCapabilities { - server_capabilities: capabilities.clone(), - code_action_kinds: None, - }) - }, - cx, - ) - } - - fn check_if_capable_for_proto_request( - &self, - buffer: &Entity, - check: F, - cx: &App, - ) -> bool - where - F: FnMut(&lsp::ServerCapabilities) -> bool, - { - let Some(language) = buffer.read(cx).language().cloned() else { - return false; - }; - let relevant_language_servers = self - .languages - .lsp_adapters(&language.name()) - .into_iter() - .map(|lsp_adapter| lsp_adapter.name()) - .collect::>(); - self.language_server_statuses - .iter() - .filter_map(|(server_id, server_status)| { - relevant_language_servers - .contains(&server_status.name) - .then_some(server_id) - }) - .filter_map(|server_id| self.lsp_server_capabilities.get(server_id)) - .any(check) - } - - fn all_capable_for_proto_request( - &self, - buffer: &Entity, - mut check: F, - cx: &App, - ) -> Vec - where - F: FnMut(&lsp::LanguageServerName, &lsp::ServerCapabilities) -> bool, - { - let Some(language) = buffer.read(cx).language().cloned() else { - return Vec::default(); - }; - let relevant_language_servers = self - .languages - .lsp_adapters(&language.name()) - .into_iter() - .map(|lsp_adapter| lsp_adapter.name()) - .collect::>(); - self.language_server_statuses - .iter() - .filter_map(|(server_id, server_status)| { - relevant_language_servers - .contains(&server_status.name) - .then_some((server_id, &server_status.name)) - }) - .filter_map(|(server_id, server_name)| { - self.lsp_server_capabilities - .get(server_id) - .map(|c| (server_id, server_name, c)) - }) - .filter(|(_, server_name, capabilities)| check(server_name, capabilities)) - .map(|(server_id, _, _)| *server_id) - .collect() - } - - pub fn request_lsp( - &mut self, - buffer: Entity, - server: LanguageServerToQuery, - request: R, - cx: &mut Context, - ) -> Task> - where - R: LspCommand, - ::Result: Send, - ::Params: Send, - { - if let Some((upstream_client, upstream_project_id)) = self.upstream_client() { - return self.send_lsp_proto_request( - buffer, - upstream_client, - upstream_project_id, - request, - cx, - ); - } - - let Some(language_server) = buffer.update(cx, |buffer, cx| match server { - LanguageServerToQuery::FirstCapable => self.as_local().and_then(|local| { - local - .language_servers_for_buffer(buffer, cx) - .find(|(_, server)| { - request.check_capabilities(server.adapter_server_capabilities()) - }) - .map(|(_, server)| server.clone()) - }), - LanguageServerToQuery::Other(id) => self - .language_server_for_local_buffer(buffer, id, cx) - .and_then(|(_, server)| { - request - .check_capabilities(server.adapter_server_capabilities()) - .then(|| Arc::clone(server)) - }), - }) else { - return Task::ready(Ok(Default::default())); - }; - - let file = File::from_dyn(buffer.read(cx).file()).and_then(File::as_local); - - let Some(file) = file else { - return Task::ready(Ok(Default::default())); - }; - - let lsp_params = match request.to_lsp_params_or_response( - &file.abs_path(cx), - buffer.read(cx), - &language_server, - cx, - ) { - Ok(LspParamsOrResponse::Params(lsp_params)) => lsp_params, - Ok(LspParamsOrResponse::Response(response)) => return Task::ready(Ok(response)), - Err(err) => { - let message = format!( - "{} via {} failed: {}", - request.display_name(), - language_server.name(), - err - ); - // rust-analyzer likes to error with this when its still loading up - if !message.ends_with("content modified") { - log::warn!("{message}"); - } - return Task::ready(Err(anyhow!(message))); - } - }; - - let status = request.status(); - if !request.check_capabilities(language_server.adapter_server_capabilities()) { - return Task::ready(Ok(Default::default())); - } - cx.spawn(async move |this, cx| { - let lsp_request = language_server.request::(lsp_params); - - let id = lsp_request.id(); - let _cleanup = if status.is_some() { - cx.update(|cx| { - this.update(cx, |this, cx| { - this.on_lsp_work_start( - language_server.server_id(), - ProgressToken::Number(id), - LanguageServerProgress { - is_disk_based_diagnostics_progress: false, - is_cancellable: false, - title: None, - message: status.clone(), - percentage: None, - last_update_at: cx.background_executor().now(), - }, - cx, - ); - }) - }) - .log_err(); - - Some(defer(|| { - cx.update(|cx| { - this.update(cx, |this, cx| { - this.on_lsp_work_end( - language_server.server_id(), - ProgressToken::Number(id), - cx, - ); - }) - }) - .log_err(); - })) - } else { - None - }; - - let result = lsp_request.await.into_response(); - - let response = result.map_err(|err| { - let message = format!( - "{} via {} failed: {}", - request.display_name(), - language_server.name(), - err - ); - // rust-analyzer likes to error with this when its still loading up - if !message.ends_with("content modified") { - log::warn!("{message}"); - } - anyhow::anyhow!(message) - })?; - - request - .response_from_lsp( - response, - this.upgrade().context("no app context")?, - buffer, - language_server.server_id(), - cx.clone(), - ) - .await - }) - } - - fn on_settings_changed(&mut self, cx: &mut Context) { - let mut language_formatters_to_check = Vec::new(); - for buffer in self.buffer_store.read(cx).buffers() { - let buffer = buffer.read(cx); - let buffer_file = File::from_dyn(buffer.file()); - let buffer_language = buffer.language(); - let settings = language_settings(buffer_language.map(|l| l.name()), buffer.file(), cx); - if buffer_language.is_some() { - language_formatters_to_check.push(( - buffer_file.map(|f| f.worktree_id(cx)), - settings.into_owned(), - )); - } - } - - self.request_workspace_config_refresh(); - - if let Some(prettier_store) = self.as_local().map(|s| s.prettier_store.clone()) { - prettier_store.update(cx, |prettier_store, cx| { - prettier_store.on_settings_changed(language_formatters_to_check, cx) - }) - } - - cx.notify(); - } - - fn refresh_server_tree(&mut self, cx: &mut Context) { - let buffer_store = self.buffer_store.clone(); - let Some(local) = self.as_local_mut() else { - return; - }; - let mut adapters = BTreeMap::default(); - let get_adapter = { - let languages = local.languages.clone(); - let environment = local.environment.clone(); - let weak = local.weak.clone(); - let worktree_store = local.worktree_store.clone(); - let http_client = local.http_client.clone(); - let fs = local.fs.clone(); - move |worktree_id, cx: &mut App| { - let worktree = worktree_store.read(cx).worktree_for_id(worktree_id, cx)?; - Some(LocalLspAdapterDelegate::new( - languages.clone(), - &environment, - weak.clone(), - &worktree, - http_client.clone(), - fs.clone(), - cx, - )) - } - }; - - let mut messages_to_report = Vec::new(); - let (new_tree, to_stop) = { - let mut rebase = local.lsp_tree.rebase(); - let buffers = buffer_store - .read(cx) - .buffers() - .filter_map(|buffer| { - let raw_buffer = buffer.read(cx); - if !local - .registered_buffers - .contains_key(&raw_buffer.remote_id()) - { - return None; - } - let file = File::from_dyn(raw_buffer.file()).cloned()?; - let language = raw_buffer.language().cloned()?; - Some((file, language, raw_buffer.remote_id())) - }) - .sorted_by_key(|(file, _, _)| Reverse(file.worktree.read(cx).is_visible())); - for (file, language, buffer_id) in buffers { - let worktree_id = file.worktree_id(cx); - let Some(worktree) = local - .worktree_store - .read(cx) - .worktree_for_id(worktree_id, cx) - else { - continue; - }; - - if let Some((_, apply)) = local.reuse_existing_language_server( - rebase.server_tree(), - &worktree, - &language.name(), - cx, - ) { - (apply)(rebase.server_tree()); - } else if let Some(lsp_delegate) = adapters - .entry(worktree_id) - .or_insert_with(|| get_adapter(worktree_id, cx)) - .clone() - { - let delegate = - Arc::new(ManifestQueryDelegate::new(worktree.read(cx).snapshot())); - let path = file - .path() - .parent() - .map(Arc::from) - .unwrap_or_else(|| file.path().clone()); - let worktree_path = ProjectPath { worktree_id, path }; - let abs_path = file.abs_path(cx); - let nodes = rebase - .walk( - worktree_path, - language.name(), - language.manifest(), - delegate.clone(), - cx, - ) - .collect::>(); - for node in nodes { - let server_id = node.server_id_or_init(|disposition| { - let path = &disposition.path; - let uri = Uri::from_file_path(worktree.read(cx).absolutize(&path.path)); - let key = LanguageServerSeed { - worktree_id, - name: disposition.server_name.clone(), - settings: disposition.settings.clone(), - toolchain: local.toolchain_store.read(cx).active_toolchain( - path.worktree_id, - &path.path, - language.name(), - ), - }; - local.language_server_ids.remove(&key); - - let server_id = local.get_or_insert_language_server( - &worktree, - lsp_delegate.clone(), - disposition, - &language.name(), - cx, - ); - if let Some(state) = local.language_servers.get(&server_id) - && let Ok(uri) = uri - { - state.add_workspace_folder(uri); - }; - server_id - }); - - if let Some(language_server_id) = server_id { - messages_to_report.push(LspStoreEvent::LanguageServerUpdate { - language_server_id, - name: node.name(), - message: - proto::update_language_server::Variant::RegisteredForBuffer( - proto::RegisteredForBuffer { - buffer_abs_path: abs_path - .to_string_lossy() - .into_owned(), - buffer_id: buffer_id.to_proto(), - }, - ), - }); - } - } - } else { - continue; - } - } - rebase.finish() - }; - for message in messages_to_report { - cx.emit(message); - } - local.lsp_tree = new_tree; - for (id, _) in to_stop { - self.stop_local_language_server(id, cx).detach(); - } - } - - pub fn apply_code_action( - &self, - buffer_handle: Entity, - mut action: CodeAction, - push_to_history: bool, - cx: &mut Context, - ) -> Task> { - if let Some((upstream_client, project_id)) = self.upstream_client() { - let request = proto::ApplyCodeAction { - project_id, - buffer_id: buffer_handle.read(cx).remote_id().into(), - action: Some(Self::serialize_code_action(&action)), - }; - let buffer_store = self.buffer_store(); - cx.spawn(async move |_, cx| { - let response = upstream_client - .request(request) - .await? - .transaction - .context("missing transaction")?; - - buffer_store - .update(cx, |buffer_store, cx| { - buffer_store.deserialize_project_transaction(response, push_to_history, cx) - })? - .await - }) - } else if self.mode.is_local() { - let Some((_, lang_server)) = buffer_handle.update(cx, |buffer, cx| { - self.language_server_for_local_buffer(buffer, action.server_id, cx) - .map(|(adapter, server)| (adapter.clone(), server.clone())) - }) else { - return Task::ready(Ok(ProjectTransaction::default())); - }; - cx.spawn(async move |this, cx| { - LocalLspStore::try_resolve_code_action(&lang_server, &mut action) - .await - .context("resolving a code action")?; - if let Some(edit) = action.lsp_action.edit() - && (edit.changes.is_some() || edit.document_changes.is_some()) { - return LocalLspStore::deserialize_workspace_edit( - this.upgrade().context("no app present")?, - edit.clone(), - push_to_history, - - lang_server.clone(), - cx, - ) - .await; - } - - if let Some(command) = action.lsp_action.command() { - let server_capabilities = lang_server.capabilities(); - let available_commands = server_capabilities - .execute_command_provider - .as_ref() - .map(|options| options.commands.as_slice()) - .unwrap_or_default(); - if available_commands.contains(&command.command) { - this.update(cx, |this, _| { - this.as_local_mut() - .unwrap() - .last_workspace_edits_by_language_server - .remove(&lang_server.server_id()); - })?; - - let _result = lang_server - .request::(lsp::ExecuteCommandParams { - command: command.command.clone(), - arguments: command.arguments.clone().unwrap_or_default(), - ..lsp::ExecuteCommandParams::default() - }) - .await.into_response() - .context("execute command")?; - - return this.update(cx, |this, _| { - this.as_local_mut() - .unwrap() - .last_workspace_edits_by_language_server - .remove(&lang_server.server_id()) - .unwrap_or_default() - }); - } else { - log::warn!("Cannot execute a command {} not listed in the language server capabilities", command.command); - } - } - - Ok(ProjectTransaction::default()) - }) - } else { - Task::ready(Err(anyhow!("no upstream client and not local"))) - } - } - - pub fn apply_code_action_kind( - &mut self, - buffers: HashSet>, - kind: CodeActionKind, - push_to_history: bool, - cx: &mut Context, - ) -> Task> { - if self.as_local().is_some() { - cx.spawn(async move |lsp_store, cx| { - let buffers = buffers.into_iter().collect::>(); - let result = LocalLspStore::execute_code_action_kind_locally( - lsp_store.clone(), - buffers, - kind, - push_to_history, - cx, - ) - .await; - lsp_store.update(cx, |lsp_store, _| { - lsp_store.update_last_formatting_failure(&result); - })?; - result - }) - } else if let Some((client, project_id)) = self.upstream_client() { - let buffer_store = self.buffer_store(); - cx.spawn(async move |lsp_store, cx| { - let result = client - .request(proto::ApplyCodeActionKind { - project_id, - kind: kind.as_str().to_owned(), - buffer_ids: buffers - .iter() - .map(|buffer| { - buffer.read_with(cx, |buffer, _| buffer.remote_id().into()) - }) - .collect::>()?, - }) - .await - .and_then(|result| result.transaction.context("missing transaction")); - lsp_store.update(cx, |lsp_store, _| { - lsp_store.update_last_formatting_failure(&result); - })?; - - let transaction_response = result?; - buffer_store - .update(cx, |buffer_store, cx| { - buffer_store.deserialize_project_transaction( - transaction_response, - push_to_history, - cx, - ) - })? - .await - }) - } else { - Task::ready(Ok(ProjectTransaction::default())) - } - } - - pub fn resolved_hint( - &mut self, - buffer_id: BufferId, - id: InlayId, - cx: &mut Context, - ) -> Option { - let buffer = self.buffer_store.read(cx).get(buffer_id)?; - - let lsp_data = self.lsp_data.get_mut(&buffer_id)?; - let buffer_lsp_hints = &mut lsp_data.inlay_hints; - let hint = buffer_lsp_hints.hint_for_id(id)?.clone(); - let (server_id, resolve_data) = match &hint.resolve_state { - ResolveState::Resolved => return Some(ResolvedHint::Resolved(hint)), - ResolveState::Resolving => { - return Some(ResolvedHint::Resolving( - buffer_lsp_hints.hint_resolves.get(&id)?.clone(), - )); - } - ResolveState::CanResolve(server_id, resolve_data) => (*server_id, resolve_data.clone()), - }; - - let resolve_task = self.resolve_inlay_hint(hint, buffer, server_id, cx); - let buffer_lsp_hints = &mut self.lsp_data.get_mut(&buffer_id)?.inlay_hints; - let previous_task = buffer_lsp_hints.hint_resolves.insert( - id, - cx.spawn(async move |lsp_store, cx| { - let resolved_hint = resolve_task.await; - lsp_store - .update(cx, |lsp_store, _| { - if let Some(old_inlay_hint) = lsp_store - .lsp_data - .get_mut(&buffer_id) - .and_then(|buffer_lsp_data| buffer_lsp_data.inlay_hints.hint_for_id(id)) - { - match resolved_hint { - Ok(resolved_hint) => { - *old_inlay_hint = resolved_hint; - } - Err(e) => { - old_inlay_hint.resolve_state = - ResolveState::CanResolve(server_id, resolve_data); - log::error!("Inlay hint resolve failed: {e:#}"); - } - } - } - }) - .ok(); - }) - .shared(), - ); - debug_assert!( - previous_task.is_none(), - "Did not change hint's resolve state after spawning its resolve" - ); - buffer_lsp_hints.hint_for_id(id)?.resolve_state = ResolveState::Resolving; - None - } - - fn resolve_inlay_hint( - &self, - mut hint: InlayHint, - buffer: Entity, - server_id: LanguageServerId, - cx: &mut Context, - ) -> Task> { - if let Some((upstream_client, project_id)) = self.upstream_client() { - if !self.check_if_capable_for_proto_request(&buffer, InlayHints::can_resolve_inlays, cx) - { - hint.resolve_state = ResolveState::Resolved; - return Task::ready(Ok(hint)); - } - let request = proto::ResolveInlayHint { - project_id, - buffer_id: buffer.read(cx).remote_id().into(), - language_server_id: server_id.0 as u64, - hint: Some(InlayHints::project_to_proto_hint(hint.clone())), - }; - cx.background_spawn(async move { - let response = upstream_client - .request(request) - .await - .context("inlay hints proto request")?; - match response.hint { - Some(resolved_hint) => InlayHints::proto_to_project_hint(resolved_hint) - .context("inlay hints proto resolve response conversion"), - None => Ok(hint), - } - }) - } else { - let Some(lang_server) = buffer.update(cx, |buffer, cx| { - self.language_server_for_local_buffer(buffer, server_id, cx) - .map(|(_, server)| server.clone()) - }) else { - return Task::ready(Ok(hint)); - }; - if !InlayHints::can_resolve_inlays(&lang_server.capabilities()) { - return Task::ready(Ok(hint)); - } - let buffer_snapshot = buffer.read(cx).snapshot(); - cx.spawn(async move |_, cx| { - let resolve_task = lang_server.request::( - InlayHints::project_to_lsp_hint(hint, &buffer_snapshot), - ); - let resolved_hint = resolve_task - .await - .into_response() - .context("inlay hint resolve LSP request")?; - let resolved_hint = InlayHints::lsp_to_project_hint( - resolved_hint, - &buffer, - server_id, - ResolveState::Resolved, - false, - cx, - ) - .await?; - Ok(resolved_hint) - }) - } - } - - pub fn resolve_color_presentation( - &mut self, - mut color: DocumentColor, - buffer: Entity, - server_id: LanguageServerId, - cx: &mut Context, - ) -> Task> { - if color.resolved { - return Task::ready(Ok(color)); - } - - if let Some((upstream_client, project_id)) = self.upstream_client() { - let start = color.lsp_range.start; - let end = color.lsp_range.end; - let request = proto::GetColorPresentation { - project_id, - server_id: server_id.to_proto(), - buffer_id: buffer.read(cx).remote_id().into(), - color: Some(proto::ColorInformation { - red: color.color.red, - green: color.color.green, - blue: color.color.blue, - alpha: color.color.alpha, - lsp_range_start: Some(proto::PointUtf16 { - row: start.line, - column: start.character, - }), - lsp_range_end: Some(proto::PointUtf16 { - row: end.line, - column: end.character, - }), - }), - }; - cx.background_spawn(async move { - let response = upstream_client - .request(request) - .await - .context("color presentation proto request")?; - color.resolved = true; - color.color_presentations = response - .presentations - .into_iter() - .map(|presentation| ColorPresentation { - label: SharedString::from(presentation.label), - text_edit: presentation.text_edit.and_then(deserialize_lsp_edit), - additional_text_edits: presentation - .additional_text_edits - .into_iter() - .filter_map(deserialize_lsp_edit) - .collect(), - }) - .collect(); - Ok(color) - }) - } else { - let path = match buffer - .update(cx, |buffer, cx| { - Some(File::from_dyn(buffer.file())?.abs_path(cx)) - }) - .context("buffer with the missing path") - { - Ok(path) => path, - Err(e) => return Task::ready(Err(e)), - }; - let Some(lang_server) = buffer.update(cx, |buffer, cx| { - self.language_server_for_local_buffer(buffer, server_id, cx) - .map(|(_, server)| server.clone()) - }) else { - return Task::ready(Ok(color)); - }; - cx.background_spawn(async move { - let resolve_task = lang_server.request::( - lsp::ColorPresentationParams { - text_document: make_text_document_identifier(&path)?, - color: color.color, - range: color.lsp_range, - work_done_progress_params: Default::default(), - partial_result_params: Default::default(), - }, - ); - color.color_presentations = resolve_task - .await - .into_response() - .context("color presentation resolve LSP request")? - .into_iter() - .map(|presentation| ColorPresentation { - label: SharedString::from(presentation.label), - text_edit: presentation.text_edit, - additional_text_edits: presentation - .additional_text_edits - .unwrap_or_default(), - }) - .collect(); - color.resolved = true; - Ok(color) - }) - } - } - - pub(crate) fn linked_edits( - &mut self, - buffer: &Entity, - position: Anchor, - cx: &mut Context, - ) -> Task>>> { - let snapshot = buffer.read(cx).snapshot(); - let scope = snapshot.language_scope_at(position); - let Some(server_id) = self - .as_local() - .and_then(|local| { - buffer.update(cx, |buffer, cx| { - local - .language_servers_for_buffer(buffer, cx) - .filter(|(_, server)| { - LinkedEditingRange::check_server_capabilities(server.capabilities()) - }) - .filter(|(adapter, _)| { - scope - .as_ref() - .map(|scope| scope.language_allowed(&adapter.name)) - .unwrap_or(true) - }) - .map(|(_, server)| LanguageServerToQuery::Other(server.server_id())) - .next() - }) - }) - .or_else(|| { - self.upstream_client() - .is_some() - .then_some(LanguageServerToQuery::FirstCapable) - }) - .filter(|_| { - maybe!({ - let language = buffer.read(cx).language_at(position)?; - Some( - language_settings(Some(language.name()), buffer.read(cx).file(), cx) - .linked_edits, - ) - }) == Some(true) - }) - else { - return Task::ready(Ok(Vec::new())); - }; - - self.request_lsp( - buffer.clone(), - server_id, - LinkedEditingRange { position }, - cx, - ) - } - - fn apply_on_type_formatting( - &mut self, - buffer: Entity, - position: Anchor, - trigger: String, - cx: &mut Context, - ) -> Task>> { - if let Some((client, project_id)) = self.upstream_client() { - if !self.check_if_capable_for_proto_request( - &buffer, - |capabilities| { - OnTypeFormatting::supports_on_type_formatting(&trigger, capabilities) - }, - cx, - ) { - return Task::ready(Ok(None)); - } - let request = proto::OnTypeFormatting { - project_id, - buffer_id: buffer.read(cx).remote_id().into(), - position: Some(serialize_anchor(&position)), - trigger, - version: serialize_version(&buffer.read(cx).version()), - }; - cx.background_spawn(async move { - client - .request(request) - .await? - .transaction - .map(language::proto::deserialize_transaction) - .transpose() - }) - } else if let Some(local) = self.as_local_mut() { - let buffer_id = buffer.read(cx).remote_id(); - local.buffers_being_formatted.insert(buffer_id); - cx.spawn(async move |this, cx| { - let _cleanup = defer({ - let this = this.clone(); - let mut cx = cx.clone(); - move || { - this.update(&mut cx, |this, _| { - if let Some(local) = this.as_local_mut() { - local.buffers_being_formatted.remove(&buffer_id); - } - }) - .ok(); - } - }); - - buffer - .update(cx, |buffer, _| { - buffer.wait_for_edits(Some(position.timestamp)) - })? - .await?; - this.update(cx, |this, cx| { - let position = position.to_point_utf16(buffer.read(cx)); - this.on_type_format(buffer, position, trigger, false, cx) - })? - .await - }) - } else { - Task::ready(Err(anyhow!("No upstream client or local language server"))) - } - } - - pub fn on_type_format( - &mut self, - buffer: Entity, - position: T, - trigger: String, - push_to_history: bool, - cx: &mut Context, - ) -> Task>> { - let position = position.to_point_utf16(buffer.read(cx)); - self.on_type_format_impl(buffer, position, trigger, push_to_history, cx) - } - - fn on_type_format_impl( - &mut self, - buffer: Entity, - position: PointUtf16, - trigger: String, - push_to_history: bool, - cx: &mut Context, - ) -> Task>> { - let options = buffer.update(cx, |buffer, cx| { - lsp_command::lsp_formatting_options( - language_settings( - buffer.language_at(position).map(|l| l.name()), - buffer.file(), - cx, - ) - .as_ref(), - ) - }); - - cx.spawn(async move |this, cx| { - if let Some(waiter) = - buffer.update(cx, |buffer, _| buffer.wait_for_autoindent_applied())? - { - waiter.await?; - } - cx.update(|cx| { - this.update(cx, |this, cx| { - this.request_lsp( - buffer.clone(), - LanguageServerToQuery::FirstCapable, - OnTypeFormatting { - position, - trigger, - options, - push_to_history, - }, - cx, - ) - }) - })?? - .await - }) - } - - pub fn definitions( - &mut self, - buffer: &Entity, - position: PointUtf16, - cx: &mut Context, - ) -> Task>>> { - if let Some((upstream_client, project_id)) = self.upstream_client() { - let request = GetDefinitions { position }; - if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(None)); - } - let request_task = upstream_client.request_lsp( - project_id, - None, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(project_id, buffer.read(cx)), - ); - let buffer = buffer.clone(); - cx.spawn(async move |weak_lsp_store, cx| { - let Some(lsp_store) = weak_lsp_store.upgrade() else { - return Ok(None); - }; - let Some(responses) = request_task.await? else { - return Ok(None); - }; - let actions = join_all(responses.payload.into_iter().map(|response| { - GetDefinitions { position }.response_from_proto( - response.response, - lsp_store.clone(), - buffer.clone(), - cx.clone(), - ) - })) - .await; - - Ok(Some( - actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect(), - )) - }) - } else { - let definitions_task = self.request_multiple_lsp_locally( - buffer, - Some(position), - GetDefinitions { position }, - cx, - ); - cx.background_spawn(async move { - Ok(Some( - definitions_task - .await - .into_iter() - .flat_map(|(_, definitions)| definitions) - .dedup() - .collect(), - )) - }) - } - } - - pub fn declarations( - &mut self, - buffer: &Entity, - position: PointUtf16, - cx: &mut Context, - ) -> Task>>> { - if let Some((upstream_client, project_id)) = self.upstream_client() { - let request = GetDeclarations { position }; - if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(None)); - } - let request_task = upstream_client.request_lsp( - project_id, - None, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(project_id, buffer.read(cx)), - ); - let buffer = buffer.clone(); - cx.spawn(async move |weak_lsp_store, cx| { - let Some(lsp_store) = weak_lsp_store.upgrade() else { - return Ok(None); - }; - let Some(responses) = request_task.await? else { - return Ok(None); - }; - let actions = join_all(responses.payload.into_iter().map(|response| { - GetDeclarations { position }.response_from_proto( - response.response, - lsp_store.clone(), - buffer.clone(), - cx.clone(), - ) - })) - .await; - - Ok(Some( - actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect(), - )) - }) - } else { - let declarations_task = self.request_multiple_lsp_locally( - buffer, - Some(position), - GetDeclarations { position }, - cx, - ); - cx.background_spawn(async move { - Ok(Some( - declarations_task - .await - .into_iter() - .flat_map(|(_, declarations)| declarations) - .dedup() - .collect(), - )) - }) - } - } - - pub fn type_definitions( - &mut self, - buffer: &Entity, - position: PointUtf16, - cx: &mut Context, - ) -> Task>>> { - if let Some((upstream_client, project_id)) = self.upstream_client() { - let request = GetTypeDefinitions { position }; - if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(None)); - } - let request_task = upstream_client.request_lsp( - project_id, - None, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(project_id, buffer.read(cx)), - ); - let buffer = buffer.clone(); - cx.spawn(async move |weak_lsp_store, cx| { - let Some(lsp_store) = weak_lsp_store.upgrade() else { - return Ok(None); - }; - let Some(responses) = request_task.await? else { - return Ok(None); - }; - let actions = join_all(responses.payload.into_iter().map(|response| { - GetTypeDefinitions { position }.response_from_proto( - response.response, - lsp_store.clone(), - buffer.clone(), - cx.clone(), - ) - })) - .await; - - Ok(Some( - actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect(), - )) - }) - } else { - let type_definitions_task = self.request_multiple_lsp_locally( - buffer, - Some(position), - GetTypeDefinitions { position }, - cx, - ); - cx.background_spawn(async move { - Ok(Some( - type_definitions_task - .await - .into_iter() - .flat_map(|(_, type_definitions)| type_definitions) - .dedup() - .collect(), - )) - }) - } - } - - pub fn implementations( - &mut self, - buffer: &Entity, - position: PointUtf16, - cx: &mut Context, - ) -> Task>>> { - if let Some((upstream_client, project_id)) = self.upstream_client() { - let request = GetImplementations { position }; - if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(None)); - } - let request_task = upstream_client.request_lsp( - project_id, - None, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(project_id, buffer.read(cx)), - ); - let buffer = buffer.clone(); - cx.spawn(async move |weak_lsp_store, cx| { - let Some(lsp_store) = weak_lsp_store.upgrade() else { - return Ok(None); - }; - let Some(responses) = request_task.await? else { - return Ok(None); - }; - let actions = join_all(responses.payload.into_iter().map(|response| { - GetImplementations { position }.response_from_proto( - response.response, - lsp_store.clone(), - buffer.clone(), - cx.clone(), - ) - })) - .await; - - Ok(Some( - actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect(), - )) - }) - } else { - let implementations_task = self.request_multiple_lsp_locally( - buffer, - Some(position), - GetImplementations { position }, - cx, - ); - cx.background_spawn(async move { - Ok(Some( - implementations_task - .await - .into_iter() - .flat_map(|(_, implementations)| implementations) - .dedup() - .collect(), - )) - }) - } - } - - pub fn references( - &mut self, - buffer: &Entity, - position: PointUtf16, - cx: &mut Context, - ) -> Task>>> { - if let Some((upstream_client, project_id)) = self.upstream_client() { - let request = GetReferences { position }; - if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(None)); - } - - let request_task = upstream_client.request_lsp( - project_id, - None, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(project_id, buffer.read(cx)), - ); - let buffer = buffer.clone(); - cx.spawn(async move |weak_lsp_store, cx| { - let Some(lsp_store) = weak_lsp_store.upgrade() else { - return Ok(None); - }; - let Some(responses) = request_task.await? else { - return Ok(None); - }; - - let locations = join_all(responses.payload.into_iter().map(|lsp_response| { - GetReferences { position }.response_from_proto( - lsp_response.response, - lsp_store.clone(), - buffer.clone(), - cx.clone(), - ) - })) - .await - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .dedup() - .collect(); - Ok(Some(locations)) - }) - } else { - let references_task = self.request_multiple_lsp_locally( - buffer, - Some(position), - GetReferences { position }, - cx, - ); - cx.background_spawn(async move { - Ok(Some( - references_task - .await - .into_iter() - .flat_map(|(_, references)| references) - .dedup() - .collect(), - )) - }) - } - } - - pub fn code_actions( - &mut self, - buffer: &Entity, - range: Range, - kinds: Option>, - cx: &mut Context, - ) -> Task>>> { - if let Some((upstream_client, project_id)) = self.upstream_client() { - let request = GetCodeActions { - range: range.clone(), - kinds: kinds.clone(), - }; - if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(None)); - } - let request_task = upstream_client.request_lsp( - project_id, - None, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(project_id, buffer.read(cx)), - ); - let buffer = buffer.clone(); - cx.spawn(async move |weak_lsp_store, cx| { - let Some(lsp_store) = weak_lsp_store.upgrade() else { - return Ok(None); - }; - let Some(responses) = request_task.await? else { - return Ok(None); - }; - let actions = join_all(responses.payload.into_iter().map(|response| { - GetCodeActions { - range: range.clone(), - kinds: kinds.clone(), - } - .response_from_proto( - response.response, - lsp_store.clone(), - buffer.clone(), - cx.clone(), - ) - })) - .await; - - Ok(Some( - actions - .into_iter() - .collect::>>>()? - .into_iter() - .flatten() - .collect(), - )) - }) - } else { - let all_actions_task = self.request_multiple_lsp_locally( - buffer, - Some(range.start), - GetCodeActions { range, kinds }, - cx, - ); - cx.background_spawn(async move { - Ok(Some( - all_actions_task - .await - .into_iter() - .flat_map(|(_, actions)| actions) - .collect(), - )) - }) - } - } - - pub fn code_lens_actions( - &mut self, - buffer: &Entity, - cx: &mut Context, - ) -> CodeLensTask { - let version_queried_for = buffer.read(cx).version(); - let buffer_id = buffer.read(cx).remote_id(); - let existing_servers = self.as_local().map(|local| { - local - .buffers_opened_in_servers - .get(&buffer_id) - .cloned() - .unwrap_or_default() - }); - - if let Some(lsp_data) = self.current_lsp_data(buffer_id) { - if let Some(cached_lens) = &lsp_data.code_lens { - if !version_queried_for.changed_since(&lsp_data.buffer_version) { - let has_different_servers = existing_servers.is_some_and(|existing_servers| { - existing_servers != cached_lens.lens.keys().copied().collect() - }); - if !has_different_servers { - return Task::ready(Ok(Some( - cached_lens.lens.values().flatten().cloned().collect(), - ))) - .shared(); - } - } else if let Some((updating_for, running_update)) = cached_lens.update.as_ref() { - if !version_queried_for.changed_since(updating_for) { - return running_update.clone(); - } - } - } - } - - let lens_lsp_data = self - .latest_lsp_data(buffer, cx) - .code_lens - .get_or_insert_default(); - let buffer = buffer.clone(); - let query_version_queried_for = version_queried_for.clone(); - let new_task = cx - .spawn(async move |lsp_store, cx| { - cx.background_executor() - .timer(Duration::from_millis(30)) - .await; - let fetched_lens = lsp_store - .update(cx, |lsp_store, cx| lsp_store.fetch_code_lens(&buffer, cx)) - .map_err(Arc::new)? - .await - .context("fetching code lens") - .map_err(Arc::new); - let fetched_lens = match fetched_lens { - Ok(fetched_lens) => fetched_lens, - Err(e) => { - lsp_store - .update(cx, |lsp_store, _| { - if let Some(lens_lsp_data) = lsp_store - .lsp_data - .get_mut(&buffer_id) - .and_then(|lsp_data| lsp_data.code_lens.as_mut()) - { - lens_lsp_data.update = None; - } - }) - .ok(); - return Err(e); - } - }; - - lsp_store - .update(cx, |lsp_store, _| { - let lsp_data = lsp_store.current_lsp_data(buffer_id)?; - let code_lens = lsp_data.code_lens.as_mut()?; - if let Some(fetched_lens) = fetched_lens { - if lsp_data.buffer_version == query_version_queried_for { - code_lens.lens.extend(fetched_lens); - } else if !lsp_data - .buffer_version - .changed_since(&query_version_queried_for) - { - lsp_data.buffer_version = query_version_queried_for; - code_lens.lens = fetched_lens; - } - } - code_lens.update = None; - Some(code_lens.lens.values().flatten().cloned().collect()) - }) - .map_err(Arc::new) - }) - .shared(); - lens_lsp_data.update = Some((version_queried_for, new_task.clone())); - new_task - } - - fn fetch_code_lens( - &mut self, - buffer: &Entity, - cx: &mut Context, - ) -> Task>>>> { - if let Some((upstream_client, project_id)) = self.upstream_client() { - let request = GetCodeLens; - if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(None)); - } - let request_task = upstream_client.request_lsp( - project_id, - None, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(project_id, buffer.read(cx)), - ); - let buffer = buffer.clone(); - cx.spawn(async move |weak_lsp_store, cx| { - let Some(lsp_store) = weak_lsp_store.upgrade() else { - return Ok(None); - }; - let Some(responses) = request_task.await? else { - return Ok(None); - }; - - let code_lens_actions = join_all(responses.payload.into_iter().map(|response| { - let lsp_store = lsp_store.clone(); - let buffer = buffer.clone(); - let cx = cx.clone(); - async move { - ( - LanguageServerId::from_proto(response.server_id), - GetCodeLens - .response_from_proto(response.response, lsp_store, buffer, cx) - .await, - ) - } - })) - .await; - - let mut has_errors = false; - let code_lens_actions = code_lens_actions - .into_iter() - .filter_map(|(server_id, code_lens)| match code_lens { - Ok(code_lens) => Some((server_id, code_lens)), - Err(e) => { - has_errors = true; - log::error!("{e:#}"); - None - } - }) - .collect::>(); - anyhow::ensure!( - !has_errors || !code_lens_actions.is_empty(), - "Failed to fetch code lens" - ); - Ok(Some(code_lens_actions)) - }) - } else { - let code_lens_actions_task = - self.request_multiple_lsp_locally(buffer, None::, GetCodeLens, cx); - cx.background_spawn(async move { - Ok(Some(code_lens_actions_task.await.into_iter().collect())) - }) - } - } - - #[inline(never)] - pub fn completions( - &self, - buffer: &Entity, - position: PointUtf16, - context: CompletionContext, - cx: &mut Context, - ) -> Task>> { - let language_registry = self.languages.clone(); - - if let Some((upstream_client, project_id)) = self.upstream_client() { - let snapshot = buffer.read(cx).snapshot(); - let offset = position.to_offset(&snapshot); - let scope = snapshot.language_scope_at(offset); - let capable_lsps = self.all_capable_for_proto_request( - buffer, - |server_name, capabilities| { - capabilities.completion_provider.is_some() - && scope - .as_ref() - .map(|scope| scope.language_allowed(server_name)) - .unwrap_or(true) - }, - cx, - ); - if capable_lsps.is_empty() { - return Task::ready(Ok(Vec::new())); - } - - let language = buffer.read(cx).language().cloned(); - - // In the future, we should provide project guests with the names of LSP adapters, - // so that they can use the correct LSP adapter when computing labels. For now, - // guests just use the first LSP adapter associated with the buffer's language. - let lsp_adapter = language.as_ref().and_then(|language| { - language_registry - .lsp_adapters(&language.name()) - .first() - .cloned() - }); - - let buffer = buffer.clone(); - - cx.spawn(async move |this, cx| { - let requests = join_all( - capable_lsps - .into_iter() - .map(|id| { - let request = GetCompletions { - position, - context: context.clone(), - server_id: Some(id), - }; - let buffer = buffer.clone(); - let language = language.clone(); - let lsp_adapter = lsp_adapter.clone(); - let upstream_client = upstream_client.clone(); - let response = this - .update(cx, |this, cx| { - this.send_lsp_proto_request( - buffer, - upstream_client, - project_id, - request, - cx, - ) - }) - .log_err(); - async move { - let response = response?.await.log_err()?; - - let completions = populate_labels_for_completions( - response.completions, - language, - lsp_adapter, - ) - .await; - - Some(CompletionResponse { - completions, - display_options: CompletionDisplayOptions::default(), - is_incomplete: response.is_incomplete, - }) - } - }) - .collect::>(), - ); - Ok(requests.await.into_iter().flatten().collect::>()) - }) - } else if let Some(local) = self.as_local() { - let snapshot = buffer.read(cx).snapshot(); - let offset = position.to_offset(&snapshot); - let scope = snapshot.language_scope_at(offset); - let language = snapshot.language().cloned(); - let completion_settings = language_settings( - language.as_ref().map(|language| language.name()), - buffer.read(cx).file(), - cx, - ) - .completions - .clone(); - if !completion_settings.lsp { - return Task::ready(Ok(Vec::new())); - } - - let server_ids: Vec<_> = buffer.update(cx, |buffer, cx| { - local - .language_servers_for_buffer(buffer, cx) - .filter(|(_, server)| server.capabilities().completion_provider.is_some()) - .filter(|(adapter, _)| { - scope - .as_ref() - .map(|scope| scope.language_allowed(&adapter.name)) - .unwrap_or(true) - }) - .map(|(_, server)| server.server_id()) - .collect() - }); - - let buffer = buffer.clone(); - let lsp_timeout = completion_settings.lsp_fetch_timeout_ms; - let lsp_timeout = if lsp_timeout > 0 { - Some(Duration::from_millis(lsp_timeout)) - } else { - None - }; - cx.spawn(async move |this, cx| { - let mut tasks = Vec::with_capacity(server_ids.len()); - this.update(cx, |lsp_store, cx| { - for server_id in server_ids { - let lsp_adapter = lsp_store.language_server_adapter_for_id(server_id); - let lsp_timeout = lsp_timeout - .map(|lsp_timeout| cx.background_executor().timer(lsp_timeout)); - let mut timeout = cx.background_spawn(async move { - match lsp_timeout { - Some(lsp_timeout) => { - lsp_timeout.await; - true - }, - None => false, - } - }).fuse(); - let mut lsp_request = lsp_store.request_lsp( - buffer.clone(), - LanguageServerToQuery::Other(server_id), - GetCompletions { - position, - context: context.clone(), - server_id: Some(server_id), - }, - cx, - ).fuse(); - let new_task = cx.background_spawn(async move { - select_biased! { - response = lsp_request => anyhow::Ok(Some(response?)), - timeout_happened = timeout => { - if timeout_happened { - log::warn!("Fetching completions from server {server_id} timed out, timeout ms: {}", completion_settings.lsp_fetch_timeout_ms); - Ok(None) - } else { - let completions = lsp_request.await?; - Ok(Some(completions)) - } - }, - } - }); - tasks.push((lsp_adapter, new_task)); - } - })?; - - let futures = tasks.into_iter().map(async |(lsp_adapter, task)| { - let completion_response = task.await.ok()??; - let completions = populate_labels_for_completions( - completion_response.completions, - language.clone(), - lsp_adapter, - ) - .await; - Some(CompletionResponse { - completions, - display_options: CompletionDisplayOptions::default(), - is_incomplete: completion_response.is_incomplete, - }) - }); - - let responses: Vec> = join_all(futures).await; - - Ok(responses.into_iter().flatten().collect()) - }) - } else { - Task::ready(Err(anyhow!("No upstream client or local language server"))) - } - } - - pub fn resolve_completions( - &self, - buffer: Entity, - completion_indices: Vec, - completions: Rc>>, - cx: &mut Context, - ) -> Task> { - let client = self.upstream_client(); - let buffer_id = buffer.read(cx).remote_id(); - let buffer_snapshot = buffer.read(cx).snapshot(); - - if !self.check_if_capable_for_proto_request( - &buffer, - GetCompletions::can_resolve_completions, - cx, - ) { - return Task::ready(Ok(false)); - } - cx.spawn(async move |lsp_store, cx| { - let mut did_resolve = false; - if let Some((client, project_id)) = client { - for completion_index in completion_indices { - let server_id = { - let completion = &completions.borrow()[completion_index]; - completion.source.server_id() - }; - if let Some(server_id) = server_id { - if Self::resolve_completion_remote( - project_id, - server_id, - buffer_id, - completions.clone(), - completion_index, - client.clone(), - ) - .await - .log_err() - .is_some() - { - did_resolve = true; - } - } else { - resolve_word_completion( - &buffer_snapshot, - &mut completions.borrow_mut()[completion_index], - ); - } - } - } else { - for completion_index in completion_indices { - let server_id = { - let completion = &completions.borrow()[completion_index]; - completion.source.server_id() - }; - if let Some(server_id) = server_id { - let server_and_adapter = lsp_store - .read_with(cx, |lsp_store, _| { - let server = lsp_store.language_server_for_id(server_id)?; - let adapter = - lsp_store.language_server_adapter_for_id(server.server_id())?; - Some((server, adapter)) - }) - .ok() - .flatten(); - let Some((server, adapter)) = server_and_adapter else { - continue; - }; - - let resolved = Self::resolve_completion_local( - server, - completions.clone(), - completion_index, - ) - .await - .log_err() - .is_some(); - if resolved { - Self::regenerate_completion_labels( - adapter, - &buffer_snapshot, - completions.clone(), - completion_index, - ) - .await - .log_err(); - did_resolve = true; - } - } else { - resolve_word_completion( - &buffer_snapshot, - &mut completions.borrow_mut()[completion_index], - ); - } - } - } - - Ok(did_resolve) - }) - } - - async fn resolve_completion_local( - server: Arc, - completions: Rc>>, - completion_index: usize, - ) -> Result<()> { - let server_id = server.server_id(); - if !GetCompletions::can_resolve_completions(&server.capabilities()) { - return Ok(()); - } - - let request = { - let completion = &completions.borrow()[completion_index]; - match &completion.source { - CompletionSource::Lsp { - lsp_completion, - resolved, - server_id: completion_server_id, - .. - } => { - if *resolved { - return Ok(()); - } - anyhow::ensure!( - server_id == *completion_server_id, - "server_id mismatch, querying completion resolve for {server_id} but completion server id is {completion_server_id}" - ); - server.request::(*lsp_completion.clone()) - } - CompletionSource::BufferWord { .. } - | CompletionSource::Dap { .. } - | CompletionSource::Custom => { - return Ok(()); - } - } - }; - let resolved_completion = request - .await - .into_response() - .context("resolve completion")?; - - // We must not use any data such as sortText, filterText, insertText and textEdit to edit `Completion` since they are not suppose change during resolve. - // Refer: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion - - let mut completions = completions.borrow_mut(); - let completion = &mut completions[completion_index]; - if let CompletionSource::Lsp { - lsp_completion, - resolved, - server_id: completion_server_id, - .. - } = &mut completion.source - { - if *resolved { - return Ok(()); - } - anyhow::ensure!( - server_id == *completion_server_id, - "server_id mismatch, applying completion resolve for {server_id} but completion server id is {completion_server_id}" - ); - *lsp_completion = Box::new(resolved_completion); - *resolved = true; - } - Ok(()) - } - - async fn regenerate_completion_labels( - adapter: Arc, - snapshot: &BufferSnapshot, - completions: Rc>>, - completion_index: usize, - ) -> Result<()> { - let completion_item = completions.borrow()[completion_index] - .source - .lsp_completion(true) - .map(Cow::into_owned); - if let Some(lsp_documentation) = completion_item - .as_ref() - .and_then(|completion_item| completion_item.documentation.clone()) - { - let mut completions = completions.borrow_mut(); - let completion = &mut completions[completion_index]; - completion.documentation = Some(lsp_documentation.into()); - } else { - let mut completions = completions.borrow_mut(); - let completion = &mut completions[completion_index]; - completion.documentation = Some(CompletionDocumentation::Undocumented); - } - - let mut new_label = match completion_item { - Some(completion_item) => { - // NB: Zed does not have `details` inside the completion resolve capabilities, but certain language servers violate the spec and do not return `details` immediately, e.g. https://github.com/yioneko/vtsls/issues/213 - // So we have to update the label here anyway... - let language = snapshot.language(); - match language { - Some(language) => { - adapter - .labels_for_completions( - std::slice::from_ref(&completion_item), - language, - ) - .await? - } - None => Vec::new(), - } - .pop() - .flatten() - .unwrap_or_else(|| { - CodeLabel::fallback_for_completion( - &completion_item, - language.map(|language| language.as_ref()), - ) - }) - } - None => CodeLabel::plain( - completions.borrow()[completion_index].new_text.clone(), - None, - ), - }; - ensure_uniform_list_compatible_label(&mut new_label); - - let mut completions = completions.borrow_mut(); - let completion = &mut completions[completion_index]; - if completion.label.filter_text() == new_label.filter_text() { - completion.label = new_label; - } else { - log::error!( - "Resolved completion changed display label from {} to {}. \ - Refusing to apply this because it changes the fuzzy match text from {} to {}", - completion.label.text(), - new_label.text(), - completion.label.filter_text(), - new_label.filter_text() - ); - } - - Ok(()) - } - - async fn resolve_completion_remote( - project_id: u64, - server_id: LanguageServerId, - buffer_id: BufferId, - completions: Rc>>, - completion_index: usize, - client: AnyProtoClient, - ) -> Result<()> { - let lsp_completion = { - let completion = &completions.borrow()[completion_index]; - match &completion.source { - CompletionSource::Lsp { - lsp_completion, - resolved, - server_id: completion_server_id, - .. - } => { - anyhow::ensure!( - server_id == *completion_server_id, - "remote server_id mismatch, querying completion resolve for {server_id} but completion server id is {completion_server_id}" - ); - if *resolved { - return Ok(()); - } - serde_json::to_string(lsp_completion).unwrap().into_bytes() - } - CompletionSource::Custom - | CompletionSource::Dap { .. } - | CompletionSource::BufferWord { .. } => { - return Ok(()); - } - } - }; - let request = proto::ResolveCompletionDocumentation { - project_id, - language_server_id: server_id.0 as u64, - lsp_completion, - buffer_id: buffer_id.into(), - }; - - let response = client - .request(request) - .await - .context("completion documentation resolve proto request")?; - let resolved_lsp_completion = serde_json::from_slice(&response.lsp_completion)?; - - let documentation = if response.documentation.is_empty() { - CompletionDocumentation::Undocumented - } else if response.documentation_is_markdown { - CompletionDocumentation::MultiLineMarkdown(response.documentation.into()) - } else if response.documentation.lines().count() <= 1 { - CompletionDocumentation::SingleLine(response.documentation.into()) - } else { - CompletionDocumentation::MultiLinePlainText(response.documentation.into()) - }; - - let mut completions = completions.borrow_mut(); - let completion = &mut completions[completion_index]; - completion.documentation = Some(documentation); - if let CompletionSource::Lsp { - insert_range, - lsp_completion, - resolved, - server_id: completion_server_id, - lsp_defaults: _, - } = &mut completion.source - { - let completion_insert_range = response - .old_insert_start - .and_then(deserialize_anchor) - .zip(response.old_insert_end.and_then(deserialize_anchor)); - *insert_range = completion_insert_range.map(|(start, end)| start..end); - - if *resolved { - return Ok(()); - } - anyhow::ensure!( - server_id == *completion_server_id, - "remote server_id mismatch, applying completion resolve for {server_id} but completion server id is {completion_server_id}" - ); - *lsp_completion = Box::new(resolved_lsp_completion); - *resolved = true; - } - - let replace_range = response - .old_replace_start - .and_then(deserialize_anchor) - .zip(response.old_replace_end.and_then(deserialize_anchor)); - if let Some((old_replace_start, old_replace_end)) = replace_range - && !response.new_text.is_empty() - { - completion.new_text = response.new_text; - completion.replace_range = old_replace_start..old_replace_end; - } - - Ok(()) - } - - pub fn apply_additional_edits_for_completion( - &self, - buffer_handle: Entity, - completions: Rc>>, - completion_index: usize, - push_to_history: bool, - cx: &mut Context, - ) -> Task>> { - if let Some((client, project_id)) = self.upstream_client() { - let buffer = buffer_handle.read(cx); - let buffer_id = buffer.remote_id(); - cx.spawn(async move |_, cx| { - let request = { - let completion = completions.borrow()[completion_index].clone(); - proto::ApplyCompletionAdditionalEdits { - project_id, - buffer_id: buffer_id.into(), - completion: Some(Self::serialize_completion(&CoreCompletion { - replace_range: completion.replace_range, - new_text: completion.new_text, - source: completion.source, - })), - } - }; - - if let Some(transaction) = client.request(request).await?.transaction { - let transaction = language::proto::deserialize_transaction(transaction)?; - buffer_handle - .update(cx, |buffer, _| { - buffer.wait_for_edits(transaction.edit_ids.iter().copied()) - })? - .await?; - if push_to_history { - buffer_handle.update(cx, |buffer, _| { - buffer.push_transaction(transaction.clone(), Instant::now()); - buffer.finalize_last_transaction(); - })?; - } - Ok(Some(transaction)) - } else { - Ok(None) - } - }) - } else { - let Some(server) = buffer_handle.update(cx, |buffer, cx| { - let completion = &completions.borrow()[completion_index]; - let server_id = completion.source.server_id()?; - Some( - self.language_server_for_local_buffer(buffer, server_id, cx)? - .1 - .clone(), - ) - }) else { - return Task::ready(Ok(None)); - }; - - cx.spawn(async move |this, cx| { - Self::resolve_completion_local( - server.clone(), - completions.clone(), - completion_index, - ) - .await - .context("resolving completion")?; - let completion = completions.borrow()[completion_index].clone(); - let additional_text_edits = completion - .source - .lsp_completion(true) - .as_ref() - .and_then(|lsp_completion| lsp_completion.additional_text_edits.clone()); - if let Some(edits) = additional_text_edits { - let edits = this - .update(cx, |this, cx| { - this.as_local_mut().unwrap().edits_from_lsp( - &buffer_handle, - edits, - server.server_id(), - None, - cx, - ) - })? - .await?; - - buffer_handle.update(cx, |buffer, cx| { - buffer.finalize_last_transaction(); - buffer.start_transaction(); - - for (range, text) in edits { - let primary = &completion.replace_range; - - // Special case: if both ranges start at the very beginning of the file (line 0, column 0), - // and the primary completion is just an insertion (empty range), then this is likely - // an auto-import scenario and should not be considered overlapping - // https://github.com/zed-industries/zed/issues/26136 - let is_file_start_auto_import = { - let snapshot = buffer.snapshot(); - let primary_start_point = primary.start.to_point(&snapshot); - let range_start_point = range.start.to_point(&snapshot); - - let result = primary_start_point.row == 0 - && primary_start_point.column == 0 - && range_start_point.row == 0 - && range_start_point.column == 0; - - result - }; - - let has_overlap = if is_file_start_auto_import { - false - } else { - let start_within = primary.start.cmp(&range.start, buffer).is_le() - && primary.end.cmp(&range.start, buffer).is_ge(); - let end_within = range.start.cmp(&primary.end, buffer).is_le() - && range.end.cmp(&primary.end, buffer).is_ge(); - let result = start_within || end_within; - result - }; - - //Skip additional edits which overlap with the primary completion edit - //https://github.com/zed-industries/zed/pull/1871 - if !has_overlap { - buffer.edit([(range, text)], None, cx); - } - } - - let transaction = if buffer.end_transaction(cx).is_some() { - let transaction = buffer.finalize_last_transaction().unwrap().clone(); - if !push_to_history { - buffer.forget_transaction(transaction.id); - } - Some(transaction) - } else { - None - }; - Ok(transaction) - })? - } else { - Ok(None) - } - }) - } - } - - pub fn pull_diagnostics( - &mut self, - buffer: Entity, - cx: &mut Context, - ) -> Task>>> { - let buffer_id = buffer.read(cx).remote_id(); - - if let Some((client, upstream_project_id)) = self.upstream_client() { - let mut suitable_capabilities = None; - // Are we capable for proto request? - let any_server_has_diagnostics_provider = self.check_if_capable_for_proto_request( - &buffer, - |capabilities| { - if let Some(caps) = &capabilities.diagnostic_provider { - suitable_capabilities = Some(caps.clone()); - true - } else { - false - } - }, - cx, - ); - // We don't really care which caps are passed into the request, as they're ignored by RPC anyways. - let Some(dynamic_caps) = suitable_capabilities else { - return Task::ready(Ok(None)); - }; - assert!(any_server_has_diagnostics_provider); - - let identifier = buffer_diagnostic_identifier(&dynamic_caps); - let request = GetDocumentDiagnostics { - previous_result_id: None, - identifier, - registration_id: None, - }; - let request_task = client.request_lsp( - upstream_project_id, - None, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(upstream_project_id, buffer.read(cx)), - ); - cx.background_spawn(async move { - // Proto requests cause the diagnostics to be pulled from language server(s) on the local side - // and then, buffer state updated with the diagnostics received, which will be later propagated to the client. - // Do not attempt to further process the dummy responses here. - let _response = request_task.await?; - Ok(None) - }) - } else { - let servers = buffer.update(cx, |buffer, cx| { - self.running_language_servers_for_local_buffer(buffer, cx) - .map(|(_, server)| server.clone()) - .collect::>() - }); - - let pull_diagnostics = servers - .into_iter() - .flat_map(|server| { - let result = maybe!({ - let local = self.as_local()?; - let server_id = server.server_id(); - let providers_with_identifiers = local - .language_server_dynamic_registrations - .get(&server_id) - .into_iter() - .flat_map(|registrations| registrations.diagnostics.clone()) - .collect::>(); - Some( - providers_with_identifiers - .into_iter() - .map(|(registration_id, dynamic_caps)| { - let identifier = buffer_diagnostic_identifier(&dynamic_caps); - let registration_id = registration_id.map(SharedString::from); - let result_id = self.result_id_for_buffer_pull( - server_id, - buffer_id, - ®istration_id, - cx, - ); - self.request_lsp( - buffer.clone(), - LanguageServerToQuery::Other(server_id), - GetDocumentDiagnostics { - previous_result_id: result_id, - registration_id, - identifier, - }, - cx, - ) - }) - .collect::>(), - ) - }); - - result.unwrap_or_default() - }) - .collect::>(); - - cx.background_spawn(async move { - let mut responses = Vec::new(); - for diagnostics in join_all(pull_diagnostics).await { - responses.extend(diagnostics?); - } - Ok(Some(responses)) - }) - } - } - - pub fn applicable_inlay_chunks( - &mut self, - buffer: &Entity, - ranges: &[Range], - cx: &mut Context, - ) -> Vec> { - self.latest_lsp_data(buffer, cx) - .inlay_hints - .applicable_chunks(ranges) - .map(|chunk| chunk.row_range()) - .collect() - } - - pub fn invalidate_inlay_hints<'a>( - &'a mut self, - for_buffers: impl IntoIterator + 'a, - ) { - for buffer_id in for_buffers { - if let Some(lsp_data) = self.lsp_data.get_mut(buffer_id) { - lsp_data.inlay_hints.clear(); - } - } - } - - pub fn inlay_hints( - &mut self, - invalidate: InvalidationStrategy, - buffer: Entity, - ranges: Vec>, - known_chunks: Option<(clock::Global, HashSet>)>, - cx: &mut Context, - ) -> HashMap, Task>> { - let next_hint_id = self.next_hint_id.clone(); - let lsp_data = self.latest_lsp_data(&buffer, cx); - let query_version = lsp_data.buffer_version.clone(); - let mut lsp_refresh_requested = false; - let for_server = if let InvalidationStrategy::RefreshRequested { - server_id, - request_id, - } = invalidate - { - let invalidated = lsp_data - .inlay_hints - .invalidate_for_server_refresh(server_id, request_id); - lsp_refresh_requested = invalidated; - Some(server_id) - } else { - None - }; - let existing_inlay_hints = &mut lsp_data.inlay_hints; - let known_chunks = known_chunks - .filter(|(known_version, _)| !lsp_data.buffer_version.changed_since(known_version)) - .map(|(_, known_chunks)| known_chunks) - .unwrap_or_default(); - - let mut hint_fetch_tasks = Vec::new(); - let mut cached_inlay_hints = None; - let mut ranges_to_query = None; - let applicable_chunks = existing_inlay_hints - .applicable_chunks(ranges.as_slice()) - .filter(|chunk| !known_chunks.contains(&chunk.row_range())) - .collect::>(); - if applicable_chunks.is_empty() { - return HashMap::default(); - } - - for row_chunk in applicable_chunks { - match ( - existing_inlay_hints - .cached_hints(&row_chunk) - .filter(|_| !lsp_refresh_requested) - .cloned(), - existing_inlay_hints - .fetched_hints(&row_chunk) - .as_ref() - .filter(|_| !lsp_refresh_requested) - .cloned(), - ) { - (None, None) => { - let Some(chunk_range) = existing_inlay_hints.chunk_range(row_chunk) else { - continue; - }; - ranges_to_query - .get_or_insert_with(Vec::new) - .push((row_chunk, chunk_range)); - } - (None, Some(fetched_hints)) => hint_fetch_tasks.push((row_chunk, fetched_hints)), - (Some(cached_hints), None) => { - for (server_id, cached_hints) in cached_hints { - if for_server.is_none_or(|for_server| for_server == server_id) { - cached_inlay_hints - .get_or_insert_with(HashMap::default) - .entry(row_chunk.row_range()) - .or_insert_with(HashMap::default) - .entry(server_id) - .or_insert_with(Vec::new) - .extend(cached_hints); - } - } - } - (Some(cached_hints), Some(fetched_hints)) => { - hint_fetch_tasks.push((row_chunk, fetched_hints)); - for (server_id, cached_hints) in cached_hints { - if for_server.is_none_or(|for_server| for_server == server_id) { - cached_inlay_hints - .get_or_insert_with(HashMap::default) - .entry(row_chunk.row_range()) - .or_insert_with(HashMap::default) - .entry(server_id) - .or_insert_with(Vec::new) - .extend(cached_hints); - } - } - } - } - } - - if hint_fetch_tasks.is_empty() - && ranges_to_query - .as_ref() - .is_none_or(|ranges| ranges.is_empty()) - && let Some(cached_inlay_hints) = cached_inlay_hints - { - cached_inlay_hints - .into_iter() - .map(|(row_chunk, hints)| (row_chunk, Task::ready(Ok(hints)))) - .collect() - } else { - for (chunk, range_to_query) in ranges_to_query.into_iter().flatten() { - let next_hint_id = next_hint_id.clone(); - let buffer = buffer.clone(); - let query_version = query_version.clone(); - let new_inlay_hints = cx - .spawn(async move |lsp_store, cx| { - let new_fetch_task = lsp_store.update(cx, |lsp_store, cx| { - lsp_store.fetch_inlay_hints(for_server, &buffer, range_to_query, cx) - })?; - new_fetch_task - .await - .and_then(|new_hints_by_server| { - lsp_store.update(cx, |lsp_store, cx| { - let lsp_data = lsp_store.latest_lsp_data(&buffer, cx); - let update_cache = lsp_data.buffer_version == query_version; - if new_hints_by_server.is_empty() { - if update_cache { - lsp_data.inlay_hints.invalidate_for_chunk(chunk); - } - HashMap::default() - } else { - new_hints_by_server - .into_iter() - .map(|(server_id, new_hints)| { - let new_hints = new_hints - .into_iter() - .map(|new_hint| { - ( - InlayId::Hint(next_hint_id.fetch_add( - 1, - atomic::Ordering::AcqRel, - )), - new_hint, - ) - }) - .collect::>(); - if update_cache { - lsp_data.inlay_hints.insert_new_hints( - chunk, - server_id, - new_hints.clone(), - ); - } - (server_id, new_hints) - }) - .collect() - } - }) - }) - .map_err(Arc::new) - }) - .shared(); - - let fetch_task = lsp_data.inlay_hints.fetched_hints(&chunk); - *fetch_task = Some(new_inlay_hints.clone()); - hint_fetch_tasks.push((chunk, new_inlay_hints)); - } - - cached_inlay_hints - .unwrap_or_default() - .into_iter() - .map(|(row_chunk, hints)| (row_chunk, Task::ready(Ok(hints)))) - .chain(hint_fetch_tasks.into_iter().map(|(chunk, hints_fetch)| { - ( - chunk.row_range(), - cx.spawn(async move |_, _| { - hints_fetch.await.map_err(|e| { - if e.error_code() != ErrorCode::Internal { - anyhow!(e.error_code()) - } else { - anyhow!("{e:#}") - } - }) - }), - ) - })) - .collect() - } - } - - fn fetch_inlay_hints( - &mut self, - for_server: Option, - buffer: &Entity, - range: Range, - cx: &mut Context, - ) -> Task>>> { - let request = InlayHints { - range: range.clone(), - }; - if let Some((upstream_client, project_id)) = self.upstream_client() { - if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(HashMap::default())); - } - let request_task = upstream_client.request_lsp( - project_id, - for_server.map(|id| id.to_proto()), - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(project_id, buffer.read(cx)), - ); - let buffer = buffer.clone(); - cx.spawn(async move |weak_lsp_store, cx| { - let Some(lsp_store) = weak_lsp_store.upgrade() else { - return Ok(HashMap::default()); - }; - let Some(responses) = request_task.await? else { - return Ok(HashMap::default()); - }; - - let inlay_hints = join_all(responses.payload.into_iter().map(|response| { - let lsp_store = lsp_store.clone(); - let buffer = buffer.clone(); - let cx = cx.clone(); - let request = request.clone(); - async move { - ( - LanguageServerId::from_proto(response.server_id), - request - .response_from_proto(response.response, lsp_store, buffer, cx) - .await, - ) - } - })) - .await; - - let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?; - let mut has_errors = false; - let inlay_hints = inlay_hints - .into_iter() - .filter_map(|(server_id, inlay_hints)| match inlay_hints { - Ok(inlay_hints) => Some((server_id, inlay_hints)), - Err(e) => { - has_errors = true; - log::error!("{e:#}"); - None - } - }) - .map(|(server_id, mut new_hints)| { - new_hints.retain(|hint| { - hint.position.is_valid(&buffer_snapshot) - && range.start.is_valid(&buffer_snapshot) - && range.end.is_valid(&buffer_snapshot) - && hint.position.cmp(&range.start, &buffer_snapshot).is_ge() - && hint.position.cmp(&range.end, &buffer_snapshot).is_lt() - }); - (server_id, new_hints) - }) - .collect::>(); - anyhow::ensure!( - !has_errors || !inlay_hints.is_empty(), - "Failed to fetch inlay hints" - ); - Ok(inlay_hints) - }) - } else { - let inlay_hints_task = match for_server { - Some(server_id) => { - let server_task = self.request_lsp( - buffer.clone(), - LanguageServerToQuery::Other(server_id), - request, - cx, - ); - cx.background_spawn(async move { - let mut responses = Vec::new(); - match server_task.await { - Ok(response) => responses.push((server_id, response)), - // rust-analyzer likes to error with this when its still loading up - Err(e) if format!("{e:#}").ends_with("content modified") => (), - Err(e) => log::error!( - "Error handling response for inlay hints request: {e:#}" - ), - } - responses - }) - } - None => self.request_multiple_lsp_locally(buffer, None::, request, cx), - }; - let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); - cx.background_spawn(async move { - Ok(inlay_hints_task - .await - .into_iter() - .map(|(server_id, mut new_hints)| { - new_hints.retain(|hint| { - hint.position.is_valid(&buffer_snapshot) - && range.start.is_valid(&buffer_snapshot) - && range.end.is_valid(&buffer_snapshot) - && hint.position.cmp(&range.start, &buffer_snapshot).is_ge() - && hint.position.cmp(&range.end, &buffer_snapshot).is_lt() - }); - (server_id, new_hints) - }) - .collect()) - }) - } - } - - pub fn pull_diagnostics_for_buffer( - &mut self, - buffer: Entity, - cx: &mut Context, - ) -> Task> { - let diagnostics = self.pull_diagnostics(buffer, cx); - cx.spawn(async move |lsp_store, cx| { - let Some(diagnostics) = diagnostics.await.context("pulling diagnostics")? else { - return Ok(()); - }; - lsp_store.update(cx, |lsp_store, cx| { - if lsp_store.as_local().is_none() { - return; - } - - let mut unchanged_buffers = HashMap::default(); - let server_diagnostics_updates = diagnostics - .into_iter() - .filter_map(|diagnostics_set| match diagnostics_set { - LspPullDiagnostics::Response { - server_id, - uri, - diagnostics, - registration_id, - } => Some((server_id, uri, diagnostics, registration_id)), - LspPullDiagnostics::Default => None, - }) - .fold( - HashMap::default(), - |mut acc, (server_id, uri, diagnostics, new_registration_id)| { - let (result_id, diagnostics) = match diagnostics { - PulledDiagnostics::Unchanged { result_id } => { - unchanged_buffers - .entry(new_registration_id.clone()) - .or_insert_with(HashSet::default) - .insert(uri.clone()); - (Some(result_id), Vec::new()) - } - PulledDiagnostics::Changed { - result_id, - diagnostics, - } => (result_id, diagnostics), - }; - let disk_based_sources = Cow::Owned( - lsp_store - .language_server_adapter_for_id(server_id) - .as_ref() - .map(|adapter| adapter.disk_based_diagnostic_sources.as_slice()) - .unwrap_or(&[]) - .to_vec(), - ); - acc.entry(server_id) - .or_insert_with(HashMap::default) - .entry(new_registration_id.clone()) - .or_insert_with(Vec::new) - .push(DocumentDiagnosticsUpdate { - server_id, - diagnostics: lsp::PublishDiagnosticsParams { - uri, - diagnostics, - version: None, - }, - result_id, - disk_based_sources, - registration_id: new_registration_id, - }); - acc - }, - ); - - for diagnostic_updates in server_diagnostics_updates.into_values() { - for (registration_id, diagnostic_updates) in diagnostic_updates { - lsp_store - .merge_lsp_diagnostics( - DiagnosticSourceKind::Pulled, - diagnostic_updates, - |document_uri, old_diagnostic, _| match old_diagnostic.source_kind { - DiagnosticSourceKind::Pulled => { - old_diagnostic.registration_id != registration_id - || unchanged_buffers - .get(&old_diagnostic.registration_id) - .is_some_and(|unchanged_buffers| { - unchanged_buffers.contains(&document_uri) - }) - } - DiagnosticSourceKind::Other | DiagnosticSourceKind::Pushed => { - true - } - }, - cx, - ) - .log_err(); - } - } - }) - }) - } - - pub fn document_colors( - &mut self, - known_cache_version: Option, - buffer: Entity, - cx: &mut Context, - ) -> Option { - let version_queried_for = buffer.read(cx).version(); - let buffer_id = buffer.read(cx).remote_id(); - - let current_language_servers = self.as_local().map(|local| { - local - .buffers_opened_in_servers - .get(&buffer_id) - .cloned() - .unwrap_or_default() - }); - - if let Some(lsp_data) = self.current_lsp_data(buffer_id) { - if let Some(cached_colors) = &lsp_data.document_colors { - if !version_queried_for.changed_since(&lsp_data.buffer_version) { - let has_different_servers = - current_language_servers.is_some_and(|current_language_servers| { - current_language_servers - != cached_colors.colors.keys().copied().collect() - }); - if !has_different_servers { - let cache_version = cached_colors.cache_version; - if Some(cache_version) == known_cache_version { - return None; - } else { - return Some( - Task::ready(Ok(DocumentColors { - colors: cached_colors - .colors - .values() - .flatten() - .cloned() - .collect(), - cache_version: Some(cache_version), - })) - .shared(), - ); - } - } - } - } - } - - let color_lsp_data = self - .latest_lsp_data(&buffer, cx) - .document_colors - .get_or_insert_default(); - if let Some((updating_for, running_update)) = &color_lsp_data.colors_update - && !version_queried_for.changed_since(updating_for) - { - return Some(running_update.clone()); - } - let buffer_version_queried_for = version_queried_for.clone(); - let new_task = cx - .spawn(async move |lsp_store, cx| { - cx.background_executor() - .timer(Duration::from_millis(30)) - .await; - let fetched_colors = lsp_store - .update(cx, |lsp_store, cx| { - lsp_store.fetch_document_colors_for_buffer(&buffer, cx) - })? - .await - .context("fetching document colors") - .map_err(Arc::new); - let fetched_colors = match fetched_colors { - Ok(fetched_colors) => { - if Some(true) - == buffer - .update(cx, |buffer, _| { - buffer.version() != buffer_version_queried_for - }) - .ok() - { - return Ok(DocumentColors::default()); - } - fetched_colors - } - Err(e) => { - lsp_store - .update(cx, |lsp_store, _| { - if let Some(lsp_data) = lsp_store.lsp_data.get_mut(&buffer_id) { - if let Some(document_colors) = &mut lsp_data.document_colors { - document_colors.colors_update = None; - } - } - }) - .ok(); - return Err(e); - } - }; - - lsp_store - .update(cx, |lsp_store, cx| { - let lsp_data = lsp_store.latest_lsp_data(&buffer, cx); - let lsp_colors = lsp_data.document_colors.get_or_insert_default(); - - if let Some(fetched_colors) = fetched_colors { - if lsp_data.buffer_version == buffer_version_queried_for { - lsp_colors.colors.extend(fetched_colors); - lsp_colors.cache_version += 1; - } else if !lsp_data - .buffer_version - .changed_since(&buffer_version_queried_for) - { - lsp_data.buffer_version = buffer_version_queried_for; - lsp_colors.colors = fetched_colors; - lsp_colors.cache_version += 1; - } - } - lsp_colors.colors_update = None; - let colors = lsp_colors - .colors - .values() - .flatten() - .cloned() - .collect::>(); - DocumentColors { - colors, - cache_version: Some(lsp_colors.cache_version), - } - }) - .map_err(Arc::new) - }) - .shared(); - color_lsp_data.colors_update = Some((version_queried_for, new_task.clone())); - Some(new_task) - } - - fn fetch_document_colors_for_buffer( - &mut self, - buffer: &Entity, - cx: &mut Context, - ) -> Task>>>> { - if let Some((client, project_id)) = self.upstream_client() { - let request = GetDocumentColor {}; - if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(Ok(None)); - } - - let request_task = client.request_lsp( - project_id, - None, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(project_id, buffer.read(cx)), - ); - let buffer = buffer.clone(); - cx.spawn(async move |lsp_store, cx| { - let Some(lsp_store) = lsp_store.upgrade() else { - return Ok(None); - }; - let colors = join_all( - request_task - .await - .log_err() - .flatten() - .map(|response| response.payload) - .unwrap_or_default() - .into_iter() - .map(|color_response| { - let response = request.response_from_proto( - color_response.response, - lsp_store.clone(), - buffer.clone(), - cx.clone(), - ); - async move { - ( - LanguageServerId::from_proto(color_response.server_id), - response.await.log_err().unwrap_or_default(), - ) - } - }), - ) - .await - .into_iter() - .fold(HashMap::default(), |mut acc, (server_id, colors)| { - acc.entry(server_id) - .or_insert_with(HashSet::default) - .extend(colors); - acc - }); - Ok(Some(colors)) - }) - } else { - let document_colors_task = - self.request_multiple_lsp_locally(buffer, None::, GetDocumentColor, cx); - cx.background_spawn(async move { - Ok(Some( - document_colors_task - .await - .into_iter() - .fold(HashMap::default(), |mut acc, (server_id, colors)| { - acc.entry(server_id) - .or_insert_with(HashSet::default) - .extend(colors); - acc - }) - .into_iter() - .collect(), - )) - }) - } - } - - pub fn signature_help( - &mut self, - buffer: &Entity, - position: T, - cx: &mut Context, - ) -> Task>> { - let position = position.to_point_utf16(buffer.read(cx)); - - if let Some((client, upstream_project_id)) = self.upstream_client() { - let request = GetSignatureHelp { position }; - if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(None); - } - let request_task = client.request_lsp( - upstream_project_id, - None, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(upstream_project_id, buffer.read(cx)), - ); - let buffer = buffer.clone(); - cx.spawn(async move |weak_lsp_store, cx| { - let lsp_store = weak_lsp_store.upgrade()?; - let signatures = join_all( - request_task - .await - .log_err() - .flatten() - .map(|response| response.payload) - .unwrap_or_default() - .into_iter() - .map(|response| { - let response = GetSignatureHelp { position }.response_from_proto( - response.response, - lsp_store.clone(), - buffer.clone(), - cx.clone(), - ); - async move { response.await.log_err().flatten() } - }), - ) - .await - .into_iter() - .flatten() - .collect(); - Some(signatures) - }) - } else { - let all_actions_task = self.request_multiple_lsp_locally( - buffer, - Some(position), - GetSignatureHelp { position }, - cx, - ); - cx.background_spawn(async move { - Some( - all_actions_task - .await - .into_iter() - .flat_map(|(_, actions)| actions) - .collect::>(), - ) - }) - } - } - - pub fn hover( - &mut self, - buffer: &Entity, - position: PointUtf16, - cx: &mut Context, - ) -> Task>> { - if let Some((client, upstream_project_id)) = self.upstream_client() { - let request = GetHover { position }; - if !self.is_capable_for_proto_request(buffer, &request, cx) { - return Task::ready(None); - } - let request_task = client.request_lsp( - upstream_project_id, - None, - LSP_REQUEST_TIMEOUT, - cx.background_executor().clone(), - request.to_proto(upstream_project_id, buffer.read(cx)), - ); - let buffer = buffer.clone(); - cx.spawn(async move |weak_lsp_store, cx| { - let lsp_store = weak_lsp_store.upgrade()?; - let hovers = join_all( - request_task - .await - .log_err() - .flatten() - .map(|response| response.payload) - .unwrap_or_default() - .into_iter() - .map(|response| { - let response = GetHover { position }.response_from_proto( - response.response, - lsp_store.clone(), - buffer.clone(), - cx.clone(), - ); - async move { - response - .await - .log_err() - .flatten() - .and_then(remove_empty_hover_blocks) - } - }), - ) - .await - .into_iter() - .flatten() - .collect(); - Some(hovers) - }) - } else { - let all_actions_task = self.request_multiple_lsp_locally( - buffer, - Some(position), - GetHover { position }, - cx, - ); - cx.background_spawn(async move { - Some( - all_actions_task - .await - .into_iter() - .filter_map(|(_, hover)| remove_empty_hover_blocks(hover?)) - .collect::>(), - ) - }) - } - } - - pub fn symbols(&self, query: &str, cx: &mut Context) -> Task>> { - let language_registry = self.languages.clone(); - - if let Some((upstream_client, project_id)) = self.upstream_client().as_ref() { - let request = upstream_client.request(proto::GetProjectSymbols { - project_id: *project_id, - query: query.to_string(), - }); - cx.foreground_executor().spawn(async move { - let response = request.await?; - let mut symbols = Vec::new(); - let core_symbols = response - .symbols - .into_iter() - .filter_map(|symbol| Self::deserialize_symbol(symbol).log_err()) - .collect::>(); - populate_labels_for_symbols(core_symbols, &language_registry, None, &mut symbols) - .await; - Ok(symbols) - }) - } else if let Some(local) = self.as_local() { - struct WorkspaceSymbolsResult { - server_id: LanguageServerId, - lsp_adapter: Arc, - worktree: WeakEntity, - lsp_symbols: Vec<(String, SymbolKind, lsp::Location)>, - } - - let mut requests = Vec::new(); - let mut requested_servers = BTreeSet::new(); - for (seed, state) in local.language_server_ids.iter() { - let Some(worktree_handle) = self - .worktree_store - .read(cx) - .worktree_for_id(seed.worktree_id, cx) - else { - continue; - }; - let worktree = worktree_handle.read(cx); - if !worktree.is_visible() { - continue; - } - - if !requested_servers.insert(state.id) { - continue; - } - - let (lsp_adapter, server) = match local.language_servers.get(&state.id) { - Some(LanguageServerState::Running { - adapter, server, .. - }) => (adapter.clone(), server), - - _ => continue, - }; - let supports_workspace_symbol_request = - match server.capabilities().workspace_symbol_provider { - Some(OneOf::Left(supported)) => supported, - Some(OneOf::Right(_)) => true, - None => false, - }; - if !supports_workspace_symbol_request { - continue; - } - let worktree_handle = worktree_handle.clone(); - let server_id = server.server_id(); - requests.push( - server - .request::( - lsp::WorkspaceSymbolParams { - query: query.to_string(), - ..Default::default() - }, - ) - .map(move |response| { - let lsp_symbols = response.into_response() - .context("workspace symbols request") - .log_err() - .flatten() - .map(|symbol_response| match symbol_response { - lsp::WorkspaceSymbolResponse::Flat(flat_responses) => { - flat_responses.into_iter().map(|lsp_symbol| { - (lsp_symbol.name, lsp_symbol.kind, lsp_symbol.location) - }).collect::>() - } - lsp::WorkspaceSymbolResponse::Nested(nested_responses) => { - nested_responses.into_iter().filter_map(|lsp_symbol| { - let location = match lsp_symbol.location { - OneOf::Left(location) => location, - OneOf::Right(_) => { - log::error!("Unexpected: client capabilities forbid symbol resolutions in workspace.symbol.resolveSupport"); - return None - } - }; - Some((lsp_symbol.name, lsp_symbol.kind, location)) - }).collect::>() - } - }).unwrap_or_default(); - - WorkspaceSymbolsResult { - server_id, - lsp_adapter, - worktree: worktree_handle.downgrade(), - lsp_symbols, - } - }), - ); - } - - cx.spawn(async move |this, cx| { - let responses = futures::future::join_all(requests).await; - let this = match this.upgrade() { - Some(this) => this, - None => return Ok(Vec::new()), - }; - - let mut symbols = Vec::new(); - for result in responses { - let core_symbols = this.update(cx, |this, cx| { - result - .lsp_symbols - .into_iter() - .filter_map(|(symbol_name, symbol_kind, symbol_location)| { - let abs_path = symbol_location.uri.to_file_path().ok()?; - let source_worktree = result.worktree.upgrade()?; - let source_worktree_id = source_worktree.read(cx).id(); - - let path = if let Some((tree, rel_path)) = - this.worktree_store.read(cx).find_worktree(&abs_path, cx) - { - let worktree_id = tree.read(cx).id(); - SymbolLocation::InProject(ProjectPath { - worktree_id, - path: rel_path, - }) - } else { - SymbolLocation::OutsideProject { - signature: this.symbol_signature(&abs_path), - abs_path: abs_path.into(), - } - }; - - Some(CoreSymbol { - source_language_server_id: result.server_id, - language_server_name: result.lsp_adapter.name.clone(), - source_worktree_id, - path, - kind: symbol_kind, - name: symbol_name, - range: range_from_lsp(symbol_location.range), - }) - }) - .collect() - })?; - - populate_labels_for_symbols( - core_symbols, - &language_registry, - Some(result.lsp_adapter), - &mut symbols, - ) - .await; - } - - Ok(symbols) - }) - } else { - Task::ready(Err(anyhow!("No upstream client or local language server"))) - } - } - - pub fn diagnostic_summary(&self, include_ignored: bool, cx: &App) -> DiagnosticSummary { - let mut summary = DiagnosticSummary::default(); - for (_, _, path_summary) in self.diagnostic_summaries(include_ignored, cx) { - summary.error_count += path_summary.error_count; - summary.warning_count += path_summary.warning_count; - } - summary - } - - /// Returns the diagnostic summary for a specific project path. - pub fn diagnostic_summary_for_path( - &self, - project_path: &ProjectPath, - _: &App, - ) -> DiagnosticSummary { - if let Some(summaries) = self - .diagnostic_summaries - .get(&project_path.worktree_id) - .and_then(|map| map.get(&project_path.path)) - { - let (error_count, warning_count) = summaries.iter().fold( - (0, 0), - |(error_count, warning_count), (_language_server_id, summary)| { - ( - error_count + summary.error_count, - warning_count + summary.warning_count, - ) - }, - ); - - DiagnosticSummary { - error_count, - warning_count, - } - } else { - DiagnosticSummary::default() - } - } - - pub fn diagnostic_summaries<'a>( - &'a self, - include_ignored: bool, - cx: &'a App, - ) -> impl Iterator + 'a { - self.worktree_store - .read(cx) - .visible_worktrees(cx) - .filter_map(|worktree| { - let worktree = worktree.read(cx); - Some((worktree, self.diagnostic_summaries.get(&worktree.id())?)) - }) - .flat_map(move |(worktree, summaries)| { - let worktree_id = worktree.id(); - summaries - .iter() - .filter(move |(path, _)| { - include_ignored - || worktree - .entry_for_path(path.as_ref()) - .is_some_and(|entry| !entry.is_ignored) - }) - .flat_map(move |(path, summaries)| { - summaries.iter().map(move |(server_id, summary)| { - ( - ProjectPath { - worktree_id, - path: path.clone(), - }, - *server_id, - *summary, - ) - }) - }) - }) - } - - pub fn on_buffer_edited( - &mut self, - buffer: Entity, - cx: &mut Context, - ) -> Option<()> { - let language_servers: Vec<_> = buffer.update(cx, |buffer, cx| { - Some( - self.as_local()? - .language_servers_for_buffer(buffer, cx) - .map(|i| i.1.clone()) - .collect(), - ) - })?; - - let buffer = buffer.read(cx); - let file = File::from_dyn(buffer.file())?; - let abs_path = file.as_local()?.abs_path(cx); - let uri = lsp::Uri::from_file_path(&abs_path) - .ok() - .with_context(|| format!("Failed to convert path to URI: {}", abs_path.display())) - .log_err()?; - let next_snapshot = buffer.text_snapshot(); - for language_server in language_servers { - let language_server = language_server.clone(); - - let buffer_snapshots = self - .as_local_mut()? - .buffer_snapshots - .get_mut(&buffer.remote_id()) - .and_then(|m| m.get_mut(&language_server.server_id()))?; - let previous_snapshot = buffer_snapshots.last()?; - - let build_incremental_change = || { - buffer - .edits_since::>( - previous_snapshot.snapshot.version(), - ) - .map(|edit| { - let edit_start = edit.new.start.0; - let edit_end = edit_start + (edit.old.end.0 - edit.old.start.0); - let new_text = next_snapshot - .text_for_range(edit.new.start.1..edit.new.end.1) - .collect(); - lsp::TextDocumentContentChangeEvent { - range: Some(lsp::Range::new( - point_to_lsp(edit_start), - point_to_lsp(edit_end), - )), - range_length: None, - text: new_text, - } - }) - .collect() - }; - - let document_sync_kind = language_server - .capabilities() - .text_document_sync - .as_ref() - .and_then(|sync| match sync { - lsp::TextDocumentSyncCapability::Kind(kind) => Some(*kind), - lsp::TextDocumentSyncCapability::Options(options) => options.change, - }); - - let content_changes: Vec<_> = match document_sync_kind { - Some(lsp::TextDocumentSyncKind::FULL) => { - vec![lsp::TextDocumentContentChangeEvent { - range: None, - range_length: None, - text: next_snapshot.text(), - }] - } - Some(lsp::TextDocumentSyncKind::INCREMENTAL) => build_incremental_change(), - _ => { - #[cfg(any(test, feature = "test-support"))] - { - build_incremental_change() - } - - #[cfg(not(any(test, feature = "test-support")))] - { - continue; - } - } - }; - - let next_version = previous_snapshot.version + 1; - buffer_snapshots.push(LspBufferSnapshot { - version: next_version, - snapshot: next_snapshot.clone(), - }); - - language_server - .notify::( - lsp::DidChangeTextDocumentParams { - text_document: lsp::VersionedTextDocumentIdentifier::new( - uri.clone(), - next_version, - ), - content_changes, - }, - ) - .ok(); - self.pull_workspace_diagnostics(language_server.server_id()); - } - - None - } - - pub fn on_buffer_saved( - &mut self, - buffer: Entity, - cx: &mut Context, - ) -> Option<()> { - let file = File::from_dyn(buffer.read(cx).file())?; - let worktree_id = file.worktree_id(cx); - let abs_path = file.as_local()?.abs_path(cx); - let text_document = lsp::TextDocumentIdentifier { - uri: file_path_to_lsp_url(&abs_path).log_err()?, - }; - let local = self.as_local()?; - - for server in local.language_servers_for_worktree(worktree_id) { - if let Some(include_text) = include_text(server.as_ref()) { - let text = if include_text { - Some(buffer.read(cx).text()) - } else { - None - }; - server - .notify::( - lsp::DidSaveTextDocumentParams { - text_document: text_document.clone(), - text, - }, - ) - .ok(); - } - } - - let language_servers = buffer.update(cx, |buffer, cx| { - local.language_server_ids_for_buffer(buffer, cx) - }); - for language_server_id in language_servers { - self.simulate_disk_based_diagnostics_events_if_needed(language_server_id, cx); - } - - None - } - - async fn refresh_workspace_configurations(lsp_store: &WeakEntity, cx: &mut AsyncApp) { - maybe!(async move { - let mut refreshed_servers = HashSet::default(); - let servers = lsp_store - .update(cx, |lsp_store, cx| { - let local = lsp_store.as_local()?; - - let servers = local - .language_server_ids - .iter() - .filter_map(|(seed, state)| { - let worktree = lsp_store - .worktree_store - .read(cx) - .worktree_for_id(seed.worktree_id, cx); - let delegate: Arc = - worktree.map(|worktree| { - LocalLspAdapterDelegate::new( - local.languages.clone(), - &local.environment, - cx.weak_entity(), - &worktree, - local.http_client.clone(), - local.fs.clone(), - cx, - ) - })?; - let server_id = state.id; - - let states = local.language_servers.get(&server_id)?; - - match states { - LanguageServerState::Starting { .. } => None, - LanguageServerState::Running { - adapter, server, .. - } => { - let adapter = adapter.clone(); - let server = server.clone(); - refreshed_servers.insert(server.name()); - let toolchain = seed.toolchain.clone(); - Some(cx.spawn(async move |_, cx| { - let settings = - LocalLspStore::workspace_configuration_for_adapter( - adapter.adapter.clone(), - &delegate, - toolchain, - None, - cx, - ) - .await - .ok()?; - server - .notify::( - lsp::DidChangeConfigurationParams { settings }, - ) - .ok()?; - Some(()) - })) - } - } - }) - .collect::>(); - - Some(servers) - }) - .ok() - .flatten()?; - - log::debug!("Refreshing workspace configurations for servers {refreshed_servers:?}"); - // TODO this asynchronous job runs concurrently with extension (de)registration and may take enough time for a certain extension - // to stop and unregister its language server wrapper. - // This is racy : an extension might have already removed all `local.language_servers` state, but here we `.clone()` and hold onto it anyway. - // This now causes errors in the logs, we should find a way to remove such servers from the processing everywhere. - let _: Vec> = join_all(servers).await; - - Some(()) - }) - .await; - } - - fn maintain_workspace_config( - external_refresh_requests: watch::Receiver<()>, - cx: &mut Context, - ) -> Task> { - let (mut settings_changed_tx, mut settings_changed_rx) = watch::channel(); - let _ = postage::stream::Stream::try_recv(&mut settings_changed_rx); - - let settings_observation = cx.observe_global::(move |_, _| { - *settings_changed_tx.borrow_mut() = (); - }); - - let mut joint_future = - futures::stream::select(settings_changed_rx, external_refresh_requests); - // Multiple things can happen when a workspace environment (selected toolchain + settings) change: - // - We might shut down a language server if it's no longer enabled for a given language (and there are no buffers using it otherwise). - // - We might also shut it down when the workspace configuration of all of the users of a given language server converges onto that of the other. - // - In the same vein, we might also decide to start a new language server if the workspace configuration *diverges* from the other. - // - In the easiest case (where we're not wrangling the lifetime of a language server anyhow), if none of the roots of a single language server diverge in their configuration, - // but it is still different to what we had before, we're gonna send out a workspace configuration update. - cx.spawn(async move |this, cx| { - while let Some(()) = joint_future.next().await { - this.update(cx, |this, cx| { - this.refresh_server_tree(cx); - }) - .ok(); - - Self::refresh_workspace_configurations(&this, cx).await; - } - - drop(settings_observation); - anyhow::Ok(()) - }) - } - - pub fn running_language_servers_for_local_buffer<'a>( - &'a self, - buffer: &Buffer, - cx: &mut App, - ) -> impl Iterator, &'a Arc)> { - let local = self.as_local(); - let language_server_ids = local - .map(|local| local.language_server_ids_for_buffer(buffer, cx)) - .unwrap_or_default(); - - language_server_ids - .into_iter() - .filter_map( - move |server_id| match local?.language_servers.get(&server_id)? { - LanguageServerState::Running { - adapter, server, .. - } => Some((adapter, server)), - _ => None, - }, - ) - } - - pub fn language_servers_for_local_buffer( - &self, - buffer: &Buffer, - cx: &mut App, - ) -> Vec { - let local = self.as_local(); - local - .map(|local| local.language_server_ids_for_buffer(buffer, cx)) - .unwrap_or_default() - } - - pub fn language_server_for_local_buffer<'a>( - &'a self, - buffer: &'a Buffer, - server_id: LanguageServerId, - cx: &'a mut App, - ) -> Option<(&'a Arc, &'a Arc)> { - self.as_local()? - .language_servers_for_buffer(buffer, cx) - .find(|(_, s)| s.server_id() == server_id) - } - - fn remove_worktree(&mut self, id_to_remove: WorktreeId, cx: &mut Context) { - self.diagnostic_summaries.remove(&id_to_remove); - if let Some(local) = self.as_local_mut() { - let to_remove = local.remove_worktree(id_to_remove, cx); - for server in to_remove { - self.language_server_statuses.remove(&server); - } - } - } - - pub fn shared( - &mut self, - project_id: u64, - downstream_client: AnyProtoClient, - _: &mut Context, - ) { - self.downstream_client = Some((downstream_client.clone(), project_id)); - - for (server_id, status) in &self.language_server_statuses { - if let Some(server) = self.language_server_for_id(*server_id) { - downstream_client - .send(proto::StartLanguageServer { - project_id, - server: Some(proto::LanguageServer { - id: server_id.to_proto(), - name: status.name.to_string(), - worktree_id: status.worktree.map(|id| id.to_proto()), - }), - capabilities: serde_json::to_string(&server.capabilities()) - .expect("serializing server LSP capabilities"), - }) - .log_err(); - } - } - } - - pub fn disconnected_from_host(&mut self) { - self.downstream_client.take(); - } - - pub fn disconnected_from_ssh_remote(&mut self) { - if let LspStoreMode::Remote(RemoteLspStore { - upstream_client, .. - }) = &mut self.mode - { - upstream_client.take(); - } - } - - pub(crate) fn set_language_server_statuses_from_proto( - &mut self, - project: WeakEntity, - language_servers: Vec, - server_capabilities: Vec, - cx: &mut Context, - ) { - let lsp_logs = cx - .try_global::() - .map(|lsp_store| lsp_store.0.clone()); - - self.language_server_statuses = language_servers - .into_iter() - .zip(server_capabilities) - .map(|(server, server_capabilities)| { - let server_id = LanguageServerId(server.id as usize); - if let Ok(server_capabilities) = serde_json::from_str(&server_capabilities) { - self.lsp_server_capabilities - .insert(server_id, server_capabilities); - } - - let name = LanguageServerName::from_proto(server.name); - let worktree = server.worktree_id.map(WorktreeId::from_proto); - - if let Some(lsp_logs) = &lsp_logs { - lsp_logs.update(cx, |lsp_logs, cx| { - lsp_logs.add_language_server( - // Only remote clients get their language servers set from proto - LanguageServerKind::Remote { - project: project.clone(), - }, - server_id, - Some(name.clone()), - worktree, - None, - cx, - ); - }); - } - - ( - server_id, - LanguageServerStatus { - name, - pending_work: Default::default(), - has_pending_diagnostic_updates: false, - progress_tokens: Default::default(), - worktree, - binary: None, - configuration: None, - workspace_folders: BTreeSet::new(), - }, - ) - }) - .collect(); - } - - #[cfg(test)] - pub fn update_diagnostic_entries( - &mut self, - server_id: LanguageServerId, - abs_path: PathBuf, - result_id: Option, - version: Option, - diagnostics: Vec>>, - cx: &mut Context, - ) -> anyhow::Result<()> { - self.merge_diagnostic_entries( - vec![DocumentDiagnosticsUpdate { - diagnostics: DocumentDiagnostics { - diagnostics, - document_abs_path: abs_path, - version, - }, - result_id, - server_id, - disk_based_sources: Cow::Borrowed(&[]), - registration_id: None, - }], - |_, _, _| false, - cx, - )?; - Ok(()) - } - - pub fn merge_diagnostic_entries<'a>( - &mut self, - diagnostic_updates: Vec>, - merge: impl Fn(&lsp::Uri, &Diagnostic, &App) -> bool + Clone, - cx: &mut Context, - ) -> anyhow::Result<()> { - let mut diagnostics_summary = None::; - let mut updated_diagnostics_paths = HashMap::default(); - for mut update in diagnostic_updates { - let abs_path = &update.diagnostics.document_abs_path; - let server_id = update.server_id; - let Some((worktree, relative_path)) = - self.worktree_store.read(cx).find_worktree(abs_path, cx) - else { - log::warn!("skipping diagnostics update, no worktree found for path {abs_path:?}"); - return Ok(()); - }; - - let worktree_id = worktree.read(cx).id(); - let project_path = ProjectPath { - worktree_id, - path: relative_path, - }; - - let document_uri = lsp::Uri::from_file_path(abs_path) - .map_err(|()| anyhow!("Failed to convert buffer path {abs_path:?} to lsp Uri"))?; - if let Some(buffer_handle) = self.buffer_store.read(cx).get_by_path(&project_path) { - 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(&document_uri, &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::>(); - - self.as_local_mut() - .context("cannot merge diagnostics on a remote LspStore")? - .update_buffer_diagnostics( - &buffer_handle, - server_id, - Some(update.registration_id), - update.result_id, - update.diagnostics.version, - update.diagnostics.diagnostics.clone(), - reused_diagnostics.clone(), - cx, - )?; - - update.diagnostics.diagnostics.extend(reused_diagnostics); - } else if let Some(local) = self.as_local() { - let reused_diagnostics = local - .diagnostics - .get(&worktree_id) - .and_then(|diagnostics_for_tree| diagnostics_for_tree.get(&project_path.path)) - .and_then(|diagnostics_by_server_id| { - diagnostics_by_server_id - .binary_search_by_key(&server_id, |e| e.0) - .ok() - .map(|ix| &diagnostics_by_server_id[ix].1) - }) - .into_iter() - .flatten() - .filter(|v| merge(&document_uri, &v.diagnostic, cx)); - - update - .diagnostics - .diagnostics - .extend(reused_diagnostics.cloned()); - } - - let updated = worktree.update(cx, |worktree, cx| { - self.update_worktree_diagnostics( - worktree.id(), - server_id, - project_path.path.clone(), - update.diagnostics.diagnostics, - cx, - ) - })?; - match updated { - ControlFlow::Continue(new_summary) => { - if let Some((project_id, new_summary)) = new_summary { - match &mut diagnostics_summary { - Some(diagnostics_summary) => { - diagnostics_summary - .more_summaries - .push(proto::DiagnosticSummary { - path: project_path.path.as_ref().to_proto(), - language_server_id: server_id.0 as u64, - error_count: new_summary.error_count, - warning_count: new_summary.warning_count, - }) - } - None => { - diagnostics_summary = Some(proto::UpdateDiagnosticSummary { - project_id, - worktree_id: worktree_id.to_proto(), - summary: Some(proto::DiagnosticSummary { - path: project_path.path.as_ref().to_proto(), - language_server_id: server_id.0 as u64, - error_count: new_summary.error_count, - warning_count: new_summary.warning_count, - }), - more_summaries: Vec::new(), - }) - } - } - } - updated_diagnostics_paths - .entry(server_id) - .or_insert_with(Vec::new) - .push(project_path); - } - ControlFlow::Break(()) => {} - } - } - - if let Some((diagnostics_summary, (downstream_client, _))) = - diagnostics_summary.zip(self.downstream_client.as_ref()) - { - downstream_client.send(diagnostics_summary).log_err(); - } - for (server_id, paths) in updated_diagnostics_paths { - cx.emit(LspStoreEvent::DiagnosticsUpdated { server_id, paths }); - } - Ok(()) - } - - fn update_worktree_diagnostics( - &mut self, - worktree_id: WorktreeId, - server_id: LanguageServerId, - path_in_worktree: Arc, - diagnostics: Vec>>, - _: &mut Context, - ) -> Result>> { - let local = match &mut self.mode { - LspStoreMode::Local(local_lsp_store) => local_lsp_store, - _ => anyhow::bail!("update_worktree_diagnostics called on remote"), - }; - - let summaries_for_tree = self.diagnostic_summaries.entry(worktree_id).or_default(); - let diagnostics_for_tree = local.diagnostics.entry(worktree_id).or_default(); - let summaries_by_server_id = summaries_for_tree - .entry(path_in_worktree.clone()) - .or_default(); - - let old_summary = summaries_by_server_id - .remove(&server_id) - .unwrap_or_default(); - - let new_summary = DiagnosticSummary::new(&diagnostics); - if diagnostics.is_empty() { - if let Some(diagnostics_by_server_id) = diagnostics_for_tree.get_mut(&path_in_worktree) - { - if let Ok(ix) = diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) { - diagnostics_by_server_id.remove(ix); - } - if diagnostics_by_server_id.is_empty() { - diagnostics_for_tree.remove(&path_in_worktree); - } - } - } else { - summaries_by_server_id.insert(server_id, new_summary); - let diagnostics_by_server_id = diagnostics_for_tree - .entry(path_in_worktree.clone()) - .or_default(); - match diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) { - Ok(ix) => { - diagnostics_by_server_id[ix] = (server_id, diagnostics); - } - Err(ix) => { - diagnostics_by_server_id.insert(ix, (server_id, diagnostics)); - } - } - } - - if !old_summary.is_empty() || !new_summary.is_empty() { - if let Some((_, project_id)) = &self.downstream_client { - Ok(ControlFlow::Continue(Some(( - *project_id, - proto::DiagnosticSummary { - path: path_in_worktree.to_proto(), - language_server_id: server_id.0 as u64, - error_count: new_summary.error_count as u32, - warning_count: new_summary.warning_count as u32, - }, - )))) - } else { - Ok(ControlFlow::Continue(None)) - } - } else { - Ok(ControlFlow::Break(())) - } - } - - pub fn open_buffer_for_symbol( - &mut self, - symbol: &Symbol, - cx: &mut Context, - ) -> Task>> { - if let Some((client, project_id)) = self.upstream_client() { - let request = client.request(proto::OpenBufferForSymbol { - project_id, - symbol: Some(Self::serialize_symbol(symbol)), - }); - cx.spawn(async move |this, cx| { - let response = request.await?; - let buffer_id = BufferId::new(response.buffer_id)?; - this.update(cx, |this, cx| this.wait_for_remote_buffer(buffer_id, cx))? - .await - }) - } else if let Some(local) = self.as_local() { - let is_valid = local.language_server_ids.iter().any(|(seed, state)| { - seed.worktree_id == symbol.source_worktree_id - && state.id == symbol.source_language_server_id - && symbol.language_server_name == seed.name - }); - if !is_valid { - return Task::ready(Err(anyhow!( - "language server for worktree and language not found" - ))); - }; - - let symbol_abs_path = match &symbol.path { - SymbolLocation::InProject(project_path) => self - .worktree_store - .read(cx) - .absolutize(&project_path, cx) - .context("no such worktree"), - SymbolLocation::OutsideProject { - abs_path, - signature: _, - } => Ok(abs_path.to_path_buf()), - }; - let symbol_abs_path = match symbol_abs_path { - Ok(abs_path) => abs_path, - Err(err) => return Task::ready(Err(err)), - }; - let symbol_uri = if let Ok(uri) = lsp::Uri::from_file_path(symbol_abs_path) { - uri - } else { - return Task::ready(Err(anyhow!("invalid symbol path"))); - }; - - self.open_local_buffer_via_lsp(symbol_uri, symbol.source_language_server_id, cx) - } else { - Task::ready(Err(anyhow!("no upstream client or local store"))) - } - } - - pub(crate) fn open_local_buffer_via_lsp( - &mut self, - abs_path: lsp::Uri, - language_server_id: LanguageServerId, - cx: &mut Context, - ) -> Task>> { - cx.spawn(async move |lsp_store, cx| { - // Escape percent-encoded string. - let current_scheme = abs_path.scheme().to_owned(); - // Uri is immutable, so we can't modify the scheme - - let abs_path = abs_path - .to_file_path() - .map_err(|()| anyhow!("can't convert URI to path"))?; - let p = abs_path.clone(); - let yarn_worktree = lsp_store - .update(cx, move |lsp_store, cx| match lsp_store.as_local() { - Some(local_lsp_store) => local_lsp_store.yarn.update(cx, |_, cx| { - cx.spawn(async move |this, cx| { - let t = this - .update(cx, |this, cx| this.process_path(&p, ¤t_scheme, cx)) - .ok()?; - t.await - }) - }), - None => Task::ready(None), - })? - .await; - let (worktree_root_target, known_relative_path) = - if let Some((zip_root, relative_path)) = yarn_worktree { - (zip_root, Some(relative_path)) - } else { - (Arc::::from(abs_path.as_path()), None) - }; - let (worktree, relative_path) = if let Some(result) = - lsp_store.update(cx, |lsp_store, cx| { - lsp_store.worktree_store.update(cx, |worktree_store, cx| { - worktree_store.find_worktree(&worktree_root_target, cx) - }) - })? { - let relative_path = known_relative_path.unwrap_or_else(|| result.1.clone()); - (result.0, relative_path) - } else { - let worktree = lsp_store - .update(cx, |lsp_store, cx| { - lsp_store.worktree_store.update(cx, |worktree_store, cx| { - worktree_store.create_worktree(&worktree_root_target, false, cx) - }) - })? - .await?; - if worktree.read_with(cx, |worktree, _| worktree.is_local())? { - lsp_store - .update(cx, |lsp_store, cx| { - if let Some(local) = lsp_store.as_local_mut() { - local.register_language_server_for_invisible_worktree( - &worktree, - language_server_id, - cx, - ) - } - }) - .ok(); - } - let worktree_root = worktree.read_with(cx, |worktree, _| worktree.abs_path())?; - let relative_path = if let Some(known_path) = known_relative_path { - known_path - } else { - RelPath::new(abs_path.strip_prefix(worktree_root)?, PathStyle::local())? - .into_arc() - }; - (worktree, relative_path) - }; - let project_path = ProjectPath { - worktree_id: worktree.read_with(cx, |worktree, _| worktree.id())?, - path: relative_path, - }; - lsp_store - .update(cx, |lsp_store, cx| { - lsp_store.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(project_path, cx) - }) - })? - .await - }) - } - - fn request_multiple_lsp_locally( - &mut self, - buffer: &Entity, - position: Option

, - request: R, - cx: &mut Context, - ) -> Task> - where - P: ToOffset, - R: LspCommand + Clone, - ::Result: Send, - ::Params: Send, - { - let Some(local) = self.as_local() else { - return Task::ready(Vec::new()); - }; - - let snapshot = buffer.read(cx).snapshot(); - let scope = position.and_then(|position| snapshot.language_scope_at(position)); - - let server_ids = buffer.update(cx, |buffer, cx| { - local - .language_servers_for_buffer(buffer, cx) - .filter(|(adapter, _)| { - scope - .as_ref() - .map(|scope| scope.language_allowed(&adapter.name)) - .unwrap_or(true) - }) - .map(|(_, server)| server.server_id()) - .filter(|server_id| { - self.as_local().is_none_or(|local| { - local - .buffers_opened_in_servers - .get(&snapshot.remote_id()) - .is_some_and(|servers| servers.contains(server_id)) - }) - }) - .collect::>() - }); - - let mut response_results = server_ids - .into_iter() - .map(|server_id| { - let task = self.request_lsp( - buffer.clone(), - LanguageServerToQuery::Other(server_id), - request.clone(), - cx, - ); - async move { (server_id, task.await) } - }) - .collect::>(); - - cx.background_spawn(async move { - let mut responses = Vec::with_capacity(response_results.len()); - while let Some((server_id, response_result)) = response_results.next().await { - match response_result { - Ok(response) => responses.push((server_id, response)), - // rust-analyzer likes to error with this when its still loading up - Err(e) if format!("{e:#}").ends_with("content modified") => (), - Err(e) => log::error!("Error handling response for request {request:?}: {e:#}"), - } - } - responses - }) - } - - async fn handle_lsp_get_completions( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let sender_id = envelope.original_sender_id().unwrap_or_default(); - - let buffer_id = GetCompletions::buffer_id_from_proto(&envelope.payload)?; - let buffer_handle = this.update(&mut cx, |this, cx| { - this.buffer_store.read(cx).get_existing(buffer_id) - })??; - let request = GetCompletions::from_proto( - envelope.payload, - this.clone(), - buffer_handle.clone(), - cx.clone(), - ) - .await?; - - let server_to_query = match request.server_id { - Some(server_id) => LanguageServerToQuery::Other(server_id), - None => LanguageServerToQuery::FirstCapable, - }; - - let response = this - .update(&mut cx, |this, cx| { - this.request_lsp(buffer_handle.clone(), server_to_query, request, cx) - })? - .await?; - this.update(&mut cx, |this, cx| { - Ok(GetCompletions::response_to_proto( - response, - this, - sender_id, - &buffer_handle.read(cx).version(), - cx, - )) - })? - } - - async fn handle_lsp_command( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<::Response> - where - ::Params: Send, - ::Result: Send, - { - let sender_id = envelope.original_sender_id().unwrap_or_default(); - let buffer_id = T::buffer_id_from_proto(&envelope.payload)?; - let buffer_handle = this.update(&mut cx, |this, cx| { - this.buffer_store.read(cx).get_existing(buffer_id) - })??; - let request = T::from_proto( - envelope.payload, - this.clone(), - buffer_handle.clone(), - cx.clone(), - ) - .await?; - let response = this - .update(&mut cx, |this, cx| { - this.request_lsp( - buffer_handle.clone(), - LanguageServerToQuery::FirstCapable, - request, - cx, - ) - })? - .await?; - this.update(&mut cx, |this, cx| { - Ok(T::response_to_proto( - response, - this, - sender_id, - &buffer_handle.read(cx).version(), - cx, - )) - })? - } - - async fn handle_lsp_query( - lsp_store: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - use proto::lsp_query::Request; - let sender_id = envelope.original_sender_id().unwrap_or_default(); - let lsp_query = envelope.payload; - let lsp_request_id = LspRequestId(lsp_query.lsp_request_id); - let server_id = lsp_query.server_id.map(LanguageServerId::from_proto); - match lsp_query.request.context("invalid LSP query request")? { - Request::GetReferences(get_references) => { - let position = get_references.position.clone().and_then(deserialize_anchor); - Self::query_lsp_locally::( - lsp_store, - server_id, - sender_id, - lsp_request_id, - get_references, - position, - &mut cx, - ) - .await?; - } - Request::GetDocumentColor(get_document_color) => { - Self::query_lsp_locally::( - lsp_store, - server_id, - sender_id, - lsp_request_id, - get_document_color, - None, - &mut cx, - ) - .await?; - } - Request::GetHover(get_hover) => { - let position = get_hover.position.clone().and_then(deserialize_anchor); - Self::query_lsp_locally::( - lsp_store, - server_id, - sender_id, - lsp_request_id, - get_hover, - position, - &mut cx, - ) - .await?; - } - Request::GetCodeActions(get_code_actions) => { - Self::query_lsp_locally::( - lsp_store, - server_id, - sender_id, - lsp_request_id, - get_code_actions, - None, - &mut cx, - ) - .await?; - } - Request::GetSignatureHelp(get_signature_help) => { - let position = get_signature_help - .position - .clone() - .and_then(deserialize_anchor); - Self::query_lsp_locally::( - lsp_store, - server_id, - sender_id, - lsp_request_id, - get_signature_help, - position, - &mut cx, - ) - .await?; - } - Request::GetCodeLens(get_code_lens) => { - Self::query_lsp_locally::( - lsp_store, - server_id, - sender_id, - lsp_request_id, - get_code_lens, - None, - &mut cx, - ) - .await?; - } - Request::GetDefinition(get_definition) => { - let position = get_definition.position.clone().and_then(deserialize_anchor); - Self::query_lsp_locally::( - lsp_store, - server_id, - sender_id, - lsp_request_id, - get_definition, - position, - &mut cx, - ) - .await?; - } - Request::GetDeclaration(get_declaration) => { - let position = get_declaration - .position - .clone() - .and_then(deserialize_anchor); - Self::query_lsp_locally::( - lsp_store, - server_id, - sender_id, - lsp_request_id, - get_declaration, - position, - &mut cx, - ) - .await?; - } - Request::GetTypeDefinition(get_type_definition) => { - let position = get_type_definition - .position - .clone() - .and_then(deserialize_anchor); - Self::query_lsp_locally::( - lsp_store, - server_id, - sender_id, - lsp_request_id, - get_type_definition, - position, - &mut cx, - ) - .await?; - } - Request::GetImplementation(get_implementation) => { - let position = get_implementation - .position - .clone() - .and_then(deserialize_anchor); - Self::query_lsp_locally::( - lsp_store, - server_id, - sender_id, - lsp_request_id, - get_implementation, - position, - &mut cx, - ) - .await?; - } - Request::GetDocumentDiagnostics(get_document_diagnostics) => { - let buffer_id = BufferId::new(get_document_diagnostics.buffer_id())?; - let version = deserialize_version(get_document_diagnostics.buffer_version()); - let buffer = lsp_store.update(&mut cx, |this, cx| { - this.buffer_store.read(cx).get_existing(buffer_id) - })??; - buffer - .update(&mut cx, |buffer, _| { - buffer.wait_for_version(version.clone()) - })? - .await?; - lsp_store.update(&mut cx, |lsp_store, cx| { - let lsp_data = lsp_store.latest_lsp_data(&buffer, cx); - let key = LspKey { - request_type: TypeId::of::(), - server_queried: server_id, - }; - if ::ProtoRequest::stop_previous_requests( - ) { - if let Some(lsp_requests) = lsp_data.lsp_requests.get_mut(&key) { - lsp_requests.clear(); - }; - } - - let existing_queries = lsp_data.lsp_requests.entry(key).or_default(); - existing_queries.insert( - lsp_request_id, - cx.spawn(async move |lsp_store, cx| { - let diagnostics_pull = lsp_store - .update(cx, |lsp_store, cx| { - lsp_store.pull_diagnostics_for_buffer(buffer, cx) - }) - .ok(); - if let Some(diagnostics_pull) = diagnostics_pull { - match diagnostics_pull.await { - Ok(()) => {} - Err(e) => log::error!("Failed to pull diagnostics: {e:#}"), - }; - } - }), - ); - })?; - } - Request::InlayHints(inlay_hints) => { - let query_start = inlay_hints - .start - .clone() - .and_then(deserialize_anchor) - .context("invalid inlay hints range start")?; - let query_end = inlay_hints - .end - .clone() - .and_then(deserialize_anchor) - .context("invalid inlay hints range end")?; - Self::deduplicate_range_based_lsp_requests::( - &lsp_store, - server_id, - lsp_request_id, - &inlay_hints, - query_start..query_end, - &mut cx, - ) - .await - .context("preparing inlay hints request")?; - Self::query_lsp_locally::( - lsp_store, - server_id, - sender_id, - lsp_request_id, - inlay_hints, - None, - &mut cx, - ) - .await - .context("querying for inlay hints")? - } - } - Ok(proto::Ack {}) - } - - async fn handle_lsp_query_response( - lsp_store: Entity, - envelope: TypedEnvelope, - cx: AsyncApp, - ) -> Result<()> { - lsp_store.read_with(&cx, |lsp_store, _| { - if let Some((upstream_client, _)) = lsp_store.upstream_client() { - upstream_client.handle_lsp_response(envelope.clone()); - } - })?; - Ok(()) - } - - async fn handle_apply_code_action( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let sender_id = envelope.original_sender_id().unwrap_or_default(); - let action = - Self::deserialize_code_action(envelope.payload.action.context("invalid action")?)?; - let apply_code_action = this.update(&mut cx, |this, cx| { - let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - let buffer = this.buffer_store.read(cx).get_existing(buffer_id)?; - anyhow::Ok(this.apply_code_action(buffer, action, false, cx)) - })??; - - let project_transaction = apply_code_action.await?; - let project_transaction = this.update(&mut cx, |this, cx| { - this.buffer_store.update(cx, |buffer_store, cx| { - buffer_store.serialize_project_transaction_for_peer( - project_transaction, - sender_id, - cx, - ) - }) - })?; - Ok(proto::ApplyCodeActionResponse { - transaction: Some(project_transaction), - }) - } - - async fn handle_register_buffer_with_language_servers( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - let peer_id = envelope.original_sender_id.unwrap_or(envelope.sender_id); - this.update(&mut cx, |this, cx| { - if let Some((upstream_client, upstream_project_id)) = this.upstream_client() { - return upstream_client.send(proto::RegisterBufferWithLanguageServers { - project_id: upstream_project_id, - buffer_id: buffer_id.to_proto(), - only_servers: envelope.payload.only_servers, - }); - } - - let Some(buffer) = this.buffer_store().read(cx).get(buffer_id) else { - anyhow::bail!("buffer is not open"); - }; - - let handle = this.register_buffer_with_language_servers( - &buffer, - envelope - .payload - .only_servers - .into_iter() - .filter_map(|selector| { - Some(match selector.selector? { - proto::language_server_selector::Selector::ServerId(server_id) => { - LanguageServerSelector::Id(LanguageServerId::from_proto(server_id)) - } - proto::language_server_selector::Selector::Name(name) => { - LanguageServerSelector::Name(LanguageServerName( - SharedString::from(name), - )) - } - }) - }) - .collect(), - false, - cx, - ); - this.buffer_store().update(cx, |buffer_store, _| { - buffer_store.register_shared_lsp_handle(peer_id, buffer_id, handle); - }); - - Ok(()) - })??; - Ok(proto::Ack {}) - } - - async fn handle_rename_project_entry( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let entry_id = ProjectEntryId::from_proto(envelope.payload.entry_id); - let new_worktree_id = WorktreeId::from_proto(envelope.payload.new_worktree_id); - let new_path = - RelPath::from_proto(&envelope.payload.new_path).context("invalid relative path")?; - - let (worktree_store, old_worktree, new_worktree, old_entry) = this - .update(&mut cx, |this, cx| { - let (worktree, entry) = this - .worktree_store - .read(cx) - .worktree_and_entry_for_id(entry_id, cx)?; - let new_worktree = this - .worktree_store - .read(cx) - .worktree_for_id(new_worktree_id, cx)?; - Some(( - this.worktree_store.clone(), - worktree, - new_worktree, - entry.clone(), - )) - })? - .context("worktree not found")?; - let (old_abs_path, old_worktree_id) = old_worktree.read_with(&cx, |worktree, _| { - (worktree.absolutize(&old_entry.path), worktree.id()) - })?; - let new_abs_path = - new_worktree.read_with(&cx, |worktree, _| worktree.absolutize(&new_path))?; - - let _transaction = Self::will_rename_entry( - this.downgrade(), - old_worktree_id, - &old_abs_path, - &new_abs_path, - old_entry.is_dir(), - cx.clone(), - ) - .await; - let response = WorktreeStore::handle_rename_project_entry( - worktree_store, - envelope.payload, - cx.clone(), - ) - .await; - this.read_with(&cx, |this, _| { - this.did_rename_entry( - old_worktree_id, - &old_abs_path, - &new_abs_path, - old_entry.is_dir(), - ); - }) - .ok(); - response - } - - async fn handle_update_diagnostic_summary( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - this.update(&mut cx, |lsp_store, cx| { - let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); - let mut updated_diagnostics_paths = HashMap::default(); - let mut diagnostics_summary = None::; - for message_summary in envelope - .payload - .summary - .into_iter() - .chain(envelope.payload.more_summaries) - { - let project_path = ProjectPath { - worktree_id, - path: RelPath::from_proto(&message_summary.path).context("invalid path")?, - }; - let path = project_path.path.clone(); - let server_id = LanguageServerId(message_summary.language_server_id as usize); - let summary = DiagnosticSummary { - error_count: message_summary.error_count as usize, - warning_count: message_summary.warning_count as usize, - }; - - if summary.is_empty() { - if let Some(worktree_summaries) = - lsp_store.diagnostic_summaries.get_mut(&worktree_id) - && let Some(summaries) = worktree_summaries.get_mut(&path) - { - summaries.remove(&server_id); - if summaries.is_empty() { - worktree_summaries.remove(&path); - } - } - } else { - lsp_store - .diagnostic_summaries - .entry(worktree_id) - .or_default() - .entry(path) - .or_default() - .insert(server_id, summary); - } - - if let Some((_, project_id)) = &lsp_store.downstream_client { - match &mut diagnostics_summary { - Some(diagnostics_summary) => { - diagnostics_summary - .more_summaries - .push(proto::DiagnosticSummary { - path: project_path.path.as_ref().to_proto(), - language_server_id: server_id.0 as u64, - error_count: summary.error_count as u32, - warning_count: summary.warning_count as u32, - }) - } - None => { - diagnostics_summary = Some(proto::UpdateDiagnosticSummary { - project_id: *project_id, - worktree_id: worktree_id.to_proto(), - summary: Some(proto::DiagnosticSummary { - path: project_path.path.as_ref().to_proto(), - language_server_id: server_id.0 as u64, - error_count: summary.error_count as u32, - warning_count: summary.warning_count as u32, - }), - more_summaries: Vec::new(), - }) - } - } - } - updated_diagnostics_paths - .entry(server_id) - .or_insert_with(Vec::new) - .push(project_path); - } - - if let Some((diagnostics_summary, (downstream_client, _))) = - diagnostics_summary.zip(lsp_store.downstream_client.as_ref()) - { - downstream_client.send(diagnostics_summary).log_err(); - } - for (server_id, paths) in updated_diagnostics_paths { - cx.emit(LspStoreEvent::DiagnosticsUpdated { server_id, paths }); - } - Ok(()) - })? - } - - async fn handle_start_language_server( - lsp_store: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - let server = envelope.payload.server.context("invalid server")?; - let server_capabilities = - serde_json::from_str::(&envelope.payload.capabilities) - .with_context(|| { - format!( - "incorrect server capabilities {}", - envelope.payload.capabilities - ) - })?; - lsp_store.update(&mut cx, |lsp_store, cx| { - let server_id = LanguageServerId(server.id as usize); - let server_name = LanguageServerName::from_proto(server.name.clone()); - lsp_store - .lsp_server_capabilities - .insert(server_id, server_capabilities); - lsp_store.language_server_statuses.insert( - server_id, - LanguageServerStatus { - name: server_name.clone(), - pending_work: Default::default(), - has_pending_diagnostic_updates: false, - progress_tokens: Default::default(), - worktree: server.worktree_id.map(WorktreeId::from_proto), - binary: None, - configuration: None, - workspace_folders: BTreeSet::new(), - }, - ); - cx.emit(LspStoreEvent::LanguageServerAdded( - server_id, - server_name, - server.worktree_id.map(WorktreeId::from_proto), - )); - cx.notify(); - })?; - Ok(()) - } - - async fn handle_update_language_server( - lsp_store: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - lsp_store.update(&mut cx, |lsp_store, cx| { - let language_server_id = LanguageServerId(envelope.payload.language_server_id as usize); - - match envelope.payload.variant.context("invalid variant")? { - proto::update_language_server::Variant::WorkStart(payload) => { - lsp_store.on_lsp_work_start( - language_server_id, - ProgressToken::from_proto(payload.token.context("missing progress token")?) - .context("invalid progress token value")?, - LanguageServerProgress { - title: payload.title, - is_disk_based_diagnostics_progress: false, - is_cancellable: payload.is_cancellable.unwrap_or(false), - message: payload.message, - percentage: payload.percentage.map(|p| p as usize), - last_update_at: cx.background_executor().now(), - }, - cx, - ); - } - proto::update_language_server::Variant::WorkProgress(payload) => { - lsp_store.on_lsp_work_progress( - language_server_id, - ProgressToken::from_proto(payload.token.context("missing progress token")?) - .context("invalid progress token value")?, - LanguageServerProgress { - title: None, - is_disk_based_diagnostics_progress: false, - is_cancellable: payload.is_cancellable.unwrap_or(false), - message: payload.message, - percentage: payload.percentage.map(|p| p as usize), - last_update_at: cx.background_executor().now(), - }, - cx, - ); - } - - proto::update_language_server::Variant::WorkEnd(payload) => { - lsp_store.on_lsp_work_end( - language_server_id, - ProgressToken::from_proto(payload.token.context("missing progress token")?) - .context("invalid progress token value")?, - cx, - ); - } - - proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(_) => { - lsp_store.disk_based_diagnostics_started(language_server_id, cx); - } - - proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(_) => { - lsp_store.disk_based_diagnostics_finished(language_server_id, cx) - } - - non_lsp @ proto::update_language_server::Variant::StatusUpdate(_) - | non_lsp @ proto::update_language_server::Variant::RegisteredForBuffer(_) - | non_lsp @ proto::update_language_server::Variant::MetadataUpdated(_) => { - cx.emit(LspStoreEvent::LanguageServerUpdate { - language_server_id, - name: envelope - .payload - .server_name - .map(SharedString::new) - .map(LanguageServerName), - message: non_lsp, - }); - } - } - - Ok(()) - })? - } - - async fn handle_language_server_log( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - let language_server_id = LanguageServerId(envelope.payload.language_server_id as usize); - let log_type = envelope - .payload - .log_type - .map(LanguageServerLogType::from_proto) - .context("invalid language server log type")?; - - let message = envelope.payload.message; - - this.update(&mut cx, |_, cx| { - cx.emit(LspStoreEvent::LanguageServerLog( - language_server_id, - log_type, - message, - )); - }) - } - - async fn handle_lsp_ext_cancel_flycheck( - lsp_store: Entity, - envelope: TypedEnvelope, - cx: AsyncApp, - ) -> Result { - let server_id = LanguageServerId(envelope.payload.language_server_id as usize); - let task = lsp_store.read_with(&cx, |lsp_store, _| { - if let Some(server) = lsp_store.language_server_for_id(server_id) { - Some(server.notify::(())) - } else { - None - } - })?; - if let Some(task) = task { - task.context("handling lsp ext cancel flycheck")?; - } - - Ok(proto::Ack {}) - } - - async fn handle_lsp_ext_run_flycheck( - lsp_store: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let server_id = LanguageServerId(envelope.payload.language_server_id as usize); - 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)) - }) - .transpose()? - } else { - None - }; - server.notify::( - lsp_store::lsp_ext_command::RunFlycheckParams { text_document }, - )?; - } - anyhow::Ok(()) - })??; - - Ok(proto::Ack {}) - } - - async fn handle_lsp_ext_clear_flycheck( - lsp_store: Entity, - envelope: TypedEnvelope, - cx: AsyncApp, - ) -> Result { - let server_id = LanguageServerId(envelope.payload.language_server_id as usize); - lsp_store - .read_with(&cx, |lsp_store, _| { - if let Some(server) = lsp_store.language_server_for_id(server_id) { - Some(server.notify::(())) - } else { - None - } - }) - .context("handling lsp ext clear flycheck")?; - - Ok(proto::Ack {}) - } - - pub fn disk_based_diagnostics_started( - &mut self, - language_server_id: LanguageServerId, - cx: &mut Context, - ) { - if let Some(language_server_status) = - self.language_server_statuses.get_mut(&language_server_id) - { - language_server_status.has_pending_diagnostic_updates = true; - } - - cx.emit(LspStoreEvent::DiskBasedDiagnosticsStarted { language_server_id }); - cx.emit(LspStoreEvent::LanguageServerUpdate { - language_server_id, - name: self - .language_server_adapter_for_id(language_server_id) - .map(|adapter| adapter.name()), - message: proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating( - Default::default(), - ), - }) - } - - pub fn disk_based_diagnostics_finished( - &mut self, - language_server_id: LanguageServerId, - cx: &mut Context, - ) { - if let Some(language_server_status) = - self.language_server_statuses.get_mut(&language_server_id) - { - language_server_status.has_pending_diagnostic_updates = false; - } - - cx.emit(LspStoreEvent::DiskBasedDiagnosticsFinished { language_server_id }); - cx.emit(LspStoreEvent::LanguageServerUpdate { - language_server_id, - name: self - .language_server_adapter_for_id(language_server_id) - .map(|adapter| adapter.name()), - message: proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( - Default::default(), - ), - }) - } - - // After saving a buffer using a language server that doesn't provide a disk-based progress token, - // kick off a timer that will reset every time the buffer is saved. If the timer eventually fires, - // simulate disk-based diagnostics being finished so that other pieces of UI (e.g., project - // diagnostics view, diagnostic status bar) can update. We don't emit an event right away because - // the language server might take some time to publish diagnostics. - fn simulate_disk_based_diagnostics_events_if_needed( - &mut self, - language_server_id: LanguageServerId, - cx: &mut Context, - ) { - const DISK_BASED_DIAGNOSTICS_DEBOUNCE: Duration = Duration::from_secs(1); - - let Some(LanguageServerState::Running { - simulate_disk_based_diagnostics_completion, - adapter, - .. - }) = self - .as_local_mut() - .and_then(|local_store| local_store.language_servers.get_mut(&language_server_id)) - else { - return; - }; - - if adapter.disk_based_diagnostics_progress_token.is_some() { - return; - } - - let prev_task = - simulate_disk_based_diagnostics_completion.replace(cx.spawn(async move |this, cx| { - cx.background_executor() - .timer(DISK_BASED_DIAGNOSTICS_DEBOUNCE) - .await; - - this.update(cx, |this, cx| { - this.disk_based_diagnostics_finished(language_server_id, cx); - - if let Some(LanguageServerState::Running { - simulate_disk_based_diagnostics_completion, - .. - }) = this.as_local_mut().and_then(|local_store| { - local_store.language_servers.get_mut(&language_server_id) - }) { - *simulate_disk_based_diagnostics_completion = None; - } - }) - .ok(); - })); - - if prev_task.is_none() { - self.disk_based_diagnostics_started(language_server_id, cx); - } - } - - pub fn language_server_statuses( - &self, - ) -> impl DoubleEndedIterator { - self.language_server_statuses - .iter() - .map(|(key, value)| (*key, value)) - } - - pub(super) fn did_rename_entry( - &self, - worktree_id: WorktreeId, - old_path: &Path, - new_path: &Path, - is_dir: bool, - ) { - maybe!({ - let local_store = self.as_local()?; - - let old_uri = lsp::Uri::from_file_path(old_path) - .ok() - .map(|uri| uri.to_string())?; - let new_uri = lsp::Uri::from_file_path(new_path) - .ok() - .map(|uri| uri.to_string())?; - - for language_server in local_store.language_servers_for_worktree(worktree_id) { - let Some(filter) = local_store - .language_server_paths_watched_for_rename - .get(&language_server.server_id()) - else { - continue; - }; - - if filter.should_send_did_rename(&old_uri, is_dir) { - language_server - .notify::(RenameFilesParams { - files: vec![FileRename { - old_uri: old_uri.clone(), - new_uri: new_uri.clone(), - }], - }) - .ok(); - } - } - Some(()) - }); - } - - pub(super) fn will_rename_entry( - this: WeakEntity, - worktree_id: WorktreeId, - old_path: &Path, - new_path: &Path, - is_dir: bool, - cx: AsyncApp, - ) -> Task { - let old_uri = lsp::Uri::from_file_path(old_path) - .ok() - .map(|uri| uri.to_string()); - let new_uri = lsp::Uri::from_file_path(new_path) - .ok() - .map(|uri| uri.to_string()); - cx.spawn(async move |cx| { - let mut tasks = vec![]; - this.update(cx, |this, cx| { - let local_store = this.as_local()?; - let old_uri = old_uri?; - let new_uri = new_uri?; - for language_server in local_store.language_servers_for_worktree(worktree_id) { - let Some(filter) = local_store - .language_server_paths_watched_for_rename - .get(&language_server.server_id()) - else { - continue; - }; - - if filter.should_send_will_rename(&old_uri, is_dir) { - let apply_edit = cx.spawn({ - let old_uri = old_uri.clone(); - let new_uri = new_uri.clone(); - let language_server = language_server.clone(); - async move |this, cx| { - let edit = language_server - .request::(RenameFilesParams { - files: vec![FileRename { old_uri, new_uri }], - }) - .await - .into_response() - .context("will rename files") - .log_err() - .flatten()?; - - let transaction = LocalLspStore::deserialize_workspace_edit( - this.upgrade()?, - edit, - false, - language_server.clone(), - cx, - ) - .await - .ok()?; - Some(transaction) - } - }); - tasks.push(apply_edit); - } - } - Some(()) - }) - .ok() - .flatten(); - let mut merged_transaction = ProjectTransaction::default(); - for task in tasks { - // Await on tasks sequentially so that the order of application of edits is deterministic - // (at least with regards to the order of registration of language servers) - if let Some(transaction) = task.await { - for (buffer, buffer_transaction) in transaction.0 { - merged_transaction.0.insert(buffer, buffer_transaction); - } - } - } - merged_transaction - }) - } - - fn lsp_notify_abs_paths_changed( - &mut self, - server_id: LanguageServerId, - changes: Vec, - ) { - maybe!({ - let server = self.language_server_for_id(server_id)?; - let changes = changes - .into_iter() - .filter_map(|event| { - let typ = match event.kind? { - PathEventKind::Created => lsp::FileChangeType::CREATED, - PathEventKind::Removed => lsp::FileChangeType::DELETED, - PathEventKind::Changed => lsp::FileChangeType::CHANGED, - }; - Some(lsp::FileEvent { - uri: file_path_to_lsp_url(&event.path).log_err()?, - typ, - }) - }) - .collect::>(); - if !changes.is_empty() { - server - .notify::( - lsp::DidChangeWatchedFilesParams { changes }, - ) - .ok(); - } - Some(()) - }); - } - - pub fn language_server_for_id(&self, id: LanguageServerId) -> Option> { - self.as_local()?.language_server_for_id(id) - } - - fn on_lsp_progress( - &mut self, - progress_params: lsp::ProgressParams, - language_server_id: LanguageServerId, - disk_based_diagnostics_progress_token: Option, - cx: &mut Context, - ) { - match progress_params.value { - lsp::ProgressParamsValue::WorkDone(progress) => { - self.handle_work_done_progress( - progress, - language_server_id, - disk_based_diagnostics_progress_token, - ProgressToken::from_lsp(progress_params.token), - cx, - ); - } - lsp::ProgressParamsValue::WorkspaceDiagnostic(report) => { - let registration_id = match progress_params.token { - lsp::NumberOrString::Number(_) => None, - lsp::NumberOrString::String(token) => token - .split_once(WORKSPACE_DIAGNOSTICS_TOKEN_START) - .map(|(_, id)| id.to_owned()), - }; - if let Some(LanguageServerState::Running { - workspace_diagnostics_refresh_tasks, - .. - }) = self - .as_local_mut() - .and_then(|local| local.language_servers.get_mut(&language_server_id)) - && let Some(workspace_diagnostics) = - workspace_diagnostics_refresh_tasks.get_mut(®istration_id) - { - workspace_diagnostics.progress_tx.try_send(()).ok(); - self.apply_workspace_diagnostic_report( - language_server_id, - report, - registration_id.map(SharedString::from), - cx, - ) - } - } - } - } - - fn handle_work_done_progress( - &mut self, - progress: lsp::WorkDoneProgress, - language_server_id: LanguageServerId, - disk_based_diagnostics_progress_token: Option, - token: ProgressToken, - cx: &mut Context, - ) { - let language_server_status = - if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { - status - } else { - return; - }; - - if !language_server_status.progress_tokens.contains(&token) { - return; - } - - let is_disk_based_diagnostics_progress = - if let (Some(disk_based_token), ProgressToken::String(token)) = - (&disk_based_diagnostics_progress_token, &token) - { - token.starts_with(disk_based_token) - } else { - false - }; - - match progress { - lsp::WorkDoneProgress::Begin(report) => { - if is_disk_based_diagnostics_progress { - self.disk_based_diagnostics_started(language_server_id, cx); - } - self.on_lsp_work_start( - language_server_id, - token.clone(), - LanguageServerProgress { - title: Some(report.title), - is_disk_based_diagnostics_progress, - is_cancellable: report.cancellable.unwrap_or(false), - message: report.message.clone(), - percentage: report.percentage.map(|p| p as usize), - last_update_at: cx.background_executor().now(), - }, - cx, - ); - } - lsp::WorkDoneProgress::Report(report) => self.on_lsp_work_progress( - language_server_id, - token, - LanguageServerProgress { - title: None, - is_disk_based_diagnostics_progress, - is_cancellable: report.cancellable.unwrap_or(false), - message: report.message, - percentage: report.percentage.map(|p| p as usize), - last_update_at: cx.background_executor().now(), - }, - cx, - ), - lsp::WorkDoneProgress::End(_) => { - language_server_status.progress_tokens.remove(&token); - self.on_lsp_work_end(language_server_id, token.clone(), cx); - if is_disk_based_diagnostics_progress { - self.disk_based_diagnostics_finished(language_server_id, cx); - } - } - } - } - - fn on_lsp_work_start( - &mut self, - language_server_id: LanguageServerId, - token: ProgressToken, - progress: LanguageServerProgress, - cx: &mut Context, - ) { - if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { - status.pending_work.insert(token.clone(), progress.clone()); - cx.notify(); - } - cx.emit(LspStoreEvent::LanguageServerUpdate { - language_server_id, - name: self - .language_server_adapter_for_id(language_server_id) - .map(|adapter| adapter.name()), - message: proto::update_language_server::Variant::WorkStart(proto::LspWorkStart { - token: Some(token.to_proto()), - title: progress.title, - message: progress.message, - percentage: progress.percentage.map(|p| p as u32), - is_cancellable: Some(progress.is_cancellable), - }), - }) - } - - fn on_lsp_work_progress( - &mut self, - language_server_id: LanguageServerId, - token: ProgressToken, - progress: LanguageServerProgress, - cx: &mut Context, - ) { - let mut did_update = false; - if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { - match status.pending_work.entry(token.clone()) { - btree_map::Entry::Vacant(entry) => { - entry.insert(progress.clone()); - did_update = true; - } - btree_map::Entry::Occupied(mut entry) => { - let entry = entry.get_mut(); - if (progress.last_update_at - entry.last_update_at) - >= SERVER_PROGRESS_THROTTLE_TIMEOUT - { - entry.last_update_at = progress.last_update_at; - if progress.message.is_some() { - entry.message = progress.message.clone(); - } - if progress.percentage.is_some() { - entry.percentage = progress.percentage; - } - if progress.is_cancellable != entry.is_cancellable { - entry.is_cancellable = progress.is_cancellable; - } - did_update = true; - } - } - } - } - - if did_update { - cx.emit(LspStoreEvent::LanguageServerUpdate { - language_server_id, - name: self - .language_server_adapter_for_id(language_server_id) - .map(|adapter| adapter.name()), - message: proto::update_language_server::Variant::WorkProgress( - proto::LspWorkProgress { - token: Some(token.to_proto()), - message: progress.message, - percentage: progress.percentage.map(|p| p as u32), - is_cancellable: Some(progress.is_cancellable), - }, - ), - }) - } - } - - fn on_lsp_work_end( - &mut self, - language_server_id: LanguageServerId, - token: ProgressToken, - cx: &mut Context, - ) { - if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { - if let Some(work) = status.pending_work.remove(&token) - && !work.is_disk_based_diagnostics_progress - { - cx.emit(LspStoreEvent::RefreshInlayHints { - server_id: language_server_id, - request_id: None, - }); - } - cx.notify(); - } - - cx.emit(LspStoreEvent::LanguageServerUpdate { - language_server_id, - name: self - .language_server_adapter_for_id(language_server_id) - .map(|adapter| adapter.name()), - message: proto::update_language_server::Variant::WorkEnd(proto::LspWorkEnd { - token: Some(token.to_proto()), - }), - }) - } - - pub async fn handle_resolve_completion_documentation( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let lsp_completion = serde_json::from_slice(&envelope.payload.lsp_completion)?; - - let completion = this - .read_with(&cx, |this, cx| { - let id = LanguageServerId(envelope.payload.language_server_id as usize); - let server = this - .language_server_for_id(id) - .with_context(|| format!("No language server {id}"))?; - - anyhow::Ok(cx.background_spawn(async move { - let can_resolve = server - .capabilities() - .completion_provider - .as_ref() - .and_then(|options| options.resolve_provider) - .unwrap_or(false); - if can_resolve { - server - .request::(lsp_completion) - .await - .into_response() - .context("resolve completion item") - } else { - anyhow::Ok(lsp_completion) - } - })) - })?? - .await?; - - let mut documentation_is_markdown = false; - let lsp_completion = serde_json::to_string(&completion)?.into_bytes(); - let documentation = match completion.documentation { - Some(lsp::Documentation::String(text)) => text, - - Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value })) => { - documentation_is_markdown = kind == lsp::MarkupKind::Markdown; - value - } - - _ => String::new(), - }; - - // If we have a new buffer_id, that means we're talking to a new client - // and want to check for new text_edits in the completion too. - let mut old_replace_start = None; - let mut old_replace_end = None; - let mut old_insert_start = None; - let mut old_insert_end = None; - let mut new_text = String::default(); - if let Ok(buffer_id) = BufferId::new(envelope.payload.buffer_id) { - let buffer_snapshot = this.update(&mut cx, |this, cx| { - let buffer = this.buffer_store.read(cx).get_existing(buffer_id)?; - anyhow::Ok(buffer.read(cx).snapshot()) - })??; - - if let Some(text_edit) = completion.text_edit.as_ref() { - let edit = parse_completion_text_edit(text_edit, &buffer_snapshot); - - if let Some(mut edit) = edit { - LineEnding::normalize(&mut edit.new_text); - - new_text = edit.new_text; - old_replace_start = Some(serialize_anchor(&edit.replace_range.start)); - old_replace_end = Some(serialize_anchor(&edit.replace_range.end)); - if let Some(insert_range) = edit.insert_range { - old_insert_start = Some(serialize_anchor(&insert_range.start)); - old_insert_end = Some(serialize_anchor(&insert_range.end)); - } - } - } - } - - Ok(proto::ResolveCompletionDocumentationResponse { - documentation, - documentation_is_markdown, - old_replace_start, - old_replace_end, - new_text, - lsp_completion, - old_insert_start, - old_insert_end, - }) - } - - async fn handle_on_type_formatting( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let on_type_formatting = this.update(&mut cx, |this, cx| { - let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - let buffer = this.buffer_store.read(cx).get_existing(buffer_id)?; - let position = envelope - .payload - .position - .and_then(deserialize_anchor) - .context("invalid position")?; - anyhow::Ok(this.apply_on_type_formatting( - buffer, - position, - envelope.payload.trigger.clone(), - cx, - )) - })??; - - let transaction = on_type_formatting - .await? - .as_ref() - .map(language::proto::serialize_transaction); - Ok(proto::OnTypeFormattingResponse { transaction }) - } - - async fn handle_refresh_inlay_hints( - lsp_store: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - lsp_store.update(&mut cx, |_, cx| { - cx.emit(LspStoreEvent::RefreshInlayHints { - server_id: LanguageServerId::from_proto(envelope.payload.server_id), - request_id: envelope.payload.request_id.map(|id| id as usize), - }); - })?; - Ok(proto::Ack {}) - } - - async fn handle_pull_workspace_diagnostics( - lsp_store: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let server_id = LanguageServerId::from_proto(envelope.payload.server_id); - lsp_store.update(&mut cx, |lsp_store, _| { - lsp_store.pull_workspace_diagnostics(server_id); - })?; - Ok(proto::Ack {}) - } - - async fn handle_get_color_presentation( - lsp_store: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - let buffer = lsp_store.update(&mut cx, |lsp_store, cx| { - lsp_store.buffer_store.read(cx).get_existing(buffer_id) - })??; - - let color = envelope - .payload - .color - .context("invalid color resolve request")?; - let start = color - .lsp_range_start - .context("invalid color resolve request")?; - let end = color - .lsp_range_end - .context("invalid color resolve request")?; - - let color = DocumentColor { - lsp_range: lsp::Range { - start: point_to_lsp(PointUtf16::new(start.row, start.column)), - end: point_to_lsp(PointUtf16::new(end.row, end.column)), - }, - color: lsp::Color { - red: color.red, - green: color.green, - blue: color.blue, - alpha: color.alpha, - }, - resolved: false, - color_presentations: Vec::new(), - }; - let resolved_color = lsp_store - .update(&mut cx, |lsp_store, cx| { - lsp_store.resolve_color_presentation( - color, - buffer.clone(), - LanguageServerId(envelope.payload.server_id as usize), - cx, - ) - })? - .await - .context("resolving color presentation")?; - - Ok(proto::GetColorPresentationResponse { - presentations: resolved_color - .color_presentations - .into_iter() - .map(|presentation| proto::ColorPresentation { - label: presentation.label.to_string(), - text_edit: presentation.text_edit.map(serialize_lsp_edit), - additional_text_edits: presentation - .additional_text_edits - .into_iter() - .map(serialize_lsp_edit) - .collect(), - }) - .collect(), - }) - } - - async fn handle_resolve_inlay_hint( - lsp_store: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let proto_hint = envelope - .payload - .hint - .expect("incorrect protobuf resolve inlay hint message: missing the inlay hint"); - let hint = InlayHints::proto_to_project_hint(proto_hint) - .context("resolved proto inlay hint conversion")?; - let buffer = lsp_store.update(&mut cx, |lsp_store, cx| { - let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - lsp_store.buffer_store.read(cx).get_existing(buffer_id) - })??; - let response_hint = lsp_store - .update(&mut cx, |lsp_store, cx| { - lsp_store.resolve_inlay_hint( - hint, - buffer, - LanguageServerId(envelope.payload.language_server_id as usize), - cx, - ) - })? - .await - .context("inlay hints fetch")?; - Ok(proto::ResolveInlayHintResponse { - hint: Some(InlayHints::project_to_proto_hint(response_hint)), - }) - } - - async fn handle_refresh_code_lens( - this: Entity, - _: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - this.update(&mut cx, |_, cx| { - cx.emit(LspStoreEvent::RefreshCodeLens); - })?; - Ok(proto::Ack {}) - } - - async fn handle_open_buffer_for_symbol( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let peer_id = envelope.original_sender_id().unwrap_or_default(); - let symbol = envelope.payload.symbol.context("invalid symbol")?; - let symbol = Self::deserialize_symbol(symbol)?; - this.read_with(&cx, |this, _| { - if let SymbolLocation::OutsideProject { - abs_path, - signature, - } = &symbol.path - { - let new_signature = this.symbol_signature(&abs_path); - anyhow::ensure!(&new_signature == signature, "invalid symbol signature"); - } - Ok(()) - })??; - let buffer = this - .update(&mut cx, |this, cx| { - this.open_buffer_for_symbol( - &Symbol { - language_server_name: symbol.language_server_name, - source_worktree_id: symbol.source_worktree_id, - source_language_server_id: symbol.source_language_server_id, - path: symbol.path, - name: symbol.name, - kind: symbol.kind, - range: symbol.range, - label: CodeLabel::default(), - }, - cx, - ) - })? - .await?; - - this.update(&mut cx, |this, cx| { - let is_private = buffer - .read(cx) - .file() - .map(|f| f.is_private()) - .unwrap_or_default(); - if is_private { - Err(anyhow!(rpc::ErrorCode::UnsharedItem)) - } else { - this.buffer_store - .update(cx, |buffer_store, cx| { - buffer_store.create_buffer_for_peer(&buffer, peer_id, cx) - }) - .detach_and_log_err(cx); - let buffer_id = buffer.read(cx).remote_id().to_proto(); - Ok(proto::OpenBufferForSymbolResponse { buffer_id }) - } - })? - } - - fn symbol_signature(&self, abs_path: &Path) -> [u8; 32] { - let mut hasher = Sha256::new(); - hasher.update(abs_path.to_string_lossy().as_bytes()); - hasher.update(self.nonce.to_be_bytes()); - hasher.finalize().as_slice().try_into().unwrap() - } - - pub async fn handle_get_project_symbols( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let symbols = this - .update(&mut cx, |this, cx| { - this.symbols(&envelope.payload.query, cx) - })? - .await?; - - Ok(proto::GetProjectSymbolsResponse { - symbols: symbols.iter().map(Self::serialize_symbol).collect(), - }) - } - - pub async fn handle_restart_language_servers( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - this.update(&mut cx, |lsp_store, cx| { - let buffers = - lsp_store.buffer_ids_to_buffers(envelope.payload.buffer_ids.into_iter(), cx); - lsp_store.restart_language_servers_for_buffers( - buffers, - envelope - .payload - .only_servers - .into_iter() - .filter_map(|selector| { - Some(match selector.selector? { - proto::language_server_selector::Selector::ServerId(server_id) => { - LanguageServerSelector::Id(LanguageServerId::from_proto(server_id)) - } - proto::language_server_selector::Selector::Name(name) => { - LanguageServerSelector::Name(LanguageServerName( - SharedString::from(name), - )) - } - }) - }) - .collect(), - cx, - ); - })?; - - Ok(proto::Ack {}) - } - - pub async fn handle_stop_language_servers( - lsp_store: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - lsp_store.update(&mut cx, |lsp_store, cx| { - if envelope.payload.all - && envelope.payload.also_servers.is_empty() - && envelope.payload.buffer_ids.is_empty() - { - lsp_store.stop_all_language_servers(cx); - } else { - let buffers = - lsp_store.buffer_ids_to_buffers(envelope.payload.buffer_ids.into_iter(), cx); - lsp_store - .stop_language_servers_for_buffers( - buffers, - envelope - .payload - .also_servers - .into_iter() - .filter_map(|selector| { - Some(match selector.selector? { - proto::language_server_selector::Selector::ServerId( - server_id, - ) => LanguageServerSelector::Id(LanguageServerId::from_proto( - server_id, - )), - proto::language_server_selector::Selector::Name(name) => { - LanguageServerSelector::Name(LanguageServerName( - SharedString::from(name), - )) - } - }) - }) - .collect(), - cx, - ) - .detach_and_log_err(cx); - } - })?; - - Ok(proto::Ack {}) - } - - pub async fn handle_cancel_language_server_work( - lsp_store: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - lsp_store.update(&mut cx, |lsp_store, cx| { - if let Some(work) = envelope.payload.work { - match work { - proto::cancel_language_server_work::Work::Buffers(buffers) => { - let buffers = - lsp_store.buffer_ids_to_buffers(buffers.buffer_ids.into_iter(), cx); - lsp_store.cancel_language_server_work_for_buffers(buffers, cx); - } - proto::cancel_language_server_work::Work::LanguageServerWork(work) => { - let server_id = LanguageServerId::from_proto(work.language_server_id); - let token = work - .token - .map(|token| { - ProgressToken::from_proto(token) - .context("invalid work progress token") - }) - .transpose()?; - lsp_store.cancel_language_server_work(server_id, token, cx); - } - } - } - anyhow::Ok(()) - })??; - - Ok(proto::Ack {}) - } - - fn buffer_ids_to_buffers( - &mut self, - buffer_ids: impl Iterator, - cx: &mut Context, - ) -> Vec> { - buffer_ids - .into_iter() - .flat_map(|buffer_id| { - self.buffer_store - .read(cx) - .get(BufferId::new(buffer_id).log_err()?) - }) - .collect::>() - } - - async fn handle_apply_additional_edits_for_completion( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let (buffer, completion) = this.update(&mut cx, |this, cx| { - let buffer_id = BufferId::new(envelope.payload.buffer_id)?; - let buffer = this.buffer_store.read(cx).get_existing(buffer_id)?; - let completion = Self::deserialize_completion( - envelope.payload.completion.context("invalid completion")?, - )?; - anyhow::Ok((buffer, completion)) - })??; - - let apply_additional_edits = this.update(&mut cx, |this, cx| { - this.apply_additional_edits_for_completion( - buffer, - Rc::new(RefCell::new(Box::new([Completion { - replace_range: completion.replace_range, - new_text: completion.new_text, - source: completion.source, - documentation: None, - label: CodeLabel::default(), - match_start: None, - snippet_deduplication_key: None, - insert_text_mode: None, - icon_path: None, - confirm: None, - }]))), - 0, - false, - cx, - ) - })?; - - Ok(proto::ApplyCompletionAdditionalEditsResponse { - transaction: apply_additional_edits - .await? - .as_ref() - .map(language::proto::serialize_transaction), - }) - } - - pub fn last_formatting_failure(&self) -> Option<&str> { - self.last_formatting_failure.as_deref() - } - - pub fn reset_last_formatting_failure(&mut self) { - self.last_formatting_failure = None; - } - - pub fn environment_for_buffer( - &self, - buffer: &Entity, - cx: &mut Context, - ) -> Shared>>> { - if let Some(environment) = &self.as_local().map(|local| local.environment.clone()) { - environment.update(cx, |env, cx| { - env.buffer_environment(buffer, &self.worktree_store, cx) - }) - } else { - Task::ready(None).shared() - } - } - - pub fn format( - &mut self, - buffers: HashSet>, - target: LspFormatTarget, - push_to_history: bool, - trigger: FormatTrigger, - cx: &mut Context, - ) -> Task> { - let logger = zlog::scoped!("format"); - if self.as_local().is_some() { - zlog::trace!(logger => "Formatting locally"); - let logger = zlog::scoped!(logger => "local"); - let buffers = buffers - .into_iter() - .map(|buffer_handle| { - let buffer = buffer_handle.read(cx); - let buffer_abs_path = File::from_dyn(buffer.file()) - .and_then(|file| file.as_local().map(|f| f.abs_path(cx))); - - (buffer_handle, buffer_abs_path, buffer.remote_id()) - }) - .collect::>(); - - cx.spawn(async move |lsp_store, cx| { - let mut formattable_buffers = Vec::with_capacity(buffers.len()); - - for (handle, abs_path, id) in buffers { - let env = lsp_store - .update(cx, |lsp_store, cx| { - lsp_store.environment_for_buffer(&handle, cx) - })? - .await; - - let ranges = match &target { - LspFormatTarget::Buffers => None, - LspFormatTarget::Ranges(ranges) => { - Some(ranges.get(&id).context("No format ranges provided for buffer")?.clone()) - } - }; - - formattable_buffers.push(FormattableBuffer { - handle, - abs_path, - env, - ranges, - }); - } - zlog::trace!(logger => "Formatting {:?} buffers", formattable_buffers.len()); - - let format_timer = zlog::time!(logger => "Formatting buffers"); - let result = LocalLspStore::format_locally( - lsp_store.clone(), - formattable_buffers, - push_to_history, - trigger, - logger, - cx, - ) - .await; - format_timer.end(); - - zlog::trace!(logger => "Formatting completed with result {:?}", result.as_ref().map(|_| "")); - - lsp_store.update(cx, |lsp_store, _| { - lsp_store.update_last_formatting_failure(&result); - })?; - - result - }) - } else if let Some((client, project_id)) = self.upstream_client() { - zlog::trace!(logger => "Formatting remotely"); - let logger = zlog::scoped!(logger => "remote"); - // Don't support formatting ranges via remote - match target { - LspFormatTarget::Buffers => {} - LspFormatTarget::Ranges(_) => { - zlog::trace!(logger => "Ignoring unsupported remote range formatting request"); - return Task::ready(Ok(ProjectTransaction::default())); - } - } - - let buffer_store = self.buffer_store(); - cx.spawn(async move |lsp_store, cx| { - zlog::trace!(logger => "Sending remote format request"); - let request_timer = zlog::time!(logger => "remote format request"); - let result = client - .request(proto::FormatBuffers { - project_id, - trigger: trigger as i32, - buffer_ids: buffers - .iter() - .map(|buffer| buffer.read_with(cx, |buffer, _| buffer.remote_id().into())) - .collect::>()?, - }) - .await - .and_then(|result| result.transaction.context("missing transaction")); - request_timer.end(); - - zlog::trace!(logger => "Remote format request resolved to {:?}", result.as_ref().map(|_| "")); - - lsp_store.update(cx, |lsp_store, _| { - lsp_store.update_last_formatting_failure(&result); - })?; - - let transaction_response = result?; - let _timer = zlog::time!(logger => "deserializing project transaction"); - buffer_store - .update(cx, |buffer_store, cx| { - buffer_store.deserialize_project_transaction( - transaction_response, - push_to_history, - cx, - ) - })? - .await - }) - } else { - zlog::trace!(logger => "Not formatting"); - Task::ready(Ok(ProjectTransaction::default())) - } - } - - async fn handle_format_buffers( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let sender_id = envelope.original_sender_id().unwrap_or_default(); - let format = this.update(&mut cx, |this, cx| { - let mut buffers = HashSet::default(); - for buffer_id in &envelope.payload.buffer_ids { - let buffer_id = BufferId::new(*buffer_id)?; - buffers.insert(this.buffer_store.read(cx).get_existing(buffer_id)?); - } - let trigger = FormatTrigger::from_proto(envelope.payload.trigger); - anyhow::Ok(this.format(buffers, LspFormatTarget::Buffers, false, trigger, cx)) - })??; - - let project_transaction = format.await?; - let project_transaction = this.update(&mut cx, |this, cx| { - this.buffer_store.update(cx, |buffer_store, cx| { - buffer_store.serialize_project_transaction_for_peer( - project_transaction, - sender_id, - cx, - ) - }) - })?; - Ok(proto::FormatBuffersResponse { - transaction: Some(project_transaction), - }) - } - - async fn handle_apply_code_action_kind( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let sender_id = envelope.original_sender_id().unwrap_or_default(); - let format = this.update(&mut cx, |this, cx| { - let mut buffers = HashSet::default(); - for buffer_id in &envelope.payload.buffer_ids { - let buffer_id = BufferId::new(*buffer_id)?; - buffers.insert(this.buffer_store.read(cx).get_existing(buffer_id)?); - } - let kind = match envelope.payload.kind.as_str() { - "" => CodeActionKind::EMPTY, - "quickfix" => CodeActionKind::QUICKFIX, - "refactor" => CodeActionKind::REFACTOR, - "refactor.extract" => CodeActionKind::REFACTOR_EXTRACT, - "refactor.inline" => CodeActionKind::REFACTOR_INLINE, - "refactor.rewrite" => CodeActionKind::REFACTOR_REWRITE, - "source" => CodeActionKind::SOURCE, - "source.organizeImports" => CodeActionKind::SOURCE_ORGANIZE_IMPORTS, - "source.fixAll" => CodeActionKind::SOURCE_FIX_ALL, - _ => anyhow::bail!( - "Invalid code action kind {}", - envelope.payload.kind.as_str() - ), - }; - anyhow::Ok(this.apply_code_action_kind(buffers, kind, false, cx)) - })??; - - let project_transaction = format.await?; - let project_transaction = this.update(&mut cx, |this, cx| { - this.buffer_store.update(cx, |buffer_store, cx| { - buffer_store.serialize_project_transaction_for_peer( - project_transaction, - sender_id, - cx, - ) - }) - })?; - Ok(proto::ApplyCodeActionKindResponse { - transaction: Some(project_transaction), - }) - } - - async fn shutdown_language_server( - server_state: Option, - name: LanguageServerName, - cx: &mut AsyncApp, - ) { - let server = match server_state { - Some(LanguageServerState::Starting { startup, .. }) => { - let mut timer = cx - .background_executor() - .timer(SERVER_LAUNCHING_BEFORE_SHUTDOWN_TIMEOUT) - .fuse(); - - select! { - server = startup.fuse() => server, - () = timer => { - log::info!("timeout waiting for language server {name} to finish launching before stopping"); - None - }, - } - } - - Some(LanguageServerState::Running { server, .. }) => Some(server), - - None => None, - }; - - if let Some(server) = server - && let Some(shutdown) = server.shutdown() - { - shutdown.await; - } - } - - // Returns a list of all of the worktrees which no longer have a language server and the root path - // for the stopped server - fn stop_local_language_server( - &mut self, - server_id: LanguageServerId, - cx: &mut Context, - ) -> Task<()> { - let local = match &mut self.mode { - LspStoreMode::Local(local) => local, - _ => { - return Task::ready(()); - } - }; - - // Remove this server ID from all entries in the given worktree. - local - .language_server_ids - .retain(|_, state| state.id != server_id); - self.buffer_store.update(cx, |buffer_store, cx| { - for buffer in buffer_store.buffers() { - buffer.update(cx, |buffer, cx| { - buffer.update_diagnostics(server_id, DiagnosticSet::new([], buffer), cx); - buffer.set_completion_triggers(server_id, Default::default(), cx); - }); - } - }); - - for (worktree_id, summaries) in self.diagnostic_summaries.iter_mut() { - summaries.retain(|path, summaries_by_server_id| { - if summaries_by_server_id.remove(&server_id).is_some() { - if let Some((client, project_id)) = self.downstream_client.clone() { - client - .send(proto::UpdateDiagnosticSummary { - project_id, - worktree_id: worktree_id.to_proto(), - summary: Some(proto::DiagnosticSummary { - path: path.as_ref().to_proto(), - language_server_id: server_id.0 as u64, - error_count: 0, - warning_count: 0, - }), - more_summaries: Vec::new(), - }) - .log_err(); - } - !summaries_by_server_id.is_empty() - } else { - true - } - }); - } - - let local = self.as_local_mut().unwrap(); - for diagnostics in local.diagnostics.values_mut() { - diagnostics.retain(|_, diagnostics_by_server_id| { - if let Ok(ix) = diagnostics_by_server_id.binary_search_by_key(&server_id, |e| e.0) { - diagnostics_by_server_id.remove(ix); - !diagnostics_by_server_id.is_empty() - } else { - true - } - }); - } - local.language_server_watched_paths.remove(&server_id); - - let server_state = local.language_servers.remove(&server_id); - self.cleanup_lsp_data(server_id); - let name = self - .language_server_statuses - .remove(&server_id) - .map(|status| status.name) - .or_else(|| { - if let Some(LanguageServerState::Running { adapter, .. }) = server_state.as_ref() { - Some(adapter.name()) - } else { - None - } - }); - - if let Some(name) = name { - log::info!("stopping language server {name}"); - self.languages - .update_lsp_binary_status(name.clone(), BinaryStatus::Stopping); - cx.notify(); - - return cx.spawn(async move |lsp_store, cx| { - Self::shutdown_language_server(server_state, name.clone(), cx).await; - lsp_store - .update(cx, |lsp_store, cx| { - lsp_store - .languages - .update_lsp_binary_status(name, BinaryStatus::Stopped); - cx.emit(LspStoreEvent::LanguageServerRemoved(server_id)); - cx.notify(); - }) - .ok(); - }); - } - - if server_state.is_some() { - cx.emit(LspStoreEvent::LanguageServerRemoved(server_id)); - } - Task::ready(()) - } - - pub fn stop_all_language_servers(&mut self, cx: &mut Context) { - if let Some((client, project_id)) = self.upstream_client() { - let request = client.request(proto::StopLanguageServers { - project_id, - buffer_ids: Vec::new(), - also_servers: Vec::new(), - all: true, - }); - cx.background_spawn(request).detach_and_log_err(cx); - } else { - let Some(local) = self.as_local_mut() else { - return; - }; - let language_servers_to_stop = local - .language_server_ids - .values() - .map(|state| state.id) - .collect(); - local.lsp_tree.remove_nodes(&language_servers_to_stop); - let tasks = language_servers_to_stop - .into_iter() - .map(|server| self.stop_local_language_server(server, cx)) - .collect::>(); - cx.background_spawn(async move { - futures::future::join_all(tasks).await; - }) - .detach(); - } - } - - pub fn restart_language_servers_for_buffers( - &mut self, - buffers: Vec>, - only_restart_servers: HashSet, - cx: &mut Context, - ) { - if let Some((client, project_id)) = self.upstream_client() { - let request = client.request(proto::RestartLanguageServers { - project_id, - buffer_ids: buffers - .into_iter() - .map(|b| b.read(cx).remote_id().to_proto()) - .collect(), - only_servers: only_restart_servers - .into_iter() - .map(|selector| { - let selector = match selector { - LanguageServerSelector::Id(language_server_id) => { - proto::language_server_selector::Selector::ServerId( - language_server_id.to_proto(), - ) - } - LanguageServerSelector::Name(language_server_name) => { - proto::language_server_selector::Selector::Name( - language_server_name.to_string(), - ) - } - }; - proto::LanguageServerSelector { - selector: Some(selector), - } - }) - .collect(), - all: false, - }); - cx.background_spawn(request).detach_and_log_err(cx); - } else { - let stop_task = if only_restart_servers.is_empty() { - self.stop_local_language_servers_for_buffers(&buffers, HashSet::default(), cx) - } else { - self.stop_local_language_servers_for_buffers(&[], only_restart_servers.clone(), cx) - }; - cx.spawn(async move |lsp_store, cx| { - stop_task.await; - lsp_store - .update(cx, |lsp_store, cx| { - for buffer in buffers { - lsp_store.register_buffer_with_language_servers( - &buffer, - only_restart_servers.clone(), - true, - cx, - ); - } - }) - .ok() - }) - .detach(); - } - } - - pub fn stop_language_servers_for_buffers( - &mut self, - buffers: Vec>, - also_stop_servers: HashSet, - cx: &mut Context, - ) -> Task> { - if let Some((client, project_id)) = self.upstream_client() { - let request = client.request(proto::StopLanguageServers { - project_id, - buffer_ids: buffers - .into_iter() - .map(|b| b.read(cx).remote_id().to_proto()) - .collect(), - also_servers: also_stop_servers - .into_iter() - .map(|selector| { - let selector = match selector { - LanguageServerSelector::Id(language_server_id) => { - proto::language_server_selector::Selector::ServerId( - language_server_id.to_proto(), - ) - } - LanguageServerSelector::Name(language_server_name) => { - proto::language_server_selector::Selector::Name( - language_server_name.to_string(), - ) - } - }; - proto::LanguageServerSelector { - selector: Some(selector), - } - }) - .collect(), - all: false, - }); - cx.background_spawn(async move { - let _ = request.await?; - Ok(()) - }) - } else { - let task = - self.stop_local_language_servers_for_buffers(&buffers, also_stop_servers, cx); - cx.background_spawn(async move { - task.await; - Ok(()) - }) - } - } - - fn stop_local_language_servers_for_buffers( - &mut self, - buffers: &[Entity], - also_stop_servers: HashSet, - cx: &mut Context, - ) -> Task<()> { - let Some(local) = self.as_local_mut() else { - return Task::ready(()); - }; - let mut language_server_names_to_stop = BTreeSet::default(); - let mut language_servers_to_stop = also_stop_servers - .into_iter() - .flat_map(|selector| match selector { - LanguageServerSelector::Id(id) => Some(id), - LanguageServerSelector::Name(name) => { - language_server_names_to_stop.insert(name); - None - } - }) - .collect::>(); - - let mut covered_worktrees = HashSet::default(); - for buffer in buffers { - buffer.update(cx, |buffer, cx| { - language_servers_to_stop.extend(local.language_server_ids_for_buffer(buffer, cx)); - if let Some(worktree_id) = buffer.file().map(|f| f.worktree_id(cx)) - && covered_worktrees.insert(worktree_id) - { - language_server_names_to_stop.retain(|name| { - let old_ids_count = language_servers_to_stop.len(); - let all_language_servers_with_this_name = local - .language_server_ids - .iter() - .filter_map(|(seed, state)| seed.name.eq(name).then(|| state.id)); - language_servers_to_stop.extend(all_language_servers_with_this_name); - old_ids_count == language_servers_to_stop.len() - }); - } - }); - } - for name in language_server_names_to_stop { - language_servers_to_stop.extend( - local - .language_server_ids - .iter() - .filter_map(|(seed, v)| seed.name.eq(&name).then(|| v.id)), - ); - } - - local.lsp_tree.remove_nodes(&language_servers_to_stop); - let tasks = language_servers_to_stop - .into_iter() - .map(|server| self.stop_local_language_server(server, cx)) - .collect::>(); - - cx.background_spawn(futures::future::join_all(tasks).map(|_| ())) - } - - fn get_buffer<'a>(&self, abs_path: &Path, cx: &'a App) -> Option<&'a Buffer> { - let (worktree, relative_path) = - self.worktree_store.read(cx).find_worktree(&abs_path, cx)?; - - let project_path = ProjectPath { - worktree_id: worktree.read(cx).id(), - path: relative_path, - }; - - Some( - self.buffer_store() - .read(cx) - .get_by_path(&project_path)? - .read(cx), - ) - } - - #[cfg(any(test, feature = "test-support"))] - pub fn update_diagnostics( - &mut self, - server_id: LanguageServerId, - diagnostics: lsp::PublishDiagnosticsParams, - result_id: Option, - source_kind: DiagnosticSourceKind, - disk_based_sources: &[String], - cx: &mut Context, - ) -> Result<()> { - self.merge_lsp_diagnostics( - source_kind, - vec![DocumentDiagnosticsUpdate { - diagnostics, - result_id, - server_id, - disk_based_sources: Cow::Borrowed(disk_based_sources), - registration_id: None, - }], - |_, _, _| false, - cx, - ) - } - - pub fn merge_lsp_diagnostics( - &mut self, - source_kind: DiagnosticSourceKind, - lsp_diagnostics: Vec>, - merge: impl Fn(&lsp::Uri, &Diagnostic, &App) -> bool + Clone, - cx: &mut Context, - ) -> Result<()> { - anyhow::ensure!(self.mode.is_local(), "called update_diagnostics on remote"); - let updates = lsp_diagnostics - .into_iter() - .filter_map(|update| { - let abs_path = update.diagnostics.uri.to_file_path().ok()?; - Some(DocumentDiagnosticsUpdate { - diagnostics: self.lsp_to_document_diagnostics( - abs_path, - source_kind, - update.server_id, - update.diagnostics, - &update.disk_based_sources, - update.registration_id.clone(), - ), - result_id: update.result_id, - server_id: update.server_id, - disk_based_sources: update.disk_based_sources, - registration_id: update.registration_id, - }) - }) - .collect(); - self.merge_diagnostic_entries(updates, merge, cx)?; - Ok(()) - } - - fn lsp_to_document_diagnostics( - &mut self, - document_abs_path: PathBuf, - source_kind: DiagnosticSourceKind, - server_id: LanguageServerId, - mut lsp_diagnostics: lsp::PublishDiagnosticsParams, - disk_based_sources: &[String], - registration_id: Option, - ) -> DocumentDiagnostics { - let mut diagnostics = Vec::default(); - let mut primary_diagnostic_group_ids = HashMap::default(); - let mut sources_by_group_id = HashMap::default(); - let mut supporting_diagnostics = HashMap::default(); - - let adapter = self.language_server_adapter_for_id(server_id); - - // Ensure that primary diagnostics are always the most severe - lsp_diagnostics - .diagnostics - .sort_by_key(|item| item.severity); - - for diagnostic in &lsp_diagnostics.diagnostics { - let source = diagnostic.source.as_ref(); - let range = range_from_lsp(diagnostic.range); - let is_supporting = diagnostic - .related_information - .as_ref() - .is_some_and(|infos| { - infos.iter().any(|info| { - primary_diagnostic_group_ids.contains_key(&( - source, - diagnostic.code.clone(), - range_from_lsp(info.location.range), - )) - }) - }); - - let is_unnecessary = diagnostic - .tags - .as_ref() - .is_some_and(|tags| tags.contains(&DiagnosticTag::UNNECESSARY)); - - let underline = self - .language_server_adapter_for_id(server_id) - .is_none_or(|adapter| adapter.underline_diagnostic(diagnostic)); - - if is_supporting { - supporting_diagnostics.insert( - (source, diagnostic.code.clone(), range), - (diagnostic.severity, is_unnecessary), - ); - } else { - let group_id = post_inc(&mut self.as_local_mut().unwrap().next_diagnostic_group_id); - let is_disk_based = - source.is_some_and(|source| disk_based_sources.contains(source)); - - sources_by_group_id.insert(group_id, source); - primary_diagnostic_group_ids - .insert((source, diagnostic.code.clone(), range.clone()), group_id); - - diagnostics.push(DiagnosticEntry { - range, - diagnostic: Diagnostic { - source: diagnostic.source.clone(), - source_kind, - code: diagnostic.code.clone(), - code_description: diagnostic - .code_description - .as_ref() - .and_then(|d| d.href.clone()), - severity: diagnostic.severity.unwrap_or(DiagnosticSeverity::ERROR), - markdown: adapter.as_ref().and_then(|adapter| { - adapter.diagnostic_message_to_markdown(&diagnostic.message) - }), - message: diagnostic.message.trim().to_string(), - group_id, - is_primary: true, - is_disk_based, - is_unnecessary, - underline, - data: diagnostic.data.clone(), - registration_id: registration_id.clone(), - }, - }); - if let Some(infos) = &diagnostic.related_information { - for info in infos { - if info.location.uri == lsp_diagnostics.uri && !info.message.is_empty() { - let range = range_from_lsp(info.location.range); - diagnostics.push(DiagnosticEntry { - range, - diagnostic: Diagnostic { - source: diagnostic.source.clone(), - source_kind, - code: diagnostic.code.clone(), - code_description: diagnostic - .code_description - .as_ref() - .and_then(|d| d.href.clone()), - severity: DiagnosticSeverity::INFORMATION, - markdown: adapter.as_ref().and_then(|adapter| { - adapter.diagnostic_message_to_markdown(&info.message) - }), - message: info.message.trim().to_string(), - group_id, - is_primary: false, - is_disk_based, - is_unnecessary: false, - underline, - data: diagnostic.data.clone(), - registration_id: registration_id.clone(), - }, - }); - } - } - } - } - } - - for entry in &mut diagnostics { - let diagnostic = &mut entry.diagnostic; - if !diagnostic.is_primary { - let source = *sources_by_group_id.get(&diagnostic.group_id).unwrap(); - if let Some(&(severity, is_unnecessary)) = supporting_diagnostics.get(&( - source, - diagnostic.code.clone(), - entry.range.clone(), - )) { - if let Some(severity) = severity { - diagnostic.severity = severity; - } - diagnostic.is_unnecessary = is_unnecessary; - } - } - } - - DocumentDiagnostics { - diagnostics, - document_abs_path, - version: lsp_diagnostics.version, - } - } - - fn insert_newly_running_language_server( - &mut self, - adapter: Arc, - language_server: Arc, - server_id: LanguageServerId, - key: LanguageServerSeed, - workspace_folders: Arc>>, - cx: &mut Context, - ) { - let Some(local) = self.as_local_mut() else { - return; - }; - // If the language server for this key doesn't match the server id, don't store the - // server. Which will cause it to be dropped, killing the process - if local - .language_server_ids - .get(&key) - .map(|state| state.id != server_id) - .unwrap_or(false) - { - return; - } - - // Update language_servers collection with Running variant of LanguageServerState - // indicating that the server is up and running and ready - let workspace_folders = workspace_folders.lock().clone(); - language_server.set_workspace_folders(workspace_folders); - - let workspace_diagnostics_refresh_tasks = language_server - .capabilities() - .diagnostic_provider - .and_then(|provider| { - local - .language_server_dynamic_registrations - .entry(server_id) - .or_default() - .diagnostics - .entry(None) - .or_insert(provider.clone()); - let workspace_refresher = - lsp_workspace_diagnostics_refresh(None, provider, language_server.clone(), cx)?; - - Some((None, workspace_refresher)) - }) - .into_iter() - .collect(); - local.language_servers.insert( - server_id, - LanguageServerState::Running { - workspace_diagnostics_refresh_tasks, - adapter: adapter.clone(), - server: language_server.clone(), - simulate_disk_based_diagnostics_completion: None, - }, - ); - local - .languages - .update_lsp_binary_status(adapter.name(), BinaryStatus::None); - if let Some(file_ops_caps) = language_server - .capabilities() - .workspace - .as_ref() - .and_then(|ws| ws.file_operations.as_ref()) - { - let did_rename_caps = file_ops_caps.did_rename.as_ref(); - let will_rename_caps = file_ops_caps.will_rename.as_ref(); - if did_rename_caps.or(will_rename_caps).is_some() { - let watcher = RenamePathsWatchedForServer::default() - .with_did_rename_patterns(did_rename_caps) - .with_will_rename_patterns(will_rename_caps); - local - .language_server_paths_watched_for_rename - .insert(server_id, watcher); - } - } - - self.language_server_statuses.insert( - server_id, - LanguageServerStatus { - name: language_server.name(), - pending_work: Default::default(), - has_pending_diagnostic_updates: false, - progress_tokens: Default::default(), - worktree: Some(key.worktree_id), - binary: Some(language_server.binary().clone()), - configuration: Some(language_server.configuration().clone()), - workspace_folders: language_server.workspace_folders(), - }, - ); - - cx.emit(LspStoreEvent::LanguageServerAdded( - server_id, - language_server.name(), - Some(key.worktree_id), - )); - - let server_capabilities = language_server.capabilities(); - if let Some((downstream_client, project_id)) = self.downstream_client.as_ref() { - downstream_client - .send(proto::StartLanguageServer { - project_id: *project_id, - server: Some(proto::LanguageServer { - id: server_id.to_proto(), - name: language_server.name().to_string(), - worktree_id: Some(key.worktree_id.to_proto()), - }), - capabilities: serde_json::to_string(&server_capabilities) - .expect("serializing server LSP capabilities"), - }) - .log_err(); - } - self.lsp_server_capabilities - .insert(server_id, server_capabilities); - - // Tell the language server about every open buffer in the worktree that matches the language. - // Also check for buffers in worktrees that reused this server - let mut worktrees_using_server = vec![key.worktree_id]; - if let Some(local) = self.as_local() { - // Find all worktrees that have this server in their language server tree - for (worktree_id, servers) in &local.lsp_tree.instances { - if *worktree_id != key.worktree_id { - for server_map in servers.roots.values() { - if server_map - .values() - .any(|(node, _)| node.id() == Some(server_id)) - { - worktrees_using_server.push(*worktree_id); - } - } - } - } - } - - let mut buffer_paths_registered = Vec::new(); - self.buffer_store.clone().update(cx, |buffer_store, cx| { - let mut lsp_adapters = HashMap::default(); - for buffer_handle in buffer_store.buffers() { - let buffer = buffer_handle.read(cx); - let file = match File::from_dyn(buffer.file()) { - Some(file) => file, - None => continue, - }; - let language = match buffer.language() { - Some(language) => language, - None => continue, - }; - - if !worktrees_using_server.contains(&file.worktree.read(cx).id()) - || !lsp_adapters - .entry(language.name()) - .or_insert_with(|| self.languages.lsp_adapters(&language.name())) - .iter() - .any(|a| a.name == key.name) - { - continue; - } - // didOpen - let file = match file.as_local() { - Some(file) => file, - None => continue, - }; - - let local = self.as_local_mut().unwrap(); - - let buffer_id = buffer.remote_id(); - if local.registered_buffers.contains_key(&buffer_id) { - let versions = local - .buffer_snapshots - .entry(buffer_id) - .or_default() - .entry(server_id) - .and_modify(|_| { - assert!( - false, - "There should not be an existing snapshot for a newly inserted buffer" - ) - }) - .or_insert_with(|| { - vec![LspBufferSnapshot { - version: 0, - snapshot: buffer.text_snapshot(), - }] - }); - - let snapshot = versions.last().unwrap(); - let version = snapshot.version; - let initial_snapshot = &snapshot.snapshot; - let uri = lsp::Uri::from_file_path(file.abs_path(cx)).unwrap(); - language_server.register_buffer( - uri, - adapter.language_id(&language.name()), - version, - initial_snapshot.text(), - ); - buffer_paths_registered.push((buffer_id, file.abs_path(cx))); - local - .buffers_opened_in_servers - .entry(buffer_id) - .or_default() - .insert(server_id); - } - buffer_handle.update(cx, |buffer, cx| { - buffer.set_completion_triggers( - server_id, - language_server - .capabilities() - .completion_provider - .as_ref() - .and_then(|provider| { - provider - .trigger_characters - .as_ref() - .map(|characters| characters.iter().cloned().collect()) - }) - .unwrap_or_default(), - cx, - ) - }); - } - }); - - for (buffer_id, abs_path) in buffer_paths_registered { - cx.emit(LspStoreEvent::LanguageServerUpdate { - language_server_id: server_id, - name: Some(adapter.name()), - message: proto::update_language_server::Variant::RegisteredForBuffer( - proto::RegisteredForBuffer { - buffer_abs_path: abs_path.to_string_lossy().into_owned(), - buffer_id: buffer_id.to_proto(), - }, - ), - }); - } - - cx.notify(); - } - - pub fn language_servers_running_disk_based_diagnostics( - &self, - ) -> impl Iterator + '_ { - self.language_server_statuses - .iter() - .filter_map(|(id, status)| { - if status.has_pending_diagnostic_updates { - Some(*id) - } else { - None - } - }) - } - - pub(crate) fn cancel_language_server_work_for_buffers( - &mut self, - buffers: impl IntoIterator>, - cx: &mut Context, - ) { - if let Some((client, project_id)) = self.upstream_client() { - let request = client.request(proto::CancelLanguageServerWork { - project_id, - work: Some(proto::cancel_language_server_work::Work::Buffers( - proto::cancel_language_server_work::Buffers { - buffer_ids: buffers - .into_iter() - .map(|b| b.read(cx).remote_id().to_proto()) - .collect(), - }, - )), - }); - cx.background_spawn(request).detach_and_log_err(cx); - } else if let Some(local) = self.as_local() { - let servers = buffers - .into_iter() - .flat_map(|buffer| { - buffer.update(cx, |buffer, cx| { - local.language_server_ids_for_buffer(buffer, cx).into_iter() - }) - }) - .collect::>(); - for server_id in servers { - self.cancel_language_server_work(server_id, None, cx); - } - } - } - - pub(crate) fn cancel_language_server_work( - &mut self, - server_id: LanguageServerId, - token_to_cancel: Option, - cx: &mut Context, - ) { - if let Some(local) = self.as_local() { - let status = self.language_server_statuses.get(&server_id); - let server = local.language_servers.get(&server_id); - if let Some((LanguageServerState::Running { server, .. }, status)) = server.zip(status) - { - for (token, progress) in &status.pending_work { - if let Some(token_to_cancel) = token_to_cancel.as_ref() - && token != token_to_cancel - { - continue; - } - if progress.is_cancellable { - server - .notify::( - WorkDoneProgressCancelParams { - token: token.to_lsp(), - }, - ) - .ok(); - } - } - } - } else if let Some((client, project_id)) = self.upstream_client() { - let request = client.request(proto::CancelLanguageServerWork { - project_id, - work: Some( - proto::cancel_language_server_work::Work::LanguageServerWork( - proto::cancel_language_server_work::LanguageServerWork { - language_server_id: server_id.to_proto(), - token: token_to_cancel.map(|token| token.to_proto()), - }, - ), - ), - }); - cx.background_spawn(request).detach_and_log_err(cx); - } - } - - fn register_supplementary_language_server( - &mut self, - id: LanguageServerId, - name: LanguageServerName, - server: Arc, - cx: &mut Context, - ) { - if let Some(local) = self.as_local_mut() { - local - .supplementary_language_servers - .insert(id, (name.clone(), server)); - cx.emit(LspStoreEvent::LanguageServerAdded(id, name, None)); - } - } - - fn unregister_supplementary_language_server( - &mut self, - id: LanguageServerId, - cx: &mut Context, - ) { - if let Some(local) = self.as_local_mut() { - local.supplementary_language_servers.remove(&id); - cx.emit(LspStoreEvent::LanguageServerRemoved(id)); - } - } - - pub(crate) fn supplementary_language_servers( - &self, - ) -> impl '_ + Iterator { - self.as_local().into_iter().flat_map(|local| { - local - .supplementary_language_servers - .iter() - .map(|(id, (name, _))| (*id, name.clone())) - }) - } - - pub fn language_server_adapter_for_id( - &self, - id: LanguageServerId, - ) -> Option> { - self.as_local() - .and_then(|local| local.language_servers.get(&id)) - .and_then(|language_server_state| match language_server_state { - LanguageServerState::Running { adapter, .. } => Some(adapter.clone()), - _ => None, - }) - } - - pub(super) fn update_local_worktree_language_servers( - &mut self, - worktree_handle: &Entity, - changes: &[(Arc, ProjectEntryId, PathChange)], - cx: &mut Context, - ) { - if changes.is_empty() { - return; - } - - let Some(local) = self.as_local() else { return }; - - local.prettier_store.update(cx, |prettier_store, cx| { - prettier_store.update_prettier_settings(worktree_handle, changes, cx) - }); - - let worktree_id = worktree_handle.read(cx).id(); - let mut language_server_ids = local - .language_server_ids - .iter() - .filter_map(|(seed, v)| seed.worktree_id.eq(&worktree_id).then(|| v.id)) - .collect::>(); - language_server_ids.sort(); - language_server_ids.dedup(); - - // let abs_path = worktree_handle.read(cx).abs_path(); - for server_id in &language_server_ids { - if let Some(LanguageServerState::Running { server, .. }) = - local.language_servers.get(server_id) - && let Some(watched_paths) = local - .language_server_watched_paths - .get(server_id) - .and_then(|paths| paths.worktree_paths.get(&worktree_id)) - { - let params = lsp::DidChangeWatchedFilesParams { - changes: changes - .iter() - .filter_map(|(path, _, change)| { - if !watched_paths.is_match(path.as_std_path()) { - return None; - } - let typ = match change { - PathChange::Loaded => return None, - PathChange::Added => lsp::FileChangeType::CREATED, - PathChange::Removed => lsp::FileChangeType::DELETED, - PathChange::Updated => lsp::FileChangeType::CHANGED, - PathChange::AddedOrUpdated => lsp::FileChangeType::CHANGED, - }; - let uri = lsp::Uri::from_file_path( - worktree_handle.read(cx).absolutize(&path), - ) - .ok()?; - Some(lsp::FileEvent { uri, typ }) - }) - .collect(), - }; - if !params.changes.is_empty() { - server - .notify::(params) - .ok(); - } - } - } - for (path, _, _) in changes { - if let Some(file_name) = path.file_name() - && local.watched_manifest_filenames.contains(file_name) - { - self.request_workspace_config_refresh(); - break; - } - } - } - - pub fn wait_for_remote_buffer( - &mut self, - id: BufferId, - cx: &mut Context, - ) -> Task>> { - self.buffer_store.update(cx, |buffer_store, cx| { - buffer_store.wait_for_remote_buffer(id, cx) - }) - } - - fn serialize_symbol(symbol: &Symbol) -> proto::Symbol { - let mut result = proto::Symbol { - language_server_name: symbol.language_server_name.0.to_string(), - source_worktree_id: symbol.source_worktree_id.to_proto(), - language_server_id: symbol.source_language_server_id.to_proto(), - name: symbol.name.clone(), - kind: unsafe { mem::transmute::(symbol.kind) }, - start: Some(proto::PointUtf16 { - row: symbol.range.start.0.row, - column: symbol.range.start.0.column, - }), - end: Some(proto::PointUtf16 { - row: symbol.range.end.0.row, - column: symbol.range.end.0.column, - }), - worktree_id: Default::default(), - path: Default::default(), - signature: Default::default(), - }; - match &symbol.path { - SymbolLocation::InProject(path) => { - result.worktree_id = path.worktree_id.to_proto(); - result.path = path.path.to_proto(); - } - SymbolLocation::OutsideProject { - abs_path, - signature, - } => { - result.path = abs_path.to_string_lossy().into_owned(); - result.signature = signature.to_vec(); - } - } - result - } - - fn deserialize_symbol(serialized_symbol: proto::Symbol) -> Result { - let source_worktree_id = WorktreeId::from_proto(serialized_symbol.source_worktree_id); - let worktree_id = WorktreeId::from_proto(serialized_symbol.worktree_id); - let kind = unsafe { mem::transmute::(serialized_symbol.kind) }; - - let path = if serialized_symbol.signature.is_empty() { - SymbolLocation::InProject(ProjectPath { - worktree_id, - path: RelPath::from_proto(&serialized_symbol.path) - .context("invalid symbol path")?, - }) - } else { - SymbolLocation::OutsideProject { - abs_path: Path::new(&serialized_symbol.path).into(), - signature: serialized_symbol - .signature - .try_into() - .map_err(|_| anyhow!("invalid signature"))?, - } - }; - - let start = serialized_symbol.start.context("invalid start")?; - let end = serialized_symbol.end.context("invalid end")?; - Ok(CoreSymbol { - language_server_name: LanguageServerName(serialized_symbol.language_server_name.into()), - source_worktree_id, - source_language_server_id: LanguageServerId::from_proto( - serialized_symbol.language_server_id, - ), - path, - name: serialized_symbol.name, - range: Unclipped(PointUtf16::new(start.row, start.column)) - ..Unclipped(PointUtf16::new(end.row, end.column)), - kind, - }) - } - - pub(crate) fn serialize_completion(completion: &CoreCompletion) -> proto::Completion { - let mut serialized_completion = proto::Completion { - old_replace_start: Some(serialize_anchor(&completion.replace_range.start)), - old_replace_end: Some(serialize_anchor(&completion.replace_range.end)), - new_text: completion.new_text.clone(), - ..proto::Completion::default() - }; - match &completion.source { - CompletionSource::Lsp { - insert_range, - server_id, - lsp_completion, - lsp_defaults, - resolved, - } => { - let (old_insert_start, old_insert_end) = insert_range - .as_ref() - .map(|range| (serialize_anchor(&range.start), serialize_anchor(&range.end))) - .unzip(); - - serialized_completion.old_insert_start = old_insert_start; - serialized_completion.old_insert_end = old_insert_end; - serialized_completion.source = proto::completion::Source::Lsp as i32; - serialized_completion.server_id = server_id.0 as u64; - serialized_completion.lsp_completion = serde_json::to_vec(lsp_completion).unwrap(); - serialized_completion.lsp_defaults = lsp_defaults - .as_deref() - .map(|lsp_defaults| serde_json::to_vec(lsp_defaults).unwrap()); - serialized_completion.resolved = *resolved; - } - CompletionSource::BufferWord { - word_range, - resolved, - } => { - serialized_completion.source = proto::completion::Source::BufferWord as i32; - serialized_completion.buffer_word_start = Some(serialize_anchor(&word_range.start)); - serialized_completion.buffer_word_end = Some(serialize_anchor(&word_range.end)); - serialized_completion.resolved = *resolved; - } - CompletionSource::Custom => { - serialized_completion.source = proto::completion::Source::Custom as i32; - serialized_completion.resolved = true; - } - CompletionSource::Dap { sort_text } => { - serialized_completion.source = proto::completion::Source::Dap as i32; - serialized_completion.sort_text = Some(sort_text.clone()); - } - } - - serialized_completion - } - - pub(crate) fn deserialize_completion(completion: proto::Completion) -> Result { - let old_replace_start = completion - .old_replace_start - .and_then(deserialize_anchor) - .context("invalid old start")?; - let old_replace_end = completion - .old_replace_end - .and_then(deserialize_anchor) - .context("invalid old end")?; - let insert_range = { - match completion.old_insert_start.zip(completion.old_insert_end) { - Some((start, end)) => { - let start = deserialize_anchor(start).context("invalid insert old start")?; - let end = deserialize_anchor(end).context("invalid insert old end")?; - Some(start..end) - } - None => None, - } - }; - Ok(CoreCompletion { - replace_range: old_replace_start..old_replace_end, - new_text: completion.new_text, - source: match proto::completion::Source::from_i32(completion.source) { - Some(proto::completion::Source::Custom) => CompletionSource::Custom, - Some(proto::completion::Source::Lsp) => CompletionSource::Lsp { - insert_range, - server_id: LanguageServerId::from_proto(completion.server_id), - lsp_completion: serde_json::from_slice(&completion.lsp_completion)?, - lsp_defaults: completion - .lsp_defaults - .as_deref() - .map(serde_json::from_slice) - .transpose()?, - resolved: completion.resolved, - }, - Some(proto::completion::Source::BufferWord) => { - let word_range = completion - .buffer_word_start - .and_then(deserialize_anchor) - .context("invalid buffer word start")? - ..completion - .buffer_word_end - .and_then(deserialize_anchor) - .context("invalid buffer word end")?; - CompletionSource::BufferWord { - word_range, - resolved: completion.resolved, - } - } - Some(proto::completion::Source::Dap) => CompletionSource::Dap { - sort_text: completion - .sort_text - .context("expected sort text to exist")?, - }, - _ => anyhow::bail!("Unexpected completion source {}", completion.source), - }, - }) - } - - pub(crate) fn serialize_code_action(action: &CodeAction) -> proto::CodeAction { - let (kind, lsp_action) = match &action.lsp_action { - LspAction::Action(code_action) => ( - proto::code_action::Kind::Action as i32, - serde_json::to_vec(code_action).unwrap(), - ), - LspAction::Command(command) => ( - proto::code_action::Kind::Command as i32, - serde_json::to_vec(command).unwrap(), - ), - LspAction::CodeLens(code_lens) => ( - proto::code_action::Kind::CodeLens as i32, - serde_json::to_vec(code_lens).unwrap(), - ), - }; - - proto::CodeAction { - server_id: action.server_id.0 as u64, - start: Some(serialize_anchor(&action.range.start)), - end: Some(serialize_anchor(&action.range.end)), - lsp_action, - kind, - resolved: action.resolved, - } - } - - pub(crate) fn deserialize_code_action(action: proto::CodeAction) -> Result { - let start = action - .start - .and_then(deserialize_anchor) - .context("invalid start")?; - let end = action - .end - .and_then(deserialize_anchor) - .context("invalid end")?; - let lsp_action = match proto::code_action::Kind::from_i32(action.kind) { - Some(proto::code_action::Kind::Action) => { - LspAction::Action(serde_json::from_slice(&action.lsp_action)?) - } - Some(proto::code_action::Kind::Command) => { - LspAction::Command(serde_json::from_slice(&action.lsp_action)?) - } - Some(proto::code_action::Kind::CodeLens) => { - LspAction::CodeLens(serde_json::from_slice(&action.lsp_action)?) - } - None => anyhow::bail!("Unknown action kind {}", action.kind), - }; - Ok(CodeAction { - server_id: LanguageServerId(action.server_id as usize), - range: start..end, - resolved: action.resolved, - lsp_action, - }) - } - - fn update_last_formatting_failure(&mut self, formatting_result: &anyhow::Result) { - match &formatting_result { - Ok(_) => self.last_formatting_failure = None, - Err(error) => { - let error_string = format!("{error:#}"); - log::error!("Formatting failed: {error_string}"); - self.last_formatting_failure - .replace(error_string.lines().join(" ")); - } - } - } - - fn cleanup_lsp_data(&mut self, for_server: LanguageServerId) { - self.lsp_server_capabilities.remove(&for_server); - for lsp_data in self.lsp_data.values_mut() { - lsp_data.remove_server_data(for_server); - } - if let Some(local) = self.as_local_mut() { - local.buffer_pull_diagnostics_result_ids.remove(&for_server); - local - .workspace_pull_diagnostics_result_ids - .remove(&for_server); - for buffer_servers in local.buffers_opened_in_servers.values_mut() { - buffer_servers.remove(&for_server); - } - } - } - - pub fn result_id_for_buffer_pull( - &self, - server_id: LanguageServerId, - buffer_id: BufferId, - registration_id: &Option, - cx: &App, - ) -> Option { - let abs_path = self - .buffer_store - .read(cx) - .get(buffer_id) - .and_then(|b| File::from_dyn(b.read(cx).file())) - .map(|f| f.abs_path(cx))?; - self.as_local()? - .buffer_pull_diagnostics_result_ids - .get(&server_id)? - .get(registration_id)? - .get(&abs_path)? - .clone() - } - - /// Gets all result_ids for a workspace diagnostics pull request. - /// First, it tries to find buffer's result_id retrieved via the diagnostics pull; if it fails, it falls back to the workspace disagnostics pull result_id. - /// The latter is supposed to be of lower priority as we keep on pulling diagnostics for open buffers eagerly. - pub fn result_ids_for_workspace_refresh( - &self, - server_id: LanguageServerId, - registration_id: &Option, - ) -> HashMap { - let Some(local) = self.as_local() else { - return HashMap::default(); - }; - local - .workspace_pull_diagnostics_result_ids - .get(&server_id) - .into_iter() - .filter_map(|diagnostics| diagnostics.get(registration_id)) - .flatten() - .filter_map(|(abs_path, result_id)| { - let result_id = local - .buffer_pull_diagnostics_result_ids - .get(&server_id) - .and_then(|buffer_ids_result_ids| { - buffer_ids_result_ids.get(registration_id)?.get(abs_path) - }) - .cloned() - .flatten() - .or_else(|| result_id.clone())?; - Some((abs_path.clone(), result_id)) - }) - .collect() - } - - pub fn pull_workspace_diagnostics(&mut self, server_id: LanguageServerId) { - if let Some(LanguageServerState::Running { - workspace_diagnostics_refresh_tasks, - .. - }) = self - .as_local_mut() - .and_then(|local| local.language_servers.get_mut(&server_id)) - { - for diagnostics in workspace_diagnostics_refresh_tasks.values_mut() { - diagnostics.refresh_tx.try_send(()).ok(); - } - } - } - - pub fn pull_workspace_diagnostics_for_buffer(&mut self, buffer_id: BufferId, cx: &mut App) { - let Some(buffer) = self.buffer_store().read(cx).get_existing(buffer_id).ok() else { - return; - }; - let Some(local) = self.as_local_mut() else { - return; - }; - - for server_id in buffer.update(cx, |buffer, cx| { - local.language_server_ids_for_buffer(buffer, cx) - }) { - if let Some(LanguageServerState::Running { - workspace_diagnostics_refresh_tasks, - .. - }) = local.language_servers.get_mut(&server_id) - { - for diagnostics in workspace_diagnostics_refresh_tasks.values_mut() { - diagnostics.refresh_tx.try_send(()).ok(); - } - } - } - } - - fn apply_workspace_diagnostic_report( - &mut self, - server_id: LanguageServerId, - report: lsp::WorkspaceDiagnosticReportResult, - registration_id: Option, - cx: &mut Context, - ) { - let workspace_diagnostics = - GetDocumentDiagnostics::deserialize_workspace_diagnostics_report( - report, - server_id, - registration_id, - ); - let mut unchanged_buffers = HashMap::default(); - let workspace_diagnostics_updates = workspace_diagnostics - .into_iter() - .filter_map( - |workspace_diagnostics| match workspace_diagnostics.diagnostics { - LspPullDiagnostics::Response { - server_id, - uri, - diagnostics, - registration_id, - } => Some(( - server_id, - uri, - diagnostics, - workspace_diagnostics.version, - registration_id, - )), - LspPullDiagnostics::Default => None, - }, - ) - .fold( - HashMap::default(), - |mut acc, (server_id, uri, diagnostics, version, new_registration_id)| { - let (result_id, diagnostics) = match diagnostics { - PulledDiagnostics::Unchanged { result_id } => { - unchanged_buffers - .entry(new_registration_id.clone()) - .or_insert_with(HashSet::default) - .insert(uri.clone()); - (Some(result_id), Vec::new()) - } - PulledDiagnostics::Changed { - result_id, - diagnostics, - } => (result_id, diagnostics), - }; - let disk_based_sources = Cow::Owned( - self.language_server_adapter_for_id(server_id) - .as_ref() - .map(|adapter| adapter.disk_based_diagnostic_sources.as_slice()) - .unwrap_or(&[]) - .to_vec(), - ); - - let Some(abs_path) = uri.to_file_path().ok() else { - return acc; - }; - let Some((worktree, relative_path)) = - self.worktree_store.read(cx).find_worktree(abs_path.clone(), cx) - else { - log::warn!("skipping workspace diagnostics update, no worktree found for path {abs_path:?}"); - return acc; - }; - let worktree_id = worktree.read(cx).id(); - let project_path = ProjectPath { - worktree_id, - path: relative_path, - }; - if let Some(local_lsp_store) = self.as_local_mut() { - local_lsp_store.workspace_pull_diagnostics_result_ids.entry(server_id) - .or_default().entry(new_registration_id.clone()).or_default().insert(abs_path, result_id.clone()); - } - // The LSP spec recommends that "diagnostics from a document pull should win over diagnostics from a workspace pull." - // Since we actively pull diagnostics for documents with open buffers, we ignore contents of workspace pulls for these documents. - if self.buffer_store.read(cx).get_by_path(&project_path).is_none() { - acc.entry(server_id) - .or_insert_with(HashMap::default) - .entry(new_registration_id.clone()) - .or_insert_with(Vec::new) - .push(DocumentDiagnosticsUpdate { - server_id, - diagnostics: lsp::PublishDiagnosticsParams { - uri, - diagnostics, - version, - }, - result_id, - disk_based_sources, - registration_id: new_registration_id, - }); - } - acc - }, - ); - - for diagnostic_updates in workspace_diagnostics_updates.into_values() { - for (registration_id, diagnostic_updates) in diagnostic_updates { - self.merge_lsp_diagnostics( - DiagnosticSourceKind::Pulled, - diagnostic_updates, - |document_uri, old_diagnostic, _| match old_diagnostic.source_kind { - DiagnosticSourceKind::Pulled => { - old_diagnostic.registration_id != registration_id - || unchanged_buffers - .get(&old_diagnostic.registration_id) - .is_some_and(|unchanged_buffers| { - unchanged_buffers.contains(&document_uri) - }) - } - DiagnosticSourceKind::Other | DiagnosticSourceKind::Pushed => true, - }, - cx, - ) - .log_err(); - } - } - } - - fn register_server_capabilities( - &mut self, - server_id: LanguageServerId, - params: lsp::RegistrationParams, - cx: &mut Context, - ) -> anyhow::Result<()> { - let server = self - .language_server_for_id(server_id) - .with_context(|| format!("no server {server_id} found"))?; - for reg in params.registrations { - match reg.method.as_str() { - "workspace/didChangeWatchedFiles" => { - if let Some(options) = reg.register_options { - let notify = if let Some(local_lsp_store) = self.as_local_mut() { - let caps = serde_json::from_value(options)?; - local_lsp_store - .on_lsp_did_change_watched_files(server_id, ®.id, caps, cx); - true - } else { - false - }; - if notify { - notify_server_capabilities_updated(&server, cx); - } - } - } - "workspace/didChangeConfiguration" => { - // Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings. - } - "workspace/didChangeWorkspaceFolders" => { - // In this case register options is an empty object, we can ignore it - let caps = lsp::WorkspaceFoldersServerCapabilities { - supported: Some(true), - change_notifications: Some(OneOf::Right(reg.id)), - }; - server.update_capabilities(|capabilities| { - capabilities - .workspace - .get_or_insert_default() - .workspace_folders = Some(caps); - }); - notify_server_capabilities_updated(&server, cx); - } - "workspace/symbol" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.workspace_symbol_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } - "workspace/fileOperations" => { - if let Some(options) = reg.register_options { - let caps = serde_json::from_value(options)?; - server.update_capabilities(|capabilities| { - capabilities - .workspace - .get_or_insert_default() - .file_operations = Some(caps); - }); - notify_server_capabilities_updated(&server, cx); - } - } - "workspace/executeCommand" => { - if let Some(options) = reg.register_options { - let options = serde_json::from_value(options)?; - server.update_capabilities(|capabilities| { - capabilities.execute_command_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } - } - "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); - } - "textDocument/onTypeFormatting" => { - if let Some(options) = reg - .register_options - .map(serde_json::from_value) - .transpose()? - { - server.update_capabilities(|capabilities| { - capabilities.document_on_type_formatting_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } - } - "textDocument/formatting" => { - let 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); - } - "textDocument/inlayHint" => { - let 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); - } - "textDocument/codeAction" => { - let options = parse_register_capabilities(reg)?; - let provider = match options { - OneOf::Left(value) => lsp::CodeActionProviderCapability::Simple(value), - OneOf::Right(caps) => caps, - }; - server.update_capabilities(|capabilities| { - capabilities.code_action_provider = Some(provider); - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/definition" => { - let options = parse_register_capabilities(reg)?; - server.update_capabilities(|capabilities| { - capabilities.definition_provider = Some(options); - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/completion" => { - if let Some(caps) = reg - .register_options - .map(serde_json::from_value::) - .transpose()? - { - server.update_capabilities(|capabilities| { - capabilities.completion_provider = Some(caps.clone()); - }); - - if let Some(local) = self.as_local() { - let mut buffers_with_language_server = Vec::new(); - for handle in self.buffer_store.read(cx).buffers() { - let buffer_id = handle.read(cx).remote_id(); - if local - .buffers_opened_in_servers - .get(&buffer_id) - .filter(|s| s.contains(&server_id)) - .is_some() - { - buffers_with_language_server.push(handle); - } - } - let triggers = caps - .trigger_characters - .unwrap_or_default() - .into_iter() - .collect::>(); - for handle in buffers_with_language_server { - let triggers = triggers.clone(); - let _ = handle.update(cx, move |buffer, cx| { - buffer.set_completion_triggers(server_id, triggers, cx); - }); - } - } - notify_server_capabilities_updated(&server, cx); - } - } - "textDocument/hover" => { - let options = parse_register_capabilities(reg)?; - let provider = match options { - OneOf::Left(value) => lsp::HoverProviderCapability::Simple(value), - OneOf::Right(caps) => caps, - }; - server.update_capabilities(|capabilities| { - capabilities.hover_provider = Some(provider); - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/signatureHelp" => { - if let Some(caps) = reg - .register_options - .map(serde_json::from_value) - .transpose()? - { - server.update_capabilities(|capabilities| { - capabilities.signature_help_provider = Some(caps); - }); - notify_server_capabilities_updated(&server, cx); - } - } - "textDocument/didChange" => { - if let Some(sync_kind) = reg - .register_options - .and_then(|opts| opts.get("syncKind").cloned()) - .map(serde_json::from_value::) - .transpose()? - { - server.update_capabilities(|capabilities| { - let mut sync_options = - Self::take_text_document_sync_options(capabilities); - sync_options.change = Some(sync_kind); - capabilities.text_document_sync = - Some(lsp::TextDocumentSyncCapability::Options(sync_options)); - }); - notify_server_capabilities_updated(&server, cx); - } - } - "textDocument/didSave" => { - if let Some(include_text) = reg - .register_options - .map(|opts| { - let transpose = opts - .get("includeText") - .cloned() - .map(serde_json::from_value::>) - .transpose(); - match transpose { - Ok(value) => Ok(value.flatten()), - Err(e) => Err(e), - } - }) - .transpose()? - { - server.update_capabilities(|capabilities| { - let mut sync_options = - Self::take_text_document_sync_options(capabilities); - sync_options.save = - Some(TextDocumentSyncSaveOptions::SaveOptions(lsp::SaveOptions { - include_text, - })); - capabilities.text_document_sync = - Some(lsp::TextDocumentSyncCapability::Options(sync_options)); - }); - notify_server_capabilities_updated(&server, cx); - } - } - "textDocument/codeLens" => { - if let Some(caps) = reg - .register_options - .map(serde_json::from_value) - .transpose()? - { - server.update_capabilities(|capabilities| { - capabilities.code_lens_provider = Some(caps); - }); - notify_server_capabilities_updated(&server, cx); - } - } - "textDocument/diagnostic" => { - if let Some(caps) = reg - .register_options - .map(serde_json::from_value::) - .transpose()? - { - let local = self - .as_local_mut() - .context("Expected LSP Store to be local")?; - let state = local - .language_servers - .get_mut(&server_id) - .context("Could not obtain Language Servers state")?; - local - .language_server_dynamic_registrations - .entry(server_id) - .or_default() - .diagnostics - .insert(Some(reg.id.clone()), caps.clone()); - - let supports_workspace_diagnostics = - |capabilities: &DiagnosticServerCapabilities| match capabilities { - DiagnosticServerCapabilities::Options(diagnostic_options) => { - diagnostic_options.workspace_diagnostics - } - DiagnosticServerCapabilities::RegistrationOptions( - diagnostic_registration_options, - ) => { - diagnostic_registration_options - .diagnostic_options - .workspace_diagnostics - } - }; - - if supports_workspace_diagnostics(&caps) { - if let LanguageServerState::Running { - workspace_diagnostics_refresh_tasks, - .. - } = state - && let Some(task) = lsp_workspace_diagnostics_refresh( - Some(reg.id.clone()), - caps.clone(), - server.clone(), - cx, - ) - { - workspace_diagnostics_refresh_tasks.insert(Some(reg.id), task); - } - } - - server.update_capabilities(|capabilities| { - capabilities.diagnostic_provider = Some(caps); - }); - - notify_server_capabilities_updated(&server, cx); - } - } - "textDocument/documentColor" => { - let options = parse_register_capabilities(reg)?; - let provider = match options { - OneOf::Left(value) => lsp::ColorProviderCapability::Simple(value), - OneOf::Right(caps) => caps, - }; - server.update_capabilities(|capabilities| { - capabilities.color_provider = Some(provider); - }); - notify_server_capabilities_updated(&server, cx); - } - _ => log::warn!("unhandled capability registration: {reg:?}"), - } - } - - Ok(()) - } - - fn unregister_server_capabilities( - &mut self, - server_id: LanguageServerId, - params: lsp::UnregistrationParams, - cx: &mut Context, - ) -> anyhow::Result<()> { - let server = self - .language_server_for_id(server_id) - .with_context(|| format!("no server {server_id} found"))?; - for unreg in params.unregisterations.iter() { - match unreg.method.as_str() { - "workspace/didChangeWatchedFiles" => { - let notify = if let Some(local_lsp_store) = self.as_local_mut() { - local_lsp_store - .on_lsp_unregister_did_change_watched_files(server_id, &unreg.id, cx); - true - } else { - false - }; - if notify { - notify_server_capabilities_updated(&server, cx); - } - } - "workspace/didChangeConfiguration" => { - // Ignore payload since we notify clients of setting changes unconditionally, relying on them pulling the latest settings. - } - "workspace/didChangeWorkspaceFolders" => { - server.update_capabilities(|capabilities| { - capabilities - .workspace - .get_or_insert_with(|| lsp::WorkspaceServerCapabilities { - workspace_folders: None, - file_operations: None, - }) - .workspace_folders = None; - }); - notify_server_capabilities_updated(&server, cx); - } - "workspace/symbol" => { - server.update_capabilities(|capabilities| { - capabilities.workspace_symbol_provider = None - }); - notify_server_capabilities_updated(&server, cx); - } - "workspace/fileOperations" => { - server.update_capabilities(|capabilities| { - capabilities - .workspace - .get_or_insert_with(|| lsp::WorkspaceServerCapabilities { - workspace_folders: None, - file_operations: None, - }) - .file_operations = None; - }); - notify_server_capabilities_updated(&server, cx); - } - "workspace/executeCommand" => { - server.update_capabilities(|capabilities| { - capabilities.execute_command_provider = None; - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/rangeFormatting" => { - server.update_capabilities(|capabilities| { - capabilities.document_range_formatting_provider = None - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/onTypeFormatting" => { - server.update_capabilities(|capabilities| { - capabilities.document_on_type_formatting_provider = None; - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/formatting" => { - server.update_capabilities(|capabilities| { - capabilities.document_formatting_provider = None; - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/rename" => { - server.update_capabilities(|capabilities| capabilities.rename_provider = None); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/codeAction" => { - server.update_capabilities(|capabilities| { - capabilities.code_action_provider = None; - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/definition" => { - server.update_capabilities(|capabilities| { - capabilities.definition_provider = None; - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/completion" => { - server.update_capabilities(|capabilities| { - capabilities.completion_provider = None; - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/hover" => { - server.update_capabilities(|capabilities| { - capabilities.hover_provider = None; - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/signatureHelp" => { - server.update_capabilities(|capabilities| { - capabilities.signature_help_provider = None; - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/didChange" => { - server.update_capabilities(|capabilities| { - let mut sync_options = Self::take_text_document_sync_options(capabilities); - sync_options.change = None; - capabilities.text_document_sync = - Some(lsp::TextDocumentSyncCapability::Options(sync_options)); - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/didSave" => { - server.update_capabilities(|capabilities| { - let mut sync_options = Self::take_text_document_sync_options(capabilities); - sync_options.save = None; - capabilities.text_document_sync = - Some(lsp::TextDocumentSyncCapability::Options(sync_options)); - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/codeLens" => { - server.update_capabilities(|capabilities| { - capabilities.code_lens_provider = None; - }); - notify_server_capabilities_updated(&server, cx); - } - "textDocument/diagnostic" => { - let local = self - .as_local_mut() - .context("Expected LSP Store to be local")?; - - let state = local - .language_servers - .get_mut(&server_id) - .context("Could not obtain Language Servers state")?; - let registrations = local - .language_server_dynamic_registrations - .get_mut(&server_id) - .with_context(|| { - format!("Expected dynamic registration to exist for server {server_id}") - })?; - registrations.diagnostics - .remove(&Some(unreg.id.clone())) - .with_context(|| format!( - "Attempted to unregister non-existent diagnostic registration with ID {}", - unreg.id) - )?; - let removed_last_diagnostic_provider = registrations.diagnostics.is_empty(); - - if let LanguageServerState::Running { - workspace_diagnostics_refresh_tasks, - .. - } = state - { - workspace_diagnostics_refresh_tasks.remove(&Some(unreg.id.clone())); - } - - if removed_last_diagnostic_provider { - server.update_capabilities(|capabilities| { - debug_assert!(capabilities.diagnostic_provider.is_some()); - capabilities.diagnostic_provider = None; - }); - } - - notify_server_capabilities_updated(&server, cx); - } - "textDocument/documentColor" => { - server.update_capabilities(|capabilities| { - capabilities.color_provider = None; - }); - notify_server_capabilities_updated(&server, cx); - } - _ => log::warn!("unhandled capability unregistration: {unreg:?}"), - } - } - - Ok(()) - } - - async fn deduplicate_range_based_lsp_requests( - lsp_store: &Entity, - server_id: Option, - lsp_request_id: LspRequestId, - proto_request: &T::ProtoRequest, - range: Range, - cx: &mut AsyncApp, - ) -> Result<()> - where - T: LspCommand, - T::ProtoRequest: proto::LspRequestMessage, - { - let buffer_id = BufferId::new(proto_request.buffer_id())?; - let version = deserialize_version(proto_request.buffer_version()); - let buffer = lsp_store.update(cx, |this, cx| { - this.buffer_store.read(cx).get_existing(buffer_id) - })??; - buffer - .update(cx, |buffer, _| buffer.wait_for_version(version))? - .await?; - lsp_store.update(cx, |lsp_store, cx| { - let lsp_data = lsp_store.latest_lsp_data(&buffer, cx); - let chunks_queried_for = lsp_data - .inlay_hints - .applicable_chunks(&[range]) - .collect::>(); - match chunks_queried_for.as_slice() { - &[chunk] => { - let key = LspKey { - request_type: TypeId::of::(), - server_queried: server_id, - }; - let previous_request = lsp_data - .chunk_lsp_requests - .entry(key) - .or_default() - .insert(chunk, lsp_request_id); - if let Some((previous_request, running_requests)) = - previous_request.zip(lsp_data.lsp_requests.get_mut(&key)) - { - running_requests.remove(&previous_request); - } - } - _ambiguous_chunks => { - // Have not found a unique chunk for the query range — be lenient and let the query to be spawned, - // there, a buffer version-based check will be performed and outdated requests discarded. - } - } - anyhow::Ok(()) - })??; - - Ok(()) - } - - async fn query_lsp_locally( - lsp_store: Entity, - for_server_id: Option, - sender_id: proto::PeerId, - lsp_request_id: LspRequestId, - proto_request: T::ProtoRequest, - position: Option, - cx: &mut AsyncApp, - ) -> Result<()> - where - T: LspCommand + Clone, - T::ProtoRequest: proto::LspRequestMessage, - ::Response: - Into<::Response>, - { - let buffer_id = BufferId::new(proto_request.buffer_id())?; - let version = deserialize_version(proto_request.buffer_version()); - let buffer = lsp_store.update(cx, |this, cx| { - this.buffer_store.read(cx).get_existing(buffer_id) - })??; - buffer - .update(cx, |buffer, _| buffer.wait_for_version(version.clone()))? - .await?; - let buffer_version = buffer.read_with(cx, |buffer, _| buffer.version())?; - let request = - T::from_proto(proto_request, lsp_store.clone(), buffer.clone(), cx.clone()).await?; - let key = LspKey { - request_type: TypeId::of::(), - server_queried: for_server_id, - }; - lsp_store.update(cx, |lsp_store, cx| { - let request_task = match for_server_id { - Some(server_id) => { - let server_task = lsp_store.request_lsp( - buffer.clone(), - LanguageServerToQuery::Other(server_id), - request.clone(), - cx, - ); - cx.background_spawn(async move { - let mut responses = Vec::new(); - match server_task.await { - Ok(response) => responses.push((server_id, response)), - // rust-analyzer likes to error with this when its still loading up - Err(e) if format!("{e:#}").ends_with("content modified") => (), - Err(e) => log::error!( - "Error handling response for request {request:?}: {e:#}" - ), - } - responses - }) - } - None => lsp_store.request_multiple_lsp_locally(&buffer, position, request, cx), - }; - let lsp_data = lsp_store.latest_lsp_data(&buffer, cx); - if T::ProtoRequest::stop_previous_requests() { - if let Some(lsp_requests) = lsp_data.lsp_requests.get_mut(&key) { - lsp_requests.clear(); - } - } - lsp_data.lsp_requests.entry(key).or_default().insert( - lsp_request_id, - cx.spawn(async move |lsp_store, cx| { - let response = request_task.await; - lsp_store - .update(cx, |lsp_store, cx| { - if let Some((client, project_id)) = lsp_store.downstream_client.clone() - { - let response = response - .into_iter() - .map(|(server_id, response)| { - ( - server_id.to_proto(), - T::response_to_proto( - response, - lsp_store, - sender_id, - &buffer_version, - cx, - ) - .into(), - ) - }) - .collect::>(); - match client.send_lsp_response::( - project_id, - lsp_request_id, - response, - ) { - Ok(()) => {} - Err(e) => { - log::error!("Failed to send LSP response: {e:#}",) - } - } - } - }) - .ok(); - }), - ); - })?; - Ok(()) - } - - fn take_text_document_sync_options( - capabilities: &mut lsp::ServerCapabilities, - ) -> lsp::TextDocumentSyncOptions { - match capabilities.text_document_sync.take() { - Some(lsp::TextDocumentSyncCapability::Options(sync_options)) => sync_options, - Some(lsp::TextDocumentSyncCapability::Kind(sync_kind)) => { - let mut sync_options = lsp::TextDocumentSyncOptions::default(); - sync_options.change = Some(sync_kind); - sync_options - } - None => lsp::TextDocumentSyncOptions::default(), - } - } - - #[cfg(any(test, feature = "test-support"))] - pub fn forget_code_lens_task(&mut self, buffer_id: BufferId) -> Option { - Some( - self.lsp_data - .get_mut(&buffer_id)? - .code_lens - .take()? - .update - .take()? - .1, - ) - } - - pub fn downstream_client(&self) -> Option<(AnyProtoClient, u64)> { - self.downstream_client.clone() - } - - pub fn worktree_store(&self) -> Entity { - self.worktree_store.clone() - } - - /// Gets what's stored in the LSP data for the given buffer. - pub fn current_lsp_data(&mut self, buffer_id: BufferId) -> Option<&mut BufferLspData> { - self.lsp_data.get_mut(&buffer_id) - } - - /// Gets the most recent LSP data for the given buffer: if the data is absent or out of date, - /// new [`BufferLspData`] will be created to replace the previous state. - pub fn latest_lsp_data(&mut self, buffer: &Entity, cx: &mut App) -> &mut BufferLspData { - let (buffer_id, buffer_version) = - buffer.read_with(cx, |buffer, _| (buffer.remote_id(), buffer.version())); - let lsp_data = self - .lsp_data - .entry(buffer_id) - .or_insert_with(|| BufferLspData::new(buffer, cx)); - if buffer_version.changed_since(&lsp_data.buffer_version) { - *lsp_data = BufferLspData::new(buffer, cx); - } - lsp_data - } -} - -// Registration with registerOptions as null, should fallback to true. -// https://github.com/microsoft/vscode-languageserver-node/blob/d90a87f9557a0df9142cfb33e251cfa6fe27d970/client/src/common/client.ts#L2133 -fn parse_register_capabilities( - reg: lsp::Registration, -) -> Result> { - Ok(match reg.register_options { - Some(options) => OneOf::Right(serde_json::from_value::(options)?), - None => OneOf::Left(true), - }) -} - -fn subscribe_to_binary_statuses( - languages: &Arc, - cx: &mut Context<'_, LspStore>, -) -> Task<()> { - let mut server_statuses = languages.language_server_binary_statuses(); - cx.spawn(async move |lsp_store, cx| { - while let Some((server_name, binary_status)) = server_statuses.next().await { - if lsp_store - .update(cx, |_, cx| { - let mut message = None; - let binary_status = match binary_status { - BinaryStatus::None => proto::ServerBinaryStatus::None, - BinaryStatus::CheckingForUpdate => { - proto::ServerBinaryStatus::CheckingForUpdate - } - BinaryStatus::Downloading => proto::ServerBinaryStatus::Downloading, - BinaryStatus::Starting => proto::ServerBinaryStatus::Starting, - BinaryStatus::Stopping => proto::ServerBinaryStatus::Stopping, - BinaryStatus::Stopped => proto::ServerBinaryStatus::Stopped, - BinaryStatus::Failed { error } => { - message = Some(error); - proto::ServerBinaryStatus::Failed - } - }; - cx.emit(LspStoreEvent::LanguageServerUpdate { - // Binary updates are about the binary that might not have any language server id at that point. - // Reuse `LanguageServerUpdate` for them and provide a fake id that won't be used on the receiver side. - language_server_id: LanguageServerId(0), - name: Some(server_name), - message: proto::update_language_server::Variant::StatusUpdate( - proto::StatusUpdate { - message, - status: Some(proto::status_update::Status::Binary( - binary_status as i32, - )), - }, - ), - }); - }) - .is_err() - { - break; - } - } - }) -} - -fn lsp_workspace_diagnostics_refresh( - registration_id: Option, - options: DiagnosticServerCapabilities, - server: Arc, - cx: &mut Context<'_, LspStore>, -) -> Option { - let identifier = workspace_diagnostic_identifier(&options)?; - let registration_id_shared = registration_id.as_ref().map(SharedString::from); - - let (progress_tx, mut progress_rx) = mpsc::channel(1); - let (mut refresh_tx, mut refresh_rx) = mpsc::channel(1); - refresh_tx.try_send(()).ok(); - - let workspace_query_language_server = cx.spawn(async move |lsp_store, cx| { - let mut attempts = 0; - let max_attempts = 50; - let mut requests = 0; - - loop { - let Some(()) = refresh_rx.recv().await else { - return; - }; - - 'request: loop { - requests += 1; - if attempts > max_attempts { - log::error!( - "Failed to pull workspace diagnostics {max_attempts} times, aborting" - ); - return; - } - let backoff_millis = (50 * (1 << attempts)).clamp(30, 1000); - cx.background_executor() - .timer(Duration::from_millis(backoff_millis)) - .await; - attempts += 1; - - let Ok(previous_result_ids) = lsp_store.update(cx, |lsp_store, _| { - lsp_store - .result_ids_for_workspace_refresh(server.server_id(), ®istration_id_shared) - .into_iter() - .filter_map(|(abs_path, result_id)| { - let uri = file_path_to_lsp_url(&abs_path).ok()?; - Some(lsp::PreviousResultId { - uri, - value: result_id.to_string(), - }) - }) - .collect() - }) else { - return; - }; - - let token = if let Some(registration_id) = ®istration_id { - format!( - "workspace/diagnostic/{}/{requests}/{WORKSPACE_DIAGNOSTICS_TOKEN_START}{registration_id}", - server.server_id(), - ) - } else { - format!("workspace/diagnostic/{}/{requests}", server.server_id()) - }; - - progress_rx.try_recv().ok(); - let timer = - LanguageServer::default_request_timer(cx.background_executor().clone()).fuse(); - let progress = pin!(progress_rx.recv().fuse()); - let response_result = server - .request_with_timer::( - lsp::WorkspaceDiagnosticParams { - previous_result_ids, - identifier: identifier.clone(), - work_done_progress_params: Default::default(), - partial_result_params: lsp::PartialResultParams { - partial_result_token: Some(lsp::ProgressToken::String(token)), - }, - }, - select(timer, progress).then(|either| match either { - Either::Left((message, ..)) => ready(message).left_future(), - Either::Right(..) => pending::().right_future(), - }), - ) - .await; - - // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#diagnostic_refresh - // > If a server closes a workspace diagnostic pull request the client should re-trigger the request. - match response_result { - ConnectionResult::Timeout => { - log::error!("Timeout during workspace diagnostics pull"); - continue 'request; - } - ConnectionResult::ConnectionReset => { - log::error!("Server closed a workspace diagnostics pull request"); - continue 'request; - } - ConnectionResult::Result(Err(e)) => { - log::error!("Error during workspace diagnostics pull: {e:#}"); - break 'request; - } - ConnectionResult::Result(Ok(pulled_diagnostics)) => { - attempts = 0; - if lsp_store - .update(cx, |lsp_store, cx| { - lsp_store.apply_workspace_diagnostic_report( - server.server_id(), - pulled_diagnostics, - registration_id_shared.clone(), - cx, - ) - }) - .is_err() - { - return; - } - break 'request; - } - } - } - } - }); - - Some(WorkspaceRefreshTask { - refresh_tx, - progress_tx, - task: workspace_query_language_server, - }) -} - -fn buffer_diagnostic_identifier(options: &DiagnosticServerCapabilities) -> Option { - match &options { - lsp::DiagnosticServerCapabilities::Options(diagnostic_options) => { - diagnostic_options.identifier.clone() - } - lsp::DiagnosticServerCapabilities::RegistrationOptions(registration_options) => { - let diagnostic_options = ®istration_options.diagnostic_options; - diagnostic_options.identifier.clone() - } - } -} - -fn workspace_diagnostic_identifier( - options: &DiagnosticServerCapabilities, -) -> Option> { - match &options { - lsp::DiagnosticServerCapabilities::Options(diagnostic_options) => { - if !diagnostic_options.workspace_diagnostics { - return None; - } - Some(diagnostic_options.identifier.clone()) - } - lsp::DiagnosticServerCapabilities::RegistrationOptions(registration_options) => { - let diagnostic_options = ®istration_options.diagnostic_options; - if !diagnostic_options.workspace_diagnostics { - return None; - } - Some(diagnostic_options.identifier.clone()) - } - } -} - -fn resolve_word_completion(snapshot: &BufferSnapshot, completion: &mut Completion) { - let CompletionSource::BufferWord { - word_range, - resolved, - } = &mut completion.source - else { - return; - }; - if *resolved { - return; - } - - if completion.new_text - != snapshot - .text_for_range(word_range.clone()) - .collect::() - { - return; - } - - let mut offset = 0; - for chunk in snapshot.chunks(word_range.clone(), true) { - let end_offset = offset + chunk.text.len(); - if let Some(highlight_id) = chunk.syntax_highlight_id { - completion - .label - .runs - .push((offset..end_offset, highlight_id)); - } - offset = end_offset; - } - *resolved = true; -} - -impl EventEmitter for LspStore {} - -fn remove_empty_hover_blocks(mut hover: Hover) -> Option { - hover - .contents - .retain(|hover_block| !hover_block.text.trim().is_empty()); - if hover.contents.is_empty() { - None - } else { - Some(hover) - } -} - -async fn populate_labels_for_completions( - new_completions: Vec, - language: Option>, - lsp_adapter: Option>, -) -> Vec { - let lsp_completions = new_completions - .iter() - .filter_map(|new_completion| { - new_completion - .source - .lsp_completion(true) - .map(|lsp_completion| lsp_completion.into_owned()) - }) - .collect::>(); - - let mut labels = if let Some((language, lsp_adapter)) = language.as_ref().zip(lsp_adapter) { - lsp_adapter - .labels_for_completions(&lsp_completions, language) - .await - .log_err() - .unwrap_or_default() - } else { - Vec::new() - } - .into_iter() - .fuse(); - - let mut completions = Vec::new(); - for completion in new_completions { - match completion.source.lsp_completion(true) { - Some(lsp_completion) => { - let documentation = lsp_completion.documentation.clone().map(|docs| docs.into()); - - let mut label = labels.next().flatten().unwrap_or_else(|| { - CodeLabel::fallback_for_completion(&lsp_completion, language.as_deref()) - }); - ensure_uniform_list_compatible_label(&mut label); - completions.push(Completion { - label, - documentation, - replace_range: completion.replace_range, - new_text: completion.new_text, - insert_text_mode: lsp_completion.insert_text_mode, - source: completion.source, - icon_path: None, - confirm: None, - match_start: None, - snippet_deduplication_key: None, - }); - } - None => { - let mut label = CodeLabel::plain(completion.new_text.clone(), None); - ensure_uniform_list_compatible_label(&mut label); - completions.push(Completion { - label, - documentation: None, - replace_range: completion.replace_range, - new_text: completion.new_text, - source: completion.source, - insert_text_mode: None, - icon_path: None, - confirm: None, - match_start: None, - snippet_deduplication_key: None, - }); - } - } - } - completions -} - -#[derive(Debug)] -pub enum LanguageServerToQuery { - /// Query language servers in order of users preference, up until one capable of handling the request is found. - FirstCapable, - /// Query a specific language server. - Other(LanguageServerId), -} - -#[derive(Default)] -struct RenamePathsWatchedForServer { - did_rename: Vec, - will_rename: Vec, -} - -impl RenamePathsWatchedForServer { - fn with_did_rename_patterns( - mut self, - did_rename: Option<&FileOperationRegistrationOptions>, - ) -> Self { - if let Some(did_rename) = did_rename { - self.did_rename = did_rename - .filters - .iter() - .filter_map(|filter| filter.try_into().log_err()) - .collect(); - } - self - } - fn with_will_rename_patterns( - mut self, - will_rename: Option<&FileOperationRegistrationOptions>, - ) -> Self { - if let Some(will_rename) = will_rename { - self.will_rename = will_rename - .filters - .iter() - .filter_map(|filter| filter.try_into().log_err()) - .collect(); - } - self - } - - fn should_send_did_rename(&self, path: &str, is_dir: bool) -> bool { - self.did_rename.iter().any(|pred| pred.eval(path, is_dir)) - } - fn should_send_will_rename(&self, path: &str, is_dir: bool) -> bool { - self.will_rename.iter().any(|pred| pred.eval(path, is_dir)) - } -} - -impl TryFrom<&FileOperationFilter> for RenameActionPredicate { - type Error = globset::Error; - fn try_from(ops: &FileOperationFilter) -> Result { - Ok(Self { - kind: ops.pattern.matches.clone(), - glob: GlobBuilder::new(&ops.pattern.glob) - .case_insensitive( - ops.pattern - .options - .as_ref() - .is_some_and(|ops| ops.ignore_case.unwrap_or(false)), - ) - .build()? - .compile_matcher(), - }) - } -} -struct RenameActionPredicate { - glob: GlobMatcher, - kind: Option, -} - -impl RenameActionPredicate { - // Returns true if language server should be notified - fn eval(&self, path: &str, is_dir: bool) -> bool { - self.kind.as_ref().is_none_or(|kind| { - let expected_kind = if is_dir { - FileOperationPatternKind::Folder - } else { - FileOperationPatternKind::File - }; - kind == &expected_kind - }) && self.glob.is_match(path) - } -} - -#[derive(Default)] -struct LanguageServerWatchedPaths { - worktree_paths: HashMap, - abs_paths: HashMap, (GlobSet, Task<()>)>, -} - -#[derive(Default)] -struct LanguageServerWatchedPathsBuilder { - worktree_paths: HashMap, - abs_paths: HashMap, GlobSet>, -} - -impl LanguageServerWatchedPathsBuilder { - fn watch_worktree(&mut self, worktree_id: WorktreeId, glob_set: GlobSet) { - self.worktree_paths.insert(worktree_id, glob_set); - } - fn watch_abs_path(&mut self, path: Arc, glob_set: GlobSet) { - self.abs_paths.insert(path, glob_set); - } - fn build( - self, - fs: Arc, - language_server_id: LanguageServerId, - cx: &mut Context, - ) -> LanguageServerWatchedPaths { - let lsp_store = cx.weak_entity(); - - const LSP_ABS_PATH_OBSERVE: Duration = Duration::from_millis(100); - let abs_paths = self - .abs_paths - .into_iter() - .map(|(abs_path, globset)| { - let task = cx.spawn({ - let abs_path = abs_path.clone(); - let fs = fs.clone(); - - let lsp_store = lsp_store.clone(); - async move |_, cx| { - maybe!(async move { - let mut push_updates = fs.watch(&abs_path, LSP_ABS_PATH_OBSERVE).await; - while let Some(update) = push_updates.0.next().await { - let action = lsp_store - .update(cx, |this, _| { - let Some(local) = this.as_local() else { - return ControlFlow::Break(()); - }; - let Some(watcher) = local - .language_server_watched_paths - .get(&language_server_id) - else { - return ControlFlow::Break(()); - }; - let (globs, _) = watcher.abs_paths.get(&abs_path).expect( - "Watched abs path is not registered with a watcher", - ); - let matching_entries = update - .into_iter() - .filter(|event| globs.is_match(&event.path)) - .collect::>(); - this.lsp_notify_abs_paths_changed( - language_server_id, - matching_entries, - ); - ControlFlow::Continue(()) - }) - .ok()?; - - if action.is_break() { - break; - } - } - Some(()) - }) - .await; - } - }); - (abs_path, (globset, task)) - }) - .collect(); - LanguageServerWatchedPaths { - worktree_paths: self.worktree_paths, - abs_paths, - } - } -} - -struct LspBufferSnapshot { - version: i32, - snapshot: TextBufferSnapshot, -} - -/// A prompt requested by LSP server. -#[derive(Clone, Debug)] -pub struct LanguageServerPromptRequest { - pub level: PromptLevel, - pub message: String, - pub actions: Vec, - pub lsp_name: String, - pub(crate) response_channel: Sender, -} - -impl LanguageServerPromptRequest { - pub async fn respond(self, index: usize) -> Option<()> { - if let Some(response) = self.actions.into_iter().nth(index) { - self.response_channel.send(response).await.ok() - } else { - None - } - } -} -impl PartialEq for LanguageServerPromptRequest { - fn eq(&self, other: &Self) -> bool { - self.message == other.message && self.actions == other.actions - } -} - -#[derive(Clone, Debug, PartialEq)] -pub enum LanguageServerLogType { - Log(MessageType), - Trace { verbose_info: Option }, - Rpc { received: bool }, -} - -impl LanguageServerLogType { - pub fn to_proto(&self) -> proto::language_server_log::LogType { - match self { - Self::Log(log_type) => { - use proto::log_message::LogLevel; - let level = match *log_type { - MessageType::ERROR => LogLevel::Error, - MessageType::WARNING => LogLevel::Warning, - MessageType::INFO => LogLevel::Info, - MessageType::LOG => LogLevel::Log, - other => { - log::warn!("Unknown lsp log message type: {other:?}"); - LogLevel::Log - } - }; - proto::language_server_log::LogType::Log(proto::LogMessage { - level: level as i32, - }) - } - Self::Trace { verbose_info } => { - proto::language_server_log::LogType::Trace(proto::TraceMessage { - verbose_info: verbose_info.to_owned(), - }) - } - Self::Rpc { received } => { - let kind = if *received { - proto::rpc_message::Kind::Received - } else { - proto::rpc_message::Kind::Sent - }; - let kind = kind as i32; - proto::language_server_log::LogType::Rpc(proto::RpcMessage { kind }) - } - } - } - - pub fn from_proto(log_type: proto::language_server_log::LogType) -> Self { - use proto::log_message::LogLevel; - use proto::rpc_message; - match log_type { - proto::language_server_log::LogType::Log(message_type) => Self::Log( - match LogLevel::from_i32(message_type.level).unwrap_or(LogLevel::Log) { - LogLevel::Error => MessageType::ERROR, - LogLevel::Warning => MessageType::WARNING, - LogLevel::Info => MessageType::INFO, - LogLevel::Log => MessageType::LOG, - }, - ), - proto::language_server_log::LogType::Trace(trace_message) => Self::Trace { - verbose_info: trace_message.verbose_info, - }, - proto::language_server_log::LogType::Rpc(message) => Self::Rpc { - received: match rpc_message::Kind::from_i32(message.kind) - .unwrap_or(rpc_message::Kind::Received) - { - rpc_message::Kind::Received => true, - rpc_message::Kind::Sent => false, - }, - }, - } - } -} - -pub struct WorkspaceRefreshTask { - refresh_tx: mpsc::Sender<()>, - progress_tx: mpsc::Sender<()>, - #[allow(dead_code)] - task: Task<()>, -} - -pub enum LanguageServerState { - Starting { - startup: Task>>, - /// List of language servers that will be added to the workspace once it's initialization completes. - pending_workspace_folders: Arc>>, - }, - - Running { - adapter: Arc, - server: Arc, - simulate_disk_based_diagnostics_completion: Option>, - workspace_diagnostics_refresh_tasks: HashMap, WorkspaceRefreshTask>, - }, -} - -impl LanguageServerState { - fn add_workspace_folder(&self, uri: Uri) { - match self { - LanguageServerState::Starting { - pending_workspace_folders, - .. - } => { - pending_workspace_folders.lock().insert(uri); - } - LanguageServerState::Running { server, .. } => { - server.add_workspace_folder(uri); - } - } - } - fn _remove_workspace_folder(&self, uri: Uri) { - match self { - LanguageServerState::Starting { - pending_workspace_folders, - .. - } => { - pending_workspace_folders.lock().remove(&uri); - } - LanguageServerState::Running { server, .. } => server.remove_workspace_folder(uri), - } - } -} - -impl std::fmt::Debug for LanguageServerState { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - LanguageServerState::Starting { .. } => { - f.debug_struct("LanguageServerState::Starting").finish() - } - LanguageServerState::Running { .. } => { - f.debug_struct("LanguageServerState::Running").finish() - } - } - } -} - -#[derive(Clone, Debug, Serialize)] -pub struct LanguageServerProgress { - pub is_disk_based_diagnostics_progress: bool, - pub is_cancellable: bool, - pub title: Option, - pub message: Option, - pub percentage: Option, - #[serde(skip_serializing)] - pub last_update_at: Instant, -} - -#[derive(Copy, Clone, Debug, Default, PartialEq, Serialize)] -pub struct DiagnosticSummary { - pub error_count: usize, - pub warning_count: usize, -} - -impl DiagnosticSummary { - pub fn new<'a, T: 'a>(diagnostics: impl IntoIterator>) -> Self { - let mut this = Self { - error_count: 0, - warning_count: 0, - }; - - for entry in diagnostics { - if entry.diagnostic.is_primary { - match entry.diagnostic.severity { - DiagnosticSeverity::ERROR => this.error_count += 1, - DiagnosticSeverity::WARNING => this.warning_count += 1, - _ => {} - } - } - } - - this - } - - pub fn is_empty(&self) -> bool { - self.error_count == 0 && self.warning_count == 0 - } - - pub fn to_proto( - self, - language_server_id: LanguageServerId, - path: &RelPath, - ) -> proto::DiagnosticSummary { - proto::DiagnosticSummary { - path: path.to_proto(), - language_server_id: language_server_id.0 as u64, - error_count: self.error_count as u32, - warning_count: self.warning_count as u32, - } - } -} - -#[derive(Clone, Debug)] -pub enum CompletionDocumentation { - /// There is no documentation for this completion. - Undocumented, - /// A single line of documentation. - SingleLine(SharedString), - /// Multiple lines of plain text documentation. - MultiLinePlainText(SharedString), - /// Markdown documentation. - MultiLineMarkdown(SharedString), - /// Both single line and multiple lines of plain text documentation. - SingleLineAndMultiLinePlainText { - single_line: SharedString, - plain_text: Option, - }, -} - -impl CompletionDocumentation { - #[cfg(any(test, feature = "test-support"))] - pub fn text(&self) -> SharedString { - match self { - CompletionDocumentation::Undocumented => "".into(), - CompletionDocumentation::SingleLine(s) => s.clone(), - CompletionDocumentation::MultiLinePlainText(s) => s.clone(), - CompletionDocumentation::MultiLineMarkdown(s) => s.clone(), - CompletionDocumentation::SingleLineAndMultiLinePlainText { single_line, .. } => { - single_line.clone() - } - } - } -} - -impl From for CompletionDocumentation { - fn from(docs: lsp::Documentation) -> Self { - match docs { - lsp::Documentation::String(text) => { - if text.lines().count() <= 1 { - CompletionDocumentation::SingleLine(text.into()) - } else { - CompletionDocumentation::MultiLinePlainText(text.into()) - } - } - - lsp::Documentation::MarkupContent(lsp::MarkupContent { kind, value }) => match kind { - lsp::MarkupKind::PlainText => { - if value.lines().count() <= 1 { - CompletionDocumentation::SingleLine(value.into()) - } else { - CompletionDocumentation::MultiLinePlainText(value.into()) - } - } - - lsp::MarkupKind::Markdown => { - CompletionDocumentation::MultiLineMarkdown(value.into()) - } - }, - } - } -} - -pub enum ResolvedHint { - Resolved(InlayHint), - Resolving(Shared>), -} - -fn glob_literal_prefix(glob: &Path) -> PathBuf { - glob.components() - .take_while(|component| match component { - path::Component::Normal(part) => !part.to_string_lossy().contains(['*', '?', '{', '}']), - _ => true, - }) - .collect() -} - -pub struct SshLspAdapter { - name: LanguageServerName, - binary: LanguageServerBinary, - initialization_options: Option, - code_action_kinds: Option>, -} - -impl SshLspAdapter { - pub fn new( - name: LanguageServerName, - binary: LanguageServerBinary, - initialization_options: Option, - code_action_kinds: Option, - ) -> Self { - Self { - name, - binary, - initialization_options, - code_action_kinds: code_action_kinds - .as_ref() - .and_then(|c| serde_json::from_str(c).ok()), - } - } -} - -impl LspInstaller for SshLspAdapter { - type BinaryVersion = (); - async fn check_if_user_installed( - &self, - _: &dyn LspAdapterDelegate, - _: Option, - _: &AsyncApp, - ) -> Option { - Some(self.binary.clone()) - } - - async fn cached_server_binary( - &self, - _: PathBuf, - _: &dyn LspAdapterDelegate, - ) -> Option { - None - } - - async fn fetch_latest_server_version( - &self, - _: &dyn LspAdapterDelegate, - _: bool, - _: &mut AsyncApp, - ) -> Result<()> { - anyhow::bail!("SshLspAdapter does not support fetch_latest_server_version") - } - - async fn fetch_server_binary( - &self, - _: (), - _: PathBuf, - _: &dyn LspAdapterDelegate, - ) -> Result { - anyhow::bail!("SshLspAdapter does not support fetch_server_binary") - } -} - -#[async_trait(?Send)] -impl LspAdapter for SshLspAdapter { - fn name(&self) -> LanguageServerName { - self.name.clone() - } - - async fn initialization_options( - self: Arc, - _: &Arc, - ) -> Result> { - let Some(options) = &self.initialization_options else { - return Ok(None); - }; - let result = serde_json::from_str(options)?; - Ok(result) - } - - fn code_action_kinds(&self) -> Option> { - self.code_action_kinds.clone() - } -} - -pub fn language_server_settings<'a>( - delegate: &'a dyn LspAdapterDelegate, - language: &LanguageServerName, - cx: &'a App, -) -> Option<&'a LspSettings> { - language_server_settings_for( - SettingsLocation { - worktree_id: delegate.worktree_id(), - path: RelPath::empty(), - }, - language, - cx, - ) -} - -pub fn language_server_settings_for<'a>( - location: SettingsLocation<'a>, - language: &LanguageServerName, - cx: &'a App, -) -> Option<&'a LspSettings> { - ProjectSettings::get(Some(location), cx).lsp.get(language) -} - -pub struct LocalLspAdapterDelegate { - lsp_store: WeakEntity, - worktree: worktree::Snapshot, - fs: Arc, - http_client: Arc, - language_registry: Arc, - load_shell_env_task: Shared>>>, -} - -impl LocalLspAdapterDelegate { - pub fn new( - language_registry: Arc, - environment: &Entity, - lsp_store: WeakEntity, - worktree: &Entity, - http_client: Arc, - fs: Arc, - cx: &mut App, - ) -> Arc { - let load_shell_env_task = - environment.update(cx, |env, cx| env.worktree_environment(worktree.clone(), cx)); - - Arc::new(Self { - lsp_store, - worktree: worktree.read(cx).snapshot(), - fs, - http_client, - language_registry, - load_shell_env_task, - }) - } - - fn from_local_lsp( - local: &LocalLspStore, - worktree: &Entity, - cx: &mut App, - ) -> Arc { - Self::new( - local.languages.clone(), - &local.environment, - local.weak.clone(), - worktree, - local.http_client.clone(), - local.fs.clone(), - cx, - ) - } -} - -#[async_trait] -impl LspAdapterDelegate for LocalLspAdapterDelegate { - fn show_notification(&self, message: &str, cx: &mut App) { - self.lsp_store - .update(cx, |_, cx| { - cx.emit(LspStoreEvent::Notification(message.to_owned())) - }) - .ok(); - } - - fn http_client(&self) -> Arc { - self.http_client.clone() - } - - fn worktree_id(&self) -> WorktreeId { - self.worktree.id() - } - - fn worktree_root_path(&self) -> &Path { - self.worktree.abs_path().as_ref() - } - - fn resolve_executable_path(&self, path: PathBuf) -> PathBuf { - self.worktree.resolve_executable_path(path) - } - - async fn shell_env(&self) -> HashMap { - let task = self.load_shell_env_task.clone(); - task.await.unwrap_or_default() - } - - async fn npm_package_installed_version( - &self, - package_name: &str, - ) -> Result> { - let local_package_directory = self.worktree_root_path(); - let node_modules_directory = local_package_directory.join("node_modules"); - - if let Some(version) = - read_package_installed_version(node_modules_directory.clone(), package_name).await? - { - return Ok(Some((node_modules_directory, version))); - } - let Some(npm) = self.which("npm".as_ref()).await else { - log::warn!( - "Failed to find npm executable for {:?}", - local_package_directory - ); - return Ok(None); - }; - - let env = self.shell_env().await; - let output = util::command::new_smol_command(&npm) - .args(["root", "-g"]) - .envs(env) - .current_dir(local_package_directory) - .output() - .await?; - let global_node_modules = - PathBuf::from(String::from_utf8_lossy(&output.stdout).to_string()); - - if let Some(version) = - read_package_installed_version(global_node_modules.clone(), package_name).await? - { - return Ok(Some((global_node_modules, version))); - } - return Ok(None); - } - - async fn which(&self, command: &OsStr) -> Option { - let mut worktree_abs_path = self.worktree_root_path().to_path_buf(); - if self.fs.is_file(&worktree_abs_path).await { - worktree_abs_path.pop(); - } - - let env = self.shell_env().await; - - let shell_path = env.get("PATH").cloned(); - - which::which_in(command, shell_path.as_ref(), worktree_abs_path).ok() - } - - async fn try_exec(&self, command: LanguageServerBinary) -> Result<()> { - let mut working_dir = self.worktree_root_path().to_path_buf(); - if self.fs.is_file(&working_dir).await { - working_dir.pop(); - } - let output = util::command::new_smol_command(&command.path) - .args(command.arguments) - .envs(command.env.clone().unwrap_or_default()) - .current_dir(working_dir) - .output() - .await?; - - anyhow::ensure!( - output.status.success(), - "{}, stdout: {:?}, stderr: {:?}", - output.status, - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - Ok(()) - } - - fn update_status(&self, server_name: LanguageServerName, status: language::BinaryStatus) { - self.language_registry - .update_lsp_binary_status(server_name, status); - } - - fn registered_lsp_adapters(&self) -> Vec> { - self.language_registry - .all_lsp_adapters() - .into_iter() - .map(|adapter| adapter.adapter.clone() as Arc) - .collect() - } - - async fn language_server_download_dir(&self, name: &LanguageServerName) -> Option> { - let dir = self.language_registry.language_server_download_dir(name)?; - - if !dir.exists() { - smol::fs::create_dir_all(&dir) - .await - .context("failed to create container directory") - .log_err()?; - } - - Some(dir) - } - - async fn read_text_file(&self, path: &RelPath) -> Result { - let entry = self - .worktree - .entry_for_path(path) - .with_context(|| format!("no worktree entry for path {path:?}"))?; - let abs_path = self.worktree.absolutize(&entry.path); - self.fs.load(&abs_path).await - } -} - -async fn populate_labels_for_symbols( - symbols: Vec, - language_registry: &Arc, - lsp_adapter: Option>, - output: &mut Vec, -) { - #[allow(clippy::mutable_key_type)] - let mut symbols_by_language = HashMap::>, Vec>::default(); - - let mut unknown_paths = BTreeSet::>::new(); - for symbol in symbols { - let Some(file_name) = symbol.path.file_name() else { - continue; - }; - let language = language_registry - .load_language_for_file_path(Path::new(file_name)) - .await - .ok() - .or_else(|| { - unknown_paths.insert(file_name.into()); - None - }); - symbols_by_language - .entry(language) - .or_default() - .push(symbol); - } - - for unknown_path in unknown_paths { - log::info!("no language found for symbol in file {unknown_path:?}"); - } - - let mut label_params = Vec::new(); - for (language, mut symbols) in symbols_by_language { - label_params.clear(); - label_params.extend( - symbols - .iter_mut() - .map(|symbol| (mem::take(&mut symbol.name), symbol.kind)), - ); - - let mut labels = Vec::new(); - if let Some(language) = language { - let lsp_adapter = lsp_adapter.clone().or_else(|| { - language_registry - .lsp_adapters(&language.name()) - .first() - .cloned() - }); - if let Some(lsp_adapter) = lsp_adapter { - labels = lsp_adapter - .labels_for_symbols(&label_params, &language) - .await - .log_err() - .unwrap_or_default(); - } - } - - for ((symbol, (name, _)), label) in symbols - .into_iter() - .zip(label_params.drain(..)) - .zip(labels.into_iter().chain(iter::repeat(None))) - { - output.push(Symbol { - language_server_name: symbol.language_server_name, - source_worktree_id: symbol.source_worktree_id, - source_language_server_id: symbol.source_language_server_id, - path: symbol.path, - label: label.unwrap_or_else(|| CodeLabel::plain(name.clone(), None)), - name, - kind: symbol.kind, - range: symbol.range, - }); - } - } -} - -fn include_text(server: &lsp::LanguageServer) -> Option { - match server.capabilities().text_document_sync.as_ref()? { - lsp::TextDocumentSyncCapability::Options(opts) => match opts.save.as_ref()? { - // Server wants didSave but didn't specify includeText. - lsp::TextDocumentSyncSaveOptions::Supported(true) => Some(false), - // Server doesn't want didSave at all. - lsp::TextDocumentSyncSaveOptions::Supported(false) => None, - // Server provided SaveOptions. - lsp::TextDocumentSyncSaveOptions::SaveOptions(save_options) => { - Some(save_options.include_text.unwrap_or(false)) - } - }, - // We do not have any save info. Kind affects didChange only. - lsp::TextDocumentSyncCapability::Kind(_) => None, - } -} - -/// Completion items are displayed in a `UniformList`. -/// Usually, those items are single-line strings, but in LSP responses, -/// completion items `label`, `detail` and `label_details.description` may contain newlines or long spaces. -/// Many language plugins construct these items by joining these parts together, and we may use `CodeLabel::fallback_for_completion` that uses `label` at least. -/// All that may lead to a newline being inserted into resulting `CodeLabel.text`, which will force `UniformList` to bloat each entry to occupy more space, -/// breaking the completions menu presentation. -/// -/// Sanitize the text to ensure there are no newlines, or, if there are some, remove them and also remove long space sequences if there were newlines. -fn ensure_uniform_list_compatible_label(label: &mut CodeLabel) { - let mut new_text = String::with_capacity(label.text.len()); - let mut offset_map = vec![0; label.text.len() + 1]; - let mut last_char_was_space = false; - let mut new_idx = 0; - let chars = label.text.char_indices().fuse(); - let mut newlines_removed = false; - - for (idx, c) in chars { - offset_map[idx] = new_idx; - - match c { - '\n' if last_char_was_space => { - newlines_removed = true; - } - '\t' | ' ' if last_char_was_space => {} - '\n' if !last_char_was_space => { - new_text.push(' '); - new_idx += 1; - last_char_was_space = true; - newlines_removed = true; - } - ' ' | '\t' => { - new_text.push(' '); - new_idx += 1; - last_char_was_space = true; - } - _ => { - new_text.push(c); - new_idx += c.len_utf8(); - last_char_was_space = false; - } - } - } - offset_map[label.text.len()] = new_idx; - - // Only modify the label if newlines were removed. - if !newlines_removed { - return; - } - - let last_index = new_idx; - let mut run_ranges_errors = Vec::new(); - label.runs.retain_mut(|(range, _)| { - match offset_map.get(range.start) { - Some(&start) => range.start = start, - None => { - run_ranges_errors.push(range.clone()); - return false; - } - } - - match offset_map.get(range.end) { - Some(&end) => range.end = end, - None => { - run_ranges_errors.push(range.clone()); - range.end = last_index; - } - } - true - }); - if !run_ranges_errors.is_empty() { - log::error!( - "Completion label has errors in its run ranges: {run_ranges_errors:?}, label text: {}", - label.text - ); - } - - let mut wrong_filter_range = None; - if label.filter_range == (0..label.text.len()) { - label.filter_range = 0..new_text.len(); - } else { - let mut original_filter_range = Some(label.filter_range.clone()); - match offset_map.get(label.filter_range.start) { - Some(&start) => label.filter_range.start = start, - None => { - wrong_filter_range = original_filter_range.take(); - label.filter_range.start = last_index; - } - } - - match offset_map.get(label.filter_range.end) { - Some(&end) => label.filter_range.end = end, - None => { - wrong_filter_range = original_filter_range.take(); - label.filter_range.end = last_index; - } - } - } - if let Some(wrong_filter_range) = wrong_filter_range { - log::error!( - "Completion label has an invalid filter range: {wrong_filter_range:?}, label text: {}", - label.text - ); - } - - label.text = new_text; -} - -#[cfg(test)] -mod tests { - use language::HighlightId; - - use super::*; - - #[test] - fn test_glob_literal_prefix() { - assert_eq!(glob_literal_prefix(Path::new("**/*.js")), Path::new("")); - assert_eq!( - glob_literal_prefix(Path::new("node_modules/**/*.js")), - Path::new("node_modules") - ); - assert_eq!( - glob_literal_prefix(Path::new("foo/{bar,baz}.js")), - Path::new("foo") - ); - assert_eq!( - glob_literal_prefix(Path::new("foo/bar/baz.js")), - Path::new("foo/bar/baz.js") - ); - - #[cfg(target_os = "windows")] - { - assert_eq!(glob_literal_prefix(Path::new("**\\*.js")), Path::new("")); - assert_eq!( - glob_literal_prefix(Path::new("node_modules\\**/*.js")), - Path::new("node_modules") - ); - assert_eq!( - glob_literal_prefix(Path::new("foo/{bar,baz}.js")), - Path::new("foo") - ); - assert_eq!( - glob_literal_prefix(Path::new("foo\\bar\\baz.js")), - Path::new("foo/bar/baz.js") - ); - } - } - - #[test] - fn test_multi_len_chars_normalization() { - let mut label = CodeLabel::new( - "myElˇ (parameter) myElˇ: {\n foo: string;\n}".to_string(), - 0..6, - vec![(0..6, HighlightId(1))], - ); - ensure_uniform_list_compatible_label(&mut label); - assert_eq!( - label, - CodeLabel::new( - "myElˇ (parameter) myElˇ: { foo: string; }".to_string(), - 0..6, - vec![(0..6, HighlightId(1))], - ) - ); - } -} diff --git a/crates/project/src/lsp_store/clangd_ext.rs b/crates/project/src/lsp_store/clangd_ext.rs deleted file mode 100644 index 466d0c6e2a..0000000000 --- a/crates/project/src/lsp_store/clangd_ext.rs +++ /dev/null @@ -1,104 +0,0 @@ -use std::{borrow::Cow, sync::Arc}; - -use ::serde::{Deserialize, Serialize}; -use gpui::WeakEntity; -use language::{CachedLspAdapter, Diagnostic, DiagnosticSourceKind}; -use lsp::{LanguageServer, LanguageServerName}; -use util::ResultExt as _; - -use crate::{LspStore, lsp_store::DocumentDiagnosticsUpdate}; - -pub const CLANGD_SERVER_NAME: LanguageServerName = LanguageServerName::new_static("clangd"); -const INACTIVE_REGION_MESSAGE: &str = "inactive region"; -const INACTIVE_DIAGNOSTIC_SEVERITY: lsp::DiagnosticSeverity = lsp::DiagnosticSeverity::INFORMATION; - -#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct InactiveRegionsParams { - pub text_document: lsp::OptionalVersionedTextDocumentIdentifier, - pub regions: Vec, -} - -/// InactiveRegions is a clangd extension that marks regions of inactive code. -pub struct InactiveRegions; - -impl lsp::notification::Notification for InactiveRegions { - type Params = InactiveRegionsParams; - const METHOD: &'static str = "textDocument/inactiveRegions"; -} - -pub fn is_inactive_region(diag: &Diagnostic) -> bool { - diag.is_unnecessary - && diag.severity == INACTIVE_DIAGNOSTIC_SEVERITY - && diag.message == INACTIVE_REGION_MESSAGE - && diag - .source - .as_ref() - .is_some_and(|v| v == &CLANGD_SERVER_NAME.0) -} - -pub fn is_lsp_inactive_region(diag: &lsp::Diagnostic) -> bool { - diag.severity == Some(INACTIVE_DIAGNOSTIC_SEVERITY) - && diag.message == INACTIVE_REGION_MESSAGE - && diag - .source - .as_ref() - .is_some_and(|v| v == &CLANGD_SERVER_NAME.0) -} - -pub fn register_notifications( - lsp_store: WeakEntity, - language_server: &LanguageServer, - adapter: Arc, -) { - if language_server.name() != CLANGD_SERVER_NAME { - return; - } - let server_id = language_server.server_id(); - - language_server - .on_notification::({ - let adapter = adapter; - let this = lsp_store; - - move |params: InactiveRegionsParams, cx| { - let adapter = adapter.clone(); - this.update(cx, |this, cx| { - let diagnostics = params - .regions - .into_iter() - .map(|range| lsp::Diagnostic { - range, - severity: Some(INACTIVE_DIAGNOSTIC_SEVERITY), - source: Some(CLANGD_SERVER_NAME.to_string()), - message: INACTIVE_REGION_MESSAGE.to_string(), - tags: Some(vec![lsp::DiagnosticTag::UNNECESSARY]), - ..lsp::Diagnostic::default() - }) - .collect(); - let mapped_diagnostics = lsp::PublishDiagnosticsParams { - uri: params.text_document.uri, - version: params.text_document.version, - diagnostics, - }; - this.merge_lsp_diagnostics( - DiagnosticSourceKind::Pushed, - vec![DocumentDiagnosticsUpdate { - server_id, - diagnostics: mapped_diagnostics, - result_id: None, - disk_based_sources: Cow::Borrowed( - &adapter.disk_based_diagnostic_sources, - ), - registration_id: None, - }], - |_, diag, _| !is_inactive_region(diag), - cx, - ) - .log_err(); - }) - .ok(); - } - }) - .detach(); -} diff --git a/crates/project/src/lsp_store/inlay_hint_cache.rs b/crates/project/src/lsp_store/inlay_hint_cache.rs deleted file mode 100644 index 804552b52c..0000000000 --- a/crates/project/src/lsp_store/inlay_hint_cache.rs +++ /dev/null @@ -1,233 +0,0 @@ -use std::{collections::hash_map, ops::Range, sync::Arc}; - -use collections::HashMap; -use futures::future::Shared; -use gpui::{App, Entity, Task}; -use language::{ - Buffer, - row_chunk::{RowChunk, RowChunks}, -}; -use lsp::LanguageServerId; -use text::Anchor; - -use crate::{InlayHint, InlayId}; - -pub type CacheInlayHints = HashMap>; -pub type CacheInlayHintsTask = Shared>>>; - -/// A logic to apply when querying for new inlay hints and deciding what to do with the old entries in the cache in case of conflicts. -#[derive(Debug, Clone, Copy)] -pub enum InvalidationStrategy { - /// Language servers reset hints via request. - /// Demands to re-query all inlay hints needed and invalidate all cached entries, but does not require instant update with invalidation. - /// - /// Despite nothing forbids language server from sending this request on every edit, it is expected to be sent only when certain internal server state update, invisible for the editor otherwise. - RefreshRequested { - server_id: LanguageServerId, - request_id: Option, - }, - /// Multibuffer excerpt(s) and/or singleton buffer(s) were edited at least on one place. - /// Neither editor nor LSP is able to tell which open file hints' are not affected, so all of them have to be invalidated, re-queried and do that fast enough to avoid being slow, but also debounce to avoid loading hints on every fast keystroke sequence. - BufferEdited, - /// A new file got opened/new excerpt was added to a multibuffer/a [multi]buffer was scrolled to a new position. - /// No invalidation should be done at all, all new hints are added to the cache. - /// - /// A special case is the editor toggles and settings change: - /// in addition to LSP capabilities, Zed allows omitting certain hint kinds (defined by the corresponding LSP part: type/parameter/other) and toggling hints. - /// This does not lead to cache invalidation, but would require cache usage for determining which hints are not displayed and issuing an update to inlays on the screen. - None, -} - -impl InvalidationStrategy { - pub fn should_invalidate(&self) -> bool { - matches!( - self, - InvalidationStrategy::RefreshRequested { .. } | InvalidationStrategy::BufferEdited - ) - } -} - -pub struct BufferInlayHints { - chunks: RowChunks, - hints_by_chunks: Vec>, - fetches_by_chunks: Vec>, - hints_by_id: HashMap, - latest_invalidation_requests: HashMap>, - pub(super) hint_resolves: HashMap>>, -} - -#[derive(Debug, Clone, Copy)] -struct HintForId { - chunk_id: usize, - server_id: LanguageServerId, - position: usize, -} - -impl std::fmt::Debug for BufferInlayHints { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("BufferInlayHints") - .field("buffer_chunks", &self.chunks) - .field("hints_by_chunks", &self.hints_by_chunks) - .field("fetches_by_chunks", &self.fetches_by_chunks) - .field("hints_by_id", &self.hints_by_id) - .finish_non_exhaustive() - } -} - -const MAX_ROWS_IN_A_CHUNK: u32 = 50; - -impl BufferInlayHints { - pub fn new(buffer: &Entity, cx: &mut App) -> Self { - let chunks = RowChunks::new(buffer.read(cx).text_snapshot(), MAX_ROWS_IN_A_CHUNK); - - Self { - hints_by_chunks: vec![None; chunks.len()], - fetches_by_chunks: vec![None; chunks.len()], - latest_invalidation_requests: HashMap::default(), - hints_by_id: HashMap::default(), - hint_resolves: HashMap::default(), - chunks, - } - } - - pub fn applicable_chunks( - &self, - ranges: &[Range], - ) -> impl Iterator { - self.chunks.applicable_chunks(ranges) - } - - pub fn cached_hints(&mut self, chunk: &RowChunk) -> Option<&CacheInlayHints> { - self.hints_by_chunks[chunk.id].as_ref() - } - - pub fn fetched_hints(&mut self, chunk: &RowChunk) -> &mut Option { - &mut self.fetches_by_chunks[chunk.id] - } - - #[cfg(any(test, feature = "test-support"))] - pub fn all_cached_hints(&self) -> Vec { - self.hints_by_chunks - .iter() - .filter_map(|hints| hints.as_ref()) - .flat_map(|hints| hints.values().cloned()) - .flatten() - .map(|(_, hint)| hint) - .collect() - } - - #[cfg(any(test, feature = "test-support"))] - pub fn all_fetched_hints(&self) -> Vec { - self.fetches_by_chunks - .iter() - .filter_map(|fetches| fetches.clone()) - .collect() - } - - pub fn remove_server_data(&mut self, for_server: LanguageServerId) { - for (chunk_index, hints) in self.hints_by_chunks.iter_mut().enumerate() { - if let Some(hints) = hints { - if hints.remove(&for_server).is_some() { - self.fetches_by_chunks[chunk_index] = None; - } - } - } - } - - pub fn clear(&mut self) { - self.hints_by_chunks = vec![None; self.chunks.len()]; - self.fetches_by_chunks = vec![None; self.chunks.len()]; - self.hints_by_id.clear(); - self.hint_resolves.clear(); - self.latest_invalidation_requests.clear(); - } - - pub fn insert_new_hints( - &mut self, - chunk: RowChunk, - server_id: LanguageServerId, - new_hints: Vec<(InlayId, InlayHint)>, - ) { - let existing_hints = self.hints_by_chunks[chunk.id] - .get_or_insert_default() - .entry(server_id) - .or_insert_with(Vec::new); - let existing_count = existing_hints.len(); - existing_hints.extend(new_hints.into_iter().enumerate().filter_map( - |(i, (id, new_hint))| { - let new_hint_for_id = HintForId { - chunk_id: chunk.id, - server_id, - position: existing_count + i, - }; - if let hash_map::Entry::Vacant(vacant_entry) = self.hints_by_id.entry(id) { - vacant_entry.insert(new_hint_for_id); - Some((id, new_hint)) - } else { - None - } - }, - )); - *self.fetched_hints(&chunk) = None; - } - - pub fn hint_for_id(&mut self, id: InlayId) -> Option<&mut InlayHint> { - let hint_for_id = self.hints_by_id.get(&id)?; - let (hint_id, hint) = self - .hints_by_chunks - .get_mut(hint_for_id.chunk_id)? - .as_mut()? - .get_mut(&hint_for_id.server_id)? - .get_mut(hint_for_id.position)?; - debug_assert_eq!(*hint_id, id, "Invalid pointer {hint_for_id:?}"); - Some(hint) - } - - pub(crate) fn invalidate_for_server_refresh( - &mut self, - for_server: LanguageServerId, - request_id: Option, - ) -> bool { - match self.latest_invalidation_requests.entry(for_server) { - hash_map::Entry::Occupied(mut o) => { - if request_id > *o.get() { - o.insert(request_id); - } else { - return false; - } - } - hash_map::Entry::Vacant(v) => { - v.insert(request_id); - } - } - - for (chunk_id, chunk_data) in self.hints_by_chunks.iter_mut().enumerate() { - if let Some(removed_hints) = chunk_data - .as_mut() - .and_then(|chunk_data| chunk_data.remove(&for_server)) - { - for (id, _) in removed_hints { - self.hints_by_id.remove(&id); - self.hint_resolves.remove(&id); - } - self.fetches_by_chunks[chunk_id] = None; - } - } - - true - } - - pub(crate) fn invalidate_for_chunk(&mut self, chunk: RowChunk) { - self.fetches_by_chunks[chunk.id] = None; - if let Some(hints_by_server) = self.hints_by_chunks[chunk.id].take() { - for (hint_id, _) in hints_by_server.into_values().flatten() { - self.hints_by_id.remove(&hint_id); - self.hint_resolves.remove(&hint_id); - } - } - } - - pub fn chunk_range(&self, chunk: RowChunk) -> Option> { - self.chunks.chunk_range(chunk) - } -} diff --git a/crates/project/src/lsp_store/json_language_server_ext.rs b/crates/project/src/lsp_store/json_language_server_ext.rs deleted file mode 100644 index 78df713273..0000000000 --- a/crates/project/src/lsp_store/json_language_server_ext.rs +++ /dev/null @@ -1,98 +0,0 @@ -use anyhow::{Context, Result}; -use gpui::{App, AsyncApp, Entity, Global, WeakEntity}; -use lsp::LanguageServer; - -use crate::LspStore; - -const LOGGER: zlog::Logger = zlog::scoped!("json-schema"); - -/// https://github.com/Microsoft/vscode/blob/main/extensions/json-language-features/server/README.md#schema-content-request -/// -/// Represents a "JSON language server-specific, non-standardized, extension to the LSP" with which the vscode-json-language-server -/// can request the contents of a schema that is associated with a uri scheme it does not support. -/// In our case, we provide the uris for actions on server startup under the `zed://schemas/action/{normalize_action_name}` scheme. -/// We can then respond to this request with the schema content on demand, thereby greatly reducing the total size of the JSON we send to the server on startup -struct SchemaContentRequest {} - -impl lsp::request::Request for SchemaContentRequest { - type Params = Vec; - - type Result = String; - - const METHOD: &'static str = "vscode/content"; -} - -type SchemaRequestHandler = fn(Entity, String, &mut AsyncApp) -> Result; -pub struct SchemaHandlingImpl(SchemaRequestHandler); - -impl Global for SchemaHandlingImpl {} - -pub fn register_schema_handler(handler: SchemaRequestHandler, cx: &mut App) { - debug_assert!( - !cx.has_global::(), - "SchemaHandlingImpl already registered" - ); - cx.set_global(SchemaHandlingImpl(handler)); -} - -struct SchemaContentsChanged {} - -impl lsp::notification::Notification for SchemaContentsChanged { - const METHOD: &'static str = "json/schemaContent"; - type Params = String; -} - -pub fn notify_schema_changed(lsp_store: Entity, uri: String, cx: &App) { - zlog::trace!(LOGGER => "Notifying schema changed for URI: {:?}", uri); - let servers = lsp_store.read_with(cx, |lsp_store, _| { - let mut servers = Vec::new(); - let Some(local) = lsp_store.as_local() else { - return servers; - }; - - for states in local.language_servers.values() { - let json_server = match states { - super::LanguageServerState::Running { - adapter, server, .. - } if adapter.adapter.is_primary_zed_json_schema_adapter() => server.clone(), - _ => continue, - }; - - servers.push(json_server); - } - servers - }); - for server in servers { - zlog::trace!(LOGGER => "Notifying server {:?} of schema change for URI: {:?}", server.server_id(), &uri); - // TODO: handle errors - server.notify::(uri.clone()).ok(); - } -} - -pub fn register_requests(lsp_store: WeakEntity, language_server: &LanguageServer) { - language_server - .on_request::(move |params, cx| { - let handler = cx.try_read_global::(|handler, _| { - handler.0 - }); - let mut cx = cx.clone(); - let uri = params.clone().pop(); - let lsp_store = lsp_store.clone(); - let resolution = async move { - let lsp_store = lsp_store.upgrade().context("LSP store has been dropped")?; - let uri = uri.context("No URI")?; - let handle_schema_request = handler.context("No schema handler registered")?; - handle_schema_request(lsp_store, uri, &mut cx) - }; - async move { - zlog::trace!(LOGGER => "Handling schema request for {:?}", ¶ms); - let result = resolution.await; - match &result { - Ok(content) => {zlog::trace!(LOGGER => "Schema request resolved with {}B schema", content.len());}, - Err(err) => {zlog::warn!(LOGGER => "Schema request failed: {}", err);}, - } - result - } - }) - .detach(); -} diff --git a/crates/project/src/lsp_store/log_store.rs b/crates/project/src/lsp_store/log_store.rs deleted file mode 100644 index 92f8fecadd..0000000000 --- a/crates/project/src/lsp_store/log_store.rs +++ /dev/null @@ -1,716 +0,0 @@ -use std::{collections::VecDeque, sync::Arc}; - -use collections::HashMap; -use futures::{StreamExt, channel::mpsc}; -use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Global, Subscription, WeakEntity}; -use lsp::{ - IoKind, LanguageServer, LanguageServerId, LanguageServerName, LanguageServerSelector, - MessageType, TraceValue, -}; -use rpc::proto; -use settings::WorktreeId; - -use crate::{LanguageServerLogType, LspStore, Project, ProjectItem as _}; - -const SEND_LINE: &str = "\n// Send:"; -const RECEIVE_LINE: &str = "\n// Receive:"; -const MAX_STORED_LOG_ENTRIES: usize = 2000; - -pub fn init(on_headless_host: bool, cx: &mut App) -> Entity { - let log_store = cx.new(|cx| LogStore::new(on_headless_host, cx)); - cx.set_global(GlobalLogStore(log_store.clone())); - log_store -} - -pub struct GlobalLogStore(pub Entity); - -impl Global for GlobalLogStore {} - -#[derive(Debug)] -pub enum Event { - NewServerLogEntry { - id: LanguageServerId, - kind: LanguageServerLogType, - text: String, - }, -} - -impl EventEmitter for LogStore {} - -pub struct LogStore { - on_headless_host: bool, - projects: HashMap, ProjectState>, - pub copilot_log_subscription: Option, - pub language_servers: HashMap, - io_tx: mpsc::UnboundedSender<(LanguageServerId, IoKind, String)>, -} - -struct ProjectState { - _subscriptions: [Subscription; 2], -} - -pub trait Message: AsRef { - type Level: Copy + std::fmt::Debug; - fn should_include(&self, _: Self::Level) -> bool { - true - } -} - -#[derive(Debug)] -pub struct LogMessage { - message: String, - typ: MessageType, -} - -impl AsRef for LogMessage { - fn as_ref(&self) -> &str { - &self.message - } -} - -impl Message for LogMessage { - type Level = MessageType; - - fn should_include(&self, level: Self::Level) -> bool { - match (self.typ, level) { - (MessageType::ERROR, _) => true, - (_, MessageType::ERROR) => false, - (MessageType::WARNING, _) => true, - (_, MessageType::WARNING) => false, - (MessageType::INFO, _) => true, - (_, MessageType::INFO) => false, - _ => true, - } - } -} - -#[derive(Debug)] -pub struct TraceMessage { - message: String, - is_verbose: bool, -} - -impl AsRef for TraceMessage { - fn as_ref(&self) -> &str { - &self.message - } -} - -impl Message for TraceMessage { - type Level = TraceValue; - - fn should_include(&self, level: Self::Level) -> bool { - match level { - TraceValue::Off => false, - TraceValue::Messages => !self.is_verbose, - TraceValue::Verbose => true, - } - } -} - -#[derive(Debug)] -pub struct RpcMessage { - message: String, -} - -impl AsRef for RpcMessage { - fn as_ref(&self) -> &str { - &self.message - } -} - -impl Message for RpcMessage { - type Level = (); -} - -pub struct LanguageServerState { - pub name: Option, - pub worktree_id: Option, - pub kind: LanguageServerKind, - log_messages: VecDeque, - trace_messages: VecDeque, - pub rpc_state: Option, - pub trace_level: TraceValue, - pub log_level: MessageType, - io_logs_subscription: Option, - pub toggled_log_kind: Option, -} - -impl std::fmt::Debug for LanguageServerState { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("LanguageServerState") - .field("name", &self.name) - .field("worktree_id", &self.worktree_id) - .field("kind", &self.kind) - .field("log_messages", &self.log_messages) - .field("trace_messages", &self.trace_messages) - .field("rpc_state", &self.rpc_state) - .field("trace_level", &self.trace_level) - .field("log_level", &self.log_level) - .field("toggled_log_kind", &self.toggled_log_kind) - .finish_non_exhaustive() - } -} - -#[derive(PartialEq, Clone)] -pub enum LanguageServerKind { - Local { project: WeakEntity }, - Remote { project: WeakEntity }, - LocalSsh { lsp_store: WeakEntity }, - Global, -} - -impl std::fmt::Debug for LanguageServerKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - LanguageServerKind::Local { .. } => write!(f, "LanguageServerKind::Local"), - LanguageServerKind::Remote { .. } => write!(f, "LanguageServerKind::Remote"), - LanguageServerKind::LocalSsh { .. } => write!(f, "LanguageServerKind::LocalSsh"), - LanguageServerKind::Global => write!(f, "LanguageServerKind::Global"), - } - } -} - -impl LanguageServerKind { - pub fn project(&self) -> Option<&WeakEntity> { - match self { - Self::Local { project } => Some(project), - Self::Remote { project } => Some(project), - Self::LocalSsh { .. } => None, - Self::Global { .. } => None, - } - } -} - -#[derive(Debug)] -pub struct LanguageServerRpcState { - pub rpc_messages: VecDeque, - last_message_kind: Option, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq)] -enum MessageKind { - Send, - Receive, -} - -#[derive(Clone, Copy, Debug, Default, PartialEq)] -pub enum LogKind { - Rpc, - Trace, - #[default] - Logs, - ServerInfo, -} - -impl LogKind { - pub fn from_server_log_type(log_type: &LanguageServerLogType) -> Self { - match log_type { - LanguageServerLogType::Log(_) => Self::Logs, - LanguageServerLogType::Trace { .. } => Self::Trace, - LanguageServerLogType::Rpc { .. } => Self::Rpc, - } - } -} - -impl LogStore { - pub fn new(on_headless_host: bool, cx: &mut Context) -> Self { - let (io_tx, mut io_rx) = mpsc::unbounded(); - - let log_store = Self { - projects: HashMap::default(), - language_servers: HashMap::default(), - copilot_log_subscription: None, - on_headless_host, - io_tx, - }; - cx.spawn(async move |log_store, cx| { - while let Some((server_id, io_kind, message)) = io_rx.next().await { - if let Some(log_store) = log_store.upgrade() { - log_store.update(cx, |log_store, cx| { - log_store.on_io(server_id, io_kind, &message, cx); - })?; - } - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - - log_store - } - - pub fn add_project(&mut self, project: &Entity, cx: &mut Context) { - let weak_project = project.downgrade(); - self.projects.insert( - project.downgrade(), - ProjectState { - _subscriptions: [ - cx.observe_release(project, move |this, _, _| { - this.projects.remove(&weak_project); - this.language_servers - .retain(|_, state| state.kind.project() != Some(&weak_project)); - }), - cx.subscribe(project, move |log_store, project, event, cx| { - let server_kind = if project.read(cx).is_local() { - LanguageServerKind::Local { - project: project.downgrade(), - } - } else { - LanguageServerKind::Remote { - project: project.downgrade(), - } - }; - match event { - crate::Event::LanguageServerAdded(id, name, worktree_id) => { - log_store.add_language_server( - server_kind, - *id, - Some(name.clone()), - *worktree_id, - project - .read(cx) - .lsp_store() - .read(cx) - .language_server_for_id(*id), - cx, - ); - } - crate::Event::LanguageServerBufferRegistered { - server_id, - buffer_id, - name, - .. - } => { - let worktree_id = project - .read(cx) - .buffer_for_id(*buffer_id, cx) - .and_then(|buffer| { - Some(buffer.read(cx).project_path(cx)?.worktree_id) - }); - let name = name.clone().or_else(|| { - project - .read(cx) - .lsp_store() - .read(cx) - .language_server_statuses - .get(server_id) - .map(|status| status.name.clone()) - }); - log_store.add_language_server( - server_kind, - *server_id, - name, - worktree_id, - None, - cx, - ); - } - crate::Event::LanguageServerRemoved(id) => { - log_store.remove_language_server(*id, cx); - } - crate::Event::LanguageServerLog(id, typ, message) => { - log_store.add_language_server( - server_kind, - *id, - None, - None, - None, - cx, - ); - match typ { - crate::LanguageServerLogType::Log(typ) => { - log_store.add_language_server_log(*id, *typ, message, cx); - } - crate::LanguageServerLogType::Trace { verbose_info } => { - log_store.add_language_server_trace( - *id, - message, - verbose_info.clone(), - cx, - ); - } - crate::LanguageServerLogType::Rpc { received } => { - let kind = if *received { - MessageKind::Receive - } else { - MessageKind::Send - }; - log_store.add_language_server_rpc(*id, kind, message, cx); - } - } - } - crate::Event::ToggleLspLogs { - server_id, - enabled, - toggled_log_kind, - } => { - log_store.toggle_lsp_logs(*server_id, *enabled, *toggled_log_kind); - } - _ => {} - } - }), - ], - }, - ); - } - - pub fn get_language_server_state( - &mut self, - id: LanguageServerId, - ) -> Option<&mut LanguageServerState> { - self.language_servers.get_mut(&id) - } - - pub fn add_language_server( - &mut self, - kind: LanguageServerKind, - server_id: LanguageServerId, - name: Option, - worktree_id: Option, - server: Option>, - cx: &mut Context, - ) -> Option<&mut LanguageServerState> { - let server_state = self.language_servers.entry(server_id).or_insert_with(|| { - cx.notify(); - LanguageServerState { - name: None, - worktree_id: None, - kind, - rpc_state: None, - log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), - trace_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), - trace_level: TraceValue::Off, - log_level: MessageType::LOG, - io_logs_subscription: None, - toggled_log_kind: None, - } - }); - - if let Some(name) = name { - server_state.name = Some(name); - } - if let Some(worktree_id) = worktree_id { - server_state.worktree_id = Some(worktree_id); - } - - if let Some(server) = server.filter(|_| server_state.io_logs_subscription.is_none()) { - let io_tx = self.io_tx.clone(); - let server_id = server.server_id(); - server_state.io_logs_subscription = Some(server.on_io(move |io_kind, message| { - io_tx - .unbounded_send((server_id, io_kind, message.to_string())) - .ok(); - })); - } - - Some(server_state) - } - - pub fn add_language_server_log( - &mut self, - id: LanguageServerId, - typ: MessageType, - message: &str, - cx: &mut Context, - ) -> Option<()> { - let store_logs = !self.on_headless_host; - let language_server_state = self.get_language_server_state(id)?; - - let log_lines = &mut language_server_state.log_messages; - let message = message.trim_end().to_string(); - if !store_logs { - // Send all messages regardless of the visibility in case of not storing, to notify the receiver anyway - self.emit_event( - Event::NewServerLogEntry { - id, - kind: LanguageServerLogType::Log(typ), - text: message, - }, - cx, - ); - } else if let Some(new_message) = Self::push_new_message( - log_lines, - LogMessage { message, typ }, - language_server_state.log_level, - ) { - self.emit_event( - Event::NewServerLogEntry { - id, - kind: LanguageServerLogType::Log(typ), - text: new_message, - }, - cx, - ); - } - Some(()) - } - - fn add_language_server_trace( - &mut self, - id: LanguageServerId, - message: &str, - verbose_info: Option, - cx: &mut Context, - ) -> Option<()> { - let store_logs = !self.on_headless_host; - let language_server_state = self.get_language_server_state(id)?; - - let log_lines = &mut language_server_state.trace_messages; - if !store_logs { - // Send all messages regardless of the visibility in case of not storing, to notify the receiver anyway - self.emit_event( - Event::NewServerLogEntry { - id, - kind: LanguageServerLogType::Trace { verbose_info }, - text: message.trim().to_string(), - }, - cx, - ); - } else if let Some(new_message) = Self::push_new_message( - log_lines, - TraceMessage { - message: message.trim().to_string(), - is_verbose: false, - }, - TraceValue::Messages, - ) { - if let Some(verbose_message) = verbose_info.as_ref() { - Self::push_new_message( - log_lines, - TraceMessage { - message: verbose_message.clone(), - is_verbose: true, - }, - TraceValue::Verbose, - ); - } - self.emit_event( - Event::NewServerLogEntry { - id, - kind: LanguageServerLogType::Trace { verbose_info }, - text: new_message, - }, - cx, - ); - } - Some(()) - } - - fn push_new_message( - log_lines: &mut VecDeque, - message: T, - current_severity: ::Level, - ) -> Option { - while log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES { - log_lines.pop_front(); - } - let visible = message.should_include(current_severity); - - let visible_message = visible.then(|| message.as_ref().to_string()); - log_lines.push_back(message); - visible_message - } - - fn add_language_server_rpc( - &mut self, - language_server_id: LanguageServerId, - kind: MessageKind, - message: &str, - cx: &mut Context<'_, Self>, - ) { - let store_logs = !self.on_headless_host; - let Some(state) = self - .get_language_server_state(language_server_id) - .and_then(|state| state.rpc_state.as_mut()) - else { - return; - }; - - let received = kind == MessageKind::Receive; - let rpc_log_lines = &mut state.rpc_messages; - if state.last_message_kind != Some(kind) { - while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES { - rpc_log_lines.pop_front(); - } - let line_before_message = match kind { - MessageKind::Send => SEND_LINE, - MessageKind::Receive => RECEIVE_LINE, - }; - if store_logs { - rpc_log_lines.push_back(RpcMessage { - message: line_before_message.to_string(), - }); - } - // Do not send a synthetic message over the wire, it will be derived from the actual RPC message - cx.emit(Event::NewServerLogEntry { - id: language_server_id, - kind: LanguageServerLogType::Rpc { received }, - text: line_before_message.to_string(), - }); - } - - while rpc_log_lines.len() + 1 >= MAX_STORED_LOG_ENTRIES { - rpc_log_lines.pop_front(); - } - - if store_logs { - rpc_log_lines.push_back(RpcMessage { - message: message.trim().to_owned(), - }); - } - - self.emit_event( - Event::NewServerLogEntry { - id: language_server_id, - kind: LanguageServerLogType::Rpc { received }, - text: message.to_owned(), - }, - cx, - ); - } - - pub fn remove_language_server(&mut self, id: LanguageServerId, cx: &mut Context) { - self.language_servers.remove(&id); - cx.notify(); - } - - pub fn server_logs(&self, server_id: LanguageServerId) -> Option<&VecDeque> { - Some(&self.language_servers.get(&server_id)?.log_messages) - } - - pub fn server_trace(&self, server_id: LanguageServerId) -> Option<&VecDeque> { - Some(&self.language_servers.get(&server_id)?.trace_messages) - } - - pub fn server_ids_for_project<'a>( - &'a self, - lookup_project: &'a WeakEntity, - ) -> impl Iterator + 'a { - self.language_servers - .iter() - .filter_map(move |(id, state)| match &state.kind { - LanguageServerKind::Local { project } | LanguageServerKind::Remote { project } => { - if project == lookup_project { - Some(*id) - } else { - None - } - } - LanguageServerKind::Global | LanguageServerKind::LocalSsh { .. } => Some(*id), - }) - } - - pub fn enable_rpc_trace_for_language_server( - &mut self, - server_id: LanguageServerId, - ) -> Option<&mut LanguageServerRpcState> { - let rpc_state = self - .language_servers - .get_mut(&server_id)? - .rpc_state - .get_or_insert_with(|| LanguageServerRpcState { - rpc_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), - last_message_kind: None, - }); - Some(rpc_state) - } - - pub fn disable_rpc_trace_for_language_server( - &mut self, - server_id: LanguageServerId, - ) -> Option<()> { - self.language_servers.get_mut(&server_id)?.rpc_state.take(); - Some(()) - } - - pub fn has_server_logs(&self, server: &LanguageServerSelector) -> bool { - match server { - LanguageServerSelector::Id(id) => self.language_servers.contains_key(id), - LanguageServerSelector::Name(name) => self - .language_servers - .iter() - .any(|(_, state)| state.name.as_ref() == Some(name)), - } - } - - fn on_io( - &mut self, - language_server_id: LanguageServerId, - io_kind: IoKind, - message: &str, - cx: &mut Context, - ) -> Option<()> { - let is_received = match io_kind { - IoKind::StdOut => true, - IoKind::StdIn => false, - IoKind::StdErr => { - self.add_language_server_log(language_server_id, MessageType::LOG, message, cx); - return Some(()); - } - }; - - let kind = if is_received { - MessageKind::Receive - } else { - MessageKind::Send - }; - - self.add_language_server_rpc(language_server_id, kind, message, cx); - cx.notify(); - Some(()) - } - - fn emit_event(&mut self, e: Event, cx: &mut Context) { - match &e { - Event::NewServerLogEntry { id, kind, text } => { - if let Some(state) = self.get_language_server_state(*id) { - let downstream_client = match &state.kind { - LanguageServerKind::Remote { project } - | LanguageServerKind::Local { project } => project - .upgrade() - .map(|project| project.read(cx).lsp_store()), - LanguageServerKind::LocalSsh { lsp_store } => lsp_store.upgrade(), - LanguageServerKind::Global => None, - } - .and_then(|lsp_store| lsp_store.read(cx).downstream_client()); - if let Some((client, project_id)) = downstream_client { - if Some(LogKind::from_server_log_type(kind)) == state.toggled_log_kind { - client - .send(proto::LanguageServerLog { - project_id, - language_server_id: id.to_proto(), - message: text.clone(), - log_type: Some(kind.to_proto()), - }) - .ok(); - } - } - } - } - } - - cx.emit(e); - } - - pub fn toggle_lsp_logs( - &mut self, - server_id: LanguageServerId, - enabled: bool, - toggled_log_kind: LogKind, - ) { - if let Some(server_state) = self.get_language_server_state(server_id) { - if enabled { - server_state.toggled_log_kind = Some(toggled_log_kind); - } else { - server_state.toggled_log_kind = None; - } - } - if LogKind::Rpc == toggled_log_kind { - if enabled { - self.enable_rpc_trace_for_language_server(server_id); - } else { - self.disable_rpc_trace_for_language_server(server_id); - } - } - } -} diff --git a/crates/project/src/lsp_store/lsp_ext_command.rs b/crates/project/src/lsp_store/lsp_ext_command.rs deleted file mode 100644 index 5066143244..0000000000 --- a/crates/project/src/lsp_store/lsp_ext_command.rs +++ /dev/null @@ -1,806 +0,0 @@ -use crate::{ - LocationLink, - lsp_command::{ - LspCommand, file_path_to_lsp_url, location_link_from_lsp, location_link_from_proto, - location_link_to_proto, location_links_from_lsp, location_links_from_proto, - location_links_to_proto, - }, - lsp_store::LspStore, - make_lsp_text_document_position, make_text_document_identifier, -}; -use anyhow::{Context as _, Result}; -use async_trait::async_trait; -use collections::HashMap; -use gpui::{App, AsyncApp, Entity}; -use language::{ - Buffer, point_to_lsp, - proto::{deserialize_anchor, serialize_anchor}, -}; -use lsp::{AdapterServerCapabilities, LanguageServer, LanguageServerId}; -use rpc::proto::{self, PeerId}; -use serde::{Deserialize, Serialize}; -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; -use task::TaskTemplate; -use text::{BufferId, PointUtf16, ToPointUtf16}; - -pub enum LspExtExpandMacro {} - -impl lsp::request::Request for LspExtExpandMacro { - type Params = ExpandMacroParams; - type Result = Option; - const METHOD: &'static str = "rust-analyzer/expandMacro"; -} - -#[derive(Deserialize, Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct ExpandMacroParams { - pub text_document: lsp::TextDocumentIdentifier, - pub position: lsp::Position, -} - -#[derive(Default, Deserialize, Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct ExpandedMacro { - pub name: String, - pub expansion: String, -} - -impl ExpandedMacro { - pub fn is_empty(&self) -> bool { - self.name.is_empty() && self.expansion.is_empty() - } -} -#[derive(Debug)] -pub struct ExpandMacro { - pub position: PointUtf16, -} - -#[async_trait(?Send)] -impl LspCommand for ExpandMacro { - type Response = ExpandedMacro; - type LspRequest = LspExtExpandMacro; - type ProtoRequest = proto::LspExtExpandMacro; - - fn display_name(&self) -> &str { - "Expand macro" - } - - fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool { - true - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(ExpandMacroParams { - text_document: make_text_document_identifier(path)?, - position: point_to_lsp(self.position), - }) - } - - async fn response_from_lsp( - self, - message: Option, - _: Entity, - _: Entity, - _: LanguageServerId, - _: AsyncApp, - ) -> anyhow::Result { - Ok(message - .map(|message| ExpandedMacro { - name: message.name, - expansion: message.expansion, - }) - .unwrap_or_default()) - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtExpandMacro { - proto::LspExtExpandMacro { - project_id, - buffer_id: buffer.remote_id().into(), - position: Some(language::proto::serialize_anchor( - &buffer.anchor_before(self.position), - )), - } - } - - async fn from_proto( - message: Self::ProtoRequest, - _: Entity, - buffer: Entity, - cx: AsyncApp, - ) -> anyhow::Result { - let position = message - .position - .and_then(deserialize_anchor) - .context("invalid position")?; - Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, - }) - } - - fn response_to_proto( - response: ExpandedMacro, - _: &mut LspStore, - _: PeerId, - _: &clock::Global, - _: &mut App, - ) -> proto::LspExtExpandMacroResponse { - proto::LspExtExpandMacroResponse { - name: response.name, - expansion: response.expansion, - } - } - - async fn response_from_proto( - self, - message: proto::LspExtExpandMacroResponse, - _: Entity, - _: Entity, - _: AsyncApp, - ) -> anyhow::Result { - Ok(ExpandedMacro { - name: message.name, - expansion: message.expansion, - }) - } - - fn buffer_id_from_proto(message: &proto::LspExtExpandMacro) -> Result { - BufferId::new(message.buffer_id) - } -} - -pub enum LspOpenDocs {} - -impl lsp::request::Request for LspOpenDocs { - type Params = OpenDocsParams; - type Result = Option; - const METHOD: &'static str = "experimental/externalDocs"; -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct OpenDocsParams { - pub text_document: lsp::TextDocumentIdentifier, - pub position: lsp::Position, -} - -#[derive(Serialize, Deserialize, Debug, Default)] -#[serde(rename_all = "camelCase")] -pub struct DocsUrls { - pub web: Option, - pub local: Option, -} - -impl DocsUrls { - pub fn is_empty(&self) -> bool { - self.web.is_none() && self.local.is_none() - } -} - -#[derive(Debug)] -pub struct OpenDocs { - pub position: PointUtf16, -} - -#[async_trait(?Send)] -impl LspCommand for OpenDocs { - type Response = DocsUrls; - type LspRequest = LspOpenDocs; - type ProtoRequest = proto::LspExtOpenDocs; - - fn display_name(&self) -> &str { - "Open docs" - } - - fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool { - true - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(OpenDocsParams { - text_document: lsp::TextDocumentIdentifier { - uri: lsp::Uri::from_file_path(path).unwrap(), - }, - position: point_to_lsp(self.position), - }) - } - - async fn response_from_lsp( - self, - message: Option, - _: Entity, - _: Entity, - _: LanguageServerId, - _: AsyncApp, - ) -> anyhow::Result { - Ok(message - .map(|message| DocsUrls { - web: message.web, - local: message.local, - }) - .unwrap_or_default()) - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtOpenDocs { - proto::LspExtOpenDocs { - project_id, - buffer_id: buffer.remote_id().into(), - position: Some(language::proto::serialize_anchor( - &buffer.anchor_before(self.position), - )), - } - } - - async fn from_proto( - message: Self::ProtoRequest, - _: Entity, - buffer: Entity, - cx: AsyncApp, - ) -> anyhow::Result { - let position = message - .position - .and_then(deserialize_anchor) - .context("invalid position")?; - Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, - }) - } - - fn response_to_proto( - response: DocsUrls, - _: &mut LspStore, - _: PeerId, - _: &clock::Global, - _: &mut App, - ) -> proto::LspExtOpenDocsResponse { - proto::LspExtOpenDocsResponse { - web: response.web, - local: response.local, - } - } - - async fn response_from_proto( - self, - message: proto::LspExtOpenDocsResponse, - _: Entity, - _: Entity, - _: AsyncApp, - ) -> anyhow::Result { - Ok(DocsUrls { - web: message.web, - local: message.local, - }) - } - - fn buffer_id_from_proto(message: &proto::LspExtOpenDocs) -> Result { - BufferId::new(message.buffer_id) - } -} - -pub enum LspSwitchSourceHeader {} - -impl lsp::request::Request for LspSwitchSourceHeader { - type Params = SwitchSourceHeaderParams; - type Result = Option; - const METHOD: &'static str = "textDocument/switchSourceHeader"; -} - -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct SwitchSourceHeaderParams(lsp::TextDocumentIdentifier); - -#[derive(Serialize, Deserialize, Debug, Default)] -#[serde(rename_all = "camelCase")] -pub struct SwitchSourceHeaderResult(pub String); - -#[derive(Default, Deserialize, Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct SwitchSourceHeader; - -#[derive(Debug)] -pub struct GoToParentModule { - pub position: PointUtf16, -} - -pub struct LspGoToParentModule {} - -impl lsp::request::Request for LspGoToParentModule { - type Params = lsp::TextDocumentPositionParams; - type Result = Option>; - const METHOD: &'static str = "experimental/parentModule"; -} - -#[async_trait(?Send)] -impl LspCommand for SwitchSourceHeader { - type Response = SwitchSourceHeaderResult; - type LspRequest = LspSwitchSourceHeader; - type ProtoRequest = proto::LspExtSwitchSourceHeader; - - fn display_name(&self) -> &str { - "Switch source header" - } - - fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool { - true - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - Ok(SwitchSourceHeaderParams(make_text_document_identifier( - path, - )?)) - } - - async fn response_from_lsp( - self, - message: Option, - _: Entity, - _: Entity, - _: LanguageServerId, - _: AsyncApp, - ) -> anyhow::Result { - Ok(message - .map(|message| SwitchSourceHeaderResult(message.0)) - .unwrap_or_default()) - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtSwitchSourceHeader { - proto::LspExtSwitchSourceHeader { - project_id, - buffer_id: buffer.remote_id().into(), - } - } - - async fn from_proto( - _: Self::ProtoRequest, - _: Entity, - _: Entity, - _: AsyncApp, - ) -> anyhow::Result { - Ok(Self {}) - } - - fn response_to_proto( - response: SwitchSourceHeaderResult, - _: &mut LspStore, - _: PeerId, - _: &clock::Global, - _: &mut App, - ) -> proto::LspExtSwitchSourceHeaderResponse { - proto::LspExtSwitchSourceHeaderResponse { - target_file: response.0, - } - } - - async fn response_from_proto( - self, - message: proto::LspExtSwitchSourceHeaderResponse, - _: Entity, - _: Entity, - _: AsyncApp, - ) -> anyhow::Result { - Ok(SwitchSourceHeaderResult(message.target_file)) - } - - fn buffer_id_from_proto(message: &proto::LspExtSwitchSourceHeader) -> Result { - BufferId::new(message.buffer_id) - } -} - -#[async_trait(?Send)] -impl LspCommand for GoToParentModule { - type Response = Vec; - type LspRequest = LspGoToParentModule; - type ProtoRequest = proto::LspExtGoToParentModule; - - fn display_name(&self) -> &str { - "Go to parent module" - } - - fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool { - true - } - - fn to_lsp( - &self, - path: &Path, - _: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - make_lsp_text_document_position(path, self.position) - } - - async fn response_from_lsp( - self, - links: Option>, - lsp_store: Entity, - buffer: Entity, - server_id: LanguageServerId, - cx: AsyncApp, - ) -> anyhow::Result> { - location_links_from_lsp( - links.map(lsp::GotoDefinitionResponse::Link), - lsp_store, - buffer, - server_id, - cx, - ) - .await - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtGoToParentModule { - proto::LspExtGoToParentModule { - project_id, - buffer_id: buffer.remote_id().to_proto(), - position: Some(language::proto::serialize_anchor( - &buffer.anchor_before(self.position), - )), - } - } - - async fn from_proto( - request: Self::ProtoRequest, - _: Entity, - buffer: Entity, - cx: AsyncApp, - ) -> anyhow::Result { - let position = request - .position - .and_then(deserialize_anchor) - .context("bad request with bad position")?; - Ok(Self { - position: buffer.read_with(&cx, |buffer, _| position.to_point_utf16(buffer))?, - }) - } - - fn response_to_proto( - links: Vec, - lsp_store: &mut LspStore, - peer_id: PeerId, - _: &clock::Global, - cx: &mut App, - ) -> proto::LspExtGoToParentModuleResponse { - proto::LspExtGoToParentModuleResponse { - links: location_links_to_proto(links, lsp_store, peer_id, cx), - } - } - - async fn response_from_proto( - self, - message: proto::LspExtGoToParentModuleResponse, - lsp_store: Entity, - _: Entity, - cx: AsyncApp, - ) -> anyhow::Result> { - location_links_from_proto(message.links, lsp_store, cx).await - } - - fn buffer_id_from_proto(message: &proto::LspExtGoToParentModule) -> Result { - BufferId::new(message.buffer_id) - } -} - -// https://rust-analyzer.github.io/book/contributing/lsp-extensions.html#runnables -// Taken from https://github.com/rust-lang/rust-analyzer/blob/a73a37a757a58b43a796d3eb86a1f7dfd0036659/crates/rust-analyzer/src/lsp/ext.rs#L425-L489 -pub enum Runnables {} - -impl lsp::request::Request for Runnables { - type Params = RunnablesParams; - type Result = Vec; - const METHOD: &'static str = "experimental/runnables"; -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct RunnablesParams { - pub text_document: lsp::TextDocumentIdentifier, - #[serde(default)] - pub position: Option, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct Runnable { - pub label: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub location: Option, - pub kind: RunnableKind, - pub args: RunnableArgs, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -#[serde(untagged)] -pub enum RunnableArgs { - Cargo(CargoRunnableArgs), - Shell(ShellRunnableArgs), -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "lowercase")] -pub enum RunnableKind { - Cargo, - Shell, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct CargoRunnableArgs { - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub environment: HashMap, - pub cwd: PathBuf, - /// Command to be executed instead of cargo - #[serde(default)] - pub override_cargo: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub workspace_root: Option, - // command, --package and --lib stuff - #[serde(default)] - pub cargo_args: Vec, - // stuff after -- - #[serde(default)] - pub executable_args: Vec, -} - -#[derive(Deserialize, Serialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct ShellRunnableArgs { - #[serde(default, skip_serializing_if = "HashMap::is_empty")] - pub environment: HashMap, - pub cwd: PathBuf, - pub program: String, - #[serde(default)] - pub args: Vec, -} - -#[derive(Debug)] -pub struct GetLspRunnables { - pub buffer_id: BufferId, - pub position: Option, -} - -#[derive(Debug, Default)] -pub struct LspRunnables { - pub runnables: Vec<(Option, TaskTemplate)>, -} - -#[async_trait(?Send)] -impl LspCommand for GetLspRunnables { - type Response = LspRunnables; - type LspRequest = Runnables; - type ProtoRequest = proto::LspExtRunnables; - - fn display_name(&self) -> &str { - "LSP Runnables" - } - - fn check_capabilities(&self, _: AdapterServerCapabilities) -> bool { - true - } - - fn to_lsp( - &self, - path: &Path, - buffer: &Buffer, - _: &Arc, - _: &App, - ) -> Result { - let url = file_path_to_lsp_url(path)?; - Ok(RunnablesParams { - text_document: lsp::TextDocumentIdentifier::new(url), - position: self - .position - .map(|anchor| point_to_lsp(anchor.to_point_utf16(&buffer.snapshot()))), - }) - } - - async fn response_from_lsp( - self, - lsp_runnables: Vec, - lsp_store: Entity, - buffer: Entity, - server_id: LanguageServerId, - mut cx: AsyncApp, - ) -> Result { - let mut runnables = Vec::with_capacity(lsp_runnables.len()); - - for runnable in lsp_runnables { - let location = match runnable.location { - Some(location) => Some( - location_link_from_lsp(location, &lsp_store, &buffer, server_id, &mut cx) - .await?, - ), - None => None, - }; - let mut task_template = TaskTemplate::default(); - task_template.label = runnable.label; - match runnable.args { - RunnableArgs::Cargo(cargo) => { - match cargo.override_cargo { - Some(override_cargo) => { - let mut override_parts = - override_cargo.split(" ").map(|s| s.to_string()); - task_template.command = override_parts - .next() - .unwrap_or_else(|| override_cargo.clone()); - task_template.args.extend(override_parts); - } - None => task_template.command = "cargo".to_string(), - }; - task_template.env = cargo.environment; - task_template.cwd = Some( - cargo - .workspace_root - .unwrap_or(cargo.cwd) - .to_string_lossy() - .to_string(), - ); - task_template.args.extend(cargo.cargo_args); - if !cargo.executable_args.is_empty() { - let shell_kind = task_template.shell.shell_kind(cfg!(windows)); - task_template.args.push("--".to_string()); - task_template.args.extend( - cargo - .executable_args - .into_iter() - // rust-analyzer's doctest data may be smth. like - // ``` - // command: "cargo", - // args: [ - // "test", - // "--doc", - // "--package", - // "cargo-output-parser", - // "--", - // "X::new", - // "--show-output", - // ], - // ``` - // and `X::new` will cause troubles if not escaped properly, as later - // the task runs as `$SHELL -i -c "cargo test ..."`. - // - // We cannot escape all shell arguments unconditionally, as we use this for ssh commands, which may involve paths starting with `~`. - // That bit is not auto-expanded when using single quotes. - // Escape extra cargo args unconditionally as those are unlikely to contain `~`. - .flat_map(|extra_arg| { - shell_kind.try_quote(&extra_arg).map(|s| s.to_string()) - }), - ); - } - } - RunnableArgs::Shell(shell) => { - task_template.command = shell.program; - task_template.args = shell.args; - task_template.env = shell.environment; - task_template.cwd = Some(shell.cwd.to_string_lossy().into_owned()); - } - } - - runnables.push((location, task_template)); - } - - Ok(LspRunnables { runnables }) - } - - fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::LspExtRunnables { - proto::LspExtRunnables { - project_id, - buffer_id: buffer.remote_id().to_proto(), - position: self.position.as_ref().map(serialize_anchor), - } - } - - async fn from_proto( - message: proto::LspExtRunnables, - _: Entity, - _: Entity, - _: AsyncApp, - ) -> Result { - let buffer_id = Self::buffer_id_from_proto(&message)?; - let position = message.position.and_then(deserialize_anchor); - Ok(Self { - buffer_id, - position, - }) - } - - fn response_to_proto( - response: LspRunnables, - lsp_store: &mut LspStore, - peer_id: PeerId, - _: &clock::Global, - cx: &mut App, - ) -> proto::LspExtRunnablesResponse { - proto::LspExtRunnablesResponse { - runnables: response - .runnables - .into_iter() - .map(|(location, task_template)| proto::LspRunnable { - location: location - .map(|location| location_link_to_proto(location, lsp_store, peer_id, cx)), - task_template: serde_json::to_vec(&task_template).unwrap(), - }) - .collect(), - } - } - - async fn response_from_proto( - self, - message: proto::LspExtRunnablesResponse, - lsp_store: Entity, - _: Entity, - mut cx: AsyncApp, - ) -> Result { - let mut runnables = LspRunnables { - runnables: Vec::new(), - }; - - for lsp_runnable in message.runnables { - let location = match lsp_runnable.location { - Some(location) => { - Some(location_link_from_proto(location, lsp_store.clone(), &mut cx).await?) - } - None => None, - }; - let task_template = serde_json::from_slice(&lsp_runnable.task_template) - .context("deserializing task template from proto")?; - runnables.runnables.push((location, task_template)); - } - - Ok(runnables) - } - - fn buffer_id_from_proto(message: &proto::LspExtRunnables) -> Result { - BufferId::new(message.buffer_id) - } -} - -#[derive(Debug)] -pub struct LspExtCancelFlycheck {} - -#[derive(Debug)] -pub struct LspExtRunFlycheck {} - -#[derive(Debug)] -pub struct LspExtClearFlycheck {} - -impl lsp::notification::Notification for LspExtCancelFlycheck { - type Params = (); - const METHOD: &'static str = "rust-analyzer/cancelFlycheck"; -} - -impl lsp::notification::Notification for LspExtRunFlycheck { - type Params = RunFlycheckParams; - const METHOD: &'static str = "rust-analyzer/runFlycheck"; -} - -#[derive(Deserialize, Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct RunFlycheckParams { - pub text_document: Option, -} - -impl lsp::notification::Notification for LspExtClearFlycheck { - type Params = (); - const METHOD: &'static str = "rust-analyzer/clearFlycheck"; -} diff --git a/crates/project/src/lsp_store/rust_analyzer_ext.rs b/crates/project/src/lsp_store/rust_analyzer_ext.rs deleted file mode 100644 index 4d5f134e5f..0000000000 --- a/crates/project/src/lsp_store/rust_analyzer_ext.rs +++ /dev/null @@ -1,275 +0,0 @@ -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 rpc::proto; - -use crate::{LspStore, LspStoreEvent, Project, ProjectPath, lsp_store}; - -pub const RUST_ANALYZER_NAME: LanguageServerName = LanguageServerName::new_static("rust-analyzer"); -pub const CARGO_DIAGNOSTICS_SOURCE_NAME: &str = "rustc"; - -/// Experimental: Informs the end user about the state of the server -/// -/// [Rust Analyzer Specification](https://rust-analyzer.github.io/book/contributing/lsp-extensions.html#server-status) -#[derive(Debug)] -enum ServerStatus {} - -#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)] -#[serde(rename_all = "camelCase")] -struct ServerStatusParams { - pub health: ServerHealth, - pub message: Option, -} - -impl lsp::notification::Notification for ServerStatus { - type Params = ServerStatusParams; - const METHOD: &'static str = "experimental/serverStatus"; -} - -pub fn register_notifications(lsp_store: WeakEntity, language_server: &LanguageServer) { - let name = language_server.name(); - let server_id = language_server.server_id(); - - language_server - .on_notification::({ - move |params, cx| { - let message = params.message; - let log_message = message.as_ref().map(|message| { - format!("Language server {name} (id {server_id}) status update: {message}") - }); - let status = match ¶ms.health { - ServerHealth::Ok => { - if let Some(log_message) = log_message { - log::info!("{log_message}"); - } - proto::ServerHealth::Ok - } - ServerHealth::Warning => { - if let Some(log_message) = log_message { - log::warn!("{log_message}"); - } - proto::ServerHealth::Warning - } - ServerHealth::Error => { - if let Some(log_message) = log_message { - log::error!("{log_message}"); - } - proto::ServerHealth::Error - } - }; - - lsp_store - .update(cx, |_, cx| { - cx.emit(LspStoreEvent::LanguageServerUpdate { - language_server_id: server_id, - name: Some(name.clone()), - message: proto::update_language_server::Variant::StatusUpdate( - proto::StatusUpdate { - message, - status: Some(proto::status_update::Status::Health( - status as i32, - )), - }, - ), - }); - }) - .ok(); - } - }) - .detach(); -} - -pub fn cancel_flycheck( - project: Entity, - buffer_path: Option, - cx: &mut App, -) -> Task> { - let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); - let lsp_store = project.read(cx).lsp_store(); - let buffer = buffer_path.map(|buffer_path| { - project.update(cx, |project, cx| { - project.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(buffer_path, cx) - }) - }) - }); - - cx.spawn(async move |cx| { - let buffer = match buffer { - Some(buffer) => Some(buffer.await?), - None => None, - }; - let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), cx) - else { - return Ok(()); - }; - - if let Some((client, project_id)) = upstream_client { - let request = proto::LspExtCancelFlycheck { - project_id, - language_server_id: rust_analyzer_server.to_proto(), - }; - client - .request(request) - .await - .context("lsp ext cancel flycheck proto request")?; - } else { - lsp_store - .read_with(cx, |lsp_store, _| { - if let Some(server) = lsp_store.language_server_for_id(rust_analyzer_server) { - server.notify::(()) - } else { - Ok(()) - } - }) - .context("lsp ext cancel flycheck")??; - }; - anyhow::Ok(()) - }) -} - -pub fn run_flycheck( - project: Entity, - buffer_path: Option, - cx: &mut App, -) -> Task> { - let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); - let lsp_store = project.read(cx).lsp_store(); - let buffer = buffer_path.map(|buffer_path| { - project.update(cx, |project, cx| { - project.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(buffer_path, cx) - }) - }) - }); - - cx.spawn(async move |cx| { - let buffer = match buffer { - Some(buffer) => Some(buffer.await?), - None => None, - }; - let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), cx) - else { - return Ok(()); - }; - - 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, - language_server_id: rust_analyzer_server.to_proto(), - current_file_only: false, - }; - client - .request(request) - .await - .context("lsp ext run flycheck proto request")?; - } else { - lsp_store - .read_with(cx, |lsp_store, _| { - if let Some(server) = lsp_store.language_server_for_id(rust_analyzer_server) { - server.notify::( - lsp_store::lsp_ext_command::RunFlycheckParams { - text_document: None, - }, - ) - } else { - Ok(()) - } - }) - .context("lsp ext run flycheck")??; - }; - anyhow::Ok(()) - }) -} - -pub fn clear_flycheck( - project: Entity, - buffer_path: Option, - cx: &mut App, -) -> Task> { - let upstream_client = project.read(cx).lsp_store().read(cx).upstream_client(); - let lsp_store = project.read(cx).lsp_store(); - let buffer = buffer_path.map(|buffer_path| { - project.update(cx, |project, cx| { - project.buffer_store().update(cx, |buffer_store, cx| { - buffer_store.open_buffer(buffer_path, cx) - }) - }) - }); - - cx.spawn(async move |cx| { - let buffer = match buffer { - Some(buffer) => Some(buffer.await?), - None => None, - }; - let Some(rust_analyzer_server) = find_rust_analyzer_server(&project, buffer.as_ref(), cx) - else { - return Ok(()); - }; - - if let Some((client, project_id)) = upstream_client { - let request = proto::LspExtClearFlycheck { - project_id, - language_server_id: rust_analyzer_server.to_proto(), - }; - client - .request(request) - .await - .context("lsp ext clear flycheck proto request")?; - } else { - lsp_store - .read_with(cx, |lsp_store, _| { - if let Some(server) = lsp_store.language_server_for_id(rust_analyzer_server) { - server.notify::(()) - } else { - Ok(()) - } - }) - .context("lsp ext 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/lsp_store/vue_language_server_ext.rs b/crates/project/src/lsp_store/vue_language_server_ext.rs deleted file mode 100644 index 2824974540..0000000000 --- a/crates/project/src/lsp_store/vue_language_server_ext.rs +++ /dev/null @@ -1,124 +0,0 @@ -use anyhow::Context as _; -use gpui::{AppContext, WeakEntity}; -use lsp::{LanguageServer, LanguageServerName}; -use serde_json::Value; - -use crate::LspStore; - -struct VueServerRequest; -struct TypescriptServerResponse; - -impl lsp::notification::Notification for VueServerRequest { - type Params = Vec<(u64, String, serde_json::Value)>; - - const METHOD: &'static str = "tsserver/request"; -} - -impl lsp::notification::Notification for TypescriptServerResponse { - type Params = Vec<(u64, serde_json::Value)>; - - const METHOD: &'static str = "tsserver/response"; -} - -const VUE_SERVER_NAME: LanguageServerName = LanguageServerName::new_static("vue-language-server"); -const VTSLS: LanguageServerName = LanguageServerName::new_static("vtsls"); -const TS_LS: LanguageServerName = LanguageServerName::new_static("typescript-language-server"); - -pub fn register_requests(lsp_store: WeakEntity, language_server: &LanguageServer) { - let language_server_name = language_server.name(); - if language_server_name == VUE_SERVER_NAME { - let vue_server_id = language_server.server_id(); - language_server - .on_notification::({ - move |params, cx| { - let lsp_store = lsp_store.clone(); - let Ok(Some(vue_server)) = lsp_store.read_with(cx, |this, _| { - this.language_server_for_id(vue_server_id) - }) else { - return; - }; - - let requests = params; - let target_server = match lsp_store.read_with(cx, |this, _| { - let language_server_id = this - .as_local() - .and_then(|local| { - local.language_server_ids.iter().find_map(|(seed, v)| { - [VTSLS, TS_LS].contains(&seed.name).then_some(v.id) - }) - }) - .context("Could not find language server")?; - - this.language_server_for_id(language_server_id) - .context("language server not found") - }) { - Ok(Ok(server)) => server, - other => { - log::warn!( - "vue-language-server forwarding skipped: {other:?}. \ - Returning null tsserver responses" - ); - if !requests.is_empty() { - let null_responses = requests - .into_iter() - .map(|(id, _, _)| (id, Value::Null)) - .collect::>(); - let _ = vue_server - .notify::(null_responses); - } - return; - } - }; - - let cx = cx.clone(); - for (request_id, command, payload) in requests.into_iter() { - let target_server = target_server.clone(); - let vue_server = vue_server.clone(); - cx.background_spawn(async move { - let response = target_server - .request::( - lsp::ExecuteCommandParams { - command: "typescript.tsserverRequest".to_owned(), - arguments: vec![Value::String(command), payload], - ..Default::default() - }, - ) - .await; - - let response_body = match response { - util::ConnectionResult::Result(Ok(result)) => match result { - Some(Value::Object(mut map)) => map - .remove("body") - .unwrap_or(Value::Object(map)), - Some(other) => other, - None => Value::Null, - }, - util::ConnectionResult::Result(Err(error)) => { - log::warn!( - "typescript.tsserverRequest failed: {error:?} for request {request_id}" - ); - Value::Null - } - other => { - log::warn!( - "typescript.tsserverRequest did not return a response: {other:?} for request {request_id}" - ); - Value::Null - } - }; - - if let Err(err) = vue_server - .notify::(vec![(request_id, response_body)]) - { - log::warn!( - "Failed to notify vue-language-server of tsserver response: {err:?}" - ); - } - }) - .detach(); - } - } - }) - .detach(); - } -} diff --git a/crates/project/src/manifest_tree.rs b/crates/project/src/manifest_tree.rs deleted file mode 100644 index ffa4872ca7..0000000000 --- a/crates/project/src/manifest_tree.rs +++ /dev/null @@ -1,224 +0,0 @@ -//! This module defines a Manifest Tree. -//! -//! A Manifest Tree is responsible for determining where the manifests for subprojects are located in a project. -//! This then is used to provide those locations to language servers & determine locations eligible for toolchain selection. - -mod manifest_store; -mod path_trie; -mod server_tree; - -use std::{borrow::Borrow, collections::hash_map::Entry, ops::ControlFlow, sync::Arc}; - -use collections::HashMap; -use gpui::{App, AppContext as _, Context, Entity, Subscription}; -use language::{ManifestDelegate, ManifestName, ManifestQuery}; -pub use manifest_store::ManifestProvidersStore; -use path_trie::{LabelPresence, RootPathTrie, TriePath}; -use settings::{SettingsStore, WorktreeId}; -use util::rel_path::RelPath; -use worktree::{Event as WorktreeEvent, Snapshot, Worktree}; - -use crate::{ - ProjectPath, - worktree_store::{WorktreeStore, WorktreeStoreEvent}, -}; - -pub(crate) use server_tree::{LanguageServerTree, LanguageServerTreeNode, LaunchDisposition}; - -struct WorktreeRoots { - roots: RootPathTrie, - worktree_store: Entity, - _worktree_subscription: Subscription, -} - -impl WorktreeRoots { - fn new( - worktree_store: Entity, - worktree: Entity, - cx: &mut App, - ) -> Entity { - cx.new(|cx| Self { - roots: RootPathTrie::new(), - worktree_store, - _worktree_subscription: cx.subscribe(&worktree, |this: &mut Self, _, event, cx| { - match event { - WorktreeEvent::UpdatedEntries(changes) => { - for (path, _, kind) in changes.iter() { - if kind == &worktree::PathChange::Removed { - let path = TriePath::from(path.as_ref()); - this.roots.remove(&path); - } - } - } - WorktreeEvent::UpdatedGitRepositories(_) => {} - WorktreeEvent::DeletedEntry(entry_id) => { - let Some(entry) = this.worktree_store.read(cx).entry_for_id(*entry_id, cx) - else { - return; - }; - let path = TriePath::from(entry.path.as_ref()); - this.roots.remove(&path); - } - } - }), - }) - } -} - -pub struct ManifestTree { - root_points: HashMap>, - worktree_store: Entity, - _subscriptions: [Subscription; 2], -} - -impl ManifestTree { - pub fn new(worktree_store: Entity, cx: &mut App) -> Entity { - cx.new(|cx| Self { - root_points: Default::default(), - _subscriptions: [ - cx.subscribe(&worktree_store, Self::on_worktree_store_event), - cx.observe_global::(|this, cx| { - for roots in this.root_points.values_mut() { - roots.update(cx, |worktree_roots, _| { - worktree_roots.roots = RootPathTrie::new(); - }) - } - }), - ], - worktree_store, - }) - } - - pub(crate) fn root_for_path( - &mut self, - ProjectPath { worktree_id, path }: &ProjectPath, - manifest_name: &ManifestName, - delegate: &Arc, - cx: &mut App, - ) -> Option { - debug_assert_eq!(delegate.worktree_id(), *worktree_id); - let (mut marked_path, mut current_presence) = (None, LabelPresence::KnownAbsent); - let worktree_roots = match self.root_points.entry(*worktree_id) { - Entry::Occupied(occupied_entry) => occupied_entry.get().clone(), - Entry::Vacant(vacant_entry) => { - let Some(worktree) = self - .worktree_store - .read(cx) - .worktree_for_id(*worktree_id, cx) - else { - return Default::default(); - }; - let roots = WorktreeRoots::new(self.worktree_store.clone(), worktree, cx); - vacant_entry.insert(roots).clone() - } - }; - - let key = TriePath::from(&**path); - worktree_roots.read_with(cx, |this, _| { - this.roots.walk(&key, &mut |path, labels| { - for (label, presence) in labels { - if label == manifest_name { - if current_presence > *presence { - debug_assert!(false, "RootPathTrie precondition violation; while walking the tree label presence is only allowed to increase"); - } - marked_path = Some(ProjectPath {worktree_id: *worktree_id, path: path.clone()}); - current_presence = *presence; - } - - } - ControlFlow::Continue(()) - }); - }); - - if current_presence == LabelPresence::KnownAbsent { - // Some part of the path is unexplored. - let depth = marked_path - .as_ref() - .map(|root_path| { - path.strip_prefix(&root_path.path) - .unwrap() - .components() - .count() - }) - .unwrap_or_else(|| path.components().count() + 1); - - if depth > 0 - && let Some(provider) = - ManifestProvidersStore::global(cx).get(manifest_name.borrow()) - { - let root = provider.search(ManifestQuery { - path: path.clone(), - depth, - delegate: delegate.clone(), - }); - match root { - Some(known_root) => worktree_roots.update(cx, |this, _| { - let root = TriePath::from(&*known_root); - this.roots - .insert(&root, manifest_name.clone(), LabelPresence::Present); - current_presence = LabelPresence::Present; - marked_path = Some(ProjectPath { - worktree_id: *worktree_id, - path: known_root, - }); - }), - None => worktree_roots.update(cx, |this, _| { - this.roots - .insert(&key, manifest_name.clone(), LabelPresence::KnownAbsent); - }), - } - } - } - marked_path.filter(|_| current_presence.eq(&LabelPresence::Present)) - } - - pub(crate) fn root_for_path_or_worktree_root( - &mut self, - project_path: &ProjectPath, - manifest_name: Option<&ManifestName>, - delegate: &Arc, - cx: &mut App, - ) -> ProjectPath { - let worktree_id = project_path.worktree_id; - // Backwards-compat: Fill in any adapters for which we did not detect the root as having the project root at the root of a worktree. - manifest_name - .and_then(|manifest_name| self.root_for_path(project_path, manifest_name, delegate, cx)) - .unwrap_or_else(|| ProjectPath { - worktree_id, - path: RelPath::empty().into(), - }) - } - - fn on_worktree_store_event( - &mut self, - _: Entity, - evt: &WorktreeStoreEvent, - _: &mut Context, - ) { - if let WorktreeStoreEvent::WorktreeRemoved(_, worktree_id) = evt { - self.root_points.remove(worktree_id); - } - } -} - -pub(crate) struct ManifestQueryDelegate { - worktree: Snapshot, -} - -impl ManifestQueryDelegate { - pub fn new(worktree: Snapshot) -> Self { - Self { worktree } - } -} - -impl ManifestDelegate for ManifestQueryDelegate { - fn exists(&self, path: &RelPath, is_dir: Option) -> bool { - self.worktree.entry_for_path(path).is_some_and(|entry| { - is_dir.is_none_or(|is_required_to_be_dir| is_required_to_be_dir == entry.is_dir()) - }) - } - - fn worktree_id(&self) -> WorktreeId { - self.worktree.id() - } -} diff --git a/crates/project/src/manifest_tree/manifest_store.rs b/crates/project/src/manifest_tree/manifest_store.rs deleted file mode 100644 index cf9f81aee4..0000000000 --- a/crates/project/src/manifest_tree/manifest_store.rs +++ /dev/null @@ -1,51 +0,0 @@ -use collections::{HashMap, HashSet}; -use gpui::{App, Global, SharedString}; -use parking_lot::RwLock; -use std::{ops::Deref, sync::Arc}; - -use language::{ManifestName, ManifestProvider}; - -#[derive(Default)] -struct ManifestProvidersState { - providers: HashMap>, -} - -#[derive(Clone, Default)] -pub struct ManifestProvidersStore(Arc>); - -#[derive(Default)] -struct GlobalManifestProvider(ManifestProvidersStore); - -impl Deref for GlobalManifestProvider { - type Target = ManifestProvidersStore; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Global for GlobalManifestProvider {} - -impl ManifestProvidersStore { - /// Returns the global [`ManifestStore`]. - /// - /// Inserts a default [`ManifestStore`] if one does not yet exist. - pub fn global(cx: &mut App) -> Self { - cx.default_global::().0.clone() - } - - pub fn register(&self, provider: Arc) { - self.0.write().providers.insert(provider.name(), provider); - } - - pub fn unregister(&self, name: &SharedString) { - self.0.write().providers.remove(name); - } - - pub(super) fn get(&self, name: &SharedString) -> Option> { - self.0.read().providers.get(name).cloned() - } - pub(crate) fn manifest_file_names(&self) -> HashSet { - self.0.read().providers.keys().cloned().collect() - } -} diff --git a/crates/project/src/manifest_tree/path_trie.rs b/crates/project/src/manifest_tree/path_trie.rs deleted file mode 100644 index 9710bb46d0..0000000000 --- a/crates/project/src/manifest_tree/path_trie.rs +++ /dev/null @@ -1,265 +0,0 @@ -use std::{ - collections::{BTreeMap, btree_map::Entry}, - ops::ControlFlow, - sync::Arc, -}; - -use util::rel_path::RelPath; - -/// [RootPathTrie] is a workhorse of [super::ManifestTree]. It is responsible for determining the closest known entry for a given path. -/// It also determines how much of a given path is unexplored, thus letting callers fill in that gap if needed. -/// Conceptually, it allows one to annotate Worktree entries with arbitrary extra metadata and run closest-ancestor searches. -/// -/// A path is unexplored when the closest ancestor of a path is not the path itself; that means that we have not yet ran the scan on that path. -/// For example, if there's a project root at path `python/project` and we query for a path `python/project/subdir/another_subdir/file.py`, there is -/// a known root at `python/project` and the unexplored part is `subdir/another_subdir` - we need to run a scan on these 2 directories. -pub(super) struct RootPathTrie

{ - const GROUP_NAME: &str = "project_entry"; - - let kind = details.kind; - let is_sticky = details.sticky.is_some(); - let sticky_index = details.sticky.as_ref().map(|this| this.sticky_index); - let settings = ProjectPanelSettings::get_global(cx); - let show_editor = details.is_editing && !details.is_processing; - - let selection = SelectedEntry { - worktree_id: details.worktree_id, - entry_id, - }; - - let is_marked = self.marked_entries.contains(&selection); - let is_active = self - .state - .selection - .is_some_and(|selection| selection.entry_id == entry_id); - - let file_name = details.filename.clone(); - - let mut icon = details.icon.clone(); - if settings.file_icons && show_editor && details.kind.is_file() { - let filename = self.filename_editor.read(cx).text(cx); - if filename.len() > 2 { - icon = FileIcons::get_icon(Path::new(&filename), cx); - } - } - - let filename_text_color = details.filename_text_color; - let diagnostic_severity = details.diagnostic_severity; - let item_colors = get_item_color(is_sticky, cx); - - let canonical_path = details - .canonical_path - .as_ref() - .map(|f| f.to_string_lossy().into_owned()); - let path_style = self.project.read(cx).path_style(cx); - let path = details.path.clone(); - let path_for_external_paths = path.clone(); - let path_for_dragged_selection = path.clone(); - - let depth = details.depth; - let worktree_id = details.worktree_id; - let dragged_selection = DraggedSelection { - active_selection: SelectedEntry { - worktree_id: selection.worktree_id, - entry_id: self.resolve_entry(selection.entry_id), - }, - marked_selections: Arc::from(self.marked_entries.clone()), - }; - - let bg_color = if is_marked { - item_colors.marked - } else { - item_colors.default - }; - - let bg_hover_color = if is_marked { - item_colors.marked - } else { - item_colors.hover - }; - - let validation_color_and_message = if show_editor { - match self - .state - .edit_state - .as_ref() - .map_or(ValidationState::None, |e| e.validation_state.clone()) - { - ValidationState::Error(msg) => Some((Color::Error.color(cx), msg)), - ValidationState::Warning(msg) => Some((Color::Warning.color(cx), msg)), - ValidationState::None => None, - } - } else { - None - }; - - let border_color = - if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) { - match validation_color_and_message { - Some((color, _)) => color, - None => item_colors.focused, - } - } else { - bg_color - }; - - let border_hover_color = - if !self.mouse_down && is_active && self.focus_handle.contains_focused(window, cx) { - match validation_color_and_message { - Some((color, _)) => color, - None => item_colors.focused, - } - } else { - bg_hover_color - }; - - let folded_directory_drag_target = self.folded_directory_drag_target; - let is_highlighted = { - if let Some(highlight_entry_id) = - self.drag_target_entry - .as_ref() - .and_then(|drag_target| match drag_target { - DragTarget::Entry { - highlight_entry_id, .. - } => Some(*highlight_entry_id), - DragTarget::Background => self.state.last_worktree_root_id, - }) - { - // Highlight if same entry or it's children - if entry_id == highlight_entry_id { - true - } else { - maybe!({ - let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?; - let highlight_entry = worktree.read(cx).entry_for_id(highlight_entry_id)?; - Some(path.starts_with(&highlight_entry.path)) - }) - .unwrap_or(false) - } - } else { - false - } - }; - - let id: ElementId = if is_sticky { - SharedString::from(format!("project_panel_sticky_item_{}", entry_id.to_usize())).into() - } else { - (entry_id.to_proto() as usize).into() - }; - - div() - .id(id.clone()) - .relative() - .group(GROUP_NAME) - .cursor_pointer() - .rounded_none() - .bg(bg_color) - .border_1() - .border_r_2() - .border_color(border_color) - .hover(|style| style.bg(bg_hover_color).border_color(border_hover_color)) - .when(is_sticky, |this| { - this.block_mouse_except_scroll() - }) - .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() - .and_then(|entry| match entry { - DragTarget::Entry { entry_id: target_id, .. } => Some(*target_id), - DragTarget::Background { .. } => None, - }) == Some(entry_id); - - if !event.bounds.contains(&event.event.position) { - // Entry responsible for setting drag target is also responsible to - // clear it up after drag is out of bounds - if is_current_target { - this.drag_target_entry = None; - } - return; - } - - if is_current_target { - return; - } - - this.marked_entries.clear(); - - let Some((entry_id, highlight_entry_id)) = maybe!({ - let target_worktree = this.project.read(cx).worktree_for_id(selection.worktree_id, cx)?.read(cx); - let target_entry = target_worktree.entry_for_path(&path_for_external_paths)?; - let highlight_entry_id = this.highlight_entry_for_external_drag(target_entry, target_worktree)?; - Some((target_entry.id, highlight_entry_id)) - }) else { - return; - }; - - this.drag_target_entry = Some(DragTarget::Entry { - entry_id, - highlight_entry_id, - }); - - }, - )) - .on_drop(cx.listener( - move |this, external_paths: &ExternalPaths, window, cx| { - this.drag_target_entry = None; - this.hover_scroll_task.take(); - this.drop_external_files(external_paths.paths(), entry_id, window, cx); - cx.stop_propagation(); - }, - )) - .on_drag_move::(cx.listener( - move |this, event: &DragMoveEvent, window, cx| { - let is_current_target = this.drag_target_entry.as_ref() - .and_then(|entry| match entry { - DragTarget::Entry { entry_id: target_id, .. } => Some(*target_id), - DragTarget::Background { .. } => None, - }) == Some(entry_id); - - if !event.bounds.contains(&event.event.position) { - // Entry responsible for setting drag target is also responsible to - // clear it up after drag is out of bounds - if is_current_target { - this.drag_target_entry = None; - } - return; - } - - if is_current_target { - return; - } - - let drag_state = event.drag(cx); - - if drag_state.items().count() == 1 { - this.marked_entries.clear(); - this.marked_entries.push(drag_state.active_selection); - } - - let Some((entry_id, highlight_entry_id)) = maybe!({ - let target_worktree = this.project.read(cx).worktree_for_id(selection.worktree_id, cx)?.read(cx); - let target_entry = target_worktree.entry_for_path(&path_for_dragged_selection)?; - let highlight_entry_id = this.highlight_entry_for_selection_drag(target_entry, target_worktree, drag_state, cx)?; - Some((target_entry.id, highlight_entry_id)) - }) else { - return; - }; - - this.drag_target_entry = Some(DragTarget::Entry { - entry_id, - highlight_entry_id, - }); - - this.hover_expand_task.take(); - - if !kind.is_dir() - || this - .state - .expanded_dir_ids - .get(&details.worktree_id) - .is_some_and(|ids| ids.binary_search(&entry_id).is_ok()) - { - return; - } - - let bounds = event.bounds; - this.hover_expand_task = - Some(cx.spawn_in(window, async move |this, cx| { - cx.background_executor() - .timer(Duration::from_millis(500)) - .await; - this.update_in(cx, |this, window, cx| { - this.hover_expand_task.take(); - if this.drag_target_entry.as_ref().and_then(|entry| match entry { - DragTarget::Entry { entry_id: target_id, .. } => Some(*target_id), - DragTarget::Background { .. } => None, - }) == Some(entry_id) - && bounds.contains(&window.mouse_position()) - { - this.expand_entry(worktree_id, entry_id, cx); - this.update_visible_entries( - Some((worktree_id, entry_id)), - false, - false, - window, - cx, - ); - cx.notify(); - } - }) - .ok(); - })); - }, - )) - .on_drag( - dragged_selection, - { - let active_component = self.state.ancestors.get(&entry_id).and_then(|ancestors| ancestors.active_component(&details.filename)); - move |selection, click_offset, _window, cx| { - let filename = active_component.as_ref().unwrap_or_else(|| &details.filename); - cx.new(|_| DraggedProjectEntryView { - icon: details.icon.clone(), - filename: filename.clone(), - click_offset, - selection: selection.active_selection, - selections: selection.marked_selections.clone(), - }) - } - } - ) - .on_drop( - cx.listener(move |this, selections: &DraggedSelection, window, cx| { - this.drag_target_entry = None; - this.hover_scroll_task.take(); - this.hover_expand_task.take(); - if folded_directory_drag_target.is_some() { - return; - } - this.drag_onto(selections, entry_id, kind.is_file(), window, cx); - }), - )) - }) - .on_mouse_down( - MouseButton::Left, - cx.listener(move |this, _, _, cx| { - this.mouse_down = true; - cx.propagate(); - }), - ) - .on_click( - cx.listener(move |project_panel, event: &gpui::ClickEvent, window, cx| { - if event.is_right_click() || event.first_focus() - || show_editor - { - return; - } - if event.standard_click() { - project_panel.mouse_down = false; - } - cx.stop_propagation(); - - if let Some(selection) = project_panel.state.selection.filter(|_| event.modifiers().shift) { - let current_selection = project_panel.index_for_selection(selection); - let clicked_entry = SelectedEntry { - entry_id, - worktree_id, - }; - let target_selection = project_panel.index_for_selection(clicked_entry); - if let Some(((_, _, source_index), (_, _, target_index))) = - current_selection.zip(target_selection) - { - let range_start = source_index.min(target_index); - let range_end = source_index.max(target_index) + 1; - let mut new_selections = Vec::new(); - project_panel.for_each_visible_entry( - range_start..range_end, - window, - cx, - |entry_id, details, _, _| { - new_selections.push(SelectedEntry { - entry_id, - worktree_id: details.worktree_id, - }); - }, - ); - - for selection in &new_selections { - if !project_panel.marked_entries.contains(selection) { - project_panel.marked_entries.push(*selection); - } - } - - project_panel.state.selection = Some(clicked_entry); - if !project_panel.marked_entries.contains(&clicked_entry) { - project_panel.marked_entries.push(clicked_entry); - } - } - } else if event.modifiers().secondary() { - if event.click_count() > 1 { - project_panel.split_entry(entry_id, false, None, cx); - } else { - project_panel.state.selection = Some(selection); - if let Some(position) = project_panel.marked_entries.iter().position(|e| *e == selection) { - project_panel.marked_entries.remove(position); - } else { - project_panel.marked_entries.push(selection); - } - } - } else if kind.is_dir() { - project_panel.marked_entries.clear(); - if is_sticky - && let Some((_, _, index)) = project_panel.index_for_entry(entry_id, worktree_id) { - project_panel.scroll_handle.scroll_to_item_strict_with_offset(index, ScrollStrategy::Top, sticky_index.unwrap_or(0)); - cx.notify(); - // move down by 1px so that clicked item - // don't count as sticky anymore - cx.on_next_frame(window, |_, window, cx| { - cx.on_next_frame(window, |this, _, cx| { - let mut offset = this.scroll_handle.offset(); - offset.y += px(1.); - this.scroll_handle.set_offset(offset); - cx.notify(); - }); - }); - return; - } - if event.modifiers().alt { - project_panel.toggle_expand_all(entry_id, window, cx); - } else { - project_panel.toggle_expanded(entry_id, window, cx); - } - } else { - let preview_tabs_enabled = PreviewTabsSettings::get_global(cx).enable_preview_from_project_panel; - let click_count = event.click_count(); - let focus_opened_item = click_count > 1; - let allow_preview = preview_tabs_enabled && click_count == 1; - project_panel.open_entry(entry_id, focus_opened_item, allow_preview, cx); - } - }), - ) - .child( - ListItem::new(id) - .indent_level(depth) - .indent_step_size(px(settings.indent_size)) - .spacing(match settings.entry_spacing { - ProjectPanelEntrySpacing::Comfortable => ListItemSpacing::Dense, - ProjectPanelEntrySpacing::Standard => { - ListItemSpacing::ExtraDense - } - }) - .selectable(false) - .when_some(canonical_path, |this, path| { - this.end_slot::( - div() - .id("symlink_icon") - .pr_3() - .tooltip(move |_window, cx| { - Tooltip::with_meta( - path.to_string(), - None, - "Symbolic Link", - cx, - ) - }) - .child( - Icon::new(IconName::ArrowUpRight) - .size(IconSize::Indicator) - .color(filename_text_color), - ) - .into_any_element(), - ) - }) - .child(if let Some(icon) = &icon { - if let Some((_, decoration_color)) = - entry_diagnostic_aware_icon_decoration_and_color(diagnostic_severity) - { - let is_warning = diagnostic_severity - .map(|severity| matches!(severity, DiagnosticSeverity::WARNING)) - .unwrap_or(false); - div().child( - DecoratedIcon::new( - Icon::from_path(icon.clone()).color(Color::Muted), - Some( - IconDecoration::new( - if kind.is_file() { - if is_warning { - IconDecorationKind::Triangle - } else { - IconDecorationKind::X - } - } else { - IconDecorationKind::Dot - }, - bg_color, - cx, - ) - .group_name(Some(GROUP_NAME.into())) - .knockout_hover_color(bg_hover_color) - .color(decoration_color.color(cx)) - .position(Point { - x: px(-2.), - y: px(-2.), - }), - ), - ) - .into_any_element(), - ) - } else { - h_flex().child(Icon::from_path(icon.to_string()).color(Color::Muted)) - } - } else if let Some((icon_name, color)) = - entry_diagnostic_aware_icon_name_and_color(diagnostic_severity) - { - h_flex() - .size(IconSize::default().rems()) - .child(Icon::new(icon_name).color(color).size(IconSize::Small)) - } else { - h_flex() - .size(IconSize::default().rems()) - .invisible() - .flex_none() - }) - .child( - if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) { - h_flex().h_6().w_full().child(editor.clone()) - } else { - h_flex().h_6().map(|mut this| { - if let Some(folded_ancestors) = self.state.ancestors.get(&entry_id) { - let components = Path::new(&file_name) - .components() - .map(|comp| comp.as_os_str().to_string_lossy().into_owned()) - .collect::>(); - let active_index = folded_ancestors.active_index(); - let components_len = components.len(); - let delimiter = SharedString::new(path_style.primary_separator()); - for (index, component) in components.iter().enumerate() { - if index != 0 { - let delimiter_target_index = index - 1; - let target_entry_id = folded_ancestors.ancestors.get(components_len - 1 - delimiter_target_index).cloned(); - this = this.child( - 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; - this.folded_directory_drag_target = None; - if let Some(target_entry_id) = target_entry_id { - this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx); - } - })) - .on_drag_move(cx.listener( - move |this, event: &DragMoveEvent, _, _| { - if event.bounds.contains(&event.event.position) { - this.folded_directory_drag_target = Some( - FoldedDirectoryDragTarget { - entry_id, - index: delimiter_target_index, - is_delimiter_target: true, - } - ); - } else { - let is_current_target = this.folded_directory_drag_target - .is_some_and(|target| - target.entry_id == entry_id && - target.index == delimiter_target_index && - target.is_delimiter_target - ); - if is_current_target { - this.folded_directory_drag_target = None; - } - } - - }, - ))) - }) - .child( - Label::new(delimiter.clone()) - .single_line() - .color(filename_text_color) - ) - ); - } - let id = SharedString::from(format!( - "project_panel_path_component_{}_{index}", - entry_id.to_usize() - )); - let label = div() - .id(id) - .when(!is_sticky,| div| { - div - .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) { - this.folded_directory_drag_target = Some( - FoldedDirectoryDragTarget { - entry_id, - index, - is_delimiter_target: false, - } - ); - } else { - let is_current_target = this.folded_directory_drag_target - .as_ref() - .is_some_and(|target| - target.entry_id == entry_id && - target.index == index && - !target.is_delimiter_target - ); - if is_current_target { - this.folded_directory_drag_target = None; - } - } - }, - )) - .on_drop(cx.listener(move |this, selections: &DraggedSelection, window,cx| { - this.hover_scroll_task.take(); - this.drag_target_entry = None; - this.folded_directory_drag_target = None; - if let Some(target_entry_id) = target_entry_id { - this.drag_onto(selections, target_entry_id, kind.is_file(), window, cx); - } - })) - .when(folded_directory_drag_target.is_some_and(|target| - target.entry_id == entry_id && - target.index == index - ), |this| { - this.bg(item_colors.drag_over) - })) - }) - }) - .on_mouse_down( - MouseButton::Left, - cx.listener(move |this, _, _, cx| { - if index != active_index - && let Some(folds) = - this.state.ancestors.get_mut(&entry_id) - { - folds.current_ancestor_depth = - components_len - 1 - index; - cx.notify(); - } - }), - ) - .child( - Label::new(component) - .single_line() - .color(filename_text_color) - .when( - index == active_index - && (is_active || is_marked), - |this| this.underline(), - ), - ); - - this = this.child(label); - } - - this - } else { - this.child( - Label::new(file_name) - .single_line() - .color(filename_text_color), - ) - } - }) - }, - ) - .on_secondary_mouse_down(cx.listener( - move |this, event: &MouseDownEvent, window, cx| { - // Stop propagation to prevent the catch-all context menu for the project - // panel from being deployed. - cx.stop_propagation(); - // Some context menu actions apply to all marked entries. If the user - // right-clicks on an entry that is not marked, they may not realize the - // action applies to multiple entries. To avoid inadvertent changes, all - // entries are unmarked. - if !this.marked_entries.contains(&selection) { - this.marked_entries.clear(); - } - this.deploy_context_menu(event.position, entry_id, window, cx); - }, - )) - .overflow_x(), - ) - .when_some( - validation_color_and_message, - |this, (color, message)| { - this - .relative() - .child( - deferred( - div() - .occlude() - .absolute() - .top_full() - .left(px(-1.)) // Used px over rem so that it doesn't change with font size - .right(px(-0.5)) - .py_1() - .px_2() - .border_1() - .border_color(color) - .bg(cx.theme().colors().background) - .child( - Label::new(message) - .color(Color::from(color)) - .size(LabelSize::Small) - ) - ) - ) - } - ) - } - - fn details_for_entry( - &self, - entry: &Entry, - worktree_id: WorktreeId, - root_name: &RelPath, - entries_paths: &HashSet>, - git_status: GitSummary, - sticky: Option, - _window: &mut Window, - cx: &mut Context, - ) -> EntryDetails { - let (show_file_icons, show_folder_icons) = { - let settings = ProjectPanelSettings::get_global(cx); - (settings.file_icons, settings.folder_icons) - }; - - let expanded_entry_ids = self - .state - .expanded_dir_ids - .get(&worktree_id) - .map(Vec::as_slice) - .unwrap_or(&[]); - let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok(); - - let icon = match entry.kind { - EntryKind::File => { - if show_file_icons { - FileIcons::get_icon(entry.path.as_std_path(), cx) - } else { - None - } - } - _ => { - if show_folder_icons { - FileIcons::get_folder_icon(is_expanded, entry.path.as_std_path(), cx) - } else { - FileIcons::get_chevron_icon(is_expanded, cx) - } - } - }; - - let path_style = self.project.read(cx).path_style(cx); - let (depth, difference) = - ProjectPanel::calculate_depth_and_difference(entry, entries_paths); - - let filename = if difference > 1 { - entry - .path - .last_n_components(difference) - .map_or(String::new(), |suffix| { - suffix.display(path_style).to_string() - }) - } else { - entry - .path - .file_name() - .map(|name| name.to_string()) - .unwrap_or_else(|| root_name.as_unix_str().to_string()) - }; - - let selection = SelectedEntry { - worktree_id, - entry_id: entry.id, - }; - let is_marked = self.marked_entries.contains(&selection); - let is_selected = self.state.selection == Some(selection); - - let diagnostic_severity = self - .diagnostics - .get(&(worktree_id, entry.path.clone())) - .cloned(); - - let filename_text_color = - entry_git_aware_label_color(git_status, entry.is_ignored, is_marked); - - let is_cut = self - .clipboard - .as_ref() - .is_some_and(|e| e.is_cut() && e.items().contains(&selection)); - - EntryDetails { - filename, - icon, - path: entry.path.clone(), - depth, - kind: entry.kind, - is_ignored: entry.is_ignored, - is_expanded, - is_selected, - is_marked, - is_editing: false, - is_processing: false, - is_cut, - sticky, - filename_text_color, - diagnostic_severity, - git_status, - is_private: entry.is_private, - worktree_id, - canonical_path: entry.canonical_path.clone(), - } - } - - fn dispatch_context(&self, window: &Window, cx: &Context) -> KeyContext { - let mut dispatch_context = KeyContext::new_with_defaults(); - dispatch_context.add("ProjectPanel"); - dispatch_context.add("menu"); - - let identifier = if self.filename_editor.focus_handle(cx).is_focused(window) { - "editing" - } else { - "not_editing" - }; - - dispatch_context.add(identifier); - dispatch_context - } - - fn reveal_entry( - &mut self, - project: Entity, - entry_id: ProjectEntryId, - skip_ignored: bool, - window: &mut Window, - cx: &mut Context, - ) -> Result<()> { - let worktree = project - .read(cx) - .worktree_for_entry(entry_id, cx) - .context("can't reveal a non-existent entry in the project panel")?; - let worktree = worktree.read(cx); - if skip_ignored - && worktree - .entry_for_id(entry_id) - .is_none_or(|entry| entry.is_ignored && !entry.is_always_included) - { - anyhow::bail!("can't reveal an ignored entry in the project panel"); - } - let is_active_item_file_diff_view = self - .workspace - .upgrade() - .and_then(|ws| ws.read(cx).active_item(cx)) - .map(|item| item.act_as_type(TypeId::of::(), cx).is_some()) - .unwrap_or(false); - if is_active_item_file_diff_view { - return Ok(()); - } - - let worktree_id = worktree.id(); - self.expand_entry(worktree_id, entry_id, cx); - self.update_visible_entries(Some((worktree_id, entry_id)), false, true, window, cx); - self.marked_entries.clear(); - self.marked_entries.push(SelectedEntry { - worktree_id, - entry_id, - }); - cx.notify(); - Ok(()) - } - - fn find_active_indent_guide( - &self, - indent_guides: &[IndentGuideLayout], - cx: &App, - ) -> Option { - let (worktree, entry) = self.selected_entry(cx)?; - - // Find the parent entry of the indent guide, this will either be the - // expanded folder we have selected, or the parent of the currently - // selected file/collapsed directory - let mut entry = entry; - loop { - let is_expanded_dir = entry.is_dir() - && self - .state - .expanded_dir_ids - .get(&worktree.id()) - .map(|ids| ids.binary_search(&entry.id).is_ok()) - .unwrap_or(false); - if is_expanded_dir { - break; - } - entry = worktree.entry_for_path(&entry.path.parent()?)?; - } - - let (active_indent_range, depth) = { - let (worktree_ix, child_offset, ix) = self.index_for_entry(entry.id, worktree.id())?; - let child_paths = &self.state.visible_entries[worktree_ix].entries; - let mut child_count = 0; - let depth = entry.path.ancestors().count(); - while let Some(entry) = child_paths.get(child_offset + child_count + 1) { - if entry.path.ancestors().count() <= depth { - break; - } - child_count += 1; - } - - let start = ix + 1; - let end = start + child_count; - - let visible_worktree = &self.state.visible_entries[worktree_ix]; - let visible_worktree_entries = visible_worktree.index.get_or_init(|| { - visible_worktree - .entries - .iter() - .map(|e| e.path.clone()) - .collect() - }); - - // Calculate the actual depth of the entry, taking into account that directories can be auto-folded. - let (depth, _) = Self::calculate_depth_and_difference(entry, visible_worktree_entries); - (start..end, depth) - }; - - let candidates = indent_guides - .iter() - .enumerate() - .filter(|(_, indent_guide)| indent_guide.offset.x == depth); - - for (i, indent) in candidates { - // Find matches that are either an exact match, partially on screen, or inside the enclosing indent - if active_indent_range.start <= indent.offset.y + indent.length - && indent.offset.y <= active_indent_range.end - { - return Some(i); - } - } - None - } - - fn render_sticky_entries( - &self, - child: StickyProjectPanelCandidate, - window: &mut Window, - cx: &mut Context, - ) -> SmallVec<[AnyElement; 8]> { - let project = self.project.read(cx); - - let Some((worktree_id, entry_ref)) = self.entry_at_index(child.index) else { - return SmallVec::new(); - }; - - let Some(visible) = self - .state - .visible_entries - .iter() - .find(|worktree| worktree.worktree_id == worktree_id) - else { - return SmallVec::new(); - }; - - let Some(worktree) = project.worktree_for_id(worktree_id, cx) else { - return SmallVec::new(); - }; - let worktree = worktree.read(cx).snapshot(); - - let paths = visible - .index - .get_or_init(|| visible.entries.iter().map(|e| e.path.clone()).collect()); - - let mut sticky_parents = Vec::new(); - let mut current_path = entry_ref.path.clone(); - - 'outer: loop { - if let Some(parent_path) = current_path.parent() { - for ancestor_path in parent_path.ancestors() { - if paths.contains(ancestor_path) - && let Some(parent_entry) = worktree.entry_for_path(ancestor_path) - { - sticky_parents.push(parent_entry.clone()); - current_path = parent_entry.path.clone(); - continue 'outer; - } - } - } - break 'outer; - } - - if sticky_parents.is_empty() { - return SmallVec::new(); - } - - sticky_parents.reverse(); - - let panel_settings = ProjectPanelSettings::get_global(cx); - let git_status_enabled = panel_settings.git_status; - let root_name = worktree.root_name(); - - let git_summaries_by_id = if git_status_enabled { - visible - .entries - .iter() - .map(|e| (e.id, e.git_summary)) - .collect::>() - } else { - Default::default() - }; - - // already checked if non empty above - let last_item_index = sticky_parents.len() - 1; - sticky_parents - .iter() - .enumerate() - .map(|(index, entry)| { - let git_status = git_summaries_by_id - .get(&entry.id) - .copied() - .unwrap_or_default(); - let sticky_details = Some(StickyDetails { - sticky_index: index, - }); - let details = self.details_for_entry( - entry, - worktree_id, - root_name, - paths, - git_status, - sticky_details, - window, - cx, - ); - self.render_entry(entry.id, details, window, cx) - .when(index == last_item_index, |this| { - let shadow_color_top = hsla(0.0, 0.0, 0.0, 0.1); - let shadow_color_bottom = hsla(0.0, 0.0, 0.0, 0.); - let sticky_shadow = div() - .absolute() - .left_0() - .bottom_neg_1p5() - .h_1p5() - .w_full() - .bg(linear_gradient( - 0., - linear_color_stop(shadow_color_top, 1.), - linear_color_stop(shadow_color_bottom, 0.), - )); - this.child(sticky_shadow) - }) - .into_any() - }) - .collect() - } -} - -#[derive(Clone)] -struct StickyProjectPanelCandidate { - index: usize, - depth: usize, -} - -impl StickyCandidate for StickyProjectPanelCandidate { - fn depth(&self) -> usize { - self.depth - } -} - -fn item_width_estimate(depth: usize, item_text_chars: usize, is_symlink: bool) -> usize { - const ICON_SIZE_FACTOR: usize = 2; - let mut item_width = depth * ICON_SIZE_FACTOR + item_text_chars; - if is_symlink { - item_width += ICON_SIZE_FACTOR; - } - item_width -} - -impl Render for ProjectPanel { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let has_worktree = !self.state.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 show_sticky_entries = { - if panel_settings.sticky_scroll { - let is_scrollable = self.scroll_handle.is_scrollable(); - let is_scrolled = self.scroll_handle.offset().y < px(0.); - is_scrollable && is_scrolled - } else { - false - } - }; - - let is_local = project.is_local(); - - if has_worktree { - let item_count = self - .state - .visible_entries - .iter() - .map(|worktree| worktree.entries.len()) - .sum(); - - fn handle_drag_move( - this: &mut ProjectPanel, - e: &DragMoveEvent, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(previous_position) = this.previous_drag_position { - // Refresh cursor only when an actual drag happens, - // because modifiers are not updated when the cursor is not moved. - if e.event.position != previous_position { - this.refresh_drag_cursor_style(&e.event.modifiers, window, cx); - } - } - this.previous_drag_position = Some(e.event.position); - - if !e.bounds.contains(&e.event.position) { - this.drag_target_entry = None; - return; - } - this.hover_scroll_task.take(); - let panel_height = e.bounds.size.height; - if panel_height <= px(0.) { - return; - } - - let event_offset = e.event.position.y - e.bounds.origin.y; - // How far along in the project panel is our cursor? (0. is the top of a list, 1. is the bottom) - let hovered_region_offset = event_offset / panel_height; - - // We want the scrolling to be a bit faster when the cursor is closer to the edge of a list. - // These pixels offsets were picked arbitrarily. - let vertical_scroll_offset = if hovered_region_offset <= 0.05 { - 8. - } else if hovered_region_offset <= 0.15 { - 5. - } else if hovered_region_offset >= 0.95 { - -8. - } else if hovered_region_offset >= 0.85 { - -5. - } else { - return; - }; - let adjustment = point(px(0.), px(vertical_scroll_offset)); - this.hover_scroll_task = Some(cx.spawn_in(window, async move |this, cx| { - loop { - let should_stop_scrolling = this - .update(cx, |this, cx| { - this.hover_scroll_task.as_ref()?; - let handle = this.scroll_handle.0.borrow_mut(); - let offset = handle.base_handle.offset(); - - handle.base_handle.set_offset(offset + adjustment); - cx.notify(); - Some(()) - }) - .ok() - .flatten() - .is_some(); - if should_stop_scrolling { - return; - } - cx.background_executor() - .timer(Duration::from_millis(16)) - .await; - } - })); - } - 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::)) - }) - .size_full() - .relative() - .on_modifiers_changed(cx.listener( - |this, event: &ModifiersChangedEvent, window, cx| { - this.refresh_drag_cursor_style(&event.modifiers, window, cx); - }, - )) - .key_context(self.dispatch_context(window, cx)) - .on_action(cx.listener(Self::scroll_up)) - .on_action(cx.listener(Self::scroll_down)) - .on_action(cx.listener(Self::scroll_cursor_center)) - .on_action(cx.listener(Self::scroll_cursor_top)) - .on_action(cx.listener(Self::scroll_cursor_bottom)) - .on_action(cx.listener(Self::select_next)) - .on_action(cx.listener(Self::select_previous)) - .on_action(cx.listener(Self::select_first)) - .on_action(cx.listener(Self::select_last)) - .on_action(cx.listener(Self::select_parent)) - .on_action(cx.listener(Self::select_next_git_entry)) - .on_action(cx.listener(Self::select_prev_git_entry)) - .on_action(cx.listener(Self::select_next_diagnostic)) - .on_action(cx.listener(Self::select_prev_diagnostic)) - .on_action(cx.listener(Self::select_next_directory)) - .on_action(cx.listener(Self::select_prev_directory)) - .on_action(cx.listener(Self::expand_selected_entry)) - .on_action(cx.listener(Self::collapse_selected_entry)) - .on_action(cx.listener(Self::collapse_all_entries)) - .on_action(cx.listener(Self::open)) - .on_action(cx.listener(Self::open_permanent)) - .on_action(cx.listener(Self::open_split_vertical)) - .on_action(cx.listener(Self::open_split_horizontal)) - .on_action(cx.listener(Self::confirm)) - .on_action(cx.listener(Self::cancel)) - .on_action(cx.listener(Self::copy_path)) - .on_action(cx.listener(Self::copy_relative_path)) - .on_action(cx.listener(Self::new_search_in_directory)) - .on_action(cx.listener(Self::unfold_directory)) - .on_action(cx.listener(Self::fold_directory)) - .on_action(cx.listener(Self::remove_from_project)) - .on_action(cx.listener(Self::compare_marked_files)) - .when(!project.is_read_only(cx), |el| { - el.on_action(cx.listener(Self::new_file)) - .on_action(cx.listener(Self::new_directory)) - .on_action(cx.listener(Self::rename)) - .on_action(cx.listener(Self::delete)) - .on_action(cx.listener(Self::cut)) - .on_action(cx.listener(Self::copy)) - .on_action(cx.listener(Self::paste)) - .on_action(cx.listener(Self::duplicate)) - .when(!project.is_remote(), |el| { - el.on_action(cx.listener(Self::trash)) - }) - }) - .when(project.is_local(), |el| { - el.on_action(cx.listener(Self::reveal_in_finder)) - .on_action(cx.listener(Self::open_system)) - .on_action(cx.listener(Self::open_in_terminal)) - }) - .when(project.is_via_remote_server(), |el| { - el.on_action(cx.listener(Self::open_in_terminal)) - }) - .track_focus(&self.focus_handle(cx)) - .child( - v_flex() - .child( - uniform_list("entries", item_count, { - cx.processor(|this, range: Range, window, cx| { - this.rendered_entries_len = range.end - range.start; - let mut items = Vec::with_capacity(this.rendered_entries_len); - this.for_each_visible_entry( - range, - window, - cx, - |id, details, window, cx| { - items.push(this.render_entry(id, details, window, cx)); - }, - ); - items - }) - }) - .when(show_indent_guides, |list| { - list.with_decoration( - ui::indent_guides( - px(indent_size), - IndentGuideColors::panel(cx), - ) - .with_compute_indents_fn( - cx.entity(), - |this, range, window, cx| { - let mut items = - SmallVec::with_capacity(range.end - range.start); - this.iter_visible_entries( - range, - window, - cx, - |entry, _, entries, _, _| { - let (depth, _) = - Self::calculate_depth_and_difference( - entry, entries, - ); - items.push(depth); - }, - ); - items - }, - ) - .on_click(cx.listener( - |this, - active_indent_guide: &IndentGuideLayout, - window, - cx| { - if window.modifiers().secondary() { - let ix = active_indent_guide.offset.y; - let Some((target_entry, worktree)) = maybe!({ - let (worktree_id, entry) = - this.entry_at_index(ix)?; - let worktree = this - .project - .read(cx) - .worktree_for_id(worktree_id, cx)?; - let target_entry = worktree - .read(cx) - .entry_for_path(&entry.path.parent()?)?; - Some((target_entry, worktree)) - }) else { - return; - }; - - this.collapse_entry( - target_entry.clone(), - worktree, - window, - cx, - ); - } - }, - )) - .with_render_fn( - cx.entity(), - move |this, params, _, cx| { - const LEFT_OFFSET: Pixels = px(14.); - const PADDING_Y: Pixels = px(4.); - const HITBOX_OVERDRAW: Pixels = px(3.); - - let active_indent_guide_index = this - .find_active_indent_guide( - ¶ms.indent_guides, - cx, - ); - - let indent_size = params.indent_size; - let item_height = params.item_height; - - params - .indent_guides - .into_iter() - .enumerate() - .map(|(idx, layout)| { - let offset = if layout.continues_offscreen { - px(0.) - } else { - PADDING_Y - }; - let bounds = Bounds::new( - point( - layout.offset.x * indent_size - + LEFT_OFFSET, - layout.offset.y * item_height + offset, - ), - size( - px(1.), - layout.length * item_height - - offset * 2., - ), - ); - ui::RenderedIndentGuide { - bounds, - layout, - is_active: Some(idx) - == active_indent_guide_index, - hitbox: Some(Bounds::new( - point( - bounds.origin.x - HITBOX_OVERDRAW, - bounds.origin.y, - ), - size( - bounds.size.width - + HITBOX_OVERDRAW * 2., - bounds.size.height, - ), - )), - } - }) - .collect() - }, - ), - ) - }) - .when(show_sticky_entries, |list| { - let sticky_items = ui::sticky_items( - cx.entity(), - |this, range, window, cx| { - let mut items = - SmallVec::with_capacity(range.end - range.start); - this.iter_visible_entries( - range, - window, - cx, - |entry, index, entries, _, _| { - let (depth, _) = - Self::calculate_depth_and_difference( - entry, entries, - ); - let candidate = - StickyProjectPanelCandidate { index, depth }; - items.push(candidate); - }, - ); - items - }, - |this, marker_entry, window, cx| { - let sticky_entries = - this.render_sticky_entries(marker_entry, window, cx); - this.sticky_items_count = sticky_entries.len(); - sticky_entries - }, - ); - list.with_decoration(if show_indent_guides { - sticky_items.with_decoration( - ui::indent_guides( - px(indent_size), - IndentGuideColors::panel(cx), - ) - .with_render_fn( - cx.entity(), - move |_, params, _, _| { - const LEFT_OFFSET: Pixels = px(14.); - - let indent_size = params.indent_size; - let item_height = params.item_height; - - params - .indent_guides - .into_iter() - .map(|layout| { - let bounds = Bounds::new( - point( - layout.offset.x * indent_size - + LEFT_OFFSET, - layout.offset.y * item_height, - ), - size( - px(1.), - layout.length * item_height, - ), - ); - ui::RenderedIndentGuide { - bounds, - layout, - is_active: false, - hitbox: None, - } - }) - .collect() - }, - ), - ) - } else { - sticky_items - }) - }) - .with_sizing_behavior(ListSizingBehavior::Infer) - .with_horizontal_sizing_behavior( - ListHorizontalSizingBehavior::Unconstrained, - ) - .with_width_from_item(self.state.max_width_item_index) - .track_scroll(&self.scroll_handle), - ) - .child( - div() - .id("project-panel-blank-area") - .block_mouse_except_scroll() - .flex_grow() - .when( - self.drag_target_entry.as_ref().is_some_and( - |entry| match entry { - DragTarget::Background => true, - DragTarget::Entry { - highlight_entry_id, .. - } => self.state.last_worktree_root_id.is_some_and( - |root_id| *highlight_entry_id == root_id, - ), - }, - ), - |div| div.bg(cx.theme().colors().drop_target_background), - ) - .on_drag_move::(cx.listener( - move |this, event: &DragMoveEvent, _, _| { - let Some(_last_root_id) = this.state.last_worktree_root_id - else { - return; - }; - if event.bounds.contains(&event.event.position) { - this.drag_target_entry = Some(DragTarget::Background); - } else { - if this.drag_target_entry.as_ref().is_some_and(|e| { - matches!(e, DragTarget::Background) - }) { - this.drag_target_entry = None; - } - } - }, - )) - .on_drag_move::(cx.listener( - move |this, event: &DragMoveEvent, _, cx| { - let Some(last_root_id) = this.state.last_worktree_root_id - else { - return; - }; - if event.bounds.contains(&event.event.position) { - let drag_state = event.drag(cx); - if this.should_highlight_background_for_selection_drag( - &drag_state, - last_root_id, - cx, - ) { - this.drag_target_entry = - Some(DragTarget::Background); - } - } else { - if this.drag_target_entry.as_ref().is_some_and(|e| { - matches!(e, DragTarget::Background) - }) { - this.drag_target_entry = None; - } - } - }, - )) - .on_drop(cx.listener( - move |this, external_paths: &ExternalPaths, window, cx| { - this.drag_target_entry = None; - this.hover_scroll_task.take(); - if let Some(entry_id) = this.state.last_worktree_root_id { - this.drop_external_files( - external_paths.paths(), - entry_id, - window, - cx, - ); - } - cx.stop_propagation(); - }, - )) - .on_drop(cx.listener( - move |this, selections: &DraggedSelection, window, cx| { - this.drag_target_entry = None; - this.hover_scroll_task.take(); - if let Some(entry_id) = this.state.last_worktree_root_id { - this.drag_onto(selections, entry_id, false, window, cx); - } - cx.stop_propagation(); - }, - )) - .on_click(cx.listener(|this, event, window, cx| { - if matches!(event, gpui::ClickEvent::Keyboard(_)) { - return; - } - cx.stop_propagation(); - this.state.selection = None; - this.marked_entries.clear(); - this.focus_handle(cx).focus(window); - })) - .on_mouse_down( - MouseButton::Right, - cx.listener(move |this, event: &MouseDownEvent, window, cx| { - // When deploying the context menu anywhere below the last project entry, - // act as if the user clicked the root of the last worktree. - if let Some(entry_id) = this.state.last_worktree_root_id { - this.deploy_context_menu( - event.position, - entry_id, - window, - cx, - ); - } - }), - ) - .when(!project.is_read_only(cx), |el| { - el.on_click(cx.listener( - |this, event: &gpui::ClickEvent, window, cx| { - if event.click_count() > 1 - && let Some(entry_id) = - this.state.last_worktree_root_id - { - let project = this.project.read(cx); - - let worktree_id = if let Some(worktree) = - project.worktree_for_entry(entry_id, cx) - { - worktree.read(cx).id() - } else { - return; - }; - - this.state.selection = Some(SelectedEntry { - worktree_id, - entry_id, - }); - - this.new_file(&NewFile, window, cx); - } - }, - )) - }), - ) - .size_full(), - ) - .custom_scrollbars( - Scrollbars::for_settings::() - .tracked_scroll_handle(&self.scroll_handle) - .with_track_along( - ScrollAxes::Horizontal, - cx.theme().colors().panel_background, - ) - .notify_content(), - window, - cx, - ) - .children(self.context_menu.as_ref().map(|(menu, position, _)| { - deferred( - anchored() - .position(*position) - .anchor(gpui::Corner::TopLeft) - .child(menu.clone()), - ) - .with_priority(3) - })) - } else { - let focus_handle = self.focus_handle(cx); - - v_flex() - .id("empty-project_panel") - .p_4() - .size_full() - .items_center() - .justify_center() - .gap_1() - .track_focus(&self.focus_handle(cx)) - .child( - Button::new("open_project", "Open Project") - .full_width() - .key_binding(KeyBinding::for_action_in( - &workspace::Open, - &focus_handle, - cx, - )) - .on_click(cx.listener(|this, _, window, cx| { - this.workspace - .update(cx, |_, cx| { - window.dispatch_action(workspace::Open.boxed_clone(), cx); - }) - .log_err(); - })), - ) - .child( - h_flex() - .w_1_2() - .gap_2() - .child(Divider::horizontal()) - .child(Label::new("or").size(LabelSize::XSmall).color(Color::Muted)) - .child(Divider::horizontal()), - ) - .child( - Button::new("clone_repo", "Clone Repository") - .full_width() - .on_click(cx.listener(|this, _, window, cx| { - this.workspace - .update(cx, |_, cx| { - window.dispatch_action(git::Clone.boxed_clone(), cx); - }) - .log_err(); - })), - ) - .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(); - }, - )) - }) - }) - } - } -} - -impl Render for DraggedProjectEntryView { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let ui_font = ThemeSettings::get_global(cx).ui_font.clone(); - h_flex() - .font(ui_font) - .pl(self.click_offset.x + px(12.)) - .pt(self.click_offset.y + px(12.)) - .child( - div() - .flex() - .gap_1() - .items_center() - .py_1() - .px_2() - .rounded_lg() - .bg(cx.theme().colors().background) - .map(|this| { - if self.selections.len() > 1 && self.selections.contains(&self.selection) { - this.child(Label::new(format!("{} entries", self.selections.len()))) - } else { - this.child(if let Some(icon) = &self.icon { - div().child(Icon::from_path(icon.clone())) - } else { - div() - }) - .child(Label::new(self.filename.clone())) - } - }), - ) - } -} - -impl EventEmitter for ProjectPanel {} - -impl EventEmitter for ProjectPanel {} - -impl Panel for ProjectPanel { - fn position(&self, _: &Window, cx: &App) -> DockPosition { - match ProjectPanelSettings::get_global(cx).dock { - DockSide::Left => DockPosition::Left, - DockSide::Right => DockPosition::Right, - } - } - - fn position_is_valid(&self, position: DockPosition) -> bool { - matches!(position, DockPosition::Left | DockPosition::Right) - } - - fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context) { - settings::update_settings_file(self.fs.clone(), cx, move |settings, _| { - let dock = match position { - DockPosition::Left | DockPosition::Bottom => DockSide::Left, - DockPosition::Right => DockSide::Right, - }; - settings.project_panel.get_or_insert_default().dock = Some(dock); - }); - } - - fn size(&self, _: &Window, cx: &App) -> Pixels { - self.width - .unwrap_or_else(|| ProjectPanelSettings::get_global(cx).default_width) - } - - fn set_size(&mut self, size: Option, window: &mut Window, cx: &mut Context) { - self.width = size; - cx.notify(); - cx.defer_in(window, |this, _, cx| { - this.serialize(cx); - }); - } - - fn icon(&self, _: &Window, cx: &App) -> Option { - ProjectPanelSettings::get_global(cx) - .button - .then_some(IconName::FileTree) - } - - fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> { - Some("Project Panel") - } - - fn toggle_action(&self) -> Box { - Box::new(ToggleFocus) - } - - fn persistent_name() -> &'static str { - "Project Panel" - } - - fn panel_key() -> &'static str { - PROJECT_PANEL_KEY - } - - fn starts_open(&self, _: &Window, cx: &App) -> bool { - if !ProjectPanelSettings::get_global(cx).starts_open { - return false; - } - - let project = &self.project.read(cx); - project.visible_worktrees(cx).any(|tree| { - tree.read(cx) - .root_entry() - .is_some_and(|entry| entry.is_dir()) - }) - } - - fn activation_priority(&self) -> u32 { - 0 - } -} - -impl Focusable for ProjectPanel { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl ClipboardEntry { - fn is_cut(&self) -> bool { - matches!(self, Self::Cut { .. }) - } - - fn items(&self) -> &BTreeSet { - match self { - ClipboardEntry::Copied(entries) | ClipboardEntry::Cut(entries) => entries, - } - } - - fn into_copy_entry(self) -> Self { - match self { - ClipboardEntry::Copied(_) => self, - ClipboardEntry::Cut(entries) => ClipboardEntry::Copied(entries), - } - } -} - -#[inline] -fn cmp_directories_first(a: &Entry, b: &Entry) -> cmp::Ordering { - util::paths::compare_rel_paths((&a.path, a.is_file()), (&b.path, b.is_file())) -} - -#[inline] -fn cmp_mixed(a: &Entry, b: &Entry) -> cmp::Ordering { - util::paths::compare_rel_paths_mixed((&a.path, a.is_file()), (&b.path, b.is_file())) -} - -#[inline] -fn cmp_files_first(a: &Entry, b: &Entry) -> cmp::Ordering { - util::paths::compare_rel_paths_files_first((&a.path, a.is_file()), (&b.path, b.is_file())) -} - -#[inline] -fn cmp_with_mode(a: &Entry, b: &Entry, mode: &settings::ProjectPanelSortMode) -> cmp::Ordering { - match mode { - settings::ProjectPanelSortMode::DirectoriesFirst => cmp_directories_first(a, b), - settings::ProjectPanelSortMode::Mixed => cmp_mixed(a, b), - settings::ProjectPanelSortMode::FilesFirst => cmp_files_first(a, b), - } -} - -pub fn sort_worktree_entries_with_mode( - entries: &mut [impl AsRef], - mode: settings::ProjectPanelSortMode, -) { - entries.sort_by(|lhs, rhs| cmp_with_mode(lhs.as_ref(), rhs.as_ref(), &mode)); -} - -pub fn par_sort_worktree_entries_with_mode( - entries: &mut Vec, - mode: settings::ProjectPanelSortMode, -) { - entries.par_sort_by(|lhs, rhs| cmp_with_mode(lhs, rhs, &mode)); -} - -#[cfg(test)] -mod project_panel_tests; diff --git a/crates/project_panel/src/project_panel_settings.rs b/crates/project_panel/src/project_panel_settings.rs deleted file mode 100644 index b031627034..0000000000 --- a/crates/project_panel/src/project_panel_settings.rs +++ /dev/null @@ -1,124 +0,0 @@ -use editor::EditorSettings; -use gpui::Pixels; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{ - DockSide, ProjectPanelEntrySpacing, ProjectPanelSortMode, RegisterSetting, Settings, - ShowDiagnostics, ShowIndentGuides, -}; -use ui::{ - px, - scrollbars::{ScrollbarVisibility, ShowScrollbar}, -}; - -#[derive(Deserialize, Debug, Clone, Copy, PartialEq, RegisterSetting)] -pub struct ProjectPanelSettings { - pub button: bool, - pub hide_gitignore: bool, - pub default_width: Pixels, - pub dock: DockSide, - pub entry_spacing: ProjectPanelEntrySpacing, - pub file_icons: bool, - pub folder_icons: bool, - pub git_status: bool, - pub indent_size: f32, - pub indent_guides: IndentGuidesSettings, - pub sticky_scroll: bool, - pub auto_reveal_entries: bool, - pub auto_fold_dirs: bool, - pub starts_open: bool, - pub scrollbar: ScrollbarSettings, - pub show_diagnostics: ShowDiagnostics, - pub hide_root: bool, - pub hide_hidden: bool, - pub drag_and_drop: bool, - pub auto_open: AutoOpenSettings, - pub sort_mode: ProjectPanelSortMode, -} - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -pub struct IndentGuidesSettings { - pub show: ShowIndentGuides, -} - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -pub struct ScrollbarSettings { - /// When to show the scrollbar in the project panel. - /// - /// Default: inherits editor scrollbar settings - pub show: Option, -} - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -pub struct AutoOpenSettings { - pub on_create: bool, - pub on_paste: bool, - pub on_drop: bool, -} - -impl AutoOpenSettings { - #[inline] - pub fn should_open_on_create(self) -> bool { - self.on_create - } - - #[inline] - pub fn should_open_on_paste(self) -> bool { - self.on_paste - } - - #[inline] - pub fn should_open_on_drop(self) -> bool { - self.on_drop - } -} - -impl ScrollbarVisibility for ProjectPanelSettings { - fn visibility(&self, cx: &ui::App) -> ShowScrollbar { - self.scrollbar - .show - .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show) - } -} - -impl Settings for ProjectPanelSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - let project_panel = content.project_panel.clone().unwrap(); - Self { - button: project_panel.button.unwrap(), - hide_gitignore: project_panel.hide_gitignore.unwrap(), - default_width: px(project_panel.default_width.unwrap()), - dock: project_panel.dock.unwrap(), - entry_spacing: project_panel.entry_spacing.unwrap(), - file_icons: project_panel.file_icons.unwrap(), - folder_icons: project_panel.folder_icons.unwrap(), - git_status: project_panel.git_status.unwrap(), - indent_size: project_panel.indent_size.unwrap(), - indent_guides: IndentGuidesSettings { - show: project_panel.indent_guides.unwrap().show.unwrap(), - }, - sticky_scroll: project_panel.sticky_scroll.unwrap(), - auto_reveal_entries: project_panel.auto_reveal_entries.unwrap(), - auto_fold_dirs: project_panel.auto_fold_dirs.unwrap(), - starts_open: project_panel.starts_open.unwrap(), - scrollbar: ScrollbarSettings { - show: project_panel.scrollbar.unwrap().show.map(Into::into), - }, - show_diagnostics: project_panel.show_diagnostics.unwrap(), - hide_root: project_panel.hide_root.unwrap(), - hide_hidden: project_panel.hide_hidden.unwrap(), - drag_and_drop: project_panel.drag_and_drop.unwrap(), - auto_open: { - let auto_open = project_panel.auto_open.unwrap(); - AutoOpenSettings { - on_create: auto_open.on_create.unwrap(), - on_paste: auto_open.on_paste.unwrap(), - on_drop: auto_open.on_drop.unwrap(), - } - }, - sort_mode: project_panel - .sort_mode - .unwrap_or(ProjectPanelSortMode::DirectoriesFirst), - } - } -} diff --git a/crates/project_panel/src/project_panel_tests.rs b/crates/project_panel/src/project_panel_tests.rs deleted file mode 100644 index 3f54e01927..0000000000 --- a/crates/project_panel/src/project_panel_tests.rs +++ /dev/null @@ -1,8195 +0,0 @@ -use super::*; -use collections::HashSet; -use editor::MultiBufferOffset; -use gpui::{Empty, Entity, TestAppContext, VisualTestContext, WindowHandle}; -use pretty_assertions::assert_eq; -use project::FakeFs; -use serde_json::json; -use settings::{ProjectPanelAutoOpenSettings, SettingsStore}; -use std::path::{Path, PathBuf}; -use util::{path, paths::PathStyle, rel_path::rel_path}; -use workspace::{ - AppState, ItemHandle, Pane, - item::{Item, ProjectItem}, - register_project_item, -}; - -#[gpui::test] -async fn test_visible_list(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root1", - json!({ - ".dockerignore": "", - ".git": { - "HEAD": "", - }, - "a": { - "0": { "q": "", "r": "", "s": "" }, - "1": { "t": "", "u": "" }, - "2": { "v": "", "w": "", "x": "", "y": "" }, - }, - "b": { - "3": { "Q": "" }, - "4": { "R": "", "S": "", "T": "", "U": "" }, - }, - "C": { - "5": {}, - "6": { "V": "", "W": "" }, - "7": { "X": "" }, - "8": { "Y": {}, "Z": "" } - } - }), - ) - .await; - fs.insert_tree( - "/root2", - json!({ - "d": { - "9": "" - }, - "e": {} - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - "v root1", - " > .git", - " > a", - " > b", - " > C", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); - - toggle_expand_dir(&panel, "root1/b", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - "v root1", - " > .git", - " > a", - " v b <== selected", - " > 3", - " > 4", - " > C", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); - - assert_eq!( - visible_entries_as_strings(&panel, 6..9, cx), - &[ - // - " > C", - " .dockerignore", - "v root2", - ] - ); -} - -#[gpui::test] -async fn test_opening_file(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/src"), - json!({ - "test": { - "first.rs": "// First Rust file", - "second.rs": "// Second Rust file", - "third.rs": "// Third Rust file", - } - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/src").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "src/test", cx); - select_path(&panel, "src/test/first.rs", cx); - panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " first.rs <== selected <== marked", - " second.rs", - " third.rs" - ] - ); - ensure_single_file_is_opened(&workspace, "test/first.rs", cx); - - select_path(&panel, "src/test/second.rs", cx); - panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " first.rs", - " second.rs <== selected <== marked", - " third.rs" - ] - ); - ensure_single_file_is_opened(&workspace, "test/second.rs", cx); -} - -#[gpui::test] -async fn test_exclusions_in_visible_list(cx: &mut gpui::TestAppContext) { - init_test(cx); - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = - Some(vec!["**/.git".to_string(), "**/4/**".to_string()]); - }); - }); - }); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/root1", - json!({ - ".dockerignore": "", - ".git": { - "HEAD": "", - }, - "a": { - "0": { "q": "", "r": "", "s": "" }, - "1": { "t": "", "u": "" }, - "2": { "v": "", "w": "", "x": "", "y": "" }, - }, - "b": { - "3": { "Q": "" }, - "4": { "R": "", "S": "", "T": "", "U": "" }, - }, - "C": { - "5": {}, - "6": { "V": "", "W": "" }, - "7": { "X": "" }, - "8": { "Y": {}, "Z": "" } - } - }), - ) - .await; - fs.insert_tree( - "/root2", - json!({ - "d": { - "4": "" - }, - "e": {} - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - "v root1", - " > a", - " > b", - " > C", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); - - toggle_expand_dir(&panel, "root1/b", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - "v root1", - " > a", - " v b <== selected", - " > 3", - " > C", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); - - toggle_expand_dir(&panel, "root2/d", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - "v root1", - " > a", - " v b", - " > 3", - " > C", - " .dockerignore", - "v root2", - " v d <== selected", - " > e", - ] - ); - - toggle_expand_dir(&panel, "root2/e", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - "v root1", - " > a", - " v b", - " > 3", - " > C", - " .dockerignore", - "v root2", - " v d", - " v e <== selected", - ] - ); -} - -#[gpui::test] -async fn test_auto_collapse_dir_paths(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root1"), - json!({ - "dir_1": { - "nested_dir_1": { - "nested_dir_2": { - "nested_dir_3": { - "file_a.java": "// File contents", - "file_b.java": "// File contents", - "file_c.java": "// File contents", - "nested_dir_4": { - "nested_dir_5": { - "file_d.java": "// File contents", - } - } - } - } - } - } - }), - ) - .await; - fs.insert_tree( - path!("/root2"), - json!({ - "dir_2": { - "file_1.java": "// File contents", - } - }), - ) - .await; - - // Test 1: Multiple worktrees with auto_fold_dirs = true - let project = Project::test( - fs.clone(), - [path!("/root1").as_ref(), path!("/root2").as_ref()], - cx, - ) - .await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - auto_fold_dirs: true, - sort_mode: settings::ProjectPanelSortMode::DirectoriesFirst, - ..settings - }, - cx, - ); - }); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > dir_1/nested_dir_1/nested_dir_2/nested_dir_3", - "v root2", - " > dir_2", - ] - ); - - toggle_expand_dir( - &panel, - "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3", - cx, - ); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " v dir_1/nested_dir_1/nested_dir_2/nested_dir_3 <== selected", - " > nested_dir_4/nested_dir_5", - " file_a.java", - " file_b.java", - " file_c.java", - "v root2", - " > dir_2", - ] - ); - - toggle_expand_dir( - &panel, - "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3/nested_dir_4/nested_dir_5", - cx, - ); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " v dir_1/nested_dir_1/nested_dir_2/nested_dir_3", - " v nested_dir_4/nested_dir_5 <== selected", - " file_d.java", - " file_a.java", - " file_b.java", - " file_c.java", - "v root2", - " > dir_2", - ] - ); - toggle_expand_dir(&panel, "root2/dir_2", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " v dir_1/nested_dir_1/nested_dir_2/nested_dir_3", - " v nested_dir_4/nested_dir_5", - " file_d.java", - " file_a.java", - " file_b.java", - " file_c.java", - "v root2", - " v dir_2 <== selected", - " file_1.java", - ] - ); - - // Test 2: Single worktree with auto_fold_dirs = true and hide_root = true - { - let project = Project::test(fs.clone(), [path!("/root1").as_ref()], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - auto_fold_dirs: true, - hide_root: true, - ..settings - }, - cx, - ); - }); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["> dir_1/nested_dir_1/nested_dir_2/nested_dir_3"], - "Single worktree with hide_root=true should hide root and show auto-folded paths" - ); - - toggle_expand_dir( - &panel, - "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3", - cx, - ); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v dir_1/nested_dir_1/nested_dir_2/nested_dir_3 <== selected", - " > nested_dir_4/nested_dir_5", - " file_a.java", - " file_b.java", - " file_c.java", - ], - "Expanded auto-folded path with hidden root should show contents without root prefix" - ); - - toggle_expand_dir( - &panel, - "root1/dir_1/nested_dir_1/nested_dir_2/nested_dir_3/nested_dir_4/nested_dir_5", - cx, - ); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v dir_1/nested_dir_1/nested_dir_2/nested_dir_3", - " v nested_dir_4/nested_dir_5 <== selected", - " file_d.java", - " file_a.java", - " file_b.java", - " file_c.java", - ], - "Nested expansion with hidden root should maintain proper indentation" - ); - } -} - -#[gpui::test(iterations = 30)] -async fn test_editing_files(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root1", - json!({ - ".dockerignore": "", - ".git": { - "HEAD": "", - }, - "a": { - "0": { "q": "", "r": "", "s": "" }, - "1": { "t": "", "u": "" }, - "2": { "v": "", "w": "", "x": "", "y": "" }, - }, - "b": { - "3": { "Q": "" }, - "4": { "R": "", "S": "", "T": "", "U": "" }, - }, - "C": { - "5": {}, - "6": { "V": "", "W": "" }, - "7": { "X": "" }, - "8": { "Y": {}, "Z": "" } - } - }), - ) - .await; - fs.insert_tree( - "/root2", - json!({ - "d": { - "9": "" - }, - "e": {} - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) - .unwrap(); - cx.run_until_parked(); - - select_path(&panel, "root1", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1 <== selected", - " > .git", - " > a", - " > b", - " > C", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); - - // Add a file with the root folder selected. The filename editor is placed - // before the first file in the root folder. - panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx)); - cx.run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " > b", - " > C", - " [EDITOR: ''] <== selected", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); - - let confirm = panel.update_in(cx, |panel, window, cx| { - panel.filename_editor.update(cx, |editor, cx| { - editor.set_text("the-new-filename", window, cx) - }); - panel.confirm_edit(true, window, cx).unwrap() - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " > b", - " > C", - " [PROCESSING: 'the-new-filename'] <== selected", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); - - confirm.await.unwrap(); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " > b", - " > C", - " .dockerignore", - " the-new-filename <== selected <== marked", - "v root2", - " > d", - " > e", - ] - ); - - select_path(&panel, "root1/b", cx); - panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx)); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > 3", - " > 4", - " [EDITOR: ''] <== selected", - " > C", - " .dockerignore", - " the-new-filename", - ] - ); - - panel - .update_in(cx, |panel, window, cx| { - panel.filename_editor.update(cx, |editor, cx| { - editor.set_text("another-filename.txt", window, cx) - }); - panel.confirm_edit(true, window, cx).unwrap() - }) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > 3", - " > 4", - " another-filename.txt <== selected <== marked", - " > C", - " .dockerignore", - " the-new-filename", - ] - ); - - select_path(&panel, "root1/b/another-filename.txt", cx); - panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx)); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > 3", - " > 4", - " [EDITOR: 'another-filename.txt'] <== selected <== marked", - " > C", - " .dockerignore", - " the-new-filename", - ] - ); - - let confirm = panel.update_in(cx, |panel, window, cx| { - panel.filename_editor.update(cx, |editor, cx| { - let file_name_selections = editor - .selections - .all::(&editor.display_snapshot(cx)); - assert_eq!( - file_name_selections.len(), - 1, - "File editing should have a single selection, but got: {file_name_selections:?}" - ); - let file_name_selection = &file_name_selections[0]; - assert_eq!( - file_name_selection.start, - MultiBufferOffset(0), - "Should select the file name from the start" - ); - assert_eq!( - file_name_selection.end, - MultiBufferOffset("another-filename".len()), - "Should not select file extension" - ); - - editor.set_text("a-different-filename.tar.gz", window, cx) - }); - panel.confirm_edit(true, window, cx).unwrap() - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > 3", - " > 4", - " [PROCESSING: 'a-different-filename.tar.gz'] <== selected <== marked", - " > C", - " .dockerignore", - " the-new-filename", - ] - ); - - confirm.await.unwrap(); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > 3", - " > 4", - " a-different-filename.tar.gz <== selected", - " > C", - " .dockerignore", - " the-new-filename", - ] - ); - - panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx)); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > 3", - " > 4", - " [EDITOR: 'a-different-filename.tar.gz'] <== selected", - " > C", - " .dockerignore", - " the-new-filename", - ] - ); - - panel.update_in(cx, |panel, window, cx| { - panel.filename_editor.update(cx, |editor, cx| { - let file_name_selections = editor.selections.all::(&editor.display_snapshot(cx)); - assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}"); - let file_name_selection = &file_name_selections[0]; - assert_eq!(file_name_selection.start, MultiBufferOffset(0), "Should select the file name from the start"); - assert_eq!(file_name_selection.end, MultiBufferOffset("a-different-filename.tar".len()), "Should not select file extension, but still may select anything up to the last dot.."); - - }); - panel.cancel(&menu::Cancel, window, cx) - }); - cx.run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - panel.new_directory(&NewDirectory, window, cx) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > [EDITOR: ''] <== selected", - " > 3", - " > 4", - " a-different-filename.tar.gz", - " > C", - " .dockerignore", - ] - ); - - let confirm = panel.update_in(cx, |panel, window, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("new-dir", window, cx)); - panel.confirm_edit(true, window, cx).unwrap() - }); - panel.update_in(cx, |panel, window, cx| { - panel.select_next(&Default::default(), window, cx) - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > [PROCESSING: 'new-dir']", - " > 3 <== selected", - " > 4", - " a-different-filename.tar.gz", - " > C", - " .dockerignore", - ] - ); - - confirm.await.unwrap(); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > 3 <== selected", - " > 4", - " > new-dir", - " a-different-filename.tar.gz", - " > C", - " .dockerignore", - ] - ); - - panel.update_in(cx, |panel, window, cx| { - panel.rename(&Default::default(), window, cx) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > [EDITOR: '3'] <== selected", - " > 4", - " > new-dir", - " a-different-filename.tar.gz", - " > C", - " .dockerignore", - ] - ); - - // Dismiss the rename editor when it loses focus. - workspace.update(cx, |_, window, _| window.blur()).unwrap(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > 3 <== selected", - " > 4", - " > new-dir", - " a-different-filename.tar.gz", - " > C", - " .dockerignore", - ] - ); - - // Test empty filename and filename with only whitespace - panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx)); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " v 3", - " [EDITOR: ''] <== selected", - " Q", - " > 4", - " > new-dir", - " a-different-filename.tar.gz", - ] - ); - panel.update_in(cx, |panel, window, cx| { - panel.filename_editor.update(cx, |editor, cx| { - editor.set_text("", window, cx); - }); - assert!(panel.confirm_edit(true, window, cx).is_none()); - panel.filename_editor.update(cx, |editor, cx| { - editor.set_text(" ", window, cx); - }); - assert!(panel.confirm_edit(true, window, cx).is_none()); - panel.cancel(&menu::Cancel, window, cx); - panel.update_visible_entries(None, false, false, window, cx); - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " v 3 <== selected", - " Q", - " > 4", - " > new-dir", - " a-different-filename.tar.gz", - " > C", - ] - ); -} - -#[gpui::test(iterations = 10)] -async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root1", - json!({ - ".dockerignore": "", - ".git": { - "HEAD": "", - }, - "a": { - "0": { "q": "", "r": "", "s": "" }, - "1": { "t": "", "u": "" }, - "2": { "v": "", "w": "", "x": "", "y": "" }, - }, - "b": { - "3": { "Q": "" }, - "4": { "R": "", "S": "", "T": "", "U": "" }, - }, - "C": { - "5": {}, - "6": { "V": "", "W": "" }, - "7": { "X": "" }, - "8": { "Y": {}, "Z": "" } - } - }), - ) - .await; - fs.insert_tree( - "/root2", - json!({ - "d": { - "9": "" - }, - "e": {} - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) - .unwrap(); - cx.run_until_parked(); - - select_path(&panel, "root1", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1 <== selected", - " > .git", - " > a", - " > b", - " > C", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); - - // Add a file with the root folder selected. The filename editor is placed - // before the first file in the root folder. - panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx)); - cx.run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " > b", - " > C", - " [EDITOR: ''] <== selected", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); - - let confirm = panel.update_in(cx, |panel, window, cx| { - panel.filename_editor.update(cx, |editor, cx| { - editor.set_text("/bdir1/dir2/the-new-filename", window, cx) - }); - panel.confirm_edit(true, window, cx).unwrap() - }); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " > b", - " > C", - " [PROCESSING: 'bdir1/dir2/the-new-filename'] <== selected", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); - - confirm.await.unwrap(); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..13, cx), - &[ - "v root1", - " > .git", - " > a", - " > b", - " v bdir1", - " v dir2", - " the-new-filename <== selected <== marked", - " > C", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); -} - -#[gpui::test] -async fn test_adding_directory_via_file(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root1"), - json!({ - ".dockerignore": "", - ".git": { - "HEAD": "", - }, - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/root1").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) - .unwrap(); - cx.run_until_parked(); - - select_path(&panel, "root1", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["v root1 <== selected", " > .git", " .dockerignore",] - ); - - // Add a file with the root folder selected. The filename editor is placed - // before the first file in the root folder. - panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx)); - cx.run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " [EDITOR: ''] <== selected", - " .dockerignore", - ] - ); - - let confirm = panel.update_in(cx, |panel, window, cx| { - // If we want to create a subdirectory, there should be no prefix slash. - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("new_dir/", window, cx)); - panel.confirm_edit(true, window, cx).unwrap() - }); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " [PROCESSING: 'new_dir'] <== selected", - " .dockerignore", - ] - ); - - confirm.await.unwrap(); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " v new_dir <== selected", - " .dockerignore", - ] - ); - - // Test filename with whitespace - select_path(&panel, "root1", cx); - panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx)); - let confirm = panel.update_in(cx, |panel, window, cx| { - // If we want to create a subdirectory, there should be no prefix slash. - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("new dir 2/", window, cx)); - panel.confirm_edit(true, window, cx).unwrap() - }); - confirm.await.unwrap(); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " v new dir 2 <== selected", - " v new_dir", - " .dockerignore", - ] - ); - - // Test filename ends with "\" - #[cfg(target_os = "windows")] - { - select_path(&panel, "root1", cx); - panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx)); - let confirm = panel.update_in(cx, |panel, window, cx| { - // If we want to create a subdirectory, there should be no prefix slash. - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("new_dir_3\\", window, cx)); - panel.confirm_edit(true, window, cx).unwrap() - }); - confirm.await.unwrap(); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " v new dir 2", - " v new_dir", - " v new_dir_3 <== selected", - " .dockerignore", - ] - ); - } -} - -#[gpui::test] -async fn test_copy_paste(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root1", - json!({ - "one.two.txt": "", - "one.txt": "" - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - panel.update_in(cx, |panel, window, cx| { - panel.select_next(&Default::default(), window, cx); - panel.select_next(&Default::default(), window, cx); - }); - - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - // - "v root1", - " one.txt <== selected", - " one.two.txt", - ] - ); - - // Regression test - file name is created correctly when - // the copied file's name contains multiple dots. - panel.update_in(cx, |panel, window, cx| { - panel.copy(&Default::default(), window, cx); - panel.paste(&Default::default(), window, cx); - }); - cx.executor().run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - // - "v root1", - " one.txt", - " [EDITOR: 'one copy.txt'] <== selected <== marked", - " one.two.txt", - ] - ); - - panel.update_in(cx, |panel, window, cx| { - panel.filename_editor.update(cx, |editor, cx| { - let file_name_selections = editor - .selections - .all::(&editor.display_snapshot(cx)); - assert_eq!( - file_name_selections.len(), - 1, - "File editing should have a single selection, but got: {file_name_selections:?}" - ); - let file_name_selection = &file_name_selections[0]; - assert_eq!( - file_name_selection.start, - MultiBufferOffset("one".len()), - "Should select the file name disambiguation after the original file name" - ); - assert_eq!( - file_name_selection.end, - MultiBufferOffset("one copy".len()), - "Should select the file name disambiguation until the extension" - ); - }); - assert!(panel.confirm_edit(true, window, cx).is_none()); - }); - - panel.update_in(cx, |panel, window, cx| { - panel.paste(&Default::default(), window, cx); - }); - cx.executor().run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - // - "v root1", - " one.txt", - " one copy.txt", - " [EDITOR: 'one copy 1.txt'] <== selected <== marked", - " one.two.txt", - ] - ); - - panel.update_in(cx, |panel, window, cx| { - assert!(panel.confirm_edit(true, window, cx).is_none()) - }); -} - -#[gpui::test] -async fn test_cut_paste(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "one.txt": "", - "two.txt": "", - "a": {}, - "b": {} - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - select_path_with_mark(&panel, "root/one.txt", cx); - select_path_with_mark(&panel, "root/two.txt", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - "v root", - " > a", - " > b", - " one.txt <== marked", - " two.txt <== selected <== marked", - ] - ); - - panel.update_in(cx, |panel, window, cx| { - panel.cut(&Default::default(), window, cx); - }); - - select_path(&panel, "root/a", cx); - - panel.update_in(cx, |panel, window, cx| { - panel.paste(&Default::default(), window, cx); - panel.update_visible_entries(None, false, false, window, cx); - }); - cx.executor().run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - "v root", - " v a", - " one.txt <== marked", - " two.txt <== selected <== marked", - " > b", - ], - "Cut entries should be moved on first paste." - ); - - panel.update_in(cx, |panel, window, cx| { - panel.cancel(&menu::Cancel {}, window, cx) - }); - cx.executor().run_until_parked(); - - select_path(&panel, "root/b", cx); - - panel.update_in(cx, |panel, window, cx| { - panel.paste(&Default::default(), window, cx); - }); - cx.executor().run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - "v root", - " v a", - " one.txt", - " two.txt", - " v b", - " one.txt", - " two.txt <== selected", - ], - "Cut entries should only be copied for the second paste!" - ); -} - -#[gpui::test] -async fn test_cut_paste_between_different_worktrees(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root1", - json!({ - "one.txt": "", - "two.txt": "", - "three.txt": "", - "a": { - "0": { "q": "", "r": "", "s": "" }, - "1": { "t": "", "u": "" }, - "2": { "v": "", "w": "", "x": "", "y": "" }, - }, - }), - ) - .await; - - fs.insert_tree( - "/root2", - json!({ - "one.txt": "", - "two.txt": "", - "four.txt": "", - "b": { - "3": { "Q": "" }, - "4": { "R": "", "S": "", "T": "", "U": "" }, - }, - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - select_path(&panel, "root1/three.txt", cx); - panel.update_in(cx, |panel, window, cx| { - panel.cut(&Default::default(), window, cx); - }); - - select_path(&panel, "root2/one.txt", cx); - panel.update_in(cx, |panel, window, cx| { - panel.select_next(&Default::default(), window, cx); - panel.paste(&Default::default(), window, cx); - }); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - // - "v root1", - " > a", - " one.txt", - " two.txt", - "v root2", - " > b", - " four.txt", - " one.txt", - " three.txt <== selected <== marked", - " two.txt", - ] - ); - - select_path(&panel, "root1/a", cx); - panel.update_in(cx, |panel, window, cx| { - panel.cut(&Default::default(), window, cx); - }); - select_path(&panel, "root2/two.txt", cx); - panel.update_in(cx, |panel, window, cx| { - panel.select_next(&Default::default(), window, cx); - panel.paste(&Default::default(), window, cx); - }); - - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - // - "v root1", - " one.txt", - " two.txt", - "v root2", - " > a <== selected", - " > b", - " four.txt", - " one.txt", - " three.txt <== marked", - " two.txt", - ] - ); -} - -#[gpui::test] -async fn test_copy_paste_between_different_worktrees(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root1", - json!({ - "one.txt": "", - "two.txt": "", - "three.txt": "", - "a": { - "0": { "q": "", "r": "", "s": "" }, - "1": { "t": "", "u": "" }, - "2": { "v": "", "w": "", "x": "", "y": "" }, - }, - }), - ) - .await; - - fs.insert_tree( - "/root2", - json!({ - "one.txt": "", - "two.txt": "", - "four.txt": "", - "b": { - "3": { "Q": "" }, - "4": { "R": "", "S": "", "T": "", "U": "" }, - }, - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - select_path(&panel, "root1/three.txt", cx); - panel.update_in(cx, |panel, window, cx| { - panel.copy(&Default::default(), window, cx); - }); - - select_path(&panel, "root2/one.txt", cx); - panel.update_in(cx, |panel, window, cx| { - panel.select_next(&Default::default(), window, cx); - panel.paste(&Default::default(), window, cx); - }); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - // - "v root1", - " > a", - " one.txt", - " three.txt", - " two.txt", - "v root2", - " > b", - " four.txt", - " one.txt", - " three.txt <== selected <== marked", - " two.txt", - ] - ); - - select_path(&panel, "root1/three.txt", cx); - panel.update_in(cx, |panel, window, cx| { - panel.copy(&Default::default(), window, cx); - }); - select_path(&panel, "root2/two.txt", cx); - panel.update_in(cx, |panel, window, cx| { - panel.select_next(&Default::default(), window, cx); - panel.paste(&Default::default(), window, cx); - }); - - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - // - "v root1", - " > a", - " one.txt", - " three.txt", - " two.txt", - "v root2", - " > b", - " four.txt", - " one.txt", - " three.txt", - " [EDITOR: 'three copy.txt'] <== selected <== marked", - " two.txt", - ] - ); - - panel.update_in(cx, |panel, window, cx| { - panel.cancel(&menu::Cancel {}, window, cx) - }); - cx.executor().run_until_parked(); - - select_path(&panel, "root1/a", cx); - panel.update_in(cx, |panel, window, cx| { - panel.copy(&Default::default(), window, cx); - }); - select_path(&panel, "root2/two.txt", cx); - panel.update_in(cx, |panel, window, cx| { - panel.select_next(&Default::default(), window, cx); - panel.paste(&Default::default(), window, cx); - }); - - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - // - "v root1", - " > a", - " one.txt", - " three.txt", - " two.txt", - "v root2", - " > a <== selected", - " > b", - " four.txt", - " one.txt", - " three.txt", - " three copy.txt", - " two.txt", - ] - ); -} - -#[gpui::test] -async fn test_copy_paste_directory(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "a": { - "one.txt": "", - "two.txt": "", - "inner_dir": { - "three.txt": "", - "four.txt": "", - } - }, - "b": {} - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - select_path(&panel, "root/a", cx); - panel.update_in(cx, |panel, window, cx| { - panel.copy(&Default::default(), window, cx); - panel.select_next(&Default::default(), window, cx); - panel.paste(&Default::default(), window, cx); - }); - cx.executor().run_until_parked(); - - let pasted_dir = find_project_entry(&panel, "root/b/a", cx); - assert_ne!(pasted_dir, None, "Pasted directory should have an entry"); - - let pasted_dir_file = find_project_entry(&panel, "root/b/a/one.txt", cx); - assert_ne!( - pasted_dir_file, None, - "Pasted directory file should have an entry" - ); - - let pasted_dir_inner_dir = find_project_entry(&panel, "root/b/a/inner_dir", cx); - assert_ne!( - pasted_dir_inner_dir, None, - "Directories inside pasted directory should have an entry" - ); - - toggle_expand_dir(&panel, "root/b/a", cx); - toggle_expand_dir(&panel, "root/b/a/inner_dir", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - // - "v root", - " > a", - " v b", - " v a", - " v inner_dir <== selected", - " four.txt", - " three.txt", - " one.txt", - " two.txt", - ] - ); - - select_path(&panel, "root", cx); - panel.update_in(cx, |panel, window, cx| { - panel.paste(&Default::default(), window, cx) - }); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - // - "v root", - " > a", - " > [EDITOR: 'a copy'] <== selected", - " v b", - " v a", - " v inner_dir", - " four.txt", - " three.txt", - " one.txt", - " two.txt" - ] - ); - - let confirm = panel.update_in(cx, |panel, window, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("c", window, cx)); - panel.confirm_edit(true, window, cx).unwrap() - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - // - "v root", - " > a", - " > [PROCESSING: 'c'] <== selected", - " v b", - " v a", - " v inner_dir", - " four.txt", - " three.txt", - " one.txt", - " two.txt" - ] - ); - - confirm.await.unwrap(); - - panel.update_in(cx, |panel, window, cx| { - panel.paste(&Default::default(), window, cx) - }); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - // - "v root", - " > a", - " v b", - " v a", - " v inner_dir", - " four.txt", - " three.txt", - " one.txt", - " two.txt", - " v c", - " > a <== selected", - " > inner_dir", - " one.txt", - " two.txt", - ] - ); -} - -#[gpui::test] -async fn test_copy_paste_directory_with_sibling_file(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/test", - json!({ - "dir1": { - "a.txt": "", - "b.txt": "", - }, - "dir2": {}, - "c.txt": "", - "d.txt": "", - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "test/dir1", cx); - - cx.simulate_modifiers_change(gpui::Modifiers { - control: true, - ..Default::default() - }); - - select_path_with_mark(&panel, "test/dir1", cx); - select_path_with_mark(&panel, "test/c.txt", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v test", - " v dir1 <== marked", - " a.txt", - " b.txt", - " > dir2", - " c.txt <== selected <== marked", - " d.txt", - ], - "Initial state before copying dir1 and c.txt" - ); - - panel.update_in(cx, |panel, window, cx| { - panel.copy(&Default::default(), window, cx); - }); - select_path(&panel, "test/dir2", cx); - panel.update_in(cx, |panel, window, cx| { - panel.paste(&Default::default(), window, cx); - }); - cx.executor().run_until_parked(); - - toggle_expand_dir(&panel, "test/dir2/dir1", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v test", - " v dir1 <== marked", - " a.txt", - " b.txt", - " v dir2", - " v dir1 <== selected", - " a.txt", - " b.txt", - " c.txt", - " c.txt <== marked", - " d.txt", - ], - "Should copy dir1 as well as c.txt into dir2" - ); - - // Disambiguating multiple files should not open the rename editor. - select_path(&panel, "test/dir2", cx); - panel.update_in(cx, |panel, window, cx| { - panel.paste(&Default::default(), window, cx); - }); - cx.executor().run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v test", - " v dir1 <== marked", - " a.txt", - " b.txt", - " v dir2", - " v dir1", - " a.txt", - " b.txt", - " > dir1 copy <== selected", - " c.txt", - " c copy.txt", - " c.txt <== marked", - " d.txt", - ], - "Should copy dir1 as well as c.txt into dir2 and disambiguate them without opening the rename editor" - ); -} - -#[gpui::test] -async fn test_copy_paste_nested_and_root_entries(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/test", - json!({ - "dir1": { - "a.txt": "", - "b.txt": "", - }, - "dir2": {}, - "c.txt": "", - "d.txt": "", - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "test/dir1", cx); - - cx.simulate_modifiers_change(gpui::Modifiers { - control: true, - ..Default::default() - }); - - select_path_with_mark(&panel, "test/dir1/a.txt", cx); - select_path_with_mark(&panel, "test/dir1", cx); - select_path_with_mark(&panel, "test/c.txt", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v test", - " v dir1 <== marked", - " a.txt <== marked", - " b.txt", - " > dir2", - " c.txt <== selected <== marked", - " d.txt", - ], - "Initial state before copying a.txt, dir1 and c.txt" - ); - - panel.update_in(cx, |panel, window, cx| { - panel.copy(&Default::default(), window, cx); - }); - select_path(&panel, "test/dir2", cx); - panel.update_in(cx, |panel, window, cx| { - panel.paste(&Default::default(), window, cx); - }); - cx.executor().run_until_parked(); - - toggle_expand_dir(&panel, "test/dir2/dir1", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v test", - " v dir1 <== marked", - " a.txt <== marked", - " b.txt", - " v dir2", - " v dir1 <== selected", - " a.txt", - " b.txt", - " c.txt", - " c.txt <== marked", - " d.txt", - ], - "Should copy dir1 and c.txt into dir2. a.txt is already present in copied dir1." - ); -} - -#[gpui::test] -async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/src"), - json!({ - "test": { - "first.rs": "// First Rust file", - "second.rs": "// Second Rust file", - "third.rs": "// Third Rust file", - } - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/src").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "src/test", cx); - select_path(&panel, "src/test/first.rs", cx); - panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " first.rs <== selected <== marked", - " second.rs", - " third.rs" - ] - ); - ensure_single_file_is_opened(&workspace, "test/first.rs", cx); - - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " second.rs <== selected", - " third.rs" - ], - "Project panel should have no deleted file, no other file is selected in it" - ); - ensure_no_open_items_and_panes(&workspace, cx); - - panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " second.rs <== selected <== marked", - " third.rs" - ] - ); - ensure_single_file_is_opened(&workspace, "test/second.rs", cx); - - workspace - .update(cx, |workspace, window, cx| { - let active_items = workspace - .panes() - .iter() - .filter_map(|pane| pane.read(cx).active_item()) - .collect::>(); - assert_eq!(active_items.len(), 1); - let open_editor = active_items - .into_iter() - .next() - .unwrap() - .downcast::() - .expect("Open item should be an editor"); - open_editor.update(cx, |editor, cx| { - editor.set_text("Another text!", window, cx) - }); - }) - .unwrap(); - submit_deletion_skipping_prompt(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["v src", " v test", " third.rs <== selected"], - "Project panel should have no deleted file, with one last file remaining" - ); - ensure_no_open_items_and_panes(&workspace, cx); -} - -#[gpui::test] -async fn test_auto_open_new_file_when_enabled(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - set_auto_open_settings( - cx, - ProjectPanelAutoOpenSettings { - on_create: Some(true), - ..Default::default() - }, - ); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/root"), json!({})).await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx)); - cx.run_until_parked(); - panel - .update_in(cx, |panel, window, cx| { - panel.filename_editor.update(cx, |editor, cx| { - editor.set_text("auto-open.rs", window, cx); - }); - panel.confirm_edit(true, window, cx).unwrap() - }) - .await - .unwrap(); - cx.run_until_parked(); - - ensure_single_file_is_opened(&workspace, "auto-open.rs", cx); -} - -#[gpui::test] -async fn test_auto_open_new_file_when_disabled(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - set_auto_open_settings( - cx, - ProjectPanelAutoOpenSettings { - on_create: Some(false), - ..Default::default() - }, - ); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/root"), json!({})).await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx)); - cx.run_until_parked(); - panel - .update_in(cx, |panel, window, cx| { - panel.filename_editor.update(cx, |editor, cx| { - editor.set_text("manual-open.rs", window, cx); - }); - panel.confirm_edit(true, window, cx).unwrap() - }) - .await - .unwrap(); - cx.run_until_parked(); - - ensure_no_open_items_and_panes(&workspace, cx); -} - -#[gpui::test] -async fn test_auto_open_on_paste_when_enabled(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - set_auto_open_settings( - cx, - ProjectPanelAutoOpenSettings { - on_paste: Some(true), - ..Default::default() - }, - ); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - "src": { - "original.rs": "" - }, - "target": {} - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "root/src", cx); - toggle_expand_dir(&panel, "root/target", cx); - - select_path(&panel, "root/src/original.rs", cx); - panel.update_in(cx, |panel, window, cx| { - panel.copy(&Default::default(), window, cx); - }); - - select_path(&panel, "root/target", cx); - panel.update_in(cx, |panel, window, cx| { - panel.paste(&Default::default(), window, cx); - }); - cx.executor().run_until_parked(); - - ensure_single_file_is_opened(&workspace, "target/original.rs", cx); -} - -#[gpui::test] -async fn test_auto_open_on_paste_when_disabled(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - set_auto_open_settings( - cx, - ProjectPanelAutoOpenSettings { - on_paste: Some(false), - ..Default::default() - }, - ); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - "src": { - "original.rs": "" - }, - "target": {} - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "root/src", cx); - toggle_expand_dir(&panel, "root/target", cx); - - select_path(&panel, "root/src/original.rs", cx); - panel.update_in(cx, |panel, window, cx| { - panel.copy(&Default::default(), window, cx); - }); - - select_path(&panel, "root/target", cx); - panel.update_in(cx, |panel, window, cx| { - panel.paste(&Default::default(), window, cx); - }); - cx.executor().run_until_parked(); - - ensure_no_open_items_and_panes(&workspace, cx); - assert!( - find_project_entry(&panel, "root/target/original.rs", cx).is_some(), - "Pasted entry should exist even when auto-open is disabled" - ); -} - -#[gpui::test] -async fn test_auto_open_on_drop_when_enabled(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - set_auto_open_settings( - cx, - ProjectPanelAutoOpenSettings { - on_drop: Some(true), - ..Default::default() - }, - ); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/root"), json!({})).await; - - let temp_dir = tempfile::tempdir().unwrap(); - let external_path = temp_dir.path().join("dropped.rs"); - std::fs::write(&external_path, "// dropped").unwrap(); - fs.insert_tree_from_real_fs(temp_dir.path(), temp_dir.path()) - .await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - let root_entry = find_project_entry(&panel, "root", cx).unwrap(); - panel.update_in(cx, |panel, window, cx| { - panel.drop_external_files(std::slice::from_ref(&external_path), root_entry, window, cx); - }); - cx.executor().run_until_parked(); - - ensure_single_file_is_opened(&workspace, "dropped.rs", cx); -} - -#[gpui::test] -async fn test_auto_open_on_drop_when_disabled(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - set_auto_open_settings( - cx, - ProjectPanelAutoOpenSettings { - on_drop: Some(false), - ..Default::default() - }, - ); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/root"), json!({})).await; - - let temp_dir = tempfile::tempdir().unwrap(); - let external_path = temp_dir.path().join("manual.rs"); - std::fs::write(&external_path, "// dropped").unwrap(); - fs.insert_tree_from_real_fs(temp_dir.path(), temp_dir.path()) - .await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - let root_entry = find_project_entry(&panel, "root", cx).unwrap(); - panel.update_in(cx, |panel, window, cx| { - panel.drop_external_files(std::slice::from_ref(&external_path), root_entry, window, cx); - }); - cx.executor().run_until_parked(); - - ensure_no_open_items_and_panes(&workspace, cx); - assert!( - find_project_entry(&panel, "root/manual.rs", cx).is_some(), - "Dropped entry should exist even when auto-open is disabled" - ); -} - -#[gpui::test] -async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/src", - json!({ - "test": { - "first.rs": "// First Rust file", - "second.rs": "// Second Rust file", - "third.rs": "// Third Rust file", - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) - .unwrap(); - cx.run_until_parked(); - - select_path(&panel, "src", cx); - panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - // - "v src <== selected", - " > test" - ] - ); - panel.update_in(cx, |panel, window, cx| { - panel.new_directory(&NewDirectory, window, cx) - }); - cx.run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - // - "v src", - " > [EDITOR: ''] <== selected", - " > test" - ] - ); - panel.update_in(cx, |panel, window, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("test", window, cx)); - assert!( - panel.confirm_edit(true, window, cx).is_none(), - "Should not allow to confirm on conflicting new directory name" - ); - }); - cx.executor().run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!( - panel.state.edit_state.is_some(), - "Edit state should not be None after conflicting new directory name" - ); - panel.cancel(&menu::Cancel, window, cx); - panel.update_visible_entries(None, false, false, window, cx); - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - // - "v src <== selected", - " > test" - ], - "File list should be unchanged after failed folder create confirmation" - ); - - select_path(&panel, "src/test", cx); - panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - // - "v src", - " > test <== selected" - ] - ); - panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx)); - cx.run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " [EDITOR: ''] <== selected", - " first.rs", - " second.rs", - " third.rs" - ] - ); - panel.update_in(cx, |panel, window, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("first.rs", window, cx)); - assert!( - panel.confirm_edit(true, window, cx).is_none(), - "Should not allow to confirm on conflicting new file name" - ); - }); - cx.executor().run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!( - panel.state.edit_state.is_some(), - "Edit state should not be None after conflicting new file name" - ); - panel.cancel(&menu::Cancel, window, cx); - panel.update_visible_entries(None, false, false, window, cx); - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test <== selected", - " first.rs", - " second.rs", - " third.rs" - ], - "File list should be unchanged after failed file create confirmation" - ); - - select_path(&panel, "src/test/first.rs", cx); - panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " first.rs <== selected", - " second.rs", - " third.rs" - ], - ); - panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx)); - cx.executor().run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " [EDITOR: 'first.rs'] <== selected", - " second.rs", - " third.rs" - ] - ); - panel.update_in(cx, |panel, window, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("second.rs", window, cx)); - assert!( - panel.confirm_edit(true, window, cx).is_none(), - "Should not allow to confirm on conflicting file rename" - ) - }); - cx.executor().run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!( - panel.state.edit_state.is_some(), - "Edit state should not be None after conflicting file rename" - ); - panel.cancel(&menu::Cancel, window, cx); - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " first.rs <== selected", - " second.rs", - " third.rs" - ], - "File list should be unchanged after failed rename confirmation" - ); -} - -// NOTE: This test is skipped on Windows, because on Windows, -// when it triggers the lsp store it converts `/src/test/first copy.txt` into an uri -// but it fails with message `"/src\\test\\first copy.txt" is not parseable as an URI` -#[gpui::test] -#[cfg_attr(target_os = "windows", ignore)] -async fn test_create_duplicate_items_and_check_history(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/src", - json!({ - "test": { - "first.txt": "// First Txt file", - "second.txt": "// Second Txt file", - "third.txt": "// Third Txt file", - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) - .unwrap(); - cx.run_until_parked(); - - select_path(&panel, "src", cx); - panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - // - "v src <== selected", - " > test" - ] - ); - panel.update_in(cx, |panel, window, cx| { - panel.new_directory(&NewDirectory, window, cx) - }); - cx.run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - // - "v src", - " > [EDITOR: ''] <== selected", - " > test" - ] - ); - panel.update_in(cx, |panel, window, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("test", window, cx)); - assert!( - panel.confirm_edit(true, window, cx).is_none(), - "Should not allow to confirm on conflicting new directory name" - ); - }); - cx.executor().run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!( - panel.state.edit_state.is_some(), - "Edit state should not be None after conflicting new directory name" - ); - panel.cancel(&menu::Cancel, window, cx); - panel.update_visible_entries(None, false, false, window, cx); - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - // - "v src <== selected", - " > test" - ], - "File list should be unchanged after failed folder create confirmation" - ); - - select_path(&panel, "src/test", cx); - panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - // - "v src", - " > test <== selected" - ] - ); - panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx)); - cx.run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " [EDITOR: ''] <== selected", - " first.txt", - " second.txt", - " third.txt" - ] - ); - panel.update_in(cx, |panel, window, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("first.txt", window, cx)); - assert!( - panel.confirm_edit(true, window, cx).is_none(), - "Should not allow to confirm on conflicting new file name" - ); - }); - cx.executor().run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!( - panel.state.edit_state.is_some(), - "Edit state should not be None after conflicting new file name" - ); - panel.cancel(&menu::Cancel, window, cx); - panel.update_visible_entries(None, false, false, window, cx); - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test <== selected", - " first.txt", - " second.txt", - " third.txt" - ], - "File list should be unchanged after failed file create confirmation" - ); - - select_path(&panel, "src/test/first.txt", cx); - panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " first.txt <== selected", - " second.txt", - " third.txt" - ], - ); - panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx)); - cx.executor().run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " [EDITOR: 'first.txt'] <== selected", - " second.txt", - " third.txt" - ] - ); - panel.update_in(cx, |panel, window, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("second.txt", window, cx)); - assert!( - panel.confirm_edit(true, window, cx).is_none(), - "Should not allow to confirm on conflicting file rename" - ) - }); - cx.executor().run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!( - panel.state.edit_state.is_some(), - "Edit state should not be None after conflicting file rename" - ); - panel.cancel(&menu::Cancel, window, cx); - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " first.txt <== selected", - " second.txt", - " third.txt" - ], - "File list should be unchanged after failed rename confirmation" - ); - panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx)); - cx.executor().run_until_parked(); - // Try to duplicate and check history - panel.update_in(cx, |panel, window, cx| { - panel.duplicate(&Duplicate, window, cx) - }); - cx.executor().run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " first.txt", - " [EDITOR: 'first copy.txt'] <== selected <== marked", - " second.txt", - " third.txt" - ], - ); - - let confirm = panel.update_in(cx, |panel, window, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("fourth.txt", window, cx)); - panel.confirm_edit(true, window, cx).unwrap() - }); - confirm.await.unwrap(); - cx.executor().run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " first.txt", - " fourth.txt <== selected", - " second.txt", - " third.txt" - ], - "File list should be different after rename confirmation" - ); - - panel.update_in(cx, |panel, window, cx| { - panel.update_visible_entries(None, false, false, window, cx); - }); - cx.executor().run_until_parked(); - - select_path(&panel, "src/test/first.txt", cx); - panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx)); - cx.executor().run_until_parked(); - - workspace - .read_with(cx, |this, cx| { - assert!( - this.recent_navigation_history_iter(cx) - .any(|(project_path, abs_path)| { - project_path.path == Arc::from(rel_path("test/fourth.txt")) - && abs_path == Some(PathBuf::from(path!("/src/test/fourth.txt"))) - }) - ); - }) - .unwrap(); -} - -// NOTE: This test is skipped on Windows, because on Windows, -// when it triggers the lsp store it converts `/src/test/first.txt` into an uri -// but it fails with message `"/src\\test\\first.txt" is not parseable as an URI` -#[gpui::test] -#[cfg_attr(target_os = "windows", ignore)] -async fn test_rename_item_and_check_history(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/src", - json!({ - "test": { - "first.txt": "// First Txt file", - "second.txt": "// Second Txt file", - "third.txt": "// Third Txt file", - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) - .unwrap(); - cx.run_until_parked(); - - select_path(&panel, "src", cx); - panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - // - "v src <== selected", - " > test" - ] - ); - - select_path(&panel, "src/test", cx); - panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - // - "v src", - " > test <== selected" - ] - ); - panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx)); - cx.run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - - select_path(&panel, "src/test/first.txt", cx); - panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx)); - cx.executor().run_until_parked(); - - panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx)); - cx.executor().run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " [EDITOR: 'first.txt'] <== selected <== marked", - " second.txt", - " third.txt" - ], - ); - - let confirm = panel.update_in(cx, |panel, window, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("fourth.txt", window, cx)); - panel.confirm_edit(true, window, cx).unwrap() - }); - confirm.await.unwrap(); - cx.executor().run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " fourth.txt <== selected", - " second.txt", - " third.txt" - ], - "File list should be different after rename confirmation" - ); - - panel.update_in(cx, |panel, window, cx| { - panel.update_visible_entries(None, false, false, window, cx); - }); - cx.executor().run_until_parked(); - - select_path(&panel, "src/test/second.txt", cx); - panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx)); - cx.executor().run_until_parked(); - - workspace - .read_with(cx, |this, cx| { - assert!( - this.recent_navigation_history_iter(cx) - .any(|(project_path, abs_path)| { - project_path.path == Arc::from(rel_path("test/fourth.txt")) - && abs_path == Some(PathBuf::from(path!("/src/test/fourth.txt"))) - }) - ); - }) - .unwrap(); -} - -#[gpui::test] -async fn test_select_git_entry(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - "tree1": { - ".git": {}, - "dir1": { - "modified1.txt": "1", - "unmodified1.txt": "1", - "modified2.txt": "1", - }, - "dir2": { - "modified3.txt": "1", - "unmodified2.txt": "1", - }, - "modified4.txt": "1", - "unmodified3.txt": "1", - }, - "tree2": { - ".git": {}, - "dir3": { - "modified5.txt": "1", - "unmodified4.txt": "1", - }, - "modified6.txt": "1", - "unmodified5.txt": "1", - } - }), - ) - .await; - - // Mark files as git modified - fs.set_head_and_index_for_repo( - path!("/root/tree1/.git").as_ref(), - &[ - ("dir1/modified1.txt", "modified".into()), - ("dir1/modified2.txt", "modified".into()), - ("modified4.txt", "modified".into()), - ("dir2/modified3.txt", "modified".into()), - ], - ); - fs.set_head_and_index_for_repo( - path!("/root/tree2/.git").as_ref(), - &[ - ("dir3/modified5.txt", "modified".into()), - ("modified6.txt", "modified".into()), - ], - ); - - let project = Project::test( - fs.clone(), - [path!("/root/tree1").as_ref(), path!("/root/tree2").as_ref()], - cx, - ) - .await; - - let (scan1_complete, scan2_complete) = project.update(cx, |project, cx| { - let mut worktrees = project.worktrees(cx); - let worktree1 = worktrees.next().unwrap(); - let worktree2 = worktrees.next().unwrap(); - ( - worktree1.read(cx).as_local().unwrap().scan_complete(), - worktree2.read(cx).as_local().unwrap().scan_complete(), - ) - }); - scan1_complete.await; - scan2_complete.await; - cx.run_until_parked(); - - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - // Check initial state - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v tree1", - " > .git", - " > dir1", - " > dir2", - " modified4.txt", - " unmodified3.txt", - "v tree2", - " > .git", - " > dir3", - " modified6.txt", - " unmodified5.txt" - ], - ); - - // Test selecting next modified entry - panel.update_in(cx, |panel, window, cx| { - panel.select_next_git_entry(&SelectNextGitEntry, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..6, cx), - &[ - "v tree1", - " > .git", - " v dir1", - " modified1.txt <== selected", - " modified2.txt", - " unmodified1.txt", - ], - ); - - panel.update_in(cx, |panel, window, cx| { - panel.select_next_git_entry(&SelectNextGitEntry, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..6, cx), - &[ - "v tree1", - " > .git", - " v dir1", - " modified1.txt", - " modified2.txt <== selected", - " unmodified1.txt", - ], - ); - - panel.update_in(cx, |panel, window, cx| { - panel.select_next_git_entry(&SelectNextGitEntry, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 6..9, cx), - &[ - " v dir2", - " modified3.txt <== selected", - " unmodified2.txt", - ], - ); - - panel.update_in(cx, |panel, window, cx| { - panel.select_next_git_entry(&SelectNextGitEntry, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 9..11, cx), - &[" modified4.txt <== selected", " unmodified3.txt",], - ); - - panel.update_in(cx, |panel, window, cx| { - panel.select_next_git_entry(&SelectNextGitEntry, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 13..16, cx), - &[ - " v dir3", - " modified5.txt <== selected", - " unmodified4.txt", - ], - ); - - panel.update_in(cx, |panel, window, cx| { - panel.select_next_git_entry(&SelectNextGitEntry, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 16..18, cx), - &[" modified6.txt <== selected", " unmodified5.txt",], - ); - - // Wraps around to first modified file - panel.update_in(cx, |panel, window, cx| { - panel.select_next_git_entry(&SelectNextGitEntry, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..18, cx), - &[ - "v tree1", - " > .git", - " v dir1", - " modified1.txt <== selected", - " modified2.txt", - " unmodified1.txt", - " v dir2", - " modified3.txt", - " unmodified2.txt", - " modified4.txt", - " unmodified3.txt", - "v tree2", - " > .git", - " v dir3", - " modified5.txt", - " unmodified4.txt", - " modified6.txt", - " unmodified5.txt", - ], - ); - - // Wraps around again to last modified file - panel.update_in(cx, |panel, window, cx| { - panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 16..18, cx), - &[" modified6.txt <== selected", " unmodified5.txt",], - ); - - panel.update_in(cx, |panel, window, cx| { - panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 13..16, cx), - &[ - " v dir3", - " modified5.txt <== selected", - " unmodified4.txt", - ], - ); - - panel.update_in(cx, |panel, window, cx| { - panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 9..11, cx), - &[" modified4.txt <== selected", " unmodified3.txt",], - ); - - panel.update_in(cx, |panel, window, cx| { - panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 6..9, cx), - &[ - " v dir2", - " modified3.txt <== selected", - " unmodified2.txt", - ], - ); - - panel.update_in(cx, |panel, window, cx| { - panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..6, cx), - &[ - "v tree1", - " > .git", - " v dir1", - " modified1.txt", - " modified2.txt <== selected", - " unmodified1.txt", - ], - ); - - panel.update_in(cx, |panel, window, cx| { - panel.select_prev_git_entry(&SelectPrevGitEntry, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..6, cx), - &[ - "v tree1", - " > .git", - " v dir1", - " modified1.txt <== selected", - " modified2.txt", - " unmodified1.txt", - ], - ); -} - -#[gpui::test] -async fn test_select_directory(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/project_root", - json!({ - "dir_1": { - "nested_dir": { - "file_a.py": "# File contents", - } - }, - "file_1.py": "# File contents", - "dir_2": { - - }, - "dir_3": { - - }, - "file_2.py": "# File contents", - "dir_4": { - - }, - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx)); - cx.executor().run_until_parked(); - select_path(&panel, "project_root/dir_1", cx); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root", - " > dir_1 <== selected", - " > dir_2", - " > dir_3", - " > dir_4", - " file_1.py", - " file_2.py", - ] - ); - panel.update_in(cx, |panel, window, cx| { - panel.select_prev_directory(&SelectPrevDirectory, window, cx) - }); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root <== selected", - " > dir_1", - " > dir_2", - " > dir_3", - " > dir_4", - " file_1.py", - " file_2.py", - ] - ); - - panel.update_in(cx, |panel, window, cx| { - panel.select_prev_directory(&SelectPrevDirectory, window, cx) - }); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root", - " > dir_1", - " > dir_2", - " > dir_3", - " > dir_4 <== selected", - " file_1.py", - " file_2.py", - ] - ); - - panel.update_in(cx, |panel, window, cx| { - panel.select_next_directory(&SelectNextDirectory, window, cx) - }); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root <== selected", - " > dir_1", - " > dir_2", - " > dir_3", - " > dir_4", - " file_1.py", - " file_2.py", - ] - ); -} - -#[gpui::test] -async fn test_select_first_last(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/project_root", - json!({ - "dir_1": { - "nested_dir": { - "file_a.py": "# File contents", - } - }, - "file_1.py": "# File contents", - "file_2.py": "# File contents", - "zdir_2": { - "nested_dir2": { - "file_b.py": "# File contents", - } - }, - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root", - " > dir_1", - " > zdir_2", - " file_1.py", - " file_2.py", - ] - ); - panel.update_in(cx, |panel, window, cx| { - panel.select_first(&SelectFirst, window, cx) - }); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root <== selected", - " > dir_1", - " > zdir_2", - " file_1.py", - " file_2.py", - ] - ); - - panel.update_in(cx, |panel, window, cx| { - panel.select_last(&SelectLast, window, cx) - }); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root", - " > dir_1", - " > zdir_2", - " file_1.py", - " file_2.py <== selected", - ] - ); - - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_root: true, - ..settings - }, - cx, - ); - }); - - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - #[rustfmt::skip] - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "> dir_1", - "> zdir_2", - " file_1.py", - " file_2.py", - ], - "With hide_root=true, root should be hidden" - ); - - panel.update_in(cx, |panel, window, cx| { - panel.select_first(&SelectFirst, window, cx) - }); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "> dir_1 <== selected", - "> zdir_2", - " file_1.py", - " file_2.py", - ], - "With hide_root=true, first entry should be dir_1, not the hidden root" - ); -} - -#[gpui::test] -async fn test_dir_toggle_collapse(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/project_root", - json!({ - "dir_1": { - "nested_dir": { - "file_a.py": "# File contents", - } - }, - "file_1.py": "# File contents", - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx)); - cx.executor().run_until_parked(); - select_path(&panel, "project_root/dir_1", cx); - panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx)); - select_path(&panel, "project_root/dir_1/nested_dir", cx); - panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx)); - panel.update_in(cx, |panel, window, cx| panel.open(&Open, window, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root", - " v dir_1", - " > nested_dir <== selected", - " file_1.py", - ] - ); -} - -#[gpui::test] -async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/project_root", - json!({ - "dir_1": { - "nested_dir": { - "file_a.py": "# File contents", - "file_b.py": "# File contents", - "file_c.py": "# File contents", - }, - "file_1.py": "# File contents", - "file_2.py": "# File contents", - "file_3.py": "# File contents", - }, - "dir_2": { - "file_1.py": "# File contents", - "file_2.py": "# File contents", - "file_3.py": "# File contents", - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - panel.update_in(cx, |panel, window, cx| { - panel.collapse_all_entries(&CollapseAllEntries, window, cx) - }); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["v project_root", " > dir_1", " > dir_2",] - ); - - // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries - toggle_expand_dir(&panel, "project_root/dir_1", cx); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root", - " v dir_1 <== selected", - " > nested_dir", - " file_1.py", - " file_2.py", - " file_3.py", - " > dir_2", - ] - ); -} - -#[gpui::test] -async fn test_collapse_all_entries_multiple_worktrees(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - let worktree_content = json!({ - "dir_1": { - "file_1.py": "# File contents", - }, - "dir_2": { - "file_1.py": "# File contents", - } - }); - - fs.insert_tree("/project_root_1", worktree_content.clone()) - .await; - fs.insert_tree("/project_root_2", worktree_content).await; - - let project = Project::test( - fs.clone(), - ["/project_root_1".as_ref(), "/project_root_2".as_ref()], - cx, - ) - .await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - panel.update_in(cx, |panel, window, cx| { - panel.collapse_all_entries(&CollapseAllEntries, window, cx) - }); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["> project_root_1", "> project_root_2",] - ); -} - -#[gpui::test] -async fn test_collapse_all_entries_with_collapsed_root(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/project_root", - json!({ - "dir_1": { - "nested_dir": { - "file_a.py": "# File contents", - "file_b.py": "# File contents", - "file_c.py": "# File contents", - }, - "file_1.py": "# File contents", - "file_2.py": "# File contents", - "file_3.py": "# File contents", - }, - "dir_2": { - "file_1.py": "# File contents", - "file_2.py": "# File contents", - "file_3.py": "# File contents", - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - // Open project_root/dir_1 to ensure that a nested directory is expanded - toggle_expand_dir(&panel, "project_root/dir_1", cx); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root", - " v dir_1 <== selected", - " > nested_dir", - " file_1.py", - " file_2.py", - " file_3.py", - " > dir_2", - ] - ); - - // Close root directory - toggle_expand_dir(&panel, "project_root", cx); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["> project_root <== selected"] - ); - - // Run collapse_all_entries and make sure root is not expanded - panel.update_in(cx, |panel, window, cx| { - panel.collapse_all_entries(&CollapseAllEntries, window, cx) - }); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["> project_root <== selected"] - ); -} - -#[gpui::test] -async fn test_new_file_move(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.as_fake().insert_tree(path!("/root"), json!({})).await; - let project = Project::test(fs, [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - // Make a new buffer with no backing file - workspace - .update(cx, |workspace, window, cx| { - Editor::new_file(workspace, &Default::default(), window, cx) - }) - .unwrap(); - - cx.executor().run_until_parked(); - - // "Save as" the buffer, creating a new backing file for it - let save_task = workspace - .update(cx, |workspace, window, cx| { - workspace.save_active_item(workspace::SaveIntent::Save, window, cx) - }) - .unwrap(); - - cx.executor().run_until_parked(); - cx.simulate_new_path_selection(|_| Some(PathBuf::from(path!("/root/new")))); - save_task.await.unwrap(); - - // Rename the file - select_path(&panel, "root/new", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["v root", " new <== selected <== marked"] - ); - panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx)); - panel.update_in(cx, |panel, window, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("newer", window, cx)); - }); - panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx)); - - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["v root", " newer <== selected"] - ); - - workspace - .update(cx, |workspace, window, cx| { - workspace.save_active_item(workspace::SaveIntent::Save, window, cx) - }) - .unwrap() - .await - .unwrap(); - - cx.executor().run_until_parked(); - // assert that saving the file doesn't restore "new" - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["v root", " newer <== selected"] - ); -} - -// NOTE: This test is skipped on Windows, because on Windows, unlike on Unix, -// you can't rename a directory which some program has already open. This is a -// limitation of the Windows. Since Zed will have the root open, it will hold an open handle -// to it, and thus renaming it will fail on Windows. -// See: https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder -// See: https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/ns-ntifs-_file_rename_information -#[gpui::test] -#[cfg_attr(target_os = "windows", ignore)] -async fn test_rename_root_of_worktree(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root1", - json!({ - "dir1": { - "file1.txt": "content 1", - }, - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "root1/dir1", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &["v root1", " v dir1 <== selected", " file1.txt",], - "Initial state with worktrees" - ); - - select_path(&panel, "root1", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &["v root1 <== selected", " v dir1", " file1.txt",], - ); - - // Rename root1 to new_root1 - panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx)); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v [EDITOR: 'root1'] <== selected", - " v dir1", - " file1.txt", - ], - ); - - let confirm = panel.update_in(cx, |panel, window, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("new_root1", window, cx)); - panel.confirm_edit(true, window, cx).unwrap() - }); - confirm.await.unwrap(); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v new_root1 <== selected", - " v dir1", - " file1.txt", - ], - "Should update worktree name" - ); - - // Ensure internal paths have been updated - select_path(&panel, "new_root1/dir1/file1.txt", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v new_root1", - " v dir1", - " file1.txt <== selected", - ], - "Files in renamed worktree are selectable" - ); -} - -#[gpui::test] -async fn test_rename_with_hide_root(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root1", - json!({ - "dir1": { "file1.txt": "content" }, - "file2.txt": "content", - }), - ) - .await; - fs.insert_tree("/root2", json!({ "file3.txt": "content" })) - .await; - - // Test 1: Single worktree, hide_root=true - rename should be blocked - { - let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_root: true, - ..settings - }, - cx, - ); - }); - - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - panel.update(cx, |panel, cx| { - let project = panel.project.read(cx); - let worktree = project.visible_worktrees(cx).next().unwrap(); - let root_entry = worktree.read(cx).root_entry().unwrap(); - panel.state.selection = Some(SelectedEntry { - worktree_id: worktree.read(cx).id(), - entry_id: root_entry.id, - }); - }); - - panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx)); - - assert!( - panel.read_with(cx, |panel, _| panel.state.edit_state.is_none()), - "Rename should be blocked when hide_root=true with single worktree" - ); - } - - // Test 2: Multiple worktrees, hide_root=true - rename should work - { - let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_root: true, - ..settings - }, - cx, - ); - }); - - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - select_path(&panel, "root1", cx); - panel.update_in(cx, |panel, window, cx| panel.rename(&Rename, window, cx)); - - #[cfg(target_os = "windows")] - assert!( - panel.read_with(cx, |panel, _| panel.state.edit_state.is_none()), - "Rename should be blocked on Windows even with multiple worktrees" - ); - - #[cfg(not(target_os = "windows"))] - { - assert!( - panel.read_with(cx, |panel, _| panel.state.edit_state.is_some()), - "Rename should work with multiple worktrees on non-Windows when hide_root=true" - ); - panel.update_in(cx, |panel, window, cx| { - panel.cancel(&menu::Cancel, window, cx) - }); - } - } -} - -#[gpui::test] -async fn test_multiple_marked_entries(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/project_root", - json!({ - "dir_1": { - "nested_dir": { - "file_a.py": "# File contents", - } - }, - "file_1.py": "# File contents", - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let worktree_id = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().read(cx).id()); - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - cx.update(|window, cx| { - panel.update(cx, |this, cx| { - this.select_next(&Default::default(), window, cx); - this.expand_selected_entry(&Default::default(), window, cx); - }) - }); - cx.run_until_parked(); - - cx.update(|window, cx| { - panel.update(cx, |this, cx| { - this.expand_selected_entry(&Default::default(), window, cx); - }) - }); - cx.run_until_parked(); - - cx.update(|window, cx| { - panel.update(cx, |this, cx| { - this.select_next(&Default::default(), window, cx); - this.expand_selected_entry(&Default::default(), window, cx); - }) - }); - cx.run_until_parked(); - - cx.update(|window, cx| { - panel.update(cx, |this, cx| { - this.select_next(&Default::default(), window, cx); - }) - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root", - " v dir_1", - " v nested_dir", - " file_a.py <== selected", - " file_1.py", - ] - ); - let modifiers_with_shift = gpui::Modifiers { - shift: true, - ..Default::default() - }; - cx.run_until_parked(); - cx.simulate_modifiers_change(modifiers_with_shift); - cx.update(|window, cx| { - panel.update(cx, |this, cx| { - this.select_next(&Default::default(), window, cx); - }) - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root", - " v dir_1", - " v nested_dir", - " file_a.py", - " file_1.py <== selected <== marked", - ] - ); - cx.update(|window, cx| { - panel.update(cx, |this, cx| { - this.select_previous(&Default::default(), window, cx); - }) - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root", - " v dir_1", - " v nested_dir", - " file_a.py <== selected <== marked", - " file_1.py <== marked", - ] - ); - cx.update(|window, cx| { - panel.update(cx, |this, cx| { - let drag = DraggedSelection { - active_selection: this.state.selection.unwrap(), - marked_selections: this.marked_entries.clone().into(), - }; - let target_entry = this - .project - .read(cx) - .entry_for_path(&(worktree_id, rel_path("")).into(), cx) - .unwrap(); - this.drag_onto(&drag, target_entry.id, false, window, cx); - }); - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root", - " v dir_1", - " v nested_dir", - " file_1.py <== marked", - " file_a.py <== selected <== marked", - ] - ); - // ESC clears out all marks - cx.update(|window, cx| { - panel.update(cx, |this, cx| { - this.cancel(&menu::Cancel, window, cx); - }) - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root", - " v dir_1", - " v nested_dir", - " file_1.py", - " file_a.py <== selected", - ] - ); - // ESC clears out all marks - cx.update(|window, cx| { - panel.update(cx, |this, cx| { - this.select_previous(&SelectPrevious, window, cx); - this.select_next(&SelectNext, window, cx); - }) - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root", - " v dir_1", - " v nested_dir", - " file_1.py <== marked", - " file_a.py <== selected <== marked", - ] - ); - cx.simulate_modifiers_change(Default::default()); - cx.update(|window, cx| { - panel.update(cx, |this, cx| { - this.cut(&Cut, window, cx); - this.select_previous(&SelectPrevious, window, cx); - this.select_previous(&SelectPrevious, window, cx); - - this.paste(&Paste, window, cx); - this.update_visible_entries(None, false, false, window, cx); - }) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root", - " v dir_1", - " v nested_dir", - " file_1.py <== marked", - " file_a.py <== selected <== marked", - ] - ); - cx.simulate_modifiers_change(modifiers_with_shift); - cx.update(|window, cx| { - panel.update(cx, |this, cx| { - this.expand_selected_entry(&Default::default(), window, cx); - this.select_next(&SelectNext, window, cx); - this.select_next(&SelectNext, window, cx); - }) - }); - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root", - " v dir_1", - " v nested_dir <== selected", - ] - ); -} - -#[gpui::test] -async fn test_dragged_selection_resolve_entry(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "a": { - "b": { - "c": { - "d": {} - } - } - }, - "target_destination": {} - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - auto_fold_dirs: true, - ..settings - }, - cx, - ); - }); - - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - // Case 1: Move last dir 'd' - should move only 'd', leaving 'a/b/c' - select_path(&panel, "root/a/b/c/d", cx); - panel.update_in(cx, |panel, window, cx| { - let drag = DraggedSelection { - active_selection: SelectedEntry { - worktree_id: panel.state.selection.as_ref().unwrap().worktree_id, - entry_id: panel.resolve_entry(panel.state.selection.as_ref().unwrap().entry_id), - }, - marked_selections: Arc::new([*panel.state.selection.as_ref().unwrap()]), - }; - let target_entry = panel - .project - .read(cx) - .visible_worktrees(cx) - .next() - .unwrap() - .read(cx) - .entry_for_path(rel_path("target_destination")) - .unwrap(); - panel.drag_onto(&drag, target_entry.id, false, window, cx); - }); - cx.executor().run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root", - " > a/b/c", - " > target_destination/d <== selected" - ], - "Moving last empty directory 'd' should leave 'a/b/c' and move only 'd'" - ); - - // Reset - select_path(&panel, "root/target_destination/d", cx); - panel.update_in(cx, |panel, window, cx| { - let drag = DraggedSelection { - active_selection: SelectedEntry { - worktree_id: panel.state.selection.as_ref().unwrap().worktree_id, - entry_id: panel.resolve_entry(panel.state.selection.as_ref().unwrap().entry_id), - }, - marked_selections: Arc::new([*panel.state.selection.as_ref().unwrap()]), - }; - let target_entry = panel - .project - .read(cx) - .visible_worktrees(cx) - .next() - .unwrap() - .read(cx) - .entry_for_path(rel_path("a/b/c")) - .unwrap(); - panel.drag_onto(&drag, target_entry.id, false, window, cx); - }); - cx.executor().run_until_parked(); - - // Case 2: Move middle dir 'b' - should move 'b/c/d', leaving only 'a' - select_path(&panel, "root/a/b", cx); - panel.update_in(cx, |panel, window, cx| { - let drag = DraggedSelection { - active_selection: SelectedEntry { - worktree_id: panel.state.selection.as_ref().unwrap().worktree_id, - entry_id: panel.resolve_entry(panel.state.selection.as_ref().unwrap().entry_id), - }, - marked_selections: Arc::new([*panel.state.selection.as_ref().unwrap()]), - }; - let target_entry = panel - .project - .read(cx) - .visible_worktrees(cx) - .next() - .unwrap() - .read(cx) - .entry_for_path(rel_path("target_destination")) - .unwrap(); - panel.drag_onto(&drag, target_entry.id, false, window, cx); - }); - cx.executor().run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["v root", " v a", " > target_destination/b/c/d"], - "Moving middle directory 'b' should leave only 'a' and move 'b/c/d'" - ); - - // Reset - select_path(&panel, "root/target_destination/b", cx); - panel.update_in(cx, |panel, window, cx| { - let drag = DraggedSelection { - active_selection: SelectedEntry { - worktree_id: panel.state.selection.as_ref().unwrap().worktree_id, - entry_id: panel.resolve_entry(panel.state.selection.as_ref().unwrap().entry_id), - }, - marked_selections: Arc::new([*panel.state.selection.as_ref().unwrap()]), - }; - let target_entry = panel - .project - .read(cx) - .visible_worktrees(cx) - .next() - .unwrap() - .read(cx) - .entry_for_path(rel_path("a")) - .unwrap(); - panel.drag_onto(&drag, target_entry.id, false, window, cx); - }); - cx.executor().run_until_parked(); - - // Case 3: Move first dir 'a' - should move whole 'a/b/c/d' - select_path(&panel, "root/a", cx); - panel.update_in(cx, |panel, window, cx| { - let drag = DraggedSelection { - active_selection: SelectedEntry { - worktree_id: panel.state.selection.as_ref().unwrap().worktree_id, - entry_id: panel.resolve_entry(panel.state.selection.as_ref().unwrap().entry_id), - }, - marked_selections: Arc::new([*panel.state.selection.as_ref().unwrap()]), - }; - let target_entry = panel - .project - .read(cx) - .visible_worktrees(cx) - .next() - .unwrap() - .read(cx) - .entry_for_path(rel_path("target_destination")) - .unwrap(); - panel.drag_onto(&drag, target_entry.id, false, window, cx); - }); - cx.executor().run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["v root", " > target_destination/a/b/c/d"], - "Moving first directory 'a' should move whole 'a/b/c/d' chain" - ); -} - -#[gpui::test] -async fn test_drag_entries_between_different_worktrees(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root_a", - json!({ - "src": { - "lib.rs": "", - "main.rs": "" - }, - "docs": { - "guide.md": "" - }, - "multi": { - "alpha.txt": "", - "beta.txt": "" - } - }), - ) - .await; - fs.insert_tree( - "/root_b", - json!({ - "dst": { - "existing.md": "" - }, - "target.txt": "" - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root_a".as_ref(), "/root_b".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - // Case 1: move a file onto a directory in another worktree. - select_path(&panel, "root_a/src/main.rs", cx); - drag_selection_to(&panel, "root_b/dst", false, cx); - assert!( - find_project_entry(&panel, "root_b/dst/main.rs", cx).is_some(), - "Dragged file should appear under destination worktree" - ); - assert_eq!( - find_project_entry(&panel, "root_a/src/main.rs", cx), - None, - "Dragged file should be removed from the source worktree" - ); - - // Case 2: drop a file onto another worktree file so it lands in the parent directory. - select_path(&panel, "root_a/docs/guide.md", cx); - drag_selection_to(&panel, "root_b/dst/existing.md", true, cx); - assert!( - find_project_entry(&panel, "root_b/dst/guide.md", cx).is_some(), - "Dropping onto a file should place the entry beside the target file" - ); - assert_eq!( - find_project_entry(&panel, "root_a/docs/guide.md", cx), - None, - "Source file should be removed after the move" - ); - - // Case 3: move an entire directory. - select_path(&panel, "root_a/src", cx); - drag_selection_to(&panel, "root_b/dst", false, cx); - assert!( - find_project_entry(&panel, "root_b/dst/src/lib.rs", cx).is_some(), - "Dragging a directory should move its nested contents" - ); - assert_eq!( - find_project_entry(&panel, "root_a/src", cx), - None, - "Directory should no longer exist in the source worktree" - ); - - // Case 4: multi-selection drag between worktrees. - panel.update(cx, |panel, _| panel.marked_entries.clear()); - select_path_with_mark(&panel, "root_a/multi/alpha.txt", cx); - select_path_with_mark(&panel, "root_a/multi/beta.txt", cx); - drag_selection_to(&panel, "root_b/dst", false, cx); - assert!( - find_project_entry(&panel, "root_b/dst/alpha.txt", cx).is_some() - && find_project_entry(&panel, "root_b/dst/beta.txt", cx).is_some(), - "All marked entries should move to the destination worktree" - ); - assert_eq!( - find_project_entry(&panel, "root_a/multi/alpha.txt", cx), - None, - "Marked entries should be removed from the origin worktree" - ); - assert_eq!( - find_project_entry(&panel, "root_a/multi/beta.txt", cx), - None, - "Marked entries should be removed from the origin worktree" - ); -} - -#[gpui::test] -async fn test_autoreveal_and_gitignored_files(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(Vec::new()); - settings - .project_panel - .get_or_insert_default() - .auto_reveal_entries = Some(false); - }); - }) - }); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/project_root", - json!({ - ".git": {}, - ".gitignore": "**/gitignored_dir", - "dir_1": { - "file_1.py": "# File 1_1 contents", - "file_2.py": "# File 1_2 contents", - "file_3.py": "# File 1_3 contents", - "gitignored_dir": { - "file_a.py": "# File contents", - "file_b.py": "# File contents", - "file_c.py": "# File contents", - }, - }, - "dir_2": { - "file_1.py": "# File 2_1 contents", - "file_2.py": "# File 2_2 contents", - "file_3.py": "# File 2_3 contents", - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " > dir_1", - " > dir_2", - " .gitignore", - ] - ); - - let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx) - .expect("dir 1 file is not ignored and should have an entry"); - let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx) - .expect("dir 2 file is not ignored and should have an entry"); - let gitignored_dir_file = - find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx); - assert_eq!( - gitignored_dir_file, None, - "File in the gitignored dir should not have an entry before its dir is toggled" - ); - - toggle_expand_dir(&panel, "project_root/dir_1", cx); - toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " v dir_1", - " v gitignored_dir <== selected", - " file_a.py", - " file_b.py", - " file_c.py", - " file_1.py", - " file_2.py", - " file_3.py", - " > dir_2", - " .gitignore", - ], - "Should show gitignored dir file list in the project panel" - ); - let gitignored_dir_file = - find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx) - .expect("after gitignored dir got opened, a file entry should be present"); - - toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx); - toggle_expand_dir(&panel, "project_root/dir_1", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " > dir_1 <== selected", - " > dir_2", - " .gitignore", - ], - "Should hide all dir contents again and prepare for the auto reveal test" - ); - - for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] { - panel.update(cx, |panel, cx| { - panel.project.update(cx, |_, cx| { - cx.emit(project::Event::ActiveEntryChanged(Some(file_entry))) - }) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " > dir_1 <== selected", - " > dir_2", - " .gitignore", - ], - "When no auto reveal is enabled, the selected entry should not be revealed in the project panel" - ); - } - - cx.update(|_, cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings - .project_panel - .get_or_insert_default() - .auto_reveal_entries = Some(true) - }); - }) - }); - - panel.update(cx, |panel, cx| { - panel.project.update(cx, |_, cx| { - cx.emit(project::Event::ActiveEntryChanged(Some(dir_1_file))) - }) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " v dir_1", - " > gitignored_dir", - " file_1.py <== selected <== marked", - " file_2.py", - " file_3.py", - " > dir_2", - " .gitignore", - ], - "When auto reveal is enabled, not ignored dir_1 entry should be revealed" - ); - - panel.update(cx, |panel, cx| { - panel.project.update(cx, |_, cx| { - cx.emit(project::Event::ActiveEntryChanged(Some(dir_2_file))) - }) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " v dir_1", - " > gitignored_dir", - " file_1.py", - " file_2.py", - " file_3.py", - " v dir_2", - " file_1.py <== selected <== marked", - " file_2.py", - " file_3.py", - " .gitignore", - ], - "When auto reveal is enabled, not ignored dir_2 entry should be revealed" - ); - - panel.update(cx, |panel, cx| { - panel.project.update(cx, |_, cx| { - cx.emit(project::Event::ActiveEntryChanged(Some( - gitignored_dir_file, - ))) - }) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " v dir_1", - " > gitignored_dir", - " file_1.py", - " file_2.py", - " file_3.py", - " v dir_2", - " file_1.py <== selected <== marked", - " file_2.py", - " file_3.py", - " .gitignore", - ], - "When auto reveal is enabled, a gitignored selected entry should not be revealed in the project panel" - ); - - panel.update(cx, |panel, cx| { - panel.project.update(cx, |_, cx| { - cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file)) - }) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " v dir_1", - " v gitignored_dir", - " file_a.py <== selected <== marked", - " file_b.py", - " file_c.py", - " file_1.py", - " file_2.py", - " file_3.py", - " v dir_2", - " file_1.py", - " file_2.py", - " file_3.py", - " .gitignore", - ], - "When a gitignored entry is explicitly revealed, it should be shown in the project tree" - ); -} - -#[gpui::test] -async fn test_gitignored_and_always_included(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(Vec::new()); - settings.project.worktree.file_scan_inclusions = - Some(vec!["always_included_but_ignored_dir/*".to_string()]); - settings - .project_panel - .get_or_insert_default() - .auto_reveal_entries = Some(false) - }); - }) - }); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/project_root", - json!({ - ".git": {}, - ".gitignore": "**/gitignored_dir\n/always_included_but_ignored_dir", - "dir_1": { - "file_1.py": "# File 1_1 contents", - "file_2.py": "# File 1_2 contents", - "file_3.py": "# File 1_3 contents", - "gitignored_dir": { - "file_a.py": "# File contents", - "file_b.py": "# File contents", - "file_c.py": "# File contents", - }, - }, - "dir_2": { - "file_1.py": "# File 2_1 contents", - "file_2.py": "# File 2_2 contents", - "file_3.py": "# File 2_3 contents", - }, - "always_included_but_ignored_dir": { - "file_a.py": "# File contents", - "file_b.py": "# File contents", - "file_c.py": "# File contents", - }, - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " > always_included_but_ignored_dir", - " > dir_1", - " > dir_2", - " .gitignore", - ] - ); - - let gitignored_dir_file = - find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx); - let always_included_but_ignored_dir_file = find_project_entry( - &panel, - "project_root/always_included_but_ignored_dir/file_a.py", - cx, - ) - .expect("file that is .gitignored but set to always be included should have an entry"); - assert_eq!( - gitignored_dir_file, None, - "File in the gitignored dir should not have an entry unless its directory is toggled" - ); - - toggle_expand_dir(&panel, "project_root/dir_1", cx); - cx.run_until_parked(); - cx.update(|_, cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings - .project_panel - .get_or_insert_default() - .auto_reveal_entries = Some(true) - }); - }) - }); - - panel.update(cx, |panel, cx| { - panel.project.update(cx, |_, cx| { - cx.emit(project::Event::ActiveEntryChanged(Some( - always_included_but_ignored_dir_file, - ))) - }) - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " v always_included_but_ignored_dir", - " file_a.py <== selected <== marked", - " file_b.py", - " file_c.py", - " v dir_1", - " > gitignored_dir", - " file_1.py", - " file_2.py", - " file_3.py", - " > dir_2", - " .gitignore", - ], - "When auto reveal is enabled, a gitignored but always included selected entry should be revealed in the project panel" - ); -} - -#[gpui::test] -async fn test_explicit_reveal(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(Vec::new()); - settings - .project_panel - .get_or_insert_default() - .auto_reveal_entries = Some(false) - }); - }) - }); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/project_root", - json!({ - ".git": {}, - ".gitignore": "**/gitignored_dir", - "dir_1": { - "file_1.py": "# File 1_1 contents", - "file_2.py": "# File 1_2 contents", - "file_3.py": "# File 1_3 contents", - "gitignored_dir": { - "file_a.py": "# File contents", - "file_b.py": "# File contents", - "file_c.py": "# File contents", - }, - }, - "dir_2": { - "file_1.py": "# File 2_1 contents", - "file_2.py": "# File 2_2 contents", - "file_3.py": "# File 2_3 contents", - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " > dir_1", - " > dir_2", - " .gitignore", - ] - ); - - let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx) - .expect("dir 1 file is not ignored and should have an entry"); - let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx) - .expect("dir 2 file is not ignored and should have an entry"); - let gitignored_dir_file = - find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx); - assert_eq!( - gitignored_dir_file, None, - "File in the gitignored dir should not have an entry before its dir is toggled" - ); - - toggle_expand_dir(&panel, "project_root/dir_1", cx); - toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " v dir_1", - " v gitignored_dir <== selected", - " file_a.py", - " file_b.py", - " file_c.py", - " file_1.py", - " file_2.py", - " file_3.py", - " > dir_2", - " .gitignore", - ], - "Should show gitignored dir file list in the project panel" - ); - let gitignored_dir_file = - find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx) - .expect("after gitignored dir got opened, a file entry should be present"); - - toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx); - toggle_expand_dir(&panel, "project_root/dir_1", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " > dir_1 <== selected", - " > dir_2", - " .gitignore", - ], - "Should hide all dir contents again and prepare for the explicit reveal test" - ); - - for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] { - panel.update(cx, |panel, cx| { - panel.project.update(cx, |_, cx| { - cx.emit(project::Event::ActiveEntryChanged(Some(file_entry))) - }) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " > dir_1 <== selected", - " > dir_2", - " .gitignore", - ], - "When no auto reveal is enabled, the selected entry should not be revealed in the project panel" - ); - } - - panel.update(cx, |panel, cx| { - panel.project.update(cx, |_, cx| { - cx.emit(project::Event::RevealInProjectPanel(dir_1_file)) - }) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " v dir_1", - " > gitignored_dir", - " file_1.py <== selected <== marked", - " file_2.py", - " file_3.py", - " > dir_2", - " .gitignore", - ], - "With no auto reveal, explicit reveal should show the dir_1 entry in the project panel" - ); - - panel.update(cx, |panel, cx| { - panel.project.update(cx, |_, cx| { - cx.emit(project::Event::RevealInProjectPanel(dir_2_file)) - }) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " v dir_1", - " > gitignored_dir", - " file_1.py", - " file_2.py", - " file_3.py", - " v dir_2", - " file_1.py <== selected <== marked", - " file_2.py", - " file_3.py", - " .gitignore", - ], - "With no auto reveal, explicit reveal should show the dir_2 entry in the project panel" - ); - - panel.update(cx, |panel, cx| { - panel.project.update(cx, |_, cx| { - cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file)) - }) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " v dir_1", - " v gitignored_dir", - " file_a.py <== selected <== marked", - " file_b.py", - " file_c.py", - " file_1.py", - " file_2.py", - " file_3.py", - " v dir_2", - " file_1.py", - " file_2.py", - " file_3.py", - " .gitignore", - ], - "With no auto reveal, explicit reveal should show the gitignored entry in the project panel" - ); -} - -#[gpui::test] -async fn test_creating_excluded_entries(cx: &mut gpui::TestAppContext) { - init_test(cx); - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = - Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]); - }); - }); - }); - - cx.update(|cx| { - register_project_item::(cx); - }); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root1", - json!({ - ".dockerignore": "", - ".git": { - "HEAD": "", - }, - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) - .unwrap(); - cx.run_until_parked(); - - select_path(&panel, "root1", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["v root1 <== selected", " .dockerignore",] - ); - workspace - .update(cx, |workspace, _, cx| { - assert!( - workspace.active_item(cx).is_none(), - "Should have no active items in the beginning" - ); - }) - .unwrap(); - - let excluded_file_path = ".git/COMMIT_EDITMSG"; - let excluded_dir_path = "excluded_dir"; - - panel.update_in(cx, |panel, window, cx| panel.new_file(&NewFile, window, cx)); - cx.run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - panel - .update_in(cx, |panel, window, cx| { - panel.filename_editor.update(cx, |editor, cx| { - editor.set_text(excluded_file_path, window, cx) - }); - panel.confirm_edit(true, window, cx).unwrap() - }) - .await - .unwrap(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..13, cx), - &["v root1", " .dockerignore"], - "Excluded dir should not be shown after opening a file in it" - ); - panel.update_in(cx, |panel, window, cx| { - assert!( - !panel.filename_editor.read(cx).is_focused(window), - "Should have closed the file name editor" - ); - }); - workspace - .update(cx, |workspace, _, cx| { - let active_entry_path = workspace - .active_item(cx) - .expect("should have opened and activated the excluded item") - .act_as::(cx) - .expect("should have opened the corresponding project item for the excluded item") - .read(cx) - .path - .clone(); - assert_eq!( - active_entry_path.path.as_ref(), - rel_path(excluded_file_path), - "Should open the excluded file" - ); - - assert!( - workspace.notification_ids().is_empty(), - "Should have no notifications after opening an excluded file" - ); - }) - .unwrap(); - assert!( - fs.is_file(Path::new("/root1/.git/COMMIT_EDITMSG")).await, - "Should have created the excluded file" - ); - - select_path(&panel, "root1", cx); - panel.update_in(cx, |panel, window, cx| { - panel.new_directory(&NewDirectory, window, cx) - }); - cx.run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - panel - .update_in(cx, |panel, window, cx| { - panel.filename_editor.update(cx, |editor, cx| { - editor.set_text(excluded_file_path, window, cx) - }); - panel.confirm_edit(true, window, cx).unwrap() - }) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..13, cx), - &["v root1", " .dockerignore"], - "Should not change the project panel after trying to create an excluded directorya directory with the same name as the excluded file" - ); - panel.update_in(cx, |panel, window, cx| { - assert!( - !panel.filename_editor.read(cx).is_focused(window), - "Should have closed the file name editor" - ); - }); - workspace - .update(cx, |workspace, _, cx| { - let notifications = workspace.notification_ids(); - assert_eq!( - notifications.len(), - 1, - "Should receive one notification with the error message" - ); - workspace.dismiss_notification(notifications.first().unwrap(), cx); - assert!(workspace.notification_ids().is_empty()); - }) - .unwrap(); - - select_path(&panel, "root1", cx); - panel.update_in(cx, |panel, window, cx| { - panel.new_directory(&NewDirectory, window, cx) - }); - cx.run_until_parked(); - - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - - panel - .update_in(cx, |panel, window, cx| { - panel.filename_editor.update(cx, |editor, cx| { - editor.set_text(excluded_dir_path, window, cx) - }); - panel.confirm_edit(true, window, cx).unwrap() - }) - .await - .unwrap(); - - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..13, cx), - &["v root1", " .dockerignore"], - "Should not change the project panel after trying to create an excluded directory" - ); - panel.update_in(cx, |panel, window, cx| { - assert!( - !panel.filename_editor.read(cx).is_focused(window), - "Should have closed the file name editor" - ); - }); - workspace - .update(cx, |workspace, _, cx| { - let notifications = workspace.notification_ids(); - assert_eq!( - notifications.len(), - 1, - "Should receive one notification explaining that no directory is actually shown" - ); - workspace.dismiss_notification(notifications.first().unwrap(), cx); - assert!(workspace.notification_ids().is_empty()); - }) - .unwrap(); - assert!( - fs.is_dir(Path::new("/root1/excluded_dir")).await, - "Should have created the excluded directory" - ); -} - -#[gpui::test] -async fn test_selection_restored_when_creation_cancelled(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/src", - json!({ - "test": { - "first.rs": "// First Rust file", - "second.rs": "// Second Rust file", - "third.rs": "// Third Rust file", - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) - .unwrap(); - cx.run_until_parked(); - - select_path(&panel, "src", cx); - panel.update_in(cx, |panel, window, cx| panel.confirm(&Confirm, window, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - // - "v src <== selected", - " > test" - ] - ); - panel.update_in(cx, |panel, window, cx| { - panel.new_directory(&NewDirectory, window, cx) - }); - cx.executor().run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - // - "v src", - " > [EDITOR: ''] <== selected", - " > test" - ] - ); - - panel.update_in(cx, |panel, window, cx| { - panel.cancel(&menu::Cancel, window, cx); - panel.update_visible_entries(None, false, false, window, cx); - }); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - // - "v src <== selected", - " > test" - ] - ); -} - -#[gpui::test] -async fn test_basic_file_deletion_scenarios(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "dir1": { - "subdir1": {}, - "file1.txt": "", - "file2.txt": "", - }, - "dir2": { - "subdir2": {}, - "file3.txt": "", - "file4.txt": "", - }, - "file5.txt": "", - "file6.txt": "", - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "root/dir1", cx); - toggle_expand_dir(&panel, "root/dir2", cx); - - // Test Case 1: Delete middle file in directory - select_path(&panel, "root/dir1/file1.txt", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v root", - " v dir1", - " > subdir1", - " file1.txt <== selected", - " file2.txt", - " v dir2", - " > subdir2", - " file3.txt", - " file4.txt", - " file5.txt", - " file6.txt", - ], - "Initial state before deleting middle file" - ); - - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v root", - " v dir1", - " > subdir1", - " file2.txt <== selected", - " v dir2", - " > subdir2", - " file3.txt", - " file4.txt", - " file5.txt", - " file6.txt", - ], - "Should select next file after deleting middle file" - ); - - // Test Case 2: Delete last file in directory - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v root", - " v dir1", - " > subdir1 <== selected", - " v dir2", - " > subdir2", - " file3.txt", - " file4.txt", - " file5.txt", - " file6.txt", - ], - "Should select next directory when last file is deleted" - ); - - // Test Case 3: Delete root level file - select_path(&panel, "root/file6.txt", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v root", - " v dir1", - " > subdir1", - " v dir2", - " > subdir2", - " file3.txt", - " file4.txt", - " file5.txt", - " file6.txt <== selected", - ], - "Initial state before deleting root level file" - ); - - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v root", - " v dir1", - " > subdir1", - " v dir2", - " > subdir2", - " file3.txt", - " file4.txt", - " file5.txt <== selected", - ], - "Should select prev entry at root level" - ); -} - -#[gpui::test] -async fn test_deletion_gitignored(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - "aa": "// Testing 1", - "bb": "// Testing 2", - "cc": "// Testing 3", - "dd": "// Testing 4", - "ee": "// Testing 5", - "ff": "// Testing 6", - "gg": "// Testing 7", - "hh": "// Testing 8", - "ii": "// Testing 8", - ".gitignore": "bb\ndd\nee\nff\nii\n'", - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - // Test 1: Auto selection with one gitignored file next to the deleted file - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_gitignore: true, - ..settings - }, - cx, - ); - }); - - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - select_path(&panel, "root/aa", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root", - " .gitignore", - " aa <== selected", - " cc", - " gg", - " hh" - ], - "Initial state should hide files on .gitignore" - ); - - submit_deletion(&panel, cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root", - " .gitignore", - " cc <== selected", - " gg", - " hh" - ], - "Should select next entry not on .gitignore" - ); - - // Test 2: Auto selection with many gitignored files next to the deleted file - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root", - " .gitignore", - " gg <== selected", - " hh" - ], - "Should select next entry not on .gitignore" - ); - - // Test 3: Auto selection of entry before deleted file - select_path(&panel, "root/hh", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root", - " .gitignore", - " gg", - " hh <== selected" - ], - "Should select next entry not on .gitignore" - ); - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["v root", " .gitignore", " gg <== selected"], - "Should select next entry not on .gitignore" - ); -} - -#[gpui::test] -async fn test_nested_deletion_gitignore(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - "dir1": { - "file1": "// Testing", - "file2": "// Testing", - "file3": "// Testing" - }, - "aa": "// Testing", - ".gitignore": "file1\nfile3\n", - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_gitignore: true, - ..settings - }, - cx, - ); - }); - - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - // Test 1: Visible items should exclude files on gitignore - toggle_expand_dir(&panel, "root/dir1", cx); - select_path(&panel, "root/dir1/file2", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root", - " v dir1", - " file2 <== selected", - " .gitignore", - " aa" - ], - "Initial state should hide files on .gitignore" - ); - submit_deletion(&panel, cx); - - // Test 2: Auto selection should go to the parent - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root", - " v dir1 <== selected", - " .gitignore", - " aa" - ], - "Initial state should hide files on .gitignore" - ); -} - -#[gpui::test] -async fn test_complex_selection_scenarios(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "dir1": { - "subdir1": { - "a.txt": "", - "b.txt": "" - }, - "file1.txt": "", - }, - "dir2": { - "subdir2": { - "c.txt": "", - "d.txt": "" - }, - "file2.txt": "", - }, - "file3.txt": "", - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "root/dir1", cx); - toggle_expand_dir(&panel, "root/dir1/subdir1", cx); - toggle_expand_dir(&panel, "root/dir2", cx); - toggle_expand_dir(&panel, "root/dir2/subdir2", cx); - - // Test Case 1: Select and delete nested directory with parent - cx.simulate_modifiers_change(gpui::Modifiers { - control: true, - ..Default::default() - }); - select_path_with_mark(&panel, "root/dir1/subdir1", cx); - select_path_with_mark(&panel, "root/dir1", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v root", - " v dir1 <== selected <== marked", - " v subdir1 <== marked", - " a.txt", - " b.txt", - " file1.txt", - " v dir2", - " v subdir2", - " c.txt", - " d.txt", - " file2.txt", - " file3.txt", - ], - "Initial state before deleting nested directory with parent" - ); - - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v root", - " v dir2 <== selected", - " v subdir2", - " c.txt", - " d.txt", - " file2.txt", - " file3.txt", - ], - "Should select next directory after deleting directory with parent" - ); - - // Test Case 2: Select mixed files and directories across levels - select_path_with_mark(&panel, "root/dir2/subdir2/c.txt", cx); - select_path_with_mark(&panel, "root/dir2/file2.txt", cx); - select_path_with_mark(&panel, "root/file3.txt", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v root", - " v dir2", - " v subdir2", - " c.txt <== marked", - " d.txt", - " file2.txt <== marked", - " file3.txt <== selected <== marked", - ], - "Initial state before deleting" - ); - - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v root", - " v dir2 <== selected", - " v subdir2", - " d.txt", - ], - "Should select sibling directory" - ); -} - -#[gpui::test] -async fn test_delete_all_files_and_directories(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "dir1": { - "subdir1": { - "a.txt": "", - "b.txt": "" - }, - "file1.txt": "", - }, - "dir2": { - "subdir2": { - "c.txt": "", - "d.txt": "" - }, - "file2.txt": "", - }, - "file3.txt": "", - "file4.txt": "", - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "root/dir1", cx); - toggle_expand_dir(&panel, "root/dir1/subdir1", cx); - toggle_expand_dir(&panel, "root/dir2", cx); - toggle_expand_dir(&panel, "root/dir2/subdir2", cx); - - // Test Case 1: Select all root files and directories - cx.simulate_modifiers_change(gpui::Modifiers { - control: true, - ..Default::default() - }); - select_path_with_mark(&panel, "root/dir1", cx); - select_path_with_mark(&panel, "root/dir2", cx); - select_path_with_mark(&panel, "root/file3.txt", cx); - select_path_with_mark(&panel, "root/file4.txt", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root", - " v dir1 <== marked", - " v subdir1", - " a.txt", - " b.txt", - " file1.txt", - " v dir2 <== marked", - " v subdir2", - " c.txt", - " d.txt", - " file2.txt", - " file3.txt <== marked", - " file4.txt <== selected <== marked", - ], - "State before deleting all contents" - ); - - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &["v root <== selected"], - "Only empty root directory should remain after deleting all contents" - ); -} - -#[gpui::test] -async fn test_nested_selection_deletion(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "dir1": { - "subdir1": { - "file_a.txt": "content a", - "file_b.txt": "content b", - }, - "subdir2": { - "file_c.txt": "content c", - }, - "file1.txt": "content 1", - }, - "dir2": { - "file2.txt": "content 2", - }, - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "root/dir1", cx); - toggle_expand_dir(&panel, "root/dir1/subdir1", cx); - toggle_expand_dir(&panel, "root/dir2", cx); - cx.simulate_modifiers_change(gpui::Modifiers { - control: true, - ..Default::default() - }); - - // Test Case 1: Select parent directory, subdirectory, and a file inside the subdirectory - select_path_with_mark(&panel, "root/dir1", cx); - select_path_with_mark(&panel, "root/dir1/subdir1", cx); - select_path_with_mark(&panel, "root/dir1/subdir1/file_a.txt", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root", - " v dir1 <== marked", - " v subdir1 <== marked", - " file_a.txt <== selected <== marked", - " file_b.txt", - " > subdir2", - " file1.txt", - " v dir2", - " file2.txt", - ], - "State with parent dir, subdir, and file selected" - ); - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &["v root", " v dir2 <== selected", " file2.txt",], - "Only dir2 should remain after deletion" - ); -} - -#[gpui::test] -async fn test_multiple_worktrees_deletion(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - // First worktree - fs.insert_tree( - "/root1", - json!({ - "dir1": { - "file1.txt": "content 1", - "file2.txt": "content 2", - }, - "dir2": { - "file3.txt": "content 3", - }, - }), - ) - .await; - - // Second worktree - fs.insert_tree( - "/root2", - json!({ - "dir3": { - "file4.txt": "content 4", - "file5.txt": "content 5", - }, - "file6.txt": "content 6", - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - // Expand all directories for testing - toggle_expand_dir(&panel, "root1/dir1", cx); - toggle_expand_dir(&panel, "root1/dir2", cx); - toggle_expand_dir(&panel, "root2/dir3", cx); - - // Test Case 1: Delete files across different worktrees - cx.simulate_modifiers_change(gpui::Modifiers { - control: true, - ..Default::default() - }); - select_path_with_mark(&panel, "root1/dir1/file1.txt", cx); - select_path_with_mark(&panel, "root2/dir3/file4.txt", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root1", - " v dir1", - " file1.txt <== marked", - " file2.txt", - " v dir2", - " file3.txt", - "v root2", - " v dir3", - " file4.txt <== selected <== marked", - " file5.txt", - " file6.txt", - ], - "Initial state with files selected from different worktrees" - ); - - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root1", - " v dir1", - " file2.txt", - " v dir2", - " file3.txt", - "v root2", - " v dir3", - " file5.txt <== selected", - " file6.txt", - ], - "Should select next file in the last worktree after deletion" - ); - - // Test Case 2: Delete directories from different worktrees - select_path_with_mark(&panel, "root1/dir1", cx); - select_path_with_mark(&panel, "root2/dir3", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root1", - " v dir1 <== marked", - " file2.txt", - " v dir2", - " file3.txt", - "v root2", - " v dir3 <== selected <== marked", - " file5.txt", - " file6.txt", - ], - "State with directories marked from different worktrees" - ); - - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root1", - " v dir2", - " file3.txt", - "v root2", - " file6.txt <== selected", - ], - "Should select remaining file in last worktree after directory deletion" - ); - - // Test Case 4: Delete all remaining files except roots - select_path_with_mark(&panel, "root1/dir2/file3.txt", cx); - select_path_with_mark(&panel, "root2/file6.txt", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root1", - " v dir2", - " file3.txt <== marked", - "v root2", - " file6.txt <== selected <== marked", - ], - "State with all remaining files marked" - ); - - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &["v root1", " v dir2", "v root2 <== selected"], - "Second parent root should be selected after deleting" - ); -} - -#[gpui::test] -async fn test_selection_vs_marked_entries_priority(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "dir1": { - "file1.txt": "", - "file2.txt": "", - "file3.txt": "", - }, - "dir2": { - "file4.txt": "", - "file5.txt": "", - }, - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "root/dir1", cx); - toggle_expand_dir(&panel, "root/dir2", cx); - - cx.simulate_modifiers_change(gpui::Modifiers { - control: true, - ..Default::default() - }); - - select_path_with_mark(&panel, "root/dir1/file2.txt", cx); - select_path(&panel, "root/dir1/file1.txt", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v root", - " v dir1", - " file1.txt <== selected", - " file2.txt <== marked", - " file3.txt", - " v dir2", - " file4.txt", - " file5.txt", - ], - "Initial state with one marked entry and different selection" - ); - - // Delete should operate on the selected entry (file1.txt) - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v root", - " v dir1", - " file2.txt <== selected <== marked", - " file3.txt", - " v dir2", - " file4.txt", - " file5.txt", - ], - "Should delete selected file, not marked file" - ); - - select_path_with_mark(&panel, "root/dir1/file3.txt", cx); - select_path_with_mark(&panel, "root/dir2/file4.txt", cx); - select_path(&panel, "root/dir2/file5.txt", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v root", - " v dir1", - " file2.txt <== marked", - " file3.txt <== marked", - " v dir2", - " file4.txt <== marked", - " file5.txt <== selected", - ], - "Initial state with multiple marked entries and different selection" - ); - - // Delete should operate on all marked entries, ignoring the selection - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..15, cx), - &[ - "v root", - " v dir1", - " v dir2", - " file5.txt <== selected", - ], - "Should delete all marked files, leaving only the selected file" - ); -} - -#[gpui::test] -async fn test_selection_fallback_to_next_highest_worktree(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root_b", - json!({ - "dir1": { - "file1.txt": "content 1", - "file2.txt": "content 2", - }, - }), - ) - .await; - - fs.insert_tree( - "/root_c", - json!({ - "dir2": {}, - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root_b".as_ref(), "/root_c".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "root_b/dir1", cx); - toggle_expand_dir(&panel, "root_c/dir2", cx); - - cx.simulate_modifiers_change(gpui::Modifiers { - control: true, - ..Default::default() - }); - select_path_with_mark(&panel, "root_b/dir1/file1.txt", cx); - select_path_with_mark(&panel, "root_b/dir1/file2.txt", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root_b", - " v dir1", - " file1.txt <== marked", - " file2.txt <== selected <== marked", - "v root_c", - " v dir2", - ], - "Initial state with files marked in root_b" - ); - - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root_b", - " v dir1 <== selected", - "v root_c", - " v dir2", - ], - "After deletion in root_b as it's last deletion, selection should be in root_b" - ); - - select_path_with_mark(&panel, "root_c/dir2", cx); - - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &["v root_b", " v dir1", "v root_c <== selected",], - "After deleting from root_c, it should remain in root_c" - ); -} - -fn toggle_expand_dir(panel: &Entity, path: &str, cx: &mut VisualTestContext) { - let path = rel_path(path); - panel.update_in(cx, |panel, window, cx| { - for worktree in panel.project.read(cx).worktrees(cx).collect::>() { - let worktree = worktree.read(cx); - if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { - let entry_id = worktree.entry_for_path(relative_path).unwrap().id; - panel.toggle_expanded(entry_id, window, cx); - return; - } - } - panic!("no worktree for path {:?}", path); - }); - cx.run_until_parked(); -} - -#[gpui::test] -async fn test_expand_all_for_entry(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - ".gitignore": "**/ignored_dir\n**/ignored_nested", - "dir1": { - "empty1": { - "empty2": { - "empty3": { - "file.txt": "" - } - } - }, - "subdir1": { - "file1.txt": "", - "file2.txt": "", - "ignored_nested": { - "ignored_file.txt": "" - } - }, - "ignored_dir": { - "subdir": { - "deep_file.txt": "" - } - } - } - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - // Test 1: When auto-fold is enabled - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - auto_fold_dirs: true, - ..settings - }, - cx, - ); - }); - - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &["v root", " > dir1", " .gitignore",], - "Initial state should show collapsed root structure" - ); - - toggle_expand_dir(&panel, "root/dir1", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root", - " v dir1 <== selected", - " > empty1/empty2/empty3", - " > ignored_dir", - " > subdir1", - " .gitignore", - ], - "Should show first level with auto-folded dirs and ignored dir visible" - ); - - let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap(); - panel.update_in(cx, |panel, window, cx| { - let project = panel.project.read(cx); - let worktree = project.worktrees(cx).next().unwrap().read(cx); - panel.expand_all_for_entry(worktree.id(), entry_id, cx); - panel.update_visible_entries(None, false, false, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root", - " v dir1 <== selected", - " v empty1", - " v empty2", - " v empty3", - " file.txt", - " > ignored_dir", - " v subdir1", - " > ignored_nested", - " file1.txt", - " file2.txt", - " .gitignore", - ], - "After expand_all with auto-fold: should not expand ignored_dir, should expand folded dirs, and should not expand ignored_nested" - ); - - // Test 2: When auto-fold is disabled - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - auto_fold_dirs: false, - ..settings - }, - cx, - ); - }); - - panel.update_in(cx, |panel, window, cx| { - panel.collapse_all_entries(&CollapseAllEntries, window, cx); - }); - - toggle_expand_dir(&panel, "root/dir1", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root", - " v dir1 <== selected", - " > empty1", - " > ignored_dir", - " > subdir1", - " .gitignore", - ], - "With auto-fold disabled: should show all directories separately" - ); - - let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap(); - panel.update_in(cx, |panel, window, cx| { - let project = panel.project.read(cx); - let worktree = project.worktrees(cx).next().unwrap().read(cx); - panel.expand_all_for_entry(worktree.id(), entry_id, cx); - panel.update_visible_entries(None, false, false, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root", - " v dir1 <== selected", - " v empty1", - " v empty2", - " v empty3", - " file.txt", - " > ignored_dir", - " v subdir1", - " > ignored_nested", - " file1.txt", - " file2.txt", - " .gitignore", - ], - "After expand_all without auto-fold: should expand all dirs normally, \ - expand ignored_dir itself but not its subdirs, and not expand ignored_nested" - ); - - // Test 3: When explicitly called on ignored directory - let ignored_dir_entry = find_project_entry(&panel, "root/dir1/ignored_dir", cx).unwrap(); - panel.update_in(cx, |panel, window, cx| { - let project = panel.project.read(cx); - let worktree = project.worktrees(cx).next().unwrap().read(cx); - panel.expand_all_for_entry(worktree.id(), ignored_dir_entry, cx); - panel.update_visible_entries(None, false, false, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root", - " v dir1 <== selected", - " v empty1", - " v empty2", - " v empty3", - " file.txt", - " v ignored_dir", - " v subdir", - " deep_file.txt", - " v subdir1", - " > ignored_nested", - " file1.txt", - " file2.txt", - " .gitignore", - ], - "After expand_all on ignored_dir: should expand all contents of the ignored directory" - ); -} - -#[gpui::test] -async fn test_collapse_all_for_entry(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - "dir1": { - "subdir1": { - "nested1": { - "file1.txt": "", - "file2.txt": "" - }, - }, - "subdir2": { - "file4.txt": "" - } - }, - "dir2": { - "single_file": { - "file5.txt": "" - } - } - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - // Test 1: Basic collapsing - { - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "root/dir1", cx); - toggle_expand_dir(&panel, "root/dir1/subdir1", cx); - toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx); - toggle_expand_dir(&panel, "root/dir1/subdir2", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root", - " v dir1", - " v subdir1", - " v nested1", - " file1.txt", - " file2.txt", - " v subdir2 <== selected", - " file4.txt", - " > dir2", - ], - "Initial state with everything expanded" - ); - - let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap(); - panel.update_in(cx, |panel, window, cx| { - let project = panel.project.read(cx); - let worktree = project.worktrees(cx).next().unwrap().read(cx); - panel.collapse_all_for_entry(worktree.id(), entry_id, cx); - panel.update_visible_entries(None, false, false, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &["v root", " > dir1", " > dir2",], - "All subdirs under dir1 should be collapsed" - ); - } - - // Test 2: With auto-fold enabled - { - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - auto_fold_dirs: true, - ..settings - }, - cx, - ); - }); - - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "root/dir1", cx); - toggle_expand_dir(&panel, "root/dir1/subdir1", cx); - toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root", - " v dir1", - " v subdir1/nested1 <== selected", - " file1.txt", - " file2.txt", - " > subdir2", - " > dir2/single_file", - ], - "Initial state with some dirs expanded" - ); - - let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap(); - panel.update(cx, |panel, cx| { - let project = panel.project.read(cx); - let worktree = project.worktrees(cx).next().unwrap().read(cx); - panel.collapse_all_for_entry(worktree.id(), entry_id, cx); - }); - - toggle_expand_dir(&panel, "root/dir1", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root", - " v dir1 <== selected", - " > subdir1/nested1", - " > subdir2", - " > dir2/single_file", - ], - "Subdirs should be collapsed and folded with auto-fold enabled" - ); - } - - // Test 3: With auto-fold disabled - { - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - auto_fold_dirs: false, - ..settings - }, - cx, - ); - }); - - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "root/dir1", cx); - toggle_expand_dir(&panel, "root/dir1/subdir1", cx); - toggle_expand_dir(&panel, "root/dir1/subdir1/nested1", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root", - " v dir1", - " v subdir1", - " v nested1 <== selected", - " file1.txt", - " file2.txt", - " > subdir2", - " > dir2", - ], - "Initial state with some dirs expanded and auto-fold disabled" - ); - - let entry_id = find_project_entry(&panel, "root/dir1", cx).unwrap(); - panel.update(cx, |panel, cx| { - let project = panel.project.read(cx); - let worktree = project.worktrees(cx).next().unwrap().read(cx); - panel.collapse_all_for_entry(worktree.id(), entry_id, cx); - }); - - toggle_expand_dir(&panel, "root/dir1", cx); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root", - " v dir1 <== selected", - " > subdir1", - " > subdir2", - " > dir2", - ], - "Subdirs should be collapsed but not folded with auto-fold disabled" - ); - } -} - -#[gpui::test] -async fn test_create_entries_without_selection(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - "dir1": { - "file1.txt": "", - }, - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) - .unwrap(); - cx.run_until_parked(); - - #[rustfmt::skip] - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root", - " > dir1", - ], - "Initial state with nothing selected" - ); - - panel.update_in(cx, |panel, window, cx| { - panel.new_file(&NewFile, window, cx); - }); - cx.run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - panel - .update_in(cx, |panel, window, cx| { - panel.filename_editor.update(cx, |editor, cx| { - editor.set_text("hello_from_no_selections", window, cx) - }); - panel.confirm_edit(true, window, cx).unwrap() - }) - .await - .unwrap(); - cx.run_until_parked(); - #[rustfmt::skip] - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root", - " > dir1", - " hello_from_no_selections <== selected <== marked", - ], - "A new file is created under the root directory" - ); -} - -#[gpui::test] -async fn test_create_entries_without_selection_hide_root(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - "existing_dir": { - "existing_file.txt": "", - }, - "existing_file.txt": "", - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_root: true, - ..settings - }, - cx, - ); - }); - - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) - .unwrap(); - cx.run_until_parked(); - - #[rustfmt::skip] - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "> existing_dir", - " existing_file.txt", - ], - "Initial state with hide_root=true, root should be hidden and nothing selected" - ); - - panel.update(cx, |panel, _| { - assert!( - panel.state.selection.is_none(), - "Should have no selection initially" - ); - }); - - // Test 1: Create new file when no entry is selected - panel.update_in(cx, |panel, window, cx| { - panel.new_file(&NewFile, window, cx); - }); - cx.run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - cx.run_until_parked(); - #[rustfmt::skip] - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "> existing_dir", - " [EDITOR: ''] <== selected", - " existing_file.txt", - ], - "Editor should appear at root level when hide_root=true and no selection" - ); - - let confirm = panel.update_in(cx, |panel, window, cx| { - panel.filename_editor.update(cx, |editor, cx| { - editor.set_text("new_file_at_root.txt", window, cx) - }); - panel.confirm_edit(true, window, cx).unwrap() - }); - confirm.await.unwrap(); - cx.run_until_parked(); - - #[rustfmt::skip] - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "> existing_dir", - " existing_file.txt", - " new_file_at_root.txt <== selected <== marked", - ], - "New file should be created at root level and visible without root prefix" - ); - - assert!( - fs.is_file(Path::new("/root/new_file_at_root.txt")).await, - "File should be created in the actual root directory" - ); - - // Test 2: Create new directory when no entry is selected - panel.update(cx, |panel, _| { - panel.state.selection = None; - }); - - panel.update_in(cx, |panel, window, cx| { - panel.new_directory(&NewDirectory, window, cx); - }); - cx.run_until_parked(); - - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - - #[rustfmt::skip] - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "> [EDITOR: ''] <== selected", - "> existing_dir", - " existing_file.txt", - " new_file_at_root.txt", - ], - "Directory editor should appear at root level when hide_root=true and no selection" - ); - - let confirm = panel.update_in(cx, |panel, window, cx| { - panel.filename_editor.update(cx, |editor, cx| { - editor.set_text("new_dir_at_root", window, cx) - }); - panel.confirm_edit(true, window, cx).unwrap() - }); - confirm.await.unwrap(); - cx.run_until_parked(); - - #[rustfmt::skip] - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "> existing_dir", - "v new_dir_at_root <== selected", - " existing_file.txt", - " new_file_at_root.txt", - ], - "New directory should be created at root level and visible without root prefix" - ); - - assert!( - fs.is_dir(Path::new("/root/new_dir_at_root")).await, - "Directory should be created in the actual root directory" - ); -} - -#[cfg(windows)] -#[gpui::test] -async fn test_create_entry_with_trailing_dot_windows(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/root"), - json!({ - "dir1": { - "file1.txt": "", - }, - }), - ) - .await; - - let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - let panel = workspace - .update(cx, |workspace, window, cx| { - let panel = ProjectPanel::new(workspace, window, cx); - workspace.add_panel(panel.clone(), window, cx); - panel - }) - .unwrap(); - cx.run_until_parked(); - - #[rustfmt::skip] - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root", - " > dir1", - ], - "Initial state with nothing selected" - ); - - panel.update_in(cx, |panel, window, cx| { - panel.new_file(&NewFile, window, cx); - }); - cx.run_until_parked(); - panel.update_in(cx, |panel, window, cx| { - assert!(panel.filename_editor.read(cx).is_focused(window)); - }); - panel - .update_in(cx, |panel, window, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("foo.", window, cx)); - panel.confirm_edit(true, window, cx).unwrap() - }) - .await - .unwrap(); - cx.run_until_parked(); - #[rustfmt::skip] - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v root", - " > dir1", - " foo <== selected <== marked", - ], - "A new file is created under the root directory without the trailing dot" - ); -} - -#[gpui::test] -async fn test_highlight_entry_for_external_drag(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "dir1": { - "file1.txt": "", - "dir2": { - "file2.txt": "" - } - }, - "file3.txt": "" - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - panel.update(cx, |panel, cx| { - let project = panel.project.read(cx); - let worktree = project.visible_worktrees(cx).next().unwrap(); - let worktree = worktree.read(cx); - - // Test 1: Target is a directory, should highlight the directory itself - let dir_entry = worktree.entry_for_path(rel_path("dir1")).unwrap(); - let result = panel.highlight_entry_for_external_drag(dir_entry, worktree); - assert_eq!( - result, - Some(dir_entry.id), - "Should highlight directory itself" - ); - - // Test 2: Target is nested file, should highlight immediate parent - let nested_file = worktree - .entry_for_path(rel_path("dir1/dir2/file2.txt")) - .unwrap(); - let nested_parent = worktree.entry_for_path(rel_path("dir1/dir2")).unwrap(); - let result = panel.highlight_entry_for_external_drag(nested_file, worktree); - assert_eq!( - result, - Some(nested_parent.id), - "Should highlight immediate parent" - ); - - // Test 3: Target is root level file, should highlight root - let root_file = worktree.entry_for_path(rel_path("file3.txt")).unwrap(); - let result = panel.highlight_entry_for_external_drag(root_file, worktree); - assert_eq!( - result, - Some(worktree.root_entry().unwrap().id), - "Root level file should return None" - ); - - // Test 4: Target is root itself, should highlight root - let root_entry = worktree.root_entry().unwrap(); - let result = panel.highlight_entry_for_external_drag(root_entry, worktree); - assert_eq!( - result, - Some(root_entry.id), - "Root level file should return None" - ); - }); -} - -#[gpui::test] -async fn test_highlight_entry_for_selection_drag(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "parent_dir": { - "child_file.txt": "", - "sibling_file.txt": "", - "child_dir": { - "nested_file.txt": "" - } - }, - "other_dir": { - "other_file.txt": "" - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - panel.update(cx, |panel, cx| { - let project = panel.project.read(cx); - let worktree = project.visible_worktrees(cx).next().unwrap(); - let worktree_id = worktree.read(cx).id(); - let worktree = worktree.read(cx); - - let parent_dir = worktree.entry_for_path(rel_path("parent_dir")).unwrap(); - let child_file = worktree - .entry_for_path(rel_path("parent_dir/child_file.txt")) - .unwrap(); - let sibling_file = worktree - .entry_for_path(rel_path("parent_dir/sibling_file.txt")) - .unwrap(); - let child_dir = worktree - .entry_for_path(rel_path("parent_dir/child_dir")) - .unwrap(); - let other_dir = worktree.entry_for_path(rel_path("other_dir")).unwrap(); - let other_file = worktree - .entry_for_path(rel_path("other_dir/other_file.txt")) - .unwrap(); - - // Test 1: Single item drag, don't highlight parent directory - let dragged_selection = DraggedSelection { - active_selection: SelectedEntry { - worktree_id, - entry_id: child_file.id, - }, - marked_selections: Arc::new([SelectedEntry { - worktree_id, - entry_id: child_file.id, - }]), - }; - let result = - panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, cx); - assert_eq!(result, None, "Should not highlight parent of dragged item"); - - // Test 2: Single item drag, don't highlight sibling files - let result = panel.highlight_entry_for_selection_drag( - sibling_file, - worktree, - &dragged_selection, - cx, - ); - assert_eq!(result, None, "Should not highlight sibling files"); - - // Test 3: Single item drag, highlight unrelated directory - let result = - panel.highlight_entry_for_selection_drag(other_dir, worktree, &dragged_selection, cx); - assert_eq!( - result, - Some(other_dir.id), - "Should highlight unrelated directory" - ); - - // Test 4: Single item drag, highlight sibling directory - let result = - panel.highlight_entry_for_selection_drag(child_dir, worktree, &dragged_selection, cx); - assert_eq!( - result, - Some(child_dir.id), - "Should highlight sibling directory" - ); - - // Test 5: Multiple items drag, highlight parent directory - let dragged_selection = DraggedSelection { - active_selection: SelectedEntry { - worktree_id, - entry_id: child_file.id, - }, - marked_selections: Arc::new([ - SelectedEntry { - worktree_id, - entry_id: child_file.id, - }, - SelectedEntry { - worktree_id, - entry_id: sibling_file.id, - }, - ]), - }; - let result = - panel.highlight_entry_for_selection_drag(parent_dir, worktree, &dragged_selection, cx); - assert_eq!( - result, - Some(parent_dir.id), - "Should highlight parent with multiple items" - ); - - // Test 6: Target is file in different directory, highlight parent - let result = - panel.highlight_entry_for_selection_drag(other_file, worktree, &dragged_selection, cx); - assert_eq!( - result, - Some(other_dir.id), - "Should highlight parent of target file" - ); - - // Test 7: Target is directory, always highlight - let result = - panel.highlight_entry_for_selection_drag(child_dir, worktree, &dragged_selection, cx); - assert_eq!( - result, - Some(child_dir.id), - "Should always highlight directories" - ); - }); -} - -#[gpui::test] -async fn test_highlight_entry_for_selection_drag_cross_worktree(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root1", - json!({ - "src": { - "main.rs": "", - "lib.rs": "" - } - }), - ) - .await; - fs.insert_tree( - "/root2", - json!({ - "src": { - "main.rs": "", - "test.rs": "" - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - panel.update(cx, |panel, cx| { - let project = panel.project.read(cx); - let worktrees: Vec<_> = project.visible_worktrees(cx).collect(); - - let worktree_a = &worktrees[0]; - let main_rs_from_a = worktree_a - .read(cx) - .entry_for_path(rel_path("src/main.rs")) - .unwrap(); - - let worktree_b = &worktrees[1]; - let src_dir_from_b = worktree_b.read(cx).entry_for_path(rel_path("src")).unwrap(); - let main_rs_from_b = worktree_b - .read(cx) - .entry_for_path(rel_path("src/main.rs")) - .unwrap(); - - // Test dragging file from worktree A onto parent of file with same relative path in worktree B - let dragged_selection = DraggedSelection { - active_selection: SelectedEntry { - worktree_id: worktree_a.read(cx).id(), - entry_id: main_rs_from_a.id, - }, - marked_selections: Arc::new([SelectedEntry { - worktree_id: worktree_a.read(cx).id(), - entry_id: main_rs_from_a.id, - }]), - }; - - let result = panel.highlight_entry_for_selection_drag( - src_dir_from_b, - worktree_b.read(cx), - &dragged_selection, - cx, - ); - assert_eq!( - result, - Some(src_dir_from_b.id), - "Should highlight target directory from different worktree even with same relative path" - ); - - // Test dragging file from worktree A onto file with same relative path in worktree B - let result = panel.highlight_entry_for_selection_drag( - main_rs_from_b, - worktree_b.read(cx), - &dragged_selection, - cx, - ); - assert_eq!( - result, - Some(src_dir_from_b.id), - "Should highlight parent of target file from different worktree" - ); - }); -} - -#[gpui::test] -async fn test_should_highlight_background_for_selection_drag(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root1", - json!({ - "parent_dir": { - "child_file.txt": "", - "nested_dir": { - "nested_file.txt": "" - } - }, - "root_file.txt": "" - }), - ) - .await; - - fs.insert_tree( - "/root2", - json!({ - "other_dir": { - "other_file.txt": "" - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - panel.update(cx, |panel, cx| { - let project = panel.project.read(cx); - let worktrees: Vec<_> = project.visible_worktrees(cx).collect(); - let worktree1 = worktrees[0].read(cx); - let worktree2 = worktrees[1].read(cx); - let worktree1_id = worktree1.id(); - let _worktree2_id = worktree2.id(); - - let root1_entry = worktree1.root_entry().unwrap(); - let root2_entry = worktree2.root_entry().unwrap(); - let _parent_dir = worktree1.entry_for_path(rel_path("parent_dir")).unwrap(); - let child_file = worktree1 - .entry_for_path(rel_path("parent_dir/child_file.txt")) - .unwrap(); - let nested_file = worktree1 - .entry_for_path(rel_path("parent_dir/nested_dir/nested_file.txt")) - .unwrap(); - let root_file = worktree1.entry_for_path(rel_path("root_file.txt")).unwrap(); - - // Test 1: Multiple entries - should always highlight background - let multiple_dragged_selection = DraggedSelection { - active_selection: SelectedEntry { - worktree_id: worktree1_id, - entry_id: child_file.id, - }, - marked_selections: Arc::new([ - SelectedEntry { - worktree_id: worktree1_id, - entry_id: child_file.id, - }, - SelectedEntry { - worktree_id: worktree1_id, - entry_id: nested_file.id, - }, - ]), - }; - - let result = panel.should_highlight_background_for_selection_drag( - &multiple_dragged_selection, - root1_entry.id, - cx, - ); - assert!(result, "Should highlight background for multiple entries"); - - // Test 2: Single entry with non-empty parent path - should highlight background - let nested_dragged_selection = DraggedSelection { - active_selection: SelectedEntry { - worktree_id: worktree1_id, - entry_id: nested_file.id, - }, - marked_selections: Arc::new([SelectedEntry { - worktree_id: worktree1_id, - entry_id: nested_file.id, - }]), - }; - - let result = panel.should_highlight_background_for_selection_drag( - &nested_dragged_selection, - root1_entry.id, - cx, - ); - assert!(result, "Should highlight background for nested file"); - - // Test 3: Single entry at root level, same worktree - should NOT highlight background - let root_file_dragged_selection = DraggedSelection { - active_selection: SelectedEntry { - worktree_id: worktree1_id, - entry_id: root_file.id, - }, - marked_selections: Arc::new([SelectedEntry { - worktree_id: worktree1_id, - entry_id: root_file.id, - }]), - }; - - let result = panel.should_highlight_background_for_selection_drag( - &root_file_dragged_selection, - root1_entry.id, - cx, - ); - assert!( - !result, - "Should NOT highlight background for root file in same worktree" - ); - - // Test 4: Single entry at root level, different worktree - should highlight background - let result = panel.should_highlight_background_for_selection_drag( - &root_file_dragged_selection, - root2_entry.id, - cx, - ); - assert!( - result, - "Should highlight background for root file from different worktree" - ); - - // Test 5: Single entry in subdirectory - should highlight background - let child_file_dragged_selection = DraggedSelection { - active_selection: SelectedEntry { - worktree_id: worktree1_id, - entry_id: child_file.id, - }, - marked_selections: Arc::new([SelectedEntry { - worktree_id: worktree1_id, - entry_id: child_file.id, - }]), - }; - - let result = panel.should_highlight_background_for_selection_drag( - &child_file_dragged_selection, - root1_entry.id, - cx, - ); - assert!( - result, - "Should highlight background for file with non-empty parent path" - ); - }); -} - -#[gpui::test] -async fn test_hide_root(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root1", - json!({ - "dir1": { - "file1.txt": "content", - "file2.txt": "content", - }, - "dir2": { - "file3.txt": "content", - }, - "file4.txt": "content", - }), - ) - .await; - - fs.insert_tree( - "/root2", - json!({ - "dir3": { - "file5.txt": "content", - }, - "file6.txt": "content", - }), - ) - .await; - - // Test 1: Single worktree with hide_root = false - { - let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_root: false, - ..settings - }, - cx, - ); - }); - - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - #[rustfmt::skip] - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > dir1", - " > dir2", - " file4.txt", - ], - "With hide_root=false and single worktree, root should be visible" - ); - } - - // Test 2: Single worktree with hide_root = true - { - let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - // Set hide_root to true - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_root: true, - ..settings - }, - cx, - ); - }); - - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["> dir1", "> dir2", " file4.txt",], - "With hide_root=true and single worktree, root should be hidden" - ); - - // Test expanding directories still works without root - toggle_expand_dir(&panel, "root1/dir1", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v dir1 <== selected", - " file1.txt", - " file2.txt", - "> dir2", - " file4.txt", - ], - "Should be able to expand directories even when root is hidden" - ); - } - - // Test 3: Multiple worktrees with hide_root = true - { - let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - // Set hide_root to true - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_root: true, - ..settings - }, - cx, - ); - }); - - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > dir1", - " > dir2", - " file4.txt", - "v root2", - " > dir3", - " file6.txt", - ], - "With hide_root=true and multiple worktrees, roots should still be visible" - ); - } - - // Test 4: Multiple worktrees with hide_root = false - { - let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_root: false, - ..settings - }, - cx, - ); - }); - - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > dir1", - " > dir2", - " file4.txt", - "v root2", - " > dir3", - " file6.txt", - ], - "With hide_root=false and multiple worktrees, roots should be visible" - ); - } -} - -#[gpui::test] -async fn test_compare_selected_files(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "file1.txt": "content of file1", - "file2.txt": "content of file2", - "dir1": { - "file3.txt": "content of file3" - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - let file1_path = "root/file1.txt"; - let file2_path = "root/file2.txt"; - select_path_with_mark(&panel, file1_path, cx); - select_path_with_mark(&panel, file2_path, cx); - - panel.update_in(cx, |panel, window, cx| { - panel.compare_marked_files(&CompareMarkedFiles, window, cx); - }); - cx.executor().run_until_parked(); - - workspace - .update(cx, |workspace, _, cx| { - let active_items = workspace - .panes() - .iter() - .filter_map(|pane| pane.read(cx).active_item()) - .collect::>(); - assert_eq!(active_items.len(), 1); - let diff_view = active_items - .into_iter() - .next() - .unwrap() - .downcast::() - .expect("Open item should be an FileDiffView"); - assert_eq!(diff_view.tab_content_text(0, cx), "file1.txt ↔ file2.txt"); - assert_eq!( - diff_view.tab_tooltip_text(cx).unwrap(), - format!( - "{} ↔ {}", - rel_path(file1_path).display(PathStyle::local()), - rel_path(file2_path).display(PathStyle::local()) - ) - ); - }) - .unwrap(); - - let file1_entry_id = find_project_entry(&panel, file1_path, cx).unwrap(); - let file2_entry_id = find_project_entry(&panel, file2_path, cx).unwrap(); - let worktree_id = panel.update(cx, |panel, cx| { - panel - .project - .read(cx) - .worktrees(cx) - .next() - .unwrap() - .read(cx) - .id() - }); - - let expected_entries = [ - SelectedEntry { - worktree_id, - entry_id: file1_entry_id, - }, - SelectedEntry { - worktree_id, - entry_id: file2_entry_id, - }, - ]; - panel.update(cx, |panel, _cx| { - assert_eq!( - &panel.marked_entries, &expected_entries, - "Should keep marked entries after comparison" - ); - }); - - panel.update(cx, |panel, cx| { - panel.project.update(cx, |_, cx| { - cx.emit(project::Event::RevealInProjectPanel(file2_entry_id)) - }) - }); - - panel.update(cx, |panel, _cx| { - assert_eq!( - &panel.marked_entries, &expected_entries, - "Marked entries should persist after focusing back on the project panel" - ); - }); -} - -#[gpui::test] -async fn test_compare_files_context_menu(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "file1.txt": "content of file1", - "file2.txt": "content of file2", - "dir1": {}, - "dir2": { - "file3.txt": "content of file3" - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - // Test 1: When only one file is selected, there should be no compare option - select_path(&panel, "root/file1.txt", cx); - - let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx)); - assert_eq!( - selected_files, None, - "Should not have compare option when only one file is selected" - ); - - // Test 2: When multiple files are selected, there should be a compare option - select_path_with_mark(&panel, "root/file1.txt", cx); - select_path_with_mark(&panel, "root/file2.txt", cx); - - let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx)); - assert!( - selected_files.is_some(), - "Should have files selected for comparison" - ); - if let Some((file1, file2)) = selected_files { - assert!( - file1.to_string_lossy().ends_with("file1.txt") - && file2.to_string_lossy().ends_with("file2.txt"), - "Should have file1.txt and file2.txt as the selected files when multi-selecting" - ); - } - - // Test 3: Selecting a directory shouldn't count as a comparable file - select_path_with_mark(&panel, "root/dir1", cx); - - let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx)); - assert!( - selected_files.is_some(), - "Directory selection should not affect comparable files" - ); - if let Some((file1, file2)) = selected_files { - assert!( - file1.to_string_lossy().ends_with("file1.txt") - && file2.to_string_lossy().ends_with("file2.txt"), - "Selecting a directory should not affect the number of comparable files" - ); - } - - // Test 4: Selecting one more file - select_path_with_mark(&panel, "root/dir2/file3.txt", cx); - - let selected_files = panel.update(cx, |panel, cx| panel.file_abs_paths_to_diff(cx)); - assert!( - selected_files.is_some(), - "Directory selection should not affect comparable files" - ); - if let Some((file1, file2)) = selected_files { - assert!( - file1.to_string_lossy().ends_with("file2.txt") - && file2.to_string_lossy().ends_with("file3.txt"), - "Selecting a directory should not affect the number of comparable files" - ); - } -} - -#[gpui::test] -async fn test_hide_hidden_entries(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - ".hidden-file.txt": "hidden file content", - "visible-file.txt": "visible file content", - ".hidden-parent-dir": { - "nested-dir": { - "file.txt": "file content", - } - }, - "visible-dir": { - "file-in-visible.txt": "file content", - "nested": { - ".hidden-nested-dir": { - ".double-hidden-dir": { - "deep-file-1.txt": "deep content 1", - "deep-file-2.txt": "deep content 2" - }, - "hidden-nested-file-1.txt": "hidden nested 1", - "hidden-nested-file-2.txt": "hidden nested 2" - }, - "visible-nested-file.txt": "visible nested content" - } - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_hidden: false, - ..settings - }, - cx, - ); - }); - - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - toggle_expand_dir(&panel, "root/.hidden-parent-dir", cx); - toggle_expand_dir(&panel, "root/.hidden-parent-dir/nested-dir", cx); - toggle_expand_dir(&panel, "root/visible-dir", cx); - toggle_expand_dir(&panel, "root/visible-dir/nested", cx); - toggle_expand_dir(&panel, "root/visible-dir/nested/.hidden-nested-dir", cx); - toggle_expand_dir( - &panel, - "root/visible-dir/nested/.hidden-nested-dir/.double-hidden-dir", - cx, - ); - - let expanded = [ - "v root", - " v .hidden-parent-dir", - " v nested-dir", - " file.txt", - " v visible-dir", - " v nested", - " v .hidden-nested-dir", - " v .double-hidden-dir <== selected", - " deep-file-1.txt", - " deep-file-2.txt", - " hidden-nested-file-1.txt", - " hidden-nested-file-2.txt", - " visible-nested-file.txt", - " file-in-visible.txt", - " .hidden-file.txt", - " visible-file.txt", - ]; - - assert_eq!( - visible_entries_as_strings(&panel, 0..30, cx), - &expanded, - "With hide_hidden=false, contents of hidden nested directory should be visible" - ); - - cx.update(|_, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_hidden: true, - ..settings - }, - cx, - ); - }); - - panel.update_in(cx, |panel, window, cx| { - panel.update_visible_entries(None, false, false, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..30, cx), - &[ - "v root", - " v visible-dir", - " v nested", - " visible-nested-file.txt", - " file-in-visible.txt", - " visible-file.txt", - ], - "With hide_hidden=false, contents of hidden nested directory should be visible" - ); - - panel.update_in(cx, |panel, window, cx| { - let settings = *ProjectPanelSettings::get_global(cx); - ProjectPanelSettings::override_global( - ProjectPanelSettings { - hide_hidden: false, - ..settings - }, - cx, - ); - panel.update_visible_entries(None, false, false, window, cx); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..30, cx), - &expanded, - "With hide_hidden=false, deeply nested hidden directories and their contents should be visible" - ); -} - -fn select_path(panel: &Entity, path: &str, cx: &mut VisualTestContext) { - let path = rel_path(path); - panel.update_in(cx, |panel, window, cx| { - for worktree in panel.project.read(cx).worktrees(cx).collect::>() { - let worktree = worktree.read(cx); - if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { - let entry_id = worktree.entry_for_path(relative_path).unwrap().id; - panel.update_visible_entries( - Some((worktree.id(), entry_id)), - false, - false, - window, - cx, - ); - return; - } - } - panic!("no worktree for path {:?}", path); - }); - cx.run_until_parked(); -} - -fn select_path_with_mark(panel: &Entity, path: &str, cx: &mut VisualTestContext) { - let path = rel_path(path); - panel.update(cx, |panel, cx| { - for worktree in panel.project.read(cx).worktrees(cx).collect::>() { - let worktree = worktree.read(cx); - if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { - let entry_id = worktree.entry_for_path(relative_path).unwrap().id; - let entry = crate::SelectedEntry { - worktree_id: worktree.id(), - entry_id, - }; - if !panel.marked_entries.contains(&entry) { - panel.marked_entries.push(entry); - } - panel.state.selection = Some(entry); - return; - } - } - panic!("no worktree for path {:?}", path); - }); -} - -fn drag_selection_to( - panel: &Entity, - target_path: &str, - is_file: bool, - cx: &mut VisualTestContext, -) { - let target_entry = find_project_entry(panel, target_path, cx) - .unwrap_or_else(|| panic!("no entry for target path {target_path:?}")); - - panel.update_in(cx, |panel, window, cx| { - let selection = panel - .state - .selection - .expect("a selection is required before dragging"); - let drag = DraggedSelection { - active_selection: SelectedEntry { - worktree_id: selection.worktree_id, - entry_id: panel.resolve_entry(selection.entry_id), - }, - marked_selections: Arc::from(panel.marked_entries.clone()), - }; - panel.drag_onto(&drag, target_entry, is_file, window, cx); - }); - cx.executor().run_until_parked(); -} - -fn find_project_entry( - panel: &Entity, - path: &str, - cx: &mut VisualTestContext, -) -> Option { - let path = rel_path(path); - panel.update(cx, |panel, cx| { - for worktree in panel.project.read(cx).worktrees(cx).collect::>() { - let worktree = worktree.read(cx); - if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { - return worktree.entry_for_path(relative_path).map(|entry| entry.id); - } - } - panic!("no worktree for path {path:?}"); - }) -} - -fn visible_entries_as_strings( - panel: &Entity, - range: Range, - cx: &mut VisualTestContext, -) -> Vec { - let mut result = Vec::new(); - let mut project_entries = HashSet::default(); - let mut has_editor = false; - - panel.update_in(cx, |panel, window, cx| { - panel.for_each_visible_entry(range, window, cx, |project_entry, details, _, _| { - if details.is_editing { - assert!(!has_editor, "duplicate editor entry"); - has_editor = true; - } else { - assert!( - project_entries.insert(project_entry), - "duplicate project entry {:?} {:?}", - project_entry, - details - ); - } - - let indent = " ".repeat(details.depth); - let icon = if details.kind.is_dir() { - if details.is_expanded { "v " } else { "> " } - } else { - " " - }; - #[cfg(windows)] - let filename = details.filename.replace("\\", "/"); - #[cfg(not(windows))] - let filename = details.filename; - let name = if details.is_editing { - format!("[EDITOR: '{}']", filename) - } else if details.is_processing { - format!("[PROCESSING: '{}']", filename) - } else { - filename - }; - let selected = if details.is_selected { - " <== selected" - } else { - "" - }; - let marked = if details.is_marked { - " <== marked" - } else { - "" - }; - - result.push(format!("{indent}{icon}{name}{selected}{marked}")); - }); - }); - - result -} - -/// Test that missing sort_mode field defaults to DirectoriesFirst -#[gpui::test] -async fn test_sort_mode_default_fallback(cx: &mut gpui::TestAppContext) { - init_test(cx); - - // Verify that when sort_mode is not specified, it defaults to DirectoriesFirst - let default_settings = cx.read(|cx| *ProjectPanelSettings::get_global(cx)); - assert_eq!( - default_settings.sort_mode, - settings::ProjectPanelSortMode::DirectoriesFirst, - "sort_mode should default to DirectoriesFirst" - ); -} - -/// Test sort modes: DirectoriesFirst (default) vs Mixed -#[gpui::test] -async fn test_sort_mode_directories_first(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "zebra.txt": "", - "Apple": {}, - "banana.rs": "", - "Carrot": {}, - "aardvark.txt": "", - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - // Default sort mode should be DirectoriesFirst - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - "v root", - " > Apple", - " > Carrot", - " aardvark.txt", - " banana.rs", - " zebra.txt", - ] - ); -} - -#[gpui::test] -async fn test_sort_mode_mixed(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "Zebra.txt": "", - "apple": {}, - "Banana.rs": "", - "carrot": {}, - "Aardvark.txt": "", - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - // Switch to Mixed mode - cx.update(|_, cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project_panel.get_or_insert_default().sort_mode = - Some(settings::ProjectPanelSortMode::Mixed); - }); - }); - }); - - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - // Mixed mode: case-insensitive sorting - // Aardvark < apple < Banana < carrot < Zebra (all case-insensitive) - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - "v root", - " Aardvark.txt", - " > apple", - " Banana.rs", - " > carrot", - " Zebra.txt", - ] - ); -} - -#[gpui::test] -async fn test_sort_mode_files_first(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "Zebra.txt": "", - "apple": {}, - "Banana.rs": "", - "carrot": {}, - "Aardvark.txt": "", - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - - // Switch to FilesFirst mode - cx.update(|_, cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project_panel.get_or_insert_default().sort_mode = - Some(settings::ProjectPanelSortMode::FilesFirst); - }); - }); - }); - - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - // FilesFirst mode: files first, then directories (both case-insensitive) - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - "v root", - " Aardvark.txt", - " Banana.rs", - " Zebra.txt", - " > apple", - " > carrot", - ] - ); -} - -#[gpui::test] -async fn test_sort_mode_toggle(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root", - json!({ - "file2.txt": "", - "dir1": {}, - "file1.txt": "", - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace.update(cx, ProjectPanel::new).unwrap(); - cx.run_until_parked(); - - // Initially DirectoriesFirst - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &["v root", " > dir1", " file1.txt", " file2.txt",] - ); - - // Toggle to Mixed - cx.update(|_, cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project_panel.get_or_insert_default().sort_mode = - Some(settings::ProjectPanelSortMode::Mixed); - }); - }); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &["v root", " > dir1", " file1.txt", " file2.txt",] - ); - - // Toggle back to DirectoriesFirst - cx.update(|_, cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project_panel.get_or_insert_default().sort_mode = - Some(settings::ProjectPanelSortMode::DirectoriesFirst); - }); - }); - }); - cx.run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &["v root", " > dir1", " file1.txt", " file2.txt",] - ); -} - -fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - theme::init(theme::LoadThemes::JustBase, cx); - crate::init(cx); - - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings - .project_panel - .get_or_insert_default() - .auto_fold_dirs = Some(false); - settings.project.worktree.file_scan_exclusions = Some(Vec::new()); - }); - }); - }); -} - -fn init_test_with_editor(cx: &mut TestAppContext) { - cx.update(|cx| { - let app_state = AppState::test(cx); - theme::init(theme::LoadThemes::JustBase, cx); - editor::init(cx); - crate::init(cx); - workspace::init(app_state, cx); - - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings - .project_panel - .get_or_insert_default() - .auto_fold_dirs = Some(false); - settings.project.worktree.file_scan_exclusions = Some(Vec::new()) - }); - }); - }); -} - -fn set_auto_open_settings( - cx: &mut TestAppContext, - auto_open_settings: ProjectPanelAutoOpenSettings, -) { - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project_panel.get_or_insert_default().auto_open = Some(auto_open_settings); - }); - }) - }); -} - -fn ensure_single_file_is_opened( - window: &WindowHandle, - expected_path: &str, - cx: &mut TestAppContext, -) { - window - .update(cx, |workspace, _, cx| { - let worktrees = workspace.worktrees(cx).collect::>(); - assert_eq!(worktrees.len(), 1); - let worktree_id = worktrees[0].read(cx).id(); - - let open_project_paths = workspace - .panes() - .iter() - .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx)) - .collect::>(); - assert_eq!( - open_project_paths, - vec![ProjectPath { - worktree_id, - path: Arc::from(rel_path(expected_path)) - }], - "Should have opened file, selected in project panel" - ); - }) - .unwrap(); -} - -fn submit_deletion(panel: &Entity, cx: &mut VisualTestContext) { - assert!( - !cx.has_pending_prompt(), - "Should have no prompts before the deletion" - ); - panel.update_in(cx, |panel, window, cx| { - panel.delete(&Delete { skip_prompt: false }, window, cx) - }); - assert!( - cx.has_pending_prompt(), - "Should have a prompt after the deletion" - ); - cx.simulate_prompt_answer("Delete"); - assert!( - !cx.has_pending_prompt(), - "Should have no prompts after prompt was replied to" - ); - cx.executor().run_until_parked(); -} - -fn submit_deletion_skipping_prompt(panel: &Entity, cx: &mut VisualTestContext) { - assert!( - !cx.has_pending_prompt(), - "Should have no prompts before the deletion" - ); - panel.update_in(cx, |panel, window, cx| { - panel.delete(&Delete { skip_prompt: true }, window, cx) - }); - assert!(!cx.has_pending_prompt(), "Should have received no prompts"); - cx.executor().run_until_parked(); -} - -fn ensure_no_open_items_and_panes(workspace: &WindowHandle, cx: &mut VisualTestContext) { - assert!( - !cx.has_pending_prompt(), - "Should have no prompts after deletion operation closes the file" - ); - workspace - .read_with(cx, |workspace, cx| { - let open_project_paths = workspace - .panes() - .iter() - .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx)) - .collect::>(); - assert!( - open_project_paths.is_empty(), - "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}" - ); - }) - .unwrap(); -} - -struct TestProjectItemView { - focus_handle: FocusHandle, - path: ProjectPath, -} - -struct TestProjectItem { - path: ProjectPath, -} - -impl project::ProjectItem for TestProjectItem { - fn try_open( - _project: &Entity, - path: &ProjectPath, - cx: &mut App, - ) -> Option>>> { - let path = path.clone(); - Some(cx.spawn(async move |cx| cx.new(|_| Self { path }))) - } - - fn entry_id(&self, _: &App) -> Option { - None - } - - fn project_path(&self, _: &App) -> Option { - Some(self.path.clone()) - } - - fn is_dirty(&self) -> bool { - false - } -} - -impl ProjectItem for TestProjectItemView { - type Item = TestProjectItem; - - fn for_project_item( - _: Entity, - _: Option<&Pane>, - project_item: Entity, - _: &mut Window, - cx: &mut Context, - ) -> Self - where - Self: Sized, - { - Self { - path: project_item.update(cx, |project_item, _| project_item.path.clone()), - focus_handle: cx.focus_handle(), - } - } -} - -impl Item for TestProjectItemView { - type Event = (); - - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { - "Test".into() - } -} - -impl EventEmitter<()> for TestProjectItemView {} - -impl Focusable for TestProjectItemView { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Render for TestProjectItemView { - fn render(&mut self, _window: &mut Window, _: &mut Context) -> impl IntoElement { - Empty - } -} diff --git a/crates/project_panel/src/utils.rs b/crates/project_panel/src/utils.rs deleted file mode 100644 index 486def9b84..0000000000 --- a/crates/project_panel/src/utils.rs +++ /dev/null @@ -1,42 +0,0 @@ -pub(crate) struct ReversibleIterable { - pub(crate) it: It, - pub(crate) reverse: bool, -} - -impl ReversibleIterable { - pub(crate) fn new(it: T, reverse: bool) -> Self { - Self { it, reverse } - } -} - -impl ReversibleIterable -where - It: Iterator, -{ - pub(crate) fn find_single_ended(mut self, pred: F) -> Option - where - F: FnMut(&Item) -> bool, - { - if self.reverse { - self.it.filter(pred).last() - } else { - self.it.find(pred) - } - } -} - -impl ReversibleIterable -where - It: DoubleEndedIterator, -{ - pub(crate) fn find(mut self, mut pred: F) -> Option - where - F: FnMut(&Item) -> bool, - { - if self.reverse { - self.it.rfind(|x| pred(x)) - } else { - self.it.find(|x| pred(x)) - } - } -} diff --git a/crates/project_symbols/Cargo.toml b/crates/project_symbols/Cargo.toml deleted file mode 100644 index 83e3cb587d..0000000000 --- a/crates/project_symbols/Cargo.toml +++ /dev/null @@ -1,40 +0,0 @@ -[package] -name = "project_symbols" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/project_symbols.rs" -doctest = false - -[dependencies] -anyhow.workspace = true -editor.workspace = true -fuzzy.workspace = true -gpui.workspace = true -ordered-float.workspace = true -picker.workspace = true -project.workspace = true -serde_json.workspace = true -settings.workspace = true -theme.workspace = true -util.workspace = true -workspace.workspace = true - -[dev-dependencies] -editor = { workspace = true, features = ["test-support"] } -futures.workspace = true -gpui = { workspace = true, features = ["test-support"] } -language = { workspace = true, features = ["test-support"] } -lsp = { workspace = true, features = ["test-support"] } -project = { workspace = true, features = ["test-support"] } -release_channel.workspace = true -semver.workspace = true -settings = { workspace = true, features = ["test-support"] } -theme = { workspace = true, features = ["test-support"] } -workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/project_symbols/LICENSE-GPL b/crates/project_symbols/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/project_symbols/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs deleted file mode 100644 index d96de4b876..0000000000 --- a/crates/project_symbols/src/project_symbols.rs +++ /dev/null @@ -1,463 +0,0 @@ -use editor::{Bias, Editor, SelectionEffects, scroll::Autoscroll, styled_runs_for_code_label}; -use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{ - App, Context, DismissEvent, Entity, HighlightStyle, ParentElement, StyledText, Task, TextStyle, - WeakEntity, Window, relative, rems, -}; -use ordered_float::OrderedFloat; -use picker::{Picker, PickerDelegate}; -use project::{Project, Symbol, lsp_store::SymbolLocation}; -use settings::Settings; -use std::{cmp::Reverse, sync::Arc}; -use theme::{ActiveTheme, ThemeSettings}; -use util::ResultExt; -use workspace::{ - Workspace, - ui::{LabelLike, ListItem, ListItemSpacing, prelude::*}, -}; - -pub fn init(cx: &mut App) { - cx.observe_new( - |workspace: &mut Workspace, _window, _: &mut Context| { - workspace.register_action( - |workspace, _: &workspace::ToggleProjectSymbols, window, cx| { - let project = workspace.project().clone(); - let handle = cx.entity().downgrade(); - workspace.toggle_modal(window, cx, move |window, cx| { - let delegate = ProjectSymbolsDelegate::new(handle, project); - Picker::uniform_list(delegate, window, cx).width(rems(34.)) - }) - }, - ); - }, - ) - .detach(); -} - -pub type ProjectSymbols = Entity>; - -pub struct ProjectSymbolsDelegate { - workspace: WeakEntity, - project: Entity, - selected_match_index: usize, - symbols: Vec, - visible_match_candidates: Vec, - external_match_candidates: Vec, - show_worktree_root_name: bool, - matches: Vec, -} - -impl ProjectSymbolsDelegate { - fn new(workspace: WeakEntity, project: Entity) -> Self { - Self { - workspace, - project, - selected_match_index: 0, - symbols: Default::default(), - visible_match_candidates: Default::default(), - external_match_candidates: Default::default(), - matches: Default::default(), - show_worktree_root_name: false, - } - } - - fn filter(&mut self, query: &str, window: &mut Window, cx: &mut Context>) { - const MAX_MATCHES: usize = 100; - let mut visible_matches = cx.background_executor().block(fuzzy::match_strings( - &self.visible_match_candidates, - query, - false, - true, - MAX_MATCHES, - &Default::default(), - cx.background_executor().clone(), - )); - let mut external_matches = cx.background_executor().block(fuzzy::match_strings( - &self.external_match_candidates, - query, - false, - true, - MAX_MATCHES - visible_matches.len().min(MAX_MATCHES), - &Default::default(), - cx.background_executor().clone(), - )); - let sort_key_for_match = |mat: &StringMatch| { - let symbol = &self.symbols[mat.candidate_id]; - (Reverse(OrderedFloat(mat.score)), symbol.label.filter_text()) - }; - - visible_matches.sort_unstable_by_key(sort_key_for_match); - external_matches.sort_unstable_by_key(sort_key_for_match); - let mut matches = visible_matches; - matches.append(&mut external_matches); - - for mat in &mut matches { - let symbol = &self.symbols[mat.candidate_id]; - let filter_start = symbol.label.filter_range.start; - for position in &mut mat.positions { - *position += filter_start; - } - } - - self.matches = matches; - self.set_selected_index(0, window, cx); - } -} - -impl PickerDelegate for ProjectSymbolsDelegate { - type ListItem = ListItem; - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - "Search project symbols...".into() - } - - fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context>) { - if let Some(symbol) = self - .matches - .get(self.selected_match_index) - .map(|mat| self.symbols[mat.candidate_id].clone()) - { - let buffer = self.project.update(cx, |project, cx| { - project.open_buffer_for_symbol(&symbol, cx) - }); - let symbol = symbol.clone(); - let workspace = self.workspace.clone(); - cx.spawn_in(window, async move |_, cx| { - let buffer = buffer.await?; - workspace.update_in(cx, |workspace, window, cx| { - let position = buffer - .read(cx) - .clip_point_utf16(symbol.range.start, Bias::Left); - let pane = if secondary { - workspace.adjacent_pane(window, cx) - } else { - workspace.active_pane().clone() - }; - - let editor = workspace.open_project_item::( - pane, buffer, true, true, true, true, window, cx, - ); - - editor.update(cx, |editor, cx| { - editor.change_selections( - SelectionEffects::scroll(Autoscroll::center()), - window, - cx, - |s| s.select_ranges([position..position]), - ); - }); - })?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - cx.emit(DismissEvent); - } - } - - fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context>) {} - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn selected_index(&self) -> usize { - self.selected_match_index - } - - fn set_selected_index( - &mut self, - ix: usize, - _window: &mut Window, - _cx: &mut Context>, - ) { - self.selected_match_index = ix; - } - - fn update_matches( - &mut self, - query: String, - window: &mut Window, - cx: &mut Context>, - ) -> Task<()> { - self.filter(&query, window, cx); - self.show_worktree_root_name = self.project.read(cx).visible_worktrees(cx).count() > 1; - let symbols = self - .project - .update(cx, |project, cx| project.symbols(&query, cx)); - cx.spawn_in(window, async move |this, cx| { - let symbols = symbols.await.log_err(); - if let Some(symbols) = symbols { - this.update_in(cx, |this, window, cx| { - let delegate = &mut this.delegate; - let project = delegate.project.read(cx); - let (visible_match_candidates, external_match_candidates) = symbols - .iter() - .enumerate() - .map(|(id, symbol)| { - StringMatchCandidate::new(id, symbol.label.filter_text()) - }) - .partition(|candidate| { - if let SymbolLocation::InProject(path) = &symbols[candidate.id].path { - project - .entry_for_path(path, cx) - .is_some_and(|e| !e.is_ignored) - } else { - false - } - }); - - delegate.visible_match_candidates = visible_match_candidates; - delegate.external_match_candidates = external_match_candidates; - delegate.symbols = symbols; - delegate.filter(&query, window, cx); - }) - .log_err(); - } - }) - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _window: &mut Window, - cx: &mut Context>, - ) -> Option { - let path_style = self.project.read(cx).path_style(cx); - let string_match = &self.matches.get(ix)?; - let symbol = &self.symbols.get(string_match.candidate_id)?; - let theme = cx.theme(); - let local_player = theme.players().local(); - let syntax_runs = styled_runs_for_code_label(&symbol.label, theme.syntax(), &local_player); - - let path = match &symbol.path { - SymbolLocation::InProject(project_path) => { - let project = self.project.read(cx); - let mut path = project_path.path.clone(); - if self.show_worktree_root_name - && let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx) - { - path = worktree.read(cx).root_name().join(&path); - } - path.display(path_style).into_owned().into() - } - SymbolLocation::OutsideProject { - abs_path, - signature: _, - } => abs_path.to_string_lossy(), - }; - let label = symbol.label.text.clone(); - let path = path.to_string(); - - let settings = ThemeSettings::get_global(cx); - - let text_style = TextStyle { - color: cx.theme().colors().text, - font_family: settings.buffer_font.family.clone(), - font_features: settings.buffer_font.features.clone(), - font_fallbacks: settings.buffer_font.fallbacks.clone(), - font_size: settings.buffer_font_size(cx).into(), - font_weight: settings.buffer_font.weight, - line_height: relative(1.), - ..Default::default() - }; - - let highlight_style = HighlightStyle { - background_color: Some(cx.theme().colors().text_accent.alpha(0.3)), - ..Default::default() - }; - let custom_highlights = string_match - .positions - .iter() - .map(|pos| (*pos..pos + 1, highlight_style)); - - let highlights = gpui::combine_highlights(custom_highlights, syntax_runs); - - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .child( - v_flex() - .child(LabelLike::new().child( - StyledText::new(label).with_default_highlights(&text_style, highlights), - )) - .child(Label::new(path).size(LabelSize::Small).color(Color::Muted)), - ), - ) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use futures::StreamExt; - use gpui::{TestAppContext, VisualContext}; - use language::{FakeLspAdapter, Language, LanguageConfig, LanguageMatcher}; - use lsp::OneOf; - use project::FakeFs; - use serde_json::json; - use settings::SettingsStore; - use std::{path::Path, sync::Arc}; - use util::path; - - #[gpui::test] - async fn test_project_symbols(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree(path!("/dir"), json!({ "test.rs": "" })) - .await; - - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - - let language_registry = project.read_with(cx, |project, _| project.languages().clone()); - language_registry.add(Arc::new(Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..Default::default() - }, - ..Default::default() - }, - None, - ))); - let mut fake_servers = language_registry.register_fake_lsp( - "Rust", - FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - workspace_symbol_provider: Some(OneOf::Left(true)), - ..Default::default() - }, - ..Default::default() - }, - ); - - let _buffer = project - .update(cx, |project, cx| { - project.open_local_buffer_with_lsp(path!("/dir/test.rs"), cx) - }) - .await - .unwrap(); - - // Set up fake language server to return fuzzy matches against - // a fixed set of symbol names. - let fake_symbols = [ - symbol("one", path!("/external")), - symbol("ton", path!("/dir/test.rs")), - symbol("uno", path!("/dir/test.rs")), - ]; - let fake_server = fake_servers.next().await.unwrap(); - fake_server.set_request_handler::( - move |params: lsp::WorkspaceSymbolParams, cx| { - let executor = cx.background_executor().clone(); - let fake_symbols = fake_symbols.clone(); - async move { - let candidates = fake_symbols - .iter() - .enumerate() - .map(|(id, symbol)| StringMatchCandidate::new(id, &symbol.name)) - .collect::>(); - let matches = if params.query.is_empty() { - Vec::new() - } else { - fuzzy::match_strings( - &candidates, - ¶ms.query, - true, - true, - 100, - &Default::default(), - executor.clone(), - ) - .await - }; - - Ok(Some(lsp::WorkspaceSymbolResponse::Flat( - matches - .into_iter() - .map(|mat| fake_symbols[mat.candidate_id].clone()) - .collect(), - ))) - } - }, - ); - - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - // Create the project symbols view. - let symbols = cx.new_window_entity(|window, cx| { - Picker::uniform_list( - ProjectSymbolsDelegate::new(workspace.downgrade(), project.clone()), - window, - cx, - ) - }); - - // Spawn multiples updates before the first update completes, - // such that in the end, there are no matches. Testing for regression: - // https://github.com/zed-industries/zed/issues/861 - symbols.update_in(cx, |p, window, cx| { - p.update_matches("o".to_string(), window, cx); - p.update_matches("on".to_string(), window, cx); - p.update_matches("onex".to_string(), window, cx); - }); - - cx.run_until_parked(); - symbols.read_with(cx, |symbols, _| { - assert_eq!(symbols.delegate.matches.len(), 0); - }); - - // Spawn more updates such that in the end, there are matches. - symbols.update_in(cx, |p, window, cx| { - p.update_matches("one".to_string(), window, cx); - p.update_matches("on".to_string(), window, cx); - }); - - cx.run_until_parked(); - symbols.read_with(cx, |symbols, _| { - let delegate = &symbols.delegate; - assert_eq!(delegate.matches.len(), 2); - assert_eq!(delegate.matches[0].string, "ton"); - assert_eq!(delegate.matches[1].string, "one"); - }); - - // Spawn more updates such that in the end, there are again no matches. - symbols.update_in(cx, |p, window, cx| { - p.update_matches("o".to_string(), window, cx); - p.update_matches("".to_string(), window, cx); - }); - - cx.run_until_parked(); - symbols.read_with(cx, |symbols, _| { - assert_eq!(symbols.delegate.matches.len(), 0); - }); - } - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let store = SettingsStore::test(cx); - cx.set_global(store); - theme::init(theme::LoadThemes::JustBase, cx); - release_channel::init(semver::Version::new(0, 0, 0), cx); - editor::init(cx); - }); - } - - fn symbol(name: &str, path: impl AsRef) -> lsp::SymbolInformation { - #[allow(deprecated)] - lsp::SymbolInformation { - name: name.to_string(), - kind: lsp::SymbolKind::FUNCTION, - tags: None, - deprecated: None, - container_name: None, - location: lsp::Location::new( - lsp::Uri::from_file_path(path.as_ref()).unwrap(), - lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)), - ), - } - } -} diff --git a/crates/prompt_store/Cargo.toml b/crates/prompt_store/Cargo.toml deleted file mode 100644 index 13bacbfad3..0000000000 --- a/crates/prompt_store/Cargo.toml +++ /dev/null @@ -1,33 +0,0 @@ -[package] -name = "prompt_store" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/prompt_store.rs" - -[dependencies] -anyhow.workspace = true -assets.workspace = true -chrono.workspace = true -collections.workspace = true -fs.workspace = true -futures.workspace = true -fuzzy.workspace = true -gpui.workspace = true -handlebars.workspace = true -heed.workspace = true -language.workspace = true -log.workspace = true -parking_lot.workspace = true -paths.workspace = true -rope.workspace = true -serde.workspace = true -text.workspace = true -util.workspace = true -uuid.workspace = true diff --git a/crates/prompt_store/LICENSE-GPL b/crates/prompt_store/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/prompt_store/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/prompt_store/src/prompt_store.rs b/crates/prompt_store/src/prompt_store.rs deleted file mode 100644 index fb087ce34d..0000000000 --- a/crates/prompt_store/src/prompt_store.rs +++ /dev/null @@ -1,471 +0,0 @@ -mod prompts; - -use anyhow::{Context as _, Result, anyhow}; -use chrono::{DateTime, Utc}; -use collections::HashMap; -use futures::FutureExt as _; -use futures::future::Shared; -use fuzzy::StringMatchCandidate; -use gpui::{ - App, AppContext, Context, Entity, EventEmitter, Global, ReadGlobal, SharedString, Task, -}; -use heed::{ - Database, RoTxn, - types::{SerdeBincode, SerdeJson, Str}, -}; -use parking_lot::RwLock; -pub use prompts::*; -use rope::Rope; -use serde::{Deserialize, Serialize}; -use std::{ - cmp::Reverse, - future::Future, - path::PathBuf, - sync::{Arc, atomic::AtomicBool}, -}; -use text::LineEnding; -use util::ResultExt; -use uuid::Uuid; - -/// Init starts loading the PromptStore in the background and assigns -/// a shared future to a global. -pub fn init(cx: &mut App) { - let db_path = paths::prompts_dir().join("prompts-library-db.0.mdb"); - let prompt_store_task = PromptStore::new(db_path, cx); - let prompt_store_entity_task = cx - .spawn(async move |cx| { - prompt_store_task - .await - .and_then(|prompt_store| cx.new(|_cx| prompt_store)) - .map_err(Arc::new) - }) - .shared(); - cx.set_global(GlobalPromptStore(prompt_store_entity_task)) -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct PromptMetadata { - pub id: PromptId, - pub title: Option, - pub default: bool, - pub saved_at: DateTime, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(tag = "kind")] -pub enum PromptId { - User { uuid: UserPromptId }, - EditWorkflow, -} - -impl PromptId { - pub fn new() -> PromptId { - UserPromptId::new().into() - } - - pub fn is_built_in(&self) -> bool { - !matches!(self, PromptId::User { .. }) - } -} - -impl From for PromptId { - fn from(uuid: UserPromptId) -> Self { - PromptId::User { uuid } - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -#[serde(transparent)] -pub struct UserPromptId(pub Uuid); - -impl UserPromptId { - pub fn new() -> UserPromptId { - UserPromptId(Uuid::new_v4()) - } -} - -impl From for UserPromptId { - fn from(uuid: Uuid) -> Self { - UserPromptId(uuid) - } -} - -impl std::fmt::Display for PromptId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - PromptId::User { uuid } => write!(f, "{}", uuid.0), - PromptId::EditWorkflow => write!(f, "Edit workflow"), - } - } -} - -pub struct PromptStore { - env: heed::Env, - metadata_cache: RwLock, - metadata: Database, SerdeJson>, - bodies: Database, Str>, -} - -pub struct PromptsUpdatedEvent; - -impl EventEmitter for PromptStore {} - -#[derive(Default)] -struct MetadataCache { - metadata: Vec, - metadata_by_id: HashMap, -} - -impl MetadataCache { - fn from_db( - db: Database, SerdeJson>, - txn: &RoTxn, - ) -> Result { - let mut cache = MetadataCache::default(); - for result in db.iter(txn)? { - let (prompt_id, metadata) = result?; - cache.metadata.push(metadata.clone()); - cache.metadata_by_id.insert(prompt_id, metadata); - } - cache.sort(); - Ok(cache) - } - - fn insert(&mut self, metadata: PromptMetadata) { - self.metadata_by_id.insert(metadata.id, metadata.clone()); - if let Some(old_metadata) = self.metadata.iter_mut().find(|m| m.id == metadata.id) { - *old_metadata = metadata; - } else { - self.metadata.push(metadata); - } - self.sort(); - } - - fn remove(&mut self, id: PromptId) { - self.metadata.retain(|metadata| metadata.id != id); - self.metadata_by_id.remove(&id); - } - - fn sort(&mut self) { - self.metadata.sort_unstable_by(|a, b| { - a.title - .cmp(&b.title) - .then_with(|| b.saved_at.cmp(&a.saved_at)) - }); - } -} - -impl PromptStore { - pub fn global(cx: &App) -> impl Future>> + use<> { - let store = GlobalPromptStore::global(cx).0.clone(); - async move { store.await.map_err(|err| anyhow!(err)) } - } - - pub fn new(db_path: PathBuf, cx: &App) -> Task> { - cx.background_spawn(async move { - std::fs::create_dir_all(&db_path)?; - - let db_env = unsafe { - heed::EnvOpenOptions::new() - .map_size(1024 * 1024 * 1024) // 1GB - .max_dbs(4) // Metadata and bodies (possibly v1 of both as well) - .open(db_path)? - }; - - let mut txn = db_env.write_txn()?; - let metadata = db_env.create_database(&mut txn, Some("metadata.v2"))?; - let bodies = db_env.create_database(&mut txn, Some("bodies.v2"))?; - - // Remove edit workflow prompt, as we decided to opt into it using - // a slash command instead. - metadata.delete(&mut txn, &PromptId::EditWorkflow).ok(); - bodies.delete(&mut txn, &PromptId::EditWorkflow).ok(); - - txn.commit()?; - - Self::upgrade_dbs(&db_env, metadata, bodies).log_err(); - - let txn = db_env.read_txn()?; - let metadata_cache = MetadataCache::from_db(metadata, &txn)?; - txn.commit()?; - - Ok(PromptStore { - env: db_env, - metadata_cache: RwLock::new(metadata_cache), - metadata, - bodies, - }) - }) - } - - fn upgrade_dbs( - env: &heed::Env, - metadata_db: heed::Database, SerdeJson>, - bodies_db: heed::Database, Str>, - ) -> Result<()> { - #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)] - pub struct PromptIdV1(Uuid); - - #[derive(Clone, Debug, Serialize, Deserialize)] - pub struct PromptMetadataV1 { - pub id: PromptIdV1, - pub title: Option, - pub default: bool, - pub saved_at: DateTime, - } - - let mut txn = env.write_txn()?; - let Some(bodies_v1_db) = env - .open_database::, SerdeBincode>( - &txn, - Some("bodies"), - )? - else { - return Ok(()); - }; - let mut bodies_v1 = bodies_v1_db - .iter(&txn)? - .collect::>>()?; - - let Some(metadata_v1_db) = env - .open_database::, SerdeBincode>( - &txn, - Some("metadata"), - )? - else { - return Ok(()); - }; - let metadata_v1 = metadata_v1_db - .iter(&txn)? - .collect::>>()?; - - for (prompt_id_v1, metadata_v1) in metadata_v1 { - let prompt_id_v2 = UserPromptId(prompt_id_v1.0).into(); - let Some(body_v1) = bodies_v1.remove(&prompt_id_v1) else { - continue; - }; - - if metadata_db - .get(&txn, &prompt_id_v2)? - .is_none_or(|metadata_v2| metadata_v1.saved_at > metadata_v2.saved_at) - { - metadata_db.put( - &mut txn, - &prompt_id_v2, - &PromptMetadata { - id: prompt_id_v2, - title: metadata_v1.title.clone(), - default: metadata_v1.default, - saved_at: metadata_v1.saved_at, - }, - )?; - bodies_db.put(&mut txn, &prompt_id_v2, &body_v1)?; - } - } - - txn.commit()?; - - Ok(()) - } - - pub fn load(&self, id: PromptId, cx: &App) -> Task> { - let env = self.env.clone(); - let bodies = self.bodies; - cx.background_spawn(async move { - let txn = env.read_txn()?; - let mut prompt = bodies.get(&txn, &id)?.context("prompt not found")?.into(); - LineEnding::normalize(&mut prompt); - Ok(prompt) - }) - } - - pub fn all_prompt_metadata(&self) -> Vec { - self.metadata_cache.read().metadata.clone() - } - - pub fn default_prompt_metadata(&self) -> Vec { - return self - .metadata_cache - .read() - .metadata - .iter() - .filter(|metadata| metadata.default) - .cloned() - .collect::>(); - } - - pub fn delete(&self, id: PromptId, cx: &Context) -> Task> { - self.metadata_cache.write().remove(id); - - let db_connection = self.env.clone(); - let bodies = self.bodies; - let metadata = self.metadata; - - let task = cx.background_spawn(async move { - let mut txn = db_connection.write_txn()?; - - metadata.delete(&mut txn, &id)?; - bodies.delete(&mut txn, &id)?; - - txn.commit()?; - anyhow::Ok(()) - }); - - cx.spawn(async move |this, cx| { - task.await?; - this.update(cx, |_, cx| cx.emit(PromptsUpdatedEvent)).ok(); - anyhow::Ok(()) - }) - } - - /// Returns the number of prompts in the store. - pub fn prompt_count(&self) -> usize { - self.metadata_cache.read().metadata.len() - } - - pub fn metadata(&self, id: PromptId) -> Option { - self.metadata_cache.read().metadata_by_id.get(&id).cloned() - } - - pub fn first(&self) -> Option { - self.metadata_cache.read().metadata.first().cloned() - } - - pub fn id_for_title(&self, title: &str) -> Option { - let metadata_cache = self.metadata_cache.read(); - let metadata = metadata_cache - .metadata - .iter() - .find(|metadata| metadata.title.as_ref().map(|title| &***title) == Some(title))?; - Some(metadata.id) - } - - pub fn search( - &self, - query: String, - cancellation_flag: Arc, - cx: &App, - ) -> Task> { - let cached_metadata = self.metadata_cache.read().metadata.clone(); - let executor = cx.background_executor().clone(); - cx.background_spawn(async move { - let mut matches = if query.is_empty() { - cached_metadata - } else { - let candidates = cached_metadata - .iter() - .enumerate() - .filter_map(|(ix, metadata)| { - Some(StringMatchCandidate::new(ix, metadata.title.as_ref()?)) - }) - .collect::>(); - let matches = fuzzy::match_strings( - &candidates, - &query, - false, - true, - 100, - &cancellation_flag, - executor, - ) - .await; - matches - .into_iter() - .map(|mat| cached_metadata[mat.candidate_id].clone()) - .collect() - }; - matches.sort_by_key(|metadata| Reverse(metadata.default)); - matches - }) - } - - pub fn save( - &self, - id: PromptId, - title: Option, - default: bool, - body: Rope, - cx: &Context, - ) -> Task> { - if id.is_built_in() { - return Task::ready(Err(anyhow!("built-in prompts cannot be saved"))); - } - - let prompt_metadata = PromptMetadata { - id, - title, - default, - saved_at: Utc::now(), - }; - self.metadata_cache.write().insert(prompt_metadata.clone()); - - let db_connection = self.env.clone(); - let bodies = self.bodies; - let metadata = self.metadata; - - let task = cx.background_spawn(async move { - let mut txn = db_connection.write_txn()?; - - metadata.put(&mut txn, &id, &prompt_metadata)?; - bodies.put(&mut txn, &id, &body.to_string())?; - - txn.commit()?; - - anyhow::Ok(()) - }); - - cx.spawn(async move |this, cx| { - task.await?; - this.update(cx, |_, cx| cx.emit(PromptsUpdatedEvent)).ok(); - anyhow::Ok(()) - }) - } - - pub fn save_metadata( - &self, - id: PromptId, - mut title: Option, - default: bool, - cx: &Context, - ) -> Task> { - let mut cache = self.metadata_cache.write(); - - if id.is_built_in() { - title = cache - .metadata_by_id - .get(&id) - .and_then(|metadata| metadata.title.clone()); - } - - let prompt_metadata = PromptMetadata { - id, - title, - default, - saved_at: Utc::now(), - }; - - cache.insert(prompt_metadata.clone()); - - let db_connection = self.env.clone(); - let metadata = self.metadata; - - let task = cx.background_spawn(async move { - let mut txn = db_connection.write_txn()?; - metadata.put(&mut txn, &id, &prompt_metadata)?; - txn.commit()?; - - anyhow::Ok(()) - }); - - cx.spawn(async move |this, cx| { - task.await?; - this.update(cx, |_, cx| cx.emit(PromptsUpdatedEvent)).ok(); - anyhow::Ok(()) - }) - } -} - -/// Wraps a shared future to a prompt store so it can be assigned as a context global. -pub struct GlobalPromptStore(Shared, Arc>>>); - -impl Global for GlobalPromptStore {} diff --git a/crates/prompt_store/src/prompts.rs b/crates/prompt_store/src/prompts.rs deleted file mode 100644 index d6a172218a..0000000000 --- a/crates/prompt_store/src/prompts.rs +++ /dev/null @@ -1,476 +0,0 @@ -use anyhow::Result; -use assets::Assets; -use fs::Fs; -use futures::StreamExt; -use gpui::{App, AppContext as _, AssetSource}; -use handlebars::{Handlebars, RenderError}; -use language::{BufferSnapshot, LanguageName, Point}; -use parking_lot::Mutex; -use serde::Serialize; -use std::{ - ops::Range, - path::{Path, PathBuf}, - sync::Arc, - time::Duration, -}; -use text::LineEnding; -use util::{ - ResultExt, get_default_system_shell_preferring_bash, rel_path::RelPath, shell::ShellKind, -}; - -use crate::UserPromptId; - -#[derive(Default, Debug, Clone, Serialize)] -pub struct ProjectContext { - pub worktrees: Vec, - /// Whether any worktree has a rules_file. Provided as a field because handlebars can't do this. - pub has_rules: bool, - pub user_rules: Vec, - /// `!user_rules.is_empty()` - provided as a field because handlebars can't do this. - pub has_user_rules: bool, - pub os: String, - pub arch: String, - pub shell: String, -} - -impl ProjectContext { - pub fn new(worktrees: Vec, default_user_rules: Vec) -> Self { - let has_rules = worktrees - .iter() - .any(|worktree| worktree.rules_file.is_some()); - Self { - worktrees, - has_rules, - has_user_rules: !default_user_rules.is_empty(), - user_rules: default_user_rules, - os: std::env::consts::OS.to_string(), - arch: std::env::consts::ARCH.to_string(), - shell: ShellKind::new(&get_default_system_shell_preferring_bash(), cfg!(windows)) - .to_string(), - } - } -} - -#[derive(Debug, Clone, Serialize)] -pub struct UserRulesContext { - pub uuid: UserPromptId, - pub title: Option, - pub contents: String, -} - -#[derive(Debug, Clone, Eq, PartialEq, Serialize)] -pub struct WorktreeContext { - pub root_name: String, - pub abs_path: Arc, - pub rules_file: Option, -} - -#[derive(Debug, Clone, Eq, PartialEq, Serialize)] -pub struct RulesFileContext { - pub path_in_worktree: Arc, - pub text: String, - // This used for opening rules files. TODO: Since it isn't related to prompt templating, this - // should be moved elsewhere. - #[serde(skip)] - pub project_entry_id: usize, -} - -#[derive(Serialize)] -pub struct ContentPromptDiagnosticContext { - pub line_number: usize, - pub error_message: String, - pub code_content: String, -} - -#[derive(Serialize)] -pub struct ContentPromptContext { - pub content_type: String, - pub language_name: Option, - pub is_insert: bool, - pub is_truncated: bool, - pub document_content: String, - pub user_prompt: String, - pub rewrite_section: Option, - pub diagnostic_errors: Vec, -} - -#[derive(Serialize)] -pub struct ContentPromptContextV2 { - pub content_type: String, - pub language_name: Option, - pub is_truncated: bool, - pub document_content: String, - pub rewrite_section: Option, - pub diagnostic_errors: Vec, -} - -#[derive(Serialize)] -pub struct TerminalAssistantPromptContext { - pub os: String, - pub arch: String, - pub shell: Option, - pub working_directory: Option, - pub latest_output: Vec, - pub user_prompt: String, -} - -pub struct PromptLoadingParams<'a> { - pub fs: Arc, - pub repo_path: Option, - pub cx: &'a gpui::App, -} - -pub struct PromptBuilder { - handlebars: Arc>>, -} - -impl PromptBuilder { - pub fn load(fs: Arc, stdout_is_a_pty: bool, cx: &mut App) -> Arc { - Self::new(Some(PromptLoadingParams { - fs: fs.clone(), - repo_path: stdout_is_a_pty - .then(|| std::env::current_dir().log_err()) - .flatten(), - cx, - })) - .log_err() - .map(Arc::new) - .unwrap_or_else(|| Arc::new(Self::new(None).unwrap())) - } - - pub fn new(loading_params: Option) -> Result { - let mut handlebars = Handlebars::new(); - Self::register_built_in_templates(&mut handlebars)?; - - let handlebars = Arc::new(Mutex::new(handlebars)); - - if let Some(params) = loading_params { - Self::watch_fs_for_template_overrides(params, handlebars.clone()); - } - - Ok(Self { handlebars }) - } - - /// Watches the filesystem for changes to prompt template overrides. - /// - /// This function sets up a file watcher on the prompt templates directory. It performs - /// an initial scan of the directory and registers any existing template overrides. - /// Then it continuously monitors for changes, reloading templates as they are - /// modified or added. - /// - /// If the templates directory doesn't exist initially, it waits for it to be created. - /// If the directory is removed, it restores the built-in templates and waits for the - /// directory to be recreated. - /// - /// # Arguments - /// - /// * `params` - A `PromptLoadingParams` struct containing the filesystem, repository path, - /// and application context. - /// * `handlebars` - An `Arc>` for registering and updating templates. - fn watch_fs_for_template_overrides( - params: PromptLoadingParams, - handlebars: Arc>>, - ) { - let templates_dir = paths::prompt_overrides_dir(params.repo_path.as_deref()); - params.cx.background_spawn(async move { - let Some(parent_dir) = templates_dir.parent() else { - return; - }; - - let mut found_dir_once = false; - loop { - // Check if the templates directory exists and handle its status - // If it exists, log its presence and check if it's a symlink - // If it doesn't exist: - // - Log that we're using built-in prompts - // - Check if it's a broken symlink and log if so - // - Set up a watcher to detect when it's created - // After the first check, set the `found_dir_once` flag - // This allows us to avoid logging when looping back around after deleting the prompt overrides directory. - let dir_status = params.fs.is_dir(&templates_dir).await; - let symlink_status = params.fs.read_link(&templates_dir).await.ok(); - if dir_status { - let mut log_message = format!("Prompt template overrides directory found at {}", templates_dir.display()); - if let Some(target) = symlink_status { - log_message.push_str(" -> "); - log_message.push_str(&target.display().to_string()); - } - log::trace!("{}.", log_message); - } else { - if !found_dir_once { - log::trace!("No prompt template overrides directory found at {}. Using built-in prompts.", templates_dir.display()); - if let Some(target) = symlink_status { - log::trace!("Symlink found pointing to {}, but target is invalid.", target.display()); - } - } - - if params.fs.is_dir(parent_dir).await { - let (mut changes, _watcher) = params.fs.watch(parent_dir, Duration::from_secs(1)).await; - while let Some(changed_paths) = changes.next().await { - if changed_paths.iter().any(|p| &p.path == &templates_dir) { - let mut log_message = format!("Prompt template overrides directory detected at {}", templates_dir.display()); - if let Ok(target) = params.fs.read_link(&templates_dir).await { - log_message.push_str(" -> "); - log_message.push_str(&target.display().to_string()); - } - log::trace!("{}.", log_message); - break; - } - } - } else { - return; - } - } - - found_dir_once = true; - - // Initial scan of the prompt overrides directory - if let Ok(mut entries) = params.fs.read_dir(&templates_dir).await { - while let Some(Ok(file_path)) = entries.next().await { - if file_path.to_string_lossy().ends_with(".hbs") - && let Ok(content) = params.fs.load(&file_path).await { - let file_name = file_path.file_stem().unwrap().to_string_lossy(); - log::debug!("Registering prompt template override: {}", file_name); - handlebars.lock().register_template_string(&file_name, content).log_err(); - } - } - } - - // Watch both the parent directory and the template overrides directory: - // - Monitor the parent directory to detect if the template overrides directory is deleted. - // - Monitor the template overrides directory to re-register templates when they change. - // Combine both watch streams into a single stream. - let (parent_changes, parent_watcher) = params.fs.watch(parent_dir, Duration::from_secs(1)).await; - let (changes, watcher) = params.fs.watch(&templates_dir, Duration::from_secs(1)).await; - let mut combined_changes = futures::stream::select(changes, parent_changes); - - while let Some(changed_paths) = combined_changes.next().await { - if changed_paths.iter().any(|p| &p.path == &templates_dir) - && !params.fs.is_dir(&templates_dir).await { - log::info!("Prompt template overrides directory removed. Restoring built-in prompt templates."); - Self::register_built_in_templates(&mut handlebars.lock()).log_err(); - break; - } - for event in changed_paths { - if event.path.starts_with(&templates_dir) && event.path.extension().is_some_and(|ext| ext == "hbs") { - log::info!("Reloading prompt template override: {}", event.path.display()); - if let Some(content) = params.fs.load(&event.path).await.log_err() { - let file_name = event.path.file_stem().unwrap().to_string_lossy(); - handlebars.lock().register_template_string(&file_name, content).log_err(); - } - } - } - } - - drop(watcher); - drop(parent_watcher); - } - }) - .detach(); - } - - fn register_built_in_templates(handlebars: &mut Handlebars) -> Result<()> { - for path in Assets.list("prompts")? { - if let Some(id) = path - .split('/') - .next_back() - .and_then(|s| s.strip_suffix(".hbs")) - && let Some(prompt) = Assets.load(path.as_ref()).log_err().flatten() - { - log::debug!("Registering built-in prompt template: {}", id); - let prompt = String::from_utf8_lossy(prompt.as_ref()); - handlebars.register_template_string(id, LineEnding::normalize_cow(prompt))? - } - } - - Ok(()) - } - - pub fn generate_inline_transformation_prompt_v2( - &self, - language_name: Option<&LanguageName>, - buffer: BufferSnapshot, - range: Range, - ) -> Result { - let content_type = match language_name.as_ref().map(|l| l.as_ref()) { - None | Some("Markdown" | "Plain Text") => "text", - Some(_) => "code", - }; - - const MAX_CTX: usize = 50000; - let is_insert = range.is_empty(); - let mut is_truncated = false; - - let before_range = 0..range.start; - let truncated_before = if before_range.len() > MAX_CTX { - is_truncated = true; - let start = buffer.clip_offset(range.start - MAX_CTX, text::Bias::Right); - start..range.start - } else { - before_range - }; - - let after_range = range.end..buffer.len(); - let truncated_after = if after_range.len() > MAX_CTX { - is_truncated = true; - let end = buffer.clip_offset(range.end + MAX_CTX, text::Bias::Left); - range.end..end - } else { - after_range - }; - - let mut document_content = String::new(); - for chunk in buffer.text_for_range(truncated_before) { - document_content.push_str(chunk); - } - if is_insert { - document_content.push_str(""); - } else { - document_content.push_str("\n"); - for chunk in buffer.text_for_range(range.clone()) { - document_content.push_str(chunk); - } - document_content.push_str("\n"); - } - for chunk in buffer.text_for_range(truncated_after) { - document_content.push_str(chunk); - } - - let rewrite_section = if !is_insert { - let mut section = String::new(); - for chunk in buffer.text_for_range(range.clone()) { - section.push_str(chunk); - } - Some(section) - } else { - None - }; - let diagnostics = buffer.diagnostics_in_range::<_, Point>(range, false); - let diagnostic_errors: Vec = diagnostics - .map(|entry| { - let start = entry.range.start; - ContentPromptDiagnosticContext { - line_number: (start.row + 1) as usize, - error_message: entry.diagnostic.message.clone(), - code_content: buffer.text_for_range(entry.range).collect(), - } - }) - .collect(); - - let context = ContentPromptContextV2 { - content_type: content_type.to_string(), - language_name: language_name.map(|s| s.to_string()), - is_truncated, - document_content, - rewrite_section, - diagnostic_errors, - }; - self.handlebars.lock().render("content_prompt_v2", &context) - } - - pub fn generate_inline_transformation_prompt( - &self, - user_prompt: String, - language_name: Option<&LanguageName>, - buffer: BufferSnapshot, - range: Range, - ) -> Result { - let content_type = match language_name.as_ref().map(|l| l.as_ref()) { - None | Some("Markdown" | "Plain Text") => "text", - Some(_) => "code", - }; - - const MAX_CTX: usize = 50000; - let is_insert = range.is_empty(); - let mut is_truncated = false; - - let before_range = 0..range.start; - let truncated_before = if before_range.len() > MAX_CTX { - is_truncated = true; - let start = buffer.clip_offset(range.start - MAX_CTX, text::Bias::Right); - start..range.start - } else { - before_range - }; - - let after_range = range.end..buffer.len(); - let truncated_after = if after_range.len() > MAX_CTX { - is_truncated = true; - let end = buffer.clip_offset(range.end + MAX_CTX, text::Bias::Left); - range.end..end - } else { - after_range - }; - - let mut document_content = String::new(); - for chunk in buffer.text_for_range(truncated_before) { - document_content.push_str(chunk); - } - if is_insert { - document_content.push_str(""); - } else { - document_content.push_str("\n"); - for chunk in buffer.text_for_range(range.clone()) { - document_content.push_str(chunk); - } - document_content.push_str("\n"); - } - for chunk in buffer.text_for_range(truncated_after) { - document_content.push_str(chunk); - } - - let rewrite_section = if !is_insert { - let mut section = String::new(); - for chunk in buffer.text_for_range(range.clone()) { - section.push_str(chunk); - } - Some(section) - } else { - None - }; - let diagnostics = buffer.diagnostics_in_range::<_, Point>(range, false); - let diagnostic_errors: Vec = diagnostics - .map(|entry| { - let start = entry.range.start; - ContentPromptDiagnosticContext { - line_number: (start.row + 1) as usize, - error_message: entry.diagnostic.message.clone(), - code_content: buffer.text_for_range(entry.range).collect(), - } - }) - .collect(); - - let context = ContentPromptContext { - content_type: content_type.to_string(), - language_name: language_name.map(|s| s.to_string()), - is_insert, - is_truncated, - document_content, - user_prompt, - rewrite_section, - diagnostic_errors, - }; - self.handlebars.lock().render("content_prompt", &context) - } - - pub fn generate_terminal_assistant_prompt( - &self, - user_prompt: &str, - shell: Option<&str>, - working_directory: Option<&str>, - latest_output: &[String], - ) -> Result { - let context = TerminalAssistantPromptContext { - os: std::env::consts::OS.to_string(), - arch: std::env::consts::ARCH.to_string(), - shell: shell.map(|s| s.to_string()), - working_directory: working_directory.map(|s| s.to_string()), - latest_output: latest_output.to_vec(), - user_prompt: user_prompt.to_string(), - }; - - self.handlebars - .lock() - .render("terminal_assistant_prompt", &context) - } -} diff --git a/crates/proto/Cargo.toml b/crates/proto/Cargo.toml deleted file mode 100644 index 5b5b8b985c..0000000000 --- a/crates/proto/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -description = "Shared protocol for communication between the Zed app and the zed.dev server" -edition.workspace = true -name = "proto" -version = "0.1.0" -publish.workspace = true -license = "GPL-3.0-or-later" - -[features] -test-support = ["collections/test-support"] - -[lints] -workspace = true - -[lib] -path = "src/proto.rs" -doctest = false - -[dependencies] -anyhow.workspace = true -prost.workspace = true -serde.workspace = true - -[build-dependencies] -prost-build.workspace = true - -[dev-dependencies] -collections = { workspace = true, features = ["test-support"] } -typed-path = "0.11" diff --git a/crates/proto/LICENSE-GPL b/crates/proto/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/proto/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/proto/build.rs b/crates/proto/build.rs deleted file mode 100644 index 184d0e53d5..0000000000 --- a/crates/proto/build.rs +++ /dev/null @@ -1,10 +0,0 @@ -fn main() { - println!("cargo:rerun-if-changed=proto"); - let mut build = prost_build::Config::new(); - build - .type_attribute(".", "#[derive(serde::Serialize, serde::Deserialize)]") - .type_attribute("ProjectPath", "#[derive(Hash, Eq)]") - .type_attribute("Anchor", "#[derive(Hash, Eq)]") - .compile_protos(&["proto/zed.proto"], &["proto"]) - .unwrap(); -} diff --git a/crates/proto/proto/ai.proto b/crates/proto/proto/ai.proto deleted file mode 100644 index 2216446a82..0000000000 --- a/crates/proto/proto/ai.proto +++ /dev/null @@ -1,220 +0,0 @@ -syntax = "proto3"; -package zed.messages; - -import "buffer.proto"; -import "task.proto"; - -message Context { - repeated ContextOperation operations = 1; -} - -message ContextMetadata { - string context_id = 1; - optional string summary = 2; -} - -message ContextMessageStatus { - oneof variant { - Done done = 1; - Pending pending = 2; - Error error = 3; - Canceled canceled = 4; - } - - message Done {} - - message Pending {} - - message Error { - string message = 1; - } - - message Canceled {} -} - -message ContextMessage { - LamportTimestamp id = 1; - Anchor start = 2; - LanguageModelRole role = 3; - ContextMessageStatus status = 4; -} - -message SlashCommandOutputSection { - AnchorRange range = 1; - string icon_name = 2; - string label = 3; - optional string metadata = 4; -} - -message ThoughtProcessOutputSection { - AnchorRange range = 1; -} - -message ContextOperation { - oneof variant { - InsertMessage insert_message = 1; - UpdateMessage update_message = 2; - UpdateSummary update_summary = 3; - BufferOperation buffer_operation = 5; - SlashCommandStarted slash_command_started = 6; - SlashCommandOutputSectionAdded slash_command_output_section_added = 7; - SlashCommandCompleted slash_command_completed = 8; - ThoughtProcessOutputSectionAdded thought_process_output_section_added = 9; - } - - reserved 4; - - message InsertMessage { - ContextMessage message = 1; - repeated VectorClockEntry version = 2; - } - - message UpdateMessage { - LamportTimestamp message_id = 1; - LanguageModelRole role = 2; - ContextMessageStatus status = 3; - LamportTimestamp timestamp = 4; - repeated VectorClockEntry version = 5; - } - - message UpdateSummary { - string summary = 1; - bool done = 2; - LamportTimestamp timestamp = 3; - repeated VectorClockEntry version = 4; - } - - message SlashCommandStarted { - LamportTimestamp id = 1; - AnchorRange output_range = 2; - string name = 3; - repeated VectorClockEntry version = 4; - } - - message SlashCommandOutputSectionAdded { - LamportTimestamp timestamp = 1; - SlashCommandOutputSection section = 2; - repeated VectorClockEntry version = 3; - } - - message SlashCommandCompleted { - LamportTimestamp id = 1; - LamportTimestamp timestamp = 3; - optional string error_message = 4; - repeated VectorClockEntry version = 5; - } - - message ThoughtProcessOutputSectionAdded { - LamportTimestamp timestamp = 1; - ThoughtProcessOutputSection section = 2; - repeated VectorClockEntry version = 3; - } - - message BufferOperation { - Operation operation = 1; - } -} - -message AdvertiseContexts { - uint64 project_id = 1; - repeated ContextMetadata contexts = 2; -} - -message OpenContext { - uint64 project_id = 1; - string context_id = 2; -} - -message OpenContextResponse { - Context context = 1; -} - -message CreateContext { - uint64 project_id = 1; -} - -message CreateContextResponse { - string context_id = 1; - Context context = 2; -} - -message UpdateContext { - uint64 project_id = 1; - string context_id = 2; - ContextOperation operation = 3; -} - -message ContextVersion { - string context_id = 1; - repeated VectorClockEntry context_version = 2; - repeated VectorClockEntry buffer_version = 3; -} - -message SynchronizeContexts { - uint64 project_id = 1; - repeated ContextVersion contexts = 2; -} - -message SynchronizeContextsResponse { - repeated ContextVersion contexts = 1; -} - -enum LanguageModelRole { - LanguageModelUser = 0; - LanguageModelAssistant = 1; - LanguageModelSystem = 2; - reserved 3; -} - -message GetAgentServerCommand { - uint64 project_id = 1; - string name = 2; - optional string root_dir = 3; -} - -message AgentServerCommand { - string path = 1; - repeated string args = 2; - map env = 3; - string root_dir = 4; - - optional SpawnInTerminal login = 5; -} - -message ExternalAgentsUpdated { - uint64 project_id = 1; - repeated string names = 2; -} - -message ExternalExtensionAgentTarget { - string archive = 1; - string cmd = 2; - repeated string args = 3; - optional string sha256 = 4; - map env = 5; -} - -message ExternalExtensionAgent { - string name = 1; - optional string icon_path = 2; - string extension_id = 3; - map targets = 4; - map env = 5; -} - -message ExternalExtensionAgentsUpdated { - uint64 project_id = 1; - repeated ExternalExtensionAgent agents = 2; -} - -message ExternalAgentLoadingStatusUpdated { - uint64 project_id = 1; - string name = 2; - string status = 3; -} - -message NewExternalAgentVersionAvailable { - uint64 project_id = 1; - string name = 2; - string version = 3; -} diff --git a/crates/proto/proto/app.proto b/crates/proto/proto/app.proto deleted file mode 100644 index 889086e200..0000000000 --- a/crates/proto/proto/app.proto +++ /dev/null @@ -1,76 +0,0 @@ -syntax = "proto3"; -package zed.messages; - -message UpdateInviteInfo { - string url = 1; - uint32 count = 2; -} - -message ShutdownRemoteServer {} - -message Toast { - uint64 project_id = 1; - string notification_id = 2; - string message = 3; -} - -message HideToast { - uint64 project_id = 1; - string notification_id = 2; -} - -message OpenServerSettings { - uint64 project_id = 1; -} - -message GetCrashFiles { -} - -message GetCrashFilesResponse { - repeated CrashReport crashes = 1; - reserved 2; // old panics -} - -message CrashReport { - reserved 1, 2; - string metadata = 3; - bytes minidump_contents = 4; -} - -message Extension { - string id = 1; - string version = 2; - bool dev = 3; -} - -message SyncExtensions { - repeated Extension extensions = 1; -} - -message SyncExtensionsResponse { - string tmp_dir = 1; - repeated Extension missing_extensions = 2; -} - -message InstallExtension { - Extension extension = 1; - string tmp_dir = 2; -} - -message AskPassRequest { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - uint64 askpass_id = 4; - string prompt = 5; -} - -message AskPassResponse { - string response = 1; -} - -message GetSupermavenApiKey {} - -message GetSupermavenApiKeyResponse { - string api_key = 1; -} diff --git a/crates/proto/proto/buf.yaml b/crates/proto/proto/buf.yaml deleted file mode 100644 index 93e819b2f7..0000000000 --- a/crates/proto/proto/buf.yaml +++ /dev/null @@ -1,4 +0,0 @@ -version: v1 -breaking: - use: - - WIRE diff --git a/crates/proto/proto/buffer.proto b/crates/proto/proto/buffer.proto deleted file mode 100644 index 486716b36a..0000000000 --- a/crates/proto/proto/buffer.proto +++ /dev/null @@ -1,315 +0,0 @@ -syntax = "proto3"; -package zed.messages; - -import "core.proto"; -import "worktree.proto"; - -message OpenNewBuffer { - uint64 project_id = 1; -} - -message OpenBufferResponse { - uint64 buffer_id = 1; -} - -message CreateBufferForPeer { - uint64 project_id = 1; - PeerId peer_id = 2; - oneof variant { - BufferState state = 3; - BufferChunk chunk = 4; - } -} - -message UpdateBuffer { - uint64 project_id = 1; - uint64 buffer_id = 2; - repeated Operation operations = 3; -} - -message OpenBufferByPath { - uint64 project_id = 1; - uint64 worktree_id = 2; - string path = 3; -} - -message OpenBufferById { - uint64 project_id = 1; - uint64 id = 2; -} - -message UpdateBufferFile { - uint64 project_id = 1; - uint64 buffer_id = 2; - File file = 3; -} - -message SaveBuffer { - uint64 project_id = 1; - uint64 buffer_id = 2; - repeated VectorClockEntry version = 3; - optional ProjectPath new_path = 4; -} - -message CloseBuffer { - uint64 project_id = 1; - uint64 buffer_id = 2; -} - -message BufferSaved { - uint64 project_id = 1; - uint64 buffer_id = 2; - repeated VectorClockEntry version = 3; - Timestamp mtime = 4; - reserved 5; -} - -message BufferReloaded { - uint64 project_id = 1; - uint64 buffer_id = 2; - repeated VectorClockEntry version = 3; - Timestamp mtime = 4; - reserved 5; - LineEnding line_ending = 6; -} - -message ReloadBuffers { - uint64 project_id = 1; - repeated uint64 buffer_ids = 2; -} - -message ReloadBuffersResponse { - ProjectTransaction transaction = 1; -} - -message SynchronizeBuffers { - uint64 project_id = 1; - repeated BufferVersion buffers = 2; -} - -message SynchronizeBuffersResponse { - repeated BufferVersion buffers = 1; -} - -message BufferVersion { - uint64 id = 1; - repeated VectorClockEntry version = 2; -} - -message BufferState { - uint64 id = 1; - optional File file = 2; - string base_text = 3; - LineEnding line_ending = 5; - repeated VectorClockEntry saved_version = 6; - Timestamp saved_mtime = 8; - - reserved 7; - reserved 4; -} - -message BufferChunk { - uint64 buffer_id = 1; - repeated Operation operations = 2; - bool is_last = 3; -} - -enum LineEnding { - Unix = 0; - Windows = 1; -} - -message VectorClockEntry { - uint32 replica_id = 1; - uint32 timestamp = 2; -} - -message UndoMapEntry { - uint32 replica_id = 1; - uint32 local_timestamp = 2; - repeated UndoCount counts = 3; -} - -message UndoCount { - uint32 replica_id = 1; - uint32 lamport_timestamp = 2; - uint32 count = 3; -} - -message Operation { - oneof variant { - Edit edit = 1; - Undo undo = 2; - UpdateSelections update_selections = 3; - UpdateDiagnostics update_diagnostics = 4; - UpdateCompletionTriggers update_completion_triggers = 5; - UpdateLineEnding update_line_ending = 6; - } - - message Edit { - uint32 replica_id = 1; - uint32 lamport_timestamp = 2; - repeated VectorClockEntry version = 3; - repeated Range ranges = 4; - repeated string new_text = 5; - } - - message Undo { - uint32 replica_id = 1; - uint32 lamport_timestamp = 2; - repeated VectorClockEntry version = 3; - repeated UndoCount counts = 4; - } - - message UpdateSelections { - uint32 replica_id = 1; - uint32 lamport_timestamp = 2; - repeated Selection selections = 3; - bool line_mode = 4; - CursorShape cursor_shape = 5; - } - - message UpdateCompletionTriggers { - uint32 replica_id = 1; - uint32 lamport_timestamp = 2; - repeated string triggers = 3; - uint64 language_server_id = 4; - } - - message UpdateLineEnding { - uint32 replica_id = 1; - uint32 lamport_timestamp = 2; - LineEnding line_ending = 3; - } -} - -message ProjectTransaction { - repeated uint64 buffer_ids = 1; - repeated Transaction transactions = 2; -} - -message Transaction { - LamportTimestamp id = 1; - repeated LamportTimestamp edit_ids = 2; - repeated VectorClockEntry start = 3; -} - -message LamportTimestamp { - uint32 replica_id = 1; - uint32 value = 2; -} - -message Range { - uint64 start = 1; - uint64 end = 2; -} - -message Selection { - uint64 id = 1; - EditorAnchor start = 2; - EditorAnchor end = 3; - bool reversed = 4; -} - -message EditorAnchor { - uint64 excerpt_id = 1; - Anchor anchor = 2; -} - -enum CursorShape { - CursorBar = 0; - CursorBlock = 1; - CursorUnderscore = 2; - CursorHollow = 3; -} - -message UpdateDiagnostics { - uint32 replica_id = 1; - uint32 lamport_timestamp = 2; - uint64 server_id = 3; - repeated Diagnostic diagnostics = 4; -} - -message Anchor { - uint32 replica_id = 1; - uint32 timestamp = 2; - uint64 offset = 3; - Bias bias = 4; - optional uint64 buffer_id = 5; -} - -message AnchorRange { - Anchor start = 1; - Anchor end = 2; -} - -message Location { - uint64 buffer_id = 1; - Anchor start = 2; - Anchor end = 3; -} - -enum Bias { - Left = 0; - Right = 1; -} - -message Diagnostic { - Anchor start = 1; - Anchor end = 2; - optional string source = 3; - optional string registration_id = 17; - - enum SourceKind { - Pulled = 0; - Pushed = 1; - Other = 2; - } - - SourceKind source_kind = 16; - Severity severity = 4; - string message = 5; - optional string code = 6; - uint64 group_id = 7; - bool is_primary = 8; - - reserved 9; - - bool is_disk_based = 10; - bool is_unnecessary = 11; - bool underline = 15; - - enum Severity { - None = 0; - Error = 1; - Warning = 2; - Information = 3; - Hint = 4; - } - optional string data = 12; - optional string code_description = 13; - optional string markdown = 14; -} - -message SearchQuery { - string query = 2; - bool regex = 3; - bool whole_word = 4; - bool case_sensitive = 5; - repeated string files_to_include = 10; - repeated string files_to_exclude = 11; - bool match_full_paths = 9; - bool include_ignored = 8; - string files_to_include_legacy = 6; - string files_to_exclude_legacy = 7; -} - -message FindSearchCandidates { - uint64 project_id = 1; - SearchQuery query = 2; - uint64 limit = 3; -} - -message FindSearchCandidatesResponse { - repeated uint64 buffer_ids = 1; -} diff --git a/crates/proto/proto/call.proto b/crates/proto/proto/call.proto deleted file mode 100644 index 9e801515af..0000000000 --- a/crates/proto/proto/call.proto +++ /dev/null @@ -1,427 +0,0 @@ -syntax = "proto3"; -package zed.messages; - -import "core.proto"; -import "worktree.proto"; -import "buffer.proto"; -import "lsp.proto"; -import "channel.proto"; -import "git.proto"; - -message CreateRoom {} - -message CreateRoomResponse { - Room room = 1; - optional LiveKitConnectionInfo live_kit_connection_info = 2; -} - -message JoinRoom { - uint64 id = 1; -} - -message JoinRoomResponse { - Room room = 1; - optional uint64 channel_id = 2; - optional LiveKitConnectionInfo live_kit_connection_info = 3; -} - -message RejoinRoom { - uint64 id = 1; - repeated UpdateProject reshared_projects = 2; - repeated RejoinProject rejoined_projects = 3; -} - -message RejoinRemoteProjects { - repeated RejoinProject rejoined_projects = 1; -} - -message RejoinRemoteProjectsResponse { - repeated RejoinedProject rejoined_projects = 1; -} - -message RejoinProject { - uint64 id = 1; - repeated RejoinWorktree worktrees = 2; - repeated RejoinRepository repositories = 3; -} - -message RejoinWorktree { - uint64 id = 1; - uint64 scan_id = 2; -} - -message RejoinRepository { - uint64 id = 1; - uint64 scan_id = 2; -} - -message RejoinRoomResponse { - Room room = 1; - repeated ResharedProject reshared_projects = 2; - repeated RejoinedProject rejoined_projects = 3; -} - -message ResharedProject { - uint64 id = 1; - repeated Collaborator collaborators = 2; -} - -message RejoinedProject { - uint64 id = 1; - repeated WorktreeMetadata worktrees = 2; - repeated Collaborator collaborators = 3; - repeated LanguageServer language_servers = 4; - repeated string language_server_capabilities = 5; -} - -message LeaveRoom {} - -message Room { - uint64 id = 1; - repeated Participant participants = 2; - repeated PendingParticipant pending_participants = 3; - repeated Follower followers = 4; - string livekit_room = 5; -} - -message Participant { - uint64 user_id = 1; - PeerId peer_id = 2; - repeated ParticipantProject projects = 3; - ParticipantLocation location = 4; - uint32 participant_index = 5; - ChannelRole role = 6; - reserved 7; -} - -message PendingParticipant { - uint64 user_id = 1; - uint64 calling_user_id = 2; - optional uint64 initial_project_id = 3; -} - -message ParticipantProject { - uint64 id = 1; - repeated string worktree_root_names = 2; -} - -message Follower { - PeerId leader_id = 1; - PeerId follower_id = 2; - uint64 project_id = 3; -} - -message ParticipantLocation { - oneof variant { - SharedProject shared_project = 1; - UnsharedProject unshared_project = 2; - External external = 3; - } - - message SharedProject { - uint64 id = 1; - } - - message UnsharedProject {} - - message External {} -} - -message Call { - uint64 room_id = 1; - uint64 called_user_id = 2; - optional uint64 initial_project_id = 3; -} - -message IncomingCall { - uint64 room_id = 1; - uint64 calling_user_id = 2; - repeated uint64 participant_user_ids = 3; - optional ParticipantProject initial_project = 4; -} - -message CallCanceled { - uint64 room_id = 1; -} - -message CancelCall { - uint64 room_id = 1; - uint64 called_user_id = 2; -} - -message DeclineCall { - uint64 room_id = 1; -} - -message UpdateParticipantLocation { - uint64 room_id = 1; - ParticipantLocation location = 2; -} - -message RoomUpdated { - Room room = 1; -} - -message LiveKitConnectionInfo { - string server_url = 1; - string token = 2; - bool can_publish = 3; -} - -message ShareProject { - uint64 room_id = 1; - repeated WorktreeMetadata worktrees = 2; - reserved 3; - bool is_ssh_project = 4; - optional bool windows_paths = 5; -} - -message ShareProjectResponse { - uint64 project_id = 1; -} - -message UnshareProject { - uint64 project_id = 1; -} - -message UpdateProject { - uint64 project_id = 1; - repeated WorktreeMetadata worktrees = 2; -} - -message JoinProject { - uint64 project_id = 1; - optional string committer_email = 2; - optional string committer_name = 3; -} - -message JoinProjectResponse { - uint64 project_id = 5; - uint32 replica_id = 1; - repeated WorktreeMetadata worktrees = 2; - repeated Collaborator collaborators = 3; - repeated LanguageServer language_servers = 4; - repeated string language_server_capabilities = 8; - ChannelRole role = 6; - bool windows_paths = 9; - reserved 7; -} - -message LeaveProject { - uint64 project_id = 1; -} - -message UpdateWorktree { - uint64 project_id = 1; - uint64 worktree_id = 2; - string root_name = 3; - repeated Entry updated_entries = 4; - repeated uint64 removed_entries = 5; - repeated RepositoryEntry updated_repositories = 6; // deprecated - repeated uint64 removed_repositories = 7; // deprecated - uint64 scan_id = 8; - bool is_last_update = 9; - string abs_path = 10; -} - -// deprecated -message RepositoryEntry { - uint64 repository_id = 1; - reserved 2; - repeated StatusEntry updated_statuses = 3; - repeated string removed_statuses = 4; - repeated string current_merge_conflicts = 5; - optional Branch branch_summary = 6; -} - -message AddProjectCollaborator { - uint64 project_id = 1; - Collaborator collaborator = 2; -} - -message UpdateProjectCollaborator { - uint64 project_id = 1; - PeerId old_peer_id = 2; - PeerId new_peer_id = 3; -} - -message RemoveProjectCollaborator { - uint64 project_id = 1; - PeerId peer_id = 2; -} - -message GetUsers { - repeated uint64 user_ids = 1; -} - -message FuzzySearchUsers { - string query = 1; -} - -message UsersResponse { - repeated User users = 1; -} - -message RequestContact { - uint64 responder_id = 1; -} - -message RemoveContact { - uint64 user_id = 1; -} - -message RespondToContactRequest { - uint64 requester_id = 1; - ContactRequestResponse response = 2; -} - -enum ContactRequestResponse { - Accept = 0; - Decline = 1; - Block = 2; - Dismiss = 3; -} - -message UpdateContacts { - repeated Contact contacts = 1; - repeated uint64 remove_contacts = 2; - repeated IncomingContactRequest incoming_requests = 3; - repeated uint64 remove_incoming_requests = 4; - repeated uint64 outgoing_requests = 5; - repeated uint64 remove_outgoing_requests = 6; -} - -message ShowContacts {} - -message IncomingContactRequest { - uint64 requester_id = 1; -} - -message Follow { - uint64 room_id = 1; - optional uint64 project_id = 2; - PeerId leader_id = 3; -} - -message FollowResponse { - View active_view = 3; - // TODO: Remove after version 0.145.x stabilizes. - optional ViewId active_view_id = 1; - repeated View views = 2; -} - -message UpdateFollowers { - uint64 room_id = 1; - optional uint64 project_id = 2; - reserved 3; - oneof variant { - View create_view = 5; - // TODO: Remove after version 0.145.x stabilizes. - UpdateActiveView update_active_view = 4; - UpdateView update_view = 6; - } -} - -message Unfollow { - uint64 room_id = 1; - optional uint64 project_id = 2; - PeerId leader_id = 3; -} - -message ViewId { - PeerId creator = 1; - uint64 id = 2; -} - -message UpdateActiveView { - optional ViewId id = 1; - optional PeerId leader_id = 2; - View view = 3; -} - -enum PanelId { - AssistantPanel = 0; - DebugPanel = 1; -} - -message UpdateView { - ViewId id = 1; - optional PeerId leader_id = 2; - - oneof variant { - Editor editor = 3; - } - - message Editor { - repeated ExcerptInsertion inserted_excerpts = 1; - repeated uint64 deleted_excerpts = 2; - repeated Selection selections = 3; - optional Selection pending_selection = 4; - EditorAnchor scroll_top_anchor = 5; - reserved 6; - reserved 7; - double scroll_x = 8; - double scroll_y = 9; - } -} - -message View { - ViewId id = 1; - optional PeerId leader_id = 2; - optional PanelId panel_id = 6; - - oneof variant { - Editor editor = 3; - ChannelView channel_view = 4; - ContextEditor context_editor = 5; - } - - message Editor { - bool singleton = 1; - optional string title = 2; - repeated Excerpt excerpts = 3; - repeated Selection selections = 4; - optional Selection pending_selection = 5; - EditorAnchor scroll_top_anchor = 6; - reserved 7; - reserved 8; - double scroll_x = 9; - double scroll_y = 10; - } - - message ChannelView { - uint64 channel_id = 1; - Editor editor = 2; - } - - message ContextEditor { - string context_id = 1; - Editor editor = 2; - } -} - -message ExcerptInsertion { - Excerpt excerpt = 1; - optional uint64 previous_excerpt_id = 2; -} - -message Excerpt { - uint64 id = 1; - uint64 buffer_id = 2; - Anchor context_start = 3; - Anchor context_end = 4; - Anchor primary_start = 5; - Anchor primary_end = 6; -} - -message Contact { - uint64 user_id = 1; - bool online = 2; - bool busy = 3; -} - -message SetRoomParticipantRole { - uint64 room_id = 1; - uint64 user_id = 2; - ChannelRole role = 3; -} diff --git a/crates/proto/proto/channel.proto b/crates/proto/proto/channel.proto deleted file mode 100644 index cada21cd5b..0000000000 --- a/crates/proto/proto/channel.proto +++ /dev/null @@ -1,294 +0,0 @@ -syntax = "proto3"; -package zed.messages; - -import "core.proto"; -import "buffer.proto"; - -message Channel { - uint64 id = 1; - string name = 2; - ChannelVisibility visibility = 3; - int32 channel_order = 4; - repeated uint64 parent_path = 5; -} - -enum ChannelVisibility { - Public = 0; - Members = 1; -} - -message UpdateChannels { - repeated Channel channels = 1; - repeated uint64 delete_channels = 4; - repeated Channel channel_invitations = 5; - repeated uint64 remove_channel_invitations = 6; - repeated ChannelParticipants channel_participants = 7; - repeated ChannelBufferVersion latest_channel_buffer_versions = 9; - - reserved 8; - reserved 10 to 15; -} - -message UpdateUserChannels { - repeated ChannelBufferVersion observed_channel_buffer_version = 2; - repeated ChannelMembership channel_memberships = 3; - - reserved 1; -} - -message ChannelMembership { - uint64 channel_id = 1; - ChannelRole role = 2; -} - -message ChannelMessageId { - uint64 channel_id = 1; - uint64 message_id = 2; -} - -message ChannelPermission { - uint64 channel_id = 1; - ChannelRole role = 3; -} - -message ChannelParticipants { - uint64 channel_id = 1; - repeated uint64 participant_user_ids = 2; -} - -message JoinChannel { - uint64 channel_id = 1; -} - -message DeleteChannel { - uint64 channel_id = 1; -} - -message GetChannelMembers { - uint64 channel_id = 1; - string query = 2; - uint64 limit = 3; -} - -message GetChannelMembersResponse { - repeated ChannelMember members = 1; - repeated User users = 2; -} - -message ChannelMember { - uint64 user_id = 1; - Kind kind = 3; - ChannelRole role = 4; - - enum Kind { - Member = 0; - Invitee = 1; - } -} - -message SubscribeToChannels {} - -message CreateChannel { - string name = 1; - optional uint64 parent_id = 2; -} - -message CreateChannelResponse { - Channel channel = 1; - optional uint64 parent_id = 2; -} - -message InviteChannelMember { - uint64 channel_id = 1; - uint64 user_id = 2; - ChannelRole role = 4; -} - -message RemoveChannelMember { - uint64 channel_id = 1; - uint64 user_id = 2; -} - -enum ChannelRole { - Admin = 0; - Member = 1; - Guest = 2; - Banned = 3; - Talker = 4; -} - -message SetChannelMemberRole { - uint64 channel_id = 1; - uint64 user_id = 2; - ChannelRole role = 3; -} - -message SetChannelVisibility { - uint64 channel_id = 1; - ChannelVisibility visibility = 2; -} - -message RenameChannel { - uint64 channel_id = 1; - string name = 2; -} - -message RenameChannelResponse { - Channel channel = 1; -} - -message JoinChannelChat { - uint64 channel_id = 1; -} - -message JoinChannelChatResponse { - repeated ChannelMessage messages = 1; - bool done = 2; -} - -message LeaveChannelChat { - uint64 channel_id = 1; -} - -message SendChannelMessage { - uint64 channel_id = 1; - string body = 2; - Nonce nonce = 3; - repeated ChatMention mentions = 4; - optional uint64 reply_to_message_id = 5; -} - -message RemoveChannelMessage { - uint64 channel_id = 1; - uint64 message_id = 2; -} - -message UpdateChannelMessage { - uint64 channel_id = 1; - uint64 message_id = 2; - Nonce nonce = 4; - string body = 5; - repeated ChatMention mentions = 6; -} - -message AckChannelMessage { - uint64 channel_id = 1; - uint64 message_id = 2; -} - -message SendChannelMessageResponse { - ChannelMessage message = 1; -} - -message ChannelMessageSent { - uint64 channel_id = 1; - ChannelMessage message = 2; -} - -message ChannelMessageUpdate { - uint64 channel_id = 1; - ChannelMessage message = 2; -} - -message GetChannelMessages { - uint64 channel_id = 1; - uint64 before_message_id = 2; -} - -message GetChannelMessagesResponse { - repeated ChannelMessage messages = 1; - bool done = 2; -} - -message GetChannelMessagesById { - repeated uint64 message_ids = 1; -} - -message MoveChannel { - uint64 channel_id = 1; - uint64 to = 2; -} - -message ReorderChannel { - uint64 channel_id = 1; - enum Direction { - Up = 0; - Down = 1; - } - Direction direction = 2; -} - -message JoinChannelBuffer { - uint64 channel_id = 1; -} - -message ChannelBufferVersion { - uint64 channel_id = 1; - repeated VectorClockEntry version = 2; - uint64 epoch = 3; -} - -message UpdateChannelBufferCollaborators { - uint64 channel_id = 1; - repeated Collaborator collaborators = 2; -} - -message UpdateChannelBuffer { - uint64 channel_id = 1; - repeated Operation operations = 2; -} - -message ChannelMessage { - uint64 id = 1; - string body = 2; - uint64 timestamp = 3; - uint64 sender_id = 4; - Nonce nonce = 5; - repeated ChatMention mentions = 6; - optional uint64 reply_to_message_id = 7; - optional uint64 edited_at = 8; -} - -message ChatMention { - Range range = 1; - uint64 user_id = 2; -} - -message RejoinChannelBuffers { - repeated ChannelBufferVersion buffers = 1; -} - -message RejoinChannelBuffersResponse { - repeated RejoinedChannelBuffer buffers = 1; -} - -message AckBufferOperation { - uint64 buffer_id = 1; - uint64 epoch = 2; - repeated VectorClockEntry version = 3; -} - -message JoinChannelBufferResponse { - uint64 buffer_id = 1; - uint32 replica_id = 2; - string base_text = 3; - repeated Operation operations = 4; - repeated Collaborator collaborators = 5; - uint64 epoch = 6; -} - -message RejoinedChannelBuffer { - uint64 channel_id = 1; - repeated VectorClockEntry version = 2; - repeated Operation operations = 3; - repeated Collaborator collaborators = 4; -} - -message LeaveChannelBuffer { - uint64 channel_id = 1; -} - -message RespondToChannelInvite { - uint64 channel_id = 1; - bool accept = 2; -} diff --git a/crates/proto/proto/core.proto b/crates/proto/proto/core.proto deleted file mode 100644 index 121ea74912..0000000000 --- a/crates/proto/proto/core.proto +++ /dev/null @@ -1,29 +0,0 @@ -syntax = "proto3"; -package zed.messages; - -message PeerId { - uint32 owner_id = 1; - uint32 id = 2; -} - -message User { - reserved 4; - uint64 id = 1; - string github_login = 2; - string avatar_url = 3; - optional string name = 5; -} - -message Nonce { - uint64 upper_half = 1; - uint64 lower_half = 2; -} - -message Collaborator { - PeerId peer_id = 1; - uint32 replica_id = 2; - uint64 user_id = 3; - bool is_host = 4; - optional string committer_name = 5; - optional string committer_email = 6; -} diff --git a/crates/proto/proto/debugger.proto b/crates/proto/proto/debugger.proto deleted file mode 100644 index dcfb91c77d..0000000000 --- a/crates/proto/proto/debugger.proto +++ /dev/null @@ -1,555 +0,0 @@ -syntax = "proto3"; -package zed.messages; - -import "core.proto"; -import "buffer.proto"; -import "task.proto"; - -enum BreakpointState { - Enabled = 0; - Disabled = 1; -} - -message Breakpoint { - Anchor position = 1; - BreakpointState state = 2; - reserved 3; - optional string message = 4; - optional string condition = 5; - optional string hit_condition = 6; - map session_state = 7; -} - -message BreakpointSessionState { - uint64 id = 1; - bool verified = 2; -} - -message BreakpointsForFile { - uint64 project_id = 1; - string path = 2; - repeated Breakpoint breakpoints = 3; -} - -message ToggleBreakpoint { - uint64 project_id = 1; - string path = 2; - Breakpoint breakpoint = 3; -} - -enum DapThreadStatus { - Running = 0; - Stopped = 1; - Exited = 2; - Ended = 3; -} - -enum VariablesArgumentsFilter { - Indexed = 0; - Named = 1; -} - -message ValueFormat { - optional bool hex = 1; -} - -message VariablesRequest { - uint64 project_id = 1; - uint64 client_id = 2; - uint64 variables_reference = 3; - optional VariablesArgumentsFilter filter = 4; - optional uint64 start = 5; - optional uint64 count = 6; - optional ValueFormat format = 7; -} - -enum SteppingGranularity { - Statement = 0; - Line = 1; - Instruction = 2; -} - -message DapLocationsRequest { - uint64 project_id = 1; - uint64 session_id = 2; - uint64 location_reference = 3; -} - -message DapLocationsResponse { - DapSource source = 1; - uint64 line = 2; - optional uint64 column = 3; - optional uint64 end_line = 4; - optional uint64 end_column = 5; -} - -enum DapEvaluateContext { - Repl = 0; - Watch = 1; - Hover = 2; - Clipboard = 3; - EvaluateVariables = 4; - EvaluateUnknown = 5; -} - -message DapEvaluateRequest { - uint64 project_id = 1; - uint64 client_id = 2; - string expression = 3; - optional uint64 frame_id = 4; - optional DapEvaluateContext context = 5; -} - -message DapEvaluateResponse { - string result = 1; - optional string evaluate_type = 2; - uint64 variable_reference = 3; - optional uint64 named_variables = 4; - optional uint64 indexed_variables = 5; - optional string memory_reference = 6; -} - - -message DapCompletionRequest { - uint64 project_id = 1; - uint64 client_id = 2; - string query = 3; - optional uint64 frame_id = 4; - optional uint64 line = 5; - uint64 column = 6; -} - -enum DapCompletionItemType { - Method = 0; - Function = 1; - Constructor = 2; - Field = 3; - Variable = 4; - Class = 5; - Interface = 6; - Module = 7; - Property = 8; - Unit = 9; - Value = 10; - Enum = 11; - Keyword = 12; - Snippet = 13; - Text = 14; - Color = 15; - CompletionItemFile = 16; - Reference = 17; - Customcolor = 19; -} - -message DapCompletionItem { - string label = 1; - optional string text = 2; - optional string sort_text = 3; - optional string detail = 4; - optional DapCompletionItemType typ = 5; - optional uint64 start = 6; - optional uint64 length = 7; - optional uint64 selection_start = 8; - optional uint64 selection_length = 9; -} - -message DapCompletionResponse { - uint64 client_id = 1; - repeated DapCompletionItem completions = 2; -} - -message DapScopesRequest { - uint64 project_id = 1; - uint64 client_id = 2; - uint64 stack_frame_id = 3; -} - -message DapScopesResponse { - repeated DapScope scopes = 1; -} - -message DapSetVariableValueRequest { - uint64 project_id = 1; - uint64 client_id = 2; - string name = 3; - string value = 4; - uint64 variables_reference = 5; -} - -message DapSetVariableValueResponse { - uint64 client_id = 1; - string value = 2; - optional string variable_type = 3; - optional uint64 variables_reference = 4; - optional uint64 named_variables = 5; - optional uint64 indexed_variables = 6; - optional string memory_reference = 7; -} - -message DapPauseRequest { - uint64 project_id = 1; - uint64 client_id = 2; - int64 thread_id = 3; -} - -message DapDisconnectRequest { - uint64 project_id = 1; - uint64 client_id = 2; - optional bool restart = 3; - optional bool terminate_debuggee = 4; - optional bool suspend_debuggee = 5; -} - -message DapTerminateThreadsRequest { - uint64 project_id = 1; - uint64 client_id = 2; - repeated int64 thread_ids = 3; -} - -message DapThreadsRequest { - uint64 project_id = 1; - uint64 client_id = 2; -} - -message DapThreadsResponse { - repeated DapThread threads = 1; -} - -message DapTerminateRequest { - uint64 project_id = 1; - uint64 client_id = 2; - optional bool restart = 3; -} - -message DapRestartRequest { - uint64 project_id = 1; - uint64 client_id = 2; - bytes raw_args = 3; -} - -message DapRestartStackFrameRequest { - uint64 project_id = 1; - uint64 client_id = 2; - uint64 stack_frame_id = 3; -} - -message ToggleIgnoreBreakpoints { - uint64 project_id = 1; - uint32 session_id = 2; -} - -message IgnoreBreakpointState { - uint64 project_id = 1; - uint64 session_id = 2; - bool ignore = 3; -} - -message DapNextRequest { - uint64 project_id = 1; - uint64 client_id = 2; - int64 thread_id = 3; - optional bool single_thread = 4; - optional SteppingGranularity granularity = 5; -} - -message DapStepInRequest { - uint64 project_id = 1; - uint64 client_id = 2; - int64 thread_id = 3; - optional uint64 target_id = 4; - optional bool single_thread = 5; - optional SteppingGranularity granularity = 6; -} - -message DapStepOutRequest { - uint64 project_id = 1; - uint64 client_id = 2; - int64 thread_id = 3; - optional bool single_thread = 4; - optional SteppingGranularity granularity = 5; -} - -message DapStepBackRequest { - uint64 project_id = 1; - uint64 client_id = 2; - int64 thread_id = 3; - optional bool single_thread = 4; - optional SteppingGranularity granularity = 5; -} - -message DapContinueRequest { - uint64 project_id = 1; - uint64 client_id = 2; - int64 thread_id = 3; - optional bool single_thread = 4; -} - -message DapContinueResponse { - uint64 client_id = 1; - optional bool all_threads_continued = 2; -} - -message DapModulesRequest { - uint64 project_id = 1; - uint64 client_id = 2; -} - -message DapModulesResponse { - uint64 client_id = 1; - repeated DapModule modules = 2; -} - -message DapLoadedSourcesRequest { - uint64 project_id = 1; - uint64 client_id = 2; -} - -message DapLoadedSourcesResponse { - uint64 client_id = 1; - repeated DapSource sources = 2; -} - -message DapStackTraceRequest { - uint64 project_id = 1; - uint64 client_id = 2; - int64 thread_id = 3; - optional uint64 start_frame = 4; - optional uint64 stack_trace_levels = 5; -} - -message DapStackTraceResponse { - repeated DapStackFrame frames = 1; -} - -message DapStackFrame { - uint64 id = 1; - string name = 2; - optional DapSource source = 3; - uint64 line = 4; - uint64 column = 5; - optional uint64 end_line = 6; - optional uint64 end_column = 7; - optional bool can_restart = 8; - optional string instruction_pointer_reference = 9; - optional DapModuleId module_id = 10; - optional DapStackPresentationHint presentation_hint = 11; -} - -message DebuggerLoadedSourceList { - uint64 client_id = 1; - repeated DapSource sources = 2; -} - -message DapVariables { - uint64 client_id = 1; - repeated DapVariable variables = 2; -} - -// Remote Debugging: Dap Types -message DapVariable { - string name = 1; - string value = 2; - optional string type = 3; - // optional DapVariablePresentationHint presentation_hint = 4; - optional string evaluate_name = 5; - uint64 variables_reference = 6; - optional uint64 named_variables = 7; - optional uint64 indexed_variables = 8; - optional string memory_reference = 9; -} - -message DapThread { - int64 id = 1; - string name = 2; -} - -message DapScope { - string name = 1; - optional DapScopePresentationHint presentation_hint = 2; - uint64 variables_reference = 3; - optional uint64 named_variables = 4; - optional uint64 indexed_variables = 5; - bool expensive = 6; - optional DapSource source = 7; - optional uint64 line = 8; - optional uint64 column = 9; - optional uint64 end_line = 10; - optional uint64 end_column = 11; -} - -message DapSource { - optional string name = 1; - optional string path = 2; - optional uint64 source_reference = 3; - optional DapSourcePresentationHint presentation_hint = 4; - optional string origin = 5; - repeated DapSource sources = 6; - optional bytes adapter_data = 7; - repeated DapChecksum checksums = 8; -} - -enum DapOutputCategory { - ConsoleOutput = 0; - Important = 1; - Stdout = 2; - Stderr = 3; - Unknown = 4; -} - -enum DapOutputEventGroup { - Start = 0; - StartCollapsed = 1; - End = 2; -} - -message DapOutputEvent { - string output = 1; - optional DapOutputCategory category = 2; - optional uint64 variables_reference = 3; - optional DapOutputEventGroup group = 4; - optional DapSource source = 5; - optional uint32 line = 6; - optional uint32 column = 7; -} - -enum DapChecksumAlgorithm { - CHECKSUM_ALGORITHM_UNSPECIFIED = 0; - MD5 = 1; - SHA1 = 2; - SHA256 = 3; - TIMESTAMP = 4; -} - -message DapChecksum { - DapChecksumAlgorithm algorithm = 1; - string checksum = 2; -} - -enum DapScopePresentationHint { - Arguments = 0; - Locals = 1; - Registers = 2; - ReturnValue = 3; - ScopeUnknown = 4; -} - -enum DapSourcePresentationHint { - SourceNormal = 0; - Emphasize = 1; - Deemphasize = 2; - SourceUnknown = 3; -} - -enum DapStackPresentationHint { - StackNormal = 0; - Label = 1; - Subtle = 2; - StackUnknown = 3; -} -message DapModule { - DapModuleId id = 1; - string name = 2; - optional string path = 3; - optional bool is_optimized = 4; - optional bool is_user_code = 5; - optional string version = 6; - optional string symbol_status = 7; - optional string symbol_file_path = 8; - optional string date_time_stamp = 9; - optional string address_range = 10; -} - -message DebugTaskDefinition { - string adapter = 1; - string label = 2; - string config = 3; - optional TcpHost tcp_connection = 4; -} - -message TcpHost { - optional uint32 port = 1; - optional string host = 2; - optional uint64 timeout = 3; -} - -message DebugLaunchRequest { - string program = 1; - optional string cwd = 2; - repeated string args = 3; - map env = 4; -} - -message DebugAttachRequest { - uint32 process_id = 1; -} - -message DapModuleId { - oneof id { - uint32 number = 1; - string string = 2; - } -} - -message GetDebugAdapterBinary { - uint64 project_id = 1; - uint64 session_id = 3; - DebugTaskDefinition definition = 2; - uint64 worktree_id = 4; -} - -message DebugAdapterBinary { - optional string command = 1; - repeated string arguments = 2; - map envs = 3; - optional string cwd = 4; - optional TcpHost connection = 5; - string configuration = 7; - LaunchType launch_type = 8; - enum LaunchType { - Attach = 0; - Launch = 1; - } -} - -message RunDebugLocators { - uint64 project_id = 1; - SpawnInTerminal build_command = 2; - string locator = 3; -} - -message DebugRequest { - oneof request { - DebugLaunchRequest debug_launch_request = 1; - DebugAttachRequest debug_attach_request = 2; - } -} - -message DebugScenario { - string label = 1; - string adapter = 2; - reserved 3; - DebugRequest request = 4; - optional TcpHost connection = 5; - optional bool stop_on_entry = 6; - optional string configuration = 7; -} - -message LogToDebugConsole { - uint64 project_id = 1; - uint64 session_id = 2; - string message = 3; -} - -message GetProcesses { - uint64 project_id = 1; -} - -message GetProcessesResponse { - repeated ProcessInfo processes = 1; -} - -message ProcessInfo { - uint32 pid = 1; - string name = 2; - repeated string command = 3; -} diff --git a/crates/proto/proto/git.proto b/crates/proto/proto/git.proto deleted file mode 100644 index d1e56f4f8c..0000000000 --- a/crates/proto/proto/git.proto +++ /dev/null @@ -1,589 +0,0 @@ -syntax = "proto3"; -package zed.messages; - -import "worktree.proto"; -import "buffer.proto"; - -message GitBranchesResponse { - repeated Branch branches = 1; -} - -message UpdateDiffBases { - uint64 project_id = 1; - uint64 buffer_id = 2; - - enum Mode { - // No collaborator is using the unstaged diff. - HEAD_ONLY = 0; - // No collaborator is using the diff from HEAD. - INDEX_ONLY = 1; - // Both the unstaged and uncommitted diffs are demanded, - // and the contents of the index and HEAD are the same for this path. - INDEX_MATCHES_HEAD = 2; - // Both the unstaged and uncommitted diffs are demanded, - // and the contents of the index and HEAD differ for this path, - // where None means the path doesn't exist in that state of the repo. - INDEX_AND_HEAD = 3; - } - - optional string staged_text = 3; - optional string committed_text = 4; - Mode mode = 5; -} - -message OpenUnstagedDiff { - uint64 project_id = 1; - uint64 buffer_id = 2; -} - -message OpenUnstagedDiffResponse { - optional string staged_text = 1; -} - -message OpenUncommittedDiff { - uint64 project_id = 1; - uint64 buffer_id = 2; -} - -message OpenUncommittedDiffResponse { - enum Mode { - INDEX_MATCHES_HEAD = 0; - INDEX_AND_HEAD = 1; - } - optional string staged_text = 1; - optional string committed_text = 2; - Mode mode = 3; -} - -message SetIndexText { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - string path = 4; - optional string text = 5; -} - -message GetPermalinkToLine { - uint64 project_id = 1; - uint64 buffer_id = 2; - Range selection = 3; -} - -message GetPermalinkToLineResponse { - string permalink = 1; -} - -message Branch { - bool is_head = 1; - string ref_name = 2; - optional uint64 unix_timestamp = 3; - optional GitUpstream upstream = 4; - optional CommitSummary most_recent_commit = 5; -} - -message GitUpstream { - string ref_name = 1; - optional UpstreamTracking tracking = 2; -} - -message UpstreamTracking { - uint64 ahead = 1; - uint64 behind = 2; -} - -message CommitSummary { - string sha = 1; - string subject = 2; - int64 commit_timestamp = 3; - string author_name = 4; -} - -message GitBranches { - uint64 project_id = 1; - ProjectPath repository = 2; -} - - -message UpdateGitBranch { - uint64 project_id = 1; - string branch_name = 2; - ProjectPath repository = 3; -} - -message UpdateRepository { - uint64 project_id = 1; - uint64 id = 2; - string abs_path = 3; - repeated uint64 entry_ids = 4; - optional Branch branch_summary = 5; - repeated StatusEntry updated_statuses = 6; - repeated string removed_statuses = 7; - repeated string current_merge_conflicts = 8; - uint64 scan_id = 9; - bool is_last_update = 10; - optional GitCommitDetails head_commit_details = 11; - optional string merge_message = 12; - repeated StashEntry stash_entries = 13; - optional string remote_upstream_url = 14; - optional string remote_origin_url = 15; -} - -message RemoveRepository { - uint64 project_id = 1; - uint64 id = 2; -} - -enum GitStatus { - Added = 0; - Modified = 1; - Conflict = 2; - Deleted = 3; - Updated = 4; - TypeChanged = 5; - Renamed = 6; - Copied = 7; - Unmodified = 8; -} - -message GitFileStatus { - oneof variant { - Untracked untracked = 1; - Ignored ignored = 2; - Unmerged unmerged = 3; - Tracked tracked = 4; - } - - message Untracked {} - message Ignored {} - message Unmerged { - GitStatus first_head = 1; - GitStatus second_head = 2; - } - message Tracked { - GitStatus index_status = 1; - GitStatus worktree_status = 2; - } -} - -message GitGetBranches { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; -} - -message GitCreateBranch { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - string branch_name = 4; -} - -message GitChangeBranch { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - string branch_name = 4; -} - -message GitRenameBranch { - uint64 project_id = 1; - uint64 repository_id = 2; - string branch = 3; - string new_name = 4; -} - -message GitCreateRemote { - uint64 project_id = 1; - uint64 repository_id = 2; - string remote_name = 3; - string remote_url = 4; -} - -message GitRemoveRemote { - uint64 project_id = 1; - uint64 repository_id = 2; - string remote_name = 3; -} - -message GitDeleteBranch { - uint64 project_id = 1; - uint64 repository_id = 2; - string branch_name = 3; -} - -message GitDiff { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - DiffType diff_type = 4; - - enum DiffType { - HEAD_TO_WORKTREE = 0; - HEAD_TO_INDEX = 1; - } -} - -message GitDiffResponse { - string diff = 1; -} - -message GitInit { - uint64 project_id = 1; - string abs_path = 2; - string fallback_branch_name = 3; -} - -message GitClone { - uint64 project_id = 1; - string abs_path = 2; - string remote_repo = 3; -} - -message GitCloneResponse { - bool success = 1; -} - -message CheckForPushedCommits { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; -} - -message CheckForPushedCommitsResponse { - repeated string pushed_to = 1; -} - -message GitShow { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - string commit = 4; -} - -message GitCommitDetails { - string sha = 1; - string message = 2; - int64 commit_timestamp = 3; - string author_email = 4; - string author_name = 5; -} - -message LoadCommitDiff { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - string commit = 4; -} - -message LoadCommitDiffResponse { - repeated CommitFile files = 1; -} - -message CommitFile { - string path = 1; - optional string old_text = 2; - optional string new_text = 3; -} - -message GitReset { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - string commit = 4; - ResetMode mode = 5; - enum ResetMode { - SOFT = 0; - MIXED = 1; - } -} - -message GitCheckoutFiles { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - string commit = 4; - repeated string paths = 5; -} - -message GitFileHistory { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - string path = 4; - uint64 skip = 5; - optional uint64 limit = 6; -} - -message GitFileHistoryResponse { - repeated FileHistoryEntry entries = 1; - string path = 2; -} - -message FileHistoryEntry { - string sha = 1; - string subject = 2; - string message = 3; - int64 commit_timestamp = 4; - string author_name = 5; - string author_email = 6; -} - -// Move to `git.proto` once collab's min version is >=0.171.0. -message StatusEntry { - string repo_path = 1; - // Can be removed once collab's min version is >=0.171.0. - GitStatus simple_status = 2; - GitFileStatus status = 3; -} - -message StashEntry { - bytes oid = 1; - string message = 2; - optional string branch = 3; - uint64 index = 4; - int64 timestamp = 5; -} - -message Stage { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - repeated string paths = 4; -} - -message Unstage { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - repeated string paths = 4; -} - -message Stash { - uint64 project_id = 1; - uint64 repository_id = 2; - repeated string paths = 3; -} - -message StashPop { - uint64 project_id = 1; - uint64 repository_id = 2; - optional uint64 stash_index = 3; -} - -message StashApply { - uint64 project_id = 1; - uint64 repository_id = 2; - optional uint64 stash_index = 3; -} - -message StashDrop { - uint64 project_id = 1; - uint64 repository_id = 2; - optional uint64 stash_index = 3; -} - -message Commit { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - optional string name = 4; - optional string email = 5; - string message = 6; - optional CommitOptions options = 7; - reserved 8; - uint64 askpass_id = 9; - - message CommitOptions { - bool amend = 1; - bool signoff = 2; - } -} - -message OpenCommitMessageBuffer { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; -} - -message Push { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - string remote_name = 4; - string branch_name = 5; - optional PushOptions options = 6; - uint64 askpass_id = 7; - - enum PushOptions { - SET_UPSTREAM = 0; - FORCE = 1; - } -} - -message Fetch { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - uint64 askpass_id = 4; - optional string remote = 5; -} - -message GetRemotes { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - optional string branch_name = 4; - bool is_push = 5; -} - -message GetRemotesResponse { - repeated Remote remotes = 1; - - message Remote { - string name = 1; - } -} - -message Pull { - uint64 project_id = 1; - reserved 2; - uint64 repository_id = 3; - string remote_name = 4; - optional string branch_name = 5; - uint64 askpass_id = 6; - bool rebase = 7; -} - -message RemoteMessageResponse { - string stdout = 1; - string stderr = 2; -} - -message BlameBuffer { - uint64 project_id = 1; - uint64 buffer_id = 2; - repeated VectorClockEntry version = 3; -} - -message BlameEntry { - bytes sha = 1; - - uint32 start_line = 2; - uint32 end_line = 3; - uint32 original_line_number = 4; - - optional string author = 5; - optional string author_mail = 6; - optional int64 author_time = 7; - optional string author_tz = 8; - - optional string committer = 9; - optional string committer_mail = 10; - optional int64 committer_time = 11; - optional string committer_tz = 12; - - optional string summary = 13; - optional string previous = 14; - - string filename = 15; -} - -message CommitMessage { - bytes oid = 1; - string message = 2; -} - -message CommitPermalink { - bytes oid = 1; - string permalink = 2; -} - -message BlameBufferResponse { - message BlameResponse { - repeated BlameEntry entries = 1; - repeated CommitMessage messages = 2; - reserved 3; - reserved 4; - } - - optional BlameResponse blame_response = 5; - - reserved 1 to 4; -} - -message GetDefaultBranch { - uint64 project_id = 1; - uint64 repository_id = 2; -} - -message GetDefaultBranchResponse { - optional string branch = 1; -} - -message GetTreeDiff { - uint64 project_id = 1; - uint64 repository_id = 2; - bool is_merge = 3; - string base = 4; - string head = 5; -} - -message GetTreeDiffResponse { - repeated TreeDiffStatus entries = 1; -} - -message TreeDiffStatus { - enum Status { - ADDED = 0; - MODIFIED = 1; - DELETED = 2; - } - - Status status = 1; - string path = 2; - optional string oid = 3; -} - -message GetBlobContent { - uint64 project_id = 1; - uint64 repository_id = 2; - string oid =3; -} - -message GetBlobContentResponse { - string content = 1; -} - -message GitGetWorktrees { - uint64 project_id = 1; - uint64 repository_id = 2; -} - -message GitWorktreesResponse { - repeated Worktree worktrees = 1; -} - -message Worktree { - string path = 1; - string ref_name = 2; - string sha = 3; -} - -message GitCreateWorktree { - uint64 project_id = 1; - uint64 repository_id = 2; - string name = 3; - string directory = 4; - optional string commit = 5; -} - -message RunGitHook { - enum GitHook { - PRE_COMMIT = 0; - reserved 1; - } - - uint64 project_id = 1; - uint64 repository_id = 2; - GitHook hook = 3; -} diff --git a/crates/proto/proto/image.proto b/crates/proto/proto/image.proto deleted file mode 100644 index e3232e6847..0000000000 --- a/crates/proto/proto/image.proto +++ /dev/null @@ -1,36 +0,0 @@ -syntax = "proto3"; -package zed.messages; - -import "core.proto"; -import "worktree.proto"; - -message OpenImageByPath { - uint64 project_id = 1; - uint64 worktree_id = 2; - string path = 3; -} - -message OpenImageResponse { - uint64 image_id = 1; -} - -message CreateImageForPeer { - uint64 project_id = 1; - PeerId peer_id = 2; - oneof variant { - ImageState state = 3; - ImageChunk chunk = 4; - } -} - -message ImageState { - uint64 id = 1; - optional File file = 2; - uint64 content_size = 3; - string format = 4; // e.g., "png", "jpeg", "webp", etc. -} - -message ImageChunk { - uint64 image_id = 1; - bytes data = 2; -} diff --git a/crates/proto/proto/lsp.proto b/crates/proto/proto/lsp.proto deleted file mode 100644 index 7717cacdef..0000000000 --- a/crates/proto/proto/lsp.proto +++ /dev/null @@ -1,971 +0,0 @@ -syntax = "proto3"; -package zed.messages; - -import "core.proto"; -import "worktree.proto"; -import "buffer.proto"; - -message GetDefinition { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor position = 3; - repeated VectorClockEntry version = 4; -} - -message GetDefinitionResponse { - repeated LocationLink links = 1; -} - -message GetDeclaration { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor position = 3; - repeated VectorClockEntry version = 4; -} - -message GetDeclarationResponse { - repeated LocationLink links = 1; -} - -message GetTypeDefinition { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor position = 3; - repeated VectorClockEntry version = 4; - } - -message GetTypeDefinitionResponse { - repeated LocationLink links = 1; -} -message GetImplementation { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor position = 3; - repeated VectorClockEntry version = 4; - } - -message GetImplementationResponse { - repeated LocationLink links = 1; -} - -message GetReferences { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor position = 3; - repeated VectorClockEntry version = 4; - } - -message GetReferencesResponse { - repeated Location locations = 1; -} - -message GetDocumentHighlights { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor position = 3; - repeated VectorClockEntry version = 4; - } - -message GetDocumentHighlightsResponse { - repeated DocumentHighlight highlights = 1; -} - -message LocationLink { - optional Location origin = 1; - Location target = 2; -} - -message DocumentHighlight { - Kind kind = 1; - Anchor start = 2; - Anchor end = 3; - - enum Kind { - Text = 0; - Read = 1; - Write = 2; - } -} - -message GetProjectSymbols { - uint64 project_id = 1; - string query = 2; -} - -message GetProjectSymbolsResponse { - repeated Symbol symbols = 4; -} - -message Symbol { - uint64 source_worktree_id = 1; - uint64 worktree_id = 2; - string language_server_name = 3; - string name = 4; - int32 kind = 5; - string path = 6; - // Cannot use generate anchors for unopened files, - // so we are forced to use point coords instead - PointUtf16 start = 7; - PointUtf16 end = 8; - bytes signature = 9; - uint64 language_server_id = 10; -} - -message GetDocumentSymbols { - uint64 project_id = 1; - uint64 buffer_id = 2; - repeated VectorClockEntry version = 3; -} - -message GetDocumentSymbolsResponse { - repeated DocumentSymbol symbols = 1; -} - -message DocumentSymbol { - string name = 1; - int32 kind = 2; - // Cannot use generate anchors for unopened files, - // so we are forced to use point coords instead - PointUtf16 start = 3; - PointUtf16 end = 4; - PointUtf16 selection_start = 5; - PointUtf16 selection_end = 6; - repeated DocumentSymbol children = 7; -} - -message InlayHints { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor start = 3; - Anchor end = 4; - repeated VectorClockEntry version = 5; -} - -message InlayHintsResponse { - repeated InlayHint hints = 1; - repeated VectorClockEntry version = 2; -} - -message PointUtf16 { - uint32 row = 1; - uint32 column = 2; -} - -message LspExtExpandMacro { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor position = 3; -} - -message LspExtExpandMacroResponse { - string name = 1; - string expansion = 2; -} - -message LspExtOpenDocs { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor position = 3; -} - -message LspExtOpenDocsResponse { - optional string web = 1; - optional string local = 2; -} - -message LspExtSwitchSourceHeader { - uint64 project_id = 1; - uint64 buffer_id = 2; -} - -message LspExtSwitchSourceHeaderResponse { - string target_file = 1; -} - -message LspExtGoToParentModule { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor position = 3; -} - -message LspExtGoToParentModuleResponse { - repeated LocationLink links = 1; -} - -message GetCompletionsResponse { - repeated Completion completions = 1; - repeated VectorClockEntry version = 2; - // `!is_complete`, inverted for a default of `is_complete = true` - bool can_reuse = 3; -} - -message ApplyCompletionAdditionalEdits { - uint64 project_id = 1; - uint64 buffer_id = 2; - Completion completion = 3; -} - -message ApplyCompletionAdditionalEditsResponse { - Transaction transaction = 1; -} - -message Completion { - Anchor old_replace_start = 1; - Anchor old_replace_end = 2; - string new_text = 3; - uint64 server_id = 4; - bytes lsp_completion = 5; - bool resolved = 6; - Source source = 7; - optional bytes lsp_defaults = 8; - optional Anchor buffer_word_start = 9; - optional Anchor buffer_word_end = 10; - Anchor old_insert_start = 11; - Anchor old_insert_end = 12; - optional string sort_text = 13; - - enum Source { - Lsp = 0; - Custom = 1; - BufferWord = 2; - Dap = 3; - } -} - -message GetCodeActions { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor start = 3; - Anchor end = 4; - repeated VectorClockEntry version = 5; -} - -message GetCodeActionsResponse { - repeated CodeAction actions = 1; - repeated VectorClockEntry version = 2; -} - -message GetSignatureHelp { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor position = 3; - repeated VectorClockEntry version = 4; -} - -message GetSignatureHelpResponse { - optional SignatureHelp signature_help = 1; -} - -message SignatureHelp { - repeated SignatureInformation signatures = 1; - optional uint32 active_signature = 2; - optional uint32 active_parameter = 3; -} - -message SignatureInformation { - string label = 1; - optional Documentation documentation = 2; - repeated ParameterInformation parameters = 3; - optional uint32 active_parameter = 4; -} - -message Documentation { - oneof content { - string value = 1; - MarkupContent markup_content = 2; - } -} - -enum MarkupKind { - PlainText = 0; - Markdown = 1; -} - -message ParameterInformation { - oneof label { - string simple = 1; - LabelOffsets label_offsets = 2; - } - optional Documentation documentation = 3; -} - -message LabelOffsets { - uint32 start = 1; - uint32 end = 2; -} - -message GetHover { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor position = 3; - repeated VectorClockEntry version = 5; -} - -message GetHoverResponse { - optional Anchor start = 1; - optional Anchor end = 2; - repeated HoverBlock contents = 3; -} - -message HoverBlock { - string text = 1; - optional string language = 2; - bool is_markdown = 3; -} - -message ApplyCodeAction { - uint64 project_id = 1; - uint64 buffer_id = 2; - CodeAction action = 3; -} - -message ApplyCodeActionResponse { - ProjectTransaction transaction = 1; -} - -message PrepareRename { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor position = 3; - repeated VectorClockEntry version = 4; -} - -message PrepareRenameResponse { - bool can_rename = 1; - Anchor start = 2; - Anchor end = 3; - repeated VectorClockEntry version = 4; - bool only_unprepared_rename_supported = 5; -} - -message PerformRename { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor position = 3; - string new_name = 4; - repeated VectorClockEntry version = 5; -} - -message OnTypeFormatting { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor position = 3; - string trigger = 4; - repeated VectorClockEntry version = 5; -} - -message OnTypeFormattingResponse { - Transaction transaction = 1; -} - - -message LinkedEditingRange { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor position = 3; - repeated VectorClockEntry version = 4; -} - -message LinkedEditingRangeResponse { - repeated AnchorRange items = 1; - repeated VectorClockEntry version = 4; -} - -message InlayHint { - Anchor position = 1; - InlayHintLabel label = 2; - optional string kind = 3; - bool padding_left = 4; - bool padding_right = 5; - InlayHintTooltip tooltip = 6; - ResolveState resolve_state = 7; -} - -message InlayHintLabel { - oneof label { - string value = 1; - InlayHintLabelParts label_parts = 2; - } -} - -message InlayHintLabelParts { - repeated InlayHintLabelPart parts = 1; -} - -message InlayHintLabelPart { - string value = 1; - InlayHintLabelPartTooltip tooltip = 2; - optional string location_url = 3; - PointUtf16 location_range_start = 4; - PointUtf16 location_range_end = 5; - optional uint64 language_server_id = 6; -} - -message InlayHintTooltip { - oneof content { - string value = 1; - MarkupContent markup_content = 2; - } -} - -message InlayHintLabelPartTooltip { - oneof content { - string value = 1; - MarkupContent markup_content = 2; - } -} - -message ResolveState { - State state = 1; - LspResolveState lsp_resolve_state = 2; - - enum State { - Resolved = 0; - CanResolve = 1; - Resolving = 2; - } - - message LspResolveState { - optional string value = 1; - uint64 server_id = 2; - } -} - -// This type is used to resolve more than just -// the documentation, but for backwards-compatibility -// reasons we can't rename the type. -message ResolveCompletionDocumentation { - uint64 project_id = 1; - uint64 language_server_id = 2; - bytes lsp_completion = 3; - uint64 buffer_id = 4; -} - -message ResolveCompletionDocumentationResponse { - string documentation = 1; - bool documentation_is_markdown = 2; - Anchor old_replace_start = 3; - Anchor old_replace_end = 4; - string new_text = 5; - bytes lsp_completion = 6; - Anchor old_insert_start = 7; - Anchor old_insert_end = 8; -} - -message ResolveInlayHint { - uint64 project_id = 1; - uint64 buffer_id = 2; - uint64 language_server_id = 3; - InlayHint hint = 4; -} - -message ResolveInlayHintResponse { - InlayHint hint = 1; -} - -message RefreshInlayHints { - uint64 project_id = 1; - uint64 server_id = 2; - optional uint64 request_id = 3; -} - -message CodeLens { - bytes lsp_lens = 1; -} - -message GetCodeLens { - uint64 project_id = 1; - uint64 buffer_id = 2; - repeated VectorClockEntry version = 3; -} - -message GetCodeLensResponse { - repeated CodeAction lens_actions = 1; - repeated VectorClockEntry version = 2; -} - -message RefreshCodeLens { - uint64 project_id = 1; -} - -message MarkupContent { - bool is_markdown = 1; - string value = 2; -} - -message PerformRenameResponse { - ProjectTransaction transaction = 2; -} - -message CodeAction { - uint64 server_id = 1; - Anchor start = 2; - Anchor end = 3; - bytes lsp_action = 4; - Kind kind = 5; - bool resolved = 6; - enum Kind { - Action = 0; - Command = 1; - CodeLens = 2; - } -} - -message LanguageServer { - uint64 id = 1; - string name = 2; - optional uint64 worktree_id = 3; -} - -message StartLanguageServer { - uint64 project_id = 1; - LanguageServer server = 2; - string capabilities = 3; -} - -message UpdateDiagnosticSummary { - uint64 project_id = 1; - uint64 worktree_id = 2; - DiagnosticSummary summary = 3; - repeated DiagnosticSummary more_summaries = 4; -} - -message DiagnosticSummary { - string path = 1; - uint64 language_server_id = 2; - uint32 error_count = 3; - uint32 warning_count = 4; -} - -message UpdateLanguageServer { - uint64 project_id = 1; - uint64 language_server_id = 2; - optional string server_name = 8; - oneof variant { - LspWorkStart work_start = 3; - LspWorkProgress work_progress = 4; - LspWorkEnd work_end = 5; - LspDiskBasedDiagnosticsUpdating disk_based_diagnostics_updating = 6; - LspDiskBasedDiagnosticsUpdated disk_based_diagnostics_updated = 7; - StatusUpdate status_update = 9; - RegisteredForBuffer registered_for_buffer = 10; - ServerMetadataUpdated metadata_updated = 11; - } -} - -message ProgressToken { - oneof value { - int32 number = 1; - string string = 2; - } -} - -message LspWorkStart { - reserved 1; - optional string title = 4; - optional string message = 2; - optional uint32 percentage = 3; - optional bool is_cancellable = 5; - ProgressToken token = 6; -} - -message LspWorkProgress { - reserved 1; - optional string message = 2; - optional uint32 percentage = 3; - optional bool is_cancellable = 4; - ProgressToken token = 5; -} - -message LspWorkEnd { - reserved 1; - ProgressToken token = 2; -} - -message LspDiskBasedDiagnosticsUpdating {} - -message LspDiskBasedDiagnosticsUpdated {} - -message StatusUpdate { - optional string message = 1; - oneof status { - ServerBinaryStatus binary = 2; - ServerHealth health = 3; - } -} - -enum ServerHealth { - OK = 0; - WARNING = 1; - ERROR = 2; -} - -enum ServerBinaryStatus { - NONE = 0; - CHECKING_FOR_UPDATE = 1; - DOWNLOADING = 2; - STARTING = 3; - STOPPING = 4; - STOPPED = 5; - FAILED = 6; -} - -message RegisteredForBuffer { - string buffer_abs_path = 1; - uint64 buffer_id = 2; -} - -message LanguageServerBinaryInfo { - string path = 1; - repeated string arguments = 2; -} - -message ServerMetadataUpdated { - optional string capabilities = 1; - optional LanguageServerBinaryInfo binary = 2; - optional string configuration = 3; - repeated string workspace_folders = 4; -} - -message LanguageServerLog { - uint64 project_id = 1; - uint64 language_server_id = 2; - string message = 3; - oneof log_type { - LogMessage log = 4; - TraceMessage trace = 5; - RpcMessage rpc = 6; - } -} - -message LogMessage { - LogLevel level = 1; - - enum LogLevel { - LOG = 0; - INFO = 1; - WARNING = 2; - ERROR = 3; - } -} - -message TraceMessage { - optional string verbose_info = 1; -} - -message RpcMessage { - Kind kind = 1; - - enum Kind { - RECEIVED = 0; - SENT = 1; - } -} - -message LspLogTrace { - optional string message = 1; -} - -message ApplyCodeActionKind { - uint64 project_id = 1; - string kind = 2; - repeated uint64 buffer_ids = 3; -} - -message ApplyCodeActionKindResponse { - ProjectTransaction transaction = 1; -} - -message RegisterBufferWithLanguageServers { - uint64 project_id = 1; - uint64 buffer_id = 2; - repeated LanguageServerSelector only_servers = 3; -} - -enum FormatTrigger { - Save = 0; - Manual = 1; -} - -message OpenBufferForSymbol { - uint64 project_id = 1; - Symbol symbol = 2; -} - -message OpenBufferForSymbolResponse { - uint64 buffer_id = 1; -} - -message FormatBuffers { - uint64 project_id = 1; - FormatTrigger trigger = 2; - repeated uint64 buffer_ids = 3; -} - -message FormatBuffersResponse { - ProjectTransaction transaction = 1; -} - -message GetCompletions { - uint64 project_id = 1; - uint64 buffer_id = 2; - Anchor position = 3; - repeated VectorClockEntry version = 4; - optional uint64 server_id = 5; -} - -message CancelLanguageServerWork { - uint64 project_id = 1; - - oneof work { - Buffers buffers = 2; - LanguageServerWork language_server_work = 3; - } - - message Buffers { - repeated uint64 buffer_ids = 2; - } - - message LanguageServerWork { - uint64 language_server_id = 1; - reserved 2; - optional ProgressToken token = 3; - } -} - -message LanguageServerPromptRequest { - uint64 project_id = 1; - - oneof level { - Info info = 2; - Warning warning = 3; - Critical critical = 4; - } - - message Info {} - message Warning {} - message Critical {} - - string message = 5; - repeated string actions = 6; - string lsp_name = 7; -} - -message LanguageServerPromptResponse { - optional uint64 action_response = 1; -} - -message GetDocumentColor { - uint64 project_id = 1; - uint64 buffer_id = 2; - repeated VectorClockEntry version = 3; - -} - -message GetDocumentColorResponse { - repeated ColorInformation colors = 1; - repeated VectorClockEntry version = 2; - -} - -message ColorInformation { - PointUtf16 lsp_range_start = 1; - PointUtf16 lsp_range_end = 2; - float red = 3; - float green = 4; - float blue = 5; - float alpha = 6; -} - -message GetColorPresentation { - uint64 project_id = 1; - uint64 buffer_id = 2; - ColorInformation color = 3; - uint64 server_id = 4; -} - -message GetColorPresentationResponse { - repeated ColorPresentation presentations = 1; -} - -message ColorPresentation { - string label = 1; - optional TextEdit text_edit = 2; - repeated TextEdit additional_text_edits = 3; -} - -message TextEdit { - string new_text = 1; - PointUtf16 lsp_range_start = 2; - PointUtf16 lsp_range_end = 3; -} - -message LspQuery { - uint64 project_id = 1; - uint64 lsp_request_id = 2; - optional uint64 server_id = 15; - oneof request { - GetReferences get_references = 3; - GetDocumentColor get_document_color = 4; - GetHover get_hover = 5; - GetCodeActions get_code_actions = 6; - GetSignatureHelp get_signature_help = 7; - GetCodeLens get_code_lens = 8; - GetDocumentDiagnostics get_document_diagnostics = 9; - GetDefinition get_definition = 10; - GetDeclaration get_declaration = 11; - GetTypeDefinition get_type_definition = 12; - GetImplementation get_implementation = 13; - InlayHints inlay_hints = 14; - } -} - -message LspQueryResponse { - uint64 project_id = 1; - uint64 lsp_request_id = 2; - repeated LspResponse responses = 3; -} - -message LspResponse { - oneof response { - GetHoverResponse get_hover_response = 1; - GetCodeActionsResponse get_code_actions_response = 2; - GetSignatureHelpResponse get_signature_help_response = 3; - GetCodeLensResponse get_code_lens_response = 4; - GetDocumentDiagnosticsResponse get_document_diagnostics_response = 5; - GetDocumentColorResponse get_document_color_response = 6; - GetDefinitionResponse get_definition_response = 8; - GetDeclarationResponse get_declaration_response = 9; - GetTypeDefinitionResponse get_type_definition_response = 10; - GetImplementationResponse get_implementation_response = 11; - GetReferencesResponse get_references_response = 12; - InlayHintsResponse inlay_hints_response = 13; - } - uint64 server_id = 7; -} - -message AllLanguageServers {} - -message LanguageServerSelector { - oneof selector { - uint64 server_id = 1; - string name = 2; - } -} - -message RestartLanguageServers { - uint64 project_id = 1; - repeated uint64 buffer_ids = 2; - repeated LanguageServerSelector only_servers = 3; - bool all = 4; -} - -message StopLanguageServers { - uint64 project_id = 1; - repeated uint64 buffer_ids = 2; - repeated LanguageServerSelector also_servers = 3; - bool all = 4; -} - -message LspExtRunnables { - uint64 project_id = 1; - uint64 buffer_id = 2; - optional Anchor position = 3; -} - -message LspExtRunnablesResponse { - repeated LspRunnable runnables = 1; -} - -message LspRunnable { - bytes task_template = 1; - optional LocationLink location = 2; -} - -message LspExtCancelFlycheck { - uint64 project_id = 1; - uint64 language_server_id = 2; -} - -message LspExtRunFlycheck { - uint64 project_id = 1; - optional 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; -} - -message LspDiagnosticRelatedInformation { - optional string location_url = 1; - PointUtf16 location_range_start = 2; - PointUtf16 location_range_end = 3; - string message = 4; -} - -enum LspDiagnosticTag { - None = 0; - Unnecessary = 1; - Deprecated = 2; -} - -message LspDiagnostic { - PointUtf16 start = 1; - PointUtf16 end = 2; - Severity severity = 3; - optional string code = 4; - optional string code_description = 5; - optional string source = 6; - string message = 7; - repeated LspDiagnosticRelatedInformation related_information = 8; - repeated LspDiagnosticTag tags = 9; - optional string data = 10; - - enum Severity { - None = 0; - Error = 1; - Warning = 2; - Information = 3; - Hint = 4; - } -} - -message GetDocumentDiagnostics { - uint64 project_id = 1; - uint64 buffer_id = 2; - repeated VectorClockEntry version = 3; -} - -message GetDocumentDiagnosticsResponse { - repeated PulledDiagnostics pulled_diagnostics = 1; -} - -message PulledDiagnostics { - uint64 server_id = 1; - string uri = 2; - optional string result_id = 3; - bool changed = 4; - repeated LspDiagnostic diagnostics = 5; - optional string registration_id = 6; -} - -message PullWorkspaceDiagnostics { - uint64 project_id = 1; - uint64 server_id = 2; -} - -message ToggleLspLogs { - uint64 project_id = 1; - LogType log_type = 2; - uint64 server_id = 3; - bool enabled = 4; - - enum LogType { - LOG = 0; - TRACE = 1; - RPC = 2; - } -} diff --git a/crates/proto/proto/notification.proto b/crates/proto/proto/notification.proto deleted file mode 100644 index ebd3d7fe44..0000000000 --- a/crates/proto/proto/notification.proto +++ /dev/null @@ -1,37 +0,0 @@ -syntax = "proto3"; -package zed.messages; - -message GetNotifications { - optional uint64 before_id = 1; -} - -message AddNotification { - Notification notification = 1; -} - -message GetNotificationsResponse { - repeated Notification notifications = 1; - bool done = 2; -} - -message DeleteNotification { - uint64 notification_id = 1; -} - -message UpdateNotification { - Notification notification = 1; -} - -message MarkNotificationRead { - uint64 notification_id = 1; -} - -message Notification { - uint64 id = 1; - uint64 timestamp = 2; - string kind = 3; - optional uint64 entity_id = 4; - string content = 5; - bool is_read = 6; - optional bool response = 7; -} diff --git a/crates/proto/proto/task.proto b/crates/proto/proto/task.proto deleted file mode 100644 index 1844087d62..0000000000 --- a/crates/proto/proto/task.proto +++ /dev/null @@ -1,60 +0,0 @@ -syntax = "proto3"; -package zed.messages; - -import "buffer.proto"; - -message TaskContextForLocation { - uint64 project_id = 1; - Location location = 2; - map task_variables = 3; -} - -message TaskContext { - optional string cwd = 1; - map task_variables = 2; - map project_env = 3; -} - -message Shell { - message WithArguments { - string program = 1; - repeated string args = 2; - } - - oneof shell_type { - System system = 1; - string program = 2; - WithArguments with_arguments = 3; - } -} - -message System {} - -enum RevealStrategy { - RevealAlways = 0; - RevealNever = 1; -} - -enum HideStrategy { - HideAlways = 0; - HideNever = 1; - HideOnSuccess = 2; -} - -message SpawnInTerminal { - string label = 1; - optional string command = 2; - repeated string args = 3; - map env = 4; - optional string cwd = 5; -} - -message GetDirectoryEnvironment { - uint64 project_id = 1; - Shell shell = 2; - string directory = 3; -} - -message DirectoryEnvironment { - map environment = 1; -} diff --git a/crates/proto/proto/toolchain.proto b/crates/proto/proto/toolchain.proto deleted file mode 100644 index b190322ca0..0000000000 --- a/crates/proto/proto/toolchain.proto +++ /dev/null @@ -1,59 +0,0 @@ -syntax = "proto3"; -package zed.messages; - -message ListToolchains { - uint64 project_id = 1; - uint64 worktree_id = 2; - string language_name = 3; - optional string path = 4; -} - -message Toolchain { - string name = 1; - string path = 2; - string raw_json = 3; -} - -message ToolchainGroup { - uint64 start_index = 1; - string name = 2; -} - -message ListToolchainsResponse { - repeated Toolchain toolchains = 1; - bool has_values = 2; - repeated ToolchainGroup groups = 3; - optional string relative_worktree_path = 4; -} - -message ActivateToolchain { - uint64 project_id = 1; - uint64 worktree_id = 2; - Toolchain toolchain = 3; - string language_name = 4; - optional string path = 5; -} - -message ActiveToolchain { - uint64 project_id = 1; - uint64 worktree_id = 2; - string language_name = 3; - optional string path = 4; -} - -message ActiveToolchainResponse { - optional Toolchain toolchain = 1; -} - -message ResolveToolchain { - uint64 project_id = 1; - string abs_path = 2; - string language_name = 3; -} - -message ResolveToolchainResponse { - oneof response { - Toolchain toolchain = 1; - string error = 2; - } -} diff --git a/crates/proto/proto/worktree.proto b/crates/proto/proto/worktree.proto deleted file mode 100644 index 9ab9e95438..0000000000 --- a/crates/proto/proto/worktree.proto +++ /dev/null @@ -1,160 +0,0 @@ -syntax = "proto3"; -package zed.messages; - -message Timestamp { - uint64 seconds = 1; - uint32 nanos = 2; -} - -message File { - uint64 worktree_id = 1; - optional uint64 entry_id = 2; - string path = 3; - Timestamp mtime = 4; - bool is_deleted = 5; -} - -message Entry { - uint64 id = 1; - bool is_dir = 2; - string path = 3; - uint64 inode = 4; - Timestamp mtime = 5; - bool is_ignored = 7; - bool is_external = 8; - reserved 6; - reserved 9; - bool is_fifo = 10; - optional uint64 size = 11; - optional string canonical_path = 12; - bool is_hidden = 13; -} - -message AddWorktree { - string path = 1; - uint64 project_id = 2; - bool visible = 3; -} - -message AddWorktreeResponse { - uint64 worktree_id = 1; - string canonicalized_path = 2; -} - -message RemoveWorktree { - uint64 worktree_id = 1; -} - -message GetPathMetadata { - uint64 project_id = 1; - string path = 2; -} - -message GetPathMetadataResponse { - bool exists = 1; - string path = 2; - bool is_dir = 3; -} - -message WorktreeMetadata { - uint64 id = 1; - string root_name = 2; - bool visible = 3; - string abs_path = 4; -} - -message ProjectPath { - uint64 worktree_id = 1; - string path = 2; -} - -message ListRemoteDirectoryConfig { - bool is_dir = 1; -} - -message ListRemoteDirectory { - uint64 dev_server_id = 1; - string path = 2; - ListRemoteDirectoryConfig config = 3; -} - -message EntryInfo { - bool is_dir = 1; -} - -message ListRemoteDirectoryResponse { - repeated string entries = 1; - repeated EntryInfo entry_info = 2; -} - -message CreateProjectEntry { - uint64 project_id = 1; - uint64 worktree_id = 2; - string path = 3; - bool is_directory = 4; - optional bytes content = 5; -} - -message RenameProjectEntry { - uint64 project_id = 1; - uint64 entry_id = 2; - string new_path = 3; - uint64 new_worktree_id = 4; -} - -message CopyProjectEntry { - uint64 project_id = 1; - uint64 entry_id = 2; - string new_path = 3; - uint64 new_worktree_id = 5; - reserved 4; -} - -message DeleteProjectEntry { - uint64 project_id = 1; - uint64 entry_id = 2; - bool use_trash = 3; -} - -message ExpandProjectEntry { - uint64 project_id = 1; - uint64 entry_id = 2; -} - -message ExpandProjectEntryResponse { - uint64 worktree_scan_id = 1; -} - -message ExpandAllForProjectEntry { - uint64 project_id = 1; - uint64 entry_id = 2; -} - -message ExpandAllForProjectEntryResponse { - uint64 worktree_scan_id = 1; -} - -message ProjectEntryResponse { - optional Entry entry = 1; - uint64 worktree_scan_id = 2; -} - -message UpdateWorktreeSettings { - uint64 project_id = 1; - uint64 worktree_id = 2; - string path = 3; - optional string content = 4; - optional LocalSettingsKind kind = 5; -} - -enum LocalSettingsKind { - Settings = 0; - Tasks = 1; - Editorconfig = 2; - Debug = 3; -} - -message UpdateUserSettings { - uint64 project_id = 1; - string contents = 2; -} diff --git a/crates/proto/proto/zed.proto b/crates/proto/proto/zed.proto deleted file mode 100644 index 8e26a26a43..0000000000 --- a/crates/proto/proto/zed.proto +++ /dev/null @@ -1,527 +0,0 @@ -syntax = "proto3"; -package zed.messages; - -import "ai.proto"; -import "app.proto"; -import "buffer.proto"; -import "call.proto"; -import "channel.proto"; -import "core.proto"; -import "debugger.proto"; -import "git.proto"; -import "image.proto"; -import "lsp.proto"; -import "notification.proto"; -import "task.proto"; -import "toolchain.proto"; -import "worktree.proto"; - -// Looking for a number? Search "// current max" - -message Envelope { - uint32 id = 1; - optional uint32 responding_to = 2; - optional PeerId original_sender_id = 3; - optional uint32 ack_id = 266; - - oneof payload { - Hello hello = 4; - Ack ack = 5; - Error error = 6; - Ping ping = 7; - Test test = 8; - EndStream end_stream = 165; - - CreateRoom create_room = 9; - CreateRoomResponse create_room_response = 10; - JoinRoom join_room = 11; - JoinRoomResponse join_room_response = 12; - RejoinRoom rejoin_room = 13; - RejoinRoomResponse rejoin_room_response = 14; - LeaveRoom leave_room = 15; - Call call = 16; - IncomingCall incoming_call = 17; - CallCanceled call_canceled = 18; - CancelCall cancel_call = 19; - DeclineCall decline_call = 20; - UpdateParticipantLocation update_participant_location = 21; - RoomUpdated room_updated = 22; - - ShareProject share_project = 23; - ShareProjectResponse share_project_response = 24; - UnshareProject unshare_project = 25; - JoinProject join_project = 26; - JoinProjectResponse join_project_response = 27; - LeaveProject leave_project = 28; - AddProjectCollaborator add_project_collaborator = 29; - UpdateProjectCollaborator update_project_collaborator = 30; - RemoveProjectCollaborator remove_project_collaborator = 31; - - GetDefinition get_definition = 32; - GetDefinitionResponse get_definition_response = 33; - GetDeclaration get_declaration = 237; - GetDeclarationResponse get_declaration_response = 238; - GetTypeDefinition get_type_definition = 34; - GetTypeDefinitionResponse get_type_definition_response = 35; - - GetReferences get_references = 36; - GetReferencesResponse get_references_response = 37; - GetDocumentHighlights get_document_highlights = 38; - GetDocumentHighlightsResponse get_document_highlights_response = 39; - GetProjectSymbols get_project_symbols = 40; - GetProjectSymbolsResponse get_project_symbols_response = 41; - OpenBufferForSymbol open_buffer_for_symbol = 42; - OpenBufferForSymbolResponse open_buffer_for_symbol_response = 43; - - UpdateProject update_project = 44; - UpdateWorktree update_worktree = 45; - - CreateProjectEntry create_project_entry = 46; - RenameProjectEntry rename_project_entry = 47; - CopyProjectEntry copy_project_entry = 48; - DeleteProjectEntry delete_project_entry = 49; - ProjectEntryResponse project_entry_response = 50; - ExpandProjectEntry expand_project_entry = 51; - ExpandProjectEntryResponse expand_project_entry_response = 52; - ExpandAllForProjectEntry expand_all_for_project_entry = 291; - ExpandAllForProjectEntryResponse expand_all_for_project_entry_response = 292; - UpdateDiagnosticSummary update_diagnostic_summary = 53; - StartLanguageServer start_language_server = 54; - UpdateLanguageServer update_language_server = 55; - - OpenBufferById open_buffer_by_id = 56; - OpenBufferByPath open_buffer_by_path = 57; - OpenBufferResponse open_buffer_response = 58; - CreateBufferForPeer create_buffer_for_peer = 59; - UpdateBuffer update_buffer = 60; - UpdateBufferFile update_buffer_file = 61; - SaveBuffer save_buffer = 62; - BufferSaved buffer_saved = 63; - BufferReloaded buffer_reloaded = 64; - ReloadBuffers reload_buffers = 65; - ReloadBuffersResponse reload_buffers_response = 66; - SynchronizeBuffers synchronize_buffers = 67; - SynchronizeBuffersResponse synchronize_buffers_response = 68; - FormatBuffers format_buffers = 69; - FormatBuffersResponse format_buffers_response = 70; - GetCompletions get_completions = 71; - GetCompletionsResponse get_completions_response = 72; - ResolveCompletionDocumentation resolve_completion_documentation = 73; - ResolveCompletionDocumentationResponse resolve_completion_documentation_response = 74; - ApplyCompletionAdditionalEdits apply_completion_additional_edits = 75; - ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 76; - GetCodeActions get_code_actions = 77; - GetCodeActionsResponse get_code_actions_response = 78; - GetHover get_hover = 79; - GetHoverResponse get_hover_response = 80; - ApplyCodeAction apply_code_action = 81; - ApplyCodeActionResponse apply_code_action_response = 82; - PrepareRename prepare_rename = 83; - PrepareRenameResponse prepare_rename_response = 84; - PerformRename perform_rename = 85; - PerformRenameResponse perform_rename_response = 86; - - UpdateContacts update_contacts = 89; - UpdateInviteInfo update_invite_info = 90; - ShowContacts show_contacts = 91; - - GetUsers get_users = 92; - FuzzySearchUsers fuzzy_search_users = 93; - UsersResponse users_response = 94; - RequestContact request_contact = 95; - RespondToContactRequest respond_to_contact_request = 96; - RemoveContact remove_contact = 97; - - Follow follow = 98; - FollowResponse follow_response = 99; - UpdateFollowers update_followers = 100; - Unfollow unfollow = 101; - UpdateDiffBases update_diff_bases = 104; - - OnTypeFormatting on_type_formatting = 105; - OnTypeFormattingResponse on_type_formatting_response = 106; - - UpdateWorktreeSettings update_worktree_settings = 107; - - InlayHints inlay_hints = 108; - InlayHintsResponse inlay_hints_response = 109; - ResolveInlayHint resolve_inlay_hint = 110; - ResolveInlayHintResponse resolve_inlay_hint_response = 111; - RefreshInlayHints refresh_inlay_hints = 112; - - CreateChannel create_channel = 113; - CreateChannelResponse create_channel_response = 114; - InviteChannelMember invite_channel_member = 115; - RemoveChannelMember remove_channel_member = 116; - RespondToChannelInvite respond_to_channel_invite = 117; - UpdateChannels update_channels = 118; - JoinChannel join_channel = 119; - DeleteChannel delete_channel = 120; - GetChannelMembers get_channel_members = 121; - GetChannelMembersResponse get_channel_members_response = 122; - SetChannelMemberRole set_channel_member_role = 123; - RenameChannel rename_channel = 124; - RenameChannelResponse rename_channel_response = 125; - SubscribeToChannels subscribe_to_channels = 207; - - JoinChannelBuffer join_channel_buffer = 126; - JoinChannelBufferResponse join_channel_buffer_response = 127; - UpdateChannelBuffer update_channel_buffer = 128; - LeaveChannelBuffer leave_channel_buffer = 129; - UpdateChannelBufferCollaborators update_channel_buffer_collaborators = 130; - RejoinChannelBuffers rejoin_channel_buffers = 131; - RejoinChannelBuffersResponse rejoin_channel_buffers_response = 132; - AckBufferOperation ack_buffer_operation = 133; - - JoinChannelChat join_channel_chat = 134; - JoinChannelChatResponse join_channel_chat_response = 135; - LeaveChannelChat leave_channel_chat = 136; - SendChannelMessage send_channel_message = 137; - SendChannelMessageResponse send_channel_message_response = 138; - ChannelMessageSent channel_message_sent = 139; - GetChannelMessages get_channel_messages = 140; - GetChannelMessagesResponse get_channel_messages_response = 141; - RemoveChannelMessage remove_channel_message = 142; - AckChannelMessage ack_channel_message = 143; - GetChannelMessagesById get_channel_messages_by_id = 144; - - MoveChannel move_channel = 147; - ReorderChannel reorder_channel = 349; - SetChannelVisibility set_channel_visibility = 148; - - AddNotification add_notification = 149; - GetNotifications get_notifications = 150; - GetNotificationsResponse get_notifications_response = 151; - DeleteNotification delete_notification = 152; - MarkNotificationRead mark_notification_read = 153; - LspExtExpandMacro lsp_ext_expand_macro = 154; - LspExtExpandMacroResponse lsp_ext_expand_macro_response = 155; - SetRoomParticipantRole set_room_participant_role = 156; - - UpdateUserChannels update_user_channels = 157; - - GetImplementation get_implementation = 162; - GetImplementationResponse get_implementation_response = 163; - - UpdateChannelMessage update_channel_message = 170; - ChannelMessageUpdate channel_message_update = 171; - - BlameBuffer blame_buffer = 172; - BlameBufferResponse blame_buffer_response = 173; - - UpdateNotification update_notification = 174; - - RestartLanguageServers restart_language_servers = 208; - - RejoinRemoteProjects rejoin_remote_projects = 186; - RejoinRemoteProjectsResponse rejoin_remote_projects_response = 187; - - OpenNewBuffer open_new_buffer = 196; - - GetSupermavenApiKey get_supermaven_api_key = 198; - GetSupermavenApiKeyResponse get_supermaven_api_key_response = 199; - - TaskContextForLocation task_context_for_location = 203; - TaskContext task_context = 204; - - LinkedEditingRange linked_editing_range = 209; - LinkedEditingRangeResponse linked_editing_range_response = 210; - - AdvertiseContexts advertise_contexts = 211; - OpenContext open_context = 212; - OpenContextResponse open_context_response = 213; - CreateContext create_context = 232; - CreateContextResponse create_context_response = 233; - UpdateContext update_context = 214; - SynchronizeContexts synchronize_contexts = 215; - SynchronizeContextsResponse synchronize_contexts_response = 216; - - GetSignatureHelp get_signature_help = 217; - GetSignatureHelpResponse get_signature_help_response = 218; - - ListRemoteDirectory list_remote_directory = 219; - ListRemoteDirectoryResponse list_remote_directory_response = 220; - AddWorktree add_worktree = 222; - AddWorktreeResponse add_worktree_response = 223; - - LspExtSwitchSourceHeader lsp_ext_switch_source_header = 241; - LspExtSwitchSourceHeaderResponse lsp_ext_switch_source_header_response = 242; - - FindSearchCandidates find_search_candidates = 243; - FindSearchCandidatesResponse find_search_candidates_response = 244; - - CloseBuffer close_buffer = 245; - - ShutdownRemoteServer shutdown_remote_server = 257; - - RemoveWorktree remove_worktree = 258; - - LanguageServerLog language_server_log = 260; - - Toast toast = 261; - HideToast hide_toast = 262; - - OpenServerSettings open_server_settings = 263; - - GetPermalinkToLine get_permalink_to_line = 264; - GetPermalinkToLineResponse get_permalink_to_line_response = 265; - - FlushBufferedMessages flush_buffered_messages = 267; - - LanguageServerPromptRequest language_server_prompt_request = 268; - LanguageServerPromptResponse language_server_prompt_response = 269; - - GitBranchesResponse git_branches_response = 271; - - UpdateGitBranch update_git_branch = 272; - - ListToolchains list_toolchains = 273; - ListToolchainsResponse list_toolchains_response = 274; - ActivateToolchain activate_toolchain = 275; - ActiveToolchain active_toolchain = 276; - ActiveToolchainResponse active_toolchain_response = 277; - - GetPathMetadata get_path_metadata = 278; - GetPathMetadataResponse get_path_metadata_response = 279; - - CancelLanguageServerWork cancel_language_server_work = 282; - - LspExtOpenDocs lsp_ext_open_docs = 283; - LspExtOpenDocsResponse lsp_ext_open_docs_response = 284; - - SyncExtensions sync_extensions = 285; - SyncExtensionsResponse sync_extensions_response = 286; - InstallExtension install_extension = 287; - - OpenUnstagedDiff open_unstaged_diff = 288; - OpenUnstagedDiffResponse open_unstaged_diff_response = 289; - - RegisterBufferWithLanguageServers register_buffer_with_language_servers = 290; - - Stage stage = 293; - Unstage unstage = 294; - Commit commit = 295; - OpenCommitMessageBuffer open_commit_message_buffer = 296; - - OpenUncommittedDiff open_uncommitted_diff = 297; - OpenUncommittedDiffResponse open_uncommitted_diff_response = 298; - - SetIndexText set_index_text = 299; - - GitShow git_show = 300; - GitReset git_reset = 301; - GitCommitDetails git_commit_details = 302; - GitCheckoutFiles git_checkout_files = 303; - - Push push = 304; - Fetch fetch = 305; - GetRemotes get_remotes = 306; - GetRemotesResponse get_remotes_response = 307; - Pull pull = 308; - - ApplyCodeActionKind apply_code_action_kind = 309; - ApplyCodeActionKindResponse apply_code_action_kind_response = 310; - - RemoteMessageResponse remote_message_response = 311; - - GitGetBranches git_get_branches = 312; - GitCreateBranch git_create_branch = 313; - GitChangeBranch git_change_branch = 314; - - CheckForPushedCommits check_for_pushed_commits = 315; - CheckForPushedCommitsResponse check_for_pushed_commits_response = 316; - - AskPassRequest ask_pass_request = 317; - AskPassResponse ask_pass_response = 318; - - GitDiff git_diff = 319; - GitDiffResponse git_diff_response = 320; - GitInit git_init = 321; - - CodeLens code_lens = 322; - GetCodeLens get_code_lens = 323; - GetCodeLensResponse get_code_lens_response = 324; - RefreshCodeLens refresh_code_lens = 325; - - ToggleBreakpoint toggle_breakpoint = 326; - BreakpointsForFile breakpoints_for_file = 327; - - UpdateRepository update_repository = 328; - RemoveRepository remove_repository = 329; - - GetDocumentSymbols get_document_symbols = 330; - GetDocumentSymbolsResponse get_document_symbols_response = 331; - - LoadCommitDiff load_commit_diff = 334; - LoadCommitDiffResponse load_commit_diff_response = 335; - - StopLanguageServers stop_language_servers = 336; - - LspExtRunnables lsp_ext_runnables = 337; - LspExtRunnablesResponse lsp_ext_runnables_response = 338; - - GetDebugAdapterBinary get_debug_adapter_binary = 339; - DebugAdapterBinary debug_adapter_binary = 340; - RunDebugLocators run_debug_locators = 341; - DebugRequest debug_request = 342; - - LspExtGoToParentModule lsp_ext_go_to_parent_module = 343; - LspExtGoToParentModuleResponse lsp_ext_go_to_parent_module_response = 344; - LspExtCancelFlycheck lsp_ext_cancel_flycheck = 345; - LspExtRunFlycheck lsp_ext_run_flycheck = 346; - LspExtClearFlycheck lsp_ext_clear_flycheck = 347; - - LogToDebugConsole log_to_debug_console = 348; - - GetDocumentDiagnostics get_document_diagnostics = 350; - GetDocumentDiagnosticsResponse get_document_diagnostics_response = 351; - PullWorkspaceDiagnostics pull_workspace_diagnostics = 352; - - GetDocumentColor get_document_color = 353; - GetDocumentColorResponse get_document_color_response = 354; - GetColorPresentation get_color_presentation = 355; - GetColorPresentationResponse get_color_presentation_response = 356; - - Stash stash = 357; - StashPop stash_pop = 358; - - GetDefaultBranch get_default_branch = 359; - GetDefaultBranchResponse get_default_branch_response = 360; - - GetCrashFiles get_crash_files = 361; - GetCrashFilesResponse get_crash_files_response = 362; - - GitClone git_clone = 363; - GitCloneResponse git_clone_response = 364; - - LspQuery lsp_query = 365; - LspQueryResponse lsp_query_response = 366; - ToggleLspLogs toggle_lsp_logs = 367; - - UpdateUserSettings update_user_settings = 368; - - GetProcesses get_processes = 369; - GetProcessesResponse get_processes_response = 370; - - ResolveToolchain resolve_toolchain = 371; - ResolveToolchainResponse resolve_toolchain_response = 372; - - GetAgentServerCommand get_agent_server_command = 373; - AgentServerCommand agent_server_command = 374; - - ExternalAgentsUpdated external_agents_updated = 375; - ExternalAgentLoadingStatusUpdated external_agent_loading_status_updated = 376; - NewExternalAgentVersionAvailable new_external_agent_version_available = 377; - - StashDrop stash_drop = 378; - StashApply stash_apply = 379; - - GitRenameBranch git_rename_branch = 380; - - RemoteStarted remote_started = 381; - - GetDirectoryEnvironment get_directory_environment = 382; - DirectoryEnvironment directory_environment = 383; - - GetTreeDiff get_tree_diff = 384; - GetTreeDiffResponse get_tree_diff_response = 385; - - GetBlobContent get_blob_content = 386; - GetBlobContentResponse get_blob_content_response = 387; - - GitWorktreesResponse git_worktrees_response = 388; - GitGetWorktrees git_get_worktrees = 389; - GitCreateWorktree git_create_worktree = 390; - - OpenImageByPath open_image_by_path = 391; - OpenImageResponse open_image_response = 392; - CreateImageForPeer create_image_for_peer = 393; - - - GitFileHistory git_file_history = 397; - GitFileHistoryResponse git_file_history_response = 398; - - RunGitHook run_git_hook = 399; - - GitDeleteBranch git_delete_branch = 400; - - ExternalExtensionAgentsUpdated external_extension_agents_updated = 401; - - GitCreateRemote git_create_remote = 402; - GitRemoveRemote git_remove_remote = 403;// current max - } - - reserved 87 to 88, 396; - reserved 102 to 103; - reserved 158 to 161; - reserved 164; - reserved 166 to 169; - reserved 175 to 185; - reserved 188; - reserved 189 to 192; - reserved 193 to 195; - reserved 197; - reserved 200 to 202; - reserved 205 to 206; - reserved 221; - reserved 224 to 229; - reserved 230 to 231; - reserved 234 to 236; - reserved 239 to 240; - reserved 246; - reserved 247 to 254; - reserved 255 to 256; - reserved 259; - reserved 270; - reserved 280 to 281; - reserved 332 to 333; - reserved 394 to 395; -} - -message Hello { - PeerId peer_id = 1; -} - -message Ping {} - -message Ack {} - -message Error { - string message = 1; - ErrorCode code = 2; - repeated string tags = 3; -} - -enum ErrorCode { - Internal = 0; - NoSuchChannel = 1; - Disconnected = 2; - SignedOut = 3; - UpgradeRequired = 4; - Forbidden = 5; - NeedsCla = 7; - NotARootChannel = 8; - BadPublicNesting = 9; - CircularNesting = 10; - WrongMoveTarget = 11; - UnsharedItem = 12; - NoSuchProject = 13; - DevServerProjectPathDoesNotExist = 16; - RemoteUpgradeRequired = 17; - RateLimitExceeded = 18; - CommitFailed = 19; - reserved 6; - reserved 14 to 15; -} - -message EndStream {} - -message Test { - uint64 id = 1; -} - -message FlushBufferedMessages {} - -message FlushBufferedMessagesResponse {} - -message RemoteStarted {} diff --git a/crates/proto/src/error.rs b/crates/proto/src/error.rs deleted file mode 100644 index d83b0fc499..0000000000 --- a/crates/proto/src/error.rs +++ /dev/null @@ -1,250 +0,0 @@ -/// Some helpers for structured error handling. -/// -/// The helpers defined here allow you to pass type-safe error codes from -/// the collab server to the client; and provide a mechanism for additional -/// structured data alongside the message. -/// -/// When returning an error, it can be as simple as: -/// -/// `return Err(Error::Forbidden.into())` -/// -/// If you'd like to log more context, you can set a message. These messages -/// show up in our logs, but are not shown visibly to users. -/// -/// `return Err(Error::Forbidden.message("not an admin").into())` -/// -/// If you'd like to provide enough context that the UI can render a good error -/// message (or would be helpful to see in a structured format in the logs), you -/// can use .with_tag(): -/// -/// `return Err(Error::WrongReleaseChannel.with_tag("required", "stable").into())` -/// -/// When handling an error you can use .error_code() to match which error it was -/// and .error_tag() to read any tags. -/// -/// ```ignore -/// use proto::{ErrorCode, ErrorExt}; -/// -/// match err.error_code() { -/// ErrorCode::Forbidden => alert("I'm sorry I can't do that."), -/// ErrorCode::WrongReleaseChannel => -/// alert(format!("You need to be on the {} release channel.", err.error_tag("required").unwrap())), -/// ErrorCode::Internal => alert("Sorry, something went wrong"), -/// } -/// ``` -/// -pub use crate::ErrorCode; - -/// ErrorCodeExt provides some helpers for structured error handling. -/// -/// The primary implementation is on the proto::ErrorCode to easily convert -/// that into an anyhow::Error, which we use pervasively. -/// -/// The RpcError struct provides support for further metadata if needed. -pub trait ErrorCodeExt { - /// Return an anyhow::Error containing this. - /// (useful in places where .into() doesn't have enough type information) - fn anyhow(self) -> anyhow::Error; - - /// Add a message to the error (by default the error code is used) - fn message(self, msg: String) -> RpcError; - - /// Add a tag to the error. Tags are key value pairs that can be used - /// to send semi-structured data along with the error. - fn with_tag(self, k: &str, v: &str) -> RpcError; -} - -impl ErrorCodeExt for ErrorCode { - fn anyhow(self) -> anyhow::Error { - self.into() - } - - fn message(self, msg: String) -> RpcError { - let err: RpcError = self.into(); - err.message(msg) - } - - fn with_tag(self, k: &str, v: &str) -> RpcError { - let err: RpcError = self.into(); - err.with_tag(k, v) - } -} - -/// ErrorExt provides helpers for structured error handling. -/// -/// The primary implementation is on the anyhow::Error, which is -/// what we use throughout our codebase. Though under the hood this -pub trait ErrorExt { - /// error_code() returns the ErrorCode (or ErrorCode::Internal if there is none) - fn error_code(&self) -> ErrorCode; - /// error_tag() returns the value of the tag with the given key, if any. - fn error_tag(&self, k: &str) -> Option<&str>; - /// to_proto() converts the error into a crate::Error - fn to_proto(&self) -> crate::Error; - /// Clones the error and turns into an [anyhow::Error]. - fn cloned(&self) -> anyhow::Error; -} - -impl ErrorExt for anyhow::Error { - fn error_code(&self) -> ErrorCode { - if let Some(rpc_error) = self.downcast_ref::() { - rpc_error.code - } else { - ErrorCode::Internal - } - } - - fn error_tag(&self, k: &str) -> Option<&str> { - if let Some(rpc_error) = self.downcast_ref::() { - rpc_error.error_tag(k) - } else { - None - } - } - - fn to_proto(&self) -> crate::Error { - if let Some(rpc_error) = self.downcast_ref::() { - rpc_error.to_proto() - } else { - ErrorCode::Internal - .message( - format!("{self:#}") - .lines() - .fold(String::new(), |mut message, line| { - if !message.is_empty() { - message.push(' '); - } - message.push_str(line); - message - }), - ) - .to_proto() - } - } - - fn cloned(&self) -> anyhow::Error { - if let Some(rpc_error) = self.downcast_ref::() { - rpc_error.cloned() - } else { - anyhow::anyhow!("{self:#}") - } - } -} - -impl From for anyhow::Error { - fn from(value: ErrorCode) -> Self { - RpcError { - request: None, - code: value, - msg: format!("{:?}", value), - tags: Default::default(), - } - .into() - } -} - -#[derive(Clone, Debug)] -pub struct RpcError { - request: Option, - msg: String, - code: ErrorCode, - tags: Vec, -} - -/// RpcError is a structured error type that is returned by the collab server. -/// In addition to a message, it lets you set a specific ErrorCode, and attach -/// small amounts of metadata to help the client handle the error appropriately. -/// -/// This struct is not typically used directly, as we pass anyhow::Error around -/// in the app; however it is useful for chaining .message() and .with_tag() on -/// ErrorCode. -impl RpcError { - /// from_proto converts a crate::Error into an anyhow::Error containing - /// an RpcError. - pub fn from_proto(error: &crate::Error, request: &str) -> anyhow::Error { - RpcError { - request: Some(request.to_string()), - code: error.code(), - msg: error.message.clone(), - tags: error.tags.clone(), - } - .into() - } -} - -impl ErrorCodeExt for RpcError { - fn message(mut self, msg: String) -> RpcError { - self.msg = msg; - self - } - - fn with_tag(mut self, k: &str, v: &str) -> RpcError { - self.tags.push(format!("{}={}", k, v)); - self - } - - fn anyhow(self) -> anyhow::Error { - self.into() - } -} - -impl ErrorExt for RpcError { - fn error_tag(&self, k: &str) -> Option<&str> { - for tag in &self.tags { - let mut parts = tag.split('='); - if let Some(key) = parts.next() - && key == k - { - return parts.next(); - } - } - None - } - - fn error_code(&self) -> ErrorCode { - self.code - } - - fn to_proto(&self) -> crate::Error { - crate::Error { - code: self.code as i32, - message: self.msg.clone(), - tags: self.tags.clone(), - } - } - - fn cloned(&self) -> anyhow::Error { - self.clone().into() - } -} - -impl std::error::Error for RpcError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - None - } -} - -impl std::fmt::Display for RpcError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - if let Some(request) = &self.request { - write!(f, "RPC request {} failed: {}", request, self.msg)? - } else { - write!(f, "{}", self.msg)? - } - for tag in &self.tags { - write!(f, " {}", tag)? - } - Ok(()) - } -} - -impl From for RpcError { - fn from(code: ErrorCode) -> Self { - RpcError { - request: None, - code, - msg: format!("{:?}", code), - tags: Default::default(), - } - } -} diff --git a/crates/proto/src/macros.rs b/crates/proto/src/macros.rs deleted file mode 100644 index 59e984d7db..0000000000 --- a/crates/proto/src/macros.rs +++ /dev/null @@ -1,100 +0,0 @@ -#[macro_export] -macro_rules! messages { - ($(($name:ident, $priority:ident)),* $(,)?) => { - pub fn build_typed_envelope(sender_id: PeerId, received_at: std::time::Instant, envelope: Envelope) -> Option> { - match envelope.payload { - $(Some(envelope::Payload::$name(payload)) => { - Some(Box::new(TypedEnvelope { - sender_id, - original_sender_id: envelope.original_sender_id, - message_id: envelope.id, - payload, - received_at, - })) - }, )* - _ => None - } - } - - $( - impl EnvelopedMessage for $name { - const NAME: &'static str = std::stringify!($name); - const PRIORITY: MessagePriority = MessagePriority::$priority; - - fn into_envelope( - self, - id: u32, - responding_to: Option, - original_sender_id: Option, - ) -> Envelope { - Envelope { - id, - responding_to, - original_sender_id, - payload: Some(envelope::Payload::$name(self)), - ack_id: None, - } - } - - fn from_envelope(envelope: Envelope) -> Option { - if let Some(envelope::Payload::$name(msg)) = envelope.payload { - Some(msg) - } else { - None - } - } - } - )* - }; -} - -#[macro_export] -macro_rules! request_messages { - ($(($request_name:ident, $response_name:ident)),* $(,)?) => { - $(impl RequestMessage for $request_name { - type Response = $response_name; - })* - }; -} - -#[macro_export] -macro_rules! entity_messages { - ({$id_field:ident, $entity_type:ty}, $($name:ident),* $(,)?) => { - $(impl EntityMessage for $name { - type Entity = $entity_type; - - fn remote_entity_id(&self) -> u64 { - self.$id_field - } - })* - }; -} - -#[macro_export] -macro_rules! lsp_messages { - ($(($request_name:ident, $response_name:ident, $stop_previous_requests:expr)),* $(,)?) => { - $(impl LspRequestMessage for $request_name { - type Response = $response_name; - - fn to_proto_query(self) -> $crate::lsp_query::Request { - $crate::lsp_query::Request::$request_name(self) - } - - fn response_to_proto_query(response: Self::Response) -> $crate::lsp_response::Response { - $crate::lsp_response::Response::$response_name(response) - } - - fn buffer_id(&self) -> u64 { - self.buffer_id - } - - fn buffer_version(&self) -> &[$crate::VectorClockEntry] { - &self.version - } - - fn stop_previous_requests() -> bool { - $stop_previous_requests - } - })* - }; -} diff --git a/crates/proto/src/proto.rs b/crates/proto/src/proto.rs deleted file mode 100644 index 455f947046..0000000000 --- a/crates/proto/src/proto.rs +++ /dev/null @@ -1,923 +0,0 @@ -#![allow(non_snake_case)] - -pub mod error; -mod macros; -mod typed_envelope; - -pub use error::*; -pub use prost::{DecodeError, Message}; -use std::{ - cmp, - fmt::Debug, - iter, mem, - time::{Duration, SystemTime, UNIX_EPOCH}, -}; -pub use typed_envelope::*; - -include!(concat!(env!("OUT_DIR"), "/zed.messages.rs")); - -pub const REMOTE_SERVER_PEER_ID: PeerId = PeerId { owner_id: 0, id: 0 }; -pub const REMOTE_SERVER_PROJECT_ID: u64 = 0; - -messages!( - (Ack, Foreground), - (AckBufferOperation, Background), - (AckChannelMessage, Background), - (ActivateToolchain, Foreground), - (ActiveToolchain, Foreground), - (ActiveToolchainResponse, Foreground), - (ResolveToolchain, Background), - (ResolveToolchainResponse, Background), - (AddNotification, Foreground), - (AddProjectCollaborator, Foreground), - (AddWorktree, Foreground), - (AddWorktreeResponse, Foreground), - (AdvertiseContexts, Foreground), - (ApplyCodeAction, Background), - (ApplyCodeActionResponse, Background), - (ApplyCompletionAdditionalEdits, Background), - (ApplyCompletionAdditionalEditsResponse, Background), - (BlameBuffer, Foreground), - (BlameBufferResponse, Foreground), - (BufferReloaded, Foreground), - (BufferSaved, Foreground), - (Call, Foreground), - (CallCanceled, Foreground), - (CancelCall, Foreground), - (CancelLanguageServerWork, Foreground), - (ChannelMessageSent, Foreground), - (ChannelMessageUpdate, Foreground), - (CloseBuffer, Foreground), - (Commit, Background), - (RunGitHook, Background), - (CopyProjectEntry, Foreground), - (CreateBufferForPeer, Foreground), - (CreateImageForPeer, Foreground), - (CreateChannel, Foreground), - (CreateChannelResponse, Foreground), - (CreateContext, Foreground), - (CreateContextResponse, Foreground), - (CreateProjectEntry, Foreground), - (CreateRoom, Foreground), - (CreateRoomResponse, Foreground), - (DeclineCall, Foreground), - (DeleteChannel, Foreground), - (DeleteNotification, Foreground), - (DeleteProjectEntry, Foreground), - (EndStream, Foreground), - (Error, Foreground), - (ExpandProjectEntry, Foreground), - (ExpandProjectEntryResponse, Foreground), - (FindSearchCandidatesResponse, Background), - (FindSearchCandidates, Background), - (FlushBufferedMessages, Foreground), - (ExpandAllForProjectEntry, Foreground), - (ExpandAllForProjectEntryResponse, Foreground), - (Follow, Foreground), - (FollowResponse, Foreground), - (ApplyCodeActionKind, Foreground), - (ApplyCodeActionKindResponse, Foreground), - (FormatBuffers, Foreground), - (FormatBuffersResponse, Foreground), - (FuzzySearchUsers, Foreground), - (GetChannelMembers, Foreground), - (GetChannelMembersResponse, Foreground), - (GetChannelMessages, Background), - (GetChannelMessagesById, Background), - (GetChannelMessagesResponse, Background), - (GetCodeActions, Background), - (GetCodeActionsResponse, Background), - (GetCompletions, Background), - (GetCompletionsResponse, Background), - (GetDeclaration, Background), - (GetDeclarationResponse, Background), - (GetDefinition, Background), - (GetDefinitionResponse, Background), - (GetDocumentHighlights, Background), - (GetDocumentHighlightsResponse, Background), - (GetDocumentSymbols, Background), - (GetDocumentSymbolsResponse, Background), - (GetHover, Background), - (GetHoverResponse, Background), - (GetNotifications, Foreground), - (GetNotificationsResponse, Foreground), - (GetCrashFiles, Background), - (GetCrashFilesResponse, Background), - (GetPathMetadata, Background), - (GetPathMetadataResponse, Background), - (GetPermalinkToLine, Foreground), - (GetProcesses, Background), - (GetProcessesResponse, Background), - (GetPermalinkToLineResponse, Foreground), - (GetProjectSymbols, Background), - (GetProjectSymbolsResponse, Background), - (GetReferences, Background), - (GetReferencesResponse, Background), - (GetSignatureHelp, Background), - (GetSignatureHelpResponse, Background), - (GetSupermavenApiKey, Background), - (GetSupermavenApiKeyResponse, Background), - (GetTypeDefinition, Background), - (GetTypeDefinitionResponse, Background), - (GetImplementation, Background), - (GetImplementationResponse, Background), - (OpenUnstagedDiff, Foreground), - (OpenUnstagedDiffResponse, Foreground), - (OpenUncommittedDiff, Foreground), - (OpenUncommittedDiffResponse, Foreground), - (GetUsers, Foreground), - (GitGetBranches, Background), - (GitBranchesResponse, Background), - (Hello, Foreground), - (HideToast, Background), - (IncomingCall, Foreground), - (InlayHints, Background), - (InlayHintsResponse, Background), - (InstallExtension, Background), - (InviteChannelMember, Foreground), - (JoinChannel, Foreground), - (JoinChannelBuffer, Foreground), - (JoinChannelBufferResponse, Foreground), - (JoinChannelChat, Foreground), - (JoinChannelChatResponse, Foreground), - (JoinProject, Foreground), - (JoinProjectResponse, Foreground), - (JoinRoom, Foreground), - (JoinRoomResponse, Foreground), - (LanguageServerLog, Foreground), - (LanguageServerPromptRequest, Foreground), - (LanguageServerPromptResponse, Foreground), - (LeaveChannelBuffer, Background), - (LeaveChannelChat, Foreground), - (LeaveProject, Foreground), - (LeaveRoom, Foreground), - (LinkedEditingRange, Background), - (LinkedEditingRangeResponse, Background), - (ListRemoteDirectory, Background), - (ListRemoteDirectoryResponse, Background), - (ListToolchains, Foreground), - (ListToolchainsResponse, Foreground), - (LoadCommitDiff, Foreground), - (LoadCommitDiffResponse, Foreground), - (LspExtExpandMacro, Background), - (LspExtExpandMacroResponse, Background), - (LspExtOpenDocs, Background), - (LspExtOpenDocsResponse, Background), - (LspExtRunnables, Background), - (LspExtRunnablesResponse, Background), - (LspExtSwitchSourceHeader, Background), - (LspExtSwitchSourceHeaderResponse, Background), - (LspExtGoToParentModule, Background), - (LspExtGoToParentModuleResponse, Background), - (LspExtCancelFlycheck, Background), - (LspExtRunFlycheck, Background), - (LspExtClearFlycheck, Background), - (MarkNotificationRead, Foreground), - (MoveChannel, Foreground), - (ReorderChannel, Foreground), - (LspQuery, Background), - (LspQueryResponse, Background), - (OnTypeFormatting, Background), - (OnTypeFormattingResponse, Background), - (OpenBufferById, Background), - (OpenBufferByPath, Background), - (OpenImageByPath, Background), - (OpenBufferForSymbol, Background), - (OpenBufferForSymbolResponse, Background), - (OpenBufferResponse, Background), - (OpenImageResponse, Background), - (OpenCommitMessageBuffer, Background), - (OpenContext, Foreground), - (OpenContextResponse, Foreground), - (OpenNewBuffer, Foreground), - (OpenServerSettings, Foreground), - (PerformRename, Background), - (PerformRenameResponse, Background), - (Ping, Foreground), - (PrepareRename, Background), - (PrepareRenameResponse, Background), - (ProjectEntryResponse, Foreground), - (RefreshInlayHints, Foreground), - (RegisterBufferWithLanguageServers, Background), - (RejoinChannelBuffers, Foreground), - (RejoinChannelBuffersResponse, Foreground), - (RejoinRemoteProjects, Foreground), - (RejoinRemoteProjectsResponse, Foreground), - (RejoinRoom, Foreground), - (RejoinRoomResponse, Foreground), - (ReloadBuffers, Foreground), - (ReloadBuffersResponse, Foreground), - (RemoveChannelMember, Foreground), - (RemoveChannelMessage, Foreground), - (RemoveContact, Foreground), - (RemoveProjectCollaborator, Foreground), - (RemoveWorktree, Foreground), - (RenameChannel, Foreground), - (RenameChannelResponse, Foreground), - (RenameProjectEntry, Foreground), - (RequestContact, Foreground), - (ResolveCompletionDocumentation, Background), - (ResolveCompletionDocumentationResponse, Background), - (ResolveInlayHint, Background), - (ResolveInlayHintResponse, Background), - (GetDocumentColor, Background), - (GetDocumentColorResponse, Background), - (GetColorPresentation, Background), - (GetColorPresentationResponse, Background), - (RefreshCodeLens, Background), - (GetCodeLens, Background), - (GetCodeLensResponse, Background), - (RespondToChannelInvite, Foreground), - (RespondToContactRequest, Foreground), - (RestartLanguageServers, Foreground), - (StopLanguageServers, Background), - (RoomUpdated, Foreground), - (SaveBuffer, Foreground), - (SendChannelMessage, Background), - (SendChannelMessageResponse, Background), - (SetChannelMemberRole, Foreground), - (SetChannelVisibility, Foreground), - (SetRoomParticipantRole, Foreground), - (ShareProject, Foreground), - (ShareProjectResponse, Foreground), - (ShowContacts, Foreground), - (ShutdownRemoteServer, Foreground), - (Stage, Background), - (StartLanguageServer, Foreground), - (SubscribeToChannels, Foreground), - (SyncExtensions, Background), - (SyncExtensionsResponse, Background), - (BreakpointsForFile, Background), - (ToggleBreakpoint, Foreground), - (SynchronizeBuffers, Foreground), - (SynchronizeBuffersResponse, Foreground), - (SynchronizeContexts, Foreground), - (SynchronizeContextsResponse, Foreground), - (TaskContext, Background), - (TaskContextForLocation, Background), - (Test, Foreground), - (Toast, Background), - (Unfollow, Foreground), - (UnshareProject, Foreground), - (Unstage, Background), - (Stash, Background), - (StashPop, Background), - (StashApply, Background), - (StashDrop, Background), - (UpdateBuffer, Foreground), - (UpdateBufferFile, Foreground), - (UpdateChannelBuffer, Foreground), - (UpdateChannelBufferCollaborators, Foreground), - (UpdateChannelMessage, Foreground), - (UpdateChannels, Foreground), - (UpdateContacts, Foreground), - (UpdateContext, Foreground), - (UpdateDiagnosticSummary, Foreground), - (UpdateDiffBases, Foreground), - (UpdateFollowers, Foreground), - (UpdateGitBranch, Background), - (UpdateInviteInfo, Foreground), - (UpdateLanguageServer, Foreground), - (UpdateNotification, Foreground), - (UpdateParticipantLocation, Foreground), - (UpdateProject, Foreground), - (UpdateProjectCollaborator, Foreground), - (UpdateUserChannels, Foreground), - (UpdateWorktree, Foreground), - (UpdateWorktreeSettings, Foreground), - (UpdateUserSettings, Background), - (UpdateRepository, Foreground), - (RemoveRepository, Foreground), - (UsersResponse, Foreground), - (GitReset, Background), - (GitDeleteBranch, Background), - (GitCheckoutFiles, Background), - (GitShow, Background), - (GitCommitDetails, Background), - (GitFileHistory, Background), - (GitFileHistoryResponse, Background), - (SetIndexText, Background), - (Push, Background), - (Fetch, Background), - (GetRemotes, Background), - (GetRemotesResponse, Background), - (Pull, Background), - (RemoteMessageResponse, Background), - (AskPassRequest, Background), - (AskPassResponse, Background), - (GitCreateRemote, Background), - (GitRemoveRemote, Background), - (GitCreateBranch, Background), - (GitChangeBranch, Background), - (GitRenameBranch, Background), - (CheckForPushedCommits, Background), - (CheckForPushedCommitsResponse, Background), - (GitDiff, Background), - (GitDiffResponse, Background), - (GitInit, Background), - (GetDebugAdapterBinary, Background), - (DebugAdapterBinary, Background), - (RunDebugLocators, Background), - (DebugRequest, Background), - (LogToDebugConsole, Background), - (GetDocumentDiagnostics, Background), - (GetDocumentDiagnosticsResponse, Background), - (PullWorkspaceDiagnostics, Background), - (GetDefaultBranch, Background), - (GetDefaultBranchResponse, Background), - (GetTreeDiff, Background), - (GetTreeDiffResponse, Background), - (GetBlobContent, Background), - (GetBlobContentResponse, Background), - (GitClone, Background), - (GitCloneResponse, Background), - (ToggleLspLogs, Background), - (GetDirectoryEnvironment, Background), - (DirectoryEnvironment, Background), - (GetAgentServerCommand, Background), - (AgentServerCommand, Background), - (ExternalAgentsUpdated, Background), - (ExternalExtensionAgentsUpdated, Background), - (ExternalAgentLoadingStatusUpdated, Background), - (NewExternalAgentVersionAvailable, Background), - (RemoteStarted, Background), - (GitGetWorktrees, Background), - (GitWorktreesResponse, Background), - (GitCreateWorktree, Background) -); - -request_messages!( - (ApplyCodeAction, ApplyCodeActionResponse), - ( - ApplyCompletionAdditionalEdits, - ApplyCompletionAdditionalEditsResponse - ), - (Call, Ack), - (CancelCall, Ack), - (Commit, Ack), - (RunGitHook, Ack), - (CopyProjectEntry, ProjectEntryResponse), - (CreateChannel, CreateChannelResponse), - (CreateProjectEntry, ProjectEntryResponse), - (CreateRoom, CreateRoomResponse), - (DeclineCall, Ack), - (DeleteChannel, Ack), - (DeleteProjectEntry, ProjectEntryResponse), - (ExpandProjectEntry, ExpandProjectEntryResponse), - (ExpandAllForProjectEntry, ExpandAllForProjectEntryResponse), - (Follow, FollowResponse), - (ApplyCodeActionKind, ApplyCodeActionKindResponse), - (FormatBuffers, FormatBuffersResponse), - (FuzzySearchUsers, UsersResponse), - (GetChannelMembers, GetChannelMembersResponse), - (GetChannelMessages, GetChannelMessagesResponse), - (GetChannelMessagesById, GetChannelMessagesResponse), - (GetCodeActions, GetCodeActionsResponse), - (GetCompletions, GetCompletionsResponse), - (GetDefinition, GetDefinitionResponse), - (GetDeclaration, GetDeclarationResponse), - (GetImplementation, GetImplementationResponse), - (GetDocumentHighlights, GetDocumentHighlightsResponse), - (GetDocumentSymbols, GetDocumentSymbolsResponse), - (GetHover, GetHoverResponse), - (GetNotifications, GetNotificationsResponse), - (GetProjectSymbols, GetProjectSymbolsResponse), - (GetReferences, GetReferencesResponse), - (GetSignatureHelp, GetSignatureHelpResponse), - (OpenUnstagedDiff, OpenUnstagedDiffResponse), - (OpenUncommittedDiff, OpenUncommittedDiffResponse), - (GetSupermavenApiKey, GetSupermavenApiKeyResponse), - (GetTypeDefinition, GetTypeDefinitionResponse), - (LinkedEditingRange, LinkedEditingRangeResponse), - (ListRemoteDirectory, ListRemoteDirectoryResponse), - (GetUsers, UsersResponse), - (IncomingCall, Ack), - (InlayHints, InlayHintsResponse), - (GetCodeLens, GetCodeLensResponse), - (InviteChannelMember, Ack), - (JoinChannel, JoinRoomResponse), - (JoinChannelBuffer, JoinChannelBufferResponse), - (JoinChannelChat, JoinChannelChatResponse), - (JoinProject, JoinProjectResponse), - (JoinRoom, JoinRoomResponse), - (LeaveChannelBuffer, Ack), - (LeaveRoom, Ack), - (LoadCommitDiff, LoadCommitDiffResponse), - (MarkNotificationRead, Ack), - (MoveChannel, Ack), - (OnTypeFormatting, OnTypeFormattingResponse), - (OpenBufferById, OpenBufferResponse), - (OpenBufferByPath, OpenBufferResponse), - (OpenImageByPath, OpenImageResponse), - (OpenBufferForSymbol, OpenBufferForSymbolResponse), - (OpenCommitMessageBuffer, OpenBufferResponse), - (OpenNewBuffer, OpenBufferResponse), - (PerformRename, PerformRenameResponse), - (Ping, Ack), - (PrepareRename, PrepareRenameResponse), - (RefreshInlayHints, Ack), - (RefreshCodeLens, Ack), - (RejoinChannelBuffers, RejoinChannelBuffersResponse), - (RejoinRoom, RejoinRoomResponse), - (ReloadBuffers, ReloadBuffersResponse), - (RemoveChannelMember, Ack), - (RemoveChannelMessage, Ack), - (UpdateChannelMessage, Ack), - (RemoveContact, Ack), - (RenameChannel, RenameChannelResponse), - (RenameProjectEntry, ProjectEntryResponse), - (ReorderChannel, Ack), - (RequestContact, Ack), - ( - ResolveCompletionDocumentation, - ResolveCompletionDocumentationResponse - ), - (ResolveInlayHint, ResolveInlayHintResponse), - (GetDocumentColor, GetDocumentColorResponse), - (GetColorPresentation, GetColorPresentationResponse), - (RespondToChannelInvite, Ack), - (RespondToContactRequest, Ack), - (SaveBuffer, BufferSaved), - (Stage, Ack), - (FindSearchCandidates, FindSearchCandidatesResponse), - (SendChannelMessage, SendChannelMessageResponse), - (SetChannelMemberRole, Ack), - (SetChannelVisibility, Ack), - (ShareProject, ShareProjectResponse), - (SynchronizeBuffers, SynchronizeBuffersResponse), - (TaskContextForLocation, TaskContext), - (Test, Test), - (Unstage, Ack), - (Stash, Ack), - (StashPop, Ack), - (StashApply, Ack), - (StashDrop, Ack), - (UpdateBuffer, Ack), - (UpdateParticipantLocation, Ack), - (UpdateProject, Ack), - (UpdateWorktree, Ack), - (UpdateRepository, Ack), - (RemoveRepository, Ack), - (LspExtExpandMacro, LspExtExpandMacroResponse), - (LspExtOpenDocs, LspExtOpenDocsResponse), - (LspExtRunnables, LspExtRunnablesResponse), - (SetRoomParticipantRole, Ack), - (BlameBuffer, BlameBufferResponse), - (RejoinRemoteProjects, RejoinRemoteProjectsResponse), - (LspQuery, Ack), - (LspQueryResponse, Ack), - (RestartLanguageServers, Ack), - (StopLanguageServers, Ack), - (OpenContext, OpenContextResponse), - (CreateContext, CreateContextResponse), - (SynchronizeContexts, SynchronizeContextsResponse), - (LspExtSwitchSourceHeader, LspExtSwitchSourceHeaderResponse), - (LspExtGoToParentModule, LspExtGoToParentModuleResponse), - (LspExtCancelFlycheck, Ack), - (LspExtRunFlycheck, Ack), - (LspExtClearFlycheck, Ack), - (AddWorktree, AddWorktreeResponse), - (ShutdownRemoteServer, Ack), - (RemoveWorktree, Ack), - (OpenServerSettings, OpenBufferResponse), - (GetPermalinkToLine, GetPermalinkToLineResponse), - (FlushBufferedMessages, Ack), - (LanguageServerPromptRequest, LanguageServerPromptResponse), - (GitGetBranches, GitBranchesResponse), - (UpdateGitBranch, Ack), - (ListToolchains, ListToolchainsResponse), - (ActivateToolchain, Ack), - (ActiveToolchain, ActiveToolchainResponse), - (ResolveToolchain, ResolveToolchainResponse), - (GetPathMetadata, GetPathMetadataResponse), - (GetCrashFiles, GetCrashFilesResponse), - (CancelLanguageServerWork, Ack), - (SyncExtensions, SyncExtensionsResponse), - (InstallExtension, Ack), - (RegisterBufferWithLanguageServers, Ack), - (GitShow, GitCommitDetails), - (GitFileHistory, GitFileHistoryResponse), - (GitReset, Ack), - (GitDeleteBranch, Ack), - (GitCheckoutFiles, Ack), - (SetIndexText, Ack), - (Push, RemoteMessageResponse), - (Fetch, RemoteMessageResponse), - (GetRemotes, GetRemotesResponse), - (Pull, RemoteMessageResponse), - (AskPassRequest, AskPassResponse), - (GitCreateRemote, Ack), - (GitRemoveRemote, Ack), - (GitCreateBranch, Ack), - (GitChangeBranch, Ack), - (GitRenameBranch, Ack), - (CheckForPushedCommits, CheckForPushedCommitsResponse), - (GitDiff, GitDiffResponse), - (GitInit, Ack), - (ToggleBreakpoint, Ack), - (GetDebugAdapterBinary, DebugAdapterBinary), - (RunDebugLocators, DebugRequest), - (GetDocumentDiagnostics, GetDocumentDiagnosticsResponse), - (PullWorkspaceDiagnostics, Ack), - (GetDefaultBranch, GetDefaultBranchResponse), - (GetBlobContent, GetBlobContentResponse), - (GetTreeDiff, GetTreeDiffResponse), - (GitClone, GitCloneResponse), - (ToggleLspLogs, Ack), - (GetDirectoryEnvironment, DirectoryEnvironment), - (GetProcesses, GetProcessesResponse), - (GetAgentServerCommand, AgentServerCommand), - (RemoteStarted, Ack), - (GitGetWorktrees, GitWorktreesResponse), - (GitCreateWorktree, Ack) -); - -lsp_messages!( - (GetReferences, GetReferencesResponse, true), - (GetDocumentColor, GetDocumentColorResponse, true), - (GetHover, GetHoverResponse, true), - (GetCodeActions, GetCodeActionsResponse, true), - (GetSignatureHelp, GetSignatureHelpResponse, true), - (GetCodeLens, GetCodeLensResponse, true), - (GetDocumentDiagnostics, GetDocumentDiagnosticsResponse, true), - (GetDefinition, GetDefinitionResponse, true), - (GetDeclaration, GetDeclarationResponse, true), - (GetTypeDefinition, GetTypeDefinitionResponse, true), - (GetImplementation, GetImplementationResponse, true), - (InlayHints, InlayHintsResponse, false), -); - -entity_messages!( - {project_id, ShareProject}, - AddProjectCollaborator, - AddWorktree, - ApplyCodeAction, - ApplyCompletionAdditionalEdits, - BlameBuffer, - BufferReloaded, - BufferSaved, - CloseBuffer, - Commit, - RunGitHook, - GetColorPresentation, - CopyProjectEntry, - CreateBufferForPeer, - CreateImageForPeer, - CreateProjectEntry, - GetDocumentColor, - DeleteProjectEntry, - ExpandProjectEntry, - ExpandAllForProjectEntry, - FindSearchCandidates, - ApplyCodeActionKind, - FormatBuffers, - GetCodeActions, - GetCodeLens, - GetCompletions, - GetDefinition, - GetDeclaration, - GetImplementation, - GetDocumentHighlights, - GetDocumentSymbols, - GetHover, - GetProjectSymbols, - GetReferences, - GetSignatureHelp, - OpenUnstagedDiff, - OpenUncommittedDiff, - GetTypeDefinition, - InlayHints, - JoinProject, - LeaveProject, - LinkedEditingRange, - LoadCommitDiff, - LspQuery, - LspQueryResponse, - RestartLanguageServers, - StopLanguageServers, - OnTypeFormatting, - OpenNewBuffer, - OpenBufferById, - OpenBufferByPath, - OpenImageByPath, - OpenBufferForSymbol, - OpenCommitMessageBuffer, - PerformRename, - PrepareRename, - RefreshInlayHints, - RefreshCodeLens, - ReloadBuffers, - RemoveProjectCollaborator, - RenameProjectEntry, - ResolveCompletionDocumentation, - ResolveInlayHint, - SaveBuffer, - Stage, - StartLanguageServer, - SynchronizeBuffers, - TaskContextForLocation, - UnshareProject, - Unstage, - Stash, - StashPop, - StashApply, - StashDrop, - UpdateBuffer, - UpdateBufferFile, - UpdateDiagnosticSummary, - UpdateDiffBases, - UpdateLanguageServer, - UpdateProject, - UpdateProjectCollaborator, - UpdateWorktree, - UpdateRepository, - RemoveRepository, - UpdateWorktreeSettings, - UpdateUserSettings, - LspExtExpandMacro, - LspExtOpenDocs, - LspExtRunnables, - AdvertiseContexts, - OpenContext, - CreateContext, - UpdateContext, - SynchronizeContexts, - LspExtSwitchSourceHeader, - LspExtGoToParentModule, - LspExtCancelFlycheck, - LspExtRunFlycheck, - LspExtClearFlycheck, - LanguageServerLog, - Toast, - HideToast, - OpenServerSettings, - GetPermalinkToLine, - LanguageServerPromptRequest, - GitGetBranches, - UpdateGitBranch, - ListToolchains, - ActivateToolchain, - ActiveToolchain, - ResolveToolchain, - GetPathMetadata, - GetProcesses, - CancelLanguageServerWork, - RegisterBufferWithLanguageServers, - GitShow, - GitFileHistory, - GitReset, - GitDeleteBranch, - GitCheckoutFiles, - SetIndexText, - ToggleLspLogs, - GetDirectoryEnvironment, - - Push, - Fetch, - GetRemotes, - Pull, - AskPassRequest, - GitChangeBranch, - GitRenameBranch, - GitCreateBranch, - GitCreateRemote, - GitRemoveRemote, - CheckForPushedCommits, - GitDiff, - GitInit, - BreakpointsForFile, - ToggleBreakpoint, - RunDebugLocators, - GetDebugAdapterBinary, - LogToDebugConsole, - GetDocumentDiagnostics, - PullWorkspaceDiagnostics, - GetDefaultBranch, - GetTreeDiff, - GetBlobContent, - GitClone, - GetAgentServerCommand, - ExternalAgentsUpdated, - ExternalExtensionAgentsUpdated, - ExternalAgentLoadingStatusUpdated, - NewExternalAgentVersionAvailable, - GitGetWorktrees, - GitCreateWorktree -); - -entity_messages!( - {channel_id, Channel}, - ChannelMessageSent, - ChannelMessageUpdate, - RemoveChannelMessage, - UpdateChannelMessage, - UpdateChannelBuffer, - UpdateChannelBufferCollaborators, -); - -impl From for SystemTime { - fn from(val: Timestamp) -> Self { - UNIX_EPOCH - .checked_add(Duration::new(val.seconds, val.nanos)) - .unwrap() - } -} - -impl From for Timestamp { - fn from(time: SystemTime) -> Self { - let duration = time.duration_since(UNIX_EPOCH).unwrap_or_default(); - Self { - seconds: duration.as_secs(), - nanos: duration.subsec_nanos(), - } - } -} - -impl From for Nonce { - fn from(nonce: u128) -> Self { - let upper_half = (nonce >> 64) as u64; - let lower_half = nonce as u64; - Self { - upper_half, - lower_half, - } - } -} - -impl From for u128 { - fn from(nonce: Nonce) -> Self { - let upper_half = (nonce.upper_half as u128) << 64; - let lower_half = nonce.lower_half as u128; - upper_half | lower_half - } -} - -#[cfg(any(test, feature = "test-support"))] -pub const MAX_WORKTREE_UPDATE_MAX_CHUNK_SIZE: usize = 2; -#[cfg(not(any(test, feature = "test-support")))] -pub const MAX_WORKTREE_UPDATE_MAX_CHUNK_SIZE: usize = 256; - -pub fn split_worktree_update(mut message: UpdateWorktree) -> impl Iterator { - let mut done = false; - - iter::from_fn(move || { - if done { - return None; - } - - let updated_entries_chunk_size = cmp::min( - message.updated_entries.len(), - MAX_WORKTREE_UPDATE_MAX_CHUNK_SIZE, - ); - let updated_entries: Vec<_> = message - .updated_entries - .drain(..updated_entries_chunk_size) - .collect(); - - let removed_entries_chunk_size = cmp::min( - message.removed_entries.len(), - MAX_WORKTREE_UPDATE_MAX_CHUNK_SIZE, - ); - let removed_entries = message - .removed_entries - .drain(..removed_entries_chunk_size) - .collect(); - - let mut updated_repositories = Vec::new(); - let mut limit = MAX_WORKTREE_UPDATE_MAX_CHUNK_SIZE; - while let Some(repo) = message.updated_repositories.first_mut() { - let updated_statuses_limit = cmp::min(repo.updated_statuses.len(), limit); - let removed_statuses_limit = cmp::min(repo.removed_statuses.len(), limit); - - updated_repositories.push(RepositoryEntry { - repository_id: repo.repository_id, - branch_summary: repo.branch_summary.clone(), - updated_statuses: repo - .updated_statuses - .drain(..updated_statuses_limit) - .collect(), - removed_statuses: repo - .removed_statuses - .drain(..removed_statuses_limit) - .collect(), - current_merge_conflicts: repo.current_merge_conflicts.clone(), - }); - if repo.removed_statuses.is_empty() && repo.updated_statuses.is_empty() { - message.updated_repositories.remove(0); - } - limit = limit.saturating_sub(removed_statuses_limit + updated_statuses_limit); - if limit == 0 { - break; - } - } - - done = message.updated_entries.is_empty() - && message.removed_entries.is_empty() - && message.updated_repositories.is_empty(); - - let removed_repositories = if done { - mem::take(&mut message.removed_repositories) - } else { - Default::default() - }; - - Some(UpdateWorktree { - project_id: message.project_id, - worktree_id: message.worktree_id, - root_name: message.root_name.clone(), - abs_path: message.abs_path.clone(), - updated_entries, - removed_entries, - scan_id: message.scan_id, - is_last_update: done && message.is_last_update, - updated_repositories, - removed_repositories, - }) - }) -} - -pub fn split_repository_update( - mut update: UpdateRepository, -) -> impl Iterator { - let mut updated_statuses_iter = mem::take(&mut update.updated_statuses).into_iter().fuse(); - let mut removed_statuses_iter = mem::take(&mut update.removed_statuses).into_iter().fuse(); - std::iter::from_fn({ - let update = update.clone(); - move || { - let updated_statuses = updated_statuses_iter - .by_ref() - .take(MAX_WORKTREE_UPDATE_MAX_CHUNK_SIZE) - .collect::>(); - let removed_statuses = removed_statuses_iter - .by_ref() - .take(MAX_WORKTREE_UPDATE_MAX_CHUNK_SIZE) - .collect::>(); - if updated_statuses.is_empty() && removed_statuses.is_empty() { - return None; - } - Some(UpdateRepository { - updated_statuses, - removed_statuses, - is_last_update: false, - ..update.clone() - }) - } - }) - .chain([UpdateRepository { - updated_statuses: Vec::new(), - removed_statuses: Vec::new(), - is_last_update: true, - ..update - }]) -} - -impl LspQuery { - pub fn query_name_and_write_permissions(&self) -> (&str, bool) { - match self.request { - Some(lsp_query::Request::GetHover(_)) => ("GetHover", false), - Some(lsp_query::Request::GetCodeActions(_)) => ("GetCodeActions", true), - Some(lsp_query::Request::GetSignatureHelp(_)) => ("GetSignatureHelp", false), - Some(lsp_query::Request::GetCodeLens(_)) => ("GetCodeLens", true), - Some(lsp_query::Request::GetDocumentDiagnostics(_)) => { - ("GetDocumentDiagnostics", false) - } - Some(lsp_query::Request::GetDefinition(_)) => ("GetDefinition", false), - Some(lsp_query::Request::GetDeclaration(_)) => ("GetDeclaration", false), - Some(lsp_query::Request::GetTypeDefinition(_)) => ("GetTypeDefinition", false), - Some(lsp_query::Request::GetImplementation(_)) => ("GetImplementation", false), - Some(lsp_query::Request::GetReferences(_)) => ("GetReferences", false), - Some(lsp_query::Request::GetDocumentColor(_)) => ("GetDocumentColor", false), - Some(lsp_query::Request::InlayHints(_)) => ("InlayHints", false), - None => ("", true), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_converting_peer_id_from_and_to_u64() { - let peer_id = PeerId { - owner_id: 10, - id: 3, - }; - assert_eq!(PeerId::from_u64(peer_id.as_u64()), peer_id); - let peer_id = PeerId { - owner_id: u32::MAX, - id: 3, - }; - assert_eq!(PeerId::from_u64(peer_id.as_u64()), peer_id); - let peer_id = PeerId { - owner_id: 10, - id: u32::MAX, - }; - assert_eq!(PeerId::from_u64(peer_id.as_u64()), peer_id); - let peer_id = PeerId { - owner_id: u32::MAX, - id: u32::MAX, - }; - assert_eq!(PeerId::from_u64(peer_id.as_u64()), peer_id); - } -} diff --git a/crates/proto/src/typed_envelope.rs b/crates/proto/src/typed_envelope.rs deleted file mode 100644 index a7a8a1c7a2..0000000000 --- a/crates/proto/src/typed_envelope.rs +++ /dev/null @@ -1,210 +0,0 @@ -use crate::{Envelope, PeerId}; -use anyhow::{Context as _, Result}; -use serde::Serialize; -use std::{ - any::{Any, TypeId}, - cmp, - fmt::{self, Debug}, -}; -use std::{marker::PhantomData, time::Instant}; - -pub trait EnvelopedMessage: Clone + Debug + Serialize + Sized + Send + Sync + 'static { - const NAME: &'static str; - const PRIORITY: MessagePriority; - fn into_envelope( - self, - id: u32, - responding_to: Option, - original_sender_id: Option, - ) -> Envelope; - fn from_envelope(envelope: Envelope) -> Option; -} - -pub trait EntityMessage: EnvelopedMessage { - type Entity; - fn remote_entity_id(&self) -> u64; -} - -pub trait RequestMessage: EnvelopedMessage { - type Response: EnvelopedMessage; -} - -/// A trait to bind LSP request and responses for the proto layer. -/// Should be used for every LSP request that has to traverse through the proto layer. -/// -/// `lsp_messages` macro in the same crate provides a convenient way to implement this. -pub trait LspRequestMessage: EnvelopedMessage { - type Response: EnvelopedMessage; - - fn to_proto_query(self) -> crate::lsp_query::Request; - - fn response_to_proto_query(response: Self::Response) -> crate::lsp_response::Response; - - fn buffer_id(&self) -> u64; - - fn buffer_version(&self) -> &[crate::VectorClockEntry]; - - /// Whether to deduplicate the requests, or keep the previous ones running when another - /// request of the same kind is processed. - fn stop_previous_requests() -> bool; -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct LspRequestId(pub u64); - -/// A response from a single language server. -/// There could be multiple responses for a single LSP request, -/// from different servers. -pub struct ProtoLspResponse { - pub server_id: u64, - pub response: R, -} - -impl ProtoLspResponse> { - pub fn into_response(self) -> Result> { - let envelope = self - .response - .into_any() - .downcast::>() - .map_err(|_| { - anyhow::anyhow!( - "cannot downcast LspResponse to {} for message {}", - T::Response::NAME, - T::NAME, - ) - })?; - - Ok(ProtoLspResponse { - server_id: self.server_id, - response: envelope.payload, - }) - } -} - -pub trait AnyTypedEnvelope: Any + Send + Sync { - fn payload_type_id(&self) -> TypeId; - fn payload_type_name(&self) -> &'static str; - fn into_any(self: Box) -> Box; - fn is_background(&self) -> bool; - fn original_sender_id(&self) -> Option; - fn sender_id(&self) -> PeerId; - fn message_id(&self) -> u32; -} - -pub enum MessagePriority { - Foreground, - Background, -} - -impl AnyTypedEnvelope for TypedEnvelope { - fn payload_type_id(&self) -> TypeId { - TypeId::of::() - } - - fn payload_type_name(&self) -> &'static str { - T::NAME - } - - fn into_any(self: Box) -> Box { - self - } - - fn is_background(&self) -> bool { - matches!(T::PRIORITY, MessagePriority::Background) - } - - fn original_sender_id(&self) -> Option { - self.original_sender_id - } - - fn sender_id(&self) -> PeerId { - self.sender_id - } - - fn message_id(&self) -> u32 { - self.message_id - } -} - -impl PeerId { - pub fn from_u64(peer_id: u64) -> Self { - let owner_id = (peer_id >> 32) as u32; - let id = peer_id as u32; - Self { owner_id, id } - } - - pub fn as_u64(self) -> u64 { - ((self.owner_id as u64) << 32) | (self.id as u64) - } -} - -impl Copy for PeerId {} - -impl Eq for PeerId {} - -impl Ord for PeerId { - fn cmp(&self, other: &Self) -> cmp::Ordering { - self.owner_id - .cmp(&other.owner_id) - .then_with(|| self.id.cmp(&other.id)) - } -} - -impl PartialOrd for PeerId { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl std::hash::Hash for PeerId { - fn hash(&self, state: &mut H) { - self.owner_id.hash(state); - self.id.hash(state); - } -} - -impl fmt::Display for PeerId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}/{}", self.owner_id, self.id) - } -} - -pub struct Receipt { - pub sender_id: PeerId, - pub message_id: u32, - payload_type: PhantomData, -} - -impl Clone for Receipt { - fn clone(&self) -> Self { - *self - } -} - -impl Copy for Receipt {} - -#[derive(Clone, Debug)] -pub struct TypedEnvelope { - pub sender_id: PeerId, - pub original_sender_id: Option, - pub message_id: u32, - pub payload: T, - pub received_at: Instant, -} - -impl TypedEnvelope { - pub fn original_sender_id(&self) -> Result { - self.original_sender_id - .context("missing original_sender_id") - } -} - -impl TypedEnvelope { - pub fn receipt(&self) -> Receipt { - Receipt { - sender_id: self.sender_id, - message_id: self.message_id, - payload_type: PhantomData, - } - } -} diff --git a/crates/recent_projects/Cargo.toml b/crates/recent_projects/Cargo.toml deleted file mode 100644 index feaf511b81..0000000000 --- a/crates/recent_projects/Cargo.toml +++ /dev/null @@ -1,62 +0,0 @@ -[package] -name = "recent_projects" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/recent_projects.rs" -doctest = false - -[dependencies] -anyhow.workspace = true -askpass.workspace = true -auto_update.workspace = true -db.workspace = true -editor.workspace = true -extension_host.workspace = true -file_finder.workspace = true -futures.workspace = true -fuzzy.workspace = true -gpui.workspace = true -language.workspace = true -log.workspace = true -markdown.workspace = true -menu.workspace = true -node_runtime.workspace = true -ordered-float.workspace = true -paths.workspace = true -picker.workspace = true -project.workspace = true -release_channel.workspace = true -remote.workspace = true -semver.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -smol.workspace = true -task.workspace = true -telemetry.workspace = true -theme.workspace = true -ui.workspace = true -util.workspace = true -workspace.workspace = true -worktree.workspace = true -zed_actions.workspace = true -indoc.workspace = true - -[target.'cfg(target_os = "windows")'.dependencies] -windows-registry = "0.6.0" - -[dev-dependencies] -dap.workspace = true -editor = { workspace = true, features = ["test-support"] } -language = { workspace = true, features = ["test-support"] } -project = { workspace = true, features = ["test-support"] } -serde_json.workspace = true -settings = { workspace = true, features = ["test-support"] } -workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/recent_projects/LICENSE-GPL b/crates/recent_projects/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/recent_projects/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/recent_projects/src/dev_container.rs b/crates/recent_projects/src/dev_container.rs deleted file mode 100644 index 0e6b8b381d..0000000000 --- a/crates/recent_projects/src/dev_container.rs +++ /dev/null @@ -1,295 +0,0 @@ -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -use gpui::AsyncWindowContext; -use node_runtime::NodeRuntime; -use serde::Deserialize; -use settings::DevContainerConnection; -use smol::fs; -use workspace::Workspace; - -use crate::remote_connections::Connection; - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct DevContainerUp { - _outcome: String, - container_id: String, - _remote_user: String, - remote_workspace_folder: String, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -struct DevContainerConfiguration { - name: Option, -} - -#[derive(Debug, Deserialize)] -struct DevContainerConfigurationOutput { - configuration: DevContainerConfiguration, -} - -#[cfg(not(target_os = "windows"))] -fn dev_container_cli() -> String { - "devcontainer".to_string() -} - -#[cfg(target_os = "windows")] -fn dev_container_cli() -> String { - "devcontainer.cmd".to_string() -} - -async fn check_for_docker() -> Result<(), DevContainerError> { - let mut command = util::command::new_smol_command("docker"); - command.arg("--version"); - - match command.output().await { - Ok(_) => Ok(()), - Err(e) => { - log::error!("Unable to find docker in $PATH: {:?}", e); - Err(DevContainerError::DockerNotAvailable) - } - } -} - -async fn ensure_devcontainer_cli(node_runtime: NodeRuntime) -> Result { - let mut command = util::command::new_smol_command(&dev_container_cli()); - command.arg("--version"); - - if let Err(e) = command.output().await { - log::error!( - "Unable to find devcontainer CLI in $PATH. Checking for a zed installed version. Error: {:?}", - e - ); - - let datadir_cli_path = paths::devcontainer_dir() - .join("node_modules") - .join(".bin") - .join(&dev_container_cli()); - - let mut command = - util::command::new_smol_command(&datadir_cli_path.as_os_str().display().to_string()); - command.arg("--version"); - - if let Err(e) = command.output().await { - log::error!( - "Unable to find devcontainer CLI in Data dir. Will try to install. Error: {:?}", - e - ); - } else { - log::info!("Found devcontainer CLI in Data dir"); - return Ok(datadir_cli_path.clone()); - } - - if let Err(e) = fs::create_dir_all(paths::devcontainer_dir()).await { - log::error!("Unable to create devcontainer directory. Error: {:?}", e); - return Err(DevContainerError::DevContainerCliNotAvailable); - } - - if let Err(e) = node_runtime - .npm_install_packages( - &paths::devcontainer_dir(), - &[("@devcontainers/cli", "latest")], - ) - .await - { - log::error!( - "Unable to install devcontainer CLI to data directory. Error: {:?}", - e - ); - return Err(DevContainerError::DevContainerCliNotAvailable); - }; - - let mut command = util::command::new_smol_command(&datadir_cli_path.display().to_string()); - command.arg("--version"); - if let Err(e) = command.output().await { - log::error!( - "Unable to find devcontainer cli after NPM install. Error: {:?}", - e - ); - Err(DevContainerError::DevContainerCliNotAvailable) - } else { - Ok(datadir_cli_path) - } - } else { - log::info!("Found devcontainer cli on $PATH, using it"); - Ok(PathBuf::from(&dev_container_cli())) - } -} - -async fn devcontainer_up( - path_to_cli: &PathBuf, - path: Arc, -) -> Result { - let mut command = util::command::new_smol_command(path_to_cli.display().to_string()); - command.arg("up"); - command.arg("--workspace-folder"); - command.arg(path.display().to_string()); - - match command.output().await { - Ok(output) => { - if output.status.success() { - let raw = String::from_utf8_lossy(&output.stdout); - serde_json::from_str::(&raw).map_err(|e| { - log::error!( - "Unable to parse response from 'devcontainer up' command, error: {:?}", - e - ); - DevContainerError::DevContainerParseFailed - }) - } else { - log::error!( - "Non-success status running devcontainer up for workspace: out: {:?}, err: {:?}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - Err(DevContainerError::DevContainerUpFailed) - } - } - Err(e) => { - log::error!("Error running devcontainer up: {:?}", e); - Err(DevContainerError::DevContainerUpFailed) - } - } -} - -async fn devcontainer_read_configuration( - path_to_cli: &PathBuf, - path: Arc, -) -> Result { - let mut command = util::command::new_smol_command(path_to_cli.display().to_string()); - command.arg("read-configuration"); - command.arg("--workspace-folder"); - command.arg(path.display().to_string()); - match command.output().await { - Ok(output) => { - if output.status.success() { - let raw = String::from_utf8_lossy(&output.stdout); - serde_json::from_str::(&raw).map_err(|e| { - log::error!( - "Unable to parse response from 'devcontainer read-configuration' command, error: {:?}", - e - ); - DevContainerError::DevContainerParseFailed - }) - } else { - log::error!( - "Non-success status running devcontainer read-configuration for workspace: out: {:?}, err: {:?}", - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr) - ); - Err(DevContainerError::DevContainerUpFailed) - } - } - Err(e) => { - log::error!("Error running devcontainer read-configuration: {:?}", e); - Err(DevContainerError::DevContainerUpFailed) - } - } -} - -// Name the project with two fallbacks -async fn get_project_name( - path_to_cli: &PathBuf, - path: Arc, - remote_workspace_folder: String, - container_id: String, -) -> Result { - if let Ok(dev_container_configuration) = - devcontainer_read_configuration(path_to_cli, path).await - && let Some(name) = dev_container_configuration.configuration.name - { - // Ideally, name the project after the name defined in devcontainer.json - Ok(name) - } else { - // Otherwise, name the project after the remote workspace folder name - Ok(Path::new(&remote_workspace_folder) - .file_name() - .and_then(|name| name.to_str()) - .map(|string| string.into()) - // Finally, name the project after the container ID as a last resort - .unwrap_or_else(|| container_id.clone())) - } -} - -fn project_directory(cx: &mut AsyncWindowContext) -> Option> { - let Some(workspace) = cx.window_handle().downcast::() else { - return None; - }; - - match workspace.update(cx, |workspace, _, cx| { - workspace.project().read(cx).active_project_directory(cx) - }) { - Ok(dir) => dir, - Err(e) => { - log::error!("Error getting project directory from workspace: {:?}", e); - None - } - } -} - -pub(crate) async fn start_dev_container( - cx: &mut AsyncWindowContext, - node_runtime: NodeRuntime, -) -> Result<(Connection, String), DevContainerError> { - check_for_docker().await?; - - let path_to_devcontainer_cli = ensure_devcontainer_cli(node_runtime).await?; - - let Some(directory) = project_directory(cx) else { - return Err(DevContainerError::DevContainerNotFound); - }; - - if let Ok(DevContainerUp { - container_id, - remote_workspace_folder, - .. - }) = devcontainer_up(&path_to_devcontainer_cli, directory.clone()).await - { - let project_name = get_project_name( - &path_to_devcontainer_cli, - directory, - remote_workspace_folder.clone(), - container_id.clone(), - ) - .await?; - - let connection = Connection::DevContainer(DevContainerConnection { - name: project_name.into(), - container_id: container_id.into(), - }); - - Ok((connection, remote_workspace_folder)) - } else { - Err(DevContainerError::DevContainerUpFailed) - } -} - -#[derive(Debug)] -pub(crate) enum DevContainerError { - DockerNotAvailable, - DevContainerCliNotAvailable, - DevContainerUpFailed, - DevContainerNotFound, - DevContainerParseFailed, -} - -#[cfg(test)] -mod test { - - use crate::dev_container::DevContainerUp; - - #[test] - fn should_parse_from_devcontainer_json() { - let json = r#"{"outcome":"success","containerId":"826abcac45afd412abff083ab30793daff2f3c8ce2c831df728baf39933cb37a","remoteUser":"vscode","remoteWorkspaceFolder":"/workspaces/zed"}"#; - let up: DevContainerUp = serde_json::from_str(json).unwrap(); - assert_eq!(up._outcome, "success"); - assert_eq!( - up.container_id, - "826abcac45afd412abff083ab30793daff2f3c8ce2c831df728baf39933cb37a" - ); - assert_eq!(up._remote_user, "vscode"); - assert_eq!(up.remote_workspace_folder, "/workspaces/zed"); - } -} diff --git a/crates/recent_projects/src/dev_container_suggest.rs b/crates/recent_projects/src/dev_container_suggest.rs deleted file mode 100644 index 1e50080ea1..0000000000 --- a/crates/recent_projects/src/dev_container_suggest.rs +++ /dev/null @@ -1,106 +0,0 @@ -use db::kvp::KEY_VALUE_STORE; -use gpui::{SharedString, Window}; -use project::{Project, WorktreeId}; -use std::sync::LazyLock; -use ui::prelude::*; -use util::rel_path::RelPath; -use workspace::Workspace; -use workspace::notifications::NotificationId; -use workspace::notifications::simple_message_notification::MessageNotification; -use worktree::UpdatedEntriesSet; - -const DEV_CONTAINER_SUGGEST_KEY: &str = "dev_container_suggest_dismissed"; - -fn devcontainer_path() -> &'static RelPath { - static PATH: LazyLock<&'static RelPath> = - LazyLock::new(|| RelPath::unix(".devcontainer").expect("valid path")); - *PATH -} - -fn project_devcontainer_key(project_path: &str) -> String { - format!("{}_{}", DEV_CONTAINER_SUGGEST_KEY, project_path) -} - -pub fn suggest_on_worktree_updated( - worktree_id: WorktreeId, - updated_entries: &UpdatedEntriesSet, - project: &gpui::Entity, - window: &mut Window, - cx: &mut Context, -) { - let devcontainer_updated = updated_entries - .iter() - .any(|(path, _, _)| path.as_ref() == devcontainer_path()); - - if !devcontainer_updated { - return; - } - - let Some(worktree) = project.read(cx).worktree_for_id(worktree_id, cx) else { - return; - }; - - let worktree = worktree.read(cx); - - if !worktree.is_local() { - return; - } - - let has_devcontainer = worktree - .entry_for_path(devcontainer_path()) - .is_some_and(|entry| entry.is_dir()); - - if !has_devcontainer { - return; - } - - let abs_path = worktree.abs_path(); - let project_path = abs_path.to_string_lossy().to_string(); - let key_for_dismiss = project_devcontainer_key(&project_path); - - let already_dismissed = KEY_VALUE_STORE - .read_kvp(&key_for_dismiss) - .ok() - .flatten() - .is_some(); - - if already_dismissed { - return; - } - - cx.on_next_frame(window, move |workspace, _window, cx| { - struct DevContainerSuggestionNotification; - - let notification_id = NotificationId::composite::( - SharedString::from(project_path.clone()), - ); - - workspace.show_notification(notification_id, cx, |cx| { - cx.new(move |cx| { - MessageNotification::new( - "This project contains a Dev Container configuration file. Would you like to re-open it in a container?", - cx, - ) - .primary_message("Yes, Open in Container") - .primary_icon(IconName::Check) - .primary_icon_color(Color::Success) - .primary_on_click({ - move |window, cx| { - window.dispatch_action(Box::new(zed_actions::OpenDevContainer), cx); - } - }) - .secondary_message("Don't Show Again") - .secondary_icon(IconName::Close) - .secondary_icon_color(Color::Error) - .secondary_on_click({ - move |_window, cx| { - let key = key_for_dismiss.clone(); - db::write_and_log(cx, move || { - KEY_VALUE_STORE.write_kvp(key, "dismissed".to_string()) - }); - } - }) - }) - }); - }); -} diff --git a/crates/recent_projects/src/disconnected_overlay.rs b/crates/recent_projects/src/disconnected_overlay.rs deleted file mode 100644 index c97f7062a8..0000000000 --- a/crates/recent_projects/src/disconnected_overlay.rs +++ /dev/null @@ -1,205 +0,0 @@ -use gpui::{ClickEvent, DismissEvent, EventEmitter, FocusHandle, Focusable, Render, WeakEntity}; -use project::project_settings::ProjectSettings; -use remote::RemoteConnectionOptions; -use settings::Settings; -use ui::{ - Button, ButtonCommon, ButtonStyle, Clickable, Context, ElevationIndex, FluentBuilder, Headline, - HeadlineSize, IconName, IconPosition, InteractiveElement, IntoElement, Label, Modal, - ModalFooter, ModalHeader, ParentElement, Section, Styled, StyledExt, Window, div, h_flex, rems, -}; -use workspace::{ModalView, OpenOptions, Workspace, notifications::DetachAndPromptErr}; - -use crate::open_remote_project; - -enum Host { - CollabGuestProject, - RemoteServerProject(RemoteConnectionOptions), -} - -pub struct DisconnectedOverlay { - workspace: WeakEntity, - host: Host, - focus_handle: FocusHandle, - finished: bool, -} - -impl EventEmitter for DisconnectedOverlay {} -impl Focusable for DisconnectedOverlay { - fn focus_handle(&self, _cx: &gpui::App) -> gpui::FocusHandle { - self.focus_handle.clone() - } -} -impl ModalView for DisconnectedOverlay { - fn on_before_dismiss( - &mut self, - _window: &mut Window, - _: &mut Context, - ) -> workspace::DismissDecision { - workspace::DismissDecision::Dismiss(self.finished) - } - fn fade_out_background(&self) -> bool { - true - } -} - -impl DisconnectedOverlay { - pub fn register( - workspace: &mut Workspace, - window: Option<&mut Window>, - cx: &mut Context, - ) { - let Some(window) = window else { - return; - }; - cx.subscribe_in( - workspace.project(), - window, - |workspace, project, event, window, cx| { - if !matches!( - event, - project::Event::DisconnectedFromHost - | project::Event::DisconnectedFromSshRemote - ) { - return; - } - let handle = cx.entity().downgrade(); - - let remote_connection_options = project.read(cx).remote_connection_options(cx); - let host = if let Some(ssh_connection_options) = remote_connection_options { - Host::RemoteServerProject(ssh_connection_options) - } else { - Host::CollabGuestProject - }; - - workspace.toggle_modal(window, cx, |_, cx| DisconnectedOverlay { - finished: false, - workspace: handle, - host, - focus_handle: cx.focus_handle(), - }); - }, - ) - .detach(); - } - - fn handle_reconnect(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { - self.finished = true; - cx.emit(DismissEvent); - - if let Host::RemoteServerProject(ssh_connection_options) = &self.host { - self.reconnect_to_remote_project(ssh_connection_options.clone(), window, cx); - } - } - - fn reconnect_to_remote_project( - &self, - connection_options: RemoteConnectionOptions, - window: &mut Window, - cx: &mut Context, - ) { - let Some(workspace) = self.workspace.upgrade() 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(); - - cx.spawn_in(window, async move |_, cx| { - open_remote_project( - connection_options, - paths, - app_state, - OpenOptions { - replace_window: Some(window_handle), - ..Default::default() - }, - cx, - ) - .await?; - Ok(()) - }) - .detach_and_prompt_err("Failed to reconnect", window, cx, |_, _, _| None); - } - - fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { - self.finished = true; - cx.emit(DismissEvent) - } -} - -impl Render for DisconnectedOverlay { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let can_reconnect = matches!(self.host, Host::RemoteServerProject(_)); - - let message = match &self.host { - Host::CollabGuestProject => { - "Your connection to the remote project has been lost.".to_string() - } - Host::RemoteServerProject(options) => { - let autosave = if ProjectSettings::get_global(cx) - .session - .restore_unsaved_buffers - { - "\nUnsaved changes are stored locally." - } else { - "" - }; - format!( - "Your connection to {} has been lost.{}", - options.display_name(), - autosave - ) - } - }; - - div() - .track_focus(&self.focus_handle(cx)) - .elevation_3(cx) - .on_action(cx.listener(Self::cancel)) - .occlude() - .w(rems(24.)) - .max_h(rems(40.)) - .child( - Modal::new("disconnected", None) - .header( - ModalHeader::new() - .show_dismiss_button(true) - .child(Headline::new("Disconnected").size(HeadlineSize::Small)), - ) - .section(Section::new().child(Label::new(message))) - .footer( - ModalFooter::new().end_slot( - h_flex() - .gap_2() - .child( - Button::new("close-window", "Close Window") - .style(ButtonStyle::Filled) - .layer(ElevationIndex::ModalSurface) - .on_click(cx.listener(move |_, _, window, _| { - window.remove_window(); - })), - ) - .when(can_reconnect, |el| { - el.child( - Button::new("reconnect", "Reconnect") - .style(ButtonStyle::Filled) - .layer(ElevationIndex::ModalSurface) - .icon(IconName::ArrowCircle) - .icon_position(IconPosition::Start) - .on_click(cx.listener(Self::handle_reconnect)), - ) - }), - ), - ), - ) - } -} diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs deleted file mode 100644 index 435933a880..0000000000 --- a/crates/recent_projects/src/recent_projects.rs +++ /dev/null @@ -1,1043 +0,0 @@ -mod dev_container; -mod dev_container_suggest; -pub mod disconnected_overlay; -mod remote_connections; -mod remote_servers; -mod ssh_config; - -use std::path::PathBuf; - -#[cfg(target_os = "windows")] -mod wsl_picker; - -use remote::RemoteConnectionOptions; -pub use remote_connections::{RemoteConnectionModal, connect, open_remote_project}; - -use disconnected_overlay::DisconnectedOverlay; -use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{ - Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, - Subscription, Task, WeakEntity, Window, -}; -use ordered_float::OrderedFloat; -use picker::{ - Picker, PickerDelegate, - highlighted_match_with_paths::{HighlightedMatch, HighlightedMatchWithPaths}, -}; -pub use remote_connections::SshSettings; -pub use remote_servers::RemoteServerProjects; -use settings::Settings; -use std::{path::Path, 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, notifications::DetachAndPromptErr, - with_active_or_new_workspace, -}; -use zed_actions::{OpenDevContainer, OpenRecent, OpenRemote}; - -pub fn init(cx: &mut App) { - #[cfg(target_os = "windows")] - cx.on_action(|open_wsl: &zed_actions::wsl_actions::OpenFolderInWsl, cx| { - let create_new_window = open_wsl.create_new_window; - with_active_or_new_workspace(cx, move |workspace, window, cx| { - use gpui::PathPromptOptions; - use project::DirectoryLister; - - let paths = workspace.prompt_for_open_path( - PathPromptOptions { - files: true, - directories: true, - multiple: false, - prompt: None, - }, - DirectoryLister::Local( - workspace.project().clone(), - workspace.app_state().fs.clone(), - ), - window, - cx, - ); - - cx.spawn_in(window, async move |workspace, cx| { - use util::paths::SanitizedPath; - - let Some(paths) = paths.await.log_err().flatten() else { - return; - }; - - let paths = paths - .into_iter() - .filter_map(|path| SanitizedPath::new(&path).local_to_wsl()) - .collect::>(); - - if paths.is_empty() { - let message = indoc::indoc! { r#" - Invalid path specified when trying to open a folder inside WSL. - - Please note that Zed currently does not support opening network share folders inside wsl. - "#}; - - let _ = cx.prompt(gpui::PromptLevel::Critical, "Invalid path", Some(&message), &["Ok"]).await; - return; - } - - workspace.update_in(cx, |workspace, window, cx| { - workspace.toggle_modal(window, cx, |window, cx| { - crate::wsl_picker::WslOpenModal::new(paths, create_new_window, window, cx) - }); - }).log_err(); - }) - .detach(); - }); - }); - - #[cfg(target_os = "windows")] - cx.on_action(|open_wsl: &zed_actions::wsl_actions::OpenWsl, cx| { - let create_new_window = open_wsl.create_new_window; - with_active_or_new_workspace(cx, move |workspace, window, cx| { - let handle = cx.entity().downgrade(); - let fs = workspace.project().read(cx).fs().clone(); - workspace.toggle_modal(window, cx, |window, cx| { - RemoteServerProjects::wsl(create_new_window, fs, window, handle, cx) - }); - }); - }); - - #[cfg(target_os = "windows")] - cx.on_action(|open_wsl: &remote::OpenWslPath, cx| { - let open_wsl = open_wsl.clone(); - with_active_or_new_workspace(cx, move |workspace, window, cx| { - let fs = workspace.project().read(cx).fs().clone(); - add_wsl_distro(fs, &open_wsl.distro, cx); - let open_options = OpenOptions { - replace_window: window.window_handle().downcast::(), - ..Default::default() - }; - - let app_state = workspace.app_state().clone(); - - cx.spawn_in(window, async move |_, cx| { - open_remote_project( - RemoteConnectionOptions::Wsl(open_wsl.distro.clone()), - open_wsl.paths, - app_state, - open_options, - cx, - ) - .await - }) - .detach(); - }); - }); - - cx.on_action(|open_recent: &OpenRecent, cx| { - let create_new_window = open_recent.create_new_window; - with_active_or_new_workspace(cx, move |workspace, window, cx| { - let Some(recent_projects) = workspace.active_modal::(cx) else { - let focus_handle = workspace.focus_handle(cx); - RecentProjects::open(workspace, create_new_window, window, focus_handle, cx); - return; - }; - - recent_projects.update(cx, |recent_projects, cx| { - recent_projects - .picker - .update(cx, |picker, cx| picker.cycle_selection(window, cx)) - }); - }); - }); - cx.on_action(|open_remote: &OpenRemote, cx| { - let from_existing_connection = open_remote.from_existing_connection; - let create_new_window = open_remote.create_new_window; - with_active_or_new_workspace(cx, move |workspace, window, cx| { - if from_existing_connection { - cx.propagate(); - return; - } - let handle = cx.entity().downgrade(); - let fs = workspace.project().read(cx).fs().clone(); - workspace.toggle_modal(window, cx, |window, cx| { - RemoteServerProjects::new(create_new_window, fs, window, handle, cx) - }) - }); - }); - - cx.observe_new(DisconnectedOverlay::register).detach(); - - cx.on_action(|_: &OpenDevContainer, cx| { - with_active_or_new_workspace(cx, move |workspace, window, cx| { - let app_state = workspace.app_state().clone(); - let replace_window = window.window_handle().downcast::(); - - cx.spawn_in(window, async move |_, mut cx| { - let (connection, starting_dir) = match dev_container::start_dev_container( - &mut cx, - app_state.node_runtime.clone(), - ) - .await - { - Ok((c, s)) => (c, s), - Err(e) => { - log::error!("Failed to start Dev Container: {:?}", e); - cx.prompt( - gpui::PromptLevel::Critical, - "Failed to start Dev Container", - Some(&format!("{:?}", e)), - &["Ok"], - ) - .await - .ok(); - return; - } - }; - - let result = open_remote_project( - connection.into(), - vec![starting_dir].into_iter().map(PathBuf::from).collect(), - app_state, - OpenOptions { - replace_window, - ..OpenOptions::default() - }, - &mut cx, - ) - .await; - - if let Err(e) = result { - log::error!("Failed to connect: {e:#}"); - cx.prompt( - gpui::PromptLevel::Critical, - "Failed to connect", - Some(&e.to_string()), - &["Ok"], - ) - .await - .ok(); - } - }) - .detach(); - - let fs = workspace.project().read(cx).fs().clone(); - let handle = cx.entity().downgrade(); - workspace.toggle_modal(window, cx, |window, cx| { - RemoteServerProjects::new_dev_container(fs, window, handle, cx) - }); - }); - }); - - // Subscribe to worktree additions to suggest opening the project in a dev container - cx.observe_new( - |workspace: &mut Workspace, window: Option<&mut Window>, cx: &mut Context| { - let Some(window) = window else { - return; - }; - cx.subscribe_in( - workspace.project(), - window, - move |_, project, event, window, cx| { - if let project::Event::WorktreeUpdatedEntries(worktree_id, updated_entries) = - event - { - dev_container_suggest::suggest_on_worktree_updated( - *worktree_id, - updated_entries, - project, - window, - cx, - ); - } - }, - ) - .detach(); - }, - ) - .detach(); -} - -#[cfg(target_os = "windows")] -pub fn add_wsl_distro( - fs: Arc, - connection_options: &remote::WslConnectionOptions, - cx: &App, -) { - use gpui::ReadGlobal; - use settings::SettingsStore; - - let distro_name = SharedString::from(&connection_options.distro_name); - let user = connection_options.user.clone(); - SettingsStore::global(cx).update_settings_file(fs, move |setting, _| { - let connections = setting - .remote - .wsl_connections - .get_or_insert(Default::default()); - - if !connections - .iter() - .any(|conn| conn.distro_name == distro_name && conn.user == user) - { - use std::collections::BTreeSet; - - connections.push(settings::WslConnection { - distro_name, - user, - projects: BTreeSet::new(), - }) - } - }); -} - -pub struct RecentProjects { - pub picker: Entity>, - rem_width: f32, - _subscription: Subscription, -} - -impl ModalView for RecentProjects {} - -impl RecentProjects { - fn new( - delegate: RecentProjectsDelegate, - rem_width: f32, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let picker = cx.new(|cx| { - // We want to use a list when we render paths, because the items can have different heights (multiple paths). - if delegate.render_paths { - Picker::list(delegate, window, cx) - } else { - Picker::uniform_list(delegate, window, cx) - } - }); - let _subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent)); - // We do not want to block the UI on a potentially lengthy call to DB, so we're gonna swap - // out workspace locations once the future runs to completion. - cx.spawn_in(window, async move |this, cx| { - let workspaces = WORKSPACE_DB - .recent_workspaces_on_disk() - .await - .log_err() - .unwrap_or_default(); - this.update_in(cx, move |this, window, cx| { - this.picker.update(cx, move |picker, cx| { - picker.delegate.set_workspaces(workspaces); - picker.update_matches(picker.query(cx), window, cx) - }) - }) - .ok() - }) - .detach(); - Self { - picker, - rem_width, - _subscription, - } - } - - pub fn open( - workspace: &mut Workspace, - create_new_window: bool, - window: &mut Window, - focus_handle: FocusHandle, - cx: &mut Context, - ) { - let weak = cx.entity().downgrade(); - workspace.toggle_modal(window, cx, |window, cx| { - let delegate = RecentProjectsDelegate::new(weak, create_new_window, true, focus_handle); - - Self::new(delegate, 34., window, cx) - }) - } -} - -impl EventEmitter for RecentProjects {} - -impl Focusable for RecentProjects { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.picker.focus_handle(cx) - } -} - -impl Render for RecentProjects { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - v_flex() - .key_context("RecentProjects") - .w(rems(self.rem_width)) - .child(self.picker.clone()) - .on_mouse_down_out(cx.listener(|this, _, window, cx| { - this.picker.update(cx, |this, cx| { - this.cancel(&Default::default(), window, cx); - }) - })) - } -} - -pub struct RecentProjectsDelegate { - workspace: WeakEntity, - workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>, - selected_match_index: usize, - matches: Vec, - render_paths: bool, - create_new_window: bool, - // Flag to reset index when there is a new query vs not reset index when user delete an item - reset_selected_match_index: bool, - has_any_non_local_projects: bool, - focus_handle: FocusHandle, -} - -impl RecentProjectsDelegate { - fn new( - workspace: WeakEntity, - create_new_window: bool, - render_paths: bool, - focus_handle: FocusHandle, - ) -> Self { - Self { - workspace, - workspaces: Vec::new(), - selected_match_index: 0, - matches: Default::default(), - create_new_window, - render_paths, - reset_selected_match_index: true, - has_any_non_local_projects: false, - focus_handle, - } - } - - pub fn set_workspaces( - &mut self, - workspaces: Vec<(WorkspaceId, SerializedWorkspaceLocation, PathList)>, - ) { - self.workspaces = workspaces; - self.has_any_non_local_projects = !self - .workspaces - .iter() - .all(|(_, location, _)| matches!(location, SerializedWorkspaceLocation::Local)); - } -} -impl EventEmitter for RecentProjectsDelegate {} -impl PickerDelegate for RecentProjectsDelegate { - type ListItem = ListItem; - - fn placeholder_text(&self, window: &mut Window, _: &mut App) -> Arc { - let (create_window, reuse_window) = if self.create_new_window { - ( - window.keystroke_text_for(&menu::Confirm), - window.keystroke_text_for(&menu::SecondaryConfirm), - ) - } else { - ( - window.keystroke_text_for(&menu::SecondaryConfirm), - window.keystroke_text_for(&menu::Confirm), - ) - }; - Arc::from(format!( - "{reuse_window} reuses this window, {create_window} opens a new one", - )) - } - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn selected_index(&self) -> usize { - self.selected_match_index - } - - fn set_selected_index( - &mut self, - ix: usize, - _window: &mut Window, - _cx: &mut Context>, - ) { - self.selected_match_index = ix; - } - - fn update_matches( - &mut self, - query: String, - _: &mut Window, - cx: &mut Context>, - ) -> gpui::Task<()> { - let query = query.trim_start(); - let smart_case = query.chars().any(|c| c.is_uppercase()); - let candidates = self - .workspaces - .iter() - .enumerate() - .filter(|(_, (id, _, _))| !self.is_current_workspace(*id, cx)) - .map(|(id, (_, _, paths))| { - let combined_string = paths - .ordered_paths() - .map(|path| path.compact().to_string_lossy().into_owned()) - .collect::>() - .join(""); - StringMatchCandidate::new(id, &combined_string) - }) - .collect::>(); - self.matches = smol::block_on(fuzzy::match_strings( - candidates.as_slice(), - query, - smart_case, - true, - 100, - &Default::default(), - cx.background_executor().clone(), - )); - self.matches.sort_unstable_by(|a, b| { - b.score - .partial_cmp(&a.score) // Descending score - .unwrap_or(std::cmp::Ordering::Equal) - .then_with(|| a.candidate_id.cmp(&b.candidate_id)) // Ascending candidate_id for ties - }); - - if self.reset_selected_match_index { - self.selected_match_index = self - .matches - .iter() - .enumerate() - .rev() - .max_by_key(|(_, m)| OrderedFloat(m.score)) - .map(|(ix, _)| ix) - .unwrap_or(0); - } - self.reset_selected_match_index = true; - Task::ready(()) - } - - fn confirm(&mut self, secondary: bool, window: &mut Window, cx: &mut Context>) { - if let Some((selected_match, workspace)) = self - .matches - .get(self.selected_index()) - .zip(self.workspace.upgrade()) - { - let (candidate_workspace_id, candidate_workspace_location, candidate_workspace_paths) = - &self.workspaces[selected_match.candidate_id]; - let replace_current_window = if self.create_new_window { - secondary - } else { - !secondary - }; - workspace.update(cx, |workspace, cx| { - if workspace.database_id() == Some(*candidate_workspace_id) { - return; - } - match candidate_workspace_location.clone() { - SerializedWorkspaceLocation::Local => { - let paths = candidate_workspace_paths.paths().to_vec(); - if replace_current_window { - cx.spawn_in(window, async move |workspace, cx| { - let continue_replacing = workspace - .update_in(cx, |workspace, window, cx| { - workspace.prepare_to_close( - CloseIntent::ReplaceWindow, - window, - cx, - ) - })? - .await?; - if continue_replacing { - workspace - .update_in(cx, |workspace, window, cx| { - workspace - .open_workspace_for_paths(true, paths, window, cx) - })? - .await - } else { - Ok(()) - } - }) - } else { - workspace.open_workspace_for_paths(false, paths, window, cx) - } - } - SerializedWorkspaceLocation::Remote(mut connection) => { - let app_state = workspace.app_state().clone(); - - let replace_window = if replace_current_window { - window.window_handle().downcast::() - } else { - None - }; - - let open_options = OpenOptions { - replace_window, - ..Default::default() - }; - - if let RemoteConnectionOptions::Ssh(connection) = &mut connection { - SshSettings::get_global(cx) - .fill_connection_options_from_settings(connection); - }; - - let paths = candidate_workspace_paths.paths().to_vec(); - - cx.spawn_in(window, async move |_, cx| { - open_remote_project( - connection.clone(), - paths, - app_state, - open_options, - cx, - ) - .await - }) - } - } - .detach_and_prompt_err( - "Failed to open project", - window, - cx, - |_, _, _| None, - ); - }); - cx.emit(DismissEvent); - } - } - - fn dismissed(&mut self, _window: &mut Window, _: &mut Context>) {} - - fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option { - let text = if self.workspaces.is_empty() { - "Recently opened projects will show up here".into() - } else { - "No matches".into() - }; - Some(text) - } - - fn render_match( - &self, - ix: usize, - selected: bool, - window: &mut Window, - cx: &mut Context>, - ) -> Option { - let hit = self.matches.get(ix)?; - - let (_, location, paths) = self.workspaces.get(hit.candidate_id)?; - - let mut path_start_offset = 0; - - let (match_labels, paths): (Vec<_>, Vec<_>) = paths - .ordered_paths() - .map(|p| p.compact()) - .map(|path| { - let highlighted_text = - highlights_for_path(path.as_ref(), &hit.positions, path_start_offset); - path_start_offset += highlighted_text.1.text.len(); - highlighted_text - }) - .unzip(); - - let prefix = match &location { - SerializedWorkspaceLocation::Remote(options) => { - Some(SharedString::from(options.display_name())) - } - _ => None, - }; - - let highlighted_match = HighlightedMatchWithPaths { - prefix, - match_label: HighlightedMatch::join(match_labels.into_iter().flatten(), ", "), - paths, - }; - - let focus_handle = self.focus_handle.clone(); - - let secondary_actions = h_flex() - .gap_px() - .child( - IconButton::new("open_new_window", IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) - .tooltip({ - move |_, cx| { - Tooltip::for_action_in( - "Open Project in New Window", - &menu::SecondaryConfirm, - &focus_handle, - cx, - ) - } - }) - .on_click(cx.listener(move |this, _event, window, cx| { - cx.stop_propagation(); - window.prevent_default(); - this.delegate.set_selected_index(ix, window, cx); - this.delegate.confirm(true, window, cx); - })), - ) - .child( - IconButton::new("delete", IconName::Close) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Delete from Recent Projects")) - .on_click(cx.listener(move |this, _event, window, cx| { - cx.stop_propagation(); - window.prevent_default(); - - this.delegate.delete_recent_project(ix, window, cx) - })), - ) - .into_any_element(); - - Some( - ListItem::new(ix) - .toggle_state(selected) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .child( - h_flex() - .id("projecy_info_container") - .gap_3() - .flex_grow() - .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::Remote(options) => { - Icon::new(match options { - RemoteConnectionOptions::Ssh { .. } => IconName::Server, - RemoteConnectionOptions::Wsl { .. } => IconName::Linux, - RemoteConnectionOptions::Docker(_) => IconName::Box, - }) - .color(Color::Muted) - .into_any_element() - } - }) - }) - .child({ - let mut highlighted = highlighted_match.clone(); - if !self.render_paths { - highlighted.paths.clear(); - } - highlighted.render(window, cx) - }) - .tooltip(move |_, cx| { - let tooltip_highlighted_location = highlighted_match.clone(); - cx.new(|_| MatchTooltip { - highlighted_location: tooltip_highlighted_location, - }) - .into() - }), - ) - .map(|el| { - if self.selected_index() == ix { - el.end_slot(secondary_actions) - } else { - el.end_hover_slot(secondary_actions) - } - }), - ) - } - - fn render_footer(&self, _: &mut Window, cx: &mut Context>) -> Option { - Some( - h_flex() - .w_full() - .p_2() - .gap_2() - .justify_end() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .child( - Button::new("remote", "Open Remote Folder") - .key_binding(KeyBinding::for_action( - &OpenRemote { - from_existing_connection: false, - create_new_window: false, - }, - cx, - )) - .on_click(|_, window, cx| { - window.dispatch_action( - OpenRemote { - from_existing_connection: false, - create_new_window: false, - } - .boxed_clone(), - cx, - ) - }), - ) - .child( - Button::new("local", "Open Local Folder") - .key_binding(KeyBinding::for_action(&workspace::Open, cx)) - .on_click(|_, window, cx| { - window.dispatch_action(workspace::Open.boxed_clone(), cx) - }), - ) - .into_any(), - ) - } -} - -// Compute the highlighted text for the name and path -fn highlights_for_path( - path: &Path, - match_positions: &Vec, - path_start_offset: usize, -) -> (Option, HighlightedMatch) { - let path_string = path.to_string_lossy(); - let path_text = path_string.to_string(); - let path_byte_len = path_text.len(); - // Get the subset of match highlight positions that line up with the given path. - // Also adjusts them to start at the path start - let path_positions = match_positions - .iter() - .copied() - .skip_while(|position| *position < path_start_offset) - .take_while(|position| *position < path_start_offset + path_byte_len) - .map(|position| position - path_start_offset) - .collect::>(); - - // Again subset the highlight positions to just those that line up with the file_name - // again adjusted to the start of the file_name - let file_name_text_and_positions = path.file_name().map(|file_name| { - let file_name_text = file_name.to_string_lossy().into_owned(); - let file_name_start_byte = path_byte_len - file_name_text.len(); - let highlight_positions = path_positions - .iter() - .copied() - .skip_while(|position| *position < file_name_start_byte) - .take_while(|position| *position < file_name_start_byte + file_name_text.len()) - .map(|position| position - file_name_start_byte) - .collect::>(); - HighlightedMatch { - text: file_name_text, - highlight_positions, - color: Color::Default, - } - }); - - ( - file_name_text_and_positions, - HighlightedMatch { - text: path_text, - highlight_positions: path_positions, - color: Color::Default, - }, - ) -} -impl RecentProjectsDelegate { - fn delete_recent_project( - &self, - ix: usize, - window: &mut Window, - cx: &mut Context>, - ) { - if let Some(selected_match) = self.matches.get(ix) { - 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 - .recent_workspaces_on_disk() - .await - .unwrap_or_default(); - this.update_in(cx, move |picker, window, cx| { - picker.delegate.set_workspaces(workspaces); - picker - .delegate - .set_selected_index(ix.saturating_sub(1), window, cx); - picker.delegate.reset_selected_match_index = false; - picker.update_matches(picker.query(cx), window, cx); - // After deleting a project, we want to update the history manager to reflect the change. - // But we do not emit a update event when user opens a project, because it's handled in `workspace::load_workspace`. - if let Some(history_manager) = HistoryManager::global(cx) { - history_manager - .update(cx, |this, cx| this.delete_history(workspace_id, cx)); - } - }) - }) - .detach(); - } - } - - fn is_current_workspace( - &self, - workspace_id: WorkspaceId, - cx: &mut Context>, - ) -> bool { - if let Some(workspace) = self.workspace.upgrade() { - let workspace = workspace.read(cx); - if Some(workspace_id) == workspace.database_id() { - return true; - } - } - - false - } -} -struct MatchTooltip { - highlighted_location: HighlightedMatchWithPaths, -} - -impl Render for MatchTooltip { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - tooltip_container(cx, |div, _| { - self.highlighted_location.render_paths_children(div) - }) - } -} - -#[cfg(test)] -mod tests { - use std::path::PathBuf; - - use editor::Editor; - use gpui::{TestAppContext, UpdateGlobal, WindowHandle}; - - use serde_json::json; - use settings::SettingsStore; - use util::path; - use workspace::{AppState, open_paths}; - - use super::*; - - #[gpui::test] - async fn test_prompts_on_dirty_before_submit(cx: &mut TestAppContext) { - let app_state = init_test(cx); - - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings - .session - .get_or_insert_default() - .restore_unsaved_buffers = Some(false) - }); - }); - }); - - app_state - .fs - .as_fake() - .insert_tree( - path!("/dir"), - json!({ - "main.ts": "a" - }), - ) - .await; - cx.update(|cx| { - open_paths( - &[PathBuf::from(path!("/dir/main.ts"))], - app_state, - workspace::OpenOptions::default(), - cx, - ) - }) - .await - .unwrap(); - assert_eq!(cx.update(|cx| cx.windows().len()), 1); - - let workspace = cx.update(|cx| cx.windows()[0].downcast::().unwrap()); - workspace - .update(cx, |workspace, _, _| assert!(!workspace.is_edited())) - .unwrap(); - - let editor = workspace - .read_with(cx, |workspace, cx| { - workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap() - }) - .unwrap(); - workspace - .update(cx, |_, window, cx| { - editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx)); - }) - .unwrap(); - workspace - .update(cx, |workspace, _, _| assert!(workspace.is_edited(), "After inserting more text into the editor without saving, we should have a dirty project")) - .unwrap(); - - let recent_projects_picker = open_recent_projects(&workspace, cx); - workspace - .update(cx, |_, _, cx| { - recent_projects_picker.update(cx, |picker, cx| { - assert_eq!(picker.query(cx), ""); - let delegate = &mut picker.delegate; - delegate.matches = vec![StringMatch { - candidate_id: 0, - score: 1.0, - positions: Vec::new(), - string: "fake candidate".to_string(), - }]; - delegate.set_workspaces(vec![( - WorkspaceId::default(), - SerializedWorkspaceLocation::Local, - PathList::new(&[path!("/test/path")]), - )]); - }); - }) - .unwrap(); - - assert!( - !cx.has_pending_prompt(), - "Should have no pending prompt on dirty project before opening the new recent project" - ); - cx.dispatch_action(*workspace, menu::Confirm); - workspace - .update(cx, |workspace, _, cx| { - assert!( - workspace.active_modal::(cx).is_none(), - "Should remove the modal after selecting new recent project" - ) - }) - .unwrap(); - assert!( - cx.has_pending_prompt(), - "Dirty workspace should prompt before opening the new recent project" - ); - cx.simulate_prompt_answer("Cancel"); - assert!( - !cx.has_pending_prompt(), - "Should have no pending prompt after cancelling" - ); - workspace - .update(cx, |workspace, _, _| { - assert!( - workspace.is_edited(), - "Should be in the same dirty project after cancelling" - ) - }) - .unwrap(); - } - - fn open_recent_projects( - workspace: &WindowHandle, - cx: &mut TestAppContext, - ) -> Entity> { - cx.dispatch_action( - (*workspace).into(), - OpenRecent { - create_new_window: false, - }, - ); - workspace - .update(cx, |workspace, _, cx| { - workspace - .active_modal::(cx) - .unwrap() - .read(cx) - .picker - .clone() - }) - .unwrap() - } - - fn init_test(cx: &mut TestAppContext) -> Arc { - cx.update(|cx| { - let state = AppState::test(cx); - crate::init(cx); - editor::init(cx); - state - }) - } -} diff --git a/crates/recent_projects/src/remote_connections.rs b/crates/recent_projects/src/remote_connections.rs deleted file mode 100644 index c0a655d19e..0000000000 --- a/crates/recent_projects/src/remote_connections.rs +++ /dev/null @@ -1,880 +0,0 @@ -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; - -use anyhow::{Context as _, Result}; -use askpass::EncryptedPassword; -use auto_update::AutoUpdater; -use editor::Editor; -use extension_host::ExtensionStore; -use futures::channel::oneshot; -use gpui::{ - AnyWindowHandle, App, AsyncApp, DismissEvent, Entity, EventEmitter, Focusable, FontFeatures, - ParentElement as _, PromptLevel, Render, SharedString, Task, TextStyleRefinement, WeakEntity, -}; - -use language::{CursorShape, Point}; -use markdown::{Markdown, MarkdownElement, MarkdownStyle}; -use release_channel::ReleaseChannel; -use remote::{ - ConnectionIdentifier, DockerConnectionOptions, RemoteClient, RemoteConnection, - RemoteConnectionOptions, RemotePlatform, SshConnectionOptions, -}; -use semver::Version; -pub use settings::SshConnection; -use settings::{DevContainerConnection, ExtendingVec, RegisterSetting, Settings, WslConnection}; -use theme::ThemeSettings; -use ui::{ - ActiveTheme, Color, CommonAnimationExt, Context, InteractiveElement, IntoElement, KeyBinding, - LabelCommon, ListItem, Styled, Window, prelude::*, -}; -use util::paths::PathWithPosition; -use workspace::{AppState, ModalView, Workspace}; - -#[derive(RegisterSetting)] -pub struct SshSettings { - pub ssh_connections: ExtendingVec, - pub wsl_connections: ExtendingVec, - /// Whether to read ~/.ssh/config for ssh connection sources. - pub read_ssh_config: bool, -} - -impl SshSettings { - pub fn ssh_connections(&self) -> impl Iterator + use<> { - self.ssh_connections.clone().0.into_iter() - } - - pub fn wsl_connections(&self) -> impl Iterator + use<> { - self.wsl_connections.clone().0.into_iter() - } - - pub fn fill_connection_options_from_settings(&self, options: &mut SshConnectionOptions) { - for conn in self.ssh_connections() { - if conn.host == options.host - && conn.username == options.username - && conn.port == options.port - { - options.nickname = conn.nickname; - options.upload_binary_over_ssh = conn.upload_binary_over_ssh.unwrap_or_default(); - options.args = Some(conn.args); - options.port_forwards = conn.port_forwards; - break; - } - } - } - - pub fn connection_options_for( - &self, - host: String, - port: Option, - username: Option, - ) -> SshConnectionOptions { - let mut options = SshConnectionOptions { - host, - port, - username, - ..Default::default() - }; - self.fill_connection_options_from_settings(&mut options); - options - } -} - -#[derive(Clone, PartialEq)] -pub enum Connection { - Ssh(SshConnection), - Wsl(WslConnection), - DevContainer(DevContainerConnection), -} - -impl From for RemoteConnectionOptions { - fn from(val: Connection) -> Self { - match val { - Connection::Ssh(conn) => RemoteConnectionOptions::Ssh(conn.into()), - Connection::Wsl(conn) => RemoteConnectionOptions::Wsl(conn.into()), - Connection::DevContainer(conn) => { - RemoteConnectionOptions::Docker(DockerConnectionOptions { - name: conn.name.to_string(), - container_id: conn.container_id.to_string(), - upload_binary_over_docker_exec: false, - }) - } - } - } -} - -impl From for Connection { - fn from(val: SshConnection) -> Self { - Connection::Ssh(val) - } -} - -impl From for Connection { - fn from(val: WslConnection) -> Self { - Connection::Wsl(val) - } -} - -impl Settings for SshSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - let remote = &content.remote; - Self { - ssh_connections: remote.ssh_connections.clone().unwrap_or_default().into(), - wsl_connections: remote.wsl_connections.clone().unwrap_or_default().into(), - read_ssh_config: remote.read_ssh_config.unwrap(), - } - } -} - -pub struct RemoteConnectionPrompt { - connection_string: SharedString, - nickname: Option, - is_wsl: bool, - is_devcontainer: bool, - status_message: Option, - prompt: Option<(Entity, oneshot::Sender)>, - cancellation: Option>, - editor: Entity, -} - -impl Drop for RemoteConnectionPrompt { - fn drop(&mut self) { - if let Some(cancel) = self.cancellation.take() { - cancel.send(()).ok(); - } - } -} - -pub struct RemoteConnectionModal { - pub prompt: Entity, - paths: Vec, - finished: bool, -} - -impl RemoteConnectionPrompt { - pub(crate) fn new( - connection_string: String, - nickname: Option, - is_wsl: bool, - is_devcontainer: bool, - window: &mut Window, - cx: &mut Context, - ) -> Self { - Self { - connection_string: connection_string.into(), - nickname: nickname.map(|nickname| nickname.into()), - is_wsl, - is_devcontainer, - editor: cx.new(|cx| Editor::single_line(window, cx)), - status_message: None, - cancellation: None, - prompt: None, - } - } - - pub fn set_cancellation_tx(&mut self, tx: oneshot::Sender<()>) { - self.cancellation = Some(tx); - } - - fn set_prompt( - &mut self, - prompt: String, - tx: oneshot::Sender, - window: &mut Window, - cx: &mut Context, - ) { - let theme = ThemeSettings::get_global(cx); - - let refinement = TextStyleRefinement { - font_family: Some(theme.buffer_font.family.clone()), - font_features: Some(FontFeatures::disable_ligatures()), - font_size: Some(theme.buffer_font_size(cx).into()), - color: Some(cx.theme().colors().editor_foreground), - background_color: Some(gpui::transparent_black()), - ..Default::default() - }; - - self.editor.update(cx, |editor, cx| { - if prompt.contains("yes/no") { - editor.set_masked(false, cx); - } else { - editor.set_masked(true, cx); - } - editor.set_text_style_refinement(refinement); - editor.set_cursor_shape(CursorShape::Block, cx); - }); - - let markdown = cx.new(|cx| Markdown::new_text(prompt.into(), cx)); - self.prompt = Some((markdown, tx)); - self.status_message.take(); - window.focus(&self.editor.focus_handle(cx)); - cx.notify(); - } - - pub fn set_status(&mut self, status: Option, cx: &mut Context) { - self.status_message = status.map(|s| s.into()); - cx.notify(); - } - - pub fn confirm(&mut self, window: &mut Window, cx: &mut Context) { - if let Some((_, tx)) = self.prompt.take() { - self.status_message = Some("Connecting".into()); - - self.editor.update(cx, |editor, cx| { - let pw = editor.text(cx); - if let Ok(secure) = EncryptedPassword::try_from(pw.as_ref()) { - tx.send(secure).ok(); - } - editor.clear(window, cx); - }); - } - } -} - -impl Render for RemoteConnectionPrompt { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let theme = ThemeSettings::get_global(cx); - - let mut text_style = window.text_style(); - let refinement = TextStyleRefinement { - font_family: Some(theme.buffer_font.family.clone()), - font_features: Some(FontFeatures::disable_ligatures()), - font_size: Some(theme.buffer_font_size(cx).into()), - color: Some(cx.theme().colors().editor_foreground), - background_color: Some(gpui::transparent_black()), - ..Default::default() - }; - - text_style.refine(&refinement); - let markdown_style = MarkdownStyle { - base_text_style: text_style, - selection_background_color: cx.theme().colors().element_selection_background, - ..Default::default() - }; - - v_flex() - .key_context("PasswordPrompt") - .p_2() - .size_full() - .text_buffer(cx) - .when_some(self.status_message.clone(), |el, status_message| { - el.child( - h_flex() - .gap_2() - .child( - Icon::new(IconName::ArrowCircle) - .color(Color::Muted) - .with_rotate_animation(2), - ) - .child( - div() - .text_ellipsis() - .overflow_x_hidden() - .child(format!("{}…", status_message)), - ), - ) - }) - .when_some(self.prompt.as_ref(), |el, prompt| { - el.child( - div() - .size_full() - .overflow_hidden() - .child(MarkdownElement::new(prompt.0.clone(), markdown_style)) - .child(self.editor.clone()), - ) - .when(window.capslock().on, |el| { - el.child(Label::new("⚠️ ⇪ is on")) - }) - }) - } -} - -impl RemoteConnectionModal { - pub fn new( - connection_options: &RemoteConnectionOptions, - paths: Vec, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let (connection_string, nickname, is_wsl, is_devcontainer) = match connection_options { - RemoteConnectionOptions::Ssh(options) => ( - options.connection_string(), - options.nickname.clone(), - false, - false, - ), - RemoteConnectionOptions::Wsl(options) => { - (options.distro_name.clone(), None, true, false) - } - RemoteConnectionOptions::Docker(options) => (options.name.clone(), None, false, true), - }; - Self { - prompt: cx.new(|cx| { - RemoteConnectionPrompt::new( - connection_string, - nickname, - is_wsl, - is_devcontainer, - window, - cx, - ) - }), - finished: false, - paths, - } - } - - fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) { - self.prompt - .update(cx, |prompt, cx| prompt.confirm(window, cx)) - } - - pub fn finished(&mut self, cx: &mut Context) { - self.finished = true; - cx.emit(DismissEvent); - } - - fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { - if let Some(tx) = self - .prompt - .update(cx, |prompt, _cx| prompt.cancellation.take()) - { - tx.send(()).ok(); - } - self.finished(cx); - } -} - -pub(crate) struct SshConnectionHeader { - pub(crate) connection_string: SharedString, - pub(crate) paths: Vec, - pub(crate) nickname: Option, - pub(crate) is_wsl: bool, - pub(crate) is_devcontainer: bool, -} - -impl RenderOnce for SshConnectionHeader { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let theme = cx.theme(); - - let mut header_color = theme.colors().text; - header_color.fade_out(0.96); - - let (main_label, meta_label) = if let Some(nickname) = self.nickname { - (nickname, Some(format!("({})", self.connection_string))) - } else { - (self.connection_string, None) - }; - - let icon = if self.is_wsl { - IconName::Linux - } else if self.is_devcontainer { - IconName::Box - } else { - IconName::Server - }; - - h_flex() - .px(DynamicSpacing::Base12.rems(cx)) - .pt(DynamicSpacing::Base08.rems(cx)) - .pb(DynamicSpacing::Base04.rems(cx)) - .rounded_t_sm() - .w_full() - .gap_1p5() - .child(Icon::new(icon).size(IconSize::Small)) - .child( - h_flex() - .gap_1() - .overflow_x_hidden() - .child( - div() - .max_w_96() - .overflow_x_hidden() - .text_ellipsis() - .child(Headline::new(main_label).size(HeadlineSize::XSmall)), - ) - .children( - meta_label.map(|label| { - Label::new(label).color(Color::Muted).size(LabelSize::Small) - }), - ) - .child(div().overflow_x_hidden().text_ellipsis().children( - self.paths.into_iter().map(|path| { - Label::new(path.to_string_lossy().into_owned()) - .size(LabelSize::Small) - .color(Color::Muted) - }), - )), - ) - } -} - -impl Render for RemoteConnectionModal { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl ui::IntoElement { - let nickname = self.prompt.read(cx).nickname.clone(); - let connection_string = self.prompt.read(cx).connection_string.clone(); - let is_wsl = self.prompt.read(cx).is_wsl; - let is_devcontainer = self.prompt.read(cx).is_devcontainer; - - let theme = cx.theme().clone(); - let body_color = theme.colors().editor_background; - - v_flex() - .elevation_3(cx) - .w(rems(34.)) - .border_1() - .border_color(theme.colors().border) - .key_context("SshConnectionModal") - .track_focus(&self.focus_handle(cx)) - .on_action(cx.listener(Self::dismiss)) - .on_action(cx.listener(Self::confirm)) - .child( - SshConnectionHeader { - paths: self.paths.clone(), - connection_string, - nickname, - is_wsl, - is_devcontainer, - } - .render(window, cx), - ) - .child( - div() - .w_full() - .bg(body_color) - .border_y_1() - .border_color(theme.colors().border_variant) - .child(self.prompt.clone()), - ) - .child( - div().w_full().py_1().child( - ListItem::new("li-devcontainer-go-back") - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Close).color(Color::Muted)) - .child(Label::new("Cancel")) - .end_slot( - KeyBinding::for_action_in(&menu::Cancel, &self.focus_handle(cx), cx) - .size(rems_from_px(12.)), - ) - .on_click(cx.listener(|this, _, window, cx| { - this.dismiss(&menu::Cancel, window, cx); - })), - ), - ) - } -} - -impl Focusable for RemoteConnectionModal { - fn focus_handle(&self, cx: &gpui::App) -> gpui::FocusHandle { - self.prompt.read(cx).editor.focus_handle(cx) - } -} - -impl EventEmitter for RemoteConnectionModal {} - -impl ModalView for RemoteConnectionModal { - fn on_before_dismiss( - &mut self, - _window: &mut Window, - _: &mut Context, - ) -> workspace::DismissDecision { - workspace::DismissDecision::Dismiss(self.finished) - } - - fn fade_out_background(&self) -> bool { - true - } -} - -#[derive(Clone)] -pub struct RemoteClientDelegate { - window: AnyWindowHandle, - ui: WeakEntity, - known_password: Option, -} - -impl remote::RemoteClientDelegate for RemoteClientDelegate { - fn ask_password( - &self, - prompt: String, - tx: oneshot::Sender, - cx: &mut AsyncApp, - ) { - let mut known_password = self.known_password.clone(); - if let Some(password) = known_password.take() { - tx.send(password).ok(); - } else { - self.window - .update(cx, |_, window, cx| { - self.ui.update(cx, |modal, cx| { - modal.set_prompt(prompt, tx, window, cx); - }) - }) - .ok(); - } - } - - fn set_status(&self, status: Option<&str>, cx: &mut AsyncApp) { - self.update_status(status, cx) - } - - fn download_server_binary_locally( - &self, - platform: RemotePlatform, - release_channel: ReleaseChannel, - version: Option, - cx: &mut AsyncApp, - ) -> Task> { - let this = self.clone(); - cx.spawn(async move |cx| { - AutoUpdater::download_remote_server_release( - release_channel, - version.clone(), - platform.os, - platform.arch, - move |status, cx| this.set_status(Some(status), cx), - cx, - ) - .await - .with_context(|| { - format!( - "Downloading remote server binary (version: {}, os: {}, arch: {})", - version - .as_ref() - .map(|v| format!("{}", v)) - .unwrap_or("unknown".to_string()), - platform.os, - platform.arch, - ) - }) - }) - } - - fn get_download_url( - &self, - platform: RemotePlatform, - release_channel: ReleaseChannel, - version: Option, - cx: &mut AsyncApp, - ) -> Task>> { - cx.spawn(async move |cx| { - AutoUpdater::get_remote_server_release_url( - release_channel, - version, - platform.os, - platform.arch, - cx, - ) - .await - }) - } -} - -impl RemoteClientDelegate { - fn update_status(&self, status: Option<&str>, cx: &mut AsyncApp) { - self.window - .update(cx, |_, _, cx| { - self.ui.update(cx, |modal, cx| { - modal.set_status(status.map(|s| s.to_string()), cx); - }) - }) - .ok(); - } -} - -pub fn connect( - unique_identifier: ConnectionIdentifier, - connection_options: RemoteConnectionOptions, - ui: Entity, - window: &mut Window, - cx: &mut App, -) -> Task>>> { - let window = window.window_handle(); - let known_password = match &connection_options { - RemoteConnectionOptions::Ssh(ssh_connection_options) => ssh_connection_options - .password - .as_deref() - .and_then(|pw| pw.try_into().ok()), - _ => None, - }; - let (tx, rx) = oneshot::channel(); - ui.update(cx, |ui, _cx| ui.set_cancellation_tx(tx)); - - let delegate = Arc::new(RemoteClientDelegate { - window, - ui: ui.downgrade(), - known_password, - }); - - cx.spawn(async move |cx| { - let connection = remote::connect(connection_options, delegate.clone(), cx).await?; - cx.update(|cx| remote::RemoteClient::new(unique_identifier, connection, rx, delegate, cx))? - .await - }) -} - -pub async fn open_remote_project( - connection_options: RemoteConnectionOptions, - paths: Vec, - app_state: Arc, - open_options: workspace::OpenOptions, - cx: &mut AsyncApp, -) -> Result<()> { - let created_new_window = open_options.replace_window.is_none(); - let window = if let Some(window) = open_options.replace_window { - window - } else { - let workspace_position = cx - .update(|cx| { - // todo: These paths are wrong they may have column and line information - workspace::remote_workspace_position_from_db(connection_options.clone(), &paths, cx) - })? - .await - .context("fetching ssh workspace position from db")?; - - let mut options = - cx.update(|cx| (app_state.build_window_options)(workspace_position.display, cx))?; - options.window_bounds = workspace_position.window_bounds; - - cx.open_window(options, |window, cx| { - let project = project::Project::local( - app_state.client.clone(), - app_state.node_runtime.clone(), - app_state.user_store.clone(), - app_state.languages.clone(), - app_state.fs.clone(), - None, - cx, - ); - cx.new(|cx| { - let mut workspace = Workspace::new(None, project, app_state.clone(), window, cx); - workspace.centered_layout = workspace_position.centered_layout; - workspace - }) - })? - }; - - loop { - let (cancel_tx, cancel_rx) = oneshot::channel(); - let delegate = window.update(cx, { - let paths = paths.clone(); - let connection_options = connection_options.clone(); - move |workspace, window, cx| { - window.activate_window(); - workspace.toggle_modal(window, cx, |window, cx| { - RemoteConnectionModal::new(&connection_options, paths, window, cx) - }); - - let ui = workspace - .active_modal::(cx)? - .read(cx) - .prompt - .clone(); - - ui.update(cx, |ui, _cx| { - ui.set_cancellation_tx(cancel_tx); - }); - - Some(Arc::new(RemoteClientDelegate { - window: window.window_handle(), - ui: ui.downgrade(), - known_password: if let RemoteConnectionOptions::Ssh(options) = - &connection_options - { - options - .password - .as_deref() - .and_then(|pw| EncryptedPassword::try_from(pw).ok()) - } else { - None - }, - })) - } - })?; - - let Some(delegate) = delegate else { break }; - - let remote_connection = - match remote::connect(connection_options.clone(), delegate.clone(), cx).await { - Ok(connection) => connection, - Err(e) => { - window - .update(cx, |workspace, _, cx| { - if let Some(ui) = workspace.active_modal::(cx) { - ui.update(cx, |modal, cx| modal.finished(cx)) - } - }) - .ok(); - log::error!("Failed to open project: {e:#}"); - let response = window - .update(cx, |_, window, cx| { - window.prompt( - PromptLevel::Critical, - match connection_options { - RemoteConnectionOptions::Ssh(_) => "Failed to connect over SSH", - RemoteConnectionOptions::Wsl(_) => "Failed to connect to WSL", - RemoteConnectionOptions::Docker(_) => { - "Failed to connect to Dev Container" - } - }, - Some(&format!("{e:#}")), - &["Retry", "Cancel"], - cx, - ) - })? - .await; - - if response == Ok(0) { - continue; - } - - if created_new_window { - window - .update(cx, |_, window, _| window.remove_window()) - .ok(); - } - break; - } - }; - - let (paths, paths_with_positions) = - determine_paths_with_positions(&remote_connection, paths.clone()).await; - - let opened_items = cx - .update(|cx| { - workspace::open_remote_project_with_new_connection( - window, - remote_connection, - cancel_rx, - delegate.clone(), - app_state.clone(), - paths.clone(), - cx, - ) - })? - .await; - - window - .update(cx, |workspace, _, cx| { - if let Some(ui) = workspace.active_modal::(cx) { - ui.update(cx, |modal, cx| modal.finished(cx)) - } - }) - .ok(); - - match opened_items { - Err(e) => { - log::error!("Failed to open project: {e:#}"); - let response = window - .update(cx, |_, window, cx| { - window.prompt( - PromptLevel::Critical, - match connection_options { - RemoteConnectionOptions::Ssh(_) => "Failed to connect over SSH", - RemoteConnectionOptions::Wsl(_) => "Failed to connect to WSL", - RemoteConnectionOptions::Docker(_) => { - "Failed to connect to Dev Container" - } - }, - Some(&format!("{e:#}")), - &["Retry", "Cancel"], - cx, - ) - })? - .await; - if response == Ok(0) { - continue; - } - - if created_new_window { - window - .update(cx, |_, window, _| window.remove_window()) - .ok(); - } - } - - Ok(items) => { - for (item, path) in items.into_iter().zip(paths_with_positions) { - let Some(item) = item else { - continue; - }; - let Some(row) = path.row else { - continue; - }; - if let Some(active_editor) = item.downcast::() { - window - .update(cx, |_, window, cx| { - active_editor.update(cx, |editor, cx| { - let row = row.saturating_sub(1); - let col = path.column.unwrap_or(0).saturating_sub(1); - editor.go_to_singleton_buffer_point( - Point::new(row, col), - window, - cx, - ); - }); - }) - .ok(); - } - } - } - } - - window - .update(cx, |workspace, _, cx| { - if let Some(client) = workspace.project().read(cx).remote_client() { - ExtensionStore::global(cx) - .update(cx, |store, cx| store.register_remote_client(client, cx)); - } - }) - .ok(); - - break; - } - - // Already showed the error to the user - Ok(()) -} - -pub(crate) async fn determine_paths_with_positions( - remote_connection: &Arc, - mut paths: Vec, -) -> (Vec, Vec) { - let mut paths_with_positions = Vec::::new(); - for path in &mut paths { - if let Some(path_str) = path.to_str() { - let path_with_position = PathWithPosition::parse_str(&path_str); - if path_with_position.row.is_some() { - if !path_exists(&remote_connection, &path).await { - *path = path_with_position.path.clone(); - paths_with_positions.push(path_with_position); - continue; - } - } - } - paths_with_positions.push(PathWithPosition::from_path(path.clone())) - } - (paths, paths_with_positions) -} - -async fn path_exists(connection: &Arc, path: &Path) -> bool { - let Ok(command) = connection.build_command( - Some("test".to_string()), - &["-e".to_owned(), path.to_string_lossy().to_string()], - &Default::default(), - None, - None, - ) else { - return false; - }; - let Ok(mut child) = util::command::new_smol_command(command.program) - .args(command.args) - .envs(command.env) - .spawn() - else { - return false; - }; - child.status().await.is_ok_and(|status| status.success()) -} diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs deleted file mode 100644 index 32a4ef1a81..0000000000 --- a/crates/recent_projects/src/remote_servers.rs +++ /dev/null @@ -1,2787 +0,0 @@ -use crate::{ - dev_container::start_dev_container, - remote_connections::{ - Connection, RemoteConnectionModal, RemoteConnectionPrompt, SshConnection, - SshConnectionHeader, SshSettings, connect, determine_paths_with_positions, - open_remote_project, - }, - ssh_config::parse_ssh_config_hosts, -}; -use editor::Editor; -use file_finder::OpenPathDelegate; -use futures::{FutureExt, channel::oneshot, future::Shared, select}; -use gpui::{ - AnyElement, App, ClickEvent, ClipboardItem, Context, DismissEvent, Entity, EventEmitter, - FocusHandle, Focusable, PromptLevel, ScrollHandle, Subscription, Task, WeakEntity, Window, - canvas, -}; -use language::Point; -use log::info; -use paths::{global_ssh_config_file, user_ssh_config_file}; -use picker::Picker; -use project::{Fs, Project}; -use remote::{ - RemoteClient, RemoteConnectionOptions, SshConnectionOptions, WslConnectionOptions, - remote_client::ConnectionIdentifier, -}; -use settings::{ - RemoteProject, RemoteSettingsContent, Settings as _, SettingsStore, update_settings_file, - watch_config_file, -}; -use smol::stream::StreamExt as _; -use std::{ - borrow::Cow, - collections::BTreeSet, - path::PathBuf, - rc::Rc, - sync::{ - Arc, - atomic::{self, AtomicUsize}, - }, -}; -use ui::{ - CommonAnimationExt, IconButtonShape, KeyBinding, List, ListItem, ListSeparator, Modal, - ModalHeader, Navigable, NavigableEntry, Section, Tooltip, WithScrollbar, prelude::*, -}; -use util::{ - ResultExt, - paths::{PathStyle, RemotePathBuf}, - rel_path::RelPath, -}; -use workspace::{ - ModalView, OpenOptions, Toast, Workspace, - notifications::{DetachAndPromptErr, NotificationId}, - open_remote_project_with_existing_connection, -}; - -pub struct RemoteServerProjects { - mode: Mode, - focus_handle: FocusHandle, - workspace: WeakEntity, - retained_connections: Vec>, - ssh_config_updates: Task<()>, - ssh_config_servers: BTreeSet, - create_new_window: bool, - _subscription: Subscription, -} - -struct CreateRemoteServer { - address_editor: Entity, - address_error: Option, - ssh_prompt: Option>, - _creating: Option>>, -} - -impl CreateRemoteServer { - fn new(window: &mut Window, cx: &mut App) -> Self { - let address_editor = cx.new(|cx| Editor::single_line(window, cx)); - address_editor.update(cx, |this, cx| { - this.focus_handle(cx).focus(window); - }); - Self { - address_editor, - address_error: None, - ssh_prompt: None, - _creating: None, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -enum DevContainerCreationProgress { - Initial, - Creating, - Error(String), -} - -#[derive(Clone)] -struct CreateRemoteDevContainer { - // 3 Navigable Options - // - Create from devcontainer.json - // - Edit devcontainer.json - // - Go back - entries: [NavigableEntry; 3], - progress: DevContainerCreationProgress, -} - -impl CreateRemoteDevContainer { - fn new(window: &mut Window, cx: &mut Context) -> Self { - let entries = std::array::from_fn(|_| NavigableEntry::focusable(cx)); - entries[0].focus_handle.focus(window); - Self { - entries, - progress: DevContainerCreationProgress::Initial, - } - } - - fn progress(&mut self, progress: DevContainerCreationProgress) -> Self { - self.progress = progress; - self.clone() - } -} - -#[cfg(target_os = "windows")] -struct AddWslDistro { - picker: Entity>, - connection_prompt: Option>, - _creating: Option>, -} - -#[cfg(target_os = "windows")] -impl AddWslDistro { - fn new(window: &mut Window, cx: &mut Context) -> Self { - use crate::wsl_picker::{WslDistroSelected, WslPickerDelegate, WslPickerDismissed}; - - let delegate = WslPickerDelegate::new(); - let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false)); - - cx.subscribe_in( - &picker, - window, - |this, _, _: &WslDistroSelected, window, cx| { - this.confirm(&menu::Confirm, window, cx); - }, - ) - .detach(); - - cx.subscribe_in( - &picker, - window, - |this, _, _: &WslPickerDismissed, window, cx| { - this.cancel(&menu::Cancel, window, cx); - }, - ) - .detach(); - - AddWslDistro { - picker, - connection_prompt: None, - _creating: None, - } - } -} - -enum ProjectPickerData { - Ssh { - connection_string: SharedString, - nickname: Option, - }, - Wsl { - distro_name: SharedString, - }, -} - -struct ProjectPicker { - data: ProjectPickerData, - picker: Entity>, - _path_task: Shared>>, -} - -struct EditNicknameState { - index: SshServerIndex, - editor: Entity, -} - -impl EditNicknameState { - fn new(index: SshServerIndex, window: &mut Window, cx: &mut App) -> Self { - let this = Self { - index, - editor: cx.new(|cx| Editor::single_line(window, cx)), - }; - let starting_text = SshSettings::get_global(cx) - .ssh_connections() - .nth(index.0) - .and_then(|state| state.nickname) - .filter(|text| !text.is_empty()); - this.editor.update(cx, |this, cx| { - this.set_placeholder_text("Add a nickname for this server", window, cx); - if let Some(starting_text) = starting_text { - this.set_text(starting_text, window, cx); - } - }); - this.editor.focus_handle(cx).focus(window); - this - } -} - -impl Focusable for ProjectPicker { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.picker.focus_handle(cx) - } -} - -impl ProjectPicker { - fn new( - create_new_window: bool, - index: ServerIndex, - connection: RemoteConnectionOptions, - project: Entity, - home_dir: RemotePathBuf, - path_style: PathStyle, - workspace: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - let (tx, rx) = oneshot::channel(); - let lister = project::DirectoryLister::Project(project.clone()); - let delegate = file_finder::OpenPathDelegate::new(tx, lister, false, path_style); - - let picker = cx.new(|cx| { - let picker = Picker::uniform_list(delegate, window, cx) - .width(rems(34.)) - .modal(false); - picker.set_query(home_dir.to_string(), window, cx); - picker - }); - - let data = match &connection { - RemoteConnectionOptions::Ssh(connection) => ProjectPickerData::Ssh { - connection_string: connection.connection_string().into(), - nickname: connection.nickname.clone().map(|nick| nick.into()), - }, - RemoteConnectionOptions::Wsl(connection) => ProjectPickerData::Wsl { - distro_name: connection.distro_name.clone().into(), - }, - RemoteConnectionOptions::Docker(_) => ProjectPickerData::Ssh { - // Not implemented as a project picker at this time - connection_string: "".into(), - nickname: None, - }, - }; - let _path_task = cx - .spawn_in(window, { - let workspace = workspace; - async move |this, cx| { - let Ok(Some(paths)) = rx.await else { - workspace - .update_in(cx, |workspace, window, cx| { - let fs = workspace.project().read(cx).fs().clone(); - let weak = cx.entity().downgrade(); - workspace.toggle_modal(window, cx, |window, cx| { - RemoteServerProjects::new( - create_new_window, - fs, - window, - weak, - cx, - ) - }); - }) - .log_err()?; - return None; - }; - - let app_state = workspace - .read_with(cx, |workspace, _| workspace.app_state().clone()) - .ok()?; - - let remote_connection = project - .read_with(cx, |project, cx| { - project.remote_client()?.read(cx).connection() - }) - .ok()??; - - let (paths, paths_with_positions) = - determine_paths_with_positions(&remote_connection, paths).await; - - cx.update(|_, cx| { - let fs = app_state.fs.clone(); - update_settings_file(fs, cx, { - let paths = paths - .iter() - .map(|path| path.to_string_lossy().into_owned()) - .collect(); - move |settings, _| match index { - ServerIndex::Ssh(index) => { - if let Some(server) = settings - .remote - .ssh_connections - .as_mut() - .and_then(|connections| connections.get_mut(index.0)) - { - server.projects.insert(RemoteProject { paths }); - }; - } - ServerIndex::Wsl(index) => { - if let Some(server) = settings - .remote - .wsl_connections - .as_mut() - .and_then(|connections| connections.get_mut(index.0)) - { - server.projects.insert(RemoteProject { paths }); - }; - } - } - }); - }) - .log_err(); - - let options = cx - .update(|_, cx| (app_state.build_window_options)(None, cx)) - .log_err()?; - let window = cx - .open_window(options, |window, cx| { - cx.new(|cx| { - telemetry::event!("SSH Project Created"); - Workspace::new(None, project.clone(), app_state.clone(), window, cx) - }) - }) - .log_err()?; - - let items = open_remote_project_with_existing_connection( - connection, project, paths, app_state, window, cx, - ) - .await - .log_err(); - - if let Some(items) = items { - for (item, path) in items.into_iter().zip(paths_with_positions) { - let Some(item) = item else { - continue; - }; - let Some(row) = path.row else { - continue; - }; - if let Some(active_editor) = item.downcast::() { - window - .update(cx, |_, window, cx| { - active_editor.update(cx, |editor, cx| { - let row = row.saturating_sub(1); - let col = path.column.unwrap_or(0).saturating_sub(1); - editor.go_to_singleton_buffer_point( - Point::new(row, col), - window, - cx, - ); - }); - }) - .ok(); - } - } - } - - this.update(cx, |_, cx| { - cx.emit(DismissEvent); - }) - .ok(); - Some(()) - } - }) - .shared(); - cx.new(|_| Self { - _path_task, - picker, - data, - }) - } -} - -impl gpui::Render for ProjectPicker { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - v_flex() - .child(match &self.data { - ProjectPickerData::Ssh { - connection_string, - nickname, - } => SshConnectionHeader { - connection_string: connection_string.clone(), - paths: Default::default(), - nickname: nickname.clone(), - is_wsl: false, - is_devcontainer: false, - } - .render(window, cx), - ProjectPickerData::Wsl { distro_name } => SshConnectionHeader { - connection_string: distro_name.clone(), - paths: Default::default(), - nickname: None, - is_wsl: true, - is_devcontainer: false, - } - .render(window, cx), - }) - .child( - div() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .child(self.picker.clone()), - ) - } -} - -#[repr(transparent)] -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -struct SshServerIndex(usize); -impl std::fmt::Display for SshServerIndex { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - -#[repr(transparent)] -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -struct WslServerIndex(usize); -impl std::fmt::Display for WslServerIndex { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] -enum ServerIndex { - Ssh(SshServerIndex), - Wsl(WslServerIndex), -} -impl From for ServerIndex { - fn from(index: SshServerIndex) -> Self { - Self::Ssh(index) - } -} -impl From for ServerIndex { - fn from(index: WslServerIndex) -> Self { - Self::Wsl(index) - } -} - -#[derive(Clone)] -enum RemoteEntry { - Project { - open_folder: NavigableEntry, - projects: Vec<(NavigableEntry, RemoteProject)>, - configure: NavigableEntry, - connection: Connection, - index: ServerIndex, - }, - SshConfig { - open_folder: NavigableEntry, - host: SharedString, - }, -} - -impl RemoteEntry { - fn is_from_zed(&self) -> bool { - matches!(self, Self::Project { .. }) - } - - fn connection(&self) -> Cow<'_, Connection> { - match self { - Self::Project { connection, .. } => Cow::Borrowed(connection), - Self::SshConfig { host, .. } => Cow::Owned( - SshConnection { - host: host.clone(), - ..SshConnection::default() - } - .into(), - ), - } - } -} - -#[derive(Clone)] -struct DefaultState { - scroll_handle: ScrollHandle, - add_new_server: NavigableEntry, - add_new_devcontainer: NavigableEntry, - add_new_wsl: NavigableEntry, - servers: Vec, -} - -impl DefaultState { - fn new(ssh_config_servers: &BTreeSet, cx: &mut App) -> Self { - let handle = ScrollHandle::new(); - let add_new_server = NavigableEntry::new(&handle, cx); - let add_new_devcontainer = NavigableEntry::new(&handle, cx); - let add_new_wsl = NavigableEntry::new(&handle, cx); - - let ssh_settings = SshSettings::get_global(cx); - let read_ssh_config = ssh_settings.read_ssh_config; - - let ssh_servers = ssh_settings - .ssh_connections() - .enumerate() - .map(|(index, connection)| { - let open_folder = NavigableEntry::new(&handle, cx); - let configure = NavigableEntry::new(&handle, cx); - let projects = connection - .projects - .iter() - .map(|project| (NavigableEntry::new(&handle, cx), project.clone())) - .collect(); - RemoteEntry::Project { - open_folder, - configure, - projects, - index: ServerIndex::Ssh(SshServerIndex(index)), - connection: connection.into(), - } - }); - - let wsl_servers = ssh_settings - .wsl_connections() - .enumerate() - .map(|(index, connection)| { - let open_folder = NavigableEntry::new(&handle, cx); - let configure = NavigableEntry::new(&handle, cx); - let projects = connection - .projects - .iter() - .map(|project| (NavigableEntry::new(&handle, cx), project.clone())) - .collect(); - RemoteEntry::Project { - open_folder, - configure, - projects, - index: ServerIndex::Wsl(WslServerIndex(index)), - connection: connection.into(), - } - }); - - let mut servers = ssh_servers.chain(wsl_servers).collect::>(); - - if read_ssh_config { - let mut extra_servers_from_config = ssh_config_servers.clone(); - for server in &servers { - if let RemoteEntry::Project { - connection: Connection::Ssh(ssh_options), - .. - } = server - { - extra_servers_from_config.remove(&SharedString::new(ssh_options.host.clone())); - } - } - servers.extend(extra_servers_from_config.into_iter().map(|host| { - RemoteEntry::SshConfig { - open_folder: NavigableEntry::new(&handle, cx), - host, - } - })); - } - - Self { - scroll_handle: handle, - add_new_server, - add_new_devcontainer, - add_new_wsl, - servers, - } - } -} - -#[derive(Clone)] -enum ViewServerOptionsState { - Ssh { - connection: SshConnectionOptions, - server_index: SshServerIndex, - entries: [NavigableEntry; 4], - }, - Wsl { - connection: WslConnectionOptions, - server_index: WslServerIndex, - entries: [NavigableEntry; 2], - }, -} - -impl ViewServerOptionsState { - fn entries(&self) -> &[NavigableEntry] { - match self { - Self::Ssh { entries, .. } => entries, - Self::Wsl { entries, .. } => entries, - } - } -} - -enum Mode { - Default(DefaultState), - ViewServerOptions(ViewServerOptionsState), - EditNickname(EditNicknameState), - ProjectPicker(Entity), - CreateRemoteServer(CreateRemoteServer), - CreateRemoteDevContainer(CreateRemoteDevContainer), - #[cfg(target_os = "windows")] - AddWslDistro(AddWslDistro), -} - -impl Mode { - fn default_mode(ssh_config_servers: &BTreeSet, cx: &mut App) -> Self { - Self::Default(DefaultState::new(ssh_config_servers, cx)) - } -} - -impl RemoteServerProjects { - #[cfg(target_os = "windows")] - pub fn wsl( - create_new_window: bool, - fs: Arc, - window: &mut Window, - workspace: WeakEntity, - cx: &mut Context, - ) -> Self { - Self::new_inner( - Mode::AddWslDistro(AddWslDistro::new(window, cx)), - create_new_window, - fs, - window, - workspace, - cx, - ) - } - - pub fn new( - create_new_window: bool, - fs: Arc, - window: &mut Window, - workspace: WeakEntity, - cx: &mut Context, - ) -> Self { - Self::new_inner( - Mode::default_mode(&BTreeSet::new(), cx), - create_new_window, - fs, - window, - workspace, - cx, - ) - } - - /// Creates a new RemoteServerProjects modal that opens directly in dev container creation mode. - /// Used when suggesting dev container connection from toast notification. - pub fn new_dev_container( - fs: Arc, - window: &mut Window, - workspace: WeakEntity, - cx: &mut Context, - ) -> Self { - Self::new_inner( - Mode::CreateRemoteDevContainer( - CreateRemoteDevContainer::new(window, cx) - .progress(DevContainerCreationProgress::Creating), - ), - false, - fs, - window, - workspace, - cx, - ) - } - - fn new_inner( - mode: Mode, - create_new_window: bool, - fs: Arc, - window: &mut Window, - workspace: WeakEntity, - cx: &mut Context, - ) -> Self { - let focus_handle = cx.focus_handle(); - let mut read_ssh_config = SshSettings::get_global(cx).read_ssh_config; - let ssh_config_updates = if read_ssh_config { - spawn_ssh_config_watch(fs.clone(), cx) - } else { - Task::ready(()) - }; - - let mut base_style = window.text_style(); - base_style.refine(&gpui::TextStyleRefinement { - color: Some(cx.theme().colors().editor_foreground), - ..Default::default() - }); - - let _subscription = - cx.observe_global_in::(window, move |recent_projects, _, cx| { - let new_read_ssh_config = SshSettings::get_global(cx).read_ssh_config; - if read_ssh_config != new_read_ssh_config { - read_ssh_config = new_read_ssh_config; - if read_ssh_config { - recent_projects.ssh_config_updates = spawn_ssh_config_watch(fs.clone(), cx); - } else { - recent_projects.ssh_config_servers.clear(); - recent_projects.ssh_config_updates = Task::ready(()); - } - } - }); - - Self { - mode, - focus_handle, - workspace, - retained_connections: Vec::new(), - ssh_config_updates, - ssh_config_servers: BTreeSet::new(), - create_new_window, - _subscription, - } - } - - fn project_picker( - create_new_window: bool, - index: ServerIndex, - connection_options: remote::RemoteConnectionOptions, - project: Entity, - home_dir: RemotePathBuf, - path_style: PathStyle, - window: &mut Window, - cx: &mut Context, - workspace: WeakEntity, - ) -> Self { - let fs = project.read(cx).fs().clone(); - let mut this = Self::new(create_new_window, fs, window, workspace.clone(), cx); - this.mode = Mode::ProjectPicker(ProjectPicker::new( - create_new_window, - index, - connection_options, - project, - home_dir, - path_style, - workspace, - window, - cx, - )); - cx.notify(); - - this - } - - fn create_ssh_server( - &mut self, - editor: Entity, - window: &mut Window, - cx: &mut Context, - ) { - let input = get_text(&editor, cx); - if input.is_empty() { - return; - } - - let connection_options = match SshConnectionOptions::parse_command_line(&input) { - Ok(c) => c, - Err(e) => { - self.mode = Mode::CreateRemoteServer(CreateRemoteServer { - address_editor: editor, - address_error: Some(format!("could not parse: {:?}", e).into()), - ssh_prompt: None, - _creating: None, - }); - return; - } - }; - let ssh_prompt = cx.new(|cx| { - RemoteConnectionPrompt::new( - connection_options.connection_string(), - connection_options.nickname.clone(), - false, - false, - window, - cx, - ) - }); - - let connection = connect( - ConnectionIdentifier::setup(), - RemoteConnectionOptions::Ssh(connection_options.clone()), - ssh_prompt.clone(), - window, - cx, - ) - .prompt_err("Failed to connect", window, cx, |_, _, _| None); - - let address_editor = editor.clone(); - let creating = cx.spawn_in(window, async move |this, cx| { - match connection.await { - Some(Some(client)) => this - .update_in(cx, |this, window, cx| { - info!("ssh server created"); - telemetry::event!("SSH Server Created"); - this.retained_connections.push(client); - this.add_ssh_server(connection_options, cx); - this.mode = Mode::default_mode(&this.ssh_config_servers, cx); - this.focus_handle(cx).focus(window); - cx.notify() - }) - .log_err(), - _ => this - .update(cx, |this, cx| { - address_editor.update(cx, |this, _| { - this.set_read_only(false); - }); - this.mode = Mode::CreateRemoteServer(CreateRemoteServer { - address_editor, - address_error: None, - ssh_prompt: None, - _creating: None, - }); - cx.notify() - }) - .log_err(), - }; - None - }); - - editor.update(cx, |this, _| { - this.set_read_only(true); - }); - self.mode = Mode::CreateRemoteServer(CreateRemoteServer { - address_editor: editor, - address_error: None, - ssh_prompt: Some(ssh_prompt), - _creating: Some(creating), - }); - } - - #[cfg(target_os = "windows")] - fn connect_wsl_distro( - &mut self, - picker: Entity>, - distro: String, - window: &mut Window, - cx: &mut Context, - ) { - let connection_options = WslConnectionOptions { - distro_name: distro, - user: None, - }; - - let prompt = cx.new(|cx| { - RemoteConnectionPrompt::new( - connection_options.distro_name.clone(), - None, - true, - false, - window, - cx, - ) - }); - let connection = connect( - ConnectionIdentifier::setup(), - connection_options.clone().into(), - prompt.clone(), - window, - cx, - ) - .prompt_err("Failed to connect", window, cx, |_, _, _| None); - - let wsl_picker = picker.clone(); - let creating = cx.spawn_in(window, async move |this, cx| { - match connection.await { - Some(Some(client)) => this.update_in(cx, |this, window, cx| { - telemetry::event!("WSL Distro Added"); - this.retained_connections.push(client); - let Some(fs) = this - .workspace - .read_with(cx, |workspace, cx| { - workspace.project().read(cx).fs().clone() - }) - .log_err() - else { - return; - }; - - crate::add_wsl_distro(fs, &connection_options, cx); - this.mode = Mode::default_mode(&BTreeSet::new(), cx); - this.focus_handle(cx).focus(window); - cx.notify(); - }), - _ => this.update(cx, |this, cx| { - this.mode = Mode::AddWslDistro(AddWslDistro { - picker: wsl_picker, - connection_prompt: None, - _creating: None, - }); - cx.notify(); - }), - } - .log_err(); - }); - - self.mode = Mode::AddWslDistro(AddWslDistro { - picker, - connection_prompt: Some(prompt), - _creating: Some(creating), - }); - } - - fn view_server_options( - &mut self, - (server_index, connection): (ServerIndex, RemoteConnectionOptions), - window: &mut Window, - cx: &mut Context, - ) { - self.mode = Mode::ViewServerOptions(match (server_index, connection) { - (ServerIndex::Ssh(server_index), RemoteConnectionOptions::Ssh(connection)) => { - ViewServerOptionsState::Ssh { - connection, - server_index, - entries: std::array::from_fn(|_| NavigableEntry::focusable(cx)), - } - } - (ServerIndex::Wsl(server_index), RemoteConnectionOptions::Wsl(connection)) => { - ViewServerOptionsState::Wsl { - connection, - server_index, - entries: std::array::from_fn(|_| NavigableEntry::focusable(cx)), - } - } - _ => { - log::error!("server index and connection options mismatch"); - self.mode = Mode::default_mode(&BTreeSet::default(), cx); - return; - } - }); - self.focus_handle(cx).focus(window); - cx.notify(); - } - - fn view_in_progress_dev_container(&mut self, window: &mut Window, cx: &mut Context) { - self.mode = Mode::CreateRemoteDevContainer( - CreateRemoteDevContainer::new(window, cx) - .progress(DevContainerCreationProgress::Creating), - ); - self.focus_handle(cx).focus(window); - cx.notify(); - } - - fn create_remote_project( - &mut self, - index: ServerIndex, - connection_options: RemoteConnectionOptions, - window: &mut Window, - cx: &mut Context, - ) { - let Some(workspace) = self.workspace.upgrade() else { - return; - }; - - let create_new_window = self.create_new_window; - workspace.update(cx, |_, cx| { - cx.defer_in(window, move |workspace, window, cx| { - let app_state = workspace.app_state().clone(); - workspace.toggle_modal(window, cx, |window, cx| { - RemoteConnectionModal::new(&connection_options, Vec::new(), window, cx) - }); - let prompt = workspace - .active_modal::(cx) - .unwrap() - .read(cx) - .prompt - .clone(); - - let connect = connect( - ConnectionIdentifier::setup(), - connection_options.clone(), - prompt, - window, - cx, - ) - .prompt_err("Failed to connect", window, cx, |_, _, _| None); - - cx.spawn_in(window, async move |workspace, cx| { - let session = connect.await; - - workspace.update(cx, |workspace, cx| { - if let Some(prompt) = workspace.active_modal::(cx) { - prompt.update(cx, |prompt, cx| prompt.finished(cx)) - } - })?; - - let Some(Some(session)) = session else { - return workspace.update_in(cx, |workspace, window, cx| { - let weak = cx.entity().downgrade(); - let fs = workspace.project().read(cx).fs().clone(); - workspace.toggle_modal(window, cx, |window, cx| { - RemoteServerProjects::new(create_new_window, fs, window, weak, cx) - }); - }); - }; - - let (path_style, project) = cx.update(|_, cx| { - ( - session.read(cx).path_style(), - project::Project::remote( - session, - app_state.client.clone(), - app_state.node_runtime.clone(), - app_state.user_store.clone(), - app_state.languages.clone(), - app_state.fs.clone(), - cx, - ), - ) - })?; - - let home_dir = project - .read_with(cx, |project, cx| project.resolve_abs_path("~", cx))? - .await - .and_then(|path| path.into_abs_path()) - .map(|path| RemotePathBuf::new(path, path_style)) - .unwrap_or_else(|| match path_style { - PathStyle::Posix => RemotePathBuf::from_str("/", PathStyle::Posix), - PathStyle::Windows => { - RemotePathBuf::from_str("C:\\", PathStyle::Windows) - } - }); - - workspace - .update_in(cx, |workspace, window, cx| { - let weak = cx.entity().downgrade(); - workspace.toggle_modal(window, cx, |window, cx| { - RemoteServerProjects::project_picker( - create_new_window, - index, - connection_options, - project, - home_dir, - path_style, - window, - cx, - weak, - ) - }); - }) - .ok(); - Ok(()) - }) - .detach(); - }) - }) - } - - fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) { - match &self.mode { - Mode::Default(_) | Mode::ViewServerOptions(_) => {} - Mode::ProjectPicker(_) => {} - Mode::CreateRemoteServer(state) => { - if let Some(prompt) = state.ssh_prompt.as_ref() { - prompt.update(cx, |prompt, cx| { - prompt.confirm(window, cx); - }); - return; - } - - self.create_ssh_server(state.address_editor.clone(), window, cx); - } - Mode::CreateRemoteDevContainer(_) => {} - Mode::EditNickname(state) => { - let text = Some(state.editor.read(cx).text(cx)).filter(|text| !text.is_empty()); - let index = state.index; - self.update_settings_file(cx, move |setting, _| { - if let Some(connections) = setting.ssh_connections.as_mut() - && let Some(connection) = connections.get_mut(index.0) - { - connection.nickname = text; - } - }); - self.mode = Mode::default_mode(&self.ssh_config_servers, cx); - self.focus_handle.focus(window); - } - #[cfg(target_os = "windows")] - Mode::AddWslDistro(state) => { - let delegate = &state.picker.read(cx).delegate; - let distro = delegate.selected_distro().unwrap(); - self.connect_wsl_distro(state.picker.clone(), distro, window, cx); - } - } - } - - fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context) { - match &self.mode { - Mode::Default(_) => cx.emit(DismissEvent), - Mode::CreateRemoteServer(state) if state.ssh_prompt.is_some() => { - let new_state = CreateRemoteServer::new(window, cx); - let old_prompt = state.address_editor.read(cx).text(cx); - new_state.address_editor.update(cx, |this, cx| { - this.set_text(old_prompt, window, cx); - }); - - self.mode = Mode::CreateRemoteServer(new_state); - cx.notify(); - } - _ => { - self.mode = Mode::default_mode(&self.ssh_config_servers, cx); - self.focus_handle(cx).focus(window); - cx.notify(); - } - } - } - - fn render_remote_connection( - &mut self, - ix: usize, - remote_server: RemoteEntry, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let connection = remote_server.connection().into_owned(); - - let (main_label, aux_label, is_wsl) = match &connection { - Connection::Ssh(connection) => { - if let Some(nickname) = connection.nickname.clone() { - let aux_label = SharedString::from(format!("({})", connection.host)); - (nickname.into(), Some(aux_label), false) - } else { - (connection.host.clone(), None, false) - } - } - Connection::Wsl(wsl_connection_options) => { - (wsl_connection_options.distro_name.clone(), None, true) - } - Connection::DevContainer(dev_container_options) => { - (dev_container_options.name.clone(), None, false) - } - }; - v_flex() - .w_full() - .child(ListSeparator) - .child( - h_flex() - .group("ssh-server") - .w_full() - .pt_0p5() - .px_3() - .gap_1() - .overflow_hidden() - .child( - h_flex() - .gap_1() - .max_w_96() - .overflow_hidden() - .text_ellipsis() - .when(is_wsl, |this| { - this.child( - Label::new("WSL:") - .size(LabelSize::Small) - .color(Color::Muted), - ) - }) - .child( - Label::new(main_label) - .size(LabelSize::Small) - .color(Color::Muted), - ), - ) - .children( - aux_label.map(|label| { - Label::new(label).size(LabelSize::Small).color(Color::Muted) - }), - ), - ) - .child(match &remote_server { - RemoteEntry::Project { - open_folder, - projects, - configure, - connection, - index, - } => { - let index = *index; - List::new() - .empty_message("No projects.") - .children(projects.iter().enumerate().map(|(pix, p)| { - v_flex().gap_0p5().child(self.render_remote_project( - index, - remote_server.clone(), - pix, - p, - window, - cx, - )) - })) - .child( - h_flex() - .id(("new-remote-project-container", ix)) - .track_focus(&open_folder.focus_handle) - .anchor_scroll(open_folder.scroll_anchor.clone()) - .on_action(cx.listener({ - let connection = connection.clone(); - move |this, _: &menu::Confirm, window, cx| { - this.create_remote_project( - index, - connection.clone().into(), - window, - cx, - ); - } - })) - .child( - ListItem::new(("new-remote-project", ix)) - .toggle_state( - open_folder.focus_handle.contains_focused(window, cx), - ) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Plus).color(Color::Muted)) - .child(Label::new("Open Folder")) - .on_click(cx.listener({ - let connection = connection.clone(); - move |this, _, window, cx| { - this.create_remote_project( - index, - connection.clone().into(), - window, - cx, - ); - } - })), - ), - ) - .child( - h_flex() - .id(("server-options-container", ix)) - .track_focus(&configure.focus_handle) - .anchor_scroll(configure.scroll_anchor.clone()) - .on_action(cx.listener({ - let connection = connection.clone(); - move |this, _: &menu::Confirm, window, cx| { - this.view_server_options( - (index, connection.clone().into()), - window, - cx, - ); - } - })) - .child( - ListItem::new(("server-options", ix)) - .toggle_state( - configure.focus_handle.contains_focused(window, cx), - ) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot( - Icon::new(IconName::Settings).color(Color::Muted), - ) - .child(Label::new("View Server Options")) - .on_click(cx.listener({ - let ssh_connection = connection.clone(); - move |this, _, window, cx| { - this.view_server_options( - (index, ssh_connection.clone().into()), - window, - cx, - ); - } - })), - ), - ) - } - RemoteEntry::SshConfig { open_folder, host } => List::new().child( - h_flex() - .id(("new-remote-project-container", ix)) - .track_focus(&open_folder.focus_handle) - .anchor_scroll(open_folder.scroll_anchor.clone()) - .on_action(cx.listener({ - let connection = connection.clone(); - let host = host.clone(); - move |this, _: &menu::Confirm, window, cx| { - let new_ix = this.create_host_from_ssh_config(&host, cx); - this.create_remote_project( - new_ix.into(), - connection.clone().into(), - window, - cx, - ); - } - })) - .child( - ListItem::new(("new-remote-project", ix)) - .toggle_state(open_folder.focus_handle.contains_focused(window, cx)) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Plus).color(Color::Muted)) - .child(Label::new("Open Folder")) - .on_click(cx.listener({ - let host = host.clone(); - move |this, _, window, cx| { - let new_ix = this.create_host_from_ssh_config(&host, cx); - this.create_remote_project( - new_ix.into(), - connection.clone().into(), - window, - cx, - ); - } - })), - ), - ), - }) - } - - fn render_remote_project( - &mut self, - server_ix: ServerIndex, - server: RemoteEntry, - ix: usize, - (navigation, project): &(NavigableEntry, RemoteProject), - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let create_new_window = self.create_new_window; - let is_from_zed = server.is_from_zed(); - let element_id_base = SharedString::from(format!( - "remote-project-{}", - match server_ix { - ServerIndex::Ssh(index) => format!("ssh-{index}"), - ServerIndex::Wsl(index) => format!("wsl-{index}"), - } - )); - let container_element_id_base = - SharedString::from(format!("remote-project-container-{element_id_base}")); - - let callback = Rc::new({ - let project = project.clone(); - move |remote_server_projects: &mut Self, - secondary_confirm: bool, - window: &mut Window, - cx: &mut Context| { - let Some(app_state) = remote_server_projects - .workspace - .read_with(cx, |workspace, _| workspace.app_state().clone()) - .log_err() - else { - return; - }; - let project = project.clone(); - let server = server.connection().into_owned(); - cx.emit(DismissEvent); - - let replace_window = match (create_new_window, secondary_confirm) { - (true, false) | (false, true) => None, - (true, true) | (false, false) => window.window_handle().downcast::(), - }; - - cx.spawn_in(window, async move |_, cx| { - let result = open_remote_project( - server.into(), - project.paths.into_iter().map(PathBuf::from).collect(), - app_state, - OpenOptions { - replace_window, - ..OpenOptions::default() - }, - cx, - ) - .await; - if let Err(e) = result { - log::error!("Failed to connect: {e:#}"); - cx.prompt( - gpui::PromptLevel::Critical, - "Failed to connect", - Some(&e.to_string()), - &["Ok"], - ) - .await - .ok(); - } - }) - .detach(); - } - }); - - div() - .id((container_element_id_base, ix)) - .track_focus(&navigation.focus_handle) - .anchor_scroll(navigation.scroll_anchor.clone()) - .on_action(cx.listener({ - let callback = callback.clone(); - move |this, _: &menu::Confirm, window, cx| { - callback(this, false, window, cx); - } - })) - .on_action(cx.listener({ - let callback = callback.clone(); - move |this, _: &menu::SecondaryConfirm, window, cx| { - callback(this, true, window, cx); - } - })) - .child( - ListItem::new((element_id_base, ix)) - .toggle_state(navigation.focus_handle.contains_focused(window, cx)) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot( - Icon::new(IconName::Folder) - .color(Color::Muted) - .size(IconSize::Small), - ) - .child(Label::new(project.paths.join(", "))) - .on_click(cx.listener(move |this, e: &ClickEvent, window, cx| { - let secondary_confirm = e.modifiers().platform; - callback(this, secondary_confirm, window, cx) - })) - .when(is_from_zed, |server_list_item| { - server_list_item.end_hover_slot::(Some( - div() - .mr_2() - .child({ - let project = project.clone(); - // Right-margin to offset it from the Scrollbar - IconButton::new("remove-remote-project", IconName::Trash) - .icon_size(IconSize::Small) - .shape(IconButtonShape::Square) - .size(ButtonSize::Large) - .tooltip(Tooltip::text("Delete Remote Project")) - .on_click(cx.listener(move |this, _, _, cx| { - this.delete_remote_project(server_ix, &project, cx) - })) - }) - .into_any_element(), - )) - }), - ) - } - - fn update_settings_file( - &mut self, - cx: &mut Context, - f: impl FnOnce(&mut RemoteSettingsContent, &App) + Send + Sync + 'static, - ) { - let Some(fs) = self - .workspace - .read_with(cx, |workspace, _| workspace.app_state().fs.clone()) - .log_err() - else { - return; - }; - update_settings_file(fs, cx, move |setting, cx| f(&mut setting.remote, cx)); - } - - fn delete_ssh_server(&mut self, server: SshServerIndex, cx: &mut Context) { - self.update_settings_file(cx, move |setting, _| { - if let Some(connections) = setting.ssh_connections.as_mut() { - connections.remove(server.0); - } - }); - } - - fn delete_remote_project( - &mut self, - server: ServerIndex, - project: &RemoteProject, - cx: &mut Context, - ) { - match server { - ServerIndex::Ssh(server) => { - self.delete_ssh_project(server, project, cx); - } - ServerIndex::Wsl(server) => { - self.delete_wsl_project(server, project, cx); - } - } - } - - fn delete_ssh_project( - &mut self, - server: SshServerIndex, - project: &RemoteProject, - cx: &mut Context, - ) { - let project = project.clone(); - self.update_settings_file(cx, move |setting, _| { - if let Some(server) = setting - .ssh_connections - .as_mut() - .and_then(|connections| connections.get_mut(server.0)) - { - server.projects.remove(&project); - } - }); - } - - fn delete_wsl_project( - &mut self, - server: WslServerIndex, - project: &RemoteProject, - cx: &mut Context, - ) { - let project = project.clone(); - self.update_settings_file(cx, move |setting, _| { - if let Some(server) = setting - .wsl_connections - .as_mut() - .and_then(|connections| connections.get_mut(server.0)) - { - server.projects.remove(&project); - } - }); - } - - fn delete_wsl_distro(&mut self, server: WslServerIndex, cx: &mut Context) { - self.update_settings_file(cx, move |setting, _| { - if let Some(connections) = setting.wsl_connections.as_mut() { - connections.remove(server.0); - } - }); - } - - fn add_ssh_server( - &mut self, - connection_options: remote::SshConnectionOptions, - cx: &mut Context, - ) { - self.update_settings_file(cx, move |setting, _| { - setting - .ssh_connections - .get_or_insert(Default::default()) - .push(SshConnection { - host: SharedString::from(connection_options.host), - username: connection_options.username, - port: connection_options.port, - projects: BTreeSet::new(), - nickname: None, - args: connection_options.args.unwrap_or_default(), - upload_binary_over_ssh: None, - port_forwards: connection_options.port_forwards, - }) - }); - } - - fn edit_in_dev_container_json(&mut self, window: &mut Window, cx: &mut Context) { - let Some(workspace) = self.workspace.upgrade() else { - cx.emit(DismissEvent); - cx.notify(); - return; - }; - - workspace.update(cx, |workspace, cx| { - let project = workspace.project().clone(); - - let worktree = project - .read(cx) - .visible_worktrees(cx) - .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree)); - - if let Some(worktree) = worktree { - let tree_id = worktree.read(cx).id(); - let devcontainer_path = RelPath::unix(".devcontainer/devcontainer.json").unwrap(); - cx.spawn_in(window, async move |workspace, cx| { - workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_path( - (tree_id, devcontainer_path), - None, - true, - window, - cx, - ) - })? - .await - }) - .detach(); - } else { - return; - } - }); - cx.emit(DismissEvent); - cx.notify(); - } - - fn open_dev_container(&self, window: &mut Window, cx: &mut Context) { - let Some(app_state) = self - .workspace - .read_with(cx, |workspace, _| workspace.app_state().clone()) - .log_err() - else { - return; - }; - - let replace_window = window.window_handle().downcast::(); - - cx.spawn_in(window, async move |entity, cx| { - let (connection, starting_dir) = - match start_dev_container(cx, app_state.node_runtime.clone()).await { - Ok((c, s)) => (c, s), - Err(e) => { - log::error!("Failed to start dev container: {:?}", e); - entity - .update_in(cx, |remote_server_projects, window, cx| { - remote_server_projects.mode = Mode::CreateRemoteDevContainer( - CreateRemoteDevContainer::new(window, cx).progress( - DevContainerCreationProgress::Error(format!("{:?}", e)), - ), - ); - }) - .log_err(); - return; - } - }; - entity - .update(cx, |_, cx| { - cx.emit(DismissEvent); - }) - .log_err(); - - let result = open_remote_project( - connection.into(), - vec![starting_dir].into_iter().map(PathBuf::from).collect(), - app_state, - OpenOptions { - replace_window, - ..OpenOptions::default() - }, - cx, - ) - .await; - if let Err(e) = result { - log::error!("Failed to connect: {e:#}"); - cx.prompt( - gpui::PromptLevel::Critical, - "Failed to connect", - Some(&e.to_string()), - &["Ok"], - ) - .await - .ok(); - } - }) - .detach(); - } - - fn render_create_dev_container( - &self, - state: &CreateRemoteDevContainer, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - match &state.progress { - DevContainerCreationProgress::Error(message) => { - self.focus_handle(cx).focus(window); - return div() - .track_focus(&self.focus_handle(cx)) - .size_full() - .child( - v_flex() - .py_1() - .child( - ListItem::new("Error") - .inset(true) - .selectable(false) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::XCircle).color(Color::Error)) - .child(Label::new("Error Creating Dev Container:")) - .child(Label::new(message).buffer_font(cx)), - ) - .child(ListSeparator) - .child( - div() - .id("devcontainer-go-back") - .track_focus(&state.entries[0].focus_handle) - .on_action(cx.listener( - |this, _: &menu::Confirm, window, cx| { - this.mode = - Mode::default_mode(&this.ssh_config_servers, cx); - cx.focus_self(window); - cx.notify(); - }, - )) - .child( - ListItem::new("li-devcontainer-go-back") - .toggle_state( - state.entries[0] - .focus_handle - .contains_focused(window, cx), - ) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot( - Icon::new(IconName::ArrowLeft).color(Color::Muted), - ) - .child(Label::new("Go Back")) - .end_slot( - KeyBinding::for_action_in( - &menu::Cancel, - &self.focus_handle, - cx, - ) - .size(rems_from_px(12.)), - ) - .on_click(cx.listener(|this, _, window, cx| { - let state = - CreateRemoteDevContainer::new(window, cx); - this.mode = Mode::CreateRemoteDevContainer(state); - - cx.notify(); - })), - ), - ), - ) - .into_any_element(); - } - _ => {} - }; - - let mut view = Navigable::new( - div() - .track_focus(&self.focus_handle(cx)) - .size_full() - .child( - v_flex() - .pb_1() - .child( - ModalHeader::new() - .child(Headline::new("Dev Containers").size(HeadlineSize::XSmall)), - ) - .child(ListSeparator) - .child( - div() - .id("confirm-create-from-devcontainer-json") - .track_focus(&state.entries[0].focus_handle) - .on_action(cx.listener({ - move |this, _: &menu::Confirm, window, cx| { - this.open_dev_container(window, cx); - this.view_in_progress_dev_container(window, cx); - } - })) - .map(|this| { - if state.progress == DevContainerCreationProgress::Creating { - this.child( - ListItem::new("creating") - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .disabled(true) - .start_slot( - Icon::new(IconName::ArrowCircle) - .color(Color::Muted) - .with_rotate_animation(2), - ) - .child( - h_flex() - .opacity(0.6) - .gap_1() - .child(Label::new("Creating From")) - .child( - Label::new("devcontainer.json") - .buffer_font(cx), - ) - .child(LoadingLabel::new("")), - ), - ) - } else { - this.child( - ListItem::new( - "li-confirm-create-from-devcontainer-json", - ) - .toggle_state( - state.entries[0] - .focus_handle - .contains_focused(window, cx), - ) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot( - Icon::new(IconName::Plus).color(Color::Muted), - ) - .child( - h_flex() - .gap_1() - .child(Label::new("Open or Create New From")) - .child( - Label::new("devcontainer.json") - .buffer_font(cx), - ), - ) - .on_click( - cx.listener({ - move |this, _, window, cx| { - this.open_dev_container(window, cx); - this.view_in_progress_dev_container( - window, cx, - ); - cx.notify(); - } - }), - ), - ) - } - }), - ) - .child( - div() - .id("edit-devcontainer-json") - .track_focus(&state.entries[1].focus_handle) - .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| { - this.edit_in_dev_container_json(window, cx); - })) - .child( - ListItem::new("li-edit-devcontainer-json") - .toggle_state( - state.entries[1] - .focus_handle - .contains_focused(window, cx), - ) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Pencil).color(Color::Muted)) - .child( - h_flex().gap_1().child(Label::new("Edit")).child( - Label::new("devcontainer.json").buffer_font(cx), - ), - ) - .on_click(cx.listener(move |this, _, window, cx| { - this.edit_in_dev_container_json(window, cx); - })), - ), - ) - .child(ListSeparator) - .child( - div() - .id("devcontainer-go-back") - .track_focus(&state.entries[2].focus_handle) - .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| { - this.mode = Mode::default_mode(&this.ssh_config_servers, cx); - cx.focus_self(window); - cx.notify(); - })) - .child( - ListItem::new("li-devcontainer-go-back") - .toggle_state( - state.entries[2] - .focus_handle - .contains_focused(window, cx), - ) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot( - Icon::new(IconName::ArrowLeft).color(Color::Muted), - ) - .child(Label::new("Go Back")) - .end_slot( - KeyBinding::for_action_in( - &menu::Cancel, - &self.focus_handle, - cx, - ) - .size(rems_from_px(12.)), - ) - .on_click(cx.listener(|this, _, window, cx| { - this.mode = - Mode::default_mode(&this.ssh_config_servers, cx); - cx.focus_self(window); - cx.notify() - })), - ), - ), - ) - .into_any_element(), - ); - - view = view.entry(state.entries[0].clone()); - view = view.entry(state.entries[1].clone()); - view = view.entry(state.entries[2].clone()); - - view.render(window, cx).into_any_element() - } - - fn render_create_remote_server( - &self, - state: &CreateRemoteServer, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let ssh_prompt = state.ssh_prompt.clone(); - - state.address_editor.update(cx, |editor, cx| { - if editor.text(cx).is_empty() { - editor.set_placeholder_text("ssh user@example -p 2222", window, cx); - } - }); - - let theme = cx.theme(); - - v_flex() - .track_focus(&self.focus_handle(cx)) - .id("create-remote-server") - .overflow_hidden() - .size_full() - .flex_1() - .child( - div() - .p_2() - .border_b_1() - .border_color(theme.colors().border_variant) - .child(state.address_editor.clone()), - ) - .child( - h_flex() - .bg(theme.colors().editor_background) - .rounded_b_sm() - .w_full() - .map(|this| { - if let Some(ssh_prompt) = ssh_prompt { - this.child(h_flex().w_full().child(ssh_prompt)) - } else if let Some(address_error) = &state.address_error { - this.child( - h_flex().p_2().w_full().gap_2().child( - Label::new(address_error.clone()) - .size(LabelSize::Small) - .color(Color::Error), - ), - ) - } else { - this.child( - h_flex() - .p_2() - .w_full() - .gap_1() - .child( - Label::new( - "Enter the command you use to SSH into this server.", - ) - .color(Color::Muted) - .size(LabelSize::Small), - ) - .child( - Button::new("learn-more", "Learn More") - .label_size(LabelSize::Small) - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::XSmall) - .on_click(|_, _, cx| { - cx.open_url( - "https://zed.dev/docs/remote-development", - ); - }), - ), - ) - } - }), - ) - } - - #[cfg(target_os = "windows")] - fn render_add_wsl_distro( - &self, - state: &AddWslDistro, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let connection_prompt = state.connection_prompt.clone(); - - state.picker.update(cx, |picker, cx| { - picker.focus_handle(cx).focus(window); - }); - - v_flex() - .id("add-wsl-distro") - .overflow_hidden() - .size_full() - .flex_1() - .map(|this| { - if let Some(connection_prompt) = connection_prompt { - this.child(connection_prompt) - } else { - this.child(state.picker.clone()) - } - }) - } - - fn render_view_options( - &mut self, - options: ViewServerOptionsState, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let last_entry = options.entries().last().unwrap(); - - let mut view = Navigable::new( - div() - .track_focus(&self.focus_handle(cx)) - .size_full() - .child(match &options { - ViewServerOptionsState::Ssh { connection, .. } => SshConnectionHeader { - connection_string: connection.host.clone().into(), - paths: Default::default(), - nickname: connection.nickname.clone().map(|s| s.into()), - is_wsl: false, - is_devcontainer: false, - } - .render(window, cx) - .into_any_element(), - ViewServerOptionsState::Wsl { connection, .. } => SshConnectionHeader { - connection_string: connection.distro_name.clone().into(), - paths: Default::default(), - nickname: None, - is_wsl: true, - is_devcontainer: false, - } - .render(window, cx) - .into_any_element(), - }) - .child( - v_flex() - .pb_1() - .child(ListSeparator) - .map(|this| match &options { - ViewServerOptionsState::Ssh { - connection, - entries, - server_index, - } => this.child(self.render_edit_ssh( - connection, - *server_index, - entries, - window, - cx, - )), - ViewServerOptionsState::Wsl { - connection, - entries, - server_index, - } => this.child(self.render_edit_wsl( - connection, - *server_index, - entries, - window, - cx, - )), - }) - .child(ListSeparator) - .child({ - div() - .id("ssh-options-copy-server-address") - .track_focus(&last_entry.focus_handle) - .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| { - this.mode = Mode::default_mode(&this.ssh_config_servers, cx); - cx.focus_self(window); - cx.notify(); - })) - .child( - ListItem::new("go-back") - .toggle_state( - last_entry.focus_handle.contains_focused(window, cx), - ) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot( - Icon::new(IconName::ArrowLeft).color(Color::Muted), - ) - .child(Label::new("Go Back")) - .on_click(cx.listener(|this, _, window, cx| { - this.mode = - Mode::default_mode(&this.ssh_config_servers, cx); - cx.focus_self(window); - cx.notify() - })), - ) - }), - ) - .into_any_element(), - ); - - for entry in options.entries() { - view = view.entry(entry.clone()); - } - - view.render(window, cx).into_any_element() - } - - fn render_edit_wsl( - &self, - connection: &WslConnectionOptions, - index: WslServerIndex, - entries: &[NavigableEntry], - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let distro_name = SharedString::new(connection.distro_name.clone()); - - v_flex().child({ - fn remove_wsl_distro( - remote_servers: Entity, - index: WslServerIndex, - distro_name: SharedString, - window: &mut Window, - cx: &mut App, - ) { - let prompt_message = format!("Remove WSL distro `{}`?", distro_name); - - let confirmation = window.prompt( - PromptLevel::Warning, - &prompt_message, - None, - &["Yes, remove it", "No, keep it"], - cx, - ); - - cx.spawn(async move |cx| { - if confirmation.await.ok() == Some(0) { - remote_servers - .update(cx, |this, cx| { - this.delete_wsl_distro(index, cx); - }) - .ok(); - remote_servers - .update(cx, |this, cx| { - this.mode = Mode::default_mode(&this.ssh_config_servers, cx); - cx.notify(); - }) - .ok(); - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - div() - .id("wsl-options-remove-distro") - .track_focus(&entries[0].focus_handle) - .on_action(cx.listener({ - let distro_name = distro_name.clone(); - move |_, _: &menu::Confirm, window, cx| { - remove_wsl_distro(cx.entity(), index, distro_name.clone(), window, cx); - cx.focus_self(window); - } - })) - .child( - ListItem::new("remove-distro") - .toggle_state(entries[0].focus_handle.contains_focused(window, cx)) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Trash).color(Color::Error)) - .child(Label::new("Remove Distro").color(Color::Error)) - .on_click(cx.listener(move |_, _, window, cx| { - remove_wsl_distro(cx.entity(), index, distro_name.clone(), window, cx); - cx.focus_self(window); - })), - ) - }) - } - - fn render_edit_ssh( - &self, - connection: &SshConnectionOptions, - index: SshServerIndex, - entries: &[NavigableEntry], - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let connection_string = SharedString::new(connection.host.clone()); - - v_flex() - .child({ - let label = if connection.nickname.is_some() { - "Edit Nickname" - } else { - "Add Nickname to Server" - }; - div() - .id("ssh-options-add-nickname") - .track_focus(&entries[0].focus_handle) - .on_action(cx.listener(move |this, _: &menu::Confirm, window, cx| { - this.mode = Mode::EditNickname(EditNicknameState::new(index, window, cx)); - cx.notify(); - })) - .child( - ListItem::new("add-nickname") - .toggle_state(entries[0].focus_handle.contains_focused(window, cx)) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Pencil).color(Color::Muted)) - .child(Label::new(label)) - .on_click(cx.listener(move |this, _, window, cx| { - this.mode = - Mode::EditNickname(EditNicknameState::new(index, window, cx)); - cx.notify(); - })), - ) - }) - .child({ - let workspace = self.workspace.clone(); - fn callback( - workspace: WeakEntity, - connection_string: SharedString, - cx: &mut App, - ) { - cx.write_to_clipboard(ClipboardItem::new_string(connection_string.to_string())); - workspace - .update(cx, |this, cx| { - struct SshServerAddressCopiedToClipboard; - let notification = format!( - "Copied server address ({}) to clipboard", - connection_string - ); - - this.show_toast( - Toast::new( - NotificationId::composite::( - connection_string.clone(), - ), - notification, - ) - .autohide(), - cx, - ); - }) - .ok(); - } - div() - .id("ssh-options-copy-server-address") - .track_focus(&entries[1].focus_handle) - .on_action({ - let connection_string = connection_string.clone(); - let workspace = self.workspace.clone(); - move |_: &menu::Confirm, _, cx| { - callback(workspace.clone(), connection_string.clone(), cx); - } - }) - .child( - ListItem::new("copy-server-address") - .toggle_state(entries[1].focus_handle.contains_focused(window, cx)) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Copy).color(Color::Muted)) - .child(Label::new("Copy Server Address")) - .end_hover_slot( - Label::new(connection_string.clone()).color(Color::Muted), - ) - .on_click({ - let connection_string = connection_string.clone(); - move |_, _, cx| { - callback(workspace.clone(), connection_string.clone(), cx); - } - }), - ) - }) - .child({ - fn remove_ssh_server( - remote_servers: Entity, - index: SshServerIndex, - connection_string: SharedString, - window: &mut Window, - cx: &mut App, - ) { - let prompt_message = format!("Remove server `{}`?", connection_string); - - let confirmation = window.prompt( - PromptLevel::Warning, - &prompt_message, - None, - &["Yes, remove it", "No, keep it"], - cx, - ); - - cx.spawn(async move |cx| { - if confirmation.await.ok() == Some(0) { - remote_servers - .update(cx, |this, cx| { - this.delete_ssh_server(index, cx); - }) - .ok(); - remote_servers - .update(cx, |this, cx| { - this.mode = Mode::default_mode(&this.ssh_config_servers, cx); - cx.notify(); - }) - .ok(); - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - div() - .id("ssh-options-copy-server-address") - .track_focus(&entries[2].focus_handle) - .on_action(cx.listener({ - let connection_string = connection_string.clone(); - move |_, _: &menu::Confirm, window, cx| { - remove_ssh_server( - cx.entity(), - index, - connection_string.clone(), - window, - cx, - ); - cx.focus_self(window); - } - })) - .child( - ListItem::new("remove-server") - .toggle_state(entries[2].focus_handle.contains_focused(window, cx)) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Trash).color(Color::Error)) - .child(Label::new("Remove Server").color(Color::Error)) - .on_click(cx.listener(move |_, _, window, cx| { - remove_ssh_server( - cx.entity(), - index, - connection_string.clone(), - window, - cx, - ); - cx.focus_self(window); - })), - ) - }) - } - - fn render_edit_nickname( - &self, - state: &EditNicknameState, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let Some(connection) = SshSettings::get_global(cx) - .ssh_connections() - .nth(state.index.0) - else { - return v_flex() - .id("ssh-edit-nickname") - .track_focus(&self.focus_handle(cx)); - }; - - let connection_string = connection.host.clone(); - let nickname = connection.nickname.map(|s| s.into()); - - v_flex() - .id("ssh-edit-nickname") - .track_focus(&self.focus_handle(cx)) - .child( - SshConnectionHeader { - connection_string, - paths: Default::default(), - nickname, - is_wsl: false, - is_devcontainer: false, - } - .render(window, cx), - ) - .child( - h_flex() - .p_2() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .child(state.editor.clone()), - ) - } - - fn render_default( - &mut self, - mut state: DefaultState, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let ssh_settings = SshSettings::get_global(cx); - let mut should_rebuild = false; - - let ssh_connections_changed = ssh_settings.ssh_connections.0.iter().ne(state - .servers - .iter() - .filter_map(|server| match server { - RemoteEntry::Project { - connection: Connection::Ssh(connection), - .. - } => Some(connection), - _ => None, - })); - - let wsl_connections_changed = ssh_settings.wsl_connections.0.iter().ne(state - .servers - .iter() - .filter_map(|server| match server { - RemoteEntry::Project { - connection: Connection::Wsl(connection), - .. - } => Some(connection), - _ => None, - })); - - if ssh_connections_changed || wsl_connections_changed { - should_rebuild = true; - }; - - if !should_rebuild && ssh_settings.read_ssh_config { - let current_ssh_hosts: BTreeSet = state - .servers - .iter() - .filter_map(|server| match server { - RemoteEntry::SshConfig { host, .. } => Some(host.clone()), - _ => None, - }) - .collect(); - let mut expected_ssh_hosts = self.ssh_config_servers.clone(); - for server in &state.servers { - if let RemoteEntry::Project { - connection: Connection::Ssh(connection), - .. - } = server - { - expected_ssh_hosts.remove(&connection.host); - } - } - should_rebuild = current_ssh_hosts != expected_ssh_hosts; - } - - if should_rebuild { - self.mode = Mode::default_mode(&self.ssh_config_servers, cx); - if let Mode::Default(new_state) = &self.mode { - state = new_state.clone(); - } - } - - let connect_button = div() - .id("ssh-connect-new-server-container") - .track_focus(&state.add_new_server.focus_handle) - .anchor_scroll(state.add_new_server.scroll_anchor.clone()) - .child( - ListItem::new("register-remote-server-button") - .toggle_state( - state - .add_new_server - .focus_handle - .contains_focused(window, cx), - ) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Plus).color(Color::Muted)) - .child(Label::new("Connect SSH Server")) - .on_click(cx.listener(|this, _, window, cx| { - let state = CreateRemoteServer::new(window, cx); - this.mode = Mode::CreateRemoteServer(state); - - cx.notify(); - })), - ) - .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| { - let state = CreateRemoteServer::new(window, cx); - this.mode = Mode::CreateRemoteServer(state); - - cx.notify(); - })); - - let connect_dev_container_button = div() - .id("connect-new-dev-container") - .track_focus(&state.add_new_devcontainer.focus_handle) - .anchor_scroll(state.add_new_devcontainer.scroll_anchor.clone()) - .child( - ListItem::new("register-dev-container-button") - .toggle_state( - state - .add_new_devcontainer - .focus_handle - .contains_focused(window, cx), - ) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Plus).color(Color::Muted)) - .child(Label::new("Connect Dev Container")) - .on_click(cx.listener(|this, _, window, cx| { - let state = CreateRemoteDevContainer::new(window, cx); - this.mode = Mode::CreateRemoteDevContainer(state); - - cx.notify(); - })), - ) - .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| { - let state = CreateRemoteDevContainer::new(window, cx); - this.mode = Mode::CreateRemoteDevContainer(state); - - cx.notify(); - })); - - #[cfg(target_os = "windows")] - let wsl_connect_button = div() - .id("wsl-connect-new-server") - .track_focus(&state.add_new_wsl.focus_handle) - .anchor_scroll(state.add_new_wsl.scroll_anchor.clone()) - .child( - ListItem::new("wsl-add-new-server") - .toggle_state(state.add_new_wsl.focus_handle.contains_focused(window, cx)) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .start_slot(Icon::new(IconName::Plus).color(Color::Muted)) - .child(Label::new("Add WSL Distro")) - .on_click(cx.listener(|this, _, window, cx| { - let state = AddWslDistro::new(window, cx); - this.mode = Mode::AddWslDistro(state); - - cx.notify(); - })), - ) - .on_action(cx.listener(|this, _: &menu::Confirm, window, cx| { - let state = AddWslDistro::new(window, cx); - this.mode = Mode::AddWslDistro(state); - - cx.notify(); - })); - - let has_open_project = self - .workspace - .upgrade() - .map(|workspace| { - workspace - .read(cx) - .project() - .read(cx) - .visible_worktrees(cx) - .next() - .is_some() - }) - .unwrap_or(false); - - let modal_section = v_flex() - .track_focus(&self.focus_handle(cx)) - .id("ssh-server-list") - .overflow_y_scroll() - .track_scroll(&state.scroll_handle) - .size_full() - .child(connect_button) - .when(has_open_project, |this| { - this.child(connect_dev_container_button) - }); - - #[cfg(target_os = "windows")] - let modal_section = modal_section.child(wsl_connect_button); - #[cfg(not(target_os = "windows"))] - let modal_section = modal_section; - - let mut modal_section = Navigable::new( - modal_section - .child( - List::new() - .empty_message( - h_flex() - .size_full() - .p_2() - .justify_center() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .child( - Label::new("No remote servers registered yet.") - .color(Color::Muted), - ) - .into_any_element(), - ) - .children(state.servers.iter().enumerate().map(|(ix, connection)| { - self.render_remote_connection(ix, connection.clone(), window, cx) - .into_any_element() - })), - ) - .into_any_element(), - ) - .entry(state.add_new_server.clone()); - - if has_open_project { - modal_section = modal_section.entry(state.add_new_devcontainer.clone()); - } - - if cfg!(target_os = "windows") { - modal_section = modal_section.entry(state.add_new_wsl.clone()); - } - - for server in &state.servers { - match server { - RemoteEntry::Project { - open_folder, - projects, - configure, - .. - } => { - for (navigation_state, _) in projects { - modal_section = modal_section.entry(navigation_state.clone()); - } - modal_section = modal_section - .entry(open_folder.clone()) - .entry(configure.clone()); - } - RemoteEntry::SshConfig { open_folder, .. } => { - modal_section = modal_section.entry(open_folder.clone()); - } - } - } - let mut modal_section = modal_section.render(window, cx).into_any_element(); - - let (create_window, reuse_window) = if self.create_new_window { - ( - window.keystroke_text_for(&menu::Confirm), - window.keystroke_text_for(&menu::SecondaryConfirm), - ) - } else { - ( - window.keystroke_text_for(&menu::SecondaryConfirm), - window.keystroke_text_for(&menu::Confirm), - ) - }; - let placeholder_text = Arc::from(format!( - "{reuse_window} reuses this window, {create_window} opens a new one", - )); - - Modal::new("remote-projects", None) - .header( - ModalHeader::new() - .child(Headline::new("Remote Projects").size(HeadlineSize::XSmall)) - .child( - Label::new(placeholder_text) - .color(Color::Muted) - .size(LabelSize::XSmall), - ), - ) - .section( - Section::new().padded(false).child( - v_flex() - .min_h(rems(20.)) - .size_full() - .relative() - .child(ListSeparator) - .child( - canvas( - |bounds, window, cx| { - modal_section.prepaint_as_root( - bounds.origin, - bounds.size.into(), - window, - cx, - ); - modal_section - }, - |_, mut modal_section, window, cx| { - modal_section.paint(window, cx); - }, - ) - .size_full(), - ) - .vertical_scrollbar_for(&state.scroll_handle, window, cx), - ), - ) - .into_any_element() - } - - fn create_host_from_ssh_config( - &mut self, - ssh_config_host: &SharedString, - cx: &mut Context<'_, Self>, - ) -> SshServerIndex { - let new_ix = Arc::new(AtomicUsize::new(0)); - - let update_new_ix = new_ix.clone(); - self.update_settings_file(cx, move |settings, _| { - update_new_ix.store( - settings - .ssh_connections - .as_ref() - .map_or(0, |connections| connections.len()), - atomic::Ordering::Release, - ); - }); - - self.add_ssh_server( - SshConnectionOptions { - host: ssh_config_host.to_string(), - ..SshConnectionOptions::default() - }, - cx, - ); - self.mode = Mode::default_mode(&self.ssh_config_servers, cx); - SshServerIndex(new_ix.load(atomic::Ordering::Acquire)) - } -} - -fn spawn_ssh_config_watch(fs: Arc, cx: &Context) -> Task<()> { - let mut user_ssh_config_watcher = - watch_config_file(cx.background_executor(), fs.clone(), user_ssh_config_file()); - let mut global_ssh_config_watcher = global_ssh_config_file() - .map(|it| watch_config_file(cx.background_executor(), fs, it.to_owned())) - .unwrap_or_else(|| futures::channel::mpsc::unbounded().1); - - cx.spawn(async move |remote_server_projects, cx| { - let mut global_hosts = BTreeSet::default(); - let mut user_hosts = BTreeSet::default(); - let mut running_receivers = 2; - - loop { - select! { - new_global_file_contents = global_ssh_config_watcher.next().fuse() => { - match new_global_file_contents { - Some(new_global_file_contents) => { - global_hosts = parse_ssh_config_hosts(&new_global_file_contents); - if remote_server_projects.update(cx, |remote_server_projects, cx| { - remote_server_projects.ssh_config_servers = global_hosts.iter().chain(user_hosts.iter()).map(SharedString::from).collect(); - cx.notify(); - }).is_err() { - return; - } - }, - None => { - running_receivers -= 1; - if running_receivers == 0 { - return; - } - } - } - }, - new_user_file_contents = user_ssh_config_watcher.next().fuse() => { - match new_user_file_contents { - Some(new_user_file_contents) => { - user_hosts = parse_ssh_config_hosts(&new_user_file_contents); - if remote_server_projects.update(cx, |remote_server_projects, cx| { - remote_server_projects.ssh_config_servers = global_hosts.iter().chain(user_hosts.iter()).map(SharedString::from).collect(); - cx.notify(); - }).is_err() { - return; - } - }, - None => { - running_receivers -= 1; - if running_receivers == 0 { - return; - } - } - } - }, - } - } - }) -} - -fn get_text(element: &Entity, cx: &mut App) -> String { - element.read(cx).text(cx).trim().to_string() -} - -impl ModalView for RemoteServerProjects {} - -impl Focusable for RemoteServerProjects { - fn focus_handle(&self, cx: &App) -> FocusHandle { - match &self.mode { - Mode::ProjectPicker(picker) => picker.focus_handle(cx), - _ => self.focus_handle.clone(), - } - } -} - -impl EventEmitter for RemoteServerProjects {} - -impl Render for RemoteServerProjects { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - div() - .elevation_3(cx) - .w(rems(34.)) - .key_context("RemoteServerModal") - .on_action(cx.listener(Self::cancel)) - .on_action(cx.listener(Self::confirm)) - .capture_any_mouse_down(cx.listener(|this, _, window, cx| { - this.focus_handle(cx).focus(window); - })) - .on_mouse_down_out(cx.listener(|this, _, _, cx| { - if matches!(this.mode, Mode::Default(_)) { - cx.emit(DismissEvent) - } - })) - .child(match &self.mode { - Mode::Default(state) => self - .render_default(state.clone(), window, cx) - .into_any_element(), - Mode::ViewServerOptions(state) => self - .render_view_options(state.clone(), window, cx) - .into_any_element(), - Mode::ProjectPicker(element) => element.clone().into_any_element(), - Mode::CreateRemoteServer(state) => self - .render_create_remote_server(state, window, cx) - .into_any_element(), - Mode::CreateRemoteDevContainer(state) => self - .render_create_dev_container(state, window, cx) - .into_any_element(), - Mode::EditNickname(state) => self - .render_edit_nickname(state, window, cx) - .into_any_element(), - #[cfg(target_os = "windows")] - Mode::AddWslDistro(state) => self - .render_add_wsl_distro(state, window, cx) - .into_any_element(), - }) - } -} diff --git a/crates/recent_projects/src/ssh_config.rs b/crates/recent_projects/src/ssh_config.rs deleted file mode 100644 index f381818205..0000000000 --- a/crates/recent_projects/src/ssh_config.rs +++ /dev/null @@ -1,96 +0,0 @@ -use std::collections::BTreeSet; - -pub fn parse_ssh_config_hosts(config: &str) -> BTreeSet { - let mut hosts = BTreeSet::new(); - let mut needs_another_line = false; - for line in config.lines() { - let line = line.trim_start(); - if let Some(line) = line.strip_prefix("Host") { - match line.chars().next() { - Some('\\') => { - needs_another_line = true; - } - Some('\n' | '\r') => { - needs_another_line = false; - } - Some(c) if c.is_whitespace() => { - parse_hosts_from(line, &mut hosts); - } - Some(_) | None => { - needs_another_line = false; - } - }; - - if needs_another_line { - parse_hosts_from(line, &mut hosts); - needs_another_line = line.trim_end().ends_with('\\'); - } else { - needs_another_line = false; - } - } else if needs_another_line { - needs_another_line = line.trim_end().ends_with('\\'); - parse_hosts_from(line, &mut hosts); - } else { - needs_another_line = false; - } - } - - hosts -} - -fn parse_hosts_from(line: &str, hosts: &mut BTreeSet) { - hosts.extend( - line.split_whitespace() - .filter(|field| !field.starts_with("!")) - .filter(|field| !field.contains("*")) - .filter(|field| !field.is_empty()) - .map(|field| field.to_owned()), - ); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_thank_you_bjorn3() { - let hosts = " - Host * - AddKeysToAgent yes - UseKeychain yes - IdentityFile ~/.ssh/id_ed25519 - - Host whatever.* - User another - - Host !not_this - User not_me - - Host something - HostName whatever.tld - - Host linux bsd host3 - User bjorn - - Host rpi - user rpi - hostname rpi.local - - Host \ - somehost \ - anotherhost - Hostname 192.168.3.3"; - - let expected_hosts = BTreeSet::from_iter([ - "something".to_owned(), - "linux".to_owned(), - "host3".to_owned(), - "bsd".to_owned(), - "rpi".to_owned(), - "somehost".to_owned(), - "anotherhost".to_owned(), - ]); - - assert_eq!(expected_hosts, parse_ssh_config_hosts(hosts)); - } -} diff --git a/crates/recent_projects/src/wsl_picker.rs b/crates/recent_projects/src/wsl_picker.rs deleted file mode 100644 index e386b723fa..0000000000 --- a/crates/recent_projects/src/wsl_picker.rs +++ /dev/null @@ -1,295 +0,0 @@ -use std::{path::PathBuf, sync::Arc}; - -use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Subscription, Task}; -use picker::Picker; -use remote::{RemoteConnectionOptions, WslConnectionOptions}; -use ui::{ - App, Context, HighlightedLabel, Icon, IconName, InteractiveElement, ListItem, ParentElement, - Render, Styled, StyledExt, Toggleable, Window, div, h_flex, rems, v_flex, -}; -use util::ResultExt as _; -use workspace::{ModalView, Workspace}; - -use crate::open_remote_project; - -#[derive(Clone, Debug)] -pub struct WslDistroSelected { - pub secondary: bool, - pub distro: String, -} - -#[derive(Clone, Debug)] -pub struct WslPickerDismissed; - -pub(crate) struct WslPickerDelegate { - selected_index: usize, - distro_list: Option>, - matches: Vec, -} - -impl WslPickerDelegate { - pub fn new() -> Self { - WslPickerDelegate { - selected_index: 0, - distro_list: None, - matches: Vec::new(), - } - } - - pub fn selected_distro(&self) -> Option { - self.matches - .get(self.selected_index) - .map(|m| m.string.clone()) - } -} - -impl WslPickerDelegate { - fn fetch_distros() -> anyhow::Result> { - use anyhow::Context; - use windows_registry::CURRENT_USER; - - let lxss_key = CURRENT_USER - .open("Software\\Microsoft\\Windows\\CurrentVersion\\Lxss") - .context("failed to get lxss wsl key")?; - - let distros = lxss_key - .keys() - .context("failed to get wsl distros")? - .filter_map(|key| { - lxss_key - .open(&key) - .context("failed to open subkey for distro") - .log_err() - }) - .filter_map(|distro| distro.get_string("DistributionName").ok()) - .collect::>(); - - Ok(distros) - } -} - -impl EventEmitter for Picker {} - -impl EventEmitter for Picker {} - -impl picker::PickerDelegate for WslPickerDelegate { - type ListItem = ListItem; - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index( - &mut self, - ix: usize, - _window: &mut Window, - cx: &mut Context>, - ) { - self.selected_index = ix; - cx.notify(); - } - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - Arc::from("Enter WSL distro name") - } - - fn update_matches( - &mut self, - query: String, - _window: &mut Window, - cx: &mut Context>, - ) -> Task<()> { - use fuzzy::StringMatchCandidate; - - let needs_fetch = self.distro_list.is_none(); - if needs_fetch { - let distros = Self::fetch_distros().log_err(); - self.distro_list = distros; - } - - if let Some(distro_list) = &self.distro_list { - use ordered_float::OrderedFloat; - - let candidates = distro_list - .iter() - .enumerate() - .map(|(id, distro)| StringMatchCandidate::new(id, distro)) - .collect::>(); - - let query = query.trim_start(); - let smart_case = query.chars().any(|c| c.is_uppercase()); - self.matches = smol::block_on(fuzzy::match_strings( - candidates.as_slice(), - query, - smart_case, - true, - 100, - &Default::default(), - cx.background_executor().clone(), - )); - self.matches.sort_unstable_by_key(|m| m.candidate_id); - - self.selected_index = self - .matches - .iter() - .enumerate() - .rev() - .max_by_key(|(_, m)| OrderedFloat(m.score)) - .map(|(index, _)| index) - .unwrap_or(0); - } - - Task::ready(()) - } - - fn confirm(&mut self, secondary: bool, _window: &mut Window, cx: &mut Context>) { - if let Some(distro) = self.matches.get(self.selected_index) { - cx.emit(WslDistroSelected { - secondary, - distro: distro.string.clone(), - }); - } - } - - fn dismissed(&mut self, _window: &mut Window, cx: &mut Context>) { - cx.emit(WslPickerDismissed); - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _: &mut Window, - _: &mut Context>, - ) -> Option { - let matched = self.matches.get(ix)?; - Some( - ListItem::new(ix) - .toggle_state(selected) - .inset(true) - .spacing(ui::ListItemSpacing::Sparse) - .child( - h_flex() - .flex_grow() - .gap_3() - .child(Icon::new(IconName::Linux)) - .child(v_flex().child(HighlightedLabel::new( - matched.string.clone(), - matched.positions.clone(), - ))), - ), - ) - } -} - -pub(crate) struct WslOpenModal { - paths: Vec, - create_new_window: bool, - picker: Entity>, - _subscriptions: [Subscription; 2], -} - -impl WslOpenModal { - pub fn new( - paths: Vec, - create_new_window: bool, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let delegate = WslPickerDelegate::new(); - let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false)); - - let selected = cx.subscribe_in( - &picker, - window, - |this, _, event: &WslDistroSelected, window, cx| { - this.confirm(&event.distro, event.secondary, window, cx); - }, - ); - - let dismissed = cx.subscribe_in( - &picker, - window, - |this, _, _: &WslPickerDismissed, window, cx| { - this.cancel(&menu::Cancel, window, cx); - }, - ); - - WslOpenModal { - paths, - create_new_window, - picker, - _subscriptions: [selected, dismissed], - } - } - - fn confirm( - &mut self, - distro: &str, - secondary: bool, - window: &mut Window, - cx: &mut Context, - ) { - let app_state = workspace::AppState::global(cx); - let Some(app_state) = app_state.upgrade() else { - return; - }; - - let connection_options = RemoteConnectionOptions::Wsl(WslConnectionOptions { - distro_name: distro.to_string(), - user: None, - }); - - let replace_current_window = match self.create_new_window { - true => secondary, - false => !secondary, - }; - let replace_window = match replace_current_window { - true => window.window_handle().downcast::(), - false => None, - }; - - let paths = self.paths.clone(); - let open_options = workspace::OpenOptions { - replace_window, - ..Default::default() - }; - - cx.emit(DismissEvent); - cx.spawn_in(window, async move |_, cx| { - open_remote_project(connection_options, paths, app_state, open_options, cx).await - }) - .detach(); - } - - fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { - cx.emit(DismissEvent); - } -} - -impl ModalView for WslOpenModal {} - -impl Focusable for WslOpenModal { - fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { - self.picker.focus_handle(cx) - } -} - -impl EventEmitter for WslOpenModal {} - -impl Render for WslOpenModal { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl ui::IntoElement { - div() - .on_mouse_down_out(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))) - .on_action(cx.listener(Self::cancel)) - .elevation_3(cx) - .w(rems(34.)) - .flex_1() - .overflow_hidden() - .child(self.picker.clone()) - } -} diff --git a/crates/release_channel/Cargo.toml b/crates/release_channel/Cargo.toml deleted file mode 100644 index 54a8afff8c..0000000000 --- a/crates/release_channel/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "release_channel" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[dependencies] -gpui.workspace = true -semver.workspace = true diff --git a/crates/release_channel/LICENSE-GPL b/crates/release_channel/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/release_channel/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/release_channel/src/lib.rs b/crates/release_channel/src/lib.rs deleted file mode 100644 index 65201ccc46..0000000000 --- a/crates/release_channel/src/lib.rs +++ /dev/null @@ -1,232 +0,0 @@ -//! Provides constructs for the Zed app version and release channel. - -#![deny(missing_docs)] - -use std::{env, str::FromStr, sync::LazyLock}; - -use gpui::{App, Global}; -use semver::Version; - -/// stable | dev | nightly | preview -pub static RELEASE_CHANNEL_NAME: LazyLock = LazyLock::new(|| { - if cfg!(debug_assertions) { - env::var("ZED_RELEASE_CHANNEL") - .unwrap_or_else(|_| include_str!("../../zed/RELEASE_CHANNEL").trim().to_string()) - } else { - include_str!("../../zed/RELEASE_CHANNEL").trim().to_string() - } -}); - -#[doc(hidden)] -pub static RELEASE_CHANNEL: LazyLock = - LazyLock::new(|| match ReleaseChannel::from_str(&RELEASE_CHANNEL_NAME) { - Ok(channel) => channel, - _ => panic!("invalid release channel {}", *RELEASE_CHANNEL_NAME), - }); - -/// The app identifier for the current release channel, Windows only. -#[cfg(target_os = "windows")] -pub fn app_identifier() -> &'static str { - match *RELEASE_CHANNEL { - ReleaseChannel::Dev => "Zed-Editor-Dev", - ReleaseChannel::Nightly => "Zed-Editor-Nightly", - ReleaseChannel::Preview => "Zed-Editor-Preview", - ReleaseChannel::Stable => "Zed-Editor-Stable", - } -} - -/// The Git commit SHA that Zed was built at. -#[derive(Clone, Eq, Debug, PartialEq)] -pub struct AppCommitSha(String); - -struct GlobalAppCommitSha(AppCommitSha); - -impl Global for GlobalAppCommitSha {} - -impl AppCommitSha { - /// Creates a new [`AppCommitSha`]. - pub fn new(sha: String) -> Self { - AppCommitSha(sha) - } - - /// Returns the global [`AppCommitSha`], if one is set. - pub fn try_global(cx: &App) -> Option { - cx.try_global::() - .map(|sha| sha.0.clone()) - } - - /// Sets the global [`AppCommitSha`]. - pub fn set_global(sha: AppCommitSha, cx: &mut App) { - cx.set_global(GlobalAppCommitSha(sha)) - } - - /// Returns the full commit SHA. - pub fn full(&self) -> String { - self.0.to_string() - } - - /// Returns the short (7 character) commit SHA. - pub fn short(&self) -> String { - self.0.chars().take(7).collect() - } -} - -struct GlobalAppVersion(Version); - -impl Global for GlobalAppVersion {} - -/// The version of Zed. -pub struct AppVersion; - -impl AppVersion { - /// Load the app version from env. - pub fn load( - pkg_version: &str, - build_id: Option<&str>, - commit_sha: Option, - ) -> Version { - let mut version: Version = if let Ok(from_env) = env::var("ZED_APP_VERSION") { - from_env.parse().expect("invalid ZED_APP_VERSION") - } else { - pkg_version.parse().expect("invalid version in Cargo.toml") - }; - let mut pre = String::from(RELEASE_CHANNEL.dev_name()); - - if let Some(build_id) = build_id { - pre.push('.'); - pre.push_str(&build_id); - } - - if let Some(sha) = commit_sha { - pre.push('.'); - pre.push_str(&sha.0); - } - if let Ok(build) = semver::BuildMetadata::new(&pre) { - version.build = build; - } - - version - } - - /// Returns the global version number. - pub fn global(cx: &App) -> Version { - if cx.has_global::() { - cx.global::().0.clone() - } else { - Version::new(0, 0, 0) - } - } -} - -/// A Zed release channel. -#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] -pub enum ReleaseChannel { - /// The development release channel. - /// - /// Used for local debug builds of Zed. - #[default] - Dev, - - /// The Nightly release channel. - Nightly, - - /// The Preview release channel. - Preview, - - /// The Stable release channel. - Stable, -} - -struct GlobalReleaseChannel(ReleaseChannel); - -impl Global for GlobalReleaseChannel {} - -/// Initializes the release channel. -pub fn init(app_version: Version, cx: &mut App) { - cx.set_global(GlobalAppVersion(app_version)); - cx.set_global(GlobalReleaseChannel(*RELEASE_CHANNEL)) -} - -/// Initializes the release channel for tests that rely on fake release channel. -pub fn init_test(app_version: Version, release_channel: ReleaseChannel, cx: &mut App) { - cx.set_global(GlobalAppVersion(app_version)); - cx.set_global(GlobalReleaseChannel(release_channel)) -} - -impl ReleaseChannel { - /// Returns the global [`ReleaseChannel`]. - pub fn global(cx: &App) -> Self { - cx.global::().0 - } - - /// Returns the global [`ReleaseChannel`], if one is set. - pub fn try_global(cx: &App) -> Option { - cx.try_global::() - .map(|channel| channel.0) - } - - /// Returns whether we want to poll for updates for this [`ReleaseChannel`] - pub fn poll_for_updates(&self) -> bool { - !matches!(self, ReleaseChannel::Dev) - } - - /// Returns the display name for this [`ReleaseChannel`]. - pub fn display_name(&self) -> &'static str { - match self { - ReleaseChannel::Dev => "Zed Dev", - ReleaseChannel::Nightly => "Zed Nightly", - ReleaseChannel::Preview => "Zed Preview", - ReleaseChannel::Stable => "Zed", - } - } - - /// Returns the programmatic name for this [`ReleaseChannel`]. - pub fn dev_name(&self) -> &'static str { - match self { - ReleaseChannel::Dev => "dev", - ReleaseChannel::Nightly => "nightly", - ReleaseChannel::Preview => "preview", - ReleaseChannel::Stable => "stable", - } - } - - /// Returns the application ID that's used by Wayland as application ID - /// and WM_CLASS on X11. - /// This also has to match the bundle identifier for Zed on macOS. - pub fn app_id(&self) -> &'static str { - match self { - ReleaseChannel::Dev => "dev.zed.Zed-Dev", - ReleaseChannel::Nightly => "dev.zed.Zed-Nightly", - ReleaseChannel::Preview => "dev.zed.Zed-Preview", - ReleaseChannel::Stable => "dev.zed.Zed", - } - } - - /// Returns the query parameter for this [`ReleaseChannel`]. - pub fn release_query_param(&self) -> Option<&'static str> { - match self { - Self::Dev => None, - Self::Nightly => Some("nightly=1"), - Self::Preview => Some("preview=1"), - Self::Stable => None, - } - } -} - -/// Error indicating that release channel string does not match any known release channel names. -#[derive(Copy, Clone, Debug, Hash, PartialEq)] -pub struct InvalidReleaseChannel; - -impl FromStr for ReleaseChannel { - type Err = InvalidReleaseChannel; - - fn from_str(channel: &str) -> Result { - Ok(match channel { - "dev" => ReleaseChannel::Dev, - "nightly" => ReleaseChannel::Nightly, - "preview" => ReleaseChannel::Preview, - "stable" => ReleaseChannel::Stable, - _ => return Err(InvalidReleaseChannel), - }) - } -} diff --git a/crates/remote/Cargo.toml b/crates/remote/Cargo.toml deleted file mode 100644 index ae32cd5cb1..0000000000 --- a/crates/remote/Cargo.toml +++ /dev/null @@ -1,48 +0,0 @@ -[package] -name = "remote" -description = "Client-side subsystem for remote editing" -edition.workspace = true -version = "0.1.0" -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/remote.rs" -doctest = false - -[features] -default = [] -test-support = ["fs/test-support"] - -[dependencies] -anyhow.workspace = true -askpass.workspace = true -async-trait.workspace = true -collections.workspace = true -fs.workspace = true -futures.workspace = true -gpui.workspace = true -log.workspace = true -parking_lot.workspace = true -paths.workspace = true -prost.workspace = true -release_channel.workspace = true -rpc = { workspace = true, features = ["gpui"] } -schemars.workspace = true -semver.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -smol.workspace = true -tempfile.workspace = true -thiserror.workspace = true -urlencoding.workspace = true -util.workspace = true -which.workspace = true - -[dev-dependencies] -gpui = { workspace = true, features = ["test-support"] } -fs = { workspace = true, features = ["test-support"] } diff --git a/crates/remote/LICENSE-GPL b/crates/remote/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/remote/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/remote/src/json_log.rs b/crates/remote/src/json_log.rs deleted file mode 100644 index 96cebbb355..0000000000 --- a/crates/remote/src/json_log.rs +++ /dev/null @@ -1,59 +0,0 @@ -use log::{Level, Log, Record}; -use serde::{Deserialize, Serialize}; - -#[derive(Deserialize, Debug, Serialize)] -pub struct LogRecord<'a> { - pub level: usize, - pub module_path: Option<&'a str>, - pub file: Option<&'a str>, - pub line: Option, - pub message: String, -} - -impl<'a> LogRecord<'a> { - pub fn new(record: &'a Record<'a>) -> Self { - Self { - level: serialize_level(record.level()), - module_path: record.module_path(), - file: record.file(), - line: record.line(), - message: record.args().to_string(), - } - } - - pub fn log(&'a self, logger: &dyn Log) { - if let Some(level) = deserialize_level(self.level) { - logger.log( - &log::Record::builder() - .module_path(self.module_path) - .target("remote_server") - .args(format_args!("{}", self.message)) - .file(self.file) - .line(self.line) - .level(level) - .build(), - ) - } - } -} - -fn serialize_level(level: Level) -> usize { - match level { - Level::Error => 1, - Level::Warn => 2, - Level::Info => 3, - Level::Debug => 4, - Level::Trace => 5, - } -} - -fn deserialize_level(level: usize) -> Option { - match level { - 1 => Some(Level::Error), - 2 => Some(Level::Warn), - 3 => Some(Level::Info), - 4 => Some(Level::Debug), - 5 => Some(Level::Trace), - _ => None, - } -} diff --git a/crates/remote/src/protocol.rs b/crates/remote/src/protocol.rs deleted file mode 100644 index 867a31b164..0000000000 --- a/crates/remote/src/protocol.rs +++ /dev/null @@ -1,76 +0,0 @@ -use anyhow::Result; -use futures::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; -use prost::Message as _; -use rpc::proto::Envelope; - -#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)] -pub struct MessageId(pub u32); - -pub type MessageLen = u32; -pub const MESSAGE_LEN_SIZE: usize = size_of::(); - -pub fn message_len_from_buffer(buffer: &[u8]) -> MessageLen { - MessageLen::from_le_bytes(buffer.try_into().unwrap()) -} - -pub async fn read_message_with_len( - stream: &mut S, - buffer: &mut Vec, - message_len: MessageLen, -) -> Result { - buffer.resize(message_len as usize, 0); - stream.read_exact(buffer).await?; - Ok(Envelope::decode(buffer.as_slice())?) -} - -pub async fn read_message( - stream: &mut S, - buffer: &mut Vec, -) -> Result { - buffer.resize(MESSAGE_LEN_SIZE, 0); - stream.read_exact(buffer).await?; - - let len = message_len_from_buffer(buffer); - - read_message_with_len(stream, buffer, len).await -} - -pub async fn write_message( - stream: &mut S, - buffer: &mut Vec, - message: Envelope, -) -> Result<()> { - let message_len = message.encoded_len() as u32; - stream - .write_all(message_len.to_le_bytes().as_slice()) - .await?; - buffer.clear(); - buffer.reserve(message_len as usize); - message.encode(buffer)?; - stream.write_all(buffer).await?; - Ok(()) -} - -pub async fn write_size_prefixed_buffer( - stream: &mut S, - buffer: &mut Vec, -) -> Result<()> { - let len = buffer.len() as u32; - stream.write_all(len.to_le_bytes().as_slice()).await?; - stream.write_all(buffer).await?; - Ok(()) -} - -pub async fn read_message_raw( - stream: &mut S, - buffer: &mut Vec, -) -> Result<()> { - buffer.resize(MESSAGE_LEN_SIZE, 0); - stream.read_exact(buffer).await?; - - let message_len = message_len_from_buffer(buffer); - buffer.resize(message_len as usize, 0); - stream.read_exact(buffer).await?; - - Ok(()) -} diff --git a/crates/remote/src/proxy.rs b/crates/remote/src/proxy.rs deleted file mode 100644 index d715d5ecf6..0000000000 --- a/crates/remote/src/proxy.rs +++ /dev/null @@ -1,25 +0,0 @@ -use thiserror::Error; - -#[derive(Error, Debug)] -pub enum ProxyLaunchError { - #[error("Attempted reconnect, but server not running.")] - ServerNotRunning, -} - -impl ProxyLaunchError { - pub fn to_exit_code(&self) -> i32 { - match self { - // We're using 90 as the exit code, because 0-78 are often taken - // by shells and other conventions and >128 also has certain meanings - // in certain contexts. - Self::ServerNotRunning => 90, - } - } - - pub fn from_exit_code(exit_code: i32) -> Option { - match exit_code { - 90 => Some(Self::ServerNotRunning), - _ => None, - } - } -} diff --git a/crates/remote/src/remote.rs b/crates/remote/src/remote.rs deleted file mode 100644 index 51b71c988a..0000000000 --- a/crates/remote/src/remote.rs +++ /dev/null @@ -1,15 +0,0 @@ -pub mod json_log; -pub mod protocol; -pub mod proxy; -pub mod remote_client; -mod transport; - -#[cfg(target_os = "windows")] -pub use remote_client::OpenWslPath; -pub use remote_client::{ - ConnectionIdentifier, ConnectionState, RemoteClient, RemoteClientDelegate, RemoteClientEvent, - RemoteConnection, RemoteConnectionOptions, RemotePlatform, connect, -}; -pub use transport::docker::DockerConnectionOptions; -pub use transport::ssh::{SshConnectionOptions, SshPortForwardOption}; -pub use transport::wsl::WslConnectionOptions; diff --git a/crates/remote/src/remote_client.rs b/crates/remote/src/remote_client.rs deleted file mode 100644 index e8fa4fe4a3..0000000000 --- a/crates/remote/src/remote_client.rs +++ /dev/null @@ -1,1714 +0,0 @@ -use crate::{ - SshConnectionOptions, - protocol::MessageId, - proxy::ProxyLaunchError, - transport::{ - docker::{DockerConnectionOptions, DockerExecConnection}, - ssh::SshRemoteConnection, - wsl::{WslConnectionOptions, WslRemoteConnection}, - }, -}; -use anyhow::{Context as _, Result, anyhow}; -use askpass::EncryptedPassword; -use async_trait::async_trait; -use collections::HashMap; -use futures::{ - Future, FutureExt as _, StreamExt as _, - channel::{ - mpsc::{self, Sender, UnboundedReceiver, UnboundedSender}, - oneshot, - }, - future::{BoxFuture, Shared}, - select, select_biased, -}; -use gpui::{ - App, AppContext as _, AsyncApp, BackgroundExecutor, BorrowAppContext, Context, Entity, - EventEmitter, FutureExt, Global, Task, WeakEntity, -}; -use parking_lot::Mutex; - -use release_channel::ReleaseChannel; -use rpc::{ - AnyProtoClient, ErrorExt, ProtoClient, ProtoMessageHandlerSet, RpcError, - proto::{self, Envelope, EnvelopedMessage, PeerId, RequestMessage, build_typed_envelope}, -}; -use semver::Version; -use std::{ - collections::VecDeque, - fmt, - ops::ControlFlow, - path::PathBuf, - sync::{ - Arc, Weak, - atomic::{AtomicU32, AtomicU64, Ordering::SeqCst}, - }, - time::{Duration, Instant}, -}; -use util::{ - ResultExt, - paths::{PathStyle, RemotePathBuf}, -}; - -#[derive(Copy, Clone, Debug)] -pub struct RemotePlatform { - pub os: &'static str, - pub arch: &'static str, -} - -#[derive(Clone, Debug)] -pub struct CommandTemplate { - pub program: String, - pub args: Vec, - pub env: HashMap, -} - -pub trait RemoteClientDelegate: Send + Sync { - fn ask_password( - &self, - prompt: String, - tx: oneshot::Sender, - cx: &mut AsyncApp, - ); - fn get_download_url( - &self, - platform: RemotePlatform, - release_channel: ReleaseChannel, - version: Option, - cx: &mut AsyncApp, - ) -> Task>>; - fn download_server_binary_locally( - &self, - platform: RemotePlatform, - release_channel: ReleaseChannel, - version: Option, - cx: &mut AsyncApp, - ) -> Task>; - fn set_status(&self, status: Option<&str>, cx: &mut AsyncApp); -} - -const MAX_MISSED_HEARTBEATS: usize = 5; -const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5); -const HEARTBEAT_TIMEOUT: Duration = Duration::from_secs(5); -const INITIAL_CONNECTION_TIMEOUT: Duration = Duration::from_secs(60); - -const MAX_RECONNECT_ATTEMPTS: usize = 3; - -enum State { - Connecting, - Connected { - remote_connection: Arc, - delegate: Arc, - - multiplex_task: Task>, - heartbeat_task: Task>, - }, - HeartbeatMissed { - missed_heartbeats: usize, - - ssh_connection: Arc, - delegate: Arc, - - multiplex_task: Task>, - heartbeat_task: Task>, - }, - Reconnecting, - ReconnectFailed { - ssh_connection: Arc, - delegate: Arc, - - error: anyhow::Error, - attempts: usize, - }, - ReconnectExhausted, - ServerNotRunning, -} - -impl fmt::Display for State { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Connecting => write!(f, "connecting"), - Self::Connected { .. } => write!(f, "connected"), - Self::Reconnecting => write!(f, "reconnecting"), - Self::ReconnectFailed { .. } => write!(f, "reconnect failed"), - Self::ReconnectExhausted => write!(f, "reconnect exhausted"), - Self::HeartbeatMissed { .. } => write!(f, "heartbeat missed"), - Self::ServerNotRunning { .. } => write!(f, "server not running"), - } - } -} - -impl State { - fn remote_connection(&self) -> Option> { - match self { - Self::Connected { - remote_connection: ssh_connection, - .. - } => Some(ssh_connection.clone()), - Self::HeartbeatMissed { ssh_connection, .. } => Some(ssh_connection.clone()), - Self::ReconnectFailed { ssh_connection, .. } => Some(ssh_connection.clone()), - _ => None, - } - } - - fn can_reconnect(&self) -> bool { - match self { - Self::Connected { .. } - | Self::HeartbeatMissed { .. } - | Self::ReconnectFailed { .. } => true, - State::Connecting - | State::Reconnecting - | State::ReconnectExhausted - | State::ServerNotRunning => false, - } - } - - fn is_reconnect_failed(&self) -> bool { - matches!(self, Self::ReconnectFailed { .. }) - } - - fn is_reconnect_exhausted(&self) -> bool { - matches!(self, Self::ReconnectExhausted { .. }) - } - - fn is_server_not_running(&self) -> bool { - matches!(self, Self::ServerNotRunning) - } - - fn is_reconnecting(&self) -> bool { - matches!(self, Self::Reconnecting { .. }) - } - - fn heartbeat_recovered(self) -> Self { - match self { - Self::HeartbeatMissed { - ssh_connection, - delegate, - multiplex_task, - heartbeat_task, - .. - } => Self::Connected { - remote_connection: ssh_connection, - delegate, - multiplex_task, - heartbeat_task, - }, - _ => self, - } - } - - fn heartbeat_missed(self) -> Self { - match self { - Self::Connected { - remote_connection: ssh_connection, - delegate, - multiplex_task, - heartbeat_task, - } => Self::HeartbeatMissed { - missed_heartbeats: 1, - ssh_connection, - delegate, - multiplex_task, - heartbeat_task, - }, - Self::HeartbeatMissed { - missed_heartbeats, - ssh_connection, - delegate, - multiplex_task, - heartbeat_task, - } => Self::HeartbeatMissed { - missed_heartbeats: missed_heartbeats + 1, - ssh_connection, - delegate, - multiplex_task, - heartbeat_task, - }, - _ => self, - } - } -} - -/// The state of the ssh connection. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum ConnectionState { - Connecting, - Connected, - HeartbeatMissed, - Reconnecting, - Disconnected, -} - -impl From<&State> for ConnectionState { - fn from(value: &State) -> Self { - match value { - State::Connecting => Self::Connecting, - State::Connected { .. } => Self::Connected, - State::Reconnecting | State::ReconnectFailed { .. } => Self::Reconnecting, - State::HeartbeatMissed { .. } => Self::HeartbeatMissed, - State::ReconnectExhausted => Self::Disconnected, - State::ServerNotRunning => Self::Disconnected, - } - } -} - -pub struct RemoteClient { - client: Arc, - unique_identifier: String, - connection_options: RemoteConnectionOptions, - path_style: PathStyle, - state: Option, -} - -#[derive(Debug)] -pub enum RemoteClientEvent { - Disconnected, -} - -impl EventEmitter for RemoteClient {} - -/// Identifies the socket on the remote server so that reconnects -/// can re-join the same project. -pub enum ConnectionIdentifier { - Setup(u64), - Workspace(i64), -} - -static NEXT_ID: AtomicU64 = AtomicU64::new(1); - -impl ConnectionIdentifier { - pub fn setup() -> Self { - Self::Setup(NEXT_ID.fetch_add(1, SeqCst)) - } - - // This string gets used in a socket name, and so must be relatively short. - // The total length of: - // /home/{username}/.local/share/zed/server_state/{name}/stdout.sock - // Must be less than about 100 characters - // https://unix.stackexchange.com/questions/367008/why-is-socket-path-length-limited-to-a-hundred-chars - // So our strings should be at most 20 characters or so. - fn to_string(&self, cx: &App) -> String { - let identifier_prefix = match ReleaseChannel::global(cx) { - ReleaseChannel::Stable => "".to_string(), - release_channel => format!("{}-", release_channel.dev_name()), - }; - match self { - Self::Setup(setup_id) => format!("{identifier_prefix}setup-{setup_id}"), - Self::Workspace(workspace_id) => { - format!("{identifier_prefix}workspace-{workspace_id}",) - } - } - } -} - -pub async fn connect( - connection_options: RemoteConnectionOptions, - delegate: Arc, - cx: &mut AsyncApp, -) -> Result> { - cx.update(|cx| { - cx.update_default_global(|pool: &mut ConnectionPool, cx| { - pool.connect(connection_options.clone(), delegate.clone(), cx) - }) - })? - .await - .map_err(|e| e.cloned()) -} - -impl RemoteClient { - pub fn new( - unique_identifier: ConnectionIdentifier, - remote_connection: Arc, - cancellation: oneshot::Receiver<()>, - delegate: Arc, - cx: &mut App, - ) -> Task>>> { - let unique_identifier = unique_identifier.to_string(cx); - cx.spawn(async move |cx| { - let success = Box::pin(async move { - let (outgoing_tx, outgoing_rx) = mpsc::unbounded::(); - let (incoming_tx, incoming_rx) = mpsc::unbounded::(); - let (connection_activity_tx, connection_activity_rx) = mpsc::channel::<()>(1); - - let client = cx.update(|cx| { - ChannelClient::new( - incoming_rx, - outgoing_tx, - cx, - "client", - remote_connection.has_wsl_interop(), - ) - })?; - - let path_style = remote_connection.path_style(); - let this = cx.new(|_| Self { - client: client.clone(), - unique_identifier: unique_identifier.clone(), - connection_options: remote_connection.connection_options(), - path_style, - state: Some(State::Connecting), - })?; - - let io_task = remote_connection.start_proxy( - unique_identifier, - false, - incoming_tx, - outgoing_rx, - connection_activity_tx, - delegate.clone(), - cx, - ); - - let ready = client - .wait_for_remote_started() - .with_timeout(INITIAL_CONNECTION_TIMEOUT, cx.background_executor()) - .await; - match ready { - Ok(Some(_)) => {} - Ok(None) => { - let mut error = "remote client exited before becoming ready".to_owned(); - if let Some(status) = io_task.now_or_never() { - match status { - Ok(exit_code) => { - error.push_str(&format!(", exit_code={exit_code:?}")) - } - Err(e) => error.push_str(&format!(", error={e:?}")), - } - } - let error = anyhow::anyhow!("{error}"); - log::error!("failed to establish connection: {}", error); - return Err(error); - } - Err(_) => { - let mut error = - "remote client did not become ready within the timeout".to_owned(); - if let Some(status) = io_task.now_or_never() { - match status { - Ok(exit_code) => { - error.push_str(&format!(", exit_code={exit_code:?}")) - } - Err(e) => error.push_str(&format!(", error={e:?}")), - } - } - let error = anyhow::anyhow!("{error}"); - log::error!("failed to establish connection: {}", error); - return Err(error); - } - } - let multiplex_task = Self::monitor(this.downgrade(), io_task, cx); - if let Err(error) = client.ping(HEARTBEAT_TIMEOUT).await { - log::error!("failed to establish connection: {}", error); - return Err(error); - } - - let heartbeat_task = Self::heartbeat(this.downgrade(), connection_activity_rx, cx); - - this.update(cx, |this, _| { - this.state = Some(State::Connected { - remote_connection, - delegate, - multiplex_task, - heartbeat_task, - }); - })?; - - Ok(Some(this)) - }); - - select! { - _ = cancellation.fuse() => { - Ok(None) - } - result = success.fuse() => result - } - }) - } - - pub fn proto_client_from_channels( - incoming_rx: mpsc::UnboundedReceiver, - outgoing_tx: mpsc::UnboundedSender, - cx: &App, - name: &'static str, - has_wsl_interop: bool, - ) -> AnyProtoClient { - ChannelClient::new(incoming_rx, outgoing_tx, cx, name, has_wsl_interop).into() - } - - pub fn shutdown_processes( - &mut self, - shutdown_request: Option, - executor: BackgroundExecutor, - ) -> Option + use> { - let state = self.state.take()?; - log::info!("shutting down ssh processes"); - - let State::Connected { - multiplex_task, - heartbeat_task, - remote_connection: ssh_connection, - delegate, - } = state - else { - return None; - }; - - let client = self.client.clone(); - - Some(async move { - if let Some(shutdown_request) = shutdown_request { - client.send(shutdown_request).log_err(); - // We wait 50ms instead of waiting for a response, because - // waiting for a response would require us to wait on the main thread - // which we want to avoid in an `on_app_quit` callback. - executor.timer(Duration::from_millis(50)).await; - } - - // Drop `multiplex_task` because it owns our ssh_proxy_process, which is a - // child of master_process. - drop(multiplex_task); - // Now drop the rest of state, which kills master process. - drop(heartbeat_task); - drop(ssh_connection); - drop(delegate); - }) - } - - fn reconnect(&mut self, cx: &mut Context) -> Result<()> { - let can_reconnect = self - .state - .as_ref() - .map(|state| state.can_reconnect()) - .unwrap_or(false); - if !can_reconnect { - log::info!("aborting reconnect, because not in state that allows reconnecting"); - let error = if let Some(state) = self.state.as_ref() { - format!("invalid state, cannot reconnect while in state {state}") - } else { - "no state set".to_string() - }; - anyhow::bail!(error); - } - - let state = self.state.take().unwrap(); - let (attempts, remote_connection, delegate) = match state { - State::Connected { - remote_connection: ssh_connection, - delegate, - multiplex_task, - heartbeat_task, - } - | State::HeartbeatMissed { - ssh_connection, - delegate, - multiplex_task, - heartbeat_task, - .. - } => { - drop(multiplex_task); - drop(heartbeat_task); - (0, ssh_connection, delegate) - } - State::ReconnectFailed { - attempts, - ssh_connection, - delegate, - .. - } => (attempts, ssh_connection, delegate), - State::Connecting - | State::Reconnecting - | State::ReconnectExhausted - | State::ServerNotRunning => unreachable!(), - }; - - let attempts = attempts + 1; - if attempts > MAX_RECONNECT_ATTEMPTS { - log::error!( - "Failed to reconnect to after {} attempts, giving up", - MAX_RECONNECT_ATTEMPTS - ); - self.set_state(State::ReconnectExhausted, cx); - return Ok(()); - } - - self.set_state(State::Reconnecting, cx); - - log::info!("Trying to reconnect to ssh server... Attempt {}", attempts); - - let unique_identifier = self.unique_identifier.clone(); - let client = self.client.clone(); - let reconnect_task = cx.spawn(async move |this, cx| { - macro_rules! failed { - ($error:expr, $attempts:expr, $ssh_connection:expr, $delegate:expr) => { - delegate.set_status(Some(&format!("{error:#}", error = $error)), cx); - return State::ReconnectFailed { - error: anyhow!($error), - attempts: $attempts, - ssh_connection: $ssh_connection, - delegate: $delegate, - }; - }; - } - - if let Err(error) = remote_connection - .kill() - .await - .context("Failed to kill ssh process") - { - failed!(error, attempts, remote_connection, delegate); - }; - - let connection_options = remote_connection.connection_options(); - - let (outgoing_tx, outgoing_rx) = mpsc::unbounded::(); - let (incoming_tx, incoming_rx) = mpsc::unbounded::(); - let (connection_activity_tx, connection_activity_rx) = mpsc::channel::<()>(1); - - let (ssh_connection, io_task) = match async { - let ssh_connection = cx - .update_global(|pool: &mut ConnectionPool, cx| { - pool.connect(connection_options, delegate.clone(), cx) - })? - .await - .map_err(|error| error.cloned())?; - - let io_task = ssh_connection.start_proxy( - unique_identifier, - true, - incoming_tx, - outgoing_rx, - connection_activity_tx, - delegate.clone(), - cx, - ); - anyhow::Ok((ssh_connection, io_task)) - } - .await - { - Ok((ssh_connection, io_task)) => (ssh_connection, io_task), - Err(error) => { - failed!(error, attempts, remote_connection, delegate); - } - }; - - let multiplex_task = Self::monitor(this.clone(), io_task, cx); - client.reconnect(incoming_rx, outgoing_tx, cx); - - if let Err(error) = client.resync(HEARTBEAT_TIMEOUT).await { - failed!(error, attempts, ssh_connection, delegate); - }; - - State::Connected { - remote_connection: ssh_connection, - delegate, - multiplex_task, - heartbeat_task: Self::heartbeat(this.clone(), connection_activity_rx, cx), - } - }); - - cx.spawn(async move |this, cx| { - let new_state = reconnect_task.await; - this.update(cx, |this, cx| { - this.try_set_state(cx, |old_state| { - if old_state.is_reconnecting() { - match &new_state { - State::Connecting - | State::Reconnecting - | State::HeartbeatMissed { .. } - | State::ServerNotRunning => {} - State::Connected { .. } => { - log::info!("Successfully reconnected"); - } - State::ReconnectFailed { - error, attempts, .. - } => { - log::error!( - "Reconnect attempt {} failed: {:?}. Starting new attempt...", - attempts, - error - ); - } - State::ReconnectExhausted => { - log::error!("Reconnect attempt failed and all attempts exhausted"); - } - } - Some(new_state) - } else { - None - } - }); - - if this.state_is(State::is_reconnect_failed) { - this.reconnect(cx) - } else if this.state_is(State::is_reconnect_exhausted) { - Ok(()) - } else { - log::debug!("State has transition from Reconnecting into new state while attempting reconnect."); - Ok(()) - } - }) - }) - .detach_and_log_err(cx); - - Ok(()) - } - - fn heartbeat( - this: WeakEntity, - mut connection_activity_rx: mpsc::Receiver<()>, - cx: &mut AsyncApp, - ) -> Task> { - let Ok(client) = this.read_with(cx, |this, _| this.client.clone()) else { - return Task::ready(Err(anyhow!("SshRemoteClient lost"))); - }; - - cx.spawn(async move |cx| { - let mut missed_heartbeats = 0; - - let keepalive_timer = cx.background_executor().timer(HEARTBEAT_INTERVAL).fuse(); - futures::pin_mut!(keepalive_timer); - - loop { - select_biased! { - result = connection_activity_rx.next().fuse() => { - if result.is_none() { - log::warn!("ssh heartbeat: connection activity channel has been dropped. stopping."); - return Ok(()); - } - - if missed_heartbeats != 0 { - missed_heartbeats = 0; - let _ =this.update(cx, |this, cx| { - this.handle_heartbeat_result(missed_heartbeats, cx) - })?; - } - } - _ = keepalive_timer => { - log::debug!("Sending heartbeat to server..."); - - let result = select_biased! { - _ = connection_activity_rx.next().fuse() => { - Ok(()) - } - ping_result = client.ping(HEARTBEAT_TIMEOUT).fuse() => { - ping_result - } - }; - - if result.is_err() { - missed_heartbeats += 1; - log::warn!( - "No heartbeat from server after {:?}. Missed heartbeat {} out of {}.", - HEARTBEAT_TIMEOUT, - missed_heartbeats, - MAX_MISSED_HEARTBEATS - ); - } else if missed_heartbeats != 0 { - missed_heartbeats = 0; - } else { - continue; - } - - let result = this.update(cx, |this, cx| { - this.handle_heartbeat_result(missed_heartbeats, cx) - })?; - if result.is_break() { - return Ok(()); - } - } - } - - keepalive_timer.set(cx.background_executor().timer(HEARTBEAT_INTERVAL).fuse()); - } - }) - } - - fn handle_heartbeat_result( - &mut self, - missed_heartbeats: usize, - cx: &mut Context, - ) -> ControlFlow<()> { - let state = self.state.take().unwrap(); - let next_state = if missed_heartbeats > 0 { - state.heartbeat_missed() - } else { - state.heartbeat_recovered() - }; - - self.set_state(next_state, cx); - - if missed_heartbeats >= MAX_MISSED_HEARTBEATS { - log::error!( - "Missed last {} heartbeats. Reconnecting...", - missed_heartbeats - ); - - self.reconnect(cx) - .context("failed to start reconnect process after missing heartbeats") - .log_err(); - ControlFlow::Break(()) - } else { - ControlFlow::Continue(()) - } - } - - fn monitor( - this: WeakEntity, - io_task: Task>, - cx: &AsyncApp, - ) -> Task> { - cx.spawn(async move |cx| { - let result = io_task.await; - - match result { - Ok(exit_code) => { - if let Some(error) = ProxyLaunchError::from_exit_code(exit_code) { - match error { - ProxyLaunchError::ServerNotRunning => { - log::error!("failed to reconnect because server is not running"); - this.update(cx, |this, cx| { - this.set_state(State::ServerNotRunning, cx); - })?; - } - } - } else if exit_code > 0 { - log::error!("proxy process terminated unexpectedly"); - this.update(cx, |this, cx| { - this.reconnect(cx).ok(); - })?; - } - } - Err(error) => { - log::warn!("ssh io task died with error: {:?}. reconnecting...", error); - this.update(cx, |this, cx| { - this.reconnect(cx).ok(); - })?; - } - } - - Ok(()) - }) - } - - fn state_is(&self, check: impl FnOnce(&State) -> bool) -> bool { - self.state.as_ref().is_some_and(check) - } - - fn try_set_state(&mut self, cx: &mut Context, map: impl FnOnce(&State) -> Option) { - let new_state = self.state.as_ref().and_then(map); - if let Some(new_state) = new_state { - self.state.replace(new_state); - cx.notify(); - } - } - - fn set_state(&mut self, state: State, cx: &mut Context) { - log::info!("setting state to '{}'", &state); - - let is_reconnect_exhausted = state.is_reconnect_exhausted(); - let is_server_not_running = state.is_server_not_running(); - self.state.replace(state); - - if is_reconnect_exhausted || is_server_not_running { - cx.emit(RemoteClientEvent::Disconnected); - } - cx.notify(); - } - - pub fn shell(&self) -> Option { - Some(self.remote_connection()?.shell()) - } - - pub fn default_system_shell(&self) -> Option { - Some(self.remote_connection()?.default_system_shell()) - } - - pub fn shares_network_interface(&self) -> bool { - self.remote_connection() - .map_or(false, |connection| connection.shares_network_interface()) - } - - pub fn build_command( - &self, - program: Option, - args: &[String], - env: &HashMap, - working_dir: Option, - port_forward: Option<(u16, String, u16)>, - ) -> Result { - let Some(connection) = self.remote_connection() else { - return Err(anyhow!("no ssh connection")); - }; - connection.build_command(program, args, env, working_dir, port_forward) - } - - pub fn build_forward_ports_command( - &self, - forwards: Vec<(u16, String, u16)>, - ) -> Result { - let Some(connection) = self.remote_connection() else { - return Err(anyhow!("no ssh connection")); - }; - connection.build_forward_ports_command(forwards) - } - - pub fn upload_directory( - &self, - src_path: PathBuf, - dest_path: RemotePathBuf, - cx: &App, - ) -> Task> { - let Some(connection) = self.remote_connection() else { - return Task::ready(Err(anyhow!("no ssh connection"))); - }; - connection.upload_directory(src_path, dest_path, cx) - } - - pub fn proto_client(&self) -> AnyProtoClient { - self.client.clone().into() - } - - pub fn connection_options(&self) -> RemoteConnectionOptions { - self.connection_options.clone() - } - - pub fn connection(&self) -> Option> { - if let State::Connected { - remote_connection, .. - } = self.state.as_ref()? - { - Some(remote_connection.clone()) - } else { - None - } - } - - pub fn connection_state(&self) -> ConnectionState { - self.state - .as_ref() - .map(ConnectionState::from) - .unwrap_or(ConnectionState::Disconnected) - } - - pub fn is_disconnected(&self) -> bool { - self.connection_state() == ConnectionState::Disconnected - } - - pub fn path_style(&self) -> PathStyle { - self.path_style - } - - #[cfg(any(test, feature = "test-support"))] - pub fn simulate_disconnect(&self, client_cx: &mut App) -> Task<()> { - let opts = self.connection_options(); - client_cx.spawn(async move |cx| { - let connection = cx - .update_global(|c: &mut ConnectionPool, _| { - if let Some(ConnectionPoolEntry::Connecting(c)) = c.connections.get(&opts) { - c.clone() - } else { - panic!("missing test connection") - } - }) - .unwrap() - .await - .unwrap(); - - connection.simulate_disconnect(cx); - }) - } - - #[cfg(any(test, feature = "test-support"))] - pub fn fake_server( - client_cx: &mut gpui::TestAppContext, - server_cx: &mut gpui::TestAppContext, - ) -> (RemoteConnectionOptions, AnyProtoClient) { - let port = client_cx - .update(|cx| cx.default_global::().connections.len() as u16 + 1); - let opts = RemoteConnectionOptions::Ssh(SshConnectionOptions { - host: "".to_string(), - port: Some(port), - ..Default::default() - }); - let (outgoing_tx, _) = mpsc::unbounded::(); - let (_, incoming_rx) = mpsc::unbounded::(); - let server_client = server_cx - .update(|cx| ChannelClient::new(incoming_rx, outgoing_tx, cx, "fake-server", false)); - let connection: Arc = Arc::new(fake::FakeRemoteConnection { - connection_options: opts.clone(), - server_cx: fake::SendableCx::new(server_cx), - server_channel: server_client.clone(), - }); - - client_cx.update(|cx| { - cx.update_default_global(|c: &mut ConnectionPool, cx| { - c.connections.insert( - opts.clone(), - ConnectionPoolEntry::Connecting( - cx.background_spawn({ - let connection = connection.clone(); - async move { Ok(connection.clone()) } - }) - .shared(), - ), - ); - }) - }); - - (opts, server_client.into()) - } - - #[cfg(any(test, feature = "test-support"))] - pub async fn fake_client( - opts: RemoteConnectionOptions, - client_cx: &mut gpui::TestAppContext, - ) -> Entity { - let (_tx, rx) = oneshot::channel(); - let mut cx = client_cx.to_async(); - let connection = connect(opts, Arc::new(fake::Delegate), &mut cx) - .await - .unwrap(); - client_cx - .update(|cx| { - Self::new( - ConnectionIdentifier::setup(), - connection, - rx, - Arc::new(fake::Delegate), - cx, - ) - }) - .await - .unwrap() - .unwrap() - } - - fn remote_connection(&self) -> Option> { - self.state - .as_ref() - .and_then(|state| state.remote_connection()) - } -} - -enum ConnectionPoolEntry { - Connecting(Shared, Arc>>>), - Connected(Weak), -} - -#[derive(Default)] -struct ConnectionPool { - connections: HashMap, -} - -impl Global for ConnectionPool {} - -impl ConnectionPool { - pub fn connect( - &mut self, - opts: RemoteConnectionOptions, - delegate: Arc, - cx: &mut App, - ) -> Shared, Arc>>> { - let connection = self.connections.get(&opts); - match connection { - Some(ConnectionPoolEntry::Connecting(task)) => { - delegate.set_status( - Some("Waiting for existing connection attempt"), - &mut cx.to_async(), - ); - return task.clone(); - } - Some(ConnectionPoolEntry::Connected(ssh)) => { - if let Some(ssh) = ssh.upgrade() - && !ssh.has_been_killed() - { - return Task::ready(Ok(ssh)).shared(); - } - self.connections.remove(&opts); - } - None => {} - } - - let task = cx - .spawn({ - let opts = opts.clone(); - let delegate = delegate.clone(); - async move |cx| { - let connection = match opts.clone() { - RemoteConnectionOptions::Ssh(opts) => { - SshRemoteConnection::new(opts, delegate, cx) - .await - .map(|connection| Arc::new(connection) as Arc) - } - RemoteConnectionOptions::Wsl(opts) => { - WslRemoteConnection::new(opts, delegate, cx) - .await - .map(|connection| Arc::new(connection) as Arc) - } - RemoteConnectionOptions::Docker(opts) => { - DockerExecConnection::new(opts, delegate, cx) - .await - .map(|connection| Arc::new(connection) as Arc) - } - }; - - cx.update_global(|pool: &mut Self, _| { - debug_assert!(matches!( - pool.connections.get(&opts), - Some(ConnectionPoolEntry::Connecting(_)) - )); - match connection { - Ok(connection) => { - pool.connections.insert( - opts.clone(), - ConnectionPoolEntry::Connected(Arc::downgrade(&connection)), - ); - Ok(connection) - } - Err(error) => { - pool.connections.remove(&opts); - Err(Arc::new(error)) - } - } - })? - } - }) - .shared(); - - self.connections - .insert(opts.clone(), ConnectionPoolEntry::Connecting(task.clone())); - task - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum RemoteConnectionOptions { - Ssh(SshConnectionOptions), - Wsl(WslConnectionOptions), - Docker(DockerConnectionOptions), -} - -impl RemoteConnectionOptions { - pub fn display_name(&self) -> String { - match self { - RemoteConnectionOptions::Ssh(opts) => opts.host.clone(), - RemoteConnectionOptions::Wsl(opts) => opts.distro_name.clone(), - RemoteConnectionOptions::Docker(opts) => opts.name.clone(), - } - } -} - -impl From for RemoteConnectionOptions { - fn from(opts: SshConnectionOptions) -> Self { - RemoteConnectionOptions::Ssh(opts) - } -} - -impl From for RemoteConnectionOptions { - fn from(opts: WslConnectionOptions) -> Self { - RemoteConnectionOptions::Wsl(opts) - } -} - -#[cfg(target_os = "windows")] -/// Open a wsl path (\\wsl.localhost\\path) -#[derive(Debug, Clone, PartialEq, Eq, gpui::Action)] -#[action(namespace = workspace, no_json, no_register)] -pub struct OpenWslPath { - pub distro: WslConnectionOptions, - pub paths: Vec, -} - -#[async_trait(?Send)] -pub trait RemoteConnection: Send + Sync { - fn start_proxy( - &self, - unique_identifier: String, - reconnect: bool, - incoming_tx: UnboundedSender, - outgoing_rx: UnboundedReceiver, - connection_activity_tx: Sender<()>, - delegate: Arc, - cx: &mut AsyncApp, - ) -> Task>; - fn upload_directory( - &self, - src_path: PathBuf, - dest_path: RemotePathBuf, - cx: &App, - ) -> Task>; - async fn kill(&self) -> Result<()>; - fn has_been_killed(&self) -> bool; - fn shares_network_interface(&self) -> bool { - false - } - fn build_command( - &self, - program: Option, - args: &[String], - env: &HashMap, - working_dir: Option, - port_forward: Option<(u16, String, u16)>, - ) -> Result; - fn build_forward_ports_command( - &self, - forwards: Vec<(u16, String, u16)>, - ) -> Result; - fn connection_options(&self) -> RemoteConnectionOptions; - fn path_style(&self) -> PathStyle; - fn shell(&self) -> String; - fn default_system_shell(&self) -> String; - fn has_wsl_interop(&self) -> bool; - - #[cfg(any(test, feature = "test-support"))] - fn simulate_disconnect(&self, _: &AsyncApp) {} -} - -type ResponseChannels = Mutex)>>>; - -struct Signal { - tx: Mutex>>, - rx: Shared>>, -} - -impl Signal { - pub fn new(cx: &App) -> Self { - let (tx, rx) = oneshot::channel(); - - let task = cx - .background_executor() - .spawn(async move { rx.await.ok() }) - .shared(); - - Self { - tx: Mutex::new(Some(tx)), - rx: task, - } - } - - fn set(&self, value: T) { - if let Some(tx) = self.tx.lock().take() { - let _ = tx.send(value); - } - } - - fn wait(&self) -> Shared>> { - self.rx.clone() - } -} - -struct ChannelClient { - next_message_id: AtomicU32, - outgoing_tx: Mutex>, - buffer: Mutex>, - response_channels: ResponseChannels, - message_handlers: Mutex, - max_received: AtomicU32, - name: &'static str, - task: Mutex>>, - remote_started: Signal<()>, - has_wsl_interop: bool, -} - -impl ChannelClient { - fn new( - incoming_rx: mpsc::UnboundedReceiver, - outgoing_tx: mpsc::UnboundedSender, - cx: &App, - name: &'static str, - has_wsl_interop: bool, - ) -> Arc { - Arc::new_cyclic(|this| Self { - outgoing_tx: Mutex::new(outgoing_tx), - next_message_id: AtomicU32::new(0), - max_received: AtomicU32::new(0), - response_channels: ResponseChannels::default(), - message_handlers: Default::default(), - buffer: Mutex::new(VecDeque::new()), - name, - task: Mutex::new(Self::start_handling_messages( - this.clone(), - incoming_rx, - &cx.to_async(), - )), - remote_started: Signal::new(cx), - has_wsl_interop, - }) - } - - fn wait_for_remote_started(&self) -> Shared>> { - self.remote_started.wait() - } - - fn start_handling_messages( - this: Weak, - mut incoming_rx: mpsc::UnboundedReceiver, - cx: &AsyncApp, - ) -> Task> { - cx.spawn(async move |cx| { - if let Some(this) = this.upgrade() { - let envelope = proto::RemoteStarted {}.into_envelope(0, None, None); - this.outgoing_tx.lock().unbounded_send(envelope).ok(); - }; - - let peer_id = PeerId { owner_id: 0, id: 0 }; - while let Some(incoming) = incoming_rx.next().await { - let Some(this) = this.upgrade() else { - return anyhow::Ok(()); - }; - if let Some(ack_id) = incoming.ack_id { - let mut buffer = this.buffer.lock(); - while buffer.front().is_some_and(|msg| msg.id <= ack_id) { - buffer.pop_front(); - } - } - if let Some(proto::envelope::Payload::FlushBufferedMessages(_)) = &incoming.payload - { - log::debug!( - "{}:ssh message received. name:FlushBufferedMessages", - this.name - ); - { - let buffer = this.buffer.lock(); - for envelope in buffer.iter() { - this.outgoing_tx - .lock() - .unbounded_send(envelope.clone()) - .ok(); - } - } - let mut envelope = proto::Ack {}.into_envelope(0, Some(incoming.id), None); - envelope.id = this.next_message_id.fetch_add(1, SeqCst); - this.outgoing_tx.lock().unbounded_send(envelope).ok(); - continue; - } - - if let Some(proto::envelope::Payload::RemoteStarted(_)) = &incoming.payload { - this.remote_started.set(()); - let mut envelope = proto::Ack {}.into_envelope(0, Some(incoming.id), None); - envelope.id = this.next_message_id.fetch_add(1, SeqCst); - this.outgoing_tx.lock().unbounded_send(envelope).ok(); - continue; - } - - this.max_received.store(incoming.id, SeqCst); - - if let Some(request_id) = incoming.responding_to { - let request_id = MessageId(request_id); - let sender = this.response_channels.lock().remove(&request_id); - if let Some(sender) = sender { - let (tx, rx) = oneshot::channel(); - if incoming.payload.is_some() { - sender.send((incoming, tx)).ok(); - } - rx.await.ok(); - } - } else if let Some(envelope) = - build_typed_envelope(peer_id, Instant::now(), incoming) - { - let type_name = envelope.payload_type_name(); - let message_id = envelope.message_id(); - if let Some(future) = ProtoMessageHandlerSet::handle_message( - &this.message_handlers, - envelope, - this.clone().into(), - cx.clone(), - ) { - log::debug!("{}:ssh message received. name:{type_name}", this.name); - cx.foreground_executor() - .spawn(async move { - match future.await { - Ok(_) => { - log::debug!( - "{}:ssh message handled. name:{type_name}", - this.name - ); - } - Err(error) => { - log::error!( - "{}:error handling message. type:{}, error:{:#}", - this.name, - type_name, - format!("{error:#}").lines().fold( - String::new(), - |mut message, line| { - if !message.is_empty() { - message.push(' '); - } - message.push_str(line); - message - } - ) - ); - } - } - }) - .detach() - } else { - log::error!("{}:unhandled ssh message name:{type_name}", this.name); - if let Err(e) = AnyProtoClient::from(this.clone()).send_response( - message_id, - anyhow::anyhow!("no handler registered for {type_name}").to_proto(), - ) { - log::error!( - "{}:error sending error response for {type_name}:{e:#}", - this.name - ); - } - } - } - } - anyhow::Ok(()) - }) - } - - fn reconnect( - self: &Arc, - incoming_rx: UnboundedReceiver, - outgoing_tx: UnboundedSender, - cx: &AsyncApp, - ) { - *self.outgoing_tx.lock() = outgoing_tx; - *self.task.lock() = Self::start_handling_messages(Arc::downgrade(self), incoming_rx, cx); - } - - fn request( - &self, - payload: T, - ) -> impl 'static + Future> { - self.request_internal(payload, true) - } - - fn request_internal( - &self, - payload: T, - use_buffer: bool, - ) -> impl 'static + Future> { - log::debug!("ssh request start. name:{}", T::NAME); - let response = - self.request_dynamic(payload.into_envelope(0, None, None), T::NAME, use_buffer); - async move { - let response = response.await?; - log::debug!("ssh request finish. name:{}", T::NAME); - T::Response::from_envelope(response).context("received a response of the wrong type") - } - } - - async fn resync(&self, timeout: Duration) -> Result<()> { - smol::future::or( - async { - self.request_internal(proto::FlushBufferedMessages {}, false) - .await?; - - for envelope in self.buffer.lock().iter() { - self.outgoing_tx - .lock() - .unbounded_send(envelope.clone()) - .ok(); - } - Ok(()) - }, - async { - smol::Timer::after(timeout).await; - anyhow::bail!("Timed out resyncing remote client") - }, - ) - .await - } - - async fn ping(&self, timeout: Duration) -> Result<()> { - smol::future::or( - async { - self.request(proto::Ping {}).await?; - Ok(()) - }, - async { - smol::Timer::after(timeout).await; - anyhow::bail!("Timed out pinging remote client") - }, - ) - .await - } - - fn send(&self, payload: T) -> Result<()> { - log::debug!("ssh send name:{}", T::NAME); - self.send_dynamic(payload.into_envelope(0, None, None)) - } - - fn request_dynamic( - &self, - mut envelope: proto::Envelope, - type_name: &'static str, - use_buffer: bool, - ) -> impl 'static + Future> { - envelope.id = self.next_message_id.fetch_add(1, SeqCst); - let (tx, rx) = oneshot::channel(); - let mut response_channels_lock = self.response_channels.lock(); - response_channels_lock.insert(MessageId(envelope.id), tx); - drop(response_channels_lock); - - let result = if use_buffer { - self.send_buffered(envelope) - } else { - self.send_unbuffered(envelope) - }; - async move { - if let Err(error) = &result { - log::error!("failed to send message: {error}"); - anyhow::bail!("failed to send message: {error}"); - } - - let response = rx.await.context("connection lost")?.0; - if let Some(proto::envelope::Payload::Error(error)) = &response.payload { - return Err(RpcError::from_proto(error, type_name)); - } - Ok(response) - } - } - - pub fn send_dynamic(&self, mut envelope: proto::Envelope) -> Result<()> { - envelope.id = self.next_message_id.fetch_add(1, SeqCst); - self.send_buffered(envelope) - } - - fn send_buffered(&self, mut envelope: proto::Envelope) -> Result<()> { - envelope.ack_id = Some(self.max_received.load(SeqCst)); - self.buffer.lock().push_back(envelope.clone()); - // ignore errors on send (happen while we're reconnecting) - // assume that the global "disconnected" overlay is sufficient. - self.outgoing_tx.lock().unbounded_send(envelope).ok(); - Ok(()) - } - - fn send_unbuffered(&self, mut envelope: proto::Envelope) -> Result<()> { - envelope.ack_id = Some(self.max_received.load(SeqCst)); - self.outgoing_tx.lock().unbounded_send(envelope).ok(); - Ok(()) - } -} - -impl ProtoClient for ChannelClient { - fn request( - &self, - envelope: proto::Envelope, - request_type: &'static str, - ) -> BoxFuture<'static, Result> { - self.request_dynamic(envelope, request_type, true).boxed() - } - - fn send(&self, envelope: proto::Envelope, _message_type: &'static str) -> Result<()> { - self.send_dynamic(envelope) - } - - fn send_response(&self, envelope: Envelope, _message_type: &'static str) -> anyhow::Result<()> { - self.send_dynamic(envelope) - } - - fn message_handler_set(&self) -> &Mutex { - &self.message_handlers - } - - fn is_via_collab(&self) -> bool { - false - } - - fn has_wsl_interop(&self) -> bool { - self.has_wsl_interop - } -} - -#[cfg(any(test, feature = "test-support"))] -mod fake { - use super::{ChannelClient, RemoteClientDelegate, RemoteConnection, RemotePlatform}; - use crate::remote_client::{CommandTemplate, RemoteConnectionOptions}; - use anyhow::Result; - use askpass::EncryptedPassword; - use async_trait::async_trait; - use collections::HashMap; - use futures::{ - FutureExt, SinkExt, StreamExt, - channel::{ - mpsc::{self, Sender}, - oneshot, - }, - select_biased, - }; - use gpui::{App, AppContext as _, AsyncApp, Task, TestAppContext}; - use release_channel::ReleaseChannel; - use rpc::proto::Envelope; - use semver::Version; - use std::{path::PathBuf, sync::Arc}; - use util::paths::{PathStyle, RemotePathBuf}; - - pub(super) struct FakeRemoteConnection { - pub(super) connection_options: RemoteConnectionOptions, - pub(super) server_channel: Arc, - pub(super) server_cx: SendableCx, - } - - pub(super) struct SendableCx(AsyncApp); - impl SendableCx { - // SAFETY: When run in test mode, GPUI is always single threaded. - pub(super) fn new(cx: &TestAppContext) -> Self { - Self(cx.to_async()) - } - - // SAFETY: Enforce that we're on the main thread by requiring a valid AsyncApp - fn get(&self, _: &AsyncApp) -> AsyncApp { - self.0.clone() - } - } - - // SAFETY: There is no way to access a SendableCx from a different thread, see [`SendableCx::new`] and [`SendableCx::get`] - unsafe impl Send for SendableCx {} - unsafe impl Sync for SendableCx {} - - #[async_trait(?Send)] - impl RemoteConnection for FakeRemoteConnection { - async fn kill(&self) -> Result<()> { - Ok(()) - } - - fn has_been_killed(&self) -> bool { - false - } - - fn build_command( - &self, - program: Option, - args: &[String], - env: &HashMap, - _: Option, - _: Option<(u16, String, u16)>, - ) -> Result { - let ssh_program = program.unwrap_or_else(|| "sh".to_string()); - let mut ssh_args = Vec::new(); - ssh_args.push(ssh_program); - ssh_args.extend(args.iter().cloned()); - Ok(CommandTemplate { - program: "ssh".into(), - args: ssh_args, - env: env.clone(), - }) - } - - fn build_forward_ports_command( - &self, - forwards: Vec<(u16, String, u16)>, - ) -> anyhow::Result { - Ok(CommandTemplate { - program: "ssh".into(), - args: std::iter::once("-N".to_owned()) - .chain(forwards.into_iter().map(|(local_port, host, remote_port)| { - format!("{local_port}:{host}:{remote_port}") - })) - .collect(), - env: Default::default(), - }) - } - - fn upload_directory( - &self, - _src_path: PathBuf, - _dest_path: RemotePathBuf, - _cx: &App, - ) -> Task> { - unreachable!() - } - - fn connection_options(&self) -> RemoteConnectionOptions { - self.connection_options.clone() - } - - fn simulate_disconnect(&self, cx: &AsyncApp) { - let (outgoing_tx, _) = mpsc::unbounded::(); - let (_, incoming_rx) = mpsc::unbounded::(); - self.server_channel - .reconnect(incoming_rx, outgoing_tx, &self.server_cx.get(cx)); - } - - fn start_proxy( - &self, - _unique_identifier: String, - _reconnect: bool, - mut client_incoming_tx: mpsc::UnboundedSender, - mut client_outgoing_rx: mpsc::UnboundedReceiver, - mut connection_activity_tx: Sender<()>, - _delegate: Arc, - cx: &mut AsyncApp, - ) -> Task> { - let (mut server_incoming_tx, server_incoming_rx) = mpsc::unbounded::(); - let (server_outgoing_tx, mut server_outgoing_rx) = mpsc::unbounded::(); - - self.server_channel.reconnect( - server_incoming_rx, - server_outgoing_tx, - &self.server_cx.get(cx), - ); - - cx.background_spawn(async move { - loop { - select_biased! { - server_to_client = server_outgoing_rx.next().fuse() => { - let Some(server_to_client) = server_to_client else { - return Ok(1) - }; - connection_activity_tx.try_send(()).ok(); - client_incoming_tx.send(server_to_client).await.ok(); - } - client_to_server = client_outgoing_rx.next().fuse() => { - let Some(client_to_server) = client_to_server else { - return Ok(1) - }; - server_incoming_tx.send(client_to_server).await.ok(); - } - } - } - }) - } - - fn path_style(&self) -> PathStyle { - PathStyle::local() - } - - fn shell(&self) -> String { - "sh".to_owned() - } - - fn default_system_shell(&self) -> String { - "sh".to_owned() - } - - fn has_wsl_interop(&self) -> bool { - false - } - } - - pub(super) struct Delegate; - - impl RemoteClientDelegate for Delegate { - fn ask_password(&self, _: String, _: oneshot::Sender, _: &mut AsyncApp) { - unreachable!() - } - - fn download_server_binary_locally( - &self, - _: RemotePlatform, - _: ReleaseChannel, - _: Option, - _: &mut AsyncApp, - ) -> Task> { - unreachable!() - } - - fn get_download_url( - &self, - _platform: RemotePlatform, - _release_channel: ReleaseChannel, - _version: Option, - _cx: &mut AsyncApp, - ) -> Task>> { - unreachable!() - } - - fn set_status(&self, _: Option<&str>, _: &mut AsyncApp) {} - } -} diff --git a/crates/remote/src/transport.rs b/crates/remote/src/transport.rs deleted file mode 100644 index 4cafbf60ee..0000000000 --- a/crates/remote/src/transport.rs +++ /dev/null @@ -1,428 +0,0 @@ -use crate::{ - RemotePlatform, - json_log::LogRecord, - protocol::{MESSAGE_LEN_SIZE, message_len_from_buffer, read_message_with_len, write_message}, -}; -use anyhow::{Context as _, Result}; -use futures::{ - AsyncReadExt as _, FutureExt as _, StreamExt as _, - channel::mpsc::{Sender, UnboundedReceiver, UnboundedSender}, -}; -use gpui::{AppContext as _, AsyncApp, Task}; -use rpc::proto::Envelope; -use smol::process::Child; - -pub mod docker; -pub mod ssh; -pub mod wsl; - -/// Parses the output of `uname -sm` to determine the remote platform. -/// Takes the last line to skip possible shell initialization output. -fn parse_platform(output: &str) -> Result { - let output = output.trim(); - let uname = output.rsplit_once('\n').map_or(output, |(_, last)| last); - let Some((os, arch)) = uname.split_once(" ") else { - anyhow::bail!("unknown uname: {uname:?}") - }; - - let os = match os { - "Darwin" => "macos", - "Linux" => "linux", - _ => anyhow::bail!( - "Prebuilt remote servers are not yet available for {os:?}. See https://zed.dev/docs/remote-development" - ), - }; - - // exclude armv5,6,7 as they are 32-bit. - let arch = if arch.starts_with("armv8") - || arch.starts_with("armv9") - || arch.starts_with("arm64") - || arch.starts_with("aarch64") - { - "aarch64" - } else if arch.starts_with("x86") { - "x86_64" - } else { - anyhow::bail!( - "Prebuilt remote servers are not yet available for {arch:?}. See https://zed.dev/docs/remote-development" - ) - }; - - Ok(RemotePlatform { os, arch }) -} - -/// Parses the output of `echo $SHELL` to determine the remote shell. -/// Takes the last line to skip possible shell initialization output. -fn parse_shell(output: &str, fallback_shell: &str) -> String { - let output = output.trim(); - let shell = output.rsplit_once('\n').map_or(output, |(_, last)| last); - if shell.is_empty() { - log::error!("$SHELL is not set, falling back to {fallback_shell}"); - fallback_shell.to_owned() - } else { - shell.to_owned() - } -} - -fn handle_rpc_messages_over_child_process_stdio( - mut remote_proxy_process: Child, - incoming_tx: UnboundedSender, - mut outgoing_rx: UnboundedReceiver, - mut connection_activity_tx: Sender<()>, - cx: &AsyncApp, -) -> Task> { - let mut child_stderr = remote_proxy_process.stderr.take().unwrap(); - let mut child_stdout = remote_proxy_process.stdout.take().unwrap(); - let mut child_stdin = remote_proxy_process.stdin.take().unwrap(); - - let mut stdin_buffer = Vec::new(); - let mut stdout_buffer = Vec::new(); - let mut stderr_buffer = Vec::new(); - let mut stderr_offset = 0; - - let stdin_task = cx.background_spawn(async move { - while let Some(outgoing) = outgoing_rx.next().await { - write_message(&mut child_stdin, &mut stdin_buffer, outgoing).await?; - } - anyhow::Ok(()) - }); - - let stdout_task = cx.background_spawn({ - let mut connection_activity_tx = connection_activity_tx.clone(); - async move { - loop { - stdout_buffer.resize(MESSAGE_LEN_SIZE, 0); - let len = child_stdout.read(&mut stdout_buffer).await?; - - if len == 0 { - return anyhow::Ok(()); - } - - if len < MESSAGE_LEN_SIZE { - child_stdout.read_exact(&mut stdout_buffer[len..]).await?; - } - - let message_len = message_len_from_buffer(&stdout_buffer); - let envelope = - read_message_with_len(&mut child_stdout, &mut stdout_buffer, message_len) - .await?; - connection_activity_tx.try_send(()).ok(); - incoming_tx.unbounded_send(envelope).ok(); - } - } - }); - - let stderr_task: Task> = cx.background_spawn(async move { - loop { - stderr_buffer.resize(stderr_offset + 1024, 0); - - let len = child_stderr - .read(&mut stderr_buffer[stderr_offset..]) - .await?; - if len == 0 { - return anyhow::Ok(()); - } - - stderr_offset += len; - let mut start_ix = 0; - while let Some(ix) = stderr_buffer[start_ix..stderr_offset] - .iter() - .position(|b| b == &b'\n') - { - let line_ix = start_ix + ix; - let content = &stderr_buffer[start_ix..line_ix]; - start_ix = line_ix + 1; - if let Ok(record) = serde_json::from_slice::(content) { - record.log(log::logger()) - } else { - eprintln!("(remote) {}", String::from_utf8_lossy(content)); - } - } - stderr_buffer.drain(0..start_ix); - stderr_offset -= start_ix; - - connection_activity_tx.try_send(()).ok(); - } - }); - - cx.background_spawn(async move { - let result = futures::select! { - result = stdin_task.fuse() => { - result.context("stdin") - } - result = stdout_task.fuse() => { - result.context("stdout") - } - result = stderr_task.fuse() => { - result.context("stderr") - } - }; - let status = remote_proxy_process.status().await?.code().unwrap_or(1); - match result { - Ok(_) => Ok(status), - Err(error) => Err(error), - } - }) -} - -#[cfg(debug_assertions)] -async fn build_remote_server_from_source( - platform: &crate::RemotePlatform, - delegate: &dyn crate::RemoteClientDelegate, - cx: &mut AsyncApp, -) -> Result> { - use smol::process::{Command, Stdio}; - use std::env::VarError; - use std::path::Path; - use util::command::new_smol_command; - - // By default, we make building remote server from source opt-out and we do not force artifact compression - // for quicker builds. - let build_remote_server = - std::env::var("ZED_BUILD_REMOTE_SERVER").unwrap_or("nocompress".into()); - - if let "false" | "no" | "off" | "0" = &*build_remote_server { - return Ok(None); - } - - async fn run_cmd(command: &mut Command) -> Result<()> { - let output = command - .kill_on_drop(true) - .stderr(Stdio::inherit()) - .output() - .await?; - anyhow::ensure!( - output.status.success(), - "Failed to run command: {command:?}" - ); - Ok(()) - } - - let use_musl = !build_remote_server.contains("nomusl"); - let triple = format!( - "{}-{}", - platform.arch, - match platform.os { - "linux" => - if use_musl { - "unknown-linux-musl" - } else { - "unknown-linux-gnu" - }, - "macos" => "apple-darwin", - _ => anyhow::bail!("can't cross compile for: {:?}", platform), - } - ); - let mut rust_flags = match std::env::var("RUSTFLAGS") { - Ok(val) => val, - Err(VarError::NotPresent) => String::new(), - Err(e) => { - log::error!("Failed to get env var `RUSTFLAGS` value: {e}"); - String::new() - } - }; - if platform.os == "linux" && use_musl { - rust_flags.push_str(" -C target-feature=+crt-static"); - - if let Ok(path) = std::env::var("ZED_ZSTD_MUSL_LIB") { - rust_flags.push_str(&format!(" -C link-arg=-L{path}")); - } - } - if build_remote_server.contains("mold") { - rust_flags.push_str(" -C link-arg=-fuse-ld=mold"); - } - - if platform.arch == std::env::consts::ARCH && platform.os == std::env::consts::OS { - delegate.set_status(Some("Building remote server binary from source"), cx); - log::info!("building remote server binary from source"); - run_cmd( - new_smol_command("cargo") - .current_dir(concat!(env!("CARGO_MANIFEST_DIR"), "/../..")) - .args([ - "build", - "--package", - "remote_server", - "--features", - "debug-embed", - "--target-dir", - "target/remote_server", - "--target", - &triple, - ]) - .env("RUSTFLAGS", &rust_flags), - ) - .await?; - } else { - if which("zig", cx).await?.is_none() { - anyhow::bail!(if cfg!(not(windows)) { - "zig not found on $PATH, install zig (see https://ziglang.org/learn/getting-started or use zigup)" - } else { - "zig not found on $PATH, install zig (use `winget install -e --id zig.zig` or see https://ziglang.org/learn/getting-started or use zigup)" - }); - } - - let rustup = which("rustup", cx) - .await? - .context("rustup not found on $PATH, install rustup (see https://rustup.rs/)")?; - delegate.set_status(Some("Adding rustup target for cross-compilation"), cx); - log::info!("adding rustup target"); - run_cmd( - new_smol_command(rustup) - .args(["target", "add"]) - .arg(&triple), - ) - .await?; - - if which("cargo-zigbuild", cx).await?.is_none() { - delegate.set_status(Some("Installing cargo-zigbuild for cross-compilation"), cx); - log::info!("installing cargo-zigbuild"); - run_cmd(new_smol_command("cargo").args(["install", "--locked", "cargo-zigbuild"])) - .await?; - } - - delegate.set_status( - Some(&format!( - "Building remote binary from source for {triple} with Zig" - )), - cx, - ); - log::info!("building remote binary from source for {triple} with Zig"); - run_cmd( - new_smol_command("cargo") - .args([ - "zigbuild", - "--package", - "remote_server", - "--features", - "debug-embed", - "--target-dir", - "target/remote_server", - "--target", - &triple, - ]) - .env("RUSTFLAGS", &rust_flags), - ) - .await?; - }; - let bin_path = Path::new("target") - .join("remote_server") - .join(&triple) - .join("debug") - .join("remote_server"); - - let path = if !build_remote_server.contains("nocompress") { - delegate.set_status(Some("Compressing binary"), cx); - - #[cfg(not(target_os = "windows"))] - { - run_cmd(new_smol_command("gzip").args(["-f", &bin_path.to_string_lossy()])).await?; - } - - #[cfg(target_os = "windows")] - { - // On Windows, we use 7z to compress the binary - - let seven_zip = which("7z.exe",cx) - .await? - .context("7z.exe not found on $PATH, install it (e.g. with `winget install -e --id 7zip.7zip`) or, if you don't want this behaviour, set $env:ZED_BUILD_REMOTE_SERVER=\"nocompress\"")?; - let gz_path = format!("target/remote_server/{}/debug/remote_server.gz", triple); - if smol::fs::metadata(&gz_path).await.is_ok() { - smol::fs::remove_file(&gz_path).await?; - } - run_cmd(new_smol_command(seven_zip).args([ - "a", - "-tgzip", - &gz_path, - &bin_path.to_string_lossy(), - ])) - .await?; - } - - let mut archive_path = bin_path; - archive_path.set_extension("gz"); - std::env::current_dir()?.join(archive_path) - } else { - bin_path - }; - - Ok(Some(path)) -} - -#[cfg(debug_assertions)] -async fn which( - binary_name: impl AsRef, - cx: &mut AsyncApp, -) -> Result> { - let binary_name = binary_name.as_ref().to_string(); - let binary_name_cloned = binary_name.clone(); - let res = cx - .background_spawn(async move { which::which(binary_name_cloned) }) - .await; - match res { - Ok(path) => Ok(Some(path)), - Err(which::Error::CannotFindBinaryPath) => Ok(None), - Err(err) => Err(anyhow::anyhow!( - "Failed to run 'which' to find the binary '{binary_name}': {err}" - )), - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_platform() { - let result = parse_platform("Linux x86_64\n").unwrap(); - assert_eq!(result.os, "linux"); - assert_eq!(result.arch, "x86_64"); - - let result = parse_platform("Darwin arm64\n").unwrap(); - assert_eq!(result.os, "macos"); - assert_eq!(result.arch, "aarch64"); - - let result = parse_platform("Linux x86_64").unwrap(); - assert_eq!(result.os, "linux"); - assert_eq!(result.arch, "x86_64"); - - let result = parse_platform("some shell init output\nLinux aarch64\n").unwrap(); - assert_eq!(result.os, "linux"); - assert_eq!(result.arch, "aarch64"); - - let result = parse_platform("some shell init output\nLinux aarch64").unwrap(); - assert_eq!(result.os, "linux"); - assert_eq!(result.arch, "aarch64"); - - assert_eq!(parse_platform("Linux armv8l\n").unwrap().arch, "aarch64"); - assert_eq!(parse_platform("Linux aarch64\n").unwrap().arch, "aarch64"); - assert_eq!(parse_platform("Linux x86_64\n").unwrap().arch, "x86_64"); - - let result = parse_platform( - r#"Linux x86_64 - What you're referring to as Linux, is in fact, GNU/Linux...\n"#, - ) - .unwrap(); - assert_eq!(result.os, "linux"); - assert_eq!(result.arch, "x86_64"); - - assert!(parse_platform("Windows x86_64\n").is_err()); - assert!(parse_platform("Linux armv7l\n").is_err()); - } - - #[test] - fn test_parse_shell() { - assert_eq!(parse_shell("/bin/bash\n", "sh"), "/bin/bash"); - assert_eq!(parse_shell("/bin/zsh\n", "sh"), "/bin/zsh"); - - assert_eq!(parse_shell("/bin/bash", "sh"), "/bin/bash"); - assert_eq!( - parse_shell("some shell init output\n/bin/bash\n", "sh"), - "/bin/bash" - ); - assert_eq!( - parse_shell("some shell init output\n/bin/bash", "sh"), - "/bin/bash" - ); - assert_eq!(parse_shell("", "sh"), "sh"); - assert_eq!(parse_shell("\n", "sh"), "sh"); - } -} diff --git a/crates/remote/src/transport/docker.rs b/crates/remote/src/transport/docker.rs deleted file mode 100644 index 09f5935ec6..0000000000 --- a/crates/remote/src/transport/docker.rs +++ /dev/null @@ -1,757 +0,0 @@ -use anyhow::Context; -use anyhow::Result; -use anyhow::anyhow; -use async_trait::async_trait; -use collections::HashMap; -use parking_lot::Mutex; -use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; -use semver::Version as SemanticVersion; -use std::time::Instant; -use std::{ - path::{Path, PathBuf}, - process::Stdio, - sync::Arc, -}; -use util::ResultExt; -use util::shell::ShellKind; -use util::{ - paths::{PathStyle, RemotePathBuf}, - rel_path::RelPath, -}; - -use futures::channel::mpsc::{Sender, UnboundedReceiver, UnboundedSender}; -use gpui::{App, AppContext, AsyncApp, Task}; -use rpc::proto::Envelope; - -use crate::{ - RemoteClientDelegate, RemoteConnection, RemoteConnectionOptions, RemotePlatform, - remote_client::CommandTemplate, -}; - -#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] -pub struct DockerConnectionOptions { - pub name: String, - pub container_id: String, - pub upload_binary_over_docker_exec: bool, -} - -pub(crate) struct DockerExecConnection { - proxy_process: Mutex>, - remote_dir_for_server: String, - remote_binary_relpath: Option>, - connection_options: DockerConnectionOptions, - remote_platform: Option, - path_style: Option, - shell: Option, -} - -impl DockerExecConnection { - pub async fn new( - connection_options: DockerConnectionOptions, - delegate: Arc, - cx: &mut AsyncApp, - ) -> Result { - let mut this = Self { - proxy_process: Mutex::new(None), - remote_dir_for_server: "/".to_string(), - remote_binary_relpath: None, - connection_options, - remote_platform: None, - path_style: None, - shell: None, - }; - let (release_channel, version, commit) = cx.update(|cx| { - ( - ReleaseChannel::global(cx), - AppVersion::global(cx), - AppCommitSha::try_global(cx), - ) - })?; - let remote_platform = this.check_remote_platform().await?; - - this.path_style = match remote_platform.os { - "windows" => Some(PathStyle::Windows), - _ => Some(PathStyle::Posix), - }; - - this.remote_platform = Some(remote_platform); - - this.shell = Some(this.discover_shell().await); - - this.remote_dir_for_server = this.docker_user_home_dir().await?.trim().to_string(); - - this.remote_binary_relpath = Some( - this.ensure_server_binary( - &delegate, - release_channel, - version, - &this.remote_dir_for_server, - commit, - cx, - ) - .await?, - ); - - Ok(this) - } - - async fn discover_shell(&self) -> String { - let default_shell = "sh"; - match self - .run_docker_exec("sh", None, &Default::default(), &["-c", "echo $SHELL"]) - .await - { - Ok(shell) => match shell.trim() { - "" => { - log::error!("$SHELL is not set, falling back to {default_shell}"); - default_shell.to_owned() - } - shell => shell.to_owned(), - }, - Err(e) => { - log::error!("Failed to get shell: {e}"); - default_shell.to_owned() - } - } - } - - async fn check_remote_platform(&self) -> Result { - let uname = self - .run_docker_exec("uname", None, &Default::default(), &["-sm"]) - .await?; - let Some((os, arch)) = uname.split_once(" ") else { - anyhow::bail!("unknown uname: {uname:?}") - }; - - let os = match os.trim() { - "Darwin" => "macos", - "Linux" => "linux", - _ => anyhow::bail!( - "Prebuilt remote servers are not yet available for {os:?}. See https://zed.dev/docs/remote-development" - ), - }; - // exclude armv5,6,7 as they are 32-bit. - let arch = if arch.starts_with("armv8") - || arch.starts_with("armv9") - || arch.starts_with("arm64") - || arch.starts_with("aarch64") - { - "aarch64" - } else if arch.starts_with("x86") { - "x86_64" - } else { - anyhow::bail!( - "Prebuilt remote servers are not yet available for {arch:?}. See https://zed.dev/docs/remote-development" - ) - }; - - Ok(RemotePlatform { os, arch }) - } - - async fn ensure_server_binary( - &self, - delegate: &Arc, - release_channel: ReleaseChannel, - version: SemanticVersion, - remote_dir_for_server: &str, - commit: Option, - cx: &mut AsyncApp, - ) -> Result> { - let remote_platform = if self.remote_platform.is_some() { - self.remote_platform.unwrap() - } else { - anyhow::bail!("No remote platform defined; cannot proceed.") - }; - - let version_str = match release_channel { - ReleaseChannel::Nightly => { - let commit = commit.map(|s| s.full()).unwrap_or_default(); - format!("{}-{}", version, commit) - } - ReleaseChannel::Dev => "build".to_string(), - _ => version.to_string(), - }; - let binary_name = format!( - "zed-remote-server-{}-{}", - release_channel.dev_name(), - version_str - ); - let dst_path = - paths::remote_server_dir_relative().join(RelPath::unix(&binary_name).unwrap()); - - #[cfg(debug_assertions)] - if let Some(remote_server_path) = - super::build_remote_server_from_source(&remote_platform, delegate.as_ref(), cx).await? - { - let tmp_path = paths::remote_server_dir_relative().join( - RelPath::unix(&format!( - "download-{}-{}", - std::process::id(), - remote_server_path.file_name().unwrap().to_string_lossy() - )) - .unwrap(), - ); - self.upload_local_server_binary( - &remote_server_path, - &tmp_path, - &remote_dir_for_server, - delegate, - cx, - ) - .await?; - self.extract_server_binary(&dst_path, &tmp_path, &remote_dir_for_server, delegate, cx) - .await?; - return Ok(dst_path); - } - - if self - .run_docker_exec( - &dst_path.display(self.path_style()), - Some(&remote_dir_for_server), - &Default::default(), - &["version"], - ) - .await - .is_ok() - { - return Ok(dst_path); - } - - let wanted_version = cx.update(|cx| match release_channel { - ReleaseChannel::Nightly => Ok(None), - ReleaseChannel::Dev => { - anyhow::bail!( - "ZED_BUILD_REMOTE_SERVER is not set and no remote server exists at ({:?})", - dst_path - ) - } - _ => Ok(Some(AppVersion::global(cx))), - })??; - - let tmp_path_gz = paths::remote_server_dir_relative().join( - RelPath::unix(&format!( - "{}-download-{}.gz", - binary_name, - std::process::id() - )) - .unwrap(), - ); - if !self.connection_options.upload_binary_over_docker_exec - && let Some(url) = delegate - .get_download_url(remote_platform, release_channel, wanted_version.clone(), cx) - .await? - { - match self - .download_binary_on_server(&url, &tmp_path_gz, &remote_dir_for_server, delegate, cx) - .await - { - Ok(_) => { - self.extract_server_binary( - &dst_path, - &tmp_path_gz, - &remote_dir_for_server, - delegate, - cx, - ) - .await - .context("extracting server binary")?; - return Ok(dst_path); - } - Err(e) => { - log::error!( - "Failed to download binary on server, attempting to download locally and then upload it the server: {e:#}", - ) - } - } - } - - let src_path = delegate - .download_server_binary_locally(remote_platform, release_channel, wanted_version, cx) - .await - .context("downloading server binary locally")?; - self.upload_local_server_binary( - &src_path, - &tmp_path_gz, - &remote_dir_for_server, - delegate, - cx, - ) - .await - .context("uploading server binary")?; - self.extract_server_binary( - &dst_path, - &tmp_path_gz, - &remote_dir_for_server, - delegate, - cx, - ) - .await - .context("extracting server binary")?; - Ok(dst_path) - } - - async fn docker_user_home_dir(&self) -> Result { - let inner_program = self.shell(); - self.run_docker_exec( - &inner_program, - None, - &Default::default(), - &["-c", "echo $HOME"], - ) - .await - } - - async fn extract_server_binary( - &self, - dst_path: &RelPath, - tmp_path: &RelPath, - remote_dir_for_server: &str, - delegate: &Arc, - cx: &mut AsyncApp, - ) -> Result<()> { - delegate.set_status(Some("Extracting remote development server"), cx); - let server_mode = 0o755; - - let shell_kind = ShellKind::Posix; - let orig_tmp_path = tmp_path.display(self.path_style()); - let server_mode = format!("{:o}", server_mode); - let server_mode = shell_kind - .try_quote(&server_mode) - .context("shell quoting")?; - let dst_path = dst_path.display(self.path_style()); - let dst_path = shell_kind.try_quote(&dst_path).context("shell quoting")?; - let script = if let Some(tmp_path) = orig_tmp_path.strip_suffix(".gz") { - let orig_tmp_path = shell_kind - .try_quote(&orig_tmp_path) - .context("shell quoting")?; - let tmp_path = shell_kind.try_quote(&tmp_path).context("shell quoting")?; - format!( - "gunzip -f {orig_tmp_path} && chmod {server_mode} {tmp_path} && mv {tmp_path} {dst_path}", - ) - } else { - let orig_tmp_path = shell_kind - .try_quote(&orig_tmp_path) - .context("shell quoting")?; - format!("chmod {server_mode} {orig_tmp_path} && mv {orig_tmp_path} {dst_path}",) - }; - let args = shell_kind.args_for_shell(false, script.to_string()); - self.run_docker_exec( - "sh", - Some(&remote_dir_for_server), - &Default::default(), - &args, - ) - .await - .log_err(); - Ok(()) - } - - async fn upload_local_server_binary( - &self, - src_path: &Path, - tmp_path_gz: &RelPath, - remote_dir_for_server: &str, - delegate: &Arc, - cx: &mut AsyncApp, - ) -> Result<()> { - if let Some(parent) = tmp_path_gz.parent() { - self.run_docker_exec( - "mkdir", - Some(remote_dir_for_server), - &Default::default(), - &["-p", parent.display(self.path_style()).as_ref()], - ) - .await?; - } - - let src_stat = smol::fs::metadata(&src_path).await?; - let size = src_stat.len(); - - let t0 = Instant::now(); - delegate.set_status(Some("Uploading remote development server"), cx); - log::info!( - "uploading remote development server to {:?} ({}kb)", - tmp_path_gz, - size / 1024 - ); - self.upload_file(src_path, tmp_path_gz, remote_dir_for_server) - .await - .context("failed to upload server binary")?; - log::info!("uploaded remote development server in {:?}", t0.elapsed()); - Ok(()) - } - - async fn upload_file( - &self, - src_path: &Path, - dest_path: &RelPath, - remote_dir_for_server: &str, - ) -> Result<()> { - log::debug!("uploading file {:?} to {:?}", src_path, dest_path); - - let src_path_display = src_path.display().to_string(); - let dest_path_str = dest_path.display(self.path_style()); - - let mut command = util::command::new_smol_command("docker"); - command.arg("cp"); - command.arg("-a"); - command.arg(&src_path_display); - command.arg(format!( - "{}:{}/{}", - &self.connection_options.container_id, remote_dir_for_server, dest_path_str - )); - - let output = command.output().await?; - - if output.status.success() { - return Ok(()); - } - - let stderr = String::from_utf8_lossy(&output.stderr); - log::debug!( - "failed to upload file via docker cp {src_path_display} -> {dest_path_str}: {stderr}", - ); - anyhow::bail!( - "failed to upload file via docker cp {} -> {}: {}", - src_path_display, - dest_path_str, - stderr, - ); - } - - async fn run_docker_command( - &self, - subcommand: &str, - args: &[impl AsRef], - ) -> Result { - let mut command = util::command::new_smol_command("docker"); - command.arg(subcommand); - for arg in args { - command.arg(arg.as_ref()); - } - let output = command.output().await?; - anyhow::ensure!( - output.status.success(), - "failed to run command {command:?}: {}", - String::from_utf8_lossy(&output.stderr) - ); - Ok(String::from_utf8_lossy(&output.stdout).to_string()) - } - - async fn run_docker_exec( - &self, - inner_program: &str, - working_directory: Option<&str>, - env: &HashMap, - program_args: &[impl AsRef], - ) -> Result { - let mut args = match working_directory { - Some(dir) => vec!["-w".to_string(), dir.to_string()], - None => vec![], - }; - - for (k, v) in env.iter() { - args.push("-e".to_string()); - let env_declaration = format!("{}={}", k, v); - args.push(env_declaration); - } - - args.push(self.connection_options.container_id.clone()); - args.push(inner_program.to_string()); - - for arg in program_args { - args.push(arg.as_ref().to_owned()); - } - self.run_docker_command("exec", args.as_ref()).await - } - - async fn download_binary_on_server( - &self, - url: &str, - tmp_path_gz: &RelPath, - remote_dir_for_server: &str, - delegate: &Arc, - cx: &mut AsyncApp, - ) -> Result<()> { - if let Some(parent) = tmp_path_gz.parent() { - self.run_docker_exec( - "mkdir", - Some(remote_dir_for_server), - &Default::default(), - &["-p", parent.display(self.path_style()).as_ref()], - ) - .await?; - } - - delegate.set_status(Some("Downloading remote development server on host"), cx); - - match self - .run_docker_exec( - "curl", - Some(remote_dir_for_server), - &Default::default(), - &[ - "-f", - "-L", - url, - "-o", - &tmp_path_gz.display(self.path_style()), - ], - ) - .await - { - Ok(_) => {} - Err(e) => { - if self - .run_docker_exec("which", None, &Default::default(), &["curl"]) - .await - .is_ok() - { - return Err(e); - } - - log::info!("curl is not available, trying wget"); - match self - .run_docker_exec( - "wget", - Some(remote_dir_for_server), - &Default::default(), - &[url, "-O", &tmp_path_gz.display(self.path_style())], - ) - .await - { - Ok(_) => {} - Err(e) => { - if self - .run_docker_exec("which", None, &Default::default(), &["wget"]) - .await - .is_ok() - { - return Err(e); - } else { - anyhow::bail!("Neither curl nor wget is available"); - } - } - } - } - } - Ok(()) - } - - fn kill_inner(&self) -> Result<()> { - if let Some(pid) = self.proxy_process.lock().take() { - if let Ok(_) = util::command::new_smol_command("kill") - .arg(pid.to_string()) - .spawn() - { - Ok(()) - } else { - Err(anyhow::anyhow!("Failed to kill process")) - } - } else { - Ok(()) - } - } -} - -#[async_trait(?Send)] -impl RemoteConnection for DockerExecConnection { - fn has_wsl_interop(&self) -> bool { - false - } - fn start_proxy( - &self, - unique_identifier: String, - reconnect: bool, - incoming_tx: UnboundedSender, - outgoing_rx: UnboundedReceiver, - connection_activity_tx: Sender<()>, - delegate: Arc, - cx: &mut AsyncApp, - ) -> Task> { - // We'll try connecting anew every time we open a devcontainer, so proactively try to kill any old connections. - if !self.has_been_killed() { - if let Err(e) = self.kill_inner() { - return Task::ready(Err(e)); - }; - } - - delegate.set_status(Some("Starting proxy"), cx); - - let Some(remote_binary_relpath) = self.remote_binary_relpath.clone() else { - return Task::ready(Err(anyhow!("Remote binary path not set"))); - }; - - let mut docker_args = vec![ - "exec".to_string(), - "-w".to_string(), - self.remote_dir_for_server.clone(), - "-i".to_string(), - self.connection_options.container_id.to_string(), - ]; - for env_var in ["RUST_LOG", "RUST_BACKTRACE", "ZED_GENERATE_MINIDUMPS"] { - if let Some(value) = std::env::var(env_var).ok() { - docker_args.push("-e".to_string()); - docker_args.push(format!("{}='{}'", env_var, value)); - } - } - let val = remote_binary_relpath - .display(self.path_style()) - .into_owned(); - docker_args.push(val); - docker_args.push("proxy".to_string()); - docker_args.push("--identifier".to_string()); - docker_args.push(unique_identifier); - if reconnect { - docker_args.push("--reconnect".to_string()); - } - let mut command = util::command::new_smol_command("docker"); - command - .kill_on_drop(true) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .args(docker_args); - - let Ok(child) = command.spawn() else { - return Task::ready(Err(anyhow::anyhow!( - "Failed to start remote server process" - ))); - }; - - let mut proxy_process = self.proxy_process.lock(); - *proxy_process = Some(child.id()); - - super::handle_rpc_messages_over_child_process_stdio( - child, - incoming_tx, - outgoing_rx, - connection_activity_tx, - cx, - ) - } - - fn upload_directory( - &self, - src_path: PathBuf, - dest_path: RemotePathBuf, - cx: &App, - ) -> Task> { - let dest_path_str = dest_path.to_string(); - let src_path_display = src_path.display().to_string(); - - let mut command = util::command::new_smol_command("docker"); - command.arg("cp"); - command.arg("-a"); // Archive mode is required to assign the file ownership to the default docker exec user - command.arg(src_path_display); - command.arg(format!( - "{}:{}", - self.connection_options.container_id, dest_path_str - )); - - cx.background_spawn(async move { - let output = command.output().await?; - - if output.status.success() { - Ok(()) - } else { - Err(anyhow::anyhow!("Failed to upload directory")) - } - }) - } - - async fn kill(&self) -> Result<()> { - self.kill_inner() - } - - fn has_been_killed(&self) -> bool { - self.proxy_process.lock().is_none() - } - - fn build_command( - &self, - program: Option, - args: &[String], - env: &HashMap, - working_dir: Option, - _port_forward: Option<(u16, String, u16)>, - ) -> Result { - let mut parsed_working_dir = None; - - let path_style = self.path_style(); - - if let Some(working_dir) = working_dir { - let working_dir = RemotePathBuf::new(working_dir, path_style).to_string(); - - const TILDE_PREFIX: &'static str = "~/"; - if working_dir.starts_with(TILDE_PREFIX) { - let working_dir = working_dir.trim_start_matches("~").trim_start_matches("/"); - parsed_working_dir = Some(format!("$HOME/{working_dir}")); - } else { - parsed_working_dir = Some(working_dir); - } - } - - let mut inner_program = Vec::new(); - - if let Some(program) = program { - inner_program.push(program); - for arg in args { - inner_program.push(arg.clone()); - } - } else { - inner_program.push(self.shell()); - inner_program.push("-l".to_string()); - }; - - let mut docker_args = vec!["exec".to_string()]; - - if let Some(parsed_working_dir) = parsed_working_dir { - docker_args.push("-w".to_string()); - docker_args.push(parsed_working_dir); - } - - for (k, v) in env.iter() { - docker_args.push("-e".to_string()); - docker_args.push(format!("{}={}", k, v)); - } - - docker_args.push("-it".to_string()); - docker_args.push(self.connection_options.container_id.to_string()); - - docker_args.append(&mut inner_program); - - Ok(CommandTemplate { - program: "docker".to_string(), - args: docker_args, - // Docker-exec pipes in environment via the "-e" argument - env: Default::default(), - }) - } - - fn build_forward_ports_command( - &self, - _forwards: Vec<(u16, String, u16)>, - ) -> Result { - Err(anyhow::anyhow!("Not currently supported for docker_exec")) - } - - fn connection_options(&self) -> RemoteConnectionOptions { - RemoteConnectionOptions::Docker(self.connection_options.clone()) - } - - fn path_style(&self) -> PathStyle { - self.path_style.unwrap_or(PathStyle::Posix) - } - - fn shell(&self) -> String { - match &self.shell { - Some(shell) => shell.clone(), - None => self.default_system_shell(), - } - } - - fn default_system_shell(&self) -> String { - String::from("/bin/sh") - } -} diff --git a/crates/remote/src/transport/ssh.rs b/crates/remote/src/transport/ssh.rs deleted file mode 100644 index 9412549f20..0000000000 --- a/crates/remote/src/transport/ssh.rs +++ /dev/null @@ -1,1501 +0,0 @@ -use crate::{ - RemoteClientDelegate, RemotePlatform, - remote_client::{CommandTemplate, RemoteConnection, RemoteConnectionOptions}, - transport::{parse_platform, parse_shell}, -}; -use anyhow::{Context as _, Result, anyhow}; -use async_trait::async_trait; -use collections::HashMap; -use futures::{ - AsyncReadExt as _, FutureExt as _, - channel::mpsc::{Sender, UnboundedReceiver, UnboundedSender}, - select_biased, -}; -use gpui::{App, AppContext as _, AsyncApp, Task}; -use parking_lot::Mutex; -use paths::remote_server_dir_relative; -use release_channel::{AppVersion, ReleaseChannel}; -use rpc::proto::Envelope; -use semver::Version; -pub use settings::SshPortForwardOption; -use smol::{ - fs, - process::{self, Child, Stdio}, -}; -use std::{ - path::{Path, PathBuf}, - sync::Arc, - time::Instant, -}; -use tempfile::TempDir; -use util::{ - paths::{PathStyle, RemotePathBuf}, - rel_path::RelPath, - shell::{Shell, ShellKind}, - shell_builder::ShellBuilder, -}; - -pub(crate) struct SshRemoteConnection { - socket: SshSocket, - master_process: Mutex>, - remote_binary_path: Option>, - ssh_platform: RemotePlatform, - ssh_path_style: PathStyle, - ssh_shell: String, - ssh_shell_kind: ShellKind, - ssh_default_system_shell: String, - _temp_dir: TempDir, -} - -#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] -pub struct SshConnectionOptions { - pub host: String, - pub username: Option, - pub port: Option, - pub password: Option, - pub args: Option>, - pub port_forwards: Option>, - - pub nickname: Option, - pub upload_binary_over_ssh: bool, -} - -impl From for SshConnectionOptions { - fn from(val: settings::SshConnection) -> Self { - SshConnectionOptions { - host: val.host.into(), - username: val.username, - port: val.port, - password: None, - args: Some(val.args), - nickname: val.nickname, - upload_binary_over_ssh: val.upload_binary_over_ssh.unwrap_or_default(), - port_forwards: val.port_forwards, - } - } -} - -struct SshSocket { - connection_options: SshConnectionOptions, - #[cfg(not(target_os = "windows"))] - socket_path: std::path::PathBuf, - envs: HashMap, - #[cfg(target_os = "windows")] - _proxy: askpass::PasswordProxy, -} - -struct MasterProcess { - process: Child, -} - -#[cfg(not(target_os = "windows"))] -impl MasterProcess { - pub fn new( - askpass_script_path: &std::ffi::OsStr, - additional_args: Vec, - socket_path: &std::path::Path, - url: &str, - ) -> Result { - let args = [ - "-N", - "-o", - "ControlPersist=no", - "-o", - "ControlMaster=yes", - "-o", - ]; - - let mut master_process = util::command::new_smol_command("ssh"); - master_process - .kill_on_drop(true) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .env("SSH_ASKPASS_REQUIRE", "force") - .env("SSH_ASKPASS", askpass_script_path) - .args(additional_args) - .args(args); - - master_process.arg(format!("ControlPath={}", socket_path.display())); - - let process = master_process.arg(&url).spawn()?; - - Ok(MasterProcess { process }) - } - - pub async fn wait_connected(&mut self) -> Result<()> { - let Some(mut stdout) = self.process.stdout.take() else { - anyhow::bail!("ssh process stdout capture failed"); - }; - - let mut output = Vec::new(); - stdout.read_to_end(&mut output).await?; - Ok(()) - } -} - -#[cfg(target_os = "windows")] -impl MasterProcess { - const CONNECTION_ESTABLISHED_MAGIC: &str = "ZED_SSH_CONNECTION_ESTABLISHED"; - - pub fn new( - askpass_script_path: &std::ffi::OsStr, - additional_args: Vec, - url: &str, - ) -> Result { - // On Windows, `ControlMaster` and `ControlPath` are not supported: - // https://github.com/PowerShell/Win32-OpenSSH/issues/405 - // https://github.com/PowerShell/Win32-OpenSSH/wiki/Project-Scope - // - // Using an ugly workaround to detect connection establishment - // -N doesn't work with JumpHosts as windows openssh never closes stdin in that case - let args = [ - "-t", - &format!("echo '{}'; exec $0", Self::CONNECTION_ESTABLISHED_MAGIC), - ]; - - let mut master_process = util::command::new_smol_command("ssh"); - master_process - .kill_on_drop(true) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .env("SSH_ASKPASS_REQUIRE", "force") - .env("SSH_ASKPASS", askpass_script_path) - .args(additional_args) - .arg(url) - .args(args); - - let process = master_process.spawn()?; - - Ok(MasterProcess { process }) - } - - pub async fn wait_connected(&mut self) -> Result<()> { - use smol::io::AsyncBufReadExt; - - let Some(stdout) = self.process.stdout.take() else { - anyhow::bail!("ssh process stdout capture failed"); - }; - - let mut reader = smol::io::BufReader::new(stdout); - - let mut line = String::new(); - - loop { - let n = reader.read_line(&mut line).await?; - if n == 0 { - anyhow::bail!("ssh process exited before connection established"); - } - - if line.contains(Self::CONNECTION_ESTABLISHED_MAGIC) { - return Ok(()); - } - } - } -} - -impl AsRef for MasterProcess { - fn as_ref(&self) -> &Child { - &self.process - } -} - -impl AsMut for MasterProcess { - fn as_mut(&mut self) -> &mut Child { - &mut self.process - } -} - -#[async_trait(?Send)] -impl RemoteConnection for SshRemoteConnection { - async fn kill(&self) -> Result<()> { - let Some(mut process) = self.master_process.lock().take() else { - return Ok(()); - }; - process.as_mut().kill().ok(); - process.as_mut().status().await?; - Ok(()) - } - - fn has_been_killed(&self) -> bool { - self.master_process.lock().is_none() - } - - fn connection_options(&self) -> RemoteConnectionOptions { - RemoteConnectionOptions::Ssh(self.socket.connection_options.clone()) - } - - fn shell(&self) -> String { - self.ssh_shell.clone() - } - - fn default_system_shell(&self) -> String { - self.ssh_default_system_shell.clone() - } - - fn build_command( - &self, - input_program: Option, - input_args: &[String], - input_env: &HashMap, - working_dir: Option, - port_forward: Option<(u16, String, u16)>, - ) -> Result { - let Self { - ssh_path_style, - socket, - ssh_shell_kind, - ssh_shell, - .. - } = self; - let env = socket.envs.clone(); - build_command( - input_program, - input_args, - input_env, - working_dir, - port_forward, - env, - *ssh_path_style, - ssh_shell, - *ssh_shell_kind, - socket.ssh_args(), - ) - } - - fn build_forward_ports_command( - &self, - forwards: Vec<(u16, String, u16)>, - ) -> Result { - let Self { socket, .. } = self; - let mut args = socket.ssh_args(); - args.push("-N".into()); - for (local_port, host, remote_port) in forwards { - args.push("-L".into()); - args.push(format!("{local_port}:{host}:{remote_port}")); - } - Ok(CommandTemplate { - program: "ssh".into(), - args, - env: Default::default(), - }) - } - - fn upload_directory( - &self, - src_path: PathBuf, - dest_path: RemotePathBuf, - cx: &App, - ) -> Task> { - let dest_path_str = dest_path.to_string(); - let src_path_display = src_path.display().to_string(); - - let mut sftp_command = self.build_sftp_command(); - let mut scp_command = - self.build_scp_command(&src_path, &dest_path_str, Some(&["-C", "-r"])); - - cx.background_spawn(async move { - // We will try SFTP first, and if that fails, we will fall back to SCP. - // If SCP fails also, we give up and return an error. - // The reason we allow a fallback from SFTP to SCP is that if the user has to specify a password, - // depending on the implementation of SSH stack, SFTP may disable interactive password prompts in batch mode. - // This is for example the case on Windows as evidenced by this implementation snippet: - // https://github.com/PowerShell/openssh-portable/blob/b8c08ef9da9450a94a9c5ef717d96a7bd83f3332/sshconnect2.c#L417 - if Self::is_sftp_available().await { - log::debug!("using SFTP for directory upload"); - let mut child = sftp_command.spawn()?; - if let Some(mut stdin) = child.stdin.take() { - use futures::AsyncWriteExt; - let sftp_batch = format!("put -r \"{src_path_display}\" \"{dest_path_str}\"\n"); - stdin.write_all(sftp_batch.as_bytes()).await?; - stdin.flush().await?; - } - - let output = child.output().await?; - if output.status.success() { - return Ok(()); - } - - let stderr = String::from_utf8_lossy(&output.stderr); - log::debug!("failed to upload directory via SFTP {src_path_display} -> {dest_path_str}: {stderr}"); - } - - log::debug!("using SCP for directory upload"); - let output = scp_command.output().await?; - - if output.status.success() { - return Ok(()); - } - - let stderr = String::from_utf8_lossy(&output.stderr); - log::debug!("failed to upload directory via SCP {src_path_display} -> {dest_path_str}: {stderr}"); - - anyhow::bail!( - "failed to upload directory via SFTP/SCP {} -> {}: {}", - src_path_display, - dest_path_str, - stderr, - ); - }) - } - - fn start_proxy( - &self, - unique_identifier: String, - reconnect: bool, - incoming_tx: UnboundedSender, - outgoing_rx: UnboundedReceiver, - connection_activity_tx: Sender<()>, - delegate: Arc, - cx: &mut AsyncApp, - ) -> Task> { - delegate.set_status(Some("Starting proxy"), cx); - - let Some(remote_binary_path) = self.remote_binary_path.clone() else { - return Task::ready(Err(anyhow!("Remote binary path not set"))); - }; - - let mut proxy_args = vec![]; - for env_var in ["RUST_LOG", "RUST_BACKTRACE", "ZED_GENERATE_MINIDUMPS"] { - if let Some(value) = std::env::var(env_var).ok() { - proxy_args.push(format!("{}='{}'", env_var, value)); - } - } - proxy_args.push(remote_binary_path.display(self.path_style()).into_owned()); - proxy_args.push("proxy".to_owned()); - proxy_args.push("--identifier".to_owned()); - proxy_args.push(unique_identifier); - - if reconnect { - proxy_args.push("--reconnect".to_owned()); - } - - let ssh_proxy_process = match self - .socket - .ssh_command(self.ssh_shell_kind, "env", &proxy_args, false) - // IMPORTANT: we kill this process when we drop the task that uses it. - .kill_on_drop(true) - .spawn() - { - Ok(process) => process, - Err(error) => { - return Task::ready(Err(anyhow!("failed to spawn remote server: {}", error))); - } - }; - - super::handle_rpc_messages_over_child_process_stdio( - ssh_proxy_process, - incoming_tx, - outgoing_rx, - connection_activity_tx, - cx, - ) - } - - fn path_style(&self) -> PathStyle { - self.ssh_path_style - } - - fn has_wsl_interop(&self) -> bool { - false - } -} - -impl SshRemoteConnection { - pub(crate) async fn new( - connection_options: SshConnectionOptions, - delegate: Arc, - cx: &mut AsyncApp, - ) -> Result { - use askpass::AskPassResult; - - let url = connection_options.ssh_url(); - - let temp_dir = tempfile::Builder::new() - .prefix("zed-ssh-session") - .tempdir()?; - let askpass_delegate = askpass::AskPassDelegate::new(cx, { - let delegate = delegate.clone(); - move |prompt, tx, cx| delegate.ask_password(prompt, tx, cx) - }); - - let mut askpass = - askpass::AskPassSession::new(cx.background_executor(), askpass_delegate).await?; - - delegate.set_status(Some("Connecting"), cx); - - // Start the master SSH process, which does not do anything except for establish - // the connection and keep it open, allowing other ssh commands to reuse it - // via a control socket. - #[cfg(not(target_os = "windows"))] - let socket_path = temp_dir.path().join("ssh.sock"); - - #[cfg(target_os = "windows")] - let mut master_process = MasterProcess::new( - askpass.script_path().as_ref(), - connection_options.additional_args(), - &url, - )?; - #[cfg(not(target_os = "windows"))] - let mut master_process = MasterProcess::new( - askpass.script_path().as_ref(), - connection_options.additional_args(), - &socket_path, - &url, - )?; - - let result = select_biased! { - result = askpass.run().fuse() => { - match result { - AskPassResult::CancelledByUser => { - master_process.as_mut().kill().ok(); - anyhow::bail!("SSH connection canceled") - } - AskPassResult::Timedout => { - anyhow::bail!("connecting to host timed out") - } - } - } - _ = master_process.wait_connected().fuse() => { - anyhow::Ok(()) - } - }; - - if let Err(e) = result { - return Err(e.context("Failed to connect to host")); - } - - if master_process.as_mut().try_status()?.is_some() { - let mut output = Vec::new(); - output.clear(); - let mut stderr = master_process.as_mut().stderr.take().unwrap(); - stderr.read_to_end(&mut output).await?; - - let error_message = format!( - "failed to connect: {}", - String::from_utf8_lossy(&output).trim() - ); - anyhow::bail!(error_message); - } - - #[cfg(not(target_os = "windows"))] - let socket = SshSocket::new(connection_options, socket_path).await?; - #[cfg(target_os = "windows")] - let socket = SshSocket::new( - connection_options, - askpass - .get_password() - .or_else(|| askpass::EncryptedPassword::try_from("").ok()) - .context("Failed to fetch askpass password")?, - cx.background_executor().clone(), - ) - .await?; - drop(askpass); - - let ssh_shell = socket.shell().await; - log::info!("Remote shell discovered: {}", ssh_shell); - let ssh_platform = socket.platform(ShellKind::new(&ssh_shell, false)).await?; - log::info!("Remote platform discovered: {:?}", ssh_platform); - let ssh_path_style = match ssh_platform.os { - "windows" => PathStyle::Windows, - _ => PathStyle::Posix, - }; - let ssh_default_system_shell = String::from("/bin/sh"); - let ssh_shell_kind = ShellKind::new( - &ssh_shell, - match ssh_platform.os { - "windows" => true, - _ => false, - }, - ); - - let mut this = Self { - socket, - master_process: Mutex::new(Some(master_process)), - _temp_dir: temp_dir, - remote_binary_path: None, - ssh_path_style, - ssh_platform, - ssh_shell, - ssh_shell_kind, - ssh_default_system_shell, - }; - - let (release_channel, version) = - cx.update(|cx| (ReleaseChannel::global(cx), AppVersion::global(cx)))?; - this.remote_binary_path = Some( - this.ensure_server_binary(&delegate, release_channel, version, cx) - .await?, - ); - - Ok(this) - } - - async fn ensure_server_binary( - &self, - delegate: &Arc, - release_channel: ReleaseChannel, - version: Version, - cx: &mut AsyncApp, - ) -> Result> { - let version_str = match release_channel { - ReleaseChannel::Dev => "build".to_string(), - _ => version.to_string(), - }; - let binary_name = format!( - "zed-remote-server-{}-{}", - release_channel.dev_name(), - version_str - ); - let dst_path = - paths::remote_server_dir_relative().join(RelPath::unix(&binary_name).unwrap()); - - #[cfg(debug_assertions)] - if let Some(remote_server_path) = - super::build_remote_server_from_source(&self.ssh_platform, delegate.as_ref(), cx) - .await? - { - let tmp_path = paths::remote_server_dir_relative().join( - RelPath::unix(&format!( - "download-{}-{}", - std::process::id(), - remote_server_path.file_name().unwrap().to_string_lossy() - )) - .unwrap(), - ); - self.upload_local_server_binary(&remote_server_path, &tmp_path, delegate, cx) - .await?; - self.extract_server_binary(&dst_path, &tmp_path, delegate, cx) - .await?; - return Ok(dst_path); - } - - if self - .socket - .run_command( - self.ssh_shell_kind, - &dst_path.display(self.path_style()), - &["version"], - true, - ) - .await - .is_ok() - { - return Ok(dst_path); - } - - let wanted_version = cx.update(|cx| match release_channel { - ReleaseChannel::Nightly => Ok(None), - ReleaseChannel::Dev => { - anyhow::bail!( - "ZED_BUILD_REMOTE_SERVER is not set and no remote server exists at ({:?})", - dst_path - ) - } - _ => Ok(Some(AppVersion::global(cx))), - })??; - - let tmp_path_gz = remote_server_dir_relative().join( - RelPath::unix(&format!( - "{}-download-{}.gz", - binary_name, - std::process::id() - )) - .unwrap(), - ); - if !self.socket.connection_options.upload_binary_over_ssh - && let Some(url) = delegate - .get_download_url( - self.ssh_platform, - release_channel, - wanted_version.clone(), - cx, - ) - .await? - { - match self - .download_binary_on_server(&url, &tmp_path_gz, delegate, cx) - .await - { - Ok(_) => { - self.extract_server_binary(&dst_path, &tmp_path_gz, delegate, cx) - .await - .context("extracting server binary")?; - return Ok(dst_path); - } - Err(e) => { - log::error!( - "Failed to download binary on server, attempting to download locally and then upload it the server: {e:#}", - ) - } - } - } - - let src_path = delegate - .download_server_binary_locally( - self.ssh_platform, - release_channel, - wanted_version.clone(), - cx, - ) - .await - .context("downloading server binary locally")?; - self.upload_local_server_binary(&src_path, &tmp_path_gz, delegate, cx) - .await - .context("uploading server binary")?; - self.extract_server_binary(&dst_path, &tmp_path_gz, delegate, cx) - .await - .context("extracting server binary")?; - Ok(dst_path) - } - - async fn download_binary_on_server( - &self, - url: &str, - tmp_path_gz: &RelPath, - delegate: &Arc, - cx: &mut AsyncApp, - ) -> Result<()> { - if let Some(parent) = tmp_path_gz.parent() { - self.socket - .run_command( - self.ssh_shell_kind, - "mkdir", - &["-p", parent.display(self.path_style()).as_ref()], - true, - ) - .await?; - } - - delegate.set_status(Some("Downloading remote development server on host"), cx); - - const CONNECT_TIMEOUT_SECS: &str = "10"; - - match self - .socket - .run_command( - self.ssh_shell_kind, - "curl", - &[ - "-f", - "-L", - "--connect-timeout", - CONNECT_TIMEOUT_SECS, - url, - "-o", - &tmp_path_gz.display(self.path_style()), - ], - true, - ) - .await - { - Ok(_) => {} - Err(e) => { - if self - .socket - .run_command(self.ssh_shell_kind, "which", &["curl"], true) - .await - .is_ok() - { - return Err(e); - } - - log::info!("curl is not available, trying wget"); - match self - .socket - .run_command( - self.ssh_shell_kind, - "wget", - &[ - "--connect-timeout", - CONNECT_TIMEOUT_SECS, - "--tries", - "1", - url, - "-O", - &tmp_path_gz.display(self.path_style()), - ], - true, - ) - .await - { - Ok(_) => {} - Err(e) => { - if self - .socket - .run_command(self.ssh_shell_kind, "which", &["wget"], true) - .await - .is_ok() - { - return Err(e); - } else { - anyhow::bail!("Neither curl nor wget is available"); - } - } - } - } - } - - Ok(()) - } - - async fn upload_local_server_binary( - &self, - src_path: &Path, - tmp_path_gz: &RelPath, - delegate: &Arc, - cx: &mut AsyncApp, - ) -> Result<()> { - if let Some(parent) = tmp_path_gz.parent() { - self.socket - .run_command( - self.ssh_shell_kind, - "mkdir", - &["-p", parent.display(self.path_style()).as_ref()], - true, - ) - .await?; - } - - let src_stat = fs::metadata(&src_path).await?; - let size = src_stat.len(); - - let t0 = Instant::now(); - delegate.set_status(Some("Uploading remote development server"), cx); - log::info!( - "uploading remote development server to {:?} ({}kb)", - tmp_path_gz, - size / 1024 - ); - self.upload_file(src_path, tmp_path_gz) - .await - .context("failed to upload server binary")?; - log::info!("uploaded remote development server in {:?}", t0.elapsed()); - Ok(()) - } - - async fn extract_server_binary( - &self, - dst_path: &RelPath, - tmp_path: &RelPath, - delegate: &Arc, - cx: &mut AsyncApp, - ) -> Result<()> { - delegate.set_status(Some("Extracting remote development server"), cx); - let server_mode = 0o755; - - let shell_kind = ShellKind::Posix; - let orig_tmp_path = tmp_path.display(self.path_style()); - let server_mode = format!("{:o}", server_mode); - let server_mode = shell_kind - .try_quote(&server_mode) - .context("shell quoting")?; - let dst_path = dst_path.display(self.path_style()); - let dst_path = shell_kind.try_quote(&dst_path).context("shell quoting")?; - let script = if let Some(tmp_path) = orig_tmp_path.strip_suffix(".gz") { - let orig_tmp_path = shell_kind - .try_quote(&orig_tmp_path) - .context("shell quoting")?; - let tmp_path = shell_kind.try_quote(&tmp_path).context("shell quoting")?; - format!( - "gunzip -f {orig_tmp_path} && chmod {server_mode} {tmp_path} && mv {tmp_path} {dst_path}", - ) - } else { - let orig_tmp_path = shell_kind - .try_quote(&orig_tmp_path) - .context("shell quoting")?; - format!("chmod {server_mode} {orig_tmp_path} && mv {orig_tmp_path} {dst_path}",) - }; - let args = shell_kind.args_for_shell(false, script.to_string()); - self.socket - .run_command(shell_kind, "sh", &args, true) - .await?; - Ok(()) - } - - fn build_scp_command( - &self, - src_path: &Path, - dest_path_str: &str, - args: Option<&[&str]>, - ) -> process::Command { - let mut command = util::command::new_smol_command("scp"); - self.socket.ssh_options(&mut command, false).args( - self.socket - .connection_options - .port - .map(|port| vec!["-P".to_string(), port.to_string()]) - .unwrap_or_default(), - ); - if let Some(args) = args { - command.args(args); - } - command.arg(src_path).arg(format!( - "{}:{}", - self.socket.connection_options.scp_url(), - dest_path_str - )); - command - } - - fn build_sftp_command(&self) -> process::Command { - let mut command = util::command::new_smol_command("sftp"); - self.socket.ssh_options(&mut command, false).args( - self.socket - .connection_options - .port - .map(|port| vec!["-P".to_string(), port.to_string()]) - .unwrap_or_default(), - ); - command.arg("-b").arg("-"); - command.arg(self.socket.connection_options.scp_url()); - command.stdin(Stdio::piped()); - command - } - - async fn upload_file(&self, src_path: &Path, dest_path: &RelPath) -> Result<()> { - log::debug!("uploading file {:?} to {:?}", src_path, dest_path); - - let src_path_display = src_path.display().to_string(); - let dest_path_str = dest_path.display(self.path_style()); - - // We will try SFTP first, and if that fails, we will fall back to SCP. - // If SCP fails also, we give up and return an error. - // The reason we allow a fallback from SFTP to SCP is that if the user has to specify a password, - // depending on the implementation of SSH stack, SFTP may disable interactive password prompts in batch mode. - // This is for example the case on Windows as evidenced by this implementation snippet: - // https://github.com/PowerShell/openssh-portable/blob/b8c08ef9da9450a94a9c5ef717d96a7bd83f3332/sshconnect2.c#L417 - if Self::is_sftp_available().await { - log::debug!("using SFTP for file upload"); - let mut command = self.build_sftp_command(); - let sftp_batch = format!("put {src_path_display} {dest_path_str}\n"); - - let mut child = command.spawn()?; - if let Some(mut stdin) = child.stdin.take() { - use futures::AsyncWriteExt; - stdin.write_all(sftp_batch.as_bytes()).await?; - stdin.flush().await?; - } - - let output = child.output().await?; - if output.status.success() { - return Ok(()); - } - - let stderr = String::from_utf8_lossy(&output.stderr); - log::debug!( - "failed to upload file via SFTP {src_path_display} -> {dest_path_str}: {stderr}" - ); - } - - log::debug!("using SCP for file upload"); - let mut command = self.build_scp_command(src_path, &dest_path_str, None); - let output = command.output().await?; - - if output.status.success() { - return Ok(()); - } - - let stderr = String::from_utf8_lossy(&output.stderr); - log::debug!( - "failed to upload file via SCP {src_path_display} -> {dest_path_str}: {stderr}", - ); - anyhow::bail!( - "failed to upload file via STFP/SCP {} -> {}: {}", - src_path_display, - dest_path_str, - stderr, - ); - } - - async fn is_sftp_available() -> bool { - which::which("sftp").is_ok() - } -} - -impl SshSocket { - #[cfg(not(target_os = "windows"))] - async fn new(options: SshConnectionOptions, socket_path: PathBuf) -> Result { - Ok(Self { - connection_options: options, - envs: HashMap::default(), - socket_path, - }) - } - - #[cfg(target_os = "windows")] - async fn new( - options: SshConnectionOptions, - password: askpass::EncryptedPassword, - executor: gpui::BackgroundExecutor, - ) -> Result { - let mut envs = HashMap::default(); - let get_password = - move |_| Task::ready(std::ops::ControlFlow::Continue(Ok(password.clone()))); - - let _proxy = askpass::PasswordProxy::new(get_password, executor).await?; - envs.insert("SSH_ASKPASS_REQUIRE".into(), "force".into()); - envs.insert( - "SSH_ASKPASS".into(), - _proxy.script_path().as_ref().display().to_string(), - ); - - Ok(Self { - connection_options: options, - envs, - _proxy, - }) - } - - // :WARNING: ssh unquotes arguments when executing on the remote :WARNING: - // e.g. $ ssh host sh -c 'ls -l' is equivalent to $ ssh host sh -c ls -l - // and passes -l as an argument to sh, not to ls. - // Furthermore, some setups (e.g. Coder) will change directory when SSH'ing - // into a machine. You must use `cd` to get back to $HOME. - // You need to do it like this: $ ssh host "cd; sh -c 'ls -l /tmp'" - fn ssh_command( - &self, - shell_kind: ShellKind, - program: &str, - args: &[impl AsRef], - allow_pseudo_tty: bool, - ) -> process::Command { - let mut command = util::command::new_smol_command("ssh"); - let program = shell_kind.prepend_command_prefix(program); - let mut to_run = shell_kind - .try_quote_prefix_aware(&program) - .expect("shell quoting") - .into_owned(); - for arg in args { - // We're trying to work with: sh, bash, zsh, fish, tcsh, ...? - debug_assert!( - !arg.as_ref().contains('\n'), - "multiline arguments do not work in all shells" - ); - to_run.push(' '); - to_run.push_str(&shell_kind.try_quote(arg.as_ref()).expect("shell quoting")); - } - let separator = shell_kind.sequential_commands_separator(); - let to_run = format!("cd{separator} {to_run}"); - self.ssh_options(&mut command, true) - .arg(self.connection_options.ssh_url()); - if !allow_pseudo_tty { - command.arg("-T"); - } - command.arg(to_run); - log::debug!("ssh {:?}", command); - command - } - - async fn run_command( - &self, - shell_kind: ShellKind, - program: &str, - args: &[impl AsRef], - allow_pseudo_tty: bool, - ) -> Result { - let mut command = self.ssh_command(shell_kind, program, args, allow_pseudo_tty); - let output = command.output().await?; - anyhow::ensure!( - output.status.success(), - "failed to run command {command:?}: {}", - String::from_utf8_lossy(&output.stderr) - ); - Ok(String::from_utf8_lossy(&output.stdout).to_string()) - } - - #[cfg(not(target_os = "windows"))] - fn ssh_options<'a>( - &self, - command: &'a mut process::Command, - include_port_forwards: bool, - ) -> &'a mut process::Command { - let args = if include_port_forwards { - self.connection_options.additional_args() - } else { - self.connection_options.additional_args_for_scp() - }; - - command - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .args(args) - .args(["-o", "ControlMaster=no", "-o"]) - .arg(format!("ControlPath={}", self.socket_path.display())) - } - - #[cfg(target_os = "windows")] - fn ssh_options<'a>( - &self, - command: &'a mut process::Command, - include_port_forwards: bool, - ) -> &'a mut process::Command { - let args = if include_port_forwards { - self.connection_options.additional_args() - } else { - self.connection_options.additional_args_for_scp() - }; - - command - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .args(args) - .envs(self.envs.clone()) - } - - // On Windows, we need to use `SSH_ASKPASS` to provide the password to ssh. - // On Linux, we use the `ControlPath` option to create a socket file that ssh can use to - #[cfg(not(target_os = "windows"))] - fn ssh_args(&self) -> Vec { - let mut arguments = self.connection_options.additional_args(); - arguments.extend(vec![ - "-o".to_string(), - "ControlMaster=no".to_string(), - "-o".to_string(), - format!("ControlPath={}", self.socket_path.display()), - self.connection_options.ssh_url(), - ]); - arguments - } - - #[cfg(target_os = "windows")] - fn ssh_args(&self) -> Vec { - let mut arguments = self.connection_options.additional_args(); - arguments.push(self.connection_options.ssh_url()); - arguments - } - - async fn platform(&self, shell: ShellKind) -> Result { - let output = self.run_command(shell, "uname", &["-sm"], false).await?; - parse_platform(&output) - } - - async fn shell(&self) -> String { - const DEFAULT_SHELL: &str = "sh"; - match self - .run_command(ShellKind::Posix, "sh", &["-c", "echo $SHELL"], false) - .await - { - Ok(output) => parse_shell(&output, DEFAULT_SHELL), - Err(e) => { - log::error!("Failed to detect remote shell: {e}"); - DEFAULT_SHELL.to_owned() - } - } - } -} - -fn parse_port_number(port_str: &str) -> Result { - port_str - .parse() - .with_context(|| format!("parsing port number: {port_str}")) -} - -fn parse_port_forward_spec(spec: &str) -> Result { - let parts: Vec<&str> = spec.split(':').collect(); - - match parts.len() { - 4 => { - let local_port = parse_port_number(parts[1])?; - let remote_port = parse_port_number(parts[3])?; - - Ok(SshPortForwardOption { - local_host: Some(parts[0].to_string()), - local_port, - remote_host: Some(parts[2].to_string()), - remote_port, - }) - } - 3 => { - let local_port = parse_port_number(parts[0])?; - let remote_port = parse_port_number(parts[2])?; - - Ok(SshPortForwardOption { - local_host: None, - local_port, - remote_host: Some(parts[1].to_string()), - remote_port, - }) - } - _ => anyhow::bail!("Invalid port forward format"), - } -} - -impl SshConnectionOptions { - pub fn parse_command_line(input: &str) -> Result { - let input = input.trim_start_matches("ssh "); - let mut hostname: Option = None; - let mut username: Option = None; - let mut port: Option = None; - let mut args = Vec::new(); - let mut port_forwards: Vec = Vec::new(); - - // disallowed: -E, -e, -F, -f, -G, -g, -M, -N, -n, -O, -q, -S, -s, -T, -t, -V, -v, -W - const ALLOWED_OPTS: &[&str] = &[ - "-4", "-6", "-A", "-a", "-C", "-K", "-k", "-X", "-x", "-Y", "-y", - ]; - const ALLOWED_ARGS: &[&str] = &[ - "-B", "-b", "-c", "-D", "-F", "-I", "-i", "-J", "-l", "-m", "-o", "-P", "-p", "-R", - "-w", - ]; - - let mut tokens = ShellKind::Posix - .split(input) - .context("invalid input")? - .into_iter(); - - 'outer: while let Some(arg) = tokens.next() { - if ALLOWED_OPTS.contains(&(&arg as &str)) { - args.push(arg.to_string()); - continue; - } - if arg == "-p" { - port = tokens.next().and_then(|arg| arg.parse().ok()); - continue; - } else if let Some(p) = arg.strip_prefix("-p") { - port = p.parse().ok(); - continue; - } - if arg == "-l" { - username = tokens.next(); - continue; - } else if let Some(l) = arg.strip_prefix("-l") { - username = Some(l.to_string()); - continue; - } - if arg == "-L" || arg.starts_with("-L") { - let forward_spec = if arg == "-L" { - tokens.next() - } else { - Some(arg.strip_prefix("-L").unwrap().to_string()) - }; - - if let Some(spec) = forward_spec { - port_forwards.push(parse_port_forward_spec(&spec)?); - } else { - anyhow::bail!("Missing port forward format"); - } - } - - for a in ALLOWED_ARGS { - if arg == *a { - args.push(arg); - if let Some(next) = tokens.next() { - args.push(next); - } - continue 'outer; - } else if arg.starts_with(a) { - args.push(arg); - continue 'outer; - } - } - if arg.starts_with("-") || hostname.is_some() { - anyhow::bail!("unsupported argument: {:?}", arg); - } - let mut input = &arg as &str; - // Destination might be: username1@username2@ip2@ip1 - if let Some((u, rest)) = input.rsplit_once('@') { - input = rest; - username = Some(u.to_string()); - } - if let Some((rest, p)) = input.split_once(':') { - input = rest; - port = p.parse().ok() - } - hostname = Some(input.to_string()) - } - - let Some(hostname) = hostname else { - anyhow::bail!("missing hostname"); - }; - - let port_forwards = match port_forwards.len() { - 0 => None, - _ => Some(port_forwards), - }; - - Ok(Self { - host: hostname, - username, - port, - port_forwards, - args: Some(args), - password: None, - nickname: None, - upload_binary_over_ssh: false, - }) - } - - pub fn ssh_url(&self) -> String { - let mut result = String::from("ssh://"); - if let Some(username) = &self.username { - // Username might be: username1@username2@ip2 - let username = urlencoding::encode(username); - result.push_str(&username); - result.push('@'); - } - result.push_str(&self.host); - if let Some(port) = self.port { - result.push(':'); - result.push_str(&port.to_string()); - } - result - } - - pub fn additional_args_for_scp(&self) -> Vec { - self.args.iter().flatten().cloned().collect::>() - } - - pub fn additional_args(&self) -> Vec { - let mut args = self.additional_args_for_scp(); - - if let Some(forwards) = &self.port_forwards { - args.extend(forwards.iter().map(|pf| { - let local_host = match &pf.local_host { - Some(host) => host, - None => "localhost", - }; - let remote_host = match &pf.remote_host { - Some(host) => host, - None => "localhost", - }; - - format!( - "-L{}:{}:{}:{}", - local_host, pf.local_port, remote_host, pf.remote_port - ) - })); - } - - args - } - - fn scp_url(&self) -> String { - if let Some(username) = &self.username { - format!("{}@{}", username, self.host) - } else { - self.host.clone() - } - } - - pub fn connection_string(&self) -> String { - let host = if let Some(username) = &self.username { - format!("{}@{}", username, self.host) - } else { - self.host.clone() - }; - if let Some(port) = &self.port { - format!("{}:{}", host, port) - } else { - host - } - } -} - -fn build_command( - input_program: Option, - input_args: &[String], - input_env: &HashMap, - working_dir: Option, - port_forward: Option<(u16, String, u16)>, - ssh_env: HashMap, - ssh_path_style: PathStyle, - ssh_shell: &str, - ssh_shell_kind: ShellKind, - ssh_args: Vec, -) -> Result { - use std::fmt::Write as _; - - let mut exec = String::new(); - if let Some(working_dir) = working_dir { - let working_dir = RemotePathBuf::new(working_dir, ssh_path_style).to_string(); - - // shlex will wrap the command in single quotes (''), disabling ~ expansion, - // replace with something that works - const TILDE_PREFIX: &'static str = "~/"; - if working_dir.starts_with(TILDE_PREFIX) { - let working_dir = working_dir.trim_start_matches("~").trim_start_matches("/"); - write!( - exec, - "cd \"$HOME/{working_dir}\" {} ", - ssh_shell_kind.sequential_and_commands_separator() - )?; - } else { - write!( - exec, - "cd \"{working_dir}\" {} ", - ssh_shell_kind.sequential_and_commands_separator() - )?; - } - } else { - write!( - exec, - "cd {} ", - ssh_shell_kind.sequential_and_commands_separator() - )?; - }; - write!(exec, "exec env ")?; - - for (k, v) in input_env.iter() { - write!( - exec, - "{}={} ", - k, - ssh_shell_kind.try_quote(v).context("shell quoting")? - )?; - } - - if let Some(input_program) = input_program { - write!( - exec, - "{}", - ssh_shell_kind - .try_quote_prefix_aware(&input_program) - .context("shell quoting")? - )?; - for arg in input_args { - let arg = ssh_shell_kind.try_quote(&arg).context("shell quoting")?; - write!(exec, " {}", &arg)?; - } - } else { - write!(exec, "{ssh_shell} -l")?; - }; - let (command, command_args) = ShellBuilder::new(&Shell::Program(ssh_shell.to_owned()), false) - .build(Some(exec.clone()), &[]); - - let mut args = Vec::new(); - args.extend(ssh_args); - - if let Some((local_port, host, remote_port)) = port_forward { - args.push("-L".into()); - args.push(format!("{local_port}:{host}:{remote_port}")); - } - - args.push("-t".into()); - args.push(command); - args.extend(command_args); - - Ok(CommandTemplate { - program: "ssh".into(), - args, - env: ssh_env, - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_build_command() -> Result<()> { - let mut input_env = HashMap::default(); - input_env.insert("INPUT_VA".to_string(), "val".to_string()); - let mut env = HashMap::default(); - env.insert("SSH_VAR".to_string(), "ssh-val".to_string()); - - let command = build_command( - Some("remote_program".to_string()), - &["arg1".to_string(), "arg2".to_string()], - &input_env, - Some("~/work".to_string()), - None, - env.clone(), - PathStyle::Posix, - "/bin/fish", - ShellKind::Fish, - vec!["-p".to_string(), "2222".to_string()], - )?; - - assert_eq!(command.program, "ssh"); - assert_eq!( - command.args.iter().map(String::as_str).collect::>(), - [ - "-p", - "2222", - "-t", - "/bin/fish", - "-i", - "-c", - "cd \"$HOME/work\" && exec env INPUT_VA=val remote_program arg1 arg2" - ] - ); - assert_eq!(command.env, env); - - let mut input_env = HashMap::default(); - input_env.insert("INPUT_VA".to_string(), "val".to_string()); - let mut env = HashMap::default(); - env.insert("SSH_VAR".to_string(), "ssh-val".to_string()); - - let command = build_command( - None, - &["arg1".to_string(), "arg2".to_string()], - &input_env, - None, - Some((1, "foo".to_owned(), 2)), - env.clone(), - PathStyle::Posix, - "/bin/fish", - ShellKind::Fish, - vec!["-p".to_string(), "2222".to_string()], - )?; - - assert_eq!(command.program, "ssh"); - assert_eq!( - command.args.iter().map(String::as_str).collect::>(), - [ - "-p", - "2222", - "-L", - "1:foo:2", - "-t", - "/bin/fish", - "-i", - "-c", - "cd && exec env INPUT_VA=val /bin/fish -l" - ] - ); - assert_eq!(command.env, env); - - Ok(()) - } - - #[test] - fn scp_args_exclude_port_forward_flags() { - let options = SshConnectionOptions { - host: "example.com".into(), - args: Some(vec![ - "-p".to_string(), - "2222".to_string(), - "-o".to_string(), - "StrictHostKeyChecking=no".to_string(), - ]), - port_forwards: Some(vec![SshPortForwardOption { - local_host: Some("127.0.0.1".to_string()), - local_port: 8080, - remote_host: Some("127.0.0.1".to_string()), - remote_port: 80, - }]), - ..Default::default() - }; - - let ssh_args = options.additional_args(); - assert!( - ssh_args.iter().any(|arg| arg.starts_with("-L")), - "expected ssh args to include port-forward: {ssh_args:?}" - ); - - let scp_args = options.additional_args_for_scp(); - assert_eq!( - scp_args, - vec![ - "-p".to_string(), - "2222".to_string(), - "-o".to_string(), - "StrictHostKeyChecking=no".to_string(), - ] - ); - } -} diff --git a/crates/remote/src/transport/wsl.rs b/crates/remote/src/transport/wsl.rs deleted file mode 100644 index d27648e678..0000000000 --- a/crates/remote/src/transport/wsl.rs +++ /dev/null @@ -1,607 +0,0 @@ -use crate::{ - RemoteClientDelegate, RemotePlatform, - remote_client::{CommandTemplate, RemoteConnection, RemoteConnectionOptions}, - transport::{parse_platform, parse_shell}, -}; -use anyhow::{Context, Result, anyhow, bail}; -use async_trait::async_trait; -use collections::HashMap; -use futures::channel::mpsc::{Sender, UnboundedReceiver, UnboundedSender}; -use gpui::{App, AppContext as _, AsyncApp, Task}; -use release_channel::{AppVersion, ReleaseChannel}; -use rpc::proto::Envelope; -use semver::Version; -use smol::{fs, process}; -use std::{ - ffi::OsStr, - fmt::Write as _, - path::{Path, PathBuf}, - process::Stdio, - sync::Arc, - time::Instant, -}; -use util::{ - paths::{PathStyle, RemotePathBuf}, - rel_path::RelPath, - shell::{Shell, ShellKind}, - shell_builder::ShellBuilder, -}; - -#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Deserialize, schemars::JsonSchema)] -pub struct WslConnectionOptions { - pub distro_name: String, - pub user: Option, -} - -impl From for WslConnectionOptions { - fn from(val: settings::WslConnection) -> Self { - WslConnectionOptions { - distro_name: val.distro_name.into(), - user: val.user, - } - } -} - -#[derive(Debug)] -pub(crate) struct WslRemoteConnection { - remote_binary_path: Option>, - platform: RemotePlatform, - shell: String, - shell_kind: ShellKind, - default_system_shell: String, - has_wsl_interop: bool, - connection_options: WslConnectionOptions, -} - -impl WslRemoteConnection { - pub(crate) async fn new( - connection_options: WslConnectionOptions, - delegate: Arc, - cx: &mut AsyncApp, - ) -> Result { - log::info!( - "Connecting to WSL distro {} with user {:?}", - connection_options.distro_name, - connection_options.user - ); - let (release_channel, version) = - cx.update(|cx| (ReleaseChannel::global(cx), AppVersion::global(cx)))?; - - let mut this = Self { - connection_options, - remote_binary_path: None, - platform: RemotePlatform { os: "", arch: "" }, - shell: String::new(), - shell_kind: ShellKind::Posix, - default_system_shell: String::from("/bin/sh"), - has_wsl_interop: false, - }; - delegate.set_status(Some("Detecting WSL environment"), cx); - this.shell = this - .detect_shell() - .await - .context("failed detecting shell")?; - log::info!("Remote shell discovered: {}", this.shell); - this.shell_kind = ShellKind::new(&this.shell, false); - this.has_wsl_interop = this.detect_has_wsl_interop().await.unwrap_or_default(); - log::info!( - "Remote has wsl interop {}", - if this.has_wsl_interop { - "enabled" - } else { - "disabled" - } - ); - this.platform = this - .detect_platform() - .await - .context("failed detecting platform")?; - log::info!("Remote platform discovered: {:?}", this.platform); - this.remote_binary_path = Some( - this.ensure_server_binary(&delegate, release_channel, version, cx) - .await - .context("failed ensuring server binary")?, - ); - log::debug!("Detected WSL environment: {this:#?}"); - - Ok(this) - } - - async fn detect_platform(&self) -> Result { - let program = self.shell_kind.prepend_command_prefix("uname"); - let output = self.run_wsl_command_with_output(&program, &["-sm"]).await?; - parse_platform(&output) - } - - async fn detect_shell(&self) -> Result { - const DEFAULT_SHELL: &str = "sh"; - match self - .run_wsl_command_with_output("sh", &["-c", "echo $SHELL"]) - .await - { - Ok(output) => Ok(parse_shell(&output, DEFAULT_SHELL)), - Err(e) => { - log::error!("Failed to detect remote shell: {e}"); - Ok(DEFAULT_SHELL.to_owned()) - } - } - } - - async fn detect_has_wsl_interop(&self) -> Result { - Ok(self - .run_wsl_command_with_output("cat", &["/proc/sys/fs/binfmt_misc/WSLInterop"]) - .await - .inspect_err(|err| log::error!("Failed to detect wsl interop: {err}"))? - .contains("enabled")) - } - - async fn windows_path_to_wsl_path(&self, source: &Path) -> Result { - windows_path_to_wsl_path_impl(&self.connection_options, source).await - } - - async fn run_wsl_command_with_output(&self, program: &str, args: &[&str]) -> Result { - run_wsl_command_with_output_impl(&self.connection_options, program, args).await - } - - async fn run_wsl_command(&self, program: &str, args: &[&str]) -> Result<()> { - run_wsl_command_impl(&self.connection_options, program, args, false) - .await - .map(|_| ()) - } - - async fn ensure_server_binary( - &self, - delegate: &Arc, - release_channel: ReleaseChannel, - version: Version, - cx: &mut AsyncApp, - ) -> Result> { - let version_str = match release_channel { - ReleaseChannel::Dev => "build".to_string(), - _ => version.to_string(), - }; - - let binary_name = format!( - "zed-remote-server-{}-{}", - release_channel.dev_name(), - version_str - ); - - let dst_path = - paths::remote_wsl_server_dir_relative().join(RelPath::unix(&binary_name).unwrap()); - - if let Some(parent) = dst_path.parent() { - let parent = parent.display(PathStyle::Posix); - let mkdir = self.shell_kind.prepend_command_prefix("mkdir"); - self.run_wsl_command(&mkdir, &["-p", &parent]) - .await - .map_err(|e| anyhow!("Failed to create directory: {}", e))?; - } - - #[cfg(debug_assertions)] - if let Some(remote_server_path) = - super::build_remote_server_from_source(&self.platform, delegate.as_ref(), cx).await? - { - let tmp_path = paths::remote_wsl_server_dir_relative().join( - &RelPath::unix(&format!( - "download-{}-{}", - std::process::id(), - remote_server_path.file_name().unwrap().to_string_lossy() - )) - .unwrap(), - ); - self.upload_file(&remote_server_path, &tmp_path, delegate, cx) - .await?; - self.extract_and_install(&tmp_path, &dst_path, delegate, cx) - .await?; - return Ok(dst_path); - } - - if self - .run_wsl_command(&dst_path.display(PathStyle::Posix), &["version"]) - .await - .is_ok() - { - return Ok(dst_path); - } - - let wanted_version = match release_channel { - ReleaseChannel::Nightly | ReleaseChannel::Dev => None, - _ => Some(cx.update(|cx| AppVersion::global(cx))?), - }; - - let src_path = delegate - .download_server_binary_locally(self.platform, release_channel, wanted_version, cx) - .await?; - - let tmp_path = format!( - "{}.{}.gz", - dst_path.display(PathStyle::Posix), - std::process::id() - ); - let tmp_path = RelPath::unix(&tmp_path).unwrap(); - - self.upload_file(&src_path, &tmp_path, delegate, cx).await?; - self.extract_and_install(&tmp_path, &dst_path, delegate, cx) - .await?; - - Ok(dst_path) - } - - async fn upload_file( - &self, - src_path: &Path, - dst_path: &RelPath, - delegate: &Arc, - cx: &mut AsyncApp, - ) -> Result<()> { - delegate.set_status(Some("Uploading remote server"), cx); - - if let Some(parent) = dst_path.parent() { - let parent = parent.display(PathStyle::Posix); - let mkdir = self.shell_kind.prepend_command_prefix("mkdir"); - self.run_wsl_command(&mkdir, &["-p", &parent]) - .await - .context("Failed to create directory when uploading file")?; - } - - let t0 = Instant::now(); - let src_stat = fs::metadata(&src_path) - .await - .with_context(|| format!("source path does not exist: {}", src_path.display()))?; - let size = src_stat.len(); - log::info!( - "uploading remote server to WSL {:?} ({}kb)", - dst_path, - size / 1024 - ); - - let src_path_in_wsl = self.windows_path_to_wsl_path(src_path).await?; - let cp = self.shell_kind.prepend_command_prefix("cp"); - self.run_wsl_command( - &cp, - &["-f", &src_path_in_wsl, &dst_path.display(PathStyle::Posix)], - ) - .await - .map_err(|e| { - anyhow!( - "Failed to copy file {}({}) to WSL {:?}: {}", - src_path.display(), - src_path_in_wsl, - dst_path, - e - ) - })?; - - log::info!("uploaded remote server in {:?}", t0.elapsed()); - Ok(()) - } - - async fn extract_and_install( - &self, - tmp_path: &RelPath, - dst_path: &RelPath, - delegate: &Arc, - cx: &mut AsyncApp, - ) -> Result<()> { - delegate.set_status(Some("Extracting remote server"), cx); - - let tmp_path_str = tmp_path.display(PathStyle::Posix); - let dst_path_str = dst_path.display(PathStyle::Posix); - - // Build extraction script with proper error handling - let script = if tmp_path_str.ends_with(".gz") { - let uncompressed = tmp_path_str.trim_end_matches(".gz"); - format!( - "set -e; gunzip -f '{}' && chmod 755 '{}' && mv -f '{}' '{}'", - tmp_path_str, uncompressed, uncompressed, dst_path_str - ) - } else { - format!( - "set -e; chmod 755 '{}' && mv -f '{}' '{}'", - tmp_path_str, tmp_path_str, dst_path_str - ) - }; - - self.run_wsl_command("sh", &["-c", &script]) - .await - .map_err(|e| anyhow!("Failed to extract server binary: {}", e))?; - Ok(()) - } -} - -#[async_trait(?Send)] -impl RemoteConnection for WslRemoteConnection { - fn start_proxy( - &self, - unique_identifier: String, - reconnect: bool, - incoming_tx: UnboundedSender, - outgoing_rx: UnboundedReceiver, - connection_activity_tx: Sender<()>, - delegate: Arc, - cx: &mut AsyncApp, - ) -> Task> { - delegate.set_status(Some("Starting proxy"), cx); - - let Some(remote_binary_path) = &self.remote_binary_path else { - return Task::ready(Err(anyhow!("Remote binary path not set"))); - }; - - let mut proxy_args = vec![]; - for env_var in ["RUST_LOG", "RUST_BACKTRACE", "ZED_GENERATE_MINIDUMPS"] { - if let Some(value) = std::env::var(env_var).ok() { - // We don't quote the value here as it seems excessive and may result in invalid envs for the - // proxy server. For example, `RUST_LOG='debug'` will result in a warning "invalid logging spec 'debug'', ignoring it" - // in the proxy server. Therefore, we pass the env vars as is. - proxy_args.push(format!("{}={}", env_var, value)); - } - } - - proxy_args.push(remote_binary_path.display(PathStyle::Posix).into_owned()); - proxy_args.push("proxy".to_owned()); - proxy_args.push("--identifier".to_owned()); - proxy_args.push(unique_identifier); - - if reconnect { - proxy_args.push("--reconnect".to_owned()); - } - - let proxy_process = - match wsl_command_impl(&self.connection_options, "env", &proxy_args, false) - .kill_on_drop(true) - .spawn() - { - Ok(process) => process, - Err(error) => { - return Task::ready(Err(anyhow!("failed to spawn remote server: {}", error))); - } - }; - - super::handle_rpc_messages_over_child_process_stdio( - proxy_process, - incoming_tx, - outgoing_rx, - connection_activity_tx, - cx, - ) - } - - fn upload_directory( - &self, - src_path: PathBuf, - dest_path: RemotePathBuf, - cx: &App, - ) -> Task> { - cx.background_spawn({ - let options = self.connection_options.clone(); - async move { - let wsl_src = windows_path_to_wsl_path_impl(&options, &src_path).await?; - - run_wsl_command_impl( - &options, - "cp", - &["-r", &wsl_src, &dest_path.to_string()], - true, - ) - .await - .map_err(|e| { - anyhow!( - "failed to upload directory {} -> {}: {}", - src_path.display(), - dest_path, - e - ) - })?; - - Ok(()) - } - }) - } - - async fn kill(&self) -> Result<()> { - Ok(()) - } - - fn has_been_killed(&self) -> bool { - false - } - - fn shares_network_interface(&self) -> bool { - true - } - - fn build_command( - &self, - program: Option, - args: &[String], - env: &HashMap, - working_dir: Option, - port_forward: Option<(u16, String, u16)>, - ) -> Result { - if port_forward.is_some() { - bail!("WSL shares the network interface with the host system"); - } - - let shell_kind = self.shell_kind; - let working_dir = working_dir - .map(|working_dir| RemotePathBuf::new(working_dir, PathStyle::Posix).to_string()) - .unwrap_or("~".to_string()); - - let mut exec = String::from("exec env "); - - for (k, v) in env.iter() { - write!( - exec, - "{}={} ", - k, - shell_kind.try_quote(v).context("shell quoting")? - )?; - } - - if let Some(program) = program { - write!( - exec, - "{}", - shell_kind - .try_quote_prefix_aware(&program) - .context("shell quoting")? - )?; - for arg in args { - let arg = shell_kind.try_quote(&arg).context("shell quoting")?; - write!(exec, " {}", &arg)?; - } - } else { - write!(&mut exec, "{} -l", self.shell)?; - } - let (command, args) = - ShellBuilder::new(&Shell::Program(self.shell.clone()), false).build(Some(exec), &[]); - - let mut wsl_args = if let Some(user) = &self.connection_options.user { - vec![ - "--distribution".to_string(), - self.connection_options.distro_name.clone(), - "--user".to_string(), - user.clone(), - "--cd".to_string(), - working_dir, - "--".to_string(), - command, - ] - } else { - vec![ - "--distribution".to_string(), - self.connection_options.distro_name.clone(), - "--cd".to_string(), - working_dir, - "--".to_string(), - command, - ] - }; - wsl_args.extend(args); - - Ok(CommandTemplate { - program: "wsl.exe".to_string(), - args: wsl_args, - env: HashMap::default(), - }) - } - - fn build_forward_ports_command( - &self, - _: Vec<(u16, String, u16)>, - ) -> anyhow::Result { - Err(anyhow!("WSL shares a network interface with the host")) - } - - fn connection_options(&self) -> RemoteConnectionOptions { - RemoteConnectionOptions::Wsl(self.connection_options.clone()) - } - - fn path_style(&self) -> PathStyle { - PathStyle::Posix - } - - fn shell(&self) -> String { - self.shell.clone() - } - - fn default_system_shell(&self) -> String { - self.default_system_shell.clone() - } - - fn has_wsl_interop(&self) -> bool { - self.has_wsl_interop - } -} - -/// `wslpath` is a executable available in WSL, it's a linux binary. -/// So it doesn't support Windows style paths. -async fn sanitize_path(path: &Path) -> Result { - let path = smol::fs::canonicalize(path) - .await - .with_context(|| format!("Failed to canonicalize path {}", path.display()))?; - let path_str = path.to_string_lossy(); - - let sanitized = path_str.strip_prefix(r"\\?\").unwrap_or(&path_str); - Ok(sanitized.replace('\\', "/")) -} - -async fn run_wsl_command_with_output_impl( - options: &WslConnectionOptions, - program: &str, - args: &[&str], -) -> Result { - match run_wsl_command_impl(options, program, args, true).await { - Ok(res) => Ok(res), - Err(exec_err) => match run_wsl_command_impl(options, program, args, false).await { - Ok(res) => Ok(res), - Err(e) => Err(e.context(exec_err)), - }, - } -} - -async fn windows_path_to_wsl_path_impl( - options: &WslConnectionOptions, - source: &Path, -) -> Result { - let source = sanitize_path(source).await?; - run_wsl_command_with_output_impl(options, "wslpath", &["-u", &source]).await -} - -async fn run_wsl_command_impl( - options: &WslConnectionOptions, - program: &str, - args: &[&str], - exec: bool, -) -> Result { - let mut command = wsl_command_impl(options, program, args, exec); - let output = command - .output() - .await - .with_context(|| format!("Failed to run command '{:?}'", command))?; - - if !output.status.success() { - return Err(anyhow!( - "Command '{:?}' failed: {}", - command, - String::from_utf8_lossy(&output.stderr).trim() - )); - } - - Ok(String::from_utf8_lossy(&output.stdout).trim().to_owned()) -} - -/// Creates a new `wsl.exe` command that runs the given program with the given arguments. -/// -/// If `exec` is true, the command will be executed in the WSL environment without spawning a new shell. -fn wsl_command_impl( - options: &WslConnectionOptions, - program: &str, - args: &[impl AsRef], - exec: bool, -) -> process::Command { - let mut command = util::command::new_smol_command("wsl.exe"); - - if let Some(user) = &options.user { - command.arg("--user").arg(user); - } - - command - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .arg("--distribution") - .arg(&options.distro_name) - .arg("--cd") - .arg("~"); - - if exec { - command.arg("--exec"); - } - - command.arg(program).args(args); - - log::debug!("wsl {:?}", command); - command -} diff --git a/crates/remote_server/Cargo.toml b/crates/remote_server/Cargo.toml deleted file mode 100644 index 114dc777c1..0000000000 --- a/crates/remote_server/Cargo.toml +++ /dev/null @@ -1,109 +0,0 @@ -[package] -name = "remote_server" -description = "Daemon used for remote editing" -edition.workspace = true -version = "0.1.0" -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/remote_server.rs" -doctest = false - -[[bin]] -name = "remote_server" - -[features] -default = [] -debug-embed = ["dep:rust-embed"] -test-support = ["fs/test-support"] - -[dependencies] -anyhow.workspace = true -askpass.workspace = true -clap.workspace = true -client.workspace = true -dap_adapters.workspace = true -debug_adapter_extension.workspace = true -env_logger.workspace = true -extension.workspace = true -extension_host.workspace = true -fs.workspace = true -futures.workspace = true -git.workspace = true -git_hosting_providers.workspace = true -git2 = { workspace = true, features = ["vendored-libgit2"] } -gpui.workspace = true -gpui_tokio.workspace = true -http_client.workspace = true -image.workspace = true -json_schema_store.workspace = true -language.workspace = true -language_extension.workspace = true -languages.workspace = true -log.workspace = true -lsp.workspace = true -node_runtime.workspace = true -paths.workspace = true -project.workspace = true -proto.workspace = true -release_channel.workspace = true -remote.workspace = true -reqwest_client.workspace = true -rpc.workspace = true -rust-embed = { workspace = true, optional = true, features = ["debug-embed"] } -semver.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -shellexpand.workspace = true -smol.workspace = true -sysinfo.workspace = true -task.workspace = true -util.workspace = true -watch.workspace = true -worktree.workspace = true -thiserror.workspace = true -rayon.workspace = true - -[target.'cfg(not(windows))'.dependencies] -crashes.workspace = true -crash-handler.workspace = true -fork.workspace = true -libc.workspace = true -minidumper.workspace = true - -[dev-dependencies] -action_log.workspace = true -agent = { workspace = true, features = ["test-support"] } -client = { workspace = true, features = ["test-support"] } -clock = { workspace = true, features = ["test-support"] } -collections.workspace = true -dap = { workspace = true, features = ["test-support"] } -editor = { workspace = true, features = ["test-support"] } -workspace = { workspace = true, features = ["test-support"] } -fs = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -http_client = { workspace = true, features = ["test-support"] } -language = { workspace = true, features = ["test-support"] } -node_runtime = { workspace = true, features = ["test-support"] } -pretty_assertions.workspace = true -project = { workspace = true, features = ["test-support"] } -remote = { workspace = true, features = ["test-support"] } -theme = { workspace = true, features = ["test-support"] } -language_model = { workspace = true, features = ["test-support"] } -lsp = { workspace = true, features = ["test-support"] } -prompt_store.workspace = true -unindent.workspace = true -serde_json.workspace = true -zlog.workspace = true - -[build-dependencies] -cargo_toml.workspace = true -toml.workspace = true - -[package.metadata.cargo-machete] -ignored = ["git2", "rust-embed", "paths"] diff --git a/crates/remote_server/LICENSE-GPL b/crates/remote_server/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/remote_server/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/remote_server/build.rs b/crates/remote_server/build.rs deleted file mode 100644 index 3ad13d3d6e..0000000000 --- a/crates/remote_server/build.rs +++ /dev/null @@ -1,34 +0,0 @@ -#![allow(clippy::disallowed_methods, reason = "build scripts are exempt")] -use std::process::Command; - -const ZED_MANIFEST: &str = include_str!("../zed/Cargo.toml"); - -fn main() { - let zed_cargo_toml: cargo_toml::Manifest = - toml::from_str(ZED_MANIFEST).expect("failed to parse zed Cargo.toml"); - println!( - "cargo:rustc-env=ZED_PKG_VERSION={}", - zed_cargo_toml.package.unwrap().version.unwrap() - ); - println!( - "cargo:rustc-env=TARGET={}", - std::env::var("TARGET").unwrap() - ); - - // Populate git sha environment variable if git is available - println!("cargo:rerun-if-changed=../../.git/logs/HEAD"); - if let Some(output) = Command::new("git") - .args(["rev-parse", "HEAD"]) - .output() - .ok() - .filter(|output| output.status.success()) - { - let git_sha = String::from_utf8_lossy(&output.stdout); - let git_sha = git_sha.trim(); - - println!("cargo:rustc-env=ZED_COMMIT_SHA={git_sha}"); - } - if let Some(build_identifier) = option_env!("GITHUB_RUN_NUMBER") { - println!("cargo:rustc-env=ZED_BUILD_ID={build_identifier}"); - } -} diff --git a/crates/remote_server/src/headless_project.rs b/crates/remote_server/src/headless_project.rs deleted file mode 100644 index 361e74579c..0000000000 --- a/crates/remote_server/src/headless_project.rs +++ /dev/null @@ -1,893 +0,0 @@ -use anyhow::{Context as _, Result, anyhow}; -use language::File; -use lsp::LanguageServerId; - -use extension::ExtensionHostProxy; -use extension_host::headless_host::HeadlessExtensionStore; -use fs::Fs; -use gpui::{App, AppContext as _, AsyncApp, Context, Entity, PromptLevel}; -use http_client::HttpClient; -use language::{Buffer, BufferEvent, LanguageRegistry, proto::serialize_operation}; -use node_runtime::NodeRuntime; -use project::{ - LspStore, LspStoreEvent, ManifestTree, PrettierStore, ProjectEnvironment, ProjectPath, - ToolchainStore, WorktreeId, - agent_server_store::AgentServerStore, - buffer_store::{BufferStore, BufferStoreEvent}, - debugger::{breakpoint_store::BreakpointStore, dap_store::DapStore}, - git_store::GitStore, - image_store::ImageId, - lsp_store::log_store::{self, GlobalLogStore, LanguageServerKind, LogKind}, - project_settings::SettingsObserver, - search::SearchQuery, - task_store::TaskStore, - worktree_store::WorktreeStore, -}; -use rpc::{ - AnyProtoClient, TypedEnvelope, - proto::{self, REMOTE_SERVER_PEER_ID, REMOTE_SERVER_PROJECT_ID}, -}; - -use settings::initial_server_settings_content; -use smol::stream::StreamExt; -use std::{ - num::NonZeroU64, - path::{Path, PathBuf}, - sync::{ - Arc, - atomic::{AtomicU64, AtomicUsize, Ordering}, - }, -}; -use sysinfo::{ProcessRefreshKind, RefreshKind, System, UpdateKind}; -use util::{ResultExt, paths::PathStyle, rel_path::RelPath}; -use worktree::Worktree; - -pub struct HeadlessProject { - pub fs: Arc, - pub session: AnyProtoClient, - pub worktree_store: Entity, - pub buffer_store: Entity, - pub lsp_store: Entity, - pub task_store: Entity, - pub dap_store: Entity, - pub agent_server_store: Entity, - pub settings_observer: Entity, - pub next_entry_id: Arc, - pub languages: Arc, - pub extensions: Entity, - pub git_store: Entity, - pub environment: Entity, - // Used mostly to keep alive the toolchain store for RPC handlers. - // Local variant is used within LSP store, but that's a separate entity. - pub _toolchain_store: Entity, -} - -pub struct HeadlessAppState { - pub session: AnyProtoClient, - pub fs: Arc, - pub http_client: Arc, - pub node_runtime: NodeRuntime, - pub languages: Arc, - pub extension_host_proxy: Arc, -} - -impl HeadlessProject { - pub fn init(cx: &mut App) { - settings::init(cx); - log_store::init(true, cx); - } - - pub fn new( - HeadlessAppState { - session, - fs, - http_client, - node_runtime, - languages, - extension_host_proxy: proxy, - }: HeadlessAppState, - cx: &mut Context, - ) -> Self { - debug_adapter_extension::init(proxy.clone(), cx); - languages::init(languages.clone(), fs.clone(), node_runtime.clone(), cx); - - let worktree_store = cx.new(|cx| { - let mut store = WorktreeStore::local(true, fs.clone()); - store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx); - store - }); - - let environment = - cx.new(|cx| ProjectEnvironment::new(None, worktree_store.downgrade(), None, true, cx)); - let manifest_tree = ManifestTree::new(worktree_store.clone(), cx); - let toolchain_store = cx.new(|cx| { - ToolchainStore::local( - languages.clone(), - worktree_store.clone(), - environment.clone(), - manifest_tree.clone(), - fs.clone(), - cx, - ) - }); - - let buffer_store = cx.new(|cx| { - let mut buffer_store = BufferStore::local(worktree_store.clone(), cx); - buffer_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx); - buffer_store - }); - - let breakpoint_store = - cx.new(|_| BreakpointStore::local(worktree_store.clone(), buffer_store.clone())); - - let dap_store = cx.new(|cx| { - let mut dap_store = DapStore::new_local( - http_client.clone(), - node_runtime.clone(), - fs.clone(), - environment.clone(), - toolchain_store.read(cx).as_language_toolchain_store(), - worktree_store.clone(), - breakpoint_store.clone(), - true, - cx, - ); - dap_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx); - dap_store - }); - - let git_store = cx.new(|cx| { - let mut store = GitStore::local( - &worktree_store, - buffer_store.clone(), - environment.clone(), - fs.clone(), - cx, - ); - store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx); - store - }); - - let prettier_store = cx.new(|cx| { - PrettierStore::new( - node_runtime.clone(), - fs.clone(), - languages.clone(), - worktree_store.clone(), - cx, - ) - }); - - let task_store = cx.new(|cx| { - let mut task_store = TaskStore::local( - buffer_store.downgrade(), - worktree_store.clone(), - toolchain_store.read(cx).as_language_toolchain_store(), - environment.clone(), - cx, - ); - task_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx); - task_store - }); - let settings_observer = cx.new(|cx| { - let mut observer = SettingsObserver::new_local( - fs.clone(), - worktree_store.clone(), - task_store.clone(), - cx, - ); - observer.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx); - observer - }); - - let lsp_store = cx.new(|cx| { - let mut lsp_store = LspStore::new_local( - buffer_store.clone(), - worktree_store.clone(), - prettier_store.clone(), - toolchain_store - .read(cx) - .as_local_store() - .expect("Toolchain store to be local") - .clone(), - environment.clone(), - manifest_tree, - languages.clone(), - http_client.clone(), - fs.clone(), - cx, - ); - lsp_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx); - lsp_store - }); - - let agent_server_store = cx.new(|cx| { - let mut agent_server_store = AgentServerStore::local( - node_runtime.clone(), - fs.clone(), - environment.clone(), - http_client.clone(), - cx, - ); - agent_server_store.shared(REMOTE_SERVER_PROJECT_ID, session.clone(), cx); - agent_server_store - }); - - cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach(); - language_extension::init( - language_extension::LspAccess::ViaLspStore(lsp_store.clone()), - proxy.clone(), - languages.clone(), - ); - - cx.subscribe(&buffer_store, |_this, _buffer_store, event, cx| { - if let BufferStoreEvent::BufferAdded(buffer) = event { - cx.subscribe(buffer, Self::on_buffer_event).detach(); - } - }) - .detach(); - - let extensions = HeadlessExtensionStore::new( - fs.clone(), - http_client.clone(), - paths::remote_extensions_dir().to_path_buf(), - proxy, - node_runtime, - cx, - ); - - // local_machine -> ssh handlers - session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &worktree_store); - session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &buffer_store); - session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &cx.entity()); - session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &lsp_store); - session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &task_store); - session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &toolchain_store); - session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &dap_store); - session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &settings_observer); - session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &git_store); - session.subscribe_to_entity(REMOTE_SERVER_PROJECT_ID, &agent_server_store); - - session.add_request_handler(cx.weak_entity(), Self::handle_list_remote_directory); - session.add_request_handler(cx.weak_entity(), Self::handle_get_path_metadata); - session.add_request_handler(cx.weak_entity(), Self::handle_shutdown_remote_server); - session.add_request_handler(cx.weak_entity(), Self::handle_ping); - session.add_request_handler(cx.weak_entity(), Self::handle_get_processes); - - session.add_entity_request_handler(Self::handle_add_worktree); - session.add_request_handler(cx.weak_entity(), Self::handle_remove_worktree); - - session.add_entity_request_handler(Self::handle_open_buffer_by_path); - session.add_entity_request_handler(Self::handle_open_new_buffer); - session.add_entity_request_handler(Self::handle_find_search_candidates); - session.add_entity_request_handler(Self::handle_open_server_settings); - session.add_entity_request_handler(Self::handle_get_directory_environment); - session.add_entity_message_handler(Self::handle_toggle_lsp_logs); - session.add_entity_request_handler(Self::handle_open_image_by_path); - - session.add_entity_request_handler(BufferStore::handle_update_buffer); - session.add_entity_message_handler(BufferStore::handle_close_buffer); - - session.add_request_handler( - extensions.downgrade(), - HeadlessExtensionStore::handle_sync_extensions, - ); - session.add_request_handler( - extensions.downgrade(), - HeadlessExtensionStore::handle_install_extension, - ); - - BufferStore::init(&session); - WorktreeStore::init(&session); - SettingsObserver::init(&session); - LspStore::init(&session); - TaskStore::init(Some(&session)); - ToolchainStore::init(&session); - DapStore::init(&session, cx); - // todo(debugger): Re init breakpoint store when we set it up for collab - // BreakpointStore::init(&client); - GitStore::init(&session); - AgentServerStore::init_headless(&session); - - HeadlessProject { - next_entry_id: Default::default(), - session, - settings_observer, - fs, - worktree_store, - buffer_store, - lsp_store, - task_store, - dap_store, - agent_server_store, - languages, - extensions, - git_store, - environment, - _toolchain_store: toolchain_store, - } - } - - fn on_buffer_event( - &mut self, - buffer: Entity, - event: &BufferEvent, - cx: &mut Context, - ) { - if let BufferEvent::Operation { - operation, - is_local: true, - } = event - { - cx.background_spawn(self.session.request(proto::UpdateBuffer { - project_id: REMOTE_SERVER_PROJECT_ID, - buffer_id: buffer.read(cx).remote_id().to_proto(), - operations: vec![serialize_operation(operation)], - })) - .detach() - } - } - - fn on_lsp_store_event( - &mut self, - lsp_store: Entity, - event: &LspStoreEvent, - cx: &mut Context, - ) { - match event { - LspStoreEvent::LanguageServerAdded(id, name, worktree_id) => { - let log_store = cx - .try_global::() - .map(|lsp_logs| lsp_logs.0.clone()); - if let Some(log_store) = log_store { - log_store.update(cx, |log_store, cx| { - log_store.add_language_server( - LanguageServerKind::LocalSsh { - lsp_store: self.lsp_store.downgrade(), - }, - *id, - Some(name.clone()), - *worktree_id, - lsp_store.read(cx).language_server_for_id(*id), - cx, - ); - }); - } - } - LspStoreEvent::LanguageServerRemoved(id) => { - let log_store = cx - .try_global::() - .map(|lsp_logs| lsp_logs.0.clone()); - if let Some(log_store) = log_store { - log_store.update(cx, |log_store, cx| { - log_store.remove_language_server(*id, cx); - }); - } - } - LspStoreEvent::LanguageServerUpdate { - language_server_id, - name, - message, - } => { - self.session - .send(proto::UpdateLanguageServer { - project_id: REMOTE_SERVER_PROJECT_ID, - server_name: name.as_ref().map(|name| name.to_string()), - language_server_id: language_server_id.to_proto(), - variant: Some(message.clone()), - }) - .log_err(); - } - LspStoreEvent::Notification(message) => { - self.session - .send(proto::Toast { - project_id: REMOTE_SERVER_PROJECT_ID, - notification_id: "lsp".to_string(), - message: message.clone(), - }) - .log_err(); - } - LspStoreEvent::LanguageServerPrompt(prompt) => { - let request = self.session.request(proto::LanguageServerPromptRequest { - project_id: REMOTE_SERVER_PROJECT_ID, - actions: prompt - .actions - .iter() - .map(|action| action.title.to_string()) - .collect(), - level: Some(prompt_to_proto(prompt)), - lsp_name: prompt.lsp_name.clone(), - message: prompt.message.clone(), - }); - let prompt = prompt.clone(); - cx.background_spawn(async move { - let response = request.await?; - if let Some(action_response) = response.action_response { - prompt.respond(action_response as usize).await; - } - anyhow::Ok(()) - }) - .detach(); - } - _ => {} - } - } - - pub async fn handle_add_worktree( - this: Entity, - message: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - use client::ErrorCodeExt; - let fs = this.read_with(&cx, |this, _| this.fs.clone())?; - let path = PathBuf::from(shellexpand::tilde(&message.payload.path).to_string()); - - let canonicalized = match fs.canonicalize(&path).await { - Ok(path) => path, - Err(e) => { - let mut parent = path - .parent() - .ok_or(e) - .with_context(|| format!("{path:?} does not exist"))?; - if parent == Path::new("") { - parent = util::paths::home_dir(); - } - let parent = fs.canonicalize(parent).await.map_err(|_| { - anyhow!( - proto::ErrorCode::DevServerProjectPathDoesNotExist - .with_tag("path", path.to_string_lossy().as_ref()) - ) - })?; - parent.join(path.file_name().unwrap()) - } - }; - - let worktree = this - .read_with(&cx.clone(), |this, _| { - Worktree::local( - Arc::from(canonicalized.as_path()), - message.payload.visible, - this.fs.clone(), - this.next_entry_id.clone(), - true, - &mut cx, - ) - })? - .await?; - - let response = this.read_with(&cx, |_, cx| { - let worktree = worktree.read(cx); - proto::AddWorktreeResponse { - worktree_id: worktree.id().to_proto(), - canonicalized_path: canonicalized.to_string_lossy().into_owned(), - } - })?; - - // We spawn this asynchronously, so that we can send the response back - // *before* `worktree_store.add()` can send out UpdateProject requests - // to the client about the new worktree. - // - // That lets the client manage the reference/handles of the newly-added - // worktree, before getting interrupted by an UpdateProject request. - // - // This fixes the problem of the client sending the AddWorktree request, - // headless project sending out a project update, client receiving it - // and immediately dropping the reference of the new client, causing it - // to be dropped on the headless project, and the client only then - // receiving a response to AddWorktree. - cx.spawn(async move |cx| { - this.update(cx, |this, cx| { - this.worktree_store.update(cx, |worktree_store, cx| { - worktree_store.add(&worktree, cx); - }); - }) - .log_err(); - }) - .detach(); - - Ok(response) - } - - pub async fn handle_remove_worktree( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); - this.update(&mut cx, |this, cx| { - this.worktree_store.update(cx, |worktree_store, cx| { - worktree_store.remove_worktree(worktree_id, cx); - }); - })?; - Ok(proto::Ack {}) - } - - pub async fn handle_open_buffer_by_path( - this: Entity, - message: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let worktree_id = WorktreeId::from_proto(message.payload.worktree_id); - let path = RelPath::from_proto(&message.payload.path)?; - let (buffer_store, buffer) = this.update(&mut cx, |this, cx| { - let buffer_store = this.buffer_store.clone(); - let buffer = this.buffer_store.update(cx, |buffer_store, cx| { - buffer_store.open_buffer(ProjectPath { worktree_id, path }, cx) - }); - anyhow::Ok((buffer_store, buffer)) - })??; - - let buffer = buffer.await?; - let buffer_id = buffer.read_with(&cx, |b, _| b.remote_id())?; - buffer_store.update(&mut cx, |buffer_store, cx| { - buffer_store - .create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx) - .detach_and_log_err(cx); - })?; - - Ok(proto::OpenBufferResponse { - buffer_id: buffer_id.to_proto(), - }) - } - - pub async fn handle_open_image_by_path( - this: Entity, - message: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - static NEXT_ID: AtomicU64 = AtomicU64::new(1); - let worktree_id = WorktreeId::from_proto(message.payload.worktree_id); - let path = RelPath::from_proto(&message.payload.path)?; - let project_id = message.payload.project_id; - use proto::create_image_for_peer::Variant; - - let (worktree_store, session) = this.read_with(&cx, |this, _| { - (this.worktree_store.clone(), this.session.clone()) - })?; - - let worktree = worktree_store - .read_with(&cx, |store, cx| store.worktree_for_id(worktree_id, cx))? - .context("worktree not found")?; - - let load_task = worktree.update(&mut cx, |worktree, cx| { - worktree.load_binary_file(path.as_ref(), cx) - })?; - - let loaded_file = load_task.await?; - let content = loaded_file.content; - let file = loaded_file.file; - - let proto_file = worktree.read_with(&cx, |_worktree, cx| file.to_proto(cx))?; - let image_id = - ImageId::from(NonZeroU64::new(NEXT_ID.fetch_add(1, Ordering::Relaxed)).unwrap()); - - let format = image::guess_format(&content) - .map(|f| format!("{:?}", f).to_lowercase()) - .unwrap_or_else(|_| "unknown".to_string()); - - let state = proto::ImageState { - id: image_id.to_proto(), - file: Some(proto_file), - content_size: content.len() as u64, - format, - }; - - session.send(proto::CreateImageForPeer { - project_id, - peer_id: Some(REMOTE_SERVER_PEER_ID), - variant: Some(Variant::State(state)), - })?; - - const CHUNK_SIZE: usize = 1024 * 1024; // 1MB chunks - for chunk in content.chunks(CHUNK_SIZE) { - session.send(proto::CreateImageForPeer { - project_id, - peer_id: Some(REMOTE_SERVER_PEER_ID), - variant: Some(Variant::Chunk(proto::ImageChunk { - image_id: image_id.to_proto(), - data: chunk.to_vec(), - })), - })?; - } - - Ok(proto::OpenImageResponse { - image_id: image_id.to_proto(), - }) - } - - pub async fn handle_open_new_buffer( - this: Entity, - _message: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let (buffer_store, buffer) = this.update(&mut cx, |this, cx| { - let buffer_store = this.buffer_store.clone(); - let buffer = this - .buffer_store - .update(cx, |buffer_store, cx| buffer_store.create_buffer(true, cx)); - anyhow::Ok((buffer_store, buffer)) - })??; - - let buffer = buffer.await?; - let buffer_id = buffer.read_with(&cx, |b, _| b.remote_id())?; - buffer_store.update(&mut cx, |buffer_store, cx| { - buffer_store - .create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx) - .detach_and_log_err(cx); - })?; - - Ok(proto::OpenBufferResponse { - buffer_id: buffer_id.to_proto(), - }) - } - - async fn handle_toggle_lsp_logs( - _: Entity, - envelope: TypedEnvelope, - cx: AsyncApp, - ) -> Result<()> { - let server_id = LanguageServerId::from_proto(envelope.payload.server_id); - cx.update(|cx| { - let log_store = cx - .try_global::() - .map(|global_log_store| global_log_store.0.clone()) - .context("lsp logs store is missing")?; - let toggled_log_kind = - match proto::toggle_lsp_logs::LogType::from_i32(envelope.payload.log_type) - .context("invalid log type")? - { - proto::toggle_lsp_logs::LogType::Log => LogKind::Logs, - proto::toggle_lsp_logs::LogType::Trace => LogKind::Trace, - proto::toggle_lsp_logs::LogType::Rpc => LogKind::Rpc, - }; - log_store.update(cx, |log_store, _| { - log_store.toggle_lsp_logs(server_id, envelope.payload.enabled, toggled_log_kind); - }); - anyhow::Ok(()) - })??; - - Ok(()) - } - - async fn handle_open_server_settings( - this: Entity, - _: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let settings_path = paths::settings_file(); - let (worktree, path) = this - .update(&mut cx, |this, cx| { - this.worktree_store.update(cx, |worktree_store, cx| { - worktree_store.find_or_create_worktree(settings_path, false, cx) - }) - })? - .await?; - - let (buffer, buffer_store) = this.update(&mut cx, |this, cx| { - let buffer = this.buffer_store.update(cx, |buffer_store, cx| { - buffer_store.open_buffer( - ProjectPath { - worktree_id: worktree.read(cx).id(), - path: path, - }, - cx, - ) - }); - - (buffer, this.buffer_store.clone()) - })?; - - let buffer = buffer.await?; - - let buffer_id = cx.update(|cx| { - if buffer.read(cx).is_empty() { - buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, initial_server_settings_content())], None, cx) - }); - } - - let buffer_id = buffer.read(cx).remote_id(); - - buffer_store.update(cx, |buffer_store, cx| { - buffer_store - .create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx) - .detach_and_log_err(cx); - }); - - buffer_id - })?; - - Ok(proto::OpenBufferResponse { - buffer_id: buffer_id.to_proto(), - }) - } - - async fn handle_find_search_candidates( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let message = envelope.payload; - let query = SearchQuery::from_proto( - message.query.context("missing query field")?, - PathStyle::local(), - )?; - let results = this.update(&mut cx, |this, cx| { - project::Search::local( - this.fs.clone(), - this.buffer_store.clone(), - this.worktree_store.clone(), - message.limit as _, - cx, - ) - .into_handle(query, cx) - .matching_buffers(cx) - })?; - - let mut response = proto::FindSearchCandidatesResponse { - buffer_ids: Vec::new(), - }; - - let buffer_store = this.read_with(&cx, |this, _| this.buffer_store.clone())?; - - while let Ok(buffer) = results.recv().await { - let buffer_id = buffer.read_with(&cx, |this, _| this.remote_id())?; - response.buffer_ids.push(buffer_id.to_proto()); - buffer_store - .update(&mut cx, |buffer_store, cx| { - buffer_store.create_buffer_for_peer(&buffer, REMOTE_SERVER_PEER_ID, cx) - })? - .await?; - } - - Ok(response) - } - - async fn handle_list_remote_directory( - this: Entity, - envelope: TypedEnvelope, - cx: AsyncApp, - ) -> Result { - let fs = cx.read_entity(&this, |this, _| this.fs.clone())?; - let expanded = PathBuf::from(shellexpand::tilde(&envelope.payload.path).to_string()); - let check_info = envelope - .payload - .config - .as_ref() - .is_some_and(|config| config.is_dir); - - let mut entries = Vec::new(); - let mut entry_info = Vec::new(); - let mut response = fs.read_dir(&expanded).await?; - while let Some(path) = response.next().await { - let path = path?; - if let Some(file_name) = path.file_name() { - entries.push(file_name.to_string_lossy().into_owned()); - if check_info { - let is_dir = fs.is_dir(&path).await; - entry_info.push(proto::EntryInfo { is_dir }); - } - } - } - Ok(proto::ListRemoteDirectoryResponse { - entries, - entry_info, - }) - } - - async fn handle_get_path_metadata( - this: Entity, - envelope: TypedEnvelope, - cx: AsyncApp, - ) -> Result { - let fs = cx.read_entity(&this, |this, _| this.fs.clone())?; - let expanded = PathBuf::from(shellexpand::tilde(&envelope.payload.path).to_string()); - - let metadata = fs.metadata(&expanded).await?; - let is_dir = metadata.map(|metadata| metadata.is_dir).unwrap_or(false); - - Ok(proto::GetPathMetadataResponse { - exists: metadata.is_some(), - is_dir, - path: expanded.to_string_lossy().into_owned(), - }) - } - - async fn handle_shutdown_remote_server( - _this: Entity, - _envelope: TypedEnvelope, - cx: AsyncApp, - ) -> Result { - cx.spawn(async move |cx| { - cx.update(|cx| { - // TODO: This is a hack, because in a headless project, shutdown isn't executed - // when calling quit, but it should be. - cx.shutdown(); - cx.quit(); - }) - }) - .detach(); - - Ok(proto::Ack {}) - } - - pub async fn handle_ping( - _this: Entity, - _envelope: TypedEnvelope, - _cx: AsyncApp, - ) -> Result { - log::debug!("Received ping from client"); - Ok(proto::Ack {}) - } - - async fn handle_get_processes( - _this: Entity, - _envelope: TypedEnvelope, - _cx: AsyncApp, - ) -> Result { - let mut processes = Vec::new(); - let refresh_kind = RefreshKind::nothing().with_processes( - ProcessRefreshKind::nothing() - .without_tasks() - .with_cmd(UpdateKind::Always), - ); - - for process in System::new_with_specifics(refresh_kind) - .processes() - .values() - { - let name = process.name().to_string_lossy().into_owned(); - let command = process - .cmd() - .iter() - .map(|s| s.to_string_lossy().into_owned()) - .collect::>(); - - processes.push(proto::ProcessInfo { - pid: process.pid().as_u32(), - name, - command, - }); - } - - processes.sort_by_key(|p| p.name.clone()); - - Ok(proto::GetProcessesResponse { processes }) - } - - async fn handle_get_directory_environment( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - let shell = task::shell_from_proto(envelope.payload.shell.context("missing shell")?)?; - let directory = PathBuf::from(envelope.payload.directory); - let environment = this - .update(&mut cx, |this, cx| { - this.environment.update(cx, |environment, cx| { - environment.local_directory_environment(&shell, directory.into(), cx) - }) - })? - .await - .context("failed to get directory environment")? - .into_iter() - .collect(); - Ok(proto::DirectoryEnvironment { environment }) - } -} - -fn prompt_to_proto( - prompt: &project::LanguageServerPromptRequest, -) -> proto::language_server_prompt_request::Level { - match prompt.level { - PromptLevel::Info => proto::language_server_prompt_request::Level::Info( - proto::language_server_prompt_request::Info {}, - ), - PromptLevel::Warning => proto::language_server_prompt_request::Level::Warning( - proto::language_server_prompt_request::Warning {}, - ), - PromptLevel::Critical => proto::language_server_prompt_request::Level::Critical( - proto::language_server_prompt_request::Critical {}, - ), - } -} diff --git a/crates/remote_server/src/main.rs b/crates/remote_server/src/main.rs deleted file mode 100644 index 368c7cb639..0000000000 --- a/crates/remote_server/src/main.rs +++ /dev/null @@ -1,55 +0,0 @@ -#![cfg_attr(target_os = "windows", allow(unused, dead_code))] - -use clap::Parser; -use remote_server::Commands; -use std::path::PathBuf; - -#[derive(Parser)] -#[command(disable_version_flag = true)] -struct Cli { - #[command(subcommand)] - command: Option, - /// Used for SSH/Git password authentication, to remove the need for netcat as a dependency, - /// by having Zed act like netcat communicating over a Unix socket. - #[arg(long, hide = true)] - askpass: Option, - /// Used for recording minidumps on crashes by having the server run a separate - /// process communicating over a socket. - #[arg(long, hide = true)] - crash_handler: Option, - /// Used for loading the environment from the project. - #[arg(long, hide = true)] - printenv: bool, -} - -#[cfg(windows)] -fn main() { - unimplemented!() -} - -#[cfg(not(windows))] -fn main() -> anyhow::Result<()> { - let cli = Cli::parse(); - - if let Some(socket_path) = &cli.askpass { - askpass::main(socket_path); - return Ok(()); - } - - if let Some(socket) = &cli.crash_handler { - crashes::crash_server(socket.as_path()); - return Ok(()); - } - - if cli.printenv { - util::shell_env::print_env(); - return Ok(()); - } - - if let Some(command) = cli.command { - remote_server::run(command) - } else { - eprintln!("usage: remote "); - std::process::exit(1); - } -} diff --git a/crates/remote_server/src/remote_editing_tests.rs b/crates/remote_server/src/remote_editing_tests.rs deleted file mode 100644 index a91d1d055d..0000000000 --- a/crates/remote_server/src/remote_editing_tests.rs +++ /dev/null @@ -1,1981 +0,0 @@ -/// todo(windows) -/// The tests in this file assume that server_cx is running on Windows too. -/// We neead to find a way to test Windows-Non-Windows interactions. -use crate::headless_project::HeadlessProject; -use agent::{AgentTool, ReadFileTool, ReadFileToolInput, Templates, Thread, ToolCallEventStream}; -use client::{Client, UserStore}; -use clock::FakeSystemClock; -use collections::{HashMap, HashSet}; -use language_model::{LanguageModelToolResultContent, fake_provider::FakeLanguageModel}; -use prompt_store::ProjectContext; - -use extension::ExtensionHostProxy; -use fs::{FakeFs, Fs}; -use gpui::{AppContext as _, Entity, SharedString, TestAppContext}; -use http_client::{BlockedHttpClient, FakeHttpClient}; -use language::{ - Buffer, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageRegistry, LineEnding, - language_settings::{AllLanguageSettings, language_settings}, -}; -use lsp::{CompletionContext, CompletionResponse, CompletionTriggerKind, LanguageServerName}; -use node_runtime::NodeRuntime; -use project::{ - ProgressToken, Project, - agent_server_store::AgentServerCommand, - search::{SearchQuery, SearchResult}, -}; -use remote::RemoteClient; -use serde_json::json; -use settings::{Settings, SettingsLocation, SettingsStore, initial_server_settings_content}; -use smol::stream::StreamExt; -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; -use unindent::Unindent as _; -use util::{path, rel_path::rel_path}; - -#[gpui::test] -async fn test_basic_remote_editing(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - path!("/code"), - json!({ - "project1": { - ".git": {}, - "README.md": "# project 1", - "src": { - "lib.rs": "fn one() -> usize { 1 }" - } - }, - "project2": { - "README.md": "# project 2", - }, - }), - ) - .await; - fs.set_index_for_repo( - Path::new(path!("/code/project1/.git")), - &[("src/lib.rs", "fn one() -> usize { 0 }".into())], - ); - - let (project, _headless) = init_test(&fs, cx, server_cx).await; - let (worktree, _) = project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/code/project1"), true, cx) - }) - .await - .unwrap(); - - // The client sees the worktree's contents. - cx.executor().run_until_parked(); - let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id()); - worktree.update(cx, |worktree, _cx| { - assert_eq!( - worktree.paths().collect::>(), - vec![ - rel_path("README.md"), - rel_path("src"), - rel_path("src/lib.rs"), - ] - ); - }); - - // The user opens a buffer in the remote worktree. The buffer's - // contents are loaded from the remote filesystem. - let buffer = project - .update(cx, |project, cx| { - project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx) - }) - .await - .unwrap(); - let diff = project - .update(cx, |project, cx| { - project.open_unstaged_diff(buffer.clone(), cx) - }) - .await - .unwrap(); - - diff.update(cx, |diff, _| { - assert_eq!(diff.base_text_string().unwrap(), "fn one() -> usize { 0 }"); - }); - - buffer.update(cx, |buffer, cx| { - assert_eq!(buffer.text(), "fn one() -> usize { 1 }"); - let ix = buffer.text().find('1').unwrap(); - buffer.edit([(ix..ix + 1, "100")], None, cx); - }); - - // The user saves the buffer. The new contents are written to the - // remote filesystem. - project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) - .await - .unwrap(); - assert_eq!( - fs.load("/code/project1/src/lib.rs".as_ref()).await.unwrap(), - "fn one() -> usize { 100 }" - ); - - // A new file is created in the remote filesystem. The user - // sees the new file. - fs.save( - path!("/code/project1/src/main.rs").as_ref(), - &"fn main() {}".into(), - Default::default(), - ) - .await - .unwrap(); - cx.executor().run_until_parked(); - worktree.update(cx, |worktree, _cx| { - assert_eq!( - worktree.paths().collect::>(), - vec![ - rel_path("README.md"), - rel_path("src"), - rel_path("src/lib.rs"), - rel_path("src/main.rs"), - ] - ); - }); - - // A file that is currently open in a buffer is renamed. - fs.rename( - path!("/code/project1/src/lib.rs").as_ref(), - path!("/code/project1/src/lib2.rs").as_ref(), - Default::default(), - ) - .await - .unwrap(); - cx.executor().run_until_parked(); - buffer.update(cx, |buffer, _| { - assert_eq!(&**buffer.file().unwrap().path(), rel_path("src/lib2.rs")); - }); - - fs.set_index_for_repo( - Path::new(path!("/code/project1/.git")), - &[("src/lib2.rs", "fn one() -> usize { 100 }".into())], - ); - cx.executor().run_until_parked(); - diff.update(cx, |diff, _| { - assert_eq!( - diff.base_text_string().unwrap(), - "fn one() -> usize { 100 }" - ); - }); -} - -#[gpui::test] -async fn test_remote_project_search(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - path!("/code"), - json!({ - "project1": { - ".git": {}, - "README.md": "# project 1", - "src": { - "lib.rs": "fn one() -> usize { 1 }" - } - }, - }), - ) - .await; - - let (project, headless) = init_test(&fs, cx, server_cx).await; - - project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/code/project1"), true, cx) - }) - .await - .unwrap(); - - cx.run_until_parked(); - - async fn do_search(project: &Entity, mut cx: TestAppContext) -> Entity { - let receiver = project.update(&mut cx, |project, cx| { - project.search( - SearchQuery::text( - "project", - false, - true, - false, - Default::default(), - Default::default(), - false, - None, - ) - .unwrap(), - cx, - ) - }); - - let first_response = receiver.recv().await.unwrap(); - let SearchResult::Buffer { buffer, .. } = first_response else { - panic!("incorrect result"); - }; - buffer.update(&mut cx, |buffer, cx| { - assert_eq!( - buffer.file().unwrap().full_path(cx).to_string_lossy(), - path!("project1/README.md") - ) - }); - - assert!(receiver.recv().await.is_err()); - buffer - } - - let buffer = do_search(&project, cx.clone()).await; - - // test that the headless server is tracking which buffers we have open correctly. - cx.run_until_parked(); - headless.update(server_cx, |headless, cx| { - assert!(headless.buffer_store.read(cx).has_shared_buffers()) - }); - do_search(&project, cx.clone()).await; - - cx.update(|_| { - drop(buffer); - }); - cx.run_until_parked(); - headless.update(server_cx, |headless, cx| { - assert!(!headless.buffer_store.read(cx).has_shared_buffers()) - }); - - do_search(&project, cx.clone()).await; -} - -#[gpui::test] -async fn test_remote_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - "/code", - json!({ - "project1": { - ".git": {}, - "README.md": "# project 1", - "src": { - "lib.rs": "fn one() -> usize { 1 }" - } - }, - }), - ) - .await; - - let (project, headless) = init_test(&fs, cx, server_cx).await; - - cx.update_global(|settings_store: &mut SettingsStore, cx| { - settings_store.set_user_settings( - r#"{"languages":{"Rust":{"language_servers":["from-local-settings"]}}}"#, - cx, - ) - }) - .unwrap(); - - cx.run_until_parked(); - - server_cx.read(|cx| { - assert_eq!( - AllLanguageSettings::get_global(cx) - .language(None, Some(&"Rust".into()), cx) - .language_servers, - ["from-local-settings"], - "User language settings should be synchronized with the server settings" - ) - }); - - server_cx - .update_global(|settings_store: &mut SettingsStore, cx| { - settings_store.set_server_settings( - r#"{"languages":{"Rust":{"language_servers":["from-server-settings"]}}}"#, - cx, - ) - }) - .unwrap(); - - cx.run_until_parked(); - - server_cx.read(|cx| { - assert_eq!( - AllLanguageSettings::get_global(cx) - .language(None, Some(&"Rust".into()), cx) - .language_servers, - ["from-server-settings".to_string()], - "Server language settings should take precedence over the user settings" - ) - }); - - fs.insert_tree( - "/code/project1/.zed", - json!({ - "settings.json": r#" - { - "languages": {"Rust":{"language_servers":["override-rust-analyzer"]}}, - "lsp": { - "override-rust-analyzer": { - "binary": { - "path": "~/.cargo/bin/rust-analyzer" - } - } - } - }"# - }), - ) - .await; - - let worktree_id = project - .update(cx, |project, cx| { - project.find_or_create_worktree("/code/project1", true, cx) - }) - .await - .unwrap() - .0 - .read_with(cx, |worktree, _| worktree.id()); - - let buffer = project - .update(cx, |project, cx| { - project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx) - }) - .await - .unwrap(); - cx.run_until_parked(); - - server_cx.read(|cx| { - let worktree_id = headless - .read(cx) - .worktree_store - .read(cx) - .worktrees() - .next() - .unwrap() - .read(cx) - .id(); - assert_eq!( - AllLanguageSettings::get( - Some(SettingsLocation { - worktree_id, - path: rel_path("src/lib.rs") - }), - cx - ) - .language(None, Some(&"Rust".into()), cx) - .language_servers, - ["override-rust-analyzer".to_string()] - ) - }); - - cx.read(|cx| { - let file = buffer.read(cx).file(); - assert_eq!( - language_settings(Some("Rust".into()), file, cx).language_servers, - ["override-rust-analyzer".to_string()] - ) - }); -} - -#[gpui::test] -async fn test_remote_lsp(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - path!("/code"), - json!({ - "project1": { - ".git": {}, - "README.md": "# project 1", - "src": { - "lib.rs": "fn one() -> usize { 1 }" - } - }, - }), - ) - .await; - - let (project, headless) = init_test(&fs, cx, server_cx).await; - - fs.insert_tree( - path!("/code/project1/.zed"), - json!({ - "settings.json": r#" - { - "languages": {"Rust":{"language_servers":["rust-analyzer", "fake-analyzer"]}}, - "lsp": { - "rust-analyzer": { - "binary": { - "path": "~/.cargo/bin/rust-analyzer" - } - }, - "fake-analyzer": { - "binary": { - "path": "~/.cargo/bin/rust-analyzer" - } - } - } - }"# - }), - ) - .await; - - cx.update_entity(&project, |project, _| { - project.languages().register_test_language(LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".into()], - ..Default::default() - }, - ..Default::default() - }); - project.languages().register_fake_lsp_adapter( - "Rust", - FakeLspAdapter { - name: "rust-analyzer", - capabilities: lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions::default()), - rename_provider: Some(lsp::OneOf::Left(true)), - ..lsp::ServerCapabilities::default() - }, - ..FakeLspAdapter::default() - }, - ); - project.languages().register_fake_lsp_adapter( - "Rust", - FakeLspAdapter { - name: "fake-analyzer", - capabilities: lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions::default()), - rename_provider: Some(lsp::OneOf::Left(true)), - ..lsp::ServerCapabilities::default() - }, - ..FakeLspAdapter::default() - }, - ) - }); - - let mut fake_lsp = server_cx.update(|cx| { - headless.read(cx).languages.register_fake_lsp_server( - LanguageServerName("rust-analyzer".into()), - lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions::default()), - rename_provider: Some(lsp::OneOf::Left(true)), - ..lsp::ServerCapabilities::default() - }, - None, - ) - }); - - let mut fake_second_lsp = server_cx.update(|cx| { - headless.read(cx).languages.register_fake_lsp_adapter( - "Rust", - FakeLspAdapter { - name: "fake-analyzer", - capabilities: lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions::default()), - rename_provider: Some(lsp::OneOf::Left(true)), - ..lsp::ServerCapabilities::default() - }, - ..FakeLspAdapter::default() - }, - ); - headless.read(cx).languages.register_fake_lsp_server( - LanguageServerName("fake-analyzer".into()), - lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions::default()), - rename_provider: Some(lsp::OneOf::Left(true)), - ..lsp::ServerCapabilities::default() - }, - None, - ) - }); - - cx.run_until_parked(); - - let worktree_id = project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/code/project1"), true, cx) - }) - .await - .unwrap() - .0 - .read_with(cx, |worktree, _| worktree.id()); - - // Wait for the settings to synchronize - cx.run_until_parked(); - - let (buffer, _handle) = project - .update(cx, |project, cx| { - project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx) - }) - .await - .unwrap(); - cx.run_until_parked(); - - let fake_lsp = fake_lsp.next().await.unwrap(); - let fake_second_lsp = fake_second_lsp.next().await.unwrap(); - - cx.read(|cx| { - let file = buffer.read(cx).file(); - assert_eq!( - language_settings(Some("Rust".into()), file, cx).language_servers, - ["rust-analyzer".to_string(), "fake-analyzer".to_string()] - ) - }); - - let buffer_id = cx.read(|cx| { - let buffer = buffer.read(cx); - assert_eq!(buffer.language().unwrap().name(), "Rust".into()); - buffer.remote_id() - }); - - server_cx.read(|cx| { - let buffer = headless - .read(cx) - .buffer_store - .read(cx) - .get(buffer_id) - .unwrap(); - - assert_eq!(buffer.read(cx).language().unwrap().name(), "Rust".into()); - }); - - server_cx.read(|cx| { - let lsp_store = headless.read(cx).lsp_store.read(cx); - assert_eq!(lsp_store.as_local().unwrap().language_servers.len(), 2); - }); - - fake_lsp.set_request_handler::(|_, _| async move { - Ok(Some(CompletionResponse::Array(vec![lsp::CompletionItem { - label: "boop".to_string(), - ..Default::default() - }]))) - }); - - fake_second_lsp.set_request_handler::(|_, _| async move { - Ok(Some(CompletionResponse::Array(vec![lsp::CompletionItem { - label: "beep".to_string(), - ..Default::default() - }]))) - }); - - let result = project - .update(cx, |project, cx| { - project.completions( - &buffer, - 0, - CompletionContext { - trigger_kind: CompletionTriggerKind::INVOKED, - trigger_character: None, - }, - cx, - ) - }) - .await - .unwrap(); - - assert_eq!( - result - .into_iter() - .flat_map(|response| response.completions) - .map(|c| c.label.text) - .collect::>(), - vec!["boop".to_string(), "beep".to_string()] - ); - - fake_lsp.set_request_handler::(|_, _| async move { - Ok(Some(lsp::WorkspaceEdit { - changes: Some( - [( - lsp::Uri::from_file_path(path!("/code/project1/src/lib.rs")).unwrap(), - vec![lsp::TextEdit::new( - lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 6)), - "two".to_string(), - )], - )] - .into_iter() - .collect(), - ), - ..Default::default() - })) - }); - - project - .update(cx, |project, cx| { - project.perform_rename(buffer.clone(), 3, "two".to_string(), cx) - }) - .await - .unwrap(); - - cx.run_until_parked(); - buffer.update(cx, |buffer, _| { - assert_eq!(buffer.text(), "fn two() -> usize { 1 }") - }) -} - -#[gpui::test] -async fn test_remote_cancel_language_server_work( - cx: &mut TestAppContext, - server_cx: &mut TestAppContext, -) { - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - path!("/code"), - json!({ - "project1": { - ".git": {}, - "README.md": "# project 1", - "src": { - "lib.rs": "fn one() -> usize { 1 }" - } - }, - }), - ) - .await; - - let (project, headless) = init_test(&fs, cx, server_cx).await; - - fs.insert_tree( - path!("/code/project1/.zed"), - json!({ - "settings.json": r#" - { - "languages": {"Rust":{"language_servers":["rust-analyzer"]}}, - "lsp": { - "rust-analyzer": { - "binary": { - "path": "~/.cargo/bin/rust-analyzer" - } - } - } - }"# - }), - ) - .await; - - cx.update_entity(&project, |project, _| { - project.languages().register_test_language(LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".into()], - ..Default::default() - }, - ..Default::default() - }); - project.languages().register_fake_lsp_adapter( - "Rust", - FakeLspAdapter { - name: "rust-analyzer", - ..Default::default() - }, - ) - }); - - let mut fake_lsp = server_cx.update(|cx| { - headless.read(cx).languages.register_fake_lsp_server( - LanguageServerName("rust-analyzer".into()), - Default::default(), - None, - ) - }); - - cx.run_until_parked(); - - let worktree_id = project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/code/project1"), true, cx) - }) - .await - .unwrap() - .0 - .read_with(cx, |worktree, _| worktree.id()); - - cx.run_until_parked(); - - let (buffer, _handle) = project - .update(cx, |project, cx| { - project.open_buffer_with_lsp((worktree_id, rel_path("src/lib.rs")), cx) - }) - .await - .unwrap(); - - cx.run_until_parked(); - - let mut fake_lsp = fake_lsp.next().await.unwrap(); - - // Cancelling all language server work for a given buffer - { - // Two operations, one cancellable and one not. - fake_lsp - .start_progress_with( - "another-token", - lsp::WorkDoneProgressBegin { - cancellable: Some(false), - ..Default::default() - }, - ) - .await; - - let progress_token = "the-progress-token"; - fake_lsp - .start_progress_with( - progress_token, - lsp::WorkDoneProgressBegin { - cancellable: Some(true), - ..Default::default() - }, - ) - .await; - - cx.executor().run_until_parked(); - - project.update(cx, |project, cx| { - project.cancel_language_server_work_for_buffers([buffer.clone()], cx) - }); - - cx.executor().run_until_parked(); - - // Verify the cancellation was received on the server side - let cancel_notification = fake_lsp - .receive_notification::() - .await; - assert_eq!( - cancel_notification.token, - lsp::NumberOrString::String(progress_token.into()) - ); - } - - // Cancelling work by server_id and token - { - let server_id = fake_lsp.server.server_id(); - let progress_token = "the-progress-token"; - - fake_lsp - .start_progress_with( - progress_token, - lsp::WorkDoneProgressBegin { - cancellable: Some(true), - ..Default::default() - }, - ) - .await; - - cx.executor().run_until_parked(); - - project.update(cx, |project, cx| { - project.cancel_language_server_work( - server_id, - Some(ProgressToken::String(SharedString::from(progress_token))), - cx, - ) - }); - - cx.executor().run_until_parked(); - - // Verify the cancellation was received on the server side - let cancel_notification = fake_lsp - .receive_notification::() - .await; - assert_eq!( - cancel_notification.token, - lsp::NumberOrString::String(progress_token.to_owned()) - ); - } -} - -#[gpui::test] -async fn test_remote_reload(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - path!("/code"), - json!({ - "project1": { - ".git": {}, - "README.md": "# project 1", - "src": { - "lib.rs": "fn one() -> usize { 1 }" - } - }, - }), - ) - .await; - - let (project, _headless) = init_test(&fs, cx, server_cx).await; - let (worktree, _) = project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/code/project1"), true, cx) - }) - .await - .unwrap(); - - let worktree_id = cx.update(|cx| worktree.read(cx).id()); - - let buffer = project - .update(cx, |project, cx| { - project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx) - }) - .await - .unwrap(); - - fs.save( - &PathBuf::from(path!("/code/project1/src/lib.rs")), - &("bangles".to_string().into()), - LineEnding::Unix, - ) - .await - .unwrap(); - - cx.run_until_parked(); - - buffer.update(cx, |buffer, cx| { - assert_eq!(buffer.text(), "bangles"); - buffer.edit([(0..0, "a")], None, cx); - }); - - fs.save( - &PathBuf::from(path!("/code/project1/src/lib.rs")), - &("bloop".to_string().into()), - LineEnding::Unix, - ) - .await - .unwrap(); - - cx.run_until_parked(); - cx.update(|cx| { - assert!(buffer.read(cx).has_conflict()); - }); - - project - .update(cx, |project, cx| { - project.reload_buffers([buffer.clone()].into_iter().collect(), false, cx) - }) - .await - .unwrap(); - cx.run_until_parked(); - - cx.update(|cx| { - assert!(!buffer.read(cx).has_conflict()); - }); -} - -#[gpui::test] -async fn test_remote_resolve_path_in_buffer( - cx: &mut TestAppContext, - server_cx: &mut TestAppContext, -) { - let fs = FakeFs::new(server_cx.executor()); - // Even though we are not testing anything from project1, it is necessary to test if project2 is picking up correct worktree - fs.insert_tree( - path!("/code"), - json!({ - "project1": { - ".git": {}, - "README.md": "# project 1", - "src": { - "lib.rs": "fn one() -> usize { 1 }" - } - }, - "project2": { - ".git": {}, - "README.md": "# project 2", - "src": { - "lib.rs": "fn two() -> usize { 2 }" - } - } - }), - ) - .await; - - let (project, _headless) = init_test(&fs, cx, server_cx).await; - - let _ = project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/code/project1"), true, cx) - }) - .await - .unwrap(); - - let (worktree2, _) = project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/code/project2"), true, cx) - }) - .await - .unwrap(); - - let worktree2_id = cx.update(|cx| worktree2.read(cx).id()); - - cx.run_until_parked(); - - let buffer2 = project - .update(cx, |project, cx| { - project.open_buffer((worktree2_id, rel_path("src/lib.rs")), cx) - }) - .await - .unwrap(); - - let path = project - .update(cx, |project, cx| { - project.resolve_path_in_buffer(path!("/code/project2/README.md"), &buffer2, cx) - }) - .await - .unwrap(); - assert!(path.is_file()); - assert_eq!(path.abs_path().unwrap(), path!("/code/project2/README.md")); - - let path = project - .update(cx, |project, cx| { - project.resolve_path_in_buffer("../README.md", &buffer2, cx) - }) - .await - .unwrap(); - assert!(path.is_file()); - assert_eq!( - path.project_path().unwrap().clone(), - (worktree2_id, rel_path("README.md")).into() - ); - - let path = project - .update(cx, |project, cx| { - project.resolve_path_in_buffer("../src", &buffer2, cx) - }) - .await - .unwrap(); - assert_eq!( - path.project_path().unwrap().clone(), - (worktree2_id, rel_path("src")).into() - ); - assert!(path.is_dir()); -} - -#[gpui::test] -async fn test_remote_resolve_abs_path(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - path!("/code"), - json!({ - "project1": { - ".git": {}, - "README.md": "# project 1", - "src": { - "lib.rs": "fn one() -> usize { 1 }" - } - }, - }), - ) - .await; - - let (project, _headless) = init_test(&fs, cx, server_cx).await; - - let path = project - .update(cx, |project, cx| { - project.resolve_abs_path(path!("/code/project1/README.md"), cx) - }) - .await - .unwrap(); - - assert!(path.is_file()); - assert_eq!(path.abs_path().unwrap(), path!("/code/project1/README.md")); - - let path = project - .update(cx, |project, cx| { - project.resolve_abs_path(path!("/code/project1/src"), cx) - }) - .await - .unwrap(); - - assert!(path.is_dir()); - assert_eq!(path.abs_path().unwrap(), path!("/code/project1/src")); - - let path = project - .update(cx, |project, cx| { - project.resolve_abs_path(path!("/code/project1/DOESNOTEXIST"), cx) - }) - .await; - assert!(path.is_none()); -} - -#[gpui::test(iterations = 10)] -async fn test_canceling_buffer_opening(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - "/code", - json!({ - "project1": { - ".git": {}, - "README.md": "# project 1", - "src": { - "lib.rs": "fn one() -> usize { 1 }" - } - }, - }), - ) - .await; - - let (project, _headless) = init_test(&fs, cx, server_cx).await; - let (worktree, _) = project - .update(cx, |project, cx| { - project.find_or_create_worktree("/code/project1", true, cx) - }) - .await - .unwrap(); - let worktree_id = worktree.read_with(cx, |tree, _| tree.id()); - - // Open a buffer on the client but cancel after a random amount of time. - let buffer = project.update(cx, |p, cx| { - p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx) - }); - cx.executor().simulate_random_delay().await; - drop(buffer); - - // Try opening the same buffer again as the client, and ensure we can - // still do it despite the cancellation above. - let buffer = project - .update(cx, |p, cx| { - p.open_buffer((worktree_id, rel_path("src/lib.rs")), cx) - }) - .await - .unwrap(); - - buffer.read_with(cx, |buf, _| { - assert_eq!(buf.text(), "fn one() -> usize { 1 }") - }); -} - -#[gpui::test] -async fn test_adding_then_removing_then_adding_worktrees( - cx: &mut TestAppContext, - server_cx: &mut TestAppContext, -) { - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - path!("/code"), - json!({ - "project1": { - ".git": {}, - "README.md": "# project 1", - "src": { - "lib.rs": "fn one() -> usize { 1 }" - } - }, - "project2": { - "README.md": "# project 2", - }, - }), - ) - .await; - - let (project, _headless) = init_test(&fs, cx, server_cx).await; - let (_worktree, _) = project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/code/project1"), true, cx) - }) - .await - .unwrap(); - - let (worktree_2, _) = project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/code/project2"), true, cx) - }) - .await - .unwrap(); - let worktree_id_2 = worktree_2.read_with(cx, |tree, _| tree.id()); - - project.update(cx, |project, cx| project.remove_worktree(worktree_id_2, cx)); - - let (worktree_2, _) = project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/code/project2"), true, cx) - }) - .await - .unwrap(); - - cx.run_until_parked(); - worktree_2.update(cx, |worktree, _cx| { - assert!(worktree.is_visible()); - let entries = worktree.entries(true, 0).collect::>(); - assert_eq!(entries.len(), 2); - assert_eq!(entries[1].path.as_unix_str(), "README.md") - }) -} - -#[gpui::test] -async fn test_open_server_settings(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - path!("/code"), - json!({ - "project1": { - ".git": {}, - "README.md": "# project 1", - "src": { - "lib.rs": "fn one() -> usize { 1 }" - } - }, - }), - ) - .await; - - let (project, _headless) = init_test(&fs, cx, server_cx).await; - let buffer = project.update(cx, |project, cx| project.open_server_settings(cx)); - cx.executor().run_until_parked(); - - let buffer = buffer.await.unwrap(); - - cx.update(|cx| { - assert_eq!( - buffer.read(cx).text(), - initial_server_settings_content() - .to_string() - .replace("\r\n", "\n") - ) - }) -} - -#[gpui::test(iterations = 20)] -async fn test_reconnect(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - path!("/code"), - json!({ - "project1": { - ".git": {}, - "README.md": "# project 1", - "src": { - "lib.rs": "fn one() -> usize { 1 }" - } - }, - }), - ) - .await; - - let (project, _headless) = init_test(&fs, cx, server_cx).await; - - let (worktree, _) = project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/code/project1"), true, cx) - }) - .await - .unwrap(); - - let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id()); - let buffer = project - .update(cx, |project, cx| { - project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx) - }) - .await - .unwrap(); - - buffer.update(cx, |buffer, cx| { - assert_eq!(buffer.text(), "fn one() -> usize { 1 }"); - let ix = buffer.text().find('1').unwrap(); - buffer.edit([(ix..ix + 1, "100")], None, cx); - }); - - let client = cx.read(|cx| project.read(cx).remote_client().unwrap()); - client - .update(cx, |client, cx| client.simulate_disconnect(cx)) - .detach(); - - project - .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx)) - .await - .unwrap(); - - assert_eq!( - fs.load(path!("/code/project1/src/lib.rs").as_ref()) - .await - .unwrap(), - "fn one() -> usize { 100 }" - ); -} - -#[gpui::test] -async fn test_remote_root_rename(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - "/code", - json!({ - "project1": { - ".git": {}, - "README.md": "# project 1", - }, - }), - ) - .await; - - let (project, _) = init_test(&fs, cx, server_cx).await; - - let (worktree, _) = project - .update(cx, |project, cx| { - project.find_or_create_worktree("/code/project1", true, cx) - }) - .await - .unwrap(); - - cx.run_until_parked(); - - fs.rename( - &PathBuf::from("/code/project1"), - &PathBuf::from("/code/project2"), - Default::default(), - ) - .await - .unwrap(); - - cx.run_until_parked(); - worktree.update(cx, |worktree, _| { - assert_eq!(worktree.root_name(), "project2") - }) -} - -#[gpui::test] -async fn test_remote_rename_entry(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - "/code", - json!({ - "project1": { - ".git": {}, - "README.md": "# project 1", - }, - }), - ) - .await; - - let (project, _) = init_test(&fs, cx, server_cx).await; - let (worktree, _) = project - .update(cx, |project, cx| { - project.find_or_create_worktree("/code/project1", true, cx) - }) - .await - .unwrap(); - - cx.run_until_parked(); - - let entry = project - .update(cx, |project, cx| { - let worktree = worktree.read(cx); - let entry = worktree.entry_for_path(rel_path("README.md")).unwrap(); - project.rename_entry(entry.id, (worktree.id(), rel_path("README.rst")).into(), cx) - }) - .await - .unwrap() - .into_included() - .unwrap(); - - cx.run_until_parked(); - - worktree.update(cx, |worktree, _| { - assert_eq!( - worktree.entry_for_path(rel_path("README.rst")).unwrap().id, - entry.id - ) - }); -} - -#[gpui::test] -async fn test_copy_file_into_remote_project( - cx: &mut TestAppContext, - server_cx: &mut TestAppContext, -) { - let remote_fs = FakeFs::new(server_cx.executor()); - remote_fs - .insert_tree( - path!("/code"), - json!({ - "project1": { - ".git": {}, - "README.md": "# project 1", - "src": { - "main.rs": "" - } - }, - }), - ) - .await; - - let (project, _) = init_test(&remote_fs, cx, server_cx).await; - let (worktree, _) = project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/code/project1"), true, cx) - }) - .await - .unwrap(); - - cx.run_until_parked(); - - let local_fs = project - .read_with(cx, |project, _| project.fs().clone()) - .as_fake(); - local_fs - .insert_tree( - path!("/local-code"), - json!({ - "dir1": { - "file1": "file 1 content", - "dir2": { - "file2": "file 2 content", - "dir3": { - "file3": "" - }, - "dir4": {} - }, - "dir5": {} - }, - "file4": "file 4 content" - }), - ) - .await; - - worktree - .update(cx, |worktree, cx| { - worktree.copy_external_entries( - rel_path("src").into(), - vec![ - Path::new(path!("/local-code/dir1/file1")).into(), - Path::new(path!("/local-code/dir1/dir2")).into(), - ], - local_fs.clone(), - cx, - ) - }) - .await - .unwrap(); - - assert_eq!( - remote_fs.paths(true), - vec![ - PathBuf::from(path!("/")), - PathBuf::from(path!("/code")), - PathBuf::from(path!("/code/project1")), - PathBuf::from(path!("/code/project1/.git")), - PathBuf::from(path!("/code/project1/README.md")), - PathBuf::from(path!("/code/project1/src")), - PathBuf::from(path!("/code/project1/src/dir2")), - PathBuf::from(path!("/code/project1/src/file1")), - PathBuf::from(path!("/code/project1/src/main.rs")), - PathBuf::from(path!("/code/project1/src/dir2/dir3")), - PathBuf::from(path!("/code/project1/src/dir2/dir4")), - PathBuf::from(path!("/code/project1/src/dir2/file2")), - PathBuf::from(path!("/code/project1/src/dir2/dir3/file3")), - ] - ); - assert_eq!( - remote_fs - .load(path!("/code/project1/src/file1").as_ref()) - .await - .unwrap(), - "file 1 content" - ); - assert_eq!( - remote_fs - .load(path!("/code/project1/src/dir2/file2").as_ref()) - .await - .unwrap(), - "file 2 content" - ); - assert_eq!( - remote_fs - .load(path!("/code/project1/src/dir2/dir3/file3").as_ref()) - .await - .unwrap(), - "" - ); -} - -#[gpui::test] -async fn test_remote_git_diffs(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { - let text_2 = " - fn one() -> usize { - 1 - } - " - .unindent(); - let text_1 = " - fn one() -> usize { - 0 - } - " - .unindent(); - - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - "/code", - json!({ - "project1": { - ".git": {}, - "src": { - "lib.rs": text_2 - }, - "README.md": "# project 1", - }, - }), - ) - .await; - fs.set_index_for_repo( - Path::new("/code/project1/.git"), - &[("src/lib.rs", text_1.clone())], - ); - fs.set_head_for_repo( - Path::new("/code/project1/.git"), - &[("src/lib.rs", text_1.clone())], - "deadbeef", - ); - - let (project, _headless) = init_test(&fs, cx, server_cx).await; - let (worktree, _) = project - .update(cx, |project, cx| { - project.find_or_create_worktree("/code/project1", true, cx) - }) - .await - .unwrap(); - let worktree_id = cx.update(|cx| worktree.read(cx).id()); - cx.executor().run_until_parked(); - - let buffer = project - .update(cx, |project, cx| { - project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx) - }) - .await - .unwrap(); - let diff = project - .update(cx, |project, cx| { - project.open_uncommitted_diff(buffer.clone(), cx) - }) - .await - .unwrap(); - - diff.read_with(cx, |diff, cx| { - assert_eq!(diff.base_text_string().unwrap(), text_1); - assert_eq!( - diff.secondary_diff() - .unwrap() - .read(cx) - .base_text_string() - .unwrap(), - text_1 - ); - }); - - // stage the current buffer's contents - fs.set_index_for_repo( - Path::new("/code/project1/.git"), - &[("src/lib.rs", text_2.clone())], - ); - - cx.executor().run_until_parked(); - diff.read_with(cx, |diff, cx| { - assert_eq!(diff.base_text_string().unwrap(), text_1); - assert_eq!( - diff.secondary_diff() - .unwrap() - .read(cx) - .base_text_string() - .unwrap(), - text_2 - ); - }); - - // commit the current buffer's contents - fs.set_head_for_repo( - Path::new("/code/project1/.git"), - &[("src/lib.rs", text_2.clone())], - "deadbeef", - ); - - cx.executor().run_until_parked(); - diff.read_with(cx, |diff, cx| { - assert_eq!(diff.base_text_string().unwrap(), text_2); - assert_eq!( - diff.secondary_diff() - .unwrap() - .read(cx) - .base_text_string() - .unwrap(), - text_2 - ); - }); -} - -#[gpui::test] -async fn test_remote_git_diffs_when_recv_update_repository_delay( - cx: &mut TestAppContext, - server_cx: &mut TestAppContext, -) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - theme::init(theme::LoadThemes::JustBase, cx); - release_channel::init(semver::Version::new(0, 0, 0), cx); - editor::init(cx); - }); - - use editor::Editor; - use gpui::VisualContext; - let text_2 = " - fn one() -> usize { - 1 - } - " - .unindent(); - let text_1 = " - fn one() -> usize { - 0 - } - " - .unindent(); - - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - path!("/code"), - json!({ - "project1": { - "src": { - "lib.rs": text_2 - }, - "README.md": "# project 1", - }, - }), - ) - .await; - - let (project, _headless) = init_test(&fs, cx, server_cx).await; - let (worktree, _) = project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/code/project1"), true, cx) - }) - .await - .unwrap(); - let worktree_id = cx.update(|cx| worktree.read(cx).id()); - let buffer = project - .update(cx, |project, cx| { - project.open_buffer((worktree_id, rel_path("src/lib.rs")), cx) - }) - .await - .unwrap(); - let buffer_id = cx.update(|cx| buffer.read(cx).remote_id()); - - let cx = cx.add_empty_window(); - let editor = cx.new_window_entity(|window, cx| { - Editor::for_buffer(buffer, Some(project.clone()), window, cx) - }); - - // Remote server will send proto::UpdateRepository after the instance of Editor create. - fs.insert_tree( - path!("/code"), - json!({ - "project1": { - ".git": {}, - }, - }), - ) - .await; - - fs.set_index_for_repo( - Path::new(path!("/code/project1/.git")), - &[("src/lib.rs", text_1.clone())], - ); - fs.set_head_for_repo( - Path::new(path!("/code/project1/.git")), - &[("src/lib.rs", text_1.clone())], - "sha", - ); - - cx.executor().run_until_parked(); - let diff = editor - .read_with(cx, |editor, cx| { - editor - .buffer() - .read_with(cx, |buffer, _| buffer.diff_for(buffer_id)) - }) - .unwrap(); - - diff.read_with(cx, |diff, cx| { - assert_eq!(diff.base_text_string().unwrap(), text_1); - assert_eq!( - diff.secondary_diff() - .unwrap() - .read(cx) - .base_text_string() - .unwrap(), - text_1 - ); - }); - - // stage the current buffer's contents - fs.set_index_for_repo( - Path::new(path!("/code/project1/.git")), - &[("src/lib.rs", text_2.clone())], - ); - - cx.executor().run_until_parked(); - diff.read_with(cx, |diff, cx| { - assert_eq!(diff.base_text_string().unwrap(), text_1); - assert_eq!( - diff.secondary_diff() - .unwrap() - .read(cx) - .base_text_string() - .unwrap(), - text_2 - ); - }); - - // commit the current buffer's contents - fs.set_head_for_repo( - Path::new(path!("/code/project1/.git")), - &[("src/lib.rs", text_2.clone())], - "sha", - ); - - cx.executor().run_until_parked(); - diff.read_with(cx, |diff, cx| { - assert_eq!(diff.base_text_string().unwrap(), text_2); - assert_eq!( - diff.secondary_diff() - .unwrap() - .read(cx) - .base_text_string() - .unwrap(), - text_2 - ); - }); -} - -#[gpui::test] -async fn test_remote_git_branches(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - path!("/code"), - json!({ - "project1": { - ".git": {}, - "README.md": "# project 1", - }, - }), - ) - .await; - - let (project, headless_project) = init_test(&fs, cx, server_cx).await; - let branches = ["main", "dev", "feature-1"]; - let branches_set = branches - .iter() - .map(ToString::to_string) - .collect::>(); - fs.insert_branches(Path::new(path!("/code/project1/.git")), &branches); - - let (_worktree, _) = project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/code/project1"), true, cx) - }) - .await - .unwrap(); - // Give the worktree a bit of time to index the file system - cx.run_until_parked(); - - let repository = project.update(cx, |project, cx| project.active_repository(cx).unwrap()); - - let remote_branches = repository - .update(cx, |repository, _| repository.branches()) - .await - .unwrap() - .unwrap(); - - let new_branch = branches[2]; - - let remote_branches = remote_branches - .into_iter() - .map(|branch| branch.name().to_string()) - .collect::>(); - - assert_eq!(&remote_branches, &branches_set); - - cx.update(|cx| { - repository.update(cx, |repository, _cx| { - repository.change_branch(new_branch.to_string()) - }) - }) - .await - .unwrap() - .unwrap(); - - cx.run_until_parked(); - - let server_branch = server_cx.update(|cx| { - headless_project.update(cx, |headless_project, cx| { - headless_project.git_store.update(cx, |git_store, cx| { - git_store - .repositories() - .values() - .next() - .unwrap() - .read(cx) - .branch - .as_ref() - .unwrap() - .clone() - }) - }) - }); - - assert_eq!(server_branch.name(), branches[2]); - - // Also try creating a new branch - cx.update(|cx| { - repository.update(cx, |repo, _cx| { - repo.create_branch("totally-new-branch".to_string(), None) - }) - }) - .await - .unwrap() - .unwrap(); - - cx.update(|cx| { - repository.update(cx, |repo, _cx| { - repo.change_branch("totally-new-branch".to_string()) - }) - }) - .await - .unwrap() - .unwrap(); - - cx.run_until_parked(); - - let server_branch = server_cx.update(|cx| { - headless_project.update(cx, |headless_project, cx| { - headless_project.git_store.update(cx, |git_store, cx| { - git_store - .repositories() - .values() - .next() - .unwrap() - .read(cx) - .branch - .as_ref() - .unwrap() - .clone() - }) - }) - }); - - assert_eq!(server_branch.name(), "totally-new-branch"); -} - -#[gpui::test] -async fn test_remote_agent_fs_tool_calls(cx: &mut TestAppContext, server_cx: &mut TestAppContext) { - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree( - path!("/project"), - json!({ - "a.txt": "A", - "b.txt": "B", - }), - ) - .await; - - let (project, _headless_project) = init_test(&fs, cx, server_cx).await; - project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/project"), true, cx) - }) - .await - .unwrap(); - - let action_log = cx.new(|_| action_log::ActionLog::new(project.clone())); - - // Create a minimal thread for the ReadFileTool - let context_server_registry = - cx.new(|cx| agent::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, - Templates::new(), - Some(model), - cx, - ) - }); - - let input = ReadFileToolInput { - path: "project/b.txt".into(), - start_line: None, - end_line: None, - }; - let read_tool = Arc::new(ReadFileTool::new(thread.downgrade(), project, action_log)); - let (event_stream, _) = ToolCallEventStream::test(); - - let exists_result = cx.update(|cx| read_tool.clone().run(input, event_stream.clone(), cx)); - let output = exists_result.await.unwrap(); - assert_eq!(output, LanguageModelToolResultContent::Text("B".into())); - - let input = ReadFileToolInput { - path: "project/c.txt".into(), - start_line: None, - end_line: None, - }; - let does_not_exist_result = cx.update(|cx| read_tool.run(input, event_stream, cx)); - does_not_exist_result.await.unwrap_err(); -} - -#[gpui::test] -async fn test_remote_external_agent_server( - cx: &mut TestAppContext, - server_cx: &mut TestAppContext, -) { - let fs = FakeFs::new(server_cx.executor()); - fs.insert_tree(path!("/project"), json!({})).await; - - let (project, _headless_project) = init_test(&fs, cx, server_cx).await; - project - .update(cx, |project, cx| { - project.find_or_create_worktree(path!("/project"), true, cx) - }) - .await - .unwrap(); - let names = project.update(cx, |project, cx| { - project - .agent_server_store() - .read(cx) - .external_agents() - .map(|name| name.to_string()) - .collect::>() - }); - pretty_assertions::assert_eq!(names, ["codex", "gemini", "claude"]); - server_cx.update_global::(|settings_store, cx| { - settings_store - .set_server_settings( - &json!({ - "agent_servers": { - "foo": { - "type": "custom", - "command": "foo-cli", - "args": ["--flag"], - "env": { - "VAR": "val" - } - } - } - }) - .to_string(), - cx, - ) - .unwrap(); - }); - server_cx.run_until_parked(); - cx.run_until_parked(); - let names = project.update(cx, |project, cx| { - project - .agent_server_store() - .read(cx) - .external_agents() - .map(|name| name.to_string()) - .collect::>() - }); - pretty_assertions::assert_eq!(names, ["gemini", "codex", "claude", "foo"]); - let (command, root, login) = project - .update(cx, |project, cx| { - project.agent_server_store().update(cx, |store, cx| { - store - .get_external_agent(&"foo".into()) - .unwrap() - .get_command( - None, - HashMap::from_iter([("OTHER_VAR".into(), "other-val".into())]), - None, - None, - &mut cx.to_async(), - ) - }) - }) - .await - .unwrap(); - assert_eq!( - command, - AgentServerCommand { - path: "ssh".into(), - args: vec!["foo-cli".into(), "--flag".into()], - env: Some(HashMap::from_iter([ - ("VAR".into(), "val".into()), - ("OTHER_VAR".into(), "other-val".into()) - ])) - } - ); - assert_eq!(&PathBuf::from(root), paths::home_dir()); - assert!(login.is_none()); -} - -pub async fn init_test( - server_fs: &Arc, - cx: &mut TestAppContext, - server_cx: &mut TestAppContext, -) -> (Entity, Entity) { - let server_fs = server_fs.clone(); - cx.update(|cx| { - release_channel::init(semver::Version::new(0, 0, 0), cx); - }); - server_cx.update(|cx| { - release_channel::init(semver::Version::new(0, 0, 0), cx); - }); - init_logger(); - - let (opts, ssh_server_client) = RemoteClient::fake_server(cx, server_cx); - let http_client = Arc::new(BlockedHttpClient); - let node_runtime = NodeRuntime::unavailable(); - let languages = Arc::new(LanguageRegistry::new(cx.executor())); - let proxy = Arc::new(ExtensionHostProxy::new()); - server_cx.update(HeadlessProject::init); - let headless = server_cx.new(|cx| { - HeadlessProject::new( - crate::HeadlessAppState { - session: ssh_server_client, - fs: server_fs.clone(), - http_client, - node_runtime, - languages, - extension_host_proxy: proxy, - }, - cx, - ) - }); - - let ssh = RemoteClient::fake_client(opts, cx).await; - let project = build_project(ssh, cx); - project - .update(cx, { - let headless = headless.clone(); - |_, cx| cx.on_release(|_, _| drop(headless)) - }) - .detach(); - (project, headless) -} - -fn init_logger() { - zlog::init_test(); -} - -fn build_project(ssh: Entity, cx: &mut TestAppContext) -> Entity { - cx.update(|cx| { - if !cx.has_global::() { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - } - }); - - let client = cx.update(|cx| { - Client::new( - Arc::new(FakeSystemClock::new()), - FakeHttpClient::with_404_response(), - cx, - ) - }); - - let node = NodeRuntime::unavailable(); - let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); - let languages = Arc::new(LanguageRegistry::test(cx.executor())); - let fs = FakeFs::new(cx.executor()); - - cx.update(|cx| { - Project::init(&client, cx); - }); - - cx.update(|cx| Project::remote(ssh, client, node, user_store, languages, fs, cx)) -} diff --git a/crates/remote_server/src/remote_server.rs b/crates/remote_server/src/remote_server.rs deleted file mode 100644 index 98e8744b11..0000000000 --- a/crates/remote_server/src/remote_server.rs +++ /dev/null @@ -1,87 +0,0 @@ -mod headless_project; - -#[cfg(not(windows))] -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 => { - let commit_sha = - option_env!("ZED_COMMIT_SHA").unwrap_or(release_channel.dev_name()); - let build_id = option_env!("ZED_BUILD_ID"); - if let Some(build_id) = build_id { - println!("{}+{}", build_id, commit_sha) - } else { - println!("{commit_sha}"); - } - } - }; - Ok(()) - } - } -} diff --git a/crates/remote_server/src/unix.rs b/crates/remote_server/src/unix.rs deleted file mode 100644 index 8adeaa5947..0000000000 --- a/crates/remote_server/src/unix.rs +++ /dev/null @@ -1,1045 +0,0 @@ -use crate::HeadlessProject; -use crate::headless_project::HeadlessAppState; -use anyhow::{Context as _, Result, anyhow}; -use client::ProxySettings; -use util::ResultExt; - -use extension::ExtensionHostProxy; -use fs::{Fs, RealFs}; -use futures::channel::{mpsc, oneshot}; -use futures::{AsyncRead, AsyncWrite, AsyncWriteExt, FutureExt, SinkExt, select, select_biased}; -use git::GitHostingProviderRegistry; -use gpui::{App, AppContext as _, Context, Entity, UpdateGlobal as _}; -use gpui_tokio::Tokio; -use http_client::{Url, read_proxy_from_env}; -use language::LanguageRegistry; -use node_runtime::{NodeBinaryOptions, NodeRuntime}; -use paths::logs_dir; -use project::project_settings::ProjectSettings; -use util::command::new_smol_command; - -use proto::CrashReport; -use release_channel::{AppCommitSha, AppVersion, RELEASE_CHANNEL, ReleaseChannel}; -use remote::RemoteClient; -use remote::{ - json_log::LogRecord, - protocol::{read_message, write_message}, - proxy::ProxyLaunchError, -}; -use reqwest_client::ReqwestClient; -use rpc::proto::{self, Envelope, REMOTE_SERVER_PROJECT_ID}; -use rpc::{AnyProtoClient, TypedEnvelope}; -use settings::{Settings, SettingsStore, watch_config_file}; -use smol::Async; -use smol::channel::{Receiver, Sender}; -use smol::io::AsyncReadExt; -use smol::{net::unix::UnixListener, stream::StreamExt as _}; -use std::{ - env, - ffi::OsStr, - fs::File, - io::Write, - mem, - ops::ControlFlow, - path::{Path, PathBuf}, - process::ExitStatus, - str::FromStr, - sync::{Arc, LazyLock}, -}; -use thiserror::Error; - -pub static VERSION: LazyLock = LazyLock::new(|| match *RELEASE_CHANNEL { - ReleaseChannel::Stable | ReleaseChannel::Preview => env!("ZED_PKG_VERSION").to_owned(), - ReleaseChannel::Nightly | ReleaseChannel::Dev => { - let commit_sha = option_env!("ZED_COMMIT_SHA").unwrap_or("missing-zed-commit-sha"); - let build_identifier = option_env!("ZED_BUILD_ID"); - if let Some(build_id) = build_identifier { - format!("{build_id}+{commit_sha}") - } else { - commit_sha.to_owned() - } - } -}); - -fn init_logging_proxy() { - env_logger::builder() - .format(|buf, record| { - let mut log_record = LogRecord::new(record); - log_record.message = format!("(remote proxy) {}", log_record.message); - serde_json::to_writer(&mut *buf, &log_record)?; - buf.write_all(b"\n")?; - Ok(()) - }) - .init(); -} - -fn init_logging_server(log_file_path: PathBuf) -> Result>> { - struct MultiWrite { - file: File, - channel: Sender>, - buffer: Vec, - } - - impl Write for MultiWrite { - fn write(&mut self, buf: &[u8]) -> std::io::Result { - let written = self.file.write(buf)?; - self.buffer.extend_from_slice(&buf[..written]); - Ok(written) - } - - fn flush(&mut self) -> std::io::Result<()> { - self.channel - .send_blocking(self.buffer.clone()) - .map_err(std::io::Error::other)?; - self.buffer.clear(); - self.file.flush() - } - } - - let log_file = std::fs::OpenOptions::new() - .create(true) - .append(true) - .open(&log_file_path) - .context("Failed to open log file in append mode")?; - - let (tx, rx) = smol::channel::unbounded(); - - let target = Box::new(MultiWrite { - file: log_file, - channel: tx, - buffer: Vec::new(), - }); - - env_logger::Builder::new() - .filter_level(log::LevelFilter::Info) - .parse_default_env() - .target(env_logger::Target::Pipe(target)) - .format(|buf, record| { - let mut log_record = LogRecord::new(record); - log_record.message = format!("(remote server) {}", log_record.message); - serde_json::to_writer(&mut *buf, &log_record)?; - buf.write_all(b"\n")?; - Ok(()) - }) - .init(); - - Ok(rx) -} - -fn handle_crash_files_requests(project: &Entity, client: &AnyProtoClient) { - client.add_request_handler( - project.downgrade(), - |_, _: TypedEnvelope, _cx| async move { - let mut legacy_panics = Vec::new(); - let mut crashes = Vec::new(); - let mut children = smol::fs::read_dir(paths::logs_dir()).await?; - while let Some(child) = children.next().await { - let child = child?; - let child_path = child.path(); - - let extension = child_path.extension(); - if extension == Some(OsStr::new("panic")) { - let filename = if let Some(filename) = child_path.file_name() { - filename.to_string_lossy() - } else { - continue; - }; - - if !filename.starts_with("zed") { - continue; - } - - let file_contents = smol::fs::read_to_string(&child_path) - .await - .context("error reading panic file")?; - - legacy_panics.push(file_contents); - smol::fs::remove_file(&child_path) - .await - .context("error removing panic") - .log_err(); - } else if extension == Some(OsStr::new("dmp")) { - let mut json_path = child_path.clone(); - json_path.set_extension("json"); - if let Ok(json_content) = smol::fs::read_to_string(&json_path).await { - crashes.push(CrashReport { - metadata: json_content, - minidump_contents: smol::fs::read(&child_path).await?, - }); - smol::fs::remove_file(&child_path).await.log_err(); - smol::fs::remove_file(&json_path).await.log_err(); - } else { - log::error!("Couldn't find json metadata for crash: {child_path:?}"); - } - } - } - - anyhow::Ok(proto::GetCrashFilesResponse { crashes }) - }, - ); -} - -struct ServerListeners { - stdin: UnixListener, - stdout: UnixListener, - stderr: UnixListener, -} - -impl ServerListeners { - pub fn new(stdin_path: PathBuf, stdout_path: PathBuf, stderr_path: PathBuf) -> Result { - Ok(Self { - stdin: UnixListener::bind(stdin_path).context("failed to bind stdin socket")?, - stdout: UnixListener::bind(stdout_path).context("failed to bind stdout socket")?, - stderr: UnixListener::bind(stderr_path).context("failed to bind stderr socket")?, - }) - } -} - -fn start_server( - listeners: ServerListeners, - log_rx: Receiver>, - cx: &mut App, - is_wsl_interop: bool, -) -> AnyProtoClient { - // This is the server idle timeout. If no connection comes in this timeout, the server will shut down. - const IDLE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(10 * 60); - - let (incoming_tx, incoming_rx) = mpsc::unbounded::(); - let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded::(); - let (app_quit_tx, mut app_quit_rx) = mpsc::unbounded::<()>(); - - cx.on_app_quit(move |_| { - let mut app_quit_tx = app_quit_tx.clone(); - async move { - log::info!("app quitting. sending signal to server main loop"); - app_quit_tx.send(()).await.ok(); - } - }) - .detach(); - - cx.spawn(async move |cx| { - let mut stdin_incoming = listeners.stdin.incoming(); - let mut stdout_incoming = listeners.stdout.incoming(); - let mut stderr_incoming = listeners.stderr.incoming(); - - loop { - let streams = futures::future::join3(stdin_incoming.next(), stdout_incoming.next(), stderr_incoming.next()); - - log::info!("accepting new connections"); - let result = select! { - streams = streams.fuse() => { - let (Some(Ok(stdin_stream)), Some(Ok(stdout_stream)), Some(Ok(stderr_stream))) = streams else { - break; - }; - anyhow::Ok((stdin_stream, stdout_stream, stderr_stream)) - } - _ = futures::FutureExt::fuse(smol::Timer::after(IDLE_TIMEOUT)) => { - log::warn!("timed out waiting for new connections after {:?}. exiting.", IDLE_TIMEOUT); - cx.update(|cx| { - // TODO: This is a hack, because in a headless project, shutdown isn't executed - // when calling quit, but it should be. - cx.shutdown(); - cx.quit(); - })?; - break; - } - _ = app_quit_rx.next().fuse() => { - break; - } - }; - - let Ok((mut stdin_stream, mut stdout_stream, mut stderr_stream)) = result else { - break; - }; - - let mut input_buffer = Vec::new(); - let mut output_buffer = Vec::new(); - - let (mut stdin_msg_tx, mut stdin_msg_rx) = mpsc::unbounded::(); - cx.background_spawn(async move { - while let Ok(msg) = read_message(&mut stdin_stream, &mut input_buffer).await { - if (stdin_msg_tx.send(msg).await).is_err() { - break; - } - } - }).detach(); - - loop { - - select_biased! { - _ = app_quit_rx.next().fuse() => { - return anyhow::Ok(()); - } - - stdin_message = stdin_msg_rx.next().fuse() => { - let Some(message) = stdin_message else { - log::warn!("error reading message on stdin. exiting."); - break; - }; - if let Err(error) = incoming_tx.unbounded_send(message) { - log::error!("failed to send message to application: {error:?}. exiting."); - return Err(anyhow!(error)); - } - } - - outgoing_message = outgoing_rx.next().fuse() => { - let Some(message) = outgoing_message else { - log::error!("stdout handler, no message"); - break; - }; - - if let Err(error) = - write_message(&mut stdout_stream, &mut output_buffer, message).await - { - log::error!("failed to write stdout message: {:?}", error); - break; - } - if let Err(error) = stdout_stream.flush().await { - log::error!("failed to flush stdout message: {:?}", error); - break; - } - } - - log_message = log_rx.recv().fuse() => { - if let Ok(log_message) = log_message { - if let Err(error) = stderr_stream.write_all(&log_message).await { - log::error!("failed to write log message to stderr: {:?}", error); - break; - } - if let Err(error) = stderr_stream.flush().await { - log::error!("failed to flush stderr stream: {:?}", error); - break; - } - } - } - } - } - } - anyhow::Ok(()) - }) - .detach(); - - RemoteClient::proto_client_from_channels(incoming_rx, outgoing_tx, cx, "server", is_wsl_interop) -} - -fn init_paths() -> anyhow::Result<()> { - for path in [ - paths::config_dir(), - paths::extensions_dir(), - paths::languages_dir(), - paths::logs_dir(), - paths::temp_dir(), - paths::hang_traces_dir(), - paths::remote_extensions_dir(), - paths::remote_extensions_uploads_dir(), - ] - .iter() - { - std::fs::create_dir_all(path).with_context(|| format!("creating directory {path:?}"))?; - } - Ok(()) -} - -pub fn execute_run( - log_file: PathBuf, - pid_file: PathBuf, - stdin_socket: PathBuf, - stdout_socket: PathBuf, - stderr_socket: PathBuf, -) -> Result<()> { - init_paths()?; - - match daemonize()? { - ControlFlow::Break(_) => return Ok(()), - ControlFlow::Continue(_) => {} - } - - let app = gpui::Application::headless(); - let id = std::process::id().to_string(); - app.background_executor() - .spawn(crashes::init(crashes::InitCrashHandler { - session_id: id, - zed_version: VERSION.to_owned(), - binary: "zed-remote-server".to_string(), - release_channel: release_channel::RELEASE_CHANNEL_NAME.clone(), - commit_sha: option_env!("ZED_COMMIT_SHA").unwrap_or("no_sha").to_owned(), - })) - .detach(); - let log_rx = init_logging_server(log_file)?; - log::info!( - "starting up. pid_file: {:?}, stdin_socket: {:?}, stdout_socket: {:?}, stderr_socket: {:?}", - pid_file, - stdin_socket, - stdout_socket, - stderr_socket - ); - - write_pid_file(&pid_file) - .with_context(|| format!("failed to write pid file: {:?}", &pid_file))?; - - let listeners = ServerListeners::new(stdin_socket, stdout_socket, stderr_socket)?; - - rayon::ThreadPoolBuilder::new() - .num_threads(std::thread::available_parallelism().map_or(1, |n| n.get().div_ceil(2))) - .stack_size(10 * 1024 * 1024) - .thread_name(|ix| format!("RayonWorker{}", ix)) - .build_global() - .unwrap(); - - let (shell_env_loaded_tx, shell_env_loaded_rx) = oneshot::channel(); - app.background_executor() - .spawn(async { - util::load_login_shell_environment().await.log_err(); - shell_env_loaded_tx.send(()).ok(); - }) - .detach(); - - let git_hosting_provider_registry = Arc::new(GitHostingProviderRegistry::new()); - app.run(move |cx| { - settings::init(cx); - let app_commit_sha = option_env!("ZED_COMMIT_SHA").map(|s| AppCommitSha::new(s.to_owned())); - let app_version = AppVersion::load( - env!("ZED_PKG_VERSION"), - option_env!("ZED_BUILD_ID"), - app_commit_sha, - ); - release_channel::init(app_version, cx); - gpui_tokio::init(cx); - - HeadlessProject::init(cx); - - let is_wsl_interop = if cfg!(target_os = "linux") { - // See: https://learn.microsoft.com/en-us/windows/wsl/filesystems#disable-interoperability - matches!(std::fs::read_to_string("/proc/sys/fs/binfmt_misc/WSLInterop"), Ok(s) if s.contains("enabled")) - } else { - false - }; - - log::info!("gpui app started, initializing server"); - let session = start_server(listeners, log_rx, cx, is_wsl_interop); - - GitHostingProviderRegistry::set_global(git_hosting_provider_registry, cx); - git_hosting_providers::init(cx); - dap_adapters::init(cx); - - extension::init(cx); - let extension_host_proxy = ExtensionHostProxy::global(cx); - - json_schema_store::init(cx); - - let project = cx.new(|cx| { - let fs = Arc::new(RealFs::new(None, cx.background_executor().clone())); - let node_settings_rx = initialize_settings(session.clone(), fs.clone(), cx); - - let proxy_url = read_proxy_settings(cx); - - let http_client = { - let _guard = Tokio::handle(cx).enter(); - Arc::new( - ReqwestClient::proxy_and_user_agent( - proxy_url, - &format!( - "Zed-Server/{} ({}; {})", - env!("CARGO_PKG_VERSION"), - std::env::consts::OS, - std::env::consts::ARCH - ), - ) - .expect("Could not start HTTP client"), - ) - }; - - let node_runtime = NodeRuntime::new( - http_client.clone(), - Some(shell_env_loaded_rx), - node_settings_rx, - ); - - let mut languages = LanguageRegistry::new(cx.background_executor().clone()); - languages.set_language_server_download_dir(paths::languages_dir().clone()); - let languages = Arc::new(languages); - - HeadlessProject::new( - HeadlessAppState { - session: session.clone(), - fs, - http_client, - node_runtime, - languages, - extension_host_proxy, - }, - cx, - ) - }); - - handle_crash_files_requests(&project, &session); - - cx.background_spawn(async move { cleanup_old_binaries() }) - .detach(); - - mem::forget(project); - }); - log::info!("gpui app is shut down. quitting."); - 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)] -struct ServerPaths { - log_file: PathBuf, - pid_file: PathBuf, - stdin_socket: PathBuf, - stdout_socket: PathBuf, - stderr_socket: PathBuf, -} - -impl ServerPaths { - 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(), - })?; - - let pid_file = server_dir.join("server.pid"); - let stdin_socket = server_dir.join("stdin.sock"); - let stdout_socket = server_dir.join("stdout.sock"); - let stderr_socket = server_dir.join("stderr.sock"); - let log_file = logs_dir().join(format!("server-{}.log", identifier)); - - Ok(Self { - pid_file, - stdin_socket, - stdout_socket, - stderr_socket, - log_file, - }) - } -} - -#[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> { - init_logging_proxy(); - - let server_paths = ServerPaths::new(&identifier)?; - - let id = std::process::id().to_string(); - smol::spawn(crashes::init(crashes::InitCrashHandler { - session_id: id, - zed_version: VERSION.to_owned(), - binary: "zed-remote-server".to_string(), - release_channel: release_channel::RELEASE_CHANNEL_NAME.clone(), - commit_sha: option_env!("ZED_COMMIT_SHA").unwrap_or("no_sha").to_owned(), - })) - .detach(); - - log::info!("starting proxy process. PID: {}", std::process::id()); - smol::block_on(async { - let server_pid = check_pid_file(&server_paths.pid_file) - .await - .map_err(|source| ExecuteProxyError::CheckPidFile { - source, - path: server_paths.pid_file.clone(), - })?; - 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, - )); - } - } else { - if let Some(pid) = server_pid { - log::info!( - "proxy found server already running with PID {}. Killing process and cleaning up files...", - pid - ); - kill_running_server(pid, &server_paths).await?; - } - - spawn_server(&server_paths) - .await - .map_err(ExecuteProxyError::SpawnServer)?; - }; - Ok(()) - })?; - - let stdin_task = smol::spawn(async move { - let stdin = Async::new(std::io::stdin())?; - let stream = smol::net::unix::UnixStream::connect(&server_paths.stdin_socket).await?; - handle_io(stdin, stream, "stdin").await - }); - - let stdout_task: smol::Task> = smol::spawn(async move { - let stdout = Async::new(std::io::stdout())?; - let stream = smol::net::unix::UnixStream::connect(&server_paths.stdout_socket).await?; - handle_io(stream, stdout, "stdout").await - }); - - let stderr_task: smol::Task> = smol::spawn(async move { - let mut stderr = Async::new(std::io::stderr())?; - let mut stream = smol::net::unix::UnixStream::connect(&server_paths.stderr_socket).await?; - let mut stderr_buffer = vec![0; 2048]; - loop { - match stream - .read(&mut stderr_buffer) - .await - .context("reading stderr")? - { - 0 => { - let error = - std::io::Error::new(std::io::ErrorKind::UnexpectedEof, "stderr closed"); - Err(anyhow!(error))?; - } - n => { - stderr.write_all(&stderr_buffer[..n]).await?; - stderr.flush().await?; - } - } - } - }); - - 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), - } - }) { - log::error!( - "encountered error while forwarding messages: {:?}, terminating...", - forwarding_result - ); - return Err(forwarding_result); - } - - Ok(()) -} - -async fn kill_running_server(pid: u32, paths: &ServerPaths) -> Result<(), ExecuteProxyError> { - log::info!("killing existing server with PID {}", pid); - new_smol_command("kill") - .arg(pid.to_string()) - .output() - .await - .map_err(|source| ExecuteProxyError::KillRunningServer { source, pid })?; - - for file in [ - &paths.pid_file, - &paths.stdin_socket, - &paths.stdout_socket, - &paths.stderr_socket, - ] { - log::debug!("cleaning up file {:?} before starting new server", file); - std::fs::remove_file(file).ok(); - } - 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 }, -} - -async fn spawn_server(paths: &ServerPaths) -> Result<(), SpawnServerError> { - if paths.stdin_socket.exists() { - std::fs::remove_file(&paths.stdin_socket).map_err(SpawnServerError::RemoveStdinSocket)?; - } - if paths.stdout_socket.exists() { - std::fs::remove_file(&paths.stdout_socket).map_err(SpawnServerError::RemoveStdoutSocket)?; - } - if paths.stderr_socket.exists() { - std::fs::remove_file(&paths.stderr_socket).map_err(SpawnServerError::RemoveStderrSocket)?; - } - - let binary_name = std::env::current_exe().map_err(SpawnServerError::CurrentExe)?; - let mut server_process = new_smol_command(binary_name); - server_process - .arg("run") - .arg("--log-file") - .arg(&paths.log_file) - .arg("--pid-file") - .arg(&paths.pid_file) - .arg("--stdin-socket") - .arg(&paths.stdin_socket) - .arg("--stdout-socket") - .arg(&paths.stdout_socket) - .arg("--stderr-socket") - .arg(&paths.stderr_socket); - - let status = server_process - .status() - .await - .map_err(SpawnServerError::ProcessStatus)?; - - if !status.success() { - return Err(SpawnServerError::LaunchStatus { - status, - paths: format!( - "log file: {:?}, pid file: {:?}", - paths.log_file, paths.pid_file, - ), - }); - } - - let mut total_time_waited = std::time::Duration::from_secs(0); - let wait_duration = std::time::Duration::from_millis(20); - while !paths.stdout_socket.exists() - || !paths.stdin_socket.exists() - || !paths.stderr_socket.exists() - { - log::debug!("waiting for server to be ready to accept connections..."); - std::thread::sleep(wait_duration); - total_time_waited += wait_duration; - } - - log::info!( - "server ready to accept connections. total time waited: {:?}", - total_time_waited - ); - - 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, -} - -async fn check_pid_file(path: &Path) -> Result, CheckPidError> { - let Some(pid) = std::fs::read_to_string(&path) - .ok() - .and_then(|contents| contents.parse::().ok()) - else { - return Ok(None); - }; - - log::debug!("Checking if process with PID {} exists...", pid); - match new_smol_command("kill") - .arg("-0") - .arg(pid.to_string()) - .output() - .await - { - Ok(output) if output.status.success() => { - log::debug!( - "Process with PID {} exists. NOT spawning new server, but attaching to existing one.", - pid - ); - Ok(Some(pid)) - } - _ => { - 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 })?; - Ok(None) - } - } -} - -fn write_pid_file(path: &Path) -> Result<()> { - if path.exists() { - std::fs::remove_file(path)?; - } - let pid = std::process::id().to_string(); - log::debug!("writing PID {} to file {:?}", pid, path); - std::fs::write(path, pid).context("Failed to write PID file") -} - -async fn handle_io(mut reader: R, mut writer: W, socket_name: &str) -> Result<()> -where - R: AsyncRead + Unpin, - W: AsyncWrite + Unpin, -{ - use remote::protocol::{read_message_raw, write_size_prefixed_buffer}; - - let mut buffer = Vec::new(); - loop { - read_message_raw(&mut reader, &mut buffer) - .await - .with_context(|| format!("failed to read message from {}", socket_name))?; - write_size_prefixed_buffer(&mut writer, &mut buffer) - .await - .with_context(|| format!("failed to write message to {}", socket_name))?; - writer.flush().await?; - buffer.clear(); - } -} - -fn initialize_settings( - session: AnyProtoClient, - fs: Arc, - cx: &mut App, -) -> watch::Receiver> { - let user_settings_file_rx = - watch_config_file(cx.background_executor(), fs, paths::settings_file().clone()); - - handle_settings_file_changes(user_settings_file_rx, cx, { - move |err, _cx| { - if let Some(e) = err { - log::info!("Server settings failed to change: {}", e); - - session - .send(proto::Toast { - project_id: REMOTE_SERVER_PROJECT_ID, - notification_id: "server-settings-failed".to_string(), - message: format!( - "Error in settings on remote host {:?}: {}", - paths::settings_file(), - e - ), - }) - .log_err(); - } else { - session - .send(proto::HideToast { - project_id: REMOTE_SERVER_PROJECT_ID, - notification_id: "server-settings-failed".to_string(), - }) - .log_err(); - } - } - }); - - let (mut tx, rx) = watch::channel(None); - let mut node_settings = None; - cx.observe_global::(move |cx| { - let new_node_settings = &ProjectSettings::get_global(cx).node; - if Some(new_node_settings) != node_settings.as_ref() { - log::info!("Got new node settings: {new_node_settings:?}"); - let options = NodeBinaryOptions { - allow_path_lookup: !new_node_settings.ignore_system_version, - // TODO: Implement this setting - allow_binary_download: true, - use_paths: new_node_settings.path.as_ref().map(|node_path| { - let node_path = PathBuf::from(shellexpand::tilde(node_path).as_ref()); - let npm_path = new_node_settings - .npm_path - .as_ref() - .map(|path| PathBuf::from(shellexpand::tilde(&path).as_ref())); - ( - node_path.clone(), - npm_path.unwrap_or_else(|| { - let base_path = PathBuf::new(); - node_path.parent().unwrap_or(&base_path).join("npm") - }), - ) - }), - }; - node_settings = Some(new_node_settings.clone()); - tx.send(Some(options)).ok(); - } - }) - .detach(); - - rx -} - -pub fn handle_settings_file_changes( - mut server_settings_file: mpsc::UnboundedReceiver, - cx: &mut App, - settings_changed: impl Fn(Option, &mut App) + 'static, -) { - let server_settings_content = cx - .background_executor() - .block(server_settings_file.next()) - .unwrap(); - SettingsStore::update_global(cx, |store, cx| { - store - .set_server_settings(&server_settings_content, cx) - .log_err(); - }); - cx.spawn(async move |cx| { - while let Some(server_settings_content) = server_settings_file.next().await { - let result = cx.update_global(|store: &mut SettingsStore, cx| { - let result = store.set_server_settings(&server_settings_content, cx); - if let Err(err) = &result { - log::error!("Failed to load server settings: {err}"); - } - settings_changed(result.err(), cx); - cx.refresh_windows(); - }); - if result.is_err() { - break; // App dropped - } - } - }) - .detach(); -} - -fn read_proxy_settings(cx: &mut Context) -> Option { - let proxy_str = ProxySettings::get_global(cx).proxy.to_owned(); - - proxy_str - .as_ref() - .and_then(|input: &String| { - input - .parse::() - .inspect_err(|e| log::error!("Error parsing proxy settings: {}", e)) - .ok() - }) - .or_else(read_proxy_from_env) -} - -fn daemonize() -> Result> { - match fork::fork().map_err(|e| anyhow!("failed to call fork with error code {e}"))? { - fork::Fork::Parent(_) => { - return Ok(ControlFlow::Break(())); - } - fork::Fork::Child => {} - } - - // Once we've detached from the parent, we want to close stdout/stderr/stdin - // so that the outer SSH process is not attached to us in any way anymore. - unsafe { redirect_standard_streams() }?; - - Ok(ControlFlow::Continue(())) -} - -unsafe fn redirect_standard_streams() -> Result<()> { - let devnull_fd = unsafe { libc::open(b"/dev/null\0" as *const [u8; 10] as _, libc::O_RDWR) }; - anyhow::ensure!(devnull_fd != -1, "failed to open /dev/null"); - - let process_stdio = |name, fd| { - let reopened_fd = unsafe { libc::dup2(devnull_fd, fd) }; - anyhow::ensure!( - reopened_fd != -1, - format!("failed to redirect {} to /dev/null", name) - ); - Ok(()) - }; - - process_stdio("stdin", libc::STDIN_FILENO)?; - process_stdio("stdout", libc::STDOUT_FILENO)?; - process_stdio("stderr", libc::STDERR_FILENO)?; - - anyhow::ensure!( - unsafe { libc::close(devnull_fd) != -1 }, - "failed to close /dev/null fd after redirecting" - ); - - Ok(()) -} - -fn cleanup_old_binaries() -> Result<()> { - let server_dir = paths::remote_server_dir_relative(); - let release_channel = release_channel::RELEASE_CHANNEL.dev_name(); - let prefix = format!("zed-remote-server-{}-", release_channel); - - for entry in std::fs::read_dir(server_dir.as_std_path())? { - let path = entry?.path(); - - if let Some(file_name) = path.file_name() - && let Some(version) = file_name.to_string_lossy().strip_prefix(&prefix) - && !is_new_version(version) - && !is_file_in_use(file_name) - { - log::info!("removing old remote server binary: {:?}", path); - std::fs::remove_file(&path)?; - } - } - - Ok(()) -} - -fn is_new_version(version: &str) -> bool { - semver::Version::from_str(version) - .ok() - .zip(semver::Version::from_str(env!("ZED_PKG_VERSION")).ok()) - .is_some_and(|(version, current_version)| version >= current_version) -} - -fn is_file_in_use(file_name: &OsStr) -> bool { - let info = sysinfo::System::new_with_specifics(sysinfo::RefreshKind::nothing().with_processes( - sysinfo::ProcessRefreshKind::nothing().with_exe(sysinfo::UpdateKind::Always), - )); - - for process in info.processes().values() { - if process - .exe() - .is_some_and(|exe| exe.file_name().is_some_and(|name| name == file_name)) - { - return true; - } - } - - false -} diff --git a/crates/repl/Cargo.toml b/crates/repl/Cargo.toml deleted file mode 100644 index 14040ba484..0000000000 --- a/crates/repl/Cargo.toml +++ /dev/null @@ -1,70 +0,0 @@ -[package] -name = "repl" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/repl.rs" -doctest = false - -[dependencies] -alacritty_terminal.workspace = true -anyhow.workspace = true -async-dispatcher.workspace = true -async-tungstenite = { workspace = true, features = ["tokio", "tokio-rustls-manual-roots", "tokio-runtime"] } -base64.workspace = true -client.workspace = true -collections.workspace = true -command_palette_hooks.workspace = true -editor.workspace = true -feature_flags.workspace = true -file_icons.workspace = true -futures.workspace = true -gpui.workspace = true -http_client.workspace = true -image.workspace = true -jupyter-websocket-client.workspace = true -jupyter-protocol.workspace = true -language.workspace = true -log.workspace = true -markdown_preview.workspace = true -menu.workspace = true -multi_buffer.workspace = true -nbformat.workspace = true -project.workspace = true -runtimelib.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -smol.workspace = true -telemetry.workspace = true -terminal.workspace = true -terminal_view.workspace = true -theme.workspace = true -ui.workspace = true -util.workspace = true -uuid.workspace = true -workspace.workspace = true -picker.workspace = true - -[dev-dependencies] -editor = { workspace = true, features = ["test-support"] } -env_logger.workspace = true -gpui = { workspace = true, features = ["test-support"] } -http_client = { workspace = true, features = ["test-support"] } -indoc.workspace = true -language = { workspace = true, features = ["test-support"] } -languages = { workspace = true, features = ["test-support"] } -project = { workspace = true, features = ["test-support"] } -settings = { workspace = true, features = ["test-support"] } -terminal_view = { workspace = true, features = ["test-support"] } -theme = { workspace = true, features = ["test-support"] } -tree-sitter-md.workspace = true -tree-sitter-typescript.workspace = true -tree-sitter-python.workspace = true -util = { workspace = true, features = ["test-support"] } diff --git a/crates/repl/LICENSE-GPL b/crates/repl/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/repl/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/repl/src/components.rs b/crates/repl/src/components.rs deleted file mode 100644 index 53236bc6a8..0000000000 --- a/crates/repl/src/components.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod kernel_list_item; -mod kernel_options; - -pub use kernel_list_item::*; -pub use kernel_options::*; diff --git a/crates/repl/src/components/kernel_list_item.rs b/crates/repl/src/components/kernel_list_item.rs deleted file mode 100644 index 467407fbd2..0000000000 --- a/crates/repl/src/components/kernel_list_item.rs +++ /dev/null @@ -1,60 +0,0 @@ -use gpui::AnyElement; -use ui::{Indicator, ListItem, prelude::*}; - -use crate::KernelSpecification; - -#[derive(IntoElement)] -pub struct KernelListItem { - kernel_specification: KernelSpecification, - status_color: Color, - buttons: Vec, - children: Vec, -} - -impl KernelListItem { - pub fn new(kernel_specification: KernelSpecification) -> Self { - Self { - kernel_specification, - status_color: Color::Disabled, - buttons: Vec::new(), - children: Vec::new(), - } - } - - pub fn status_color(mut self, color: Color) -> Self { - self.status_color = color; - self - } - - pub fn button(mut self, button: impl IntoElement) -> Self { - self.buttons.push(button.into_any_element()); - self - } - - pub fn buttons(mut self, buttons: impl IntoIterator) -> Self { - self.buttons - .extend(buttons.into_iter().map(|button| button.into_any_element())); - self - } -} - -impl ParentElement for KernelListItem { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements); - } -} - -impl RenderOnce for KernelListItem { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - ListItem::new(self.kernel_specification.name()) - .selectable(false) - .start_slot( - h_flex() - .size_3() - .justify_center() - .child(Indicator::dot().color(self.status_color)), - ) - .children(self.children) - .end_slot(h_flex().gap_2().children(self.buttons)) - } -} diff --git a/crates/repl/src/components/kernel_options.rs b/crates/repl/src/components/kernel_options.rs deleted file mode 100644 index bceefd08cc..0000000000 --- a/crates/repl/src/components/kernel_options.rs +++ /dev/null @@ -1,283 +0,0 @@ -use crate::KERNEL_DOCS_URL; -use crate::kernels::KernelSpecification; -use crate::repl_store::ReplStore; - -use gpui::AnyView; -use gpui::DismissEvent; - -use gpui::FontWeight; -use picker::Picker; -use picker::PickerDelegate; -use project::WorktreeId; - -use std::sync::Arc; -use ui::ListItemSpacing; - -use gpui::SharedString; -use gpui::Task; -use ui::{ListItem, PopoverMenu, PopoverMenuHandle, PopoverTrigger, prelude::*}; - -type OnSelect = Box; - -#[derive(IntoElement)] -pub struct KernelSelector -where - T: PopoverTrigger + ButtonCommon, - TT: Fn(&mut Window, &mut App) -> AnyView + 'static, -{ - handle: Option>>, - on_select: OnSelect, - trigger: T, - tooltip: TT, - info_text: Option, - worktree_id: WorktreeId, -} - -pub struct KernelPickerDelegate { - all_kernels: Vec, - filtered_kernels: Vec, - selected_kernelspec: Option, - on_select: OnSelect, -} - -// Helper function to truncate long paths -fn truncate_path(path: &SharedString, max_length: usize) -> SharedString { - if path.len() <= max_length { - path.to_string().into() - } else { - let truncated = path.chars().rev().take(max_length - 3).collect::(); - format!("...{}", truncated.chars().rev().collect::()).into() - } -} - -impl KernelSelector -where - T: PopoverTrigger + ButtonCommon, - TT: Fn(&mut Window, &mut App) -> AnyView + 'static, -{ - pub fn new(on_select: OnSelect, worktree_id: WorktreeId, trigger: T, tooltip: TT) -> Self { - KernelSelector { - on_select, - handle: None, - trigger, - tooltip, - info_text: None, - worktree_id, - } - } - - pub fn with_handle(mut self, handle: PopoverMenuHandle>) -> Self { - self.handle = Some(handle); - self - } - - pub fn with_info_text(mut self, text: impl Into) -> Self { - self.info_text = Some(text.into()); - self - } -} - -impl PickerDelegate for KernelPickerDelegate { - type ListItem = ListItem; - - fn match_count(&self) -> usize { - self.filtered_kernels.len() - } - - fn selected_index(&self) -> usize { - if let Some(kernelspec) = self.selected_kernelspec.as_ref() { - self.filtered_kernels - .iter() - .position(|k| k == kernelspec) - .unwrap_or(0) - } else { - 0 - } - } - - fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context>) { - self.selected_kernelspec = self.filtered_kernels.get(ix).cloned(); - cx.notify(); - } - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - "Select a kernel...".into() - } - - fn update_matches( - &mut self, - query: String, - _window: &mut Window, - _cx: &mut Context>, - ) -> Task<()> { - let all_kernels = self.all_kernels.clone(); - - if query.is_empty() { - self.filtered_kernels = all_kernels; - return Task::ready(()); - } - - self.filtered_kernels = if query.is_empty() { - all_kernels - } else { - all_kernels - .into_iter() - .filter(|kernel| kernel.name().to_lowercase().contains(&query.to_lowercase())) - .collect() - }; - - Task::ready(()) - } - - fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context>) { - if let Some(kernelspec) = &self.selected_kernelspec { - (self.on_select)(kernelspec.clone(), window, cx); - cx.emit(DismissEvent); - } - } - - fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context>) {} - - fn render_match( - &self, - ix: usize, - selected: bool, - _: &mut Window, - cx: &mut Context>, - ) -> Option { - let kernelspec = self.filtered_kernels.get(ix)?; - let is_selected = self.selected_kernelspec.as_ref() == Some(kernelspec); - let icon = kernelspec.icon(cx); - - let (name, kernel_type, path_or_url) = match kernelspec { - KernelSpecification::Jupyter(_) => (kernelspec.name(), "Jupyter", None), - KernelSpecification::PythonEnv(_) => ( - kernelspec.name(), - "Python Env", - Some(truncate_path(&kernelspec.path(), 42)), - ), - KernelSpecification::Remote(_) => ( - kernelspec.name(), - "Remote", - Some(truncate_path(&kernelspec.path(), 42)), - ), - }; - - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .child( - h_flex() - .w_full() - .gap_3() - .child(icon.color(Color::Default).size(IconSize::Medium)) - .child( - v_flex() - .flex_grow() - .gap_0p5() - .child( - h_flex() - .justify_between() - .child( - div().w_48().text_ellipsis().child( - Label::new(name) - .weight(FontWeight::MEDIUM) - .size(LabelSize::Default), - ), - ) - .when_some(path_or_url, |flex, path| { - flex.text_ellipsis().child( - Label::new(path) - .size(LabelSize::Small) - .color(Color::Muted), - ) - }), - ) - .child( - h_flex() - .gap_1() - .child( - Label::new(kernelspec.language()) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child( - Label::new(kernel_type) - .size(LabelSize::Small) - .color(Color::Muted), - ), - ), - ), - ) - .when(is_selected, |item| { - item.end_slot( - Icon::new(IconName::Check) - .color(Color::Accent) - .size(IconSize::Small), - ) - }), - ) - } - - fn render_footer( - &self, - _: &mut Window, - cx: &mut Context>, - ) -> Option { - Some( - h_flex() - .w_full() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .p_1() - .gap_4() - .child( - Button::new("kernel-docs", "Kernel Docs") - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .icon_position(IconPosition::End) - .on_click(move |_, _, cx| cx.open_url(KERNEL_DOCS_URL)), - ) - .into_any(), - ) - } -} - -impl RenderOnce for KernelSelector -where - T: PopoverTrigger + ButtonCommon, - TT: Fn(&mut Window, &mut App) -> AnyView + 'static, -{ - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let store = ReplStore::global(cx).read(cx); - - let all_kernels: Vec = store - .kernel_specifications_for_worktree(self.worktree_id) - .cloned() - .collect(); - - let selected_kernelspec = store.active_kernelspec(self.worktree_id, None, cx); - - let delegate = KernelPickerDelegate { - on_select: self.on_select, - all_kernels: all_kernels.clone(), - filtered_kernels: all_kernels, - selected_kernelspec, - }; - - let picker_view = cx.new(|cx| { - Picker::uniform_list(delegate, window, cx) - .width(rems(30.)) - .max_height(Some(rems(20.).into())) - }); - - PopoverMenu::new("kernel-switcher") - .menu(move |_window, _cx| Some(picker_view.clone())) - .trigger_with_tooltip(self.trigger, self.tooltip) - .attach(gpui::Corner::BottomLeft) - .when_some(self.handle, |menu, handle| menu.with_handle(handle)) - } -} diff --git a/crates/repl/src/jupyter_settings.rs b/crates/repl/src/jupyter_settings.rs deleted file mode 100644 index 0adf80dc66..0000000000 --- a/crates/repl/src/jupyter_settings.rs +++ /dev/null @@ -1,28 +0,0 @@ -use collections::HashMap; - -use editor::EditorSettings; -use gpui::App; -use settings::{RegisterSetting, Settings}; - -#[derive(Debug, Default, RegisterSetting)] -pub struct JupyterSettings { - pub kernel_selections: HashMap, -} - -impl JupyterSettings { - pub fn enabled(cx: &App) -> bool { - // In order to avoid a circular dependency between `editor` and `repl` crates, - // we put the `enable` flag on its settings. - // This allows the editor to set up context for key bindings/actions. - EditorSettings::jupyter_enabled(cx) - } -} - -impl Settings for JupyterSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - let jupyter = content.editor.jupyter.clone().unwrap(); - Self { - kernel_selections: jupyter.kernel_selections.unwrap_or_default(), - } - } -} diff --git a/crates/repl/src/kernels/mod.rs b/crates/repl/src/kernels/mod.rs deleted file mode 100644 index ab8f27121e..0000000000 --- a/crates/repl/src/kernels/mod.rs +++ /dev/null @@ -1,259 +0,0 @@ -mod native_kernel; -use std::{fmt::Debug, future::Future, path::PathBuf}; - -use futures::{ - channel::mpsc::{self, Receiver}, - future::Shared, - stream, -}; -use gpui::{App, Entity, Task, Window}; -use language::LanguageName; -pub use native_kernel::*; - -mod remote_kernels; -use project::{Project, ProjectPath, Toolchains, WorktreeId}; -pub use remote_kernels::*; - -use anyhow::Result; -use jupyter_protocol::JupyterKernelspec; -use runtimelib::{ExecutionState, JupyterMessage, KernelInfoReply}; -use ui::{Icon, IconName, SharedString}; -use util::rel_path::RelPath; - -pub type JupyterMessageChannel = stream::SelectAll>; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum KernelSpecification { - Remote(RemoteKernelSpecification), - Jupyter(LocalKernelSpecification), - PythonEnv(LocalKernelSpecification), -} - -impl KernelSpecification { - pub fn name(&self) -> SharedString { - match self { - Self::Jupyter(spec) => spec.name.clone().into(), - Self::PythonEnv(spec) => spec.name.clone().into(), - Self::Remote(spec) => spec.name.clone().into(), - } - } - - pub fn type_name(&self) -> SharedString { - match self { - Self::Jupyter(_) => "Jupyter".into(), - Self::PythonEnv(_) => "Python Environment".into(), - Self::Remote(_) => "Remote".into(), - } - } - - pub fn path(&self) -> SharedString { - SharedString::from(match self { - Self::Jupyter(spec) => spec.path.to_string_lossy().into_owned(), - Self::PythonEnv(spec) => spec.path.to_string_lossy().into_owned(), - Self::Remote(spec) => spec.url.to_string(), - }) - } - - pub fn language(&self) -> SharedString { - SharedString::from(match self { - Self::Jupyter(spec) => spec.kernelspec.language.clone(), - Self::PythonEnv(spec) => spec.kernelspec.language.clone(), - Self::Remote(spec) => spec.kernelspec.language.clone(), - }) - } - - pub fn icon(&self, cx: &App) -> Icon { - let lang_name = match self { - Self::Jupyter(spec) => spec.kernelspec.language.clone(), - Self::PythonEnv(spec) => spec.kernelspec.language.clone(), - Self::Remote(spec) => spec.kernelspec.language.clone(), - }; - - file_icons::FileIcons::get(cx) - .get_icon_for_type(&lang_name.to_lowercase(), cx) - .map(Icon::from_path) - .unwrap_or(Icon::new(IconName::ReplNeutral)) - } -} - -pub fn python_env_kernel_specifications( - project: &Entity, - worktree_id: WorktreeId, - cx: &mut App, -) -> impl Future>> + use<> { - let python_language = LanguageName::new_static("Python"); - let toolchains = project.read(cx).available_toolchains( - ProjectPath { - worktree_id, - path: RelPath::empty().into(), - }, - python_language, - cx, - ); - let background_executor = cx.background_executor().clone(); - - async move { - let (toolchains, user_toolchains) = if let Some(Toolchains { - toolchains, - root_path: _, - user_toolchains, - }) = toolchains.await - { - (toolchains, user_toolchains) - } else { - return Ok(Vec::new()); - }; - - let kernelspecs = user_toolchains - .into_values() - .flatten() - .chain(toolchains.toolchains) - .map(|toolchain| { - background_executor.spawn(async move { - let python_path = toolchain.path.to_string(); - - // Check if ipykernel is installed - let ipykernel_check = util::command::new_smol_command(&python_path) - .args(&["-c", "import ipykernel"]) - .output() - .await; - - if ipykernel_check.is_ok() && ipykernel_check.unwrap().status.success() { - // Create a default kernelspec for this environment - let default_kernelspec = JupyterKernelspec { - argv: vec![ - python_path.clone(), - "-m".to_string(), - "ipykernel_launcher".to_string(), - "-f".to_string(), - "{connection_file}".to_string(), - ], - display_name: toolchain.name.to_string(), - language: "python".to_string(), - interrupt_mode: None, - metadata: None, - env: None, - }; - - Some(KernelSpecification::PythonEnv(LocalKernelSpecification { - name: toolchain.name.to_string(), - path: PathBuf::from(&python_path), - kernelspec: default_kernelspec, - })) - } else { - None - } - }) - }); - - let kernel_specs = futures::future::join_all(kernelspecs) - .await - .into_iter() - .flatten() - .collect(); - - anyhow::Ok(kernel_specs) - } -} - -pub trait RunningKernel: Send + Debug { - fn request_tx(&self) -> mpsc::Sender; - fn working_directory(&self) -> &PathBuf; - fn execution_state(&self) -> &ExecutionState; - fn set_execution_state(&mut self, state: ExecutionState); - fn kernel_info(&self) -> Option<&KernelInfoReply>; - fn set_kernel_info(&mut self, info: KernelInfoReply); - fn force_shutdown(&mut self, window: &mut Window, cx: &mut App) -> Task>; -} - -#[derive(Debug, Clone)] -pub enum KernelStatus { - Idle, - Busy, - Starting, - Error, - ShuttingDown, - Shutdown, - Restarting, -} - -impl KernelStatus { - pub fn is_connected(&self) -> bool { - matches!(self, KernelStatus::Idle | KernelStatus::Busy) - } -} - -impl ToString for KernelStatus { - fn to_string(&self) -> String { - match self { - KernelStatus::Idle => "Idle".to_string(), - KernelStatus::Busy => "Busy".to_string(), - KernelStatus::Starting => "Starting".to_string(), - KernelStatus::Error => "Error".to_string(), - KernelStatus::ShuttingDown => "Shutting Down".to_string(), - KernelStatus::Shutdown => "Shutdown".to_string(), - KernelStatus::Restarting => "Restarting".to_string(), - } - } -} - -#[derive(Debug)] -pub enum Kernel { - RunningKernel(Box), - StartingKernel(Shared>), - ErroredLaunch(String), - ShuttingDown, - Shutdown, - Restarting, -} - -impl From<&Kernel> for KernelStatus { - fn from(kernel: &Kernel) -> Self { - match kernel { - Kernel::RunningKernel(kernel) => match kernel.execution_state() { - ExecutionState::Idle => KernelStatus::Idle, - ExecutionState::Busy => KernelStatus::Busy, - ExecutionState::Unknown => KernelStatus::Error, - ExecutionState::Starting => KernelStatus::Starting, - ExecutionState::Restarting => KernelStatus::Restarting, - ExecutionState::Terminating => KernelStatus::ShuttingDown, - ExecutionState::AutoRestarting => KernelStatus::Restarting, - ExecutionState::Dead => KernelStatus::Error, - ExecutionState::Other(_) => KernelStatus::Error, - }, - Kernel::StartingKernel(_) => KernelStatus::Starting, - Kernel::ErroredLaunch(_) => KernelStatus::Error, - Kernel::ShuttingDown => KernelStatus::ShuttingDown, - Kernel::Shutdown => KernelStatus::Shutdown, - Kernel::Restarting => KernelStatus::Restarting, - } - } -} - -impl Kernel { - pub fn status(&self) -> KernelStatus { - self.into() - } - - pub fn set_execution_state(&mut self, status: &ExecutionState) { - if let Kernel::RunningKernel(running_kernel) = self { - running_kernel.set_execution_state(status.clone()); - } - } - - pub fn set_kernel_info(&mut self, kernel_info: &KernelInfoReply) { - if let Kernel::RunningKernel(running_kernel) = self { - running_kernel.set_kernel_info(kernel_info.clone()); - } - } - - pub fn is_shutting_down(&self) -> bool { - match self { - Kernel::Restarting | Kernel::ShuttingDown => true, - Kernel::RunningKernel(_) - | Kernel::StartingKernel(_) - | Kernel::ErroredLaunch(_) - | Kernel::Shutdown => false, - } - } -} diff --git a/crates/repl/src/kernels/native_kernel.rs b/crates/repl/src/kernels/native_kernel.rs deleted file mode 100644 index 8630768dec..0000000000 --- a/crates/repl/src/kernels/native_kernel.rs +++ /dev/null @@ -1,542 +0,0 @@ -use anyhow::{Context as _, Result}; -use futures::{ - AsyncBufReadExt as _, SinkExt as _, - channel::mpsc::{self}, - io::BufReader, - stream::{FuturesUnordered, SelectAll, StreamExt}, -}; -use gpui::{App, AppContext as _, Entity, EntityId, Task, Window}; -use jupyter_protocol::{ - ExecutionState, JupyterKernelspec, JupyterMessage, JupyterMessageContent, KernelInfoReply, - connection_info::{ConnectionInfo, Transport}, -}; -use project::Fs; -use runtimelib::dirs; -use smol::{net::TcpListener, process::Command}; -use std::{ - env, - fmt::Debug, - net::{IpAddr, Ipv4Addr, SocketAddr}, - path::PathBuf, - sync::Arc, -}; -use uuid::Uuid; - -use crate::Session; - -use super::RunningKernel; - -#[derive(Debug, Clone)] -pub struct LocalKernelSpecification { - pub name: String, - pub path: PathBuf, - pub kernelspec: JupyterKernelspec, -} - -impl PartialEq for LocalKernelSpecification { - fn eq(&self, other: &Self) -> bool { - self.name == other.name && self.path == other.path - } -} - -impl Eq for LocalKernelSpecification {} - -impl LocalKernelSpecification { - #[must_use] - fn command(&self, connection_path: &PathBuf) -> Result { - let argv = &self.kernelspec.argv; - - anyhow::ensure!(!argv.is_empty(), "Empty argv in kernelspec {}", self.name); - anyhow::ensure!(argv.len() >= 2, "Invalid argv in kernelspec {}", self.name); - anyhow::ensure!( - argv.iter().any(|arg| arg == "{connection_file}"), - "Missing 'connection_file' in argv in kernelspec {}", - self.name - ); - - let mut cmd = util::command::new_smol_command(&argv[0]); - - for arg in &argv[1..] { - if arg == "{connection_file}" { - cmd.arg(connection_path); - } else { - cmd.arg(arg); - } - } - - if let Some(env) = &self.kernelspec.env { - cmd.envs(env); - } - - Ok(cmd) - } -} - -// Find a set of open ports. This creates a listener with port set to 0. The listener will be closed at the end when it goes out of scope. -// There's a race condition between closing the ports and usage by a kernel, but it's inherent to the Jupyter protocol. -async fn peek_ports(ip: IpAddr) -> Result<[u16; 5]> { - let mut addr_zeroport: SocketAddr = SocketAddr::new(ip, 0); - addr_zeroport.set_port(0); - let mut ports: [u16; 5] = [0; 5]; - for i in 0..5 { - let listener = TcpListener::bind(addr_zeroport).await?; - let addr = listener.local_addr()?; - ports[i] = addr.port(); - } - Ok(ports) -} - -pub struct NativeRunningKernel { - pub process: smol::process::Child, - connection_path: PathBuf, - _process_status_task: Option>, - pub working_directory: PathBuf, - pub request_tx: mpsc::Sender, - pub execution_state: ExecutionState, - pub kernel_info: Option, -} - -impl Debug for NativeRunningKernel { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("RunningKernel") - .field("process", &self.process) - .finish() - } -} - -impl NativeRunningKernel { - pub fn new( - kernel_specification: LocalKernelSpecification, - entity_id: EntityId, - working_directory: PathBuf, - fs: Arc, - // todo: convert to weak view - session: Entity, - window: &mut Window, - cx: &mut App, - ) -> Task>> { - window.spawn(cx, async move |cx| { - let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); - let ports = peek_ports(ip).await?; - - let connection_info = ConnectionInfo { - transport: Transport::TCP, - ip: ip.to_string(), - stdin_port: ports[0], - control_port: ports[1], - hb_port: ports[2], - shell_port: ports[3], - iopub_port: ports[4], - signature_scheme: "hmac-sha256".to_string(), - key: uuid::Uuid::new_v4().to_string(), - kernel_name: Some(format!("zed-{}", kernel_specification.name)), - }; - - let runtime_dir = dirs::runtime_dir(); - fs.create_dir(&runtime_dir) - .await - .with_context(|| format!("Failed to create jupyter runtime dir {runtime_dir:?}"))?; - let connection_path = runtime_dir.join(format!("kernel-zed-{entity_id}.json")); - let content = serde_json::to_string(&connection_info)?; - fs.atomic_write(connection_path.clone(), content).await?; - - let mut cmd = kernel_specification.command(&connection_path)?; - - let mut process = cmd - .current_dir(&working_directory) - .stdout(std::process::Stdio::piped()) - .stderr(std::process::Stdio::piped()) - .stdin(std::process::Stdio::piped()) - .kill_on_drop(true) - .spawn() - .context("failed to start the kernel process")?; - - let session_id = Uuid::new_v4().to_string(); - - let mut iopub_socket = - runtimelib::create_client_iopub_connection(&connection_info, "", &session_id) - .await?; - let mut shell_socket = - runtimelib::create_client_shell_connection(&connection_info, &session_id).await?; - let mut control_socket = - runtimelib::create_client_control_connection(&connection_info, &session_id).await?; - - let (request_tx, mut request_rx) = - futures::channel::mpsc::channel::(100); - - let (mut control_reply_tx, control_reply_rx) = futures::channel::mpsc::channel(100); - let (mut shell_reply_tx, shell_reply_rx) = futures::channel::mpsc::channel(100); - - let mut messages_rx = SelectAll::new(); - messages_rx.push(control_reply_rx); - messages_rx.push(shell_reply_rx); - - cx.spawn({ - let session = session.clone(); - - async move |cx| { - while let Some(message) = messages_rx.next().await { - session - .update_in(cx, |session, window, cx| { - session.route(&message, window, cx); - }) - .ok(); - } - } - }) - .detach(); - - // iopub task - let iopub_task = cx.spawn({ - let session = session.clone(); - - async move |cx| -> anyhow::Result<()> { - loop { - let message = iopub_socket.read().await?; - session - .update_in(cx, |session, window, cx| { - session.route(&message, window, cx); - }) - .ok(); - } - } - }); - - let (mut control_request_tx, mut control_request_rx) = - futures::channel::mpsc::channel(100); - let (mut shell_request_tx, mut shell_request_rx) = futures::channel::mpsc::channel(100); - - let routing_task = cx.background_spawn({ - async move { - while let Some(message) = request_rx.next().await { - match message.content { - JupyterMessageContent::DebugRequest(_) - | JupyterMessageContent::InterruptRequest(_) - | JupyterMessageContent::ShutdownRequest(_) => { - control_request_tx.send(message).await?; - } - _ => { - shell_request_tx.send(message).await?; - } - } - } - anyhow::Ok(()) - } - }); - - let shell_task = cx.background_spawn({ - async move { - while let Some(message) = shell_request_rx.next().await { - shell_socket.send(message).await.ok(); - let reply = shell_socket.read().await?; - shell_reply_tx.send(reply).await?; - } - anyhow::Ok(()) - } - }); - - let control_task = cx.background_spawn({ - async move { - while let Some(message) = control_request_rx.next().await { - control_socket.send(message).await.ok(); - let reply = control_socket.read().await?; - control_reply_tx.send(reply).await?; - } - anyhow::Ok(()) - } - }); - - let stderr = process.stderr.take(); - - cx.spawn(async move |_cx| { - if stderr.is_none() { - return; - } - let reader = BufReader::new(stderr.unwrap()); - let mut lines = reader.lines(); - while let Some(Ok(line)) = lines.next().await { - log::error!("kernel: {}", line); - } - }) - .detach(); - - let stdout = process.stdout.take(); - - cx.spawn(async move |_cx| { - if stdout.is_none() { - return; - } - let reader = BufReader::new(stdout.unwrap()); - let mut lines = reader.lines(); - while let Some(Ok(line)) = lines.next().await { - log::info!("kernel: {}", line); - } - }) - .detach(); - - cx.spawn({ - let session = session.clone(); - async move |cx| { - async fn with_name( - name: &'static str, - task: Task>, - ) -> (&'static str, Result<()>) { - (name, task.await) - } - - let mut tasks = FuturesUnordered::new(); - tasks.push(with_name("iopub task", iopub_task)); - tasks.push(with_name("shell task", shell_task)); - tasks.push(with_name("control task", control_task)); - tasks.push(with_name("routing task", routing_task)); - - while let Some((name, result)) = tasks.next().await { - if let Err(err) = result { - log::error!("kernel: handling failed for {name}: {err:?}"); - - session - .update(cx, |session, cx| { - session.kernel_errored( - format!("handling failed for {name}: {err}"), - cx, - ); - cx.notify(); - }) - .ok(); - } - } - } - }) - .detach(); - - let status = process.status(); - - let process_status_task = cx.spawn(async move |cx| { - let error_message = match status.await { - Ok(status) => { - if status.success() { - log::info!("kernel process exited successfully"); - return; - } - - format!("kernel process exited with status: {:?}", status) - } - Err(err) => { - format!("kernel process exited with error: {:?}", err) - } - }; - - log::error!("{}", error_message); - - session - .update(cx, |session, cx| { - session.kernel_errored(error_message, cx); - - cx.notify(); - }) - .ok(); - }); - - anyhow::Ok(Box::new(Self { - process, - request_tx, - working_directory, - _process_status_task: Some(process_status_task), - connection_path, - execution_state: ExecutionState::Idle, - kernel_info: None, - }) as Box) - }) - } -} - -impl RunningKernel for NativeRunningKernel { - fn request_tx(&self) -> mpsc::Sender { - self.request_tx.clone() - } - - fn working_directory(&self) -> &PathBuf { - &self.working_directory - } - - fn execution_state(&self) -> &ExecutionState { - &self.execution_state - } - - fn set_execution_state(&mut self, state: ExecutionState) { - self.execution_state = state; - } - - fn kernel_info(&self) -> Option<&KernelInfoReply> { - self.kernel_info.as_ref() - } - - fn set_kernel_info(&mut self, info: KernelInfoReply) { - self.kernel_info = Some(info); - } - - fn force_shutdown(&mut self, _window: &mut Window, _cx: &mut App) -> Task> { - self._process_status_task.take(); - self.request_tx.close_channel(); - Task::ready(self.process.kill().context("killing the kernel process")) - } -} - -impl Drop for NativeRunningKernel { - fn drop(&mut self) { - std::fs::remove_file(&self.connection_path).ok(); - self.request_tx.close_channel(); - self.process.kill().ok(); - } -} - -async fn read_kernelspec_at( - // Path should be a directory to a jupyter kernelspec, as in - // /usr/local/share/jupyter/kernels/python3 - kernel_dir: PathBuf, - fs: &dyn Fs, -) -> Result { - let path = kernel_dir; - let kernel_name = if let Some(kernel_name) = path.file_name() { - kernel_name.to_string_lossy().into_owned() - } else { - anyhow::bail!("Invalid kernelspec directory: {path:?}"); - }; - - if !fs.is_dir(path.as_path()).await { - anyhow::bail!("Not a directory: {path:?}"); - } - - let expected_kernel_json = path.join("kernel.json"); - let spec = fs.load(expected_kernel_json.as_path()).await?; - let spec = serde_json::from_str::(&spec)?; - - Ok(LocalKernelSpecification { - name: kernel_name, - path, - kernelspec: spec, - }) -} - -/// Read a directory of kernelspec directories -async fn read_kernels_dir(path: PathBuf, fs: &dyn Fs) -> Result> { - let mut kernelspec_dirs = fs.read_dir(&path).await?; - - let mut valid_kernelspecs = Vec::new(); - while let Some(path) = kernelspec_dirs.next().await { - match path { - Ok(path) => { - if fs.is_dir(path.as_path()).await - && let Ok(kernelspec) = read_kernelspec_at(path, fs).await - { - valid_kernelspecs.push(kernelspec); - } - } - Err(err) => log::warn!("Error reading kernelspec directory: {err:?}"), - } - } - - Ok(valid_kernelspecs) -} - -pub async fn local_kernel_specifications(fs: Arc) -> Result> { - let mut data_dirs = dirs::data_dirs(); - - // Pick up any kernels from conda or conda environment - if let Ok(conda_prefix) = env::var("CONDA_PREFIX") { - let conda_prefix = PathBuf::from(conda_prefix); - let conda_data_dir = conda_prefix.join("share").join("jupyter"); - data_dirs.push(conda_data_dir); - } - - // Search for kernels inside the base python environment - let command = util::command::new_smol_command("python") - .arg("-c") - .arg("import sys; print(sys.prefix)") - .output() - .await; - - if let Ok(command) = command - && command.status.success() - { - let python_prefix = String::from_utf8(command.stdout); - if let Ok(python_prefix) = python_prefix { - let python_prefix = PathBuf::from(python_prefix.trim()); - let python_data_dir = python_prefix.join("share").join("jupyter"); - data_dirs.push(python_data_dir); - } - } - - let kernel_dirs = data_dirs - .iter() - .map(|dir| dir.join("kernels")) - .map(|path| read_kernels_dir(path, fs.as_ref())) - .collect::>(); - - let kernel_dirs = futures::future::join_all(kernel_dirs).await; - let kernel_dirs = kernel_dirs - .into_iter() - .filter_map(Result::ok) - .flatten() - .collect::>(); - - Ok(kernel_dirs) -} - -#[cfg(test)] -mod test { - use super::*; - use std::path::PathBuf; - - use gpui::TestAppContext; - use project::FakeFs; - use serde_json::json; - - #[gpui::test] - async fn test_get_kernelspecs(cx: &mut TestAppContext) { - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/jupyter", - json!({ - ".zed": { - "settings.json": r#"{ "tab_size": 8 }"#, - "tasks.json": r#"[{ - "label": "cargo check", - "command": "cargo", - "args": ["check", "--all"] - },]"#, - }, - "kernels": { - "python": { - "kernel.json": r#"{ - "display_name": "Python 3", - "language": "python", - "argv": ["python3", "-m", "ipykernel_launcher", "-f", "{connection_file}"], - "env": {} - }"# - }, - "deno": { - "kernel.json": r#"{ - "display_name": "Deno", - "language": "typescript", - "argv": ["deno", "run", "--unstable", "--allow-net", "--allow-read", "https://deno.land/std/http/file_server.ts", "{connection_file}"], - "env": {} - }"# - } - }, - }), - ) - .await; - - let mut kernels = read_kernels_dir(PathBuf::from("/jupyter/kernels"), fs.as_ref()) - .await - .unwrap(); - - kernels.sort_by(|a, b| a.name.cmp(&b.name)); - - assert_eq!( - kernels.iter().map(|c| c.name.clone()).collect::>(), - vec!["deno", "python"] - ); - } -} diff --git a/crates/repl/src/kernels/remote_kernels.rs b/crates/repl/src/kernels/remote_kernels.rs deleted file mode 100644 index 6bc8b0d1b1..0000000000 --- a/crates/repl/src/kernels/remote_kernels.rs +++ /dev/null @@ -1,294 +0,0 @@ -use futures::{SinkExt as _, channel::mpsc}; -use gpui::{App, AppContext as _, Entity, Task, Window}; -use http_client::{AsyncBody, HttpClient, Request}; -use jupyter_protocol::{ExecutionState, JupyterKernelspec, JupyterMessage, KernelInfoReply}; - -use async_tungstenite::tokio::connect_async; -use async_tungstenite::tungstenite::{client::IntoClientRequest, http::HeaderValue}; - -use futures::StreamExt; -use smol::io::AsyncReadExt as _; - -use crate::Session; - -use super::RunningKernel; -use anyhow::Result; -use jupyter_websocket_client::{ - JupyterWebSocket, JupyterWebSocketReader, JupyterWebSocketWriter, KernelLaunchRequest, - KernelSpecsResponse, RemoteServer, -}; -use std::{fmt::Debug, sync::Arc}; - -#[derive(Debug, Clone)] -pub struct RemoteKernelSpecification { - pub name: String, - pub url: String, - pub token: String, - pub kernelspec: JupyterKernelspec, -} - -pub async fn launch_remote_kernel( - remote_server: &RemoteServer, - http_client: Arc, - kernel_name: &str, - _path: &str, -) -> Result { - // - let kernel_launch_request = KernelLaunchRequest { - name: kernel_name.to_string(), - // Note: since the path we have locally may not be the same as the one on the remote server, - // we don't send it. We'll have to evaluate this decision along the way. - path: None, - }; - - let kernel_launch_request = serde_json::to_string(&kernel_launch_request)?; - - let request = Request::builder() - .method("POST") - .uri(&remote_server.api_url("/kernels")) - .header("Authorization", format!("token {}", remote_server.token)) - .body(AsyncBody::from(kernel_launch_request))?; - - let response = http_client.send(request).await?; - - if !response.status().is_success() { - let mut body = String::new(); - response.into_body().read_to_string(&mut body).await?; - anyhow::bail!("Failed to launch kernel: {body}"); - } - - let mut body = String::new(); - response.into_body().read_to_string(&mut body).await?; - - let response: jupyter_websocket_client::Kernel = serde_json::from_str(&body)?; - - Ok(response.id) -} - -pub async fn list_remote_kernelspecs( - remote_server: RemoteServer, - http_client: Arc, -) -> Result> { - let url = remote_server.api_url("/kernelspecs"); - - let request = Request::builder() - .method("GET") - .uri(&url) - .header("Authorization", format!("token {}", remote_server.token)) - .body(AsyncBody::default())?; - - let response = http_client.send(request).await?; - - anyhow::ensure!( - response.status().is_success(), - "Failed to fetch kernel specs: {}", - response.status() - ); - let mut body = response.into_body(); - - let mut body_bytes = Vec::new(); - body.read_to_end(&mut body_bytes).await?; - - let kernel_specs: KernelSpecsResponse = serde_json::from_slice(&body_bytes)?; - - let remote_kernelspecs = kernel_specs - .kernelspecs - .into_iter() - .map(|(name, spec)| RemoteKernelSpecification { - name, - url: remote_server.base_url.clone(), - token: remote_server.token.clone(), - kernelspec: spec.spec, - }) - .collect::>(); - - anyhow::ensure!(!remote_kernelspecs.is_empty(), "No kernel specs found"); - Ok(remote_kernelspecs) -} - -impl PartialEq for RemoteKernelSpecification { - fn eq(&self, other: &Self) -> bool { - self.name == other.name && self.url == other.url - } -} - -impl Eq for RemoteKernelSpecification {} - -pub struct RemoteRunningKernel { - remote_server: RemoteServer, - _receiving_task: Task>, - _routing_task: Task>, - http_client: Arc, - pub working_directory: std::path::PathBuf, - pub request_tx: mpsc::Sender, - pub execution_state: ExecutionState, - pub kernel_info: Option, - pub kernel_id: String, -} - -impl RemoteRunningKernel { - pub fn new( - kernelspec: RemoteKernelSpecification, - working_directory: std::path::PathBuf, - session: Entity, - window: &mut Window, - cx: &mut App, - ) -> Task>> { - let remote_server = RemoteServer { - base_url: kernelspec.url, - token: kernelspec.token, - }; - - let http_client = cx.http_client(); - - window.spawn(cx, async move |cx| { - let kernel_id = launch_remote_kernel( - &remote_server, - http_client.clone(), - &kernelspec.name, - working_directory.to_str().unwrap_or_default(), - ) - .await?; - - let ws_url = format!( - "{}/api/kernels/{}/channels?token={}", - remote_server.base_url.replace("http", "ws"), - kernel_id, - remote_server.token - ); - - let mut req: Request<()> = ws_url.into_client_request()?; - let headers = req.headers_mut(); - - headers.insert( - "User-Agent", - HeaderValue::from_str(&format!( - "Zed/{} ({}; {})", - "repl", - std::env::consts::OS, - std::env::consts::ARCH - ))?, - ); - - let response = connect_async(req).await; - - let (ws_stream, _response) = response?; - - let kernel_socket = JupyterWebSocket { inner: ws_stream }; - - let (mut w, mut r): (JupyterWebSocketWriter, JupyterWebSocketReader) = - kernel_socket.split(); - - let (request_tx, mut request_rx) = - futures::channel::mpsc::channel::(100); - - let routing_task = cx.background_spawn({ - async move { - while let Some(message) = request_rx.next().await { - w.send(message).await.ok(); - } - Ok(()) - } - }); - - let receiving_task = cx.spawn({ - let session = session.clone(); - - async move |cx| { - while let Some(message) = r.next().await { - match message { - Ok(message) => { - session - .update_in(cx, |session, window, cx| { - session.route(&message, window, cx); - }) - .ok(); - } - Err(e) => { - log::error!("Error receiving message: {:?}", e); - } - } - } - Ok(()) - } - }); - - anyhow::Ok(Box::new(Self { - _routing_task: routing_task, - _receiving_task: receiving_task, - remote_server, - working_directory, - request_tx, - // todo(kyle): pull this from the kernel API to start with - execution_state: ExecutionState::Idle, - kernel_info: None, - kernel_id, - http_client: http_client.clone(), - }) as Box) - }) - } -} - -impl Debug for RemoteRunningKernel { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("RemoteRunningKernel") - // custom debug that keeps tokens out of logs - .field("remote_server url", &self.remote_server.base_url) - .field("working_directory", &self.working_directory) - .field("request_tx", &self.request_tx) - .field("execution_state", &self.execution_state) - .field("kernel_info", &self.kernel_info) - .finish() - } -} - -impl RunningKernel for RemoteRunningKernel { - fn request_tx(&self) -> futures::channel::mpsc::Sender { - self.request_tx.clone() - } - - fn working_directory(&self) -> &std::path::PathBuf { - &self.working_directory - } - - fn execution_state(&self) -> &runtimelib::ExecutionState { - &self.execution_state - } - - fn set_execution_state(&mut self, state: runtimelib::ExecutionState) { - self.execution_state = state; - } - - fn kernel_info(&self) -> Option<&runtimelib::KernelInfoReply> { - self.kernel_info.as_ref() - } - - fn set_kernel_info(&mut self, info: runtimelib::KernelInfoReply) { - self.kernel_info = Some(info); - } - - fn force_shutdown(&mut self, window: &mut Window, cx: &mut App) -> Task> { - let url = self - .remote_server - .api_url(&format!("/kernels/{}", self.kernel_id)); - let token = self.remote_server.token.clone(); - let http_client = self.http_client.clone(); - - window.spawn(cx, async move |_| { - let request = Request::builder() - .method("DELETE") - .uri(&url) - .header("Authorization", format!("token {}", token)) - .body(AsyncBody::default())?; - - let response = http_client.send(request).await?; - - anyhow::ensure!( - response.status().is_success(), - "Failed to shutdown kernel: {}", - response.status() - ); - Ok(()) - }) - } -} diff --git a/crates/repl/src/notebook.rs b/crates/repl/src/notebook.rs deleted file mode 100644 index 9c6738f799..0000000000 --- a/crates/repl/src/notebook.rs +++ /dev/null @@ -1,4 +0,0 @@ -mod cell; -mod notebook_ui; -pub use cell::*; -pub use notebook_ui::*; diff --git a/crates/repl/src/notebook/cell.rs b/crates/repl/src/notebook/cell.rs deleted file mode 100644 index 87b8e1d55a..0000000000 --- a/crates/repl/src/notebook/cell.rs +++ /dev/null @@ -1,746 +0,0 @@ -#![allow(unused, dead_code)] -use std::sync::Arc; - -use editor::{Editor, EditorMode, MultiBuffer}; -use futures::future::Shared; -use gpui::{ - App, Entity, Hsla, RetainAllImageCache, Task, TextStyleRefinement, image_cache, prelude::*, -}; -use language::{Buffer, Language, LanguageRegistry}; -use markdown_preview::{markdown_parser::parse_markdown, markdown_renderer::render_markdown_block}; -use nbformat::v4::{CellId, CellMetadata, CellType}; -use settings::Settings as _; -use theme::ThemeSettings; -use ui::{IconButtonShape, prelude::*}; -use util::ResultExt; - -use crate::{ - notebook::{CODE_BLOCK_INSET, GUTTER_WIDTH}, - outputs::{Output, plain::TerminalOutput, user_error::ErrorView}, -}; - -#[derive(Copy, Clone, PartialEq, PartialOrd)] -pub enum CellPosition { - First, - Middle, - Last, -} - -pub enum CellControlType { - RunCell, - RerunCell, - ClearCell, - CellOptions, - CollapseCell, - ExpandCell, -} - -impl CellControlType { - fn icon_name(&self) -> IconName { - match self { - CellControlType::RunCell => IconName::PlayFilled, - CellControlType::RerunCell => IconName::ArrowCircle, - CellControlType::ClearCell => IconName::ListX, - CellControlType::CellOptions => IconName::Ellipsis, - CellControlType::CollapseCell => IconName::ChevronDown, - CellControlType::ExpandCell => IconName::ChevronRight, - } - } -} - -pub struct CellControl { - button: IconButton, -} - -impl CellControl { - fn new(id: impl Into, control_type: CellControlType) -> Self { - let icon_name = control_type.icon_name(); - let id = id.into(); - let button = IconButton::new(id, icon_name) - .icon_size(IconSize::Small) - .shape(IconButtonShape::Square); - Self { button } - } -} - -impl Clickable for CellControl { - fn on_click( - self, - handler: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - let button = self.button.on_click(handler); - Self { button } - } - - fn cursor_style(self, _cursor_style: gpui::CursorStyle) -> Self { - self - } -} - -/// A notebook cell -#[derive(Clone)] -pub enum Cell { - Code(Entity), - Markdown(Entity), - Raw(Entity), -} - -fn convert_outputs( - outputs: &Vec, - window: &mut Window, - cx: &mut App, -) -> Vec { - outputs - .iter() - .map(|output| match output { - nbformat::v4::Output::Stream { text, .. } => Output::Stream { - content: cx.new(|cx| TerminalOutput::from(&text.0, window, cx)), - }, - nbformat::v4::Output::DisplayData(display_data) => { - Output::new(&display_data.data, None, window, cx) - } - nbformat::v4::Output::ExecuteResult(execute_result) => { - Output::new(&execute_result.data, None, window, cx) - } - nbformat::v4::Output::Error(error) => Output::ErrorOutput(ErrorView { - ename: error.ename.clone(), - evalue: error.evalue.clone(), - traceback: cx - .new(|cx| TerminalOutput::from(&error.traceback.join("\n"), window, cx)), - }), - }) - .collect() -} - -impl Cell { - pub fn load( - cell: &nbformat::v4::Cell, - languages: &Arc, - notebook_language: Shared>>>, - window: &mut Window, - cx: &mut App, - ) -> Self { - match cell { - nbformat::v4::Cell::Markdown { - id, - metadata, - source, - .. - } => { - let source = source.join(""); - - let entity = cx.new(|cx| { - let markdown_parsing_task = { - let languages = languages.clone(); - let source = source.clone(); - - cx.spawn_in(window, async move |this, cx| { - let parsed_markdown = cx - .background_spawn(async move { - parse_markdown(&source, None, Some(languages)).await - }) - .await; - - this.update(cx, |cell: &mut MarkdownCell, _| { - cell.parsed_markdown = Some(parsed_markdown); - }) - .log_err(); - }) - }; - - MarkdownCell { - markdown_parsing_task, - image_cache: RetainAllImageCache::new(cx), - languages: languages.clone(), - id: id.clone(), - metadata: metadata.clone(), - source: source.clone(), - parsed_markdown: None, - selected: false, - cell_position: None, - } - }); - - Cell::Markdown(entity) - } - nbformat::v4::Cell::Code { - id, - metadata, - execution_count, - source, - outputs, - } => Cell::Code(cx.new(|cx| { - let text = source.join(""); - - let buffer = cx.new(|cx| Buffer::local(text.clone(), cx)); - let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx)); - - let editor_view = cx.new(|cx| { - let mut editor = Editor::new( - EditorMode::AutoHeight { - min_lines: 1, - max_lines: Some(1024), - }, - multi_buffer, - None, - window, - cx, - ); - - let theme = ThemeSettings::get_global(cx); - - let refinement = TextStyleRefinement { - font_family: Some(theme.buffer_font.family.clone()), - font_size: Some(theme.buffer_font_size(cx).into()), - color: Some(cx.theme().colors().editor_foreground), - background_color: Some(gpui::transparent_black()), - ..Default::default() - }; - - editor.set_text(text, window, cx); - editor.set_show_gutter(false, cx); - editor.set_text_style_refinement(refinement); - - // editor.set_read_only(true); - editor - }); - - let buffer = buffer.clone(); - let language_task = cx.spawn_in(window, async move |this, cx| { - let language = notebook_language.await; - - buffer.update(cx, |buffer, cx| { - buffer.set_language(language.clone(), cx); - }); - }); - - CodeCell { - id: id.clone(), - metadata: metadata.clone(), - execution_count: *execution_count, - source: source.join(""), - editor: editor_view, - outputs: convert_outputs(outputs, window, cx), - selected: false, - language_task, - cell_position: None, - } - })), - nbformat::v4::Cell::Raw { - id, - metadata, - source, - } => Cell::Raw(cx.new(|_| RawCell { - id: id.clone(), - metadata: metadata.clone(), - source: source.join(""), - selected: false, - cell_position: None, - })), - } - } -} - -pub trait RenderableCell: Render { - const CELL_TYPE: CellType; - - fn id(&self) -> &CellId; - fn cell_type(&self) -> CellType; - fn metadata(&self) -> &CellMetadata; - fn source(&self) -> &String; - fn selected(&self) -> bool; - fn set_selected(&mut self, selected: bool) -> &mut Self; - fn selected_bg_color(&self, window: &mut Window, cx: &mut Context) -> Hsla { - if self.selected() { - let mut color = cx.theme().colors().icon_accent; - color.fade_out(0.9); - color - } else { - // TODO: this is wrong - cx.theme().colors().tab_bar_background - } - } - fn control(&self, _window: &mut Window, _cx: &mut Context) -> Option { - None - } - - fn cell_position_spacer( - &self, - is_first: bool, - window: &mut Window, - cx: &mut Context, - ) -> Option { - let cell_position = self.cell_position(); - - if (cell_position == Some(&CellPosition::First) && is_first) - || (cell_position == Some(&CellPosition::Last) && !is_first) - { - Some(div().flex().w_full().h(DynamicSpacing::Base12.px(cx))) - } else { - None - } - } - - fn gutter(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let is_selected = self.selected(); - - div() - .relative() - .h_full() - .w(px(GUTTER_WIDTH)) - .child( - div() - .w(px(GUTTER_WIDTH)) - .flex() - .flex_none() - .justify_center() - .h_full() - .child( - div() - .flex_none() - .w(px(1.)) - .h_full() - .when(is_selected, |this| this.bg(cx.theme().colors().icon_accent)) - .when(!is_selected, |this| this.bg(cx.theme().colors().border)), - ), - ) - .when_some(self.control(window, cx), |this, control| { - this.child( - div() - .absolute() - .top(px(CODE_BLOCK_INSET - 2.0)) - .left_0() - .flex() - .flex_none() - .w(px(GUTTER_WIDTH)) - .h(px(GUTTER_WIDTH + 12.0)) - .items_center() - .justify_center() - .bg(cx.theme().colors().tab_bar_background) - .child(control.button), - ) - }) - } - - fn cell_position(&self) -> Option<&CellPosition>; - fn set_cell_position(&mut self, position: CellPosition) -> &mut Self; -} - -pub trait RunnableCell: RenderableCell { - fn execution_count(&self) -> Option; - fn set_execution_count(&mut self, count: i32) -> &mut Self; - fn run(&mut self, window: &mut Window, cx: &mut Context) -> (); -} - -pub struct MarkdownCell { - id: CellId, - metadata: CellMetadata, - image_cache: Entity, - source: String, - parsed_markdown: Option, - markdown_parsing_task: Task<()>, - selected: bool, - cell_position: Option, - languages: Arc, -} - -impl RenderableCell for MarkdownCell { - const CELL_TYPE: CellType = CellType::Markdown; - - fn id(&self) -> &CellId { - &self.id - } - - fn cell_type(&self) -> CellType { - CellType::Markdown - } - - fn metadata(&self) -> &CellMetadata { - &self.metadata - } - - fn source(&self) -> &String { - &self.source - } - - fn selected(&self) -> bool { - self.selected - } - - fn set_selected(&mut self, selected: bool) -> &mut Self { - self.selected = selected; - self - } - - fn control(&self, _window: &mut Window, _: &mut Context) -> Option { - None - } - - fn cell_position(&self) -> Option<&CellPosition> { - self.cell_position.as_ref() - } - - fn set_cell_position(&mut self, cell_position: CellPosition) -> &mut Self { - self.cell_position = Some(cell_position); - self - } -} - -impl Render for MarkdownCell { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let Some(parsed) = self.parsed_markdown.as_ref() else { - return div(); - }; - - let mut markdown_render_context = - markdown_preview::markdown_renderer::RenderContext::new(None, window, cx); - - v_flex() - .size_full() - // TODO: Move base cell render into trait impl so we don't have to repeat this - .children(self.cell_position_spacer(true, window, cx)) - .child( - h_flex() - .w_full() - .pr_6() - .rounded_xs() - .items_start() - .gap(DynamicSpacing::Base08.rems(cx)) - .bg(self.selected_bg_color(window, cx)) - .child(self.gutter(window, cx)) - .child( - v_flex() - .image_cache(self.image_cache.clone()) - .size_full() - .flex_1() - .p_3() - .font_ui(cx) - .text_size(TextSize::Default.rems(cx)) - .children(parsed.children.iter().map(|child| { - div().relative().child(div().relative().child( - render_markdown_block(child, &mut markdown_render_context), - )) - })), - ), - ) - // TODO: Move base cell render into trait impl so we don't have to repeat this - .children(self.cell_position_spacer(false, window, cx)) - } -} - -pub struct CodeCell { - id: CellId, - metadata: CellMetadata, - execution_count: Option, - source: String, - editor: Entity, - outputs: Vec, - selected: bool, - cell_position: Option, - language_task: Task<()>, -} - -impl CodeCell { - pub fn is_dirty(&self, cx: &App) -> bool { - self.editor.read(cx).buffer().read(cx).is_dirty(cx) - } - pub fn has_outputs(&self) -> bool { - !self.outputs.is_empty() - } - - pub fn clear_outputs(&mut self) { - self.outputs.clear(); - } - - fn output_control(&self) -> Option { - if self.has_outputs() { - Some(CellControlType::ClearCell) - } else { - None - } - } - - pub fn gutter_output(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let is_selected = self.selected(); - - div() - .relative() - .h_full() - .w(px(GUTTER_WIDTH)) - .child( - div() - .w(px(GUTTER_WIDTH)) - .flex() - .flex_none() - .justify_center() - .h_full() - .child( - div() - .flex_none() - .w(px(1.)) - .h_full() - .when(is_selected, |this| this.bg(cx.theme().colors().icon_accent)) - .when(!is_selected, |this| this.bg(cx.theme().colors().border)), - ), - ) - .when(self.has_outputs(), |this| { - this.child( - div() - .absolute() - .top(px(CODE_BLOCK_INSET - 2.0)) - .left_0() - .flex() - .flex_none() - .w(px(GUTTER_WIDTH)) - .h(px(GUTTER_WIDTH + 12.0)) - .items_center() - .justify_center() - .bg(cx.theme().colors().tab_bar_background) - .child(IconButton::new("control", IconName::Ellipsis)), - ) - }) - } -} - -impl RenderableCell for CodeCell { - const CELL_TYPE: CellType = CellType::Code; - - fn id(&self) -> &CellId { - &self.id - } - - fn cell_type(&self) -> CellType { - CellType::Code - } - - fn metadata(&self) -> &CellMetadata { - &self.metadata - } - - fn source(&self) -> &String { - &self.source - } - - fn control(&self, window: &mut Window, cx: &mut Context) -> Option { - let cell_control = if self.has_outputs() { - CellControl::new("rerun-cell", CellControlType::RerunCell) - } else { - CellControl::new("run-cell", CellControlType::RunCell) - .on_click(cx.listener(move |this, _, window, cx| this.run(window, cx))) - }; - - Some(cell_control) - } - - fn selected(&self) -> bool { - self.selected - } - - fn set_selected(&mut self, selected: bool) -> &mut Self { - self.selected = selected; - self - } - - fn cell_position(&self) -> Option<&CellPosition> { - self.cell_position.as_ref() - } - - fn set_cell_position(&mut self, cell_position: CellPosition) -> &mut Self { - self.cell_position = Some(cell_position); - self - } -} - -impl RunnableCell for CodeCell { - fn run(&mut self, window: &mut Window, cx: &mut Context) { - println!("Running code cell: {}", self.id); - } - - fn execution_count(&self) -> Option { - self.execution_count - .and_then(|count| if count > 0 { Some(count) } else { None }) - } - - fn set_execution_count(&mut self, count: i32) -> &mut Self { - self.execution_count = Some(count); - self - } -} - -impl Render for CodeCell { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - v_flex() - .size_full() - // TODO: Move base cell render into trait impl so we don't have to repeat this - .children(self.cell_position_spacer(true, window, cx)) - // Editor portion - .child( - h_flex() - .w_full() - .pr_6() - .rounded_xs() - .items_start() - .gap(DynamicSpacing::Base08.rems(cx)) - .bg(self.selected_bg_color(window, cx)) - .child(self.gutter(window, cx)) - .child( - div().py_1p5().w_full().child( - div() - .flex() - .size_full() - .flex_1() - .py_3() - .px_5() - .rounded_lg() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().editor_background) - .child(div().w_full().child(self.editor.clone())), - ), - ), - ) - // Output portion - .child( - h_flex() - .w_full() - .pr_6() - .rounded_xs() - .items_start() - .gap(DynamicSpacing::Base08.rems(cx)) - .bg(self.selected_bg_color(window, cx)) - .child(self.gutter_output(window, cx)) - .child( - div().py_1p5().w_full().child( - div() - .flex() - .size_full() - .flex_1() - .py_3() - .px_5() - .rounded_lg() - .border_1() - // .border_color(cx.theme().colors().border) - // .bg(cx.theme().colors().editor_background) - .child(div().w_full().children(self.outputs.iter().map( - |output| { - let content = match output { - Output::Plain { content, .. } => { - Some(content.clone().into_any_element()) - } - Output::Markdown { content, .. } => { - Some(content.clone().into_any_element()) - } - Output::Stream { content, .. } => { - Some(content.clone().into_any_element()) - } - Output::Image { content, .. } => { - Some(content.clone().into_any_element()) - } - Output::Message(message) => Some( - div().child(message.clone()).into_any_element(), - ), - Output::Table { content, .. } => { - Some(content.clone().into_any_element()) - } - Output::ErrorOutput(error_view) => { - error_view.render(window, cx) - } - Output::ClearOutputWaitMarker => None, - }; - - div() - // .w_full() - // .mt_3() - // .p_3() - // .rounded_sm() - // .bg(cx.theme().colors().editor_background) - // .border(px(1.)) - // .border_color(cx.theme().colors().border) - // .shadow_xs() - .children(content) - }, - ))), - ), - ), - ) - // TODO: Move base cell render into trait impl so we don't have to repeat this - .children(self.cell_position_spacer(false, window, cx)) - } -} - -pub struct RawCell { - id: CellId, - metadata: CellMetadata, - source: String, - selected: bool, - cell_position: Option, -} - -impl RenderableCell for RawCell { - const CELL_TYPE: CellType = CellType::Raw; - - fn id(&self) -> &CellId { - &self.id - } - - fn cell_type(&self) -> CellType { - CellType::Raw - } - - fn metadata(&self) -> &CellMetadata { - &self.metadata - } - - fn source(&self) -> &String { - &self.source - } - - fn selected(&self) -> bool { - self.selected - } - - fn set_selected(&mut self, selected: bool) -> &mut Self { - self.selected = selected; - self - } - - fn cell_position(&self) -> Option<&CellPosition> { - self.cell_position.as_ref() - } - - fn set_cell_position(&mut self, cell_position: CellPosition) -> &mut Self { - self.cell_position = Some(cell_position); - self - } -} - -impl Render for RawCell { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - v_flex() - .size_full() - // TODO: Move base cell render into trait impl so we don't have to repeat this - .children(self.cell_position_spacer(true, window, cx)) - .child( - h_flex() - .w_full() - .pr_2() - .rounded_xs() - .items_start() - .gap(DynamicSpacing::Base08.rems(cx)) - .bg(self.selected_bg_color(window, cx)) - .child(self.gutter(window, cx)) - .child( - div() - .flex() - .size_full() - .flex_1() - .p_3() - .font_ui(cx) - .text_size(TextSize::Default.rems(cx)) - .child(self.source.clone()), - ), - ) - // TODO: Move base cell render into trait impl so we don't have to repeat this - .children(self.cell_position_spacer(false, window, cx)) - } -} diff --git a/crates/repl/src/notebook/notebook_ui.rs b/crates/repl/src/notebook/notebook_ui.rs deleted file mode 100644 index 07c6e9c8aa..0000000000 --- a/crates/repl/src/notebook/notebook_ui.rs +++ /dev/null @@ -1,836 +0,0 @@ -#![allow(unused, dead_code)] -use std::future::Future; -use std::{path::PathBuf, sync::Arc}; - -use anyhow::{Context as _, Result}; -use client::proto::ViewId; -use collections::HashMap; -use feature_flags::{FeatureFlagAppExt as _, NotebookFeatureFlag}; -use futures::FutureExt; -use futures::future::Shared; -use gpui::{ - AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, ListScrollEvent, ListState, - Point, Task, actions, list, prelude::*, -}; -use language::{Language, LanguageRegistry}; -use project::{Project, ProjectEntryId, ProjectPath}; -use ui::{Tooltip, prelude::*}; -use workspace::item::{ItemEvent, SaveOptions, TabContentParams}; -use workspace::searchable::SearchableItemHandle; -use workspace::{Item, ItemHandle, Pane, ProjectItem, ToolbarItemLocation}; -use workspace::{ToolbarItemEvent, ToolbarItemView}; - -use super::{Cell, CellPosition, RenderableCell}; - -use nbformat::v4::CellId; -use nbformat::v4::Metadata as NotebookMetadata; - -actions!( - notebook, - [ - /// Opens a Jupyter notebook file. - OpenNotebook, - /// Runs all cells in the notebook. - RunAll, - /// Clears all cell outputs. - ClearOutputs, - /// Moves the current cell up. - MoveCellUp, - /// Moves the current cell down. - MoveCellDown, - /// Adds a new markdown cell. - AddMarkdownBlock, - /// Adds a new code cell. - AddCodeBlock, - ] -); - -pub(crate) const MAX_TEXT_BLOCK_WIDTH: f32 = 9999.0; -pub(crate) const SMALL_SPACING_SIZE: f32 = 8.0; -pub(crate) const MEDIUM_SPACING_SIZE: f32 = 12.0; -pub(crate) const LARGE_SPACING_SIZE: f32 = 16.0; -pub(crate) const GUTTER_WIDTH: f32 = 19.0; -pub(crate) const CODE_BLOCK_INSET: f32 = MEDIUM_SPACING_SIZE; -pub(crate) const CONTROL_SIZE: f32 = 20.0; - -pub fn init(cx: &mut App) { - if cx.has_flag::() || std::env::var("LOCAL_NOTEBOOK_DEV").is_ok() { - workspace::register_project_item::(cx); - } - - cx.observe_flag::({ - move |is_enabled, cx| { - if is_enabled { - workspace::register_project_item::(cx); - } else { - // todo: there is no way to unregister a project item, so if the feature flag - // gets turned off they need to restart Zed. - } - } - }) - .detach(); -} - -pub struct NotebookEditor { - languages: Arc, - project: Entity, - - focus_handle: FocusHandle, - notebook_item: Entity, - - remote_id: Option, - cell_list: ListState, - - selected_cell_index: usize, - cell_order: Vec, - cell_map: HashMap, -} - -impl NotebookEditor { - pub fn new( - project: Entity, - notebook_item: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let focus_handle = cx.focus_handle(); - - let languages = project.read(cx).languages().clone(); - let language_name = notebook_item.read(cx).language_name(); - - let notebook_language = notebook_item.read(cx).notebook_language(); - let notebook_language = cx - .spawn_in(window, async move |_, _| notebook_language.await) - .shared(); - - let mut cell_order = vec![]; // Vec - let mut cell_map = HashMap::default(); // HashMap - - for (index, cell) in notebook_item - .read(cx) - .notebook - .clone() - .cells - .iter() - .enumerate() - { - let cell_id = cell.id(); - cell_order.push(cell_id.clone()); - cell_map.insert( - cell_id.clone(), - Cell::load(cell, &languages, notebook_language.clone(), window, cx), - ); - } - - let notebook_handle = cx.entity().downgrade(); - let cell_count = cell_order.len(); - - let this = cx.entity(); - let cell_list = ListState::new(cell_count, gpui::ListAlignment::Top, px(1000.)); - - Self { - project, - languages: languages.clone(), - focus_handle, - notebook_item, - remote_id: None, - cell_list, - selected_cell_index: 0, - cell_order: cell_order.clone(), - cell_map: cell_map.clone(), - } - } - - fn has_outputs(&self, window: &mut Window, cx: &mut Context) -> bool { - self.cell_map.values().any(|cell| { - if let Cell::Code(code_cell) = cell { - code_cell.read(cx).has_outputs() - } else { - false - } - }) - } - - fn clear_outputs(&mut self, window: &mut Window, cx: &mut Context) { - for cell in self.cell_map.values() { - if let Cell::Code(code_cell) = cell { - code_cell.update(cx, |cell, _cx| { - cell.clear_outputs(); - }); - } - } - } - - fn run_cells(&mut self, window: &mut Window, cx: &mut Context) { - println!("Cells would all run here, if that was implemented!"); - } - - fn open_notebook(&mut self, _: &OpenNotebook, _window: &mut Window, _cx: &mut Context) { - println!("Open notebook triggered"); - } - - fn move_cell_up(&mut self, window: &mut Window, cx: &mut Context) { - println!("Move cell up triggered"); - } - - fn move_cell_down(&mut self, window: &mut Window, cx: &mut Context) { - println!("Move cell down triggered"); - } - - fn add_markdown_block(&mut self, window: &mut Window, cx: &mut Context) { - println!("Add markdown block triggered"); - } - - fn add_code_block(&mut self, window: &mut Window, cx: &mut Context) { - println!("Add code block triggered"); - } - - fn cell_count(&self) -> usize { - self.cell_map.len() - } - - fn selected_index(&self) -> usize { - self.selected_cell_index - } - - pub fn set_selected_index( - &mut self, - index: usize, - jump_to_index: bool, - window: &mut Window, - cx: &mut Context, - ) { - // let previous_index = self.selected_cell_index; - self.selected_cell_index = index; - let current_index = self.selected_cell_index; - - // in the future we may have some `on_cell_change` event that we want to fire here - - if jump_to_index { - self.jump_to_cell(current_index, window, cx); - } - } - - pub fn select_next( - &mut self, - _: &menu::SelectNext, - window: &mut Window, - cx: &mut Context, - ) { - let count = self.cell_count(); - if count > 0 { - let index = self.selected_index(); - let ix = if index == count - 1 { - count - 1 - } else { - index + 1 - }; - self.set_selected_index(ix, true, window, cx); - cx.notify(); - } - } - - pub fn select_previous( - &mut self, - _: &menu::SelectPrevious, - window: &mut Window, - cx: &mut Context, - ) { - let count = self.cell_count(); - if count > 0 { - let index = self.selected_index(); - let ix = if index == 0 { 0 } else { index - 1 }; - self.set_selected_index(ix, true, window, cx); - cx.notify(); - } - } - - pub fn select_first( - &mut self, - _: &menu::SelectFirst, - window: &mut Window, - cx: &mut Context, - ) { - let count = self.cell_count(); - if count > 0 { - self.set_selected_index(0, true, window, cx); - cx.notify(); - } - } - - pub fn select_last( - &mut self, - _: &menu::SelectLast, - window: &mut Window, - cx: &mut Context, - ) { - let count = self.cell_count(); - if count > 0 { - self.set_selected_index(count - 1, true, window, cx); - cx.notify(); - } - } - - fn jump_to_cell(&mut self, index: usize, _window: &mut Window, _cx: &mut Context) { - self.cell_list.scroll_to_reveal_item(index); - } - - fn button_group(window: &mut Window, cx: &mut Context) -> Div { - v_flex() - .gap(DynamicSpacing::Base04.rems(cx)) - .items_center() - .w(px(CONTROL_SIZE + 4.0)) - .overflow_hidden() - .rounded(px(5.)) - .bg(cx.theme().colors().title_bar_background) - .p_px() - .border_1() - .border_color(cx.theme().colors().border) - } - - fn render_notebook_control( - id: impl Into, - icon: IconName, - _window: &mut Window, - _cx: &mut Context, - ) -> IconButton { - let id: ElementId = ElementId::Name(id.into()); - IconButton::new(id, icon).width(px(CONTROL_SIZE)) - } - - fn render_notebook_controls( - &self, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let has_outputs = self.has_outputs(window, cx); - - v_flex() - .max_w(px(CONTROL_SIZE + 4.0)) - .items_center() - .gap(DynamicSpacing::Base16.rems(cx)) - .justify_between() - .flex_none() - .h_full() - .py(DynamicSpacing::Base12.px(cx)) - .child( - v_flex() - .gap(DynamicSpacing::Base08.rems(cx)) - .child( - Self::button_group(window, cx) - .child( - Self::render_notebook_control( - "run-all-cells", - IconName::PlayFilled, - window, - cx, - ) - .tooltip(move |window, cx| { - Tooltip::for_action("Execute all cells", &RunAll, cx) - }) - .on_click(|_, window, cx| { - window.dispatch_action(Box::new(RunAll), cx); - }), - ) - .child( - Self::render_notebook_control( - "clear-all-outputs", - IconName::ListX, - window, - cx, - ) - .disabled(!has_outputs) - .tooltip(move |window, cx| { - Tooltip::for_action("Clear all outputs", &ClearOutputs, cx) - }) - .on_click(|_, window, cx| { - window.dispatch_action(Box::new(ClearOutputs), cx); - }), - ), - ) - .child( - Self::button_group(window, cx) - .child( - Self::render_notebook_control( - "move-cell-up", - IconName::ArrowUp, - window, - cx, - ) - .tooltip(move |window, cx| { - Tooltip::for_action("Move cell up", &MoveCellUp, cx) - }) - .on_click(|_, window, cx| { - window.dispatch_action(Box::new(MoveCellUp), cx); - }), - ) - .child( - Self::render_notebook_control( - "move-cell-down", - IconName::ArrowDown, - window, - cx, - ) - .tooltip(move |window, cx| { - Tooltip::for_action("Move cell down", &MoveCellDown, cx) - }) - .on_click(|_, window, cx| { - window.dispatch_action(Box::new(MoveCellDown), cx); - }), - ), - ) - .child( - Self::button_group(window, cx) - .child( - Self::render_notebook_control( - "new-markdown-cell", - IconName::Plus, - window, - cx, - ) - .tooltip(move |window, cx| { - Tooltip::for_action("Add markdown block", &AddMarkdownBlock, cx) - }) - .on_click(|_, window, cx| { - window.dispatch_action(Box::new(AddMarkdownBlock), cx); - }), - ) - .child( - Self::render_notebook_control( - "new-code-cell", - IconName::Code, - window, - cx, - ) - .tooltip(move |window, cx| { - Tooltip::for_action("Add code block", &AddCodeBlock, cx) - }) - .on_click(|_, window, cx| { - window.dispatch_action(Box::new(AddCodeBlock), cx); - }), - ), - ), - ) - .child( - v_flex() - .gap(DynamicSpacing::Base08.rems(cx)) - .items_center() - .child(Self::render_notebook_control( - "more-menu", - IconName::Ellipsis, - window, - cx, - )) - .child( - Self::button_group(window, cx) - .child(IconButton::new("repl", IconName::ReplNeutral)), - ), - ) - } - - fn cell_position(&self, index: usize) -> CellPosition { - match index { - 0 => CellPosition::First, - index if index == self.cell_count() - 1 => CellPosition::Last, - _ => CellPosition::Middle, - } - } - - fn render_cell( - &self, - index: usize, - cell: &Cell, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let cell_position = self.cell_position(index); - - let is_selected = index == self.selected_cell_index; - - match cell { - Cell::Code(cell) => { - cell.update(cx, |cell, _cx| { - cell.set_selected(is_selected) - .set_cell_position(cell_position); - }); - cell.clone().into_any_element() - } - Cell::Markdown(cell) => { - cell.update(cx, |cell, _cx| { - cell.set_selected(is_selected) - .set_cell_position(cell_position); - }); - cell.clone().into_any_element() - } - Cell::Raw(cell) => { - cell.update(cx, |cell, _cx| { - cell.set_selected(is_selected) - .set_cell_position(cell_position); - }); - cell.clone().into_any_element() - } - } - } -} - -impl Render for NotebookEditor { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - div() - .key_context("notebook") - .track_focus(&self.focus_handle) - .on_action(cx.listener(|this, &OpenNotebook, window, cx| { - this.open_notebook(&OpenNotebook, window, cx) - })) - .on_action( - cx.listener(|this, &ClearOutputs, window, cx| this.clear_outputs(window, cx)), - ) - .on_action(cx.listener(|this, &RunAll, window, cx| this.run_cells(window, cx))) - .on_action(cx.listener(|this, &MoveCellUp, window, cx| this.move_cell_up(window, cx))) - .on_action( - cx.listener(|this, &MoveCellDown, window, cx| this.move_cell_down(window, cx)), - ) - .on_action(cx.listener(|this, &AddMarkdownBlock, window, cx| { - this.add_markdown_block(window, cx) - })) - .on_action( - cx.listener(|this, &AddCodeBlock, window, cx| this.add_code_block(window, cx)), - ) - .on_action(cx.listener(Self::select_next)) - .on_action(cx.listener(Self::select_previous)) - .on_action(cx.listener(Self::select_first)) - .on_action(cx.listener(Self::select_last)) - .flex() - .items_start() - .size_full() - .overflow_hidden() - .px(DynamicSpacing::Base12.px(cx)) - .gap(DynamicSpacing::Base12.px(cx)) - .bg(cx.theme().colors().tab_bar_background) - .child( - v_flex() - .id("notebook-cells") - .flex_1() - .size_full() - .overflow_y_scroll() - .child(list( - self.cell_list.clone(), - cx.processor(|this, ix, window, cx| { - this.cell_order - .get(ix) - .and_then(|cell_id| this.cell_map.get(cell_id)) - .map(|cell| { - this.render_cell(ix, cell, window, cx).into_any_element() - }) - .unwrap_or_else(|| div().into_any()) - }), - )) - .size_full(), - ) - .child(self.render_notebook_controls(window, cx)) - } -} - -impl Focusable for NotebookEditor { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -// Intended to be a NotebookBuffer -pub struct NotebookItem { - path: PathBuf, - project_path: ProjectPath, - languages: Arc, - // Raw notebook data - notebook: nbformat::v4::Notebook, - // Store our version of the notebook in memory (cell_order, cell_map) - id: ProjectEntryId, -} - -impl project::ProjectItem for NotebookItem { - fn try_open( - project: &Entity, - path: &ProjectPath, - cx: &mut App, - ) -> Option>>> { - let path = path.clone(); - let project = project.clone(); - let fs = project.read(cx).fs().clone(); - let languages = project.read(cx).languages().clone(); - - if path.path.extension().unwrap_or_default() == "ipynb" { - Some(cx.spawn(async move |cx| { - let abs_path = project - .read_with(cx, |project, cx| project.absolute_path(&path, cx))? - .with_context(|| format!("finding the absolute path of {path:?}"))?; - - // todo: watch for changes to the file - let file_content = fs.load(abs_path.as_path()).await?; - let notebook = nbformat::parse_notebook(&file_content); - - let notebook = match notebook { - Ok(nbformat::Notebook::V4(notebook)) => notebook, - // 4.1 - 4.4 are converted to 4.5 - Ok(nbformat::Notebook::Legacy(legacy_notebook)) => { - // TODO: Decide if we want to mutate the notebook by including Cell IDs - // and any other conversions - - nbformat::upgrade_legacy_notebook(legacy_notebook)? - } - // Bad notebooks and notebooks v4.0 and below are not supported - Err(e) => { - anyhow::bail!("Failed to parse notebook: {:?}", e); - } - }; - - let id = project - .update(cx, |project, cx| { - project.entry_for_path(&path, cx).map(|entry| entry.id) - })? - .context("Entry not found")?; - - cx.new(|_| NotebookItem { - path: abs_path, - project_path: path, - languages, - notebook, - id, - }) - })) - } else { - None - } - } - - fn entry_id(&self, _: &App) -> Option { - Some(self.id) - } - - fn project_path(&self, _: &App) -> Option { - Some(self.project_path.clone()) - } - - fn is_dirty(&self) -> bool { - false - } -} - -impl NotebookItem { - pub fn language_name(&self) -> Option { - self.notebook - .metadata - .language_info - .as_ref() - .map(|l| l.name.clone()) - .or(self - .notebook - .metadata - .kernelspec - .as_ref() - .and_then(|spec| spec.language.clone())) - } - - pub fn notebook_language(&self) -> impl Future>> + use<> { - let language_name = self.language_name(); - let languages = self.languages.clone(); - - async move { - if let Some(language_name) = language_name { - languages.language_for_name(&language_name).await.ok() - } else { - None - } - } - } -} - -impl EventEmitter<()> for NotebookEditor {} - -// pub struct NotebookControls { -// pane_focused: bool, -// active_item: Option>, -// // subscription: Option, -// } - -// impl NotebookControls { -// pub fn new() -> Self { -// Self { -// pane_focused: false, -// active_item: Default::default(), -// // subscription: Default::default(), -// } -// } -// } - -// impl EventEmitter for NotebookControls {} - -// impl Render for NotebookControls { -// fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { -// div().child("notebook controls") -// } -// } - -// impl ToolbarItemView for NotebookControls { -// fn set_active_pane_item( -// &mut self, -// active_pane_item: Option<&dyn workspace::ItemHandle>, -// window: &mut Window, cx: &mut Context, -// ) -> workspace::ToolbarItemLocation { -// cx.notify(); -// self.active_item = None; - -// let Some(item) = active_pane_item else { -// return ToolbarItemLocation::Hidden; -// }; - -// ToolbarItemLocation::PrimaryLeft -// } - -// fn pane_focus_update(&mut self, pane_focused: bool, _window: &mut Window, _cx: &mut Context) { -// self.pane_focused = pane_focused; -// } -// } - -impl Item for NotebookEditor { - type Event = (); - - fn can_split(&self) -> bool { - true - } - - fn clone_on_split( - &self, - _workspace_id: Option, - window: &mut Window, - cx: &mut Context, - ) -> Task>> - where - Self: Sized, - { - Task::ready(Some(cx.new(|cx| { - Self::new(self.project.clone(), self.notebook_item.clone(), window, cx) - }))) - } - - fn buffer_kind(&self, _: &App) -> workspace::item::ItemBufferKind { - workspace::item::ItemBufferKind::Singleton - } - - fn for_each_project_item( - &self, - cx: &App, - f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem), - ) { - f(self.notebook_item.entity_id(), self.notebook_item.read(cx)) - } - - fn tab_content(&self, params: TabContentParams, window: &Window, cx: &App) -> AnyElement { - Label::new(self.tab_content_text(params.detail.unwrap_or(0), cx)) - .single_line() - .color(params.text_color()) - .when(params.preview, |this| this.italic()) - .into_any_element() - } - - fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString { - let path = &self.notebook_item.read(cx).path; - let title = path - .file_name() - .unwrap_or_else(|| path.as_os_str()) - .to_string_lossy() - .to_string(); - title.into() - } - - fn tab_icon(&self, _window: &Window, _cx: &App) -> Option { - Some(IconName::Book.into()) - } - - fn show_toolbar(&self) -> bool { - false - } - - // TODO - fn pixel_position_of_cursor(&self, _: &App) -> Option> { - None - } - - // TODO - fn as_searchable(&self, _: &Entity, _: &App) -> Option> { - None - } - - fn set_nav_history( - &mut self, - _: workspace::ItemNavHistory, - _window: &mut Window, - _: &mut Context, - ) { - // TODO - } - - // TODO - fn can_save(&self, _cx: &App) -> bool { - false - } - // TODO - fn save( - &mut self, - _options: SaveOptions, - _project: Entity, - _window: &mut Window, - _cx: &mut Context, - ) -> Task> { - unimplemented!("save() must be implemented if can_save() returns true") - } - - // TODO - fn save_as( - &mut self, - _project: Entity, - _path: ProjectPath, - _window: &mut Window, - _cx: &mut Context, - ) -> Task> { - unimplemented!("save_as() must be implemented if can_save() returns true") - } - // TODO - fn reload( - &mut self, - _project: Entity, - _window: &mut Window, - _cx: &mut Context, - ) -> Task> { - unimplemented!("reload() must be implemented if can_save() returns true") - } - - fn is_dirty(&self, cx: &App) -> bool { - self.cell_map.values().any(|cell| { - if let Cell::Code(code_cell) = cell { - code_cell.read(cx).is_dirty(cx) - } else { - false - } - }) - } -} - -// TODO: Implement this to allow us to persist to the database, etc: -// impl SerializableItem for NotebookEditor {} - -impl ProjectItem for NotebookEditor { - type Item = NotebookItem; - - fn for_project_item( - project: Entity, - _: Option<&Pane>, - item: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Self - where - Self: Sized, - { - Self::new(project, item, window, cx) - } -} diff --git a/crates/repl/src/outputs.rs b/crates/repl/src/outputs.rs deleted file mode 100644 index b99562393a..0000000000 --- a/crates/repl/src/outputs.rs +++ /dev/null @@ -1,617 +0,0 @@ -//! # REPL Output Module -//! -//! This module provides the core functionality for handling and displaying -//! various types of output from Jupyter kernels. -//! -//! ## Key Components -//! -//! - `OutputContent`: An enum that encapsulates different types of output content. -//! - `ExecutionView`: Manages the display of outputs for a single execution. -//! - `ExecutionStatus`: Represents the current status of an execution. -//! -//! ## Output Types -//! -//! The module supports several output types, including: -//! - Plain text -//! - Markdown -//! - Images (PNG and JPEG) -//! - Tables -//! - Error messages -//! -//! ## Clipboard Support -//! -//! Most output types implement the `SupportsClipboard` trait, allowing -//! users to easily copy output content to the system clipboard. -//! -//! ## Rendering -//! -//! The module provides rendering capabilities for each output type, -//! ensuring proper display within the REPL interface. -//! -//! ## Jupyter Integration -//! -//! This module is designed to work with Jupyter message protocols, -//! interpreting and displaying various types of Jupyter output. - -use editor::{Editor, MultiBuffer}; -use gpui::{AnyElement, ClipboardItem, Entity, Render, WeakEntity}; -use language::Buffer; -use runtimelib::{ExecutionState, JupyterMessageContent, MimeBundle, MimeType}; -use ui::{ - ButtonStyle, CommonAnimationExt, Context, IconButton, IconName, IntoElement, Styled, Tooltip, - Window, div, h_flex, prelude::*, v_flex, -}; - -mod image; -use image::ImageView; - -mod markdown; -use markdown::MarkdownView; - -mod table; -use table::TableView; - -pub mod plain; -use plain::TerminalOutput; - -pub(crate) mod user_error; -use user_error::ErrorView; -use workspace::Workspace; - -/// When deciding what to render from a collection of mediatypes, we need to rank them in order of importance -fn rank_mime_type(mimetype: &MimeType) -> usize { - match mimetype { - MimeType::DataTable(_) => 6, - MimeType::Png(_) => 4, - MimeType::Jpeg(_) => 3, - MimeType::Markdown(_) => 2, - MimeType::Plain(_) => 1, - // All other media types are not supported in Zed at this time - _ => 0, - } -} - -pub(crate) trait OutputContent { - fn clipboard_content(&self, window: &Window, cx: &App) -> Option; - fn has_clipboard_content(&self, _window: &Window, _cx: &App) -> bool { - false - } - fn has_buffer_content(&self, _window: &Window, _cx: &App) -> bool { - false - } - fn buffer_content(&mut self, _window: &mut Window, _cx: &mut App) -> Option> { - None - } -} - -impl OutputContent for Entity { - fn clipboard_content(&self, window: &Window, cx: &App) -> Option { - self.read(cx).clipboard_content(window, cx) - } - - fn has_clipboard_content(&self, window: &Window, cx: &App) -> bool { - self.read(cx).has_clipboard_content(window, cx) - } - - fn has_buffer_content(&self, window: &Window, cx: &App) -> bool { - self.read(cx).has_buffer_content(window, cx) - } - - fn buffer_content(&mut self, window: &mut Window, cx: &mut App) -> Option> { - self.update(cx, |item, cx| item.buffer_content(window, cx)) - } -} - -pub enum Output { - Plain { - content: Entity, - display_id: Option, - }, - Stream { - content: Entity, - }, - Image { - content: Entity, - display_id: Option, - }, - ErrorOutput(ErrorView), - Message(String), - Table { - content: Entity, - display_id: Option, - }, - Markdown { - content: Entity, - display_id: Option, - }, - ClearOutputWaitMarker, -} - -impl Output { - fn render_output_controls( - v: Entity, - workspace: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) -> Option { - if !v.has_clipboard_content(window, cx) && !v.has_buffer_content(window, cx) { - return None; - } - - Some( - h_flex() - .pl_1() - .when(v.has_clipboard_content(window, cx), |el| { - let v = v.clone(); - el.child( - IconButton::new(ElementId::Name("copy-output".into()), IconName::Copy) - .style(ButtonStyle::Transparent) - .tooltip(Tooltip::text("Copy Output")) - .on_click(move |_, window, cx| { - let clipboard_content = v.clipboard_content(window, cx); - - if let Some(clipboard_content) = clipboard_content.as_ref() { - cx.write_to_clipboard(clipboard_content.clone()); - } - }), - ) - }) - .when(v.has_buffer_content(window, cx), |el| { - let v = v.clone(); - el.child( - IconButton::new( - ElementId::Name("open-in-buffer".into()), - IconName::FileTextOutlined, - ) - .style(ButtonStyle::Transparent) - .tooltip(Tooltip::text("Open in Buffer")) - .on_click({ - let workspace = workspace.clone(); - move |_, window, cx| { - let buffer_content = - v.update(cx, |item, cx| item.buffer_content(window, cx)); - - if let Some(buffer_content) = buffer_content.as_ref() { - let buffer = buffer_content.clone(); - let editor = Box::new(cx.new(|cx| { - let multibuffer = cx.new(|cx| { - let mut multi_buffer = - MultiBuffer::singleton(buffer.clone(), cx); - - multi_buffer.set_title("REPL Output".to_string(), cx); - multi_buffer - }); - - Editor::for_multibuffer(multibuffer, None, window, cx) - })); - workspace - .update(cx, |workspace, cx| { - workspace.add_item_to_active_pane( - editor, None, true, window, cx, - ); - }) - .ok(); - } - } - }), - ) - }) - .into_any_element(), - ) - } - - pub fn render( - &self, - workspace: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement + use<> { - let content = match self { - Self::Plain { content, .. } => Some(content.clone().into_any_element()), - Self::Markdown { content, .. } => Some(content.clone().into_any_element()), - Self::Stream { content, .. } => Some(content.clone().into_any_element()), - Self::Image { content, .. } => Some(content.clone().into_any_element()), - Self::Message(message) => Some(div().child(message.clone()).into_any_element()), - Self::Table { content, .. } => Some(content.clone().into_any_element()), - Self::ErrorOutput(error_view) => error_view.render(window, cx), - Self::ClearOutputWaitMarker => None, - }; - - h_flex() - .id("output-content") - .w_full() - .overflow_x_scroll() - .items_start() - .child(div().flex_1().children(content)) - .children(match self { - Self::Plain { content, .. } => { - Self::render_output_controls(content.clone(), workspace, window, cx) - } - Self::Markdown { content, .. } => { - Self::render_output_controls(content.clone(), workspace, window, cx) - } - Self::Stream { content, .. } => { - Self::render_output_controls(content.clone(), workspace, window, cx) - } - Self::Image { content, .. } => { - Self::render_output_controls(content.clone(), workspace, window, cx) - } - Self::ErrorOutput(err) => { - // Add buttons for the traceback section - Some( - h_flex() - .pl_1() - .child( - IconButton::new( - ElementId::Name("copy-full-error-traceback".into()), - IconName::Copy, - ) - .style(ButtonStyle::Transparent) - .tooltip(Tooltip::text("Copy Full Error")) - .on_click({ - let ename = err.ename.clone(); - let evalue = err.evalue.clone(); - let traceback = err.traceback.clone(); - move |_, _window, cx| { - let traceback_text = traceback.read(cx).full_text(); - let full_error = - format!("{}: {}\n{}", ename, evalue, traceback_text); - let clipboard_content = - ClipboardItem::new_string(full_error); - cx.write_to_clipboard(clipboard_content); - } - }), - ) - .child( - IconButton::new( - ElementId::Name("open-full-error-in-buffer-traceback".into()), - IconName::FileTextOutlined, - ) - .style(ButtonStyle::Transparent) - .tooltip(Tooltip::text("Open Full Error in Buffer")) - .on_click({ - let ename = err.ename.clone(); - let evalue = err.evalue.clone(); - let traceback = err.traceback.clone(); - move |_, window, cx| { - if let Some(workspace) = workspace.upgrade() { - let traceback_text = traceback.read(cx).full_text(); - let full_error = format!( - "{}: {}\n{}", - ename, evalue, traceback_text - ); - let buffer = cx.new(|cx| { - let mut buffer = Buffer::local(full_error, cx) - .with_language( - language::PLAIN_TEXT.clone(), - cx, - ); - buffer.set_capability( - language::Capability::ReadOnly, - cx, - ); - buffer - }); - let editor = Box::new(cx.new(|cx| { - let multibuffer = cx.new(|cx| { - let mut multi_buffer = - MultiBuffer::singleton(buffer.clone(), cx); - multi_buffer - .set_title("Full Error".to_string(), cx); - multi_buffer - }); - Editor::for_multibuffer( - multibuffer, - None, - window, - cx, - ) - })); - workspace.update(cx, |workspace, cx| { - workspace.add_item_to_active_pane( - editor, None, true, window, cx, - ); - }); - } - } - }), - ) - .into_any_element(), - ) - } - Self::Message(_) => None, - Self::Table { content, .. } => { - Self::render_output_controls(content.clone(), workspace, window, cx) - } - Self::ClearOutputWaitMarker => None, - }) - } - - pub fn display_id(&self) -> Option { - match self { - Output::Plain { display_id, .. } => display_id.clone(), - Output::Stream { .. } => None, - Output::Image { display_id, .. } => display_id.clone(), - Output::ErrorOutput(_) => None, - Output::Message(_) => None, - Output::Table { display_id, .. } => display_id.clone(), - Output::Markdown { display_id, .. } => display_id.clone(), - Output::ClearOutputWaitMarker => None, - } - } - - pub fn new( - data: &MimeBundle, - display_id: Option, - window: &mut Window, - cx: &mut App, - ) -> Self { - match data.richest(rank_mime_type) { - Some(MimeType::Plain(text)) => Output::Plain { - content: cx.new(|cx| TerminalOutput::from(text, window, cx)), - display_id, - }, - Some(MimeType::Markdown(text)) => { - let content = cx.new(|cx| MarkdownView::from(text.clone(), cx)); - Output::Markdown { - content, - display_id, - } - } - Some(MimeType::Png(data)) | Some(MimeType::Jpeg(data)) => match ImageView::from(data) { - Ok(view) => Output::Image { - content: cx.new(|_| view), - display_id, - }, - Err(error) => Output::Message(format!("Failed to load image: {}", error)), - }, - Some(MimeType::DataTable(data)) => Output::Table { - content: cx.new(|cx| TableView::new(data, window, cx)), - display_id, - }, - // Any other media types are not supported - _ => Output::Message("Unsupported media type".to_string()), - } - } -} - -#[derive(Default, Clone, Debug)] -pub enum ExecutionStatus { - #[default] - Unknown, - ConnectingToKernel, - Queued, - Executing, - Finished, - ShuttingDown, - Shutdown, - KernelErrored(String), - Restarting, -} - -/// An ExecutionView shows the outputs of an execution. -/// It can hold zero or more outputs, which the user -/// sees as "the output" for a single execution. -pub struct ExecutionView { - #[allow(unused)] - workspace: WeakEntity, - pub outputs: Vec, - pub status: ExecutionStatus, -} - -impl ExecutionView { - pub fn new( - status: ExecutionStatus, - workspace: WeakEntity, - _cx: &mut Context, - ) -> Self { - Self { - workspace, - outputs: Default::default(), - status, - } - } - - /// Accept a Jupyter message belonging to this execution - pub fn push_message( - &mut self, - message: &JupyterMessageContent, - window: &mut Window, - cx: &mut Context, - ) { - let output: Output = match message { - JupyterMessageContent::ExecuteResult(result) => Output::new( - &result.data, - result.transient.as_ref().and_then(|t| t.display_id.clone()), - window, - cx, - ), - JupyterMessageContent::DisplayData(result) => Output::new( - &result.data, - result.transient.as_ref().and_then(|t| t.display_id.clone()), - window, - cx, - ), - JupyterMessageContent::StreamContent(result) => { - // Previous stream data will combine together, handling colors, carriage returns, etc - if let Some(new_terminal) = self.apply_terminal_text(&result.text, window, cx) { - new_terminal - } else { - return; - } - } - JupyterMessageContent::ErrorOutput(result) => { - let terminal = - cx.new(|cx| TerminalOutput::from(&result.traceback.join("\n"), window, cx)); - - Output::ErrorOutput(ErrorView { - ename: result.ename.clone(), - evalue: result.evalue.clone(), - traceback: terminal, - }) - } - JupyterMessageContent::ExecuteReply(reply) => { - for payload in reply.payload.iter() { - if let runtimelib::Payload::Page { data, .. } = payload { - let output = Output::new(data, None, window, cx); - self.outputs.push(output); - } - } - cx.notify(); - return; - } - JupyterMessageContent::ClearOutput(options) => { - if !options.wait { - self.outputs.clear(); - cx.notify(); - return; - } - - // Create a marker to clear the output after we get in a new output - Output::ClearOutputWaitMarker - } - JupyterMessageContent::Status(status) => { - match status.execution_state { - ExecutionState::Busy => { - self.status = ExecutionStatus::Executing; - } - ExecutionState::Idle => self.status = ExecutionStatus::Finished, - ExecutionState::Unknown => self.status = ExecutionStatus::Unknown, - ExecutionState::Starting => self.status = ExecutionStatus::ConnectingToKernel, - ExecutionState::Restarting => self.status = ExecutionStatus::Restarting, - ExecutionState::Terminating => self.status = ExecutionStatus::ShuttingDown, - ExecutionState::AutoRestarting => self.status = ExecutionStatus::Restarting, - ExecutionState::Dead => self.status = ExecutionStatus::Shutdown, - ExecutionState::Other(_) => self.status = ExecutionStatus::Unknown, - } - cx.notify(); - return; - } - _msg => { - return; - } - }; - - // Check for a clear output marker as the previous output, so we can clear it out - if let Some(output) = self.outputs.last() - && let Output::ClearOutputWaitMarker = output - { - self.outputs.clear(); - } - - self.outputs.push(output); - - cx.notify(); - } - - pub fn update_display_data( - &mut self, - data: &MimeBundle, - display_id: &str, - window: &mut Window, - cx: &mut Context, - ) { - let mut any = false; - - self.outputs.iter_mut().for_each(|output| { - if let Some(other_display_id) = output.display_id().as_ref() - && other_display_id == display_id - { - *output = Output::new(data, Some(display_id.to_owned()), window, cx); - any = true; - } - }); - - if any { - cx.notify(); - } - } - - fn apply_terminal_text( - &mut self, - text: &str, - window: &mut Window, - cx: &mut Context, - ) -> Option { - if let Some(last_output) = self.outputs.last_mut() - && let Output::Stream { - content: last_stream, - } = last_output - { - // Don't need to add a new output, we already have a terminal output - // and can just update the most recent terminal output - last_stream.update(cx, |last_stream, cx| { - last_stream.append_text(text, cx); - cx.notify(); - }); - return None; - } - - Some(Output::Stream { - content: cx.new(|cx| TerminalOutput::from(text, window, cx)), - }) - } -} - -impl Render for ExecutionView { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let status = match &self.status { - ExecutionStatus::ConnectingToKernel => Label::new("Connecting to kernel...") - .color(Color::Muted) - .into_any_element(), - ExecutionStatus::Executing => h_flex() - .gap_2() - .child( - Icon::new(IconName::ArrowCircle) - .size(IconSize::Small) - .color(Color::Muted) - .with_rotate_animation(3), - ) - .child(Label::new("Executing...").color(Color::Muted)) - .into_any_element(), - ExecutionStatus::Finished => Icon::new(IconName::Check) - .size(IconSize::Small) - .into_any_element(), - ExecutionStatus::Unknown => Label::new("Unknown status") - .color(Color::Muted) - .into_any_element(), - ExecutionStatus::ShuttingDown => Label::new("Kernel shutting down...") - .color(Color::Muted) - .into_any_element(), - ExecutionStatus::Restarting => Label::new("Kernel restarting...") - .color(Color::Muted) - .into_any_element(), - ExecutionStatus::Shutdown => Label::new("Kernel shutdown") - .color(Color::Muted) - .into_any_element(), - ExecutionStatus::Queued => Label::new("Queued...") - .color(Color::Muted) - .into_any_element(), - ExecutionStatus::KernelErrored(error) => Label::new(format!("Kernel error: {}", error)) - .color(Color::Error) - .into_any_element(), - }; - - if self.outputs.is_empty() { - return v_flex() - .min_h(window.line_height()) - .justify_center() - .child(status) - .into_any_element(); - } - - div() - .w_full() - .children( - self.outputs - .iter() - .map(|output| output.render(self.workspace.clone(), window, cx)), - ) - .children(match self.status { - ExecutionStatus::Executing => vec![status], - ExecutionStatus::Queued => vec![status], - _ => vec![], - }) - .into_any_element() - } -} diff --git a/crates/repl/src/outputs/image.rs b/crates/repl/src/outputs/image.rs deleted file mode 100644 index fefdbec2fa..0000000000 --- a/crates/repl/src/outputs/image.rs +++ /dev/null @@ -1,98 +0,0 @@ -use anyhow::Result; -use base64::{ - Engine as _, alphabet, - engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig}, -}; -use gpui::{App, ClipboardItem, Image, ImageFormat, RenderImage, Window, img}; -use std::sync::Arc; -use ui::{IntoElement, Styled, div, prelude::*}; - -use crate::outputs::OutputContent; - -/// ImageView renders an image inline in an editor, adapting to the line height to fit the image. -pub struct ImageView { - clipboard_image: Arc, - height: u32, - width: u32, - image: Arc, -} - -pub const STANDARD_INDIFFERENT: GeneralPurpose = GeneralPurpose::new( - &alphabet::STANDARD, - GeneralPurposeConfig::new() - .with_encode_padding(false) - .with_decode_padding_mode(DecodePaddingMode::Indifferent), -); - -impl ImageView { - pub fn from(base64_encoded_data: &str) -> Result { - let filtered = - base64_encoded_data.replace(&[' ', '\n', '\t', '\r', '\x0b', '\x0c'][..], ""); - let bytes = STANDARD_INDIFFERENT.decode(filtered)?; - - let format = image::guess_format(&bytes)?; - - let mut data = image::load_from_memory_with_format(&bytes, format)?.into_rgba8(); - - // Convert from RGBA to BGRA. - for pixel in data.chunks_exact_mut(4) { - pixel.swap(0, 2); - } - - let height = data.height(); - let width = data.width(); - - let gpui_image_data = RenderImage::new(vec![image::Frame::new(data)]); - - let format = match format { - image::ImageFormat::Png => ImageFormat::Png, - image::ImageFormat::Jpeg => ImageFormat::Jpeg, - image::ImageFormat::Gif => ImageFormat::Gif, - image::ImageFormat::WebP => ImageFormat::Webp, - image::ImageFormat::Tiff => ImageFormat::Tiff, - image::ImageFormat::Bmp => ImageFormat::Bmp, - image::ImageFormat::Ico => ImageFormat::Ico, - format => { - anyhow::bail!("unsupported image format {format:?}"); - } - }; - - // Convert back to a GPUI image for use with the clipboard - let clipboard_image = Arc::new(Image::from_bytes(format, bytes)); - - Ok(ImageView { - clipboard_image, - height, - width, - image: Arc::new(gpui_image_data), - }) - } -} - -impl Render for ImageView { - fn render(&mut self, window: &mut Window, _: &mut Context) -> impl IntoElement { - let line_height = window.line_height(); - - let (height, width) = if self.height as f32 / f32::from(line_height) == u8::MAX as f32 { - let height = u8::MAX as f32 * line_height; - let width = self.width as f32 * height / self.height as f32; - (height, width) - } else { - (self.height.into(), self.width.into()) - }; - - let image = self.image.clone(); - - div().h(height).w(width).child(img(image)) - } -} - -impl OutputContent for ImageView { - fn clipboard_content(&self, _window: &Window, _cx: &App) -> Option { - Some(ClipboardItem::new_image(self.clipboard_image.as_ref())) - } - - fn has_clipboard_content(&self, _window: &Window, _cx: &App) -> bool { - true - } -} diff --git a/crates/repl/src/outputs/markdown.rs b/crates/repl/src/outputs/markdown.rs deleted file mode 100644 index bd88f4e159..0000000000 --- a/crates/repl/src/outputs/markdown.rs +++ /dev/null @@ -1,93 +0,0 @@ -use anyhow::Result; -use gpui::{ - App, ClipboardItem, Context, Entity, RetainAllImageCache, Task, Window, div, prelude::*, -}; -use language::Buffer; -use markdown_preview::{ - markdown_elements::ParsedMarkdown, markdown_parser::parse_markdown, - markdown_renderer::render_markdown_block, -}; -use ui::v_flex; - -use crate::outputs::OutputContent; - -pub struct MarkdownView { - raw_text: String, - image_cache: Entity, - contents: Option, - parsing_markdown_task: Option>>, -} - -impl MarkdownView { - pub fn from(text: String, cx: &mut Context) -> Self { - let parsed = { - let text = text.clone(); - cx.background_spawn(async move { parse_markdown(&text.clone(), None, None).await }) - }; - let task = cx.spawn(async move |markdown_view, cx| { - let content = parsed.await; - - markdown_view.update(cx, |markdown, cx| { - markdown.parsing_markdown_task.take(); - markdown.contents = Some(content); - cx.notify(); - }) - }); - - Self { - raw_text: text, - image_cache: RetainAllImageCache::new(cx), - contents: None, - parsing_markdown_task: Some(task), - } - } -} - -impl OutputContent for MarkdownView { - fn clipboard_content(&self, _window: &Window, _cx: &App) -> Option { - Some(ClipboardItem::new_string(self.raw_text.clone())) - } - - fn has_clipboard_content(&self, _window: &Window, _cx: &App) -> bool { - true - } - - fn has_buffer_content(&self, _window: &Window, _cx: &App) -> bool { - true - } - - fn buffer_content(&mut self, _: &mut Window, cx: &mut App) -> Option> { - let buffer = cx.new(|cx| { - // TODO: Bring in the language registry so we can set the language to markdown - let mut buffer = Buffer::local(self.raw_text.clone(), cx) - .with_language(language::PLAIN_TEXT.clone(), cx); - buffer.set_capability(language::Capability::ReadOnly, cx); - buffer - }); - Some(buffer) - } -} - -impl Render for MarkdownView { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let Some(parsed) = self.contents.as_ref() else { - return div().into_any_element(); - }; - - let mut markdown_render_context = - markdown_preview::markdown_renderer::RenderContext::new(None, window, cx); - - v_flex() - .image_cache(self.image_cache.clone()) - .gap_3() - .py_4() - .children(parsed.children.iter().map(|child| { - div().relative().child( - div() - .relative() - .child(render_markdown_block(child, &mut markdown_render_context)), - ) - })) - .into_any_element() - } -} diff --git a/crates/repl/src/outputs/plain.rs b/crates/repl/src/outputs/plain.rs deleted file mode 100644 index 54e4983b9f..0000000000 --- a/crates/repl/src/outputs/plain.rs +++ /dev/null @@ -1,351 +0,0 @@ -//! # Plain Text Output -//! -//! This module provides functionality for rendering plain text output in a terminal-like format. -//! It uses the Alacritty terminal emulator backend to process and display text, supporting -//! ANSI escape sequences for formatting, colors, and other terminal features. -//! -//! The main component of this module is the `TerminalOutput` struct, which handles the parsing -//! and rendering of text input, simulating a basic terminal environment within REPL output. -//! -//! This module is used for displaying: -//! -//! - Standard output (stdout) -//! - Standard error (stderr) -//! - Plain text content -//! - Error tracebacks -//! - -use alacritty_terminal::{ - event::VoidListener, - grid::Dimensions as _, - index::{Column, Line, Point}, - term::Config, - vte::ansi::Processor, -}; -use gpui::{Bounds, ClipboardItem, Entity, FontStyle, TextStyle, WhiteSpace, canvas, size}; -use language::Buffer; -use settings::Settings as _; -use terminal::terminal_settings::TerminalSettings; -use terminal_view::terminal_element::TerminalElement; -use theme::ThemeSettings; -use ui::{IntoElement, prelude::*}; - -use crate::outputs::OutputContent; -use crate::repl_settings::ReplSettings; - -/// The `TerminalOutput` struct handles the parsing and rendering of text input, -/// simulating a basic terminal environment within REPL output. -/// -/// `TerminalOutput` is designed to handle various types of text-based output, including: -/// -/// * stdout (standard output) -/// * stderr (standard error) -/// * text/plain content -/// * error tracebacks -/// -/// It uses the Alacritty terminal emulator backend to process and render text, -/// supporting ANSI escape sequences for text formatting and colors. -/// -pub struct TerminalOutput { - full_buffer: Option>, - /// ANSI escape sequence processor for parsing input text. - parser: Processor, - /// Alacritty terminal instance that manages the terminal state and content. - handler: alacritty_terminal::Term, -} - -/// Returns the default text style for the terminal output. -pub fn text_style(window: &mut Window, cx: &mut App) -> TextStyle { - let settings = ThemeSettings::get_global(cx).clone(); - - let font_size = settings.buffer_font_size(cx).into(); - let font_family = settings.buffer_font.family; - let font_features = settings.buffer_font.features; - let font_weight = settings.buffer_font.weight; - let font_fallbacks = settings.buffer_font.fallbacks; - - let theme = cx.theme(); - - TextStyle { - font_family, - font_features, - font_weight, - font_fallbacks, - font_size, - font_style: FontStyle::Normal, - line_height: window.line_height().into(), - background_color: Some(theme.colors().terminal_ansi_background), - white_space: WhiteSpace::Normal, - // These are going to be overridden per-cell - color: theme.colors().terminal_foreground, - ..Default::default() - } -} - -/// Returns the default terminal size for the terminal output. -pub fn terminal_size(window: &mut Window, cx: &mut App) -> terminal::TerminalBounds { - let text_style = text_style(window, cx); - let text_system = window.text_system(); - - let line_height = window.line_height(); - - let font_pixels = text_style.font_size.to_pixels(window.rem_size()); - let font_id = text_system.resolve_font(&text_style.font()); - - let cell_width = text_system - .advance(font_id, font_pixels, 'w') - .unwrap() - .width; - - let num_lines = ReplSettings::get_global(cx).max_lines; - let columns = ReplSettings::get_global(cx).max_columns; - - // Reversed math from terminal::TerminalSize to get pixel width according to terminal width - let width = columns as f32 * cell_width; - let height = num_lines as f32 * window.line_height(); - - terminal::TerminalBounds { - cell_width, - line_height, - bounds: Bounds { - origin: gpui::Point::default(), - size: size(width, height), - }, - } -} - -impl TerminalOutput { - /// Creates a new `TerminalOutput` instance. - /// - /// This method initializes a new terminal emulator with default configuration - /// and sets up the necessary components for handling terminal events and rendering. - /// - pub fn new(window: &mut Window, cx: &mut App) -> Self { - let term = alacritty_terminal::Term::new( - Config::default(), - &terminal_size(window, cx), - VoidListener, - ); - - Self { - parser: Processor::new(), - handler: term, - full_buffer: None, - } - } - - /// Creates a new `TerminalOutput` instance with initial content. - /// - /// Initializes a new terminal output and populates it with the provided text. - /// - /// # Arguments - /// - /// * `text` - A string slice containing the initial text for the terminal output. - /// * `cx` - A mutable reference to the `WindowContext` for initialization. - /// - /// # Returns - /// - /// A new instance of `TerminalOutput` containing the provided text. - pub fn from(text: &str, window: &mut Window, cx: &mut App) -> Self { - let mut output = Self::new(window, cx); - output.append_text(text, cx); - output - } - - /// Appends text to the terminal output. - /// - /// Processes each byte of the input text, handling newline characters specially - /// to ensure proper cursor movement. Uses the ANSI parser to process the input - /// and update the terminal state. - /// - /// As an example, if the user runs the following Python code in this REPL: - /// - /// ```python - /// import time - /// print("Hello,", end="") - /// time.sleep(1) - /// print(" world!") - /// ``` - /// - /// Then append_text will be called twice, with the following arguments: - /// - /// ```ignore - /// terminal_output.append_text("Hello,"); - /// terminal_output.append_text(" world!"); - /// ``` - /// Resulting in a single output of "Hello, world!". - /// - /// # Arguments - /// - /// * `text` - A string slice containing the text to be appended. - pub fn append_text(&mut self, text: &str, cx: &mut App) { - for byte in text.as_bytes() { - if *byte == b'\n' { - // Dirty (?) hack to move the cursor down - self.parser.advance(&mut self.handler, &[b'\r']); - self.parser.advance(&mut self.handler, &[b'\n']); - } else { - self.parser.advance(&mut self.handler, &[*byte]); - } - } - - // This will keep the buffer up to date, though with some terminal codes it won't be perfect - if let Some(buffer) = self.full_buffer.as_ref() { - buffer.update(cx, |buffer, cx| { - buffer.edit([(buffer.len()..buffer.len(), text)], None, cx); - }); - } - } - - pub fn full_text(&self) -> String { - fn sanitize(mut line: String) -> Option { - line.retain(|ch| ch != '\u{0}' && ch != '\r'); - if line.trim().is_empty() { - return None; - } - let trimmed = line.trim_end_matches([' ', '\t']); - Some(trimmed.to_owned()) - } - - let mut lines = Vec::new(); - - // Get the total number of lines, including history - let total_lines = self.handler.grid().total_lines(); - let visible_lines = self.handler.screen_lines(); - let history_lines = total_lines - visible_lines; - - // Capture history lines in correct order (oldest to newest) - for line in (0..history_lines).rev() { - let line_index = Line(-(line as i32) - 1); - let start = Point::new(line_index, Column(0)); - let end = Point::new(line_index, Column(self.handler.columns() - 1)); - if let Some(cleaned) = sanitize(self.handler.bounds_to_string(start, end)) { - lines.push(cleaned); - } - } - - // Capture visible lines - for line in 0..visible_lines { - let line_index = Line(line as i32); - let start = Point::new(line_index, Column(0)); - let end = Point::new(line_index, Column(self.handler.columns() - 1)); - if let Some(cleaned) = sanitize(self.handler.bounds_to_string(start, end)) { - lines.push(cleaned); - } - } - - if lines.is_empty() { - String::new() - } else { - let mut full_text = lines.join("\n"); - full_text.push('\n'); - full_text - } - } -} - -impl Render for TerminalOutput { - /// Renders the terminal output as a GPUI element. - /// - /// Converts the current terminal state into a renderable GPUI element. It handles - /// the layout of the terminal grid, calculates the dimensions of the output, and - /// creates a canvas element that paints the terminal cells and background rectangles. - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let text_style = text_style(window, cx); - let text_system = window.text_system(); - - let grid = self - .handler - .renderable_content() - .display_iter - .map(|ic| terminal::IndexedCell { - point: ic.point, - cell: ic.cell.clone(), - }); - let minimum_contrast = TerminalSettings::get_global(cx).minimum_contrast; - let (rects, batched_text_runs) = - TerminalElement::layout_grid(grid, 0, &text_style, None, minimum_contrast, cx); - - // lines are 0-indexed, so we must add 1 to get the number of lines - let text_line_height = text_style.line_height_in_pixels(window.rem_size()); - let num_lines = batched_text_runs - .iter() - .map(|b| b.start_point.line) - .max() - .unwrap_or(0) - + 1; - let height = num_lines as f32 * text_line_height; - - let font_pixels = text_style.font_size.to_pixels(window.rem_size()); - let font_id = text_system.resolve_font(&text_style.font()); - - let cell_width = text_system - .advance(font_id, font_pixels, 'w') - .map(|advance| advance.width) - .unwrap_or(Pixels::ZERO); - - canvas( - // prepaint - move |_bounds, _, _| {}, - // paint - move |bounds, _, window, cx| { - for rect in rects { - rect.paint( - bounds.origin, - &terminal::TerminalBounds { - cell_width, - line_height: text_line_height, - bounds, - }, - window, - ); - } - - for batch in batched_text_runs { - batch.paint( - bounds.origin, - &terminal::TerminalBounds { - cell_width, - line_height: text_line_height, - bounds, - }, - window, - cx, - ); - } - }, - ) - // We must set the height explicitly for the editor block to size itself correctly - .h(height) - } -} - -impl OutputContent for TerminalOutput { - fn clipboard_content(&self, _window: &Window, _cx: &App) -> Option { - Some(ClipboardItem::new_string(self.full_text())) - } - - fn has_clipboard_content(&self, _window: &Window, _cx: &App) -> bool { - true - } - - fn has_buffer_content(&self, _window: &Window, _cx: &App) -> bool { - true - } - - fn buffer_content(&mut self, _: &mut Window, cx: &mut App) -> Option> { - if self.full_buffer.as_ref().is_some() { - return self.full_buffer.clone(); - } - - let buffer = cx.new(|cx| { - let mut buffer = - Buffer::local(self.full_text(), cx).with_language(language::PLAIN_TEXT.clone(), cx); - buffer.set_capability(language::Capability::ReadOnly, cx); - buffer - }); - - self.full_buffer = Some(buffer.clone()); - Some(buffer) - } -} diff --git a/crates/repl/src/outputs/table.rs b/crates/repl/src/outputs/table.rs deleted file mode 100644 index f6bf30f394..0000000000 --- a/crates/repl/src/outputs/table.rs +++ /dev/null @@ -1,290 +0,0 @@ -//! # Table Output for REPL -//! -//! This module provides functionality to render tabular data in Zed's REPL output. -//! -//! It supports the [Frictionless Data Table Schema](https://specs.frictionlessdata.io/table-schema/) -//! for data interchange, implemented by Pandas in Python and Polars for Deno. -//! -//! # Python Example -//! -//! Tables can be created and displayed in two main ways: -//! -//! 1. Using raw JSON data conforming to the Tabular Data Resource specification. -//! 2. Using Pandas DataFrames (in Python kernels). -//! -//! ## Raw JSON Method -//! -//! To create a table using raw JSON, you need to provide a JSON object that conforms -//! to the Tabular Data Resource specification. Here's an example: -//! -//! ```json -//! { -//! "schema": { -//! "fields": [ -//! {"name": "id", "type": "integer"}, -//! {"name": "name", "type": "string"}, -//! {"name": "age", "type": "integer"} -//! ] -//! }, -//! "data": [ -//! {"id": 1, "name": "Alice", "age": 30}, -//! {"id": 2, "name": "Bob", "age": 28}, -//! {"id": 3, "name": "Charlie", "age": 35} -//! ] -//! } -//! ``` -//! -//! ## Pandas Method -//! -//! To create a table using Pandas in a Python kernel, you can use the following steps: -//! -//! ```python -//! import pandas as pd -//! -//! # Enable table schema output -//! pd.set_option('display.html.table_schema', True) -//! -//! # Create a DataFrame -//! df = pd.DataFrame({ -//! 'id': [1, 2, 3], -//! 'name': ['Alice', 'Bob', 'Charlie'], -//! 'age': [30, 28, 35] -//! }) -//! -//! # Display the DataFrame -//! display(df) -//! ``` -use gpui::{AnyElement, ClipboardItem, TextRun}; -use runtimelib::datatable::TableSchema; -use runtimelib::media::datatable::TabularDataResource; -use serde_json::Value; -use settings::Settings; -use theme::ThemeSettings; -use ui::{IntoElement, Styled, div, prelude::*, v_flex}; -use util::markdown::MarkdownEscaped; - -use crate::outputs::OutputContent; - -/// TableView renders a static table inline in a buffer. -/// -/// It uses the -/// specification for data interchange. -pub struct TableView { - pub table: TabularDataResource, - pub widths: Vec, - cached_clipboard_content: ClipboardItem, -} - -fn cell_content(row: &Value, field: &str) -> String { - match row.get(field) { - Some(Value::String(s)) => s.clone(), - Some(Value::Number(n)) => n.to_string(), - Some(Value::Bool(b)) => b.to_string(), - Some(Value::Array(arr)) => format!("{:?}", arr), - Some(Value::Object(obj)) => format!("{:?}", obj), - Some(Value::Null) | None => String::new(), - } -} - -// Declare constant for the padding multiple on the line height -const TABLE_Y_PADDING_MULTIPLE: f32 = 0.5; - -impl TableView { - pub fn new(table: &TabularDataResource, window: &mut Window, cx: &mut App) -> Self { - let mut widths = Vec::with_capacity(table.schema.fields.len()); - - let text_system = window.text_system(); - let text_style = window.text_style(); - let text_font = ThemeSettings::get_global(cx).buffer_font.clone(); - let font_size = ThemeSettings::get_global(cx).buffer_font_size(cx); - let mut runs = [TextRun { - len: 0, - font: text_font, - color: text_style.color, - ..Default::default() - }]; - - for field in table.schema.fields.iter() { - runs[0].len = field.name.len(); - let mut width = text_system - .layout_line(&field.name, font_size, &runs, None) - .width; - - let Some(data) = table.data.as_ref() else { - widths.push(width); - continue; - }; - - for row in data { - let content = cell_content(row, &field.name); - runs[0].len = content.len(); - let cell_width = window - .text_system() - .layout_line(&content, font_size, &runs, None) - .width; - - width = width.max(cell_width) - } - - widths.push(width) - } - - let cached_clipboard_content = Self::create_clipboard_content(table); - - Self { - table: table.clone(), - widths, - cached_clipboard_content: ClipboardItem::new_string(cached_clipboard_content), - } - } - - fn create_clipboard_content(table: &TabularDataResource) -> String { - let data = match table.data.as_ref() { - Some(data) => data, - None => &Vec::new(), - }; - let schema = table.schema.clone(); - - let mut markdown = format!( - "| {} |\n", - table - .schema - .fields - .iter() - .map(|field| field.name.clone()) - .collect::>() - .join(" | ") - ); - - markdown.push_str("|---"); - for _ in 1..table.schema.fields.len() { - markdown.push_str("|---"); - } - markdown.push_str("|\n"); - - let body = data - .iter() - .map(|record: &Value| { - let row_content = schema - .fields - .iter() - .map(|field| MarkdownEscaped(&cell_content(record, &field.name)).to_string()) - .collect::>(); - - row_content.join(" | ") - }) - .collect::>(); - - for row in body { - markdown.push_str(&format!("| {} |\n", row)); - } - - markdown - } - - pub fn render_row( - &self, - schema: &TableSchema, - is_header: bool, - row: &Value, - window: &mut Window, - cx: &mut App, - ) -> AnyElement { - let theme = cx.theme(); - - let line_height = window.line_height(); - - let row_cells = schema - .fields - .iter() - .zip(self.widths.iter()) - .map(|(field, width)| { - let container = match field.field_type { - runtimelib::datatable::FieldType::String => div(), - - runtimelib::datatable::FieldType::Number - | runtimelib::datatable::FieldType::Integer - | runtimelib::datatable::FieldType::Date - | runtimelib::datatable::FieldType::Time - | runtimelib::datatable::FieldType::Datetime - | runtimelib::datatable::FieldType::Year - | runtimelib::datatable::FieldType::Duration - | runtimelib::datatable::FieldType::Yearmonth => v_flex().items_end(), - - _ => div(), - }; - - let value = cell_content(row, &field.name); - - let mut cell = container - .min_w(*width + px(22.)) - .w(*width + px(22.)) - .child(value) - .px_2() - .py((TABLE_Y_PADDING_MULTIPLE / 2.0) * line_height) - .border_color(theme.colors().border); - - if is_header { - cell = cell.border_1().bg(theme.colors().border_focused) - } else { - cell = cell.border_1() - } - cell - }) - .collect::>(); - - let mut total_width = px(0.); - for width in self.widths.iter() { - // Width fudge factor: border + 2 (heading), padding - total_width += *width + px(22.); - } - - h_flex() - .w(total_width) - .children(row_cells) - .into_any_element() - } -} - -impl Render for TableView { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let data = match &self.table.data { - Some(data) => data, - None => return div().into_any_element(), - }; - - let mut headings = serde_json::Map::new(); - for field in &self.table.schema.fields { - headings.insert(field.name.clone(), Value::String(field.name.clone())); - } - let header = self.render_row( - &self.table.schema, - true, - &Value::Object(headings), - window, - cx, - ); - - let body = data - .iter() - .map(|row| self.render_row(&self.table.schema, false, row, window, cx)); - - v_flex() - .id("table") - .overflow_x_scroll() - .w_full() - .child(header) - .children(body) - .into_any_element() - } -} - -impl OutputContent for TableView { - fn clipboard_content(&self, _window: &Window, _cx: &App) -> Option { - Some(self.cached_clipboard_content.clone()) - } - - fn has_clipboard_content(&self, _window: &Window, _cx: &App) -> bool { - true - } -} diff --git a/crates/repl/src/outputs/user_error.rs b/crates/repl/src/outputs/user_error.rs deleted file mode 100644 index 4218b417c5..0000000000 --- a/crates/repl/src/outputs/user_error.rs +++ /dev/null @@ -1,45 +0,0 @@ -use gpui::{AnyElement, App, Entity, FontWeight, Window}; -use ui::{Label, h_flex, prelude::*, v_flex}; - -use crate::outputs::plain::TerminalOutput; - -/// Userspace error from the kernel -#[derive(Clone)] -pub struct ErrorView { - pub ename: String, - pub evalue: String, - pub traceback: Entity, -} - -impl ErrorView { - pub fn render(&self, window: &mut Window, cx: &mut App) -> Option { - let theme = cx.theme(); - - let padding = window.line_height() / 2.; - - Some( - v_flex() - .gap_3() - .child( - h_flex() - .font_buffer(cx) - .child( - Label::new(format!("{}: ", self.ename.clone())) - .color(Color::Error) - .weight(FontWeight::BOLD), - ) - .child(Label::new(self.evalue.clone()).weight(FontWeight::BOLD)), - ) - .child( - div() - .w_full() - .px(padding) - .py(padding) - .border_l_1() - .border_color(theme.status().error_border) - .child(self.traceback.clone()), - ) - .into_any_element(), - ) - } -} diff --git a/crates/repl/src/repl.rs b/crates/repl/src/repl.rs deleted file mode 100644 index 346cca0211..0000000000 --- a/crates/repl/src/repl.rs +++ /dev/null @@ -1,61 +0,0 @@ -pub mod components; -mod jupyter_settings; -pub mod kernels; -pub mod notebook; -mod outputs; -mod repl_editor; -mod repl_sessions_ui; -mod repl_settings; -mod repl_store; -mod session; - -use std::{sync::Arc, time::Duration}; - -use async_dispatcher::{Dispatcher, Runnable, set_dispatcher}; -use gpui::{App, PlatformDispatcher, Priority, RunnableVariant}; -use project::Fs; -pub use runtimelib::ExecutionState; - -pub use crate::jupyter_settings::JupyterSettings; -pub use crate::kernels::{Kernel, KernelSpecification, KernelStatus}; -pub use crate::repl_editor::*; -pub use crate::repl_sessions_ui::{ - ClearOutputs, Interrupt, ReplSessionsPage, Restart, Run, Sessions, Shutdown, -}; -pub use crate::repl_settings::ReplSettings; -use crate::repl_store::ReplStore; -pub use crate::session::Session; - -pub const KERNEL_DOCS_URL: &str = "https://zed.dev/docs/repl#changing-kernels"; - -pub fn init(fs: Arc, cx: &mut App) { - set_dispatcher(zed_dispatcher(cx)); - repl_sessions_ui::init(cx); - ReplStore::init(fs, cx); -} - -fn zed_dispatcher(cx: &mut App) -> impl Dispatcher { - struct ZedDispatcher { - dispatcher: Arc, - } - - // PlatformDispatcher is _super_ close to the same interface we put in - // async-dispatcher, except for the task label in dispatch. Later we should - // just make that consistent so we have this dispatcher ready to go for - // other crates in Zed. - impl Dispatcher for ZedDispatcher { - fn dispatch(&self, runnable: Runnable) { - self.dispatcher - .dispatch(RunnableVariant::Compat(runnable), None, Priority::default()); - } - - fn dispatch_after(&self, duration: Duration, runnable: Runnable) { - self.dispatcher - .dispatch_after(duration, RunnableVariant::Compat(runnable)); - } - } - - ZedDispatcher { - dispatcher: cx.background_executor().dispatcher.clone(), - } -} diff --git a/crates/repl/src/repl_editor.rs b/crates/repl/src/repl_editor.rs deleted file mode 100644 index 9e52637ab7..0000000000 --- a/crates/repl/src/repl_editor.rs +++ /dev/null @@ -1,824 +0,0 @@ -//! REPL operations on an [`Editor`]. - -use std::ops::Range; -use std::sync::Arc; - -use anyhow::{Context as _, Result}; -use editor::{Editor, MultiBufferOffset}; -use gpui::{App, Entity, WeakEntity, Window, prelude::*}; -use language::{BufferSnapshot, Language, LanguageName, Point}; -use project::{ProjectItem as _, WorktreeId}; - -use crate::repl_store::ReplStore; -use crate::session::SessionEvent; -use crate::{ - ClearOutputs, Interrupt, JupyterSettings, KernelSpecification, Restart, Session, Shutdown, -}; - -pub fn assign_kernelspec( - kernel_specification: KernelSpecification, - weak_editor: WeakEntity, - window: &mut Window, - cx: &mut App, -) -> Result<()> { - let store = ReplStore::global(cx); - if !store.read(cx).is_enabled() { - return Ok(()); - } - - let worktree_id = crate::repl_editor::worktree_id_for_editor(weak_editor.clone(), cx) - .context("editor is not in a worktree")?; - - store.update(cx, |store, cx| { - store.set_active_kernelspec(worktree_id, kernel_specification.clone(), cx); - }); - - let fs = store.read(cx).fs().clone(); - - if let Some(session) = store.read(cx).get_session(weak_editor.entity_id()).cloned() { - // Drop previous session, start new one - session.update(cx, |session, cx| { - session.clear_outputs(cx); - session.shutdown(window, cx); - cx.notify(); - }); - } - - let session = - cx.new(|cx| Session::new(weak_editor.clone(), fs, kernel_specification, window, cx)); - - weak_editor - .update(cx, |_editor, cx| { - cx.notify(); - - cx.subscribe(&session, { - let store = store.clone(); - move |_this, _session, event, cx| match event { - SessionEvent::Shutdown(shutdown_event) => { - store.update(cx, |store, _cx| { - store.remove_session(shutdown_event.entity_id()); - }); - } - } - }) - .detach(); - }) - .ok(); - - store.update(cx, |store, _cx| { - store.insert_session(weak_editor.entity_id(), session.clone()); - }); - - Ok(()) -} - -pub fn run( - editor: WeakEntity, - move_down: bool, - window: &mut Window, - cx: &mut App, -) -> Result<()> { - let store = ReplStore::global(cx); - if !store.read(cx).is_enabled() { - return Ok(()); - } - - let editor = editor.upgrade().context("editor was dropped")?; - let selected_range = editor - .update(cx, |editor, cx| { - editor - .selections - .newest_adjusted(&editor.display_snapshot(cx)) - }) - .range(); - let multibuffer = editor.read(cx).buffer().clone(); - let Some(buffer) = multibuffer.read(cx).as_singleton() else { - return Ok(()); - }; - - let Some(project_path) = buffer.read(cx).project_path(cx) else { - return Ok(()); - }; - - let (runnable_ranges, next_cell_point) = - runnable_ranges(&buffer.read(cx).snapshot(), selected_range, cx); - - for runnable_range in runnable_ranges { - let Some(language) = multibuffer.read(cx).language_at(runnable_range.start, cx) else { - continue; - }; - - let kernel_specification = store - .read(cx) - .active_kernelspec(project_path.worktree_id, Some(language.clone()), cx) - .with_context(|| format!("No kernel found for language: {}", language.name()))?; - - let fs = store.read(cx).fs().clone(); - - let session = if let Some(session) = store.read(cx).get_session(editor.entity_id()).cloned() - { - session - } else { - let weak_editor = editor.downgrade(); - let session = - cx.new(|cx| Session::new(weak_editor, fs, kernel_specification, window, cx)); - - editor.update(cx, |_editor, cx| { - cx.notify(); - - cx.subscribe(&session, { - let store = store.clone(); - move |_this, _session, event, cx| match event { - SessionEvent::Shutdown(shutdown_event) => { - store.update(cx, |store, _cx| { - store.remove_session(shutdown_event.entity_id()); - }); - } - } - }) - .detach(); - }); - - store.update(cx, |store, _cx| { - store.insert_session(editor.entity_id(), session.clone()); - }); - - session - }; - - let selected_text; - let anchor_range; - let next_cursor; - { - let snapshot = multibuffer.read(cx).read(cx); - selected_text = snapshot - .text_for_range(runnable_range.clone()) - .collect::(); - anchor_range = snapshot.anchor_before(runnable_range.start) - ..snapshot.anchor_after(runnable_range.end); - next_cursor = next_cell_point.map(|point| snapshot.anchor_after(point)); - } - - session.update(cx, |session, cx| { - session.execute( - selected_text, - anchor_range, - next_cursor, - move_down, - window, - cx, - ); - }); - } - - anyhow::Ok(()) -} - -pub enum SessionSupport { - ActiveSession(Entity), - Inactive(KernelSpecification), - RequiresSetup(LanguageName), - Unsupported, -} - -pub fn worktree_id_for_editor(editor: WeakEntity, cx: &mut App) -> Option { - editor.upgrade().and_then(|editor| { - editor - .read(cx) - .buffer() - .read(cx) - .as_singleton()? - .read(cx) - .project_path(cx) - .map(|path| path.worktree_id) - }) -} - -pub fn session(editor: WeakEntity, cx: &mut App) -> SessionSupport { - let store = ReplStore::global(cx); - let entity_id = editor.entity_id(); - - if let Some(session) = store.read(cx).get_session(entity_id).cloned() { - return SessionSupport::ActiveSession(session); - }; - - let Some(language) = get_language(editor.clone(), cx) else { - return SessionSupport::Unsupported; - }; - - let worktree_id = worktree_id_for_editor(editor, cx); - - let Some(worktree_id) = worktree_id else { - return SessionSupport::Unsupported; - }; - - let kernelspec = store - .read(cx) - .active_kernelspec(worktree_id, Some(language.clone()), cx); - - match kernelspec { - Some(kernelspec) => SessionSupport::Inactive(kernelspec), - None => { - // For language_supported, need to check available kernels for language - if language_supported(&language, cx) { - SessionSupport::RequiresSetup(language.name()) - } else { - SessionSupport::Unsupported - } - } - } -} - -pub fn clear_outputs(editor: WeakEntity, cx: &mut App) { - let store = ReplStore::global(cx); - let entity_id = editor.entity_id(); - let Some(session) = store.read(cx).get_session(entity_id).cloned() else { - return; - }; - session.update(cx, |session, cx| { - session.clear_outputs(cx); - cx.notify(); - }); -} - -pub fn interrupt(editor: WeakEntity, cx: &mut App) { - let store = ReplStore::global(cx); - let entity_id = editor.entity_id(); - let Some(session) = store.read(cx).get_session(entity_id).cloned() else { - return; - }; - - session.update(cx, |session, cx| { - session.interrupt(cx); - cx.notify(); - }); -} - -pub fn shutdown(editor: WeakEntity, window: &mut Window, cx: &mut App) { - let store = ReplStore::global(cx); - let entity_id = editor.entity_id(); - let Some(session) = store.read(cx).get_session(entity_id).cloned() else { - return; - }; - - session.update(cx, |session, cx| { - session.shutdown(window, cx); - cx.notify(); - }); -} - -pub fn restart(editor: WeakEntity, window: &mut Window, cx: &mut App) { - let Some(editor) = editor.upgrade() else { - return; - }; - - let entity_id = editor.entity_id(); - - let Some(session) = ReplStore::global(cx) - .read(cx) - .get_session(entity_id) - .cloned() - else { - return; - }; - - session.update(cx, |session, cx| { - session.restart(window, cx); - cx.notify(); - }); -} - -pub fn setup_editor_session_actions(editor: &mut Editor, editor_handle: WeakEntity) { - editor - .register_action({ - let editor_handle = editor_handle.clone(); - move |_: &ClearOutputs, _, cx| { - if !JupyterSettings::enabled(cx) { - return; - } - - crate::clear_outputs(editor_handle.clone(), cx); - } - }) - .detach(); - - editor - .register_action({ - let editor_handle = editor_handle.clone(); - move |_: &Interrupt, _, cx| { - if !JupyterSettings::enabled(cx) { - return; - } - - crate::interrupt(editor_handle.clone(), cx); - } - }) - .detach(); - - editor - .register_action({ - let editor_handle = editor_handle.clone(); - move |_: &Shutdown, window, cx| { - if !JupyterSettings::enabled(cx) { - return; - } - - crate::shutdown(editor_handle.clone(), window, cx); - } - }) - .detach(); - - editor - .register_action({ - let editor_handle = editor_handle; - move |_: &Restart, window, cx| { - if !JupyterSettings::enabled(cx) { - return; - } - - crate::restart(editor_handle.clone(), window, cx); - } - }) - .detach(); -} - -fn cell_range(buffer: &BufferSnapshot, start_row: u32, end_row: u32) -> Range { - let mut snippet_end_row = end_row; - while buffer.is_line_blank(snippet_end_row) && snippet_end_row > start_row { - snippet_end_row -= 1; - } - Point::new(start_row, 0)..Point::new(snippet_end_row, buffer.line_len(snippet_end_row)) -} - -// Returns the ranges of the snippets in the buffer and the next point for moving the cursor to -fn jupytext_cells( - buffer: &BufferSnapshot, - range: Range, -) -> (Vec>, Option) { - let mut current_row = range.start.row; - - let Some(language) = buffer.language() else { - return (Vec::new(), None); - }; - - let default_scope = language.default_scope(); - let comment_prefixes = default_scope.line_comment_prefixes(); - if comment_prefixes.is_empty() { - return (Vec::new(), None); - } - - let jupytext_prefixes = comment_prefixes - .iter() - .map(|comment_prefix| format!("{comment_prefix}%%")) - .collect::>(); - - let mut snippet_start_row = None; - loop { - if jupytext_prefixes - .iter() - .any(|prefix| buffer.contains_str_at(Point::new(current_row, 0), prefix)) - { - snippet_start_row = Some(current_row); - break; - } else if current_row > 0 { - current_row -= 1; - } else { - break; - } - } - - let mut snippets = Vec::new(); - if let Some(mut snippet_start_row) = snippet_start_row { - for current_row in range.start.row + 1..=buffer.max_point().row { - if jupytext_prefixes - .iter() - .any(|prefix| buffer.contains_str_at(Point::new(current_row, 0), prefix)) - { - snippets.push(cell_range(buffer, snippet_start_row, current_row - 1)); - - if current_row <= range.end.row { - snippet_start_row = current_row; - } else { - // Return our snippets as well as the next point for moving the cursor to - return (snippets, Some(Point::new(current_row, 0))); - } - } - } - - // Go to the end of the buffer (no more jupytext cells found) - snippets.push(cell_range( - buffer, - snippet_start_row, - buffer.max_point().row, - )); - } - - (snippets, None) -} - -fn runnable_ranges( - buffer: &BufferSnapshot, - range: Range, - cx: &mut App, -) -> (Vec>, Option) { - if let Some(language) = buffer.language() - && language.name() == "Markdown".into() - { - return (markdown_code_blocks(buffer, range, cx), None); - } - - let (jupytext_snippets, next_cursor) = jupytext_cells(buffer, range.clone()); - if !jupytext_snippets.is_empty() { - return (jupytext_snippets, next_cursor); - } - - let snippet_range = cell_range(buffer, range.start.row, range.end.row); - let start_language = buffer.language_at(snippet_range.start); - let end_language = buffer.language_at(snippet_range.end); - - if start_language - .zip(end_language) - .is_some_and(|(start, end)| start == end) - { - (vec![snippet_range], None) - } else { - (Vec::new(), None) - } -} - -// We allow markdown code blocks to end in a trailing newline in order to render the output -// below the final code fence. This is different than our behavior for selections and Jupytext cells. -fn markdown_code_blocks( - buffer: &BufferSnapshot, - range: Range, - cx: &mut App, -) -> Vec> { - buffer - .injections_intersecting_range(range) - .filter(|(_, language)| language_supported(language, cx)) - .map(|(content_range, _)| { - buffer.offset_to_point(content_range.start)..buffer.offset_to_point(content_range.end) - }) - .collect() -} - -fn language_supported(language: &Arc, cx: &mut App) -> bool { - let store = ReplStore::global(cx); - let store_read = store.read(cx); - - // Since we're just checking for general language support, we only need to look at - // the pure Jupyter kernels - these are all the globally available ones - store_read.pure_jupyter_kernel_specifications().any(|spec| { - // Convert to lowercase for case-insensitive comparison since kernels might report "python" while our language is "Python" - spec.language().as_ref().to_lowercase() == language.name().as_ref().to_lowercase() - }) -} - -fn get_language(editor: WeakEntity, cx: &mut App) -> Option> { - editor - .update(cx, |editor, cx| { - let display_snapshot = editor.display_snapshot(cx); - let selection = editor - .selections - .newest::(&display_snapshot); - display_snapshot - .buffer_snapshot() - .language_at(selection.head()) - .cloned() - }) - .ok() - .flatten() -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::App; - use indoc::indoc; - use language::{Buffer, Language, LanguageConfig, LanguageRegistry}; - - #[gpui::test] - fn test_snippet_ranges(cx: &mut App) { - // Create a test language - let test_language = Arc::new(Language::new( - LanguageConfig { - name: "TestLang".into(), - line_comments: vec!["# ".into()], - ..Default::default() - }, - None, - )); - - let buffer = cx.new(|cx| { - Buffer::local( - indoc! { r#" - print(1 + 1) - print(2 + 2) - - print(4 + 4) - - - "# }, - cx, - ) - .with_language(test_language, cx) - }); - let snapshot = buffer.read(cx).snapshot(); - - // Single-point selection - let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 4)..Point::new(0, 4), cx); - let snippets = snippets - .into_iter() - .map(|range| snapshot.text_for_range(range).collect::()) - .collect::>(); - assert_eq!(snippets, vec!["print(1 + 1)"]); - - // Multi-line selection - let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(2, 0), cx); - let snippets = snippets - .into_iter() - .map(|range| snapshot.text_for_range(range).collect::()) - .collect::>(); - assert_eq!( - snippets, - vec![indoc! { r#" - print(1 + 1) - print(2 + 2)"# }] - ); - - // Trimming multiple trailing blank lines - let (snippets, _) = runnable_ranges(&snapshot, Point::new(0, 5)..Point::new(5, 0), cx); - - let snippets = snippets - .into_iter() - .map(|range| snapshot.text_for_range(range).collect::()) - .collect::>(); - assert_eq!( - snippets, - vec![indoc! { r#" - print(1 + 1) - print(2 + 2) - - print(4 + 4)"# }] - ); - } - - #[gpui::test] - fn test_jupytext_snippet_ranges(cx: &mut App) { - // Create a test language - let test_language = Arc::new(Language::new( - LanguageConfig { - name: "TestLang".into(), - line_comments: vec!["# ".into()], - ..Default::default() - }, - None, - )); - - let buffer = cx.new(|cx| { - Buffer::local( - indoc! { r#" - # Hello! - # %% [markdown] - # This is some arithmetic - print(1 + 1) - print(2 + 2) - - # %% - print(3 + 3) - print(4 + 4) - - print(5 + 5) - - - - "# }, - cx, - ) - .with_language(test_language, cx) - }); - let snapshot = buffer.read(cx).snapshot(); - - // Jupytext snippet surrounding an empty selection - let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(2, 5), cx); - - let snippets = snippets - .into_iter() - .map(|range| snapshot.text_for_range(range).collect::()) - .collect::>(); - assert_eq!( - snippets, - vec![indoc! { r#" - # %% [markdown] - # This is some arithmetic - print(1 + 1) - print(2 + 2)"# }] - ); - - // Jupytext snippets intersecting a non-empty selection - let (snippets, _) = runnable_ranges(&snapshot, Point::new(2, 5)..Point::new(6, 2), cx); - let snippets = snippets - .into_iter() - .map(|range| snapshot.text_for_range(range).collect::()) - .collect::>(); - assert_eq!( - snippets, - vec![ - indoc! { r#" - # %% [markdown] - # This is some arithmetic - print(1 + 1) - print(2 + 2)"# - }, - indoc! { r#" - # %% - print(3 + 3) - print(4 + 4) - - print(5 + 5)"# - } - ] - ); - } - - #[gpui::test] - fn test_markdown_code_blocks(cx: &mut App) { - use crate::kernels::LocalKernelSpecification; - use jupyter_protocol::JupyterKernelspec; - - // Initialize settings - settings::init(cx); - editor::init(cx); - - // Initialize the ReplStore with a fake filesystem - let fs = Arc::new(project::RealFs::new(None, cx.background_executor().clone())); - ReplStore::init(fs, cx); - - // Add mock kernel specifications for TypeScript and Python - let store = ReplStore::global(cx); - store.update(cx, |store, cx| { - let typescript_spec = KernelSpecification::Jupyter(LocalKernelSpecification { - name: "typescript".into(), - kernelspec: JupyterKernelspec { - argv: vec![], - display_name: "TypeScript".into(), - language: "typescript".into(), - interrupt_mode: None, - metadata: None, - env: None, - }, - path: std::path::PathBuf::new(), - }); - - let python_spec = KernelSpecification::Jupyter(LocalKernelSpecification { - name: "python".into(), - kernelspec: JupyterKernelspec { - argv: vec![], - display_name: "Python".into(), - language: "python".into(), - interrupt_mode: None, - metadata: None, - env: None, - }, - path: std::path::PathBuf::new(), - }); - - store.set_kernel_specs_for_testing(vec![typescript_spec, python_spec], cx); - }); - - let markdown = languages::language("markdown", tree_sitter_md::LANGUAGE.into()); - let typescript = languages::language( - "typescript", - tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into(), - ); - let python = languages::language("python", tree_sitter_python::LANGUAGE.into()); - let language_registry = Arc::new(LanguageRegistry::new(cx.background_executor().clone())); - language_registry.add(markdown.clone()); - language_registry.add(typescript); - language_registry.add(python); - - // Two code blocks intersecting with selection - let buffer = cx.new(|cx| { - let mut buffer = Buffer::local( - indoc! { r#" - Hey this is Markdown! - - ```typescript - let foo = 999; - console.log(foo + 1999); - ``` - - ```typescript - console.log("foo") - ``` - "# - }, - cx, - ); - buffer.set_language_registry(language_registry.clone()); - buffer.set_language(Some(markdown.clone()), cx); - buffer - }); - let snapshot = buffer.read(cx).snapshot(); - - let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(8, 5), cx); - let snippets = snippets - .into_iter() - .map(|range| snapshot.text_for_range(range).collect::()) - .collect::>(); - - assert_eq!( - snippets, - vec![ - indoc! { r#" - let foo = 999; - console.log(foo + 1999); - "# - }, - "console.log(\"foo\")\n" - ] - ); - - // Three code blocks intersecting with selection - let buffer = cx.new(|cx| { - let mut buffer = Buffer::local( - indoc! { r#" - Hey this is Markdown! - - ```typescript - let foo = 999; - console.log(foo + 1999); - ``` - - ```ts - console.log("foo") - ``` - - ```typescript - console.log("another code block") - ``` - "# }, - cx, - ); - buffer.set_language_registry(language_registry.clone()); - buffer.set_language(Some(markdown.clone()), cx); - buffer - }); - let snapshot = buffer.read(cx).snapshot(); - - let (snippets, _) = runnable_ranges(&snapshot, Point::new(3, 5)..Point::new(12, 5), cx); - let snippets = snippets - .into_iter() - .map(|range| snapshot.text_for_range(range).collect::()) - .collect::>(); - - assert_eq!( - snippets, - vec![ - indoc! { r#" - let foo = 999; - console.log(foo + 1999); - "# - }, - "console.log(\"foo\")\n", - "console.log(\"another code block\")\n", - ] - ); - - // Python code block - let buffer = cx.new(|cx| { - let mut buffer = Buffer::local( - indoc! { r#" - Hey this is Markdown! - - ```python - print("hello there") - print("hello there") - print("hello there") - ``` - "# }, - cx, - ); - buffer.set_language_registry(language_registry.clone()); - buffer.set_language(Some(markdown.clone()), cx); - buffer - }); - let snapshot = buffer.read(cx).snapshot(); - - let (snippets, _) = runnable_ranges(&snapshot, Point::new(4, 5)..Point::new(5, 5), cx); - let snippets = snippets - .into_iter() - .map(|range| snapshot.text_for_range(range).collect::()) - .collect::>(); - - assert_eq!( - snippets, - vec![indoc! { r#" - print("hello there") - print("hello there") - print("hello there") - "# - },] - ); - } -} diff --git a/crates/repl/src/repl_sessions_ui.rs b/crates/repl/src/repl_sessions_ui.rs deleted file mode 100644 index d8bd8869f2..0000000000 --- a/crates/repl/src/repl_sessions_ui.rs +++ /dev/null @@ -1,282 +0,0 @@ -use editor::Editor; -use gpui::{ - AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Subscription, actions, - prelude::*, -}; -use project::ProjectItem as _; -use ui::{ButtonLike, ElevationIndex, KeyBinding, prelude::*}; -use util::ResultExt as _; -use workspace::item::ItemEvent; -use workspace::{Workspace, item::Item}; - -use crate::jupyter_settings::JupyterSettings; -use crate::repl_store::ReplStore; - -actions!( - repl, - [ - /// Runs the current cell and advances to the next one. - Run, - /// Runs the current cell without advancing. - RunInPlace, - /// Clears all outputs in the REPL. - ClearOutputs, - /// Opens the REPL sessions panel. - Sessions, - /// Interrupts the currently running kernel. - Interrupt, - /// Shuts down the current kernel. - Shutdown, - /// Restarts the current kernel. - Restart, - /// Refreshes the list of available kernelspecs. - RefreshKernelspecs - ] -); - -pub fn init(cx: &mut App) { - cx.observe_new( - |workspace: &mut Workspace, _window, _cx: &mut Context| { - workspace.register_action(|workspace, _: &Sessions, window, cx| { - let existing = workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::()); - - if let Some(existing) = existing { - workspace.activate_item(&existing, true, true, window, cx); - } else { - let repl_sessions_page = ReplSessionsPage::new(window, cx); - workspace.add_item_to_active_pane( - Box::new(repl_sessions_page), - None, - true, - window, - cx, - ) - } - }); - - workspace.register_action(|_workspace, _: &RefreshKernelspecs, _, cx| { - let store = ReplStore::global(cx); - store.update(cx, |store, cx| { - store.refresh_kernelspecs(cx).detach(); - }); - }); - }, - ) - .detach(); - - cx.observe_new( - move |editor: &mut Editor, window, cx: &mut Context| { - let Some(window) = window else { - return; - }; - - if !editor.use_modal_editing() || !editor.buffer().read(cx).is_singleton() { - return; - } - - cx.defer_in(window, |editor, window, cx| { - let workspace = Workspace::for_window(window, cx); - let project = workspace.map(|workspace| workspace.read(cx).project().clone()); - - let is_local_project = project - .as_ref() - .map(|project| project.read(cx).is_local()) - .unwrap_or(false); - - if !is_local_project { - return; - } - - let buffer = editor.buffer().read(cx).as_singleton(); - - let language = buffer - .as_ref() - .and_then(|buffer| buffer.read(cx).language()); - - let project_path = buffer.and_then(|buffer| buffer.read(cx).project_path(cx)); - - let editor_handle = cx.entity().downgrade(); - - if let Some(language) = language - && language.name() == "Python".into() - && let (Some(project_path), Some(project)) = (project_path, project) - { - let store = ReplStore::global(cx); - store.update(cx, |store, cx| { - store - .refresh_python_kernelspecs(project_path.worktree_id, &project, cx) - .detach_and_log_err(cx); - }); - } - - editor - .register_action({ - let editor_handle = editor_handle.clone(); - move |_: &Run, window, cx| { - if !JupyterSettings::enabled(cx) { - return; - } - - crate::run(editor_handle.clone(), true, window, cx).log_err(); - } - }) - .detach(); - - editor - .register_action({ - move |_: &RunInPlace, window, cx| { - if !JupyterSettings::enabled(cx) { - return; - } - - crate::run(editor_handle.clone(), false, window, cx).log_err(); - } - }) - .detach(); - }); - }, - ) - .detach(); -} - -pub struct ReplSessionsPage { - focus_handle: FocusHandle, - _subscriptions: Vec, -} - -impl ReplSessionsPage { - pub fn new(window: &mut Window, cx: &mut Context) -> Entity { - cx.new(|cx| { - let focus_handle = cx.focus_handle(); - - let subscriptions = vec![ - cx.on_focus_in(&focus_handle, window, |_this, _window, cx| cx.notify()), - cx.on_focus_out(&focus_handle, window, |_this, _event, _window, cx| { - cx.notify() - }), - ]; - - Self { - focus_handle, - _subscriptions: subscriptions, - } - }) - } -} - -impl EventEmitter for ReplSessionsPage {} - -impl Focusable for ReplSessionsPage { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Item for ReplSessionsPage { - type Event = ItemEvent; - - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { - "REPL Sessions".into() - } - - fn telemetry_event_text(&self) -> Option<&'static str> { - Some("REPL Session Started") - } - - fn show_toolbar(&self) -> bool { - false - } - - fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) { - f(*event) - } -} - -impl Render for ReplSessionsPage { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let store = ReplStore::global(cx); - - let (kernel_specifications, sessions) = store.update(cx, |store, _cx| { - ( - store - .pure_jupyter_kernel_specifications() - .cloned() - .collect::>(), - store.sessions().cloned().collect::>(), - ) - }); - - // When there are no kernel specifications, show a link to the Zed docs explaining how to - // install kernels. It can be assumed they don't have a running kernel if we have no - // specifications. - if kernel_specifications.is_empty() { - let instructions = "To start interactively running code in your editor, you need to install and configure Jupyter kernels."; - - return ReplSessionsContainer::new("No Jupyter Kernels Available") - .child(Label::new(instructions)) - .child( - h_flex().w_full().p_4().justify_center().gap_2().child( - ButtonLike::new("install-kernels") - .style(ButtonStyle::Filled) - .size(ButtonSize::Large) - .layer(ElevationIndex::ModalSurface) - .child(Label::new("Install Kernels")) - .on_click(move |_, _, cx| { - cx.open_url( - "https://zed.dev/docs/repl#language-specific-instructions", - ) - }), - ), - ); - } - - // When there are no sessions, show the command to run code in an editor - if sessions.is_empty() { - let instructions = "To run code in a Jupyter kernel, select some code and use the 'repl::Run' command."; - - return ReplSessionsContainer::new("No Jupyter Kernel Sessions").child( - v_flex() - .child(Label::new(instructions)) - .child(KeyBinding::for_action(&Run, cx)), - ); - } - - ReplSessionsContainer::new("Jupyter Kernel Sessions").children(sessions) - } -} - -#[derive(IntoElement)] -struct ReplSessionsContainer { - title: SharedString, - children: Vec, -} - -impl ReplSessionsContainer { - pub fn new(title: impl Into) -> Self { - Self { - title: title.into(), - children: Vec::new(), - } - } -} - -impl ParentElement for ReplSessionsContainer { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl RenderOnce for ReplSessionsContainer { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - v_flex() - .p_4() - .gap_2() - .size_full() - .child(Label::new(self.title).size(LabelSize::Large)) - .children(self.children) - } -} diff --git a/crates/repl/src/repl_settings.rs b/crates/repl/src/repl_settings.rs deleted file mode 100644 index 9faed72e55..0000000000 --- a/crates/repl/src/repl_settings.rs +++ /dev/null @@ -1,27 +0,0 @@ -use settings::{RegisterSetting, Settings}; - -/// Settings for configuring REPL display and behavior. -#[derive(Clone, Debug, RegisterSetting)] -pub struct ReplSettings { - /// Maximum number of lines to keep in REPL's scrollback buffer. - /// Clamped with [4, 256] range. - /// - /// Default: 32 - pub max_lines: usize, - /// Maximum number of columns to keep in REPL's scrollback buffer. - /// Clamped with [20, 512] range. - /// - /// Default: 128 - pub max_columns: usize, -} - -impl Settings for ReplSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - let repl = content.repl.as_ref().unwrap(); - - Self { - max_lines: repl.max_lines.unwrap(), - max_columns: repl.max_columns.unwrap(), - } - } -} diff --git a/crates/repl/src/repl_store.rs b/crates/repl/src/repl_store.rs deleted file mode 100644 index a5dc7b6c7b..0000000000 --- a/crates/repl/src/repl_store.rs +++ /dev/null @@ -1,292 +0,0 @@ -use std::sync::Arc; - -use anyhow::{Context as _, Result}; -use collections::HashMap; -use command_palette_hooks::CommandPaletteFilter; -use gpui::{App, Context, Entity, EntityId, Global, Subscription, Task, prelude::*}; -use jupyter_websocket_client::RemoteServer; -use language::Language; -use project::{Fs, Project, WorktreeId}; -use settings::{Settings, SettingsStore}; - -use crate::kernels::{ - list_remote_kernelspecs, local_kernel_specifications, python_env_kernel_specifications, -}; -use crate::{JupyterSettings, KernelSpecification, Session}; - -struct GlobalReplStore(Entity); - -impl Global for GlobalReplStore {} - -pub struct ReplStore { - fs: Arc, - enabled: bool, - sessions: HashMap>, - kernel_specifications: Vec, - selected_kernel_for_worktree: HashMap, - kernel_specifications_for_worktree: HashMap>, - _subscriptions: Vec, -} - -impl ReplStore { - const NAMESPACE: &'static str = "repl"; - - pub(crate) fn init(fs: Arc, cx: &mut App) { - let store = cx.new(move |cx| Self::new(fs, cx)); - - store - .update(cx, |store, cx| store.refresh_kernelspecs(cx)) - .detach_and_log_err(cx); - - cx.set_global(GlobalReplStore(store)) - } - - pub fn global(cx: &App) -> Entity { - cx.global::().0.clone() - } - - pub fn new(fs: Arc, cx: &mut Context) -> Self { - let subscriptions = vec![cx.observe_global::(move |this, cx| { - this.set_enabled(JupyterSettings::enabled(cx), cx); - })]; - - let this = Self { - fs, - enabled: JupyterSettings::enabled(cx), - sessions: HashMap::default(), - kernel_specifications: Vec::new(), - _subscriptions: subscriptions, - kernel_specifications_for_worktree: HashMap::default(), - selected_kernel_for_worktree: HashMap::default(), - }; - this.on_enabled_changed(cx); - this - } - - pub fn fs(&self) -> &Arc { - &self.fs - } - - pub fn is_enabled(&self) -> bool { - self.enabled - } - - pub fn kernel_specifications_for_worktree( - &self, - worktree_id: WorktreeId, - ) -> impl Iterator { - self.kernel_specifications_for_worktree - .get(&worktree_id) - .into_iter() - .flat_map(|specs| specs.iter()) - .chain(self.kernel_specifications.iter()) - } - - pub fn pure_jupyter_kernel_specifications(&self) -> impl Iterator { - self.kernel_specifications.iter() - } - - pub fn sessions(&self) -> impl Iterator> { - self.sessions.values() - } - - fn set_enabled(&mut self, enabled: bool, cx: &mut Context) { - if self.enabled == enabled { - return; - } - - self.enabled = enabled; - self.on_enabled_changed(cx); - } - - fn on_enabled_changed(&self, cx: &mut Context) { - if !self.enabled { - CommandPaletteFilter::update_global(cx, |filter, _cx| { - filter.hide_namespace(Self::NAMESPACE); - }); - - return; - } - - CommandPaletteFilter::update_global(cx, |filter, _cx| { - filter.show_namespace(Self::NAMESPACE); - }); - - cx.notify(); - } - - pub fn refresh_python_kernelspecs( - &mut self, - worktree_id: WorktreeId, - project: &Entity, - cx: &mut Context, - ) -> Task> { - let kernel_specifications = python_env_kernel_specifications(project, worktree_id, cx); - cx.spawn(async move |this, cx| { - let kernel_specifications = kernel_specifications - .await - .context("getting python kernelspecs")?; - - this.update(cx, |this, cx| { - this.kernel_specifications_for_worktree - .insert(worktree_id, kernel_specifications); - cx.notify(); - }) - }) - } - - fn get_remote_kernel_specifications( - &self, - cx: &mut Context, - ) -> Option>>> { - match ( - std::env::var("JUPYTER_SERVER"), - std::env::var("JUPYTER_TOKEN"), - ) { - (Ok(server), Ok(token)) => { - let remote_server = RemoteServer { - base_url: server, - token, - }; - let http_client = cx.http_client(); - Some(cx.spawn(async move |_, _| { - list_remote_kernelspecs(remote_server, http_client) - .await - .map(|specs| specs.into_iter().map(KernelSpecification::Remote).collect()) - })) - } - _ => None, - } - } - - pub fn refresh_kernelspecs(&mut self, cx: &mut Context) -> Task> { - let local_kernel_specifications = local_kernel_specifications(self.fs.clone()); - - let remote_kernel_specifications = self.get_remote_kernel_specifications(cx); - - let all_specs = cx.background_spawn(async move { - let mut all_specs = local_kernel_specifications - .await? - .into_iter() - .map(KernelSpecification::Jupyter) - .collect::>(); - - if let Some(remote_task) = remote_kernel_specifications - && let Ok(remote_specs) = remote_task.await - { - all_specs.extend(remote_specs); - } - - anyhow::Ok(all_specs) - }); - - cx.spawn(async move |this, cx| { - let all_specs = all_specs.await; - - if let Ok(specs) = all_specs { - this.update(cx, |this, cx| { - this.kernel_specifications = specs; - cx.notify(); - }) - .ok(); - } - - anyhow::Ok(()) - }) - } - - pub fn set_active_kernelspec( - &mut self, - worktree_id: WorktreeId, - kernelspec: KernelSpecification, - _cx: &mut Context, - ) { - self.selected_kernel_for_worktree - .insert(worktree_id, kernelspec); - } - - pub fn active_kernelspec( - &self, - worktree_id: WorktreeId, - language_at_cursor: Option>, - cx: &App, - ) -> Option { - let selected_kernelspec = self.selected_kernel_for_worktree.get(&worktree_id).cloned(); - - if let Some(language_at_cursor) = language_at_cursor { - selected_kernelspec.or_else(|| { - self.kernelspec_legacy_by_lang_only(worktree_id, language_at_cursor, cx) - }) - } else { - selected_kernelspec - } - } - - fn kernelspec_legacy_by_lang_only( - &self, - worktree_id: WorktreeId, - language_at_cursor: Arc, - cx: &App, - ) -> Option { - let settings = JupyterSettings::get_global(cx); - let selected_kernel = settings - .kernel_selections - .get(language_at_cursor.code_fence_block_name().as_ref()); - - let found_by_name = self - .kernel_specifications_for_worktree(worktree_id) - .find(|runtime_specification| { - if let (Some(selected), KernelSpecification::Jupyter(runtime_specification)) = - (selected_kernel, runtime_specification) - { - // Top priority is the selected kernel - return runtime_specification.name.to_lowercase() == selected.to_lowercase(); - } - false - }) - .cloned(); - - if let Some(found_by_name) = found_by_name { - return Some(found_by_name); - } - - self.kernel_specifications_for_worktree(worktree_id) - .find(|kernel_option| match kernel_option { - KernelSpecification::Jupyter(runtime_specification) => { - runtime_specification.kernelspec.language.to_lowercase() - == language_at_cursor.code_fence_block_name().to_lowercase() - } - KernelSpecification::PythonEnv(runtime_specification) => { - runtime_specification.kernelspec.language.to_lowercase() - == language_at_cursor.code_fence_block_name().to_lowercase() - } - KernelSpecification::Remote(remote_spec) => { - remote_spec.kernelspec.language.to_lowercase() - == language_at_cursor.code_fence_block_name().to_lowercase() - } - }) - .cloned() - } - - pub fn get_session(&self, entity_id: EntityId) -> Option<&Entity> { - self.sessions.get(&entity_id) - } - - pub fn insert_session(&mut self, entity_id: EntityId, session: Entity) { - self.sessions.insert(entity_id, session); - } - - pub fn remove_session(&mut self, entity_id: EntityId) { - self.sessions.remove(&entity_id); - } - - #[cfg(test)] - pub fn set_kernel_specs_for_testing( - &mut self, - specs: Vec, - cx: &mut Context, - ) { - self.kernel_specifications = specs; - cx.notify(); - } -} diff --git a/crates/repl/src/session.rs b/crates/repl/src/session.rs deleted file mode 100644 index 1fa0bfec35..0000000000 --- a/crates/repl/src/session.rs +++ /dev/null @@ -1,702 +0,0 @@ -use crate::components::KernelListItem; -use crate::kernels::RemoteRunningKernel; -use crate::setup_editor_session_actions; -use crate::{ - KernelStatus, - kernels::{Kernel, KernelSpecification, NativeRunningKernel}, - outputs::{ExecutionStatus, ExecutionView}, -}; -use anyhow::Context as _; -use collections::{HashMap, HashSet}; -use editor::SelectionEffects; -use editor::{ - Anchor, AnchorRangeExt as _, Editor, MultiBuffer, ToPoint, - display_map::{ - BlockContext, BlockId, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, - RenderBlock, - }, - scroll::Autoscroll, -}; -use futures::FutureExt as _; -use gpui::{ - Context, Entity, EventEmitter, Render, Subscription, Task, WeakEntity, Window, div, prelude::*, -}; -use language::Point; -use project::Fs; -use runtimelib::{ - ExecuteRequest, ExecutionState, InterruptRequest, JupyterMessage, JupyterMessageContent, - ShutdownRequest, -}; -use std::{env::temp_dir, ops::Range, sync::Arc, time::Duration}; -use theme::ActiveTheme; -use ui::{IconButtonShape, Tooltip, prelude::*}; -use util::ResultExt as _; - -pub struct Session { - fs: Arc, - editor: WeakEntity, - pub kernel: Kernel, - blocks: HashMap, - pub kernel_specification: KernelSpecification, - _buffer_subscription: Subscription, -} - -struct EditorBlock { - code_range: Range, - invalidation_anchor: Anchor, - block_id: CustomBlockId, - execution_view: Entity, -} - -type CloseBlockFn = - Arc Fn(CustomBlockId, &'a mut Window, &mut App) + Send + Sync + 'static>; - -impl EditorBlock { - fn new( - editor: WeakEntity, - code_range: Range, - status: ExecutionStatus, - on_close: CloseBlockFn, - cx: &mut Context, - ) -> anyhow::Result { - let editor = editor.upgrade().context("editor is not open")?; - let workspace = editor.read(cx).workspace().context("workspace dropped")?; - - let execution_view = cx.new(|cx| ExecutionView::new(status, workspace.downgrade(), cx)); - - let (block_id, invalidation_anchor) = editor.update(cx, |editor, cx| { - let buffer = editor.buffer().clone(); - let buffer_snapshot = buffer.read(cx).snapshot(cx); - let end_point = code_range.end.to_point(&buffer_snapshot); - let next_row_start = end_point + Point::new(1, 0); - if next_row_start > buffer_snapshot.max_point() { - buffer.update(cx, |buffer, cx| { - buffer.edit( - [( - buffer_snapshot.max_point()..buffer_snapshot.max_point(), - "\n", - )], - None, - cx, - ) - }); - } - - let invalidation_anchor = buffer.read(cx).read(cx).anchor_before(next_row_start); - let block = BlockProperties { - placement: BlockPlacement::Below(code_range.end), - // Take up at least one height for status, allow the editor to determine the real height based on the content from render - height: Some(1), - style: BlockStyle::Sticky, - render: Self::create_output_area_renderer(execution_view.clone(), on_close.clone()), - priority: 0, - }; - - let block_id = editor.insert_blocks([block], None, cx)[0]; - (block_id, invalidation_anchor) - }); - - anyhow::Ok(Self { - code_range, - invalidation_anchor, - block_id, - execution_view, - }) - } - - fn handle_message( - &mut self, - message: &JupyterMessage, - window: &mut Window, - cx: &mut Context, - ) { - self.execution_view.update(cx, |execution_view, cx| { - execution_view.push_message(&message.content, window, cx); - }); - } - - fn create_output_area_renderer( - execution_view: Entity, - on_close: CloseBlockFn, - ) -> RenderBlock { - Arc::new(move |cx: &mut BlockContext| { - let execution_view = execution_view.clone(); - let text_style = crate::outputs::plain::text_style(cx.window, cx.app); - - let editor_margins = cx.margins; - let gutter = editor_margins.gutter; - - let block_id = cx.block_id; - let on_close = on_close.clone(); - - let rem_size = cx.window.rem_size(); - - let text_line_height = text_style.line_height_in_pixels(rem_size); - - let close_button = h_flex() - .flex_none() - .items_center() - .justify_center() - .absolute() - .top(text_line_height / 2.) - .right( - // 2px is a magic number to nudge the button just a bit closer to - // the line number start - gutter.full_width() / 2.0 - text_line_height / 2.0 - px(2.), - ) - .w(text_line_height) - .h(text_line_height) - .child( - IconButton::new("close_output_area", IconName::Close) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .size(ButtonSize::Compact) - .shape(IconButtonShape::Square) - .tooltip(Tooltip::text("Close output area")) - .on_click(move |_, window, cx| { - if let BlockId::Custom(block_id) = block_id { - (on_close)(block_id, window, cx) - } - }), - ); - - div() - .id(cx.block_id) - .block_mouse_except_scroll() - .flex() - .items_start() - .min_h(text_line_height) - .w_full() - .border_y_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().background) - .child( - div() - .relative() - .w(gutter.full_width()) - .h(text_line_height * 2) - .child(close_button), - ) - .child( - div() - .flex_1() - .size_full() - .py(text_line_height / 2.) - .mr(editor_margins.right) - .pr_2() - .child(execution_view), - ) - .into_any_element() - }) - } -} - -impl Session { - pub fn new( - editor: WeakEntity, - fs: Arc, - kernel_specification: KernelSpecification, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let subscription = match editor.upgrade() { - Some(editor) => { - let buffer = editor.read(cx).buffer().clone(); - cx.subscribe(&buffer, Self::on_buffer_event) - } - None => Subscription::new(|| {}), - }; - - let editor_handle = editor.clone(); - - editor - .update(cx, |editor, _cx| { - setup_editor_session_actions(editor, editor_handle); - }) - .ok(); - - let mut session = Self { - fs, - editor, - kernel: Kernel::StartingKernel(Task::ready(()).shared()), - blocks: HashMap::default(), - kernel_specification, - _buffer_subscription: subscription, - }; - - session.start_kernel(window, cx); - session - } - - fn start_kernel(&mut self, window: &mut Window, cx: &mut Context) { - let kernel_language = self.kernel_specification.language(); - let entity_id = self.editor.entity_id(); - let working_directory = self - .editor - .upgrade() - .and_then(|editor| editor.read(cx).working_directory(cx)) - .unwrap_or_else(temp_dir); - - telemetry::event!( - "Kernel Status Changed", - kernel_language, - kernel_status = KernelStatus::Starting.to_string(), - repl_session_id = cx.entity_id().to_string(), - ); - - let session_view = cx.entity(); - - let kernel = match self.kernel_specification.clone() { - KernelSpecification::Jupyter(kernel_specification) - | KernelSpecification::PythonEnv(kernel_specification) => NativeRunningKernel::new( - kernel_specification, - entity_id, - working_directory, - self.fs.clone(), - session_view, - window, - cx, - ), - KernelSpecification::Remote(remote_kernel_specification) => RemoteRunningKernel::new( - remote_kernel_specification, - working_directory, - session_view, - window, - cx, - ), - }; - - let pending_kernel = cx - .spawn(async move |this, cx| { - let kernel = kernel.await; - - match kernel { - Ok(kernel) => { - this.update(cx, |session, cx| { - session.kernel(Kernel::RunningKernel(kernel), cx); - }) - .ok(); - } - Err(err) => { - this.update(cx, |session, cx| { - session.kernel_errored(err.to_string(), cx); - }) - .ok(); - } - } - }) - .shared(); - - self.kernel(Kernel::StartingKernel(pending_kernel), cx); - cx.notify(); - } - - pub fn kernel_errored(&mut self, error_message: String, cx: &mut Context) { - self.kernel(Kernel::ErroredLaunch(error_message.clone()), cx); - - self.blocks.values().for_each(|block| { - block.execution_view.update(cx, |execution_view, cx| { - match execution_view.status { - ExecutionStatus::Finished => { - // Do nothing when the output was good - } - _ => { - // All other cases, set the status to errored - execution_view.status = - ExecutionStatus::KernelErrored(error_message.clone()) - } - } - cx.notify(); - }); - }); - } - - fn on_buffer_event( - &mut self, - buffer: Entity, - event: &multi_buffer::Event, - cx: &mut Context, - ) { - if let multi_buffer::Event::Edited { .. } = event { - let snapshot = buffer.read(cx).snapshot(cx); - - let mut blocks_to_remove: HashSet = HashSet::default(); - - self.blocks.retain(|_id, block| { - if block.invalidation_anchor.is_valid(&snapshot) { - true - } else { - blocks_to_remove.insert(block.block_id); - false - } - }); - - if !blocks_to_remove.is_empty() { - self.editor - .update(cx, |editor, cx| { - editor.remove_blocks(blocks_to_remove, None, cx); - }) - .ok(); - cx.notify(); - } - } - } - - fn send(&mut self, message: JupyterMessage, _cx: &mut Context) -> anyhow::Result<()> { - if let Kernel::RunningKernel(kernel) = &mut self.kernel { - kernel.request_tx().try_send(message).ok(); - } - - anyhow::Ok(()) - } - - pub fn clear_outputs(&mut self, cx: &mut Context) { - let blocks_to_remove: HashSet = - self.blocks.values().map(|block| block.block_id).collect(); - - self.editor - .update(cx, |editor, cx| { - editor.remove_blocks(blocks_to_remove, None, cx); - }) - .ok(); - - self.blocks.clear(); - } - - pub fn execute( - &mut self, - code: String, - anchor_range: Range, - next_cell: Option, - move_down: bool, - window: &mut Window, - cx: &mut Context, - ) { - let Some(editor) = self.editor.upgrade() else { - return; - }; - - if code.is_empty() { - return; - } - - let execute_request = ExecuteRequest { - code, - ..ExecuteRequest::default() - }; - - let message: JupyterMessage = execute_request.into(); - - let mut blocks_to_remove: HashSet = HashSet::default(); - - let buffer = editor.read(cx).buffer().read(cx).snapshot(cx); - - self.blocks.retain(|_key, block| { - if anchor_range.overlaps(&block.code_range, &buffer) { - blocks_to_remove.insert(block.block_id); - false - } else { - true - } - }); - - self.editor - .update(cx, |editor, cx| { - editor.remove_blocks(blocks_to_remove, None, cx); - }) - .ok(); - - let status = match &self.kernel { - Kernel::Restarting => ExecutionStatus::Restarting, - Kernel::RunningKernel(_) => ExecutionStatus::Queued, - Kernel::StartingKernel(_) => ExecutionStatus::ConnectingToKernel, - Kernel::ErroredLaunch(error) => ExecutionStatus::KernelErrored(error.clone()), - Kernel::ShuttingDown => ExecutionStatus::ShuttingDown, - Kernel::Shutdown => ExecutionStatus::Shutdown, - }; - - let parent_message_id = message.header.msg_id.clone(); - let session_view = cx.entity().downgrade(); - let weak_editor = self.editor.clone(); - - let on_close: CloseBlockFn = Arc::new( - move |block_id: CustomBlockId, _: &mut Window, cx: &mut App| { - if let Some(session) = session_view.upgrade() { - session.update(cx, |session, cx| { - session.blocks.remove(&parent_message_id); - cx.notify(); - }); - } - - if let Some(editor) = weak_editor.upgrade() { - editor.update(cx, |editor, cx| { - let mut block_ids = HashSet::default(); - block_ids.insert(block_id); - editor.remove_blocks(block_ids, None, cx); - }); - } - }, - ); - - let Ok(editor_block) = - EditorBlock::new(self.editor.clone(), anchor_range, status, on_close, cx) - else { - return; - }; - - let new_cursor_pos = if let Some(next_cursor) = next_cell { - next_cursor - } else { - editor_block.invalidation_anchor - }; - - self.blocks - .insert(message.header.msg_id.clone(), editor_block); - - match &self.kernel { - Kernel::RunningKernel(_) => { - self.send(message, cx).ok(); - } - Kernel::StartingKernel(task) => { - // Queue up the execution as a task to run after the kernel starts - let task = task.clone(); - - cx.spawn(async move |this, cx| { - task.await; - this.update(cx, |session, cx| { - session.send(message, cx).ok(); - }) - .ok(); - }) - .detach(); - } - _ => {} - } - - if move_down { - editor.update(cx, move |editor, cx| { - editor.change_selections( - SelectionEffects::scroll(Autoscroll::top_relative(8)), - window, - cx, - |selections| { - selections.select_ranges([new_cursor_pos..new_cursor_pos]); - }, - ); - }); - } - } - - pub fn route(&mut self, message: &JupyterMessage, window: &mut Window, cx: &mut Context) { - let parent_message_id = match message.parent_header.as_ref() { - Some(header) => &header.msg_id, - None => return, - }; - - match &message.content { - JupyterMessageContent::Status(status) => { - self.kernel.set_execution_state(&status.execution_state); - - telemetry::event!( - "Kernel Status Changed", - kernel_language = self.kernel_specification.language(), - kernel_status = KernelStatus::from(&self.kernel).to_string(), - repl_session_id = cx.entity_id().to_string(), - ); - - cx.notify(); - } - JupyterMessageContent::KernelInfoReply(reply) => { - self.kernel.set_kernel_info(reply); - cx.notify(); - } - JupyterMessageContent::UpdateDisplayData(update) => { - let display_id = if let Some(display_id) = update.transient.display_id.clone() { - display_id - } else { - return; - }; - - self.blocks.iter_mut().for_each(|(_, block)| { - block.execution_view.update(cx, |execution_view, cx| { - execution_view.update_display_data(&update.data, &display_id, window, cx); - }); - }); - return; - } - _ => {} - } - - if let Some(block) = self.blocks.get_mut(parent_message_id) { - block.handle_message(message, window, cx); - } - } - - pub fn interrupt(&mut self, cx: &mut Context) { - match &mut self.kernel { - Kernel::RunningKernel(_kernel) => { - self.send(InterruptRequest {}.into(), cx).ok(); - } - Kernel::StartingKernel(_task) => { - // NOTE: If we switch to a literal queue instead of chaining on to the task, clear all queued executions - } - _ => {} - } - } - - pub fn kernel(&mut self, kernel: Kernel, cx: &mut Context) { - if let Kernel::Shutdown = kernel { - cx.emit(SessionEvent::Shutdown(self.editor.clone())); - } - - let kernel_status = KernelStatus::from(&kernel).to_string(); - let kernel_language = self.kernel_specification.language(); - - telemetry::event!( - "Kernel Status Changed", - kernel_language, - kernel_status, - repl_session_id = cx.entity_id().to_string(), - ); - - self.kernel = kernel; - } - - pub fn shutdown(&mut self, window: &mut Window, cx: &mut Context) { - let kernel = std::mem::replace(&mut self.kernel, Kernel::ShuttingDown); - - match kernel { - Kernel::RunningKernel(mut kernel) => { - let mut request_tx = kernel.request_tx(); - - let forced = kernel.force_shutdown(window, cx); - - cx.spawn(async move |this, cx| { - let message: JupyterMessage = ShutdownRequest { restart: false }.into(); - request_tx.try_send(message).ok(); - - forced.await.log_err(); - - // Give the kernel a bit of time to clean up - cx.background_executor().timer(Duration::from_secs(3)).await; - - this.update(cx, |session, cx| { - session.clear_outputs(cx); - session.kernel(Kernel::Shutdown, cx); - cx.notify(); - }) - .ok(); - }) - .detach(); - } - _ => { - self.kernel(Kernel::Shutdown, cx); - } - } - cx.notify(); - } - - pub fn restart(&mut self, window: &mut Window, cx: &mut Context) { - let kernel = std::mem::replace(&mut self.kernel, Kernel::Restarting); - - match kernel { - Kernel::Restarting => { - // Do nothing if already restarting - } - Kernel::RunningKernel(mut kernel) => { - let mut request_tx = kernel.request_tx(); - - let forced = kernel.force_shutdown(window, cx); - - cx.spawn_in(window, async move |this, cx| { - // Send shutdown request with restart flag - log::debug!("restarting kernel"); - let message: JupyterMessage = ShutdownRequest { restart: true }.into(); - request_tx.try_send(message).ok(); - - // Wait for kernel to shutdown - cx.background_executor().timer(Duration::from_secs(1)).await; - - // Force kill the kernel if it hasn't shut down - forced.await.log_err(); - - // Start a new kernel - this.update_in(cx, |session, window, cx| { - // TODO: Differentiate between restart and restart+clear-outputs - session.clear_outputs(cx); - session.start_kernel(window, cx); - }) - .ok(); - }) - .detach(); - } - _ => { - self.clear_outputs(cx); - self.start_kernel(window, cx); - } - } - cx.notify(); - } -} - -pub enum SessionEvent { - Shutdown(WeakEntity), -} - -impl EventEmitter for Session {} - -impl Render for Session { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let (status_text, interrupt_button) = match &self.kernel { - Kernel::RunningKernel(kernel) => ( - kernel - .kernel_info() - .as_ref() - .map(|info| info.language_info.name.clone()), - Some( - Button::new("interrupt", "Interrupt") - .style(ButtonStyle::Subtle) - .on_click(cx.listener(move |session, _, _, cx| { - session.interrupt(cx); - })), - ), - ), - Kernel::StartingKernel(_) => (Some("Starting".into()), None), - Kernel::ErroredLaunch(err) => (Some(format!("Error: {err}")), None), - Kernel::ShuttingDown => (Some("Shutting Down".into()), None), - Kernel::Shutdown => (Some("Shutdown".into()), None), - Kernel::Restarting => (Some("Restarting".into()), None), - }; - - KernelListItem::new(self.kernel_specification.clone()) - .status_color(match &self.kernel { - Kernel::RunningKernel(kernel) => match kernel.execution_state() { - ExecutionState::Idle => Color::Success, - ExecutionState::Busy => Color::Modified, - ExecutionState::Unknown => Color::Modified, - ExecutionState::Starting => Color::Modified, - ExecutionState::Restarting => Color::Modified, - ExecutionState::Terminating => Color::Disabled, - ExecutionState::AutoRestarting => Color::Modified, - ExecutionState::Dead => Color::Disabled, - ExecutionState::Other(_) => Color::Modified, - }, - Kernel::StartingKernel(_) => Color::Modified, - Kernel::ErroredLaunch(_) => Color::Error, - Kernel::ShuttingDown => Color::Modified, - Kernel::Shutdown => Color::Disabled, - Kernel::Restarting => Color::Modified, - }) - .child(Label::new(self.kernel_specification.name())) - .children(status_text.map(|status_text| Label::new(format!("({status_text})")))) - .button( - Button::new("shutdown", "Shutdown") - .style(ButtonStyle::Subtle) - .disabled(self.kernel.is_shutting_down()) - .on_click(cx.listener(move |session, _, window, cx| { - session.shutdown(window, cx); - })), - ) - .buttons(interrupt_button) - } -} diff --git a/crates/rich_text/Cargo.toml b/crates/rich_text/Cargo.toml deleted file mode 100644 index 17bd8d2a4b..0000000000 --- a/crates/rich_text/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "rich_text" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/rich_text.rs" -doctest = false - -[features] -test-support = [ - "gpui/test-support", - "util/test-support", -] - -[dependencies] -futures.workspace = true -gpui.workspace = true -language.workspace = true -linkify.workspace = true -pulldown-cmark.workspace = true -theme.workspace = true -ui.workspace = true -util.workspace = true diff --git a/crates/rich_text/LICENSE-GPL b/crates/rich_text/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/rich_text/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/rich_text/src/rich_text.rs b/crates/rich_text/src/rich_text.rs deleted file mode 100644 index 2af9988f03..0000000000 --- a/crates/rich_text/src/rich_text.rs +++ /dev/null @@ -1,418 +0,0 @@ -use futures::FutureExt; -use gpui::{ - AnyElement, AnyView, App, ElementId, FontStyle, FontWeight, HighlightStyle, InteractiveText, - IntoElement, SharedString, StrikethroughStyle, StyledText, UnderlineStyle, Window, -}; -use language::{HighlightId, Language, LanguageRegistry}; -use std::{ops::Range, sync::Arc}; -use theme::ActiveTheme; -use ui::LinkPreview; -use util::RangeExt; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum Highlight { - Code, - Id(HighlightId), - InlineCode(bool), - Highlight(HighlightStyle), - Mention, - SelfMention, -} - -impl From for Highlight { - fn from(style: HighlightStyle) -> Self { - Self::Highlight(style) - } -} - -impl From for Highlight { - fn from(style: HighlightId) -> Self { - Self::Id(style) - } -} - -#[derive(Clone, Default)] -pub struct RichText { - pub text: SharedString, - pub highlights: Vec<(Range, Highlight)>, - pub link_ranges: Vec>, - pub link_urls: Arc<[String]>, - - pub custom_ranges: Vec>, - custom_ranges_tooltip_fn: - Option, &mut Window, &mut App) -> Option>>, -} - -/// Allows one to specify extra links to the rendered markdown, which can be used -/// for e.g. mentions. -#[derive(Debug)] -pub struct Mention { - pub range: Range, - pub is_self_mention: bool, -} - -impl RichText { - pub fn new( - block: String, - mentions: &[Mention], - language_registry: &Arc, - ) -> Self { - let mut text = String::new(); - let mut highlights = Vec::new(); - let mut link_ranges = Vec::new(); - let mut link_urls = Vec::new(); - render_markdown_mut( - &block, - mentions, - language_registry, - None, - &mut text, - &mut highlights, - &mut link_ranges, - &mut link_urls, - ); - text.truncate(text.trim_end().len()); - - RichText { - text: SharedString::from(text), - link_urls: link_urls.into(), - link_ranges, - highlights, - custom_ranges: Vec::new(), - custom_ranges_tooltip_fn: None, - } - } - - pub fn set_tooltip_builder_for_custom_ranges( - &mut self, - f: impl Fn(usize, Range, &mut Window, &mut App) -> Option + 'static, - ) { - self.custom_ranges_tooltip_fn = Some(Arc::new(f)); - } - - pub fn element(&self, id: ElementId, window: &mut Window, cx: &mut App) -> AnyElement { - let theme = cx.theme(); - let code_background = theme.colors().surface_background; - - InteractiveText::new( - id, - StyledText::new(self.text.clone()).with_default_highlights( - &window.text_style(), - self.highlights.iter().map(|(range, highlight)| { - ( - range.clone(), - match highlight { - Highlight::Code => HighlightStyle { - background_color: Some(code_background), - ..Default::default() - }, - Highlight::Id(id) => HighlightStyle { - background_color: Some(code_background), - ..id.style(theme.syntax()).unwrap_or_default() - }, - Highlight::InlineCode(link) => { - if *link { - HighlightStyle { - background_color: Some(code_background), - underline: Some(UnderlineStyle { - thickness: 1.0.into(), - ..Default::default() - }), - ..Default::default() - } - } else { - HighlightStyle { - background_color: Some(code_background), - ..Default::default() - } - } - } - Highlight::Highlight(highlight) => *highlight, - Highlight::Mention => HighlightStyle { - font_weight: Some(FontWeight::BOLD), - ..Default::default() - }, - Highlight::SelfMention => HighlightStyle { - font_weight: Some(FontWeight::BOLD), - ..Default::default() - }, - }, - ) - }), - ), - ) - .on_click(self.link_ranges.clone(), { - let link_urls = self.link_urls.clone(); - move |ix, _, cx| { - let url = &link_urls[ix]; - if url.starts_with("http") { - cx.open_url(url); - } - } - }) - .tooltip({ - let link_ranges = self.link_ranges.clone(); - let link_urls = self.link_urls.clone(); - let custom_tooltip_ranges = self.custom_ranges.clone(); - let custom_tooltip_fn = self.custom_ranges_tooltip_fn.clone(); - move |idx, window, cx| { - for (ix, range) in link_ranges.iter().enumerate() { - if range.contains(&idx) { - return Some(LinkPreview::new(&link_urls[ix], cx)); - } - } - for range in &custom_tooltip_ranges { - if range.contains(&idx) - && let Some(f) = &custom_tooltip_fn - { - return f(idx, range.clone(), window, cx); - } - } - None - } - }) - .into_any_element() - } -} - -pub fn render_markdown_mut( - block: &str, - mut mentions: &[Mention], - language_registry: &Arc, - language: Option<&Arc>, - text: &mut String, - highlights: &mut Vec<(Range, Highlight)>, - link_ranges: &mut Vec>, - link_urls: &mut Vec, -) { - use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag, TagEnd}; - - let mut bold_depth = 0; - let mut italic_depth = 0; - let mut strikethrough_depth = 0; - let mut link_url = None; - let mut current_language = None; - let mut list_stack = Vec::new(); - - let mut options = Options::all(); - options.remove(pulldown_cmark::Options::ENABLE_DEFINITION_LIST); - - for (event, source_range) in Parser::new_ext(block, options).into_offset_iter() { - let prev_len = text.len(); - match event { - Event::Text(t) => { - if let Some(language) = ¤t_language { - render_code(text, highlights, t.as_ref(), language); - } else { - while let Some(mention) = mentions.first() { - if !source_range.contains_inclusive(&mention.range) { - break; - } - mentions = &mentions[1..]; - let range = (prev_len + mention.range.start - source_range.start) - ..(prev_len + mention.range.end - source_range.start); - highlights.push(( - range.clone(), - if mention.is_self_mention { - Highlight::SelfMention - } else { - Highlight::Mention - }, - )); - } - - text.push_str(t.as_ref()); - let mut style = HighlightStyle::default(); - if bold_depth > 0 { - style.font_weight = Some(FontWeight::BOLD); - } - if italic_depth > 0 { - style.font_style = Some(FontStyle::Italic); - } - if strikethrough_depth > 0 { - style.strikethrough = Some(StrikethroughStyle { - thickness: 1.0.into(), - ..Default::default() - }); - } - let last_run_len = if let Some(link_url) = link_url.clone() { - link_ranges.push(prev_len..text.len()); - link_urls.push(link_url); - style.underline = Some(UnderlineStyle { - thickness: 1.0.into(), - ..Default::default() - }); - prev_len - } else { - // Manually scan for links - let mut finder = linkify::LinkFinder::new(); - finder.kinds(&[linkify::LinkKind::Url]); - let mut last_link_len = prev_len; - for link in finder.links(&t) { - let start = link.start(); - let end = link.end(); - let range = (prev_len + start)..(prev_len + end); - link_ranges.push(range.clone()); - link_urls.push(link.as_str().to_string()); - - // If there is a style before we match a link, we have to add this to the highlighted ranges - if style != HighlightStyle::default() && last_link_len < link.start() { - highlights.push(( - last_link_len..link.start(), - Highlight::Highlight(style), - )); - } - - highlights.push(( - range, - Highlight::Highlight(HighlightStyle { - underline: Some(UnderlineStyle { - thickness: 1.0.into(), - ..Default::default() - }), - ..style - }), - )); - - last_link_len = end; - } - last_link_len - }; - - if style != HighlightStyle::default() && last_run_len < text.len() { - let mut new_highlight = true; - if let Some((last_range, last_style)) = highlights.last_mut() - && last_range.end == last_run_len - && last_style == &Highlight::Highlight(style) - { - last_range.end = text.len(); - new_highlight = false; - } - if new_highlight { - highlights - .push((last_run_len..text.len(), Highlight::Highlight(style))); - } - } - } - } - Event::Code(t) => { - text.push_str(t.as_ref()); - let is_link = link_url.is_some(); - - if let Some(link_url) = link_url.clone() { - link_ranges.push(prev_len..text.len()); - link_urls.push(link_url); - } - - highlights.push((prev_len..text.len(), Highlight::InlineCode(is_link))) - } - Event::Start(tag) => match tag { - Tag::Paragraph => new_paragraph(text, &mut list_stack), - Tag::Heading { .. } => { - new_paragraph(text, &mut list_stack); - bold_depth += 1; - } - Tag::CodeBlock(kind) => { - new_paragraph(text, &mut list_stack); - current_language = if let CodeBlockKind::Fenced(language) = kind { - language_registry - .language_for_name(language.as_ref()) - .now_or_never() - .and_then(Result::ok) - } else { - language.cloned() - } - } - Tag::Emphasis => italic_depth += 1, - Tag::Strong => bold_depth += 1, - Tag::Strikethrough => strikethrough_depth += 1, - Tag::Link { dest_url, .. } => link_url = Some(dest_url.to_string()), - Tag::List(number) => { - list_stack.push((number, false)); - } - Tag::Item => { - let len = list_stack.len(); - if let Some((list_number, has_content)) = list_stack.last_mut() { - *has_content = false; - if !text.is_empty() && !text.ends_with('\n') { - text.push('\n'); - } - for _ in 0..len - 1 { - text.push_str(" "); - } - if let Some(number) = list_number { - text.push_str(&format!("{}. ", number)); - *number += 1; - *has_content = false; - } else { - text.push_str("- "); - } - } - } - _ => {} - }, - Event::End(tag) => match tag { - TagEnd::Heading(_) => bold_depth -= 1, - TagEnd::CodeBlock => current_language = None, - TagEnd::Emphasis => italic_depth -= 1, - TagEnd::Strong => bold_depth -= 1, - TagEnd::Strikethrough => strikethrough_depth -= 1, - TagEnd::Link => link_url = None, - TagEnd::List(_) => drop(list_stack.pop()), - _ => {} - }, - Event::HardBreak => text.push('\n'), - Event::SoftBreak => text.push('\n'), - _ => {} - } - } -} - -pub fn render_code( - text: &mut String, - highlights: &mut Vec<(Range, Highlight)>, - content: &str, - language: &Arc, -) { - let prev_len = text.len(); - text.push_str(content); - let mut offset = 0; - for (range, highlight_id) in language.highlight_text(&content.into(), 0..content.len()) { - if range.start > offset { - highlights.push((prev_len + offset..prev_len + range.start, Highlight::Code)); - } - highlights.push(( - prev_len + range.start..prev_len + range.end, - Highlight::Id(highlight_id), - )); - offset = range.end; - } - if offset < content.len() { - highlights.push((prev_len + offset..prev_len + content.len(), Highlight::Code)); - } -} - -pub fn new_paragraph(text: &mut String, list_stack: &mut Vec<(Option, bool)>) { - let mut is_subsequent_paragraph_of_list = false; - if let Some((_, has_content)) = list_stack.last_mut() { - if *has_content { - is_subsequent_paragraph_of_list = true; - } else { - *has_content = true; - return; - } - } - - if !text.is_empty() { - if !text.ends_with('\n') { - text.push('\n'); - } - text.push('\n'); - } - for _ in 0..list_stack.len().saturating_sub(1) { - text.push_str(" "); - } - if is_subsequent_paragraph_of_list { - text.push_str(" "); - } -} diff --git a/crates/rope/Cargo.toml b/crates/rope/Cargo.toml deleted file mode 100644 index 9f0fc2be8a..0000000000 --- a/crates/rope/Cargo.toml +++ /dev/null @@ -1,37 +0,0 @@ -[package] -name = "rope" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/rope.rs" - -[dependencies] -arrayvec = "0.7.1" -log.workspace = true -rayon.workspace = true -sum_tree.workspace = true -unicode-segmentation.workspace = true -util.workspace = true -ztracing.workspace = true -tracing.workspace = true - -[dev-dependencies] -ctor.workspace = true -gpui = { workspace = true, features = ["test-support"] } -rand.workspace = true -util = { workspace = true, features = ["test-support"] } -criterion.workspace = true -zlog.workspace = true - -[[bench]] -name = "rope_benchmark" -harness = false - -[package.metadata.cargo-machete] -ignored = ["tracing"] diff --git a/crates/rope/LICENSE-GPL b/crates/rope/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/rope/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/rope/benches/rope_benchmark.rs b/crates/rope/benches/rope_benchmark.rs deleted file mode 100644 index 8599328aac..0000000000 --- a/crates/rope/benches/rope_benchmark.rs +++ /dev/null @@ -1,273 +0,0 @@ -use std::ops::Range; - -use criterion::{ - BatchSize, BenchmarkId, Criterion, Throughput, black_box, criterion_group, criterion_main, -}; -use rand::prelude::*; -use rand::rngs::StdRng; -use rope::{Point, Rope}; -use sum_tree::Bias; -use util::RandomCharIter; - -/// Returns a biased random string whose UTF-8 length is close to but no more than `len` bytes. -/// -/// The string is biased towards characters expected to occur in text or likely to exercise edge -/// cases. -fn generate_random_text(rng: &mut StdRng, len: usize) -> String { - let mut str = String::with_capacity(len); - let mut chars = RandomCharIter::new(rng); - loop { - let ch = chars.next().unwrap(); - if str.len() + ch.len_utf8() > len { - break; - } - str.push(ch); - } - str -} - -fn generate_random_rope(rng: &mut StdRng, text_len: usize) -> Rope { - let text = generate_random_text(rng, text_len); - let mut rope = Rope::new(); - rope.push(&text); - rope -} - -fn generate_random_rope_ranges(rng: &mut StdRng, rope: &Rope) -> Vec> { - let range_max_len = 50; - let num_ranges = rope.len() / range_max_len; - - let mut ranges = Vec::new(); - let mut start = 0; - for _ in 0..num_ranges { - let range_start = rope.clip_offset( - rng.random_range(start..=(start + range_max_len)), - sum_tree::Bias::Left, - ); - let range_end = rope.clip_offset( - rng.random_range(range_start..(range_start + range_max_len)), - sum_tree::Bias::Right, - ); - - let range = range_start..range_end; - if !range.is_empty() { - ranges.push(range); - } - - start = range_end + 1; - } - - ranges -} - -fn generate_random_rope_points(rng: &mut StdRng, rope: &Rope) -> Vec { - let num_points = rope.len() / 10; - - let mut points = Vec::new(); - for _ in 0..num_points { - points.push(rope.offset_to_point(rng.random_range(0..rope.len()))); - } - points -} - -fn rope_benchmarks(c: &mut Criterion) { - static SEED: u64 = 9999; - static KB: usize = 1024; - - let sizes = [4 * KB, 64 * KB]; - - let mut group = c.benchmark_group("push"); - for size in sizes.iter() { - group.throughput(Throughput::Bytes(*size as u64)); - group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| { - let mut rng = StdRng::seed_from_u64(SEED); - let text = generate_random_text(&mut rng, *size); - - b.iter(|| { - let mut rope = Rope::new(); - for _ in 0..10 { - rope.push(&text); - } - }); - }); - } - group.finish(); - - let mut group = c.benchmark_group("append"); - for size in sizes.iter() { - group.throughput(Throughput::Bytes(*size as u64)); - group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| { - let mut rng = StdRng::seed_from_u64(SEED); - let mut random_ropes = Vec::new(); - for _ in 0..5 { - let rope = generate_random_rope(&mut rng, *size); - random_ropes.push(rope); - } - - b.iter(|| { - let mut rope_b = Rope::new(); - for rope in &random_ropes { - rope_b.append(rope.clone()) - } - }); - }); - } - group.finish(); - - let mut group = c.benchmark_group("slice"); - for size in sizes.iter() { - group.throughput(Throughput::Bytes(*size as u64)); - group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| { - let mut rng = StdRng::seed_from_u64(SEED); - let rope = generate_random_rope(&mut rng, *size); - - b.iter_batched( - || generate_random_rope_ranges(&mut rng, &rope), - |ranges| { - for range in ranges.iter() { - rope.slice(range.clone()); - } - }, - BatchSize::SmallInput, - ); - }); - } - group.finish(); - - let mut group = c.benchmark_group("bytes_in_range"); - for size in sizes.iter() { - group.throughput(Throughput::Bytes(*size as u64)); - group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| { - let mut rng = StdRng::seed_from_u64(SEED); - let rope = generate_random_rope(&mut rng, *size); - - b.iter_batched( - || generate_random_rope_ranges(&mut rng, &rope), - |ranges| { - for range in ranges.iter() { - let bytes = rope.bytes_in_range(range.clone()); - assert!(bytes.into_iter().count() > 0); - } - }, - BatchSize::SmallInput, - ); - }); - } - group.finish(); - - let mut group = c.benchmark_group("chars"); - for size in sizes.iter() { - group.throughput(Throughput::Bytes(*size as u64)); - group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| { - let mut rng = StdRng::seed_from_u64(SEED); - let rope = generate_random_rope(&mut rng, *size); - - b.iter(|| { - let chars = rope.chars().count(); - assert!(chars > 0); - }); - }); - } - group.finish(); - - let mut group = c.benchmark_group("clip_point"); - for size in sizes.iter() { - group.throughput(Throughput::Bytes(*size as u64)); - group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| { - let mut rng = StdRng::seed_from_u64(SEED); - let rope = generate_random_rope(&mut rng, *size); - - b.iter_batched( - || generate_random_rope_points(&mut rng, &rope), - |offsets| { - for offset in offsets.iter() { - black_box(rope.clip_point(*offset, Bias::Left)); - black_box(rope.clip_point(*offset, Bias::Right)); - } - }, - BatchSize::SmallInput, - ); - }); - } - group.finish(); - - let mut group = c.benchmark_group("point_to_offset"); - for size in sizes.iter() { - group.throughput(Throughput::Bytes(*size as u64)); - group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| { - let mut rng = StdRng::seed_from_u64(SEED); - let rope = generate_random_rope(&mut rng, *size); - - b.iter_batched( - || generate_random_rope_points(&mut rng, &rope), - |offsets| { - for offset in offsets.iter() { - black_box(rope.point_to_offset(*offset)); - } - }, - BatchSize::SmallInput, - ); - }); - } - group.finish(); - - let mut group = c.benchmark_group("cursor"); - for size in sizes.iter() { - group.throughput(Throughput::Bytes(*size as u64)); - group.bench_with_input(BenchmarkId::from_parameter(size), &size, |b, &size| { - let mut rng = StdRng::seed_from_u64(SEED); - let rope = generate_random_rope(&mut rng, *size); - - b.iter_batched( - || { - let num_points = rope.len() / 10; - - let mut points = Vec::new(); - for _ in 0..num_points { - points.push(rng.random_range(0..rope.len())); - } - points - }, - |offsets| { - for offset in offsets.iter() { - black_box(rope.cursor(*offset)); - } - }, - BatchSize::SmallInput, - ); - }); - } - group.finish(); - - let mut group = c.benchmark_group("append many"); - group.throughput(Throughput::Bytes(128 * 100_000)); - - group.bench_function("small to large", |b| { - b.iter(|| { - let mut rope = Rope::new(); - let small = Rope::from("A".repeat(128)); - for _ in 0..100_000 { - rope.append(small.clone()); - } - assert_eq!(rope.len(), 128 * 100_000); - }); - }); - - group.bench_function("large to small", |b| { - b.iter(|| { - let mut rope = Rope::new(); - let small = Rope::from("A".repeat(128)); - for _ in 0..100_000 { - let large = rope; - rope = small.clone(); - rope.append(large); - } - assert_eq!(rope.len(), 128 * 100_000); - }); - }); - - group.finish(); -} - -criterion_group!(benches, rope_benchmarks); -criterion_main!(benches); diff --git a/crates/rope/src/chunk.rs b/crates/rope/src/chunk.rs deleted file mode 100644 index a2a8e8d58d..0000000000 --- a/crates/rope/src/chunk.rs +++ /dev/null @@ -1,1141 +0,0 @@ -use crate::{OffsetUtf16, Point, PointUtf16, TextSummary, Unclipped}; -use arrayvec::ArrayString; -use std::{cmp, ops::Range}; -use sum_tree::Bias; -use unicode_segmentation::GraphemeCursor; -use util::debug_panic; - -#[cfg(not(all(test, not(rust_analyzer))))] -pub(crate) type Bitmap = u128; -#[cfg(all(test, not(rust_analyzer)))] -pub(crate) type Bitmap = u16; - -pub(crate) const MIN_BASE: usize = MAX_BASE / 2; -pub(crate) const MAX_BASE: usize = Bitmap::BITS as usize; - -#[derive(Clone, Debug, Default)] -pub struct Chunk { - /// If bit[i] is set, then the character at index i is the start of a UTF-8 character in the - /// text. - chars: Bitmap, - /// The number of set bits is the number of UTF-16 code units it would take to represent the - /// text. - /// - /// Bit[i] is set if text[i] is the start of a UTF-8 character. If the character would - /// take two UTF-16 code units, then bit[i+1] is also set. (Rust chars never take more - /// than two UTF-16 code units.) - chars_utf16: Bitmap, - /// If bit[i] is set, then the character at index i is an ascii newline. - newlines: Bitmap, - /// If bit[i] is set, then the character at index i is an ascii tab. - tabs: Bitmap, - pub text: ArrayString, -} - -#[inline(always)] -const fn saturating_shl_mask(offset: u32) -> Bitmap { - (1 as Bitmap).unbounded_shl(offset).wrapping_sub(1) -} - -#[inline(always)] -const fn saturating_shr_mask(offset: u32) -> Bitmap { - !Bitmap::MAX.unbounded_shr(offset) -} - -impl Chunk { - pub const MASK_BITS: usize = Bitmap::BITS as usize; - - #[inline(always)] - pub fn new(text: &str) -> Self { - let mut this = Chunk::default(); - this.push_str(text); - this - } - - #[inline(always)] - pub fn push_str(&mut self, text: &str) { - for (char_ix, c) in text.char_indices() { - let ix = self.text.len() + char_ix; - self.chars |= 1 << ix; - self.chars_utf16 |= 1 << ix; - self.chars_utf16 |= (c.len_utf16() as Bitmap) << ix; - self.newlines |= ((c == '\n') as Bitmap) << ix; - self.tabs |= ((c == '\t') as Bitmap) << ix; - } - self.text.push_str(text); - } - - #[inline(always)] - pub fn append(&mut self, slice: ChunkSlice) { - if slice.is_empty() { - return; - }; - - let base_ix = self.text.len(); - self.chars |= slice.chars << base_ix; - self.chars_utf16 |= slice.chars_utf16 << base_ix; - self.newlines |= slice.newlines << base_ix; - self.tabs |= slice.tabs << base_ix; - self.text.push_str(slice.text); - } - - #[inline(always)] - pub fn as_slice(&self) -> ChunkSlice<'_> { - ChunkSlice { - chars: self.chars, - chars_utf16: self.chars_utf16, - newlines: self.newlines, - tabs: self.tabs, - text: &self.text, - } - } - - #[inline(always)] - pub fn slice(&self, range: Range) -> ChunkSlice<'_> { - self.as_slice().slice(range) - } - - #[inline(always)] - pub fn chars(&self) -> Bitmap { - self.chars - } - - pub fn tabs(&self) -> Bitmap { - self.tabs - } - - #[inline(always)] - pub fn is_char_boundary(&self, offset: usize) -> bool { - (1 as Bitmap).unbounded_shl(offset as u32) & self.chars != 0 || offset == self.text.len() - } - - pub fn floor_char_boundary(&self, index: usize) -> usize { - if index >= self.text.len() { - self.text.len() - } else { - let mut i = index; - while i > 0 { - if util::is_utf8_char_boundary(self.text.as_bytes()[i]) { - break; - } - i -= 1; - } - - i - } - } - - #[track_caller] - #[inline(always)] - pub fn assert_char_boundary(&self, offset: usize) -> bool { - if self.is_char_boundary(offset) { - return true; - } - if PANIC { - panic_char_boundary(&self.text, offset); - } else { - log_err_char_boundary(&self.text, offset); - false - } - } -} - -#[derive(Clone, Copy, Debug)] -pub struct ChunkSlice<'a> { - chars: Bitmap, - chars_utf16: Bitmap, - newlines: Bitmap, - tabs: Bitmap, - text: &'a str, -} - -impl Into for ChunkSlice<'_> { - fn into(self) -> Chunk { - Chunk { - chars: self.chars, - chars_utf16: self.chars_utf16, - newlines: self.newlines, - tabs: self.tabs, - text: self.text.try_into().unwrap(), - } - } -} - -impl<'a> ChunkSlice<'a> { - #[inline(always)] - pub fn is_empty(&self) -> bool { - self.text.is_empty() - } - - #[inline(always)] - pub fn is_char_boundary(&self, offset: usize) -> bool { - (1 as Bitmap).unbounded_shl(offset as u32) & self.chars != 0 || offset == self.text.len() - } - - #[inline(always)] - pub fn split_at(self, mid: usize) -> (ChunkSlice<'a>, ChunkSlice<'a>) { - if mid == MAX_BASE { - let left = self; - let right = ChunkSlice { - chars: 0, - chars_utf16: 0, - newlines: 0, - tabs: 0, - text: "", - }; - (left, right) - } else { - let mask = ((1 as Bitmap) << mid) - 1; - let (left_text, right_text) = self.text.split_at(mid); - let left = ChunkSlice { - chars: self.chars & mask, - chars_utf16: self.chars_utf16 & mask, - newlines: self.newlines & mask, - tabs: self.tabs & mask, - text: left_text, - }; - let right = ChunkSlice { - chars: self.chars >> mid, - chars_utf16: self.chars_utf16 >> mid, - newlines: self.newlines >> mid, - tabs: self.tabs >> mid, - text: right_text, - }; - (left, right) - } - } - - #[inline(always)] - pub fn slice(self, mut range: Range) -> Self { - if range.start == MAX_BASE { - Self { - chars: 0, - chars_utf16: 0, - newlines: 0, - tabs: 0, - text: "", - } - } else { - if !self.assert_char_boundary::(range.start) { - range.start = self.text.ceil_char_boundary(range.start); - } - if !self.assert_char_boundary::(range.end) { - range.end = if range.end < range.start { - range.start - } else { - self.text.floor_char_boundary(range.end) - }; - } - let mask = (1 as Bitmap) - .unbounded_shl(range.end as u32) - .wrapping_sub(1); - Self { - chars: (self.chars & mask) >> range.start, - chars_utf16: (self.chars_utf16 & mask) >> range.start, - newlines: (self.newlines & mask) >> range.start, - tabs: (self.tabs & mask) >> range.start, - text: &self.text[range], - } - } - } - - #[inline(always)] - pub fn text_summary(&self) -> TextSummary { - let mut chars = 0; - let (longest_row, longest_row_chars) = self.longest_row(&mut chars); - TextSummary { - len: self.len(), - chars, - len_utf16: self.len_utf16(), - lines: self.lines(), - first_line_chars: self.first_line_chars(), - last_line_chars: self.last_line_chars(), - last_line_len_utf16: self.last_line_len_utf16(), - longest_row, - longest_row_chars, - } - } - - /// Get length in bytes - #[inline(always)] - pub fn len(&self) -> usize { - self.text.len() - } - - /// Get length in UTF-16 code units - #[inline(always)] - pub fn len_utf16(&self) -> OffsetUtf16 { - OffsetUtf16(self.chars_utf16.count_ones() as usize) - } - - /// Get point representing number of lines and length of last line - #[inline(always)] - pub fn lines(&self) -> Point { - let row = self.newlines.count_ones(); - let column = self.newlines.leading_zeros() - (Bitmap::BITS - self.text.len() as u32); - Point::new(row, column) - } - - /// Get number of chars in first line - #[inline(always)] - pub fn first_line_chars(&self) -> u32 { - (self.chars & saturating_shl_mask(self.newlines.trailing_zeros())).count_ones() - } - - /// Get number of chars in last line - #[inline(always)] - pub fn last_line_chars(&self) -> u32 { - (self.chars & saturating_shr_mask(self.newlines.leading_zeros())).count_ones() - } - - /// Get number of UTF-16 code units in last line - #[inline(always)] - pub fn last_line_len_utf16(&self) -> u32 { - (self.chars_utf16 & saturating_shr_mask(self.newlines.leading_zeros())).count_ones() - } - - /// Get the longest row in the chunk and its length in characters. - /// Calculate the total number of characters in the chunk along the way. - #[inline(always)] - pub fn longest_row(&self, total_chars: &mut usize) -> (u32, u32) { - let mut chars = self.chars; - let mut newlines = self.newlines; - *total_chars = 0; - let mut row = 0; - let mut longest_row = 0; - let mut longest_row_chars = 0; - while newlines > 0 { - let newline_ix = newlines.trailing_zeros(); - let row_chars = (chars & ((1 << newline_ix) - 1)).count_ones() as u8; - *total_chars += usize::from(row_chars); - if row_chars > longest_row_chars { - longest_row = row; - longest_row_chars = row_chars; - } - - newlines >>= newline_ix; - newlines >>= 1; - chars >>= newline_ix; - chars >>= 1; - row += 1; - *total_chars += 1; - } - - let row_chars = chars.count_ones() as u8; - *total_chars += usize::from(row_chars); - if row_chars > longest_row_chars { - (row, row_chars as u32) - } else { - (longest_row, longest_row_chars as u32) - } - } - - #[inline(always)] - pub fn offset_to_point(&self, offset: usize) -> Point { - let mask = (1 as Bitmap).unbounded_shl(offset as u32).wrapping_sub(1); - let row = (self.newlines & mask).count_ones(); - let newline_ix = Bitmap::BITS - (self.newlines & mask).leading_zeros(); - let column = (offset - newline_ix as usize) as u32; - Point::new(row, column) - } - - #[inline(always)] - pub fn point_to_offset(&self, point: Point) -> usize { - if point.row > self.lines().row { - debug_panic!( - "point {:?} extends beyond rows for string {:?}", - point, - self.text - ); - return self.len(); - } - - let row_offset_range = self.offset_range_for_row(point.row); - if point.column > row_offset_range.len() as u32 { - debug_panic!( - "point {:?} extends beyond row for string {:?}", - point, - self.text - ); - row_offset_range.end - } else { - row_offset_range.start + point.column as usize - } - } - - #[track_caller] - #[inline(always)] - pub fn assert_char_boundary(&self, offset: usize) -> bool { - if self.is_char_boundary(offset) { - return true; - } - if PANIC { - panic_char_boundary(self.text, offset); - } else { - log_err_char_boundary(self.text, offset); - false - } - } - - pub fn floor_char_boundary(&self, index: usize) -> usize { - self.text.floor_char_boundary(index) - } - - #[inline(always)] - pub fn point_to_offset_utf16(&self, point: Point) -> OffsetUtf16 { - if point.row > self.lines().row { - debug_panic!( - "point {:?} extends beyond rows for string {:?}", - point, - self.text - ); - return self.len_utf16(); - } - self.offset_to_offset_utf16(self.point_to_offset(point)) - } - - #[inline(always)] - pub fn offset_to_offset_utf16(&self, offset: usize) -> OffsetUtf16 { - let mask = (1 as Bitmap).unbounded_shl(offset as u32).wrapping_sub(1); - OffsetUtf16((self.chars_utf16 & mask).count_ones() as usize) - } - - #[inline(always)] - pub fn offset_utf16_to_offset(&self, target: OffsetUtf16) -> usize { - if target.0 == 0 { - 0 - } else { - #[cfg(not(test))] - let chars_utf16 = self.chars_utf16; - #[cfg(test)] - let chars_utf16 = self.chars_utf16 as u128; - let ix = nth_set_bit(chars_utf16, target.0) + 1; - if ix == MAX_BASE { - MAX_BASE - } else { - let utf8_additional_len = cmp::min( - (self.chars_utf16 >> ix).trailing_zeros() as usize, - self.text.len() - ix, - ); - ix + utf8_additional_len - } - } - } - - #[inline(always)] - pub fn offset_to_point_utf16(&self, offset: usize) -> PointUtf16 { - let mask = saturating_shl_mask(offset as u32); - let row = (self.newlines & saturating_shl_mask(offset as u32)).count_ones(); - let newline_ix = Bitmap::BITS - (self.newlines & mask).leading_zeros(); - let column = if newline_ix as usize == MAX_BASE { - 0 - } else { - ((self.chars_utf16 & mask) >> newline_ix).count_ones() - }; - PointUtf16::new(row, column) - } - - #[inline(always)] - pub fn point_to_point_utf16(&self, point: Point) -> PointUtf16 { - self.offset_to_point_utf16(self.point_to_offset(point)) - } - - #[inline(always)] - pub fn point_utf16_to_offset(&self, point: PointUtf16, clip: bool) -> usize { - let lines = self.lines(); - if point.row > lines.row { - if !clip { - debug_panic!( - "point {:?} is beyond this chunk's extent {:?}", - point, - self.text - ); - } - return self.len(); - } - - let row_offset_range = self.offset_range_for_row(point.row); - let line = self.slice(row_offset_range.clone()); - if point.column > line.last_line_len_utf16() { - if !clip { - debug_panic!( - "point {:?} is beyond the end of the line in chunk {:?}", - point, - self.text - ); - } - return line.len(); - } - - let mut offset = row_offset_range.start; - if point.column > 0 { - offset += line.offset_utf16_to_offset(OffsetUtf16(point.column as usize)); - if !self.text.is_char_boundary(offset) { - offset -= 1; - while !self.text.is_char_boundary(offset) { - offset -= 1; - } - if !clip { - debug_panic!( - "point {:?} is within character in chunk {:?}", - point, - self.text, - ); - } - } - } - offset - } - - #[inline(always)] - pub fn unclipped_point_utf16_to_point(&self, point: Unclipped) -> Point { - let max_point = self.lines(); - if point.0.row > max_point.row { - return max_point; - } - - let row_offset_range = self.offset_range_for_row(point.0.row); - let line = self.slice(row_offset_range); - if point.0.column == 0 { - Point::new(point.0.row, 0) - } else if point.0.column >= line.len_utf16().0 as u32 { - Point::new(point.0.row, line.len() as u32) - } else { - let mut column = line.offset_utf16_to_offset(OffsetUtf16(point.0.column as usize)); - while !line.text.is_char_boundary(column) { - column -= 1; - } - Point::new(point.0.row, column as u32) - } - } - - #[inline(always)] - pub fn clip_point(&self, point: Point, bias: Bias) -> Point { - let max_point = self.lines(); - if point.row > max_point.row { - return max_point; - } - - let line = self.slice(self.offset_range_for_row(point.row)); - if point.column == 0 { - point - } else if point.column >= line.len() as u32 { - Point::new(point.row, line.len() as u32) - } else { - let mut column = point.column as usize; - let bytes = line.text.as_bytes(); - if bytes[column - 1] < 128 && bytes[column] < 128 { - return Point::new(point.row, column as u32); - } - - let mut grapheme_cursor = GraphemeCursor::new(column, bytes.len(), true); - loop { - if line.is_char_boundary(column) - && grapheme_cursor.is_boundary(line.text, 0).unwrap_or(false) - { - break; - } - - match bias { - Bias::Left => column -= 1, - Bias::Right => column += 1, - } - grapheme_cursor.set_cursor(column); - } - Point::new(point.row, column as u32) - } - } - - #[inline(always)] - pub fn clip_point_utf16(&self, point: Unclipped, bias: Bias) -> PointUtf16 { - let max_point = self.lines(); - if point.0.row > max_point.row { - PointUtf16::new(max_point.row, self.last_line_len_utf16()) - } else { - let line = self.slice(self.offset_range_for_row(point.0.row)); - let column = line.clip_offset_utf16(OffsetUtf16(point.0.column as usize), bias); - PointUtf16::new(point.0.row, column.0 as u32) - } - } - - #[inline(always)] - pub fn clip_offset_utf16(&self, target: OffsetUtf16, bias: Bias) -> OffsetUtf16 { - if target == OffsetUtf16::default() { - OffsetUtf16::default() - } else if target >= self.len_utf16() { - self.len_utf16() - } else { - let mut offset = self.offset_utf16_to_offset(target); - while !self.text.is_char_boundary(offset) { - if bias == Bias::Left { - offset -= 1; - } else { - offset += 1; - } - } - self.offset_to_offset_utf16(offset) - } - } - - #[inline(always)] - fn offset_range_for_row(&self, row: u32) -> Range { - let row_start = if row > 0 { - #[cfg(not(test))] - let newlines = self.newlines; - #[cfg(test)] - let newlines = self.newlines as u128; - nth_set_bit(newlines, row as usize) + 1 - } else { - 0 - }; - let row_len = if row_start == MAX_BASE { - 0 - } else { - cmp::min( - (self.newlines >> row_start).trailing_zeros(), - (self.text.len() - row_start) as u32, - ) - }; - row_start..row_start + row_len as usize - } - - #[inline(always)] - pub fn tabs(&self) -> Tabs { - Tabs { - tabs: self.tabs, - chars: self.chars, - } - } -} - -pub struct Tabs { - tabs: Bitmap, - chars: Bitmap, -} - -#[derive(Debug, PartialEq, Eq)] -pub struct TabPosition { - pub byte_offset: usize, - pub char_offset: usize, -} - -impl Iterator for Tabs { - type Item = TabPosition; - - fn next(&mut self) -> Option { - if self.tabs == 0 { - return None; - } - - let tab_offset = self.tabs.trailing_zeros() as usize; - let chars_mask = (1 << tab_offset) - 1; - let char_offset = (self.chars & chars_mask).count_ones() as usize; - - // Since tabs are 1 byte the tab offset is the same as the byte offset - let position = TabPosition { - byte_offset: tab_offset, - char_offset, - }; - // Remove the tab we've just seen - self.tabs ^= 1 << tab_offset; - - Some(position) - } -} - -/// Finds the n-th bit that is set to 1. -#[inline(always)] -fn nth_set_bit(v: u128, n: usize) -> usize { - let low = v as u64; - let high = (v >> 64) as u64; - - let low_count = low.count_ones() as usize; - if n > low_count { - 64 + nth_set_bit_u64(high, (n - low_count) as u64) as usize - } else { - nth_set_bit_u64(low, n as u64) as usize - } -} - -#[cold] -#[inline(never)] -#[track_caller] -fn panic_char_boundary(text: &str, offset: usize) -> ! { - if offset > text.len() { - panic!( - "byte index {} is out of bounds of `{:?}` (length: {})", - offset, - text, - text.len() - ); - } - // find the character - let char_start = text.floor_char_boundary(offset); - // `char_start` must be less than len and a char boundary - let ch = text.get(char_start..).unwrap().chars().next().unwrap(); - let char_range = char_start..char_start + ch.len_utf8(); - panic!( - "byte index {} is not a char boundary; it is inside {:?} (bytes {:?})", - offset, ch, char_range, - ); -} - -#[cold] -#[inline(never)] -#[track_caller] -fn log_err_char_boundary(text: &str, offset: usize) { - if offset > text.len() { - log::error!( - "byte index {} is out of bounds of `{:?}` (length: {})", - offset, - text, - text.len() - ); - } - // find the character - let char_start = text.floor_char_boundary(offset); - // `char_start` must be less than len and a char boundary - let ch = text.get(char_start..).unwrap().chars().next().unwrap(); - let char_range = char_start..char_start + ch.len_utf8(); - log::error!( - "byte index {} is not a char boundary; it is inside {:?} (bytes {:?})", - offset, - ch, - char_range, - ); -} - -#[inline(always)] -fn nth_set_bit_u64(v: u64, mut n: u64) -> u64 { - let v = v.reverse_bits(); - let mut s: u64 = 64; - - // Parallel bit count intermediates - let a = v - ((v >> 1) & (u64::MAX / 3)); - let b = (a & (u64::MAX / 5)) + ((a >> 2) & (u64::MAX / 5)); - let c = (b + (b >> 4)) & (u64::MAX / 0x11); - let d = (c + (c >> 8)) & (u64::MAX / 0x101); - - // Branchless select - let t = (d >> 32) + (d >> 48); - s -= (t.wrapping_sub(n) & 256) >> 3; - n -= t & (t.wrapping_sub(n) >> 8); - - let t = (d >> (s - 16)) & 0xff; - s -= (t.wrapping_sub(n) & 256) >> 4; - n -= t & (t.wrapping_sub(n) >> 8); - - let t = (c >> (s - 8)) & 0xf; - s -= (t.wrapping_sub(n) & 256) >> 5; - n -= t & (t.wrapping_sub(n) >> 8); - - let t = (b >> (s - 4)) & 0x7; - s -= (t.wrapping_sub(n) & 256) >> 6; - n -= t & (t.wrapping_sub(n) >> 8); - - let t = (a >> (s - 2)) & 0x3; - s -= (t.wrapping_sub(n) & 256) >> 7; - n -= t & (t.wrapping_sub(n) >> 8); - - let t = (v >> (s - 1)) & 0x1; - s -= (t.wrapping_sub(n) & 256) >> 8; - - 65 - s - 1 -} - -#[cfg(test)] -mod tests { - use super::*; - use rand::prelude::*; - use util::RandomCharIter; - - #[gpui::test(iterations = 100)] - fn test_random_chunks(mut rng: StdRng) { - let text = random_string_with_utf8_len(&mut rng, MAX_BASE); - log::info!("Chunk: {:?}", text); - let chunk = Chunk::new(&text); - verify_chunk(chunk.as_slice(), &text); - - // Verify Chunk::chars() bitmap - let expected_chars = char_offsets(&text) - .into_iter() - .inspect(|i| assert!(*i < MAX_BASE)) - .fold(0 as Bitmap, |acc, i| acc | (1 << i)); - assert_eq!(chunk.chars(), expected_chars); - - for _ in 0..10 { - let mut start = rng.random_range(0..=chunk.text.len()); - let mut end = rng.random_range(start..=chunk.text.len()); - while !chunk.text.is_char_boundary(start) { - start -= 1; - } - while !chunk.text.is_char_boundary(end) { - end -= 1; - } - let range = start..end; - log::info!("Range: {:?}", range); - let text_slice = &text[range.clone()]; - let chunk_slice = chunk.slice(range); - verify_chunk(chunk_slice, text_slice); - } - } - - #[gpui::test(iterations = 100)] - fn test_split_chunk_slice(mut rng: StdRng) { - let text = &random_string_with_utf8_len(&mut rng, MAX_BASE); - let chunk = Chunk::new(text); - let offset = char_offsets_with_end(text) - .into_iter() - .choose(&mut rng) - .unwrap(); - let (a, b) = chunk.as_slice().split_at(offset); - let (a_str, b_str) = text.split_at(offset); - verify_chunk(a, a_str); - verify_chunk(b, b_str); - } - - #[gpui::test(iterations = 1000)] - fn test_nth_set_bit_random(mut rng: StdRng) { - let set_count = rng.random_range(0..=128); - let mut set_bits = (0..128).choose_multiple(&mut rng, set_count); - set_bits.sort(); - let mut n = 0; - for ix in set_bits.iter().copied() { - n |= 1 << ix; - } - - for (mut ix, position) in set_bits.into_iter().enumerate() { - ix += 1; - assert_eq!( - nth_set_bit(n, ix), - position, - "nth_set_bit({:0128b}, {})", - n, - ix - ); - } - } - - /// Returns a (biased) random string whose UTF-8 length is no more than `len`. - fn random_string_with_utf8_len(rng: &mut StdRng, len: usize) -> String { - let mut str = String::new(); - let mut chars = RandomCharIter::new(rng); - loop { - let ch = chars.next().unwrap(); - if str.len() + ch.len_utf8() > len { - break; - } - str.push(ch); - } - str - } - - #[gpui::test(iterations = 1000)] - fn test_append_random_strings(mut rng: StdRng) { - let len1 = rng.random_range(0..=MAX_BASE); - let len2 = rng.random_range(0..=MAX_BASE).saturating_sub(len1); - let str1 = random_string_with_utf8_len(&mut rng, len1); - let str2 = random_string_with_utf8_len(&mut rng, len2); - let mut chunk1 = Chunk::new(&str1); - let chunk2 = Chunk::new(&str2); - let char_offsets = char_offsets_with_end(&str2); - let start_index = rng.random_range(0..char_offsets.len()); - let start_offset = char_offsets[start_index]; - let end_offset = char_offsets[rng.random_range(start_index..char_offsets.len())]; - chunk1.append(chunk2.slice(start_offset..end_offset)); - verify_chunk(chunk1.as_slice(), &(str1 + &str2[start_offset..end_offset])); - } - - /// Return the byte offsets for each character in a string. - /// - /// These are valid offsets to split the string. - fn char_offsets(text: &str) -> Vec { - text.char_indices().map(|(i, _c)| i).collect() - } - - /// Return the byte offsets for each character in a string, plus the offset - /// past the end of the string. - fn char_offsets_with_end(text: &str) -> Vec { - let mut v = char_offsets(text); - v.push(text.len()); - v - } - - fn verify_chunk(chunk: ChunkSlice<'_>, text: &str) { - let mut offset = 0; - let mut offset_utf16 = OffsetUtf16(0); - let mut point = Point::zero(); - let mut point_utf16 = PointUtf16::zero(); - - log::info!("Verifying chunk {:?}", text); - assert_eq!(chunk.offset_to_point(0), Point::zero()); - - let mut expected_tab_positions = Vec::new(); - - for (char_offset, c) in text.chars().enumerate() { - let expected_point = chunk.offset_to_point(offset); - assert_eq!(point, expected_point, "mismatch at offset {}", offset); - assert_eq!( - chunk.point_to_offset(point), - offset, - "mismatch at point {:?}", - point - ); - assert_eq!( - chunk.offset_to_offset_utf16(offset), - offset_utf16, - "mismatch at offset {}", - offset - ); - assert_eq!( - chunk.offset_utf16_to_offset(offset_utf16), - offset, - "mismatch at offset_utf16 {:?}", - offset_utf16 - ); - assert_eq!( - chunk.point_to_point_utf16(point), - point_utf16, - "mismatch at point {:?}", - point - ); - assert_eq!( - chunk.point_utf16_to_offset(point_utf16, false), - offset, - "mismatch at point_utf16 {:?}", - point_utf16 - ); - assert_eq!( - chunk.unclipped_point_utf16_to_point(Unclipped(point_utf16)), - point, - "mismatch for unclipped_point_utf16_to_point at {:?}", - point_utf16 - ); - - assert_eq!( - chunk.clip_point(point, Bias::Left), - point, - "incorrect left clip at {:?}", - point - ); - assert_eq!( - chunk.clip_point(point, Bias::Right), - point, - "incorrect right clip at {:?}", - point - ); - - for i in 1..c.len_utf8() { - let test_point = Point::new(point.row, point.column + i as u32); - assert_eq!( - chunk.clip_point(test_point, Bias::Left), - point, - "incorrect left clip within multi-byte char at {:?}", - test_point - ); - assert_eq!( - chunk.clip_point(test_point, Bias::Right), - Point::new(point.row, point.column + c.len_utf8() as u32), - "incorrect right clip within multi-byte char at {:?}", - test_point - ); - } - - for i in 1..c.len_utf16() { - let test_point = Unclipped(PointUtf16::new( - point_utf16.row, - point_utf16.column + i as u32, - )); - assert_eq!( - chunk.unclipped_point_utf16_to_point(test_point), - point, - "incorrect unclipped_point_utf16_to_point within multi-byte char at {:?}", - test_point - ); - assert_eq!( - chunk.clip_point_utf16(test_point, Bias::Left), - point_utf16, - "incorrect left clip_point_utf16 within multi-byte char at {:?}", - test_point - ); - assert_eq!( - chunk.clip_point_utf16(test_point, Bias::Right), - PointUtf16::new(point_utf16.row, point_utf16.column + c.len_utf16() as u32), - "incorrect right clip_point_utf16 within multi-byte char at {:?}", - test_point - ); - - let test_offset = OffsetUtf16(offset_utf16.0 + i); - assert_eq!( - chunk.clip_offset_utf16(test_offset, Bias::Left), - offset_utf16, - "incorrect left clip_offset_utf16 within multi-byte char at {:?}", - test_offset - ); - assert_eq!( - chunk.clip_offset_utf16(test_offset, Bias::Right), - OffsetUtf16(offset_utf16.0 + c.len_utf16()), - "incorrect right clip_offset_utf16 within multi-byte char at {:?}", - test_offset - ); - } - - if c == '\n' { - point.row += 1; - point.column = 0; - point_utf16.row += 1; - point_utf16.column = 0; - } else { - point.column += c.len_utf8() as u32; - point_utf16.column += c.len_utf16() as u32; - } - - if c == '\t' { - expected_tab_positions.push(TabPosition { - byte_offset: offset, - char_offset, - }); - } - - offset += c.len_utf8(); - offset_utf16.0 += c.len_utf16(); - } - - let final_point = chunk.offset_to_point(offset); - assert_eq!(point, final_point, "mismatch at final offset {}", offset); - assert_eq!( - chunk.point_to_offset(point), - offset, - "mismatch at point {:?}", - point - ); - assert_eq!( - chunk.offset_to_offset_utf16(offset), - offset_utf16, - "mismatch at offset {}", - offset - ); - assert_eq!( - chunk.offset_utf16_to_offset(offset_utf16), - offset, - "mismatch at offset_utf16 {:?}", - offset_utf16 - ); - assert_eq!( - chunk.point_to_point_utf16(point), - point_utf16, - "mismatch at final point {:?}", - point - ); - assert_eq!( - chunk.point_utf16_to_offset(point_utf16, false), - offset, - "mismatch at final point_utf16 {:?}", - point_utf16 - ); - assert_eq!( - chunk.unclipped_point_utf16_to_point(Unclipped(point_utf16)), - point, - "mismatch for unclipped_point_utf16_to_point at final point {:?}", - point_utf16 - ); - assert_eq!( - chunk.clip_point(point, Bias::Left), - point, - "incorrect left clip at final point {:?}", - point - ); - assert_eq!( - chunk.clip_point(point, Bias::Right), - point, - "incorrect right clip at final point {:?}", - point - ); - assert_eq!( - chunk.clip_point_utf16(Unclipped(point_utf16), Bias::Left), - point_utf16, - "incorrect left clip_point_utf16 at final point {:?}", - point_utf16 - ); - assert_eq!( - chunk.clip_point_utf16(Unclipped(point_utf16), Bias::Right), - point_utf16, - "incorrect right clip_point_utf16 at final point {:?}", - point_utf16 - ); - assert_eq!( - chunk.clip_offset_utf16(offset_utf16, Bias::Left), - offset_utf16, - "incorrect left clip_offset_utf16 at final offset {:?}", - offset_utf16 - ); - assert_eq!( - chunk.clip_offset_utf16(offset_utf16, Bias::Right), - offset_utf16, - "incorrect right clip_offset_utf16 at final offset {:?}", - offset_utf16 - ); - - // Verify length methods - assert_eq!(chunk.len(), text.len()); - assert_eq!( - chunk.len_utf16().0, - text.chars().map(|c| c.len_utf16()).sum::() - ); - - // Verify line counting - let lines = chunk.lines(); - let mut newline_count = 0; - let mut last_line_len = 0; - for c in text.chars() { - if c == '\n' { - newline_count += 1; - last_line_len = 0; - } else { - last_line_len += c.len_utf8() as u32; - } - } - assert_eq!(lines, Point::new(newline_count, last_line_len)); - - // Verify first/last line chars - if !text.is_empty() { - let first_line = text.split('\n').next().unwrap(); - assert_eq!(chunk.first_line_chars(), first_line.chars().count() as u32); - - let last_line = text.split('\n').next_back().unwrap(); - assert_eq!(chunk.last_line_chars(), last_line.chars().count() as u32); - assert_eq!( - chunk.last_line_len_utf16(), - last_line.chars().map(|c| c.len_utf16() as u32).sum::() - ); - } - - // Verify longest row - let (longest_row, longest_chars) = chunk.longest_row(&mut 0); - let mut max_chars = 0; - let mut current_row = 0; - let mut current_chars = 0; - let mut max_row = 0; - - for c in text.chars() { - if c == '\n' { - if current_chars > max_chars { - max_chars = current_chars; - max_row = current_row; - } - current_row += 1; - current_chars = 0; - } else { - current_chars += 1; - } - } - - if current_chars > max_chars { - max_chars = current_chars; - max_row = current_row; - } - - assert_eq!((max_row, max_chars as u32), (longest_row, longest_chars)); - assert_eq!(chunk.tabs().collect::>(), expected_tab_positions); - } -} diff --git a/crates/rope/src/offset_utf16.rs b/crates/rope/src/offset_utf16.rs deleted file mode 100644 index 1223fbbe38..0000000000 --- a/crates/rope/src/offset_utf16.rs +++ /dev/null @@ -1,49 +0,0 @@ -use std::ops::{Add, AddAssign, Sub}; - -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)] -pub struct OffsetUtf16(pub usize); - -impl<'a> Add<&'a Self> for OffsetUtf16 { - type Output = Self; - - fn add(self, other: &'a Self) -> Self::Output { - Self(self.0 + other.0) - } -} - -impl Add for OffsetUtf16 { - type Output = Self; - - fn add(self, other: Self) -> Self::Output { - Self(self.0 + other.0) - } -} - -impl<'a> Sub<&'a Self> for OffsetUtf16 { - type Output = Self; - - fn sub(self, other: &'a Self) -> Self::Output { - debug_assert!(*other <= self); - Self(self.0 - other.0) - } -} - -impl Sub for OffsetUtf16 { - type Output = OffsetUtf16; - - fn sub(self, other: Self) -> Self::Output { - Self(self.0 - other.0) - } -} - -impl<'a> AddAssign<&'a Self> for OffsetUtf16 { - fn add_assign(&mut self, other: &'a Self) { - self.0 += other.0; - } -} - -impl AddAssign for OffsetUtf16 { - fn add_assign(&mut self, other: Self) { - self.0 += other.0; - } -} diff --git a/crates/rope/src/point.rs b/crates/rope/src/point.rs deleted file mode 100644 index a2491f6b0e..0000000000 --- a/crates/rope/src/point.rs +++ /dev/null @@ -1,146 +0,0 @@ -use std::{ - cmp::Ordering, - fmt::{self, Debug}, - ops::{Add, AddAssign, Range, Sub}, -}; - -/// A zero-indexed point in a text buffer consisting of a row and column. -#[derive(Clone, Copy, Default, Eq, PartialEq, Hash)] -pub struct Point { - pub row: u32, - pub column: u32, -} - -impl Debug for Point { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Point({}:{})", self.row, self.column) - } -} - -impl Point { - pub const MAX: Self = Self { - row: u32::MAX, - column: u32::MAX, - }; - - pub fn new(row: u32, column: u32) -> Self { - Point { row, column } - } - - pub fn row_range(range: Range) -> Range { - Point { - row: range.start, - column: 0, - }..Point { - row: range.end, - column: 0, - } - } - - pub fn zero() -> Self { - Point::new(0, 0) - } - - pub fn parse_str(s: &str) -> Self { - let mut point = Self::zero(); - for (row, line) in s.split('\n').enumerate() { - point.row = row as u32; - point.column = line.len() as u32; - } - point - } - - pub fn is_zero(&self) -> bool { - self.row == 0 && self.column == 0 - } - - pub fn saturating_sub(self, other: Self) -> Self { - if self < other { - Self::zero() - } else { - self - other - } - } -} - -impl<'a> Add<&'a Self> for Point { - type Output = Point; - - fn add(self, other: &'a Self) -> Self::Output { - self + *other - } -} - -impl Add for Point { - type Output = Point; - - fn add(self, other: Self) -> Self::Output { - if other.row == 0 { - Point::new(self.row, self.column + other.column) - } else { - Point::new(self.row + other.row, other.column) - } - } -} - -impl<'a> Sub<&'a Self> for Point { - type Output = Point; - - fn sub(self, other: &'a Self) -> Self::Output { - self - *other - } -} - -impl Sub for Point { - type Output = Point; - - fn sub(self, other: Self) -> Self::Output { - debug_assert!(other <= self); - - if self.row == other.row { - Point::new(0, self.column - other.column) - } else { - Point::new(self.row - other.row, self.column) - } - } -} - -impl<'a> AddAssign<&'a Self> for Point { - fn add_assign(&mut self, other: &'a Self) { - *self += *other; - } -} - -impl AddAssign for Point { - fn add_assign(&mut self, other: Self) { - if other.row == 0 { - self.column += other.column; - } else { - self.row += other.row; - self.column = other.column; - } - } -} - -impl PartialOrd for Point { - fn partial_cmp(&self, other: &Point) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for Point { - #[cfg(target_pointer_width = "64")] - fn cmp(&self, other: &Point) -> Ordering { - let a = ((self.row as usize) << 32) | self.column as usize; - let b = ((other.row as usize) << 32) | other.column as usize; - a.cmp(&b) - } - - #[cfg(target_pointer_width = "32")] - fn cmp(&self, other: &Point) -> Ordering { - match self.row.cmp(&other.row) { - Ordering::Equal => self.column.cmp(&other.column), - comparison @ _ => comparison, - } - } -} diff --git a/crates/rope/src/point_utf16.rs b/crates/rope/src/point_utf16.rs deleted file mode 100644 index 096c2defa4..0000000000 --- a/crates/rope/src/point_utf16.rs +++ /dev/null @@ -1,119 +0,0 @@ -use std::{ - cmp::Ordering, - ops::{Add, AddAssign, Sub}, -}; - -#[derive(Clone, Copy, Default, Eq, PartialEq, Debug, Hash)] -pub struct PointUtf16 { - pub row: u32, - pub column: u32, -} - -impl PointUtf16 { - pub const MAX: Self = Self { - row: u32::MAX, - column: u32::MAX, - }; - - pub fn new(row: u32, column: u32) -> Self { - PointUtf16 { row, column } - } - - pub fn zero() -> Self { - PointUtf16::new(0, 0) - } - - pub fn is_zero(&self) -> bool { - self.row == 0 && self.column == 0 - } - - pub fn saturating_sub(self, other: Self) -> Self { - if self < other { - Self::zero() - } else { - self - other - } - } -} - -impl<'a> Add<&'a Self> for PointUtf16 { - type Output = PointUtf16; - - fn add(self, other: &'a Self) -> Self::Output { - self + *other - } -} - -impl Add for PointUtf16 { - type Output = PointUtf16; - - fn add(self, other: Self) -> Self::Output { - if other.row == 0 { - PointUtf16::new(self.row, self.column + other.column) - } else { - PointUtf16::new(self.row + other.row, other.column) - } - } -} - -impl<'a> Sub<&'a Self> for PointUtf16 { - type Output = PointUtf16; - - fn sub(self, other: &'a Self) -> Self::Output { - self - *other - } -} - -impl Sub for PointUtf16 { - type Output = PointUtf16; - - fn sub(self, other: Self) -> Self::Output { - debug_assert!(other <= self); - - if self.row == other.row { - PointUtf16::new(0, self.column - other.column) - } else { - PointUtf16::new(self.row - other.row, self.column) - } - } -} - -impl<'a> AddAssign<&'a Self> for PointUtf16 { - fn add_assign(&mut self, other: &'a Self) { - *self += *other; - } -} - -impl AddAssign for PointUtf16 { - fn add_assign(&mut self, other: Self) { - if other.row == 0 { - self.column += other.column; - } else { - self.row += other.row; - self.column = other.column; - } - } -} - -impl PartialOrd for PointUtf16 { - fn partial_cmp(&self, other: &PointUtf16) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for PointUtf16 { - #[cfg(target_pointer_width = "64")] - fn cmp(&self, other: &PointUtf16) -> Ordering { - let a = ((self.row as usize) << 32) | self.column as usize; - let b = ((other.row as usize) << 32) | other.column as usize; - a.cmp(&b) - } - - #[cfg(target_pointer_width = "32")] - fn cmp(&self, other: &PointUtf16) -> Ordering { - match self.row.cmp(&other.row) { - Ordering::Equal => self.column.cmp(&other.column), - comparison @ _ => comparison, - } - } -} diff --git a/crates/rope/src/rope.rs b/crates/rope/src/rope.rs deleted file mode 100644 index 50f9ba044d..0000000000 --- a/crates/rope/src/rope.rs +++ /dev/null @@ -1,2247 +0,0 @@ -mod chunk; -mod offset_utf16; -mod point; -mod point_utf16; -mod unclipped; - -use arrayvec::ArrayVec; -use rayon::iter::{IntoParallelIterator, ParallelIterator as _}; -use std::{ - cmp, fmt, io, mem, - ops::{self, AddAssign, Range}, - str, -}; -use sum_tree::{Bias, Dimension, Dimensions, SumTree}; -use ztracing::instrument; - -pub use chunk::{Chunk, ChunkSlice}; -pub use offset_utf16::OffsetUtf16; -pub use point::Point; -pub use point_utf16::PointUtf16; -pub use unclipped::Unclipped; - -use crate::chunk::Bitmap; - -#[derive(Clone, Default)] -pub struct Rope { - chunks: SumTree, -} - -impl Rope { - pub fn new() -> Self { - Self::default() - } - - /// Checks that `index`-th byte is the first byte in a UTF-8 code point - /// sequence or the end of the string. - /// - /// The start and end of the string (when `index == self.len()`) are - /// considered to be boundaries. - /// - /// Returns `false` if `index` is greater than `self.len()`. - pub fn is_char_boundary(&self, offset: usize) -> bool { - if self.chunks.is_empty() { - return offset == 0; - } - let (start, _, item) = self.chunks.find::((), &offset, Bias::Left); - let chunk_offset = offset - start; - item.map(|chunk| chunk.is_char_boundary(chunk_offset)) - .unwrap_or(false) - } - - #[track_caller] - #[inline(always)] - pub fn assert_char_boundary(&self, offset: usize) -> bool { - if self.chunks.is_empty() && offset == 0 { - return true; - } - let (start, _, item) = self.chunks.find::((), &offset, Bias::Left); - match item { - Some(chunk) => { - let chunk_offset = offset - start; - chunk.assert_char_boundary::(chunk_offset) - } - None if PANIC => { - panic!( - "byte index {} is out of bounds of rope (length: {})", - offset, - self.len() - ); - } - None => { - log::error!( - "byte index {} is out of bounds of rope (length: {})", - offset, - self.len() - ); - false - } - } - } - - pub fn floor_char_boundary(&self, index: usize) -> usize { - if index >= self.len() { - self.len() - } else { - let (start, _, item) = self.chunks.find::((), &index, Bias::Left); - let chunk_offset = index - start; - let lower_idx = item.map(|chunk| chunk.text.floor_char_boundary(chunk_offset)); - lower_idx.map_or_else(|| self.len(), |idx| start + idx) - } - } - - pub fn ceil_char_boundary(&self, index: usize) -> usize { - if index > self.len() { - self.len() - } else { - let (start, _, item) = self.chunks.find::((), &index, Bias::Left); - let chunk_offset = index - start; - let upper_idx = item.map(|chunk| chunk.text.ceil_char_boundary(chunk_offset)); - upper_idx.map_or_else(|| self.len(), |idx| start + idx) - } - } - - pub fn append(&mut self, rope: Rope) { - if let Some(chunk) = rope.chunks.first() - && (self - .chunks - .last() - .is_some_and(|c| c.text.len() < chunk::MIN_BASE) - || chunk.text.len() < chunk::MIN_BASE) - { - self.push_chunk(chunk.as_slice()); - - let mut chunks = rope.chunks.cursor::<()>(()); - chunks.next(); - chunks.next(); - self.chunks.append(chunks.suffix(), ()); - } else { - self.chunks.append(rope.chunks, ()); - } - self.check_invariants(); - } - - pub fn replace(&mut self, range: Range, text: &str) { - let mut new_rope = Rope::new(); - let mut cursor = self.cursor(0); - new_rope.append(cursor.slice(range.start)); - cursor.seek_forward(range.end); - new_rope.push(text); - new_rope.append(cursor.suffix()); - *self = new_rope; - } - - pub fn slice(&self, range: Range) -> Rope { - let mut cursor = self.cursor(0); - cursor.seek_forward(range.start); - cursor.slice(range.end) - } - - pub fn slice_rows(&self, range: Range) -> Rope { - // This would be more efficient with a forward advance after the first, but it's fine. - let start = self.point_to_offset(Point::new(range.start, 0)); - let end = self.point_to_offset(Point::new(range.end, 0)); - self.slice(start..end) - } - - pub fn push(&mut self, mut text: &str) { - self.chunks.update_last( - |last_chunk| { - let split_ix = if last_chunk.text.len() + text.len() <= chunk::MAX_BASE { - text.len() - } else { - let mut split_ix = cmp::min( - chunk::MIN_BASE.saturating_sub(last_chunk.text.len()), - text.len(), - ); - while !text.is_char_boundary(split_ix) { - split_ix += 1; - } - split_ix - }; - - let (suffix, remainder) = text.split_at(split_ix); - last_chunk.push_str(suffix); - text = remainder; - }, - (), - ); - - #[cfg(all(test, not(rust_analyzer)))] - const NUM_CHUNKS: usize = 16; - #[cfg(not(all(test, not(rust_analyzer))))] - const NUM_CHUNKS: usize = 4; - - // We accommodate for NUM_CHUNKS chunks of size MAX_BASE - // but given the chunk boundary can land within a character - // we need to accommodate for the worst case where every chunk gets cut short by up to 4 bytes - if text.len() > NUM_CHUNKS * chunk::MAX_BASE - NUM_CHUNKS * 4 { - return self.push_large(text); - } - // 16 is enough as otherwise we will hit the branch above - let mut new_chunks = ArrayVec::<_, NUM_CHUNKS>::new(); - - while !text.is_empty() { - let mut split_ix = cmp::min(chunk::MAX_BASE, text.len()); - while !text.is_char_boundary(split_ix) { - split_ix -= 1; - } - let (chunk, remainder) = text.split_at(split_ix); - new_chunks.push(chunk); - text = remainder; - } - self.chunks - .extend(new_chunks.into_iter().map(Chunk::new), ()); - - self.check_invariants(); - } - - /// A copy of `push` specialized for working with large quantities of text. - fn push_large(&mut self, mut text: &str) { - // To avoid frequent reallocs when loading large swaths of file contents, - // we estimate worst-case `new_chunks` capacity; - // Chunk is a fixed-capacity buffer. If a character falls on - // chunk boundary, we push it off to the following chunk (thus leaving a small bit of capacity unfilled in current chunk). - // Worst-case chunk count when loading a file is then a case where every chunk ends up with that unused capacity. - // Since we're working with UTF-8, each character is at most 4 bytes wide. It follows then that the worst case is where - // a chunk ends with 3 bytes of a 4-byte character. These 3 bytes end up being stored in the following chunk, thus wasting - // 3 bytes of storage in current chunk. - // For example, a 1024-byte string can occupy between 32 (full ASCII, 1024/32) and 36 (full 4-byte UTF-8, 1024 / 29 rounded up) chunks. - const MIN_CHUNK_SIZE: usize = chunk::MAX_BASE - 3; - - // We also round up the capacity up by one, for a good measure; we *really* don't want to realloc here, as we assume that the # of characters - // we're working with there is large. - let capacity = text.len().div_ceil(MIN_CHUNK_SIZE); - let mut new_chunks = Vec::with_capacity(capacity); - - while !text.is_empty() { - let mut split_ix = cmp::min(chunk::MAX_BASE, text.len()); - while !text.is_char_boundary(split_ix) { - split_ix -= 1; - } - let (chunk, remainder) = text.split_at(split_ix); - new_chunks.push(chunk); - text = remainder; - } - - #[cfg(all(test, not(rust_analyzer)))] - const PARALLEL_THRESHOLD: usize = 4; - #[cfg(not(all(test, not(rust_analyzer))))] - const PARALLEL_THRESHOLD: usize = 4 * (2 * sum_tree::TREE_BASE); - - if new_chunks.len() >= PARALLEL_THRESHOLD { - self.chunks - .par_extend(new_chunks.into_par_iter().map(Chunk::new), ()); - } else { - self.chunks - .extend(new_chunks.into_iter().map(Chunk::new), ()); - } - - self.check_invariants(); - } - - fn push_chunk(&mut self, mut chunk: ChunkSlice) { - self.chunks.update_last( - |last_chunk| { - let split_ix = if last_chunk.text.len() + chunk.len() <= chunk::MAX_BASE { - chunk.len() - } else { - let mut split_ix = cmp::min( - chunk::MIN_BASE.saturating_sub(last_chunk.text.len()), - chunk.len(), - ); - while !chunk.is_char_boundary(split_ix) { - split_ix += 1; - } - split_ix - }; - - let (suffix, remainder) = chunk.split_at(split_ix); - last_chunk.append(suffix); - chunk = remainder; - }, - (), - ); - - if !chunk.is_empty() { - self.chunks.push(chunk.into(), ()); - } - } - - pub fn push_front(&mut self, text: &str) { - let suffix = mem::replace(self, Rope::from(text)); - self.append(suffix); - } - - fn check_invariants(&self) { - #[cfg(test)] - { - // Ensure all chunks except maybe the last one are not underflowing. - // Allow some wiggle room for multibyte characters at chunk boundaries. - let mut chunks = self.chunks.cursor::<()>(()).peekable(); - while let Some(chunk) = chunks.next() { - if chunks.peek().is_some() { - assert!(chunk.text.len() + 3 >= chunk::MIN_BASE); - } - } - } - } - - pub fn summary(&self) -> TextSummary { - self.chunks.summary().text - } - - pub fn len(&self) -> usize { - self.chunks.extent(()) - } - - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - pub fn max_point(&self) -> Point { - self.chunks.extent(()) - } - - pub fn max_point_utf16(&self) -> PointUtf16 { - self.chunks.extent(()) - } - - pub fn cursor(&self, offset: usize) -> Cursor<'_> { - Cursor::new(self, offset) - } - - pub fn chars(&self) -> impl Iterator + '_ { - self.chars_at(0) - } - - pub fn chars_at(&self, start: usize) -> impl Iterator + '_ { - self.chunks_in_range(start..self.len()).flat_map(str::chars) - } - - pub fn reversed_chars_at(&self, start: usize) -> impl Iterator + '_ { - self.reversed_chunks_in_range(0..start) - .flat_map(|chunk| chunk.chars().rev()) - } - - pub fn bytes_in_range(&self, range: Range) -> Bytes<'_> { - Bytes::new(self, range, false) - } - - pub fn reversed_bytes_in_range(&self, range: Range) -> Bytes<'_> { - Bytes::new(self, range, true) - } - - pub fn chunks(&self) -> Chunks<'_> { - self.chunks_in_range(0..self.len()) - } - - pub fn chunks_in_range(&self, range: Range) -> Chunks<'_> { - Chunks::new(self, range, false) - } - - pub fn reversed_chunks_in_range(&self, range: Range) -> Chunks<'_> { - Chunks::new(self, range, true) - } - - pub fn offset_to_offset_utf16(&self, offset: usize) -> OffsetUtf16 { - if offset >= self.summary().len { - return self.summary().len_utf16; - } - let (start, _, item) = - self.chunks - .find::, _>((), &offset, Bias::Left); - let overshoot = offset - start.0; - start.1 - + item.map_or(Default::default(), |chunk| { - chunk.as_slice().offset_to_offset_utf16(overshoot) - }) - } - - pub fn offset_utf16_to_offset(&self, offset: OffsetUtf16) -> usize { - if offset >= self.summary().len_utf16 { - return self.summary().len; - } - let (start, _, item) = - self.chunks - .find::, _>((), &offset, Bias::Left); - let overshoot = offset - start.0; - start.1 - + item.map_or(Default::default(), |chunk| { - chunk.as_slice().offset_utf16_to_offset(overshoot) - }) - } - - pub fn offset_to_point(&self, offset: usize) -> Point { - if offset >= self.summary().len { - return self.summary().lines; - } - let (start, _, item) = - self.chunks - .find::, _>((), &offset, Bias::Left); - let overshoot = offset - start.0; - start.1 - + item.map_or(Point::zero(), |chunk| { - chunk.as_slice().offset_to_point(overshoot) - }) - } - - pub fn offset_to_point_utf16(&self, offset: usize) -> PointUtf16 { - if offset >= self.summary().len { - return self.summary().lines_utf16(); - } - let (start, _, item) = - self.chunks - .find::, _>((), &offset, Bias::Left); - let overshoot = offset - start.0; - start.1 - + item.map_or(PointUtf16::zero(), |chunk| { - chunk.as_slice().offset_to_point_utf16(overshoot) - }) - } - - pub fn point_to_point_utf16(&self, point: Point) -> PointUtf16 { - if point >= self.summary().lines { - return self.summary().lines_utf16(); - } - let (start, _, item) = - self.chunks - .find::, _>((), &point, Bias::Left); - let overshoot = point - start.0; - start.1 - + item.map_or(PointUtf16::zero(), |chunk| { - chunk.as_slice().point_to_point_utf16(overshoot) - }) - } - - pub fn point_utf16_to_point(&self, point: PointUtf16) -> Point { - if point >= self.summary().lines_utf16() { - return self.summary().lines; - } - let mut cursor = self.chunks.cursor::>(()); - cursor.seek(&point, Bias::Left); - let overshoot = point - cursor.start().0; - cursor.start().1 - + cursor.item().map_or(Point::zero(), |chunk| { - chunk - .as_slice() - .offset_to_point(chunk.as_slice().point_utf16_to_offset(overshoot, false)) - }) - } - - #[instrument(skip_all)] - pub fn point_to_offset(&self, point: Point) -> usize { - if point >= self.summary().lines { - return self.summary().len; - } - let (start, _, item) = - self.chunks - .find::, _>((), &point, Bias::Left); - let overshoot = point - start.0; - start.1 + item.map_or(0, |chunk| chunk.as_slice().point_to_offset(overshoot)) - } - - pub fn point_to_offset_utf16(&self, point: Point) -> OffsetUtf16 { - if point >= self.summary().lines { - return self.summary().len_utf16; - } - let mut cursor = self.chunks.cursor::>(()); - cursor.seek(&point, Bias::Left); - let overshoot = point - cursor.start().0; - cursor.start().1 - + cursor.item().map_or(OffsetUtf16(0), |chunk| { - chunk.as_slice().point_to_offset_utf16(overshoot) - }) - } - - pub fn point_utf16_to_offset(&self, point: PointUtf16) -> usize { - self.point_utf16_to_offset_impl(point, false) - } - - pub fn point_utf16_to_offset_utf16(&self, point: PointUtf16) -> OffsetUtf16 { - self.point_utf16_to_offset_utf16_impl(point, false) - } - - pub fn unclipped_point_utf16_to_offset(&self, point: Unclipped) -> usize { - self.point_utf16_to_offset_impl(point.0, true) - } - - fn point_utf16_to_offset_impl(&self, point: PointUtf16, clip: bool) -> usize { - if point >= self.summary().lines_utf16() { - return self.summary().len; - } - let (start, _, item) = - self.chunks - .find::, _>((), &point, Bias::Left); - let overshoot = point - start.0; - start.1 - + item.map_or(0, |chunk| { - chunk.as_slice().point_utf16_to_offset(overshoot, clip) - }) - } - - fn point_utf16_to_offset_utf16_impl(&self, point: PointUtf16, clip: bool) -> OffsetUtf16 { - if point >= self.summary().lines_utf16() { - return self.summary().len_utf16; - } - let mut cursor = self - .chunks - .cursor::>(()); - cursor.seek(&point, Bias::Left); - let overshoot = point - cursor.start().0; - cursor.start().1 - + cursor.item().map_or(OffsetUtf16(0), |chunk| { - chunk - .as_slice() - .offset_to_offset_utf16(chunk.as_slice().point_utf16_to_offset(overshoot, clip)) - }) - } - - pub fn unclipped_point_utf16_to_point(&self, point: Unclipped) -> Point { - if point.0 >= self.summary().lines_utf16() { - return self.summary().lines; - } - let (start, _, item) = - self.chunks - .find::, _>((), &point.0, Bias::Left); - let overshoot = Unclipped(point.0 - start.0); - start.1 - + item.map_or(Point::zero(), |chunk| { - chunk.as_slice().unclipped_point_utf16_to_point(overshoot) - }) - } - - pub fn clip_offset(&self, offset: usize, bias: Bias) -> usize { - match bias { - Bias::Left => self.floor_char_boundary(offset), - Bias::Right => self.ceil_char_boundary(offset), - } - } - - pub fn clip_offset_utf16(&self, offset: OffsetUtf16, bias: Bias) -> OffsetUtf16 { - let (start, _, item) = self.chunks.find::((), &offset, Bias::Right); - if let Some(chunk) = item { - let overshoot = offset - start; - start + chunk.as_slice().clip_offset_utf16(overshoot, bias) - } else { - self.summary().len_utf16 - } - } - - pub fn clip_point(&self, point: Point, bias: Bias) -> Point { - let (start, _, item) = self.chunks.find::((), &point, Bias::Right); - if let Some(chunk) = item { - let overshoot = point - start; - start + chunk.as_slice().clip_point(overshoot, bias) - } else { - self.summary().lines - } - } - - pub fn clip_point_utf16(&self, point: Unclipped, bias: Bias) -> PointUtf16 { - let (start, _, item) = self.chunks.find::((), &point.0, Bias::Right); - if let Some(chunk) = item { - let overshoot = Unclipped(point.0 - start); - start + chunk.as_slice().clip_point_utf16(overshoot, bias) - } else { - self.summary().lines_utf16() - } - } - - pub fn line_len(&self, row: u32) -> u32 { - self.clip_point(Point::new(row, u32::MAX), Bias::Left) - .column - } -} - -impl<'a> From<&'a str> for Rope { - fn from(text: &'a str) -> Self { - let mut rope = Self::new(); - rope.push(text); - rope - } -} - -impl<'a> FromIterator<&'a str> for Rope { - fn from_iter>(iter: T) -> Self { - let mut rope = Rope::new(); - for chunk in iter { - rope.push(chunk); - } - rope - } -} - -impl From for Rope { - #[inline(always)] - fn from(text: String) -> Self { - Rope::from(text.as_str()) - } -} - -impl From<&String> for Rope { - #[inline(always)] - fn from(text: &String) -> Self { - Rope::from(text.as_str()) - } -} - -impl fmt::Display for Rope { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for chunk in self.chunks() { - write!(f, "{}", chunk)?; - } - Ok(()) - } -} - -impl fmt::Debug for Rope { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use std::fmt::Write as _; - - write!(f, "\"")?; - let mut format_string = String::new(); - for chunk in self.chunks() { - write!(&mut format_string, "{:?}", chunk)?; - write!(f, "{}", &format_string[1..format_string.len() - 1])?; - format_string.clear(); - } - write!(f, "\"")?; - Ok(()) - } -} - -pub struct Cursor<'a> { - rope: &'a Rope, - chunks: sum_tree::Cursor<'a, 'static, Chunk, usize>, - offset: usize, -} - -impl<'a> Cursor<'a> { - pub fn new(rope: &'a Rope, offset: usize) -> Self { - let mut chunks = rope.chunks.cursor(()); - chunks.seek(&offset, Bias::Right); - Self { - rope, - chunks, - offset, - } - } - - pub fn seek_forward(&mut self, end_offset: usize) { - debug_assert!(end_offset >= self.offset); - - self.chunks.seek_forward(&end_offset, Bias::Right); - self.offset = end_offset; - } - - pub fn slice(&mut self, end_offset: usize) -> Rope { - debug_assert!( - end_offset >= self.offset, - "cannot slice backwards from {} to {}", - self.offset, - end_offset - ); - - let mut slice = Rope::new(); - if let Some(start_chunk) = self.chunks.item() { - let start_ix = self.offset - self.chunks.start(); - let end_ix = cmp::min(end_offset, self.chunks.end()) - self.chunks.start(); - slice.push_chunk(start_chunk.slice(start_ix..end_ix)); - } - - if end_offset > self.chunks.end() { - self.chunks.next(); - slice.append(Rope { - chunks: self.chunks.slice(&end_offset, Bias::Right), - }); - if let Some(end_chunk) = self.chunks.item() { - let end_ix = end_offset - self.chunks.start(); - slice.push_chunk(end_chunk.slice(0..end_ix)); - } - } - - self.offset = end_offset; - slice - } - - pub fn summary(&mut self, end_offset: usize) -> D { - debug_assert!(end_offset >= self.offset); - - let mut summary = D::zero(()); - if let Some(start_chunk) = self.chunks.item() { - let start_ix = self.offset - self.chunks.start(); - let end_ix = cmp::min(end_offset, self.chunks.end()) - self.chunks.start(); - summary.add_assign(&D::from_chunk(start_chunk.slice(start_ix..end_ix))); - } - - if end_offset > self.chunks.end() { - self.chunks.next(); - summary.add_assign(&self.chunks.summary(&end_offset, Bias::Right)); - if let Some(end_chunk) = self.chunks.item() { - let end_ix = end_offset - self.chunks.start(); - summary.add_assign(&D::from_chunk(end_chunk.slice(0..end_ix))); - } - } - - self.offset = end_offset; - summary - } - - pub fn suffix(mut self) -> Rope { - self.slice(self.rope.chunks.extent(())) - } - - pub fn offset(&self) -> usize { - self.offset - } -} - -pub struct ChunkBitmaps<'a> { - /// A slice of text up to 128 bytes in size - pub text: &'a str, - /// Bitmap of character locations in text. LSB ordered - pub chars: Bitmap, - /// Bitmap of tab locations in text. LSB ordered - pub tabs: Bitmap, -} - -#[derive(Clone)] -pub struct Chunks<'a> { - chunks: sum_tree::Cursor<'a, 'static, Chunk, usize>, - range: Range, - offset: usize, - reversed: bool, -} - -impl<'a> Chunks<'a> { - pub fn new(rope: &'a Rope, range: Range, reversed: bool) -> Self { - let mut chunks = rope.chunks.cursor(()); - let offset = if reversed { - chunks.seek(&range.end, Bias::Left); - range.end - } else { - chunks.seek(&range.start, Bias::Right); - range.start - }; - let chunk_offset = offset - chunks.start(); - if let Some(chunk) = chunks.item() { - chunk.assert_char_boundary::(chunk_offset); - } - Self { - chunks, - range, - offset, - reversed, - } - } - - fn offset_is_valid(&self) -> bool { - if self.reversed { - if self.offset <= self.range.start || self.offset > self.range.end { - return false; - } - } else if self.offset < self.range.start || self.offset >= self.range.end { - return false; - } - - true - } - - pub fn offset(&self) -> usize { - self.offset - } - - pub fn seek(&mut self, mut offset: usize) { - offset = offset.clamp(self.range.start, self.range.end); - - if self.reversed { - if offset > self.chunks.end() { - self.chunks.seek_forward(&offset, Bias::Left); - } else if offset <= *self.chunks.start() { - self.chunks.seek(&offset, Bias::Left); - } - } else { - if offset >= self.chunks.end() { - self.chunks.seek_forward(&offset, Bias::Right); - } else if offset < *self.chunks.start() { - self.chunks.seek(&offset, Bias::Right); - } - }; - - self.offset = offset; - } - - pub fn set_range(&mut self, range: Range) { - self.range = range.clone(); - self.seek(range.start); - } - - /// Moves this cursor to the start of the next line in the rope. - /// - /// This method advances the cursor to the beginning of the next line. - /// If the cursor is already at the end of the rope, this method does nothing. - /// Reversed chunks iterators are not currently supported and will panic. - /// - /// Returns `true` if the cursor was successfully moved to the next line start, - /// or `false` if the cursor was already at the end of the rope. - pub fn next_line(&mut self) -> bool { - assert!(!self.reversed); - - let mut found = false; - if let Some(chunk) = self.peek() { - if let Some(newline_ix) = chunk.find('\n') { - self.offset += newline_ix + 1; - found = self.offset <= self.range.end; - } else { - self.chunks - .search_forward(|summary| summary.text.lines.row > 0); - self.offset = *self.chunks.start(); - - if let Some(newline_ix) = self.peek().and_then(|chunk| chunk.find('\n')) { - self.offset += newline_ix + 1; - found = self.offset <= self.range.end; - } else { - self.offset = self.chunks.end(); - } - } - - if self.offset == self.chunks.end() { - self.next(); - } - } - - if self.offset > self.range.end { - self.offset = cmp::min(self.offset, self.range.end); - self.chunks.seek(&self.offset, Bias::Right); - } - - found - } - - /// Move this cursor to the preceding position in the rope that starts a new line. - /// Reversed chunks iterators are not currently supported and will panic. - /// - /// If this cursor is not on the start of a line, it will be moved to the start of - /// its current line. Otherwise it will be moved to the start of the previous line. - /// It updates the cursor's position and returns true if a previous line was found, - /// or false if the cursor was already at the start of the rope. - pub fn prev_line(&mut self) -> bool { - assert!(!self.reversed); - - let initial_offset = self.offset; - - if self.offset == *self.chunks.start() { - self.chunks.prev(); - } - - if let Some(chunk) = self.chunks.item() { - let mut end_ix = self.offset - *self.chunks.start(); - if chunk.text.as_bytes()[end_ix - 1] == b'\n' { - end_ix -= 1; - } - - if let Some(newline_ix) = chunk.text[..end_ix].rfind('\n') { - self.offset = *self.chunks.start() + newline_ix + 1; - if self.offset_is_valid() { - return true; - } - } - } - - self.chunks - .search_backward(|summary| summary.text.lines.row > 0); - self.offset = *self.chunks.start(); - if let Some(chunk) = self.chunks.item() - && let Some(newline_ix) = chunk.text.rfind('\n') - { - self.offset += newline_ix + 1; - if self.offset_is_valid() { - if self.offset == self.chunks.end() { - self.chunks.next(); - } - - return true; - } - } - - if !self.offset_is_valid() || self.chunks.item().is_none() { - self.offset = self.range.start; - self.chunks.seek(&self.offset, Bias::Right); - } - - self.offset < initial_offset && self.offset == 0 - } - - pub fn peek(&self) -> Option<&'a str> { - if !self.offset_is_valid() { - return None; - } - - let chunk = self.chunks.item()?; - let chunk_start = *self.chunks.start(); - let slice_range = if self.reversed { - let slice_start = cmp::max(chunk_start, self.range.start) - chunk_start; - let slice_end = self.offset - chunk_start; - slice_start..slice_end - } else { - let slice_start = self.offset - chunk_start; - let slice_end = cmp::min(self.chunks.end(), self.range.end) - chunk_start; - slice_start..slice_end - }; - - Some(&chunk.text[slice_range]) - } - - /// Returns bitmaps that represent character positions and tab positions - pub fn peek_with_bitmaps(&self) -> Option> { - if !self.offset_is_valid() { - return None; - } - - let chunk = self.chunks.item()?; - let chunk_start = *self.chunks.start(); - let slice_range = if self.reversed { - let slice_start = cmp::max(chunk_start, self.range.start) - chunk_start; - let slice_end = self.offset - chunk_start; - slice_start..slice_end - } else { - let slice_start = self.offset - chunk_start; - let slice_end = cmp::min(self.chunks.end(), self.range.end) - chunk_start; - slice_start..slice_end - }; - let chunk_start_offset = slice_range.start; - let slice_text = &chunk.text[slice_range]; - - // Shift the tabs to align with our slice window - let shifted_tabs = chunk.tabs() >> chunk_start_offset; - let shifted_chars = chunk.chars() >> chunk_start_offset; - - Some(ChunkBitmaps { - text: slice_text, - chars: shifted_chars, - tabs: shifted_tabs, - }) - } - - pub fn lines(self) -> Lines<'a> { - let reversed = self.reversed; - Lines { - chunks: self, - current_line: String::new(), - done: false, - reversed, - } - } - - pub fn equals_str(&self, other: &str) -> bool { - let chunk = self.clone(); - if chunk.reversed { - let mut offset = other.len(); - for chunk in chunk { - if other[0..offset].ends_with(chunk) { - offset -= chunk.len(); - } else { - return false; - } - } - if offset != 0 { - return false; - } - } else { - let mut offset = 0; - for chunk in chunk { - if offset >= other.len() { - return false; - } - if other[offset..].starts_with(chunk) { - offset += chunk.len(); - } else { - return false; - } - } - if offset != other.len() { - return false; - } - } - - true - } -} - -pub struct ChunkWithBitmaps<'a>(pub Chunks<'a>); - -impl<'a> Iterator for ChunkWithBitmaps<'a> { - /// text, chars bitmap, tabs bitmap - type Item = ChunkBitmaps<'a>; - - fn next(&mut self) -> Option { - let chunk_bitmaps = self.0.peek_with_bitmaps()?; - if self.0.reversed { - self.0.offset -= chunk_bitmaps.text.len(); - if self.0.offset <= *self.0.chunks.start() { - self.0.chunks.prev(); - } - } else { - self.0.offset += chunk_bitmaps.text.len(); - if self.0.offset >= self.0.chunks.end() { - self.0.chunks.next(); - } - } - - Some(chunk_bitmaps) - } -} - -impl<'a> Iterator for Chunks<'a> { - type Item = &'a str; - - fn next(&mut self) -> Option { - let chunk = self.peek()?; - if self.reversed { - self.offset -= chunk.len(); - if self.offset <= *self.chunks.start() { - self.chunks.prev(); - } - } else { - self.offset += chunk.len(); - if self.offset >= self.chunks.end() { - self.chunks.next(); - } - } - - Some(chunk) - } -} - -pub struct Bytes<'a> { - chunks: sum_tree::Cursor<'a, 'static, Chunk, usize>, - range: Range, - reversed: bool, -} - -impl<'a> Bytes<'a> { - pub fn new(rope: &'a Rope, range: Range, reversed: bool) -> Self { - let mut chunks = rope.chunks.cursor(()); - if reversed { - chunks.seek(&range.end, Bias::Left); - } else { - chunks.seek(&range.start, Bias::Right); - } - Self { - chunks, - range, - reversed, - } - } - - pub fn peek(&self) -> Option<&'a [u8]> { - let chunk = self.chunks.item()?; - if self.reversed && self.range.start >= self.chunks.end() { - return None; - } - let chunk_start = *self.chunks.start(); - if self.range.end <= chunk_start { - return None; - } - let start = self.range.start.saturating_sub(chunk_start); - let end = self.range.end - chunk_start; - Some(&chunk.text.as_bytes()[start..chunk.text.len().min(end)]) - } -} - -impl<'a> Iterator for Bytes<'a> { - type Item = &'a [u8]; - - fn next(&mut self) -> Option { - let result = self.peek(); - if result.is_some() { - if self.reversed { - self.chunks.prev(); - } else { - self.chunks.next(); - } - } - result - } -} - -impl io::Read for Bytes<'_> { - fn read(&mut self, buf: &mut [u8]) -> io::Result { - if let Some(chunk) = self.peek() { - let len = cmp::min(buf.len(), chunk.len()); - if self.reversed { - buf[..len].copy_from_slice(&chunk[chunk.len() - len..]); - buf[..len].reverse(); - self.range.end -= len; - } else { - buf[..len].copy_from_slice(&chunk[..len]); - self.range.start += len; - } - - if len == chunk.len() { - if self.reversed { - self.chunks.prev(); - } else { - self.chunks.next(); - } - } - Ok(len) - } else { - Ok(0) - } - } -} - -pub struct Lines<'a> { - chunks: Chunks<'a>, - current_line: String, - done: bool, - reversed: bool, -} - -impl<'a> Lines<'a> { - pub fn next(&mut self) -> Option<&str> { - if self.done { - return None; - } - - self.current_line.clear(); - - while let Some(chunk) = self.chunks.peek() { - let chunk_lines = chunk.split('\n'); - if self.reversed { - let mut chunk_lines = chunk_lines.rev().peekable(); - if let Some(chunk_line) = chunk_lines.next() { - let done = chunk_lines.peek().is_some(); - if done { - self.chunks - .seek(self.chunks.offset() - chunk_line.len() - "\n".len()); - if self.current_line.is_empty() { - return Some(chunk_line); - } - } - self.current_line.insert_str(0, chunk_line); - if done { - return Some(&self.current_line); - } - } - } else { - let mut chunk_lines = chunk_lines.peekable(); - if let Some(chunk_line) = chunk_lines.next() { - let done = chunk_lines.peek().is_some(); - if done { - self.chunks - .seek(self.chunks.offset() + chunk_line.len() + "\n".len()); - if self.current_line.is_empty() { - return Some(chunk_line); - } - } - self.current_line.push_str(chunk_line); - if done { - return Some(&self.current_line); - } - } - } - - self.chunks.next(); - } - - self.done = true; - Some(&self.current_line) - } - - pub fn seek(&mut self, offset: usize) { - self.chunks.seek(offset); - self.current_line.clear(); - self.done = false; - } - - pub fn offset(&self) -> usize { - self.chunks.offset() - } -} - -impl sum_tree::Item for Chunk { - type Summary = ChunkSummary; - - fn summary(&self, _cx: ()) -> Self::Summary { - ChunkSummary { - text: self.as_slice().text_summary(), - } - } -} - -#[derive(Clone, Debug, Default, Eq, PartialEq)] -pub struct ChunkSummary { - text: TextSummary, -} - -impl sum_tree::ContextLessSummary for ChunkSummary { - fn zero() -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &Self) { - self.text += &summary.text; - } -} - -/// Summary of a string of text. -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] -pub struct TextSummary { - /// Length in bytes. - pub len: usize, - /// Length in UTF-8. - pub chars: usize, - /// Length in UTF-16 code units - pub len_utf16: OffsetUtf16, - /// A point representing the number of lines and the length of the last line. - /// - /// In other words, it marks the point after the last byte in the text, (if - /// EOF was a character, this would be its position). - pub lines: Point, - /// How many `char`s are in the first line - pub first_line_chars: u32, - /// How many `char`s are in the last line - pub last_line_chars: u32, - /// How many UTF-16 code units are in the last line - pub last_line_len_utf16: u32, - /// The row idx of the longest row - pub longest_row: u32, - /// How many `char`s are in the longest row - pub longest_row_chars: u32, -} - -impl TextSummary { - pub fn lines_utf16(&self) -> PointUtf16 { - PointUtf16 { - row: self.lines.row, - column: self.last_line_len_utf16, - } - } - - pub fn newline() -> Self { - Self { - len: 1, - chars: 1, - len_utf16: OffsetUtf16(1), - first_line_chars: 0, - last_line_chars: 0, - last_line_len_utf16: 0, - lines: Point::new(1, 0), - longest_row: 0, - longest_row_chars: 0, - } - } - - pub fn add_newline(&mut self) { - self.len += 1; - self.len_utf16 += OffsetUtf16(self.len_utf16.0 + 1); - self.last_line_chars = 0; - self.last_line_len_utf16 = 0; - self.lines += Point::new(1, 0); - } -} - -impl<'a> From<&'a str> for TextSummary { - fn from(text: &'a str) -> Self { - let mut len_utf16 = OffsetUtf16(0); - let mut lines = Point::new(0, 0); - let mut first_line_chars = 0; - let mut last_line_chars = 0; - let mut last_line_len_utf16 = 0; - let mut longest_row = 0; - let mut longest_row_chars = 0; - let mut chars = 0; - for c in text.chars() { - chars += 1; - len_utf16.0 += c.len_utf16(); - - if c == '\n' { - lines += Point::new(1, 0); - last_line_len_utf16 = 0; - last_line_chars = 0; - } else { - lines.column += c.len_utf8() as u32; - last_line_len_utf16 += c.len_utf16() as u32; - last_line_chars += 1; - } - - if lines.row == 0 { - first_line_chars = last_line_chars; - } - - if last_line_chars > longest_row_chars { - longest_row = lines.row; - longest_row_chars = last_line_chars; - } - } - - TextSummary { - len: text.len(), - chars, - len_utf16, - lines, - first_line_chars, - last_line_chars, - last_line_len_utf16, - longest_row, - longest_row_chars, - } - } -} - -impl sum_tree::ContextLessSummary for TextSummary { - fn zero() -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &Self) { - *self += summary; - } -} - -impl ops::Add for TextSummary { - type Output = Self; - - fn add(mut self, rhs: Self) -> Self::Output { - AddAssign::add_assign(&mut self, &rhs); - self - } -} - -impl<'a> ops::AddAssign<&'a Self> for TextSummary { - fn add_assign(&mut self, other: &'a Self) { - let joined_chars = self.last_line_chars + other.first_line_chars; - if joined_chars > self.longest_row_chars { - self.longest_row = self.lines.row; - self.longest_row_chars = joined_chars; - } - if other.longest_row_chars > self.longest_row_chars { - self.longest_row = self.lines.row + other.longest_row; - self.longest_row_chars = other.longest_row_chars; - } - - if self.lines.row == 0 { - self.first_line_chars += other.first_line_chars; - } - - if other.lines.row == 0 { - self.last_line_chars += other.first_line_chars; - self.last_line_len_utf16 += other.last_line_len_utf16; - } else { - self.last_line_chars = other.last_line_chars; - self.last_line_len_utf16 = other.last_line_len_utf16; - } - - self.chars += other.chars; - self.len += other.len; - self.len_utf16 += other.len_utf16; - self.lines += other.lines; - } -} - -impl ops::AddAssign for TextSummary { - fn add_assign(&mut self, other: Self) { - *self += &other; - } -} - -pub trait TextDimension: - 'static + Clone + Copy + Default + for<'a> Dimension<'a, ChunkSummary> + std::fmt::Debug -{ - fn from_text_summary(summary: &TextSummary) -> Self; - fn from_chunk(chunk: ChunkSlice) -> Self; - fn add_assign(&mut self, other: &Self); -} - -impl TextDimension for Dimensions { - fn from_text_summary(summary: &TextSummary) -> Self { - Dimensions( - D1::from_text_summary(summary), - D2::from_text_summary(summary), - (), - ) - } - - fn from_chunk(chunk: ChunkSlice) -> Self { - Dimensions(D1::from_chunk(chunk), D2::from_chunk(chunk), ()) - } - - fn add_assign(&mut self, other: &Self) { - self.0.add_assign(&other.0); - self.1.add_assign(&other.1); - } -} - -impl<'a> sum_tree::Dimension<'a, ChunkSummary> for TextSummary { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a ChunkSummary, _: ()) { - *self += &summary.text; - } -} - -impl TextDimension for TextSummary { - fn from_text_summary(summary: &TextSummary) -> Self { - *summary - } - - fn from_chunk(chunk: ChunkSlice) -> Self { - chunk.text_summary() - } - - fn add_assign(&mut self, other: &Self) { - *self += other; - } -} - -impl<'a> sum_tree::Dimension<'a, ChunkSummary> for usize { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a ChunkSummary, _: ()) { - *self += summary.text.len; - } -} - -impl TextDimension for usize { - fn from_text_summary(summary: &TextSummary) -> Self { - summary.len - } - - fn from_chunk(chunk: ChunkSlice) -> Self { - chunk.len() - } - - fn add_assign(&mut self, other: &Self) { - *self += other; - } -} - -impl<'a> sum_tree::Dimension<'a, ChunkSummary> for OffsetUtf16 { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a ChunkSummary, _: ()) { - *self += summary.text.len_utf16; - } -} - -impl TextDimension for OffsetUtf16 { - fn from_text_summary(summary: &TextSummary) -> Self { - summary.len_utf16 - } - - fn from_chunk(chunk: ChunkSlice) -> Self { - chunk.len_utf16() - } - - fn add_assign(&mut self, other: &Self) { - *self += other; - } -} - -impl<'a> sum_tree::Dimension<'a, ChunkSummary> for Point { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a ChunkSummary, _: ()) { - *self += summary.text.lines; - } -} - -impl TextDimension for Point { - fn from_text_summary(summary: &TextSummary) -> Self { - summary.lines - } - - fn from_chunk(chunk: ChunkSlice) -> Self { - chunk.lines() - } - - fn add_assign(&mut self, other: &Self) { - *self += other; - } -} - -impl<'a> sum_tree::Dimension<'a, ChunkSummary> for PointUtf16 { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a ChunkSummary, _: ()) { - *self += summary.text.lines_utf16(); - } -} - -impl TextDimension for PointUtf16 { - fn from_text_summary(summary: &TextSummary) -> Self { - summary.lines_utf16() - } - - fn from_chunk(chunk: ChunkSlice) -> Self { - PointUtf16 { - row: chunk.lines().row, - column: chunk.last_line_len_utf16(), - } - } - - fn add_assign(&mut self, other: &Self) { - *self += other; - } -} - -/// A pair of text dimensions in which only the first dimension is used for comparison, -/// but both dimensions are updated during addition and subtraction. -#[derive(Clone, Copy, Debug)] -pub struct DimensionPair { - pub key: K, - pub value: Option, -} - -impl Default for DimensionPair { - fn default() -> Self { - Self { - key: Default::default(), - value: Some(Default::default()), - } - } -} - -impl cmp::Ord for DimensionPair -where - K: cmp::Ord, -{ - fn cmp(&self, other: &Self) -> cmp::Ordering { - self.key.cmp(&other.key) - } -} - -impl cmp::PartialOrd for DimensionPair -where - K: cmp::PartialOrd, -{ - fn partial_cmp(&self, other: &Self) -> Option { - self.key.partial_cmp(&other.key) - } -} - -impl cmp::PartialEq for DimensionPair -where - K: cmp::PartialEq, -{ - fn eq(&self, other: &Self) -> bool { - self.key.eq(&other.key) - } -} - -impl ops::Sub for DimensionPair -where - K: ops::Sub, - V: ops::Sub, -{ - type Output = DimensionPair; - - fn sub(self, rhs: Self) -> Self::Output { - DimensionPair { - key: self.key - rhs.key, - value: self.value.zip(rhs.value).map(|(a, b)| a - b), - } - } -} - -impl ops::AddAssign> for DimensionPair -where - K: ops::AddAssign, - V: ops::AddAssign, -{ - fn add_assign(&mut self, rhs: DimensionPair) { - self.key += rhs.key; - if let Some(value) = &mut self.value { - if let Some(other_value) = rhs.value { - *value += other_value; - } else { - self.value.take(); - } - } - } -} - -impl std::ops::AddAssign> for Point { - fn add_assign(&mut self, rhs: DimensionPair) { - *self += rhs.key; - } -} - -impl cmp::Eq for DimensionPair where K: cmp::Eq {} - -impl<'a, K, V, S> sum_tree::Dimension<'a, S> for DimensionPair -where - S: sum_tree::Summary, - K: sum_tree::Dimension<'a, S>, - V: sum_tree::Dimension<'a, S>, -{ - fn zero(cx: S::Context<'_>) -> Self { - Self { - key: K::zero(cx), - value: Some(V::zero(cx)), - } - } - - fn add_summary(&mut self, summary: &'a S, cx: S::Context<'_>) { - self.key.add_summary(summary, cx); - if let Some(value) = &mut self.value { - value.add_summary(summary, cx); - } - } -} - -impl TextDimension for DimensionPair -where - K: TextDimension, - V: TextDimension, -{ - fn add_assign(&mut self, other: &Self) { - self.key.add_assign(&other.key); - if let Some(value) = &mut self.value { - if let Some(other_value) = other.value.as_ref() { - value.add_assign(other_value); - } else { - self.value.take(); - } - } - } - - fn from_chunk(chunk: ChunkSlice) -> Self { - Self { - key: K::from_chunk(chunk), - value: Some(V::from_chunk(chunk)), - } - } - - fn from_text_summary(summary: &TextSummary) -> Self { - Self { - key: K::from_text_summary(summary), - value: Some(V::from_text_summary(summary)), - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use Bias::{Left, Right}; - use rand::prelude::*; - use std::{cmp::Ordering, env, io::Read}; - use util::RandomCharIter; - - #[ctor::ctor] - fn init_logger() { - zlog::init_test(); - } - - #[test] - fn test_all_4_byte_chars() { - let mut rope = Rope::new(); - let text = "🏀".repeat(256); - rope.push(&text); - assert_eq!(rope.text(), text); - } - - #[test] - fn test_clip() { - let rope = Rope::from("🧘"); - - assert_eq!(rope.clip_offset(1, Bias::Left), 0); - assert_eq!(rope.clip_offset(1, Bias::Right), 4); - assert_eq!(rope.clip_offset(5, Bias::Right), 4); - - assert_eq!( - rope.clip_point(Point::new(0, 1), Bias::Left), - Point::new(0, 0) - ); - assert_eq!( - rope.clip_point(Point::new(0, 1), Bias::Right), - Point::new(0, 4) - ); - assert_eq!( - rope.clip_point(Point::new(0, 5), Bias::Right), - Point::new(0, 4) - ); - - assert_eq!( - rope.clip_point_utf16(Unclipped(PointUtf16::new(0, 1)), Bias::Left), - PointUtf16::new(0, 0) - ); - assert_eq!( - rope.clip_point_utf16(Unclipped(PointUtf16::new(0, 1)), Bias::Right), - PointUtf16::new(0, 2) - ); - assert_eq!( - rope.clip_point_utf16(Unclipped(PointUtf16::new(0, 3)), Bias::Right), - PointUtf16::new(0, 2) - ); - - assert_eq!( - rope.clip_offset_utf16(OffsetUtf16(1), Bias::Left), - OffsetUtf16(0) - ); - assert_eq!( - rope.clip_offset_utf16(OffsetUtf16(1), Bias::Right), - OffsetUtf16(2) - ); - assert_eq!( - rope.clip_offset_utf16(OffsetUtf16(3), Bias::Right), - OffsetUtf16(2) - ); - } - - #[test] - fn test_prev_next_line() { - let rope = Rope::from("abc\ndef\nghi\njkl"); - - let mut chunks = rope.chunks(); - assert_eq!(chunks.peek().unwrap().chars().next().unwrap(), 'a'); - - assert!(chunks.next_line()); - assert_eq!(chunks.peek().unwrap().chars().next().unwrap(), 'd'); - - assert!(chunks.next_line()); - assert_eq!(chunks.peek().unwrap().chars().next().unwrap(), 'g'); - - assert!(chunks.next_line()); - assert_eq!(chunks.peek().unwrap().chars().next().unwrap(), 'j'); - - assert!(!chunks.next_line()); - assert_eq!(chunks.peek(), None); - - assert!(chunks.prev_line()); - assert_eq!(chunks.peek().unwrap().chars().next().unwrap(), 'j'); - - assert!(chunks.prev_line()); - assert_eq!(chunks.peek().unwrap().chars().next().unwrap(), 'g'); - - assert!(chunks.prev_line()); - assert_eq!(chunks.peek().unwrap().chars().next().unwrap(), 'd'); - - assert!(chunks.prev_line()); - assert_eq!(chunks.peek().unwrap().chars().next().unwrap(), 'a'); - - assert!(!chunks.prev_line()); - assert_eq!(chunks.peek().unwrap().chars().next().unwrap(), 'a'); - - // Only return true when the cursor has moved to the start of a line - let mut chunks = rope.chunks_in_range(5..7); - chunks.seek(6); - assert!(!chunks.prev_line()); - assert_eq!(chunks.peek().unwrap().chars().next().unwrap(), 'e'); - - assert!(!chunks.next_line()); - assert_eq!(chunks.peek(), None); - } - - #[test] - fn test_lines() { - let rope = Rope::from("abc\ndefg\nhi"); - let mut lines = rope.chunks().lines(); - assert_eq!(lines.next(), Some("abc")); - assert_eq!(lines.next(), Some("defg")); - assert_eq!(lines.next(), Some("hi")); - assert_eq!(lines.next(), None); - - let rope = Rope::from("abc\ndefg\nhi\n"); - let mut lines = rope.chunks().lines(); - assert_eq!(lines.next(), Some("abc")); - assert_eq!(lines.next(), Some("defg")); - assert_eq!(lines.next(), Some("hi")); - assert_eq!(lines.next(), Some("")); - assert_eq!(lines.next(), None); - - let rope = Rope::from("abc\ndefg\nhi"); - let mut lines = rope.reversed_chunks_in_range(0..rope.len()).lines(); - assert_eq!(lines.next(), Some("hi")); - assert_eq!(lines.next(), Some("defg")); - assert_eq!(lines.next(), Some("abc")); - assert_eq!(lines.next(), None); - - let rope = Rope::from("abc\ndefg\nhi\n"); - let mut lines = rope.reversed_chunks_in_range(0..rope.len()).lines(); - assert_eq!(lines.next(), Some("")); - assert_eq!(lines.next(), Some("hi")); - assert_eq!(lines.next(), Some("defg")); - assert_eq!(lines.next(), Some("abc")); - assert_eq!(lines.next(), None); - - let rope = Rope::from("abc\nlonger line test\nhi"); - let mut lines = rope.chunks().lines(); - assert_eq!(lines.next(), Some("abc")); - assert_eq!(lines.next(), Some("longer line test")); - assert_eq!(lines.next(), Some("hi")); - assert_eq!(lines.next(), None); - - let rope = Rope::from("abc\nlonger line test\nhi"); - let mut lines = rope.reversed_chunks_in_range(0..rope.len()).lines(); - assert_eq!(lines.next(), Some("hi")); - assert_eq!(lines.next(), Some("longer line test")); - assert_eq!(lines.next(), Some("abc")); - assert_eq!(lines.next(), None); - } - - #[gpui::test(iterations = 100)] - fn test_random_rope(mut rng: StdRng) { - let operations = env::var("OPERATIONS") - .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) - .unwrap_or(10); - - let mut expected = String::new(); - let mut actual = Rope::new(); - for _ in 0..operations { - let end_ix = clip_offset(&expected, rng.random_range(0..=expected.len()), Right); - let start_ix = clip_offset(&expected, rng.random_range(0..=end_ix), Left); - let len = rng.random_range(0..=64); - let new_text: String = RandomCharIter::new(&mut rng).take(len).collect(); - - let mut new_actual = Rope::new(); - let mut cursor = actual.cursor(0); - new_actual.append(cursor.slice(start_ix)); - new_actual.push(&new_text); - cursor.seek_forward(end_ix); - new_actual.append(cursor.suffix()); - actual = new_actual; - - expected.replace_range(start_ix..end_ix, &new_text); - - assert_eq!(actual.text(), expected); - log::info!("text: {:?}", expected); - - for _ in 0..5 { - let end_ix = clip_offset(&expected, rng.random_range(0..=expected.len()), Right); - let start_ix = clip_offset(&expected, rng.random_range(0..=end_ix), Left); - - let actual_text = actual.chunks_in_range(start_ix..end_ix).collect::(); - assert_eq!(actual_text, &expected[start_ix..end_ix]); - - let mut actual_text = String::new(); - actual - .bytes_in_range(start_ix..end_ix) - .read_to_string(&mut actual_text) - .unwrap(); - assert_eq!(actual_text, &expected[start_ix..end_ix]); - - assert_eq!( - actual - .reversed_chunks_in_range(start_ix..end_ix) - .collect::>() - .into_iter() - .rev() - .collect::(), - &expected[start_ix..end_ix] - ); - - let mut expected_line_starts: Vec<_> = expected[start_ix..end_ix] - .match_indices('\n') - .map(|(index, _)| start_ix + index + 1) - .collect(); - - let mut chunks = actual.chunks_in_range(start_ix..end_ix); - - let mut actual_line_starts = Vec::new(); - while chunks.next_line() { - actual_line_starts.push(chunks.offset()); - } - assert_eq!( - actual_line_starts, - expected_line_starts, - "actual line starts != expected line starts when using next_line() for {:?} ({:?})", - &expected[start_ix..end_ix], - start_ix..end_ix - ); - - if start_ix < end_ix - && (start_ix == 0 || expected.as_bytes()[start_ix - 1] == b'\n') - { - expected_line_starts.insert(0, start_ix); - } - // Remove the last index if it starts at the end of the range. - if expected_line_starts.last() == Some(&end_ix) { - expected_line_starts.pop(); - } - - let mut actual_line_starts = Vec::new(); - while chunks.prev_line() { - actual_line_starts.push(chunks.offset()); - } - actual_line_starts.reverse(); - assert_eq!( - actual_line_starts, - expected_line_starts, - "actual line starts != expected line starts when using prev_line() for {:?} ({:?})", - &expected[start_ix..end_ix], - start_ix..end_ix - ); - - // Check that next_line/prev_line work correctly from random positions - let mut offset = rng.random_range(start_ix..=end_ix); - while !expected.is_char_boundary(offset) { - offset -= 1; - } - chunks.seek(offset); - - for _ in 0..5 { - if rng.random() { - let expected_next_line_start = expected[offset..end_ix] - .find('\n') - .map(|newline_ix| offset + newline_ix + 1); - - let moved = chunks.next_line(); - assert_eq!( - moved, - expected_next_line_start.is_some(), - "unexpected result from next_line after seeking to {} in range {:?} ({:?})", - offset, - start_ix..end_ix, - &expected[start_ix..end_ix] - ); - if let Some(expected_next_line_start) = expected_next_line_start { - assert_eq!( - chunks.offset(), - expected_next_line_start, - "invalid position after seeking to {} in range {:?} ({:?})", - offset, - start_ix..end_ix, - &expected[start_ix..end_ix] - ); - } else { - assert_eq!( - chunks.offset(), - end_ix, - "invalid position after seeking to {} in range {:?} ({:?})", - offset, - start_ix..end_ix, - &expected[start_ix..end_ix] - ); - } - } else { - let search_end = if offset > 0 && expected.as_bytes()[offset - 1] == b'\n' { - offset - 1 - } else { - offset - }; - - let expected_prev_line_start = expected[..search_end] - .rfind('\n') - .and_then(|newline_ix| { - let line_start_ix = newline_ix + 1; - if line_start_ix >= start_ix { - Some(line_start_ix) - } else { - None - } - }) - .or({ - if offset > 0 && start_ix == 0 { - Some(0) - } else { - None - } - }); - - let moved = chunks.prev_line(); - assert_eq!( - moved, - expected_prev_line_start.is_some(), - "unexpected result from prev_line after seeking to {} in range {:?} ({:?})", - offset, - start_ix..end_ix, - &expected[start_ix..end_ix] - ); - if let Some(expected_prev_line_start) = expected_prev_line_start { - assert_eq!( - chunks.offset(), - expected_prev_line_start, - "invalid position after seeking to {} in range {:?} ({:?})", - offset, - start_ix..end_ix, - &expected[start_ix..end_ix] - ); - } else { - assert_eq!( - chunks.offset(), - start_ix, - "invalid position after seeking to {} in range {:?} ({:?})", - offset, - start_ix..end_ix, - &expected[start_ix..end_ix] - ); - } - } - - assert!((start_ix..=end_ix).contains(&chunks.offset())); - if rng.random() { - offset = rng.random_range(start_ix..=end_ix); - while !expected.is_char_boundary(offset) { - offset -= 1; - } - chunks.seek(offset); - } else { - chunks.next(); - offset = chunks.offset(); - assert!((start_ix..=end_ix).contains(&chunks.offset())); - } - } - } - - let mut offset_utf16 = OffsetUtf16(0); - let mut point = Point::new(0, 0); - let mut point_utf16 = PointUtf16::new(0, 0); - for (ix, ch) in expected.char_indices().chain(Some((expected.len(), '\0'))) { - assert_eq!(actual.offset_to_point(ix), point, "offset_to_point({})", ix); - assert_eq!( - actual.offset_to_point_utf16(ix), - point_utf16, - "offset_to_point_utf16({})", - ix - ); - assert_eq!( - actual.point_to_offset(point), - ix, - "point_to_offset({:?})", - point - ); - assert_eq!( - actual.point_utf16_to_offset(point_utf16), - ix, - "point_utf16_to_offset({:?})", - point_utf16 - ); - assert_eq!( - actual.offset_to_offset_utf16(ix), - offset_utf16, - "offset_to_offset_utf16({:?})", - ix - ); - assert_eq!( - actual.offset_utf16_to_offset(offset_utf16), - ix, - "offset_utf16_to_offset({:?})", - offset_utf16 - ); - if ch == '\n' { - point += Point::new(1, 0); - point_utf16 += PointUtf16::new(1, 0); - } else { - point.column += ch.len_utf8() as u32; - point_utf16.column += ch.len_utf16() as u32; - } - offset_utf16.0 += ch.len_utf16(); - } - - let mut offset_utf16 = OffsetUtf16(0); - let mut point_utf16 = Unclipped(PointUtf16::zero()); - for unit in expected.encode_utf16() { - let left_offset = actual.clip_offset_utf16(offset_utf16, Bias::Left); - let right_offset = actual.clip_offset_utf16(offset_utf16, Bias::Right); - assert!(right_offset >= left_offset); - // Ensure translating UTF-16 offsets to UTF-8 offsets doesn't panic. - actual.offset_utf16_to_offset(left_offset); - actual.offset_utf16_to_offset(right_offset); - - let left_point = actual.clip_point_utf16(point_utf16, Bias::Left); - let right_point = actual.clip_point_utf16(point_utf16, Bias::Right); - assert!(right_point >= left_point); - // Ensure translating valid UTF-16 points to offsets doesn't panic. - actual.point_utf16_to_offset(left_point); - actual.point_utf16_to_offset(right_point); - - offset_utf16.0 += 1; - if unit == b'\n' as u16 { - point_utf16.0 += PointUtf16::new(1, 0); - } else { - point_utf16.0 += PointUtf16::new(0, 1); - } - } - - for _ in 0..5 { - let end_ix = clip_offset(&expected, rng.random_range(0..=expected.len()), Right); - let start_ix = clip_offset(&expected, rng.random_range(0..=end_ix), Left); - assert_eq!( - actual.cursor(start_ix).summary::(end_ix), - TextSummary::from(&expected[start_ix..end_ix]) - ); - } - - let mut expected_longest_rows = Vec::new(); - let mut longest_line_len = -1_isize; - for (row, line) in expected.split('\n').enumerate() { - let row = row as u32; - assert_eq!( - actual.line_len(row), - line.len() as u32, - "invalid line len for row {}", - row - ); - - let line_char_count = line.chars().count() as isize; - match line_char_count.cmp(&longest_line_len) { - Ordering::Less => {} - Ordering::Equal => expected_longest_rows.push(row), - Ordering::Greater => { - longest_line_len = line_char_count; - expected_longest_rows.clear(); - expected_longest_rows.push(row); - } - } - } - - let longest_row = actual.summary().longest_row; - assert!( - expected_longest_rows.contains(&longest_row), - "incorrect longest row {}. expected {:?} with length {}", - longest_row, - expected_longest_rows, - longest_line_len, - ); - } - } - - #[test] - fn test_chunks_equals_str() { - let text = "This is a multi-chunk\n& multi-line test string!"; - let rope = Rope::from(text); - for start in 0..text.len() { - for end in start..text.len() { - let range = start..end; - let correct_substring = &text[start..end]; - - // Test that correct range returns true - assert!( - rope.chunks_in_range(range.clone()) - .equals_str(correct_substring) - ); - assert!( - rope.reversed_chunks_in_range(range.clone()) - .equals_str(correct_substring) - ); - - // Test that all other ranges return false (unless they happen to match) - for other_start in 0..text.len() { - for other_end in other_start..text.len() { - if other_start == start && other_end == end { - continue; - } - let other_substring = &text[other_start..other_end]; - - // Only assert false if the substrings are actually different - if other_substring == correct_substring { - continue; - } - assert!( - !rope - .chunks_in_range(range.clone()) - .equals_str(other_substring) - ); - assert!( - !rope - .reversed_chunks_in_range(range.clone()) - .equals_str(other_substring) - ); - } - } - } - } - - let rope = Rope::from(""); - assert!(rope.chunks_in_range(0..0).equals_str("")); - assert!(rope.reversed_chunks_in_range(0..0).equals_str("")); - assert!(!rope.chunks_in_range(0..0).equals_str("foo")); - assert!(!rope.reversed_chunks_in_range(0..0).equals_str("foo")); - } - - #[test] - fn test_is_char_boundary() { - let fixture = "地"; - let rope = Rope::from("地"); - for b in 0..=fixture.len() { - assert_eq!(rope.is_char_boundary(b), fixture.is_char_boundary(b)); - } - let fixture = ""; - let rope = Rope::from(""); - for b in 0..=fixture.len() { - assert_eq!(rope.is_char_boundary(b), fixture.is_char_boundary(b)); - } - let fixture = "🔴🟠🟡🟢🔵🟣⚫️⚪️🟤\n🏳️‍⚧️🏁🏳️‍🌈🏴‍☠️⛳️📬📭🏴🏳️🚩"; - let rope = Rope::from("🔴🟠🟡🟢🔵🟣⚫️⚪️🟤\n🏳️‍⚧️🏁🏳️‍🌈🏴‍☠️⛳️📬📭🏴🏳️🚩"); - for b in 0..=fixture.len() { - assert_eq!(rope.is_char_boundary(b), fixture.is_char_boundary(b)); - } - } - - #[test] - fn test_floor_char_boundary() { - let fixture = "地"; - let rope = Rope::from("地"); - for b in 0..=fixture.len() { - assert_eq!(rope.floor_char_boundary(b), fixture.floor_char_boundary(b)); - } - - let fixture = ""; - let rope = Rope::from(""); - for b in 0..=fixture.len() { - assert_eq!(rope.floor_char_boundary(b), fixture.floor_char_boundary(b)); - } - - let fixture = "🔴🟠🟡🟢🔵🟣⚫️⚪️🟤\n🏳️‍⚧️🏁🏳️‍🌈🏴‍☠️⛳️📬📭🏴🏳️🚩"; - let rope = Rope::from("🔴🟠🟡🟢🔵🟣⚫️⚪️🟤\n🏳️‍⚧️🏁🏳️‍🌈🏴‍☠️⛳️📬📭🏴🏳️🚩"); - for b in 0..=fixture.len() { - assert_eq!(rope.floor_char_boundary(b), fixture.floor_char_boundary(b)); - } - } - - #[test] - fn test_ceil_char_boundary() { - let fixture = "地"; - let rope = Rope::from("地"); - for b in 0..=fixture.len() { - assert_eq!(rope.ceil_char_boundary(b), fixture.ceil_char_boundary(b)); - } - - let fixture = ""; - let rope = Rope::from(""); - for b in 0..=fixture.len() { - assert_eq!(rope.ceil_char_boundary(b), fixture.ceil_char_boundary(b)); - } - - let fixture = "🔴🟠🟡🟢🔵🟣⚫️⚪️🟤\n🏳️‍⚧️🏁🏳️‍🌈🏴‍☠️⛳️📬📭🏴🏳️🚩"; - let rope = Rope::from("🔴🟠🟡🟢🔵🟣⚫️⚪️🟤\n🏳️‍⚧️🏁🏳️‍🌈🏴‍☠️⛳️📬📭🏴🏳️🚩"); - for b in 0..=fixture.len() { - assert_eq!(rope.ceil_char_boundary(b), fixture.ceil_char_boundary(b)); - } - } - - fn clip_offset(text: &str, mut offset: usize, bias: Bias) -> usize { - while !text.is_char_boundary(offset) { - match bias { - Bias::Left => offset -= 1, - Bias::Right => offset += 1, - } - } - offset - } - - impl Rope { - fn text(&self) -> String { - let mut text = String::new(); - for chunk in self.chunks.cursor::<()>(()) { - text.push_str(&chunk.text); - } - text - } - } -} diff --git a/crates/rope/src/unclipped.rs b/crates/rope/src/unclipped.rs deleted file mode 100644 index abf82504ea..0000000000 --- a/crates/rope/src/unclipped.rs +++ /dev/null @@ -1,51 +0,0 @@ -use crate::ChunkSummary; -use std::ops::{Add, AddAssign, Sub, SubAssign}; - -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Unclipped(pub T); - -impl From for Unclipped { - fn from(value: T) -> Self { - Unclipped(value) - } -} - -impl<'a, T: sum_tree::Dimension<'a, ChunkSummary>> sum_tree::Dimension<'a, ChunkSummary> - for Unclipped -{ - fn zero(_: ()) -> Self { - Self(T::zero(())) - } - - fn add_summary(&mut self, summary: &'a ChunkSummary, _: ()) { - self.0.add_summary(summary, ()); - } -} - -impl> Add> for Unclipped { - type Output = Unclipped; - - fn add(self, rhs: Unclipped) -> Self::Output { - Unclipped(self.0 + rhs.0) - } -} - -impl> Sub> for Unclipped { - type Output = Unclipped; - - fn sub(self, rhs: Unclipped) -> Self::Output { - Unclipped(self.0 - rhs.0) - } -} - -impl> AddAssign> for Unclipped { - fn add_assign(&mut self, rhs: Unclipped) { - self.0 += rhs.0; - } -} - -impl> SubAssign> for Unclipped { - fn sub_assign(&mut self, rhs: Unclipped) { - self.0 -= rhs.0; - } -} diff --git a/crates/rpc/Cargo.toml b/crates/rpc/Cargo.toml deleted file mode 100644 index 10ebde26b6..0000000000 --- a/crates/rpc/Cargo.toml +++ /dev/null @@ -1,44 +0,0 @@ -[package] -description = "Shared logic for communication between the Zed app and the zed.dev server" -edition.workspace = true -name = "rpc" -version = "0.1.0" -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/rpc.rs" -doctest = false - -[features] -gpui = ["dep:gpui"] -test-support = ["collections/test-support", "gpui/test-support", "proto/test-support"] - -[dependencies] -anyhow.workspace = true -async-tungstenite.workspace = true -base64.workspace = true -chrono.workspace = true -collections.workspace = true -futures.workspace = true -gpui = { workspace = true, optional = true } -parking_lot.workspace = true -proto.workspace = true -rand.workspace = true -rsa.workspace = true -serde.workspace = true -serde_json.workspace = true -sha2.workspace = true -strum.workspace = true -tracing = { version = "0.1.34", features = ["log"] } -util.workspace = true -zstd.workspace = true - -[dev-dependencies] -collections = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -proto = { workspace = true, features = ["test-support"] } -zlog.workspace = true diff --git a/crates/rpc/LICENSE-GPL b/crates/rpc/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/rpc/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/rpc/src/auth.rs b/crates/rpc/src/auth.rs deleted file mode 100644 index 3829f3d36b..0000000000 --- a/crates/rpc/src/auth.rs +++ /dev/null @@ -1,240 +0,0 @@ -use anyhow::{Context as _, Result}; -use base64::prelude::*; -use rand::prelude::*; -use rsa::pkcs1::{DecodeRsaPublicKey, EncodeRsaPublicKey}; -use rsa::traits::PaddingScheme; -use rsa::{Oaep, Pkcs1v15Encrypt, RsaPrivateKey, RsaPublicKey}; -use sha2::Sha256; -use std::convert::TryFrom; - -fn oaep_sha256_padding() -> impl PaddingScheme { - Oaep::new::() -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum EncryptionFormat { - /// The original encryption format. - /// - /// This is using [`Pkcs1v15Encrypt`], which is vulnerable to side-channel attacks. - /// As such, we're in the process of phasing it out. - /// - /// See [here](https://people.redhat.com/~hkario/marvin/) for more details. - V0, - - /// The new encryption key format using Optimal Asymmetric Encryption Padding (OAEP) with a SHA-256 digest. - V1, -} - -pub struct PublicKey(RsaPublicKey); - -pub struct PrivateKey(RsaPrivateKey); - -/// Generate a public and private key for asymmetric encryption. -pub fn keypair() -> Result<(PublicKey, PrivateKey)> { - let mut rng = RsaRngCompat::new(); - let bits = 2048; - let private_key = RsaPrivateKey::new(&mut rng, bits)?; - let public_key = RsaPublicKey::from(&private_key); - Ok((PublicKey(public_key), PrivateKey(private_key))) -} - -/// Generate a random 64-character base64 string. -pub fn random_token() -> String { - let mut rng = rand::rng(); - let mut token_bytes = [0; 48]; - for byte in token_bytes.iter_mut() { - *byte = rng.random(); - } - BASE64_URL_SAFE.encode(token_bytes) -} - -impl PublicKey { - /// Convert a string to a base64-encoded string that can only be decoded with the corresponding - /// private key. - pub fn encrypt_string(&self, string: &str, format: EncryptionFormat) -> Result { - let mut rng = RsaRngCompat::new(); - let bytes = string.as_bytes(); - let encrypted_bytes = match format { - EncryptionFormat::V0 => self.0.encrypt(&mut rng, Pkcs1v15Encrypt, bytes), - EncryptionFormat::V1 => self.0.encrypt(&mut rng, oaep_sha256_padding(), bytes), - } - .context("failed to encrypt string with public key")?; - let encrypted_string = BASE64_URL_SAFE.encode(&encrypted_bytes); - Ok(encrypted_string) - } -} - -impl PrivateKey { - /// Decrypt a base64-encoded string that was encrypted by the corresponding public key. - pub fn decrypt_string(&self, encrypted_string: &str) -> Result { - let encrypted_bytes = BASE64_URL_SAFE - .decode(encrypted_string) - .context("failed to base64-decode encrypted string")?; - let bytes = self - .0 - .decrypt(oaep_sha256_padding(), &encrypted_bytes) - .or_else(|_err| { - // If we failed to decrypt using the new format, try decrypting with the old - // one to handle mismatches between the client and server. - self.0.decrypt(Pkcs1v15Encrypt, &encrypted_bytes) - }) - .context("failed to decrypt string with private key")?; - let string = String::from_utf8(bytes).context("decrypted content was not valid utf8")?; - Ok(string) - } -} - -impl TryFrom for String { - type Error = anyhow::Error; - fn try_from(key: PublicKey) -> Result { - let bytes = key - .0 - .to_pkcs1_der() - .context("failed to serialize public key")?; - let string = BASE64_URL_SAFE.encode(&bytes); - Ok(string) - } -} - -impl TryFrom for PublicKey { - type Error = anyhow::Error; - fn try_from(value: String) -> Result { - let bytes = BASE64_URL_SAFE - .decode(&value) - .context("failed to base64-decode public key string")?; - let key = Self(RsaPublicKey::from_pkcs1_der(&bytes).context("failed to parse public key")?); - Ok(key) - } -} - -// TODO: remove once we rsa v0.10 is released. -struct RsaRngCompat(rand::rngs::ThreadRng); - -impl RsaRngCompat { - fn new() -> Self { - Self(rand::rng()) - } -} - -impl rsa::signature::rand_core::RngCore for RsaRngCompat { - fn next_u32(&mut self) -> u32 { - self.0.next_u32() - } - - fn next_u64(&mut self) -> u64 { - self.0.next_u64() - } - - fn fill_bytes(&mut self, dest: &mut [u8]) { - self.0.fill_bytes(dest); - } - - fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rsa::signature::rand_core::Error> { - self.fill_bytes(dest); - Ok(()) - } -} - -impl rsa::signature::rand_core::CryptoRng for RsaRngCompat {} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_generate_encrypt_and_decrypt_token() { - // CLIENT: - // * generate a keypair for asymmetric encryption - // * serialize the public key to send it to the server. - let (public, private) = keypair().unwrap(); - let public_string = String::try_from(public).unwrap(); - assert_printable(&public_string); - - // SERVER: - // * parse the public key - // * generate a random token. - // * encrypt the token using the public key. - let public = PublicKey::try_from(public_string).unwrap(); - let token = random_token(); - let encrypted_token = public.encrypt_string(&token, EncryptionFormat::V1).unwrap(); - assert_eq!(token.len(), 64); - assert_ne!(encrypted_token, token); - assert_printable(&token); - assert_printable(&encrypted_token); - - // CLIENT: - // * decrypt the token using the private key. - let decrypted_token = private.decrypt_string(&encrypted_token).unwrap(); - assert_eq!(decrypted_token, token); - } - - #[test] - fn test_generate_encrypt_and_decrypt_token_with_v0_encryption_format() { - // CLIENT: - // * generate a keypair for asymmetric encryption - // * serialize the public key to send it to the server. - let (public, private) = keypair().unwrap(); - let public_string = String::try_from(public).unwrap(); - assert_printable(&public_string); - - // SERVER: - // * parse the public key - // * generate a random token. - // * encrypt the token using the public key. - let public = PublicKey::try_from(public_string).unwrap(); - let token = random_token(); - let encrypted_token = public.encrypt_string(&token, EncryptionFormat::V0).unwrap(); - assert_eq!(token.len(), 64); - assert_ne!(encrypted_token, token); - assert_printable(&token); - assert_printable(&encrypted_token); - - // CLIENT: - // * decrypt the token using the private key. - let decrypted_token = private.decrypt_string(&encrypted_token).unwrap(); - assert_eq!(decrypted_token, token); - } - - #[test] - fn test_encode_and_decode_base64_public_key() { - // A base64-encoded public key. - // - // We're using a literal string to ensure that encoding and decoding works across differences in implementations. - let encoded_public_key = "MIGJAoGBAMPvufou8wOuUIF1Wlkbtn0ZMM9nC55QJ06nTZvgMfZv5esFVU9-cQO_JC1P9ZoEcMDJweFERnQuQLqzsrMDLFbkdgL128ZU43WOLiQraxaICFIZsPUeTtWMKp2D5bPWsNxs-lnCma7vCAry6fpXuj5AKQdk7cTZJNucgvZQ0uUfAgMBAAE=".to_string(); - - // Make sure we can parse the public key. - let public_key = PublicKey::try_from(encoded_public_key.clone()).unwrap(); - - // Make sure we re-encode to the same format. - assert_eq!(encoded_public_key, String::try_from(public_key).unwrap()); - } - - #[test] - fn test_tokens_are_always_url_safe() { - for _ in 0..5 { - let token = random_token(); - let (public_key, _) = keypair().unwrap(); - let encrypted_token = public_key - .encrypt_string(&token, EncryptionFormat::V1) - .unwrap(); - let public_key_str = String::try_from(public_key).unwrap(); - - assert_printable(&token); - assert_printable(&public_key_str); - assert_printable(&encrypted_token); - } - } - - fn assert_printable(token: &str) { - for c in token.chars() { - assert!( - c.is_ascii_graphic(), - "token {:?} has non-printable char {}", - token, - c - ); - assert_ne!(c, '/', "token {:?} is not URL-safe", token); - assert_ne!(c, '&', "token {:?} is not URL-safe", token); - } - } -} diff --git a/crates/rpc/src/conn.rs b/crates/rpc/src/conn.rs deleted file mode 100644 index e598e5f7bc..0000000000 --- a/crates/rpc/src/conn.rs +++ /dev/null @@ -1,102 +0,0 @@ -use async_tungstenite::tungstenite::Message as WebSocketMessage; -use futures::{SinkExt as _, StreamExt as _}; - -pub struct Connection { - pub(crate) tx: - Box>, - pub(crate) rx: - Box>>, -} - -impl Connection { - pub fn new(stream: S) -> Self - where - S: 'static - + Send - + Unpin - + futures::Sink - + futures::Stream>, - { - let (tx, rx) = stream.split(); - Self { - tx: Box::new(tx), - rx: Box::new(rx), - } - } - - pub async fn send(&mut self, message: WebSocketMessage) -> anyhow::Result<()> { - self.tx.send(message).await - } - - #[cfg(any(test, feature = "test-support"))] - pub fn in_memory( - executor: gpui::BackgroundExecutor, - ) -> (Self, Self, std::sync::Arc) { - use std::sync::{ - Arc, - atomic::{AtomicBool, Ordering::SeqCst}, - }; - - let killed = Arc::new(AtomicBool::new(false)); - let (a_tx, a_rx) = channel(killed.clone(), executor.clone()); - let (b_tx, b_rx) = channel(killed.clone(), executor); - return ( - Self { tx: a_tx, rx: b_rx }, - Self { tx: b_tx, rx: a_rx }, - killed, - ); - - #[allow(clippy::type_complexity)] - fn channel( - killed: Arc, - executor: gpui::BackgroundExecutor, - ) -> ( - Box>, - Box>>, - ) { - use anyhow::anyhow; - use futures::channel::mpsc; - use std::io::Error; - - let (tx, rx) = mpsc::unbounded::(); - - let tx = tx.sink_map_err(|error| anyhow!(error)).with({ - let killed = killed.clone(); - let executor = executor.clone(); - move |msg| { - let killed = killed.clone(); - let executor = executor.clone(); - Box::pin(async move { - executor.simulate_random_delay().await; - - // Writes to a half-open TCP connection will error. - if killed.load(SeqCst) { - std::io::Result::Err(Error::other("connection lost"))?; - } - - Ok(msg) - }) - } - }); - - let rx = rx.then({ - move |msg| { - let killed = killed.clone(); - let executor = executor.clone(); - Box::pin(async move { - executor.simulate_random_delay().await; - - // Reads from a half-open TCP connection will hang. - if killed.load(SeqCst) { - futures::future::pending::<()>().await; - } - - Ok(msg) - }) - } - }); - - (Box::new(tx), Box::new(rx)) - } - } -} diff --git a/crates/rpc/src/extension.rs b/crates/rpc/src/extension.rs deleted file mode 100644 index 1b00312bad..0000000000 --- a/crates/rpc/src/extension.rs +++ /dev/null @@ -1,64 +0,0 @@ -use std::collections::BTreeSet; -use std::sync::Arc; - -use chrono::{DateTime, Utc}; -use serde::{Deserialize, Serialize}; -use strum::EnumString; - -#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] -pub struct ExtensionApiManifest { - pub name: String, - pub version: Arc, - pub description: Option, - pub authors: Vec, - pub repository: String, - pub schema_version: Option, - pub wasm_api_version: Option, - #[serde(default)] - pub provides: BTreeSet, -} - -#[derive( - Debug, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - Clone, - Copy, - Serialize, - Deserialize, - EnumString, - strum::Display, - strum::EnumIter, -)] -#[serde(rename_all = "kebab-case")] -#[strum(serialize_all = "kebab-case")] -pub enum ExtensionProvides { - Themes, - IconThemes, - Languages, - Grammars, - LanguageServers, - ContextServers, - AgentServers, - SlashCommands, - IndexedDocsProviders, - Snippets, - DebugAdapters, -} - -#[derive(Clone, Serialize, Deserialize, Debug, PartialEq)] -pub struct ExtensionMetadata { - pub id: Arc, - #[serde(flatten)] - pub manifest: ExtensionApiManifest, - pub published_at: DateTime, - pub download_count: u64, -} - -#[derive(Serialize, Deserialize)] -pub struct GetExtensionsResponse { - pub data: Vec, -} diff --git a/crates/rpc/src/macros.rs b/crates/rpc/src/macros.rs deleted file mode 100644 index f85889a97b..0000000000 --- a/crates/rpc/src/macros.rs +++ /dev/null @@ -1,73 +0,0 @@ -#[macro_export] -macro_rules! messages { - ($(($name:ident, $priority:ident)),* $(,)?) => { - pub fn build_typed_envelope(sender_id: ConnectionId, received_at: Instant, envelope: Envelope) -> Option> { - match envelope.payload { - $(Some(envelope::Payload::$name(payload)) => { - Some(Box::new(TypedEnvelope { - sender_id, - original_sender_id: envelope.original_sender_id.map(|original_sender| PeerId { - owner_id: original_sender.owner_id, - id: original_sender.id - }), - message_id: envelope.id, - payload, - received_at, - })) - }, )* - _ => None - } - } - - $( - impl EnvelopedMessage for $name { - const NAME: &'static str = std::stringify!($name); - const PRIORITY: MessagePriority = MessagePriority::$priority; - - fn into_envelope( - self, - id: u32, - responding_to: Option, - original_sender_id: Option, - ) -> Envelope { - Envelope { - id, - responding_to, - original_sender_id, - payload: Some(envelope::Payload::$name(self)), - } - } - - fn from_envelope(envelope: Envelope) -> Option { - if let Some(envelope::Payload::$name(msg)) = envelope.payload { - Some(msg) - } else { - None - } - } - } - )* - }; -} - -#[macro_export] -macro_rules! request_messages { - ($(($request_name:ident, $response_name:ident)),* $(,)?) => { - $(impl RequestMessage for $request_name { - type Response = $response_name; - })* - }; -} - -#[macro_export] -macro_rules! entity_messages { - ({$id_field:ident, $entity_type:ty}, $($name:ident),* $(,)?) => { - $(impl EntityMessage for $name { - type Entity = $entity_type; - - fn remote_entity_id(&self) -> u64 { - self.$id_field - } - })* - }; -} diff --git a/crates/rpc/src/message_stream.rs b/crates/rpc/src/message_stream.rs deleted file mode 100644 index 023e916df3..0000000000 --- a/crates/rpc/src/message_stream.rs +++ /dev/null @@ -1,143 +0,0 @@ -#![allow(non_snake_case)] - -pub use ::proto::*; - -use async_tungstenite::tungstenite::Message as WebSocketMessage; -use futures::{SinkExt as _, StreamExt as _}; -use proto::Message as _; -use std::time::Instant; -use std::{fmt::Debug, io}; -use zstd::zstd_safe::WriteBuf; - -const KIB: usize = 1024; -const MIB: usize = KIB * 1024; -const MAX_BUFFER_LEN: usize = MIB; - -/// A stream of protobuf messages. -pub struct MessageStream { - stream: S, - encoding_buffer: Vec, -} - -#[derive(Debug)] -pub enum Message { - Envelope(Envelope), - Ping, - Pong, -} - -impl MessageStream { - pub fn new(stream: S) -> Self { - Self { - stream, - encoding_buffer: Vec::new(), - } - } -} - -impl MessageStream -where - S: futures::Sink + Unpin, -{ - pub async fn write(&mut self, message: Message) -> anyhow::Result<()> { - #[cfg(any(test, feature = "test-support"))] - const COMPRESSION_LEVEL: i32 = -7; - - #[cfg(not(any(test, feature = "test-support")))] - const COMPRESSION_LEVEL: i32 = 4; - - match message { - Message::Envelope(message) => { - self.encoding_buffer.reserve(message.encoded_len()); - message - .encode(&mut self.encoding_buffer) - .map_err(io::Error::from)?; - let buffer = - zstd::stream::encode_all(self.encoding_buffer.as_slice(), COMPRESSION_LEVEL) - .unwrap(); - - self.encoding_buffer.clear(); - self.encoding_buffer.shrink_to(MAX_BUFFER_LEN); - self.stream - .send(WebSocketMessage::Binary(buffer.into())) - .await?; - } - Message::Ping => { - self.stream - .send(WebSocketMessage::Ping(Default::default())) - .await?; - } - Message::Pong => { - self.stream - .send(WebSocketMessage::Pong(Default::default())) - .await?; - } - } - - Ok(()) - } -} - -impl MessageStream -where - S: futures::Stream> + Unpin, -{ - pub async fn read(&mut self) -> anyhow::Result<(Message, Instant)> { - while let Some(bytes) = self.stream.next().await { - let received_at = Instant::now(); - match bytes? { - WebSocketMessage::Binary(bytes) => { - zstd::stream::copy_decode(bytes.as_slice(), &mut self.encoding_buffer)?; - let envelope = Envelope::decode(self.encoding_buffer.as_slice()) - .map_err(io::Error::from)?; - - self.encoding_buffer.clear(); - self.encoding_buffer.shrink_to(MAX_BUFFER_LEN); - return Ok((Message::Envelope(envelope), received_at)); - } - WebSocketMessage::Ping(_) => return Ok((Message::Ping, received_at)), - WebSocketMessage::Pong(_) => return Ok((Message::Pong, received_at)), - WebSocketMessage::Close(_) => break, - _ => {} - } - } - anyhow::bail!("connection closed"); - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[gpui::test] - async fn test_buffer_size() { - let (tx, rx) = futures::channel::mpsc::unbounded(); - let mut sink = MessageStream::new(tx.sink_map_err(|_| anyhow::anyhow!(""))); - sink.write(Message::Envelope(Envelope { - payload: Some(envelope::Payload::UpdateWorktree(UpdateWorktree { - root_name: "abcdefg".repeat(10), - ..Default::default() - })), - ..Default::default() - })) - .await - .unwrap(); - assert!(sink.encoding_buffer.capacity() <= MAX_BUFFER_LEN); - sink.write(Message::Envelope(Envelope { - payload: Some(envelope::Payload::UpdateWorktree(UpdateWorktree { - root_name: "abcdefg".repeat(1000000), - ..Default::default() - })), - ..Default::default() - })) - .await - .unwrap(); - assert!(sink.encoding_buffer.capacity() <= MAX_BUFFER_LEN); - - let mut stream = MessageStream::new(rx.map(anyhow::Ok)); - stream.read().await.unwrap(); - assert!(stream.encoding_buffer.capacity() <= MAX_BUFFER_LEN); - stream.read().await.unwrap(); - assert!(stream.encoding_buffer.capacity() <= MAX_BUFFER_LEN); - } -} diff --git a/crates/rpc/src/notification.rs b/crates/rpc/src/notification.rs deleted file mode 100644 index 50364c7387..0000000000 --- a/crates/rpc/src/notification.rs +++ /dev/null @@ -1,99 +0,0 @@ -use crate::proto; -use serde::{Deserialize, Serialize}; -use serde_json::{Value, map}; -use strum::VariantNames; - -const KIND: &str = "kind"; -const ENTITY_ID: &str = "entity_id"; - -/// A notification that can be stored, associated with a given recipient. -/// -/// This struct is stored in the collab database as JSON, so it shouldn't be -/// changed in a backward-incompatible way. For example, when renaming a -/// variant, add a serde alias for the old name. -/// -/// Most notification types have a special field which is aliased to -/// `entity_id`. This field is stored in its own database column, and can -/// be used to query the notification. -#[derive(Debug, Clone, PartialEq, Eq, VariantNames, Serialize, Deserialize)] -#[serde(tag = "kind")] -pub enum Notification { - ContactRequest { - #[serde(rename = "entity_id")] - sender_id: u64, - }, - ContactRequestAccepted { - #[serde(rename = "entity_id")] - responder_id: u64, - }, - ChannelInvitation { - #[serde(rename = "entity_id")] - channel_id: u64, - channel_name: String, - inviter_id: u64, - }, -} - -impl Notification { - pub fn to_proto(&self) -> proto::Notification { - let mut value = serde_json::to_value(self).unwrap(); - let mut entity_id = None; - let value = value.as_object_mut().unwrap(); - let Some(Value::String(kind)) = value.remove(KIND) else { - unreachable!("kind is the enum tag") - }; - if let map::Entry::Occupied(e) = value.entry(ENTITY_ID) - && e.get().is_u64() - { - entity_id = e.remove().as_u64(); - } - proto::Notification { - kind, - entity_id, - content: serde_json::to_string(&value).unwrap(), - ..Default::default() - } - } - - pub fn from_proto(notification: &proto::Notification) -> Option { - let mut value = serde_json::from_str::(¬ification.content).ok()?; - let object = value.as_object_mut()?; - object.insert(KIND.into(), notification.kind.to_string().into()); - if let Some(entity_id) = notification.entity_id { - object.insert(ENTITY_ID.into(), entity_id.into()); - } - serde_json::from_value(value).ok() - } - - pub fn all_variant_names() -> &'static [&'static str] { - Self::VARIANTS - } -} - -#[cfg(test)] -mod tests { - use crate::Notification; - - #[test] - fn test_notification() { - // Notifications can be serialized and deserialized. - for notification in [ - Notification::ContactRequest { sender_id: 1 }, - Notification::ContactRequestAccepted { responder_id: 2 }, - Notification::ChannelInvitation { - channel_id: 100, - channel_name: "the-channel".into(), - inviter_id: 50, - }, - ] { - let message = notification.to_proto(); - let deserialized = Notification::from_proto(&message).unwrap(); - assert_eq!(deserialized, notification); - } - - // When notifications are serialized, the `kind` and `actor_id` fields are - // stored separately, and do not appear redundantly in the JSON. - let notification = Notification::ContactRequest { sender_id: 1 }; - assert_eq!(notification.to_proto().content, "{}"); - } -} diff --git a/crates/rpc/src/peer.rs b/crates/rpc/src/peer.rs deleted file mode 100644 index 73be0f19fe..0000000000 --- a/crates/rpc/src/peer.rs +++ /dev/null @@ -1,1055 +0,0 @@ -use super::{ - Connection, - message_stream::{Message, MessageStream}, - proto::{ - self, AnyTypedEnvelope, EnvelopedMessage, PeerId, Receipt, RequestMessage, TypedEnvelope, - }, -}; -use anyhow::{Context as _, Result, anyhow}; -use collections::HashMap; -use futures::{ - FutureExt, SinkExt, Stream, StreamExt, TryFutureExt, - channel::{mpsc, oneshot}, - stream::BoxStream, -}; -use parking_lot::{Mutex, RwLock}; -use proto::{ErrorCode, ErrorCodeExt, ErrorExt, RpcError}; -use serde::{Serialize, ser::SerializeStruct}; -use std::{ - fmt, future, - future::Future, - sync::atomic::Ordering::SeqCst, - sync::{ - Arc, - atomic::{self, AtomicU32}, - }, - time::Duration, - time::Instant, -}; - -#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize)] -pub struct ConnectionId { - pub owner_id: u32, - pub id: u32, -} - -impl From for PeerId { - fn from(id: ConnectionId) -> Self { - PeerId { - owner_id: id.owner_id, - id: id.id, - } - } -} - -impl From for ConnectionId { - fn from(peer_id: PeerId) -> Self { - Self { - owner_id: peer_id.owner_id, - id: peer_id.id, - } - } -} - -impl fmt::Display for ConnectionId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}/{}", self.owner_id, self.id) - } -} - -pub struct Peer { - epoch: AtomicU32, - pub connections: RwLock>, - next_connection_id: AtomicU32, -} - -#[derive(Clone, Serialize)] -pub struct ConnectionState { - #[serde(skip)] - outgoing_tx: mpsc::UnboundedSender, - next_message_id: Arc, - #[allow(clippy::type_complexity)] - #[serde(skip)] - response_channels: Arc< - Mutex< - Option< - HashMap< - u32, - oneshot::Sender<(proto::Envelope, std::time::Instant, oneshot::Sender<()>)>, - >, - >, - >, - >, - #[allow(clippy::type_complexity)] - #[serde(skip)] - stream_response_channels: Arc< - Mutex< - Option< - HashMap, oneshot::Sender<()>)>>, - >, - >, - >, -} - -const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(1); -const WRITE_TIMEOUT: Duration = Duration::from_secs(2); -pub const RECEIVE_TIMEOUT: Duration = Duration::from_secs(10); - -impl Peer { - pub fn new(epoch: u32) -> Arc { - Arc::new(Self { - epoch: AtomicU32::new(epoch), - connections: Default::default(), - next_connection_id: Default::default(), - }) - } - - pub fn epoch(&self) -> u32 { - self.epoch.load(SeqCst) - } - - pub fn add_connection( - self: &Arc, - connection: Connection, - create_timer: F, - ) -> ( - ConnectionId, - impl Future> + Send + use, - BoxStream<'static, Box>, - ) - where - F: Send + Fn(Duration) -> Fut, - Fut: Send + Future, - Out: Send, - { - // For outgoing messages, use an unbounded channel so that application code - // can always send messages without yielding. For incoming messages, use a - // bounded channel so that other peers will receive backpressure if they send - // messages faster than this peer can process them. - #[cfg(any(test, feature = "test-support"))] - const INCOMING_BUFFER_SIZE: usize = 1; - #[cfg(not(any(test, feature = "test-support")))] - const INCOMING_BUFFER_SIZE: usize = 256; - let (mut incoming_tx, incoming_rx) = mpsc::channel(INCOMING_BUFFER_SIZE); - let (outgoing_tx, mut outgoing_rx) = mpsc::unbounded(); - - let connection_id = ConnectionId { - owner_id: self.epoch.load(SeqCst), - id: self.next_connection_id.fetch_add(1, SeqCst), - }; - let connection_state = ConnectionState { - outgoing_tx, - next_message_id: Default::default(), - response_channels: Arc::new(Mutex::new(Some(Default::default()))), - stream_response_channels: Arc::new(Mutex::new(Some(Default::default()))), - }; - let mut writer = MessageStream::new(connection.tx); - let mut reader = MessageStream::new(connection.rx); - - let this = self.clone(); - let response_channels = connection_state.response_channels.clone(); - let stream_response_channels = connection_state.stream_response_channels.clone(); - - let handle_io = async move { - tracing::trace!(%connection_id, "handle io future: start"); - - let _end_connection = util::defer(|| { - response_channels.lock().take(); - if let Some(channels) = stream_response_channels.lock().take() { - for channel in channels.values() { - let _ = channel.unbounded_send(( - Err(anyhow!("connection closed")), - oneshot::channel().0, - )); - } - } - this.connections.write().remove(&connection_id); - tracing::trace!(%connection_id, "handle io future: end"); - }); - - // Send messages on this frequency so the connection isn't closed. - let keepalive_timer = create_timer(KEEPALIVE_INTERVAL).fuse(); - futures::pin_mut!(keepalive_timer); - - // Disconnect if we don't receive messages at least this frequently. - let receive_timeout = create_timer(RECEIVE_TIMEOUT).fuse(); - futures::pin_mut!(receive_timeout); - - loop { - tracing::trace!(%connection_id, "outer loop iteration start"); - let read_message = reader.read().fuse(); - futures::pin_mut!(read_message); - - loop { - tracing::trace!(%connection_id, "inner loop iteration start"); - futures::select_biased! { - outgoing = outgoing_rx.next().fuse() => match outgoing { - Some(outgoing) => { - tracing::trace!(%connection_id, "outgoing rpc message: writing"); - futures::select_biased! { - result = writer.write(outgoing).fuse() => { - tracing::trace!(%connection_id, "outgoing rpc message: done writing"); - result.context("failed to write RPC message")?; - tracing::trace!(%connection_id, "keepalive interval: resetting after sending message"); - keepalive_timer.set(create_timer(KEEPALIVE_INTERVAL).fuse()); - } - _ = create_timer(WRITE_TIMEOUT).fuse() => { - tracing::trace!(%connection_id, "outgoing rpc message: writing timed out"); - anyhow::bail!("timed out writing message"); - } - } - } - None => { - tracing::trace!(%connection_id, "outgoing rpc message: channel closed"); - return Ok(()) - }, - }, - _ = keepalive_timer => { - tracing::trace!(%connection_id, "keepalive interval: pinging"); - futures::select_biased! { - result = writer.write(Message::Ping).fuse() => { - tracing::trace!(%connection_id, "keepalive interval: done pinging"); - result.context("failed to send keepalive")?; - tracing::trace!(%connection_id, "keepalive interval: resetting after pinging"); - keepalive_timer.set(create_timer(KEEPALIVE_INTERVAL).fuse()); - } - _ = create_timer(WRITE_TIMEOUT).fuse() => { - tracing::trace!(%connection_id, "keepalive interval: pinging timed out"); - anyhow::bail!("timed out sending keepalive"); - } - } - } - incoming = read_message => { - let incoming = incoming.context("error reading rpc message from socket")?; - tracing::trace!(%connection_id, "incoming rpc message: received"); - tracing::trace!(%connection_id, "receive timeout: resetting"); - receive_timeout.set(create_timer(RECEIVE_TIMEOUT).fuse()); - if let (Message::Envelope(incoming), received_at) = incoming { - tracing::trace!(%connection_id, "incoming rpc message: processing"); - futures::select_biased! { - result = incoming_tx.send((incoming, received_at)).fuse() => match result { - Ok(_) => { - tracing::trace!(%connection_id, "incoming rpc message: processed"); - } - Err(_) => { - tracing::trace!(%connection_id, "incoming rpc message: channel closed"); - return Ok(()) - } - }, - _ = create_timer(WRITE_TIMEOUT).fuse() => { - tracing::trace!(%connection_id, "incoming rpc message: processing timed out"); - anyhow::bail!("timed out processing incoming message"); - } - } - } - break; - }, - _ = receive_timeout => { - tracing::trace!(%connection_id, "receive timeout: delay between messages too long"); - anyhow::bail!("delay between messages too long"); - } - } - } - } - }; - - let response_channels = connection_state.response_channels.clone(); - let stream_response_channels = connection_state.stream_response_channels.clone(); - self.connections - .write() - .insert(connection_id, connection_state); - - let incoming_rx = incoming_rx.filter_map(move |(incoming, received_at)| { - let response_channels = response_channels.clone(); - let stream_response_channels = stream_response_channels.clone(); - async move { - let message_id = incoming.id; - tracing::trace!(?incoming, "incoming message future: start"); - let _end = util::defer(move || { - tracing::trace!(%connection_id, message_id, "incoming message future: end"); - }); - - if let Some(responding_to) = incoming.responding_to { - tracing::trace!( - %connection_id, - message_id, - responding_to, - "incoming response: received" - ); - let response_channel = - response_channels.lock().as_mut()?.remove(&responding_to); - let stream_response_channel = stream_response_channels - .lock() - .as_ref()? - .get(&responding_to) - .cloned(); - - if let Some(tx) = response_channel { - let requester_resumed = oneshot::channel(); - if let Err(error) = tx.send((incoming, received_at, requester_resumed.0)) { - tracing::trace!( - %connection_id, - message_id, - responding_to = responding_to, - ?error, - "incoming response: request future dropped", - ); - } - - tracing::trace!( - %connection_id, - message_id, - responding_to, - "incoming response: waiting to resume requester" - ); - let _ = requester_resumed.1.await; - tracing::trace!( - %connection_id, - message_id, - responding_to, - "incoming response: requester resumed" - ); - } else if let Some(tx) = stream_response_channel { - let requester_resumed = oneshot::channel(); - if let Err(error) = tx.unbounded_send((Ok(incoming), requester_resumed.0)) { - tracing::debug!( - %connection_id, - message_id, - responding_to = responding_to, - ?error, - "incoming stream response: request future dropped", - ); - } - - tracing::debug!( - %connection_id, - message_id, - responding_to, - "incoming stream response: waiting to resume requester" - ); - let _ = requester_resumed.1.await; - tracing::debug!( - %connection_id, - message_id, - responding_to, - "incoming stream response: requester resumed" - ); - } else { - let message_type = proto::build_typed_envelope( - connection_id.into(), - received_at, - incoming, - ) - .map(|p| p.payload_type_name()); - tracing::warn!( - %connection_id, - message_id, - responding_to, - message_type, - "incoming response: unknown request" - ); - } - - None - } else { - tracing::trace!(%connection_id, message_id, "incoming message: received"); - proto::build_typed_envelope(connection_id.into(), received_at, incoming) - .or_else(|| { - tracing::error!( - %connection_id, - message_id, - "unable to construct a typed envelope" - ); - None - }) - } - } - }); - (connection_id, handle_io, incoming_rx.boxed()) - } - - #[cfg(any(test, feature = "test-support"))] - pub fn add_test_connection( - self: &Arc, - connection: Connection, - executor: gpui::BackgroundExecutor, - ) -> ( - ConnectionId, - impl Future> + Send + use<>, - BoxStream<'static, Box>, - ) { - self.add_connection(connection, move |duration| executor.timer(duration)) - } - - pub fn disconnect(&self, connection_id: ConnectionId) { - self.connections.write().remove(&connection_id); - } - - #[cfg(any(test, feature = "test-support"))] - pub fn reset(&self, epoch: u32) { - self.next_connection_id.store(0, SeqCst); - self.epoch.store(epoch, SeqCst); - } - - pub fn teardown(&self) { - self.connections.write().clear(); - } - - /// Make a request and wait for a response. - pub fn request( - &self, - receiver_id: ConnectionId, - request: T, - ) -> impl Future> + use { - self.request_internal(None, receiver_id, request) - .map_ok(|envelope| envelope.payload) - } - - pub fn request_envelope( - &self, - receiver_id: ConnectionId, - request: T, - ) -> impl Future>> + use { - self.request_internal(None, receiver_id, request) - } - - pub fn forward_request( - &self, - sender_id: ConnectionId, - receiver_id: ConnectionId, - request: T, - ) -> impl Future> { - self.request_internal(Some(sender_id), receiver_id, request) - .map_ok(|envelope| envelope.payload) - } - - fn request_internal( - &self, - original_sender_id: Option, - receiver_id: ConnectionId, - request: T, - ) -> impl Future>> + use { - let envelope = request.into_envelope(0, None, original_sender_id.map(Into::into)); - let response = self.request_dynamic(receiver_id, envelope, T::NAME); - async move { - let (response, received_at) = response.await?; - Ok(TypedEnvelope { - message_id: response.id, - sender_id: receiver_id.into(), - original_sender_id: response.original_sender_id, - payload: T::Response::from_envelope(response) - .context("received response of the wrong type")?, - received_at, - }) - } - } - - /// Make a request and wait for a response. - /// - /// The caller must make sure to deserialize the response into the request's - /// response type. This interface is only useful in trait objects, where - /// generics can't be used. If you have a concrete type, use `request`. - pub fn request_dynamic( - &self, - receiver_id: ConnectionId, - mut envelope: proto::Envelope, - type_name: &'static str, - ) -> impl Future> + use<> { - let (tx, rx) = oneshot::channel(); - let send = self.connection_state(receiver_id).and_then(|connection| { - envelope.id = connection.next_message_id.fetch_add(1, SeqCst); - connection - .response_channels - .lock() - .as_mut() - .context("connection was closed")? - .insert(envelope.id, tx); - connection - .outgoing_tx - .unbounded_send(Message::Envelope(envelope)) - .context("connection was closed")?; - Ok(()) - }); - async move { - send?; - let (response, received_at, _barrier) = rx.await.context("connection was closed")?; - if let Some(proto::envelope::Payload::Error(error)) = &response.payload { - return Err(RpcError::from_proto(error, type_name)); - } - Ok((response, received_at)) - } - } - - pub fn request_stream( - &self, - receiver_id: ConnectionId, - request: T, - ) -> impl Future>>> { - let (tx, rx) = mpsc::unbounded(); - let send = self.connection_state(receiver_id).and_then(|connection| { - let message_id = connection.next_message_id.fetch_add(1, SeqCst); - let stream_response_channels = connection.stream_response_channels.clone(); - stream_response_channels - .lock() - .as_mut() - .context("connection was closed")? - .insert(message_id, tx); - connection - .outgoing_tx - .unbounded_send(Message::Envelope( - request.into_envelope(message_id, None, None), - )) - .context("connection was closed")?; - Ok((message_id, stream_response_channels)) - }); - - async move { - let (message_id, stream_response_channels) = send?; - let stream_response_channels = Arc::downgrade(&stream_response_channels); - - Ok(rx.filter_map(move |(response, _barrier)| { - let stream_response_channels = stream_response_channels.clone(); - future::ready(match response { - Ok(response) => { - if let Some(proto::envelope::Payload::Error(error)) = &response.payload { - Some(Err(RpcError::from_proto(error, T::NAME))) - } else if let Some(proto::envelope::Payload::EndStream(_)) = - &response.payload - { - // Remove the transmitting end of the response channel to end the stream. - if let Some(channels) = stream_response_channels.upgrade() - && let Some(channels) = channels.lock().as_mut() - { - channels.remove(&message_id); - } - None - } else { - Some( - T::Response::from_envelope(response) - .context("received response of the wrong type"), - ) - } - } - Err(error) => Some(Err(error)), - }) - })) - } - } - - pub fn send(&self, receiver_id: ConnectionId, message: T) -> Result<()> { - let connection = self.connection_state(receiver_id)?; - let message_id = connection - .next_message_id - .fetch_add(1, atomic::Ordering::SeqCst); - connection.outgoing_tx.unbounded_send(Message::Envelope( - message.into_envelope(message_id, None, None), - ))?; - Ok(()) - } - - pub fn send_dynamic(&self, receiver_id: ConnectionId, message: proto::Envelope) -> Result<()> { - let connection = self.connection_state(receiver_id)?; - connection - .outgoing_tx - .unbounded_send(Message::Envelope(message))?; - Ok(()) - } - - pub fn forward_send( - &self, - sender_id: ConnectionId, - receiver_id: ConnectionId, - message: T, - ) -> Result<()> { - let connection = self.connection_state(receiver_id)?; - let message_id = connection - .next_message_id - .fetch_add(1, atomic::Ordering::SeqCst); - connection - .outgoing_tx - .unbounded_send(Message::Envelope(message.into_envelope( - message_id, - None, - Some(sender_id.into()), - )))?; - Ok(()) - } - - pub fn respond( - &self, - receipt: Receipt, - response: T::Response, - ) -> Result<()> { - let connection = self.connection_state(receipt.sender_id.into())?; - let message_id = connection - .next_message_id - .fetch_add(1, atomic::Ordering::SeqCst); - connection - .outgoing_tx - .unbounded_send(Message::Envelope(response.into_envelope( - message_id, - Some(receipt.message_id), - None, - )))?; - Ok(()) - } - - pub fn end_stream(&self, receipt: Receipt) -> Result<()> { - let connection = self.connection_state(receipt.sender_id.into())?; - let message_id = connection - .next_message_id - .fetch_add(1, atomic::Ordering::SeqCst); - - let message = proto::EndStream {}; - - connection - .outgoing_tx - .unbounded_send(Message::Envelope(message.into_envelope( - message_id, - Some(receipt.message_id), - None, - )))?; - Ok(()) - } - - pub fn respond_with_error( - &self, - receipt: Receipt, - response: proto::Error, - ) -> Result<()> { - let connection = self.connection_state(receipt.sender_id.into())?; - let message_id = connection - .next_message_id - .fetch_add(1, atomic::Ordering::SeqCst); - connection - .outgoing_tx - .unbounded_send(Message::Envelope(response.into_envelope( - message_id, - Some(receipt.message_id), - None, - )))?; - Ok(()) - } - - pub fn respond_with_unhandled_message( - &self, - sender_id: ConnectionId, - request_message_id: u32, - message_type_name: &'static str, - ) -> Result<()> { - let connection = self.connection_state(sender_id)?; - let response = ErrorCode::Internal - .message(format!("message {} was not handled", message_type_name)) - .to_proto(); - let message_id = connection - .next_message_id - .fetch_add(1, atomic::Ordering::SeqCst); - connection - .outgoing_tx - .unbounded_send(Message::Envelope(response.into_envelope( - message_id, - Some(request_message_id), - None, - )))?; - Ok(()) - } - - fn connection_state(&self, connection_id: ConnectionId) -> Result { - let connections = self.connections.read(); - let connection = connections - .get(&connection_id) - .with_context(|| format!("no such connection: {connection_id}"))?; - Ok(connection.clone()) - } -} - -impl Serialize for Peer { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - let mut state = serializer.serialize_struct("Peer", 2)?; - state.serialize_field("connections", &*self.connections.read())?; - state.end() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use async_tungstenite::tungstenite::Message as WebSocketMessage; - use gpui::TestAppContext; - - fn init_logger() { - zlog::init_test(); - } - - #[gpui::test(iterations = 50)] - async fn test_request_response(cx: &mut TestAppContext) { - init_logger(); - - let executor = cx.executor(); - - // create 2 clients connected to 1 server - let server = Peer::new(0); - let client1 = Peer::new(0); - let client2 = Peer::new(0); - - let (client1_to_server_conn, server_to_client_1_conn, _kill) = - Connection::in_memory(cx.executor()); - let (client1_conn_id, io_task1, client1_incoming) = - client1.add_test_connection(client1_to_server_conn, cx.executor()); - let (_, io_task2, server_incoming1) = - server.add_test_connection(server_to_client_1_conn, cx.executor()); - - let (client2_to_server_conn, server_to_client_2_conn, _kill) = - Connection::in_memory(cx.executor()); - let (client2_conn_id, io_task3, client2_incoming) = - client2.add_test_connection(client2_to_server_conn, cx.executor()); - let (_, io_task4, server_incoming2) = - server.add_test_connection(server_to_client_2_conn, cx.executor()); - - executor.spawn(io_task1).detach(); - executor.spawn(io_task2).detach(); - executor.spawn(io_task3).detach(); - executor.spawn(io_task4).detach(); - executor - .spawn(handle_messages(server_incoming1, server.clone())) - .detach(); - executor - .spawn(handle_messages(client1_incoming, client1.clone())) - .detach(); - executor - .spawn(handle_messages(server_incoming2, server.clone())) - .detach(); - executor - .spawn(handle_messages(client2_incoming, client2.clone())) - .detach(); - - assert_eq!( - client1 - .request(client1_conn_id, proto::Ping {},) - .await - .unwrap(), - proto::Ack {} - ); - - assert_eq!( - client2 - .request(client2_conn_id, proto::Ping {},) - .await - .unwrap(), - proto::Ack {} - ); - - assert_eq!( - client1 - .request(client1_conn_id, proto::Test { id: 1 },) - .await - .unwrap(), - proto::Test { id: 1 } - ); - - assert_eq!( - client2 - .request(client2_conn_id, proto::Test { id: 2 }) - .await - .unwrap(), - proto::Test { id: 2 } - ); - - client1.disconnect(client1_conn_id); - client2.disconnect(client1_conn_id); - - async fn handle_messages( - mut messages: BoxStream<'static, Box>, - peer: Arc, - ) -> Result<()> { - while let Some(envelope) = messages.next().await { - let envelope = envelope.into_any(); - if let Some(envelope) = envelope.downcast_ref::>() { - let receipt = envelope.receipt(); - peer.respond(receipt, proto::Ack {})? - } else if let Some(envelope) = envelope.downcast_ref::>() - { - peer.respond(envelope.receipt(), envelope.payload.clone())? - } else { - panic!("unknown message type"); - } - } - - Ok(()) - } - } - - #[gpui::test(iterations = 50)] - async fn test_order_of_response_and_incoming(cx: &mut TestAppContext) { - let executor = cx.executor(); - let server = Peer::new(0); - let client = Peer::new(0); - - let (client_to_server_conn, server_to_client_conn, _kill) = - Connection::in_memory(executor.clone()); - let (client_to_server_conn_id, io_task1, mut client_incoming) = - client.add_test_connection(client_to_server_conn, executor.clone()); - - let (server_to_client_conn_id, io_task2, mut server_incoming) = - server.add_test_connection(server_to_client_conn, executor.clone()); - - executor.spawn(io_task1).detach(); - executor.spawn(io_task2).detach(); - - executor - .spawn(async move { - let future = server_incoming.next().await; - let request = future - .unwrap() - .into_any() - .downcast::>() - .unwrap(); - - server - .send( - server_to_client_conn_id, - ErrorCode::Internal - .message("message 1".to_string()) - .to_proto(), - ) - .unwrap(); - server - .send( - server_to_client_conn_id, - ErrorCode::Internal - .message("message 2".to_string()) - .to_proto(), - ) - .unwrap(); - server.respond(request.receipt(), proto::Ack {}).unwrap(); - - // Prevent the connection from being dropped - server_incoming.next().await; - }) - .detach(); - - let events = Arc::new(Mutex::new(Vec::new())); - - let response = client.request(client_to_server_conn_id, proto::Ping {}); - let response_task = executor.spawn({ - let events = events.clone(); - async move { - response.await.unwrap(); - events.lock().push("response".to_string()); - } - }); - - executor - .spawn({ - let events = events.clone(); - async move { - let incoming1 = client_incoming - .next() - .await - .unwrap() - .into_any() - .downcast::>() - .unwrap(); - events.lock().push(incoming1.payload.message); - let incoming2 = client_incoming - .next() - .await - .unwrap() - .into_any() - .downcast::>() - .unwrap(); - events.lock().push(incoming2.payload.message); - - // Prevent the connection from being dropped - client_incoming.next().await; - } - }) - .detach(); - - response_task.await; - assert_eq!( - &*events.lock(), - &[ - "message 1".to_string(), - "message 2".to_string(), - "response".to_string() - ] - ); - } - - #[gpui::test(iterations = 50)] - async fn test_dropping_request_before_completion(cx: &mut TestAppContext) { - let executor = cx.executor(); - let server = Peer::new(0); - let client = Peer::new(0); - - let (client_to_server_conn, server_to_client_conn, _kill) = - Connection::in_memory(cx.executor()); - let (client_to_server_conn_id, io_task1, mut client_incoming) = - client.add_test_connection(client_to_server_conn, cx.executor()); - let (server_to_client_conn_id, io_task2, mut server_incoming) = - server.add_test_connection(server_to_client_conn, cx.executor()); - - executor.spawn(io_task1).detach(); - executor.spawn(io_task2).detach(); - - executor - .spawn(async move { - let request1 = server_incoming - .next() - .await - .unwrap() - .into_any() - .downcast::>() - .unwrap(); - let request2 = server_incoming - .next() - .await - .unwrap() - .into_any() - .downcast::>() - .unwrap(); - - server - .send( - server_to_client_conn_id, - ErrorCode::Internal - .message("message 1".to_string()) - .to_proto(), - ) - .unwrap(); - server - .send( - server_to_client_conn_id, - ErrorCode::Internal - .message("message 2".to_string()) - .to_proto(), - ) - .unwrap(); - server.respond(request1.receipt(), proto::Ack {}).unwrap(); - server.respond(request2.receipt(), proto::Ack {}).unwrap(); - - // Prevent the connection from being dropped - server_incoming.next().await; - }) - .detach(); - - let events = Arc::new(Mutex::new(Vec::new())); - - let request1 = client.request(client_to_server_conn_id, proto::Ping {}); - let request1_task = executor.spawn(request1); - let request2 = client.request(client_to_server_conn_id, proto::Ping {}); - let request2_task = executor.spawn({ - let events = events.clone(); - async move { - request2.await.unwrap(); - events.lock().push("response 2".to_string()); - } - }); - - executor - .spawn({ - let events = events.clone(); - async move { - let incoming1 = client_incoming - .next() - .await - .unwrap() - .into_any() - .downcast::>() - .unwrap(); - events.lock().push(incoming1.payload.message); - let incoming2 = client_incoming - .next() - .await - .unwrap() - .into_any() - .downcast::>() - .unwrap(); - events.lock().push(incoming2.payload.message); - - // Prevent the connection from being dropped - client_incoming.next().await; - } - }) - .detach(); - - // Allow the request to make some progress before dropping it. - cx.executor().simulate_random_delay().await; - drop(request1_task); - - request2_task.await; - assert_eq!( - &*events.lock(), - &[ - "message 1".to_string(), - "message 2".to_string(), - "response 2".to_string() - ] - ); - } - - #[gpui::test(iterations = 50)] - async fn test_disconnect(cx: &mut TestAppContext) { - let executor = cx.executor(); - - let (client_conn, mut server_conn, _kill) = Connection::in_memory(executor.clone()); - - let client = Peer::new(0); - let (connection_id, io_handler, mut incoming) = - client.add_test_connection(client_conn, executor.clone()); - - let (io_ended_tx, io_ended_rx) = oneshot::channel(); - executor - .spawn(async move { - io_handler.await.ok(); - io_ended_tx.send(()).unwrap(); - }) - .detach(); - - let (messages_ended_tx, messages_ended_rx) = oneshot::channel(); - executor - .spawn(async move { - incoming.next().await; - messages_ended_tx.send(()).unwrap(); - }) - .detach(); - - client.disconnect(connection_id); - - let _ = io_ended_rx.await; - let _ = messages_ended_rx.await; - assert!( - server_conn - .send(WebSocketMessage::Binary(vec![].into())) - .await - .is_err() - ); - } - - #[gpui::test(iterations = 50)] - async fn test_io_error(cx: &mut TestAppContext) { - let executor = cx.executor(); - let (client_conn, mut server_conn, _kill) = Connection::in_memory(executor.clone()); - - let client = Peer::new(0); - let (connection_id, io_handler, mut incoming) = - client.add_test_connection(client_conn, executor.clone()); - executor.spawn(io_handler).detach(); - executor - .spawn(async move { incoming.next().await }) - .detach(); - - let response = executor.spawn(client.request(connection_id, proto::Ping {})); - let _request = server_conn.rx.next().await.unwrap().unwrap(); - - drop(server_conn); - assert_eq!( - response.await.unwrap_err().to_string(), - "connection was closed" - ); - } -} diff --git a/crates/rpc/src/proto_client.rs b/crates/rpc/src/proto_client.rs deleted file mode 100644 index 3850ff5820..0000000000 --- a/crates/rpc/src/proto_client.rs +++ /dev/null @@ -1,531 +0,0 @@ -use anyhow::{Context, Result}; -use collections::HashMap; -use futures::{ - Future, FutureExt as _, - channel::oneshot, - future::{BoxFuture, LocalBoxFuture}, -}; -use gpui::{AnyEntity, AnyWeakEntity, AsyncApp, BackgroundExecutor, Entity, FutureExt as _}; -use parking_lot::Mutex; -use proto::{ - AnyTypedEnvelope, EntityMessage, Envelope, EnvelopedMessage, LspRequestId, LspRequestMessage, - RequestMessage, TypedEnvelope, error::ErrorExt as _, -}; -use std::{ - any::{Any, TypeId}, - sync::{ - Arc, OnceLock, - atomic::{self, AtomicU64}, - }, - time::Duration, -}; - -#[derive(Clone)] -pub struct AnyProtoClient(Arc); - -type RequestIds = Arc< - Mutex< - HashMap< - LspRequestId, - oneshot::Sender< - Result< - Option>>>>, - >, - >, - >, - >, ->; - -static NEXT_LSP_REQUEST_ID: OnceLock> = OnceLock::new(); -static REQUEST_IDS: OnceLock = OnceLock::new(); - -struct State { - client: Arc, - next_lsp_request_id: Arc, - request_ids: RequestIds, -} - -pub trait ProtoClient: Send + Sync { - fn request( - &self, - envelope: Envelope, - request_type: &'static str, - ) -> BoxFuture<'static, Result>; - - fn send(&self, envelope: Envelope, message_type: &'static str) -> Result<()>; - - fn send_response(&self, envelope: Envelope, message_type: &'static str) -> Result<()>; - - fn message_handler_set(&self) -> &parking_lot::Mutex; - - fn is_via_collab(&self) -> bool; - fn has_wsl_interop(&self) -> bool; -} - -#[derive(Default)] -pub struct ProtoMessageHandlerSet { - pub entity_types_by_message_type: HashMap, - pub entities_by_type_and_remote_id: HashMap<(TypeId, u64), EntityMessageSubscriber>, - pub entity_id_extractors: HashMap u64>, - pub entities_by_message_type: HashMap, - pub message_handlers: HashMap, -} - -pub type ProtoMessageHandler = Arc< - dyn Send - + Sync - + Fn( - AnyEntity, - Box, - AnyProtoClient, - AsyncApp, - ) -> LocalBoxFuture<'static, Result<()>>, ->; - -impl ProtoMessageHandlerSet { - pub fn clear(&mut self) { - self.message_handlers.clear(); - self.entities_by_message_type.clear(); - self.entities_by_type_and_remote_id.clear(); - self.entity_id_extractors.clear(); - } - - fn add_message_handler( - &mut self, - message_type_id: TypeId, - entity: gpui::AnyWeakEntity, - handler: ProtoMessageHandler, - ) { - self.entities_by_message_type - .insert(message_type_id, entity); - let prev_handler = self.message_handlers.insert(message_type_id, handler); - if prev_handler.is_some() { - panic!("registered handler for the same message twice"); - } - } - - fn add_entity_message_handler( - &mut self, - message_type_id: TypeId, - entity_type_id: TypeId, - entity_id_extractor: fn(&dyn AnyTypedEnvelope) -> u64, - handler: ProtoMessageHandler, - ) { - self.entity_id_extractors - .entry(message_type_id) - .or_insert(entity_id_extractor); - self.entity_types_by_message_type - .insert(message_type_id, entity_type_id); - let prev_handler = self.message_handlers.insert(message_type_id, handler); - if prev_handler.is_some() { - panic!("registered handler for the same message twice"); - } - } - - pub fn handle_message( - this: &parking_lot::Mutex, - message: Box, - client: AnyProtoClient, - cx: AsyncApp, - ) -> Option>> { - let payload_type_id = message.payload_type_id(); - let mut this = this.lock(); - let handler = this.message_handlers.get(&payload_type_id)?.clone(); - let entity = if let Some(entity) = this.entities_by_message_type.get(&payload_type_id) { - entity.upgrade()? - } else { - let extract_entity_id = *this.entity_id_extractors.get(&payload_type_id)?; - let entity_type_id = *this.entity_types_by_message_type.get(&payload_type_id)?; - let entity_id = (extract_entity_id)(message.as_ref()); - match this - .entities_by_type_and_remote_id - .get_mut(&(entity_type_id, entity_id))? - { - EntityMessageSubscriber::Pending(pending) => { - pending.push(message); - return None; - } - EntityMessageSubscriber::Entity { handle } => handle.upgrade()?, - } - }; - drop(this); - Some(handler(entity, message, client, cx)) - } -} - -pub enum EntityMessageSubscriber { - Entity { handle: AnyWeakEntity }, - Pending(Vec>), -} - -impl std::fmt::Debug for EntityMessageSubscriber { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - EntityMessageSubscriber::Entity { handle } => f - .debug_struct("EntityMessageSubscriber::Entity") - .field("handle", handle) - .finish(), - EntityMessageSubscriber::Pending(vec) => f - .debug_struct("EntityMessageSubscriber::Pending") - .field( - "envelopes", - &vec.iter() - .map(|envelope| envelope.payload_type_name()) - .collect::>(), - ) - .finish(), - } - } -} - -impl From> for AnyProtoClient -where - T: ProtoClient + 'static, -{ - fn from(client: Arc) -> Self { - Self::new(client) - } -} - -impl AnyProtoClient { - pub fn new(client: Arc) -> Self { - Self(Arc::new(State { - client, - next_lsp_request_id: NEXT_LSP_REQUEST_ID - .get_or_init(|| Arc::new(AtomicU64::new(0))) - .clone(), - request_ids: REQUEST_IDS.get_or_init(RequestIds::default).clone(), - })) - } - - pub fn is_via_collab(&self) -> bool { - self.0.client.is_via_collab() - } - - pub fn request( - &self, - request: T, - ) -> impl Future> + use { - let envelope = request.into_envelope(0, None, None); - let response = self.0.client.request(envelope, T::NAME); - async move { - T::Response::from_envelope(response.await?) - .context("received response of the wrong type") - } - } - - pub fn send(&self, request: T) -> Result<()> { - let envelope = request.into_envelope(0, None, None); - self.0.client.send(envelope, T::NAME) - } - - pub fn send_response(&self, request_id: u32, request: T) -> Result<()> { - let envelope = request.into_envelope(0, Some(request_id), None); - self.0.client.send(envelope, T::NAME) - } - - pub fn request_lsp( - &self, - project_id: u64, - server_id: Option, - timeout: Duration, - executor: BackgroundExecutor, - request: T, - ) -> impl Future< - Output = Result>>>>, - > + use - where - T: LspRequestMessage, - { - let new_id = LspRequestId( - self.0 - .next_lsp_request_id - .fetch_add(1, atomic::Ordering::Acquire), - ); - let (tx, rx) = oneshot::channel(); - { - self.0.request_ids.lock().insert(new_id, tx); - } - - let query = proto::LspQuery { - project_id, - server_id, - lsp_request_id: new_id.0, - request: Some(request.to_proto_query()), - }; - let request = self.request(query); - let request_ids = self.0.request_ids.clone(); - async move { - match request.await { - Ok(_request_enqueued) => {} - Err(e) => { - request_ids.lock().remove(&new_id); - return Err(e).context("sending LSP proto request"); - } - } - - let response = rx.with_timeout(timeout, &executor).await; - { - request_ids.lock().remove(&new_id); - } - match response { - Ok(Ok(response)) => { - let response = response - .context("waiting for LSP proto response")? - .map(|response| { - anyhow::Ok(TypedEnvelope { - payload: response - .payload - .into_iter() - .map(|lsp_response| lsp_response.into_response::()) - .collect::>>()?, - sender_id: response.sender_id, - original_sender_id: response.original_sender_id, - message_id: response.message_id, - received_at: response.received_at, - }) - }) - .transpose() - .context("converting LSP proto response")?; - Ok(response) - } - Err(_cancelled_due_timeout) => Ok(None), - Ok(Err(_channel_dropped)) => Ok(None), - } - } - } - - pub fn send_lsp_response( - &self, - project_id: u64, - lsp_request_id: LspRequestId, - server_responses: HashMap, - ) -> Result<()> { - self.send(proto::LspQueryResponse { - project_id, - lsp_request_id: lsp_request_id.0, - responses: server_responses - .into_iter() - .map(|(server_id, response)| proto::LspResponse { - server_id, - response: Some(T::response_to_proto_query(response)), - }) - .collect(), - }) - } - - pub fn handle_lsp_response(&self, mut envelope: TypedEnvelope) { - let request_id = LspRequestId(envelope.payload.lsp_request_id); - let mut response_senders = self.0.request_ids.lock(); - if let Some(tx) = response_senders.remove(&request_id) { - let responses = envelope.payload.responses.drain(..).collect::>(); - tx.send(Ok(Some(proto::TypedEnvelope { - sender_id: envelope.sender_id, - original_sender_id: envelope.original_sender_id, - message_id: envelope.message_id, - received_at: envelope.received_at, - payload: responses - .into_iter() - .filter_map(|response| { - use proto::lsp_response::Response; - - let server_id = response.server_id; - let response = match response.response? { - Response::GetReferencesResponse(response) => { - to_any_envelope(&envelope, response) - } - Response::GetDocumentColorResponse(response) => { - to_any_envelope(&envelope, response) - } - Response::GetHoverResponse(response) => { - to_any_envelope(&envelope, response) - } - Response::GetCodeActionsResponse(response) => { - to_any_envelope(&envelope, response) - } - Response::GetSignatureHelpResponse(response) => { - to_any_envelope(&envelope, response) - } - Response::GetCodeLensResponse(response) => { - to_any_envelope(&envelope, response) - } - Response::GetDocumentDiagnosticsResponse(response) => { - to_any_envelope(&envelope, response) - } - Response::GetDefinitionResponse(response) => { - to_any_envelope(&envelope, response) - } - Response::GetDeclarationResponse(response) => { - to_any_envelope(&envelope, response) - } - Response::GetTypeDefinitionResponse(response) => { - to_any_envelope(&envelope, response) - } - Response::GetImplementationResponse(response) => { - to_any_envelope(&envelope, response) - } - Response::InlayHintsResponse(response) => { - to_any_envelope(&envelope, response) - } - }; - Some(proto::ProtoLspResponse { - server_id, - response, - }) - }) - .collect(), - }))) - .ok(); - } - } - - pub fn add_request_handler(&self, entity: gpui::WeakEntity, handler: H) - where - M: RequestMessage, - E: 'static, - H: 'static + Sync + Fn(Entity, TypedEnvelope, AsyncApp) -> F + Send + Sync, - F: 'static + Future>, - { - self.0 - .client - .message_handler_set() - .lock() - .add_message_handler( - TypeId::of::(), - entity.into(), - Arc::new(move |entity, envelope, client, cx| { - let entity = entity.downcast::().unwrap(); - let envelope = envelope.into_any().downcast::>().unwrap(); - let request_id = envelope.message_id(); - handler(entity, *envelope, cx) - .then(move |result| async move { - match result { - Ok(response) => { - client.send_response(request_id, response)?; - Ok(()) - } - Err(error) => { - client.send_response(request_id, error.to_proto())?; - Err(error) - } - } - }) - .boxed_local() - }), - ) - } - - pub fn add_entity_request_handler(&self, handler: H) - where - M: EnvelopedMessage + RequestMessage + EntityMessage, - E: 'static, - H: 'static + Sync + Send + Fn(gpui::Entity, TypedEnvelope, AsyncApp) -> F, - F: 'static + Future>, - { - let message_type_id = TypeId::of::(); - let entity_type_id = TypeId::of::(); - let entity_id_extractor = |envelope: &dyn AnyTypedEnvelope| { - (envelope as &dyn Any) - .downcast_ref::>() - .unwrap() - .payload - .remote_entity_id() - }; - self.0 - .client - .message_handler_set() - .lock() - .add_entity_message_handler( - message_type_id, - entity_type_id, - entity_id_extractor, - Arc::new(move |entity, envelope, client, cx| { - let entity = entity.downcast::().unwrap(); - let envelope = envelope.into_any().downcast::>().unwrap(); - let request_id = envelope.message_id(); - handler(entity, *envelope, cx) - .then(move |result| async move { - match result { - Ok(response) => { - client.send_response(request_id, response)?; - Ok(()) - } - Err(error) => { - client.send_response(request_id, error.to_proto())?; - Err(error) - } - } - }) - .boxed_local() - }), - ); - } - - pub fn add_entity_message_handler(&self, handler: H) - where - M: EnvelopedMessage + EntityMessage, - E: 'static, - H: 'static + Sync + Send + Fn(gpui::Entity, TypedEnvelope, AsyncApp) -> F, - F: 'static + Future>, - { - let message_type_id = TypeId::of::(); - let entity_type_id = TypeId::of::(); - let entity_id_extractor = |envelope: &dyn AnyTypedEnvelope| { - (envelope as &dyn Any) - .downcast_ref::>() - .unwrap() - .payload - .remote_entity_id() - }; - self.0 - .client - .message_handler_set() - .lock() - .add_entity_message_handler( - message_type_id, - entity_type_id, - entity_id_extractor, - Arc::new(move |entity, envelope, _, cx| { - let entity = entity.downcast::().unwrap(); - let envelope = envelope.into_any().downcast::>().unwrap(); - handler(entity, *envelope, cx).boxed_local() - }), - ); - } - - pub fn subscribe_to_entity(&self, remote_id: u64, entity: &Entity) { - let id = (TypeId::of::(), remote_id); - - let mut message_handlers = self.0.client.message_handler_set().lock(); - if message_handlers - .entities_by_type_and_remote_id - .contains_key(&id) - { - panic!("already subscribed to entity"); - } - - message_handlers.entities_by_type_and_remote_id.insert( - id, - EntityMessageSubscriber::Entity { - handle: entity.downgrade().into(), - }, - ); - } - - pub fn has_wsl_interop(&self) -> bool { - self.0.client.has_wsl_interop() - } -} - -fn to_any_envelope( - envelope: &TypedEnvelope, - response: T, -) -> Box { - Box::new(proto::TypedEnvelope { - sender_id: envelope.sender_id, - original_sender_id: envelope.original_sender_id, - message_id: envelope.message_id, - received_at: envelope.received_at, - payload: response, - }) as Box<_> -} diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs deleted file mode 100644 index ad1ebb757c..0000000000 --- a/crates/rpc/src/rpc.rs +++ /dev/null @@ -1,21 +0,0 @@ -pub mod auth; -mod conn; -mod extension; -mod message_stream; -mod notification; -mod peer; - -pub use conn::Connection; -pub use extension::*; -pub use notification::*; -pub use peer::*; -pub use proto; -pub use proto::{Receipt, TypedEnvelope, error::*}; -mod macros; - -#[cfg(feature = "gpui")] -mod proto_client; -#[cfg(feature = "gpui")] -pub use proto_client::*; - -pub const PROTOCOL_VERSION: u32 = 68; diff --git a/crates/rules_library/Cargo.toml b/crates/rules_library/Cargo.toml deleted file mode 100644 index d2fdd765e0..0000000000 --- a/crates/rules_library/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "rules_library" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/rules_library.rs" - -[dependencies] -anyhow.workspace = true -collections.workspace = true -editor.workspace = true -gpui.workspace = true -language.workspace = true -language_model.workspace = true -log.workspace = true -menu.workspace = true -picker.workspace = true -prompt_store.workspace = true -release_channel.workspace = true -rope.workspace = true -serde.workspace = true -settings.workspace = true -theme.workspace = true -title_bar.workspace = true -ui.workspace = true -util.workspace = true -workspace.workspace = true -zed_actions.workspace = true diff --git a/crates/rules_library/LICENSE-GPL b/crates/rules_library/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/rules_library/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs deleted file mode 100644 index 09b7e0b539..0000000000 --- a/crates/rules_library/src/rules_library.rs +++ /dev/null @@ -1,1431 +0,0 @@ -use anyhow::Result; -use collections::{HashMap, HashSet}; -use editor::{CompletionProvider, SelectionEffects}; -use editor::{CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle, actions::Tab}; -use gpui::{ - Action, App, Bounds, DEFAULT_ADDITIONAL_WINDOW_SIZE, Entity, EventEmitter, Focusable, - PromptLevel, Subscription, Task, TextStyle, TitlebarOptions, WindowBounds, WindowHandle, - WindowOptions, actions, point, size, transparent_black, -}; -use language::{Buffer, LanguageRegistry, language_settings::SoftWrap}; -use language_model::{ - ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role, -}; -use picker::{Picker, PickerDelegate}; -use release_channel::ReleaseChannel; -use rope::Rope; -use settings::Settings; -use std::rc::Rc; -use std::sync::Arc; -use std::sync::atomic::AtomicBool; -use std::time::Duration; -use theme::ThemeSettings; -use title_bar::platform_title_bar::PlatformTitleBar; -use ui::{ - Divider, KeyBinding, ListItem, ListItemSpacing, ListSubHeader, Render, Tooltip, prelude::*, -}; -use util::{ResultExt, TryFutureExt}; -use workspace::{Workspace, WorkspaceSettings, client_side_decorations}; -use zed_actions::assistant::InlineAssist; - -use prompt_store::*; - -pub fn init(cx: &mut App) { - prompt_store::init(cx); -} - -actions!( - rules_library, - [ - /// Creates a new rule in the rules library. - NewRule, - /// Deletes the selected rule. - DeleteRule, - /// Duplicates the selected rule. - DuplicateRule, - /// Toggles whether the selected rule is a default rule. - ToggleDefaultRule - ] -); - -const BUILT_IN_TOOLTIP_TEXT: &str = concat!( - "This rule supports special functionality.\n", - "It's read-only, but you can remove it from your default rules." -); - -pub trait InlineAssistDelegate { - fn assist( - &self, - prompt_editor: &Entity, - initial_prompt: Option, - window: &mut Window, - cx: &mut Context, - ); - - /// Returns whether the Agent panel was focused. - fn focus_agent_panel( - &self, - workspace: &mut Workspace, - window: &mut Window, - cx: &mut Context, - ) -> bool; -} - -/// This function opens a new rules library window if one doesn't exist already. -/// If one exists, it brings it to the foreground. -/// -/// Note that, when opening a new window, this waits for the PromptStore to be -/// initialized. If it was initialized successfully, it returns a window handle -/// to a rules library. -pub fn open_rules_library( - language_registry: Arc, - inline_assist_delegate: Box, - make_completion_provider: Rc Rc>, - prompt_to_select: Option, - cx: &mut App, -) -> Task>> { - let store = PromptStore::global(cx); - cx.spawn(async move |cx| { - // We query windows in spawn so that all windows have been returned to GPUI - let existing_window = cx - .update(|cx| { - let existing_window = cx - .windows() - .into_iter() - .find_map(|window| window.downcast::()); - if let Some(existing_window) = existing_window { - existing_window - .update(cx, |rules_library, window, cx| { - if let Some(prompt_to_select) = prompt_to_select { - rules_library.load_rule(prompt_to_select, true, window, cx); - } - window.activate_window() - }) - .ok(); - - Some(existing_window) - } else { - None - } - }) - .ok() - .flatten(); - - if let Some(existing_window) = existing_window { - return Ok(existing_window); - } - - let store = store.await?; - cx.update(|cx| { - let app_id = ReleaseChannel::global(cx).app_id(); - let bounds = Bounds::centered(None, size(px(1024.0), px(768.0)), cx); - let window_decorations = match std::env::var("ZED_WINDOW_DECORATIONS") { - Ok(val) if val == "server" => gpui::WindowDecorations::Server, - Ok(val) if val == "client" => gpui::WindowDecorations::Client, - _ => match WorkspaceSettings::get_global(cx).window_decorations { - settings::WindowDecorations::Server => gpui::WindowDecorations::Server, - settings::WindowDecorations::Client => gpui::WindowDecorations::Client, - }, - }; - cx.open_window( - WindowOptions { - titlebar: Some(TitlebarOptions { - title: Some("Rules Library".into()), - appears_transparent: true, - traffic_light_position: Some(point(px(12.0), px(12.0))), - }), - app_id: Some(app_id.to_owned()), - window_bounds: Some(WindowBounds::Windowed(bounds)), - window_background: cx.theme().window_background_appearance(), - window_decorations: Some(window_decorations), - window_min_size: Some(DEFAULT_ADDITIONAL_WINDOW_SIZE), - kind: gpui::WindowKind::Floating, - ..Default::default() - }, - |window, cx| { - cx.new(|cx| { - RulesLibrary::new( - store, - language_registry, - inline_assist_delegate, - make_completion_provider, - prompt_to_select, - window, - cx, - ) - }) - }, - ) - })? - }) -} - -pub struct RulesLibrary { - title_bar: Option>, - store: Entity, - language_registry: Arc, - rule_editors: HashMap, - active_rule_id: Option, - picker: Entity>, - pending_load: Task<()>, - inline_assist_delegate: Box, - make_completion_provider: Rc Rc>, - _subscriptions: Vec, -} - -struct RuleEditor { - title_editor: Entity, - body_editor: Entity, - token_count: Option, - pending_token_count: Task>, - next_title_and_body_to_save: Option<(String, Rope)>, - pending_save: Option>>, - _subscriptions: Vec, -} - -enum RulePickerEntry { - Header(SharedString), - Rule(PromptMetadata), - Separator, -} - -struct RulePickerDelegate { - store: Entity, - selected_index: usize, - filtered_entries: Vec, -} - -enum RulePickerEvent { - Selected { prompt_id: PromptId }, - Confirmed { prompt_id: PromptId }, - Deleted { prompt_id: PromptId }, - ToggledDefault { prompt_id: PromptId }, -} - -impl EventEmitter for Picker {} - -impl PickerDelegate for RulePickerDelegate { - type ListItem = AnyElement; - - fn match_count(&self) -> usize { - self.filtered_entries.len() - } - - fn no_matches_text(&self, _window: &mut Window, cx: &mut App) -> Option { - let text = if self.store.read(cx).prompt_count() == 0 { - "No rules.".into() - } else { - "No rules found matching your search.".into() - }; - Some(text) - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context>) { - self.selected_index = ix.min(self.filtered_entries.len().saturating_sub(1)); - - if let Some(RulePickerEntry::Rule(rule)) = self.filtered_entries.get(self.selected_index) { - cx.emit(RulePickerEvent::Selected { prompt_id: rule.id }); - } - - cx.notify(); - } - - fn can_select(&mut self, ix: usize, _: &mut Window, _: &mut Context>) -> bool { - match self.filtered_entries.get(ix) { - Some(RulePickerEntry::Rule(_)) => true, - Some(RulePickerEntry::Header(_)) | Some(RulePickerEntry::Separator) | None => false, - } - } - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - "Search…".into() - } - - fn update_matches( - &mut self, - query: String, - window: &mut Window, - cx: &mut Context>, - ) -> Task<()> { - let cancellation_flag = Arc::new(AtomicBool::default()); - let search = self.store.read(cx).search(query, cancellation_flag, cx); - - let prev_prompt_id = self - .filtered_entries - .get(self.selected_index) - .and_then(|entry| { - if let RulePickerEntry::Rule(rule) = entry { - Some(rule.id) - } else { - None - } - }); - - cx.spawn_in(window, async move |this, cx| { - let (filtered_entries, selected_index) = cx - .background_spawn(async move { - let matches = search.await; - - let (default_rules, non_default_rules): (Vec<_>, Vec<_>) = - matches.iter().partition(|rule| rule.default); - - let mut filtered_entries = Vec::new(); - - if !default_rules.is_empty() { - filtered_entries.push(RulePickerEntry::Header("Default Rules".into())); - - for rule in default_rules { - filtered_entries.push(RulePickerEntry::Rule(rule.clone())); - } - - filtered_entries.push(RulePickerEntry::Separator); - } - - for rule in non_default_rules { - filtered_entries.push(RulePickerEntry::Rule(rule.clone())); - } - - let selected_index = prev_prompt_id - .and_then(|prev_prompt_id| { - filtered_entries.iter().position(|entry| { - if let RulePickerEntry::Rule(rule) = entry { - rule.id == prev_prompt_id - } else { - false - } - }) - }) - .unwrap_or_else(|| { - filtered_entries - .iter() - .position(|entry| matches!(entry, RulePickerEntry::Rule(_))) - .unwrap_or(0) - }); - - (filtered_entries, selected_index) - }) - .await; - - this.update_in(cx, |this, window, cx| { - this.delegate.filtered_entries = filtered_entries; - this.set_selected_index( - selected_index, - Some(picker::Direction::Down), - true, - window, - cx, - ); - cx.notify(); - }) - .ok(); - }) - } - - fn confirm(&mut self, _secondary: bool, _: &mut Window, cx: &mut Context>) { - if let Some(RulePickerEntry::Rule(rule)) = self.filtered_entries.get(self.selected_index) { - cx.emit(RulePickerEvent::Confirmed { prompt_id: rule.id }); - } - } - - fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context>) {} - - fn render_match( - &self, - ix: usize, - selected: bool, - _: &mut Window, - cx: &mut Context>, - ) -> Option { - match self.filtered_entries.get(ix)? { - RulePickerEntry::Header(title) => Some( - ListSubHeader::new(title.clone()) - .end_slot( - IconButton::new("info", IconName::Info) - .style(ButtonStyle::Transparent) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .tooltip(Tooltip::text( - "Default Rules are attached by default with every new thread.", - )) - .into_any_element(), - ) - .inset(true) - .into_any_element(), - ), - RulePickerEntry::Separator => Some( - h_flex() - .py_1() - .child(Divider::horizontal()) - .into_any_element(), - ), - RulePickerEntry::Rule(rule) => { - let default = rule.default; - let prompt_id = rule.id; - - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .child( - Label::new(rule.title.clone().unwrap_or("Untitled".into())) - .truncate() - .mr_10(), - ) - .end_slot::(default.then(|| { - IconButton::new("toggle-default-rule", IconName::Paperclip) - .toggle_state(true) - .icon_color(Color::Accent) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Remove from Default Rules")) - .on_click(cx.listener(move |_, _, _, cx| { - cx.emit(RulePickerEvent::ToggledDefault { prompt_id }) - })) - })) - .end_hover_slot( - h_flex() - .child(if prompt_id.is_built_in() { - div() - .id("built-in-rule") - .child(Icon::new(IconName::FileLock).color(Color::Muted)) - .tooltip(move |_window, cx| { - Tooltip::with_meta( - "Built-in rule", - None, - BUILT_IN_TOOLTIP_TEXT, - cx, - ) - }) - .into_any() - } else { - IconButton::new("delete-rule", IconName::Trash) - .icon_color(Color::Muted) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Delete Rule")) - .on_click(cx.listener(move |_, _, _, cx| { - cx.emit(RulePickerEvent::Deleted { prompt_id }) - })) - .into_any_element() - }) - .child( - IconButton::new("toggle-default-rule", IconName::Plus) - .selected_icon(IconName::Dash) - .toggle_state(default) - .icon_size(IconSize::Small) - .icon_color(if default { - Color::Accent - } else { - Color::Muted - }) - .map(|this| { - if default { - this.tooltip(Tooltip::text( - "Remove from Default Rules", - )) - } else { - this.tooltip(move |_window, cx| { - Tooltip::with_meta( - "Add to Default Rules", - None, - "Always included in every thread.", - cx, - ) - }) - } - }) - .on_click(cx.listener(move |_, _, _, cx| { - cx.emit(RulePickerEvent::ToggledDefault { prompt_id }) - })), - ), - ) - .into_any_element(), - ) - } - } - } - - fn render_editor( - &self, - editor: &Entity, - _: &mut Window, - cx: &mut Context>, - ) -> Div { - h_flex() - .py_1() - .px_1p5() - .mx_1() - .gap_1p5() - .rounded_sm() - .bg(cx.theme().colors().editor_background) - .border_1() - .border_color(cx.theme().colors().border) - .child(Icon::new(IconName::MagnifyingGlass).color(Color::Muted)) - .child(editor.clone()) - } -} - -impl RulesLibrary { - fn new( - store: Entity, - language_registry: Arc, - inline_assist_delegate: Box, - make_completion_provider: Rc Rc>, - rule_to_select: Option, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let (_selected_index, _matches) = if let Some(rule_to_select) = rule_to_select { - let matches = store.read(cx).all_prompt_metadata(); - let selected_index = matches - .iter() - .enumerate() - .find(|(_, metadata)| metadata.id == rule_to_select) - .map_or(0, |(ix, _)| ix); - (selected_index, matches) - } else { - (0, vec![]) - }; - - let picker_delegate = RulePickerDelegate { - store: store.clone(), - selected_index: 0, - filtered_entries: Vec::new(), - }; - - let picker = cx.new(|cx| { - let picker = Picker::list(picker_delegate, window, cx) - .modal(false) - .max_height(None); - picker.focus(window, cx); - picker - }); - - Self { - title_bar: if !cfg!(target_os = "macos") { - Some(cx.new(|cx| PlatformTitleBar::new("rules-library-title-bar", cx))) - } else { - None - }, - store, - language_registry, - rule_editors: HashMap::default(), - active_rule_id: None, - pending_load: Task::ready(()), - inline_assist_delegate, - make_completion_provider, - _subscriptions: vec![cx.subscribe_in(&picker, window, Self::handle_picker_event)], - picker, - } - } - - fn handle_picker_event( - &mut self, - _: &Entity>, - event: &RulePickerEvent, - window: &mut Window, - cx: &mut Context, - ) { - match event { - RulePickerEvent::Selected { prompt_id } => { - self.load_rule(*prompt_id, false, window, cx); - } - RulePickerEvent::Confirmed { prompt_id } => { - self.load_rule(*prompt_id, true, window, cx); - } - RulePickerEvent::ToggledDefault { prompt_id } => { - self.toggle_default_for_rule(*prompt_id, window, cx); - } - RulePickerEvent::Deleted { prompt_id } => { - self.delete_rule(*prompt_id, window, cx); - } - } - } - - pub fn new_rule(&mut self, window: &mut Window, cx: &mut Context) { - // If we already have an untitled rule, use that instead - // of creating a new one. - if let Some(metadata) = self.store.read(cx).first() - && metadata.title.is_none() - { - self.load_rule(metadata.id, true, window, cx); - return; - } - - let prompt_id = PromptId::new(); - let save = self.store.update(cx, |store, cx| { - store.save(prompt_id, None, false, "".into(), cx) - }); - self.picker - .update(cx, |picker, cx| picker.refresh(window, cx)); - cx.spawn_in(window, async move |this, cx| { - save.await?; - this.update_in(cx, |this, window, cx| { - this.load_rule(prompt_id, true, window, cx) - }) - }) - .detach_and_log_err(cx); - } - - pub fn save_rule(&mut self, prompt_id: PromptId, window: &mut Window, cx: &mut Context) { - const SAVE_THROTTLE: Duration = Duration::from_millis(500); - - if prompt_id.is_built_in() { - return; - } - - let rule_metadata = self.store.read(cx).metadata(prompt_id).unwrap(); - let rule_editor = self.rule_editors.get_mut(&prompt_id).unwrap(); - let title = rule_editor.title_editor.read(cx).text(cx); - let body = rule_editor.body_editor.update(cx, |editor, cx| { - editor - .buffer() - .read(cx) - .as_singleton() - .unwrap() - .read(cx) - .as_rope() - .clone() - }); - - let store = self.store.clone(); - let executor = cx.background_executor().clone(); - - rule_editor.next_title_and_body_to_save = Some((title, body)); - if rule_editor.pending_save.is_none() { - rule_editor.pending_save = Some(cx.spawn_in(window, async move |this, cx| { - async move { - loop { - let title_and_body = this.update(cx, |this, _| { - this.rule_editors - .get_mut(&prompt_id)? - .next_title_and_body_to_save - .take() - })?; - - if let Some((title, body)) = title_and_body { - let title = if title.trim().is_empty() { - None - } else { - Some(SharedString::from(title)) - }; - cx.update(|_window, cx| { - store.update(cx, |store, cx| { - store.save(prompt_id, title, rule_metadata.default, body, cx) - }) - })? - .await - .log_err(); - this.update_in(cx, |this, window, cx| { - this.picker - .update(cx, |picker, cx| picker.refresh(window, cx)); - cx.notify(); - })?; - - executor.timer(SAVE_THROTTLE).await; - } else { - break; - } - } - - this.update(cx, |this, _cx| { - if let Some(rule_editor) = this.rule_editors.get_mut(&prompt_id) { - rule_editor.pending_save = None; - } - }) - } - .log_err() - .await - })); - } - } - - pub fn delete_active_rule(&mut self, window: &mut Window, cx: &mut Context) { - if let Some(active_rule_id) = self.active_rule_id { - self.delete_rule(active_rule_id, window, cx); - } - } - - pub fn duplicate_active_rule(&mut self, window: &mut Window, cx: &mut Context) { - if let Some(active_rule_id) = self.active_rule_id { - self.duplicate_rule(active_rule_id, window, cx); - } - } - - pub fn toggle_default_for_active_rule(&mut self, window: &mut Window, cx: &mut Context) { - if let Some(active_rule_id) = self.active_rule_id { - self.toggle_default_for_rule(active_rule_id, window, cx); - } - } - - pub fn toggle_default_for_rule( - &mut self, - prompt_id: PromptId, - window: &mut Window, - cx: &mut Context, - ) { - self.store.update(cx, move |store, cx| { - if let Some(rule_metadata) = store.metadata(prompt_id) { - store - .save_metadata(prompt_id, rule_metadata.title, !rule_metadata.default, cx) - .detach_and_log_err(cx); - } - }); - self.picker - .update(cx, |picker, cx| picker.refresh(window, cx)); - cx.notify(); - } - - pub fn load_rule( - &mut self, - prompt_id: PromptId, - focus: bool, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(rule_editor) = self.rule_editors.get(&prompt_id) { - if focus { - rule_editor - .body_editor - .update(cx, |editor, cx| window.focus(&editor.focus_handle(cx))); - } - self.set_active_rule(Some(prompt_id), window, cx); - } else if let Some(rule_metadata) = self.store.read(cx).metadata(prompt_id) { - let language_registry = self.language_registry.clone(); - let rule = self.store.read(cx).load(prompt_id, cx); - let make_completion_provider = self.make_completion_provider.clone(); - self.pending_load = cx.spawn_in(window, async move |this, cx| { - let rule = rule.await; - let markdown = language_registry.language_for_name("Markdown").await; - this.update_in(cx, |this, window, cx| match rule { - Ok(rule) => { - let title_editor = cx.new(|cx| { - let mut editor = Editor::single_line(window, cx); - editor.set_placeholder_text("Untitled", window, cx); - editor.set_text(rule_metadata.title.unwrap_or_default(), window, cx); - if prompt_id.is_built_in() { - editor.set_read_only(true); - editor.set_show_edit_predictions(Some(false), window, cx); - } - editor - }); - let body_editor = cx.new(|cx| { - let buffer = cx.new(|cx| { - let mut buffer = Buffer::local(rule, cx); - buffer.set_language(markdown.log_err(), cx); - buffer.set_language_registry(language_registry); - buffer - }); - - let mut editor = Editor::for_buffer(buffer, None, window, cx); - if prompt_id.is_built_in() { - editor.set_read_only(true); - editor.set_show_edit_predictions(Some(false), window, cx); - } - editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); - editor.set_show_gutter(false, cx); - editor.set_show_wrap_guides(false, cx); - editor.set_show_indent_guides(false, cx); - editor.set_use_modal_editing(true); - editor.set_current_line_highlight(Some(CurrentLineHighlight::None)); - editor.set_completion_provider(Some(make_completion_provider())); - if focus { - window.focus(&editor.focus_handle(cx)); - } - editor - }); - let _subscriptions = vec![ - cx.subscribe_in( - &title_editor, - window, - move |this, editor, event, window, cx| { - this.handle_rule_title_editor_event( - prompt_id, editor, event, window, cx, - ) - }, - ), - cx.subscribe_in( - &body_editor, - window, - move |this, editor, event, window, cx| { - this.handle_rule_body_editor_event( - prompt_id, editor, event, window, cx, - ) - }, - ), - ]; - this.rule_editors.insert( - prompt_id, - RuleEditor { - title_editor, - body_editor, - next_title_and_body_to_save: None, - pending_save: None, - token_count: None, - pending_token_count: Task::ready(None), - _subscriptions, - }, - ); - this.set_active_rule(Some(prompt_id), window, cx); - this.count_tokens(prompt_id, window, cx); - } - Err(error) => { - // TODO: we should show the error in the UI. - log::error!("error while loading rule: {:?}", error); - } - }) - .ok(); - }); - } - } - - fn set_active_rule( - &mut self, - prompt_id: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.active_rule_id = prompt_id; - self.picker.update(cx, |picker, cx| { - if let Some(prompt_id) = prompt_id { - if picker - .delegate - .filtered_entries - .get(picker.delegate.selected_index()) - .is_none_or(|old_selected_prompt| { - if let RulePickerEntry::Rule(rule) = old_selected_prompt { - rule.id != prompt_id - } else { - true - } - }) - && let Some(ix) = picker.delegate.filtered_entries.iter().position(|mat| { - if let RulePickerEntry::Rule(rule) = mat { - rule.id == prompt_id - } else { - false - } - }) - { - picker.set_selected_index(ix, None, true, window, cx); - } - } else { - picker.focus(window, cx); - } - }); - cx.notify(); - } - - pub fn delete_rule( - &mut self, - prompt_id: PromptId, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(metadata) = self.store.read(cx).metadata(prompt_id) { - let confirmation = window.prompt( - PromptLevel::Warning, - &format!( - "Are you sure you want to delete {}", - metadata.title.unwrap_or("Untitled".into()) - ), - None, - &["Delete", "Cancel"], - cx, - ); - - cx.spawn_in(window, async move |this, cx| { - if confirmation.await.ok() == Some(0) { - this.update_in(cx, |this, window, cx| { - if this.active_rule_id == Some(prompt_id) { - this.set_active_rule(None, window, cx); - } - this.rule_editors.remove(&prompt_id); - this.store - .update(cx, |store, cx| store.delete(prompt_id, cx)) - .detach_and_log_err(cx); - this.picker - .update(cx, |picker, cx| picker.refresh(window, cx)); - cx.notify(); - })?; - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - } - - pub fn duplicate_rule( - &mut self, - prompt_id: PromptId, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(rule) = self.rule_editors.get(&prompt_id) { - const DUPLICATE_SUFFIX: &str = " copy"; - let title_to_duplicate = rule.title_editor.read(cx).text(cx); - let existing_titles = self - .rule_editors - .iter() - .filter(|&(&id, _)| id != prompt_id) - .map(|(_, rule_editor)| rule_editor.title_editor.read(cx).text(cx)) - .filter(|title| title.starts_with(&title_to_duplicate)) - .collect::>(); - - let title = if existing_titles.is_empty() { - title_to_duplicate + DUPLICATE_SUFFIX - } else { - let mut i = 1; - loop { - let new_title = format!("{title_to_duplicate}{DUPLICATE_SUFFIX} {i}"); - if !existing_titles.contains(&new_title) { - break new_title; - } - i += 1; - } - }; - - let new_id = PromptId::new(); - let body = rule.body_editor.read(cx).text(cx); - let save = self.store.update(cx, |store, cx| { - store.save(new_id, Some(title.into()), false, body.into(), cx) - }); - self.picker - .update(cx, |picker, cx| picker.refresh(window, cx)); - cx.spawn_in(window, async move |this, cx| { - save.await?; - this.update_in(cx, |rules_library, window, cx| { - rules_library.load_rule(new_id, true, window, cx) - }) - }) - .detach_and_log_err(cx); - } - } - - fn focus_active_rule(&mut self, _: &Tab, window: &mut Window, cx: &mut Context) { - if let Some(active_rule) = self.active_rule_id { - self.rule_editors[&active_rule] - .body_editor - .update(cx, |editor, cx| window.focus(&editor.focus_handle(cx))); - cx.stop_propagation(); - } - } - - fn focus_picker(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context) { - self.picker - .update(cx, |picker, cx| picker.focus(window, cx)); - } - - pub fn inline_assist( - &mut self, - action: &InlineAssist, - window: &mut Window, - cx: &mut Context, - ) { - let Some(active_rule_id) = self.active_rule_id else { - cx.propagate(); - return; - }; - - let rule_editor = &self.rule_editors[&active_rule_id].body_editor; - let Some(ConfiguredModel { provider, .. }) = - LanguageModelRegistry::read_global(cx).inline_assistant_model() - else { - return; - }; - - let initial_prompt = action.prompt.clone(); - if provider.is_authenticated(cx) { - self.inline_assist_delegate - .assist(rule_editor, initial_prompt, window, cx); - } else { - for window in cx.windows() { - if let Some(workspace) = window.downcast::() { - let panel = workspace - .update(cx, |workspace, window, cx| { - window.activate_window(); - self.inline_assist_delegate - .focus_agent_panel(workspace, window, cx) - }) - .ok(); - if panel == Some(true) { - return; - } - } - } - } - } - - fn move_down_from_title( - &mut self, - _: &editor::actions::MoveDown, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(rule_id) = self.active_rule_id - && let Some(rule_editor) = self.rule_editors.get(&rule_id) - { - window.focus(&rule_editor.body_editor.focus_handle(cx)); - } - } - - fn move_up_from_body( - &mut self, - _: &editor::actions::MoveUp, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(rule_id) = self.active_rule_id - && let Some(rule_editor) = self.rule_editors.get(&rule_id) - { - window.focus(&rule_editor.title_editor.focus_handle(cx)); - } - } - - fn handle_rule_title_editor_event( - &mut self, - prompt_id: PromptId, - title_editor: &Entity, - event: &EditorEvent, - window: &mut Window, - cx: &mut Context, - ) { - match event { - EditorEvent::BufferEdited => { - self.save_rule(prompt_id, window, cx); - self.count_tokens(prompt_id, window, cx); - } - EditorEvent::Blurred => { - title_editor.update(cx, |title_editor, cx| { - title_editor.change_selections( - SelectionEffects::no_scroll(), - window, - cx, - |selections| { - let cursor = selections.oldest_anchor().head(); - selections.select_anchor_ranges([cursor..cursor]); - }, - ); - }); - } - _ => {} - } - } - - fn handle_rule_body_editor_event( - &mut self, - prompt_id: PromptId, - body_editor: &Entity, - event: &EditorEvent, - window: &mut Window, - cx: &mut Context, - ) { - match event { - EditorEvent::BufferEdited => { - self.save_rule(prompt_id, window, cx); - self.count_tokens(prompt_id, window, cx); - } - EditorEvent::Blurred => { - body_editor.update(cx, |body_editor, cx| { - body_editor.change_selections( - SelectionEffects::no_scroll(), - window, - cx, - |selections| { - let cursor = selections.oldest_anchor().head(); - selections.select_anchor_ranges([cursor..cursor]); - }, - ); - }); - } - _ => {} - } - } - - fn count_tokens(&mut self, prompt_id: PromptId, window: &mut Window, cx: &mut Context) { - let Some(ConfiguredModel { model, .. }) = - LanguageModelRegistry::read_global(cx).default_model() - else { - return; - }; - if let Some(rule) = self.rule_editors.get_mut(&prompt_id) { - let editor = &rule.body_editor.read(cx); - let buffer = &editor.buffer().read(cx).as_singleton().unwrap().read(cx); - let body = buffer.as_rope().clone(); - rule.pending_token_count = cx.spawn_in(window, async move |this, cx| { - async move { - const DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1); - - cx.background_executor().timer(DEBOUNCE_TIMEOUT).await; - let token_count = cx - .update(|_, cx| { - model.count_tokens( - LanguageModelRequest { - thread_id: None, - prompt_id: None, - intent: None, - mode: None, - messages: vec![LanguageModelRequestMessage { - role: Role::System, - content: vec![body.to_string().into()], - cache: false, - reasoning_details: None, - }], - tools: Vec::new(), - tool_choice: None, - stop: Vec::new(), - temperature: None, - thinking_allowed: true, - }, - cx, - ) - })? - .await?; - - this.update(cx, |this, cx| { - let rule_editor = this.rule_editors.get_mut(&prompt_id).unwrap(); - rule_editor.token_count = Some(token_count); - cx.notify(); - }) - } - .log_err() - .await - }); - } - } - - fn render_rule_list(&mut self, cx: &mut Context) -> impl IntoElement { - v_flex() - .id("rule-list") - .capture_action(cx.listener(Self::focus_active_rule)) - .px_1p5() - .h_full() - .w_64() - .overflow_x_hidden() - .bg(cx.theme().colors().panel_background) - .map(|this| { - if cfg!(target_os = "macos") { - this.child( - h_flex() - .p(DynamicSpacing::Base04.rems(cx)) - .h_9() - .w_full() - .flex_none() - .justify_end() - .child( - IconButton::new("new-rule", IconName::Plus) - .tooltip(move |_window, cx| { - Tooltip::for_action("New Rule", &NewRule, cx) - }) - .on_click(|_, window, cx| { - window.dispatch_action(Box::new(NewRule), cx); - }), - ), - ) - } else { - this.child( - h_flex().p_1().w_full().child( - Button::new("new-rule", "New Rule") - .full_width() - .style(ButtonStyle::Outlined) - .icon(IconName::Plus) - .icon_size(IconSize::Small) - .icon_position(IconPosition::Start) - .icon_color(Color::Muted) - .on_click(|_, window, cx| { - window.dispatch_action(Box::new(NewRule), cx); - }), - ), - ) - } - }) - .child(div().flex_grow().child(self.picker.clone())) - } - - fn render_active_rule_editor( - &self, - editor: &Entity, - cx: &mut Context, - ) -> impl IntoElement { - let settings = ThemeSettings::get_global(cx); - - div() - .w_full() - .on_action(cx.listener(Self::move_down_from_title)) - .pl_1() - .border_1() - .border_color(transparent_black()) - .rounded_sm() - .group_hover("active-editor-header", |this| { - this.border_color(cx.theme().colors().border_variant) - }) - .child(EditorElement::new( - &editor, - EditorStyle { - background: cx.theme().system().transparent, - local_player: cx.theme().players().local(), - text: TextStyle { - color: cx.theme().colors().editor_foreground, - font_family: settings.ui_font.family.clone(), - font_features: settings.ui_font.features.clone(), - font_size: HeadlineSize::Large.rems().into(), - font_weight: settings.ui_font.weight, - line_height: relative(settings.buffer_line_height.value()), - ..Default::default() - }, - scrollbar_width: Pixels::ZERO, - syntax: cx.theme().syntax().clone(), - status: cx.theme().status().clone(), - inlay_hints_style: editor::make_inlay_hints_style(cx), - edit_prediction_styles: editor::make_suggestion_styles(cx), - ..EditorStyle::default() - }, - )) - } - - fn render_active_rule(&mut self, cx: &mut Context) -> gpui::Stateful
{ - div() - .id("rule-editor") - .h_full() - .flex_grow() - .border_l_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().editor_background) - .children(self.active_rule_id.and_then(|prompt_id| { - let rule_metadata = self.store.read(cx).metadata(prompt_id)?; - let rule_editor = &self.rule_editors[&prompt_id]; - let focus_handle = rule_editor.body_editor.focus_handle(cx); - let model = LanguageModelRegistry::read_global(cx) - .default_model() - .map(|default| default.model); - - Some( - v_flex() - .id("rule-editor-inner") - .size_full() - .relative() - .overflow_hidden() - .on_click(cx.listener(move |_, _, window, _| { - window.focus(&focus_handle); - })) - .child( - h_flex() - .group("active-editor-header") - .pt_2() - .pl_1p5() - .pr_2p5() - .gap_2() - .justify_between() - .child( - self.render_active_rule_editor(&rule_editor.title_editor, cx), - ) - .child( - h_flex() - .h_full() - .flex_shrink_0() - .children(rule_editor.token_count.map(|token_count| { - let token_count: SharedString = - token_count.to_string().into(); - let label_token_count: SharedString = - token_count.to_string().into(); - - div() - .id("token_count") - .mr_1() - .flex_shrink_0() - .tooltip(move |_window, cx| { - Tooltip::with_meta( - "Token Estimation", - None, - format!( - "Model: {}", - model - .as_ref() - .map(|model| model.name().0) - .unwrap_or_default() - ), - cx, - ) - }) - .child( - Label::new(format!( - "{} tokens", - label_token_count - )) - .color(Color::Muted), - ) - })) - .child(if prompt_id.is_built_in() { - div() - .id("built-in-rule") - .child( - Icon::new(IconName::FileLock) - .color(Color::Muted), - ) - .tooltip(move |_window, cx| { - Tooltip::with_meta( - "Built-in rule", - None, - BUILT_IN_TOOLTIP_TEXT, - cx, - ) - }) - .into_any() - } else { - IconButton::new("delete-rule", IconName::Trash) - .tooltip(move |_window, cx| { - Tooltip::for_action( - "Delete Rule", - &DeleteRule, - cx, - ) - }) - .on_click(|_, window, cx| { - window - .dispatch_action(Box::new(DeleteRule), cx); - }) - .into_any_element() - }) - .child( - IconButton::new("duplicate-rule", IconName::BookCopy) - .tooltip(move |_window, cx| { - Tooltip::for_action( - "Duplicate Rule", - &DuplicateRule, - cx, - ) - }) - .on_click(|_, window, cx| { - window.dispatch_action( - Box::new(DuplicateRule), - cx, - ); - }), - ) - .child( - IconButton::new( - "toggle-default-rule", - IconName::Paperclip, - ) - .toggle_state(rule_metadata.default) - .icon_color(if rule_metadata.default { - Color::Accent - } else { - Color::Muted - }) - .map(|this| { - if rule_metadata.default { - this.tooltip(Tooltip::text( - "Remove from Default Rules", - )) - } else { - this.tooltip(move |_window, cx| { - Tooltip::with_meta( - "Add to Default Rules", - None, - "Always included in every thread.", - cx, - ) - }) - } - }) - .on_click( - |_, window, cx| { - window.dispatch_action( - Box::new(ToggleDefaultRule), - cx, - ); - }, - ), - ), - ), - ) - .child( - div() - .on_action(cx.listener(Self::focus_picker)) - .on_action(cx.listener(Self::inline_assist)) - .on_action(cx.listener(Self::move_up_from_body)) - .h_full() - .flex_grow() - .child( - h_flex() - .py_2() - .pl_2p5() - .h_full() - .flex_1() - .child(rule_editor.body_editor.clone()), - ), - ), - ) - })) - } -} - -impl Render for RulesLibrary { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let ui_font = theme::setup_ui_font(window, cx); - let theme = cx.theme().clone(); - - client_side_decorations( - v_flex() - .id("rules-library") - .key_context("RulesLibrary") - .on_action(cx.listener(|this, &NewRule, window, cx| this.new_rule(window, cx))) - .on_action( - cx.listener(|this, &DeleteRule, window, cx| { - this.delete_active_rule(window, cx) - }), - ) - .on_action(cx.listener(|this, &DuplicateRule, window, cx| { - this.duplicate_active_rule(window, cx) - })) - .on_action(cx.listener(|this, &ToggleDefaultRule, window, cx| { - this.toggle_default_for_active_rule(window, cx) - })) - .size_full() - .overflow_hidden() - .font(ui_font) - .text_color(theme.colors().text) - .children(self.title_bar.clone()) - .bg(theme.colors().background) - .child( - h_flex() - .flex_1() - .when(!cfg!(target_os = "macos"), |this| { - this.border_t_1().border_color(cx.theme().colors().border) - }) - .child(self.render_rule_list(cx)) - .map(|el| { - if self.store.read(cx).prompt_count() == 0 { - el.child( - v_flex() - .h_full() - .flex_1() - .items_center() - .justify_center() - .border_l_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().editor_background) - .child( - Button::new("create-rule", "New Rule") - .style(ButtonStyle::Outlined) - .key_binding(KeyBinding::for_action(&NewRule, cx)) - .on_click(|_, window, cx| { - window - .dispatch_action(NewRule.boxed_clone(), cx) - }), - ), - ) - } else { - el.child(self.render_active_rule(cx)) - } - }), - ), - window, - cx, - ) - } -} diff --git a/crates/schema_generator/Cargo.toml b/crates/schema_generator/Cargo.toml deleted file mode 100644 index 865f76f4af..0000000000 --- a/crates/schema_generator/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "schema_generator" -version = "0.1.0" -publish.workspace = true -edition.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[dependencies] -anyhow.workspace = true -clap = { workspace = true, features = ["derive"] } -env_logger.workspace = true -schemars = { workspace = true, features = ["indexmap2"] } -serde.workspace = true -serde_json.workspace = true -theme.workspace = true diff --git a/crates/schema_generator/LICENSE-GPL b/crates/schema_generator/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/schema_generator/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/schema_generator/README.md b/crates/schema_generator/README.md deleted file mode 100644 index f8ef5bc234..0000000000 --- a/crates/schema_generator/README.md +++ /dev/null @@ -1,12 +0,0 @@ -# Zed Schema Generator - -Prints various Zed schemas to stdout. - -## Usage - -```sh -cargo run -p schema_generator -- --help - -cargo run -p schema_generator -- theme -cargo run -p schema_generator -- icon_theme -``` diff --git a/crates/schema_generator/src/main.rs b/crates/schema_generator/src/main.rs deleted file mode 100644 index a7e406a1a9..0000000000 --- a/crates/schema_generator/src/main.rs +++ /dev/null @@ -1,36 +0,0 @@ -use anyhow::Result; -use clap::{Parser, ValueEnum}; -use schemars::schema_for; -use theme::{IconThemeFamilyContent, ThemeFamilyContent}; - -#[derive(Parser, Debug)] -pub struct Args { - #[arg(value_enum)] - pub schema_type: SchemaType, -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] -#[clap(rename_all = "snake_case")] -pub enum SchemaType { - Theme, - IconTheme, -} - -fn main() -> Result<()> { - env_logger::init(); - - let args = Args::parse(); - - match args.schema_type { - SchemaType::Theme => { - let schema = schema_for!(ThemeFamilyContent); - println!("{}", serde_json::to_string_pretty(&schema)?); - } - SchemaType::IconTheme => { - let schema = schema_for!(IconThemeFamilyContent); - println!("{}", serde_json::to_string_pretty(&schema)?); - } - } - - Ok(()) -} diff --git a/crates/search/Cargo.toml b/crates/search/Cargo.toml deleted file mode 100644 index 02eb611fc2..0000000000 --- a/crates/search/Cargo.toml +++ /dev/null @@ -1,61 +0,0 @@ -[package] -name = "search" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[features] -test-support = [ - "client/test-support", - "editor/test-support", - "gpui/test-support", - "workspace/test-support", -] - -[lints] -workspace = true - -[lib] -path = "src/search.rs" -doctest = false - -[dependencies] -anyhow.workspace = true -any_vec.workspace = true -bitflags.workspace = true -collections.workspace = true -editor.workspace = true -futures.workspace = true -gpui.workspace = true -language.workspace = true -menu.workspace = true -project.workspace = true -schemars.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -smol.workspace = true -theme.workspace = true -ui.workspace = true -util.workspace = true -util_macros.workspace = true -workspace.workspace = true -zed_actions.workspace = true -itertools.workspace = true -ztracing.workspace = true -tracing.workspace = true - -[dev-dependencies] -client = { workspace = true, features = ["test-support"] } -editor = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -language = { workspace = true, features = ["test-support"] } -lsp.workspace = true -pretty_assertions.workspace = true -unindent.workspace = true -workspace = { workspace = true, features = ["test-support"] } - -[package.metadata.cargo-machete] -ignored = ["tracing"] - diff --git a/crates/search/LICENSE-GPL b/crates/search/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/search/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs deleted file mode 100644 index a9c26ac9ba..0000000000 --- a/crates/search/src/buffer_search.rs +++ /dev/null @@ -1,3118 +0,0 @@ -mod registrar; - -use crate::{ - FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOption, - SearchOptions, SearchSource, SelectAllMatches, SelectNextMatch, SelectPreviousMatch, - ToggleCaseSensitive, ToggleRegex, ToggleReplace, ToggleSelection, ToggleWholeWord, - search_bar::{ActionButtonState, input_base_styles, render_action_button, render_text_input}, -}; -use any_vec::AnyVec; -use anyhow::Context as _; -use collections::HashMap; -use editor::{ - DisplayPoint, Editor, EditorSettings, MultiBufferOffset, - actions::{Backtab, Tab}, -}; -use futures::channel::oneshot; -use gpui::{ - Action, App, ClickEvent, Context, Entity, EventEmitter, Focusable, InteractiveElement as _, - IntoElement, KeyContext, ParentElement as _, Render, ScrollHandle, Styled, Subscription, Task, - Window, actions, div, -}; -use language::{Language, LanguageRegistry}; -use project::{ - search::SearchQuery, - search_history::{SearchHistory, SearchHistoryCursor}, -}; -use schemars::JsonSchema; -use serde::Deserialize; -use settings::Settings; -use std::sync::Arc; -use zed_actions::{outline::ToggleOutline, workspace::CopyPath, workspace::CopyRelativePath}; - -use ui::{ - BASE_REM_SIZE_IN_PX, IconButton, IconButtonShape, IconName, Tooltip, h_flex, prelude::*, - utils::SearchInputWidth, -}; -use util::{ResultExt, paths::PathMatcher}; -use workspace::{ - ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, - item::ItemHandle, - searchable::{ - Direction, FilteredSearchRange, SearchEvent, SearchableItemHandle, WeakSearchableItemHandle, - }, -}; - -pub use registrar::DivRegistrar; -use registrar::{ForDeployed, ForDismissed, SearchActionsRegistrar, WithResults}; - -const MAX_BUFFER_SEARCH_HISTORY_SIZE: usize = 50; - -/// Opens the buffer search interface with the specified configuration. -#[derive(PartialEq, Clone, Deserialize, JsonSchema, Action)] -#[action(namespace = buffer_search)] -#[serde(deny_unknown_fields)] -pub struct Deploy { - #[serde(default = "util::serde::default_true")] - pub focus: bool, - #[serde(default)] - pub replace_enabled: bool, - #[serde(default)] - pub selection_search_enabled: bool, -} - -actions!( - buffer_search, - [ - /// Deploys the search and replace interface. - DeployReplace, - /// Dismisses the search bar. - Dismiss, - /// Focuses back on the editor. - FocusEditor - ] -); - -impl Deploy { - pub fn find() -> Self { - Self { - focus: true, - replace_enabled: false, - selection_search_enabled: false, - } - } - - pub fn replace() -> Self { - Self { - focus: true, - replace_enabled: true, - selection_search_enabled: false, - } - } -} - -pub enum Event { - UpdateLocation, -} - -pub fn init(cx: &mut App) { - cx.observe_new(|workspace: &mut Workspace, _, _| BufferSearchBar::register(workspace)) - .detach(); -} - -pub struct BufferSearchBar { - query_editor: Entity, - query_editor_focused: bool, - replacement_editor: Entity, - replacement_editor_focused: bool, - active_searchable_item: Option>, - active_match_index: Option, - active_searchable_item_subscription: Option, - active_search: Option>, - searchable_items_with_matches: HashMap, AnyVec>, - pending_search: Option>, - search_options: SearchOptions, - default_options: SearchOptions, - configured_options: SearchOptions, - query_error: Option, - dismissed: bool, - search_history: SearchHistory, - search_history_cursor: SearchHistoryCursor, - replace_enabled: bool, - selection_search_enabled: Option, - scroll_handle: ScrollHandle, - editor_scroll_handle: ScrollHandle, - editor_needed_width: Pixels, - regex_language: Option>, -} - -impl EventEmitter for BufferSearchBar {} -impl EventEmitter for BufferSearchBar {} -impl Render for BufferSearchBar { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - if self.dismissed { - return div().id("search_bar"); - } - - let focus_handle = self.focus_handle(cx); - - let narrow_mode = - self.scroll_handle.bounds().size.width / window.rem_size() < 340. / BASE_REM_SIZE_IN_PX; - let hide_inline_icons = self.editor_needed_width - > self.editor_scroll_handle.bounds().size.width - window.rem_size() * 6.; - - let workspace::searchable::SearchOptions { - case, - word, - regex, - replacement, - selection, - find_in_results, - } = self.supported_options(cx); - - self.query_editor.update(cx, |query_editor, cx| { - if query_editor.placeholder_text(cx).is_none() { - query_editor.set_placeholder_text("Search…", window, cx); - } - }); - - self.replacement_editor.update(cx, |editor, cx| { - editor.set_placeholder_text("Replace with…", window, cx); - }); - - let mut color_override = None; - let match_text = self - .active_searchable_item - .as_ref() - .and_then(|searchable_item| { - if self.query(cx).is_empty() { - return None; - } - let matches_count = self - .searchable_items_with_matches - .get(&searchable_item.downgrade()) - .map(AnyVec::len) - .unwrap_or(0); - if let Some(match_ix) = self.active_match_index { - Some(format!("{}/{}", match_ix + 1, matches_count)) - } else { - color_override = Some(Color::Error); // No matches found - None - } - }) - .unwrap_or_else(|| "0/0".to_string()); - let should_show_replace_input = self.replace_enabled && replacement; - let in_replace = self.replacement_editor.focus_handle(cx).is_focused(window); - - let theme_colors = cx.theme().colors(); - let query_border = if self.query_error.is_some() { - Color::Error.color(cx) - } else { - theme_colors.border - }; - let replacement_border = theme_colors.border; - - let container_width = window.viewport_size().width; - let input_width = SearchInputWidth::calc_width(container_width); - - let input_base_styles = - |border_color| input_base_styles(border_color, |div| div.w(input_width)); - - let query_column = input_base_styles(query_border) - .id("editor-scroll") - .track_scroll(&self.editor_scroll_handle) - .child(render_text_input(&self.query_editor, color_override, cx)) - .when(!hide_inline_icons, |div| { - div.child( - h_flex() - .gap_1() - .when(case, |div| { - div.child(SearchOption::CaseSensitive.as_button( - self.search_options, - SearchSource::Buffer, - focus_handle.clone(), - )) - }) - .when(word, |div| { - div.child(SearchOption::WholeWord.as_button( - self.search_options, - SearchSource::Buffer, - focus_handle.clone(), - )) - }) - .when(regex, |div| { - div.child(SearchOption::Regex.as_button( - self.search_options, - SearchSource::Buffer, - focus_handle.clone(), - )) - }), - ) - }); - - let mode_column = h_flex() - .gap_1() - .min_w_64() - .when(replacement, |this| { - this.child(render_action_button( - "buffer-search-bar-toggle", - IconName::Replace, - self.replace_enabled.then_some(ActionButtonState::Toggled), - "Toggle Replace", - &ToggleReplace, - focus_handle.clone(), - )) - }) - .when(selection, |this| { - this.child( - IconButton::new( - "buffer-search-bar-toggle-search-selection-button", - IconName::Quote, - ) - .style(ButtonStyle::Subtle) - .shape(IconButtonShape::Square) - .when(self.selection_search_enabled.is_some(), |button| { - button.style(ButtonStyle::Filled) - }) - .on_click(cx.listener(|this, _: &ClickEvent, window, cx| { - this.toggle_selection(&ToggleSelection, window, cx); - })) - .toggle_state(self.selection_search_enabled.is_some()) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |_window, cx| { - Tooltip::for_action_in( - "Toggle Search Selection", - &ToggleSelection, - &focus_handle, - cx, - ) - } - }), - ) - }) - .when(!find_in_results, |el| { - let query_focus = self.query_editor.focus_handle(cx); - let matches_column = h_flex() - .pl_2() - .ml_2() - .border_l_1() - .border_color(theme_colors.border_variant) - .child(render_action_button( - "buffer-search-nav-button", - ui::IconName::ChevronLeft, - self.active_match_index - .is_none() - .then_some(ActionButtonState::Disabled), - "Select Previous Match", - &SelectPreviousMatch, - query_focus.clone(), - )) - .child(render_action_button( - "buffer-search-nav-button", - ui::IconName::ChevronRight, - self.active_match_index - .is_none() - .then_some(ActionButtonState::Disabled), - "Select Next Match", - &SelectNextMatch, - query_focus.clone(), - )) - .when(!narrow_mode, |this| { - this.child(div().ml_2().min_w(rems_from_px(40.)).child( - Label::new(match_text).size(LabelSize::Small).color( - if self.active_match_index.is_some() { - Color::Default - } else { - Color::Disabled - }, - ), - )) - }); - - el.child(render_action_button( - "buffer-search-nav-button", - IconName::SelectAll, - Default::default(), - "Select All Matches", - &SelectAllMatches, - query_focus, - )) - .child(matches_column) - }) - .when(find_in_results, |el| { - el.child(render_action_button( - "buffer-search", - IconName::Close, - Default::default(), - "Close Search Bar", - &Dismiss, - focus_handle.clone(), - )) - }); - - let search_line = h_flex() - .w_full() - .gap_2() - .when(find_in_results, |el| { - el.child(Label::new("Find in results").color(Color::Hint)) - }) - .child(query_column) - .child(mode_column); - - let replace_line = - should_show_replace_input.then(|| { - let replace_column = input_base_styles(replacement_border) - .child(render_text_input(&self.replacement_editor, None, cx)); - let focus_handle = self.replacement_editor.read(cx).focus_handle(cx); - - let replace_actions = h_flex() - .min_w_64() - .gap_1() - .child(render_action_button( - "buffer-search-replace-button", - IconName::ReplaceNext, - Default::default(), - "Replace Next Match", - &ReplaceNext, - focus_handle.clone(), - )) - .child(render_action_button( - "buffer-search-replace-button", - IconName::ReplaceAll, - Default::default(), - "Replace All Matches", - &ReplaceAll, - focus_handle, - )); - h_flex() - .w_full() - .gap_2() - .child(replace_column) - .child(replace_actions) - }); - - let mut key_context = KeyContext::new_with_defaults(); - key_context.add("BufferSearchBar"); - if in_replace { - key_context.add("in_replace"); - } - - let query_error_line = self.query_error.as_ref().map(|error| { - Label::new(error) - .size(LabelSize::Small) - .color(Color::Error) - .mt_neg_1() - .ml_2() - }); - - let search_line = - h_flex() - .relative() - .child(search_line) - .when(!narrow_mode && !find_in_results, |div| { - div.child(h_flex().absolute().right_0().child(render_action_button( - "buffer-search", - IconName::Close, - Default::default(), - "Close Search Bar", - &Dismiss, - focus_handle.clone(), - ))) - .w_full() - }); - v_flex() - .id("buffer_search") - .gap_2() - .py(px(1.0)) - .w_full() - .track_scroll(&self.scroll_handle) - .key_context(key_context) - .capture_action(cx.listener(Self::tab)) - .capture_action(cx.listener(Self::backtab)) - .on_action(cx.listener(Self::previous_history_query)) - .on_action(cx.listener(Self::next_history_query)) - .on_action(cx.listener(Self::dismiss)) - .on_action(cx.listener(Self::select_next_match)) - .on_action(cx.listener(Self::select_prev_match)) - .on_action(cx.listener(|this, _: &ToggleOutline, window, cx| { - if let Some(active_searchable_item) = &mut this.active_searchable_item { - active_searchable_item.relay_action(Box::new(ToggleOutline), window, cx); - } - })) - .on_action(cx.listener(|this, _: &CopyPath, window, cx| { - if let Some(active_searchable_item) = &mut this.active_searchable_item { - active_searchable_item.relay_action(Box::new(CopyPath), window, cx); - } - })) - .on_action(cx.listener(|this, _: &CopyRelativePath, window, cx| { - if let Some(active_searchable_item) = &mut this.active_searchable_item { - active_searchable_item.relay_action(Box::new(CopyRelativePath), window, cx); - } - })) - .when(replacement, |this| { - this.on_action(cx.listener(Self::toggle_replace)) - .on_action(cx.listener(Self::replace_next)) - .on_action(cx.listener(Self::replace_all)) - }) - .when(case, |this| { - this.on_action(cx.listener(Self::toggle_case_sensitive)) - }) - .when(word, |this| { - this.on_action(cx.listener(Self::toggle_whole_word)) - }) - .when(regex, |this| { - this.on_action(cx.listener(Self::toggle_regex)) - }) - .when(selection, |this| { - this.on_action(cx.listener(Self::toggle_selection)) - }) - .child(search_line) - .children(query_error_line) - .children(replace_line) - } -} - -impl Focusable for BufferSearchBar { - fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { - self.query_editor.focus_handle(cx) - } -} - -impl ToolbarItemView for BufferSearchBar { - fn contribute_context(&self, context: &mut KeyContext, _cx: &App) { - if !self.dismissed { - context.add("buffer_search_deployed"); - } - } - - fn set_active_pane_item( - &mut self, - item: Option<&dyn ItemHandle>, - window: &mut Window, - cx: &mut Context, - ) -> ToolbarItemLocation { - cx.notify(); - self.active_searchable_item_subscription.take(); - self.active_searchable_item.take(); - - self.pending_search.take(); - - if let Some(searchable_item_handle) = - item.and_then(|item| item.to_searchable_item_handle(cx)) - { - let this = cx.entity().downgrade(); - - self.active_searchable_item_subscription = - Some(searchable_item_handle.subscribe_to_search_events( - window, - cx, - Box::new(move |search_event, window, cx| { - if let Some(this) = this.upgrade() { - this.update(cx, |this, cx| { - this.on_active_searchable_item_event(search_event, window, cx) - }); - } - }), - )); - - let is_project_search = searchable_item_handle.supported_options(cx).find_in_results; - self.active_searchable_item = Some(searchable_item_handle); - drop(self.update_matches(true, false, window, cx)); - if !self.dismissed { - if is_project_search { - self.dismiss(&Default::default(), window, cx); - } else { - return ToolbarItemLocation::Secondary; - } - } - } - ToolbarItemLocation::Hidden - } -} - -impl BufferSearchBar { - pub fn query_editor_focused(&self) -> bool { - self.query_editor_focused - } - - pub fn register(registrar: &mut impl SearchActionsRegistrar) { - registrar.register_handler(ForDeployed(|this, _: &FocusSearch, window, cx| { - this.query_editor.focus_handle(cx).focus(window); - this.select_query(window, cx); - })); - registrar.register_handler(ForDeployed( - |this, action: &ToggleCaseSensitive, window, cx| { - if this.supported_options(cx).case { - this.toggle_case_sensitive(action, window, cx); - } - }, - )); - registrar.register_handler(ForDeployed(|this, action: &ToggleWholeWord, window, cx| { - if this.supported_options(cx).word { - this.toggle_whole_word(action, window, cx); - } - })); - registrar.register_handler(ForDeployed(|this, action: &ToggleRegex, window, cx| { - if this.supported_options(cx).regex { - this.toggle_regex(action, window, cx); - } - })); - registrar.register_handler(ForDeployed(|this, action: &ToggleSelection, window, cx| { - if this.supported_options(cx).selection { - this.toggle_selection(action, window, cx); - } else { - cx.propagate(); - } - })); - registrar.register_handler(ForDeployed(|this, action: &ToggleReplace, window, cx| { - if this.supported_options(cx).replacement { - this.toggle_replace(action, window, cx); - } else { - cx.propagate(); - } - })); - registrar.register_handler(WithResults(|this, action: &SelectNextMatch, window, cx| { - if this.supported_options(cx).find_in_results { - cx.propagate(); - } else { - this.select_next_match(action, window, cx); - } - })); - registrar.register_handler(WithResults( - |this, action: &SelectPreviousMatch, window, cx| { - if this.supported_options(cx).find_in_results { - cx.propagate(); - } else { - this.select_prev_match(action, window, cx); - } - }, - )); - registrar.register_handler(WithResults( - |this, action: &SelectAllMatches, window, cx| { - if this.supported_options(cx).find_in_results { - cx.propagate(); - } else { - this.select_all_matches(action, window, cx); - } - }, - )); - registrar.register_handler(ForDeployed( - |this, _: &editor::actions::Cancel, window, cx| { - this.dismiss(&Dismiss, window, cx); - }, - )); - registrar.register_handler(ForDeployed(|this, _: &Dismiss, window, cx| { - this.dismiss(&Dismiss, window, cx); - })); - - // register deploy buffer search for both search bar states, since we want to focus into the search bar - // when the deploy action is triggered in the buffer. - registrar.register_handler(ForDeployed(|this, deploy, window, cx| { - this.deploy(deploy, window, cx); - })); - registrar.register_handler(ForDismissed(|this, deploy, window, cx| { - this.deploy(deploy, window, cx); - })); - registrar.register_handler(ForDeployed(|this, _: &DeployReplace, window, cx| { - if this.supported_options(cx).find_in_results { - cx.propagate(); - } else { - this.deploy(&Deploy::replace(), window, cx); - } - })); - registrar.register_handler(ForDismissed(|this, _: &DeployReplace, window, cx| { - if this.supported_options(cx).find_in_results { - cx.propagate(); - } else { - this.deploy(&Deploy::replace(), window, cx); - } - })); - } - - pub fn new( - languages: Option>, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let query_editor = cx.new(|cx| { - let mut editor = Editor::single_line(window, cx); - editor.set_use_autoclose(false); - editor - }); - cx.subscribe_in(&query_editor, window, Self::on_query_editor_event) - .detach(); - let replacement_editor = cx.new(|cx| Editor::single_line(window, cx)); - cx.subscribe(&replacement_editor, Self::on_replacement_editor_event) - .detach(); - - let search_options = SearchOptions::from_settings(&EditorSettings::get_global(cx).search); - if let Some(languages) = languages { - let query_buffer = query_editor - .read(cx) - .buffer() - .read(cx) - .as_singleton() - .expect("query editor should be backed by a singleton buffer"); - query_buffer - .read(cx) - .set_language_registry(languages.clone()); - - cx.spawn(async move |buffer_search_bar, cx| { - let regex_language = languages - .language_for_name("regex") - .await - .context("loading regex language")?; - buffer_search_bar - .update(cx, |buffer_search_bar, cx| { - buffer_search_bar.regex_language = Some(regex_language); - buffer_search_bar.adjust_query_regex_language(cx); - }) - .ok(); - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - - Self { - query_editor, - query_editor_focused: false, - replacement_editor, - replacement_editor_focused: false, - active_searchable_item: None, - active_searchable_item_subscription: None, - active_match_index: None, - searchable_items_with_matches: Default::default(), - default_options: search_options, - configured_options: search_options, - search_options, - pending_search: None, - query_error: None, - dismissed: true, - search_history: SearchHistory::new( - Some(MAX_BUFFER_SEARCH_HISTORY_SIZE), - project::search_history::QueryInsertionBehavior::ReplacePreviousIfContains, - ), - search_history_cursor: Default::default(), - active_search: None, - replace_enabled: false, - selection_search_enabled: None, - scroll_handle: ScrollHandle::new(), - editor_scroll_handle: ScrollHandle::new(), - editor_needed_width: px(0.), - regex_language: None, - } - } - - pub fn is_dismissed(&self) -> bool { - self.dismissed - } - - pub fn dismiss(&mut self, _: &Dismiss, window: &mut Window, cx: &mut Context) { - self.dismissed = true; - self.query_error = None; - self.sync_select_next_case_sensitivity(cx); - - for searchable_item in self.searchable_items_with_matches.keys() { - if let Some(searchable_item) = - WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx) - { - searchable_item.clear_matches(window, cx); - } - } - if let Some(active_editor) = self.active_searchable_item.as_mut() { - self.selection_search_enabled = None; - self.replace_enabled = false; - active_editor.search_bar_visibility_changed(false, window, cx); - active_editor.toggle_filtered_search_ranges(None, window, cx); - let handle = active_editor.item_focus_handle(cx); - self.focus(&handle, window); - } - - cx.emit(Event::UpdateLocation); - cx.emit(ToolbarItemEvent::ChangeLocation( - ToolbarItemLocation::Hidden, - )); - cx.notify(); - } - - pub fn deploy(&mut self, deploy: &Deploy, window: &mut Window, cx: &mut Context) -> bool { - let filtered_search_range = if deploy.selection_search_enabled { - Some(FilteredSearchRange::Default) - } else { - None - }; - if self.show(window, cx) { - if let Some(active_item) = self.active_searchable_item.as_mut() { - active_item.toggle_filtered_search_ranges(filtered_search_range, window, cx); - } - self.search_suggested(window, cx); - self.smartcase(window, cx); - self.sync_select_next_case_sensitivity(cx); - self.replace_enabled = deploy.replace_enabled; - self.selection_search_enabled = if deploy.selection_search_enabled { - Some(FilteredSearchRange::Default) - } else { - None - }; - if deploy.focus { - let mut handle = self.query_editor.focus_handle(cx); - let mut select_query = true; - if deploy.replace_enabled && handle.is_focused(window) { - handle = self.replacement_editor.focus_handle(cx); - select_query = false; - }; - - if select_query { - self.select_query(window, cx); - } - - window.focus(&handle); - } - return true; - } - - cx.propagate(); - false - } - - pub fn toggle(&mut self, action: &Deploy, window: &mut Window, cx: &mut Context) { - if self.is_dismissed() { - self.deploy(action, window, cx); - } else { - self.dismiss(&Dismiss, window, cx); - } - } - - pub fn show(&mut self, window: &mut Window, cx: &mut Context) -> bool { - let Some(handle) = self.active_searchable_item.as_ref() else { - return false; - }; - - let configured_options = - SearchOptions::from_settings(&EditorSettings::get_global(cx).search); - let settings_changed = configured_options != self.configured_options; - - if self.dismissed && settings_changed { - // Only update configuration options when search bar is dismissed, - // so we don't miss updates even after calling show twice - self.configured_options = configured_options; - self.search_options = configured_options; - self.default_options = configured_options; - } - - self.dismissed = false; - self.adjust_query_regex_language(cx); - handle.search_bar_visibility_changed(true, window, cx); - cx.notify(); - cx.emit(Event::UpdateLocation); - cx.emit(ToolbarItemEvent::ChangeLocation( - ToolbarItemLocation::Secondary, - )); - true - } - - fn supported_options(&self, cx: &mut Context) -> workspace::searchable::SearchOptions { - self.active_searchable_item - .as_ref() - .map(|item| item.supported_options(cx)) - .unwrap_or_default() - } - - pub fn search_suggested(&mut self, window: &mut Window, cx: &mut Context) { - let search = self.query_suggestion(window, cx).map(|suggestion| { - self.search(&suggestion, Some(self.default_options), true, window, cx) - }); - - if let Some(search) = search { - cx.spawn_in(window, async move |this, cx| { - if search.await.is_ok() { - this.update_in(cx, |this, window, cx| { - this.activate_current_match(window, cx) - }) - } else { - Ok(()) - } - }) - .detach_and_log_err(cx); - } - } - - pub fn activate_current_match(&mut self, window: &mut Window, cx: &mut Context) { - if let Some(match_ix) = self.active_match_index - && let Some(active_searchable_item) = self.active_searchable_item.as_ref() - && let Some(matches) = self - .searchable_items_with_matches - .get(&active_searchable_item.downgrade()) - { - active_searchable_item.activate_match(match_ix, matches, window, cx) - } - } - - pub fn select_query(&mut self, window: &mut Window, cx: &mut Context) { - self.query_editor.update(cx, |query_editor, cx| { - query_editor.select_all(&Default::default(), window, cx); - }); - } - - pub fn query(&self, cx: &App) -> String { - self.query_editor.read(cx).text(cx) - } - - pub fn replacement(&self, cx: &mut App) -> String { - self.replacement_editor.read(cx).text(cx) - } - - pub fn query_suggestion( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> Option { - self.active_searchable_item - .as_ref() - .map(|searchable_item| searchable_item.query_suggestion(window, cx)) - .filter(|suggestion| !suggestion.is_empty()) - } - - pub fn set_replacement(&mut self, replacement: Option<&str>, cx: &mut Context) { - if replacement.is_none() { - self.replace_enabled = false; - return; - } - self.replace_enabled = true; - self.replacement_editor - .update(cx, |replacement_editor, cx| { - replacement_editor - .buffer() - .update(cx, |replacement_buffer, cx| { - let len = replacement_buffer.len(cx); - replacement_buffer.edit( - [(MultiBufferOffset(0)..len, replacement.unwrap())], - None, - cx, - ); - }); - }); - } - - pub fn focus_replace(&mut self, window: &mut Window, cx: &mut Context) { - self.focus(&self.replacement_editor.focus_handle(cx), window); - cx.notify(); - } - - pub fn search( - &mut self, - query: &str, - options: Option, - add_to_history: bool, - window: &mut Window, - cx: &mut Context, - ) -> oneshot::Receiver<()> { - let options = options.unwrap_or(self.default_options); - let updated = query != self.query(cx) || self.search_options != options; - if updated { - self.query_editor.update(cx, |query_editor, cx| { - query_editor.buffer().update(cx, |query_buffer, cx| { - let len = query_buffer.len(cx); - query_buffer.edit([(MultiBufferOffset(0)..len, query)], None, cx); - }); - }); - self.set_search_options(options, cx); - self.clear_matches(window, cx); - cx.notify(); - } - self.update_matches(!updated, add_to_history, window, cx) - } - - pub fn focus_editor(&mut self, _: &FocusEditor, window: &mut Window, cx: &mut Context) { - if let Some(active_editor) = self.active_searchable_item.as_ref() { - let handle = active_editor.item_focus_handle(cx); - window.focus(&handle); - } - } - - pub fn toggle_search_option( - &mut self, - search_option: SearchOptions, - window: &mut Window, - cx: &mut Context, - ) { - self.search_options.toggle(search_option); - self.default_options = self.search_options; - drop(self.update_matches(false, false, window, cx)); - self.adjust_query_regex_language(cx); - self.sync_select_next_case_sensitivity(cx); - cx.notify(); - } - - pub fn has_search_option(&mut self, search_option: SearchOptions) -> bool { - self.search_options.contains(search_option) - } - - pub fn enable_search_option( - &mut self, - search_option: SearchOptions, - window: &mut Window, - cx: &mut Context, - ) { - if !self.search_options.contains(search_option) { - self.toggle_search_option(search_option, window, cx) - } - } - - pub fn set_search_within_selection( - &mut self, - search_within_selection: Option, - window: &mut Window, - cx: &mut Context, - ) -> Option> { - let active_item = self.active_searchable_item.as_mut()?; - self.selection_search_enabled = search_within_selection; - active_item.toggle_filtered_search_ranges(self.selection_search_enabled, window, cx); - cx.notify(); - Some(self.update_matches(false, false, window, cx)) - } - - pub fn set_search_options(&mut self, search_options: SearchOptions, cx: &mut Context) { - self.search_options = search_options; - self.adjust_query_regex_language(cx); - self.sync_select_next_case_sensitivity(cx); - cx.notify(); - } - - pub fn clear_search_within_ranges( - &mut self, - search_options: SearchOptions, - cx: &mut Context, - ) { - self.search_options = search_options; - self.adjust_query_regex_language(cx); - cx.notify(); - } - - fn select_next_match( - &mut self, - _: &SelectNextMatch, - window: &mut Window, - cx: &mut Context, - ) { - self.select_match(Direction::Next, 1, window, cx); - } - - fn select_prev_match( - &mut self, - _: &SelectPreviousMatch, - window: &mut Window, - cx: &mut Context, - ) { - self.select_match(Direction::Prev, 1, window, cx); - } - - pub fn select_all_matches( - &mut self, - _: &SelectAllMatches, - window: &mut Window, - cx: &mut Context, - ) { - if !self.dismissed - && self.active_match_index.is_some() - && let Some(searchable_item) = self.active_searchable_item.as_ref() - && let Some(matches) = self - .searchable_items_with_matches - .get(&searchable_item.downgrade()) - { - searchable_item.select_matches(matches, window, cx); - self.focus_editor(&FocusEditor, window, cx); - } - } - - pub fn select_match( - &mut self, - direction: Direction, - count: usize, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(index) = self.active_match_index - && let Some(searchable_item) = self.active_searchable_item.as_ref() - && let Some(matches) = self - .searchable_items_with_matches - .get(&searchable_item.downgrade()) - .filter(|matches| !matches.is_empty()) - { - // If 'wrapscan' is disabled, searches do not wrap around the end of the file. - if !EditorSettings::get_global(cx).search_wrap - && ((direction == Direction::Next && index + count >= matches.len()) - || (direction == Direction::Prev && index < count)) - { - crate::show_no_more_matches(window, cx); - return; - } - let new_match_index = searchable_item - .match_index_for_direction(matches, index, direction, count, window, cx); - - searchable_item.update_matches(matches, Some(new_match_index), window, cx); - searchable_item.activate_match(new_match_index, matches, window, cx); - } - } - - pub fn select_first_match(&mut self, window: &mut Window, cx: &mut Context) { - if let Some(searchable_item) = self.active_searchable_item.as_ref() - && let Some(matches) = self - .searchable_items_with_matches - .get(&searchable_item.downgrade()) - { - if matches.is_empty() { - return; - } - searchable_item.update_matches(matches, Some(0), window, cx); - searchable_item.activate_match(0, matches, window, cx); - } - } - - pub fn select_last_match(&mut self, window: &mut Window, cx: &mut Context) { - if let Some(searchable_item) = self.active_searchable_item.as_ref() - && let Some(matches) = self - .searchable_items_with_matches - .get(&searchable_item.downgrade()) - { - if matches.is_empty() { - return; - } - let new_match_index = matches.len() - 1; - searchable_item.update_matches(matches, Some(new_match_index), window, cx); - searchable_item.activate_match(new_match_index, matches, window, cx); - } - } - - fn on_query_editor_event( - &mut self, - editor: &Entity, - event: &editor::EditorEvent, - window: &mut Window, - cx: &mut Context, - ) { - match event { - editor::EditorEvent::Focused => self.query_editor_focused = true, - editor::EditorEvent::Blurred => self.query_editor_focused = false, - editor::EditorEvent::Edited { .. } => { - self.smartcase(window, cx); - self.clear_matches(window, cx); - let search = self.update_matches(false, true, window, cx); - - let width = editor.update(cx, |editor, cx| { - let text_layout_details = editor.text_layout_details(window); - let snapshot = editor.snapshot(window, cx).display_snapshot; - - snapshot.x_for_display_point(snapshot.max_point(), &text_layout_details) - - snapshot.x_for_display_point(DisplayPoint::zero(), &text_layout_details) - }); - self.editor_needed_width = width; - cx.notify(); - - cx.spawn_in(window, async move |this, cx| { - if search.await.is_ok() { - this.update_in(cx, |this, window, cx| { - this.activate_current_match(window, cx) - }) - } else { - Ok(()) - } - }) - .detach_and_log_err(cx); - } - _ => {} - } - } - - fn on_replacement_editor_event( - &mut self, - _: Entity, - event: &editor::EditorEvent, - _: &mut Context, - ) { - match event { - editor::EditorEvent::Focused => self.replacement_editor_focused = true, - editor::EditorEvent::Blurred => self.replacement_editor_focused = false, - _ => {} - } - } - - fn on_active_searchable_item_event( - &mut self, - event: &SearchEvent, - window: &mut Window, - cx: &mut Context, - ) { - match event { - SearchEvent::MatchesInvalidated => { - drop(self.update_matches(false, false, window, cx)); - } - SearchEvent::ActiveMatchChanged => self.update_match_index(window, cx), - } - } - - fn toggle_case_sensitive( - &mut self, - _: &ToggleCaseSensitive, - window: &mut Window, - cx: &mut Context, - ) { - self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx) - } - - fn toggle_whole_word( - &mut self, - _: &ToggleWholeWord, - window: &mut Window, - cx: &mut Context, - ) { - self.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx) - } - - fn toggle_selection( - &mut self, - _: &ToggleSelection, - window: &mut Window, - cx: &mut Context, - ) { - self.set_search_within_selection( - if let Some(_) = self.selection_search_enabled { - None - } else { - Some(FilteredSearchRange::Default) - }, - window, - cx, - ); - } - - fn toggle_regex(&mut self, _: &ToggleRegex, window: &mut Window, cx: &mut Context) { - self.toggle_search_option(SearchOptions::REGEX, window, cx) - } - - fn clear_active_searchable_item_matches(&mut self, window: &mut Window, cx: &mut App) { - if let Some(active_searchable_item) = self.active_searchable_item.as_ref() { - self.active_match_index = None; - self.searchable_items_with_matches - .remove(&active_searchable_item.downgrade()); - active_searchable_item.clear_matches(window, cx); - } - } - - pub fn has_active_match(&self) -> bool { - self.active_match_index.is_some() - } - - fn clear_matches(&mut self, window: &mut Window, cx: &mut Context) { - let mut active_item_matches = None; - for (searchable_item, matches) in self.searchable_items_with_matches.drain() { - if let Some(searchable_item) = - WeakSearchableItemHandle::upgrade(searchable_item.as_ref(), cx) - { - if Some(&searchable_item) == self.active_searchable_item.as_ref() { - active_item_matches = Some((searchable_item.downgrade(), matches)); - } else { - searchable_item.clear_matches(window, cx); - } - } - } - - self.searchable_items_with_matches - .extend(active_item_matches); - } - - fn update_matches( - &mut self, - reuse_existing_query: bool, - add_to_history: bool, - window: &mut Window, - cx: &mut Context, - ) -> oneshot::Receiver<()> { - let (done_tx, done_rx) = oneshot::channel(); - let query = self.query(cx); - self.pending_search.take(); - - if let Some(active_searchable_item) = self.active_searchable_item.as_ref() { - self.query_error = None; - if query.is_empty() { - self.clear_active_searchable_item_matches(window, cx); - let _ = done_tx.send(()); - cx.notify(); - } else { - let query: Arc<_> = if let Some(search) = - self.active_search.take().filter(|_| reuse_existing_query) - { - search - } else { - // Value doesn't matter, we only construct empty matchers with it - - if self.search_options.contains(SearchOptions::REGEX) { - match SearchQuery::regex( - query, - self.search_options.contains(SearchOptions::WHOLE_WORD), - self.search_options.contains(SearchOptions::CASE_SENSITIVE), - false, - self.search_options - .contains(SearchOptions::ONE_MATCH_PER_LINE), - PathMatcher::default(), - PathMatcher::default(), - false, - None, - ) { - Ok(query) => query.with_replacement(self.replacement(cx)), - Err(e) => { - self.query_error = Some(e.to_string()); - self.clear_active_searchable_item_matches(window, cx); - cx.notify(); - return done_rx; - } - } - } else { - match SearchQuery::text( - query, - self.search_options.contains(SearchOptions::WHOLE_WORD), - self.search_options.contains(SearchOptions::CASE_SENSITIVE), - false, - PathMatcher::default(), - PathMatcher::default(), - false, - None, - ) { - Ok(query) => query.with_replacement(self.replacement(cx)), - Err(e) => { - self.query_error = Some(e.to_string()); - self.clear_active_searchable_item_matches(window, cx); - cx.notify(); - return done_rx; - } - } - } - .into() - }; - - self.active_search = Some(query.clone()); - let query_text = query.as_str().to_string(); - - let matches = active_searchable_item.find_matches(query, window, cx); - - let active_searchable_item = active_searchable_item.downgrade(); - self.pending_search = Some(cx.spawn_in(window, async move |this, cx| { - let matches = matches.await; - - this.update_in(cx, |this, window, cx| { - if let Some(active_searchable_item) = - WeakSearchableItemHandle::upgrade(active_searchable_item.as_ref(), cx) - { - this.searchable_items_with_matches - .insert(active_searchable_item.downgrade(), matches); - - this.update_match_index(window, cx); - if add_to_history { - this.search_history - .add(&mut this.search_history_cursor, query_text); - } - if !this.dismissed { - let matches = this - .searchable_items_with_matches - .get(&active_searchable_item.downgrade()) - .unwrap(); - if matches.is_empty() { - active_searchable_item.clear_matches(window, cx); - } else { - active_searchable_item.update_matches( - matches, - this.active_match_index, - window, - cx, - ); - } - let _ = done_tx.send(()); - } - cx.notify(); - } - }) - .log_err(); - })); - } - } - done_rx - } - - fn reverse_direction_if_backwards(&self, direction: Direction) -> Direction { - if self.search_options.contains(SearchOptions::BACKWARDS) { - direction.opposite() - } else { - direction - } - } - - pub fn update_match_index(&mut self, window: &mut Window, cx: &mut Context) { - let direction = self.reverse_direction_if_backwards(Direction::Next); - let new_index = self - .active_searchable_item - .as_ref() - .and_then(|searchable_item| { - let matches = self - .searchable_items_with_matches - .get(&searchable_item.downgrade())?; - searchable_item.active_match_index(direction, matches, window, cx) - }); - if new_index != self.active_match_index { - self.active_match_index = new_index; - if !self.dismissed { - if let Some(searchable_item) = self.active_searchable_item.as_ref() { - if let Some(matches) = self - .searchable_items_with_matches - .get(&searchable_item.downgrade()) - { - if !matches.is_empty() { - searchable_item.update_matches(matches, new_index, window, cx); - } - } - } - } - cx.notify(); - } - } - - fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context) { - self.cycle_field(Direction::Next, window, cx); - } - - fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context) { - self.cycle_field(Direction::Prev, window, cx); - } - fn cycle_field(&mut self, direction: Direction, window: &mut Window, cx: &mut Context) { - let mut handles = vec![self.query_editor.focus_handle(cx)]; - if self.replace_enabled { - handles.push(self.replacement_editor.focus_handle(cx)); - } - if let Some(item) = self.active_searchable_item.as_ref() { - handles.push(item.item_focus_handle(cx)); - } - let current_index = match handles.iter().position(|focus| focus.is_focused(window)) { - Some(index) => index, - None => return, - }; - - let new_index = match direction { - Direction::Next => (current_index + 1) % handles.len(), - Direction::Prev if current_index == 0 => handles.len() - 1, - Direction::Prev => (current_index - 1) % handles.len(), - }; - let next_focus_handle = &handles[new_index]; - self.focus(next_focus_handle, window); - cx.stop_propagation(); - } - - fn next_history_query( - &mut self, - _: &NextHistoryQuery, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(new_query) = self - .search_history - .next(&mut self.search_history_cursor) - .map(str::to_string) - { - drop(self.search(&new_query, Some(self.search_options), false, window, cx)); - } else { - self.search_history_cursor.reset(); - drop(self.search("", Some(self.search_options), false, window, cx)); - } - } - - fn previous_history_query( - &mut self, - _: &PreviousHistoryQuery, - window: &mut Window, - cx: &mut Context, - ) { - if self.query(cx).is_empty() - && let Some(new_query) = self - .search_history - .current(&self.search_history_cursor) - .map(str::to_string) - { - drop(self.search(&new_query, Some(self.search_options), false, window, cx)); - return; - } - - if let Some(new_query) = self - .search_history - .previous(&mut self.search_history_cursor) - .map(str::to_string) - { - drop(self.search(&new_query, Some(self.search_options), false, window, cx)); - } - } - - fn focus(&self, handle: &gpui::FocusHandle, window: &mut Window) { - window.invalidate_character_coordinates(); - window.focus(handle); - } - - fn toggle_replace(&mut self, _: &ToggleReplace, window: &mut Window, cx: &mut Context) { - if self.active_searchable_item.is_some() { - self.replace_enabled = !self.replace_enabled; - let handle = if self.replace_enabled { - self.replacement_editor.focus_handle(cx) - } else { - self.query_editor.focus_handle(cx) - }; - self.focus(&handle, window); - cx.notify(); - } - } - - fn replace_next(&mut self, _: &ReplaceNext, window: &mut Window, cx: &mut Context) { - let mut should_propagate = true; - if !self.dismissed - && self.active_search.is_some() - && let Some(searchable_item) = self.active_searchable_item.as_ref() - && let Some(query) = self.active_search.as_ref() - && let Some(matches) = self - .searchable_items_with_matches - .get(&searchable_item.downgrade()) - { - if let Some(active_index) = self.active_match_index { - let query = query - .as_ref() - .clone() - .with_replacement(self.replacement(cx)); - searchable_item.replace(matches.at(active_index), &query, window, cx); - self.select_next_match(&SelectNextMatch, window, cx); - } - should_propagate = false; - } - if !should_propagate { - cx.stop_propagation(); - } - } - - pub fn replace_all(&mut self, _: &ReplaceAll, window: &mut Window, cx: &mut Context) { - if !self.dismissed - && self.active_search.is_some() - && let Some(searchable_item) = self.active_searchable_item.as_ref() - && let Some(query) = self.active_search.as_ref() - && let Some(matches) = self - .searchable_items_with_matches - .get(&searchable_item.downgrade()) - { - let query = query - .as_ref() - .clone() - .with_replacement(self.replacement(cx)); - searchable_item.replace_all(&mut matches.iter(), &query, window, cx); - } - } - - pub fn match_exists(&mut self, window: &mut Window, cx: &mut Context) -> bool { - self.update_match_index(window, cx); - self.active_match_index.is_some() - } - - pub fn should_use_smartcase_search(&mut self, cx: &mut Context) -> bool { - EditorSettings::get_global(cx).use_smartcase_search - } - - pub fn is_contains_uppercase(&mut self, str: &String) -> bool { - str.chars().any(|c| c.is_uppercase()) - } - - fn smartcase(&mut self, window: &mut Window, cx: &mut Context) { - if self.should_use_smartcase_search(cx) { - let query = self.query(cx); - if !query.is_empty() { - let is_case = self.is_contains_uppercase(&query); - if self.has_search_option(SearchOptions::CASE_SENSITIVE) != is_case { - self.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx); - } - } - } - } - - fn adjust_query_regex_language(&self, cx: &mut App) { - let enable = self.search_options.contains(SearchOptions::REGEX); - let query_buffer = self - .query_editor - .read(cx) - .buffer() - .read(cx) - .as_singleton() - .expect("query editor should be backed by a singleton buffer"); - - if enable { - if let Some(regex_language) = self.regex_language.clone() { - query_buffer.update(cx, |query_buffer, cx| { - query_buffer.set_language(Some(regex_language), cx); - }) - } - } else { - query_buffer.update(cx, |query_buffer, cx| { - query_buffer.set_language(None, cx); - }) - } - } - - /// Updates the searchable item's case sensitivity option to match the - /// search bar's current case sensitivity setting. This ensures that - /// editor's `select_next`/ `select_previous` operations respect the buffer - /// search bar's search options. - /// - /// Clears the case sensitivity when the search bar is dismissed so that - /// only the editor's settings are respected. - fn sync_select_next_case_sensitivity(&self, cx: &mut Context) { - let case_sensitive = match self.dismissed { - true => None, - false => Some(self.search_options.contains(SearchOptions::CASE_SENSITIVE)), - }; - - if let Some(active_searchable_item) = self.active_searchable_item.as_ref() { - active_searchable_item.set_search_is_case_sensitive(case_sensitive, cx); - } - } -} - -#[cfg(test)] -mod tests { - use std::ops::Range; - - use super::*; - use editor::{ - DisplayPoint, Editor, MultiBuffer, SearchSettings, SelectionEffects, - display_map::DisplayRow, test::editor_test_context::EditorTestContext, - }; - use gpui::{Hsla, TestAppContext, UpdateGlobal, VisualTestContext}; - use language::{Buffer, Point}; - use settings::{SearchSettingsContent, SettingsStore}; - use smol::stream::StreamExt as _; - use unindent::Unindent as _; - use util_macros::perf; - - fn init_globals(cx: &mut TestAppContext) { - cx.update(|cx| { - let store = settings::SettingsStore::test(cx); - cx.set_global(store); - editor::init(cx); - - theme::init(theme::LoadThemes::JustBase, cx); - crate::init(cx); - }); - } - - fn init_test( - cx: &mut TestAppContext, - ) -> ( - Entity, - Entity, - &mut VisualTestContext, - ) { - init_globals(cx); - let buffer = cx.new(|cx| { - Buffer::local( - r#" - A regular expression (shortened as regex or regexp;[1] also referred to as - rational expression[2][3]) is a sequence of characters that specifies a search - pattern in text. Usually such patterns are used by string-searching algorithms - for "find" or "find and replace" operations on strings, or for input validation. - "# - .unindent(), - cx, - ) - }); - let mut editor = None; - let window = cx.add_window(|window, cx| { - let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure( - "keymaps/default-macos.json", - cx, - ) - .unwrap(); - cx.bind_keys(default_key_bindings); - editor = Some(cx.new(|cx| Editor::for_buffer(buffer.clone(), None, window, cx))); - let mut search_bar = BufferSearchBar::new(None, window, cx); - search_bar.set_active_pane_item(Some(&editor.clone().unwrap()), window, cx); - search_bar.show(window, cx); - search_bar - }); - let search_bar = window.root(cx).unwrap(); - - let cx = VisualTestContext::from_window(*window, cx).into_mut(); - - (editor.unwrap(), search_bar, cx) - } - - #[perf] - #[gpui::test] - async fn test_search_simple(cx: &mut TestAppContext) { - let (editor, search_bar, cx) = init_test(cx); - let display_points_of = |background_highlights: Vec<(Range, Hsla)>| { - background_highlights - .into_iter() - .map(|(range, _)| range) - .collect::>() - }; - // Search for a string that appears with different casing. - // By default, search is case-insensitive. - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search("us", None, true, window, cx) - }) - .await - .unwrap(); - editor.update_in(cx, |editor, window, cx| { - assert_eq!( - display_points_of(editor.all_text_background_highlights(window, cx)), - &[ - DisplayPoint::new(DisplayRow(2), 17)..DisplayPoint::new(DisplayRow(2), 19), - DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45), - ] - ); - }); - - // Switch to a case sensitive search. - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx); - }); - let mut editor_notifications = cx.notifications(&editor); - editor_notifications.next().await; - editor.update_in(cx, |editor, window, cx| { - assert_eq!( - display_points_of(editor.all_text_background_highlights(window, cx)), - &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),] - ); - }); - - // Search for a string that appears both as a whole word and - // within other words. By default, all results are found. - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search("or", None, true, window, cx) - }) - .await - .unwrap(); - editor.update_in(cx, |editor, window, cx| { - assert_eq!( - display_points_of(editor.all_text_background_highlights(window, cx)), - &[ - DisplayPoint::new(DisplayRow(0), 24)..DisplayPoint::new(DisplayRow(0), 26), - DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43), - DisplayPoint::new(DisplayRow(2), 71)..DisplayPoint::new(DisplayRow(2), 73), - DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 3), - DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13), - DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58), - DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 62), - ] - ); - }); - - // Switch to a whole word search. - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx); - }); - let mut editor_notifications = cx.notifications(&editor); - editor_notifications.next().await; - editor.update_in(cx, |editor, window, cx| { - assert_eq!( - display_points_of(editor.all_text_background_highlights(window, cx)), - &[ - DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43), - DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13), - DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58), - ] - ); - }); - - editor.update_in(cx, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0) - ]) - }); - }); - search_bar.update_in(cx, |search_bar, window, cx| { - assert_eq!(search_bar.active_match_index, Some(0)); - search_bar.select_next_match(&SelectNextMatch, window, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor - .selections - .display_ranges(&editor.display_snapshot(cx))), - [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)] - ); - }); - search_bar.read_with(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(0)); - }); - - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.select_next_match(&SelectNextMatch, window, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor - .selections - .display_ranges(&editor.display_snapshot(cx))), - [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)] - ); - }); - search_bar.read_with(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(1)); - }); - - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.select_next_match(&SelectNextMatch, window, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor - .selections - .display_ranges(&editor.display_snapshot(cx))), - [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)] - ); - }); - search_bar.read_with(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(2)); - }); - - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.select_next_match(&SelectNextMatch, window, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor - .selections - .display_ranges(&editor.display_snapshot(cx))), - [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)] - ); - }); - search_bar.read_with(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(0)); - }); - - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.select_prev_match(&SelectPreviousMatch, window, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor - .selections - .display_ranges(&editor.display_snapshot(cx))), - [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)] - ); - }); - search_bar.read_with(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(2)); - }); - - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.select_prev_match(&SelectPreviousMatch, window, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor - .selections - .display_ranges(&editor.display_snapshot(cx))), - [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)] - ); - }); - search_bar.read_with(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(1)); - }); - - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.select_prev_match(&SelectPreviousMatch, window, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor - .selections - .display_ranges(&editor.display_snapshot(cx))), - [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)] - ); - }); - search_bar.read_with(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(0)); - }); - - // Park the cursor in between matches and ensure that going to the previous match selects - // the closest match to the left. - editor.update_in(cx, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0) - ]) - }); - }); - search_bar.update_in(cx, |search_bar, window, cx| { - assert_eq!(search_bar.active_match_index, Some(1)); - search_bar.select_prev_match(&SelectPreviousMatch, window, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor - .selections - .display_ranges(&editor.display_snapshot(cx))), - [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)] - ); - }); - search_bar.read_with(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(0)); - }); - - // Park the cursor in between matches and ensure that going to the next match selects the - // closest match to the right. - editor.update_in(cx, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0) - ]) - }); - }); - search_bar.update_in(cx, |search_bar, window, cx| { - assert_eq!(search_bar.active_match_index, Some(1)); - search_bar.select_next_match(&SelectNextMatch, window, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor - .selections - .display_ranges(&editor.display_snapshot(cx))), - [DisplayPoint::new(DisplayRow(3), 11)..DisplayPoint::new(DisplayRow(3), 13)] - ); - }); - search_bar.read_with(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(1)); - }); - - // Park the cursor after the last match and ensure that going to the previous match selects - // the last match. - editor.update_in(cx, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60) - ]) - }); - }); - search_bar.update_in(cx, |search_bar, window, cx| { - assert_eq!(search_bar.active_match_index, Some(2)); - search_bar.select_prev_match(&SelectPreviousMatch, window, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor - .selections - .display_ranges(&editor.display_snapshot(cx))), - [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)] - ); - }); - search_bar.read_with(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(2)); - }); - - // Park the cursor after the last match and ensure that going to the next match selects the - // first match. - editor.update_in(cx, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(DisplayRow(3), 60)..DisplayPoint::new(DisplayRow(3), 60) - ]) - }); - }); - search_bar.update_in(cx, |search_bar, window, cx| { - assert_eq!(search_bar.active_match_index, Some(2)); - search_bar.select_next_match(&SelectNextMatch, window, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor - .selections - .display_ranges(&editor.display_snapshot(cx))), - [DisplayPoint::new(DisplayRow(0), 41)..DisplayPoint::new(DisplayRow(0), 43)] - ); - }); - search_bar.read_with(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(0)); - }); - - // Park the cursor before the first match and ensure that going to the previous match - // selects the last match. - editor.update_in(cx, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_display_ranges([ - DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0) - ]) - }); - }); - search_bar.update_in(cx, |search_bar, window, cx| { - assert_eq!(search_bar.active_match_index, Some(0)); - search_bar.select_prev_match(&SelectPreviousMatch, window, cx); - assert_eq!( - editor.update(cx, |editor, cx| editor - .selections - .display_ranges(&editor.display_snapshot(cx))), - [DisplayPoint::new(DisplayRow(3), 56)..DisplayPoint::new(DisplayRow(3), 58)] - ); - }); - search_bar.read_with(cx, |search_bar, _| { - assert_eq!(search_bar.active_match_index, Some(2)); - }); - } - - fn display_points_of( - background_highlights: Vec<(Range, Hsla)>, - ) -> Vec> { - background_highlights - .into_iter() - .map(|(range, _)| range) - .collect::>() - } - - #[perf] - #[gpui::test] - async fn test_search_option_handling(cx: &mut TestAppContext) { - let (editor, search_bar, cx) = init_test(cx); - - // show with options should make current search case sensitive - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.show(window, cx); - search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), true, window, cx) - }) - .await - .unwrap(); - editor.update_in(cx, |editor, window, cx| { - assert_eq!( - display_points_of(editor.all_text_background_highlights(window, cx)), - &[DisplayPoint::new(DisplayRow(2), 43)..DisplayPoint::new(DisplayRow(2), 45),] - ); - }); - - // search_suggested should restore default options - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.search_suggested(window, cx); - assert_eq!(search_bar.search_options, SearchOptions::NONE) - }); - - // toggling a search option should update the defaults - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search( - "regex", - Some(SearchOptions::CASE_SENSITIVE), - true, - window, - cx, - ) - }) - .await - .unwrap(); - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx) - }); - let mut editor_notifications = cx.notifications(&editor); - editor_notifications.next().await; - editor.update_in(cx, |editor, window, cx| { - assert_eq!( - display_points_of(editor.all_text_background_highlights(window, cx)), - &[DisplayPoint::new(DisplayRow(0), 35)..DisplayPoint::new(DisplayRow(0), 40),] - ); - }); - - // defaults should still include whole word - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.search_suggested(window, cx); - assert_eq!( - search_bar.search_options, - SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD - ) - }); - } - - #[perf] - #[gpui::test] - async fn test_search_select_all_matches(cx: &mut TestAppContext) { - init_globals(cx); - let buffer_text = r#" - A regular expression (shortened as regex or regexp;[1] also referred to as - rational expression[2][3]) is a sequence of characters that specifies a search - pattern in text. Usually such patterns are used by string-searching algorithms - for "find" or "find and replace" operations on strings, or for input validation. - "# - .unindent(); - let expected_query_matches_count = buffer_text - .chars() - .filter(|c| c.eq_ignore_ascii_case(&'a')) - .count(); - assert!( - expected_query_matches_count > 1, - "Should pick a query with multiple results" - ); - let buffer = cx.new(|cx| Buffer::local(buffer_text, cx)); - let window = cx.add_window(|_, _| gpui::Empty); - - let editor = window.build_entity(cx, |window, cx| { - Editor::for_buffer(buffer.clone(), None, window, cx) - }); - - let search_bar = window.build_entity(cx, |window, cx| { - let mut search_bar = BufferSearchBar::new(None, window, cx); - search_bar.set_active_pane_item(Some(&editor), window, cx); - search_bar.show(window, cx); - search_bar - }); - - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.search("a", None, true, window, cx) - }) - }) - .unwrap() - .await - .unwrap(); - let initial_selections = window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - let handle = search_bar.query_editor.focus_handle(cx); - window.focus(&handle); - search_bar.activate_current_match(window, cx); - }); - assert!( - !editor.read(cx).is_focused(window), - "Initially, the editor should not be focused" - ); - let initial_selections = editor.update(cx, |editor, cx| { - let initial_selections = editor.selections.display_ranges(&editor.display_snapshot(cx)); - assert_eq!( - initial_selections.len(), 1, - "Expected to have only one selection before adding carets to all matches, but got: {initial_selections:?}", - ); - initial_selections - }); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.active_match_index, Some(0)); - let handle = search_bar.query_editor.focus_handle(cx); - window.focus(&handle); - search_bar.select_all_matches(&SelectAllMatches, window, cx); - }); - assert!( - editor.read(cx).is_focused(window), - "Should focus editor after successful SelectAllMatches" - ); - search_bar.update(cx, |search_bar, cx| { - let all_selections = - editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx))); - assert_eq!( - all_selections.len(), - expected_query_matches_count, - "Should select all `a` characters in the buffer, but got: {all_selections:?}" - ); - assert_eq!( - search_bar.active_match_index, - Some(0), - "Match index should not change after selecting all matches" - ); - }); - - search_bar.update(cx, |this, cx| this.select_next_match(&SelectNextMatch, window, cx)); - initial_selections - }).unwrap(); - - window - .update(cx, |_, window, cx| { - assert!( - editor.read(cx).is_focused(window), - "Should still have editor focused after SelectNextMatch" - ); - search_bar.update(cx, |search_bar, cx| { - let all_selections = editor.update(cx, |editor, cx| { - editor - .selections - .display_ranges(&editor.display_snapshot(cx)) - }); - assert_eq!( - all_selections.len(), - 1, - "On next match, should deselect items and select the next match" - ); - assert_ne!( - all_selections, initial_selections, - "Next match should be different from the first selection" - ); - assert_eq!( - search_bar.active_match_index, - Some(1), - "Match index should be updated to the next one" - ); - let handle = search_bar.query_editor.focus_handle(cx); - window.focus(&handle); - search_bar.select_all_matches(&SelectAllMatches, window, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, window, cx| { - assert!( - editor.read(cx).is_focused(window), - "Should focus editor after successful SelectAllMatches" - ); - search_bar.update(cx, |search_bar, cx| { - let all_selections = - editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx))); - assert_eq!( - all_selections.len(), - expected_query_matches_count, - "Should select all `a` characters in the buffer, but got: {all_selections:?}" - ); - assert_eq!( - search_bar.active_match_index, - Some(1), - "Match index should not change after selecting all matches" - ); - }); - search_bar.update(cx, |search_bar, cx| { - search_bar.select_prev_match(&SelectPreviousMatch, window, cx); - }); - }) - .unwrap(); - let last_match_selections = window - .update(cx, |_, window, cx| { - assert!( - editor.read(cx).is_focused(window), - "Should still have editor focused after SelectPreviousMatch" - ); - - search_bar.update(cx, |search_bar, cx| { - let all_selections = editor.update(cx, |editor, cx| { - editor - .selections - .display_ranges(&editor.display_snapshot(cx)) - }); - assert_eq!( - all_selections.len(), - 1, - "On previous match, should deselect items and select the previous item" - ); - assert_eq!( - all_selections, initial_selections, - "Previous match should be the same as the first selection" - ); - assert_eq!( - search_bar.active_match_index, - Some(0), - "Match index should be updated to the previous one" - ); - all_selections - }) - }) - .unwrap(); - - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - let handle = search_bar.query_editor.focus_handle(cx); - window.focus(&handle); - search_bar.search("abas_nonexistent_match", None, true, window, cx) - }) - }) - .unwrap() - .await - .unwrap(); - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.select_all_matches(&SelectAllMatches, window, cx); - }); - assert!( - editor.update(cx, |this, _cx| !this.is_focused(window)), - "Should not switch focus to editor if SelectAllMatches does not find any matches" - ); - search_bar.update(cx, |search_bar, cx| { - let all_selections = - editor.update(cx, |editor, cx| editor.selections.display_ranges(&editor.display_snapshot(cx))); - assert_eq!( - all_selections, last_match_selections, - "Should not select anything new if there are no matches" - ); - assert!( - search_bar.active_match_index.is_none(), - "For no matches, there should be no active match index" - ); - }); - }) - .unwrap(); - } - - #[perf] - #[gpui::test] - async fn test_search_query_with_match_whole_word(cx: &mut TestAppContext) { - init_globals(cx); - let buffer_text = r#" - self.buffer.update(cx, |buffer, cx| { - buffer.edit( - edits, - Some(AutoindentMode::Block { - original_indent_columns, - }), - cx, - ) - }); - - this.buffer.update(cx, |buffer, cx| { - buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx) - }); - "# - .unindent(); - let buffer = cx.new(|cx| Buffer::local(buffer_text, cx)); - let cx = cx.add_empty_window(); - - let editor = - cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx)); - - let search_bar = cx.new_window_entity(|window, cx| { - let mut search_bar = BufferSearchBar::new(None, window, cx); - search_bar.set_active_pane_item(Some(&editor), window, cx); - search_bar.show(window, cx); - search_bar - }); - - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search( - "edit\\(", - Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX), - true, - window, - cx, - ) - }) - .await - .unwrap(); - - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.select_all_matches(&SelectAllMatches, window, cx); - }); - search_bar.update(cx, |_, cx| { - let all_selections = editor.update(cx, |editor, cx| { - editor - .selections - .display_ranges(&editor.display_snapshot(cx)) - }); - assert_eq!( - all_selections.len(), - 2, - "Should select all `edit(` in the buffer, but got: {all_selections:?}" - ); - }); - - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search( - "edit(", - Some(SearchOptions::WHOLE_WORD | SearchOptions::CASE_SENSITIVE), - true, - window, - cx, - ) - }) - .await - .unwrap(); - - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.select_all_matches(&SelectAllMatches, window, cx); - }); - search_bar.update(cx, |_, cx| { - let all_selections = editor.update(cx, |editor, cx| { - editor - .selections - .display_ranges(&editor.display_snapshot(cx)) - }); - assert_eq!( - all_selections.len(), - 2, - "Should select all `edit(` in the buffer, but got: {all_selections:?}" - ); - }); - } - - #[perf] - #[gpui::test] - async fn test_search_query_history(cx: &mut TestAppContext) { - let (_editor, search_bar, cx) = init_test(cx); - - // Add 3 search items into the history. - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search("a", None, true, window, cx) - }) - .await - .unwrap(); - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search("b", None, true, window, cx) - }) - .await - .unwrap(); - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), true, window, cx) - }) - .await - .unwrap(); - // Ensure that the latest search is active. - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "c"); - assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); - }); - - // Next history query after the latest should set the query to the empty string. - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.next_history_query(&NextHistoryQuery, window, cx); - }); - cx.background_executor.run_until_parked(); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), ""); - assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); - }); - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.next_history_query(&NextHistoryQuery, window, cx); - }); - cx.background_executor.run_until_parked(); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), ""); - assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); - }); - - // First previous query for empty current query should set the query to the latest. - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, window, cx); - }); - cx.background_executor.run_until_parked(); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "c"); - assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); - }); - - // Further previous items should go over the history in reverse order. - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, window, cx); - }); - cx.background_executor.run_until_parked(); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "b"); - assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); - }); - - // Previous items should never go behind the first history item. - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, window, cx); - }); - cx.background_executor.run_until_parked(); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "a"); - assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); - }); - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, window, cx); - }); - cx.background_executor.run_until_parked(); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "a"); - assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); - }); - - // Next items should go over the history in the original order. - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.next_history_query(&NextHistoryQuery, window, cx); - }); - cx.background_executor.run_until_parked(); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "b"); - assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); - }); - - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search("ba", None, true, window, cx) - }) - .await - .unwrap(); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "ba"); - assert_eq!(search_bar.search_options, SearchOptions::NONE); - }); - - // New search input should add another entry to history and move the selection to the end of the history. - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, window, cx); - }); - cx.background_executor.run_until_parked(); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "c"); - assert_eq!(search_bar.search_options, SearchOptions::NONE); - }); - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.previous_history_query(&PreviousHistoryQuery, window, cx); - }); - cx.background_executor.run_until_parked(); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "b"); - assert_eq!(search_bar.search_options, SearchOptions::NONE); - }); - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.next_history_query(&NextHistoryQuery, window, cx); - }); - cx.background_executor.run_until_parked(); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "c"); - assert_eq!(search_bar.search_options, SearchOptions::NONE); - }); - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.next_history_query(&NextHistoryQuery, window, cx); - }); - cx.background_executor.run_until_parked(); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), "ba"); - assert_eq!(search_bar.search_options, SearchOptions::NONE); - }); - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.next_history_query(&NextHistoryQuery, window, cx); - }); - cx.background_executor.run_until_parked(); - search_bar.update(cx, |search_bar, cx| { - assert_eq!(search_bar.query(cx), ""); - assert_eq!(search_bar.search_options, SearchOptions::NONE); - }); - } - - #[perf] - #[gpui::test] - async fn test_replace_simple(cx: &mut TestAppContext) { - let (editor, search_bar, cx) = init_test(cx); - - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search("expression", None, true, window, cx) - }) - .await - .unwrap(); - - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.replacement_editor.update(cx, |editor, cx| { - // We use $1 here as initially we should be in Text mode, where `$1` should be treated literally. - editor.set_text("expr$1", window, cx); - }); - search_bar.replace_all(&ReplaceAll, window, cx) - }); - assert_eq!( - editor.read_with(cx, |this, cx| { this.text(cx) }), - r#" - A regular expr$1 (shortened as regex or regexp;[1] also referred to as - rational expr$1[2][3]) is a sequence of characters that specifies a search - pattern in text. Usually such patterns are used by string-searching algorithms - for "find" or "find and replace" operations on strings, or for input validation. - "# - .unindent() - ); - - // Search for word boundaries and replace just a single one. - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search("or", Some(SearchOptions::WHOLE_WORD), true, window, cx) - }) - .await - .unwrap(); - - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.replacement_editor.update(cx, |editor, cx| { - editor.set_text("banana", window, cx); - }); - search_bar.replace_next(&ReplaceNext, window, cx) - }); - // Notice how the first or in the text (shORtened) is not replaced. Neither are the remaining hits of `or` in the text. - assert_eq!( - editor.read_with(cx, |this, cx| { this.text(cx) }), - r#" - A regular expr$1 (shortened as regex banana regexp;[1] also referred to as - rational expr$1[2][3]) is a sequence of characters that specifies a search - pattern in text. Usually such patterns are used by string-searching algorithms - for "find" or "find and replace" operations on strings, or for input validation. - "# - .unindent() - ); - // Let's turn on regex mode. - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search( - "\\[([^\\]]+)\\]", - Some(SearchOptions::REGEX), - true, - window, - cx, - ) - }) - .await - .unwrap(); - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.replacement_editor.update(cx, |editor, cx| { - editor.set_text("${1}number", window, cx); - }); - search_bar.replace_all(&ReplaceAll, window, cx) - }); - assert_eq!( - editor.read_with(cx, |this, cx| { this.text(cx) }), - r#" - A regular expr$1 (shortened as regex banana regexp;1number also referred to as - rational expr$12number3number) is a sequence of characters that specifies a search - pattern in text. Usually such patterns are used by string-searching algorithms - for "find" or "find and replace" operations on strings, or for input validation. - "# - .unindent() - ); - // Now with a whole-word twist. - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search( - "a\\w+s", - Some(SearchOptions::REGEX | SearchOptions::WHOLE_WORD), - true, - window, - cx, - ) - }) - .await - .unwrap(); - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.replacement_editor.update(cx, |editor, cx| { - editor.set_text("things", window, cx); - }); - search_bar.replace_all(&ReplaceAll, window, cx) - }); - // The only word affected by this edit should be `algorithms`, even though there's a bunch - // of words in this text that would match this regex if not for WHOLE_WORD. - assert_eq!( - editor.read_with(cx, |this, cx| { this.text(cx) }), - r#" - A regular expr$1 (shortened as regex banana regexp;1number also referred to as - rational expr$12number3number) is a sequence of characters that specifies a search - pattern in text. Usually such patterns are used by string-searching things - for "find" or "find and replace" operations on strings, or for input validation. - "# - .unindent() - ); - } - - #[gpui::test] - async fn test_replace_focus(cx: &mut TestAppContext) { - let (editor, search_bar, cx) = init_test(cx); - - editor.update_in(cx, |editor, window, cx| { - editor.set_text("What a bad day!", window, cx) - }); - - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search("bad", None, true, window, cx) - }) - .await - .unwrap(); - - // Calling `toggle_replace` in the search bar ensures that the "Replace - // *" buttons are rendered, so we can then simulate clicking the - // buttons. - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.toggle_replace(&ToggleReplace, window, cx) - }); - - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.replacement_editor.update(cx, |editor, cx| { - editor.set_text("great", window, cx); - }); - }); - - // Focus on the editor instead of the search bar, as we want to ensure - // that pressing the "Replace Next Match" button will work, even if the - // search bar is not focused. - cx.focus(&editor); - - // We'll not simulate clicking the "Replace Next Match " button, asserting that - // the replacement was done. - let button_bounds = cx - .debug_bounds("ICON-ReplaceNext") - .expect("'Replace Next Match' button should be visible"); - cx.simulate_click(button_bounds.center(), gpui::Modifiers::none()); - - assert_eq!( - editor.read_with(cx, |editor, cx| editor.text(cx)), - "What a great day!" - ); - } - - struct ReplacementTestParams<'a> { - editor: &'a Entity, - search_bar: &'a Entity, - cx: &'a mut VisualTestContext, - search_text: &'static str, - search_options: Option, - replacement_text: &'static str, - replace_all: bool, - expected_text: String, - } - - async fn run_replacement_test(options: ReplacementTestParams<'_>) { - options - .search_bar - .update_in(options.cx, |search_bar, window, cx| { - if let Some(options) = options.search_options { - search_bar.set_search_options(options, cx); - } - search_bar.search( - options.search_text, - options.search_options, - true, - window, - cx, - ) - }) - .await - .unwrap(); - - options - .search_bar - .update_in(options.cx, |search_bar, window, cx| { - search_bar.replacement_editor.update(cx, |editor, cx| { - editor.set_text(options.replacement_text, window, cx); - }); - - if options.replace_all { - search_bar.replace_all(&ReplaceAll, window, cx) - } else { - search_bar.replace_next(&ReplaceNext, window, cx) - } - }); - - assert_eq!( - options - .editor - .read_with(options.cx, |this, cx| { this.text(cx) }), - options.expected_text - ); - } - - #[perf] - #[gpui::test] - async fn test_replace_special_characters(cx: &mut TestAppContext) { - let (editor, search_bar, cx) = init_test(cx); - - run_replacement_test(ReplacementTestParams { - editor: &editor, - search_bar: &search_bar, - cx, - search_text: "expression", - search_options: None, - replacement_text: r"\n", - replace_all: true, - expected_text: r#" - A regular \n (shortened as regex or regexp;[1] also referred to as - rational \n[2][3]) is a sequence of characters that specifies a search - pattern in text. Usually such patterns are used by string-searching algorithms - for "find" or "find and replace" operations on strings, or for input validation. - "# - .unindent(), - }) - .await; - - run_replacement_test(ReplacementTestParams { - editor: &editor, - search_bar: &search_bar, - cx, - search_text: "or", - search_options: Some(SearchOptions::WHOLE_WORD | SearchOptions::REGEX), - replacement_text: r"\\\n\\\\", - replace_all: false, - expected_text: r#" - A regular \n (shortened as regex \ - \\ regexp;[1] also referred to as - rational \n[2][3]) is a sequence of characters that specifies a search - pattern in text. Usually such patterns are used by string-searching algorithms - for "find" or "find and replace" operations on strings, or for input validation. - "# - .unindent(), - }) - .await; - - run_replacement_test(ReplacementTestParams { - editor: &editor, - search_bar: &search_bar, - cx, - search_text: r"(that|used) ", - search_options: Some(SearchOptions::REGEX), - replacement_text: r"$1\n", - replace_all: true, - expected_text: r#" - A regular \n (shortened as regex \ - \\ regexp;[1] also referred to as - rational \n[2][3]) is a sequence of characters that - specifies a search - pattern in text. Usually such patterns are used - by string-searching algorithms - for "find" or "find and replace" operations on strings, or for input validation. - "# - .unindent(), - }) - .await; - } - - #[perf] - #[gpui::test] - async fn test_find_matches_in_selections_singleton_buffer_multiple_selections( - cx: &mut TestAppContext, - ) { - init_globals(cx); - let buffer = cx.new(|cx| { - Buffer::local( - r#" - aaa bbb aaa ccc - aaa bbb aaa ccc - aaa bbb aaa ccc - aaa bbb aaa ccc - aaa bbb aaa ccc - aaa bbb aaa ccc - "# - .unindent(), - cx, - ) - }); - let cx = cx.add_empty_window(); - let editor = - cx.new_window_entity(|window, cx| Editor::for_buffer(buffer.clone(), None, window, cx)); - - let search_bar = cx.new_window_entity(|window, cx| { - let mut search_bar = BufferSearchBar::new(None, window, cx); - search_bar.set_active_pane_item(Some(&editor), window, cx); - search_bar.show(window, cx); - search_bar - }); - - editor.update_in(cx, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges(vec![Point::new(1, 0)..Point::new(2, 4)]) - }) - }); - - search_bar.update_in(cx, |search_bar, window, cx| { - let deploy = Deploy { - focus: true, - replace_enabled: false, - selection_search_enabled: true, - }; - search_bar.deploy(&deploy, window, cx); - }); - - cx.run_until_parked(); - - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search("aaa", None, true, window, cx) - }) - .await - .unwrap(); - - editor.update(cx, |editor, cx| { - assert_eq!( - editor.search_background_highlights(cx), - &[ - Point::new(1, 0)..Point::new(1, 3), - Point::new(1, 8)..Point::new(1, 11), - Point::new(2, 0)..Point::new(2, 3), - ] - ); - }); - } - - #[perf] - #[gpui::test] - async fn test_find_matches_in_selections_multiple_excerpts_buffer_multiple_selections( - cx: &mut TestAppContext, - ) { - init_globals(cx); - let text = r#" - aaa bbb aaa ccc - aaa bbb aaa ccc - aaa bbb aaa ccc - aaa bbb aaa ccc - aaa bbb aaa ccc - aaa bbb aaa ccc - - aaa bbb aaa ccc - aaa bbb aaa ccc - aaa bbb aaa ccc - aaa bbb aaa ccc - aaa bbb aaa ccc - aaa bbb aaa ccc - "# - .unindent(); - - let cx = cx.add_empty_window(); - let editor = cx.new_window_entity(|window, cx| { - let multibuffer = MultiBuffer::build_multi( - [ - ( - &text, - vec![ - Point::new(0, 0)..Point::new(2, 0), - Point::new(4, 0)..Point::new(5, 0), - ], - ), - (&text, vec![Point::new(9, 0)..Point::new(11, 0)]), - ], - cx, - ); - Editor::for_multibuffer(multibuffer, None, window, cx) - }); - - let search_bar = cx.new_window_entity(|window, cx| { - let mut search_bar = BufferSearchBar::new(None, window, cx); - search_bar.set_active_pane_item(Some(&editor), window, cx); - search_bar.show(window, cx); - search_bar - }); - - editor.update_in(cx, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges(vec![ - Point::new(1, 0)..Point::new(1, 4), - Point::new(5, 3)..Point::new(6, 4), - ]) - }) - }); - - search_bar.update_in(cx, |search_bar, window, cx| { - let deploy = Deploy { - focus: true, - replace_enabled: false, - selection_search_enabled: true, - }; - search_bar.deploy(&deploy, window, cx); - }); - - cx.run_until_parked(); - - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search("aaa", None, true, window, cx) - }) - .await - .unwrap(); - - editor.update(cx, |editor, cx| { - assert_eq!( - editor.search_background_highlights(cx), - &[ - Point::new(1, 0)..Point::new(1, 3), - Point::new(5, 8)..Point::new(5, 11), - Point::new(6, 0)..Point::new(6, 3), - ] - ); - }); - } - - #[perf] - #[gpui::test] - async fn test_invalid_regexp_search_after_valid(cx: &mut TestAppContext) { - let (editor, search_bar, cx) = init_test(cx); - // Search using valid regexp - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.enable_search_option(SearchOptions::REGEX, window, cx); - search_bar.search("expression", None, true, window, cx) - }) - .await - .unwrap(); - editor.update_in(cx, |editor, window, cx| { - assert_eq!( - display_points_of(editor.all_text_background_highlights(window, cx)), - &[ - DisplayPoint::new(DisplayRow(0), 10)..DisplayPoint::new(DisplayRow(0), 20), - DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 19), - ], - ); - }); - - // Now, the expression is invalid - search_bar - .update_in(cx, |search_bar, window, cx| { - search_bar.search("expression (", None, true, window, cx) - }) - .await - .unwrap_err(); - editor.update_in(cx, |editor, window, cx| { - assert!( - display_points_of(editor.all_text_background_highlights(window, cx)).is_empty(), - ); - }); - } - - #[perf] - #[gpui::test] - async fn test_search_options_changes(cx: &mut TestAppContext) { - let (_editor, search_bar, cx) = init_test(cx); - update_search_settings( - SearchSettings { - button: true, - whole_word: false, - case_sensitive: false, - include_ignored: false, - regex: false, - center_on_match: false, - }, - cx, - ); - - let deploy = Deploy { - focus: true, - replace_enabled: false, - selection_search_enabled: true, - }; - - search_bar.update_in(cx, |search_bar, window, cx| { - assert_eq!( - search_bar.search_options, - SearchOptions::NONE, - "Should have no search options enabled by default" - ); - search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx); - assert_eq!( - search_bar.search_options, - SearchOptions::WHOLE_WORD, - "Should enable the option toggled" - ); - assert!( - !search_bar.dismissed, - "Search bar should be present and visible" - ); - search_bar.deploy(&deploy, window, cx); - assert_eq!( - search_bar.search_options, - SearchOptions::WHOLE_WORD, - "After (re)deploying, the option should still be enabled" - ); - - search_bar.dismiss(&Dismiss, window, cx); - search_bar.deploy(&deploy, window, cx); - assert_eq!( - search_bar.search_options, - SearchOptions::WHOLE_WORD, - "After hiding and showing the search bar, search options should be preserved" - ); - - search_bar.toggle_search_option(SearchOptions::REGEX, window, cx); - search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx); - assert_eq!( - search_bar.search_options, - SearchOptions::REGEX, - "Should enable the options toggled" - ); - assert!( - !search_bar.dismissed, - "Search bar should be present and visible" - ); - search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx); - }); - - update_search_settings( - SearchSettings { - button: true, - whole_word: false, - case_sensitive: true, - include_ignored: false, - regex: false, - center_on_match: false, - }, - cx, - ); - search_bar.update_in(cx, |search_bar, window, cx| { - assert_eq!( - search_bar.search_options, - SearchOptions::REGEX | SearchOptions::WHOLE_WORD, - "Should have no search options enabled by default" - ); - - search_bar.deploy(&deploy, window, cx); - assert_eq!( - search_bar.search_options, - SearchOptions::REGEX | SearchOptions::WHOLE_WORD, - "Toggling a non-dismissed search bar with custom options should not change the default options" - ); - search_bar.dismiss(&Dismiss, window, cx); - search_bar.deploy(&deploy, window, cx); - assert_eq!( - search_bar.configured_options, - SearchOptions::CASE_SENSITIVE, - "After a settings update and toggling the search bar, configured options should be updated" - ); - assert_eq!( - search_bar.search_options, - SearchOptions::CASE_SENSITIVE, - "After a settings update and toggling the search bar, configured options should be used" - ); - }); - - update_search_settings( - SearchSettings { - button: true, - whole_word: true, - case_sensitive: true, - include_ignored: false, - regex: false, - center_on_match: false, - }, - cx, - ); - - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.deploy(&deploy, window, cx); - search_bar.dismiss(&Dismiss, window, cx); - search_bar.show(window, cx); - assert_eq!( - search_bar.search_options, - SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD, - "Calling deploy on an already deployed search bar should not prevent settings updates from being detected" - ); - }); - } - - #[gpui::test] - async fn test_select_occurrence_case_sensitivity(cx: &mut TestAppContext) { - let (editor, search_bar, cx) = init_test(cx); - let mut editor_cx = EditorTestContext::for_editor_in(editor, cx).await; - - // Start with case sensitive search settings. - let mut search_settings = SearchSettings::default(); - search_settings.case_sensitive = true; - update_search_settings(search_settings, cx); - search_bar.update(cx, |search_bar, cx| { - let mut search_options = search_bar.search_options; - search_options.insert(SearchOptions::CASE_SENSITIVE); - search_bar.set_search_options(search_options, cx); - }); - - editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo"); - editor_cx.update_editor(|e, window, cx| { - e.select_next(&Default::default(), window, cx).unwrap(); - }); - editor_cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»"); - - // Update the search bar's case sensitivite toggle, so we can later - // confirm that `select_next` will now be case-insensitive. - editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo"); - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.toggle_case_sensitive(&Default::default(), window, cx); - }); - editor_cx.update_editor(|e, window, cx| { - e.select_next(&Default::default(), window, cx).unwrap(); - }); - editor_cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\nFoo\nfoo"); - - // Confirm that, after dismissing the search bar, only the editor's - // search settings actually affect the behavior of `select_next`. - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.dismiss(&Default::default(), window, cx); - }); - editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo"); - editor_cx.update_editor(|e, window, cx| { - e.select_next(&Default::default(), window, cx).unwrap(); - }); - editor_cx.assert_editor_state("«ˇfoo»\nFOO\nFoo\n«ˇfoo»"); - - // Update the editor's search settings, disabling case sensitivity, to - // check that the value is respected. - let mut search_settings = SearchSettings::default(); - search_settings.case_sensitive = false; - update_search_settings(search_settings, cx); - editor_cx.set_state("«ˇfoo»\nFOO\nFoo\nfoo"); - editor_cx.update_editor(|e, window, cx| { - e.select_next(&Default::default(), window, cx).unwrap(); - }); - editor_cx.assert_editor_state("«ˇfoo»\n«ˇFOO»\nFoo\nfoo"); - } - - fn update_search_settings(search_settings: SearchSettings, cx: &mut TestAppContext) { - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings.editor.search = Some(SearchSettingsContent { - button: Some(search_settings.button), - whole_word: Some(search_settings.whole_word), - case_sensitive: Some(search_settings.case_sensitive), - include_ignored: Some(search_settings.include_ignored), - regex: Some(search_settings.regex), - center_on_match: Some(search_settings.center_on_match), - }); - }); - }); - }); - } -} diff --git a/crates/search/src/buffer_search/registrar.rs b/crates/search/src/buffer_search/registrar.rs deleted file mode 100644 index 2c640e67ce..0000000000 --- a/crates/search/src/buffer_search/registrar.rs +++ /dev/null @@ -1,176 +0,0 @@ -use gpui::{Action, Context, Div, Entity, InteractiveElement, Window, div}; -use workspace::Workspace; - -use crate::BufferSearchBar; - -/// Registrar inverts the dependency between search and its downstream user, allowing said downstream user to register search action without knowing exactly what those actions are. -pub trait SearchActionsRegistrar { - fn register_handler(&mut self, callback: impl ActionExecutor); -} - -type SearchBarActionCallback = - fn(&mut BufferSearchBar, &A, &mut Window, &mut Context); - -type GetSearchBar = - for<'a, 'b> fn(&'a T, &'a mut Window, &mut Context<'b, T>) -> Option>; - -/// Registers search actions on a div that can be taken out. -pub struct DivRegistrar<'a, 'b, T: 'static> { - div: Option
, - cx: &'a mut Context<'b, T>, - search_getter: GetSearchBar, -} - -impl<'a, 'b, T: 'static> DivRegistrar<'a, 'b, T> { - pub fn new(search_getter: GetSearchBar, cx: &'a mut Context<'b, T>) -> Self { - Self { - div: Some(div()), - cx, - search_getter, - } - } - pub fn into_div(self) -> Div { - // This option is always Some; it's an option in the first place because we want to call methods - // on div that require ownership. - self.div.unwrap() - } -} - -impl SearchActionsRegistrar for DivRegistrar<'_, '_, T> { - fn register_handler(&mut self, callback: impl ActionExecutor) { - let getter = self.search_getter; - self.div = self.div.take().map(|div| { - div.on_action(self.cx.listener(move |this, action, window, cx| { - let should_notify = (getter)(this, window, cx) - .map(|search_bar| { - search_bar.update(cx, |search_bar, cx| { - callback.execute(search_bar, action, window, cx) - }) - }) - .unwrap_or(false); - if should_notify { - cx.notify(); - } else { - cx.propagate(); - } - })) - }); - } -} - -/// Register actions for an active pane. -impl SearchActionsRegistrar for Workspace { - fn register_handler(&mut self, callback: impl ActionExecutor) { - self.register_action(move |workspace, action: &A, window, cx| { - if workspace.has_active_modal(window, cx) && !workspace.hide_modal(window, cx) { - cx.propagate(); - return; - } - - let pane = workspace.active_pane(); - let callback = callback.clone(); - pane.update(cx, |this, cx| { - this.toolbar().update(cx, move |this, cx| { - if let Some(search_bar) = this.item_of_type::() { - let should_notify = search_bar.update(cx, move |search_bar, cx| { - callback.execute(search_bar, action, window, cx) - }); - if should_notify { - cx.notify(); - } else { - cx.propagate(); - } - } - }) - }); - }); - } -} - -type DidHandleAction = bool; -/// Potentially executes the underlying action if some preconditions are met (e.g. buffer search bar is visible) -pub trait ActionExecutor: 'static + Clone { - fn execute( - &self, - search_bar: &mut BufferSearchBar, - action: &A, - window: &mut Window, - cx: &mut Context, - ) -> DidHandleAction; -} - -/// Run an action when the search bar has been dismissed from the panel. -pub struct ForDismissed(pub(super) SearchBarActionCallback); -impl Clone for ForDismissed { - fn clone(&self) -> Self { - Self(self.0) - } -} - -impl ActionExecutor for ForDismissed { - fn execute( - &self, - search_bar: &mut BufferSearchBar, - action: &A, - window: &mut Window, - cx: &mut Context, - ) -> DidHandleAction { - if search_bar.is_dismissed() { - self.0(search_bar, action, window, cx); - true - } else { - false - } - } -} - -/// Run an action when the search bar is deployed. -pub struct ForDeployed(pub(super) SearchBarActionCallback); -impl Clone for ForDeployed { - fn clone(&self) -> Self { - Self(self.0) - } -} - -impl ActionExecutor for ForDeployed { - fn execute( - &self, - search_bar: &mut BufferSearchBar, - action: &A, - window: &mut Window, - cx: &mut Context, - ) -> DidHandleAction { - if search_bar.is_dismissed() || search_bar.active_searchable_item.is_none() { - false - } else { - self.0(search_bar, action, window, cx); - true - } - } -} - -/// Run an action when the search bar has any matches, regardless of whether it -/// is visible or not. -pub struct WithResults(pub(super) SearchBarActionCallback); -impl Clone for WithResults { - fn clone(&self) -> Self { - Self(self.0) - } -} - -impl ActionExecutor for WithResults { - fn execute( - &self, - search_bar: &mut BufferSearchBar, - action: &A, - window: &mut Window, - cx: &mut Context, - ) -> DidHandleAction { - if search_bar.active_match_index.is_some() { - self.0(search_bar, action, window, cx); - true - } else { - false - } - } -} diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs deleted file mode 100644 index a9ca77a5b8..0000000000 --- a/crates/search/src/project_search.rs +++ /dev/null @@ -1,4690 +0,0 @@ -use crate::{ - BufferSearchBar, FocusSearch, NextHistoryQuery, PreviousHistoryQuery, ReplaceAll, ReplaceNext, - SearchOption, SearchOptions, SearchSource, SelectNextMatch, SelectPreviousMatch, - ToggleCaseSensitive, ToggleIncludeIgnored, ToggleRegex, ToggleReplace, ToggleWholeWord, - buffer_search::Deploy, - search_bar::{ActionButtonState, input_base_styles, render_action_button, render_text_input}, -}; -use anyhow::Context as _; -use collections::HashMap; -use editor::{ - Anchor, Editor, EditorEvent, EditorSettings, MAX_TAB_TITLE_LEN, MultiBuffer, PathKey, - SelectionEffects, - actions::{Backtab, SelectAll, Tab}, - items::active_match_index, - multibuffer_context_lines, - scroll::Autoscroll, -}; -use futures::{StreamExt, stream::FuturesOrdered}; -use gpui::{ - Action, AnyElement, App, Axis, Context, Entity, EntityId, EventEmitter, FocusHandle, Focusable, - Global, Hsla, InteractiveElement, IntoElement, KeyContext, ParentElement, Point, Render, - SharedString, Styled, Subscription, Task, UpdateGlobal, WeakEntity, Window, actions, div, -}; -use itertools::Itertools; -use language::{Buffer, Language}; -use menu::Confirm; -use project::{ - Project, ProjectPath, - search::{SearchInputKind, SearchQuery}, - search_history::SearchHistoryCursor, -}; -use settings::Settings; -use std::{ - any::{Any, TypeId}, - mem, - ops::{Not, Range}, - pin::pin, - sync::Arc, -}; -use ui::{IconButtonShape, KeyBinding, Toggleable, Tooltip, prelude::*, utils::SearchInputWidth}; -use util::{ResultExt as _, paths::PathMatcher, rel_path::RelPath}; -use workspace::{ - DeploySearch, ItemNavHistory, NewSearch, ToolbarItemEvent, ToolbarItemLocation, - ToolbarItemView, Workspace, WorkspaceId, - item::{BreadcrumbText, Item, ItemEvent, ItemHandle, SaveOptions}, - searchable::{Direction, SearchableItem, SearchableItemHandle}, -}; - -actions!( - project_search, - [ - /// Searches in a new project search tab. - SearchInNew, - /// Toggles focus between the search bar and the search results. - ToggleFocus, - /// Moves to the next input field. - NextField, - /// Toggles the search filters panel. - ToggleFilters, - /// Toggles collapse/expand state of all search result excerpts. - ToggleAllSearchResults - ] -); - -#[derive(Default)] -struct ActiveSettings(HashMap, ProjectSearchSettings>); - -impl Global for ActiveSettings {} - -pub fn init(cx: &mut App) { - cx.set_global(ActiveSettings::default()); - cx.observe_new(|workspace: &mut Workspace, _window, _cx| { - register_workspace_action(workspace, move |search_bar, _: &Deploy, window, cx| { - search_bar.focus_search(window, cx); - }); - register_workspace_action(workspace, move |search_bar, _: &FocusSearch, window, cx| { - search_bar.focus_search(window, cx); - }); - register_workspace_action( - workspace, - move |search_bar, _: &ToggleFilters, window, cx| { - search_bar.toggle_filters(window, cx); - }, - ); - register_workspace_action( - workspace, - move |search_bar, _: &ToggleCaseSensitive, window, cx| { - search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx); - }, - ); - register_workspace_action( - workspace, - move |search_bar, _: &ToggleWholeWord, window, cx| { - search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx); - }, - ); - register_workspace_action(workspace, move |search_bar, _: &ToggleRegex, window, cx| { - search_bar.toggle_search_option(SearchOptions::REGEX, window, cx); - }); - register_workspace_action( - workspace, - move |search_bar, action: &ToggleReplace, window, cx| { - search_bar.toggle_replace(action, window, cx) - }, - ); - register_workspace_action( - workspace, - move |search_bar, action: &SelectPreviousMatch, window, cx| { - search_bar.select_prev_match(action, window, cx) - }, - ); - register_workspace_action( - workspace, - move |search_bar, action: &SelectNextMatch, window, cx| { - search_bar.select_next_match(action, window, cx) - }, - ); - - // Only handle search_in_new if there is a search present - register_workspace_action_for_present_search(workspace, |workspace, action, window, cx| { - ProjectSearchView::search_in_new(workspace, action, window, cx) - }); - - register_workspace_action_for_present_search( - workspace, - |workspace, action: &ToggleAllSearchResults, window, cx| { - if let Some(search_view) = workspace - .active_item(cx) - .and_then(|item| item.downcast::()) - { - search_view.update(cx, |search_view, cx| { - search_view.toggle_all_search_results(action, window, cx); - }); - } - }, - ); - - register_workspace_action_for_present_search( - workspace, - |workspace, _: &menu::Cancel, window, cx| { - if let Some(project_search_bar) = workspace - .active_pane() - .read(cx) - .toolbar() - .read(cx) - .item_of_type::() - { - project_search_bar.update(cx, |project_search_bar, cx| { - let search_is_focused = project_search_bar - .active_project_search - .as_ref() - .is_some_and(|search_view| { - search_view - .read(cx) - .query_editor - .read(cx) - .focus_handle(cx) - .is_focused(window) - }); - if search_is_focused { - project_search_bar.move_focus_to_results(window, cx); - } else { - project_search_bar.focus_search(window, cx) - } - }); - } else { - cx.propagate(); - } - }, - ); - - // Both on present and dismissed search, we need to unconditionally handle those actions to focus from the editor. - workspace.register_action(move |workspace, action: &DeploySearch, window, cx| { - if workspace.has_active_modal(window, cx) && !workspace.hide_modal(window, cx) { - cx.propagate(); - return; - } - ProjectSearchView::deploy_search(workspace, action, window, cx); - cx.notify(); - }); - workspace.register_action(move |workspace, action: &NewSearch, window, cx| { - if workspace.has_active_modal(window, cx) && !workspace.hide_modal(window, cx) { - cx.propagate(); - return; - } - ProjectSearchView::new_search(workspace, action, window, cx); - cx.notify(); - }); - }) - .detach(); -} - -fn contains_uppercase(str: &str) -> bool { - str.chars().any(|c| c.is_uppercase()) -} - -pub struct ProjectSearch { - project: Entity, - excerpts: Entity, - pending_search: Option>>, - match_ranges: Vec>, - active_query: Option, - last_search_query_text: Option, - search_id: usize, - no_results: Option, - limit_reached: bool, - search_history_cursor: SearchHistoryCursor, - search_included_history_cursor: SearchHistoryCursor, - search_excluded_history_cursor: SearchHistoryCursor, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -enum InputPanel { - Query, - Replacement, - Exclude, - Include, -} - -pub struct ProjectSearchView { - workspace: WeakEntity, - focus_handle: FocusHandle, - entity: Entity, - query_editor: Entity, - replacement_editor: Entity, - results_editor: Entity, - search_options: SearchOptions, - panels_with_errors: HashMap, - active_match_index: Option, - search_id: usize, - included_files_editor: Entity, - excluded_files_editor: Entity, - filters_enabled: bool, - replace_enabled: bool, - included_opened_only: bool, - regex_language: Option>, - results_collapsed: bool, - _subscriptions: Vec, -} - -#[derive(Debug, Clone)] -pub struct ProjectSearchSettings { - search_options: SearchOptions, - filters_enabled: bool, -} - -pub struct ProjectSearchBar { - active_project_search: Option>, - subscription: Option, -} - -impl ProjectSearch { - pub fn new(project: Entity, cx: &mut Context) -> Self { - let capability = project.read(cx).capability(); - - Self { - project, - excerpts: cx.new(|_| MultiBuffer::new(capability)), - pending_search: Default::default(), - match_ranges: Default::default(), - active_query: None, - last_search_query_text: None, - search_id: 0, - no_results: None, - limit_reached: false, - search_history_cursor: Default::default(), - search_included_history_cursor: Default::default(), - search_excluded_history_cursor: Default::default(), - } - } - - fn clone(&self, cx: &mut Context) -> Entity { - cx.new(|cx| Self { - project: self.project.clone(), - excerpts: self - .excerpts - .update(cx, |excerpts, cx| cx.new(|cx| excerpts.clone(cx))), - pending_search: Default::default(), - match_ranges: self.match_ranges.clone(), - active_query: self.active_query.clone(), - last_search_query_text: self.last_search_query_text.clone(), - search_id: self.search_id, - no_results: self.no_results, - limit_reached: self.limit_reached, - search_history_cursor: self.search_history_cursor.clone(), - search_included_history_cursor: self.search_included_history_cursor.clone(), - search_excluded_history_cursor: self.search_excluded_history_cursor.clone(), - }) - } - fn cursor(&self, kind: SearchInputKind) -> &SearchHistoryCursor { - match kind { - SearchInputKind::Query => &self.search_history_cursor, - SearchInputKind::Include => &self.search_included_history_cursor, - SearchInputKind::Exclude => &self.search_excluded_history_cursor, - } - } - fn cursor_mut(&mut self, kind: SearchInputKind) -> &mut SearchHistoryCursor { - match kind { - SearchInputKind::Query => &mut self.search_history_cursor, - SearchInputKind::Include => &mut self.search_included_history_cursor, - SearchInputKind::Exclude => &mut self.search_excluded_history_cursor, - } - } - - fn search(&mut self, query: SearchQuery, cx: &mut Context) { - let search = self.project.update(cx, |project, cx| { - project - .search_history_mut(SearchInputKind::Query) - .add(&mut self.search_history_cursor, query.as_str().to_string()); - let included = query.as_inner().files_to_include().sources().join(","); - if !included.is_empty() { - project - .search_history_mut(SearchInputKind::Include) - .add(&mut self.search_included_history_cursor, included); - } - let excluded = query.as_inner().files_to_exclude().sources().join(","); - if !excluded.is_empty() { - project - .search_history_mut(SearchInputKind::Exclude) - .add(&mut self.search_excluded_history_cursor, excluded); - } - project.search(query.clone(), cx) - }); - self.last_search_query_text = Some(query.as_str().to_string()); - self.search_id += 1; - self.active_query = Some(query); - self.match_ranges.clear(); - self.pending_search = Some(cx.spawn(async move |project_search, cx| { - let mut matches = pin!(search.ready_chunks(1024)); - project_search - .update(cx, |project_search, cx| { - project_search.match_ranges.clear(); - project_search - .excerpts - .update(cx, |excerpts, cx| excerpts.clear(cx)); - project_search.no_results = Some(true); - project_search.limit_reached = false; - }) - .ok()?; - - let mut limit_reached = false; - while let Some(results) = matches.next().await { - let (buffers_with_ranges, has_reached_limit) = cx - .background_executor() - .spawn(async move { - let mut limit_reached = false; - let mut buffers_with_ranges = Vec::with_capacity(results.len()); - for result in results { - match result { - project::search::SearchResult::Buffer { buffer, ranges } => { - buffers_with_ranges.push((buffer, ranges)); - } - project::search::SearchResult::LimitReached => { - limit_reached = true; - } - } - } - (buffers_with_ranges, limit_reached) - }) - .await; - limit_reached |= has_reached_limit; - let mut new_ranges = project_search - .update(cx, |project_search, cx| { - project_search.excerpts.update(cx, |excerpts, cx| { - buffers_with_ranges - .into_iter() - .map(|(buffer, ranges)| { - excerpts.set_anchored_excerpts_for_path( - PathKey::for_buffer(&buffer, cx), - buffer, - ranges, - multibuffer_context_lines(cx), - cx, - ) - }) - .collect::>() - }) - }) - .ok()?; - while let Some(new_ranges) = new_ranges.next().await { - // `new_ranges.next().await` likely never gets hit while still pending so `async_task` - // will not reschedule, starving other front end tasks, insert a yield point for that here - smol::future::yield_now().await; - project_search - .update(cx, |project_search, cx| { - project_search.match_ranges.extend(new_ranges); - cx.notify(); - }) - .ok()?; - } - } - - project_search - .update(cx, |project_search, cx| { - if !project_search.match_ranges.is_empty() { - project_search.no_results = Some(false); - } - project_search.limit_reached = limit_reached; - project_search.pending_search.take(); - cx.notify(); - }) - .ok()?; - - None - })); - cx.notify(); - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum ViewEvent { - UpdateTab, - Activate, - EditorEvent(editor::EditorEvent), - Dismiss, -} - -impl EventEmitter for ProjectSearchView {} - -impl Render for ProjectSearchView { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - if self.has_matches() { - div() - .flex_1() - .size_full() - .track_focus(&self.focus_handle(cx)) - .child(self.results_editor.clone()) - } else { - let model = self.entity.read(cx); - let has_no_results = model.no_results.unwrap_or(false); - let is_search_underway = model.pending_search.is_some(); - - let heading_text = if is_search_underway { - "Searching…" - } else if has_no_results { - "No Results" - } else { - "Search All Files" - }; - - let heading_text = div() - .justify_center() - .child(Label::new(heading_text).size(LabelSize::Large)); - - let page_content: Option = if let Some(no_results) = model.no_results { - if model.pending_search.is_none() && no_results { - Some( - Label::new("No results found in this project for the provided query") - .size(LabelSize::Small) - .into_any_element(), - ) - } else { - None - } - } else { - Some(self.landing_text_minor(cx).into_any_element()) - }; - - let page_content = page_content.map(|text| div().child(text)); - - h_flex() - .size_full() - .items_center() - .justify_center() - .overflow_hidden() - .bg(cx.theme().colors().editor_background) - .track_focus(&self.focus_handle(cx)) - .child( - v_flex() - .id("project-search-landing-page") - .overflow_y_scroll() - .gap_1() - .child(heading_text) - .children(page_content), - ) - } - } -} - -impl Focusable for ProjectSearchView { - fn focus_handle(&self, _: &App) -> gpui::FocusHandle { - self.focus_handle.clone() - } -} - -impl Item for ProjectSearchView { - type Event = ViewEvent; - fn tab_tooltip_text(&self, cx: &App) -> Option { - let query_text = self.query_editor.read(cx).text(cx); - - query_text - .is_empty() - .not() - .then(|| query_text.into()) - .or_else(|| Some("Project Search".into())) - } - - fn act_as_type<'a>( - &'a self, - type_id: TypeId, - self_handle: &'a Entity, - _: &'a App, - ) -> Option { - if type_id == TypeId::of::() { - Some(self_handle.clone().into()) - } else if type_id == TypeId::of::() { - Some(self.results_editor.clone().into()) - } else { - None - } - } - fn as_searchable(&self, _: &Entity, _: &App) -> Option> { - Some(Box::new(self.results_editor.clone())) - } - - fn deactivated(&mut self, window: &mut Window, cx: &mut Context) { - self.results_editor - .update(cx, |editor, cx| editor.deactivated(window, cx)); - } - - fn tab_icon(&self, _window: &Window, _cx: &App) -> Option { - Some(Icon::new(IconName::MagnifyingGlass)) - } - - fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString { - let last_query: Option = self - .entity - .read(cx) - .last_search_query_text - .as_ref() - .map(|query| { - let query = query.replace('\n', ""); - let query_text = util::truncate_and_trailoff(&query, MAX_TAB_TITLE_LEN); - query_text.into() - }); - - last_query - .filter(|query| !query.is_empty()) - .unwrap_or_else(|| "Project Search".into()) - } - - fn telemetry_event_text(&self) -> Option<&'static str> { - Some("Project Search Opened") - } - - fn for_each_project_item( - &self, - cx: &App, - f: &mut dyn FnMut(EntityId, &dyn project::ProjectItem), - ) { - self.results_editor.for_each_project_item(cx, f) - } - - fn can_save(&self, _: &App) -> bool { - true - } - - fn is_dirty(&self, cx: &App) -> bool { - self.results_editor.read(cx).is_dirty(cx) - } - - fn has_conflict(&self, cx: &App) -> bool { - self.results_editor.read(cx).has_conflict(cx) - } - - fn save( - &mut self, - options: SaveOptions, - project: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.results_editor - .update(cx, |editor, cx| editor.save(options, project, window, cx)) - } - - fn save_as( - &mut self, - _: Entity, - _: ProjectPath, - _window: &mut Window, - _: &mut Context, - ) -> Task> { - unreachable!("save_as should not have been called") - } - - fn reload( - &mut self, - project: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.results_editor - .update(cx, |editor, cx| editor.reload(project, window, cx)) - } - - fn can_split(&self) -> bool { - true - } - - fn clone_on_split( - &self, - _workspace_id: Option, - window: &mut Window, - cx: &mut Context, - ) -> Task>> - where - Self: Sized, - { - let model = self.entity.update(cx, |model, cx| model.clone(cx)); - Task::ready(Some(cx.new(|cx| { - Self::new(self.workspace.clone(), model, window, cx, None) - }))) - } - - fn added_to_workspace( - &mut self, - workspace: &mut Workspace, - window: &mut Window, - cx: &mut Context, - ) { - self.results_editor.update(cx, |editor, cx| { - editor.added_to_workspace(workspace, window, cx) - }); - } - - fn set_nav_history( - &mut self, - nav_history: ItemNavHistory, - _: &mut Window, - cx: &mut Context, - ) { - self.results_editor.update(cx, |editor, _| { - editor.set_nav_history(Some(nav_history)); - }); - } - - fn navigate( - &mut self, - data: Box, - window: &mut Window, - cx: &mut Context, - ) -> bool { - self.results_editor - .update(cx, |editor, cx| editor.navigate(data, window, cx)) - } - - fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { - match event { - ViewEvent::UpdateTab => { - f(ItemEvent::UpdateBreadcrumbs); - f(ItemEvent::UpdateTab); - } - ViewEvent::EditorEvent(editor_event) => { - Editor::to_item_events(editor_event, f); - } - ViewEvent::Dismiss => f(ItemEvent::CloseItem), - _ => {} - } - } - - fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation { - if self.has_matches() { - ToolbarItemLocation::Secondary - } else { - ToolbarItemLocation::Hidden - } - } - - fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option> { - self.results_editor.breadcrumbs(theme, cx) - } - - fn breadcrumb_prefix( - &self, - _window: &mut Window, - cx: &mut Context, - ) -> Option { - if !self.has_matches() { - return None; - } - - let is_collapsed = self.results_collapsed; - - let (icon, tooltip_label) = if is_collapsed { - (IconName::ChevronUpDown, "Expand All Search Results") - } else { - (IconName::ChevronDownUp, "Collapse All Search Results") - }; - - let focus_handle = self.query_editor.focus_handle(cx); - - Some( - IconButton::new("project-search-collapse-expand", icon) - .shape(IconButtonShape::Square) - .icon_size(IconSize::Small) - .tooltip(move |_, cx| { - Tooltip::for_action_in( - tooltip_label, - &ToggleAllSearchResults, - &focus_handle, - cx, - ) - }) - .on_click(cx.listener(|this, _, window, cx| { - this.toggle_all_search_results(&ToggleAllSearchResults, window, cx); - })) - .into_any_element(), - ) - } -} - -impl ProjectSearchView { - pub fn get_matches(&self, cx: &App) -> Vec> { - self.entity.read(cx).match_ranges.clone() - } - - fn toggle_filters(&mut self, cx: &mut Context) { - self.filters_enabled = !self.filters_enabled; - ActiveSettings::update_global(cx, |settings, cx| { - settings.0.insert( - self.entity.read(cx).project.downgrade(), - self.current_settings(), - ); - }); - } - - fn current_settings(&self) -> ProjectSearchSettings { - ProjectSearchSettings { - search_options: self.search_options, - filters_enabled: self.filters_enabled, - } - } - - fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut Context) { - self.search_options.toggle(option); - ActiveSettings::update_global(cx, |settings, cx| { - settings.0.insert( - self.entity.read(cx).project.downgrade(), - self.current_settings(), - ); - }); - self.adjust_query_regex_language(cx); - } - - fn toggle_opened_only(&mut self, _window: &mut Window, _cx: &mut Context) { - self.included_opened_only = !self.included_opened_only; - } - - pub fn replacement(&self, cx: &App) -> String { - self.replacement_editor.read(cx).text(cx) - } - - fn replace_next(&mut self, _: &ReplaceNext, window: &mut Window, cx: &mut Context) { - if let Some(last_search_query_text) = &self.entity.read(cx).last_search_query_text - && self.query_editor.read(cx).text(cx) != *last_search_query_text - { - // search query has changed, restart search and bail - self.search(cx); - return; - } - if self.entity.read(cx).match_ranges.is_empty() { - return; - } - let Some(active_index) = self.active_match_index else { - return; - }; - - let query = self.entity.read(cx).active_query.clone(); - if let Some(query) = query { - let query = query.with_replacement(self.replacement(cx)); - - // TODO: Do we need the clone here? - let mat = self.entity.read(cx).match_ranges[active_index].clone(); - self.results_editor.update(cx, |editor, cx| { - editor.replace(&mat, &query, window, cx); - }); - self.select_match(Direction::Next, window, cx) - } - } - fn replace_all(&mut self, _: &ReplaceAll, window: &mut Window, cx: &mut Context) { - if let Some(last_search_query_text) = &self.entity.read(cx).last_search_query_text - && self.query_editor.read(cx).text(cx) != *last_search_query_text - { - // search query has changed, restart search and bail - self.search(cx); - return; - } - if self.active_match_index.is_none() { - return; - } - let Some(query) = self.entity.read(cx).active_query.as_ref() else { - return; - }; - let query = query.clone().with_replacement(self.replacement(cx)); - - let match_ranges = self - .entity - .update(cx, |model, _| mem::take(&mut model.match_ranges)); - if match_ranges.is_empty() { - return; - } - - self.results_editor.update(cx, |editor, cx| { - editor.replace_all(&mut match_ranges.iter(), &query, window, cx); - }); - - self.entity.update(cx, |model, _cx| { - model.match_ranges = match_ranges; - }); - } - - fn toggle_all_search_results( - &mut self, - _: &ToggleAllSearchResults, - _window: &mut Window, - cx: &mut Context, - ) { - self.results_collapsed = !self.results_collapsed; - self.update_results_visibility(cx); - } - - fn update_results_visibility(&mut self, cx: &mut Context) { - self.results_editor.update(cx, |editor, cx| { - let multibuffer = editor.buffer().read(cx); - let buffer_ids = multibuffer.excerpt_buffer_ids(); - - if self.results_collapsed { - for buffer_id in buffer_ids { - editor.fold_buffer(buffer_id, cx); - } - } else { - for buffer_id in buffer_ids { - editor.unfold_buffer(buffer_id, cx); - } - } - }); - cx.notify(); - } - - pub fn new( - workspace: WeakEntity, - entity: Entity, - window: &mut Window, - cx: &mut Context, - settings: Option, - ) -> Self { - let project; - let excerpts; - let mut replacement_text = None; - let mut query_text = String::new(); - let mut subscriptions = Vec::new(); - - // Read in settings if available - let (mut options, filters_enabled) = if let Some(settings) = settings { - (settings.search_options, settings.filters_enabled) - } else { - let search_options = - SearchOptions::from_settings(&EditorSettings::get_global(cx).search); - (search_options, false) - }; - - { - let entity = entity.read(cx); - project = entity.project.clone(); - excerpts = entity.excerpts.clone(); - if let Some(active_query) = entity.active_query.as_ref() { - query_text = active_query.as_str().to_string(); - replacement_text = active_query.replacement().map(ToOwned::to_owned); - options = SearchOptions::from_query(active_query); - } - } - subscriptions.push(cx.observe_in(&entity, window, |this, _, window, cx| { - this.entity_changed(window, cx) - })); - - let query_editor = cx.new(|cx| { - let mut editor = Editor::single_line(window, cx); - editor.set_placeholder_text("Search all files…", window, cx); - editor.set_text(query_text, window, cx); - editor - }); - // Subscribe to query_editor in order to reraise editor events for workspace item activation purposes - subscriptions.push( - cx.subscribe(&query_editor, |this, _, event: &EditorEvent, cx| { - if let EditorEvent::Edited { .. } = event - && EditorSettings::get_global(cx).use_smartcase_search - { - let query = this.search_query_text(cx); - if !query.is_empty() - && this.search_options.contains(SearchOptions::CASE_SENSITIVE) - != contains_uppercase(&query) - { - this.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); - } - } - cx.emit(ViewEvent::EditorEvent(event.clone())) - }), - ); - let replacement_editor = cx.new(|cx| { - let mut editor = Editor::single_line(window, cx); - editor.set_placeholder_text("Replace in project…", window, cx); - if let Some(text) = replacement_text { - editor.set_text(text, window, cx); - } - editor - }); - let results_editor = cx.new(|cx| { - let mut editor = Editor::for_multibuffer(excerpts, Some(project.clone()), window, cx); - editor.set_searchable(false); - editor.set_in_project_search(true); - editor - }); - subscriptions.push(cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab))); - - subscriptions.push( - cx.subscribe(&results_editor, |this, _, event: &EditorEvent, cx| { - if matches!(event, editor::EditorEvent::SelectionsChanged { .. }) { - this.update_match_index(cx); - } - // Reraise editor events for workspace item activation purposes - cx.emit(ViewEvent::EditorEvent(event.clone())); - }), - ); - - let included_files_editor = cx.new(|cx| { - let mut editor = Editor::single_line(window, cx); - editor.set_placeholder_text("Include: crates/**/*.toml", window, cx); - - editor - }); - // Subscribe to include_files_editor in order to reraise editor events for workspace item activation purposes - subscriptions.push( - cx.subscribe(&included_files_editor, |_, _, event: &EditorEvent, cx| { - cx.emit(ViewEvent::EditorEvent(event.clone())) - }), - ); - - let excluded_files_editor = cx.new(|cx| { - let mut editor = Editor::single_line(window, cx); - editor.set_placeholder_text("Exclude: vendor/*, *.lock", window, cx); - - editor - }); - // Subscribe to excluded_files_editor in order to reraise editor events for workspace item activation purposes - subscriptions.push( - cx.subscribe(&excluded_files_editor, |_, _, event: &EditorEvent, cx| { - cx.emit(ViewEvent::EditorEvent(event.clone())) - }), - ); - - let focus_handle = cx.focus_handle(); - subscriptions.push(cx.on_focus(&focus_handle, window, |_, window, cx| { - cx.on_next_frame(window, |this, window, cx| { - if this.focus_handle.is_focused(window) { - if this.has_matches() { - this.results_editor.focus_handle(cx).focus(window); - } else { - this.query_editor.focus_handle(cx).focus(window); - } - } - }); - })); - - let languages = project.read(cx).languages().clone(); - cx.spawn(async move |project_search_view, cx| { - let regex_language = languages - .language_for_name("regex") - .await - .context("loading regex language")?; - project_search_view - .update(cx, |project_search_view, cx| { - project_search_view.regex_language = Some(regex_language); - project_search_view.adjust_query_regex_language(cx); - }) - .ok(); - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - - // Check if Worktrees have all been previously indexed - let mut this = ProjectSearchView { - workspace, - focus_handle, - replacement_editor, - search_id: entity.read(cx).search_id, - entity, - query_editor, - results_editor, - search_options: options, - panels_with_errors: HashMap::default(), - active_match_index: None, - included_files_editor, - excluded_files_editor, - filters_enabled, - replace_enabled: false, - included_opened_only: false, - regex_language: None, - results_collapsed: false, - _subscriptions: subscriptions, - }; - - this.entity_changed(window, cx); - this - } - - pub fn new_search_in_directory( - workspace: &mut Workspace, - dir_path: &RelPath, - window: &mut Window, - cx: &mut Context, - ) { - let filter_str = dir_path.display(workspace.path_style(cx)); - - let weak_workspace = cx.entity().downgrade(); - - let entity = cx.new(|cx| ProjectSearch::new(workspace.project().clone(), cx)); - let search = cx.new(|cx| ProjectSearchView::new(weak_workspace, entity, window, cx, None)); - workspace.add_item_to_active_pane(Box::new(search.clone()), None, true, window, cx); - search.update(cx, |search, cx| { - search - .included_files_editor - .update(cx, |editor, cx| editor.set_text(filter_str, window, cx)); - search.filters_enabled = true; - search.focus_query_editor(window, cx) - }); - } - - /// Re-activate the most recently activated search in this pane or the most recent if it has been closed. - /// If no search exists in the workspace, create a new one. - pub fn deploy_search( - workspace: &mut Workspace, - action: &workspace::DeploySearch, - window: &mut Window, - cx: &mut Context, - ) { - let existing = workspace - .active_pane() - .read(cx) - .items() - .find_map(|item| item.downcast::()); - - Self::existing_or_new_search(workspace, existing, action, window, cx); - } - - fn search_in_new( - workspace: &mut Workspace, - _: &SearchInNew, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(search_view) = workspace - .active_item(cx) - .and_then(|item| item.downcast::()) - { - let new_query = search_view.update(cx, |search_view, cx| { - let open_buffers = if search_view.included_opened_only { - Some(search_view.open_buffers(cx, workspace)) - } else { - None - }; - let new_query = search_view.build_search_query(cx, open_buffers); - if new_query.is_some() - && let Some(old_query) = search_view.entity.read(cx).active_query.clone() - { - search_view.query_editor.update(cx, |editor, cx| { - editor.set_text(old_query.as_str(), window, cx); - }); - search_view.search_options = SearchOptions::from_query(&old_query); - search_view.adjust_query_regex_language(cx); - } - new_query - }); - if let Some(new_query) = new_query { - let entity = cx.new(|cx| { - let mut entity = ProjectSearch::new(workspace.project().clone(), cx); - entity.search(new_query, cx); - entity - }); - let weak_workspace = cx.entity().downgrade(); - workspace.add_item_to_active_pane( - Box::new(cx.new(|cx| { - ProjectSearchView::new(weak_workspace, entity, window, cx, None) - })), - None, - true, - window, - cx, - ); - } - } - } - - // Add another search tab to the workspace. - fn new_search( - workspace: &mut Workspace, - _: &workspace::NewSearch, - window: &mut Window, - cx: &mut Context, - ) { - Self::existing_or_new_search(workspace, None, &DeploySearch::find(), window, cx) - } - - fn existing_or_new_search( - workspace: &mut Workspace, - existing: Option>, - action: &workspace::DeploySearch, - window: &mut Window, - cx: &mut Context, - ) { - let query = workspace.active_item(cx).and_then(|item| { - if let Some(buffer_search_query) = buffer_search_query(workspace, item.as_ref(), cx) { - return Some(buffer_search_query); - } - - let editor = item.act_as::(cx)?; - let query = editor.query_suggestion(window, cx); - if query.is_empty() { None } else { Some(query) } - }); - - let search = if let Some(existing) = existing { - workspace.activate_item(&existing, true, true, window, cx); - existing - } else { - let settings = cx - .global::() - .0 - .get(&workspace.project().downgrade()); - - let settings = settings.cloned(); - - let weak_workspace = cx.entity().downgrade(); - - let project_search = cx.new(|cx| ProjectSearch::new(workspace.project().clone(), cx)); - let project_search_view = cx.new(|cx| { - ProjectSearchView::new(weak_workspace, project_search, window, cx, settings) - }); - - workspace.add_item_to_active_pane( - Box::new(project_search_view.clone()), - None, - true, - window, - cx, - ); - project_search_view - }; - - search.update(cx, |search, cx| { - search.replace_enabled = action.replace_enabled; - if let Some(query) = query { - search.set_query(&query, window, cx); - } - if let Some(included_files) = action.included_files.as_deref() { - search - .included_files_editor - .update(cx, |editor, cx| editor.set_text(included_files, window, cx)); - search.filters_enabled = true; - } - if let Some(excluded_files) = action.excluded_files.as_deref() { - search - .excluded_files_editor - .update(cx, |editor, cx| editor.set_text(excluded_files, window, cx)); - search.filters_enabled = true; - } - search.focus_query_editor(window, cx) - }); - } - - fn prompt_to_save_if_dirty_then_search( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - let project = self.entity.read(cx).project.clone(); - - let can_autosave = self.results_editor.can_autosave(cx); - let autosave_setting = self.results_editor.workspace_settings(cx).autosave; - - let will_autosave = can_autosave && autosave_setting.should_save_on_close(); - - let is_dirty = self.is_dirty(cx); - - cx.spawn_in(window, async move |this, cx| { - let skip_save_on_close = this - .read_with(cx, |this, cx| { - this.workspace.read_with(cx, |workspace, cx| { - workspace::Pane::skip_save_on_close(&this.results_editor, workspace, cx) - }) - })? - .unwrap_or(false); - - let should_prompt_to_save = !skip_save_on_close && !will_autosave && is_dirty; - - let should_search = if should_prompt_to_save { - let options = &["Save", "Don't Save", "Cancel"]; - let result_channel = this.update_in(cx, |_, window, cx| { - window.prompt( - gpui::PromptLevel::Warning, - "Project search buffer contains unsaved edits. Do you want to save it?", - None, - options, - cx, - ) - })?; - let result = result_channel.await?; - let should_save = result == 0; - if should_save { - this.update_in(cx, |this, window, cx| { - this.save( - SaveOptions { - format: true, - autosave: false, - }, - project, - window, - cx, - ) - })? - .await - .log_err(); - } - - result != 2 - } else { - true - }; - if should_search { - this.update(cx, |this, cx| { - this.search(cx); - })?; - } - anyhow::Ok(()) - }) - } - - fn search(&mut self, cx: &mut Context) { - let open_buffers = if self.included_opened_only { - self.workspace - .update(cx, |workspace, cx| self.open_buffers(cx, workspace)) - .ok() - } else { - None - }; - if let Some(query) = self.build_search_query(cx, open_buffers) { - self.entity.update(cx, |model, cx| model.search(query, cx)); - } - } - - pub fn search_query_text(&self, cx: &App) -> String { - self.query_editor.read(cx).text(cx) - } - - fn build_search_query( - &mut self, - cx: &mut Context, - open_buffers: Option>>, - ) -> Option { - // Do not bail early in this function, as we want to fill out `self.panels_with_errors`. - - let text = self.search_query_text(cx); - let included_files = self - .filters_enabled - .then(|| { - match self.parse_path_matches(self.included_files_editor.read(cx).text(cx), cx) { - Ok(included_files) => { - let should_unmark_error = - self.panels_with_errors.remove(&InputPanel::Include); - if should_unmark_error.is_some() { - cx.notify(); - } - included_files - } - Err(e) => { - let should_mark_error = self - .panels_with_errors - .insert(InputPanel::Include, e.to_string()); - if should_mark_error.is_none() { - cx.notify(); - } - PathMatcher::default() - } - } - }) - .unwrap_or(PathMatcher::default()); - let excluded_files = self - .filters_enabled - .then(|| { - match self.parse_path_matches(self.excluded_files_editor.read(cx).text(cx), cx) { - Ok(excluded_files) => { - let should_unmark_error = - self.panels_with_errors.remove(&InputPanel::Exclude); - if should_unmark_error.is_some() { - cx.notify(); - } - - excluded_files - } - Err(e) => { - let should_mark_error = self - .panels_with_errors - .insert(InputPanel::Exclude, e.to_string()); - if should_mark_error.is_none() { - cx.notify(); - } - PathMatcher::default() - } - } - }) - .unwrap_or(PathMatcher::default()); - - // If the project contains multiple visible worktrees, we match the - // include/exclude patterns against full paths to allow them to be - // disambiguated. For single worktree projects we use worktree relative - // paths for convenience. - let match_full_paths = self - .entity - .read(cx) - .project - .read(cx) - .visible_worktrees(cx) - .count() - > 1; - - let query = if self.search_options.contains(SearchOptions::REGEX) { - match SearchQuery::regex( - text, - self.search_options.contains(SearchOptions::WHOLE_WORD), - self.search_options.contains(SearchOptions::CASE_SENSITIVE), - self.search_options.contains(SearchOptions::INCLUDE_IGNORED), - self.search_options - .contains(SearchOptions::ONE_MATCH_PER_LINE), - included_files, - excluded_files, - match_full_paths, - open_buffers, - ) { - Ok(query) => { - let should_unmark_error = self.panels_with_errors.remove(&InputPanel::Query); - if should_unmark_error.is_some() { - cx.notify(); - } - - Some(query) - } - Err(e) => { - let should_mark_error = self - .panels_with_errors - .insert(InputPanel::Query, e.to_string()); - if should_mark_error.is_none() { - cx.notify(); - } - - None - } - } - } else { - match SearchQuery::text( - text, - self.search_options.contains(SearchOptions::WHOLE_WORD), - self.search_options.contains(SearchOptions::CASE_SENSITIVE), - self.search_options.contains(SearchOptions::INCLUDE_IGNORED), - included_files, - excluded_files, - match_full_paths, - open_buffers, - ) { - Ok(query) => { - let should_unmark_error = self.panels_with_errors.remove(&InputPanel::Query); - if should_unmark_error.is_some() { - cx.notify(); - } - - Some(query) - } - Err(e) => { - let should_mark_error = self - .panels_with_errors - .insert(InputPanel::Query, e.to_string()); - if should_mark_error.is_none() { - cx.notify(); - } - - None - } - } - }; - if !self.panels_with_errors.is_empty() { - return None; - } - if query.as_ref().is_some_and(|query| query.is_empty()) { - return None; - } - query - } - - fn open_buffers(&self, cx: &App, workspace: &Workspace) -> Vec> { - let mut buffers = Vec::new(); - for editor in workspace.items_of_type::(cx) { - if let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() { - buffers.push(buffer); - } - } - buffers - } - - fn parse_path_matches(&self, text: String, cx: &App) -> anyhow::Result { - let path_style = self.entity.read(cx).project.read(cx).path_style(cx); - let queries = text - .split(',') - .map(str::trim) - .filter(|maybe_glob_str| !maybe_glob_str.is_empty()) - .map(str::to_owned) - .collect::>(); - Ok(PathMatcher::new(&queries, path_style)?) - } - - fn select_match(&mut self, direction: Direction, window: &mut Window, cx: &mut Context) { - if let Some(index) = self.active_match_index { - let match_ranges = self.entity.read(cx).match_ranges.clone(); - - if !EditorSettings::get_global(cx).search_wrap - && ((direction == Direction::Next && index + 1 >= match_ranges.len()) - || (direction == Direction::Prev && index == 0)) - { - crate::show_no_more_matches(window, cx); - return; - } - - let new_index = self.results_editor.update(cx, |editor, cx| { - editor.match_index_for_direction(&match_ranges, index, direction, 1, window, cx) - }); - - let range_to_select = match_ranges[new_index].clone(); - self.results_editor.update(cx, |editor, cx| { - let range_to_select = editor.range_for_match(&range_to_select); - let autoscroll = if EditorSettings::get_global(cx).search.center_on_match { - Autoscroll::center() - } else { - Autoscroll::fit() - }; - editor.unfold_ranges(std::slice::from_ref(&range_to_select), false, true, cx); - editor.change_selections(SelectionEffects::scroll(autoscroll), window, cx, |s| { - s.select_ranges([range_to_select]) - }); - }); - self.highlight_matches(&match_ranges, Some(new_index), cx); - } - } - - fn focus_query_editor(&mut self, window: &mut Window, cx: &mut Context) { - self.query_editor.update(cx, |query_editor, cx| { - query_editor.select_all(&SelectAll, window, cx); - }); - let editor_handle = self.query_editor.focus_handle(cx); - window.focus(&editor_handle); - } - - fn set_query(&mut self, query: &str, window: &mut Window, cx: &mut Context) { - self.set_search_editor(SearchInputKind::Query, query, window, cx); - if EditorSettings::get_global(cx).use_smartcase_search - && !query.is_empty() - && self.search_options.contains(SearchOptions::CASE_SENSITIVE) - != contains_uppercase(query) - { - self.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx) - } - } - - fn set_search_editor( - &mut self, - kind: SearchInputKind, - text: &str, - window: &mut Window, - cx: &mut Context, - ) { - let editor = match kind { - SearchInputKind::Query => &self.query_editor, - SearchInputKind::Include => &self.included_files_editor, - - SearchInputKind::Exclude => &self.excluded_files_editor, - }; - editor.update(cx, |included_editor, cx| { - included_editor.set_text(text, window, cx) - }); - } - - fn focus_results_editor(&mut self, window: &mut Window, cx: &mut Context) { - self.query_editor.update(cx, |query_editor, cx| { - let cursor = query_editor.selections.newest_anchor().head(); - query_editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges([cursor..cursor]) - }); - }); - let results_handle = self.results_editor.focus_handle(cx); - window.focus(&results_handle); - } - - fn entity_changed(&mut self, window: &mut Window, cx: &mut Context) { - let match_ranges = self.entity.read(cx).match_ranges.clone(); - - if match_ranges.is_empty() { - self.active_match_index = None; - self.results_editor.update(cx, |editor, cx| { - editor.clear_background_highlights::(cx); - }); - } else { - self.active_match_index = Some(0); - self.update_match_index(cx); - let prev_search_id = mem::replace(&mut self.search_id, self.entity.read(cx).search_id); - let is_new_search = self.search_id != prev_search_id; - self.results_editor.update(cx, |editor, cx| { - if is_new_search { - let range_to_select = match_ranges - .first() - .map(|range| editor.range_for_match(range)); - editor.change_selections(Default::default(), window, cx, |s| { - s.select_ranges(range_to_select) - }); - editor.scroll(Point::default(), Some(Axis::Vertical), window, cx); - } - }); - if is_new_search && self.query_editor.focus_handle(cx).is_focused(window) { - self.focus_results_editor(window, cx); - } - } - - cx.emit(ViewEvent::UpdateTab); - cx.notify(); - } - - fn update_match_index(&mut self, cx: &mut Context) { - let results_editor = self.results_editor.read(cx); - let match_ranges = self.entity.read(cx).match_ranges.clone(); - let new_index = active_match_index( - Direction::Next, - &match_ranges, - &results_editor.selections.newest_anchor().head(), - &results_editor.buffer().read(cx).snapshot(cx), - ); - self.highlight_matches(&match_ranges, new_index, cx); - if self.active_match_index != new_index { - self.active_match_index = new_index; - cx.notify(); - } - } - - #[ztracing::instrument(skip_all)] - fn highlight_matches( - &self, - match_ranges: &[Range], - active_index: Option, - cx: &mut Context, - ) { - self.results_editor.update(cx, |editor, cx| { - editor.highlight_background::( - match_ranges, - move |index, theme| { - if active_index == Some(*index) { - theme.colors().search_active_match_background - } else { - theme.colors().search_match_background - } - }, - cx, - ); - }); - } - - pub fn has_matches(&self) -> bool { - self.active_match_index.is_some() - } - - fn landing_text_minor(&self, cx: &App) -> impl IntoElement { - let focus_handle = self.focus_handle.clone(); - v_flex() - .gap_1() - .child( - Label::new("Hit enter to search. For more options:") - .color(Color::Muted) - .mb_2(), - ) - .child( - Button::new("filter-paths", "Include/exclude specific paths") - .icon(IconName::Filter) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .key_binding(KeyBinding::for_action_in(&ToggleFilters, &focus_handle, cx)) - .on_click(|_event, window, cx| { - window.dispatch_action(ToggleFilters.boxed_clone(), cx) - }), - ) - .child( - Button::new("find-replace", "Find and replace") - .icon(IconName::Replace) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .key_binding(KeyBinding::for_action_in(&ToggleReplace, &focus_handle, cx)) - .on_click(|_event, window, cx| { - window.dispatch_action(ToggleReplace.boxed_clone(), cx) - }), - ) - .child( - Button::new("regex", "Match with regex") - .icon(IconName::Regex) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .key_binding(KeyBinding::for_action_in(&ToggleRegex, &focus_handle, cx)) - .on_click(|_event, window, cx| { - window.dispatch_action(ToggleRegex.boxed_clone(), cx) - }), - ) - .child( - Button::new("match-case", "Match case") - .icon(IconName::CaseSensitive) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .key_binding(KeyBinding::for_action_in( - &ToggleCaseSensitive, - &focus_handle, - cx, - )) - .on_click(|_event, window, cx| { - window.dispatch_action(ToggleCaseSensitive.boxed_clone(), cx) - }), - ) - .child( - Button::new("match-whole-words", "Match whole words") - .icon(IconName::WholeWord) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .key_binding(KeyBinding::for_action_in( - &ToggleWholeWord, - &focus_handle, - cx, - )) - .on_click(|_event, window, cx| { - window.dispatch_action(ToggleWholeWord.boxed_clone(), cx) - }), - ) - } - - fn border_color_for(&self, panel: InputPanel, cx: &App) -> Hsla { - if self.panels_with_errors.contains_key(&panel) { - Color::Error.color(cx) - } else { - cx.theme().colors().border - } - } - - fn move_focus_to_results(&mut self, window: &mut Window, cx: &mut Context) { - if !self.results_editor.focus_handle(cx).is_focused(window) - && !self.entity.read(cx).match_ranges.is_empty() - { - cx.stop_propagation(); - self.focus_results_editor(window, cx) - } - } - - #[cfg(any(test, feature = "test-support"))] - pub fn results_editor(&self) -> &Entity { - &self.results_editor - } - - fn adjust_query_regex_language(&self, cx: &mut App) { - let enable = self.search_options.contains(SearchOptions::REGEX); - let query_buffer = self - .query_editor - .read(cx) - .buffer() - .read(cx) - .as_singleton() - .expect("query editor should be backed by a singleton buffer"); - if enable { - if let Some(regex_language) = self.regex_language.clone() { - query_buffer.update(cx, |query_buffer, cx| { - query_buffer.set_language(Some(regex_language), cx); - }) - } - } else { - query_buffer.update(cx, |query_buffer, cx| { - query_buffer.set_language(None, cx); - }) - } - } -} - -fn buffer_search_query( - workspace: &mut Workspace, - item: &dyn ItemHandle, - cx: &mut Context, -) -> Option { - let buffer_search_bar = workspace - .pane_for(item) - .and_then(|pane| { - pane.read(cx) - .toolbar() - .read(cx) - .item_of_type::() - })? - .read(cx); - if buffer_search_bar.query_editor_focused() { - let buffer_search_query = buffer_search_bar.query(cx); - if !buffer_search_query.is_empty() { - return Some(buffer_search_query); - } - } - None -} - -impl Default for ProjectSearchBar { - fn default() -> Self { - Self::new() - } -} - -impl ProjectSearchBar { - pub fn new() -> Self { - Self { - active_project_search: None, - subscription: None, - } - } - - fn confirm(&mut self, _: &Confirm, window: &mut Window, cx: &mut Context) { - if let Some(search_view) = self.active_project_search.as_ref() { - search_view.update(cx, |search_view, cx| { - if !search_view - .replacement_editor - .focus_handle(cx) - .is_focused(window) - { - cx.stop_propagation(); - search_view - .prompt_to_save_if_dirty_then_search(window, cx) - .detach_and_log_err(cx); - } - }); - } - } - - fn tab(&mut self, _: &Tab, window: &mut Window, cx: &mut Context) { - self.cycle_field(Direction::Next, window, cx); - } - - fn backtab(&mut self, _: &Backtab, window: &mut Window, cx: &mut Context) { - self.cycle_field(Direction::Prev, window, cx); - } - - fn focus_search(&mut self, window: &mut Window, cx: &mut Context) { - if let Some(search_view) = self.active_project_search.as_ref() { - search_view.update(cx, |search_view, cx| { - search_view.query_editor.focus_handle(cx).focus(window); - }); - } - } - - fn cycle_field(&mut self, direction: Direction, window: &mut Window, cx: &mut Context) { - let active_project_search = match &self.active_project_search { - Some(active_project_search) => active_project_search, - None => return, - }; - - active_project_search.update(cx, |project_view, cx| { - let mut views = vec![project_view.query_editor.focus_handle(cx)]; - if project_view.replace_enabled { - views.push(project_view.replacement_editor.focus_handle(cx)); - } - if project_view.filters_enabled { - views.extend([ - project_view.included_files_editor.focus_handle(cx), - project_view.excluded_files_editor.focus_handle(cx), - ]); - } - let current_index = match views.iter().position(|focus| focus.is_focused(window)) { - Some(index) => index, - None => return, - }; - - let new_index = match direction { - Direction::Next => (current_index + 1) % views.len(), - Direction::Prev if current_index == 0 => views.len() - 1, - Direction::Prev => (current_index - 1) % views.len(), - }; - let next_focus_handle = &views[new_index]; - window.focus(next_focus_handle); - cx.stop_propagation(); - }); - } - - pub(crate) fn toggle_search_option( - &mut self, - option: SearchOptions, - window: &mut Window, - cx: &mut Context, - ) -> bool { - if self.active_project_search.is_none() { - return false; - } - - cx.spawn_in(window, async move |this, cx| { - let task = this.update_in(cx, |this, window, cx| { - let search_view = this.active_project_search.as_ref()?; - search_view.update(cx, |search_view, cx| { - search_view.toggle_search_option(option, cx); - search_view - .entity - .read(cx) - .active_query - .is_some() - .then(|| search_view.prompt_to_save_if_dirty_then_search(window, cx)) - }) - })?; - if let Some(task) = task { - task.await?; - } - this.update(cx, |_, cx| { - cx.notify(); - })?; - anyhow::Ok(()) - }) - .detach(); - true - } - - fn toggle_replace(&mut self, _: &ToggleReplace, window: &mut Window, cx: &mut Context) { - if let Some(search) = &self.active_project_search { - search.update(cx, |this, cx| { - this.replace_enabled = !this.replace_enabled; - let editor_to_focus = if this.replace_enabled { - this.replacement_editor.focus_handle(cx) - } else { - this.query_editor.focus_handle(cx) - }; - window.focus(&editor_to_focus); - cx.notify(); - }); - } - } - - fn toggle_filters(&mut self, window: &mut Window, cx: &mut Context) -> bool { - if let Some(search_view) = self.active_project_search.as_ref() { - search_view.update(cx, |search_view, cx| { - search_view.toggle_filters(cx); - search_view - .included_files_editor - .update(cx, |_, cx| cx.notify()); - search_view - .excluded_files_editor - .update(cx, |_, cx| cx.notify()); - window.refresh(); - cx.notify(); - }); - cx.notify(); - true - } else { - false - } - } - - fn toggle_opened_only(&mut self, window: &mut Window, cx: &mut Context) -> bool { - if self.active_project_search.is_none() { - return false; - } - - cx.spawn_in(window, async move |this, cx| { - let task = this.update_in(cx, |this, window, cx| { - let search_view = this.active_project_search.as_ref()?; - search_view.update(cx, |search_view, cx| { - search_view.toggle_opened_only(window, cx); - search_view - .entity - .read(cx) - .active_query - .is_some() - .then(|| search_view.prompt_to_save_if_dirty_then_search(window, cx)) - }) - })?; - if let Some(task) = task { - task.await?; - } - this.update(cx, |_, cx| { - cx.notify(); - })?; - anyhow::Ok(()) - }) - .detach(); - true - } - - fn is_opened_only_enabled(&self, cx: &App) -> bool { - if let Some(search_view) = self.active_project_search.as_ref() { - search_view.read(cx).included_opened_only - } else { - false - } - } - - fn move_focus_to_results(&self, window: &mut Window, cx: &mut Context) { - if let Some(search_view) = self.active_project_search.as_ref() { - search_view.update(cx, |search_view, cx| { - search_view.move_focus_to_results(window, cx); - }); - cx.notify(); - } - } - - fn next_history_query( - &mut self, - _: &NextHistoryQuery, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(search_view) = self.active_project_search.as_ref() { - search_view.update(cx, |search_view, cx| { - for (editor, kind) in [ - (search_view.query_editor.clone(), SearchInputKind::Query), - ( - search_view.included_files_editor.clone(), - SearchInputKind::Include, - ), - ( - search_view.excluded_files_editor.clone(), - SearchInputKind::Exclude, - ), - ] { - if editor.focus_handle(cx).is_focused(window) { - let new_query = search_view.entity.update(cx, |model, cx| { - let project = model.project.clone(); - - if let Some(new_query) = project.update(cx, |project, _| { - project - .search_history_mut(kind) - .next(model.cursor_mut(kind)) - .map(str::to_string) - }) { - new_query - } else { - model.cursor_mut(kind).reset(); - String::new() - } - }); - search_view.set_search_editor(kind, &new_query, window, cx); - } - } - }); - } - } - - fn previous_history_query( - &mut self, - _: &PreviousHistoryQuery, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(search_view) = self.active_project_search.as_ref() { - search_view.update(cx, |search_view, cx| { - for (editor, kind) in [ - (search_view.query_editor.clone(), SearchInputKind::Query), - ( - search_view.included_files_editor.clone(), - SearchInputKind::Include, - ), - ( - search_view.excluded_files_editor.clone(), - SearchInputKind::Exclude, - ), - ] { - if editor.focus_handle(cx).is_focused(window) { - if editor.read(cx).text(cx).is_empty() - && let Some(new_query) = search_view - .entity - .read(cx) - .project - .read(cx) - .search_history(kind) - .current(search_view.entity.read(cx).cursor(kind)) - .map(str::to_string) - { - search_view.set_search_editor(kind, &new_query, window, cx); - return; - } - - if let Some(new_query) = search_view.entity.update(cx, |model, cx| { - let project = model.project.clone(); - project.update(cx, |project, _| { - project - .search_history_mut(kind) - .previous(model.cursor_mut(kind)) - .map(str::to_string) - }) - }) { - search_view.set_search_editor(kind, &new_query, window, cx); - } - } - } - }); - } - } - - fn select_next_match( - &mut self, - _: &SelectNextMatch, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(search) = self.active_project_search.as_ref() { - search.update(cx, |this, cx| { - this.select_match(Direction::Next, window, cx); - }) - } - } - - fn select_prev_match( - &mut self, - _: &SelectPreviousMatch, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(search) = self.active_project_search.as_ref() { - search.update(cx, |this, cx| { - this.select_match(Direction::Prev, window, cx); - }) - } - } -} - -impl Render for ProjectSearchBar { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let Some(search) = self.active_project_search.clone() else { - return div(); - }; - let search = search.read(cx); - let focus_handle = search.focus_handle(cx); - - let container_width = window.viewport_size().width; - let input_width = SearchInputWidth::calc_width(container_width); - - let input_base_styles = |panel: InputPanel| { - input_base_styles(search.border_color_for(panel, cx), |div| match panel { - InputPanel::Query | InputPanel::Replacement => div.w(input_width), - InputPanel::Include | InputPanel::Exclude => div.flex_grow(), - }) - }; - let theme_colors = cx.theme().colors(); - let project_search = search.entity.read(cx); - let limit_reached = project_search.limit_reached; - - let color_override = match ( - &project_search.pending_search, - project_search.no_results, - &project_search.active_query, - &project_search.last_search_query_text, - ) { - (None, Some(true), Some(q), Some(p)) if q.as_str() == p => Some(Color::Error), - _ => None, - }; - - let match_text = search - .active_match_index - .and_then(|index| { - let index = index + 1; - let match_quantity = project_search.match_ranges.len(); - if match_quantity > 0 { - debug_assert!(match_quantity >= index); - if limit_reached { - Some(format!("{index}/{match_quantity}+")) - } else { - Some(format!("{index}/{match_quantity}")) - } - } else { - None - } - }) - .unwrap_or_else(|| "0/0".to_string()); - - let query_focus = search.query_editor.focus_handle(cx); - - let query_column = input_base_styles(InputPanel::Query) - .on_action(cx.listener(|this, action, window, cx| this.confirm(action, window, cx))) - .on_action(cx.listener(|this, action, window, cx| { - this.previous_history_query(action, window, cx) - })) - .on_action( - cx.listener(|this, action, window, cx| this.next_history_query(action, window, cx)), - ) - .child(render_text_input(&search.query_editor, color_override, cx)) - .child( - h_flex() - .gap_1() - .child(SearchOption::CaseSensitive.as_button( - search.search_options, - SearchSource::Project(cx), - focus_handle.clone(), - )) - .child(SearchOption::WholeWord.as_button( - search.search_options, - SearchSource::Project(cx), - focus_handle.clone(), - )) - .child(SearchOption::Regex.as_button( - search.search_options, - SearchSource::Project(cx), - focus_handle.clone(), - )), - ); - - let matches_column = h_flex() - .ml_1() - .pl_1p5() - .border_l_1() - .border_color(theme_colors.border_variant) - .child(render_action_button( - "project-search-nav-button", - IconName::ChevronLeft, - search - .active_match_index - .is_none() - .then_some(ActionButtonState::Disabled), - "Select Previous Match", - &SelectPreviousMatch, - query_focus.clone(), - )) - .child(render_action_button( - "project-search-nav-button", - IconName::ChevronRight, - search - .active_match_index - .is_none() - .then_some(ActionButtonState::Disabled), - "Select Next Match", - &SelectNextMatch, - query_focus, - )) - .child( - div() - .id("matches") - .ml_2() - .min_w(rems_from_px(40.)) - .child(Label::new(match_text).size(LabelSize::Small).color( - if search.active_match_index.is_some() { - Color::Default - } else { - Color::Disabled - }, - )) - .when(limit_reached, |el| { - el.tooltip(Tooltip::text( - "Search limits reached.\nTry narrowing your search.", - )) - }), - ); - - let mode_column = h_flex() - .gap_1() - .min_w_64() - .child( - IconButton::new("project-search-filter-button", IconName::Filter) - .shape(IconButtonShape::Square) - .tooltip(|_window, cx| { - Tooltip::for_action("Toggle Filters", &ToggleFilters, cx) - }) - .on_click(cx.listener(|this, _, window, cx| { - this.toggle_filters(window, cx); - })) - .toggle_state( - self.active_project_search - .as_ref() - .map(|search| search.read(cx).filters_enabled) - .unwrap_or_default(), - ) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |_window, cx| { - Tooltip::for_action_in( - "Toggle Filters", - &ToggleFilters, - &focus_handle, - cx, - ) - } - }), - ) - .child(render_action_button( - "project-search", - IconName::Replace, - self.active_project_search - .as_ref() - .map(|search| search.read(cx).replace_enabled) - .and_then(|enabled| enabled.then_some(ActionButtonState::Toggled)), - "Toggle Replace", - &ToggleReplace, - focus_handle.clone(), - )) - .child(matches_column); - - let search_line = h_flex() - .w_full() - .gap_2() - .child(query_column) - .child(mode_column); - - let replace_line = search.replace_enabled.then(|| { - let replace_column = input_base_styles(InputPanel::Replacement) - .child(render_text_input(&search.replacement_editor, None, cx)); - - let focus_handle = search.replacement_editor.read(cx).focus_handle(cx); - - let replace_actions = h_flex() - .min_w_64() - .gap_1() - .child(render_action_button( - "project-search-replace-button", - IconName::ReplaceNext, - Default::default(), - "Replace Next Match", - &ReplaceNext, - focus_handle.clone(), - )) - .child(render_action_button( - "project-search-replace-button", - IconName::ReplaceAll, - Default::default(), - "Replace All Matches", - &ReplaceAll, - focus_handle, - )); - - h_flex() - .w_full() - .gap_2() - .child(replace_column) - .child(replace_actions) - }); - - let filter_line = search.filters_enabled.then(|| { - let include = input_base_styles(InputPanel::Include) - .on_action(cx.listener(|this, action, window, cx| { - this.previous_history_query(action, window, cx) - })) - .on_action(cx.listener(|this, action, window, cx| { - this.next_history_query(action, window, cx) - })) - .child(render_text_input(&search.included_files_editor, None, cx)); - let exclude = input_base_styles(InputPanel::Exclude) - .on_action(cx.listener(|this, action, window, cx| { - this.previous_history_query(action, window, cx) - })) - .on_action(cx.listener(|this, action, window, cx| { - this.next_history_query(action, window, cx) - })) - .child(render_text_input(&search.excluded_files_editor, None, cx)); - let mode_column = h_flex() - .gap_1() - .min_w_64() - .child( - IconButton::new("project-search-opened-only", IconName::FolderSearch) - .shape(IconButtonShape::Square) - .toggle_state(self.is_opened_only_enabled(cx)) - .tooltip(Tooltip::text("Only Search Open Files")) - .on_click(cx.listener(|this, _, window, cx| { - this.toggle_opened_only(window, cx); - })), - ) - .child(SearchOption::IncludeIgnored.as_button( - search.search_options, - SearchSource::Project(cx), - focus_handle.clone(), - )); - h_flex() - .w_full() - .gap_2() - .child( - h_flex() - .gap_2() - .w(input_width) - .child(include) - .child(exclude), - ) - .child(mode_column) - }); - - let mut key_context = KeyContext::default(); - key_context.add("ProjectSearchBar"); - if search - .replacement_editor - .focus_handle(cx) - .is_focused(window) - { - key_context.add("in_replace"); - } - - let query_error_line = search - .panels_with_errors - .get(&InputPanel::Query) - .map(|error| { - Label::new(error) - .size(LabelSize::Small) - .color(Color::Error) - .mt_neg_1() - .ml_2() - }); - - let filter_error_line = search - .panels_with_errors - .get(&InputPanel::Include) - .or_else(|| search.panels_with_errors.get(&InputPanel::Exclude)) - .map(|error| { - Label::new(error) - .size(LabelSize::Small) - .color(Color::Error) - .mt_neg_1() - .ml_2() - }); - - v_flex() - .gap_2() - .py(px(1.0)) - .w_full() - .key_context(key_context) - .on_action(cx.listener(|this, _: &ToggleFocus, window, cx| { - this.move_focus_to_results(window, cx) - })) - .on_action(cx.listener(|this, _: &ToggleFilters, window, cx| { - this.toggle_filters(window, cx); - })) - .capture_action(cx.listener(Self::tab)) - .capture_action(cx.listener(Self::backtab)) - .on_action(cx.listener(|this, action, window, cx| this.confirm(action, window, cx))) - .on_action(cx.listener(|this, action, window, cx| { - this.toggle_replace(action, window, cx); - })) - .on_action(cx.listener(|this, _: &ToggleWholeWord, window, cx| { - this.toggle_search_option(SearchOptions::WHOLE_WORD, window, cx); - })) - .on_action(cx.listener(|this, _: &ToggleCaseSensitive, window, cx| { - this.toggle_search_option(SearchOptions::CASE_SENSITIVE, window, cx); - })) - .on_action(cx.listener(|this, action, window, cx| { - if let Some(search) = this.active_project_search.as_ref() { - search.update(cx, |this, cx| { - this.replace_next(action, window, cx); - }) - } - })) - .on_action(cx.listener(|this, action, window, cx| { - if let Some(search) = this.active_project_search.as_ref() { - search.update(cx, |this, cx| { - this.replace_all(action, window, cx); - }) - } - })) - .when(search.filters_enabled, |this| { - this.on_action(cx.listener(|this, _: &ToggleIncludeIgnored, window, cx| { - this.toggle_search_option(SearchOptions::INCLUDE_IGNORED, window, cx); - })) - }) - .on_action(cx.listener(Self::select_next_match)) - .on_action(cx.listener(Self::select_prev_match)) - .child(search_line) - .children(query_error_line) - .children(replace_line) - .children(filter_line) - .children(filter_error_line) - } -} - -impl EventEmitter for ProjectSearchBar {} - -impl ToolbarItemView for ProjectSearchBar { - fn set_active_pane_item( - &mut self, - active_pane_item: Option<&dyn ItemHandle>, - _: &mut Window, - cx: &mut Context, - ) -> ToolbarItemLocation { - cx.notify(); - self.subscription = None; - self.active_project_search = None; - if let Some(search) = active_pane_item.and_then(|i| i.downcast::()) { - self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify())); - self.active_project_search = Some(search); - ToolbarItemLocation::PrimaryLeft {} - } else { - ToolbarItemLocation::Hidden - } - } -} - -fn register_workspace_action( - workspace: &mut Workspace, - callback: fn(&mut ProjectSearchBar, &A, &mut Window, &mut Context), -) { - workspace.register_action(move |workspace, action: &A, window, cx| { - if workspace.has_active_modal(window, cx) && !workspace.hide_modal(window, cx) { - cx.propagate(); - return; - } - - workspace.active_pane().update(cx, |pane, cx| { - pane.toolbar().update(cx, move |workspace, cx| { - if let Some(search_bar) = workspace.item_of_type::() { - search_bar.update(cx, move |search_bar, cx| { - if search_bar.active_project_search.is_some() { - callback(search_bar, action, window, cx); - cx.notify(); - } else { - cx.propagate(); - } - }); - } - }); - }) - }); -} - -fn register_workspace_action_for_present_search( - workspace: &mut Workspace, - callback: fn(&mut Workspace, &A, &mut Window, &mut Context), -) { - workspace.register_action(move |workspace, action: &A, window, cx| { - if workspace.has_active_modal(window, cx) && !workspace.hide_modal(window, cx) { - cx.propagate(); - return; - } - - let should_notify = workspace - .active_pane() - .read(cx) - .toolbar() - .read(cx) - .item_of_type::() - .map(|search_bar| search_bar.read(cx).active_project_search.is_some()) - .unwrap_or(false); - if should_notify { - callback(workspace, action, window, cx); - cx.notify(); - } else { - cx.propagate(); - } - }); -} - -#[cfg(any(test, feature = "test-support"))] -pub fn perform_project_search( - search_view: &Entity, - text: impl Into>, - cx: &mut gpui::VisualTestContext, -) { - cx.run_until_parked(); - search_view.update_in(cx, |search_view, window, cx| { - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text(text, window, cx) - }); - search_view.search(cx); - }); - cx.run_until_parked(); -} - -#[cfg(test)] -pub mod tests { - use std::{ - ops::Deref as _, - path::PathBuf, - sync::{ - Arc, - atomic::{self, AtomicUsize}, - }, - time::Duration, - }; - - use super::*; - use editor::{DisplayPoint, display_map::DisplayRow}; - use gpui::{Action, TestAppContext, VisualTestContext, WindowHandle}; - use language::{FakeLspAdapter, rust_lang}; - use pretty_assertions::assert_eq; - use project::FakeFs; - use serde_json::json; - use settings::{ - InlayHintSettingsContent, SettingsStore, ThemeColorsContent, ThemeStyleContent, - }; - use util::{path, paths::PathStyle, rel_path::rel_path}; - use util_macros::perf; - use workspace::DeploySearch; - - #[perf] - #[gpui::test] - async fn test_project_search(cx: &mut TestAppContext) { - fn dp(row: u32, col: u32) -> DisplayPoint { - DisplayPoint::new(DisplayRow(row), col) - } - - fn assert_active_match_index( - search_view: &WindowHandle, - cx: &mut TestAppContext, - expected_index: usize, - ) { - search_view - .update(cx, |search_view, _window, _cx| { - assert_eq!(search_view.active_match_index, Some(expected_index)); - }) - .unwrap(); - } - - fn assert_selection_range( - search_view: &WindowHandle, - cx: &mut TestAppContext, - expected_range: Range, - ) { - search_view - .update(cx, |search_view, _window, cx| { - assert_eq!( - search_view.results_editor.update(cx, |editor, cx| editor - .selections - .display_ranges(&editor.display_snapshot(cx))), - [expected_range] - ); - }) - .unwrap(); - } - - fn assert_highlights( - search_view: &WindowHandle, - cx: &mut TestAppContext, - expected_highlights: Vec<(Range, &str)>, - ) { - search_view - .update(cx, |search_view, window, cx| { - let match_bg = cx.theme().colors().search_match_background; - let active_match_bg = cx.theme().colors().search_active_match_background; - let selection_bg = cx - .theme() - .colors() - .editor_document_highlight_bracket_background; - - let highlights: Vec<_> = expected_highlights - .into_iter() - .map(|(range, color_type)| { - let color = match color_type { - "active" => active_match_bg, - "match" => match_bg, - "selection" => selection_bg, - _ => panic!("Unknown color type"), - }; - (range, color) - }) - .collect(); - - assert_eq!( - search_view.results_editor.update(cx, |editor, cx| editor - .all_text_background_highlights(window, cx)), - highlights.as_slice() - ); - }) - .unwrap(); - } - - fn select_match( - search_view: &WindowHandle, - cx: &mut TestAppContext, - direction: Direction, - ) { - search_view - .update(cx, |search_view, window, cx| { - search_view.select_match(direction, window, cx); - }) - .unwrap(); - } - - init_test(cx); - - // Override active search match color since the fallback theme uses the same color - // for normal search match and active one, which can make this test less robust. - cx.update(|cx| { - SettingsStore::update_global(cx, |settings, cx| { - settings.update_user_settings(cx, |settings| { - settings.theme.experimental_theme_overrides = Some(ThemeStyleContent { - colors: ThemeColorsContent { - search_active_match_background: Some("#ff0000ff".to_string()), - ..Default::default() - }, - ..Default::default() - }); - }); - }); - }); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/dir"), - json!({ - "one.rs": "const ONE: usize = 1;", - "two.rs": "const TWO: usize = one::ONE + one::ONE;", - "three.rs": "const THREE: usize = one::ONE + two::TWO;", - "four.rs": "const FOUR: usize = one::ONE + three::THREE;", - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let workspace = window.root(cx).unwrap(); - let search = cx.new(|cx| ProjectSearch::new(project.clone(), cx)); - let search_view = cx.add_window(|window, cx| { - ProjectSearchView::new(workspace.downgrade(), search.clone(), window, cx, None) - }); - - perform_search(search_view, "TWO", cx); - cx.run_until_parked(); - - search_view - .update(cx, |search_view, _window, cx| { - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;" - ); - }) - .unwrap(); - - assert_active_match_index(&search_view, cx, 0); - assert_selection_range(&search_view, cx, dp(2, 32)..dp(2, 35)); - assert_highlights( - &search_view, - cx, - vec![ - (dp(2, 32)..dp(2, 35), "active"), - (dp(2, 37)..dp(2, 40), "selection"), - (dp(2, 37)..dp(2, 40), "match"), - (dp(5, 6)..dp(5, 9), "match"), - // TODO: we should be getting selection highlight here after project search - // but for some reason we are not getting it here - ], - ); - select_match(&search_view, cx, Direction::Next); - cx.run_until_parked(); - - assert_active_match_index(&search_view, cx, 1); - assert_selection_range(&search_view, cx, dp(2, 37)..dp(2, 40)); - assert_highlights( - &search_view, - cx, - vec![ - (dp(2, 32)..dp(2, 35), "selection"), - (dp(2, 32)..dp(2, 35), "match"), - (dp(2, 37)..dp(2, 40), "active"), - (dp(5, 6)..dp(5, 9), "selection"), - (dp(5, 6)..dp(5, 9), "match"), - ], - ); - select_match(&search_view, cx, Direction::Next); - cx.run_until_parked(); - - assert_active_match_index(&search_view, cx, 2); - assert_selection_range(&search_view, cx, dp(5, 6)..dp(5, 9)); - assert_highlights( - &search_view, - cx, - vec![ - (dp(2, 32)..dp(2, 35), "selection"), - (dp(2, 32)..dp(2, 35), "match"), - (dp(2, 37)..dp(2, 40), "selection"), - (dp(2, 37)..dp(2, 40), "match"), - (dp(5, 6)..dp(5, 9), "active"), - ], - ); - select_match(&search_view, cx, Direction::Next); - cx.run_until_parked(); - - assert_active_match_index(&search_view, cx, 0); - assert_selection_range(&search_view, cx, dp(2, 32)..dp(2, 35)); - assert_highlights( - &search_view, - cx, - vec![ - (dp(2, 32)..dp(2, 35), "active"), - (dp(2, 37)..dp(2, 40), "selection"), - (dp(2, 37)..dp(2, 40), "match"), - (dp(5, 6)..dp(5, 9), "selection"), - (dp(5, 6)..dp(5, 9), "match"), - ], - ); - select_match(&search_view, cx, Direction::Prev); - cx.run_until_parked(); - - assert_active_match_index(&search_view, cx, 2); - assert_selection_range(&search_view, cx, dp(5, 6)..dp(5, 9)); - assert_highlights( - &search_view, - cx, - vec![ - (dp(2, 32)..dp(2, 35), "selection"), - (dp(2, 32)..dp(2, 35), "match"), - (dp(2, 37)..dp(2, 40), "selection"), - (dp(2, 37)..dp(2, 40), "match"), - (dp(5, 6)..dp(5, 9), "active"), - ], - ); - select_match(&search_view, cx, Direction::Prev); - cx.run_until_parked(); - - assert_active_match_index(&search_view, cx, 1); - assert_selection_range(&search_view, cx, dp(2, 37)..dp(2, 40)); - assert_highlights( - &search_view, - cx, - vec![ - (dp(2, 32)..dp(2, 35), "selection"), - (dp(2, 32)..dp(2, 35), "match"), - (dp(2, 37)..dp(2, 40), "active"), - (dp(5, 6)..dp(5, 9), "selection"), - (dp(5, 6)..dp(5, 9), "match"), - ], - ); - } - - #[perf] - #[gpui::test] - async fn test_deploy_project_search_focus(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/dir", - json!({ - "one.rs": "const ONE: usize = 1;", - "two.rs": "const TWO: usize = one::ONE + one::ONE;", - "three.rs": "const THREE: usize = one::ONE + two::TWO;", - "four.rs": "const FOUR: usize = one::ONE + three::THREE;", - }), - ) - .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let workspace = window; - let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new()); - - let active_item = cx.read(|cx| { - workspace - .read(cx) - .unwrap() - .active_pane() - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - }); - assert!( - active_item.is_none(), - "Expected no search panel to be active" - ); - - window - .update(cx, move |workspace, window, cx| { - assert_eq!(workspace.panes().len(), 1); - workspace.panes()[0].update(cx, |pane, cx| { - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) - }); - - ProjectSearchView::deploy_search( - workspace, - &workspace::DeploySearch::find(), - window, - cx, - ) - }) - .unwrap(); - - let Some(search_view) = cx.read(|cx| { - workspace - .read(cx) - .unwrap() - .active_pane() - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - }) else { - panic!("Search view expected to appear after new search event trigger") - }; - - cx.spawn(|mut cx| async move { - window - .update(&mut cx, |_, window, cx| { - window.dispatch_action(ToggleFocus.boxed_clone(), cx) - }) - .unwrap(); - }) - .detach(); - cx.background_executor.run_until_parked(); - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - assert!( - search_view.query_editor.focus_handle(cx).is_focused(window), - "Empty search view should be focused after the toggle focus event: no results panel to focus on", - ); - }); - }).unwrap(); - - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - let query_editor = &search_view.query_editor; - assert!( - query_editor.focus_handle(cx).is_focused(window), - "Search view should be focused after the new search view is activated", - ); - let query_text = query_editor.read(cx).text(cx); - assert!( - query_text.is_empty(), - "New search query should be empty but got '{query_text}'", - ); - let results_text = search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)); - assert!( - results_text.is_empty(), - "Empty search view should have no results but got '{results_text}'" - ); - }); - }) - .unwrap(); - - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text("sOMETHINGtHATsURELYdOESnOTeXIST", window, cx) - }); - search_view.search(cx); - }); - }) - .unwrap(); - cx.background_executor.run_until_parked(); - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - let results_text = search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)); - assert!( - results_text.is_empty(), - "Search view for mismatching query should have no results but got '{results_text}'" - ); - assert!( - search_view.query_editor.focus_handle(cx).is_focused(window), - "Search view should be focused after mismatching query had been used in search", - ); - }); - }).unwrap(); - - cx.spawn(|mut cx| async move { - window.update(&mut cx, |_, window, cx| { - window.dispatch_action(ToggleFocus.boxed_clone(), cx) - }) - }) - .detach(); - cx.background_executor.run_until_parked(); - window.update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - assert!( - search_view.query_editor.focus_handle(cx).is_focused(window), - "Search view with mismatching query should be focused after the toggle focus event: still no results panel to focus on", - ); - }); - }).unwrap(); - - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text("TWO", window, cx) - }); - search_view.search(cx); - }); - }) - .unwrap(); - cx.background_executor.run_until_parked(); - window.update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;", - "Search view results should match the query" - ); - assert!( - search_view.results_editor.focus_handle(cx).is_focused(window), - "Search view with mismatching query should be focused after search results are available", - ); - }); - }).unwrap(); - cx.spawn(|mut cx| async move { - window - .update(&mut cx, |_, window, cx| { - window.dispatch_action(ToggleFocus.boxed_clone(), cx) - }) - .unwrap(); - }) - .detach(); - cx.background_executor.run_until_parked(); - window.update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - assert!( - search_view.results_editor.focus_handle(cx).is_focused(window), - "Search view with matching query should still have its results editor focused after the toggle focus event", - ); - }); - }).unwrap(); - - workspace - .update(cx, |workspace, window, cx| { - ProjectSearchView::deploy_search( - workspace, - &workspace::DeploySearch::find(), - window, - cx, - ) - }) - .unwrap(); - window.update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "two", "Query should be updated to first search result after search view 2nd open in a row"); - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;", - "Results should be unchanged after search view 2nd open in a row" - ); - assert!( - search_view.query_editor.focus_handle(cx).is_focused(window), - "Focus should be moved into query editor again after search view 2nd open in a row" - ); - }); - }).unwrap(); - - cx.spawn(|mut cx| async move { - window - .update(&mut cx, |_, window, cx| { - window.dispatch_action(ToggleFocus.boxed_clone(), cx) - }) - .unwrap(); - }) - .detach(); - cx.background_executor.run_until_parked(); - window.update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - assert!( - search_view.results_editor.focus_handle(cx).is_focused(window), - "Search view with matching query should switch focus to the results editor after the toggle focus event", - ); - }); - }).unwrap(); - } - - #[perf] - #[gpui::test] - async fn test_filters_consider_toggle_state(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/dir", - json!({ - "one.rs": "const ONE: usize = 1;", - "two.rs": "const TWO: usize = one::ONE + one::ONE;", - "three.rs": "const THREE: usize = one::ONE + two::TWO;", - "four.rs": "const FOUR: usize = one::ONE + three::THREE;", - }), - ) - .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let workspace = window; - let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new()); - - window - .update(cx, move |workspace, window, cx| { - workspace.panes()[0].update(cx, |pane, cx| { - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) - }); - - ProjectSearchView::deploy_search( - workspace, - &workspace::DeploySearch::find(), - window, - cx, - ) - }) - .unwrap(); - - let Some(search_view) = cx.read(|cx| { - workspace - .read(cx) - .unwrap() - .active_pane() - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - }) else { - panic!("Search view expected to appear after new search event trigger") - }; - - cx.spawn(|mut cx| async move { - window - .update(&mut cx, |_, window, cx| { - window.dispatch_action(ToggleFocus.boxed_clone(), cx) - }) - .unwrap(); - }) - .detach(); - cx.background_executor.run_until_parked(); - - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text("const FOUR", window, cx) - }); - search_view.toggle_filters(cx); - search_view - .excluded_files_editor - .update(cx, |exclude_editor, cx| { - exclude_editor.set_text("four.rs", window, cx) - }); - search_view.search(cx); - }); - }) - .unwrap(); - cx.background_executor.run_until_parked(); - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - let results_text = search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)); - assert!( - results_text.is_empty(), - "Search view for query with the only match in an excluded file should have no results but got '{results_text}'" - ); - }); - }).unwrap(); - - cx.spawn(|mut cx| async move { - window.update(&mut cx, |_, window, cx| { - window.dispatch_action(ToggleFocus.boxed_clone(), cx) - }) - }) - .detach(); - cx.background_executor.run_until_parked(); - - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - search_view.toggle_filters(cx); - search_view.search(cx); - }); - }) - .unwrap(); - cx.background_executor.run_until_parked(); - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst FOUR: usize = one::ONE + three::THREE;", - "Search view results should contain the queried result in the previously excluded file with filters toggled off" - ); - }); - }) - .unwrap(); - } - - #[perf] - #[gpui::test] - async fn test_new_project_search_focus(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/dir"), - json!({ - "one.rs": "const ONE: usize = 1;", - "two.rs": "const TWO: usize = one::ONE + one::ONE;", - "three.rs": "const THREE: usize = one::ONE + two::TWO;", - "four.rs": "const FOUR: usize = one::ONE + three::THREE;", - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let workspace = window; - let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new()); - - let active_item = cx.read(|cx| { - workspace - .read(cx) - .unwrap() - .active_pane() - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - }); - assert!( - active_item.is_none(), - "Expected no search panel to be active" - ); - - window - .update(cx, move |workspace, window, cx| { - assert_eq!(workspace.panes().len(), 1); - workspace.panes()[0].update(cx, |pane, cx| { - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) - }); - - ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx) - }) - .unwrap(); - - let Some(search_view) = cx.read(|cx| { - workspace - .read(cx) - .unwrap() - .active_pane() - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - }) else { - panic!("Search view expected to appear after new search event trigger") - }; - - cx.spawn(|mut cx| async move { - window - .update(&mut cx, |_, window, cx| { - window.dispatch_action(ToggleFocus.boxed_clone(), cx) - }) - .unwrap(); - }) - .detach(); - cx.background_executor.run_until_parked(); - - window.update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - assert!( - search_view.query_editor.focus_handle(cx).is_focused(window), - "Empty search view should be focused after the toggle focus event: no results panel to focus on", - ); - }); - }).unwrap(); - - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - let query_editor = &search_view.query_editor; - assert!( - query_editor.focus_handle(cx).is_focused(window), - "Search view should be focused after the new search view is activated", - ); - let query_text = query_editor.read(cx).text(cx); - assert!( - query_text.is_empty(), - "New search query should be empty but got '{query_text}'", - ); - let results_text = search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)); - assert!( - results_text.is_empty(), - "Empty search view should have no results but got '{results_text}'" - ); - }); - }) - .unwrap(); - - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text("sOMETHINGtHATsURELYdOESnOTeXIST", window, cx) - }); - search_view.search(cx); - }); - }) - .unwrap(); - - cx.background_executor.run_until_parked(); - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - let results_text = search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)); - assert!( - results_text.is_empty(), - "Search view for mismatching query should have no results but got '{results_text}'" - ); - assert!( - search_view.query_editor.focus_handle(cx).is_focused(window), - "Search view should be focused after mismatching query had been used in search", - ); - }); - }) - .unwrap(); - cx.spawn(|mut cx| async move { - window.update(&mut cx, |_, window, cx| { - window.dispatch_action(ToggleFocus.boxed_clone(), cx) - }) - }) - .detach(); - cx.background_executor.run_until_parked(); - window.update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - assert!( - search_view.query_editor.focus_handle(cx).is_focused(window), - "Search view with mismatching query should be focused after the toggle focus event: still no results panel to focus on", - ); - }); - }).unwrap(); - - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text("TWO", window, cx) - }); - search_view.search(cx); - }) - }) - .unwrap(); - cx.background_executor.run_until_parked(); - window.update(cx, |_, window, cx| - search_view.update(cx, |search_view, cx| { - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;", - "Search view results should match the query" - ); - assert!( - search_view.results_editor.focus_handle(cx).is_focused(window), - "Search view with mismatching query should be focused after search results are available", - ); - })).unwrap(); - cx.spawn(|mut cx| async move { - window - .update(&mut cx, |_, window, cx| { - window.dispatch_action(ToggleFocus.boxed_clone(), cx) - }) - .unwrap(); - }) - .detach(); - cx.background_executor.run_until_parked(); - window.update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - assert!( - search_view.results_editor.focus_handle(cx).is_focused(window), - "Search view with matching query should still have its results editor focused after the toggle focus event", - ); - }); - }).unwrap(); - - workspace - .update(cx, |workspace, window, cx| { - ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx) - }) - .unwrap(); - cx.background_executor.run_until_parked(); - let Some(search_view_2) = cx.read(|cx| { - workspace - .read(cx) - .unwrap() - .active_pane() - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - }) else { - panic!("Search view expected to appear after new search event trigger") - }; - assert!( - search_view_2 != search_view, - "New search view should be open after `workspace::NewSearch` event" - ); - - window.update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO", "First search view should not have an updated query"); - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;", - "Results of the first search view should not update too" - ); - assert!( - !search_view.query_editor.focus_handle(cx).is_focused(window), - "Focus should be moved away from the first search view" - ); - }); - }).unwrap(); - - window.update(cx, |_, window, cx| { - search_view_2.update(cx, |search_view_2, cx| { - assert_eq!( - search_view_2.query_editor.read(cx).text(cx), - "two", - "New search view should get the query from the text cursor was at during the event spawn (first search view's first result)" - ); - assert_eq!( - search_view_2 - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "", - "No search results should be in the 2nd view yet, as we did not spawn a search for it" - ); - assert!( - search_view_2.query_editor.focus_handle(cx).is_focused(window), - "Focus should be moved into query editor of the new window" - ); - }); - }).unwrap(); - - window - .update(cx, |_, window, cx| { - search_view_2.update(cx, |search_view_2, cx| { - search_view_2.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text("FOUR", window, cx) - }); - search_view_2.search(cx); - }); - }) - .unwrap(); - - cx.background_executor.run_until_parked(); - window.update(cx, |_, window, cx| { - search_view_2.update(cx, |search_view_2, cx| { - assert_eq!( - search_view_2 - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst FOUR: usize = one::ONE + three::THREE;", - "New search view with the updated query should have new search results" - ); - assert!( - search_view_2.results_editor.focus_handle(cx).is_focused(window), - "Search view with mismatching query should be focused after search results are available", - ); - }); - }).unwrap(); - - cx.spawn(|mut cx| async move { - window - .update(&mut cx, |_, window, cx| { - window.dispatch_action(ToggleFocus.boxed_clone(), cx) - }) - .unwrap(); - }) - .detach(); - cx.background_executor.run_until_parked(); - window.update(cx, |_, window, cx| { - search_view_2.update(cx, |search_view_2, cx| { - assert!( - search_view_2.results_editor.focus_handle(cx).is_focused(window), - "Search view with matching query should switch focus to the results editor after the toggle focus event", - ); - });}).unwrap(); - } - - #[perf] - #[gpui::test] - async fn test_new_project_search_in_directory(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/dir"), - json!({ - "a": { - "one.rs": "const ONE: usize = 1;", - "two.rs": "const TWO: usize = one::ONE + one::ONE;", - }, - "b": { - "three.rs": "const THREE: usize = one::ONE + two::TWO;", - "four.rs": "const FOUR: usize = one::ONE + three::THREE;", - }, - }), - ) - .await; - let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - let worktree_id = project.read_with(cx, |project, cx| { - project.worktrees(cx).next().unwrap().read(cx).id() - }); - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let workspace = window.root(cx).unwrap(); - let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new()); - - let active_item = cx.read(|cx| { - workspace - .read(cx) - .active_pane() - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - }); - assert!( - active_item.is_none(), - "Expected no search panel to be active" - ); - - window - .update(cx, move |workspace, window, cx| { - assert_eq!(workspace.panes().len(), 1); - workspace.panes()[0].update(cx, move |pane, cx| { - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) - }); - }) - .unwrap(); - - let a_dir_entry = cx.update(|cx| { - workspace - .read(cx) - .project() - .read(cx) - .entry_for_path(&(worktree_id, rel_path("a")).into(), cx) - .expect("no entry for /a/ directory") - .clone() - }); - assert!(a_dir_entry.is_dir()); - window - .update(cx, |workspace, window, cx| { - ProjectSearchView::new_search_in_directory(workspace, &a_dir_entry.path, window, cx) - }) - .unwrap(); - - let Some(search_view) = cx.read(|cx| { - workspace - .read(cx) - .active_pane() - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - }) else { - panic!("Search view expected to appear after new search in directory event trigger") - }; - cx.background_executor.run_until_parked(); - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - assert!( - search_view.query_editor.focus_handle(cx).is_focused(window), - "On new search in directory, focus should be moved into query editor" - ); - search_view.excluded_files_editor.update(cx, |editor, cx| { - assert!( - editor.display_text(cx).is_empty(), - "New search in directory should not have any excluded files" - ); - }); - search_view.included_files_editor.update(cx, |editor, cx| { - assert_eq!( - editor.display_text(cx), - a_dir_entry.path.display(PathStyle::local()), - "New search in directory should have included dir entry path" - ); - }); - }); - }) - .unwrap(); - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text("const", window, cx) - }); - search_view.search(cx); - }); - }) - .unwrap(); - cx.background_executor.run_until_parked(); - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nconst ONE: usize = 1;\n\n\nconst TWO: usize = one::ONE + one::ONE;", - "New search in directory should have a filter that matches a certain directory" - ); - }) - }) - .unwrap(); - } - - #[perf] - #[gpui::test] - async fn test_search_query_history(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/dir"), - json!({ - "one.rs": "const ONE: usize = 1;", - "two.rs": "const TWO: usize = one::ONE + one::ONE;", - "three.rs": "const THREE: usize = one::ONE + two::TWO;", - "four.rs": "const FOUR: usize = one::ONE + three::THREE;", - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let workspace = window.root(cx).unwrap(); - let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new()); - - window - .update(cx, { - let search_bar = search_bar.clone(); - |workspace, window, cx| { - assert_eq!(workspace.panes().len(), 1); - workspace.panes()[0].update(cx, |pane, cx| { - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) - }); - - ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx) - } - }) - .unwrap(); - - let search_view = cx.read(|cx| { - workspace - .read(cx) - .active_pane() - .read(cx) - .active_item() - .and_then(|item| item.downcast::()) - .expect("Search view expected to appear after new search event trigger") - }); - - // Add 3 search items into the history + another unsubmitted one. - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - search_view.search_options = SearchOptions::CASE_SENSITIVE; - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text("ONE", window, cx) - }); - search_view.search(cx); - }); - }) - .unwrap(); - - cx.background_executor.run_until_parked(); - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text("TWO", window, cx) - }); - search_view.search(cx); - }); - }) - .unwrap(); - cx.background_executor.run_until_parked(); - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text("THREE", window, cx) - }); - search_view.search(cx); - }) - }) - .unwrap(); - cx.background_executor.run_until_parked(); - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text("JUST_TEXT_INPUT", window, cx) - }); - }) - }) - .unwrap(); - cx.background_executor.run_until_parked(); - - // Ensure that the latest input with search settings is active. - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!( - search_view.query_editor.read(cx).text(cx), - "JUST_TEXT_INPUT" - ); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - - // Next history query after the latest should set the query to the empty string. - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.focus_search(window, cx); - search_bar.next_history_query(&NextHistoryQuery, window, cx); - }) - }) - .unwrap(); - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), ""); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.focus_search(window, cx); - search_bar.next_history_query(&NextHistoryQuery, window, cx); - }) - }) - .unwrap(); - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), ""); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - - // First previous query for empty current query should set the query to the latest submitted one. - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.focus_search(window, cx); - search_bar.previous_history_query(&PreviousHistoryQuery, window, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - - // Further previous items should go over the history in reverse order. - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.focus_search(window, cx); - search_bar.previous_history_query(&PreviousHistoryQuery, window, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - - // Previous items should never go behind the first history item. - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.focus_search(window, cx); - search_bar.previous_history_query(&PreviousHistoryQuery, window, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.focus_search(window, cx); - search_bar.previous_history_query(&PreviousHistoryQuery, window, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - - // Next items should go over the history in the original order. - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.focus_search(window, cx); - search_bar.next_history_query(&NextHistoryQuery, window, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text("TWO_NEW", window, cx) - }); - search_view.search(cx); - }); - }) - .unwrap(); - cx.background_executor.run_until_parked(); - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - - // New search input should add another entry to history and move the selection to the end of the history. - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.focus_search(window, cx); - search_bar.previous_history_query(&PreviousHistoryQuery, window, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.focus_search(window, cx); - search_bar.previous_history_query(&PreviousHistoryQuery, window, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.focus_search(window, cx); - search_bar.next_history_query(&NextHistoryQuery, window, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.focus_search(window, cx); - search_bar.next_history_query(&NextHistoryQuery, window, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW"); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.focus_search(window, cx); - search_bar.next_history_query(&NextHistoryQuery, window, cx); - }); - }) - .unwrap(); - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - assert_eq!(search_view.query_editor.read(cx).text(cx), ""); - assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); - }); - }) - .unwrap(); - } - - #[perf] - #[gpui::test] - async fn test_search_query_history_with_multiple_views(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/dir"), - json!({ - "one.rs": "const ONE: usize = 1;", - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let worktree_id = project.update(cx, |this, cx| { - this.worktrees(cx).next().unwrap().read(cx).id() - }); - - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let workspace = window.root(cx).unwrap(); - - let panes: Vec<_> = window - .update(cx, |this, _, _| this.panes().to_owned()) - .unwrap(); - - let search_bar_1 = window.build_entity(cx, |_, _| ProjectSearchBar::new()); - let search_bar_2 = window.build_entity(cx, |_, _| ProjectSearchBar::new()); - - assert_eq!(panes.len(), 1); - let first_pane = panes.first().cloned().unwrap(); - assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 0); - window - .update(cx, |workspace, window, cx| { - workspace.open_path( - (worktree_id, rel_path("one.rs")), - Some(first_pane.downgrade()), - true, - window, - cx, - ) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 1); - - // Add a project search item to the first pane - window - .update(cx, { - let search_bar = search_bar_1.clone(); - |workspace, window, cx| { - first_pane.update(cx, |pane, cx| { - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) - }); - - ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx) - } - }) - .unwrap(); - let search_view_1 = cx.read(|cx| { - workspace - .read(cx) - .active_item(cx) - .and_then(|item| item.downcast::()) - .expect("Search view expected to appear after new search event trigger") - }); - - let second_pane = window - .update(cx, |workspace, window, cx| { - workspace.split_and_clone( - first_pane.clone(), - workspace::SplitDirection::Right, - window, - cx, - ) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 1); - - assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 1); - assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 2); - - // Add a project search item to the second pane - window - .update(cx, { - let search_bar = search_bar_2.clone(); - let pane = second_pane.clone(); - move |workspace, window, cx| { - assert_eq!(workspace.panes().len(), 2); - pane.update(cx, |pane, cx| { - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) - }); - - ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx) - } - }) - .unwrap(); - - let search_view_2 = cx.read(|cx| { - workspace - .read(cx) - .active_item(cx) - .and_then(|item| item.downcast::()) - .expect("Search view expected to appear after new search event trigger") - }); - - cx.run_until_parked(); - assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 2); - assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 2); - - let update_search_view = - |search_view: &Entity, query: &str, cx: &mut TestAppContext| { - window - .update(cx, |_, window, cx| { - search_view.update(cx, |search_view, cx| { - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text(query, window, cx) - }); - search_view.search(cx); - }); - }) - .unwrap(); - }; - - let active_query = - |search_view: &Entity, cx: &mut TestAppContext| -> String { - window - .update(cx, |_, _, cx| { - search_view.update(cx, |search_view, cx| { - search_view.query_editor.read(cx).text(cx) - }) - }) - .unwrap() - }; - - let select_prev_history_item = - |search_bar: &Entity, cx: &mut TestAppContext| { - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.focus_search(window, cx); - search_bar.previous_history_query(&PreviousHistoryQuery, window, cx); - }) - }) - .unwrap(); - }; - - let select_next_history_item = - |search_bar: &Entity, cx: &mut TestAppContext| { - window - .update(cx, |_, window, cx| { - search_bar.update(cx, |search_bar, cx| { - search_bar.focus_search(window, cx); - search_bar.next_history_query(&NextHistoryQuery, window, cx); - }) - }) - .unwrap(); - }; - - update_search_view(&search_view_1, "ONE", cx); - cx.background_executor.run_until_parked(); - - update_search_view(&search_view_2, "TWO", cx); - cx.background_executor.run_until_parked(); - - assert_eq!(active_query(&search_view_1, cx), "ONE"); - assert_eq!(active_query(&search_view_2, cx), "TWO"); - - // Selecting previous history item should select the query from search view 1. - select_prev_history_item(&search_bar_2, cx); - assert_eq!(active_query(&search_view_2, cx), "ONE"); - - // Selecting the previous history item should not change the query as it is already the first item. - select_prev_history_item(&search_bar_2, cx); - assert_eq!(active_query(&search_view_2, cx), "ONE"); - - // Changing the query in search view 2 should not affect the history of search view 1. - assert_eq!(active_query(&search_view_1, cx), "ONE"); - - // Deploying a new search in search view 2 - update_search_view(&search_view_2, "THREE", cx); - cx.background_executor.run_until_parked(); - - select_next_history_item(&search_bar_2, cx); - assert_eq!(active_query(&search_view_2, cx), ""); - - select_prev_history_item(&search_bar_2, cx); - assert_eq!(active_query(&search_view_2, cx), "THREE"); - - select_prev_history_item(&search_bar_2, cx); - assert_eq!(active_query(&search_view_2, cx), "TWO"); - - select_prev_history_item(&search_bar_2, cx); - assert_eq!(active_query(&search_view_2, cx), "ONE"); - - select_prev_history_item(&search_bar_2, cx); - assert_eq!(active_query(&search_view_2, cx), "ONE"); - - // Search view 1 should now see the query from search view 2. - assert_eq!(active_query(&search_view_1, cx), "ONE"); - - select_next_history_item(&search_bar_2, cx); - assert_eq!(active_query(&search_view_2, cx), "TWO"); - - // Here is the new query from search view 2 - select_next_history_item(&search_bar_2, cx); - assert_eq!(active_query(&search_view_2, cx), "THREE"); - - select_next_history_item(&search_bar_2, cx); - assert_eq!(active_query(&search_view_2, cx), ""); - - select_next_history_item(&search_bar_1, cx); - assert_eq!(active_query(&search_view_1, cx), "TWO"); - - select_next_history_item(&search_bar_1, cx); - assert_eq!(active_query(&search_view_1, cx), "THREE"); - - select_next_history_item(&search_bar_1, cx); - assert_eq!(active_query(&search_view_1, cx), ""); - } - - #[perf] - #[gpui::test] - async fn test_deploy_search_with_multiple_panes(cx: &mut TestAppContext) { - init_test(cx); - - // Setup 2 panes, both with a file open and one with a project search. - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/dir"), - json!({ - "one.rs": "const ONE: usize = 1;", - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let worktree_id = project.update(cx, |this, cx| { - this.worktrees(cx).next().unwrap().read(cx).id() - }); - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let panes: Vec<_> = window - .update(cx, |this, _, _| this.panes().to_owned()) - .unwrap(); - assert_eq!(panes.len(), 1); - let first_pane = panes.first().cloned().unwrap(); - assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 0); - window - .update(cx, |workspace, window, cx| { - workspace.open_path( - (worktree_id, rel_path("one.rs")), - Some(first_pane.downgrade()), - true, - window, - cx, - ) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 1); - let second_pane = window - .update(cx, |workspace, window, cx| { - workspace.split_and_clone( - first_pane.clone(), - workspace::SplitDirection::Right, - window, - cx, - ) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 1); - assert!( - window - .update(cx, |_, window, cx| second_pane - .focus_handle(cx) - .contains_focused(window, cx)) - .unwrap() - ); - let search_bar = window.build_entity(cx, |_, _| ProjectSearchBar::new()); - window - .update(cx, { - let search_bar = search_bar.clone(); - let pane = first_pane.clone(); - move |workspace, window, cx| { - assert_eq!(workspace.panes().len(), 2); - pane.update(cx, move |pane, cx| { - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) - }); - } - }) - .unwrap(); - - // Add a project search item to the second pane - window - .update(cx, { - |workspace, window, cx| { - assert_eq!(workspace.panes().len(), 2); - second_pane.update(cx, |pane, cx| { - pane.toolbar() - .update(cx, |toolbar, cx| toolbar.add_item(search_bar, window, cx)) - }); - - ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx) - } - }) - .unwrap(); - - cx.run_until_parked(); - assert_eq!(cx.update(|cx| second_pane.read(cx).items_len()), 2); - assert_eq!(cx.update(|cx| first_pane.read(cx).items_len()), 1); - - // Focus the first pane - window - .update(cx, |workspace, window, cx| { - 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); - assert_eq!(this.active_item_index(), 0); - }); - workspace.activate_pane_in_direction(workspace::SplitDirection::Left, window, cx); - }) - .unwrap(); - window - .update(cx, |workspace, _, cx| { - assert_eq!(workspace.active_pane(), &first_pane); - assert_eq!(first_pane.read(cx).items_len(), 1); - assert_eq!(second_pane.read(cx).items_len(), 2); - }) - .unwrap(); - - // Deploy a new search - cx.dispatch_action(window.into(), DeploySearch::find()); - - // Both panes should now have a project search in them - window - .update(cx, |workspace, window, cx| { - assert_eq!(workspace.active_pane(), &first_pane); - first_pane.read_with(cx, |this, _| { - assert_eq!(this.active_item_index(), 1); - assert_eq!(this.items_len(), 2); - }); - second_pane.update(cx, |this, cx| { - assert!(!cx.focus_handle().contains_focused(window, cx)); - assert_eq!(this.items_len(), 2); - }); - }) - .unwrap(); - - // 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) - }); - }) - .unwrap(); - - // Deploy a new search - cx.dispatch_action(window.into(), DeploySearch::find()); - - // The project search view should now be focused in the second pane - // And the number of items should be unchanged. - window - .update(cx, |_workspace, _, cx| { - second_pane.update(cx, |pane, _cx| { - assert!( - pane.active_item() - .unwrap() - .downcast::() - .is_some() - ); - - assert_eq!(pane.items_len(), 2); - }); - }) - .unwrap(); - } - - #[perf] - #[gpui::test] - async fn test_scroll_search_results_to_top(cx: &mut TestAppContext) { - init_test(cx); - - // We need many lines in the search results to be able to scroll the window - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/dir"), - json!({ - "1.txt": "\n\n\n\n\n A \n\n\n\n\n", - "2.txt": "\n\n\n\n\n A \n\n\n\n\n", - "3.rs": "\n\n\n\n\n A \n\n\n\n\n", - "4.rs": "\n\n\n\n\n A \n\n\n\n\n", - "5.rs": "\n\n\n\n\n A \n\n\n\n\n", - "6.rs": "\n\n\n\n\n A \n\n\n\n\n", - "7.rs": "\n\n\n\n\n A \n\n\n\n\n", - "8.rs": "\n\n\n\n\n A \n\n\n\n\n", - "9.rs": "\n\n\n\n\n A \n\n\n\n\n", - "a.rs": "\n\n\n\n\n A \n\n\n\n\n", - "b.rs": "\n\n\n\n\n B \n\n\n\n\n", - "c.rs": "\n\n\n\n\n B \n\n\n\n\n", - "d.rs": "\n\n\n\n\n B \n\n\n\n\n", - "e.rs": "\n\n\n\n\n B \n\n\n\n\n", - "f.rs": "\n\n\n\n\n B \n\n\n\n\n", - "g.rs": "\n\n\n\n\n B \n\n\n\n\n", - "h.rs": "\n\n\n\n\n B \n\n\n\n\n", - "i.rs": "\n\n\n\n\n B \n\n\n\n\n", - "j.rs": "\n\n\n\n\n B \n\n\n\n\n", - "k.rs": "\n\n\n\n\n B \n\n\n\n\n", - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let workspace = window.root(cx).unwrap(); - let search = cx.new(|cx| ProjectSearch::new(project, cx)); - let search_view = cx.add_window(|window, cx| { - ProjectSearchView::new(workspace.downgrade(), search.clone(), window, cx, None) - }); - - // First search - perform_search(search_view, "A", cx); - search_view - .update(cx, |search_view, window, cx| { - search_view.results_editor.update(cx, |results_editor, cx| { - // Results are correct and scrolled to the top - assert_eq!( - results_editor.display_text(cx).match_indices(" A ").count(), - 10 - ); - assert_eq!(results_editor.scroll_position(cx), Point::default()); - - // Scroll results all the way down - results_editor.scroll( - Point::new(0., f64::MAX), - Some(Axis::Vertical), - window, - cx, - ); - }); - }) - .expect("unable to update search view"); - - // Second search - perform_search(search_view, "B", cx); - search_view - .update(cx, |search_view, _, cx| { - search_view.results_editor.update(cx, |results_editor, cx| { - // Results are correct... - assert_eq!( - results_editor.display_text(cx).match_indices(" B ").count(), - 10 - ); - // ...and scrolled back to the top - assert_eq!(results_editor.scroll_position(cx), Point::default()); - }); - }) - .expect("unable to update search view"); - } - - #[perf] - #[gpui::test] - async fn test_buffer_search_query_reused(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/dir"), - json!({ - "one.rs": "const ONE: usize = 1;", - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let worktree_id = project.update(cx, |this, cx| { - this.worktrees(cx).next().unwrap().read(cx).id() - }); - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let workspace = window.root(cx).unwrap(); - let mut cx = VisualTestContext::from_window(*window.deref(), cx); - - let editor = workspace - .update_in(&mut cx, |workspace, window, cx| { - workspace.open_path((worktree_id, rel_path("one.rs")), None, true, window, cx) - }) - .await - .unwrap() - .downcast::() - .unwrap(); - - // Wait for the unstaged changes to be loaded - cx.run_until_parked(); - - let buffer_search_bar = cx.new_window_entity(|window, cx| { - let mut search_bar = - BufferSearchBar::new(Some(project.read(cx).languages().clone()), window, cx); - search_bar.set_active_pane_item(Some(&editor), window, cx); - search_bar.show(window, cx); - search_bar - }); - - let panes: Vec<_> = window - .update(&mut cx, |this, _, _| this.panes().to_owned()) - .unwrap(); - assert_eq!(panes.len(), 1); - let pane = panes.first().cloned().unwrap(); - pane.update_in(&mut cx, |pane, window, cx| { - pane.toolbar().update(cx, |toolbar, cx| { - toolbar.add_item(buffer_search_bar.clone(), window, cx); - }) - }); - - let buffer_search_query = "search bar query"; - buffer_search_bar - .update_in(&mut cx, |buffer_search_bar, window, cx| { - buffer_search_bar.focus_handle(cx).focus(window); - buffer_search_bar.search(buffer_search_query, None, true, window, cx) - }) - .await - .unwrap(); - - workspace.update_in(&mut cx, |workspace, window, cx| { - ProjectSearchView::new_search(workspace, &workspace::NewSearch, window, cx) - }); - cx.run_until_parked(); - let project_search_view = pane - .read_with(&cx, |pane, _| { - pane.active_item() - .and_then(|item| item.downcast::()) - }) - .expect("should open a project search view after spawning a new search"); - project_search_view.update(&mut cx, |search_view, cx| { - assert_eq!( - search_view.search_query_text(cx), - buffer_search_query, - "Project search should take the query from the buffer search bar since it got focused and had a query inside" - ); - }); - } - - #[gpui::test] - async fn test_search_dismisses_modal(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/dir"), - json!({ - "one.rs": "const ONE: usize = 1;", - }), - ) - .await; - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - struct EmptyModalView { - focus_handle: gpui::FocusHandle, - } - impl EventEmitter for EmptyModalView {} - impl Render for EmptyModalView { - fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement { - div() - } - } - impl Focusable for EmptyModalView { - fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle { - self.focus_handle.clone() - } - } - impl workspace::ModalView for EmptyModalView {} - - window - .update(cx, |workspace, window, cx| { - workspace.toggle_modal(window, cx, |_, cx| EmptyModalView { - focus_handle: cx.focus_handle(), - }); - assert!(workspace.has_active_modal(window, cx)); - }) - .unwrap(); - - cx.dispatch_action(window.into(), Deploy::find()); - - window - .update(cx, |workspace, window, cx| { - assert!(!workspace.has_active_modal(window, cx)); - workspace.toggle_modal(window, cx, |_, cx| EmptyModalView { - focus_handle: cx.focus_handle(), - }); - assert!(workspace.has_active_modal(window, cx)); - }) - .unwrap(); - - cx.dispatch_action(window.into(), DeploySearch::find()); - - window - .update(cx, |workspace, window, cx| { - assert!(!workspace.has_active_modal(window, cx)); - }) - .unwrap(); - } - - #[perf] - #[gpui::test] - async fn test_search_with_inlays(cx: &mut TestAppContext) { - init_test(cx); - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.all_languages.defaults.inlay_hints = - Some(InlayHintSettingsContent { - enabled: Some(true), - ..InlayHintSettingsContent::default() - }) - }); - }); - }); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - path!("/dir"), - // `\n` , a trailing line on the end, is important for the test case - json!({ - "main.rs": "fn main() { let a = 2; }\n", - }), - ) - .await; - - let requests_count = Arc::new(AtomicUsize::new(0)); - let closure_requests_count = requests_count.clone(); - let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await; - let language_registry = project.read_with(cx, |project, _| project.languages().clone()); - let language = rust_lang(); - language_registry.add(language); - let mut fake_servers = language_registry.register_fake_lsp( - "Rust", - FakeLspAdapter { - capabilities: lsp::ServerCapabilities { - inlay_hint_provider: Some(lsp::OneOf::Left(true)), - ..lsp::ServerCapabilities::default() - }, - initializer: Some(Box::new(move |fake_server| { - let requests_count = closure_requests_count.clone(); - fake_server.set_request_handler::({ - move |_, _| { - let requests_count = requests_count.clone(); - async move { - requests_count.fetch_add(1, atomic::Ordering::Release); - Ok(Some(vec![lsp::InlayHint { - position: lsp::Position::new(0, 17), - label: lsp::InlayHintLabel::String(": i32".to_owned()), - kind: Some(lsp::InlayHintKind::TYPE), - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - }])) - } - } - }); - })), - ..FakeLspAdapter::default() - }, - ); - - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let workspace = window.root(cx).unwrap(); - let search = cx.new(|cx| ProjectSearch::new(project.clone(), cx)); - let search_view = cx.add_window(|window, cx| { - ProjectSearchView::new(workspace.downgrade(), search.clone(), window, cx, None) - }); - - perform_search(search_view, "let ", cx); - let fake_server = fake_servers.next().await.unwrap(); - cx.executor().advance_clock(Duration::from_secs(1)); - cx.executor().run_until_parked(); - search_view - .update(cx, |search_view, _, cx| { - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nfn main() { let a: i32 = 2; }\n" - ); - }) - .unwrap(); - assert_eq!( - requests_count.load(atomic::Ordering::Acquire), - 1, - "New hints should have been queried", - ); - - // Can do the 2nd search without any panics - perform_search(search_view, "let ", cx); - cx.executor().advance_clock(Duration::from_secs(1)); - cx.executor().run_until_parked(); - search_view - .update(cx, |search_view, _, cx| { - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nfn main() { let a: i32 = 2; }\n" - ); - }) - .unwrap(); - assert_eq!( - requests_count.load(atomic::Ordering::Acquire), - 2, - "We did drop the previous buffer when cleared the old project search results, hence another query was made", - ); - - let singleton_editor = window - .update(cx, |workspace, window, cx| { - workspace.open_abs_path( - PathBuf::from(path!("/dir/main.rs")), - workspace::OpenOptions::default(), - window, - cx, - ) - }) - .unwrap() - .await - .unwrap() - .downcast::() - .unwrap(); - cx.executor().advance_clock(Duration::from_millis(100)); - cx.executor().run_until_parked(); - singleton_editor.update(cx, |editor, cx| { - assert_eq!( - editor.display_text(cx), - "fn main() { let a: i32 = 2; }\n", - "Newly opened editor should have the correct text with hints", - ); - }); - assert_eq!( - requests_count.load(atomic::Ordering::Acquire), - 2, - "Opening the same buffer again should reuse the cached hints", - ); - - window - .update(cx, |_, window, cx| { - singleton_editor.update(cx, |editor, cx| { - editor.handle_input("test", window, cx); - }); - }) - .unwrap(); - - cx.executor().advance_clock(Duration::from_secs(1)); - cx.executor().run_until_parked(); - singleton_editor.update(cx, |editor, cx| { - assert_eq!( - editor.display_text(cx), - "testfn main() { l: i32et a = 2; }\n", - "Newly opened editor should have the correct text with hints", - ); - }); - assert_eq!( - requests_count.load(atomic::Ordering::Acquire), - 3, - "We have edited the buffer and should send a new request", - ); - - window - .update(cx, |_, window, cx| { - singleton_editor.update(cx, |editor, cx| { - editor.undo(&editor::actions::Undo, window, cx); - }); - }) - .unwrap(); - cx.executor().advance_clock(Duration::from_secs(1)); - cx.executor().run_until_parked(); - assert_eq!( - requests_count.load(atomic::Ordering::Acquire), - 4, - "We have edited the buffer again and should send a new request again", - ); - singleton_editor.update(cx, |editor, cx| { - assert_eq!( - editor.display_text(cx), - "fn main() { let a: i32 = 2; }\n", - "Newly opened editor should have the correct text with hints", - ); - }); - project.update(cx, |_, cx| { - cx.emit(project::Event::RefreshInlayHints { - server_id: fake_server.server.server_id(), - request_id: Some(1), - }); - }); - cx.executor().advance_clock(Duration::from_secs(1)); - cx.executor().run_until_parked(); - assert_eq!( - requests_count.load(atomic::Ordering::Acquire), - 5, - "After a simulated server refresh request, we should have sent another request", - ); - - perform_search(search_view, "let ", cx); - cx.executor().advance_clock(Duration::from_secs(1)); - cx.executor().run_until_parked(); - assert_eq!( - requests_count.load(atomic::Ordering::Acquire), - 5, - "New project search should reuse the cached hints", - ); - search_view - .update(cx, |search_view, _, cx| { - assert_eq!( - search_view - .results_editor - .update(cx, |editor, cx| editor.display_text(cx)), - "\n\nfn main() { let a: i32 = 2; }\n" - ); - }) - .unwrap(); - } - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings = SettingsStore::test(cx); - cx.set_global(settings); - - theme::init(theme::LoadThemes::JustBase, cx); - - editor::init(cx); - crate::init(cx); - }); - } - - fn perform_search( - search_view: WindowHandle, - text: impl Into>, - cx: &mut TestAppContext, - ) { - search_view - .update(cx, |search_view, window, cx| { - search_view.query_editor.update(cx, |query_editor, cx| { - query_editor.set_text(text, window, cx) - }); - search_view.search(cx); - }) - .unwrap(); - // Ensure editor highlights appear after the search is done - cx.executor().advance_clock( - editor::SELECTION_HIGHLIGHT_DEBOUNCE_TIMEOUT + Duration::from_millis(100), - ); - cx.background_executor.run_until_parked(); - } -} diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs deleted file mode 100644 index 6663f8c318..0000000000 --- a/crates/search/src/search.rs +++ /dev/null @@ -1,204 +0,0 @@ -use bitflags::bitflags; -pub use buffer_search::BufferSearchBar; -use editor::SearchSettings; -use gpui::{Action, App, ClickEvent, FocusHandle, IntoElement, actions}; -use project::search::SearchQuery; -pub use project_search::ProjectSearchView; -use ui::{ButtonStyle, IconButton, IconButtonShape}; -use ui::{Tooltip, prelude::*}; -use workspace::notifications::NotificationId; -use workspace::{Toast, Workspace}; - -pub use search_status_button::SEARCH_ICON; - -use crate::project_search::ProjectSearchBar; - -pub mod buffer_search; -pub mod project_search; -pub(crate) mod search_bar; -pub mod search_status_button; - -pub fn init(cx: &mut App) { - menu::init(); - buffer_search::init(cx); - project_search::init(cx); -} - -actions!( - search, - [ - /// Focuses on the search input field. - FocusSearch, - /// Toggles whole word matching. - ToggleWholeWord, - /// Toggles case-sensitive search. - ToggleCaseSensitive, - /// Toggles searching in ignored files. - ToggleIncludeIgnored, - /// Toggles regular expression mode. - ToggleRegex, - /// Toggles the replace interface. - ToggleReplace, - /// Toggles searching within selection only. - ToggleSelection, - /// Selects the next search match. - SelectNextMatch, - /// Selects the previous search match. - SelectPreviousMatch, - /// Selects all search matches. - SelectAllMatches, - /// Cycles through search modes. - CycleMode, - /// Navigates to the next query in search history. - NextHistoryQuery, - /// Navigates to the previous query in search history. - PreviousHistoryQuery, - /// Replaces all matches. - ReplaceAll, - /// Replaces the next match. - ReplaceNext, - ] -); - -bitflags! { - #[derive(Debug, PartialEq, Eq, Clone, Copy, Default)] - pub struct SearchOptions: u8 { - const NONE = 0; - const WHOLE_WORD = 1 << SearchOption::WholeWord as u8; - const CASE_SENSITIVE = 1 << SearchOption::CaseSensitive as u8; - const INCLUDE_IGNORED = 1 << SearchOption::IncludeIgnored as u8; - const REGEX = 1 << SearchOption::Regex as u8; - const ONE_MATCH_PER_LINE = 1 << SearchOption::OneMatchPerLine as u8; - /// If set, reverse direction when finding the active match - const BACKWARDS = 1 << SearchOption::Backwards as u8; - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -#[repr(u8)] -pub enum SearchOption { - WholeWord = 0, - CaseSensitive, - IncludeIgnored, - Regex, - OneMatchPerLine, - Backwards, -} - -pub(crate) enum SearchSource<'a, 'b> { - Buffer, - Project(&'a Context<'b, ProjectSearchBar>), -} - -impl SearchOption { - pub fn as_options(&self) -> SearchOptions { - SearchOptions::from_bits(1 << *self as u8).unwrap() - } - - pub fn label(&self) -> &'static str { - match self { - SearchOption::WholeWord => "Match Whole Words", - SearchOption::CaseSensitive => "Match Case Sensitivity", - SearchOption::IncludeIgnored => "Also search files ignored by configuration", - SearchOption::Regex => "Use Regular Expressions", - SearchOption::OneMatchPerLine => "One Match Per Line", - SearchOption::Backwards => "Search Backwards", - } - } - - pub fn icon(&self) -> ui::IconName { - match self { - SearchOption::WholeWord => ui::IconName::WholeWord, - SearchOption::CaseSensitive => ui::IconName::CaseSensitive, - SearchOption::IncludeIgnored => ui::IconName::Sliders, - SearchOption::Regex => ui::IconName::Regex, - _ => panic!("{self:?} is not a named SearchOption"), - } - } - - pub fn to_toggle_action(self) -> &'static dyn Action { - match self { - SearchOption::WholeWord => &ToggleWholeWord, - SearchOption::CaseSensitive => &ToggleCaseSensitive, - SearchOption::IncludeIgnored => &ToggleIncludeIgnored, - SearchOption::Regex => &ToggleRegex, - _ => panic!("{self:?} is not a toggle action"), - } - } - - pub(crate) fn as_button( - &self, - active: SearchOptions, - search_source: SearchSource, - focus_handle: FocusHandle, - ) -> impl IntoElement { - let action = self.to_toggle_action(); - let label = self.label(); - IconButton::new( - (label, matches!(search_source, SearchSource::Buffer) as u32), - self.icon(), - ) - .map(|button| match search_source { - SearchSource::Buffer => { - let focus_handle = focus_handle.clone(); - button.on_click(move |_: &ClickEvent, window, cx| { - if !focus_handle.is_focused(window) { - window.focus(&focus_handle); - } - window.dispatch_action(action.boxed_clone(), cx); - }) - } - SearchSource::Project(cx) => { - let options = self.as_options(); - button.on_click(cx.listener(move |this, _: &ClickEvent, window, cx| { - this.toggle_search_option(options, window, cx); - })) - } - }) - .style(ButtonStyle::Subtle) - .shape(IconButtonShape::Square) - .toggle_state(active.contains(self.as_options())) - .tooltip(move |_window, cx| Tooltip::for_action_in(label, action, &focus_handle, cx)) - } -} - -impl SearchOptions { - pub fn none() -> SearchOptions { - SearchOptions::NONE - } - - pub fn from_query(query: &SearchQuery) -> SearchOptions { - let mut options = SearchOptions::NONE; - options.set(SearchOptions::WHOLE_WORD, query.whole_word()); - options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive()); - options.set(SearchOptions::INCLUDE_IGNORED, query.include_ignored()); - options.set(SearchOptions::REGEX, query.is_regex()); - options - } - - pub fn from_settings(settings: &SearchSettings) -> SearchOptions { - let mut options = SearchOptions::NONE; - options.set(SearchOptions::WHOLE_WORD, settings.whole_word); - options.set(SearchOptions::CASE_SENSITIVE, settings.case_sensitive); - options.set(SearchOptions::INCLUDE_IGNORED, settings.include_ignored); - options.set(SearchOptions::REGEX, settings.regex); - options - } -} - -pub(crate) fn show_no_more_matches(window: &mut Window, cx: &mut App) { - window.defer(cx, |window, cx| { - struct NotifType(); - let notification_id = NotificationId::unique::(); - - let Some(workspace) = window.root::().flatten() else { - return; - }; - workspace.update(cx, |workspace, cx| { - workspace.show_toast( - Toast::new(notification_id.clone(), "No more matches").autohide(), - cx, - ); - }) - }); -} diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs deleted file mode 100644 index 13b4df9574..0000000000 --- a/crates/search/src/search_bar.rs +++ /dev/null @@ -1,91 +0,0 @@ -use editor::{Editor, EditorElement, EditorStyle}; -use gpui::{Action, Entity, FocusHandle, Hsla, IntoElement, TextStyle}; -use settings::Settings; -use theme::ThemeSettings; -use ui::{IconButton, IconButtonShape}; -use ui::{Tooltip, prelude::*}; - -pub(super) enum ActionButtonState { - Disabled, - Toggled, -} - -pub(super) fn render_action_button( - id_prefix: &'static str, - icon: ui::IconName, - button_state: Option, - tooltip: &'static str, - action: &'static dyn Action, - focus_handle: FocusHandle, -) -> impl IntoElement { - IconButton::new( - SharedString::from(format!("{id_prefix}-{}", action.name())), - icon, - ) - .shape(IconButtonShape::Square) - .on_click({ - let focus_handle = focus_handle.clone(); - move |_, window, cx| { - if !focus_handle.is_focused(window) { - window.focus(&focus_handle); - } - window.dispatch_action(action.boxed_clone(), cx); - } - }) - .tooltip(move |_window, cx| Tooltip::for_action_in(tooltip, action, &focus_handle, cx)) - .when_some(button_state, |this, state| match state { - ActionButtonState::Toggled => this.toggle_state(true), - ActionButtonState::Disabled => this.disabled(true), - }) -} - -pub(crate) fn input_base_styles(border_color: Hsla, map: impl FnOnce(Div) -> Div) -> Div { - h_flex() - .map(map) - .min_w_32() - .h_8() - .pl_2() - .pr_1() - .border_1() - .border_color(border_color) - .rounded_md() -} - -pub(crate) fn render_text_input( - editor: &Entity, - color_override: Option, - app: &App, -) -> impl IntoElement { - let (color, use_syntax) = if editor.read(app).read_only(app) { - (app.theme().colors().text_disabled, false) - } else { - match color_override { - Some(color_override) => (color_override.color(app), false), - None => (app.theme().colors().text, true), - } - }; - - let settings = ThemeSettings::get_global(app); - let text_style = TextStyle { - color, - font_family: settings.buffer_font.family.clone(), - font_features: settings.buffer_font.features.clone(), - font_fallbacks: settings.buffer_font.fallbacks.clone(), - font_size: rems(0.875).into(), - font_weight: settings.buffer_font.weight, - line_height: relative(1.3), - ..TextStyle::default() - }; - - let mut editor_style = EditorStyle { - background: app.theme().colors().toolbar_background, - local_player: app.theme().players().local(), - text: text_style, - ..EditorStyle::default() - }; - if use_syntax { - editor_style.syntax = app.theme().syntax().clone(); - } - - EditorElement::new(editor, editor_style) -} diff --git a/crates/search/src/search_status_button.rs b/crates/search/src/search_status_button.rs deleted file mode 100644 index 712a322c10..0000000000 --- a/crates/search/src/search_status_button.rs +++ /dev/null @@ -1,45 +0,0 @@ -use editor::EditorSettings; -use settings::Settings as _; -use ui::{ButtonCommon, Clickable, Context, Render, Tooltip, Window, prelude::*}; -use workspace::{ItemHandle, StatusItemView}; - -pub const SEARCH_ICON: IconName = IconName::MagnifyingGlass; - -pub struct SearchButton; - -impl SearchButton { - pub fn new() -> Self { - Self {} - } -} - -impl Render for SearchButton { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl ui::IntoElement { - let button = div(); - - if !EditorSettings::get_global(cx).search.button { - return button.hidden(); - } - - button.child( - IconButton::new("project-search-indicator", SEARCH_ICON) - .icon_size(IconSize::Small) - .tooltip(|_window, cx| { - Tooltip::for_action("Project Search", &workspace::DeploySearch::default(), cx) - }) - .on_click(cx.listener(|_this, _, window, cx| { - window.dispatch_action(Box::new(workspace::DeploySearch::default()), cx); - })), - ) - } -} - -impl StatusItemView for SearchButton { - fn set_active_pane_item( - &mut self, - _active_pane_item: Option<&dyn ItemHandle>, - _window: &mut Window, - _cx: &mut Context, - ) { - } -} diff --git a/crates/session/Cargo.toml b/crates/session/Cargo.toml deleted file mode 100644 index 15c3acb8f0..0000000000 --- a/crates/session/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "session" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/session.rs" -doctest = false - -[features] -test-support = [ - "db/test-support", -] - -[dependencies] -db.workspace = true -gpui.workspace = true -uuid.workspace = true -util.workspace = true -serde_json.workspace = true diff --git a/crates/session/LICENSE-GPL b/crates/session/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/session/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/session/src/session.rs b/crates/session/src/session.rs deleted file mode 100644 index fd45982bf4..0000000000 --- a/crates/session/src/session.rs +++ /dev/null @@ -1,127 +0,0 @@ -use std::time::Duration; - -use db::kvp::KEY_VALUE_STORE; -use gpui::{App, AppContext as _, Context, Subscription, Task, WindowId}; -use util::ResultExt; - -pub struct Session { - session_id: String, - old_session_id: Option, - old_window_ids: Option>, -} - -const SESSION_ID_KEY: &str = "session_id"; -const SESSION_WINDOW_STACK_KEY: &str = "session_window_stack"; - -impl Session { - pub async fn new(session_id: String) -> Self { - let old_session_id = KEY_VALUE_STORE.read_kvp(SESSION_ID_KEY).ok().flatten(); - - KEY_VALUE_STORE - .write_kvp(SESSION_ID_KEY.to_string(), session_id.clone()) - .await - .log_err(); - - let old_window_ids = KEY_VALUE_STORE - .read_kvp(SESSION_WINDOW_STACK_KEY) - .ok() - .flatten() - .and_then(|json| serde_json::from_str::>(&json).ok()) - .map(|vec| { - vec.into_iter() - .map(WindowId::from) - .collect::>() - }); - - Self { - session_id, - old_session_id, - old_window_ids, - } - } - - #[cfg(any(test, feature = "test-support"))] - pub fn test() -> Self { - Self { - session_id: uuid::Uuid::new_v4().to_string(), - old_session_id: None, - old_window_ids: None, - } - } - - pub fn id(&self) -> &str { - &self.session_id - } -} - -pub struct AppSession { - session: Session, - _serialization_task: Task<()>, - _subscriptions: Vec, -} - -impl AppSession { - pub fn new(session: Session, cx: &Context) -> Self { - let _subscriptions = vec![cx.on_app_quit(Self::app_will_quit)]; - - let _serialization_task = cx.spawn(async move |_, cx| { - let mut current_window_stack = Vec::new(); - loop { - if let Some(windows) = cx.update(|cx| window_stack(cx)).ok().flatten() - && windows != current_window_stack - { - store_window_stack(&windows).await; - current_window_stack = windows; - } - - cx.background_executor() - .timer(Duration::from_millis(500)) - .await; - } - }); - - Self { - session, - _subscriptions, - _serialization_task, - } - } - - fn app_will_quit(&mut self, cx: &mut Context) -> Task<()> { - if let Some(window_stack) = window_stack(cx) { - cx.background_spawn(async move { store_window_stack(&window_stack).await }) - } else { - Task::ready(()) - } - } - - pub fn id(&self) -> &str { - self.session.id() - } - - pub fn last_session_id(&self) -> Option<&str> { - self.session.old_session_id.as_deref() - } - - pub fn last_session_window_stack(&self) -> Option> { - self.session.old_window_ids.clone() - } -} - -fn window_stack(cx: &App) -> Option> { - Some( - cx.window_stack()? - .into_iter() - .map(|window| window.window_id().as_u64()) - .collect(), - ) -} - -async fn store_window_stack(windows: &[u64]) { - if let Ok(window_ids_json) = serde_json::to_string(windows) { - KEY_VALUE_STORE - .write_kvp(SESSION_WINDOW_STACK_KEY.to_string(), window_ids_json) - .await - .log_err(); - } -} diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml deleted file mode 100644 index 1f1513d621..0000000000 --- a/crates/settings/Cargo.toml +++ /dev/null @@ -1,50 +0,0 @@ -[package] -name = "settings" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/settings.rs" -doctest = false - -[features] -test-support = ["gpui/test-support", "fs/test-support"] - -[dependencies] -anyhow.workspace = true -collections.workspace = true -derive_more.workspace = true -ec4rs.workspace = true -fs.workspace = true -futures.workspace = true -gpui.workspace = true -inventory.workspace = true -log.workspace = true -migrator.workspace = true -paths.workspace = true -release_channel.workspace = true -rust-embed.workspace = true -schemars.workspace = true -serde.workspace = true -serde_json.workspace = true -serde_json_lenient.workspace = true -serde_repr.workspace = true -settings_json.workspace = true -settings_macros.workspace = true -smallvec.workspace = true -strum.workspace = true -util.workspace = true -zlog.workspace = true - -[dev-dependencies] -fs = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } - -indoc.workspace = true -pretty_assertions.workspace = true -unindent.workspace = true diff --git a/crates/settings/LICENSE-GPL b/crates/settings/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/settings/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/settings/src/base_keymap_setting.rs b/crates/settings/src/base_keymap_setting.rs deleted file mode 100644 index 8e872dae40..0000000000 --- a/crates/settings/src/base_keymap_setting.rs +++ /dev/null @@ -1,135 +0,0 @@ -use std::fmt::{Display, Formatter}; - -use crate::{self as settings, settings_content::BaseKeymapContent}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings::{RegisterSetting, Settings}; - -/// Base key bindings scheme. Base keymaps can be overridden with user keymaps. -/// -/// Default: VSCode -#[derive( - Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default, RegisterSetting, -)] -pub enum BaseKeymap { - #[default] - VSCode, - JetBrains, - SublimeText, - Atom, - TextMate, - Emacs, - Cursor, - None, -} - -impl From for BaseKeymap { - fn from(value: BaseKeymapContent) -> Self { - match value { - BaseKeymapContent::VSCode => Self::VSCode, - BaseKeymapContent::JetBrains => Self::JetBrains, - BaseKeymapContent::SublimeText => Self::SublimeText, - BaseKeymapContent::Atom => Self::Atom, - BaseKeymapContent::TextMate => Self::TextMate, - BaseKeymapContent::Emacs => Self::Emacs, - BaseKeymapContent::Cursor => Self::Cursor, - BaseKeymapContent::None => Self::None, - } - } -} -impl Into for BaseKeymap { - fn into(self) -> BaseKeymapContent { - match self { - BaseKeymap::VSCode => BaseKeymapContent::VSCode, - BaseKeymap::JetBrains => BaseKeymapContent::JetBrains, - BaseKeymap::SublimeText => BaseKeymapContent::SublimeText, - BaseKeymap::Atom => BaseKeymapContent::Atom, - BaseKeymap::TextMate => BaseKeymapContent::TextMate, - BaseKeymap::Emacs => BaseKeymapContent::Emacs, - BaseKeymap::Cursor => BaseKeymapContent::Cursor, - BaseKeymap::None => BaseKeymapContent::None, - } - } -} - -impl Display for BaseKeymap { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - BaseKeymap::VSCode => write!(f, "VS Code"), - BaseKeymap::JetBrains => write!(f, "JetBrains"), - BaseKeymap::SublimeText => write!(f, "Sublime Text"), - BaseKeymap::Atom => write!(f, "Atom"), - BaseKeymap::TextMate => write!(f, "TextMate"), - BaseKeymap::Emacs => write!(f, "Emacs (beta)"), - BaseKeymap::Cursor => write!(f, "Cursor (beta)"), - BaseKeymap::None => write!(f, "None"), - } - } -} - -impl BaseKeymap { - #[cfg(target_os = "macos")] - pub const OPTIONS: [(&'static str, Self); 7] = [ - ("VS Code (Default)", Self::VSCode), - ("Atom", Self::Atom), - ("JetBrains", Self::JetBrains), - ("Sublime Text", Self::SublimeText), - ("Emacs (beta)", Self::Emacs), - ("TextMate", Self::TextMate), - ("Cursor", Self::Cursor), - ]; - - #[cfg(not(target_os = "macos"))] - pub const OPTIONS: [(&'static str, Self); 6] = [ - ("VS Code (Default)", Self::VSCode), - ("Atom", Self::Atom), - ("JetBrains", Self::JetBrains), - ("Sublime Text", Self::SublimeText), - ("Emacs (beta)", Self::Emacs), - ("Cursor", Self::Cursor), - ]; - - pub fn asset_path(&self) -> Option<&'static str> { - #[cfg(target_os = "macos")] - match self { - BaseKeymap::JetBrains => Some("keymaps/macos/jetbrains.json"), - BaseKeymap::SublimeText => Some("keymaps/macos/sublime_text.json"), - BaseKeymap::Atom => Some("keymaps/macos/atom.json"), - BaseKeymap::TextMate => Some("keymaps/macos/textmate.json"), - BaseKeymap::Emacs => Some("keymaps/macos/emacs.json"), - BaseKeymap::Cursor => Some("keymaps/macos/cursor.json"), - BaseKeymap::VSCode => None, - BaseKeymap::None => None, - } - - #[cfg(not(target_os = "macos"))] - match self { - BaseKeymap::JetBrains => Some("keymaps/linux/jetbrains.json"), - BaseKeymap::SublimeText => Some("keymaps/linux/sublime_text.json"), - BaseKeymap::Atom => Some("keymaps/linux/atom.json"), - BaseKeymap::Emacs => Some("keymaps/linux/emacs.json"), - BaseKeymap::Cursor => Some("keymaps/linux/cursor.json"), - BaseKeymap::TextMate => None, - BaseKeymap::VSCode => None, - BaseKeymap::None => None, - } - } - - pub fn names() -> impl Iterator { - Self::OPTIONS.iter().map(|(name, _)| *name) - } - - pub fn from_names(option: &str) -> BaseKeymap { - Self::OPTIONS - .iter() - .copied() - .find_map(|(name, value)| (name == option).then_some(value)) - .unwrap_or_default() - } -} - -impl Settings for BaseKeymap { - fn from_settings(s: &crate::settings_content::SettingsContent) -> Self { - s.base_keymap.unwrap().into() - } -} diff --git a/crates/settings/src/editable_setting_control.rs b/crates/settings/src/editable_setting_control.rs deleted file mode 100644 index fd9d986962..0000000000 --- a/crates/settings/src/editable_setting_control.rs +++ /dev/null @@ -1,30 +0,0 @@ -use fs::Fs; -use gpui::{App, RenderOnce, SharedString}; - -use crate::{settings_content::SettingsContent, update_settings_file}; - -/// A UI control that can be used to edit a setting. -pub trait EditableSettingControl: RenderOnce { - /// The type of the setting value. - type Value: Send; - - /// Returns the name of this setting. - fn name(&self) -> SharedString; - - /// Reads the setting value from the settings. - fn read(cx: &App) -> Self::Value; - - /// Applies the given setting file to the settings file contents. - /// - /// This will be called when writing the setting value back to the settings file. - fn apply(settings: &mut SettingsContent, value: Self::Value, cx: &App); - - /// Writes the given setting value to the settings files. - fn write(value: Self::Value, cx: &App) { - let fs = ::global(cx); - - update_settings_file(fs, cx, move |settings, cx| { - Self::apply(settings, value, cx); - }); - } -} diff --git a/crates/settings/src/fallible_options.rs b/crates/settings/src/fallible_options.rs deleted file mode 100644 index e0eea451f1..0000000000 --- a/crates/settings/src/fallible_options.rs +++ /dev/null @@ -1,112 +0,0 @@ -use std::cell::RefCell; - -use serde::Deserialize; - -use crate::ParseStatus; - -thread_local! { - static ERRORS: RefCell>> = const { RefCell::new(None) }; -} - -pub(crate) fn parse_json<'de, T>(json: &'de str) -> (Option, ParseStatus) -where - T: Deserialize<'de>, -{ - ERRORS.with_borrow_mut(|errors| { - errors.replace(Vec::default()); - }); - - let mut deserializer = serde_json_lenient::Deserializer::from_str(json); - let value = T::deserialize(&mut deserializer); - let value = match value { - Ok(value) => value, - Err(error) => { - return ( - None, - ParseStatus::Failed { - error: error.to_string(), - }, - ); - } - }; - - if let Some(errors) = ERRORS.with_borrow_mut(|errors| errors.take().filter(|e| !e.is_empty())) { - let error = errors - .into_iter() - .map(|e| e.to_string()) - .flat_map(|e| ["\n".to_owned(), e]) - .skip(1) - .collect::(); - return (Some(value), ParseStatus::Failed { error }); - } - - (Some(value), ParseStatus::Success) -} - -pub(crate) fn deserialize<'de, D, T>(deserializer: D) -> Result -where - D: serde::Deserializer<'de>, - T: serde::Deserialize<'de> + FallibleOption, -{ - match T::deserialize(deserializer) { - Ok(value) => Ok(value), - Err(e) => ERRORS.with_borrow_mut(|errors| { - if let Some(errors) = errors { - errors.push(anyhow::anyhow!("{}", e)); - Ok(Default::default()) - } else { - Err(e) - } - }), - } -} - -pub trait FallibleOption: Default {} -impl FallibleOption for Option {} - -#[cfg(test)] -mod tests { - use serde::Deserialize; - use settings_macros::with_fallible_options; - - use crate::ParseStatus; - - #[with_fallible_options] - #[derive(Deserialize, Debug, PartialEq)] - struct Foo { - foo: Option, - bar: Option, - baz: Option, - } - - #[test] - fn test_fallible() { - let input = r#" - {"foo": "bar", - "bar": "foo", - "baz": 3, - } - "#; - - let (settings, result) = crate::fallible_options::parse_json::(&input); - assert_eq!( - settings.unwrap(), - Foo { - foo: Some("bar".into()), - bar: None, - baz: None, - } - ); - - assert!(crate::parse_json_with_comments::(&input).is_err()); - - let ParseStatus::Failed { error } = result else { - panic!("Expected parse to fail") - }; - - assert_eq!( - error, - "invalid type: string \"foo\", expected usize at line 3 column 24\ninvalid type: integer `3`, expected a boolean at line 4 column 20".to_string() - ) - } -} diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs deleted file mode 100644 index 2ef1dfc538..0000000000 --- a/crates/settings/src/keymap_file.rs +++ /dev/null @@ -1,1971 +0,0 @@ -use anyhow::{Context as _, Result}; -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, register_action, -}; -use schemars::{JsonSchema, json_schema}; -use serde::Deserialize; -use serde_json::{Value, json}; -use std::borrow::Cow; -use std::{any::TypeId, fmt::Write, rc::Rc, sync::Arc, sync::LazyLock}; -use util::ResultExt as _; -use util::{ - asset_str, - markdown::{MarkdownEscaped, MarkdownInlineCode, MarkdownString}, - schemars::AllowTrailingCommas, -}; - -use crate::SettingsAssets; -use settings_json::{ - append_top_level_array_value_in_json_text, parse_json_with_comments, - replace_top_level_array_value_in_json_text, -}; - -pub trait KeyBindingValidator: Send + Sync { - fn action_type_id(&self) -> TypeId; - fn validate(&self, binding: &KeyBinding) -> Result<(), MarkdownString>; -} - -pub struct KeyBindingValidatorRegistration(pub fn() -> Box); - -inventory::collect!(KeyBindingValidatorRegistration); - -pub(crate) static KEY_BINDING_VALIDATORS: LazyLock>> = - LazyLock::new(|| { - let mut validators = BTreeMap::new(); - for validator_registration in inventory::iter:: { - let validator = validator_registration.0(); - validators.insert(validator.action_type_id(), validator); - } - validators - }); - -// Note that the doc comments on these are shown by json-language-server when editing the keymap, so -// they should be considered user-facing documentation. Documentation is not handled well with -// schemars-0.8 - when there are newlines, it is rendered as plaintext (see -// https://github.com/GREsau/schemars/issues/38#issuecomment-2282883519). So for now these docs -// avoid newlines. -// -// TODO: Update to schemars-1.0 once it's released, and add more docs as newlines would be -// supported. Tracking issue is https://github.com/GREsau/schemars/issues/112. - -/// Keymap configuration consisting of sections. Each section may have a context predicate which -/// determines whether its bindings are used. -#[derive(Debug, Deserialize, Default, Clone, JsonSchema)] -#[serde(transparent)] -pub struct KeymapFile(Vec); - -/// Keymap section which binds keystrokes to actions. -#[derive(Debug, Deserialize, Default, Clone, JsonSchema)] -pub struct KeymapSection { - /// Determines when these bindings are active. When just a name is provided, like `Editor` or - /// `Workspace`, the bindings will be active in that context. Boolean expressions like `X && Y`, - /// `X || Y`, `!X` are also supported. Some more complex logic including checking OS and the - /// current file extension are also supported - see [the - /// documentation](https://zed.dev/docs/key-bindings#contexts) for more details. - #[serde(default)] - pub context: String, - /// This option enables specifying keys based on their position on a QWERTY keyboard, by using - /// position-equivalent mappings for some non-QWERTY keyboards. This is currently only supported - /// on macOS. See the documentation for more details. - #[serde(default)] - use_key_equivalents: bool, - /// This keymap section's bindings, as a JSON object mapping keystrokes to actions. The - /// keystrokes key is a string representing a sequence of keystrokes to type, where the - /// keystrokes are separated by whitespace. Each keystroke is a sequence of modifiers (`ctrl`, - /// `alt`, `shift`, `fn`, `cmd`, `super`, or `win`) followed by a key, separated by `-`. The - /// order of bindings does matter. When the same keystrokes are bound at the same context depth, - /// the binding that occurs later in the file is preferred. For displaying keystrokes in the UI, - /// the later binding for the same action is preferred. - #[serde(default)] - bindings: Option>, - #[serde(flatten)] - unrecognized_fields: IndexMap, - // This struct intentionally uses permissive types for its fields, rather than validating during - // deserialization. The purpose of this is to allow loading the portion of the keymap that doesn't - // have errors. The downside of this is that the errors are not reported with line+column info. - // Unfortunately the implementations of the `Spanned` types for preserving this information are - // highly inconvenient (`serde_spanned`) and in some cases don't work at all here - // (`json_spanned_>value`). Serde should really have builtin support for this. -} - -impl KeymapSection { - pub fn bindings(&self) -> impl DoubleEndedIterator { - self.bindings.iter().flatten() - } -} - -/// Keymap action as a JSON value, since it can either be null for no action, or the name of the -/// action, or an array of the name of the action and the action input. -/// -/// Unlike the other json types involved in keymaps (including actions), this doc-comment will not -/// be included in the generated JSON schema, as it manually defines its `JsonSchema` impl. The -/// actual schema used for it is automatically generated in `KeymapFile::generate_json_schema`. -#[derive(Debug, Deserialize, Default, Clone)] -#[serde(transparent)] -pub struct KeymapAction(Value); - -impl std::fmt::Display for KeymapAction { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match &self.0 { - Value::String(s) => write!(f, "{}", s), - Value::Array(arr) => { - let strings: Vec = arr.iter().map(|v| v.to_string()).collect(); - write!(f, "{}", strings.join(", ")) - } - _ => write!(f, "{}", self.0), - } - } -} - -impl JsonSchema for KeymapAction { - /// This is used when generating the JSON schema for the `KeymapAction` type, so that it can - /// reference the keymap action schema. - fn schema_name() -> Cow<'static, str> { - "KeymapAction".into() - } - - /// This schema will be replaced with the full action schema in - /// `KeymapFile::generate_json_schema`. - fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { - json_schema!(true) - } -} - -#[derive(Debug)] -#[must_use] -pub enum KeymapFileLoadResult { - Success { - key_bindings: Vec, - }, - SomeFailedToLoad { - key_bindings: Vec, - error_message: MarkdownString, - }, - JsonParseFailure { - error: anyhow::Error, - }, -} - -impl KeymapFile { - pub fn parse(content: &str) -> anyhow::Result { - if content.trim().is_empty() { - return Ok(Self(Vec::new())); - } - parse_json_with_comments::(content) - } - - pub fn load_asset( - asset_path: &str, - source: Option, - cx: &App, - ) -> anyhow::Result> { - match Self::load(asset_str::(asset_path).as_ref(), cx) { - KeymapFileLoadResult::Success { mut key_bindings } => match source { - Some(source) => Ok({ - for key_binding in &mut key_bindings { - key_binding.set_meta(source.meta()); - } - key_bindings - }), - None => Ok(key_bindings), - }, - KeymapFileLoadResult::SomeFailedToLoad { error_message, .. } => { - anyhow::bail!("Error loading built-in keymap \"{asset_path}\": {error_message}",) - } - KeymapFileLoadResult::JsonParseFailure { error } => { - anyhow::bail!("JSON parse error in built-in keymap \"{asset_path}\": {error}") - } - } - } - - pub fn load_asset_allow_partial_failure( - asset_path: &str, - cx: &App, - ) -> anyhow::Result> { - match Self::load(asset_str::(asset_path).as_ref(), cx) { - KeymapFileLoadResult::SomeFailedToLoad { - key_bindings, - error_message, - .. - } if key_bindings.is_empty() => { - anyhow::bail!("Error loading built-in keymap \"{asset_path}\": {error_message}",) - } - KeymapFileLoadResult::Success { key_bindings, .. } - | KeymapFileLoadResult::SomeFailedToLoad { key_bindings, .. } => Ok(key_bindings), - KeymapFileLoadResult::JsonParseFailure { error } => { - anyhow::bail!("JSON parse error in built-in keymap \"{asset_path}\": {error}") - } - } - } - - #[cfg(feature = "test-support")] - pub fn load_panic_on_failure(content: &str, cx: &App) -> Vec { - match Self::load(content, cx) { - KeymapFileLoadResult::Success { key_bindings, .. } => key_bindings, - KeymapFileLoadResult::SomeFailedToLoad { error_message, .. } => { - panic!("{error_message}"); - } - KeymapFileLoadResult::JsonParseFailure { error } => { - panic!("JSON parse error: {error}"); - } - } - } - - pub fn load(content: &str, cx: &App) -> KeymapFileLoadResult { - let keymap_file = match Self::parse(content) { - Ok(keymap_file) => keymap_file, - Err(error) => { - return KeymapFileLoadResult::JsonParseFailure { error }; - } - }; - - // Accumulate errors in order to support partial load of user keymap in the presence of - // errors in context and binding parsing. - let mut errors = Vec::new(); - let mut key_bindings = Vec::new(); - - for KeymapSection { - context, - use_key_equivalents, - bindings, - unrecognized_fields, - } in keymap_file.0.iter() - { - let context_predicate: Option> = if context.is_empty() { - None - } else { - match KeyBindingContextPredicate::parse(context) { - Ok(context_predicate) => Some(context_predicate.into()), - Err(err) => { - // Leading space is to separate from the message indicating which section - // the error occurred in. - errors.push(( - context, - format!(" Parse error in section `context` field: {}", err), - )); - continue; - } - } - }; - - let mut section_errors = String::new(); - - if !unrecognized_fields.is_empty() { - write!( - section_errors, - "\n\n - Unrecognized fields: {}", - MarkdownInlineCode(&format!("{:?}", unrecognized_fields.keys())) - ) - .unwrap(); - } - - if let Some(bindings) = bindings { - for (keystrokes, action) in bindings { - let result = Self::load_keybinding( - keystrokes, - action, - context_predicate.clone(), - *use_key_equivalents, - cx, - ); - match result { - Ok(key_binding) => { - key_bindings.push(key_binding); - } - Err(err) => { - let mut lines = err.lines(); - let mut indented_err = lines.next().unwrap().to_string(); - for line in lines { - indented_err.push_str(" "); - indented_err.push_str(line); - indented_err.push_str("\n"); - } - write!( - section_errors, - "\n\n- In binding {}, {indented_err}", - MarkdownInlineCode(&format!("\"{}\"", keystrokes)) - ) - .unwrap(); - } - } - } - } - - if !section_errors.is_empty() { - errors.push((context, section_errors)) - } - } - - if errors.is_empty() { - KeymapFileLoadResult::Success { key_bindings } - } else { - let mut error_message = "Errors in user keymap file.\n".to_owned(); - for (context, section_errors) in errors { - if context.is_empty() { - let _ = write!(error_message, "\n\nIn section without context predicate:"); - } else { - let _ = write!( - error_message, - "\n\nIn section with {}:", - MarkdownInlineCode(&format!("context = \"{}\"", context)) - ); - } - let _ = write!(error_message, "{section_errors}"); - } - KeymapFileLoadResult::SomeFailedToLoad { - key_bindings, - error_message: MarkdownString(error_message), - } - } - } - - fn load_keybinding( - keystrokes: &str, - action: &KeymapAction, - context: Option>, - use_key_equivalents: bool, - cx: &App, - ) -> std::result::Result { - let (action, action_input_string) = Self::build_keymap_action(action, cx)?; - - let key_binding = match KeyBinding::load( - keystrokes, - action, - context, - use_key_equivalents, - action_input_string.map(SharedString::from), - cx.keyboard_mapper().as_ref(), - ) { - Ok(key_binding) => key_binding, - Err(InvalidKeystrokeError { keystroke }) => { - return Err(format!( - "invalid keystroke {}. {}", - MarkdownInlineCode(&format!("\"{}\"", &keystroke)), - KEYSTROKE_PARSE_EXPECTED_MESSAGE - )); - } - }; - - if let Some(validator) = KEY_BINDING_VALIDATORS.get(&key_binding.action().type_id()) { - match validator.validate(&key_binding) { - Ok(()) => Ok(key_binding), - Err(error) => Err(error.0), - } - } else { - Ok(key_binding) - } - } - - pub fn parse_action( - action: &KeymapAction, - ) -> Result)>, String> { - let name_and_input = match &action.0 { - Value::Array(items) => { - if items.len() != 2 { - return Err(format!( - "expected two-element array of `[name, input]`. \ - Instead found {}.", - MarkdownInlineCode(&action.0.to_string()) - )); - } - let serde_json::Value::String(ref name) = items[0] else { - return Err(format!( - "expected two-element array of `[name, input]`, \ - but the first element is not a string in {}.", - MarkdownInlineCode(&action.0.to_string()) - )); - }; - Some((name, Some(&items[1]))) - } - Value::String(name) => Some((name, None)), - Value::Null => None, - _ => { - return Err(format!( - "expected two-element array of `[name, input]`. \ - Instead found {}.", - MarkdownInlineCode(&action.0.to_string()) - )); - } - }; - Ok(name_and_input) - } - - fn build_keymap_action( - action: &KeymapAction, - cx: &App, - ) -> std::result::Result<(Box, Option), String> { - let (build_result, action_input_string) = match Self::parse_action(action)? { - Some((name, action_input)) if name.as_str() == ActionSequence::name_for_type() => { - match action_input { - Some(action_input) => ( - ActionSequence::build_sequence(action_input.clone(), cx), - None, - ), - None => (Err(ActionSequence::expected_array_error()), None), - } - } - Some((name, Some(action_input))) => { - let action_input_string = action_input.to_string(); - ( - cx.build_action(name, Some(action_input.clone())), - Some(action_input_string), - ) - } - Some((name, None)) => (cx.build_action(name, None), None), - None => (Ok(NoAction.boxed_clone()), None), - }; - - let action = match build_result { - Ok(action) => action, - Err(ActionBuildError::NotFound { name }) => { - return Err(format!( - "didn't find an action named {}.", - MarkdownInlineCode(&format!("\"{}\"", &name)) - )); - } - Err(ActionBuildError::BuildError { name, error }) => match action_input_string { - Some(action_input_string) => { - return Err(format!( - "can't build {} action from input value {}: {}", - MarkdownInlineCode(&format!("\"{}\"", &name)), - MarkdownInlineCode(&action_input_string), - MarkdownEscaped(&error.to_string()) - )); - } - None => { - return Err(format!( - "can't build {} action - it requires input data via [name, input]: {}", - MarkdownInlineCode(&format!("\"{}\"", &name)), - MarkdownEscaped(&error.to_string()) - )); - } - }, - }; - - Ok((action, action_input_string)) - } - - /// Creates a JSON schema generator, suitable for generating json schemas - /// for actions - pub fn action_schema_generator() -> schemars::SchemaGenerator { - schemars::generate::SchemaSettings::draft2019_09() - .with_transform(AllowTrailingCommas) - .into_generator() - } - - pub fn generate_json_schema_for_registered_actions(cx: &mut App) -> Value { - // instead of using DefaultDenyUnknownFields, actions typically use - // `#[serde(deny_unknown_fields)]` so that these cases are reported as parse failures. This - // is because the rest of the keymap will still load in these cases, whereas other settings - // files would not. - let mut generator = Self::action_schema_generator(); - - let action_schemas = cx.action_schemas(&mut generator); - let action_documentation = cx.action_documentation(); - let deprecations = cx.deprecated_actions_to_preferred_actions(); - let deprecation_messages = cx.action_deprecation_messages(); - KeymapFile::generate_json_schema( - generator, - action_schemas, - action_documentation, - deprecations, - deprecation_messages, - ) - } - - fn generate_json_schema( - mut generator: schemars::SchemaGenerator, - action_schemas: Vec<(&'static str, Option)>, - action_documentation: &HashMap<&'static str, &'static str>, - deprecations: &HashMap<&'static str, &'static str>, - deprecation_messages: &HashMap<&'static str, &'static str>, - ) -> serde_json::Value { - fn add_deprecation(schema: &mut schemars::Schema, message: String) { - schema.insert( - // deprecationMessage is not part of the JSON Schema spec, but - // json-language-server recognizes it. - "deprecationMessage".to_string(), - Value::String(message), - ); - } - - fn add_deprecation_preferred_name(schema: &mut schemars::Schema, new_name: &str) { - add_deprecation(schema, format!("Deprecated, use {new_name}")); - } - - fn add_description(schema: &mut schemars::Schema, description: &str) { - schema.insert( - "description".to_string(), - Value::String(description.to_string()), - ); - } - - let empty_object = json_schema!({ - "type": "object" - }); - - // This is a workaround for a json-language-server issue where it matches the first - // alternative that matches the value's shape and uses that for documentation. - // - // In the case of the array validations, it would even provide an error saying that the name - // must match the name of the first alternative. - let mut empty_action_name = json_schema!({ - "type": "string", - "const": "" - }); - let no_action_message = "No action named this."; - add_description(&mut empty_action_name, no_action_message); - add_deprecation(&mut empty_action_name, no_action_message.to_string()); - let empty_action_name_with_input = json_schema!({ - "type": "array", - "items": [ - empty_action_name, - true - ], - "minItems": 2, - "maxItems": 2 - }); - let mut keymap_action_alternatives = vec![empty_action_name, empty_action_name_with_input]; - - let mut empty_schema_action_names = vec![]; - for (name, action_schema) in action_schemas.into_iter() { - let deprecation = if name == NoAction.name() { - Some("null") - } else { - deprecations.get(name).copied() - }; - - // Add an alternative for plain action names. - let mut plain_action = json_schema!({ - "type": "string", - "const": name - }); - if let Some(message) = deprecation_messages.get(name) { - add_deprecation(&mut plain_action, message.to_string()); - } else if let Some(new_name) = deprecation { - add_deprecation_preferred_name(&mut plain_action, new_name); - } - let description = action_documentation.get(name); - if let Some(description) = &description { - add_description(&mut plain_action, description); - } - keymap_action_alternatives.push(plain_action); - - // Add an alternative for actions with data specified as a [name, data] array. - // - // When a struct with no deserializable fields is added by deriving `Action`, an empty - // object schema is produced. The action should be invoked without data in this case. - if let Some(schema) = action_schema - && schema != empty_object - { - let mut matches_action_name = json_schema!({ - "const": name - }); - if let Some(description) = &description { - add_description(&mut matches_action_name, description); - } - if let Some(message) = deprecation_messages.get(name) { - add_deprecation(&mut matches_action_name, message.to_string()); - } else if let Some(new_name) = deprecation { - add_deprecation_preferred_name(&mut matches_action_name, new_name); - } - let action_with_input = json_schema!({ - "type": "array", - "items": [matches_action_name, schema], - "minItems": 2, - "maxItems": 2 - }); - keymap_action_alternatives.push(action_with_input); - } else { - empty_schema_action_names.push(name); - } - } - - if !empty_schema_action_names.is_empty() { - let action_names = json_schema!({ "enum": empty_schema_action_names }); - let no_properties_allowed = json_schema!({ - "type": "object", - "additionalProperties": false - }); - let mut actions_with_empty_input = json_schema!({ - "type": "array", - "items": [action_names, no_properties_allowed], - "minItems": 2, - "maxItems": 2 - }); - add_deprecation( - &mut actions_with_empty_input, - "This action does not take input - just the action name string should be used." - .to_string(), - ); - keymap_action_alternatives.push(actions_with_empty_input); - } - - // Placing null first causes json-language-server to default assuming actions should be - // null, so place it last. - keymap_action_alternatives.push(json_schema!({ - "type": "null" - })); - - // The `KeymapSection` schema will reference the `KeymapAction` schema by name, so setting - // the definition of `KeymapAction` results in the full action schema being used. - generator.definitions_mut().insert( - KeymapAction::schema_name().to_string(), - json!({ - "oneOf": keymap_action_alternatives - }), - ); - - generator.root_schema_for::().to_value() - } - - pub fn sections(&self) -> impl DoubleEndedIterator { - self.0.iter() - } - - pub async fn load_keymap_file(fs: &Arc) -> Result { - match fs.load(paths::keymap_file()).await { - result @ Ok(_) => result, - Err(err) => { - if let Some(e) = err.downcast_ref::() - && e.kind() == std::io::ErrorKind::NotFound - { - return Ok(crate::initial_keymap_content().to_string()); - } - Err(err) - } - } - } - - pub fn update_keybinding<'a>( - 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 - KeybindUpdateOperation::Replace { - target_keybind_source: target_source, - source, - target, - } if target_source != KeybindSource::User => { - operation = KeybindUpdateOperation::Add { - source, - from: Some(target), - }; - } - // if trying to remove a keybinding that is not user-defined, treat it as creating a binding - // that binds it to `zed::NoAction` - KeybindUpdateOperation::Remove { - target, - target_keybind_source, - } if target_keybind_source != KeybindSource::User => { - let mut source = target.clone(); - source.action_name = gpui::NoAction.name(); - source.action_arguments.take(); - operation = KeybindUpdateOperation::Add { - source, - from: Some(target), - }; - } - _ => {} - } - - // Sanity check that keymap contents are valid, even though we only use it for Replace. - // We don't want to modify the file if it's invalid. - let keymap = Self::parse(&keymap_contents).context("Failed to parse keymap")?; - - if let KeybindUpdateOperation::Remove { target, .. } = operation { - let target_action_value = target - .action_value() - .context("Failed to generate target action JSON value")?; - let Some((index, keystrokes_str)) = - find_binding(&keymap, &target, &target_action_value, keyboard_mapper) - else { - anyhow::bail!("Failed to find keybinding to remove"); - }; - let is_only_binding = keymap.0[index] - .bindings - .as_ref() - .is_none_or(|bindings| bindings.len() == 1); - let key_path: &[&str] = if is_only_binding { - &[] - } else { - &["bindings", keystrokes_str] - }; - let (replace_range, replace_value) = replace_top_level_array_value_in_json_text( - &keymap_contents, - key_path, - None, - None, - index, - tab_size, - ); - keymap_contents.replace_range(replace_range, &replace_value); - return Ok(keymap_contents); - } - - if let KeybindUpdateOperation::Replace { source, target, .. } = operation { - let target_action_value = target - .action_value() - .context("Failed to generate target action JSON value")?; - let source_action_value = source - .action_value() - .context("Failed to generate source action JSON value")?; - - if let Some((index, keystrokes_str)) = - find_binding(&keymap, &target, &target_action_value, keyboard_mapper) - { - if target.context == source.context { - // if we are only changing the keybinding (common case) - // not the context, etc. Then just update the binding in place - - let (replace_range, replace_value) = replace_top_level_array_value_in_json_text( - &keymap_contents, - &["bindings", keystrokes_str], - Some(&source_action_value), - Some(&source.keystrokes_unparsed()), - index, - tab_size, - ); - keymap_contents.replace_range(replace_range, &replace_value); - - return Ok(keymap_contents); - } else if keymap.0[index] - .bindings - .as_ref() - .is_none_or(|bindings| bindings.len() == 1) - { - // if we are replacing the only binding in the section, - // just update the section in place, updating the context - // and the binding - - let (replace_range, replace_value) = replace_top_level_array_value_in_json_text( - &keymap_contents, - &["bindings", keystrokes_str], - Some(&source_action_value), - Some(&source.keystrokes_unparsed()), - index, - tab_size, - ); - keymap_contents.replace_range(replace_range, &replace_value); - - let (replace_range, replace_value) = replace_top_level_array_value_in_json_text( - &keymap_contents, - &["context"], - source.context.map(Into::into).as_ref(), - None, - index, - tab_size, - ); - keymap_contents.replace_range(replace_range, &replace_value); - return Ok(keymap_contents); - } else { - // if we are replacing one of multiple bindings in a section - // with a context change, remove the existing binding from the - // section, then treat this operation as an add operation of the - // new binding with the updated context. - - let (replace_range, replace_value) = replace_top_level_array_value_in_json_text( - &keymap_contents, - &["bindings", keystrokes_str], - None, - None, - index, - tab_size, - ); - keymap_contents.replace_range(replace_range, &replace_value); - operation = KeybindUpdateOperation::Add { - source, - from: Some(target), - }; - } - } else { - log::warn!( - "Failed to find keybinding to update `{:?} -> {}` creating new binding for `{:?} -> {}` instead", - target.keystrokes, - target_action_value, - source.keystrokes, - source_action_value, - ); - operation = KeybindUpdateOperation::Add { - source, - from: Some(target), - }; - } - } - - if let KeybindUpdateOperation::Add { - source: keybinding, - from, - } = operation - { - let mut value = serde_json::Map::with_capacity(4); - if let Some(context) = keybinding.context { - value.insert("context".to_string(), context.into()); - } - 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)?; - Some(keymap.0[index].use_key_equivalents) - }).unwrap_or(false); - if use_key_equivalents { - value.insert("use_key_equivalents".to_string(), true.into()); - } - - value.insert("bindings".to_string(), { - let mut bindings = serde_json::Map::new(); - let action = keybinding.action_value()?; - bindings.insert(keybinding.keystrokes_unparsed(), action); - bindings.into() - }); - - let (replace_range, replace_value) = append_top_level_array_value_in_json_text( - &keymap_contents, - &value.into(), - tab_size, - ); - keymap_contents.replace_range(replace_range, &replace_value); - } - return Ok(keymap_contents); - - fn find_binding<'a, 'b>( - 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(); - for (index, section) in keymap.sections().enumerate() { - let section_context_parsed = - KeyBindingContextPredicate::parse(§ion.context).ok(); - if section_context_parsed != target_context_parsed { - continue; - } - let Some(bindings) = §ion.bindings else { - continue; - }; - for (keystrokes_str, action) in bindings { - let Ok(keystrokes) = keystrokes_str - .split_whitespace() - .map(|source| { - let keystroke = Keystroke::parse(source)?; - Ok(KeybindingKeystroke::new_with_mapper( - keystroke, - false, - keyboard_mapper, - )) - }) - .collect::, InvalidKeystrokeError>>() - else { - continue; - }; - if keystrokes.len() != target.keystrokes.len() - || !keystrokes - .iter() - .zip(target.keystrokes) - .all(|(a, b)| a.inner().should_match(b)) - { - continue; - } - if &action.0 != target_action_value { - continue; - } - return Some((index, keystrokes_str)); - } - } - None - } - } -} - -#[derive(Clone, Debug)] -pub enum KeybindUpdateOperation<'a> { - Replace { - /// Describes the keybind to create - source: KeybindUpdateTarget<'a>, - /// Describes the keybind to remove - target: KeybindUpdateTarget<'a>, - target_keybind_source: KeybindSource, - }, - Add { - source: KeybindUpdateTarget<'a>, - from: Option>, - }, - Remove { - target: KeybindUpdateTarget<'a>, - target_keybind_source: KeybindSource, - }, -} - -impl KeybindUpdateOperation<'_> { - pub fn generate_telemetry( - &self, - ) -> ( - // The keybind that is created - String, - // The keybinding that was removed - String, - // The source of the keybinding - String, - ) { - let (new_binding, removed_binding, source) = match &self { - KeybindUpdateOperation::Replace { - source, - target, - target_keybind_source, - } => (Some(source), Some(target), Some(*target_keybind_source)), - KeybindUpdateOperation::Add { source, .. } => (Some(source), None, None), - KeybindUpdateOperation::Remove { - target, - target_keybind_source, - } => (None, Some(target), Some(*target_keybind_source)), - }; - - let new_binding = new_binding - .map(KeybindUpdateTarget::telemetry_string) - .unwrap_or("null".to_owned()); - let removed_binding = removed_binding - .map(KeybindUpdateTarget::telemetry_string) - .unwrap_or("null".to_owned()); - - let source = source - .as_ref() - .map(KeybindSource::name) - .map(ToOwned::to_owned) - .unwrap_or("null".to_owned()); - - (new_binding, removed_binding, source) - } -} - -impl<'a> KeybindUpdateOperation<'a> { - pub fn add(source: KeybindUpdateTarget<'a>) -> Self { - Self::Add { source, from: None } - } -} - -#[derive(Debug, Clone)] -pub struct KeybindUpdateTarget<'a> { - pub context: Option<&'a str>, - pub keystrokes: &'a [KeybindingKeystroke], - pub action_name: &'a str, - pub action_arguments: Option<&'a str>, -} - -impl<'a> KeybindUpdateTarget<'a> { - fn action_value(&self) -> Result { - if self.action_name == gpui::NoAction.name() { - return Ok(Value::Null); - } - let action_name: Value = self.action_name.into(); - let value = match self.action_arguments { - Some(args) if !args.is_empty() => { - let args = serde_json::from_str::(args) - .context("Failed to parse action arguments as JSON")?; - serde_json::json!([action_name, args]) - } - _ => action_name, - }; - Ok(value) - } - - 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(' '); - } - keystrokes.pop(); - keystrokes - } - - fn telemetry_string(&self) -> String { - format!( - "action_name: {}, context: {}, action_arguments: {}, keystrokes: {}", - self.action_name, - self.context.unwrap_or("global"), - self.action_arguments.unwrap_or("none"), - self.keystrokes_unparsed() - ) - } -} - -#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Debug)] -pub enum KeybindSource { - User, - Vim, - Base, - #[default] - Default, - Unknown, -} - -impl KeybindSource { - const BASE: KeyBindingMetaIndex = KeyBindingMetaIndex(KeybindSource::Base as u32); - const DEFAULT: KeyBindingMetaIndex = KeyBindingMetaIndex(KeybindSource::Default as u32); - const VIM: KeyBindingMetaIndex = KeyBindingMetaIndex(KeybindSource::Vim as u32); - const USER: KeyBindingMetaIndex = KeyBindingMetaIndex(KeybindSource::User as u32); - - pub fn name(&self) -> &'static str { - match self { - KeybindSource::User => "User", - KeybindSource::Default => "Default", - KeybindSource::Base => "Base", - KeybindSource::Vim => "Vim", - KeybindSource::Unknown => "Unknown", - } - } - - pub fn meta(&self) -> KeyBindingMetaIndex { - match self { - KeybindSource::User => Self::USER, - KeybindSource::Default => Self::DEFAULT, - KeybindSource::Base => Self::BASE, - KeybindSource::Vim => Self::VIM, - KeybindSource::Unknown => KeyBindingMetaIndex(*self as u32), - } - } - - pub fn from_meta(index: KeyBindingMetaIndex) -> Self { - match index { - Self::USER => KeybindSource::User, - Self::BASE => KeybindSource::Base, - Self::DEFAULT => KeybindSource::Default, - Self::VIM => KeybindSource::Vim, - _ => KeybindSource::Unknown, - } - } -} - -impl From for KeybindSource { - fn from(index: KeyBindingMetaIndex) -> Self { - Self::from_meta(index) - } -} - -impl From for KeyBindingMetaIndex { - fn from(source: KeybindSource) -> Self { - source.meta() - } -} - -/// Runs a sequence of actions. Does not wait for asynchronous actions to complete before running -/// the next action. Currently only works in workspace windows. -/// -/// This action is special-cased in keymap parsing to allow it to access `App` while parsing, so -/// that it can parse its input actions. -pub struct ActionSequence(pub Vec>); - -register_action!(ActionSequence); - -impl ActionSequence { - fn build_sequence( - value: Value, - cx: &App, - ) -> std::result::Result, ActionBuildError> { - match value { - Value::Array(values) => { - let actions = values - .into_iter() - .enumerate() - .map(|(index, action)| { - match KeymapFile::build_keymap_action(&KeymapAction(action), cx) { - Ok((action, _)) => Ok(action), - Err(err) => { - return Err(ActionBuildError::BuildError { - name: Self::name_for_type().to_string(), - error: anyhow::anyhow!( - "error at sequence index {index}: {err}" - ), - }); - } - } - }) - .collect::, _>>()?; - Ok(Box::new(Self(actions))) - } - _ => Err(Self::expected_array_error()), - } - } - - fn expected_array_error() -> ActionBuildError { - ActionBuildError::BuildError { - name: Self::name_for_type().to_string(), - error: anyhow::anyhow!("expected array of actions"), - } - } -} - -impl Action for ActionSequence { - fn name(&self) -> &'static str { - Self::name_for_type() - } - - fn name_for_type() -> &'static str - where - Self: Sized, - { - "action::Sequence" - } - - fn partial_eq(&self, action: &dyn Action) -> bool { - action - .as_any() - .downcast_ref::() - .map_or(false, |other| { - self.0.len() == other.0.len() - && self - .0 - .iter() - .zip(other.0.iter()) - .all(|(a, b)| a.partial_eq(b.as_ref())) - }) - } - - fn boxed_clone(&self) -> Box { - Box::new(ActionSequence( - self.0 - .iter() - .map(|action| action.boxed_clone()) - .collect::>(), - )) - } - - fn build(_value: Value) -> Result> { - Err(anyhow::anyhow!( - "{} cannot be built directly", - Self::name_for_type() - )) - } - - fn action_json_schema(generator: &mut schemars::SchemaGenerator) -> Option { - let keymap_action_schema = generator.subschema_for::(); - Some(json_schema!({ - "type": "array", - "items": keymap_action_schema - })) - } - - fn deprecated_aliases() -> &'static [&'static str] { - &[] - } - - fn deprecation_message() -> Option<&'static str> { - None - } - - fn documentation() -> Option<&'static str> { - Some( - "Runs a sequence of actions.\n\n\ - NOTE: This does **not** wait for asynchronous actions to complete before running the next action.", - ) - } -} - -#[cfg(test)] -mod tests { - use gpui::{DummyKeyboardMapper, KeybindingKeystroke, Keystroke}; - use unindent::Unindent; - - use crate::{ - KeybindSource, KeymapFile, - keymap_file::{KeybindUpdateOperation, KeybindUpdateTarget}, - }; - - #[test] - fn can_deserialize_keymap_with_trailing_comma() { - let json = indoc::indoc! {"[ - // Standard macOS bindings - { - \"bindings\": { - \"up\": \"menu::SelectPrevious\", - }, - }, - ] - " - }; - KeymapFile::parse(json).unwrap(); - } - - #[track_caller] - fn check_keymap_update( - input: impl ToString, - operation: KeybindUpdateOperation, - expected: impl ToString, - ) { - let result = KeymapFile::update_keybinding( - operation, - input.to_string(), - 4, - &gpui::DummyKeyboardMapper, - ) - .expect("Update succeeded"); - pretty_assertions::assert_eq!(expected.to_string(), result); - } - - #[track_caller] - fn parse_keystrokes(keystrokes: &str) -> Vec { - keystrokes - .split(' ') - .map(|s| { - KeybindingKeystroke::new_with_mapper( - Keystroke::parse(s).expect("Keystrokes valid"), - false, - &DummyKeyboardMapper, - ) - }) - .collect() - } - - #[test] - fn keymap_update() { - zlog::init_test(); - - check_keymap_update( - "[]", - KeybindUpdateOperation::add(KeybindUpdateTarget { - keystrokes: &parse_keystrokes("ctrl-a"), - action_name: "zed::SomeAction", - context: None, - action_arguments: None, - }), - r#"[ - { - "bindings": { - "ctrl-a": "zed::SomeAction" - } - } - ]"# - .unindent(), - ); - - check_keymap_update( - "[]", - KeybindUpdateOperation::add(KeybindUpdateTarget { - keystrokes: &parse_keystrokes("\\ a"), - action_name: "zed::SomeAction", - context: None, - action_arguments: None, - }), - r#"[ - { - "bindings": { - "\\ a": "zed::SomeAction" - } - } - ]"# - .unindent(), - ); - - check_keymap_update( - "[]", - KeybindUpdateOperation::add(KeybindUpdateTarget { - keystrokes: &parse_keystrokes("ctrl-a"), - action_name: "zed::SomeAction", - context: None, - action_arguments: Some(""), - }), - r#"[ - { - "bindings": { - "ctrl-a": "zed::SomeAction" - } - } - ]"# - .unindent(), - ); - - check_keymap_update( - r#"[ - { - "bindings": { - "ctrl-a": "zed::SomeAction" - } - } - ]"# - .unindent(), - KeybindUpdateOperation::add(KeybindUpdateTarget { - keystrokes: &parse_keystrokes("ctrl-b"), - action_name: "zed::SomeOtherAction", - context: None, - action_arguments: None, - }), - r#"[ - { - "bindings": { - "ctrl-a": "zed::SomeAction" - } - }, - { - "bindings": { - "ctrl-b": "zed::SomeOtherAction" - } - } - ]"# - .unindent(), - ); - - check_keymap_update( - r#"[ - { - "bindings": { - "ctrl-a": "zed::SomeAction" - } - } - ]"# - .unindent(), - KeybindUpdateOperation::add(KeybindUpdateTarget { - keystrokes: &parse_keystrokes("ctrl-b"), - action_name: "zed::SomeOtherAction", - context: None, - action_arguments: Some(r#"{"foo": "bar"}"#), - }), - r#"[ - { - "bindings": { - "ctrl-a": "zed::SomeAction" - } - }, - { - "bindings": { - "ctrl-b": [ - "zed::SomeOtherAction", - { - "foo": "bar" - } - ] - } - } - ]"# - .unindent(), - ); - - check_keymap_update( - r#"[ - { - "bindings": { - "ctrl-a": "zed::SomeAction" - } - } - ]"# - .unindent(), - KeybindUpdateOperation::add(KeybindUpdateTarget { - keystrokes: &parse_keystrokes("ctrl-b"), - action_name: "zed::SomeOtherAction", - context: Some("Zed > Editor && some_condition = true"), - action_arguments: Some(r#"{"foo": "bar"}"#), - }), - r#"[ - { - "bindings": { - "ctrl-a": "zed::SomeAction" - } - }, - { - "context": "Zed > Editor && some_condition = true", - "bindings": { - "ctrl-b": [ - "zed::SomeOtherAction", - { - "foo": "bar" - } - ] - } - } - ]"# - .unindent(), - ); - - check_keymap_update( - r#"[ - { - "bindings": { - "ctrl-a": "zed::SomeAction" - } - } - ]"# - .unindent(), - KeybindUpdateOperation::Replace { - target: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("ctrl-a"), - action_name: "zed::SomeAction", - context: None, - action_arguments: None, - }, - source: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("ctrl-b"), - action_name: "zed::SomeOtherAction", - context: None, - action_arguments: Some(r#"{"foo": "bar"}"#), - }, - target_keybind_source: KeybindSource::Base, - }, - r#"[ - { - "bindings": { - "ctrl-a": "zed::SomeAction" - } - }, - { - "bindings": { - "ctrl-b": [ - "zed::SomeOtherAction", - { - "foo": "bar" - } - ] - } - } - ]"# - .unindent(), - ); - - check_keymap_update( - r#"[ - { - "bindings": { - "a": "zed::SomeAction" - } - } - ]"# - .unindent(), - KeybindUpdateOperation::Replace { - target: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("a"), - action_name: "zed::SomeAction", - context: None, - action_arguments: None, - }, - source: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("ctrl-b"), - action_name: "zed::SomeOtherAction", - context: None, - action_arguments: Some(r#"{"foo": "bar"}"#), - }, - target_keybind_source: KeybindSource::User, - }, - r#"[ - { - "bindings": { - "ctrl-b": [ - "zed::SomeOtherAction", - { - "foo": "bar" - } - ] - } - } - ]"# - .unindent(), - ); - - check_keymap_update( - r#"[ - { - "bindings": { - "\\ a": "zed::SomeAction" - } - } - ]"# - .unindent(), - KeybindUpdateOperation::Replace { - target: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("\\ a"), - action_name: "zed::SomeAction", - context: None, - action_arguments: None, - }, - source: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("\\ b"), - action_name: "zed::SomeOtherAction", - context: None, - action_arguments: Some(r#"{"foo": "bar"}"#), - }, - target_keybind_source: KeybindSource::User, - }, - r#"[ - { - "bindings": { - "\\ b": [ - "zed::SomeOtherAction", - { - "foo": "bar" - } - ] - } - } - ]"# - .unindent(), - ); - - check_keymap_update( - r#"[ - { - "bindings": { - "\\ a": "zed::SomeAction" - } - } - ]"# - .unindent(), - KeybindUpdateOperation::Replace { - target: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("\\ a"), - action_name: "zed::SomeAction", - context: None, - action_arguments: None, - }, - source: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("\\ a"), - action_name: "zed::SomeAction", - context: None, - action_arguments: None, - }, - target_keybind_source: KeybindSource::User, - }, - r#"[ - { - "bindings": { - "\\ a": "zed::SomeAction" - } - } - ]"# - .unindent(), - ); - - check_keymap_update( - r#"[ - { - "bindings": { - "ctrl-a": "zed::SomeAction" - } - } - ]"# - .unindent(), - KeybindUpdateOperation::Replace { - target: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("ctrl-a"), - action_name: "zed::SomeNonexistentAction", - context: None, - action_arguments: None, - }, - source: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("ctrl-b"), - action_name: "zed::SomeOtherAction", - context: None, - action_arguments: None, - }, - target_keybind_source: KeybindSource::User, - }, - r#"[ - { - "bindings": { - "ctrl-a": "zed::SomeAction" - } - }, - { - "bindings": { - "ctrl-b": "zed::SomeOtherAction" - } - } - ]"# - .unindent(), - ); - - check_keymap_update( - r#"[ - { - "bindings": { - // some comment - "ctrl-a": "zed::SomeAction" - // some other comment - } - } - ]"# - .unindent(), - KeybindUpdateOperation::Replace { - target: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("ctrl-a"), - action_name: "zed::SomeAction", - context: None, - action_arguments: None, - }, - source: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("ctrl-b"), - action_name: "zed::SomeOtherAction", - context: None, - action_arguments: Some(r#"{"foo": "bar"}"#), - }, - target_keybind_source: KeybindSource::User, - }, - r#"[ - { - "bindings": { - // some comment - "ctrl-b": [ - "zed::SomeOtherAction", - { - "foo": "bar" - } - ] - // some other comment - } - } - ]"# - .unindent(), - ); - - check_keymap_update( - r#"[ - { - "context": "SomeContext", - "bindings": { - "a": "foo::bar", - "b": "baz::qux", - } - } - ]"# - .unindent(), - KeybindUpdateOperation::Replace { - target: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("a"), - action_name: "foo::bar", - context: Some("SomeContext"), - action_arguments: None, - }, - source: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("c"), - action_name: "foo::baz", - context: Some("SomeOtherContext"), - action_arguments: None, - }, - target_keybind_source: KeybindSource::User, - }, - r#"[ - { - "context": "SomeContext", - "bindings": { - "b": "baz::qux", - } - }, - { - "context": "SomeOtherContext", - "bindings": { - "c": "foo::baz" - } - } - ]"# - .unindent(), - ); - - check_keymap_update( - r#"[ - { - "context": "SomeContext", - "bindings": { - "a": "foo::bar", - } - } - ]"# - .unindent(), - KeybindUpdateOperation::Replace { - target: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("a"), - action_name: "foo::bar", - context: Some("SomeContext"), - action_arguments: None, - }, - source: KeybindUpdateTarget { - keystrokes: &parse_keystrokes("c"), - action_name: "foo::baz", - context: Some("SomeOtherContext"), - action_arguments: None, - }, - target_keybind_source: KeybindSource::User, - }, - r#"[ - { - "context": "SomeOtherContext", - "bindings": { - "c": "foo::baz", - } - } - ]"# - .unindent(), - ); - - check_keymap_update( - r#"[ - { - "context": "SomeContext", - "bindings": { - "a": "foo::bar", - "c": "foo::baz", - } - }, - ]"# - .unindent(), - KeybindUpdateOperation::Remove { - target: KeybindUpdateTarget { - context: Some("SomeContext"), - keystrokes: &parse_keystrokes("a"), - action_name: "foo::bar", - action_arguments: None, - }, - target_keybind_source: KeybindSource::User, - }, - r#"[ - { - "context": "SomeContext", - "bindings": { - "c": "foo::baz", - } - }, - ]"# - .unindent(), - ); - - check_keymap_update( - r#"[ - { - "context": "SomeContext", - "bindings": { - "\\ a": "foo::bar", - "c": "foo::baz", - } - }, - ]"# - .unindent(), - KeybindUpdateOperation::Remove { - target: KeybindUpdateTarget { - context: Some("SomeContext"), - keystrokes: &parse_keystrokes("\\ a"), - action_name: "foo::bar", - action_arguments: None, - }, - target_keybind_source: KeybindSource::User, - }, - r#"[ - { - "context": "SomeContext", - "bindings": { - "c": "foo::baz", - } - }, - ]"# - .unindent(), - ); - - check_keymap_update( - r#"[ - { - "context": "SomeContext", - "bindings": { - "a": ["foo::bar", true], - "c": "foo::baz", - } - }, - ]"# - .unindent(), - KeybindUpdateOperation::Remove { - target: KeybindUpdateTarget { - context: Some("SomeContext"), - keystrokes: &parse_keystrokes("a"), - action_name: "foo::bar", - action_arguments: Some("true"), - }, - target_keybind_source: KeybindSource::User, - }, - r#"[ - { - "context": "SomeContext", - "bindings": { - "c": "foo::baz", - } - }, - ]"# - .unindent(), - ); - - check_keymap_update( - r#"[ - { - "context": "SomeContext", - "bindings": { - "b": "foo::baz", - } - }, - { - "context": "SomeContext", - "bindings": { - "a": ["foo::bar", true], - } - }, - { - "context": "SomeContext", - "bindings": { - "c": "foo::baz", - } - }, - ]"# - .unindent(), - KeybindUpdateOperation::Remove { - target: KeybindUpdateTarget { - context: Some("SomeContext"), - keystrokes: &parse_keystrokes("a"), - action_name: "foo::bar", - action_arguments: Some("true"), - }, - target_keybind_source: KeybindSource::User, - }, - r#"[ - { - "context": "SomeContext", - "bindings": { - "b": "foo::baz", - } - }, - { - "context": "SomeContext", - "bindings": { - "c": "foo::baz", - } - }, - ]"# - .unindent(), - ); - check_keymap_update( - r#"[ - { - "context": "SomeOtherContext", - "use_key_equivalents": true, - "bindings": { - "b": "foo::bar", - } - }, - ]"# - .unindent(), - KeybindUpdateOperation::Add { - source: KeybindUpdateTarget { - context: Some("SomeContext"), - keystrokes: &parse_keystrokes("a"), - action_name: "foo::baz", - action_arguments: Some("true"), - }, - from: Some(KeybindUpdateTarget { - context: Some("SomeOtherContext"), - keystrokes: &parse_keystrokes("b"), - action_name: "foo::bar", - action_arguments: None, - }), - }, - r#"[ - { - "context": "SomeOtherContext", - "use_key_equivalents": true, - "bindings": { - "b": "foo::bar", - } - }, - { - "context": "SomeContext", - "use_key_equivalents": true, - "bindings": { - "a": [ - "foo::baz", - true - ] - } - } - ]"# - .unindent(), - ); - - check_keymap_update( - r#"[ - { - "context": "SomeOtherContext", - "use_key_equivalents": true, - "bindings": { - "b": "foo::bar", - } - }, - ]"# - .unindent(), - KeybindUpdateOperation::Remove { - target: KeybindUpdateTarget { - context: Some("SomeContext"), - keystrokes: &parse_keystrokes("a"), - action_name: "foo::baz", - action_arguments: Some("true"), - }, - target_keybind_source: KeybindSource::Default, - }, - r#"[ - { - "context": "SomeOtherContext", - "use_key_equivalents": true, - "bindings": { - "b": "foo::bar", - } - }, - { - "context": "SomeContext", - "bindings": { - "a": null - } - } - ]"# - .unindent(), - ); - } - - #[test] - fn test_keymap_remove() { - zlog::init_test(); - - check_keymap_update( - r#" - [ - { - "context": "Editor", - "bindings": { - "cmd-k cmd-u": "editor::ConvertToUpperCase", - "cmd-k cmd-l": "editor::ConvertToLowerCase", - "cmd-[": "pane::GoBack", - } - }, - ] - "#, - KeybindUpdateOperation::Remove { - target: KeybindUpdateTarget { - context: Some("Editor"), - keystrokes: &parse_keystrokes("cmd-k cmd-l"), - action_name: "editor::ConvertToLowerCase", - action_arguments: None, - }, - target_keybind_source: KeybindSource::User, - }, - r#" - [ - { - "context": "Editor", - "bindings": { - "cmd-k cmd-u": "editor::ConvertToUpperCase", - "cmd-[": "pane::GoBack", - } - }, - ] - "#, - ); - } -} diff --git a/crates/settings/src/merge_from.rs b/crates/settings/src/merge_from.rs deleted file mode 100644 index 30ad0d3671..0000000000 --- a/crates/settings/src/merge_from.rs +++ /dev/null @@ -1,176 +0,0 @@ -/// Trait for recursively merging settings structures. -/// -/// When Zed starts it loads settings from `default.json` to initialize -/// everything. These may be further refined by loading the user's settings, -/// and any settings profiles; and then further refined by loading any -/// local project settings. -/// -/// The default behaviour of merging is: -/// * For objects with named keys (HashMap, structs, etc.). The values are merged deeply -/// (so if the default settings has languages.JSON.prettier.allowed = true, and the user's settings has -/// languages.JSON.tab_size = 4; the merged settings file will have both settings). -/// * For options, a None value is ignored, but Some values are merged recursively. -/// * For other types (including Vec), a merge overwrites the current value. -/// -/// If you want to break the rules you can (e.g. ExtendingVec, or SaturatingBool). -#[allow(unused)] -pub trait MergeFrom { - /// Merge from a source of the same type. - fn merge_from(&mut self, other: &Self); - - /// Merge from an optional source of the same type. - fn merge_from_option(&mut self, other: Option<&Self>) { - if let Some(other) = other { - self.merge_from(other); - } - } -} - -macro_rules! merge_from_overwrites { - ($($type:ty),+) => { - $( - impl MergeFrom for $type { - fn merge_from(&mut self, other: &Self) { - *self = other.clone(); - } - } - )+ - } -} - -merge_from_overwrites!( - u16, - u32, - u64, - usize, - i16, - i32, - i64, - bool, - f64, - f32, - char, - std::num::NonZeroUsize, - std::num::NonZeroU32, - String, - std::sync::Arc, - gpui::SharedString, - std::path::PathBuf, - gpui::Modifiers, - gpui::FontFeatures, - gpui::FontWeight -); - -impl MergeFrom for Option { - fn merge_from(&mut self, other: &Self) { - let Some(other) = other else { - return; - }; - if let Some(this) = self { - this.merge_from(other); - } else { - self.replace(other.clone()); - } - } -} - -impl MergeFrom for Vec { - fn merge_from(&mut self, other: &Self) { - *self = other.clone() - } -} - -impl MergeFrom for Box { - fn merge_from(&mut self, other: &Self) { - self.as_mut().merge_from(other.as_ref()) - } -} - -// Implementations for collections that extend/merge their contents -impl MergeFrom for collections::HashMap -where - K: Clone + std::hash::Hash + Eq, - V: Clone + MergeFrom, -{ - fn merge_from(&mut self, other: &Self) { - for (k, v) in other { - if let Some(existing) = self.get_mut(k) { - existing.merge_from(v); - } else { - self.insert(k.clone(), v.clone()); - } - } - } -} - -impl MergeFrom for collections::BTreeMap -where - K: Clone + std::hash::Hash + Eq + Ord, - V: Clone + MergeFrom, -{ - fn merge_from(&mut self, other: &Self) { - for (k, v) in other { - if let Some(existing) = self.get_mut(k) { - existing.merge_from(v); - } else { - self.insert(k.clone(), v.clone()); - } - } - } -} - -impl MergeFrom for collections::IndexMap -where - K: std::hash::Hash + Eq + Clone, - // Q: ?Sized + std::hash::Hash + collections::Equivalent + Eq, - V: Clone + MergeFrom, -{ - fn merge_from(&mut self, other: &Self) { - for (k, v) in other { - if let Some(existing) = self.get_mut(k) { - existing.merge_from(v); - } else { - self.insert(k.clone(), v.clone()); - } - } - } -} - -impl MergeFrom for collections::BTreeSet -where - T: Clone + Ord, -{ - fn merge_from(&mut self, other: &Self) { - for item in other { - self.insert(item.clone()); - } - } -} - -impl MergeFrom for collections::HashSet -where - T: Clone + std::hash::Hash + Eq, -{ - fn merge_from(&mut self, other: &Self) { - for item in other { - self.insert(item.clone()); - } - } -} - -impl MergeFrom for serde_json::Value { - fn merge_from(&mut self, other: &Self) { - match (self, other) { - (serde_json::Value::Object(this), serde_json::Value::Object(other)) => { - for (k, v) in other { - if let Some(existing) = this.get_mut(k) { - existing.merge_from(v); - } else { - this.insert(k.clone(), v.clone()); - } - } - } - (this, other) => *this = other.clone(), - } - } -} diff --git a/crates/settings/src/serde_helper.rs b/crates/settings/src/serde_helper.rs deleted file mode 100644 index 1c1826abd4..0000000000 --- a/crates/settings/src/serde_helper.rs +++ /dev/null @@ -1,135 +0,0 @@ -use serde::Serializer; - -/// Serializes an f32 value with 2 decimal places of precision. -/// -/// This function rounds the value to 2 decimal places and formats it as a string, -/// then parses it back to f64 before serialization. This ensures clean JSON output -/// without IEEE 754 floating-point artifacts. -/// -/// # Arguments -/// -/// * `value` - The f32 value to serialize -/// * `serializer` - The serde serializer to use -/// -/// # Returns -/// -/// Result of the serialization operation -/// -/// # Usage -/// -/// This function can be used with Serde's `serialize_with` attribute: -/// ``` -/// use serde::Serialize; -/// use settings::serialize_f32_with_two_decimal_places; -/// -/// #[derive(Serialize)] -/// struct ExampleStruct(#[serde(serialize_with = "serialize_f32_with_two_decimal_places")] f32); -/// ``` -pub fn serialize_f32_with_two_decimal_places( - value: &f32, - serializer: S, -) -> Result -where - S: Serializer, -{ - let rounded = (value * 100.0).round() / 100.0; - let formatted = format!("{:.2}", rounded); - let clean_value: f64 = formatted.parse().unwrap_or(rounded as f64); - serializer.serialize_f64(clean_value) -} - -/// Serializes an optional f32 value with 2 decimal places of precision. -/// -/// This function handles `Option` types, serializing `Some` values with 2 decimal -/// places of precision and `None` values as null. For `Some` values, it rounds to 2 decimal -/// places and formats as a string, then parses back to f64 before serialization. This ensures -/// clean JSON output without IEEE 754 floating-point artifacts. -/// -/// # Arguments -/// -/// * `value` - The optional f32 value to serialize -/// * `serializer` - The serde serializer to use -/// -/// # Returns -/// -/// Result of the serialization operation -/// -/// # Behavior -/// -/// * `Some(v)` - Serializes the value rounded to 2 decimal places -/// * `None` - Serializes as JSON null -/// -/// # Usage -/// -/// This function can be used with Serde's `serialize_with` attribute: -/// ``` -/// use serde::Serialize; -/// use settings::serialize_optional_f32_with_two_decimal_places; -/// -/// #[derive(Serialize)] -/// struct ExampleStruct { -/// #[serde(serialize_with = "serialize_optional_f32_with_two_decimal_places")] -/// optional_value: Option, -/// } -/// ``` -pub fn serialize_optional_f32_with_two_decimal_places( - value: &Option, - serializer: S, -) -> Result -where - S: Serializer, -{ - match value { - Some(v) => { - let rounded = (v * 100.0).round() / 100.0; - let formatted = format!("{:.2}", rounded); - let clean_value: f64 = formatted.parse().unwrap_or(rounded as f64); - serializer.serialize_some(&clean_value) - } - None => serializer.serialize_none(), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use serde::{Deserialize, Serialize}; - - #[derive(Serialize, Deserialize)] - struct TestOptional { - #[serde(serialize_with = "serialize_optional_f32_with_two_decimal_places")] - value: Option, - } - - #[derive(Serialize, Deserialize)] - struct TestNonOptional { - #[serde(serialize_with = "serialize_f32_with_two_decimal_places")] - value: f32, - } - - #[test] - fn test_serialize_optional_f32_with_two_decimal_places() { - let cases = [ - (Some(123.456789), r#"{"value":123.46}"#), - (Some(1.2), r#"{"value":1.2}"#), - (Some(300.00000), r#"{"value":300.0}"#), - ]; - for (value, expected) in cases { - let value = TestOptional { value }; - assert_eq!(serde_json::to_string(&value).unwrap(), expected); - } - } - - #[test] - fn test_serialize_f32_with_two_decimal_places() { - let cases = [ - (123.456789, r#"{"value":123.46}"#), - (1.200, r#"{"value":1.2}"#), - (300.00000, r#"{"value":300.0}"#), - ]; - for (value, expected) in cases { - let value = TestNonOptional { value }; - assert_eq!(serde_json::to_string(&value).unwrap(), expected); - } - } -} diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs deleted file mode 100644 index 5f07ebe52f..0000000000 --- a/crates/settings/src/settings.rs +++ /dev/null @@ -1,144 +0,0 @@ -mod base_keymap_setting; -mod editable_setting_control; -mod fallible_options; -mod keymap_file; -pub mod merge_from; -mod serde_helper; -mod settings_content; -mod settings_file; -mod settings_store; -mod vscode_import; - -pub use settings_content::*; -pub use settings_macros::RegisterSetting; - -#[doc(hidden)] -pub mod private { - pub use crate::settings_store::{RegisteredSetting, SettingValue}; - pub use inventory; -} - -use gpui::{App, Global}; -use rust_embed::RustEmbed; -use std::{borrow::Cow, fmt, str}; -use util::asset_str; - -pub use base_keymap_setting::*; -pub use editable_setting_control::*; -pub use keymap_file::{ - KeyBindingValidator, KeyBindingValidatorRegistration, KeybindSource, KeybindUpdateOperation, - KeybindUpdateTarget, KeymapFile, KeymapFileLoadResult, -}; -pub use serde_helper::*; -pub use settings_file::*; -pub use settings_json::*; -pub use settings_store::{ - InvalidSettingsError, LocalSettingsKind, MigrationStatus, ParseStatus, Settings, SettingsFile, - SettingsJsonSchemaParams, SettingsKey, SettingsLocation, SettingsParseResult, SettingsStore, -}; - -pub use vscode_import::{VsCodeSettings, VsCodeSettingsSource}; - -pub use keymap_file::ActionSequence; - -#[derive(Clone, Debug, PartialEq)] -pub struct ActiveSettingsProfileName(pub String); - -impl Global for ActiveSettingsProfileName {} - -#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord, serde::Serialize)] -pub struct WorktreeId(usize); - -impl From for usize { - fn from(value: WorktreeId) -> Self { - value.0 - } -} - -impl WorktreeId { - pub fn from_usize(handle_id: usize) -> Self { - Self(handle_id) - } - - pub fn from_proto(id: u64) -> Self { - Self(id as usize) - } - - pub fn to_proto(self) -> u64 { - self.0 as u64 - } - - pub fn to_usize(self) -> usize { - self.0 - } -} - -impl fmt::Display for WorktreeId { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - std::fmt::Display::fmt(&self.0, f) - } -} - -#[derive(RustEmbed)] -#[folder = "../../assets"] -#[include = "settings/*"] -#[include = "keymaps/*"] -#[exclude = "*.DS_Store"] -pub struct SettingsAssets; - -pub fn init(cx: &mut App) { - let settings = SettingsStore::new(cx, &default_settings()); - cx.set_global(settings); - SettingsStore::observe_active_settings_profile_name(cx).detach(); -} - -pub fn default_settings() -> Cow<'static, str> { - asset_str::("settings/default.json") -} - -#[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")))] -pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-linux.json"; - -pub fn default_keymap() -> Cow<'static, str> { - asset_str::(DEFAULT_KEYMAP_PATH) -} - -pub const VIM_KEYMAP_PATH: &str = "keymaps/vim.json"; - -pub fn vim_keymap() -> Cow<'static, str> { - asset_str::(VIM_KEYMAP_PATH) -} - -pub fn initial_user_settings_content() -> Cow<'static, str> { - asset_str::("settings/initial_user_settings.json") -} - -pub fn initial_server_settings_content() -> Cow<'static, str> { - asset_str::("settings/initial_server_settings.json") -} - -pub fn initial_project_settings_content() -> Cow<'static, str> { - asset_str::("settings/initial_local_settings.json") -} - -pub fn initial_keymap_content() -> Cow<'static, str> { - asset_str::("keymaps/initial.json") -} - -pub fn initial_tasks_content() -> Cow<'static, str> { - asset_str::("settings/initial_tasks.json") -} - -pub fn initial_debug_tasks_content() -> Cow<'static, str> { - asset_str::("settings/initial_debug_tasks.json") -} - -pub fn initial_local_debug_tasks_content() -> Cow<'static, str> { - asset_str::("settings/initial_local_debug_tasks.json") -} diff --git a/crates/settings/src/settings_content.rs b/crates/settings/src/settings_content.rs deleted file mode 100644 index 743e22b04d..0000000000 --- a/crates/settings/src/settings_content.rs +++ /dev/null @@ -1,1053 +0,0 @@ -mod agent; -mod editor; -mod extension; -mod language; -mod language_model; -mod project; -mod terminal; -mod theme; -mod workspace; - -pub use agent::*; -pub use editor::*; -pub use extension::*; -pub use language::*; -pub use language_model::*; -pub use project::*; -pub use terminal::*; -pub use theme::*; -pub use workspace::*; - -use collections::{HashMap, IndexMap}; -use gpui::{App, SharedString}; -use release_channel::ReleaseChannel; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings_macros::{MergeFrom, with_fallible_options}; -use std::collections::BTreeSet; -use std::env; -use std::sync::Arc; -pub use util::serde::default_true; - -use crate::{ActiveSettingsProfileName, merge_from}; - -#[with_fallible_options] -#[derive(Debug, PartialEq, Default, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct SettingsContent { - #[serde(flatten)] - pub project: ProjectSettingsContent, - - #[serde(flatten)] - pub theme: Box, - - #[serde(flatten)] - pub extension: ExtensionSettingsContent, - - #[serde(flatten)] - pub workspace: WorkspaceSettingsContent, - - #[serde(flatten)] - pub editor: EditorSettingsContent, - - #[serde(flatten)] - pub remote: RemoteSettingsContent, - - /// Settings related to the file finder. - pub file_finder: Option, - - pub git_panel: Option, - - pub tabs: Option, - pub tab_bar: Option, - pub status_bar: Option, - - pub preview_tabs: Option, - - pub agent: Option, - pub agent_servers: Option, - - /// Configuration of audio in Zed. - pub audio: Option, - - /// Whether or not to automatically check for updates. - /// - /// Default: true - pub auto_update: Option, - - /// This base keymap settings adjusts the default keybindings in Zed to be similar - /// to other common code editors. By default, Zed's keymap closely follows VSCode's - /// keymap, with minor adjustments, this corresponds to the "VSCode" setting. - /// - /// Default: VSCode - pub base_keymap: Option, - - /// Configuration for the collab panel visual settings. - pub collaboration_panel: Option, - - pub debugger: Option, - - /// Configuration for Diagnostics-related features. - pub diagnostics: Option, - - /// Configuration for Git-related features - pub git: Option, - - /// Common language server settings. - pub global_lsp_settings: Option, - - /// The settings for the image viewer. - pub image_viewer: Option, - - pub repl: Option, - - /// Whether or not to enable Helix mode. - /// - /// Default: false - pub helix_mode: Option, - - pub journal: Option, - - /// A map of log scopes to the desired log level. - /// Useful for filtering out noisy logs or enabling more verbose logging. - /// - /// Example: {"log": {"client": "warn"}} - pub log: Option>, - - pub line_indicator_format: Option, - - pub language_models: Option, - - pub outline_panel: Option, - - pub project_panel: Option, - - /// Configuration for the Message Editor - pub message_editor: Option, - - /// Configuration for Node-related features - pub node: Option, - - /// Configuration for the Notification Panel - pub notification_panel: Option, - - pub proxy: Option, - - /// The URL of the Zed server to connect to. - pub server_url: Option, - - /// Configuration for session-related features - pub session: Option, - /// Control what info is collected by Zed. - pub telemetry: Option, - - /// Configuration of the terminal in Zed. - pub terminal: Option, - - pub title_bar: Option, - - /// Whether or not to enable Vim mode. - /// - /// Default: false - pub vim_mode: Option, - - // Settings related to calls in Zed - pub calls: Option, - - /// Whether to disable all AI features in Zed. - /// - /// Default: false - pub disable_ai: Option, - - /// Settings related to Vim mode in Zed. - pub vim: Option, -} - -impl SettingsContent { - pub fn languages_mut(&mut self) -> &mut HashMap { - &mut self.project.all_languages.languages.0 - } -} - -#[with_fallible_options] -#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct UserSettingsContent { - #[serde(flatten)] - pub content: Box, - - pub dev: Option>, - pub nightly: Option>, - pub preview: Option>, - pub stable: Option>, - - pub macos: Option>, - pub windows: Option>, - pub linux: Option>, - - #[serde(default)] - pub profiles: IndexMap, -} - -pub struct ExtensionsSettingsContent { - pub all_languages: AllLanguageSettingsContent, -} - -impl UserSettingsContent { - pub fn for_release_channel(&self) -> Option<&SettingsContent> { - match *release_channel::RELEASE_CHANNEL { - ReleaseChannel::Dev => self.dev.as_deref(), - ReleaseChannel::Nightly => self.nightly.as_deref(), - ReleaseChannel::Preview => self.preview.as_deref(), - ReleaseChannel::Stable => self.stable.as_deref(), - } - } - - pub fn for_os(&self) -> Option<&SettingsContent> { - match env::consts::OS { - "macos" => self.macos.as_deref(), - "linux" => self.linux.as_deref(), - "windows" => self.windows.as_deref(), - _ => None, - } - } - - pub fn for_profile(&self, cx: &App) -> Option<&SettingsContent> { - let Some(active_profile) = cx.try_global::() else { - return None; - }; - self.profiles.get(&active_profile.0) - } -} - -/// Base key bindings scheme. Base keymaps can be overridden with user keymaps. -/// -/// Default: VSCode -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - Default, - strum::VariantArray, -)] -pub enum BaseKeymapContent { - #[default] - VSCode, - JetBrains, - SublimeText, - Atom, - TextMate, - Emacs, - Cursor, - None, -} - -impl strum::VariantNames for BaseKeymapContent { - const VARIANTS: &'static [&'static str] = &[ - "VSCode", - "JetBrains", - "Sublime Text", - "Atom", - "TextMate", - "Emacs", - "Cursor", - "None", - ]; -} - -#[with_fallible_options] -#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)] -pub struct TitleBarSettingsContent { - /// Whether to show the branch icon beside branch switcher in the title bar. - /// - /// Default: false - pub show_branch_icon: Option, - /// Whether to show onboarding banners in the title bar. - /// - /// Default: true - pub show_onboarding_banner: Option, - /// Whether to show user avatar in the title bar. - /// - /// Default: true - pub show_user_picture: Option, - /// Whether to show the branch name button in the titlebar. - /// - /// Default: true - pub show_branch_name: Option, - /// Whether to show the project host and name in the titlebar. - /// - /// Default: true - pub show_project_items: Option, - /// Whether to show the sign in button in the title bar. - /// - /// Default: true - pub show_sign_in: Option, - /// Whether to show the menus in the title bar. - /// - /// Default: false - pub show_menus: Option, -} - -/// Configuration of audio in Zed. -#[with_fallible_options] -#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)] -pub struct AudioSettingsContent { - /// Opt into the new audio system. - /// - /// You need to rejoin a call for this setting to apply - #[serde(rename = "experimental.rodio_audio")] - pub rodio_audio: Option, // default is false - /// Requires 'rodio_audio: true' - /// - /// Automatically increase or decrease you microphone's volume. This affects how - /// loud you sound to others. - /// - /// Recommended: off (default) - /// Microphones are too quite in zed, until everyone is on experimental - /// audio and has auto speaker volume on this will make you very loud - /// compared to other speakers. - #[serde(rename = "experimental.auto_microphone_volume")] - pub auto_microphone_volume: Option, - /// Requires 'rodio_audio: true' - /// - /// Automatically increate or decrease the volume of other call members. - /// This only affects how things sound for you. - #[serde(rename = "experimental.auto_speaker_volume")] - pub auto_speaker_volume: Option, - /// Requires 'rodio_audio: true' - /// - /// Remove background noises. Works great for typing, cars, dogs, AC. Does - /// not work well on music. - #[serde(rename = "experimental.denoise")] - pub denoise: Option, - /// Requires 'rodio_audio: true' - /// - /// Use audio parameters compatible with the previous versions of - /// experimental audio and non-experimental audio. When this is false you - /// will sound strange to anyone not on the latest experimental audio. In - /// the future we will migrate by setting this to false - /// - /// You need to rejoin a call for this setting to apply - #[serde(rename = "experimental.legacy_audio_compatible")] - pub legacy_audio_compatible: Option, -} - -/// Control what info is collected by Zed. -#[with_fallible_options] -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Debug, MergeFrom)] -pub struct TelemetrySettingsContent { - /// Send debug info like crash reports. - /// - /// Default: true - pub diagnostics: Option, - /// Send anonymized usage data like what languages you're using Zed with. - /// - /// Default: true - pub metrics: Option, -} - -impl Default for TelemetrySettingsContent { - fn default() -> Self { - Self { - diagnostics: Some(true), - metrics: Some(true), - } - } -} - -#[with_fallible_options] -#[derive(Default, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Clone, MergeFrom)] -pub struct DebuggerSettingsContent { - /// Determines the stepping granularity. - /// - /// Default: line - pub stepping_granularity: Option, - /// Whether the breakpoints should be reused across Zed sessions. - /// - /// Default: true - pub save_breakpoints: Option, - /// Whether to show the debug button in the status bar. - /// - /// Default: true - pub button: Option, - /// Time in milliseconds until timeout error when connecting to a TCP debug adapter - /// - /// Default: 2000ms - pub timeout: Option, - /// Whether to log messages between active debug adapters and Zed - /// - /// Default: true - pub log_dap_communications: Option, - /// Whether to format dap messages in when adding them to debug adapter logger - /// - /// Default: true - pub format_dap_log_messages: Option, - /// The dock position of the debug panel - /// - /// Default: Bottom - pub dock: Option, -} - -/// The granularity of one 'step' in the stepping requests `next`, `stepIn`, `stepOut`, and `stepBack`. -#[derive( - PartialEq, - Eq, - Debug, - Hash, - Clone, - Copy, - Deserialize, - Serialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum SteppingGranularity { - /// The step should allow the program to run until the current statement has finished executing. - /// The meaning of a statement is determined by the adapter and it may be considered equivalent to a line. - /// For example 'for(int i = 0; i < 10; i++)' could be considered to have 3 statements 'int i = 0', 'i < 10', and 'i++'. - Statement, - /// The step should allow the program to run until the current source line has executed. - Line, - /// The step should allow one instruction to execute (e.g. one x86 instruction). - Instruction, -} - -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum DockPosition { - Left, - Bottom, - Right, -} - -/// Settings for slash commands. -#[with_fallible_options] -#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, MergeFrom, PartialEq, Eq)] -pub struct SlashCommandSettings { - /// Settings for the `/cargo-workspace` slash command. - pub cargo_workspace: Option, -} - -/// Settings for the `/cargo-workspace` slash command. -#[with_fallible_options] -#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, MergeFrom, PartialEq, Eq)] -pub struct CargoWorkspaceCommandSettings { - /// Whether `/cargo-workspace` is enabled. - pub enabled: Option, -} - -/// Configuration of voice calls in Zed. -#[with_fallible_options] -#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)] -pub struct CallSettingsContent { - /// Whether the microphone should be muted when joining a channel or a call. - /// - /// Default: false - pub mute_on_join: Option, - - /// Whether your current project should be shared when joining an empty channel. - /// - /// Default: false - pub share_on_join: Option, -} - -#[with_fallible_options] -#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)] -pub struct GitPanelSettingsContent { - /// Whether to show the panel button in the status bar. - /// - /// Default: true - pub button: Option, - /// Where to dock the panel. - /// - /// Default: left - pub dock: Option, - /// Default width of the panel in pixels. - /// - /// Default: 360 - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub default_width: Option, - /// How entry statuses are displayed. - /// - /// Default: icon - pub status_style: Option, - /// How and when the scrollbar should be displayed. - /// - /// Default: inherits editor scrollbar settings - pub scrollbar: Option, - - /// What the default branch name should be when - /// `init.defaultBranch` is not set in git - /// - /// Default: main - pub fallback_branch_name: Option, - - /// Whether to sort entries in the panel by path - /// or by status (the default). - /// - /// Default: false - pub sort_by_path: Option, - - /// Whether to collapse untracked files in the diff panel. - /// - /// Default: false - pub collapse_untracked_diff: Option, - - /// Whether to show entries with tree or flat view in the panel - /// - /// Default: false - pub tree_view: Option, -} - -#[derive( - Default, - Copy, - Clone, - Debug, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum StatusStyle { - #[default] - Icon, - LabelColor, -} - -#[with_fallible_options] -#[derive( - Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq, -)] -pub struct ScrollbarSettings { - pub show: Option, -} - -#[with_fallible_options] -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)] -pub struct NotificationPanelSettingsContent { - /// Whether to show the panel button in the status bar. - /// - /// Default: true - pub button: Option, - /// Where to dock the panel. - /// - /// Default: right - pub dock: Option, - /// Default width of the panel in pixels. - /// - /// Default: 300 - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub default_width: Option, -} - -#[with_fallible_options] -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)] -pub struct PanelSettingsContent { - /// Whether to show the panel button in the status bar. - /// - /// Default: true - pub button: Option, - /// Where to dock the panel. - /// - /// Default: left - pub dock: Option, - /// Default width of the panel in pixels. - /// - /// Default: 240 - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub default_width: Option, -} - -#[with_fallible_options] -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)] -pub struct MessageEditorSettings { - /// Whether to automatically replace emoji shortcodes with emoji characters. - /// For example: typing `:wave:` gets replaced with `👋`. - /// - /// Default: false - pub auto_replace_emoji_shortcode: Option, -} - -#[with_fallible_options] -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)] -pub struct FileFinderSettingsContent { - /// Whether to show file icons in the file finder. - /// - /// Default: true - pub file_icons: Option, - /// Determines how much space the file finder can take up in relation to the available window width. - /// - /// Default: small - pub modal_max_width: Option, - /// Determines whether the file finder should skip focus for the active file in search results. - /// - /// Default: true - pub skip_focus_for_active_in_search: Option, - /// Determines whether to show the git status in the file finder - /// - /// Default: true - pub git_status: Option, - /// Whether to use gitignored files when searching. - /// Only the file Zed had indexed will be used, not necessary all the gitignored files. - /// - /// Default: Smart - pub include_ignored: Option, -} - -#[derive( - Debug, - PartialEq, - Eq, - Clone, - Copy, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum IncludeIgnoredContent { - /// Use all gitignored files - All, - /// Use only the files Zed had indexed - Indexed, - /// Be smart and search for ignored when called from a gitignored worktree - #[default] - Smart, -} - -#[derive( - Debug, - PartialEq, - Eq, - Clone, - Copy, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "lowercase")] -pub enum FileFinderWidthContent { - #[default] - Small, - Medium, - Large, - XLarge, - Full, -} - -#[with_fallible_options] -#[derive(Clone, Default, Serialize, Deserialize, PartialEq, Debug, JsonSchema, MergeFrom)] -pub struct VimSettingsContent { - pub default_mode: Option, - pub toggle_relative_line_numbers: Option, - pub use_system_clipboard: Option, - pub use_smartcase_find: Option, - pub custom_digraphs: Option>>, - pub highlight_on_yank_duration: Option, - pub cursor_shape: Option, -} - -#[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Debug)] -#[serde(rename_all = "snake_case")] -pub enum ModeContent { - #[default] - Normal, - Insert, -} - -/// Controls when to use system clipboard. -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -pub enum UseSystemClipboard { - /// Don't use system clipboard. - Never, - /// Use system clipboard. - Always, - /// Use system clipboard for yank operations. - OnYank, -} - -/// The settings for cursor shape. -#[with_fallible_options] -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)] -pub struct CursorShapeSettings { - /// Cursor shape for the normal mode. - /// - /// Default: block - pub normal: Option, - /// Cursor shape for the replace mode. - /// - /// Default: underline - pub replace: Option, - /// Cursor shape for the visual mode. - /// - /// Default: block - pub visual: Option, - /// Cursor shape for the insert mode. - /// - /// The default value follows the primary cursor_shape. - pub insert: Option, -} - -/// Settings specific to journaling -#[with_fallible_options] -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -pub struct JournalSettingsContent { - /// The path of the directory where journal entries are stored. - /// - /// Default: `~` - pub path: Option, - /// What format to display the hours in. - /// - /// Default: hour12 - pub hour_format: Option, -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -#[serde(rename_all = "snake_case")] -pub enum HourFormat { - #[default] - Hour12, - Hour24, -} - -#[with_fallible_options] -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)] -pub struct OutlinePanelSettingsContent { - /// Whether to show the outline panel button in the status bar. - /// - /// Default: true - pub button: Option, - /// Customize default width (in pixels) taken by outline panel - /// - /// Default: 240 - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub default_width: Option, - /// The position of outline panel - /// - /// Default: left - pub dock: Option, - /// Whether to show file icons in the outline panel. - /// - /// Default: true - pub file_icons: Option, - /// Whether to show folder icons or chevrons for directories in the outline panel. - /// - /// Default: true - pub folder_icons: Option, - /// Whether to show the git status in the outline panel. - /// - /// Default: true - pub git_status: Option, - /// Amount of indentation (in pixels) for nested items. - /// - /// Default: 20 - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub indent_size: Option, - /// Whether to reveal it in the outline panel automatically, - /// when a corresponding project entry becomes active. - /// Gitignored entries are never auto revealed. - /// - /// Default: true - pub auto_reveal_entries: Option, - /// Whether to fold directories automatically - /// when directory has only one directory inside. - /// - /// Default: true - pub auto_fold_dirs: Option, - /// Settings related to indent guides in the outline panel. - pub indent_guides: Option, - /// Scrollbar-related settings - pub scrollbar: Option, - /// Default depth to expand outline items in the current file. - /// The default depth to which outline entries are expanded on reveal. - /// - Set to 0 to collapse all items that have children - /// - Set to 1 or higher to collapse items at that depth or deeper - /// - /// Default: 100 - pub expand_outlines_with_depth: Option, -} - -#[derive( - Clone, - Copy, - Debug, - PartialEq, - Eq, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum DockSide { - Left, - Right, -} - -#[derive( - Copy, - Clone, - Debug, - PartialEq, - Eq, - Deserialize, - Serialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum ShowIndentGuides { - Always, - Never, -} - -#[with_fallible_options] -#[derive( - Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq, Default, -)] -pub struct IndentGuidesSettingsContent { - /// When to show the scrollbar in the outline panel. - pub show: Option, -} - -#[derive(Clone, Copy, Default, PartialEq, Debug, JsonSchema, MergeFrom, Deserialize, Serialize)] -#[serde(rename_all = "snake_case")] -pub enum LineIndicatorFormat { - Short, - #[default] - Long, -} - -/// The settings for the image viewer. -#[with_fallible_options] -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, Default, PartialEq)] -pub struct ImageViewerSettingsContent { - /// The unit to use for displaying image file sizes. - /// - /// Default: "binary" - pub unit: Option, -} - -#[with_fallible_options] -#[derive( - Clone, - Copy, - Debug, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - Default, - PartialEq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum ImageFileSizeUnit { - /// Displays file size in binary units (e.g., KiB, MiB). - #[default] - Binary, - /// Displays file size in decimal units (e.g., KB, MB). - Decimal, -} - -#[with_fallible_options] -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -pub struct RemoteSettingsContent { - pub ssh_connections: Option>, - pub wsl_connections: Option>, - pub dev_container_connections: Option>, - pub read_ssh_config: Option, -} - -#[with_fallible_options] -#[derive( - Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom, Hash, -)] -pub struct DevContainerConnection { - pub name: SharedString, - pub container_id: SharedString, -} - -#[with_fallible_options] -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -pub struct SshConnection { - pub host: SharedString, - pub username: Option, - pub port: Option, - #[serde(default)] - pub args: Vec, - #[serde(default)] - pub projects: collections::BTreeSet, - /// Name to use for this server in UI. - pub nickname: Option, - // By default Zed will download the binary to the host directly. - // If this is set to true, Zed will download the binary to your local machine, - // and then upload it over the SSH connection. Useful if your SSH server has - // limited outbound internet access. - pub upload_binary_over_ssh: Option, - - pub port_forwards: Option>, -} - -#[derive(Clone, Default, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom, Debug)] -pub struct WslConnection { - pub distro_name: SharedString, - pub user: Option, - #[serde(default)] - pub projects: BTreeSet, -} - -#[with_fallible_options] -#[derive( - Clone, Debug, Default, Serialize, PartialEq, Eq, PartialOrd, Ord, Deserialize, JsonSchema, -)] -pub struct RemoteProject { - pub paths: Vec, -} - -#[with_fallible_options] -#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize, JsonSchema, MergeFrom)] -pub struct SshPortForwardOption { - pub local_host: Option, - pub local_port: u16, - pub remote_host: Option, - pub remote_port: u16, -} - -/// Settings for configuring REPL display and behavior. -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct ReplSettingsContent { - /// Maximum number of lines to keep in REPL's scrollback buffer. - /// Clamped with [4, 256] range. - /// - /// Default: 32 - pub max_lines: Option, - /// Maximum number of columns to keep in REPL's scrollback buffer. - /// Clamped with [20, 512] range. - /// - /// Default: 128 - pub max_columns: Option, -} - -#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] -/// An ExtendingVec in the settings can only accumulate new values. -/// -/// This is useful for things like private files where you only want -/// to allow new values to be added. -/// -/// Consider using a HashMap instead of this type -/// (like auto_install_extensions) so that user settings files can both add -/// and remove values from the set. -pub struct ExtendingVec(pub Vec); - -impl Into> for ExtendingVec { - fn into(self) -> Vec { - self.0 - } -} -impl From> for ExtendingVec { - fn from(vec: Vec) -> Self { - ExtendingVec(vec) - } -} - -impl merge_from::MergeFrom for ExtendingVec { - fn merge_from(&mut self, other: &Self) { - self.0.extend_from_slice(other.0.as_slice()); - } -} - -/// A SaturatingBool in the settings can only ever be set to true, -/// later attempts to set it to false will be ignored. -/// -/// Used by `disable_ai`. -#[derive(Debug, Default, Copy, Clone, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct SaturatingBool(pub bool); - -impl From for SaturatingBool { - fn from(value: bool) -> Self { - SaturatingBool(value) - } -} - -impl From for bool { - fn from(value: SaturatingBool) -> bool { - value.0 - } -} - -impl merge_from::MergeFrom for SaturatingBool { - fn merge_from(&mut self, other: &Self) { - self.0 |= other.0 - } -} - -#[derive( - Copy, - Clone, - Default, - Debug, - PartialEq, - Eq, - PartialOrd, - Ord, - Serialize, - Deserialize, - MergeFrom, - JsonSchema, - derive_more::FromStr, -)] -#[serde(transparent)] -pub struct DelayMs(pub u64); - -impl From for DelayMs { - fn from(n: u64) -> Self { - Self(n) - } -} - -impl std::fmt::Display for DelayMs { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}ms", self.0) - } -} diff --git a/crates/settings/src/settings_content/agent.rs b/crates/settings/src/settings_content/agent.rs deleted file mode 100644 index 2ea9f0cd57..0000000000 --- a/crates/settings/src/settings_content/agent.rs +++ /dev/null @@ -1,379 +0,0 @@ -use collections::{HashMap, IndexMap}; -use gpui::SharedString; -use schemars::{JsonSchema, json_schema}; -use serde::{Deserialize, Serialize}; -use settings_macros::{MergeFrom, with_fallible_options}; -use std::{borrow::Cow, path::PathBuf, sync::Arc}; - -use crate::DockPosition; - -#[with_fallible_options] -#[derive(Clone, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, Default)] -pub struct AgentSettingsContent { - /// Whether the Agent is enabled. - /// - /// Default: true - pub enabled: Option, - /// Whether to show the agent panel button in the status bar. - /// - /// Default: true - pub button: Option, - /// Where to dock the agent panel. - /// - /// Default: right - pub dock: Option, - /// Default width in pixels when the agent panel is docked to the left or right. - /// - /// Default: 640 - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub default_width: Option, - /// Default height in pixels when the agent panel is docked to the bottom. - /// - /// Default: 320 - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub default_height: Option, - /// The default model to use when creating new chats and for other features when a specific model is not specified. - pub default_model: Option, - /// Model to use for the inline assistant. Defaults to default_model when not specified. - pub inline_assistant_model: Option, - /// Model to use for generating git commit messages. Defaults to default_model when not specified. - pub commit_message_model: Option, - /// Model to use for generating thread summaries. Defaults to default_model when not specified. - pub thread_summary_model: Option, - /// Additional models with which to generate alternatives when performing inline assists. - pub inline_alternatives: Option>, - /// The default profile to use in the Agent. - /// - /// Default: write - pub default_profile: Option>, - /// Which view type to show by default in the agent panel. - /// - /// Default: "thread" - pub default_view: Option, - /// The available agent profiles. - pub profiles: Option, AgentProfileContent>>, - /// Whenever a tool action would normally wait for your confirmation - /// that you allow it, always choose to allow it. - /// - /// This setting has no effect on external agents that support permission modes, such as Claude Code. - /// - /// Set `agent_servers.claude.default_mode` to `bypassPermissions`, to disable all permission requests when using Claude Code. - /// - /// Default: false - pub always_allow_tool_actions: Option, - /// Where to show a popup notification when the agent is waiting for user input. - /// - /// Default: "primary_screen" - pub notify_when_agent_waiting: Option, - /// Whether to play a sound when the agent has either completed its response, or needs user input. - /// - /// Default: false - pub play_sound_when_agent_done: Option, - /// Whether to display agent edits in single-file editors in addition to the review multibuffer pane. - /// - /// Default: true - pub single_file_review: Option, - /// Additional parameters for language model requests. When making a request - /// to a model, parameters will be taken from the last entry in this list - /// that matches the model's provider and name. In each entry, both provider - /// and model are optional, so that you can specify parameters for either - /// one. - /// - /// Default: [] - #[serde(default)] - pub model_parameters: Vec, - /// What completion mode to enable for new threads - /// - /// Default: normal - pub preferred_completion_mode: Option, - /// Whether to show thumb buttons for feedback in the agent panel. - /// - /// Default: true - pub enable_feedback: Option, - /// Whether to have edit cards in the agent panel expanded, showing a preview of the full diff. - /// - /// Default: true - pub expand_edit_card: Option, - /// Whether to have terminal cards in the agent panel expanded, showing the whole command output. - /// - /// Default: true - pub expand_terminal_card: Option, - /// Whether to always use cmd-enter (or ctrl-enter on Linux or Windows) to send messages in the agent panel. - /// - /// Default: false - pub use_modifier_to_send: Option, - /// Minimum number of lines of height the agent message editor should have. - /// - /// Default: 4 - pub message_editor_min_lines: Option, -} - -impl AgentSettingsContent { - pub fn set_dock(&mut self, dock: DockPosition) { - self.dock = Some(dock); - } - - pub fn set_model(&mut self, language_model: LanguageModelSelection) { - // let model = language_model.id().0.to_string(); - // let provider = language_model.provider_id().0.to_string(); - // self.default_model = Some(LanguageModelSelection { - // provider: provider.into(), - // model, - // }); - self.default_model = Some(language_model) - } - - pub fn set_inline_assistant_model(&mut self, provider: String, model: String) { - self.inline_assistant_model = Some(LanguageModelSelection { - provider: provider.into(), - model, - }); - } - - pub fn set_commit_message_model(&mut self, provider: String, model: String) { - self.commit_message_model = Some(LanguageModelSelection { - provider: provider.into(), - model, - }); - } - - pub fn set_thread_summary_model(&mut self, provider: String, model: String) { - self.thread_summary_model = Some(LanguageModelSelection { - provider: provider.into(), - model, - }); - } - - pub fn set_always_allow_tool_actions(&mut self, allow: bool) { - self.always_allow_tool_actions = Some(allow); - } - - pub fn set_play_sound_when_agent_done(&mut self, allow: bool) { - self.play_sound_when_agent_done = Some(allow); - } - - pub fn set_single_file_review(&mut self, allow: bool) { - self.single_file_review = Some(allow); - } - - pub fn set_use_modifier_to_send(&mut self, always_use: bool) { - self.use_modifier_to_send = Some(always_use); - } - - pub fn set_profile(&mut self, profile_id: Arc) { - self.default_profile = Some(profile_id); - } -} - -#[with_fallible_options] -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct AgentProfileContent { - pub name: Arc, - #[serde(default)] - pub tools: IndexMap, bool>, - /// Whether all context servers are enabled by default. - pub enable_all_context_servers: Option, - #[serde(default)] - pub context_servers: IndexMap, ContextServerPresetContent>, - /// The default language model selected when using this profile. - pub default_model: Option, -} - -#[with_fallible_options] -#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct ContextServerPresetContent { - pub tools: IndexMap, bool>, -} - -#[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -pub enum DefaultAgentView { - #[default] - Thread, - TextThread, -} - -#[derive( - Copy, - Clone, - Default, - Debug, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum NotifyWhenAgentWaiting { - #[default] - PrimaryScreen, - AllScreens, - Never, -} - -#[with_fallible_options] -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -pub struct LanguageModelSelection { - pub provider: LanguageModelProviderSetting, - pub model: String, -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Default)] -#[serde(rename_all = "snake_case")] -pub enum CompletionMode { - #[default] - Normal, - #[serde(alias = "max")] - Burn, -} - -#[with_fallible_options] -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -pub struct LanguageModelParameters { - pub provider: Option, - pub model: Option, - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub temperature: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, MergeFrom)] -pub struct LanguageModelProviderSetting(pub String); - -impl JsonSchema for LanguageModelProviderSetting { - fn schema_name() -> Cow<'static, str> { - "LanguageModelProviderSetting".into() - } - - fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { - // list the builtin providers as a subset so that we still auto complete them in the settings - json_schema!({ - "anyOf": [ - { - "type": "string", - "enum": [ - "amazon-bedrock", - "anthropic", - "copilot_chat", - "deepseek", - "google", - "lmstudio", - "mistral", - "ollama", - "openai", - "openrouter", - "vercel", - "x_ai", - "zed.dev" - ] - }, - { - "type": "string", - } - ] - }) - } -} - -impl From for LanguageModelProviderSetting { - fn from(provider: String) -> Self { - Self(provider) - } -} - -impl From<&str> for LanguageModelProviderSetting { - fn from(provider: &str) -> Self { - Self(provider.to_string()) - } -} - -#[with_fallible_options] -#[derive(Default, PartialEq, Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug)] -pub struct AllAgentServersSettings { - pub gemini: Option, - pub claude: Option, - pub codex: Option, - - /// Custom agent servers configured by the user - #[serde(flatten)] - pub custom: HashMap, -} - -#[with_fallible_options] -#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug, PartialEq)] -pub struct BuiltinAgentServerSettings { - /// Absolute path to a binary to be used when launching this agent. - /// - /// This can be used to run a specific binary without automatic downloads or searching `$PATH`. - #[serde(rename = "command")] - pub path: Option, - /// If a binary is specified in `command`, it will be passed these arguments. - pub args: Option>, - /// If a binary is specified in `command`, it will be passed these environment variables. - pub env: Option>, - /// Whether to skip searching `$PATH` for an agent server binary when - /// launching this agent. - /// - /// This has no effect if a `command` is specified. Otherwise, when this is - /// `false`, Zed will search `$PATH` for an agent server binary and, if one - /// is found, use it for threads with this agent. If no agent binary is - /// found on `$PATH`, Zed will automatically install and use its own binary. - /// When this is `true`, Zed will not search `$PATH`, and will always use - /// its own binary. - /// - /// Default: true - pub ignore_system_version: Option, - /// The default mode to use for this agent. - /// - /// Note: Not only all agents support modes. - /// - /// Default: None - pub default_mode: Option, - /// The default model to use for this agent. - /// - /// This should be the model ID as reported by the agent. - /// - /// Default: None - pub default_model: Option, -} - -#[with_fallible_options] -#[derive(Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug, PartialEq)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum CustomAgentServerSettings { - Custom { - #[serde(rename = "command")] - path: PathBuf, - #[serde(default)] - args: Vec, - env: Option>, - /// The default mode to use for this agent. - /// - /// Note: Not only all agents support modes. - /// - /// Default: None - default_mode: Option, - /// The default model to use for this agent. - /// - /// This should be the model ID as reported by the agent. - /// - /// Default: None - default_model: Option, - }, - Extension { - /// The default mode to use for this agent. - /// - /// Note: Not only all agents support modes. - /// - /// Default: None - default_mode: Option, - /// The default model to use for this agent. - /// - /// This should be the model ID as reported by the agent. - /// - /// Default: None - default_model: Option, - }, -} diff --git a/crates/settings/src/settings_content/editor.rs b/crates/settings/src/settings_content/editor.rs deleted file mode 100644 index 9ec5542e9b..0000000000 --- a/crates/settings/src/settings_content/editor.rs +++ /dev/null @@ -1,983 +0,0 @@ -use std::fmt::Display; -use std::num; - -use collections::HashMap; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings_macros::{MergeFrom, with_fallible_options}; - -use crate::{ - DelayMs, DiagnosticSeverityContent, ShowScrollbar, serialize_f32_with_two_decimal_places, -}; - -#[with_fallible_options] -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct EditorSettingsContent { - /// Whether the cursor blinks in the editor. - /// - /// Default: true - pub cursor_blink: Option, - /// Cursor shape for the default editor. - /// Can be "bar", "block", "underline", or "hollow". - /// - /// Default: bar - pub cursor_shape: Option, - /// Determines when the mouse cursor should be hidden in an editor or input box. - /// - /// Default: on_typing_and_movement - pub hide_mouse: Option, - /// Determines how snippets are sorted relative to other completion items. - /// - /// Default: inline - pub snippet_sort_order: Option, - /// How to highlight the current line in the editor. - /// - /// Default: all - pub current_line_highlight: Option, - /// Whether to highlight all occurrences of the selected text in an editor. - /// - /// Default: true - pub selection_highlight: Option, - /// Whether the text selection should have rounded corners. - /// - /// Default: true - pub rounded_selection: Option, - /// The debounce delay before querying highlights from the language - /// server based on the current cursor location. - /// - /// Default: 75 - pub lsp_highlight_debounce: Option, - /// Whether to show the informational hover box when moving the mouse - /// over symbols in the editor. - /// - /// Default: true - pub hover_popover_enabled: Option, - /// Time to wait in milliseconds before showing the informational hover box. - /// - /// Default: 300 - pub hover_popover_delay: Option, - /// Toolbar related settings - pub toolbar: Option, - /// Scrollbar related settings - pub scrollbar: Option, - /// Minimap related settings - pub minimap: Option, - /// Gutter related settings - pub gutter: Option, - /// Whether the editor will scroll beyond the last line. - /// - /// Default: one_page - pub scroll_beyond_last_line: Option, - /// The number of lines to keep above/below the cursor when auto-scrolling. - /// - /// Default: 3. - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub vertical_scroll_margin: Option, - /// Whether to scroll when clicking near the edge of the visible text area. - /// - /// Default: false - pub autoscroll_on_clicks: Option, - /// The number of characters to keep on either side when scrolling with the mouse. - /// - /// Default: 5. - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub horizontal_scroll_margin: Option, - /// Scroll sensitivity multiplier. This multiplier is applied - /// to both the horizontal and vertical delta values while scrolling. - /// - /// Default: 1.0 - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub scroll_sensitivity: Option, - /// Scroll sensitivity multiplier for fast scrolling. This multiplier is applied - /// to both the horizontal and vertical delta values while scrolling. Fast scrolling - /// happens when a user holds the alt or option key while scrolling. - /// - /// Default: 4.0 - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub fast_scroll_sensitivity: Option, - /// Settings for sticking scopes to the top of the editor. - /// - /// Default: sticky scroll is disabled - pub sticky_scroll: Option, - /// Whether the line numbers on editors gutter are relative or not. - /// When "enabled" shows relative number of buffer lines, when "wrapped" shows - /// relative number of display lines. - /// - /// Default: "disabled" - pub relative_line_numbers: Option, - /// When to populate a new search's query based on the text under the cursor. - /// - /// Default: always - pub seed_search_query_from_cursor: Option, - pub use_smartcase_search: Option, - /// Determines the modifier to be used to add multiple cursors with the mouse. The open hover link mouse gestures will adapt such that it do not conflict with the multicursor modifier. - /// - /// Default: alt - pub multi_cursor_modifier: Option, - /// Hide the values of variables in `private` files, as defined by the - /// private_files setting. This only changes the visual representation, - /// the values are still present in the file and can be selected / copied / pasted - /// - /// Default: false - pub redact_private_values: Option, - - /// How many lines to expand the multibuffer excerpts by default - /// - /// Default: 3 - pub expand_excerpt_lines: Option, - - /// How many lines of context to provide in multibuffer excerpts by default - /// - /// Default: 2 - pub excerpt_context_lines: Option, - - /// Whether to enable middle-click paste on Linux - /// - /// Default: true - pub middle_click_paste: Option, - - /// What to do when multibuffer is double clicked in some of its excerpts - /// (parts of singleton buffers). - /// - /// Default: select - pub double_click_in_multibuffer: Option, - /// Whether the editor search results will loop - /// - /// Default: true - pub search_wrap: Option, - - /// Defaults to use when opening a new buffer and project search items. - /// - /// Default: nothing is enabled - pub search: Option, - - /// Whether to automatically show a signature help pop-up or not. - /// - /// Default: false - pub auto_signature_help: Option, - - /// Whether to show the signature help pop-up after completions or bracket pairs inserted. - /// - /// Default: false - pub show_signature_help_after_edits: Option, - /// The minimum APCA perceptual contrast to maintain when - /// rendering text over highlight backgrounds in the editor. - /// - /// Values range from 0 to 106. Set to 0 to disable adjustments. - /// Default: 45 - #[schemars(range(min = 0, max = 106))] - pub minimum_contrast_for_highlights: Option, - - /// Whether to follow-up empty go to definition responses from the language server or not. - /// `FindAllReferences` allows to look up references of the same symbol instead. - /// `None` disables the fallback. - /// - /// Default: FindAllReferences - pub go_to_definition_fallback: Option, - - /// Jupyter REPL settings. - pub jupyter: Option, - - /// Which level to use to filter out diagnostics displayed in the editor. - /// - /// Affects the editor rendering only, and does not interrupt - /// the functionality of diagnostics fetching and project diagnostics editor. - /// Which files containing diagnostic errors/warnings to mark in the tabs. - /// Diagnostics are only shown when file icons are also active. - /// - /// Shows all diagnostics if not specified. - /// - /// Default: warning - pub diagnostics_max_severity: Option, - - /// Whether to show code action button at start of buffer line. - /// - /// Default: true - pub inline_code_actions: Option, - - /// Drag and drop related settings - pub drag_and_drop_selection: Option, - - /// How to render LSP `textDocument/documentColor` colors in the editor. - /// - /// Default: [`DocumentColorsRenderMode::Inlay`] - pub lsp_document_colors: Option, - /// When to show the scrollbar in the completion menu. - /// This setting can take four values: - /// - /// 1. Show the scrollbar if there's important information or - /// follow the system's configured behavior - /// "auto" - /// 2. Match the system's configured behavior: - /// "system" - /// 3. Always show the scrollbar: - /// "always" - /// 4. Never show the scrollbar: - /// "never" (default) - pub completion_menu_scrollbar: Option, -} - -#[derive( - Debug, - Clone, - Copy, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum RelativeLineNumbers { - Disabled, - Enabled, - Wrapped, -} - -impl RelativeLineNumbers { - pub fn enabled(&self) -> bool { - match self { - RelativeLineNumbers::Enabled | RelativeLineNumbers::Wrapped => true, - RelativeLineNumbers::Disabled => false, - } - } - pub fn wrapped(&self) -> bool { - match self { - RelativeLineNumbers::Enabled | RelativeLineNumbers::Disabled => false, - RelativeLineNumbers::Wrapped => true, - } - } -} - -// Toolbar related settings -#[with_fallible_options] -#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)] -pub struct ToolbarContent { - /// Whether to display breadcrumbs in the editor toolbar. - /// - /// Default: true - pub breadcrumbs: Option, - /// Whether to display quick action buttons in the editor toolbar. - /// - /// Default: true - pub quick_actions: Option, - /// Whether to show the selections menu in the editor toolbar. - /// - /// Default: true - pub selections_menu: Option, - /// Whether to display Agent review buttons in the editor toolbar. - /// Only applicable while reviewing a file edited by the Agent. - /// - /// Default: true - pub agent_review: Option, - /// Whether to display code action buttons in the editor toolbar. - /// - /// Default: false - pub code_actions: Option, -} - -/// Scrollbar related settings -#[with_fallible_options] -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Default)] -pub struct ScrollbarContent { - /// When to show the scrollbar in the editor. - /// - /// Default: auto - pub show: Option, - /// Whether to show git diff indicators in the scrollbar. - /// - /// Default: true - pub git_diff: Option, - /// Whether to show buffer search result indicators in the scrollbar. - /// - /// Default: true - pub search_results: Option, - /// Whether to show selected text occurrences in the scrollbar. - /// - /// Default: true - pub selected_text: Option, - /// Whether to show selected symbol occurrences in the scrollbar. - /// - /// Default: true - pub selected_symbol: Option, - /// Which diagnostic indicators to show in the scrollbar: - /// - /// Default: all - pub diagnostics: Option, - /// Whether to show cursor positions in the scrollbar. - /// - /// Default: true - pub cursors: Option, - /// Forcefully enable or disable the scrollbar for each axis - pub axes: Option, -} - -/// Sticky scroll related settings -#[with_fallible_options] -#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -pub struct StickyScrollContent { - /// Whether sticky scroll is enabled. - /// - /// Default: false - pub enabled: Option, -} - -/// Minimap related settings -#[with_fallible_options] -#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -pub struct MinimapContent { - /// When to show the minimap in the editor. - /// - /// Default: never - pub show: Option, - - /// Where to show the minimap in the editor. - /// - /// Default: [`DisplayIn::ActiveEditor`] - pub display_in: Option, - - /// When to show the minimap thumb. - /// - /// Default: always - pub thumb: Option, - - /// Defines the border style for the minimap's scrollbar thumb. - /// - /// Default: left_open - pub thumb_border: Option, - - /// How to highlight the current line in the minimap. - /// - /// Default: inherits editor line highlights setting - pub current_line_highlight: Option, - - /// Maximum number of columns to display in the minimap. - /// - /// Default: 80 - pub max_width_columns: Option, -} - -/// Forcefully enable or disable the scrollbar for each axis -#[with_fallible_options] -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Default)] -pub struct ScrollbarAxesContent { - /// When false, forcefully disables the horizontal scrollbar. Otherwise, obey other settings. - /// - /// Default: true - pub horizontal: Option, - - /// When false, forcefully disables the vertical scrollbar. Otherwise, obey other settings. - /// - /// Default: true - pub vertical: Option, -} - -/// Gutter related settings -#[with_fallible_options] -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)] -pub struct GutterContent { - /// Whether to show line numbers in the gutter. - /// - /// Default: true - pub line_numbers: Option, - /// Minimum number of characters to reserve space for in the gutter. - /// - /// Default: 4 - pub min_line_number_digits: Option, - /// Whether to show runnable buttons in the gutter. - /// - /// Default: true - pub runnables: Option, - /// Whether to show breakpoints in the gutter. - /// - /// Default: true - pub breakpoints: Option, - /// Whether to show fold buttons in the gutter. - /// - /// Default: true - pub folds: Option, -} - -/// How to render LSP `textDocument/documentColor` colors in the editor. -#[derive( - Copy, - Clone, - Debug, - Default, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum DocumentColorsRenderMode { - /// Do not query and render document colors. - None, - /// Render document colors as inlay hints near the color text. - #[default] - Inlay, - /// Draw a border around the color text. - Border, - /// Draw a background behind the color text. - Background, -} - -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum CurrentLineHighlight { - // Don't highlight the current line. - None, - // Highlight the gutter area. - Gutter, - // Highlight the editor area. - Line, - // Highlight the full line. - All, -} - -/// When to populate a new search's query based on the text under the cursor. -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum SeedQuerySetting { - /// Always populate the search query with the word under the cursor. - Always, - /// Only populate the search query when there is text selected. - Selection, - /// Never populate the search query - Never, -} - -/// What to do when multibuffer is double clicked in some of its excerpts (parts of singleton buffers). -#[derive( - Default, - Copy, - Clone, - Debug, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum DoubleClickInMultibuffer { - /// Behave as a regular buffer and select the whole word. - #[default] - Select, - /// Open the excerpt clicked as a new buffer in the new tab, if no `alt` modifier was pressed during double click. - /// Otherwise, behave as a regular buffer and select the whole word. - Open, -} - -/// When to show the minimap thumb. -/// -/// Default: always -#[derive( - Copy, - Clone, - Debug, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum MinimapThumb { - /// Show the minimap thumb only when the mouse is hovering over the minimap. - Hover, - /// Always show the minimap thumb. - #[default] - Always, -} - -/// Defines the border style for the minimap's scrollbar thumb. -/// -/// Default: left_open -#[derive( - Copy, - Clone, - Debug, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum MinimapThumbBorder { - /// Displays a border on all sides of the thumb. - Full, - /// Displays a border on all sides except the left side of the thumb. - #[default] - LeftOpen, - /// Displays a border on all sides except the right side of the thumb. - RightOpen, - /// Displays a border only on the left side of the thumb. - LeftOnly, - /// Displays the thumb without any border. - None, -} - -/// Which diagnostic indicators to show in the scrollbar. -/// -/// Default: all -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "lowercase")] -pub enum ScrollbarDiagnostics { - /// Show all diagnostic levels: hint, information, warnings, error. - All, - /// Show only the following diagnostic levels: information, warning, error. - Information, - /// Show only the following diagnostic levels: warning, error. - Warning, - /// Show only the following diagnostic level: error. - Error, - /// Do not show diagnostics. - None, -} - -/// The key to use for adding multiple cursors -/// -/// Default: alt -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum MultiCursorModifier { - Alt, - #[serde(alias = "cmd", alias = "ctrl")] - CmdOrCtrl, -} - -/// Whether the editor will scroll beyond the last line. -/// -/// Default: one_page -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum ScrollBeyondLastLine { - /// The editor will not scroll beyond the last line. - Off, - - /// The editor will scroll beyond the last line by one page. - OnePage, - - /// The editor will scroll beyond the last line by the same number of lines as vertical_scroll_margin. - VerticalScrollMargin, -} - -/// The shape of a selection cursor. -#[derive( - Copy, - Clone, - Debug, - Default, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum CursorShape { - /// A vertical bar - #[default] - Bar, - /// A block that surrounds the following character - Block, - /// An underline that runs along the following character - Underline, - /// A box drawn around the following character - Hollow, -} - -/// What to do when go to definition yields no results. -#[derive( - Copy, - Clone, - Debug, - Default, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum GoToDefinitionFallback { - /// Disables the fallback. - None, - /// Looks up references of the same symbol instead. - #[default] - FindAllReferences, -} - -/// Determines when the mouse cursor should be hidden in an editor or input box. -/// -/// Default: on_typing_and_movement -#[derive( - Copy, - Clone, - Debug, - Default, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum HideMouseMode { - /// Never hide the mouse cursor - Never, - /// Hide only when typing - OnTyping, - /// Hide on both typing and cursor movement - #[default] - OnTypingAndMovement, -} - -/// Determines how snippets are sorted relative to other completion items. -/// -/// Default: inline -#[derive( - Copy, - Clone, - Debug, - Default, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum SnippetSortOrder { - /// Place snippets at the top of the completion list - Top, - /// Sort snippets normally using the default comparison logic - #[default] - Inline, - /// Place snippets at the bottom of the completion list - Bottom, - /// Do not show snippets in the completion list - None, -} - -/// Default options for buffer and project search items. -#[with_fallible_options] -#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)] -pub struct SearchSettingsContent { - /// Whether to show the project search button in the status bar. - pub button: Option, - /// Whether to only match on whole words. - pub whole_word: Option, - /// Whether to match case sensitively. - pub case_sensitive: Option, - /// Whether to include gitignored files in search results. - pub include_ignored: Option, - /// Whether to interpret the search query as a regular expression. - pub regex: Option, - /// Whether to center the cursor on each search match when navigating. - pub center_on_match: Option, -} - -#[with_fallible_options] -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -pub struct JupyterContent { - /// Whether the Jupyter feature is enabled. - /// - /// Default: true - pub enabled: Option, - - /// Default kernels to select for each language. - /// - /// Default: `{}` - pub kernel_selections: Option>, -} - -/// Whether to allow drag and drop text selection in buffer. -#[with_fallible_options] -#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)] -pub struct DragAndDropSelectionContent { - /// When true, enables drag and drop text selection in buffer. - /// - /// Default: true - pub enabled: Option, - - /// The delay in milliseconds that must elapse before drag and drop is allowed. Otherwise, a new text selection is created. - /// - /// Default: 300 - pub delay: Option, -} - -/// When to show the minimap in the editor. -/// -/// Default: never -#[derive( - Copy, - Clone, - Debug, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum ShowMinimap { - /// Follow the visibility of the scrollbar. - Auto, - /// Always show the minimap. - Always, - /// Never show the minimap. - #[default] - Never, -} - -/// Where to show the minimap in the editor. -/// -/// Default: all_editors -#[derive( - Copy, - Clone, - Debug, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum DisplayIn { - /// Show on all open editors. - AllEditors, - /// Show the minimap on the active editor only. - #[default] - ActiveEditor, -} - -/// Minimum APCA perceptual contrast for text over highlight backgrounds. -/// -/// Valid range: 0.0 to 106.0 -/// Default: 45.0 -#[derive( - Clone, - Copy, - Debug, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - PartialOrd, - derive_more::FromStr, -)] -#[serde(transparent)] -pub struct MinimumContrast( - #[serde(serialize_with = "crate::serialize_f32_with_two_decimal_places")] pub f32, -); - -impl Display for MinimumContrast { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:.1}", self.0) - } -} - -impl From for MinimumContrast { - fn from(x: f32) -> Self { - Self(x) - } -} - -/// Opacity of the inactive panes. 0 means transparent, 1 means opaque. -/// -/// Valid range: 0.0 to 1.0 -/// Default: 1.0 -#[derive( - Clone, - Copy, - Debug, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - PartialOrd, - derive_more::FromStr, -)] -#[serde(transparent)] -pub struct InactiveOpacity( - #[serde(serialize_with = "serialize_f32_with_two_decimal_places")] pub f32, -); - -impl Display for InactiveOpacity { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:.1}", self.0) - } -} - -impl From for InactiveOpacity { - fn from(x: f32) -> Self { - Self(x) - } -} - -/// Centered layout related setting (left/right). -/// -/// Valid range: 0.0 to 0.4 -/// Default: 2.0 -#[derive( - Clone, - Copy, - Debug, - Serialize, - Deserialize, - MergeFrom, - PartialEq, - PartialOrd, - derive_more::FromStr, -)] -#[serde(transparent)] -pub struct CenteredPaddingSettings( - #[serde(serialize_with = "serialize_f32_with_two_decimal_places")] pub f32, -); - -impl CenteredPaddingSettings { - pub const MIN_PADDING: f32 = 0.0; - // This is an f64 so serde_json can give a type hint without random numbers in the back - pub const DEFAULT_PADDING: f64 = 0.2; - pub const MAX_PADDING: f32 = 0.4; -} - -impl Display for CenteredPaddingSettings { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:.2}", self.0) - } -} - -impl From for CenteredPaddingSettings { - fn from(x: f32) -> Self { - Self(x) - } -} - -impl Default for CenteredPaddingSettings { - fn default() -> Self { - Self(Self::DEFAULT_PADDING as f32) - } -} - -impl schemars::JsonSchema for CenteredPaddingSettings { - fn schema_name() -> std::borrow::Cow<'static, str> { - "CenteredPaddingSettings".into() - } - - fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema { - use schemars::json_schema; - json_schema!({ - "type": "number", - "minimum": Self::MIN_PADDING, - "maximum": Self::MAX_PADDING, - "default": Self::DEFAULT_PADDING, - "description": "Centered layout related setting (left/right)." - }) - } -} diff --git a/crates/settings/src/settings_content/extension.rs b/crates/settings/src/settings_content/extension.rs deleted file mode 100644 index 2fefd4ef38..0000000000 --- a/crates/settings/src/settings_content/extension.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::sync::Arc; - -use collections::HashMap; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings_macros::{MergeFrom, with_fallible_options}; - -#[with_fallible_options] -#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct ExtensionSettingsContent { - /// The extensions that should be automatically installed by Zed. - /// - /// This is used to make functionality provided by extensions (e.g., language support) - /// available out-of-the-box. - /// - /// Default: { "html": true } - #[serde(default)] - pub auto_install_extensions: HashMap, bool>, - #[serde(default)] - pub auto_update_extensions: HashMap, bool>, - /// The capabilities granted to extensions. - pub granted_extension_capabilities: Option>, -} - -/// A capability for an extension. -#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, JsonSchema)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum ExtensionCapabilityContent { - #[serde(rename = "process:exec")] - ProcessExec { - /// The command to execute. - command: String, - /// The arguments to pass to the command. Use `*` for a single wildcard argument. - /// If the last element is `**`, then any trailing arguments are allowed. - args: Vec, - }, - DownloadFile { - host: String, - path: Vec, - }, - #[serde(rename = "npm:install")] - NpmInstallPackage { - package: String, - }, -} diff --git a/crates/settings/src/settings_content/language.rs b/crates/settings/src/settings_content/language.rs deleted file mode 100644 index 25ff60e9f4..0000000000 --- a/crates/settings/src/settings_content/language.rs +++ /dev/null @@ -1,1023 +0,0 @@ -use std::num::NonZeroU32; - -use collections::{HashMap, HashSet}; -use gpui::{Modifiers, SharedString}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize, de::Error as _}; -use settings_macros::{MergeFrom, with_fallible_options}; -use std::sync::Arc; - -use crate::{ExtendingVec, merge_from}; - -#[with_fallible_options] -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)] -pub struct AllLanguageSettingsContent { - /// The settings for enabling/disabling features. - pub features: Option, - /// The edit prediction settings. - pub edit_predictions: Option, - /// The default language settings. - #[serde(flatten)] - pub defaults: LanguageSettingsContent, - /// The settings for individual languages. - #[serde(default)] - pub languages: LanguageToSettingsMap, - /// Settings for associating file extensions and filenames - /// with languages. - pub file_types: Option, ExtendingVec>>, -} - -impl merge_from::MergeFrom for AllLanguageSettingsContent { - fn merge_from(&mut self, other: &Self) { - self.file_types.merge_from(&other.file_types); - self.features.merge_from(&other.features); - self.edit_predictions.merge_from(&other.edit_predictions); - - // A user's global settings override the default global settings and - // all default language-specific settings. - // - self.defaults.merge_from(&other.defaults); - for language_settings in self.languages.0.values_mut() { - language_settings.merge_from(&other.defaults); - } - - // A user's language-specific settings override default language-specific settings. - for (language_name, user_language_settings) in &other.languages.0 { - if let Some(existing) = self.languages.0.get_mut(language_name) { - existing.merge_from(&user_language_settings); - } else { - let mut new_settings = self.defaults.clone(); - new_settings.merge_from(&user_language_settings); - - self.languages.0.insert(language_name.clone(), new_settings); - } - } - } -} - -/// The settings for enabling/disabling features. -#[with_fallible_options] -#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -pub struct FeaturesContent { - /// Determines which edit prediction provider to use. - pub edit_prediction_provider: Option, - /// Enables the experimental edit prediction context retrieval system. - pub experimental_edit_prediction_context_retrieval: Option, -} - -/// The provider that supplies edit predictions. -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -pub enum EditPredictionProvider { - None, - #[default] - Copilot, - Supermaven, - Zed, - Codestral, - Experimental(&'static str), -} - -pub const EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME: &str = "sweep"; -pub const EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME: &str = "zeta2"; -pub const EXPERIMENTAL_MERCURY_EDIT_PREDICTION_PROVIDER_NAME: &str = "mercury"; - -impl<'de> Deserialize<'de> for EditPredictionProvider { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - #[serde(rename_all = "snake_case")] - pub enum Content { - None, - Copilot, - Supermaven, - Zed, - Codestral, - Experimental(String), - } - - Ok(match Content::deserialize(deserializer)? { - Content::None => EditPredictionProvider::None, - Content::Copilot => EditPredictionProvider::Copilot, - Content::Supermaven => EditPredictionProvider::Supermaven, - Content::Zed => EditPredictionProvider::Zed, - Content::Codestral => EditPredictionProvider::Codestral, - Content::Experimental(name) - if name == EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME => - { - EditPredictionProvider::Experimental( - EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME, - ) - } - Content::Experimental(name) - if name == EXPERIMENTAL_MERCURY_EDIT_PREDICTION_PROVIDER_NAME => - { - EditPredictionProvider::Experimental( - EXPERIMENTAL_MERCURY_EDIT_PREDICTION_PROVIDER_NAME, - ) - } - Content::Experimental(name) - if name == EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME => - { - EditPredictionProvider::Experimental( - EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME, - ) - } - Content::Experimental(name) => { - return Err(D::Error::custom(format!( - "Unknown experimental edit prediction provider: {}", - name - ))); - } - }) - } -} - -impl EditPredictionProvider { - pub fn is_zed(&self) -> bool { - match self { - EditPredictionProvider::Zed => true, - EditPredictionProvider::None - | EditPredictionProvider::Copilot - | EditPredictionProvider::Supermaven - | EditPredictionProvider::Codestral - | EditPredictionProvider::Experimental(_) => false, - } - } -} - -/// The contents of the edit prediction settings. -#[with_fallible_options] -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -pub struct EditPredictionSettingsContent { - /// A list of globs representing files that edit predictions should be disabled for. - /// This list adds to a pre-existing, sensible default set of globs. - /// Any additional ones you add are combined with them. - pub disabled_globs: Option>, - /// The mode used to display edit predictions in the buffer. - /// Provider support required. - pub mode: Option, - /// Settings specific to GitHub Copilot. - pub copilot: Option, - /// Settings specific to Codestral. - pub codestral: Option, - /// Whether edit predictions are enabled in the assistant prompt editor. - /// This has no effect if globally disabled. - pub enabled_in_text_threads: Option, -} - -#[with_fallible_options] -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -pub struct CopilotSettingsContent { - /// HTTP/HTTPS proxy to use for Copilot. - /// - /// Default: none - pub proxy: Option, - /// Disable certificate verification for the proxy (not recommended). - /// - /// Default: false - pub proxy_no_verify: Option, - /// Enterprise URI for Copilot. - /// - /// Default: none - pub enterprise_uri: Option, -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -pub struct CodestralSettingsContent { - /// Model to use for completions. - /// - /// Default: "codestral-latest" - #[serde(default)] - pub model: Option, - /// Maximum tokens to generate. - /// - /// Default: 150 - #[serde(default)] - pub max_tokens: Option, - /// Api URL to use for completions. - /// - /// Default: "https://codestral.mistral.ai" - #[serde(default)] - pub api_url: Option, -} - -/// The mode in which edit predictions should be displayed. -#[derive( - Copy, - Clone, - Debug, - Default, - Eq, - PartialEq, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum EditPredictionsMode { - /// If provider supports it, display inline when holding modifier key (e.g., alt). - /// Otherwise, eager preview is used. - #[serde(alias = "auto")] - Subtle, - /// Display inline when there are no language server completions available. - #[default] - #[serde(alias = "eager_preview")] - Eager, -} - -/// Controls the soft-wrapping behavior in the editor. -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum SoftWrap { - /// Prefer a single line generally, unless an overly long line is encountered. - None, - /// Deprecated: use None instead. Left to avoid breaking existing users' configs. - /// Prefer a single line generally, unless an overly long line is encountered. - PreferLine, - /// Soft wrap lines that exceed the editor width. - EditorWidth, - /// Soft wrap lines at the preferred line length. - PreferredLineLength, - /// Soft wrap line at the preferred line length or the editor width (whichever is smaller). - Bounded, -} - -/// The settings for a particular language. -#[with_fallible_options] -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct LanguageSettingsContent { - /// How many columns a tab should occupy. - /// - /// Default: 4 - #[schemars(range(min = 1, max = 128))] - pub tab_size: Option, - /// Whether to indent lines using tab characters, as opposed to multiple - /// spaces. - /// - /// Default: false - pub hard_tabs: Option, - /// How to soft-wrap long lines of text. - /// - /// Default: none - pub soft_wrap: Option, - /// The column at which to soft-wrap lines, for buffers where soft-wrap - /// is enabled. - /// - /// Default: 80 - pub preferred_line_length: Option, - /// Whether to show wrap guides in the editor. Setting this to true will - /// show a guide at the 'preferred_line_length' value if softwrap is set to - /// 'preferred_line_length', and will show any additional guides as specified - /// by the 'wrap_guides' setting. - /// - /// Default: true - pub show_wrap_guides: Option, - /// Character counts at which to show wrap guides in the editor. - /// - /// Default: [] - pub wrap_guides: Option>, - /// Indent guide related settings. - pub indent_guides: Option, - /// Whether or not to perform a buffer format before saving. - /// - /// Default: on - pub format_on_save: Option, - /// Whether or not to remove any trailing whitespace from lines of a buffer - /// before saving it. - /// - /// Default: true - pub remove_trailing_whitespace_on_save: Option, - /// Whether or not to ensure there's a single newline at the end of a buffer - /// when saving it. - /// - /// Default: true - pub ensure_final_newline_on_save: Option, - /// How to perform a buffer format. - /// - /// Default: auto - pub formatter: Option, - /// Zed's Prettier integration settings. - /// Allows to enable/disable formatting with Prettier - /// and configure default Prettier, used when no project-level Prettier installation is found. - /// - /// Default: off - pub prettier: Option, - /// Whether to automatically close JSX tags. - pub jsx_tag_auto_close: Option, - /// Whether to use language servers to provide code intelligence. - /// - /// Default: true - pub enable_language_server: Option, - /// The list of language servers to use (or disable) for this language. - /// - /// This array should consist of language server IDs, as well as the following - /// special tokens: - /// - `"!"` - A language server ID prefixed with a `!` will be disabled. - /// - `"..."` - A placeholder to refer to the **rest** of the registered language servers for this language. - /// - /// Default: ["..."] - pub language_servers: Option>, - /// Controls where the `editor::Rewrap` action is allowed for this language. - /// - /// Note: This setting has no effect in Vim mode, as rewrap is already - /// allowed everywhere. - /// - /// Default: "in_comments" - pub allow_rewrap: Option, - /// Controls whether edit predictions are shown immediately (true) - /// or manually by triggering `editor::ShowEditPrediction` (false). - /// - /// Default: true - pub show_edit_predictions: Option, - /// Controls whether edit predictions are shown in the given language - /// scopes. - /// - /// Example: ["string", "comment"] - /// - /// Default: [] - pub edit_predictions_disabled_in: Option>, - /// Whether to show tabs and spaces in the editor. - pub show_whitespaces: Option, - /// Visible characters used to render whitespace when show_whitespaces is enabled. - /// - /// Default: "•" for spaces, "→" for tabs. - pub whitespace_map: Option, - /// Whether to start a new line with a comment when a previous line is a comment as well. - /// - /// Default: true - pub extend_comment_on_newline: Option, - /// Inlay hint related settings. - pub inlay_hints: Option, - /// Whether to automatically type closing characters for you. For example, - /// when you type '(', Zed will automatically add a closing ')' at the correct position. - /// - /// Default: true - pub use_autoclose: Option, - /// Whether to automatically surround text with characters for you. For example, - /// when you select text and type '(', Zed will automatically surround text with (). - /// - /// Default: true - pub use_auto_surround: Option, - /// Controls how the editor handles the autoclosed characters. - /// When set to `false`(default), skipping over and auto-removing of the closing characters - /// happen only for auto-inserted characters. - /// Otherwise(when `true`), the closing characters are always skipped over and auto-removed - /// no matter how they were inserted. - /// - /// Default: false - pub always_treat_brackets_as_autoclosed: Option, - /// Whether to use additional LSP queries to format (and amend) the code after - /// every "trigger" symbol input, defined by LSP server capabilities. - /// - /// Default: true - pub use_on_type_format: Option, - /// Which code actions to run on save before the formatter. - /// These are not run if formatting is off. - /// - /// Default: {} (or {"source.organizeImports": true} for Go). - pub code_actions_on_format: Option>, - /// Whether to perform linked edits of associated ranges, if the language server supports it. - /// For example, when editing opening tag, the contents of the closing tag will be edited as well. - /// - /// Default: true - pub linked_edits: Option, - /// Whether indentation should be adjusted based on the context whilst typing. - /// - /// Default: true - pub auto_indent: Option, - /// Whether indentation of pasted content should be adjusted based on the context. - /// - /// Default: true - pub auto_indent_on_paste: Option, - /// Task configuration for this language. - /// - /// Default: {} - pub tasks: Option, - /// Whether to pop the completions menu while typing in an editor without - /// explicitly requesting it. - /// - /// Default: true - pub show_completions_on_input: Option, - /// Whether to display inline and alongside documentation for items in the - /// completions menu. - /// - /// Default: true - pub show_completion_documentation: Option, - /// Controls how completions are processed for this language. - pub completions: Option, - /// Preferred debuggers for this language. - /// - /// Default: [] - pub debuggers: Option>, - /// Whether to enable word diff highlighting in the editor. - /// - /// When enabled, changed words within modified lines are highlighted - /// to show exactly what changed. - /// - /// Default: true - pub word_diff_enabled: Option, - /// Whether to use tree-sitter bracket queries to detect and colorize the brackets in the editor. - /// - /// Default: false - pub colorize_brackets: Option, -} - -/// Controls how whitespace should be displayedin the editor. -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum ShowWhitespaceSetting { - /// Draw whitespace only for the selected text. - Selection, - /// Do not draw any tabs or spaces. - None, - /// Draw all invisible symbols. - All, - /// Draw whitespaces at boundaries only. - /// - /// For a whitespace to be on a boundary, any of the following conditions need to be met: - /// - It is a tab - /// - It is adjacent to an edge (start or end) - /// - It is adjacent to a whitespace (left or right) - Boundary, - /// Draw whitespaces only after non-whitespace characters. - Trailing, -} - -#[with_fallible_options] -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -pub struct WhitespaceMapContent { - pub space: Option, - pub tab: Option, -} - -/// The behavior of `editor::Rewrap`. -#[derive( - Debug, - PartialEq, - Clone, - Copy, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum RewrapBehavior { - /// Only rewrap within comments. - #[default] - InComments, - /// Only rewrap within the current selection(s). - InSelections, - /// Allow rewrapping anywhere. - Anywhere, -} - -#[with_fallible_options] -#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct JsxTagAutoCloseSettingsContent { - /// Enables or disables auto-closing of JSX tags. - pub enabled: Option, -} - -/// The settings for inlay hints. -#[with_fallible_options] -#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)] -pub struct InlayHintSettingsContent { - /// Global switch to toggle hints on and off. - /// - /// Default: false - pub enabled: Option, - /// Global switch to toggle inline values on and off when debugging. - /// - /// Default: true - pub show_value_hints: Option, - /// Whether type hints should be shown. - /// - /// Default: true - pub show_type_hints: Option, - /// Whether parameter hints should be shown. - /// - /// Default: true - pub show_parameter_hints: Option, - /// Whether other hints should be shown. - /// - /// Default: true - pub show_other_hints: Option, - /// Whether to show a background for inlay hints. - /// - /// If set to `true`, the background will use the `hint.background` color - /// from the current theme. - /// - /// Default: false - pub show_background: Option, - /// Whether or not to debounce inlay hints updates after buffer edits. - /// - /// Set to 0 to disable debouncing. - /// - /// Default: 700 - pub edit_debounce_ms: Option, - /// Whether or not to debounce inlay hints updates after buffer scrolls. - /// - /// Set to 0 to disable debouncing. - /// - /// Default: 50 - pub scroll_debounce_ms: Option, - /// Toggles inlay hints (hides or shows) when the user presses the modifiers specified. - /// If only a subset of the modifiers specified is pressed, hints are not toggled. - /// If no modifiers are specified, this is equivalent to `null`. - /// - /// Default: null - pub toggle_on_modifiers_press: Option, -} - -/// The kind of an inlay hint. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum InlayHintKind { - /// An inlay hint for a type. - Type, - /// An inlay hint for a parameter. - Parameter, -} - -impl InlayHintKind { - /// Returns the [`InlayHintKind`]fromthe given name. - /// - /// Returns `None` if `name` does not match any of the expected - /// string representations. - pub fn from_name(name: &str) -> Option { - match name { - "type" => Some(InlayHintKind::Type), - "parameter" => Some(InlayHintKind::Parameter), - _ => None, - } - } - - /// Returns the name of this [`InlayHintKind`]. - pub fn name(&self) -> &'static str { - match self { - InlayHintKind::Type => "type", - InlayHintKind::Parameter => "parameter", - } - } -} - -/// Controls how completions are processed for this language. -#[with_fallible_options] -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom, Default)] -#[serde(rename_all = "snake_case")] -pub struct CompletionSettingsContent { - /// Controls how words are completed. - /// For large documents, not all words may be fetched for completion. - /// - /// Default: `fallback` - pub words: Option, - /// 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 - pub words_min_length: Option, - /// Whether to fetch LSP completions or not. - /// - /// Default: true - pub lsp: Option, - /// When fetching LSP completions, determines how long to wait for a response of a particular server. - /// When set to 0, waits indefinitely. - /// - /// Default: 0 - pub lsp_fetch_timeout_ms: Option, - /// Controls how LSP completions are inserted. - /// - /// Default: "replace_suffix" - pub lsp_insert_mode: Option, -} - -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum LspInsertMode { - /// Replaces text before the cursor, using the `insert` range described in the LSP specification. - Insert, - /// Replaces text before and after the cursor, using the `replace` range described in the LSP specification. - Replace, - /// Behaves like `"replace"` if the text that would be replaced is a subsequence of the completion text, - /// and like `"insert"` otherwise. - ReplaceSubsequence, - /// Behaves like `"replace"` if the text after the cursor is a suffix of the completion, and like - /// `"insert"` otherwise. - ReplaceSuffix, -} - -/// Controls how document's words are completed. -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum WordsCompletionMode { - /// Always fetch document's words for completions along with LSP completions. - Enabled, - /// Only if LSP response errors or times out, - /// use document's words to show completions. - Fallback, - /// Never fetch or complete document's words for completions. - /// (Word-based completions can still be queried via a separate action) - Disabled, -} - -/// Allows to enable/disable formatting with Prettier -/// and configure default Prettier, used when no project-level Prettier installation is found. -/// Prettier formatting is disabled by default. -#[with_fallible_options] -#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct PrettierSettingsContent { - /// Enables or disables formatting with Prettier for a given language. - pub allowed: Option, - - /// Forces Prettier integration to use a specific parser name when formatting files with the language. - pub parser: Option, - - /// Forces Prettier integration to use specific plugins when formatting files with the language. - /// The default Prettier will be installed with these plugins. - pub plugins: Option>, - - /// Default Prettier options, in the format as in package.json section for Prettier. - /// If project installs Prettier via its package.json, these options will be ignored. - #[serde(flatten)] - pub options: Option>, -} - -/// TODO: this should just be a bool -/// Controls the behavior of formatting files when they are saved. -#[derive( - Debug, - Clone, - Copy, - PartialEq, - Eq, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "lowercase")] -pub enum FormatOnSave { - /// Files should be formatted on save. - On, - /// Files should not be formatted on save. - Off, -} - -/// Controls which formatters should be used when formatting code. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)] -#[serde(untagged)] -pub enum FormatterList { - Single(Formatter), - Vec(Vec), -} - -impl Default for FormatterList { - fn default() -> Self { - Self::Single(Formatter::default()) - } -} - -impl AsRef<[Formatter]> for FormatterList { - fn as_ref(&self) -> &[Formatter] { - match &self { - Self::Single(single) => std::slice::from_ref(single), - Self::Vec(v) => v, - } - } -} - -/// Controls which formatter should be used when formatting code. If there are multiple formatters, they are executed in the order of declaration. -#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -pub enum Formatter { - /// Format files using Zed's Prettier integration (if applicable), - /// or falling back to formatting via language server. - #[default] - Auto, - /// Format code using Zed's Prettier integration. - Prettier, - /// Format code using an external command. - External { - /// The external program to run. - command: Arc, - /// The arguments to pass to the program. - arguments: Option>, - }, - /// Files should be formatted using a code action executed by language servers. - CodeAction(String), - /// Format code using a language server. - #[serde(untagged)] - LanguageServer(LanguageServerFormatterSpecifier), -} - -#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)] -#[serde( - rename_all = "snake_case", - // allow specifying language servers as "language_server" or {"language_server": {"name": ...}} - from = "LanguageServerVariantContent", - into = "LanguageServerVariantContent" -)] -pub enum LanguageServerFormatterSpecifier { - Specific { - name: String, - }, - #[default] - Current, -} - -impl From for LanguageServerFormatterSpecifier { - fn from(value: LanguageServerVariantContent) -> Self { - match value { - LanguageServerVariantContent::Specific { - language_server: LanguageServerSpecifierContent { name: Some(name) }, - } => Self::Specific { name }, - _ => Self::Current, - } - } -} - -impl From for LanguageServerVariantContent { - fn from(value: LanguageServerFormatterSpecifier) -> Self { - match value { - LanguageServerFormatterSpecifier::Specific { name } => Self::Specific { - language_server: LanguageServerSpecifierContent { name: Some(name) }, - }, - LanguageServerFormatterSpecifier::Current => { - Self::Current(CurrentLanguageServerContent::LanguageServer) - } - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case", untagged)] -enum LanguageServerVariantContent { - /// Format code using a specific language server. - Specific { - language_server: LanguageServerSpecifierContent, - }, - /// Format code using the current language server. - Current(CurrentLanguageServerContent), -} - -#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -enum CurrentLanguageServerContent { - #[default] - LanguageServer, -} - -#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -struct LanguageServerSpecifierContent { - /// The name of the language server to format with - name: Option, -} - -/// The settings for indent guides. -#[with_fallible_options] -#[derive(Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct IndentGuideSettingsContent { - /// Whether to display indent guides in the editor. - /// - /// Default: true - pub enabled: Option, - /// The width of the indent guides in pixels, between 1 and 10. - /// - /// Default: 1 - pub line_width: Option, - /// The width of the active indent guide in pixels, between 1 and 10. - /// - /// Default: 1 - pub active_line_width: Option, - /// Determines how indent guides are colored. - /// - /// Default: Fixed - pub coloring: Option, - /// Determines how indent guide backgrounds are colored. - /// - /// Default: Disabled - pub background_coloring: Option, -} - -/// The task settings for a particular language. -#[with_fallible_options] -#[derive(Debug, Clone, Default, Deserialize, PartialEq, Serialize, JsonSchema, MergeFrom)] -pub struct LanguageTaskSettingsContent { - /// Extra task variables to set for a particular language. - pub variables: Option>, - pub enabled: Option, - /// Use LSP tasks over Zed language extension ones. - /// If no LSP tasks are returned due to error/timeout or regular execution, - /// Zed language extension tasks will be used instead. - /// - /// Other Zed tasks will still be shown: - /// * Zed task from either of the task config file - /// * Zed task from history (e.g. one-off task was spawned before) - pub prefer_lsp: Option, -} - -/// Map from language name to settings. -#[with_fallible_options] -#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct LanguageToSettingsMap(pub HashMap); - -/// Determines how indent guides are colored. -#[derive( - Default, - Debug, - Copy, - Clone, - PartialEq, - Eq, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum IndentGuideColoring { - /// Do not render any lines for indent guides. - Disabled, - /// Use the same color for all indentation levels. - #[default] - Fixed, - /// Use a different color for each indentation level. - IndentAware, -} - -/// Determines how indent guide backgrounds are colored. -#[derive( - Default, - Debug, - Copy, - Clone, - PartialEq, - Eq, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum IndentGuideBackgroundColoring { - /// Do not render any background for indent guides. - #[default] - Disabled, - /// Use a different color for each indentation level. - IndentAware, -} - -#[cfg(test)] -mod test { - - use crate::{ParseStatus, fallible_options}; - - use super::*; - - #[test] - fn test_formatter_deserialization() { - let raw_auto = "{\"formatter\": \"auto\"}"; - let settings: LanguageSettingsContent = serde_json::from_str(raw_auto).unwrap(); - assert_eq!( - settings.formatter, - Some(FormatterList::Single(Formatter::Auto)) - ); - let raw = "{\"formatter\": \"language_server\"}"; - let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap(); - assert_eq!( - settings.formatter, - Some(FormatterList::Single(Formatter::LanguageServer( - LanguageServerFormatterSpecifier::Current - ))) - ); - - let raw = "{\"formatter\": [{\"language_server\": {\"name\": null}}]}"; - let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap(); - assert_eq!( - settings.formatter, - Some(FormatterList::Vec(vec![Formatter::LanguageServer( - LanguageServerFormatterSpecifier::Current - )])) - ); - let raw = "{\"formatter\": [{\"language_server\": {\"name\": null}}, \"language_server\", \"prettier\"]}"; - let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap(); - assert_eq!( - settings.formatter, - Some(FormatterList::Vec(vec![ - Formatter::LanguageServer(LanguageServerFormatterSpecifier::Current), - Formatter::LanguageServer(LanguageServerFormatterSpecifier::Current), - Formatter::Prettier - ])) - ); - - let raw = "{\"formatter\": [{\"language_server\": {\"name\": \"ruff\"}}, \"prettier\"]}"; - let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap(); - assert_eq!( - settings.formatter, - Some(FormatterList::Vec(vec![ - Formatter::LanguageServer(LanguageServerFormatterSpecifier::Specific { - name: "ruff".to_string() - }), - Formatter::Prettier - ])) - ); - - assert_eq!( - serde_json::to_string(&LanguageServerFormatterSpecifier::Current).unwrap(), - "\"language_server\"", - ); - } - - #[test] - fn test_formatter_deserialization_invalid() { - let raw_auto = "{\"formatter\": {}}"; - let (_, result) = fallible_options::parse_json::(raw_auto); - assert!(matches!(result, ParseStatus::Failed { .. })); - } - - #[test] - fn test_prettier_options() { - let raw_prettier = r#"{"allowed": false, "tabWidth": 4, "semi": false}"#; - let result = serde_json::from_str::(raw_prettier) - .expect("Failed to parse prettier options"); - assert!( - result - .options - .as_ref() - .expect("options were flattened") - .contains_key("semi") - ); - assert!( - result - .options - .as_ref() - .expect("options were flattened") - .contains_key("tabWidth") - ); - } -} diff --git a/crates/settings/src/settings_content/language_model.rs b/crates/settings/src/settings_content/language_model.rs deleted file mode 100644 index 48f5a463a4..0000000000 --- a/crates/settings/src/settings_content/language_model.rs +++ /dev/null @@ -1,423 +0,0 @@ -use collections::HashMap; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings_macros::{MergeFrom, with_fallible_options}; - -use std::sync::Arc; - -#[with_fallible_options] -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -pub struct AllLanguageModelSettingsContent { - pub anthropic: Option, - pub bedrock: Option, - pub deepseek: Option, - pub google: Option, - pub lmstudio: Option, - pub mistral: Option, - pub ollama: Option, - pub open_router: Option, - pub openai: Option, - pub openai_compatible: Option, OpenAiCompatibleSettingsContent>>, - pub vercel: Option, - pub x_ai: Option, - #[serde(rename = "zed.dev")] - pub zed_dot_dev: Option, -} - -#[with_fallible_options] -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -pub struct AnthropicSettingsContent { - pub api_url: Option, - pub available_models: Option>, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct AnthropicAvailableModel { - /// The model's name in the Anthropic API. e.g. claude-3-5-sonnet-latest, claude-3-opus-20240229, etc - pub name: String, - /// The model's name in Zed's UI, such as in the model selector dropdown menu in the assistant panel. - pub display_name: Option, - /// The model's context window size. - pub max_tokens: u64, - /// A model `name` to substitute when calling tools, in case the primary model doesn't support tool calling. - pub tool_override: Option, - /// Configuration of Anthropic's caching API. - pub cache_configuration: Option, - pub max_output_tokens: Option, - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub default_temperature: Option, - #[serde(default)] - pub extra_beta_headers: Vec, - /// The model's mode (e.g. thinking) - pub mode: Option, -} - -#[with_fallible_options] -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -pub struct AmazonBedrockSettingsContent { - pub available_models: Option>, - pub endpoint_url: Option, - pub region: Option, - pub profile: Option, - pub authentication_method: Option, - pub allow_global: Option, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct BedrockAvailableModel { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub cache_configuration: Option, - pub max_output_tokens: Option, - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub default_temperature: Option, - pub mode: Option, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub enum BedrockAuthMethodContent { - #[serde(rename = "named_profile")] - NamedProfile, - #[serde(rename = "sso")] - SingleSignOn, - /// IMDSv2, PodIdentity, env vars, etc. - #[serde(rename = "default")] - Automatic, -} - -#[with_fallible_options] -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -pub struct OllamaSettingsContent { - pub api_url: Option, - pub available_models: Option>, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct OllamaAvailableModel { - /// The model name in the Ollama API (e.g. "llama3.2:latest") - pub name: String, - /// The model's name in Zed's UI, such as in the model selector dropdown menu in the assistant panel. - pub display_name: Option, - /// The Context Length parameter to the model (aka num_ctx or n_ctx) - pub max_tokens: u64, - /// The number of seconds to keep the connection open after the last request - pub keep_alive: Option, - /// Whether the model supports tools - pub supports_tools: Option, - /// Whether the model supports vision - pub supports_images: Option, - /// Whether to enable think mode - pub supports_thinking: Option, -} - -#[derive(Clone, Serialize, Deserialize, Debug, Eq, PartialEq, JsonSchema, MergeFrom)] -#[serde(untagged)] -pub enum KeepAlive { - /// Keep model alive for N seconds - Seconds(isize), - /// Keep model alive for a fixed duration. Accepts durations like "5m", "10m", "1h", "1d", etc. - Duration(String), -} - -impl KeepAlive { - /// Keep model alive until a new model is loaded or until Ollama shuts down - pub fn indefinite() -> Self { - Self::Seconds(-1) - } -} - -impl Default for KeepAlive { - fn default() -> Self { - Self::indefinite() - } -} - -#[with_fallible_options] -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -pub struct LmStudioSettingsContent { - pub api_url: Option, - pub available_models: Option>, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct LmStudioAvailableModel { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub supports_tool_calls: bool, - pub supports_images: bool, -} - -#[with_fallible_options] -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -pub struct DeepseekSettingsContent { - pub api_url: Option, - pub available_models: Option>, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct DeepseekAvailableModel { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub max_output_tokens: Option, -} - -#[with_fallible_options] -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -pub struct MistralSettingsContent { - pub api_url: Option, - pub available_models: Option>, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct MistralAvailableModel { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub max_output_tokens: Option, - pub max_completion_tokens: Option, - pub supports_tools: Option, - pub supports_images: Option, - pub supports_thinking: Option, -} - -#[with_fallible_options] -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -pub struct OpenAiSettingsContent { - pub api_url: Option, - pub available_models: Option>, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct OpenAiAvailableModel { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub max_output_tokens: Option, - pub max_completion_tokens: Option, - pub reasoning_effort: Option, -} - -#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, JsonSchema, MergeFrom)] -#[serde(rename_all = "lowercase")] -pub enum OpenAiReasoningEffort { - Minimal, - Low, - Medium, - High, -} - -#[with_fallible_options] -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -pub struct OpenAiCompatibleSettingsContent { - pub api_url: String, - pub available_models: Vec, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct OpenAiCompatibleAvailableModel { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub max_output_tokens: Option, - pub max_completion_tokens: Option, - #[serde(default)] - pub capabilities: OpenAiCompatibleModelCapabilities, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct OpenAiCompatibleModelCapabilities { - pub tools: bool, - pub images: bool, - pub parallel_tool_calls: bool, - pub prompt_cache_key: bool, -} - -impl Default for OpenAiCompatibleModelCapabilities { - fn default() -> Self { - Self { - tools: true, - images: false, - parallel_tool_calls: false, - prompt_cache_key: false, - } - } -} - -#[with_fallible_options] -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -pub struct VercelSettingsContent { - pub api_url: Option, - pub available_models: Option>, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct VercelAvailableModel { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub max_output_tokens: Option, - pub max_completion_tokens: Option, -} - -#[with_fallible_options] -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -pub struct GoogleSettingsContent { - pub api_url: Option, - pub available_models: Option>, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct GoogleAvailableModel { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub mode: Option, -} - -#[with_fallible_options] -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -pub struct XAiSettingsContent { - pub api_url: Option, - pub available_models: Option>, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct XaiAvailableModel { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub max_output_tokens: Option, - pub max_completion_tokens: Option, - pub supports_images: Option, - pub supports_tools: Option, - pub parallel_tool_calls: Option, -} - -#[with_fallible_options] -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -pub struct ZedDotDevSettingsContent { - pub available_models: Option>, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct ZedDotDevAvailableModel { - /// The provider of the language model. - pub provider: ZedDotDevAvailableProvider, - /// The model's name in the provider's API. e.g. claude-3-5-sonnet-20240620 - pub name: String, - /// The name displayed in the UI, such as in the assistant panel model dropdown menu. - pub display_name: Option, - /// The size of the context window, indicating the maximum number of tokens the model can process. - pub max_tokens: usize, - /// The maximum number of output tokens allowed by the model. - pub max_output_tokens: Option, - /// The maximum number of completion tokens allowed by the model (o1-* only) - pub max_completion_tokens: Option, - /// Override this model with a different Anthropic model for tool calls. - pub tool_override: Option, - /// Indicates whether this custom model supports caching. - pub cache_configuration: Option, - /// The default temperature to use for this model. - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub default_temperature: Option, - /// Any extra beta headers to provide when using the model. - #[serde(default)] - pub extra_beta_headers: Vec, - /// The model's mode (e.g. thinking) - pub mode: Option, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -#[serde(rename_all = "lowercase")] -pub enum ZedDotDevAvailableProvider { - Anthropic, - OpenAi, - Google, -} - -#[with_fallible_options] -#[derive(Default, Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -pub struct OpenRouterSettingsContent { - pub api_url: Option, - pub available_models: Option>, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct OpenRouterAvailableModel { - pub name: String, - pub display_name: Option, - pub max_tokens: u64, - pub max_output_tokens: Option, - pub max_completion_tokens: Option, - pub supports_tools: Option, - pub supports_images: Option, - pub mode: Option, - pub provider: Option, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct OpenRouterProvider { - order: Option>, - #[serde(default = "default_true")] - allow_fallbacks: bool, - #[serde(default)] - require_parameters: bool, - #[serde(default)] - data_collection: DataCollection, - only: Option>, - ignore: Option>, - quantizations: Option>, - sort: Option, -} - -#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -#[serde(rename_all = "lowercase")] -pub enum DataCollection { - #[default] - Allow, - Disallow, -} - -fn default_true() -> bool { - true -} - -/// Configuration for caching language model messages. -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct LanguageModelCacheConfiguration { - pub max_cache_anchors: usize, - pub should_speculate: bool, - pub min_total_token: u64, -} - -#[derive( - Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom, -)] -#[serde(tag = "type", rename_all = "lowercase")] -pub enum ModelMode { - #[default] - Default, - Thinking { - /// The maximum number of tokens to use for reasoning. Must be lower than the model's `max_output_tokens`. - budget_tokens: Option, - }, -} diff --git a/crates/settings/src/settings_content/project.rs b/crates/settings/src/settings_content/project.rs deleted file mode 100644 index 5cd708694d..0000000000 --- a/crates/settings/src/settings_content/project.rs +++ /dev/null @@ -1,565 +0,0 @@ -use std::{path::PathBuf, sync::Arc}; - -use collections::{BTreeMap, HashMap}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings_macros::{MergeFrom, with_fallible_options}; -use util::serde::default_true; - -use crate::{ - AllLanguageSettingsContent, DelayMs, ExtendingVec, ProjectTerminalSettingsContent, - SlashCommandSettings, -}; - -#[with_fallible_options] -#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct ProjectSettingsContent { - #[serde(flatten)] - pub all_languages: AllLanguageSettingsContent, - - #[serde(flatten)] - pub worktree: WorktreeSettingsContent, - - /// Configuration for language servers. - /// - /// The following settings can be overridden for specific language servers: - /// - initialization_options - /// - /// To override settings for a language, add an entry for that language server's - /// name to the lsp value. - /// Default: null - #[serde(default)] - pub lsp: HashMap, LspSettings>, - - pub terminal: Option, - - /// Configuration for Debugger-related features - #[serde(default)] - pub dap: HashMap, DapSettingsContent>, - - /// Settings for context servers used for AI-related features. - #[serde(default)] - pub context_servers: HashMap, ContextServerSettingsContent>, - - /// Configuration for how direnv configuration should be loaded - pub load_direnv: Option, - - /// Settings for slash commands. - pub slash_commands: Option, - - /// The list of custom Git hosting providers. - pub git_hosting_providers: Option>, -} - -#[with_fallible_options] -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct WorktreeSettingsContent { - /// The displayed name of this project. If not set or null, the root directory name - /// will be displayed. - /// - /// Default: null - pub project_name: Option, - - /// Whether to prevent this project from being shared in public channels. - /// - /// Default: false - #[serde(default)] - pub prevent_sharing_in_public_channels: bool, - - /// Completely ignore files matching globs from `file_scan_exclusions`. Overrides - /// `file_scan_inclusions`. - /// - /// Default: [ - /// "**/.git", - /// "**/.svn", - /// "**/.hg", - /// "**/.jj", - /// "**/CVS", - /// "**/.DS_Store", - /// "**/Thumbs.db", - /// "**/.classpath", - /// "**/.settings" - /// ] - pub file_scan_exclusions: Option>, - - /// Always include files that match these globs when scanning for files, even if they're - /// ignored by git. This setting is overridden by `file_scan_exclusions`. - /// Default: [ - /// ".env*", - /// "docker-compose.*.yml", - /// ] - pub file_scan_inclusions: Option>, - - /// Treat the files matching these globs as `.env` files. - /// Default: ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"] - pub private_files: Option>, - - /// Treat the files matching these globs as hidden files. You can hide hidden files in the project panel. - /// Default: ["**/.*"] - pub hidden_files: Option>, -} - -#[with_fallible_options] -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom, Hash)] -#[serde(rename_all = "snake_case")] -pub struct LspSettings { - pub binary: Option, - /// Options passed to the language server at startup. - /// - /// Ref: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize - /// - /// Consult the documentation for the specific language server to see which settings are supported. - pub initialization_options: Option, - /// Language server settings. - /// - /// Ref: https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#workspace_configuration - /// - /// Consult the documentation for the specific language server to see which settings are supported. - pub settings: Option, - /// If the server supports sending tasks over LSP extensions, - /// this setting can be used to enable or disable them in Zed. - /// Default: true - #[serde(default = "default_true")] - pub enable_lsp_tasks: bool, - pub fetch: Option, -} - -impl Default for LspSettings { - fn default() -> Self { - Self { - binary: None, - initialization_options: None, - settings: None, - enable_lsp_tasks: true, - fetch: None, - } - } -} - -#[with_fallible_options] -#[derive( - Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom, Hash, -)] -pub struct BinarySettings { - pub path: Option, - pub arguments: Option>, - pub env: Option>, - pub ignore_system_version: Option, -} - -#[with_fallible_options] -#[derive( - Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema, MergeFrom, Hash, -)] -pub struct FetchSettings { - // Whether to consider pre-releases for fetching - pub pre_release: Option, -} - -/// Common language server settings. -#[with_fallible_options] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct GlobalLspSettingsContent { - /// Whether to show the LSP servers button in the status bar. - /// - /// Default: `true` - pub button: Option, -} - -#[with_fallible_options] -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -pub struct DapSettingsContent { - pub binary: Option, - pub args: Option>, - pub env: Option>, -} - -#[with_fallible_options] -#[derive( - Default, Copy, Clone, PartialEq, Eq, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, -)] -pub struct SessionSettingsContent { - /// Whether or not to restore unsaved buffers on restart. - /// - /// If this is true, user won't be prompted whether to save/discard - /// dirty files when closing the application. - /// - /// Default: true - pub restore_unsaved_buffers: Option, -} - -#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, MergeFrom, Debug)] -#[serde(untagged, rename_all = "snake_case")] -pub enum ContextServerSettingsContent { - Stdio { - /// Whether the context server is enabled. - #[serde(default = "default_true")] - enabled: bool, - - #[serde(flatten)] - command: ContextServerCommand, - }, - Http { - /// Whether the context server is enabled. - #[serde(default = "default_true")] - enabled: bool, - /// The URL of the remote context server. - url: String, - /// Optional headers to send. - #[serde(skip_serializing_if = "HashMap::is_empty", default)] - headers: HashMap, - }, - Extension { - /// Whether the context server is enabled. - #[serde(default = "default_true")] - enabled: bool, - /// The settings for this context server specified by the extension. - /// - /// Consult the documentation for the context server to see what settings - /// are supported. - settings: serde_json::Value, - }, -} - -impl ContextServerSettingsContent { - pub fn set_enabled(&mut self, enabled: bool) { - match self { - ContextServerSettingsContent::Stdio { - enabled: custom_enabled, - .. - } => { - *custom_enabled = enabled; - } - ContextServerSettingsContent::Extension { - enabled: ext_enabled, - .. - } => *ext_enabled = enabled, - ContextServerSettingsContent::Http { - enabled: remote_enabled, - .. - } => *remote_enabled = enabled, - } - } -} - -#[with_fallible_options] -#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, MergeFrom)] -pub struct ContextServerCommand { - #[serde(rename = "command")] - pub path: PathBuf, - pub args: Vec, - pub env: Option>, - /// Timeout for tool calls in milliseconds. Defaults to 60000 (60 seconds) if not specified. - pub timeout: Option, -} - -impl std::fmt::Debug for ContextServerCommand { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let filtered_env = self.env.as_ref().map(|env| { - env.iter() - .map(|(k, v)| { - ( - k, - if util::redact::should_redact(k) { - "[REDACTED]" - } else { - v - }, - ) - }) - .collect::>() - }); - - f.debug_struct("ContextServerCommand") - .field("path", &self.path) - .field("args", &self.args) - .field("env", &filtered_env) - .finish() - } -} - -#[with_fallible_options] -#[derive(Copy, Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct GitSettings { - /// Whether or not to show the git gutter. - /// - /// Default: tracked_files - pub git_gutter: Option, - /// Sets the debounce threshold (in milliseconds) after which changes are reflected in the git gutter. - /// - /// Default: 0 - pub gutter_debounce: Option, - /// Whether or not to show git blame data inline in - /// the currently focused line. - /// - /// Default: on - pub inline_blame: Option, - /// Git blame settings. - pub blame: Option, - /// Which information to show in the branch picker. - /// - /// Default: on - pub branch_picker: Option, - /// How hunks are displayed visually in the editor. - /// - /// Default: staged_hollow - pub hunk_style: Option, - /// How file paths are displayed in the git gutter. - /// - /// Default: file_name_first - pub path_style: Option, -} - -#[derive( - Clone, - Copy, - Debug, - PartialEq, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum GitGutterSetting { - /// Show git gutter in tracked files. - #[default] - TrackedFiles, - /// Hide git gutter - Hide, -} - -#[with_fallible_options] -#[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -pub struct InlineBlameSettings { - /// Whether or not to show git blame data inline in - /// the currently focused line. - /// - /// Default: true - pub enabled: Option, - /// Whether to only show the inline blame information - /// after a delay once the cursor stops moving. - /// - /// Default: 0 - pub delay_ms: Option, - /// The amount of padding between the end of the source line and the start - /// of the inline blame in units of columns. - /// - /// Default: 7 - pub padding: Option, - /// The minimum column number to show the inline blame information at - /// - /// Default: 0 - pub min_column: Option, - /// Whether to show commit summary as part of the inline blame. - /// - /// Default: false - pub show_commit_summary: Option, -} - -#[with_fallible_options] -#[derive(Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -pub struct BlameSettings { - /// Whether to show the avatar of the author of the commit. - /// - /// Default: true - pub show_avatar: Option, -} - -#[with_fallible_options] -#[derive(Clone, Copy, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -pub struct BranchPickerSettingsContent { - /// Whether to show author name as part of the commit information. - /// - /// Default: false - pub show_author_name: Option, -} - -#[derive( - Clone, - Copy, - PartialEq, - Debug, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum GitHunkStyleSetting { - /// Show unstaged hunks with a filled background and staged hunks hollow. - #[default] - StagedHollow, - /// Show unstaged hunks hollow and staged hunks with a filled background. - UnstagedHollow, -} - -#[with_fallible_options] -#[derive( - Copy, - Clone, - Debug, - PartialEq, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum GitPathStyle { - /// Show file name first, then path - #[default] - FileNameFirst, - /// Show full path first - FilePathFirst, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct DiagnosticsSettingsContent { - /// Whether to show the project diagnostics button in the status bar. - pub button: Option, - - /// Whether or not to include warning diagnostics. - pub include_warnings: Option, - - /// Settings for using LSP pull diagnostics mechanism in Zed. - pub lsp_pull_diagnostics: Option, - - /// Settings for showing inline diagnostics. - pub inline: Option, -} - -#[with_fallible_options] -#[derive( - Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq, -)] -pub struct LspPullDiagnosticsSettingsContent { - /// Whether to pull for diagnostics or not. - /// - /// Default: true - pub enabled: Option, - /// Minimum time to wait before pulling diagnostics from the language server(s). - /// 0 turns the debounce off. - /// - /// Default: 50 - pub debounce_ms: Option, -} - -#[with_fallible_options] -#[derive( - Clone, Copy, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Eq, -)] -pub struct InlineDiagnosticsSettingsContent { - /// Whether or not to show inline diagnostics - /// - /// Default: false - pub enabled: Option, - /// Whether to only show the inline diagnostics after a delay after the - /// last editor event. - /// - /// Default: 150 - pub update_debounce_ms: Option, - /// The amount of padding between the end of the source line and the start - /// of the inline diagnostic in units of columns. - /// - /// Default: 4 - pub padding: Option, - /// The minimum column to display inline diagnostics. This setting can be - /// used to horizontally align inline diagnostics at some position. Lines - /// longer than this value will still push diagnostics further to the right. - /// - /// Default: 0 - pub min_column: Option, - - pub max_severity: Option, -} - -#[with_fallible_options] -#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct NodeBinarySettings { - /// The path to the Node binary. - pub path: Option, - /// The path to the npm binary Zed should use (defaults to `.path/../npm`). - pub npm_path: Option, - /// If enabled, Zed will download its own copy of Node. - pub ignore_system_version: Option, -} - -#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -pub enum DirenvSettings { - /// Load direnv configuration through a shell hook - ShellHook, - /// Load direnv configuration directly using `direnv export json` - #[default] - Direct, - /// Do not load direnv configuration - Disabled, -} - -#[derive( - Clone, - Copy, - Debug, - Eq, - PartialEq, - Ord, - PartialOrd, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum DiagnosticSeverityContent { - // No diagnostics are shown. - Off, - Error, - Warning, - Info, - Hint, - All, -} - -/// A custom Git hosting provider. -#[with_fallible_options] -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct GitHostingProviderConfig { - /// The type of the provider. - /// - /// Must be one of `github`, `gitlab`, `bitbucket`, `gitea`, `forgejo`, or `source_hut`. - pub provider: GitHostingProviderKind, - - /// The base URL for the provider (e.g., "https://code.corp.big.com"). - pub base_url: String, - - /// The display name for the provider (e.g., "BigCorp GitHub"). - pub name: String, -} - -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -pub enum GitHostingProviderKind { - Github, - Gitlab, - Bitbucket, - Gitea, - Forgejo, - SourceHut, -} diff --git a/crates/settings/src/settings_content/terminal.rs b/crates/settings/src/settings_content/terminal.rs deleted file mode 100644 index 1a30eecaa1..0000000000 --- a/crates/settings/src/settings_content/terminal.rs +++ /dev/null @@ -1,517 +0,0 @@ -use std::path::PathBuf; - -use collections::HashMap; -use gpui::{AbsoluteLength, FontFeatures, FontWeight, SharedString, px}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings_macros::{MergeFrom, with_fallible_options}; - -use crate::FontFamilyName; - -#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct ProjectTerminalSettingsContent { - /// What shell to use when opening a terminal. - /// - /// Default: system - pub shell: Option, - /// What working directory to use when launching the terminal - /// - /// Default: current_project_directory - pub working_directory: Option, - /// Any key-value pairs added to this list will be added to the terminal's - /// environment. Use `:` to separate multiple values. - /// - /// Default: {} - pub env: Option>, - /// Activates the python virtual environment, if one is found, in the - /// terminal's working directory (as resolved by the working_directory - /// setting). Set this to "off" to disable this behavior. - /// - /// Default: on - pub detect_venv: Option, - /// Regexes used to identify paths for hyperlink navigation. - /// - /// Default: [ - /// // Python-style diagnostics - /// "File \"(?[^\"]+)\", line (?[0-9]+)", - /// // Common path syntax with optional line, column, description, trailing punctuation, or - /// // surrounding symbols or quotes - /// [ - /// "(?x)", - /// "# optionally starts with 0-2 opening prefix symbols", - /// "[({\\[<]{0,2}", - /// "# which may be followed by an opening quote", - /// "(?[\"'`])?", - /// "# `path` is the shortest sequence of any non-space character", - /// "(?(?[^ ]+?", - /// " # which may end with a line and optionally a column,", - /// " (?:+[0-9]+(:[0-9]+)?|:?\\([0-9]+([,:][0-9]+)?\\))?", - /// "))", - /// "# which must be followed by a matching quote", - /// "(?()\\k)", - /// "# and optionally a single closing symbol", - /// "[)}\\]>]?", - /// "# if line/column matched, may be followed by a description", - /// "(?():[^ 0-9][^ ]*)?", - /// "# which may be followed by trailing punctuation", - /// "[.,:)}\\]>]*", - /// "# and always includes trailing whitespace or end of line", - /// "([ ]+|$)" - /// ] - /// ] - pub path_hyperlink_regexes: Option>, - /// Timeout for hover and Cmd-click path hyperlink discovery in milliseconds. - /// - /// Default: 1 - pub path_hyperlink_timeout_ms: Option, -} - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct TerminalSettingsContent { - #[serde(flatten)] - pub project: ProjectTerminalSettingsContent, - /// Sets the terminal's font size. - /// - /// If this option is not included, - /// the terminal will default to matching the buffer's font size. - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub font_size: Option, - /// Sets the terminal's font family. - /// - /// If this option is not included, - /// the terminal will default to matching the buffer's font family. - pub font_family: Option, - - /// Sets the terminal's font fallbacks. - /// - /// If this option is not included, - /// the terminal will default to matching the buffer's font fallbacks. - #[schemars(extend("uniqueItems" = true))] - pub font_fallbacks: Option>, - - /// Sets the terminal's line height. - /// - /// Default: comfortable - pub line_height: Option, - pub font_features: Option, - /// Sets the terminal's font weight in CSS weight units 0-900. - pub font_weight: Option, - /// Default cursor shape for the terminal. - /// Can be "bar", "block", "underline", or "hollow". - /// - /// Default: "block" - pub cursor_shape: Option, - /// Sets the cursor blinking behavior in the terminal. - /// - /// Default: terminal_controlled - pub blinking: Option, - /// Sets whether Alternate Scroll mode (code: ?1007) is active by default. - /// Alternate Scroll mode converts mouse scroll events into up / down key - /// presses when in the alternate screen (e.g. when running applications - /// like vim or less). The terminal can still set and unset this mode. - /// - /// Default: on - pub alternate_scroll: Option, - /// Sets whether the option key behaves as the meta key. - /// - /// Default: false - pub option_as_meta: Option, - /// Whether or not selecting text in the terminal will automatically - /// copy to the system clipboard. - /// - /// Default: false - pub copy_on_select: Option, - /// Whether to keep the text selection after copying it to the clipboard. - /// - /// Default: true - pub keep_selection_on_copy: Option, - /// Whether to show the terminal button in the status bar. - /// - /// Default: true - pub button: Option, - pub dock: Option, - /// Default width when the terminal is docked to the left or right. - /// - /// Default: 640 - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub default_width: Option, - /// Default height when the terminal is docked to the bottom. - /// - /// Default: 320 - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub default_height: Option, - /// The maximum number of lines to keep in the scrollback history. - /// Maximum allowed value is 100_000, all values above that will be treated as 100_000. - /// 0 disables the scrolling. - /// Existing terminals will not pick up this change until they are recreated. - /// See Alacritty documentation for more information. - /// - /// Default: 10_000 - pub max_scroll_history_lines: Option, - /// The multiplier for scrolling with the mouse wheel. - /// - /// Default: 1.0 - pub scroll_multiplier: Option, - /// Toolbar related settings - pub toolbar: Option, - /// Scrollbar-related settings - pub scrollbar: Option, - /// The minimum APCA perceptual contrast between foreground and background colors. - /// - /// APCA (Accessible Perceptual Contrast Algorithm) is more accurate than WCAG 2.x, - /// especially for dark mode. Values range from 0 to 106. - /// - /// Based on APCA Readability Criterion (ARC) Bronze Simple Mode: - /// https://readtech.org/ARC/tests/bronze-simple-mode/ - /// - 0: No contrast adjustment - /// - 45: Minimum for large fluent text (36px+) - /// - 60: Minimum for other content text - /// - 75: Minimum for body text - /// - 90: Preferred for body text - /// - /// Default: 45 - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub minimum_contrast: Option, -} - -/// Shell configuration to open the terminal with. -#[derive( - Clone, - Debug, - Default, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::EnumDiscriminants, -)] -#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))] -#[serde(rename_all = "snake_case")] -pub enum Shell { - /// Use the system's default terminal configuration in /etc/passwd - #[default] - System, - /// Use a specific program with no arguments. - Program(String), - /// Use a specific program with arguments. - WithArguments { - /// The program to run. - program: String, - /// The arguments to pass to the program. - args: Vec, - /// An optional string to override the title of the terminal tab - title_override: Option, - }, -} - -#[derive( - Clone, - Debug, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::EnumDiscriminants, -)] -#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))] -#[serde(rename_all = "snake_case")] -pub enum WorkingDirectory { - /// Use the current file's project directory. Fallback to the - /// first project directory strategy if unsuccessful. - CurrentProjectDirectory, - /// Use the first project in this workspace's directory. Fallback to using - /// this platform's home directory. - FirstProjectDirectory, - /// Always use this platform's home directory (if it can be found). - AlwaysHome, - /// Always use a specific directory. This value will be shell expanded. - /// If this path is not a valid directory the terminal will default to - /// this platform's home directory (if it can be found). - Always { directory: String }, -} - -#[with_fallible_options] -#[derive( - Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq, Default, -)] -pub struct ScrollbarSettingsContent { - /// When to show the scrollbar in the terminal. - /// - /// Default: inherits editor scrollbar settings - pub show: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom, Default)] -#[serde(rename_all = "snake_case")] -pub enum TerminalLineHeight { - /// Use a line height that's comfortable for reading, 1.618 - #[default] - Comfortable, - /// Use a standard line height, 1.3. This option is useful for TUIs, - /// particularly if they use box characters - Standard, - /// Use a custom line height. - Custom(#[serde(serialize_with = "crate::serialize_f32_with_two_decimal_places")] f32), -} - -impl TerminalLineHeight { - pub fn value(&self) -> AbsoluteLength { - let value = match self { - TerminalLineHeight::Comfortable => 1.618, - TerminalLineHeight::Standard => 1.3, - TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.), - }; - px(value).into() - } -} - -/// When to show the scrollbar. -/// -/// Default: auto -#[derive( - Copy, - Clone, - Debug, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum ShowScrollbar { - /// Show the scrollbar if there's important information or - /// follow the system's configured behavior. - #[default] - Auto, - /// Match the system's configured behavior. - System, - /// Always show the scrollbar. - Always, - /// Never show the scrollbar. - Never, -} - -#[derive( - Clone, - Copy, - Debug, - Default, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -// todo() -> combine with CursorShape -pub enum CursorShapeContent { - /// Cursor is a block like `█`. - #[default] - Block, - /// Cursor is an underscore like `_`. - Underline, - /// Cursor is a vertical bar like `⎸`. - Bar, - /// Cursor is a hollow box like `▯`. - Hollow, -} - -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum TerminalBlink { - /// Never blink the cursor, ignoring the terminal mode. - Off, - /// Default the cursor blink to off, but allow the terminal to - /// set blinking. - TerminalControlled, - /// Always blink the cursor, ignoring the terminal mode. - On, -} - -#[derive( - Clone, - Copy, - Debug, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum AlternateScroll { - On, - Off, -} - -// Toolbar related settings -#[with_fallible_options] -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)] -pub struct TerminalToolbarContent { - /// Whether to display the terminal title in breadcrumbs inside the terminal pane. - /// Only shown if the terminal title is not empty. - /// - /// The shell running in the terminal needs to be configured to emit the title. - /// Example: `echo -e "\e]2;New Title\007";` - /// - /// Default: true - pub breadcrumbs: Option, -} - -#[derive( - Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema, MergeFrom, -)] -#[serde(rename_all = "snake_case")] -pub enum CondaManager { - /// Automatically detect the conda manager - #[default] - Auto, - /// Use conda - Conda, - /// Use mamba - Mamba, - /// Use micromamba - Micromamba, -} - -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -pub enum VenvSettings { - #[default] - Off, - On { - /// Default directories to search for virtual environments, relative - /// to the current working directory. We recommend overriding this - /// in your project's settings, rather than globally. - activate_script: Option, - venv_name: Option, - directories: Option>, - /// Preferred Conda manager to use when activating Conda environments. - /// - /// Default: auto - conda_manager: Option, - }, -} -#[with_fallible_options] -pub struct VenvSettingsContent<'a> { - pub activate_script: ActivateScript, - pub venv_name: &'a str, - pub directories: &'a [PathBuf], - pub conda_manager: CondaManager, -} - -impl VenvSettings { - pub fn as_option(&self) -> Option> { - match self { - VenvSettings::Off => None, - VenvSettings::On { - activate_script, - venv_name, - directories, - conda_manager, - } => Some(VenvSettingsContent { - activate_script: activate_script.unwrap_or(ActivateScript::Default), - venv_name: venv_name.as_deref().unwrap_or(""), - directories: directories.as_deref().unwrap_or(&[]), - conda_manager: conda_manager.unwrap_or(CondaManager::Auto), - }), - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, MergeFrom)] -#[serde(untagged)] -pub enum PathHyperlinkRegex { - SingleLine(String), - MultiLine(Vec), -} - -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum TerminalDockPosition { - Left, - Bottom, - Right, -} - -#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -pub enum ActivateScript { - #[default] - Default, - Csh, - Fish, - Nushell, - PowerShell, - Pyenv, -} - -#[cfg(test)] -mod test { - use serde_json::json; - - use crate::{ProjectSettingsContent, Shell, UserSettingsContent}; - - #[test] - fn test_project_settings() { - let project_content = - json!({"terminal": {"shell": {"program": "/bin/project"}}, "option_as_meta": true}); - - let user_content = - json!({"terminal": {"shell": {"program": "/bin/user"}}, "option_as_meta": false}); - - let user_settings = serde_json::from_value::(user_content).unwrap(); - let project_settings = - serde_json::from_value::(project_content).unwrap(); - - assert_eq!( - user_settings.content.terminal.unwrap().project.shell, - Some(Shell::Program("/bin/user".to_owned())) - ); - assert_eq!(user_settings.content.project.terminal, None); - assert_eq!( - project_settings.terminal.unwrap().shell, - Some(Shell::Program("/bin/project".to_owned())) - ); - } -} diff --git a/crates/settings/src/settings_content/theme.rs b/crates/settings/src/settings_content/theme.rs deleted file mode 100644 index 94045b75a1..0000000000 --- a/crates/settings/src/settings_content/theme.rs +++ /dev/null @@ -1,1269 +0,0 @@ -use collections::{HashMap, IndexMap}; -use gpui::{FontFallbacks, FontFeatures, FontStyle, FontWeight, SharedString}; -use schemars::{JsonSchema, JsonSchema_repr}; -use serde::{Deserialize, Deserializer, Serialize}; -use serde_json::Value; -use serde_repr::{Deserialize_repr, Serialize_repr}; -use settings_macros::{MergeFrom, with_fallible_options}; -use std::{fmt::Display, sync::Arc}; - -use crate::serialize_f32_with_two_decimal_places; - -/// Settings for rendering text in UI and text buffers. - -#[with_fallible_options] -#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct ThemeSettingsContent { - /// The default font size for text in the UI. - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub ui_font_size: Option, - /// The name of a font to use for rendering in the UI. - pub ui_font_family: Option, - /// The font fallbacks to use for rendering in the UI. - #[schemars(default = "default_font_fallbacks")] - #[schemars(extend("uniqueItems" = true))] - pub ui_font_fallbacks: Option>, - /// The OpenType features to enable for text in the UI. - #[schemars(default = "default_font_features")] - pub ui_font_features: Option, - /// The weight of the UI font in CSS units from 100 to 900. - #[schemars(default = "default_buffer_font_weight")] - pub ui_font_weight: Option, - /// The name of a font to use for rendering in text buffers. - pub buffer_font_family: Option, - /// The font fallbacks to use for rendering in text buffers. - #[schemars(extend("uniqueItems" = true))] - pub buffer_font_fallbacks: Option>, - /// The default font size for rendering in text buffers. - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub buffer_font_size: Option, - /// The weight of the editor font in CSS units from 100 to 900. - #[schemars(default = "default_buffer_font_weight")] - pub buffer_font_weight: Option, - /// The buffer's line height. - pub buffer_line_height: Option, - /// The OpenType features to enable for rendering in text buffers. - #[schemars(default = "default_font_features")] - pub buffer_font_features: Option, - /// The font size for agent responses in the agent panel. Falls back to the UI font size if unset. - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub agent_ui_font_size: Option, - /// The font size for user messages in the agent panel. - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub agent_buffer_font_size: Option, - /// The name of the Zed theme to use. - pub theme: Option, - /// The name of the icon theme to use. - pub icon_theme: Option, - - /// UNSTABLE: Expect many elements to be broken. - /// - // Controls the density of the UI. - #[serde(rename = "unstable.ui_density")] - pub ui_density: Option, - - /// How much to fade out unused code. - #[schemars(range(min = 0.0, max = 0.9))] - pub unnecessary_code_fade: Option, - - /// EXPERIMENTAL: Overrides for the current theme. - /// - /// These values will override the ones on the current theme specified in `theme`. - #[serde(rename = "experimental.theme_overrides")] - pub experimental_theme_overrides: Option, - - /// Overrides per theme - /// - /// These values will override the ones on the specified theme - #[serde(default)] - pub theme_overrides: HashMap, -} - -#[derive( - Clone, - Copy, - Debug, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - PartialOrd, - derive_more::FromStr, -)] -#[serde(transparent)] -pub struct CodeFade(#[serde(serialize_with = "serialize_f32_with_two_decimal_places")] pub f32); - -impl Display for CodeFade { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{:.2}", self.0) - } -} - -impl From for CodeFade { - fn from(x: f32) -> Self { - Self(x) - } -} - -fn default_font_features() -> Option { - Some(FontFeatures::default()) -} - -fn default_font_fallbacks() -> Option { - Some(FontFallbacks::default()) -} - -fn default_buffer_font_weight() -> Option { - Some(FontWeight::default()) -} - -/// Represents the selection of a theme, which can be either static or dynamic. -#[derive( - Clone, - Debug, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::EnumDiscriminants, -)] -#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))] -#[serde(untagged)] -pub enum ThemeSelection { - /// A static theme selection, represented by a single theme name. - Static(ThemeName), - /// A dynamic theme selection, which can change based the [ThemeMode]. - Dynamic { - /// The mode used to determine which theme to use. - #[serde(default)] - mode: ThemeAppearanceMode, - /// The theme to use for light mode. - light: ThemeName, - /// The theme to use for dark mode. - dark: ThemeName, - }, -} - -/// Represents the selection of an icon theme, which can be either static or dynamic. -#[derive( - Clone, - Debug, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::EnumDiscriminants, -)] -#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))] -#[serde(untagged)] -pub enum IconThemeSelection { - /// A static icon theme selection, represented by a single icon theme name. - Static(IconThemeName), - /// A dynamic icon theme selection, which can change based on the [`ThemeMode`]. - Dynamic { - /// The mode used to determine which theme to use. - #[serde(default)] - mode: ThemeAppearanceMode, - /// The icon theme to use for light mode. - light: IconThemeName, - /// The icon theme to use for dark mode. - dark: IconThemeName, - }, -} - -/// The mode use to select a theme. -/// -/// `Light` and `Dark` will select their respective themes. -/// -/// `System` will select the theme based on the system's appearance. -#[derive( - Debug, - PartialEq, - Eq, - Clone, - Copy, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum ThemeAppearanceMode { - /// Use the specified `light` theme. - Light, - - /// Use the specified `dark` theme. - Dark, - - /// Use the theme based on the system's appearance. - #[default] - System, -} - -/// Specifies the density of the UI. -/// Note: This setting is still experimental. See [this tracking issue](https://github.com/zed-industries/zed/issues/18078) -#[derive( - Debug, - Default, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - Clone, - Copy, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, -)] -#[serde(rename_all = "snake_case")] -pub enum UiDensity { - /// A denser UI with tighter spacing and smaller elements. - #[serde(alias = "compact")] - Compact, - #[default] - #[serde(alias = "default")] - /// The default UI density. - Default, - #[serde(alias = "comfortable")] - /// A looser UI with more spacing and larger elements. - Comfortable, -} - -impl UiDensity { - /// The spacing ratio of a given density. - /// TODO: Standardize usage throughout the app or remove - pub fn spacing_ratio(self) -> f32 { - match self { - UiDensity::Compact => 0.75, - UiDensity::Default => 1.0, - UiDensity::Comfortable => 1.25, - } - } -} - -/// Font family name. -#[with_fallible_options] -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)] -#[serde(transparent)] -pub struct FontFamilyName(pub Arc); - -impl AsRef for FontFamilyName { - fn as_ref(&self) -> &str { - &self.0 - } -} - -impl From for FontFamilyName { - fn from(value: SharedString) -> Self { - Self(Arc::from(value)) - } -} - -impl From for SharedString { - fn from(value: FontFamilyName) -> Self { - SharedString::new(value.0) - } -} - -impl From for FontFamilyName { - fn from(value: String) -> Self { - Self(Arc::from(value)) - } -} - -impl From for String { - fn from(value: FontFamilyName) -> Self { - value.0.to_string() - } -} - -/// The buffer's line height. -#[derive( - Clone, - Copy, - Debug, - Serialize, - Deserialize, - PartialEq, - JsonSchema, - MergeFrom, - Default, - strum::EnumDiscriminants, -)] -#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))] -#[serde(rename_all = "snake_case")] -pub enum BufferLineHeight { - /// A less dense line height. - #[default] - Comfortable, - /// The default line height. - Standard, - /// A custom line height, where 1.0 is the font's height. Must be at least 1.0. - Custom(#[serde(deserialize_with = "deserialize_line_height")] f32), -} - -fn deserialize_line_height<'de, D>(deserializer: D) -> Result -where - D: serde::Deserializer<'de>, -{ - let value = f32::deserialize(deserializer)?; - if value < 1.0 { - return Err(serde::de::Error::custom( - "buffer_line_height.custom must be at least 1.0", - )); - } - - Ok(value) -} - -/// The content of a serialized theme. -#[with_fallible_options] -#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -#[serde(default)] -pub struct ThemeStyleContent { - #[serde(rename = "background.appearance")] - pub window_background_appearance: Option, - - #[serde(default)] - pub accents: Vec, - - #[serde(flatten, default)] - pub colors: ThemeColorsContent, - - #[serde(flatten, default)] - pub status: StatusColorsContent, - - #[serde(default)] - pub players: Vec, - - /// The styles for syntax nodes. - #[serde(default)] - pub syntax: IndexMap, -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -pub struct AccentContent(pub Option); - -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -pub struct PlayerColorContent { - pub cursor: Option, - pub background: Option, - pub selection: Option, -} - -/// Theme name. -#[with_fallible_options] -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)] -#[serde(transparent)] -pub struct ThemeName(pub Arc); - -/// Icon Theme Name -#[with_fallible_options] -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq)] -#[serde(transparent)] -pub struct IconThemeName(pub Arc); - -#[with_fallible_options] -#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -#[serde(default)] -pub struct ThemeColorsContent { - /// Border color. Used for most borders, is usually a high contrast color. - #[serde(rename = "border")] - pub border: Option, - - /// Border color. Used for deemphasized borders, like a visual divider between two sections - #[serde(rename = "border.variant")] - pub border_variant: Option, - - /// Border color. Used for focused elements, like keyboard focused list item. - #[serde(rename = "border.focused")] - pub border_focused: Option, - - /// Border color. Used for selected elements, like an active search filter or selected checkbox. - #[serde(rename = "border.selected")] - pub border_selected: Option, - - /// Border color. Used for transparent borders. Used for placeholder borders when an element gains a border on state change. - #[serde(rename = "border.transparent")] - pub border_transparent: Option, - - /// Border color. Used for disabled elements, like a disabled input or button. - #[serde(rename = "border.disabled")] - pub border_disabled: Option, - - /// Background color. Used for elevated surfaces, like a context menu, popup, or dialog. - #[serde(rename = "elevated_surface.background")] - pub elevated_surface_background: Option, - - /// Background Color. Used for grounded surfaces like a panel or tab. - #[serde(rename = "surface.background")] - pub surface_background: Option, - - /// Background Color. Used for the app background and blank panels or windows. - #[serde(rename = "background")] - pub background: Option, - - /// Background Color. Used for the background of an element that should have a different background than the surface it's on. - /// - /// Elements might include: Buttons, Inputs, Checkboxes, Radio Buttons... - /// - /// For an element that should have the same background as the surface it's on, use `ghost_element_background`. - #[serde(rename = "element.background")] - pub element_background: Option, - - /// Background Color. Used for the hover state of an element that should have a different background than the surface it's on. - /// - /// Hover states are triggered by the mouse entering an element, or a finger touching an element on a touch screen. - #[serde(rename = "element.hover")] - pub element_hover: Option, - - /// Background Color. Used for the active state of an element that should have a different background than the surface it's on. - /// - /// Active states are triggered by the mouse button being pressed down on an element, or the Return button or other activator being pressed. - #[serde(rename = "element.active")] - pub element_active: Option, - - /// Background Color. Used for the selected state of an element that should have a different background than the surface it's on. - /// - /// Selected states are triggered by the element being selected (or "activated") by the user. - /// - /// This could include a selected checkbox, a toggleable button that is toggled on, etc. - #[serde(rename = "element.selected")] - pub element_selected: Option, - - /// Background Color. Used for the disabled state of an element that should have a different background than the surface it's on. - /// - /// Disabled states are shown when a user cannot interact with an element, like a disabled button or input. - #[serde(rename = "element.disabled")] - pub element_disabled: Option, - - /// Background Color. Used for the background of selections in a UI element. - #[serde(rename = "element.selection_background")] - pub element_selection_background: Option, - - /// Background Color. Used for the area that shows where a dragged element will be dropped. - #[serde(rename = "drop_target.background")] - pub drop_target_background: Option, - - /// Border Color. Used for the border that shows where a dragged element will be dropped. - #[serde(rename = "drop_target.border")] - pub drop_target_border: Option, - - /// Used for the background of a ghost element that should have the same background as the surface it's on. - /// - /// Elements might include: Buttons, Inputs, Checkboxes, Radio Buttons... - /// - /// For an element that should have a different background than the surface it's on, use `element_background`. - #[serde(rename = "ghost_element.background")] - pub ghost_element_background: Option, - - /// Background Color. Used for the hover state of a ghost element that should have the same background as the surface it's on. - /// - /// Hover states are triggered by the mouse entering an element, or a finger touching an element on a touch screen. - #[serde(rename = "ghost_element.hover")] - pub ghost_element_hover: Option, - - /// Background Color. Used for the active state of a ghost element that should have the same background as the surface it's on. - /// - /// Active states are triggered by the mouse button being pressed down on an element, or the Return button or other activator being pressed. - #[serde(rename = "ghost_element.active")] - pub ghost_element_active: Option, - - /// Background Color. Used for the selected state of a ghost element that should have the same background as the surface it's on. - /// - /// Selected states are triggered by the element being selected (or "activated") by the user. - /// - /// This could include a selected checkbox, a toggleable button that is toggled on, etc. - #[serde(rename = "ghost_element.selected")] - pub ghost_element_selected: Option, - - /// Background Color. Used for the disabled state of a ghost element that should have the same background as the surface it's on. - /// - /// Disabled states are shown when a user cannot interact with an element, like a disabled button or input. - #[serde(rename = "ghost_element.disabled")] - pub ghost_element_disabled: Option, - - /// Text Color. Default text color used for most text. - #[serde(rename = "text")] - pub text: Option, - - /// Text Color. Color of muted or deemphasized text. It is a subdued version of the standard text color. - #[serde(rename = "text.muted")] - pub text_muted: Option, - - /// Text Color. Color of the placeholder text typically shown in input fields to guide the user to enter valid data. - #[serde(rename = "text.placeholder")] - pub text_placeholder: Option, - - /// Text Color. Color used for text denoting disabled elements. Typically, the color is faded or grayed out to emphasize the disabled state. - #[serde(rename = "text.disabled")] - pub text_disabled: Option, - - /// Text Color. Color used for emphasis or highlighting certain text, like an active filter or a matched character in a search. - #[serde(rename = "text.accent")] - pub text_accent: Option, - - /// Fill Color. Used for the default fill color of an icon. - #[serde(rename = "icon")] - pub icon: Option, - - /// Fill Color. Used for the muted or deemphasized fill color of an icon. - /// - /// This might be used to show an icon in an inactive pane, or to deemphasize a series of icons to give them less visual weight. - #[serde(rename = "icon.muted")] - pub icon_muted: Option, - - /// Fill Color. Used for the disabled fill color of an icon. - /// - /// Disabled states are shown when a user cannot interact with an element, like a icon button. - #[serde(rename = "icon.disabled")] - pub icon_disabled: Option, - - /// Fill Color. Used for the placeholder fill color of an icon. - /// - /// This might be used to show an icon in an input that disappears when the user enters text. - #[serde(rename = "icon.placeholder")] - pub icon_placeholder: Option, - - /// Fill Color. Used for the accent fill color of an icon. - /// - /// This might be used to show when a toggleable icon button is selected. - #[serde(rename = "icon.accent")] - pub icon_accent: Option, - - /// Color used to accent some of the debuggers elements - /// Only accent breakpoint & breakpoint related symbols right now - #[serde(rename = "debugger.accent")] - pub debugger_accent: Option, - - #[serde(rename = "status_bar.background")] - pub status_bar_background: Option, - - #[serde(rename = "title_bar.background")] - pub title_bar_background: Option, - - #[serde(rename = "title_bar.inactive_background")] - pub title_bar_inactive_background: Option, - - #[serde(rename = "toolbar.background")] - pub toolbar_background: Option, - - #[serde(rename = "tab_bar.background")] - pub tab_bar_background: Option, - - #[serde(rename = "tab.inactive_background")] - pub tab_inactive_background: Option, - - #[serde(rename = "tab.active_background")] - pub tab_active_background: Option, - - #[serde(rename = "search.match_background")] - pub search_match_background: Option, - - #[serde(rename = "search.active_match_background")] - pub search_active_match_background: Option, - - #[serde(rename = "panel.background")] - pub panel_background: Option, - - #[serde(rename = "panel.focused_border")] - pub panel_focused_border: Option, - - #[serde(rename = "panel.indent_guide")] - pub panel_indent_guide: Option, - - #[serde(rename = "panel.indent_guide_hover")] - pub panel_indent_guide_hover: Option, - - #[serde(rename = "panel.indent_guide_active")] - pub panel_indent_guide_active: Option, - - #[serde(rename = "panel.overlay_background")] - pub panel_overlay_background: Option, - - #[serde(rename = "panel.overlay_hover")] - pub panel_overlay_hover: Option, - - #[serde(rename = "pane.focused_border")] - pub pane_focused_border: Option, - - #[serde(rename = "pane_group.border")] - pub pane_group_border: Option, - - /// The deprecated version of `scrollbar.thumb.background`. - /// - /// Don't use this field. - #[serde(rename = "scrollbar_thumb.background", skip_serializing)] - #[schemars(skip)] - pub deprecated_scrollbar_thumb_background: Option, - - /// The color of the scrollbar thumb. - #[serde(rename = "scrollbar.thumb.background")] - pub scrollbar_thumb_background: Option, - - /// The color of the scrollbar thumb when hovered over. - #[serde(rename = "scrollbar.thumb.hover_background")] - pub scrollbar_thumb_hover_background: Option, - - /// The color of the scrollbar thumb whilst being actively dragged. - #[serde(rename = "scrollbar.thumb.active_background")] - pub scrollbar_thumb_active_background: Option, - - /// The border color of the scrollbar thumb. - #[serde(rename = "scrollbar.thumb.border")] - pub scrollbar_thumb_border: Option, - - /// The background color of the scrollbar track. - #[serde(rename = "scrollbar.track.background")] - pub scrollbar_track_background: Option, - - /// The border color of the scrollbar track. - #[serde(rename = "scrollbar.track.border")] - pub scrollbar_track_border: Option, - - /// The color of the minimap thumb. - #[serde(rename = "minimap.thumb.background")] - pub minimap_thumb_background: Option, - - /// The color of the minimap thumb when hovered over. - #[serde(rename = "minimap.thumb.hover_background")] - pub minimap_thumb_hover_background: Option, - - /// The color of the minimap thumb whilst being actively dragged. - #[serde(rename = "minimap.thumb.active_background")] - pub minimap_thumb_active_background: Option, - - /// The border color of the minimap thumb. - #[serde(rename = "minimap.thumb.border")] - pub minimap_thumb_border: Option, - - #[serde(rename = "editor.foreground")] - pub editor_foreground: Option, - - #[serde(rename = "editor.background")] - pub editor_background: Option, - - #[serde(rename = "editor.gutter.background")] - pub editor_gutter_background: Option, - - #[serde(rename = "editor.subheader.background")] - pub editor_subheader_background: Option, - - #[serde(rename = "editor.active_line.background")] - pub editor_active_line_background: Option, - - #[serde(rename = "editor.highlighted_line.background")] - pub editor_highlighted_line_background: Option, - - /// Background of active line of debugger - #[serde(rename = "editor.debugger_active_line.background")] - pub editor_debugger_active_line_background: Option, - - /// Text Color. Used for the text of the line number in the editor gutter. - #[serde(rename = "editor.line_number")] - pub editor_line_number: Option, - - /// Text Color. Used for the text of the line number in the editor gutter when the line is highlighted. - #[serde(rename = "editor.active_line_number")] - pub editor_active_line_number: Option, - - /// Text Color. Used for the text of the line number in the editor gutter when the line is hovered over. - #[serde(rename = "editor.hover_line_number")] - pub editor_hover_line_number: Option, - - /// Text Color. Used to mark invisible characters in the editor. - /// - /// Example: spaces, tabs, carriage returns, etc. - #[serde(rename = "editor.invisible")] - pub editor_invisible: Option, - - #[serde(rename = "editor.wrap_guide")] - pub editor_wrap_guide: Option, - - #[serde(rename = "editor.active_wrap_guide")] - pub editor_active_wrap_guide: Option, - - #[serde(rename = "editor.indent_guide")] - pub editor_indent_guide: Option, - - #[serde(rename = "editor.indent_guide_active")] - pub editor_indent_guide_active: Option, - - /// Read-access of a symbol, like reading a variable. - /// - /// A document highlight is a range inside a text document which deserves - /// special attention. Usually a document highlight is visualized by changing - /// the background color of its range. - #[serde(rename = "editor.document_highlight.read_background")] - pub editor_document_highlight_read_background: Option, - - /// Read-access of a symbol, like reading a variable. - /// - /// A document highlight is a range inside a text document which deserves - /// special attention. Usually a document highlight is visualized by changing - /// the background color of its range. - #[serde(rename = "editor.document_highlight.write_background")] - pub editor_document_highlight_write_background: Option, - - /// Highlighted brackets background color. - /// - /// Matching brackets in the cursor scope are highlighted with this background color. - #[serde(rename = "editor.document_highlight.bracket_background")] - pub editor_document_highlight_bracket_background: Option, - - /// Terminal background color. - #[serde(rename = "terminal.background")] - pub terminal_background: Option, - - /// Terminal foreground color. - #[serde(rename = "terminal.foreground")] - pub terminal_foreground: Option, - - /// Terminal ANSI background color. - #[serde(rename = "terminal.ansi.background")] - pub terminal_ansi_background: Option, - - /// Bright terminal foreground color. - #[serde(rename = "terminal.bright_foreground")] - pub terminal_bright_foreground: Option, - - /// Dim terminal foreground color. - #[serde(rename = "terminal.dim_foreground")] - pub terminal_dim_foreground: Option, - - /// Black ANSI terminal color. - #[serde(rename = "terminal.ansi.black")] - pub terminal_ansi_black: Option, - - /// Bright black ANSI terminal color. - #[serde(rename = "terminal.ansi.bright_black")] - pub terminal_ansi_bright_black: Option, - - /// Dim black ANSI terminal color. - #[serde(rename = "terminal.ansi.dim_black")] - pub terminal_ansi_dim_black: Option, - - /// Red ANSI terminal color. - #[serde(rename = "terminal.ansi.red")] - pub terminal_ansi_red: Option, - - /// Bright red ANSI terminal color. - #[serde(rename = "terminal.ansi.bright_red")] - pub terminal_ansi_bright_red: Option, - - /// Dim red ANSI terminal color. - #[serde(rename = "terminal.ansi.dim_red")] - pub terminal_ansi_dim_red: Option, - - /// Green ANSI terminal color. - #[serde(rename = "terminal.ansi.green")] - pub terminal_ansi_green: Option, - - /// Bright green ANSI terminal color. - #[serde(rename = "terminal.ansi.bright_green")] - pub terminal_ansi_bright_green: Option, - - /// Dim green ANSI terminal color. - #[serde(rename = "terminal.ansi.dim_green")] - pub terminal_ansi_dim_green: Option, - - /// Yellow ANSI terminal color. - #[serde(rename = "terminal.ansi.yellow")] - pub terminal_ansi_yellow: Option, - - /// Bright yellow ANSI terminal color. - #[serde(rename = "terminal.ansi.bright_yellow")] - pub terminal_ansi_bright_yellow: Option, - - /// Dim yellow ANSI terminal color. - #[serde(rename = "terminal.ansi.dim_yellow")] - pub terminal_ansi_dim_yellow: Option, - - /// Blue ANSI terminal color. - #[serde(rename = "terminal.ansi.blue")] - pub terminal_ansi_blue: Option, - - /// Bright blue ANSI terminal color. - #[serde(rename = "terminal.ansi.bright_blue")] - pub terminal_ansi_bright_blue: Option, - - /// Dim blue ANSI terminal color. - #[serde(rename = "terminal.ansi.dim_blue")] - pub terminal_ansi_dim_blue: Option, - - /// Magenta ANSI terminal color. - #[serde(rename = "terminal.ansi.magenta")] - pub terminal_ansi_magenta: Option, - - /// Bright magenta ANSI terminal color. - #[serde(rename = "terminal.ansi.bright_magenta")] - pub terminal_ansi_bright_magenta: Option, - - /// Dim magenta ANSI terminal color. - #[serde(rename = "terminal.ansi.dim_magenta")] - pub terminal_ansi_dim_magenta: Option, - - /// Cyan ANSI terminal color. - #[serde(rename = "terminal.ansi.cyan")] - pub terminal_ansi_cyan: Option, - - /// Bright cyan ANSI terminal color. - #[serde(rename = "terminal.ansi.bright_cyan")] - pub terminal_ansi_bright_cyan: Option, - - /// Dim cyan ANSI terminal color. - #[serde(rename = "terminal.ansi.dim_cyan")] - pub terminal_ansi_dim_cyan: Option, - - /// White ANSI terminal color. - #[serde(rename = "terminal.ansi.white")] - pub terminal_ansi_white: Option, - - /// Bright white ANSI terminal color. - #[serde(rename = "terminal.ansi.bright_white")] - pub terminal_ansi_bright_white: Option, - - /// Dim white ANSI terminal color. - #[serde(rename = "terminal.ansi.dim_white")] - pub terminal_ansi_dim_white: Option, - - #[serde(rename = "link_text.hover")] - pub link_text_hover: Option, - - /// Added version control color. - #[serde(rename = "version_control.added")] - pub version_control_added: Option, - - /// Deleted version control color. - #[serde(rename = "version_control.deleted")] - pub version_control_deleted: Option, - - /// Modified version control color. - #[serde(rename = "version_control.modified")] - pub version_control_modified: Option, - - /// Renamed version control color. - #[serde(rename = "version_control.renamed")] - pub version_control_renamed: Option, - - /// Conflict version control color. - #[serde(rename = "version_control.conflict")] - pub version_control_conflict: Option, - - /// Ignored version control color. - #[serde(rename = "version_control.ignored")] - pub version_control_ignored: Option, - - /// Color for added words in word diffs. - #[serde(rename = "version_control.word_added")] - pub version_control_word_added: Option, - - /// Color for deleted words in word diffs. - #[serde(rename = "version_control.word_deleted")] - pub version_control_word_deleted: Option, - - /// Background color for row highlights of "ours" regions in merge conflicts. - #[serde(rename = "version_control.conflict_marker.ours")] - pub version_control_conflict_marker_ours: Option, - - /// Background color for row highlights of "theirs" regions in merge conflicts. - #[serde(rename = "version_control.conflict_marker.theirs")] - pub version_control_conflict_marker_theirs: Option, - - /// Deprecated in favor of `version_control_conflict_marker_ours`. - #[deprecated] - pub version_control_conflict_ours_background: Option, - - /// Deprecated in favor of `version_control_conflict_marker_theirs`. - #[deprecated] - pub version_control_conflict_theirs_background: Option, - - /// Background color for Vim Normal mode indicator. - #[serde(rename = "vim.normal.background")] - pub vim_normal_background: Option, - /// Background color for Vim Insert mode indicator. - #[serde(rename = "vim.insert.background")] - pub vim_insert_background: Option, - /// Background color for Vim Replace mode indicator. - #[serde(rename = "vim.replace.background")] - pub vim_replace_background: Option, - /// Background color for Vim Visual mode indicator. - #[serde(rename = "vim.visual.background")] - pub vim_visual_background: Option, - /// Background color for Vim Visual Line mode indicator. - #[serde(rename = "vim.visual_line.background")] - pub vim_visual_line_background: Option, - /// Background color for Vim Visual Block mode indicator. - #[serde(rename = "vim.visual_block.background")] - pub vim_visual_block_background: Option, - /// Background color for Vim Helix Normal mode indicator. - #[serde(rename = "vim.helix_normal.background")] - pub vim_helix_normal_background: Option, - /// Background color for Vim Helix Select mode indicator. - #[serde(rename = "vim.helix_select.background")] - pub vim_helix_select_background: Option, - - /// Text color for Vim mode indicator label. - #[serde(rename = "vim.mode.text")] - pub vim_mode_text: Option, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -#[serde(default)] -pub struct HighlightStyleContent { - pub color: Option, - - #[serde( - skip_serializing_if = "Option::is_none", - deserialize_with = "treat_error_as_none" - )] - pub background_color: Option, - - #[serde( - skip_serializing_if = "Option::is_none", - deserialize_with = "treat_error_as_none" - )] - pub font_style: Option, - - #[serde( - skip_serializing_if = "Option::is_none", - deserialize_with = "treat_error_as_none" - )] - pub font_weight: Option, -} - -impl HighlightStyleContent { - pub fn is_empty(&self) -> bool { - self.color.is_none() - && self.background_color.is_none() - && self.font_style.is_none() - && self.font_weight.is_none() - } -} - -fn treat_error_as_none<'de, T, D>(deserializer: D) -> Result, D::Error> -where - T: Deserialize<'de>, - D: Deserializer<'de>, -{ - let value: Value = Deserialize::deserialize(deserializer)?; - Ok(T::deserialize(value).ok()) -} - -#[with_fallible_options] -#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -#[serde(default)] -pub struct StatusColorsContent { - /// Indicates some kind of conflict, like a file changed on disk while it was open, or - /// merge conflicts in a Git repository. - #[serde(rename = "conflict")] - pub conflict: Option, - - #[serde(rename = "conflict.background")] - pub conflict_background: Option, - - #[serde(rename = "conflict.border")] - pub conflict_border: Option, - - /// Indicates something new, like a new file added to a Git repository. - #[serde(rename = "created")] - pub created: Option, - - #[serde(rename = "created.background")] - pub created_background: Option, - - #[serde(rename = "created.border")] - pub created_border: Option, - - /// Indicates that something no longer exists, like a deleted file. - #[serde(rename = "deleted")] - pub deleted: Option, - - #[serde(rename = "deleted.background")] - pub deleted_background: Option, - - #[serde(rename = "deleted.border")] - pub deleted_border: Option, - - /// Indicates a system error, a failed operation or a diagnostic error. - #[serde(rename = "error")] - pub error: Option, - - #[serde(rename = "error.background")] - pub error_background: Option, - - #[serde(rename = "error.border")] - pub error_border: Option, - - /// Represents a hidden status, such as a file being hidden in a file tree. - #[serde(rename = "hidden")] - pub hidden: Option, - - #[serde(rename = "hidden.background")] - pub hidden_background: Option, - - #[serde(rename = "hidden.border")] - pub hidden_border: Option, - - /// Indicates a hint or some kind of additional information. - #[serde(rename = "hint")] - pub hint: Option, - - #[serde(rename = "hint.background")] - pub hint_background: Option, - - #[serde(rename = "hint.border")] - pub hint_border: Option, - - /// Indicates that something is deliberately ignored, such as a file or operation ignored by Git. - #[serde(rename = "ignored")] - pub ignored: Option, - - #[serde(rename = "ignored.background")] - pub ignored_background: Option, - - #[serde(rename = "ignored.border")] - pub ignored_border: Option, - - /// Represents informational status updates or messages. - #[serde(rename = "info")] - pub info: Option, - - #[serde(rename = "info.background")] - pub info_background: Option, - - #[serde(rename = "info.border")] - pub info_border: Option, - - /// Indicates a changed or altered status, like a file that has been edited. - #[serde(rename = "modified")] - pub modified: Option, - - #[serde(rename = "modified.background")] - pub modified_background: Option, - - #[serde(rename = "modified.border")] - pub modified_border: Option, - - /// Indicates something that is predicted, like automatic code completion, or generated code. - #[serde(rename = "predictive")] - pub predictive: Option, - - #[serde(rename = "predictive.background")] - pub predictive_background: Option, - - #[serde(rename = "predictive.border")] - pub predictive_border: Option, - - /// Represents a renamed status, such as a file that has been renamed. - #[serde(rename = "renamed")] - pub renamed: Option, - - #[serde(rename = "renamed.background")] - pub renamed_background: Option, - - #[serde(rename = "renamed.border")] - pub renamed_border: Option, - - /// Indicates a successful operation or task completion. - #[serde(rename = "success")] - pub success: Option, - - #[serde(rename = "success.background")] - pub success_background: Option, - - #[serde(rename = "success.border")] - pub success_border: Option, - - /// Indicates some kind of unreachable status, like a block of code that can never be reached. - #[serde(rename = "unreachable")] - pub unreachable: Option, - - #[serde(rename = "unreachable.background")] - pub unreachable_background: Option, - - #[serde(rename = "unreachable.border")] - pub unreachable_border: Option, - - /// Represents a warning status, like an operation that is about to fail. - #[serde(rename = "warning")] - pub warning: Option, - - #[serde(rename = "warning.background")] - pub warning_background: Option, - - #[serde(rename = "warning.border")] - pub warning_border: Option, -} - -/// The background appearance of the window. -#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -pub enum WindowBackgroundContent { - Opaque, - Transparent, - Blurred, -} - -impl Into for WindowBackgroundContent { - fn into(self) -> gpui::WindowBackgroundAppearance { - match self { - WindowBackgroundContent::Opaque => gpui::WindowBackgroundAppearance::Opaque, - WindowBackgroundContent::Transparent => gpui::WindowBackgroundAppearance::Transparent, - WindowBackgroundContent::Blurred => gpui::WindowBackgroundAppearance::Blurred, - } - } -} - -#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq)] -#[serde(rename_all = "snake_case")] -pub enum FontStyleContent { - Normal, - Italic, - Oblique, -} - -impl From for FontStyle { - fn from(value: FontStyleContent) -> Self { - match value { - FontStyleContent::Normal => FontStyle::Normal, - FontStyleContent::Italic => FontStyle::Italic, - FontStyleContent::Oblique => FontStyle::Oblique, - } - } -} - -#[derive( - Debug, Clone, Copy, Serialize_repr, Deserialize_repr, JsonSchema_repr, PartialEq, MergeFrom, -)] -#[repr(u16)] -pub enum FontWeightContent { - Thin = 100, - ExtraLight = 200, - Light = 300, - Normal = 400, - Medium = 500, - Semibold = 600, - Bold = 700, - ExtraBold = 800, - Black = 900, -} - -impl From for FontWeight { - fn from(value: FontWeightContent) -> Self { - match value { - FontWeightContent::Thin => FontWeight::THIN, - FontWeightContent::ExtraLight => FontWeight::EXTRA_LIGHT, - FontWeightContent::Light => FontWeight::LIGHT, - FontWeightContent::Normal => FontWeight::NORMAL, - FontWeightContent::Medium => FontWeight::MEDIUM, - FontWeightContent::Semibold => FontWeight::SEMIBOLD, - FontWeightContent::Bold => FontWeight::BOLD, - FontWeightContent::ExtraBold => FontWeight::EXTRA_BOLD, - FontWeightContent::Black => FontWeight::BLACK, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::json; - - #[test] - fn test_buffer_line_height_deserialize_valid() { - assert_eq!( - serde_json::from_value::(json!("comfortable")).unwrap(), - BufferLineHeight::Comfortable - ); - assert_eq!( - serde_json::from_value::(json!("standard")).unwrap(), - BufferLineHeight::Standard - ); - assert_eq!( - serde_json::from_value::(json!({"custom": 1.0})).unwrap(), - BufferLineHeight::Custom(1.0) - ); - assert_eq!( - serde_json::from_value::(json!({"custom": 1.5})).unwrap(), - BufferLineHeight::Custom(1.5) - ); - } - - #[test] - fn test_buffer_line_height_deserialize_invalid() { - assert!( - serde_json::from_value::(json!({"custom": 0.99})) - .err() - .unwrap() - .to_string() - .contains("buffer_line_height.custom must be at least 1.0") - ); - assert!( - serde_json::from_value::(json!({"custom": 0.0})) - .err() - .unwrap() - .to_string() - .contains("buffer_line_height.custom must be at least 1.0") - ); - assert!( - serde_json::from_value::(json!({"custom": -1.0})) - .err() - .unwrap() - .to_string() - .contains("buffer_line_height.custom must be at least 1.0") - ); - } - - #[test] - fn test_buffer_font_weight_schema_has_default() { - use schemars::schema_for; - - let schema = schema_for!(ThemeSettingsContent); - let schema_value = serde_json::to_value(&schema).unwrap(); - - let properties = &schema_value["properties"]; - let buffer_font_weight = &properties["buffer_font_weight"]; - - assert!( - buffer_font_weight.get("default").is_some(), - "buffer_font_weight should have a default value in the schema" - ); - - let default_value = &buffer_font_weight["default"]; - assert_eq!( - default_value.as_f64(), - Some(FontWeight::NORMAL.0 as f64), - "buffer_font_weight default should be 400.0 (FontWeight::NORMAL)" - ); - - let defs = &schema_value["$defs"]; - let font_weight_def = &defs["FontWeight"]; - - assert_eq!( - font_weight_def["minimum"].as_f64(), - Some(FontWeight::THIN.0 as f64), - "FontWeight should have minimum of 100.0" - ); - assert_eq!( - font_weight_def["maximum"].as_f64(), - Some(FontWeight::BLACK.0 as f64), - "FontWeight should have maximum of 900.0" - ); - assert_eq!( - font_weight_def["default"].as_f64(), - Some(FontWeight::NORMAL.0 as f64), - "FontWeight should have default of 400.0" - ); - } -} diff --git a/crates/settings/src/settings_content/workspace.rs b/crates/settings/src/settings_content/workspace.rs deleted file mode 100644 index b809a8fa85..0000000000 --- a/crates/settings/src/settings_content/workspace.rs +++ /dev/null @@ -1,713 +0,0 @@ -use std::num::NonZeroUsize; - -use collections::HashMap; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use settings_macros::{MergeFrom, with_fallible_options}; - -use crate::{ - CenteredPaddingSettings, DelayMs, DockPosition, DockSide, InactiveOpacity, - ScrollbarSettingsContent, ShowIndentGuides, serialize_optional_f32_with_two_decimal_places, -}; - -#[with_fallible_options] -#[derive(Clone, Debug, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct WorkspaceSettingsContent { - /// Active pane styling settings. - pub active_pane_modifiers: Option, - /// Layout mode for the bottom dock - /// - /// Default: contained - pub bottom_dock_layout: Option, - /// Direction to split horizontally. - /// - /// Default: "up" - pub pane_split_direction_horizontal: Option, - /// Direction to split vertically. - /// - /// Default: "left" - pub pane_split_direction_vertical: Option, - /// Centered layout related settings. - pub centered_layout: Option, - /// Whether or not to prompt the user to confirm before closing the application. - /// - /// Default: false - pub confirm_quit: Option, - /// Whether or not to show the call status icon in the status bar. - /// - /// Default: true - pub show_call_status_icon: Option, - /// When to automatically save edited buffers. - /// - /// Default: off - pub autosave: Option, - /// Controls previous session restoration in freshly launched Zed instance. - /// Values: none, last_workspace, last_session - /// Default: last_session - pub restore_on_startup: Option, - /// Whether to attempt to restore previous file's state when opening it again. - /// The state is stored per pane. - /// When disabled, defaults are applied instead of the state restoration. - /// - /// E.g. for editors, selections, folds and scroll positions are restored, if the same file is closed and, later, opened again in the same pane. - /// When disabled, a single selection in the very beginning of the file, zero scroll position and no folds state is used as a default. - /// - /// Default: true - pub restore_on_file_reopen: Option, - /// The size of the workspace split drop targets on the outer edges. - /// Given as a fraction that will be multiplied by the smaller dimension of the workspace. - /// - /// Default: `0.2` (20% of the smaller dimension of the workspace) - #[serde(serialize_with = "serialize_optional_f32_with_two_decimal_places")] - pub drop_target_size: Option, - /// Whether to close the window when using 'close active item' on a workspace with no tabs - /// - /// Default: auto ("on" on macOS, "off" otherwise) - pub when_closing_with_no_tabs: Option, - /// 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. - /// - /// Default: true - pub use_system_path_prompts: Option, - /// Whether to use the system provided prompts. - /// When set to false, Zed will use the built-in prompts. - /// Note that this setting has no effect on Linux, where Zed will always - /// use the built-in prompts. - /// - /// Default: true - pub use_system_prompts: Option, - /// Aliases for the command palette. When you type a key in this map, - /// it will be assumed to equal the value. - /// - /// Default: true - #[serde(default)] - pub command_aliases: HashMap, - /// Maximum open tabs in a pane. Will not close an unsaved - /// tab. Set to `None` for unlimited tabs. - /// - /// Default: none - pub max_tabs: Option, - /// What to do when the last window is closed - /// - /// Default: auto (nothing on macOS, "app quit" otherwise) - pub on_last_window_closed: Option, - /// Whether to resize all the panels in a dock when resizing the dock. - /// - /// Default: ["left"] - pub resize_all_panels_in_dock: Option>, - /// Whether to automatically close files that have been deleted on disk. - /// - /// Default: false - pub close_on_file_delete: Option, - /// Whether to allow windows to tab together based on the user’s tabbing preference (macOS only). - /// - /// Default: false - pub use_system_window_tabs: 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, - /// What draws window decorations/titlebar, the client application (Zed) or display server - /// Default: client - pub window_decorations: Option, -} - -#[with_fallible_options] -#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct ItemSettingsContent { - /// Whether to show the Git file status on a tab item. - /// - /// Default: false - pub git_status: Option, - /// Position of the close button in a tab. - /// - /// Default: right - pub close_position: Option, - /// Whether to show the file icon for a tab. - /// - /// Default: false - pub file_icons: Option, - /// What to do after closing the current tab. - /// - /// Default: history - pub activate_on_close: Option, - /// Which files containing diagnostic errors/warnings to mark in the tabs. - /// This setting can take the following three values: - /// - /// Default: off - pub show_diagnostics: Option, - /// Whether to always show the close button on tabs. - /// - /// Default: false - pub show_close_button: Option, -} - -#[with_fallible_options] -#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize, JsonSchema, MergeFrom)] -pub struct PreviewTabsSettingsContent { - /// Whether to show opened editors as preview tabs. - /// Preview tabs do not stay open, are reused until explicitly set to be kept open opened (via double-click or editing) and show file names in italic. - /// - /// Default: true - pub enabled: Option, - /// Whether to open tabs in preview mode when opened from the project panel with a single click. - /// - /// Default: true - pub enable_preview_from_project_panel: Option, - /// Whether to open tabs in preview mode when selected from the file finder. - /// - /// Default: false - pub enable_preview_from_file_finder: Option, - /// Whether to open tabs in preview mode when opened from a multibuffer. - /// - /// Default: true - pub enable_preview_from_multibuffer: Option, - /// Whether to open tabs in preview mode when code navigation is used to open a multibuffer. - /// - /// Default: false - pub enable_preview_multibuffer_from_code_navigation: Option, - /// Whether to open tabs in preview mode when code navigation is used to open a single file. - /// - /// Default: true - pub enable_preview_file_from_code_navigation: Option, - /// Whether to keep tabs in preview mode when code navigation is used to navigate away from them. - /// If `enable_preview_file_from_code_navigation` or `enable_preview_multibuffer_from_code_navigation` is also true, the new tab may replace the existing one. - /// - /// Default: false - pub enable_keep_preview_on_code_navigation: Option, -} - -#[derive( - Copy, - Clone, - Debug, - PartialEq, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "lowercase")] -pub enum ClosePosition { - Left, - #[default] - Right, -} - -#[derive( - Copy, - Clone, - Debug, - PartialEq, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "lowercase")] -pub enum ShowCloseButton { - Always, - #[default] - Hover, - Hidden, -} - -#[derive( - Copy, - Clone, - Debug, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum ShowDiagnostics { - #[default] - Off, - Errors, - All, -} - -#[derive( - Copy, - Clone, - Debug, - PartialEq, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum ActivateOnClose { - #[default] - History, - Neighbour, - LeftNeighbour, -} - -#[with_fallible_options] -#[derive(Copy, Clone, PartialEq, Debug, Default, Serialize, Deserialize, JsonSchema, MergeFrom)] -#[serde(rename_all = "snake_case")] -pub struct ActivePaneModifiers { - /// Size of the border surrounding the active pane. - /// When set to 0, the active pane doesn't have any border. - /// The border is drawn inset. - /// - /// Default: `0.0` - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub border_size: Option, - /// Opacity of inactive panels. - /// When set to 1.0, the inactive panes have the same opacity as the active one. - /// If set to 0, the inactive panes content will not be visible at all. - /// Values are clamped to the [0.0, 1.0] range. - /// - /// Default: `1.0` - #[schemars(range(min = 0.0, max = 1.0))] - pub inactive_opacity: Option, -} - -#[derive( - Copy, - Clone, - Debug, - Default, - Serialize, - Deserialize, - PartialEq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum BottomDockLayout { - /// Contained between the left and right docks - #[default] - Contained, - /// Takes up the full width of the window - Full, - /// Extends under the left dock while snapping to the right dock - LeftAligned, - /// Extends under the right dock while snapping to the left dock - RightAligned, -} - -#[derive( - Copy, - Clone, - Default, - Debug, - Serialize, - Deserialize, - PartialEq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum WindowDecorations { - /// Zed draws its own window decorations/titlebar (client-side decoration) - #[default] - Client, - /// Show system's window titlebar (server-side decoration; not supported by GNOME Wayland) - Server, -} - -#[derive( - Copy, - Clone, - PartialEq, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - Debug, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum CloseWindowWhenNoItems { - /// Match platform conventions by default, so "on" on macOS and "off" everywhere else - #[default] - PlatformDefault, - /// Close the window when there are no tabs - CloseWindow, - /// Leave the window open when there are no tabs - KeepWindowOpen, -} - -impl CloseWindowWhenNoItems { - pub fn should_close(&self) -> bool { - match self { - CloseWindowWhenNoItems::PlatformDefault => cfg!(target_os = "macos"), - CloseWindowWhenNoItems::CloseWindow => true, - CloseWindowWhenNoItems::KeepWindowOpen => false, - } - } -} - -#[derive( - Copy, - Clone, - PartialEq, - Eq, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - Debug, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum RestoreOnStartupBehavior { - /// Always start with an empty editor - None, - /// Restore the workspace that was closed last. - LastWorkspace, - /// Restore all workspaces that were open when quitting Zed. - #[default] - LastSession, -} - -#[with_fallible_options] -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq)] -pub struct TabBarSettingsContent { - /// Whether or not to show the tab bar in the editor. - /// - /// Default: true - pub show: Option, - /// Whether or not to show the navigation history buttons in the tab bar. - /// - /// Default: true - pub show_nav_history_buttons: Option, - /// Whether or not to show the tab bar buttons. - /// - /// Default: true - pub show_tab_bar_buttons: Option, -} - -#[with_fallible_options] -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug, PartialEq, Eq)] -pub struct StatusBarSettingsContent { - /// Whether to show the status bar. - /// - /// Default: true - #[serde(rename = "experimental.show")] - pub show: Option, - /// Whether to display the active language button in the status bar. - /// - /// Default: true - pub active_language_button: Option, - /// Whether to show the cursor position button in the status bar. - /// - /// Default: true - pub cursor_position_button: Option, - /// Whether to show active line endings button in the status bar. - /// - /// Default: false - pub line_endings_button: Option, -} - -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::EnumDiscriminants, -)] -#[strum_discriminants(derive(strum::VariantArray, strum::VariantNames, strum::FromRepr))] -#[serde(rename_all = "snake_case")] -pub enum AutosaveSetting { - /// Disable autosave. - Off, - /// Save after inactivity period of `milliseconds`. - AfterDelay { milliseconds: DelayMs }, - /// Autosave when focus changes. - OnFocusChange, - /// Autosave when the active window changes. - OnWindowChange, -} - -impl AutosaveSetting { - pub fn should_save_on_close(&self) -> bool { - matches!( - &self, - AutosaveSetting::OnFocusChange - | AutosaveSetting::OnWindowChange - | AutosaveSetting::AfterDelay { .. } - ) - } -} - -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum PaneSplitDirectionHorizontal { - Up, - Down, -} - -#[derive( - Copy, - Clone, - Debug, - Serialize, - Deserialize, - PartialEq, - Eq, - JsonSchema, - MergeFrom, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum PaneSplitDirectionVertical { - Left, - Right, -} - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Default)] -#[serde(rename_all = "snake_case")] -#[with_fallible_options] -pub struct CenteredLayoutSettings { - /// The relative width of the left padding of the central pane from the - /// workspace when the centered layout is used. - /// - /// Default: 0.2 - pub left_padding: Option, - // The relative width of the right padding of the central pane from the - // workspace when the centered layout is used. - /// - /// Default: 0.2 - pub right_padding: Option, -} - -#[derive( - Copy, - Clone, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Debug, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum OnLastWindowClosed { - /// Match platform conventions by default, so don't quit on macOS, and quit on other platforms - #[default] - PlatformDefault, - /// Quit the application the last window is closed - QuitApp, -} - -impl OnLastWindowClosed { - pub fn is_quit_app(&self) -> bool { - match self { - OnLastWindowClosed::PlatformDefault => false, - OnLastWindowClosed::QuitApp => true, - } - } -} - -#[with_fallible_options] -#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)] -pub struct ProjectPanelAutoOpenSettings { - /// Whether to automatically open newly created files in the editor. - /// - /// Default: true - pub on_create: Option, - /// Whether to automatically open files after pasting or duplicating them. - /// - /// Default: true - pub on_paste: Option, - /// Whether to automatically open files dropped from external sources. - /// - /// Default: true - pub on_drop: Option, -} - -#[with_fallible_options] -#[derive(Clone, PartialEq, Default, Serialize, Deserialize, JsonSchema, MergeFrom, Debug)] -pub struct ProjectPanelSettingsContent { - /// Whether to show the project panel button in the status bar. - /// - /// Default: true - pub button: Option, - /// Whether to hide gitignore files in the project panel. - /// - /// Default: false - pub hide_gitignore: Option, - /// Customize default width (in pixels) taken by project panel - /// - /// Default: 240 - #[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")] - pub default_width: Option, - /// The position of project panel - /// - /// Default: left - pub dock: Option, - /// Spacing between worktree entries in the project panel. - /// - /// Default: comfortable - pub entry_spacing: Option, - /// Whether to show file icons in the project panel. - /// - /// Default: true - pub file_icons: Option, - /// Whether to show folder icons or chevrons for directories in the project panel. - /// - /// Default: true - pub folder_icons: Option, - /// Whether to show the git status in the project panel. - /// - /// Default: true - pub git_status: Option, - /// Amount of indentation (in pixels) for nested items. - /// - /// Default: 20 - #[serde(serialize_with = "serialize_optional_f32_with_two_decimal_places")] - pub indent_size: Option, - /// Whether to reveal it in the project panel automatically, - /// when a corresponding project entry becomes active. - /// Gitignored entries are never auto revealed. - /// - /// Default: true - pub auto_reveal_entries: Option, - /// Whether to fold directories automatically - /// when directory has only one directory inside. - /// - /// Default: true - pub auto_fold_dirs: Option, - /// Whether the project panel should open on startup. - /// - /// Default: true - pub starts_open: Option, - /// Scrollbar-related settings - pub scrollbar: Option, - /// Which files containing diagnostic errors/warnings to mark in the project panel. - /// - /// Default: all - pub show_diagnostics: Option, - /// Settings related to indent guides in the project panel. - pub indent_guides: Option, - /// Whether to hide the root entry when only one folder is open in the window. - /// - /// Default: false - pub hide_root: Option, - /// Whether to hide the hidden entries in the project panel. - /// - /// Default: false - pub hide_hidden: Option, - /// Whether to stick parent directories at top of the project panel. - /// - /// Default: true - pub sticky_scroll: Option, - /// Whether to enable drag-and-drop operations in the project panel. - /// - /// Default: true - pub drag_and_drop: Option, - /// Settings for automatically opening files. - pub auto_open: Option, - /// How to order sibling entries in the project panel. - /// - /// Default: directories_first - pub sort_mode: Option, -} - -#[derive( - Copy, - Clone, - Debug, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum ProjectPanelEntrySpacing { - /// Comfortable spacing of entries. - #[default] - Comfortable, - /// The standard spacing of entries. - Standard, -} - -#[derive( - Copy, - Clone, - Debug, - Default, - Serialize, - Deserialize, - JsonSchema, - MergeFrom, - PartialEq, - Eq, - strum::VariantArray, - strum::VariantNames, -)] -#[serde(rename_all = "snake_case")] -pub enum ProjectPanelSortMode { - /// Show directories first, then files - #[default] - DirectoriesFirst, - /// Mix directories and files together - Mixed, - /// Show files first, then directories - FilesFirst, -} - -#[with_fallible_options] -#[derive( - Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, MergeFrom, PartialEq, Eq, Default, -)] -pub struct ProjectPanelIndentGuidesSettings { - pub show: Option, -} diff --git a/crates/settings/src/settings_file.rs b/crates/settings/src/settings_file.rs deleted file mode 100644 index df6aa8bbb4..0000000000 --- a/crates/settings/src/settings_file.rs +++ /dev/null @@ -1,135 +0,0 @@ -use crate::{settings_content::SettingsContent, settings_store::SettingsStore}; -use collections::HashSet; -use fs::{Fs, PathEventKind}; -use futures::{StreamExt, channel::mpsc}; -use gpui::{App, BackgroundExecutor, ReadGlobal}; -use std::{path::PathBuf, sync::Arc, time::Duration}; - -pub const EMPTY_THEME_NAME: &str = "empty-theme"; - -#[cfg(any(test, feature = "test-support"))] -pub fn test_settings() -> String { - let mut value = - crate::parse_json_with_comments::(crate::default_settings().as_ref()) - .unwrap(); - #[cfg(not(target_os = "windows"))] - util::merge_non_null_json_value_into( - serde_json::json!({ - "ui_font_family": "Courier", - "ui_font_features": {}, - "ui_font_size": 14, - "ui_font_fallback": [], - "buffer_font_family": "Courier", - "buffer_font_features": {}, - "buffer_font_size": 14, - "buffer_font_fallbacks": [], - "theme": EMPTY_THEME_NAME, - }), - &mut value, - ); - #[cfg(target_os = "windows")] - util::merge_non_null_json_value_into( - serde_json::json!({ - "ui_font_family": "Courier New", - "ui_font_features": {}, - "ui_font_size": 14, - "ui_font_fallback": [], - "buffer_font_family": "Courier New", - "buffer_font_features": {}, - "buffer_font_size": 14, - "buffer_font_fallbacks": [], - "theme": EMPTY_THEME_NAME, - }), - &mut value, - ); - value.as_object_mut().unwrap().remove("languages"); - serde_json::to_string(&value).unwrap() -} - -pub fn watch_config_file( - executor: &BackgroundExecutor, - fs: Arc, - path: PathBuf, -) -> mpsc::UnboundedReceiver { - let (tx, rx) = mpsc::unbounded(); - executor - .spawn(async move { - let (events, _) = fs.watch(&path, Duration::from_millis(100)).await; - futures::pin_mut!(events); - - let contents = fs.load(&path).await.unwrap_or_default(); - if tx.unbounded_send(contents).is_err() { - return; - } - - loop { - if events.next().await.is_none() { - break; - } - - if let Ok(contents) = fs.load(&path).await - && tx.unbounded_send(contents).is_err() - { - break; - } - } - }) - .detach(); - rx -} - -pub fn watch_config_dir( - executor: &BackgroundExecutor, - fs: Arc, - dir_path: PathBuf, - config_paths: HashSet, -) -> mpsc::UnboundedReceiver { - let (tx, rx) = mpsc::unbounded(); - executor - .spawn(async move { - for file_path in &config_paths { - if fs.metadata(file_path).await.is_ok_and(|v| v.is_some()) - && let Ok(contents) = fs.load(file_path).await - && tx.unbounded_send(contents).is_err() - { - return; - } - } - - let (events, _) = fs.watch(&dir_path, Duration::from_millis(100)).await; - futures::pin_mut!(events); - - while let Some(event_batch) = events.next().await { - for event in event_batch { - if config_paths.contains(&event.path) { - match event.kind { - Some(PathEventKind::Removed) => { - if tx.unbounded_send(String::new()).is_err() { - return; - } - } - Some(PathEventKind::Created) | Some(PathEventKind::Changed) => { - if let Ok(contents) = fs.load(&event.path).await - && tx.unbounded_send(contents).is_err() - { - return; - } - } - _ => {} - } - } - } - } - }) - .detach(); - - rx -} - -pub fn update_settings_file( - fs: Arc, - cx: &App, - update: impl 'static + Send + FnOnce(&mut SettingsContent, &App), -) { - SettingsStore::global(cx).update_settings_file(fs, update); -} diff --git a/crates/settings/src/settings_store.rs b/crates/settings/src/settings_store.rs deleted file mode 100644 index 72e2d3ef09..0000000000 --- a/crates/settings/src/settings_store.rs +++ /dev/null @@ -1,2296 +0,0 @@ -use anyhow::{Context as _, Result}; -use collections::{BTreeMap, HashMap, btree_map, hash_map}; -use ec4rs::{ConfigParser, PropertiesSource, Section}; -use fs::Fs; -use futures::{ - FutureExt, StreamExt, - channel::{mpsc, oneshot}, - future::LocalBoxFuture, -}; -use gpui::{App, AsyncApp, BorrowAppContext, Global, SharedString, Task, UpdateGlobal}; - -use paths::{EDITORCONFIG_NAME, local_settings_file_relative_path, task_file_name}; -use schemars::{JsonSchema, json_schema}; -use serde_json::Value; -use smallvec::SmallVec; -use std::{ - any::{Any, TypeId, type_name}, - fmt::Debug, - ops::Range, - path::PathBuf, - rc::Rc, - str::{self, FromStr}, - sync::Arc, -}; -use util::{ - ResultExt as _, - rel_path::RelPath, - schemars::{AllowTrailingCommas, DefaultDenyUnknownFields, replace_subschema}, -}; - -pub type EditorconfigProperties = ec4rs::Properties; - -use crate::{ - ActiveSettingsProfileName, FontFamilyName, IconThemeName, LanguageSettingsContent, - LanguageToSettingsMap, ThemeName, VsCodeSettings, WorktreeId, fallible_options, - merge_from::MergeFrom, - settings_content::{ - ExtensionsSettingsContent, ProjectSettingsContent, SettingsContent, UserSettingsContent, - }, -}; - -use settings_json::{infer_json_indent_size, parse_json_with_comments, update_value_in_json_text}; - -pub trait SettingsKey: 'static + Send + Sync { - /// The name of a key within the JSON file from which this setting should - /// be deserialized. If this is `None`, then the setting will be deserialized - /// from the root object. - const KEY: Option<&'static str>; - - const FALLBACK_KEY: Option<&'static str> = None; -} - -/// A value that can be defined as a user setting. -/// -/// Settings can be loaded from a combination of multiple JSON files. -pub trait Settings: 'static + Send + Sync + Sized { - /// The name of the keys in the [`FileContent`](Self::FileContent) that should - /// always be written to a settings file, even if their value matches the default - /// value. - /// - /// This is useful for tagged [`FileContent`](Self::FileContent)s where the tag - /// is a "version" field that should always be persisted, even if the current - /// user settings match the current version of the settings. - const PRESERVED_KEYS: Option<&'static [&'static str]> = None; - - /// Read the value from default.json. - /// - /// This function *should* panic if default values are missing, - /// and you should add a default to default.json for documentation. - fn from_settings(content: &SettingsContent) -> Self; - - #[track_caller] - fn register(cx: &mut App) - where - Self: Sized, - { - SettingsStore::update_global(cx, |store, _| { - store.register_setting::(); - }); - } - - #[track_caller] - fn get<'a>(path: Option, cx: &'a App) -> &'a Self - where - Self: Sized, - { - cx.global::().get(path) - } - - #[track_caller] - fn get_global(cx: &App) -> &Self - where - Self: Sized, - { - cx.global::().get(None) - } - - #[track_caller] - fn try_get(cx: &App) -> Option<&Self> - where - Self: Sized, - { - if cx.has_global::() { - cx.global::().try_get(None) - } else { - None - } - } - - #[track_caller] - fn try_read_global(cx: &AsyncApp, f: impl FnOnce(&Self) -> R) -> Option - where - Self: Sized, - { - cx.try_read_global(|s: &SettingsStore, _| f(s.get(None))) - } - - #[track_caller] - fn override_global(settings: Self, cx: &mut App) - where - Self: Sized, - { - cx.global_mut::().override_global(settings) - } -} - -pub struct RegisteredSetting { - pub settings_value: fn() -> Box, - pub from_settings: fn(&SettingsContent) -> Box, - pub id: fn() -> TypeId, -} - -inventory::collect!(RegisteredSetting); - -#[derive(Clone, Copy, Debug)] -pub struct SettingsLocation<'a> { - pub worktree_id: WorktreeId, - pub path: &'a RelPath, -} - -pub struct SettingsStore { - setting_values: HashMap>, - default_settings: Rc, - user_settings: Option, - global_settings: Option>, - - extension_settings: Option>, - server_settings: Option>, - - merged_settings: Rc, - - local_settings: BTreeMap<(WorktreeId, Arc), SettingsContent>, - raw_editorconfig_settings: BTreeMap<(WorktreeId, Arc), (String, Option)>, - - _setting_file_updates: Task<()>, - setting_file_updates_tx: - mpsc::UnboundedSender LocalBoxFuture<'static, Result<()>>>>, - file_errors: BTreeMap, -} - -#[derive(Clone, PartialEq, Eq, Debug)] -pub enum SettingsFile { - Default, - Global, - User, - Server, - /// Represents project settings in ssh projects as well as local projects - Project((WorktreeId, Arc)), -} - -impl PartialOrd for SettingsFile { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -/// Sorted in order of precedence -impl Ord for SettingsFile { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - use SettingsFile::*; - use std::cmp::Ordering; - match (self, other) { - (User, User) => Ordering::Equal, - (Server, Server) => Ordering::Equal, - (Default, Default) => Ordering::Equal, - (Project((id1, rel_path1)), Project((id2, rel_path2))) => id1 - .cmp(id2) - .then_with(|| rel_path1.cmp(rel_path2).reverse()), - (Project(_), _) => Ordering::Less, - (_, Project(_)) => Ordering::Greater, - (Server, _) => Ordering::Less, - (_, Server) => Ordering::Greater, - (User, _) => Ordering::Less, - (_, User) => Ordering::Greater, - (Global, _) => Ordering::Less, - (_, Global) => Ordering::Greater, - } - } -} - -#[derive(Clone)] -pub struct Editorconfig { - pub is_root: bool, - pub sections: SmallVec<[Section; 5]>, -} - -impl FromStr for Editorconfig { - type Err = anyhow::Error; - - fn from_str(contents: &str) -> Result { - let parser = ConfigParser::new_buffered(contents.as_bytes()) - .context("creating editorconfig parser")?; - let is_root = parser.is_root; - let sections = parser - .collect::, _>>() - .context("parsing editorconfig sections")?; - Ok(Self { is_root, sections }) - } -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] -pub enum LocalSettingsKind { - Settings, - Tasks, - Editorconfig, - Debug, -} - -impl Global for SettingsStore {} - -#[doc(hidden)] -#[derive(Debug)] -pub struct SettingValue { - #[doc(hidden)] - pub global_value: Option, - #[doc(hidden)] - pub local_values: Vec<(WorktreeId, Arc, T)>, -} - -#[doc(hidden)] -pub trait AnySettingValue: 'static + Send + Sync { - fn setting_type_name(&self) -> &'static str; - - fn from_settings(&self, s: &SettingsContent) -> Box; - - fn value_for_path(&self, path: Option) -> &dyn Any; - fn all_local_values(&self) -> Vec<(WorktreeId, Arc, &dyn Any)>; - fn set_global_value(&mut self, value: Box); - fn set_local_value(&mut self, root_id: WorktreeId, path: Arc, value: Box); -} - -/// Parameters that are used when generating some JSON schemas at runtime. -pub struct SettingsJsonSchemaParams<'a> { - pub language_names: &'a [String], - pub font_names: &'a [String], - pub theme_names: &'a [SharedString], - pub icon_theme_names: &'a [SharedString], -} - -impl SettingsStore { - pub fn new(cx: &App, default_settings: &str) -> Self { - let (setting_file_updates_tx, mut setting_file_updates_rx) = mpsc::unbounded(); - let default_settings: Rc = - parse_json_with_comments(default_settings).unwrap(); - let mut this = Self { - setting_values: Default::default(), - default_settings: default_settings.clone(), - global_settings: None, - server_settings: None, - user_settings: None, - extension_settings: None, - - merged_settings: default_settings, - local_settings: BTreeMap::default(), - raw_editorconfig_settings: BTreeMap::default(), - setting_file_updates_tx, - _setting_file_updates: cx.spawn(async move |cx| { - while let Some(setting_file_update) = setting_file_updates_rx.next().await { - (setting_file_update)(cx.clone()).await.log_err(); - } - }), - file_errors: BTreeMap::default(), - }; - - this.load_settings_types(); - - this - } - - pub fn observe_active_settings_profile_name(cx: &mut App) -> gpui::Subscription { - cx.observe_global::(|cx| { - Self::update_global(cx, |store, cx| { - store.recompute_values(None, cx); - }); - }) - } - - pub fn update(cx: &mut C, f: impl FnOnce(&mut Self, &mut C) -> R) -> R - where - C: BorrowAppContext, - { - cx.update_global(f) - } - - /// Add a new type of setting to the store. - pub fn register_setting(&mut self) { - self.register_setting_internal(&RegisteredSetting { - settings_value: || { - Box::new(SettingValue:: { - global_value: None, - local_values: Vec::new(), - }) - }, - from_settings: |content| Box::new(T::from_settings(content)), - id: || TypeId::of::(), - }); - } - - fn load_settings_types(&mut self) { - for registered_setting in inventory::iter::() { - self.register_setting_internal(registered_setting); - } - } - - fn register_setting_internal(&mut self, registered_setting: &RegisteredSetting) { - let entry = self.setting_values.entry((registered_setting.id)()); - - if matches!(entry, hash_map::Entry::Occupied(_)) { - return; - } - - let setting_value = entry.or_insert((registered_setting.settings_value)()); - let value = (registered_setting.from_settings)(&self.merged_settings); - setting_value.set_global_value(value); - } - - /// Get the value of a setting. - /// - /// Panics if the given setting type has not been registered, or if there is no - /// value for this setting. - pub fn get(&self, path: Option) -> &T { - self.setting_values - .get(&TypeId::of::()) - .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::())) - .value_for_path(path) - .downcast_ref::() - .expect("no default value for setting type") - } - - /// Get the value of a setting. - /// - /// Does not panic - pub fn try_get(&self, path: Option) -> Option<&T> { - self.setting_values - .get(&TypeId::of::()) - .map(|value| value.value_for_path(path)) - .and_then(|value| value.downcast_ref::()) - } - - /// Get all values from project specific settings - pub fn get_all_locals(&self) -> Vec<(WorktreeId, Arc, &T)> { - self.setting_values - .get(&TypeId::of::()) - .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::())) - .all_local_values() - .into_iter() - .map(|(id, path, any)| { - ( - id, - path, - any.downcast_ref::() - .expect("wrong value type for setting"), - ) - }) - .collect() - } - - /// Override the global value for a setting. - /// - /// The given value will be overwritten if the user settings file changes. - pub fn override_global(&mut self, value: T) { - self.setting_values - .get_mut(&TypeId::of::()) - .unwrap_or_else(|| panic!("unregistered setting type {}", type_name::())) - .set_global_value(Box::new(value)) - } - - /// Get the user's settings content. - /// - /// For user-facing functionality use the typed setting interface. - /// (e.g. ProjectSettings::get_global(cx)) - pub fn raw_user_settings(&self) -> Option<&UserSettingsContent> { - self.user_settings.as_ref() - } - - /// Get the default settings content as a raw JSON value. - pub fn raw_default_settings(&self) -> &SettingsContent { - &self.default_settings - } - - /// Get the configured settings profile names. - pub fn configured_settings_profiles(&self) -> impl Iterator { - self.user_settings - .iter() - .flat_map(|settings| settings.profiles.keys().map(|k| k.as_str())) - } - - #[cfg(any(test, feature = "test-support"))] - pub fn test(cx: &mut App) -> Self { - Self::new(cx, &crate::test_settings()) - } - - /// Updates the value of a setting in the user's global configuration. - /// - /// This is only for tests. Normally, settings are only loaded from - /// JSON files. - #[cfg(any(test, feature = "test-support"))] - pub fn update_user_settings( - &mut self, - cx: &mut App, - update: impl FnOnce(&mut SettingsContent), - ) { - let mut content = self.user_settings.clone().unwrap_or_default().content; - update(&mut content); - let new_text = serde_json::to_string(&UserSettingsContent { - content, - ..Default::default() - }) - .unwrap(); - _ = self.set_user_settings(&new_text, cx); - } - - pub async fn load_settings(fs: &Arc) -> Result { - match fs.load(paths::settings_file()).await { - result @ Ok(_) => result, - Err(err) => { - if let Some(e) = err.downcast_ref::() - && e.kind() == std::io::ErrorKind::NotFound - { - return Ok(crate::initial_user_settings_content().to_string()); - } - Err(err) - } - } - } - - fn update_settings_file_inner( - &self, - fs: Arc, - update: impl 'static + Send + FnOnce(String, AsyncApp) -> Result, - ) -> oneshot::Receiver> { - let (tx, rx) = oneshot::channel::>(); - self.setting_file_updates_tx - .unbounded_send(Box::new(move |cx: AsyncApp| { - async move { - let res = async move { - let old_text = Self::load_settings(&fs).await?; - let new_text = update(old_text, cx)?; - let settings_path = paths::settings_file().as_path(); - if fs.is_file(settings_path).await { - let resolved_path = - fs.canonicalize(settings_path).await.with_context(|| { - format!( - "Failed to canonicalize settings path {:?}", - settings_path - ) - })?; - - fs.atomic_write(resolved_path.clone(), new_text) - .await - .with_context(|| { - format!("Failed to write settings to file {:?}", resolved_path) - })?; - } else { - fs.atomic_write(settings_path.to_path_buf(), new_text) - .await - .with_context(|| { - format!("Failed to write settings to file {:?}", settings_path) - })?; - } - anyhow::Ok(()) - } - .await; - - let new_res = match &res { - Ok(_) => anyhow::Ok(()), - Err(e) => Err(anyhow::anyhow!("Failed to write settings to file {:?}", e)), - }; - - _ = tx.send(new_res); - res - } - .boxed_local() - })) - .map_err(|err| anyhow::format_err!("Failed to update settings file: {}", err)) - .log_with_level(log::Level::Warn); - return rx; - } - - pub fn update_settings_file( - &self, - fs: Arc, - update: impl 'static + Send + FnOnce(&mut SettingsContent, &App), - ) { - _ = self.update_settings_file_inner(fs, move |old_text: String, cx: AsyncApp| { - cx.read_global(|store: &SettingsStore, cx| { - store.new_text_for_update(old_text, |content| update(content, cx)) - }) - }); - } - - pub fn import_vscode_settings( - &self, - fs: Arc, - vscode_settings: VsCodeSettings, - ) -> oneshot::Receiver> { - self.update_settings_file_inner(fs, move |old_text: String, cx: AsyncApp| { - cx.read_global(|store: &SettingsStore, _cx| { - store.get_vscode_edits(old_text, &vscode_settings) - }) - }) - } - - pub fn get_all_files(&self) -> Vec { - let mut files = Vec::from_iter( - self.local_settings - .keys() - // rev because these are sorted by path, so highest precedence is last - .rev() - .cloned() - .map(SettingsFile::Project), - ); - - if self.server_settings.is_some() { - files.push(SettingsFile::Server); - } - // ignoring profiles - // ignoring os profiles - // ignoring release channel profiles - // ignoring global - // ignoring extension - - if self.user_settings.is_some() { - files.push(SettingsFile::User); - } - files.push(SettingsFile::Default); - files - } - - pub fn get_content_for_file(&self, file: SettingsFile) -> Option<&SettingsContent> { - match file { - SettingsFile::User => self - .user_settings - .as_ref() - .map(|settings| settings.content.as_ref()), - SettingsFile::Default => Some(self.default_settings.as_ref()), - SettingsFile::Server => self.server_settings.as_deref(), - SettingsFile::Project(ref key) => self.local_settings.get(key), - SettingsFile::Global => self.global_settings.as_deref(), - } - } - - pub fn get_overrides_for_field( - &self, - target_file: SettingsFile, - get: fn(&SettingsContent) -> &Option, - ) -> Vec { - let all_files = self.get_all_files(); - let mut found_file = false; - let mut overrides = Vec::new(); - - for file in all_files.into_iter().rev() { - if !found_file { - found_file = file == target_file; - continue; - } - - if let SettingsFile::Project((wt_id, ref path)) = file - && let SettingsFile::Project((target_wt_id, ref target_path)) = target_file - && (wt_id != target_wt_id || !target_path.starts_with(path)) - { - // if requesting value from a local file, don't return values from local files in different worktrees - continue; - } - - let Some(content) = self.get_content_for_file(file.clone()) else { - continue; - }; - if get(content).is_some() { - overrides.push(file); - } - } - - overrides - } - - /// Checks the given file, and files that the passed file overrides for the given field. - /// Returns the first file found that contains the value. - /// The value will only be None if no file contains the value. - /// I.e. if no file contains the value, returns `(File::Default, None)` - pub fn get_value_from_file<'a, T: 'a>( - &'a self, - target_file: SettingsFile, - pick: fn(&'a SettingsContent) -> Option, - ) -> (SettingsFile, Option) { - self.get_value_from_file_inner(target_file, pick, true) - } - - /// Same as `Self::get_value_from_file` except that it does not include the current file. - /// Therefore it returns the value that was potentially overloaded by the target file. - pub fn get_value_up_to_file<'a, T: 'a>( - &'a self, - target_file: SettingsFile, - pick: fn(&'a SettingsContent) -> Option, - ) -> (SettingsFile, Option) { - self.get_value_from_file_inner(target_file, pick, false) - } - - fn get_value_from_file_inner<'a, T: 'a>( - &'a self, - target_file: SettingsFile, - pick: fn(&'a SettingsContent) -> Option, - include_target_file: bool, - ) -> (SettingsFile, Option) { - // todo(settings_ui): Add a metadata field for overriding the "overrides" tag, for contextually different settings - // e.g. disable AI isn't overridden, or a vec that gets extended instead or some such - - // todo(settings_ui) cache all files - let all_files = self.get_all_files(); - let mut found_file = false; - - for file in all_files.into_iter() { - if !found_file && file != SettingsFile::Default { - if file != target_file { - continue; - } - found_file = true; - if !include_target_file { - continue; - } - } - - if let SettingsFile::Project((worktree_id, ref path)) = file - && let SettingsFile::Project((target_worktree_id, ref target_path)) = target_file - && (worktree_id != target_worktree_id || !target_path.starts_with(&path)) - { - // if requesting value from a local file, don't return values from local files in different worktrees - continue; - } - - let Some(content) = self.get_content_for_file(file.clone()) else { - continue; - }; - if let Some(value) = pick(content) { - return (file, Some(value)); - } - } - - (SettingsFile::Default, None) - } - - #[inline(always)] - fn parse_and_migrate_zed_settings( - &mut self, - user_settings_content: &str, - file: SettingsFile, - ) -> (Option, SettingsParseResult) { - let mut migration_status = MigrationStatus::NotNeeded; - let (settings, parse_status) = if user_settings_content.is_empty() { - fallible_options::parse_json("{}") - } else { - let migration_res = migrator::migrate_settings(user_settings_content); - migration_status = match &migration_res { - Ok(Some(_)) => MigrationStatus::Succeeded, - Ok(None) => MigrationStatus::NotNeeded, - Err(err) => MigrationStatus::Failed { - error: err.to_string(), - }, - }; - let content = match &migration_res { - Ok(Some(content)) => content, - Ok(None) => user_settings_content, - Err(_) => user_settings_content, - }; - fallible_options::parse_json(content) - }; - - let result = SettingsParseResult { - parse_status, - migration_status, - }; - self.file_errors.insert(file, result.clone()); - return (settings, result); - } - - pub fn error_for_file(&self, file: SettingsFile) -> Option { - self.file_errors - .get(&file) - .filter(|parse_result| parse_result.requires_user_action()) - .cloned() - } -} - -impl SettingsStore { - /// Updates the value of a setting in a JSON file, returning the new text - /// for that JSON file. - pub fn new_text_for_update( - &self, - old_text: String, - update: impl FnOnce(&mut SettingsContent), - ) -> String { - let edits = self.edits_for_update(&old_text, update); - let mut new_text = old_text; - for (range, replacement) in edits.into_iter() { - new_text.replace_range(range, &replacement); - } - new_text - } - - pub fn get_vscode_edits(&self, old_text: String, vscode: &VsCodeSettings) -> String { - self.new_text_for_update(old_text, |content| { - content.merge_from(&vscode.settings_content()) - }) - } - - /// Updates the value of a setting in a JSON file, returning a list - /// of edits to apply to the JSON file. - pub fn edits_for_update( - &self, - text: &str, - update: impl FnOnce(&mut SettingsContent), - ) -> Vec<(Range, String)> { - let old_content: UserSettingsContent = - parse_json_with_comments(text).log_err().unwrap_or_default(); - let mut new_content = old_content.clone(); - update(&mut new_content.content); - - let old_value = serde_json::to_value(&old_content).unwrap(); - let new_value = serde_json::to_value(new_content).unwrap(); - - let mut key_path = Vec::new(); - let mut edits = Vec::new(); - let tab_size = infer_json_indent_size(&text); - let mut text = text.to_string(); - update_value_in_json_text( - &mut text, - &mut key_path, - tab_size, - &old_value, - &new_value, - &mut edits, - ); - edits - } - - /// Sets the default settings via a JSON string. - /// - /// The string should contain a JSON object with a default value for every setting. - pub fn set_default_settings( - &mut self, - default_settings_content: &str, - cx: &mut App, - ) -> Result<()> { - self.default_settings = parse_json_with_comments(default_settings_content)?; - self.recompute_values(None, cx); - Ok(()) - } - - /// Sets the user settings via a JSON string. - #[must_use] - pub fn set_user_settings( - &mut self, - user_settings_content: &str, - cx: &mut App, - ) -> SettingsParseResult { - let (settings, parse_result) = self.parse_and_migrate_zed_settings::( - user_settings_content, - SettingsFile::User, - ); - - if let Some(settings) = settings { - self.user_settings = Some(settings); - self.recompute_values(None, cx); - } - return parse_result; - } - - /// Sets the global settings via a JSON string. - #[must_use] - pub fn set_global_settings( - &mut self, - global_settings_content: &str, - cx: &mut App, - ) -> SettingsParseResult { - let (settings, parse_result) = self.parse_and_migrate_zed_settings::( - global_settings_content, - SettingsFile::Global, - ); - - if let Some(settings) = settings { - self.global_settings = Some(Box::new(settings)); - self.recompute_values(None, cx); - } - return parse_result; - } - - pub fn set_server_settings( - &mut self, - server_settings_content: &str, - cx: &mut App, - ) -> Result<()> { - let settings: Option = if server_settings_content.is_empty() { - None - } else { - parse_json_with_comments(server_settings_content)? - }; - - // Rewrite the server settings into a content type - self.server_settings = settings.map(|settings| Box::new(settings)); - - self.recompute_values(None, cx); - Ok(()) - } - - /// Add or remove a set of local settings via a JSON string. - pub fn set_local_settings( - &mut self, - root_id: WorktreeId, - directory_path: Arc, - kind: LocalSettingsKind, - settings_content: Option<&str>, - cx: &mut App, - ) -> std::result::Result<(), InvalidSettingsError> { - let mut zed_settings_changed = false; - match ( - kind, - settings_content - .map(|content| content.trim()) - .filter(|content| !content.is_empty()), - ) { - (LocalSettingsKind::Tasks, _) => { - return Err(InvalidSettingsError::Tasks { - message: "Attempted to submit tasks into the settings store".to_string(), - path: directory_path - .join(RelPath::unix(task_file_name()).unwrap()) - .as_std_path() - .to_path_buf(), - }); - } - (LocalSettingsKind::Debug, _) => { - return Err(InvalidSettingsError::Debug { - message: "Attempted to submit debugger config into the settings store" - .to_string(), - path: directory_path - .join(RelPath::unix(task_file_name()).unwrap()) - .as_std_path() - .to_path_buf(), - }); - } - (LocalSettingsKind::Settings, None) => { - zed_settings_changed = self - .local_settings - .remove(&(root_id, directory_path.clone())) - .is_some(); - self.file_errors - .remove(&SettingsFile::Project((root_id, directory_path.clone()))); - } - (LocalSettingsKind::Editorconfig, None) => { - self.raw_editorconfig_settings - .remove(&(root_id, directory_path.clone())); - } - (LocalSettingsKind::Settings, Some(settings_contents)) => { - let (new_settings, parse_result) = self - .parse_and_migrate_zed_settings::( - settings_contents, - SettingsFile::Project((root_id, directory_path.clone())), - ); - match parse_result.parse_status { - ParseStatus::Success => Ok(()), - ParseStatus::Failed { error } => Err(InvalidSettingsError::LocalSettings { - path: directory_path.join(local_settings_file_relative_path()), - message: error, - }), - }?; - if let Some(new_settings) = new_settings { - match self.local_settings.entry((root_id, directory_path.clone())) { - btree_map::Entry::Vacant(v) => { - v.insert(SettingsContent { - project: new_settings, - ..Default::default() - }); - zed_settings_changed = true; - } - btree_map::Entry::Occupied(mut o) => { - if &o.get().project != &new_settings { - o.insert(SettingsContent { - project: new_settings, - ..Default::default() - }); - zed_settings_changed = true; - } - } - } - } - } - (LocalSettingsKind::Editorconfig, Some(editorconfig_contents)) => { - match self - .raw_editorconfig_settings - .entry((root_id, directory_path.clone())) - { - btree_map::Entry::Vacant(v) => match editorconfig_contents.parse() { - Ok(new_contents) => { - v.insert((editorconfig_contents.to_owned(), Some(new_contents))); - } - Err(e) => { - v.insert((editorconfig_contents.to_owned(), None)); - return Err(InvalidSettingsError::Editorconfig { - message: e.to_string(), - path: directory_path - .join(RelPath::unix(EDITORCONFIG_NAME).unwrap()), - }); - } - }, - btree_map::Entry::Occupied(mut o) => { - if o.get().0 != editorconfig_contents { - match editorconfig_contents.parse() { - Ok(new_contents) => { - o.insert(( - editorconfig_contents.to_owned(), - Some(new_contents), - )); - } - Err(e) => { - o.insert((editorconfig_contents.to_owned(), None)); - return Err(InvalidSettingsError::Editorconfig { - message: e.to_string(), - path: directory_path - .join(RelPath::unix(EDITORCONFIG_NAME).unwrap()), - }); - } - } - } - } - } - } - }; - - if zed_settings_changed { - self.recompute_values(Some((root_id, &directory_path)), cx); - } - Ok(()) - } - - pub fn set_extension_settings( - &mut self, - content: ExtensionsSettingsContent, - cx: &mut App, - ) -> Result<()> { - self.extension_settings = Some(Box::new(SettingsContent { - project: ProjectSettingsContent { - all_languages: content.all_languages, - ..Default::default() - }, - ..Default::default() - })); - self.recompute_values(None, cx); - Ok(()) - } - - /// Add or remove a set of local settings via a JSON string. - pub fn clear_local_settings(&mut self, root_id: WorktreeId, cx: &mut App) -> Result<()> { - self.local_settings - .retain(|(worktree_id, _), _| worktree_id != &root_id); - self.recompute_values(Some((root_id, RelPath::empty())), cx); - Ok(()) - } - - pub fn local_settings( - &self, - root_id: WorktreeId, - ) -> impl '_ + Iterator, &ProjectSettingsContent)> { - self.local_settings - .range( - (root_id, RelPath::empty().into()) - ..( - WorktreeId::from_usize(root_id.to_usize() + 1), - RelPath::empty().into(), - ), - ) - .map(|((_, path), content)| (path.clone(), &content.project)) - } - - pub fn local_editorconfig_settings( - &self, - root_id: WorktreeId, - ) -> impl '_ + Iterator, String, Option)> { - self.raw_editorconfig_settings - .range( - (root_id, RelPath::empty().into()) - ..( - WorktreeId::from_usize(root_id.to_usize() + 1), - RelPath::empty().into(), - ), - ) - .map(|((_, path), (content, parsed_content))| { - (path.clone(), content.clone(), parsed_content.clone()) - }) - } - - pub fn json_schema(&self, params: &SettingsJsonSchemaParams) -> Value { - let mut generator = schemars::generate::SchemaSettings::draft2019_09() - .with_transform(DefaultDenyUnknownFields) - .with_transform(AllowTrailingCommas) - .into_generator(); - - UserSettingsContent::json_schema(&mut generator); - - let language_settings_content_ref = generator - .subschema_for::() - .to_value(); - - replace_subschema::(&mut generator, || { - json_schema!({ - "type": "object", - "properties": params - .language_names - .iter() - .map(|name| { - ( - name.clone(), - language_settings_content_ref.clone(), - ) - }) - .collect::>(), - "errorMessage": "No language with this name is installed." - }) - }); - - replace_subschema::(&mut generator, || { - json_schema!({ - "type": "string", - "enum": params.font_names, - }) - }); - - replace_subschema::(&mut generator, || { - json_schema!({ - "type": "string", - "enum": params.theme_names, - }) - }); - - replace_subschema::(&mut generator, || { - json_schema!({ - "type": "string", - "enum": params.icon_theme_names, - }) - }); - - generator - .root_schema_for::() - .to_value() - } - - fn recompute_values( - &mut self, - changed_local_path: Option<(WorktreeId, &RelPath)>, - cx: &mut App, - ) { - // Reload the global and local values for every setting. - let mut project_settings_stack = Vec::::new(); - let mut paths_stack = Vec::>::new(); - - if changed_local_path.is_none() { - let mut merged = self.default_settings.as_ref().clone(); - merged.merge_from_option(self.extension_settings.as_deref()); - merged.merge_from_option(self.global_settings.as_deref()); - if let Some(user_settings) = self.user_settings.as_ref() { - merged.merge_from(&user_settings.content); - merged.merge_from_option(user_settings.for_release_channel()); - merged.merge_from_option(user_settings.for_os()); - merged.merge_from_option(user_settings.for_profile(cx)); - } - merged.merge_from_option(self.server_settings.as_deref()); - self.merged_settings = Rc::new(merged); - - for setting_value in self.setting_values.values_mut() { - let value = setting_value.from_settings(&self.merged_settings); - setting_value.set_global_value(value); - } - } - - for ((root_id, directory_path), local_settings) in &self.local_settings { - // Build a stack of all of the local values for that setting. - while let Some(prev_entry) = paths_stack.last() { - if let Some((prev_root_id, prev_path)) = prev_entry - && (root_id != prev_root_id || !directory_path.starts_with(prev_path)) - { - paths_stack.pop(); - project_settings_stack.pop(); - continue; - } - break; - } - - paths_stack.push(Some((*root_id, directory_path.as_ref()))); - let mut merged_local_settings = if let Some(deepest) = project_settings_stack.last() { - (*deepest).clone() - } else { - self.merged_settings.as_ref().clone() - }; - merged_local_settings.merge_from(local_settings); - - project_settings_stack.push(merged_local_settings); - - // If a local settings file changed, then avoid recomputing local - // settings for any path outside of that directory. - if changed_local_path.is_some_and(|(changed_root_id, changed_local_path)| { - *root_id != changed_root_id || !directory_path.starts_with(changed_local_path) - }) { - continue; - } - - for setting_value in self.setting_values.values_mut() { - let value = setting_value.from_settings(&project_settings_stack.last().unwrap()); - setting_value.set_local_value(*root_id, directory_path.clone(), value); - } - } - } - - pub fn editorconfig_properties( - &self, - for_worktree: WorktreeId, - for_path: &RelPath, - ) -> Option { - let mut properties = EditorconfigProperties::new(); - - for (directory_with_config, _, parsed_editorconfig) in - self.local_editorconfig_settings(for_worktree) - { - if !for_path.starts_with(&directory_with_config) { - properties.use_fallbacks(); - return Some(properties); - } - let parsed_editorconfig = parsed_editorconfig?; - if parsed_editorconfig.is_root { - properties = EditorconfigProperties::new(); - } - for section in parsed_editorconfig.sections { - section - .apply_to(&mut properties, for_path.as_std_path()) - .log_err()?; - } - } - - properties.use_fallbacks(); - Some(properties) - } -} - -/// The result of parsing settings, including any migration attempts -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct SettingsParseResult { - /// The result of parsing the settings file (possibly after migration) - pub parse_status: ParseStatus, - /// The result of attempting to migrate the settings file - pub migration_status: MigrationStatus, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ParseStatus { - /// Settings were parsed successfully - Success, - /// Settings failed to parse - Failed { error: String }, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum MigrationStatus { - /// No migration was needed - settings are up to date - NotNeeded, - /// Settings were automatically migrated in memory, but the file needs to be updated - Succeeded, - /// Migration was attempted but failed. Original settings were parsed instead. - Failed { error: String }, -} - -impl Default for SettingsParseResult { - fn default() -> Self { - Self { - parse_status: ParseStatus::Success, - migration_status: MigrationStatus::NotNeeded, - } - } -} - -impl SettingsParseResult { - pub fn unwrap(self) -> bool { - self.result().unwrap() - } - - pub fn expect(self, message: &str) -> bool { - self.result().expect(message) - } - - /// Formats the ParseResult as a Result type. This is a lossy conversion - pub fn result(self) -> Result { - let migration_result = match self.migration_status { - MigrationStatus::NotNeeded => Ok(false), - MigrationStatus::Succeeded => Ok(true), - MigrationStatus::Failed { error } => { - Err(anyhow::format_err!(error)).context("Failed to migrate settings") - } - }; - - let parse_result = match self.parse_status { - ParseStatus::Success => Ok(()), - ParseStatus::Failed { error } => { - Err(anyhow::format_err!(error)).context("Failed to parse settings") - } - }; - - match (migration_result, parse_result) { - (migration_result @ Ok(_), Ok(())) => migration_result, - (Err(migration_err), Ok(())) => Err(migration_err), - (_, Err(parse_err)) => Err(parse_err), - } - } - - /// Returns true if there were any errors migrating and parsing the settings content or if migration was required but there were no errors - pub fn requires_user_action(&self) -> bool { - matches!(self.parse_status, ParseStatus::Failed { .. }) - || matches!( - self.migration_status, - MigrationStatus::Succeeded | MigrationStatus::Failed { .. } - ) - } - - pub fn ok(self) -> Option { - self.result().ok() - } - - pub fn parse_error(&self) -> Option { - match &self.parse_status { - ParseStatus::Failed { error } => Some(error.clone()), - ParseStatus::Success => None, - } - } -} - -#[derive(Debug, Clone, PartialEq)] -pub enum InvalidSettingsError { - LocalSettings { path: Arc, message: String }, - UserSettings { message: String }, - ServerSettings { message: String }, - DefaultSettings { message: String }, - Editorconfig { path: Arc, message: String }, - Tasks { path: PathBuf, message: String }, - Debug { path: PathBuf, message: String }, -} - -impl std::fmt::Display for InvalidSettingsError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - InvalidSettingsError::LocalSettings { message, .. } - | InvalidSettingsError::UserSettings { message } - | InvalidSettingsError::ServerSettings { message } - | InvalidSettingsError::DefaultSettings { message } - | InvalidSettingsError::Tasks { message, .. } - | InvalidSettingsError::Editorconfig { message, .. } - | InvalidSettingsError::Debug { message, .. } => { - write!(f, "{message}") - } - } - } -} -impl std::error::Error for InvalidSettingsError {} - -impl Debug for SettingsStore { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("SettingsStore") - .field( - "types", - &self - .setting_values - .values() - .map(|value| value.setting_type_name()) - .collect::>(), - ) - .field("default_settings", &self.default_settings) - .field("user_settings", &self.user_settings) - .field("local_settings", &self.local_settings) - .finish_non_exhaustive() - } -} - -impl AnySettingValue for SettingValue { - fn from_settings(&self, s: &SettingsContent) -> Box { - Box::new(T::from_settings(s)) as _ - } - - fn setting_type_name(&self) -> &'static str { - type_name::() - } - - fn all_local_values(&self) -> Vec<(WorktreeId, Arc, &dyn Any)> { - self.local_values - .iter() - .map(|(id, path, value)| (*id, path.clone(), value as _)) - .collect() - } - - fn value_for_path(&self, path: Option) -> &dyn Any { - if let Some(SettingsLocation { worktree_id, path }) = path { - for (settings_root_id, settings_path, value) in self.local_values.iter().rev() { - if worktree_id == *settings_root_id && path.starts_with(settings_path) { - return value; - } - } - } - - self.global_value - .as_ref() - .unwrap_or_else(|| panic!("no default value for setting {}", self.setting_type_name())) - } - - fn set_global_value(&mut self, value: Box) { - self.global_value = Some(*value.downcast().unwrap()); - } - - fn set_local_value(&mut self, root_id: WorktreeId, path: Arc, value: Box) { - let value = *value.downcast().unwrap(); - match self - .local_values - .binary_search_by_key(&(root_id, &path), |e| (e.0, &e.1)) - { - Ok(ix) => self.local_values[ix].2 = value, - Err(ix) => self.local_values.insert(ix, (root_id, path, value)), - } - } -} - -#[cfg(test)] -mod tests { - use std::num::NonZeroU32; - - use crate::{ - ClosePosition, ItemSettingsContent, VsCodeSettingsSource, default_settings, - settings_content::LanguageSettingsContent, test_settings, - }; - - use super::*; - use unindent::Unindent; - use util::rel_path::rel_path; - - #[derive(Debug, PartialEq)] - struct AutoUpdateSetting { - auto_update: bool, - } - - impl Settings for AutoUpdateSetting { - fn from_settings(content: &SettingsContent) -> Self { - AutoUpdateSetting { - auto_update: content.auto_update.unwrap(), - } - } - } - - #[derive(Debug, PartialEq)] - struct ItemSettings { - close_position: ClosePosition, - git_status: bool, - } - - impl Settings for ItemSettings { - fn from_settings(content: &SettingsContent) -> Self { - let content = content.tabs.clone().unwrap(); - ItemSettings { - close_position: content.close_position.unwrap(), - git_status: content.git_status.unwrap(), - } - } - } - - #[derive(Debug, PartialEq)] - struct DefaultLanguageSettings { - tab_size: NonZeroU32, - preferred_line_length: u32, - } - - impl Settings for DefaultLanguageSettings { - fn from_settings(content: &SettingsContent) -> Self { - let content = &content.project.all_languages.defaults; - DefaultLanguageSettings { - tab_size: content.tab_size.unwrap(), - preferred_line_length: content.preferred_line_length.unwrap(), - } - } - } - - #[derive(Debug, PartialEq)] - struct ThemeSettings { - buffer_font_family: FontFamilyName, - buffer_font_fallbacks: Vec, - } - - impl Settings for ThemeSettings { - fn from_settings(content: &SettingsContent) -> Self { - let content = content.theme.clone(); - ThemeSettings { - buffer_font_family: content.buffer_font_family.unwrap(), - buffer_font_fallbacks: content.buffer_font_fallbacks.unwrap(), - } - } - } - - #[gpui::test] - fn test_settings_store_basic(cx: &mut App) { - let mut store = SettingsStore::new(cx, &default_settings()); - store.register_setting::(); - store.register_setting::(); - store.register_setting::(); - - assert_eq!( - store.get::(None), - &AutoUpdateSetting { auto_update: true } - ); - assert_eq!( - store.get::(None).close_position, - ClosePosition::Right - ); - - store - .set_user_settings( - r#"{ - "auto_update": false, - "tabs": { - "close_position": "left" - } - }"#, - cx, - ) - .unwrap(); - - assert_eq!( - store.get::(None), - &AutoUpdateSetting { auto_update: false } - ); - assert_eq!( - store.get::(None).close_position, - ClosePosition::Left - ); - - store - .set_local_settings( - WorktreeId::from_usize(1), - rel_path("root1").into(), - LocalSettingsKind::Settings, - Some(r#"{ "tab_size": 5 }"#), - cx, - ) - .unwrap(); - store - .set_local_settings( - WorktreeId::from_usize(1), - rel_path("root1/subdir").into(), - LocalSettingsKind::Settings, - Some(r#"{ "preferred_line_length": 50 }"#), - cx, - ) - .unwrap(); - - store - .set_local_settings( - WorktreeId::from_usize(1), - rel_path("root2").into(), - LocalSettingsKind::Settings, - Some(r#"{ "tab_size": 9, "auto_update": true}"#), - cx, - ) - .unwrap(); - - assert_eq!( - store.get::(Some(SettingsLocation { - worktree_id: WorktreeId::from_usize(1), - path: rel_path("root1/something"), - })), - &DefaultLanguageSettings { - preferred_line_length: 80, - tab_size: 5.try_into().unwrap(), - } - ); - assert_eq!( - store.get::(Some(SettingsLocation { - worktree_id: WorktreeId::from_usize(1), - path: rel_path("root1/subdir/something"), - })), - &DefaultLanguageSettings { - preferred_line_length: 50, - tab_size: 5.try_into().unwrap(), - } - ); - assert_eq!( - store.get::(Some(SettingsLocation { - worktree_id: WorktreeId::from_usize(1), - path: rel_path("root2/something"), - })), - &DefaultLanguageSettings { - preferred_line_length: 80, - tab_size: 9.try_into().unwrap(), - } - ); - assert_eq!( - store.get::(Some(SettingsLocation { - worktree_id: WorktreeId::from_usize(1), - path: rel_path("root2/something") - })), - &AutoUpdateSetting { auto_update: false } - ); - } - - #[gpui::test] - fn test_setting_store_assign_json_before_register(cx: &mut App) { - let mut store = SettingsStore::new(cx, &test_settings()); - store - .set_user_settings(r#"{ "auto_update": false }"#, cx) - .unwrap(); - store.register_setting::(); - - assert_eq!( - store.get::(None), - &AutoUpdateSetting { auto_update: false } - ); - } - - #[track_caller] - fn check_settings_update( - store: &mut SettingsStore, - old_json: String, - update: fn(&mut SettingsContent), - expected_new_json: String, - cx: &mut App, - ) { - store.set_user_settings(&old_json, cx).ok(); - let edits = store.edits_for_update(&old_json, update); - let mut new_json = old_json; - for (range, replacement) in edits.into_iter() { - new_json.replace_range(range, &replacement); - } - pretty_assertions::assert_eq!(new_json, expected_new_json); - } - - #[gpui::test] - fn test_setting_store_update(cx: &mut App) { - let mut store = SettingsStore::new(cx, &test_settings()); - - // entries added and updated - check_settings_update( - &mut store, - r#"{ - "languages": { - "JSON": { - "auto_indent": true - } - } - }"# - .unindent(), - |settings| { - settings - .languages_mut() - .get_mut("JSON") - .unwrap() - .auto_indent = Some(false); - - settings.languages_mut().insert( - "Rust".into(), - LanguageSettingsContent { - auto_indent: Some(true), - ..Default::default() - }, - ); - }, - r#"{ - "languages": { - "Rust": { - "auto_indent": true - }, - "JSON": { - "auto_indent": false - } - } - }"# - .unindent(), - cx, - ); - - // entries removed - check_settings_update( - &mut store, - r#"{ - "languages": { - "Rust": { - "language_setting_2": true - }, - "JSON": { - "language_setting_1": false - } - } - }"# - .unindent(), - |settings| { - settings.languages_mut().remove("JSON").unwrap(); - }, - r#"{ - "languages": { - "Rust": { - "language_setting_2": true - } - } - }"# - .unindent(), - cx, - ); - - check_settings_update( - &mut store, - r#"{ - "languages": { - "Rust": { - "language_setting_2": true - }, - "JSON": { - "language_setting_1": false - } - } - }"# - .unindent(), - |settings| { - settings.languages_mut().remove("Rust").unwrap(); - }, - r#"{ - "languages": { - "JSON": { - "language_setting_1": false - } - } - }"# - .unindent(), - cx, - ); - - // weird formatting - check_settings_update( - &mut store, - r#"{ - "tabs": { "close_position": "left", "name": "Max" } - }"# - .unindent(), - |settings| { - settings.tabs.as_mut().unwrap().close_position = Some(ClosePosition::Left); - }, - r#"{ - "tabs": { "close_position": "left", "name": "Max" } - }"# - .unindent(), - cx, - ); - - // single-line formatting, other keys - check_settings_update( - &mut store, - r#"{ "one": 1, "two": 2 }"#.to_owned(), - |settings| settings.auto_update = Some(true), - r#"{ "auto_update": true, "one": 1, "two": 2 }"#.to_owned(), - cx, - ); - - // empty object - check_settings_update( - &mut store, - r#"{ - "tabs": {} - }"# - .unindent(), - |settings| settings.tabs.as_mut().unwrap().close_position = Some(ClosePosition::Left), - r#"{ - "tabs": { - "close_position": "left" - } - }"# - .unindent(), - cx, - ); - - // no content - check_settings_update( - &mut store, - r#""#.unindent(), - |settings| { - settings.tabs = Some(ItemSettingsContent { - git_status: Some(true), - ..Default::default() - }) - }, - r#"{ - "tabs": { - "git_status": true - } - } - "# - .unindent(), - cx, - ); - - check_settings_update( - &mut store, - r#"{ - } - "# - .unindent(), - |settings| settings.title_bar.get_or_insert_default().show_branch_name = Some(true), - r#"{ - "title_bar": { - "show_branch_name": true - } - } - "# - .unindent(), - cx, - ); - } - - #[gpui::test] - fn test_vscode_import(cx: &mut App) { - let mut store = SettingsStore::new(cx, &test_settings()); - store.register_setting::(); - store.register_setting::(); - store.register_setting::(); - store.register_setting::(); - - // create settings that werent present - check_vscode_import( - &mut store, - r#"{ - } - "# - .unindent(), - r#" { "editor.tabSize": 37 } "#.to_owned(), - r#"{ - "base_keymap": "VSCode", - "tab_size": 37 - } - "# - .unindent(), - cx, - ); - - // persist settings that were present - check_vscode_import( - &mut store, - r#"{ - "preferred_line_length": 99, - } - "# - .unindent(), - r#"{ "editor.tabSize": 42 }"#.to_owned(), - r#"{ - "base_keymap": "VSCode", - "tab_size": 42, - "preferred_line_length": 99, - } - "# - .unindent(), - cx, - ); - - // don't clobber settings that aren't present in vscode - check_vscode_import( - &mut store, - r#"{ - "preferred_line_length": 99, - "tab_size": 42 - } - "# - .unindent(), - r#"{}"#.to_owned(), - r#"{ - "base_keymap": "VSCode", - "preferred_line_length": 99, - "tab_size": 42 - } - "# - .unindent(), - cx, - ); - - // custom enum - check_vscode_import( - &mut store, - r#"{ - } - "# - .unindent(), - r#"{ "git.decorations.enabled": true }"#.to_owned(), - r#"{ - "project_panel": { - "git_status": true - }, - "outline_panel": { - "git_status": true - }, - "base_keymap": "VSCode", - "tabs": { - "git_status": true - } - } - "# - .unindent(), - cx, - ); - - // font-family - check_vscode_import( - &mut store, - r#"{ - } - "# - .unindent(), - r#"{ "editor.fontFamily": "Cascadia Code, 'Consolas', Courier New" }"#.to_owned(), - r#"{ - "base_keymap": "VSCode", - "buffer_font_fallbacks": [ - "Consolas", - "Courier New" - ], - "buffer_font_family": "Cascadia Code" - } - "# - .unindent(), - cx, - ); - } - - #[track_caller] - fn check_vscode_import( - store: &mut SettingsStore, - old: String, - vscode: String, - expected: String, - cx: &mut App, - ) { - store.set_user_settings(&old, cx).ok(); - let new = store.get_vscode_edits( - old, - &VsCodeSettings::from_str(&vscode, VsCodeSettingsSource::VsCode).unwrap(), - ); - pretty_assertions::assert_eq!(new, expected); - } - - #[gpui::test] - fn test_update_git_settings(cx: &mut App) { - let store = SettingsStore::new(cx, &test_settings()); - - let actual = store.new_text_for_update("{}".to_string(), |current| { - current - .git - .get_or_insert_default() - .inline_blame - .get_or_insert_default() - .enabled = Some(true); - }); - pretty_assertions::assert_str_eq!( - actual, - r#"{ - "git": { - "inline_blame": { - "enabled": true - } - } - } - "# - .unindent() - ); - } - - #[gpui::test] - fn test_global_settings(cx: &mut App) { - let mut store = SettingsStore::new(cx, &test_settings()); - store.register_setting::(); - - // Set global settings - these should override defaults but not user settings - store - .set_global_settings( - r#"{ - "tabs": { - "close_position": "right", - "git_status": true, - } - }"#, - cx, - ) - .unwrap(); - - // Before user settings, global settings should apply - assert_eq!( - store.get::(None), - &ItemSettings { - close_position: ClosePosition::Right, - git_status: true, - } - ); - - // Set user settings - these should override both defaults and global - store - .set_user_settings( - r#"{ - "tabs": { - "close_position": "left" - } - }"#, - cx, - ) - .unwrap(); - - // User settings should override global settings - assert_eq!( - store.get::(None), - &ItemSettings { - close_position: ClosePosition::Left, - git_status: true, // Staff from global settings - } - ); - } - - #[gpui::test] - fn test_get_value_for_field_basic(cx: &mut App) { - let mut store = SettingsStore::new(cx, &test_settings()); - store.register_setting::(); - - store - .set_user_settings(r#"{"preferred_line_length": 0}"#, cx) - .unwrap(); - let local = (WorktreeId::from_usize(0), RelPath::empty().into_arc()); - store - .set_local_settings( - local.0, - local.1.clone(), - LocalSettingsKind::Settings, - Some(r#"{}"#), - cx, - ) - .unwrap(); - - fn get(content: &SettingsContent) -> Option<&u32> { - content - .project - .all_languages - .defaults - .preferred_line_length - .as_ref() - } - - let default_value = *get(&store.default_settings).unwrap(); - - assert_eq!( - store.get_value_from_file(SettingsFile::Project(local.clone()), get), - (SettingsFile::User, Some(&0)) - ); - assert_eq!( - store.get_value_from_file(SettingsFile::User, get), - (SettingsFile::User, Some(&0)) - ); - store.set_user_settings(r#"{}"#, cx).unwrap(); - assert_eq!( - store.get_value_from_file(SettingsFile::Project(local.clone()), get), - (SettingsFile::Default, Some(&default_value)) - ); - store - .set_local_settings( - local.0, - local.1.clone(), - LocalSettingsKind::Settings, - Some(r#"{"preferred_line_length": 80}"#), - cx, - ) - .unwrap(); - assert_eq!( - store.get_value_from_file(SettingsFile::Project(local.clone()), get), - (SettingsFile::Project(local), Some(&80)) - ); - assert_eq!( - store.get_value_from_file(SettingsFile::User, get), - (SettingsFile::Default, Some(&default_value)) - ); - } - - #[gpui::test] - fn test_get_value_for_field_local_worktrees_dont_interfere(cx: &mut App) { - let mut store = SettingsStore::new(cx, &test_settings()); - store.register_setting::(); - store.register_setting::(); - - let local_1 = (WorktreeId::from_usize(0), RelPath::empty().into_arc()); - - let local_1_child = ( - WorktreeId::from_usize(0), - RelPath::new( - std::path::Path::new("child1"), - util::paths::PathStyle::Posix, - ) - .unwrap() - .into_arc(), - ); - - let local_2 = (WorktreeId::from_usize(1), RelPath::empty().into_arc()); - let local_2_child = ( - WorktreeId::from_usize(1), - RelPath::new( - std::path::Path::new("child2"), - util::paths::PathStyle::Posix, - ) - .unwrap() - .into_arc(), - ); - - fn get(content: &SettingsContent) -> Option<&u32> { - content - .project - .all_languages - .defaults - .preferred_line_length - .as_ref() - } - - store - .set_local_settings( - local_1.0, - local_1.1.clone(), - LocalSettingsKind::Settings, - Some(r#"{"preferred_line_length": 1}"#), - cx, - ) - .unwrap(); - store - .set_local_settings( - local_1_child.0, - local_1_child.1.clone(), - LocalSettingsKind::Settings, - Some(r#"{}"#), - cx, - ) - .unwrap(); - store - .set_local_settings( - local_2.0, - local_2.1.clone(), - LocalSettingsKind::Settings, - Some(r#"{"preferred_line_length": 2}"#), - cx, - ) - .unwrap(); - store - .set_local_settings( - local_2_child.0, - local_2_child.1.clone(), - LocalSettingsKind::Settings, - Some(r#"{}"#), - cx, - ) - .unwrap(); - - // each local child should only inherit from it's parent - assert_eq!( - store.get_value_from_file(SettingsFile::Project(local_2_child), get), - (SettingsFile::Project(local_2), Some(&2)) - ); - assert_eq!( - store.get_value_from_file(SettingsFile::Project(local_1_child.clone()), get), - (SettingsFile::Project(local_1.clone()), Some(&1)) - ); - - // adjacent children should be treated as siblings not inherit from each other - let local_1_adjacent_child = (local_1.0, rel_path("adjacent_child").into_arc()); - store - .set_local_settings( - local_1_adjacent_child.0, - local_1_adjacent_child.1.clone(), - LocalSettingsKind::Settings, - Some(r#"{}"#), - cx, - ) - .unwrap(); - store - .set_local_settings( - local_1_child.0, - local_1_child.1.clone(), - LocalSettingsKind::Settings, - Some(r#"{"preferred_line_length": 3}"#), - cx, - ) - .unwrap(); - - assert_eq!( - store.get_value_from_file(SettingsFile::Project(local_1_adjacent_child.clone()), get), - (SettingsFile::Project(local_1.clone()), Some(&1)) - ); - store - .set_local_settings( - local_1_adjacent_child.0, - local_1_adjacent_child.1, - LocalSettingsKind::Settings, - Some(r#"{"preferred_line_length": 3}"#), - cx, - ) - .unwrap(); - store - .set_local_settings( - local_1_child.0, - local_1_child.1.clone(), - LocalSettingsKind::Settings, - Some(r#"{}"#), - cx, - ) - .unwrap(); - assert_eq!( - store.get_value_from_file(SettingsFile::Project(local_1_child), get), - (SettingsFile::Project(local_1), Some(&1)) - ); - } - - #[gpui::test] - fn test_get_overrides_for_field(cx: &mut App) { - let mut store = SettingsStore::new(cx, &test_settings()); - store.register_setting::(); - - let wt0_root = (WorktreeId::from_usize(0), RelPath::empty().into_arc()); - let wt0_child1 = (WorktreeId::from_usize(0), rel_path("child1").into_arc()); - let wt0_child2 = (WorktreeId::from_usize(0), rel_path("child2").into_arc()); - - let wt1_root = (WorktreeId::from_usize(1), RelPath::empty().into_arc()); - let wt1_subdir = (WorktreeId::from_usize(1), rel_path("subdir").into_arc()); - - fn get(content: &SettingsContent) -> &Option { - &content.project.all_languages.defaults.preferred_line_length - } - - store - .set_user_settings(r#"{"preferred_line_length": 100}"#, cx) - .unwrap(); - - store - .set_local_settings( - wt0_root.0, - wt0_root.1.clone(), - LocalSettingsKind::Settings, - Some(r#"{"preferred_line_length": 80}"#), - cx, - ) - .unwrap(); - store - .set_local_settings( - wt0_child1.0, - wt0_child1.1.clone(), - LocalSettingsKind::Settings, - Some(r#"{"preferred_line_length": 120}"#), - cx, - ) - .unwrap(); - store - .set_local_settings( - wt0_child2.0, - wt0_child2.1.clone(), - LocalSettingsKind::Settings, - Some(r#"{}"#), - cx, - ) - .unwrap(); - - store - .set_local_settings( - wt1_root.0, - wt1_root.1.clone(), - LocalSettingsKind::Settings, - Some(r#"{"preferred_line_length": 90}"#), - cx, - ) - .unwrap(); - store - .set_local_settings( - wt1_subdir.0, - wt1_subdir.1.clone(), - LocalSettingsKind::Settings, - Some(r#"{}"#), - cx, - ) - .unwrap(); - - let overrides = store.get_overrides_for_field(SettingsFile::Default, get); - assert_eq!( - overrides, - vec![ - SettingsFile::User, - SettingsFile::Project(wt0_root.clone()), - SettingsFile::Project(wt0_child1.clone()), - SettingsFile::Project(wt1_root.clone()), - ] - ); - - let overrides = store.get_overrides_for_field(SettingsFile::User, get); - assert_eq!( - overrides, - vec![ - SettingsFile::Project(wt0_root.clone()), - SettingsFile::Project(wt0_child1.clone()), - SettingsFile::Project(wt1_root.clone()), - ] - ); - - let overrides = store.get_overrides_for_field(SettingsFile::Project(wt0_root), get); - assert_eq!(overrides, vec![]); - - let overrides = - store.get_overrides_for_field(SettingsFile::Project(wt0_child1.clone()), get); - assert_eq!(overrides, vec![]); - - let overrides = store.get_overrides_for_field(SettingsFile::Project(wt0_child2), get); - assert_eq!(overrides, vec![]); - - let overrides = store.get_overrides_for_field(SettingsFile::Project(wt1_root), get); - assert_eq!(overrides, vec![]); - - let overrides = store.get_overrides_for_field(SettingsFile::Project(wt1_subdir), get); - assert_eq!(overrides, vec![]); - - let wt0_deep_child = ( - WorktreeId::from_usize(0), - rel_path("child1/subdir").into_arc(), - ); - store - .set_local_settings( - wt0_deep_child.0, - wt0_deep_child.1.clone(), - LocalSettingsKind::Settings, - Some(r#"{"preferred_line_length": 140}"#), - cx, - ) - .unwrap(); - - let overrides = store.get_overrides_for_field(SettingsFile::Project(wt0_deep_child), get); - assert_eq!(overrides, vec![]); - - let overrides = store.get_overrides_for_field(SettingsFile::Project(wt0_child1), get); - assert_eq!(overrides, vec![]); - } - - #[test] - fn test_file_ord() { - let wt0_root = - SettingsFile::Project((WorktreeId::from_usize(0), RelPath::empty().into_arc())); - let wt0_child1 = - SettingsFile::Project((WorktreeId::from_usize(0), rel_path("child1").into_arc())); - let wt0_child2 = - SettingsFile::Project((WorktreeId::from_usize(0), rel_path("child2").into_arc())); - - let wt1_root = - SettingsFile::Project((WorktreeId::from_usize(1), RelPath::empty().into_arc())); - let wt1_subdir = - SettingsFile::Project((WorktreeId::from_usize(1), rel_path("subdir").into_arc())); - - let mut files = vec![ - &wt1_root, - &SettingsFile::Default, - &wt0_root, - &wt1_subdir, - &wt0_child2, - &SettingsFile::Server, - &wt0_child1, - &SettingsFile::User, - ]; - - files.sort(); - pretty_assertions::assert_eq!( - files, - vec![ - &wt0_child2, - &wt0_child1, - &wt0_root, - &wt1_subdir, - &wt1_root, - &SettingsFile::Server, - &SettingsFile::User, - &SettingsFile::Default, - ] - ) - } -} diff --git a/crates/settings/src/vscode_import.rs b/crates/settings/src/vscode_import.rs deleted file mode 100644 index 587850303f..0000000000 --- a/crates/settings/src/vscode_import.rs +++ /dev/null @@ -1,914 +0,0 @@ -use crate::*; -use anyhow::{Context as _, Result, anyhow}; -use collections::HashMap; -use fs::Fs; -use paths::{cursor_settings_file_paths, vscode_settings_file_paths}; -use serde::Deserialize; -use serde_json::{Map, Value}; -use std::{ - num::{NonZeroU32, NonZeroUsize}, - path::{Path, PathBuf}, - sync::Arc, -}; - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum VsCodeSettingsSource { - VsCode, - Cursor, -} - -impl std::fmt::Display for VsCodeSettingsSource { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - VsCodeSettingsSource::VsCode => write!(f, "VS Code"), - VsCodeSettingsSource::Cursor => write!(f, "Cursor"), - } - } -} - -pub struct VsCodeSettings { - pub source: VsCodeSettingsSource, - pub path: Arc, - content: Map, -} - -impl VsCodeSettings { - #[cfg(any(test, feature = "test-support"))] - pub fn from_str(content: &str, source: VsCodeSettingsSource) -> Result { - Ok(Self { - source, - path: Path::new("/example-path/Code/User/settings.json").into(), - content: serde_json_lenient::from_str(content)?, - }) - } - - pub async fn load_user_settings(source: VsCodeSettingsSource, fs: Arc) -> Result { - let candidate_paths = match source { - VsCodeSettingsSource::VsCode => vscode_settings_file_paths(), - VsCodeSettingsSource::Cursor => cursor_settings_file_paths(), - }; - let mut path = None; - for candidate_path in candidate_paths.iter() { - if fs.is_file(candidate_path).await { - path = Some(candidate_path.clone()); - } - } - let Some(path) = path else { - return Err(anyhow!( - "No settings file found, expected to find it in one of the following paths:\n{}", - candidate_paths - .into_iter() - .map(|path| path.to_string_lossy().into_owned()) - .collect::>() - .join("\n") - )); - }; - let content = fs.load(&path).await.with_context(|| { - format!( - "Error loading {} settings file from {}", - source, - path.display() - ) - })?; - let content = serde_json_lenient::from_str(&content).with_context(|| { - format!( - "Error parsing {} settings file from {}", - source, - path.display() - ) - })?; - Ok(Self { - source, - path: path.into(), - content, - }) - } - - fn read_value(&self, setting: &str) -> Option<&Value> { - self.content.get(setting) - } - - fn read_str(&self, setting: &str) -> Option<&str> { - self.read_value(setting).and_then(|v| v.as_str()) - } - - fn read_string(&self, setting: &str) -> Option { - self.read_value(setting) - .and_then(|v| v.as_str()) - .map(|s| s.to_owned()) - } - - fn read_bool(&self, setting: &str) -> Option { - self.read_value(setting).and_then(|v| v.as_bool()) - } - - fn read_f32(&self, setting: &str) -> Option { - self.read_value(setting) - .and_then(|v| v.as_f64()) - .map(|v| v as f32) - } - - fn read_u64(&self, setting: &str) -> Option { - self.read_value(setting).and_then(|v| v.as_u64()) - } - - fn read_usize(&self, setting: &str) -> Option { - self.read_value(setting) - .and_then(|v| v.as_u64()) - .and_then(|v| v.try_into().ok()) - } - - fn read_u32(&self, setting: &str) -> Option { - self.read_value(setting) - .and_then(|v| v.as_u64()) - .and_then(|v| v.try_into().ok()) - } - - fn read_enum(&self, key: &str, f: impl FnOnce(&str) -> Option) -> Option { - self.content.get(key).and_then(Value::as_str).and_then(f) - } - - fn read_fonts(&self, key: &str) -> (Option, Option>) { - let Some(css_name) = self.content.get(key).and_then(Value::as_str) else { - return (None, None); - }; - - let mut name_buffer = String::new(); - let mut quote_char: Option = None; - let mut fonts = Vec::new(); - let mut add_font = |buffer: &mut String| { - let trimmed = buffer.trim(); - if !trimmed.is_empty() { - fonts.push(trimmed.to_string().into()); - } - - buffer.clear(); - }; - - for ch in css_name.chars() { - match (ch, quote_char) { - ('"' | '\'', None) => { - quote_char = Some(ch); - } - (_, Some(q)) if ch == q => { - quote_char = None; - } - (',', None) => { - add_font(&mut name_buffer); - } - _ => { - name_buffer.push(ch); - } - } - } - - add_font(&mut name_buffer); - if fonts.is_empty() { - return (None, None); - } - (Some(fonts.remove(0)), skip_default(fonts)) - } - - pub fn settings_content(&self) -> SettingsContent { - SettingsContent { - agent: self.agent_settings_content(), - agent_servers: None, - audio: None, - auto_update: None, - base_keymap: Some(BaseKeymapContent::VSCode), - calls: None, - collaboration_panel: None, - debugger: None, - diagnostics: None, - disable_ai: None, - editor: self.editor_settings_content(), - extension: ExtensionSettingsContent::default(), - file_finder: None, - git: self.git_settings_content(), - git_panel: self.git_panel_settings_content(), - global_lsp_settings: None, - helix_mode: None, - image_viewer: None, - journal: None, - language_models: None, - line_indicator_format: None, - log: None, - message_editor: None, - node: self.node_binary_settings(), - notification_panel: None, - outline_panel: self.outline_panel_settings_content(), - preview_tabs: self.preview_tabs_settings_content(), - project: self.project_settings_content(), - project_panel: self.project_panel_settings_content(), - proxy: self.read_string("http.proxy"), - remote: RemoteSettingsContent::default(), - repl: None, - server_url: None, - session: None, - status_bar: self.status_bar_settings_content(), - tab_bar: self.tab_bar_settings_content(), - tabs: self.item_settings_content(), - telemetry: self.telemetry_settings_content(), - terminal: self.terminal_settings_content(), - theme: Box::new(self.theme_settings_content()), - title_bar: None, - vim: None, - vim_mode: None, - workspace: self.workspace_settings_content(), - } - } - - fn agent_settings_content(&self) -> Option { - let enabled = self.read_bool("chat.agent.enabled"); - skip_default(AgentSettingsContent { - enabled: enabled, - button: enabled, - ..Default::default() - }) - } - - fn editor_settings_content(&self) -> EditorSettingsContent { - EditorSettingsContent { - auto_signature_help: self.read_bool("editor.parameterHints.enabled"), - autoscroll_on_clicks: None, - cursor_blink: self.read_enum("editor.cursorBlinking", |s| match s { - "blink" | "phase" | "expand" | "smooth" => Some(true), - "solid" => Some(false), - _ => None, - }), - cursor_shape: self.read_enum("editor.cursorStyle", |s| match s { - "block" => Some(CursorShape::Block), - "block-outline" => Some(CursorShape::Hollow), - "line" | "line-thin" => Some(CursorShape::Bar), - "underline" | "underline-thin" => Some(CursorShape::Underline), - _ => None, - }), - current_line_highlight: self.read_enum("editor.renderLineHighlight", |s| match s { - "gutter" => Some(CurrentLineHighlight::Gutter), - "line" => Some(CurrentLineHighlight::Line), - "all" => Some(CurrentLineHighlight::All), - _ => None, - }), - diagnostics_max_severity: None, - double_click_in_multibuffer: None, - drag_and_drop_selection: None, - excerpt_context_lines: None, - expand_excerpt_lines: None, - fast_scroll_sensitivity: self.read_f32("editor.fastScrollSensitivity"), - sticky_scroll: self.sticky_scroll_content(), - go_to_definition_fallback: None, - gutter: self.gutter_content(), - hide_mouse: None, - horizontal_scroll_margin: None, - hover_popover_delay: self.read_u64("editor.hover.delay").map(Into::into), - hover_popover_enabled: self.read_bool("editor.hover.enabled"), - inline_code_actions: None, - jupyter: None, - lsp_document_colors: None, - lsp_highlight_debounce: None, - middle_click_paste: None, - minimap: self.minimap_content(), - minimum_contrast_for_highlights: None, - multi_cursor_modifier: self.read_enum("editor.multiCursorModifier", |s| match s { - "ctrlCmd" => Some(MultiCursorModifier::CmdOrCtrl), - "alt" => Some(MultiCursorModifier::Alt), - _ => None, - }), - redact_private_values: None, - relative_line_numbers: self.read_enum("editor.lineNumbers", |s| match s { - "relative" => Some(RelativeLineNumbers::Enabled), - _ => None, - }), - rounded_selection: self.read_bool("editor.roundedSelection"), - scroll_beyond_last_line: None, - scroll_sensitivity: self.read_f32("editor.mouseWheelScrollSensitivity"), - scrollbar: self.scrollbar_content(), - search: self.search_content(), - search_wrap: None, - seed_search_query_from_cursor: self.read_enum( - "editor.find.seedSearchStringFromSelection", - |s| match s { - "always" => Some(SeedQuerySetting::Always), - "selection" => Some(SeedQuerySetting::Selection), - "never" => Some(SeedQuerySetting::Never), - _ => None, - }, - ), - selection_highlight: self.read_bool("editor.selectionHighlight"), - show_signature_help_after_edits: self.read_bool("editor.parameterHints.enabled"), - snippet_sort_order: None, - toolbar: None, - use_smartcase_search: self.read_bool("search.smartCase"), - vertical_scroll_margin: self.read_f32("editor.cursorSurroundingLines"), - completion_menu_scrollbar: None, - } - } - - fn sticky_scroll_content(&self) -> Option { - skip_default(StickyScrollContent { - enabled: self.read_bool("editor.stickyScroll.enabled"), - }) - } - - fn gutter_content(&self) -> Option { - skip_default(GutterContent { - line_numbers: self.read_enum("editor.lineNumbers", |s| match s { - "on" | "relative" => Some(true), - "off" => Some(false), - _ => None, - }), - min_line_number_digits: None, - runnables: None, - breakpoints: None, - folds: self.read_enum("editor.showFoldingControls", |s| match s { - "always" | "mouseover" => Some(true), - "never" => Some(false), - _ => None, - }), - }) - } - - fn scrollbar_content(&self) -> Option { - let scrollbar_axes = skip_default(ScrollbarAxesContent { - horizontal: self.read_enum("editor.scrollbar.horizontal", |s| match s { - "auto" | "visible" => Some(true), - "hidden" => Some(false), - _ => None, - }), - vertical: self.read_enum("editor.scrollbar.vertical", |s| match s { - "auto" | "visible" => Some(true), - "hidden" => Some(false), - _ => None, - }), - })?; - - Some(ScrollbarContent { - axes: Some(scrollbar_axes), - ..Default::default() - }) - } - - fn search_content(&self) -> Option { - skip_default(SearchSettingsContent { - include_ignored: self.read_bool("search.useIgnoreFiles"), - ..Default::default() - }) - } - - fn minimap_content(&self) -> Option { - let minimap_enabled = self.read_bool("editor.minimap.enabled"); - let autohide = self.read_bool("editor.minimap.autohide"); - let show = match (minimap_enabled, autohide) { - (Some(true), Some(false)) => Some(ShowMinimap::Always), - (Some(true), _) => Some(ShowMinimap::Auto), - (Some(false), _) => Some(ShowMinimap::Never), - _ => None, - }; - - skip_default(MinimapContent { - show, - thumb: self.read_enum("editor.minimap.showSlider", |s| match s { - "always" => Some(MinimapThumb::Always), - "mouseover" => Some(MinimapThumb::Hover), - _ => None, - }), - max_width_columns: self - .read_u32("editor.minimap.maxColumn") - .and_then(|v| NonZeroU32::new(v)), - ..Default::default() - }) - } - - fn git_panel_settings_content(&self) -> Option { - skip_default(GitPanelSettingsContent { - button: self.read_bool("git.enabled"), - fallback_branch_name: self.read_string("git.defaultBranchName"), - ..Default::default() - }) - } - - fn project_settings_content(&self) -> ProjectSettingsContent { - ProjectSettingsContent { - all_languages: AllLanguageSettingsContent { - features: None, - edit_predictions: self.edit_predictions_settings_content(), - defaults: self.default_language_settings_content(), - languages: Default::default(), - file_types: self.file_types(), - }, - worktree: self.worktree_settings_content(), - lsp: Default::default(), - terminal: None, - dap: Default::default(), - context_servers: self.context_servers(), - load_direnv: None, - slash_commands: None, - git_hosting_providers: None, - } - } - - fn default_language_settings_content(&self) -> LanguageSettingsContent { - LanguageSettingsContent { - allow_rewrap: None, - always_treat_brackets_as_autoclosed: None, - auto_indent: None, - auto_indent_on_paste: self.read_bool("editor.formatOnPaste"), - code_actions_on_format: None, - completions: skip_default(CompletionSettingsContent { - words: self.read_bool("editor.suggest.showWords").map(|b| { - if b { - WordsCompletionMode::Enabled - } else { - WordsCompletionMode::Disabled - } - }), - ..Default::default() - }), - debuggers: None, - edit_predictions_disabled_in: None, - enable_language_server: None, - ensure_final_newline_on_save: self.read_bool("files.insertFinalNewline"), - extend_comment_on_newline: None, - format_on_save: self.read_bool("editor.guides.formatOnSave").map(|b| { - if b { - FormatOnSave::On - } else { - FormatOnSave::Off - } - }), - formatter: None, - hard_tabs: self.read_bool("editor.insertSpaces").map(|v| !v), - indent_guides: skip_default(IndentGuideSettingsContent { - enabled: self.read_bool("editor.guides.indentation"), - ..Default::default() - }), - inlay_hints: None, - jsx_tag_auto_close: None, - language_servers: None, - linked_edits: self.read_bool("editor.linkedEditing"), - preferred_line_length: self.read_u32("editor.wordWrapColumn"), - prettier: None, - remove_trailing_whitespace_on_save: self.read_bool("editor.trimAutoWhitespace"), - show_completion_documentation: None, - colorize_brackets: self.read_bool("editor.bracketPairColorization.enabled"), - show_completions_on_input: self.read_bool("editor.suggestOnTriggerCharacters"), - show_edit_predictions: self.read_bool("editor.inlineSuggest.enabled"), - show_whitespaces: self.read_enum("editor.renderWhitespace", |s| { - Some(match s { - "boundary" => ShowWhitespaceSetting::Boundary, - "trailing" => ShowWhitespaceSetting::Trailing, - "selection" => ShowWhitespaceSetting::Selection, - "all" => ShowWhitespaceSetting::All, - _ => ShowWhitespaceSetting::None, - }) - }), - show_wrap_guides: None, - soft_wrap: self.read_enum("editor.wordWrap", |s| match s { - "on" => Some(SoftWrap::EditorWidth), - "wordWrapColumn" => Some(SoftWrap::PreferLine), - "bounded" => Some(SoftWrap::Bounded), - "off" => Some(SoftWrap::None), - _ => None, - }), - tab_size: self - .read_u32("editor.tabSize") - .and_then(|n| NonZeroU32::new(n)), - tasks: None, - use_auto_surround: self.read_enum("editor.autoSurround", |s| match s { - "languageDefined" | "quotes" | "brackets" => Some(true), - "never" => Some(false), - _ => None, - }), - use_autoclose: None, - use_on_type_format: self.read_bool("editor.formatOnType"), - whitespace_map: None, - wrap_guides: self - .read_value("editor.rulers") - .and_then(|v| v.as_array()) - .map(|v| { - v.iter() - .flat_map(|n| n.as_u64().map(|n| n as usize)) - .collect() - }), - word_diff_enabled: None, - } - } - - fn file_types(&self) -> Option, ExtendingVec>> { - // vscodes file association map is inverted from ours, so we flip the mapping before merging - let mut associations: HashMap, ExtendingVec> = HashMap::default(); - let map = self.read_value("files.associations")?.as_object()?; - for (k, v) in map { - let Some(v) = v.as_str() else { continue }; - associations.entry(v.into()).or_default().0.push(k.clone()); - } - skip_default(associations) - } - - fn edit_predictions_settings_content(&self) -> Option { - let disabled_globs = self - .read_value("cursor.general.globalCursorIgnoreList")? - .as_array()?; - - skip_default(EditPredictionSettingsContent { - disabled_globs: skip_default( - disabled_globs - .iter() - .filter_map(|glob| glob.as_str()) - .map(|s| s.to_string()) - .collect(), - ), - ..Default::default() - }) - } - - fn outline_panel_settings_content(&self) -> Option { - skip_default(OutlinePanelSettingsContent { - file_icons: self.read_bool("outline.icons"), - folder_icons: self.read_bool("outline.icons"), - git_status: self.read_bool("git.decorations.enabled"), - ..Default::default() - }) - } - - fn node_binary_settings(&self) -> Option { - // this just sets the binary name instead of a full path so it relies on path lookup - // resolving to the one you want - skip_default(NodeBinarySettings { - npm_path: self.read_enum("npm.packageManager", |s| match s { - v @ ("npm" | "yarn" | "bun" | "pnpm") => Some(v.to_owned()), - _ => None, - }), - ..Default::default() - }) - } - - fn git_settings_content(&self) -> Option { - let inline_blame = self.read_bool("git.blame.editorDecoration.enabled")?; - skip_default(GitSettings { - inline_blame: Some(InlineBlameSettings { - enabled: Some(inline_blame), - ..Default::default() - }), - ..Default::default() - }) - } - - fn context_servers(&self) -> HashMap, ContextServerSettingsContent> { - #[derive(Deserialize)] - struct VsCodeContextServerCommand { - command: PathBuf, - args: Option>, - env: Option>, - // note: we don't support envFile and type - } - let Some(mcp) = self.read_value("mcp").and_then(|v| v.as_object()) else { - return Default::default(); - }; - mcp.iter() - .filter_map(|(k, v)| { - Some(( - k.clone().into(), - ContextServerSettingsContent::Stdio { - enabled: true, - command: serde_json::from_value::(v.clone()) - .ok() - .map(|cmd| ContextServerCommand { - path: cmd.command, - args: cmd.args.unwrap_or_default(), - env: cmd.env, - timeout: None, - })?, - }, - )) - }) - .collect() - } - - fn item_settings_content(&self) -> Option { - skip_default(ItemSettingsContent { - git_status: self.read_bool("git.decorations.enabled"), - close_position: self.read_enum("workbench.editor.tabActionLocation", |s| match s { - "right" => Some(ClosePosition::Right), - "left" => Some(ClosePosition::Left), - _ => None, - }), - file_icons: self.read_bool("workbench.editor.showIcons"), - activate_on_close: self - .read_bool("workbench.editor.focusRecentEditorAfterClose") - .map(|b| { - if b { - ActivateOnClose::History - } else { - ActivateOnClose::LeftNeighbour - } - }), - show_diagnostics: None, - show_close_button: self - .read_bool("workbench.editor.tabActionCloseVisibility") - .map(|b| { - if b { - ShowCloseButton::Always - } else { - ShowCloseButton::Hidden - } - }), - }) - } - - fn preview_tabs_settings_content(&self) -> Option { - skip_default(PreviewTabsSettingsContent { - enabled: self.read_bool("workbench.editor.enablePreview"), - enable_preview_from_project_panel: None, - enable_preview_from_file_finder: self - .read_bool("workbench.editor.enablePreviewFromQuickOpen"), - enable_preview_from_multibuffer: None, - enable_preview_multibuffer_from_code_navigation: None, - enable_preview_file_from_code_navigation: None, - enable_keep_preview_on_code_navigation: self - .read_bool("workbench.editor.enablePreviewFromCodeNavigation"), - }) - } - - fn tab_bar_settings_content(&self) -> Option { - skip_default(TabBarSettingsContent { - show: self.read_enum("workbench.editor.showTabs", |s| match s { - "multiple" => Some(true), - "single" | "none" => Some(false), - _ => None, - }), - show_nav_history_buttons: None, - show_tab_bar_buttons: self - .read_str("workbench.editor.editorActionsLocation") - .and_then(|str| if str == "hidden" { Some(false) } else { None }), - }) - } - - fn status_bar_settings_content(&self) -> Option { - skip_default(StatusBarSettingsContent { - show: self.read_bool("workbench.statusBar.visible"), - active_language_button: None, - cursor_position_button: None, - line_endings_button: None, - }) - } - - fn project_panel_settings_content(&self) -> Option { - let mut project_panel_settings = ProjectPanelSettingsContent { - auto_fold_dirs: self.read_bool("explorer.compactFolders"), - auto_reveal_entries: self.read_bool("explorer.autoReveal"), - button: None, - default_width: None, - dock: None, - drag_and_drop: None, - entry_spacing: None, - file_icons: None, - folder_icons: None, - git_status: self.read_bool("git.decorations.enabled"), - hide_gitignore: self.read_bool("explorer.excludeGitIgnore"), - hide_hidden: None, - hide_root: None, - indent_guides: None, - indent_size: None, - scrollbar: None, - show_diagnostics: self - .read_bool("problems.decorations.enabled") - .and_then(|b| if b { Some(ShowDiagnostics::Off) } else { None }), - sort_mode: None, - starts_open: None, - sticky_scroll: None, - auto_open: None, - }; - - if let (Some(false), Some(false)) = ( - self.read_bool("explorer.decorations.badges"), - self.read_bool("explorer.decorations.colors"), - ) { - project_panel_settings.git_status = Some(false); - project_panel_settings.show_diagnostics = Some(ShowDiagnostics::Off); - } - - skip_default(project_panel_settings) - } - - fn telemetry_settings_content(&self) -> Option { - self.read_enum("telemetry.telemetryLevel", |level| { - let (metrics, diagnostics) = match level { - "all" => (true, true), - "error" | "crash" => (false, true), - "off" => (false, false), - _ => return None, - }; - Some(TelemetrySettingsContent { - metrics: Some(metrics), - diagnostics: Some(diagnostics), - }) - }) - } - - fn terminal_settings_content(&self) -> Option { - let (font_family, font_fallbacks) = self.read_fonts("terminal.integrated.fontFamily"); - skip_default(TerminalSettingsContent { - alternate_scroll: None, - blinking: self - .read_bool("terminal.integrated.cursorBlinking") - .map(|b| { - if b { - TerminalBlink::On - } else { - TerminalBlink::Off - } - }), - button: None, - copy_on_select: self.read_bool("terminal.integrated.copyOnSelection"), - cursor_shape: self.read_enum("terminal.integrated.cursorStyle", |s| match s { - "block" => Some(CursorShapeContent::Block), - "line" => Some(CursorShapeContent::Bar), - "underline" => Some(CursorShapeContent::Underline), - _ => None, - }), - default_height: None, - default_width: None, - dock: None, - font_fallbacks, - font_family, - font_features: None, - font_size: self.read_f32("terminal.integrated.fontSize"), - font_weight: None, - keep_selection_on_copy: None, - line_height: self - .read_f32("terminal.integrated.lineHeight") - .map(|lh| TerminalLineHeight::Custom(lh)), - max_scroll_history_lines: self.read_usize("terminal.integrated.scrollback"), - minimum_contrast: None, - option_as_meta: self.read_bool("terminal.integrated.macOptionIsMeta"), - project: self.project_terminal_settings_content(), - scrollbar: None, - scroll_multiplier: None, - toolbar: None, - }) - } - - fn project_terminal_settings_content(&self) -> ProjectTerminalSettingsContent { - #[cfg(target_os = "windows")] - let platform = "windows"; - #[cfg(target_os = "linux")] - let platform = "linux"; - #[cfg(target_os = "macos")] - let platform = "osx"; - #[cfg(target_os = "freebsd")] - let platform = "freebsd"; - let env = self - .read_value(&format!("terminal.integrated.env.{platform}")) - .and_then(|v| v.as_object()) - .map(|v| { - v.iter() - .map(|(k, v)| (k.clone(), v.to_string())) - // zed does not support substitutions, so this can break env vars - .filter(|(_, v)| !v.contains('$')) - .collect() - }); - - ProjectTerminalSettingsContent { - // TODO: handle arguments - shell: self - .read_string(&format!("terminal.integrated.{platform}Exec")) - .map(|s| Shell::Program(s)), - working_directory: None, - env, - detect_venv: None, - path_hyperlink_regexes: None, - path_hyperlink_timeout_ms: None, - } - } - - fn theme_settings_content(&self) -> ThemeSettingsContent { - let (buffer_font_family, buffer_font_fallbacks) = self.read_fonts("editor.fontFamily"); - ThemeSettingsContent { - ui_font_size: None, - ui_font_family: None, - ui_font_fallbacks: None, - ui_font_features: None, - ui_font_weight: None, - buffer_font_family, - buffer_font_fallbacks, - buffer_font_size: self.read_f32("editor.fontSize"), - buffer_font_weight: self.read_f32("editor.fontWeight").map(|w| w.into()), - buffer_line_height: None, - buffer_font_features: None, - agent_ui_font_size: None, - agent_buffer_font_size: None, - theme: None, - icon_theme: None, - ui_density: None, - unnecessary_code_fade: None, - experimental_theme_overrides: None, - theme_overrides: Default::default(), - } - } - - fn workspace_settings_content(&self) -> WorkspaceSettingsContent { - WorkspaceSettingsContent { - active_pane_modifiers: self.active_pane_modifiers(), - autosave: self.read_enum("files.autoSave", |s| match s { - "off" => Some(AutosaveSetting::Off), - "afterDelay" => Some(AutosaveSetting::AfterDelay { - milliseconds: self - .read_value("files.autoSaveDelay") - .and_then(|v| v.as_u64()) - .unwrap_or(1000) - .into(), - }), - "onFocusChange" => Some(AutosaveSetting::OnFocusChange), - "onWindowChange" => Some(AutosaveSetting::OnWindowChange), - _ => None, - }), - bottom_dock_layout: None, - centered_layout: None, - close_on_file_delete: None, - command_aliases: Default::default(), - confirm_quit: self.read_enum("window.confirmBeforeClose", |s| match s { - "always" | "keyboardOnly" => Some(true), - "never" => Some(false), - _ => None, - }), - drop_target_size: None, - // workbench.editor.limit contains "enabled", "value", and "perEditorGroup" - // our semantics match if those are set to true, some N, and true respectively. - // we'll ignore "perEditorGroup" for now since we only support a global max - max_tabs: if self.read_bool("workbench.editor.limit.enabled") == Some(true) { - self.read_usize("workbench.editor.limit.value") - .and_then(|n| NonZeroUsize::new(n)) - } else { - None - }, - on_last_window_closed: None, - pane_split_direction_horizontal: None, - pane_split_direction_vertical: None, - resize_all_panels_in_dock: None, - restore_on_file_reopen: self.read_bool("workbench.editor.restoreViewState"), - restore_on_startup: None, - window_decorations: None, - show_call_status_icon: None, - use_system_path_prompts: self.read_bool("files.simpleDialog.enable"), - use_system_prompts: None, - use_system_window_tabs: self.read_bool("window.nativeTabs"), - when_closing_with_no_tabs: self.read_bool("window.closeWhenEmpty").map(|b| { - if b { - CloseWindowWhenNoItems::CloseWindow - } else { - CloseWindowWhenNoItems::KeepWindowOpen - } - }), - zoomed_padding: None, - } - } - - fn active_pane_modifiers(&self) -> Option { - if self.read_bool("accessibility.dimUnfocused.enabled") == Some(true) - && let Some(opacity) = self.read_f32("accessibility.dimUnfocused.opacity") - { - Some(ActivePaneModifiers { - border_size: None, - inactive_opacity: Some(InactiveOpacity(opacity)), - }) - } else { - None - } - } - - fn worktree_settings_content(&self) -> WorktreeSettingsContent { - WorktreeSettingsContent { - project_name: None, - prevent_sharing_in_public_channels: false, - file_scan_exclusions: self - .read_value("files.watcherExclude") - .and_then(|v| v.as_array()) - .map(|v| { - v.iter() - .filter_map(|n| n.as_str().map(str::to_owned)) - .collect::>() - }) - .filter(|r| !r.is_empty()), - file_scan_inclusions: self - .read_value("files.watcherInclude") - .and_then(|v| v.as_array()) - .map(|v| { - v.iter() - .filter_map(|n| n.as_str().map(str::to_owned)) - .collect::>() - }) - .filter(|r| !r.is_empty()), - private_files: None, - hidden_files: None, - } - } -} - -fn skip_default(value: T) -> Option { - if value == T::default() { - None - } else { - Some(value) - } -} diff --git a/crates/settings_json/Cargo.toml b/crates/settings_json/Cargo.toml deleted file mode 100644 index 2ba9887ca0..0000000000 --- a/crates/settings_json/Cargo.toml +++ /dev/null @@ -1,35 +0,0 @@ -[package] -name = "settings_json" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/settings_json.rs" - -[features] -default = [] - -[dependencies] -anyhow.workspace = true -tree-sitter.workspace = true -tree-sitter-json.workspace = true -util.workspace = true -serde.workspace = true -serde_json.workspace = true -serde_json_lenient.workspace = true -serde_path_to_error.workspace = true - -[dev-dependencies] -unindent.workspace = true -pretty_assertions.workspace = true - -# Uncomment other workspace dependencies as needed -# assistant.workspace = true -# client.workspace = true -# project.workspace = true -# settings.workspace = true diff --git a/crates/settings_json/LICENSE-GPL b/crates/settings_json/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/settings_json/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/settings_json/src/settings_json.rs b/crates/settings_json/src/settings_json.rs deleted file mode 100644 index 5198e475af..0000000000 --- a/crates/settings_json/src/settings_json.rs +++ /dev/null @@ -1,2635 +0,0 @@ -use anyhow::Result; -use serde::{Serialize, de::DeserializeOwned}; -use serde_json::Value; -use std::{ops::Range, sync::LazyLock}; -use tree_sitter::{Query, StreamingIterator as _}; -use util::RangeExt; - -pub fn update_value_in_json_text<'a>( - text: &mut String, - key_path: &mut Vec<&'a str>, - tab_size: usize, - old_value: &'a Value, - new_value: &'a Value, - edits: &mut Vec<(Range, String)>, -) { - // If the old and new values are both objects, then compare them key by key, - // preserving the comments and formatting of the unchanged parts. Otherwise, - // replace the old value with the new value. - if let (Value::Object(old_object), Value::Object(new_object)) = (old_value, new_value) { - for (key, old_sub_value) in old_object.iter() { - key_path.push(key); - if let Some(new_sub_value) = new_object.get(key) { - // Key exists in both old and new, recursively update - update_value_in_json_text( - text, - key_path, - tab_size, - old_sub_value, - new_sub_value, - edits, - ); - } else { - // Key was removed from new object, remove the entire key-value pair - let (range, replacement) = - replace_value_in_json_text(text, key_path, 0, None, None); - text.replace_range(range.clone(), &replacement); - edits.push((range, replacement)); - } - key_path.pop(); - } - for (key, new_sub_value) in new_object.iter() { - key_path.push(key); - if !old_object.contains_key(key) { - update_value_in_json_text( - text, - key_path, - tab_size, - &Value::Null, - new_sub_value, - edits, - ); - } - key_path.pop(); - } - } else if old_value != new_value { - let mut new_value = new_value.clone(); - if let Some(new_object) = new_value.as_object_mut() { - new_object.retain(|_, v| !v.is_null()); - } - let (range, replacement) = - replace_value_in_json_text(text, key_path, tab_size, Some(&new_value), None); - text.replace_range(range.clone(), &replacement); - edits.push((range, replacement)); - } -} - -/// * `replace_key` - When an exact key match according to `key_path` is found, replace the key with `replace_key` if `Some`. -pub fn replace_value_in_json_text>( - text: &str, - key_path: &[T], - tab_size: usize, - new_value: Option<&Value>, - replace_key: Option<&str>, -) -> (Range, String) { - static PAIR_QUERY: LazyLock = LazyLock::new(|| { - Query::new( - &tree_sitter_json::LANGUAGE.into(), - "(pair key: (string) @key value: (_) @value)", - ) - .expect("Failed to create PAIR_QUERY") - }); - - let mut parser = tree_sitter::Parser::new(); - parser - .set_language(&tree_sitter_json::LANGUAGE.into()) - .unwrap(); - let syntax_tree = parser.parse(text, None).unwrap(); - - let mut cursor = tree_sitter::QueryCursor::new(); - - let mut depth = 0; - let mut last_value_range = 0..0; - let mut first_key_start = None; - let mut existing_value_range = 0..text.len(); - - let mut matches = cursor.matches(&PAIR_QUERY, syntax_tree.root_node(), text.as_bytes()); - while let Some(mat) = matches.next() { - if mat.captures.len() != 2 { - continue; - } - - let key_range = mat.captures[0].node.byte_range(); - let value_range = mat.captures[1].node.byte_range(); - - // Don't enter sub objects until we find an exact - // match for the current keypath - if last_value_range.contains_inclusive(&value_range) { - continue; - } - - last_value_range = value_range.clone(); - - if key_range.start > existing_value_range.end { - break; - } - - first_key_start.get_or_insert(key_range.start); - - let found_key = text - .get(key_range.clone()) - .zip(key_path.get(depth)) - .and_then(|(key_text, key_path_value)| { - serde_json::to_string(key_path_value.as_ref()) - .ok() - .map(|key_path| depth < key_path.len() && key_text == key_path) - }) - .unwrap_or(false); - - if found_key { - existing_value_range = value_range; - // Reset last value range when increasing in depth - last_value_range = existing_value_range.start..existing_value_range.start; - depth += 1; - - if depth == key_path.len() { - break; - } - - if let Some(array_replacement) = handle_possible_array_value( - &mat.captures[0].node, - &mat.captures[1].node, - text, - &key_path[depth..], - new_value, - replace_key, - tab_size, - ) { - return array_replacement; - } - - first_key_start = None; - } - } - - // We found the exact key we want - if depth == key_path.len() { - if let Some(new_value) = new_value { - let new_val = to_pretty_json(new_value, tab_size, tab_size * depth); - if let Some(replace_key) = replace_key.and_then(|str| serde_json::to_string(str).ok()) { - let new_key = format!("{}: ", replace_key); - if let Some(key_start) = text[..existing_value_range.start].rfind('"') { - if let Some(prev_key_start) = text[..key_start].rfind('"') { - existing_value_range.start = prev_key_start; - } else { - existing_value_range.start = key_start; - } - } - (existing_value_range, new_key + &new_val) - } else { - (existing_value_range, new_val) - } - } else { - let mut removal_start = first_key_start.unwrap_or(existing_value_range.start); - let mut removal_end = existing_value_range.end; - - // Find the actual key position by looking for the key in the pair - // We need to extend the range to include the key, not just the value - if let Some(key_start) = text[..existing_value_range.start].rfind('"') { - if let Some(prev_key_start) = text[..key_start].rfind('"') { - removal_start = prev_key_start; - } else { - removal_start = key_start; - } - } - - let mut removed_comma = false; - // Look backward for a preceding comma first - let preceding_text = text.get(0..removal_start).unwrap_or(""); - if let Some(comma_pos) = preceding_text.rfind(',') { - // Check if there are only whitespace characters between the comma and our key - let between_comma_and_key = text.get(comma_pos + 1..removal_start).unwrap_or(""); - if between_comma_and_key.trim().is_empty() { - removal_start = comma_pos; - removed_comma = true; - } - } - if let Some(remaining_text) = text.get(existing_value_range.end..) - && !removed_comma - { - let mut chars = remaining_text.char_indices(); - while let Some((offset, ch)) = chars.next() { - if ch == ',' { - removal_end = existing_value_range.end + offset + 1; - // Also consume whitespace after the comma - for (_, next_ch) in chars.by_ref() { - if next_ch.is_whitespace() { - removal_end += next_ch.len_utf8(); - } else { - break; - } - } - break; - } else if !ch.is_whitespace() { - break; - } - } - } - (removal_start..removal_end, String::new()) - } - } else { - if let Some(first_key_start) = first_key_start { - // We have key paths, construct the sub objects - let new_key = key_path[depth].as_ref(); - // We don't have the key, construct the nested objects - let new_value = construct_json_value(&key_path[(depth + 1)..], new_value); - - let mut row = 0; - let mut column = 0; - for (ix, char) in text.char_indices() { - if ix == first_key_start { - break; - } - if char == '\n' { - row += 1; - column = 0; - } else { - column += char.len_utf8(); - } - } - - if row > 0 { - // depth is 0 based, but division needs to be 1 based. - let new_val = to_pretty_json(&new_value, column / (depth + 1), column); - let space = ' '; - let content = format!("\"{new_key}\": {new_val},\n{space:width$}", width = column); - (first_key_start..first_key_start, content) - } else { - let new_val = serde_json::to_string(&new_value).unwrap(); - let mut content = format!(r#""{new_key}": {new_val},"#); - content.push(' '); - (first_key_start..first_key_start, content) - } - } else { - // We don't have the key, construct the nested objects - let new_value = construct_json_value(&key_path[depth..], new_value); - let indent_prefix_len = tab_size * depth; - let mut new_val = to_pretty_json(&new_value, tab_size, indent_prefix_len); - if depth == 0 { - new_val.push('\n'); - } - // best effort to keep comments with best effort indentation - let mut replace_text = &text[existing_value_range.clone()]; - while let Some(comment_start) = replace_text.rfind("//") { - if let Some(comment_end) = replace_text[comment_start..].find('\n') { - let mut comment_with_indent_start = replace_text[..comment_start] - .rfind('\n') - .unwrap_or(comment_start); - if !replace_text[comment_with_indent_start..comment_start] - .trim() - .is_empty() - { - comment_with_indent_start = comment_start; - } - new_val.insert_str( - 1, - &replace_text[comment_with_indent_start..comment_start + comment_end], - ); - } - replace_text = &replace_text[..comment_start]; - } - - (existing_value_range, new_val) - } - } -} - -fn construct_json_value( - key_path: &[impl AsRef], - new_value: Option<&serde_json::Value>, -) -> serde_json::Value { - let mut new_value = - serde_json::to_value(new_value.unwrap_or(&serde_json::Value::Null)).unwrap(); - for key in key_path.iter().rev() { - if parse_index_key(key.as_ref()).is_some() { - new_value = serde_json::json!([new_value]); - } else { - new_value = serde_json::json!({ key.as_ref().to_string(): new_value }); - } - } - return new_value; -} - -fn parse_index_key(index_key: &str) -> Option { - index_key.strip_prefix('#')?.parse().ok() -} - -fn handle_possible_array_value( - key_node: &tree_sitter::Node, - value_node: &tree_sitter::Node, - text: &str, - remaining_key_path: &[impl AsRef], - new_value: Option<&Value>, - replace_key: Option<&str>, - tab_size: usize, -) -> Option<(Range, String)> { - if remaining_key_path.is_empty() { - return None; - } - let key_path = remaining_key_path; - let index = parse_index_key(key_path[0].as_ref())?; - - let value_is_array = value_node.kind() == TS_ARRAY_KIND; - - let array_str = if value_is_array { - &text[value_node.byte_range()] - } else { - "" - }; - - let (mut replace_range, mut replace_value) = replace_top_level_array_value_in_json_text( - array_str, - &key_path[1..], - new_value, - replace_key, - index, - tab_size, - ); - - if value_is_array { - replace_range.start += value_node.start_byte(); - replace_range.end += value_node.start_byte(); - } else { - // replace the full value if it wasn't an array - replace_range = value_node.byte_range(); - } - let non_whitespace_char_count = replace_value.len() - - replace_value - .chars() - .filter(char::is_ascii_whitespace) - .count(); - let needs_indent = replace_value.ends_with('\n') - || (replace_value - .chars() - .zip(replace_value.chars().skip(1)) - .any(|(c, next_c)| c == '\n' && !next_c.is_ascii_whitespace())); - let contains_comment = (replace_value.contains("//") && replace_value.contains('\n')) - || (replace_value.contains("/*") && replace_value.contains("*/")); - if needs_indent { - let indent_width = key_node.start_position().column; - let increased_indent = format!("\n{space:width$}", space = ' ', width = indent_width); - replace_value = replace_value.replace('\n', &increased_indent); - } else if non_whitespace_char_count < 32 && !contains_comment { - // remove indentation - while let Some(idx) = replace_value.find("\n ") { - replace_value.remove(idx); - } - while let Some(idx) = replace_value.find(" ") { - replace_value.remove(idx); - } - } - return Some((replace_range, replace_value)); -} - -const TS_DOCUMENT_KIND: &str = "document"; -const TS_ARRAY_KIND: &str = "array"; -const TS_COMMENT_KIND: &str = "comment"; - -pub fn replace_top_level_array_value_in_json_text( - text: &str, - key_path: &[impl AsRef], - new_value: Option<&Value>, - replace_key: Option<&str>, - array_index: usize, - tab_size: usize, -) -> (Range, String) { - let mut parser = tree_sitter::Parser::new(); - parser - .set_language(&tree_sitter_json::LANGUAGE.into()) - .unwrap(); - - let syntax_tree = parser.parse(text, None).unwrap(); - - let mut cursor = syntax_tree.walk(); - - if cursor.node().kind() == TS_DOCUMENT_KIND { - cursor.goto_first_child(); - } - - while cursor.node().kind() != TS_ARRAY_KIND { - if !cursor.goto_next_sibling() { - let json_value = construct_json_value(key_path, new_value); - let json_value = serde_json::json!([json_value]); - return (0..text.len(), to_pretty_json(&json_value, tab_size, 0)); - } - } - - // false if no children - // - cursor.goto_first_child(); - debug_assert_eq!(cursor.node().kind(), "["); - - let mut index = 0; - - while index <= array_index { - let node = cursor.node(); - if !matches!(node.kind(), "[" | "]" | TS_COMMENT_KIND | ",") - && !node.is_extra() - && !node.is_missing() - { - if index == array_index { - break; - } - index += 1; - } - if !cursor.goto_next_sibling() { - if let Some(new_value) = new_value { - return append_top_level_array_value_in_json_text(text, new_value, tab_size); - } else { - return (0..0, String::new()); - } - } - } - - let range = cursor.node().range(); - let indent_width = range.start_point.column; - let offset = range.start_byte; - let text_range = range.start_byte..range.end_byte; - let value_str = &text[text_range.clone()]; - let needs_indent = range.start_point.row > 0; - - if new_value.is_none() && key_path.is_empty() { - let mut remove_range = text_range; - if index == 0 { - while cursor.goto_next_sibling() - && (cursor.node().is_extra() || cursor.node().is_missing()) - {} - if cursor.node().kind() == "," { - remove_range.end = cursor.node().range().end_byte; - } - if let Some(next_newline) = &text[remove_range.end + 1..].find('\n') - && text[remove_range.end + 1..remove_range.end + next_newline] - .chars() - .all(|c| c.is_ascii_whitespace()) - { - remove_range.end = remove_range.end + next_newline; - } - } else { - while cursor.goto_previous_sibling() - && (cursor.node().is_extra() || cursor.node().is_missing()) - {} - if cursor.node().kind() == "," { - remove_range.start = cursor.node().range().start_byte; - } - } - (remove_range, String::new()) - } else { - if let Some(array_replacement) = handle_possible_array_value( - &cursor.node(), - &cursor.node(), - text, - key_path, - new_value, - replace_key, - tab_size, - ) { - return array_replacement; - } - let (mut replace_range, mut replace_value) = - replace_value_in_json_text(value_str, key_path, tab_size, new_value, replace_key); - - replace_range.start += offset; - replace_range.end += offset; - - if needs_indent { - let increased_indent = format!("\n{space:width$}", space = ' ', width = indent_width); - replace_value = replace_value.replace('\n', &increased_indent); - } else { - while let Some(idx) = replace_value.find("\n ") { - replace_value.remove(idx + 1); - } - while let Some(idx) = replace_value.find("\n") { - replace_value.replace_range(idx..idx + 1, " "); - } - } - - (replace_range, replace_value) - } -} - -pub fn append_top_level_array_value_in_json_text( - text: &str, - new_value: &Value, - tab_size: usize, -) -> (Range, String) { - let mut parser = tree_sitter::Parser::new(); - parser - .set_language(&tree_sitter_json::LANGUAGE.into()) - .unwrap(); - let syntax_tree = parser.parse(text, None).unwrap(); - - let mut cursor = syntax_tree.walk(); - - if cursor.node().kind() == TS_DOCUMENT_KIND { - cursor.goto_first_child(); - } - - while cursor.node().kind() != TS_ARRAY_KIND { - if !cursor.goto_next_sibling() { - let json_value = serde_json::json!([new_value]); - return (0..text.len(), to_pretty_json(&json_value, tab_size, 0)); - } - } - - let went_to_last_child = cursor.goto_last_child(); - debug_assert!( - went_to_last_child && cursor.node().kind() == "]", - "Malformed JSON syntax tree, expected `]` at end of array" - ); - let close_bracket_start = cursor.node().start_byte(); - while cursor.goto_previous_sibling() - && (cursor.node().is_extra() || cursor.node().is_missing()) - && !cursor.node().is_error() - {} - - let mut comma_range = None; - let mut prev_item_range = None; - - if cursor.node().kind() == "," || is_error_of_kind(&mut cursor, ",") { - comma_range = Some(cursor.node().byte_range()); - while cursor.goto_previous_sibling() - && (cursor.node().is_extra() || cursor.node().is_missing()) - {} - - debug_assert_ne!(cursor.node().kind(), "["); - prev_item_range = Some(cursor.node().range()); - } else { - while (cursor.node().is_extra() || cursor.node().is_missing()) - && cursor.goto_previous_sibling() - {} - if cursor.node().kind() != "[" { - prev_item_range = Some(cursor.node().range()); - } - } - - let (mut replace_range, mut replace_value) = - replace_value_in_json_text::<&str>("", &[], tab_size, Some(new_value), None); - - replace_range.start = close_bracket_start; - replace_range.end = close_bracket_start; - - let space = ' '; - if let Some(prev_item_range) = prev_item_range { - let needs_newline = prev_item_range.start_point.row > 0; - let indent_width = text[..prev_item_range.start_byte].rfind('\n').map_or( - prev_item_range.start_point.column, - |idx| { - prev_item_range.start_point.column - - text[idx + 1..prev_item_range.start_byte].trim_start().len() - }, - ); - - let prev_item_end = comma_range - .as_ref() - .map_or(prev_item_range.end_byte, |range| range.end); - if text[prev_item_end..replace_range.start].trim().is_empty() { - replace_range.start = prev_item_end; - } - - if needs_newline { - let increased_indent = format!("\n{space:width$}", width = indent_width); - replace_value = replace_value.replace('\n', &increased_indent); - replace_value.push('\n'); - replace_value.insert_str(0, &format!("\n{space:width$}", width = indent_width)); - } else { - while let Some(idx) = replace_value.find("\n ") { - replace_value.remove(idx + 1); - } - while let Some(idx) = replace_value.find('\n') { - replace_value.replace_range(idx..idx + 1, " "); - } - replace_value.insert(0, ' '); - } - - if comma_range.is_none() { - replace_value.insert(0, ','); - } - } else if replace_value.contains('\n') || text.contains('\n') { - if let Some(prev_newline) = text[..replace_range.start].rfind('\n') - && text[prev_newline..replace_range.start].trim().is_empty() - { - replace_range.start = prev_newline; - } - let indent = format!("\n{space:width$}", width = tab_size); - replace_value = replace_value.replace('\n', &indent); - replace_value.insert_str(0, &indent); - replace_value.push('\n'); - } - return (replace_range, replace_value); - - fn is_error_of_kind(cursor: &mut tree_sitter::TreeCursor<'_>, kind: &str) -> bool { - if cursor.node().kind() != "ERROR" { - return false; - } - - let descendant_index = cursor.descendant_index(); - let res = cursor.goto_first_child() && cursor.node().kind() == kind; - cursor.goto_descendant(descendant_index); - res - } -} - -/// Infers the indentation size used in JSON text by analyzing the tree structure. -/// Returns the detected indent size, or a default of 2 if no indentation is found. -pub fn infer_json_indent_size(text: &str) -> usize { - const MAX_INDENT_SIZE: usize = 64; - - let mut parser = tree_sitter::Parser::new(); - parser - .set_language(&tree_sitter_json::LANGUAGE.into()) - .unwrap(); - - let Some(syntax_tree) = parser.parse(text, None) else { - return 4; - }; - - let mut cursor = syntax_tree.walk(); - let mut indent_counts = [0u32; MAX_INDENT_SIZE]; - - // Traverse the tree to find indentation patterns - fn visit_node( - cursor: &mut tree_sitter::TreeCursor, - indent_counts: &mut [u32; MAX_INDENT_SIZE], - depth: usize, - ) { - if depth >= 3 { - return; - } - let node = cursor.node(); - let node_kind = node.kind(); - - // For objects and arrays, check the indentation of their first content child - if matches!(node_kind, "object" | "array") { - let container_column = node.start_position().column; - let container_row = node.start_position().row; - - if cursor.goto_first_child() { - // Skip the opening bracket - loop { - let child = cursor.node(); - let child_kind = child.kind(); - - // Look for the first actual content (pair for objects, value for arrays) - if (node_kind == "object" && child_kind == "pair") - || (node_kind == "array" - && !matches!(child_kind, "[" | "]" | "," | "comment")) - { - let child_column = child.start_position().column; - let child_row = child.start_position().row; - - // Only count if the child is on a different line - if child_row > container_row && child_column > container_column { - let indent = child_column - container_column; - if indent > 0 && indent < MAX_INDENT_SIZE { - indent_counts[indent] += 1; - } - } - break; - } - - if !cursor.goto_next_sibling() { - break; - } - } - cursor.goto_parent(); - } - } - - // Recurse to children - if cursor.goto_first_child() { - loop { - visit_node(cursor, indent_counts, depth + 1); - if !cursor.goto_next_sibling() { - break; - } - } - cursor.goto_parent(); - } - } - - visit_node(&mut cursor, &mut indent_counts, 0); - - // Find the indent size with the highest count - let mut max_count = 0; - let mut max_indent = 4; - - for (indent, &count) in indent_counts.iter().enumerate() { - if count > max_count { - max_count = count; - max_indent = indent; - } - } - - if max_count == 0 { 2 } else { max_indent } -} - -pub fn to_pretty_json( - value: &impl Serialize, - indent_size: usize, - indent_prefix_len: usize, -) -> String { - let mut output = Vec::new(); - let indent = " ".repeat(indent_size); - let mut ser = serde_json::Serializer::with_formatter( - &mut output, - serde_json::ser::PrettyFormatter::with_indent(indent.as_bytes()), - ); - - value.serialize(&mut ser).unwrap(); - let text = String::from_utf8(output).unwrap(); - - let mut adjusted_text = String::new(); - for (i, line) in text.split('\n').enumerate() { - if i > 0 { - adjusted_text.extend(std::iter::repeat(' ').take(indent_prefix_len)); - } - adjusted_text.push_str(line); - adjusted_text.push('\n'); - } - adjusted_text.pop(); - adjusted_text -} - -pub fn parse_json_with_comments(content: &str) -> Result { - let mut deserializer = serde_json_lenient::Deserializer::from_str(content); - Ok(serde_path_to_error::deserialize(&mut deserializer)?) -} - -#[cfg(test)] -mod tests { - use super::*; - use serde_json::{Value, json}; - use unindent::Unindent; - - #[test] - fn object_replace() { - #[track_caller] - fn check_object_replace( - input: String, - key_path: &[&str], - value: Option, - expected: String, - ) { - let result = replace_value_in_json_text(&input, key_path, 4, value.as_ref(), None); - let mut result_str = input; - result_str.replace_range(result.0, &result.1); - pretty_assertions::assert_eq!(expected, result_str); - } - check_object_replace( - r#"{ - "a": 1, - "b": 2 - }"# - .unindent(), - &["b"], - Some(json!(3)), - r#"{ - "a": 1, - "b": 3 - }"# - .unindent(), - ); - check_object_replace( - r#"{ - "a": 1, - "b": 2 - }"# - .unindent(), - &["b"], - None, - r#"{ - "a": 1 - }"# - .unindent(), - ); - check_object_replace( - r#"{ - "a": 1, - "b": 2 - }"# - .unindent(), - &["c"], - Some(json!(3)), - r#"{ - "c": 3, - "a": 1, - "b": 2 - }"# - .unindent(), - ); - check_object_replace( - r#"{ - "a": 1, - "b": { - "c": 2, - "d": 3, - } - }"# - .unindent(), - &["b", "c"], - Some(json!([1, 2, 3])), - r#"{ - "a": 1, - "b": { - "c": [ - 1, - 2, - 3 - ], - "d": 3, - } - }"# - .unindent(), - ); - - check_object_replace( - r#"{ - "name": "old_name", - "id": 123 - }"# - .unindent(), - &["name"], - Some(json!("new_name")), - r#"{ - "name": "new_name", - "id": 123 - }"# - .unindent(), - ); - - check_object_replace( - r#"{ - "enabled": false, - "count": 5 - }"# - .unindent(), - &["enabled"], - Some(json!(true)), - r#"{ - "enabled": true, - "count": 5 - }"# - .unindent(), - ); - - check_object_replace( - r#"{ - "value": null, - "other": "test" - }"# - .unindent(), - &["value"], - Some(json!(42)), - r#"{ - "value": 42, - "other": "test" - }"# - .unindent(), - ); - - check_object_replace( - r#"{ - "config": { - "old": true - }, - "name": "test" - }"# - .unindent(), - &["config"], - Some(json!({"new": false, "count": 3})), - r#"{ - "config": { - "new": false, - "count": 3 - }, - "name": "test" - }"# - .unindent(), - ); - - check_object_replace( - r#"{ - // This is a comment - "a": 1, - "b": 2 // Another comment - }"# - .unindent(), - &["b"], - Some(json!({"foo": "bar"})), - r#"{ - // This is a comment - "a": 1, - "b": { - "foo": "bar" - } // Another comment - }"# - .unindent(), - ); - - check_object_replace( - r#"{}"#.to_string(), - &["new_key"], - Some(json!("value")), - r#"{ - "new_key": "value" - } - "# - .unindent(), - ); - - check_object_replace( - r#"{ - "only_key": 123 - }"# - .unindent(), - &["only_key"], - None, - "{\n \n}".to_string(), - ); - - check_object_replace( - r#"{ - "level1": { - "level2": { - "level3": { - "target": "old" - } - } - } - }"# - .unindent(), - &["level1", "level2", "level3", "target"], - Some(json!("new")), - r#"{ - "level1": { - "level2": { - "level3": { - "target": "new" - } - } - } - }"# - .unindent(), - ); - - check_object_replace( - r#"{ - "parent": {} - }"# - .unindent(), - &["parent", "child"], - Some(json!("value")), - r#"{ - "parent": { - "child": "value" - } - }"# - .unindent(), - ); - - check_object_replace( - r#"{ - "a": 1, - "b": 2, - }"# - .unindent(), - &["b"], - Some(json!(3)), - r#"{ - "a": 1, - "b": 3, - }"# - .unindent(), - ); - - check_object_replace( - r#"{ - "items": [1, 2, 3], - "count": 3 - }"# - .unindent(), - &["items", "1"], - Some(json!(5)), - r#"{ - "items": { - "1": 5 - }, - "count": 3 - }"# - .unindent(), - ); - - check_object_replace( - r#"{ - "items": [1, 2, 3], - "count": 3 - }"# - .unindent(), - &["items", "1"], - None, - r#"{ - "items": { - "1": null - }, - "count": 3 - }"# - .unindent(), - ); - - check_object_replace( - r#"{ - "items": [1, 2, 3], - "count": 3 - }"# - .unindent(), - &["items"], - Some(json!(["a", "b", "c", "d"])), - r#"{ - "items": [ - "a", - "b", - "c", - "d" - ], - "count": 3 - }"# - .unindent(), - ); - - check_object_replace( - r#"{ - "0": "zero", - "1": "one" - }"# - .unindent(), - &["1"], - Some(json!("ONE")), - r#"{ - "0": "zero", - "1": "ONE" - }"# - .unindent(), - ); - // Test with comments between object members - check_object_replace( - r#"{ - "a": 1, - // Comment between members - "b": 2, - /* Block comment */ - "c": 3 - }"# - .unindent(), - &["b"], - Some(json!({"nested": true})), - r#"{ - "a": 1, - // Comment between members - "b": { - "nested": true - }, - /* Block comment */ - "c": 3 - }"# - .unindent(), - ); - - // Test with trailing comments on replaced value - check_object_replace( - r#"{ - "a": 1, // keep this comment - "b": 2 // this should stay - }"# - .unindent(), - &["a"], - Some(json!("changed")), - r#"{ - "a": "changed", // keep this comment - "b": 2 // this should stay - }"# - .unindent(), - ); - - // Test with deep indentation - check_object_replace( - r#"{ - "deeply": { - "nested": { - "value": "old" - } - } - }"# - .unindent(), - &["deeply", "nested", "value"], - Some(json!("new")), - r#"{ - "deeply": { - "nested": { - "value": "new" - } - } - }"# - .unindent(), - ); - - // Test removing value with comment preservation - check_object_replace( - r#"{ - // Header comment - "a": 1, - // This comment belongs to b - "b": 2, - // This comment belongs to c - "c": 3 - }"# - .unindent(), - &["b"], - None, - r#"{ - // Header comment - "a": 1, - // This comment belongs to b - // This comment belongs to c - "c": 3 - }"# - .unindent(), - ); - - // Test with multiline block comments - check_object_replace( - r#"{ - /* - * This is a multiline - * block comment - */ - "value": "old", - /* Another block */ "other": 123 - }"# - .unindent(), - &["value"], - Some(json!("new")), - r#"{ - /* - * This is a multiline - * block comment - */ - "value": "new", - /* Another block */ "other": 123 - }"# - .unindent(), - ); - - check_object_replace( - r#"{ - // This object is empty - }"# - .unindent(), - &["key"], - Some(json!("value")), - r#"{ - // This object is empty - "key": "value" - } - "# - .unindent(), - ); - - // Test replacing in object with only comments - check_object_replace( - r#"{ - // Comment 1 - // Comment 2 - }"# - .unindent(), - &["new"], - Some(json!(42)), - r#"{ - // Comment 1 - // Comment 2 - "new": 42 - } - "# - .unindent(), - ); - - // Test with inconsistent spacing - check_object_replace( - r#"{ - "a":1, - "b" : 2 , - "c": 3 - }"# - .unindent(), - &["b"], - Some(json!("spaced")), - r#"{ - "a":1, - "b" : "spaced" , - "c": 3 - }"# - .unindent(), - ); - } - - #[test] - fn object_replace_array() { - // Tests replacing values within arrays that are nested inside objects. - // Uses "#N" syntax in key paths to indicate array indices. - #[track_caller] - fn check_object_replace_array( - input: String, - key_path: &[&str], - value: Option, - expected: String, - ) { - let result = replace_value_in_json_text(&input, key_path, 4, value.as_ref(), None); - let mut result_str = input; - result_str.replace_range(result.0, &result.1); - pretty_assertions::assert_eq!(expected, result_str); - } - - // Basic array element replacement - check_object_replace_array( - r#"{ - "a": [1, 3], - }"# - .unindent(), - &["a", "#1"], - Some(json!(2)), - r#"{ - "a": [1, 2], - }"# - .unindent(), - ); - - // Replace first element - check_object_replace_array( - r#"{ - "items": [1, 2, 3] - }"# - .unindent(), - &["items", "#0"], - Some(json!(10)), - r#"{ - "items": [10, 2, 3] - }"# - .unindent(), - ); - - // Replace last element - check_object_replace_array( - r#"{ - "items": [1, 2, 3] - }"# - .unindent(), - &["items", "#2"], - Some(json!(30)), - r#"{ - "items": [1, 2, 30] - }"# - .unindent(), - ); - - // Replace string in array - check_object_replace_array( - r#"{ - "names": ["alice", "bob", "charlie"] - }"# - .unindent(), - &["names", "#1"], - Some(json!("robert")), - r#"{ - "names": ["alice", "robert", "charlie"] - }"# - .unindent(), - ); - - // Replace boolean - check_object_replace_array( - r#"{ - "flags": [true, false, true] - }"# - .unindent(), - &["flags", "#0"], - Some(json!(false)), - r#"{ - "flags": [false, false, true] - }"# - .unindent(), - ); - - // Replace null with value - check_object_replace_array( - r#"{ - "values": [null, 2, null] - }"# - .unindent(), - &["values", "#0"], - Some(json!(1)), - r#"{ - "values": [1, 2, null] - }"# - .unindent(), - ); - - // Replace value with null - check_object_replace_array( - r#"{ - "data": [1, 2, 3] - }"# - .unindent(), - &["data", "#1"], - Some(json!(null)), - r#"{ - "data": [1, null, 3] - }"# - .unindent(), - ); - - // Replace simple value with object - check_object_replace_array( - r#"{ - "list": [1, 2, 3] - }"# - .unindent(), - &["list", "#1"], - Some(json!({"value": 2, "label": "two"})), - r#"{ - "list": [1, { "value": 2, "label": "two" }, 3] - }"# - .unindent(), - ); - - // Replace simple value with nested array - check_object_replace_array( - r#"{ - "matrix": [1, 2, 3] - }"# - .unindent(), - &["matrix", "#1"], - Some(json!([20, 21, 22])), - r#"{ - "matrix": [1, [ 20, 21, 22 ], 3] - }"# - .unindent(), - ); - - // Replace object in array - check_object_replace_array( - r#"{ - "users": [ - {"name": "alice"}, - {"name": "bob"}, - {"name": "charlie"} - ] - }"# - .unindent(), - &["users", "#1"], - Some(json!({"name": "robert", "age": 30})), - r#"{ - "users": [ - {"name": "alice"}, - { "name": "robert", "age": 30 }, - {"name": "charlie"} - ] - }"# - .unindent(), - ); - - // Replace property within object in array - check_object_replace_array( - r#"{ - "users": [ - {"name": "alice", "age": 25}, - {"name": "bob", "age": 30}, - {"name": "charlie", "age": 35} - ] - }"# - .unindent(), - &["users", "#1", "age"], - Some(json!(31)), - r#"{ - "users": [ - {"name": "alice", "age": 25}, - {"name": "bob", "age": 31}, - {"name": "charlie", "age": 35} - ] - }"# - .unindent(), - ); - - // Add new property to object in array - check_object_replace_array( - r#"{ - "items": [ - {"id": 1}, - {"id": 2}, - {"id": 3} - ] - }"# - .unindent(), - &["items", "#1", "name"], - Some(json!("Item Two")), - r#"{ - "items": [ - {"id": 1}, - {"name": "Item Two", "id": 2}, - {"id": 3} - ] - }"# - .unindent(), - ); - - // Remove property from object in array - check_object_replace_array( - r#"{ - "items": [ - {"id": 1, "name": "one"}, - {"id": 2, "name": "two"}, - {"id": 3, "name": "three"} - ] - }"# - .unindent(), - &["items", "#1", "name"], - None, - r#"{ - "items": [ - {"id": 1, "name": "one"}, - {"id": 2}, - {"id": 3, "name": "three"} - ] - }"# - .unindent(), - ); - - // Deeply nested: array in object in array - check_object_replace_array( - r#"{ - "data": [ - { - "values": [1, 2, 3] - }, - { - "values": [4, 5, 6] - } - ] - }"# - .unindent(), - &["data", "#0", "values", "#1"], - Some(json!(20)), - r#"{ - "data": [ - { - "values": [1, 20, 3] - }, - { - "values": [4, 5, 6] - } - ] - }"# - .unindent(), - ); - - // Multiple levels of nesting - check_object_replace_array( - r#"{ - "root": { - "level1": [ - { - "level2": { - "level3": [10, 20, 30] - } - } - ] - } - }"# - .unindent(), - &["root", "level1", "#0", "level2", "level3", "#2"], - Some(json!(300)), - r#"{ - "root": { - "level1": [ - { - "level2": { - "level3": [10, 20, 300] - } - } - ] - } - }"# - .unindent(), - ); - - // Array with mixed types - check_object_replace_array( - r#"{ - "mixed": [1, "two", true, null, {"five": 5}] - }"# - .unindent(), - &["mixed", "#3"], - Some(json!({"four": 4})), - r#"{ - "mixed": [1, "two", true, { "four": 4 }, {"five": 5}] - }"# - .unindent(), - ); - - // Replace with complex object - check_object_replace_array( - r#"{ - "config": [ - "simple", - "values" - ] - }"# - .unindent(), - &["config", "#0"], - Some(json!({ - "type": "complex", - "settings": { - "enabled": true, - "level": 5 - } - })), - r#"{ - "config": [ - { - "type": "complex", - "settings": { - "enabled": true, - "level": 5 - } - }, - "values" - ] - }"# - .unindent(), - ); - - // Array with trailing comma - check_object_replace_array( - r#"{ - "items": [ - 1, - 2, - 3, - ] - }"# - .unindent(), - &["items", "#1"], - Some(json!(20)), - r#"{ - "items": [ - 1, - 20, - 3, - ] - }"# - .unindent(), - ); - - // Array with comments - check_object_replace_array( - r#"{ - "items": [ - 1, // first item - 2, // second item - 3 // third item - ] - }"# - .unindent(), - &["items", "#1"], - Some(json!(20)), - r#"{ - "items": [ - 1, // first item - 20, // second item - 3 // third item - ] - }"# - .unindent(), - ); - - // Multiple arrays in object - check_object_replace_array( - r#"{ - "first": [1, 2, 3], - "second": [4, 5, 6], - "third": [7, 8, 9] - }"# - .unindent(), - &["second", "#1"], - Some(json!(50)), - r#"{ - "first": [1, 2, 3], - "second": [4, 50, 6], - "third": [7, 8, 9] - }"# - .unindent(), - ); - - // Empty array - add first element - check_object_replace_array( - r#"{ - "empty": [] - }"# - .unindent(), - &["empty", "#0"], - Some(json!("first")), - r#"{ - "empty": ["first"] - }"# - .unindent(), - ); - - // Array of arrays - check_object_replace_array( - r#"{ - "matrix": [ - [1, 2], - [3, 4], - [5, 6] - ] - }"# - .unindent(), - &["matrix", "#1", "#0"], - Some(json!(30)), - r#"{ - "matrix": [ - [1, 2], - [30, 4], - [5, 6] - ] - }"# - .unindent(), - ); - - // Replace nested object property in array element - check_object_replace_array( - r#"{ - "users": [ - { - "name": "alice", - "address": { - "city": "NYC", - "zip": "10001" - } - } - ] - }"# - .unindent(), - &["users", "#0", "address", "city"], - Some(json!("Boston")), - r#"{ - "users": [ - { - "name": "alice", - "address": { - "city": "Boston", - "zip": "10001" - } - } - ] - }"# - .unindent(), - ); - - // Add element past end of array - check_object_replace_array( - r#"{ - "items": [1, 2] - }"# - .unindent(), - &["items", "#5"], - Some(json!(6)), - r#"{ - "items": [1, 2, 6] - }"# - .unindent(), - ); - - // Complex nested structure - check_object_replace_array( - r#"{ - "app": { - "modules": [ - { - "name": "auth", - "routes": [ - {"path": "/login", "method": "POST"}, - {"path": "/logout", "method": "POST"} - ] - }, - { - "name": "api", - "routes": [ - {"path": "/users", "method": "GET"}, - {"path": "/users", "method": "POST"} - ] - } - ] - } - }"# - .unindent(), - &["app", "modules", "#1", "routes", "#0", "method"], - Some(json!("PUT")), - r#"{ - "app": { - "modules": [ - { - "name": "auth", - "routes": [ - {"path": "/login", "method": "POST"}, - {"path": "/logout", "method": "POST"} - ] - }, - { - "name": "api", - "routes": [ - {"path": "/users", "method": "PUT"}, - {"path": "/users", "method": "POST"} - ] - } - ] - } - }"# - .unindent(), - ); - - // Escaped strings in array - check_object_replace_array( - r#"{ - "messages": ["hello", "world"] - }"# - .unindent(), - &["messages", "#0"], - Some(json!("hello \"quoted\" world")), - r#"{ - "messages": ["hello \"quoted\" world", "world"] - }"# - .unindent(), - ); - - // Block comments - check_object_replace_array( - r#"{ - "data": [ - /* first */ 1, - /* second */ 2, - /* third */ 3 - ] - }"# - .unindent(), - &["data", "#1"], - Some(json!(20)), - r#"{ - "data": [ - /* first */ 1, - /* second */ 20, - /* third */ 3 - ] - }"# - .unindent(), - ); - - // Inline array - check_object_replace_array( - r#"{"items": [1, 2, 3], "count": 3}"#.to_string(), - &["items", "#1"], - Some(json!(20)), - r#"{"items": [1, 20, 3], "count": 3}"#.to_string(), - ); - - // Single element array - check_object_replace_array( - r#"{ - "single": [42] - }"# - .unindent(), - &["single", "#0"], - Some(json!(100)), - r#"{ - "single": [100] - }"# - .unindent(), - ); - - // Inconsistent formatting - check_object_replace_array( - r#"{ - "messy": [1, - 2, - 3, - 4] - }"# - .unindent(), - &["messy", "#2"], - Some(json!(30)), - r#"{ - "messy": [1, - 2, - 30, - 4] - }"# - .unindent(), - ); - - // Creates array if has numbered key - check_object_replace_array( - r#"{ - "array": {"foo": "bar"} - }"# - .unindent(), - &["array", "#3"], - Some(json!(4)), - r#"{ - "array": [ - 4 - ] - }"# - .unindent(), - ); - - // Replace non-array element within array with array - check_object_replace_array( - r#"{ - "matrix": [ - [1, 2], - [3, 4], - [5, 6] - ] - }"# - .unindent(), - &["matrix", "#1", "#0"], - Some(json!(["foo", "bar"])), - r#"{ - "matrix": [ - [1, 2], - [[ "foo", "bar" ], 4], - [5, 6] - ] - }"# - .unindent(), - ); - // Replace non-array element within array with array - check_object_replace_array( - r#"{ - "matrix": [ - [1, 2], - [3, 4], - [5, 6] - ] - }"# - .unindent(), - &["matrix", "#1", "#0", "#3"], - Some(json!(["foo", "bar"])), - r#"{ - "matrix": [ - [1, 2], - [[ [ "foo", "bar" ] ], 4], - [5, 6] - ] - }"# - .unindent(), - ); - - // Create array in key that doesn't exist - check_object_replace_array( - r#"{ - "foo": {} - }"# - .unindent(), - &["foo", "bar", "#0"], - Some(json!({"is_object": true})), - r#"{ - "foo": { - "bar": [ - { - "is_object": true - } - ] - } - }"# - .unindent(), - ); - } - - #[test] - fn array_replace() { - #[track_caller] - fn check_array_replace( - input: impl ToString, - index: usize, - key_path: &[&str], - value: Option, - expected: impl ToString, - ) { - let input = input.to_string(); - let result = replace_top_level_array_value_in_json_text( - &input, - key_path, - value.as_ref(), - None, - index, - 4, - ); - let mut result_str = input; - result_str.replace_range(result.0, &result.1); - pretty_assertions::assert_eq!(expected.to_string(), result_str); - } - - check_array_replace(r#"[1, 3, 3]"#, 1, &[], Some(json!(2)), r#"[1, 2, 3]"#); - check_array_replace(r#"[1, 3, 3]"#, 2, &[], Some(json!(2)), r#"[1, 3, 2]"#); - check_array_replace(r#"[1, 3, 3,]"#, 3, &[], Some(json!(2)), r#"[1, 3, 3, 2]"#); - check_array_replace(r#"[1, 3, 3,]"#, 100, &[], Some(json!(2)), r#"[1, 3, 3, 2]"#); - check_array_replace( - r#"[ - 1, - 2, - 3, - ]"# - .unindent(), - 1, - &[], - Some(json!({"foo": "bar", "baz": "qux"})), - r#"[ - 1, - { - "foo": "bar", - "baz": "qux" - }, - 3, - ]"# - .unindent(), - ); - check_array_replace( - r#"[1, 3, 3,]"#, - 1, - &[], - Some(json!({"foo": "bar", "baz": "qux"})), - r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#, - ); - - check_array_replace( - r#"[1, { "foo": "bar", "baz": "qux" }, 3,]"#, - 1, - &["baz"], - Some(json!({"qux": "quz"})), - r#"[1, { "foo": "bar", "baz": { "qux": "quz" } }, 3,]"#, - ); - - check_array_replace( - r#"[ - 1, - { - "foo": "bar", - "baz": "qux" - }, - 3 - ]"#, - 1, - &["baz"], - Some(json!({"qux": "quz"})), - r#"[ - 1, - { - "foo": "bar", - "baz": { - "qux": "quz" - } - }, - 3 - ]"#, - ); - - check_array_replace( - r#"[ - 1, - { - "foo": "bar", - "baz": { - "qux": "quz" - } - }, - 3 - ]"#, - 1, - &["baz"], - Some(json!("qux")), - r#"[ - 1, - { - "foo": "bar", - "baz": "qux" - }, - 3 - ]"#, - ); - - check_array_replace( - r#"[ - 1, - { - "foo": "bar", - // some comment to keep - "baz": { - // some comment to remove - "qux": "quz" - } - // some other comment to keep - }, - 3 - ]"#, - 1, - &["baz"], - Some(json!("qux")), - r#"[ - 1, - { - "foo": "bar", - // some comment to keep - "baz": "qux" - // some other comment to keep - }, - 3 - ]"#, - ); - - // Test with comments between array elements - check_array_replace( - r#"[ - 1, - // This is element 2 - 2, - /* Block comment */ 3, - 4 // Trailing comment - ]"#, - 2, - &[], - Some(json!("replaced")), - r#"[ - 1, - // This is element 2 - 2, - /* Block comment */ "replaced", - 4 // Trailing comment - ]"#, - ); - - // Test empty array with comments - check_array_replace( - r#"[ - // Empty array with comment - ]"# - .unindent(), - 0, - &[], - Some(json!("first")), - r#"[ - // Empty array with comment - "first" - ]"# - .unindent(), - ); - check_array_replace( - r#"[]"#.unindent(), - 0, - &[], - Some(json!("first")), - r#"["first"]"#.unindent(), - ); - - // Test array with leading comments - check_array_replace( - r#"[ - // Leading comment - // Another leading comment - 1, - 2 - ]"#, - 0, - &[], - Some(json!({"new": "object"})), - r#"[ - // Leading comment - // Another leading comment - { - "new": "object" - }, - 2 - ]"#, - ); - - // Test with deep indentation - check_array_replace( - r#"[ - 1, - 2, - 3 - ]"#, - 1, - &[], - Some(json!("deep")), - r#"[ - 1, - "deep", - 3 - ]"#, - ); - - // Test with mixed spacing - check_array_replace( - r#"[1,2, 3, 4]"#, - 2, - &[], - Some(json!("spaced")), - r#"[1,2, "spaced", 4]"#, - ); - - // Test replacing nested array element - check_array_replace( - r#"[ - [1, 2, 3], - [4, 5, 6], - [7, 8, 9] - ]"#, - 1, - &[], - Some(json!(["a", "b", "c", "d"])), - r#"[ - [1, 2, 3], - [ - "a", - "b", - "c", - "d" - ], - [7, 8, 9] - ]"#, - ); - - // Test with multiline block comments - check_array_replace( - r#"[ - /* - * This is a - * multiline comment - */ - "first", - "second" - ]"#, - 0, - &[], - Some(json!("updated")), - r#"[ - /* - * This is a - * multiline comment - */ - "updated", - "second" - ]"#, - ); - - // Test replacing with null - check_array_replace( - r#"[true, false, true]"#, - 1, - &[], - Some(json!(null)), - r#"[true, null, true]"#, - ); - - // Test single element array - check_array_replace( - r#"[42]"#, - 0, - &[], - Some(json!({"answer": 42})), - r#"[{ "answer": 42 }]"#, - ); - - // Test array with only comments - check_array_replace( - r#"[ - // Comment 1 - // Comment 2 - // Comment 3 - ]"# - .unindent(), - 10, - &[], - Some(json!(123)), - r#"[ - // Comment 1 - // Comment 2 - // Comment 3 - 123 - ]"# - .unindent(), - ); - - check_array_replace( - r#"[ - { - "key": "value" - }, - { - "key": "value2" - } - ]"# - .unindent(), - 0, - &[], - None, - r#"[ - { - "key": "value2" - } - ]"# - .unindent(), - ); - - check_array_replace( - r#"[ - { - "key": "value" - }, - { - "key": "value2" - }, - { - "key": "value3" - }, - ]"# - .unindent(), - 1, - &[], - None, - r#"[ - { - "key": "value" - }, - { - "key": "value3" - }, - ]"# - .unindent(), - ); - - check_array_replace( - r#""#, - 2, - &[], - Some(json!(42)), - r#"[ - 42 - ]"# - .unindent(), - ); - - check_array_replace( - r#""#, - 2, - &["foo", "bar"], - Some(json!(42)), - r#"[ - { - "foo": { - "bar": 42 - } - } - ]"# - .unindent(), - ); - } - - #[test] - fn array_append() { - #[track_caller] - fn check_array_append(input: impl ToString, value: Value, expected: impl ToString) { - let input = input.to_string(); - let result = append_top_level_array_value_in_json_text(&input, &value, 4); - let mut result_str = input; - result_str.replace_range(result.0, &result.1); - pretty_assertions::assert_eq!(expected.to_string(), result_str); - } - check_array_append(r#"[1, 3, 3]"#, json!(4), r#"[1, 3, 3, 4]"#); - check_array_append(r#"[1, 3, 3,]"#, json!(4), r#"[1, 3, 3, 4]"#); - check_array_append(r#"[1, 3, 3 ]"#, json!(4), r#"[1, 3, 3, 4]"#); - check_array_append(r#"[1, 3, 3, ]"#, json!(4), r#"[1, 3, 3, 4]"#); - check_array_append( - r#"[ - 1, - 2, - 3 - ]"# - .unindent(), - json!(4), - r#"[ - 1, - 2, - 3, - 4 - ]"# - .unindent(), - ); - check_array_append( - r#"[ - 1, - 2, - 3, - ]"# - .unindent(), - json!(4), - r#"[ - 1, - 2, - 3, - 4 - ]"# - .unindent(), - ); - check_array_append( - r#"[ - 1, - 2, - 3, - ]"# - .unindent(), - json!({"foo": "bar", "baz": "qux"}), - r#"[ - 1, - 2, - 3, - { - "foo": "bar", - "baz": "qux" - } - ]"# - .unindent(), - ); - check_array_append( - r#"[ 1, 2, 3, ]"#.unindent(), - json!({"foo": "bar", "baz": "qux"}), - r#"[ 1, 2, 3, { "foo": "bar", "baz": "qux" }]"#.unindent(), - ); - check_array_append( - r#"[]"#, - json!({"foo": "bar"}), - r#"[ - { - "foo": "bar" - } - ]"# - .unindent(), - ); - - // Test with comments between array elements - check_array_append( - r#"[ - 1, - // Comment between elements - 2, - /* Block comment */ 3 - ]"# - .unindent(), - json!(4), - r#"[ - 1, - // Comment between elements - 2, - /* Block comment */ 3, - 4 - ]"# - .unindent(), - ); - - // Test with trailing comment on last element - check_array_append( - r#"[ - 1, - 2, - 3 // Trailing comment - ]"# - .unindent(), - json!("new"), - r#"[ - 1, - 2, - 3 // Trailing comment - , - "new" - ]"# - .unindent(), - ); - - // Test empty array with comments - check_array_append( - r#"[ - // Empty array with comment - ]"# - .unindent(), - json!("first"), - r#"[ - // Empty array with comment - "first" - ]"# - .unindent(), - ); - - // Test with multiline block comment at end - check_array_append( - r#"[ - 1, - 2 - /* - * This is a - * multiline comment - */ - ]"# - .unindent(), - json!(3), - r#"[ - 1, - 2 - /* - * This is a - * multiline comment - */ - , - 3 - ]"# - .unindent(), - ); - - // Test with deep indentation - check_array_append( - r#"[ - 1, - 2, - 3 - ]"# - .unindent(), - json!("deep"), - r#"[ - 1, - 2, - 3, - "deep" - ]"# - .unindent(), - ); - - // Test with no spacing - check_array_append(r#"[1,2,3]"#, json!(4), r#"[1,2,3, 4]"#); - - // Test appending complex nested structure - check_array_append( - r#"[ - {"a": 1}, - {"b": 2} - ]"# - .unindent(), - json!({"c": {"nested": [1, 2, 3]}}), - r#"[ - {"a": 1}, - {"b": 2}, - { - "c": { - "nested": [ - 1, - 2, - 3 - ] - } - } - ]"# - .unindent(), - ); - - // Test array ending with comment after bracket - check_array_append( - r#"[ - 1, - 2, - 3 - ] // Comment after array"# - .unindent(), - json!(4), - r#"[ - 1, - 2, - 3, - 4 - ] // Comment after array"# - .unindent(), - ); - - // Test with inconsistent element formatting - check_array_append( - r#"[1, - 2, - 3, - ]"# - .unindent(), - json!(4), - r#"[1, - 2, - 3, - 4 - ]"# - .unindent(), - ); - - // Test appending to single-line array with trailing comma - check_array_append( - r#"[1, 2, 3,]"#, - json!({"key": "value"}), - r#"[1, 2, 3, { "key": "value" }]"#, - ); - - // Test appending null value - check_array_append(r#"[true, false]"#, json!(null), r#"[true, false, null]"#); - - // Test appending to array with only comments - check_array_append( - r#"[ - // Just comments here - // More comments - ]"# - .unindent(), - json!(42), - r#"[ - // Just comments here - // More comments - 42 - ]"# - .unindent(), - ); - - check_array_append( - r#""#, - json!(42), - r#"[ - 42 - ]"# - .unindent(), - ) - } - - #[test] - fn test_infer_json_indent_size() { - let json_2_spaces = r#"{ - "key1": "value1", - "nested": { - "key2": "value2", - "array": [ - 1, - 2, - 3 - ] - } -}"#; - assert_eq!(infer_json_indent_size(json_2_spaces), 2); - - let json_4_spaces = r#"{ - "key1": "value1", - "nested": { - "key2": "value2", - "array": [ - 1, - 2, - 3 - ] - } -}"#; - assert_eq!(infer_json_indent_size(json_4_spaces), 4); - - let json_8_spaces = r#"{ - "key1": "value1", - "nested": { - "key2": "value2" - } -}"#; - assert_eq!(infer_json_indent_size(json_8_spaces), 8); - - let json_single_line = r#"{"key": "value", "nested": {"inner": "data"}}"#; - assert_eq!(infer_json_indent_size(json_single_line), 2); - - let json_empty = r#"{}"#; - assert_eq!(infer_json_indent_size(json_empty), 2); - - let json_array = r#"[ - { - "id": 1, - "name": "first" - }, - { - "id": 2, - "name": "second" - } -]"#; - assert_eq!(infer_json_indent_size(json_array), 2); - - let json_mixed = r#"{ - "a": { - "b": { - "c": "value" - } - }, - "d": "value2" -}"#; - assert_eq!(infer_json_indent_size(json_mixed), 2); - } -} diff --git a/crates/settings_macros/Cargo.toml b/crates/settings_macros/Cargo.toml deleted file mode 100644 index 175c2f26a3..0000000000 --- a/crates/settings_macros/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "settings_macros" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lib] -path = "src/settings_macros.rs" -proc-macro = true - -[lints] -workspace = true - -[features] -default = [] - -[dependencies] -quote.workspace = true -syn.workspace = true - -[dev-dependencies] -settings.workspace = true diff --git a/crates/settings_macros/LICENSE-GPL b/crates/settings_macros/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/settings_macros/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/settings_macros/src/settings_macros.rs b/crates/settings_macros/src/settings_macros.rs deleted file mode 100644 index bad786991d..0000000000 --- a/crates/settings_macros/src/settings_macros.rs +++ /dev/null @@ -1,152 +0,0 @@ -use proc_macro::TokenStream; - -use quote::quote; -use syn::{ - Data, DeriveInput, Field, Fields, ItemEnum, ItemStruct, Type, parse_macro_input, parse_quote, -}; - -/// Derives the `MergeFrom` trait for a struct. -/// -/// This macro automatically implements `MergeFrom` by calling `merge_from` -/// on all fields in the struct. -/// -/// # Example -/// -/// ```ignore -/// #[derive(Clone, MergeFrom)] -/// struct MySettings { -/// field1: Option, -/// field2: SomeOtherSettings, -/// } -/// ``` -#[proc_macro_derive(MergeFrom)] -pub fn derive_merge_from(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - - let name = &input.ident; - let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); - - let merge_body = match &input.data { - Data::Struct(data_struct) => match &data_struct.fields { - Fields::Named(fields) => { - let field_merges = fields.named.iter().map(|field| { - let field_name = &field.ident; - quote! { - self.#field_name.merge_from(&other.#field_name); - } - }); - - quote! { - #(#field_merges)* - } - } - Fields::Unnamed(fields) => { - let field_merges = fields.unnamed.iter().enumerate().map(|(i, _)| { - let field_index = syn::Index::from(i); - quote! { - self.#field_index.merge_from(&other.#field_index); - } - }); - - quote! { - #(#field_merges)* - } - } - Fields::Unit => { - quote! { - // No fields to merge for unit structs - } - } - }, - Data::Enum(_) => { - quote! { - *self = other.clone(); - } - } - Data::Union(_) => { - panic!("MergeFrom cannot be derived for unions"); - } - }; - - let expanded = quote! { - impl #impl_generics crate::merge_from::MergeFrom for #name #ty_generics #where_clause { - fn merge_from(&mut self, other: &Self) { - use crate::merge_from::MergeFrom as _; - #merge_body - } - } - }; - - TokenStream::from(expanded) -} - -/// Registers the setting type with the SettingsStore. Note that you need to -/// have `gpui` in your dependencies for this to work. -#[proc_macro_derive(RegisterSetting)] -pub fn derive_register_setting(input: TokenStream) -> TokenStream { - let input = syn::parse_macro_input!(input as DeriveInput); - let type_name = &input.ident; - - quote! { - settings::private::inventory::submit! { - settings::private::RegisteredSetting { - settings_value: || { - Box::new(settings::private::SettingValue::<#type_name> { - global_value: None, - local_values: Vec::new(), - }) - }, - from_settings: |content| Box::new(<#type_name as settings::Settings>::from_settings(content)), - id: || std::any::TypeId::of::<#type_name>(), - } - } - } - .into() -} - -// Adds serde attributes to each field with type Option: -// #serde(default, skip_serializing_if = "Option::is_none", deserialize_with = "settings::deserialize_fallible") -#[proc_macro_attribute] -pub fn with_fallible_options(_args: TokenStream, input: TokenStream) -> TokenStream { - fn apply_on_fields(fields: &mut Fields) { - match fields { - Fields::Unit => {} - Fields::Named(fields) => { - for field in &mut fields.named { - add_if_option(field) - } - } - Fields::Unnamed(fields) => { - for field in &mut fields.unnamed { - add_if_option(field) - } - } - } - } - - fn add_if_option(field: &mut Field) { - match &field.ty { - Type::Path(syn::TypePath { qself: None, path }) - if path.leading_colon.is_none() - && path.segments.len() == 1 - && path.segments[0].ident == "Option" => {} - _ => return, - } - let attr = parse_quote!( - #[serde(default, skip_serializing_if = "Option::is_none", deserialize_with="crate::fallible_options::deserialize")] - ); - field.attrs.push(attr); - } - - if let Ok(mut input) = syn::parse::(input.clone()) { - apply_on_fields(&mut input.fields); - quote!(#input).into() - } else if let Ok(mut input) = syn::parse::(input) { - for variant in &mut input.variants { - apply_on_fields(&mut variant.fields); - } - quote!(#input).into() - } else { - panic!("with_fallible_options can only be applied to struct or enum definitions."); - } -} diff --git a/crates/settings_profile_selector/Cargo.toml b/crates/settings_profile_selector/Cargo.toml deleted file mode 100644 index 23ccac2e43..0000000000 --- a/crates/settings_profile_selector/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "settings_profile_selector" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/settings_profile_selector.rs" -doctest = false - -[dependencies] -fuzzy.workspace = true -gpui.workspace = true -picker.workspace = true -settings.workspace = true -ui.workspace = true -workspace.workspace = true -zed_actions.workspace = true - -[dev-dependencies] -client = { workspace = true, features = ["test-support"] } -editor = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -language = { workspace = true, features = ["test-support"] } -menu.workspace = true -project = { workspace = true, features = ["test-support"] } -serde_json.workspace = true -settings = { workspace = true, features = ["test-support"] } -theme = { workspace = true, features = ["test-support"] } -workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/settings_profile_selector/LICENSE-GPL b/crates/settings_profile_selector/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/settings_profile_selector/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/settings_profile_selector/src/settings_profile_selector.rs b/crates/settings_profile_selector/src/settings_profile_selector.rs deleted file mode 100644 index 42d714283a..0000000000 --- a/crates/settings_profile_selector/src/settings_profile_selector.rs +++ /dev/null @@ -1,612 +0,0 @@ -use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; -use gpui::{ - App, Context, DismissEvent, Entity, EventEmitter, Focusable, Render, Task, WeakEntity, Window, -}; -use picker::{Picker, PickerDelegate}; -use settings::{ActiveSettingsProfileName, SettingsStore}; -use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*}; -use workspace::{ModalView, Workspace}; - -pub fn init(cx: &mut App) { - cx.on_action(|_: &zed_actions::settings_profile_selector::Toggle, cx| { - workspace::with_active_or_new_workspace(cx, |workspace, window, cx| { - toggle_settings_profile_selector(workspace, window, cx); - }); - }); -} - -fn toggle_settings_profile_selector( - workspace: &mut Workspace, - window: &mut Window, - cx: &mut Context, -) { - workspace.toggle_modal(window, cx, |window, cx| { - let delegate = SettingsProfileSelectorDelegate::new(cx.entity().downgrade(), window, cx); - SettingsProfileSelector::new(delegate, window, cx) - }); -} - -pub struct SettingsProfileSelector { - picker: Entity>, -} - -impl ModalView for SettingsProfileSelector {} - -impl EventEmitter for SettingsProfileSelector {} - -impl Focusable for SettingsProfileSelector { - fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { - self.picker.focus_handle(cx) - } -} - -impl Render for SettingsProfileSelector { - fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { - v_flex().w(rems(22.)).child(self.picker.clone()) - } -} - -impl SettingsProfileSelector { - pub fn new( - delegate: SettingsProfileSelectorDelegate, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); - Self { picker } - } -} - -pub struct SettingsProfileSelectorDelegate { - matches: Vec, - profile_names: Vec>, - original_profile_name: Option, - selected_profile_name: Option, - selected_index: usize, - selection_completed: bool, - selector: WeakEntity, -} - -impl SettingsProfileSelectorDelegate { - fn new( - selector: WeakEntity, - _: &mut Window, - cx: &mut Context, - ) -> Self { - let settings_store = cx.global::(); - let mut profile_names: Vec> = settings_store - .configured_settings_profiles() - .map(|s| Some(s.to_string())) - .collect(); - profile_names.insert(0, None); - - let matches = profile_names - .iter() - .enumerate() - .map(|(ix, profile_name)| StringMatch { - candidate_id: ix, - score: 0.0, - positions: Default::default(), - string: display_name(profile_name), - }) - .collect(); - - let profile_name = cx - .try_global::() - .map(|p| p.0.clone()); - - let mut this = Self { - matches, - profile_names, - original_profile_name: profile_name.clone(), - selected_profile_name: None, - selected_index: 0, - selection_completed: false, - selector, - }; - - if let Some(profile_name) = profile_name { - this.select_if_matching(&profile_name); - } - - this - } - - fn select_if_matching(&mut self, profile_name: &str) { - self.selected_index = self - .matches - .iter() - .position(|mat| mat.string == profile_name) - .unwrap_or(self.selected_index); - } - - fn set_selected_profile( - &self, - cx: &mut Context>, - ) -> Option { - let mat = self.matches.get(self.selected_index)?; - let profile_name = self.profile_names.get(mat.candidate_id)?; - Self::update_active_profile_name_global(profile_name.clone(), cx) - } - - fn update_active_profile_name_global( - profile_name: Option, - cx: &mut Context>, - ) -> Option { - if let Some(profile_name) = profile_name { - cx.set_global(ActiveSettingsProfileName(profile_name.clone())); - return Some(profile_name); - } - - if cx.has_global::() { - cx.remove_global::(); - } - - None - } -} - -impl PickerDelegate for SettingsProfileSelectorDelegate { - type ListItem = ListItem; - - fn placeholder_text(&self, _: &mut Window, _: &mut App) -> std::sync::Arc { - "Select a settings profile...".into() - } - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index( - &mut self, - ix: usize, - _: &mut Window, - cx: &mut Context>, - ) { - self.selected_index = ix; - self.selected_profile_name = self.set_selected_profile(cx); - } - - fn update_matches( - &mut self, - query: String, - window: &mut Window, - cx: &mut Context>, - ) -> Task<()> { - let background = cx.background_executor().clone(); - let candidates = self - .profile_names - .iter() - .enumerate() - .map(|(id, profile_name)| StringMatchCandidate::new(id, &display_name(profile_name))) - .collect::>(); - - cx.spawn_in(window, async move |this, cx| { - let matches = if query.is_empty() { - candidates - .into_iter() - .enumerate() - .map(|(index, candidate)| StringMatch { - candidate_id: index, - string: candidate.string, - positions: Vec::new(), - score: 0.0, - }) - .collect() - } else { - match_strings( - &candidates, - &query, - false, - true, - 100, - &Default::default(), - background, - ) - .await - }; - - this.update_in(cx, |this, _, cx| { - this.delegate.matches = matches; - this.delegate.selected_index = this - .delegate - .selected_index - .min(this.delegate.matches.len().saturating_sub(1)); - this.delegate.selected_profile_name = this.delegate.set_selected_profile(cx); - }) - .ok(); - }) - } - - fn confirm( - &mut self, - _: bool, - _: &mut Window, - cx: &mut Context>, - ) { - self.selection_completed = true; - self.selector - .update(cx, |_, cx| { - cx.emit(DismissEvent); - }) - .ok(); - } - - fn dismissed( - &mut self, - _: &mut Window, - cx: &mut Context>, - ) { - if !self.selection_completed { - SettingsProfileSelectorDelegate::update_active_profile_name_global( - self.original_profile_name.clone(), - cx, - ); - } - self.selector.update(cx, |_, cx| cx.emit(DismissEvent)).ok(); - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _: &mut Window, - _: &mut Context>, - ) -> Option { - let mat = &self.matches.get(ix)?; - let profile_name = &self.profile_names.get(mat.candidate_id)?; - - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .child(HighlightedLabel::new( - display_name(profile_name), - mat.positions.clone(), - )), - ) - } -} - -fn display_name(profile_name: &Option) -> String { - profile_name.clone().unwrap_or("Disabled".into()) -} - -#[cfg(test)] -mod tests { - use super::*; - use editor; - use gpui::{TestAppContext, UpdateGlobal, VisualTestContext}; - use menu::{Cancel, Confirm, SelectNext, SelectPrevious}; - use project::{FakeFs, Project}; - use serde_json::json; - use settings::Settings; - use theme::{self, ThemeSettings}; - use workspace::{self, AppState}; - use zed_actions::settings_profile_selector; - - async fn init_test( - profiles_json: serde_json::Value, - cx: &mut TestAppContext, - ) -> (Entity, &mut VisualTestContext) { - cx.update(|cx| { - let state = AppState::test(cx); - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - settings::init(cx); - theme::init(theme::LoadThemes::JustBase, cx); - super::init(cx); - editor::init(cx); - state - }); - - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - let settings_json = json!({ - "buffer_font_size": 10.0, - "profiles": profiles_json, - }); - - store - .set_user_settings(&settings_json.to_string(), cx) - .unwrap(); - }); - }); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, ["/test".as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - cx.update(|_, cx| { - assert!(!cx.has_global::()); - assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(10.0)); - }); - - (workspace, cx) - } - - #[track_caller] - fn active_settings_profile_picker( - workspace: &Entity, - cx: &mut VisualTestContext, - ) -> Entity> { - workspace.update(cx, |workspace, cx| { - workspace - .active_modal::(cx) - .expect("settings profile selector is not open") - .read(cx) - .picker - .clone() - }) - } - - #[gpui::test] - async fn test_settings_profile_selector_state(cx: &mut TestAppContext) { - let classroom_and_streaming_profile_name = "Classroom / Streaming".to_string(); - let demo_videos_profile_name = "Demo Videos".to_string(); - - let profiles_json = json!({ - classroom_and_streaming_profile_name.clone(): { - "buffer_font_size": 20.0, - }, - demo_videos_profile_name.clone(): { - "buffer_font_size": 15.0 - } - }); - let (workspace, cx) = init_test(profiles_json.clone(), cx).await; - - cx.dispatch_action(settings_profile_selector::Toggle); - let picker = active_settings_profile_picker(&workspace, cx); - - picker.read_with(cx, |picker, cx| { - assert_eq!(picker.delegate.matches.len(), 3); - assert_eq!(picker.delegate.matches[0].string, display_name(&None)); - assert_eq!( - picker.delegate.matches[1].string, - classroom_and_streaming_profile_name - ); - assert_eq!(picker.delegate.matches[2].string, demo_videos_profile_name); - assert_eq!(picker.delegate.matches.get(3), None); - - assert_eq!(picker.delegate.selected_index, 0); - assert_eq!(picker.delegate.selected_profile_name, None); - - assert_eq!(cx.try_global::(), None); - assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(10.0)); - }); - - cx.dispatch_action(Confirm); - - cx.update(|_, cx| { - assert_eq!(cx.try_global::(), None); - }); - - cx.dispatch_action(settings_profile_selector::Toggle); - let picker = active_settings_profile_picker(&workspace, cx); - cx.dispatch_action(SelectNext); - - picker.read_with(cx, |picker, cx| { - assert_eq!(picker.delegate.selected_index, 1); - assert_eq!( - picker.delegate.selected_profile_name, - Some(classroom_and_streaming_profile_name.clone()) - ); - - assert_eq!( - cx.try_global::() - .map(|p| p.0.clone()), - Some(classroom_and_streaming_profile_name.clone()) - ); - - assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(20.0)); - }); - - cx.dispatch_action(Cancel); - - cx.update(|_, cx| { - assert_eq!(cx.try_global::(), None); - assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(10.0)); - }); - - cx.dispatch_action(settings_profile_selector::Toggle); - let picker = active_settings_profile_picker(&workspace, cx); - - cx.dispatch_action(SelectNext); - - picker.read_with(cx, |picker, cx| { - assert_eq!(picker.delegate.selected_index, 1); - assert_eq!( - picker.delegate.selected_profile_name, - Some(classroom_and_streaming_profile_name.clone()) - ); - - assert_eq!( - cx.try_global::() - .map(|p| p.0.clone()), - Some(classroom_and_streaming_profile_name.clone()) - ); - - assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(20.0)); - }); - - cx.dispatch_action(SelectNext); - - picker.read_with(cx, |picker, cx| { - assert_eq!(picker.delegate.selected_index, 2); - assert_eq!( - picker.delegate.selected_profile_name, - Some(demo_videos_profile_name.clone()) - ); - - assert_eq!( - cx.try_global::() - .map(|p| p.0.clone()), - Some(demo_videos_profile_name.clone()) - ); - - assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(15.0)); - }); - - cx.dispatch_action(Confirm); - - cx.update(|_, cx| { - assert_eq!( - cx.try_global::() - .map(|p| p.0.clone()), - Some(demo_videos_profile_name.clone()) - ); - assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(15.0)); - }); - - cx.dispatch_action(settings_profile_selector::Toggle); - let picker = active_settings_profile_picker(&workspace, cx); - - picker.read_with(cx, |picker, cx| { - assert_eq!(picker.delegate.selected_index, 2); - assert_eq!( - picker.delegate.selected_profile_name, - Some(demo_videos_profile_name.clone()) - ); - - assert_eq!( - cx.try_global::() - .map(|p| p.0.clone()), - Some(demo_videos_profile_name.clone()) - ); - assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(15.0)); - }); - - cx.dispatch_action(SelectPrevious); - - picker.read_with(cx, |picker, cx| { - assert_eq!(picker.delegate.selected_index, 1); - assert_eq!( - picker.delegate.selected_profile_name, - Some(classroom_and_streaming_profile_name.clone()) - ); - - assert_eq!( - cx.try_global::() - .map(|p| p.0.clone()), - Some(classroom_and_streaming_profile_name.clone()) - ); - - assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(20.0)); - }); - - cx.dispatch_action(Cancel); - - cx.update(|_, cx| { - assert_eq!( - cx.try_global::() - .map(|p| p.0.clone()), - Some(demo_videos_profile_name.clone()) - ); - - assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(15.0)); - }); - - cx.dispatch_action(settings_profile_selector::Toggle); - let picker = active_settings_profile_picker(&workspace, cx); - - picker.read_with(cx, |picker, cx| { - assert_eq!(picker.delegate.selected_index, 2); - assert_eq!( - picker.delegate.selected_profile_name, - Some(demo_videos_profile_name.clone()) - ); - - assert_eq!( - cx.try_global::() - .map(|p| p.0.clone()), - Some(demo_videos_profile_name) - ); - - assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(15.0)); - }); - - cx.dispatch_action(SelectPrevious); - - picker.read_with(cx, |picker, cx| { - assert_eq!(picker.delegate.selected_index, 1); - assert_eq!( - picker.delegate.selected_profile_name, - Some(classroom_and_streaming_profile_name.clone()) - ); - - assert_eq!( - cx.try_global::() - .map(|p| p.0.clone()), - Some(classroom_and_streaming_profile_name) - ); - - assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(20.0)); - }); - - cx.dispatch_action(SelectPrevious); - - picker.read_with(cx, |picker, cx| { - assert_eq!(picker.delegate.selected_index, 0); - assert_eq!(picker.delegate.selected_profile_name, None); - - assert_eq!( - cx.try_global::() - .map(|p| p.0.clone()), - None - ); - - assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(10.0)); - }); - - cx.dispatch_action(Confirm); - - cx.update(|_, cx| { - assert_eq!(cx.try_global::(), None); - assert_eq!(ThemeSettings::get_global(cx).buffer_font_size(cx), px(10.0)); - }); - } - - #[gpui::test] - async fn test_settings_profile_selector_is_in_user_configuration_order( - cx: &mut TestAppContext, - ) { - // Must be unique names (HashMap) - let profiles_json = json!({ - "z": {}, - "e": {}, - "d": {}, - " ": {}, - "r": {}, - "u": {}, - "l": {}, - "3": {}, - "s": {}, - "!": {}, - }); - let (workspace, cx) = init_test(profiles_json.clone(), cx).await; - - cx.dispatch_action(settings_profile_selector::Toggle); - let picker = active_settings_profile_picker(&workspace, cx); - - picker.read_with(cx, |picker, _| { - assert_eq!(picker.delegate.matches.len(), 11); - assert_eq!(picker.delegate.matches[0].string, display_name(&None)); - assert_eq!(picker.delegate.matches[1].string, "z"); - assert_eq!(picker.delegate.matches[2].string, "e"); - assert_eq!(picker.delegate.matches[3].string, "d"); - assert_eq!(picker.delegate.matches[4].string, " "); - assert_eq!(picker.delegate.matches[5].string, "r"); - assert_eq!(picker.delegate.matches[6].string, "u"); - assert_eq!(picker.delegate.matches[7].string, "l"); - assert_eq!(picker.delegate.matches[8].string, "3"); - assert_eq!(picker.delegate.matches[9].string, "s"); - assert_eq!(picker.delegate.matches[10].string, "!"); - }); - } -} diff --git a/crates/settings_ui/Cargo.toml b/crates/settings_ui/Cargo.toml deleted file mode 100644 index b5a259a3b9..0000000000 --- a/crates/settings_ui/Cargo.toml +++ /dev/null @@ -1,58 +0,0 @@ -[package] -name = "settings_ui" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/settings_ui.rs" - -[features] -default = [] -test-support = [] - -[dependencies] -anyhow.workspace = true -bm25 = "2.3.2" -editor.workspace = true -feature_flags.workspace = true -fs.workspace = true -fuzzy.workspace = true -gpui.workspace = true -heck.workspace = true -log.workspace = true -menu.workspace = true -paths.workspace = true -picker.workspace = true -project.workspace = true -release_channel.workspace = true -schemars.workspace = true -search.workspace = true -serde.workspace = true -settings.workspace = true -strum.workspace = true -telemetry.workspace = true -theme.workspace = true -title_bar.workspace = true -ui.workspace = true -ui_input.workspace = true -util.workspace = true -workspace.workspace = true -zed_actions.workspace = true - -[dev-dependencies] -assets.workspace = true -client.workspace = true -futures.workspace = true -gpui = { workspace = true, features = ["test-support"] } -language.workspace = true -node_runtime.workspace = true -paths.workspace = true -pretty_assertions.workspace = true -session.workspace = true -settings.workspace = true -zlog.workspace = true diff --git a/crates/settings_ui/LICENSE-GPL b/crates/settings_ui/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/settings_ui/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/settings_ui/src/components.rs b/crates/settings_ui/src/components.rs deleted file mode 100644 index b073372ac9..0000000000 --- a/crates/settings_ui/src/components.rs +++ /dev/null @@ -1,11 +0,0 @@ -mod dropdown; -mod font_picker; -mod icon_theme_picker; -mod input_field; -mod theme_picker; - -pub use dropdown::*; -pub use font_picker::font_picker; -pub use icon_theme_picker::icon_theme_picker; -pub use input_field::*; -pub use theme_picker::theme_picker; diff --git a/crates/settings_ui/src/components/dropdown.rs b/crates/settings_ui/src/components/dropdown.rs deleted file mode 100644 index ec9ecb4eaf..0000000000 --- a/crates/settings_ui/src/components/dropdown.rs +++ /dev/null @@ -1,108 +0,0 @@ -use std::rc::Rc; - -use gpui::{App, ElementId, IntoElement, RenderOnce}; -use heck::ToTitleCase as _; -use ui::{ - ButtonSize, ContextMenu, DropdownMenu, DropdownStyle, FluentBuilder as _, IconPosition, px, -}; - -#[derive(IntoElement)] -pub struct EnumVariantDropdown -where - T: strum::VariantArray + strum::VariantNames + Copy + PartialEq + Send + Sync + 'static, -{ - id: ElementId, - current_value: T, - variants: &'static [T], - labels: &'static [&'static str], - should_do_title_case: bool, - tab_index: Option, - on_change: Rc, -} - -impl EnumVariantDropdown -where - T: strum::VariantArray + strum::VariantNames + Copy + PartialEq + Send + Sync + 'static, -{ - pub fn new( - id: impl Into, - current_value: T, - variants: &'static [T], - labels: &'static [&'static str], - on_change: impl Fn(T, &mut App) + 'static, - ) -> Self { - Self { - id: id.into(), - current_value, - variants, - labels, - should_do_title_case: true, - tab_index: None, - on_change: Rc::new(on_change), - } - } - - pub fn title_case(mut self, title_case: bool) -> Self { - self.should_do_title_case = title_case; - self - } - - pub fn tab_index(mut self, tab_index: isize) -> Self { - self.tab_index = Some(tab_index); - self - } -} - -impl RenderOnce for EnumVariantDropdown -where - T: strum::VariantArray + strum::VariantNames + Copy + PartialEq + Send + Sync + 'static, -{ - fn render(self, window: &mut ui::Window, cx: &mut ui::App) -> impl gpui::IntoElement { - let current_value_label = self.labels[self - .variants - .iter() - .position(|v| *v == self.current_value) - .unwrap()]; - - let context_menu = window.use_keyed_state(current_value_label, cx, |window, cx| { - ContextMenu::new(window, cx, move |mut menu, _, _| { - for (&value, &label) in std::iter::zip(self.variants, self.labels) { - let on_change = self.on_change.clone(); - let current_value = self.current_value; - menu = menu.toggleable_entry( - if self.should_do_title_case { - label.to_title_case() - } else { - label.to_string() - }, - value == current_value, - IconPosition::End, - None, - move |_, cx| { - on_change(value, cx); - }, - ); - } - menu - }) - }); - - DropdownMenu::new( - self.id, - if self.should_do_title_case { - current_value_label.to_title_case() - } else { - current_value_label.to_string() - }, - context_menu, - ) - .when_some(self.tab_index, |elem, tab_index| elem.tab_index(tab_index)) - .trigger_size(ButtonSize::Medium) - .style(DropdownStyle::Outlined) - .offset(gpui::Point { - x: px(0.0), - y: px(2.0), - }) - .into_any_element() - } -} diff --git a/crates/settings_ui/src/components/font_picker.rs b/crates/settings_ui/src/components/font_picker.rs deleted file mode 100644 index 7a79009efd..0000000000 --- a/crates/settings_ui/src/components/font_picker.rs +++ /dev/null @@ -1,181 +0,0 @@ -use std::sync::Arc; - -use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{AnyElement, App, Context, DismissEvent, SharedString, Task, Window}; -use picker::{Picker, PickerDelegate}; -use theme::FontFamilyCache; -use ui::{ListItem, ListItemSpacing, prelude::*}; - -type FontPicker = Picker; - -pub struct FontPickerDelegate { - fonts: Vec, - filtered_fonts: Vec, - selected_index: usize, - current_font: SharedString, - on_font_changed: Arc, -} - -impl FontPickerDelegate { - fn new( - current_font: SharedString, - on_font_changed: impl Fn(SharedString, &mut App) + 'static, - cx: &mut Context, - ) -> Self { - let font_family_cache = FontFamilyCache::global(cx); - - let fonts = font_family_cache - .try_list_font_families() - .unwrap_or_else(|| vec![current_font.clone()]); - let selected_index = fonts - .iter() - .position(|font| *font == current_font) - .unwrap_or(0); - - let filtered_fonts = fonts - .iter() - .enumerate() - .map(|(index, font)| StringMatch { - candidate_id: index, - string: font.to_string(), - positions: Vec::new(), - score: 0.0, - }) - .collect(); - - Self { - fonts, - filtered_fonts, - selected_index, - current_font, - on_font_changed: Arc::new(on_font_changed), - } - } -} - -impl PickerDelegate for FontPickerDelegate { - type ListItem = AnyElement; - - fn match_count(&self) -> usize { - self.filtered_fonts.len() - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context) { - self.selected_index = ix.min(self.filtered_fonts.len().saturating_sub(1)); - cx.notify(); - } - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - "Search fonts…".into() - } - - fn update_matches( - &mut self, - query: String, - _window: &mut Window, - cx: &mut Context, - ) -> Task<()> { - let fonts = self.fonts.clone(); - let current_font = self.current_font.clone(); - - let matches: Vec = if query.is_empty() { - fonts - .iter() - .enumerate() - .map(|(index, font)| StringMatch { - candidate_id: index, - string: font.to_string(), - positions: Vec::new(), - score: 0.0, - }) - .collect() - } else { - let _candidates: Vec = fonts - .iter() - .enumerate() - .map(|(id, font)| StringMatchCandidate::new(id, font.as_ref())) - .collect(); - - fonts - .iter() - .enumerate() - .filter(|(_, font)| font.to_lowercase().contains(&query.to_lowercase())) - .map(|(index, font)| StringMatch { - candidate_id: index, - string: font.to_string(), - positions: Vec::new(), - score: 0.0, - }) - .collect() - }; - - let selected_index = if query.is_empty() { - fonts - .iter() - .position(|font| *font == current_font) - .unwrap_or(0) - } else { - matches - .iter() - .position(|m| fonts[m.candidate_id] == current_font) - .unwrap_or(0) - }; - - self.filtered_fonts = matches; - self.selected_index = selected_index; - cx.notify(); - - Task::ready(()) - } - - fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context) { - if let Some(font_match) = self.filtered_fonts.get(self.selected_index) { - let font = font_match.string.clone(); - (self.on_font_changed)(font.into(), cx); - } - } - - fn dismissed(&mut self, window: &mut Window, cx: &mut Context) { - cx.defer_in(window, |picker, window, cx| { - picker.set_query("", window, cx); - }); - cx.emit(DismissEvent); - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _window: &mut Window, - _cx: &mut Context, - ) -> Option { - let font_match = self.filtered_fonts.get(ix)?; - - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .child(Label::new(font_match.string.clone())) - .into_any_element(), - ) - } -} - -pub fn font_picker( - current_font: SharedString, - on_font_changed: impl Fn(SharedString, &mut App) + 'static, - window: &mut Window, - cx: &mut Context, -) -> FontPicker { - let delegate = FontPickerDelegate::new(current_font, on_font_changed, cx); - - Picker::uniform_list(delegate, window, cx) - .show_scrollbar(true) - .width(rems_from_px(210.)) - .max_height(Some(rems(18.).into())) -} diff --git a/crates/settings_ui/src/components/icon_theme_picker.rs b/crates/settings_ui/src/components/icon_theme_picker.rs deleted file mode 100644 index 33a648f81b..0000000000 --- a/crates/settings_ui/src/components/icon_theme_picker.rs +++ /dev/null @@ -1,189 +0,0 @@ -use std::sync::Arc; - -use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{AnyElement, App, Context, DismissEvent, SharedString, Task, Window}; -use picker::{Picker, PickerDelegate}; -use theme::ThemeRegistry; -use ui::{ListItem, ListItemSpacing, prelude::*}; - -type IconThemePicker = Picker; - -pub struct IconThemePickerDelegate { - icon_themes: Vec, - filtered_themes: Vec, - selected_index: usize, - current_theme: SharedString, - on_theme_changed: Arc, -} - -impl IconThemePickerDelegate { - fn new( - current_theme: SharedString, - on_theme_changed: impl Fn(SharedString, &mut App) + 'static, - cx: &mut Context, - ) -> Self { - let theme_registry = ThemeRegistry::global(cx); - - let icon_themes: Vec = theme_registry - .list_icon_themes() - .into_iter() - .map(|theme_meta| theme_meta.name) - .collect(); - - let selected_index = icon_themes - .iter() - .position(|icon_themes| *icon_themes == current_theme) - .unwrap_or(0); - - let filtered_themes = icon_themes - .iter() - .enumerate() - .map(|(index, icon_themes)| StringMatch { - candidate_id: index, - string: icon_themes.to_string(), - positions: Vec::new(), - score: 0.0, - }) - .collect(); - - Self { - icon_themes, - filtered_themes, - selected_index, - current_theme, - on_theme_changed: Arc::new(on_theme_changed), - } - } -} - -impl PickerDelegate for IconThemePickerDelegate { - type ListItem = AnyElement; - - fn match_count(&self) -> usize { - self.filtered_themes.len() - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context) { - self.selected_index = ix.min(self.filtered_themes.len().saturating_sub(1)); - cx.notify(); - } - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - "Search icon theme…".into() - } - - fn update_matches( - &mut self, - query: String, - _window: &mut Window, - cx: &mut Context, - ) -> Task<()> { - let icon_themes = self.icon_themes.clone(); - let current_theme = self.current_theme.clone(); - - let matches: Vec = if query.is_empty() { - icon_themes - .iter() - .enumerate() - .map(|(index, icon_theme)| StringMatch { - candidate_id: index, - string: icon_theme.to_string(), - positions: Vec::new(), - score: 0.0, - }) - .collect() - } else { - let _candidates: Vec = icon_themes - .iter() - .enumerate() - .map(|(id, icon_theme)| StringMatchCandidate::new(id, icon_theme.as_ref())) - .collect(); - - icon_themes - .iter() - .enumerate() - .filter(|(_, icon_theme)| icon_theme.to_lowercase().contains(&query.to_lowercase())) - .map(|(index, icon_theme)| StringMatch { - candidate_id: index, - string: icon_theme.to_string(), - positions: Vec::new(), - score: 0.0, - }) - .collect() - }; - - let selected_index = if query.is_empty() { - icon_themes - .iter() - .position(|icon_theme| *icon_theme == current_theme) - .unwrap_or(0) - } else { - matches - .iter() - .position(|m| icon_themes[m.candidate_id] == current_theme) - .unwrap_or(0) - }; - - self.filtered_themes = matches; - self.selected_index = selected_index; - cx.notify(); - - Task::ready(()) - } - - fn confirm( - &mut self, - _secondary: bool, - _window: &mut Window, - cx: &mut Context, - ) { - if let Some(theme_match) = self.filtered_themes.get(self.selected_index) { - let theme = theme_match.string.clone(); - (self.on_theme_changed)(theme.into(), cx); - } - } - - fn dismissed(&mut self, window: &mut Window, cx: &mut Context) { - cx.defer_in(window, |picker, window, cx| { - picker.set_query("", window, cx); - }); - cx.emit(DismissEvent); - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _window: &mut Window, - _cx: &mut Context, - ) -> Option { - let theme_match = self.filtered_themes.get(ix)?; - - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .child(Label::new(theme_match.string.clone())) - .into_any_element(), - ) - } -} - -pub fn icon_theme_picker( - current_theme: SharedString, - on_theme_changed: impl Fn(SharedString, &mut App) + 'static, - window: &mut Window, - cx: &mut Context, -) -> IconThemePicker { - let delegate = IconThemePickerDelegate::new(current_theme, on_theme_changed, cx); - - Picker::uniform_list(delegate, window, cx) - .show_scrollbar(true) - .width(rems_from_px(210.)) - .max_height(Some(rems(18.).into())) -} diff --git a/crates/settings_ui/src/components/input_field.rs b/crates/settings_ui/src/components/input_field.rs deleted file mode 100644 index 57917c3211..0000000000 --- a/crates/settings_ui/src/components/input_field.rs +++ /dev/null @@ -1,96 +0,0 @@ -use editor::Editor; -use gpui::{Focusable, div}; -use ui::{ - ActiveTheme as _, App, FluentBuilder as _, InteractiveElement as _, IntoElement, - ParentElement as _, RenderOnce, Styled as _, Window, -}; - -#[derive(IntoElement)] -pub struct SettingsInputField { - initial_text: Option, - placeholder: Option<&'static str>, - confirm: Option, &mut App)>>, - tab_index: Option, -} - -impl SettingsInputField { - pub fn new() -> Self { - Self { - initial_text: None, - placeholder: None, - confirm: None, - tab_index: None, - } - } - - pub fn with_initial_text(mut self, initial_text: String) -> Self { - self.initial_text = Some(initial_text); - self - } - - pub fn with_placeholder(mut self, placeholder: &'static str) -> Self { - self.placeholder = Some(placeholder); - self - } - - pub fn on_confirm(mut self, confirm: impl Fn(Option, &mut App) + 'static) -> Self { - self.confirm = Some(Box::new(confirm)); - self - } - - pub(crate) fn tab_index(mut self, arg: isize) -> Self { - self.tab_index = Some(arg); - self - } -} - -impl RenderOnce for SettingsInputField { - fn render(self, window: &mut Window, cx: &mut App) -> impl ui::IntoElement { - let editor = window.use_state(cx, { - move |window, cx| { - let mut editor = Editor::single_line(window, cx); - if let Some(text) = self.initial_text { - editor.set_text(text, window, cx); - } - - if let Some(placeholder) = self.placeholder { - editor.set_placeholder_text(placeholder, window, cx); - } - // todo(settings_ui): We should have an observe global use for settings store - // so whenever a settings file is updated, the settings ui updates too - editor - } - }); - - let weak_editor = editor.downgrade(); - - let theme_colors = cx.theme().colors(); - - div() - .py_1() - .px_2() - .min_w_64() - .rounded_md() - .border_1() - .border_color(theme_colors.border) - .bg(theme_colors.editor_background) - .when_some(self.tab_index, |this, tab_index| { - let focus_handle = editor.focus_handle(cx).tab_index(tab_index).tab_stop(true); - this.track_focus(&focus_handle) - .focus(|s| s.border_color(theme_colors.border_focused)) - }) - .child(editor) - .when_some(self.confirm, |this, confirm| { - this.on_action::({ - move |_, _, cx| { - let Some(editor) = weak_editor.upgrade() else { - return; - }; - let new_value = editor.read_with(cx, |editor, cx| editor.text(cx)); - let new_value = (!new_value.is_empty()).then_some(new_value); - confirm(new_value, cx); - } - }) - }) - } -} diff --git a/crates/settings_ui/src/components/theme_picker.rs b/crates/settings_ui/src/components/theme_picker.rs deleted file mode 100644 index 2146ab314f..0000000000 --- a/crates/settings_ui/src/components/theme_picker.rs +++ /dev/null @@ -1,179 +0,0 @@ -use std::sync::Arc; - -use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{AnyElement, App, Context, DismissEvent, SharedString, Task, Window}; -use picker::{Picker, PickerDelegate}; -use theme::ThemeRegistry; -use ui::{ListItem, ListItemSpacing, prelude::*}; - -type ThemePicker = Picker; - -pub struct ThemePickerDelegate { - themes: Vec, - filtered_themes: Vec, - selected_index: usize, - current_theme: SharedString, - on_theme_changed: Arc, -} - -impl ThemePickerDelegate { - fn new( - current_theme: SharedString, - on_theme_changed: impl Fn(SharedString, &mut App) + 'static, - cx: &mut Context, - ) -> Self { - let theme_registry = ThemeRegistry::global(cx); - - let themes = theme_registry.list_names(); - let selected_index = themes - .iter() - .position(|theme| *theme == current_theme) - .unwrap_or(0); - - let filtered_themes = themes - .iter() - .enumerate() - .map(|(index, theme)| StringMatch { - candidate_id: index, - string: theme.to_string(), - positions: Vec::new(), - score: 0.0, - }) - .collect(); - - Self { - themes, - filtered_themes, - selected_index, - current_theme, - on_theme_changed: Arc::new(on_theme_changed), - } - } -} - -impl PickerDelegate for ThemePickerDelegate { - type ListItem = AnyElement; - - fn match_count(&self) -> usize { - self.filtered_themes.len() - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context) { - self.selected_index = ix.min(self.filtered_themes.len().saturating_sub(1)); - cx.notify(); - } - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - "Search theme…".into() - } - - fn update_matches( - &mut self, - query: String, - _window: &mut Window, - cx: &mut Context, - ) -> Task<()> { - let themes = self.themes.clone(); - let current_theme = self.current_theme.clone(); - - let matches: Vec = if query.is_empty() { - themes - .iter() - .enumerate() - .map(|(index, theme)| StringMatch { - candidate_id: index, - string: theme.to_string(), - positions: Vec::new(), - score: 0.0, - }) - .collect() - } else { - let _candidates: Vec = themes - .iter() - .enumerate() - .map(|(id, theme)| StringMatchCandidate::new(id, theme.as_ref())) - .collect(); - - themes - .iter() - .enumerate() - .filter(|(_, theme)| theme.to_lowercase().contains(&query.to_lowercase())) - .map(|(index, theme)| StringMatch { - candidate_id: index, - string: theme.to_string(), - positions: Vec::new(), - score: 0.0, - }) - .collect() - }; - - let selected_index = if query.is_empty() { - themes - .iter() - .position(|theme| *theme == current_theme) - .unwrap_or(0) - } else { - matches - .iter() - .position(|m| themes[m.candidate_id] == current_theme) - .unwrap_or(0) - }; - - self.filtered_themes = matches; - self.selected_index = selected_index; - cx.notify(); - - Task::ready(()) - } - - fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context) { - if let Some(theme_match) = self.filtered_themes.get(self.selected_index) { - let theme = theme_match.string.clone(); - (self.on_theme_changed)(theme.into(), cx); - } - } - - fn dismissed(&mut self, window: &mut Window, cx: &mut Context) { - cx.defer_in(window, |picker, window, cx| { - picker.set_query("", window, cx); - }); - cx.emit(DismissEvent); - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _window: &mut Window, - _cx: &mut Context, - ) -> Option { - let theme_match = self.filtered_themes.get(ix)?; - - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .child(Label::new(theme_match.string.clone())) - .into_any_element(), - ) - } -} - -pub fn theme_picker( - current_theme: SharedString, - on_theme_changed: impl Fn(SharedString, &mut App) + 'static, - window: &mut Window, - cx: &mut Context, -) -> ThemePicker { - let delegate = ThemePickerDelegate::new(current_theme, on_theme_changed, cx); - - Picker::uniform_list(delegate, window, cx) - .show_scrollbar(true) - .width(rems_from_px(210.)) - .max_height(Some(rems(18.).into())) -} diff --git a/crates/settings_ui/src/page_data.rs b/crates/settings_ui/src/page_data.rs deleted file mode 100644 index 8652ccf68b..0000000000 --- a/crates/settings_ui/src/page_data.rs +++ /dev/null @@ -1,7549 +0,0 @@ -use gpui::App; -use settings::{LanguageSettingsContent, SettingsContent}; -use std::sync::Arc; -use strum::IntoDiscriminant as _; -use ui::{IntoElement, SharedString}; - -use crate::{ - DynamicItem, PROJECT, SettingField, SettingItem, SettingsFieldMetadata, SettingsPage, - SettingsPageItem, SubPageLink, USER, all_language_names, sub_page_stack, -}; - -const DEFAULT_STRING: String = String::new(); -/// A default empty string reference. Useful in `pick` functions for cases either in dynamic item fields, or when dealing with `settings::Maybe` -/// to avoid the "NO DEFAULT" case. -const DEFAULT_EMPTY_STRING: Option<&String> = Some(&DEFAULT_STRING); - -const DEFAULT_SHARED_STRING: SharedString = SharedString::new_static(""); -/// A default empty string reference. Useful in `pick` functions for cases either in dynamic item fields, or when dealing with `settings::Maybe` -/// to avoid the "NO DEFAULT" case. -const DEFAULT_EMPTY_SHARED_STRING: Option<&SharedString> = Some(&DEFAULT_SHARED_STRING); - -pub(crate) fn settings_data(cx: &App) -> Vec { - vec![ - SettingsPage { - title: "General", - items: vec![ - SettingsPageItem::SectionHeader("General Settings"), - SettingsPageItem::SettingItem(SettingItem { - files: PROJECT, - title: "Project Name", - description: "The displayed name of this project. If left empty, the root directory name will be displayed.", - field: Box::new( - SettingField { - json_path: Some("project_name"), - pick: |settings_content| { - settings_content.project.worktree.project_name.as_ref().or(DEFAULT_EMPTY_STRING) - }, - write: |settings_content, value| { - settings_content.project.worktree.project_name = value.filter(|name| !name.is_empty()); - }, - } - ), - metadata: Some(Box::new(SettingsFieldMetadata { placeholder: Some("Project Name"), ..Default::default() })), - }), - SettingsPageItem::SettingItem(SettingItem { - title: "When Closing With No Tabs", - description: "What to do when using the 'close active item' action with no tabs.", - field: Box::new(SettingField { - json_path: Some("when_closing_with_no_tabs"), - pick: |settings_content| { - settings_content - .workspace - .when_closing_with_no_tabs - .as_ref() - }, - write: |settings_content, value| { - settings_content.workspace.when_closing_with_no_tabs = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "On Last Window Closed", - description: "What to do when the last window is closed.", - field: Box::new(SettingField { - json_path: Some("on_last_window_closed"), - pick: |settings_content| { - settings_content.workspace.on_last_window_closed.as_ref() - }, - write: |settings_content, value| { - settings_content.workspace.on_last_window_closed = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Use System Path Prompts", - description: "Use native OS dialogs for 'Open' and 'Save As'.", - field: Box::new(SettingField { - json_path: Some("use_system_path_prompts"), - pick: |settings_content| { - settings_content.workspace.use_system_path_prompts.as_ref() - }, - write: |settings_content, value| { - settings_content.workspace.use_system_path_prompts = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Use System Prompts", - description: "Use native OS dialogs for confirmations.", - field: Box::new(SettingField { - json_path: Some("use_system_prompts"), - pick: |settings_content| { - settings_content.workspace.use_system_prompts.as_ref() - }, - write: |settings_content, value| { - settings_content.workspace.use_system_prompts = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Redact Private Values", - description: "Hide the values of variables in private files.", - field: Box::new(SettingField { - json_path: Some("redact_private_values"), - pick: |settings_content| { - settings_content.editor.redact_private_values.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.redact_private_values = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Private Files", - description: "Globs to match against file paths to determine if a file is private.", - field: Box::new( - SettingField { - json_path: Some("worktree.private_files"), - pick: |settings_content| { - settings_content.project.worktree.private_files.as_ref() - }, - write: |settings_content, value| { - settings_content.project.worktree.private_files = value; - }, - } - .unimplemented(), - ), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Workspace Restoration"), - SettingsPageItem::SettingItem(SettingItem { - title: "Restore Unsaved Buffers", - description: "Whether or not to restore unsaved buffers on restart.", - field: Box::new(SettingField { - json_path: Some("session.restore_unsaved_buffers"), - pick: |settings_content| { - settings_content - .session - .as_ref() - .and_then(|session| session.restore_unsaved_buffers.as_ref()) - }, - write: |settings_content, value| { - settings_content - .session - .get_or_insert_default() - .restore_unsaved_buffers = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Restore On Startup", - description: "What to restore from the previous session when opening Zed.", - field: Box::new(SettingField { - json_path: Some("restore_on_startup"), - pick: |settings_content| { - settings_content.workspace.restore_on_startup.as_ref() - }, - write: |settings_content, value| { - settings_content.workspace.restore_on_startup = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Scoped Settings"), - SettingsPageItem::SettingItem(SettingItem { - files: USER, - title: "Preview Channel", - description: "Which settings should be activated only in Preview build of Zed.", - field: Box::new( - SettingField { - json_path: Some("preview_channel_settings"), - pick: |settings_content| { - Some(settings_content) - }, - write: |_settings_content, _value| { - - }, - } - .unimplemented(), - ), - metadata: None, - }), - SettingsPageItem::SettingItem(SettingItem { - files: USER, - title: "Settings Profiles", - description: "Any number of settings profiles that are temporarily applied on top of your existing user settings.", - field: Box::new( - SettingField { - json_path: Some("settings_profiles"), - pick: |settings_content| { - Some(settings_content) - }, - write: |_settings_content, _value| { - }, - } - .unimplemented(), - ), - metadata: None, - }), - SettingsPageItem::SectionHeader("Privacy"), - SettingsPageItem::SettingItem(SettingItem { - title: "Telemetry Diagnostics", - description: "Send debug information like crash reports.", - field: Box::new(SettingField { - json_path: Some("telemetry.diagnostics"), - pick: |settings_content| { - settings_content - .telemetry - .as_ref() - .and_then(|telemetry| telemetry.diagnostics.as_ref()) - }, - write: |settings_content, value| { - settings_content - .telemetry - .get_or_insert_default() - .diagnostics = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Telemetry Metrics", - description: "Send anonymized usage data like what languages you're using Zed with.", - field: Box::new(SettingField { - json_path: Some("telemetry.metrics"), - pick: |settings_content| { - settings_content - .telemetry - .as_ref() - .and_then(|telemetry| telemetry.metrics.as_ref()) - }, - write: |settings_content, value| { - settings_content.telemetry.get_or_insert_default().metrics = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Auto Update"), - SettingsPageItem::SettingItem(SettingItem { - title: "Auto Update", - description: "Whether or not to automatically check for updates.", - field: Box::new(SettingField { - json_path: Some("auto_update"), - pick: |settings_content| settings_content.auto_update.as_ref(), - write: |settings_content, value| { - settings_content.auto_update = value; - }, - }), - metadata: None, - files: USER, - }), - ], - }, - SettingsPage { - title: "Appearance", - items: vec![ - SettingsPageItem::SectionHeader("Theme"), - SettingsPageItem::DynamicItem(DynamicItem { - discriminant: SettingItem { - files: USER, - title: "Theme Mode", - description: "Choose a static, fixed theme or dynamically select themes based on appearance and light/dark modes.", - field: Box::new(SettingField { - json_path: Some("theme$"), - pick: |settings_content| { - Some(&dynamic_variants::()[ - settings_content - .theme - .theme - .as_ref()? - .discriminant() as usize]) - }, - write: |settings_content, value| { - let Some(value) = value else { - settings_content.theme.theme = None; - return; - }; - let settings_value = settings_content.theme.theme.get_or_insert_with(|| { - settings::ThemeSelection::Static(theme::ThemeName(theme::default_theme(theme::SystemAppearance::default().0).into())) - }); - *settings_value = match value { - settings::ThemeSelectionDiscriminants::Static => { - let name = match settings_value { - settings::ThemeSelection::Static(_) => return, - settings::ThemeSelection::Dynamic { mode, light, dark } => { - match mode { - theme::ThemeAppearanceMode::Light => light.clone(), - theme::ThemeAppearanceMode::Dark => dark.clone(), - theme::ThemeAppearanceMode::System => dark.clone(), // no cx, can't determine correct choice - } - }, - }; - settings::ThemeSelection::Static(name) - }, - settings::ThemeSelectionDiscriminants::Dynamic => { - let static_name = match settings_value { - settings::ThemeSelection::Static(theme_name) => theme_name.clone(), - settings::ThemeSelection::Dynamic {..} => return, - }; - - settings::ThemeSelection::Dynamic { - mode: settings::ThemeAppearanceMode::System, - light: static_name.clone(), - dark: static_name, - } - }, - }; - }, - }), - metadata: None, - }, - pick_discriminant: |settings_content| { - Some(settings_content.theme.theme.as_ref()?.discriminant() as usize) - }, - fields: dynamic_variants::().into_iter().map(|variant| { - match variant { - settings::ThemeSelectionDiscriminants::Static => vec![ - SettingItem { - files: USER, - title: "Theme Name", - description: "The name of your selected theme.", - field: Box::new(SettingField { - json_path: Some("theme"), - pick: |settings_content| { - match settings_content.theme.theme.as_ref() { - Some(settings::ThemeSelection::Static(name)) => Some(name), - _ => None - } - }, - write: |settings_content, value| { - let Some(value) = value else { - return; - }; - match settings_content - .theme - .theme.as_mut() { - Some(settings::ThemeSelection::Static(theme_name)) => *theme_name = value, - _ => return - } - }, - }), - metadata: None, - } - ], - settings::ThemeSelectionDiscriminants::Dynamic => vec![ - SettingItem { - files: USER, - title: "Mode", - description: "Choose whether to use the selected light or dark theme or to follow your OS appearance configuration.", - field: Box::new(SettingField { - json_path: Some("theme.mode"), - pick: |settings_content| { - match settings_content.theme.theme.as_ref() { - Some(settings::ThemeSelection::Dynamic { mode, ..}) => Some(mode), - _ => None - } - }, - write: |settings_content, value| { - let Some(value) = value else { - return; - }; - match settings_content - .theme - .theme.as_mut() { - Some(settings::ThemeSelection::Dynamic{ mode, ..}) => *mode = value, - _ => return - } - }, - }), - metadata: None, - }, - SettingItem { - files: USER, - title: "Light Theme", - description: "The theme to use when mode is set to light, or when mode is set to system and it is in light mode.", - field: Box::new(SettingField { - json_path: Some("theme.light"), - pick: |settings_content| { - match settings_content.theme.theme.as_ref() { - Some(settings::ThemeSelection::Dynamic { light, ..}) => Some(light), - _ => None - } - }, - write: |settings_content, value| { - let Some(value) = value else { - return; - }; - match settings_content - .theme - .theme.as_mut() { - Some(settings::ThemeSelection::Dynamic{ light, ..}) => *light = value, - _ => return - } - }, - }), - metadata: None, - }, - SettingItem { - files: USER, - title: "Dark Theme", - description: "The theme to use when mode is set to dark, or when mode is set to system and it is in dark mode.", - field: Box::new(SettingField { - json_path: Some("theme.dark"), - pick: |settings_content| { - match settings_content.theme.theme.as_ref() { - Some(settings::ThemeSelection::Dynamic { dark, ..}) => Some(dark), - _ => None - } - }, - write: |settings_content, value| { - let Some(value) = value else { - return; - }; - match settings_content - .theme - .theme.as_mut() { - Some(settings::ThemeSelection::Dynamic{ dark, ..}) => *dark = value, - _ => return - } - }, - }), - metadata: None, - } - ], - } - }).collect(), - }), - SettingsPageItem::DynamicItem(DynamicItem { - discriminant: SettingItem { - files: USER, - title: "Icon Theme", - description: "The custom set of icons Zed will associate with files and directories.", - field: Box::new(SettingField { - json_path: Some("icon_theme$"), - pick: |settings_content| { - Some(&dynamic_variants::()[ - settings_content - .theme - .icon_theme - .as_ref()? - .discriminant() as usize]) - }, - write: |settings_content, value| { - let Some(value) = value else { - settings_content.theme.icon_theme = None; - return; - }; - let settings_value = settings_content.theme.icon_theme.get_or_insert_with(|| { - settings::IconThemeSelection::Static(settings::IconThemeName(theme::default_icon_theme().name.clone().into())) - }); - *settings_value = match value { - settings::IconThemeSelectionDiscriminants::Static => { - let name = match settings_value { - settings::IconThemeSelection::Static(_) => return, - settings::IconThemeSelection::Dynamic { mode, light, dark } => { - match mode { - theme::ThemeAppearanceMode::Light => light.clone(), - theme::ThemeAppearanceMode::Dark => dark.clone(), - theme::ThemeAppearanceMode::System => dark.clone(), // no cx, can't determine correct choice - } - }, - }; - settings::IconThemeSelection::Static(name) - }, - settings::IconThemeSelectionDiscriminants::Dynamic => { - let static_name = match settings_value { - settings::IconThemeSelection::Static(theme_name) => theme_name.clone(), - settings::IconThemeSelection::Dynamic {..} => return, - }; - - settings::IconThemeSelection::Dynamic { - mode: settings::ThemeAppearanceMode::System, - light: static_name.clone(), - dark: static_name, - } - }, - }; - }, - }), - metadata: None, - }, - pick_discriminant: |settings_content| { - Some(settings_content.theme.icon_theme.as_ref()?.discriminant() as usize) - }, - fields: dynamic_variants::().into_iter().map(|variant| { - match variant { - settings::IconThemeSelectionDiscriminants::Static => vec![ - SettingItem { - files: USER, - title: "Icon Theme Name", - description: "The name of your selected icon theme.", - field: Box::new(SettingField { - json_path: Some("icon_theme$string"), - pick: |settings_content| { - match settings_content.theme.icon_theme.as_ref() { - Some(settings::IconThemeSelection::Static(name)) => Some(name), - _ => None - } - }, - write: |settings_content, value| { - let Some(value) = value else { - return; - }; - match settings_content - .theme - .icon_theme.as_mut() { - Some(settings::IconThemeSelection::Static(theme_name)) => *theme_name = value, - _ => return - } - }, - }), - metadata: None, - } - ], - settings::IconThemeSelectionDiscriminants::Dynamic => vec![ - SettingItem { - files: USER, - title: "Mode", - description: "Choose whether to use the selected light or dark icon theme or to follow your OS appearance configuration.", - field: Box::new(SettingField { - json_path: Some("icon_theme"), - pick: |settings_content| { - match settings_content.theme.icon_theme.as_ref() { - Some(settings::IconThemeSelection::Dynamic { mode, ..}) => Some(mode), - _ => None - } - }, - write: |settings_content, value| { - let Some(value) = value else { - return; - }; - match settings_content - .theme - .icon_theme.as_mut() { - Some(settings::IconThemeSelection::Dynamic{ mode, ..}) => *mode = value, - _ => return - } - }, - }), - metadata: None, - }, - SettingItem { - files: USER, - title: "Light Icon Theme", - description: "The icon theme to use when mode is set to light, or when mode is set to system and it is in light mode.", - field: Box::new(SettingField { - json_path: Some("icon_theme.light"), - pick: |settings_content| { - match settings_content.theme.icon_theme.as_ref() { - Some(settings::IconThemeSelection::Dynamic { light, ..}) => Some(light), - _ => None - } - }, - write: |settings_content, value| { - let Some(value) = value else { - return; - }; - match settings_content - .theme - .icon_theme.as_mut() { - Some(settings::IconThemeSelection::Dynamic{ light, ..}) => *light = value, - _ => return - } - }, - }), - metadata: None, - }, - SettingItem { - files: USER, - title: "Dark Icon Theme", - description: "The icon theme to use when mode is set to dark, or when mode is set to system and it is in dark mode.", - field: Box::new(SettingField { - json_path: Some("icon_theme.dark"), - pick: |settings_content| { - match settings_content.theme.icon_theme.as_ref() { - Some(settings::IconThemeSelection::Dynamic { dark, ..}) => Some(dark), - _ => None - } - }, - write: |settings_content, value| { - let Some(value) = value else { - return; - }; - match settings_content - .theme - .icon_theme.as_mut() { - Some(settings::IconThemeSelection::Dynamic{ dark, ..}) => *dark = value, - _ => return - } - }, - }), - metadata: None, - } - ], - } - }).collect(), - }), - SettingsPageItem::SectionHeader("Buffer Font"), - SettingsPageItem::SettingItem(SettingItem { - title: "Font Family", - description: "Font family for editor text.", - field: Box::new(SettingField { - json_path: Some("buffer_font_family"), - pick: |settings_content| settings_content.theme.buffer_font_family.as_ref(), - write: |settings_content, value|{ settings_content.theme.buffer_font_family = value;}, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Font Size", - description: "Font size for editor text.", - field: Box::new(SettingField { - json_path: Some("buffer_font_size"), - pick: |settings_content| settings_content.theme.buffer_font_size.as_ref(), - write: |settings_content, value|{ settings_content.theme.buffer_font_size = value;}, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Font Weight", - description: "Font weight for editor text (100-900).", - field: Box::new(SettingField { - json_path: Some("buffer_font_weight"), - pick: |settings_content| settings_content.theme.buffer_font_weight.as_ref(), - write: |settings_content, value|{ settings_content.theme.buffer_font_weight = value;}, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::DynamicItem(DynamicItem { - discriminant: SettingItem { - files: USER, - title: "Line Height", - description: "Line height for editor text.", - field: Box::new(SettingField { - json_path: Some("buffer_line_height$"), - pick: |settings_content| { - Some(&dynamic_variants::()[ - settings_content - .theme - .buffer_line_height - .as_ref()? - .discriminant() as usize]) - }, - write: |settings_content, value| { - let Some(value) = value else { - settings_content.theme.buffer_line_height = None; - return; - }; - let settings_value = settings_content.theme.buffer_line_height.get_or_insert_with(|| { - settings::BufferLineHeight::default() - }); - *settings_value = match value { - settings::BufferLineHeightDiscriminants::Comfortable => { - settings::BufferLineHeight::Comfortable - }, - settings::BufferLineHeightDiscriminants::Standard => { - settings::BufferLineHeight::Standard - }, - settings::BufferLineHeightDiscriminants::Custom => { - let custom_value = theme::BufferLineHeight::from(*settings_value).value(); - settings::BufferLineHeight::Custom(custom_value) - }, - }; - }, - }), - metadata: None, - }, - pick_discriminant: |settings_content| { - Some(settings_content.theme.buffer_line_height.as_ref()?.discriminant() as usize) - }, - fields: dynamic_variants::().into_iter().map(|variant| { - match variant { - settings::BufferLineHeightDiscriminants::Comfortable => vec![], - settings::BufferLineHeightDiscriminants::Standard => vec![], - settings::BufferLineHeightDiscriminants::Custom => vec![ - SettingItem { - files: USER, - title: "Custom Line Height", - description: "Custom line height value (must be at least 1.0).", - field: Box::new(SettingField { - json_path: Some("buffer_line_height"), - pick: |settings_content| { - match settings_content.theme.buffer_line_height.as_ref() { - Some(settings::BufferLineHeight::Custom(value)) => Some(value), - _ => None - } - }, - write: |settings_content, value| { - let Some(value) = value else { - return; - }; - match settings_content - .theme - .buffer_line_height.as_mut() { - Some(settings::BufferLineHeight::Custom(line_height)) => *line_height = f32::max(value, 1.0), - _ => return - } - }, - }), - metadata: None, - } - ], - } - }).collect(), - }), - SettingsPageItem::SettingItem(SettingItem { - files: USER, - title: "Font Features", - description: "The OpenType features to enable for rendering in text buffers.", - field: Box::new( - SettingField { - json_path: Some("buffer_font_features"), - pick: |settings_content| { - settings_content.theme.buffer_font_features.as_ref() - }, - write: |settings_content, value| { - settings_content.theme.buffer_font_features = value; - - }, - } - .unimplemented(), - ), - metadata: None, - }), - SettingsPageItem::SettingItem(SettingItem { - files: USER, - title: "Font Fallbacks", - description: "The font fallbacks to use for rendering in text buffers.", - field: Box::new( - SettingField { - json_path: Some("buffer_font_fallbacks"), - pick: |settings_content| { - settings_content.theme.buffer_font_fallbacks.as_ref() - }, - write: |settings_content, value| { - settings_content.theme.buffer_font_fallbacks = value; - - }, - } - .unimplemented(), - ), - metadata: None, - }), - SettingsPageItem::SectionHeader("UI Font"), - SettingsPageItem::SettingItem(SettingItem { - title: "Font Family", - description: "Font family for UI elements.", - field: Box::new(SettingField { - json_path: Some("ui_font_family"), - pick: |settings_content| settings_content.theme.ui_font_family.as_ref(), - write: |settings_content, value|{ settings_content.theme.ui_font_family = value;}, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Font Size", - description: "Font size for UI elements.", - field: Box::new(SettingField { - json_path: Some("ui_font_size"), - pick: |settings_content| settings_content.theme.ui_font_size.as_ref(), - write: |settings_content, value|{ settings_content.theme.ui_font_size = value;}, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Font Weight", - description: "Font weight for UI elements (100-900).", - field: Box::new(SettingField { - json_path: Some("ui_font_weight"), - pick: |settings_content| settings_content.theme.ui_font_weight.as_ref(), - write: |settings_content, value|{ settings_content.theme.ui_font_weight = value;}, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - files: USER, - title: "Font Features", - description: "The OpenType features to enable for rendering in UI elements.", - field: Box::new( - SettingField { - json_path: Some("ui_font_features"), - pick: |settings_content| { - settings_content.theme.ui_font_features.as_ref() - }, - write: |settings_content, value| { - settings_content.theme.ui_font_features = value; - - }, - } - .unimplemented(), - ), - metadata: None, - }), - SettingsPageItem::SettingItem(SettingItem { - files: USER, - title: "Font Fallbacks", - description: "The font fallbacks to use for rendering in the UI.", - field: Box::new( - SettingField { - json_path: Some("ui_font_fallbacks"), - pick: |settings_content| { - settings_content.theme.ui_font_fallbacks.as_ref() - }, - write: |settings_content, value| { - settings_content.theme.ui_font_fallbacks = value; - - }, - } - .unimplemented(), - ), - metadata: None, - }), - SettingsPageItem::SectionHeader("Agent Panel Font"), - SettingsPageItem::SettingItem(SettingItem { - title: "UI Font Size", - description: "Font size for agent response text in the agent panel. Falls back to the regular UI font size.", - field: Box::new(SettingField { - json_path: Some("agent_ui_font_size"), - pick: |settings_content| { - settings_content - .theme - .agent_ui_font_size - .as_ref() - .or(settings_content.theme.ui_font_size.as_ref()) - }, - write: |settings_content, value|{ settings_content.theme.agent_ui_font_size = value;}, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Buffer Font Size", - description: "Font size for user messages text in the agent panel.", - field: Box::new(SettingField { - json_path: Some("agent_buffer_font_size"), - pick: |settings_content| { - settings_content - .theme - .agent_buffer_font_size - .as_ref() - .or(settings_content.theme.buffer_font_size.as_ref()) - }, - write: |settings_content, value| { - settings_content.theme.agent_buffer_font_size = value; - - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Cursor"), - SettingsPageItem::SettingItem(SettingItem { - title: "Multi Cursor Modifier", - description: "Modifier key for adding multiple cursors.", - field: Box::new(SettingField { - json_path: Some("multi_cursor_modifier"), - pick: |settings_content| { - settings_content.editor.multi_cursor_modifier.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.multi_cursor_modifier = value; - - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Cursor Blink", - description: "Whether the cursor blinks in the editor.", - field: Box::new(SettingField { - json_path: Some("cursor_blink"), - pick: |settings_content| settings_content.editor.cursor_blink.as_ref(), - write: |settings_content, value|{ settings_content.editor.cursor_blink = value;}, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Cursor Shape", - description: "Cursor shape for the editor.", - field: Box::new(SettingField { - json_path: Some("cursor_shape"), - pick: |settings_content| settings_content.editor.cursor_shape.as_ref(), - write: |settings_content, value|{ settings_content.editor.cursor_shape = value;}, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Hide Mouse", - description: "When to hide the mouse cursor.", - field: Box::new(SettingField { - json_path: Some("hide_mouse"), - pick: |settings_content| settings_content.editor.hide_mouse.as_ref(), - write: |settings_content, value|{ settings_content.editor.hide_mouse = value;}, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Highlighting"), - SettingsPageItem::SettingItem(SettingItem { - title: "Unnecessary Code Fade", - description: "How much to fade out unused code (0.0 - 0.9).", - field: Box::new(SettingField { - json_path: Some("unnecessary_code_fade"), - pick: |settings_content| { - settings_content.theme.unnecessary_code_fade.as_ref() - }, - write: |settings_content, value| { - settings_content.theme.unnecessary_code_fade = value; - - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Current Line Highlight", - description: "How to highlight the current line.", - field: Box::new(SettingField { - json_path: Some("current_line_highlight"), - pick: |settings_content| { - settings_content.editor.current_line_highlight.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.current_line_highlight = value; - - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Selection Highlight", - description: "Highlight all occurrences of selected text.", - field: Box::new(SettingField { - json_path: Some("selection_highlight"), - pick: |settings_content| { - settings_content.editor.selection_highlight.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.selection_highlight = value; - - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Rounded Selection", - description: "Whether the text selection should have rounded corners.", - field: Box::new(SettingField { - json_path: Some("rounded_selection"), - pick: |settings_content| settings_content.editor.rounded_selection.as_ref(), - write: |settings_content, value|{ settings_content.editor.rounded_selection = value;}, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Minimum Contrast For Highlights", - description: "The minimum APCA perceptual contrast to maintain when rendering text over highlight backgrounds.", - field: Box::new(SettingField { - json_path: Some("minimum_contrast_for_highlights"), - pick: |settings_content| { - settings_content - .editor - .minimum_contrast_for_highlights - .as_ref() - }, - write: |settings_content, value| { - settings_content.editor.minimum_contrast_for_highlights = value; - - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Guides"), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Wrap Guides", - description: "Show wrap guides (vertical rulers).", - field: Box::new(SettingField { - json_path: Some("show_wrap_guides"), - pick: |settings_content| { - settings_content - .project - .all_languages - .defaults - .show_wrap_guides - .as_ref() - }, - write: |settings_content, value| { - settings_content - - .project - .all_languages - .defaults - .show_wrap_guides = value; - }, - }), - metadata: None, - files: USER | PROJECT, - }), - // todo(settings_ui): This needs a custom component - SettingsPageItem::SettingItem(SettingItem { - title: "Wrap Guides", - description: "Character counts at which to show wrap guides.", - field: Box::new( - SettingField { - json_path: Some("wrap_guides"), - pick: |settings_content| { - settings_content - .project - .all_languages - .defaults - .wrap_guides - .as_ref() - }, - write: |settings_content, value| { - settings_content.project.all_languages.defaults.wrap_guides = value; - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - ], - }, - SettingsPage { - title: "Keymap", - items: vec![ - SettingsPageItem::SectionHeader("Base Keymap"), - SettingsPageItem::SettingItem(SettingItem { - title: "Base Keymap", - description: "The name of a base set of key bindings to use.", - field: Box::new(SettingField { - json_path: Some("base_keymap"), - pick: |settings_content| settings_content.base_keymap.as_ref(), - write: |settings_content, value| { - settings_content.base_keymap = value; - }, - }), - metadata: Some(Box::new(SettingsFieldMetadata { - should_do_titlecase: Some(false), - ..Default::default() - })), - files: USER, - }), - SettingsPageItem::SectionHeader("Modal Editing"), - // todo(settings_ui): Vim/Helix Mode should be apart of one type because it's undefined - // behavior to have them both enabled at the same time - SettingsPageItem::SettingItem(SettingItem { - title: "Vim Mode", - description: "Enable Vim mode and key bindings.", - field: Box::new(SettingField { - json_path: Some("vim_mode"), - pick: |settings_content| settings_content.vim_mode.as_ref(), - write: |settings_content, value| { - settings_content.vim_mode = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Helix Mode", - description: "Enable Helix mode and key bindings.", - field: Box::new(SettingField { - json_path: Some("helix_mode"), - pick: |settings_content| settings_content.helix_mode.as_ref(), - write: |settings_content, value| { - settings_content.helix_mode = value; - }, - }), - metadata: None, - files: USER, - }), - ], - }, - SettingsPage { - title: "Editor", - items: { - let mut items = vec![ - SettingsPageItem::SectionHeader("Auto Save"), - SettingsPageItem::DynamicItem(DynamicItem { - discriminant: SettingItem { - files: USER, - title: "Auto Save Mode", - description: "When to auto save buffer changes.", - field: Box::new(SettingField { - json_path: Some("autosave$"), - pick: |settings_content| { - Some(&dynamic_variants::()[ - settings_content - .workspace - .autosave - .as_ref()? - .discriminant() as usize]) - }, - write: |settings_content, value| { - let Some(value) = value else { - settings_content.workspace.autosave = None; - return; - }; - let settings_value = settings_content.workspace.autosave.get_or_insert_with(|| { - settings::AutosaveSetting::Off - }); - *settings_value = match value { - settings::AutosaveSettingDiscriminants::Off => { - settings::AutosaveSetting::Off - }, - settings::AutosaveSettingDiscriminants::AfterDelay => { - let milliseconds = match settings_value { - settings::AutosaveSetting::AfterDelay { milliseconds } => *milliseconds, - _ => settings::DelayMs(1000), - }; - settings::AutosaveSetting::AfterDelay { milliseconds } - }, - settings::AutosaveSettingDiscriminants::OnFocusChange => { - settings::AutosaveSetting::OnFocusChange - }, - settings::AutosaveSettingDiscriminants::OnWindowChange => { - settings::AutosaveSetting::OnWindowChange - }, - }; - }, - }), - metadata: None, - }, - pick_discriminant: |settings_content| { - Some(settings_content.workspace.autosave.as_ref()?.discriminant() as usize) - }, - fields: dynamic_variants::().into_iter().map(|variant| { - match variant { - settings::AutosaveSettingDiscriminants::Off => vec![], - settings::AutosaveSettingDiscriminants::AfterDelay => vec![ - SettingItem { - files: USER, - title: "Delay (milliseconds)", - description: "Save after inactivity period (in milliseconds).", - field: Box::new(SettingField { - json_path: Some("autosave.after_delay.milliseconds"), - pick: |settings_content| { - match settings_content.workspace.autosave.as_ref() { - Some(settings::AutosaveSetting::AfterDelay { milliseconds }) => Some(milliseconds), - _ => None - } - }, - write: |settings_content, value| { - let Some(value) = value else { - settings_content.workspace.autosave = None; - return; - }; - match settings_content - .workspace - .autosave.as_mut() { - Some(settings::AutosaveSetting::AfterDelay { milliseconds }) => *milliseconds = value, - _ => return - } - }, - }), - metadata: None, - } - ], - settings::AutosaveSettingDiscriminants::OnFocusChange => vec![], - settings::AutosaveSettingDiscriminants::OnWindowChange => vec![], - } - }).collect(), - }), - SettingsPageItem::SectionHeader("Multibuffer"), - SettingsPageItem::SettingItem(SettingItem { - title: "Double Click In Multibuffer", - description: "What to do when multibuffer is double-clicked in some of its excerpts.", - field: Box::new(SettingField { - json_path: Some("double_click_in_multibuffer"), - pick: |settings_content| { - settings_content.editor.double_click_in_multibuffer.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.double_click_in_multibuffer = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Expand Excerpt Lines", - description: "How many lines to expand the multibuffer excerpts by default.", - field: Box::new(SettingField { - json_path: Some("expand_excerpt_lines"), - pick: |settings_content| { - settings_content.editor.expand_excerpt_lines.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.expand_excerpt_lines = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Excerpt Context Lines", - description: "How many lines of context to provide in multibuffer excerpts by default.", - field: Box::new(SettingField { - json_path: Some("excerpt_context_lines"), - pick: |settings_content| { - settings_content.editor.excerpt_context_lines.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.excerpt_context_lines = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Expand Outlines With Depth", - description: "Default depth to expand outline items in the current file.", - field: Box::new(SettingField { - json_path: Some("outline_panel.expand_outlines_with_depth"), - pick: |settings_content| { - settings_content - .outline_panel - .as_ref() - .and_then(|outline_panel| { - outline_panel.expand_outlines_with_depth.as_ref() - }) - }, - write: |settings_content, value| { - settings_content - .outline_panel - .get_or_insert_default() - .expand_outlines_with_depth = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Scrolling"), - SettingsPageItem::SettingItem(SettingItem { - title: "Scroll Beyond Last Line", - description: "Whether the editor will scroll beyond the last line.", - field: Box::new(SettingField { - json_path: Some("scroll_beyond_last_line"), - pick: |settings_content| { - settings_content.editor.scroll_beyond_last_line.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.scroll_beyond_last_line = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Vertical Scroll Margin", - description: "The number of lines to keep above/below the cursor when auto-scrolling.", - field: Box::new(SettingField { - json_path: Some("vertical_scroll_margin"), - pick: |settings_content| { - settings_content.editor.vertical_scroll_margin.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.vertical_scroll_margin = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Horizontal Scroll Margin", - description: "The number of characters to keep on either side when scrolling with the mouse.", - field: Box::new(SettingField { - json_path: Some("horizontal_scroll_margin"), - pick: |settings_content| { - settings_content.editor.horizontal_scroll_margin.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.horizontal_scroll_margin = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Scroll Sensitivity", - description: "Scroll sensitivity multiplier for both horizontal and vertical scrolling.", - field: Box::new(SettingField { - json_path: Some("scroll_sensitivity"), - pick: |settings_content| { - settings_content.editor.scroll_sensitivity.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.scroll_sensitivity = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Fast Scroll Sensitivity", - description: "Fast scroll sensitivity multiplier for both horizontal and vertical scrolling.", - field: Box::new(SettingField { - json_path: Some("fast_scroll_sensitivity"), - pick: |settings_content| { - settings_content.editor.fast_scroll_sensitivity.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.fast_scroll_sensitivity = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Autoscroll On Clicks", - description: "Whether to scroll when clicking near the edge of the visible text area.", - field: Box::new(SettingField { - json_path: Some("autoscroll_on_clicks"), - pick: |settings_content| { - settings_content.editor.autoscroll_on_clicks.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.autoscroll_on_clicks = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Sticky Scroll", - description: "Whether to stick scopes to the top of the editor", - field: Box::new(SettingField { - json_path: Some("sticky_scroll.enabled"), - pick: |settings_content| { - settings_content.editor.sticky_scroll.as_ref().and_then(|sticky_scroll| sticky_scroll.enabled.as_ref()) - }, - write: |settings_content, value| { - settings_content.editor.sticky_scroll.get_or_insert_default().enabled = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Signature Help"), - SettingsPageItem::SettingItem(SettingItem { - title: "Auto Signature Help", - description: "Automatically show a signature help pop-up.", - field: Box::new(SettingField { - json_path: Some("auto_signature_help"), - pick: |settings_content| { - settings_content.editor.auto_signature_help.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.auto_signature_help = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Signature Help After Edits", - description: "Show the signature help pop-up after completions or bracket pairs are inserted.", - field: Box::new(SettingField { - json_path: Some("show_signature_help_after_edits"), - pick: |settings_content| { - settings_content - .editor - .show_signature_help_after_edits - .as_ref() - }, - write: |settings_content, value| { - settings_content.editor.show_signature_help_after_edits = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Snippet Sort Order", - description: "Determines how snippets are sorted relative to other completion items.", - field: Box::new(SettingField { - json_path: Some("snippet_sort_order"), - pick: |settings_content| { - settings_content.editor.snippet_sort_order.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.snippet_sort_order = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Hover Popover"), - SettingsPageItem::SettingItem(SettingItem { - title: "Enabled", - description: "Show the informational hover box when moving the mouse over symbols in the editor.", - field: Box::new(SettingField { - json_path: Some("hover_popover_enabled"), - pick: |settings_content| { - settings_content.editor.hover_popover_enabled.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.hover_popover_enabled = value; - }, - }), - metadata: None, - files: USER, - }), - // todo(settings ui): add units to this number input - SettingsPageItem::SettingItem(SettingItem { - title: "Delay", - description: "Time to wait in milliseconds before showing the informational hover box.", - field: Box::new(SettingField { - json_path: Some("hover_popover_enabled"), - pick: |settings_content| { - settings_content.editor.hover_popover_delay.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.hover_popover_delay = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Drag And Drop Selection"), - SettingsPageItem::SettingItem(SettingItem { - title: "Enabled", - description: "Enable drag and drop selection.", - field: Box::new(SettingField { - json_path: Some("drag_and_drop_selection.enabled"), - pick: |settings_content| { - settings_content - .editor - .drag_and_drop_selection - .as_ref() - .and_then(|drag_and_drop| drag_and_drop.enabled.as_ref()) - }, - write: |settings_content, value| { - settings_content - .editor - .drag_and_drop_selection - .get_or_insert_default() - .enabled = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Delay", - description: "Delay in milliseconds before drag and drop selection starts.", - field: Box::new(SettingField { - json_path: Some("drag_and_drop_selection.delay"), - pick: |settings_content| { - settings_content - .editor - .drag_and_drop_selection - .as_ref() - .and_then(|drag_and_drop| drag_and_drop.delay.as_ref()) - }, - write: |settings_content, value| { - settings_content - .editor - .drag_and_drop_selection - .get_or_insert_default() - .delay = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Gutter"), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Line Numbers", - description: "Show line numbers in the gutter.", - field: Box::new(SettingField { - json_path: Some("gutter.line_numbers"), - pick: |settings_content| { - settings_content - .editor - .gutter - .as_ref() - .and_then(|gutter| gutter.line_numbers.as_ref()) - }, - write: |settings_content, value| { - settings_content - .editor - .gutter - .get_or_insert_default() - .line_numbers = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Relative Line Numbers", - description: "Controls line number display in the editor's gutter. \"disabled\" shows absolute line numbers, \"enabled\" shows relative line numbers for each absolute line, and \"wrapped\" shows relative line numbers for every line, absolute or wrapped.", - field: Box::new(SettingField { - json_path: Some("relative_line_numbers"), - pick: |settings_content| { - settings_content.editor.relative_line_numbers.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.relative_line_numbers = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Runnables", - description: "Show runnable buttons in the gutter.", - field: Box::new(SettingField { - json_path: Some("gutter.runnables"), - pick: |settings_content| { - settings_content - .editor - .gutter - .as_ref() - .and_then(|gutter| gutter.runnables.as_ref()) - }, - write: |settings_content, value| { - settings_content - .editor - .gutter - .get_or_insert_default() - .runnables = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Breakpoints", - description: "Show breakpoints in the gutter.", - field: Box::new(SettingField { - json_path: Some("gutter.breakpoints"), - pick: |settings_content| { - settings_content - .editor - .gutter - .as_ref() - .and_then(|gutter| gutter.breakpoints.as_ref()) - }, - write: |settings_content, value| { - settings_content - .editor - .gutter - .get_or_insert_default() - .breakpoints = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Folds", - description: "Show code folding controls in the gutter.", - field: Box::new(SettingField { - json_path: Some("gutter.folds"), - pick: |settings_content| { - settings_content - .editor - .gutter - .as_ref() - .and_then(|gutter| gutter.folds.as_ref()) - }, - write: |settings_content, value| { - settings_content.editor.gutter.get_or_insert_default().folds = - value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Min Line Number Digits", - description: "Minimum number of characters to reserve space for in the gutter.", - field: Box::new(SettingField { - json_path: Some("gutter.min_line_number_digits"), - pick: |settings_content| { - settings_content - .editor - .gutter - .as_ref() - .and_then(|gutter| gutter.min_line_number_digits.as_ref()) - }, - write: |settings_content, value| { - settings_content - .editor - .gutter - .get_or_insert_default() - .min_line_number_digits = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Inline Code Actions", - description: "Show code action button at start of buffer line.", - field: Box::new(SettingField { - json_path: Some("inline_code_actions"), - pick: |settings_content| { - settings_content.editor.inline_code_actions.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.inline_code_actions = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Scrollbar"), - SettingsPageItem::SettingItem(SettingItem { - title: "Show", - description: "When to show the scrollbar in the editor.", - field: Box::new(SettingField { - json_path: Some("scrollbar"), - pick: |settings_content| { - settings_content.editor.scrollbar.as_ref()?.show.as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .scrollbar - .get_or_insert_default() - .show = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Cursors", - description: "Show cursor positions in the scrollbar.", - field: Box::new(SettingField { - json_path: Some("scrollbar.cursors"), - pick: |settings_content| { - settings_content.editor.scrollbar.as_ref()?.cursors.as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .scrollbar - .get_or_insert_default() - .cursors = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Git Diff", - description: "Show Git diff indicators in the scrollbar.", - field: Box::new(SettingField { - json_path: Some("scrollbar.git_diff"), - pick: |settings_content| { - settings_content - .editor - .scrollbar - .as_ref()? - .git_diff - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .scrollbar - .get_or_insert_default() - .git_diff = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Search Results", - description: "Show buffer search result indicators in the scrollbar.", - field: Box::new(SettingField { - json_path: Some("scrollbar.search_results"), - pick: |settings_content| { - settings_content - .editor - .scrollbar - .as_ref()? - .search_results - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .scrollbar - .get_or_insert_default() - .search_results = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Selected Text", - description: "Show selected text occurrences in the scrollbar.", - field: Box::new(SettingField { - json_path: Some("scrollbar.selected_text"), - pick: |settings_content| { - settings_content - .editor - .scrollbar - .as_ref()? - .selected_text - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .scrollbar - .get_or_insert_default() - .selected_text = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Selected Symbol", - description: "Show selected symbol occurrences in the scrollbar.", - field: Box::new(SettingField { - json_path: Some("scrollbar.selected_symbol"), - pick: |settings_content| { - settings_content - .editor - .scrollbar - .as_ref()? - .selected_symbol - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .scrollbar - .get_or_insert_default() - .selected_symbol = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Diagnostics", - description: "Which diagnostic indicators to show in the scrollbar.", - field: Box::new(SettingField { - json_path: Some("scrollbar.diagnostics"), - pick: |settings_content| { - settings_content - .editor - .scrollbar - .as_ref()? - .diagnostics - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .scrollbar - .get_or_insert_default() - .diagnostics = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Horizontal Scrollbar", - description: "When false, forcefully disables the horizontal scrollbar.", - field: Box::new(SettingField { - json_path: Some("scrollbar.axes.horizontal"), - pick: |settings_content| { - settings_content - .editor - .scrollbar - .as_ref()? - .axes - .as_ref()? - .horizontal - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .scrollbar - .get_or_insert_default() - .axes - .get_or_insert_default() - .horizontal = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Vertical Scrollbar", - description: "When false, forcefully disables the vertical scrollbar.", - field: Box::new(SettingField { - json_path: Some("scrollbar.axes.vertical"), - pick: |settings_content| { - settings_content - .editor - .scrollbar - .as_ref()? - .axes - .as_ref()? - .vertical - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .scrollbar - .get_or_insert_default() - .axes - .get_or_insert_default() - .vertical = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Minimap"), - SettingsPageItem::SettingItem(SettingItem { - title: "Show", - description: "When to show the minimap in the editor.", - field: Box::new(SettingField { - json_path: Some("minimap.show"), - pick: |settings_content| { - settings_content.editor.minimap.as_ref()?.show.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.minimap.get_or_insert_default().show = - value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Display In", - description: "Where to show the minimap in the editor.", - field: Box::new(SettingField { - json_path: Some("minimap.display_in"), - pick: |settings_content| { - settings_content - .editor - .minimap - .as_ref()? - .display_in - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .minimap - .get_or_insert_default() - .display_in = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Thumb", - description: "When to show the minimap thumb.", - field: Box::new(SettingField { - json_path: Some("minimap.thumb"), - pick: |settings_content| { - settings_content.editor.minimap.as_ref()?.thumb.as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .minimap - .get_or_insert_default() - .thumb = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Thumb Border", - description: "Border style for the minimap's scrollbar thumb.", - field: Box::new(SettingField { - json_path: Some("minimap.thumb_border"), - pick: |settings_content| { - settings_content - .editor - .minimap - .as_ref()? - .thumb_border - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .minimap - .get_or_insert_default() - .thumb_border = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Current Line Highlight", - description: "How to highlight the current line in the minimap.", - field: Box::new(SettingField { - json_path: Some("minimap.current_line_highlight"), - pick: |settings_content| { - settings_content - .editor - .minimap - .as_ref() - .and_then(|minimap| minimap.current_line_highlight.as_ref()) - .or(settings_content.editor.current_line_highlight.as_ref()) - }, - write: |settings_content, value| { - settings_content - .editor - .minimap - .get_or_insert_default() - .current_line_highlight = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Max Width Columns", - description: "Maximum number of columns to display in the minimap.", - field: Box::new(SettingField { - json_path: Some("minimap.max_width_columns"), - pick: |settings_content| { - settings_content - .editor - .minimap - .as_ref()? - .max_width_columns - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .minimap - .get_or_insert_default() - .max_width_columns = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Toolbar"), - SettingsPageItem::SettingItem(SettingItem { - title: "Breadcrumbs", - description: "Show breadcrumbs.", - field: Box::new(SettingField { - json_path: Some("toolbar.breadcrumbs"), - pick: |settings_content| { - settings_content - .editor - .toolbar - .as_ref()? - .breadcrumbs - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .toolbar - .get_or_insert_default() - .breadcrumbs = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Quick Actions", - description: "Show quick action buttons (e.g., search, selection, editor controls, etc.).", - field: Box::new(SettingField { - json_path: Some("toolbar.quick_actions"), - pick: |settings_content| { - settings_content - .editor - .toolbar - .as_ref()? - .quick_actions - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .toolbar - .get_or_insert_default() - .quick_actions = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Selections Menu", - description: "Show the selections menu in the editor toolbar.", - field: Box::new(SettingField { - json_path: Some("toolbar.selections_menu"), - pick: |settings_content| { - settings_content - .editor - .toolbar - .as_ref()? - .selections_menu - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .toolbar - .get_or_insert_default() - .selections_menu = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Agent Review", - description: "Show agent review buttons in the editor toolbar.", - field: Box::new(SettingField { - json_path: Some("toolbar.agent_review"), - pick: |settings_content| { - settings_content - .editor - .toolbar - .as_ref()? - .agent_review - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .toolbar - .get_or_insert_default() - .agent_review = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Code Actions", - description: "Show code action buttons in the editor toolbar.", - field: Box::new(SettingField { - json_path: Some("toolbar.code_actions"), - pick: |settings_content| { - settings_content - .editor - .toolbar - .as_ref()? - .code_actions - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .toolbar - .get_or_insert_default() - .code_actions = value; - }, - }), - metadata: None, - files: USER, - }), - ]; - items.extend(language_settings_data()); - items - }, - }, - SettingsPage { - title: "Languages & Tools", - items: { - let mut items = vec![]; - items.extend(non_editor_language_settings_data()); - items.extend([ - SettingsPageItem::SectionHeader("File Types"), - SettingsPageItem::SettingItem(SettingItem { - title: "File Type Associations", - description: "A mapping from languages to files and file extensions that should be treated as that language.", - field: Box::new( - SettingField { - json_path: Some("file_type_associations"), - pick: |settings_content| { - settings_content.project.all_languages.file_types.as_ref() - }, - write: |settings_content, value| { - settings_content.project.all_languages.file_types = value; - - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - ]); - - items.extend([ - SettingsPageItem::SectionHeader("Diagnostics"), - SettingsPageItem::SettingItem(SettingItem { - title: "Max Severity", - description: "Which level to use to filter out diagnostics displayed in the editor.", - field: Box::new(SettingField { - json_path: Some("diagnostics_max_severity"), - pick: |settings_content| settings_content.editor.diagnostics_max_severity.as_ref(), - write: |settings_content, value| { - settings_content.editor.diagnostics_max_severity = value; - - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Include Warnings", - description: "Whether to show warnings or not by default.", - field: Box::new(SettingField { - json_path: Some("diagnostics.include_warnings"), - pick: |settings_content| { - settings_content.diagnostics.as_ref()?.include_warnings.as_ref() - }, - write: |settings_content, value| { - settings_content - - .diagnostics - .get_or_insert_default() - .include_warnings - = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Inline Diagnostics"), - SettingsPageItem::SettingItem(SettingItem { - title: "Enabled", - description: "Whether to show diagnostics inline or not.", - field: Box::new(SettingField { - json_path: Some("diagnostics.inline.enabled"), - pick: |settings_content| { - settings_content.diagnostics.as_ref()?.inline.as_ref()?.enabled.as_ref() - }, - write: |settings_content, value| { - settings_content - - .diagnostics - .get_or_insert_default() - .inline - .get_or_insert_default() - .enabled - = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Update Debounce", - description: "The delay in milliseconds to show inline diagnostics after the last diagnostic update.", - field: Box::new(SettingField { - json_path: Some("diagnostics.inline.update_debounce_ms"), - pick: |settings_content| { - settings_content.diagnostics.as_ref()?.inline.as_ref()?.update_debounce_ms.as_ref() - }, - write: |settings_content, value| { - settings_content - - .diagnostics - .get_or_insert_default() - .inline - .get_or_insert_default() - .update_debounce_ms - = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Padding", - description: "The amount of padding between the end of the source line and the start of the inline diagnostic.", - field: Box::new(SettingField { - json_path: Some("diagnostics.inline.padding"), - pick: |settings_content| { - settings_content.diagnostics.as_ref()?.inline.as_ref()?.padding.as_ref() - }, - write: |settings_content, value| { - settings_content - - .diagnostics - .get_or_insert_default() - .inline - .get_or_insert_default() - .padding - = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Minimum Column", - description: "The minimum column at which to display inline diagnostics.", - field: Box::new(SettingField { - json_path: Some("diagnostics.inline.min_column"), - pick: |settings_content| { - settings_content.diagnostics.as_ref()?.inline.as_ref()?.min_column.as_ref() - }, - write: |settings_content, value| { - settings_content - - .diagnostics - .get_or_insert_default() - .inline - .get_or_insert_default() - .min_column - = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("LSP Pull Diagnostics"), - SettingsPageItem::SettingItem(SettingItem { - title: "Enabled", - description: "Whether to pull for language server-powered diagnostics or not.", - field: Box::new(SettingField { - json_path: Some("diagnostics.lsp_pull_diagnostics.enabled"), - pick: |settings_content| { - settings_content.diagnostics.as_ref()?.lsp_pull_diagnostics.as_ref()?.enabled.as_ref() - }, - write: |settings_content, value| { - settings_content - - .diagnostics - .get_or_insert_default() - .lsp_pull_diagnostics - .get_or_insert_default() - .enabled - = value; - }, - }), - metadata: None, - files: USER, - }), - // todo(settings_ui): Needs unit - SettingsPageItem::SettingItem(SettingItem { - title: "Debounce", - description: "Minimum time to wait before pulling diagnostics from the language server(s).", - field: Box::new(SettingField { - json_path: Some("diagnostics.lsp_pull_diagnostics.debounce_ms"), - pick: |settings_content| { - settings_content.diagnostics.as_ref()?.lsp_pull_diagnostics.as_ref()?.debounce_ms.as_ref() - }, - write: |settings_content, value| { - settings_content - - .diagnostics - .get_or_insert_default() - .lsp_pull_diagnostics - .get_or_insert_default() - .debounce_ms - = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("LSP Highlights"), - SettingsPageItem::SettingItem(SettingItem { - title: "Debounce", - description: "The debounce delay before querying highlights from the language.", - field: Box::new(SettingField { - json_path: Some("lsp_highlight_debounce"), - pick: |settings_content| settings_content.editor.lsp_highlight_debounce.as_ref(), - write: |settings_content, value| { - settings_content.editor.lsp_highlight_debounce = value; - }, - }), - metadata: None, - files: USER, - }), - ]); - - // todo(settings_ui): Refresh on extension (un)/installed - // Note that `crates/json_schema_store` solves the same problem, there is probably a way to unify the two - items.push(SettingsPageItem::SectionHeader(LANGUAGES_SECTION_HEADER)); - items.extend(all_language_names(cx).into_iter().map(|language_name| { - SettingsPageItem::SubPageLink(SubPageLink { - title: language_name, - files: USER | PROJECT, - render: Arc::new(|this, window, cx| { - this.render_sub_page_items( - language_settings_data() - .iter() - .chain(non_editor_language_settings_data().iter()) - .chain(edit_prediction_language_settings_section().iter()) - .enumerate(), - None, - window, - cx, - ) - .into_any_element() - }), - }) - })); - items - }, - }, - SettingsPage { - title: "Search & Files", - items: vec![ - SettingsPageItem::SectionHeader("Search"), - SettingsPageItem::SettingItem(SettingItem { - title: "Whole Word", - description: "Search for whole words by default.", - field: Box::new(SettingField { - json_path: Some("search.whole_word"), - pick: |settings_content| { - settings_content.editor.search.as_ref()?.whole_word.as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .search - .get_or_insert_default() - .whole_word = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Case Sensitive", - description: "Search case-sensitively by default.", - field: Box::new(SettingField { - json_path: Some("search.case_sensitive"), - pick: |settings_content| { - settings_content - .editor - .search - .as_ref()? - .case_sensitive - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .search - .get_or_insert_default() - .case_sensitive = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Use Smartcase Search", - description: "Whether to automatically enable case-sensitive search based on the search query.", - field: Box::new(SettingField { - json_path: Some("use_smartcase_search"), - pick: |settings_content| { - settings_content.editor.use_smartcase_search.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.use_smartcase_search = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Include Ignored", - description: "Include ignored files in search results by default.", - field: Box::new(SettingField { - json_path: Some("search.include_ignored"), - pick: |settings_content| { - settings_content - .editor - .search - .as_ref()? - .include_ignored - .as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .search - .get_or_insert_default() - .include_ignored = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Regex", - description: "Use regex search by default.", - field: Box::new(SettingField { - json_path: Some("search.regex"), - pick: |settings_content| { - settings_content.editor.search.as_ref()?.regex.as_ref() - }, - write: |settings_content, value| { - settings_content.editor.search.get_or_insert_default().regex = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Search Wrap", - description: "Whether the editor search results will loop.", - field: Box::new(SettingField { - json_path: Some("search_wrap"), - pick: |settings_content| settings_content.editor.search_wrap.as_ref(), - write: |settings_content, value| { - settings_content.editor.search_wrap = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Center on Match", - description: "Whether to center the current match in the editor", - field: Box::new(SettingField { - json_path: Some("editor.search.center_on_match"), - pick: |settings_content| { - settings_content - .editor - .search - .as_ref() - .and_then(|search| search.center_on_match.as_ref()) - }, - write: |settings_content, value| { - settings_content - .editor - .search - .get_or_insert_default() - .center_on_match = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Seed Search Query From Cursor", - description: "When to populate a new search's query based on the text under the cursor.", - field: Box::new(SettingField { - json_path: Some("seed_search_query_from_cursor"), - pick: |settings_content| { - settings_content - .editor - .seed_search_query_from_cursor - .as_ref() - }, - write: |settings_content, value| { - settings_content.editor.seed_search_query_from_cursor = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("File Finder"), - // todo: null by default - SettingsPageItem::SettingItem(SettingItem { - title: "Include Ignored in Search", - description: "Use gitignored files when searching.", - field: Box::new( - SettingField { - json_path: Some("file_finder.include_ignored"), - pick: |settings_content| { - settings_content - .file_finder - .as_ref()? - .include_ignored - .as_ref() - }, - write: |settings_content, value| { - settings_content - .file_finder - .get_or_insert_default() - .include_ignored = value; - }, - } - ), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "File Icons", - description: "Show file icons in the file finder.", - field: Box::new(SettingField { - json_path: Some("file_finder.file_icons"), - pick: |settings_content| { - settings_content.file_finder.as_ref()?.file_icons.as_ref() - }, - write: |settings_content, value| { - settings_content - .file_finder - .get_or_insert_default() - .file_icons = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Modal Max Width", - description: "Determines how much space the file finder can take up in relation to the available window width.", - field: Box::new(SettingField { - json_path: Some("file_finder.modal_max_width"), - pick: |settings_content| { - settings_content - .file_finder - .as_ref()? - .modal_max_width - .as_ref() - }, - write: |settings_content, value| { - settings_content - .file_finder - .get_or_insert_default() - .modal_max_width = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Skip Focus For Active In Search", - description: "Whether the file finder should skip focus for the active file in search results.", - field: Box::new(SettingField { - json_path: Some("file_finder.skip_focus_for_active_in_search"), - pick: |settings_content| { - settings_content - .file_finder - .as_ref()? - .skip_focus_for_active_in_search - .as_ref() - }, - write: |settings_content, value| { - settings_content - .file_finder - .get_or_insert_default() - .skip_focus_for_active_in_search = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Git Status", - description: "Show the Git status in the file finder.", - field: Box::new(SettingField { - json_path: Some("file_finder.git_status"), - pick: |settings_content| { - settings_content.file_finder.as_ref()?.git_status.as_ref() - }, - write: |settings_content, value| { - settings_content - .file_finder - .get_or_insert_default() - .git_status = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("File Scan"), - SettingsPageItem::SettingItem(SettingItem { - title: "File Scan Exclusions", - description: "Files or globs of files that will be excluded by Zed entirely. They will be skipped during file scans, file searches, and not be displayed in the project file tree. Takes precedence over \"File Scan Inclusions\"", - field: Box::new( - SettingField { - json_path: Some("file_scan_exclusions"), - pick: |settings_content| { - settings_content - .project - .worktree - .file_scan_exclusions - .as_ref() - }, - write: |settings_content, value| { - settings_content.project.worktree.file_scan_exclusions = value; - }, - } - .unimplemented(), - ), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "File Scan Inclusions", - description: "Files or globs of files that will be included by Zed, even when ignored by git. This is useful for files that are not tracked by git, but are still important to your project. Note that globs that are overly broad can slow down Zed's file scanning. \"File Scan Exclusions\" takes precedence over these inclusions", - field: Box::new( - SettingField { - json_path: Some("file_scan_inclusions"), - pick: |settings_content| { - settings_content - .project - .worktree - .file_scan_inclusions - .as_ref() - }, - write: |settings_content, value| { - settings_content.project.worktree.file_scan_inclusions = value; - }, - } - .unimplemented(), - ), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Restore File State", - description: "Restore previous file state when reopening.", - field: Box::new(SettingField { - json_path: Some("restore_on_file_reopen"), - pick: |settings_content| { - settings_content.workspace.restore_on_file_reopen.as_ref() - }, - write: |settings_content, value| { - settings_content.workspace.restore_on_file_reopen = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Close on File Delete", - description: "Automatically close files that have been deleted.", - field: Box::new(SettingField { - json_path: Some("close_on_file_delete"), - pick: |settings_content| { - settings_content.workspace.close_on_file_delete.as_ref() - }, - write: |settings_content, value| { - settings_content.workspace.close_on_file_delete = value; - }, - }), - metadata: None, - files: USER, - }), - ], - }, - SettingsPage { - title: "Window & Layout", - items: vec![ - SettingsPageItem::SectionHeader("Status Bar"), - SettingsPageItem::SettingItem(SettingItem { - title: "Project Panel Button", - description: "Show the project panel button in the status bar.", - field: Box::new(SettingField { - json_path: Some("project_panel.button"), - pick: |settings_content| { - settings_content.project_panel.as_ref()?.button.as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .button = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Active Language Button", - description: "Show the active language button in the status bar.", - field: Box::new(SettingField { - json_path: Some("status_bar.active_language_button"), - pick: |settings_content| { - settings_content - .status_bar - .as_ref()? - .active_language_button - .as_ref() - }, - write: |settings_content, value| { - settings_content - .status_bar - .get_or_insert_default() - .active_language_button = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Cursor Position Button", - description: "Show the cursor position button in the status bar.", - field: Box::new(SettingField { - json_path: Some("status_bar.cursor_position_button"), - pick: |settings_content| { - settings_content - .status_bar - .as_ref()? - .cursor_position_button - .as_ref() - }, - write: |settings_content, value| { - settings_content - .status_bar - .get_or_insert_default() - .cursor_position_button = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Terminal Button", - description: "Show the terminal button in the status bar.", - field: Box::new(SettingField { - json_path: Some("terminal.button"), - pick: |settings_content| { - settings_content.terminal.as_ref()?.button.as_ref() - }, - write: |settings_content, value| { - settings_content.terminal.get_or_insert_default().button = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Diagnostics Button", - description: "Show the project diagnostics button in the status bar.", - field: Box::new(SettingField { - json_path: Some("diagnostics.button"), - pick: |settings_content| { - settings_content.diagnostics.as_ref()?.button.as_ref() - }, - write: |settings_content, value| { - settings_content.diagnostics.get_or_insert_default().button = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Project Search Button", - description: "Show the project search button in the status bar.", - field: Box::new(SettingField { - json_path: Some("search.button"), - pick: |settings_content| { - settings_content.editor.search.as_ref()?.button.as_ref() - }, - write: |settings_content, value| { - settings_content - .editor - .search - .get_or_insert_default() - .button = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Debugger Button", - description: "Show the debugger button in the status bar.", - field: Box::new(SettingField { - json_path: Some("debugger.button"), - pick: |settings_content| { - settings_content.debugger.as_ref()?.button.as_ref() - }, - write: |settings_content, value| { - settings_content.debugger.get_or_insert_default().button = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Title Bar"), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Branch Icon", - description: "Show the branch icon beside branch switcher in the titlebar.", - field: Box::new(SettingField { - json_path: Some("title_bar.show_branch_icon"), - pick: |settings_content| { - settings_content - .title_bar - .as_ref()? - .show_branch_icon - .as_ref() - }, - write: |settings_content, value| { - settings_content - .title_bar - .get_or_insert_default() - .show_branch_icon = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Branch Name", - description: "Show the branch name button in the titlebar.", - field: Box::new(SettingField { - json_path: Some("title_bar.show_branch_name"), - pick: |settings_content| { - settings_content - .title_bar - .as_ref()? - .show_branch_name - .as_ref() - }, - write: |settings_content, value| { - settings_content - .title_bar - .get_or_insert_default() - .show_branch_name = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Project Items", - description: "Show the project host and name in the titlebar.", - field: Box::new(SettingField { - json_path: Some("title_bar.show_project_items"), - pick: |settings_content| { - settings_content - .title_bar - .as_ref()? - .show_project_items - .as_ref() - }, - write: |settings_content, value| { - settings_content - .title_bar - .get_or_insert_default() - .show_project_items = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Onboarding Banner", - description: "Show banners announcing new features in the titlebar.", - field: Box::new(SettingField { - json_path: Some("title_bar.show_onboarding_banner"), - pick: |settings_content| { - settings_content - .title_bar - .as_ref()? - .show_onboarding_banner - .as_ref() - }, - write: |settings_content, value| { - settings_content - .title_bar - .get_or_insert_default() - .show_onboarding_banner = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show User Picture", - description: "Show user picture in the titlebar.", - field: Box::new(SettingField { - json_path: Some("title_bar.show_user_picture"), - pick: |settings_content| { - settings_content - .title_bar - .as_ref()? - .show_user_picture - .as_ref() - }, - write: |settings_content, value| { - settings_content - .title_bar - .get_or_insert_default() - .show_user_picture = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Sign In", - description: "Show the sign in button in the titlebar.", - field: Box::new(SettingField { - json_path: Some("title_bar.show_sign_in"), - pick: |settings_content| { - settings_content.title_bar.as_ref()?.show_sign_in.as_ref() - }, - write: |settings_content, value| { - settings_content - .title_bar - .get_or_insert_default() - .show_sign_in = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Menus", - description: "Show the menus in the titlebar.", - field: Box::new(SettingField { - json_path: Some("title_bar.show_menus"), - pick: |settings_content| { - settings_content.title_bar.as_ref()?.show_menus.as_ref() - }, - write: |settings_content, value| { - settings_content - .title_bar - .get_or_insert_default() - .show_menus = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Tab Bar"), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Tab Bar", - description: "Show the tab bar in the editor.", - field: Box::new(SettingField { - json_path: Some("tab_bar.show"), - pick: |settings_content| settings_content.tab_bar.as_ref()?.show.as_ref(), - write: |settings_content, value| { - settings_content.tab_bar.get_or_insert_default().show = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Git Status In Tabs", - description: "Show the Git file status on a tab item.", - field: Box::new(SettingField { - json_path: Some("tabs.git_status"), - pick: |settings_content| { - settings_content.tabs.as_ref()?.git_status.as_ref() - }, - write: |settings_content, value| { - settings_content.tabs.get_or_insert_default().git_status = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show File Icons In Tabs", - description: "Show the file icon for a tab.", - field: Box::new(SettingField { - json_path: Some("tabs.file_icons"), - pick: |settings_content| { - settings_content.tabs.as_ref()?.file_icons.as_ref() - }, - write: |settings_content, value| { - settings_content.tabs.get_or_insert_default().file_icons = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Tab Close Position", - description: "Position of the close button in a tab.", - field: Box::new(SettingField { - json_path: Some("tabs.close_position"), - pick: |settings_content| { - settings_content.tabs.as_ref()?.close_position.as_ref() - }, - write: |settings_content, value| { - settings_content.tabs.get_or_insert_default().close_position = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - files: USER, - title: "Maximum Tabs", - description: "Maximum open tabs in a pane. Will not close an unsaved tab.", - // todo(settings_ui): The default for this value is null and it's use in code - // is complex, so I'm going to come back to this later - field: Box::new( - SettingField { - json_path: Some("max_tabs"), - pick: |settings_content| settings_content.workspace.max_tabs.as_ref(), - write: |settings_content, value| { - settings_content.workspace.max_tabs = value; - }, - } - .unimplemented(), - ), - metadata: None, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Navigation History Buttons", - description: "Show the navigation history buttons in the tab bar.", - field: Box::new(SettingField { - json_path: Some("tab_bar.show_nav_history_buttons"), - pick: |settings_content| { - settings_content - .tab_bar - .as_ref()? - .show_nav_history_buttons - .as_ref() - }, - write: |settings_content, value| { - settings_content - .tab_bar - .get_or_insert_default() - .show_nav_history_buttons = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Tab Bar Buttons", - description: "Show the tab bar buttons (New, Split Pane, Zoom).", - field: Box::new(SettingField { - json_path: Some("tab_bar.show_tab_bar_buttons"), - pick: |settings_content| { - settings_content - .tab_bar - .as_ref()? - .show_tab_bar_buttons - .as_ref() - }, - write: |settings_content, value| { - settings_content - .tab_bar - .get_or_insert_default() - .show_tab_bar_buttons = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Tab Settings"), - SettingsPageItem::SettingItem(SettingItem { - title: "Activate On Close", - description: "What to do after closing the current tab.", - field: Box::new(SettingField { - json_path: Some("tabs.activate_on_close"), - pick: |settings_content| { - settings_content.tabs.as_ref()?.activate_on_close.as_ref() - }, - write: |settings_content, value| { - settings_content - .tabs - .get_or_insert_default() - .activate_on_close = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Tab Show Diagnostics", - description: "Which files containing diagnostic errors/warnings to mark in the tabs.", - field: Box::new(SettingField { - json_path: Some("tabs.show_diagnostics"), - pick: |settings_content| { - settings_content.tabs.as_ref()?.show_diagnostics.as_ref() - }, - write: |settings_content, value| { - settings_content - .tabs - .get_or_insert_default() - .show_diagnostics = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Close Button", - description: "Controls the appearance behavior of the tab's close button.", - field: Box::new(SettingField { - json_path: Some("tabs.show_close_button"), - pick: |settings_content| { - settings_content.tabs.as_ref()?.show_close_button.as_ref() - }, - write: |settings_content, value| { - settings_content - .tabs - .get_or_insert_default() - .show_close_button = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Preview Tabs"), - SettingsPageItem::SettingItem(SettingItem { - title: "Preview Tabs Enabled", - description: "Show opened editors as preview tabs.", - field: Box::new(SettingField { - json_path: Some("preview_tabs.enabled"), - pick: |settings_content| { - settings_content.preview_tabs.as_ref()?.enabled.as_ref() - }, - write: |settings_content, value| { - settings_content - .preview_tabs - .get_or_insert_default() - .enabled = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Enable Preview From Project Panel", - description: "Whether to open tabs in preview mode when opened from the project panel with a single click.", - field: Box::new(SettingField { - json_path: Some("preview_tabs.enable_preview_from_project_panel"), - pick: |settings_content| { - settings_content - .preview_tabs - .as_ref()? - .enable_preview_from_project_panel - .as_ref() - }, - write: |settings_content, value| { - settings_content - .preview_tabs - .get_or_insert_default() - .enable_preview_from_project_panel = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Enable Preview From File Finder", - description: "Whether to open tabs in preview mode when selected from the file finder.", - field: Box::new(SettingField { - json_path: Some("preview_tabs.enable_preview_from_file_finder"), - pick: |settings_content| { - settings_content - .preview_tabs - .as_ref()? - .enable_preview_from_file_finder - .as_ref() - }, - write: |settings_content, value| { - settings_content - .preview_tabs - .get_or_insert_default() - .enable_preview_from_file_finder = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Enable Preview From Multibuffer", - description: "Whether to open tabs in preview mode when opened from a multibuffer.", - field: Box::new(SettingField { - json_path: Some("preview_tabs.enable_preview_from_multibuffer"), - pick: |settings_content| { - settings_content - .preview_tabs - .as_ref()? - .enable_preview_from_multibuffer - .as_ref() - }, - write: |settings_content, value| { - settings_content - .preview_tabs - .get_or_insert_default() - .enable_preview_from_multibuffer = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Enable Preview Multibuffer From Code Navigation", - description: "Whether to open tabs in preview mode when code navigation is used to open a multibuffer.", - field: Box::new(SettingField { - json_path: Some("preview_tabs.enable_preview_multibuffer_from_code_navigation"), - pick: |settings_content| { - settings_content - .preview_tabs - .as_ref()? - .enable_preview_multibuffer_from_code_navigation - .as_ref() - }, - write: |settings_content, value| { - settings_content - .preview_tabs - .get_or_insert_default() - .enable_preview_multibuffer_from_code_navigation = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Enable Preview File From Code Navigation", - description: "Whether to open tabs in preview mode when code navigation is used to open a single file.", - field: Box::new(SettingField { - json_path: Some("preview_tabs.enable_preview_file_from_code_navigation"), - pick: |settings_content| { - settings_content - .preview_tabs - .as_ref()? - .enable_preview_file_from_code_navigation - .as_ref() - }, - write: |settings_content, value| { - settings_content - .preview_tabs - .get_or_insert_default() - .enable_preview_file_from_code_navigation = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Enable Keep Preview On Code Navigation", - description: "Whether to keep tabs in preview mode when code navigation is used to navigate away from them. If `enable_preview_file_from_code_navigation` or `enable_preview_multibuffer_from_code_navigation` is also true, the new tab may replace the existing one.", - field: Box::new(SettingField { - json_path: Some("preview_tabs.enable_keep_preview_on_code_navigation"), - pick: |settings_content| { - settings_content - .preview_tabs - .as_ref()? - .enable_keep_preview_on_code_navigation - .as_ref() - }, - write: |settings_content, value| { - settings_content - .preview_tabs - .get_or_insert_default() - .enable_keep_preview_on_code_navigation = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Layout"), - SettingsPageItem::SettingItem(SettingItem { - title: "Bottom Dock Layout", - description: "Layout mode for the bottom dock.", - field: Box::new(SettingField { - json_path: Some("bottom_dock_layout"), - pick: |settings_content| { - settings_content.workspace.bottom_dock_layout.as_ref() - }, - write: |settings_content, value| { - settings_content.workspace.bottom_dock_layout = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - files: USER, - title: "Centered Layout Left Padding", - description: "Left padding for centered layout.", - field: Box::new(SettingField { - json_path: Some("centered_layout.left_padding"), - pick: |settings_content| { - settings_content - .workspace - .centered_layout - .as_ref()? - .left_padding - .as_ref() - }, - write: |settings_content, value| { - settings_content - .workspace - .centered_layout - .get_or_insert_default() - .left_padding = value; - }, - }), - metadata: None, - }), - SettingsPageItem::SettingItem(SettingItem { - files: USER, - title: "Centered Layout Right Padding", - description: "Right padding for centered layout.", - field: Box::new(SettingField { - json_path: Some("centered_layout.right_padding"), - pick: |settings_content| { - settings_content - .workspace - .centered_layout - .as_ref()? - .right_padding - .as_ref() - }, - write: |settings_content, value| { - settings_content - .workspace - .centered_layout - .get_or_insert_default() - .right_padding = value; - }, - }), - metadata: None, - }), - SettingsPageItem::SectionHeader("Window"), - // todo(settings_ui): Should we filter by platform.as_ref()? - SettingsPageItem::SettingItem(SettingItem { - title: "Use System Window Tabs", - description: "(macOS only) whether to allow Windows to tab together.", - field: Box::new(SettingField { - json_path: Some("use_system_window_tabs"), - pick: |settings_content| { - settings_content.workspace.use_system_window_tabs.as_ref() - }, - write: |settings_content, value| { - settings_content.workspace.use_system_window_tabs = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Window Decorations", - description: "(Linux only) whether Zed or your compositor should draw window decorations.", - field: Box::new(SettingField { - json_path: Some("window_decorations"), - pick: |settings_content| { - settings_content.workspace.window_decorations.as_ref() - }, - write: |settings_content, value| { - settings_content.workspace.window_decorations = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Pane Modifiers"), - SettingsPageItem::SettingItem(SettingItem { - title: "Inactive Opacity", - description: "Opacity of inactive panels (0.0 - 1.0).", - field: Box::new(SettingField { - json_path: Some("active_pane_modifiers.inactive_opacity"), - pick: |settings_content| { - settings_content - .workspace - .active_pane_modifiers - .as_ref()? - .inactive_opacity - .as_ref() - }, - write: |settings_content, value| { - settings_content - .workspace - .active_pane_modifiers - .get_or_insert_default() - .inactive_opacity = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Border Size", - description: "Size of the border surrounding the active pane.", - field: Box::new(SettingField { - json_path: Some("active_pane_modifiers.border_size"), - pick: |settings_content| { - settings_content - .workspace - .active_pane_modifiers - .as_ref()? - .border_size - .as_ref() - }, - write: |settings_content, value| { - settings_content - .workspace - .active_pane_modifiers - .get_or_insert_default() - .border_size = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Zoomed Padding", - description: "Show padding for zoomed panes.", - field: Box::new(SettingField { - json_path: Some("zoomed_padding"), - pick: |settings_content| settings_content.workspace.zoomed_padding.as_ref(), - write: |settings_content, value| { - settings_content.workspace.zoomed_padding = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Pane Split Direction"), - SettingsPageItem::SettingItem(SettingItem { - title: "Vertical Split Direction", - description: "Direction to split vertically.", - field: Box::new(SettingField { - json_path: Some("pane_split_direction_vertical"), - pick: |settings_content| { - settings_content - .workspace - .pane_split_direction_vertical - .as_ref() - }, - write: |settings_content, value| { - settings_content.workspace.pane_split_direction_vertical = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Horizontal Split Direction", - description: "Direction to split horizontally.", - field: Box::new(SettingField { - json_path: Some("pane_split_direction_horizontal"), - pick: |settings_content| { - settings_content - .workspace - .pane_split_direction_horizontal - .as_ref() - }, - write: |settings_content, value| { - settings_content.workspace.pane_split_direction_horizontal = value; - }, - }), - metadata: None, - files: USER, - }), - ], - }, - SettingsPage { - title: "Panels", - items: vec![ - SettingsPageItem::SectionHeader("Project Panel"), - SettingsPageItem::SettingItem(SettingItem { - title: "Project Panel Dock", - description: "Where to dock the project panel.", - field: Box::new(SettingField { - json_path: Some("project_panel.dock"), - pick: |settings_content| { - settings_content.project_panel.as_ref()?.dock.as_ref() - }, - write: |settings_content, value| { - settings_content.project_panel.get_or_insert_default().dock = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Project Panel Default Width", - description: "Default width of the project panel in pixels.", - field: Box::new(SettingField { - json_path: Some("project_panel.default_width"), - pick: |settings_content| { - settings_content - .project_panel - .as_ref()? - .default_width - .as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .default_width = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Hide .gitignore", - description: "Whether to hide the gitignore entries in the project panel.", - field: Box::new(SettingField { - json_path: Some("project_panel.hide_gitignore"), - pick: |settings_content| { - settings_content - .project_panel - .as_ref()? - .hide_gitignore - .as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .hide_gitignore = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Entry Spacing", - description: "Spacing between worktree entries in the project panel.", - field: Box::new(SettingField { - json_path: Some("project_panel.entry_spacing"), - pick: |settings_content| { - settings_content - .project_panel - .as_ref()? - .entry_spacing - .as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .entry_spacing = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "File Icons", - description: "Show file icons in the project panel.", - field: Box::new(SettingField { - json_path: Some("project_panel.file_icons"), - pick: |settings_content| { - settings_content.project_panel.as_ref()?.file_icons.as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .file_icons = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Folder Icons", - description: "Whether to show folder icons or chevrons for directories in the project panel.", - field: Box::new(SettingField { - json_path: Some("project_panel.folder_icons"), - pick: |settings_content| { - settings_content - .project_panel - .as_ref()? - .folder_icons - .as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .folder_icons = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Git Status", - description: "Show the Git status in the project panel.", - field: Box::new(SettingField { - json_path: Some("project_panel.git_status"), - pick: |settings_content| { - settings_content.project_panel.as_ref()?.git_status.as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .git_status = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Indent Size", - description: "Amount of indentation for nested items.", - field: Box::new(SettingField { - json_path: Some("project_panel.indent_size"), - pick: |settings_content| { - settings_content - .project_panel - .as_ref()? - .indent_size - .as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .indent_size = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Auto Reveal Entries", - description: "Whether to reveal entries in the project panel automatically when a corresponding project entry becomes active.", - field: Box::new(SettingField { - json_path: Some("project_panel.auto_reveal_entries"), - pick: |settings_content| { - settings_content - .project_panel - .as_ref()? - .auto_reveal_entries - .as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .auto_reveal_entries = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Starts Open", - description: "Whether the project panel should open on startup.", - field: Box::new(SettingField { - json_path: Some("project_panel.starts_open"), - pick: |settings_content| { - settings_content - .project_panel - .as_ref()? - .starts_open - .as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .starts_open = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Auto Fold Directories", - description: "Whether to fold directories automatically and show compact folders when a directory has only one subdirectory inside.", - field: Box::new(SettingField { - json_path: Some("project_panel.auto_fold_dirs"), - pick: |settings_content| { - settings_content - .project_panel - .as_ref()? - .auto_fold_dirs - .as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .auto_fold_dirs = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Scrollbar", - description: "Show the scrollbar in the project panel.", - field: Box::new(SettingField { - json_path: Some("project_panel.scrollbar.show"), - pick: |settings_content| { - show_scrollbar_or_editor(settings_content, |settings_content| { - settings_content - .project_panel - .as_ref()? - .scrollbar - .as_ref()? - .show - .as_ref() - }) - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .scrollbar - .get_or_insert_default() - .show = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Diagnostics", - description: "Which files containing diagnostic errors/warnings to mark in the project panel.", - field: Box::new(SettingField { - json_path: Some("project_panel.show_diagnostics"), - pick: |settings_content| { - settings_content - .project_panel - .as_ref()? - .show_diagnostics - .as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .show_diagnostics = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Sticky Scroll", - description: "Whether to stick parent directories at top of the project panel.", - field: Box::new(SettingField { - json_path: Some("project_panel.sticky_scroll"), - pick: |settings_content| { - settings_content - .project_panel - .as_ref()? - .sticky_scroll - .as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .sticky_scroll = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - files: USER, - title: "Show Indent Guides", - description: "Show indent guides in the project panel.", - field: Box::new( - SettingField { - json_path: Some("project_panel.indent_guides.show"), - pick: |settings_content| { - settings_content - .project_panel - .as_ref()? - .indent_guides - .as_ref()? - .show - .as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .indent_guides - .get_or_insert_default() - .show = value; - }, - } - ), - metadata: None, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Drag and Drop", - description: "Whether to enable drag-and-drop operations in the project panel.", - field: Box::new(SettingField { - json_path: Some("project_panel.drag_and_drop"), - pick: |settings_content| { - settings_content - .project_panel - .as_ref()? - .drag_and_drop - .as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .drag_and_drop = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Hide Root", - description: "Whether to hide the root entry when only one folder is open in the window.", - field: Box::new(SettingField { - json_path: Some("project_panel.drag_and_drop"), - pick: |settings_content| { - settings_content.project_panel.as_ref()?.hide_root.as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .hide_root = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Hide Hidden", - description: "Whether to hide the hidden entries in the project panel.", - field: Box::new(SettingField { - json_path: Some("project_panel.hide_hidden"), - pick: |settings_content| { - settings_content - .project_panel - .as_ref()? - .hide_hidden - .as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .hide_hidden = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Hidden Files", - description: "Globs to match files that will be considered \"hidden\" and can be hidden from the project panel.", - field: Box::new( - SettingField { - json_path: Some("worktree.hidden_files"), - pick: |settings_content| { - settings_content.project.worktree.hidden_files.as_ref() - }, - write: |settings_content, value| { - settings_content.project.worktree.hidden_files = value; - }, - } - .unimplemented(), - ), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Auto Open Files"), - SettingsPageItem::SettingItem(SettingItem { - title: "On Create", - description: "Whether to automatically open newly created files in the editor.", - field: Box::new(SettingField { - json_path: Some("project_panel.auto_open.on_create"), - pick: |settings_content| { - settings_content.project_panel.as_ref()?.auto_open.as_ref()?.on_create.as_ref() - }, - write: |settings_content, value| { - settings_content.project_panel.get_or_insert_default().auto_open.get_or_insert_default().on_create = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "On Paste", - description: "Whether to automatically open files after pasting or duplicating them.", - field: Box::new(SettingField { - json_path: Some("project_panel.auto_open.on_paste"), - pick: |settings_content| { - settings_content.project_panel.as_ref()?.auto_open.as_ref()?.on_paste.as_ref() - }, - write: |settings_content, value| { - settings_content.project_panel.get_or_insert_default().auto_open.get_or_insert_default().on_paste = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "On Drop", - description: "Whether to automatically open files dropped from external sources.", - field: Box::new(SettingField { - json_path: Some("project_panel.auto_open.on_drop"), - pick: |settings_content| { - settings_content.project_panel.as_ref()?.auto_open.as_ref()?.on_drop.as_ref() - }, - write: |settings_content, value| { - settings_content.project_panel.get_or_insert_default().auto_open.get_or_insert_default().on_drop = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Sort Mode", - description: "Sort order for entries in the project panel.", - field: Box::new(SettingField { - pick: |settings_content| { - settings_content.project_panel.as_ref()?.sort_mode.as_ref() - }, - write: |settings_content, value| { - settings_content - .project_panel - .get_or_insert_default() - .sort_mode = value; - }, - json_path: Some("project_panel.sort_mode"), - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Terminal Panel"), - SettingsPageItem::SettingItem(SettingItem { - title: "Terminal Dock", - description: "Where to dock the terminal panel.", - field: Box::new(SettingField { - json_path: Some("terminal.dock"), - pick: |settings_content| settings_content.terminal.as_ref()?.dock.as_ref(), - write: |settings_content, value| { - settings_content.terminal.get_or_insert_default().dock = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Outline Panel"), - SettingsPageItem::SettingItem(SettingItem { - title: "Outline Panel Button", - description: "Show the outline panel button in the status bar.", - field: Box::new(SettingField { - json_path: Some("outline_panel.button"), - pick: |settings_content| { - settings_content.outline_panel.as_ref()?.button.as_ref() - }, - write: |settings_content, value| { - settings_content - .outline_panel - .get_or_insert_default() - .button = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Outline Panel Dock", - description: "Where to dock the outline panel.", - field: Box::new(SettingField { - json_path: Some("outline_panel.dock"), - pick: |settings_content| { - settings_content.outline_panel.as_ref()?.dock.as_ref() - }, - write: |settings_content, value| { - settings_content.outline_panel.get_or_insert_default().dock = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Outline Panel Default Width", - description: "Default width of the outline panel in pixels.", - field: Box::new(SettingField { - json_path: Some("outline_panel.default_width"), - pick: |settings_content| { - settings_content - .outline_panel - .as_ref()? - .default_width - .as_ref() - }, - write: |settings_content, value| { - settings_content - .outline_panel - .get_or_insert_default() - .default_width = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "File Icons", - description: "Show file icons in the outline panel.", - field: Box::new(SettingField { - json_path: Some("outline_panel.file_icons"), - pick: |settings_content| { - settings_content.outline_panel.as_ref()?.file_icons.as_ref() - }, - write: |settings_content, value| { - settings_content - .outline_panel - .get_or_insert_default() - .file_icons = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Folder Icons", - description: "Whether to show folder icons or chevrons for directories in the outline panel.", - field: Box::new(SettingField { - json_path: Some("outline_panel.folder_icons"), - pick: |settings_content| { - settings_content - .outline_panel - .as_ref()? - .folder_icons - .as_ref() - }, - write: |settings_content, value| { - settings_content - .outline_panel - .get_or_insert_default() - .folder_icons = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Git Status", - description: "Show the Git status in the outline panel.", - field: Box::new(SettingField { - json_path: Some("outline_panel.git_status"), - pick: |settings_content| { - settings_content.outline_panel.as_ref()?.git_status.as_ref() - }, - write: |settings_content, value| { - settings_content - .outline_panel - .get_or_insert_default() - .git_status = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Indent Size", - description: "Amount of indentation for nested items.", - field: Box::new(SettingField { - json_path: Some("outline_panel.indent_size"), - pick: |settings_content| { - settings_content - .outline_panel - .as_ref()? - .indent_size - .as_ref() - }, - write: |settings_content, value| { - settings_content - .outline_panel - .get_or_insert_default() - .indent_size = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Auto Reveal Entries", - description: "Whether to reveal when a corresponding outline entry becomes active.", - field: Box::new(SettingField { - json_path: Some("outline_panel.auto_reveal_entries"), - pick: |settings_content| { - settings_content - .outline_panel - .as_ref()? - .auto_reveal_entries - .as_ref() - }, - write: |settings_content, value| { - settings_content - .outline_panel - .get_or_insert_default() - .auto_reveal_entries = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Auto Fold Directories", - description: "Whether to fold directories automatically when a directory contains only one subdirectory.", - field: Box::new(SettingField { - json_path: Some("outline_panel.auto_fold_dirs"), - pick: |settings_content| { - settings_content - .outline_panel - .as_ref()? - .auto_fold_dirs - .as_ref() - }, - write: |settings_content, value| { - settings_content - .outline_panel - .get_or_insert_default() - .auto_fold_dirs = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - files: USER, - title: "Show Indent Guides", - description: "When to show indent guides in the outline panel.", - field: Box::new( - SettingField { - json_path: Some("outline_panel.indent_guides.show"), - pick: |settings_content| { - settings_content - .outline_panel - .as_ref()? - .indent_guides - .as_ref()? - .show - .as_ref() - }, - write: |settings_content, value| { - settings_content - .outline_panel - .get_or_insert_default() - .indent_guides - .get_or_insert_default() - .show = value; - }, - } - ), - metadata: None, - }), - SettingsPageItem::SectionHeader("Git Panel"), - SettingsPageItem::SettingItem(SettingItem { - title: "Git Panel Button", - description: "Show the Git panel button in the status bar.", - field: Box::new(SettingField { - json_path: Some("git_panel.button"), - pick: |settings_content| { - settings_content.git_panel.as_ref()?.button.as_ref() - }, - write: |settings_content, value| { - settings_content.git_panel.get_or_insert_default().button = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Git Panel Dock", - description: "Where to dock the Git panel.", - field: Box::new(SettingField { - json_path: Some("git_panel.dock"), - pick: |settings_content| settings_content.git_panel.as_ref()?.dock.as_ref(), - write: |settings_content, value| { - settings_content.git_panel.get_or_insert_default().dock = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Git Panel Default Width", - description: "Default width of the Git panel in pixels.", - field: Box::new(SettingField { - json_path: Some("git_panel.default_width"), - pick: |settings_content| { - settings_content.git_panel.as_ref()?.default_width.as_ref() - }, - write: |settings_content, value| { - settings_content - .git_panel - .get_or_insert_default() - .default_width = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Git Panel Status Style", - description: "How entry statuses are displayed.", - field: Box::new(SettingField { - json_path: Some("git_panel.status_style"), - pick: |settings_content| { - settings_content.git_panel.as_ref()?.status_style.as_ref() - }, - write: |settings_content, value| { - settings_content - .git_panel - .get_or_insert_default() - .status_style = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Fallback Branch Name", - description: "Default branch name will be when init.defaultbranch is not set in Git.", - field: Box::new(SettingField { - json_path: Some("git_panel.fallback_branch_name"), - pick: |settings_content| { - settings_content - .git_panel - .as_ref()? - .fallback_branch_name - .as_ref() - }, - write: |settings_content, value| { - settings_content - .git_panel - .get_or_insert_default() - .fallback_branch_name = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Sort By Path", - description: "Enable to sort entries in the panel by path, disable to sort by status.", - field: Box::new(SettingField { - json_path: Some("git_panel.sort_by_path"), - pick: |settings_content| { - settings_content.git_panel.as_ref()?.sort_by_path.as_ref() - }, - write: |settings_content, value| { - settings_content - .git_panel - .get_or_insert_default() - .sort_by_path = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Collapse Untracked Diff", - description: "Whether to collapse untracked files in the diff panel.", - field: Box::new(SettingField { - json_path: Some("git_panel.collapse_untracked_diff"), - pick: |settings_content| { - settings_content - .git_panel - .as_ref()? - .collapse_untracked_diff - .as_ref() - }, - write: |settings_content, value| { - settings_content - .git_panel - .get_or_insert_default() - .collapse_untracked_diff = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Tree View", - description: "Enable to show entries in tree view list, disable to show in flat view list.", - field: Box::new(SettingField { - json_path: Some("git_panel.tree_view"), - pick: |settings_content| { - settings_content.git_panel.as_ref()?.tree_view.as_ref() - }, - write: |settings_content, value| { - settings_content - .git_panel - .get_or_insert_default() - .tree_view = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Scroll Bar", - description: "How and when the scrollbar should be displayed.", - field: Box::new(SettingField { - json_path: Some("git_panel.scrollbar.show"), - pick: |settings_content| { - show_scrollbar_or_editor(settings_content, |settings_content| { - settings_content - .git_panel - .as_ref()? - .scrollbar - .as_ref()? - .show - .as_ref() - }) - }, - write: |settings_content, value| { - settings_content - .git_panel - .get_or_insert_default() - .scrollbar - .get_or_insert_default() - .show = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Debugger Panel"), - SettingsPageItem::SettingItem(SettingItem { - title: "Debugger Panel Dock", - description: "The dock position of the debug panel.", - field: Box::new(SettingField { - json_path: Some("debugger.dock"), - pick: |settings_content| settings_content.debugger.as_ref()?.dock.as_ref(), - write: |settings_content, value| { - settings_content.debugger.get_or_insert_default().dock = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Notification Panel"), - SettingsPageItem::SettingItem(SettingItem { - title: "Notification Panel Button", - description: "Show the notification panel button in the status bar.", - field: Box::new(SettingField { - json_path: Some("notification_panel.button"), - pick: |settings_content| { - settings_content - .notification_panel - .as_ref()? - .button - .as_ref() - }, - write: |settings_content, value| { - settings_content - .notification_panel - .get_or_insert_default() - .button = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Notification Panel Dock", - description: "Where to dock the notification panel.", - field: Box::new(SettingField { - json_path: Some("notification_panel.dock"), - pick: |settings_content| { - settings_content.notification_panel.as_ref()?.dock.as_ref() - }, - write: |settings_content, value| { - settings_content - .notification_panel - .get_or_insert_default() - .dock = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Notification Panel Default Width", - description: "Default width of the notification panel in pixels.", - field: Box::new(SettingField { - json_path: Some("notification_panel.default_width"), - pick: |settings_content| { - settings_content - .notification_panel - .as_ref()? - .default_width - .as_ref() - }, - write: |settings_content, value| { - settings_content - .notification_panel - .get_or_insert_default() - .default_width = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Collaboration Panel"), - SettingsPageItem::SettingItem(SettingItem { - title: "Collaboration Panel Button", - description: "Show the collaboration panel button in the status bar.", - field: Box::new(SettingField { - json_path: Some("collaboration_panel.button"), - pick: |settings_content| { - settings_content - .collaboration_panel - .as_ref()? - .button - .as_ref() - }, - write: |settings_content, value| { - settings_content - .collaboration_panel - .get_or_insert_default() - .button = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Collaboration Panel Dock", - description: "Where to dock the collaboration panel.", - field: Box::new(SettingField { - json_path: Some("collaboration_panel.dock"), - pick: |settings_content| { - settings_content.collaboration_panel.as_ref()?.dock.as_ref() - }, - write: |settings_content, value| { - settings_content - .collaboration_panel - .get_or_insert_default() - .dock = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Collaboration Panel Default Width", - description: "Default width of the collaboration panel in pixels.", - field: Box::new(SettingField { - json_path: Some("collaboration_panel.dock"), - pick: |settings_content| { - settings_content - .collaboration_panel - .as_ref()? - .default_width - .as_ref() - }, - write: |settings_content, value| { - settings_content - .collaboration_panel - .get_or_insert_default() - .default_width = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Agent Panel"), - SettingsPageItem::SettingItem(SettingItem { - title: "Agent Panel Button", - description: "Whether to show the agent panel button in the status bar.", - field: Box::new(SettingField { - json_path: Some("agent.button"), - pick: |settings_content| settings_content.agent.as_ref()?.button.as_ref(), - write: |settings_content, value| { - settings_content.agent.get_or_insert_default().button = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Agent Panel Dock", - description: "Where to dock the agent panel.", - field: Box::new(SettingField { - json_path: Some("agent.dock"), - pick: |settings_content| settings_content.agent.as_ref()?.dock.as_ref(), - write: |settings_content, value| { - settings_content.agent.get_or_insert_default().dock = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Agent Panel Default Width", - description: "Default width when the agent panel is docked to the left or right.", - field: Box::new(SettingField { - json_path: Some("agent.default_width"), - pick: |settings_content| { - settings_content.agent.as_ref()?.default_width.as_ref() - }, - write: |settings_content, value| { - settings_content.agent.get_or_insert_default().default_width = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Agent Panel Default Height", - description: "Default height when the agent panel is docked to the bottom.", - field: Box::new(SettingField { - json_path: Some("agent.default_height"), - pick: |settings_content| { - settings_content.agent.as_ref()?.default_height.as_ref() - }, - write: |settings_content, value| { - settings_content - .agent - .get_or_insert_default() - .default_height = value; - }, - }), - metadata: None, - files: USER, - }), - ], - }, - SettingsPage { - title: "Debugger", - items: vec![ - SettingsPageItem::SectionHeader("General"), - SettingsPageItem::SettingItem(SettingItem { - title: "Stepping Granularity", - description: "Determines the stepping granularity for debug operations.", - field: Box::new(SettingField { - json_path: Some("debugger.stepping_granularity"), - pick: |settings_content| { - settings_content - .debugger - .as_ref()? - .stepping_granularity - .as_ref() - }, - write: |settings_content, value| { - settings_content - .debugger - .get_or_insert_default() - .stepping_granularity = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Save Breakpoints", - description: "Whether breakpoints should be reused across Zed sessions.", - field: Box::new(SettingField { - json_path: Some("debugger.save_breakpoints"), - pick: |settings_content| { - settings_content - .debugger - .as_ref()? - .save_breakpoints - .as_ref() - }, - write: |settings_content, value| { - settings_content - .debugger - .get_or_insert_default() - .save_breakpoints = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Timeout", - description: "Time in milliseconds until timeout error when connecting to a TCP debug adapter.", - field: Box::new(SettingField { - json_path: Some("debugger.timeout"), - pick: |settings_content| { - settings_content.debugger.as_ref()?.timeout.as_ref() - }, - write: |settings_content, value| { - settings_content.debugger.get_or_insert_default().timeout = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Log DAP Communications", - description: "Whether to log messages between active debug adapters and Zed.", - field: Box::new(SettingField { - json_path: Some("debugger.log_dap_communications"), - pick: |settings_content| { - settings_content - .debugger - .as_ref()? - .log_dap_communications - .as_ref() - }, - write: |settings_content, value| { - settings_content - .debugger - .get_or_insert_default() - .log_dap_communications = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Format DAP Log Messages", - description: "Whether to format DAP messages when adding them to debug adapter logger.", - field: Box::new(SettingField { - json_path: Some("debugger.format_dap_log_messages"), - pick: |settings_content| { - settings_content - .debugger - .as_ref()? - .format_dap_log_messages - .as_ref() - }, - write: |settings_content, value| { - settings_content - .debugger - .get_or_insert_default() - .format_dap_log_messages = value; - }, - }), - metadata: None, - files: USER, - }), - ], - }, - SettingsPage { - title: "Terminal", - items: vec![ - SettingsPageItem::SectionHeader("Environment"), - SettingsPageItem::DynamicItem(DynamicItem { - discriminant: SettingItem { - files: USER | PROJECT, - title: "Shell", - description: "What shell to use when opening a terminal.", - field: Box::new(SettingField { - json_path: Some("terminal.shell$"), - pick: |settings_content| { - Some(&dynamic_variants::()[ - settings_content - .terminal - .as_ref()? - .project - .shell - .as_ref()? - .discriminant() as usize]) - }, - write: |settings_content, value| { - let Some(value) = value else { - if let Some(terminal) = settings_content.terminal.as_mut() { - terminal.project.shell = None; - } - return; - }; - let settings_value = settings_content - .terminal - .get_or_insert_default() - .project - .shell - .get_or_insert_with(|| settings::Shell::default()); - let default_shell = if cfg!(target_os = "windows") { - "powershell.exe" - } else { - "sh" - }; - *settings_value = match value { - settings::ShellDiscriminants::System => { - settings::Shell::System - }, - settings::ShellDiscriminants::Program => { - let program = match settings_value { - settings::Shell::Program(p) => p.clone(), - settings::Shell::WithArguments { program, .. } => program.clone(), - _ => String::from(default_shell), - }; - settings::Shell::Program(program) - }, - settings::ShellDiscriminants::WithArguments => { - let (program, args, title_override) = match settings_value { - settings::Shell::Program(p) => (p.clone(), vec![], None), - settings::Shell::WithArguments { program, args, title_override } => { - (program.clone(), args.clone(), title_override.clone()) - }, - _ => (String::from(default_shell), vec![], None), - }; - settings::Shell::WithArguments { - program, - args, - title_override, - } - }, - }; - }, - }), - metadata: None, - }, - pick_discriminant: |settings_content| { - Some(settings_content.terminal.as_ref()?.project.shell.as_ref()?.discriminant() as usize) - }, - fields: dynamic_variants::().into_iter().map(|variant| { - match variant { - settings::ShellDiscriminants::System => vec![], - settings::ShellDiscriminants::Program => vec![ - SettingItem { - files: USER | PROJECT, - title: "Program", - description: "The shell program to use.", - field: Box::new(SettingField { - json_path: Some("terminal.shell"), - pick: |settings_content| { - match settings_content.terminal.as_ref()?.project.shell.as_ref() { - Some(settings::Shell::Program(program)) => Some(program), - _ => None - } - }, - write: |settings_content, value| { - let Some(value) = value else { - return; - }; - match settings_content - .terminal - .get_or_insert_default() - .project - .shell.as_mut() { - Some(settings::Shell::Program(program)) => *program = value, - _ => return - } - }, - }), - metadata: None, - } - ], - settings::ShellDiscriminants::WithArguments => vec![ - SettingItem { - files: USER | PROJECT, - title: "Program", - description: "The shell program to run.", - field: Box::new(SettingField { - json_path: Some("terminal.shell.program"), - pick: |settings_content| { - match settings_content.terminal.as_ref()?.project.shell.as_ref() { - Some(settings::Shell::WithArguments { program, .. }) => Some(program), - _ => None - } - }, - write: |settings_content, value| { - let Some(value) = value else { - return; - }; - match settings_content - .terminal - .get_or_insert_default() - .project - .shell.as_mut() { - Some(settings::Shell::WithArguments { program, .. }) => *program = value, - _ => return - } - }, - }), - metadata: None, - }, - SettingItem { - files: USER | PROJECT, - title: "Arguments", - description: "The arguments to pass to the shell program.", - field: Box::new( - SettingField { - json_path: Some("terminal.shell.args"), - pick: |settings_content| { - match settings_content.terminal.as_ref()?.project.shell.as_ref() { - Some(settings::Shell::WithArguments { args, .. }) => Some(args), - _ => None - } - }, - write: |settings_content, value| { - let Some(value) = value else { - return; - }; - match settings_content - .terminal - .get_or_insert_default() - .project - .shell.as_mut() { - Some(settings::Shell::WithArguments { args, .. }) => *args = value, - _ => return - } - }, - } - .unimplemented(), - ), - metadata: None, - }, - SettingItem { - files: USER | PROJECT, - title: "Title Override", - description: "An optional string to override the title of the terminal tab.", - field: Box::new(SettingField { - json_path: Some("terminal.shell.title_override"), - pick: |settings_content| { - match settings_content.terminal.as_ref()?.project.shell.as_ref() { - Some(settings::Shell::WithArguments { title_override, .. }) => title_override.as_ref().or(DEFAULT_EMPTY_SHARED_STRING), - _ => None - } - }, - write: |settings_content, value| { - match settings_content - .terminal - .get_or_insert_default() - .project - .shell.as_mut() { - Some(settings::Shell::WithArguments { title_override, .. }) => *title_override = value.filter(|s| !s.is_empty()), - _ => return - } - }, - }), - metadata: None, - } - ], - } - }).collect(), - }), - SettingsPageItem::DynamicItem(DynamicItem { - discriminant: SettingItem { - files: USER | PROJECT, - title: "Working Directory", - description: "What working directory to use when launching the terminal.", - field: Box::new(SettingField { - json_path: Some("terminal.working_directory$"), - pick: |settings_content| { - Some(&dynamic_variants::()[ - settings_content - .terminal - .as_ref()? - .project - .working_directory - .as_ref()? - .discriminant() as usize]) - }, - write: |settings_content, value| { - let Some(value) = value else { - if let Some(terminal) = settings_content.terminal.as_mut() { - terminal.project.working_directory = None; - } - return; - }; - let settings_value = settings_content - .terminal - .get_or_insert_default() - .project - .working_directory - .get_or_insert_with(|| settings::WorkingDirectory::CurrentProjectDirectory); - *settings_value = match value { - settings::WorkingDirectoryDiscriminants::CurrentProjectDirectory => { - settings::WorkingDirectory::CurrentProjectDirectory - }, - settings::WorkingDirectoryDiscriminants::FirstProjectDirectory => { - settings::WorkingDirectory::FirstProjectDirectory - }, - settings::WorkingDirectoryDiscriminants::AlwaysHome => { - settings::WorkingDirectory::AlwaysHome - }, - settings::WorkingDirectoryDiscriminants::Always => { - let directory = match settings_value { - settings::WorkingDirectory::Always { .. } => return, - _ => String::new(), - }; - settings::WorkingDirectory::Always { directory } - }, - }; - }, - }), - metadata: None, - }, - pick_discriminant: |settings_content| { - Some(settings_content.terminal.as_ref()?.project.working_directory.as_ref()?.discriminant() as usize) - }, - fields: dynamic_variants::().into_iter().map(|variant| { - match variant { - settings::WorkingDirectoryDiscriminants::CurrentProjectDirectory => vec![], - settings::WorkingDirectoryDiscriminants::FirstProjectDirectory => vec![], - settings::WorkingDirectoryDiscriminants::AlwaysHome => vec![], - settings::WorkingDirectoryDiscriminants::Always => vec![ - SettingItem { - files: USER | PROJECT, - title: "Directory", - description: "The directory path to use (will be shell expanded).", - field: Box::new(SettingField { - json_path: Some("terminal.working_directory.always"), - pick: |settings_content| { - match settings_content.terminal.as_ref()?.project.working_directory.as_ref() { - Some(settings::WorkingDirectory::Always { directory }) => Some(directory), - _ => None - } - }, - write: |settings_content, value| { - let value = value.unwrap_or_default(); - match settings_content - .terminal - .get_or_insert_default() - .project - .working_directory.as_mut() { - Some(settings::WorkingDirectory::Always { directory }) => *directory = value, - _ => return - } - }, - }), - metadata: None, - } - ], - } - }).collect(), - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Environment Variables", - description: "Key-value pairs to add to the terminal's environment.", - field: Box::new( - SettingField { - json_path: Some("terminal.env"), - pick: |settings_content| { - settings_content.terminal.as_ref()?.project.env.as_ref() - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .project - .env = value; - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Detect Virtual Environment", - description: "Activates the Python virtual environment, if one is found, in the terminal's working directory.", - field: Box::new( - SettingField { - json_path: Some("terminal.detect_venv"), - pick: |settings_content| { - settings_content - .terminal - .as_ref()? - .project - .detect_venv - .as_ref() - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .project - .detect_venv = value; - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SectionHeader("Font"), - SettingsPageItem::SettingItem(SettingItem { - title: "Font Size", - description: "Font size for terminal text. If not set, defaults to buffer font size.", - field: Box::new(SettingField { - json_path: Some("terminal.font_size"), - pick: |settings_content| { - settings_content - .terminal - .as_ref() - .and_then(|terminal| terminal.font_size.as_ref()) - .or(settings_content.theme.buffer_font_size.as_ref()) - }, - write: |settings_content, value| { - settings_content.terminal.get_or_insert_default().font_size = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Font Family", - description: "Font family for terminal text. If not set, defaults to buffer font family.", - field: Box::new(SettingField { - json_path: Some("terminal.font_family"), - pick: |settings_content| { - settings_content - .terminal - .as_ref() - .and_then(|terminal| terminal.font_family.as_ref()) - .or(settings_content.theme.buffer_font_family.as_ref()) - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .font_family = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Font Fallbacks", - description: "Font fallbacks for terminal text. If not set, defaults to buffer font fallbacks.", - field: Box::new( - SettingField { - json_path: Some("terminal.font_fallbacks"), - pick: |settings_content| { - settings_content - .terminal - .as_ref() - .and_then(|terminal| terminal.font_fallbacks.as_ref()) - .or(settings_content.theme.buffer_font_fallbacks.as_ref()) - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .font_fallbacks = value; - }, - } - .unimplemented(), - ), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Font Weight", - description: "Font weight for terminal text in CSS weight units (100-900).", - field: Box::new(SettingField { - json_path: Some("terminal.font_weight"), - pick: |settings_content| { - settings_content.terminal.as_ref()?.font_weight.as_ref() - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .font_weight = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Font Features", - description: "Font features for terminal text.", - field: Box::new( - SettingField { - json_path: Some("terminal.font_features"), - pick: |settings_content| { - settings_content - .terminal - .as_ref() - .and_then(|terminal| terminal.font_features.as_ref()) - .or(settings_content.theme.buffer_font_features.as_ref()) - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .font_features = value; - }, - } - .unimplemented(), - ), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Display Settings"), - SettingsPageItem::SettingItem(SettingItem { - title: "Line Height", - description: "Line height for terminal text.", - field: Box::new( - SettingField { - json_path: Some("terminal.line_height"), - pick: |settings_content| { - settings_content.terminal.as_ref()?.line_height.as_ref() - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .line_height = value; - }, - } - .unimplemented(), - ), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Cursor Shape", - description: "Default cursor shape for the terminal (bar, block, underline, or hollow).", - field: Box::new(SettingField { - json_path: Some("terminal.cursor_shape"), - pick: |settings_content| { - settings_content.terminal.as_ref()?.cursor_shape.as_ref() - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .cursor_shape = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Cursor Blinking", - description: "Sets the cursor blinking behavior in the terminal.", - field: Box::new(SettingField { - json_path: Some("terminal.blinking"), - pick: |settings_content| { - settings_content.terminal.as_ref()?.blinking.as_ref() - }, - write: |settings_content, value| { - settings_content.terminal.get_or_insert_default().blinking = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Alternate Scroll", - description: "Whether alternate scroll mode is active by default (converts mouse scroll to arrow keys in apps like Vim).", - field: Box::new(SettingField { - json_path: Some("terminal.alternate_scroll"), - pick: |settings_content| { - settings_content - .terminal - .as_ref()? - .alternate_scroll - .as_ref() - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .alternate_scroll = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Minimum Contrast", - description: "The minimum APCA perceptual contrast between foreground and background colors (0-106).", - field: Box::new(SettingField { - json_path: Some("terminal.minimum_contrast"), - pick: |settings_content| { - settings_content - .terminal - .as_ref()? - .minimum_contrast - .as_ref() - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .minimum_contrast = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Behavior Settings"), - SettingsPageItem::SettingItem(SettingItem { - title: "Option As Meta", - description: "Whether the option key behaves as the meta key.", - field: Box::new(SettingField { - json_path: Some("terminal.option_as_meta"), - pick: |settings_content| { - settings_content.terminal.as_ref()?.option_as_meta.as_ref() - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .option_as_meta = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Copy On Select", - description: "Whether selecting text in the terminal automatically copies to the system clipboard.", - field: Box::new(SettingField { - json_path: Some("terminal.copy_on_select"), - pick: |settings_content| { - settings_content.terminal.as_ref()?.copy_on_select.as_ref() - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .copy_on_select = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Keep Selection On Copy", - description: "Whether to keep the text selection after copying it to the clipboard.", - field: Box::new(SettingField { - json_path: Some("terminal.keep_selection_on_copy"), - pick: |settings_content| { - settings_content - .terminal - .as_ref()? - .keep_selection_on_copy - .as_ref() - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .keep_selection_on_copy = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Layout Settings"), - SettingsPageItem::SettingItem(SettingItem { - title: "Default Width", - description: "Default width when the terminal is docked to the left or right (in pixels).", - field: Box::new(SettingField { - json_path: Some("terminal.default_width"), - pick: |settings_content| { - settings_content.terminal.as_ref()?.default_width.as_ref() - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .default_width = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Default Height", - description: "Default height when the terminal is docked to the bottom (in pixels).", - field: Box::new(SettingField { - json_path: Some("terminal.default_height"), - pick: |settings_content| { - settings_content.terminal.as_ref()?.default_height.as_ref() - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .default_height = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Advanced Settings"), - SettingsPageItem::SettingItem(SettingItem { - title: "Max Scroll History Lines", - description: "Maximum number of lines to keep in scrollback history (max: 100,000; 0 disables scrolling).", - field: Box::new(SettingField { - json_path: Some("terminal.max_scroll_history_lines"), - pick: |settings_content| { - settings_content - .terminal - .as_ref()? - .max_scroll_history_lines - .as_ref() - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .max_scroll_history_lines = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Scroll Multiplier", - description: "The multiplier for scrolling in the terminal with the mouse wheel", - field: Box::new(SettingField { - json_path: Some("terminal.scroll_multiplier"), - pick: |settings_content| { - settings_content.terminal.as_ref()?.scroll_multiplier.as_ref() - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .scroll_multiplier = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Toolbar"), - SettingsPageItem::SettingItem(SettingItem { - title: "Breadcrumbs", - description: "Display the terminal title in breadcrumbs inside the terminal pane.", - field: Box::new(SettingField { - json_path: Some("terminal.toolbar.breadcrumbs"), - pick: |settings_content| { - settings_content - .terminal - .as_ref()? - .toolbar - .as_ref()? - .breadcrumbs - .as_ref() - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .toolbar - .get_or_insert_default() - .breadcrumbs = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Scrollbar"), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Scrollbar", - description: "When to show the scrollbar in the terminal.", - field: Box::new(SettingField { - json_path: Some("terminal.scrollbar.show"), - pick: |settings_content| { - show_scrollbar_or_editor(settings_content, |settings_content| { - settings_content - .terminal - .as_ref()? - .scrollbar - .as_ref()? - .show - .as_ref() - }) - }, - write: |settings_content, value| { - settings_content - .terminal - .get_or_insert_default() - .scrollbar - .get_or_insert_default() - .show = value; - }, - }), - metadata: None, - files: USER, - }), - ], - }, - SettingsPage { - title: "Version Control", - items: vec![ - SettingsPageItem::SectionHeader("Git Gutter"), - SettingsPageItem::SettingItem(SettingItem { - title: "Visibility", - description: "Control whether Git status is shown in the editor's gutter.", - field: Box::new(SettingField { - json_path: Some("git.git_gutter"), - pick: |settings_content| settings_content.git.as_ref()?.git_gutter.as_ref(), - write: |settings_content, value| { - settings_content.git.get_or_insert_default().git_gutter = value; - }, - }), - metadata: None, - files: USER, - }), - // todo(settings_ui): Figure out the right default for this value in default.json - SettingsPageItem::SettingItem(SettingItem { - title: "Debounce", - description: "Debounce threshold in milliseconds after which changes are reflected in the Git gutter.", - field: Box::new(SettingField { - json_path: Some("git.gutter_debounce"), - pick: |settings_content| { - settings_content.git.as_ref()?.gutter_debounce.as_ref() - }, - write: |settings_content, value| { - settings_content.git.get_or_insert_default().gutter_debounce = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Inline Git Blame"), - SettingsPageItem::SettingItem(SettingItem { - title: "Enabled", - description: "Whether or not to show Git blame data inline in the currently focused line.", - field: Box::new(SettingField { - json_path: Some("git.inline_blame.enabled"), - pick: |settings_content| { - settings_content - .git - .as_ref()? - .inline_blame - .as_ref()? - .enabled - .as_ref() - }, - write: |settings_content, value| { - settings_content - .git - .get_or_insert_default() - .inline_blame - .get_or_insert_default() - .enabled = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Delay", - description: "The delay after which the inline blame information is shown.", - field: Box::new(SettingField { - json_path: Some("git.inline_blame.delay_ms"), - pick: |settings_content| { - settings_content - .git - .as_ref()? - .inline_blame - .as_ref()? - .delay_ms - .as_ref() - }, - write: |settings_content, value| { - settings_content - .git - .get_or_insert_default() - .inline_blame - .get_or_insert_default() - .delay_ms = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Padding", - description: "Padding between the end of the source line and the start of the inline blame in columns.", - field: Box::new(SettingField { - json_path: Some("git.inline_blame.padding"), - pick: |settings_content| { - settings_content - .git - .as_ref()? - .inline_blame - .as_ref()? - .padding - .as_ref() - }, - write: |settings_content, value| { - settings_content - .git - .get_or_insert_default() - .inline_blame - .get_or_insert_default() - .padding = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Minimum Column", - description: "The minimum column number at which to show the inline blame information.", - field: Box::new(SettingField { - json_path: Some("git.inline_blame.min_column"), - pick: |settings_content| { - settings_content - .git - .as_ref()? - .inline_blame - .as_ref()? - .min_column - .as_ref() - }, - write: |settings_content, value| { - settings_content - .git - .get_or_insert_default() - .inline_blame - .get_or_insert_default() - .min_column = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Commit Summary", - description: "Show commit summary as part of the inline blame.", - field: Box::new(SettingField { - json_path: Some("git.inline_blame.show_commit_summary"), - pick: |settings_content| { - settings_content - .git - .as_ref()? - .inline_blame - .as_ref()? - .show_commit_summary - .as_ref() - }, - write: |settings_content, value| { - settings_content - .git - .get_or_insert_default() - .inline_blame - .get_or_insert_default() - .show_commit_summary = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Git Blame View"), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Avatar", - description: "Show the avatar of the author of the commit.", - field: Box::new(SettingField { - json_path: Some("git.blame.show_avatar"), - pick: |settings_content| { - settings_content - .git - .as_ref()? - .blame - .as_ref()? - .show_avatar - .as_ref() - }, - write: |settings_content, value| { - settings_content - .git - .get_or_insert_default() - .blame - .get_or_insert_default() - .show_avatar = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Branch Picker"), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Author Name", - description: "Show author name as part of the commit information in branch picker.", - field: Box::new(SettingField { - json_path: Some("git.branch_picker.show_author_name"), - pick: |settings_content| { - settings_content - .git - .as_ref()? - .branch_picker - .as_ref()? - .show_author_name - .as_ref() - }, - write: |settings_content, value| { - settings_content - .git - .get_or_insert_default() - .branch_picker - .get_or_insert_default() - .show_author_name = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Git Hunks"), - SettingsPageItem::SettingItem(SettingItem { - title: "Hunk Style", - description: "How Git hunks are displayed visually in the editor.", - field: Box::new(SettingField { - json_path: Some("git.hunk_style"), - pick: |settings_content| settings_content.git.as_ref()?.hunk_style.as_ref(), - write: |settings_content, value| { - settings_content.git.get_or_insert_default().hunk_style = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Path Style", - description: "Should the name or path be displayed first in the git view.", - field: Box::new(SettingField { - json_path: Some("git.path_style"), - pick: |settings_content| settings_content.git.as_ref()?.path_style.as_ref(), - write: |settings_content, value| { - settings_content.git.get_or_insert_default().path_style = value; - }, - }), - metadata: None, - files: USER, - }), - ], - }, - SettingsPage { - title: "Collaboration", - items: vec![ - SettingsPageItem::SectionHeader("Calls"), - SettingsPageItem::SettingItem(SettingItem { - title: "Mute On Join", - description: "Whether the microphone should be muted when joining a channel or a call.", - field: Box::new(SettingField { - json_path: Some("calls.mute_on_join"), - pick: |settings_content| { - settings_content.calls.as_ref()?.mute_on_join.as_ref() - }, - write: |settings_content, value| { - settings_content.calls.get_or_insert_default().mute_on_join = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Share On Join", - description: "Whether your current project should be shared when joining an empty channel.", - field: Box::new(SettingField { - json_path: Some("calls.share_on_join"), - pick: |settings_content| { - settings_content.calls.as_ref()?.share_on_join.as_ref() - }, - write: |settings_content, value| { - settings_content.calls.get_or_insert_default().share_on_join = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Experimental"), - SettingsPageItem::SettingItem(SettingItem { - title: "Rodio Audio", - description: "Opt into the new audio system.", - field: Box::new(SettingField { - json_path: Some("audio.experimental.rodio_audio"), - pick: |settings_content| { - settings_content.audio.as_ref()?.rodio_audio.as_ref() - }, - write: |settings_content, value| { - settings_content.audio.get_or_insert_default().rodio_audio = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Auto Microphone Volume", - description: "Automatically adjust microphone volume (requires rodio audio).", - field: Box::new(SettingField { - json_path: Some("audio.experimental.auto_microphone_volume"), - pick: |settings_content| { - settings_content - .audio - .as_ref()? - .auto_microphone_volume - .as_ref() - }, - write: |settings_content, value| { - settings_content - .audio - .get_or_insert_default() - .auto_microphone_volume = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Auto Speaker Volume", - description: "Automatically adjust volume of other call members (requires rodio audio).", - field: Box::new(SettingField { - json_path: Some("audio.experimental.auto_speaker_volume"), - pick: |settings_content| { - settings_content - .audio - .as_ref()? - .auto_speaker_volume - .as_ref() - }, - write: |settings_content, value| { - settings_content - .audio - .get_or_insert_default() - .auto_speaker_volume = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Denoise", - description: "Remove background noises (requires rodio audio).", - field: Box::new(SettingField { - json_path: Some("audio.experimental.denoise"), - pick: |settings_content| settings_content.audio.as_ref()?.denoise.as_ref(), - write: |settings_content, value| { - settings_content.audio.get_or_insert_default().denoise = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Legacy Audio Compatible", - description: "Use audio parameters compatible with previous versions (requires rodio audio).", - field: Box::new(SettingField { - json_path: Some("audio.experimental.legacy_audio_compatible"), - pick: |settings_content| { - settings_content - .audio - .as_ref()? - .legacy_audio_compatible - .as_ref() - }, - write: |settings_content, value| { - settings_content - .audio - .get_or_insert_default() - .legacy_audio_compatible = value; - }, - }), - metadata: None, - files: USER, - }), - ], - }, - SettingsPage { - title: "AI", - items: { - let mut items = vec![ - SettingsPageItem::SectionHeader("General"), - SettingsPageItem::SettingItem(SettingItem { - title: "Disable AI", - description: "Whether to disable all AI features in Zed.", - field: Box::new(SettingField { - json_path: Some("disable_ai"), - pick: |settings_content| settings_content.disable_ai.as_ref(), - write: |settings_content, value| { - settings_content.disable_ai = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Agent Configuration"), - SettingsPageItem::SettingItem(SettingItem { - title: "Always Allow Tool Actions", - description: "When enabled, the agent can run potentially destructive actions without asking for your confirmation. This setting has no effect on external agents.", - field: Box::new(SettingField { - json_path: Some("agent.always_allow_tool_actions"), - pick: |settings_content| { - settings_content - .agent - .as_ref()? - .always_allow_tool_actions - .as_ref() - }, - write: |settings_content, value| { - settings_content - .agent - .get_or_insert_default() - .always_allow_tool_actions = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Single File Review", - description: "When enabled, agent edits will also be displayed in single-file buffers for review.", - field: Box::new(SettingField { - json_path: Some("agent.single_file_review"), - pick: |settings_content| { - settings_content.agent.as_ref()?.single_file_review.as_ref() - }, - write: |settings_content, value| { - settings_content - .agent - .get_or_insert_default() - .single_file_review = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Enable Feedback", - description: "Show voting thumbs up/down icon buttons for feedback on agent edits.", - field: Box::new(SettingField { - json_path: Some("agent.enable_feedback"), - pick: |settings_content| { - settings_content.agent.as_ref()?.enable_feedback.as_ref() - }, - write: |settings_content, value| { - settings_content - .agent - .get_or_insert_default() - .enable_feedback = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Notify When Agent Waiting", - description: "Where to show notifications when the agent has completed its response or needs confirmation before running a tool action.", - field: Box::new(SettingField { - json_path: Some("agent.notify_when_agent_waiting"), - pick: |settings_content| { - settings_content - .agent - .as_ref()? - .notify_when_agent_waiting - .as_ref() - }, - write: |settings_content, value| { - settings_content - .agent - .get_or_insert_default() - .notify_when_agent_waiting = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Play Sound When Agent Done", - description: "Whether to play a sound when the agent has either completed its response, or needs user input.", - field: Box::new(SettingField { - json_path: Some("agent.play_sound_when_agent_done"), - pick: |settings_content| { - settings_content - .agent - .as_ref()? - .play_sound_when_agent_done - .as_ref() - }, - write: |settings_content, value| { - settings_content - .agent - .get_or_insert_default() - .play_sound_when_agent_done = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Expand Edit Card", - description: "Whether to have edit cards in the agent panel expanded, showing a Preview of the diff.", - field: Box::new(SettingField { - json_path: Some("agent.expand_edit_card"), - pick: |settings_content| { - settings_content.agent.as_ref()?.expand_edit_card.as_ref() - }, - write: |settings_content, value| { - settings_content - .agent - .get_or_insert_default() - .expand_edit_card = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Expand Terminal Card", - description: "Whether to have terminal cards in the agent panel expanded, showing the whole command output.", - field: Box::new(SettingField { - json_path: Some("agent.expand_terminal_card"), - pick: |settings_content| { - settings_content - .agent - .as_ref()? - .expand_terminal_card - .as_ref() - }, - write: |settings_content, value| { - settings_content - .agent - .get_or_insert_default() - .expand_terminal_card = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Use Modifier To Send", - description: "Whether to always use cmd-enter (or ctrl-enter on Linux or Windows) to send messages.", - field: Box::new(SettingField { - json_path: Some("agent.use_modifier_to_send"), - pick: |settings_content| { - settings_content - .agent - .as_ref()? - .use_modifier_to_send - .as_ref() - }, - write: |settings_content, value| { - settings_content - .agent - .get_or_insert_default() - .use_modifier_to_send = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Message Editor Min Lines", - description: "Minimum number of lines to display in the agent message editor.", - field: Box::new(SettingField { - json_path: Some("agent.message_editor_min_lines"), - pick: |settings_content| { - settings_content - .agent - .as_ref()? - .message_editor_min_lines - .as_ref() - }, - write: |settings_content, value| { - settings_content - .agent - .get_or_insert_default() - .message_editor_min_lines = value; - }, - }), - metadata: None, - files: USER, - }), - ]; - items.extend(edit_prediction_language_settings_section()); - items.extend( - [ - SettingsPageItem::SettingItem(SettingItem { - title: "Display Mode", - description: "When to show edit predictions previews in buffer. The eager mode displays them inline, while the subtle mode displays them only when holding a modifier key.", - field: Box::new(SettingField { - json_path: Some("edit_prediction.display_mode"), - pick: |settings_content| { - settings_content.project.all_languages.edit_predictions.as_ref()?.mode.as_ref() - }, - write: |settings_content, value| { - settings_content.project.all_languages.edit_predictions.get_or_insert_default().mode = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "In Text Threads", - description: "Whether edit predictions are enabled when editing text threads in the agent panel.", - field: Box::new(SettingField { - json_path: Some("edit_prediction.in_text_threads"), - pick: |settings_content| { - settings_content.project.all_languages.edit_predictions.as_ref()?.enabled_in_text_threads.as_ref() - }, - write: |settings_content, value| { - settings_content.project.all_languages.edit_predictions.get_or_insert_default().enabled_in_text_threads = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Copilot Provider", - description: "Use GitHub Copilot as your edit prediction provider.", - field: Box::new( - SettingField { - json_path: Some("edit_prediction.copilot_provider"), - pick: |settings_content| { - settings_content.project.all_languages.edit_predictions.as_ref()?.copilot.as_ref() - }, - write: |settings_content, value| { - settings_content.project.all_languages.edit_predictions.get_or_insert_default().copilot = value; - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Codestral Provider", - description: "Use Mistral's Codestral as your edit prediction provider.", - field: Box::new( - SettingField { - json_path: Some("edit_prediction.codestral_provider"), - pick: |settings_content| { - settings_content.project.all_languages.edit_predictions.as_ref()?.codestral.as_ref() - }, - write: |settings_content, value| { - settings_content.project.all_languages.edit_predictions.get_or_insert_default().codestral = value; - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - ] - ); - items - }, - }, - SettingsPage { - title: "Network", - items: vec![ - SettingsPageItem::SectionHeader("Network"), - // todo(settings_ui): Proxy needs a default - SettingsPageItem::SettingItem(SettingItem { - title: "Proxy", - description: "The proxy to use for network requests.", - field: Box::new( - SettingField { - json_path: Some("proxy"), - pick: |settings_content| settings_content.proxy.as_ref(), - write: |settings_content, value| { - settings_content.proxy = value; - }, - } - .unimplemented(), - ), - metadata: Some(Box::new(SettingsFieldMetadata { - placeholder: Some("socks5h://localhost:10808"), - ..Default::default() - })), - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Server URL", - description: "The URL of the Zed server to connect to.", - field: Box::new(SettingField { - json_path: Some("server_url"), - pick: |settings_content| settings_content.server_url.as_ref(), - write: |settings_content, value| { - settings_content.server_url = value; - }, - }), - metadata: Some(Box::new(SettingsFieldMetadata { - placeholder: Some("https://zed.dev"), - ..Default::default() - })), - files: USER, - }), - ], - }, - ] -} - -const LANGUAGES_SECTION_HEADER: &'static str = "Languages"; - -fn current_language() -> Option { - sub_page_stack().iter().find_map(|page| { - (page.section_header == LANGUAGES_SECTION_HEADER).then(|| page.link.title.clone()) - }) -} - -fn language_settings_field( - settings_content: &SettingsContent, - get: fn(&LanguageSettingsContent) -> Option<&T>, -) -> Option<&T> { - let all_languages = &settings_content.project.all_languages; - if let Some(current_language_name) = current_language() { - if let Some(current_language) = all_languages.languages.0.get(¤t_language_name) { - let value = get(current_language); - if value.is_some() { - return value; - } - } - } - let default_value = get(&all_languages.defaults); - return default_value; -} - -fn language_settings_field_mut( - settings_content: &mut SettingsContent, - value: Option, - write: fn(&mut LanguageSettingsContent, Option), -) { - let all_languages = &mut settings_content.project.all_languages; - let language_content = if let Some(current_language) = current_language() { - all_languages - .languages - .0 - .entry(current_language) - .or_default() - } else { - &mut all_languages.defaults - }; - write(language_content, value); -} - -fn language_settings_data() -> Vec { - let mut items = vec![ - SettingsPageItem::SectionHeader("Indentation"), - SettingsPageItem::SettingItem(SettingItem { - title: "Tab Size", - description: "How many columns a tab should occupy.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).tab_size"), // TODO(cameron): not JQ syntax because not URL-safe - pick: |settings_content| { - language_settings_field(settings_content, |language| language.tab_size.as_ref()) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.tab_size = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Hard Tabs", - description: "Whether to indent lines using tab characters, as opposed to multiple spaces.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).hard_tabs"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.hard_tabs.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.hard_tabs = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Auto Indent", - description: "Whether indentation should be adjusted based on the context whilst typing.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).auto_indent"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.auto_indent.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.auto_indent = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Auto Indent On Paste", - description: "Whether indentation of pasted content should be adjusted based on the context.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).auto_indent_on_paste"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.auto_indent_on_paste.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.auto_indent_on_paste = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SectionHeader("Wrapping"), - SettingsPageItem::SettingItem(SettingItem { - title: "Soft Wrap", - description: "How to soft-wrap long lines of text.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).soft_wrap"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.soft_wrap.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.soft_wrap = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Wrap Guides", - description: "Show wrap guides in the editor.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).show_wrap_guides"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.show_wrap_guides.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.show_wrap_guides = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Preferred Line Length", - description: "The column at which to soft-wrap lines, for buffers where soft-wrap is enabled.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).preferred_line_length"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.preferred_line_length.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.preferred_line_length = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Wrap Guides", - description: "Character counts at which to show wrap guides in the editor.", - field: Box::new( - SettingField { - json_path: Some("languages.$(language).wrap_guides"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.wrap_guides.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.wrap_guides = value; - }) - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Allow Rewrap", - description: "Controls where the `editor::rewrap` action is allowed for this language.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).allow_rewrap"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.allow_rewrap.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.allow_rewrap = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SectionHeader("Indent Guides"), - SettingsPageItem::SettingItem(SettingItem { - title: "Enabled", - description: "Display indent guides in the editor.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).indent_guides.enabled"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language - .indent_guides - .as_ref() - .and_then(|indent_guides| indent_guides.enabled.as_ref()) - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.indent_guides.get_or_insert_default().enabled = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Line Width", - description: "The width of the indent guides in pixels, between 1 and 10.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).indent_guides.line_width"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language - .indent_guides - .as_ref() - .and_then(|indent_guides| indent_guides.line_width.as_ref()) - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.indent_guides.get_or_insert_default().line_width = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Active Line Width", - description: "The width of the active indent guide in pixels, between 1 and 10.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).indent_guides.active_line_width"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language - .indent_guides - .as_ref() - .and_then(|indent_guides| indent_guides.active_line_width.as_ref()) - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language - .indent_guides - .get_or_insert_default() - .active_line_width = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Coloring", - description: "Determines how indent guides are colored.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).indent_guides.coloring"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language - .indent_guides - .as_ref() - .and_then(|indent_guides| indent_guides.coloring.as_ref()) - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.indent_guides.get_or_insert_default().coloring = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Background Coloring", - description: "Determines how indent guide backgrounds are colored.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).indent_guides.background_coloring"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language - .indent_guides - .as_ref() - .and_then(|indent_guides| indent_guides.background_coloring.as_ref()) - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language - .indent_guides - .get_or_insert_default() - .background_coloring = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SectionHeader("Formatting"), - SettingsPageItem::SettingItem(SettingItem { - title: "Format On Save", - description: "Whether or not to perform a buffer format before saving.", - field: Box::new( - // TODO(settings_ui): this setting should just be a bool - SettingField { - json_path: Some("languages.$(language).format_on_save"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.format_on_save.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.format_on_save = value; - }) - }, - }, - ), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Remove Trailing Whitespace On Save", - description: "Whether or not to remove any trailing whitespace from lines of a buffer before saving it.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).remove_trailing_whitespace_on_save"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.remove_trailing_whitespace_on_save.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.remove_trailing_whitespace_on_save = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Ensure Final Newline On Save", - description: "Whether or not to ensure there's a single newline at the end of a buffer when saving it.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).ensure_final_newline_on_save"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.ensure_final_newline_on_save.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.ensure_final_newline_on_save = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Formatter", - description: "How to perform a buffer format.", - field: Box::new( - SettingField { - json_path: Some("languages.$(language).formatter"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.formatter.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.formatter = value; - }) - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Use On Type Format", - description: "Whether to use additional LSP queries to format (and amend) the code after every \"trigger\" symbol input, defined by LSP server capabilities", - field: Box::new(SettingField { - json_path: Some("languages.$(language).use_on_type_format"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.use_on_type_format.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.use_on_type_format = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Code Actions On Format", - description: "Additional code actions to run when formatting.", - field: Box::new( - SettingField { - json_path: Some("languages.$(language).code_actions_on_format"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.code_actions_on_format.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.code_actions_on_format = value; - }) - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SectionHeader("Autoclose"), - SettingsPageItem::SettingItem(SettingItem { - title: "Use Autoclose", - description: "Whether to automatically type closing characters for you. For example, when you type '(', Zed will automatically add a closing ')' at the correct position.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).use_autoclose"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.use_autoclose.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.use_autoclose = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Use Auto Surround", - description: "Whether to automatically surround text with characters for you. For example, when you select text and type '(', Zed will automatically surround text with ().", - field: Box::new(SettingField { - json_path: Some("languages.$(language).use_auto_surround"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.use_auto_surround.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.use_auto_surround = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Always Treat Brackets As Autoclosed", - description: "Controls whether the closing characters are always skipped over and auto-removed no matter how they were inserted.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).always_treat_brackets_as_autoclosed"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.always_treat_brackets_as_autoclosed.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.always_treat_brackets_as_autoclosed = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "JSX Tag Auto Close", - description: "Whether to automatically close JSX tags.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).jsx_tag_auto_close"), - // TODO(settings_ui): this setting should just be a bool - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.jsx_tag_auto_close.as_ref()?.enabled.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.jsx_tag_auto_close.get_or_insert_default().enabled = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SectionHeader("Whitespace"), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Whitespaces", - description: "Whether to show tabs and spaces in the editor.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).show_whitespaces"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.show_whitespaces.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.show_whitespaces = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Space Whitespace Indicator", - description: "Visible character used to render space characters when show_whitespaces is enabled (default: \"•\")", - field: Box::new( - SettingField { - json_path: Some("languages.$(language).whitespace_map.space"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.whitespace_map.as_ref()?.space.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.whitespace_map.get_or_insert_default().space = value; - }) - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Tab Whitespace Indicator", - description: "Visible character used to render tab characters when show_whitespaces is enabled (default: \"→\")", - field: Box::new( - SettingField { - json_path: Some("languages.$(language).whitespace_map.tab"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.whitespace_map.as_ref()?.tab.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.whitespace_map.get_or_insert_default().tab = value; - }) - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SectionHeader("Completions"), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Completions On Input", - description: "Whether to pop the completions menu while typing in an editor without explicitly requesting it.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).show_completions_on_input"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.show_completions_on_input.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.show_completions_on_input = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Completion Documentation", - description: "Whether to display inline and alongside documentation for items in the completions menu.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).show_completion_documentation"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.show_completion_documentation.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.show_completion_documentation = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Words", - description: "Controls how words are completed.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).completions.words"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.completions.as_ref()?.words.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.completions.get_or_insert_default().words = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Words Min Length", - description: "How many characters has to be in the completions query to automatically show the words-based completions.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).completions.words_min_length"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.completions.as_ref()?.words_min_length.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language - .completions - .get_or_insert_default() - .words_min_length = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Completion Menu Scrollbar", - description: "When to show the scrollbar in the completion menu.", - field: Box::new(SettingField { - json_path: Some("editor.completion_menu_scrollbar"), - pick: |settings_content| settings_content.editor.completion_menu_scrollbar.as_ref(), - write: |settings_content, value| { - settings_content.editor.completion_menu_scrollbar = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("Inlay Hints"), - SettingsPageItem::SettingItem(SettingItem { - title: "Enabled", - description: "Global switch to toggle hints on and off.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).inlay_hints.enabled"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.inlay_hints.as_ref()?.enabled.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.inlay_hints.get_or_insert_default().enabled = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Value Hints", - description: "Global switch to toggle inline values on and off when debugging.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).inlay_hints.show_value_hints"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.inlay_hints.as_ref()?.show_value_hints.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language - .inlay_hints - .get_or_insert_default() - .show_value_hints = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Type Hints", - description: "Whether type hints should be shown.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).inlay_hints.show_type_hints"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.inlay_hints.as_ref()?.show_type_hints.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.inlay_hints.get_or_insert_default().show_type_hints = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Parameter Hints", - description: "Whether parameter hints should be shown.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).inlay_hints.show_parameter_hints"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.inlay_hints.as_ref()?.show_parameter_hints.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language - .inlay_hints - .get_or_insert_default() - .show_parameter_hints = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Other Hints", - description: "Whether other hints should be shown.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).inlay_hints.show_other_hints"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.inlay_hints.as_ref()?.show_other_hints.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language - .inlay_hints - .get_or_insert_default() - .show_other_hints = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Background", - description: "Show a background for inlay hints.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).inlay_hints.show_background"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.inlay_hints.as_ref()?.show_background.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.inlay_hints.get_or_insert_default().show_background = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Edit Debounce Ms", - description: "Whether or not to debounce inlay hints updates after buffer edits (set to 0 to disable debouncing).", - field: Box::new(SettingField { - json_path: Some("languages.$(language).inlay_hints.edit_debounce_ms"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.inlay_hints.as_ref()?.edit_debounce_ms.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language - .inlay_hints - .get_or_insert_default() - .edit_debounce_ms = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Scroll Debounce Ms", - description: "Whether or not to debounce inlay hints updates after buffer scrolls (set to 0 to disable debouncing).", - field: Box::new(SettingField { - json_path: Some("languages.$(language).inlay_hints.scroll_debounce_ms"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.inlay_hints.as_ref()?.scroll_debounce_ms.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language - .inlay_hints - .get_or_insert_default() - .scroll_debounce_ms = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Toggle On Modifiers Press", - description: "Toggles inlay hints (hides or shows) when the user presses the modifiers specified.", - field: Box::new( - SettingField { - json_path: Some("languages.$(language).inlay_hints.toggle_on_modifiers_press"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language - .inlay_hints - .as_ref()? - .toggle_on_modifiers_press - .as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language - .inlay_hints - .get_or_insert_default() - .toggle_on_modifiers_press = value; - }) - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - ]; - if current_language().is_none() { - items.push(SettingsPageItem::SettingItem(SettingItem { - title: "LSP Document Colors", - description: "How to render LSP color previews in the editor.", - field: Box::new(SettingField { - json_path: Some("lsp_document_colors"), - pick: |settings_content| settings_content.editor.lsp_document_colors.as_ref(), - write: |settings_content, value| { - settings_content.editor.lsp_document_colors = value; - }, - }), - metadata: None, - files: USER, - })) - } - items.extend([ - SettingsPageItem::SectionHeader("Tasks"), - SettingsPageItem::SettingItem(SettingItem { - title: "Enabled", - description: "Whether tasks are enabled for this language.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).tasks.enabled"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.tasks.as_ref()?.enabled.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.tasks.get_or_insert_default().enabled = value; - - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Variables", - description: "Extra task variables to set for a particular language.", - field: Box::new( - SettingField { - json_path: Some("languages.$(language).tasks.variables"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.tasks.as_ref()?.variables.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.tasks.get_or_insert_default().variables = value; - - }) - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Prefer LSP", - description: "Use LSP tasks over Zed language extension tasks.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).tasks.prefer_lsp"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.tasks.as_ref()?.prefer_lsp.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.tasks.get_or_insert_default().prefer_lsp = value; - - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SectionHeader("Miscellaneous"), - SettingsPageItem::SettingItem(SettingItem { - title: "Word Diff Enabled", - description: "Whether to enable word diff highlighting in the editor. When enabled, changed words within modified lines are highlighted to show exactly what changed.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).word_diff_enabled"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.word_diff_enabled.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.word_diff_enabled = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Debuggers", - description: "Preferred debuggers for this language.", - field: Box::new( - SettingField { - json_path: Some("languages.$(language).debuggers"), - pick: |settings_content| { - language_settings_field(settings_content, |language| language.debuggers.as_ref()) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.debuggers = value; - - }) - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Middle Click Paste", - description: "Enable middle-click paste on Linux.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).editor.middle_click_paste"), - pick: |settings_content| settings_content.editor.middle_click_paste.as_ref(), - write: |settings_content, value| {settings_content.editor.middle_click_paste = value;}, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Extend Comment On Newline", - description: "Whether to start a new line with a comment when a previous line is a comment as well.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).extend_comment_on_newline"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.extend_comment_on_newline.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.extend_comment_on_newline = value; - - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Colorize Brackets", - description: "Whether to colorize brackets in the editor.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).colorize_brackets"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.colorize_brackets.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.colorize_brackets = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - ]); - - if current_language().is_none() { - items.extend([ - SettingsPageItem::SettingItem(SettingItem { - title: "Image Viewer", - description: "The unit for image file sizes.", - field: Box::new(SettingField { - json_path: Some("image_viewer.unit"), - pick: |settings_content| { - settings_content.image_viewer.as_ref().and_then(|image_viewer| image_viewer.unit.as_ref()) - }, - write: |settings_content, value| { - settings_content.image_viewer.get_or_insert_default().unit = value; - - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Auto Replace Emoji Shortcode", - description: "Whether to automatically replace emoji shortcodes with emoji characters.", - field: Box::new(SettingField { - json_path: Some("message_editor.auto_replace_emoji_shortcode"), - pick: |settings_content| { - settings_content.message_editor.as_ref().and_then(|message_editor| message_editor.auto_replace_emoji_shortcode.as_ref()) - }, - write: |settings_content, value| { - settings_content.message_editor.get_or_insert_default().auto_replace_emoji_shortcode = value; - - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Drop Size Target", - description: "Relative size of the drop target in the editor that will open dropped file as a split pane.", - field: Box::new(SettingField { - json_path: Some("drop_target_size"), - pick: |settings_content| { - settings_content.workspace.drop_target_size.as_ref() - }, - write: |settings_content, value| { - settings_content.workspace.drop_target_size = value; - - }, - }), - metadata: None, - files: USER, - }), - ]); - } - items -} - -/// LanguageSettings items that should be included in the "Languages & Tools" page -/// not the "Editor" page -fn non_editor_language_settings_data() -> Vec { - vec![ - SettingsPageItem::SectionHeader("LSP"), - SettingsPageItem::SettingItem(SettingItem { - title: "Enable Language Server", - description: "Whether to use language servers to provide code intelligence.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).enable_language_server"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.enable_language_server.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.enable_language_server = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Language Servers", - description: "The list of language servers to use (or disable) for this language.", - field: Box::new( - SettingField { - json_path: Some("languages.$(language).language_servers"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.language_servers.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.language_servers = value; - }) - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Linked Edits", - description: "Whether to perform linked edits of associated ranges, if the LS supports it. For example, when editing opening tag, the contents of the closing tag will be edited as well.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).linked_edits"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.linked_edits.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.linked_edits = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Go To Definition Fallback", - description: "Whether to follow-up empty Go to definition responses from the language server.", - field: Box::new(SettingField { - json_path: Some("go_to_definition_fallback"), - pick: |settings_content| settings_content.editor.go_to_definition_fallback.as_ref(), - write: |settings_content, value| { - settings_content.editor.go_to_definition_fallback = value; - }, - }), - metadata: None, - files: USER, - }), - SettingsPageItem::SectionHeader("LSP Completions"), - SettingsPageItem::SettingItem(SettingItem { - title: "Enabled", - description: "Whether to fetch LSP completions or not.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).completions.lsp"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.completions.as_ref()?.lsp.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.completions.get_or_insert_default().lsp = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Fetch Timeout (milliseconds)", - description: "When fetching LSP completions, determines how long to wait for a response of a particular server (set to 0 to wait indefinitely).", - field: Box::new(SettingField { - json_path: Some("languages.$(language).completions.lsp_fetch_timeout_ms"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.completions.as_ref()?.lsp_fetch_timeout_ms.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language - .completions - .get_or_insert_default() - .lsp_fetch_timeout_ms = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Insert Mode", - description: "Controls how LSP completions are inserted.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).completions.lsp_insert_mode"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.completions.as_ref()?.lsp_insert_mode.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.completions.get_or_insert_default().lsp_insert_mode = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SectionHeader("Debuggers"), - SettingsPageItem::SettingItem(SettingItem { - title: "Debuggers", - description: "Preferred debuggers for this language.", - field: Box::new( - SettingField { - json_path: Some("languages.$(language).debuggers"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.debuggers.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.debuggers = value; - }) - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SectionHeader("Prettier"), - SettingsPageItem::SettingItem(SettingItem { - title: "Allowed", - description: "Enables or disables formatting with Prettier for a given language.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).prettier.allowed"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.prettier.as_ref()?.allowed.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.prettier.get_or_insert_default().allowed = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Parser", - description: "Forces Prettier integration to use a specific parser name when formatting files with the language.", - field: Box::new(SettingField { - json_path: Some("languages.$(language).prettier.parser"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.prettier.as_ref()?.parser.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.prettier.get_or_insert_default().parser = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Plugins", - description: "Forces Prettier integration to use specific plugins when formatting files with the language.", - field: Box::new( - SettingField { - json_path: Some("languages.$(language).prettier.plugins"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.prettier.as_ref()?.plugins.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.prettier.get_or_insert_default().plugins = value; - }) - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Options", - description: "Default Prettier options, in the format as in package.json section for Prettier.", - field: Box::new( - SettingField { - json_path: Some("languages.$(language).prettier.options"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.prettier.as_ref()?.options.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.prettier.get_or_insert_default().options = value; - }) - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - ] -} - -fn edit_prediction_language_settings_section() -> Vec { - vec![ - SettingsPageItem::SectionHeader("Edit Predictions"), - SettingsPageItem::SettingItem(SettingItem { - title: "Show Edit Predictions", - description: "Controls whether edit predictions are shown immediately or manually by triggering `editor::showeditprediction` (false).", - field: Box::new(SettingField { - json_path: Some("languages.$(language).show_edit_predictions"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.show_edit_predictions.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.show_edit_predictions = value; - }) - }, - }), - metadata: None, - files: USER | PROJECT, - }), - SettingsPageItem::SettingItem(SettingItem { - title: "Edit Predictions Disabled In", - description: "Controls whether edit predictions are shown in the given language scopes.", - field: Box::new( - SettingField { - json_path: Some("languages.$(language).edit_predictions_disabled_in"), - pick: |settings_content| { - language_settings_field(settings_content, |language| { - language.edit_predictions_disabled_in.as_ref() - }) - }, - write: |settings_content, value| { - language_settings_field_mut(settings_content, value, |language, value| { - language.edit_predictions_disabled_in = value; - }) - }, - } - .unimplemented(), - ), - metadata: None, - files: USER | PROJECT, - }), - ] -} - -fn show_scrollbar_or_editor( - settings_content: &SettingsContent, - show: fn(&SettingsContent) -> Option<&settings::ShowScrollbar>, -) -> Option<&settings::ShowScrollbar> { - show(settings_content).or(settings_content - .editor - .scrollbar - .as_ref() - .and_then(|scrollbar| scrollbar.show.as_ref())) -} - -fn dynamic_variants() -> &'static [T::Discriminant] -where - T: strum::IntoDiscriminant, - T::Discriminant: strum::VariantArray, -{ - <::Discriminant as strum::VariantArray>::VARIANTS -} diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs deleted file mode 100644 index 4464d3bdd9..0000000000 --- a/crates/settings_ui/src/settings_ui.rs +++ /dev/null @@ -1,4047 +0,0 @@ -mod components; -mod page_data; - -use anyhow::Result; -use editor::{Editor, EditorEvent}; -use feature_flags::FeatureFlag; -use fuzzy::StringMatchCandidate; -use gpui::{ - Action, App, ClipboardItem, DEFAULT_ADDITIONAL_WINDOW_SIZE, Div, Entity, FocusHandle, - Focusable, Global, KeyContext, ListState, ReadGlobal as _, ScrollHandle, Stateful, - Subscription, Task, TitlebarOptions, UniformListScrollHandle, Window, WindowBounds, - WindowHandle, WindowOptions, actions, div, list, point, prelude::*, px, uniform_list, -}; -use project::{Project, WorktreeId}; -use release_channel::ReleaseChannel; -use schemars::JsonSchema; -use serde::Deserialize; -use settings::{Settings, SettingsContent, SettingsStore, initial_project_settings_content}; -use std::{ - any::{Any, TypeId, type_name}, - cell::RefCell, - collections::{HashMap, HashSet}, - num::{NonZero, NonZeroU32}, - ops::Range, - rc::Rc, - sync::{Arc, LazyLock, RwLock}, - time::Duration, -}; -use title_bar::platform_title_bar::PlatformTitleBar; -use ui::{ - Banner, ContextMenu, Divider, DividerColor, DropdownMenu, DropdownStyle, IconButtonShape, - KeyBinding, KeybindingHint, PopoverMenu, Switch, Tooltip, TreeViewItem, WithScrollbar, - prelude::*, -}; -use ui_input::{NumberField, NumberFieldType}; -use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath}; -use workspace::{AppState, OpenOptions, OpenVisible, Workspace, client_side_decorations}; -use zed_actions::{OpenProjectSettings, OpenSettings, OpenSettingsAt}; - -use crate::components::{ - EnumVariantDropdown, SettingsInputField, font_picker, icon_theme_picker, theme_picker, -}; - -const NAVBAR_CONTAINER_TAB_INDEX: isize = 0; -const NAVBAR_GROUP_TAB_INDEX: isize = 1; - -const HEADER_CONTAINER_TAB_INDEX: isize = 2; -const HEADER_GROUP_TAB_INDEX: isize = 3; - -const CONTENT_CONTAINER_TAB_INDEX: isize = 4; -const CONTENT_GROUP_TAB_INDEX: isize = 5; - -actions!( - settings_editor, - [ - /// Minimizes the settings UI window. - Minimize, - /// Toggles focus between the navbar and the main content. - ToggleFocusNav, - /// Expands the navigation entry. - ExpandNavEntry, - /// Collapses the navigation entry. - CollapseNavEntry, - /// Focuses the next file in the file list. - FocusNextFile, - /// Focuses the previous file in the file list. - FocusPreviousFile, - /// Opens an editor for the current file - OpenCurrentFile, - /// Focuses the previous root navigation entry. - FocusPreviousRootNavEntry, - /// Focuses the next root navigation entry. - FocusNextRootNavEntry, - /// Focuses the first navigation entry. - FocusFirstNavEntry, - /// Focuses the last navigation entry. - FocusLastNavEntry, - /// Focuses and opens the next navigation entry without moving focus to content. - FocusNextNavEntry, - /// Focuses and opens the previous navigation entry without moving focus to content. - FocusPreviousNavEntry - ] -); - -#[derive(Action, PartialEq, Eq, Clone, Copy, Debug, JsonSchema, Deserialize)] -#[action(namespace = settings_editor)] -struct FocusFile(pub u32); - -struct SettingField { - pick: fn(&SettingsContent) -> Option<&T>, - write: fn(&mut SettingsContent, Option), - - /// A json-path-like string that gives a unique-ish string that identifies - /// where in the JSON the setting is defined. - /// - /// The syntax is `jq`-like, but modified slightly to be URL-safe (and - /// without the leading dot), e.g. `foo.bar`. - /// - /// They are URL-safe (this is important since links are the main use-case - /// for these paths). - /// - /// There are a couple of special cases: - /// - discrimminants are represented with a trailing `$`, for example - /// `terminal.working_directory$`. This is to distinguish the discrimminant - /// setting (i.e. the setting that changes whether the value is a string or - /// an object) from the setting in the case that it is a string. - /// - language-specific settings begin `languages.$(language)`. Links - /// targeting these settings should take the form `languages/Rust/...`, for - /// example, but are not currently supported. - json_path: Option<&'static str>, -} - -impl Clone for SettingField { - fn clone(&self) -> Self { - *self - } -} - -// manual impl because derive puts a Copy bound on T, which is inaccurate in our case -impl Copy for SettingField {} - -/// Helper for unimplemented settings, used in combination with `SettingField::unimplemented` -/// to keep the setting around in the UI with valid pick and write implementations, but don't actually try to render it. -/// TODO(settings_ui): In non-dev builds (`#[cfg(not(debug_assertions))]`) make this render as edit-in-json -#[derive(Clone, Copy)] -struct UnimplementedSettingField; - -impl PartialEq for UnimplementedSettingField { - fn eq(&self, _other: &Self) -> bool { - true - } -} - -impl SettingField { - /// Helper for settings with types that are not yet implemented. - #[allow(unused)] - fn unimplemented(self) -> SettingField { - SettingField { - pick: |_| Some(&UnimplementedSettingField), - write: |_, _| unreachable!(), - json_path: self.json_path, - } - } -} - -trait AnySettingField { - fn as_any(&self) -> &dyn Any; - fn type_name(&self) -> &'static str; - fn type_id(&self) -> TypeId; - // Returns the file this value was set in and true, or File::Default and false to indicate it was not found in any file (missing default) - fn file_set_in(&self, file: SettingsUiFile, cx: &App) -> (settings::SettingsFile, bool); - fn reset_to_default_fn( - &self, - current_file: &SettingsUiFile, - file_set_in: &settings::SettingsFile, - cx: &App, - ) -> Option>; - - fn json_path(&self) -> Option<&'static str>; -} - -impl AnySettingField for SettingField { - fn as_any(&self) -> &dyn Any { - self - } - - fn type_name(&self) -> &'static str { - type_name::() - } - - fn type_id(&self) -> TypeId { - TypeId::of::() - } - - fn file_set_in(&self, file: SettingsUiFile, cx: &App) -> (settings::SettingsFile, bool) { - let (file, value) = cx - .global::() - .get_value_from_file(file.to_settings(), self.pick); - return (file, value.is_some()); - } - - fn reset_to_default_fn( - &self, - current_file: &SettingsUiFile, - file_set_in: &settings::SettingsFile, - cx: &App, - ) -> Option> { - if file_set_in == &settings::SettingsFile::Default { - return None; - } - if file_set_in != ¤t_file.to_settings() { - return None; - } - let this = *self; - let store = SettingsStore::global(cx); - let default_value = (this.pick)(store.raw_default_settings()); - let is_default = store - .get_content_for_file(file_set_in.clone()) - .map_or(None, this.pick) - == default_value; - if is_default { - return None; - } - let current_file = current_file.clone(); - - return Some(Box::new(move |cx| { - let store = SettingsStore::global(cx); - let default_value = (this.pick)(store.raw_default_settings()); - let is_set_somewhere_other_than_default = store - .get_value_up_to_file(current_file.to_settings(), this.pick) - .0 - != settings::SettingsFile::Default; - let value_to_set = if is_set_somewhere_other_than_default { - default_value.cloned() - } else { - None - }; - update_settings_file(current_file.clone(), None, cx, move |settings, _| { - (this.write)(settings, value_to_set); - }) - // todo(settings_ui): Don't log err - .log_err(); - })); - } - - fn json_path(&self) -> Option<&'static str> { - self.json_path - } -} - -#[derive(Default, Clone)] -struct SettingFieldRenderer { - renderers: Rc< - RefCell< - HashMap< - TypeId, - Box< - dyn Fn( - &SettingsWindow, - &SettingItem, - SettingsUiFile, - Option<&SettingsFieldMetadata>, - bool, - &mut Window, - &mut Context, - ) -> Stateful
, - >, - >, - >, - >, -} - -impl Global for SettingFieldRenderer {} - -impl SettingFieldRenderer { - fn add_basic_renderer( - &mut self, - render_control: impl Fn( - SettingField, - SettingsUiFile, - Option<&SettingsFieldMetadata>, - &mut Window, - &mut App, - ) -> AnyElement - + 'static, - ) -> &mut Self { - self.add_renderer( - move |settings_window: &SettingsWindow, - item: &SettingItem, - field: SettingField, - settings_file: SettingsUiFile, - metadata: Option<&SettingsFieldMetadata>, - sub_field: bool, - window: &mut Window, - cx: &mut Context| { - render_settings_item( - settings_window, - item, - settings_file.clone(), - render_control(field, settings_file, metadata, window, cx), - sub_field, - cx, - ) - }, - ) - } - - fn add_renderer( - &mut self, - renderer: impl Fn( - &SettingsWindow, - &SettingItem, - SettingField, - SettingsUiFile, - Option<&SettingsFieldMetadata>, - bool, - &mut Window, - &mut Context, - ) -> Stateful
- + 'static, - ) -> &mut Self { - let key = TypeId::of::(); - let renderer = Box::new( - move |settings_window: &SettingsWindow, - item: &SettingItem, - settings_file: SettingsUiFile, - metadata: Option<&SettingsFieldMetadata>, - sub_field: bool, - window: &mut Window, - cx: &mut Context| { - let field = *item - .field - .as_ref() - .as_any() - .downcast_ref::>() - .unwrap(); - renderer( - settings_window, - item, - field, - settings_file, - metadata, - sub_field, - window, - cx, - ) - }, - ); - self.renderers.borrow_mut().insert(key, renderer); - self - } -} - -struct NonFocusableHandle { - handle: FocusHandle, - _subscription: Subscription, -} - -impl NonFocusableHandle { - fn new(tab_index: isize, tab_stop: bool, window: &mut Window, cx: &mut App) -> Entity { - let handle = cx.focus_handle().tab_index(tab_index).tab_stop(tab_stop); - Self::from_handle(handle, window, cx) - } - - fn from_handle(handle: FocusHandle, window: &mut Window, cx: &mut App) -> Entity { - cx.new(|cx| { - let _subscription = cx.on_focus(&handle, window, { - move |_, window, _| { - window.focus_next(); - } - }); - Self { - handle, - _subscription, - } - }) - } -} - -impl Focusable for NonFocusableHandle { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.handle.clone() - } -} - -#[derive(Default)] -struct SettingsFieldMetadata { - placeholder: Option<&'static str>, - should_do_titlecase: Option, -} - -pub struct SettingsUiFeatureFlag; - -impl FeatureFlag for SettingsUiFeatureFlag { - const NAME: &'static str = "settings-ui"; -} - -pub fn init(cx: &mut App) { - init_renderers(cx); - - cx.observe_new(|workspace: &mut workspace::Workspace, _, _| { - workspace - .register_action( - |workspace, OpenSettingsAt { path }: &OpenSettingsAt, window, cx| { - let window_handle = window - .window_handle() - .downcast::() - .expect("Workspaces are root Windows"); - open_settings_editor(workspace, Some(&path), false, window_handle, cx); - }, - ) - .register_action(|workspace, _: &OpenSettings, window, cx| { - let window_handle = window - .window_handle() - .downcast::() - .expect("Workspaces are root Windows"); - open_settings_editor(workspace, None, false, window_handle, cx); - }) - .register_action(|workspace, _: &OpenProjectSettings, window, cx| { - let window_handle = window - .window_handle() - .downcast::() - .expect("Workspaces are root Windows"); - open_settings_editor(workspace, None, true, window_handle, cx); - }); - }) - .detach(); -} - -fn init_renderers(cx: &mut App) { - cx.default_global::() - .add_renderer::( - |settings_window, item, _, settings_file, _, sub_field, _, cx| { - render_settings_item( - settings_window, - item, - settings_file, - Button::new("open-in-settings-file", "Edit in settings.json") - .style(ButtonStyle::Outlined) - .size(ButtonSize::Medium) - .tab_index(0_isize) - .tooltip(Tooltip::for_action_title_in( - "Edit in settings.json", - &OpenCurrentFile, - &settings_window.focus_handle, - )) - .on_click(cx.listener(|this, _, window, cx| { - this.open_current_settings_file(window, cx); - })) - .into_any_element(), - sub_field, - cx, - ) - }, - ) - .add_basic_renderer::(render_toggle_button) - .add_basic_renderer::(render_text_field) - .add_basic_renderer::(render_text_field) - .add_basic_renderer::(render_toggle_button) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_font_picker) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_number_field) - .add_basic_renderer::(render_number_field) - .add_basic_renderer::(render_number_field) - .add_basic_renderer::(render_number_field) - .add_basic_renderer::>(render_number_field) - .add_basic_renderer::(render_number_field) - .add_basic_renderer::(render_number_field) - .add_basic_renderer::(render_number_field) - .add_basic_renderer::(render_number_field) - .add_basic_renderer::(render_number_field) - .add_basic_renderer::(render_number_field) - .add_basic_renderer::(render_number_field) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_theme_picker) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_icon_theme_picker) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - .add_basic_renderer::(render_dropdown) - // please semicolon stay on next line - ; -} - -pub fn open_settings_editor( - _workspace: &mut Workspace, - path: Option<&str>, - open_project_settings: bool, - workspace_handle: WindowHandle, - cx: &mut App, -) { - telemetry::event!("Settings Viewed"); - - /// Assumes a settings GUI window is already open - fn open_path( - path: &str, - // Note: This option is unsupported right now - _open_project_settings: bool, - settings_window: &mut SettingsWindow, - window: &mut Window, - cx: &mut Context, - ) { - if path.starts_with("languages.$(language)") { - log::error!("language-specific settings links are not currently supported"); - return; - } - - settings_window.search_bar.update(cx, |editor, cx| { - editor.set_text(format!("#{path}"), window, cx); - }); - settings_window.update_matches(cx); - } - - let existing_window = cx - .windows() - .into_iter() - .find_map(|window| window.downcast::()); - - if let Some(existing_window) = existing_window { - existing_window - .update(cx, |settings_window, window, cx| { - settings_window.original_window = Some(workspace_handle); - window.activate_window(); - if let Some(path) = path { - open_path(path, open_project_settings, settings_window, window, cx); - } else if open_project_settings { - if let Some(file_index) = settings_window - .files - .iter() - .position(|(file, _)| file.worktree_id().is_some()) - { - settings_window.change_file(file_index, window, cx); - } - - cx.notify(); - } - }) - .ok(); - return; - } - - // We have to defer this to get the workspace off the stack. - - let path = path.map(ToOwned::to_owned); - cx.defer(move |cx| { - let current_rem_size: f32 = theme::ThemeSettings::get_global(cx).ui_font_size(cx).into(); - - let default_bounds = DEFAULT_ADDITIONAL_WINDOW_SIZE; - let default_rem_size = 16.0; - let scale_factor = current_rem_size / default_rem_size; - let scaled_bounds: gpui::Size = default_bounds.map(|axis| axis * scale_factor); - - let app_id = ReleaseChannel::global(cx).app_id(); - let window_decorations = match std::env::var("ZED_WINDOW_DECORATIONS") { - Ok(val) if val == "server" => gpui::WindowDecorations::Server, - Ok(val) if val == "client" => gpui::WindowDecorations::Client, - _ => gpui::WindowDecorations::Client, - }; - - cx.open_window( - WindowOptions { - titlebar: Some(TitlebarOptions { - title: Some("Zed — Settings".into()), - appears_transparent: true, - traffic_light_position: Some(point(px(12.0), px(12.0))), - }), - focus: true, - show: true, - is_movable: true, - kind: gpui::WindowKind::Floating, - window_background: cx.theme().window_background_appearance(), - app_id: Some(app_id.to_owned()), - window_decorations: Some(window_decorations), - window_min_size: Some(gpui::Size { - width: px(360.0), - height: px(240.0), - }), - window_bounds: Some(WindowBounds::centered(scaled_bounds, cx)), - ..Default::default() - }, - |window, cx| { - let settings_window = - cx.new(|cx| SettingsWindow::new(Some(workspace_handle), window, cx)); - settings_window.update(cx, |settings_window, cx| { - if let Some(path) = path { - open_path(&path, open_project_settings, settings_window, window, cx); - } else if open_project_settings { - if let Some(file_index) = settings_window - .files - .iter() - .position(|(file, _)| file.worktree_id().is_some()) - { - settings_window.change_file(file_index, window, cx); - } - - settings_window.fetch_files(window, cx); - } - }); - - settings_window - }, - ) - .log_err(); - }); -} - -/// The current sub page path that is selected. -/// If this is empty the selected page is rendered, -/// otherwise the last sub page gets rendered. -/// -/// Global so that `pick` and `write` callbacks can access it -/// and use it to dynamically render sub pages (e.g. for language settings) -static SUB_PAGE_STACK: LazyLock>> = LazyLock::new(|| RwLock::new(Vec::new())); - -fn sub_page_stack() -> std::sync::RwLockReadGuard<'static, Vec> { - SUB_PAGE_STACK - .read() - .expect("SUB_PAGE_STACK is never poisoned") -} - -fn sub_page_stack_mut() -> std::sync::RwLockWriteGuard<'static, Vec> { - SUB_PAGE_STACK - .write() - .expect("SUB_PAGE_STACK is never poisoned") -} - -pub struct SettingsWindow { - title_bar: Option>, - original_window: Option>, - files: Vec<(SettingsUiFile, FocusHandle)>, - worktree_root_dirs: HashMap, - current_file: SettingsUiFile, - pages: Vec, - search_bar: Entity, - search_task: Option>, - /// Index into navbar_entries - navbar_entry: usize, - navbar_entries: Vec, - navbar_scroll_handle: UniformListScrollHandle, - /// [page_index][page_item_index] will be false - /// when the item is filtered out either by searches - /// or by the current file - navbar_focus_subscriptions: Vec, - filter_table: Vec>, - has_query: bool, - content_handles: Vec>>, - sub_page_scroll_handle: ScrollHandle, - focus_handle: FocusHandle, - navbar_focus_handle: Entity, - content_focus_handle: Entity, - files_focus_handle: FocusHandle, - search_index: Option>, - list_state: ListState, - shown_errors: HashSet, -} - -struct SearchIndex { - bm25_engine: bm25::SearchEngine, - fuzzy_match_candidates: Vec, - key_lut: Vec, -} - -struct SearchKeyLUTEntry { - page_index: usize, - header_index: usize, - item_index: usize, - json_path: Option<&'static str>, -} - -struct SubPage { - link: SubPageLink, - section_header: &'static str, -} - -#[derive(Debug)] -struct NavBarEntry { - title: &'static str, - is_root: bool, - expanded: bool, - page_index: usize, - item_index: Option, - focus_handle: FocusHandle, -} - -struct SettingsPage { - title: &'static str, - items: Vec, -} - -#[derive(PartialEq)] -enum SettingsPageItem { - SectionHeader(&'static str), - SettingItem(SettingItem), - SubPageLink(SubPageLink), - DynamicItem(DynamicItem), -} - -impl std::fmt::Debug for SettingsPageItem { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - SettingsPageItem::SectionHeader(header) => write!(f, "SectionHeader({})", header), - SettingsPageItem::SettingItem(setting_item) => { - write!(f, "SettingItem({})", setting_item.title) - } - SettingsPageItem::SubPageLink(sub_page_link) => { - write!(f, "SubPageLink({})", sub_page_link.title) - } - SettingsPageItem::DynamicItem(dynamic_item) => { - write!(f, "DynamicItem({})", dynamic_item.discriminant.title) - } - } - } -} - -impl SettingsPageItem { - fn render( - &self, - settings_window: &SettingsWindow, - item_index: usize, - is_last: bool, - window: &mut Window, - cx: &mut Context, - ) -> AnyElement { - let file = settings_window.current_file.clone(); - - let apply_padding = |element: Stateful
| -> Stateful
{ - let element = element.pt_4(); - if is_last { - element.pb_10() - } else { - element.pb_4() - } - }; - - let mut render_setting_item_inner = - |setting_item: &SettingItem, - padding: bool, - sub_field: bool, - cx: &mut Context| { - let renderer = cx.default_global::().clone(); - let (_, found) = setting_item.field.file_set_in(file.clone(), cx); - - let renderers = renderer.renderers.borrow(); - - let field_renderer = - renderers.get(&AnySettingField::type_id(setting_item.field.as_ref())); - let field_renderer_or_warning = - field_renderer.ok_or("NO RENDERER").and_then(|renderer| { - if cfg!(debug_assertions) && !found { - Err("NO DEFAULT") - } else { - Ok(renderer) - } - }); - - let field = match field_renderer_or_warning { - Ok(field_renderer) => window.with_id(item_index, |window| { - field_renderer( - settings_window, - setting_item, - file.clone(), - setting_item.metadata.as_deref(), - sub_field, - window, - cx, - ) - }), - Err(warning) => render_settings_item( - settings_window, - setting_item, - file.clone(), - Button::new("error-warning", warning) - .style(ButtonStyle::Outlined) - .size(ButtonSize::Medium) - .icon(Some(IconName::Debug)) - .icon_position(IconPosition::Start) - .icon_color(Color::Error) - .tab_index(0_isize) - .tooltip(Tooltip::text(setting_item.field.type_name())) - .into_any_element(), - sub_field, - cx, - ), - }; - - let field = if padding { - field.map(apply_padding) - } else { - field - }; - - (field, field_renderer_or_warning.is_ok()) - }; - - match self { - SettingsPageItem::SectionHeader(header) => v_flex() - .w_full() - .px_8() - .gap_1p5() - .child( - Label::new(SharedString::new_static(header)) - .size(LabelSize::Small) - .color(Color::Muted) - .buffer_font(cx), - ) - .child(Divider::horizontal().color(DividerColor::BorderFaded)) - .into_any_element(), - SettingsPageItem::SettingItem(setting_item) => { - let (field_with_padding, _) = - render_setting_item_inner(setting_item, true, false, cx); - - v_flex() - .group("setting-item") - .px_8() - .child(field_with_padding) - .when(!is_last, |this| this.child(Divider::horizontal())) - .into_any_element() - } - SettingsPageItem::SubPageLink(sub_page_link) => v_flex() - .group("setting-item") - .px_8() - .child( - h_flex() - .id(sub_page_link.title.clone()) - .w_full() - .min_w_0() - .justify_between() - .map(apply_padding) - .child( - v_flex() - .w_full() - .max_w_1_2() - .child(Label::new(sub_page_link.title.clone())), - ) - .child( - Button::new( - ("sub-page".into(), sub_page_link.title.clone()), - "Configure", - ) - .icon(IconName::ChevronRight) - .tab_index(0_isize) - .icon_position(IconPosition::End) - .icon_color(Color::Muted) - .icon_size(IconSize::Small) - .style(ButtonStyle::OutlinedGhost) - .size(ButtonSize::Medium) - .on_click({ - let sub_page_link = sub_page_link.clone(); - cx.listener(move |this, _, _, cx| { - let mut section_index = item_index; - let current_page = this.current_page(); - - while !matches!( - current_page.items[section_index], - SettingsPageItem::SectionHeader(_) - ) { - section_index -= 1; - } - - let SettingsPageItem::SectionHeader(header) = - current_page.items[section_index] - else { - unreachable!( - "All items always have a section header above them" - ) - }; - - this.push_sub_page(sub_page_link.clone(), header, cx) - }) - }), - ), - ) - .when(!is_last, |this| this.child(Divider::horizontal())) - .into_any_element(), - SettingsPageItem::DynamicItem(DynamicItem { - discriminant: discriminant_setting_item, - pick_discriminant, - fields, - }) => { - let file = file.to_settings(); - let discriminant = SettingsStore::global(cx) - .get_value_from_file(file, *pick_discriminant) - .1; - - let (discriminant_element, rendered_ok) = - render_setting_item_inner(discriminant_setting_item, true, false, cx); - - let has_sub_fields = - rendered_ok && discriminant.map(|d| !fields[d].is_empty()).unwrap_or(false); - - let mut content = v_flex() - .id("dynamic-item") - .child( - div() - .group("setting-item") - .px_8() - .child(discriminant_element.when(has_sub_fields, |this| this.pb_4())), - ) - .when(!has_sub_fields && !is_last, |this| { - this.child(h_flex().px_8().child(Divider::horizontal())) - }); - - if rendered_ok { - let discriminant = - discriminant.expect("This should be Some if rendered_ok is true"); - let sub_fields = &fields[discriminant]; - let sub_field_count = sub_fields.len(); - - for (index, field) in sub_fields.iter().enumerate() { - let is_last_sub_field = index == sub_field_count - 1; - let (raw_field, _) = render_setting_item_inner(field, false, true, cx); - - content = content.child( - raw_field - .group("setting-sub-item") - .mx_8() - .p_4() - .border_t_1() - .when(is_last_sub_field, |this| this.border_b_1()) - .when(is_last_sub_field && is_last, |this| this.mb_8()) - .border_dashed() - .border_color(cx.theme().colors().border_variant) - .bg(cx.theme().colors().element_background.opacity(0.2)), - ); - } - } - - return content.into_any_element(); - } - } - } -} - -fn render_settings_item( - settings_window: &SettingsWindow, - setting_item: &SettingItem, - file: SettingsUiFile, - control: AnyElement, - sub_field: bool, - cx: &mut Context<'_, SettingsWindow>, -) -> Stateful
{ - let (found_in_file, _) = setting_item.field.file_set_in(file.clone(), cx); - let file_set_in = SettingsUiFile::from_settings(found_in_file.clone()); - - let clipboard_has_link = cx - .read_from_clipboard() - .and_then(|entry| entry.text()) - .map_or(false, |maybe_url| { - setting_item.field.json_path().is_some() - && maybe_url.strip_prefix("zed://settings/") == setting_item.field.json_path() - }); - - let (link_icon, link_icon_color) = if clipboard_has_link { - (IconName::Check, Color::Success) - } else { - (IconName::Link, Color::Muted) - }; - - h_flex() - .id(setting_item.title) - .min_w_0() - .justify_between() - .child( - v_flex() - .relative() - .w_1_2() - .child( - h_flex() - .w_full() - .gap_1() - .child(Label::new(SharedString::new_static(setting_item.title))) - .when_some( - if sub_field { - None - } else { - setting_item - .field - .reset_to_default_fn(&file, &found_in_file, cx) - }, - |this, reset_to_default| { - this.child( - IconButton::new("reset-to-default-btn", IconName::Undo) - .icon_color(Color::Muted) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Reset to Default")) - .on_click({ - move |_, _, cx| { - reset_to_default(cx); - } - }), - ) - }, - ) - .when_some( - file_set_in.filter(|file_set_in| file_set_in != &file), - |this, file_set_in| { - this.child( - Label::new(format!( - "— Modified in {}", - settings_window - .display_name(&file_set_in) - .expect("File name should exist") - )) - .color(Color::Muted) - .size(LabelSize::Small), - ) - }, - ), - ) - .child( - Label::new(SharedString::new_static(setting_item.description)) - .size(LabelSize::Small) - .color(Color::Muted), - ), - ) - .child(control) - .when(sub_page_stack().is_empty(), |this| { - // Intentionally using the description to make the icon button - // unique because some items share the same title (e.g., "Font Size") - let icon_button_id = - SharedString::new(format!("copy-link-btn-{}", setting_item.description)); - - this.child( - div() - .absolute() - .top(rems_from_px(18.)) - .map(|this| { - if sub_field { - this.visible_on_hover("setting-sub-item") - .left(rems_from_px(-8.5)) - } else { - this.visible_on_hover("setting-item") - .left(rems_from_px(-22.)) - } - }) - .child({ - IconButton::new(icon_button_id, link_icon) - .icon_color(link_icon_color) - .icon_size(IconSize::Small) - .shape(IconButtonShape::Square) - .tooltip(Tooltip::text("Copy Link")) - .when_some(setting_item.field.json_path(), |this, path| { - this.on_click(cx.listener(move |_, _, _, cx| { - let link = format!("zed://settings/{}", path); - cx.write_to_clipboard(ClipboardItem::new_string(link)); - cx.notify(); - })) - }) - }), - ) - }) -} - -struct SettingItem { - title: &'static str, - description: &'static str, - field: Box, - metadata: Option>, - files: FileMask, -} - -struct DynamicItem { - discriminant: SettingItem, - pick_discriminant: fn(&SettingsContent) -> Option, - fields: Vec>, -} - -impl PartialEq for DynamicItem { - fn eq(&self, other: &Self) -> bool { - self.discriminant == other.discriminant && self.fields == other.fields - } -} - -#[derive(PartialEq, Eq, Clone, Copy)] -struct FileMask(u8); - -impl std::fmt::Debug for FileMask { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "FileMask(")?; - let mut items = vec![]; - - if self.contains(USER) { - items.push("USER"); - } - if self.contains(PROJECT) { - items.push("LOCAL"); - } - if self.contains(SERVER) { - items.push("SERVER"); - } - - write!(f, "{})", items.join(" | ")) - } -} - -const USER: FileMask = FileMask(1 << 0); -const PROJECT: FileMask = FileMask(1 << 2); -const SERVER: FileMask = FileMask(1 << 3); - -impl std::ops::BitAnd for FileMask { - type Output = Self; - - fn bitand(self, other: Self) -> Self { - Self(self.0 & other.0) - } -} - -impl std::ops::BitOr for FileMask { - type Output = Self; - - fn bitor(self, other: Self) -> Self { - Self(self.0 | other.0) - } -} - -impl FileMask { - fn contains(&self, other: FileMask) -> bool { - self.0 & other.0 != 0 - } -} - -impl PartialEq for SettingItem { - fn eq(&self, other: &Self) -> bool { - self.title == other.title - && self.description == other.description - && (match (&self.metadata, &other.metadata) { - (None, None) => true, - (Some(m1), Some(m2)) => m1.placeholder == m2.placeholder, - _ => false, - }) - } -} - -#[derive(Clone)] -struct SubPageLink { - title: SharedString, - files: FileMask, - render: Arc< - dyn Fn(&mut SettingsWindow, &mut Window, &mut Context) -> AnyElement - + 'static - + Send - + Sync, - >, -} - -impl PartialEq for SubPageLink { - fn eq(&self, other: &Self) -> bool { - self.title == other.title - } -} - -fn all_language_names(cx: &App) -> Vec { - workspace::AppState::global(cx) - .upgrade() - .map_or(vec![], |state| { - state - .languages - .language_names() - .into_iter() - .filter(|name| name.as_ref() != "Zed Keybind Context") - .map(Into::into) - .collect() - }) -} - -#[allow(unused)] -#[derive(Clone, PartialEq, Debug)] -enum SettingsUiFile { - User, // Uses all settings. - Project((WorktreeId, Arc)), // Has a special name, and special set of settings - Server(&'static str), // Uses a special name, and the user settings -} - -impl SettingsUiFile { - fn setting_type(&self) -> &'static str { - match self { - SettingsUiFile::User => "User", - SettingsUiFile::Project(_) => "Project", - SettingsUiFile::Server(_) => "Server", - } - } - - fn is_server(&self) -> bool { - matches!(self, SettingsUiFile::Server(_)) - } - - fn worktree_id(&self) -> Option { - match self { - SettingsUiFile::User => None, - SettingsUiFile::Project((worktree_id, _)) => Some(*worktree_id), - SettingsUiFile::Server(_) => None, - } - } - - fn from_settings(file: settings::SettingsFile) -> Option { - Some(match file { - settings::SettingsFile::User => SettingsUiFile::User, - settings::SettingsFile::Project(location) => SettingsUiFile::Project(location), - settings::SettingsFile::Server => SettingsUiFile::Server("todo: server name"), - settings::SettingsFile::Default => return None, - settings::SettingsFile::Global => return None, - }) - } - - fn to_settings(&self) -> settings::SettingsFile { - match self { - SettingsUiFile::User => settings::SettingsFile::User, - SettingsUiFile::Project(location) => settings::SettingsFile::Project(location.clone()), - SettingsUiFile::Server(_) => settings::SettingsFile::Server, - } - } - - fn mask(&self) -> FileMask { - match self { - SettingsUiFile::User => USER, - SettingsUiFile::Project(_) => PROJECT, - SettingsUiFile::Server(_) => SERVER, - } - } -} - -impl SettingsWindow { - fn new( - original_window: Option>, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let font_family_cache = theme::FontFamilyCache::global(cx); - - cx.spawn(async move |this, cx| { - font_family_cache.prefetch(cx).await; - this.update(cx, |_, cx| { - cx.notify(); - }) - }) - .detach(); - - let current_file = SettingsUiFile::User; - let search_bar = cx.new(|cx| { - let mut editor = Editor::single_line(window, cx); - editor.set_placeholder_text("Search settings…", window, cx); - editor - }); - - cx.subscribe(&search_bar, |this, _, event: &EditorEvent, cx| { - let EditorEvent::Edited { transaction_id: _ } = event else { - return; - }; - - this.update_matches(cx); - }) - .detach(); - - cx.observe_global_in::(window, move |this, window, cx| { - this.fetch_files(window, cx); - cx.notify(); - }) - .detach(); - - cx.on_window_closed(|cx| { - if let Some(existing_window) = cx - .windows() - .into_iter() - .find_map(|window| window.downcast::()) - && cx.windows().len() == 1 - { - cx.update_window(*existing_window, |_, window, _| { - window.remove_window(); - }) - .ok(); - - telemetry::event!("Settings Closed") - } - }) - .detach(); - - if let Some(app_state) = AppState::global(cx).upgrade() { - for project in app_state - .workspace_store - .read(cx) - .workspaces() - .iter() - .filter_map(|space| { - space - .read(cx) - .ok() - .map(|workspace| workspace.project().clone()) - }) - .collect::>() - { - cx.observe_release_in(&project, window, |this, _, window, cx| { - this.fetch_files(window, cx) - }) - .detach(); - cx.subscribe_in(&project, window, Self::handle_project_event) - .detach(); - } - - for workspace in app_state - .workspace_store - .read(cx) - .workspaces() - .iter() - .filter_map(|space| space.entity(cx).ok()) - { - cx.observe_release_in(&workspace, window, |this, _, window, cx| { - this.fetch_files(window, cx) - }) - .detach(); - } - } else { - log::error!("App state doesn't exist when creating a new settings window"); - } - - let this_weak = cx.weak_entity(); - cx.observe_new::({ - let this_weak = this_weak.clone(); - - move |_, window, cx| { - let project = cx.entity(); - let Some(window) = window else { - return; - }; - - this_weak - .update(cx, |this, cx| { - this.fetch_files(window, cx); - cx.observe_release_in(&project, window, |_, _, window, cx| { - cx.defer_in(window, |this, window, cx| this.fetch_files(window, cx)); - }) - .detach(); - - cx.subscribe_in(&project, window, Self::handle_project_event) - .detach(); - }) - .ok(); - } - }) - .detach(); - - cx.observe_new::(move |_, window, cx| { - let workspace = cx.entity(); - let Some(window) = window else { - return; - }; - - this_weak - .update(cx, |this, cx| { - this.fetch_files(window, cx); - cx.observe_release_in(&workspace, window, |this, _, window, cx| { - this.fetch_files(window, cx) - }) - .detach(); - }) - .ok(); - }) - .detach(); - - let title_bar = if !cfg!(target_os = "macos") { - Some(cx.new(|cx| PlatformTitleBar::new("settings-title-bar", cx))) - } else { - None - }; - - // high overdraw value so the list scrollbar len doesn't change too much - let list_state = gpui::ListState::new(0, gpui::ListAlignment::Top, px(0.0)).measure_all(); - list_state.set_scroll_handler(|_, _, _| {}); - - let mut this = Self { - title_bar, - original_window, - - worktree_root_dirs: HashMap::default(), - files: vec![], - - current_file: current_file, - pages: vec![], - navbar_entries: vec![], - navbar_entry: 0, - navbar_scroll_handle: UniformListScrollHandle::default(), - search_bar, - search_task: None, - filter_table: vec![], - has_query: false, - content_handles: vec![], - sub_page_scroll_handle: ScrollHandle::new(), - focus_handle: cx.focus_handle(), - navbar_focus_handle: NonFocusableHandle::new( - NAVBAR_CONTAINER_TAB_INDEX, - false, - window, - cx, - ), - navbar_focus_subscriptions: vec![], - content_focus_handle: NonFocusableHandle::new( - CONTENT_CONTAINER_TAB_INDEX, - false, - window, - cx, - ), - files_focus_handle: cx - .focus_handle() - .tab_index(HEADER_CONTAINER_TAB_INDEX) - .tab_stop(false), - search_index: None, - shown_errors: HashSet::default(), - list_state, - }; - - this.fetch_files(window, cx); - this.build_ui(window, cx); - this.build_search_index(); - - this.search_bar.update(cx, |editor, cx| { - editor.focus_handle(cx).focus(window); - }); - - this - } - - fn handle_project_event( - &mut self, - _: &Entity, - event: &project::Event, - window: &mut Window, - cx: &mut Context, - ) { - match event { - project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded(_) => { - cx.defer_in(window, |this, window, cx| { - this.fetch_files(window, cx); - }); - } - _ => {} - } - } - - fn toggle_navbar_entry(&mut self, nav_entry_index: usize) { - // We can only toggle root entries - if !self.navbar_entries[nav_entry_index].is_root { - return; - } - - let expanded = &mut self.navbar_entries[nav_entry_index].expanded; - *expanded = !*expanded; - self.navbar_entry = nav_entry_index; - self.reset_list_state(); - } - - fn build_navbar(&mut self, cx: &App) { - let mut navbar_entries = Vec::new(); - - for (page_index, page) in self.pages.iter().enumerate() { - navbar_entries.push(NavBarEntry { - title: page.title, - is_root: true, - expanded: false, - page_index, - item_index: None, - focus_handle: cx.focus_handle().tab_index(0).tab_stop(true), - }); - - for (item_index, item) in page.items.iter().enumerate() { - let SettingsPageItem::SectionHeader(title) = item else { - continue; - }; - navbar_entries.push(NavBarEntry { - title, - is_root: false, - expanded: false, - page_index, - item_index: Some(item_index), - focus_handle: cx.focus_handle().tab_index(0).tab_stop(true), - }); - } - } - - self.navbar_entries = navbar_entries; - } - - fn setup_navbar_focus_subscriptions( - &mut self, - window: &mut Window, - cx: &mut Context, - ) { - let mut focus_subscriptions = Vec::new(); - - for entry_index in 0..self.navbar_entries.len() { - let focus_handle = self.navbar_entries[entry_index].focus_handle.clone(); - - let subscription = cx.on_focus( - &focus_handle, - window, - move |this: &mut SettingsWindow, - window: &mut Window, - cx: &mut Context| { - this.open_and_scroll_to_navbar_entry(entry_index, None, false, window, cx); - }, - ); - focus_subscriptions.push(subscription); - } - self.navbar_focus_subscriptions = focus_subscriptions; - } - - fn visible_navbar_entries(&self) -> impl Iterator { - let mut index = 0; - let entries = &self.navbar_entries; - let search_matches = &self.filter_table; - let has_query = self.has_query; - std::iter::from_fn(move || { - while index < entries.len() { - let entry = &entries[index]; - let included_in_search = if let Some(item_index) = entry.item_index { - search_matches[entry.page_index][item_index] - } else { - search_matches[entry.page_index].iter().any(|b| *b) - || search_matches[entry.page_index].is_empty() - }; - if included_in_search { - break; - } - index += 1; - } - if index >= self.navbar_entries.len() { - return None; - } - let entry = &entries[index]; - let entry_index = index; - - index += 1; - if entry.is_root && !entry.expanded && !has_query { - while index < entries.len() { - if entries[index].is_root { - break; - } - index += 1; - } - } - - return Some((entry_index, entry)); - }) - } - - fn filter_matches_to_file(&mut self) { - let current_file = self.current_file.mask(); - for (page, page_filter) in std::iter::zip(&self.pages, &mut self.filter_table) { - let mut header_index = 0; - let mut any_found_since_last_header = true; - - for (index, item) in page.items.iter().enumerate() { - match item { - SettingsPageItem::SectionHeader(_) => { - if !any_found_since_last_header { - page_filter[header_index] = false; - } - header_index = index; - any_found_since_last_header = false; - } - SettingsPageItem::SettingItem(SettingItem { files, .. }) - | SettingsPageItem::SubPageLink(SubPageLink { files, .. }) - | SettingsPageItem::DynamicItem(DynamicItem { - discriminant: SettingItem { files, .. }, - .. - }) => { - if !files.contains(current_file) { - page_filter[index] = false; - } else { - any_found_since_last_header = true; - } - } - } - } - if let Some(last_header) = page_filter.get_mut(header_index) - && !any_found_since_last_header - { - *last_header = false; - } - } - } - - fn update_matches(&mut self, cx: &mut Context) { - self.search_task.take(); - let mut query = self.search_bar.read(cx).text(cx); - if query.is_empty() || self.search_index.is_none() { - for page in &mut self.filter_table { - page.fill(true); - } - self.has_query = false; - self.filter_matches_to_file(); - self.reset_list_state(); - cx.notify(); - return; - } - - let is_json_link_query; - if query.starts_with("#") { - query.remove(0); - is_json_link_query = true; - } else { - is_json_link_query = false; - } - - let search_index = self.search_index.as_ref().unwrap().clone(); - - fn update_matches_inner( - this: &mut SettingsWindow, - search_index: &SearchIndex, - match_indices: impl Iterator, - cx: &mut Context, - ) { - for page in &mut this.filter_table { - page.fill(false); - } - - for match_index in match_indices { - let SearchKeyLUTEntry { - page_index, - header_index, - item_index, - .. - } = search_index.key_lut[match_index]; - let page = &mut this.filter_table[page_index]; - page[header_index] = true; - page[item_index] = true; - } - this.has_query = true; - this.filter_matches_to_file(); - this.open_first_nav_page(); - this.reset_list_state(); - cx.notify(); - } - - self.search_task = Some(cx.spawn(async move |this, cx| { - if is_json_link_query { - let mut indices = vec![]; - for (index, SearchKeyLUTEntry { json_path, .. }) in - search_index.key_lut.iter().enumerate() - { - let Some(json_path) = json_path else { - continue; - }; - - if let Some(post) = query.strip_prefix(json_path) - && (post.is_empty() || post.starts_with('.')) - { - indices.push(index); - } - } - if !indices.is_empty() { - this.update(cx, |this, cx| { - update_matches_inner(this, search_index.as_ref(), indices.into_iter(), cx); - }) - .ok(); - return; - } - } - let bm25_task = cx.background_spawn({ - let search_index = search_index.clone(); - let max_results = search_index.key_lut.len(); - let query = query.clone(); - async move { search_index.bm25_engine.search(&query, max_results) } - }); - let cancel_flag = std::sync::atomic::AtomicBool::new(false); - let fuzzy_search_task = fuzzy::match_strings( - search_index.fuzzy_match_candidates.as_slice(), - &query, - false, - true, - search_index.fuzzy_match_candidates.len(), - &cancel_flag, - cx.background_executor().clone(), - ); - - let fuzzy_matches = fuzzy_search_task.await; - - _ = this - .update(cx, |this, cx| { - // For tuning the score threshold - // for fuzzy_match in &fuzzy_matches { - // let SearchItemKey { - // page_index, - // header_index, - // item_index, - // } = search_index.key_lut[fuzzy_match.candidate_id]; - // let SettingsPageItem::SectionHeader(header) = - // this.pages[page_index].items[header_index] - // else { - // continue; - // }; - // let SettingsPageItem::SettingItem(SettingItem { - // title, description, .. - // }) = this.pages[page_index].items[item_index] - // else { - // continue; - // }; - // let score = fuzzy_match.score; - // eprint!("# {header} :: QUERY = {query} :: SCORE = {score}\n{title}\n{description}\n\n"); - // } - update_matches_inner( - this, - search_index.as_ref(), - fuzzy_matches - .into_iter() - // MAGIC NUMBER: Was found to have right balance between not too many weird matches, but also - // flexible enough to catch misspellings and <4 letter queries - // More flexible is good for us here because fuzzy matches will only be used for things that don't - // match using bm25 - .take_while(|fuzzy_match| fuzzy_match.score >= 0.3) - .map(|fuzzy_match| fuzzy_match.candidate_id), - cx, - ); - }) - .ok(); - - let bm25_matches = bm25_task.await; - - _ = this - .update(cx, |this, cx| { - if bm25_matches.is_empty() { - return; - } - update_matches_inner( - this, - search_index.as_ref(), - bm25_matches - .into_iter() - .map(|bm25_match| bm25_match.document.id), - cx, - ); - }) - .ok(); - - cx.background_executor().timer(Duration::from_secs(1)).await; - telemetry::event!("Settings Searched", query = query) - })); - } - - fn build_filter_table(&mut self) { - self.filter_table = self - .pages - .iter() - .map(|page| vec![true; page.items.len()]) - .collect::>(); - } - - fn build_search_index(&mut self) { - let mut key_lut: Vec = vec![]; - let mut documents = Vec::default(); - let mut fuzzy_match_candidates = Vec::default(); - - fn push_candidates( - fuzzy_match_candidates: &mut Vec, - key_index: usize, - input: &str, - ) { - for word in input.split_ascii_whitespace() { - fuzzy_match_candidates.push(StringMatchCandidate::new(key_index, word)); - } - } - - // PERF: We are currently searching all items even in project files - // where many settings are filtered out, using the logic in filter_matches_to_file - // we could only search relevant items based on the current file - for (page_index, page) in self.pages.iter().enumerate() { - let mut header_index = 0; - let mut header_str = ""; - for (item_index, item) in page.items.iter().enumerate() { - let key_index = key_lut.len(); - let mut json_path = None; - match item { - SettingsPageItem::DynamicItem(DynamicItem { - discriminant: item, .. - }) - | SettingsPageItem::SettingItem(item) => { - json_path = item - .field - .json_path() - .map(|path| path.trim_end_matches('$')); - documents.push(bm25::Document { - id: key_index, - contents: [page.title, header_str, item.title, item.description] - .join("\n"), - }); - push_candidates(&mut fuzzy_match_candidates, key_index, item.title); - push_candidates(&mut fuzzy_match_candidates, key_index, item.description); - } - SettingsPageItem::SectionHeader(header) => { - documents.push(bm25::Document { - id: key_index, - contents: header.to_string(), - }); - push_candidates(&mut fuzzy_match_candidates, key_index, header); - header_index = item_index; - header_str = *header; - } - SettingsPageItem::SubPageLink(sub_page_link) => { - documents.push(bm25::Document { - id: key_index, - contents: [page.title, header_str, sub_page_link.title.as_ref()] - .join("\n"), - }); - push_candidates( - &mut fuzzy_match_candidates, - key_index, - sub_page_link.title.as_ref(), - ); - } - } - push_candidates(&mut fuzzy_match_candidates, key_index, page.title); - push_candidates(&mut fuzzy_match_candidates, key_index, header_str); - - key_lut.push(SearchKeyLUTEntry { - page_index, - header_index, - item_index, - json_path, - }); - } - } - let engine = - bm25::SearchEngineBuilder::with_documents(bm25::Language::English, documents).build(); - self.search_index = Some(Arc::new(SearchIndex { - bm25_engine: engine, - key_lut, - fuzzy_match_candidates, - })); - } - - fn build_content_handles(&mut self, window: &mut Window, cx: &mut Context) { - self.content_handles = self - .pages - .iter() - .map(|page| { - std::iter::repeat_with(|| NonFocusableHandle::new(0, false, window, cx)) - .take(page.items.len()) - .collect() - }) - .collect::>(); - } - - fn reset_list_state(&mut self) { - // plus one for the title - let mut visible_items_count = self.visible_page_items().count(); - - if visible_items_count > 0 { - // show page title if page is non empty - visible_items_count += 1; - } - - self.list_state.reset(visible_items_count); - } - - fn build_ui(&mut self, window: &mut Window, cx: &mut Context) { - if self.pages.is_empty() { - self.pages = page_data::settings_data(cx); - self.build_navbar(cx); - self.setup_navbar_focus_subscriptions(window, cx); - self.build_content_handles(window, cx); - } - sub_page_stack_mut().clear(); - // PERF: doesn't have to be rebuilt, can just be filled with true. pages is constant once it is built - self.build_filter_table(); - self.reset_list_state(); - self.update_matches(cx); - - cx.notify(); - } - - #[track_caller] - fn fetch_files(&mut self, window: &mut Window, cx: &mut Context) { - self.worktree_root_dirs.clear(); - let prev_files = self.files.clone(); - let settings_store = cx.global::(); - let mut ui_files = vec![]; - let mut all_files = settings_store.get_all_files(); - if !all_files.contains(&settings::SettingsFile::User) { - all_files.push(settings::SettingsFile::User); - } - for file in all_files { - let Some(settings_ui_file) = SettingsUiFile::from_settings(file) else { - continue; - }; - if settings_ui_file.is_server() { - continue; - } - - if let Some(worktree_id) = settings_ui_file.worktree_id() { - let directory_name = all_projects(cx) - .find_map(|project| project.read(cx).worktree_for_id(worktree_id, cx)) - .and_then(|worktree| worktree.read(cx).root_dir()) - .and_then(|root_dir| { - root_dir - .file_name() - .map(|os_string| os_string.to_string_lossy().to_string()) - }); - - let Some(directory_name) = directory_name else { - log::error!( - "No directory name found for settings file at worktree ID: {}", - worktree_id - ); - continue; - }; - - self.worktree_root_dirs.insert(worktree_id, directory_name); - } - - let focus_handle = prev_files - .iter() - .find_map(|(prev_file, handle)| { - (prev_file == &settings_ui_file).then(|| handle.clone()) - }) - .unwrap_or_else(|| cx.focus_handle().tab_index(0).tab_stop(true)); - ui_files.push((settings_ui_file, focus_handle)); - } - - ui_files.reverse(); - - let mut missing_worktrees = Vec::new(); - - for worktree in all_projects(cx) - .flat_map(|project| project.read(cx).visible_worktrees(cx)) - .filter(|tree| !self.worktree_root_dirs.contains_key(&tree.read(cx).id())) - { - let worktree = worktree.read(cx); - let worktree_id = worktree.id(); - let Some(directory_name) = worktree.root_dir().and_then(|file| { - file.file_name() - .map(|os_string| os_string.to_string_lossy().to_string()) - }) else { - continue; - }; - - missing_worktrees.push((worktree_id, directory_name.clone())); - let path = RelPath::empty().to_owned().into_arc(); - - let settings_ui_file = SettingsUiFile::Project((worktree_id, path)); - - let focus_handle = prev_files - .iter() - .find_map(|(prev_file, handle)| { - (prev_file == &settings_ui_file).then(|| handle.clone()) - }) - .unwrap_or_else(|| cx.focus_handle().tab_index(0).tab_stop(true)); - - ui_files.push((settings_ui_file, focus_handle)); - } - - self.worktree_root_dirs.extend(missing_worktrees); - - self.files = ui_files; - let current_file_still_exists = self - .files - .iter() - .any(|(file, _)| file == &self.current_file); - if !current_file_still_exists { - self.change_file(0, window, cx); - } - } - - fn open_navbar_entry_page(&mut self, navbar_entry: usize) { - if !self.is_nav_entry_visible(navbar_entry) { - self.open_first_nav_page(); - } - - let is_new_page = self.navbar_entries[self.navbar_entry].page_index - != self.navbar_entries[navbar_entry].page_index; - self.navbar_entry = navbar_entry; - - // We only need to reset visible items when updating matches - // and selecting a new page - if is_new_page { - self.reset_list_state(); - } - - sub_page_stack_mut().clear(); - } - - fn open_first_nav_page(&mut self) { - let Some(first_navbar_entry_index) = self.visible_navbar_entries().next().map(|e| e.0) - else { - return; - }; - self.open_navbar_entry_page(first_navbar_entry_index); - } - - fn change_file(&mut self, ix: usize, window: &mut Window, cx: &mut Context) { - if ix >= self.files.len() { - self.current_file = SettingsUiFile::User; - self.build_ui(window, cx); - return; - } - - if self.files[ix].0 == self.current_file { - return; - } - self.current_file = self.files[ix].0.clone(); - - if let SettingsUiFile::Project((_, _)) = &self.current_file { - telemetry::event!("Setting Project Clicked"); - } - - self.build_ui(window, cx); - - if self - .visible_navbar_entries() - .any(|(index, _)| index == self.navbar_entry) - { - self.open_and_scroll_to_navbar_entry(self.navbar_entry, None, true, window, cx); - } else { - self.open_first_nav_page(); - }; - } - - fn render_files_header( - &self, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - static OVERFLOW_LIMIT: usize = 1; - - let file_button = - |ix, file: &SettingsUiFile, focus_handle, cx: &mut Context| { - Button::new( - ix, - self.display_name(&file) - .expect("Files should always have a name"), - ) - .toggle_state(file == &self.current_file) - .selected_style(ButtonStyle::Tinted(ui::TintColor::Accent)) - .track_focus(focus_handle) - .on_click(cx.listener({ - let focus_handle = focus_handle.clone(); - move |this, _: &gpui::ClickEvent, window, cx| { - this.change_file(ix, window, cx); - focus_handle.focus(window); - } - })) - }; - - let this = cx.entity(); - - let selected_file_ix = self - .files - .iter() - .enumerate() - .skip(OVERFLOW_LIMIT) - .find_map(|(ix, (file, _))| { - if file == &self.current_file { - Some(ix) - } else { - None - } - }) - .unwrap_or(OVERFLOW_LIMIT); - let edit_in_json_id = SharedString::new(format!("edit-in-json-{}", selected_file_ix)); - - h_flex() - .w_full() - .gap_1() - .justify_between() - .track_focus(&self.files_focus_handle) - .tab_group() - .tab_index(HEADER_GROUP_TAB_INDEX) - .child( - h_flex() - .gap_1() - .children( - self.files.iter().enumerate().take(OVERFLOW_LIMIT).map( - |(ix, (file, focus_handle))| file_button(ix, file, focus_handle, cx), - ), - ) - .when(self.files.len() > OVERFLOW_LIMIT, |div| { - let (file, focus_handle) = &self.files[selected_file_ix]; - - div.child(file_button(selected_file_ix, file, focus_handle, cx)) - .when(self.files.len() > OVERFLOW_LIMIT + 1, |div| { - div.child( - DropdownMenu::new( - "more-files", - format!("+{}", self.files.len() - (OVERFLOW_LIMIT + 1)), - ContextMenu::build(window, cx, move |mut menu, _, _| { - for (mut ix, (file, focus_handle)) in self - .files - .iter() - .enumerate() - .skip(OVERFLOW_LIMIT + 1) - { - let (display_name, focus_handle) = - if selected_file_ix == ix { - ix = OVERFLOW_LIMIT; - ( - self.display_name(&self.files[ix].0), - self.files[ix].1.clone(), - ) - } else { - ( - self.display_name(&file), - focus_handle.clone(), - ) - }; - - menu = menu.entry( - display_name - .expect("Files should always have a name"), - None, - { - let this = this.clone(); - move |window, cx| { - this.update(cx, |this, cx| { - this.change_file(ix, window, cx); - }); - focus_handle.focus(window); - } - }, - ); - } - - menu - }), - ) - .style(DropdownStyle::Subtle) - .trigger_tooltip(Tooltip::text("View Other Projects")) - .trigger_icon(IconName::ChevronDown) - .attach(gpui::Corner::BottomLeft) - .offset(gpui::Point { - x: px(0.0), - y: px(2.0), - }) - .tab_index(0), - ) - }) - }), - ) - .child( - Button::new(edit_in_json_id, "Edit in settings.json") - .tab_index(0_isize) - .style(ButtonStyle::OutlinedGhost) - .tooltip(Tooltip::for_action_title_in( - "Edit in settings.json", - &OpenCurrentFile, - &self.focus_handle, - )) - .on_click(cx.listener(|this, _, window, cx| { - this.open_current_settings_file(window, cx); - })), - ) - } - - pub(crate) fn display_name(&self, file: &SettingsUiFile) -> Option { - match file { - SettingsUiFile::User => Some("User".to_string()), - SettingsUiFile::Project((worktree_id, path)) => self - .worktree_root_dirs - .get(&worktree_id) - .map(|directory_name| { - let path_style = PathStyle::local(); - if path.is_empty() { - directory_name.clone() - } else { - format!( - "{}{}{}", - directory_name, - path_style.primary_separator(), - path.display(path_style) - ) - } - }), - SettingsUiFile::Server(file) => Some(file.to_string()), - } - } - - // TODO: - // Reconsider this after preview launch - // fn file_location_str(&self) -> String { - // match &self.current_file { - // SettingsUiFile::User => "settings.json".to_string(), - // SettingsUiFile::Project((worktree_id, path)) => self - // .worktree_root_dirs - // .get(&worktree_id) - // .map(|directory_name| { - // let path_style = PathStyle::local(); - // let file_path = path.join(paths::local_settings_file_relative_path()); - // format!( - // "{}{}{}", - // directory_name, - // path_style.separator(), - // file_path.display(path_style) - // ) - // }) - // .expect("Current file should always be present in root dir map"), - // SettingsUiFile::Server(file) => file.to_string(), - // } - // } - - fn render_search(&self, _window: &mut Window, cx: &mut App) -> Div { - h_flex() - .py_1() - .px_1p5() - .mb_3() - .gap_1p5() - .rounded_sm() - .bg(cx.theme().colors().editor_background) - .border_1() - .border_color(cx.theme().colors().border) - .child(Icon::new(IconName::MagnifyingGlass).color(Color::Muted)) - .child(self.search_bar.clone()) - } - - fn render_nav( - &self, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let visible_count = self.visible_navbar_entries().count(); - - let focus_keybind_label = if self - .navbar_focus_handle - .read(cx) - .handle - .contains_focused(window, cx) - || self - .visible_navbar_entries() - .any(|(_, entry)| entry.focus_handle.is_focused(window)) - { - "Focus Content" - } else { - "Focus Navbar" - }; - - let mut key_context = KeyContext::new_with_defaults(); - key_context.add("NavigationMenu"); - key_context.add("menu"); - if self.search_bar.focus_handle(cx).is_focused(window) { - key_context.add("search"); - } - - v_flex() - .key_context(key_context) - .on_action(cx.listener(|this, _: &CollapseNavEntry, window, cx| { - let Some(focused_entry) = this.focused_nav_entry(window, cx) else { - return; - }; - let focused_entry_parent = this.root_entry_containing(focused_entry); - if this.navbar_entries[focused_entry_parent].expanded { - this.toggle_navbar_entry(focused_entry_parent); - window.focus(&this.navbar_entries[focused_entry_parent].focus_handle); - } - cx.notify(); - })) - .on_action(cx.listener(|this, _: &ExpandNavEntry, window, cx| { - let Some(focused_entry) = this.focused_nav_entry(window, cx) else { - return; - }; - if !this.navbar_entries[focused_entry].is_root { - return; - } - if !this.navbar_entries[focused_entry].expanded { - this.toggle_navbar_entry(focused_entry); - } - cx.notify(); - })) - .on_action( - cx.listener(|this, _: &FocusPreviousRootNavEntry, window, cx| { - let entry_index = this - .focused_nav_entry(window, cx) - .unwrap_or(this.navbar_entry); - let mut root_index = None; - for (index, entry) in this.visible_navbar_entries() { - if index >= entry_index { - break; - } - if entry.is_root { - root_index = Some(index); - } - } - let Some(previous_root_index) = root_index else { - return; - }; - this.focus_and_scroll_to_nav_entry(previous_root_index, window, cx); - }), - ) - .on_action(cx.listener(|this, _: &FocusNextRootNavEntry, window, cx| { - let entry_index = this - .focused_nav_entry(window, cx) - .unwrap_or(this.navbar_entry); - let mut root_index = None; - for (index, entry) in this.visible_navbar_entries() { - if index <= entry_index { - continue; - } - if entry.is_root { - root_index = Some(index); - break; - } - } - let Some(next_root_index) = root_index else { - return; - }; - this.focus_and_scroll_to_nav_entry(next_root_index, window, cx); - })) - .on_action(cx.listener(|this, _: &FocusFirstNavEntry, window, cx| { - if let Some((first_entry_index, _)) = this.visible_navbar_entries().next() { - this.focus_and_scroll_to_nav_entry(first_entry_index, window, cx); - } - })) - .on_action(cx.listener(|this, _: &FocusLastNavEntry, window, cx| { - if let Some((last_entry_index, _)) = this.visible_navbar_entries().last() { - this.focus_and_scroll_to_nav_entry(last_entry_index, window, cx); - } - })) - .on_action(cx.listener(|this, _: &FocusNextNavEntry, window, cx| { - let entry_index = this - .focused_nav_entry(window, cx) - .unwrap_or(this.navbar_entry); - let mut next_index = None; - for (index, _) in this.visible_navbar_entries() { - if index > entry_index { - next_index = Some(index); - break; - } - } - let Some(next_entry_index) = next_index else { - return; - }; - this.open_and_scroll_to_navbar_entry( - next_entry_index, - Some(gpui::ScrollStrategy::Bottom), - false, - window, - cx, - ); - })) - .on_action(cx.listener(|this, _: &FocusPreviousNavEntry, window, cx| { - let entry_index = this - .focused_nav_entry(window, cx) - .unwrap_or(this.navbar_entry); - let mut prev_index = None; - for (index, _) in this.visible_navbar_entries() { - if index >= entry_index { - break; - } - prev_index = Some(index); - } - let Some(prev_entry_index) = prev_index else { - return; - }; - this.open_and_scroll_to_navbar_entry( - prev_entry_index, - Some(gpui::ScrollStrategy::Top), - false, - window, - cx, - ); - })) - .w_56() - .h_full() - .p_2p5() - .when(cfg!(target_os = "macos"), |this| this.pt_10()) - .flex_none() - .border_r_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().panel_background) - .child(self.render_search(window, cx)) - .child( - v_flex() - .flex_1() - .overflow_hidden() - .track_focus(&self.navbar_focus_handle.focus_handle(cx)) - .tab_group() - .tab_index(NAVBAR_GROUP_TAB_INDEX) - .child( - uniform_list( - "settings-ui-nav-bar", - visible_count + 1, - cx.processor(move |this, range: Range, _, cx| { - this.visible_navbar_entries() - .skip(range.start.saturating_sub(1)) - .take(range.len()) - .map(|(entry_index, entry)| { - TreeViewItem::new( - ("settings-ui-navbar-entry", entry_index), - entry.title, - ) - .track_focus(&entry.focus_handle) - .root_item(entry.is_root) - .toggle_state(this.is_navbar_entry_selected(entry_index)) - .when(entry.is_root, |item| { - item.expanded(entry.expanded || this.has_query) - .on_toggle(cx.listener( - move |this, _, window, cx| { - this.toggle_navbar_entry(entry_index); - window.focus( - &this.navbar_entries[entry_index] - .focus_handle, - ); - cx.notify(); - }, - )) - }) - .on_click({ - let category = this.pages[entry.page_index].title; - let subcategory = - (!entry.is_root).then_some(entry.title); - - cx.listener(move |this, _, window, cx| { - telemetry::event!( - "Settings Navigation Clicked", - category = category, - subcategory = subcategory - ); - - this.open_and_scroll_to_navbar_entry( - entry_index, - None, - true, - window, - cx, - ); - }) - }) - }) - .collect() - }), - ) - .size_full() - .track_scroll(&self.navbar_scroll_handle), - ) - .vertical_scrollbar_for(&self.navbar_scroll_handle, window, cx), - ) - .child( - h_flex() - .w_full() - .h_8() - .p_2() - .pb_0p5() - .flex_shrink_0() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .child( - KeybindingHint::new( - KeyBinding::for_action_in( - &ToggleFocusNav, - &self.navbar_focus_handle.focus_handle(cx), - cx, - ), - cx.theme().colors().surface_background.opacity(0.5), - ) - .suffix(focus_keybind_label), - ), - ) - } - - fn open_and_scroll_to_navbar_entry( - &mut self, - navbar_entry_index: usize, - scroll_strategy: Option, - focus_content: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.open_navbar_entry_page(navbar_entry_index); - cx.notify(); - - let mut handle_to_focus = None; - - if self.navbar_entries[navbar_entry_index].is_root - || !self.is_nav_entry_visible(navbar_entry_index) - { - self.sub_page_scroll_handle - .set_offset(point(px(0.), px(0.))); - if focus_content { - let Some(first_item_index) = - self.visible_page_items().next().map(|(index, _)| index) - else { - return; - }; - handle_to_focus = Some(self.focus_handle_for_content_element(first_item_index, cx)); - } else if !self.is_nav_entry_visible(navbar_entry_index) { - let Some(first_visible_nav_entry_index) = - self.visible_navbar_entries().next().map(|(index, _)| index) - else { - return; - }; - self.focus_and_scroll_to_nav_entry(first_visible_nav_entry_index, window, cx); - } else { - handle_to_focus = - Some(self.navbar_entries[navbar_entry_index].focus_handle.clone()); - } - } else { - let entry_item_index = self.navbar_entries[navbar_entry_index] - .item_index - .expect("Non-root items should have an item index"); - self.scroll_to_content_item(entry_item_index, window, cx); - if focus_content { - handle_to_focus = Some(self.focus_handle_for_content_element(entry_item_index, cx)); - } else { - handle_to_focus = - Some(self.navbar_entries[navbar_entry_index].focus_handle.clone()); - } - } - - if let Some(scroll_strategy) = scroll_strategy - && let Some(logical_entry_index) = self - .visible_navbar_entries() - .into_iter() - .position(|(index, _)| index == navbar_entry_index) - { - self.navbar_scroll_handle - .scroll_to_item(logical_entry_index + 1, scroll_strategy); - } - - // Page scroll handle updates the active item index - // in it's next paint call after using scroll_handle.scroll_to_top_of_item - // The call after that updates the offset of the scroll handle. So to - // ensure the scroll handle doesn't lag behind we need to render three frames - // back to back. - cx.on_next_frame(window, move |_, window, cx| { - if let Some(handle) = handle_to_focus.as_ref() { - window.focus(handle); - } - - cx.on_next_frame(window, |_, _, cx| { - cx.notify(); - }); - cx.notify(); - }); - cx.notify(); - } - - fn scroll_to_content_item( - &self, - content_item_index: usize, - _window: &mut Window, - cx: &mut Context, - ) { - let index = self - .visible_page_items() - .position(|(index, _)| index == content_item_index) - .unwrap_or(0); - if index == 0 { - self.sub_page_scroll_handle - .set_offset(point(px(0.), px(0.))); - self.list_state.scroll_to(gpui::ListOffset { - item_ix: 0, - offset_in_item: px(0.), - }); - return; - } - self.list_state.scroll_to(gpui::ListOffset { - item_ix: index + 1, - offset_in_item: px(0.), - }); - cx.notify(); - } - - fn is_nav_entry_visible(&self, nav_entry_index: usize) -> bool { - self.visible_navbar_entries() - .any(|(index, _)| index == nav_entry_index) - } - - fn focus_and_scroll_to_first_visible_nav_entry( - &self, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(nav_entry_index) = self.visible_navbar_entries().next().map(|(index, _)| index) - { - self.focus_and_scroll_to_nav_entry(nav_entry_index, window, cx); - } - } - - fn focus_and_scroll_to_nav_entry( - &self, - nav_entry_index: usize, - window: &mut Window, - cx: &mut Context, - ) { - let Some(position) = self - .visible_navbar_entries() - .position(|(index, _)| index == nav_entry_index) - else { - return; - }; - self.navbar_scroll_handle - .scroll_to_item(position, gpui::ScrollStrategy::Top); - window.focus(&self.navbar_entries[nav_entry_index].focus_handle); - cx.notify(); - } - - fn visible_page_items(&self) -> impl Iterator { - let page_idx = self.current_page_index(); - - self.current_page() - .items - .iter() - .enumerate() - .filter_map(move |(item_index, item)| { - self.filter_table[page_idx][item_index].then_some((item_index, item)) - }) - } - - fn render_sub_page_breadcrumbs(&self) -> impl IntoElement { - let mut items = vec![]; - items.push(self.current_page().title.into()); - items.extend( - sub_page_stack() - .iter() - .flat_map(|page| [page.section_header.into(), page.link.title.clone()]), - ); - - let last = items.pop().unwrap(); - h_flex() - .gap_1() - .children( - items - .into_iter() - .flat_map(|item| [item, "/".into()]) - .map(|item| Label::new(item).color(Color::Muted)), - ) - .child(Label::new(last)) - } - - fn render_empty_state(&self, search_query: SharedString) -> impl IntoElement { - v_flex() - .size_full() - .items_center() - .justify_center() - .gap_1() - .child(Label::new("No Results")) - .child( - Label::new(search_query) - .size(LabelSize::Small) - .color(Color::Muted), - ) - } - - fn render_page_items( - &mut self, - page_index: usize, - _window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let mut page_content = v_flex().id("settings-ui-page").size_full(); - - let has_active_search = !self.search_bar.read(cx).is_empty(cx); - let has_no_results = self.visible_page_items().next().is_none() && has_active_search; - - if has_no_results { - let search_query = self.search_bar.read(cx).text(cx); - page_content = page_content.child( - self.render_empty_state(format!("No settings match \"{}\"", search_query).into()), - ) - } else { - let last_non_header_index = self - .visible_page_items() - .filter_map(|(index, item)| { - (!matches!(item, SettingsPageItem::SectionHeader(_))).then_some(index) - }) - .last(); - - let root_nav_label = self - .navbar_entries - .iter() - .find(|entry| entry.is_root && entry.page_index == self.current_page_index()) - .map(|entry| entry.title); - - let list_content = list( - self.list_state.clone(), - cx.processor(move |this, index, window, cx| { - if index == 0 { - return div() - .px_8() - .when(sub_page_stack().is_empty(), |this| { - this.when_some(root_nav_label, |this, title| { - this.child( - Label::new(title).size(LabelSize::Large).mt_2().mb_3(), - ) - }) - }) - .into_any_element(); - } - - let mut visible_items = this.visible_page_items(); - let Some((actual_item_index, item)) = visible_items.nth(index - 1) else { - return gpui::Empty.into_any_element(); - }; - - let no_bottom_border = visible_items - .next() - .map(|(_, item)| matches!(item, SettingsPageItem::SectionHeader(_))) - .unwrap_or(false); - - let is_last = Some(actual_item_index) == last_non_header_index; - - let item_focus_handle = - this.content_handles[page_index][actual_item_index].focus_handle(cx); - - v_flex() - .id(("settings-page-item", actual_item_index)) - .track_focus(&item_focus_handle) - .w_full() - .min_w_0() - .child(item.render( - this, - actual_item_index, - no_bottom_border || is_last, - window, - cx, - )) - .into_any_element() - }), - ); - - page_content = page_content.child(list_content.size_full()) - } - page_content - } - - fn render_sub_page_items<'a, Items: Iterator>( - &self, - items: Items, - page_index: Option, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let mut page_content = v_flex() - .id("settings-ui-page") - .size_full() - .overflow_y_scroll() - .track_scroll(&self.sub_page_scroll_handle); - - let items: Vec<_> = items.collect(); - let items_len = items.len(); - let mut section_header = None; - - let has_active_search = !self.search_bar.read(cx).is_empty(cx); - let has_no_results = items_len == 0 && has_active_search; - - if has_no_results { - let search_query = self.search_bar.read(cx).text(cx); - page_content = page_content.child( - self.render_empty_state(format!("No settings match \"{}\"", search_query).into()), - ) - } else { - let last_non_header_index = items - .iter() - .enumerate() - .rev() - .find(|(_, (_, item))| !matches!(item, SettingsPageItem::SectionHeader(_))) - .map(|(index, _)| index); - - let root_nav_label = self - .navbar_entries - .iter() - .find(|entry| entry.is_root && entry.page_index == self.current_page_index()) - .map(|entry| entry.title); - - page_content = page_content - .when(sub_page_stack().is_empty(), |this| { - this.when_some(root_nav_label, |this, title| { - this.child(Label::new(title).size(LabelSize::Large).mt_2().mb_3()) - }) - }) - .children(items.clone().into_iter().enumerate().map( - |(index, (actual_item_index, item))| { - let no_bottom_border = items - .get(index + 1) - .map(|(_, next_item)| { - matches!(next_item, SettingsPageItem::SectionHeader(_)) - }) - .unwrap_or(false); - let is_last = Some(index) == last_non_header_index; - - if let SettingsPageItem::SectionHeader(header) = item { - section_header = Some(*header); - } - v_flex() - .w_full() - .min_w_0() - .id(("settings-page-item", actual_item_index)) - .when_some(page_index, |element, page_index| { - element.track_focus( - &self.content_handles[page_index][actual_item_index] - .focus_handle(cx), - ) - }) - .child(item.render( - self, - actual_item_index, - no_bottom_border || is_last, - window, - cx, - )) - }, - )) - } - page_content - } - - fn render_page( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let page_header; - let page_content; - - if sub_page_stack().is_empty() { - page_header = self.render_files_header(window, cx).into_any_element(); - - page_content = self - .render_page_items(self.current_page_index(), window, cx) - .into_any_element(); - } else { - page_header = h_flex() - .w_full() - .justify_between() - .child( - h_flex() - .ml_neg_1p5() - .gap_1() - .child( - IconButton::new("back-btn", IconName::ArrowLeft) - .icon_size(IconSize::Small) - .shape(IconButtonShape::Square) - .on_click(cx.listener(|this, _, _, cx| { - this.pop_sub_page(cx); - })), - ) - .child(self.render_sub_page_breadcrumbs()), - ) - .child( - Button::new("open-in-settings-file", "Edit in settings.json") - .tab_index(0_isize) - .style(ButtonStyle::OutlinedGhost) - .tooltip(Tooltip::for_action_title_in( - "Edit in settings.json", - &OpenCurrentFile, - &self.focus_handle, - )) - .on_click(cx.listener(|this, _, window, cx| { - this.open_current_settings_file(window, cx); - })), - ) - .into_any_element(); - - let active_page_render_fn = sub_page_stack().last().unwrap().link.render.clone(); - page_content = (active_page_render_fn)(self, window, cx); - } - - let mut warning_banner = gpui::Empty.into_any_element(); - if let Some(error) = - SettingsStore::global(cx).error_for_file(self.current_file.to_settings()) - { - fn banner( - label: &'static str, - error: String, - shown_errors: &mut HashSet, - cx: &mut Context, - ) -> impl IntoElement { - if shown_errors.insert(error.clone()) { - telemetry::event!("Settings Error Shown", label = label, error = &error); - } - Banner::new() - .severity(Severity::Warning) - .child( - v_flex() - .my_0p5() - .gap_0p5() - .child(Label::new(label)) - .child(Label::new(error).size(LabelSize::Small).color(Color::Muted)), - ) - .action_slot( - div().pr_1().pb_1().child( - Button::new("fix-in-json", "Fix in settings.json") - .tab_index(0_isize) - .style(ButtonStyle::Tinted(ui::TintColor::Warning)) - .on_click(cx.listener(|this, _, window, cx| { - this.open_current_settings_file(window, cx); - })), - ), - ) - } - - let parse_error = error.parse_error(); - let parse_failed = parse_error.is_some(); - - warning_banner = v_flex() - .gap_2() - .when_some(parse_error, |this, err| { - this.child(banner( - "Failed to load your settings. Some values may be incorrect and changes may be lost.", - err, - &mut self.shown_errors, - cx, - )) - }) - .map(|this| match &error.migration_status { - settings::MigrationStatus::Succeeded => this.child(banner( - "Your settings are out of date, and need to be updated.", - match &self.current_file { - SettingsUiFile::User => "They can be automatically migrated to the latest version.", - SettingsUiFile::Server(_) | SettingsUiFile::Project(_) => "They must be manually migrated to the latest version." - }.to_string(), - &mut self.shown_errors, - cx, - )), - settings::MigrationStatus::Failed { error: err } if !parse_failed => this - .child(banner( - "Your settings file is out of date, automatic migration failed", - err.clone(), - &mut self.shown_errors, - cx, - )), - _ => this, - }) - .into_any_element() - } - - return v_flex() - .id("settings-ui-page") - .on_action(cx.listener(|this, _: &menu::SelectNext, window, cx| { - if !sub_page_stack().is_empty() { - window.focus_next(); - return; - } - for (logical_index, (actual_index, _)) in this.visible_page_items().enumerate() { - let handle = this.content_handles[this.current_page_index()][actual_index] - .focus_handle(cx); - let mut offset = 1; // for page header - - if let Some((_, next_item)) = this.visible_page_items().nth(logical_index + 1) - && matches!(next_item, SettingsPageItem::SectionHeader(_)) - { - offset += 1; - } - if handle.contains_focused(window, cx) { - let next_logical_index = logical_index + offset + 1; - this.list_state.scroll_to_reveal_item(next_logical_index); - // We need to render the next item to ensure it's focus handle is in the element tree - cx.on_next_frame(window, |_, window, cx| { - cx.notify(); - cx.on_next_frame(window, |_, window, cx| { - window.focus_next(); - cx.notify(); - }); - }); - cx.notify(); - return; - } - } - window.focus_next(); - })) - .on_action(cx.listener(|this, _: &menu::SelectPrevious, window, cx| { - if !sub_page_stack().is_empty() { - window.focus_prev(); - return; - } - let mut prev_was_header = false; - for (logical_index, (actual_index, item)) in this.visible_page_items().enumerate() { - let is_header = matches!(item, SettingsPageItem::SectionHeader(_)); - let handle = this.content_handles[this.current_page_index()][actual_index] - .focus_handle(cx); - let mut offset = 1; // for page header - - if prev_was_header { - offset -= 1; - } - if handle.contains_focused(window, cx) { - let next_logical_index = logical_index + offset - 1; - this.list_state.scroll_to_reveal_item(next_logical_index); - // We need to render the next item to ensure it's focus handle is in the element tree - cx.on_next_frame(window, |_, window, cx| { - cx.notify(); - cx.on_next_frame(window, |_, window, cx| { - window.focus_prev(); - cx.notify(); - }); - }); - cx.notify(); - return; - } - prev_was_header = is_header; - } - window.focus_prev(); - })) - .when(sub_page_stack().is_empty(), |this| { - this.vertical_scrollbar_for(&self.list_state, window, cx) - }) - .when(!sub_page_stack().is_empty(), |this| { - this.vertical_scrollbar_for(&self.sub_page_scroll_handle, window, cx) - }) - .track_focus(&self.content_focus_handle.focus_handle(cx)) - .pt_6() - .gap_4() - .flex_1() - .bg(cx.theme().colors().editor_background) - .child( - v_flex() - .px_8() - .gap_2() - .child(page_header) - .child(warning_banner), - ) - .child( - div() - .flex_1() - .size_full() - .tab_group() - .tab_index(CONTENT_GROUP_TAB_INDEX) - .child(page_content), - ); - } - - /// This function will create a new settings file if one doesn't exist - /// if the current file is a project settings with a valid worktree id - /// We do this because the settings ui allows initializing project settings - fn open_current_settings_file(&mut self, window: &mut Window, cx: &mut Context) { - match &self.current_file { - SettingsUiFile::User => { - let Some(original_window) = self.original_window else { - return; - }; - original_window - .update(cx, |workspace, window, cx| { - workspace - .with_local_workspace(window, cx, |workspace, window, cx| { - let create_task = workspace.project().update(cx, |project, cx| { - project.find_or_create_worktree( - paths::config_dir().as_path(), - false, - cx, - ) - }); - let open_task = workspace.open_paths( - vec![paths::settings_file().to_path_buf()], - OpenOptions { - visible: Some(OpenVisible::None), - ..Default::default() - }, - None, - window, - cx, - ); - - cx.spawn_in(window, async move |workspace, cx| { - create_task.await.ok(); - open_task.await; - - workspace.update_in(cx, |_, window, cx| { - window.activate_window(); - cx.notify(); - }) - }) - .detach(); - }) - .detach(); - }) - .ok(); - - window.remove_window(); - } - SettingsUiFile::Project((worktree_id, path)) => { - let settings_path = path.join(paths::local_settings_file_relative_path()); - let Some(app_state) = workspace::AppState::global(cx).upgrade() else { - return; - }; - - let Some((worktree, corresponding_workspace)) = app_state - .workspace_store - .read(cx) - .workspaces() - .iter() - .find_map(|workspace| { - workspace - .read_with(cx, |workspace, cx| { - workspace - .project() - .read(cx) - .worktree_for_id(*worktree_id, cx) - }) - .ok() - .flatten() - .zip(Some(*workspace)) - }) - else { - log::error!( - "No corresponding workspace contains worktree id: {}", - worktree_id - ); - - return; - }; - - let create_task = if worktree.read(cx).entry_for_path(&settings_path).is_some() { - None - } else { - Some(worktree.update(cx, |tree, cx| { - tree.create_entry( - settings_path.clone(), - false, - Some(initial_project_settings_content().as_bytes().to_vec()), - cx, - ) - })) - }; - - let worktree_id = *worktree_id; - - // TODO: move zed::open_local_file() APIs to this crate, and - // re-implement the "initial_contents" behavior - corresponding_workspace - .update(cx, |_, window, cx| { - cx.spawn_in(window, async move |workspace, cx| { - if let Some(create_task) = create_task { - create_task.await.ok()?; - }; - - workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_path( - (worktree_id, settings_path.clone()), - None, - true, - window, - cx, - ) - }) - .ok()? - .await - .log_err()?; - - workspace - .update_in(cx, |_, window, cx| { - window.activate_window(); - cx.notify(); - }) - .ok(); - - Some(()) - }) - .detach(); - }) - .ok(); - - window.remove_window(); - } - SettingsUiFile::Server(_) => { - // Server files are not editable - return; - } - }; - } - - fn current_page_index(&self) -> usize { - self.page_index_from_navbar_index(self.navbar_entry) - } - - fn current_page(&self) -> &SettingsPage { - &self.pages[self.current_page_index()] - } - - fn page_index_from_navbar_index(&self, index: usize) -> usize { - if self.navbar_entries.is_empty() { - return 0; - } - - self.navbar_entries[index].page_index - } - - fn is_navbar_entry_selected(&self, ix: usize) -> bool { - ix == self.navbar_entry - } - - fn push_sub_page( - &mut self, - sub_page_link: SubPageLink, - section_header: &'static str, - cx: &mut Context, - ) { - sub_page_stack_mut().push(SubPage { - link: sub_page_link, - section_header, - }); - cx.notify(); - } - - fn pop_sub_page(&mut self, cx: &mut Context) { - sub_page_stack_mut().pop(); - cx.notify(); - } - - fn focus_file_at_index(&mut self, index: usize, window: &mut Window) { - if let Some((_, handle)) = self.files.get(index) { - handle.focus(window); - } - } - - fn focused_file_index(&self, window: &Window, cx: &Context) -> usize { - if self.files_focus_handle.contains_focused(window, cx) - && let Some(index) = self - .files - .iter() - .position(|(_, handle)| handle.is_focused(window)) - { - return index; - } - if let Some(current_file_index) = self - .files - .iter() - .position(|(file, _)| file == &self.current_file) - { - return current_file_index; - } - 0 - } - - fn focus_handle_for_content_element( - &self, - actual_item_index: usize, - cx: &Context, - ) -> FocusHandle { - let page_index = self.current_page_index(); - self.content_handles[page_index][actual_item_index].focus_handle(cx) - } - - fn focused_nav_entry(&self, window: &Window, cx: &App) -> Option { - if !self - .navbar_focus_handle - .focus_handle(cx) - .contains_focused(window, cx) - { - return None; - } - for (index, entry) in self.navbar_entries.iter().enumerate() { - if entry.focus_handle.is_focused(window) { - return Some(index); - } - } - None - } - - fn root_entry_containing(&self, nav_entry_index: usize) -> usize { - let mut index = Some(nav_entry_index); - while let Some(prev_index) = index - && !self.navbar_entries[prev_index].is_root - { - index = prev_index.checked_sub(1); - } - return index.expect("No root entry found"); - } -} - -impl Render for SettingsWindow { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let ui_font = theme::setup_ui_font(window, cx); - - client_side_decorations( - v_flex() - .text_color(cx.theme().colors().text) - .size_full() - .children(self.title_bar.clone()) - .child( - div() - .id("settings-window") - .key_context("SettingsWindow") - .track_focus(&self.focus_handle) - .on_action(cx.listener(|this, _: &OpenCurrentFile, window, cx| { - this.open_current_settings_file(window, cx); - })) - .on_action(|_: &Minimize, window, _cx| { - window.minimize_window(); - }) - .on_action(cx.listener(|this, _: &search::FocusSearch, window, cx| { - this.search_bar.focus_handle(cx).focus(window); - })) - .on_action(cx.listener(|this, _: &ToggleFocusNav, window, cx| { - if this - .navbar_focus_handle - .focus_handle(cx) - .contains_focused(window, cx) - { - this.open_and_scroll_to_navbar_entry( - this.navbar_entry, - None, - true, - window, - cx, - ); - } else { - this.focus_and_scroll_to_nav_entry(this.navbar_entry, window, cx); - } - })) - .on_action(cx.listener( - |this, FocusFile(file_index): &FocusFile, window, _| { - this.focus_file_at_index(*file_index as usize, window); - }, - )) - .on_action(cx.listener(|this, _: &FocusNextFile, window, cx| { - let next_index = usize::min( - this.focused_file_index(window, cx) + 1, - this.files.len().saturating_sub(1), - ); - this.focus_file_at_index(next_index, window); - })) - .on_action(cx.listener(|this, _: &FocusPreviousFile, window, cx| { - let prev_index = this.focused_file_index(window, cx).saturating_sub(1); - this.focus_file_at_index(prev_index, window); - })) - .on_action(cx.listener(|this, _: &menu::SelectNext, window, cx| { - if this - .search_bar - .focus_handle(cx) - .contains_focused(window, cx) - { - this.focus_and_scroll_to_first_visible_nav_entry(window, cx); - } else { - window.focus_next(); - } - })) - .on_action(|_: &menu::SelectPrevious, window, _| { - window.focus_prev(); - }) - .flex() - .flex_row() - .flex_1() - .min_h_0() - .font(ui_font) - .bg(cx.theme().colors().background) - .text_color(cx.theme().colors().text) - .when(!cfg!(target_os = "macos"), |this| { - this.border_t_1().border_color(cx.theme().colors().border) - }) - .child(self.render_nav(window, cx)) - .child(self.render_page(window, cx)), - ), - window, - cx, - ) - } -} - -fn all_projects(cx: &App) -> impl Iterator> { - workspace::AppState::global(cx) - .upgrade() - .map(|app_state| { - app_state - .workspace_store - .read(cx) - .workspaces() - .iter() - .filter_map(|workspace| Some(workspace.read(cx).ok()?.project().clone())) - }) - .into_iter() - .flatten() -} - -fn update_settings_file( - file: SettingsUiFile, - file_name: Option<&'static str>, - cx: &mut App, - update: impl 'static + Send + FnOnce(&mut SettingsContent, &App), -) -> Result<()> { - telemetry::event!("Settings Change", setting = file_name, type = file.setting_type()); - - match file { - SettingsUiFile::Project((worktree_id, rel_path)) => { - let rel_path = rel_path.join(paths::local_settings_file_relative_path()); - let Some((worktree, project)) = all_projects(cx).find_map(|project| { - project - .read(cx) - .worktree_for_id(worktree_id, cx) - .zip(Some(project)) - }) else { - anyhow::bail!("Could not find project with worktree id: {}", worktree_id); - }; - - project.update(cx, |project, cx| { - let task = if project.contains_local_settings_file(worktree_id, &rel_path, cx) { - None - } else { - Some(worktree.update(cx, |worktree, cx| { - worktree.create_entry(rel_path.clone(), false, None, cx) - })) - }; - - cx.spawn(async move |project, cx| { - if let Some(task) = task - && task.await.is_err() - { - return; - }; - - project - .update(cx, |project, cx| { - project.update_local_settings_file(worktree_id, rel_path, cx, update); - }) - .ok(); - }) - .detach(); - }); - - return Ok(()); - } - SettingsUiFile::User => { - // todo(settings_ui) error? - SettingsStore::global(cx).update_settings_file(::global(cx), update); - Ok(()) - } - SettingsUiFile::Server(_) => unimplemented!(), - } -} - -fn render_text_field + Into + AsRef + Clone>( - field: SettingField, - file: SettingsUiFile, - metadata: Option<&SettingsFieldMetadata>, - _window: &mut Window, - cx: &mut App, -) -> AnyElement { - let (_, initial_text) = - SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick); - let initial_text = initial_text.filter(|s| !s.as_ref().is_empty()); - - SettingsInputField::new() - .tab_index(0) - .when_some(initial_text, |editor, text| { - editor.with_initial_text(text.as_ref().to_string()) - }) - .when_some( - metadata.and_then(|metadata| metadata.placeholder), - |editor, placeholder| editor.with_placeholder(placeholder), - ) - .on_confirm({ - move |new_text, cx| { - update_settings_file(file.clone(), field.json_path, cx, move |settings, _cx| { - (field.write)(settings, new_text.map(Into::into)); - }) - .log_err(); // todo(settings_ui) don't log err - } - }) - .into_any_element() -} - -fn render_toggle_button + From + Copy>( - field: SettingField, - file: SettingsUiFile, - _metadata: Option<&SettingsFieldMetadata>, - _window: &mut Window, - cx: &mut App, -) -> AnyElement { - let (_, value) = SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick); - - let toggle_state = if value.copied().map_or(false, Into::into) { - ToggleState::Selected - } else { - ToggleState::Unselected - }; - - Switch::new("toggle_button", toggle_state) - .tab_index(0_isize) - .on_click({ - move |state, _window, cx| { - telemetry::event!("Settings Change", setting = field.json_path, type = file.setting_type()); - - let state = *state == ui::ToggleState::Selected; - update_settings_file(file.clone(), field.json_path, cx, move |settings, _cx| { - (field.write)(settings, Some(state.into())); - }) - .log_err(); // todo(settings_ui) don't log err - } - }) - .into_any_element() -} - -fn render_number_field( - field: SettingField, - file: SettingsUiFile, - _metadata: Option<&SettingsFieldMetadata>, - window: &mut Window, - cx: &mut App, -) -> AnyElement { - let (_, value) = SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick); - let value = value.copied().unwrap_or_else(T::min_value); - NumberField::new("numeric_stepper", value, window, cx) - .on_change({ - move |value, _window, cx| { - let value = *value; - update_settings_file(file.clone(), field.json_path, cx, move |settings, _cx| { - (field.write)(settings, Some(value)); - }) - .log_err(); // todo(settings_ui) don't log err - } - }) - .into_any_element() -} - -fn render_dropdown( - field: SettingField, - file: SettingsUiFile, - metadata: Option<&SettingsFieldMetadata>, - _window: &mut Window, - cx: &mut App, -) -> AnyElement -where - T: strum::VariantArray + strum::VariantNames + Copy + PartialEq + Send + Sync + 'static, -{ - let variants = || -> &'static [T] { ::VARIANTS }; - let labels = || -> &'static [&'static str] { ::VARIANTS }; - let should_do_titlecase = metadata - .and_then(|metadata| metadata.should_do_titlecase) - .unwrap_or(true); - - let (_, current_value) = - SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick); - let current_value = current_value.copied().unwrap_or(variants()[0]); - - EnumVariantDropdown::new("dropdown", current_value, variants(), labels(), { - move |value, cx| { - if value == current_value { - return; - } - update_settings_file(file.clone(), field.json_path, cx, move |settings, _cx| { - (field.write)(settings, Some(value)); - }) - .log_err(); // todo(settings_ui) don't log err - } - }) - .tab_index(0) - .title_case(should_do_titlecase) - .into_any_element() -} - -fn render_picker_trigger_button(id: SharedString, label: SharedString) -> Button { - Button::new(id, label) - .tab_index(0_isize) - .style(ButtonStyle::Outlined) - .size(ButtonSize::Medium) - .icon(IconName::ChevronUpDown) - .icon_color(Color::Muted) - .icon_size(IconSize::Small) - .icon_position(IconPosition::End) -} - -fn render_font_picker( - field: SettingField, - file: SettingsUiFile, - _metadata: Option<&SettingsFieldMetadata>, - _window: &mut Window, - cx: &mut App, -) -> AnyElement { - let current_value = SettingsStore::global(cx) - .get_value_from_file(file.to_settings(), field.pick) - .1 - .cloned() - .unwrap_or_else(|| SharedString::default().into()); - - PopoverMenu::new("font-picker") - .trigger(render_picker_trigger_button( - "font_family_picker_trigger".into(), - current_value.clone().into(), - )) - .menu(move |window, cx| { - let file = file.clone(); - let current_value = current_value.clone(); - - Some(cx.new(move |cx| { - font_picker( - current_value.clone().into(), - move |font_name, cx| { - update_settings_file( - file.clone(), - field.json_path, - cx, - move |settings, _cx| { - (field.write)(settings, Some(font_name.into())); - }, - ) - .log_err(); // todo(settings_ui) don't log err - }, - window, - cx, - ) - })) - }) - .anchor(gpui::Corner::TopLeft) - .offset(gpui::Point { - x: px(0.0), - y: px(2.0), - }) - .with_handle(ui::PopoverMenuHandle::default()) - .into_any_element() -} - -fn render_theme_picker( - field: SettingField, - file: SettingsUiFile, - _metadata: Option<&SettingsFieldMetadata>, - _window: &mut Window, - cx: &mut App, -) -> AnyElement { - let (_, value) = SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick); - let current_value = value - .cloned() - .map(|theme_name| theme_name.0.into()) - .unwrap_or_else(|| cx.theme().name.clone()); - - PopoverMenu::new("theme-picker") - .trigger(render_picker_trigger_button( - "theme_picker_trigger".into(), - current_value.clone(), - )) - .menu(move |window, cx| { - Some(cx.new(|cx| { - let file = file.clone(); - let current_value = current_value.clone(); - theme_picker( - current_value, - move |theme_name, cx| { - update_settings_file( - file.clone(), - field.json_path, - cx, - move |settings, _cx| { - (field.write)( - settings, - Some(settings::ThemeName(theme_name.into())), - ); - }, - ) - .log_err(); // todo(settings_ui) don't log err - }, - window, - cx, - ) - })) - }) - .anchor(gpui::Corner::TopLeft) - .offset(gpui::Point { - x: px(0.0), - y: px(2.0), - }) - .with_handle(ui::PopoverMenuHandle::default()) - .into_any_element() -} - -fn render_icon_theme_picker( - field: SettingField, - file: SettingsUiFile, - _metadata: Option<&SettingsFieldMetadata>, - _window: &mut Window, - cx: &mut App, -) -> AnyElement { - let (_, value) = SettingsStore::global(cx).get_value_from_file(file.to_settings(), field.pick); - let current_value = value - .cloned() - .map(|theme_name| theme_name.0.into()) - .unwrap_or_else(|| cx.theme().name.clone()); - - PopoverMenu::new("icon-theme-picker") - .trigger(render_picker_trigger_button( - "icon_theme_picker_trigger".into(), - current_value.clone(), - )) - .menu(move |window, cx| { - Some(cx.new(|cx| { - let file = file.clone(); - let current_value = current_value.clone(); - icon_theme_picker( - current_value, - move |theme_name, cx| { - update_settings_file( - file.clone(), - field.json_path, - cx, - move |settings, _cx| { - (field.write)( - settings, - Some(settings::IconThemeName(theme_name.into())), - ); - }, - ) - .log_err(); // todo(settings_ui) don't log err - }, - window, - cx, - ) - })) - }) - .anchor(gpui::Corner::TopLeft) - .offset(gpui::Point { - x: px(0.0), - y: px(2.0), - }) - .with_handle(ui::PopoverMenuHandle::default()) - .into_any_element() -} - -#[cfg(test)] -pub mod test { - - use super::*; - - impl SettingsWindow { - fn navbar_entry(&self) -> usize { - self.navbar_entry - } - } - - impl PartialEq for NavBarEntry { - fn eq(&self, other: &Self) -> bool { - self.title == other.title - && self.is_root == other.is_root - && self.expanded == other.expanded - && self.page_index == other.page_index - && self.item_index == other.item_index - // ignoring focus_handle - } - } - - pub fn register_settings(cx: &mut App) { - settings::init(cx); - theme::init(theme::LoadThemes::JustBase, cx); - editor::init(cx); - menu::init(); - } - - fn parse(input: &'static str, window: &mut Window, cx: &mut App) -> SettingsWindow { - let mut pages: Vec = Vec::new(); - let mut expanded_pages = Vec::new(); - let mut selected_idx = None; - let mut index = 0; - let mut in_expanded_section = false; - - for mut line in input - .lines() - .map(|line| line.trim()) - .filter(|line| !line.is_empty()) - { - if let Some(pre) = line.strip_suffix('*') { - assert!(selected_idx.is_none(), "Only one selected entry allowed"); - selected_idx = Some(index); - line = pre; - } - let (kind, title) = line.split_once(" ").unwrap(); - assert_eq!(kind.len(), 1); - let kind = kind.chars().next().unwrap(); - if kind == 'v' { - let page_idx = pages.len(); - expanded_pages.push(page_idx); - pages.push(SettingsPage { - title, - items: vec![], - }); - index += 1; - in_expanded_section = true; - } else if kind == '>' { - pages.push(SettingsPage { - title, - items: vec![], - }); - index += 1; - in_expanded_section = false; - } else if kind == '-' { - pages - .last_mut() - .unwrap() - .items - .push(SettingsPageItem::SectionHeader(title)); - if selected_idx == Some(index) && !in_expanded_section { - panic!("Items in unexpanded sections cannot be selected"); - } - index += 1; - } else { - panic!( - "Entries must start with one of 'v', '>', or '-'\n line: {}", - line - ); - } - } - - let mut settings_window = SettingsWindow { - title_bar: None, - original_window: None, - worktree_root_dirs: HashMap::default(), - files: Vec::default(), - current_file: crate::SettingsUiFile::User, - pages, - search_bar: cx.new(|cx| Editor::single_line(window, cx)), - navbar_entry: selected_idx.expect("Must have a selected navbar entry"), - navbar_entries: Vec::default(), - navbar_scroll_handle: UniformListScrollHandle::default(), - navbar_focus_subscriptions: vec![], - filter_table: vec![], - has_query: false, - content_handles: vec![], - search_task: None, - sub_page_scroll_handle: ScrollHandle::new(), - focus_handle: cx.focus_handle(), - navbar_focus_handle: NonFocusableHandle::new( - NAVBAR_CONTAINER_TAB_INDEX, - false, - window, - cx, - ), - content_focus_handle: NonFocusableHandle::new( - CONTENT_CONTAINER_TAB_INDEX, - false, - window, - cx, - ), - files_focus_handle: cx.focus_handle(), - search_index: None, - list_state: ListState::new(0, gpui::ListAlignment::Top, px(0.0)), - shown_errors: HashSet::default(), - }; - - settings_window.build_filter_table(); - settings_window.build_navbar(cx); - for expanded_page_index in expanded_pages { - for entry in &mut settings_window.navbar_entries { - if entry.page_index == expanded_page_index && entry.is_root { - entry.expanded = true; - } - } - } - settings_window - } - - #[track_caller] - fn check_navbar_toggle( - before: &'static str, - toggle_page: &'static str, - after: &'static str, - window: &mut Window, - cx: &mut App, - ) { - let mut settings_window = parse(before, window, cx); - let toggle_page_idx = settings_window - .pages - .iter() - .position(|page| page.title == toggle_page) - .expect("page not found"); - let toggle_idx = settings_window - .navbar_entries - .iter() - .position(|entry| entry.page_index == toggle_page_idx) - .expect("page not found"); - settings_window.toggle_navbar_entry(toggle_idx); - - let expected_settings_window = parse(after, window, cx); - - pretty_assertions::assert_eq!( - settings_window - .visible_navbar_entries() - .map(|(_, entry)| entry) - .collect::>(), - expected_settings_window - .visible_navbar_entries() - .map(|(_, entry)| entry) - .collect::>(), - ); - pretty_assertions::assert_eq!( - settings_window.navbar_entries[settings_window.navbar_entry()], - expected_settings_window.navbar_entries[expected_settings_window.navbar_entry()], - ); - } - - macro_rules! check_navbar_toggle { - ($name:ident, before: $before:expr, toggle_page: $toggle_page:expr, after: $after:expr) => { - #[gpui::test] - fn $name(cx: &mut gpui::TestAppContext) { - let window = cx.add_empty_window(); - window.update(|window, cx| { - register_settings(cx); - check_navbar_toggle($before, $toggle_page, $after, window, cx); - }); - } - }; - } - - check_navbar_toggle!( - navbar_basic_open, - before: r" - v General - - General - - Privacy* - v Project - - Project Settings - ", - toggle_page: "General", - after: r" - > General* - v Project - - Project Settings - " - ); - - check_navbar_toggle!( - navbar_basic_close, - before: r" - > General* - - General - - Privacy - v Project - - Project Settings - ", - toggle_page: "General", - after: r" - v General* - - General - - Privacy - v Project - - Project Settings - " - ); - - check_navbar_toggle!( - navbar_basic_second_root_entry_close, - before: r" - > General - - General - - Privacy - v Project - - Project Settings* - ", - toggle_page: "Project", - after: r" - > General - > Project* - " - ); - - check_navbar_toggle!( - navbar_toggle_subroot, - before: r" - v General Page - - General - - Privacy - v Project - - Worktree Settings Content* - v AI - - General - > Appearance & Behavior - ", - toggle_page: "Project", - after: r" - v General Page - - General - - Privacy - > Project* - v AI - - General - > Appearance & Behavior - " - ); - - check_navbar_toggle!( - navbar_toggle_close_propagates_selected_index, - before: r" - v General Page - - General - - Privacy - v Project - - Worktree Settings Content - v AI - - General* - > Appearance & Behavior - ", - toggle_page: "General Page", - after: r" - > General Page* - v Project - - Worktree Settings Content - v AI - - General - > Appearance & Behavior - " - ); - - check_navbar_toggle!( - navbar_toggle_expand_propagates_selected_index, - before: r" - > General Page - - General - - Privacy - v Project - - Worktree Settings Content - v AI - - General* - > Appearance & Behavior - ", - toggle_page: "General Page", - after: r" - v General Page* - - General - - Privacy - v Project - - Worktree Settings Content - v AI - - General - > Appearance & Behavior - " - ); -} diff --git a/crates/snippet/Cargo.toml b/crates/snippet/Cargo.toml deleted file mode 100644 index 2dde5c2d00..0000000000 --- a/crates/snippet/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "snippet" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/snippet.rs" -doctest = false - -[dependencies] -anyhow.workspace = true -smallvec.workspace = true diff --git a/crates/snippet/LICENSE-GPL b/crates/snippet/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/snippet/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/snippet/src/snippet.rs b/crates/snippet/src/snippet.rs deleted file mode 100644 index 4be4281d9a..0000000000 --- a/crates/snippet/src/snippet.rs +++ /dev/null @@ -1,334 +0,0 @@ -use anyhow::{Context as _, Result}; -use smallvec::SmallVec; -use std::{collections::BTreeMap, ops::Range}; - -#[derive(Clone, Debug, Default, PartialEq)] -pub struct Snippet { - pub text: String, - pub tabstops: Vec, -} - -#[derive(Clone, Debug, Default, PartialEq)] -pub struct TabStop { - pub ranges: SmallVec<[Range; 2]>, - pub choices: Option>, -} - -impl Snippet { - pub fn parse(source: &str) -> Result { - let mut text = String::with_capacity(source.len()); - let mut tabstops = BTreeMap::new(); - parse_snippet(source, false, &mut text, &mut tabstops) - .context("failed to parse snippet")?; - - let len = text.len() as isize; - let final_tabstop = tabstops.remove(&0); - let mut tabstops = tabstops.into_values().collect::>(); - - if let Some(final_tabstop) = final_tabstop { - tabstops.push(final_tabstop); - } else { - let end_tabstop = TabStop { - ranges: [len..len].into_iter().collect(), - choices: None, - }; - - if !tabstops.last().is_some_and(|t| *t == end_tabstop) { - tabstops.push(end_tabstop); - } - } - - Ok(Snippet { text, tabstops }) - } -} - -fn parse_snippet<'a>( - mut source: &'a str, - nested: bool, - text: &mut String, - tabstops: &mut BTreeMap, -) -> Result<&'a str> { - loop { - match source.chars().next() { - None => return Ok(""), - Some('$') => { - source = parse_tabstop(&source[1..], text, tabstops)?; - } - Some('\\') => { - // As specified in the LSP spec (`Grammar` section), - // backslashes can escape some characters: - // https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#snippet_syntax - source = &source[1..]; - if let Some(c) = source.chars().next() { - if c == '$' || c == '\\' || c == '}' { - text.push(c); - // All escapable characters are 1 byte long: - source = &source[1..]; - } else { - text.push('\\'); - } - } else { - text.push('\\'); - } - } - Some('}') => { - if nested { - return Ok(source); - } else { - text.push('}'); - source = &source[1..]; - } - } - Some(_) => { - let chunk_end = source.find(['}', '$', '\\']).unwrap_or(source.len()); - let (chunk, rest) = source.split_at(chunk_end); - text.push_str(chunk); - source = rest; - } - } - } -} - -fn parse_tabstop<'a>( - mut source: &'a str, - text: &mut String, - tabstops: &mut BTreeMap, -) -> Result<&'a str> { - let tabstop_start = text.len(); - let tabstop_index; - let mut choices = None; - - if source.starts_with('{') { - let (index, rest) = parse_int(&source[1..])?; - tabstop_index = index; - source = rest; - - if source.starts_with("|") { - (source, choices) = parse_choices(&source[1..], text)?; - } - - if source.starts_with(':') { - source = parse_snippet(&source[1..], true, text, tabstops)?; - } - - if source.starts_with('}') { - source = &source[1..]; - } else { - anyhow::bail!("expected a closing brace"); - } - } else { - let (index, rest) = parse_int(source)?; - tabstop_index = index; - source = rest; - } - - tabstops - .entry(tabstop_index) - .or_insert_with(|| TabStop { - ranges: Default::default(), - choices, - }) - .ranges - .push(tabstop_start as isize..text.len() as isize); - Ok(source) -} - -fn parse_int(source: &str) -> Result<(usize, &str)> { - let len = source - .find(|c: char| !c.is_ascii_digit()) - .unwrap_or(source.len()); - anyhow::ensure!(len > 0, "expected an integer"); - let (prefix, suffix) = source.split_at(len); - Ok((prefix.parse()?, suffix)) -} - -fn parse_choices<'a>( - mut source: &'a str, - text: &mut String, -) -> Result<(&'a str, Option>)> { - let mut found_default_choice = false; - let mut current_choice = String::new(); - let mut choices = Vec::new(); - - loop { - match source.chars().next() { - None => return Ok(("", Some(choices))), - Some('\\') => { - source = &source[1..]; - - if let Some(c) = source.chars().next() { - if !found_default_choice { - current_choice.push(c); - text.push(c); - } - source = &source[c.len_utf8()..]; - } - } - Some(',') => { - found_default_choice = true; - source = &source[1..]; - choices.push(current_choice); - current_choice = String::new(); - } - Some('|') => { - source = &source[1..]; - choices.push(current_choice); - return Ok((source, Some(choices))); - } - Some(_) => { - let chunk_end = source.find([',', '|', '\\']); - - anyhow::ensure!( - chunk_end.is_some(), - "Placeholder choice doesn't contain closing pipe-character '|'" - ); - - let (chunk, rest) = source.split_at(chunk_end.unwrap()); - - if !found_default_choice { - text.push_str(chunk); - } - - current_choice.push_str(chunk); - source = rest; - } - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_snippet_without_tabstops() { - let snippet = Snippet::parse("one-two-three").unwrap(); - assert_eq!(snippet.text, "one-two-three"); - assert_eq!(tabstops(&snippet), &[vec![13..13]]); - } - - #[test] - fn test_snippet_with_tabstops() { - let snippet = Snippet::parse("one$1two").unwrap(); - assert_eq!(snippet.text, "onetwo"); - assert_eq!(tabstops(&snippet), &[vec![3..3], vec![6..6]]); - assert_eq!(tabstop_choices(&snippet), &[&None, &None]); - - // Multi-digit numbers - let snippet = Snippet::parse("one$123-$99-two").unwrap(); - assert_eq!(snippet.text, "one--two"); - assert_eq!(tabstops(&snippet), &[vec![4..4], vec![3..3], vec![8..8]]); - assert_eq!(tabstop_choices(&snippet), &[&None, &None, &None]); - } - - #[test] - fn test_snippet_with_last_tabstop_at_end() { - let snippet = Snippet::parse(r#"foo.$1"#).unwrap(); - - // If the final tabstop is already at the end of the text, don't insert - // an additional tabstop at the end. - assert_eq!(snippet.text, r#"foo."#); - assert_eq!(tabstops(&snippet), &[vec![4..4]]); - assert_eq!(tabstop_choices(&snippet), &[&None]); - } - - #[test] - fn test_snippet_with_explicit_final_tabstop() { - let snippet = Snippet::parse(r#"
$0
"#).unwrap(); - - // If the final tabstop is explicitly specified via '$0', then - // don't insert an additional tabstop at the end. - assert_eq!(snippet.text, r#"
"#); - assert_eq!(tabstops(&snippet), &[vec![12..12], vec![14..14]]); - assert_eq!(tabstop_choices(&snippet), &[&None, &None]); - } - - #[test] - fn test_snippet_with_placeholders() { - let snippet = Snippet::parse("one${1:two}three${2:four}").unwrap(); - assert_eq!(snippet.text, "onetwothreefour"); - assert_eq!( - tabstops(&snippet), - &[vec![3..6], vec![11..15], vec![15..15]] - ); - assert_eq!(tabstop_choices(&snippet), &[&None, &None, &None]); - } - - #[test] - fn test_snippet_with_choice_placeholders() { - let snippet = Snippet::parse("type ${1|i32, u32|} = $2") - .expect("Should be able to unpack choice placeholders"); - - assert_eq!(snippet.text, "type i32 = "); - assert_eq!(tabstops(&snippet), &[vec![5..8], vec![11..11],]); - assert_eq!( - tabstop_choices(&snippet), - &[&Some(vec!["i32".to_string(), " u32".to_string()]), &None] - ); - - let snippet = Snippet::parse(r"${1|\$\{1\|one\,two\,tree\|\}|}") - .expect("Should be able to parse choice with escape characters"); - - assert_eq!(snippet.text, "${1|one,two,tree|}"); - assert_eq!(tabstops(&snippet), &[vec![0..18], vec![18..18]]); - assert_eq!( - tabstop_choices(&snippet), - &[&Some(vec!["${1|one,two,tree|}".to_string(),]), &None] - ); - } - - #[test] - fn test_snippet_with_nested_placeholders() { - let snippet = Snippet::parse( - "for (${1:var ${2:i} = 0; ${2:i} < ${3:${4:array}.length}; ${2:i}++}) {$0}", - ) - .unwrap(); - assert_eq!(snippet.text, "for (var i = 0; i < array.length; i++) {}"); - assert_eq!( - tabstops(&snippet), - &[ - vec![5..37], - vec![9..10, 16..17, 34..35], - vec![20..32], - vec![20..25], - vec![40..40], - ] - ); - assert_eq!( - tabstop_choices(&snippet), - &[&None, &None, &None, &None, &None] - ); - } - - #[test] - fn test_snippet_parsing_with_escaped_chars() { - let snippet = Snippet::parse("\"\\$schema\": $1").unwrap(); - assert_eq!(snippet.text, "\"$schema\": "); - assert_eq!(tabstops(&snippet), &[vec![11..11]]); - assert_eq!(tabstop_choices(&snippet), &[&None]); - - let snippet = Snippet::parse("{a\\}").unwrap(); - assert_eq!(snippet.text, "{a}"); - assert_eq!(tabstops(&snippet), &[vec![3..3]]); - assert_eq!(tabstop_choices(&snippet), &[&None]); - - // backslash not functioning as an escape - let snippet = Snippet::parse("a\\b").unwrap(); - assert_eq!(snippet.text, "a\\b"); - assert_eq!(tabstops(&snippet), &[vec![3..3]]); - - // first backslash cancelling escaping that would - // have happened with second backslash - let snippet = Snippet::parse("one\\\\$1two").unwrap(); - assert_eq!(snippet.text, "one\\two"); - assert_eq!(tabstops(&snippet), &[vec![4..4], vec![7..7]]); - } - - fn tabstops(snippet: &Snippet) -> Vec>> { - snippet.tabstops.iter().map(|t| t.ranges.to_vec()).collect() - } - - fn tabstop_choices(snippet: &Snippet) -> Vec<&Option>> { - snippet.tabstops.iter().map(|t| &t.choices).collect() - } -} diff --git a/crates/snippet_provider/Cargo.toml b/crates/snippet_provider/Cargo.toml deleted file mode 100644 index c1f04117d4..0000000000 --- a/crates/snippet_provider/Cargo.toml +++ /dev/null @@ -1,33 +0,0 @@ -[package] -name = "snippet_provider" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[features] -test-support = [] - -[dependencies] -anyhow.workspace = true -collections.workspace = true -extension.workspace = true -fs.workspace = true -futures.workspace = true -gpui.workspace = true -parking_lot.workspace = true -paths.workspace = true -serde.workspace = true -serde_json.workspace = true -serde_json_lenient.workspace = true -snippet.workspace = true -util.workspace = true -schemars.workspace = true - -[dev-dependencies] -fs = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -indoc.workspace = true diff --git a/crates/snippet_provider/LICENSE-GPL b/crates/snippet_provider/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/snippet_provider/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/snippet_provider/src/extension_snippet.rs b/crates/snippet_provider/src/extension_snippet.rs deleted file mode 100644 index cd5fb083f9..0000000000 --- a/crates/snippet_provider/src/extension_snippet.rs +++ /dev/null @@ -1,26 +0,0 @@ -use std::path::PathBuf; -use std::sync::Arc; - -use anyhow::Result; -use extension::{ExtensionHostProxy, ExtensionSnippetProxy}; -use gpui::App; - -use crate::SnippetRegistry; - -pub fn init(cx: &mut App) { - let proxy = ExtensionHostProxy::default_global(cx); - proxy.register_snippet_proxy(SnippetRegistryProxy { - snippet_registry: SnippetRegistry::global(cx), - }); -} - -struct SnippetRegistryProxy { - snippet_registry: Arc, -} - -impl ExtensionSnippetProxy for SnippetRegistryProxy { - fn register_snippet(&self, path: &PathBuf, snippet_contents: &str) -> Result<()> { - self.snippet_registry - .register_snippets(path, snippet_contents) - } -} diff --git a/crates/snippet_provider/src/format.rs b/crates/snippet_provider/src/format.rs deleted file mode 100644 index f9abb987d9..0000000000 --- a/crates/snippet_provider/src/format.rs +++ /dev/null @@ -1,78 +0,0 @@ -use collections::HashMap; -use schemars::{JsonSchema, json_schema}; -use serde::Deserialize; -use std::borrow::Cow; -use util::schemars::{AllowTrailingCommas, DefaultDenyUnknownFields}; - -#[derive(Deserialize)] -pub struct VsSnippetsFile { - #[serde(flatten)] - pub(crate) snippets: HashMap, -} - -impl VsSnippetsFile { - pub fn generate_json_schema() -> serde_json::Value { - let schema = schemars::generate::SchemaSettings::draft2019_09() - .with_transform(DefaultDenyUnknownFields) - .with_transform(AllowTrailingCommas) - .into_generator() - .root_schema_for::(); - - serde_json::to_value(schema).unwrap() - } -} - -impl JsonSchema for VsSnippetsFile { - fn schema_name() -> Cow<'static, str> { - "VsSnippetsFile".into() - } - - fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema { - let snippet_schema = generator.subschema_for::(); - json_schema!({ - "type": "object", - "additionalProperties": snippet_schema - }) - } -} - -#[derive(Deserialize, JsonSchema)] -#[serde(untagged)] -pub(crate) enum ListOrDirect { - Single(String), - List(Vec), -} - -impl From for Vec { - fn from(list: ListOrDirect) -> Self { - match list { - ListOrDirect::Single(entry) => vec![entry], - ListOrDirect::List(entries) => entries, - } - } -} - -impl std::fmt::Display for ListOrDirect { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Self::Single(v) => v.to_owned(), - Self::List(v) => v.join("\n"), - } - ) - } -} - -#[derive(Deserialize, JsonSchema)] -pub(crate) struct VsCodeSnippet { - /// The snippet prefix used to decide whether a completion menu should be shown. - pub(crate) prefix: Option, - - /// The snippet content. Use `$1` and `${1:defaultText}` to define cursor positions and `$0` for final cursor position. - pub(crate) body: ListOrDirect, - - /// The snippet description displayed inside the completion menu. - pub(crate) description: Option, -} diff --git a/crates/snippet_provider/src/lib.rs b/crates/snippet_provider/src/lib.rs deleted file mode 100644 index 5eff6c917f..0000000000 --- a/crates/snippet_provider/src/lib.rs +++ /dev/null @@ -1,295 +0,0 @@ -mod extension_snippet; -pub mod format; -mod registry; - -use std::{ - path::{Path, PathBuf}, - sync::Arc, - time::Duration, -}; - -use anyhow::Result; -use collections::{BTreeMap, BTreeSet, HashMap}; -use format::VsSnippetsFile; -use fs::Fs; -use futures::stream::StreamExt; -use gpui::{App, AppContext as _, AsyncApp, Context, Entity, Task, WeakEntity}; -pub use registry::*; -use util::ResultExt; - -pub fn init(cx: &mut App) { - SnippetRegistry::init_global(cx); - extension_snippet::init(cx); -} - -/// Language name, or `None` if the snippet file is global. -type SnippetKind = Option; -fn file_stem_to_key(stem: &str) -> SnippetKind { - if stem == "snippets" { - None - } else { - Some(stem.to_owned()) - } -} - -fn file_to_snippets(file_contents: VsSnippetsFile) -> Vec> { - let mut snippets = vec![]; - for (name, snippet) in file_contents.snippets { - let snippet_name = name.clone(); - let prefixes = snippet - .prefix - .map_or_else(move || vec![snippet_name], |prefixes| prefixes.into()); - let description = snippet - .description - .map(|description| description.to_string()); - let body = snippet.body.to_string(); - if snippet::Snippet::parse(&body).log_err().is_none() { - continue; - }; - snippets.push(Arc::new(Snippet { - body, - prefix: prefixes, - description, - name, - })); - } - snippets -} -// Snippet with all of the metadata -#[derive(Debug)] -pub struct Snippet { - pub prefix: Vec, - pub body: String, - pub description: Option, - pub name: String, -} - -async fn process_updates( - this: WeakEntity, - entries: Vec, - mut cx: AsyncApp, -) -> Result<()> { - let fs = this.read_with(&cx, |this, _| this.fs.clone())?; - for entry_path in entries { - if entry_path - .extension() - .is_none_or(|extension| extension != "json") - { - continue; - } - let entry_metadata = fs.metadata(&entry_path).await; - // Entry could have been removed, in which case we should no longer show completions for it. - let entry_exists = entry_metadata.is_ok(); - if entry_metadata.is_ok_and(|entry| entry.is_some_and(|e| e.is_dir)) { - // Don't process dirs. - continue; - } - let Some(stem) = entry_path.file_stem().and_then(|s| s.to_str()) else { - continue; - }; - let key = file_stem_to_key(stem); - - let contents = if entry_exists { - fs.load(&entry_path).await.ok() - } else { - None - }; - - this.update(&mut cx, move |this, _| { - let snippets_of_kind = this.snippets.entry(key).or_default(); - if entry_exists { - let Some(file_contents) = contents else { - return; - }; - let Ok(as_json) = serde_json_lenient::from_str::(&file_contents) - else { - return; - }; - let snippets = file_to_snippets(as_json); - *snippets_of_kind.entry(entry_path).or_default() = snippets; - } else { - snippets_of_kind.remove(&entry_path); - } - })?; - } - Ok(()) -} - -async fn initial_scan( - this: WeakEntity, - path: Arc, - cx: AsyncApp, -) -> Result<()> { - let fs = this.read_with(&cx, |this, _| this.fs.clone())?; - let entries = fs.read_dir(&path).await; - if let Ok(entries) = entries { - let entries = entries - .collect::>() - .await - .into_iter() - .collect::>>()?; - process_updates(this, entries, cx).await?; - } - Ok(()) -} - -pub struct SnippetProvider { - fs: Arc, - snippets: HashMap>>>, - watch_tasks: Vec>>, -} - -// Watches global snippet directory, is created just once and reused across multiple projects -struct GlobalSnippetWatcher(Entity); - -impl GlobalSnippetWatcher { - fn new(fs: Arc, cx: &mut App) -> Self { - let global_snippets_dir = paths::snippets_dir(); - let provider = cx.new(|_cx| SnippetProvider { - fs, - snippets: Default::default(), - watch_tasks: vec![], - }); - provider.update(cx, |this, cx| this.watch_directory(global_snippets_dir, cx)); - Self(provider) - } -} - -impl gpui::Global for GlobalSnippetWatcher {} - -impl SnippetProvider { - pub fn new(fs: Arc, dirs_to_watch: BTreeSet, cx: &mut App) -> Entity { - cx.new(move |cx| { - if !cx.has_global::() { - let global_watcher = GlobalSnippetWatcher::new(fs.clone(), cx); - cx.set_global(global_watcher); - } - let mut this = Self { - fs, - watch_tasks: Vec::new(), - snippets: Default::default(), - }; - - for dir in dirs_to_watch { - this.watch_directory(&dir, cx); - } - - this - }) - } - - /// Add directory to be watched for content changes - fn watch_directory(&mut self, path: &Path, cx: &Context) { - let path: Arc = Arc::from(path); - - self.watch_tasks.push(cx.spawn(async move |this, cx| { - let fs = this.read_with(cx, |this, _| this.fs.clone())?; - let watched_path = path.clone(); - let watcher = fs.watch(&watched_path, Duration::from_secs(1)); - initial_scan(this.clone(), path, cx.clone()).await?; - - let (mut entries, _) = watcher.await; - while let Some(entries) = entries.next().await { - process_updates( - this.clone(), - entries.into_iter().map(|event| event.path).collect(), - cx.clone(), - ) - .await?; - } - Ok(()) - })); - } - - fn lookup_snippets<'a, const LOOKUP_GLOBALS: bool>( - &'a self, - language: &'a SnippetKind, - cx: &App, - ) -> Vec> { - let mut user_snippets: Vec<_> = self - .snippets - .get(language) - .cloned() - .unwrap_or_default() - .into_iter() - .flat_map(|(_, snippets)| snippets.into_iter()) - .collect(); - if LOOKUP_GLOBALS { - if let Some(global_watcher) = cx.try_global::() { - user_snippets.extend( - global_watcher - .0 - .read(cx) - .lookup_snippets::(language, cx), - ); - } - - let Some(registry) = SnippetRegistry::try_global(cx) else { - return user_snippets; - }; - - let registry_snippets = registry.get_snippets(language); - user_snippets.extend(registry_snippets); - } - - user_snippets - } - - #[cfg(any(test, feature = "test-support"))] - pub fn add_snippet_for_test( - &mut self, - language: SnippetKind, - path: PathBuf, - snippet: Vec>, - ) { - self.snippets - .entry(language) - .or_default() - .insert(path, snippet); - } - - pub fn snippets_for(&self, language: SnippetKind, cx: &App) -> Vec> { - let mut requested_snippets = self.lookup_snippets::(&language, cx); - - if language.is_some() { - // Look up global snippets as well. - requested_snippets.extend(self.lookup_snippets::(&None, cx)); - } - requested_snippets - } -} - -#[cfg(test)] -mod tests { - use super::*; - use fs::FakeFs; - use gpui; - use gpui::TestAppContext; - use indoc::indoc; - - #[gpui::test] - fn test_lookup_snippets_dup_registry_snippets(cx: &mut TestAppContext) { - let fs = FakeFs::new(cx.background_executor.clone()); - cx.update(|cx| { - SnippetRegistry::init_global(cx); - SnippetRegistry::global(cx) - .register_snippets( - "ruby".as_ref(), - indoc! {r#" - { - "Log to console": { - "prefix": "log", - "body": ["console.info(\"Hello, ${1:World}!\")", "$0"], - "description": "Logs to console" - } - } - "#}, - ) - .unwrap(); - let provider = SnippetProvider::new(fs.clone(), Default::default(), cx); - cx.update_entity(&provider, |provider, cx| { - assert_eq!(1, provider.snippets_for(Some("ruby".to_owned()), cx).len()); - }); - }); - } -} diff --git a/crates/snippet_provider/src/registry.rs b/crates/snippet_provider/src/registry.rs deleted file mode 100644 index 65850d650e..0000000000 --- a/crates/snippet_provider/src/registry.rs +++ /dev/null @@ -1,54 +0,0 @@ -use std::{path::Path, sync::Arc}; - -use anyhow::Result; -use collections::HashMap; -use gpui::{App, Global, ReadGlobal, UpdateGlobal}; -use parking_lot::RwLock; - -use crate::{Snippet, SnippetKind, file_stem_to_key}; - -struct GlobalSnippetRegistry(Arc); - -impl Global for GlobalSnippetRegistry {} - -#[derive(Default)] -pub struct SnippetRegistry { - snippets: RwLock>>>, -} - -impl SnippetRegistry { - pub fn global(cx: &App) -> Arc { - GlobalSnippetRegistry::global(cx).0.clone() - } - - pub fn try_global(cx: &App) -> Option> { - cx.try_global::() - .map(|registry| registry.0.clone()) - } - - pub fn init_global(cx: &mut App) { - GlobalSnippetRegistry::set_global(cx, GlobalSnippetRegistry(Arc::new(Self::new()))) - } - - pub fn new() -> Self { - Self { - snippets: RwLock::new(HashMap::default()), - } - } - - pub fn register_snippets(&self, file_path: &Path, contents: &str) -> Result<()> { - let snippets_in_file: crate::format::VsSnippetsFile = - serde_json_lenient::from_str(contents)?; - let kind = file_path - .file_stem() - .and_then(|stem| stem.to_str().and_then(file_stem_to_key)); - let snippets = crate::file_to_snippets(snippets_in_file); - self.snippets.write().insert(kind, snippets); - - Ok(()) - } - - pub fn get_snippets(&self, kind: &SnippetKind) -> Vec> { - self.snippets.read().get(kind).cloned().unwrap_or_default() - } -} diff --git a/crates/snippets_ui/Cargo.toml b/crates/snippets_ui/Cargo.toml deleted file mode 100644 index 3139a41dad..0000000000 --- a/crates/snippets_ui/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "snippets_ui" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/snippets_ui.rs" - -[dependencies] -file_finder.workspace = true -file_icons.workspace = true -fuzzy.workspace = true -gpui.workspace = true -language.workspace = true -paths.workspace = true -picker.workspace = true -settings.workspace = true -ui.workspace = true -util.workspace = true -workspace.workspace = true diff --git a/crates/snippets_ui/LICENSE-GPL b/crates/snippets_ui/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/snippets_ui/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/snippets_ui/src/snippets_ui.rs b/crates/snippets_ui/src/snippets_ui.rs deleted file mode 100644 index cfe41144ba..0000000000 --- a/crates/snippets_ui/src/snippets_ui.rs +++ /dev/null @@ -1,364 +0,0 @@ -use file_finder::file_finder_settings::FileFinderSettings; -use file_icons::FileIcons; -use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; -use gpui::{ - App, Context, DismissEvent, Entity, EventEmitter, Focusable, ParentElement, Render, Styled, - WeakEntity, Window, actions, -}; -use language::{LanguageMatcher, LanguageName, LanguageRegistry}; -use paths::snippets_dir; -use picker::{Picker, PickerDelegate}; -use settings::Settings; -use std::{ - borrow::{Borrow, Cow}, - collections::HashSet, - fs, - path::Path, - sync::Arc, -}; -use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*}; -use util::ResultExt; -use workspace::{ModalView, OpenOptions, OpenVisible, Workspace, notifications::NotifyResultExt}; - -#[derive(Eq, Hash, PartialEq)] -struct ScopeName(Cow<'static, str>); - -struct ScopeFileName(Cow<'static, str>); - -impl ScopeFileName { - fn with_extension(self) -> String { - format!("{}.json", self.0) - } -} - -const GLOBAL_SCOPE_NAME: &str = "global"; -const GLOBAL_SCOPE_FILE_NAME: &str = "snippets"; - -impl From for ScopeFileName { - fn from(value: ScopeName) -> Self { - if value.0 == GLOBAL_SCOPE_NAME { - ScopeFileName(Cow::Borrowed(GLOBAL_SCOPE_FILE_NAME)) - } else { - ScopeFileName(value.0) - } - } -} - -impl From for ScopeName { - fn from(value: ScopeFileName) -> Self { - if value.0 == GLOBAL_SCOPE_FILE_NAME { - ScopeName(Cow::Borrowed(GLOBAL_SCOPE_NAME)) - } else { - ScopeName(value.0) - } - } -} - -actions!( - snippets, - [ - /// Opens the snippets configuration file. - ConfigureSnippets, - /// Opens the snippets folder in the file manager. - OpenFolder - ] -); - -pub fn init(cx: &mut App) { - cx.observe_new(register).detach(); -} - -fn register(workspace: &mut Workspace, _window: Option<&mut Window>, _: &mut Context) { - workspace.register_action(configure_snippets); - workspace.register_action(open_folder); -} - -fn configure_snippets( - workspace: &mut Workspace, - _: &ConfigureSnippets, - window: &mut Window, - cx: &mut Context, -) { - let language_registry = workspace.app_state().languages.clone(); - let workspace_handle = workspace.weak_handle(); - - workspace.toggle_modal(window, cx, move |window, cx| { - ScopeSelector::new(language_registry, workspace_handle, window, cx) - }); -} - -fn open_folder( - workspace: &mut Workspace, - _: &OpenFolder, - _: &mut Window, - cx: &mut Context, -) { - fs::create_dir_all(snippets_dir()).notify_err(workspace, cx); - cx.open_with_system(snippets_dir().borrow()); -} - -pub struct ScopeSelector { - picker: Entity>, -} - -impl ScopeSelector { - fn new( - language_registry: Arc, - workspace: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let delegate = - ScopeSelectorDelegate::new(workspace, cx.entity().downgrade(), language_registry); - - let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); - - Self { picker } - } -} - -impl ModalView for ScopeSelector {} - -impl EventEmitter for ScopeSelector {} - -impl Focusable for ScopeSelector { - fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { - self.picker.focus_handle(cx) - } -} - -impl Render for ScopeSelector { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - v_flex().w(rems(34.)).child(self.picker.clone()) - } -} - -pub struct ScopeSelectorDelegate { - workspace: WeakEntity, - scope_selector: WeakEntity, - language_registry: Arc, - candidates: Vec, - matches: Vec, - selected_index: usize, - existing_scopes: HashSet, -} - -impl ScopeSelectorDelegate { - fn new( - workspace: WeakEntity, - scope_selector: WeakEntity, - language_registry: Arc, - ) -> Self { - let languages = language_registry.language_names().into_iter(); - - let candidates = std::iter::once(LanguageName::new(GLOBAL_SCOPE_NAME)) - .chain(languages) - .enumerate() - .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name.as_ref())) - .collect::>(); - - let mut existing_scopes = HashSet::new(); - - if let Some(read_dir) = fs::read_dir(snippets_dir()).log_err() { - for entry in read_dir { - if let Some(entry) = entry.log_err() { - let path = entry.path(); - if let (Some(stem), Some(extension)) = (path.file_stem(), path.extension()) - && extension.to_os_string().to_str() == Some("json") - && let Ok(file_name) = stem.to_os_string().into_string() - { - existing_scopes - .insert(ScopeName::from(ScopeFileName(Cow::Owned(file_name)))); - } - } - } - } - - Self { - workspace, - scope_selector, - language_registry, - candidates, - matches: Vec::new(), - selected_index: 0, - existing_scopes, - } - } - - fn scope_icon(&self, matcher: &LanguageMatcher, cx: &App) -> Option { - matcher - .path_suffixes - .iter() - .find_map(|extension| FileIcons::get_icon(Path::new(extension), cx)) - .or(FileIcons::get(cx).get_icon_for_type("default", cx)) - .map(Icon::from_path) - .map(|icon| icon.color(Color::Muted)) - } -} - -impl PickerDelegate for ScopeSelectorDelegate { - type ListItem = ListItem; - - fn placeholder_text(&self, _window: &mut Window, _: &mut App) -> Arc { - "Select snippet scope...".into() - } - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { - if let Some(mat) = self.matches.get(self.selected_index) { - let scope_name = self.candidates[mat.candidate_id].string.clone(); - let language = self.language_registry.language_for_name(&scope_name); - - if let Some(workspace) = self.workspace.upgrade() { - cx.spawn_in(window, async move |_, cx| { - let scope_file_name = ScopeFileName(match scope_name.to_lowercase().as_str() { - GLOBAL_SCOPE_NAME => Cow::Borrowed(GLOBAL_SCOPE_FILE_NAME), - _ => Cow::Owned(language.await?.lsp_id()), - }); - - workspace.update_in(cx, |workspace, window, cx| { - workspace - .with_local_workspace(window, cx, |workspace, window, cx| { - workspace - .open_abs_path( - snippets_dir().join(scope_file_name.with_extension()), - OpenOptions { - visible: Some(OpenVisible::None), - ..Default::default() - }, - window, - cx, - ) - .detach(); - }) - .detach(); - }) - }) - .detach_and_log_err(cx); - }; - } - self.dismissed(window, cx); - } - - fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { - self.scope_selector - .update(cx, |_, cx| cx.emit(DismissEvent)) - .log_err(); - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index( - &mut self, - ix: usize, - _window: &mut Window, - _: &mut Context>, - ) { - self.selected_index = ix; - } - - fn update_matches( - &mut self, - query: String, - window: &mut Window, - cx: &mut Context>, - ) -> gpui::Task<()> { - let background = cx.background_executor().clone(); - let candidates = self.candidates.clone(); - cx.spawn_in(window, async move |this, cx| { - let matches = if query.is_empty() { - candidates - .into_iter() - .enumerate() - .map(|(index, candidate)| StringMatch { - candidate_id: index, - string: candidate.string, - positions: Vec::new(), - score: 0.0, - }) - .collect() - } else { - match_strings( - &candidates, - &query, - false, - true, - 100, - &Default::default(), - background, - ) - .await - }; - - this.update(cx, |this, cx| { - let delegate = &mut this.delegate; - delegate.matches = matches; - delegate.selected_index = delegate - .selected_index - .min(delegate.matches.len().saturating_sub(1)); - cx.notify(); - }) - .log_err(); - }) - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _window: &mut Window, - cx: &mut Context>, - ) -> Option { - let mat = &self.matches.get(ix)?; - let name_label = mat.string.clone(); - - let scope_name = ScopeName(Cow::Owned( - LanguageName::new(&self.candidates[mat.candidate_id].string).lsp_id(), - )); - let file_label = if self.existing_scopes.contains(&scope_name) { - Some(ScopeFileName::from(scope_name).with_extension()) - } else { - None - }; - - let language_icon = if FileFinderSettings::get_global(cx).file_icons { - let language_name = LanguageName::new(mat.string.as_str()); - self.language_registry - .available_language_for_name(language_name.as_ref()) - .and_then(|available_language| self.scope_icon(available_language.matcher(), cx)) - .or_else(|| { - Some( - Icon::from_path(IconName::ToolWeb.path()) - .map(|icon| icon.color(Color::Muted)), - ) - }) - } else { - None - }; - - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .start_slot::(language_icon) - .child( - h_flex() - .gap_x_2() - .child(HighlightedLabel::new(name_label, mat.positions.clone())) - .when_some(file_label, |item, path_label| { - item.child( - Label::new(path_label) - .color(Color::Muted) - .size(LabelSize::Small), - ) - }), - ), - ) - } -} diff --git a/crates/sqlez/.gitignore b/crates/sqlez/.gitignore deleted file mode 100644 index 8130c3ab47..0000000000 --- a/crates/sqlez/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -debug/ -target/ diff --git a/crates/sqlez/Cargo.toml b/crates/sqlez/Cargo.toml deleted file mode 100644 index 5f4a0bef67..0000000000 --- a/crates/sqlez/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "sqlez" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[dependencies] -anyhow.workspace = true -collections.workspace = true -futures.workspace = true -indoc.workspace = true -libsqlite3-sys.workspace = true -log.workspace = true -parking_lot.workspace = true -smol.workspace = true -sqlformat.workspace = true -thread_local = "1.1.4" -util.workspace = true -uuid.workspace = true diff --git a/crates/sqlez/LICENSE-GPL b/crates/sqlez/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/sqlez/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/sqlez/src/bindable.rs b/crates/sqlez/src/bindable.rs deleted file mode 100644 index 9370389ff4..0000000000 --- a/crates/sqlez/src/bindable.rs +++ /dev/null @@ -1,466 +0,0 @@ -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; - -use anyhow::{Context as _, Result}; -use util::paths::PathExt; - -use crate::statement::{SqlType, Statement}; - -/// Define the number of columns that a type occupies in a query/database -pub trait StaticColumnCount { - fn column_count() -> usize { - 1 - } -} - -/// Bind values of different types to placeholders in a prepared SQL statement. -pub trait Bind { - fn bind(&self, statement: &Statement, start_index: i32) -> Result; -} - -pub trait Column: Sized { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)>; -} - -impl StaticColumnCount for bool {} -impl Bind for bool { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - statement - .bind(&self.then_some(1).unwrap_or(0), start_index) - .with_context(|| format!("Failed to bind bool at index {start_index}")) - } -} - -impl Column for bool { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - i32::column(statement, start_index) - .map(|(i, next_index)| (i != 0, next_index)) - .with_context(|| format!("Failed to read bool at index {start_index}")) - } -} - -impl StaticColumnCount for &[u8] {} -impl Bind for &[u8] { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - statement - .bind_blob(start_index, self) - .with_context(|| format!("Failed to bind &[u8] at index {start_index}"))?; - Ok(start_index + 1) - } -} - -impl StaticColumnCount for &[u8; C] {} -impl Bind for &[u8; C] { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - statement - .bind_blob(start_index, self.as_slice()) - .with_context(|| format!("Failed to bind &[u8; C] at index {start_index}"))?; - Ok(start_index + 1) - } -} - -impl Column for [u8; C] { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let bytes_slice = statement.column_blob(start_index)?; - let array = bytes_slice.try_into()?; - Ok((array, start_index + 1)) - } -} - -impl StaticColumnCount for Vec {} -impl Bind for Vec { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - statement - .bind_blob(start_index, self) - .with_context(|| format!("Failed to bind Vec at index {start_index}"))?; - Ok(start_index + 1) - } -} - -impl Column for Vec { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let result = statement - .column_blob(start_index) - .with_context(|| format!("Failed to read Vec at index {start_index}"))?; - - Ok((Vec::from(result), start_index + 1)) - } -} - -impl StaticColumnCount for f64 {} -impl Bind for f64 { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - statement - .bind_double(start_index, *self) - .with_context(|| format!("Failed to bind f64 at index {start_index}"))?; - Ok(start_index + 1) - } -} - -impl Column for f64 { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let result = statement - .column_double(start_index) - .with_context(|| format!("Failed to parse f64 at index {start_index}"))?; - - Ok((result, start_index + 1)) - } -} - -impl StaticColumnCount for f32 {} -impl Bind for f32 { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - statement - .bind_double(start_index, *self as f64) - .with_context(|| format!("Failed to bind f64 at index {start_index}"))?; - Ok(start_index + 1) - } -} - -impl Column for f32 { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let result = statement - .column_double(start_index) - .with_context(|| format!("Failed to parse f32 at index {start_index}"))? - as f32; - - Ok((result, start_index + 1)) - } -} - -impl StaticColumnCount for i32 {} -impl Bind for i32 { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - statement - .bind_int(start_index, *self) - .with_context(|| format!("Failed to bind i32 at index {start_index}"))?; - - Ok(start_index + 1) - } -} - -impl Column for i32 { - fn column<'a>(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let result = statement.column_int(start_index)?; - Ok((result, start_index + 1)) - } -} - -impl StaticColumnCount for i64 {} -impl Bind for i64 { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - statement - .bind_int64(start_index, *self) - .with_context(|| format!("Failed to bind i64 at index {start_index}"))?; - Ok(start_index + 1) - } -} - -impl Column for i64 { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let result = statement.column_int64(start_index)?; - Ok((result, start_index + 1)) - } -} - -impl StaticColumnCount for u64 {} -impl Bind for u64 { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - statement - .bind_int64(start_index, (*self) as i64) - .with_context(|| format!("Failed to bind i64 at index {start_index}"))?; - Ok(start_index + 1) - } -} - -impl Column for u64 { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let result = statement.column_int64(start_index)? as u64; - Ok((result, start_index + 1)) - } -} - -impl StaticColumnCount for u32 {} -impl Bind for u32 { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - (*self as i64) - .bind(statement, start_index) - .with_context(|| format!("Failed to bind usize at index {start_index}")) - } -} - -impl Column for u32 { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let result = statement.column_int64(start_index)?; - Ok((result as u32, start_index + 1)) - } -} - -impl StaticColumnCount for u16 {} -impl Bind for u16 { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - (*self as i64) - .bind(statement, start_index) - .with_context(|| format!("Failed to bind usize at index {start_index}")) - } -} - -impl Column for u16 { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let result = statement.column_int64(start_index)?; - Ok((result as u16, start_index + 1)) - } -} - -impl StaticColumnCount for usize {} -impl Bind for usize { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - (*self as i64) - .bind(statement, start_index) - .with_context(|| format!("Failed to bind usize at index {start_index}")) - } -} - -impl Column for usize { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let result = statement.column_int64(start_index)?; - Ok((result as usize, start_index + 1)) - } -} - -impl StaticColumnCount for &str {} -impl Bind for &str { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - statement.bind_text(start_index, self)?; - Ok(start_index + 1) - } -} - -impl StaticColumnCount for Arc {} -impl Bind for Arc { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - statement.bind_text(start_index, self.as_ref())?; - Ok(start_index + 1) - } -} - -impl StaticColumnCount for String {} -impl Bind for String { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - statement.bind_text(start_index, self)?; - Ok(start_index + 1) - } -} - -impl Column for Arc { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let result = statement.column_text(start_index)?; - Ok((Arc::from(result), start_index + 1)) - } -} - -impl Column for String { - fn column<'a>(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let result = statement.column_text(start_index)?; - Ok((result.to_owned(), start_index + 1)) - } -} - -impl StaticColumnCount for Option { - fn column_count() -> usize { - T::column_count() - } -} -impl Bind for Option { - fn bind(&self, statement: &Statement, mut start_index: i32) -> Result { - if let Some(this) = self { - this.bind(statement, start_index) - } else { - for _ in 0..T::column_count() { - statement.bind_null(start_index)?; - start_index += 1; - } - Ok(start_index) - } - } -} - -impl Column for Option { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - if let SqlType::Null = statement.column_type(start_index)? { - Ok((None, start_index + T::column_count() as i32)) - } else { - T::column(statement, start_index).map(|(result, next_index)| (Some(result), next_index)) - } - } -} - -impl StaticColumnCount for [T; COUNT] { - fn column_count() -> usize { - T::column_count() * COUNT - } -} -impl Bind for [T; COUNT] { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - let mut current_index = start_index; - for binding in self { - current_index = binding.bind(statement, current_index)? - } - - Ok(current_index) - } -} - -impl StaticColumnCount for &Path {} -impl Bind for &Path { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - self.as_os_str() - .as_encoded_bytes() - .bind(statement, start_index) - } -} - -impl StaticColumnCount for Arc {} -impl Bind for Arc { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - self.as_ref().bind(statement, start_index) - } -} -impl Column for Arc { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let blob = statement.column_blob(start_index)?; - - PathBuf::try_from_bytes(blob).map(|path| (Arc::from(path.as_path()), start_index + 1)) - } -} - -impl StaticColumnCount for PathBuf {} -impl Bind for PathBuf { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - (self.as_ref() as &Path).bind(statement, start_index) - } -} - -impl Column for PathBuf { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let blob = statement.column_blob(start_index)?; - - PathBuf::try_from_bytes(blob).map(|path| (path, start_index + 1)) - } -} - -impl StaticColumnCount for uuid::Uuid { - fn column_count() -> usize { - 1 - } -} - -impl Bind for uuid::Uuid { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - self.as_bytes().bind(statement, start_index) - } -} - -impl Column for uuid::Uuid { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let (bytes, next_index) = Column::column(statement, start_index)?; - Ok((uuid::Uuid::from_bytes(bytes), next_index)) - } -} - -impl StaticColumnCount for () { - fn column_count() -> usize { - 0 - } -} -/// Unit impls do nothing. This simplifies query macros -impl Bind for () { - fn bind(&self, _statement: &Statement, start_index: i32) -> Result { - Ok(start_index) - } -} - -impl Column for () { - fn column(_statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - Ok(((), start_index)) - } -} - -macro_rules! impl_tuple_row_traits { - ( $($local:ident: $type:ident),+ ) => { - impl<$($type: StaticColumnCount),+> StaticColumnCount for ($($type,)+) { - fn column_count() -> usize { - let mut count = 0; - $(count += $type::column_count();)+ - count - } - } - - impl<$($type: Bind),+> Bind for ($($type,)+) { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - let mut next_index = start_index; - let ($($local,)+) = self; - $(next_index = $local.bind(statement, next_index)?;)+ - Ok(next_index) - } - } - - impl<$($type: Column),+> Column for ($($type,)+) { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let mut next_index = start_index; - Ok(( - ( - $({ - let value; - (value, next_index) = $type::column(statement, next_index)?; - value - },)+ - ), - next_index, - )) - } - } - } -} - -impl_tuple_row_traits!(t1: T1, t2: T2); -impl_tuple_row_traits!(t1: T1, t2: T2, t3: T3); -impl_tuple_row_traits!(t1: T1, t2: T2, t3: T3, t4: T4); -impl_tuple_row_traits!(t1: T1, t2: T2, t3: T3, t4: T4, t5: T5); -impl_tuple_row_traits!(t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6); -impl_tuple_row_traits!(t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7); -impl_tuple_row_traits!( - t1: T1, - t2: T2, - t3: T3, - t4: T4, - t5: T5, - t6: T6, - t7: T7, - t8: T8 -); -impl_tuple_row_traits!( - t1: T1, - t2: T2, - t3: T3, - t4: T4, - t5: T5, - t6: T6, - t7: T7, - t8: T8, - t9: T9 -); -impl_tuple_row_traits!( - t1: T1, - t2: T2, - t3: T3, - t4: T4, - t5: T5, - t6: T6, - t7: T7, - t8: T8, - t9: T9, - t10: T10 -); diff --git a/crates/sqlez/src/connection.rs b/crates/sqlez/src/connection.rs deleted file mode 100644 index 53f0d4e261..0000000000 --- a/crates/sqlez/src/connection.rs +++ /dev/null @@ -1,445 +0,0 @@ -use std::{ - cell::RefCell, - ffi::{CStr, CString}, - marker::PhantomData, - path::Path, - ptr, -}; - -use anyhow::Result; -use libsqlite3_sys::*; - -pub struct Connection { - pub(crate) sqlite3: *mut sqlite3, - persistent: bool, - pub(crate) write: RefCell, - _sqlite: PhantomData, -} -unsafe impl Send for Connection {} - -impl Connection { - pub(crate) fn open(uri: &str, persistent: bool) -> Result { - let mut connection = Self { - sqlite3: ptr::null_mut(), - persistent, - write: RefCell::new(true), - _sqlite: PhantomData, - }; - - let flags = SQLITE_OPEN_CREATE | SQLITE_OPEN_NOMUTEX | SQLITE_OPEN_READWRITE; - unsafe { - sqlite3_open_v2( - CString::new(uri)?.as_ptr(), - &mut connection.sqlite3, - flags, - ptr::null(), - ); - - // Turn on extended error codes - sqlite3_extended_result_codes(connection.sqlite3, 1); - - connection.last_error()?; - } - - Ok(connection) - } - - /// Attempts to open the database at uri. If it fails, a shared memory db will be opened - /// instead. - pub fn open_file(uri: &str) -> Self { - Self::open(uri, true).unwrap_or_else(|_| Self::open_memory(Some(uri))) - } - - pub fn open_memory(uri: Option<&str>) -> Self { - let in_memory_path = if let Some(uri) = uri { - format!("file:{}?mode=memory&cache=shared", uri) - } else { - ":memory:".to_string() - }; - - Self::open(&in_memory_path, false).expect("Could not create fallback in memory db") - } - - pub fn persistent(&self) -> bool { - self.persistent - } - - pub fn can_write(&self) -> bool { - *self.write.borrow() - } - - pub fn backup_main(&self, destination: &Connection) -> Result<()> { - unsafe { - let backup = sqlite3_backup_init( - destination.sqlite3, - CString::new("main")?.as_ptr(), - self.sqlite3, - CString::new("main")?.as_ptr(), - ); - sqlite3_backup_step(backup, -1); - sqlite3_backup_finish(backup); - destination.last_error() - } - } - - pub fn backup_main_to(&self, destination: impl AsRef) -> Result<()> { - let destination = Self::open_file(destination.as_ref().to_string_lossy().as_ref()); - self.backup_main(&destination) - } - - pub fn sql_has_syntax_error(&self, sql: &str) -> Option<(String, usize)> { - let sql = CString::new(sql).unwrap(); - let mut remaining_sql = sql.as_c_str(); - let sql_start = remaining_sql.as_ptr(); - - let mut alter_table = None; - while { - let remaining_sql_str = remaining_sql.to_str().unwrap().trim(); - let any_remaining_sql = remaining_sql_str != ";" && !remaining_sql_str.is_empty(); - if any_remaining_sql { - alter_table = parse_alter_table(remaining_sql_str); - } - any_remaining_sql - } { - let mut raw_statement = ptr::null_mut::(); - let mut remaining_sql_ptr = ptr::null(); - - let (res, offset, message, _conn) = if let Some((table_to_alter, column)) = alter_table - { - // ALTER TABLE is a weird statement. When preparing the statement the table's - // existence is checked *before* syntax checking any other part of the statement. - // Therefore, we need to make sure that the table has been created before calling - // prepare. As we don't want to trash whatever database this is connected to, we - // create a new in-memory DB to test. - - let temp_connection = Connection::open_memory(None); - //This should always succeed, if it doesn't then you really should know about it - temp_connection - .exec(&format!("CREATE TABLE {table_to_alter}({column})")) - .unwrap()() - .unwrap(); - - unsafe { - sqlite3_prepare_v2( - temp_connection.sqlite3, - remaining_sql.as_ptr(), - -1, - &mut raw_statement, - &mut remaining_sql_ptr, - ) - }; - - #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] - let offset = unsafe { sqlite3_error_offset(temp_connection.sqlite3) }; - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - let offset = 0; - - unsafe { - ( - sqlite3_errcode(temp_connection.sqlite3), - offset, - sqlite3_errmsg(temp_connection.sqlite3), - Some(temp_connection), - ) - } - } else { - unsafe { - sqlite3_prepare_v2( - self.sqlite3, - remaining_sql.as_ptr(), - -1, - &mut raw_statement, - &mut remaining_sql_ptr, - ) - }; - - #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] - let offset = unsafe { sqlite3_error_offset(self.sqlite3) }; - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - let offset = 0; - - unsafe { - ( - sqlite3_errcode(self.sqlite3), - offset, - sqlite3_errmsg(self.sqlite3), - None, - ) - } - }; - - unsafe { sqlite3_finalize(raw_statement) }; - - if res == 1 && offset >= 0 { - let sub_statement_correction = remaining_sql.as_ptr() as usize - sql_start as usize; - let err_msg = String::from_utf8_lossy(unsafe { - CStr::from_ptr(message as *const _).to_bytes() - }) - .into_owned(); - - return Some((err_msg, offset as usize + sub_statement_correction)); - } - remaining_sql = unsafe { CStr::from_ptr(remaining_sql_ptr) }; - alter_table = None; - } - None - } - - pub(crate) fn last_error(&self) -> Result<()> { - unsafe { - let code = sqlite3_errcode(self.sqlite3); - const NON_ERROR_CODES: &[i32] = &[SQLITE_OK, SQLITE_ROW]; - if NON_ERROR_CODES.contains(&code) { - return Ok(()); - } - - let message = sqlite3_errmsg(self.sqlite3); - let message = if message.is_null() { - None - } else { - Some( - String::from_utf8_lossy(CStr::from_ptr(message as *const _).to_bytes()) - .into_owned(), - ) - }; - - anyhow::bail!("Sqlite call failed with code {code} and message: {message:?}") - } - } - - pub(crate) fn with_write(&self, callback: impl FnOnce(&Connection) -> T) -> T { - *self.write.borrow_mut() = true; - let result = callback(self); - *self.write.borrow_mut() = false; - result - } -} - -fn parse_alter_table(remaining_sql_str: &str) -> Option<(String, String)> { - let remaining_sql_str = remaining_sql_str.to_lowercase(); - if remaining_sql_str.starts_with("alter") - && let Some(table_offset) = remaining_sql_str.find("table") - { - let after_table_offset = table_offset + "table".len(); - let table_to_alter = remaining_sql_str - .chars() - .skip(after_table_offset) - .skip_while(|c| c.is_whitespace()) - .take_while(|c| !c.is_whitespace()) - .collect::(); - if !table_to_alter.is_empty() { - let column_name = if let Some(rename_offset) = remaining_sql_str.find("rename column") { - let after_rename_offset = rename_offset + "rename column".len(); - remaining_sql_str - .chars() - .skip(after_rename_offset) - .skip_while(|c| c.is_whitespace()) - .take_while(|c| !c.is_whitespace()) - .collect::() - } else if let Some(drop_offset) = remaining_sql_str.find("drop column") { - let after_drop_offset = drop_offset + "drop column".len(); - remaining_sql_str - .chars() - .skip(after_drop_offset) - .skip_while(|c| c.is_whitespace()) - .take_while(|c| !c.is_whitespace()) - .collect::() - } else { - "__place_holder_column_for_syntax_checking".to_string() - }; - return Some((table_to_alter, column_name)); - } - } - None -} - -impl Drop for Connection { - fn drop(&mut self) { - unsafe { sqlite3_close(self.sqlite3) }; - } -} - -#[cfg(test)] -mod test { - use anyhow::Result; - use indoc::indoc; - - use crate::connection::Connection; - - #[test] - fn string_round_trips() -> Result<()> { - let connection = Connection::open_memory(Some("string_round_trips")); - connection - .exec(indoc! {" - CREATE TABLE text ( - text TEXT - );"}) - .unwrap()() - .unwrap(); - - let text = "Some test text"; - - connection - .exec_bound("INSERT INTO text (text) VALUES (?);") - .unwrap()(text) - .unwrap(); - - assert_eq!( - connection.select_row("SELECT text FROM text;").unwrap()().unwrap(), - Some(text.to_string()) - ); - - Ok(()) - } - - #[test] - fn tuple_round_trips() { - let connection = Connection::open_memory(Some("tuple_round_trips")); - connection - .exec(indoc! {" - CREATE TABLE test ( - text TEXT, - integer INTEGER, - blob BLOB - );"}) - .unwrap()() - .unwrap(); - - let tuple1 = ("test".to_string(), 64, vec![0, 1, 2, 4, 8, 16, 32, 64]); - let tuple2 = ("test2".to_string(), 32, vec![64, 32, 16, 8, 4, 2, 1, 0]); - - let mut insert = connection - .exec_bound::<(String, usize, Vec)>( - "INSERT INTO test (text, integer, blob) VALUES (?, ?, ?)", - ) - .unwrap(); - - insert(tuple1.clone()).unwrap(); - insert(tuple2.clone()).unwrap(); - - assert_eq!( - connection - .select::<(String, usize, Vec)>("SELECT * FROM test") - .unwrap()() - .unwrap(), - vec![tuple1, tuple2] - ); - } - - #[test] - fn bool_round_trips() { - let connection = Connection::open_memory(Some("bool_round_trips")); - connection - .exec(indoc! {" - CREATE TABLE bools ( - t INTEGER, - f INTEGER - );"}) - .unwrap()() - .unwrap(); - - connection - .exec_bound("INSERT INTO bools(t, f) VALUES (?, ?)") - .unwrap()((true, false)) - .unwrap(); - - assert_eq!( - connection - .select_row::<(bool, bool)>("SELECT * FROM bools;") - .unwrap()() - .unwrap(), - Some((true, false)) - ); - } - - #[test] - fn backup_works() { - let connection1 = Connection::open_memory(Some("backup_works")); - connection1 - .exec(indoc! {" - CREATE TABLE blobs ( - data BLOB - );"}) - .unwrap()() - .unwrap(); - let blob = vec![0, 1, 2, 4, 8, 16, 32, 64]; - connection1 - .exec_bound::>("INSERT INTO blobs (data) VALUES (?);") - .unwrap()(blob.clone()) - .unwrap(); - - // Backup connection1 to connection2 - let connection2 = Connection::open_memory(Some("backup_works_other")); - connection1.backup_main(&connection2).unwrap(); - - // Delete the added blob and verify its deleted on the other side - let read_blobs = connection1 - .select::>("SELECT * FROM blobs;") - .unwrap()() - .unwrap(); - assert_eq!(read_blobs, vec![blob]); - } - - #[test] - fn multi_step_statement_works() { - let connection = Connection::open_memory(Some("multi_step_statement_works")); - - connection - .exec(indoc! {" - CREATE TABLE test ( - col INTEGER - )"}) - .unwrap()() - .unwrap(); - - connection - .exec(indoc! {" - INSERT INTO test(col) VALUES (2)"}) - .unwrap()() - .unwrap(); - - assert_eq!( - connection - .select_row::("SELECT * FROM test") - .unwrap()() - .unwrap(), - Some(2) - ); - } - - #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] - #[test] - fn test_sql_has_syntax_errors() { - let connection = Connection::open_memory(Some("test_sql_has_syntax_errors")); - let first_stmt = - "CREATE TABLE kv_store(key TEXT PRIMARY KEY, value TEXT NOT NULL) STRICT ;"; - let second_stmt = "SELECT FROM"; - - let second_offset = connection.sql_has_syntax_error(second_stmt).unwrap().1; - - let res = connection - .sql_has_syntax_error(&format!("{}\n{}", first_stmt, second_stmt)) - .map(|(_, offset)| offset); - - assert_eq!(res, Some(first_stmt.len() + second_offset + 1)); - } - - #[test] - fn test_alter_table_syntax() { - let connection = Connection::open_memory(Some("test_alter_table_syntax")); - - assert!( - connection - .sql_has_syntax_error("ALTER TABLE test ADD x TEXT") - .is_none() - ); - - assert!( - connection - .sql_has_syntax_error("ALTER TABLE test AAD x TEXT") - .is_some() - ); - } -} diff --git a/crates/sqlez/src/domain.rs b/crates/sqlez/src/domain.rs deleted file mode 100644 index 5744a67da2..0000000000 --- a/crates/sqlez/src/domain.rs +++ /dev/null @@ -1,64 +0,0 @@ -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 - } -} - -pub trait Migrator: 'static { - fn migrate(connection: &Connection) -> anyhow::Result<()>; -} - -impl Migrator for () { - fn migrate(_connection: &Connection) -> anyhow::Result<()> { - Ok(()) // Do nothing - } -} - -impl Migrator for D { - fn migrate(connection: &Connection) -> anyhow::Result<()> { - connection.migrate( - Self::NAME, - Self::MIGRATIONS, - Self::should_allow_migration_change, - ) - } -} - -impl Migrator for (D1, D2) { - fn migrate(connection: &Connection) -> anyhow::Result<()> { - D1::migrate(connection)?; - D2::migrate(connection) - } -} - -impl Migrator for (D1, D2, D3) { - fn migrate(connection: &Connection) -> anyhow::Result<()> { - D1::migrate(connection)?; - D2::migrate(connection)?; - D3::migrate(connection) - } -} - -impl Migrator for (D1, D2, D3, D4) { - fn migrate(connection: &Connection) -> anyhow::Result<()> { - D1::migrate(connection)?; - D2::migrate(connection)?; - D3::migrate(connection)?; - D4::migrate(connection) - } -} - -impl Migrator for (D1, D2, D3, D4, D5) { - fn migrate(connection: &Connection) -> anyhow::Result<()> { - D1::migrate(connection)?; - D2::migrate(connection)?; - D3::migrate(connection)?; - D4::migrate(connection)?; - D5::migrate(connection) - } -} diff --git a/crates/sqlez/src/lib.rs b/crates/sqlez/src/lib.rs deleted file mode 100644 index a22cfff2b3..0000000000 --- a/crates/sqlez/src/lib.rs +++ /dev/null @@ -1,11 +0,0 @@ -pub mod bindable; -pub mod connection; -pub mod domain; -pub mod migrations; -pub mod savepoint; -pub mod statement; -pub mod thread_safe_connection; -pub mod typed_statements; -mod util; - -pub use anyhow; diff --git a/crates/sqlez/src/migrations.rs b/crates/sqlez/src/migrations.rs deleted file mode 100644 index 567d82f9af..0000000000 --- a/crates/sqlez/src/migrations.rs +++ /dev/null @@ -1,390 +0,0 @@ -// Migrations are constructed by domain, and stored in a table in the connection db with domain name, -// effected tables, actual query text, and order. -// If a migration is run and any of the query texts don't match, the app panics on startup (maybe fallback -// to creating a new db?) -// Otherwise any missing migrations are run on the connection - -use std::ffi::CString; - -use anyhow::{Context as _, Result}; -use indoc::{formatdoc, indoc}; -use libsqlite3_sys::sqlite3_exec; - -use crate::connection::Connection; - -impl Connection { - fn eager_exec(&self, sql: &str) -> anyhow::Result<()> { - let sql_str = CString::new(sql).context("Error creating cstr")?; - unsafe { - sqlite3_exec( - self.sqlite3, - sql_str.as_c_str().as_ptr(), - None, - std::ptr::null_mut(), - std::ptr::null_mut(), - ); - } - self.last_error() - .with_context(|| format!("Prepare call failed for query:\n{}", sql))?; - - Ok(()) - } - - /// Migrate the database, for the given domain. - /// 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<()> { - self.with_savepoint("migrating", || { - // Setup the migrations table unconditionally - self.exec(indoc! {" - CREATE TABLE IF NOT EXISTS migrations ( - domain TEXT, - step INTEGER, - migration TEXT - )"})?()?; - - let completed_migrations = - self.select_bound::<&str, (String, usize, String)>(indoc! {" - SELECT domain, step, migration FROM migrations - WHERE domain = ? - ORDER BY step - "})?(domain)?; - - let mut store_completed_migration = self - .exec_bound("INSERT INTO migrations (domain, step, migration) VALUES (?, ?, ?)")?; - - let mut did_migrate = false; - for (index, migration) in migrations.iter().enumerate() { - let migration = - sqlformat::format(migration, &sqlformat::QueryParams::None, Default::default()); - if let Some((_, _, completed_migration)) = completed_migrations.get(index) { - // Reformat completed migrations with the current `sqlformat` version, so that past migrations stored - // conform to the new formatting rules. - let completed_migration = sqlformat::format( - completed_migration, - &sqlformat::QueryParams::None, - Default::default(), - ); - 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} - - Stored migration: - {completed_migration} - - Proposed migration: - {migration}"}); - } - } - - self.eager_exec(&migration)?; - did_migrate = true; - store_completed_migration((domain, index, migration))?; - } - - if did_migrate { - self.delete_rows_with_orphaned_foreign_key_references()?; - self.exec("PRAGMA foreign_key_check;")?()?; - } - - Ok(()) - }) - } - - /// Delete any rows that were orphaned by a migration. This is needed - /// because we disable foreign key constraints during migrations, so - /// that it's possible to re-create a table with the same name, without - /// deleting all associated data. - fn delete_rows_with_orphaned_foreign_key_references(&self) -> Result<()> { - let foreign_key_info: Vec<(String, String, String, String)> = self.select( - r#" - SELECT DISTINCT - schema.name as child_table, - foreign_keys.[from] as child_key, - foreign_keys.[table] as parent_table, - foreign_keys.[to] as parent_key - FROM sqlite_schema schema - JOIN pragma_foreign_key_list(schema.name) foreign_keys - WHERE - schema.type = 'table' AND - schema.name NOT LIKE "sqlite_%" - "#, - )?()?; - - if !foreign_key_info.is_empty() { - log::info!( - "Found {} foreign key relationships to check", - foreign_key_info.len() - ); - } - - for (child_table, child_key, parent_table, parent_key) in foreign_key_info { - self.exec(&format!( - " - DELETE FROM {child_table} - WHERE {child_key} IS NOT NULL and {child_key} NOT IN - (SELECT {parent_key} FROM {parent_table}) - " - ))?()?; - } - - Ok(()) - } -} - -#[cfg(test)] -mod test { - use indoc::indoc; - - use crate::connection::Connection; - - #[test] - fn test_migrations_are_added_to_table() { - let connection = Connection::open_memory(Some("migrations_are_added_to_table")); - - // Create first migration with a single step and run it - connection - .migrate( - "test", - &[indoc! {" - CREATE TABLE test1 ( - a TEXT, - b TEXT - )"}], - disallow_migration_change, - ) - .unwrap(); - - // Verify it got added to the migrations table - assert_eq!( - &connection - .select::("SELECT (migration) FROM migrations") - .unwrap()() - .unwrap()[..], - &[indoc! {"CREATE TABLE test1 (a TEXT, b TEXT)"}], - ); - - // Add another step to the migration and run it again - connection - .migrate( - "test", - &[ - indoc! {" - CREATE TABLE test1 ( - a TEXT, - b TEXT - )"}, - indoc! {" - CREATE TABLE test2 ( - c TEXT, - d TEXT - )"}, - ], - disallow_migration_change, - ) - .unwrap(); - - // Verify it is also added to the migrations table - assert_eq!( - &connection - .select::("SELECT (migration) FROM migrations") - .unwrap()() - .unwrap()[..], - &[ - indoc! {"CREATE TABLE test1 (a TEXT, b TEXT)"}, - indoc! {"CREATE TABLE test2 (c TEXT, d TEXT)"}, - ], - ); - } - - #[test] - fn test_migration_setup_works() { - let connection = Connection::open_memory(Some("migration_setup_works")); - - connection - .exec(indoc! {" - CREATE TABLE IF NOT EXISTS migrations ( - domain TEXT, - step INTEGER, - migration TEXT - );"}) - .unwrap()() - .unwrap(); - - let mut store_completed_migration = connection - .exec_bound::<(&str, usize, String)>(indoc! {" - INSERT INTO migrations (domain, step, migration) - VALUES (?, ?, ?)"}) - .unwrap(); - - let domain = "test_domain"; - for i in 0..5 { - // Create a table forcing a schema change - connection - .exec(&format!("CREATE TABLE table{} ( test TEXT );", i)) - .unwrap()() - .unwrap(); - - store_completed_migration((domain, i, i.to_string())).unwrap(); - } - } - - #[test] - fn migrations_dont_rerun() { - let connection = Connection::open_memory(Some("migrations_dont_rerun")); - - // Create migration which clears a table - - // Manually create the table for that migration with a row - connection - .exec(indoc! {" - CREATE TABLE test_table ( - test_column INTEGER - );"}) - .unwrap()() - .unwrap(); - connection - .exec(indoc! {" - INSERT INTO test_table (test_column) VALUES (1);"}) - .unwrap()() - .unwrap(); - - assert_eq!( - connection - .select_row::("SELECT * FROM test_table") - .unwrap()() - .unwrap(), - Some(1) - ); - - // Run the migration verifying that the row got dropped - connection - .migrate( - "test", - &["DELETE FROM test_table"], - disallow_migration_change, - ) - .unwrap(); - assert_eq!( - connection - .select_row::("SELECT * FROM test_table") - .unwrap()() - .unwrap(), - None - ); - - // Recreate the dropped row - connection - .exec("INSERT INTO test_table (test_column) VALUES (2)") - .unwrap()() - .unwrap(); - - // Run the same migration again and verify that the table was left unchanged - connection - .migrate( - "test", - &["DELETE FROM test_table"], - disallow_migration_change, - ) - .unwrap(); - assert_eq!( - connection - .select_row::("SELECT * FROM test_table") - .unwrap()() - .unwrap(), - Some(2) - ); - } - - #[test] - fn changed_migration_fails() { - let connection = Connection::open_memory(Some("changed_migration_fails")); - - // Create a migration with two steps and run it - connection - .migrate( - "test migration", - &[ - "CREATE TABLE test (col INTEGER)", - "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)", - ], - |_, 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 - assert!(second_migration_result.is_err()) - } - - #[test] - fn test_create_alter_drop() { - let connection = Connection::open_memory(Some("test_create_alter_drop")); - - connection - .migrate( - "first_migration", - &["CREATE TABLE table1(a TEXT) STRICT;"], - disallow_migration_change, - ) - .unwrap(); - - connection - .exec("INSERT INTO table1(a) VALUES (\"test text\");") - .unwrap()() - .unwrap(); - - connection - .migrate( - "second_migration", - &[indoc! {" - CREATE TABLE table2(b TEXT) STRICT; - - INSERT INTO table2 (b) - SELECT a FROM table1; - - DROP TABLE table1; - - ALTER TABLE table2 RENAME TO table1; - "}], - disallow_migration_change, - ) - .unwrap(); - - let res = &connection.select::("SELECT b FROM table1").unwrap()().unwrap()[0]; - - assert_eq!(res, "test text"); - } - - fn disallow_migration_change(_: usize, _: &str, _: &str) -> bool { - false - } -} diff --git a/crates/sqlez/src/savepoint.rs b/crates/sqlez/src/savepoint.rs deleted file mode 100644 index 3177cea39f..0000000000 --- a/crates/sqlez/src/savepoint.rs +++ /dev/null @@ -1,150 +0,0 @@ -use anyhow::Result; -use indoc::formatdoc; - -use crate::connection::Connection; - -impl Connection { - // Run a set of commands within the context of a `SAVEPOINT name`. If the callback - // returns Err(_), the savepoint will be rolled back. Otherwise, the save - // point is released. - pub fn with_savepoint(&self, name: impl AsRef, f: F) -> Result - where - F: FnOnce() -> Result, - { - let name = name.as_ref(); - self.exec(&format!("SAVEPOINT {name}"))?()?; - let result = f(); - match result { - Ok(_) => { - self.exec(&format!("RELEASE {name}"))?()?; - } - Err(_) => { - self.exec(&formatdoc! {" - ROLLBACK TO {name}; - RELEASE {name}"})?()?; - } - } - result - } - - // Run a set of commands within the context of a `SAVEPOINT name`. If the callback - // returns Ok(None) or Err(_), the savepoint will be rolled back. Otherwise, the save - // point is released. - pub fn with_savepoint_rollback(&self, name: impl AsRef, f: F) -> Result> - where - F: FnOnce() -> Result>, - { - let name = name.as_ref(); - self.exec(&format!("SAVEPOINT {name}"))?()?; - let result = f(); - match result { - Ok(Some(_)) => { - self.exec(&format!("RELEASE {name}"))?()?; - } - Ok(None) | Err(_) => { - self.exec(&formatdoc! {" - ROLLBACK TO {name}; - RELEASE {name}"})?()?; - } - } - result - } -} - -#[cfg(test)] -mod tests { - use crate::connection::Connection; - use anyhow::Result; - use indoc::indoc; - - #[test] - fn test_nested_savepoints() -> Result<()> { - let connection = Connection::open_memory(Some("nested_savepoints")); - - connection - .exec(indoc! {" - CREATE TABLE text ( - text TEXT, - idx INTEGER - );"}) - .unwrap()() - .unwrap(); - - let save1_text = "test save1"; - let save2_text = "test save2"; - - connection.with_savepoint("first", || { - connection.exec_bound("INSERT INTO text(text, idx) VALUES (?, ?)")?((save1_text, 1))?; - - assert!( - connection - .with_savepoint("second", || -> anyhow::Result> { - connection.exec_bound("INSERT INTO text(text, idx) VALUES (?, ?)")?(( - save2_text, 2, - ))?; - - assert_eq!( - connection - .select::("SELECT text FROM text ORDER BY text.idx ASC")?( - )?, - vec![save1_text, save2_text], - ); - - anyhow::bail!("Failed second save point :(") - }) - .err() - .is_some() - ); - - assert_eq!( - connection.select::("SELECT text FROM text ORDER BY text.idx ASC")?()?, - vec![save1_text], - ); - - connection.with_savepoint_rollback::<(), _>("second", || { - connection.exec_bound("INSERT INTO text(text, idx) VALUES (?, ?)")?(( - save2_text, 2, - ))?; - - assert_eq!( - connection.select::("SELECT text FROM text ORDER BY text.idx ASC")?()?, - vec![save1_text, save2_text], - ); - - Ok(None) - })?; - - assert_eq!( - connection.select::("SELECT text FROM text ORDER BY text.idx ASC")?()?, - vec![save1_text], - ); - - connection.with_savepoint_rollback("second", || { - connection.exec_bound("INSERT INTO text(text, idx) VALUES (?, ?)")?(( - save2_text, 2, - ))?; - - assert_eq!( - connection.select::("SELECT text FROM text ORDER BY text.idx ASC")?()?, - vec![save1_text, save2_text], - ); - - Ok(Some(())) - })?; - - assert_eq!( - connection.select::("SELECT text FROM text ORDER BY text.idx ASC")?()?, - vec![save1_text, save2_text], - ); - - Ok(()) - })?; - - assert_eq!( - connection.select::("SELECT text FROM text ORDER BY text.idx ASC")?()?, - vec![save1_text, save2_text], - ); - - Ok(()) - } -} diff --git a/crates/sqlez/src/statement.rs b/crates/sqlez/src/statement.rs deleted file mode 100644 index d08e58a6f9..0000000000 --- a/crates/sqlez/src/statement.rs +++ /dev/null @@ -1,497 +0,0 @@ -use std::ffi::{CStr, CString, c_int}; -use std::marker::PhantomData; -use std::{ptr, slice, str}; - -use anyhow::{Context as _, Result, bail}; -use libsqlite3_sys::*; - -use crate::bindable::{Bind, Column}; -use crate::connection::Connection; - -pub struct Statement<'a> { - /// vector of pointers to the raw SQLite statement objects. - /// it holds the actual prepared statements that will be executed. - pub raw_statements: Vec<*mut sqlite3_stmt>, - /// Index of the current statement being executed from the `raw_statements` vector. - current_statement: usize, - /// A reference to the database connection. - /// This is used to execute the statements and check for errors. - connection: &'a Connection, - ///Indicates that the `Statement` struct is tied to the lifetime of the SQLite statement - phantom: PhantomData, -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum StepResult { - Row, - Done, -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub enum SqlType { - Text, - Integer, - Blob, - Float, - Null, -} - -impl<'a> Statement<'a> { - pub fn prepare>(connection: &'a Connection, query: T) -> Result { - let mut statement = Self { - raw_statements: Default::default(), - current_statement: 0, - connection, - phantom: PhantomData, - }; - let sql = CString::new(query.as_ref()).context("Error creating cstr")?; - let mut remaining_sql = sql.as_c_str(); - while { - let remaining_sql_str = remaining_sql - .to_str() - .context("Parsing remaining sql")? - .trim(); - remaining_sql_str != ";" && !remaining_sql_str.is_empty() - } { - let mut raw_statement = ptr::null_mut::(); - let mut remaining_sql_ptr = ptr::null(); - unsafe { - sqlite3_prepare_v2( - connection.sqlite3, - remaining_sql.as_ptr(), - -1, - &mut raw_statement, - &mut remaining_sql_ptr, - ) - }; - - connection - .last_error() - .with_context(|| format!("Prepare call failed for query:\n{}", query.as_ref()))?; - - remaining_sql = unsafe { CStr::from_ptr(remaining_sql_ptr) }; - statement.raw_statements.push(raw_statement); - - if !connection.can_write() && unsafe { sqlite3_stmt_readonly(raw_statement) == 0 } { - let sql = unsafe { CStr::from_ptr(sqlite3_sql(raw_statement)) }; - - bail!( - "Write statement prepared with connection that is not write capable. SQL:\n{} ", - sql.to_str()? - ) - } - } - - Ok(statement) - } - - fn current_statement(&self) -> *mut sqlite3_stmt { - *self.raw_statements.get(self.current_statement).unwrap() - } - - pub fn reset(&mut self) { - unsafe { - for raw_statement in self.raw_statements.iter() { - sqlite3_reset(*raw_statement); - } - } - self.current_statement = 0; - } - - pub fn parameter_count(&self) -> i32 { - unsafe { - self.raw_statements - .iter() - .map(|raw_statement| sqlite3_bind_parameter_count(*raw_statement)) - .max() - .unwrap_or(0) - } - } - - fn bind_index_with(&self, index: i32, bind: impl Fn(&*mut sqlite3_stmt)) -> Result<()> { - let mut any_succeed = false; - unsafe { - for raw_statement in self.raw_statements.iter() { - if index <= sqlite3_bind_parameter_count(*raw_statement) { - bind(raw_statement); - self.connection - .last_error() - .with_context(|| format!("Failed to bind value at index {index}"))?; - any_succeed = true; - } else { - continue; - } - } - } - if any_succeed { - Ok(()) - } else { - anyhow::bail!("Failed to bind parameters") - } - } - - pub fn bind_blob(&self, index: i32, blob: &[u8]) -> Result<()> { - let index = index as c_int; - let blob_pointer = blob.as_ptr() as *const _; - let len = blob.len() as c_int; - - self.bind_index_with(index, |raw_statement| unsafe { - sqlite3_bind_blob(*raw_statement, index, blob_pointer, len, SQLITE_TRANSIENT()); - }) - } - - pub fn column_blob(&mut self, index: i32) -> Result<&[u8]> { - let index = index as c_int; - let pointer = unsafe { sqlite3_column_blob(self.current_statement(), index) }; - - self.connection - .last_error() - .with_context(|| format!("Failed to read blob at index {index}"))?; - if pointer.is_null() { - return Ok(&[]); - } - let len = unsafe { sqlite3_column_bytes(self.current_statement(), index) as usize }; - self.connection - .last_error() - .with_context(|| format!("Failed to read length of blob at index {index}"))?; - - unsafe { Ok(slice::from_raw_parts(pointer as *const u8, len)) } - } - - pub fn bind_double(&self, index: i32, double: f64) -> Result<()> { - let index = index as c_int; - - self.bind_index_with(index, |raw_statement| unsafe { - sqlite3_bind_double(*raw_statement, index, double); - }) - } - - pub fn column_double(&self, index: i32) -> Result { - let index = index as c_int; - let result = unsafe { sqlite3_column_double(self.current_statement(), index) }; - self.connection - .last_error() - .with_context(|| format!("Failed to read double at index {index}"))?; - Ok(result) - } - - pub fn bind_int(&self, index: i32, int: i32) -> Result<()> { - let index = index as c_int; - self.bind_index_with(index, |raw_statement| unsafe { - sqlite3_bind_int(*raw_statement, index, int); - }) - } - - pub fn column_int(&self, index: i32) -> Result { - let index = index as c_int; - let result = unsafe { sqlite3_column_int(self.current_statement(), index) }; - self.connection - .last_error() - .with_context(|| format!("Failed to read int at index {index}"))?; - Ok(result) - } - - pub fn bind_int64(&self, index: i32, int: i64) -> Result<()> { - let index = index as c_int; - self.bind_index_with(index, |raw_statement| unsafe { - sqlite3_bind_int64(*raw_statement, index, int); - }) - } - - pub fn column_int64(&self, index: i32) -> Result { - let index = index as c_int; - let result = unsafe { sqlite3_column_int64(self.current_statement(), index) }; - self.connection - .last_error() - .with_context(|| format!("Failed to read i64 at index {index}"))?; - Ok(result) - } - - pub fn bind_null(&self, index: i32) -> Result<()> { - let index = index as c_int; - self.bind_index_with(index, |raw_statement| unsafe { - sqlite3_bind_null(*raw_statement, index); - }) - } - - pub fn bind_text(&self, index: i32, text: &str) -> Result<()> { - let index = index as c_int; - let text_pointer = text.as_ptr() as *const _; - let len = text.len() as c_int; - - self.bind_index_with(index, |raw_statement| unsafe { - sqlite3_bind_text(*raw_statement, index, text_pointer, len, SQLITE_TRANSIENT()); - }) - } - - pub fn column_text(&mut self, index: i32) -> Result<&str> { - let index = index as c_int; - let pointer = unsafe { sqlite3_column_text(self.current_statement(), index) }; - - self.connection - .last_error() - .with_context(|| format!("Failed to read text from column {index}"))?; - if pointer.is_null() { - return Ok(""); - } - let len = unsafe { sqlite3_column_bytes(self.current_statement(), index) as usize }; - self.connection - .last_error() - .with_context(|| format!("Failed to read text length at {index}"))?; - - let slice = unsafe { slice::from_raw_parts(pointer, len) }; - Ok(str::from_utf8(slice)?) - } - - pub fn bind(&self, value: &T, index: i32) -> Result { - debug_assert!(index > 0); - value.bind(self, index) - } - - pub fn column(&mut self) -> Result { - Ok(T::column(self, 0)?.0) - } - - pub fn column_type(&mut self, index: i32) -> Result { - let result = unsafe { sqlite3_column_type(self.current_statement(), index) }; - self.connection.last_error()?; - match result { - SQLITE_INTEGER => Ok(SqlType::Integer), - SQLITE_FLOAT => Ok(SqlType::Float), - SQLITE_TEXT => Ok(SqlType::Text), - SQLITE_BLOB => Ok(SqlType::Blob), - SQLITE_NULL => Ok(SqlType::Null), - _ => anyhow::bail!("Column type returned was incorrect"), - } - } - - pub fn with_bindings(&mut self, bindings: &impl Bind) -> Result<&mut Self> { - self.bind(bindings, 1)?; - Ok(self) - } - - fn step(&mut self) -> Result { - match unsafe { sqlite3_step(self.current_statement()) } { - SQLITE_ROW => Ok(StepResult::Row), - SQLITE_DONE => { - if self.current_statement >= self.raw_statements.len() - 1 { - Ok(StepResult::Done) - } else { - self.current_statement += 1; - self.step() - } - } - SQLITE_MISUSE => anyhow::bail!("Statement step returned SQLITE_MISUSE"), - _other_error => { - self.connection.last_error()?; - unreachable!("Step returned error code and last error failed to catch it"); - } - } - } - - pub fn exec(&mut self) -> Result<()> { - fn logic(this: &mut Statement) -> Result<()> { - while this.step()? == StepResult::Row {} - Ok(()) - } - let result = logic(self); - self.reset(); - result - } - - pub fn map(&mut self, callback: impl FnMut(&mut Statement) -> Result) -> Result> { - fn logic( - this: &mut Statement, - mut callback: impl FnMut(&mut Statement) -> Result, - ) -> Result> { - let mut mapped_rows = Vec::new(); - while this.step()? == StepResult::Row { - mapped_rows.push(callback(this)?); - } - Ok(mapped_rows) - } - - let result = logic(self, callback); - self.reset(); - result - } - - pub fn rows(&mut self) -> Result> { - self.map(|s| s.column::()) - } - - pub fn single(&mut self, callback: impl FnOnce(&mut Statement) -> Result) -> Result { - fn logic( - this: &mut Statement, - callback: impl FnOnce(&mut Statement) -> Result, - ) -> Result { - println!("{:?}", std::any::type_name::()); - anyhow::ensure!( - this.step()? == StepResult::Row, - "single called with query that returns no rows." - ); - let result = callback(this)?; - - anyhow::ensure!( - this.step()? == StepResult::Done, - "single called with a query that returns more than one row." - ); - - Ok(result) - } - let result = logic(self, callback); - self.reset(); - result - } - - pub fn row(&mut self) -> Result { - self.single(|this| this.column::()) - } - - pub fn maybe( - &mut self, - callback: impl FnOnce(&mut Statement) -> Result, - ) -> Result> { - fn logic( - this: &mut Statement, - callback: impl FnOnce(&mut Statement) -> Result, - ) -> Result> { - if this.step().context("Failed on step call")? != StepResult::Row { - return Ok(None); - } - - let result = callback(this) - .map(|r| Some(r)) - .context("Failed to parse row result")?; - - anyhow::ensure!( - this.step().context("Second step call")? == StepResult::Done, - "maybe called with a query that returns more than one row." - ); - - Ok(result) - } - let result = logic(self, callback); - self.reset(); - result - } - - pub fn maybe_row(&mut self) -> Result> { - self.maybe(|this| this.column::()) - } -} - -impl Drop for Statement<'_> { - fn drop(&mut self) { - unsafe { - for raw_statement in self.raw_statements.iter() { - sqlite3_finalize(*raw_statement); - } - } - } -} - -#[cfg(test)] -mod test { - use indoc::indoc; - - use crate::{ - connection::Connection, - statement::{Statement, StepResult}, - }; - - #[test] - fn binding_multiple_statements_with_parameter_gaps() { - let connection = - Connection::open_memory(Some("binding_multiple_statements_with_parameter_gaps")); - - connection - .exec(indoc! {" - CREATE TABLE test ( - col INTEGER - )"}) - .unwrap()() - .unwrap(); - - let statement = Statement::prepare( - &connection, - indoc! {" - INSERT INTO test(col) VALUES (?3); - SELECT * FROM test WHERE col = ?1"}, - ) - .unwrap(); - - statement - .bind_int(1, 1) - .expect("Could not bind parameter to first index"); - statement - .bind_int(2, 2) - .expect("Could not bind parameter to second index"); - statement - .bind_int(3, 3) - .expect("Could not bind parameter to third index"); - } - - #[test] - fn blob_round_trips() { - let connection1 = Connection::open_memory(Some("blob_round_trips")); - connection1 - .exec(indoc! {" - CREATE TABLE blobs ( - data BLOB - )"}) - .unwrap()() - .unwrap(); - - let blob = &[0, 1, 2, 4, 8, 16, 32, 64]; - - let mut write = - Statement::prepare(&connection1, "INSERT INTO blobs (data) VALUES (?)").unwrap(); - write.bind_blob(1, blob).unwrap(); - assert_eq!(write.step().unwrap(), StepResult::Done); - - // Read the blob from the - let connection2 = Connection::open_memory(Some("blob_round_trips")); - let mut read = Statement::prepare(&connection2, "SELECT * FROM blobs").unwrap(); - assert_eq!(read.step().unwrap(), StepResult::Row); - assert_eq!(read.column_blob(0).unwrap(), blob); - assert_eq!(read.step().unwrap(), StepResult::Done); - - // Delete the added blob and verify its deleted on the other side - connection2.exec("DELETE FROM blobs").unwrap()().unwrap(); - let mut read = Statement::prepare(&connection1, "SELECT * FROM blobs").unwrap(); - assert_eq!(read.step().unwrap(), StepResult::Done); - } - - #[test] - pub fn maybe_returns_options() { - let connection = Connection::open_memory(Some("maybe_returns_options")); - connection - .exec(indoc! {" - CREATE TABLE texts ( - text TEXT - )"}) - .unwrap()() - .unwrap(); - - assert!( - connection - .select_row::("SELECT text FROM texts") - .unwrap()() - .unwrap() - .is_none() - ); - - let text_to_insert = "This is a test"; - - connection - .exec_bound("INSERT INTO texts VALUES (?)") - .unwrap()(text_to_insert) - .unwrap(); - - assert_eq!( - connection.select_row("SELECT text FROM texts").unwrap()().unwrap(), - Some(text_to_insert.to_string()) - ); - } -} diff --git a/crates/sqlez/src/thread_safe_connection.rs b/crates/sqlez/src/thread_safe_connection.rs deleted file mode 100644 index 966f14a9c2..0000000000 --- a/crates/sqlez/src/thread_safe_connection.rs +++ /dev/null @@ -1,355 +0,0 @@ -use anyhow::Context as _; -use collections::HashMap; -use futures::{Future, FutureExt, channel::oneshot}; -use parking_lot::{Mutex, RwLock}; -use std::{ - marker::PhantomData, - ops::Deref, - sync::{Arc, LazyLock}, - thread, -}; -use thread_local::ThreadLocal; - -use crate::{connection::Connection, domain::Migrator, util::UnboundedSyncSender}; - -const MIGRATION_RETRIES: usize = 10; - -type QueuedWrite = Box; -type WriteQueue = Box; -type WriteQueueConstructor = Box WriteQueue>; - -/// List of queues of tasks by database uri. This lets us serialize writes to the database -/// and have a single worker thread per db file. This means many thread safe connections -/// (possibly with different migrations) could all be communicating with the same background -/// thread. -static QUEUES: LazyLock, WriteQueue>>> = LazyLock::new(Default::default); - -/// Thread safe connection to a given database file or in memory db. This can be cloned, shared, static, -/// whatever. It derefs to a synchronous connection by thread that is read only. A write capable connection -/// may be accessed by passing a callback to the `write` function which will queue the callback -#[derive(Clone)] -pub struct ThreadSafeConnection { - uri: Arc, - persistent: bool, - connection_initialize_query: Option<&'static str>, - connections: Arc>, -} - -unsafe impl Send for ThreadSafeConnection {} -unsafe impl Sync for ThreadSafeConnection {} - -pub struct ThreadSafeConnectionBuilder { - db_initialize_query: Option<&'static str>, - write_queue_constructor: Option, - connection: ThreadSafeConnection, - _migrator: PhantomData<*mut M>, -} - -impl ThreadSafeConnectionBuilder { - /// Sets the query to run every time a connection is opened. This must - /// be infallible (EG only use pragma statements) and not cause writes. - /// to the db or it will panic. - pub fn with_connection_initialize_query(mut self, initialize_query: &'static str) -> Self { - self.connection.connection_initialize_query = Some(initialize_query); - self - } - - /// Queues an initialization query for the database file. This must be infallible - /// but may cause changes to the database file such as with `PRAGMA journal_mode` - pub fn with_db_initialization_query(mut self, initialize_query: &'static str) -> Self { - self.db_initialize_query = Some(initialize_query); - self - } - - /// Specifies how the thread safe connection should serialize writes. If provided - /// the connection will call the write_queue_constructor for each database file in - /// this process. The constructor is responsible for setting up a background thread or - /// async task which handles queued writes with the provided connection. - pub fn with_write_queue_constructor( - mut self, - write_queue_constructor: WriteQueueConstructor, - ) -> Self { - self.write_queue_constructor = Some(write_queue_constructor); - self - } - - pub async fn build(self) -> anyhow::Result { - self.connection - .initialize_queues(self.write_queue_constructor); - - let db_initialize_query = self.db_initialize_query; - - self.connection - .write(move |connection| { - if let Some(db_initialize_query) = db_initialize_query { - connection.exec(db_initialize_query).with_context(|| { - format!( - "Db initialize query failed to execute: {}", - db_initialize_query - ) - })?()?; - } - - // Retry failed migrations in case they were run in parallel from different - // processes. This gives a best attempt at migrating before bailing - let mut migration_result = - anyhow::Result::<()>::Err(anyhow::anyhow!("Migration never run")); - - let foreign_keys_enabled: bool = - connection.select_row::("PRAGMA foreign_keys")?() - .unwrap_or(None) - .map(|enabled| enabled != 0) - .unwrap_or(false); - - connection.exec("PRAGMA foreign_keys = OFF;")?()?; - - for _ in 0..MIGRATION_RETRIES { - migration_result = connection - .with_savepoint("thread_safe_multi_migration", || M::migrate(connection)); - - if migration_result.is_ok() { - break; - } - } - - if foreign_keys_enabled { - connection.exec("PRAGMA foreign_keys = ON;")?()?; - } - migration_result - }) - .await?; - - Ok(self.connection) - } -} - -impl ThreadSafeConnection { - fn initialize_queues(&self, write_queue_constructor: Option) -> bool { - if !QUEUES.read().contains_key(&self.uri) { - let mut queues = QUEUES.write(); - if !queues.contains_key(&self.uri) { - let mut write_queue_constructor = - write_queue_constructor.unwrap_or_else(background_thread_queue); - queues.insert(self.uri.clone(), write_queue_constructor()); - return true; - } - } - false - } - - pub fn builder(uri: &str, persistent: bool) -> ThreadSafeConnectionBuilder { - ThreadSafeConnectionBuilder:: { - db_initialize_query: None, - write_queue_constructor: None, - connection: Self { - uri: Arc::from(uri), - persistent, - connection_initialize_query: None, - connections: Default::default(), - }, - _migrator: PhantomData, - } - } - - /// Opens a new db connection with the initialized file path. This is internal and only - /// called from the deref function. - fn open_file(uri: &str) -> Connection { - Connection::open_file(uri) - } - - /// Opens a shared memory connection using the file path as the identifier. This is internal - /// and only called from the deref function. - fn open_shared_memory(uri: &str) -> Connection { - Connection::open_memory(Some(uri)) - } - - pub fn write( - &self, - callback: impl 'static + Send + FnOnce(&Connection) -> T, - ) -> impl Future { - // Check and invalidate queue and maybe recreate queue - let queues = QUEUES.read(); - let write_channel = queues - .get(&self.uri) - .expect("Queues are inserted when build is called. This should always succeed"); - - // Create a one shot channel for the result of the queued write - // so we can await on the result - let (sender, receiver) = oneshot::channel(); - - let thread_safe_connection = (*self).clone(); - write_channel(Box::new(move || { - let connection = thread_safe_connection.deref(); - let result = connection.with_write(|connection| callback(connection)); - sender.send(result).ok(); - })); - receiver.map(|response| response.expect("Write queue unexpectedly closed")) - } - - pub(crate) fn create_connection( - persistent: bool, - uri: &str, - connection_initialize_query: Option<&'static str>, - ) -> Connection { - let mut connection = if persistent { - Self::open_file(uri) - } else { - Self::open_shared_memory(uri) - }; - - // Disallow writes on the connection. The only writes allowed for thread safe connections - // are from the background thread that can serialize them. - *connection.write.get_mut() = false; - - if let Some(initialize_query) = connection_initialize_query { - connection.exec(initialize_query).unwrap_or_else(|_| { - panic!("Initialize query failed to execute: {}", initialize_query) - })() - .unwrap() - } - - connection - } -} - -impl ThreadSafeConnection { - /// Special constructor for ThreadSafeConnection which disallows db initialization and migrations. - /// This allows construction to be infallible and not write to the db. - pub fn new( - uri: &str, - persistent: bool, - connection_initialize_query: Option<&'static str>, - write_queue_constructor: Option, - ) -> Self { - let connection = Self { - uri: Arc::from(uri), - persistent, - connection_initialize_query, - connections: Default::default(), - }; - - connection.initialize_queues(write_queue_constructor); - connection - } -} - -impl Deref for ThreadSafeConnection { - type Target = Connection; - - fn deref(&self) -> &Self::Target { - self.connections.get_or(|| { - Self::create_connection(self.persistent, &self.uri, self.connection_initialize_query) - }) - } -} - -pub fn background_thread_queue() -> WriteQueueConstructor { - use std::sync::mpsc::channel; - - Box::new(|| { - let (sender, receiver) = channel::(); - - thread::Builder::new() - .name("sqlezWorker".to_string()) - .spawn(move || { - while let Ok(write) = receiver.recv() { - write() - } - }) - .unwrap(); - - let sender = UnboundedSyncSender::new(sender); - Box::new(move |queued_write| { - sender - .send(queued_write) - .expect("Could not send write action to background thread"); - }) - }) -} - -pub fn locking_queue() -> WriteQueueConstructor { - Box::new(|| { - let write_mutex = Mutex::new(()); - Box::new(move |queued_write| { - let _lock = write_mutex.lock(); - queued_write(); - }) - }) -} - -#[cfg(test)] -mod test { - use indoc::indoc; - use std::ops::Deref; - - use std::thread; - - use crate::{domain::Domain, thread_safe_connection::ThreadSafeConnection}; - - #[test] - fn many_initialize_and_migrate_queries_at_once() { - let mut handles = vec![]; - - enum TestDomain {} - impl Domain for TestDomain { - const NAME: &str = "test"; - const MIGRATIONS: &[&str] = &["CREATE TABLE test(col1 TEXT, col2 TEXT) STRICT;"]; - } - - for _ in 0..100 { - handles.push(thread::spawn(|| { - let builder = - ThreadSafeConnection::builder::("annoying-test.db", false) - .with_db_initialization_query("PRAGMA journal_mode=WAL") - .with_connection_initialize_query(indoc! {" - PRAGMA synchronous=NORMAL; - PRAGMA busy_timeout=1; - PRAGMA foreign_keys=TRUE; - PRAGMA case_sensitive_like=TRUE; - "}); - - let _ = smol::block_on(builder.build()).unwrap().deref(); - })); - } - - for handle in handles { - let _ = handle.join(); - } - } - - #[test] - #[should_panic] - fn wild_zed_lost_failure() { - enum TestWorkspace {} - impl Domain for TestWorkspace { - const NAME: &str = "workspace"; - - const MIGRATIONS: &[&str] = &[" - CREATE TABLE workspaces( - workspace_id INTEGER PRIMARY KEY, - dock_visible INTEGER, -- Boolean - dock_anchor TEXT, -- Enum: 'Bottom' / 'Right' / 'Expanded' - dock_pane INTEGER, -- NULL indicates that we don't have a dock pane yet - timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, - FOREIGN KEY(dock_pane) REFERENCES panes(pane_id), - FOREIGN KEY(active_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; - "]; - } - - let builder = - ThreadSafeConnection::builder::("wild_zed_lost_failure", false) - .with_connection_initialize_query("PRAGMA FOREIGN_KEYS=true"); - - smol::block_on(builder.build()).unwrap(); - } -} diff --git a/crates/sqlez/src/typed_statements.rs b/crates/sqlez/src/typed_statements.rs deleted file mode 100644 index e300a6e7d5..0000000000 --- a/crates/sqlez/src/typed_statements.rs +++ /dev/null @@ -1,96 +0,0 @@ -use anyhow::{Context as _, Result}; - -use crate::{ - bindable::{Bind, Column}, - connection::Connection, - statement::Statement, -}; - -impl Connection { - /// Prepare a statement which has no bindings and returns nothing. - /// - /// Note: If there are multiple statements that depend upon each other - /// (such as those which make schema changes), preparation will fail. - /// Use a true migration instead. - pub fn exec<'a>(&'a self, query: &str) -> Result Result<()>> { - let mut statement = Statement::prepare(self, query)?; - Ok(move || statement.exec()) - } - - /// Prepare a statement which takes a binding, but returns nothing. - /// The bindings for a given invocation should be passed to the returned - /// closure - /// - /// Note: If there are multiple statements that depend upon each other - /// (such as those which make schema changes), preparation will fail. - /// Use a true migration instead. - pub fn exec_bound<'a, B: Bind>( - &'a self, - query: &str, - ) -> Result Result<()>> { - let mut statement = Statement::prepare(self, query)?; - Ok(move |bindings| statement.with_bindings(&bindings)?.exec()) - } - - /// Prepare a statement which has no bindings and returns a `Vec`. - /// - /// Note: If there are multiple statements that depend upon each other - /// (such as those which make schema changes), preparation will fail. - /// Use a true migration instead. - pub fn select<'a, C: Column>( - &'a self, - query: &str, - ) -> Result Result>> { - let mut statement = Statement::prepare(self, query)?; - Ok(move || statement.rows::()) - } - - /// Prepare a statement which takes a binding and returns a `Vec`. - /// - /// Note: If there are multiple statements that depend upon each other - /// (such as those which make schema changes), preparation will fail. - /// Use a true migration instead. - pub fn select_bound<'a, B: Bind, C: Column>( - &'a self, - query: &str, - ) -> Result Result>> { - let mut statement = Statement::prepare(self, query)?; - Ok(move |bindings| statement.with_bindings(&bindings)?.rows::()) - } - - /// Prepare a statement that selects a single row from the database. - /// Will return none if no rows are returned and will error if more than - /// 1 row - /// - /// Note: If there are multiple statements that depend upon each other - /// (such as those which make schema changes), preparation will fail. - /// Use a true migration instead. - pub fn select_row<'a, C: Column>( - &'a self, - query: &str, - ) -> Result Result>> { - let mut statement = Statement::prepare(self, query)?; - Ok(move || statement.maybe_row::()) - } - - /// Prepare a statement which takes a binding and selects a single row - /// from the database. Will return none if no rows are returned and will - /// error if more than 1 row is returned. - /// - /// Note: If there are multiple statements that depend upon each other - /// (such as those which make schema changes), preparation will fail. - /// Use a true migration instead. - pub fn select_row_bound<'a, B: Bind, C: Column>( - &'a self, - query: &str, - ) -> Result Result>> { - let mut statement = Statement::prepare(self, query)?; - Ok(move |bindings| { - statement - .with_bindings(&bindings) - .context("Bindings failed")? - .maybe_row::() - .context("Maybe row failed") - }) - } -} diff --git a/crates/sqlez/src/util.rs b/crates/sqlez/src/util.rs deleted file mode 100644 index 8be6d3f4e0..0000000000 --- a/crates/sqlez/src/util.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::ops::Deref; -use std::sync::mpsc::Sender; - -use parking_lot::Mutex; -use thread_local::ThreadLocal; - -/// Unbounded standard library sender which is stored per thread to get around -/// the lack of sync on the standard library version while still being unbounded -/// Note: this locks on the cloneable sender, but its done once per thread, so it -/// shouldn't result in too much contention -pub struct UnboundedSyncSender { - cloneable_sender: Mutex>, - local_senders: ThreadLocal>, -} - -impl UnboundedSyncSender { - pub fn new(sender: Sender) -> Self { - Self { - cloneable_sender: Mutex::new(sender), - local_senders: ThreadLocal::new(), - } - } -} - -impl Deref for UnboundedSyncSender { - type Target = Sender; - - fn deref(&self) -> &Self::Target { - self.local_senders - .get_or(|| self.cloneable_sender.lock().clone()) - } -} diff --git a/crates/sqlez_macros/Cargo.toml b/crates/sqlez_macros/Cargo.toml deleted file mode 100644 index cff96d0b89..0000000000 --- a/crates/sqlez_macros/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "sqlez_macros" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/sqlez_macros.rs" -proc-macro = true -doctest = false - -[dependencies] -sqlez.workspace = true -sqlformat.workspace = true -syn.workspace = true diff --git a/crates/sqlez_macros/LICENSE-GPL b/crates/sqlez_macros/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/sqlez_macros/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/sqlez_macros/src/sqlez_macros.rs b/crates/sqlez_macros/src/sqlez_macros.rs deleted file mode 100644 index 5ddfe4cd87..0000000000 --- a/crates/sqlez_macros/src/sqlez_macros.rs +++ /dev/null @@ -1,102 +0,0 @@ -use proc_macro::{Delimiter, Span, TokenStream, TokenTree}; -use syn::Error; - -#[cfg(not(any(target_os = "linux", target_os = "freebsd")))] -static SQLITE: std::sync::LazyLock = - std::sync::LazyLock::new(|| { - sqlez::thread_safe_connection::ThreadSafeConnection::new( - ":memory:", - false, - None, - Some(sqlez::thread_safe_connection::locking_queue()), - ) - }); - -#[proc_macro] -pub fn sql(tokens: TokenStream) -> TokenStream { - let (spans, sql) = make_sql(tokens); - - #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] - let error = SQLITE.sql_has_syntax_error(sql.trim()); - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - let error: Option<(String, usize)> = None; - - let formatted_sql = sqlformat::format(&sql, &sqlformat::QueryParams::None, Default::default()); - - if let Some((error, error_offset)) = error { - create_error(spans, error_offset, error, &formatted_sql) - } else { - format!("r#\"{}\"#", &formatted_sql).parse().unwrap() - } -} - -fn create_error( - spans: Vec<(usize, Span)>, - error_offset: usize, - error: String, - formatted_sql: &String, -) -> TokenStream { - let error_span = spans - .into_iter() - .skip_while(|(offset, _)| offset <= &error_offset) - .map(|(_, span)| span) - .next() - .unwrap_or_else(Span::call_site); - let error_text = format!("Sql Error: {}\nFor Query: {}", error, formatted_sql); - TokenStream::from(Error::new(error_span.into(), error_text).into_compile_error()) -} - -fn make_sql(tokens: TokenStream) -> (Vec<(usize, Span)>, String) { - let mut sql_tokens = vec![]; - flatten_stream(tokens, &mut sql_tokens); - // Lookup of spans by offset at the end of the token - let mut spans: Vec<(usize, Span)> = Vec::new(); - let mut sql = String::new(); - for (token_text, span) in sql_tokens { - sql.push_str(&token_text); - spans.push((sql.len(), span)); - } - (spans, sql) -} - -/// This method exists to normalize the representation of groups -/// to always include spaces between tokens. This is why we don't use the usual .to_string(). -/// This allows our token search in token_at_offset to resolve -/// ambiguity of '(tokens)' vs. '( token )', due to sqlite requiring byte offsets -fn flatten_stream(tokens: TokenStream, result: &mut Vec<(String, Span)>) { - for token_tree in tokens.into_iter() { - match token_tree { - TokenTree::Group(group) => { - // push open delimiter - result.push((open_delimiter(group.delimiter()), group.span())); - // recurse - flatten_stream(group.stream(), result); - // push close delimiter - result.push((close_delimiter(group.delimiter()), group.span())); - } - TokenTree::Ident(ident) => { - result.push((format!("{} ", ident), ident.span())); - } - leaf_tree => result.push((leaf_tree.to_string(), leaf_tree.span())), - } - } -} - -fn open_delimiter(delimiter: Delimiter) -> String { - match delimiter { - Delimiter::Parenthesis => "( ".to_string(), - Delimiter::Brace => "[ ".to_string(), - Delimiter::Bracket => "{ ".to_string(), - Delimiter::None => "".to_string(), - } -} - -fn close_delimiter(delimiter: Delimiter) -> String { - match delimiter { - Delimiter::Parenthesis => " ) ".to_string(), - Delimiter::Brace => " ] ".to_string(), - Delimiter::Bracket => " } ".to_string(), - Delimiter::None => "".to_string(), - } -} diff --git a/crates/story/Cargo.toml b/crates/story/Cargo.toml deleted file mode 100644 index 798461402d..0000000000 --- a/crates/story/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "story" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lib] -path = "src/story.rs" - -[lints] -workspace = true - -[dependencies] -gpui.workspace = true -itertools.workspace = true -smallvec.workspace = true diff --git a/crates/story/LICENSE-GPL b/crates/story/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/story/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/story/src/story.rs b/crates/story/src/story.rs deleted file mode 100644 index b59cb6fb99..0000000000 --- a/crates/story/src/story.rs +++ /dev/null @@ -1,209 +0,0 @@ -use gpui::{ - AnyElement, App, Div, SharedString, Window, colors::DefaultColors, div, prelude::*, px, rems, -}; -use itertools::Itertools; -use smallvec::SmallVec; - -pub struct Story {} - -impl Story { - pub fn container(cx: &App) -> gpui::Stateful
{ - div() - .id("story_container") - .overflow_y_scroll() - .w_full() - .min_h_full() - .flex() - .flex_col() - .text_color(cx.default_colors().text) - .bg(cx.default_colors().background) - } - - pub fn title(title: impl Into, cx: &App) -> impl Element { - div() - .text_xs() - .text_color(cx.default_colors().text) - .child(title.into()) - } - - pub fn title_for(cx: &App) -> impl Element { - Self::title(std::any::type_name::(), cx) - } - - pub fn section(cx: &App) -> Div { - div() - .p_4() - .m_4() - .border_1() - .border_color(cx.default_colors().separator) - } - - pub fn section_title(cx: &App) -> Div { - div().text_lg().text_color(cx.default_colors().text) - } - - pub fn group(cx: &App) -> Div { - div().my_2().bg(cx.default_colors().container) - } - - pub fn code_block(code: impl Into, cx: &App) -> Div { - div() - .size_full() - .p_2() - .max_w(rems(36.)) - .bg(cx.default_colors().container) - .rounded_sm() - .text_sm() - .text_color(cx.default_colors().text) - .overflow_hidden() - .child(code.into()) - } - - pub fn divider(cx: &App) -> Div { - div().my_2().h(px(1.)).bg(cx.default_colors().separator) - } - - pub fn description(description: impl Into, cx: &App) -> impl Element { - div() - .text_sm() - .text_color(cx.default_colors().text) - .min_w_96() - .child(description.into()) - } - - pub fn label(label: impl Into, cx: &App) -> impl Element { - div() - .text_xs() - .text_color(cx.default_colors().text) - .child(label.into()) - } - - /// Note: Not `ui::v_flex` as the `story` crate doesn't depend on the `ui` crate. - pub fn v_flex() -> Div { - div().flex().flex_col().gap_1() - } -} - -#[derive(IntoElement)] -pub struct StoryItem { - label: SharedString, - item: AnyElement, - description: Option, - usage: Option, -} - -impl StoryItem { - pub fn new(label: impl Into, item: impl IntoElement) -> Self { - Self { - label: label.into(), - item: item.into_any_element(), - description: None, - usage: None, - } - } - - pub fn description(mut self, description: impl Into) -> Self { - self.description = Some(description.into()); - self - } - - pub fn usage(mut self, code: impl Into) -> Self { - self.usage = Some(code.into()); - self - } -} - -impl RenderOnce for StoryItem { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let colors = cx.default_colors(); - - div() - .my_2() - .flex() - .gap_4() - .w_full() - .child( - Story::v_flex() - .px_2() - .w_1_2() - .min_h_px() - .child(Story::label(self.label, cx)) - .child( - div() - .rounded_sm() - .bg(colors.background) - .border_1() - .border_color(colors.border) - .py_1() - .px_2() - .overflow_hidden() - .child(self.item), - ) - .when_some(self.description, |this, description| { - this.child(Story::description(description, cx)) - }), - ) - .child( - Story::v_flex() - .px_2() - .flex_none() - .w_1_2() - .min_h_px() - .when_some(self.usage, |this, usage| { - this.child(Story::label("Example Usage", cx)) - .child(Story::code_block(usage, cx)) - }), - ) - } -} - -#[derive(IntoElement)] -pub struct StorySection { - description: Option, - children: SmallVec<[AnyElement; 2]>, -} - -impl Default for StorySection { - fn default() -> Self { - Self::new() - } -} - -impl StorySection { - pub fn new() -> Self { - Self { - description: None, - children: SmallVec::new(), - } - } - - pub fn description(mut self, description: impl Into) -> Self { - self.description = Some(description.into()); - self - } -} - -impl RenderOnce for StorySection { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let children: SmallVec<[AnyElement; 2]> = SmallVec::from_iter(Itertools::intersperse_with( - self.children.into_iter(), - || Story::divider(cx).into_any_element(), - )); - - Story::section(cx) - // Section title - .py_2() - // Section description - .when_some(self.description, |section, description| { - section.child(Story::description(description, cx)) - }) - .child(div().flex().flex_col().gap_2().children(children)) - .child(Story::divider(cx)) - } -} - -impl ParentElement for StorySection { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} diff --git a/crates/storybook/Cargo.toml b/crates/storybook/Cargo.toml deleted file mode 100644 index 148f036134..0000000000 --- a/crates/storybook/Cargo.toml +++ /dev/null @@ -1,40 +0,0 @@ -[package] -name = "storybook" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[[bin]] -name = "storybook" -path = "src/storybook.rs" - -[dependencies] -anyhow.workspace = true -clap = { workspace = true, features = ["derive", "string"] } -collab_ui = { workspace = true, features = ["stories"] } -ctrlc = "3.4" -dialoguer = { version = "0.11.0", features = ["fuzzy-select"] } -editor.workspace = true -fuzzy.workspace = true -gpui = { workspace = true, default-features = true } -indoc.workspace = true -language.workspace = true -log.workspace = true -menu.workspace = true -picker.workspace = true -reqwest_client.workspace = true -rust-embed.workspace = true -settings.workspace = true -simplelog.workspace = true -story.workspace = true -strum = { workspace = true, features = ["derive"] } -theme.workspace = true -title_bar = { workspace = true, features = ["stories"] } -ui = { workspace = true, features = ["stories"] } - -[dev-dependencies] -gpui = { workspace = true, features = ["test-support"] } diff --git a/crates/storybook/LICENSE-GPL b/crates/storybook/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/storybook/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/storybook/build.rs b/crates/storybook/build.rs deleted file mode 100644 index 66791cae42..0000000000 --- a/crates/storybook/build.rs +++ /dev/null @@ -1,9 +0,0 @@ -fn main() { - #[cfg(target_os = "windows")] - { - #[cfg(target_env = "msvc")] - { - println!("cargo:rustc-link-arg=/stack:{}", 8 * 1024 * 1024); - } - } -} diff --git a/crates/storybook/docs/thoughts.md b/crates/storybook/docs/thoughts.md deleted file mode 100644 index cdeef621f3..0000000000 --- a/crates/storybook/docs/thoughts.md +++ /dev/null @@ -1,57 +0,0 @@ -Much of element styling is now handled by an external engine. - -How do I make an element hover. - -There's a hover style. - -Hoverable needs to wrap another element. That element can be styled. - -```rs -struct Hoverable { - -} - -impl Element for Hoverable { - -} -``` - -```rs -#[derive(Styled, Interactive)] -pub struct Div { - declared_style: StyleRefinement, - interactions: Interactions -} - -pub trait Styled { - fn declared_style(&mut self) -> &mut StyleRefinement; - fn compute_style(&mut self) -> Style { - Style::default().refine(self.declared_style()) - } - - // All the tailwind classes, modifying self.declared_style() -} - -impl Style { - pub fn paint_background(layout: Layout, cx: &mut PaintContext); - pub fn paint_foreground(layout: Layout, cx: &mut PaintContext); -} - -pub trait Interactive { - fn interactions(&mut self) -> &mut Interactions; - - fn on_click(self, ) -} - -struct Interactions { - click: SmallVec<[; 1]>, -} -``` - -```rs -trait Stylable { - type Style; - - fn with_style(self, style: Self::Style) -> Self; -} -``` diff --git a/crates/storybook/src/actions.rs b/crates/storybook/src/actions.rs deleted file mode 100644 index 03ee5b580c..0000000000 --- a/crates/storybook/src/actions.rs +++ /dev/null @@ -1,2 +0,0 @@ -use gpui::actions; -actions!(storybook, [Quit]); diff --git a/crates/storybook/src/app_menus.rs b/crates/storybook/src/app_menus.rs deleted file mode 100644 index 4e84b4c85d..0000000000 --- a/crates/storybook/src/app_menus.rs +++ /dev/null @@ -1,10 +0,0 @@ -use gpui::{Menu, MenuItem}; - -pub fn app_menus() -> Vec { - use crate::actions::Quit; - - vec![Menu { - name: "Storybook".into(), - items: vec![MenuItem::action("Quit", Quit)], - }] -} diff --git a/crates/storybook/src/assets.rs b/crates/storybook/src/assets.rs deleted file mode 100644 index 4da4081212..0000000000 --- a/crates/storybook/src/assets.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::borrow::Cow; - -use anyhow::{Context as _, Result}; -use gpui::{AssetSource, SharedString}; -use rust_embed::RustEmbed; - -#[derive(RustEmbed)] -#[folder = "../../assets"] -#[include = "fonts/**/*"] -#[include = "icons/**/*"] -#[include = "images/**/*"] -#[include = "themes/**/*"] -#[include = "sounds/**/*"] -#[include = "*.md"] -#[exclude = "*.DS_Store"] -pub struct Assets; - -impl AssetSource for Assets { - fn load(&self, path: &str) -> Result>> { - Self::get(path) - .map(|f| f.data) - .with_context(|| format!("could not find asset at path {path:?}")) - .map(Some) - } - - fn list(&self, path: &str) -> Result> { - Ok(Self::iter() - .filter(|p| p.starts_with(path)) - .map(SharedString::from) - .collect()) - } -} diff --git a/crates/storybook/src/stories.rs b/crates/storybook/src/stories.rs deleted file mode 100644 index 63992d259c..0000000000 --- a/crates/storybook/src/stories.rs +++ /dev/null @@ -1,23 +0,0 @@ -mod auto_height_editor; -mod cursor; -mod focus; -mod indent_guides; -mod kitchen_sink; -mod overflow_scroll; -mod picker; -mod scroll; -mod text; -mod viewport_units; -mod with_rem_size; - -pub use auto_height_editor::*; -pub use cursor::*; -pub use focus::*; -pub use indent_guides::*; -pub use kitchen_sink::*; -pub use overflow_scroll::*; -pub use picker::*; -pub use scroll::*; -pub use text::*; -pub use viewport_units::*; -pub use with_rem_size::*; diff --git a/crates/storybook/src/stories/auto_height_editor.rs b/crates/storybook/src/stories/auto_height_editor.rs deleted file mode 100644 index 702d5774f2..0000000000 --- a/crates/storybook/src/stories/auto_height_editor.rs +++ /dev/null @@ -1,36 +0,0 @@ -use editor::Editor; -use gpui::{ - App, AppContext as _, Context, Entity, IntoElement, KeyBinding, ParentElement, Render, Styled, - Window, div, white, -}; - -pub struct AutoHeightEditorStory { - editor: Entity, -} - -impl AutoHeightEditorStory { - pub fn new(window: &mut Window, cx: &mut App) -> gpui::Entity { - cx.bind_keys([KeyBinding::new( - "enter", - editor::actions::Newline, - Some("Editor"), - )]); - cx.new(|cx| Self { - editor: cx.new(|cx| { - let mut editor = Editor::auto_height(1, 3, window, cx); - editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); - editor - }), - }) - } -} - -impl Render for AutoHeightEditorStory { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div() - .size_full() - .bg(white()) - .text_sm() - .child(div().w_32().bg(gpui::black()).child(self.editor.clone())) - } -} diff --git a/crates/storybook/src/stories/cursor.rs b/crates/storybook/src/stories/cursor.rs deleted file mode 100644 index 00bae99917..0000000000 --- a/crates/storybook/src/stories/cursor.rs +++ /dev/null @@ -1,109 +0,0 @@ -use gpui::{Div, Render, Stateful}; -use story::Story; -use ui::prelude::*; - -pub struct CursorStory; - -impl Render for CursorStory { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let all_cursors: [(&str, Box) -> Stateful
>); 19] = [ - ( - "cursor_default", - Box::new(|el: Stateful
| el.cursor_default()), - ), - ( - "cursor_pointer", - Box::new(|el: Stateful
| el.cursor_pointer()), - ), - ( - "cursor_text", - Box::new(|el: Stateful
| el.cursor_text()), - ), - ( - "cursor_move", - Box::new(|el: Stateful
| el.cursor_move()), - ), - ( - "cursor_not_allowed", - Box::new(|el: Stateful
| el.cursor_not_allowed()), - ), - ( - "cursor_context_menu", - Box::new(|el: Stateful
| el.cursor_context_menu()), - ), - ( - "cursor_crosshair", - Box::new(|el: Stateful
| el.cursor_crosshair()), - ), - ( - "cursor_vertical_text", - Box::new(|el: Stateful
| el.cursor_vertical_text()), - ), - ( - "cursor_alias", - Box::new(|el: Stateful
| el.cursor_alias()), - ), - ( - "cursor_copy", - Box::new(|el: Stateful
| el.cursor_copy()), - ), - ( - "cursor_no_drop", - Box::new(|el: Stateful
| el.cursor_no_drop()), - ), - ( - "cursor_grab", - Box::new(|el: Stateful
| el.cursor_grab()), - ), - ( - "cursor_grabbing", - Box::new(|el: Stateful
| el.cursor_grabbing()), - ), - ( - "cursor_col_resize", - Box::new(|el: Stateful
| el.cursor_col_resize()), - ), - ( - "cursor_row_resize", - Box::new(|el: Stateful
| el.cursor_row_resize()), - ), - ( - "cursor_n_resize", - Box::new(|el: Stateful
| el.cursor_n_resize()), - ), - ( - "cursor_e_resize", - Box::new(|el: Stateful
| el.cursor_e_resize()), - ), - ( - "cursor_s_resize", - Box::new(|el: Stateful
| el.cursor_s_resize()), - ), - ( - "cursor_w_resize", - Box::new(|el: Stateful
| el.cursor_w_resize()), - ), - ]; - - Story::container(cx) - .flex() - .gap_1() - .child(Story::title("cursor", cx)) - .children(all_cursors.map(|(name, apply_cursor)| { - div().gap_1().flex().text_color(gpui::white()).child( - div() - .flex() - .items_center() - .justify_center() - .id(name) - .map(apply_cursor) - .w_64() - .h_8() - .bg(gpui::red()) - .active(|style| style.bg(gpui::green())) - .text_sm() - .child(Story::label(name, cx)), - ) - })) - } -} diff --git a/crates/storybook/src/stories/focus.rs b/crates/storybook/src/stories/focus.rs deleted file mode 100644 index a64c272ba7..0000000000 --- a/crates/storybook/src/stories/focus.rs +++ /dev/null @@ -1,123 +0,0 @@ -use gpui::{ - App, Entity, FocusHandle, KeyBinding, Render, Subscription, Window, actions, div, prelude::*, -}; -use ui::prelude::*; - -actions!(focus, [ActionA, ActionB, ActionC]); - -pub struct FocusStory { - parent_focus: FocusHandle, - child_1_focus: FocusHandle, - child_2_focus: FocusHandle, - _focus_subscriptions: Vec, -} - -impl FocusStory { - pub fn model(window: &mut Window, cx: &mut App) -> Entity { - cx.bind_keys([ - KeyBinding::new("cmd-a", ActionA, Some("parent")), - KeyBinding::new("cmd-a", ActionB, Some("child-1")), - KeyBinding::new("cmd-c", ActionC, None), - ]); - - cx.new(|cx| { - let parent_focus = cx.focus_handle(); - let child_1_focus = cx.focus_handle(); - let child_2_focus = cx.focus_handle(); - let _focus_subscriptions = vec![ - cx.on_focus(&parent_focus, window, |_, _, _| { - println!("Parent focused"); - }), - cx.on_blur(&parent_focus, window, |_, _, _| { - println!("Parent blurred"); - }), - cx.on_focus(&child_1_focus, window, |_, _, _| { - println!("Child 1 focused"); - }), - cx.on_blur(&child_1_focus, window, |_, _, _| { - println!("Child 1 blurred"); - }), - cx.on_focus(&child_2_focus, window, |_, _, _| { - println!("Child 2 focused"); - }), - cx.on_blur(&child_2_focus, window, |_, _, _| { - println!("Child 2 blurred"); - }), - ]; - - Self { - parent_focus, - child_1_focus, - child_2_focus, - _focus_subscriptions, - } - }) - } -} - -impl Render for FocusStory { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let theme = cx.theme(); - let color_1 = theme.status().created; - let color_2 = theme.status().modified; - let color_4 = theme.status().conflict; - let color_5 = theme.status().ignored; - let color_6 = theme.status().renamed; - let color_7 = theme.status().hint; - - div() - .id("parent") - .active(|style| style.bg(color_7)) - .track_focus(&self.parent_focus) - .key_context("parent") - .on_action(cx.listener(|_, _action: &ActionA, _window, _cx| { - println!("Action A dispatched on parent"); - })) - .on_action(cx.listener(|_, _action: &ActionB, _window, _cx| { - println!("Action B dispatched on parent"); - })) - .on_key_down(cx.listener(|_, event, _, _| println!("Key down on parent {:?}", event))) - .on_key_up(cx.listener(|_, event, _, _| println!("Key up on parent {:?}", event))) - .size_full() - .bg(color_1) - .focus(|style| style.bg(color_2)) - .child( - div() - .track_focus(&self.child_1_focus) - .key_context("child-1") - .on_action(cx.listener(|_, _action: &ActionB, _window, _cx| { - println!("Action B dispatched on child 1 during"); - })) - .w_full() - .h_6() - .bg(color_4) - .focus(|style| style.bg(color_5)) - .in_focus(|style| style.bg(color_6)) - .on_key_down( - cx.listener(|_, event, _, _| println!("Key down on child 1 {:?}", event)), - ) - .on_key_up( - cx.listener(|_, event, _, _| println!("Key up on child 1 {:?}", event)), - ) - .child("Child 1"), - ) - .child( - div() - .track_focus(&self.child_2_focus) - .key_context("child-2") - .on_action(cx.listener(|_, _action: &ActionC, _window, _cx| { - println!("Action C dispatched on child 2"); - })) - .w_full() - .h_6() - .bg(color_4) - .on_key_down( - cx.listener(|_, event, _, _| println!("Key down on child 2 {:?}", event)), - ) - .on_key_up( - cx.listener(|_, event, _, _| println!("Key up on child 2 {:?}", event)), - ) - .child("Child 2"), - ) - } -} diff --git a/crates/storybook/src/stories/indent_guides.rs b/crates/storybook/src/stories/indent_guides.rs deleted file mode 100644 index db23ea79bd..0000000000 --- a/crates/storybook/src/stories/indent_guides.rs +++ /dev/null @@ -1,82 +0,0 @@ -use std::ops::Range; - -use gpui::{Entity, Render, div, uniform_list}; -use gpui::{prelude::*, *}; -use ui::{AbsoluteLength, Color, DefiniteLength, Label, LabelCommon, px, v_flex}; - -use story::Story; - -const LENGTH: usize = 100; - -pub struct IndentGuidesStory { - depths: Vec, -} - -impl IndentGuidesStory { - pub fn model(_window: &mut Window, cx: &mut App) -> Entity { - let mut depths = Vec::new(); - depths.push(0); - depths.push(1); - depths.push(2); - for _ in 0..LENGTH - 6 { - depths.push(3); - } - depths.push(2); - depths.push(1); - depths.push(0); - - cx.new(|_cx| Self { depths }) - } -} - -impl Render for IndentGuidesStory { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - Story::container(cx) - .child(Story::title("Indent guides", cx)) - .child( - v_flex().size_full().child( - uniform_list( - "some-list", - self.depths.len(), - cx.processor(move |this, range: Range, _window, _cx| { - this.depths - .iter() - .enumerate() - .skip(range.start) - .take(range.end - range.start) - .map(|(i, depth)| { - div() - .pl(DefiniteLength::Absolute(AbsoluteLength::Pixels(px( - 16. * (*depth as f32), - )))) - .child(Label::new(format!("Item {}", i)).color(Color::Info)) - }) - .collect() - }), - ) - .with_sizing_behavior(gpui::ListSizingBehavior::Infer) - .with_decoration( - ui::indent_guides( - px(16.), - ui::IndentGuideColors { - default: Color::Info.color(cx), - hover: Color::Accent.color(cx), - active: Color::Accent.color(cx), - }, - ) - .with_compute_indents_fn( - cx.entity(), - |this, range, _cx, _context| { - this.depths - .iter() - .skip(range.start) - .take(range.end - range.start) - .cloned() - .collect() - }, - ), - ), - ), - ) - } -} diff --git a/crates/storybook/src/stories/kitchen_sink.rs b/crates/storybook/src/stories/kitchen_sink.rs deleted file mode 100644 index aaddf733f8..0000000000 --- a/crates/storybook/src/stories/kitchen_sink.rs +++ /dev/null @@ -1,32 +0,0 @@ -use gpui::{Entity, Render, prelude::*}; -use story::Story; -use strum::IntoEnumIterator; -use ui::prelude::*; - -use crate::story_selector::ComponentStory; - -pub struct KitchenSinkStory; - -impl KitchenSinkStory { - pub fn model(cx: &mut App) -> Entity { - cx.new(|_| Self) - } -} - -impl Render for KitchenSinkStory { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let component_stories = ComponentStory::iter() - .map(|selector| selector.story(window, cx)) - .collect::>(); - - Story::container(cx) - .id("kitchen-sink") - .overflow_y_scroll() - .child(Story::title("Kitchen Sink", cx)) - .child(Story::label("Components", cx)) - .child(div().flex().flex_col().children(component_stories)) - // Add a bit of space at the bottom of the kitchen sink so elements - // don't end up squished right up against the bottom of the screen. - .child(div().p_4()) - } -} diff --git a/crates/storybook/src/stories/overflow_scroll.rs b/crates/storybook/src/stories/overflow_scroll.rs deleted file mode 100644 index a9ba09d6a3..0000000000 --- a/crates/storybook/src/stories/overflow_scroll.rs +++ /dev/null @@ -1,41 +0,0 @@ -use gpui::Render; -use story::Story; - -use ui::prelude::*; - -pub struct OverflowScrollStory; - -impl Render for OverflowScrollStory { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - Story::container(cx) - .child(Story::title("Overflow Scroll", cx)) - .child(Story::label("`overflow_x_scroll`", cx)) - .child( - h_flex() - .id("overflow_x_scroll") - .gap_2() - .overflow_x_scroll() - .children((0..100).map(|i| { - div() - .p_4() - .debug_bg_cyan() - .child(SharedString::from(format!("Child {}", i + 1))) - })), - ) - .child(Story::label("`overflow_y_scroll`", cx)) - .child( - v_flex() - .w_full() - .flex_1() - .id("overflow_y_scroll") - .gap_2() - .overflow_y_scroll() - .children((0..100).map(|i| { - div() - .p_4() - .debug_bg_green() - .child(SharedString::from(format!("Child {}", i + 1))) - })), - ) - } -} diff --git a/crates/storybook/src/stories/picker.rs b/crates/storybook/src/stories/picker.rs deleted file mode 100644 index d2d9a854a1..0000000000 --- a/crates/storybook/src/stories/picker.rs +++ /dev/null @@ -1,206 +0,0 @@ -use fuzzy::StringMatchCandidate; -use gpui::{App, Entity, KeyBinding, Render, SharedString, Styled, Task, Window, div, prelude::*}; -use picker::{Picker, PickerDelegate}; -use std::sync::Arc; -use ui::{Label, ListItem}; -use ui::{ListItemSpacing, prelude::*}; - -pub struct PickerStory { - picker: Entity>, -} - -struct Delegate { - candidates: Arc<[StringMatchCandidate]>, - matches: Vec, - selected_ix: usize, -} - -impl Delegate { - fn new(strings: &[&str]) -> Self { - Self { - candidates: strings - .iter() - .copied() - .enumerate() - .map(|(id, string)| StringMatchCandidate::new(id, string)) - .collect(), - matches: vec![], - selected_ix: 0, - } - } -} - -impl PickerDelegate for Delegate { - type ListItem = ListItem; - - fn match_count(&self) -> usize { - self.candidates.len() - } - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - "Test".into() - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _window: &mut Window, - _cx: &mut Context>, - ) -> Option { - let candidate_ix = self.matches.get(ix)?; - // TASK: Make StringMatchCandidate::string a SharedString - let candidate = SharedString::from(self.candidates[*candidate_ix].string.clone()); - - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .child(Label::new(candidate)), - ) - } - - fn selected_index(&self) -> usize { - self.selected_ix - } - - fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context>) { - self.selected_ix = ix; - cx.notify(); - } - - fn confirm(&mut self, secondary: bool, _window: &mut Window, _cx: &mut Context>) { - let candidate_ix = self.matches[self.selected_ix]; - let candidate = self.candidates[candidate_ix].string.clone(); - - if secondary { - eprintln!("Secondary confirmed {}", candidate) - } else { - eprintln!("Confirmed {}", candidate) - } - } - - fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { - cx.quit(); - } - - fn update_matches( - &mut self, - query: String, - _: &mut Window, - cx: &mut Context>, - ) -> Task<()> { - let candidates = self.candidates.clone(); - self.matches = cx - .background_executor() - .block(fuzzy::match_strings( - &candidates, - &query, - true, - true, - 100, - &Default::default(), - cx.background_executor().clone(), - )) - .into_iter() - .map(|r| r.candidate_id) - .collect(); - self.selected_ix = 0; - Task::ready(()) - } -} - -impl PickerStory { - pub fn new(window: &mut Window, cx: &mut App) -> Entity { - cx.new(|cx| { - cx.bind_keys([ - KeyBinding::new("up", menu::SelectPrevious, Some("picker")), - KeyBinding::new("pageup", menu::SelectFirst, Some("picker")), - KeyBinding::new("shift-pageup", menu::SelectFirst, Some("picker")), - KeyBinding::new("ctrl-p", menu::SelectPrevious, Some("picker")), - KeyBinding::new("down", menu::SelectNext, Some("picker")), - KeyBinding::new("pagedown", menu::SelectLast, Some("picker")), - KeyBinding::new("shift-pagedown", menu::SelectFirst, Some("picker")), - KeyBinding::new("ctrl-n", menu::SelectNext, Some("picker")), - KeyBinding::new("cmd-up", menu::SelectFirst, Some("picker")), - KeyBinding::new("cmd-down", menu::SelectLast, Some("picker")), - KeyBinding::new("enter", menu::Confirm, Some("picker")), - KeyBinding::new("ctrl-enter", menu::SecondaryConfirm, Some("picker")), - KeyBinding::new("cmd-enter", menu::SecondaryConfirm, Some("picker")), - KeyBinding::new("escape", menu::Cancel, Some("picker")), - KeyBinding::new("ctrl-c", menu::Cancel, Some("picker")), - ]); - - PickerStory { - picker: cx.new(|cx| { - let mut delegate = Delegate::new(&[ - "Baguette (France)", - "Baklava (Turkey)", - "Beef Wellington (UK)", - "Biryani (India)", - "Borscht (Ukraine)", - "Bratwurst (Germany)", - "Bulgogi (Korea)", - "Burrito (USA)", - "Ceviche (Peru)", - "Chicken Tikka Masala (India)", - "Churrasco (Brazil)", - "Couscous (North Africa)", - "Croissant (France)", - "Dim Sum (China)", - "Empanada (Argentina)", - "Fajitas (Mexico)", - "Falafel (Middle East)", - "Feijoada (Brazil)", - "Fish and Chips (UK)", - "Fondue (Switzerland)", - "Goulash (Hungary)", - "Haggis (Scotland)", - "Kebab (Middle East)", - "Kimchi (Korea)", - "Lasagna (Italy)", - "Maple Syrup Pancakes (Canada)", - "Moussaka (Greece)", - "Pad Thai (Thailand)", - "Paella (Spain)", - "Pancakes (USA)", - "Pasta Carbonara (Italy)", - "Pavlova (Australia)", - "Peking Duck (China)", - "Pho (Vietnam)", - "Pierogi (Poland)", - "Pizza (Italy)", - "Poutine (Canada)", - "Pretzel (Germany)", - "Ramen (Japan)", - "Rendang (Indonesia)", - "Sashimi (Japan)", - "Satay (Indonesia)", - "Shepherd's Pie (Ireland)", - "Sushi (Japan)", - "Tacos (Mexico)", - "Tandoori Chicken (India)", - "Tortilla (Spain)", - "Tzatziki (Greece)", - "Wiener Schnitzel (Austria)", - ]); - delegate.update_matches("".into(), window, cx).detach(); - - let picker = Picker::uniform_list(delegate, window, cx); - picker.focus(window, cx); - picker - }), - } - }) - } -} - -impl Render for PickerStory { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - div() - .bg(cx.theme().styles.colors.background) - .size_full() - .child(self.picker.clone()) - } -} diff --git a/crates/storybook/src/stories/scroll.rs b/crates/storybook/src/stories/scroll.rs deleted file mode 100644 index 8a4c7ea768..0000000000 --- a/crates/storybook/src/stories/scroll.rs +++ /dev/null @@ -1,52 +0,0 @@ -use gpui::{App, Entity, Render, SharedString, Styled, Window, div, prelude::*, px}; -use ui::Tooltip; -use ui::prelude::*; - -pub struct ScrollStory; - -impl ScrollStory { - pub fn model(cx: &mut App) -> Entity { - cx.new(|_| ScrollStory) - } -} - -impl Render for ScrollStory { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let theme = cx.theme(); - let color_1 = theme.status().created; - let color_2 = theme.status().modified; - - div() - .id("parent") - .bg(theme.colors().background) - .size_full() - .overflow_scroll() - .children((0..10).map(|row| { - div() - .w(px(1000.)) - .h(px(100.)) - .flex() - .flex_row() - .children((0..10).map(|column| { - let id = SharedString::from(format!("{}, {}", row, column)); - let bg = if row % 2 == column % 2 { - color_1 - } else { - color_2 - }; - div() - .id(id.clone()) - .tooltip(Tooltip::text(id)) - .bg(bg) - .size(px(100_f32)) - .when(row >= 5 && column >= 5, |d| { - d.overflow_scroll() - .child(div().size(px(50.)).bg(color_1)) - .child(div().size(px(50.)).bg(color_2)) - .child(div().size(px(50.)).bg(color_1)) - .child(div().size(px(50.)).bg(color_2)) - }) - })) - })) - } -} diff --git a/crates/storybook/src/stories/text.rs b/crates/storybook/src/stories/text.rs deleted file mode 100644 index 7ba2378307..0000000000 --- a/crates/storybook/src/stories/text.rs +++ /dev/null @@ -1,120 +0,0 @@ -use gpui::{ - App, AppContext as _, Context, Entity, HighlightStyle, InteractiveText, IntoElement, - ParentElement, Render, Styled, StyledText, Window, div, green, red, -}; -use indoc::indoc; -use story::*; - -pub struct TextStory; - -impl TextStory { - pub fn model(cx: &mut App) -> Entity { - cx.new(|_| Self) - } -} - -impl Render for TextStory { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - Story::container(cx) - .child(Story::title("Text", cx)) - .children(vec![ - StorySection::new() - .child( - StoryItem::new("Default", div().bg(gpui::blue()).child("Hello World!")) - .usage(indoc! {r##" - div() - .child("Hello World!") - "## - }), - ) - .child( - StoryItem::new( - "Wrapping Text", - div().max_w_96().child(concat!( - "The quick brown fox jumps over the lazy dog. ", - "Meanwhile, the lazy dog decided it was time for a change. ", - "He started daily workout routines, ate healthier and became the fastest dog in town.", - )), - ) - .description("Set a width or max-width to enable text wrapping.") - .usage(indoc! {r##" - div() - .max_w_96() - .child("Some text that you want to wrap.") - "## - }), - ) - .child( - StoryItem::new( - "tbd", - div().flex().w_96().child( - div().overflow_hidden().child(concat!( - "flex-row. width 96. overflow-hidden. The quick brown fox jumps over the lazy dog. ", - "Meanwhile, the lazy dog decided it was time for a change. ", - "He started daily workout routines, ate healthier and became the fastest dog in town.", - )), - ), - ), - ) - .child( - StoryItem::new( - "Text in Horizontal Flex", - div().flex().w_96().bg(red()).child(concat!( - "flex-row. width 96. The quick brown fox jumps over the lazy dog. ", - "Meanwhile, the lazy dog decided it was time for a change. ", - "He started daily workout routines, ate healthier and became the fastest dog in town.", - )), - ) - .usage(indoc! {r##" - // NOTE: When rendering text in a horizontal flex container, - // Taffy will not pass width constraints down from the parent. - // To fix this, render text in a parent with overflow: hidden - - div() - .max_w_96() - .child("Some text that you want to wrap.") - "## - }), - ) - .child( - StoryItem::new( - "Interactive Text", - InteractiveText::new( - "interactive", - StyledText::new("Hello world, how is it going?").with_default_highlights( - &window.text_style(), - [ - ( - 6..11, - HighlightStyle { - background_color: Some(green()), - ..Default::default() - }, - ), - ], - ), - ) - .on_click(vec![2..4, 1..3, 7..9], |range_ix, _, _cx| { - println!("Clicked range {range_ix}"); - }), - ) - .usage(indoc! {r##" - InteractiveText::new( - "interactive", - StyledText::new("Hello world, how is it going?").with_highlights(&window.text_style(), [ - (6..11, HighlightStyle { - background_color: Some(green()), - ..Default::default() - }), - ]), - ) - .on_click(vec![2..4, 1..3, 7..9], |range_ix, _cx| { - println!("Clicked range {range_ix}"); - }) - "## - }), - ), - ]) - .into_element() - } -} diff --git a/crates/storybook/src/stories/viewport_units.rs b/crates/storybook/src/stories/viewport_units.rs deleted file mode 100644 index 1259a713ee..0000000000 --- a/crates/storybook/src/stories/viewport_units.rs +++ /dev/null @@ -1,32 +0,0 @@ -use gpui::Render; -use story::Story; - -use ui::prelude::*; - -pub struct ViewportUnitsStory; - -impl Render for ViewportUnitsStory { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - Story::container(cx).child( - div() - .flex() - .flex_row() - .child( - div() - .w(vw(0.5, window)) - .h(vh(0.8, window)) - .bg(gpui::red()) - .text_color(gpui::white()) - .child("50vw, 80vh"), - ) - .child( - div() - .w(vw(0.25, window)) - .h(vh(0.33, window)) - .bg(gpui::green()) - .text_color(gpui::white()) - .child("25vw, 33vh"), - ), - ) - } -} diff --git a/crates/storybook/src/stories/with_rem_size.rs b/crates/storybook/src/stories/with_rem_size.rs deleted file mode 100644 index eeca3fb89f..0000000000 --- a/crates/storybook/src/stories/with_rem_size.rs +++ /dev/null @@ -1,61 +0,0 @@ -use gpui::{AnyElement, Hsla, Render}; -use story::Story; - -use ui::{prelude::*, utils::WithRemSize}; - -pub struct WithRemSizeStory; - -impl Render for WithRemSizeStory { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - Story::container(cx).child( - Example::new(16., gpui::red()) - .child( - Example::new(24., gpui::green()) - .child(Example::new(8., gpui::blue())) - .child(Example::new(16., gpui::yellow())), - ) - .child( - Example::new(12., gpui::green()) - .child(Example::new(48., gpui::blue())) - .child(Example::new(16., gpui::yellow())), - ), - ) - } -} - -#[derive(IntoElement)] -struct Example { - rem_size: Pixels, - border_color: Hsla, - children: Vec, -} - -impl Example { - pub fn new(rem_size: impl Into, border_color: Hsla) -> Self { - Self { - rem_size: rem_size.into(), - border_color, - children: Vec::new(), - } - } -} - -impl ParentElement for Example { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements); - } -} - -impl RenderOnce for Example { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - WithRemSize::new(self.rem_size).child( - v_flex() - .gap_2() - .p_2() - .border_2() - .border_color(self.border_color) - .child(Label::new(format!("1rem = {}px", f32::from(self.rem_size)))) - .children(self.children), - ) - } -} diff --git a/crates/storybook/src/story_selector.rs b/crates/storybook/src/story_selector.rs deleted file mode 100644 index 7f70d58b3b..0000000000 --- a/crates/storybook/src/story_selector.rs +++ /dev/null @@ -1,113 +0,0 @@ -use std::str::FromStr; -use std::sync::OnceLock; - -use crate::stories::*; -use clap::ValueEnum; -use clap::builder::PossibleValue; -use gpui::AnyView; -use strum::{EnumIter, EnumString, IntoEnumIterator}; -use ui::prelude::*; - -#[derive(Debug, PartialEq, Eq, Clone, Copy, strum::Display, EnumString, EnumIter)] -#[strum(serialize_all = "snake_case")] -pub enum ComponentStory { - ApplicationMenu, - AutoHeightEditor, - CollabNotification, - ContextMenu, - Cursor, - Focus, - OverflowScroll, - Picker, - Scroll, - Text, - ViewportUnits, - WithRemSize, - IndentGuides, -} - -impl ComponentStory { - pub fn story(&self, window: &mut Window, cx: &mut App) -> AnyView { - match self { - Self::ApplicationMenu => cx - .new(|cx| title_bar::ApplicationMenuStory::new(window, cx)) - .into(), - Self::AutoHeightEditor => AutoHeightEditorStory::new(window, cx).into(), - Self::CollabNotification => cx - .new(|_| collab_ui::notifications::CollabNotificationStory) - .into(), - Self::ContextMenu => cx.new(|_| ui::ContextMenuStory).into(), - Self::Cursor => cx.new(|_| crate::stories::CursorStory).into(), - Self::Focus => FocusStory::model(window, cx).into(), - Self::OverflowScroll => cx.new(|_| crate::stories::OverflowScrollStory).into(), - Self::Picker => PickerStory::new(window, cx).into(), - Self::Scroll => ScrollStory::model(cx).into(), - Self::Text => TextStory::model(cx).into(), - Self::ViewportUnits => cx.new(|_| crate::stories::ViewportUnitsStory).into(), - Self::WithRemSize => cx.new(|_| crate::stories::WithRemSizeStory).into(), - Self::IndentGuides => crate::stories::IndentGuidesStory::model(window, cx).into(), - } - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum StorySelector { - Component(ComponentStory), - KitchenSink, -} - -impl FromStr for StorySelector { - type Err = anyhow::Error; - - fn from_str(raw_story_name: &str) -> std::result::Result { - use anyhow::Context as _; - - let story = raw_story_name.to_ascii_lowercase(); - - if story == "kitchen_sink" { - return Ok(Self::KitchenSink); - } - - if let Some((_, story)) = story.split_once("components/") { - let component_story = ComponentStory::from_str(story) - .with_context(|| format!("story not found for component '{story}'"))?; - - return Ok(Self::Component(component_story)); - } - - anyhow::bail!("story not found for '{raw_story_name}'") - } -} - -impl StorySelector { - pub fn story(&self, window: &mut Window, cx: &mut App) -> AnyView { - match self { - Self::Component(component_story) => component_story.story(window, cx), - Self::KitchenSink => KitchenSinkStory::model(cx).into(), - } - } -} - -/// The list of all stories available in the storybook. -static ALL_STORY_SELECTORS: OnceLock> = OnceLock::new(); - -impl ValueEnum for StorySelector { - fn value_variants<'a>() -> &'a [Self] { - (ALL_STORY_SELECTORS.get_or_init(|| { - let component_stories = ComponentStory::iter().map(StorySelector::Component); - - component_stories - .chain(std::iter::once(StorySelector::KitchenSink)) - .collect::>() - })) as _ - } - - fn to_possible_value(&self) -> Option { - let value = match self { - Self::Component(story) => format!("components/{story}"), - Self::KitchenSink => "kitchen_sink".to_string(), - }; - - Some(PossibleValue::new(value)) - } -} diff --git a/crates/storybook/src/storybook.rs b/crates/storybook/src/storybook.rs deleted file mode 100644 index 42ca921e63..0000000000 --- a/crates/storybook/src/storybook.rs +++ /dev/null @@ -1,161 +0,0 @@ -mod actions; -mod app_menus; -mod assets; -mod stories; -mod story_selector; - -use std::sync::Arc; - -use clap::Parser; -use dialoguer::FuzzySelect; -use gpui::{ - AnyView, App, Bounds, Context, Render, Window, WindowBounds, WindowOptions, - colors::{Colors, GlobalColors}, - div, px, size, -}; -use log::LevelFilter; -use reqwest_client::ReqwestClient; -use settings::{KeymapFile, Settings}; -use simplelog::SimpleLogger; -use strum::IntoEnumIterator; -use theme::ThemeSettings; -use ui::prelude::*; - -use crate::app_menus::app_menus; -use crate::assets::Assets; -use crate::story_selector::{ComponentStory, StorySelector}; -use actions::Quit; -pub use indoc::indoc; - -#[derive(Parser)] -#[command(author, version, about, long_about = None)] -struct Args { - #[arg(value_enum)] - story: Option, - - /// The name of the theme to use in the storybook. - /// - /// If not provided, the default theme will be used. - #[arg(long)] - theme: Option, -} - -fn main() { - SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger"); - - menu::init(); - let args = Args::parse(); - - let story_selector = args.story.unwrap_or_else(|| { - let stories = ComponentStory::iter().collect::>(); - - ctrlc::set_handler(move || {}).unwrap(); - - let result = FuzzySelect::new() - .with_prompt("Choose a story to run:") - .items(&stories) - .interact(); - - let Ok(selection) = result else { - dialoguer::console::Term::stderr().show_cursor().unwrap(); - std::process::exit(0); - }; - - StorySelector::Component(stories[selection]) - }); - let theme_name = args.theme.unwrap_or("One Dark".to_string()); - - gpui::Application::new().with_assets(Assets).run(move |cx| { - load_embedded_fonts(cx).unwrap(); - - cx.set_global(GlobalColors(Arc::new(Colors::default()))); - - let http_client = ReqwestClient::user_agent("zed_storybook").unwrap(); - cx.set_http_client(Arc::new(http_client)); - - settings::init(cx); - theme::init(theme::LoadThemes::All(Box::new(Assets)), cx); - - let selector = story_selector; - - let mut theme_settings = ThemeSettings::get_global(cx).clone(); - theme_settings.theme = - theme::ThemeSelection::Static(settings::ThemeName(theme_name.into())); - ThemeSettings::override_global(theme_settings, cx); - - editor::init(cx); - init(cx); - load_storybook_keymap(cx); - cx.set_menus(app_menus()); - - let size = size(px(1500.), px(780.)); - let bounds = Bounds::centered(None, size, cx); - let _window = cx.open_window( - WindowOptions { - window_bounds: Some(WindowBounds::Windowed(bounds)), - ..Default::default() - }, - move |window, cx| { - theme::setup_ui_font(window, cx); - - cx.new(|cx| StoryWrapper::new(selector.story(window, cx))) - }, - ); - - cx.activate(true); - }); -} - -#[derive(Clone)] -pub struct StoryWrapper { - story: AnyView, -} - -impl StoryWrapper { - pub(crate) fn new(story: AnyView) -> Self { - Self { story } - } -} - -impl Render for StoryWrapper { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div() - .flex() - .flex_col() - .size_full() - .font_family(".ZedMono") - .child(self.story.clone()) - } -} - -fn load_embedded_fonts(cx: &App) -> anyhow::Result<()> { - let font_paths = cx.asset_source().list("fonts")?; - let mut embedded_fonts = Vec::new(); - for font_path in font_paths { - if font_path.ends_with(".ttf") { - let font_bytes = cx - .asset_source() - .load(&font_path)? - .expect("Should never be None in the storybook"); - embedded_fonts.push(font_bytes); - } - } - - cx.text_system().add_fonts(embedded_fonts) -} - -fn load_storybook_keymap(cx: &mut App) { - cx.bind_keys(KeymapFile::load_asset("keymaps/storybook.json", None, cx).unwrap()); -} - -pub fn init(cx: &mut App) { - cx.on_action(quit); -} - -fn quit(_: &Quit, cx: &mut App) { - cx.spawn(async move |cx| { - cx.update(|cx| cx.quit())?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx); -} diff --git a/crates/streaming_diff/Cargo.toml b/crates/streaming_diff/Cargo.toml deleted file mode 100644 index b3645a182c..0000000000 --- a/crates/streaming_diff/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "streaming_diff" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/streaming_diff.rs" - -[dependencies] -ordered-float.workspace = true -rope.workspace = true - -[dev-dependencies] -rand.workspace = true -util = { workspace = true, features = ["test-support"] } diff --git a/crates/streaming_diff/LICENSE-GPL b/crates/streaming_diff/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/streaming_diff/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/streaming_diff/src/streaming_diff.rs b/crates/streaming_diff/src/streaming_diff.rs deleted file mode 100644 index 5677981b0d..0000000000 --- a/crates/streaming_diff/src/streaming_diff.rs +++ /dev/null @@ -1,1104 +0,0 @@ -use ordered_float::OrderedFloat; -use rope::{Point, Rope, TextSummary}; -use std::collections::{BTreeSet, HashMap}; -use std::{ - cmp, - fmt::{self, Debug}, - ops::Range, -}; - -#[derive(Default)] -struct Matrix { - cells: Vec, - rows: usize, - cols: usize, -} - -impl Matrix { - fn new() -> Self { - Self { - cells: Vec::new(), - rows: 0, - cols: 0, - } - } - - fn resize(&mut self, rows: usize, cols: usize) { - self.cells.resize(rows * cols, 0.); - self.rows = rows; - self.cols = cols; - } - - fn swap_columns(&mut self, col1: usize, col2: usize) { - if col1 == col2 { - return; - } - - if col1 >= self.cols { - panic!("column out of bounds"); - } - - if col2 >= self.cols { - panic!("column out of bounds"); - } - - unsafe { - let ptr = self.cells.as_mut_ptr(); - std::ptr::swap_nonoverlapping( - ptr.add(col1 * self.rows), - ptr.add(col2 * self.rows), - self.rows, - ); - } - } - - fn get(&self, row: usize, col: usize) -> f64 { - if row >= self.rows { - panic!("row out of bounds") - } - - if col >= self.cols { - panic!("column out of bounds") - } - self.cells[col * self.rows + row] - } - - fn set(&mut self, row: usize, col: usize, value: f64) { - if row >= self.rows { - panic!("row out of bounds") - } - - if col >= self.cols { - panic!("column out of bounds") - } - - self.cells[col * self.rows + row] = value; - } -} - -impl Debug for Matrix { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - writeln!(f)?; - for i in 0..self.rows { - for j in 0..self.cols { - write!(f, "{:5}", self.get(i, j))?; - } - writeln!(f)?; - } - Ok(()) - } -} - -#[derive(Debug, Clone)] -pub enum CharOperation { - Insert { text: String }, - Delete { bytes: usize }, - Keep { bytes: usize }, -} - -#[derive(Default)] -pub struct StreamingDiff { - old: Vec, - new: Vec, - scores: Matrix, - old_text_ix: usize, - new_text_ix: usize, - equal_runs: HashMap<(usize, usize), u32>, -} - -impl StreamingDiff { - const INSERTION_SCORE: f64 = -1.; - const DELETION_SCORE: f64 = -20.; - const EQUALITY_BASE: f64 = 1.8; - const MAX_EQUALITY_EXPONENT: i32 = 16; - - pub fn new(old: String) -> Self { - let old = old.chars().collect::>(); - let mut scores = Matrix::new(); - scores.resize(old.len() + 1, 1); - for i in 0..=old.len() { - scores.set(i, 0, i as f64 * Self::DELETION_SCORE); - } - Self { - old, - new: Vec::new(), - scores, - old_text_ix: 0, - new_text_ix: 0, - equal_runs: Default::default(), - } - } - - pub fn push_new(&mut self, text: &str) -> Vec { - self.new.extend(text.chars()); - self.scores.swap_columns(0, self.scores.cols - 1); - self.scores - .resize(self.old.len() + 1, self.new.len() - self.new_text_ix + 1); - self.equal_runs.retain(|(_i, j), _| *j == self.new_text_ix); - - for j in self.new_text_ix + 1..=self.new.len() { - let relative_j = j - self.new_text_ix; - - self.scores - .set(0, relative_j, j as f64 * Self::INSERTION_SCORE); - for i in 1..=self.old.len() { - let insertion_score = self.scores.get(i, relative_j - 1) + Self::INSERTION_SCORE; - let deletion_score = self.scores.get(i - 1, relative_j) + Self::DELETION_SCORE; - let equality_score = if self.old[i - 1] == self.new[j - 1] { - let mut equal_run = self.equal_runs.get(&(i - 1, j - 1)).copied().unwrap_or(0); - equal_run += 1; - self.equal_runs.insert((i, j), equal_run); - - let exponent = cmp::min(equal_run as i32 / 4, Self::MAX_EQUALITY_EXPONENT); - self.scores.get(i - 1, relative_j - 1) + Self::EQUALITY_BASE.powi(exponent) - } else { - f64::NEG_INFINITY - }; - - let score = insertion_score.max(deletion_score).max(equality_score); - self.scores.set(i, relative_j, score); - } - } - - let mut max_score = f64::NEG_INFINITY; - let mut next_old_text_ix = self.old_text_ix; - let next_new_text_ix = self.new.len(); - for i in self.old_text_ix..=self.old.len() { - let score = self.scores.get(i, next_new_text_ix - self.new_text_ix); - if score > max_score { - max_score = score; - next_old_text_ix = i; - } - } - - let hunks = self.backtrack(next_old_text_ix, next_new_text_ix); - self.old_text_ix = next_old_text_ix; - self.new_text_ix = next_new_text_ix; - hunks - } - - fn backtrack(&self, old_text_ix: usize, new_text_ix: usize) -> Vec { - let mut pending_insert: Option> = None; - let mut hunks = Vec::new(); - let mut i = old_text_ix; - let mut j = new_text_ix; - while (i, j) != (self.old_text_ix, self.new_text_ix) { - let insertion_score = if j > self.new_text_ix { - Some((i, j - 1)) - } else { - None - }; - let deletion_score = if i > self.old_text_ix { - Some((i - 1, j)) - } else { - None - }; - let equality_score = if i > self.old_text_ix && j > self.new_text_ix { - if self.old[i - 1] == self.new[j - 1] { - Some((i - 1, j - 1)) - } else { - None - } - } else { - None - }; - - let (prev_i, prev_j) = [insertion_score, deletion_score, equality_score] - .iter() - .max_by_key(|cell| { - cell.map(|(i, j)| OrderedFloat(self.scores.get(i, j - self.new_text_ix))) - }) - .unwrap() - .unwrap(); - - if prev_i == i && prev_j == j - 1 { - if let Some(pending_insert) = pending_insert.as_mut() { - pending_insert.start = prev_j; - } else { - pending_insert = Some(prev_j..j); - } - } else { - if let Some(range) = pending_insert.take() { - hunks.push(CharOperation::Insert { - text: self.new[range].iter().collect(), - }); - } - - let char_len = self.old[i - 1].len_utf8(); - if prev_i == i - 1 && prev_j == j { - if let Some(CharOperation::Delete { bytes: len }) = hunks.last_mut() { - *len += char_len; - } else { - hunks.push(CharOperation::Delete { bytes: char_len }) - } - } else if let Some(CharOperation::Keep { bytes: len }) = hunks.last_mut() { - *len += char_len; - } else { - hunks.push(CharOperation::Keep { bytes: char_len }) - } - } - - i = prev_i; - j = prev_j; - } - - if let Some(range) = pending_insert.take() { - hunks.push(CharOperation::Insert { - text: self.new[range].iter().collect(), - }); - } - - hunks.reverse(); - hunks - } - - pub fn finish(self) -> Vec { - self.backtrack(self.old.len(), self.new.len()) - } -} - -#[derive(Debug, Clone, PartialEq)] -pub enum LineOperation { - Insert { lines: u32 }, - Delete { lines: u32 }, - Keep { lines: u32 }, -} - -#[derive(Debug, Default)] -pub struct LineDiff { - inserted_newline_at_end: bool, - /// The extent of kept and deleted text. - old_end: Point, - /// The extent of kept and inserted text. - new_end: Point, - /// Deleted rows, expressed in terms of the old text. - deleted_rows: BTreeSet, - /// Inserted rows, expressed in terms of the new text. - inserted_rows: BTreeSet, - buffered_insert: String, - /// After deleting a newline, we buffer deletion until we keep or insert a character. - buffered_delete: usize, -} - -impl LineDiff { - pub fn push_char_operations<'a>( - &mut self, - operations: impl IntoIterator, - old_text: &Rope, - ) { - for operation in operations { - self.push_char_operation(operation, old_text); - } - } - - pub fn push_char_operation(&mut self, operation: &CharOperation, old_text: &Rope) { - match operation { - CharOperation::Insert { text } => { - self.flush_delete(old_text); - - if is_line_start(self.old_end) { - if let Some(newline_ix) = text.rfind('\n') { - let (prefix, suffix) = text.split_at(newline_ix + 1); - self.buffered_insert.push_str(prefix); - self.flush_insert(old_text); - self.buffered_insert.push_str(suffix); - } else { - self.buffered_insert.push_str(text); - } - } else { - self.buffered_insert.push_str(text); - if !text.ends_with('\n') { - self.flush_insert(old_text); - } - } - } - CharOperation::Delete { bytes } => { - self.buffered_delete += bytes; - - let common_suffix_len = self.trim_buffered_end(old_text); - self.flush_insert(old_text); - - if common_suffix_len > 0 || !is_line_end(self.old_end, old_text) { - self.flush_delete(old_text); - self.keep(common_suffix_len, old_text); - } - } - CharOperation::Keep { bytes } => { - self.flush_delete(old_text); - self.flush_insert(old_text); - self.keep(*bytes, old_text); - } - } - } - - fn flush_insert(&mut self, old_text: &Rope) { - if self.buffered_insert.is_empty() { - return; - } - - let new_start = self.new_end; - let lines = TextSummary::from(self.buffered_insert.as_str()).lines; - self.new_end += lines; - - if is_line_start(self.old_end) { - if self.new_end.column == 0 { - self.inserted_rows.extend(new_start.row..self.new_end.row); - } else { - self.deleted_rows.insert(self.old_end.row); - self.inserted_rows.extend(new_start.row..=self.new_end.row); - } - } else if is_line_end(self.old_end, old_text) { - if self.buffered_insert.starts_with('\n') { - self.inserted_rows - .extend(new_start.row + 1..=self.new_end.row); - self.inserted_newline_at_end = true; - } else { - if !self.inserted_newline_at_end { - self.deleted_rows.insert(self.old_end.row); - } - self.inserted_rows.extend(new_start.row..=self.new_end.row); - } - } else { - self.deleted_rows.insert(self.old_end.row); - self.inserted_rows.extend(new_start.row..=self.new_end.row); - } - - self.buffered_insert.clear(); - } - - fn flush_delete(&mut self, old_text: &Rope) { - if self.buffered_delete == 0 { - return; - } - - let old_start = self.old_end; - self.old_end = - old_text.offset_to_point(old_text.point_to_offset(self.old_end) + self.buffered_delete); - - if is_line_end(old_start, old_text) && is_line_end(self.old_end, old_text) { - self.deleted_rows - .extend(old_start.row + 1..=self.old_end.row); - } else if is_line_start(old_start) - && (is_line_start(self.old_end) && self.old_end < old_text.max_point()) - && self.new_end.column == 0 - { - self.deleted_rows.extend(old_start.row..self.old_end.row); - } else { - self.inserted_rows.insert(self.new_end.row); - self.deleted_rows.extend(old_start.row..=self.old_end.row); - } - - self.inserted_newline_at_end = false; - self.buffered_delete = 0; - } - - fn keep(&mut self, bytes: usize, old_text: &Rope) { - if bytes == 0 { - return; - } - - let lines = - old_text.offset_to_point(old_text.point_to_offset(self.old_end) + bytes) - self.old_end; - self.old_end += lines; - self.new_end += lines; - self.inserted_newline_at_end = false; - } - - fn trim_buffered_end(&mut self, old_text: &Rope) -> usize { - let old_start_offset = old_text.point_to_offset(self.old_end); - let old_end_offset = old_start_offset + self.buffered_delete; - - let new_chars = self.buffered_insert.chars().rev(); - let old_chars = old_text - .chunks_in_range(old_start_offset..old_end_offset) - .flat_map(|chunk| chunk.chars().rev()); - - let mut common_suffix_len = 0; - for (new_ch, old_ch) in new_chars.zip(old_chars) { - if new_ch == old_ch { - common_suffix_len += new_ch.len_utf8(); - } else { - break; - } - } - - self.buffered_delete -= common_suffix_len; - self.buffered_insert - .truncate(self.buffered_insert.len() - common_suffix_len); - - common_suffix_len - } - - pub fn finish(&mut self, old_text: &Rope) { - self.flush_insert(old_text); - self.flush_delete(old_text); - - let old_start = self.old_end; - self.old_end = old_text.max_point(); - self.new_end += self.old_end - old_start; - } - - pub fn line_operations(&self) -> Vec { - let mut ops = Vec::new(); - let mut deleted_rows = self.deleted_rows.iter().copied().peekable(); - let mut inserted_rows = self.inserted_rows.iter().copied().peekable(); - let mut old_row = 0; - let mut new_row = 0; - - while deleted_rows.peek().is_some() || inserted_rows.peek().is_some() { - // Check for a run of deleted lines at current old row. - if Some(old_row) == deleted_rows.peek().copied() { - if let Some(LineOperation::Delete { lines }) = ops.last_mut() { - *lines += 1; - } else { - ops.push(LineOperation::Delete { lines: 1 }); - } - old_row += 1; - deleted_rows.next(); - } else if Some(new_row) == inserted_rows.peek().copied() { - if let Some(LineOperation::Insert { lines }) = ops.last_mut() { - *lines += 1; - } else { - ops.push(LineOperation::Insert { lines: 1 }); - } - new_row += 1; - inserted_rows.next(); - } else { - // Keep lines until the next deletion, insertion, or the end of the old text. - let lines_to_next_deletion = inserted_rows - .peek() - .copied() - .unwrap_or(self.new_end.row + 1) - - new_row; - let lines_to_next_insertion = - deleted_rows.peek().copied().unwrap_or(self.old_end.row + 1) - old_row; - let kept_lines = - cmp::max(1, cmp::min(lines_to_next_insertion, lines_to_next_deletion)); - if kept_lines > 0 { - ops.push(LineOperation::Keep { lines: kept_lines }); - old_row += kept_lines; - new_row += kept_lines; - } - } - } - - if old_row < self.old_end.row + 1 { - ops.push(LineOperation::Keep { - lines: self.old_end.row + 1 - old_row, - }); - } - - ops - } -} - -fn is_line_start(point: Point) -> bool { - point.column == 0 -} - -fn is_line_end(point: Point, text: &Rope) -> bool { - text.line_len(point.row) == point.column -} - -#[cfg(test)] -mod tests { - use super::*; - use rand::prelude::*; - use std::env; - - #[test] - fn test_delete_first_of_two_lines() { - let old_text = "aaaa\nbbbb"; - let char_ops = vec![ - CharOperation::Delete { bytes: 5 }, - CharOperation::Keep { bytes: 4 }, - ]; - let expected_line_ops = vec![ - LineOperation::Delete { lines: 1 }, - LineOperation::Keep { lines: 1 }, - ]; - let new_text = apply_char_operations(old_text, &char_ops); - assert_eq!( - new_text, - apply_line_operations(old_text, &new_text, &expected_line_ops) - ); - - let line_ops = char_ops_to_line_ops(old_text, &char_ops); - assert_eq!(line_ops, expected_line_ops); - } - - #[test] - fn test_delete_second_of_two_lines() { - let old_text = "aaaa\nbbbb"; - let char_ops = vec![ - CharOperation::Keep { bytes: 5 }, - CharOperation::Delete { bytes: 4 }, - ]; - let line_ops = char_ops_to_line_ops(old_text, &char_ops); - assert_eq!( - line_ops, - vec![ - LineOperation::Keep { lines: 1 }, - LineOperation::Delete { lines: 1 }, - LineOperation::Insert { lines: 1 } - ] - ); - let new_text = apply_char_operations(old_text, &char_ops); - assert_eq!( - new_text, - apply_line_operations(old_text, &new_text, &line_ops) - ); - } - - #[test] - fn test_add_new_line() { - let old_text = "aaaa\nbbbb"; - let char_ops = vec![ - CharOperation::Keep { bytes: 9 }, - CharOperation::Insert { - text: "\ncccc".into(), - }, - ]; - let line_ops = char_ops_to_line_ops(old_text, &char_ops); - assert_eq!( - line_ops, - vec![ - LineOperation::Keep { lines: 2 }, - LineOperation::Insert { lines: 1 } - ] - ); - let new_text = apply_char_operations(old_text, &char_ops); - assert_eq!( - new_text, - apply_line_operations(old_text, &new_text, &line_ops) - ); - } - - #[test] - fn test_delete_line_in_middle() { - let old_text = "aaaa\nbbbb\ncccc"; - let char_ops = vec![ - CharOperation::Keep { bytes: 5 }, - CharOperation::Delete { bytes: 5 }, - CharOperation::Keep { bytes: 4 }, - ]; - let line_ops = char_ops_to_line_ops(old_text, &char_ops); - assert_eq!( - line_ops, - vec![ - LineOperation::Keep { lines: 1 }, - LineOperation::Delete { lines: 1 }, - LineOperation::Keep { lines: 1 } - ] - ); - let new_text = apply_char_operations(old_text, &char_ops); - assert_eq!( - new_text, - apply_line_operations(old_text, &new_text, &line_ops) - ); - } - - #[test] - fn test_replace_line() { - let old_text = "aaaa\nbbbb\ncccc"; - let char_ops = vec![ - CharOperation::Keep { bytes: 5 }, - CharOperation::Delete { bytes: 4 }, - CharOperation::Insert { - text: "BBBB".into(), - }, - CharOperation::Keep { bytes: 5 }, - ]; - let line_ops = char_ops_to_line_ops(old_text, &char_ops); - assert_eq!( - line_ops, - vec![ - LineOperation::Keep { lines: 1 }, - LineOperation::Delete { lines: 1 }, - LineOperation::Insert { lines: 1 }, - LineOperation::Keep { lines: 1 } - ] - ); - let new_text = apply_char_operations(old_text, &char_ops); - assert_eq!( - new_text, - apply_line_operations(old_text, &new_text, &line_ops) - ); - } - - #[test] - fn test_multiple_edits_on_different_lines() { - let old_text = "aaaa\nbbbb\ncccc\ndddd"; - let char_ops = vec![ - CharOperation::Insert { text: "A".into() }, - CharOperation::Keep { bytes: 9 }, - CharOperation::Delete { bytes: 5 }, - CharOperation::Keep { bytes: 4 }, - CharOperation::Insert { - text: "\nEEEE".into(), - }, - ]; - let line_ops = char_ops_to_line_ops(old_text, &char_ops); - assert_eq!( - line_ops, - vec![ - LineOperation::Delete { lines: 1 }, - LineOperation::Insert { lines: 1 }, - LineOperation::Keep { lines: 1 }, - LineOperation::Delete { lines: 2 }, - LineOperation::Insert { lines: 2 }, - ] - ); - let new_text = apply_char_operations(old_text, &char_ops); - assert_eq!( - new_text, - apply_line_operations(old_text, &new_text, &line_ops) - ); - } - - #[test] - fn test_edit_at_end_of_line() { - let old_text = "aaaa\nbbbb\ncccc"; - let char_ops = vec![ - CharOperation::Keep { bytes: 4 }, - CharOperation::Insert { text: "A".into() }, - CharOperation::Keep { bytes: 10 }, - ]; - let line_ops = char_ops_to_line_ops(old_text, &char_ops); - assert_eq!( - line_ops, - vec![ - LineOperation::Delete { lines: 1 }, - LineOperation::Insert { lines: 1 }, - LineOperation::Keep { lines: 2 } - ] - ); - let new_text = apply_char_operations(old_text, &char_ops); - assert_eq!( - new_text, - apply_line_operations(old_text, &new_text, &line_ops) - ); - } - - #[test] - fn test_insert_newline_character() { - let old_text = "aaaabbbb"; - let char_ops = vec![ - CharOperation::Keep { bytes: 4 }, - CharOperation::Insert { text: "\n".into() }, - CharOperation::Keep { bytes: 4 }, - ]; - let new_text = apply_char_operations(old_text, &char_ops); - let line_ops = char_ops_to_line_ops(old_text, &char_ops); - assert_eq!( - line_ops, - vec![ - LineOperation::Delete { lines: 1 }, - LineOperation::Insert { lines: 2 } - ] - ); - assert_eq!( - new_text, - apply_line_operations(old_text, &new_text, &line_ops) - ); - } - - #[test] - fn test_insert_newline_at_beginning() { - let old_text = "aaaa\nbbbb"; - let char_ops = vec![ - CharOperation::Insert { text: "\n".into() }, - CharOperation::Keep { bytes: 9 }, - ]; - let line_ops = char_ops_to_line_ops(old_text, &char_ops); - assert_eq!( - line_ops, - vec![ - LineOperation::Insert { lines: 1 }, - LineOperation::Keep { lines: 2 } - ] - ); - let new_text = apply_char_operations(old_text, &char_ops); - assert_eq!( - new_text, - apply_line_operations(old_text, &new_text, &line_ops) - ); - } - - #[test] - fn test_delete_newline() { - let old_text = "aaaa\nbbbb"; - let char_ops = vec![ - CharOperation::Keep { bytes: 4 }, - CharOperation::Delete { bytes: 1 }, - CharOperation::Keep { bytes: 4 }, - ]; - let line_ops = char_ops_to_line_ops(old_text, &char_ops); - assert_eq!( - line_ops, - vec![ - LineOperation::Delete { lines: 2 }, - LineOperation::Insert { lines: 1 } - ] - ); - - let new_text = apply_char_operations(old_text, &char_ops); - assert_eq!( - new_text, - apply_line_operations(old_text, &new_text, &line_ops) - ); - } - - #[test] - fn test_insert_multiple_newlines() { - let old_text = "aaaa\nbbbb"; - let char_ops = vec![ - CharOperation::Keep { bytes: 5 }, - CharOperation::Insert { - text: "\n\n".into(), - }, - CharOperation::Keep { bytes: 4 }, - ]; - let line_ops = char_ops_to_line_ops(old_text, &char_ops); - assert_eq!( - line_ops, - vec![ - LineOperation::Keep { lines: 1 }, - LineOperation::Insert { lines: 2 }, - LineOperation::Keep { lines: 1 } - ] - ); - let new_text = apply_char_operations(old_text, &char_ops); - assert_eq!( - new_text, - apply_line_operations(old_text, &new_text, &line_ops) - ); - } - - #[test] - fn test_delete_multiple_newlines() { - let old_text = "aaaa\n\n\nbbbb"; - let char_ops = vec![ - CharOperation::Keep { bytes: 5 }, - CharOperation::Delete { bytes: 2 }, - CharOperation::Keep { bytes: 4 }, - ]; - let line_ops = char_ops_to_line_ops(old_text, &char_ops); - assert_eq!( - line_ops, - vec![ - LineOperation::Keep { lines: 1 }, - LineOperation::Delete { lines: 2 }, - LineOperation::Keep { lines: 1 } - ] - ); - let new_text = apply_char_operations(old_text, &char_ops); - assert_eq!( - new_text, - apply_line_operations(old_text, &new_text, &line_ops) - ); - } - - #[test] - fn test_complex_scenario() { - let old_text = "line1\nline2\nline3\nline4"; - let char_ops = vec![ - CharOperation::Keep { bytes: 6 }, - CharOperation::Insert { - text: "inserted\n".into(), - }, - CharOperation::Delete { bytes: 6 }, - CharOperation::Keep { bytes: 5 }, - CharOperation::Insert { - text: "\nnewline".into(), - }, - CharOperation::Keep { bytes: 6 }, - ]; - let line_ops = char_ops_to_line_ops(old_text, &char_ops); - assert_eq!( - line_ops, - vec![ - LineOperation::Keep { lines: 1 }, - LineOperation::Delete { lines: 1 }, - LineOperation::Insert { lines: 1 }, - LineOperation::Keep { lines: 1 }, - LineOperation::Insert { lines: 1 }, - LineOperation::Keep { lines: 1 } - ] - ); - let new_text = apply_char_operations(old_text, &char_ops); - assert_eq!(new_text, "line1\ninserted\nline3\nnewline\nline4"); - assert_eq!( - apply_line_operations(old_text, &new_text, &line_ops), - new_text, - ); - } - - #[test] - fn test_cleaning_up_common_suffix() { - let old_text = concat!( - " for y in 0..size.y() {\n", - " let a = 10;\n", - " let b = 20;\n", - " }", - ); - let char_ops = [ - CharOperation::Keep { bytes: 8 }, - CharOperation::Insert { text: "let".into() }, - CharOperation::Insert { - text: " mut".into(), - }, - CharOperation::Insert { text: " y".into() }, - CharOperation::Insert { text: " =".into() }, - CharOperation::Insert { text: " 0".into() }, - CharOperation::Insert { text: ";".into() }, - CharOperation::Insert { text: "\n".into() }, - CharOperation::Insert { - text: " while".into(), - }, - CharOperation::Insert { text: " y".into() }, - CharOperation::Insert { - text: " < size".into(), - }, - CharOperation::Insert { text: ".".into() }, - CharOperation::Insert { text: "y".into() }, - CharOperation::Insert { text: "()".into() }, - CharOperation::Insert { text: " {".into() }, - CharOperation::Insert { text: "\n".into() }, - CharOperation::Delete { bytes: 23 }, - CharOperation::Keep { bytes: 23 }, - CharOperation::Keep { bytes: 1 }, - CharOperation::Keep { bytes: 23 }, - CharOperation::Keep { bytes: 1 }, - CharOperation::Keep { bytes: 8 }, - CharOperation::Insert { - text: " y".into(), - }, - CharOperation::Insert { text: " +=".into() }, - CharOperation::Insert { text: " 1".into() }, - CharOperation::Insert { text: ";".into() }, - CharOperation::Insert { text: "\n".into() }, - CharOperation::Insert { - text: " ".into(), - }, - CharOperation::Keep { bytes: 1 }, - ]; - let line_ops = char_ops_to_line_ops(old_text, &char_ops); - assert_eq!( - line_ops, - vec![ - LineOperation::Delete { lines: 1 }, - LineOperation::Insert { lines: 2 }, - LineOperation::Keep { lines: 2 }, - LineOperation::Delete { lines: 1 }, - LineOperation::Insert { lines: 2 }, - ] - ); - let new_text = apply_char_operations(old_text, &char_ops); - assert_eq!( - new_text, - apply_line_operations(old_text, &new_text, &line_ops) - ); - } - - #[test] - fn test_random_diffs() { - random_test(|mut rng| { - let old_text_len = env::var("OLD_TEXT_LEN") - .map(|i| i.parse().expect("invalid `OLD_TEXT_LEN` variable")) - .unwrap_or(10); - - let old = random_text(&mut rng, old_text_len); - println!("old text: {:?}", old); - - let new = randomly_edit(&old, &mut rng); - println!("new text: {:?}", new); - - let char_operations = random_streaming_diff(&mut rng, &old, &new); - println!("char operations: {:?}", char_operations); - - // Use apply_char_operations to verify the result - let patched = apply_char_operations(&old, &char_operations); - assert_eq!(patched, new); - - // Test char_ops_to_line_ops - let line_ops = char_ops_to_line_ops(&old, &char_operations); - println!("line operations: {:?}", line_ops); - let patched = apply_line_operations(&old, &new, &line_ops); - assert_eq!(patched, new); - }); - } - - fn char_ops_to_line_ops(old_text: &str, char_ops: &[CharOperation]) -> Vec { - let old_rope = Rope::from(old_text); - let mut diff = LineDiff::default(); - for op in char_ops { - diff.push_char_operation(op, &old_rope); - } - diff.finish(&old_rope); - diff.line_operations() - } - - fn random_streaming_diff(rng: &mut impl Rng, old: &str, new: &str) -> Vec { - let mut diff = StreamingDiff::new(old.to_string()); - let mut char_operations = Vec::new(); - let mut new_len = 0; - - while new_len < new.len() { - let mut chunk_len = rng.random_range(1..=new.len() - new_len); - while !new.is_char_boundary(new_len + chunk_len) { - chunk_len += 1; - } - let chunk = &new[new_len..new_len + chunk_len]; - let new_hunks = diff.push_new(chunk); - char_operations.extend(new_hunks); - new_len += chunk_len; - } - - char_operations.extend(diff.finish()); - char_operations - } - - fn random_test(mut test_fn: F) - where - F: FnMut(StdRng), - { - let iterations = env::var("ITERATIONS") - .map(|i| i.parse().expect("invalid `ITERATIONS` variable")) - .unwrap_or(100); - - let seed: u64 = env::var("SEED") - .map(|s| s.parse().expect("invalid `SEED` variable")) - .unwrap_or(0); - - println!( - "Running test with {} iterations and seed {}", - iterations, seed - ); - - for i in 0..iterations { - println!("Iteration {}", i + 1); - let rng = StdRng::seed_from_u64(seed + i); - test_fn(rng); - } - } - - fn apply_line_operations(old_text: &str, new_text: &str, line_ops: &[LineOperation]) -> String { - let mut result: Vec<&str> = Vec::new(); - - let old_lines: Vec<&str> = old_text.split('\n').collect(); - let new_lines: Vec<&str> = new_text.split('\n').collect(); - let mut old_start = 0_usize; - let mut new_start = 0_usize; - - for op in line_ops { - match op { - LineOperation::Keep { lines } => { - let old_end = old_start + *lines as usize; - result.extend(&old_lines[old_start..old_end]); - old_start = old_end; - new_start += *lines as usize; - } - LineOperation::Delete { lines } => { - old_start += *lines as usize; - } - LineOperation::Insert { lines } => { - let new_end = new_start + *lines as usize; - result.extend(&new_lines[new_start..new_end]); - new_start = new_end; - } - } - } - - result.join("\n") - } - - #[test] - fn test_apply_char_operations() { - let old_text = "Hello, world!"; - let char_ops = vec![ - CharOperation::Keep { bytes: 7 }, - CharOperation::Delete { bytes: 5 }, - CharOperation::Insert { - text: "Rust".to_string(), - }, - CharOperation::Keep { bytes: 1 }, - ]; - let result = apply_char_operations(old_text, &char_ops); - assert_eq!(result, "Hello, Rust!"); - } - - fn random_text(rng: &mut impl Rng, length: usize) -> String { - util::RandomCharIter::new(rng).take(length).collect() - } - - fn randomly_edit(text: &str, rng: &mut impl Rng) -> String { - let mut result = String::from(text); - let edit_count = rng.random_range(1..=5); - - fn random_char_range(text: &str, rng: &mut impl Rng) -> (usize, usize) { - let mut start = rng.random_range(0..=text.len()); - while !text.is_char_boundary(start) { - start -= 1; - } - let mut end = rng.random_range(start..=text.len()); - while !text.is_char_boundary(end) { - end += 1; - } - (start, end) - } - - for _ in 0..edit_count { - match rng.random_range(0..3) { - 0 => { - // Insert - let (pos, _) = random_char_range(&result, rng); - let insert_len = rng.random_range(1..=5); - let insert_text: String = random_text(rng, insert_len); - result.insert_str(pos, &insert_text); - } - 1 => { - // Delete - if !result.is_empty() { - let (start, end) = random_char_range(&result, rng); - result.replace_range(start..end, ""); - } - } - 2 => { - // Replace - if !result.is_empty() { - let (start, end) = random_char_range(&result, rng); - let replace_len = end - start; - let replace_text: String = random_text(rng, replace_len); - result.replace_range(start..end, &replace_text); - } - } - _ => unreachable!(), - } - } - - result - } - - fn apply_char_operations(old_text: &str, char_ops: &[CharOperation]) -> String { - let mut result = String::new(); - let mut old_ix = 0; - - for operation in char_ops { - match operation { - CharOperation::Keep { bytes } => { - result.push_str(&old_text[old_ix..old_ix + bytes]); - old_ix += bytes; - } - CharOperation::Delete { bytes } => { - old_ix += bytes; - } - CharOperation::Insert { text } => { - result.push_str(text); - } - } - } - - result - } -} diff --git a/crates/sum_tree/Cargo.toml b/crates/sum_tree/Cargo.toml index 3e06ede162..d9924a4210 100644 --- a/crates/sum_tree/Cargo.toml +++ b/crates/sum_tree/Cargo.toml @@ -17,13 +17,11 @@ doctest = false arrayvec = "0.7.1" rayon.workspace = true log.workspace = true -ztracing.workspace = true tracing.workspace = true [dev-dependencies] ctor.workspace = true rand.workspace = true -zlog.workspace = true [package.metadata.cargo-machete] ignored = ["tracing"] diff --git a/crates/sum_tree/src/cursor.rs b/crates/sum_tree/src/cursor.rs index 589ae96a2a..efdb6ac13a 100644 --- a/crates/sum_tree/src/cursor.rs +++ b/crates/sum_tree/src/cursor.rs @@ -1,7 +1,7 @@ use super::*; use arrayvec::ArrayVec; use std::{cmp::Ordering, mem, sync::Arc}; -use ztracing::instrument; +use tracing::instrument; #[derive(Clone)] struct StackEntry<'a, T: Item, D> { diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index bfc4587969..4a83730ebd 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -8,7 +8,7 @@ use std::marker::PhantomData; use std::mem; use std::{cmp::Ordering, fmt, iter::FromIterator, sync::Arc}; pub use tree_map::{MapSeekTarget, TreeMap, TreeSet}; -use ztracing::instrument; +use tracing::instrument; #[cfg(test)] pub const TREE_BASE: usize = 2; diff --git a/crates/supermaven/Cargo.toml b/crates/supermaven/Cargo.toml deleted file mode 100644 index c2d0c48a9e..0000000000 --- a/crates/supermaven/Cargo.toml +++ /dev/null @@ -1,44 +0,0 @@ -[package] -name = "supermaven" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/supermaven.rs" -doctest = false - -[dependencies] -anyhow.workspace = true -client.workspace = true -collections.workspace = true -edit_prediction_types.workspace = true -futures.workspace = true -gpui.workspace = true -language.workspace = true -log.workspace = true -postage.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -smol.workspace = true -supermaven_api.workspace = true -text.workspace = true -ui.workspace = true -unicode-segmentation.workspace = true -util.workspace = true - -[dev-dependencies] -editor = { workspace = true, features = ["test-support"] } -env_logger.workspace = true -gpui = { workspace = true, features = ["test-support"] } -language = { workspace = true, features = ["test-support"] } -project = { workspace = true, features = ["test-support"] } -settings = { workspace = true, features = ["test-support"] } -theme = { workspace = true, features = ["test-support"] } -util = { workspace = true, features = ["test-support"] } -http_client = { workspace = true, features = ["test-support"] } diff --git a/crates/supermaven/LICENSE-GPL b/crates/supermaven/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/supermaven/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/supermaven/src/messages.rs b/crates/supermaven/src/messages.rs deleted file mode 100644 index f515d6353b..0000000000 --- a/crates/supermaven/src/messages.rs +++ /dev/null @@ -1,153 +0,0 @@ -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct SetApiKey { - pub api_key: String, -} - -// Outbound messages -#[derive(Debug, Serialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum OutboundMessage { - SetApiKey(SetApiKey), - StateUpdate(StateUpdateMessage), - #[allow(dead_code)] - UseFreeVersion, - Logout, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct StateUpdateMessage { - pub new_id: String, - pub updates: Vec, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum StateUpdate { - FileUpdate(FileUpdateMessage), - CursorUpdate(CursorPositionUpdateMessage), -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub struct FileUpdateMessage { - pub path: String, - pub content: String, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub struct CursorPositionUpdateMessage { - pub path: String, - pub offset: usize, -} - -// Inbound messages coming in on stdout - -#[derive(Debug, Serialize, Deserialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum ResponseItem { - // A completion - Text { text: String }, - // Vestigial message type from old versions -- safe to ignore - Del { text: String }, - // Be able to delete whitespace prior to the cursor, likely for the rest of the completion - Dedent { text: String }, - // When the completion is over - End, - // Got the closing parentheses and shouldn't show any more after - Barrier, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SupermavenResponse { - pub state_id: String, - pub items: Vec, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct SupermavenMetadataMessage { - pub dust_strings: Option>, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct SupermavenTaskUpdateMessage { - pub task: String, - pub status: TaskStatus, - pub percent_complete: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum TaskStatus { - InProgress, - Complete, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct SupermavenActiveRepoMessage { - pub repo_simple_name: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum SupermavenPopupAction { - OpenUrl { label: String, url: String }, - NoOp { label: String }, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub struct SupermavenPopupMessage { - pub message: String, - pub actions: Vec, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(tag = "kind", rename_all = "camelCase")] -pub struct ActivationRequest { - pub activate_url: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SupermavenSetMessage { - pub key: String, - pub value: serde_json::Value, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub enum ServiceTier { - FreeNoLicense, - #[serde(other)] - Unknown, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum SupermavenMessage { - Response(SupermavenResponse), - Metadata(SupermavenMetadataMessage), - Apology { - message: Option, - }, - ActivationRequest(ActivationRequest), - ActivationSuccess, - Passthrough { - passthrough: Box, - }, - Popup(SupermavenPopupMessage), - TaskStatus(SupermavenTaskUpdateMessage), - ActiveRepo(SupermavenActiveRepoMessage), - ServiceTier { - service_tier: ServiceTier, - }, - - Set(SupermavenSetMessage), - #[serde(other)] - Unknown, -} diff --git a/crates/supermaven/src/supermaven.rs b/crates/supermaven/src/supermaven.rs deleted file mode 100644 index 527f4ec37d..0000000000 --- a/crates/supermaven/src/supermaven.rs +++ /dev/null @@ -1,511 +0,0 @@ -mod messages; -mod supermaven_edit_prediction_delegate; - -pub use supermaven_edit_prediction_delegate::*; - -use anyhow::{Context as _, Result}; -#[allow(unused_imports)] -use client::{Client, proto}; -use collections::BTreeMap; - -use futures::{AsyncBufReadExt, StreamExt, channel::mpsc, io::BufReader}; -use gpui::{App, AsyncApp, Context, Entity, EntityId, Global, Task, WeakEntity, actions}; -use language::{ - Anchor, Buffer, BufferSnapshot, ToOffset, language_settings::all_language_settings, -}; -use messages::*; -use postage::watch; -use serde::{Deserialize, Serialize}; -use settings::SettingsStore; -use smol::{ - io::AsyncWriteExt, - process::{Child, ChildStdin, ChildStdout}, -}; -use std::{path::PathBuf, process::Stdio, sync::Arc}; -use ui::prelude::*; -use util::ResultExt; - -actions!( - supermaven, - [ - /// Signs out of Supermaven. - SignOut - ] -); - -pub fn init(client: Arc, cx: &mut App) { - let supermaven = cx.new(|_| Supermaven::Starting); - Supermaven::set_global(supermaven.clone(), cx); - - let mut provider = all_language_settings(None, cx).edit_predictions.provider; - if provider == language::language_settings::EditPredictionProvider::Supermaven { - supermaven.update(cx, |supermaven, cx| supermaven.start(client.clone(), cx)); - } - - cx.observe_global::(move |cx| { - let new_provider = all_language_settings(None, cx).edit_predictions.provider; - if new_provider != provider { - provider = new_provider; - if provider == language::language_settings::EditPredictionProvider::Supermaven { - supermaven.update(cx, |supermaven, cx| supermaven.start(client.clone(), cx)); - } else { - supermaven.update(cx, |supermaven, _cx| supermaven.stop()); - } - } - }) - .detach(); - - cx.on_action(|_: &SignOut, cx| { - if let Some(supermaven) = Supermaven::global(cx) { - supermaven.update(cx, |supermaven, _cx| supermaven.sign_out()); - } - }); -} - -pub enum Supermaven { - Starting, - FailedDownload { error: anyhow::Error }, - Spawned(SupermavenAgent), - Error { error: anyhow::Error }, -} - -#[derive(Clone)] -pub enum AccountStatus { - Unknown, - NeedsActivation { activate_url: String }, - Ready, -} - -#[derive(Clone)] -struct SupermavenGlobal(Entity); - -impl Global for SupermavenGlobal {} - -impl Supermaven { - pub fn global(cx: &App) -> Option> { - cx.try_global::() - .map(|model| model.0.clone()) - } - - pub fn set_global(supermaven: Entity, cx: &mut App) { - cx.set_global(SupermavenGlobal(supermaven)); - } - - pub fn start(&mut self, client: Arc, cx: &mut Context) { - if let Self::Starting = self { - cx.spawn(async move |this, cx| { - let binary_path = - supermaven_api::get_supermaven_agent_path(client.http_client()).await?; - - this.update(cx, |this, cx| { - if let Self::Starting = this { - *this = - Self::Spawned(SupermavenAgent::new(binary_path, client.clone(), cx)?); - } - anyhow::Ok(()) - }) - }) - .detach_and_log_err(cx) - } - } - - pub fn stop(&mut self) { - *self = Self::Starting; - } - - pub fn is_enabled(&self) -> bool { - matches!(self, Self::Spawned { .. }) - } - - pub fn complete( - &mut self, - buffer: &Entity, - cursor_position: Anchor, - cx: &App, - ) -> Option { - if let Self::Spawned(agent) = self { - let buffer_id = buffer.entity_id(); - let buffer = buffer.read(cx); - let path = buffer - .file() - .and_then(|file| Some(file.as_local()?.abs_path(cx))) - .unwrap_or_else(|| PathBuf::from("untitled")) - .to_string_lossy() - .to_string(); - let content = buffer.text(); - let offset = cursor_position.to_offset(buffer); - let state_id = agent.next_state_id; - agent.next_state_id.0 += 1; - - let (updates_tx, mut updates_rx) = watch::channel(); - postage::stream::Stream::try_recv(&mut updates_rx).unwrap(); - - agent.states.insert( - state_id, - SupermavenCompletionState { - buffer_id, - prefix_anchor: cursor_position, - prefix_offset: offset, - text: String::new(), - dedent: String::new(), - updates_tx, - }, - ); - // ensure the states map is max 1000 elements - if agent.states.len() > 1000 { - // state id is monotonic so it's sufficient to remove the first element - agent - .states - .remove(&agent.states.keys().next().unwrap().clone()); - } - - let _ = agent - .outgoing_tx - .unbounded_send(OutboundMessage::StateUpdate(StateUpdateMessage { - new_id: state_id.0.to_string(), - updates: vec![ - StateUpdate::FileUpdate(FileUpdateMessage { - path: path.clone(), - content, - }), - StateUpdate::CursorUpdate(CursorPositionUpdateMessage { path, offset }), - ], - })); - - Some(SupermavenCompletion { - id: state_id, - updates: updates_rx, - }) - } else { - None - } - } - - pub fn completion( - &self, - buffer: &Entity, - cursor_position: Anchor, - cx: &App, - ) -> Option<&str> { - if let Self::Spawned(agent) = self { - find_relevant_completion( - &agent.states, - buffer.entity_id(), - &buffer.read(cx).snapshot(), - cursor_position, - ) - } else { - None - } - } - - pub fn sign_out(&mut self) { - if let Self::Spawned(agent) = self { - agent - .outgoing_tx - .unbounded_send(OutboundMessage::Logout) - .ok(); - // The account status will get set to RequiresActivation or Ready when the next - // message from the agent comes in. Until that happens, set the status to Unknown - // to disable the button. - agent.account_status = AccountStatus::Unknown; - } - } -} - -fn find_relevant_completion<'a>( - states: &'a BTreeMap, - buffer_id: EntityId, - buffer: &BufferSnapshot, - cursor_position: Anchor, -) -> Option<&'a str> { - let mut best_completion: Option<&str> = None; - 'completions: for state in states.values() { - if state.buffer_id != buffer_id { - continue; - } - let Some(state_completion) = state.text.strip_prefix(&state.dedent) else { - continue; - }; - - let current_cursor_offset = cursor_position.to_offset(buffer); - if current_cursor_offset < state.prefix_offset { - continue; - } - - let original_cursor_offset = buffer.clip_offset(state.prefix_offset, text::Bias::Left); - let text_inserted_since_completion_request: String = buffer - .text_for_range(original_cursor_offset..current_cursor_offset) - .collect(); - let trimmed_completion = - match state_completion.strip_prefix(&text_inserted_since_completion_request) { - Some(suffix) => suffix, - None => continue 'completions, - }; - - if best_completion.is_some_and(|best| best.len() > trimmed_completion.len()) { - continue; - } - - best_completion = Some(trimmed_completion); - } - best_completion -} - -pub struct SupermavenAgent { - _process: Child, - next_state_id: SupermavenCompletionStateId, - states: BTreeMap, - outgoing_tx: mpsc::UnboundedSender, - _handle_outgoing_messages: Task>, - _handle_incoming_messages: Task>, - pub account_status: AccountStatus, - service_tier: Option, - #[allow(dead_code)] - client: Arc, -} - -impl SupermavenAgent { - fn new( - binary_path: PathBuf, - client: Arc, - cx: &mut Context, - ) -> Result { - let mut process = util::command::new_smol_command(&binary_path) - .arg("stdio") - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) - .kill_on_drop(true) - .spawn() - .context("failed to start the binary")?; - - let stdin = process - .stdin - .take() - .context("failed to get stdin for process")?; - let stdout = process - .stdout - .take() - .context("failed to get stdout for process")?; - - let (outgoing_tx, outgoing_rx) = mpsc::unbounded(); - - cx.spawn({ - let client = client.clone(); - let outgoing_tx = outgoing_tx.clone(); - async move |this, cx| { - let mut status = client.status(); - while let Some(status) = status.next().await { - if status.is_connected() { - let api_key = client.request(proto::GetSupermavenApiKey {}).await?.api_key; - outgoing_tx - .unbounded_send(OutboundMessage::SetApiKey(SetApiKey { api_key })) - .ok(); - this.update(cx, |this, cx| { - if let Supermaven::Spawned(this) = this { - this.account_status = AccountStatus::Ready; - cx.notify(); - } - })?; - break; - } - } - anyhow::Ok(()) - } - }) - .detach(); - - Ok(Self { - _process: process, - next_state_id: SupermavenCompletionStateId::default(), - states: BTreeMap::default(), - outgoing_tx, - _handle_outgoing_messages: cx.spawn(async move |_, _cx| { - Self::handle_outgoing_messages(outgoing_rx, stdin).await - }), - _handle_incoming_messages: cx.spawn(async move |this, cx| { - Self::handle_incoming_messages(this, stdout, cx).await - }), - account_status: AccountStatus::Unknown, - service_tier: None, - client, - }) - } - - async fn handle_outgoing_messages( - mut outgoing: mpsc::UnboundedReceiver, - mut stdin: ChildStdin, - ) -> Result<()> { - while let Some(message) = outgoing.next().await { - let bytes = serde_json::to_vec(&message)?; - stdin.write_all(&bytes).await?; - stdin.write_all(&[b'\n']).await?; - } - Ok(()) - } - - async fn handle_incoming_messages( - this: WeakEntity, - stdout: ChildStdout, - cx: &mut AsyncApp, - ) -> Result<()> { - const MESSAGE_PREFIX: &str = "SM-MESSAGE "; - - let stdout = BufReader::new(stdout); - let mut lines = stdout.lines(); - while let Some(line) = lines.next().await { - let Some(line) = line.context("failed to read line from stdout").log_err() else { - continue; - }; - let Some(line) = line.strip_prefix(MESSAGE_PREFIX) else { - continue; - }; - let Some(message) = serde_json::from_str::(line) - .with_context(|| format!("failed to deserialize line from stdout: {:?}", line)) - .log_err() - else { - continue; - }; - - this.update(cx, |this, _cx| { - if let Supermaven::Spawned(this) = this { - this.handle_message(message); - } - Task::ready(anyhow::Ok(())) - })? - .await?; - } - - Ok(()) - } - - fn handle_message(&mut self, message: SupermavenMessage) { - match message { - SupermavenMessage::ActivationRequest(request) => { - self.account_status = match request.activate_url { - Some(activate_url) => AccountStatus::NeedsActivation { activate_url }, - None => AccountStatus::Ready, - }; - } - SupermavenMessage::ActivationSuccess => { - self.account_status = AccountStatus::Ready; - } - SupermavenMessage::ServiceTier { service_tier } => { - self.account_status = AccountStatus::Ready; - self.service_tier = Some(service_tier); - } - SupermavenMessage::Response(response) => { - let state_id = SupermavenCompletionStateId(response.state_id.parse().unwrap()); - if let Some(state) = self.states.get_mut(&state_id) { - for item in &response.items { - match item { - ResponseItem::Text { text } => state.text.push_str(text), - ResponseItem::Dedent { text } => state.dedent.push_str(text), - _ => {} - } - } - *state.updates_tx.borrow_mut() = (); - } - } - SupermavenMessage::Passthrough { passthrough } => self.handle_message(*passthrough), - _ => { - log::warn!("unhandled message: {:?}", message); - } - } - } -} - -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)] -pub struct SupermavenCompletionStateId(usize); - -#[allow(dead_code)] -pub struct SupermavenCompletionState { - buffer_id: EntityId, - prefix_anchor: Anchor, - // prefix_offset is tracked independently because the anchor biases left which - // doesn't allow us to determine if the prior text has been deleted. - prefix_offset: usize, - text: String, - dedent: String, - updates_tx: watch::Sender<()>, -} - -pub struct SupermavenCompletion { - pub id: SupermavenCompletionStateId, - pub updates: watch::Receiver<()>, -} - -#[cfg(test)] -mod tests { - use super::*; - use collections::BTreeMap; - use gpui::TestAppContext; - use language::Buffer; - - #[gpui::test] - async fn test_find_relevant_completion_no_first_letter_skip(cx: &mut TestAppContext) { - let buffer = cx.new(|cx| Buffer::local("hello world", cx)); - let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); - - let mut states = BTreeMap::new(); - let state_id = SupermavenCompletionStateId(1); - let (updates_tx, _) = watch::channel(); - - states.insert( - state_id, - SupermavenCompletionState { - buffer_id: buffer.entity_id(), - prefix_anchor: buffer_snapshot.anchor_before(0), // Start of buffer - prefix_offset: 0, - text: "hello".to_string(), - dedent: String::new(), - updates_tx, - }, - ); - - let cursor_position = buffer_snapshot.anchor_after(1); - - let result = find_relevant_completion( - &states, - buffer.entity_id(), - &buffer_snapshot, - cursor_position, - ); - - assert_eq!(result, Some("ello")); - } - - #[gpui::test] - async fn test_find_relevant_completion_with_multiple_chars(cx: &mut TestAppContext) { - let buffer = cx.new(|cx| Buffer::local("hello world", cx)); - let buffer_snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); - - let mut states = BTreeMap::new(); - let state_id = SupermavenCompletionStateId(1); - let (updates_tx, _) = watch::channel(); - - states.insert( - state_id, - SupermavenCompletionState { - buffer_id: buffer.entity_id(), - prefix_anchor: buffer_snapshot.anchor_before(0), // Start of buffer - prefix_offset: 0, - text: "hello".to_string(), - dedent: String::new(), - updates_tx, - }, - ); - - let cursor_position = buffer_snapshot.anchor_after(3); - - let result = find_relevant_completion( - &states, - buffer.entity_id(), - &buffer_snapshot, - cursor_position, - ); - - assert_eq!(result, Some("lo")); - } -} diff --git a/crates/supermaven/src/supermaven_edit_prediction_delegate.rs b/crates/supermaven/src/supermaven_edit_prediction_delegate.rs deleted file mode 100644 index 578bc894f2..0000000000 --- a/crates/supermaven/src/supermaven_edit_prediction_delegate.rs +++ /dev/null @@ -1,302 +0,0 @@ -use crate::{Supermaven, SupermavenCompletionStateId}; -use anyhow::Result; -use edit_prediction_types::{Direction, EditPrediction, EditPredictionDelegate}; -use futures::StreamExt as _; -use gpui::{App, Context, Entity, EntityId, Task}; -use language::{Anchor, Buffer, BufferSnapshot}; -use std::{ - ops::{AddAssign, Range}, - path::Path, - sync::Arc, - time::Duration, -}; -use text::{ToOffset, ToPoint}; -use unicode_segmentation::UnicodeSegmentation; - -pub const DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); - -pub struct SupermavenEditPredictionDelegate { - supermaven: Entity, - buffer_id: Option, - completion_id: Option, - completion_text: Option, - file_extension: Option, - pending_refresh: Option>>, - completion_position: Option, -} - -impl SupermavenEditPredictionDelegate { - pub fn new(supermaven: Entity) -> Self { - Self { - supermaven, - buffer_id: None, - completion_id: None, - completion_text: None, - file_extension: None, - pending_refresh: None, - completion_position: None, - } - } -} - -// Computes the edit prediction from the difference between the completion text. -// This is defined by greedily matching the buffer text against the completion text. -// Inlays are inserted for parts of the completion text that are not present in the buffer text. -// For example, given the completion text "axbyc" and the buffer text "xy", the rendered output in the editor would be "[a]x[b]y[c]". -// The parts in brackets are the inlays. -fn completion_from_diff( - snapshot: BufferSnapshot, - completion_text: &str, - position: Anchor, - delete_range: Range, -) -> EditPrediction { - let buffer_text = snapshot.text_for_range(delete_range).collect::(); - - let mut edits: Vec<(Range, Arc)> = Vec::new(); - - let completion_graphemes: Vec<&str> = completion_text.graphemes(true).collect(); - let buffer_graphemes: Vec<&str> = buffer_text.graphemes(true).collect(); - - let mut offset = position.to_offset(&snapshot); - - let mut i = 0; - let mut j = 0; - while i < completion_graphemes.len() && j < buffer_graphemes.len() { - // find the next instance of the buffer text in the completion text. - let k = completion_graphemes[i..] - .iter() - .position(|c| *c == buffer_graphemes[j]); - match k { - Some(k) => { - if k != 0 { - let offset = snapshot.anchor_after(offset); - // the range from the current position to item is an inlay. - let edit = ( - offset..offset, - completion_graphemes[i..i + k].join("").into(), - ); - edits.push(edit); - } - i += k + 1; - j += 1; - offset.add_assign(buffer_graphemes[j - 1].len()); - } - None => { - // there are no more matching completions, so drop the remaining - // completion text as an inlay. - break; - } - } - } - - if j == buffer_graphemes.len() && i < completion_graphemes.len() { - let offset = snapshot.anchor_after(offset); - // there is leftover completion text, so drop it as an inlay. - let edit_range = offset..offset; - let edit_text = completion_graphemes[i..].join(""); - edits.push((edit_range, edit_text.into())); - } - - EditPrediction::Local { - id: None, - edits, - edit_preview: None, - } -} - -impl EditPredictionDelegate for SupermavenEditPredictionDelegate { - fn name() -> &'static str { - "supermaven" - } - - fn display_name() -> &'static str { - "Supermaven" - } - - fn show_predictions_in_menu() -> bool { - true - } - - fn show_tab_accept_marker() -> bool { - true - } - - fn supports_jump_to_edit() -> bool { - false - } - - fn is_enabled(&self, _buffer: &Entity, _cursor_position: Anchor, cx: &App) -> bool { - self.supermaven.read(cx).is_enabled() - } - - fn is_refreshing(&self, _cx: &App) -> bool { - self.pending_refresh.is_some() && self.completion_id.is_none() - } - - fn refresh( - &mut self, - buffer_handle: Entity, - cursor_position: Anchor, - debounce: bool, - cx: &mut Context, - ) { - // Only make new completion requests when debounce is true (i.e., when text is typed) - // When debounce is false (i.e., cursor movement), we should not make new requests - if !debounce { - return; - } - - reset_completion_cache(self, cx); - - let Some(mut completion) = self.supermaven.update(cx, |supermaven, cx| { - supermaven.complete(&buffer_handle, cursor_position, cx) - }) else { - return; - }; - - self.pending_refresh = Some(cx.spawn(async move |this, cx| { - if debounce { - cx.background_executor().timer(DEBOUNCE_TIMEOUT).await; - } - - while let Some(()) = completion.updates.next().await { - this.update(cx, |this, cx| { - // Get the completion text and cache it - if let Some(text) = - this.supermaven - .read(cx) - .completion(&buffer_handle, cursor_position, cx) - { - this.completion_text = Some(text.to_string()); - - this.completion_position = Some(cursor_position); - } - - this.completion_id = Some(completion.id); - this.buffer_id = Some(buffer_handle.entity_id()); - this.file_extension = buffer_handle.read(cx).file().and_then(|file| { - Some( - Path::new(file.file_name(cx)) - .extension()? - .to_str()? - .to_string(), - ) - }); - cx.notify(); - })?; - } - Ok(()) - })); - } - - fn cycle( - &mut self, - _buffer: Entity, - _cursor_position: Anchor, - _direction: Direction, - _cx: &mut Context, - ) { - } - - fn accept(&mut self, _cx: &mut Context) { - reset_completion_cache(self, _cx); - } - - fn discard(&mut self, _cx: &mut Context) { - reset_completion_cache(self, _cx); - } - - fn suggest( - &mut self, - buffer: &Entity, - cursor_position: Anchor, - cx: &mut Context, - ) -> Option { - if self.buffer_id != Some(buffer.entity_id()) { - return None; - } - - if self.completion_id.is_none() { - return None; - } - - let completion_text = if let Some(cached_text) = &self.completion_text { - cached_text.as_str() - } else { - let text = self - .supermaven - .read(cx) - .completion(buffer, cursor_position, cx)?; - self.completion_text = Some(text.to_string()); - text - }; - - // Check if the cursor is still at the same position as the completion request - // If we don't have a completion position stored, don't show the completion - if let Some(completion_position) = self.completion_position { - if cursor_position != completion_position { - return None; - } - } else { - return None; - } - - let completion_text = trim_to_end_of_line_unless_leading_newline(completion_text); - - let completion_text = completion_text.trim_end(); - - if !completion_text.trim().is_empty() { - let snapshot = buffer.read(cx).snapshot(); - - // Calculate the range from cursor to end of line correctly - let cursor_point = cursor_position.to_point(&snapshot); - let end_of_line = snapshot.anchor_after(language::Point::new( - cursor_point.row, - snapshot.line_len(cursor_point.row), - )); - let delete_range = cursor_position..end_of_line; - - Some(completion_from_diff( - snapshot, - completion_text, - cursor_position, - delete_range, - )) - } else { - None - } - } -} - -fn reset_completion_cache( - provider: &mut SupermavenEditPredictionDelegate, - _cx: &mut Context, -) { - provider.pending_refresh = None; - provider.completion_id = None; - provider.completion_text = None; - provider.completion_position = None; - provider.buffer_id = None; -} - -fn trim_to_end_of_line_unless_leading_newline(text: &str) -> &str { - if has_leading_newline(text) { - text - } else if let Some(i) = text.find('\n') { - &text[..i] - } else { - text - } -} - -fn has_leading_newline(text: &str) -> bool { - for c in text.chars() { - if c == '\n' { - return true; - } - if !c.is_whitespace() { - return false; - } - } - false -} diff --git a/crates/supermaven_api/Cargo.toml b/crates/supermaven_api/Cargo.toml deleted file mode 100644 index 28868a9a74..0000000000 --- a/crates/supermaven_api/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "supermaven_api" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/supermaven_api.rs" -doctest = false - -[dependencies] -anyhow.workspace = true -futures.workspace = true -http_client.workspace = true -paths.workspace = true -serde.workspace = true -serde_json.workspace = true -smol.workspace = true -util.workspace = true diff --git a/crates/supermaven_api/LICENSE-GPL b/crates/supermaven_api/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/supermaven_api/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/supermaven_api/src/supermaven_api.rs b/crates/supermaven_api/src/supermaven_api.rs deleted file mode 100644 index 539826c817..0000000000 --- a/crates/supermaven_api/src/supermaven_api.rs +++ /dev/null @@ -1,291 +0,0 @@ -use anyhow::{Context as _, Result, anyhow}; -use futures::AsyncReadExt; -use futures::io::BufReader; -use http_client::{AsyncBody, HttpClient, Request as HttpRequest}; -use paths::supermaven_dir; -use serde::{Deserialize, Serialize}; -use smol::fs::{self, File}; -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -use util::fs::{make_file_executable, remove_matching}; - -#[derive(Serialize)] -pub struct GetExternalUserRequest { - pub id: String, -} - -#[derive(Serialize)] -pub struct CreateExternalUserRequest { - pub id: String, - pub email: String, -} - -#[derive(Serialize)] -pub struct DeleteExternalUserRequest { - pub id: String, -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CreateExternalUserResponse { - pub api_key: String, -} - -#[derive(Deserialize)] -pub struct SupermavenApiError { - pub message: String, -} - -pub struct SupermavenBinary {} - -pub struct SupermavenAdminApi { - admin_api_key: String, - api_url: String, - http_client: Arc, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SupermavenDownloadResponse { - pub download_url: String, - pub version: u64, - pub sha256_hash: String, -} - -#[derive(Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct SupermavenUser { - #[expect( - unused, - reason = "This field was found to be unused with serde library bump; it's left as is due to insufficient context on PO's side, but it *may* be fine to remove" - )] - id: String, - #[expect( - unused, - reason = "This field was found to be unused with serde library bump; it's left as is due to insufficient context on PO's side, but it *may* be fine to remove" - )] - email: String, - api_key: String, -} - -impl SupermavenAdminApi { - pub fn new(admin_api_key: String, http_client: Arc) -> Self { - Self { - admin_api_key, - api_url: "https://supermaven.com/api/".to_string(), - http_client, - } - } - - pub async fn try_get_user( - &self, - request: GetExternalUserRequest, - ) -> Result> { - let uri = format!("{}external-user/{}", &self.api_url, &request.id); - - let request = HttpRequest::get(&uri).header("Authorization", self.admin_api_key.clone()); - - let mut response = self - .http_client - .send(request.body(AsyncBody::default())?) - .await - .with_context(|| "Unable to get Supermaven API Key".to_string())?; - - let mut body = Vec::new(); - response.body_mut().read_to_end(&mut body).await?; - - if response.status().is_client_error() { - let error: SupermavenApiError = serde_json::from_slice(&body)?; - if error.message == "User not found" { - return Ok(None); - } else { - anyhow::bail!("Supermaven API error: {}", error.message); - } - } else if response.status().is_server_error() { - let error: SupermavenApiError = serde_json::from_slice(&body)?; - return Err(anyhow!("Supermaven API server error").context(error.message)); - } - - let body_str = std::str::from_utf8(&body)?; - - Ok(Some( - serde_json::from_str::(body_str) - .with_context(|| "Unable to parse Supermaven user response".to_string())?, - )) - } - - pub async fn try_create_user( - &self, - request: CreateExternalUserRequest, - ) -> Result { - let uri = format!("{}external-user", &self.api_url); - - let request = HttpRequest::post(&uri) - .header("Authorization", self.admin_api_key.clone()) - .body(AsyncBody::from(serde_json::to_vec(&request)?))?; - - let mut response = self - .http_client - .send(request) - .await - .with_context(|| "Unable to create Supermaven API Key".to_string())?; - - let mut body = Vec::new(); - response.body_mut().read_to_end(&mut body).await?; - - let body_str = std::str::from_utf8(&body)?; - - if !response.status().is_success() { - let error: SupermavenApiError = serde_json::from_slice(&body)?; - return Err(anyhow!("Supermaven API server error").context(error.message)); - } - - serde_json::from_str::(body_str) - .with_context(|| "Unable to parse Supermaven API Key response".to_string()) - } - - pub async fn try_delete_user(&self, request: DeleteExternalUserRequest) -> Result<()> { - let uri = format!("{}external-user/{}", &self.api_url, &request.id); - - let request = HttpRequest::delete(&uri).header("Authorization", self.admin_api_key.clone()); - - let mut response = self - .http_client - .send(request.body(AsyncBody::default())?) - .await - .with_context(|| "Unable to delete Supermaven User".to_string())?; - - let mut body = Vec::new(); - response.body_mut().read_to_end(&mut body).await?; - - if response.status().is_client_error() { - let error: SupermavenApiError = serde_json::from_slice(&body)?; - if error.message == "User not found" { - return Ok(()); - } else { - anyhow::bail!("Supermaven API error: {}", error.message); - } - } else if response.status().is_server_error() { - let error: SupermavenApiError = serde_json::from_slice(&body)?; - return Err(anyhow!("Supermaven API server error").context(error.message)); - } - - Ok(()) - } - - pub async fn try_get_or_create_user( - &self, - request: CreateExternalUserRequest, - ) -> Result { - let get_user_request = GetExternalUserRequest { - id: request.id.clone(), - }; - - match self.try_get_user(get_user_request).await? { - None => self.try_create_user(request).await, - Some(SupermavenUser { api_key, .. }) => Ok(CreateExternalUserResponse { api_key }), - } - } -} - -pub async fn latest_release( - client: Arc, - platform: &str, - arch: &str, -) -> Result { - let uri = format!( - "https://supermaven.com/api/download-path?platform={}&arch={}", - platform, arch - ); - - // Download is not authenticated - let request = HttpRequest::get(&uri); - - let mut response = client - .send(request.body(AsyncBody::default())?) - .await - .with_context(|| "Unable to acquire Supermaven Agent".to_string())?; - - let mut body = Vec::new(); - response.body_mut().read_to_end(&mut body).await?; - - if response.status().is_client_error() || response.status().is_server_error() { - let body_str = std::str::from_utf8(&body)?; - let error: SupermavenApiError = serde_json::from_str(body_str)?; - anyhow::bail!("Supermaven API error: {}", error.message); - } - - serde_json::from_slice::(&body) - .with_context(|| "Unable to parse Supermaven Agent response".to_string()) -} - -pub fn version_path(version: u64) -> PathBuf { - supermaven_dir().join(format!( - "sm-agent-{}{}", - version, - std::env::consts::EXE_SUFFIX - )) -} - -pub async fn has_version(version_path: &Path) -> bool { - fs::metadata(version_path).await.is_ok_and(|m| m.is_file()) -} - -pub async fn get_supermaven_agent_path(client: Arc) -> Result { - fs::create_dir_all(supermaven_dir()) - .await - .with_context(|| { - format!( - "Could not create Supermaven Agent Directory at {:?}", - supermaven_dir() - ) - })?; - - let platform = match std::env::consts::OS { - "macos" => "darwin", - "windows" => "windows", - "linux" => "linux", - unsupported => anyhow::bail!("unsupported platform {unsupported}"), - }; - - let arch = match std::env::consts::ARCH { - "x86_64" => "amd64", - "aarch64" => "arm64", - unsupported => anyhow::bail!("unsupported architecture {unsupported}"), - }; - - let download_info = latest_release(client.clone(), platform, arch).await?; - - let binary_path = version_path(download_info.version); - - if has_version(&binary_path).await { - // Due to an issue with the Supermaven binary not being made executable on - // earlier Zed versions and Supermaven releases not occurring that frequently, - // we ensure here that the found binary is actually executable. - make_file_executable(&binary_path).await?; - - return Ok(binary_path); - } - - let request = HttpRequest::get(&download_info.download_url); - - let mut response = client - .send(request.body(AsyncBody::default())?) - .await - .with_context(|| "Unable to download Supermaven Agent".to_string())?; - - let mut file = File::create(&binary_path) - .await - .with_context(|| format!("Unable to create file at {:?}", binary_path))?; - - futures::io::copy(BufReader::new(response.body_mut()), &mut file) - .await - .with_context(|| format!("Unable to write binary to file at {:?}", binary_path))?; - - make_file_executable(&binary_path).await?; - - remove_matching(supermaven_dir(), |file| file != binary_path).await; - - Ok(binary_path) -} diff --git a/crates/svg_preview/Cargo.toml b/crates/svg_preview/Cargo.toml deleted file mode 100644 index 18f55e28d5..0000000000 --- a/crates/svg_preview/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "svg_preview" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/svg_preview.rs" - -[dependencies] -multi_buffer.workspace = true -file_icons.workspace = true -gpui.workspace = true -language.workspace = true -ui.workspace = true -workspace.workspace = true diff --git a/crates/svg_preview/LICENSE-GPL b/crates/svg_preview/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/svg_preview/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/svg_preview/src/svg_preview.rs b/crates/svg_preview/src/svg_preview.rs deleted file mode 100644 index ca1891394d..0000000000 --- a/crates/svg_preview/src/svg_preview.rs +++ /dev/null @@ -1,26 +0,0 @@ -use gpui::{App, actions}; -use workspace::Workspace; - -pub mod svg_preview_view; - -actions!( - svg, - [ - /// Opens an SVG preview for the current file. - OpenPreview, - /// Opens an SVG preview in a split pane. - OpenPreviewToTheSide, - /// Opens a following SVG preview that syncs with the editor. - OpenFollowingPreview - ] -); - -pub fn init(cx: &mut App) { - cx.observe_new(|workspace: &mut Workspace, window, cx| { - let Some(window) = window else { - return; - }; - crate::svg_preview_view::SvgPreviewView::register(workspace, window, cx); - }) - .detach(); -} diff --git a/crates/svg_preview/src/svg_preview_view.rs b/crates/svg_preview/src/svg_preview_view.rs deleted file mode 100644 index a286dba437..0000000000 --- a/crates/svg_preview/src/svg_preview_view.rs +++ /dev/null @@ -1,341 +0,0 @@ -use std::mem; -use std::sync::Arc; - -use file_icons::FileIcons; -use gpui::{ - App, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render, - RenderImage, Styled, Subscription, Task, WeakEntity, Window, div, img, -}; -use language::{Buffer, BufferEvent}; -use multi_buffer::MultiBuffer; -use ui::prelude::*; -use workspace::item::Item; -use workspace::{Pane, Workspace}; - -use crate::{OpenFollowingPreview, OpenPreview, OpenPreviewToTheSide}; - -pub struct SvgPreviewView { - focus_handle: FocusHandle, - buffer: Option>, - current_svg: Option, SharedString>>, - _refresh: Task<()>, - _buffer_subscription: Option, - _workspace_subscription: Option, -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum SvgPreviewMode { - /// The preview will always show the contents of the provided editor. - Default, - /// The preview will "follow" the last active editor of an SVG file. - Follow, -} - -impl SvgPreviewView { - pub fn new( - mode: SvgPreviewMode, - active_buffer: Entity, - workspace_handle: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - cx.new(|cx| { - let workspace_subscription = if mode == SvgPreviewMode::Follow - && let Some(workspace) = workspace_handle.upgrade() - { - Some(Self::subscribe_to_workspace(workspace, window, cx)) - } else { - None - }; - - let buffer = active_buffer.read_with(cx, |buffer, _cx| buffer.as_singleton()); - - let subscription = buffer - .as_ref() - .map(|buffer| Self::create_buffer_subscription(buffer, window, cx)); - - let mut this = Self { - focus_handle: cx.focus_handle(), - buffer, - current_svg: None, - _buffer_subscription: subscription, - _workspace_subscription: workspace_subscription, - _refresh: Task::ready(()), - }; - this.render_image(window, cx); - - this - }) - } - - fn subscribe_to_workspace( - workspace: Entity, - window: &Window, - cx: &mut Context, - ) -> Subscription { - cx.subscribe_in( - &workspace, - window, - move |this: &mut SvgPreviewView, workspace, event: &workspace::Event, window, cx| { - if let workspace::Event::ActiveItemChanged = event { - let workspace = workspace.read(cx); - if let Some(active_item) = workspace.active_item(cx) - && let Some(buffer) = active_item.downcast::() - && Self::is_svg_file(&buffer, cx) - { - let Some(buffer) = buffer.read(cx).as_singleton() else { - return; - }; - if this.buffer.as_ref() != Some(&buffer) { - this._buffer_subscription = - Some(Self::create_buffer_subscription(&buffer, window, cx)); - this.buffer = Some(buffer); - this.render_image(window, cx); - cx.notify(); - } - } else { - this.set_current(None, window, cx); - } - } - }, - ) - } - - fn render_image(&mut self, window: &Window, cx: &mut Context) { - let Some(buffer) = self.buffer.as_ref() else { - return; - }; - const SCALE_FACTOR: f32 = 1.0; - - let renderer = cx.svg_renderer(); - let content = buffer.read(cx).snapshot(); - let background_task = cx.background_spawn(async move { - renderer.render_single_frame(content.text().as_bytes(), SCALE_FACTOR, true) - }); - - self._refresh = cx.spawn_in(window, async move |this, cx| { - let result = background_task.await; - - this.update_in(cx, |view, window, cx| { - let current = result.map_err(|e| e.to_string().into()); - view.set_current(Some(current), window, cx); - }) - .ok(); - }); - } - - fn set_current( - &mut self, - image: Option, SharedString>>, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(Ok(image)) = mem::replace(&mut self.current_svg, image) { - window.drop_image(image).ok(); - } - cx.notify(); - } - - fn find_existing_preview_item_idx( - pane: &Pane, - buffer: &Entity, - cx: &App, - ) -> Option { - let buffer_id = buffer.entity_id(); - pane.items_of_type::() - .find(|view| { - view.read(cx) - .buffer - .as_ref() - .is_some_and(|buffer| buffer.entity_id() == buffer_id) - }) - .and_then(|view| pane.index_for_item(&view)) - } - - pub fn resolve_active_item_as_svg_buffer( - workspace: &Workspace, - cx: &mut Context, - ) -> Option> { - workspace - .active_item(cx)? - .act_as::(cx) - .filter(|buffer| Self::is_svg_file(&buffer, cx)) - } - - fn create_svg_view( - mode: SvgPreviewMode, - workspace: &mut Workspace, - buffer: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - let workspace_handle = workspace.weak_handle(); - SvgPreviewView::new(mode, buffer, workspace_handle, window, cx) - } - - fn create_buffer_subscription( - buffer: &Entity, - window: &Window, - cx: &mut Context, - ) -> Subscription { - cx.subscribe_in( - buffer, - window, - move |this, _buffer, event: &BufferEvent, window, cx| match event { - BufferEvent::Edited | BufferEvent::Saved => { - this.render_image(window, cx); - } - _ => {} - }, - ) - } - - pub fn is_svg_file(buffer: &Entity, cx: &App) -> bool { - buffer - .read(cx) - .as_singleton() - .and_then(|buffer| buffer.read(cx).file()) - .is_some_and(|file| { - file.path() - .extension() - .is_some_and(|ext| ext.eq_ignore_ascii_case("svg")) - }) - } - - pub fn register(workspace: &mut Workspace, _window: &mut Window, _cx: &mut Context) { - workspace.register_action(move |workspace, _: &OpenPreview, window, cx| { - if let Some(buffer) = Self::resolve_active_item_as_svg_buffer(workspace, cx) - && Self::is_svg_file(&buffer, cx) - { - let view = Self::create_svg_view( - SvgPreviewMode::Default, - workspace, - buffer.clone(), - window, - cx, - ); - workspace.active_pane().update(cx, |pane, cx| { - if let Some(existing_view_idx) = - Self::find_existing_preview_item_idx(pane, &buffer, cx) - { - pane.activate_item(existing_view_idx, true, true, window, cx); - } else { - pane.add_item(Box::new(view), true, true, None, window, cx) - } - }); - cx.notify(); - } - }); - - workspace.register_action(move |workspace, _: &OpenPreviewToTheSide, window, cx| { - if let Some(editor) = Self::resolve_active_item_as_svg_buffer(workspace, cx) - && Self::is_svg_file(&editor, cx) - { - let editor_clone = editor.clone(); - let view = Self::create_svg_view( - SvgPreviewMode::Default, - workspace, - editor_clone, - window, - cx, - ); - let pane = workspace - .find_pane_in_direction(workspace::SplitDirection::Right, cx) - .unwrap_or_else(|| { - workspace.split_pane( - workspace.active_pane().clone(), - workspace::SplitDirection::Right, - window, - cx, - ) - }); - pane.update(cx, |pane, cx| { - if let Some(existing_view_idx) = - Self::find_existing_preview_item_idx(pane, &editor, cx) - { - pane.activate_item(existing_view_idx, true, true, window, cx); - } else { - pane.add_item(Box::new(view), false, false, None, window, cx) - } - }); - cx.notify(); - } - }); - - workspace.register_action(move |workspace, _: &OpenFollowingPreview, window, cx| { - if let Some(editor) = Self::resolve_active_item_as_svg_buffer(workspace, cx) - && Self::is_svg_file(&editor, cx) - { - let view = - Self::create_svg_view(SvgPreviewMode::Follow, workspace, editor, window, cx); - workspace.active_pane().update(cx, |pane, cx| { - pane.add_item(Box::new(view), true, true, None, window, cx) - }); - cx.notify(); - } - }); - } -} - -impl Render for SvgPreviewView { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - v_flex() - .id("SvgPreview") - .key_context("SvgPreview") - .track_focus(&self.focus_handle(cx)) - .size_full() - .bg(cx.theme().colors().editor_background) - .flex() - .justify_center() - .items_center() - .map(|this| match self.current_svg.clone() { - Some(Ok(image)) => { - this.child(img(image).max_w_full().max_h_full().with_fallback(|| { - h_flex() - .p_4() - .gap_2() - .child(Icon::new(IconName::Warning)) - .child("Failed to load SVG image") - .into_any_element() - })) - } - Some(Err(e)) => this.child(div().p_4().child(e).into_any_element()), - None => this.child(div().p_4().child("No SVG file selected")), - }) - } -} - -impl Focusable for SvgPreviewView { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl EventEmitter<()> for SvgPreviewView {} - -impl Item for SvgPreviewView { - type Event = (); - - fn tab_icon(&self, _window: &Window, cx: &App) -> Option { - self.buffer - .as_ref() - .and_then(|buffer| buffer.read(cx).file()) - .and_then(|file| FileIcons::get_icon(file.path().as_std_path(), cx)) - .map(Icon::from_path) - .or_else(|| Some(Icon::new(IconName::Image))) - } - - fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString { - self.buffer - .as_ref() - .and_then(|svg_path| svg_path.read(cx).file()) - .map(|name| format!("Preview {}", name.file_name(cx)).into()) - .unwrap_or_else(|| "SVG Preview".into()) - } - - fn telemetry_event_text(&self) -> Option<&'static str> { - Some("svg preview: open") - } - - fn to_item_events(_event: &Self::Event, _f: impl FnMut(workspace::item::ItemEvent)) {} -} diff --git a/crates/system_specs/Cargo.toml b/crates/system_specs/Cargo.toml deleted file mode 100644 index 15d6822b38..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 -semver.workspace = true -serde.workspace = true -sysinfo.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/system_specs/src/system_specs.rs b/crates/system_specs/src/system_specs.rs deleted file mode 100644 index 139f23d193..0000000000 --- a/crates/system_specs/src/system_specs.rs +++ /dev/null @@ -1,295 +0,0 @@ -use client::telemetry; -pub use gpui::GpuSpecs; -use gpui::{App, AppContext as _, Task, Window, actions}; -use human_bytes::human_bytes; -use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; -use semver::Version; -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, - release_channel: &'static str, - os_name: String, - os_version: String, - memory: u64, - architecture: &'static str, - commit_sha: Option, - bundle_type: Option, - gpu_specs: Option, -} - -impl SystemSpecs { - pub fn new(window: &mut Window, cx: &mut App) -> Task { - let app_version = AppVersion::global(cx).to_string(); - let release_channel = ReleaseChannel::global(cx); - let os_name = telemetry::os_name(); - let system = System::new_with_specifics( - RefreshKind::nothing().with_memory(MemoryRefreshKind::everything()), - ); - let memory = system.total_memory(); - let architecture = env::consts::ARCH; - let commit_sha = match release_channel { - ReleaseChannel::Dev | ReleaseChannel::Nightly => { - AppCommitSha::try_global(cx).map(|sha| sha.full()) - } - _ => None, - }; - let bundle_type = bundle_type(); - - let gpu_specs = window.gpu_specs().map(|specs| { - format!( - "{} || {} || {}", - specs.device_name, specs.driver_name, specs.driver_info - ) - }); - - cx.background_spawn(async move { - let os_version = telemetry::os_version(); - SystemSpecs { - app_version, - release_channel: release_channel.display_name(), - bundle_type, - os_name, - os_version, - memory, - architecture, - commit_sha, - gpu_specs, - } - }) - } - - pub fn new_stateless( - app_version: Version, - app_commit_sha: Option, - release_channel: ReleaseChannel, - ) -> Self { - let os_name = telemetry::os_name(); - let os_version = telemetry::os_version(); - let system = System::new_with_specifics( - RefreshKind::nothing().with_memory(MemoryRefreshKind::everything()), - ); - let memory = system.total_memory(); - let architecture = env::consts::ARCH; - let commit_sha = match release_channel { - ReleaseChannel::Dev | ReleaseChannel::Nightly => app_commit_sha.map(|sha| sha.full()), - _ => None, - }; - let bundle_type = bundle_type(); - - Self { - app_version: app_version.to_string(), - release_channel: release_channel.display_name(), - os_name, - os_version, - memory, - architecture, - commit_sha, - bundle_type, - gpu_specs: try_determine_available_gpus(), - } - } -} - -impl Display for SystemSpecs { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let os_information = format!("OS: {} {}", self.os_name, self.os_version); - let app_version_information = format!( - "Zed: v{} ({}) {}{}", - self.app_version, - match &self.commit_sha { - Some(commit_sha) => format!("{} {}", self.release_channel, commit_sha), - None => self.release_channel.to_string(), - }, - if let Some(bundle_type) = &self.bundle_type { - format!("({bundle_type})") - } else { - "".to_string() - }, - if cfg!(debug_assertions) { - "(Taylor's Version)" - } else { - "" - }, - ); - let system_specs = [ - app_version_information, - os_information, - format!("Memory: {}", human_bytes(self.memory as f64)), - format!("Architecture: {}", self.architecture), - ] - .into_iter() - .chain( - self.gpu_specs - .as_ref() - .map(|specs| format!("GPU: {}", specs)), - ) - .collect::>() - .join("\n"); - - write!(f, "{system_specs}") - } -} - -fn try_determine_available_gpus() -> Option { - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - { - #[allow( - clippy::disallowed_methods, - reason = "we are not running in an executor" - )] - std::process::Command::new("vulkaninfo") - .args(&["--summary"]) - .output() - .ok() - .map(|output| { - [ - "
`vulkaninfo --summary` output", - "", - "```", - String::from_utf8_lossy(&output.stdout).as_ref(), - "```", - "
", - ] - .join("\n") - }) - .or(Some("Failed to run `vulkaninfo --summary`".to_string())) - } - #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] - { - None - } -} - -#[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 -/// runtime environment variable. -/// -/// The runtime value is used by snap since the Zed snaps use release binaries directly, and so -/// cannot have this baked in. -fn bundle_type() -> Option { - option_env!("ZED_BUNDLE_TYPE") - .map(|bundle_type| bundle_type.to_string()) - .or_else(|| env::var("ZED_BUNDLE_TYPE").ok()) -} diff --git a/crates/tab_switcher/Cargo.toml b/crates/tab_switcher/Cargo.toml deleted file mode 100644 index 36e4ba7734..0000000000 --- a/crates/tab_switcher/Cargo.toml +++ /dev/null @@ -1,39 +0,0 @@ -[package] -name = "tab_switcher" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/tab_switcher.rs" -doctest = false - -[dependencies] -collections.workspace = true -editor.workspace = true -fuzzy.workspace = true -gpui.workspace = true -menu.workspace = true -picker.workspace = true -project.workspace = true -schemars.workspace = true -serde.workspace = true -settings.workspace = true -smol.workspace = true -ui.workspace = true -util.workspace = true -workspace.workspace = true - -[dev-dependencies] -anyhow.workspace = true -ctor.workspace = true -gpui = { workspace = true, features = ["test-support"] } -language = { workspace = true, features = ["test-support"] } -serde_json.workspace = true -theme = { workspace = true, features = ["test-support"] } -workspace = { workspace = true, features = ["test-support"] } -zlog.workspace = true diff --git a/crates/tab_switcher/LICENSE-GPL b/crates/tab_switcher/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/tab_switcher/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 deleted file mode 100644 index 85186ad504..0000000000 --- a/crates/tab_switcher/src/tab_switcher.rs +++ /dev/null @@ -1,758 +0,0 @@ -#[cfg(test)] -mod tab_switcher_tests; - -use collections::HashMap; -use editor::items::{ - entry_diagnostic_aware_icon_decoration_and_color, 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, -}; -use picker::{Picker, PickerDelegate}; -use project::Project; -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 util::ResultExt; -use workspace::{ - Event as WorkspaceEvent, ModalView, Pane, SaveIntent, Workspace, - item::{ItemHandle, ItemSettings, ShowDiagnostics, TabContentParams}, - pane::{render_item_indicator, tab_details}, -}; - -const PANEL_WIDTH_REMS: f32 = 28.; - -/// Toggles the tab switcher interface. -#[derive(PartialEq, Clone, Deserialize, JsonSchema, Default, Action)] -#[action(namespace = tab_switcher)] -#[serde(deny_unknown_fields)] -pub struct Toggle { - #[serde(default)] - pub select_last: bool, -} -actions!( - tab_switcher, - [ - /// Closes the selected item in the tab switcher. - CloseSelectedItem, - /// Toggles between showing all tabs or just the current pane's tabs. - ToggleAll - ] -); - -pub struct TabSwitcher { - picker: Entity>, - init_modifiers: Option, -} - -impl ModalView for TabSwitcher {} - -pub fn init(cx: &mut App) { - cx.observe_new(TabSwitcher::register).detach(); -} - -impl TabSwitcher { - fn register( - workspace: &mut Workspace, - _window: Option<&mut Window>, - _: &mut Context, - ) { - workspace.register_action(|workspace, action: &Toggle, window, cx| { - let Some(tab_switcher) = workspace.active_modal::(cx) else { - Self::open(workspace, action.select_last, false, window, cx); - return; - }; - - tab_switcher.update(cx, |tab_switcher, cx| { - tab_switcher - .picker - .update(cx, |picker, cx| picker.cycle_selection(window, cx)) - }); - }); - workspace.register_action(|workspace, _action: &ToggleAll, window, cx| { - let Some(tab_switcher) = workspace.active_modal::(cx) else { - Self::open(workspace, false, true, window, cx); - return; - }; - - tab_switcher.update(cx, |tab_switcher, cx| { - tab_switcher - .picker - .update(cx, |picker, cx| picker.cycle_selection(window, cx)) - }); - }); - } - - fn open( - workspace: &mut Workspace, - select_last: bool, - is_global: bool, - window: &mut Window, - cx: &mut Context, - ) { - let mut weak_pane = workspace.active_pane().downgrade(); - for dock in [ - workspace.left_dock(), - workspace.bottom_dock(), - workspace.right_dock(), - ] { - dock.update(cx, |this, cx| { - let Some(panel) = this - .active_panel() - .filter(|panel| panel.panel_focus_handle(cx).contains_focused(window, cx)) - else { - return; - }; - if let Some(pane) = panel.pane(cx) { - weak_pane = pane.downgrade(); - } - }) - } - - 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, - select_last, - cx.entity().downgrade(), - weak_pane, - weak_workspace, - is_global, - window, - cx, - original_items, - ); - TabSwitcher::new(delegate, window, is_global, cx) - }); - } - - fn new( - delegate: TabSwitcherDelegate, - window: &mut Window, - is_global: bool, - cx: &mut Context, - ) -> Self { - let init_modifiers = if is_global { - None - } else { - window.modifiers().modified().then_some(window.modifiers()) - }; - Self { - picker: cx.new(|cx| { - if is_global { - Picker::list(delegate, window, cx) - } else { - Picker::nonsearchable_list(delegate, window, cx) - } - }), - init_modifiers, - } - } - - fn handle_modifiers_changed( - &mut self, - event: &ModifiersChangedEvent, - window: &mut Window, - cx: &mut Context, - ) { - let Some(init_modifiers) = self.init_modifiers else { - return; - }; - if !event.modified() || !init_modifiers.is_subset_of(event) { - self.init_modifiers = None; - if self.picker.read(cx).delegate.matches.is_empty() { - cx.emit(DismissEvent) - } else { - window.dispatch_action(menu::Confirm.boxed_clone(), cx); - } - } - } - - fn handle_close_selected_item( - &mut self, - _: &CloseSelectedItem, - window: &mut Window, - cx: &mut Context, - ) { - self.picker.update(cx, |picker, cx| { - picker - .delegate - .close_item_at(picker.delegate.selected_index(), window, cx) - }); - } -} - -impl EventEmitter for TabSwitcher {} - -impl Focusable for TabSwitcher { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.picker.focus_handle(cx) - } -} - -impl Render for TabSwitcher { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - v_flex() - .key_context("TabSwitcher") - .w(rems(PANEL_WIDTH_REMS)) - .on_modifiers_changed(cx.listener(Self::handle_modifiers_changed)) - .on_action(cx.listener(Self::handle_close_selected_item)) - .child(self.picker.clone()) - } -} - -#[derive(Clone)] -struct TabMatch { - pane: WeakEntity, - item_index: usize, - item: Box, - detail: usize, - preview: bool, -} - -pub struct TabSwitcherDelegate { - select_last: bool, - tab_switcher: WeakEntity, - selected_index: usize, - pane: WeakEntity, - 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_severe_diagnostic_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_severe_diagnostic_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 { - #[allow(clippy::complexity)] - fn new( - project: Entity, - select_last: bool, - tab_switcher: WeakEntity, - pane: WeakEntity, - workspace: WeakEntity, - is_all_panes: bool, - window: &mut Window, - cx: &mut Context, - original_items: Vec<(Entity, usize)>, - ) -> Self { - Self::subscribe_to_updates(&workspace, window, cx); - Self { - select_last, - tab_switcher, - selected_index: 0, - pane, - workspace, - project, - matches: Vec::new(), - is_all_panes, - original_items, - restored_items: false, - } - } - - fn subscribe_to_updates( - workspace: &WeakEntity, - window: &mut Window, - cx: &mut Context, - ) { - let Some(workspace) = workspace.upgrade() else { - return; - }; - cx.subscribe_in(&workspace, window, |tab_switcher, _, event, window, cx| { - match event { - WorkspaceEvent::ItemAdded { .. } | WorkspaceEvent::PaneRemoved => { - tab_switcher.picker.update(cx, |picker, cx| { - let query = picker.query(cx); - picker.delegate.update_matches(query, window, cx); - cx.notify(); - }) - } - WorkspaceEvent::ItemRemoved { .. } => { - tab_switcher.picker.update(cx, |picker, cx| { - let query = picker.query(cx); - picker.delegate.update_matches(query, window, cx); - - // When the Tab Switcher is being used and an item is - // removed, there's a chance that the new selected index - // will not match the actual tab that is now being displayed - // by the pane, as such, the selected index needs to be - // updated to match the pane's state. - picker.delegate.sync_selected_index(cx); - cx.notify(); - }) - } - _ => {} - }; - }) - .detach(); - } - - fn update_all_pane_matches( - &mut self, - query: String, - window: &mut Window, - cx: &mut Context>, - ) { - let Some(workspace) = self.workspace.upgrade() else { - return; - }; - let mut all_items = Vec::new(); - let mut item_index = 0; - for pane_handle in workspace.read(cx).panes() { - let pane = pane_handle.read(cx); - let items: Vec> = - pane.items().map(|item| item.boxed_clone()).collect(); - for ((_detail, item), detail) in items - .iter() - .enumerate() - .zip(tab_details(&items, window, cx)) - { - all_items.push(TabMatch { - pane: pane_handle.downgrade(), - item_index, - item: item.clone(), - detail, - preview: pane.is_active_preview_item(item.item_id()), - }); - item_index += 1; - } - } - - let matches = if query.is_empty() { - let history = workspace.read(cx).recently_activated_items(cx); - all_items - .sort_by_key(|tab| (Reverse(history.get(&tab.item.item_id())), tab.item_index)); - all_items - } else { - let candidates = all_items - .iter() - .enumerate() - .flat_map(|(ix, tab_match)| { - Some(StringMatchCandidate::new( - ix, - &tab_match.item.tab_content_text(0, cx), - )) - }) - .collect::>(); - smol::block_on(fuzzy::match_strings( - &candidates, - &query, - true, - true, - 10000, - &Default::default(), - cx.background_executor().clone(), - )) - .into_iter() - .map(|m| all_items[m.candidate_id].clone()) - .collect() - }; - - let selected_item_id = self.selected_item_id(); - self.matches = matches; - self.selected_index = self.compute_selected_index(selected_item_id, window, cx); - } - - fn update_matches( - &mut self, - query: String, - window: &mut Window, - cx: &mut Context>, - ) { - if self.is_all_panes { - // needed because we need to borrow the workspace, but that may be borrowed when the picker - // calls update_matches. - let this = cx.entity(); - window.defer(cx, move |window, cx| { - this.update(cx, |this, cx| { - this.delegate.update_all_pane_matches(query, window, cx); - }) - }); - return; - } - let selected_item_id = self.selected_item_id(); - self.matches.clear(); - let Some(pane) = self.pane.upgrade() else { - return; - }; - - let pane = pane.read(cx); - let mut history_indices = HashMap::default(); - pane.activation_history().iter().rev().enumerate().for_each( - |(history_index, history_entry)| { - history_indices.insert(history_entry.entity_id, history_index); - }, - ); - - let items: Vec> = pane.items().map(|item| item.boxed_clone()).collect(); - items - .iter() - .enumerate() - .zip(tab_details(&items, window, cx)) - .map(|((item_index, item), detail)| TabMatch { - pane: self.pane.clone(), - item_index, - item: item.boxed_clone(), - detail, - preview: pane.is_active_preview_item(item.item_id()), - }) - .for_each(|tab_match| self.matches.push(tab_match)); - - let non_history_base = history_indices.len(); - self.matches.sort_by(move |a, b| { - let a_score = *history_indices - .get(&a.item.item_id()) - .unwrap_or(&(a.item_index + non_history_base)); - let b_score = *history_indices - .get(&b.item.item_id()) - .unwrap_or(&(b.item_index + non_history_base)); - a_score.cmp(&b_score) - }); - - self.selected_index = self.compute_selected_index(selected_item_id, window, cx); - } - - fn selected_item_id(&self) -> Option { - self.matches - .get(self.selected_index()) - .map(|tab_match| tab_match.item.item_id()) - } - - fn compute_selected_index( - &mut self, - prev_selected_item_id: Option, - window: &mut Window, - cx: &mut Context>, - ) -> usize { - if self.matches.is_empty() { - return 0; - } - - if let Some(selected_item_id) = prev_selected_item_id { - // If the previously selected item is still in the list, select its new position. - if let Some(item_index) = self - .matches - .iter() - .position(|tab_match| tab_match.item.item_id() == selected_item_id) - { - return item_index; - } - // Otherwise, try to preserve the previously selected index. - return self.selected_index.min(self.matches.len() - 1); - } - - if self.select_last { - return self.matches.len() - 1; - } - - // This only runs when initially opening the picker - // Index 0 is already active, so don't preselect it for switching. - if self.matches.len() > 1 { - self.set_selected_index(1, window, cx); - return 1; - } - - 0 - } - - fn close_item_at( - &mut self, - ix: usize, - window: &mut Window, - cx: &mut Context>, - ) { - let Some(tab_match) = self.matches.get(ix) else { - return; - }; - let Some(pane) = tab_match.pane.upgrade() else { - return; - }; - - pane.update(cx, |pane, cx| { - pane.close_item_by_id(tab_match.item.item_id(), SaveIntent::Close, window, cx) - .detach_and_log_err(cx); - }); - } - - /// Updates the selected index to ensure it matches the pane's active item, - /// as the pane's active item can be indirectly updated and this method - /// ensures that the picker can react to those changes. - fn sync_selected_index(&mut self, cx: &mut Context>) { - let item = if self.is_all_panes { - self.workspace - .read_with(cx, |workspace, cx| workspace.active_item(cx)) - } else { - self.pane.read_with(cx, |pane, _cx| pane.active_item()) - }; - - let Ok(Some(item)) = item else { - return; - }; - - let item_id = item.item_id(); - let Some((index, _tab_match)) = self - .matches - .iter() - .enumerate() - .find(|(_index, tab_match)| tab_match.item.item_id() == item_id) - else { - return; - }; - - self.selected_index = index; - } -} - -impl PickerDelegate for TabSwitcherDelegate { - type ListItem = ListItem; - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - "Search all tabs…".into() - } - - fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option { - Some("No tabs".into()) - } - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index( - &mut self, - ix: usize, - window: &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(); - } - - fn separators_after_indices(&self) -> Vec { - Vec::new() - } - - fn update_matches( - &mut self, - raw_query: String, - window: &mut Window, - cx: &mut Context>, - ) -> Task<()> { - self.update_matches(raw_query, window, cx); - Task::ready(()) - } - - fn confirm( - &mut self, - _secondary: bool, - window: &mut Window, - cx: &mut Context>, - ) { - 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| { - if let Some(index) = pane.index_for_item(selected_match.item.as_ref()) { - pane.activate_item(index, true, true, window, cx); - } - }) - .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); - }) - } - } - - self.tab_switcher - .update(cx, |_, cx| cx.emit(DismissEvent)) - .log_err(); - } - - fn render_match( - &self, - ix: usize, - selected: bool, - window: &mut Window, - cx: &mut Context>, - ) -> Option { - let tab_match = self.matches.get(ix)?; - - let params = TabContentParams { - detail: Some(tab_match.detail), - selected: true, - preview: tab_match.preview, - deemphasized: false, - }; - let label = tab_match.item.tab_content(params, window, cx); - - let icon = tab_match.icon(&self.project, selected, window, cx); - - let indicator = render_item_indicator(tab_match.item.boxed_clone(), cx); - let indicator_color = if let Some(ref indicator) = indicator { - indicator.color - } else { - Color::default() - }; - let indicator = h_flex() - .flex_shrink_0() - .children(indicator) - .child(div().w_2()) - .into_any_element(); - let close_button = div() - .id("close-button") - .on_mouse_up( - // We need this on_mouse_up here because on macOS you may have ctrl held - // down to open the menu, and a ctrl-click comes through as a right click. - MouseButton::Right, - cx.listener(move |picker, _: &MouseUpEvent, window, cx| { - cx.stop_propagation(); - picker.delegate.close_item_at(ix, window, cx); - }), - ) - .child( - IconButton::new("close_tab", IconName::Close) - .icon_size(IconSize::Small) - .icon_color(indicator_color) - .tooltip(Tooltip::for_action_title("Close", &CloseSelectedItem)) - .on_click(cx.listener(move |picker, _, window, cx| { - cx.stop_propagation(); - picker.delegate.close_item_at(ix, window, cx); - })), - ) - .into_any_element(); - - Some( - ListItem::new(ix) - .spacing(ListItemSpacing::Sparse) - .inset(true) - .toggle_state(selected) - .child(h_flex().w_full().child(label)) - .start_slot::(icon) - .map(|el| { - if self.selected_index == ix { - el.end_slot::(close_button) - } else { - el.end_slot::(indicator) - .end_hover_slot::(close_button) - } - }), - ) - } -} diff --git a/crates/tab_switcher/src/tab_switcher_tests.rs b/crates/tab_switcher/src/tab_switcher_tests.rs deleted file mode 100644 index 85177f29ed..0000000000 --- a/crates/tab_switcher/src/tab_switcher_tests.rs +++ /dev/null @@ -1,345 +0,0 @@ -use super::*; -use editor::Editor; -use gpui::{TestAppContext, VisualTestContext}; -use menu::SelectPrevious; -use project::{Project, ProjectPath}; -use serde_json::json; -use util::{path, rel_path::rel_path}; -use workspace::{ActivatePreviousItem, AppState, Workspace}; - -#[ctor::ctor] -fn init_logger() { - zlog::init_test(); -} - -#[gpui::test] -async fn test_open_with_prev_tab_selected_and_cycle_on_toggle_action( - cx: &mut gpui::TestAppContext, -) { - let app_state = init_test(cx); - - app_state - .fs - .as_fake() - .insert_tree( - path!("/root"), - json!({ - "1.txt": "First file", - "2.txt": "Second file", - "3.txt": "Third file", - "4.txt": "Fourth file", - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let tab_1 = open_buffer("1.txt", &workspace, cx).await; - let tab_2 = open_buffer("2.txt", &workspace, cx).await; - let tab_3 = open_buffer("3.txt", &workspace, cx).await; - let tab_4 = open_buffer("4.txt", &workspace, cx).await; - - // Starts with the previously opened item selected - let tab_switcher = open_tab_switcher(false, &workspace, cx); - tab_switcher.update(cx, |tab_switcher, _| { - assert_eq!(tab_switcher.delegate.matches.len(), 4); - assert_match_at_position(tab_switcher, 0, tab_4.boxed_clone()); - assert_match_selection(tab_switcher, 1, tab_3.boxed_clone()); - assert_match_at_position(tab_switcher, 2, tab_2.boxed_clone()); - assert_match_at_position(tab_switcher, 3, tab_1.boxed_clone()); - }); - - cx.dispatch_action(Toggle { select_last: false }); - cx.dispatch_action(Toggle { select_last: false }); - tab_switcher.update(cx, |tab_switcher, _| { - assert_eq!(tab_switcher.delegate.matches.len(), 4); - assert_match_at_position(tab_switcher, 0, tab_4.boxed_clone()); - assert_match_at_position(tab_switcher, 1, tab_3.boxed_clone()); - assert_match_at_position(tab_switcher, 2, tab_2.boxed_clone()); - assert_match_selection(tab_switcher, 3, tab_1.boxed_clone()); - }); - - cx.dispatch_action(SelectPrevious); - tab_switcher.update(cx, |tab_switcher, _| { - assert_eq!(tab_switcher.delegate.matches.len(), 4); - assert_match_at_position(tab_switcher, 0, tab_4.boxed_clone()); - assert_match_at_position(tab_switcher, 1, tab_3.boxed_clone()); - assert_match_selection(tab_switcher, 2, tab_2.boxed_clone()); - assert_match_at_position(tab_switcher, 3, tab_1.boxed_clone()); - }); -} - -#[gpui::test] -async fn test_open_with_last_tab_selected(cx: &mut gpui::TestAppContext) { - let app_state = init_test(cx); - - app_state - .fs - .as_fake() - .insert_tree( - path!("/root"), - json!({ - "1.txt": "First file", - "2.txt": "Second file", - "3.txt": "Third file", - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let tab_1 = open_buffer("1.txt", &workspace, cx).await; - let tab_2 = open_buffer("2.txt", &workspace, cx).await; - let tab_3 = open_buffer("3.txt", &workspace, cx).await; - - // Starts with the last item selected - let tab_switcher = open_tab_switcher(true, &workspace, cx); - tab_switcher.update(cx, |tab_switcher, _| { - assert_eq!(tab_switcher.delegate.matches.len(), 3); - assert_match_at_position(tab_switcher, 0, tab_3); - assert_match_at_position(tab_switcher, 1, tab_2); - assert_match_selection(tab_switcher, 2, tab_1); - }); -} - -#[gpui::test] -async fn test_open_item_on_modifiers_release(cx: &mut gpui::TestAppContext) { - let app_state = init_test(cx); - - app_state - .fs - .as_fake() - .insert_tree( - path!("/root"), - json!({ - "1.txt": "First file", - "2.txt": "Second file", - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let tab_1 = open_buffer("1.txt", &workspace, cx).await; - let tab_2 = open_buffer("2.txt", &workspace, cx).await; - - cx.simulate_modifiers_change(Modifiers::control()); - let tab_switcher = open_tab_switcher(false, &workspace, cx); - tab_switcher.update(cx, |tab_switcher, _| { - assert_eq!(tab_switcher.delegate.matches.len(), 2); - assert_match_at_position(tab_switcher, 0, tab_2.boxed_clone()); - assert_match_selection(tab_switcher, 1, tab_1.boxed_clone()); - }); - - cx.simulate_modifiers_change(Modifiers::none()); - cx.read(|cx| { - let active_editor = workspace.read(cx).active_item_as::(cx).unwrap(); - assert_eq!(active_editor.read(cx).title(cx), "1.txt"); - }); - assert_tab_switcher_is_closed(workspace, cx); -} - -#[gpui::test] -async fn test_open_on_empty_pane(cx: &mut gpui::TestAppContext) { - let app_state = init_test(cx); - app_state.fs.as_fake().insert_tree("/root", json!({})).await; - - let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - cx.simulate_modifiers_change(Modifiers::control()); - let tab_switcher = open_tab_switcher(false, &workspace, cx); - tab_switcher.update(cx, |tab_switcher, _| { - assert!(tab_switcher.delegate.matches.is_empty()); - }); - - cx.simulate_modifiers_change(Modifiers::none()); - assert_tab_switcher_is_closed(workspace, cx); -} - -#[gpui::test] -async fn test_open_with_single_item(cx: &mut gpui::TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree(path!("/root"), json!({"1.txt": "Single file"})) - .await; - - let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let tab = open_buffer("1.txt", &workspace, cx).await; - - let tab_switcher = open_tab_switcher(false, &workspace, cx); - tab_switcher.update(cx, |tab_switcher, _| { - assert_eq!(tab_switcher.delegate.matches.len(), 1); - assert_match_selection(tab_switcher, 0, tab); - }); -} - -#[gpui::test] -async fn test_close_selected_item(cx: &mut gpui::TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - path!("/root"), - json!({ - "1.txt": "First file", - "2.txt": "Second file", - "3.txt": "Third file", - "4.txt": "Fourth file", - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let tab_1 = open_buffer("1.txt", &workspace, cx).await; - let tab_3 = open_buffer("3.txt", &workspace, cx).await; - let tab_2 = open_buffer("2.txt", &workspace, cx).await; - let tab_4 = open_buffer("4.txt", &workspace, cx).await; - - // After opening all buffers, let's navigate to the previous item two times, finishing with: - // - // 1.txt | [3.txt] | 2.txt | 4.txt - // - // With 3.txt being the active item in the pane. - cx.dispatch_action(ActivatePreviousItem); - cx.dispatch_action(ActivatePreviousItem); - cx.run_until_parked(); - - cx.simulate_modifiers_change(Modifiers::control()); - let tab_switcher = open_tab_switcher(false, &workspace, cx); - tab_switcher.update(cx, |tab_switcher, _| { - assert_eq!(tab_switcher.delegate.matches.len(), 4); - assert_match_at_position(tab_switcher, 0, tab_3.boxed_clone()); - assert_match_selection(tab_switcher, 1, tab_2.boxed_clone()); - assert_match_at_position(tab_switcher, 2, tab_4.boxed_clone()); - assert_match_at_position(tab_switcher, 3, tab_1.boxed_clone()); - }); - - cx.simulate_modifiers_change(Modifiers::control()); - cx.dispatch_action(CloseSelectedItem); - tab_switcher.update(cx, |tab_switcher, _| { - assert_eq!(tab_switcher.delegate.matches.len(), 3); - assert_match_selection(tab_switcher, 0, tab_3); - assert_match_at_position(tab_switcher, 1, tab_4); - assert_match_at_position(tab_switcher, 2, tab_1); - }); - - // Still switches tab on modifiers release - cx.simulate_modifiers_change(Modifiers::none()); - cx.read(|cx| { - let active_editor = workspace.read(cx).active_item_as::(cx).unwrap(); - assert_eq!(active_editor.read(cx).title(cx), "3.txt"); - }); - assert_tab_switcher_is_closed(workspace, cx); -} - -fn init_test(cx: &mut TestAppContext) -> Arc { - cx.update(|cx| { - let state = AppState::test(cx); - theme::init(theme::LoadThemes::JustBase, cx); - super::init(cx); - editor::init(cx); - state - }) -} - -#[track_caller] -fn open_tab_switcher( - select_last: bool, - workspace: &Entity, - cx: &mut VisualTestContext, -) -> Entity> { - cx.dispatch_action(Toggle { select_last }); - get_active_tab_switcher(workspace, cx) -} - -#[track_caller] -fn get_active_tab_switcher( - workspace: &Entity, - cx: &mut VisualTestContext, -) -> Entity> { - workspace.update(cx, |workspace, cx| { - workspace - .active_modal::(cx) - .expect("tab switcher is not open") - .read(cx) - .picker - .clone() - }) -} - -async fn open_buffer( - file_path: &str, - workspace: &Entity, - cx: &mut gpui::VisualTestContext, -) -> Box { - let project = workspace.read_with(cx, |workspace, _| workspace.project().clone()); - let worktree_id = project.update(cx, |project, cx| { - let worktree = project.worktrees(cx).last().expect("worktree not found"); - worktree.read(cx).id() - }); - let project_path = ProjectPath { - worktree_id, - path: rel_path(file_path).into(), - }; - workspace - .update_in(cx, move |workspace, window, cx| { - workspace.open_path(project_path, None, true, window, cx) - }) - .await - .unwrap() -} - -#[track_caller] -fn assert_match_selection( - tab_switcher: &Picker, - expected_selection_index: usize, - expected_item: Box, -) { - assert_eq!( - tab_switcher.delegate.selected_index(), - expected_selection_index, - "item is not selected" - ); - assert_match_at_position(tab_switcher, expected_selection_index, expected_item); -} - -#[track_caller] -fn assert_match_at_position( - tab_switcher: &Picker, - match_index: usize, - expected_item: Box, -) { - let match_item = tab_switcher - .delegate - .matches - .get(match_index) - .unwrap_or_else(|| panic!("Tab Switcher has no match for index {match_index}")); - assert_eq!(match_item.item.item_id(), expected_item.item_id()); -} - -#[track_caller] -fn assert_tab_switcher_is_closed(workspace: Entity, cx: &mut VisualTestContext) { - workspace.update(cx, |workspace, cx| { - assert!( - workspace.active_modal::(cx).is_none(), - "tab switcher is still open" - ); - }); -} diff --git a/crates/task/Cargo.toml b/crates/task/Cargo.toml deleted file mode 100644 index b3cb63bf00..0000000000 --- a/crates/task/Cargo.toml +++ /dev/null @@ -1,41 +0,0 @@ -[package] -name = "task" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[features] -test-support = [ - "gpui/test-support", - "util/test-support" -] - -[lib] -path = "src/task.rs" -doctest = false - -[lints] -workspace = true - -[dependencies] -anyhow.workspace = true -collections.workspace = true -futures.workspace = true -gpui.workspace = true -hex.workspace = true -log.workspace = true -parking_lot.workspace = true -proto.workspace = true -schemars.workspace = true -serde.workspace = true -serde_json.workspace = true -serde_json_lenient.workspace = true -sha2.workspace = true -shellexpand.workspace = true -util.workspace = true -zed_actions.workspace = true - -[dev-dependencies] -gpui = { workspace = true, features = ["test-support"] } -pretty_assertions.workspace = true diff --git a/crates/task/LICENSE-GPL b/crates/task/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/task/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/task/src/adapter_schema.rs b/crates/task/src/adapter_schema.rs deleted file mode 100644 index 2c58bc0eab..0000000000 --- a/crates/task/src/adapter_schema.rs +++ /dev/null @@ -1,16 +0,0 @@ -use gpui::SharedString; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -/// JSON schema for a specific adapter -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -pub struct AdapterSchema { - /// The adapter name identifier - pub adapter: SharedString, - /// The JSON schema for this adapter's configuration - pub schema: serde_json::Value, -} - -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(transparent)] -pub struct AdapterSchemas(pub Vec); diff --git a/crates/task/src/debug_format.rs b/crates/task/src/debug_format.rs deleted file mode 100644 index 5609e2565c..0000000000 --- a/crates/task/src/debug_format.rs +++ /dev/null @@ -1,534 +0,0 @@ -use anyhow::{Context as _, Result}; -use collections::FxHashMap; -use gpui::SharedString; -use log as _; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::net::Ipv4Addr; -use std::path::PathBuf; -use util::{debug_panic, schemars::add_new_subschema}; - -use crate::{TaskTemplate, adapter_schema::AdapterSchemas}; - -/// Represents the host information of the debug adapter -#[derive(Default, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] -pub struct TcpArgumentsTemplate { - /// The port that the debug adapter is listening on - /// - /// Default: We will try to find an open port - pub port: Option, - /// The host that the debug adapter is listening too - /// - /// Default: 127.0.0.1 - pub host: Option, - /// The max amount of time in milliseconds to connect to a tcp DAP before returning an error - /// - /// Default: 2000ms - pub timeout: Option, -} - -impl TcpArgumentsTemplate { - /// Get the host or fallback to the default host - pub fn host(&self) -> Ipv4Addr { - self.host.unwrap_or_else(|| Ipv4Addr::new(127, 0, 0, 1)) - } - - pub fn from_proto(proto: proto::TcpHost) -> Result { - Ok(Self { - port: proto.port.map(|p| p.try_into()).transpose()?, - host: proto.host.map(|h| h.parse()).transpose()?, - timeout: proto.timeout, - }) - } - - pub fn to_proto(&self) -> proto::TcpHost { - proto::TcpHost { - port: self.port.map(|p| p.into()), - host: self.host.map(|h| h.to_string()), - timeout: self.timeout, - } - } -} - -/// Represents the attach request information of the debug adapter -#[derive(Default, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] -pub struct AttachRequest { - /// The processId to attach to, if left empty we will show a process picker - pub process_id: Option, -} - -impl<'de> Deserialize<'de> for AttachRequest { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - struct Helper { - process_id: Option, - } - - let helper = Helper::deserialize(deserializer)?; - - // Skip creating an AttachRequest if process_id is None - if helper.process_id.is_none() { - return Err(serde::de::Error::custom("process_id is required")); - } - - Ok(AttachRequest { - process_id: helper.process_id, - }) - } -} - -/// Represents the launch request information of the debug adapter -#[derive(Deserialize, Serialize, Default, PartialEq, Eq, JsonSchema, Clone, Debug)] -pub struct LaunchRequest { - /// The program that you trying to debug - pub program: String, - /// The current working directory of your project - #[serde(default)] - pub cwd: Option, - /// Arguments to pass to a debuggee - #[serde(default)] - pub args: Vec, - #[serde(default)] - pub env: FxHashMap, -} - -impl LaunchRequest { - pub fn env_json(&self) -> serde_json::Value { - serde_json::Value::Object( - self.env - .iter() - .map(|(k, v)| (k.clone(), v.to_owned().into())) - .collect::>(), - ) - } -} - -/// Represents the type that will determine which request to call on the debug adapter -#[derive(Deserialize, Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] -#[serde(rename_all = "lowercase", tag = "request")] -pub enum DebugRequest { - /// Call the `launch` request on the debug adapter - Launch(LaunchRequest), - /// Call the `attach` request on the debug adapter - Attach(AttachRequest), -} - -impl DebugRequest { - pub fn to_proto(&self) -> proto::DebugRequest { - match self { - DebugRequest::Launch(launch_request) => proto::DebugRequest { - request: Some(proto::debug_request::Request::DebugLaunchRequest( - proto::DebugLaunchRequest { - program: launch_request.program.clone(), - cwd: launch_request - .cwd - .as_ref() - .map(|cwd| cwd.to_string_lossy().into_owned()), - args: launch_request.args.clone(), - env: launch_request - .env - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect(), - }, - )), - }, - DebugRequest::Attach(attach_request) => proto::DebugRequest { - request: Some(proto::debug_request::Request::DebugAttachRequest( - proto::DebugAttachRequest { - process_id: attach_request - .process_id - .expect("The process ID to be already filled out."), - }, - )), - }, - } - } - - pub fn from_proto(val: proto::DebugRequest) -> Result { - let request = val.request.context("Missing debug request")?; - match request { - proto::debug_request::Request::DebugLaunchRequest(proto::DebugLaunchRequest { - program, - cwd, - args, - env, - }) => Ok(DebugRequest::Launch(LaunchRequest { - program, - cwd: cwd.map(From::from), - args, - env: env.into_iter().collect(), - })), - - proto::debug_request::Request::DebugAttachRequest(proto::DebugAttachRequest { - process_id, - }) => Ok(DebugRequest::Attach(AttachRequest { - process_id: Some(process_id), - })), - } - } -} - -impl From for DebugRequest { - fn from(launch_config: LaunchRequest) -> Self { - DebugRequest::Launch(launch_config) - } -} - -impl From for DebugRequest { - fn from(attach_config: AttachRequest) -> Self { - DebugRequest::Attach(attach_config) - } -} - -#[derive(Serialize, PartialEq, Eq, JsonSchema, Clone, Debug)] -#[serde(untagged)] -pub enum BuildTaskDefinition { - ByName(SharedString), - Template { - #[serde(flatten)] - task_template: TaskTemplate, - #[serde(skip)] - locator_name: Option, - }, -} - -impl<'de> Deserialize<'de> for BuildTaskDefinition { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - #[derive(Deserialize)] - struct TemplateHelper { - #[serde(default)] - label: Option, - #[serde(flatten)] - rest: serde_json::Value, - } - - let value = serde_json::Value::deserialize(deserializer)?; - - if let Ok(name) = serde_json::from_value::(value.clone()) { - return Ok(BuildTaskDefinition::ByName(name)); - } - - let helper: TemplateHelper = - serde_json::from_value(value).map_err(serde::de::Error::custom)?; - - let mut template_value = helper.rest; - if let serde_json::Value::Object(ref mut map) = template_value { - map.insert( - "label".to_string(), - serde_json::to_value(helper.label.unwrap_or_else(|| "debug-build".to_owned())) - .map_err(serde::de::Error::custom)?, - ); - } - - let task_template: TaskTemplate = - serde_json::from_value(template_value).map_err(serde::de::Error::custom)?; - - Ok(BuildTaskDefinition::Template { - task_template, - locator_name: None, - }) - } -} - -#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug, JsonSchema)] -pub enum Request { - Launch, - Attach, -} - -/// This struct represent a user created debug task from the new process modal -#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct ZedDebugConfig { - /// Name of the debug task - pub label: SharedString, - /// The debug adapter to use - pub adapter: SharedString, - #[serde(flatten)] - pub request: DebugRequest, - /// Whether to tell the debug adapter to stop on entry - #[serde(default, skip_serializing_if = "Option::is_none")] - pub stop_on_entry: Option, -} - -/// This struct represent a user created debug task -#[derive(Deserialize, Serialize, PartialEq, Eq, Clone, Debug, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct DebugScenario { - pub adapter: SharedString, - /// Name of the debug task - pub label: SharedString, - /// A task to run prior to spawning the debuggee. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub build: Option, - /// The main arguments to be sent to the debug adapter - #[serde(default, flatten)] - pub config: serde_json::Value, - /// Optional TCP connection information - /// - /// If provided, this will be used to connect to the debug adapter instead of - /// spawning a new process. This is useful for connecting to a debug adapter - /// that is already running or is started by another process. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub tcp_connection: Option, -} - -/// A group of Debug Tasks defined in a JSON file. -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(transparent)] -pub struct DebugTaskFile(pub Vec); - -impl DebugTaskFile { - pub fn generate_json_schema(schemas: &AdapterSchemas) -> serde_json::Value { - let mut generator = schemars::generate::SchemaSettings::draft2019_09().into_generator(); - - let mut build_task_value = BuildTaskDefinition::json_schema(&mut generator).to_value(); - - if let Some(template_object) = build_task_value - .get_mut("anyOf") - .and_then(|array| array.as_array_mut()) - .and_then(|array| array.get_mut(1)) - { - if let Some(properties) = template_object - .get_mut("properties") - .and_then(|value| value.as_object_mut()) - && properties.remove("label").is_none() - { - debug_panic!( - "Generated TaskTemplate json schema did not have expected 'label' field. \ - Schema of 2nd alternative is: {template_object:?}" - ); - } - - if let Some(arr) = template_object - .get_mut("required") - .and_then(|array| array.as_array_mut()) - { - arr.retain(|v| v.as_str() != Some("label")); - } - } else { - debug_panic!( - "Generated TaskTemplate json schema did not match expectations. \ - Schema is: {build_task_value:?}" - ); - } - - let adapter_conditions = schemas - .0 - .iter() - .map(|adapter_schema| { - let adapter_name = adapter_schema.adapter.to_string(); - add_new_subschema( - &mut generator, - &format!("{adapter_name}DebugSettings"), - serde_json::json!({ - "if": { - "properties": { - "adapter": { "const": adapter_name } - } - }, - "then": adapter_schema.schema - }), - ) - }) - .collect::>(); - - let build_task_definition_ref = add_new_subschema( - &mut generator, - BuildTaskDefinition::schema_name().as_ref(), - build_task_value, - ); - - let meta_schema = generator - .settings() - .meta_schema - .as_ref() - .expect("meta_schema should be present in schemars settings") - .to_string(); - - serde_json::json!({ - "$schema": meta_schema, - "title": "Debug Configurations", - "description": "Configuration for debug scenarios", - "allowTrailingCommas": true, - "type": "array", - "items": { - "type": "object", - "required": ["adapter", "label"], - // TODO: Uncommenting this will cause json-language-server to provide warnings for - // unrecognized properties. It should be enabled if/when there's an adapter JSON - // schema that's comprehensive. In order to not get warnings for the other schemas, - // `additionalProperties` or `unevaluatedProperties` (to handle "allOf" etc style - // schema combinations) could be set to `true` for that schema. - // - // "unevaluatedProperties": false, - "properties": { - "adapter": { - "type": "string", - "description": "The name of the debug adapter" - }, - "label": { - "type": "string", - "description": "The name of the debug configuration" - }, - "build": build_task_definition_ref, - "tcp_connection": { - "type": "object", - "description": "Optional TCP connection information for connecting to an already running debug adapter", - "properties": { - "port": { - "type": "integer", - "description": "The port that the debug adapter is listening on (default: auto-find open port)" - }, - "host": { - "type": "string", - "pattern": "^((25[0-5]|(2[0-4]|1\\d|[1-9]|)\\d)\\.?\\b){4}$", - "description": "The host that the debug adapter is listening to (default: 127.0.0.1)" - }, - "timeout": { - "type": "integer", - "description": "The max amount of time in milliseconds to connect to a tcp DAP before returning an error (default: 2000ms)" - } - } - } - }, - "allOf": adapter_conditions - }, - "$defs": generator.take_definitions(true), - }) - } -} - -#[cfg(test)] -mod tests { - use crate::DebugScenario; - use serde_json::json; - - #[test] - fn test_just_build_args() { - let json = r#"{ - "label": "Build & debug rust", - "adapter": "CodeLLDB", - "build": { - "command": "rust", - "args": ["build"] - } - }"#; - - let deserialized: DebugScenario = serde_json::from_str(json).unwrap(); - assert!(deserialized.build.is_some()); - match deserialized.build.as_ref().unwrap() { - crate::BuildTaskDefinition::Template { task_template, .. } => { - assert_eq!("debug-build", task_template.label); - assert_eq!("rust", task_template.command); - assert_eq!(vec!["build"], task_template.args); - } - _ => panic!("Expected Template variant"), - } - assert_eq!(json!({}), deserialized.config); - assert_eq!("CodeLLDB", deserialized.adapter.as_ref()); - assert_eq!("Build & debug rust", deserialized.label.as_ref()); - } - - #[test] - fn test_empty_scenario_has_none_request() { - let json = r#"{ - "label": "Build & debug rust", - "build": "rust", - "adapter": "CodeLLDB" - }"#; - - let deserialized: DebugScenario = serde_json::from_str(json).unwrap(); - - assert_eq!(json!({}), deserialized.config); - assert_eq!("CodeLLDB", deserialized.adapter.as_ref()); - assert_eq!("Build & debug rust", deserialized.label.as_ref()); - } - - #[test] - fn test_launch_scenario_deserialization() { - let json = r#"{ - "label": "Launch program", - "adapter": "CodeLLDB", - "request": "launch", - "program": "target/debug/myapp", - "args": ["--test"] - }"#; - - let deserialized: DebugScenario = serde_json::from_str(json).unwrap(); - - assert_eq!( - json!({ "request": "launch", "program": "target/debug/myapp", "args": ["--test"] }), - deserialized.config - ); - assert_eq!("CodeLLDB", deserialized.adapter.as_ref()); - assert_eq!("Launch program", deserialized.label.as_ref()); - } - - #[test] - fn test_attach_scenario_deserialization() { - let json = r#"{ - "label": "Attach to process", - "adapter": "CodeLLDB", - "process_id": 1234, - "request": "attach" - }"#; - - let deserialized: DebugScenario = serde_json::from_str(json).unwrap(); - - assert_eq!( - json!({ "request": "attach", "process_id": 1234 }), - deserialized.config - ); - assert_eq!("CodeLLDB", deserialized.adapter.as_ref()); - assert_eq!("Attach to process", deserialized.label.as_ref()); - } - - #[test] - fn test_build_task_definition_without_label() { - use crate::BuildTaskDefinition; - - let json = r#""my_build_task""#; - let deserialized: BuildTaskDefinition = serde_json::from_str(json).unwrap(); - match deserialized { - BuildTaskDefinition::ByName(name) => assert_eq!("my_build_task", name.as_ref()), - _ => panic!("Expected ByName variant"), - } - - let json = r#"{ - "command": "cargo", - "args": ["build", "--release"] - }"#; - let deserialized: BuildTaskDefinition = serde_json::from_str(json).unwrap(); - match deserialized { - BuildTaskDefinition::Template { task_template, .. } => { - assert_eq!("debug-build", task_template.label); - assert_eq!("cargo", task_template.command); - assert_eq!(vec!["build", "--release"], task_template.args); - } - _ => panic!("Expected Template variant"), - } - - let json = r#"{ - "label": "Build Release", - "command": "cargo", - "args": ["build", "--release"] - }"#; - let deserialized: BuildTaskDefinition = serde_json::from_str(json).unwrap(); - match deserialized { - BuildTaskDefinition::Template { task_template, .. } => { - assert_eq!("Build Release", task_template.label); - assert_eq!("cargo", task_template.command); - assert_eq!(vec!["build", "--release"], task_template.args); - } - _ => panic!("Expected Template variant"), - } - } -} diff --git a/crates/task/src/serde_helpers.rs b/crates/task/src/serde_helpers.rs deleted file mode 100644 index a95214d8b0..0000000000 --- a/crates/task/src/serde_helpers.rs +++ /dev/null @@ -1,37 +0,0 @@ -use serde::de::{self, Deserializer, Visitor}; -use std::fmt; - -/// Deserializes a non-empty string array. -pub fn non_empty_string_vec<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - struct NonEmptyStringVecVisitor; - - impl<'de> Visitor<'de> for NonEmptyStringVecVisitor { - type Value = Vec; - - fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str("a list of non-empty strings") - } - - fn visit_seq(self, mut seq: V) -> Result, V::Error> - where - V: de::SeqAccess<'de>, - { - let mut vec = Vec::new(); - while let Some(value) = seq.next_element::()? { - if value.is_empty() { - return Err(de::Error::invalid_value( - de::Unexpected::Str(&value), - &"a non-empty string", - )); - } - vec.push(value); - } - Ok(vec) - } - } - - deserializer.deserialize_seq(NonEmptyStringVecVisitor) -} diff --git a/crates/task/src/static_source.rs b/crates/task/src/static_source.rs deleted file mode 100644 index 9e4051ef97..0000000000 --- a/crates/task/src/static_source.rs +++ /dev/null @@ -1,123 +0,0 @@ -//! A source of tasks, based on a static configuration, deserialized from the tasks config file, and related infrastructure for tracking changes to the file. - -use std::sync::Arc; - -use futures::{StreamExt, channel::mpsc::UnboundedSender}; -use gpui::{App, AppContext}; -use parking_lot::RwLock; -use serde::Deserialize; -use util::ResultExt; - -use crate::TaskTemplates; -use futures::channel::mpsc::UnboundedReceiver; - -/// The source of tasks defined in a tasks config file. -pub struct StaticSource { - tasks: TrackedFile, -} - -/// A Wrapper around deserializable T that keeps track of its contents -/// via a provided channel. -pub struct TrackedFile { - parsed_contents: Arc>, -} - -impl TrackedFile { - /// Initializes new [`TrackedFile`] with a type that's deserializable. - pub fn new( - mut tracker: UnboundedReceiver, - notification_outlet: UnboundedSender<()>, - cx: &App, - ) -> Self - where - T: for<'a> Deserialize<'a> + Default + Send, - { - let parsed_contents: Arc> = Arc::default(); - cx.background_spawn({ - let parsed_contents = parsed_contents.clone(); - async move { - while let Some(new_contents) = tracker.next().await { - if Arc::strong_count(&parsed_contents) == 1 { - // We're no longer being observed. Stop polling. - break; - } - if !new_contents.trim().is_empty() { - let Some(new_contents) = - serde_json_lenient::from_str::(&new_contents).log_err() - else { - continue; - }; - let mut contents = parsed_contents.write(); - if *contents != new_contents { - *contents = new_contents; - if notification_outlet.unbounded_send(()).is_err() { - // Whoever cared about contents is not around anymore. - break; - } - } - } - } - anyhow::Ok(()) - } - }) - .detach_and_log_err(cx); - Self { parsed_contents } - } - - /// Initializes new [`TrackedFile`] with a type that's convertible from another deserializable type. - pub fn new_convertible Deserialize<'a> + TryInto>( - mut tracker: UnboundedReceiver, - notification_outlet: UnboundedSender<()>, - cx: &App, - ) -> Self - where - T: Default + Send, - { - let parsed_contents: Arc> = Arc::default(); - cx.background_spawn({ - async move { - while let Some(new_contents) = tracker.next().await { - if Arc::strong_count(&parsed_contents) == 1 { - // We're no longer being observed. Stop polling. - break; - } - - if !new_contents.trim().is_empty() { - let Some(new_contents) = - serde_json_lenient::from_str::(&new_contents).log_err() - else { - continue; - }; - let Some(new_contents) = new_contents.try_into().log_err() else { - continue; - }; - let mut contents = parsed_contents.write(); - if *contents != new_contents { - *contents = new_contents; - if notification_outlet.unbounded_send(()).is_err() { - // Whoever cared about contents is not around anymore. - break; - } - } - } - } - anyhow::Ok(()) - } - }) - .detach_and_log_err(cx); - Self { - parsed_contents: Default::default(), - } - } -} - -impl StaticSource { - /// Initializes the static source, reacting on tasks config changes. - pub fn new(tasks: TrackedFile) -> Self { - Self { tasks } - } - /// Returns current list of tasks - pub fn tasks_to_schedule(&self) -> TaskTemplates { - self.tasks.parsed_contents.read().clone() - } -} diff --git a/crates/task/src/task.rs b/crates/task/src/task.rs deleted file mode 100644 index 92d5909419..0000000000 --- a/crates/task/src/task.rs +++ /dev/null @@ -1,431 +0,0 @@ -//! Baseline interface of Tasks in Zed: all tasks in Zed are intended to use those for implementing their own logic. - -mod adapter_schema; -mod debug_format; -mod serde_helpers; -pub mod static_source; -mod task_template; -mod vscode_debug_format; -mod vscode_format; - -use anyhow::Context as _; -use collections::{HashMap, HashSet, hash_map}; -use gpui::SharedString; -use serde::{Deserialize, Serialize}; -use std::borrow::Cow; -use std::path::PathBuf; -use std::str::FromStr; - -pub use adapter_schema::{AdapterSchema, AdapterSchemas}; -pub use debug_format::{ - AttachRequest, BuildTaskDefinition, DebugRequest, DebugScenario, DebugTaskFile, LaunchRequest, - Request, TcpArgumentsTemplate, ZedDebugConfig, -}; -pub use task_template::{ - DebugArgsRequest, HideStrategy, RevealStrategy, TaskTemplate, TaskTemplates, - substitute_variables_in_map, substitute_variables_in_str, -}; -pub use util::shell::{Shell, ShellKind}; -pub use util::shell_builder::ShellBuilder; -pub use vscode_debug_format::VsCodeDebugTaskFile; -pub use vscode_format::VsCodeTaskFile; -pub use zed_actions::RevealTarget; - -/// Task identifier, unique within the application. -/// Based on it, task reruns and terminal tabs are managed. -#[derive(Default, Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Deserialize)] -pub struct TaskId(pub String); - -/// Contains all information needed by Zed to spawn a new terminal tab for the given task. -#[derive(Default, Debug, Clone, PartialEq, Eq)] -pub struct SpawnInTerminal { - /// Id of the task to use when determining task tab affinity. - pub id: TaskId, - /// Full unshortened form of `label` field. - pub full_label: String, - /// Human readable name of the terminal tab. - pub label: String, - /// Executable command to spawn. - pub command: Option, - /// Arguments to the command, potentially unsubstituted, - /// to let the shell that spawns the command to do the substitution, if needed. - pub args: Vec, - /// A human-readable label, containing command and all of its arguments, joined and substituted. - pub command_label: String, - /// Current working directory to spawn the command into. - pub cwd: Option, - /// Env overrides for the command, will be appended to the terminal's environment from the settings. - pub env: HashMap, - /// Whether to use a new terminal tab or reuse the existing one to spawn the process. - pub use_new_terminal: bool, - /// Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish. - pub allow_concurrent_runs: bool, - /// What to do with the terminal pane and tab, after the command was started. - pub reveal: RevealStrategy, - /// Where to show tasks' terminal output. - pub reveal_target: RevealTarget, - /// What to do with the terminal pane and tab, after the command had finished. - pub hide: HideStrategy, - /// Which shell to use when spawning the task. - pub shell: Shell, - /// Whether to show the task summary line in the task output (sucess/failure). - pub show_summary: bool, - /// Whether to show the command line in the task output. - pub show_command: bool, - /// Whether to show the rerun button in the terminal tab. - pub show_rerun: bool, -} - -impl SpawnInTerminal { - pub fn to_proto(&self) -> proto::SpawnInTerminal { - proto::SpawnInTerminal { - label: self.label.clone(), - command: self.command.clone(), - args: self.args.clone(), - env: self - .env - .iter() - .map(|(k, v)| (k.clone(), v.clone())) - .collect(), - cwd: self - .cwd - .clone() - .map(|cwd| cwd.to_string_lossy().into_owned()), - } - } - - pub fn from_proto(proto: proto::SpawnInTerminal) -> Self { - Self { - label: proto.label.clone(), - command: proto.command.clone(), - args: proto.args.clone(), - env: proto.env.into_iter().collect(), - cwd: proto.cwd.map(PathBuf::from), - ..Default::default() - } - } -} - -/// A final form of the [`TaskTemplate`], that got resolved with a particular [`TaskContext`] and now is ready to spawn the actual task. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ResolvedTask { - /// A way to distinguish tasks produced by the same template, but different contexts. - /// NOTE: Resolved tasks may have the same labels, commands and do the same things, - /// but still may have different ids if the context was different during the resolution. - /// Since the template has `env` field, for a generic task that may be a bash command, - /// so it's impossible to determine the id equality without more context in a generic case. - pub id: TaskId, - /// A template the task got resolved from. - original_task: TaskTemplate, - /// Full, unshortened label of the task after all resolutions are made. - pub resolved_label: String, - /// Variables that were substituted during the task template resolution. - substituted_variables: HashSet, - /// Further actions that need to take place after the resolved task is spawned, - /// with all task variables resolved. - pub resolved: SpawnInTerminal, -} - -impl ResolvedTask { - /// A task template before the resolution. - pub fn original_task(&self) -> &TaskTemplate { - &self.original_task - } - - /// Variables that were substituted during the task template resolution. - pub fn substituted_variables(&self) -> &HashSet { - &self.substituted_variables - } - - /// A human-readable label to display in the UI. - pub fn display_label(&self) -> &str { - self.resolved.label.as_str() - } -} - -/// Variables, available for use in [`TaskContext`] when a Zed's [`TaskTemplate`] gets resolved into a [`ResolvedTask`]. -/// Name of the variable must be a valid shell variable identifier, which generally means that it is -/// a word consisting only of alphanumeric characters and underscores, -/// and beginning with an alphabetic character or an underscore. -#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)] -pub enum VariableName { - /// An absolute path of the currently opened file. - File, - /// A path of the currently opened file (relative to worktree root). - RelativeFile, - /// A path of the currently opened file's directory (relative to worktree root). - RelativeDir, - /// The currently opened filename. - Filename, - /// The path to a parent directory of a currently opened file. - Dirname, - /// Stem (filename without extension) of the currently opened file. - Stem, - /// An absolute path of the currently opened worktree, that contains the file. - WorktreeRoot, - /// A symbol text, that contains latest cursor/selection position. - Symbol, - /// A row with the latest cursor/selection position. - Row, - /// A column with the latest cursor/selection position. - Column, - /// Text from the latest selection. - SelectedText, - /// The symbol selected by the symbol tagging system, specifically the @run capture in a runnables.scm - RunnableSymbol, - /// Open a Picker to select a process ID to use in place - /// Can only be used to debug configurations - PickProcessId, - /// Custom variable, provided by the plugin or other external source. - /// Will be printed with `CUSTOM_` prefix to avoid potential conflicts with other variables. - Custom(Cow<'static, str>), -} - -impl VariableName { - /// Generates a `$VARIABLE`-like string value to be used in templates. - pub fn template_value(&self) -> String { - format!("${self}") - } - /// Generates a `"$VARIABLE"`-like string, to be used instead of `Self::template_value` when expanded value could contain spaces or special characters. - pub fn template_value_with_whitespace(&self) -> String { - format!("\"${self}\"") - } -} - -impl FromStr for VariableName { - type Err = (); - - fn from_str(s: &str) -> Result { - let without_prefix = s.strip_prefix(ZED_VARIABLE_NAME_PREFIX).ok_or(())?; - let value = match without_prefix { - "FILE" => Self::File, - "FILENAME" => Self::Filename, - "RELATIVE_FILE" => Self::RelativeFile, - "RELATIVE_DIR" => Self::RelativeDir, - "DIRNAME" => Self::Dirname, - "STEM" => Self::Stem, - "WORKTREE_ROOT" => Self::WorktreeRoot, - "SYMBOL" => Self::Symbol, - "RUNNABLE_SYMBOL" => Self::RunnableSymbol, - "SELECTED_TEXT" => Self::SelectedText, - "ROW" => Self::Row, - "COLUMN" => Self::Column, - _ => { - if let Some(custom_name) = - without_prefix.strip_prefix(ZED_CUSTOM_VARIABLE_NAME_PREFIX) - { - Self::Custom(Cow::Owned(custom_name.to_owned())) - } else { - return Err(()); - } - } - }; - Ok(value) - } -} - -/// A prefix that all [`VariableName`] variants are prefixed with when used in environment variables and similar template contexts. -pub const ZED_VARIABLE_NAME_PREFIX: &str = "ZED_"; -const ZED_CUSTOM_VARIABLE_NAME_PREFIX: &str = "CUSTOM_"; - -impl std::fmt::Display for VariableName { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - Self::File => write!(f, "{ZED_VARIABLE_NAME_PREFIX}FILE"), - Self::Filename => write!(f, "{ZED_VARIABLE_NAME_PREFIX}FILENAME"), - Self::RelativeFile => write!(f, "{ZED_VARIABLE_NAME_PREFIX}RELATIVE_FILE"), - Self::RelativeDir => write!(f, "{ZED_VARIABLE_NAME_PREFIX}RELATIVE_DIR"), - Self::Dirname => write!(f, "{ZED_VARIABLE_NAME_PREFIX}DIRNAME"), - Self::Stem => write!(f, "{ZED_VARIABLE_NAME_PREFIX}STEM"), - Self::WorktreeRoot => write!(f, "{ZED_VARIABLE_NAME_PREFIX}WORKTREE_ROOT"), - Self::Symbol => write!(f, "{ZED_VARIABLE_NAME_PREFIX}SYMBOL"), - Self::Row => write!(f, "{ZED_VARIABLE_NAME_PREFIX}ROW"), - Self::Column => write!(f, "{ZED_VARIABLE_NAME_PREFIX}COLUMN"), - Self::SelectedText => write!(f, "{ZED_VARIABLE_NAME_PREFIX}SELECTED_TEXT"), - Self::RunnableSymbol => write!(f, "{ZED_VARIABLE_NAME_PREFIX}RUNNABLE_SYMBOL"), - Self::PickProcessId => write!(f, "{ZED_VARIABLE_NAME_PREFIX}PICK_PID"), - Self::Custom(s) => write!( - f, - "{ZED_VARIABLE_NAME_PREFIX}{ZED_CUSTOM_VARIABLE_NAME_PREFIX}{s}" - ), - } - } -} - -/// Container for predefined environment variables that describe state of Zed at the time the task was spawned. -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize)] -pub struct TaskVariables(HashMap); - -impl TaskVariables { - /// Inserts another variable into the container, overwriting the existing one if it already exists — in this case, the old value is returned. - pub fn insert(&mut self, variable: VariableName, value: String) -> Option { - self.0.insert(variable, value) - } - - /// Extends the container with another one, overwriting the existing variables on collision. - pub fn extend(&mut self, other: Self) { - self.0.extend(other.0); - } - /// Get the value associated with given variable name, if there is one. - pub fn get(&self, key: &VariableName) -> Option<&str> { - self.0.get(key).map(|s| s.as_str()) - } - /// Clear out variables obtained from tree-sitter queries, which are prefixed with '_' character - pub fn sweep(&mut self) { - self.0.retain(|name, _| { - if let VariableName::Custom(name) = name { - !name.starts_with('_') - } else { - true - } - }) - } - - pub fn iter(&self) -> impl Iterator { - self.0.iter() - } -} - -impl FromIterator<(VariableName, String)> for TaskVariables { - fn from_iter>(iter: T) -> Self { - Self(HashMap::from_iter(iter)) - } -} - -impl IntoIterator for TaskVariables { - type Item = (VariableName, String); - - type IntoIter = hash_map::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} - -/// Keeps track of the file associated with a task and context of tasks execution (i.e. current file or current function). -/// Keeps all Zed-related state inside, used to produce a resolved task out of its template. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct TaskContext { - /// A path to a directory in which the task should be executed. - pub cwd: Option, - /// Additional environment variables associated with a given task. - pub task_variables: TaskVariables, - /// Environment variables obtained when loading the project into Zed. - /// This is the environment one would get when `cd`ing in a terminal - /// into the project's root directory. - pub project_env: HashMap, -} - -/// This is a new type representing a 'tag' on a 'runnable symbol', typically a test of main() function, found via treesitter. -#[derive(Clone, Debug)] -pub struct RunnableTag(pub SharedString); - -pub fn shell_from_proto(proto: proto::Shell) -> anyhow::Result { - let shell_type = proto.shell_type.context("invalid shell type")?; - let shell = match shell_type { - proto::shell::ShellType::System(_) => Shell::System, - proto::shell::ShellType::Program(program) => Shell::Program(program), - proto::shell::ShellType::WithArguments(program) => Shell::WithArguments { - program: program.program, - args: program.args, - title_override: None, - }, - }; - Ok(shell) -} - -pub fn shell_to_proto(shell: Shell) -> proto::Shell { - let shell_type = match shell { - Shell::System => proto::shell::ShellType::System(proto::System {}), - Shell::Program(program) => proto::shell::ShellType::Program(program), - Shell::WithArguments { - program, - args, - title_override: _, - } => proto::shell::ShellType::WithArguments(proto::shell::WithArguments { program, args }), - }; - proto::Shell { - shell_type: Some(shell_type), - } -} - -type VsCodeEnvVariable = String; -type VsCodeCommand = String; -type ZedEnvVariable = String; - -struct EnvVariableReplacer { - variables: HashMap, - commands: HashMap, -} - -impl EnvVariableReplacer { - fn new(variables: HashMap) -> Self { - Self { - variables, - commands: HashMap::default(), - } - } - - fn with_commands( - mut self, - commands: impl IntoIterator, - ) -> Self { - self.commands = commands.into_iter().collect(); - self - } - - fn replace_value(&self, input: serde_json::Value) -> serde_json::Value { - match input { - serde_json::Value::String(s) => serde_json::Value::String(self.replace(&s)), - serde_json::Value::Array(arr) => { - serde_json::Value::Array(arr.into_iter().map(|v| self.replace_value(v)).collect()) - } - serde_json::Value::Object(obj) => serde_json::Value::Object( - obj.into_iter() - .map(|(k, v)| (self.replace(&k), self.replace_value(v))) - .collect(), - ), - _ => input, - } - } - // Replaces occurrences of VsCode-specific environment variables with Zed equivalents. - fn replace(&self, input: &str) -> String { - shellexpand::env_with_context_no_errors(&input, |var: &str| { - // Colons denote a default value in case the variable is not set. We want to preserve that default, as otherwise shellexpand will substitute it for us. - let colon_position = var.find(':').unwrap_or(var.len()); - let (left, right) = var.split_at(colon_position); - if left == "env" && !right.is_empty() { - let variable_name = &right[1..]; - return Some(format!("${{{variable_name}}}")); - } else if left == "command" && !right.is_empty() { - let command_name = &right[1..]; - if let Some(replacement_command) = self.commands.get(command_name) { - return Some(format!("${{{replacement_command}}}")); - } - } - - let (variable_name, default) = (left, right); - let append_previous_default = |ret: &mut String| { - if !default.is_empty() { - ret.push_str(default); - } - }; - if let Some(substitution) = self.variables.get(variable_name) { - // Got a VSCode->Zed hit, perform a substitution - let mut name = format!("${{{substitution}"); - append_previous_default(&mut name); - name.push('}'); - return Some(name); - } - // This is an unknown variable. - // We should not error out, as they may come from user environment (e.g. $PATH). That means that the variable substitution might not be perfect. - // If there's a default, we need to return the string verbatim as otherwise shellexpand will apply that default for us. - if !default.is_empty() { - return Some(format!("${{{var}}}")); - } - // Else we can just return None and that variable will be left as is. - None - }) - .into_owned() - } -} diff --git a/crates/task/src/task_template.rs b/crates/task/src/task_template.rs deleted file mode 100644 index 0c319db061..0000000000 --- a/crates/task/src/task_template.rs +++ /dev/null @@ -1,974 +0,0 @@ -use anyhow::{Context as _, bail}; -use collections::{HashMap, HashSet}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; -use std::path::PathBuf; -use util::schemars::{AllowTrailingCommas, DefaultDenyUnknownFields}; -use util::serde::default_true; -use util::{ResultExt, truncate_and_remove_front}; - -use crate::{ - AttachRequest, ResolvedTask, RevealTarget, Shell, SpawnInTerminal, TaskContext, TaskId, - VariableName, ZED_VARIABLE_NAME_PREFIX, serde_helpers::non_empty_string_vec, -}; - -/// A template definition of a Zed task to run. -/// May use the [`VariableName`] to get the corresponding substitutions into its fields. -/// -/// Template itself is not ready to spawn a task, it needs to be resolved with a [`TaskContext`] first, that -/// contains all relevant Zed state in task variables. -/// A single template may produce different tasks (or none) for different contexts. -#[derive(Clone, Default, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub struct TaskTemplate { - /// Human readable name of the task to display in the UI. - pub label: String, - /// Executable command to spawn. - pub command: String, - /// Arguments to the command. - #[serde(default)] - pub args: Vec, - /// Env overrides for the command, will be appended to the terminal's environment from the settings. - #[serde(default)] - pub env: HashMap, - /// Current working directory to spawn the command into, defaults to current project root. - #[serde(default)] - pub cwd: Option, - /// Whether to use a new terminal tab or reuse the existing one to spawn the process. - #[serde(default)] - pub use_new_terminal: bool, - /// Whether to allow multiple instances of the same task to be run, or rather wait for the existing ones to finish. - #[serde(default)] - pub allow_concurrent_runs: bool, - /// What to do with the terminal pane and tab, after the command was started: - /// * `always` — always show the task's pane, and focus the corresponding tab in it (default) - // * `no_focus` — always show the task's pane, add the task's tab in it, but don't focus it - // * `never` — do not alter focus, but still add/reuse the task's tab in its pane - #[serde(default)] - pub reveal: RevealStrategy, - /// Where to place the task's terminal item after starting the task. - /// * `dock` — in the terminal dock, "regular" terminal items' place (default). - /// * `center` — in the central pane group, "main" editor area. - #[serde(default)] - pub reveal_target: RevealTarget, - /// What to do with the terminal pane and tab, after the command had finished: - /// * `never` — do nothing when the command finishes (default) - /// * `always` — always hide the terminal tab, hide the pane also if it was the last tab in it - /// * `on_success` — hide the terminal tab on task success only, otherwise behaves similar to `always`. - #[serde(default)] - pub hide: HideStrategy, - /// Represents the tags which this template attaches to. - /// Adding this removes this task from other UI and gives you ability to run it by tag. - #[serde(default, deserialize_with = "non_empty_string_vec")] - #[schemars(length(min = 1))] - pub tags: Vec, - /// Which shell to use when spawning the task. - #[serde(default)] - pub shell: Shell, - /// Whether to show the task line in the task output. - #[serde(default = "default_true")] - pub show_summary: bool, - /// Whether to show the command line in the task output. - #[serde(default = "default_true")] - pub show_command: bool, -} - -#[derive(Deserialize, Eq, PartialEq, Clone, Debug)] -/// Use to represent debug request type -pub enum DebugArgsRequest { - /// launch (program, cwd) are stored in TaskTemplate as (command, cwd) - Launch, - /// Attach - Attach(AttachRequest), -} - -/// What to do with the terminal pane and tab, after the command was started. -#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum RevealStrategy { - /// Always show the task's pane, and focus the corresponding tab in it. - #[default] - Always, - /// Always show the task's pane, add the task's tab in it, but don't focus it. - NoFocus, - /// Do not alter focus, but still add/reuse the task's tab in its pane. - Never, -} - -/// What to do with the terminal pane and tab, after the command has finished. -#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum HideStrategy { - /// Do nothing when the command finishes. - #[default] - Never, - /// Always hide the terminal tab, hide the pane also if it was the last tab in it. - Always, - /// Hide the terminal tab on task success only, otherwise behaves similar to `Always`. - OnSuccess, -} - -/// A group of Tasks defined in a JSON file. -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -pub struct TaskTemplates(pub Vec); - -impl TaskTemplates { - /// Generates JSON schema of Tasks JSON template format. - pub fn generate_json_schema() -> serde_json::Value { - let schema = schemars::generate::SchemaSettings::draft2019_09() - .with_transform(DefaultDenyUnknownFields) - .with_transform(AllowTrailingCommas) - .into_generator() - .root_schema_for::(); - - serde_json::to_value(schema).unwrap() - } -} - -impl TaskTemplate { - /// Replaces all `VariableName` task variables in the task template string fields. - /// If any replacement fails or the new string substitutions still have [`ZED_VARIABLE_NAME_PREFIX`], - /// `None` is returned. - /// - /// Every [`ResolvedTask`] gets a [`TaskId`], based on the `id_base` (to avoid collision with various task sources), - /// and hashes of its template and [`TaskContext`], see [`ResolvedTask`] fields' documentation for more details. - pub fn resolve_task(&self, id_base: &str, cx: &TaskContext) -> Option { - if self.label.trim().is_empty() || self.command.trim().is_empty() { - return None; - } - - let mut variable_names = HashMap::default(); - let mut substituted_variables = HashSet::default(); - let task_variables = cx - .task_variables - .0 - .iter() - .map(|(key, value)| { - let key_string = key.to_string(); - if !variable_names.contains_key(&key_string) { - variable_names.insert(key_string.clone(), key.clone()); - } - (key_string, value.as_str()) - }) - .collect::>(); - let truncated_variables = truncate_variables(&task_variables); - let cwd = match self.cwd.as_deref() { - Some(cwd) => { - let substituted_cwd = substitute_all_template_variables_in_str( - cwd, - &task_variables, - &variable_names, - &mut substituted_variables, - )?; - Some(PathBuf::from(substituted_cwd)) - } - None => None, - } - .or(cx.cwd.clone()); - let full_label = substitute_all_template_variables_in_str( - &self.label, - &task_variables, - &variable_names, - &mut substituted_variables, - )?; - - // Arbitrarily picked threshold below which we don't truncate any variables. - const TRUNCATION_THRESHOLD: usize = 64; - - let human_readable_label = if full_label.len() > TRUNCATION_THRESHOLD { - substitute_all_template_variables_in_str( - &self.label, - &truncated_variables, - &variable_names, - &mut substituted_variables, - )? - } else { - #[allow( - clippy::redundant_clone, - reason = "We want to clone the full_label to avoid borrowing it in the fold closure" - )] - full_label.clone() - } - .lines() - .fold(String::new(), |mut string, line| { - if string.is_empty() { - string.push_str(line); - } else { - string.push_str("\\n"); - string.push_str(line); - } - string - }); - - let command = substitute_all_template_variables_in_str( - &self.command, - &task_variables, - &variable_names, - &mut substituted_variables, - )?; - let args_with_substitutions = substitute_all_template_variables_in_vec( - &self.args, - &task_variables, - &variable_names, - &mut substituted_variables, - )?; - - let task_hash = to_hex_hash(self) - .context("hashing task template") - .log_err()?; - let variables_hash = to_hex_hash(&task_variables) - .context("hashing task variables") - .log_err()?; - let id = TaskId(format!("{id_base}_{task_hash}_{variables_hash}")); - - let env = { - // Start with the project environment as the base. - let mut env = cx.project_env.clone(); - - // Extend that environment with what's defined in the TaskTemplate - env.extend(self.env.clone()); - - // Then we replace all task variables that could be set in environment variables - let mut env = substitute_all_template_variables_in_map( - &env, - &task_variables, - &variable_names, - &mut substituted_variables, - )?; - - // Last step: set the task variables as environment variables too - env.extend(task_variables.into_iter().map(|(k, v)| (k, v.to_owned()))); - env - }; - - Some(ResolvedTask { - id: id.clone(), - substituted_variables, - original_task: self.clone(), - resolved_label: full_label.clone(), - resolved: SpawnInTerminal { - id, - cwd, - full_label, - label: human_readable_label, - command_label: args_with_substitutions.iter().fold( - command.clone(), - |mut command_label, arg| { - command_label.push(' '); - command_label.push_str(arg); - command_label - }, - ), - command: Some(command), - args: args_with_substitutions, - env, - use_new_terminal: self.use_new_terminal, - allow_concurrent_runs: self.allow_concurrent_runs, - reveal: self.reveal, - reveal_target: self.reveal_target, - hide: self.hide, - shell: self.shell.clone(), - show_summary: self.show_summary, - show_command: self.show_command, - show_rerun: true, - }, - }) - } -} - -const MAX_DISPLAY_VARIABLE_LENGTH: usize = 15; - -fn truncate_variables(task_variables: &HashMap) -> HashMap { - task_variables - .iter() - .map(|(key, value)| { - ( - key.clone(), - truncate_and_remove_front(value, MAX_DISPLAY_VARIABLE_LENGTH), - ) - }) - .collect() -} - -fn to_hex_hash(object: impl Serialize) -> anyhow::Result { - let json = serde_json_lenient::to_string(&object).context("serializing the object")?; - let mut hasher = Sha256::new(); - hasher.update(json.as_bytes()); - Ok(hex::encode(hasher.finalize())) -} - -pub fn substitute_variables_in_str(template_str: &str, context: &TaskContext) -> Option { - let mut variable_names = HashMap::default(); - let mut substituted_variables = HashSet::default(); - let task_variables = context - .task_variables - .0 - .iter() - .map(|(key, value)| { - let key_string = key.to_string(); - if !variable_names.contains_key(&key_string) { - variable_names.insert(key_string.clone(), key.clone()); - } - (key_string, value.as_str()) - }) - .collect::>(); - substitute_all_template_variables_in_str( - template_str, - &task_variables, - &variable_names, - &mut substituted_variables, - ) -} -fn substitute_all_template_variables_in_str>( - template_str: &str, - task_variables: &HashMap, - variable_names: &HashMap, - substituted_variables: &mut HashSet, -) -> Option { - let substituted_string = shellexpand::env_with_context(template_str, |var| { - // Colons denote a default value in case the variable is not set. We want to preserve that default, as otherwise shellexpand will substitute it for us. - let colon_position = var.find(':').unwrap_or(var.len()); - let (variable_name, default) = var.split_at(colon_position); - if let Some(name) = task_variables.get(variable_name) { - if let Some(substituted_variable) = variable_names.get(variable_name) { - substituted_variables.insert(substituted_variable.clone()); - } - // Got a task variable hit - use the variable value, ignore default - return Ok(Some(name.as_ref().to_owned())); - } else if variable_name.starts_with(ZED_VARIABLE_NAME_PREFIX) { - // Unknown ZED variable - use default if available - if !default.is_empty() { - // Strip the colon and return the default value - return Ok(Some(default[1..].to_owned())); - } else { - bail!("Unknown variable name: {variable_name}"); - } - } - // This is an unknown variable. - // We should not error out, as they may come from user environment (e.g. $PATH). That means that the variable substitution might not be perfect. - // If there's a default, we need to return the string verbatim as otherwise shellexpand will apply that default for us. - if !default.is_empty() { - return Ok(Some(format!("${{{var}}}"))); - } - // Else we can just return None and that variable will be left as is. - Ok(None) - }) - .ok()?; - Some(substituted_string.into_owned()) -} - -fn substitute_all_template_variables_in_vec( - template_strs: &[String], - task_variables: &HashMap, - variable_names: &HashMap, - substituted_variables: &mut HashSet, -) -> Option> { - let mut expanded = Vec::with_capacity(template_strs.len()); - for variable in template_strs { - let new_value = substitute_all_template_variables_in_str( - variable, - task_variables, - variable_names, - substituted_variables, - )?; - expanded.push(new_value); - } - Some(expanded) -} - -pub fn substitute_variables_in_map( - keys_and_values: &HashMap, - context: &TaskContext, -) -> Option> { - let mut variable_names = HashMap::default(); - let mut substituted_variables = HashSet::default(); - let task_variables = context - .task_variables - .0 - .iter() - .map(|(key, value)| { - let key_string = key.to_string(); - if !variable_names.contains_key(&key_string) { - variable_names.insert(key_string.clone(), key.clone()); - } - (key_string, value.as_str()) - }) - .collect::>(); - substitute_all_template_variables_in_map( - keys_and_values, - &task_variables, - &variable_names, - &mut substituted_variables, - ) -} -fn substitute_all_template_variables_in_map( - keys_and_values: &HashMap, - task_variables: &HashMap, - variable_names: &HashMap, - substituted_variables: &mut HashSet, -) -> Option> { - let mut new_map: HashMap = Default::default(); - for (key, value) in keys_and_values { - let new_value = substitute_all_template_variables_in_str( - value, - task_variables, - variable_names, - substituted_variables, - )?; - let new_key = substitute_all_template_variables_in_str( - key, - task_variables, - variable_names, - substituted_variables, - )?; - new_map.insert(new_key, new_value); - } - Some(new_map) -} - -#[cfg(test)] -mod tests { - use std::{borrow::Cow, path::Path}; - - use crate::{TaskVariables, VariableName}; - - use super::*; - - const TEST_ID_BASE: &str = "test_base"; - - #[test] - fn test_resolving_templates_with_blank_command_and_label() { - let task_with_all_properties = TaskTemplate { - label: "test_label".to_string(), - command: "test_command".to_string(), - args: vec!["test_arg".to_string()], - env: HashMap::from_iter([("test_env_key".to_string(), "test_env_var".to_string())]), - ..TaskTemplate::default() - }; - - for task_with_blank_property in &[ - TaskTemplate { - label: "".to_string(), - ..task_with_all_properties.clone() - }, - TaskTemplate { - command: "".to_string(), - ..task_with_all_properties.clone() - }, - TaskTemplate { - label: "".to_string(), - command: "".to_string(), - ..task_with_all_properties - }, - ] { - assert_eq!( - task_with_blank_property.resolve_task(TEST_ID_BASE, &TaskContext::default()), - None, - "should not resolve task with blank label and/or command: {task_with_blank_property:?}" - ); - } - } - - #[test] - fn test_template_cwd_resolution() { - let task_without_cwd = TaskTemplate { - cwd: None, - label: "test task".to_string(), - command: "echo 4".to_string(), - ..TaskTemplate::default() - }; - - let resolved_task = |task_template: &TaskTemplate, task_cx| { - let resolved_task = task_template - .resolve_task(TEST_ID_BASE, task_cx) - .unwrap_or_else(|| panic!("failed to resolve task {task_without_cwd:?}")); - assert_substituted_variables(&resolved_task, Vec::new()); - resolved_task.resolved - }; - - let cx = TaskContext { - cwd: None, - task_variables: TaskVariables::default(), - project_env: HashMap::default(), - }; - assert_eq!( - resolved_task(&task_without_cwd, &cx).cwd, - None, - "When neither task nor task context have cwd, it should be None" - ); - - let context_cwd = Path::new("a").join("b").join("c"); - let cx = TaskContext { - cwd: Some(context_cwd.clone()), - task_variables: TaskVariables::default(), - project_env: HashMap::default(), - }; - assert_eq!( - resolved_task(&task_without_cwd, &cx).cwd, - Some(context_cwd.clone()), - "TaskContext's cwd should be taken on resolve if task's cwd is None" - ); - - let task_cwd = Path::new("d").join("e").join("f"); - let mut task_with_cwd = task_without_cwd.clone(); - task_with_cwd.cwd = Some(task_cwd.display().to_string()); - let task_with_cwd = task_with_cwd; - - let cx = TaskContext { - cwd: None, - task_variables: TaskVariables::default(), - project_env: HashMap::default(), - }; - assert_eq!( - resolved_task(&task_with_cwd, &cx).cwd, - Some(task_cwd.clone()), - "TaskTemplate's cwd should be taken on resolve if TaskContext's cwd is None" - ); - - let cx = TaskContext { - cwd: Some(context_cwd), - task_variables: TaskVariables::default(), - project_env: HashMap::default(), - }; - assert_eq!( - resolved_task(&task_with_cwd, &cx).cwd, - Some(task_cwd), - "TaskTemplate's cwd should be taken on resolve if TaskContext's cwd is not None" - ); - } - - #[test] - fn test_template_variables_resolution() { - let custom_variable_1 = VariableName::Custom(Cow::Borrowed("custom_variable_1")); - let custom_variable_2 = VariableName::Custom(Cow::Borrowed("custom_variable_2")); - let long_value = "01".repeat(MAX_DISPLAY_VARIABLE_LENGTH * 2); - let all_variables = [ - (VariableName::Row, "1234".to_string()), - (VariableName::Column, "5678".to_string()), - (VariableName::File, "test_file".to_string()), - (VariableName::SelectedText, "test_selected_text".to_string()), - (VariableName::Symbol, long_value.clone()), - (VariableName::WorktreeRoot, "/test_root/".to_string()), - ( - custom_variable_1.clone(), - "test_custom_variable_1".to_string(), - ), - ( - custom_variable_2.clone(), - "test_custom_variable_2".to_string(), - ), - ]; - - let task_with_all_variables = TaskTemplate { - label: format!( - "test label for {} and {}", - VariableName::Row.template_value(), - VariableName::Symbol.template_value(), - ), - command: format!( - "echo {} {}", - VariableName::File.template_value(), - VariableName::Symbol.template_value(), - ), - args: vec![ - format!("arg1 {}", VariableName::SelectedText.template_value()), - format!("arg2 {}", VariableName::Column.template_value()), - format!("arg3 {}", VariableName::Symbol.template_value()), - ], - env: HashMap::from_iter([ - ("test_env_key".to_string(), "test_env_var".to_string()), - ( - "env_key_1".to_string(), - VariableName::WorktreeRoot.template_value(), - ), - ( - "env_key_2".to_string(), - format!( - "env_var_2 {} {}", - custom_variable_1.template_value(), - custom_variable_2.template_value() - ), - ), - ( - "env_key_3".to_string(), - format!("env_var_3 {}", VariableName::Symbol.template_value()), - ), - ]), - ..TaskTemplate::default() - }; - - let mut first_resolved_id = None; - for i in 0..15 { - let resolved_task = task_with_all_variables.resolve_task( - TEST_ID_BASE, - &TaskContext { - cwd: None, - task_variables: TaskVariables::from_iter(all_variables.clone()), - project_env: HashMap::default(), - }, - ).unwrap_or_else(|| panic!("Should successfully resolve task {task_with_all_variables:?} with variables {all_variables:?}")); - - match &first_resolved_id { - None => first_resolved_id = Some(resolved_task.id.clone()), - Some(first_id) => assert_eq!( - &resolved_task.id, first_id, - "Step {i}, for the same task template and context, there should be the same resolved task id" - ), - } - - assert_eq!( - resolved_task.original_task, task_with_all_variables, - "Resolved task should store its template without changes" - ); - assert_eq!( - resolved_task.resolved_label, - format!("test label for 1234 and {long_value}"), - "Resolved task label should be substituted with variables and those should not be shortened" - ); - assert_substituted_variables( - &resolved_task, - all_variables.iter().map(|(name, _)| name.clone()).collect(), - ); - - let spawn_in_terminal = &resolved_task.resolved; - assert_eq!( - spawn_in_terminal.label, - format!( - "test label for 1234 and …{}", - &long_value[long_value.len() - MAX_DISPLAY_VARIABLE_LENGTH..] - ), - "Human-readable label should have long substitutions trimmed" - ); - assert_eq!( - spawn_in_terminal.command.clone().unwrap(), - format!("echo test_file {long_value}"), - "Command should be substituted with variables and those should not be shortened" - ); - assert_eq!( - spawn_in_terminal.args, - &[ - "arg1 test_selected_text", - "arg2 5678", - "arg3 010101010101010101010101010101010101010101010101010101010101", - ], - "Args should be substituted with variables" - ); - assert_eq!( - spawn_in_terminal.command_label, - format!( - "{} arg1 test_selected_text arg2 5678 arg3 {long_value}", - spawn_in_terminal.command.clone().unwrap() - ), - "Command label args should be substituted with variables and those should not be shortened" - ); - - assert_eq!( - spawn_in_terminal - .env - .get("test_env_key") - .map(|s| s.as_str()), - Some("test_env_var") - ); - assert_eq!( - spawn_in_terminal.env.get("env_key_1").map(|s| s.as_str()), - Some("/test_root/") - ); - assert_eq!( - spawn_in_terminal.env.get("env_key_2").map(|s| s.as_str()), - Some("env_var_2 test_custom_variable_1 test_custom_variable_2") - ); - assert_eq!( - spawn_in_terminal.env.get("env_key_3"), - Some(&format!("env_var_3 {long_value}")), - "Env vars should be substituted with variables and those should not be shortened" - ); - } - - for i in 0..all_variables.len() { - let mut not_all_variables = all_variables.to_vec(); - let removed_variable = not_all_variables.remove(i); - let resolved_task_attempt = task_with_all_variables.resolve_task( - TEST_ID_BASE, - &TaskContext { - cwd: None, - task_variables: TaskVariables::from_iter(not_all_variables), - project_env: HashMap::default(), - }, - ); - assert_eq!( - resolved_task_attempt, None, - "If any of the Zed task variables is not substituted, the task should not be resolved, but got some resolution without the variable {removed_variable:?} (index {i})" - ); - } - } - - #[test] - fn test_can_resolve_free_variables() { - let task = TaskTemplate { - label: "My task".into(), - command: "echo".into(), - args: vec!["$PATH".into()], - ..TaskTemplate::default() - }; - let resolved_task = task - .resolve_task(TEST_ID_BASE, &TaskContext::default()) - .unwrap(); - assert_substituted_variables(&resolved_task, Vec::new()); - let resolved = resolved_task.resolved; - assert_eq!(resolved.label, task.label); - assert_eq!(resolved.command, Some(task.command)); - assert_eq!(resolved.args, task.args); - } - - #[test] - fn test_errors_on_missing_zed_variable() { - let task = TaskTemplate { - label: "My task".into(), - command: "echo".into(), - args: vec!["$ZED_VARIABLE".into()], - ..TaskTemplate::default() - }; - assert!( - task.resolve_task(TEST_ID_BASE, &TaskContext::default()) - .is_none() - ); - } - - #[test] - fn test_symbol_dependent_tasks() { - let task_with_all_properties = TaskTemplate { - label: "test_label".to_string(), - command: "test_command".to_string(), - args: vec!["test_arg".to_string()], - env: HashMap::from_iter([("test_env_key".to_string(), "test_env_var".to_string())]), - ..TaskTemplate::default() - }; - let cx = TaskContext { - cwd: None, - task_variables: TaskVariables::from_iter(Some(( - VariableName::Symbol, - "test_symbol".to_string(), - ))), - project_env: HashMap::default(), - }; - - for (i, symbol_dependent_task) in [ - TaskTemplate { - label: format!("test_label_{}", VariableName::Symbol.template_value()), - ..task_with_all_properties.clone() - }, - TaskTemplate { - command: format!("test_command_{}", VariableName::Symbol.template_value()), - ..task_with_all_properties.clone() - }, - TaskTemplate { - args: vec![format!( - "test_arg_{}", - VariableName::Symbol.template_value() - )], - ..task_with_all_properties.clone() - }, - TaskTemplate { - env: HashMap::from_iter([( - "test_env_key".to_string(), - format!("test_env_var_{}", VariableName::Symbol.template_value()), - )]), - ..task_with_all_properties - }, - ] - .into_iter() - .enumerate() - { - let resolved = symbol_dependent_task - .resolve_task(TEST_ID_BASE, &cx) - .unwrap_or_else(|| panic!("Failed to resolve task {symbol_dependent_task:?}")); - assert_eq!( - resolved.substituted_variables, - HashSet::from_iter(Some(VariableName::Symbol)), - "(index {i}) Expected the task to depend on symbol task variable: {resolved:?}" - ) - } - } - - #[track_caller] - fn assert_substituted_variables(resolved_task: &ResolvedTask, mut expected: Vec) { - let mut resolved_variables = resolved_task - .substituted_variables - .iter() - .cloned() - .collect::>(); - resolved_variables.sort_by_key(|var| var.to_string()); - expected.sort_by_key(|var| var.to_string()); - assert_eq!(resolved_variables, expected) - } - - #[test] - fn substitute_funky_labels() { - let faulty_go_test = TaskTemplate { - label: format!( - "go test {}/{}", - VariableName::Symbol.template_value(), - VariableName::Symbol.template_value(), - ), - command: "go".into(), - args: vec![format!( - "^{}$/^{}$", - VariableName::Symbol.template_value(), - VariableName::Symbol.template_value() - )], - ..TaskTemplate::default() - }; - let mut context = TaskContext::default(); - context - .task_variables - .insert(VariableName::Symbol, "my-symbol".to_string()); - assert!(faulty_go_test.resolve_task("base", &context).is_some()); - } - - #[test] - fn test_project_env() { - let all_variables = [ - (VariableName::Row, "1234".to_string()), - (VariableName::Column, "5678".to_string()), - (VariableName::File, "test_file".to_string()), - (VariableName::Symbol, "my symbol".to_string()), - ]; - - let template = TaskTemplate { - label: "my task".to_string(), - command: format!( - "echo {} {}", - VariableName::File.template_value(), - VariableName::Symbol.template_value(), - ), - args: vec![], - env: HashMap::from_iter([ - ( - "TASK_ENV_VAR1".to_string(), - "TASK_ENV_VAR1_VALUE".to_string(), - ), - ( - "TASK_ENV_VAR2".to_string(), - format!( - "env_var_2 {} {}", - VariableName::Row.template_value(), - VariableName::Column.template_value() - ), - ), - ( - "PROJECT_ENV_WILL_BE_OVERWRITTEN".to_string(), - "overwritten".to_string(), - ), - ]), - ..TaskTemplate::default() - }; - - let project_env = HashMap::from_iter([ - ( - "PROJECT_ENV_VAR1".to_string(), - "PROJECT_ENV_VAR1_VALUE".to_string(), - ), - ( - "PROJECT_ENV_WILL_BE_OVERWRITTEN".to_string(), - "PROJECT_ENV_WILL_BE_OVERWRITTEN_VALUE".to_string(), - ), - ]); - - let context = TaskContext { - cwd: None, - task_variables: TaskVariables::from_iter(all_variables), - project_env, - }; - - let resolved = template - .resolve_task(TEST_ID_BASE, &context) - .unwrap() - .resolved; - - assert_eq!(resolved.env["TASK_ENV_VAR1"], "TASK_ENV_VAR1_VALUE"); - assert_eq!(resolved.env["TASK_ENV_VAR2"], "env_var_2 1234 5678"); - assert_eq!(resolved.env["PROJECT_ENV_VAR1"], "PROJECT_ENV_VAR1_VALUE"); - assert_eq!( - resolved.env["PROJECT_ENV_WILL_BE_OVERWRITTEN"], - "overwritten" - ); - } - - #[test] - fn test_variable_default_values() { - let task_with_defaults = TaskTemplate { - label: "test with defaults".to_string(), - command: format!( - "echo ${{{}}}", - VariableName::File.to_string() + ":fallback.txt" - ), - args: vec![ - "${ZED_MISSING_VAR:default_value}".to_string(), - format!("${{{}}}", VariableName::Row.to_string() + ":42"), - ], - ..TaskTemplate::default() - }; - - // Test 1: When ZED_FILE exists, should use actual value and ignore default - let context_with_file = TaskContext { - cwd: None, - task_variables: TaskVariables::from_iter(vec![ - (VariableName::File, "actual_file.rs".to_string()), - (VariableName::Row, "123".to_string()), - ]), - project_env: HashMap::default(), - }; - - let resolved = task_with_defaults - .resolve_task(TEST_ID_BASE, &context_with_file) - .expect("Should resolve task with existing variables"); - - assert_eq!( - resolved.resolved.command.unwrap(), - "echo actual_file.rs", - "Should use actual ZED_FILE value, not default" - ); - assert_eq!( - resolved.resolved.args, - vec!["default_value", "123"], - "Should use default for missing var, actual value for existing var" - ); - - // Test 2: When ZED_FILE doesn't exist, should use default value - let context_without_file = TaskContext { - cwd: None, - task_variables: TaskVariables::from_iter(vec![(VariableName::Row, "456".to_string())]), - project_env: HashMap::default(), - }; - - let resolved = task_with_defaults - .resolve_task(TEST_ID_BASE, &context_without_file) - .expect("Should resolve task using default values"); - - assert_eq!( - resolved.resolved.command.unwrap(), - "echo fallback.txt", - "Should use default value when ZED_FILE is missing" - ); - assert_eq!( - resolved.resolved.args, - vec!["default_value", "456"], - "Should use defaults for missing vars" - ); - - // Test 3: Missing ZED variable without default should fail - let task_no_default = TaskTemplate { - label: "test no default".to_string(), - command: "${ZED_MISSING_NO_DEFAULT}".to_string(), - ..TaskTemplate::default() - }; - - assert!( - task_no_default - .resolve_task(TEST_ID_BASE, &TaskContext::default()) - .is_none(), - "Should fail when ZED variable has no default and doesn't exist" - ); - } -} diff --git a/crates/task/src/vscode_debug_format.rs b/crates/task/src/vscode_debug_format.rs deleted file mode 100644 index bef64c8d40..0000000000 --- a/crates/task/src/vscode_debug_format.rs +++ /dev/null @@ -1,194 +0,0 @@ -use collections::HashMap; -use serde::Deserialize; -use util::ResultExt as _; - -use crate::{ - DebugScenario, DebugTaskFile, EnvVariableReplacer, TcpArgumentsTemplate, VariableName, -}; - -// TODO support preLaunchTask linkage with other tasks -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -struct VsCodeDebugTaskDefinition { - r#type: String, - name: String, - #[serde(default)] - port: Option, - #[serde(flatten)] - other_attributes: serde_json::Value, -} - -impl VsCodeDebugTaskDefinition { - fn try_to_zed(mut self, replacer: &EnvVariableReplacer) -> anyhow::Result { - let label = replacer.replace(&self.name); - let mut config = replacer.replace_value(self.other_attributes); - let adapter = task_type_to_adapter_name(&self.r#type); - if let Some(config) = config.as_object_mut() - && adapter == "JavaScript" - { - config.insert("type".to_owned(), self.r#type.clone().into()); - if let Some(port) = self.port.take() { - config.insert("port".to_owned(), port.into()); - } - } - let definition = DebugScenario { - label: label.into(), - build: None, - adapter: adapter.into(), - tcp_connection: self.port.map(|port| TcpArgumentsTemplate { - port: Some(port), - host: None, - timeout: None, - }), - config, - }; - Ok(definition) - } -} - -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -pub struct VsCodeDebugTaskFile { - #[serde(default)] - version: Option, - configurations: Vec, -} - -impl TryFrom for DebugTaskFile { - type Error = anyhow::Error; - - fn try_from(file: VsCodeDebugTaskFile) -> Result { - let replacer = EnvVariableReplacer::new(HashMap::from_iter([ - ( - "workspaceFolder".to_owned(), - VariableName::WorktreeRoot.to_string(), - ), - ( - "relativeFile".to_owned(), - VariableName::RelativeFile.to_string(), - ), - ("file".to_owned(), VariableName::File.to_string()), - ])) - .with_commands([( - "pickMyProcess".to_owned(), - VariableName::PickProcessId.to_string(), - )]); - let templates = file - .configurations - .into_iter() - .filter_map(|config| config.try_to_zed(&replacer).log_err()) - .collect::>(); - Ok(DebugTaskFile(templates)) - } -} - -fn task_type_to_adapter_name(task_type: &str) -> String { - match task_type { - "pwa-node" | "node" | "node-terminal" | "chrome" | "pwa-chrome" | "edge" | "pwa-edge" - | "msedge" | "pwa-msedge" => "JavaScript", - "go" => "Delve", - "php" => "Xdebug", - "cppdbg" | "lldb" => "CodeLLDB", - "debugpy" => "Debugpy", - "rdbg" => "rdbg", - _ => task_type, - } - .to_owned() -} - -#[cfg(test)] -mod tests { - use serde_json::json; - - use crate::{DebugScenario, DebugTaskFile, VariableName}; - - use super::VsCodeDebugTaskFile; - - #[test] - fn test_parsing_vscode_launch_json() { - let raw = r#" - { - "version": "0.2.0", - "configurations": [ - { - "name": "Debug my JS app", - "request": "launch", - "type": "node", - "program": "${workspaceFolder}/xyz.js", - "showDevDebugOutput": false, - "stopOnEntry": true, - "args": ["--foo", "${workspaceFolder}/thing"], - "cwd": "${workspaceFolder}/${env:FOO}/sub", - "env": { - "X": "Y" - }, - "port": 17 - }, - ] - } - "#; - let parsed: VsCodeDebugTaskFile = - serde_json_lenient::from_str(raw).expect("deserializing launch.json"); - let zed = DebugTaskFile::try_from(parsed).expect("converting to Zed debug templates"); - pretty_assertions::assert_eq!( - zed, - DebugTaskFile(vec![DebugScenario { - label: "Debug my JS app".into(), - adapter: "JavaScript".into(), - config: json!({ - "request": "launch", - "program": "${ZED_WORKTREE_ROOT}/xyz.js", - "showDevDebugOutput": false, - "stopOnEntry": true, - "args": [ - "--foo", - "${ZED_WORKTREE_ROOT}/thing", - ], - "cwd": "${ZED_WORKTREE_ROOT}/${FOO}/sub", - "env": { - "X": "Y", - }, - "type": "node", - "port": 17, - }), - tcp_connection: None, - build: None - }]) - ); - } - - #[test] - fn test_command_pickmyprocess_replacement() { - let raw = r#" - { - "version": "0.2.0", - "configurations": [ - { - "name": "Attach to Process", - "request": "attach", - "type": "cppdbg", - "processId": "${command:pickMyProcess}" - } - ] - } - "#; - let parsed: VsCodeDebugTaskFile = - serde_json_lenient::from_str(raw).expect("deserializing launch.json"); - let zed = DebugTaskFile::try_from(parsed).expect("converting to Zed debug templates"); - - let expected_placeholder = format!("${{{}}}", VariableName::PickProcessId); - pretty_assertions::assert_eq!( - zed, - DebugTaskFile(vec![DebugScenario { - label: "Attach to Process".into(), - adapter: "CodeLLDB".into(), - config: json!({ - "request": "attach", - "processId": expected_placeholder, - }), - tcp_connection: None, - build: None - }]) - ); - } -} diff --git a/crates/task/src/vscode_format.rs b/crates/task/src/vscode_format.rs deleted file mode 100644 index 9078a73fbb..0000000000 --- a/crates/task/src/vscode_format.rs +++ /dev/null @@ -1,361 +0,0 @@ -use anyhow::bail; -use collections::HashMap; -use serde::Deserialize; -use util::ResultExt; - -use crate::{EnvVariableReplacer, TaskTemplate, TaskTemplates, VariableName}; - -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -struct TaskOptions { - cwd: Option, - #[serde(default)] - env: HashMap, -} - -#[derive(Clone, Debug, Deserialize, PartialEq)] -#[serde(rename_all = "camelCase")] -struct VsCodeTaskDefinition { - label: String, - #[serde(flatten)] - command: Option, - #[serde(flatten)] - other_attributes: HashMap, - options: Option, -} - -#[derive(Clone, Deserialize, PartialEq, Debug)] -#[serde(tag = "type")] -#[serde(rename_all = "camelCase")] -enum Command { - Npm { - script: String, - }, - Shell { - command: String, - #[serde(default)] - args: Vec, - }, - Gulp { - task: String, - }, -} - -impl VsCodeTaskDefinition { - fn into_zed_format( - self, - replacer: &EnvVariableReplacer, - ) -> anyhow::Result> { - if self.other_attributes.contains_key("dependsOn") { - log::warn!( - "Skipping deserializing of a task `{}` with the unsupported `dependsOn` key", - self.label - ); - return Ok(None); - } - // `type` might not be set in e.g. tasks that use `dependsOn`; we still want to deserialize the whole object though (hence command is an Option), - // as that way we can provide more specific description of why deserialization failed. - // E.g. if the command is missing due to `dependsOn` presence, we can check other_attributes first before doing this (and provide nice error message) - // catch-all if on value.command presence. - let Some(command) = self.command else { - bail!("Missing `type` field in task"); - }; - - let (command, args) = match command { - Command::Npm { script } => ("npm".to_owned(), vec!["run".to_string(), script]), - Command::Shell { command, args } => (command, args), - Command::Gulp { task } => ("gulp".to_owned(), vec![task]), - }; - // Per VSC docs, only `command`, `args` and `options` support variable substitution. - let command = replacer.replace(&command); - let args = args.into_iter().map(|arg| replacer.replace(&arg)).collect(); - let mut template = TaskTemplate { - label: self.label, - command, - args, - ..TaskTemplate::default() - }; - if let Some(options) = self.options { - template.cwd = options.cwd.map(|cwd| replacer.replace(&cwd)); - template.env = options.env; - } - Ok(Some(template)) - } -} - -/// [`VsCodeTaskFile`] is a superset of Code's task definition format. -#[derive(Debug, Deserialize, PartialEq)] -pub struct VsCodeTaskFile { - tasks: Vec, -} - -impl TryFrom for TaskTemplates { - type Error = anyhow::Error; - - fn try_from(value: VsCodeTaskFile) -> Result { - let replacer = EnvVariableReplacer::new(HashMap::from_iter([ - ( - "workspaceFolder".to_owned(), - VariableName::WorktreeRoot.to_string(), - ), - ("file".to_owned(), VariableName::File.to_string()), - ("lineNumber".to_owned(), VariableName::Row.to_string()), - ( - "selectedText".to_owned(), - VariableName::SelectedText.to_string(), - ), - ])); - let templates = value - .tasks - .into_iter() - .filter_map(|vscode_definition| { - vscode_definition - .into_zed_format(&replacer) - .log_err() - .flatten() - }) - .collect(); - Ok(Self(templates)) - } -} - -#[cfg(test)] -mod tests { - use std::collections::HashMap; - - use crate::{ - TaskTemplate, TaskTemplates, VsCodeTaskFile, - vscode_format::{Command, VsCodeTaskDefinition}, - }; - - use super::EnvVariableReplacer; - - fn compare_without_other_attributes(lhs: VsCodeTaskDefinition, rhs: VsCodeTaskDefinition) { - assert_eq!( - VsCodeTaskDefinition { - other_attributes: Default::default(), - ..lhs - }, - VsCodeTaskDefinition { - other_attributes: Default::default(), - ..rhs - }, - ); - } - - #[test] - fn test_variable_substitution() { - let replacer = EnvVariableReplacer::new(Default::default()); - assert_eq!(replacer.replace("Food"), "Food"); - // Unknown variables are left in tact. - assert_eq!( - replacer.replace("$PATH is an environment variable"), - "$PATH is an environment variable" - ); - assert_eq!(replacer.replace("${PATH}"), "${PATH}"); - assert_eq!(replacer.replace("${PATH:food}"), "${PATH:food}"); - // And now, the actual replacing - let replacer = EnvVariableReplacer::new(HashMap::from_iter([( - "PATH".to_owned(), - "ZED_PATH".to_owned(), - )])); - assert_eq!(replacer.replace("Food"), "Food"); - assert_eq!( - replacer.replace("$PATH is an environment variable"), - "${ZED_PATH} is an environment variable" - ); - assert_eq!(replacer.replace("${PATH}"), "${ZED_PATH}"); - assert_eq!(replacer.replace("${PATH:food}"), "${ZED_PATH:food}"); - } - - #[test] - fn can_deserialize_ts_tasks() { - const TYPESCRIPT_TASKS: &str = include_str!("../test_data/typescript.json"); - let vscode_definitions: VsCodeTaskFile = - serde_json_lenient::from_str(TYPESCRIPT_TASKS).unwrap(); - - let expected = vec![ - VsCodeTaskDefinition { - label: "gulp: tests".to_string(), - command: Some(Command::Npm { - script: "build:tests:notypecheck".to_string(), - }), - other_attributes: Default::default(), - options: None, - }, - VsCodeTaskDefinition { - label: "tsc: watch ./src".to_string(), - command: Some(Command::Shell { - command: "node".to_string(), - args: vec![ - "${workspaceFolder}/node_modules/typescript/lib/tsc.js".to_string(), - "--build".to_string(), - "${workspaceFolder}/src".to_string(), - "--watch".to_string(), - ], - }), - other_attributes: Default::default(), - options: None, - }, - VsCodeTaskDefinition { - label: "npm: build:compiler".to_string(), - command: Some(Command::Npm { - script: "build:compiler".to_string(), - }), - other_attributes: Default::default(), - options: None, - }, - VsCodeTaskDefinition { - label: "npm: build:tests".to_string(), - command: Some(Command::Npm { - script: "build:tests:notypecheck".to_string(), - }), - other_attributes: Default::default(), - options: None, - }, - ]; - - assert_eq!(vscode_definitions.tasks.len(), expected.len()); - vscode_definitions - .tasks - .iter() - .zip(expected) - .for_each(|(lhs, rhs)| compare_without_other_attributes(lhs.clone(), rhs)); - - let expected = vec![ - TaskTemplate { - label: "gulp: tests".to_string(), - command: "npm".to_string(), - args: vec!["run".to_string(), "build:tests:notypecheck".to_string()], - ..Default::default() - }, - TaskTemplate { - label: "tsc: watch ./src".to_string(), - command: "node".to_string(), - args: vec![ - "${ZED_WORKTREE_ROOT}/node_modules/typescript/lib/tsc.js".to_string(), - "--build".to_string(), - "${ZED_WORKTREE_ROOT}/src".to_string(), - "--watch".to_string(), - ], - ..Default::default() - }, - TaskTemplate { - label: "npm: build:compiler".to_string(), - command: "npm".to_string(), - args: vec!["run".to_string(), "build:compiler".to_string()], - ..Default::default() - }, - TaskTemplate { - label: "npm: build:tests".to_string(), - command: "npm".to_string(), - args: vec!["run".to_string(), "build:tests:notypecheck".to_string()], - ..Default::default() - }, - ]; - - let tasks: TaskTemplates = vscode_definitions.try_into().unwrap(); - assert_eq!(tasks.0, expected); - } - - #[test] - fn can_deserialize_rust_analyzer_tasks() { - const RUST_ANALYZER_TASKS: &str = include_str!("../test_data/rust-analyzer.json"); - let vscode_definitions: VsCodeTaskFile = - serde_json_lenient::from_str(RUST_ANALYZER_TASKS).unwrap(); - let expected = vec![ - VsCodeTaskDefinition { - label: "Build Extension in Background".to_string(), - command: Some(Command::Npm { - script: "watch".to_string(), - }), - options: None, - other_attributes: Default::default(), - }, - VsCodeTaskDefinition { - label: "Build Extension".to_string(), - command: Some(Command::Npm { - script: "build".to_string(), - }), - options: None, - other_attributes: Default::default(), - }, - VsCodeTaskDefinition { - label: "Build Server".to_string(), - command: Some(Command::Shell { - command: "cargo build --package rust-analyzer".to_string(), - args: Default::default(), - }), - options: None, - other_attributes: Default::default(), - }, - VsCodeTaskDefinition { - label: "Build Server (Release)".to_string(), - command: Some(Command::Shell { - command: "cargo build --release --package rust-analyzer".to_string(), - args: Default::default(), - }), - options: None, - other_attributes: Default::default(), - }, - VsCodeTaskDefinition { - label: "Pretest".to_string(), - command: Some(Command::Npm { - script: "pretest".to_string(), - }), - options: None, - other_attributes: Default::default(), - }, - VsCodeTaskDefinition { - label: "Build Server and Extension".to_string(), - command: None, - options: None, - other_attributes: Default::default(), - }, - VsCodeTaskDefinition { - label: "Build Server (Release) and Extension".to_string(), - command: None, - options: None, - other_attributes: Default::default(), - }, - ]; - assert_eq!(vscode_definitions.tasks.len(), expected.len()); - vscode_definitions - .tasks - .iter() - .zip(expected) - .for_each(|(lhs, rhs)| compare_without_other_attributes(lhs.clone(), rhs)); - let expected = vec![ - TaskTemplate { - label: "Build Extension in Background".to_string(), - command: "npm".to_string(), - args: vec!["run".to_string(), "watch".to_string()], - ..Default::default() - }, - TaskTemplate { - label: "Build Extension".to_string(), - command: "npm".to_string(), - args: vec!["run".to_string(), "build".to_string()], - ..Default::default() - }, - TaskTemplate { - label: "Build Server".to_string(), - command: "cargo build --package rust-analyzer".to_string(), - ..Default::default() - }, - TaskTemplate { - label: "Build Server (Release)".to_string(), - command: "cargo build --release --package rust-analyzer".to_string(), - ..Default::default() - }, - TaskTemplate { - label: "Pretest".to_string(), - command: "npm".to_string(), - args: vec!["run".to_string(), "pretest".to_string()], - ..Default::default() - }, - ]; - let tasks: TaskTemplates = vscode_definitions.try_into().unwrap(); - assert_eq!(tasks.0, expected); - } -} diff --git a/crates/task/test_data/rust-analyzer.json b/crates/task/test_data/rust-analyzer.json deleted file mode 100644 index 0ea585c4b8..0000000000 --- a/crates/task/test_data/rust-analyzer.json +++ /dev/null @@ -1,67 +0,0 @@ -// See https://go.microsoft.com/fwlink/?LinkId=733558 -// for the documentation about the tasks.json format -{ - "version": "2.0.0", - "tasks": [ - { - "label": "Build Extension in Background", - "group": "build", - "type": "npm", - "script": "watch", - "path": "editors/code/", - "problemMatcher": { - "base": "$tsc-watch", - "fileLocation": ["relative", "${workspaceFolder}/editors/code/"] - }, - "isBackground": true - }, - { - "label": "Build Extension", - "group": "build", - "type": "npm", - "script": "build", - "path": "editors/code/", - "problemMatcher": { - "base": "$tsc", - "fileLocation": ["relative", "${workspaceFolder}/editors/code/"] - } - }, - { - "label": "Build Server", - "group": "build", - "type": "shell", - "command": "cargo build --package rust-analyzer", - "problemMatcher": "$rustc" - }, - { - "label": "Build Server (Release)", - "group": "build", - "type": "shell", - "command": "cargo build --release --package rust-analyzer", - "problemMatcher": "$rustc" - }, - { - "label": "Pretest", - "group": "build", - "isBackground": false, - "type": "npm", - "script": "pretest", - "path": "editors/code/", - "problemMatcher": { - "base": "$tsc", - "fileLocation": ["relative", "${workspaceFolder}/editors/code/"] - } - }, - - { - "label": "Build Server and Extension", - "dependsOn": ["Build Server", "Build Extension"], - "problemMatcher": "$rustc" - }, - { - "label": "Build Server (Release) and Extension", - "dependsOn": ["Build Server (Release)", "Build Extension"], - "problemMatcher": "$rustc" - } - ] -} diff --git a/crates/task/test_data/typescript.json b/crates/task/test_data/typescript.json deleted file mode 100644 index 91d2343682..0000000000 --- a/crates/task/test_data/typescript.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - // See https://go.microsoft.com/fwlink/?LinkId=733558 - // for the documentation about the tasks.json format - "version": "2.0.0", - "tasks": [ - { - // Kept for backwards compat for old launch.json files so it's - // less annoying if moving up to the new build or going back to - // the old build. - // - // This is first because the actual "npm: build:tests" task - // below has the same script value, and VS Code ignores labels - // and deduplicates them. - // https://github.com/microsoft/vscode/issues/93001 - "label": "gulp: tests", - "type": "npm", - "script": "build:tests:notypecheck", - "group": "build", - "hide": true, - "problemMatcher": ["$tsc"] - }, - { - "label": "tsc: watch ./src", - "type": "shell", - "command": "node", - "args": [ - "${workspaceFolder}/node_modules/typescript/lib/tsc.js", - "--build", - "${workspaceFolder}/src", - "--watch" - ], - "group": "build", - "isBackground": true, - "problemMatcher": ["$tsc-watch"] - }, - { - "label": "npm: build:compiler", - "type": "npm", - "script": "build:compiler", - "group": "build", - "problemMatcher": ["$tsc"] - }, - { - "label": "npm: build:tests", - "type": "npm", - "script": "build:tests:notypecheck", - "group": "build", - "problemMatcher": ["$tsc"] - } - ] -} diff --git a/crates/tasks_ui/Cargo.toml b/crates/tasks_ui/Cargo.toml deleted file mode 100644 index 2f75a0b57c..0000000000 --- a/crates/tasks_ui/Cargo.toml +++ /dev/null @@ -1,41 +0,0 @@ -[package] -name = "tasks_ui" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/tasks_ui.rs" - -[dependencies] -anyhow.workspace = true -collections.workspace = true -editor.workspace = true -file_icons.workspace = true -fuzzy.workspace = true -itertools.workspace = true -gpui.workspace = true -menu.workspace = true -picker.workspace = true -project.workspace = true -task.workspace = true -serde.workspace = true -ui.workspace = true -util.workspace = true -workspace.workspace = true -language.workspace = true -zed_actions.workspace = true - -[dev-dependencies] -editor = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -language = { workspace = true, features = ["test-support"] } -project = { workspace = true, features = ["test-support"] } -serde_json.workspace = true -tree-sitter-rust.workspace = true -tree-sitter-typescript.workspace = true -workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/tasks_ui/LICENSE-GPL b/crates/tasks_ui/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/tasks_ui/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/tasks_ui/src/modal.rs b/crates/tasks_ui/src/modal.rs deleted file mode 100644 index 644f82285b..0000000000 --- a/crates/tasks_ui/src/modal.rs +++ /dev/null @@ -1,1312 +0,0 @@ -use std::sync::Arc; - -use crate::TaskContexts; -use editor::Editor; -use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{ - Action, AnyElement, App, AppContext as _, Context, DismissEvent, Entity, EventEmitter, - Focusable, InteractiveElement, ParentElement, Render, Styled, Subscription, Task, WeakEntity, - Window, rems, -}; -use itertools::Itertools; -use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch}; -use project::{TaskSourceKind, task_store::TaskStore}; -use task::{DebugScenario, ResolvedTask, RevealTarget, TaskContext, TaskTemplate}; -use ui::{ - ActiveTheme, Clickable, FluentBuilder as _, IconButtonShape, IconWithIndicator, Indicator, - IntoElement, KeyBinding, ListItem, ListItemSpacing, RenderOnce, Toggleable, Tooltip, div, - prelude::*, -}; - -use util::{ResultExt, truncate_and_trailoff}; -use workspace::{ModalView, Workspace}; -pub use zed_actions::{Rerun, Spawn}; - -/// A modal used to spawn new tasks. -pub struct TasksModalDelegate { - task_store: Entity, - candidates: Option>, - task_overrides: Option, - last_used_candidate_index: Option, - divider_index: Option, - matches: Vec, - selected_index: usize, - workspace: WeakEntity, - prompt: String, - task_contexts: Arc, - placeholder_text: Arc, -} - -/// Task template amendments to do before resolving the context. -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct TaskOverrides { - /// See [`RevealTarget`]. - pub reveal_target: Option, -} - -impl TasksModalDelegate { - fn new( - task_store: Entity, - task_contexts: Arc, - task_overrides: Option, - workspace: WeakEntity, - ) -> Self { - let placeholder_text = if let Some(TaskOverrides { - reveal_target: Some(RevealTarget::Center), - }) = &task_overrides - { - Arc::from("Find a task, or run a command in the central pane") - } else { - Arc::from("Find a task, or run a command") - }; - Self { - task_store, - workspace, - candidates: None, - matches: Vec::new(), - last_used_candidate_index: None, - divider_index: None, - selected_index: 0, - prompt: String::default(), - task_contexts, - task_overrides, - placeholder_text, - } - } - - fn spawn_oneshot(&mut self) -> Option<(TaskSourceKind, ResolvedTask)> { - if self.prompt.trim().is_empty() { - return None; - } - - let default_context = TaskContext::default(); - let active_context = self - .task_contexts - .active_context() - .unwrap_or(&default_context); - let source_kind = TaskSourceKind::UserInput; - let id_base = source_kind.to_id_base(); - let mut new_oneshot = TaskTemplate { - label: self.prompt.clone(), - command: self.prompt.clone(), - ..TaskTemplate::default() - }; - if let Some(TaskOverrides { - reveal_target: Some(reveal_target), - }) = &self.task_overrides - { - new_oneshot.reveal_target = *reveal_target; - } - Some(( - source_kind, - new_oneshot.resolve_task(&id_base, active_context)?, - )) - } - - fn delete_previously_used(&mut self, ix: usize, cx: &mut App) { - let Some(candidates) = self.candidates.as_mut() else { - return; - }; - let Some(task) = candidates.get(ix).map(|(_, task)| task.clone()) else { - return; - }; - // We remove this candidate manually instead of .taking() the candidates, as we already know the index; - // it doesn't make sense to requery the inventory for new candidates, as that's potentially costly and more often than not it should just return back - // the original list without a removed entry. - candidates.remove(ix); - if let Some(inventory) = self.task_store.read(cx).task_inventory().cloned() { - inventory.update(cx, |inventory, _| { - inventory.delete_previously_used(&task.id); - }) - }; - } -} - -pub struct TasksModal { - pub picker: Entity>, - _subscription: [Subscription; 2], -} - -impl TasksModal { - pub fn new( - task_store: Entity, - task_contexts: Arc, - task_overrides: Option, - is_modal: bool, - workspace: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let picker = cx.new(|cx| { - Picker::uniform_list( - TasksModalDelegate::new(task_store, task_contexts, task_overrides, workspace), - window, - cx, - ) - .modal(is_modal) - }); - let _subscription = [ - cx.subscribe(&picker, |_, _, _: &DismissEvent, cx| { - cx.emit(DismissEvent); - }), - cx.subscribe(&picker, |_, _, event: &ShowAttachModal, cx| { - cx.emit(ShowAttachModal { - debug_config: event.debug_config.clone(), - }); - }), - ]; - Self { - picker, - _subscription, - } - } - - pub fn tasks_loaded( - &mut self, - task_contexts: Arc, - lsp_tasks: Vec<(TaskSourceKind, task::ResolvedTask)>, - used_tasks: Vec<(TaskSourceKind, task::ResolvedTask)>, - current_resolved_tasks: Vec<(TaskSourceKind, task::ResolvedTask)>, - add_current_language_tasks: bool, - window: &mut Window, - cx: &mut Context, - ) { - let last_used_candidate_index = if used_tasks.is_empty() { - None - } else { - Some(used_tasks.len() - 1) - }; - let mut new_candidates = used_tasks; - new_candidates.extend(lsp_tasks); - let hide_vscode = current_resolved_tasks.iter().any(|(kind, _)| match kind { - TaskSourceKind::Worktree { - id: _, - directory_in_worktree: dir, - id_base: _, - } => dir.file_name().is_some_and(|name| name == ".zed"), - _ => false, - }); - // todo(debugger): We're always adding lsp tasks here even if prefer_lsp is false - // We should move the filter to new_candidates instead of on current - // and add a test for this - new_candidates.extend(current_resolved_tasks.into_iter().filter(|(task_kind, _)| { - match task_kind { - TaskSourceKind::Worktree { - directory_in_worktree: dir, - .. - } => !(hide_vscode && dir.file_name().is_some_and(|name| name == ".vscode")), - TaskSourceKind::Language { .. } => add_current_language_tasks, - _ => true, - } - })); - self.picker.update(cx, |picker, cx| { - picker.delegate.task_contexts = task_contexts; - picker.delegate.last_used_candidate_index = last_used_candidate_index; - picker.delegate.candidates = Some(new_candidates); - picker.refresh(window, cx); - cx.notify(); - }) - } -} - -impl Render for TasksModal { - fn render( - &mut self, - _window: &mut Window, - _: &mut Context, - ) -> impl gpui::prelude::IntoElement { - v_flex() - .key_context("TasksModal") - .w(rems(34.)) - .child(self.picker.clone()) - } -} - -pub struct ShowAttachModal { - pub debug_config: DebugScenario, -} - -impl EventEmitter for TasksModal {} -impl EventEmitter for TasksModal {} -impl EventEmitter for Picker {} - -impl Focusable for TasksModal { - fn focus_handle(&self, cx: &gpui::App) -> gpui::FocusHandle { - self.picker.read(cx).focus_handle(cx) - } -} - -impl ModalView for TasksModal {} - -const MAX_TAGS_LINE_LEN: usize = 30; - -impl PickerDelegate for TasksModalDelegate { - type ListItem = ListItem; - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index( - &mut self, - ix: usize, - _window: &mut Window, - _cx: &mut Context>, - ) { - self.selected_index = ix; - } - - fn placeholder_text(&self, _window: &mut Window, _: &mut App) -> Arc { - self.placeholder_text.clone() - } - - fn update_matches( - &mut self, - query: String, - window: &mut Window, - cx: &mut Context>, - ) -> Task<()> { - let candidates = match &self.candidates { - Some(candidates) => Task::ready(string_match_candidates(candidates)), - None => { - if let Some(task_inventory) = self.task_store.read(cx).task_inventory().cloned() { - let task_list = task_inventory.update(cx, |this, cx| { - this.used_and_current_resolved_tasks(self.task_contexts.clone(), cx) - }); - let workspace = self.workspace.clone(); - let lsp_task_sources = self.task_contexts.lsp_task_sources.clone(); - let task_position = self.task_contexts.latest_selection; - cx.spawn(async move |picker, cx| { - let (used, current) = task_list.await; - let Ok((lsp_tasks, prefer_lsp)) = workspace.update(cx, |workspace, cx| { - let lsp_tasks = editor::lsp_tasks( - workspace.project().clone(), - &lsp_task_sources, - task_position, - cx, - ); - let prefer_lsp = workspace - .active_item(cx) - .and_then(|item| item.downcast::()) - .map(|editor| { - editor - .read(cx) - .buffer() - .read(cx) - .language_settings(cx) - .tasks - .prefer_lsp - }) - .unwrap_or(false); - (lsp_tasks, prefer_lsp) - }) else { - return Vec::new(); - }; - - let lsp_tasks = lsp_tasks.await; - picker - .update(cx, |picker, _| { - picker.delegate.last_used_candidate_index = if used.is_empty() { - None - } else { - Some(used.len() - 1) - }; - - let mut new_candidates = used; - let add_current_language_tasks = - !prefer_lsp || lsp_tasks.is_empty(); - new_candidates.extend(lsp_tasks.into_iter().flat_map( - |(kind, tasks_with_locations)| { - tasks_with_locations - .into_iter() - .sorted_by_key(|(location, task)| { - (location.is_none(), task.resolved_label.clone()) - }) - .map(move |(_, task)| (kind.clone(), task)) - }, - )); - // todo(debugger): We're always adding lsp tasks here even if prefer_lsp is false - // We should move the filter to new_candidates instead of on current - // and add a test for this - new_candidates.extend(current.into_iter().filter( - |(task_kind, _)| { - add_current_language_tasks - || !matches!(task_kind, TaskSourceKind::Language { .. }) - }, - )); - let match_candidates = string_match_candidates(&new_candidates); - let _ = picker.delegate.candidates.insert(new_candidates); - match_candidates - }) - .ok() - .unwrap_or_default() - }) - } else { - Task::ready(Vec::new()) - } - } - }; - - cx.spawn_in(window, async move |picker, cx| { - let candidates = candidates.await; - let matches = fuzzy::match_strings( - &candidates, - &query, - true, - true, - 1000, - &Default::default(), - cx.background_executor().clone(), - ) - .await; - picker - .update(cx, |picker, _| { - let delegate = &mut picker.delegate; - delegate.matches = matches; - if let Some(index) = delegate.last_used_candidate_index { - delegate.matches.sort_by_key(|m| m.candidate_id > index); - } - - delegate.prompt = query; - delegate.divider_index = delegate.last_used_candidate_index.and_then(|index| { - let index = delegate - .matches - .partition_point(|matching_task| matching_task.candidate_id <= index); - Some(index).and_then(|index| (index != 0).then(|| index - 1)) - }); - - if delegate.matches.is_empty() { - delegate.selected_index = 0; - } else { - delegate.selected_index = - delegate.selected_index.min(delegate.matches.len() - 1); - } - }) - .log_err(); - }) - } - - fn confirm( - &mut self, - omit_history_entry: bool, - window: &mut Window, - cx: &mut Context>, - ) { - let current_match_index = self.selected_index(); - let task = self - .matches - .get(current_match_index) - .and_then(|current_match| { - let ix = current_match.candidate_id; - self.candidates - .as_ref() - .map(|candidates| candidates[ix].clone()) - }); - let Some((task_source_kind, mut task)) = task else { - return; - }; - if let Some(TaskOverrides { - reveal_target: Some(reveal_target), - }) = &self.task_overrides - { - task.resolved.reveal_target = *reveal_target; - } - - self.workspace - .update(cx, |workspace, cx| { - workspace.schedule_resolved_task( - task_source_kind, - task, - omit_history_entry, - window, - cx, - ); - }) - .ok(); - - cx.emit(DismissEvent); - } - - fn dismissed(&mut self, _window: &mut Window, cx: &mut Context>) { - cx.emit(DismissEvent); - } - - fn render_match( - &self, - ix: usize, - selected: bool, - window: &mut Window, - cx: &mut Context>, - ) -> Option { - let candidates = self.candidates.as_ref()?; - let hit = &self.matches.get(ix)?; - let (source_kind, resolved_task) = &candidates.get(hit.candidate_id)?; - let template = resolved_task.original_task(); - let display_label = resolved_task.display_label(); - - let mut tooltip_label_text = - if display_label != &template.label || source_kind == &TaskSourceKind::UserInput { - resolved_task.resolved_label.clone() - } else { - String::new() - }; - - if resolved_task.resolved.command_label != resolved_task.resolved_label { - if !tooltip_label_text.trim().is_empty() { - tooltip_label_text.push('\n'); - } - tooltip_label_text.push_str(&resolved_task.resolved.command_label); - } - - if !template.tags.is_empty() { - tooltip_label_text.push('\n'); - tooltip_label_text.push_str( - template - .tags - .iter() - .map(|tag| format!("\n#{}", tag)) - .collect::>() - .join("") - .as_str(), - ); - } - let tooltip_label = if tooltip_label_text.trim().is_empty() { - None - } else { - Some(Tooltip::simple(tooltip_label_text, cx)) - }; - - let highlighted_location = HighlightedMatch { - text: hit.string.clone(), - highlight_positions: hit.positions.clone(), - color: Color::Default, - }; - let icon = match source_kind { - TaskSourceKind::UserInput => Some(Icon::new(IconName::Terminal)), - TaskSourceKind::AbsPath { .. } => Some(Icon::new(IconName::Settings)), - TaskSourceKind::Worktree { .. } => Some(Icon::new(IconName::FileTree)), - TaskSourceKind::Lsp { - language_name: name, - .. - } - | TaskSourceKind::Language { name, .. } => file_icons::FileIcons::get(cx) - .get_icon_for_type(&name.to_lowercase(), cx) - .map(Icon::from_path), - } - .map(|icon| icon.color(Color::Muted).size(IconSize::Small)); - let indicator = if matches!(source_kind, TaskSourceKind::Lsp { .. }) { - Some(Indicator::icon( - Icon::new(IconName::BoltOutlined).size(IconSize::Small), - )) - } else { - None - }; - let icon = icon.map(|icon| { - IconWithIndicator::new(icon, indicator) - .indicator_border_color(Some(cx.theme().colors().border_transparent)) - }); - let history_run_icon = if Some(ix) <= self.divider_index { - Some( - Icon::new(IconName::HistoryRerun) - .color(Color::Muted) - .size(IconSize::Small) - .into_any_element(), - ) - } else { - Some( - v_flex() - .flex_none() - .size(IconSize::Small.rems()) - .into_any_element(), - ) - }; - - Some( - ListItem::new(format!("tasks-modal-{ix}")) - .inset(true) - .start_slot::(icon) - .end_slot::( - h_flex() - .gap_1() - .child(Label::new(truncate_and_trailoff( - &template - .tags - .iter() - .map(|tag| format!("#{}", tag)) - .collect::>() - .join(" "), - MAX_TAGS_LINE_LEN, - ))) - .flex_none() - .child(history_run_icon.unwrap()) - .into_any_element(), - ) - .spacing(ListItemSpacing::Sparse) - .when_some(tooltip_label, |list_item, item_label| { - list_item.tooltip(move |_, _| item_label.clone()) - }) - .map(|item| { - if matches!(source_kind, TaskSourceKind::UserInput) - || Some(ix) <= self.divider_index - { - let task_index = hit.candidate_id; - let delete_button = div().child( - IconButton::new("delete", IconName::Close) - .shape(IconButtonShape::Square) - .icon_color(Color::Muted) - .size(ButtonSize::None) - .icon_size(IconSize::XSmall) - .on_click(cx.listener(move |picker, _event, window, cx| { - cx.stop_propagation(); - window.prevent_default(); - - picker.delegate.delete_previously_used(task_index, cx); - picker.delegate.last_used_candidate_index = picker - .delegate - .last_used_candidate_index - .unwrap_or(0) - .checked_sub(1); - picker.refresh(window, cx); - })) - .tooltip(|_, cx| { - Tooltip::simple("Delete Previously Scheduled Task", cx) - }), - ); - item.end_hover_slot(delete_button) - } else { - item - } - }) - .toggle_state(selected) - .child(highlighted_location.render(window, cx)), - ) - } - - fn confirm_completion( - &mut self, - _: String, - _window: &mut Window, - _: &mut Context>, - ) -> Option { - let task_index = self.matches.get(self.selected_index())?.candidate_id; - let tasks = self.candidates.as_ref()?; - let (_, task) = tasks.get(task_index)?; - Some(task.resolved.command_label.clone()) - } - - fn confirm_input( - &mut self, - omit_history_entry: bool, - window: &mut Window, - cx: &mut Context>, - ) { - let Some((task_source_kind, mut task)) = self.spawn_oneshot() else { - return; - }; - - if let Some(TaskOverrides { - reveal_target: Some(reveal_target), - }) = self.task_overrides - { - task.resolved.reveal_target = reveal_target; - } - self.workspace - .update(cx, |workspace, cx| { - workspace.schedule_resolved_task( - task_source_kind, - task, - omit_history_entry, - window, - cx, - ) - }) - .ok(); - cx.emit(DismissEvent); - } - - fn separators_after_indices(&self) -> Vec { - if let Some(i) = self.divider_index { - vec![i] - } else { - Vec::new() - } - } - - fn render_footer( - &self, - window: &mut Window, - cx: &mut Context>, - ) -> Option { - let is_recent_selected = self.divider_index >= Some(self.selected_index); - let current_modifiers = window.modifiers(); - let left_button = if self - .task_store - .read(cx) - .task_inventory()? - .read(cx) - .last_scheduled_task(None) - .is_some() - { - Some(("Rerun Last Task", Rerun::default().boxed_clone())) - } else { - None - }; - Some( - h_flex() - .w_full() - .p_1p5() - .justify_between() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .child( - left_button - .map(|(label, action)| { - let keybind = KeyBinding::for_action(&*action, cx); - - Button::new("edit-current-task", label) - .key_binding(keybind) - .on_click(move |_, window, cx| { - window.dispatch_action(action.boxed_clone(), cx); - }) - .into_any_element() - }) - .unwrap_or_else(|| h_flex().into_any_element()), - ) - .map(|this| { - if (current_modifiers.alt || self.matches.is_empty()) && !self.prompt.is_empty() - { - let action = picker::ConfirmInput { - secondary: current_modifiers.secondary(), - } - .boxed_clone(); - this.child({ - let spawn_oneshot_label = if current_modifiers.secondary() { - "Spawn Oneshot Without History" - } else { - "Spawn Oneshot" - }; - - Button::new("spawn-onehshot", spawn_oneshot_label) - .key_binding(KeyBinding::for_action(&*action, cx)) - .on_click(move |_, window, cx| { - window.dispatch_action(action.boxed_clone(), cx) - }) - }) - } else if current_modifiers.secondary() { - this.child({ - let label = if is_recent_selected { - "Rerun Without History" - } else { - "Spawn Without History" - }; - Button::new("spawn", label) - .key_binding(KeyBinding::for_action(&menu::SecondaryConfirm, cx)) - .on_click(move |_, window, cx| { - window.dispatch_action(menu::SecondaryConfirm.boxed_clone(), cx) - }) - }) - } else { - this.child({ - let run_entry_label = - if is_recent_selected { "Rerun" } else { "Spawn" }; - - Button::new("spawn", run_entry_label) - .key_binding(KeyBinding::for_action(&menu::Confirm, cx)) - .on_click(|_, window, cx| { - window.dispatch_action(menu::Confirm.boxed_clone(), cx); - }) - }) - } - }) - .into_any_element(), - ) - } -} - -fn string_match_candidates<'a>( - candidates: impl IntoIterator + 'a, -) -> Vec { - candidates - .into_iter() - .enumerate() - .map(|(index, (_, candidate))| StringMatchCandidate::new(index, candidate.display_label())) - .collect() -} - -#[cfg(test)] -mod tests { - use std::{path::PathBuf, sync::Arc}; - - use editor::{Editor, SelectionEffects}; - use gpui::{TestAppContext, VisualTestContext}; - use language::{Language, LanguageConfig, LanguageMatcher, Point}; - use project::{ContextProviderWithTasks, FakeFs, Project}; - use serde_json::json; - use task::TaskTemplates; - use util::path; - use workspace::{CloseInactiveTabsAndPanes, OpenOptions, OpenVisible}; - - use crate::{modal::Spawn, tests::init_test}; - - use super::*; - - #[gpui::test] - async fn test_spawn_tasks_modal_query_reuse(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/dir"), - json!({ - ".zed": { - "tasks.json": r#"[ - { - "label": "example task", - "command": "echo", - "args": ["4"] - }, - { - "label": "another one", - "command": "echo", - "args": ["55"] - }, - ]"#, - }, - "a.ts": "a" - }), - ) - .await; - - let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - - let tasks_picker = open_spawn_tasks(&workspace, cx); - assert_eq!( - query(&tasks_picker, cx), - "", - "Initial query should be empty" - ); - assert_eq!( - task_names(&tasks_picker, cx), - vec!["another one", "example task"], - "With no global tasks and no open item, a single worktree should be used and its tasks listed" - ); - drop(tasks_picker); - - let _ = workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_abs_path( - PathBuf::from(path!("/dir/a.ts")), - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - window, - cx, - ) - }) - .await - .unwrap(); - let tasks_picker = open_spawn_tasks(&workspace, cx); - assert_eq!( - task_names(&tasks_picker, cx), - vec!["another one", "example task"], - "Initial tasks should be listed in alphabetical order" - ); - - let query_str = "tas"; - cx.simulate_input(query_str); - assert_eq!(query(&tasks_picker, cx), query_str); - assert_eq!( - task_names(&tasks_picker, cx), - vec!["example task"], - "Only one task should match the query {query_str}" - ); - - cx.dispatch_action(picker::ConfirmCompletion); - assert_eq!( - query(&tasks_picker, cx), - "echo 4", - "Query should be set to the selected task's command" - ); - assert_eq!( - task_names(&tasks_picker, cx), - Vec::::new(), - "No task should be listed" - ); - cx.dispatch_action(picker::ConfirmInput { secondary: false }); - - let tasks_picker = open_spawn_tasks(&workspace, cx); - assert_eq!( - query(&tasks_picker, cx), - "", - "Query should be reset after confirming" - ); - assert_eq!( - task_names(&tasks_picker, cx), - vec!["echo 4", "another one", "example task"], - "New oneshot task should be listed first" - ); - - let query_str = "echo 4"; - cx.simulate_input(query_str); - assert_eq!(query(&tasks_picker, cx), query_str); - assert_eq!( - task_names(&tasks_picker, cx), - vec!["echo 4"], - "New oneshot should match custom command query" - ); - - cx.dispatch_action(picker::ConfirmInput { secondary: false }); - let tasks_picker = open_spawn_tasks(&workspace, cx); - assert_eq!( - query(&tasks_picker, cx), - "", - "Query should be reset after confirming" - ); - assert_eq!( - task_names(&tasks_picker, cx), - vec![query_str, "another one", "example task"], - "Last recently used one show task should be listed first" - ); - - cx.dispatch_action(picker::ConfirmCompletion); - assert_eq!( - query(&tasks_picker, cx), - query_str, - "Query should be set to the custom task's name" - ); - assert_eq!( - task_names(&tasks_picker, cx), - vec![query_str], - "Only custom task should be listed" - ); - - let query_str = "0"; - cx.simulate_input(query_str); - assert_eq!(query(&tasks_picker, cx), "echo 40"); - assert_eq!( - task_names(&tasks_picker, cx), - Vec::::new(), - "New oneshot should not match any command query" - ); - - cx.dispatch_action(picker::ConfirmInput { secondary: true }); - let tasks_picker = open_spawn_tasks(&workspace, cx); - assert_eq!( - query(&tasks_picker, cx), - "", - "Query should be reset after confirming" - ); - assert_eq!( - task_names(&tasks_picker, cx), - vec!["echo 4", "another one", "example task"], - "No query should be added to the list, as it was submitted with secondary action (that maps to omit_history = true)" - ); - - cx.dispatch_action(Spawn::ByName { - task_name: "example task".to_string(), - reveal_target: None, - }); - let tasks_picker = workspace.update(cx, |workspace, cx| { - workspace - .active_modal::(cx) - .unwrap() - .read(cx) - .picker - .clone() - }); - assert_eq!( - task_names(&tasks_picker, cx), - vec!["echo 4", "another one", "example task"], - ); - } - - #[gpui::test] - async fn test_basic_context_for_simple_files(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/dir"), - json!({ - ".zed": { - "tasks.json": r#"[ - { - "label": "hello from $ZED_FILE:$ZED_ROW:$ZED_COLUMN", - "command": "echo", - "args": ["hello", "from", "$ZED_FILE", ":", "$ZED_ROW", ":", "$ZED_COLUMN"] - }, - { - "label": "opened now: $ZED_WORKTREE_ROOT", - "command": "echo", - "args": ["opened", "now:", "$ZED_WORKTREE_ROOT"] - } - ]"#, - }, - "file_without_extension": "aaaaaaaaaaaaaaaaaaaa\naaaaaaaaaaaaaaaaaa", - "file_with.odd_extension": "b", - }), - ) - .await; - - let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let tasks_picker = open_spawn_tasks(&workspace, cx); - assert_eq!( - task_names(&tasks_picker, cx), - vec![concat!("opened now: ", path!("/dir")).to_string()], - "When no file is open for a single worktree, should autodetect all worktree-related tasks" - ); - tasks_picker.update(cx, |_, cx| { - cx.emit(DismissEvent); - }); - drop(tasks_picker); - cx.executor().run_until_parked(); - - let _ = workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_abs_path( - PathBuf::from(path!("/dir/file_with.odd_extension")), - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - window, - cx, - ) - }) - .await - .unwrap(); - cx.executor().run_until_parked(); - let tasks_picker = open_spawn_tasks(&workspace, cx); - assert_eq!( - task_names(&tasks_picker, cx), - vec![ - concat!("hello from ", path!("/dir/file_with.odd_extension:1:1")).to_string(), - concat!("opened now: ", path!("/dir")).to_string(), - ], - "Second opened buffer should fill the context, labels should be trimmed if long enough" - ); - tasks_picker.update(cx, |_, cx| { - cx.emit(DismissEvent); - }); - drop(tasks_picker); - cx.executor().run_until_parked(); - - let second_item = workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_abs_path( - PathBuf::from(path!("/dir/file_without_extension")), - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - window, - cx, - ) - }) - .await - .unwrap(); - - let editor = cx - .update(|_window, cx| second_item.act_as::(cx)) - .unwrap(); - editor.update_in(cx, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges(Some(Point::new(1, 2)..Point::new(1, 5))) - }) - }); - cx.executor().run_until_parked(); - let tasks_picker = open_spawn_tasks(&workspace, cx); - assert_eq!( - task_names(&tasks_picker, cx), - vec![ - concat!("hello from ", path!("/dir/file_without_extension:2:3")).to_string(), - concat!("opened now: ", path!("/dir")).to_string(), - ], - "Opened buffer should fill the context, labels should be trimmed if long enough" - ); - tasks_picker.update(cx, |_, cx| { - cx.emit(DismissEvent); - }); - drop(tasks_picker); - cx.executor().run_until_parked(); - } - - #[gpui::test] - async fn test_language_task_filtering(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/dir"), - json!({ - "a1.ts": "// a1", - "a2.ts": "// a2", - "b.rs": "// b", - }), - ) - .await; - - let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; - project.read_with(cx, |project, _| { - let language_registry = project.languages(); - language_registry.add(Arc::new( - Language::new( - LanguageConfig { - name: "TypeScript".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["ts".to_string()], - ..LanguageMatcher::default() - }, - ..LanguageConfig::default() - }, - None, - ) - .with_context_provider(Some(Arc::new( - ContextProviderWithTasks::new(TaskTemplates(vec![ - TaskTemplate { - label: "Task without variables".to_string(), - command: "npm run clean".to_string(), - ..TaskTemplate::default() - }, - TaskTemplate { - label: "TypeScript task from file $ZED_FILE".to_string(), - command: "npm run build".to_string(), - ..TaskTemplate::default() - }, - TaskTemplate { - label: "Another task from file $ZED_FILE".to_string(), - command: "npm run lint".to_string(), - ..TaskTemplate::default() - }, - ])), - ))), - )); - language_registry.add(Arc::new( - Language::new( - LanguageConfig { - name: "Rust".into(), - matcher: LanguageMatcher { - path_suffixes: vec!["rs".to_string()], - ..LanguageMatcher::default() - }, - ..LanguageConfig::default() - }, - None, - ) - .with_context_provider(Some(Arc::new( - ContextProviderWithTasks::new(TaskTemplates(vec![TaskTemplate { - label: "Rust task".to_string(), - command: "cargo check".into(), - ..TaskTemplate::default() - }])), - ))), - )); - }); - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let _ts_file_1 = workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_abs_path( - PathBuf::from(path!("/dir/a1.ts")), - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - window, - cx, - ) - }) - .await - .unwrap(); - let tasks_picker = open_spawn_tasks(&workspace, cx); - assert_eq!( - task_names(&tasks_picker, cx), - vec![ - concat!("Another task from file ", path!("/dir/a1.ts")), - concat!("TypeScript task from file ", path!("/dir/a1.ts")), - "Task without variables", - ], - "Should open spawn TypeScript tasks for the opened file, tasks with most template variables above, all groups sorted alphanumerically" - ); - - emulate_task_schedule( - tasks_picker, - &project, - concat!("TypeScript task from file ", path!("/dir/a1.ts")), - cx, - ); - - let tasks_picker = open_spawn_tasks(&workspace, cx); - assert_eq!( - task_names(&tasks_picker, cx), - vec![ - concat!("TypeScript task from file ", path!("/dir/a1.ts")), - concat!("Another task from file ", path!("/dir/a1.ts")), - "Task without variables", - ], - "After spawning the task and getting it into the history, it should be up in the sort as recently used. - Tasks with the same labels and context are deduplicated." - ); - tasks_picker.update(cx, |_, cx| { - cx.emit(DismissEvent); - }); - drop(tasks_picker); - cx.executor().run_until_parked(); - - let _ts_file_2 = workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_abs_path( - PathBuf::from(path!("/dir/a2.ts")), - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - window, - cx, - ) - }) - .await - .unwrap(); - let tasks_picker = open_spawn_tasks(&workspace, cx); - assert_eq!( - task_names(&tasks_picker, cx), - vec![ - concat!("TypeScript task from file ", path!("/dir/a1.ts")), - concat!("Another task from file ", path!("/dir/a2.ts")), - concat!("TypeScript task from file ", path!("/dir/a2.ts")), - "Task without variables", - ], - "Even when both TS files are open, should only show the history (on the top), and tasks, resolved for the current file" - ); - tasks_picker.update(cx, |_, cx| { - cx.emit(DismissEvent); - }); - drop(tasks_picker); - cx.executor().run_until_parked(); - - let _rs_file = workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_abs_path( - PathBuf::from(path!("/dir/b.rs")), - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - window, - cx, - ) - }) - .await - .unwrap(); - let tasks_picker = open_spawn_tasks(&workspace, cx); - assert_eq!( - task_names(&tasks_picker, cx), - vec!["Rust task"], - "Even when both TS files are open and one TS task spawned, opened file's language tasks should be displayed only" - ); - - cx.dispatch_action(CloseInactiveTabsAndPanes::default()); - emulate_task_schedule(tasks_picker, &project, "Rust task", cx); - let _ts_file_2 = workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_abs_path( - PathBuf::from(path!("/dir/a2.ts")), - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - window, - cx, - ) - }) - .await - .unwrap(); - let tasks_picker = open_spawn_tasks(&workspace, cx); - assert_eq!( - task_names(&tasks_picker, cx), - vec![ - concat!("TypeScript task from file ", path!("/dir/a1.ts")), - concat!("Another task from file ", path!("/dir/a2.ts")), - concat!("TypeScript task from file ", path!("/dir/a2.ts")), - "Task without variables", - ], - "After closing all but *.rs tabs, running a Rust task and switching back to TS tasks, \ - same TS spawn history should be restored" - ); - } - - fn emulate_task_schedule( - tasks_picker: Entity>, - project: &Entity, - scheduled_task_label: &str, - cx: &mut VisualTestContext, - ) { - let scheduled_task = tasks_picker.read_with(cx, |tasks_picker, _| { - tasks_picker - .delegate - .candidates - .iter() - .flatten() - .find(|(_, task)| task.resolved_label == scheduled_task_label) - .cloned() - .unwrap() - }); - project.update(cx, |project, cx| { - if let Some(task_inventory) = project.task_store().read(cx).task_inventory().cloned() { - task_inventory.update(cx, |inventory, _| { - let (kind, task) = scheduled_task; - inventory.task_scheduled(kind, task); - }); - } - }); - tasks_picker.update(cx, |_, cx| { - cx.emit(DismissEvent); - }); - drop(tasks_picker); - cx.executor().run_until_parked() - } - - fn open_spawn_tasks( - workspace: &Entity, - cx: &mut VisualTestContext, - ) -> Entity> { - cx.dispatch_action(Spawn::modal()); - workspace.update(cx, |workspace, cx| { - workspace - .active_modal::(cx) - .expect("no task modal after `Spawn` action was dispatched") - .read(cx) - .picker - .clone() - }) - } - - fn query( - spawn_tasks: &Entity>, - cx: &mut VisualTestContext, - ) -> String { - spawn_tasks.read_with(cx, |spawn_tasks, cx| spawn_tasks.query(cx)) - } - - fn task_names( - spawn_tasks: &Entity>, - cx: &mut VisualTestContext, - ) -> Vec { - spawn_tasks.read_with(cx, |spawn_tasks, _| { - spawn_tasks - .delegate - .matches - .iter() - .map(|hit| hit.string.clone()) - .collect::>() - }) - } -} diff --git a/crates/tasks_ui/src/tasks_ui.rs b/crates/tasks_ui/src/tasks_ui.rs deleted file mode 100644 index 35c8a2ee22..0000000000 --- a/crates/tasks_ui/src/tasks_ui.rs +++ /dev/null @@ -1,611 +0,0 @@ -use std::{path::Path, sync::Arc}; - -use collections::HashMap; -use editor::Editor; -use gpui::{App, AppContext as _, Context, Entity, Task, Window}; -use project::{Location, TaskContexts, TaskSourceKind, Worktree}; -use task::{RevealTarget, TaskContext, TaskId, TaskTemplate, TaskVariables, VariableName}; -use workspace::Workspace; - -mod modal; - -pub use modal::{Rerun, ShowAttachModal, Spawn, TaskOverrides, TasksModal}; - -pub fn init(cx: &mut App) { - cx.observe_new( - |workspace: &mut Workspace, _: Option<&mut Window>, _: &mut Context| { - workspace - .register_action(spawn_task_or_modal) - .register_action(move |workspace, action: &modal::Rerun, window, cx| { - if let Some((task_source_kind, mut last_scheduled_task)) = workspace - .project() - .read(cx) - .task_store() - .read(cx) - .task_inventory() - .and_then(|inventory| { - inventory.read(cx).last_scheduled_task( - action - .task_id - .as_ref() - .map(|id| TaskId(id.clone())) - .as_ref(), - ) - }) - { - if action.reevaluate_context { - let mut original_task = last_scheduled_task.original_task().clone(); - if let Some(allow_concurrent_runs) = action.allow_concurrent_runs { - original_task.allow_concurrent_runs = allow_concurrent_runs; - } - if let Some(use_new_terminal) = action.use_new_terminal { - original_task.use_new_terminal = use_new_terminal; - } - let task_contexts = task_contexts(workspace, window, cx); - cx.spawn_in(window, async move |workspace, cx| { - let task_contexts = task_contexts.await; - let default_context = TaskContext::default(); - workspace - .update_in(cx, |workspace, window, cx| { - workspace.schedule_task( - task_source_kind, - &original_task, - task_contexts - .active_context() - .unwrap_or(&default_context), - false, - window, - cx, - ) - }) - .ok() - }) - .detach() - } else { - let resolved = &mut last_scheduled_task.resolved; - - if let Some(allow_concurrent_runs) = action.allow_concurrent_runs { - resolved.allow_concurrent_runs = allow_concurrent_runs; - } - if let Some(use_new_terminal) = action.use_new_terminal { - resolved.use_new_terminal = use_new_terminal; - } - - workspace.schedule_resolved_task( - task_source_kind, - last_scheduled_task, - false, - window, - cx, - ); - } - } else { - spawn_task_or_modal( - workspace, - &Spawn::ViaModal { - reveal_target: None, - }, - window, - cx, - ); - }; - }); - }, - ) - .detach(); -} - -fn spawn_task_or_modal( - workspace: &mut Workspace, - action: &Spawn, - window: &mut Window, - cx: &mut Context, -) { - if let Some(provider) = workspace.debugger_provider() { - provider.spawn_task_or_modal(workspace, action, window, cx); - return; - } - - match action { - Spawn::ByName { - task_name, - reveal_target, - } => { - let overrides = reveal_target.map(|reveal_target| TaskOverrides { - reveal_target: Some(reveal_target), - }); - let name = task_name.clone(); - spawn_tasks_filtered(move |(_, task)| task.label.eq(&name), overrides, window, cx) - .detach_and_log_err(cx) - } - Spawn::ByTag { - task_tag, - reveal_target, - } => { - let overrides = reveal_target.map(|reveal_target| TaskOverrides { - reveal_target: Some(reveal_target), - }); - let tag = task_tag.clone(); - spawn_tasks_filtered( - move |(_, task)| task.tags.contains(&tag), - overrides, - window, - cx, - ) - .detach_and_log_err(cx) - } - Spawn::ViaModal { reveal_target } => { - toggle_modal(workspace, *reveal_target, window, cx).detach() - } - } -} - -pub fn toggle_modal( - workspace: &mut Workspace, - reveal_target: Option, - window: &mut Window, - cx: &mut Context, -) -> Task<()> { - let task_store = workspace.project().read(cx).task_store().clone(); - let workspace_handle = workspace.weak_handle(); - let can_open_modal = workspace - .project() - .read_with(cx, |project, _| !project.is_via_collab()); - if can_open_modal { - let task_contexts = task_contexts(workspace, window, cx); - cx.spawn_in(window, async move |workspace, cx| { - let task_contexts = Arc::new(task_contexts.await); - workspace - .update_in(cx, |workspace, window, cx| { - workspace.toggle_modal(window, cx, |window, cx| { - TasksModal::new( - task_store.clone(), - task_contexts, - reveal_target.map(|target| TaskOverrides { - reveal_target: Some(target), - }), - true, - workspace_handle, - window, - cx, - ) - }) - }) - .ok(); - }) - } else { - Task::ready(()) - } -} - -pub fn spawn_tasks_filtered( - mut predicate: F, - overrides: Option, - window: &mut Window, - cx: &mut Context, -) -> Task> -where - F: FnMut((&TaskSourceKind, &TaskTemplate)) -> bool + 'static, -{ - cx.spawn_in(window, async move |workspace, cx| { - let task_contexts = workspace.update_in(cx, |workspace, window, cx| { - task_contexts(workspace, window, cx) - })?; - let task_contexts = task_contexts.await; - let mut tasks = workspace - .update(cx, |workspace, cx| { - let Some(task_inventory) = workspace - .project() - .read(cx) - .task_store() - .read(cx) - .task_inventory() - .cloned() - else { - return Task::ready(Vec::new()); - }; - let (file, language) = task_contexts - .location() - .map(|location| { - let buffer = location.buffer.read(cx); - ( - buffer.file().cloned(), - buffer.language_at(location.range.start), - ) - }) - .unwrap_or_default(); - task_inventory - .read(cx) - .list_tasks(file, language, task_contexts.worktree(), cx) - })? - .await; - - let did_spawn = workspace - .update_in(cx, |workspace, window, cx| { - let default_context = TaskContext::default(); - let active_context = task_contexts.active_context().unwrap_or(&default_context); - - tasks.retain_mut(|(task_source_kind, target_task)| { - if predicate((task_source_kind, target_task)) { - if let Some(overrides) = &overrides - && let Some(target_override) = overrides.reveal_target - { - target_task.reveal_target = target_override; - } - workspace.schedule_task( - task_source_kind.clone(), - target_task, - active_context, - false, - window, - cx, - ); - true - } else { - false - } - }); - - if tasks.is_empty() { None } else { Some(()) } - })? - .is_some(); - if !did_spawn { - workspace - .update_in(cx, |workspace, window, cx| { - spawn_task_or_modal( - workspace, - &Spawn::ViaModal { - reveal_target: overrides.and_then(|overrides| overrides.reveal_target), - }, - window, - cx, - ); - }) - .ok(); - } - - Ok(()) - }) -} - -pub fn task_contexts( - workspace: &Workspace, - window: &mut Window, - cx: &mut App, -) -> Task { - let active_item = workspace.active_item(cx); - let active_worktree = active_item - .as_ref() - .and_then(|item| item.project_path(cx)) - .map(|project_path| project_path.worktree_id) - .filter(|worktree_id| { - workspace - .project() - .read(cx) - .worktree_for_id(*worktree_id, cx) - .is_some_and(|worktree| is_visible_directory(&worktree, cx)) - }) - .or_else(|| { - workspace - .visible_worktrees(cx) - .next() - .map(|tree| tree.read(cx).id()) - }); - - let active_editor = active_item.and_then(|item| item.act_as::(cx)); - - let editor_context_task = active_editor.as_ref().map(|active_editor| { - active_editor.update(cx, |editor, cx| editor.task_context(window, cx)) - }); - - let location = active_editor.as_ref().and_then(|editor| { - editor.update(cx, |editor, cx| { - let selection = editor.selections.newest_anchor(); - let multi_buffer = editor.buffer().clone(); - let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx); - let (buffer_snapshot, buffer_offset) = - multi_buffer_snapshot.point_to_buffer_offset(selection.head())?; - let buffer_anchor = buffer_snapshot.anchor_before(buffer_offset); - let buffer = multi_buffer.read(cx).buffer(buffer_snapshot.remote_id())?; - Some(Location { - buffer, - range: buffer_anchor..buffer_anchor, - }) - }) - }); - - let lsp_task_sources = active_editor - .as_ref() - .map(|active_editor| active_editor.update(cx, |editor, cx| editor.lsp_task_sources(cx))) - .unwrap_or_default(); - - let latest_selection = active_editor.as_ref().map(|active_editor| { - active_editor - .read(cx) - .selections - .newest_anchor() - .head() - .text_anchor - }); - - let mut worktree_abs_paths = workspace - .worktrees(cx) - .filter(|worktree| is_visible_directory(worktree, cx)) - .map(|worktree| { - let worktree = worktree.read(cx); - (worktree.id(), worktree.abs_path()) - }) - .collect::>(); - - cx.background_spawn(async move { - let mut task_contexts = TaskContexts::default(); - - task_contexts.lsp_task_sources = lsp_task_sources; - task_contexts.latest_selection = latest_selection; - - if let Some(editor_context_task) = editor_context_task - && let Some(editor_context) = editor_context_task.await - { - task_contexts.active_item_context = Some((active_worktree, location, editor_context)); - } - - if let Some(active_worktree) = active_worktree { - if let Some(active_worktree_abs_path) = worktree_abs_paths.remove(&active_worktree) { - task_contexts.active_worktree_context = - Some((active_worktree, worktree_context(&active_worktree_abs_path))); - } - } else if worktree_abs_paths.len() == 1 { - task_contexts.active_worktree_context = worktree_abs_paths - .drain() - .next() - .map(|(id, abs_path)| (id, worktree_context(&abs_path))); - } - - task_contexts.other_worktree_contexts.extend( - worktree_abs_paths - .into_iter() - .map(|(id, abs_path)| (id, worktree_context(&abs_path))), - ); - task_contexts - }) -} - -fn is_visible_directory(worktree: &Entity, cx: &App) -> bool { - let worktree = worktree.read(cx); - worktree.is_visible() && worktree.root_entry().is_some_and(|entry| entry.is_dir()) -} - -fn worktree_context(worktree_abs_path: &Path) -> TaskContext { - let mut task_variables = TaskVariables::default(); - task_variables.insert( - VariableName::WorktreeRoot, - worktree_abs_path.to_string_lossy().into_owned(), - ); - TaskContext { - cwd: Some(worktree_abs_path.to_path_buf()), - task_variables, - project_env: HashMap::default(), - } -} - -#[cfg(test)] -mod tests { - use std::{collections::HashMap, sync::Arc}; - - use editor::{Editor, MultiBufferOffset, SelectionEffects}; - use gpui::TestAppContext; - use language::{Language, LanguageConfig}; - use project::{BasicContextProvider, FakeFs, Project, task_store::TaskStore}; - use serde_json::json; - use task::{TaskContext, TaskVariables, VariableName}; - use ui::VisualContext; - use util::{path, rel_path::rel_path}; - use workspace::{AppState, Workspace}; - - use crate::task_contexts; - - #[gpui::test] - async fn test_default_language_context(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - path!("/dir"), - json!({ - ".zed": { - "tasks.json": r#"[ - { - "label": "example task", - "command": "echo", - "args": ["4"] - }, - { - "label": "another one", - "command": "echo", - "args": ["55"] - }, - ]"#, - }, - "a.ts": "function this_is_a_test() { }", - "rust": { - "b.rs": "use std; fn this_is_a_rust_file() { }", - } - - }), - ) - .await; - let project = Project::test(fs, [path!("/dir").as_ref()], cx).await; - let worktree_store = project.read_with(cx, |project, _| project.worktree_store()); - let rust_language = Arc::new( - Language::new( - LanguageConfig::default(), - Some(tree_sitter_rust::LANGUAGE.into()), - ) - .with_outline_query( - r#"(function_item - "fn" @context - name: (_) @name) @item"#, - ) - .unwrap() - .with_context_provider(Some(Arc::new(BasicContextProvider::new( - worktree_store.clone(), - )))), - ); - - let typescript_language = Arc::new( - Language::new( - LanguageConfig::default(), - Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()), - ) - .with_outline_query( - r#"(function_declaration - "async"? @context - "function" @context - name: (_) @name - parameters: (formal_parameters - "(" @context - ")" @context)) @item"#, - ) - .unwrap() - .with_context_provider(Some(Arc::new(BasicContextProvider::new( - worktree_store.clone(), - )))), - ); - - let worktree_id = project.update(cx, |project, cx| { - project.worktrees(cx).next().unwrap().read(cx).id() - }); - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let buffer1 = workspace - .update(cx, |this, cx| { - this.project().update(cx, |this, cx| { - this.open_buffer((worktree_id, rel_path("a.ts")), cx) - }) - }) - .await - .unwrap(); - buffer1.update(cx, |this, cx| { - this.set_language(Some(typescript_language), cx) - }); - let editor1 = cx.new_window_entity(|window, cx| { - Editor::for_buffer(buffer1, Some(project.clone()), window, cx) - }); - let buffer2 = workspace - .update(cx, |this, cx| { - this.project().update(cx, |this, cx| { - this.open_buffer((worktree_id, rel_path("rust/b.rs")), cx) - }) - }) - .await - .unwrap(); - buffer2.update(cx, |this, cx| this.set_language(Some(rust_language), cx)); - let editor2 = cx - .new_window_entity(|window, cx| Editor::for_buffer(buffer2, Some(project), window, cx)); - - let first_context = workspace - .update_in(cx, |workspace, window, cx| { - workspace.add_item_to_center(Box::new(editor1.clone()), window, cx); - workspace.add_item_to_center(Box::new(editor2.clone()), window, cx); - assert_eq!( - workspace.active_item(cx).unwrap().item_id(), - editor2.entity_id() - ); - task_contexts(workspace, window, cx) - }) - .await; - - assert_eq!( - first_context - .active_context() - .expect("Should have an active context"), - &TaskContext { - cwd: Some(path!("/dir").into()), - task_variables: TaskVariables::from_iter([ - (VariableName::File, path!("/dir/rust/b.rs").into()), - (VariableName::Filename, "b.rs".into()), - (VariableName::RelativeFile, path!("rust/b.rs").into()), - (VariableName::RelativeDir, "rust".into()), - (VariableName::Dirname, path!("/dir/rust").into()), - (VariableName::Stem, "b".into()), - (VariableName::WorktreeRoot, path!("/dir").into()), - (VariableName::Row, "1".into()), - (VariableName::Column, "1".into()), - ]), - project_env: HashMap::default(), - } - ); - - // And now, let's select an identifier. - editor2.update_in(cx, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| { - selections.select_ranges([MultiBufferOffset(14)..MultiBufferOffset(18)]) - }) - }); - - assert_eq!( - workspace - .update_in(cx, |workspace, window, cx| { - task_contexts(workspace, window, cx) - }) - .await - .active_context() - .expect("Should have an active context"), - &TaskContext { - cwd: Some(path!("/dir").into()), - task_variables: TaskVariables::from_iter([ - (VariableName::File, path!("/dir/rust/b.rs").into()), - (VariableName::Filename, "b.rs".into()), - (VariableName::RelativeFile, path!("rust/b.rs").into()), - (VariableName::RelativeDir, "rust".into()), - (VariableName::Dirname, path!("/dir/rust").into()), - (VariableName::Stem, "b".into()), - (VariableName::WorktreeRoot, path!("/dir").into()), - (VariableName::Row, "1".into()), - (VariableName::Column, "15".into()), - (VariableName::SelectedText, "is_i".into()), - (VariableName::Symbol, "this_is_a_rust_file".into()), - ]), - project_env: HashMap::default(), - } - ); - - assert_eq!( - workspace - .update_in(cx, |workspace, window, cx| { - // Now, let's switch the active item to .ts file. - workspace.activate_item(&editor1, true, true, window, cx); - task_contexts(workspace, window, cx) - }) - .await - .active_context() - .expect("Should have an active context"), - &TaskContext { - cwd: Some(path!("/dir").into()), - task_variables: TaskVariables::from_iter([ - (VariableName::File, path!("/dir/a.ts").into()), - (VariableName::Filename, "a.ts".into()), - (VariableName::RelativeFile, "a.ts".into()), - (VariableName::RelativeDir, ".".into()), - (VariableName::Dirname, path!("/dir").into()), - (VariableName::Stem, "a".into()), - (VariableName::WorktreeRoot, path!("/dir").into()), - (VariableName::Row, "1".into()), - (VariableName::Column, "1".into()), - (VariableName::Symbol, "this_is_a_test".into()), - ]), - project_env: HashMap::default(), - } - ); - } - - pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc { - cx.update(|cx| { - let state = AppState::test(cx); - crate::init(cx); - editor::init(cx); - TaskStore::init(None); - state - }) - } -} diff --git a/crates/telemetry/Cargo.toml b/crates/telemetry/Cargo.toml deleted file mode 100644 index ed166ea4c7..0000000000 --- a/crates/telemetry/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "telemetry" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/telemetry.rs" - -[dependencies] -serde.workspace = true -serde_json.workspace = true -telemetry_events.workspace = true -futures.workspace = true diff --git a/crates/telemetry/LICENSE-GPL b/crates/telemetry/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/telemetry/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/telemetry/src/telemetry.rs b/crates/telemetry/src/telemetry.rs deleted file mode 100644 index e6c8516967..0000000000 --- a/crates/telemetry/src/telemetry.rs +++ /dev/null @@ -1,66 +0,0 @@ -//! See [Telemetry in Zed](https://zed.dev/docs/telemetry) for additional information. -use futures::channel::mpsc; -pub use serde_json; -use std::sync::OnceLock; -pub use telemetry_events::FlexibleEvent as Event; - -/// Macro to create telemetry events and send them to the telemetry queue. -/// -/// By convention, the name should be "Noun Verbed", e.g. "Keymap Changed" -/// or "Project Diagnostics Opened". -/// -/// The properties can be any value that implements serde::Serialize. -/// -/// ``` -/// # let url = "https://example.com"; -/// telemetry::event!("Keymap Changed", version = "1.0.0"); -/// telemetry::event!("Documentation Viewed", url, source = "Extension Upsell"); -/// ``` -/// -/// If you want to debug logging in development, export `RUST_LOG=telemetry=trace` -#[macro_export] -macro_rules! event { - ($name:expr) => {{ - let event = $crate::Event { - event_type: $name.to_string(), - event_properties: std::collections::HashMap::new(), - }; - $crate::send_event(event); - }}; - ($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => {{ - let event = $crate::Event { - event_type: $name.to_string(), - event_properties: std::collections::HashMap::from([ - $( - (stringify!($key).to_string(), - $crate::serde_json::value::to_value(&$crate::serialize_property!($key $(= $value)?)) - .unwrap_or_else(|_| $crate::serde_json::to_value(&()).unwrap()) - ), - )+ - ]), - }; - $crate::send_event(event); - }}; -} - -#[macro_export] -macro_rules! serialize_property { - ($key:ident) => { - $key - }; - ($key:ident = $value:expr) => { - $value - }; -} - -pub fn send_event(event: Event) { - if let Some(queue) = TELEMETRY_QUEUE.get() { - queue.unbounded_send(event).ok(); - } -} - -pub fn init(tx: mpsc::UnboundedSender) { - TELEMETRY_QUEUE.set(tx).ok(); -} - -static TELEMETRY_QUEUE: OnceLock> = OnceLock::new(); diff --git a/crates/telemetry_events/Cargo.toml b/crates/telemetry_events/Cargo.toml deleted file mode 100644 index 6a5149c545..0000000000 --- a/crates/telemetry_events/Cargo.toml +++ /dev/null @@ -1,17 +0,0 @@ -[package] -name = "telemetry_events" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/telemetry_events.rs" - -[dependencies] -semver.workspace = true -serde.workspace = true -serde_json.workspace = true diff --git a/crates/telemetry_events/LICENSE-GPL b/crates/telemetry_events/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/telemetry_events/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/telemetry_events/src/telemetry_events.rs b/crates/telemetry_events/src/telemetry_events.rs deleted file mode 100644 index 83ec2c0644..0000000000 --- a/crates/telemetry_events/src/telemetry_events.rs +++ /dev/null @@ -1,126 +0,0 @@ -//! See [Telemetry in Zed](https://zed.dev/docs/telemetry) for additional information. - -use semver::Version; -use serde::{Deserialize, Serialize}; -use std::{collections::HashMap, fmt::Display, time::Duration}; - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct EventRequestBody { - /// Identifier unique to each system Zed is installed on - pub system_id: Option, - /// Identifier unique to each Zed installation (differs for stable, preview, dev) - pub installation_id: Option, - /// Identifier unique to each logged in Zed user (randomly generated on first sign in) - /// Identifier unique to each Zed session (differs for each time you open Zed) - pub session_id: Option, - pub metrics_id: Option, - /// True for Zed staff, otherwise false - #[serde(skip_serializing_if = "Option::is_none")] - pub is_staff: Option, - /// Zed version number - pub app_version: String, - pub os_name: String, - pub os_version: Option, - pub architecture: String, - /// Zed release channel (stable, preview, dev) - pub release_channel: Option, - pub events: Vec, -} - -impl EventRequestBody { - pub fn semver(&self) -> Option { - self.app_version.parse().ok() - } -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct EventWrapper { - pub signed_in: bool, - /// Duration between this event's timestamp and the timestamp of the first event in the current batch - pub milliseconds_since_first_event: i64, - /// The event itself - #[serde(flatten)] - pub event: Event, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum AssistantKind { - Panel, - Inline, - InlineTerminal, -} -impl Display for AssistantKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Self::Panel => "panel", - Self::Inline => "inline", - Self::InlineTerminal => "inline_terminal", - } - ) - } -} - -#[derive(Default, Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum AssistantPhase { - #[default] - Response, - Invoked, - Accepted, - Rejected, -} - -impl Display for AssistantPhase { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Self::Response => "response", - Self::Invoked => "invoked", - Self::Accepted => "accepted", - Self::Rejected => "rejected", - } - ) - } -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(tag = "type")] -pub enum Event { - Flexible(FlexibleEvent), -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct FlexibleEvent { - pub event_type: String, - pub event_properties: HashMap, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub enum EditPredictionRating { - Positive, - Negative, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct AssistantEventData { - /// Unique random identifier for each assistant tab (None for inline assist) - pub conversation_id: Option, - /// Server-generated message ID (only supported for some providers) - pub message_id: Option, - /// The kind of assistant (Panel, Inline) - pub kind: AssistantKind, - #[serde(default)] - pub phase: AssistantPhase, - /// Name of the AI model used (gpt-4o, claude-3-5-sonnet, etc) - pub model: String, - pub model_provider: String, - pub response_latency: Option, - pub error_message: Option, - pub language_name: Option, -} diff --git a/crates/terminal/Cargo.toml b/crates/terminal/Cargo.toml deleted file mode 100644 index 1266c5a6e5..0000000000 --- a/crates/terminal/Cargo.toml +++ /dev/null @@ -1,53 +0,0 @@ -[package] -name = "terminal" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[features] -test-support = [ - "collections/test-support", - "gpui/test-support", - "settings/test-support", -] - -[lints] -workspace = true - -[lib] -path = "src/terminal.rs" -doctest = false - -[dependencies] -alacritty_terminal.workspace = true -anyhow.workspace = true -collections.workspace = true -futures.workspace = true -gpui.workspace = true -itertools.workspace = true -libc.workspace = true -log.workspace = true -regex.workspace = true -release_channel.workspace = true -schemars.workspace = true -serde.workspace = true -settings.workspace = true -sysinfo.workspace = true -smol.workspace = true -task.workspace = true -theme.workspace = true -thiserror.workspace = true -util.workspace = true -urlencoding.workspace = true - -[target.'cfg(windows)'.dependencies] -windows.workspace = true - -[dev-dependencies] -gpui = { workspace = true, features = ["test-support"] } -rand.workspace = true -serde_json.workspace = true -settings = { workspace = true, features = ["test-support"] } -url.workspace = true -util_macros.workspace = true diff --git a/crates/terminal/LICENSE-GPL b/crates/terminal/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/terminal/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/terminal/src/mappings/colors.rs b/crates/terminal/src/mappings/colors.rs deleted file mode 100644 index 876e366606..0000000000 --- a/crates/terminal/src/mappings/colors.rs +++ /dev/null @@ -1,11 +0,0 @@ -use alacritty_terminal::vte::ansi::Rgb as AlacRgb; -use gpui::Rgba; - -//Convenience method to convert from a GPUI color to an alacritty Rgb -pub fn to_alac_rgb(color: impl Into) -> AlacRgb { - let color = color.into(); - let r = ((color.r * color.a) * 255.) as u8; - let g = ((color.g * color.a) * 255.) as u8; - let b = ((color.b * color.a) * 255.) as u8; - AlacRgb { r, g, b } -} diff --git a/crates/terminal/src/mappings/keys.rs b/crates/terminal/src/mappings/keys.rs deleted file mode 100644 index 8073961fc5..0000000000 --- a/crates/terminal/src/mappings/keys.rs +++ /dev/null @@ -1,449 +0,0 @@ -use std::borrow::Cow; - -/// The mappings defined in this file where created from reading the alacritty source -use alacritty_terminal::term::TermMode; -use gpui::Keystroke; - -#[derive(Debug, PartialEq, Eq)] -enum AlacModifiers { - None, - Alt, - Ctrl, - Shift, - CtrlShift, - Other, -} - -impl AlacModifiers { - fn new(ks: &Keystroke) -> Self { - match ( - ks.modifiers.alt, - ks.modifiers.control, - ks.modifiers.shift, - ks.modifiers.platform, - ) { - (false, false, false, false) => AlacModifiers::None, - (true, false, false, false) => AlacModifiers::Alt, - (false, true, false, false) => AlacModifiers::Ctrl, - (false, false, true, false) => AlacModifiers::Shift, - (false, true, true, false) => AlacModifiers::CtrlShift, - _ => AlacModifiers::Other, - } - } - - fn any(&self) -> bool { - match &self { - AlacModifiers::None => false, - AlacModifiers::Alt => true, - AlacModifiers::Ctrl => true, - AlacModifiers::Shift => true, - AlacModifiers::CtrlShift => true, - AlacModifiers::Other => true, - } - } -} - -pub fn to_esc_str( - keystroke: &Keystroke, - mode: &TermMode, - option_as_meta: bool, -) -> Option> { - let modifiers = AlacModifiers::new(keystroke); - - // Manual Bindings including modifiers - let manual_esc_str: Option<&'static str> = match (keystroke.key.as_ref(), &modifiers) { - //Basic special keys - ("tab", AlacModifiers::None) => Some("\x09"), - ("escape", AlacModifiers::None) => Some("\x1b"), - ("enter", AlacModifiers::None) => Some("\x0d"), - ("enter", AlacModifiers::Shift) => Some("\x0a"), - ("enter", AlacModifiers::Alt) => Some("\x1b\x0d"), - ("backspace", AlacModifiers::None) => Some("\x7f"), - //Interesting escape codes - ("tab", AlacModifiers::Shift) => Some("\x1b[Z"), - ("backspace", AlacModifiers::Ctrl) => Some("\x08"), - ("backspace", AlacModifiers::Alt) => Some("\x1b\x7f"), - ("backspace", AlacModifiers::Shift) => Some("\x7f"), - ("space", AlacModifiers::Ctrl) => Some("\x00"), - ("home", AlacModifiers::Shift) if mode.contains(TermMode::ALT_SCREEN) => Some("\x1b[1;2H"), - ("end", AlacModifiers::Shift) if mode.contains(TermMode::ALT_SCREEN) => Some("\x1b[1;2F"), - ("pageup", AlacModifiers::Shift) if mode.contains(TermMode::ALT_SCREEN) => { - Some("\x1b[5;2~") - } - ("pagedown", AlacModifiers::Shift) if mode.contains(TermMode::ALT_SCREEN) => { - Some("\x1b[6;2~") - } - ("home", AlacModifiers::None) if mode.contains(TermMode::APP_CURSOR) => Some("\x1bOH"), - ("home", AlacModifiers::None) if !mode.contains(TermMode::APP_CURSOR) => Some("\x1b[H"), - ("end", AlacModifiers::None) if mode.contains(TermMode::APP_CURSOR) => Some("\x1bOF"), - ("end", AlacModifiers::None) if !mode.contains(TermMode::APP_CURSOR) => Some("\x1b[F"), - ("up", AlacModifiers::None) if mode.contains(TermMode::APP_CURSOR) => Some("\x1bOA"), - ("up", AlacModifiers::None) if !mode.contains(TermMode::APP_CURSOR) => Some("\x1b[A"), - ("down", AlacModifiers::None) if mode.contains(TermMode::APP_CURSOR) => Some("\x1bOB"), - ("down", AlacModifiers::None) if !mode.contains(TermMode::APP_CURSOR) => Some("\x1b[B"), - ("right", AlacModifiers::None) if mode.contains(TermMode::APP_CURSOR) => Some("\x1bOC"), - ("right", AlacModifiers::None) if !mode.contains(TermMode::APP_CURSOR) => Some("\x1b[C"), - ("left", AlacModifiers::None) if mode.contains(TermMode::APP_CURSOR) => Some("\x1bOD"), - ("left", AlacModifiers::None) if !mode.contains(TermMode::APP_CURSOR) => Some("\x1b[D"), - ("back", AlacModifiers::None) => Some("\x7f"), - ("insert", AlacModifiers::None) => Some("\x1b[2~"), - ("delete", AlacModifiers::None) => Some("\x1b[3~"), - ("pageup", AlacModifiers::None) => Some("\x1b[5~"), - ("pagedown", AlacModifiers::None) => Some("\x1b[6~"), - ("f1", AlacModifiers::None) => Some("\x1bOP"), - ("f2", AlacModifiers::None) => Some("\x1bOQ"), - ("f3", AlacModifiers::None) => Some("\x1bOR"), - ("f4", AlacModifiers::None) => Some("\x1bOS"), - ("f5", AlacModifiers::None) => Some("\x1b[15~"), - ("f6", AlacModifiers::None) => Some("\x1b[17~"), - ("f7", AlacModifiers::None) => Some("\x1b[18~"), - ("f8", AlacModifiers::None) => Some("\x1b[19~"), - ("f9", AlacModifiers::None) => Some("\x1b[20~"), - ("f10", AlacModifiers::None) => Some("\x1b[21~"), - ("f11", AlacModifiers::None) => Some("\x1b[23~"), - ("f12", AlacModifiers::None) => Some("\x1b[24~"), - ("f13", AlacModifiers::None) => Some("\x1b[25~"), - ("f14", AlacModifiers::None) => Some("\x1b[26~"), - ("f15", AlacModifiers::None) => Some("\x1b[28~"), - ("f16", AlacModifiers::None) => Some("\x1b[29~"), - ("f17", AlacModifiers::None) => Some("\x1b[31~"), - ("f18", AlacModifiers::None) => Some("\x1b[32~"), - ("f19", AlacModifiers::None) => Some("\x1b[33~"), - ("f20", AlacModifiers::None) => Some("\x1b[34~"), - // NumpadEnter, Action::Esc("\n".into()); - //Mappings for caret notation keys - ("a", AlacModifiers::Ctrl) => Some("\x01"), //1 - ("A", AlacModifiers::CtrlShift) => Some("\x01"), //1 - ("b", AlacModifiers::Ctrl) => Some("\x02"), //2 - ("B", AlacModifiers::CtrlShift) => Some("\x02"), //2 - ("c", AlacModifiers::Ctrl) => Some("\x03"), //3 - ("C", AlacModifiers::CtrlShift) => Some("\x03"), //3 - ("d", AlacModifiers::Ctrl) => Some("\x04"), //4 - ("D", AlacModifiers::CtrlShift) => Some("\x04"), //4 - ("e", AlacModifiers::Ctrl) => Some("\x05"), //5 - ("E", AlacModifiers::CtrlShift) => Some("\x05"), //5 - ("f", AlacModifiers::Ctrl) => Some("\x06"), //6 - ("F", AlacModifiers::CtrlShift) => Some("\x06"), //6 - ("g", AlacModifiers::Ctrl) => Some("\x07"), //7 - ("G", AlacModifiers::CtrlShift) => Some("\x07"), //7 - ("h", AlacModifiers::Ctrl) => Some("\x08"), //8 - ("H", AlacModifiers::CtrlShift) => Some("\x08"), //8 - ("i", AlacModifiers::Ctrl) => Some("\x09"), //9 - ("I", AlacModifiers::CtrlShift) => Some("\x09"), //9 - ("j", AlacModifiers::Ctrl) => Some("\x0a"), //10 - ("J", AlacModifiers::CtrlShift) => Some("\x0a"), //10 - ("k", AlacModifiers::Ctrl) => Some("\x0b"), //11 - ("K", AlacModifiers::CtrlShift) => Some("\x0b"), //11 - ("l", AlacModifiers::Ctrl) => Some("\x0c"), //12 - ("L", AlacModifiers::CtrlShift) => Some("\x0c"), //12 - ("m", AlacModifiers::Ctrl) => Some("\x0d"), //13 - ("M", AlacModifiers::CtrlShift) => Some("\x0d"), //13 - ("n", AlacModifiers::Ctrl) => Some("\x0e"), //14 - ("N", AlacModifiers::CtrlShift) => Some("\x0e"), //14 - ("o", AlacModifiers::Ctrl) => Some("\x0f"), //15 - ("O", AlacModifiers::CtrlShift) => Some("\x0f"), //15 - ("p", AlacModifiers::Ctrl) => Some("\x10"), //16 - ("P", AlacModifiers::CtrlShift) => Some("\x10"), //16 - ("q", AlacModifiers::Ctrl) => Some("\x11"), //17 - ("Q", AlacModifiers::CtrlShift) => Some("\x11"), //17 - ("r", AlacModifiers::Ctrl) => Some("\x12"), //18 - ("R", AlacModifiers::CtrlShift) => Some("\x12"), //18 - ("s", AlacModifiers::Ctrl) => Some("\x13"), //19 - ("S", AlacModifiers::CtrlShift) => Some("\x13"), //19 - ("t", AlacModifiers::Ctrl) => Some("\x14"), //20 - ("T", AlacModifiers::CtrlShift) => Some("\x14"), //20 - ("u", AlacModifiers::Ctrl) => Some("\x15"), //21 - ("U", AlacModifiers::CtrlShift) => Some("\x15"), //21 - ("v", AlacModifiers::Ctrl) => Some("\x16"), //22 - ("V", AlacModifiers::CtrlShift) => Some("\x16"), //22 - ("w", AlacModifiers::Ctrl) => Some("\x17"), //23 - ("W", AlacModifiers::CtrlShift) => Some("\x17"), //23 - ("x", AlacModifiers::Ctrl) => Some("\x18"), //24 - ("X", AlacModifiers::CtrlShift) => Some("\x18"), //24 - ("y", AlacModifiers::Ctrl) => Some("\x19"), //25 - ("Y", AlacModifiers::CtrlShift) => Some("\x19"), //25 - ("z", AlacModifiers::Ctrl) => Some("\x1a"), //26 - ("Z", AlacModifiers::CtrlShift) => Some("\x1a"), //26 - ("@", AlacModifiers::Ctrl) => Some("\x00"), //0 - ("[", AlacModifiers::Ctrl) => Some("\x1b"), //27 - ("\\", AlacModifiers::Ctrl) => Some("\x1c"), //28 - ("]", AlacModifiers::Ctrl) => Some("\x1d"), //29 - ("^", AlacModifiers::Ctrl) => Some("\x1e"), //30 - ("_", AlacModifiers::Ctrl) => Some("\x1f"), //31 - ("?", AlacModifiers::Ctrl) => Some("\x7f"), //127 - _ => None, - }; - if let Some(esc_str) = manual_esc_str { - return Some(Cow::Borrowed(esc_str)); - } - - // Automated bindings applying modifiers - if modifiers.any() { - let modifier_code = modifier_code(keystroke); - let modified_esc_str = match keystroke.key.as_ref() { - "up" => Some(format!("\x1b[1;{}A", modifier_code)), - "down" => Some(format!("\x1b[1;{}B", modifier_code)), - "right" => Some(format!("\x1b[1;{}C", modifier_code)), - "left" => Some(format!("\x1b[1;{}D", modifier_code)), - "f1" => Some(format!("\x1b[1;{}P", modifier_code)), - "f2" => Some(format!("\x1b[1;{}Q", modifier_code)), - "f3" => Some(format!("\x1b[1;{}R", modifier_code)), - "f4" => Some(format!("\x1b[1;{}S", modifier_code)), - "F5" => Some(format!("\x1b[15;{}~", modifier_code)), - "f6" => Some(format!("\x1b[17;{}~", modifier_code)), - "f7" => Some(format!("\x1b[18;{}~", modifier_code)), - "f8" => Some(format!("\x1b[19;{}~", modifier_code)), - "f9" => Some(format!("\x1b[20;{}~", modifier_code)), - "f10" => Some(format!("\x1b[21;{}~", modifier_code)), - "f11" => Some(format!("\x1b[23;{}~", modifier_code)), - "f12" => Some(format!("\x1b[24;{}~", modifier_code)), - "f13" => Some(format!("\x1b[25;{}~", modifier_code)), - "f14" => Some(format!("\x1b[26;{}~", modifier_code)), - "f15" => Some(format!("\x1b[28;{}~", modifier_code)), - "f16" => Some(format!("\x1b[29;{}~", modifier_code)), - "f17" => Some(format!("\x1b[31;{}~", modifier_code)), - "f18" => Some(format!("\x1b[32;{}~", modifier_code)), - "f19" => Some(format!("\x1b[33;{}~", modifier_code)), - "f20" => Some(format!("\x1b[34;{}~", modifier_code)), - _ if modifier_code == 2 => None, - "insert" => Some(format!("\x1b[2;{}~", modifier_code)), - "pageup" => Some(format!("\x1b[5;{}~", modifier_code)), - "pagedown" => Some(format!("\x1b[6;{}~", modifier_code)), - "end" => Some(format!("\x1b[1;{}F", modifier_code)), - "home" => Some(format!("\x1b[1;{}H", modifier_code)), - _ => None, - }; - if let Some(esc_str) = modified_esc_str { - return Some(Cow::Owned(esc_str)); - } - } - - if !cfg!(target_os = "macos") || option_as_meta { - let is_alt_lowercase_ascii = modifiers == AlacModifiers::Alt && keystroke.key.is_ascii(); - let is_alt_uppercase_ascii = - keystroke.modifiers.alt && keystroke.modifiers.shift && keystroke.key.is_ascii(); - if is_alt_lowercase_ascii || is_alt_uppercase_ascii { - let key = if is_alt_uppercase_ascii { - &keystroke.key.to_ascii_uppercase() - } else { - &keystroke.key - }; - return Some(Cow::Owned(format!("\x1b{}", key))); - } - } - - None -} - -/// Code Modifiers -/// ---------+--------------------------- -/// 2 | Shift -/// 3 | Alt -/// 4 | Shift + Alt -/// 5 | Control -/// 6 | Shift + Control -/// 7 | Alt + Control -/// 8 | Shift + Alt + Control -/// ---------+--------------------------- -/// from: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys -fn modifier_code(keystroke: &Keystroke) -> u32 { - let mut modifier_code = 0; - if keystroke.modifiers.shift { - modifier_code |= 1; - } - if keystroke.modifiers.alt { - modifier_code |= 1 << 1; - } - if keystroke.modifiers.control { - modifier_code |= 1 << 2; - } - modifier_code + 1 -} - -#[cfg(test)] -mod test { - use gpui::Modifiers; - - use super::*; - - #[test] - fn test_scroll_keys() { - //These keys should be handled by the scrolling element directly - //Need to signify this by returning 'None' - let shift_pageup = Keystroke::parse("shift-pageup").unwrap(); - let shift_pagedown = Keystroke::parse("shift-pagedown").unwrap(); - let shift_home = Keystroke::parse("shift-home").unwrap(); - let shift_end = Keystroke::parse("shift-end").unwrap(); - - let none = TermMode::NONE; - assert_eq!(to_esc_str(&shift_pageup, &none, false), None); - assert_eq!(to_esc_str(&shift_pagedown, &none, false), None); - assert_eq!(to_esc_str(&shift_home, &none, false), None); - assert_eq!(to_esc_str(&shift_end, &none, false), None); - - let alt_screen = TermMode::ALT_SCREEN; - assert_eq!( - to_esc_str(&shift_pageup, &alt_screen, false), - Some("\x1b[5;2~".into()) - ); - assert_eq!( - to_esc_str(&shift_pagedown, &alt_screen, false), - Some("\x1b[6;2~".into()) - ); - assert_eq!( - to_esc_str(&shift_home, &alt_screen, false), - Some("\x1b[1;2H".into()) - ); - assert_eq!( - to_esc_str(&shift_end, &alt_screen, false), - Some("\x1b[1;2F".into()) - ); - - let pageup = Keystroke::parse("pageup").unwrap(); - let pagedown = Keystroke::parse("pagedown").unwrap(); - let any = TermMode::ANY; - - assert_eq!(to_esc_str(&pageup, &any, false), Some("\x1b[5~".into())); - assert_eq!(to_esc_str(&pagedown, &any, false), Some("\x1b[6~".into())); - } - - #[test] - fn test_plain_inputs() { - let ks = Keystroke { - modifiers: Modifiers { - control: false, - alt: false, - shift: false, - platform: false, - function: false, - }, - key: "🖖🏻".to_string(), //2 char string - key_char: None, - }; - assert_eq!(to_esc_str(&ks, &TermMode::NONE, false), None); - } - - #[test] - fn test_application_mode() { - let app_cursor = TermMode::APP_CURSOR; - let none = TermMode::NONE; - - let up = Keystroke::parse("up").unwrap(); - let down = Keystroke::parse("down").unwrap(); - let left = Keystroke::parse("left").unwrap(); - let right = Keystroke::parse("right").unwrap(); - - assert_eq!(to_esc_str(&up, &none, false), Some("\x1b[A".into())); - assert_eq!(to_esc_str(&down, &none, false), Some("\x1b[B".into())); - assert_eq!(to_esc_str(&right, &none, false), Some("\x1b[C".into())); - assert_eq!(to_esc_str(&left, &none, false), Some("\x1b[D".into())); - - assert_eq!(to_esc_str(&up, &app_cursor, false), Some("\x1bOA".into())); - assert_eq!(to_esc_str(&down, &app_cursor, false), Some("\x1bOB".into())); - assert_eq!( - to_esc_str(&right, &app_cursor, false), - Some("\x1bOC".into()) - ); - assert_eq!(to_esc_str(&left, &app_cursor, false), Some("\x1bOD".into())); - } - - #[test] - fn test_ctrl_codes() { - let letters_lower = 'a'..='z'; - let letters_upper = 'A'..='Z'; - let mode = TermMode::ANY; - - for (lower, upper) in letters_lower.zip(letters_upper) { - assert_eq!( - to_esc_str( - &Keystroke::parse(&format!("ctrl-shift-{}", lower)).unwrap(), - &mode, - false - ), - to_esc_str( - &Keystroke::parse(&format!("ctrl-{}", upper)).unwrap(), - &mode, - false - ), - "On letter: {}/{}", - lower, - upper - ) - } - } - - #[test] - fn alt_is_meta() { - let ascii_printable = ' '..='~'; - for character in ascii_printable { - assert_eq!( - to_esc_str( - &Keystroke::parse(&format!("alt-{}", character)).unwrap(), - &TermMode::NONE, - true - ) - .unwrap(), - format!("\x1b{}", character) - ); - } - - let gpui_keys = [ - "up", "down", "right", "left", "f1", "f2", "f3", "f4", "F5", "f6", "f7", "f8", "f9", - "f10", "f11", "f12", "f13", "f14", "f15", "f16", "f17", "f18", "f19", "f20", "insert", - "pageup", "pagedown", "end", "home", - ]; - - for key in gpui_keys { - assert_ne!( - to_esc_str( - &Keystroke::parse(&format!("alt-{}", key)).unwrap(), - &TermMode::NONE, - true - ) - .unwrap(), - format!("\x1b{}", key) - ); - } - } - - #[test] - fn test_shift_enter_newline() { - let shift_enter = Keystroke::parse("shift-enter").unwrap(); - let regular_enter = Keystroke::parse("enter").unwrap(); - let mode = TermMode::NONE; - - // Shift-enter should send line feed (newline) - assert_eq!(to_esc_str(&shift_enter, &mode, false), Some("\x0a".into())); - - // Regular enter should still send carriage return - assert_eq!( - to_esc_str(®ular_enter, &mode, false), - Some("\x0d".into()) - ); - } - - #[test] - fn test_modifier_code_calc() { - // Code Modifiers - // ---------+--------------------------- - // 2 | Shift - // 3 | Alt - // 4 | Shift + Alt - // 5 | Control - // 6 | Shift + Control - // 7 | Alt + Control - // 8 | Shift + Alt + Control - // ---------+--------------------------- - // from: https://invisible-island.net/xterm/ctlseqs/ctlseqs.html#h2-PC-Style-Function-Keys - assert_eq!(2, modifier_code(&Keystroke::parse("shift-a").unwrap())); - assert_eq!(3, modifier_code(&Keystroke::parse("alt-a").unwrap())); - assert_eq!(4, modifier_code(&Keystroke::parse("shift-alt-a").unwrap())); - assert_eq!(5, modifier_code(&Keystroke::parse("ctrl-a").unwrap())); - assert_eq!(6, modifier_code(&Keystroke::parse("shift-ctrl-a").unwrap())); - assert_eq!(7, modifier_code(&Keystroke::parse("alt-ctrl-a").unwrap())); - assert_eq!( - 8, - modifier_code(&Keystroke::parse("shift-ctrl-alt-a").unwrap()) - ); - } -} diff --git a/crates/terminal/src/mappings/mod.rs b/crates/terminal/src/mappings/mod.rs deleted file mode 100644 index d58dd27f96..0000000000 --- a/crates/terminal/src/mappings/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod colors; -pub mod keys; -pub mod mouse; diff --git a/crates/terminal/src/mappings/mouse.rs b/crates/terminal/src/mappings/mouse.rs deleted file mode 100644 index 8c3eed8b54..0000000000 --- a/crates/terminal/src/mappings/mouse.rs +++ /dev/null @@ -1,277 +0,0 @@ -use std::cmp::{self, max, min}; -use std::iter::repeat; - -use alacritty_terminal::grid::Dimensions; -/// Most of the code, and specifically the constants, in this are copied from Alacritty, -/// with modifications for our circumstances -use alacritty_terminal::index::{Column as GridCol, Line as GridLine, Point as AlacPoint, Side}; -use alacritty_terminal::term::TermMode; -use gpui::{Modifiers, MouseButton, Pixels, Point, ScrollWheelEvent, px}; - -use crate::TerminalBounds; - -enum MouseFormat { - Sgr, - Normal(bool), -} - -impl MouseFormat { - fn from_mode(mode: TermMode) -> Self { - if mode.contains(TermMode::SGR_MOUSE) { - MouseFormat::Sgr - } else if mode.contains(TermMode::UTF8_MOUSE) { - MouseFormat::Normal(true) - } else { - MouseFormat::Normal(false) - } - } -} - -#[derive(Debug)] -enum AlacMouseButton { - LeftButton = 0, - MiddleButton = 1, - RightButton = 2, - LeftMove = 32, - MiddleMove = 33, - RightMove = 34, - NoneMove = 35, - ScrollUp = 64, - ScrollDown = 65, - Other = 99, -} - -impl AlacMouseButton { - fn from_move_button(e: Option) -> Self { - match e { - Some(gpui::MouseButton::Left) => AlacMouseButton::LeftMove, - Some(gpui::MouseButton::Middle) => AlacMouseButton::MiddleMove, - Some(gpui::MouseButton::Right) => AlacMouseButton::RightMove, - Some(gpui::MouseButton::Navigate(_)) => AlacMouseButton::Other, - None => AlacMouseButton::NoneMove, - } - } - - fn from_button(e: MouseButton) -> Self { - match e { - gpui::MouseButton::Left => AlacMouseButton::LeftButton, - gpui::MouseButton::Right => AlacMouseButton::MiddleButton, - gpui::MouseButton::Middle => AlacMouseButton::RightButton, - gpui::MouseButton::Navigate(_) => AlacMouseButton::Other, - } - } - - fn from_scroll(e: &ScrollWheelEvent) -> Self { - let is_positive = match e.delta { - gpui::ScrollDelta::Pixels(pixels) => pixels.y > px(0.), - gpui::ScrollDelta::Lines(lines) => lines.y > 0., - }; - - if is_positive { - AlacMouseButton::ScrollUp - } else { - AlacMouseButton::ScrollDown - } - } - - fn is_other(&self) -> bool { - matches!(self, AlacMouseButton::Other) - } -} - -pub fn scroll_report( - point: AlacPoint, - scroll_lines: i32, - e: &ScrollWheelEvent, - mode: TermMode, -) -> Option>> { - if mode.intersects(TermMode::MOUSE_MODE) { - mouse_report( - point, - AlacMouseButton::from_scroll(e), - true, - e.modifiers, - MouseFormat::from_mode(mode), - ) - .map(|report| repeat(report).take(max(scroll_lines, 1) as usize)) - } else { - None - } -} - -pub fn alt_scroll(scroll_lines: i32) -> Vec { - let cmd = if scroll_lines > 0 { b'A' } else { b'B' }; - - let mut content = Vec::with_capacity(scroll_lines.unsigned_abs() as usize * 3); - for _ in 0..scroll_lines.abs() { - content.push(0x1b); - content.push(b'O'); - content.push(cmd); - } - content -} - -pub fn mouse_button_report( - point: AlacPoint, - button: gpui::MouseButton, - modifiers: Modifiers, - pressed: bool, - mode: TermMode, -) -> Option> { - let button = AlacMouseButton::from_button(button); - if !button.is_other() && mode.intersects(TermMode::MOUSE_MODE) { - mouse_report( - point, - button, - pressed, - modifiers, - MouseFormat::from_mode(mode), - ) - } else { - None - } -} - -pub fn mouse_moved_report( - point: AlacPoint, - button: Option, - modifiers: Modifiers, - mode: TermMode, -) -> Option> { - let button = AlacMouseButton::from_move_button(button); - - if !button.is_other() && mode.intersects(TermMode::MOUSE_MOTION | TermMode::MOUSE_DRAG) { - //Only drags are reported in drag mode, so block NoneMove. - if mode.contains(TermMode::MOUSE_DRAG) && matches!(button, AlacMouseButton::NoneMove) { - None - } else { - mouse_report(point, button, true, modifiers, MouseFormat::from_mode(mode)) - } - } else { - None - } -} - -pub fn grid_point( - pos: Point, - cur_size: TerminalBounds, - display_offset: usize, -) -> AlacPoint { - grid_point_and_side(pos, cur_size, display_offset).0 -} - -pub fn grid_point_and_side( - pos: Point, - cur_size: TerminalBounds, - display_offset: usize, -) -> (AlacPoint, Side) { - let mut col = GridCol((pos.x / cur_size.cell_width) as usize); - let cell_x = cmp::max(px(0.), pos.x) % cur_size.cell_width; - let half_cell_width = cur_size.cell_width / 2.0; - let mut side = if cell_x > half_cell_width { - Side::Right - } else { - Side::Left - }; - - if col > cur_size.last_column() { - col = cur_size.last_column(); - side = Side::Right; - } - let col = min(col, cur_size.last_column()); - let mut line = (pos.y / cur_size.line_height) as i32; - if line > cur_size.bottommost_line() { - line = cur_size.bottommost_line().0; - side = Side::Right; - } else if line < 0 { - side = Side::Left; - } - - ( - AlacPoint::new(GridLine(line - display_offset as i32), col), - side, - ) -} - -///Generate the bytes to send to the terminal, from the cell location, a mouse event, and the terminal mode -fn mouse_report( - point: AlacPoint, - button: AlacMouseButton, - pressed: bool, - modifiers: Modifiers, - format: MouseFormat, -) -> Option> { - if point.line < 0 { - return None; - } - - let mut mods = 0; - if modifiers.shift { - mods += 4; - } - if modifiers.alt { - mods += 8; - } - if modifiers.control { - mods += 16; - } - - match format { - MouseFormat::Sgr => { - Some(sgr_mouse_report(point, button as u8 + mods, pressed).into_bytes()) - } - MouseFormat::Normal(utf8) => { - if pressed { - normal_mouse_report(point, button as u8 + mods, utf8) - } else { - normal_mouse_report(point, 3 + mods, utf8) - } - } - } -} - -fn normal_mouse_report(point: AlacPoint, button: u8, utf8: bool) -> Option> { - let AlacPoint { line, column } = point; - let max_point = if utf8 { 2015 } else { 223 }; - - if line >= max_point || column >= max_point { - return None; - } - - let mut msg = vec![b'\x1b', b'[', b'M', 32 + button]; - - let mouse_pos_encode = |pos: usize| -> Vec { - let pos = 32 + 1 + pos; - let first = 0xC0 + pos / 64; - let second = 0x80 + (pos & 63); - vec![first as u8, second as u8] - }; - - if utf8 && column >= 95 { - msg.append(&mut mouse_pos_encode(column.0)); - } else { - msg.push(32 + 1 + column.0 as u8); - } - - if utf8 && line >= 95 { - msg.append(&mut mouse_pos_encode(line.0 as usize)); - } else { - msg.push(32 + 1 + line.0 as u8); - } - - Some(msg) -} - -fn sgr_mouse_report(point: AlacPoint, button: u8, pressed: bool) -> String { - let c = if pressed { 'M' } else { 'm' }; - - let msg = format!( - "\x1b[<{};{};{}{}", - button, - point.column + 1, - point.line + 1, - c - ); - - msg -} diff --git a/crates/terminal/src/pty_info.rs b/crates/terminal/src/pty_info.rs deleted file mode 100644 index c92de2f23b..0000000000 --- a/crates/terminal/src/pty_info.rs +++ /dev/null @@ -1,170 +0,0 @@ -use alacritty_terminal::tty::Pty; -#[cfg(target_os = "windows")] -use std::num::NonZeroU32; -#[cfg(unix)] -use std::os::fd::AsRawFd; -use std::path::PathBuf; - -#[cfg(target_os = "windows")] -use windows::Win32::{Foundation::HANDLE, System::Threading::GetProcessId}; - -use sysinfo::{Pid, Process, ProcessRefreshKind, RefreshKind, System, UpdateKind}; - -pub struct ProcessIdGetter { - handle: i32, - fallback_pid: u32, -} - -impl ProcessIdGetter { - pub fn fallback_pid(&self) -> Pid { - Pid::from_u32(self.fallback_pid) - } -} - -#[cfg(unix)] -impl ProcessIdGetter { - fn new(pty: &Pty) -> ProcessIdGetter { - ProcessIdGetter { - handle: pty.file().as_raw_fd(), - fallback_pid: pty.child().id(), - } - } - - fn pid(&self) -> Option { - let pid = unsafe { libc::tcgetpgrp(self.handle) }; - if pid < 0 { - return Some(Pid::from_u32(self.fallback_pid)); - } - Some(Pid::from_u32(pid as u32)) - } -} - -#[cfg(windows)] -impl ProcessIdGetter { - fn new(pty: &Pty) -> ProcessIdGetter { - let child = pty.child_watcher(); - let handle = child.raw_handle(); - let fallback_pid = child.pid().unwrap_or_else(|| unsafe { - NonZeroU32::new_unchecked(GetProcessId(HANDLE(handle as _))) - }); - - ProcessIdGetter { - handle: handle as i32, - fallback_pid: u32::from(fallback_pid), - } - } - - fn pid(&self) -> Option { - let pid = unsafe { GetProcessId(HANDLE(self.handle as _)) }; - // the GetProcessId may fail and returns zero, which will lead to a stack overflow issue - if pid == 0 { - // in the builder process, there is a small chance, almost negligible, - // that this value could be zero, which means child_watcher returns None, - // GetProcessId returns 0. - if self.fallback_pid == 0 { - return None; - } - return Some(Pid::from_u32(self.fallback_pid)); - } - Some(Pid::from_u32(pid)) - } -} - -#[derive(Clone, Debug)] -pub struct ProcessInfo { - pub name: String, - pub cwd: PathBuf, - pub argv: Vec, -} - -/// Fetches Zed-relevant Pseudo-Terminal (PTY) process information -pub struct PtyProcessInfo { - system: System, - refresh_kind: ProcessRefreshKind, - pid_getter: ProcessIdGetter, - pub current: Option, -} - -impl PtyProcessInfo { - pub fn new(pty: &Pty) -> PtyProcessInfo { - let process_refresh_kind = ProcessRefreshKind::nothing() - .with_cmd(UpdateKind::Always) - .with_cwd(UpdateKind::Always) - .with_exe(UpdateKind::Always); - let refresh_kind = RefreshKind::nothing().with_processes(process_refresh_kind); - let system = System::new_with_specifics(refresh_kind); - - PtyProcessInfo { - system, - refresh_kind: process_refresh_kind, - pid_getter: ProcessIdGetter::new(pty), - current: None, - } - } - - pub fn pid_getter(&self) -> &ProcessIdGetter { - &self.pid_getter - } - - fn refresh(&mut self) -> Option<&Process> { - let pid = self.pid_getter.pid()?; - if self.system.refresh_processes_specifics( - sysinfo::ProcessesToUpdate::Some(&[pid]), - true, - self.refresh_kind, - ) == 1 - { - self.system.process(pid) - } else { - None - } - } - - fn get_child(&self) -> Option<&Process> { - let pid = self.pid_getter.fallback_pid(); - self.system.process(pid) - } - - pub(crate) fn kill_current_process(&mut self) -> bool { - self.refresh().is_some_and(|process| process.kill()) - } - - pub(crate) fn kill_child_process(&mut self) -> bool { - self.get_child().is_some_and(|process| process.kill()) - } - - fn load(&mut self) -> Option { - let process = self.refresh()?; - let cwd = process.cwd().map_or(PathBuf::new(), |p| p.to_owned()); - - let info = ProcessInfo { - name: process.name().to_str()?.to_owned(), - cwd, - argv: process - .cmd() - .iter() - .filter_map(|s| s.to_str().map(ToOwned::to_owned)) - .collect(), - }; - self.current = Some(info.clone()); - Some(info) - } - - /// Updates the cached process info, returns whether the Zed-relevant info has changed - pub fn has_changed(&mut self) -> bool { - let current = self.load(); - let has_changed = match (self.current.as_ref(), current.as_ref()) { - (None, None) => false, - (Some(prev), Some(now)) => prev.cwd != now.cwd || prev.name != now.name, - _ => true, - }; - if has_changed { - self.current = current; - } - has_changed - } - - pub fn pid(&self) -> Option { - self.pid_getter.pid() - } -} diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs deleted file mode 100644 index caca93eac5..0000000000 --- a/crates/terminal/src/terminal.rs +++ /dev/null @@ -1,2854 +0,0 @@ -pub mod mappings; - -pub use alacritty_terminal; - -mod pty_info; -mod terminal_hyperlinks; -pub mod terminal_settings; - -use alacritty_terminal::{ - Term, - event::{Event as AlacTermEvent, EventListener, Notify, WindowSize}, - event_loop::{EventLoop, Msg, Notifier}, - grid::{Dimensions, Grid, Row, Scroll as AlacScroll}, - index::{Boundary, Column, Direction as AlacDirection, Line, Point as AlacPoint}, - selection::{Selection, SelectionRange, SelectionType}, - sync::FairMutex, - term::{ - Config, RenderableCursor, TermMode, - cell::{Cell, Flags}, - search::{Match, RegexIter, RegexSearch}, - }, - tty::{self}, - vi_mode::{ViModeCursor, ViMotion}, - vte::ansi::{ - ClearMode, CursorStyle as AlacCursorStyle, Handler, NamedPrivateMode, PrivateMode, - }, -}; -use anyhow::{Context as _, Result, bail}; -use log::trace; - -use futures::{ - FutureExt, - channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded}, -}; - -use itertools::Itertools as _; -use mappings::mouse::{ - alt_scroll, grid_point, grid_point_and_side, mouse_button_report, mouse_moved_report, - scroll_report, -}; - -use collections::{HashMap, VecDeque}; -use futures::StreamExt; -use pty_info::{ProcessIdGetter, PtyProcessInfo}; -use serde::{Deserialize, Serialize}; -use settings::Settings; -use smol::channel::{Receiver, Sender}; -use task::{HideStrategy, Shell, SpawnInTerminal}; -use terminal_hyperlinks::RegexSearches; -use terminal_settings::{AlternateScroll, CursorShape, TerminalSettings}; -use theme::{ActiveTheme, Theme}; -use urlencoding; -use util::truncate_and_trailoff; - -use std::{ - borrow::Cow, - cmp::{self, min}, - fmt::Display, - ops::{Deref, RangeInclusive}, - path::PathBuf, - process::ExitStatus, - sync::Arc, - time::Instant, -}; -use thiserror::Error; - -use gpui::{ - App, AppContext as _, Bounds, ClipboardItem, Context, EventEmitter, Hsla, Keystroke, Modifiers, - MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, Rgba, - ScrollWheelEvent, Size, Task, TouchPhase, Window, actions, black, px, -}; - -use crate::mappings::{colors::to_alac_rgb, keys::to_esc_str}; - -actions!( - terminal, - [ - /// Clears the terminal screen. - Clear, - /// Copies selected text to the clipboard. - Copy, - /// Pastes from the clipboard. - Paste, - /// Shows the character palette for special characters. - ShowCharacterPalette, - /// Searches for text in the terminal. - SearchTest, - /// Scrolls up by one line. - ScrollLineUp, - /// Scrolls down by one line. - ScrollLineDown, - /// Scrolls up by one page. - ScrollPageUp, - /// Scrolls down by one page. - ScrollPageDown, - /// Scrolls up by half a page. - ScrollHalfPageUp, - /// Scrolls down by half a page. - ScrollHalfPageDown, - /// Scrolls to the top of the terminal buffer. - ScrollToTop, - /// Scrolls to the bottom of the terminal buffer. - ScrollToBottom, - /// Toggles vi mode in the terminal. - ToggleViMode, - /// Selects all text in the terminal. - SelectAll, - ] -); - -const DEBUG_TERMINAL_WIDTH: Pixels = px(500.); -const DEBUG_TERMINAL_HEIGHT: Pixels = px(30.); -const DEBUG_CELL_WIDTH: Pixels = px(5.); -const DEBUG_LINE_HEIGHT: Pixels = px(5.); - -///Upward flowing events, for changing the title and such -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Event { - TitleChanged, - BreadcrumbsChanged, - CloseTerminal, - Bell, - Wakeup, - BlinkChanged(bool), - SelectionsChanged, - NewNavigationTarget(Option), - Open(MaybeNavigationTarget), -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct PathLikeTarget { - /// File system path, absolute or relative, existing or not. - /// Might have line and column number(s) attached as `file.rs:1:23` - pub maybe_path: String, - /// Current working directory of the terminal - pub terminal_dir: Option, -} - -/// A string inside terminal, potentially useful as a URI that can be opened. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum MaybeNavigationTarget { - /// HTTP, git, etc. string determined by the `URL_REGEX` regex. - Url(String), - /// File system path, absolute or relative, existing or not. - /// Might have line and column number(s) attached as `file.rs:1:23` - PathLike(PathLikeTarget), -} - -#[derive(Clone)] -enum InternalEvent { - Resize(TerminalBounds), - Clear, - // FocusNextMatch, - Scroll(AlacScroll), - ScrollToAlacPoint(AlacPoint), - SetSelection(Option<(Selection, AlacPoint)>), - UpdateSelection(Point), - // Adjusted mouse position, should open - FindHyperlink(Point, bool), - // Whether keep selection when copy - Copy(Option), - // Vi mode events - ToggleViMode, - ViMotion(ViMotion), - MoveViCursorToAlacPoint(AlacPoint), -} - -///A translation struct for Alacritty to communicate with us from their event loop -#[derive(Clone)] -pub struct ZedListener(pub UnboundedSender); - -impl EventListener for ZedListener { - fn send_event(&self, event: AlacTermEvent) { - self.0.unbounded_send(event).ok(); - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct TerminalBounds { - pub cell_width: Pixels, - pub line_height: Pixels, - pub bounds: Bounds, -} - -impl TerminalBounds { - pub fn new(line_height: Pixels, cell_width: Pixels, bounds: Bounds) -> Self { - TerminalBounds { - cell_width, - line_height, - bounds, - } - } - - pub fn num_lines(&self) -> usize { - (self.bounds.size.height / self.line_height).floor() as usize - } - - pub fn num_columns(&self) -> usize { - (self.bounds.size.width / self.cell_width).floor() as usize - } - - pub fn height(&self) -> Pixels { - self.bounds.size.height - } - - pub fn width(&self) -> Pixels { - self.bounds.size.width - } - - pub fn cell_width(&self) -> Pixels { - self.cell_width - } - - pub fn line_height(&self) -> Pixels { - self.line_height - } -} - -impl Default for TerminalBounds { - fn default() -> Self { - TerminalBounds::new( - DEBUG_LINE_HEIGHT, - DEBUG_CELL_WIDTH, - Bounds { - origin: Point::default(), - size: Size { - width: DEBUG_TERMINAL_WIDTH, - height: DEBUG_TERMINAL_HEIGHT, - }, - }, - ) - } -} - -impl From for WindowSize { - fn from(val: TerminalBounds) -> Self { - WindowSize { - num_lines: val.num_lines() as u16, - num_cols: val.num_columns() as u16, - cell_width: f32::from(val.cell_width()) as u16, - cell_height: f32::from(val.line_height()) as u16, - } - } -} - -impl Dimensions for TerminalBounds { - /// Note: this is supposed to be for the back buffer's length, - /// but we exclusively use it to resize the terminal, which does not - /// use this method. We still have to implement it for the trait though, - /// hence, this comment. - fn total_lines(&self) -> usize { - self.screen_lines() - } - - fn screen_lines(&self) -> usize { - self.num_lines() - } - - fn columns(&self) -> usize { - self.num_columns() - } -} - -#[derive(Error, Debug)] -pub struct TerminalError { - pub directory: Option, - pub program: Option, - pub args: Option>, - pub title_override: Option, - pub source: std::io::Error, -} - -impl TerminalError { - pub fn fmt_directory(&self) -> String { - self.directory - .clone() - .map(|path| { - match path - .into_os_string() - .into_string() - .map_err(|os_str| format!(" {}", os_str.to_string_lossy())) - { - Ok(s) => s, - Err(s) => s, - } - }) - .unwrap_or_else(|| "".to_string()) - } - - pub fn fmt_shell(&self) -> String { - if let Some(title_override) = &self.title_override { - format!( - "{} {} ({})", - self.program.as_deref().unwrap_or(""), - self.args.as_ref().into_iter().flatten().format(" "), - title_override - ) - } else { - format!( - "{} {}", - self.program.as_deref().unwrap_or(""), - self.args.as_ref().into_iter().flatten().format(" ") - ) - } - } -} - -impl Display for TerminalError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let dir_string: String = self.fmt_directory(); - let shell = self.fmt_shell(); - - write!( - f, - "Working directory: {} Shell command: `{}`, IOError: {}", - dir_string, shell, self.source - ) - } -} - -// https://github.com/alacritty/alacritty/blob/cb3a79dbf6472740daca8440d5166c1d4af5029e/extra/man/alacritty.5.scd?plain=1#L207-L213 -const DEFAULT_SCROLL_HISTORY_LINES: usize = 10_000; -pub const MAX_SCROLL_HISTORY_LINES: usize = 100_000; - -pub struct TerminalBuilder { - terminal: Terminal, - events_rx: UnboundedReceiver, -} - -impl TerminalBuilder { - pub fn new_display_only( - cursor_shape: CursorShape, - alternate_scroll: AlternateScroll, - max_scroll_history_lines: Option, - window_id: u64, - ) -> Result { - // Create a display-only terminal (no actual PTY). - let default_cursor_style = AlacCursorStyle::from(cursor_shape); - let scrolling_history = max_scroll_history_lines - .unwrap_or(DEFAULT_SCROLL_HISTORY_LINES) - .min(MAX_SCROLL_HISTORY_LINES); - let config = Config { - scrolling_history, - default_cursor_style, - ..Config::default() - }; - - let (events_tx, events_rx) = unbounded(); - let mut term = Term::new( - config.clone(), - &TerminalBounds::default(), - ZedListener(events_tx), - ); - - if let AlternateScroll::Off = alternate_scroll { - term.unset_private_mode(PrivateMode::Named(NamedPrivateMode::AlternateScroll)); - } - - let term = Arc::new(FairMutex::new(term)); - - let terminal = Terminal { - task: None, - terminal_type: TerminalType::DisplayOnly, - completion_tx: None, - term, - term_config: config, - title_override: None, - events: VecDeque::with_capacity(10), - last_content: Default::default(), - last_mouse: None, - matches: Vec::new(), - selection_head: None, - breadcrumb_text: String::new(), - scroll_px: px(0.), - next_link_id: 0, - selection_phase: SelectionPhase::Ended, - hyperlink_regex_searches: RegexSearches::default(), - vi_mode_enabled: false, - is_remote_terminal: false, - last_mouse_move_time: Instant::now(), - last_hyperlink_search_position: None, - #[cfg(windows)] - shell_program: None, - activation_script: Vec::new(), - template: CopyTemplate { - shell: Shell::System, - env: HashMap::default(), - cursor_shape, - alternate_scroll, - max_scroll_history_lines, - path_hyperlink_regexes: Vec::default(), - path_hyperlink_timeout_ms: 0, - window_id, - }, - child_exited: None, - event_loop_task: Task::ready(Ok(())), - }; - - Ok(TerminalBuilder { - terminal, - events_rx, - }) - } - - pub fn new( - working_directory: Option, - task: Option, - shell: Shell, - mut env: HashMap, - cursor_shape: CursorShape, - alternate_scroll: AlternateScroll, - max_scroll_history_lines: Option, - path_hyperlink_regexes: Vec, - path_hyperlink_timeout_ms: u64, - is_remote_terminal: bool, - window_id: u64, - completion_tx: Option>>, - cx: &App, - activation_script: Vec, - ) -> Task> { - let version = release_channel::AppVersion::global(cx); - let fut = async move { - // If the parent environment doesn't have a locale set - // (As is the case when launched from a .app on MacOS), - // and the Project doesn't have a locale set, then - // set a fallback for our child environment to use. - if std::env::var("LANG").is_err() { - env.entry("LANG".to_string()) - .or_insert_with(|| "en_US.UTF-8".to_string()); - } - - env.insert("ZED_TERM".to_string(), "true".to_string()); - env.insert("TERM_PROGRAM".to_string(), "zed".to_string()); - env.insert("TERM".to_string(), "xterm-256color".to_string()); - env.insert("COLORTERM".to_string(), "truecolor".to_string()); - env.insert("TERM_PROGRAM_VERSION".to_string(), version.to_string()); - - #[derive(Default)] - struct ShellParams { - program: String, - args: Option>, - title_override: Option, - } - - impl ShellParams { - fn new( - program: String, - args: Option>, - title_override: Option, - ) -> Self { - log::debug!("Using {program} as shell"); - Self { - program, - args, - title_override, - } - } - } - - let shell_params = match shell.clone() { - Shell::System => { - if cfg!(windows) { - Some(ShellParams::new( - util::shell::get_windows_system_shell(), - None, - None, - )) - } else { - None - } - } - Shell::Program(program) => Some(ShellParams::new(program, None, None)), - Shell::WithArguments { - program, - args, - title_override, - } => Some(ShellParams::new(program, Some(args), title_override)), - }; - let terminal_title_override = - shell_params.as_ref().and_then(|e| e.title_override.clone()); - - #[cfg(windows)] - let shell_program = shell_params.as_ref().map(|params| { - use util::ResultExt; - - Self::resolve_path(¶ms.program) - .log_err() - .unwrap_or(params.program.clone()) - }); - - // Note: when remoting, this shell_kind will scrutinize `ssh` or - // `wsl.exe` as a shell and fall back to posix or powershell based on - // the compilation target. This is fine right now due to the restricted - // way we use the return value, but would become incorrect if we - // supported remoting into windows. - let shell_kind = shell.shell_kind(cfg!(windows)); - - let pty_options = { - let alac_shell = shell_params.as_ref().map(|params| { - alacritty_terminal::tty::Shell::new( - params.program.clone(), - params.args.clone().unwrap_or_default(), - ) - }); - - alacritty_terminal::tty::Options { - shell: alac_shell, - working_directory: working_directory.clone(), - drain_on_exit: true, - env: env.clone().into_iter().collect(), - #[cfg(windows)] - escape_args: shell_kind.tty_escape_args(), - } - }; - - let default_cursor_style = AlacCursorStyle::from(cursor_shape); - let scrolling_history = if task.is_some() { - // Tasks like `cargo build --all` may produce a lot of output, ergo allow maximum scrolling. - // After the task finishes, we do not allow appending to that terminal, so small tasks output should not - // cause excessive memory usage over time. - MAX_SCROLL_HISTORY_LINES - } else { - max_scroll_history_lines - .unwrap_or(DEFAULT_SCROLL_HISTORY_LINES) - .min(MAX_SCROLL_HISTORY_LINES) - }; - let config = Config { - scrolling_history, - default_cursor_style, - ..Config::default() - }; - - //Setup the pty... - let pty = match tty::new(&pty_options, TerminalBounds::default().into(), window_id) { - Ok(pty) => pty, - Err(error) => { - bail!(TerminalError { - directory: working_directory, - program: shell_params.as_ref().map(|params| params.program.clone()), - args: shell_params.as_ref().and_then(|params| params.args.clone()), - title_override: terminal_title_override, - source: error, - }); - } - }; - - //Spawn a task so the Alacritty EventLoop can communicate with us - //TODO: Remove with a bounded sender which can be dispatched on &self - let (events_tx, events_rx) = unbounded(); - //Set up the terminal... - let mut term = Term::new( - config.clone(), - &TerminalBounds::default(), - ZedListener(events_tx.clone()), - ); - - //Alacritty defaults to alternate scrolling being on, so we just need to turn it off. - if let AlternateScroll::Off = alternate_scroll { - term.unset_private_mode(PrivateMode::Named(NamedPrivateMode::AlternateScroll)); - } - - let term = Arc::new(FairMutex::new(term)); - - let pty_info = PtyProcessInfo::new(&pty); - - //And connect them together - let event_loop = EventLoop::new( - term.clone(), - ZedListener(events_tx), - pty, - pty_options.drain_on_exit, - false, - ) - .context("failed to create event loop")?; - - let pty_tx = event_loop.channel(); - let _io_thread = event_loop.spawn(); // DANGER - - let no_task = task.is_none(); - let terminal = Terminal { - task, - terminal_type: TerminalType::Pty { - pty_tx: Notifier(pty_tx), - info: pty_info, - }, - completion_tx, - term, - term_config: config, - title_override: terminal_title_override, - events: VecDeque::with_capacity(10), //Should never get this high. - last_content: Default::default(), - last_mouse: None, - matches: Vec::new(), - selection_head: None, - breadcrumb_text: String::new(), - scroll_px: px(0.), - next_link_id: 0, - selection_phase: SelectionPhase::Ended, - hyperlink_regex_searches: RegexSearches::new( - &path_hyperlink_regexes, - path_hyperlink_timeout_ms, - ), - vi_mode_enabled: false, - is_remote_terminal, - last_mouse_move_time: Instant::now(), - last_hyperlink_search_position: None, - #[cfg(windows)] - shell_program, - activation_script: activation_script.clone(), - template: CopyTemplate { - shell, - env, - cursor_shape, - alternate_scroll, - max_scroll_history_lines, - path_hyperlink_regexes, - path_hyperlink_timeout_ms, - window_id, - }, - child_exited: None, - event_loop_task: Task::ready(Ok(())), - }; - - if !activation_script.is_empty() && no_task { - for activation_script in activation_script { - terminal.write_to_pty(activation_script.into_bytes()); - // Simulate enter key press - // NOTE(PowerShell): using `\r\n` will put PowerShell in a continuation mode (infamous >> character) - // and generally mess up the rendering. - terminal.write_to_pty(b"\x0d"); - } - // In order to clear the screen at this point, we have two options: - // 1. We can send a shell-specific command such as "clear" or "cls" - // 2. We can "echo" a marker message that we will then catch when handling a Wakeup event - // and clear the screen using `terminal.clear()` method - // We cannot issue a `terminal.clear()` command at this point as alacritty is evented - // and while we have sent the activation script to the pty, it will be executed asynchronously. - // Therefore, we somehow need to wait for the activation script to finish executing before we - // can proceed with clearing the screen. - terminal.write_to_pty(shell_kind.clear_screen_command().as_bytes()); - // Simulate enter key press - terminal.write_to_pty(b"\x0d"); - } - - Ok(TerminalBuilder { - terminal, - events_rx, - }) - }; - // the thread we spawn things on has an effect on signal handling - if !cfg!(target_os = "windows") { - cx.spawn(async move |_| fut.await) - } else { - cx.background_spawn(fut) - } - } - - pub fn subscribe(mut self, cx: &Context) -> Terminal { - //Event loop - self.terminal.event_loop_task = cx.spawn(async move |terminal, cx| { - while let Some(event) = self.events_rx.next().await { - terminal.update(cx, |terminal, cx| { - //Process the first event immediately for lowered latency - terminal.process_event(event, cx); - })?; - - 'outer: loop { - let mut events = Vec::new(); - - #[cfg(any(test, feature = "test-support"))] - let mut timer = cx.background_executor().simulate_random_delay().fuse(); - #[cfg(not(any(test, feature = "test-support")))] - let mut timer = cx - .background_executor() - .timer(std::time::Duration::from_millis(4)) - .fuse(); - - let mut wakeup = false; - loop { - futures::select_biased! { - _ = timer => break, - event = self.events_rx.next() => { - if let Some(event) = event { - if matches!(event, AlacTermEvent::Wakeup) { - wakeup = true; - } else { - events.push(event); - } - - if events.len() > 100 { - break; - } - } else { - break; - } - }, - } - } - - if events.is_empty() && !wakeup { - smol::future::yield_now().await; - break 'outer; - } - - terminal.update(cx, |this, cx| { - if wakeup { - this.process_event(AlacTermEvent::Wakeup, cx); - } - - for event in events { - this.process_event(event, cx); - } - })?; - smol::future::yield_now().await; - } - } - anyhow::Ok(()) - }); - self.terminal - } - - #[cfg(windows)] - fn resolve_path(path: &str) -> Result { - use windows::Win32::Storage::FileSystem::SearchPathW; - use windows::core::HSTRING; - - let path = if path.starts_with(r"\\?\") || !path.contains(&['/', '\\']) { - path.to_string() - } else { - r"\\?\".to_string() + path - }; - - let required_length = unsafe { SearchPathW(None, &HSTRING::from(&path), None, None, None) }; - let mut buf = vec![0u16; required_length as usize]; - let size = unsafe { SearchPathW(None, &HSTRING::from(&path), None, Some(&mut buf), None) }; - - Ok(String::from_utf16(&buf[..size as usize])?) - } -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct IndexedCell { - pub point: AlacPoint, - pub cell: Cell, -} - -impl Deref for IndexedCell { - type Target = Cell; - - #[inline] - fn deref(&self) -> &Cell { - &self.cell - } -} - -// TODO: Un-pub -#[derive(Clone)] -pub struct TerminalContent { - pub cells: Vec, - pub mode: TermMode, - pub display_offset: usize, - pub selection_text: Option, - pub selection: Option, - pub cursor: RenderableCursor, - pub cursor_char: char, - pub terminal_bounds: TerminalBounds, - pub last_hovered_word: Option, - pub scrolled_to_top: bool, - pub scrolled_to_bottom: bool, -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct HoveredWord { - pub word: String, - pub word_match: RangeInclusive, - pub id: usize, -} - -impl Default for TerminalContent { - fn default() -> Self { - TerminalContent { - cells: Default::default(), - mode: Default::default(), - display_offset: Default::default(), - selection_text: Default::default(), - selection: Default::default(), - cursor: RenderableCursor { - shape: alacritty_terminal::vte::ansi::CursorShape::Block, - point: AlacPoint::new(Line(0), Column(0)), - }, - cursor_char: Default::default(), - terminal_bounds: Default::default(), - last_hovered_word: None, - scrolled_to_top: false, - scrolled_to_bottom: false, - } - } -} - -#[derive(PartialEq, Eq)] -pub enum SelectionPhase { - Selecting, - Ended, -} - -enum TerminalType { - Pty { - pty_tx: Notifier, - info: PtyProcessInfo, - }, - DisplayOnly, -} - -pub struct Terminal { - terminal_type: TerminalType, - completion_tx: Option>>, - term: Arc>>, - term_config: Config, - events: VecDeque, - /// This is only used for mouse mode cell change detection - last_mouse: Option<(AlacPoint, AlacDirection)>, - pub matches: Vec>, - pub last_content: TerminalContent, - pub selection_head: Option, - pub breadcrumb_text: String, - title_override: Option, - scroll_px: Pixels, - next_link_id: usize, - selection_phase: SelectionPhase, - hyperlink_regex_searches: RegexSearches, - task: Option, - vi_mode_enabled: bool, - is_remote_terminal: bool, - last_mouse_move_time: Instant, - last_hyperlink_search_position: Option>, - #[cfg(windows)] - shell_program: Option, - template: CopyTemplate, - activation_script: Vec, - child_exited: Option, - event_loop_task: Task>, -} - -struct CopyTemplate { - shell: Shell, - env: HashMap, - cursor_shape: CursorShape, - alternate_scroll: AlternateScroll, - max_scroll_history_lines: Option, - path_hyperlink_regexes: Vec, - path_hyperlink_timeout_ms: u64, - window_id: u64, -} - -#[derive(Debug)] -pub struct TaskState { - pub status: TaskStatus, - pub completion_rx: Receiver>, - pub spawned_task: SpawnInTerminal, -} - -/// A status of the current terminal tab's task. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum TaskStatus { - /// The task had been started, but got cancelled or somehow otherwise it did not - /// report its exit code before the terminal event loop was shut down. - Unknown, - /// The task is started and running currently. - Running, - /// After the start, the task stopped running and reported its error code back. - Completed { success: bool }, -} - -impl TaskStatus { - fn register_terminal_exit(&mut self) { - if self == &Self::Running { - *self = Self::Unknown; - } - } - - fn register_task_exit(&mut self, error_code: i32) { - *self = TaskStatus::Completed { - success: error_code == 0, - }; - } -} - -impl Terminal { - fn process_event(&mut self, event: AlacTermEvent, cx: &mut Context) { - match event { - AlacTermEvent::Title(title) => { - // ignore default shell program title change as windows always sends those events - // and it would end up showing the shell executable path in breadcrumbs - #[cfg(windows)] - { - if self - .shell_program - .as_ref() - .map(|e| *e == title) - .unwrap_or(false) - { - return; - } - } - - self.breadcrumb_text = title; - cx.emit(Event::BreadcrumbsChanged); - } - AlacTermEvent::ResetTitle => { - self.breadcrumb_text = String::new(); - cx.emit(Event::BreadcrumbsChanged); - } - AlacTermEvent::ClipboardStore(_, data) => { - cx.write_to_clipboard(ClipboardItem::new_string(data)) - } - AlacTermEvent::ClipboardLoad(_, format) => { - self.write_to_pty( - match &cx.read_from_clipboard().and_then(|item| item.text()) { - // The terminal only supports pasting strings, not images. - Some(text) => format(text), - _ => format(""), - } - .into_bytes(), - ) - } - AlacTermEvent::PtyWrite(out) => self.write_to_pty(out.into_bytes()), - AlacTermEvent::TextAreaSizeRequest(format) => { - self.write_to_pty(format(self.last_content.terminal_bounds.into()).into_bytes()) - } - AlacTermEvent::CursorBlinkingChange => { - let terminal = self.term.lock(); - let blinking = terminal.cursor_style().blinking; - cx.emit(Event::BlinkChanged(blinking)); - } - AlacTermEvent::Bell => { - cx.emit(Event::Bell); - } - AlacTermEvent::Exit => self.register_task_finished(None, cx), - AlacTermEvent::MouseCursorDirty => { - //NOOP, Handled in render - } - AlacTermEvent::Wakeup => { - cx.emit(Event::Wakeup); - - if let TerminalType::Pty { info, .. } = &mut self.terminal_type { - if info.has_changed() { - cx.emit(Event::TitleChanged); - } - } - } - AlacTermEvent::ColorRequest(index, format) => { - // It's important that the color request is processed here to retain relative order - // with other PTY writes. Otherwise applications might witness out-of-order - // responses to requests. For example: An application sending `OSC 11 ; ? ST` - // (color request) followed by `CSI c` (request device attributes) would receive - // the response to `CSI c` first. - // Instead of locking, we could store the colors in `self.last_content`. But then - // we might respond with out of date value if a "set color" sequence is immediately - // followed by a color request sequence. - let color = self.term.lock().colors()[index] - .unwrap_or_else(|| to_alac_rgb(get_color_at_index(index, cx.theme().as_ref()))); - self.write_to_pty(format(color).into_bytes()); - } - AlacTermEvent::ChildExit(error_code) => { - self.register_task_finished(Some(error_code), cx); - } - } - } - - pub fn selection_started(&self) -> bool { - self.selection_phase == SelectionPhase::Selecting - } - - fn process_terminal_event( - &mut self, - event: &InternalEvent, - term: &mut Term, - window: &mut Window, - cx: &mut Context, - ) { - match event { - &InternalEvent::Resize(mut new_bounds) => { - trace!("Resizing: new_bounds={new_bounds:?}"); - new_bounds.bounds.size.height = - cmp::max(new_bounds.line_height, new_bounds.height()); - new_bounds.bounds.size.width = cmp::max(new_bounds.cell_width, new_bounds.width()); - - self.last_content.terminal_bounds = new_bounds; - - if let TerminalType::Pty { pty_tx, .. } = &self.terminal_type { - pty_tx.0.send(Msg::Resize(new_bounds.into())).ok(); - } - - term.resize(new_bounds); - // If there are matches we need to emit a wake up event to - // invalidate the matches and recalculate their locations - // in the new terminal layout - if !self.matches.is_empty() { - cx.emit(Event::Wakeup); - } - } - InternalEvent::Clear => { - trace!("Clearing"); - // Clear back buffer - term.clear_screen(ClearMode::Saved); - - let cursor = term.grid().cursor.point; - - // Clear the lines above - term.grid_mut().reset_region(..cursor.line); - - // Copy the current line up - let line = term.grid()[cursor.line][..Column(term.grid().columns())] - .iter() - .cloned() - .enumerate() - .collect::>(); - - for (i, cell) in line { - term.grid_mut()[Line(0)][Column(i)] = cell; - } - - // Reset the cursor - term.grid_mut().cursor.point = - AlacPoint::new(Line(0), term.grid_mut().cursor.point.column); - let new_cursor = term.grid().cursor.point; - - // Clear the lines below the new cursor - if (new_cursor.line.0 as usize) < term.screen_lines() - 1 { - term.grid_mut().reset_region((new_cursor.line + 1)..); - } - - cx.emit(Event::Wakeup); - } - InternalEvent::Scroll(scroll) => { - trace!("Scrolling: scroll={scroll:?}"); - term.scroll_display(*scroll); - self.refresh_hovered_word(window); - - if self.vi_mode_enabled { - match *scroll { - AlacScroll::Delta(delta) => { - term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, delta); - } - AlacScroll::PageUp => { - let lines = term.screen_lines() as i32; - term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, lines); - } - AlacScroll::PageDown => { - let lines = -(term.screen_lines() as i32); - term.vi_mode_cursor = term.vi_mode_cursor.scroll(term, lines); - } - AlacScroll::Top => { - let point = AlacPoint::new(term.topmost_line(), Column(0)); - term.vi_mode_cursor = ViModeCursor::new(point); - } - AlacScroll::Bottom => { - let point = AlacPoint::new(term.bottommost_line(), Column(0)); - term.vi_mode_cursor = ViModeCursor::new(point); - } - } - if let Some(mut selection) = term.selection.take() { - let point = term.vi_mode_cursor.point; - selection.update(point, AlacDirection::Right); - term.selection = Some(selection); - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - if let Some(selection_text) = term.selection_to_string() { - cx.write_to_primary(ClipboardItem::new_string(selection_text)); - } - - self.selection_head = Some(point); - cx.emit(Event::SelectionsChanged) - } - } - } - InternalEvent::SetSelection(selection) => { - trace!("Setting selection: selection={selection:?}"); - term.selection = selection.as_ref().map(|(sel, _)| sel.clone()); - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - if let Some(selection_text) = term.selection_to_string() { - cx.write_to_primary(ClipboardItem::new_string(selection_text)); - } - - if let Some((_, head)) = selection { - self.selection_head = Some(*head); - } - cx.emit(Event::SelectionsChanged) - } - InternalEvent::UpdateSelection(position) => { - trace!("Updating selection: position={position:?}"); - if let Some(mut selection) = term.selection.take() { - let (point, side) = grid_point_and_side( - *position, - self.last_content.terminal_bounds, - term.grid().display_offset(), - ); - - selection.update(point, side); - term.selection = Some(selection); - - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - if let Some(selection_text) = term.selection_to_string() { - cx.write_to_primary(ClipboardItem::new_string(selection_text)); - } - - self.selection_head = Some(point); - cx.emit(Event::SelectionsChanged) - } - } - - InternalEvent::Copy(keep_selection) => { - trace!("Copying selection: keep_selection={keep_selection:?}"); - if let Some(txt) = term.selection_to_string() { - cx.write_to_clipboard(ClipboardItem::new_string(txt)); - if !keep_selection.unwrap_or_else(|| { - let settings = TerminalSettings::get_global(cx); - settings.keep_selection_on_copy - }) { - self.events.push_back(InternalEvent::SetSelection(None)); - } - } - } - InternalEvent::ScrollToAlacPoint(point) => { - trace!("Scrolling to point: point={point:?}"); - term.scroll_to_point(*point); - self.refresh_hovered_word(window); - } - InternalEvent::MoveViCursorToAlacPoint(point) => { - trace!("Move vi cursor to point: point={point:?}"); - term.vi_goto_point(*point); - self.refresh_hovered_word(window); - } - InternalEvent::ToggleViMode => { - trace!("Toggling vi mode"); - self.vi_mode_enabled = !self.vi_mode_enabled; - term.toggle_vi_mode(); - } - InternalEvent::ViMotion(motion) => { - trace!("Performing vi motion: motion={motion:?}"); - term.vi_motion(*motion); - } - InternalEvent::FindHyperlink(position, open) => { - trace!("Finding hyperlink at position: position={position:?}, open={open:?}"); - let prev_hovered_word = self.last_content.last_hovered_word.take(); - - let point = grid_point( - *position, - self.last_content.terminal_bounds, - term.grid().display_offset(), - ) - .grid_clamp(term, Boundary::Grid); - - match terminal_hyperlinks::find_from_grid_point( - term, - point, - &mut self.hyperlink_regex_searches, - ) { - Some((maybe_url_or_path, is_url, url_match)) => { - let target = if is_url { - // Treat "file://" URLs like file paths to ensure - // that line numbers at the end of the path are - // handled correctly. - // file://{path} should be urldecoded, returning a urldecoded {path} - if let Some(path) = maybe_url_or_path.strip_prefix("file://") { - let decoded_path = urlencoding::decode(path) - .map(|decoded| decoded.into_owned()) - .unwrap_or(path.to_owned()); - - MaybeNavigationTarget::PathLike(PathLikeTarget { - maybe_path: decoded_path, - terminal_dir: self.working_directory(), - }) - } else { - MaybeNavigationTarget::Url(maybe_url_or_path.clone()) - } - } else { - MaybeNavigationTarget::PathLike(PathLikeTarget { - maybe_path: maybe_url_or_path.clone(), - terminal_dir: self.working_directory(), - }) - }; - if *open { - cx.emit(Event::Open(target)); - } else { - self.update_selected_word( - prev_hovered_word, - url_match, - maybe_url_or_path, - target, - cx, - ); - } - } - None => { - cx.emit(Event::NewNavigationTarget(None)); - } - } - } - } - } - - fn update_selected_word( - &mut self, - prev_word: Option, - word_match: RangeInclusive, - word: String, - navigation_target: MaybeNavigationTarget, - cx: &mut Context, - ) { - if let Some(prev_word) = prev_word - && prev_word.word == word - && prev_word.word_match == word_match - { - self.last_content.last_hovered_word = Some(HoveredWord { - word, - word_match, - id: prev_word.id, - }); - return; - } - - self.last_content.last_hovered_word = Some(HoveredWord { - word, - word_match, - id: self.next_link_id(), - }); - cx.emit(Event::NewNavigationTarget(Some(navigation_target))); - cx.notify() - } - - fn next_link_id(&mut self) -> usize { - let res = self.next_link_id; - self.next_link_id = self.next_link_id.wrapping_add(1); - res - } - - pub fn last_content(&self) -> &TerminalContent { - &self.last_content - } - - pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape) { - self.term_config.default_cursor_style = cursor_shape.into(); - self.term.lock().set_options(self.term_config.clone()); - } - - pub fn write_output(&mut self, bytes: &[u8], cx: &mut Context) { - // Inject bytes directly into the terminal emulator and refresh the UI. - // This bypasses the PTY/event loop for display-only terminals. - // - // We first convert LF to CRLF, to get the expected line wrapping in Alacritty. - // When output comes from piped commands (not a PTY) such as codex-acp, and that - // output only contains LF (\n) without a CR (\r) after it, such as the output - // of the `ls` command when running outside a PTY, Alacritty moves the cursor - // cursor down a line but does not move it back to the initial column. This makes - // the rendered output look ridiculous. To prevent this, we insert a CR (\r) before - // each LF that didn't already have one. (Alacritty doesn't have a setting for this.) - let mut converted = Vec::with_capacity(bytes.len()); - let mut prev_byte = 0u8; - for &byte in bytes { - if byte == b'\n' && prev_byte != b'\r' { - converted.push(b'\r'); - } - converted.push(byte); - prev_byte = byte; - } - - let mut processor = alacritty_terminal::vte::ansi::Processor::< - alacritty_terminal::vte::ansi::StdSyncHandler, - >::new(); - { - let mut term = self.term.lock(); - processor.advance(&mut *term, &converted); - } - cx.emit(Event::Wakeup); - } - - pub fn total_lines(&self) -> usize { - self.term.lock_unfair().total_lines() - } - - pub fn viewport_lines(&self) -> usize { - self.term.lock_unfair().screen_lines() - } - - //To test: - //- Activate match on terminal (scrolling and selection) - //- Editor search snapping behavior - - pub fn activate_match(&mut self, index: usize) { - if let Some(search_match) = self.matches.get(index).cloned() { - self.set_selection(Some((make_selection(&search_match), *search_match.end()))); - if self.vi_mode_enabled { - self.events - .push_back(InternalEvent::MoveViCursorToAlacPoint(*search_match.end())); - } else { - self.events - .push_back(InternalEvent::ScrollToAlacPoint(*search_match.start())); - } - } - } - - pub fn select_matches(&mut self, matches: &[RangeInclusive]) { - let matches_to_select = self - .matches - .iter() - .filter(|self_match| matches.contains(self_match)) - .cloned() - .collect::>(); - for match_to_select in matches_to_select { - self.set_selection(Some(( - make_selection(&match_to_select), - *match_to_select.end(), - ))); - } - } - - pub fn select_all(&mut self) { - let term = self.term.lock(); - let start = AlacPoint::new(term.topmost_line(), Column(0)); - let end = AlacPoint::new(term.bottommost_line(), term.last_column()); - drop(term); - self.set_selection(Some((make_selection(&(start..=end)), end))); - } - - fn set_selection(&mut self, selection: Option<(Selection, AlacPoint)>) { - self.events - .push_back(InternalEvent::SetSelection(selection)); - } - - pub fn copy(&mut self, keep_selection: Option) { - self.events.push_back(InternalEvent::Copy(keep_selection)); - } - - pub fn clear(&mut self) { - self.events.push_back(InternalEvent::Clear) - } - - pub fn scroll_line_up(&mut self) { - self.events - .push_back(InternalEvent::Scroll(AlacScroll::Delta(1))); - } - - pub fn scroll_up_by(&mut self, lines: usize) { - self.events - .push_back(InternalEvent::Scroll(AlacScroll::Delta(lines as i32))); - } - - pub fn scroll_line_down(&mut self) { - self.events - .push_back(InternalEvent::Scroll(AlacScroll::Delta(-1))); - } - - pub fn scroll_down_by(&mut self, lines: usize) { - self.events - .push_back(InternalEvent::Scroll(AlacScroll::Delta(-(lines as i32)))); - } - - pub fn scroll_page_up(&mut self) { - self.events - .push_back(InternalEvent::Scroll(AlacScroll::PageUp)); - } - - pub fn scroll_page_down(&mut self) { - self.events - .push_back(InternalEvent::Scroll(AlacScroll::PageDown)); - } - - pub fn scroll_to_top(&mut self) { - self.events - .push_back(InternalEvent::Scroll(AlacScroll::Top)); - } - - pub fn scroll_to_bottom(&mut self) { - self.events - .push_back(InternalEvent::Scroll(AlacScroll::Bottom)); - } - - pub fn scrolled_to_top(&self) -> bool { - self.last_content.scrolled_to_top - } - - pub fn scrolled_to_bottom(&self) -> bool { - self.last_content.scrolled_to_bottom - } - - ///Resize the terminal and the PTY. - pub fn set_size(&mut self, new_bounds: TerminalBounds) { - if self.last_content.terminal_bounds != new_bounds { - self.events.push_back(InternalEvent::Resize(new_bounds)) - } - } - - /// Write the Input payload to the PTY, if applicable. - /// (This is a no-op for display-only terminals.) - fn write_to_pty(&self, input: impl Into>) { - if let TerminalType::Pty { pty_tx, .. } = &self.terminal_type { - let input = input.into(); - if log::log_enabled!(log::Level::Debug) { - if let Ok(str) = str::from_utf8(&input) { - log::debug!("Writing to PTY: {:?}", str); - } else { - log::debug!("Writing to PTY: {:?}", input); - } - } - pty_tx.notify(input); - } - } - - pub fn input(&mut self, input: impl Into>) { - self.events - .push_back(InternalEvent::Scroll(AlacScroll::Bottom)); - self.events.push_back(InternalEvent::SetSelection(None)); - - self.write_to_pty(input); - } - - pub fn toggle_vi_mode(&mut self) { - self.events.push_back(InternalEvent::ToggleViMode); - } - - pub fn vi_motion(&mut self, keystroke: &Keystroke) { - if !self.vi_mode_enabled { - return; - } - - let key: Cow<'_, str> = if keystroke.modifiers.shift { - Cow::Owned(keystroke.key.to_uppercase()) - } else { - Cow::Borrowed(keystroke.key.as_str()) - }; - - let motion: Option = match key.as_ref() { - "h" | "left" => Some(ViMotion::Left), - "j" | "down" => Some(ViMotion::Down), - "k" | "up" => Some(ViMotion::Up), - "l" | "right" => Some(ViMotion::Right), - "w" => Some(ViMotion::WordRight), - "b" if !keystroke.modifiers.control => Some(ViMotion::WordLeft), - "e" => Some(ViMotion::WordRightEnd), - "%" => Some(ViMotion::Bracket), - "$" => Some(ViMotion::Last), - "0" => Some(ViMotion::First), - "^" => Some(ViMotion::FirstOccupied), - "H" => Some(ViMotion::High), - "M" => Some(ViMotion::Middle), - "L" => Some(ViMotion::Low), - _ => None, - }; - - if let Some(motion) = motion { - let cursor = self.last_content.cursor.point; - let cursor_pos = Point { - x: cursor.column.0 as f32 * self.last_content.terminal_bounds.cell_width, - y: cursor.line.0 as f32 * self.last_content.terminal_bounds.line_height, - }; - self.events - .push_back(InternalEvent::UpdateSelection(cursor_pos)); - self.events.push_back(InternalEvent::ViMotion(motion)); - return; - } - - let scroll_motion = match key.as_ref() { - "g" => Some(AlacScroll::Top), - "G" => Some(AlacScroll::Bottom), - "b" if keystroke.modifiers.control => Some(AlacScroll::PageUp), - "f" if keystroke.modifiers.control => Some(AlacScroll::PageDown), - "d" if keystroke.modifiers.control => { - let amount = self.last_content.terminal_bounds.line_height().to_f64() as i32 / 2; - Some(AlacScroll::Delta(-amount)) - } - "u" if keystroke.modifiers.control => { - let amount = self.last_content.terminal_bounds.line_height().to_f64() as i32 / 2; - Some(AlacScroll::Delta(amount)) - } - _ => None, - }; - - if let Some(scroll_motion) = scroll_motion { - self.events.push_back(InternalEvent::Scroll(scroll_motion)); - return; - } - - match key.as_ref() { - "v" => { - let point = self.last_content.cursor.point; - let selection_type = SelectionType::Simple; - let side = AlacDirection::Right; - let selection = Selection::new(selection_type, point, side); - self.events - .push_back(InternalEvent::SetSelection(Some((selection, point)))); - } - - "escape" => { - self.events.push_back(InternalEvent::SetSelection(None)); - } - - "y" => { - self.copy(Some(false)); - } - - "i" => { - self.scroll_to_bottom(); - self.toggle_vi_mode(); - } - _ => {} - } - } - - pub fn try_keystroke(&mut self, keystroke: &Keystroke, option_as_meta: bool) -> bool { - if self.vi_mode_enabled { - self.vi_motion(keystroke); - return true; - } - - // Keep default terminal behavior - let esc = to_esc_str(keystroke, &self.last_content.mode, option_as_meta); - if let Some(esc) = esc { - match esc { - Cow::Borrowed(string) => self.input(string.as_bytes()), - Cow::Owned(string) => self.input(string.into_bytes()), - }; - true - } else { - false - } - } - - pub fn try_modifiers_change( - &mut self, - modifiers: &Modifiers, - window: &Window, - cx: &mut Context, - ) { - if self - .last_content - .terminal_bounds - .bounds - .contains(&window.mouse_position()) - && modifiers.secondary() - { - self.refresh_hovered_word(window); - } - cx.notify(); - } - - ///Paste text into the terminal - pub fn paste(&mut self, text: &str) { - let paste_text = if self.last_content.mode.contains(TermMode::BRACKETED_PASTE) { - format!("{}{}{}", "\x1b[200~", text.replace('\x1b', ""), "\x1b[201~") - } else { - text.replace("\r\n", "\r").replace('\n', "\r") - }; - - self.input(paste_text.into_bytes()); - } - - pub fn sync(&mut self, window: &mut Window, cx: &mut Context) { - let term = self.term.clone(); - let mut terminal = term.lock_unfair(); - //Note that the ordering of events matters for event processing - while let Some(e) = self.events.pop_front() { - self.process_terminal_event(&e, &mut terminal, window, cx) - } - - self.last_content = Self::make_content(&terminal, &self.last_content); - } - - fn make_content(term: &Term, last_content: &TerminalContent) -> TerminalContent { - let content = term.renderable_content(); - - // Pre-allocate with estimated size to reduce reallocations - let estimated_size = content.display_iter.size_hint().0; - let mut cells = Vec::with_capacity(estimated_size); - - cells.extend(content.display_iter.map(|ic| IndexedCell { - point: ic.point, - cell: ic.cell.clone(), - })); - - let selection_text = if content.selection.is_some() { - term.selection_to_string() - } else { - None - }; - - TerminalContent { - cells, - mode: content.mode, - display_offset: content.display_offset, - selection_text, - selection: content.selection, - cursor: content.cursor, - cursor_char: term.grid()[content.cursor.point].c, - terminal_bounds: last_content.terminal_bounds, - last_hovered_word: last_content.last_hovered_word.clone(), - scrolled_to_top: content.display_offset == term.history_size(), - scrolled_to_bottom: content.display_offset == 0, - } - } - - pub fn get_content(&self) -> String { - let term = self.term.lock_unfair(); - let start = AlacPoint::new(term.topmost_line(), Column(0)); - let end = AlacPoint::new(term.bottommost_line(), term.last_column()); - term.bounds_to_string(start, end) - } - - pub fn last_n_non_empty_lines(&self, n: usize) -> Vec { - let term = self.term.clone(); - let terminal = term.lock_unfair(); - let grid = terminal.grid(); - let mut lines = Vec::new(); - - let mut current_line = grid.bottommost_line().0; - let topmost_line = grid.topmost_line().0; - - while current_line >= topmost_line && lines.len() < n { - let logical_line_start = self.find_logical_line_start(grid, current_line, topmost_line); - let logical_line = self.construct_logical_line(grid, logical_line_start, current_line); - - if let Some(line) = self.process_line(logical_line) { - lines.push(line); - } - - // Move to the line above the start of the current logical line - current_line = logical_line_start - 1; - } - - lines.reverse(); - lines - } - - fn find_logical_line_start(&self, grid: &Grid, current: i32, topmost: i32) -> i32 { - let mut line_start = current; - while line_start > topmost { - let prev_line = Line(line_start - 1); - let last_cell = &grid[prev_line][Column(grid.columns() - 1)]; - if !last_cell.flags.contains(Flags::WRAPLINE) { - break; - } - line_start -= 1; - } - line_start - } - - fn construct_logical_line(&self, grid: &Grid, start: i32, end: i32) -> String { - let mut logical_line = String::new(); - for row in start..=end { - let grid_row = &grid[Line(row)]; - logical_line.push_str(&row_to_string(grid_row)); - } - logical_line - } - - fn process_line(&self, line: String) -> Option { - let trimmed = line.trim_end().to_string(); - if !trimmed.is_empty() { - Some(trimmed) - } else { - None - } - } - - pub fn focus_in(&self) { - if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) { - self.write_to_pty("\x1b[I".as_bytes()); - } - } - - pub fn focus_out(&mut self) { - if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) { - self.write_to_pty("\x1b[O".as_bytes()); - } - } - - pub fn mouse_changed(&mut self, point: AlacPoint, side: AlacDirection) -> bool { - match self.last_mouse { - Some((old_point, old_side)) => { - if old_point == point && old_side == side { - false - } else { - self.last_mouse = Some((point, side)); - true - } - } - None => { - self.last_mouse = Some((point, side)); - true - } - } - } - - pub fn mouse_mode(&self, shift: bool) -> bool { - self.last_content.mode.intersects(TermMode::MOUSE_MODE) && !shift - } - - pub fn mouse_move(&mut self, e: &MouseMoveEvent, cx: &mut Context) { - let position = e.position - self.last_content.terminal_bounds.bounds.origin; - if self.mouse_mode(e.modifiers.shift) { - let (point, side) = grid_point_and_side( - position, - self.last_content.terminal_bounds, - self.last_content.display_offset, - ); - - if self.mouse_changed(point, side) - && let Some(bytes) = - mouse_moved_report(point, e.pressed_button, e.modifiers, self.last_content.mode) - { - self.write_to_pty(bytes); - } - } else if e.modifiers.secondary() { - self.word_from_position(e.position); - } - cx.notify(); - } - - fn word_from_position(&mut self, position: Point) { - if self.selection_phase == SelectionPhase::Selecting { - self.last_content.last_hovered_word = None; - } else if self.last_content.terminal_bounds.bounds.contains(&position) { - // Throttle hyperlink searches to avoid excessive processing - let now = Instant::now(); - let should_search = if let Some(last_pos) = self.last_hyperlink_search_position { - // Only search if mouse moved significantly or enough time passed - let distance_moved = - ((position.x - last_pos.x).abs() + (position.y - last_pos.y).abs()) > px(5.0); - let time_elapsed = now.duration_since(self.last_mouse_move_time).as_millis() > 100; - distance_moved || time_elapsed - } else { - true - }; - - if should_search { - self.last_mouse_move_time = now; - self.last_hyperlink_search_position = Some(position); - self.events.push_back(InternalEvent::FindHyperlink( - position - self.last_content.terminal_bounds.bounds.origin, - false, - )); - } - } else { - self.last_content.last_hovered_word = None; - } - } - - pub fn select_word_at_event_position(&mut self, e: &MouseDownEvent) { - let position = e.position - self.last_content.terminal_bounds.bounds.origin; - let (point, side) = grid_point_and_side( - position, - self.last_content.terminal_bounds, - self.last_content.display_offset, - ); - let selection = Selection::new(SelectionType::Semantic, point, side); - self.events - .push_back(InternalEvent::SetSelection(Some((selection, point)))); - } - - pub fn mouse_drag( - &mut self, - e: &MouseMoveEvent, - region: Bounds, - cx: &mut Context, - ) { - let position = e.position - self.last_content.terminal_bounds.bounds.origin; - if !self.mouse_mode(e.modifiers.shift) { - self.selection_phase = SelectionPhase::Selecting; - // Alacritty has the same ordering, of first updating the selection - // then scrolling 15ms later - self.events - .push_back(InternalEvent::UpdateSelection(position)); - - // Doesn't make sense to scroll the alt screen - if !self.last_content.mode.contains(TermMode::ALT_SCREEN) { - let scroll_lines = match self.drag_line_delta(e, region) { - Some(value) => value, - None => return, - }; - - self.events - .push_back(InternalEvent::Scroll(AlacScroll::Delta(scroll_lines))); - } - - cx.notify(); - } - } - - fn drag_line_delta(&self, e: &MouseMoveEvent, region: Bounds) -> Option { - let top = region.origin.y; - let bottom = region.bottom_left().y; - - let scroll_lines = if e.position.y < top { - let scroll_delta = (top - e.position.y).pow(1.1); - (scroll_delta / self.last_content.terminal_bounds.line_height).ceil() as i32 - } else if e.position.y > bottom { - let scroll_delta = -((e.position.y - bottom).pow(1.1)); - (scroll_delta / self.last_content.terminal_bounds.line_height).floor() as i32 - } else { - return None; - }; - - Some(scroll_lines.clamp(-3, 3)) - } - - pub fn mouse_down(&mut self, e: &MouseDownEvent, _cx: &mut Context) { - let position = e.position - self.last_content.terminal_bounds.bounds.origin; - let point = grid_point( - position, - self.last_content.terminal_bounds, - self.last_content.display_offset, - ); - - if self.mouse_mode(e.modifiers.shift) { - if let Some(bytes) = - mouse_button_report(point, e.button, e.modifiers, true, self.last_content.mode) - { - self.write_to_pty(bytes); - } - } else { - match e.button { - MouseButton::Left => { - let (point, side) = grid_point_and_side( - position, - self.last_content.terminal_bounds, - self.last_content.display_offset, - ); - - let selection_type = match e.click_count { - 0 => return, //This is a release - 1 => Some(SelectionType::Simple), - 2 => Some(SelectionType::Semantic), - 3 => Some(SelectionType::Lines), - _ => None, - }; - - if selection_type == Some(SelectionType::Simple) && e.modifiers.shift { - self.events - .push_back(InternalEvent::UpdateSelection(position)); - return; - } - - let selection = selection_type - .map(|selection_type| Selection::new(selection_type, point, side)); - - if let Some(sel) = selection { - self.events - .push_back(InternalEvent::SetSelection(Some((sel, point)))); - } - } - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - MouseButton::Middle => { - if let Some(item) = _cx.read_from_primary() { - let text = item.text().unwrap_or_default(); - self.input(text.into_bytes()); - } - } - _ => {} - } - } - } - - pub fn mouse_up(&mut self, e: &MouseUpEvent, cx: &Context) { - let setting = TerminalSettings::get_global(cx); - - let position = e.position - self.last_content.terminal_bounds.bounds.origin; - if self.mouse_mode(e.modifiers.shift) { - let point = grid_point( - position, - self.last_content.terminal_bounds, - self.last_content.display_offset, - ); - - if let Some(bytes) = - mouse_button_report(point, e.button, e.modifiers, false, self.last_content.mode) - { - self.write_to_pty(bytes); - } - } else { - if e.button == MouseButton::Left && setting.copy_on_select { - self.copy(Some(true)); - } - - //Hyperlinks - if self.selection_phase == SelectionPhase::Ended { - let mouse_cell_index = - content_index_for_mouse(position, &self.last_content.terminal_bounds); - if let Some(link) = self.last_content.cells[mouse_cell_index].hyperlink() { - cx.open_url(link.uri()); - } else if e.modifiers.secondary() { - self.events - .push_back(InternalEvent::FindHyperlink(position, true)); - } - } - } - - self.selection_phase = SelectionPhase::Ended; - self.last_mouse = None; - } - - ///Scroll the terminal - pub fn scroll_wheel(&mut self, e: &ScrollWheelEvent, scroll_multiplier: f32) { - let mouse_mode = self.mouse_mode(e.shift); - let scroll_multiplier = if mouse_mode { 1. } else { scroll_multiplier }; - - if let Some(scroll_lines) = self.determine_scroll_lines(e, scroll_multiplier) { - if mouse_mode { - let point = grid_point( - e.position - self.last_content.terminal_bounds.bounds.origin, - self.last_content.terminal_bounds, - self.last_content.display_offset, - ); - - if let Some(scrolls) = scroll_report(point, scroll_lines, e, self.last_content.mode) - { - for scroll in scrolls { - self.write_to_pty(scroll); - } - }; - } else if self - .last_content - .mode - .contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL) - && !e.shift - { - self.write_to_pty(alt_scroll(scroll_lines)); - } else if scroll_lines != 0 { - let scroll = AlacScroll::Delta(scroll_lines); - - self.events.push_back(InternalEvent::Scroll(scroll)); - } - } - } - - fn refresh_hovered_word(&mut self, window: &Window) { - self.word_from_position(window.mouse_position()); - } - - fn determine_scroll_lines( - &mut self, - e: &ScrollWheelEvent, - scroll_multiplier: f32, - ) -> Option { - let line_height = self.last_content.terminal_bounds.line_height; - match e.touch_phase { - /* Reset scroll state on started */ - TouchPhase::Started => { - self.scroll_px = px(0.); - None - } - /* Calculate the appropriate scroll lines */ - TouchPhase::Moved => { - let old_offset = (self.scroll_px / line_height) as i32; - - self.scroll_px += e.delta.pixel_delta(line_height).y * scroll_multiplier; - - let new_offset = (self.scroll_px / line_height) as i32; - - // Whenever we hit the edges, reset our stored scroll to 0 - // so we can respond to changes in direction quickly - self.scroll_px %= self.last_content.terminal_bounds.height(); - - Some(new_offset - old_offset) - } - TouchPhase::Ended => None, - } - } - - pub fn find_matches( - &self, - mut searcher: RegexSearch, - cx: &Context, - ) -> Task>> { - let term = self.term.clone(); - cx.background_spawn(async move { - let term = term.lock(); - - all_search_matches(&term, &mut searcher).collect() - }) - } - - pub fn working_directory(&self) -> Option { - if self.is_remote_terminal { - // We can't yet reliably detect the working directory of a shell on the - // SSH host. Until we can do that, it doesn't make sense to display - // the working directory on the client and persist that. - None - } else { - self.client_side_working_directory() - } - } - - /// Returns the working directory of the process that's connected to the PTY. - /// That means it returns the working directory of the local shell or program - /// that's running inside the terminal. - /// - /// This does *not* return the working directory of the shell that runs on the - /// remote host, in case Zed is connected to a remote host. - fn client_side_working_directory(&self) -> Option { - match &self.terminal_type { - TerminalType::Pty { info, .. } => { - info.current.as_ref().map(|process| process.cwd.clone()) - } - TerminalType::DisplayOnly => None, - } - } - - pub fn title(&self, truncate: bool) -> String { - const MAX_CHARS: usize = 25; - match &self.task { - Some(task_state) => { - if truncate { - truncate_and_trailoff(&task_state.spawned_task.label, MAX_CHARS) - } else { - task_state.spawned_task.full_label.clone() - } - } - None => self - .title_override - .as_ref() - .map(|title_override| title_override.to_string()) - .unwrap_or_else(|| match &self.terminal_type { - TerminalType::Pty { info, .. } => info - .current - .as_ref() - .map(|fpi| { - let process_file = fpi - .cwd - .file_name() - .map(|name| name.to_string_lossy().into_owned()) - .unwrap_or_default(); - - let argv = fpi.argv.as_slice(); - let process_name = format!( - "{}{}", - fpi.name, - if !argv.is_empty() { - format!(" {}", (argv[1..]).join(" ")) - } else { - "".to_string() - } - ); - let (process_file, process_name) = if truncate { - ( - truncate_and_trailoff(&process_file, MAX_CHARS), - truncate_and_trailoff(&process_name, MAX_CHARS), - ) - } else { - (process_file, process_name) - }; - format!("{process_file} — {process_name}") - }) - .unwrap_or_else(|| "Terminal".to_string()), - TerminalType::DisplayOnly => "Terminal".to_string(), - }), - } - } - - pub fn kill_active_task(&mut self) { - if let Some(task) = self.task() - && task.status == TaskStatus::Running - { - if let TerminalType::Pty { info, .. } = &mut self.terminal_type { - info.kill_current_process(); - } - } - } - - pub fn pid(&self) -> Option { - match &self.terminal_type { - TerminalType::Pty { info, .. } => info.pid(), - TerminalType::DisplayOnly => None, - } - } - - pub fn pid_getter(&self) -> Option<&ProcessIdGetter> { - match &self.terminal_type { - TerminalType::Pty { info, .. } => Some(info.pid_getter()), - TerminalType::DisplayOnly => None, - } - } - - pub fn task(&self) -> Option<&TaskState> { - self.task.as_ref() - } - - pub fn wait_for_completed_task(&self, cx: &App) -> Task> { - if let Some(task) = self.task() { - if task.status == TaskStatus::Running { - let completion_receiver = task.completion_rx.clone(); - return cx.spawn(async move |_| completion_receiver.recv().await.ok().flatten()); - } else if let Ok(status) = task.completion_rx.try_recv() { - return Task::ready(status); - } - } - Task::ready(None) - } - - fn register_task_finished(&mut self, error_code: Option, cx: &mut Context) { - let e: Option = error_code.map(|code| { - #[cfg(unix)] - { - std::os::unix::process::ExitStatusExt::from_raw(code) - } - #[cfg(windows)] - { - std::os::windows::process::ExitStatusExt::from_raw(code as u32) - } - }); - - if let Some(tx) = &self.completion_tx { - tx.try_send(e).ok(); - } - if let Some(e) = e { - self.child_exited = Some(e); - } - let task = match &mut self.task { - Some(task) => task, - None => { - if self.child_exited.is_none_or(|e| e.code() == Some(0)) { - cx.emit(Event::CloseTerminal); - } - return; - } - }; - if task.status != TaskStatus::Running { - return; - } - match error_code { - Some(error_code) => { - task.status.register_task_exit(error_code); - } - None => { - task.status.register_terminal_exit(); - } - }; - - let (finished_successfully, task_line, command_line) = task_summary(task, error_code); - let mut lines_to_show = Vec::new(); - if task.spawned_task.show_summary { - lines_to_show.push(task_line.as_str()); - } - if task.spawned_task.show_command { - lines_to_show.push(command_line.as_str()); - } - - if !lines_to_show.is_empty() { - // SAFETY: the invocation happens on non `TaskStatus::Running` tasks, once, - // after either `AlacTermEvent::Exit` or `AlacTermEvent::ChildExit` events that are spawned - // when Zed task finishes and no more output is made. - // After the task summary is output once, no more text is appended to the terminal. - unsafe { append_text_to_term(&mut self.term.lock(), &lines_to_show) }; - } - - match task.spawned_task.hide { - HideStrategy::Never => {} - HideStrategy::Always => { - cx.emit(Event::CloseTerminal); - } - HideStrategy::OnSuccess => { - if finished_successfully { - cx.emit(Event::CloseTerminal); - } - } - } - } - - pub fn vi_mode_enabled(&self) -> bool { - self.vi_mode_enabled - } - - pub fn clone_builder(&self, cx: &App, cwd: Option) -> Task> { - let working_directory = self.working_directory().or_else(|| cwd); - TerminalBuilder::new( - working_directory, - None, - self.template.shell.clone(), - self.template.env.clone(), - self.template.cursor_shape, - self.template.alternate_scroll, - self.template.max_scroll_history_lines, - self.template.path_hyperlink_regexes.clone(), - self.template.path_hyperlink_timeout_ms, - self.is_remote_terminal, - self.template.window_id, - None, - cx, - self.activation_script.clone(), - ) - } -} - -// Helper function to convert a grid row to a string -pub fn row_to_string(row: &Row) -> String { - row[..Column(row.len())] - .iter() - .map(|cell| cell.c) - .collect::() -} - -const TASK_DELIMITER: &str = "⏵ "; -fn task_summary(task: &TaskState, error_code: Option) -> (bool, String, String) { - let escaped_full_label = task - .spawned_task - .full_label - .replace("\r\n", "\r") - .replace('\n', "\r"); - let success = error_code == Some(0); - let task_line = match error_code { - Some(0) => format!("{TASK_DELIMITER}Task `{escaped_full_label}` finished successfully"), - Some(error_code) => format!( - "{TASK_DELIMITER}Task `{escaped_full_label}` finished with non-zero error code: {error_code}" - ), - None => format!("{TASK_DELIMITER}Task `{escaped_full_label}` finished"), - }; - let escaped_command_label = task - .spawned_task - .command_label - .replace("\r\n", "\r") - .replace('\n', "\r"); - let command_line = format!("{TASK_DELIMITER}Command: {escaped_command_label}"); - (success, task_line, command_line) -} - -/// Appends a stringified task summary to the terminal, after its output. -/// -/// SAFETY: This function should only be called after terminal's PTY is no longer alive. -/// New text being added to the terminal here, uses "less public" APIs, -/// which are not maintaining the entire terminal state intact. -/// -/// -/// The library -/// -/// * does not increment inner grid cursor's _lines_ on `input` calls -/// (but displaying the lines correctly and incrementing cursor's columns) -/// -/// * ignores `\n` and \r` character input, requiring the `newline` call instead -/// -/// * does not alter grid state after `newline` call -/// so its `bottommost_line` is always the same additions, and -/// the cursor's `point` is not updated to the new line and column values -/// -/// * ??? there could be more consequences, and any further "proper" streaming from the PTY might bug and/or panic. -/// Still, subsequent `append_text_to_term` invocations are possible and display the contents correctly. -/// -/// Despite the quirks, this is the simplest approach to appending text to the terminal: its alternative, `grid_mut` manipulations, -/// do not properly set the scrolling state and display odd text after appending; also those manipulations are more tedious and error-prone. -/// The function achieves proper display and scrolling capabilities, at a cost of grid state not properly synchronized. -/// This is enough for printing moderately-sized texts like task summaries, but might break or perform poorly for larger texts. -unsafe fn append_text_to_term(term: &mut Term, text_lines: &[&str]) { - term.newline(); - term.grid_mut().cursor.point.column = Column(0); - for line in text_lines { - for c in line.chars() { - term.input(c); - } - term.newline(); - term.grid_mut().cursor.point.column = Column(0); - } -} - -impl Drop for Terminal { - fn drop(&mut self) { - if let TerminalType::Pty { pty_tx, info } = &mut self.terminal_type { - info.kill_child_process(); - pty_tx.0.send(Msg::Shutdown).ok(); - } - } -} - -impl EventEmitter for Terminal {} - -fn make_selection(range: &RangeInclusive) -> Selection { - let mut selection = Selection::new(SelectionType::Simple, *range.start(), AlacDirection::Left); - selection.update(*range.end(), AlacDirection::Right); - selection -} - -fn all_search_matches<'a, T>( - term: &'a Term, - regex: &'a mut RegexSearch, -) -> impl Iterator + 'a { - let start = AlacPoint::new(term.grid().topmost_line(), Column(0)); - let end = AlacPoint::new(term.grid().bottommost_line(), term.grid().last_column()); - RegexIter::new(start, end, AlacDirection::Right, term, regex) -} - -fn content_index_for_mouse(pos: Point, terminal_bounds: &TerminalBounds) -> usize { - let col = (pos.x / terminal_bounds.cell_width()).round() as usize; - let clamped_col = min(col, terminal_bounds.columns() - 1); - let row = (pos.y / terminal_bounds.line_height()).round() as usize; - let clamped_row = min(row, terminal_bounds.screen_lines() - 1); - clamped_row * terminal_bounds.columns() + clamped_col -} - -/// Converts an 8 bit ANSI color to its GPUI equivalent. -/// Accepts `usize` for compatibility with the `alacritty::Colors` interface, -/// Other than that use case, should only be called with values in the `[0,255]` range -pub fn get_color_at_index(index: usize, theme: &Theme) -> Hsla { - let colors = theme.colors(); - - match index { - // 0-15 are the same as the named colors above - 0 => colors.terminal_ansi_black, - 1 => colors.terminal_ansi_red, - 2 => colors.terminal_ansi_green, - 3 => colors.terminal_ansi_yellow, - 4 => colors.terminal_ansi_blue, - 5 => colors.terminal_ansi_magenta, - 6 => colors.terminal_ansi_cyan, - 7 => colors.terminal_ansi_white, - 8 => colors.terminal_ansi_bright_black, - 9 => colors.terminal_ansi_bright_red, - 10 => colors.terminal_ansi_bright_green, - 11 => colors.terminal_ansi_bright_yellow, - 12 => colors.terminal_ansi_bright_blue, - 13 => colors.terminal_ansi_bright_magenta, - 14 => colors.terminal_ansi_bright_cyan, - 15 => colors.terminal_ansi_bright_white, - // 16-231 are a 6x6x6 RGB color cube, mapped to 0-255 using steps defined by XTerm. - // See: https://github.com/xterm-x11/xterm-snapshots/blob/master/256colres.pl - 16..=231 => { - let (r, g, b) = rgb_for_index(index as u8); - rgba_color( - if r == 0 { 0 } else { r * 40 + 55 }, - if g == 0 { 0 } else { g * 40 + 55 }, - if b == 0 { 0 } else { b * 40 + 55 }, - ) - } - // 232-255 are a 24-step grayscale ramp from (8, 8, 8) to (238, 238, 238). - 232..=255 => { - let i = index as u8 - 232; // Align index to 0..24 - let value = i * 10 + 8; - rgba_color(value, value, value) - } - // For compatibility with the alacritty::Colors interface - // See: https://github.com/alacritty/alacritty/blob/master/alacritty_terminal/src/term/color.rs - 256 => colors.terminal_foreground, - 257 => colors.terminal_background, - 258 => theme.players().local().cursor, - 259 => colors.terminal_ansi_dim_black, - 260 => colors.terminal_ansi_dim_red, - 261 => colors.terminal_ansi_dim_green, - 262 => colors.terminal_ansi_dim_yellow, - 263 => colors.terminal_ansi_dim_blue, - 264 => colors.terminal_ansi_dim_magenta, - 265 => colors.terminal_ansi_dim_cyan, - 266 => colors.terminal_ansi_dim_white, - 267 => colors.terminal_bright_foreground, - 268 => colors.terminal_ansi_black, // 'Dim Background', non-standard color - - _ => black(), - } -} - -/// Generates the RGB channels in [0, 5] for a given index into the 6x6x6 ANSI color cube. -/// -/// See: [8 bit ANSI color](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit). -/// -/// Wikipedia gives a formula for calculating the index for a given color: -/// -/// ```text -/// index = 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5) -/// ``` -/// -/// This function does the reverse, calculating the `r`, `g`, and `b` components from a given index. -fn rgb_for_index(i: u8) -> (u8, u8, u8) { - debug_assert!((16..=231).contains(&i)); - let i = i - 16; - let r = (i - (i % 36)) / 36; - let g = ((i % 36) - (i % 6)) / 6; - let b = (i % 36) % 6; - (r, g, b) -} - -pub fn rgba_color(r: u8, g: u8, b: u8) -> Hsla { - Rgba { - r: (r as f32 / 255.), - g: (g as f32 / 255.), - b: (b as f32 / 255.), - a: 1., - } - .into() -} - -#[cfg(test)] -mod tests { - use std::time::Duration; - - use super::*; - use crate::{ - IndexedCell, TerminalBounds, TerminalBuilder, TerminalContent, content_index_for_mouse, - rgb_for_index, - }; - use alacritty_terminal::{ - index::{Column, Line, Point as AlacPoint}, - term::cell::Cell, - }; - use collections::HashMap; - use gpui::{Pixels, Point, TestAppContext, bounds, point, size, smol_timeout}; - use rand::{Rng, distr, rngs::ThreadRng}; - use task::ShellBuilder; - - #[gpui::test] - async fn test_basic_terminal(cx: &mut TestAppContext) { - cx.executor().allow_parking(); - - let (completion_tx, completion_rx) = smol::channel::unbounded(); - let (program, args) = ShellBuilder::new(&Shell::System, false) - .build(Some("echo".to_owned()), &["hello".to_owned()]); - let builder = cx - .update(|cx| { - TerminalBuilder::new( - None, - None, - task::Shell::WithArguments { - program, - args, - title_override: None, - }, - HashMap::default(), - CursorShape::default(), - AlternateScroll::On, - None, - vec![], - 0, - false, - 0, - Some(completion_tx), - cx, - vec![], - ) - }) - .await - .unwrap(); - let terminal = cx.new(|cx| builder.subscribe(cx)); - assert_eq!( - completion_rx.recv().await.unwrap(), - Some(ExitStatus::default()) - ); - assert_eq!( - terminal.update(cx, |term, _| term.get_content()).trim(), - "hello" - ); - - // Inject additional output directly into the emulator (display-only path) - terminal.update(cx, |term, cx| { - term.write_output(b"\nfrom_injection", cx); - }); - - let content_after = terminal.update(cx, |term, _| term.get_content()); - assert!( - content_after.contains("from_injection"), - "expected injected output to appear, got: {content_after}" - ); - } - - // TODO should be tested on Linux too, but does not work there well - #[cfg(target_os = "macos")] - #[gpui::test(iterations = 10)] - async fn test_terminal_eof(cx: &mut TestAppContext) { - cx.executor().allow_parking(); - - let (completion_tx, completion_rx) = smol::channel::unbounded(); - let builder = cx - .update(|cx| { - TerminalBuilder::new( - None, - None, - task::Shell::System, - HashMap::default(), - CursorShape::default(), - AlternateScroll::On, - None, - vec![], - 0, - false, - 0, - Some(completion_tx), - cx, - Vec::new(), - ) - }) - .await - .unwrap(); - // Build an empty command, which will result in a tty shell spawned. - let terminal = cx.new(|cx| builder.subscribe(cx)); - - let (event_tx, event_rx) = smol::channel::unbounded::(); - cx.update(|cx| { - cx.subscribe(&terminal, move |_, e, _| { - event_tx.send_blocking(e.clone()).unwrap(); - }) - }) - .detach(); - cx.background_spawn(async move { - assert_eq!( - completion_rx.recv().await.unwrap(), - Some(ExitStatus::default()), - "EOF should result in the tty shell exiting successfully", - ); - }) - .detach(); - - let first_event = event_rx.recv().await.expect("No wakeup event received"); - - terminal.update(cx, |terminal, _| { - let success = terminal.try_keystroke(&Keystroke::parse("ctrl-c").unwrap(), false); - assert!(success, "Should have registered ctrl-c sequence"); - }); - terminal.update(cx, |terminal, _| { - let success = terminal.try_keystroke(&Keystroke::parse("ctrl-d").unwrap(), false); - assert!(success, "Should have registered ctrl-d sequence"); - }); - - let mut all_events = vec![first_event]; - while let Ok(Ok(new_event)) = smol_timeout(Duration::from_secs(1), event_rx.recv()).await { - all_events.push(new_event.clone()); - if new_event == Event::CloseTerminal { - break; - } - } - assert!( - all_events.contains(&Event::CloseTerminal), - "EOF command sequence should have triggered a TTY terminal exit, but got events: {all_events:?}", - ); - } - - #[gpui::test(iterations = 10)] - async fn test_terminal_no_exit_on_spawn_failure(cx: &mut TestAppContext) { - cx.executor().allow_parking(); - - let (completion_tx, completion_rx) = smol::channel::unbounded(); - let (program, args) = ShellBuilder::new(&Shell::System, false) - .build(Some("asdasdasdasd".to_owned()), &["@@@@@".to_owned()]); - let builder = cx - .update(|cx| { - TerminalBuilder::new( - None, - None, - task::Shell::WithArguments { - program, - args, - title_override: None, - }, - HashMap::default(), - CursorShape::default(), - AlternateScroll::On, - None, - Vec::new(), - 0, - false, - 0, - Some(completion_tx), - cx, - Vec::new(), - ) - }) - .await - .unwrap(); - let terminal = cx.new(|cx| builder.subscribe(cx)); - - let (event_tx, event_rx) = smol::channel::unbounded::(); - cx.update(|cx| { - cx.subscribe(&terminal, move |_, e, _| { - event_tx.send_blocking(e.clone()).unwrap(); - }) - }) - .detach(); - cx.background_spawn(async move { - #[cfg(target_os = "windows")] - { - let exit_status = completion_rx.recv().await.ok().flatten(); - if let Some(exit_status) = exit_status { - assert!( - !exit_status.success(), - "Wrong shell command should result in a failure" - ); - assert_eq!(exit_status.code(), Some(1)); - } - } - #[cfg(not(target_os = "windows"))] - { - let exit_status = completion_rx.recv().await.unwrap().unwrap(); - assert!( - !exit_status.success(), - "Wrong shell command should result in a failure" - ); - assert_eq!(exit_status.code(), None); - } - }) - .detach(); - - let mut all_events = Vec::new(); - while let Ok(Ok(new_event)) = - smol_timeout(Duration::from_millis(500), event_rx.recv()).await - { - all_events.push(new_event.clone()); - } - - assert!( - !all_events - .iter() - .any(|event| event == &Event::CloseTerminal), - "Wrong shell command should update the title but not should not close the terminal to show the error message, but got events: {all_events:?}", - ); - } - - #[test] - fn test_rgb_for_index() { - // Test every possible value in the color cube. - for i in 16..=231 { - let (r, g, b) = rgb_for_index(i); - assert_eq!(i, 16 + 36 * r + 6 * g + b); - } - } - - #[test] - fn test_mouse_to_cell_test() { - let mut rng = rand::rng(); - const ITERATIONS: usize = 10; - const PRECISION: usize = 1000; - - for _ in 0..ITERATIONS { - let viewport_cells = rng.random_range(15..20); - let cell_size = - rng.random_range(5 * PRECISION..20 * PRECISION) as f32 / PRECISION as f32; - - let size = crate::TerminalBounds { - cell_width: Pixels::from(cell_size), - line_height: Pixels::from(cell_size), - bounds: bounds( - Point::default(), - size( - Pixels::from(cell_size * (viewport_cells as f32)), - Pixels::from(cell_size * (viewport_cells as f32)), - ), - ), - }; - - let cells = get_cells(size, &mut rng); - let content = convert_cells_to_content(size, &cells); - - for row in 0..(viewport_cells - 1) { - let row = row as usize; - for col in 0..(viewport_cells - 1) { - let col = col as usize; - - let row_offset = rng.random_range(0..PRECISION) as f32 / PRECISION as f32; - let col_offset = rng.random_range(0..PRECISION) as f32 / PRECISION as f32; - - let mouse_pos = point( - Pixels::from(col as f32 * cell_size + col_offset), - Pixels::from(row as f32 * cell_size + row_offset), - ); - - let content_index = - content_index_for_mouse(mouse_pos, &content.terminal_bounds); - let mouse_cell = content.cells[content_index].c; - let real_cell = cells[row][col]; - - assert_eq!(mouse_cell, real_cell); - } - } - } - } - - #[test] - fn test_mouse_to_cell_clamp() { - let mut rng = rand::rng(); - - let size = crate::TerminalBounds { - cell_width: Pixels::from(10.), - line_height: Pixels::from(10.), - bounds: bounds( - Point::default(), - size(Pixels::from(100.), Pixels::from(100.)), - ), - }; - - let cells = get_cells(size, &mut rng); - let content = convert_cells_to_content(size, &cells); - - assert_eq!( - content.cells[content_index_for_mouse( - point(Pixels::from(-10.), Pixels::from(-10.)), - &content.terminal_bounds, - )] - .c, - cells[0][0] - ); - assert_eq!( - content.cells[content_index_for_mouse( - point(Pixels::from(1000.), Pixels::from(1000.)), - &content.terminal_bounds, - )] - .c, - cells[9][9] - ); - } - - fn get_cells(size: TerminalBounds, rng: &mut ThreadRng) -> Vec> { - let mut cells = Vec::new(); - - for _ in 0..((size.height() / size.line_height()) as usize) { - let mut row_vec = Vec::new(); - for _ in 0..((size.width() / size.cell_width()) as usize) { - let cell_char = rng.sample(distr::Alphanumeric) as char; - row_vec.push(cell_char) - } - cells.push(row_vec) - } - - cells - } - - fn convert_cells_to_content( - terminal_bounds: TerminalBounds, - cells: &[Vec], - ) -> TerminalContent { - let mut ic = Vec::new(); - - for (index, row) in cells.iter().enumerate() { - for (cell_index, cell_char) in row.iter().enumerate() { - ic.push(IndexedCell { - point: AlacPoint::new(Line(index as i32), Column(cell_index)), - cell: Cell { - c: *cell_char, - ..Default::default() - }, - }); - } - } - - TerminalContent { - cells: ic, - terminal_bounds, - ..Default::default() - } - } - - #[gpui::test] - async fn test_write_output_converts_lf_to_crlf(cx: &mut TestAppContext) { - let terminal = cx.new(|cx| { - TerminalBuilder::new_display_only(CursorShape::default(), AlternateScroll::On, None, 0) - .unwrap() - .subscribe(cx) - }); - - // Test simple LF conversion - terminal.update(cx, |terminal, cx| { - terminal.write_output(b"line1\nline2\n", cx); - }); - - // Get the content by directly accessing the term - let content = terminal.update(cx, |terminal, _cx| { - let term = terminal.term.lock_unfair(); - Terminal::make_content(&term, &terminal.last_content) - }); - - // If LF is properly converted to CRLF, each line should start at column 0 - // The diagonal staircase bug would cause increasing column positions - - // Get the cells and check that lines start at column 0 - let cells = &content.cells; - let mut line1_col0 = false; - let mut line2_col0 = false; - - for cell in cells { - if cell.c == 'l' && cell.point.column.0 == 0 { - if cell.point.line.0 == 0 && !line1_col0 { - line1_col0 = true; - } else if cell.point.line.0 == 1 && !line2_col0 { - line2_col0 = true; - } - } - } - - assert!(line1_col0, "First line should start at column 0"); - assert!(line2_col0, "Second line should start at column 0"); - } - - #[gpui::test] - async fn test_write_output_preserves_existing_crlf(cx: &mut TestAppContext) { - let terminal = cx.new(|cx| { - TerminalBuilder::new_display_only(CursorShape::default(), AlternateScroll::On, None, 0) - .unwrap() - .subscribe(cx) - }); - - // Test that existing CRLF doesn't get doubled - terminal.update(cx, |terminal, cx| { - terminal.write_output(b"line1\r\nline2\r\n", cx); - }); - - // Get the content by directly accessing the term - let content = terminal.update(cx, |terminal, _cx| { - let term = terminal.term.lock_unfair(); - Terminal::make_content(&term, &terminal.last_content) - }); - - let cells = &content.cells; - - // Check that both lines start at column 0 - let mut found_lines_at_column_0 = 0; - for cell in cells { - if cell.c == 'l' && cell.point.column.0 == 0 { - found_lines_at_column_0 += 1; - } - } - - assert!( - found_lines_at_column_0 >= 2, - "Both lines should start at column 0" - ); - } - - #[gpui::test] - async fn test_write_output_preserves_bare_cr(cx: &mut TestAppContext) { - let terminal = cx.new(|cx| { - TerminalBuilder::new_display_only(CursorShape::default(), AlternateScroll::On, None, 0) - .unwrap() - .subscribe(cx) - }); - - // Test that bare CR (without LF) is preserved - terminal.update(cx, |terminal, cx| { - terminal.write_output(b"hello\rworld", cx); - }); - - // Get the content by directly accessing the term - let content = terminal.update(cx, |terminal, _cx| { - let term = terminal.term.lock_unfair(); - Terminal::make_content(&term, &terminal.last_content) - }); - - let cells = &content.cells; - - // Check that we have "world" at the beginning of the line - let mut text = String::new(); - for cell in cells.iter().take(5) { - if cell.point.line.0 == 0 { - text.push(cell.c); - } - } - - assert!( - text.starts_with("world"), - "Bare CR should allow overwriting: got '{}'", - text - ); - } -} diff --git a/crates/terminal/src/terminal_hyperlinks.rs b/crates/terminal/src/terminal_hyperlinks.rs deleted file mode 100644 index 71a1634076..0000000000 --- a/crates/terminal/src/terminal_hyperlinks.rs +++ /dev/null @@ -1,1673 +0,0 @@ -use alacritty_terminal::{ - Term, - event::EventListener, - grid::Dimensions, - index::{Boundary, Column, Direction as AlacDirection, Point as AlacPoint}, - term::{ - cell::Flags, - search::{Match, RegexIter, RegexSearch}, - }, -}; -use log::{info, warn}; -use regex::Regex; -use std::{ - ops::{Index, Range}, - time::{Duration, Instant}, -}; - -const URL_REGEX: &str = r#"(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file://|git://|ssh:|ftp://)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>"\s{-}\^⟨⟩`']+"#; -const WIDE_CHAR_SPACERS: Flags = - Flags::from_bits(Flags::LEADING_WIDE_CHAR_SPACER.bits() | Flags::WIDE_CHAR_SPACER.bits()) - .unwrap(); - -pub(super) struct RegexSearches { - url_regex: RegexSearch, - path_hyperlink_regexes: Vec, - path_hyperlink_timeout: Duration, -} - -impl Default for RegexSearches { - fn default() -> Self { - Self { - url_regex: RegexSearch::new(URL_REGEX).unwrap(), - path_hyperlink_regexes: Vec::default(), - path_hyperlink_timeout: Duration::default(), - } - } -} -impl RegexSearches { - pub(super) fn new( - path_hyperlink_regexes: impl IntoIterator>, - path_hyperlink_timeout_ms: u64, - ) -> Self { - Self { - url_regex: RegexSearch::new(URL_REGEX).unwrap(), - path_hyperlink_regexes: path_hyperlink_regexes - .into_iter() - .filter_map(|regex| { - Regex::new(regex.as_ref()) - .inspect_err(|error| { - warn!( - concat!( - "Ignoring path hyperlink regex specified in ", - "`terminal.path_hyperlink_regexes`:\n\n\t{}\n\nError: {}", - ), - regex.as_ref(), - error - ); - }) - .ok() - }) - .collect(), - path_hyperlink_timeout: Duration::from_millis(path_hyperlink_timeout_ms), - } - } -} - -pub(super) fn find_from_grid_point( - term: &Term, - point: AlacPoint, - regex_searches: &mut RegexSearches, -) -> Option<(String, bool, Match)> { - let grid = term.grid(); - let link = grid.index(point).hyperlink(); - let found_word = if let Some(ref url) = link { - let mut min_index = point; - loop { - let new_min_index = min_index.sub(term, Boundary::Cursor, 1); - if new_min_index == min_index || grid.index(new_min_index).hyperlink() != link { - break; - } else { - min_index = new_min_index - } - } - - let mut max_index = point; - loop { - let new_max_index = max_index.add(term, Boundary::Cursor, 1); - if new_max_index == max_index || grid.index(new_max_index).hyperlink() != link { - break; - } else { - max_index = new_max_index - } - } - - let url = url.uri().to_owned(); - let url_match = min_index..=max_index; - - Some((url, true, url_match)) - } else { - let (line_start, line_end) = (term.line_search_left(point), term.line_search_right(point)); - if let Some((url, url_match)) = RegexIter::new( - line_start, - line_end, - AlacDirection::Right, - term, - &mut regex_searches.url_regex, - ) - .find(|rm| rm.contains(&point)) - .map(|url_match| { - let url = term.bounds_to_string(*url_match.start(), *url_match.end()); - sanitize_url_punctuation(url, url_match, term) - }) { - Some((url, true, url_match)) - } else { - path_match( - &term, - line_start, - line_end, - point, - &mut regex_searches.path_hyperlink_regexes, - regex_searches.path_hyperlink_timeout, - ) - .map(|(path, path_match)| (path, false, path_match)) - } - }; - - found_word.map(|(maybe_url_or_path, is_url, word_match)| { - if is_url { - // Treat "file://" IRIs like file paths to ensure - // that line numbers at the end of the path are - // handled correctly - if let Some(path) = maybe_url_or_path.strip_prefix("file://") { - (path.to_string(), false, word_match) - } else { - (maybe_url_or_path, true, word_match) - } - } else { - (maybe_url_or_path, false, word_match) - } - }) -} - -fn sanitize_url_punctuation( - url: String, - url_match: Match, - term: &Term, -) -> (String, Match) { - let mut sanitized_url = url; - let mut chars_trimmed = 0; - - // First, handle parentheses balancing using single traversal - let (open_parens, close_parens) = - sanitized_url - .chars() - .fold((0, 0), |(opens, closes), c| match c { - '(' => (opens + 1, closes), - ')' => (opens, closes + 1), - _ => (opens, closes), - }); - - // Trim unbalanced closing parentheses - if close_parens > open_parens { - let mut remaining_close = close_parens; - while sanitized_url.ends_with(')') && remaining_close > open_parens { - sanitized_url.pop(); - chars_trimmed += 1; - remaining_close -= 1; - } - } - - // Handle trailing periods - if sanitized_url.ends_with('.') { - let trailing_periods = sanitized_url - .chars() - .rev() - .take_while(|&c| c == '.') - .count(); - - if trailing_periods > 1 { - sanitized_url.truncate(sanitized_url.len() - trailing_periods); - chars_trimmed += trailing_periods; - } else if trailing_periods == 1 - && let Some(second_last_char) = sanitized_url.chars().rev().nth(1) - && (second_last_char.is_alphanumeric() || second_last_char == '/') - { - sanitized_url.pop(); - chars_trimmed += 1; - } - } - - if chars_trimmed > 0 { - let new_end = url_match.end().sub(term, Boundary::Grid, chars_trimmed); - let sanitized_match = Match::new(*url_match.start(), new_end); - (sanitized_url, sanitized_match) - } else { - (sanitized_url, url_match) - } -} - -fn path_match( - term: &Term, - line_start: AlacPoint, - line_end: AlacPoint, - hovered: AlacPoint, - path_hyperlink_regexes: &mut Vec, - path_hyperlink_timeout: Duration, -) -> Option<(String, Match)> { - if path_hyperlink_regexes.is_empty() || path_hyperlink_timeout.as_millis() == 0 { - return None; - } - debug_assert!(line_start <= hovered); - debug_assert!(line_end >= hovered); - let search_start_time = Instant::now(); - - let timed_out = || { - let elapsed_time = Instant::now().saturating_duration_since(search_start_time); - (elapsed_time > path_hyperlink_timeout) - .then_some((elapsed_time.as_millis(), path_hyperlink_timeout.as_millis())) - }; - - // This used to be: `let line = term.bounds_to_string(line_start, line_end)`, however, that - // api compresses tab characters into a single space, whereas we require a cell accurate - // string representation of the line. The below algorithm does this, but seems a bit odd. - // Maybe there is a clean api for doing this, but I couldn't find it. - let mut line = String::with_capacity( - (line_end.line.0 - line_start.line.0 + 1) as usize * term.grid().columns(), - ); - let first_cell = &term.grid()[line_start]; - line.push(first_cell.c); - let mut start_offset = 0; - let mut hovered_point_byte_offset = None; - - if !first_cell.flags.intersects(WIDE_CHAR_SPACERS) { - start_offset += first_cell.c.len_utf8(); - if line_start == hovered { - hovered_point_byte_offset = Some(0); - } - } - - for cell in term.grid().iter_from(line_start) { - if cell.point > line_end { - break; - } - let is_spacer = cell.flags.intersects(WIDE_CHAR_SPACERS); - if cell.point == hovered { - debug_assert!(hovered_point_byte_offset.is_none()); - if start_offset > 0 && cell.flags.contains(Flags::WIDE_CHAR_SPACER) { - // If we hovered on a trailing spacer, back up to the end of the previous char's bytes. - start_offset -= 1; - } - hovered_point_byte_offset = Some(start_offset); - } else if cell.point < hovered && !is_spacer { - start_offset += cell.c.len_utf8(); - } - - if !is_spacer { - line.push(match cell.c { - '\t' => ' ', - c @ _ => c, - }); - } - } - let line = line.trim_ascii_end(); - let hovered_point_byte_offset = hovered_point_byte_offset?; - let found_from_range = |path_range: Range, - link_range: Range, - position: Option<(u32, Option)>| { - let advance_point_by_str = |mut point: AlacPoint, s: &str| { - for _ in s.chars() { - point = term - .expand_wide(point, AlacDirection::Right) - .add(term, Boundary::Grid, 1); - } - - // There does not appear to be an alacritty api that is - // "move to start of current wide char", so we have to do it ourselves. - let flags = term.grid().index(point).flags; - if flags.contains(Flags::LEADING_WIDE_CHAR_SPACER) { - AlacPoint::new(point.line + 1, Column(0)) - } else if flags.contains(Flags::WIDE_CHAR_SPACER) { - AlacPoint::new(point.line, point.column - 1) - } else { - point - } - }; - - let link_start = advance_point_by_str(line_start, &line[..link_range.start]); - let link_end = advance_point_by_str(link_start, &line[link_range]); - let link_match = link_start - ..=term - .expand_wide(link_end, AlacDirection::Left) - .sub(term, Boundary::Grid, 1); - - ( - { - let mut path = line[path_range].to_string(); - position.inspect(|(line, column)| { - path += &format!(":{line}"); - column.inspect(|column| path += &format!(":{column}")); - }); - path - }, - link_match, - ) - }; - - for regex in path_hyperlink_regexes { - let mut path_found = false; - - for captures in regex.captures_iter(&line) { - path_found = true; - let match_range = captures.get(0).unwrap().range(); - let (path_range, line_column) = if let Some(path) = captures.name("path") { - let parse = |name: &str| { - captures - .name(name) - .and_then(|capture| capture.as_str().parse().ok()) - }; - - ( - path.range(), - parse("line").map(|line| (line, parse("column"))), - ) - } else { - (match_range.clone(), None) - }; - let link_range = captures - .name("link") - .map_or_else(|| match_range.clone(), |link| link.range()); - - if !link_range.contains(&hovered_point_byte_offset) { - // No match, just skip. - continue; - } - let found = found_from_range(path_range, link_range, line_column); - - if found.1.contains(&hovered) { - return Some(found); - } - } - - if path_found { - return None; - } - - if let Some((timed_out_ms, timeout_ms)) = timed_out() { - warn!("Timed out processing path hyperlink regexes after {timed_out_ms}ms"); - info!("{timeout_ms}ms time out specified in `terminal.path_hyperlink_timeout_ms`"); - return None; - } - } - - None -} - -#[cfg(test)] -mod tests { - use crate::terminal_settings::TerminalSettings; - - use super::*; - use alacritty_terminal::{ - event::VoidListener, - grid::Dimensions, - index::{Boundary, Column, Line, Point as AlacPoint}, - term::{Config, cell::Flags, test::TermSize}, - vte::ansi::Handler, - }; - use regex::Regex; - use settings::{self, Settings, SettingsContent}; - use std::{cell::RefCell, ops::RangeInclusive, path::PathBuf, rc::Rc}; - use url::Url; - use util::paths::PathWithPosition; - - fn re_test(re: &str, hay: &str, expected: Vec<&str>) { - let results: Vec<_> = Regex::new(re) - .unwrap() - .find_iter(hay) - .map(|m| m.as_str()) - .collect(); - assert_eq!(results, expected); - } - - #[test] - fn test_url_regex() { - re_test( - URL_REGEX, - "test http://example.com test 'https://website1.com' test mailto:bob@example.com train", - vec![ - "http://example.com", - "https://website1.com", - "mailto:bob@example.com", - ], - ); - } - - #[test] - fn test_url_parentheses_sanitization() { - // Test our sanitize_url_parentheses function directly - let test_cases = vec![ - // Cases that should be sanitized (unbalanced parentheses) - ("https://www.google.com/)", "https://www.google.com/"), - ("https://example.com/path)", "https://example.com/path"), - ("https://test.com/))", "https://test.com/"), - // Cases that should NOT be sanitized (balanced parentheses) - ( - "https://en.wikipedia.org/wiki/Example_(disambiguation)", - "https://en.wikipedia.org/wiki/Example_(disambiguation)", - ), - ("https://test.com/(hello)", "https://test.com/(hello)"), - ( - "https://example.com/path(1)(2)", - "https://example.com/path(1)(2)", - ), - // Edge cases - ("https://test.com/", "https://test.com/"), - ("https://example.com", "https://example.com"), - ]; - - for (input, expected) in test_cases { - // Create a minimal terminal for testing - let term = Term::new(Config::default(), &TermSize::new(80, 24), VoidListener); - - // Create a dummy match that spans the entire input - let start_point = AlacPoint::new(Line(0), Column(0)); - let end_point = AlacPoint::new(Line(0), Column(input.len())); - let dummy_match = Match::new(start_point, end_point); - - let (result, _) = sanitize_url_punctuation(input.to_string(), dummy_match, &term); - assert_eq!(result, expected, "Failed for input: {}", input); - } - } - - #[test] - fn test_url_periods_sanitization() { - // Test URLs with trailing periods (sentence punctuation) - let test_cases = vec![ - // Cases that should be sanitized (trailing periods likely punctuation) - ("https://example.com.", "https://example.com"), - ( - "https://github.com/zed-industries/zed.", - "https://github.com/zed-industries/zed", - ), - ( - "https://example.com/path/file.html.", - "https://example.com/path/file.html", - ), - ( - "https://example.com/file.pdf.", - "https://example.com/file.pdf", - ), - ("https://example.com:8080.", "https://example.com:8080"), - ("https://example.com..", "https://example.com"), - ( - "https://en.wikipedia.org/wiki/C.E.O.", - "https://en.wikipedia.org/wiki/C.E.O", - ), - // Cases that should NOT be sanitized (periods are part of URL structure) - ( - "https://example.com/v1.0/api", - "https://example.com/v1.0/api", - ), - ("https://192.168.1.1", "https://192.168.1.1"), - ("https://sub.domain.com", "https://sub.domain.com"), - ]; - - for (input, expected) in test_cases { - // Create a minimal terminal for testing - let term = Term::new(Config::default(), &TermSize::new(80, 24), VoidListener); - - // Create a dummy match that spans the entire input - let start_point = AlacPoint::new(Line(0), Column(0)); - let end_point = AlacPoint::new(Line(0), Column(input.len())); - let dummy_match = Match::new(start_point, end_point); - - // This test should initially fail since we haven't implemented period sanitization yet - let (result, _) = sanitize_url_punctuation(input.to_string(), dummy_match, &term); - assert_eq!(result, expected, "Failed for input: {}", input); - } - } - - macro_rules! test_hyperlink { - ($($lines:expr),+; $hyperlink_kind:ident) => { { - use crate::terminal_hyperlinks::tests::line_cells_count; - use std::cmp; - - let test_lines = vec![$($lines),+]; - let (total_cells, longest_line_cells) = - test_lines.iter().copied() - .map(line_cells_count) - .fold((0, 0), |state, cells| (state.0 + cells, cmp::max(state.1, cells))); - let contains_tab_char = test_lines.iter().copied() - .map(str::chars).flatten().find(|&c| c == '\t'); - let columns = if contains_tab_char.is_some() { - // This avoids tabs at end of lines causing whitespace-eating line wraps... - vec![longest_line_cells + 1] - } else { - // Alacritty has issues with 2 columns, use 3 as the minimum for now. - vec![3, longest_line_cells / 2, longest_line_cells + 1] - }; - test_hyperlink!( - columns; - total_cells; - test_lines.iter().copied(); - $hyperlink_kind - ) - } }; - - ($columns:expr; $total_cells:expr; $lines:expr; $hyperlink_kind:ident) => { { - use crate::terminal_hyperlinks::tests::{ test_hyperlink, HyperlinkKind }; - - let source_location = format!("{}:{}", std::file!(), std::line!()); - for columns in $columns { - test_hyperlink(columns, $total_cells, $lines, HyperlinkKind::$hyperlink_kind, - &source_location); - } - } }; - } - - mod path { - /// 👉 := **hovered** on following char - /// - /// 👈 := **hovered** on wide char spacer of previous full width char - /// - /// **`‹›`** := expected **hyperlink** match - /// - /// **`«»`** := expected **path**, **row**, and **column** capture groups - /// - /// [**`c₀, c₁, …, cₙ;`**]ₒₚₜ := use specified terminal widths of `c₀, c₁, …, cₙ` **columns** - /// (defaults to `3, longest_line_cells / 2, longest_line_cells + 1;`) - /// - macro_rules! test_path { - ($($lines:literal),+) => { test_hyperlink!($($lines),+; Path) }; - } - - #[test] - fn simple() { - // Rust paths - // Just the path - test_path!("‹«/👉test/cool.rs»›"); - test_path!("‹«/test/cool👉.rs»›"); - - // path and line - test_path!("‹«/👉test/cool.rs»:«4»›"); - test_path!("‹«/test/cool.rs»👉:«4»›"); - test_path!("‹«/test/cool.rs»:«👉4»›"); - test_path!("‹«/👉test/cool.rs»(«4»)›"); - test_path!("‹«/test/cool.rs»👉(«4»)›"); - test_path!("‹«/test/cool.rs»(«👉4»)›"); - test_path!("‹«/test/cool.rs»(«4»👉)›"); - - // path, line, and column - test_path!("‹«/👉test/cool.rs»:«4»:«2»›"); - test_path!("‹«/test/cool.rs»:«4»:«👉2»›"); - test_path!("‹«/👉test/cool.rs»(«4»,«2»)›"); - test_path!("‹«/test/cool.rs»(«4»👉,«2»)›"); - - // path, line, column, and ':' suffix - test_path!("‹«/👉test/cool.rs»:«4»:«2»›:"); - test_path!("‹«/test/cool.rs»:«4»:«👉2»›:"); - test_path!("‹«/👉test/cool.rs»(«4»,«2»)›:"); - test_path!("‹«/test/cool.rs»(«4»,«2»👉)›:"); - test_path!("‹«/👉test/cool.rs»:(«4»,«2»)›:"); - test_path!("‹«/test/cool.rs»:(«4»,«2»👉)›:"); - test_path!("‹«/👉test/cool.rs»:(«4»:«2»)›:"); - test_path!("‹«/test/cool.rs»:(«4»:«2»👉)›:"); - test_path!("/test/cool.rs:4:2👉:", "What is this?"); - test_path!("/test/cool.rs(4,2)👉:", "What is this?"); - - // path, line, column, and description - test_path!("‹«/test/co👉ol.rs»:«4»:«2»›:Error!"); - test_path!("‹«/test/co👉ol.rs»(«4»,«2»)›:Error!"); - - // Cargo output - test_path!(" Compiling Cool 👉(/test/Cool)"); - test_path!(" Compiling Cool (‹«/👉test/Cool»›)"); - test_path!(" Compiling Cool (/test/Cool👉)"); - - // Python - test_path!("‹«awe👉some.py»›"); - test_path!("‹«👉a»› "); - - test_path!(" ‹F👉ile \"«/awesome.py»\", line «42»›: Wat?"); - test_path!(" ‹File \"«/awe👉some.py»\", line «42»›"); - test_path!(" ‹File \"«/awesome.py»👉\", line «42»›: Wat?"); - test_path!(" ‹File \"«/awesome.py»\", line «4👉2»›"); - } - - #[test] - fn simple_with_descriptions() { - // path, line, column and description - test_path!("‹«/👉test/cool.rs»:«4»:«2»›:例Desc例例例"); - test_path!("‹«/test/cool.rs»:«4»:«👉2»›:例Desc例例例"); - test_path!("‹«/👉test/cool.rs»(«4»,«2»)›:例Desc例例例"); - test_path!("‹«/test/cool.rs»(«4»👉,«2»)›:例Desc例例例"); - - // path, line, column and description w/extra colons - test_path!("‹«/👉test/cool.rs»:«4»:«2»›::例Desc例例例"); - test_path!("‹«/test/cool.rs»:«4»:«👉2»›::例Desc例例例"); - test_path!("‹«/👉test/cool.rs»(«4»,«2»)›::例Desc例例例"); - test_path!("‹«/test/cool.rs»(«4»,«2»👉)›::例Desc例例例"); - } - - #[test] - fn multiple_same_line() { - test_path!("‹«/👉test/cool.rs»› /test/cool.rs"); - test_path!("/test/cool.rs ‹«/👉test/cool.rs»›"); - - test_path!( - "‹«🦀 multiple_👉same_line 🦀» 🚣«4» 🏛️«2»›: 🦀 multiple_same_line 🦀 🚣4 🏛️2:" - ); - test_path!( - "🦀 multiple_same_line 🦀 🚣4 🏛️2 ‹«🦀 multiple_👉same_line 🦀» 🚣«4» 🏛️«2»›:" - ); - - // ls output (tab separated) - test_path!( - "‹«Carg👉o.toml»›\t\texperiments\t\tnotebooks\t\trust-toolchain.toml\ttooling" - ); - test_path!( - "Cargo.toml\t\t‹«exper👉iments»›\t\tnotebooks\t\trust-toolchain.toml\ttooling" - ); - test_path!( - "Cargo.toml\t\texperiments\t\t‹«note👉books»›\t\trust-toolchain.toml\ttooling" - ); - test_path!( - "Cargo.toml\t\texperiments\t\tnotebooks\t\t‹«rust-t👉oolchain.toml»›\ttooling" - ); - test_path!( - "Cargo.toml\t\texperiments\t\tnotebooks\t\trust-toolchain.toml\t‹«too👉ling»›" - ); - } - - #[test] - fn colons_galore() { - test_path!("‹«/test/co👉ol.rs»:«4»›"); - test_path!("‹«/test/co👉ol.rs»:«4»›:"); - test_path!("‹«/test/co👉ol.rs»:«4»:«2»›"); - test_path!("‹«/test/co👉ol.rs»:«4»:«2»›:"); - test_path!("‹«/test/co👉ol.rs»(«1»)›"); - test_path!("‹«/test/co👉ol.rs»(«1»)›:"); - test_path!("‹«/test/co👉ol.rs»(«1»,«618»)›"); - test_path!("‹«/test/co👉ol.rs»(«1»,«618»)›:"); - test_path!("‹«/test/co👉ol.rs»::«42»›"); - test_path!("‹«/test/co👉ol.rs»::«42»›:"); - test_path!("‹«/test/co👉ol.rs»(«1»,«618»)›::"); - } - - #[test] - fn quotes_and_brackets() { - test_path!("\"‹«/test/co👉ol.rs»:«4»›\""); - test_path!("'‹«/test/co👉ol.rs»:«4»›'"); - test_path!("`‹«/test/co👉ol.rs»:«4»›`"); - - test_path!("[‹«/test/co👉ol.rs»:«4»›]"); - test_path!("(‹«/test/co👉ol.rs»:«4»›)"); - test_path!("{‹«/test/co👉ol.rs»:«4»›}"); - test_path!("<‹«/test/co👉ol.rs»:«4»›>"); - - test_path!("[\"‹«/test/co👉ol.rs»:«4»›\"]"); - test_path!("'(‹«/test/co👉ol.rs»:«4»›)'"); - - test_path!("\"‹«/test/co👉ol.rs»:«4»:«2»›\""); - test_path!("'‹«/test/co👉ol.rs»:«4»:«2»›'"); - test_path!("`‹«/test/co👉ol.rs»:«4»:«2»›`"); - - test_path!("[‹«/test/co👉ol.rs»:«4»:«2»›]"); - test_path!("(‹«/test/co👉ol.rs»:«4»:«2»›)"); - test_path!("{‹«/test/co👉ol.rs»:«4»:«2»›}"); - test_path!("<‹«/test/co👉ol.rs»:«4»:«2»›>"); - - test_path!("[\"‹«/test/co👉ol.rs»:«4»:«2»›\"]"); - - test_path!("\"‹«/test/co👉ol.rs»(«4»)›\""); - test_path!("'‹«/test/co👉ol.rs»(«4»)›'"); - test_path!("`‹«/test/co👉ol.rs»(«4»)›`"); - - test_path!("[‹«/test/co👉ol.rs»(«4»)›]"); - test_path!("(‹«/test/co👉ol.rs»(«4»)›)"); - test_path!("{‹«/test/co👉ol.rs»(«4»)›}"); - test_path!("<‹«/test/co👉ol.rs»(«4»)›>"); - - test_path!("[\"‹«/test/co👉ol.rs»(«4»)›\"]"); - - test_path!("\"‹«/test/co👉ol.rs»(«4»,«2»)›\""); - test_path!("'‹«/test/co👉ol.rs»(«4»,«2»)›'"); - test_path!("`‹«/test/co👉ol.rs»(«4»,«2»)›`"); - - test_path!("[‹«/test/co👉ol.rs»(«4»,«2»)›]"); - test_path!("(‹«/test/co👉ol.rs»(«4»,«2»)›)"); - test_path!("{‹«/test/co👉ol.rs»(«4»,«2»)›}"); - test_path!("<‹«/test/co👉ol.rs»(«4»,«2»)›>"); - - test_path!("[\"‹«/test/co👉ol.rs»(«4»,«2»)›\"]"); - - // Imbalanced - test_path!("([‹«/test/co👉ol.rs»:«4»›] was here...)"); - test_path!("[Here's <‹«/test/co👉ol.rs»:«4»›>]"); - test_path!("('‹«/test/co👉ol.rs»:«4»›' was here...)"); - test_path!("[Here's `‹«/test/co👉ol.rs»:«4»›`]"); - } - - #[test] - fn trailing_punctuation() { - test_path!("‹«/test/co👉ol.rs»›:,.."); - test_path!("/test/cool.rs:,👉.."); - test_path!("‹«/test/co👉ol.rs»:«4»›:,"); - test_path!("/test/cool.rs:4:👉,"); - test_path!("[\"‹«/test/co👉ol.rs»:«4»›\"]:,"); - test_path!("'(‹«/test/co👉ol.rs»:«4»›),,'..."); - test_path!("('‹«/test/co👉ol.rs»:«4»›'::: was here...)"); - test_path!("[Here's <‹«/test/co👉ol.rs»:«4»›>]::: "); - } - - #[test] - fn word_wide_chars() { - // Rust paths - test_path!("‹«/👉例/cool.rs»›"); - test_path!("‹«/例👈/cool.rs»›"); - test_path!("‹«/例/cool.rs»:«👉4»›"); - test_path!("‹«/例/cool.rs»:«4»:«👉2»›"); - - // Cargo output - test_path!(" Compiling Cool (‹«/👉例/Cool»›)"); - test_path!(" Compiling Cool (‹«/例👈/Cool»›)"); - - test_path!(" Compiling Cool (‹«/👉例/Cool Spaces»›)"); - test_path!(" Compiling Cool (‹«/例👈/Cool Spaces»›)"); - test_path!(" Compiling Cool (‹«/👉例/Cool Spaces»:«4»:«2»›)"); - test_path!(" Compiling Cool (‹«/例👈/Cool Spaces»(«4»,«2»)›)"); - - test_path!(" --> ‹«/👉例/Cool Spaces»›"); - test_path!(" ::: ‹«/例👈/Cool Spaces»›"); - test_path!(" --> ‹«/👉例/Cool Spaces»:«4»:«2»›"); - test_path!(" ::: ‹«/例👈/Cool Spaces»(«4»,«2»)›"); - test_path!(" panicked at ‹«/👉例/Cool Spaces»:«4»:«2»›:"); - test_path!(" panicked at ‹«/例👈/Cool Spaces»(«4»,«2»)›:"); - test_path!(" at ‹«/👉例/Cool Spaces»:«4»:«2»›"); - test_path!(" at ‹«/例👈/Cool Spaces»(«4»,«2»)›"); - - // Python - test_path!("‹«👉例wesome.py»›"); - test_path!("‹«例👈wesome.py»›"); - test_path!(" ‹File \"«/👉例wesome.py»\", line «42»›: Wat?"); - test_path!(" ‹File \"«/例👈wesome.py»\", line «42»›: Wat?"); - } - - #[test] - fn non_word_wide_chars() { - // Mojo diagnostic message - test_path!(" ‹File \"«/awe👉some.🔥»\", line «42»›: Wat?"); - test_path!(" ‹File \"«/awesome👉.🔥»\", line «42»›: Wat?"); - test_path!(" ‹File \"«/awesome.👉🔥»\", line «42»›: Wat?"); - test_path!(" ‹File \"«/awesome.🔥👈»\", line «42»›: Wat?"); - } - - /// These likely rise to the level of being worth fixing. - mod issues { - #[test] - // - fn issue_alacritty_8586() { - // Rust paths - test_path!("‹«/👉例/cool.rs»›"); - test_path!("‹«/例👈/cool.rs»›"); - test_path!("‹«/例/cool.rs»:«👉4»›"); - test_path!("‹«/例/cool.rs»:«4»:«👉2»›"); - - // Cargo output - test_path!(" Compiling Cool (‹«/👉例/Cool»›)"); - test_path!(" Compiling Cool (‹«/例👈/Cool»›)"); - - // Python - test_path!("‹«👉例wesome.py»›"); - test_path!("‹«例👈wesome.py»›"); - test_path!(" ‹File \"«/👉例wesome.py»\", line «42»›: Wat?"); - test_path!(" ‹File \"«/例👈wesome.py»\", line «42»›: Wat?"); - } - - #[test] - // - fn issue_12338_regex() { - // Issue #12338 - test_path!(".rw-r--r-- 0 staff 05-27 14:03 ‹«'test file 👉1.txt'»›"); - test_path!(".rw-r--r-- 0 staff 05-27 14:03 ‹«👉'test file 1.txt'»›"); - } - - #[test] - // - fn issue_12338() { - // Issue #12338 - test_path!(".rw-r--r-- 0 staff 05-27 14:03 ‹«test👉、2.txt»›"); - test_path!(".rw-r--r-- 0 staff 05-27 14:03 ‹«test、👈2.txt»›"); - test_path!(".rw-r--r-- 0 staff 05-27 14:03 ‹«test👉。3.txt»›"); - test_path!(".rw-r--r-- 0 staff 05-27 14:03 ‹«test。👈3.txt»›"); - - // Rust paths - test_path!("‹«/👉🏃/🦀.rs»›"); - test_path!("‹«/🏃👈/🦀.rs»›"); - test_path!("‹«/🏃/👉🦀.rs»:«4»›"); - test_path!("‹«/🏃/🦀👈.rs»:«4»:«2»›"); - - // Cargo output - test_path!(" Compiling Cool (‹«/👉🏃/Cool»›)"); - test_path!(" Compiling Cool (‹«/🏃👈/Cool»›)"); - - // Python - test_path!("‹«👉🏃wesome.py»›"); - test_path!("‹«🏃👈wesome.py»›"); - test_path!(" ‹File \"«/👉🏃wesome.py»\", line «42»›: Wat?"); - test_path!(" ‹File \"«/🏃👈wesome.py»\", line «42»›: Wat?"); - - // Mojo - test_path!("‹«/awe👉some.🔥»› is some good Mojo!"); - test_path!("‹«/awesome👉.🔥»› is some good Mojo!"); - test_path!("‹«/awesome.👉🔥»› is some good Mojo!"); - test_path!("‹«/awesome.🔥👈»› is some good Mojo!"); - test_path!(" ‹File \"«/👉🏃wesome.🔥»\", line «42»›: Wat?"); - test_path!(" ‹File \"«/🏃👈wesome.🔥»\", line «42»›: Wat?"); - } - - #[test] - // - fn issue_40202() { - // Elixir - test_path!("[‹«lib/blitz_apex_👉server/stats/aggregate_rank_stats.ex»:«35»›: BlitzApexServer.Stats.AggregateRankStats.update/2] - 1 #=> 1"); - } - - #[test] - // - fn issue_28194() { - test_path!( - "‹«test/c👉ontrollers/template_items_controller_test.rb»:«20»›:in 'block (2 levels) in '" - ); - } - - #[test] - #[cfg_attr( - not(target_os = "windows"), - should_panic( - expected = "Path = «/test/cool.rs:4:NotDesc», at grid cells (0, 1)..=(7, 2)" - ) - )] - #[cfg_attr( - target_os = "windows", - should_panic( - expected = r#"Path = «C:\\test\\cool.rs:4:NotDesc», at grid cells (0, 1)..=(8, 1)"# - ) - )] - // PathWithPosition::parse_str considers "/test/co👉ol.rs:4:NotDesc" invalid input, but - // still succeeds and truncates the part after the position. Ideally this would be - // parsed as the path "/test/co👉ol.rs:4:NotDesc" with no position. - fn path_with_position_parse_str() { - test_path!("`‹«/test/co👉ol.rs:4:NotDesc»›`"); - test_path!("<‹«/test/co👉ol.rs:4:NotDesc»›>"); - - test_path!("'‹«(/test/co👉ol.rs:4:2)»›'"); - test_path!("'‹«(/test/co👉ol.rs(4))»›'"); - test_path!("'‹«(/test/co👉ol.rs(4,2))»›'"); - } - } - - /// Minor issues arguably not important enough to fix/workaround... - mod nits { - #[test] - fn alacritty_bugs_with_two_columns() { - test_path!("‹«/👉test/cool.rs»(«4»)›"); - test_path!("‹«/test/cool.rs»(«👉4»)›"); - test_path!("‹«/test/cool.rs»(«4»,«👉2»)›"); - - // Python - test_path!("‹«awe👉some.py»›"); - } - - #[test] - #[cfg_attr( - not(target_os = "windows"), - should_panic( - expected = "Path = «/test/cool.rs», line = 1, at grid cells (0, 0)..=(9, 0)" - ) - )] - #[cfg_attr( - target_os = "windows", - should_panic( - expected = r#"Path = «C:\\test\\cool.rs», line = 1, at grid cells (0, 0)..=(9, 2)"# - ) - )] - fn invalid_row_column_should_be_part_of_path() { - test_path!("‹«/👉test/cool.rs:1:618033988749»›"); - test_path!("‹«/👉test/cool.rs(1,618033988749)»›"); - } - - #[test] - #[cfg_attr( - not(target_os = "windows"), - should_panic(expected = "Path = «/te:st/co:ol.r:s:4:2::::::»") - )] - #[cfg_attr( - target_os = "windows", - should_panic(expected = r#"Path = «C:\\te:st\\co:ol.r:s:4:2::::::»"#) - )] - fn many_trailing_colons_should_be_parsed_as_part_of_the_path() { - test_path!("‹«/te:st/👉co:ol.r:s:4:2::::::»›"); - test_path!("/test/cool.rs:::👉:"); - } - } - - mod windows { - // Lots of fun to be had with long file paths (verbatim) and UNC paths on Windows. - // See - // See - // See - - #[test] - fn default_prompts() { - // Windows command prompt - test_path!(r#"‹«C:\Users\someone\👉test»›>"#); - test_path!(r#"C:\Users\someone\test👉>"#); - - // Windows PowerShell - test_path!(r#"PS ‹«C:\Users\someone\👉test\cool.rs»›>"#); - test_path!(r#"PS C:\Users\someone\test\cool.rs👉>"#); - } - - #[test] - fn unc() { - test_path!(r#"‹«\\server\share\👉test\cool.rs»›"#); - test_path!(r#"‹«\\server\share\test\cool👉.rs»›"#); - } - - mod issues { - #[test] - fn issue_verbatim() { - test_path!(r#"‹«\\?\C:\👉test\cool.rs»›"#); - test_path!(r#"‹«\\?\C:\test\cool👉.rs»›"#); - } - - #[test] - fn issue_verbatim_unc() { - test_path!(r#"‹«\\?\UNC\server\share\👉test\cool.rs»›"#); - test_path!(r#"‹«\\?\UNC\server\share\test\cool👉.rs»›"#); - } - } - } - - mod perf { - use super::super::*; - use crate::TerminalSettings; - use alacritty_terminal::{ - event::VoidListener, - grid::Dimensions, - index::{Column, Point as AlacPoint}, - term::test::mock_term, - term::{Term, search::Match}, - }; - use settings::{self, Settings, SettingsContent}; - use std::{cell::RefCell, rc::Rc}; - use util_macros::perf; - - fn build_test_term(line: &str) -> (Term, AlacPoint) { - let content = line.repeat(500); - let term = mock_term(&content); - let point = AlacPoint::new( - term.grid().bottommost_line() - 1, - Column(term.grid().last_column().0 / 2), - ); - - (term, point) - } - - #[perf] - pub fn cargo_hyperlink_benchmark() { - const LINE: &str = " Compiling terminal v0.1.0 (/Hyperlinks/Bench/Source/zed-hyperlinks/crates/terminal)\r\n"; - thread_local! { - static TEST_TERM_AND_POINT: (Term, AlacPoint) = - build_test_term(LINE); - } - TEST_TERM_AND_POINT.with(|(term, point)| { - assert!( - find_from_grid_point_bench(term, *point).is_some(), - "Hyperlink should have been found" - ); - }); - } - - #[perf] - pub fn rust_hyperlink_benchmark() { - const LINE: &str = " --> /Hyperlinks/Bench/Source/zed-hyperlinks/crates/terminal/terminal.rs:1000:42\r\n"; - thread_local! { - static TEST_TERM_AND_POINT: (Term, AlacPoint) = - build_test_term(LINE); - } - TEST_TERM_AND_POINT.with(|(term, point)| { - assert!( - find_from_grid_point_bench(term, *point).is_some(), - "Hyperlink should have been found" - ); - }); - } - - #[perf] - pub fn ls_hyperlink_benchmark() { - const LINE: &str = "Cargo.toml experiments notebooks rust-toolchain.toml tooling\r\n"; - thread_local! { - static TEST_TERM_AND_POINT: (Term, AlacPoint) = - build_test_term(LINE); - } - TEST_TERM_AND_POINT.with(|(term, point)| { - assert!( - find_from_grid_point_bench(term, *point).is_some(), - "Hyperlink should have been found" - ); - }); - } - - pub fn find_from_grid_point_bench( - term: &Term, - point: AlacPoint, - ) -> Option<(String, bool, Match)> { - const PATH_HYPERLINK_TIMEOUT_MS: u64 = 1000; - - thread_local! { - static TEST_REGEX_SEARCHES: RefCell = - RefCell::new({ - let default_settings_content: Rc = - settings::parse_json_with_comments(&settings::default_settings()) - .unwrap(); - let default_terminal_settings = - TerminalSettings::from_settings(&default_settings_content); - - RegexSearches::new( - &default_terminal_settings.path_hyperlink_regexes, - PATH_HYPERLINK_TIMEOUT_MS - ) - }); - } - - TEST_REGEX_SEARCHES.with(|regex_searches| { - find_from_grid_point(&term, point, &mut regex_searches.borrow_mut()) - }) - } - } - } - - mod file_iri { - // File IRIs have a ton of use cases, most of which we currently do not support. A few of - // those cases are documented here as tests which are expected to fail. - // See https://en.wikipedia.org/wiki/File_URI_scheme - - /// [**`c₀, c₁, …, cₙ;`**]ₒₚₜ := use specified terminal widths of `c₀, c₁, …, cₙ` **columns** - /// (defaults to `3, longest_line_cells / 2, longest_line_cells + 1;`) - /// - macro_rules! test_file_iri { - ($file_iri:literal) => { { test_hyperlink!(concat!("‹«👉", $file_iri, "»›"); FileIri) } }; - } - - #[cfg(not(target_os = "windows"))] - #[test] - fn absolute_file_iri() { - test_file_iri!("file:///test/cool/index.rs"); - test_file_iri!("file:///test/cool/"); - } - - mod issues { - #[cfg(not(target_os = "windows"))] - #[test] - #[should_panic(expected = "Path = «/test/Ῥόδος/», at grid cells (0, 0)..=(15, 1)")] - fn issue_file_iri_with_percent_encoded_characters() { - // Non-space characters - // file:///test/Ῥόδος/ - test_file_iri!("file:///test/%E1%BF%AC%CF%8C%CE%B4%CE%BF%CF%82/"); // URI - - // Spaces - test_file_iri!("file:///te%20st/co%20ol/index.rs"); - test_file_iri!("file:///te%20st/co%20ol/"); - } - } - - #[cfg(target_os = "windows")] - mod windows { - mod issues { - // The test uses Url::to_file_path(), but it seems that the Url crate doesn't - // support relative file IRIs. - #[test] - #[should_panic( - expected = r#"Failed to interpret file IRI `file:/test/cool/index.rs` as a path"# - )] - fn issue_relative_file_iri() { - test_file_iri!("file:/test/cool/index.rs"); - test_file_iri!("file:/test/cool/"); - } - - // See https://en.wikipedia.org/wiki/File_URI_scheme - // https://github.com/zed-industries/zed/issues/39189 - #[test] - #[should_panic( - expected = r#"Path = «C:\\test\\cool\\index.rs», at grid cells (0, 0)..=(9, 1)"# - )] - fn issue_39189() { - test_file_iri!("file:///C:/test/cool/index.rs"); - test_file_iri!("file:///C:/test/cool/"); - } - - #[test] - #[should_panic( - expected = r#"Path = «C:\\test\\Ῥόδος\\», at grid cells (0, 0)..=(16, 1)"# - )] - fn issue_file_iri_with_percent_encoded_characters() { - // Non-space characters - // file:///test/Ῥόδος/ - test_file_iri!("file:///C:/test/%E1%BF%AC%CF%8C%CE%B4%CE%BF%CF%82/"); // URI - - // Spaces - test_file_iri!("file:///C:/te%20st/co%20ol/index.rs"); - test_file_iri!("file:///C:/te%20st/co%20ol/"); - } - } - } - } - - mod iri { - /// [**`c₀, c₁, …, cₙ;`**]ₒₚₜ := use specified terminal widths of `c₀, c₁, …, cₙ` **columns** - /// (defaults to `3, longest_line_cells / 2, longest_line_cells + 1;`) - /// - macro_rules! test_iri { - ($iri:literal) => { { test_hyperlink!(concat!("‹«👉", $iri, "»›"); Iri) } }; - } - - #[test] - fn simple() { - // In the order they appear in URL_REGEX, except 'file://' which is treated as a path - test_iri!("ipfs://test/cool.ipfs"); - test_iri!("ipns://test/cool.ipns"); - test_iri!("magnet://test/cool.git"); - test_iri!("mailto:someone@somewhere.here"); - test_iri!("gemini://somewhere.here"); - test_iri!("gopher://somewhere.here"); - test_iri!("http://test/cool/index.html"); - test_iri!("http://10.10.10.10:1111/cool.html"); - test_iri!("http://test/cool/index.html?amazing=1"); - test_iri!("http://test/cool/index.html#right%20here"); - test_iri!("http://test/cool/index.html?amazing=1#right%20here"); - test_iri!("https://test/cool/index.html"); - test_iri!("https://10.10.10.10:1111/cool.html"); - test_iri!("https://test/cool/index.html?amazing=1"); - test_iri!("https://test/cool/index.html#right%20here"); - test_iri!("https://test/cool/index.html?amazing=1#right%20here"); - test_iri!("news://test/cool.news"); - test_iri!("git://test/cool.git"); - test_iri!("ssh://user@somewhere.over.here:12345/test/cool.git"); - test_iri!("ftp://test/cool.ftp"); - } - - #[test] - fn wide_chars() { - // In the order they appear in URL_REGEX, except 'file://' which is treated as a path - test_iri!("ipfs://例🏃🦀/cool.ipfs"); - test_iri!("ipns://例🏃🦀/cool.ipns"); - test_iri!("magnet://例🏃🦀/cool.git"); - test_iri!("mailto:someone@somewhere.here"); - test_iri!("gemini://somewhere.here"); - test_iri!("gopher://somewhere.here"); - test_iri!("http://例🏃🦀/cool/index.html"); - test_iri!("http://10.10.10.10:1111/cool.html"); - test_iri!("http://例🏃🦀/cool/index.html?amazing=1"); - test_iri!("http://例🏃🦀/cool/index.html#right%20here"); - test_iri!("http://例🏃🦀/cool/index.html?amazing=1#right%20here"); - test_iri!("https://例🏃🦀/cool/index.html"); - test_iri!("https://10.10.10.10:1111/cool.html"); - test_iri!("https://例🏃🦀/cool/index.html?amazing=1"); - test_iri!("https://例🏃🦀/cool/index.html#right%20here"); - test_iri!("https://例🏃🦀/cool/index.html?amazing=1#right%20here"); - test_iri!("news://例🏃🦀/cool.news"); - test_iri!("git://例/cool.git"); - test_iri!("ssh://user@somewhere.over.here:12345/例🏃🦀/cool.git"); - test_iri!("ftp://例🏃🦀/cool.ftp"); - } - - // There are likely more tests needed for IRI vs URI - #[test] - fn iris() { - // These refer to the same location, see example here: - // - test_iri!("https://en.wiktionary.org/wiki/Ῥόδος"); // IRI - test_iri!("https://en.wiktionary.org/wiki/%E1%BF%AC%CF%8C%CE%B4%CE%BF%CF%82"); // URI - } - - #[test] - #[should_panic(expected = "Expected a path, but was a iri")] - fn file_is_a_path() { - test_iri!("file://test/cool/index.rs"); - } - } - - #[derive(Debug, PartialEq)] - enum HyperlinkKind { - FileIri, - Iri, - Path, - } - - struct ExpectedHyperlink { - hovered_grid_point: AlacPoint, - hovered_char: char, - hyperlink_kind: HyperlinkKind, - iri_or_path: String, - row: Option, - column: Option, - hyperlink_match: RangeInclusive, - } - - /// Converts to Windows style paths on Windows, like path!(), but at runtime for improved test - /// readability. - fn build_term_from_test_lines<'a>( - hyperlink_kind: HyperlinkKind, - term_size: TermSize, - test_lines: impl Iterator, - ) -> (Term, ExpectedHyperlink) { - #[derive(Default, Eq, PartialEq)] - enum HoveredState { - #[default] - HoveredScan, - HoveredNextChar, - Done, - } - - #[derive(Default, Eq, PartialEq)] - enum MatchState { - #[default] - MatchScan, - MatchNextChar, - Match(AlacPoint), - Done, - } - - #[derive(Default, Eq, PartialEq)] - enum CapturesState { - #[default] - PathScan, - PathNextChar, - Path(AlacPoint), - RowScan, - Row(String), - ColumnScan, - Column(String), - Done, - } - - fn prev_input_point_from_term(term: &Term) -> AlacPoint { - let grid = term.grid(); - let cursor = &grid.cursor; - let mut point = cursor.point; - - if !cursor.input_needs_wrap { - point = point.sub(term, Boundary::Grid, 1); - } - - if grid.index(point).flags.contains(Flags::WIDE_CHAR_SPACER) { - point.column -= 1; - } - - point - } - - fn end_point_from_prev_input_point( - term: &Term, - prev_input_point: AlacPoint, - ) -> AlacPoint { - if term - .grid() - .index(prev_input_point) - .flags - .contains(Flags::WIDE_CHAR) - { - prev_input_point.add(term, Boundary::Grid, 1) - } else { - prev_input_point - } - } - - fn process_input(term: &mut Term, c: char) { - match c { - '\t' => term.put_tab(1), - c @ _ => term.input(c), - } - } - - let mut hovered_grid_point: Option = None; - let mut hyperlink_match = AlacPoint::default()..=AlacPoint::default(); - let mut iri_or_path = String::default(); - let mut row = None; - let mut column = None; - let mut prev_input_point = AlacPoint::default(); - let mut hovered_state = HoveredState::default(); - let mut match_state = MatchState::default(); - let mut captures_state = CapturesState::default(); - let mut term = Term::new(Config::default(), &term_size, VoidListener); - - for text in test_lines { - let chars: Box> = - if cfg!(windows) && hyperlink_kind == HyperlinkKind::Path { - Box::new(text.chars().map(|c| if c == '/' { '\\' } else { c })) as _ - } else { - Box::new(text.chars()) as _ - }; - let mut chars = chars.peekable(); - while let Some(c) = chars.next() { - match c { - '👉' => { - hovered_state = HoveredState::HoveredNextChar; - } - '👈' => { - hovered_grid_point = Some(prev_input_point.add(&term, Boundary::Grid, 1)); - } - '«' | '»' => { - captures_state = match captures_state { - CapturesState::PathScan => CapturesState::PathNextChar, - CapturesState::PathNextChar => { - panic!("Should have been handled by char input") - } - CapturesState::Path(start_point) => { - iri_or_path = term.bounds_to_string( - start_point, - end_point_from_prev_input_point(&term, prev_input_point), - ); - CapturesState::RowScan - } - CapturesState::RowScan => CapturesState::Row(String::new()), - CapturesState::Row(number) => { - row = Some(number.parse::().unwrap()); - CapturesState::ColumnScan - } - CapturesState::ColumnScan => CapturesState::Column(String::new()), - CapturesState::Column(number) => { - column = Some(number.parse::().unwrap()); - CapturesState::Done - } - CapturesState::Done => { - panic!("Extra '«', '»'") - } - } - } - '‹' | '›' => { - match_state = match match_state { - MatchState::MatchScan => MatchState::MatchNextChar, - MatchState::MatchNextChar => { - panic!("Should have been handled by char input") - } - MatchState::Match(start_point) => { - hyperlink_match = start_point - ..=end_point_from_prev_input_point(&term, prev_input_point); - MatchState::Done - } - MatchState::Done => { - panic!("Extra '‹', '›'") - } - } - } - _ => { - if let CapturesState::Row(number) | CapturesState::Column(number) = - &mut captures_state - { - number.push(c) - } - - let is_windows_abs_path_start = captures_state - == CapturesState::PathNextChar - && cfg!(windows) - && hyperlink_kind == HyperlinkKind::Path - && c == '\\' - && chars.peek().is_some_and(|c| *c != '\\'); - - if is_windows_abs_path_start { - // Convert Unix abs path start into Windows abs path start so that the - // same test can be used for both OSes. - term.input('C'); - prev_input_point = prev_input_point_from_term(&term); - term.input(':'); - process_input(&mut term, c); - } else { - process_input(&mut term, c); - prev_input_point = prev_input_point_from_term(&term); - } - - if hovered_state == HoveredState::HoveredNextChar { - hovered_grid_point = Some(prev_input_point); - hovered_state = HoveredState::Done; - } - if captures_state == CapturesState::PathNextChar { - captures_state = CapturesState::Path(prev_input_point); - } - if match_state == MatchState::MatchNextChar { - match_state = MatchState::Match(prev_input_point); - } - } - } - } - term.move_down_and_cr(1); - } - - if hyperlink_kind == HyperlinkKind::FileIri { - let Ok(url) = Url::parse(&iri_or_path) else { - panic!("Failed to parse file IRI `{iri_or_path}`"); - }; - let Ok(path) = url.to_file_path() else { - panic!("Failed to interpret file IRI `{iri_or_path}` as a path"); - }; - iri_or_path = path.to_string_lossy().into_owned(); - } - - let hovered_grid_point = hovered_grid_point.expect("Missing hovered point (👉 or 👈)"); - let hovered_char = term.grid().index(hovered_grid_point).c; - ( - term, - ExpectedHyperlink { - hovered_grid_point, - hovered_char, - hyperlink_kind, - iri_or_path, - row, - column, - hyperlink_match, - }, - ) - } - - fn line_cells_count(line: &str) -> usize { - // This avoids taking a dependency on the unicode-width crate - fn width(c: char) -> usize { - match c { - // Fullwidth unicode characters used in tests - '例' | '🏃' | '🦀' | '🔥' => 2, - '\t' => 8, // it's really 0-8, use the max always - _ => 1, - } - } - const CONTROL_CHARS: &str = "‹«👉👈»›"; - line.chars() - .filter(|c| !CONTROL_CHARS.contains(*c)) - .map(width) - .sum::() - } - - struct CheckHyperlinkMatch<'a> { - term: &'a Term, - expected_hyperlink: &'a ExpectedHyperlink, - source_location: &'a str, - } - - impl<'a> CheckHyperlinkMatch<'a> { - fn new( - term: &'a Term, - expected_hyperlink: &'a ExpectedHyperlink, - source_location: &'a str, - ) -> Self { - Self { - term, - expected_hyperlink, - source_location, - } - } - - fn check_path_with_position_and_match( - &self, - path_with_position: PathWithPosition, - hyperlink_match: &Match, - ) { - let format_path_with_position_and_match = - |path_with_position: &PathWithPosition, hyperlink_match: &Match| { - let mut result = - format!("Path = «{}»", &path_with_position.path.to_string_lossy()); - if let Some(row) = path_with_position.row { - result += &format!(", line = {row}"); - if let Some(column) = path_with_position.column { - result += &format!(", column = {column}"); - } - } - - result += &format!( - ", at grid cells {}", - Self::format_hyperlink_match(hyperlink_match) - ); - result - }; - - assert_ne!( - self.expected_hyperlink.hyperlink_kind, - HyperlinkKind::Iri, - "\n at {}\nExpected a path, but was a iri:\n{}", - self.source_location, - self.format_renderable_content() - ); - - assert_eq!( - format_path_with_position_and_match( - &PathWithPosition { - path: PathBuf::from(self.expected_hyperlink.iri_or_path.clone()), - row: self.expected_hyperlink.row, - column: self.expected_hyperlink.column - }, - &self.expected_hyperlink.hyperlink_match - ), - format_path_with_position_and_match(&path_with_position, hyperlink_match), - "\n at {}:\n{}", - self.source_location, - self.format_renderable_content() - ); - } - - fn check_iri_and_match(&self, iri: String, hyperlink_match: &Match) { - let format_iri_and_match = |iri: &String, hyperlink_match: &Match| { - format!( - "Url = «{iri}», at grid cells {}", - Self::format_hyperlink_match(hyperlink_match) - ) - }; - - assert_eq!( - self.expected_hyperlink.hyperlink_kind, - HyperlinkKind::Iri, - "\n at {}\nExpected a iri, but was a path:\n{}", - self.source_location, - self.format_renderable_content() - ); - - assert_eq!( - format_iri_and_match( - &self.expected_hyperlink.iri_or_path, - &self.expected_hyperlink.hyperlink_match - ), - format_iri_and_match(&iri, hyperlink_match), - "\n at {}:\n{}", - self.source_location, - self.format_renderable_content() - ); - } - - fn format_hyperlink_match(hyperlink_match: &Match) -> String { - format!( - "({}, {})..=({}, {})", - hyperlink_match.start().line.0, - hyperlink_match.start().column.0, - hyperlink_match.end().line.0, - hyperlink_match.end().column.0 - ) - } - - fn format_renderable_content(&self) -> String { - let mut result = format!("\nHovered on '{}'\n", self.expected_hyperlink.hovered_char); - - let mut first_header_row = String::new(); - let mut second_header_row = String::new(); - let mut marker_header_row = String::new(); - for index in 0..self.term.columns() { - let remainder = index % 10; - if index > 0 && remainder == 0 { - first_header_row.push_str(&format!("{:>10}", (index / 10))); - } - second_header_row += &remainder.to_string(); - if index == self.expected_hyperlink.hovered_grid_point.column.0 { - marker_header_row.push('↓'); - } else { - marker_header_row.push(' '); - } - } - - let remainder = (self.term.columns() - 1) % 10; - if remainder != 0 { - first_header_row.push_str(&" ".repeat(remainder)); - } - - result += &format!("\n [ {}]\n", first_header_row); - result += &format!(" [{}]\n", second_header_row); - result += &format!(" {}", marker_header_row); - - for cell in self - .term - .renderable_content() - .display_iter - .filter(|cell| !cell.flags.intersects(WIDE_CHAR_SPACERS)) - { - if cell.point.column.0 == 0 { - let prefix = - if cell.point.line == self.expected_hyperlink.hovered_grid_point.line { - '→' - } else { - ' ' - }; - result += &format!("\n{prefix}[{:>3}] ", cell.point.line.to_string()); - } - - match cell.c { - '\t' => result.push(' '), - c @ _ => result.push(c), - } - } - - result - } - } - - fn test_hyperlink<'a>( - columns: usize, - total_cells: usize, - test_lines: impl Iterator, - hyperlink_kind: HyperlinkKind, - source_location: &str, - ) { - const CARGO_DIR_REGEX: &str = - r#"\s+(Compiling|Checking|Documenting) [^(]+\((?(?.+))\)"#; - const RUST_DIAGNOSTIC_REGEX: &str = r#"\s+(-->|:::|at) (?(?.+?))(:$|$)"#; - const ISSUE_12338_REGEX: &str = - r#"[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2} (?(?.+))"#; - const MULTIPLE_SAME_LINE_REGEX: &str = - r#"(?(?🦀 multiple_same_line 🦀) 🚣(?[0-9]+) 🏛(?[0-9]+)):"#; - const PATH_HYPERLINK_TIMEOUT_MS: u64 = 1000; - - thread_local! { - static TEST_REGEX_SEARCHES: RefCell = - RefCell::new({ - let default_settings_content: Rc = - settings::parse_json_with_comments(&settings::default_settings()).unwrap(); - let default_terminal_settings = TerminalSettings::from_settings(&default_settings_content); - - RegexSearches::new([ - RUST_DIAGNOSTIC_REGEX, - CARGO_DIR_REGEX, - ISSUE_12338_REGEX, - MULTIPLE_SAME_LINE_REGEX, - ] - .into_iter() - .chain(default_terminal_settings.path_hyperlink_regexes - .iter() - .map(AsRef::as_ref)), - PATH_HYPERLINK_TIMEOUT_MS) - }); - } - - let term_size = TermSize::new(columns, total_cells / columns + 2); - let (term, expected_hyperlink) = - build_term_from_test_lines(hyperlink_kind, term_size, test_lines); - let hyperlink_found = TEST_REGEX_SEARCHES.with(|regex_searches| { - find_from_grid_point( - &term, - expected_hyperlink.hovered_grid_point, - &mut regex_searches.borrow_mut(), - ) - }); - let check_hyperlink_match = - CheckHyperlinkMatch::new(&term, &expected_hyperlink, source_location); - match hyperlink_found { - Some((hyperlink_word, false, hyperlink_match)) => { - check_hyperlink_match.check_path_with_position_and_match( - PathWithPosition::parse_str(&hyperlink_word), - &hyperlink_match, - ); - } - Some((hyperlink_word, true, hyperlink_match)) => { - check_hyperlink_match.check_iri_and_match(hyperlink_word, &hyperlink_match); - } - None => { - if expected_hyperlink.hyperlink_match.start() - != expected_hyperlink.hyperlink_match.end() - { - assert!( - false, - "No hyperlink found\n at {source_location}:\n{}", - check_hyperlink_match.format_renderable_content() - ) - } - } - } - } -} diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs deleted file mode 100644 index 3d70d85f35..0000000000 --- a/crates/terminal/src/terminal_settings.rs +++ /dev/null @@ -1,178 +0,0 @@ -use alacritty_terminal::vte::ansi::{ - CursorShape as AlacCursorShape, CursorStyle as AlacCursorStyle, -}; -use collections::HashMap; -use gpui::{FontFallbacks, FontFeatures, FontWeight, Pixels, px}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -pub use settings::AlternateScroll; - -use settings::{ - PathHyperlinkRegex, RegisterSetting, ShowScrollbar, TerminalBlink, TerminalDockPosition, - TerminalLineHeight, VenvSettings, WorkingDirectory, merge_from::MergeFrom, -}; -use task::Shell; -use theme::FontFamilyName; - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -pub struct Toolbar { - pub breadcrumbs: bool, -} - -#[derive(Clone, Debug, Deserialize, RegisterSetting)] -pub struct TerminalSettings { - pub shell: Shell, - pub working_directory: WorkingDirectory, - pub font_size: Option, // todo(settings_refactor) can be non-optional... - pub font_family: Option, - pub font_fallbacks: Option, - pub font_features: Option, - pub font_weight: Option, - pub line_height: TerminalLineHeight, - pub env: HashMap, - pub cursor_shape: CursorShape, - pub blinking: TerminalBlink, - pub alternate_scroll: AlternateScroll, - pub option_as_meta: bool, - pub copy_on_select: bool, - pub keep_selection_on_copy: bool, - pub button: bool, - pub dock: TerminalDockPosition, - pub default_width: Pixels, - pub default_height: Pixels, - pub detect_venv: VenvSettings, - pub max_scroll_history_lines: Option, - pub scroll_multiplier: f32, - pub toolbar: Toolbar, - pub scrollbar: ScrollbarSettings, - pub minimum_contrast: f32, - pub path_hyperlink_regexes: Vec, - pub path_hyperlink_timeout_ms: u64, -} - -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -pub struct ScrollbarSettings { - /// When to show the scrollbar in the terminal. - /// - /// Default: inherits editor scrollbar settings - pub show: Option, -} - -fn settings_shell_to_task_shell(shell: settings::Shell) -> Shell { - match shell { - settings::Shell::System => Shell::System, - settings::Shell::Program(program) => Shell::Program(program), - settings::Shell::WithArguments { - program, - args, - title_override, - } => Shell::WithArguments { - program, - args, - title_override: title_override.map(Into::into), - }, - } -} - -impl settings::Settings for TerminalSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - let user_content = content.terminal.clone().unwrap(); - // Note: we allow a subset of "terminal" settings in the project files. - let mut project_content = user_content.project.clone(); - project_content.merge_from_option(content.project.terminal.as_ref()); - TerminalSettings { - shell: settings_shell_to_task_shell(project_content.shell.unwrap()), - working_directory: project_content.working_directory.unwrap(), - font_size: user_content.font_size.map(px), - font_family: user_content.font_family, - font_fallbacks: user_content.font_fallbacks.map(|fallbacks| { - FontFallbacks::from_fonts( - fallbacks - .into_iter() - .map(|family| family.0.to_string()) - .collect(), - ) - }), - font_features: user_content.font_features, - font_weight: user_content.font_weight, - line_height: user_content.line_height.unwrap(), - env: project_content.env.unwrap(), - cursor_shape: user_content.cursor_shape.unwrap().into(), - blinking: user_content.blinking.unwrap(), - alternate_scroll: user_content.alternate_scroll.unwrap(), - option_as_meta: user_content.option_as_meta.unwrap(), - copy_on_select: user_content.copy_on_select.unwrap(), - keep_selection_on_copy: user_content.keep_selection_on_copy.unwrap(), - button: user_content.button.unwrap(), - dock: user_content.dock.unwrap(), - default_width: px(user_content.default_width.unwrap()), - default_height: px(user_content.default_height.unwrap()), - detect_venv: project_content.detect_venv.unwrap(), - scroll_multiplier: user_content.scroll_multiplier.unwrap(), - max_scroll_history_lines: user_content.max_scroll_history_lines, - toolbar: Toolbar { - breadcrumbs: user_content.toolbar.unwrap().breadcrumbs.unwrap(), - }, - scrollbar: ScrollbarSettings { - show: user_content.scrollbar.unwrap().show, - }, - minimum_contrast: user_content.minimum_contrast.unwrap(), - path_hyperlink_regexes: project_content - .path_hyperlink_regexes - .unwrap() - .into_iter() - .map(|regex| match regex { - PathHyperlinkRegex::SingleLine(regex) => regex, - PathHyperlinkRegex::MultiLine(regex) => regex.join("\n"), - }) - .collect(), - path_hyperlink_timeout_ms: project_content.path_hyperlink_timeout_ms.unwrap(), - } - } -} - -#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum CursorShape { - /// Cursor is a block like `█`. - #[default] - Block, - /// Cursor is an underscore like `_`. - Underline, - /// Cursor is a vertical bar like `⎸`. - Bar, - /// Cursor is a hollow box like `▯`. - Hollow, -} - -impl From for CursorShape { - fn from(value: settings::CursorShapeContent) -> Self { - match value { - settings::CursorShapeContent::Block => CursorShape::Block, - settings::CursorShapeContent::Underline => CursorShape::Underline, - settings::CursorShapeContent::Bar => CursorShape::Bar, - settings::CursorShapeContent::Hollow => CursorShape::Hollow, - } - } -} - -impl From for AlacCursorShape { - fn from(value: CursorShape) -> Self { - match value { - CursorShape::Block => AlacCursorShape::Block, - CursorShape::Underline => AlacCursorShape::Underline, - CursorShape::Bar => AlacCursorShape::Beam, - CursorShape::Hollow => AlacCursorShape::HollowBlock, - } - } -} - -impl From for AlacCursorStyle { - fn from(value: CursorShape) -> Self { - AlacCursorStyle { - shape: value.into(), - blinking: false, - } - } -} diff --git a/crates/terminal_view/Cargo.toml b/crates/terminal_view/Cargo.toml deleted file mode 100644 index eadd00bcbb..0000000000 --- a/crates/terminal_view/Cargo.toml +++ /dev/null @@ -1,58 +0,0 @@ -[package] -name = "terminal_view" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[features] -test-support = ["editor/test-support", "gpui/test-support"] - -[lib] -path = "src/terminal_view.rs" -doctest = false - -[dependencies] -anyhow.workspace = true -async-recursion.workspace = true -assistant_slash_command.workspace = true -breadcrumbs.workspace = true -collections.workspace = true -db.workspace = true -dirs.workspace = true -editor.workspace = true -futures.workspace = true -gpui.workspace = true -itertools.workspace = true -language.workspace = true -log.workspace = true -pretty_assertions.workspace = true -project.workspace = true -regex.workspace = true -task.workspace = true -schemars.workspace = true -search.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -shellexpand.workspace = true -terminal.workspace = true -theme.workspace = true -ui.workspace = true -util.workspace = true -workspace.workspace = true -zed_actions.workspace = true - -[dev-dependencies] -client = { workspace = true, features = ["test-support"] } -editor = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -project = { workspace = true, features = ["test-support"] } -rand.workspace = true -workspace = { workspace = true, features = ["test-support"] } - -[package.metadata.cargo-machete] -ignored = ["log"] diff --git a/crates/terminal_view/LICENSE-GPL b/crates/terminal_view/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/terminal_view/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/terminal_view/README.md b/crates/terminal_view/README.md deleted file mode 100644 index ca48f54542..0000000000 --- a/crates/terminal_view/README.md +++ /dev/null @@ -1,23 +0,0 @@ -Design notes: - -This crate is split into two conceptual halves: -- The terminal.rs file and the src/mappings/ folder, these contain the code for interacting with Alacritty and maintaining the pty event loop. Some behavior in this file is constrained by terminal protocols and standards. The Zed init function is also placed here. -- Everything else. These other files integrate the `Terminal` struct created in terminal.rs into the rest of GPUI. The main entry point for GPUI is the terminal_view.rs file and the modal.rs file. - -ttys are created externally, and so can fail in unexpected ways. However, GPUI currently does not have an API for models than can fail to instantiate. `TerminalBuilder` solves this by using Rust's type system to split tty instantiation into a 2 step process: first attempt to create the file handles with `TerminalBuilder::new()`, check the result, then call `TerminalBuilder::subscribe(cx)` from within a model context. - -The TerminalView struct abstracts over failed and successful terminals, passing focus through to the associated view and allowing clients to build a terminal without worrying about errors. - -#Input - -There are currently many distinct paths for getting keystrokes to the terminal: - -1. Terminal specific characters and bindings. Things like ctrl-a mapping to ASCII control character 1, ANSI escape codes associated with the function keys, etc. These are caught with a raw key-down handler in the element and are processed immediately. This is done with the `try_keystroke()` method on Terminal - -2. GPU Action handlers. GPUI clobbers a few vital keys by adding bindings to them in the global context. These keys are synthesized and then dispatched through the same `try_keystroke()` API as the above mappings - -3. IME text. When the special character mappings fail, we pass the keystroke back to GPUI to hand it to the IME system. This comes back to us in the `View::replace_text_in_range()` method, and we then send that to the terminal directly, bypassing `try_keystroke()`. - -4. Pasted text has a separate pathway. - -Generally, there's a distinction between 'keystrokes that need to be mapped' and 'strings which need to be written'. I've attempted to unify these under the '.try_keystroke()' API and the `.input()` API (which try_keystroke uses) so we have consistent input handling across the terminal diff --git a/crates/terminal_view/scripts/print256color.sh b/crates/terminal_view/scripts/print256color.sh deleted file mode 100755 index 9cb3b1c47c..0000000000 --- a/crates/terminal_view/scripts/print256color.sh +++ /dev/null @@ -1,96 +0,0 @@ -#!/usr/bin/env bash - -# Tom Hale, 2016. MIT Licence. -# Print out 256 colours, with each number printed in its corresponding colour -# See http://askubuntu.com/questions/821157/print-a-256-color-test-pattern-in-the-terminal/821163#821163 - -set -eu # Fail on errors or undeclared variables - -printable_colours=256 - -# Return a colour that contrasts with the given colour -# Bash only does integer division, so keep it integral -function contrast_colour { - local r g b luminance - colour="$1" - - if (( colour < 16 )); then # Initial 16 ANSI colours - (( colour == 0 )) && printf "15" || printf "0" - return - fi - - # Greyscale # rgb_R = rgb_G = rgb_B = (number - 232) * 10 + 8 - if (( colour > 231 )); then # Greyscale ramp - (( colour < 244 )) && printf "15" || printf "0" - return - fi - - # All other colours: - # 6x6x6 colour cube = 16 + 36*R + 6*G + B # Where RGB are [0..5] - # See http://stackoverflow.com/a/27165165/5353461 - - # r=$(( (colour-16) / 36 )) - g=$(( ((colour-16) % 36) / 6 )) - # b=$(( (colour-16) % 6 )) - - # If luminance is bright, print number in black, white otherwise. - # Green contributes 587/1000 to human perceived luminance - ITU R-REC-BT.601 - (( g > 2)) && printf "0" || printf "15" - return - - # Uncomment the below for more precise luminance calculations - - # # Calculate perceived brightness - # # See https://www.w3.org/TR/AERT#color-contrast - # # and http://www.itu.int/rec/R-REC-BT.601 - # # Luminance is in range 0..5000 as each value is 0..5 - # luminance=$(( (r * 299) + (g * 587) + (b * 114) )) - # (( $luminance > 2500 )) && printf "0" || printf "15" -} - -# Print a coloured block with the number of that colour -function print_colour { - local colour="$1" contrast - contrast=$(contrast_colour "$1") - printf "\e[48;5;%sm" "$colour" # Start block of colour - printf "\e[38;5;%sm%3d" "$contrast" "$colour" # In contrast, print number - printf "\e[0m " # Reset colour -} - -# Starting at $1, print a run of $2 colours -function print_run { - local i - for (( i = "$1"; i < "$1" + "$2" && i < printable_colours; i++ )) do - print_colour "$i" - done - printf " " -} - -# Print blocks of colours -function print_blocks { - local start="$1" i - local end="$2" # inclusive - local block_cols="$3" - local block_rows="$4" - local blocks_per_line="$5" - local block_length=$((block_cols * block_rows)) - - # Print sets of blocks - for (( i = start; i <= end; i += (blocks_per_line-1) * block_length )) do - printf "\n" # Space before each set of blocks - # For each block row - for (( row = 0; row < block_rows; row++ )) do - # Print block columns for all blocks on the line - for (( block = 0; block < blocks_per_line; block++ )) do - print_run $(( i + (block * block_length) )) "$block_cols" - done - (( i += block_cols )) # Prepare to print the next row - printf "\n" - done - done -} - -print_run 0 16 # The first 16 colours are spread over the whole spectrum -printf "\n" -print_blocks 16 231 6 6 3 # 6x6x6 colour cube between 16 and 231 inclusive -print_blocks 232 255 12 2 1 # Not 50, but 24 Shades of Grey diff --git a/crates/terminal_view/scripts/truecolor.sh b/crates/terminal_view/scripts/truecolor.sh deleted file mode 100755 index 622051f242..0000000000 --- a/crates/terminal_view/scripts/truecolor.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash -# Copied from: https://unix.stackexchange.com/a/696756 -# Based on: https://gist.github.com/XVilka/8346728 and https://unix.stackexchange.com/a/404415/395213 - -awk -v term_cols="${width:-$(tput cols || echo 80)}" -v term_lines="${height:-1}" 'BEGIN{ - s="/\\"; - total_cols=term_cols*term_lines; - for (colnum = 0; colnum255) g = 510-g; - printf "\033[48;2;%d;%d;%dm", r,g,b; - printf "\033[38;2;%d;%d;%dm", 255-r,255-g,255-b; - printf "%s\033[0m", substr(s,colnum%2+1,1); - if (colnum%term_cols==term_cols) printf "\n"; - } - printf "\n"; -}' diff --git a/crates/terminal_view/src/persistence.rs b/crates/terminal_view/src/persistence.rs deleted file mode 100644 index 8d6ef03fd7..0000000000 --- a/crates/terminal_view/src/persistence.rs +++ /dev/null @@ -1,487 +0,0 @@ -use anyhow::Result; -use async_recursion::async_recursion; -use collections::HashSet; -use futures::{StreamExt as _, stream::FuturesUnordered}; -use gpui::{AppContext as _, AsyncWindowContext, Axis, Entity, Task, WeakEntity}; -use project::Project; -use serde::{Deserialize, Serialize}; -use std::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 workspace::{ - ItemHandle, ItemId, Member, Pane, PaneAxis, PaneGroup, SerializableItem as _, Workspace, - WorkspaceDb, WorkspaceId, -}; - -use crate::{ - TerminalView, default_working_directory, - terminal_panel::{TerminalPanel, new_terminal_pane}, -}; - -pub(crate) fn serialize_pane_group( - pane_group: &PaneGroup, - active_pane: &Entity, - cx: &mut App, -) -> SerializedPaneGroup { - build_serialized_pane_group(&pane_group.root, active_pane, cx) -} - -fn build_serialized_pane_group( - pane_group: &Member, - active_pane: &Entity, - cx: &mut App, -) -> SerializedPaneGroup { - match pane_group { - Member::Axis(PaneAxis { - axis, - members, - flexes, - bounding_boxes: _, - }) => SerializedPaneGroup::Group { - axis: SerializedAxis(*axis), - children: members - .iter() - .map(|member| build_serialized_pane_group(member, active_pane, cx)) - .collect::>(), - flexes: Some(flexes.lock().clone()), - }, - Member::Pane(pane_handle) => { - SerializedPaneGroup::Pane(serialize_pane(pane_handle, pane_handle == active_pane, cx)) - } - } -} - -fn serialize_pane(pane: &Entity, active: bool, cx: &mut App) -> SerializedPane { - let mut items_to_serialize = HashSet::default(); - let pane = pane.read(cx); - let children = pane - .items() - .filter_map(|item| { - let terminal_view = item.act_as::(cx)?; - if terminal_view.read(cx).terminal().read(cx).task().is_some() { - None - } else { - let id = item.item_id().as_u64(); - items_to_serialize.insert(id); - Some(id) - } - }) - .collect::>(); - let active_item = pane - .active_item() - .map(|item| item.item_id().as_u64()) - .filter(|active_id| items_to_serialize.contains(active_id)); - - let pinned_count = pane.pinned_count(); - SerializedPane { - active, - children, - active_item, - pinned_count, - } -} - -pub(crate) fn deserialize_terminal_panel( - workspace: WeakEntity, - project: Entity, - database_id: WorkspaceId, - serialized_panel: SerializedTerminalPanel, - window: &mut Window, - cx: &mut App, -) -> Task>> { - window.spawn(cx, async move |cx| { - let terminal_panel = workspace.update_in(cx, |workspace, window, cx| { - cx.new(|cx| { - let mut panel = TerminalPanel::new(workspace, window, cx); - panel.height = serialized_panel.height.map(|h| h.round()); - panel.width = serialized_panel.width.map(|w| w.round()); - panel - }) - })?; - match &serialized_panel.items { - SerializedItems::NoSplits(item_ids) => { - let items = deserialize_terminal_views( - database_id, - project, - workspace, - item_ids.as_slice(), - cx, - ) - .await; - let active_item = serialized_panel.active_item_id; - terminal_panel.update_in(cx, |terminal_panel, window, cx| { - terminal_panel.active_pane.update(cx, |pane, cx| { - populate_pane_items(pane, items, active_item, window, cx); - }); - })?; - } - SerializedItems::WithSplits(serialized_pane_group) => { - let center_pane = deserialize_pane_group( - workspace, - project, - terminal_panel.clone(), - database_id, - serialized_pane_group, - cx, - ) - .await; - if let Some((center_group, active_pane)) = center_pane { - terminal_panel.update(cx, |terminal_panel, _| { - terminal_panel.center = PaneGroup::with_root(center_group); - terminal_panel.active_pane = - active_pane.unwrap_or_else(|| terminal_panel.center.first_pane()); - })?; - } - } - } - - Ok(terminal_panel) - }) -} - -fn populate_pane_items( - pane: &mut Pane, - items: Vec>, - active_item: Option, - window: &mut Window, - cx: &mut Context, -) { - let mut item_index = pane.items_len(); - let mut active_item_index = None; - for item in items { - if Some(item.item_id().as_u64()) == active_item { - active_item_index = Some(item_index); - } - pane.add_item(Box::new(item), false, false, None, window, cx); - item_index += 1; - } - if let Some(index) = active_item_index { - pane.activate_item(index, false, false, window, cx); - } -} - -#[async_recursion(?Send)] -async fn deserialize_pane_group( - workspace: WeakEntity, - project: Entity, - panel: Entity, - workspace_id: WorkspaceId, - serialized: &SerializedPaneGroup, - cx: &mut AsyncWindowContext, -) -> Option<(Member, Option>)> { - match serialized { - SerializedPaneGroup::Group { - axis, - flexes, - children, - } => { - let mut current_active_pane = None; - let mut members = Vec::new(); - for child in children { - if let Some((new_member, active_pane)) = deserialize_pane_group( - workspace.clone(), - project.clone(), - panel.clone(), - workspace_id, - child, - cx, - ) - .await - { - members.push(new_member); - current_active_pane = current_active_pane.or(active_pane); - } - } - - if members.is_empty() { - return None; - } - - if members.len() == 1 { - return Some((members.remove(0), current_active_pane)); - } - - Some(( - Member::Axis(PaneAxis::load(axis.0, members, flexes.clone())), - current_active_pane, - )) - } - SerializedPaneGroup::Pane(serialized_pane) => { - let active = serialized_pane.active; - - let pane = panel - .update_in(cx, |terminal_panel, window, cx| { - new_terminal_pane( - workspace.clone(), - project.clone(), - terminal_panel.active_pane.read(cx).is_zoomed(), - window, - cx, - ) - }) - .log_err()?; - let active_item = serialized_pane.active_item; - let pinned_count = serialized_pane.pinned_count; - let new_items = deserialize_terminal_views( - workspace_id, - project.clone(), - workspace.clone(), - serialized_pane.children.as_slice(), - cx, - ); - cx.spawn({ - let pane = pane.downgrade(); - async move |cx| { - let new_items = new_items.await; - - let items = pane.update_in(cx, |pane, window, cx| { - populate_pane_items(pane, new_items, active_item, window, cx); - pane.set_pinned_count(pinned_count); - pane.items_len() - }); - // Avoid blank panes in splits - if items.is_ok_and(|items| items == 0) { - let working_directory = workspace - .update(cx, |workspace, cx| default_working_directory(workspace, cx)) - .ok() - .flatten(); - let Some(terminal) = project - .update(cx, |project, cx| { - project.create_terminal_shell(working_directory, cx) - }) - .log_err() - else { - return; - }; - - let terminal = terminal.await.log_err(); - pane.update_in(cx, |pane, window, cx| { - if let Some(terminal) = terminal { - let terminal_view = Box::new(cx.new(|cx| { - TerminalView::new( - terminal, - workspace.clone(), - Some(workspace_id), - project.downgrade(), - window, - cx, - ) - })); - pane.add_item(terminal_view, true, false, None, window, cx); - } - }) - .ok(); - } - } - }) - .await; - Some((Member::Pane(pane.clone()), active.then_some(pane))) - } - } -} - -fn deserialize_terminal_views( - workspace_id: WorkspaceId, - project: Entity, - workspace: WeakEntity, - item_ids: &[u64], - cx: &mut AsyncWindowContext, -) -> impl Future>> + use<> { - let mut deserialized_items = item_ids - .iter() - .map(|item_id| { - cx.update(|window, cx| { - TerminalView::deserialize( - project.clone(), - workspace.clone(), - workspace_id, - *item_id, - window, - cx, - ) - }) - .unwrap_or_else(|e| Task::ready(Err(e.context("no window present")))) - }) - .collect::>(); - async move { - let mut items = Vec::with_capacity(deserialized_items.len()); - while let Some(item) = deserialized_items.next().await { - if let Some(item) = item.log_err() { - items.push(item); - } - } - items - } -} - -#[derive(Debug, Serialize, Deserialize)] -pub(crate) struct SerializedTerminalPanel { - pub items: SerializedItems, - // A deprecated field, kept for backwards compatibility for the code before terminal splits were introduced. - pub active_item_id: Option, - pub width: Option, - pub height: Option, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(untagged)] -pub(crate) enum SerializedItems { - // The data stored before terminal splits were introduced. - NoSplits(Vec), - WithSplits(SerializedPaneGroup), -} - -#[derive(Debug, Serialize, Deserialize)] -pub(crate) enum SerializedPaneGroup { - Pane(SerializedPane), - Group { - axis: SerializedAxis, - flexes: Option>, - children: Vec, - }, -} - -#[derive(Debug, Serialize, Deserialize)] -pub(crate) struct SerializedPane { - pub active: bool, - pub children: Vec, - pub active_item: Option, - #[serde(default)] - pub pinned_count: usize, -} - -#[derive(Debug)] -pub(crate) struct SerializedAxis(pub Axis); - -impl Serialize for SerializedAxis { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - match self.0 { - Axis::Horizontal => serializer.serialize_str("horizontal"), - Axis::Vertical => serializer.serialize_str("vertical"), - } - } -} - -impl<'de> Deserialize<'de> for SerializedAxis { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - match s.as_str() { - "horizontal" => Ok(SerializedAxis(Axis::Horizontal)), - "vertical" => Ok(SerializedAxis(Axis::Vertical)), - invalid => Err(serde::de::Error::custom(format!( - "Invalid axis value: '{invalid}'" - ))), - } - } -} - -pub struct TerminalDb(ThreadSafeConnection); - -impl Domain for TerminalDb { - const NAME: &str = stringify!(TerminalDb); - - const MIGRATIONS: &[&str] = &[ - sql!( - CREATE TABLE terminals ( - workspace_id INTEGER, - item_id INTEGER UNIQUE, - working_directory BLOB, - PRIMARY KEY(workspace_id, item_id), - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ) STRICT; - ), - // Remove the unique constraint on the item_id table - // SQLite doesn't have a way of doing this automatically, so - // we have to do this silly copying. - sql!( - CREATE TABLE terminals2 ( - workspace_id INTEGER, - item_id INTEGER, - working_directory BLOB, - PRIMARY KEY(workspace_id, item_id), - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ) STRICT; - - INSERT INTO terminals2 (workspace_id, item_id, working_directory) - SELECT workspace_id, item_id, working_directory FROM terminals; - - DROP TABLE terminals; - - ALTER TABLE terminals2 RENAME TO terminals; - ), - sql! ( - ALTER TABLE terminals ADD COLUMN working_directory_path TEXT; - UPDATE terminals SET working_directory_path = CAST(working_directory AS TEXT); - ), - ]; -} - -db::static_connection!(TERMINAL_DB, TerminalDb, [WorkspaceDb]); - -impl TerminalDb { - query! { - pub async fn update_workspace_id( - new_id: WorkspaceId, - old_id: WorkspaceId, - item_id: ItemId - ) -> Result<()> { - UPDATE terminals - SET workspace_id = ? - WHERE workspace_id = ? AND item_id = ? - } - } - - pub async fn save_working_directory( - &self, - item_id: ItemId, - workspace_id: WorkspaceId, - working_directory: PathBuf, - ) -> Result<()> { - log::debug!( - "Saving working directory {working_directory:?} for item {item_id} in workspace {workspace_id:?}" - ); - let query = - "INSERT INTO terminals(item_id, workspace_id, working_directory, working_directory_path) - VALUES (?1, ?2, ?3, ?4) - ON CONFLICT DO UPDATE SET - item_id = ?1, - workspace_id = ?2, - working_directory = ?3, - working_directory_path = ?4" - ; - self.write(move |conn| { - let mut statement = Statement::prepare(conn, query)?; - let mut next_index = statement.bind(&item_id, 1)?; - next_index = statement.bind(&workspace_id, next_index)?; - next_index = statement.bind(&working_directory, next_index)?; - statement.bind( - &working_directory.to_string_lossy().into_owned(), - next_index, - )?; - statement.exec() - }) - .await - } - - query! { - pub fn get_working_directory(item_id: ItemId, workspace_id: WorkspaceId) -> Result> { - SELECT working_directory - FROM terminals - WHERE item_id = ? AND workspace_id = ? - } - } -} diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs deleted file mode 100644 index fd9568b0c5..0000000000 --- a/crates/terminal_view/src/terminal_element.rs +++ /dev/null @@ -1,2049 +0,0 @@ -use editor::{CursorLayout, EditorSettings, HighlightedRange, HighlightedRangeLine}; -use gpui::{ - AbsoluteLength, AnyElement, App, AvailableSpace, Bounds, ContentMask, Context, DispatchPhase, - Element, ElementId, Entity, FocusHandle, Font, FontFeatures, FontStyle, FontWeight, - GlobalElementId, HighlightStyle, Hitbox, Hsla, InputHandler, InteractiveElement, Interactivity, - IntoElement, LayoutId, Length, ModifiersChangedEvent, MouseButton, MouseMoveEvent, Pixels, - Point, ShapedLine, StatefulInteractiveElement, StrikethroughStyle, Styled, TextRun, TextStyle, - UTF16Selection, UnderlineStyle, WeakEntity, WhiteSpace, Window, div, fill, point, px, relative, - size, -}; -use itertools::Itertools; -use language::CursorShape; -use settings::Settings; -use std::time::Instant; -use terminal::{ - IndexedCell, Terminal, TerminalBounds, TerminalContent, - alacritty_terminal::{ - grid::Dimensions, - index::Point as AlacPoint, - term::{TermMode, cell::Flags}, - vte::ansi::{ - Color::{self as AnsiColor, Named}, - CursorShape as AlacCursorShape, NamedColor, - }, - }, - terminal_settings::TerminalSettings, -}; -use theme::{ActiveTheme, Theme, ThemeSettings}; -use ui::utils::ensure_minimum_contrast; -use ui::{ParentElement, Tooltip}; -use util::ResultExt; -use workspace::Workspace; - -use std::mem; -use std::{fmt::Debug, ops::RangeInclusive, rc::Rc}; - -use crate::{BlockContext, BlockProperties, ContentMode, TerminalMode, TerminalView}; - -/// The information generated during layout that is necessary for painting. -pub struct LayoutState { - hitbox: Hitbox, - batched_text_runs: Vec, - rects: Vec, - relative_highlighted_ranges: Vec<(RangeInclusive, Hsla)>, - cursor: Option, - background_color: Hsla, - dimensions: TerminalBounds, - mode: TermMode, - display_offset: usize, - hyperlink_tooltip: Option, - gutter: Pixels, - block_below_cursor_element: Option, - base_text_style: TextStyle, - content_mode: ContentMode, -} - -/// Helper struct for converting data between Alacritty's cursor points, and displayed cursor points. -struct DisplayCursor { - line: i32, - col: usize, -} - -impl DisplayCursor { - fn from(cursor_point: AlacPoint, display_offset: usize) -> Self { - Self { - line: cursor_point.line.0 + display_offset as i32, - col: cursor_point.column.0, - } - } - - pub fn line(&self) -> i32 { - self.line - } - - pub fn col(&self) -> usize { - self.col - } -} - -/// A batched text run that combines multiple adjacent cells with the same style -#[derive(Debug)] -pub struct BatchedTextRun { - pub start_point: AlacPoint, - pub text: String, - pub cell_count: usize, - pub style: TextRun, - pub font_size: AbsoluteLength, -} - -impl BatchedTextRun { - fn new_from_char( - start_point: AlacPoint, - c: char, - style: TextRun, - font_size: AbsoluteLength, - ) -> Self { - let mut text = String::with_capacity(100); // Pre-allocate for typical line length - text.push(c); - BatchedTextRun { - start_point, - text, - cell_count: 1, - style, - font_size, - } - } - - fn can_append(&self, other_style: &TextRun) -> bool { - self.style.font == other_style.font - && self.style.color == other_style.color - && self.style.background_color == other_style.background_color - && self.style.underline == other_style.underline - && self.style.strikethrough == other_style.strikethrough - } - - fn append_char(&mut self, c: char) { - self.append_char_internal(c, true); - } - - fn append_zero_width_chars(&mut self, chars: &[char]) { - for &c in chars { - self.append_char_internal(c, false); - } - } - - fn append_char_internal(&mut self, c: char, counts_cell: bool) { - self.text.push(c); - if counts_cell { - self.cell_count += 1; - } - self.style.len += c.len_utf8(); - } - - pub fn paint( - &self, - origin: Point, - dimensions: &TerminalBounds, - window: &mut Window, - cx: &mut App, - ) { - let pos = Point::new( - origin.x + self.start_point.column as f32 * dimensions.cell_width, - origin.y + self.start_point.line as f32 * dimensions.line_height, - ); - - let _ = window - .text_system() - .shape_line( - self.text.clone().into(), - self.font_size.to_pixels(window.rem_size()), - std::slice::from_ref(&self.style), - Some(dimensions.cell_width), - ) - .paint(pos, dimensions.line_height, window, cx); - } -} - -#[derive(Clone, Debug, Default)] -pub struct LayoutRect { - point: AlacPoint, - num_of_cells: usize, - color: Hsla, -} - -impl LayoutRect { - fn new(point: AlacPoint, num_of_cells: usize, color: Hsla) -> LayoutRect { - LayoutRect { - point, - num_of_cells, - color, - } - } - - pub fn paint(&self, origin: Point, dimensions: &TerminalBounds, window: &mut Window) { - let position = { - let alac_point = self.point; - point( - (origin.x + alac_point.column as f32 * dimensions.cell_width).floor(), - origin.y + alac_point.line as f32 * dimensions.line_height, - ) - }; - let size = point( - (dimensions.cell_width * self.num_of_cells as f32).ceil(), - dimensions.line_height, - ) - .into(); - - window.paint_quad(fill(Bounds::new(position, size), self.color)); - } -} - -/// Represents a rectangular region with a specific background color -#[derive(Debug, Clone)] -struct BackgroundRegion { - start_line: i32, - start_col: i32, - end_line: i32, - end_col: i32, - color: Hsla, -} - -impl BackgroundRegion { - fn new(line: i32, col: i32, color: Hsla) -> Self { - BackgroundRegion { - start_line: line, - start_col: col, - end_line: line, - end_col: col, - color, - } - } - - /// Check if this region can be merged with another region - fn can_merge_with(&self, other: &BackgroundRegion) -> bool { - if self.color != other.color { - return false; - } - - // Check if regions are adjacent horizontally - if self.start_line == other.start_line && self.end_line == other.end_line { - return self.end_col + 1 == other.start_col || other.end_col + 1 == self.start_col; - } - - // Check if regions are adjacent vertically with same column span - if self.start_col == other.start_col && self.end_col == other.end_col { - return self.end_line + 1 == other.start_line || other.end_line + 1 == self.start_line; - } - - false - } - - /// Merge this region with another region - fn merge_with(&mut self, other: &BackgroundRegion) { - self.start_line = self.start_line.min(other.start_line); - self.start_col = self.start_col.min(other.start_col); - self.end_line = self.end_line.max(other.end_line); - self.end_col = self.end_col.max(other.end_col); - } -} - -/// Merge background regions to minimize the number of rectangles -fn merge_background_regions(regions: Vec) -> Vec { - if regions.is_empty() { - return regions; - } - - let mut merged = regions; - let mut changed = true; - - // Keep merging until no more merges are possible - while changed { - changed = false; - let mut i = 0; - - while i < merged.len() { - let mut j = i + 1; - while j < merged.len() { - if merged[i].can_merge_with(&merged[j]) { - let other = merged.remove(j); - merged[i].merge_with(&other); - changed = true; - } else { - j += 1; - } - } - i += 1; - } - } - - merged -} - -/// The GPUI element that paints the terminal. -/// We need to keep a reference to the model for mouse events, do we need it for any other terminal stuff, or can we move that to connection? -pub struct TerminalElement { - terminal: Entity, - terminal_view: Entity, - workspace: WeakEntity, - focus: FocusHandle, - focused: bool, - cursor_visible: bool, - interactivity: Interactivity, - mode: TerminalMode, - block_below_cursor: Option>, -} - -impl InteractiveElement for TerminalElement { - fn interactivity(&mut self) -> &mut Interactivity { - &mut self.interactivity - } -} - -impl StatefulInteractiveElement for TerminalElement {} - -impl TerminalElement { - pub fn new( - terminal: Entity, - terminal_view: Entity, - workspace: WeakEntity, - focus: FocusHandle, - focused: bool, - cursor_visible: bool, - block_below_cursor: Option>, - mode: TerminalMode, - ) -> TerminalElement { - TerminalElement { - terminal, - terminal_view, - workspace, - focused, - focus: focus.clone(), - cursor_visible, - block_below_cursor, - mode, - interactivity: Default::default(), - } - .track_focus(&focus) - } - - //Vec> -> Clip out the parts of the ranges - - pub fn layout_grid( - grid: impl Iterator, - start_line_offset: i32, - text_style: &TextStyle, - hyperlink: Option<(HighlightStyle, &RangeInclusive)>, - minimum_contrast: f32, - cx: &App, - ) -> (Vec, Vec) { - let start_time = Instant::now(); - let theme = cx.theme(); - - // Pre-allocate with estimated capacity to reduce reallocations - let estimated_cells = grid.size_hint().0; - let estimated_runs = estimated_cells / 10; // Estimate ~10 cells per run - let estimated_regions = estimated_cells / 20; // Estimate ~20 cells per background region - - let mut batched_runs = Vec::with_capacity(estimated_runs); - let mut cell_count = 0; - - // Collect background regions for efficient merging - let mut background_regions: Vec = Vec::with_capacity(estimated_regions); - let mut current_batch: Option = None; - - // First pass: collect all cells and their backgrounds - let linegroups = grid.into_iter().chunk_by(|i| i.point.line); - for (line_index, (_, line)) in linegroups.into_iter().enumerate() { - let alac_line = start_line_offset + line_index as i32; - - // Flush any existing batch at line boundaries - if let Some(batch) = current_batch.take() { - batched_runs.push(batch); - } - - let mut previous_cell_had_extras = false; - - for cell in line { - let mut fg = cell.fg; - let mut bg = cell.bg; - if cell.flags.contains(Flags::INVERSE) { - mem::swap(&mut fg, &mut bg); - } - - // Collect background regions (skip default background) - if !matches!(bg, Named(NamedColor::Background)) { - let color = convert_color(&bg, theme); - let col = cell.point.column.0 as i32; - - // Try to extend the last region if it's on the same line with the same color - if let Some(last_region) = background_regions.last_mut() { - if last_region.color == color - && last_region.start_line == alac_line - && last_region.end_line == alac_line - && last_region.end_col + 1 == col - { - last_region.end_col = col; - } else { - background_regions.push(BackgroundRegion::new(alac_line, col, color)); - } - } else { - background_regions.push(BackgroundRegion::new(alac_line, col, color)); - } - } - // Skip wide character spacers - they're just placeholders for the second cell of wide characters - if cell.flags.contains(Flags::WIDE_CHAR_SPACER) { - continue; - } - - // Skip spaces that follow cells with extras (emoji variation sequences) - if cell.c == ' ' && previous_cell_had_extras { - previous_cell_had_extras = false; - continue; - } - // Update tracking for next iteration - previous_cell_had_extras = - matches!(cell.zerowidth(), Some(chars) if !chars.is_empty()); - - //Layout current cell text - { - if !is_blank(&cell) { - cell_count += 1; - let cell_style = TerminalElement::cell_style( - &cell, - fg, - bg, - theme, - text_style, - hyperlink, - minimum_contrast, - ); - - let cell_point = AlacPoint::new(alac_line, cell.point.column.0 as i32); - let zero_width_chars = cell.zerowidth(); - - // Try to batch with existing run - if let Some(ref mut batch) = current_batch { - if batch.can_append(&cell_style) - && batch.start_point.line == cell_point.line - && batch.start_point.column + batch.cell_count as i32 - == cell_point.column - { - batch.append_char(cell.c); - if let Some(chars) = zero_width_chars { - batch.append_zero_width_chars(chars); - } - } else { - // Flush current batch and start new one - let old_batch = current_batch.take().unwrap(); - batched_runs.push(old_batch); - let mut new_batch = BatchedTextRun::new_from_char( - cell_point, - cell.c, - cell_style, - text_style.font_size, - ); - if let Some(chars) = zero_width_chars { - new_batch.append_zero_width_chars(chars); - } - current_batch = Some(new_batch); - } - } else { - // Start new batch - let mut new_batch = BatchedTextRun::new_from_char( - cell_point, - cell.c, - cell_style, - text_style.font_size, - ); - if let Some(chars) = zero_width_chars { - new_batch.append_zero_width_chars(chars); - } - current_batch = Some(new_batch); - } - }; - } - } - } - - // Flush any remaining batch - if let Some(batch) = current_batch { - batched_runs.push(batch); - } - - // Second pass: merge background regions and convert to layout rects - let region_count = background_regions.len(); - let merged_regions = merge_background_regions(background_regions); - let mut rects = Vec::with_capacity(merged_regions.len() * 2); // Estimate 2 rects per merged region - - // Convert merged regions to layout rects - // Since LayoutRect only supports single-line rectangles, we need to split multi-line regions - for region in merged_regions { - for line in region.start_line..=region.end_line { - rects.push(LayoutRect::new( - AlacPoint::new(line, region.start_col), - (region.end_col - region.start_col + 1) as usize, - region.color, - )); - } - } - - let layout_time = start_time.elapsed(); - log::debug!( - "Terminal layout_grid: {} cells processed, {} batched runs created, {} rects (from {} merged regions), layout took {:?}", - cell_count, - batched_runs.len(), - rects.len(), - region_count, - layout_time - ); - - (rects, batched_runs) - } - - /// Computes the cursor position and expected block width, may return a zero width if x_for_index returns - /// the same position for sequential indexes. Use em_width instead - fn shape_cursor( - cursor_point: DisplayCursor, - size: TerminalBounds, - text_fragment: &ShapedLine, - ) -> Option<(Point, Pixels)> { - if cursor_point.line() < size.total_lines() as i32 { - let cursor_width = if text_fragment.width == Pixels::ZERO { - size.cell_width() - } else { - text_fragment.width - }; - - // Cursor should always surround as much of the text as possible, - // hence when on pixel boundaries round the origin down and the width up - Some(( - point( - (cursor_point.col() as f32 * size.cell_width()).floor(), - (cursor_point.line() as f32 * size.line_height()).floor(), - ), - cursor_width.ceil(), - )) - } else { - None - } - } - - /// Checks if a character is a decorative block/box-like character that should - /// preserve its exact colors without contrast adjustment. - /// - /// This specifically targets characters used as visual connectors, separators, - /// and borders where color matching with adjacent backgrounds is critical. - /// Regular icons (git, folders, etc.) are excluded as they need to remain readable. - /// - /// Fixes https://github.com/zed-industries/zed/issues/34234 - fn is_decorative_character(ch: char) -> bool { - matches!( - ch as u32, - // Unicode Box Drawing and Block Elements - 0x2500..=0x257F // Box Drawing (└ ┐ ─ │ etc.) - | 0x2580..=0x259F // Block Elements (▀ ▄ █ ░ ▒ ▓ etc.) - | 0x25A0..=0x25FF // Geometric Shapes (■ ▶ ● etc. - includes triangular/circular separators) - - // Private Use Area - Powerline separator symbols only - | 0xE0B0..=0xE0B7 // Powerline separators: triangles (E0B0-E0B3) and half circles (E0B4-E0B7) - | 0xE0B8..=0xE0BF // Powerline separators: corner triangles - | 0xE0C0..=0xE0CA // Powerline separators: flames (E0C0-E0C3), pixelated (E0C4-E0C7), and ice (E0C8 & E0CA) - | 0xE0CC..=0xE0D1 // Powerline separators: honeycombs (E0CC-E0CD) and lego (E0CE-E0D1) - | 0xE0D2..=0xE0D7 // Powerline separators: trapezoid (E0D2 & E0D4) and inverted triangles (E0D6-E0D7) - ) - } - - /// Converts the Alacritty cell styles to GPUI text styles and background color. - fn cell_style( - indexed: &IndexedCell, - fg: terminal::alacritty_terminal::vte::ansi::Color, - bg: terminal::alacritty_terminal::vte::ansi::Color, - colors: &Theme, - text_style: &TextStyle, - hyperlink: Option<(HighlightStyle, &RangeInclusive)>, - minimum_contrast: f32, - ) -> TextRun { - let flags = indexed.cell.flags; - let mut fg = convert_color(&fg, colors); - let bg = convert_color(&bg, colors); - - // Only apply contrast adjustment to non-decorative characters - if !Self::is_decorative_character(indexed.c) { - fg = ensure_minimum_contrast(fg, bg, minimum_contrast); - } - - // Ghostty uses (175/255) as the multiplier (~0.69), Alacritty uses 0.66, Kitty - // uses 0.75. We're using 0.7 because it's pretty well in the middle of that. - if flags.intersects(Flags::DIM) { - fg.a *= 0.7; - } - - let underline = (flags.intersects(Flags::ALL_UNDERLINES) - || indexed.cell.hyperlink().is_some()) - .then(|| UnderlineStyle { - color: Some(fg), - thickness: Pixels::from(1.0), - wavy: flags.contains(Flags::UNDERCURL), - }); - - let strikethrough = flags - .intersects(Flags::STRIKEOUT) - .then(|| StrikethroughStyle { - color: Some(fg), - thickness: Pixels::from(1.0), - }); - - let weight = if flags.intersects(Flags::BOLD) { - FontWeight::BOLD - } else { - text_style.font_weight - }; - - let style = if flags.intersects(Flags::ITALIC) { - FontStyle::Italic - } else { - FontStyle::Normal - }; - - let mut result = TextRun { - len: indexed.c.len_utf8(), - color: fg, - background_color: None, - font: Font { - weight, - style, - ..text_style.font() - }, - underline, - strikethrough, - }; - - if let Some((style, range)) = hyperlink - && range.contains(&indexed.point) - { - if let Some(underline) = style.underline { - result.underline = Some(underline); - } - - if let Some(color) = style.color { - result.color = color; - } - } - - result - } - - fn generic_button_handler( - connection: Entity, - focus_handle: FocusHandle, - steal_focus: bool, - f: impl Fn(&mut Terminal, &E, &mut Context), - ) -> impl Fn(&E, &mut Window, &mut App) { - move |event, window, cx| { - if steal_focus { - window.focus(&focus_handle); - } else if !focus_handle.is_focused(window) { - return; - } - connection.update(cx, |terminal, cx| { - f(terminal, event, cx); - - cx.notify(); - }) - } - } - - fn register_mouse_listeners( - &mut self, - mode: TermMode, - hitbox: &Hitbox, - content_mode: &ContentMode, - window: &mut Window, - ) { - let focus = self.focus.clone(); - let terminal = self.terminal.clone(); - let terminal_view = self.terminal_view.clone(); - - self.interactivity.on_mouse_down(MouseButton::Left, { - let terminal = terminal.clone(); - let focus = focus.clone(); - let terminal_view = terminal_view.clone(); - - move |e, window, cx| { - window.focus(&focus); - - let scroll_top = terminal_view.read(cx).scroll_top; - terminal.update(cx, |terminal, cx| { - let mut adjusted_event = e.clone(); - if scroll_top > Pixels::ZERO { - adjusted_event.position.y += scroll_top; - } - terminal.mouse_down(&adjusted_event, cx); - cx.notify(); - }) - } - }); - - window.on_mouse_event({ - let terminal = self.terminal.clone(); - let hitbox = hitbox.clone(); - let focus = focus.clone(); - let terminal_view = terminal_view; - move |e: &MouseMoveEvent, phase, window, cx| { - if phase != DispatchPhase::Bubble { - return; - } - - if e.pressed_button.is_some() && !cx.has_active_drag() && focus.is_focused(window) { - let hovered = hitbox.is_hovered(window); - - let scroll_top = terminal_view.read(cx).scroll_top; - terminal.update(cx, |terminal, cx| { - if terminal.selection_started() || hovered { - let mut adjusted_event = e.clone(); - if scroll_top > Pixels::ZERO { - adjusted_event.position.y += scroll_top; - } - terminal.mouse_drag(&adjusted_event, hitbox.bounds, cx); - cx.notify(); - } - }) - } - - if hitbox.is_hovered(window) { - terminal.update(cx, |terminal, cx| { - terminal.mouse_move(e, cx); - }) - } - } - }); - - self.interactivity.on_mouse_up( - MouseButton::Left, - TerminalElement::generic_button_handler( - terminal.clone(), - focus.clone(), - false, - move |terminal, e, cx| { - terminal.mouse_up(e, cx); - }, - ), - ); - self.interactivity.on_mouse_down( - MouseButton::Middle, - TerminalElement::generic_button_handler( - terminal.clone(), - focus.clone(), - true, - move |terminal, e, cx| { - terminal.mouse_down(e, cx); - }, - ), - ); - - if content_mode.is_scrollable() { - self.interactivity.on_scroll_wheel({ - let terminal_view = self.terminal_view.downgrade(); - move |e, window, cx| { - terminal_view - .update(cx, |terminal_view, cx| { - if matches!(terminal_view.mode, TerminalMode::Standalone) - || terminal_view.focus_handle.is_focused(window) - { - terminal_view.scroll_wheel(e, cx); - cx.notify(); - } - }) - .ok(); - } - }); - } - - // Mouse mode handlers: - // All mouse modes need the extra click handlers - if mode.intersects(TermMode::MOUSE_MODE) { - self.interactivity.on_mouse_down( - MouseButton::Right, - TerminalElement::generic_button_handler( - terminal.clone(), - focus.clone(), - true, - move |terminal, e, cx| { - terminal.mouse_down(e, cx); - }, - ), - ); - self.interactivity.on_mouse_up( - MouseButton::Right, - TerminalElement::generic_button_handler( - terminal.clone(), - focus.clone(), - false, - move |terminal, e, cx| { - terminal.mouse_up(e, cx); - }, - ), - ); - self.interactivity.on_mouse_up( - MouseButton::Middle, - TerminalElement::generic_button_handler( - terminal, - focus, - false, - move |terminal, e, cx| { - terminal.mouse_up(e, cx); - }, - ), - ); - } - } - - fn rem_size(&self, cx: &mut App) -> Option { - let settings = ThemeSettings::get_global(cx).clone(); - let buffer_font_size = settings.buffer_font_size(cx); - let rem_size_scale = { - // Our default UI font size is 14px on a 16px base scale. - // This means the default UI font size is 0.875rems. - let default_font_size_scale = 14. / ui::BASE_REM_SIZE_IN_PX; - - // We then determine the delta between a single rem and the default font - // size scale. - let default_font_size_delta = 1. - default_font_size_scale; - - // Finally, we add this delta to 1rem to get the scale factor that - // should be used to scale up the UI. - 1. + default_font_size_delta - }; - - Some(buffer_font_size * rem_size_scale) - } -} - -impl Element for TerminalElement { - type RequestLayoutState = (); - type PrepaintState = LayoutState; - - fn id(&self) -> Option { - self.interactivity.element_id.clone() - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - global_id: Option<&GlobalElementId>, - inspector_id: Option<&gpui::InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState) { - let height: Length = match self.terminal_view.read(cx).content_mode(window, cx) { - ContentMode::Inline { - displayed_lines, - total_lines: _, - } => { - let rem_size = window.rem_size(); - let line_height = f32::from(window.text_style().font_size.to_pixels(rem_size)) - * TerminalSettings::get_global(cx) - .line_height - .value() - .to_pixels(rem_size); - (displayed_lines * line_height).into() - } - ContentMode::Scrollable => { - if let TerminalMode::Embedded { .. } = &self.mode { - let term = self.terminal.read(cx); - if !term.scrolled_to_top() && !term.scrolled_to_bottom() && self.focused { - self.interactivity.occlude_mouse(); - } - } - - relative(1.).into() - } - }; - - let layout_id = self.interactivity.request_layout( - global_id, - inspector_id, - window, - cx, - |mut style, window, cx| { - style.size.width = relative(1.).into(); - style.size.height = height; - - window.request_layout(style, None, cx) - }, - ); - (layout_id, ()) - } - - fn prepaint( - &mut self, - global_id: Option<&GlobalElementId>, - inspector_id: Option<&gpui::InspectorElementId>, - bounds: Bounds, - _: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Self::PrepaintState { - let rem_size = self.rem_size(cx); - self.interactivity.prepaint( - global_id, - inspector_id, - bounds, - bounds.size, - window, - cx, - |_, _, hitbox, window, cx| { - let hitbox = hitbox.unwrap(); - let settings = ThemeSettings::get_global(cx).clone(); - - let buffer_font_size = settings.buffer_font_size(cx); - - let terminal_settings = TerminalSettings::get_global(cx); - let minimum_contrast = terminal_settings.minimum_contrast; - - let font_family = terminal_settings.font_family.as_ref().map_or_else( - || settings.buffer_font.family.clone(), - |font_family| font_family.0.clone().into(), - ); - - let font_fallbacks = terminal_settings - .font_fallbacks - .as_ref() - .or(settings.buffer_font.fallbacks.as_ref()) - .cloned(); - - let font_features = terminal_settings - .font_features - .as_ref() - .unwrap_or(&FontFeatures::disable_ligatures()) - .clone(); - - let font_weight = terminal_settings.font_weight.unwrap_or_default(); - - let line_height = terminal_settings.line_height.value(); - - let font_size = match &self.mode { - TerminalMode::Embedded { .. } => { - window.text_style().font_size.to_pixels(window.rem_size()) - } - TerminalMode::Standalone => terminal_settings - .font_size - .map_or(buffer_font_size, |size| theme::adjusted_font_size(size, cx)), - }; - - let theme = cx.theme().clone(); - - let link_style = HighlightStyle { - color: Some(theme.colors().link_text_hover), - font_weight: Some(font_weight), - font_style: None, - background_color: None, - underline: Some(UnderlineStyle { - thickness: px(1.0), - color: Some(theme.colors().link_text_hover), - wavy: false, - }), - strikethrough: None, - fade_out: None, - }; - - let text_style = TextStyle { - font_family, - font_features, - font_weight, - font_fallbacks, - font_size: font_size.into(), - font_style: FontStyle::Normal, - line_height: line_height.into(), - background_color: Some(theme.colors().terminal_ansi_background), - white_space: WhiteSpace::Normal, - // These are going to be overridden per-cell - color: theme.colors().terminal_foreground, - ..Default::default() - }; - - let text_system = cx.text_system(); - let player_color = theme.players().local(); - let match_color = theme.colors().search_match_background; - let gutter; - let (dimensions, line_height_px) = { - let rem_size = window.rem_size(); - let font_pixels = text_style.font_size.to_pixels(rem_size); - // TODO: line_height should be an f32 not an AbsoluteLength. - let line_height = f32::from(font_pixels) * line_height.to_pixels(rem_size); - let font_id = cx.text_system().resolve_font(&text_style.font()); - - let cell_width = text_system - .advance(font_id, font_pixels, 'm') - .unwrap() - .width; - gutter = cell_width; - - let mut size = bounds.size; - size.width -= gutter; - - // https://github.com/zed-industries/zed/issues/2750 - // if the terminal is one column wide, rendering 🦀 - // causes alacritty to misbehave. - if size.width < cell_width * 2.0 { - size.width = cell_width * 2.0; - } - - let mut origin = bounds.origin; - origin.x += gutter; - - ( - TerminalBounds::new(line_height, cell_width, Bounds { origin, size }), - line_height, - ) - }; - - let search_matches = self.terminal.read(cx).matches.clone(); - - let background_color = theme.colors().terminal_background; - - let (last_hovered_word, hover_tooltip) = - self.terminal.update(cx, |terminal, cx| { - terminal.set_size(dimensions); - terminal.sync(window, cx); - - if window.modifiers().secondary() - && bounds.contains(&window.mouse_position()) - && self.terminal_view.read(cx).hover.is_some() - { - let registered_hover = self.terminal_view.read(cx).hover.as_ref(); - if terminal.last_content.last_hovered_word.as_ref() - == registered_hover.map(|hover| &hover.hovered_word) - { - ( - terminal.last_content.last_hovered_word.clone(), - registered_hover.map(|hover| hover.tooltip.clone()), - ) - } else { - (None, None) - } - } else { - (None, None) - } - }); - - let scroll_top = self.terminal_view.read(cx).scroll_top; - let hyperlink_tooltip = hover_tooltip.map(|hover_tooltip| { - let offset = bounds.origin + point(gutter, px(0.)) - point(px(0.), scroll_top); - let mut element = div() - .size_full() - .id("terminal-element") - .tooltip(Tooltip::text(hover_tooltip)) - .into_any_element(); - element.prepaint_as_root(offset, bounds.size.into(), window, cx); - element - }); - - let TerminalContent { - cells, - mode, - display_offset, - cursor_char, - selection, - cursor, - .. - } = &self.terminal.read(cx).last_content; - let mode = *mode; - let display_offset = *display_offset; - - // searches, highlights to a single range representations - let mut relative_highlighted_ranges = Vec::new(); - for search_match in search_matches { - relative_highlighted_ranges.push((search_match, match_color)) - } - if let Some(selection) = selection { - relative_highlighted_ranges - .push((selection.start..=selection.end, player_color.selection)); - } - - // then have that representation be converted to the appropriate highlight data structure - - let content_mode = self.terminal_view.read(cx).content_mode(window, cx); - let (rects, batched_text_runs) = match content_mode { - ContentMode::Scrollable => { - // In scrollable mode, the terminal already provides cells - // that are correctly positioned for the current viewport - // based on its display_offset. We don't need additional filtering. - TerminalElement::layout_grid( - cells.iter().cloned(), - 0, - &text_style, - last_hovered_word.as_ref().map(|last_hovered_word| { - (link_style, &last_hovered_word.word_match) - }), - minimum_contrast, - cx, - ) - } - ContentMode::Inline { .. } => { - let intersection = window.content_mask().bounds.intersect(&bounds); - let start_row = (intersection.top() - bounds.top()) / line_height_px; - let end_row = start_row + intersection.size.height / line_height_px; - let line_range = (start_row as i32)..=(end_row as i32); - - TerminalElement::layout_grid( - cells - .iter() - .skip_while(|i| &i.point.line < line_range.start()) - .take_while(|i| &i.point.line <= line_range.end()) - .cloned(), - *line_range.start(), - &text_style, - last_hovered_word.as_ref().map(|last_hovered_word| { - (link_style, &last_hovered_word.word_match) - }), - minimum_contrast, - cx, - ) - } - }; - - // Layout cursor. Rectangle is used for IME, so we should lay it out even - // if we don't end up showing it. - let cursor = if let AlacCursorShape::Hidden = cursor.shape { - None - } else { - let cursor_point = DisplayCursor::from(cursor.point, display_offset); - let cursor_text = { - let str_trxt = cursor_char.to_string(); - let len = str_trxt.len(); - window.text_system().shape_line( - str_trxt.into(), - text_style.font_size.to_pixels(window.rem_size()), - &[TextRun { - len, - font: text_style.font(), - color: theme.colors().terminal_ansi_background, - ..Default::default() - }], - None, - ) - }; - - let focused = self.focused; - TerminalElement::shape_cursor(cursor_point, dimensions, &cursor_text).map( - move |(cursor_position, block_width)| { - let (shape, text) = match cursor.shape { - AlacCursorShape::Block if !focused => (CursorShape::Hollow, None), - AlacCursorShape::Block => (CursorShape::Block, Some(cursor_text)), - AlacCursorShape::Underline => (CursorShape::Underline, None), - AlacCursorShape::Beam => (CursorShape::Bar, None), - AlacCursorShape::HollowBlock => (CursorShape::Hollow, None), - //This case is handled in the if wrapping the whole cursor layout - AlacCursorShape::Hidden => unreachable!(), - }; - - CursorLayout::new( - cursor_position, - block_width, - dimensions.line_height, - theme.players().local().cursor, - shape, - text, - ) - }, - ) - }; - - let block_below_cursor_element = if let Some(block) = &self.block_below_cursor { - let terminal = self.terminal.read(cx); - if terminal.last_content.display_offset == 0 { - let target_line = terminal.last_content.cursor.point.line.0 + 1; - let render = &block.render; - let mut block_cx = BlockContext { - window, - context: cx, - dimensions, - }; - let element = render(&mut block_cx); - let mut element = div().occlude().child(element).into_any_element(); - let available_space = size( - AvailableSpace::Definite(dimensions.width() + gutter), - AvailableSpace::Definite( - block.height as f32 * dimensions.line_height(), - ), - ); - let origin = bounds.origin - + point(px(0.), target_line as f32 * dimensions.line_height()) - - point(px(0.), scroll_top); - window.with_rem_size(rem_size, |window| { - element.prepaint_as_root(origin, available_space, window, cx); - }); - Some(element) - } else { - None - } - } else { - None - }; - - LayoutState { - hitbox, - batched_text_runs, - cursor, - background_color, - dimensions, - rects, - relative_highlighted_ranges, - mode, - display_offset, - hyperlink_tooltip, - gutter, - block_below_cursor_element, - base_text_style: text_style, - content_mode, - } - }, - ) - } - - fn paint( - &mut self, - global_id: Option<&GlobalElementId>, - inspector_id: Option<&gpui::InspectorElementId>, - bounds: Bounds, - _: &mut Self::RequestLayoutState, - layout: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - let paint_start = Instant::now(); - window.with_content_mask(Some(ContentMask { bounds }), |window| { - let scroll_top = self.terminal_view.read(cx).scroll_top; - - window.paint_quad(fill(bounds, layout.background_color)); - let origin = - bounds.origin + Point::new(layout.gutter, px(0.)) - Point::new(px(0.), scroll_top); - - let marked_text_cloned: Option = { - let ime_state = &self.terminal_view.read(cx).ime_state; - ime_state.as_ref().map(|state| state.marked_text.clone()) - }; - - let terminal_input_handler = TerminalInputHandler { - terminal: self.terminal.clone(), - terminal_view: self.terminal_view.clone(), - cursor_bounds: layout - .cursor - .as_ref() - .map(|cursor| cursor.bounding_rect(origin)), - workspace: self.workspace.clone(), - }; - - self.register_mouse_listeners( - layout.mode, - &layout.hitbox, - &layout.content_mode, - window, - ); - if window.modifiers().secondary() - && bounds.contains(&window.mouse_position()) - && self.terminal_view.read(cx).hover.is_some() - { - window.set_cursor_style(gpui::CursorStyle::PointingHand, &layout.hitbox); - } else { - window.set_cursor_style(gpui::CursorStyle::IBeam, &layout.hitbox); - } - - let original_cursor = layout.cursor.take(); - let hyperlink_tooltip = layout.hyperlink_tooltip.take(); - let block_below_cursor_element = layout.block_below_cursor_element.take(); - self.interactivity.paint( - global_id, - inspector_id, - bounds, - Some(&layout.hitbox), - window, - cx, - |_, window, cx| { - window.handle_input(&self.focus, terminal_input_handler, cx); - - window.on_key_event({ - let this = self.terminal.clone(); - move |event: &ModifiersChangedEvent, phase, window, cx| { - if phase != DispatchPhase::Bubble { - return; - } - - this.update(cx, |term, cx| { - term.try_modifiers_change(&event.modifiers, window, cx) - }); - } - }); - - for rect in &layout.rects { - rect.paint(origin, &layout.dimensions, window); - } - - for (relative_highlighted_range, color) in -& layout.relative_highlighted_ranges - { - if let Some((start_y, highlighted_range_lines)) = - to_highlighted_range_lines(relative_highlighted_range, layout, origin) - { - let corner_radius = if EditorSettings::get_global(cx).rounded_selection { - 0.15 * layout.dimensions.line_height - } else { - Pixels::ZERO - }; - let hr = HighlightedRange { - start_y, - line_height: layout.dimensions.line_height, - lines: highlighted_range_lines, - color: *color, - corner_radius: corner_radius, - }; - hr.paint(true, bounds, window); - } - } - - // Paint batched text runs instead of individual cells - let text_paint_start = Instant::now(); - for batch in &layout.batched_text_runs { - batch.paint(origin, &layout.dimensions, window, cx); - } - let text_paint_time = text_paint_start.elapsed(); - - if let Some(text_to_mark) = &marked_text_cloned - && !text_to_mark.is_empty() - && let Some(cursor_layout) = &original_cursor { - let ime_position = cursor_layout.bounding_rect(origin).origin; - let mut ime_style = layout.base_text_style.clone(); - ime_style.underline = Some(UnderlineStyle { - color: Some(ime_style.color), - thickness: px(1.0), - wavy: false, - }); - - let shaped_line = window.text_system().shape_line( - text_to_mark.clone().into(), - ime_style.font_size.to_pixels(window.rem_size()), - &[TextRun { - len: text_to_mark.len(), - font: ime_style.font(), - color: ime_style.color, - underline: ime_style.underline, - ..Default::default() - }], - None - ); - shaped_line - .paint(ime_position, layout.dimensions.line_height, window, cx) - .log_err(); - } - - if self.cursor_visible && marked_text_cloned.is_none() - && let Some(mut cursor) = original_cursor { - cursor.paint(origin, window, cx); - } - - if let Some(mut element) = block_below_cursor_element { - element.paint(window, cx); - } - - if let Some(mut element) = hyperlink_tooltip { - element.paint(window, cx); - } - let total_paint_time = paint_start.elapsed(); - log::debug!( - "Terminal paint: {} text runs, {} rects, text paint took {:?}, total paint took {:?}", - layout.batched_text_runs.len(), - layout.rects.len(), - text_paint_time, - total_paint_time - ); - }, - ); - }); - } -} - -impl IntoElement for TerminalElement { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - -struct TerminalInputHandler { - terminal: Entity, - terminal_view: Entity, - workspace: WeakEntity, - cursor_bounds: Option>, -} - -impl InputHandler for TerminalInputHandler { - fn selected_text_range( - &mut self, - _ignore_disabled_input: bool, - _: &mut Window, - cx: &mut App, - ) -> Option { - if self - .terminal - .read(cx) - .last_content - .mode - .contains(TermMode::ALT_SCREEN) - { - None - } else { - Some(UTF16Selection { - range: 0..0, - reversed: false, - }) - } - } - - fn marked_text_range( - &mut self, - _window: &mut Window, - cx: &mut App, - ) -> Option> { - self.terminal_view.read(cx).marked_text_range() - } - - fn text_for_range( - &mut self, - _: std::ops::Range, - _: &mut Option>, - _: &mut Window, - _: &mut App, - ) -> Option { - None - } - - fn replace_text_in_range( - &mut self, - _replacement_range: Option>, - text: &str, - window: &mut Window, - cx: &mut App, - ) { - self.terminal_view.update(cx, |view, view_cx| { - view.clear_marked_text(view_cx); - view.commit_text(text, view_cx); - }); - - self.workspace - .update(cx, |this, cx| { - window.invalidate_character_coordinates(); - let project = this.project().read(cx); - let telemetry = project.client().telemetry().clone(); - telemetry.log_edit_event("terminal", project.is_via_remote_server()); - }) - .ok(); - } - - fn replace_and_mark_text_in_range( - &mut self, - _range_utf16: Option>, - new_text: &str, - new_marked_range: Option>, - _window: &mut Window, - cx: &mut App, - ) { - self.terminal_view.update(cx, |view, view_cx| { - view.set_marked_text(new_text.to_string(), new_marked_range, view_cx); - }); - } - - fn unmark_text(&mut self, _window: &mut Window, cx: &mut App) { - self.terminal_view.update(cx, |view, view_cx| { - view.clear_marked_text(view_cx); - }); - } - - fn bounds_for_range( - &mut self, - range_utf16: std::ops::Range, - _window: &mut Window, - cx: &mut App, - ) -> Option> { - let term_bounds = self.terminal_view.read(cx).terminal_bounds(cx); - - let mut bounds = self.cursor_bounds?; - let offset_x = term_bounds.cell_width * range_utf16.start as f32; - bounds.origin.x += offset_x; - - Some(bounds) - } - - fn apple_press_and_hold_enabled(&mut self) -> bool { - false - } - - fn character_index_for_point( - &mut self, - _point: Point, - _window: &mut Window, - _cx: &mut App, - ) -> Option { - None - } -} - -pub fn is_blank(cell: &IndexedCell) -> bool { - if cell.c != ' ' { - return false; - } - - if cell.bg != AnsiColor::Named(NamedColor::Background) { - return false; - } - - if cell.hyperlink().is_some() { - return false; - } - - if cell - .flags - .intersects(Flags::ALL_UNDERLINES | Flags::INVERSE | Flags::STRIKEOUT) - { - return false; - } - - true -} - -fn to_highlighted_range_lines( - range: &RangeInclusive, - layout: &LayoutState, - origin: Point, -) -> Option<(Pixels, Vec)> { - // Step 1. Normalize the points to be viewport relative. - // When display_offset = 1, here's how the grid is arranged: - //-2,0 -2,1... - //--- Viewport top - //-1,0 -1,1... - //--------- Terminal Top - // 0,0 0,1... - // 1,0 1,1... - //--- Viewport Bottom - // 2,0 2,1... - //--------- Terminal Bottom - - // Normalize to viewport relative, from terminal relative. - // lines are i32s, which are negative above the top left corner of the terminal - // If the user has scrolled, we use the display_offset to tell us which offset - // of the grid data we should be looking at. But for the rendering step, we don't - // want negatives. We want things relative to the 'viewport' (the area of the grid - // which is currently shown according to the display offset) - let unclamped_start = AlacPoint::new( - range.start().line + layout.display_offset, - range.start().column, - ); - let unclamped_end = - AlacPoint::new(range.end().line + layout.display_offset, range.end().column); - - // Step 2. Clamp range to viewport, and return None if it doesn't overlap - if unclamped_end.line.0 < 0 || unclamped_start.line.0 > layout.dimensions.num_lines() as i32 { - return None; - } - - let clamped_start_line = unclamped_start.line.0.max(0) as usize; - - let clamped_end_line = unclamped_end - .line - .0 - .min(layout.dimensions.num_lines() as i32) as usize; - - // Convert the start of the range to pixels - let start_y = origin.y + clamped_start_line as f32 * layout.dimensions.line_height; - - // Step 3. Expand ranges that cross lines into a collection of single-line ranges. - // (also convert to pixels) - let mut highlighted_range_lines = Vec::new(); - for line in clamped_start_line..=clamped_end_line { - let mut line_start = 0; - let mut line_end = layout.dimensions.columns(); - - if line == clamped_start_line && unclamped_start.line.0 >= 0 { - line_start = unclamped_start.column.0; - } - if line == clamped_end_line && unclamped_end.line.0 <= layout.dimensions.num_lines() as i32 - { - line_end = unclamped_end.column.0 + 1; // +1 for inclusive - } - - highlighted_range_lines.push(HighlightedRangeLine { - start_x: origin.x + line_start as f32 * layout.dimensions.cell_width, - end_x: origin.x + line_end as f32 * layout.dimensions.cell_width, - }); - } - - Some((start_y, highlighted_range_lines)) -} - -/// Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent. -pub fn convert_color(fg: &terminal::alacritty_terminal::vte::ansi::Color, theme: &Theme) -> Hsla { - let colors = theme.colors(); - match fg { - // Named and theme defined colors - terminal::alacritty_terminal::vte::ansi::Color::Named(n) => match n { - NamedColor::Black => colors.terminal_ansi_black, - NamedColor::Red => colors.terminal_ansi_red, - NamedColor::Green => colors.terminal_ansi_green, - NamedColor::Yellow => colors.terminal_ansi_yellow, - NamedColor::Blue => colors.terminal_ansi_blue, - NamedColor::Magenta => colors.terminal_ansi_magenta, - NamedColor::Cyan => colors.terminal_ansi_cyan, - NamedColor::White => colors.terminal_ansi_white, - NamedColor::BrightBlack => colors.terminal_ansi_bright_black, - NamedColor::BrightRed => colors.terminal_ansi_bright_red, - NamedColor::BrightGreen => colors.terminal_ansi_bright_green, - NamedColor::BrightYellow => colors.terminal_ansi_bright_yellow, - NamedColor::BrightBlue => colors.terminal_ansi_bright_blue, - NamedColor::BrightMagenta => colors.terminal_ansi_bright_magenta, - NamedColor::BrightCyan => colors.terminal_ansi_bright_cyan, - NamedColor::BrightWhite => colors.terminal_ansi_bright_white, - NamedColor::Foreground => colors.terminal_foreground, - NamedColor::Background => colors.terminal_ansi_background, - NamedColor::Cursor => theme.players().local().cursor, - NamedColor::DimBlack => colors.terminal_ansi_dim_black, - NamedColor::DimRed => colors.terminal_ansi_dim_red, - NamedColor::DimGreen => colors.terminal_ansi_dim_green, - NamedColor::DimYellow => colors.terminal_ansi_dim_yellow, - NamedColor::DimBlue => colors.terminal_ansi_dim_blue, - NamedColor::DimMagenta => colors.terminal_ansi_dim_magenta, - NamedColor::DimCyan => colors.terminal_ansi_dim_cyan, - NamedColor::DimWhite => colors.terminal_ansi_dim_white, - NamedColor::BrightForeground => colors.terminal_bright_foreground, - NamedColor::DimForeground => colors.terminal_dim_foreground, - }, - // 'True' colors - terminal::alacritty_terminal::vte::ansi::Color::Spec(rgb) => { - terminal::rgba_color(rgb.r, rgb.g, rgb.b) - } - // 8 bit, indexed colors - terminal::alacritty_terminal::vte::ansi::Color::Indexed(i) => { - terminal::get_color_at_index(*i as usize, theme) - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::{AbsoluteLength, Hsla, font}; - use ui::utils::apca_contrast; - - #[test] - fn test_is_decorative_character() { - // Box Drawing characters (U+2500 to U+257F) - assert!(TerminalElement::is_decorative_character('─')); // U+2500 - assert!(TerminalElement::is_decorative_character('│')); // U+2502 - assert!(TerminalElement::is_decorative_character('┌')); // U+250C - assert!(TerminalElement::is_decorative_character('┐')); // U+2510 - assert!(TerminalElement::is_decorative_character('└')); // U+2514 - assert!(TerminalElement::is_decorative_character('┘')); // U+2518 - assert!(TerminalElement::is_decorative_character('┼')); // U+253C - - // Block Elements (U+2580 to U+259F) - assert!(TerminalElement::is_decorative_character('▀')); // U+2580 - assert!(TerminalElement::is_decorative_character('▄')); // U+2584 - assert!(TerminalElement::is_decorative_character('█')); // U+2588 - assert!(TerminalElement::is_decorative_character('░')); // U+2591 - assert!(TerminalElement::is_decorative_character('▒')); // U+2592 - assert!(TerminalElement::is_decorative_character('▓')); // U+2593 - - // Geometric Shapes - block/box-like subset (U+25A0 to U+25D7) - assert!(TerminalElement::is_decorative_character('■')); // U+25A0 - assert!(TerminalElement::is_decorative_character('□')); // U+25A1 - assert!(TerminalElement::is_decorative_character('▲')); // U+25B2 - assert!(TerminalElement::is_decorative_character('▼')); // U+25BC - assert!(TerminalElement::is_decorative_character('◆')); // U+25C6 - assert!(TerminalElement::is_decorative_character('●')); // U+25CF - - // The specific character from the issue - assert!(TerminalElement::is_decorative_character('◗')); // U+25D7 - assert!(TerminalElement::is_decorative_character('◘')); // U+25D8 (now included in Geometric Shapes) - assert!(TerminalElement::is_decorative_character('◙')); // U+25D9 (now included in Geometric Shapes) - - // Powerline symbols (Private Use Area) - assert!(TerminalElement::is_decorative_character('\u{E0B0}')); // Powerline right triangle - assert!(TerminalElement::is_decorative_character('\u{E0B2}')); // Powerline left triangle - assert!(TerminalElement::is_decorative_character('\u{E0B4}')); // Powerline right half circle (the actual issue!) - assert!(TerminalElement::is_decorative_character('\u{E0B6}')); // Powerline left half circle - assert!(TerminalElement::is_decorative_character('\u{E0CA}')); // Powerline mirrored ice waveform - assert!(TerminalElement::is_decorative_character('\u{E0D7}')); // Powerline left triangle inverted - - // Characters that should NOT be considered decorative - assert!(!TerminalElement::is_decorative_character('A')); // Regular letter - assert!(!TerminalElement::is_decorative_character('$')); // Symbol - assert!(!TerminalElement::is_decorative_character(' ')); // Space - assert!(!TerminalElement::is_decorative_character('←')); // U+2190 (Arrow, not in our ranges) - assert!(!TerminalElement::is_decorative_character('→')); // U+2192 (Arrow, not in our ranges) - assert!(!TerminalElement::is_decorative_character('\u{F00C}')); // Font Awesome check (icon, needs contrast) - assert!(!TerminalElement::is_decorative_character('\u{E711}')); // Devicons (icon, needs contrast) - assert!(!TerminalElement::is_decorative_character('\u{EA71}')); // Codicons folder (icon, needs contrast) - assert!(!TerminalElement::is_decorative_character('\u{F401}')); // Octicons (icon, needs contrast) - assert!(!TerminalElement::is_decorative_character('\u{1F600}')); // Emoji (not in our ranges) - } - - #[test] - fn test_decorative_character_boundary_cases() { - // Test exact boundaries of our ranges - // Box Drawing range boundaries - assert!(TerminalElement::is_decorative_character('\u{2500}')); // First char - assert!(TerminalElement::is_decorative_character('\u{257F}')); // Last char - assert!(!TerminalElement::is_decorative_character('\u{24FF}')); // Just before - - // Block Elements range boundaries - assert!(TerminalElement::is_decorative_character('\u{2580}')); // First char - assert!(TerminalElement::is_decorative_character('\u{259F}')); // Last char - - // Geometric Shapes subset boundaries - assert!(TerminalElement::is_decorative_character('\u{25A0}')); // First char - assert!(TerminalElement::is_decorative_character('\u{25FF}')); // Last char - assert!(!TerminalElement::is_decorative_character('\u{2600}')); // Just after - } - - #[test] - fn test_decorative_characters_bypass_contrast_adjustment() { - // Decorative characters should not be affected by contrast adjustment - - // The specific character from issue #34234 - let problematic_char = '◗'; // U+25D7 - assert!( - TerminalElement::is_decorative_character(problematic_char), - "Character ◗ (U+25D7) should be recognized as decorative" - ); - - // Verify some other commonly used decorative characters - assert!(TerminalElement::is_decorative_character('│')); // Vertical line - assert!(TerminalElement::is_decorative_character('─')); // Horizontal line - assert!(TerminalElement::is_decorative_character('█')); // Full block - assert!(TerminalElement::is_decorative_character('▓')); // Dark shade - assert!(TerminalElement::is_decorative_character('■')); // Black square - assert!(TerminalElement::is_decorative_character('●')); // Black circle - - // Verify normal text characters are NOT decorative - assert!(!TerminalElement::is_decorative_character('A')); - assert!(!TerminalElement::is_decorative_character('1')); - assert!(!TerminalElement::is_decorative_character('$')); - assert!(!TerminalElement::is_decorative_character(' ')); - } - - #[test] - fn test_contrast_adjustment_logic() { - // Test the core contrast adjustment logic without needing full app context - - // Test case 1: Light colors (poor contrast) - let white_fg = gpui::Hsla { - h: 0.0, - s: 0.0, - l: 1.0, - a: 1.0, - }; - let light_gray_bg = gpui::Hsla { - h: 0.0, - s: 0.0, - l: 0.95, - a: 1.0, - }; - - // Should have poor contrast - let actual_contrast = apca_contrast(white_fg, light_gray_bg).abs(); - assert!( - actual_contrast < 30.0, - "White on light gray should have poor APCA contrast: {}", - actual_contrast - ); - - // After adjustment with minimum APCA contrast of 45, should be darker - let adjusted = 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(); - assert!(adjusted_contrast >= 45.0, "Should meet minimum contrast"); - - // Test case 2: Dark colors (poor contrast) - let black_fg = gpui::Hsla { - h: 0.0, - s: 0.0, - l: 0.0, - a: 1.0, - }; - let dark_gray_bg = gpui::Hsla { - h: 0.0, - s: 0.0, - l: 0.05, - a: 1.0, - }; - - // Should have poor contrast - let actual_contrast = apca_contrast(black_fg, dark_gray_bg).abs(); - assert!( - actual_contrast < 30.0, - "Black on dark gray should have poor APCA contrast: {}", - actual_contrast - ); - - // After adjustment with minimum APCA contrast of 45, should be lighter - let adjusted = 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(); - 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); - assert_eq!( - good_contrast, black_fg, - "Good contrast should not be adjusted" - ); - } - - #[test] - fn test_white_on_white_contrast_issue() { - // This test reproduces the exact issue from the bug report - // where white ANSI text on white background should be adjusted - - // Simulate One Light theme colors - let white_fg = gpui::Hsla { - h: 0.0, - s: 0.0, - l: 0.98, // #fafafaff is approximately 98% lightness - a: 1.0, - }; - let white_bg = gpui::Hsla { - h: 0.0, - s: 0.0, - l: 0.98, // Same as foreground - this is the problem! - a: 1.0, - }; - - // With minimum contrast of 0.0, no adjustment should happen - let no_adjust = 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); - assert!( - adjusted.l < white_fg.l, - "White on white should become darker, got l={}", - adjusted.l - ); - - // Verify the contrast is now acceptable - let new_contrast = apca_contrast(adjusted, white_bg).abs(); - assert!( - new_contrast >= 15.0, - "Adjusted APCA contrast {} should be >= 15.0", - new_contrast - ); - } - - #[test] - fn test_batched_text_run_can_append() { - let style1 = TextRun { - len: 1, - font: font("Helvetica"), - color: Hsla::red(), - ..Default::default() - }; - - let style2 = TextRun { - len: 1, - font: font("Helvetica"), - color: Hsla::red(), - ..Default::default() - }; - - let style3 = TextRun { - len: 1, - font: font("Helvetica"), - color: Hsla::blue(), // Different color - ..Default::default() - }; - - let font_size = AbsoluteLength::Pixels(px(12.0)); - let batch = BatchedTextRun::new_from_char(AlacPoint::new(0, 0), 'a', style1, font_size); - - // Should be able to append same style - assert!(batch.can_append(&style2)); - - // Should not be able to append different style - assert!(!batch.can_append(&style3)); - } - - #[test] - fn test_batched_text_run_append() { - let style = TextRun { - len: 1, - font: font("Helvetica"), - color: Hsla::red(), - ..Default::default() - }; - - let font_size = AbsoluteLength::Pixels(px(12.0)); - let mut batch = BatchedTextRun::new_from_char(AlacPoint::new(0, 0), 'a', style, font_size); - - assert_eq!(batch.text, "a"); - assert_eq!(batch.cell_count, 1); - assert_eq!(batch.style.len, 1); - - batch.append_char('b'); - - assert_eq!(batch.text, "ab"); - assert_eq!(batch.cell_count, 2); - assert_eq!(batch.style.len, 2); - - batch.append_char('c'); - - assert_eq!(batch.text, "abc"); - assert_eq!(batch.cell_count, 3); - assert_eq!(batch.style.len, 3); - } - - #[test] - fn test_batched_text_run_append_char() { - let style = TextRun { - len: 1, - font: font("Helvetica"), - color: Hsla::red(), - ..Default::default() - }; - - let font_size = AbsoluteLength::Pixels(px(12.0)); - let mut batch = BatchedTextRun::new_from_char(AlacPoint::new(0, 0), 'x', style, font_size); - - assert_eq!(batch.text, "x"); - assert_eq!(batch.cell_count, 1); - assert_eq!(batch.style.len, 1); - - batch.append_char('y'); - - assert_eq!(batch.text, "xy"); - assert_eq!(batch.cell_count, 2); - assert_eq!(batch.style.len, 2); - - // Test with multi-byte character - batch.append_char('😀'); - - assert_eq!(batch.text, "xy😀"); - assert_eq!(batch.cell_count, 3); - assert_eq!(batch.style.len, 6); // 1 + 1 + 4 bytes for emoji - } - - #[test] - fn test_batched_text_run_append_zero_width_char() { - let style = TextRun { - len: 1, - font: font("Helvetica"), - color: Hsla::red(), - ..Default::default() - }; - - let font_size = AbsoluteLength::Pixels(px(12.0)); - let mut batch = BatchedTextRun::new_from_char(AlacPoint::new(0, 0), 'x', style, font_size); - - let combining = '\u{0301}'; - batch.append_zero_width_chars(&[combining]); - - assert_eq!(batch.text, format!("x{}", combining)); - assert_eq!(batch.cell_count, 1); - assert_eq!(batch.style.len, 1 + combining.len_utf8()); - } - - #[test] - fn test_background_region_can_merge() { - let color1 = Hsla::red(); - let color2 = Hsla::blue(); - - // Test horizontal merging - let mut region1 = BackgroundRegion::new(0, 0, color1); - region1.end_col = 5; - let region2 = BackgroundRegion::new(0, 6, color1); - assert!(region1.can_merge_with(®ion2)); - - // Test vertical merging with same column span - let mut region3 = BackgroundRegion::new(0, 0, color1); - region3.end_col = 5; - let mut region4 = BackgroundRegion::new(1, 0, color1); - region4.end_col = 5; - assert!(region3.can_merge_with(®ion4)); - - // Test cannot merge different colors - let region5 = BackgroundRegion::new(0, 0, color1); - let region6 = BackgroundRegion::new(0, 1, color2); - assert!(!region5.can_merge_with(®ion6)); - - // Test cannot merge non-adjacent regions - let region7 = BackgroundRegion::new(0, 0, color1); - let region8 = BackgroundRegion::new(0, 2, color1); - assert!(!region7.can_merge_with(®ion8)); - - // Test cannot merge vertical regions with different column spans - let mut region9 = BackgroundRegion::new(0, 0, color1); - region9.end_col = 5; - let mut region10 = BackgroundRegion::new(1, 0, color1); - region10.end_col = 6; - assert!(!region9.can_merge_with(®ion10)); - } - - #[test] - fn test_background_region_merge() { - let color = Hsla::red(); - - // Test horizontal merge - let mut region1 = BackgroundRegion::new(0, 0, color); - region1.end_col = 5; - let mut region2 = BackgroundRegion::new(0, 6, color); - region2.end_col = 10; - region1.merge_with(®ion2); - assert_eq!(region1.start_col, 0); - assert_eq!(region1.end_col, 10); - assert_eq!(region1.start_line, 0); - assert_eq!(region1.end_line, 0); - - // Test vertical merge - let mut region3 = BackgroundRegion::new(0, 0, color); - region3.end_col = 5; - let mut region4 = BackgroundRegion::new(1, 0, color); - region4.end_col = 5; - region3.merge_with(®ion4); - assert_eq!(region3.start_col, 0); - assert_eq!(region3.end_col, 5); - assert_eq!(region3.start_line, 0); - assert_eq!(region3.end_line, 1); - } - - #[test] - fn test_merge_background_regions() { - let color = Hsla::red(); - - // Test merging multiple adjacent regions - let regions = vec![ - BackgroundRegion::new(0, 0, color), - BackgroundRegion::new(0, 1, color), - BackgroundRegion::new(0, 2, color), - BackgroundRegion::new(1, 0, color), - BackgroundRegion::new(1, 1, color), - BackgroundRegion::new(1, 2, color), - ]; - - let merged = merge_background_regions(regions); - assert_eq!(merged.len(), 1); - assert_eq!(merged[0].start_line, 0); - assert_eq!(merged[0].end_line, 1); - assert_eq!(merged[0].start_col, 0); - assert_eq!(merged[0].end_col, 2); - - // Test with non-mergeable regions - let color2 = Hsla::blue(); - let regions2 = vec![ - BackgroundRegion::new(0, 0, color), - BackgroundRegion::new(0, 2, color), // Gap at column 1 - BackgroundRegion::new(1, 0, color2), // Different color - ]; - - let merged2 = merge_background_regions(regions2); - assert_eq!(merged2.len(), 3); - } -} diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs deleted file mode 100644 index ab89787fc8..0000000000 --- a/crates/terminal_view/src/terminal_panel.rs +++ /dev/null @@ -1,1978 +0,0 @@ -use std::{cmp, ops::ControlFlow, path::PathBuf, process::ExitStatus, sync::Arc, time::Duration}; - -use crate::{ - TerminalView, default_working_directory, - persistence::{ - SerializedItems, SerializedTerminalPanel, deserialize_terminal_panel, serialize_pane_group, - }, -}; -use breadcrumbs::Breadcrumbs; -use collections::HashMap; -use db::kvp::KEY_VALUE_STORE; -use futures::{channel::oneshot, future::join_all}; -use gpui::{ - Action, AnyView, App, AsyncApp, AsyncWindowContext, Context, Corner, Entity, EventEmitter, - ExternalPaths, FocusHandle, Focusable, IntoElement, ParentElement, Pixels, Render, Styled, - Task, WeakEntity, Window, actions, -}; -use itertools::Itertools; -use project::{Fs, Project, ProjectEntryId}; -use search::{BufferSearchBar, buffer_search::DivRegistrar}; -use settings::{Settings, TerminalDockPosition}; -use task::{RevealStrategy, RevealTarget, Shell, ShellBuilder, SpawnInTerminal, TaskId}; -use terminal::{Terminal, terminal_settings::TerminalSettings}; -use ui::{ - ButtonLike, Clickable, ContextMenu, FluentBuilder, PopoverMenu, SplitButton, Toggleable, - Tooltip, prelude::*, -}; -use util::{ResultExt, TryFutureExt}; -use workspace::{ - ActivateNextPane, ActivatePane, ActivatePaneDown, ActivatePaneLeft, ActivatePaneRight, - ActivatePaneUp, ActivatePreviousPane, DraggedSelection, DraggedTab, ItemId, MoveItemToPane, - MoveItemToPaneInDirection, MovePaneDown, MovePaneLeft, MovePaneRight, MovePaneUp, NewTerminal, - Pane, PaneGroup, SplitDirection, SplitDown, SplitLeft, SplitRight, SplitUp, SwapPaneDown, - SwapPaneLeft, SwapPaneRight, SwapPaneUp, ToggleZoom, Workspace, - dock::{DockPosition, Panel, PanelEvent, PanelHandle}, - item::SerializableItem, - move_active_item, move_item, pane, -}; - -use anyhow::{Result, anyhow}; -use zed_actions::assistant::InlineAssist; - -const TERMINAL_PANEL_KEY: &str = "TerminalPanel"; - -actions!( - terminal_panel, - [ - /// Toggles the terminal panel. - Toggle, - /// Toggles focus on the terminal panel. - ToggleFocus - ] -); - -pub fn init(cx: &mut App) { - cx.observe_new( - |workspace: &mut Workspace, _window, _: &mut Context| { - workspace.register_action(TerminalPanel::new_terminal); - workspace.register_action(TerminalPanel::open_terminal); - workspace.register_action(|workspace, _: &ToggleFocus, window, cx| { - if is_enabled_in_workspace(workspace, cx) { - workspace.toggle_panel_focus::(window, cx); - } - }); - workspace.register_action(|workspace, _: &Toggle, window, cx| { - if is_enabled_in_workspace(workspace, cx) { - if !workspace.toggle_panel_focus::(window, cx) { - workspace.close_panel::(window, cx); - } - } - }); - }, - ) - .detach(); -} - -pub struct TerminalPanel { - pub(crate) active_pane: Entity, - pub(crate) center: PaneGroup, - fs: Arc, - workspace: WeakEntity, - pub(crate) width: Option, - pub(crate) height: Option, - pending_serialization: Task>, - pending_terminals_to_add: usize, - deferred_tasks: HashMap>, - assistant_enabled: bool, - assistant_tab_bar_button: Option, - active: bool, -} - -impl TerminalPanel { - pub fn new(workspace: &Workspace, window: &mut Window, cx: &mut Context) -> Self { - let project = workspace.project(); - let pane = new_terminal_pane(workspace.weak_handle(), project.clone(), false, window, cx); - let center = PaneGroup::new(pane.clone()); - let terminal_panel = Self { - center, - active_pane: pane, - fs: workspace.app_state().fs.clone(), - workspace: workspace.weak_handle(), - pending_serialization: Task::ready(None), - width: None, - height: None, - pending_terminals_to_add: 0, - deferred_tasks: HashMap::default(), - assistant_enabled: false, - assistant_tab_bar_button: None, - active: false, - }; - terminal_panel.apply_tab_bar_buttons(&terminal_panel.active_pane, cx); - terminal_panel - } - - pub fn set_assistant_enabled(&mut self, enabled: bool, cx: &mut Context) { - self.assistant_enabled = enabled; - if enabled { - let focus_handle = self - .active_pane - .read(cx) - .active_item() - .map(|item| item.item_focus_handle(cx)) - .unwrap_or(self.focus_handle(cx)); - self.assistant_tab_bar_button = Some( - cx.new(move |_| InlineAssistTabBarButton { focus_handle }) - .into(), - ); - } else { - self.assistant_tab_bar_button = None; - } - for pane in self.center.panes() { - self.apply_tab_bar_buttons(pane, cx); - } - } - - fn apply_tab_bar_buttons(&self, terminal_pane: &Entity, cx: &mut Context) { - let assistant_tab_bar_button = self.assistant_tab_bar_button.clone(); - terminal_pane.update(cx, |pane, cx| { - pane.set_render_tab_bar_buttons(cx, move |pane, window, cx| { - let split_context = pane - .active_item() - .and_then(|item| item.downcast::()) - .map(|terminal_view| terminal_view.read(cx).focus_handle.clone()); - if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) { - return (None, None); - } - let focus_handle = pane.focus_handle(cx); - let right_children = h_flex() - .gap(DynamicSpacing::Base02.rems(cx)) - .child( - PopoverMenu::new("terminal-tab-bar-popover-menu") - .trigger_with_tooltip( - IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small), - Tooltip::text("New…"), - ) - .anchor(Corner::TopRight) - .with_handle(pane.new_item_context_menu_handle.clone()) - .menu(move |window, cx| { - let focus_handle = focus_handle.clone(); - let menu = ContextMenu::build(window, cx, |menu, _, _| { - menu.context(focus_handle.clone()) - .action( - "New Terminal", - workspace::NewTerminal.boxed_clone(), - ) - // We want the focus to go back to terminal panel once task modal is dismissed, - // hence we focus that first. Otherwise, we'd end up without a focused element, as - // context menu will be gone the moment we spawn the modal. - .action( - "Spawn Task", - zed_actions::Spawn::modal().boxed_clone(), - ) - }); - - Some(menu) - }), - ) - .children(assistant_tab_bar_button.clone()) - .child( - PopoverMenu::new("terminal-pane-tab-bar-split") - .trigger_with_tooltip( - IconButton::new("terminal-pane-split", IconName::Split) - .icon_size(IconSize::Small), - Tooltip::text("Split Pane"), - ) - .anchor(Corner::TopRight) - .with_handle(pane.split_item_context_menu_handle.clone()) - .menu({ - move |window, cx| { - ContextMenu::build(window, cx, |menu, _, _| { - menu.when_some( - split_context.clone(), - |menu, split_context| menu.context(split_context), - ) - .action("Split Right", SplitRight.boxed_clone()) - .action("Split Left", SplitLeft.boxed_clone()) - .action("Split Up", SplitUp.boxed_clone()) - .action("Split Down", SplitDown.boxed_clone()) - }) - .into() - } - }), - ) - .child({ - let zoomed = pane.is_zoomed(); - IconButton::new("toggle_zoom", IconName::Maximize) - .icon_size(IconSize::Small) - .toggle_state(zoomed) - .selected_icon(IconName::Minimize) - .on_click(cx.listener(|pane, _, window, cx| { - pane.toggle_zoom(&workspace::ToggleZoom, window, cx); - })) - .tooltip(move |_window, cx| { - Tooltip::for_action( - if zoomed { "Zoom Out" } else { "Zoom In" }, - &ToggleZoom, - cx, - ) - }) - }) - .into_any_element() - .into(); - (None, right_children) - }); - }); - } - - fn serialization_key(workspace: &Workspace) -> Option { - workspace - .database_id() - .map(|id| i64::from(id).to_string()) - .or(workspace.session_id()) - .map(|id| format!("{:?}-{:?}", TERMINAL_PANEL_KEY, id)) - } - - pub async fn load( - workspace: WeakEntity, - mut cx: AsyncWindowContext, - ) -> Result> { - let mut terminal_panel = None; - - if let Some((database_id, serialization_key)) = workspace - .read_with(&cx, |workspace, _| { - workspace - .database_id() - .zip(TerminalPanel::serialization_key(workspace)) - }) - .ok() - .flatten() - && let Some(serialized_panel) = cx - .background_spawn(async move { KEY_VALUE_STORE.read_kvp(&serialization_key) }) - .await - .log_err() - .flatten() - .map(|panel| serde_json::from_str::(&panel)) - .transpose() - .log_err() - .flatten() - && let Ok(serialized) = workspace - .update_in(&mut cx, |workspace, window, cx| { - deserialize_terminal_panel( - workspace.weak_handle(), - workspace.project().clone(), - database_id, - serialized_panel, - window, - cx, - ) - })? - .await - { - terminal_panel = Some(serialized); - } - - let terminal_panel = if let Some(panel) = terminal_panel { - panel - } else { - workspace.update_in(&mut cx, |workspace, window, cx| { - cx.new(|cx| TerminalPanel::new(workspace, window, cx)) - })? - }; - - if let Some(workspace) = workspace.upgrade() { - workspace - .update(&mut cx, |workspace, _| { - workspace.set_terminal_provider(TerminalProvider(terminal_panel.clone())) - }) - .ok(); - } - - // Since panels/docks are loaded outside from the workspace, we cleanup here, instead of through the workspace. - if let Some(workspace) = workspace.upgrade() { - let cleanup_task = workspace.update_in(&mut cx, |workspace, window, cx| { - let alive_item_ids = terminal_panel - .read(cx) - .center - .panes() - .into_iter() - .flat_map(|pane| pane.read(cx).items()) - .map(|item| item.item_id().as_u64() as ItemId) - .collect(); - workspace.database_id().map(|workspace_id| { - TerminalView::cleanup(workspace_id, alive_item_ids, window, cx) - }) - })?; - if let Some(task) = cleanup_task { - task.await.log_err(); - } - } - - if let Some(workspace) = workspace.upgrade() { - let should_focus = workspace - .update_in(&mut cx, |workspace, window, cx| { - workspace.active_item(cx).is_none() - && workspace - .is_dock_at_position_open(terminal_panel.position(window, cx), cx) - }) - .unwrap_or(false); - - if should_focus { - terminal_panel - .update_in(&mut cx, |panel, window, cx| { - panel.active_pane.update(cx, |pane, cx| { - pane.focus_active_item(window, cx); - }); - }) - .ok(); - } - } - Ok(terminal_panel) - } - - fn handle_pane_event( - &mut self, - pane: &Entity, - event: &pane::Event, - window: &mut Window, - cx: &mut Context, - ) { - match event { - pane::Event::ActivateItem { .. } => self.serialize(cx), - pane::Event::RemovedItem { .. } => self.serialize(cx), - pane::Event::Remove { focus_on_pane } => { - let pane_count_before_removal = self.center.panes().len(); - let _removal_result = self.center.remove(pane); - if pane_count_before_removal == 1 { - self.center.first_pane().update(cx, |pane, cx| { - pane.set_zoomed(false, cx); - }); - cx.emit(PanelEvent::Close); - } else if let Some(focus_on_pane) = - focus_on_pane.as_ref().or_else(|| self.center.panes().pop()) - { - focus_on_pane.focus_handle(cx).focus(window); - } - } - pane::Event::ZoomIn => { - for pane in self.center.panes() { - pane.update(cx, |pane, cx| { - pane.set_zoomed(true, cx); - }) - } - cx.emit(PanelEvent::ZoomIn); - cx.notify(); - } - pane::Event::ZoomOut => { - for pane in self.center.panes() { - pane.update(cx, |pane, cx| { - pane.set_zoomed(false, cx); - }) - } - cx.emit(PanelEvent::ZoomOut); - cx.notify(); - } - pane::Event::AddItem { item } => { - if let Some(workspace) = self.workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - item.added_to_pane(workspace, pane.clone(), window, cx) - }) - } - self.serialize(cx); - } - &pane::Event::Split { - direction, - clone_active_item, - } => { - if clone_active_item { - let fut = self.new_pane_with_cloned_active_terminal(window, cx); - let pane = pane.clone(); - cx.spawn_in(window, async move |panel, cx| { - let Some(new_pane) = fut.await else { - return; - }; - panel - .update_in(cx, |panel, window, cx| { - panel.center.split(&pane, &new_pane, direction).log_err(); - window.focus(&new_pane.focus_handle(cx)); - }) - .ok(); - }) - .detach(); - } else { - let Some(item) = pane.update(cx, |pane, cx| pane.take_active_item(window, cx)) - else { - return; - }; - let Ok(project) = self - .workspace - .update(cx, |workspace, _| workspace.project().clone()) - else { - return; - }; - let new_pane = - new_terminal_pane(self.workspace.clone(), project, false, window, cx); - new_pane.update(cx, |pane, cx| { - pane.add_item(item, true, true, None, window, cx); - }); - self.center.split(&pane, &new_pane, direction).log_err(); - window.focus(&new_pane.focus_handle(cx)); - } - } - pane::Event::Focus => { - self.active_pane = pane.clone(); - } - pane::Event::ItemPinned | pane::Event::ItemUnpinned => { - self.serialize(cx); - } - - _ => {} - } - } - - fn new_pane_with_cloned_active_terminal( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - let Some(workspace) = self.workspace.upgrade() else { - return Task::ready(None); - }; - let workspace = workspace.read(cx); - let database_id = workspace.database_id(); - let weak_workspace = self.workspace.clone(); - let project = workspace.project().clone(); - let active_pane = &self.active_pane; - let terminal_view = active_pane - .read(cx) - .active_item() - .and_then(|item| item.downcast::()); - let working_directory = terminal_view - .as_ref() - .and_then(|terminal_view| { - terminal_view - .read(cx) - .terminal() - .read(cx) - .working_directory() - }) - .or_else(|| default_working_directory(workspace, cx)); - let is_zoomed = active_pane.read(cx).is_zoomed(); - cx.spawn_in(window, async move |panel, cx| { - let terminal = project - .update(cx, |project, cx| match terminal_view { - Some(view) => project.clone_terminal( - &view.read(cx).terminal.clone(), - cx, - working_directory, - ), - None => project.create_terminal_shell(working_directory, cx), - }) - .ok()? - .await - .log_err()?; - - panel - .update_in(cx, move |terminal_panel, window, cx| { - let terminal_view = Box::new(cx.new(|cx| { - TerminalView::new( - terminal.clone(), - weak_workspace.clone(), - database_id, - project.downgrade(), - window, - cx, - ) - })); - let pane = new_terminal_pane(weak_workspace, project, is_zoomed, window, cx); - terminal_panel.apply_tab_bar_buttons(&pane, cx); - pane.update(cx, |pane, cx| { - pane.add_item(terminal_view, true, true, None, window, cx); - }); - Some(pane) - }) - .ok() - .flatten() - }) - } - - pub fn open_terminal( - workspace: &mut Workspace, - action: &workspace::OpenTerminal, - window: &mut Window, - cx: &mut Context, - ) { - let Some(terminal_panel) = workspace.panel::(cx) else { - return; - }; - - terminal_panel - .update(cx, |panel, cx| { - panel.add_terminal_shell( - Some(action.working_directory.clone()), - RevealStrategy::Always, - window, - cx, - ) - }) - .detach_and_log_err(cx); - } - - pub fn spawn_task( - &mut self, - task: &SpawnInTerminal, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - let Some(workspace) = self.workspace.upgrade() else { - return Task::ready(Err(anyhow!("failed to read workspace"))); - }; - - let project = workspace.read(cx).project().read(cx); - - if project.is_via_collab() { - return Task::ready(Err(anyhow!("cannot spawn tasks as a guest"))); - } - - let remote_client = project.remote_client(); - let is_windows = project.path_style(cx).is_windows(); - let remote_shell = remote_client - .as_ref() - .and_then(|remote_client| remote_client.read(cx).shell()); - - let shell = if let Some(remote_shell) = remote_shell - && task.shell == Shell::System - { - Shell::Program(remote_shell) - } else { - task.shell.clone() - }; - - let builder = ShellBuilder::new(&shell, is_windows); - let command_label = builder.command_label(task.command.as_deref().unwrap_or("")); - let (command, args) = builder.build_no_quote(task.command.clone(), &task.args); - - let task = SpawnInTerminal { - command_label, - command: Some(command), - args, - ..task.clone() - }; - - if task.allow_concurrent_runs && task.use_new_terminal { - return self.spawn_in_new_terminal(task, window, cx); - } - - let mut terminals_for_task = self.terminals_for_task(&task.full_label, cx); - let Some(existing) = terminals_for_task.pop() else { - return self.spawn_in_new_terminal(task, window, cx); - }; - - let (existing_item_index, task_pane, existing_terminal) = existing; - if task.allow_concurrent_runs { - return self.replace_terminal( - task, - task_pane, - existing_item_index, - existing_terminal, - window, - cx, - ); - } - - let (tx, rx) = oneshot::channel(); - - self.deferred_tasks.insert( - task.id.clone(), - cx.spawn_in(window, async move |terminal_panel, cx| { - wait_for_terminals_tasks(terminals_for_task, cx).await; - let task = terminal_panel.update_in(cx, |terminal_panel, window, cx| { - if task.use_new_terminal { - terminal_panel.spawn_in_new_terminal(task, window, cx) - } else { - terminal_panel.replace_terminal( - task, - task_pane, - existing_item_index, - existing_terminal, - window, - cx, - ) - } - }); - if let Ok(task) = task { - tx.send(task.await).ok(); - } - }), - ); - - cx.spawn(async move |_, _| rx.await?) - } - - fn spawn_in_new_terminal( - &mut self, - spawn_task: SpawnInTerminal, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - let reveal = spawn_task.reveal; - let reveal_target = spawn_task.reveal_target; - match reveal_target { - RevealTarget::Center => self - .workspace - .update(cx, |workspace, cx| { - Self::add_center_terminal(workspace, window, cx, |project, cx| { - project.create_terminal_task(spawn_task, cx) - }) - }) - .unwrap_or_else(|e| Task::ready(Err(e))), - RevealTarget::Dock => self.add_terminal_task(spawn_task, reveal, window, cx), - } - } - - /// Create a new Terminal in the current working directory or the user's home directory - fn new_terminal( - workspace: &mut Workspace, - _: &workspace::NewTerminal, - window: &mut Window, - cx: &mut Context, - ) { - let Some(terminal_panel) = workspace.panel::(cx) else { - return; - }; - - terminal_panel - .update(cx, |this, cx| { - this.add_terminal_shell( - default_working_directory(workspace, cx), - RevealStrategy::Always, - window, - cx, - ) - }) - .detach_and_log_err(cx); - } - - fn terminals_for_task( - &self, - label: &str, - cx: &mut App, - ) -> Vec<(usize, Entity, Entity)> { - let Some(workspace) = self.workspace.upgrade() else { - return Vec::new(); - }; - - let pane_terminal_views = |pane: Entity| { - pane.read(cx) - .items() - .enumerate() - .filter_map(|(index, item)| Some((index, item.act_as::(cx)?))) - .filter_map(|(index, terminal_view)| { - let task_state = terminal_view.read(cx).terminal().read(cx).task()?; - if &task_state.spawned_task.full_label == label { - Some((index, terminal_view)) - } else { - None - } - }) - .map(move |(index, terminal_view)| (index, pane.clone(), terminal_view)) - }; - - self.center - .panes() - .into_iter() - .cloned() - .flat_map(pane_terminal_views) - .chain( - workspace - .read(cx) - .panes() - .iter() - .cloned() - .flat_map(pane_terminal_views), - ) - .sorted_by_key(|(_, _, terminal_view)| terminal_view.entity_id()) - .collect() - } - - fn activate_terminal_view( - &self, - pane: &Entity, - item_index: usize, - focus: bool, - window: &mut Window, - cx: &mut App, - ) { - pane.update(cx, |pane, cx| { - pane.activate_item(item_index, true, focus, window, cx) - }) - } - - pub fn add_center_terminal( - workspace: &mut Workspace, - window: &mut Window, - cx: &mut Context, - create_terminal: impl FnOnce( - &mut Project, - &mut Context, - ) -> Task>> - + 'static, - ) -> Task>> { - if !is_enabled_in_workspace(workspace, cx) { - return Task::ready(Err(anyhow!( - "terminal not yet supported for remote projects" - ))); - } - let project = workspace.project().downgrade(); - cx.spawn_in(window, async move |workspace, cx| { - let terminal = project.update(cx, create_terminal)?.await?; - - workspace.update_in(cx, |workspace, window, cx| { - let terminal_view = cx.new(|cx| { - TerminalView::new( - terminal.clone(), - workspace.weak_handle(), - workspace.database_id(), - workspace.project().downgrade(), - window, - cx, - ) - }); - workspace.add_item_to_active_pane(Box::new(terminal_view), None, true, window, cx); - })?; - Ok(terminal.downgrade()) - }) - } - - pub fn add_terminal_task( - &mut self, - task: SpawnInTerminal, - reveal_strategy: RevealStrategy, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - let workspace = self.workspace.clone(); - cx.spawn_in(window, async move |terminal_panel, cx| { - if workspace.update(cx, |workspace, cx| !is_enabled_in_workspace(workspace, cx))? { - anyhow::bail!("terminal not yet supported for remote projects"); - } - let pane = terminal_panel.update(cx, |terminal_panel, _| { - terminal_panel.pending_terminals_to_add += 1; - terminal_panel.active_pane.clone() - })?; - let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?; - let terminal = project - .update(cx, |project, cx| project.create_terminal_task(task, cx))? - .await?; - let result = workspace.update_in(cx, |workspace, window, cx| { - let terminal_view = Box::new(cx.new(|cx| { - TerminalView::new( - terminal.clone(), - workspace.weak_handle(), - workspace.database_id(), - workspace.project().downgrade(), - window, - cx, - ) - })); - - match reveal_strategy { - RevealStrategy::Always => { - workspace.focus_panel::(window, cx); - } - RevealStrategy::NoFocus => { - workspace.open_panel::(window, cx); - } - RevealStrategy::Never => {} - } - - pane.update(cx, |pane, cx| { - let focus = pane.has_focus(window, cx) - || matches!(reveal_strategy, RevealStrategy::Always); - pane.add_item(terminal_view, true, focus, None, window, cx); - }); - - Ok(terminal.downgrade()) - })?; - terminal_panel.update(cx, |terminal_panel, cx| { - terminal_panel.pending_terminals_to_add = - terminal_panel.pending_terminals_to_add.saturating_sub(1); - terminal_panel.serialize(cx) - })?; - result - }) - } - - fn add_terminal_shell( - &mut self, - cwd: Option, - reveal_strategy: RevealStrategy, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - let workspace = self.workspace.clone(); - - cx.spawn_in(window, async move |terminal_panel, cx| { - if workspace.update(cx, |workspace, cx| !is_enabled_in_workspace(workspace, cx))? { - anyhow::bail!("terminal not yet supported for collaborative projects"); - } - let pane = terminal_panel.update(cx, |terminal_panel, _| { - terminal_panel.pending_terminals_to_add += 1; - terminal_panel.active_pane.clone() - })?; - let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?; - let terminal = project - .update(cx, |project, cx| project.create_terminal_shell(cwd, cx))? - .await; - - match terminal { - Ok(terminal) => { - let result = workspace.update_in(cx, |workspace, window, cx| { - let terminal_view = Box::new(cx.new(|cx| { - TerminalView::new( - terminal.clone(), - workspace.weak_handle(), - workspace.database_id(), - workspace.project().downgrade(), - window, - cx, - ) - })); - - match reveal_strategy { - RevealStrategy::Always => { - workspace.focus_panel::(window, cx); - } - RevealStrategy::NoFocus => { - workspace.open_panel::(window, cx); - } - RevealStrategy::Never => {} - } - - pane.update(cx, |pane, cx| { - let focus = pane.has_focus(window, cx) - || matches!(reveal_strategy, RevealStrategy::Always); - pane.add_item(terminal_view, true, focus, None, window, cx); - }); - - Ok(terminal.downgrade()) - })?; - terminal_panel.update(cx, |terminal_panel, cx| { - terminal_panel.pending_terminals_to_add = - terminal_panel.pending_terminals_to_add.saturating_sub(1); - terminal_panel.serialize(cx) - })?; - result - } - Err(error) => { - pane.update_in(cx, |pane, window, cx| { - let focus = pane.has_focus(window, cx); - let failed_to_spawn = cx.new(|cx| FailedToSpawnTerminal { - error: error.to_string(), - focus_handle: cx.focus_handle(), - }); - pane.add_item(Box::new(failed_to_spawn), true, focus, None, window, cx); - })?; - Err(error) - } - } - }) - } - - fn serialize(&mut self, cx: &mut Context) { - let height = self.height; - let width = self.width; - let Some(serialization_key) = self - .workspace - .read_with(cx, |workspace, _| { - TerminalPanel::serialization_key(workspace) - }) - .ok() - .flatten() - else { - return; - }; - self.pending_serialization = cx.spawn(async move |terminal_panel, cx| { - cx.background_executor() - .timer(Duration::from_millis(50)) - .await; - let terminal_panel = terminal_panel.upgrade()?; - let items = terminal_panel - .update(cx, |terminal_panel, cx| { - SerializedItems::WithSplits(serialize_pane_group( - &terminal_panel.center, - &terminal_panel.active_pane, - cx, - )) - }) - .ok()?; - cx.background_spawn( - async move { - KEY_VALUE_STORE - .write_kvp( - serialization_key, - serde_json::to_string(&SerializedTerminalPanel { - items, - active_item_id: None, - height, - width, - })?, - ) - .await?; - anyhow::Ok(()) - } - .log_err(), - ) - .await; - Some(()) - }); - } - - fn replace_terminal( - &self, - spawn_task: SpawnInTerminal, - task_pane: Entity, - terminal_item_index: usize, - terminal_to_replace: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - let reveal = spawn_task.reveal; - let reveal_target = spawn_task.reveal_target; - let task_workspace = self.workspace.clone(); - cx.spawn_in(window, async move |terminal_panel, cx| { - let project = terminal_panel.update(cx, |this, cx| { - this.workspace - .update(cx, |workspace, _| workspace.project().clone()) - })??; - let new_terminal = project - .update(cx, |project, cx| { - project.create_terminal_task(spawn_task, cx) - })? - .await?; - terminal_to_replace.update_in(cx, |terminal_to_replace, window, cx| { - terminal_to_replace.set_terminal(new_terminal.clone(), window, cx); - })?; - - match reveal { - RevealStrategy::Always => match reveal_target { - RevealTarget::Center => { - task_workspace.update_in(cx, |workspace, window, cx| { - let did_activate = workspace.activate_item( - &terminal_to_replace, - true, - true, - window, - cx, - ); - - anyhow::ensure!(did_activate, "Failed to retrieve terminal pane"); - - anyhow::Ok(()) - })??; - } - RevealTarget::Dock => { - terminal_panel.update_in(cx, |terminal_panel, window, cx| { - terminal_panel.activate_terminal_view( - &task_pane, - terminal_item_index, - true, - window, - cx, - ) - })?; - - cx.spawn(async move |cx| { - task_workspace - .update_in(cx, |workspace, window, cx| { - workspace.focus_panel::(window, cx) - }) - .ok() - }) - .detach(); - } - }, - RevealStrategy::NoFocus => match reveal_target { - RevealTarget::Center => { - task_workspace.update_in(cx, |workspace, window, cx| { - workspace.active_pane().focus_handle(cx).focus(window); - })?; - } - RevealTarget::Dock => { - terminal_panel.update_in(cx, |terminal_panel, window, cx| { - terminal_panel.activate_terminal_view( - &task_pane, - terminal_item_index, - false, - window, - cx, - ) - })?; - - cx.spawn(async move |cx| { - task_workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_panel::(window, cx) - }) - .ok() - }) - .detach(); - } - }, - RevealStrategy::Never => {} - } - - Ok(new_terminal.downgrade()) - }) - } - - fn has_no_terminals(&self, cx: &App) -> bool { - self.active_pane.read(cx).items_len() == 0 && self.pending_terminals_to_add == 0 - } - - pub fn assistant_enabled(&self) -> bool { - self.assistant_enabled - } - - fn is_enabled(&self, cx: &App) -> bool { - self.workspace - .upgrade() - .is_some_and(|workspace| is_enabled_in_workspace(workspace.read(cx), cx)) - } - - fn activate_pane_in_direction( - &mut self, - direction: SplitDirection, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(pane) = self - .center - .find_pane_in_direction(&self.active_pane, direction, cx) - { - window.focus(&pane.focus_handle(cx)); - } else { - self.workspace - .update(cx, |workspace, cx| { - workspace.activate_pane_in_direction(direction, window, cx) - }) - .ok(); - } - } - - fn swap_pane_in_direction(&mut self, direction: SplitDirection, cx: &mut Context) { - if let Some(to) = self - .center - .find_pane_in_direction(&self.active_pane, direction, cx) - .cloned() - { - self.center.swap(&self.active_pane, &to); - cx.notify(); - } - } - - fn move_pane_to_border(&mut self, direction: SplitDirection, cx: &mut Context) { - if self - .center - .move_to_border(&self.active_pane, direction) - .unwrap() - { - cx.notify(); - } - } -} - -fn is_enabled_in_workspace(workspace: &Workspace, cx: &App) -> bool { - workspace.project().read(cx).supports_terminal(cx) -} - -pub fn new_terminal_pane( - workspace: WeakEntity, - project: Entity, - zoomed: bool, - window: &mut Window, - cx: &mut Context, -) -> Entity { - let is_local = project.read(cx).is_local(); - let terminal_panel = cx.entity(); - let pane = cx.new(|cx| { - let mut pane = Pane::new( - workspace.clone(), - project.clone(), - Default::default(), - None, - NewTerminal.boxed_clone(), - false, - window, - cx, - ); - pane.set_zoomed(zoomed, cx); - pane.set_can_navigate(false, cx); - pane.display_nav_history_buttons(None); - pane.set_should_display_tab_bar(|_, _| true); - pane.set_zoom_out_on_close(false); - - let split_closure_terminal_panel = terminal_panel.downgrade(); - pane.set_can_split(Some(Arc::new(move |pane, dragged_item, _window, cx| { - if let Some(tab) = dragged_item.downcast_ref::() { - let is_current_pane = tab.pane == cx.entity(); - let Some(can_drag_away) = split_closure_terminal_panel - .read_with(cx, |terminal_panel, _| { - let current_panes = terminal_panel.center.panes(); - !current_panes.contains(&&tab.pane) - || current_panes.len() > 1 - || (!is_current_pane || pane.items_len() > 1) - }) - .ok() - else { - return false; - }; - if can_drag_away { - let item = if is_current_pane { - pane.item_for_index(tab.ix) - } else { - tab.pane.read(cx).item_for_index(tab.ix) - }; - if let Some(item) = item { - return item.downcast::().is_some(); - } - } - } - false - }))); - - let buffer_search_bar = cx.new(|cx| { - search::BufferSearchBar::new(Some(project.read(cx).languages().clone()), window, cx) - }); - let breadcrumbs = cx.new(|_| Breadcrumbs::new()); - pane.toolbar().update(cx, |toolbar, cx| { - toolbar.add_item(buffer_search_bar, window, cx); - toolbar.add_item(breadcrumbs, window, cx); - }); - - let drop_closure_project = project.downgrade(); - let drop_closure_terminal_panel = terminal_panel.downgrade(); - pane.set_custom_drop_handle(cx, move |pane, dropped_item, window, cx| { - let Some(project) = drop_closure_project.upgrade() else { - return ControlFlow::Break(()); - }; - if let Some(tab) = dropped_item.downcast_ref::() { - let this_pane = cx.entity(); - let item = if tab.pane == this_pane { - pane.item_for_index(tab.ix) - } else { - tab.pane.read(cx).item_for_index(tab.ix) - }; - if let Some(item) = item { - if item.downcast::().is_some() { - let source = tab.pane.clone(); - let item_id_to_move = item.item_id(); - - let Ok(new_split_pane) = pane - .drag_split_direction() - .map(|split_direction| { - drop_closure_terminal_panel.update(cx, |terminal_panel, cx| { - let is_zoomed = if terminal_panel.active_pane == this_pane { - pane.is_zoomed() - } else { - terminal_panel.active_pane.read(cx).is_zoomed() - }; - let new_pane = new_terminal_pane( - workspace.clone(), - project.clone(), - is_zoomed, - window, - cx, - ); - terminal_panel.apply_tab_bar_buttons(&new_pane, cx); - terminal_panel.center.split( - &this_pane, - &new_pane, - split_direction, - )?; - anyhow::Ok(new_pane) - }) - }) - .transpose() - else { - return ControlFlow::Break(()); - }; - - match new_split_pane.transpose() { - // Source pane may be the one currently updated, so defer the move. - Ok(Some(new_pane)) => cx - .spawn_in(window, async move |_, cx| { - cx.update(|window, cx| { - move_item( - &source, - &new_pane, - item_id_to_move, - new_pane.read(cx).active_item_index(), - true, - window, - cx, - ); - }) - .ok(); - }) - .detach(), - // If we drop into existing pane or current pane, - // regular pane drop handler will take care of it, - // using the right tab index for the operation. - Ok(None) => return ControlFlow::Continue(()), - err @ Err(_) => { - err.log_err(); - return ControlFlow::Break(()); - } - }; - } else if let Some(project_path) = item.project_path(cx) - && let Some(entry_path) = project.read(cx).absolute_path(&project_path, cx) - { - add_paths_to_terminal(pane, &[entry_path], window, cx); - } - } - } else if let Some(selection) = dropped_item.downcast_ref::() { - let project = project.read(cx); - let paths_to_add = selection - .items() - .map(|selected_entry| selected_entry.entry_id) - .filter_map(|entry_id| project.path_for_entry(entry_id, cx)) - .filter_map(|project_path| project.absolute_path(&project_path, cx)) - .collect::>(); - if !paths_to_add.is_empty() { - add_paths_to_terminal(pane, &paths_to_add, window, cx); - } - } else if let Some(&entry_id) = dropped_item.downcast_ref::() { - if let Some(entry_path) = project - .read(cx) - .path_for_entry(entry_id, cx) - .and_then(|project_path| project.read(cx).absolute_path(&project_path, cx)) - { - add_paths_to_terminal(pane, &[entry_path], window, cx); - } - } else if is_local && let Some(paths) = dropped_item.downcast_ref::() { - add_paths_to_terminal(pane, paths.paths(), window, cx); - } - - ControlFlow::Break(()) - }); - - pane - }); - - cx.subscribe_in(&pane, window, TerminalPanel::handle_pane_event) - .detach(); - cx.observe(&pane, |_, _, cx| cx.notify()).detach(); - - pane -} - -async fn wait_for_terminals_tasks( - terminals_for_task: Vec<(usize, Entity, Entity)>, - cx: &mut AsyncApp, -) { - let pending_tasks = terminals_for_task.iter().filter_map(|(_, _, terminal)| { - terminal - .update(cx, |terminal_view, cx| { - terminal_view - .terminal() - .update(cx, |terminal, cx| terminal.wait_for_completed_task(cx)) - }) - .ok() - }); - join_all(pending_tasks).await; -} - -fn add_paths_to_terminal( - pane: &mut Pane, - paths: &[PathBuf], - window: &mut Window, - cx: &mut Context, -) { - if let Some(terminal_view) = pane - .active_item() - .and_then(|item| item.downcast::()) - { - window.focus(&terminal_view.focus_handle(cx)); - let mut new_text = paths.iter().map(|path| format!(" {path:?}")).join(""); - new_text.push(' '); - terminal_view.update(cx, |terminal_view, cx| { - terminal_view.terminal().update(cx, |terminal, _| { - terminal.paste(&new_text); - }); - }); - } -} - -struct FailedToSpawnTerminal { - error: String, - focus_handle: FocusHandle, -} - -impl Focusable for FailedToSpawnTerminal { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Render for FailedToSpawnTerminal { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let popover_menu = PopoverMenu::new("settings-popover") - .trigger( - IconButton::new("icon-button-popover", IconName::ChevronDown) - .icon_size(IconSize::XSmall), - ) - .menu(move |window, cx| { - Some(ContextMenu::build(window, cx, |context_menu, _, _| { - context_menu - .action("Open Settings", zed_actions::OpenSettings.boxed_clone()) - .action( - "Edit settings.json", - zed_actions::OpenSettingsFile.boxed_clone(), - ) - })) - }) - .anchor(Corner::TopRight) - .offset(gpui::Point { - x: px(0.0), - y: px(2.0), - }); - - v_flex() - .track_focus(&self.focus_handle) - .size_full() - .p_4() - .items_center() - .justify_center() - .bg(cx.theme().colors().editor_background) - .child( - v_flex() - .max_w_112() - .items_center() - .justify_center() - .text_center() - .child(Label::new("Failed to spawn terminal")) - .child( - Label::new(self.error.to_string()) - .size(LabelSize::Small) - .color(Color::Muted) - .mb_4(), - ) - .child(SplitButton::new( - ButtonLike::new("open-settings-ui") - .child(Label::new("Edit Settings").size(LabelSize::Small)) - .on_click(|_, window, cx| { - window.dispatch_action(zed_actions::OpenSettings.boxed_clone(), cx); - }), - popover_menu.into_any_element(), - )), - ) - } -} - -impl EventEmitter<()> for FailedToSpawnTerminal {} - -impl workspace::Item for FailedToSpawnTerminal { - type Event = (); - - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { - SharedString::new_static("Failed to spawn terminal") - } -} - -impl EventEmitter for TerminalPanel {} - -impl Render for TerminalPanel { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let mut registrar = DivRegistrar::new( - |panel, _, cx| { - panel - .active_pane - .read(cx) - .toolbar() - .read(cx) - .item_of_type::() - }, - cx, - ); - BufferSearchBar::register(&mut registrar); - let registrar = registrar.into_div(); - self.workspace - .update(cx, |workspace, cx| { - registrar.size_full().child(self.center.render( - workspace.zoomed_item(), - &workspace::PaneRenderContext { - follower_states: &HashMap::default(), - active_call: workspace.active_call(), - active_pane: &self.active_pane, - app_state: workspace.app_state(), - project: workspace.project(), - workspace: &workspace.weak_handle(), - }, - window, - cx, - )) - }) - .ok() - .map(|div| { - div.on_action({ - cx.listener(|terminal_panel, _: &ActivatePaneLeft, window, cx| { - terminal_panel.activate_pane_in_direction(SplitDirection::Left, window, cx); - }) - }) - .on_action({ - cx.listener(|terminal_panel, _: &ActivatePaneRight, window, cx| { - terminal_panel.activate_pane_in_direction( - SplitDirection::Right, - window, - cx, - ); - }) - }) - .on_action({ - cx.listener(|terminal_panel, _: &ActivatePaneUp, window, cx| { - terminal_panel.activate_pane_in_direction(SplitDirection::Up, window, cx); - }) - }) - .on_action({ - cx.listener(|terminal_panel, _: &ActivatePaneDown, window, cx| { - terminal_panel.activate_pane_in_direction(SplitDirection::Down, window, cx); - }) - }) - .on_action( - cx.listener(|terminal_panel, _action: &ActivateNextPane, window, cx| { - let panes = terminal_panel.center.panes(); - if let Some(ix) = panes - .iter() - .position(|pane| **pane == terminal_panel.active_pane) - { - let next_ix = (ix + 1) % panes.len(); - window.focus(&panes[next_ix].focus_handle(cx)); - } - }), - ) - .on_action(cx.listener( - |terminal_panel, _action: &ActivatePreviousPane, window, cx| { - let panes = terminal_panel.center.panes(); - if let Some(ix) = panes - .iter() - .position(|pane| **pane == terminal_panel.active_pane) - { - let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1); - window.focus(&panes[prev_ix].focus_handle(cx)); - } - }, - )) - .on_action( - cx.listener(|terminal_panel, action: &ActivatePane, window, cx| { - let panes = terminal_panel.center.panes(); - if let Some(&pane) = panes.get(action.0) { - window.focus(&pane.read(cx).focus_handle(cx)); - } else { - let future = - terminal_panel.new_pane_with_cloned_active_terminal(window, cx); - cx.spawn_in(window, async move |terminal_panel, cx| { - if let Some(new_pane) = future.await { - _ = terminal_panel.update_in( - cx, - |terminal_panel, window, cx| { - terminal_panel - .center - .split( - &terminal_panel.active_pane, - &new_pane, - SplitDirection::Right, - ) - .log_err(); - let new_pane = new_pane.read(cx); - window.focus(&new_pane.focus_handle(cx)); - }, - ); - } - }) - .detach(); - } - }), - ) - .on_action(cx.listener(|terminal_panel, _: &SwapPaneLeft, _, cx| { - terminal_panel.swap_pane_in_direction(SplitDirection::Left, cx); - })) - .on_action(cx.listener(|terminal_panel, _: &SwapPaneRight, _, cx| { - terminal_panel.swap_pane_in_direction(SplitDirection::Right, cx); - })) - .on_action(cx.listener(|terminal_panel, _: &SwapPaneUp, _, cx| { - terminal_panel.swap_pane_in_direction(SplitDirection::Up, cx); - })) - .on_action(cx.listener(|terminal_panel, _: &SwapPaneDown, _, cx| { - terminal_panel.swap_pane_in_direction(SplitDirection::Down, cx); - })) - .on_action(cx.listener(|terminal_panel, _: &MovePaneLeft, _, cx| { - terminal_panel.move_pane_to_border(SplitDirection::Left, cx); - })) - .on_action(cx.listener(|terminal_panel, _: &MovePaneRight, _, cx| { - terminal_panel.move_pane_to_border(SplitDirection::Right, cx); - })) - .on_action(cx.listener(|terminal_panel, _: &MovePaneUp, _, cx| { - terminal_panel.move_pane_to_border(SplitDirection::Up, cx); - })) - .on_action(cx.listener(|terminal_panel, _: &MovePaneDown, _, cx| { - terminal_panel.move_pane_to_border(SplitDirection::Down, cx); - })) - .on_action( - cx.listener(|terminal_panel, action: &MoveItemToPane, window, cx| { - let Some(&target_pane) = - terminal_panel.center.panes().get(action.destination) - else { - return; - }; - move_active_item( - &terminal_panel.active_pane, - target_pane, - action.focus, - true, - window, - cx, - ); - }), - ) - .on_action(cx.listener( - |terminal_panel, action: &MoveItemToPaneInDirection, window, cx| { - let source_pane = &terminal_panel.active_pane; - if let Some(destination_pane) = terminal_panel - .center - .find_pane_in_direction(source_pane, action.direction, cx) - { - move_active_item( - source_pane, - destination_pane, - action.focus, - true, - window, - cx, - ); - }; - }, - )) - }) - .unwrap_or_else(|| div()) - } -} - -impl Focusable for TerminalPanel { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.active_pane.focus_handle(cx) - } -} - -impl Panel for TerminalPanel { - fn position(&self, _window: &Window, cx: &App) -> DockPosition { - match TerminalSettings::get_global(cx).dock { - TerminalDockPosition::Left => DockPosition::Left, - TerminalDockPosition::Bottom => DockPosition::Bottom, - TerminalDockPosition::Right => DockPosition::Right, - } - } - - fn position_is_valid(&self, _: DockPosition) -> bool { - true - } - - fn set_position( - &mut self, - position: DockPosition, - _window: &mut Window, - cx: &mut Context, - ) { - settings::update_settings_file(self.fs.clone(), cx, move |settings, _| { - let dock = match position { - DockPosition::Left => TerminalDockPosition::Left, - DockPosition::Bottom => TerminalDockPosition::Bottom, - DockPosition::Right => TerminalDockPosition::Right, - }; - settings.terminal.get_or_insert_default().dock = Some(dock); - }); - } - - fn size(&self, window: &Window, cx: &App) -> Pixels { - let settings = TerminalSettings::get_global(cx); - match self.position(window, cx) { - DockPosition::Left | DockPosition::Right => { - self.width.unwrap_or(settings.default_width) - } - DockPosition::Bottom => self.height.unwrap_or(settings.default_height), - } - } - - fn set_size(&mut self, size: Option, window: &mut Window, cx: &mut Context) { - match self.position(window, cx) { - DockPosition::Left | DockPosition::Right => self.width = size, - DockPosition::Bottom => self.height = size, - } - cx.notify(); - cx.defer_in(window, |this, _, cx| { - this.serialize(cx); - }) - } - - fn is_zoomed(&self, _window: &Window, cx: &App) -> bool { - self.active_pane.read(cx).is_zoomed() - } - - fn set_zoomed(&mut self, zoomed: bool, _: &mut Window, cx: &mut Context) { - for pane in self.center.panes() { - pane.update(cx, |pane, cx| { - pane.set_zoomed(zoomed, cx); - }) - } - cx.notify(); - } - - fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context) { - let old_active = self.active; - self.active = active; - if !active || old_active == active || !self.has_no_terminals(cx) { - return; - } - cx.defer_in(window, |this, window, cx| { - let Ok(kind) = this - .workspace - .update(cx, |workspace, cx| default_working_directory(workspace, cx)) - else { - return; - }; - - this.add_terminal_shell(kind, RevealStrategy::Always, window, cx) - .detach_and_log_err(cx) - }) - } - - fn icon_label(&self, _window: &Window, cx: &App) -> Option { - let count = self - .center - .panes() - .into_iter() - .map(|pane| pane.read(cx).items_len()) - .sum::(); - if count == 0 { - None - } else { - Some(count.to_string()) - } - } - - fn persistent_name() -> &'static str { - "TerminalPanel" - } - - fn panel_key() -> &'static str { - TERMINAL_PANEL_KEY - } - - fn icon(&self, _window: &Window, cx: &App) -> Option { - if (self.is_enabled(cx) || !self.has_no_terminals(cx)) - && TerminalSettings::get_global(cx).button - { - Some(IconName::TerminalAlt) - } else { - None - } - } - - fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> { - Some("Terminal Panel") - } - - fn toggle_action(&self) -> Box { - Box::new(ToggleFocus) - } - - fn pane(&self) -> Option> { - Some(self.active_pane.clone()) - } - - fn activation_priority(&self) -> u32 { - 1 - } -} - -struct TerminalProvider(Entity); - -impl workspace::TerminalProvider for TerminalProvider { - fn spawn( - &self, - task: SpawnInTerminal, - window: &mut Window, - cx: &mut App, - ) -> Task>> { - let terminal_panel = self.0.clone(); - window.spawn(cx, async move |cx| { - let terminal = terminal_panel - .update_in(cx, |terminal_panel, window, cx| { - terminal_panel.spawn_task(&task, window, cx) - }) - .ok()? - .await; - match terminal { - Ok(terminal) => { - let exit_status = terminal - .read_with(cx, |terminal, cx| terminal.wait_for_completed_task(cx)) - .ok()? - .await?; - Some(Ok(exit_status)) - } - Err(e) => Some(Err(e)), - } - }) - } -} - -struct InlineAssistTabBarButton { - focus_handle: FocusHandle, -} - -impl Render for InlineAssistTabBarButton { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let focus_handle = self.focus_handle.clone(); - IconButton::new("terminal_inline_assistant", IconName::ZedAssistant) - .icon_size(IconSize::Small) - .on_click(cx.listener(|_, _, window, cx| { - window.dispatch_action(InlineAssist::default().boxed_clone(), cx); - })) - .tooltip(move |_window, cx| { - Tooltip::for_action_in("Inline Assist", &InlineAssist::default(), &focus_handle, cx) - }) - } -} - -#[cfg(test)] -mod tests { - use std::num::NonZero; - - use super::*; - use gpui::{TestAppContext, UpdateGlobal as _}; - use pretty_assertions::assert_eq; - use project::FakeFs; - use settings::SettingsStore; - - #[gpui::test] - async fn test_spawn_an_empty_task(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - - let (window_handle, terminal_panel) = workspace - .update(cx, |workspace, window, cx| { - let window_handle = window.window_handle(); - let terminal_panel = cx.new(|cx| TerminalPanel::new(workspace, window, cx)); - (window_handle, terminal_panel) - }) - .unwrap(); - - let task = window_handle - .update(cx, |_, window, cx| { - terminal_panel.update(cx, |terminal_panel, cx| { - terminal_panel.spawn_task(&SpawnInTerminal::default(), window, cx) - }) - }) - .unwrap(); - - let terminal = task.await.unwrap(); - let expected_shell = util::get_system_shell(); - terminal - .update(cx, |terminal, _| { - let task_metadata = terminal - .task() - .expect("When spawning a task, should have the task metadata") - .spawned_task - .clone(); - assert_eq!(task_metadata.env, HashMap::default()); - assert_eq!(task_metadata.cwd, None); - assert_eq!(task_metadata.shell, task::Shell::System); - assert_eq!( - task_metadata.command, - Some(expected_shell.clone()), - "Empty tasks should spawn a -i shell" - ); - assert_eq!(task_metadata.args, Vec::::new()); - assert_eq!( - task_metadata.command_label, expected_shell, - "We show the shell launch for empty commands" - ); - }) - .unwrap(); - } - - #[gpui::test] - async fn test_bypass_max_tabs_limit(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - - let (window_handle, terminal_panel) = workspace - .update(cx, |workspace, window, cx| { - let window_handle = window.window_handle(); - let terminal_panel = cx.new(|cx| TerminalPanel::new(workspace, window, cx)); - (window_handle, terminal_panel) - }) - .unwrap(); - - set_max_tabs(cx, Some(3)); - - for _ in 0..5 { - let task = window_handle - .update(cx, |_, window, cx| { - terminal_panel.update(cx, |panel, cx| { - panel.add_terminal_shell(None, RevealStrategy::Always, window, cx) - }) - }) - .unwrap(); - task.await.unwrap(); - } - - cx.run_until_parked(); - - let item_count = - terminal_panel.read_with(cx, |panel, cx| panel.active_pane.read(cx).items_len()); - - assert_eq!( - item_count, 5, - "Terminal panel should bypass max_tabs limit and have all 5 terminals" - ); - } - - // A complex Unix command won't be properly parsed by the Windows terminal hence omit the test there. - #[cfg(unix)] - #[gpui::test] - async fn test_spawn_script_like_task(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - - let (window_handle, terminal_panel) = workspace - .update(cx, |workspace, window, cx| { - let window_handle = window.window_handle(); - let terminal_panel = cx.new(|cx| TerminalPanel::new(workspace, window, cx)); - (window_handle, terminal_panel) - }) - .unwrap(); - - let user_command = r#"REPO_URL=$(git remote get-url origin | sed -e \"s/^git@\\(.*\\):\\(.*\\)\\.git$/https:\\/\\/\\1\\/\\2/\"); COMMIT_SHA=$(git log -1 --format=\"%H\" -- \"${ZED_RELATIVE_FILE}\"); echo \"${REPO_URL}/blob/${COMMIT_SHA}/${ZED_RELATIVE_FILE}#L${ZED_ROW}-$(echo $(($(wc -l <<< \"$ZED_SELECTED_TEXT\") + $ZED_ROW - 1)))\" | xclip -selection clipboard"#.to_string(); - - let expected_cwd = PathBuf::from("/some/work"); - let task = window_handle - .update(cx, |_, window, cx| { - terminal_panel.update(cx, |terminal_panel, cx| { - terminal_panel.spawn_task( - &SpawnInTerminal { - command: Some(user_command.clone()), - cwd: Some(expected_cwd.clone()), - ..SpawnInTerminal::default() - }, - window, - cx, - ) - }) - }) - .unwrap(); - - let terminal = task.await.unwrap(); - let shell = util::get_system_shell(); - terminal - .update(cx, |terminal, _| { - let task_metadata = terminal - .task() - .expect("When spawning a task, should have the task metadata") - .spawned_task - .clone(); - assert_eq!(task_metadata.env, HashMap::default()); - assert_eq!(task_metadata.cwd, Some(expected_cwd)); - assert_eq!(task_metadata.shell, task::Shell::System); - assert_eq!(task_metadata.command, Some(shell.clone())); - assert_eq!( - task_metadata.args, - vec!["-i".to_string(), "-c".to_string(), user_command.clone(),], - "Use command should have been moved into the arguments, as we're spawning a new -i shell", - ); - assert_eq!( - task_metadata.command_label, - format!("{shell} {interactive}-c '{user_command}'", interactive = if cfg!(windows) {""} else {"-i "}), - "We want to show to the user the entire command spawned"); - }) - .unwrap(); - } - - #[gpui::test] - async fn renders_error_if_default_shell_fails(cx: &mut TestAppContext) { - init_test(cx); - - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings.terminal.get_or_insert_default().project.shell = - Some(settings::Shell::Program("asdf".to_owned())); - }); - }); - }); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - - let (window_handle, terminal_panel) = workspace - .update(cx, |workspace, window, cx| { - let window_handle = window.window_handle(); - let terminal_panel = cx.new(|cx| TerminalPanel::new(workspace, window, cx)); - (window_handle, terminal_panel) - }) - .unwrap(); - - window_handle - .update(cx, |_, window, cx| { - terminal_panel.update(cx, |terminal_panel, cx| { - terminal_panel.add_terminal_shell(None, RevealStrategy::Always, window, cx) - }) - }) - .unwrap() - .await - .unwrap_err(); - - window_handle - .update(cx, |_, _, cx| { - terminal_panel.update(cx, |terminal_panel, cx| { - assert!( - terminal_panel - .active_pane - .read(cx) - .items() - .any(|item| item.downcast::().is_some()), - "should spawn `FailedToSpawnTerminal` pane" - ); - }) - }) - .unwrap(); - } - - fn set_max_tabs(cx: &mut TestAppContext, value: Option) { - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |settings| { - settings.workspace.max_tabs = value.map(|v| NonZero::new(v).unwrap()) - }); - }); - } - - pub fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let store = SettingsStore::test(cx); - cx.set_global(store); - theme::init(theme::LoadThemes::JustBase, cx); - editor::init(cx); - crate::init(cx); - }); - } -} 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 fa40196645..0000000000 --- a/crates/terminal_view/src/terminal_path_like_target.rs +++ /dev/null @@ -1,1338 +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::{PathStyle, PathWithPosition}, - rel_path::RelPath, -}; -use workspace::{OpenOptions, OpenVisible, Workspace}; - -/// The way we found the open target. This is important to have for test assertions. -/// For example, remote projects never look in the file system. -#[cfg(test)] -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -enum OpenTargetFoundBy { - WorktreeExact, - WorktreeScan, - FileSystemBackground, -} - -#[cfg(test)] -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -enum BackgroundFsChecks { - Enabled, - Disabled, -} - -#[derive(Debug, Clone)] -enum OpenTarget { - Worktree(PathWithPosition, Entry, #[cfg(test)] OpenTargetFoundBy), - 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, - } - } - - #[cfg(test)] - fn found_by(&self) -> OpenTargetFoundBy { - match self { - OpenTarget::Worktree(.., found_by) => *found_by, - OpenTarget::File(..) => OpenTargetFoundBy::FileSystemBackground, - } - } -} - -pub(super) fn hover_path_like_target( - workspace: &WeakEntity, - hovered_word: HoveredWord, - path_like_target: &PathLikeTarget, - cx: &mut Context, -) -> Task<()> { - #[cfg(not(test))] - { - possible_hover_target(workspace, hovered_word, path_like_target, cx) - } - #[cfg(test)] - { - possible_hover_target( - workspace, - hovered_word, - path_like_target, - cx, - BackgroundFsChecks::Enabled, - ) - } -} - -fn possible_hover_target( - workspace: &WeakEntity, - hovered_word: HoveredWord, - path_like_target: &PathLikeTarget, - cx: &mut Context, - #[cfg(test)] background_fs_checks: BackgroundFsChecks, -) -> Task<()> { - let file_to_open_task = possible_open_target( - workspace, - path_like_target, - cx, - #[cfg(test)] - background_fs_checks, - ); - 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().into_owned()), - hovered_word, - }); - } - None => { - terminal_view.hover = None; - } - }) - .ok(); - }) -} - -fn possible_open_target( - workspace: &WeakEntity, - path_like_target: &PathLikeTarget, - cx: &App, - #[cfg(test)] background_fs_checks: BackgroundFsChecks, -) -> 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(); - let mut is_cwd_in_worktree = false; - let mut open_target = None; - 'worktree_loop: for worktree in &worktree_candidates { - let worktree_root = worktree.read(cx).abs_path(); - let mut paths_to_check = Vec::with_capacity(potential_paths.len()); - let relative_cwd = cwd - .and_then(|cwd| cwd.strip_prefix(&worktree_root).ok()) - .and_then(|cwd| RelPath::new(cwd, PathStyle::local()).ok()) - .and_then(|cwd_stripped| { - (cwd_stripped.as_ref() != RelPath::empty()).then(|| { - is_cwd_in_worktree = true; - cwd_stripped - }) - }); - - 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) => { - open_target = Some(OpenTarget::Worktree( - root_path_with_position, - root_entry.clone(), - #[cfg(test)] - OpenTargetFoundBy::WorktreeExact, - )); - break 'worktree_loop; - } - 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 let Ok(relative_path_to_check) = - RelPath::new(&path_to_check.path, PathStyle::local()) - && !worktree.read(cx).is_single_file() - && let Some(entry) = relative_cwd - .clone() - .and_then(|relative_cwd| { - worktree - .read(cx) - .entry_for_path(&relative_cwd.join(&relative_path_to_check)) - }) - .or_else(|| worktree.read(cx).entry_for_path(&relative_path_to_check)) - { - open_target = Some(OpenTarget::Worktree( - PathWithPosition { - path: worktree.read(cx).absolutize(&entry.path), - row: path_to_check.row, - column: path_to_check.column, - }, - entry.clone(), - #[cfg(test)] - OpenTargetFoundBy::WorktreeExact, - )); - break 'worktree_loop; - } - - paths_to_check.push(path_to_check); - } - - if !paths_to_check.is_empty() { - worktree_paths_to_check.push((worktree.clone(), paths_to_check)); - } - } - - #[cfg(not(test))] - let enable_background_fs_checks = workspace.read(cx).project().read(cx).is_local(); - #[cfg(test)] - let enable_background_fs_checks = background_fs_checks == BackgroundFsChecks::Enabled; - - if open_target.is_some() { - // We we want to prefer open targets found via background fs checks over worktree matches, - // however we can return early if either: - // - This is a remote project, or - // - If the terminal working directory is inside of at least one worktree - if !enable_background_fs_checks || is_cwd_in_worktree { - return Task::ready(open_target); - } - } - - // Before entire worktree traversal(s), make an attempt to do FS checks if available. - let fs_paths_to_check = - if enable_background_fs_checks { - let fs_cwd_paths_to_check = cwd - .iter() - .flat_map(|cwd| { - let mut paths_to_check = Vec::new(); - for path_to_check in &potential_paths { - let maybe_path = &path_to_check.path; - if path_to_check.path.is_relative() { - paths_to_check.push(PathWithPosition { - path: cwd.join(&maybe_path), - row: path_to_check.row, - column: path_to_check.column, - }); - } - } - paths_to_check - }) - .collect::>(); - fs_cwd_paths_to_check - .into_iter() - .chain( - 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() { - for worktree in &worktree_candidates { - if !worktree.read(cx).is_single_file() { - 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::>(), - ) - .collect() - } else { - Vec::new() - }; - - let fs = workspace.read(cx).project().read(cx).fs().clone(); - let background_fs_checks_task = 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() - { - if open_target - .as_ref() - .map(|open_target| open_target.path().path != fs_path_to_check) - .unwrap_or(true) - { - path_to_check.path = fs_path_to_check; - return Some(OpenTarget::File(path_to_check, metadata)); - } - - break; - } - } - - open_target - }); - - cx.spawn(async move |cx| { - background_fs_checks_task.await.or_else(|| { - for (worktree, worktree_paths_to_check) in worktree_paths_to_check { - let found_entry = worktree - .update(cx, |worktree, _| -> Option { - let traversal = - worktree.traverse_from_path(true, true, false, RelPath::empty()); - for entry in traversal { - if let Some(path_in_worktree) = - worktree_paths_to_check.iter().find(|path_to_check| { - RelPath::new(&path_to_check.path, PathStyle::local()) - .is_ok_and(|path| entry.path.ends_with(&path)) - }) - { - return Some(OpenTarget::Worktree( - PathWithPosition { - path: worktree.absolutize(&entry.path), - row: path_in_worktree.row, - column: path_in_worktree.column, - }, - entry.clone(), - #[cfg(test)] - OpenTargetFoundBy::WorktreeScan, - )); - } - } - None - }) - .ok()?; - if let Some(found_entry) = found_entry { - return Some(found_entry); - } - } - None - }) - }) -} - -pub(super) fn open_path_like_target( - workspace: &WeakEntity, - terminal_view: &mut TerminalView, - path_like_target: &PathLikeTarget, - window: &mut Window, - cx: &mut Context, -) { - #[cfg(not(test))] - { - possibly_open_target(workspace, terminal_view, path_like_target, window, cx) - .detach_and_log_err(cx) - } - #[cfg(test)] - { - possibly_open_target( - workspace, - terminal_view, - path_like_target, - window, - cx, - BackgroundFsChecks::Enabled, - ) - .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, - #[cfg(test)] background_fs_checks: BackgroundFsChecks, -) -> 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, - #[cfg(test)] - background_fs_checks, - ) - })? - .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; - 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, - BackgroundFsChecks, - ) -> (Option, Option) { - let fs = app_cx.update(AppState::test).fs.as_fake().clone(); - - app_cx.update(|cx| { - theme::init(theme::LoadThemes::JustBase, cx); - editor::init(cx); - }); - - for (path, tree) in trees { - fs.insert_tree(path, tree).await; - } - - let project: gpui::Entity = Project::test( - fs.clone(), - worktree_roots.into_iter().map(Path::new), - app_cx, - ) - .await; - - let (workspace, cx) = - app_cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let cwd = std::env::current_dir().expect("Failed to get working directory"); - let terminal = project - .update(cx, |project: &mut Project, cx| { - project.create_terminal_shell(Some(cwd), 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, - background_fs_checks: BackgroundFsChecks| - -> (Option, Option) { - let workspace_a = workspace.clone(); - terminal_view - .update(cx, |_, cx| { - possible_hover_target( - &workspace_a.downgrade(), - hovered_word, - &path_like_target, - cx, - background_fs_checks, - ) - }) - .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, - background_fs_checks, - ) - }) - .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, - BackgroundFsChecks, - ) -> (Option, Option), - maybe_path: &str, - tooltip: &str, - terminal_dir: Option, - background_fs_checks: BackgroundFsChecks, - mut open_target_found_by: OpenTargetFoundBy, - 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, - }, - background_fs_checks, - ) - .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}:" - ); - - if background_fs_checks == BackgroundFsChecks::Disabled - && open_target_found_by == OpenTargetFoundBy::FileSystemBackground - { - open_target_found_by = OpenTargetFoundBy::WorktreeScan; - } - - assert_eq!( - open_target.found_by(), - open_target_found_by, - "Open target found by mismatch at {file}:{line}:" - ); - } - - macro_rules! none_or_some_pathbuf { - (None) => { - None - }; - ($cwd:literal) => { - Some($crate::PathBuf::from(path!($cwd))) - }; - } - - macro_rules! test_path_like { - ( - $test_path_like:expr, - $maybe_path:literal, - $tooltip:literal, - $cwd:tt, - $found_by:expr - ) => {{ - test_path_like!( - $test_path_like, - $maybe_path, - $tooltip, - $cwd, - BackgroundFsChecks::Enabled, - $found_by - ); - test_path_like!( - $test_path_like, - $maybe_path, - $tooltip, - $cwd, - BackgroundFsChecks::Disabled, - $found_by - ); - }}; - - ( - $test_path_like:expr, - $maybe_path:literal, - $tooltip:literal, - $cwd:tt, - $background_fs_checks:path, - $found_by:expr - ) => { - test_path_like_simple( - &mut $test_path_like, - path!($maybe_path), - path!($tooltip), - none_or_some_pathbuf!($cwd), - $background_fs_checks, - $found_by, - std::file!(), - std::line!(), - ) - .await - }; - } - - // Note the arms of `test`, `test_local`, and `test_remote` should be collapsed once macro - // metavariable expressions (#![feature(macro_metavar_expr)]) are stabilized. - // See https://github.com/rust-lang/rust/issues/83527 - #[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!(, , "] - #[doc ="\\[, found by \\])"] - #[allow(unused_macros)] - macro_rules! test { - ($maybe_path:literal, $tooltip:literal, $cwd:tt) => { - test_path_like!( - test_path_like, - $maybe_path, - $tooltip, - $cwd, - OpenTargetFoundBy::WorktreeExact - ) - }; - ($maybe_path:literal, $tooltip:literal, $cwd:tt, $found_by:ident) => { - test_path_like!( - test_path_like, - $maybe_path, - $tooltip, - $cwd, - OpenTargetFoundBy::$found_by - ) - } - } - #[doc ="test_local!(, , "] - #[doc ="\\[, found by \\])"] - #[allow(unused_macros)] - macro_rules! test_local { - ($maybe_path:literal, $tooltip:literal, $cwd:tt) => { - test_path_like!( - test_path_like, - $maybe_path, - $tooltip, - $cwd, - BackgroundFsChecks::Enabled, - OpenTargetFoundBy::WorktreeExact - ) - }; - ($maybe_path:literal, $tooltip:literal, $cwd:tt, $found_by:ident) => { - test_path_like!( - test_path_like, - $maybe_path, - $tooltip, - $cwd, - BackgroundFsChecks::Enabled, - OpenTargetFoundBy::$found_by - ) - } - } - #[doc ="test_remote!(, , "] - #[doc ="\\[, found by \\])"] - #[allow(unused_macros)] - macro_rules! test_remote { - ($maybe_path:literal, $tooltip:literal, $cwd:tt) => { - test_path_like!( - test_path_like, - $maybe_path, - $tooltip, - $cwd, - BackgroundFsChecks::Disabled, - OpenTargetFoundBy::WorktreeExact - ) - }; - ($maybe_path:literal, $tooltip:literal, $cwd:tt, $found_by:ident) => { - test_path_like!( - test_path_like, - $maybe_path, - $tooltip, - $cwd, - BackgroundFsChecks::Disabled, - OpenTargetFoundBy::$found_by - ) - } - } - $($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", None); - test!("/test/lib.rs", "/test/lib.rs", None); - test!("test.rs", "/test/test.rs", None); - test!("/test/test.rs", "/test/test.rs", None); - } - ) - } - - #[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!("/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"); - - test!("/test/lib.rs", "/test/lib.rs", "/test"); - test!("/test/test.rs", "/test/test.rs", "/test"); - 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", WorktreeScan); - 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] - async fn issue_28407_nesting(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![( - path!("/project"), - json!({ - "lib": { - "src": { - "main.rs": "", - "only_in_lib.rs": "" - }, - }, - "src": { - "main.rs": "" - }, - }), - )], - vec![path!("/project")], - { - test!("main.rs", "/project/src/main.rs", "/project/src"); - 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"); - test!("src/main.rs", "/project/lib/src/main.rs", "/project/lib"); - - 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" - ); - test!( - "src/only_in_lib.rs", - "/project/lib/src/only_in_lib.rs", - "/project/lib/src", - WorktreeScan - ); - } - ) - } - - // https://github.com/zed-industries/zed/issues/28339 - // Note: These could all be found by WorktreeExact if we used - // `fs::normalize_path(&maybe_path)` - #[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_local!( - "foo/./bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339", - WorktreeExact - ); - test_local!( - "foo/../foo/bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339", - WorktreeExact - ); - test_local!( - "foo/..///foo/bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339", - WorktreeExact - ); - test_local!( - "issue28339/../issue28339/foo/../foo/bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339", - WorktreeExact - ); - test_local!( - "./bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339/foo", - WorktreeExact - ); - test_local!( - "../foo/bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339/foo", - FileSystemBackground - ); - } - ) - } - - // https://github.com/zed-industries/zed/issues/28339 - // Note: These could all be found by WorktreeExact if we used - // `fs::normalize_path(&maybe_path)` - #[gpui::test] - #[should_panic(expected = "Hover target should not be `None`")] - async fn issue_28339_remote(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![( - path!("/tmp"), - json!({ - "issue28339": { - "foo": { - "bar.txt": "" - }, - }, - }), - )], - vec![path!("/tmp")], - { - test_remote!( - "foo/./bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339" - ); - test_remote!( - "foo/../foo/bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339" - ); - test_remote!( - "foo/..///foo/bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339" - ); - test_remote!( - "issue28339/../issue28339/foo/../foo/bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339" - ); - test_remote!( - "./bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339/foo" - ); - test_remote!( - "../foo/bar.txt", - "/tmp/issue28339/foo/bar.txt", - "/tmp/issue28339/foo" - ); - } - ) - } - - // https://github.com/zed-industries/zed/issues/34027 - #[gpui::test] - 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] - async fn issue_34027_siblings(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![( - path!("/test"), - json!({ - "sub1": { - "file.txt": "", - }, - "sub2": { - "file.txt": "", - } - }), - ),], - vec![path!("/test")], - { - test!("file.txt", "/test/sub1/file.txt", "/test/sub1"); - test!("file.txt", "/test/sub2/file.txt", "/test/sub2"); - test!("sub1/file.txt", "/test/sub1/file.txt", "/test/sub1"); - test!("sub2/file.txt", "/test/sub2/file.txt", "/test/sub2"); - test!("sub1/file.txt", "/test/sub1/file.txt", "/test/sub2"); - test!("sub2/file.txt", "/test/sub2/file.txt", "/test/sub1"); - } - ) - } - - // https://github.com/zed-industries/zed/issues/34027 - #[gpui::test] - async fn issue_34027_nesting(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![( - path!("/test"), - json!({ - "sub1": { - "file.txt": "", - "subsub1": { - "file.txt": "", - } - }, - "sub2": { - "file.txt": "", - "subsub1": { - "file.txt": "", - } - } - }), - ),], - vec![path!("/test")], - { - test!( - "file.txt", - "/test/sub1/subsub1/file.txt", - "/test/sub1/subsub1" - ); - test!( - "file.txt", - "/test/sub2/subsub1/file.txt", - "/test/sub2/subsub1" - ); - test!( - "subsub1/file.txt", - "/test/sub1/subsub1/file.txt", - "/test", - WorktreeScan - ); - test!( - "subsub1/file.txt", - "/test/sub1/subsub1/file.txt", - "/test", - WorktreeScan - ); - test!( - "subsub1/file.txt", - "/test/sub1/subsub1/file.txt", - "/test/sub1" - ); - test!( - "subsub1/file.txt", - "/test/sub2/subsub1/file.txt", - "/test/sub2" - ); - test!( - "subsub1/file.txt", - "/test/sub1/subsub1/file.txt", - "/test/sub1/subsub1", - WorktreeScan - ); - } - ) - } - - // https://github.com/zed-industries/zed/issues/34027 - #[gpui::test] - async fn issue_34027_non_worktree_local_file(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![ - ( - path!("/"), - json!({ - "file.txt": "", - }), - ), - ( - path!("/test"), - json!({ - "file.txt": "", - }), - ), - ], - vec![path!("/test")], - { - // Note: Opening a non-worktree file adds that file as a single file worktree. - test_local!("file.txt", "/file.txt", "/", FileSystemBackground); - } - ) - } - - // https://github.com/zed-industries/zed/issues/34027 - #[gpui::test] - async fn issue_34027_non_worktree_remote_file(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![ - ( - path!("/"), - json!({ - "file.txt": "", - }), - ), - ( - path!("/test"), - json!({ - "file.txt": "", - }), - ), - ], - vec![path!("/test")], - { - // Note: Opening a non-worktree file adds that file as a single file worktree. - test_remote!("file.txt", "/test/file.txt", "/"); - test_remote!("/test/file.txt", "/test/file.txt", "/"); - } - ) - } - - // See https://github.com/zed-industries/zed/issues/34027 - #[gpui::test] - #[should_panic(expected = "Tooltip mismatch")] - async fn issue_34027_gaps(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![( - path!("/project"), - json!({ - "lib": { - "src": { - "main.rs": "" - }, - }, - "src": { - "main.rs": "" - }, - }), - )], - vec![path!("/project")], - { - test!("main.rs", "/project/src/main.rs", "/project"); - test!("main.rs", "/project/lib/src/main.rs", "/project/lib"); - } - ) - } - - // See https://github.com/zed-industries/zed/issues/34027 - #[gpui::test] - #[should_panic(expected = "Tooltip mismatch")] - async fn issue_34027_overlap(cx: &mut TestAppContext) { - test_path_likes!( - cx, - vec![( - path!("/project"), - json!({ - "lib": { - "src": { - "main.rs": "" - }, - }, - "src": { - "main.rs": "" - }, - }), - )], - vec![path!("/project")], - { - // Finds "/project/src/main.rs" - test!( - "src/main.rs", - "/project/lib/src/main.rs", - "/project/lib/src" - ); - } - ) - } - } -} diff --git a/crates/terminal_view/src/terminal_scrollbar.rs b/crates/terminal_view/src/terminal_scrollbar.rs deleted file mode 100644 index 871bb60230..0000000000 --- a/crates/terminal_view/src/terminal_scrollbar.rs +++ /dev/null @@ -1,91 +0,0 @@ -use std::{ - cell::{Cell, RefCell}, - rc::Rc, -}; - -use gpui::{Bounds, Point, Size, size}; -use terminal::Terminal; -use ui::{Pixels, ScrollableHandle, px}; - -#[derive(Debug)] -struct ScrollHandleState { - line_height: Pixels, - total_lines: usize, - viewport_lines: usize, - display_offset: usize, -} - -impl ScrollHandleState { - fn new(terminal: &Terminal) -> Self { - Self { - line_height: terminal.last_content().terminal_bounds.line_height, - total_lines: terminal.total_lines(), - viewport_lines: terminal.viewport_lines(), - display_offset: terminal.last_content().display_offset, - } - } -} - -#[derive(Debug, Clone)] -pub struct TerminalScrollHandle { - state: Rc>, - pub future_display_offset: Rc>>, -} - -impl TerminalScrollHandle { - pub fn new(terminal: &Terminal) -> Self { - Self { - state: Rc::new(RefCell::new(ScrollHandleState::new(terminal))), - future_display_offset: Rc::new(Cell::new(None)), - } - } - - pub fn update(&self, terminal: &Terminal) { - *self.state.borrow_mut() = ScrollHandleState::new(terminal); - } -} - -impl ScrollableHandle for TerminalScrollHandle { - fn max_offset(&self) -> Size { - let state = self.state.borrow(); - size( - Pixels::ZERO, - state - .total_lines - .checked_sub(state.viewport_lines) - .unwrap_or(0) as f32 - * state.line_height, - ) - } - - fn offset(&self) -> Point { - let state = self.state.borrow(); - let scroll_offset = state.total_lines - state.viewport_lines - state.display_offset; - Point::new( - Pixels::ZERO, - -(scroll_offset as f32 * self.state.borrow().line_height), - ) - } - - fn set_offset(&self, point: Point) { - let state = self.state.borrow(); - let offset_delta = (point.y / state.line_height).round() as i32; - - let max_offset = state.total_lines - state.viewport_lines; - let display_offset = (max_offset as i32 + offset_delta).clamp(0, max_offset as i32); - - self.future_display_offset - .set(Some(display_offset as usize)); - } - - fn viewport(&self) -> Bounds { - let state = self.state.borrow(); - Bounds::new( - Point::new(px(0.), px(0.)), - size( - Pixels::ZERO, - state.viewport_lines as f32 * state.line_height, - ), - ) - } -} diff --git a/crates/terminal_view/src/terminal_slash_command.rs b/crates/terminal_view/src/terminal_slash_command.rs deleted file mode 100644 index 13c2cef48c..0000000000 --- a/crates/terminal_view/src/terminal_slash_command.rs +++ /dev/null @@ -1,129 +0,0 @@ -use std::sync::Arc; -use std::sync::atomic::AtomicBool; - -use crate::{TerminalView, terminal_panel::TerminalPanel}; -use anyhow::Result; -use assistant_slash_command::{ - ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection, - SlashCommandResult, -}; -use gpui::{App, Entity, Task, WeakEntity}; -use language::{BufferSnapshot, CodeLabel, LspAdapterDelegate}; -use ui::prelude::*; -use workspace::{Workspace, dock::Panel}; - -use assistant_slash_command::create_label_for_command; - -pub struct TerminalSlashCommand; - -const LINE_COUNT_ARG: &str = "--line-count"; - -const DEFAULT_CONTEXT_LINES: usize = 50; - -impl SlashCommand for TerminalSlashCommand { - fn name(&self) -> String { - "terminal".into() - } - - fn label(&self, cx: &App) -> CodeLabel { - create_label_for_command("terminal", &[LINE_COUNT_ARG], cx) - } - - fn description(&self) -> String { - "Insert terminal output".into() - } - - fn icon(&self) -> IconName { - IconName::Terminal - } - - fn menu_text(&self) -> String { - self.description() - } - - fn requires_argument(&self) -> bool { - false - } - - fn accepts_arguments(&self) -> bool { - true - } - - fn complete_argument( - self: Arc, - _arguments: &[String], - _cancel: Arc, - _workspace: Option>, - _window: &mut Window, - _cx: &mut App, - ) -> Task>> { - Task::ready(Ok(Vec::new())) - } - - fn run( - self: Arc, - arguments: &[String], - _context_slash_command_output_sections: &[SlashCommandOutputSection], - _context_buffer: BufferSnapshot, - workspace: WeakEntity, - _delegate: Option>, - _: &mut Window, - cx: &mut App, - ) -> Task { - let Some(workspace) = workspace.upgrade() else { - return Task::ready(Err(anyhow::anyhow!("workspace was dropped"))); - }; - - let Some(active_terminal) = resolve_active_terminal(&workspace, cx) else { - return Task::ready(Err(anyhow::anyhow!("no active terminal"))); - }; - - let line_count = arguments - .get(0) - .and_then(|s| s.parse::().ok()) - .unwrap_or(DEFAULT_CONTEXT_LINES); - - let lines = active_terminal - .read(cx) - .entity() - .read(cx) - .last_n_non_empty_lines(line_count); - - let mut text = String::new(); - text.push_str("Terminal output:\n"); - text.push_str(&lines.join("\n")); - let range = 0..text.len(); - - Task::ready(Ok(SlashCommandOutput { - text, - sections: vec![SlashCommandOutputSection { - range, - icon: IconName::Terminal, - label: "Terminal".into(), - metadata: None, - }], - run_commands_in_text: false, - } - .into_event_stream())) - } -} - -fn resolve_active_terminal( - workspace: &Entity, - cx: &mut App, -) -> Option> { - if let Some(terminal_view) = workspace - .read(cx) - .active_item(cx) - .and_then(|item| item.act_as::(cx)) - { - return Some(terminal_view); - } - - let terminal_panel = workspace.read(cx).panel::(cx)?; - terminal_panel.read(cx).pane().and_then(|pane| { - pane.read(cx) - .active_item() - .and_then(|t| t.downcast::()) - }) -} diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs deleted file mode 100644 index 98f7a17a27..0000000000 --- a/crates/terminal_view/src/terminal_view.rs +++ /dev/null @@ -1,1786 +0,0 @@ -mod persistence; -pub mod terminal_element; -pub mod terminal_panel; -mod terminal_path_like_target; -pub mod terminal_scrollbar; -mod terminal_slash_command; - -use assistant_slash_command::SlashCommandRegistry; -use editor::{EditorSettings, actions::SelectAll, blink_manager::BlinkManager}; -use gpui::{ - Action, AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, - KeyContext, KeyDownEvent, Keystroke, MouseButton, MouseDownEvent, Pixels, Render, - ScrollWheelEvent, Styled, Subscription, Task, WeakEntity, actions, anchored, deferred, div, -}; -use persistence::TERMINAL_DB; -use project::{Project, search::SearchQuery}; -use schemars::JsonSchema; -use task::TaskId; -use terminal::{ - Clear, Copy, Event, HoveredWord, MaybeNavigationTarget, Paste, ScrollLineDown, ScrollLineUp, - ScrollPageDown, ScrollPageUp, ScrollToBottom, ScrollToTop, ShowCharacterPalette, TaskState, - TaskStatus, Terminal, TerminalBounds, ToggleViMode, - alacritty_terminal::{ - index::Point, - term::{TermMode, point_to_viewport, search::RegexSearch}, - }, - terminal_settings::{CursorShape, TerminalSettings}, -}; -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 ui::{ - ContextMenu, Divider, ScrollAxes, Scrollbars, Tooltip, WithScrollbar, - prelude::*, - scrollbars::{self, GlobalSetting, ScrollbarVisibility}, -}; -use util::ResultExt; -use workspace::{ - CloseActiveItem, NewCenterTerminal, NewTerminal, ToolbarItemLocation, Workspace, WorkspaceId, - delete_unloaded_items, - item::{ - BreadcrumbText, Item, ItemEvent, SerializableItem, TabContentParams, TabTooltipContent, - }, - register_serializable_item, - searchable::{Direction, SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle}, -}; - -use serde::Deserialize; -use settings::{Settings, SettingsStore, TerminalBlink, WorkingDirectory}; -use zed_actions::assistant::InlineAssist; - -use std::{ - cmp, - ops::{Range, RangeInclusive}, - path::{Path, PathBuf}, - rc::Rc, - sync::Arc, - time::Duration, -}; - -struct ImeState { - marked_text: String, - marked_range_utf16: Option>, -} - -const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); - -/// Event to transmit the scroll from the element to the view -#[derive(Clone, Debug, PartialEq)] -pub struct ScrollTerminal(pub i32); - -/// Sends the specified text directly to the terminal. -#[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = terminal)] -pub struct SendText(String); - -/// Sends a keystroke sequence to the terminal. -#[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = terminal)] -pub struct SendKeystroke(String); - -actions!( - terminal, - [ - /// Reruns the last executed task in the terminal. - RerunTask - ] -); - -pub fn init(cx: &mut App) { - assistant_slash_command::init(cx); - terminal_panel::init(cx); - - register_serializable_item::(cx); - - cx.observe_new(|workspace: &mut Workspace, _window, _cx| { - workspace.register_action(TerminalView::deploy); - }) - .detach(); - SlashCommandRegistry::global(cx).register_command(TerminalSlashCommand, true); -} - -pub struct BlockProperties { - pub height: u8, - pub render: Box AnyElement>, -} - -pub struct BlockContext<'a, 'b> { - pub window: &'a mut Window, - pub context: &'b mut App, - pub dimensions: TerminalBounds, -} - -///A terminal view, maintains the PTY's file handles and communicates with the terminal -pub struct TerminalView { - terminal: Entity, - workspace: WeakEntity, - project: WeakEntity, - focus_handle: FocusHandle, - //Currently using iTerm bell, show bell emoji in tab until input is received - has_bell: bool, - context_menu: Option<(Entity, gpui::Point, Subscription)>, - cursor_shape: CursorShape, - blink_manager: Entity, - mode: TerminalMode, - blinking_terminal_enabled: bool, - cwd_serialized: bool, - hover: Option, - hover_tooltip_update: Task<()>, - workspace_id: Option, - show_breadcrumbs: bool, - block_below_cursor: Option>, - scroll_top: Pixels, - scroll_handle: TerminalScrollHandle, - ime_state: Option, - _subscriptions: Vec, - _terminal_subscriptions: Vec, -} - -#[derive(Default, Clone)] -pub enum TerminalMode { - #[default] - Standalone, - Embedded { - max_lines_when_unfocused: Option, - }, -} - -#[derive(Clone)] -pub enum ContentMode { - Scrollable, - Inline { - displayed_lines: usize, - total_lines: usize, - }, -} - -impl ContentMode { - pub fn is_limited(&self) -> bool { - match self { - ContentMode::Scrollable => false, - ContentMode::Inline { - displayed_lines, - total_lines, - } => displayed_lines < total_lines, - } - } - - pub fn is_scrollable(&self) -> bool { - matches!(self, ContentMode::Scrollable) - } -} - -#[derive(Debug)] -#[cfg_attr(test, derive(Clone, Eq, PartialEq))] -struct HoverTarget { - tooltip: String, - hovered_word: HoveredWord, -} - -impl EventEmitter for TerminalView {} -impl EventEmitter for TerminalView {} -impl EventEmitter for TerminalView {} - -impl Focusable for TerminalView { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl TerminalView { - ///Create a new Terminal in the current working directory or the user's home directory - pub fn deploy( - workspace: &mut Workspace, - _: &NewCenterTerminal, - window: &mut Window, - cx: &mut Context, - ) { - let working_directory = default_working_directory(workspace, cx); - TerminalPanel::add_center_terminal(workspace, window, cx, |project, cx| { - project.create_terminal_shell(working_directory, cx) - }) - .detach_and_log_err(cx); - } - - pub fn new( - terminal: Entity, - workspace: WeakEntity, - workspace_id: Option, - project: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let workspace_handle = workspace.clone(); - let terminal_subscriptions = - subscribe_for_terminal_events(&terminal, workspace, window, cx); - - let focus_handle = cx.focus_handle(); - let focus_in = cx.on_focus_in(&focus_handle, window, |terminal_view, window, cx| { - terminal_view.focus_in(window, cx); - }); - let focus_out = cx.on_focus_out( - &focus_handle, - window, - |terminal_view, _event, window, cx| { - terminal_view.focus_out(window, cx); - }, - ); - let cursor_shape = TerminalSettings::get_global(cx).cursor_shape; - - let scroll_handle = TerminalScrollHandle::new(terminal.read(cx)); - - let blink_manager = cx.new(|cx| { - BlinkManager::new( - CURSOR_BLINK_INTERVAL, - |cx| { - !matches!( - TerminalSettings::get_global(cx).blinking, - TerminalBlink::Off - ) - }, - cx, - ) - }); - - let _subscriptions = vec![ - focus_in, - focus_out, - cx.observe(&blink_manager, |_, _, cx| cx.notify()), - cx.observe_global::(Self::settings_changed), - ]; - Self { - terminal, - workspace: workspace_handle, - project, - has_bell: false, - focus_handle, - context_menu: None, - cursor_shape, - blink_manager, - blinking_terminal_enabled: false, - hover: None, - hover_tooltip_update: Task::ready(()), - mode: TerminalMode::Standalone, - workspace_id, - show_breadcrumbs: TerminalSettings::get_global(cx).toolbar.breadcrumbs, - block_below_cursor: None, - scroll_top: Pixels::ZERO, - scroll_handle, - cwd_serialized: false, - ime_state: None, - _subscriptions, - _terminal_subscriptions: terminal_subscriptions, - } - } - - /// Enable 'embedded' mode where the terminal displays the full content with an optional limit of lines. - pub fn set_embedded_mode( - &mut self, - max_lines_when_unfocused: Option, - cx: &mut Context, - ) { - self.mode = TerminalMode::Embedded { - max_lines_when_unfocused, - }; - cx.notify(); - } - - const MAX_EMBEDDED_LINES: usize = 1_000; - - /// Returns the current `ContentMode` depending on the set `TerminalMode` and the current number of lines - /// - /// Note: Even in embedded mode, the terminal will fallback to scrollable when its content exceeds `MAX_EMBEDDED_LINES` - pub fn content_mode(&self, window: &Window, cx: &App) -> ContentMode { - match &self.mode { - TerminalMode::Standalone => ContentMode::Scrollable, - TerminalMode::Embedded { - max_lines_when_unfocused, - } => { - let total_lines = self.terminal.read(cx).total_lines(); - - if total_lines > Self::MAX_EMBEDDED_LINES { - ContentMode::Scrollable - } else { - let mut displayed_lines = total_lines; - - if !self.focus_handle.is_focused(window) - && let Some(max_lines) = max_lines_when_unfocused - { - displayed_lines = displayed_lines.min(*max_lines) - } - - ContentMode::Inline { - displayed_lines, - total_lines, - } - } - } - } - } - - /// Sets the marked (pre-edit) text from the IME. - pub(crate) fn set_marked_text( - &mut self, - text: String, - range: Option>, - cx: &mut Context, - ) { - self.ime_state = Some(ImeState { - marked_text: text, - marked_range_utf16: range, - }); - cx.notify(); - } - - /// Gets the current marked range (UTF-16). - pub(crate) fn marked_text_range(&self) -> Option> { - self.ime_state - .as_ref() - .and_then(|state| state.marked_range_utf16.clone()) - } - - /// Clears the marked (pre-edit) text state. - pub(crate) fn clear_marked_text(&mut self, cx: &mut Context) { - if self.ime_state.is_some() { - self.ime_state = None; - cx.notify(); - } - } - - /// Commits (sends) the given text to the PTY. Called by InputHandler::replace_text_in_range. - pub(crate) fn commit_text(&mut self, text: &str, cx: &mut Context) { - if !text.is_empty() { - self.terminal.update(cx, |term, _| { - term.input(text.to_string().into_bytes()); - }); - } - } - - pub(crate) fn terminal_bounds(&self, cx: &App) -> TerminalBounds { - self.terminal.read(cx).last_content().terminal_bounds - } - - pub fn entity(&self) -> &Entity { - &self.terminal - } - - pub fn has_bell(&self) -> bool { - self.has_bell - } - - pub fn clear_bell(&mut self, cx: &mut Context) { - self.has_bell = false; - cx.emit(Event::Wakeup); - } - - pub fn deploy_context_menu( - &mut self, - position: gpui::Point, - window: &mut Window, - cx: &mut Context, - ) { - let assistant_enabled = self - .workspace - .upgrade() - .and_then(|workspace| workspace.read(cx).panel::(cx)) - .is_some_and(|terminal_panel| terminal_panel.read(cx).assistant_enabled()); - let context_menu = ContextMenu::build(window, cx, |menu, _, _| { - menu.context(self.focus_handle.clone()) - .action("New Terminal", Box::new(NewTerminal)) - .separator() - .action("Copy", Box::new(Copy)) - .action("Paste", Box::new(Paste)) - .action("Select All", Box::new(SelectAll)) - .action("Clear", Box::new(Clear)) - .when(assistant_enabled, |menu| { - menu.separator() - .action("Inline Assist", Box::new(InlineAssist::default())) - }) - .separator() - .action( - "Close Terminal Tab", - Box::new(CloseActiveItem { - save_intent: None, - close_pinned: true, - }), - ) - }); - - window.focus(&context_menu.focus_handle(cx)); - let subscription = cx.subscribe_in( - &context_menu, - window, - |this, _, _: &DismissEvent, window, cx| { - if this.context_menu.as_ref().is_some_and(|context_menu| { - context_menu.0.focus_handle(cx).contains_focused(window, cx) - }) { - cx.focus_self(window); - } - this.context_menu.take(); - cx.notify(); - }, - ); - - self.context_menu = Some((context_menu, position, subscription)); - } - - fn settings_changed(&mut self, cx: &mut Context) { - let settings = TerminalSettings::get_global(cx); - let breadcrumb_visibility_changed = self.show_breadcrumbs != settings.toolbar.breadcrumbs; - self.show_breadcrumbs = settings.toolbar.breadcrumbs; - - let should_blink = match settings.blinking { - TerminalBlink::Off => false, - TerminalBlink::On => true, - TerminalBlink::TerminalControlled => self.blinking_terminal_enabled, - }; - let new_cursor_shape = settings.cursor_shape; - let old_cursor_shape = self.cursor_shape; - if old_cursor_shape != new_cursor_shape { - self.cursor_shape = new_cursor_shape; - self.terminal.update(cx, |term, _| { - term.set_cursor_shape(self.cursor_shape); - }); - } - - self.blink_manager.update( - cx, - if should_blink { - BlinkManager::enable - } else { - BlinkManager::disable - }, - ); - - if breadcrumb_visibility_changed { - cx.emit(ItemEvent::UpdateBreadcrumbs); - } - cx.notify(); - } - - fn show_character_palette( - &mut self, - _: &ShowCharacterPalette, - window: &mut Window, - cx: &mut Context, - ) { - if self - .terminal - .read(cx) - .last_content - .mode - .contains(TermMode::ALT_SCREEN) - { - self.terminal.update(cx, |term, cx| { - term.try_keystroke( - &Keystroke::parse("ctrl-cmd-space").unwrap(), - TerminalSettings::get_global(cx).option_as_meta, - ) - }); - } else { - window.show_character_palette(); - } - } - - fn select_all(&mut self, _: &SelectAll, _: &mut Window, cx: &mut Context) { - self.terminal.update(cx, |term, _| term.select_all()); - cx.notify(); - } - - fn rerun_task(&mut self, _: &RerunTask, window: &mut Window, cx: &mut Context) { - let task = self - .terminal - .read(cx) - .task() - .map(|task| terminal_rerun_override(&task.spawned_task.id)) - .unwrap_or_default(); - window.dispatch_action(Box::new(task), cx); - } - - fn clear(&mut self, _: &Clear, _: &mut Window, cx: &mut Context) { - self.scroll_top = px(0.); - self.terminal.update(cx, |term, _| term.clear()); - cx.notify(); - } - - fn max_scroll_top(&self, cx: &App) -> Pixels { - let terminal = self.terminal.read(cx); - - let Some(block) = self.block_below_cursor.as_ref() else { - return Pixels::ZERO; - }; - - let line_height = terminal.last_content().terminal_bounds.line_height; - let viewport_lines = terminal.viewport_lines(); - let cursor = point_to_viewport( - terminal.last_content.display_offset, - terminal.last_content.cursor.point, - ) - .unwrap_or_default(); - let max_scroll_top_in_lines = - (block.height as usize).saturating_sub(viewport_lines.saturating_sub(cursor.line + 1)); - - max_scroll_top_in_lines as f32 * line_height - } - - fn scroll_wheel(&mut self, event: &ScrollWheelEvent, cx: &mut Context) { - let terminal_content = self.terminal.read(cx).last_content(); - - if self.block_below_cursor.is_some() && terminal_content.display_offset == 0 { - let line_height = terminal_content.terminal_bounds.line_height; - let y_delta = event.delta.pixel_delta(line_height).y; - if y_delta < Pixels::ZERO || self.scroll_top > Pixels::ZERO { - self.scroll_top = cmp::max( - Pixels::ZERO, - cmp::min(self.scroll_top - y_delta, self.max_scroll_top(cx)), - ); - cx.notify(); - return; - } - } - self.terminal.update(cx, |term, cx| { - term.scroll_wheel( - event, - TerminalSettings::get_global(cx).scroll_multiplier.max(0.01), - ) - }); - } - - fn scroll_line_up(&mut self, _: &ScrollLineUp, _: &mut Window, cx: &mut Context) { - let terminal_content = self.terminal.read(cx).last_content(); - if self.block_below_cursor.is_some() - && terminal_content.display_offset == 0 - && self.scroll_top > Pixels::ZERO - { - let line_height = terminal_content.terminal_bounds.line_height; - self.scroll_top = cmp::max(self.scroll_top - line_height, Pixels::ZERO); - return; - } - - self.terminal.update(cx, |term, _| term.scroll_line_up()); - cx.notify(); - } - - fn scroll_line_down(&mut self, _: &ScrollLineDown, _: &mut Window, cx: &mut Context) { - let terminal_content = self.terminal.read(cx).last_content(); - if self.block_below_cursor.is_some() && terminal_content.display_offset == 0 { - let max_scroll_top = self.max_scroll_top(cx); - if self.scroll_top < max_scroll_top { - let line_height = terminal_content.terminal_bounds.line_height; - self.scroll_top = cmp::min(self.scroll_top + line_height, max_scroll_top); - } - return; - } - - self.terminal.update(cx, |term, _| term.scroll_line_down()); - cx.notify(); - } - - fn scroll_page_up(&mut self, _: &ScrollPageUp, _: &mut Window, cx: &mut Context) { - if self.scroll_top == Pixels::ZERO { - self.terminal.update(cx, |term, _| term.scroll_page_up()); - } else { - let line_height = self - .terminal - .read(cx) - .last_content - .terminal_bounds - .line_height(); - let visible_block_lines = (self.scroll_top / line_height) as usize; - let viewport_lines = self.terminal.read(cx).viewport_lines(); - let visible_content_lines = viewport_lines - visible_block_lines; - - if visible_block_lines >= viewport_lines { - self.scroll_top = ((visible_block_lines - viewport_lines) as f32) * line_height; - } else { - self.scroll_top = px(0.); - self.terminal - .update(cx, |term, _| term.scroll_up_by(visible_content_lines)); - } - } - cx.notify(); - } - - fn scroll_page_down(&mut self, _: &ScrollPageDown, _: &mut Window, cx: &mut Context) { - self.terminal.update(cx, |term, _| term.scroll_page_down()); - let terminal = self.terminal.read(cx); - if terminal.last_content().display_offset < terminal.viewport_lines() { - self.scroll_top = self.max_scroll_top(cx); - } - cx.notify(); - } - - fn scroll_to_top(&mut self, _: &ScrollToTop, _: &mut Window, cx: &mut Context) { - self.terminal.update(cx, |term, _| term.scroll_to_top()); - cx.notify(); - } - - fn scroll_to_bottom(&mut self, _: &ScrollToBottom, _: &mut Window, cx: &mut Context) { - self.terminal.update(cx, |term, _| term.scroll_to_bottom()); - if self.block_below_cursor.is_some() { - self.scroll_top = self.max_scroll_top(cx); - } - cx.notify(); - } - - fn toggle_vi_mode(&mut self, _: &ToggleViMode, _: &mut Window, cx: &mut Context) { - self.terminal.update(cx, |term, _| term.toggle_vi_mode()); - cx.notify(); - } - - pub fn should_show_cursor(&self, focused: bool, cx: &mut Context) -> bool { - // Always show cursor when not focused or in special modes - if !focused - || self - .terminal - .read(cx) - .last_content - .mode - .contains(TermMode::ALT_SCREEN) - { - return true; - } - - // When focused, check blinking settings and blink manager state - match TerminalSettings::get_global(cx).blinking { - TerminalBlink::Off => true, - TerminalBlink::TerminalControlled => { - !self.blinking_terminal_enabled || self.blink_manager.read(cx).visible() - } - TerminalBlink::On => self.blink_manager.read(cx).visible(), - } - } - - pub fn pause_cursor_blinking(&mut self, _window: &mut Window, cx: &mut Context) { - self.blink_manager.update(cx, BlinkManager::pause_blinking); - } - - pub fn terminal(&self) -> &Entity { - &self.terminal - } - - pub fn set_block_below_cursor( - &mut self, - block: BlockProperties, - window: &mut Window, - cx: &mut Context, - ) { - self.block_below_cursor = Some(Rc::new(block)); - self.scroll_to_bottom(&ScrollToBottom, window, cx); - cx.notify(); - } - - pub fn clear_block_below_cursor(&mut self, cx: &mut Context) { - self.block_below_cursor = None; - self.scroll_top = Pixels::ZERO; - cx.notify(); - } - - ///Attempt to paste the clipboard into the terminal - fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context) { - self.terminal.update(cx, |term, _| term.copy(None)); - cx.notify(); - } - - ///Attempt to paste the clipboard into the terminal - fn paste(&mut self, _: &Paste, _: &mut Window, cx: &mut Context) { - if let Some(clipboard_string) = cx.read_from_clipboard().and_then(|item| item.text()) { - self.terminal - .update(cx, |terminal, _cx| terminal.paste(&clipboard_string)); - } - } - - fn send_text(&mut self, text: &SendText, _: &mut Window, cx: &mut Context) { - self.clear_bell(cx); - self.terminal.update(cx, |term, _| { - term.input(text.0.to_string().into_bytes()); - }); - } - - fn send_keystroke(&mut self, text: &SendKeystroke, _: &mut Window, cx: &mut Context) { - if let Some(keystroke) = Keystroke::parse(&text.0).log_err() { - self.clear_bell(cx); - self.terminal.update(cx, |term, cx| { - let processed = - term.try_keystroke(&keystroke, TerminalSettings::get_global(cx).option_as_meta); - if processed && term.vi_mode_enabled() { - cx.notify(); - } - processed - }); - } - } - - fn dispatch_context(&self, cx: &App) -> KeyContext { - let mut dispatch_context = KeyContext::new_with_defaults(); - dispatch_context.add("Terminal"); - - if self.terminal.read(cx).vi_mode_enabled() { - dispatch_context.add("vi_mode"); - } - - let mode = self.terminal.read(cx).last_content.mode; - dispatch_context.set( - "screen", - if mode.contains(TermMode::ALT_SCREEN) { - "alt" - } else { - "normal" - }, - ); - - if mode.contains(TermMode::APP_CURSOR) { - dispatch_context.add("DECCKM"); - } - if mode.contains(TermMode::APP_KEYPAD) { - dispatch_context.add("DECPAM"); - } else { - dispatch_context.add("DECPNM"); - } - if mode.contains(TermMode::SHOW_CURSOR) { - dispatch_context.add("DECTCEM"); - } - if mode.contains(TermMode::LINE_WRAP) { - dispatch_context.add("DECAWM"); - } - if mode.contains(TermMode::ORIGIN) { - dispatch_context.add("DECOM"); - } - if mode.contains(TermMode::INSERT) { - dispatch_context.add("IRM"); - } - //LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html - if mode.contains(TermMode::LINE_FEED_NEW_LINE) { - dispatch_context.add("LNM"); - } - if mode.contains(TermMode::FOCUS_IN_OUT) { - dispatch_context.add("report_focus"); - } - if mode.contains(TermMode::ALTERNATE_SCROLL) { - dispatch_context.add("alternate_scroll"); - } - if mode.contains(TermMode::BRACKETED_PASTE) { - dispatch_context.add("bracketed_paste"); - } - if mode.intersects(TermMode::MOUSE_MODE) { - dispatch_context.add("any_mouse_reporting"); - } - { - let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) { - "click" - } else if mode.contains(TermMode::MOUSE_DRAG) { - "drag" - } else if mode.contains(TermMode::MOUSE_MOTION) { - "motion" - } else { - "off" - }; - dispatch_context.set("mouse_reporting", mouse_reporting); - } - { - let format = if mode.contains(TermMode::SGR_MOUSE) { - "sgr" - } else if mode.contains(TermMode::UTF8_MOUSE) { - "utf8" - } else { - "normal" - }; - dispatch_context.set("mouse_format", format); - }; - - if self.terminal.read(cx).last_content.selection.is_some() { - dispatch_context.add("selection"); - } - - dispatch_context - } - - fn set_terminal( - &mut self, - terminal: Entity, - window: &mut Window, - cx: &mut Context, - ) { - self._terminal_subscriptions = - subscribe_for_terminal_events(&terminal, self.workspace.clone(), window, cx); - self.terminal = terminal; - } - - fn rerun_button(task: &TaskState) -> Option { - if !task.spawned_task.show_rerun { - return None; - } - - let task_id = task.spawned_task.id.clone(); - Some( - IconButton::new("rerun-icon", IconName::Rerun) - .icon_size(IconSize::Small) - .size(ButtonSize::Compact) - .icon_color(Color::Default) - .shape(ui::IconButtonShape::Square) - .tooltip(move |_window, cx| Tooltip::for_action("Rerun task", &RerunTask, cx)) - .on_click(move |_, window, cx| { - window.dispatch_action(Box::new(terminal_rerun_override(&task_id)), cx); - }), - ) - } -} - -fn terminal_rerun_override(task: &TaskId) -> zed_actions::Rerun { - zed_actions::Rerun { - task_id: Some(task.0.clone()), - allow_concurrent_runs: Some(true), - use_new_terminal: Some(false), - reevaluate_context: false, - } -} - -fn subscribe_for_terminal_events( - terminal: &Entity, - workspace: WeakEntity, - window: &mut Window, - cx: &mut Context, -) -> Vec { - let terminal_subscription = cx.observe(terminal, |_, _, cx| cx.notify()); - let mut previous_cwd = None; - let terminal_events_subscription = cx.subscribe_in( - terminal, - window, - move |terminal_view, terminal, event, window, cx| { - let current_cwd = terminal.read(cx).working_directory(); - if current_cwd != previous_cwd { - previous_cwd = current_cwd; - terminal_view.cwd_serialized = false; - } - - match event { - Event::Wakeup => { - cx.notify(); - cx.emit(Event::Wakeup); - cx.emit(ItemEvent::UpdateTab); - cx.emit(SearchEvent::MatchesInvalidated); - } - - Event::Bell => { - terminal_view.has_bell = true; - cx.emit(Event::Wakeup); - } - - Event::BlinkChanged(blinking) => { - terminal_view.blinking_terminal_enabled = *blinking; - - // If in terminal-controlled mode and focused, update blink manager - if matches!( - TerminalSettings::get_global(cx).blinking, - TerminalBlink::TerminalControlled - ) && terminal_view.focus_handle.is_focused(window) - { - terminal_view.blink_manager.update(cx, |manager, cx| { - if *blinking { - manager.enable(cx); - } else { - manager.disable(cx); - } - }); - } - } - - Event::TitleChanged => { - cx.emit(ItemEvent::UpdateTab); - } - - Event::NewNavigationTarget(maybe_navigation_target) => { - match maybe_navigation_target - .as_ref() - .zip(terminal.read(cx).last_content.last_hovered_word.as_ref()) - { - Some((MaybeNavigationTarget::Url(url), hovered_word)) => { - if Some(hovered_word) - != terminal_view - .hover - .as_ref() - .map(|hover| &hover.hovered_word) - { - terminal_view.hover = Some(HoverTarget { - tooltip: url.clone(), - hovered_word: hovered_word.clone(), - }); - terminal_view.hover_tooltip_update = Task::ready(()); - cx.notify(); - } - } - Some((MaybeNavigationTarget::PathLike(path_like_target), hovered_word)) => { - if Some(hovered_word) - != terminal_view - .hover - .as_ref() - .map(|hover| &hover.hovered_word) - { - terminal_view.hover = None; - terminal_view.hover_tooltip_update = hover_path_like_target( - &workspace, - hovered_word.clone(), - path_like_target, - cx, - ); - cx.notify(); - } - } - None => { - terminal_view.hover = None; - terminal_view.hover_tooltip_update = Task::ready(()); - cx.notify(); - } - } - } - - 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, - ), - }, - Event::BreadcrumbsChanged => cx.emit(ItemEvent::UpdateBreadcrumbs), - Event::CloseTerminal => cx.emit(ItemEvent::CloseItem), - Event::SelectionsChanged => { - window.invalidate_character_coordinates(); - cx.emit(SearchEvent::ActiveMatchChanged) - } - } - }, - ); - vec![terminal_subscription, terminal_events_subscription] -} - -fn regex_search_for_query(query: &project::search::SearchQuery) -> Option { - let str = query.as_str(); - if query.is_regex() { - if str == "." { - return None; - } - RegexSearch::new(str).ok() - } else { - RegexSearch::new(®ex::escape(str)).ok() - } -} - -struct TerminalScrollbarSettingsWrapper; - -impl GlobalSetting for TerminalScrollbarSettingsWrapper { - fn get_value(_cx: &App) -> &Self { - &Self - } -} - -impl ScrollbarVisibility for TerminalScrollbarSettingsWrapper { - fn visibility(&self, cx: &App) -> scrollbars::ShowScrollbar { - TerminalSettings::get_global(cx) - .scrollbar - .show - .map(Into::into) - .unwrap_or_else(|| EditorSettings::get_global(cx).scrollbar.show) - } -} - -impl TerminalView { - fn key_down(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context) { - self.clear_bell(cx); - self.pause_cursor_blinking(window, cx); - - self.terminal.update(cx, |term, cx| { - let handled = term.try_keystroke( - &event.keystroke, - TerminalSettings::get_global(cx).option_as_meta, - ); - if handled { - cx.stop_propagation(); - } - }); - } - - fn focus_in(&mut self, window: &mut Window, cx: &mut Context) { - self.terminal.update(cx, |terminal, _| { - terminal.set_cursor_shape(self.cursor_shape); - terminal.focus_in(); - }); - - let should_blink = match TerminalSettings::get_global(cx).blinking { - TerminalBlink::Off => false, - TerminalBlink::On => true, - TerminalBlink::TerminalControlled => self.blinking_terminal_enabled, - }; - - if should_blink { - self.blink_manager.update(cx, BlinkManager::enable); - } - - window.invalidate_character_coordinates(); - cx.notify(); - } - - fn focus_out(&mut self, _window: &mut Window, cx: &mut Context) { - self.blink_manager.update(cx, BlinkManager::disable); - self.terminal.update(cx, |terminal, _| { - terminal.focus_out(); - terminal.set_cursor_shape(CursorShape::Hollow); - }); - cx.notify(); - } -} - -impl Render for TerminalView { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - // TODO: this should be moved out of render - self.scroll_handle.update(self.terminal.read(cx)); - - if let Some(new_display_offset) = self.scroll_handle.future_display_offset.take() { - self.terminal.update(cx, |term, _| { - let delta = new_display_offset as i32 - term.last_content.display_offset as i32; - match delta.cmp(&0) { - std::cmp::Ordering::Greater => term.scroll_up_by(delta as usize), - std::cmp::Ordering::Less => term.scroll_down_by(-delta as usize), - std::cmp::Ordering::Equal => {} - } - }); - } - - let terminal_handle = self.terminal.clone(); - let terminal_view_handle = cx.entity(); - - let focused = self.focus_handle.is_focused(window); - - div() - .id("terminal-view") - .size_full() - .relative() - .track_focus(&self.focus_handle(cx)) - .key_context(self.dispatch_context(cx)) - .on_action(cx.listener(TerminalView::send_text)) - .on_action(cx.listener(TerminalView::send_keystroke)) - .on_action(cx.listener(TerminalView::copy)) - .on_action(cx.listener(TerminalView::paste)) - .on_action(cx.listener(TerminalView::clear)) - .on_action(cx.listener(TerminalView::scroll_line_up)) - .on_action(cx.listener(TerminalView::scroll_line_down)) - .on_action(cx.listener(TerminalView::scroll_page_up)) - .on_action(cx.listener(TerminalView::scroll_page_down)) - .on_action(cx.listener(TerminalView::scroll_to_top)) - .on_action(cx.listener(TerminalView::scroll_to_bottom)) - .on_action(cx.listener(TerminalView::toggle_vi_mode)) - .on_action(cx.listener(TerminalView::show_character_palette)) - .on_action(cx.listener(TerminalView::select_all)) - .on_action(cx.listener(TerminalView::rerun_task)) - .on_key_down(cx.listener(Self::key_down)) - .on_mouse_down( - MouseButton::Right, - cx.listener(|this, event: &MouseDownEvent, window, cx| { - if !this.terminal.read(cx).mouse_mode(event.modifiers.shift) { - if this.terminal.read(cx).last_content.selection.is_none() { - this.terminal.update(cx, |terminal, _| { - terminal.select_word_at_event_position(event); - }); - }; - this.deploy_context_menu(event.position, window, cx); - cx.notify(); - } - }), - ) - .child( - // TODO: Oddly this wrapper div is needed for TerminalElement to not steal events from the context menu - div() - .id("terminal-view-container") - .size_full() - .bg(cx.theme().colors().editor_background) - .child(TerminalElement::new( - terminal_handle, - terminal_view_handle, - self.workspace.clone(), - self.focus_handle.clone(), - focused, - self.should_show_cursor(focused, cx), - self.block_below_cursor.clone(), - self.mode.clone(), - )) - .when(self.content_mode(window, cx).is_scrollable(), |div| { - div.custom_scrollbars( - Scrollbars::for_settings::() - .show_along(ScrollAxes::Vertical) - .with_track_along( - ScrollAxes::Vertical, - cx.theme().colors().editor_background, - ) - .tracked_scroll_handle(&self.scroll_handle), - window, - cx, - ) - }), - ) - .children(self.context_menu.as_ref().map(|(menu, position, _)| { - deferred( - anchored() - .position(*position) - .anchor(gpui::Corner::TopLeft) - .child(menu.clone()), - ) - .with_priority(1) - })) - } -} - -impl Item for TerminalView { - type Event = ItemEvent; - - fn tab_tooltip_content(&self, cx: &App) -> Option { - Some(TabTooltipContent::Custom(Box::new(Tooltip::element({ - let terminal = self.terminal().read(cx); - let title = terminal.title(false); - let pid = terminal.pid_getter()?.fallback_pid(); - - move |_, _| { - v_flex() - .gap_1() - .child(Label::new(title.clone())) - .child(h_flex().flex_grow().child(Divider::horizontal())) - .child( - Label::new(format!("Process ID (PID): {}", pid)) - .color(Color::Muted) - .size(LabelSize::Small), - ) - .into_any_element() - } - })))) - } - - fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement { - let terminal = self.terminal().read(cx); - let title = terminal.title(true); - - let (icon, icon_color, rerun_button) = match terminal.task() { - Some(terminal_task) => match &terminal_task.status { - TaskStatus::Running => ( - IconName::PlayFilled, - Color::Disabled, - TerminalView::rerun_button(terminal_task), - ), - TaskStatus::Unknown => ( - IconName::Warning, - Color::Warning, - TerminalView::rerun_button(terminal_task), - ), - TaskStatus::Completed { success } => { - let rerun_button = TerminalView::rerun_button(terminal_task); - - if *success { - (IconName::Check, Color::Success, rerun_button) - } else { - (IconName::XCircle, Color::Error, rerun_button) - } - } - }, - None => (IconName::Terminal, Color::Muted, None), - }; - - h_flex() - .gap_1() - .group("term-tab-icon") - .child( - h_flex() - .group("term-tab-icon") - .child( - div() - .when(rerun_button.is_some(), |this| { - this.hover(|style| style.invisible().w_0()) - }) - .child(Icon::new(icon).color(icon_color)), - ) - .when_some(rerun_button, |this, rerun_button| { - this.child( - div() - .absolute() - .visible_on_hover("term-tab-icon") - .child(rerun_button), - ) - }), - ) - .child(Label::new(title).color(params.text_color())) - .into_any() - } - - fn tab_content_text(&self, detail: usize, cx: &App) -> SharedString { - let terminal = self.terminal().read(cx); - terminal.title(detail == 0).into() - } - - fn telemetry_event_text(&self) -> Option<&'static str> { - None - } - - fn buffer_kind(&self, _: &App) -> workspace::item::ItemBufferKind { - workspace::item::ItemBufferKind::Singleton - } - - fn can_split(&self) -> bool { - true - } - - fn clone_on_split( - &self, - workspace_id: Option, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - let Ok(terminal) = self.project.update(cx, |project, cx| { - let cwd = project - .active_project_directory(cx) - .map(|it| it.to_path_buf()); - project.clone_terminal(self.terminal(), cx, cwd) - }) else { - return Task::ready(None); - }; - cx.spawn_in(window, async move |this, cx| { - let terminal = terminal.await.log_err()?; - this.update_in(cx, |this, window, cx| { - cx.new(|cx| { - TerminalView::new( - terminal, - this.workspace.clone(), - workspace_id, - this.project.clone(), - window, - cx, - ) - }) - }) - .ok() - }) - } - - fn is_dirty(&self, cx: &gpui::App) -> bool { - match self.terminal.read(cx).task() { - Some(task) => task.status == TaskStatus::Running, - None => self.has_bell(), - } - } - - fn has_conflict(&self, _cx: &App) -> bool { - false - } - - fn can_save_as(&self, _cx: &App) -> bool { - false - } - - fn as_searchable( - &self, - handle: &Entity, - _: &App, - ) -> Option> { - Some(Box::new(handle.clone())) - } - - fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation { - if self.show_breadcrumbs && !self.terminal().read(cx).breadcrumb_text.trim().is_empty() { - ToolbarItemLocation::PrimaryLeft - } else { - ToolbarItemLocation::Hidden - } - } - - fn breadcrumbs(&self, _: &theme::Theme, cx: &App) -> Option> { - Some(vec![BreadcrumbText { - text: self.terminal().read(cx).breadcrumb_text.clone(), - highlights: None, - font: None, - }]) - } - - fn added_to_workspace( - &mut self, - workspace: &mut Workspace, - _: &mut Window, - cx: &mut Context, - ) { - if self.terminal().read(cx).task().is_none() { - if let Some((new_id, old_id)) = workspace.database_id().zip(self.workspace_id) { - log::debug!( - "Updating workspace id for the terminal, old: {old_id:?}, new: {new_id:?}", - ); - cx.background_spawn(TERMINAL_DB.update_workspace_id( - new_id, - old_id, - cx.entity_id().as_u64(), - )) - .detach(); - } - self.workspace_id = workspace.database_id(); - } - } - - fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { - f(*event) - } -} - -impl SerializableItem for TerminalView { - fn serialized_item_kind() -> &'static str { - "Terminal" - } - - fn cleanup( - workspace_id: WorkspaceId, - alive_items: Vec, - _window: &mut Window, - cx: &mut App, - ) -> Task> { - delete_unloaded_items(alive_items, workspace_id, "terminals", &TERMINAL_DB, cx) - } - - fn serialize( - &mut self, - _workspace: &mut Workspace, - item_id: workspace::ItemId, - _closing: bool, - _: &mut Window, - cx: &mut Context, - ) -> Option>> { - let terminal = self.terminal().read(cx); - if terminal.task().is_some() { - return None; - } - - if let Some((cwd, workspace_id)) = terminal.working_directory().zip(self.workspace_id) { - self.cwd_serialized = true; - Some(cx.background_spawn(async move { - TERMINAL_DB - .save_working_directory(item_id, workspace_id, cwd) - .await - })) - } else { - None - } - } - - fn should_serialize(&self, _: &Self::Event) -> bool { - !self.cwd_serialized - } - - fn deserialize( - project: Entity, - workspace: WeakEntity, - workspace_id: workspace::WorkspaceId, - item_id: workspace::ItemId, - window: &mut Window, - cx: &mut App, - ) -> Task>> { - window.spawn(cx, async move |cx| { - let cwd = cx - .update(|_window, cx| { - let from_db = TERMINAL_DB - .get_working_directory(item_id, workspace_id) - .log_err() - .flatten(); - if from_db - .as_ref() - .is_some_and(|from_db| !from_db.as_os_str().is_empty()) - { - from_db - } else { - workspace - .upgrade() - .and_then(|workspace| default_working_directory(workspace.read(cx), cx)) - } - }) - .ok() - .flatten(); - - let terminal = project - .update(cx, |project, cx| project.create_terminal_shell(cwd, cx))? - .await?; - cx.update(|window, cx| { - cx.new(|cx| { - TerminalView::new( - terminal, - workspace, - Some(workspace_id), - project.downgrade(), - window, - cx, - ) - }) - }) - }) - } -} - -impl SearchableItem for TerminalView { - type Match = RangeInclusive; - - fn supported_options(&self) -> SearchOptions { - SearchOptions { - case: false, - word: false, - regex: true, - replacement: false, - selection: false, - find_in_results: false, - } - } - - /// Clear stored matches - fn clear_matches(&mut self, _window: &mut Window, cx: &mut Context) { - self.terminal().update(cx, |term, _| term.matches.clear()) - } - - /// Store matches returned from find_matches somewhere for rendering - fn update_matches( - &mut self, - matches: &[Self::Match], - _active_match_index: Option, - _window: &mut Window, - cx: &mut Context, - ) { - self.terminal() - .update(cx, |term, _| term.matches = matches.to_vec()) - } - - /// Returns the selection content to pre-load into this search - fn query_suggestion(&mut self, _window: &mut Window, cx: &mut Context) -> String { - self.terminal() - .read(cx) - .last_content - .selection_text - .clone() - .unwrap_or_default() - } - - /// Focus match at given index into the Vec of matches - fn activate_match( - &mut self, - index: usize, - _: &[Self::Match], - _window: &mut Window, - cx: &mut Context, - ) { - self.terminal() - .update(cx, |term, _| term.activate_match(index)); - cx.notify(); - } - - /// Add selections for all matches given. - fn select_matches(&mut self, matches: &[Self::Match], _: &mut Window, cx: &mut Context) { - self.terminal() - .update(cx, |term, _| term.select_matches(matches)); - cx.notify(); - } - - /// Get all of the matches for this query, should be done on the background - fn find_matches( - &mut self, - query: Arc, - _: &mut Window, - cx: &mut Context, - ) -> Task> { - if let Some(s) = regex_search_for_query(&query) { - self.terminal() - .update(cx, |term, cx| term.find_matches(s, cx)) - } else { - Task::ready(vec![]) - } - } - - /// Reports back to the search toolbar what the active match should be (the selection) - fn active_match_index( - &mut self, - direction: Direction, - matches: &[Self::Match], - _: &mut Window, - cx: &mut Context, - ) -> Option { - // Selection head might have a value if there's a selection that isn't - // associated with a match. Therefore, if there are no matches, we should - // report None, no matter the state of the terminal - - if !matches.is_empty() { - if let Some(selection_head) = self.terminal().read(cx).selection_head { - // If selection head is contained in a match. Return that match - match direction { - Direction::Prev => { - // If no selection before selection head, return the first match - Some( - matches - .iter() - .enumerate() - .rev() - .find(|(_, search_match)| { - search_match.contains(&selection_head) - || search_match.start() < &selection_head - }) - .map(|(ix, _)| ix) - .unwrap_or(0), - ) - } - Direction::Next => { - // If no selection after selection head, return the last match - Some( - matches - .iter() - .enumerate() - .find(|(_, search_match)| { - search_match.contains(&selection_head) - || search_match.start() > &selection_head - }) - .map(|(ix, _)| ix) - .unwrap_or(matches.len().saturating_sub(1)), - ) - } - } - } else { - // Matches found but no active selection, return the first last one (closest to cursor) - Some(matches.len().saturating_sub(1)) - } - } else { - None - } - } - fn replace( - &mut self, - _: &Self::Match, - _: &SearchQuery, - _window: &mut Window, - _: &mut Context, - ) { - // Replacement is not supported in terminal view, so this is a no-op. - } -} - -///Gets the working directory for the given workspace, respecting the user's settings. -/// None implies "~" on whichever machine we end up on. -pub(crate) fn default_working_directory(workspace: &Workspace, cx: &App) -> Option { - match &TerminalSettings::get_global(cx).working_directory { - WorkingDirectory::CurrentProjectDirectory => workspace - .project() - .read(cx) - .active_project_directory(cx) - .as_deref() - .map(Path::to_path_buf) - .or_else(|| first_project_directory(workspace, cx)), - WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx), - WorkingDirectory::AlwaysHome => None, - WorkingDirectory::Always { directory } => { - shellexpand::full(&directory) //TODO handle this better - .ok() - .map(|dir| Path::new(&dir.to_string()).to_path_buf()) - .filter(|dir| dir.is_dir()) - } - } -} -///Gets the first project's home directory, or the home directory -fn first_project_directory(workspace: &Workspace, cx: &App) -> Option { - let worktree = workspace.worktrees(cx).next()?.read(cx); - let worktree_path = worktree.abs_path(); - if worktree.root_entry()?.is_dir() { - Some(worktree_path.to_path_buf()) - } else { - // If worktree is a file, return its parent directory - worktree_path.parent().map(|p| p.to_path_buf()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::TestAppContext; - use project::{Entry, Project, ProjectPath, Worktree}; - use std::path::Path; - use util::rel_path::RelPath; - use workspace::AppState; - - // Working directory calculation tests - - // No Worktrees in project -> home_dir() - #[gpui::test] - async fn no_worktree(cx: &mut TestAppContext) { - let (project, workspace) = init_test(cx).await; - cx.read(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - //Make sure environment is as expected - assert!(active_entry.is_none()); - assert!(workspace.worktrees(cx).next().is_none()); - - let res = default_working_directory(workspace, cx); - assert_eq!(res, None); - let res = first_project_directory(workspace, cx); - assert_eq!(res, None); - }); - } - - // No active entry, but a worktree, worktree is a file -> parent directory - #[gpui::test] - async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) { - let (project, workspace) = init_test(cx).await; - - create_file_wt(project.clone(), "/root.txt", cx).await; - cx.read(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - //Make sure environment is as expected - assert!(active_entry.is_none()); - assert!(workspace.worktrees(cx).next().is_some()); - - let res = default_working_directory(workspace, cx); - assert_eq!(res, Some(Path::new("/").to_path_buf())); - let res = first_project_directory(workspace, cx); - assert_eq!(res, Some(Path::new("/").to_path_buf())); - }); - } - - // No active entry, but a worktree, worktree is a folder -> worktree_folder - #[gpui::test] - async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) { - let (project, workspace) = init_test(cx).await; - - let (_wt, _entry) = create_folder_wt(project.clone(), "/root/", cx).await; - cx.update(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - assert!(active_entry.is_none()); - assert!(workspace.worktrees(cx).next().is_some()); - - let res = default_working_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root/")).to_path_buf())); - let res = first_project_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root/")).to_path_buf())); - }); - } - - // Active entry with a work tree, worktree is a file -> worktree_folder() - #[gpui::test] - async fn active_entry_worktree_is_file(cx: &mut TestAppContext) { - let (project, workspace) = init_test(cx).await; - - let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await; - let (wt2, entry2) = create_file_wt(project.clone(), "/root2.txt", cx).await; - insert_active_entry_for(wt2, entry2, project.clone(), cx); - - cx.update(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - assert!(active_entry.is_some()); - - let res = default_working_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root1/")).to_path_buf())); - let res = first_project_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root1/")).to_path_buf())); - }); - } - - // Active entry, with a worktree, worktree is a folder -> worktree_folder - #[gpui::test] - async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) { - let (project, workspace) = init_test(cx).await; - - let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await; - let (wt2, entry2) = create_folder_wt(project.clone(), "/root2/", cx).await; - insert_active_entry_for(wt2, entry2, project.clone(), cx); - - cx.update(|cx| { - let workspace = workspace.read(cx); - let active_entry = project.read(cx).active_entry(); - - assert!(active_entry.is_some()); - - let res = default_working_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root2/")).to_path_buf())); - let res = first_project_directory(workspace, cx); - assert_eq!(res, Some((Path::new("/root1/")).to_path_buf())); - }); - } - - /// Creates a worktree with 1 file: /root.txt - pub async fn init_test(cx: &mut TestAppContext) -> (Entity, Entity) { - let params = cx.update(AppState::test); - cx.update(|cx| { - theme::init(theme::LoadThemes::JustBase, cx); - }); - - let project = Project::test(params.fs.clone(), [], cx).await; - let workspace = cx - .add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)) - .root(cx) - .unwrap(); - - (project, workspace) - } - - /// Creates a worktree with 1 folder: /root{suffix}/ - async fn create_folder_wt( - project: Entity, - path: impl AsRef, - cx: &mut TestAppContext, - ) -> (Entity, Entry) { - create_wt(project, true, path, cx).await - } - - /// Creates a worktree with 1 file: /root{suffix}.txt - async fn create_file_wt( - project: Entity, - path: impl AsRef, - cx: &mut TestAppContext, - ) -> (Entity, Entry) { - create_wt(project, false, path, cx).await - } - - async fn create_wt( - project: Entity, - is_dir: bool, - path: impl AsRef, - cx: &mut TestAppContext, - ) -> (Entity, Entry) { - let (wt, _) = project - .update(cx, |project, cx| { - project.find_or_create_worktree(path, true, cx) - }) - .await - .unwrap(); - - let entry = cx - .update(|cx| { - wt.update(cx, |wt, cx| { - wt.create_entry(RelPath::empty().into(), is_dir, None, cx) - }) - }) - .await - .unwrap() - .into_included() - .unwrap(); - - (wt, entry) - } - - pub fn insert_active_entry_for( - wt: Entity, - entry: Entry, - project: Entity, - cx: &mut TestAppContext, - ) { - cx.update(|cx| { - let p = ProjectPath { - worktree_id: wt.read(cx).id(), - path: entry.path, - }; - project.update(cx, |project, cx| project.set_active_path(Some(p), cx)); - }); - } -} diff --git a/crates/text/Cargo.toml b/crates/text/Cargo.toml deleted file mode 100644 index ed02381eb8..0000000000 --- a/crates/text/Cargo.toml +++ /dev/null @@ -1,39 +0,0 @@ -[package] -name = "text" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/text.rs" -doctest = false - -[features] -test-support = ["rand", "util/test-support"] - -[dependencies] -anyhow.workspace = true -clock.workspace = true -collections.workspace = true -log.workspace = true -parking_lot.workspace = true -postage.workspace = true -rand = { workspace = true, optional = true } -regex.workspace = true -rope.workspace = true -smallvec.workspace = true -sum_tree.workspace = true -util.workspace = true - -[dev-dependencies] -collections = { workspace = true, features = ["test-support"] } -ctor.workspace = true -gpui = { workspace = true, features = ["test-support"] } -rand.workspace = true -util = { workspace = true, features = ["test-support"] } -http_client = { workspace = true, features = ["test-support"] } -zlog.workspace = true diff --git a/crates/text/LICENSE-GPL b/crates/text/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/text/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs deleted file mode 100644 index bf660b1302..0000000000 --- a/crates/text/src/anchor.rs +++ /dev/null @@ -1,219 +0,0 @@ -use crate::{ - BufferId, BufferSnapshot, Point, PointUtf16, TextDimension, ToOffset, ToPoint, ToPointUtf16, - locator::Locator, -}; -use std::{cmp::Ordering, fmt::Debug, ops::Range}; -use sum_tree::{Bias, Dimensions}; - -/// A timestamped position in a buffer -#[derive(Copy, Clone, Eq, PartialEq, Hash)] -pub struct Anchor { - /// The timestamp of the operation that inserted the text - /// in which this anchor is located. - pub timestamp: clock::Lamport, - /// The byte offset into the text inserted in the operation - /// at `timestamp`. - pub offset: usize, - /// Whether this anchor stays attached to the character *before* or *after* - /// the offset. - pub bias: Bias, - pub buffer_id: Option, -} - -impl Debug for Anchor { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - if self.is_min() { - return write!(f, "Anchor::min({:?})", self.buffer_id); - } - if self.is_max() { - return write!(f, "Anchor::max({:?})", self.buffer_id); - } - - f.debug_struct("Anchor") - .field("timestamp", &self.timestamp) - .field("offset", &self.offset) - .field("bias", &self.bias) - .field("buffer_id", &self.buffer_id) - .finish() - } -} - -impl Anchor { - pub const MIN: Self = Self { - timestamp: clock::Lamport::MIN, - offset: usize::MIN, - bias: Bias::Left, - buffer_id: None, - }; - - pub const MAX: Self = Self { - timestamp: clock::Lamport::MAX, - offset: usize::MAX, - bias: Bias::Right, - buffer_id: None, - }; - - pub fn min_for_buffer(buffer_id: BufferId) -> Self { - Self { - timestamp: clock::Lamport::MIN, - offset: usize::MIN, - bias: Bias::Left, - buffer_id: Some(buffer_id), - } - } - - pub fn max_for_buffer(buffer_id: BufferId) -> Self { - Self { - timestamp: clock::Lamport::MAX, - offset: usize::MAX, - bias: Bias::Right, - buffer_id: Some(buffer_id), - } - } - - pub fn min_min_range_for_buffer(buffer_id: BufferId) -> std::ops::Range { - let min = Self::min_for_buffer(buffer_id); - min..min - } - pub fn max_max_range_for_buffer(buffer_id: BufferId) -> std::ops::Range { - let max = Self::max_for_buffer(buffer_id); - max..max - } - pub fn min_max_range_for_buffer(buffer_id: BufferId) -> std::ops::Range { - Self::min_for_buffer(buffer_id)..Self::max_for_buffer(buffer_id) - } - - pub fn cmp(&self, other: &Anchor, buffer: &BufferSnapshot) -> Ordering { - let fragment_id_comparison = if self.timestamp == other.timestamp { - Ordering::Equal - } else { - buffer - .fragment_id_for_anchor(self) - .cmp(buffer.fragment_id_for_anchor(other)) - }; - - fragment_id_comparison - .then_with(|| self.offset.cmp(&other.offset)) - .then_with(|| self.bias.cmp(&other.bias)) - } - - pub fn min<'a>(&'a self, other: &'a Self, buffer: &BufferSnapshot) -> &'a Self { - if self.cmp(other, buffer).is_le() { - self - } else { - other - } - } - - pub fn max<'a>(&'a self, other: &'a Self, buffer: &BufferSnapshot) -> &'a Self { - if self.cmp(other, buffer).is_ge() { - self - } else { - other - } - } - - pub fn bias(&self, bias: Bias, buffer: &BufferSnapshot) -> Anchor { - match bias { - Bias::Left => self.bias_left(buffer), - Bias::Right => self.bias_right(buffer), - } - } - - pub fn bias_left(&self, buffer: &BufferSnapshot) -> Anchor { - match self.bias { - Bias::Left => *self, - Bias::Right => buffer.anchor_before(self), - } - } - - pub fn bias_right(&self, buffer: &BufferSnapshot) -> Anchor { - match self.bias { - Bias::Left => buffer.anchor_after(self), - Bias::Right => *self, - } - } - - pub fn summary(&self, content: &BufferSnapshot) -> D - where - D: TextDimension, - { - content.summary_for_anchor(self) - } - - /// Returns true when the [`Anchor`] is located inside a visible fragment. - pub fn is_valid(&self, buffer: &BufferSnapshot) -> bool { - if self.is_min() || self.is_max() { - true - } else if self.buffer_id.is_none_or(|id| id != buffer.remote_id) { - false - } else { - let Some(fragment_id) = buffer.try_fragment_id_for_anchor(self) else { - return false; - }; - let (.., item) = buffer - .fragments - .find::, usize>, _>( - &None, - &Some(fragment_id), - Bias::Left, - ); - item.is_some_and(|fragment| fragment.visible) - } - } - - pub fn is_min(&self) -> bool { - self.timestamp == clock::Lamport::MIN - && self.offset == usize::MIN - && self.bias == Bias::Left - } - - pub fn is_max(&self) -> bool { - self.timestamp == clock::Lamport::MAX - && self.offset == usize::MAX - && self.bias == Bias::Right - } -} - -pub trait OffsetRangeExt { - fn to_offset(&self, snapshot: &BufferSnapshot) -> Range; - fn to_point(&self, snapshot: &BufferSnapshot) -> Range; - fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> Range; -} - -impl OffsetRangeExt for Range -where - T: ToOffset, -{ - fn to_offset(&self, snapshot: &BufferSnapshot) -> Range { - self.start.to_offset(snapshot)..self.end.to_offset(snapshot) - } - - fn to_point(&self, snapshot: &BufferSnapshot) -> Range { - self.start.to_offset(snapshot).to_point(snapshot) - ..self.end.to_offset(snapshot).to_point(snapshot) - } - - fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> Range { - self.start.to_offset(snapshot).to_point_utf16(snapshot) - ..self.end.to_offset(snapshot).to_point_utf16(snapshot) - } -} - -pub trait AnchorRangeExt { - fn cmp(&self, b: &Range, buffer: &BufferSnapshot) -> Ordering; - fn overlaps(&self, b: &Range, buffer: &BufferSnapshot) -> bool; -} - -impl AnchorRangeExt for Range { - fn cmp(&self, other: &Range, buffer: &BufferSnapshot) -> Ordering { - match self.start.cmp(&other.start, buffer) { - Ordering::Equal => other.end.cmp(&self.end, buffer), - ord => ord, - } - } - - fn overlaps(&self, other: &Range, buffer: &BufferSnapshot) -> bool { - self.start.cmp(&other.end, buffer).is_lt() && other.start.cmp(&self.end, buffer).is_lt() - } -} diff --git a/crates/text/src/locator.rs b/crates/text/src/locator.rs deleted file mode 100644 index cc94441a3d..0000000000 --- a/crates/text/src/locator.rs +++ /dev/null @@ -1,130 +0,0 @@ -use smallvec::SmallVec; -use std::iter; - -/// An identifier for a position in a ordered collection. -/// -/// Allows prepending and appending without needing to renumber existing locators -/// using `Locator::between(lhs, rhs)`. -/// -/// The initial location for a collection should be `Locator::between(Locator::min(), Locator::max())`, -/// leaving room for items to be inserted before and after it. -#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Locator(SmallVec<[u64; 4]>); - -impl Locator { - pub const fn min() -> Self { - // SAFETY: 1 is <= 4 - Self(unsafe { SmallVec::from_const_with_len_unchecked([u64::MIN; 4], 1) }) - } - - pub const fn max() -> Self { - // SAFETY: 1 is <= 4 - Self(unsafe { SmallVec::from_const_with_len_unchecked([u64::MAX; 4], 1) }) - } - - pub const fn min_ref() -> &'static Self { - const { &Self::min() } - } - - pub const fn max_ref() -> &'static Self { - const { &Self::max() } - } - - pub fn assign(&mut self, other: &Self) { - self.0.resize(other.0.len(), 0); - self.0.copy_from_slice(&other.0); - } - - pub fn between(lhs: &Self, rhs: &Self) -> Self { - let lhs = lhs.0.iter().copied().chain(iter::repeat(u64::MIN)); - let rhs = rhs.0.iter().copied().chain(iter::repeat(u64::MAX)); - let mut location = SmallVec::new(); - for (lhs, rhs) in lhs.zip(rhs) { - let mid = lhs + ((rhs.saturating_sub(lhs)) >> 48); - location.push(mid); - if mid > lhs { - break; - } - } - Self(location) - } - - pub fn len(&self) -> usize { - self.0.len() - } - - pub fn is_empty(&self) -> bool { - self.len() == 0 - } -} - -impl Default for Locator { - fn default() -> Self { - Self::min() - } -} - -impl sum_tree::Item for Locator { - type Summary = Locator; - - fn summary(&self, _cx: ()) -> Self::Summary { - self.clone() - } -} - -impl sum_tree::KeyedItem for Locator { - type Key = Locator; - - fn key(&self) -> Self::Key { - self.clone() - } -} - -impl sum_tree::ContextLessSummary for Locator { - fn zero() -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &Self) { - self.assign(summary); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use rand::prelude::*; - use std::mem; - - #[gpui::test(iterations = 100)] - fn test_locators(mut rng: StdRng) { - let mut lhs = Default::default(); - let mut rhs = Default::default(); - while lhs == rhs { - lhs = Locator( - (0..rng.random_range(1..=5)) - .map(|_| rng.random_range(0..=100)) - .collect(), - ); - rhs = Locator( - (0..rng.random_range(1..=5)) - .map(|_| rng.random_range(0..=100)) - .collect(), - ); - } - - if lhs > rhs { - mem::swap(&mut lhs, &mut rhs); - } - - let middle = Locator::between(&lhs, &rhs); - assert!(middle > lhs); - assert!(middle < rhs); - for ix in 0..middle.0.len() - 1 { - assert!( - middle.0[ix] == *lhs.0.get(ix).unwrap_or(&0) - || middle.0[ix] == *rhs.0.get(ix).unwrap_or(&0) - ); - } - } -} diff --git a/crates/text/src/network.rs b/crates/text/src/network.rs deleted file mode 100644 index d0d1b650ad..0000000000 --- a/crates/text/src/network.rs +++ /dev/null @@ -1,94 +0,0 @@ -use std::fmt::Debug; - -use clock::ReplicaId; -use collections::{BTreeMap, HashSet}; - -pub struct Network { - inboxes: BTreeMap>>, - disconnected_peers: HashSet, - rng: R, -} - -#[derive(Clone, Debug)] -struct Envelope { - message: T, -} - -impl Network { - pub fn new(rng: R) -> Self { - Network { - inboxes: BTreeMap::default(), - disconnected_peers: HashSet::default(), - rng, - } - } - - pub fn add_peer(&mut self, id: ReplicaId) { - self.inboxes.insert(id, Vec::new()); - } - - pub fn disconnect_peer(&mut self, id: ReplicaId) { - self.disconnected_peers.insert(id); - self.inboxes.get_mut(&id).unwrap().clear(); - } - - pub fn reconnect_peer(&mut self, id: ReplicaId, replicate_from: ReplicaId) { - assert!(self.disconnected_peers.remove(&id)); - self.replicate(replicate_from, id); - } - - pub fn is_disconnected(&self, id: ReplicaId) -> bool { - self.disconnected_peers.contains(&id) - } - - pub fn contains_disconnected_peers(&self) -> bool { - !self.disconnected_peers.is_empty() - } - - pub fn replicate(&mut self, old_replica_id: ReplicaId, new_replica_id: ReplicaId) { - self.inboxes - .insert(new_replica_id, self.inboxes[&old_replica_id].clone()); - } - - pub fn is_idle(&self) -> bool { - self.inboxes.values().all(|i| i.is_empty()) - } - - pub fn broadcast(&mut self, sender: ReplicaId, messages: Vec) { - // Drop messages from disconnected peers. - if self.disconnected_peers.contains(&sender) { - return; - } - - for (replica, inbox) in self.inboxes.iter_mut() { - if *replica != sender && !self.disconnected_peers.contains(replica) { - for message in &messages { - // Insert one or more duplicates of this message, potentially *before* the previous - // message sent by this peer to simulate out-of-order delivery. - for _ in 0..self.rng.random_range(1..4) { - let insertion_index = self.rng.random_range(0..inbox.len() + 1); - inbox.insert( - insertion_index, - Envelope { - message: message.clone(), - }, - ); - } - } - } - } - } - - pub fn has_unreceived(&self, receiver: ReplicaId) -> bool { - !self.inboxes[&receiver].is_empty() - } - - pub fn receive(&mut self, receiver: ReplicaId) -> Vec { - let inbox = self.inboxes.get_mut(&receiver).unwrap(); - let count = self.rng.random_range(0..inbox.len() + 1); - inbox - .drain(0..count) - .map(|envelope| envelope.message) - .collect() - } -} diff --git a/crates/text/src/operation_queue.rs b/crates/text/src/operation_queue.rs deleted file mode 100644 index f87af381ff..0000000000 --- a/crates/text/src/operation_queue.rs +++ /dev/null @@ -1,165 +0,0 @@ -use clock::Lamport; -use std::{fmt::Debug, ops::Add}; -use sum_tree::{ContextLessSummary, Dimension, Edit, Item, KeyedItem, SumTree}; - -pub trait Operation: Clone + Debug { - fn lamport_timestamp(&self) -> clock::Lamport; -} - -#[derive(Clone, Debug)] -struct OperationItem(T); - -#[derive(Clone, Debug)] -pub struct OperationQueue(SumTree>); - -#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] -pub struct OperationKey(clock::Lamport); - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct OperationSummary { - pub key: OperationKey, - pub len: usize, -} - -impl OperationKey { - pub fn new(timestamp: clock::Lamport) -> Self { - Self(timestamp) - } -} - -impl Default for OperationQueue { - fn default() -> Self { - OperationQueue::new() - } -} - -impl OperationQueue { - pub fn new() -> Self { - OperationQueue(SumTree::default()) - } - - pub fn len(&self) -> usize { - self.0.summary().len - } - - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - pub fn insert(&mut self, mut ops: Vec) { - ops.sort_by_key(|op| op.lamport_timestamp()); - ops.dedup_by_key(|op| op.lamport_timestamp()); - self.0.edit( - ops.into_iter() - .map(|op| Edit::Insert(OperationItem(op))) - .collect(), - (), - ); - } - - pub fn drain(&mut self) -> Self { - let clone = self.clone(); - self.0 = SumTree::default(); - clone - } - - pub fn iter(&self) -> impl Iterator { - self.0.iter().map(|i| &i.0) - } -} - -impl ContextLessSummary for OperationSummary { - fn zero() -> Self { - OperationSummary { - key: OperationKey::new(Lamport::MIN), - len: 0, - } - } - - fn add_summary(&mut self, other: &Self) { - assert!(self.key < other.key); - self.key = other.key; - self.len += other.len; - } -} - -impl Add<&Self> for OperationSummary { - type Output = Self; - - fn add(self, other: &Self) -> Self { - assert!(self.key < other.key); - OperationSummary { - key: other.key, - len: self.len + other.len, - } - } -} - -impl Dimension<'_, OperationSummary> for OperationKey { - fn zero(_cx: ()) -> Self { - OperationKey::new(Lamport::MIN) - } - - fn add_summary(&mut self, summary: &OperationSummary, _: ()) { - assert!(*self <= summary.key); - *self = summary.key; - } -} - -impl Item for OperationItem { - type Summary = OperationSummary; - - fn summary(&self, _cx: ()) -> Self::Summary { - OperationSummary { - key: OperationKey::new(self.0.lamport_timestamp()), - len: 1, - } - } -} - -impl KeyedItem for OperationItem { - type Key = OperationKey; - - fn key(&self) -> Self::Key { - OperationKey::new(self.0.lamport_timestamp()) - } -} - -#[cfg(test)] -mod tests { - use clock::ReplicaId; - - use super::*; - - #[test] - fn test_len() { - let mut clock = clock::Lamport::new(ReplicaId::LOCAL); - - let mut queue = OperationQueue::new(); - assert_eq!(queue.len(), 0); - - queue.insert(vec![ - TestOperation(clock.tick()), - TestOperation(clock.tick()), - ]); - assert_eq!(queue.len(), 2); - - queue.insert(vec![TestOperation(clock.tick())]); - assert_eq!(queue.len(), 3); - - drop(queue.drain()); - assert_eq!(queue.len(), 0); - - queue.insert(vec![TestOperation(clock.tick())]); - assert_eq!(queue.len(), 1); - } - - #[derive(Clone, Debug, Eq, PartialEq)] - struct TestOperation(clock::Lamport); - - impl Operation for TestOperation { - fn lamport_timestamp(&self) -> clock::Lamport { - self.0 - } - } -} diff --git a/crates/text/src/patch.rs b/crates/text/src/patch.rs deleted file mode 100644 index ec495f60fd..0000000000 --- a/crates/text/src/patch.rs +++ /dev/null @@ -1,608 +0,0 @@ -use crate::Edit; -use std::{ - cmp, mem, - ops::{Add, AddAssign, Sub}, -}; - -#[derive(Clone, Default, Debug, PartialEq, Eq)] -pub struct Patch(Vec>); - -impl Patch -where - T: 'static + Clone + Copy + Ord + Default, -{ - pub fn new(edits: Vec>) -> Self { - #[cfg(debug_assertions)] - { - let mut last_edit: Option<&Edit> = None; - for edit in &edits { - if let Some(last_edit) = last_edit { - assert!(edit.old.start > last_edit.old.end); - assert!(edit.new.start > last_edit.new.end); - } - last_edit = Some(edit); - } - } - Self(edits) - } - - pub fn edits(&self) -> &[Edit] { - &self.0 - } - - pub fn into_inner(self) -> Vec> { - self.0 - } - pub fn invert(&mut self) -> &mut Self { - for edit in &mut self.0 { - mem::swap(&mut edit.old, &mut edit.new); - } - self - } - - pub fn clear(&mut self) { - self.0.clear(); - } - - pub fn is_empty(&self) -> bool { - self.0.is_empty() - } - - pub fn push(&mut self, edit: Edit) { - if edit.is_empty() { - return; - } - - if let Some(last) = self.0.last_mut() { - if last.old.end >= edit.old.start { - last.old.end = edit.old.end; - last.new.end = edit.new.end; - } else { - self.0.push(edit); - } - } else { - self.0.push(edit); - } - } -} - -impl Patch -where - T: 'static - + Copy - + Ord - + Sub - + Add - + AddAssign - + Default, - TDelta: Ord + Copy, -{ - #[must_use] - pub fn compose(&self, new_edits_iter: impl IntoIterator>) -> Self { - let mut old_edits_iter = self.0.iter().cloned().peekable(); - let mut new_edits_iter = new_edits_iter.into_iter().peekable(); - let mut composed = Patch(Vec::new()); - - let mut old_start = T::default(); - let mut new_start = T::default(); - loop { - let old_edit = old_edits_iter.peek_mut(); - let new_edit = new_edits_iter.peek_mut(); - - // Push the old edit if its new end is before the new edit's old start. - if let Some(old_edit) = old_edit.as_ref() { - let new_edit = new_edit.as_ref(); - if new_edit.is_none_or(|new_edit| old_edit.new.end < new_edit.old.start) { - let catchup = old_edit.old.start - old_start; - old_start += catchup; - new_start += catchup; - - let old_end = old_start + old_edit.old_len(); - let new_end = new_start + old_edit.new_len(); - composed.push(Edit { - old: old_start..old_end, - new: new_start..new_end, - }); - old_start = old_end; - new_start = new_end; - old_edits_iter.next(); - continue; - } - } - - // Push the new edit if its old end is before the old edit's new start. - if let Some(new_edit) = new_edit.as_ref() { - let old_edit = old_edit.as_ref(); - if old_edit.is_none_or(|old_edit| new_edit.old.end < old_edit.new.start) { - let catchup = new_edit.new.start - new_start; - old_start += catchup; - new_start += catchup; - - let old_end = old_start + new_edit.old_len(); - let new_end = new_start + new_edit.new_len(); - composed.push(Edit { - old: old_start..old_end, - new: new_start..new_end, - }); - old_start = old_end; - new_start = new_end; - new_edits_iter.next(); - continue; - } - } - - // If we still have edits by this point then they must intersect, so we compose them. - if let Some((old_edit, new_edit)) = old_edit.zip(new_edit) { - if old_edit.new.start < new_edit.old.start { - let catchup = old_edit.old.start - old_start; - old_start += catchup; - new_start += catchup; - - let overshoot = new_edit.old.start - old_edit.new.start; - let old_end = cmp::min(old_start + overshoot, old_edit.old.end); - let new_end = new_start + overshoot; - composed.push(Edit { - old: old_start..old_end, - new: new_start..new_end, - }); - - old_edit.old.start = old_end; - old_edit.new.start += overshoot; - old_start = old_end; - new_start = new_end; - } else { - let catchup = new_edit.new.start - new_start; - old_start += catchup; - new_start += catchup; - - let overshoot = old_edit.new.start - new_edit.old.start; - let old_end = old_start + overshoot; - let new_end = cmp::min(new_start + overshoot, new_edit.new.end); - composed.push(Edit { - old: old_start..old_end, - new: new_start..new_end, - }); - - new_edit.old.start += overshoot; - new_edit.new.start = new_end; - old_start = old_end; - new_start = new_end; - } - - if old_edit.new.end > new_edit.old.end { - let old_end = old_start + cmp::min(old_edit.old_len(), new_edit.old_len()); - let new_end = new_start + new_edit.new_len(); - composed.push(Edit { - old: old_start..old_end, - new: new_start..new_end, - }); - - old_edit.old.start = old_end; - old_edit.new.start = new_edit.old.end; - old_start = old_end; - new_start = new_end; - new_edits_iter.next(); - } else { - let old_end = old_start + old_edit.old_len(); - let new_end = new_start + cmp::min(old_edit.new_len(), new_edit.new_len()); - composed.push(Edit { - old: old_start..old_end, - new: new_start..new_end, - }); - - new_edit.old.start = old_edit.new.end; - new_edit.new.start = new_end; - old_start = old_end; - new_start = new_end; - old_edits_iter.next(); - } - } else { - break; - } - } - - composed - } - - pub fn old_to_new(&self, old: T) -> T { - let ix = match self.0.binary_search_by(|probe| probe.old.start.cmp(&old)) { - Ok(ix) => ix, - Err(ix) => { - if ix == 0 { - return old; - } else { - ix - 1 - } - } - }; - if let Some(edit) = self.0.get(ix) { - if old >= edit.old.end { - edit.new.end + (old - edit.old.end) - } else { - edit.new.start - } - } else { - old - } - } -} - -impl Patch { - pub fn retain_mut(&mut self, f: F) - where - F: FnMut(&mut Edit) -> bool, - { - self.0.retain_mut(f); - } -} - -impl IntoIterator for Patch { - type Item = Edit; - type IntoIter = std::vec::IntoIter>; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} - -impl<'a, T: Clone> IntoIterator for &'a Patch { - type Item = Edit; - type IntoIter = std::iter::Cloned>>; - - fn into_iter(self) -> Self::IntoIter { - self.0.iter().cloned() - } -} - -impl<'a, T: Clone> IntoIterator for &'a mut Patch { - type Item = Edit; - type IntoIter = std::iter::Cloned>>; - - fn into_iter(self) -> Self::IntoIter { - self.0.iter().cloned() - } -} - -#[cfg(test)] -mod tests { - use super::*; - use rand::prelude::*; - use std::env; - - #[gpui::test] - fn test_one_disjoint_edit() { - assert_patch_composition( - Patch(vec![Edit { - old: 1..3, - new: 1..4, - }]), - Patch(vec![Edit { - old: 0..0, - new: 0..4, - }]), - Patch(vec![ - Edit { - old: 0..0, - new: 0..4, - }, - Edit { - old: 1..3, - new: 5..8, - }, - ]), - ); - - assert_patch_composition( - Patch(vec![Edit { - old: 1..3, - new: 1..4, - }]), - Patch(vec![Edit { - old: 5..9, - new: 5..7, - }]), - Patch(vec![ - Edit { - old: 1..3, - new: 1..4, - }, - Edit { - old: 4..8, - new: 5..7, - }, - ]), - ); - } - - #[gpui::test] - fn test_one_overlapping_edit() { - assert_patch_composition( - Patch(vec![Edit { - old: 1..3, - new: 1..4, - }]), - Patch(vec![Edit { - old: 3..5, - new: 3..6, - }]), - Patch(vec![Edit { - old: 1..4, - new: 1..6, - }]), - ); - } - - #[gpui::test] - fn test_two_disjoint_and_overlapping() { - assert_patch_composition( - Patch(vec![ - Edit { - old: 1..3, - new: 1..4, - }, - Edit { - old: 8..12, - new: 9..11, - }, - ]), - Patch(vec![ - Edit { - old: 0..0, - new: 0..4, - }, - Edit { - old: 3..10, - new: 7..9, - }, - ]), - Patch(vec![ - Edit { - old: 0..0, - new: 0..4, - }, - Edit { - old: 1..12, - new: 5..10, - }, - ]), - ); - } - - #[gpui::test] - fn test_two_new_edits_overlapping_one_old_edit() { - assert_patch_composition( - Patch(vec![Edit { - old: 0..0, - new: 0..3, - }]), - Patch(vec![ - Edit { - old: 0..0, - new: 0..1, - }, - Edit { - old: 1..2, - new: 2..2, - }, - ]), - Patch(vec![Edit { - old: 0..0, - new: 0..3, - }]), - ); - - assert_patch_composition( - Patch(vec![Edit { - old: 2..3, - new: 2..4, - }]), - Patch(vec![ - Edit { - old: 0..2, - new: 0..1, - }, - Edit { - old: 3..3, - new: 2..5, - }, - ]), - Patch(vec![Edit { - old: 0..3, - new: 0..6, - }]), - ); - - assert_patch_composition( - Patch(vec![Edit { - old: 0..0, - new: 0..2, - }]), - Patch(vec![ - Edit { - old: 0..0, - new: 0..2, - }, - Edit { - old: 2..5, - new: 4..4, - }, - ]), - Patch(vec![Edit { - old: 0..3, - new: 0..4, - }]), - ); - } - - #[gpui::test] - fn test_two_new_edits_touching_one_old_edit() { - assert_patch_composition( - Patch(vec![ - Edit { - old: 2..3, - new: 2..4, - }, - Edit { - old: 7..7, - new: 8..11, - }, - ]), - Patch(vec![ - Edit { - old: 2..3, - new: 2..2, - }, - Edit { - old: 4..4, - new: 3..4, - }, - ]), - Patch(vec![ - Edit { - old: 2..3, - new: 2..4, - }, - Edit { - old: 7..7, - new: 8..11, - }, - ]), - ); - } - - #[gpui::test] - fn test_old_to_new() { - let patch = Patch(vec![ - Edit { - old: 2..4, - new: 2..4, - }, - Edit { - old: 7..8, - new: 7..11, - }, - ]); - assert_eq!(patch.old_to_new(0), 0); - assert_eq!(patch.old_to_new(1), 1); - assert_eq!(patch.old_to_new(2), 2); - assert_eq!(patch.old_to_new(3), 2); - assert_eq!(patch.old_to_new(4), 4); - assert_eq!(patch.old_to_new(5), 5); - assert_eq!(patch.old_to_new(6), 6); - assert_eq!(patch.old_to_new(7), 7); - assert_eq!(patch.old_to_new(8), 11); - assert_eq!(patch.old_to_new(9), 12); - } - - #[gpui::test(iterations = 100)] - fn test_random_patch_compositions(mut rng: StdRng) { - let operations = env::var("OPERATIONS") - .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) - .unwrap_or(20); - - let initial_chars = (0..rng.random_range(0..=100)) - .map(|_| rng.random_range(b'a'..=b'z') as char) - .collect::>(); - log::info!("initial chars: {:?}", initial_chars); - - // Generate two sequential patches - let mut patches = Vec::new(); - let mut expected_chars = initial_chars.clone(); - for i in 0..2 { - log::info!("patch {}:", i); - - let mut delta = 0i32; - let mut last_edit_end = 0; - let mut edits = Vec::new(); - - for _ in 0..operations { - if last_edit_end >= expected_chars.len() { - break; - } - - let end = rng.random_range(last_edit_end..=expected_chars.len()); - let start = rng.random_range(last_edit_end..=end); - let old_len = end - start; - - let mut new_len = rng.random_range(0..=3); - if start == end && new_len == 0 { - new_len += 1; - } - - last_edit_end = start + new_len + 1; - - let new_chars = (0..new_len) - .map(|_| rng.random_range(b'A'..=b'Z') as char) - .collect::>(); - log::info!( - " editing {:?}: {:?}", - start..end, - new_chars.iter().collect::() - ); - edits.push(Edit { - old: (start as i32 - delta) as u32..(end as i32 - delta) as u32, - new: start as u32..(start + new_len) as u32, - }); - expected_chars.splice(start..end, new_chars); - - delta += new_len as i32 - old_len as i32; - } - - patches.push(Patch(edits)); - } - - log::info!("old patch: {:?}", &patches[0]); - log::info!("new patch: {:?}", &patches[1]); - log::info!("initial chars: {:?}", initial_chars); - log::info!("final chars: {:?}", expected_chars); - - // Compose the patches, and verify that it has the same effect as applying the - // two patches separately. - let composed = patches[0].compose(&patches[1]); - log::info!("composed patch: {:?}", &composed); - - let mut actual_chars = initial_chars; - for edit in composed.0 { - actual_chars.splice( - edit.new.start as usize..edit.new.start as usize + edit.old.len(), - expected_chars[edit.new.start as usize..edit.new.end as usize] - .iter() - .copied(), - ); - } - - assert_eq!(actual_chars, expected_chars); - } - - #[track_caller] - #[allow(clippy::almost_complete_range)] - fn assert_patch_composition(old: Patch, new: Patch, composed: Patch) { - let original = ('a'..'z').collect::>(); - let inserted = ('A'..'Z').collect::>(); - - let mut expected = original.clone(); - apply_patch(&mut expected, &old, &inserted); - apply_patch(&mut expected, &new, &inserted); - - let mut actual = original; - apply_patch(&mut actual, &composed, &expected); - assert_eq!( - actual.into_iter().collect::(), - expected.into_iter().collect::(), - "expected patch is incorrect" - ); - - assert_eq!(old.compose(&new), composed); - } - - fn apply_patch(text: &mut Vec, patch: &Patch, new_text: &[char]) { - for edit in patch.0.iter().rev() { - text.splice( - edit.old.start as usize..edit.old.end as usize, - new_text[edit.new.start as usize..edit.new.end as usize] - .iter() - .copied(), - ); - } - } -} diff --git a/crates/text/src/selection.rs b/crates/text/src/selection.rs deleted file mode 100644 index e355f70c49..0000000000 --- a/crates/text/src/selection.rs +++ /dev/null @@ -1,169 +0,0 @@ -use crate::{Anchor, BufferSnapshot, TextDimension}; -use std::cmp::Ordering; -use std::ops::Range; - -#[derive(Default, Copy, Clone, Debug, PartialEq)] -pub enum SelectionGoal { - #[default] - None, - HorizontalPosition(f64), - HorizontalRange { - start: f64, - end: f64, - }, - WrappedHorizontalPosition((u32, f32)), -} - -#[derive(Clone, Debug, PartialEq)] -pub struct Selection { - pub id: usize, - pub start: T, - pub end: T, - pub reversed: bool, - pub goal: SelectionGoal, -} - -impl Selection { - /// A place where the selection had stopped at. - pub fn head(&self) -> T { - if self.reversed { - self.start.clone() - } else { - self.end.clone() - } - } - - /// A place where selection was initiated from. - pub fn tail(&self) -> T { - if self.reversed { - self.end.clone() - } else { - self.start.clone() - } - } - - pub fn map(&self, f: F) -> Selection - where - F: Fn(T) -> S, - { - Selection:: { - id: self.id, - start: f(self.start.clone()), - end: f(self.end.clone()), - reversed: self.reversed, - goal: self.goal, - } - } - - pub fn collapse_to(&mut self, point: T, new_goal: SelectionGoal) { - self.start = point.clone(); - self.end = point; - self.goal = new_goal; - self.reversed = false; - } -} - -impl Selection { - pub fn is_empty(&self) -> bool { - self.start == self.end - } - - pub fn set_head(&mut self, head: T, new_goal: SelectionGoal) { - if head.cmp(&self.tail()) < Ordering::Equal { - if !self.reversed { - self.end = self.start; - self.reversed = true; - } - self.start = head; - } else { - if self.reversed { - self.start = self.end; - self.reversed = false; - } - self.end = head; - } - self.goal = new_goal; - } - - pub fn set_tail(&mut self, tail: T, new_goal: SelectionGoal) { - if tail.cmp(&self.head()) <= Ordering::Equal { - if self.reversed { - self.end = self.start; - self.reversed = false; - } - self.start = tail; - } else { - if !self.reversed { - self.start = self.end; - self.reversed = true; - } - self.end = tail; - } - self.goal = new_goal; - } - - pub fn set_head_tail(&mut self, head: T, tail: T, new_goal: SelectionGoal) { - if head < tail { - self.reversed = true; - self.start = head; - self.end = tail; - } else { - self.reversed = false; - self.start = tail; - self.end = head; - } - self.goal = new_goal; - } - - pub fn swap_head_tail(&mut self) { - if self.reversed { - self.reversed = false; - } else { - std::mem::swap(&mut self.start, &mut self.end); - } - } -} - -impl Selection { - pub fn range(&self) -> Range { - self.start..self.end - } -} - -impl Selection { - pub fn len(&self) -> ::Output { - self.end - self.start - } -} - -impl Selection { - #[cfg(feature = "test-support")] - pub fn from_offset(offset: T) -> Self { - Selection { - id: 0, - start: offset, - end: offset, - goal: SelectionGoal::None, - reversed: false, - } - } - - pub fn equals(&self, offset_range: &Range) -> bool { - self.start == offset_range.start && self.end == offset_range.end - } -} - -impl Selection { - pub fn resolve<'a, D: 'a + TextDimension>( - &'a self, - snapshot: &'a BufferSnapshot, - ) -> Selection { - Selection { - id: self.id, - start: snapshot.summary_for_anchor(&self.start), - end: snapshot.summary_for_anchor(&self.end), - reversed: self.reversed, - goal: self.goal, - } - } -} diff --git a/crates/text/src/subscription.rs b/crates/text/src/subscription.rs deleted file mode 100644 index 50857a2de4..0000000000 --- a/crates/text/src/subscription.rs +++ /dev/null @@ -1,67 +0,0 @@ -use crate::{Edit, Patch}; -use parking_lot::Mutex; -use std::{ - mem, - sync::{Arc, Weak}, -}; - -#[derive(Default)] -pub struct Topic(Mutex>>>>); - -pub struct Subscription(Arc>>); - -impl Topic -where - T: 'static - + Copy - + Ord - + std::ops::Sub - + std::ops::Add - + std::ops::AddAssign - + Default, - TDelta: Ord + Copy, -{ - pub fn subscribe(&mut self) -> Subscription { - let subscription = Subscription(Default::default()); - self.0.get_mut().push(Arc::downgrade(&subscription.0)); - subscription - } - - pub fn publish(&self, edits: impl Clone + IntoIterator>) { - publish(&mut self.0.lock(), edits); - } - - pub fn publish_mut(&mut self, edits: impl Clone + IntoIterator>) { - publish(self.0.get_mut(), edits); - } -} - -impl Subscription { - pub fn consume(&self) -> Patch { - mem::take(&mut *self.0.lock()) - } -} - -fn publish( - subscriptions: &mut Vec>>>, - edits: impl Clone + IntoIterator>, -) where - T: 'static - + Copy - + Ord - + std::ops::Sub - + std::ops::Add - + std::ops::AddAssign - + Default, - TDelta: Ord + Copy, -{ - subscriptions.retain(|subscription| { - if let Some(subscription) = subscription.upgrade() { - let mut patch = subscription.lock(); - *patch = patch.compose(edits.clone()); - true - } else { - false - } - }); -} diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs deleted file mode 100644 index c9e04e407f..0000000000 --- a/crates/text/src/tests.rs +++ /dev/null @@ -1,812 +0,0 @@ -use super::{network::Network, *}; -use clock::ReplicaId; -use rand::prelude::*; -use std::{ - cmp::Ordering, - env, - iter::Iterator, - time::{Duration, Instant}, -}; - -#[cfg(test)] -#[ctor::ctor] -fn init_logger() { - zlog::init_test(); -} - -#[test] -fn test_edit() { - let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), "abc"); - assert_eq!(buffer.text(), "abc"); - buffer.edit([(3..3, "def")]); - assert_eq!(buffer.text(), "abcdef"); - buffer.edit([(0..0, "ghi")]); - assert_eq!(buffer.text(), "ghiabcdef"); - buffer.edit([(5..5, "jkl")]); - assert_eq!(buffer.text(), "ghiabjklcdef"); - buffer.edit([(6..7, "")]); - assert_eq!(buffer.text(), "ghiabjlcdef"); - buffer.edit([(4..9, "mno")]); - assert_eq!(buffer.text(), "ghiamnoef"); -} - -#[gpui::test(iterations = 100)] -fn test_random_edits(mut rng: StdRng) { - let operations = env::var("OPERATIONS") - .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) - .unwrap_or(10); - - let reference_string_len = rng.random_range(0..3); - let mut reference_string = RandomCharIter::new(&mut rng) - .take(reference_string_len) - .collect::(); - let mut buffer = Buffer::new( - ReplicaId::LOCAL, - BufferId::new(1).unwrap(), - reference_string.clone(), - ); - LineEnding::normalize(&mut reference_string); - - buffer.set_group_interval(Duration::from_millis(rng.random_range(0..=200))); - let mut buffer_versions = Vec::new(); - log::info!( - "buffer text {:?}, version: {:?}", - buffer.text(), - buffer.version() - ); - - for _i in 0..operations { - let (edits, _) = buffer.randomly_edit(&mut rng, 5); - for (old_range, new_text) in edits.iter().rev() { - reference_string.replace_range(old_range.clone(), new_text); - } - - assert_eq!(buffer.text(), reference_string); - log::info!( - "buffer text {:?}, version: {:?}", - buffer.text(), - buffer.version() - ); - - if rng.random_bool(0.25) { - buffer.randomly_undo_redo(&mut rng); - reference_string = buffer.text(); - log::info!( - "buffer text {:?}, version: {:?}", - buffer.text(), - buffer.version() - ); - } - - let range = buffer.random_byte_range(0, &mut rng); - assert_eq!( - buffer.text_summary_for_range::(range.clone()), - TextSummary::from(&reference_string[range]) - ); - - buffer.check_invariants(); - - if rng.random_bool(0.3) { - buffer_versions.push((buffer.clone(), buffer.subscribe())); - } - } - - for (old_buffer, subscription) in buffer_versions { - let edits = buffer - .edits_since::(&old_buffer.version) - .collect::>(); - - log::info!( - "applying edits since version {:?} to old text: {:?}: {:?}", - old_buffer.version(), - old_buffer.text(), - edits, - ); - - let mut text = old_buffer.visible_text.clone(); - for edit in edits { - let new_text: String = buffer.text_for_range(edit.new.clone()).collect(); - text.replace(edit.new.start..edit.new.start + edit.old.len(), &new_text); - } - assert_eq!(text.to_string(), buffer.text()); - - assert_eq!( - buffer.rope_for_version(old_buffer.version()).to_string(), - old_buffer.text() - ); - - for _ in 0..5 { - let end_ix = - old_buffer.clip_offset(rng.random_range(0..=old_buffer.len()), Bias::Right); - let start_ix = old_buffer.clip_offset(rng.random_range(0..=end_ix), Bias::Left); - let range = old_buffer.anchor_before(start_ix)..old_buffer.anchor_after(end_ix); - let mut old_text = old_buffer.text_for_range(range.clone()).collect::(); - let edits = buffer - .edits_since_in_range::(&old_buffer.version, range.clone()) - .collect::>(); - log::info!( - "applying edits since version {:?} to old text in range {:?}: {:?}: {:?}", - old_buffer.version(), - start_ix..end_ix, - old_text, - edits, - ); - - let new_text = buffer.text_for_range(range).collect::(); - for edit in edits { - old_text.replace_range( - edit.new.start..edit.new.start + edit.old_len(), - &new_text[edit.new], - ); - } - assert_eq!(old_text, new_text); - } - - assert_eq!( - buffer.has_edits_since(&old_buffer.version), - buffer - .edits_since::(&old_buffer.version) - .next() - .is_some(), - ); - - let subscription_edits = subscription.consume(); - log::info!( - "applying subscription edits since version {:?} to old text: {:?}: {:?}", - old_buffer.version(), - old_buffer.text(), - subscription_edits, - ); - - let mut text = old_buffer.visible_text.clone(); - for edit in subscription_edits.into_inner() { - let new_text: String = buffer.text_for_range(edit.new.clone()).collect(); - text.replace(edit.new.start..edit.new.start + edit.old.len(), &new_text); - } - assert_eq!(text.to_string(), buffer.text()); - } -} - -#[test] -fn test_line_endings() { - assert_eq!(LineEnding::detect(&"🍐✅\n".repeat(1000)), LineEnding::Unix); - assert_eq!(LineEnding::detect(&"abcd\n".repeat(1000)), LineEnding::Unix); - assert_eq!( - LineEnding::detect(&"🍐✅\r\n".repeat(1000)), - LineEnding::Windows - ); - assert_eq!( - LineEnding::detect(&"abcd\r\n".repeat(1000)), - LineEnding::Windows - ); - - let mut buffer = Buffer::new( - ReplicaId::LOCAL, - BufferId::new(1).unwrap(), - "one\r\ntwo\rthree", - ); - assert_eq!(buffer.text(), "one\ntwo\nthree"); - assert_eq!(buffer.line_ending(), LineEnding::Windows); - buffer.check_invariants(); - - buffer.edit([(buffer.len()..buffer.len(), "\r\nfour")]); - buffer.edit([(0..0, "zero\r\n")]); - assert_eq!(buffer.text(), "zero\none\ntwo\nthree\nfour"); - assert_eq!(buffer.line_ending(), LineEnding::Windows); - buffer.check_invariants(); -} - -#[test] -fn test_line_len() { - let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), ""); - buffer.edit([(0..0, "abcd\nefg\nhij")]); - buffer.edit([(12..12, "kl\nmno")]); - buffer.edit([(18..18, "\npqrs\n")]); - buffer.edit([(18..21, "\nPQ")]); - - assert_eq!(buffer.line_len(0), 4); - assert_eq!(buffer.line_len(1), 3); - assert_eq!(buffer.line_len(2), 5); - assert_eq!(buffer.line_len(3), 3); - assert_eq!(buffer.line_len(4), 4); - assert_eq!(buffer.line_len(5), 0); -} - -#[test] -fn test_common_prefix_at_position() { - let text = "a = str; b = δα"; - let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), text); - - let offset1 = offset_after(text, "str"); - let offset2 = offset_after(text, "δα"); - - // the preceding word is a prefix of the suggestion - assert_eq!( - buffer.common_prefix_at(offset1, "string"), - range_of(text, "str"), - ); - // a suffix of the preceding word is a prefix of the suggestion - assert_eq!( - buffer.common_prefix_at(offset1, "tree"), - range_of(text, "tr"), - ); - // the preceding word is a substring of the suggestion, but not a prefix - assert_eq!( - buffer.common_prefix_at(offset1, "astro"), - empty_range_after(text, "str"), - ); - - // prefix matching is case insensitive. - assert_eq!( - buffer.common_prefix_at(offset1, "Strαngε"), - range_of(text, "str"), - ); - assert_eq!( - buffer.common_prefix_at(offset2, "ΔΑΜΝ"), - range_of(text, "δα"), - ); - - fn offset_after(text: &str, part: &str) -> usize { - text.find(part).unwrap() + part.len() - } - - fn empty_range_after(text: &str, part: &str) -> Range { - let offset = offset_after(text, part); - offset..offset - } - - fn range_of(text: &str, part: &str) -> Range { - let start = text.find(part).unwrap(); - start..start + part.len() - } -} - -#[test] -fn test_text_summary_for_range() { - let buffer = Buffer::new( - ReplicaId::LOCAL, - BufferId::new(1).unwrap(), - "ab\nefg\nhklm\nnopqrs\ntuvwxyz", - ); - assert_eq!( - buffer.text_summary_for_range::(0..2), - TextSummary { - len: 2, - chars: 2, - len_utf16: OffsetUtf16(2), - lines: Point::new(0, 2), - first_line_chars: 2, - last_line_chars: 2, - last_line_len_utf16: 2, - longest_row: 0, - longest_row_chars: 2, - } - ); - assert_eq!( - buffer.text_summary_for_range::(1..3), - TextSummary { - len: 2, - chars: 2, - len_utf16: OffsetUtf16(2), - lines: Point::new(1, 0), - first_line_chars: 1, - last_line_chars: 0, - last_line_len_utf16: 0, - longest_row: 0, - longest_row_chars: 1, - } - ); - assert_eq!( - buffer.text_summary_for_range::(1..12), - TextSummary { - len: 11, - chars: 11, - len_utf16: OffsetUtf16(11), - lines: Point::new(3, 0), - first_line_chars: 1, - last_line_chars: 0, - last_line_len_utf16: 0, - longest_row: 2, - longest_row_chars: 4, - } - ); - assert_eq!( - buffer.text_summary_for_range::(0..20), - TextSummary { - len: 20, - chars: 20, - len_utf16: OffsetUtf16(20), - lines: Point::new(4, 1), - first_line_chars: 2, - last_line_chars: 1, - last_line_len_utf16: 1, - longest_row: 3, - longest_row_chars: 6, - } - ); - assert_eq!( - buffer.text_summary_for_range::(0..22), - TextSummary { - len: 22, - chars: 22, - len_utf16: OffsetUtf16(22), - lines: Point::new(4, 3), - first_line_chars: 2, - last_line_chars: 3, - last_line_len_utf16: 3, - longest_row: 3, - longest_row_chars: 6, - } - ); - assert_eq!( - buffer.text_summary_for_range::(7..22), - TextSummary { - len: 15, - chars: 15, - len_utf16: OffsetUtf16(15), - lines: Point::new(2, 3), - first_line_chars: 4, - last_line_chars: 3, - last_line_len_utf16: 3, - longest_row: 1, - longest_row_chars: 6, - } - ); -} - -#[test] -fn test_chars_at() { - let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), ""); - buffer.edit([(0..0, "abcd\nefgh\nij")]); - buffer.edit([(12..12, "kl\nmno")]); - buffer.edit([(18..18, "\npqrs")]); - buffer.edit([(18..21, "\nPQ")]); - - let chars = buffer.chars_at(Point::new(0, 0)); - assert_eq!(chars.collect::(), "abcd\nefgh\nijkl\nmno\nPQrs"); - - let chars = buffer.chars_at(Point::new(1, 0)); - assert_eq!(chars.collect::(), "efgh\nijkl\nmno\nPQrs"); - - let chars = buffer.chars_at(Point::new(2, 0)); - assert_eq!(chars.collect::(), "ijkl\nmno\nPQrs"); - - let chars = buffer.chars_at(Point::new(3, 0)); - assert_eq!(chars.collect::(), "mno\nPQrs"); - - let chars = buffer.chars_at(Point::new(4, 0)); - assert_eq!(chars.collect::(), "PQrs"); - - // Regression test: - let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), ""); - buffer.edit([(0..0, "[workspace]\nmembers = [\n \"xray_core\",\n \"xray_server\",\n \"xray_cli\",\n \"xray_wasm\",\n]\n")]); - buffer.edit([(60..60, "\n")]); - - let chars = buffer.chars_at(Point::new(6, 0)); - assert_eq!(chars.collect::(), " \"xray_wasm\",\n]\n"); -} - -#[test] -fn test_anchors() { - let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), ""); - buffer.edit([(0..0, "abc")]); - let left_anchor = buffer.anchor_before(2); - let right_anchor = buffer.anchor_after(2); - - buffer.edit([(1..1, "def\n")]); - assert_eq!(buffer.text(), "adef\nbc"); - assert_eq!(left_anchor.to_offset(&buffer), 6); - assert_eq!(right_anchor.to_offset(&buffer), 6); - assert_eq!(left_anchor.to_point(&buffer), Point { row: 1, column: 1 }); - assert_eq!(right_anchor.to_point(&buffer), Point { row: 1, column: 1 }); - - buffer.edit([(2..3, "")]); - assert_eq!(buffer.text(), "adf\nbc"); - assert_eq!(left_anchor.to_offset(&buffer), 5); - assert_eq!(right_anchor.to_offset(&buffer), 5); - assert_eq!(left_anchor.to_point(&buffer), Point { row: 1, column: 1 }); - assert_eq!(right_anchor.to_point(&buffer), Point { row: 1, column: 1 }); - - buffer.edit([(5..5, "ghi\n")]); - assert_eq!(buffer.text(), "adf\nbghi\nc"); - assert_eq!(left_anchor.to_offset(&buffer), 5); - assert_eq!(right_anchor.to_offset(&buffer), 9); - assert_eq!(left_anchor.to_point(&buffer), Point { row: 1, column: 1 }); - assert_eq!(right_anchor.to_point(&buffer), Point { row: 2, column: 0 }); - - buffer.edit([(7..9, "")]); - assert_eq!(buffer.text(), "adf\nbghc"); - assert_eq!(left_anchor.to_offset(&buffer), 5); - assert_eq!(right_anchor.to_offset(&buffer), 7); - assert_eq!(left_anchor.to_point(&buffer), Point { row: 1, column: 1 },); - assert_eq!(right_anchor.to_point(&buffer), Point { row: 1, column: 3 }); - - // Ensure anchoring to a point is equivalent to anchoring to an offset. - assert_eq!( - buffer.anchor_before(Point { row: 0, column: 0 }), - buffer.anchor_before(0) - ); - assert_eq!( - buffer.anchor_before(Point { row: 0, column: 1 }), - buffer.anchor_before(1) - ); - assert_eq!( - buffer.anchor_before(Point { row: 0, column: 2 }), - buffer.anchor_before(2) - ); - assert_eq!( - buffer.anchor_before(Point { row: 0, column: 3 }), - buffer.anchor_before(3) - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 0 }), - buffer.anchor_before(4) - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 1 }), - buffer.anchor_before(5) - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 2 }), - buffer.anchor_before(6) - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 3 }), - buffer.anchor_before(7) - ); - assert_eq!( - buffer.anchor_before(Point { row: 1, column: 4 }), - buffer.anchor_before(8) - ); - - // Comparison between anchors. - let anchor_at_offset_0 = buffer.anchor_before(0); - let anchor_at_offset_1 = buffer.anchor_before(1); - let anchor_at_offset_2 = buffer.anchor_before(2); - - assert_eq!( - anchor_at_offset_0.cmp(&anchor_at_offset_0, &buffer), - Ordering::Equal - ); - assert_eq!( - anchor_at_offset_1.cmp(&anchor_at_offset_1, &buffer), - Ordering::Equal - ); - assert_eq!( - anchor_at_offset_2.cmp(&anchor_at_offset_2, &buffer), - Ordering::Equal - ); - - assert_eq!( - anchor_at_offset_0.cmp(&anchor_at_offset_1, &buffer), - Ordering::Less - ); - assert_eq!( - anchor_at_offset_1.cmp(&anchor_at_offset_2, &buffer), - Ordering::Less - ); - assert_eq!( - anchor_at_offset_0.cmp(&anchor_at_offset_2, &buffer), - Ordering::Less - ); - - assert_eq!( - anchor_at_offset_1.cmp(&anchor_at_offset_0, &buffer), - Ordering::Greater - ); - assert_eq!( - anchor_at_offset_2.cmp(&anchor_at_offset_1, &buffer), - Ordering::Greater - ); - assert_eq!( - anchor_at_offset_2.cmp(&anchor_at_offset_0, &buffer), - Ordering::Greater - ); -} - -#[test] -fn test_anchors_at_start_and_end() { - let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), ""); - let before_start_anchor = buffer.anchor_before(0); - let after_end_anchor = buffer.anchor_after(0); - - buffer.edit([(0..0, "abc")]); - assert_eq!(buffer.text(), "abc"); - assert_eq!(before_start_anchor.to_offset(&buffer), 0); - assert_eq!(after_end_anchor.to_offset(&buffer), 3); - - let after_start_anchor = buffer.anchor_after(0); - let before_end_anchor = buffer.anchor_before(3); - - buffer.edit([(3..3, "def")]); - buffer.edit([(0..0, "ghi")]); - assert_eq!(buffer.text(), "ghiabcdef"); - assert_eq!(before_start_anchor.to_offset(&buffer), 0); - assert_eq!(after_start_anchor.to_offset(&buffer), 3); - assert_eq!(before_end_anchor.to_offset(&buffer), 6); - assert_eq!(after_end_anchor.to_offset(&buffer), 9); -} - -#[test] -fn test_undo_redo() { - let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), "1234"); - // Set group interval to zero so as to not group edits in the undo stack. - buffer.set_group_interval(Duration::from_secs(0)); - - buffer.edit([(1..1, "abx")]); - buffer.edit([(3..4, "yzef")]); - buffer.edit([(3..5, "cd")]); - assert_eq!(buffer.text(), "1abcdef234"); - - let entries = buffer.history.undo_stack.clone(); - assert_eq!(entries.len(), 3); - - buffer.undo_or_redo(entries[0].transaction.clone()); - assert_eq!(buffer.text(), "1cdef234"); - buffer.undo_or_redo(entries[0].transaction.clone()); - assert_eq!(buffer.text(), "1abcdef234"); - - buffer.undo_or_redo(entries[1].transaction.clone()); - assert_eq!(buffer.text(), "1abcdx234"); - buffer.undo_or_redo(entries[2].transaction.clone()); - assert_eq!(buffer.text(), "1abx234"); - buffer.undo_or_redo(entries[1].transaction.clone()); - assert_eq!(buffer.text(), "1abyzef234"); - buffer.undo_or_redo(entries[2].transaction.clone()); - assert_eq!(buffer.text(), "1abcdef234"); - - buffer.undo_or_redo(entries[2].transaction.clone()); - assert_eq!(buffer.text(), "1abyzef234"); - buffer.undo_or_redo(entries[0].transaction.clone()); - assert_eq!(buffer.text(), "1yzef234"); - buffer.undo_or_redo(entries[1].transaction.clone()); - assert_eq!(buffer.text(), "1234"); -} - -#[test] -fn test_history() { - let mut now = Instant::now(); - let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), "123456"); - buffer.set_group_interval(Duration::from_millis(300)); - - let transaction_1 = buffer.start_transaction_at(now).unwrap(); - buffer.edit([(2..4, "cd")]); - buffer.end_transaction_at(now); - assert_eq!(buffer.text(), "12cd56"); - - buffer.start_transaction_at(now); - buffer.edit([(4..5, "e")]); - buffer.end_transaction_at(now).unwrap(); - assert_eq!(buffer.text(), "12cde6"); - - now += buffer.transaction_group_interval() + Duration::from_millis(1); - buffer.start_transaction_at(now); - buffer.edit([(0..1, "a")]); - buffer.edit([(1..1, "b")]); - buffer.end_transaction_at(now).unwrap(); - assert_eq!(buffer.text(), "ab2cde6"); - - // Last transaction happened past the group interval, undo it on its own. - buffer.undo(); - assert_eq!(buffer.text(), "12cde6"); - - // First two transactions happened within the group interval, undo them together. - buffer.undo(); - assert_eq!(buffer.text(), "123456"); - - // Redo the first two transactions together. - buffer.redo(); - assert_eq!(buffer.text(), "12cde6"); - - // Redo the last transaction on its own. - buffer.redo(); - assert_eq!(buffer.text(), "ab2cde6"); - - buffer.start_transaction_at(now); - assert!(buffer.end_transaction_at(now).is_none()); - buffer.undo(); - assert_eq!(buffer.text(), "12cde6"); - - // Redo stack gets cleared after performing an edit. - buffer.start_transaction_at(now); - buffer.edit([(0..0, "X")]); - buffer.end_transaction_at(now); - assert_eq!(buffer.text(), "X12cde6"); - buffer.redo(); - assert_eq!(buffer.text(), "X12cde6"); - buffer.undo(); - assert_eq!(buffer.text(), "12cde6"); - buffer.undo(); - assert_eq!(buffer.text(), "123456"); - - // Transactions can be grouped manually. - buffer.redo(); - buffer.redo(); - assert_eq!(buffer.text(), "X12cde6"); - buffer.group_until_transaction(transaction_1); - buffer.undo(); - assert_eq!(buffer.text(), "123456"); - buffer.redo(); - assert_eq!(buffer.text(), "X12cde6"); -} - -#[test] -fn test_finalize_last_transaction() { - let now = Instant::now(); - let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), "123456"); - buffer.history.group_interval = Duration::from_millis(1); - - buffer.start_transaction_at(now); - buffer.edit([(2..4, "cd")]); - buffer.end_transaction_at(now); - assert_eq!(buffer.text(), "12cd56"); - - buffer.finalize_last_transaction(); - buffer.start_transaction_at(now); - buffer.edit([(4..5, "e")]); - buffer.end_transaction_at(now).unwrap(); - assert_eq!(buffer.text(), "12cde6"); - - buffer.start_transaction_at(now); - buffer.edit([(0..1, "a")]); - buffer.edit([(1..1, "b")]); - buffer.end_transaction_at(now).unwrap(); - assert_eq!(buffer.text(), "ab2cde6"); - - buffer.undo(); - assert_eq!(buffer.text(), "12cd56"); - - buffer.undo(); - assert_eq!(buffer.text(), "123456"); - - buffer.redo(); - assert_eq!(buffer.text(), "12cd56"); - - buffer.redo(); - assert_eq!(buffer.text(), "ab2cde6"); -} - -#[test] -fn test_edited_ranges_for_transaction() { - let now = Instant::now(); - let mut buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), "1234567"); - - buffer.start_transaction_at(now); - buffer.edit([(2..4, "cd")]); - buffer.edit([(6..6, "efg")]); - buffer.end_transaction_at(now); - assert_eq!(buffer.text(), "12cd56efg7"); - - let tx = buffer.finalize_last_transaction().unwrap().clone(); - assert_eq!( - buffer - .edited_ranges_for_transaction::(&tx) - .collect::>(), - [2..4, 6..9] - ); - - buffer.edit([(5..5, "hijk")]); - assert_eq!(buffer.text(), "12cd5hijk6efg7"); - assert_eq!( - buffer - .edited_ranges_for_transaction::(&tx) - .collect::>(), - [2..4, 10..13] - ); - - buffer.edit([(4..4, "l")]); - assert_eq!(buffer.text(), "12cdl5hijk6efg7"); - assert_eq!( - buffer - .edited_ranges_for_transaction::(&tx) - .collect::>(), - [2..4, 11..14] - ); -} - -#[test] -fn test_concurrent_edits() { - let text = "abcdef"; - - let mut buffer1 = Buffer::new(ReplicaId::new(1), BufferId::new(1).unwrap(), text); - let mut buffer2 = Buffer::new(ReplicaId::new(2), BufferId::new(1).unwrap(), text); - let mut buffer3 = Buffer::new(ReplicaId::new(3), BufferId::new(1).unwrap(), text); - - let buf1_op = buffer1.edit([(1..2, "12")]); - assert_eq!(buffer1.text(), "a12cdef"); - let buf2_op = buffer2.edit([(3..4, "34")]); - assert_eq!(buffer2.text(), "abc34ef"); - let buf3_op = buffer3.edit([(5..6, "56")]); - assert_eq!(buffer3.text(), "abcde56"); - - buffer1.apply_op(buf2_op.clone()); - buffer1.apply_op(buf3_op.clone()); - buffer2.apply_op(buf1_op.clone()); - buffer2.apply_op(buf3_op); - buffer3.apply_op(buf1_op); - buffer3.apply_op(buf2_op); - - assert_eq!(buffer1.text(), "a12c34e56"); - assert_eq!(buffer2.text(), "a12c34e56"); - assert_eq!(buffer3.text(), "a12c34e56"); -} - -#[gpui::test(iterations = 100)] -fn test_random_concurrent_edits(mut rng: StdRng) { - let peers = env::var("PEERS") - .map(|i| i.parse().expect("invalid `PEERS` variable")) - .unwrap_or(5); - let operations = env::var("OPERATIONS") - .map(|i| i.parse().expect("invalid `OPERATIONS` variable")) - .unwrap_or(10); - - let base_text_len = rng.random_range(0..10); - let base_text = RandomCharIter::new(&mut rng) - .take(base_text_len) - .collect::(); - let mut replica_ids = Vec::new(); - let mut buffers = Vec::new(); - let mut network = Network::new(rng.clone()); - - for i in 0..peers { - let mut buffer = Buffer::new( - ReplicaId::new(i as u16), - BufferId::new(1).unwrap(), - base_text.clone(), - ); - buffer.history.group_interval = Duration::from_millis(rng.random_range(0..=200)); - buffers.push(buffer); - replica_ids.push(ReplicaId::new(i as u16)); - network.add_peer(ReplicaId::new(i as u16)); - } - - log::info!("initial text: {:?}", base_text); - - let mut mutation_count = operations; - loop { - let replica_index = rng.random_range(0..peers); - let replica_id = replica_ids[replica_index]; - let buffer = &mut buffers[replica_index]; - match rng.random_range(0..=100) { - 0..=50 if mutation_count != 0 => { - let op = buffer.randomly_edit(&mut rng, 5).1; - network.broadcast(buffer.replica_id, vec![op]); - log::info!("buffer {:?} text: {:?}", buffer.replica_id, buffer.text()); - mutation_count -= 1; - } - 51..=70 if mutation_count != 0 => { - let ops = buffer.randomly_undo_redo(&mut rng); - network.broadcast(buffer.replica_id, ops); - mutation_count -= 1; - } - 71..=100 if network.has_unreceived(replica_id) => { - let ops = network.receive(replica_id); - if !ops.is_empty() { - log::info!( - "peer {:?} applying {} ops from the network.", - replica_id, - ops.len() - ); - buffer.apply_ops(ops); - } - } - _ => {} - } - buffer.check_invariants(); - - if mutation_count == 0 && network.is_idle() { - break; - } - } - - let first_buffer = &buffers[0]; - for buffer in &buffers[1..] { - assert_eq!( - buffer.text(), - first_buffer.text(), - "Replica {:?} text != Replica 0 text", - buffer.replica_id - ); - buffer.check_invariants(); - } -} diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs deleted file mode 100644 index 866552e4e5..0000000000 --- a/crates/text/src/text.rs +++ /dev/null @@ -1,3564 +0,0 @@ -mod anchor; -pub mod locator; -#[cfg(any(test, feature = "test-support"))] -pub mod network; -pub mod operation_queue; -mod patch; -mod selection; -pub mod subscription; -#[cfg(test)] -mod tests; -mod undo_map; - -pub use anchor::*; -use anyhow::{Context as _, Result}; -use clock::Lamport; -pub use clock::ReplicaId; -use collections::{HashMap, HashSet}; -use locator::Locator; -use operation_queue::OperationQueue; -pub use patch::Patch; -use postage::{oneshot, prelude::*}; - -use regex::Regex; -pub use rope::*; -pub use selection::*; -use std::{ - borrow::Cow, - cmp::{self, Ordering, Reverse}, - fmt::Display, - future::Future, - iter::Iterator, - num::NonZeroU64, - ops::{self, Deref, Range, Sub}, - str, - sync::{Arc, LazyLock}, - time::{Duration, Instant}, -}; -pub use subscription::*; -pub use sum_tree::Bias; -use sum_tree::{Dimensions, FilterCursor, SumTree, TreeMap, TreeSet}; -use undo_map::UndoMap; -use util::debug_panic; - -#[cfg(any(test, feature = "test-support"))] -use util::RandomCharIter; - -static LINE_SEPARATORS_REGEX: LazyLock = - LazyLock::new(|| Regex::new(r"\r\n|\r").expect("Failed to create LINE_SEPARATORS_REGEX")); - -pub type TransactionId = clock::Lamport; - -pub struct Buffer { - snapshot: BufferSnapshot, - history: History, - deferred_ops: OperationQueue, - deferred_replicas: HashSet, - pub lamport_clock: clock::Lamport, - subscriptions: Topic, - edit_id_resolvers: HashMap>>, - wait_for_version_txs: Vec<(clock::Global, oneshot::Sender<()>)>, -} - -#[repr(transparent)] -#[derive(Clone, Copy, Debug, Hash, PartialEq, PartialOrd, Ord, Eq)] -pub struct BufferId(NonZeroU64); - -impl Display for BufferId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl From for BufferId { - fn from(id: NonZeroU64) -> Self { - BufferId(id) - } -} - -impl BufferId { - /// Returns Err if `id` is outside of BufferId domain. - pub fn new(id: u64) -> anyhow::Result { - let id = NonZeroU64::new(id).context("Buffer id cannot be 0.")?; - Ok(Self(id)) - } - - /// Increments this buffer id, returning the old value. - /// So that's a post-increment operator in disguise. - pub fn next(&mut self) -> Self { - let old = *self; - self.0 = self.0.saturating_add(1); - old - } - - pub fn to_proto(self) -> u64 { - self.into() - } -} - -impl From for u64 { - fn from(id: BufferId) -> Self { - id.0.get() - } -} - -#[derive(Clone)] -pub struct BufferSnapshot { - replica_id: ReplicaId, - remote_id: BufferId, - visible_text: Rope, - deleted_text: Rope, - line_ending: LineEnding, - undo_map: UndoMap, - fragments: SumTree, - insertions: SumTree, - insertion_slices: TreeSet, - pub version: clock::Global, -} - -#[derive(Clone, Debug)] -pub struct HistoryEntry { - transaction: Transaction, - first_edit_at: Instant, - last_edit_at: Instant, - suppress_grouping: bool, -} - -#[derive(Clone, Debug)] -pub struct Transaction { - pub id: TransactionId, - pub edit_ids: Vec, - pub start: clock::Global, -} - -impl Transaction { - pub fn merge_in(&mut self, other: Transaction) { - self.edit_ids.extend(other.edit_ids); - } -} - -impl HistoryEntry { - pub fn transaction_id(&self) -> TransactionId { - self.transaction.id - } -} - -struct History { - base_text: Rope, - operations: TreeMap, - undo_stack: Vec, - redo_stack: Vec, - transaction_depth: usize, - group_interval: Duration, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -struct InsertionSlice { - edit_id: clock::Lamport, - insertion_id: clock::Lamport, - range: Range, -} - -impl Ord for InsertionSlice { - fn cmp(&self, other: &Self) -> Ordering { - self.edit_id - .cmp(&other.edit_id) - .then_with(|| self.insertion_id.cmp(&other.insertion_id)) - .then_with(|| self.range.start.cmp(&other.range.start)) - .then_with(|| self.range.end.cmp(&other.range.end)) - } -} - -impl PartialOrd for InsertionSlice { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl InsertionSlice { - fn from_fragment(edit_id: clock::Lamport, fragment: &Fragment) -> Self { - Self { - edit_id, - insertion_id: fragment.timestamp, - range: fragment.insertion_offset..fragment.insertion_offset + fragment.len, - } - } -} - -impl History { - pub fn new(base_text: Rope) -> Self { - Self { - base_text, - operations: Default::default(), - undo_stack: Vec::new(), - redo_stack: Vec::new(), - transaction_depth: 0, - // Don't group transactions in tests unless we opt in, because it's a footgun. - #[cfg(any(test, feature = "test-support"))] - group_interval: Duration::ZERO, - #[cfg(not(any(test, feature = "test-support")))] - group_interval: Duration::from_millis(300), - } - } - - fn push(&mut self, op: Operation) { - self.operations.insert(op.timestamp(), op); - } - - fn start_transaction( - &mut self, - start: clock::Global, - now: Instant, - clock: &mut clock::Lamport, - ) -> Option { - self.transaction_depth += 1; - if self.transaction_depth == 1 { - let id = clock.tick(); - self.undo_stack.push(HistoryEntry { - transaction: Transaction { - id, - start, - edit_ids: Default::default(), - }, - first_edit_at: now, - last_edit_at: now, - suppress_grouping: false, - }); - Some(id) - } else { - None - } - } - - fn end_transaction(&mut self, now: Instant) -> Option<&HistoryEntry> { - assert_ne!(self.transaction_depth, 0); - self.transaction_depth -= 1; - if self.transaction_depth == 0 { - if self - .undo_stack - .last() - .unwrap() - .transaction - .edit_ids - .is_empty() - { - self.undo_stack.pop(); - None - } else { - self.redo_stack.clear(); - let entry = self.undo_stack.last_mut().unwrap(); - entry.last_edit_at = now; - Some(entry) - } - } else { - None - } - } - - fn group(&mut self) -> Option { - let mut count = 0; - let mut entries = self.undo_stack.iter(); - if let Some(mut entry) = entries.next_back() { - while let Some(prev_entry) = entries.next_back() { - if !prev_entry.suppress_grouping - && entry.first_edit_at - prev_entry.last_edit_at < self.group_interval - { - entry = prev_entry; - count += 1; - } else { - break; - } - } - } - self.group_trailing(count) - } - - fn group_until(&mut self, transaction_id: TransactionId) { - let mut count = 0; - for entry in self.undo_stack.iter().rev() { - if entry.transaction_id() == transaction_id { - self.group_trailing(count); - break; - } else if entry.suppress_grouping { - break; - } else { - count += 1; - } - } - } - - fn group_trailing(&mut self, n: usize) -> Option { - let new_len = self.undo_stack.len() - n; - let (entries_to_keep, entries_to_merge) = self.undo_stack.split_at_mut(new_len); - if let Some(last_entry) = entries_to_keep.last_mut() { - for entry in &*entries_to_merge { - for edit_id in &entry.transaction.edit_ids { - last_entry.transaction.edit_ids.push(*edit_id); - } - } - - if let Some(entry) = entries_to_merge.last_mut() { - last_entry.last_edit_at = entry.last_edit_at; - } - } - - self.undo_stack.truncate(new_len); - self.undo_stack.last().map(|e| e.transaction.id) - } - - fn finalize_last_transaction(&mut self) -> Option<&Transaction> { - self.undo_stack.last_mut().map(|entry| { - entry.suppress_grouping = true; - &entry.transaction - }) - } - - fn push_transaction(&mut self, transaction: Transaction, now: Instant) { - assert_eq!(self.transaction_depth, 0); - self.undo_stack.push(HistoryEntry { - transaction, - first_edit_at: now, - last_edit_at: now, - suppress_grouping: false, - }); - } - - /// Differs from `push_transaction` in that it does not clear the redo - /// stack. Intended to be used to create a parent transaction to merge - /// potential child transactions into. - /// - /// The caller is responsible for removing it from the undo history using - /// `forget_transaction` if no edits are merged into it. Otherwise, if edits - /// are merged into this transaction, the caller is responsible for ensuring - /// the redo stack is cleared. The easiest way to ensure the redo stack is - /// cleared is to create transactions with the usual `start_transaction` and - /// `end_transaction` methods and merging the resulting transactions into - /// the transaction created by this method - fn push_empty_transaction( - &mut self, - start: clock::Global, - now: Instant, - clock: &mut clock::Lamport, - ) -> TransactionId { - assert_eq!(self.transaction_depth, 0); - let id = clock.tick(); - let transaction = Transaction { - id, - start, - edit_ids: Vec::new(), - }; - self.undo_stack.push(HistoryEntry { - transaction, - first_edit_at: now, - last_edit_at: now, - suppress_grouping: false, - }); - id - } - - fn push_undo(&mut self, op_id: clock::Lamport) { - assert_ne!(self.transaction_depth, 0); - if let Some(Operation::Edit(_)) = self.operations.get(&op_id) { - let last_transaction = self.undo_stack.last_mut().unwrap(); - last_transaction.transaction.edit_ids.push(op_id); - } - } - - fn pop_undo(&mut self) -> Option<&HistoryEntry> { - assert_eq!(self.transaction_depth, 0); - if let Some(entry) = self.undo_stack.pop() { - self.redo_stack.push(entry); - self.redo_stack.last() - } else { - None - } - } - - fn remove_from_undo(&mut self, transaction_id: TransactionId) -> Option<&HistoryEntry> { - assert_eq!(self.transaction_depth, 0); - - let entry_ix = self - .undo_stack - .iter() - .rposition(|entry| entry.transaction.id == transaction_id)?; - let entry = self.undo_stack.remove(entry_ix); - self.redo_stack.push(entry); - self.redo_stack.last() - } - - fn remove_from_undo_until(&mut self, transaction_id: TransactionId) -> &[HistoryEntry] { - assert_eq!(self.transaction_depth, 0); - - let redo_stack_start_len = self.redo_stack.len(); - if let Some(entry_ix) = self - .undo_stack - .iter() - .rposition(|entry| entry.transaction.id == transaction_id) - { - self.redo_stack - .extend(self.undo_stack.drain(entry_ix..).rev()); - } - &self.redo_stack[redo_stack_start_len..] - } - - fn forget(&mut self, transaction_id: TransactionId) -> Option { - assert_eq!(self.transaction_depth, 0); - if let Some(entry_ix) = self - .undo_stack - .iter() - .rposition(|entry| entry.transaction.id == transaction_id) - { - Some(self.undo_stack.remove(entry_ix).transaction) - } else if let Some(entry_ix) = self - .redo_stack - .iter() - .rposition(|entry| entry.transaction.id == transaction_id) - { - Some(self.redo_stack.remove(entry_ix).transaction) - } else { - None - } - } - - fn transaction(&self, transaction_id: TransactionId) -> Option<&Transaction> { - let entry = self - .undo_stack - .iter() - .rfind(|entry| entry.transaction.id == transaction_id) - .or_else(|| { - self.redo_stack - .iter() - .rfind(|entry| entry.transaction.id == transaction_id) - })?; - Some(&entry.transaction) - } - - fn transaction_mut(&mut self, transaction_id: TransactionId) -> Option<&mut Transaction> { - let entry = self - .undo_stack - .iter_mut() - .rfind(|entry| entry.transaction.id == transaction_id) - .or_else(|| { - self.redo_stack - .iter_mut() - .rfind(|entry| entry.transaction.id == transaction_id) - })?; - Some(&mut entry.transaction) - } - - fn merge_transactions(&mut self, transaction: TransactionId, destination: TransactionId) { - if let Some(transaction) = self.forget(transaction) - && let Some(destination) = self.transaction_mut(destination) - { - destination.edit_ids.extend(transaction.edit_ids); - } - } - - fn pop_redo(&mut self) -> Option<&HistoryEntry> { - assert_eq!(self.transaction_depth, 0); - if let Some(entry) = self.redo_stack.pop() { - self.undo_stack.push(entry); - self.undo_stack.last() - } else { - None - } - } - - fn remove_from_redo(&mut self, transaction_id: TransactionId) -> &[HistoryEntry] { - assert_eq!(self.transaction_depth, 0); - - let undo_stack_start_len = self.undo_stack.len(); - if let Some(entry_ix) = self - .redo_stack - .iter() - .rposition(|entry| entry.transaction.id == transaction_id) - { - self.undo_stack - .extend(self.redo_stack.drain(entry_ix..).rev()); - } - &self.undo_stack[undo_stack_start_len..] - } -} - -struct Edits<'a, D: TextDimension, F: FnMut(&FragmentSummary) -> bool> { - visible_cursor: rope::Cursor<'a>, - deleted_cursor: rope::Cursor<'a>, - fragments_cursor: Option>, - undos: &'a UndoMap, - since: &'a clock::Global, - old_end: D, - new_end: D, - range: Range<(&'a Locator, usize)>, - buffer_id: BufferId, -} - -#[derive(Clone, Debug, Default, Eq, PartialEq)] -pub struct Edit { - pub old: Range, - pub new: Range, -} -impl Edit -where - D: PartialEq, -{ - pub fn is_empty(&self) -> bool { - self.old.start == self.old.end && self.new.start == self.new.end - } -} - -impl Edit -where - D: Sub + Copy, -{ - pub fn old_len(&self) -> DDelta { - self.old.end - self.old.start - } - - pub fn new_len(&self) -> DDelta { - self.new.end - self.new.start - } -} - -impl Edit<(D1, D2)> { - pub fn flatten(self) -> (Edit, Edit) { - ( - Edit { - old: self.old.start.0..self.old.end.0, - new: self.new.start.0..self.new.end.0, - }, - Edit { - old: self.old.start.1..self.old.end.1, - new: self.new.start.1..self.new.end.1, - }, - ) - } -} - -#[derive(Eq, PartialEq, Clone, Debug)] -pub struct Fragment { - pub id: Locator, - pub timestamp: clock::Lamport, - pub insertion_offset: usize, - pub len: usize, - pub visible: bool, - pub deletions: HashSet, - pub max_undos: clock::Global, -} - -#[derive(Eq, PartialEq, Clone, Debug)] -pub struct FragmentSummary { - text: FragmentTextSummary, - max_id: Locator, - max_version: clock::Global, - min_insertion_version: clock::Global, - max_insertion_version: clock::Global, -} - -#[derive(Copy, Default, Clone, Debug, PartialEq, Eq)] -struct FragmentTextSummary { - visible: usize, - deleted: usize, -} - -impl<'a> sum_tree::Dimension<'a, FragmentSummary> for FragmentTextSummary { - fn zero(_: &Option) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a FragmentSummary, _: &Option) { - self.visible += summary.text.visible; - self.deleted += summary.text.deleted; - } -} - -#[derive(Eq, PartialEq, Clone, Debug)] -struct InsertionFragment { - timestamp: clock::Lamport, - split_offset: usize, - fragment_id: Locator, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -struct InsertionFragmentKey { - timestamp: clock::Lamport, - split_offset: usize, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum Operation { - Edit(EditOperation), - Undo(UndoOperation), -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct EditOperation { - pub timestamp: clock::Lamport, - pub version: clock::Global, - pub ranges: Vec>, - pub new_text: Vec>, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct UndoOperation { - pub timestamp: clock::Lamport, - pub version: clock::Global, - pub counts: HashMap, -} - -/// Stores information about the indentation of a line (tabs and spaces). -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct LineIndent { - pub tabs: u32, - pub spaces: u32, - pub line_blank: bool, -} - -impl LineIndent { - pub fn from_chunks(chunks: &mut Chunks) -> Self { - let mut tabs = 0; - let mut spaces = 0; - let mut line_blank = true; - - 'outer: while let Some(chunk) = chunks.peek() { - for ch in chunk.chars() { - if ch == '\t' { - tabs += 1; - } else if ch == ' ' { - spaces += 1; - } else { - if ch != '\n' { - line_blank = false; - } - break 'outer; - } - } - - chunks.next(); - } - - Self { - tabs, - spaces, - line_blank, - } - } - - /// Constructs a new `LineIndent` which only contains spaces. - pub fn spaces(spaces: u32) -> Self { - Self { - tabs: 0, - spaces, - line_blank: true, - } - } - - /// Constructs a new `LineIndent` which only contains tabs. - pub fn tabs(tabs: u32) -> Self { - Self { - tabs, - spaces: 0, - line_blank: true, - } - } - - /// Indicates whether the line is empty. - pub fn is_line_empty(&self) -> bool { - self.tabs == 0 && self.spaces == 0 && self.line_blank - } - - /// Indicates whether the line is blank (contains only whitespace). - pub fn is_line_blank(&self) -> bool { - self.line_blank - } - - /// Returns the number of indentation characters (tabs or spaces). - pub fn raw_len(&self) -> u32 { - self.tabs + self.spaces - } - - /// Returns the number of indentation characters (tabs or spaces), taking tab size into account. - pub fn len(&self, tab_size: u32) -> u32 { - self.tabs * tab_size + self.spaces - } -} - -impl From<&str> for LineIndent { - fn from(value: &str) -> Self { - Self::from_iter(value.chars()) - } -} - -impl FromIterator for LineIndent { - fn from_iter>(chars: T) -> Self { - let mut tabs = 0; - let mut spaces = 0; - let mut line_blank = true; - for c in chars { - if c == '\t' { - tabs += 1; - } else if c == ' ' { - spaces += 1; - } else { - if c != '\n' { - line_blank = false; - } - break; - } - } - Self { - tabs, - spaces, - line_blank, - } - } -} - -impl Buffer { - pub fn new(replica_id: ReplicaId, remote_id: BufferId, base_text: impl Into) -> Buffer { - let mut base_text = base_text.into(); - let line_ending = LineEnding::detect(&base_text); - LineEnding::normalize(&mut base_text); - Self::new_normalized(replica_id, remote_id, line_ending, Rope::from(&*base_text)) - } - - pub fn new_normalized( - replica_id: ReplicaId, - remote_id: BufferId, - line_ending: LineEnding, - normalized: Rope, - ) -> Buffer { - let history = History::new(normalized); - let mut fragments = SumTree::new(&None); - let mut insertions = SumTree::default(); - - let mut lamport_clock = clock::Lamport::new(replica_id); - let mut version = clock::Global::new(); - - let visible_text = history.base_text.clone(); - if !visible_text.is_empty() { - let insertion_timestamp = clock::Lamport::new(ReplicaId::LOCAL); - lamport_clock.observe(insertion_timestamp); - version.observe(insertion_timestamp); - let fragment_id = Locator::between(&Locator::min(), &Locator::max()); - let fragment = Fragment { - id: fragment_id, - timestamp: insertion_timestamp, - insertion_offset: 0, - len: visible_text.len(), - visible: true, - deletions: Default::default(), - max_undos: Default::default(), - }; - insertions.push(InsertionFragment::new(&fragment), ()); - fragments.push(fragment, &None); - } - - Buffer { - snapshot: BufferSnapshot { - replica_id, - remote_id, - visible_text, - deleted_text: Rope::new(), - line_ending, - fragments, - insertions, - version, - undo_map: Default::default(), - insertion_slices: Default::default(), - }, - history, - deferred_ops: OperationQueue::new(), - deferred_replicas: HashSet::default(), - lamport_clock, - subscriptions: Default::default(), - edit_id_resolvers: Default::default(), - wait_for_version_txs: Default::default(), - } - } - - pub fn version(&self) -> clock::Global { - self.version.clone() - } - - pub fn snapshot(&self) -> BufferSnapshot { - self.snapshot.clone() - } - - pub fn branch(&self) -> Self { - Self { - snapshot: self.snapshot.clone(), - history: History::new(self.base_text().clone()), - deferred_ops: OperationQueue::new(), - deferred_replicas: HashSet::default(), - lamport_clock: clock::Lamport::new(ReplicaId::LOCAL_BRANCH), - subscriptions: Default::default(), - edit_id_resolvers: Default::default(), - wait_for_version_txs: Default::default(), - } - } - - pub fn replica_id(&self) -> ReplicaId { - self.lamport_clock.replica_id - } - - pub fn remote_id(&self) -> BufferId { - self.remote_id - } - - pub fn deferred_ops_len(&self) -> usize { - self.deferred_ops.len() - } - - pub fn transaction_group_interval(&self) -> Duration { - self.history.group_interval - } - - pub fn edit(&mut self, edits: R) -> Operation - where - R: IntoIterator, - I: ExactSizeIterator, T)>, - S: ToOffset, - T: Into>, - { - let edits = edits - .into_iter() - .map(|(range, new_text)| (range, new_text.into())); - - self.start_transaction(); - let timestamp = self.lamport_clock.tick(); - let operation = Operation::Edit(self.apply_local_edit(edits, timestamp)); - - self.history.push(operation.clone()); - self.history.push_undo(operation.timestamp()); - self.snapshot.version.observe(operation.timestamp()); - self.end_transaction(); - operation - } - - fn apply_local_edit>>( - &mut self, - edits: impl ExactSizeIterator, T)>, - timestamp: clock::Lamport, - ) -> EditOperation { - let mut edits_patch = Patch::default(); - let mut edit_op = EditOperation { - timestamp, - version: self.version(), - ranges: Vec::with_capacity(edits.len()), - new_text: Vec::with_capacity(edits.len()), - }; - let mut new_insertions = Vec::new(); - let mut insertion_offset = 0; - let mut insertion_slices = Vec::new(); - - let mut edits = edits - .map(|(range, new_text)| (range.to_offset(&*self), new_text)) - .peekable(); - - let mut new_ropes = - RopeBuilder::new(self.visible_text.cursor(0), self.deleted_text.cursor(0)); - let mut old_fragments = self.fragments.cursor::(&None); - let mut new_fragments = old_fragments.slice(&edits.peek().unwrap().0.start, Bias::Right); - new_ropes.append(new_fragments.summary().text); - - let mut fragment_start = old_fragments.start().visible; - for (range, new_text) in edits { - let new_text = LineEnding::normalize_arc(new_text.into()); - let fragment_end = old_fragments.end().visible; - - // If the current fragment ends before this range, then jump ahead to the first fragment - // that extends past the start of this range, reusing any intervening fragments. - if fragment_end < range.start { - // If the current fragment has been partially consumed, then consume the rest of it - // and advance to the next fragment before slicing. - if fragment_start > old_fragments.start().visible { - if fragment_end > fragment_start { - let mut suffix = old_fragments.item().unwrap().clone(); - suffix.len = fragment_end - fragment_start; - suffix.insertion_offset += fragment_start - old_fragments.start().visible; - new_insertions.push(InsertionFragment::insert_new(&suffix)); - new_ropes.push_fragment(&suffix, suffix.visible); - new_fragments.push(suffix, &None); - } - old_fragments.next(); - } - - let slice = old_fragments.slice(&range.start, Bias::Right); - new_ropes.append(slice.summary().text); - new_fragments.append(slice, &None); - fragment_start = old_fragments.start().visible; - } - - let full_range_start = FullOffset(range.start + old_fragments.start().deleted); - - // Preserve any portion of the current fragment that precedes this range. - if fragment_start < range.start { - let mut prefix = old_fragments.item().unwrap().clone(); - prefix.len = range.start - fragment_start; - prefix.insertion_offset += fragment_start - old_fragments.start().visible; - prefix.id = Locator::between(&new_fragments.summary().max_id, &prefix.id); - new_insertions.push(InsertionFragment::insert_new(&prefix)); - new_ropes.push_fragment(&prefix, prefix.visible); - new_fragments.push(prefix, &None); - fragment_start = range.start; - } - - // Insert the new text before any existing fragments within the range. - if !new_text.is_empty() { - let new_start = new_fragments.summary().text.visible; - - let fragment = Fragment { - id: Locator::between( - &new_fragments.summary().max_id, - old_fragments - .item() - .map_or(&Locator::max(), |old_fragment| &old_fragment.id), - ), - timestamp, - insertion_offset, - len: new_text.len(), - deletions: Default::default(), - max_undos: Default::default(), - visible: true, - }; - edits_patch.push(Edit { - old: fragment_start..fragment_start, - new: new_start..new_start + new_text.len(), - }); - insertion_slices.push(InsertionSlice::from_fragment(timestamp, &fragment)); - new_insertions.push(InsertionFragment::insert_new(&fragment)); - new_ropes.push_str(new_text.as_ref()); - new_fragments.push(fragment, &None); - insertion_offset += new_text.len(); - } - - // Advance through every fragment that intersects this range, marking the intersecting - // portions as deleted. - while fragment_start < range.end { - let fragment = old_fragments.item().unwrap(); - let fragment_end = old_fragments.end().visible; - let mut intersection = fragment.clone(); - let intersection_end = cmp::min(range.end, fragment_end); - if fragment.visible { - intersection.len = intersection_end - fragment_start; - intersection.insertion_offset += fragment_start - old_fragments.start().visible; - intersection.id = - Locator::between(&new_fragments.summary().max_id, &intersection.id); - intersection.deletions.insert(timestamp); - intersection.visible = false; - } - if intersection.len > 0 { - if fragment.visible && !intersection.visible { - let new_start = new_fragments.summary().text.visible; - edits_patch.push(Edit { - old: fragment_start..intersection_end, - new: new_start..new_start, - }); - insertion_slices - .push(InsertionSlice::from_fragment(timestamp, &intersection)); - } - new_insertions.push(InsertionFragment::insert_new(&intersection)); - new_ropes.push_fragment(&intersection, fragment.visible); - new_fragments.push(intersection, &None); - fragment_start = intersection_end; - } - if fragment_end <= range.end { - old_fragments.next(); - } - } - - let full_range_end = FullOffset(range.end + old_fragments.start().deleted); - edit_op.ranges.push(full_range_start..full_range_end); - edit_op.new_text.push(new_text); - } - - // If the current fragment has been partially consumed, then consume the rest of it - // and advance to the next fragment before slicing. - if fragment_start > old_fragments.start().visible { - let fragment_end = old_fragments.end().visible; - if fragment_end > fragment_start { - let mut suffix = old_fragments.item().unwrap().clone(); - suffix.len = fragment_end - fragment_start; - suffix.insertion_offset += fragment_start - old_fragments.start().visible; - new_insertions.push(InsertionFragment::insert_new(&suffix)); - new_ropes.push_fragment(&suffix, suffix.visible); - new_fragments.push(suffix, &None); - } - old_fragments.next(); - } - - let suffix = old_fragments.suffix(); - new_ropes.append(suffix.summary().text); - new_fragments.append(suffix, &None); - let (visible_text, deleted_text) = new_ropes.finish(); - drop(old_fragments); - - self.snapshot.fragments = new_fragments; - self.snapshot.insertions.edit(new_insertions, ()); - self.snapshot.visible_text = visible_text; - self.snapshot.deleted_text = deleted_text; - self.subscriptions.publish_mut(&edits_patch); - self.snapshot.insertion_slices.extend(insertion_slices); - edit_op - } - - pub fn set_line_ending(&mut self, line_ending: LineEnding) { - self.snapshot.line_ending = line_ending; - } - - pub fn apply_ops>(&mut self, ops: I) { - let mut deferred_ops = Vec::new(); - for op in ops { - self.history.push(op.clone()); - if self.can_apply_op(&op) { - self.apply_op(op); - } else { - self.deferred_replicas.insert(op.replica_id()); - deferred_ops.push(op); - } - } - self.deferred_ops.insert(deferred_ops); - self.flush_deferred_ops(); - } - - fn apply_op(&mut self, op: Operation) { - match op { - Operation::Edit(edit) => { - if !self.version.observed(edit.timestamp) { - self.apply_remote_edit( - &edit.version, - &edit.ranges, - &edit.new_text, - edit.timestamp, - ); - self.snapshot.version.observe(edit.timestamp); - self.lamport_clock.observe(edit.timestamp); - self.resolve_edit(edit.timestamp); - } - } - Operation::Undo(undo) => { - if !self.version.observed(undo.timestamp) { - self.apply_undo(&undo); - self.snapshot.version.observe(undo.timestamp); - self.lamport_clock.observe(undo.timestamp); - } - } - } - self.wait_for_version_txs.retain_mut(|(version, tx)| { - if self.snapshot.version().observed_all(version) { - tx.try_send(()).ok(); - false - } else { - true - } - }); - } - - fn apply_remote_edit( - &mut self, - version: &clock::Global, - ranges: &[Range], - new_text: &[Arc], - timestamp: clock::Lamport, - ) { - if ranges.is_empty() { - return; - } - - let edits = ranges.iter().zip(new_text.iter()); - let mut edits_patch = Patch::default(); - let mut insertion_slices = Vec::new(); - let cx = Some(version.clone()); - let mut new_insertions = Vec::new(); - let mut insertion_offset = 0; - let mut new_ropes = - RopeBuilder::new(self.visible_text.cursor(0), self.deleted_text.cursor(0)); - let mut old_fragments = self - .fragments - .cursor::>(&cx); - let mut new_fragments = - old_fragments.slice(&VersionedFullOffset::Offset(ranges[0].start), Bias::Left); - new_ropes.append(new_fragments.summary().text); - - let mut fragment_start = old_fragments.start().0.full_offset(); - for (range, new_text) in edits { - let fragment_end = old_fragments.end().0.full_offset(); - - // If the current fragment ends before this range, then jump ahead to the first fragment - // that extends past the start of this range, reusing any intervening fragments. - if fragment_end < range.start { - // If the current fragment has been partially consumed, then consume the rest of it - // and advance to the next fragment before slicing. - if fragment_start > old_fragments.start().0.full_offset() { - if fragment_end > fragment_start { - let mut suffix = old_fragments.item().unwrap().clone(); - suffix.len = fragment_end.0 - fragment_start.0; - suffix.insertion_offset += - fragment_start - old_fragments.start().0.full_offset(); - new_insertions.push(InsertionFragment::insert_new(&suffix)); - new_ropes.push_fragment(&suffix, suffix.visible); - new_fragments.push(suffix, &None); - } - old_fragments.next(); - } - - let slice = - old_fragments.slice(&VersionedFullOffset::Offset(range.start), Bias::Left); - new_ropes.append(slice.summary().text); - new_fragments.append(slice, &None); - fragment_start = old_fragments.start().0.full_offset(); - } - - // If we are at the end of a non-concurrent fragment, advance to the next one. - let fragment_end = old_fragments.end().0.full_offset(); - if fragment_end == range.start && fragment_end > fragment_start { - let mut fragment = old_fragments.item().unwrap().clone(); - fragment.len = fragment_end.0 - fragment_start.0; - fragment.insertion_offset += fragment_start - old_fragments.start().0.full_offset(); - new_insertions.push(InsertionFragment::insert_new(&fragment)); - new_ropes.push_fragment(&fragment, fragment.visible); - new_fragments.push(fragment, &None); - old_fragments.next(); - fragment_start = old_fragments.start().0.full_offset(); - } - - // Skip over insertions that are concurrent to this edit, but have a lower lamport - // timestamp. - while let Some(fragment) = old_fragments.item() { - if fragment_start == range.start && fragment.timestamp > timestamp { - new_ropes.push_fragment(fragment, fragment.visible); - new_fragments.push(fragment.clone(), &None); - old_fragments.next(); - debug_assert_eq!(fragment_start, range.start); - } else { - break; - } - } - debug_assert!(fragment_start <= range.start); - - // Preserve any portion of the current fragment that precedes this range. - if fragment_start < range.start { - let mut prefix = old_fragments.item().unwrap().clone(); - prefix.len = range.start.0 - fragment_start.0; - prefix.insertion_offset += fragment_start - old_fragments.start().0.full_offset(); - prefix.id = Locator::between(&new_fragments.summary().max_id, &prefix.id); - new_insertions.push(InsertionFragment::insert_new(&prefix)); - fragment_start = range.start; - new_ropes.push_fragment(&prefix, prefix.visible); - new_fragments.push(prefix, &None); - } - - // Insert the new text before any existing fragments within the range. - if !new_text.is_empty() { - let mut old_start = old_fragments.start().1; - if old_fragments.item().is_some_and(|f| f.visible) { - old_start += fragment_start.0 - old_fragments.start().0.full_offset().0; - } - let new_start = new_fragments.summary().text.visible; - let fragment = Fragment { - id: Locator::between( - &new_fragments.summary().max_id, - old_fragments - .item() - .map_or(&Locator::max(), |old_fragment| &old_fragment.id), - ), - timestamp, - insertion_offset, - len: new_text.len(), - deletions: Default::default(), - max_undos: Default::default(), - visible: true, - }; - edits_patch.push(Edit { - old: old_start..old_start, - new: new_start..new_start + new_text.len(), - }); - insertion_slices.push(InsertionSlice::from_fragment(timestamp, &fragment)); - new_insertions.push(InsertionFragment::insert_new(&fragment)); - new_ropes.push_str(new_text); - new_fragments.push(fragment, &None); - insertion_offset += new_text.len(); - } - - // Advance through every fragment that intersects this range, marking the intersecting - // portions as deleted. - while fragment_start < range.end { - let fragment = old_fragments.item().unwrap(); - let fragment_end = old_fragments.end().0.full_offset(); - let mut intersection = fragment.clone(); - let intersection_end = cmp::min(range.end, fragment_end); - if fragment.was_visible(version, &self.undo_map) { - intersection.len = intersection_end.0 - fragment_start.0; - intersection.insertion_offset += - fragment_start - old_fragments.start().0.full_offset(); - intersection.id = - Locator::between(&new_fragments.summary().max_id, &intersection.id); - intersection.deletions.insert(timestamp); - intersection.visible = false; - insertion_slices.push(InsertionSlice::from_fragment(timestamp, &intersection)); - } - if intersection.len > 0 { - if fragment.visible && !intersection.visible { - let old_start = old_fragments.start().1 - + (fragment_start.0 - old_fragments.start().0.full_offset().0); - let new_start = new_fragments.summary().text.visible; - edits_patch.push(Edit { - old: old_start..old_start + intersection.len, - new: new_start..new_start, - }); - } - new_insertions.push(InsertionFragment::insert_new(&intersection)); - new_ropes.push_fragment(&intersection, fragment.visible); - new_fragments.push(intersection, &None); - fragment_start = intersection_end; - } - if fragment_end <= range.end { - old_fragments.next(); - } - } - } - - // If the current fragment has been partially consumed, then consume the rest of it - // and advance to the next fragment before slicing. - if fragment_start > old_fragments.start().0.full_offset() { - let fragment_end = old_fragments.end().0.full_offset(); - if fragment_end > fragment_start { - let mut suffix = old_fragments.item().unwrap().clone(); - suffix.len = fragment_end.0 - fragment_start.0; - suffix.insertion_offset += fragment_start - old_fragments.start().0.full_offset(); - new_insertions.push(InsertionFragment::insert_new(&suffix)); - new_ropes.push_fragment(&suffix, suffix.visible); - new_fragments.push(suffix, &None); - } - old_fragments.next(); - } - - let suffix = old_fragments.suffix(); - new_ropes.append(suffix.summary().text); - new_fragments.append(suffix, &None); - let (visible_text, deleted_text) = new_ropes.finish(); - drop(old_fragments); - - self.snapshot.fragments = new_fragments; - self.snapshot.visible_text = visible_text; - self.snapshot.deleted_text = deleted_text; - self.snapshot.insertions.edit(new_insertions, ()); - self.snapshot.insertion_slices.extend(insertion_slices); - self.subscriptions.publish_mut(&edits_patch) - } - - fn fragment_ids_for_edits<'a>( - &'a self, - edit_ids: impl Iterator, - ) -> Vec<&'a Locator> { - // Get all of the insertion slices changed by the given edits. - let mut insertion_slices = Vec::new(); - for edit_id in edit_ids { - let insertion_slice = InsertionSlice { - edit_id: *edit_id, - insertion_id: clock::Lamport::MIN, - range: 0..0, - }; - let slices = self - .snapshot - .insertion_slices - .iter_from(&insertion_slice) - .take_while(|slice| slice.edit_id == *edit_id); - insertion_slices.extend(slices) - } - insertion_slices - .sort_unstable_by_key(|s| (s.insertion_id, s.range.start, Reverse(s.range.end))); - - // Get all of the fragments corresponding to these insertion slices. - let mut fragment_ids = Vec::new(); - let mut insertions_cursor = self.insertions.cursor::(()); - for insertion_slice in &insertion_slices { - if insertion_slice.insertion_id != insertions_cursor.start().timestamp - || insertion_slice.range.start > insertions_cursor.start().split_offset - { - insertions_cursor.seek_forward( - &InsertionFragmentKey { - timestamp: insertion_slice.insertion_id, - split_offset: insertion_slice.range.start, - }, - Bias::Left, - ); - } - while let Some(item) = insertions_cursor.item() { - if item.timestamp != insertion_slice.insertion_id - || item.split_offset >= insertion_slice.range.end - { - break; - } - fragment_ids.push(&item.fragment_id); - insertions_cursor.next(); - } - } - fragment_ids.sort_unstable(); - fragment_ids - } - - fn apply_undo(&mut self, undo: &UndoOperation) { - self.snapshot.undo_map.insert(undo); - - let mut edits = Patch::default(); - let mut old_fragments = self - .fragments - .cursor::, usize>>(&None); - let mut new_fragments = SumTree::new(&None); - let mut new_ropes = - RopeBuilder::new(self.visible_text.cursor(0), self.deleted_text.cursor(0)); - - for fragment_id in self.fragment_ids_for_edits(undo.counts.keys()) { - let preceding_fragments = old_fragments.slice(&Some(fragment_id), Bias::Left); - new_ropes.append(preceding_fragments.summary().text); - new_fragments.append(preceding_fragments, &None); - - if let Some(fragment) = old_fragments.item() { - let mut fragment = fragment.clone(); - let fragment_was_visible = fragment.visible; - - fragment.visible = fragment.is_visible(&self.undo_map); - fragment.max_undos.observe(undo.timestamp); - - let old_start = old_fragments.start().1; - let new_start = new_fragments.summary().text.visible; - if fragment_was_visible && !fragment.visible { - edits.push(Edit { - old: old_start..old_start + fragment.len, - new: new_start..new_start, - }); - } else if !fragment_was_visible && fragment.visible { - edits.push(Edit { - old: old_start..old_start, - new: new_start..new_start + fragment.len, - }); - } - new_ropes.push_fragment(&fragment, fragment_was_visible); - new_fragments.push(fragment, &None); - - old_fragments.next(); - } - } - - let suffix = old_fragments.suffix(); - new_ropes.append(suffix.summary().text); - new_fragments.append(suffix, &None); - - drop(old_fragments); - let (visible_text, deleted_text) = new_ropes.finish(); - self.snapshot.fragments = new_fragments; - self.snapshot.visible_text = visible_text; - self.snapshot.deleted_text = deleted_text; - self.subscriptions.publish_mut(&edits); - } - - fn flush_deferred_ops(&mut self) { - self.deferred_replicas.clear(); - let mut deferred_ops = Vec::new(); - for op in self.deferred_ops.drain().iter().cloned() { - if self.can_apply_op(&op) { - self.apply_op(op); - } else { - self.deferred_replicas.insert(op.replica_id()); - deferred_ops.push(op); - } - } - self.deferred_ops.insert(deferred_ops); - } - - fn can_apply_op(&self, op: &Operation) -> bool { - if self.deferred_replicas.contains(&op.replica_id()) { - false - } else { - self.version.observed_all(match op { - Operation::Edit(edit) => &edit.version, - Operation::Undo(undo) => &undo.version, - }) - } - } - - pub fn has_deferred_ops(&self) -> bool { - !self.deferred_ops.is_empty() - } - - pub fn peek_undo_stack(&self) -> Option<&HistoryEntry> { - self.history.undo_stack.last() - } - - pub fn peek_redo_stack(&self) -> Option<&HistoryEntry> { - self.history.redo_stack.last() - } - - pub fn start_transaction(&mut self) -> Option { - self.start_transaction_at(Instant::now()) - } - - pub fn start_transaction_at(&mut self, now: Instant) -> Option { - self.history - .start_transaction(self.version.clone(), now, &mut self.lamport_clock) - } - - pub fn end_transaction(&mut self) -> Option<(TransactionId, clock::Global)> { - self.end_transaction_at(Instant::now()) - } - - pub fn end_transaction_at(&mut self, now: Instant) -> Option<(TransactionId, clock::Global)> { - if let Some(entry) = self.history.end_transaction(now) { - let since = entry.transaction.start.clone(); - let id = self.history.group().unwrap(); - Some((id, since)) - } else { - None - } - } - - pub fn finalize_last_transaction(&mut self) -> Option<&Transaction> { - self.history.finalize_last_transaction() - } - - pub fn group_until_transaction(&mut self, transaction_id: TransactionId) { - self.history.group_until(transaction_id); - } - - pub fn base_text(&self) -> &Rope { - &self.history.base_text - } - - pub fn operations(&self) -> &TreeMap { - &self.history.operations - } - - pub fn undo(&mut self) -> Option<(TransactionId, Operation)> { - if let Some(entry) = self.history.pop_undo() { - let transaction = entry.transaction.clone(); - let transaction_id = transaction.id; - let op = self.undo_or_redo(transaction); - Some((transaction_id, op)) - } else { - None - } - } - - pub fn undo_transaction(&mut self, transaction_id: TransactionId) -> Option { - let transaction = self - .history - .remove_from_undo(transaction_id)? - .transaction - .clone(); - Some(self.undo_or_redo(transaction)) - } - - pub fn undo_to_transaction(&mut self, transaction_id: TransactionId) -> Vec { - let transactions = self - .history - .remove_from_undo_until(transaction_id) - .iter() - .map(|entry| entry.transaction.clone()) - .collect::>(); - - transactions - .into_iter() - .map(|transaction| self.undo_or_redo(transaction)) - .collect() - } - - pub fn forget_transaction(&mut self, transaction_id: TransactionId) -> Option { - self.history.forget(transaction_id) - } - - pub fn get_transaction(&self, transaction_id: TransactionId) -> Option<&Transaction> { - self.history.transaction(transaction_id) - } - - pub fn merge_transactions(&mut self, transaction: TransactionId, destination: TransactionId) { - self.history.merge_transactions(transaction, destination); - } - - pub fn redo(&mut self) -> Option<(TransactionId, Operation)> { - if let Some(entry) = self.history.pop_redo() { - let transaction = entry.transaction.clone(); - let transaction_id = transaction.id; - let op = self.undo_or_redo(transaction); - Some((transaction_id, op)) - } else { - None - } - } - - pub fn redo_to_transaction(&mut self, transaction_id: TransactionId) -> Vec { - let transactions = self - .history - .remove_from_redo(transaction_id) - .iter() - .map(|entry| entry.transaction.clone()) - .collect::>(); - - transactions - .into_iter() - .map(|transaction| self.undo_or_redo(transaction)) - .collect() - } - - fn undo_or_redo(&mut self, transaction: Transaction) -> Operation { - let mut counts = HashMap::default(); - for edit_id in transaction.edit_ids { - counts.insert(edit_id, self.undo_map.undo_count(edit_id).saturating_add(1)); - } - - let operation = self.undo_operations(counts); - self.history.push(operation.clone()); - operation - } - - pub fn undo_operations(&mut self, counts: HashMap) -> Operation { - let timestamp = self.lamport_clock.tick(); - let version = self.version(); - self.snapshot.version.observe(timestamp); - let undo = UndoOperation { - timestamp, - version, - counts, - }; - self.apply_undo(&undo); - Operation::Undo(undo) - } - - pub fn push_transaction(&mut self, transaction: Transaction, now: Instant) { - self.history.push_transaction(transaction, now); - } - - /// Differs from `push_transaction` in that it does not clear the redo stack. - /// The caller responsible for - /// Differs from `push_transaction` in that it does not clear the redo - /// stack. Intended to be used to create a parent transaction to merge - /// potential child transactions into. - /// - /// The caller is responsible for removing it from the undo history using - /// `forget_transaction` if no edits are merged into it. Otherwise, if edits - /// are merged into this transaction, the caller is responsible for ensuring - /// the redo stack is cleared. The easiest way to ensure the redo stack is - /// cleared is to create transactions with the usual `start_transaction` and - /// `end_transaction` methods and merging the resulting transactions into - /// the transaction created by this method - pub fn push_empty_transaction(&mut self, now: Instant) -> TransactionId { - self.history - .push_empty_transaction(self.version.clone(), now, &mut self.lamport_clock) - } - - pub fn edited_ranges_for_transaction_id( - &self, - transaction_id: TransactionId, - ) -> impl '_ + Iterator> - where - D: TextDimension, - { - self.history - .transaction(transaction_id) - .into_iter() - .flat_map(|transaction| self.edited_ranges_for_transaction(transaction)) - } - - pub fn edited_ranges_for_edit_ids<'a, D>( - &'a self, - edit_ids: impl IntoIterator, - ) -> impl 'a + Iterator> - where - D: TextDimension, - { - // get fragment ranges - let mut cursor = self - .fragments - .cursor::, usize>>(&None); - let offset_ranges = self - .fragment_ids_for_edits(edit_ids.into_iter()) - .into_iter() - .filter_map(move |fragment_id| { - cursor.seek_forward(&Some(fragment_id), Bias::Left); - let fragment = cursor.item()?; - let start_offset = cursor.start().1; - let end_offset = start_offset + if fragment.visible { fragment.len } else { 0 }; - Some(start_offset..end_offset) - }); - - // combine adjacent ranges - let mut prev_range: Option> = None; - let disjoint_ranges = offset_ranges - .map(Some) - .chain([None]) - .filter_map(move |range| { - if let Some((range, prev_range)) = range.as_ref().zip(prev_range.as_mut()) - && prev_range.end == range.start - { - prev_range.end = range.end; - return None; - } - let result = prev_range.clone(); - prev_range = range; - result - }); - - // convert to the desired text dimension. - let mut position = D::zero(()); - let mut rope_cursor = self.visible_text.cursor(0); - disjoint_ranges.map(move |range| { - position.add_assign(&rope_cursor.summary(range.start)); - let start = position; - position.add_assign(&rope_cursor.summary(range.end)); - let end = position; - start..end - }) - } - - pub fn edited_ranges_for_transaction<'a, D>( - &'a self, - transaction: &'a Transaction, - ) -> impl 'a + Iterator> - where - D: TextDimension, - { - self.edited_ranges_for_edit_ids(&transaction.edit_ids) - } - - pub fn subscribe(&mut self) -> Subscription { - self.subscriptions.subscribe() - } - - pub fn wait_for_edits>( - &mut self, - edit_ids: It, - ) -> impl 'static + Future> + use { - let mut futures = Vec::new(); - for edit_id in edit_ids { - if !self.version.observed(edit_id) { - let (tx, rx) = oneshot::channel(); - self.edit_id_resolvers.entry(edit_id).or_default().push(tx); - futures.push(rx); - } - } - - async move { - for mut future in futures { - if future.recv().await.is_none() { - anyhow::bail!("gave up waiting for edits"); - } - } - Ok(()) - } - } - - pub fn wait_for_anchors>( - &mut self, - anchors: It, - ) -> impl 'static + Future> + use { - let mut futures = Vec::new(); - for anchor in anchors { - if !self.version.observed(anchor.timestamp) && !anchor.is_max() && !anchor.is_min() { - let (tx, rx) = oneshot::channel(); - self.edit_id_resolvers - .entry(anchor.timestamp) - .or_default() - .push(tx); - futures.push(rx); - } - } - - async move { - for mut future in futures { - if future.recv().await.is_none() { - anyhow::bail!("gave up waiting for anchors"); - } - } - Ok(()) - } - } - - pub fn wait_for_version( - &mut self, - version: clock::Global, - ) -> impl Future> + use<> { - let mut rx = None; - if !self.snapshot.version.observed_all(&version) { - let channel = oneshot::channel(); - self.wait_for_version_txs.push((version, channel.0)); - rx = Some(channel.1); - } - async move { - if let Some(mut rx) = rx - && rx.recv().await.is_none() - { - anyhow::bail!("gave up waiting for version"); - } - Ok(()) - } - } - - pub fn give_up_waiting(&mut self) { - self.edit_id_resolvers.clear(); - self.wait_for_version_txs.clear(); - } - - fn resolve_edit(&mut self, edit_id: clock::Lamport) { - for mut tx in self - .edit_id_resolvers - .remove(&edit_id) - .into_iter() - .flatten() - { - tx.try_send(()).ok(); - } - } -} - -#[cfg(any(test, feature = "test-support"))] -impl Buffer { - #[track_caller] - pub fn edit_via_marked_text(&mut self, marked_string: &str) { - let edits = self.edits_for_marked_text(marked_string); - self.edit(edits); - } - - #[track_caller] - pub fn edits_for_marked_text(&self, marked_string: &str) -> Vec<(Range, String)> { - let old_text = self.text(); - let (new_text, mut ranges) = util::test::marked_text_ranges(marked_string, false); - if ranges.is_empty() { - ranges.push(0..new_text.len()); - } - - assert_eq!( - old_text[..ranges[0].start], - new_text[..ranges[0].start], - "invalid edit" - ); - - let mut delta = 0; - let mut edits = Vec::new(); - let mut ranges = ranges.into_iter().peekable(); - - while let Some(inserted_range) = ranges.next() { - let new_start = inserted_range.start; - let old_start = (new_start as isize - delta) as usize; - - let following_text = if let Some(next_range) = ranges.peek() { - &new_text[inserted_range.end..next_range.start] - } else { - &new_text[inserted_range.end..] - }; - - let inserted_len = inserted_range.len(); - let deleted_len = old_text[old_start..] - .find(following_text) - .expect("invalid edit"); - - let old_range = old_start..old_start + deleted_len; - edits.push((old_range, new_text[inserted_range].to_string())); - delta += inserted_len as isize - deleted_len as isize; - } - - assert_eq!( - old_text.len() as isize + delta, - new_text.len() as isize, - "invalid edit" - ); - - edits - } - - pub fn check_invariants(&self) { - // Ensure every fragment is ordered by locator in the fragment tree and corresponds - // to an insertion fragment in the insertions tree. - let mut prev_fragment_id = Locator::min(); - for fragment in self.snapshot.fragments.items(&None) { - assert!(fragment.id > prev_fragment_id); - prev_fragment_id = fragment.id.clone(); - - let insertion_fragment = self - .snapshot - .insertions - .get( - &InsertionFragmentKey { - timestamp: fragment.timestamp, - split_offset: fragment.insertion_offset, - }, - (), - ) - .unwrap(); - assert_eq!( - insertion_fragment.fragment_id, fragment.id, - "fragment: {:?}\ninsertion: {:?}", - fragment, insertion_fragment - ); - } - - let mut cursor = self.snapshot.fragments.cursor::>(&None); - for insertion_fragment in self.snapshot.insertions.cursor::<()>(()) { - cursor.seek(&Some(&insertion_fragment.fragment_id), Bias::Left); - let fragment = cursor.item().unwrap(); - assert_eq!(insertion_fragment.fragment_id, fragment.id); - assert_eq!(insertion_fragment.split_offset, fragment.insertion_offset); - } - - let fragment_summary = self.snapshot.fragments.summary(); - assert_eq!( - fragment_summary.text.visible, - self.snapshot.visible_text.len() - ); - assert_eq!( - fragment_summary.text.deleted, - self.snapshot.deleted_text.len() - ); - - assert!(!self.text().contains("\r\n")); - } - - pub fn set_group_interval(&mut self, group_interval: Duration) { - self.history.group_interval = group_interval; - } - - pub fn random_byte_range(&self, start_offset: usize, rng: &mut impl rand::Rng) -> Range { - let end = self.clip_offset(rng.random_range(start_offset..=self.len()), Bias::Right); - let start = self.clip_offset(rng.random_range(start_offset..=end), Bias::Right); - start..end - } - - pub fn get_random_edits( - &self, - rng: &mut T, - edit_count: usize, - ) -> Vec<(Range, Arc)> - where - T: rand::Rng, - { - let mut edits: Vec<(Range, Arc)> = Vec::new(); - let mut last_end = None; - for _ in 0..edit_count { - if last_end.is_some_and(|last_end| last_end >= self.len()) { - break; - } - let new_start = last_end.map_or(0, |last_end| last_end + 1); - let range = self.random_byte_range(new_start, rng); - last_end = Some(range.end); - - let new_text_len = rng.random_range(0..10); - let new_text: String = RandomCharIter::new(&mut *rng).take(new_text_len).collect(); - - edits.push((range, new_text.into())); - } - edits - } - - pub fn randomly_edit( - &mut self, - rng: &mut T, - edit_count: usize, - ) -> (Vec<(Range, Arc)>, Operation) - where - T: rand::Rng, - { - let mut edits = self.get_random_edits(rng, edit_count); - log::info!("mutating buffer {:?} with {:?}", self.replica_id, edits); - - let op = self.edit(edits.iter().cloned()); - if let Operation::Edit(edit) = &op { - assert_eq!(edits.len(), edit.new_text.len()); - for (edit, new_text) in edits.iter_mut().zip(&edit.new_text) { - edit.1 = new_text.clone(); - } - } else { - unreachable!() - } - - (edits, op) - } - - pub fn randomly_undo_redo(&mut self, rng: &mut impl rand::Rng) -> Vec { - use rand::prelude::*; - - let mut ops = Vec::new(); - for _ in 0..rng.random_range(1..=5) { - if let Some(entry) = self.history.undo_stack.choose(rng) { - let transaction = entry.transaction.clone(); - log::info!( - "undoing buffer {:?} transaction {:?}", - self.replica_id, - transaction - ); - ops.push(self.undo_or_redo(transaction)); - } - } - ops - } -} - -impl Deref for Buffer { - type Target = BufferSnapshot; - - fn deref(&self) -> &Self::Target { - &self.snapshot - } -} - -impl BufferSnapshot { - pub fn as_rope(&self) -> &Rope { - &self.visible_text - } - - pub fn rope_for_version(&self, version: &clock::Global) -> Rope { - let mut rope = Rope::new(); - - let mut cursor = self - .fragments - .filter::<_, FragmentTextSummary>(&None, move |summary| { - !version.observed_all(&summary.max_version) - }); - cursor.next(); - - let mut visible_cursor = self.visible_text.cursor(0); - let mut deleted_cursor = self.deleted_text.cursor(0); - - while let Some(fragment) = cursor.item() { - if cursor.start().visible > visible_cursor.offset() { - let text = visible_cursor.slice(cursor.start().visible); - rope.append(text); - } - - if fragment.was_visible(version, &self.undo_map) { - if fragment.visible { - let text = visible_cursor.slice(cursor.end().visible); - rope.append(text); - } else { - deleted_cursor.seek_forward(cursor.start().deleted); - let text = deleted_cursor.slice(cursor.end().deleted); - rope.append(text); - } - } else if fragment.visible { - visible_cursor.seek_forward(cursor.end().visible); - } - - cursor.next(); - } - - if cursor.start().visible > visible_cursor.offset() { - let text = visible_cursor.slice(cursor.start().visible); - rope.append(text); - } - - rope - } - - pub fn remote_id(&self) -> BufferId { - self.remote_id - } - - pub fn replica_id(&self) -> ReplicaId { - self.replica_id - } - - pub fn row_count(&self) -> u32 { - self.max_point().row + 1 - } - - pub fn len(&self) -> usize { - self.visible_text.len() - } - - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - pub fn chars(&self) -> impl Iterator + '_ { - self.chars_at(0) - } - - pub fn chars_for_range(&self, range: Range) -> impl Iterator + '_ { - self.text_for_range(range).flat_map(str::chars) - } - - pub fn reversed_chars_for_range( - &self, - range: Range, - ) -> impl Iterator + '_ { - self.reversed_chunks_in_range(range) - .flat_map(|chunk| chunk.chars().rev()) - } - - pub fn contains_str_at(&self, position: T, needle: &str) -> bool - where - T: ToOffset, - { - let position = position.to_offset(self); - position == self.clip_offset(position, Bias::Left) - && self - .bytes_in_range(position..self.len()) - .flatten() - .copied() - .take(needle.len()) - .eq(needle.bytes()) - } - - pub fn common_prefix_at(&self, position: T, needle: &str) -> Range - where - T: ToOffset + TextDimension, - { - let offset = position.to_offset(self); - let common_prefix_len = needle - .char_indices() - .map(|(index, _)| index) - .chain([needle.len()]) - .take_while(|&len| len <= offset) - .filter(|&len| { - let left = self - .chars_for_range(offset - len..offset) - .flat_map(char::to_lowercase); - let right = needle[..len].chars().flat_map(char::to_lowercase); - left.eq(right) - }) - .last() - .unwrap_or(0); - let start_offset = offset - common_prefix_len; - let start = self.text_summary_for_range(0..start_offset); - start..position - } - - pub fn text(&self) -> String { - self.visible_text.to_string() - } - - pub fn line_ending(&self) -> LineEnding { - self.line_ending - } - - pub fn deleted_text(&self) -> String { - self.deleted_text.to_string() - } - - pub fn fragments(&self) -> impl Iterator { - self.fragments.iter() - } - - pub fn text_summary(&self) -> TextSummary { - self.visible_text.summary() - } - - pub fn max_point(&self) -> Point { - self.visible_text.max_point() - } - - pub fn max_point_utf16(&self) -> PointUtf16 { - self.visible_text.max_point_utf16() - } - - pub fn point_to_offset(&self, point: Point) -> usize { - self.visible_text.point_to_offset(point) - } - - pub fn point_to_offset_utf16(&self, point: Point) -> OffsetUtf16 { - self.visible_text.point_to_offset_utf16(point) - } - - pub fn point_utf16_to_offset_utf16(&self, point: PointUtf16) -> OffsetUtf16 { - self.visible_text.point_utf16_to_offset_utf16(point) - } - - pub fn point_utf16_to_offset(&self, point: PointUtf16) -> usize { - self.visible_text.point_utf16_to_offset(point) - } - - pub fn unclipped_point_utf16_to_offset(&self, point: Unclipped) -> usize { - self.visible_text.unclipped_point_utf16_to_offset(point) - } - - pub fn unclipped_point_utf16_to_point(&self, point: Unclipped) -> Point { - self.visible_text.unclipped_point_utf16_to_point(point) - } - - pub fn offset_utf16_to_offset(&self, offset: OffsetUtf16) -> usize { - self.visible_text.offset_utf16_to_offset(offset) - } - - pub fn offset_to_offset_utf16(&self, offset: usize) -> OffsetUtf16 { - self.visible_text.offset_to_offset_utf16(offset) - } - - pub fn offset_to_point(&self, offset: usize) -> Point { - self.visible_text.offset_to_point(offset) - } - - pub fn offset_to_point_utf16(&self, offset: usize) -> PointUtf16 { - self.visible_text.offset_to_point_utf16(offset) - } - - pub fn point_to_point_utf16(&self, point: Point) -> PointUtf16 { - self.visible_text.point_to_point_utf16(point) - } - - pub fn point_utf16_to_point(&self, point: PointUtf16) -> Point { - self.visible_text.point_utf16_to_point(point) - } - - pub fn version(&self) -> &clock::Global { - &self.version - } - - pub fn chars_at(&self, position: T) -> impl Iterator + '_ { - let offset = position.to_offset(self); - self.visible_text.chars_at(offset) - } - - pub fn reversed_chars_at(&self, position: T) -> impl Iterator + '_ { - let offset = position.to_offset(self); - self.visible_text.reversed_chars_at(offset) - } - - pub fn reversed_chunks_in_range(&self, range: Range) -> rope::Chunks<'_> { - let range = range.start.to_offset(self)..range.end.to_offset(self); - self.visible_text.reversed_chunks_in_range(range) - } - - pub fn bytes_in_range(&self, range: Range) -> rope::Bytes<'_> { - let start = range.start.to_offset(self); - let end = range.end.to_offset(self); - self.visible_text.bytes_in_range(start..end) - } - - pub fn reversed_bytes_in_range(&self, range: Range) -> rope::Bytes<'_> { - let start = range.start.to_offset(self); - let end = range.end.to_offset(self); - self.visible_text.reversed_bytes_in_range(start..end) - } - - pub fn text_for_range(&self, range: Range) -> Chunks<'_> { - let start = range.start.to_offset(self); - let end = range.end.to_offset(self); - self.visible_text.chunks_in_range(start..end) - } - - pub fn line_len(&self, row: u32) -> u32 { - let row_start_offset = Point::new(row, 0).to_offset(self); - let row_end_offset = if row >= self.max_point().row { - self.len() - } else { - Point::new(row + 1, 0).to_previous_offset(self) - }; - (row_end_offset - row_start_offset) as u32 - } - - pub fn line_indents_in_row_range( - &self, - row_range: Range, - ) -> impl Iterator + '_ { - let start = Point::new(row_range.start, 0).to_offset(self); - let end = Point::new(row_range.end, self.line_len(row_range.end)).to_offset(self); - - let mut chunks = self.as_rope().chunks_in_range(start..end); - let mut row = row_range.start; - let mut done = false; - std::iter::from_fn(move || { - if done { - None - } else { - let indent = (row, LineIndent::from_chunks(&mut chunks)); - done = !chunks.next_line(); - row += 1; - Some(indent) - } - }) - } - - /// Returns the line indents in the given row range, exclusive of end row, in reversed order. - pub fn reversed_line_indents_in_row_range( - &self, - row_range: Range, - ) -> impl Iterator + '_ { - let start = Point::new(row_range.start, 0).to_offset(self); - - let end_point; - let end; - if row_range.end > row_range.start { - end_point = Point::new(row_range.end - 1, self.line_len(row_range.end - 1)); - end = end_point.to_offset(self); - } else { - end_point = Point::new(row_range.start, 0); - end = start; - }; - - let mut chunks = self.as_rope().chunks_in_range(start..end); - // Move the cursor to the start of the last line if it's not empty. - chunks.seek(end); - if end_point.column > 0 { - chunks.prev_line(); - } - - let mut row = end_point.row; - let mut done = false; - std::iter::from_fn(move || { - if done { - None - } else { - let initial_offset = chunks.offset(); - let indent = (row, LineIndent::from_chunks(&mut chunks)); - if chunks.offset() > initial_offset { - chunks.prev_line(); - } - done = !chunks.prev_line(); - if !done { - row -= 1; - } - - Some(indent) - } - }) - } - - pub fn line_indent_for_row(&self, row: u32) -> LineIndent { - LineIndent::from_iter(self.chars_at(Point::new(row, 0))) - } - - pub fn is_line_blank(&self, row: u32) -> bool { - self.text_for_range(Point::new(row, 0)..Point::new(row, self.line_len(row))) - .all(|chunk| chunk.matches(|c: char| !c.is_whitespace()).next().is_none()) - } - - pub fn text_summary_for_range(&self, range: Range) -> D - where - D: TextDimension, - { - self.visible_text - .cursor(range.start.to_offset(self)) - .summary(range.end.to_offset(self)) - } - - pub fn summaries_for_anchors<'a, D, A>(&'a self, anchors: A) -> impl 'a + Iterator - where - D: 'a + TextDimension, - A: 'a + IntoIterator, - { - let anchors = anchors.into_iter(); - self.summaries_for_anchors_with_payload::(anchors.map(|a| (a, ()))) - .map(|d| d.0) - } - - pub fn summaries_for_anchors_with_payload<'a, D, A, T>( - &'a self, - anchors: A, - ) -> impl 'a + Iterator - where - D: 'a + TextDimension, - A: 'a + IntoIterator, - { - let anchors = anchors.into_iter(); - let mut insertion_cursor = self.insertions.cursor::(()); - let mut fragment_cursor = self - .fragments - .cursor::, usize>>(&None); - let mut text_cursor = self.visible_text.cursor(0); - let mut position = D::zero(()); - - anchors.map(move |(anchor, payload)| { - if anchor.is_min() { - return (D::zero(()), payload); - } else if anchor.is_max() { - return (D::from_text_summary(&self.visible_text.summary()), payload); - } - - let anchor_key = InsertionFragmentKey { - timestamp: anchor.timestamp, - split_offset: anchor.offset, - }; - insertion_cursor.seek(&anchor_key, anchor.bias); - if let Some(insertion) = insertion_cursor.item() { - let comparison = sum_tree::KeyedItem::key(insertion).cmp(&anchor_key); - if comparison == Ordering::Greater - || (anchor.bias == Bias::Left - && comparison == Ordering::Equal - && anchor.offset > 0) - { - insertion_cursor.prev(); - } - } else { - insertion_cursor.prev(); - } - let Some(insertion) = insertion_cursor.item() else { - panic!( - "invalid insertion for buffer {}@{:?} with anchor {:?}", - self.remote_id(), - self.version, - anchor - ); - }; - assert_eq!( - insertion.timestamp, - anchor.timestamp, - "invalid insertion for buffer {}@{:?} and anchor {:?}", - self.remote_id(), - self.version, - anchor - ); - - fragment_cursor.seek_forward(&Some(&insertion.fragment_id), Bias::Left); - let fragment = fragment_cursor.item().unwrap(); - let mut fragment_offset = fragment_cursor.start().1; - if fragment.visible { - fragment_offset += anchor.offset - insertion.split_offset; - } - - position.add_assign(&text_cursor.summary(fragment_offset)); - (position, payload) - }) - } - - pub fn summary_for_anchor(&self, anchor: &Anchor) -> D - where - D: TextDimension, - { - self.text_summary_for_range(0..self.offset_for_anchor(anchor)) - } - - pub fn offset_for_anchor(&self, anchor: &Anchor) -> usize { - if anchor.is_min() { - 0 - } else if anchor.is_max() { - self.visible_text.len() - } else { - debug_assert_eq!(anchor.buffer_id, Some(self.remote_id)); - debug_assert!( - self.version.observed(anchor.timestamp), - "Anchor timestamp {:?} not observed by buffer {:?}", - anchor.timestamp, - self.version - ); - let anchor_key = InsertionFragmentKey { - timestamp: anchor.timestamp, - split_offset: anchor.offset, - }; - let mut insertion_cursor = self.insertions.cursor::(()); - insertion_cursor.seek(&anchor_key, anchor.bias); - if let Some(insertion) = insertion_cursor.item() { - let comparison = sum_tree::KeyedItem::key(insertion).cmp(&anchor_key); - if comparison == Ordering::Greater - || (anchor.bias == Bias::Left - && comparison == Ordering::Equal - && anchor.offset > 0) - { - insertion_cursor.prev(); - } - } else { - insertion_cursor.prev(); - } - - let Some(insertion) = insertion_cursor - .item() - .filter(|insertion| insertion.timestamp == anchor.timestamp) - else { - self.panic_bad_anchor(anchor); - }; - - let (start, _, item) = self - .fragments - .find::, usize>, _>( - &None, - &Some(&insertion.fragment_id), - Bias::Left, - ); - let fragment = item.unwrap(); - let mut fragment_offset = start.1; - if fragment.visible { - fragment_offset += anchor.offset - insertion.split_offset; - } - fragment_offset - } - } - - #[cold] - fn panic_bad_anchor(&self, anchor: &Anchor) -> ! { - if anchor.buffer_id.is_some_and(|id| id != self.remote_id) { - panic!( - "invalid anchor - buffer id does not match: anchor {anchor:?}; buffer id: {}, version: {:?}", - self.remote_id, self.version - ); - } else if !self.version.observed(anchor.timestamp) { - panic!( - "invalid anchor - snapshot has not observed lamport: {:?}; version: {:?}", - anchor, self.version - ); - } else { - panic!( - "invalid anchor {:?}. buffer id: {}, version: {:?}", - anchor, self.remote_id, self.version - ); - } - } - - fn fragment_id_for_anchor(&self, anchor: &Anchor) -> &Locator { - self.try_fragment_id_for_anchor(anchor) - .unwrap_or_else(|| self.panic_bad_anchor(anchor)) - } - - fn try_fragment_id_for_anchor(&self, anchor: &Anchor) -> Option<&Locator> { - if anchor.is_min() { - Some(Locator::min_ref()) - } else if anchor.is_max() { - Some(Locator::max_ref()) - } else { - let anchor_key = InsertionFragmentKey { - timestamp: anchor.timestamp, - split_offset: anchor.offset, - }; - let mut insertion_cursor = self.insertions.cursor::(()); - insertion_cursor.seek(&anchor_key, anchor.bias); - if let Some(insertion) = insertion_cursor.item() { - let comparison = sum_tree::KeyedItem::key(insertion).cmp(&anchor_key); - if comparison == Ordering::Greater - || (anchor.bias == Bias::Left - && comparison == Ordering::Equal - && anchor.offset > 0) - { - insertion_cursor.prev(); - } - } else { - insertion_cursor.prev(); - } - - insertion_cursor - .item() - .filter(|insertion| { - !cfg!(debug_assertions) || insertion.timestamp == anchor.timestamp - }) - .map(|insertion| &insertion.fragment_id) - } - } - - pub fn anchor_before(&self, position: T) -> Anchor { - self.anchor_at(position, Bias::Left) - } - - pub fn anchor_after(&self, position: T) -> Anchor { - self.anchor_at(position, Bias::Right) - } - - pub fn anchor_at(&self, position: T, bias: Bias) -> Anchor { - self.anchor_at_offset(position.to_offset(self), bias) - } - - fn anchor_at_offset(&self, mut offset: usize, bias: Bias) -> Anchor { - if bias == Bias::Left && offset == 0 { - Anchor::min_for_buffer(self.remote_id) - } else if bias == Bias::Right - && ((!cfg!(debug_assertions) && offset >= self.len()) || offset == self.len()) - { - Anchor::max_for_buffer(self.remote_id) - } else { - if self - .visible_text - .assert_char_boundary::<{ cfg!(debug_assertions) }>(offset) - { - offset = match bias { - Bias::Left => self.visible_text.floor_char_boundary(offset), - Bias::Right => self.visible_text.ceil_char_boundary(offset), - }; - } - let (start, _, item) = self.fragments.find::(&None, &offset, bias); - let Some(fragment) = item else { - // We got a bad offset, likely out of bounds - debug_panic!( - "Failed to find fragment at offset {} (len: {})", - offset, - self.len() - ); - return Anchor::max_for_buffer(self.remote_id); - }; - let overshoot = offset - start; - Anchor { - timestamp: fragment.timestamp, - offset: fragment.insertion_offset + overshoot, - bias, - buffer_id: Some(self.remote_id), - } - } - } - - pub fn can_resolve(&self, anchor: &Anchor) -> bool { - anchor.is_min() - || anchor.is_max() - || (Some(self.remote_id) == anchor.buffer_id && self.version.observed(anchor.timestamp)) - } - - pub fn clip_offset(&self, offset: usize, bias: Bias) -> usize { - self.visible_text.clip_offset(offset, bias) - } - - pub fn clip_point(&self, point: Point, bias: Bias) -> Point { - self.visible_text.clip_point(point, bias) - } - - pub fn clip_offset_utf16(&self, offset: OffsetUtf16, bias: Bias) -> OffsetUtf16 { - self.visible_text.clip_offset_utf16(offset, bias) - } - - pub fn clip_point_utf16(&self, point: Unclipped, bias: Bias) -> PointUtf16 { - self.visible_text.clip_point_utf16(point, bias) - } - - pub fn edits_since<'a, D>( - &'a self, - since: &'a clock::Global, - ) -> impl 'a + Iterator> - where - D: TextDimension + Ord, - { - self.edits_since_in_range(since, Anchor::MIN..Anchor::MAX) - } - - pub fn anchored_edits_since<'a, D>( - &'a self, - since: &'a clock::Global, - ) -> impl 'a + Iterator, Range)> - where - D: TextDimension + Ord, - { - self.anchored_edits_since_in_range(since, Anchor::MIN..Anchor::MAX) - } - - pub fn edits_since_in_range<'a, D>( - &'a self, - since: &'a clock::Global, - range: Range, - ) -> impl 'a + Iterator> - where - D: TextDimension + Ord, - { - self.anchored_edits_since_in_range(since, range) - .map(|item| item.0) - } - - pub fn anchored_edits_since_in_range<'a, D>( - &'a self, - since: &'a clock::Global, - range: Range, - ) -> impl 'a + Iterator, Range)> - where - D: TextDimension + Ord, - { - let fragments_cursor = if *since == self.version { - None - } else { - let mut cursor = self.fragments.filter(&None, move |summary| { - !since.observed_all(&summary.max_version) - }); - cursor.next(); - Some(cursor) - }; - let start_fragment_id = self.fragment_id_for_anchor(&range.start); - let (start, _, item) = self - .fragments - .find::, FragmentTextSummary>, _>( - &None, - &Some(start_fragment_id), - Bias::Left, - ); - let mut visible_start = start.1.visible; - let mut deleted_start = start.1.deleted; - if let Some(fragment) = item { - let overshoot = range.start.offset - fragment.insertion_offset; - if fragment.visible { - visible_start += overshoot; - } else { - deleted_start += overshoot; - } - } - let end_fragment_id = self.fragment_id_for_anchor(&range.end); - - Edits { - visible_cursor: self.visible_text.cursor(visible_start), - deleted_cursor: self.deleted_text.cursor(deleted_start), - fragments_cursor, - undos: &self.undo_map, - since, - old_end: D::zero(()), - new_end: D::zero(()), - range: (start_fragment_id, range.start.offset)..(end_fragment_id, range.end.offset), - buffer_id: self.remote_id, - } - } - - pub fn has_edits_since_in_range(&self, since: &clock::Global, range: Range) -> bool { - if *since != self.version { - let start_fragment_id = self.fragment_id_for_anchor(&range.start); - let end_fragment_id = self.fragment_id_for_anchor(&range.end); - let mut cursor = self.fragments.filter::<_, usize>(&None, move |summary| { - !since.observed_all(&summary.max_version) - }); - cursor.next(); - while let Some(fragment) = cursor.item() { - if fragment.id > *end_fragment_id { - break; - } - if fragment.id > *start_fragment_id { - let was_visible = fragment.was_visible(since, &self.undo_map); - let is_visible = fragment.visible; - if was_visible != is_visible { - return true; - } - } - cursor.next(); - } - } - false - } - - pub fn has_edits_since(&self, since: &clock::Global) -> bool { - if *since != self.version { - let mut cursor = self.fragments.filter::<_, usize>(&None, move |summary| { - !since.observed_all(&summary.max_version) - }); - cursor.next(); - while let Some(fragment) = cursor.item() { - let was_visible = fragment.was_visible(since, &self.undo_map); - let is_visible = fragment.visible; - if was_visible != is_visible { - return true; - } - cursor.next(); - } - } - false - } - - pub fn range_to_version(&self, range: Range, version: &clock::Global) -> Range { - let mut offsets = self.offsets_to_version([range.start, range.end], version); - offsets.next().unwrap()..offsets.next().unwrap() - } - - /// Converts the given sequence of offsets into their corresponding offsets - /// at a prior version of this buffer. - pub fn offsets_to_version<'a>( - &'a self, - offsets: impl 'a + IntoIterator, - version: &'a clock::Global, - ) -> impl 'a + Iterator { - let mut edits = self.edits_since(version).peekable(); - let mut last_old_end = 0; - let mut last_new_end = 0; - offsets.into_iter().map(move |new_offset| { - while let Some(edit) = edits.peek() { - if edit.new.start > new_offset { - break; - } - - if edit.new.end <= new_offset { - last_new_end = edit.new.end; - last_old_end = edit.old.end; - edits.next(); - continue; - } - - let overshoot = new_offset - edit.new.start; - return (edit.old.start + overshoot).min(edit.old.end); - } - - last_old_end + new_offset.saturating_sub(last_new_end) - }) - } - - /// Visually annotates a position or range with the `Debug` representation of a value. The - /// callsite of this function is used as a key - previous annotations will be removed. - #[cfg(debug_assertions)] - #[track_caller] - pub fn debug(&self, ranges: &R, value: V) - where - R: debug::ToDebugRanges, - V: std::fmt::Debug, - { - self.debug_with_key(std::panic::Location::caller(), ranges, value); - } - - /// Visually annotates a position or range with the `Debug` representation of a value. Previous - /// debug annotations with the same key will be removed. The key is also used to determine the - /// annotation's color. - #[cfg(debug_assertions)] - pub fn debug_with_key(&self, key: &K, ranges: &R, value: V) - where - K: std::hash::Hash + 'static, - R: debug::ToDebugRanges, - V: std::fmt::Debug, - { - let ranges = ranges - .to_debug_ranges(self) - .into_iter() - .map(|range| self.anchor_after(range.start)..self.anchor_before(range.end)) - .collect(); - debug::GlobalDebugRanges::with_locked(|debug_ranges| { - debug_ranges.insert(key, ranges, format!("{value:?}").into()); - }); - } -} - -struct RopeBuilder<'a> { - old_visible_cursor: rope::Cursor<'a>, - old_deleted_cursor: rope::Cursor<'a>, - new_visible: Rope, - new_deleted: Rope, -} - -impl<'a> RopeBuilder<'a> { - fn new(old_visible_cursor: rope::Cursor<'a>, old_deleted_cursor: rope::Cursor<'a>) -> Self { - Self { - old_visible_cursor, - old_deleted_cursor, - new_visible: Rope::new(), - new_deleted: Rope::new(), - } - } - - fn append(&mut self, len: FragmentTextSummary) { - self.push(len.visible, true, true); - self.push(len.deleted, false, false); - } - - fn push_fragment(&mut self, fragment: &Fragment, was_visible: bool) { - debug_assert!(fragment.len > 0); - self.push(fragment.len, was_visible, fragment.visible) - } - - fn push(&mut self, len: usize, was_visible: bool, is_visible: bool) { - let text = if was_visible { - self.old_visible_cursor - .slice(self.old_visible_cursor.offset() + len) - } else { - self.old_deleted_cursor - .slice(self.old_deleted_cursor.offset() + len) - }; - if is_visible { - self.new_visible.append(text); - } else { - self.new_deleted.append(text); - } - } - - fn push_str(&mut self, text: &str) { - self.new_visible.push(text); - } - - fn finish(mut self) -> (Rope, Rope) { - self.new_visible.append(self.old_visible_cursor.suffix()); - self.new_deleted.append(self.old_deleted_cursor.suffix()); - (self.new_visible, self.new_deleted) - } -} - -impl bool> Iterator for Edits<'_, D, F> { - type Item = (Edit, Range); - - fn next(&mut self) -> Option { - let mut pending_edit: Option = None; - let cursor = self.fragments_cursor.as_mut()?; - - while let Some(fragment) = cursor.item() { - if fragment.id < *self.range.start.0 { - cursor.next(); - continue; - } else if fragment.id > *self.range.end.0 { - break; - } - - if cursor.start().visible > self.visible_cursor.offset() { - let summary = self.visible_cursor.summary(cursor.start().visible); - self.old_end.add_assign(&summary); - self.new_end.add_assign(&summary); - } - - if pending_edit - .as_ref() - .is_some_and(|(change, _)| change.new.end < self.new_end) - { - break; - } - - let start_anchor = Anchor { - timestamp: fragment.timestamp, - offset: fragment.insertion_offset, - bias: Bias::Right, - buffer_id: Some(self.buffer_id), - }; - let end_anchor = Anchor { - timestamp: fragment.timestamp, - offset: fragment.insertion_offset + fragment.len, - bias: Bias::Left, - buffer_id: Some(self.buffer_id), - }; - - if !fragment.was_visible(self.since, self.undos) && fragment.visible { - let mut visible_end = cursor.end().visible; - if fragment.id == *self.range.end.0 { - visible_end = cmp::min( - visible_end, - cursor.start().visible + (self.range.end.1 - fragment.insertion_offset), - ); - } - - let fragment_summary = self.visible_cursor.summary(visible_end); - let mut new_end = self.new_end; - new_end.add_assign(&fragment_summary); - if let Some((edit, range)) = pending_edit.as_mut() { - edit.new.end = new_end; - range.end = end_anchor; - } else { - pending_edit = Some(( - Edit { - old: self.old_end..self.old_end, - new: self.new_end..new_end, - }, - start_anchor..end_anchor, - )); - } - - self.new_end = new_end; - } else if fragment.was_visible(self.since, self.undos) && !fragment.visible { - let mut deleted_end = cursor.end().deleted; - if fragment.id == *self.range.end.0 { - deleted_end = cmp::min( - deleted_end, - cursor.start().deleted + (self.range.end.1 - fragment.insertion_offset), - ); - } - - if cursor.start().deleted > self.deleted_cursor.offset() { - self.deleted_cursor.seek_forward(cursor.start().deleted); - } - let fragment_summary = self.deleted_cursor.summary(deleted_end); - let mut old_end = self.old_end; - old_end.add_assign(&fragment_summary); - if let Some((edit, range)) = pending_edit.as_mut() { - edit.old.end = old_end; - range.end = end_anchor; - } else { - pending_edit = Some(( - Edit { - old: self.old_end..old_end, - new: self.new_end..self.new_end, - }, - start_anchor..end_anchor, - )); - } - - self.old_end = old_end; - } - - cursor.next(); - } - - pending_edit - } -} - -impl Fragment { - fn is_visible(&self, undos: &UndoMap) -> bool { - !undos.is_undone(self.timestamp) && self.deletions.iter().all(|d| undos.is_undone(*d)) - } - - fn was_visible(&self, version: &clock::Global, undos: &UndoMap) -> bool { - (version.observed(self.timestamp) && !undos.was_undone(self.timestamp, version)) - && self - .deletions - .iter() - .all(|d| !version.observed(*d) || undos.was_undone(*d, version)) - } -} - -impl sum_tree::Item for Fragment { - type Summary = FragmentSummary; - - fn summary(&self, _cx: &Option) -> Self::Summary { - let mut max_version = clock::Global::new(); - max_version.observe(self.timestamp); - for deletion in &self.deletions { - max_version.observe(*deletion); - } - max_version.join(&self.max_undos); - - let mut min_insertion_version = clock::Global::new(); - min_insertion_version.observe(self.timestamp); - let max_insertion_version = min_insertion_version.clone(); - if self.visible { - FragmentSummary { - max_id: self.id.clone(), - text: FragmentTextSummary { - visible: self.len, - deleted: 0, - }, - max_version, - min_insertion_version, - max_insertion_version, - } - } else { - FragmentSummary { - max_id: self.id.clone(), - text: FragmentTextSummary { - visible: 0, - deleted: self.len, - }, - max_version, - min_insertion_version, - max_insertion_version, - } - } - } -} - -impl sum_tree::Summary for FragmentSummary { - type Context<'a> = &'a Option; - - fn zero(_cx: Self::Context<'_>) -> Self { - Default::default() - } - - fn add_summary(&mut self, other: &Self, _: Self::Context<'_>) { - self.max_id.assign(&other.max_id); - self.text.visible += &other.text.visible; - self.text.deleted += &other.text.deleted; - self.max_version.join(&other.max_version); - self.min_insertion_version - .meet(&other.min_insertion_version); - self.max_insertion_version - .join(&other.max_insertion_version); - } -} - -impl Default for FragmentSummary { - fn default() -> Self { - FragmentSummary { - max_id: Locator::min(), - text: FragmentTextSummary::default(), - max_version: clock::Global::new(), - min_insertion_version: clock::Global::new(), - max_insertion_version: clock::Global::new(), - } - } -} - -impl sum_tree::Item for InsertionFragment { - type Summary = InsertionFragmentKey; - - fn summary(&self, _cx: ()) -> Self::Summary { - InsertionFragmentKey { - timestamp: self.timestamp, - split_offset: self.split_offset, - } - } -} - -impl sum_tree::KeyedItem for InsertionFragment { - type Key = InsertionFragmentKey; - - fn key(&self) -> Self::Key { - sum_tree::Item::summary(self, ()) - } -} - -impl InsertionFragment { - fn new(fragment: &Fragment) -> Self { - Self { - timestamp: fragment.timestamp, - split_offset: fragment.insertion_offset, - fragment_id: fragment.id.clone(), - } - } - - fn insert_new(fragment: &Fragment) -> sum_tree::Edit { - sum_tree::Edit::Insert(Self::new(fragment)) - } -} - -impl sum_tree::ContextLessSummary for InsertionFragmentKey { - fn zero() -> Self { - InsertionFragmentKey { - timestamp: Lamport::MIN, - split_offset: 0, - } - } - - fn add_summary(&mut self, summary: &Self) { - *self = *summary; - } -} - -#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct FullOffset(pub usize); - -impl ops::AddAssign for FullOffset { - fn add_assign(&mut self, rhs: usize) { - self.0 += rhs; - } -} - -impl ops::Add for FullOffset { - type Output = Self; - - fn add(mut self, rhs: usize) -> Self::Output { - self += rhs; - self - } -} - -impl ops::Sub for FullOffset { - type Output = usize; - - fn sub(self, rhs: Self) -> Self::Output { - self.0 - rhs.0 - } -} - -impl sum_tree::Dimension<'_, FragmentSummary> for usize { - fn zero(_: &Option) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &FragmentSummary, _: &Option) { - *self += summary.text.visible; - } -} - -impl sum_tree::Dimension<'_, FragmentSummary> for FullOffset { - fn zero(_: &Option) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &FragmentSummary, _: &Option) { - self.0 += summary.text.visible + summary.text.deleted; - } -} - -impl<'a> sum_tree::Dimension<'a, FragmentSummary> for Option<&'a Locator> { - fn zero(_: &Option) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a FragmentSummary, _: &Option) { - *self = Some(&summary.max_id); - } -} - -impl sum_tree::SeekTarget<'_, FragmentSummary, FragmentTextSummary> for usize { - fn cmp( - &self, - cursor_location: &FragmentTextSummary, - _: &Option, - ) -> cmp::Ordering { - Ord::cmp(self, &cursor_location.visible) - } -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -enum VersionedFullOffset { - Offset(FullOffset), - Invalid, -} - -impl VersionedFullOffset { - fn full_offset(&self) -> FullOffset { - if let Self::Offset(position) = self { - *position - } else { - panic!("invalid version") - } - } -} - -impl Default for VersionedFullOffset { - fn default() -> Self { - Self::Offset(Default::default()) - } -} - -impl<'a> sum_tree::Dimension<'a, FragmentSummary> for VersionedFullOffset { - fn zero(_cx: &Option) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a FragmentSummary, cx: &Option) { - if let Self::Offset(offset) = self { - let version = cx.as_ref().unwrap(); - if version.observed_all(&summary.max_insertion_version) { - *offset += summary.text.visible + summary.text.deleted; - } else if version.observed_any(&summary.min_insertion_version) { - *self = Self::Invalid; - } - } - } -} - -impl sum_tree::SeekTarget<'_, FragmentSummary, Self> for VersionedFullOffset { - fn cmp(&self, cursor_position: &Self, _: &Option) -> cmp::Ordering { - match (self, cursor_position) { - (Self::Offset(a), Self::Offset(b)) => Ord::cmp(a, b), - (Self::Offset(_), Self::Invalid) => cmp::Ordering::Less, - (Self::Invalid, _) => unreachable!(), - } - } -} - -impl Operation { - fn replica_id(&self) -> ReplicaId { - operation_queue::Operation::lamport_timestamp(self).replica_id - } - - pub fn timestamp(&self) -> clock::Lamport { - match self { - Operation::Edit(edit) => edit.timestamp, - Operation::Undo(undo) => undo.timestamp, - } - } - - pub fn as_edit(&self) -> Option<&EditOperation> { - match self { - Operation::Edit(edit) => Some(edit), - _ => None, - } - } - - pub fn is_edit(&self) -> bool { - matches!(self, Operation::Edit { .. }) - } -} - -impl operation_queue::Operation for Operation { - fn lamport_timestamp(&self) -> clock::Lamport { - match self { - Operation::Edit(edit) => edit.timestamp, - Operation::Undo(undo) => undo.timestamp, - } - } -} - -pub trait ToOffset { - fn to_offset(&self, snapshot: &BufferSnapshot) -> usize; - /// Turns this point into the next offset in the buffer that comes after this, respecting utf8 boundaries. - fn to_next_offset(&self, snapshot: &BufferSnapshot) -> usize { - snapshot - .visible_text - .ceil_char_boundary(self.to_offset(snapshot) + 1) - } - /// Turns this point into the previous offset in the buffer that comes before this, respecting utf8 boundaries. - fn to_previous_offset(&self, snapshot: &BufferSnapshot) -> usize { - snapshot - .visible_text - .floor_char_boundary(self.to_offset(snapshot).saturating_sub(1)) - } -} - -impl ToOffset for Point { - #[inline] - fn to_offset(&self, snapshot: &BufferSnapshot) -> usize { - snapshot.point_to_offset(*self) - } -} - -impl ToOffset for usize { - fn to_offset(&self, snapshot: &BufferSnapshot) -> usize { - if snapshot - .as_rope() - .assert_char_boundary::<{ cfg!(debug_assertions) }>(*self) - { - snapshot.as_rope().floor_char_boundary(*self) - } else { - *self - } - } -} - -impl ToOffset for Anchor { - #[inline] - fn to_offset(&self, snapshot: &BufferSnapshot) -> usize { - snapshot.summary_for_anchor(self) - } -} - -impl ToOffset for &T { - #[inline] - fn to_offset(&self, content: &BufferSnapshot) -> usize { - (*self).to_offset(content) - } -} - -impl ToOffset for PointUtf16 { - #[inline] - fn to_offset(&self, snapshot: &BufferSnapshot) -> usize { - snapshot.point_utf16_to_offset(*self) - } -} - -impl ToOffset for Unclipped { - #[inline] - fn to_offset(&self, snapshot: &BufferSnapshot) -> usize { - snapshot.unclipped_point_utf16_to_offset(*self) - } -} - -pub trait ToPoint { - fn to_point(&self, snapshot: &BufferSnapshot) -> Point; -} - -impl ToPoint for Anchor { - #[inline] - fn to_point(&self, snapshot: &BufferSnapshot) -> Point { - snapshot.summary_for_anchor(self) - } -} - -impl ToPoint for usize { - #[inline] - fn to_point(&self, snapshot: &BufferSnapshot) -> Point { - snapshot.offset_to_point(*self) - } -} - -impl ToPoint for Point { - #[inline] - fn to_point(&self, _: &BufferSnapshot) -> Point { - *self - } -} - -impl ToPoint for Unclipped { - #[inline] - fn to_point(&self, snapshot: &BufferSnapshot) -> Point { - snapshot.unclipped_point_utf16_to_point(*self) - } -} - -pub trait ToPointUtf16 { - fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> PointUtf16; -} - -impl ToPointUtf16 for Anchor { - #[inline] - fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> PointUtf16 { - snapshot.summary_for_anchor(self) - } -} - -impl ToPointUtf16 for usize { - #[inline] - fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> PointUtf16 { - snapshot.offset_to_point_utf16(*self) - } -} - -impl ToPointUtf16 for PointUtf16 { - #[inline] - fn to_point_utf16(&self, _: &BufferSnapshot) -> PointUtf16 { - *self - } -} - -impl ToPointUtf16 for Point { - #[inline] - fn to_point_utf16(&self, snapshot: &BufferSnapshot) -> PointUtf16 { - snapshot.point_to_point_utf16(*self) - } -} - -pub trait ToOffsetUtf16 { - fn to_offset_utf16(&self, snapshot: &BufferSnapshot) -> OffsetUtf16; -} - -impl ToOffsetUtf16 for Anchor { - #[inline] - fn to_offset_utf16(&self, snapshot: &BufferSnapshot) -> OffsetUtf16 { - snapshot.summary_for_anchor(self) - } -} - -impl ToOffsetUtf16 for usize { - #[inline] - fn to_offset_utf16(&self, snapshot: &BufferSnapshot) -> OffsetUtf16 { - snapshot.offset_to_offset_utf16(*self) - } -} - -impl ToOffsetUtf16 for OffsetUtf16 { - #[inline] - fn to_offset_utf16(&self, _snapshot: &BufferSnapshot) -> OffsetUtf16 { - *self - } -} - -pub trait FromAnchor { - fn from_anchor(anchor: &Anchor, snapshot: &BufferSnapshot) -> Self; -} - -impl FromAnchor for Anchor { - #[inline] - fn from_anchor(anchor: &Anchor, _snapshot: &BufferSnapshot) -> Self { - *anchor - } -} - -impl FromAnchor for Point { - #[inline] - fn from_anchor(anchor: &Anchor, snapshot: &BufferSnapshot) -> Self { - snapshot.summary_for_anchor(anchor) - } -} - -impl FromAnchor for PointUtf16 { - #[inline] - fn from_anchor(anchor: &Anchor, snapshot: &BufferSnapshot) -> Self { - snapshot.summary_for_anchor(anchor) - } -} - -impl FromAnchor for usize { - #[inline] - fn from_anchor(anchor: &Anchor, snapshot: &BufferSnapshot) -> Self { - snapshot.summary_for_anchor(anchor) - } -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum LineEnding { - Unix, - Windows, -} - -impl Default for LineEnding { - fn default() -> Self { - #[cfg(unix)] - return Self::Unix; - - #[cfg(not(unix))] - return Self::Windows; - } -} - -impl LineEnding { - pub fn as_str(&self) -> &'static str { - match self { - LineEnding::Unix => "\n", - LineEnding::Windows => "\r\n", - } - } - - pub fn label(&self) -> &'static str { - match self { - LineEnding::Unix => "LF", - LineEnding::Windows => "CRLF", - } - } - - pub fn detect(text: &str) -> Self { - let mut max_ix = cmp::min(text.len(), 1000); - while !text.is_char_boundary(max_ix) { - max_ix -= 1; - } - - if let Some(ix) = text[..max_ix].find(['\n']) { - if ix > 0 && text.as_bytes()[ix - 1] == b'\r' { - Self::Windows - } else { - Self::Unix - } - } else { - Self::default() - } - } - - pub fn normalize(text: &mut String) { - if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(text, "\n") { - *text = replaced; - } - } - - pub fn normalize_arc(text: Arc) -> Arc { - if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(&text, "\n") { - replaced.into() - } else { - text - } - } - - pub fn normalize_cow(text: Cow) -> Cow { - if let Cow::Owned(replaced) = LINE_SEPARATORS_REGEX.replace_all(&text, "\n") { - replaced.into() - } else { - text - } - } -} - -pub fn chunks_with_line_ending(rope: &Rope, line_ending: LineEnding) -> impl Iterator { - rope.chunks().flat_map(move |chunk| { - let mut newline = false; - let end_with_newline = chunk.ends_with('\n').then_some(line_ending.as_str()); - chunk - .lines() - .flat_map(move |line| { - let ending = if newline { - Some(line_ending.as_str()) - } else { - None - }; - newline = true; - ending.into_iter().chain([line]) - }) - .chain(end_with_newline) - }) -} - -#[cfg(debug_assertions)] -pub mod debug { - use super::*; - use parking_lot::Mutex; - use std::any::TypeId; - use std::hash::{Hash, Hasher}; - - static GLOBAL_DEBUG_RANGES: Mutex> = Mutex::new(None); - - pub struct GlobalDebugRanges { - pub ranges: Vec, - key_to_occurrence_index: HashMap, - next_occurrence_index: usize, - } - - pub struct DebugRange { - key: Key, - pub ranges: Vec>, - pub value: Arc, - pub occurrence_index: usize, - } - - #[derive(Debug, Clone, PartialEq, Eq, Hash)] - struct Key { - type_id: TypeId, - hash: u64, - } - - impl GlobalDebugRanges { - pub fn with_locked(f: impl FnOnce(&mut Self) -> R) -> R { - let mut state = GLOBAL_DEBUG_RANGES.lock(); - if state.is_none() { - *state = Some(GlobalDebugRanges { - ranges: Vec::new(), - key_to_occurrence_index: HashMap::default(), - next_occurrence_index: 0, - }); - } - if let Some(global_debug_ranges) = state.as_mut() { - f(global_debug_ranges) - } else { - unreachable!() - } - } - - pub fn insert( - &mut self, - key: &K, - ranges: Vec>, - value: Arc, - ) { - let occurrence_index = *self - .key_to_occurrence_index - .entry(Key::new(key)) - .or_insert_with(|| { - let occurrence_index = self.next_occurrence_index; - self.next_occurrence_index += 1; - occurrence_index - }); - let key = Key::new(key); - let existing = self - .ranges - .iter() - .enumerate() - .rfind(|(_, existing)| existing.key == key); - if let Some((existing_ix, _)) = existing { - self.ranges.remove(existing_ix); - } - self.ranges.push(DebugRange { - ranges, - key, - value, - occurrence_index, - }); - } - - pub fn remove(&mut self, key: &K) { - self.remove_impl(&Key::new(key)); - } - - fn remove_impl(&mut self, key: &Key) { - let existing = self - .ranges - .iter() - .enumerate() - .rfind(|(_, existing)| &existing.key == key); - if let Some((existing_ix, _)) = existing { - self.ranges.remove(existing_ix); - } - } - - pub fn remove_all_with_key_type(&mut self) { - self.ranges - .retain(|item| item.key.type_id != TypeId::of::()); - } - } - - impl Key { - fn new(key: &K) -> Self { - let type_id = TypeId::of::(); - let mut hasher = collections::FxHasher::default(); - key.hash(&mut hasher); - Key { - type_id, - hash: hasher.finish(), - } - } - } - - pub trait ToDebugRanges { - fn to_debug_ranges(&self, snapshot: &BufferSnapshot) -> Vec>; - } - - impl ToDebugRanges for T { - fn to_debug_ranges(&self, snapshot: &BufferSnapshot) -> Vec> { - [self.to_offset(snapshot)].to_debug_ranges(snapshot) - } - } - - impl ToDebugRanges for Range { - fn to_debug_ranges(&self, snapshot: &BufferSnapshot) -> Vec> { - [self.clone()].to_debug_ranges(snapshot) - } - } - - impl ToDebugRanges for Vec { - fn to_debug_ranges(&self, snapshot: &BufferSnapshot) -> Vec> { - self.as_slice().to_debug_ranges(snapshot) - } - } - - impl ToDebugRanges for Vec> { - fn to_debug_ranges(&self, snapshot: &BufferSnapshot) -> Vec> { - self.as_slice().to_debug_ranges(snapshot) - } - } - - impl ToDebugRanges for [T] { - fn to_debug_ranges(&self, snapshot: &BufferSnapshot) -> Vec> { - self.iter() - .map(|item| { - let offset = item.to_offset(snapshot); - offset..offset - }) - .collect() - } - } - - impl ToDebugRanges for [Range] { - fn to_debug_ranges(&self, snapshot: &BufferSnapshot) -> Vec> { - self.iter() - .map(|range| range.start.to_offset(snapshot)..range.end.to_offset(snapshot)) - .collect() - } - } -} diff --git a/crates/text/src/undo_map.rs b/crates/text/src/undo_map.rs deleted file mode 100644 index 2c2eba8de6..0000000000 --- a/crates/text/src/undo_map.rs +++ /dev/null @@ -1,115 +0,0 @@ -use crate::UndoOperation; -use clock::Lamport; -use std::cmp; -use sum_tree::{Bias, SumTree}; - -#[derive(Copy, Clone, Debug)] -struct UndoMapEntry { - key: UndoMapKey, - undo_count: u32, -} - -impl sum_tree::Item for UndoMapEntry { - type Summary = UndoMapKey; - - fn summary(&self, _cx: ()) -> Self::Summary { - self.key - } -} - -impl sum_tree::KeyedItem for UndoMapEntry { - type Key = UndoMapKey; - - fn key(&self) -> Self::Key { - self.key - } -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] -struct UndoMapKey { - edit_id: clock::Lamport, - undo_id: clock::Lamport, -} - -impl sum_tree::ContextLessSummary for UndoMapKey { - fn zero() -> Self { - UndoMapKey { - edit_id: Lamport::MIN, - undo_id: Lamport::MIN, - } - } - - fn add_summary(&mut self, summary: &Self) { - *self = cmp::max(*self, *summary); - } -} - -#[derive(Clone, Default)] -pub struct UndoMap(SumTree); - -impl UndoMap { - pub fn insert(&mut self, undo: &UndoOperation) { - let edits = undo - .counts - .iter() - .map(|(edit_id, count)| { - sum_tree::Edit::Insert(UndoMapEntry { - key: UndoMapKey { - edit_id: *edit_id, - undo_id: undo.timestamp, - }, - undo_count: *count, - }) - }) - .collect::>(); - self.0.edit(edits, ()); - } - - pub fn is_undone(&self, edit_id: clock::Lamport) -> bool { - self.undo_count(edit_id) % 2 == 1 - } - pub fn was_undone(&self, edit_id: clock::Lamport, version: &clock::Global) -> bool { - let mut cursor = self.0.cursor::(()); - cursor.seek( - &UndoMapKey { - edit_id, - undo_id: Lamport::MIN, - }, - Bias::Left, - ); - - let mut undo_count = 0; - for entry in cursor { - if entry.key.edit_id != edit_id { - break; - } - - if version.observed(entry.key.undo_id) { - undo_count = cmp::max(undo_count, entry.undo_count); - } - } - - undo_count % 2 == 1 - } - - pub fn undo_count(&self, edit_id: clock::Lamport) -> u32 { - let mut cursor = self.0.cursor::(()); - cursor.seek( - &UndoMapKey { - edit_id, - undo_id: Lamport::MIN, - }, - Bias::Left, - ); - - let mut undo_count = 0; - for entry in cursor { - if entry.key.edit_id != edit_id { - break; - } - - undo_count = cmp::max(undo_count, entry.undo_count); - } - undo_count - } -} diff --git a/crates/theme/Cargo.toml b/crates/theme/Cargo.toml deleted file mode 100644 index ef193c500d..0000000000 --- a/crates/theme/Cargo.toml +++ /dev/null @@ -1,43 +0,0 @@ -[package] -name = "theme" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[features] -default = [] -test-support = ["gpui/test-support", "fs/test-support", "settings/test-support"] - -[lib] -path = "src/theme.rs" -doctest = false - -[dependencies] -anyhow.workspace = true -collections.workspace = true -derive_more.workspace = true -fs.workspace = true -futures.workspace = true -gpui.workspace = true -log.workspace = true -palette = { workspace = true, default-features = false, features = ["std"] } -parking_lot.workspace = true -refineable.workspace = true -schemars = { workspace = true, features = ["indexmap2"] } -serde.workspace = true -serde_json.workspace = true -serde_json_lenient.workspace = true -settings.workspace = true -strum.workspace = true -thiserror.workspace = true -util.workspace = true -uuid.workspace = true - -[dev-dependencies] -fs = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -settings = { workspace = true, features = ["test-support"] } diff --git a/crates/theme/LICENSE-GPL b/crates/theme/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/theme/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/theme/src/default_colors.rs b/crates/theme/src/default_colors.rs deleted file mode 100644 index 82be2896c6..0000000000 --- a/crates/theme/src/default_colors.rs +++ /dev/null @@ -1,2504 +0,0 @@ -use gpui::{Hsla, Rgba}; - -use crate::ColorScale; -use crate::scale::{ColorScaleSet, ColorScales}; -use crate::{SystemColors, ThemeColors}; - -pub(crate) fn neutral() -> ColorScaleSet { - sand() -} - -const ADDED_COLOR: Hsla = Hsla { - h: 134. / 360., - s: 0.55, - l: 0.40, - a: 1.0, -}; -const WORD_ADDED_COLOR: Hsla = Hsla { - h: 134. / 360., - s: 0.55, - l: 0.40, - a: 0.35, -}; -const MODIFIED_COLOR: Hsla = Hsla { - h: 48. / 360., - s: 0.76, - l: 0.47, - a: 1.0, -}; -const REMOVED_COLOR: Hsla = Hsla { - h: 350. / 360., - s: 0.88, - l: 0.25, - a: 1.0, -}; -const WORD_DELETED_COLOR: Hsla = Hsla { - h: 350. / 360., - s: 0.88, - l: 0.25, - a: 0.80, -}; - -/// The default colors for the theme. -/// -/// Themes that do not specify all colors are refined off of these defaults. -impl ThemeColors { - /// Returns the default colors for light themes. - /// - /// Themes that do not specify all colors are refined off of these defaults. - pub fn light() -> Self { - let system = SystemColors::default(); - - Self { - border: neutral().light().step_6(), - border_variant: neutral().light().step_5(), - border_focused: blue().light().step_5(), - border_selected: blue().light().step_5(), - border_transparent: system.transparent, - border_disabled: neutral().light().step_3(), - elevated_surface_background: neutral().light().step_2(), - surface_background: neutral().light().step_2(), - background: neutral().light().step_1(), - element_background: neutral().light().step_3(), - element_hover: neutral().light_alpha().step_4(), - element_active: neutral().light_alpha().step_5(), - element_selected: neutral().light_alpha().step_5(), - element_disabled: neutral().light_alpha().step_3(), - element_selection_background: blue().light().step_3().alpha(0.25), - drop_target_background: blue().light_alpha().step_2(), - drop_target_border: neutral().light().step_12(), - ghost_element_background: system.transparent, - ghost_element_hover: neutral().light_alpha().step_3(), - ghost_element_active: neutral().light_alpha().step_4(), - ghost_element_selected: neutral().light_alpha().step_5(), - ghost_element_disabled: neutral().light_alpha().step_3(), - text: neutral().light().step_12(), - text_muted: neutral().light().step_10(), - text_placeholder: neutral().light().step_10(), - text_disabled: neutral().light().step_9(), - text_accent: blue().light().step_11(), - icon: neutral().light().step_11(), - icon_muted: neutral().light().step_10(), - icon_disabled: neutral().light().step_9(), - icon_placeholder: neutral().light().step_10(), - icon_accent: blue().light().step_11(), - debugger_accent: red().light().step_10(), - status_bar_background: neutral().light().step_2(), - title_bar_background: neutral().light().step_2(), - title_bar_inactive_background: neutral().light().step_3(), - toolbar_background: neutral().light().step_1(), - tab_bar_background: neutral().light().step_2(), - tab_inactive_background: neutral().light().step_2(), - tab_active_background: neutral().light().step_1(), - search_match_background: neutral().light().step_5(), - search_active_match_background: neutral().light().step_7(), - panel_background: neutral().light().step_2(), - panel_focused_border: blue().light().step_10(), - panel_indent_guide: neutral().light_alpha().step_5(), - panel_indent_guide_hover: neutral().light_alpha().step_6(), - panel_indent_guide_active: neutral().light_alpha().step_6(), - panel_overlay_background: neutral().light().step_2(), - panel_overlay_hover: neutral().light().step_4(), - pane_focused_border: blue().light().step_5(), - pane_group_border: neutral().light().step_6(), - scrollbar_thumb_background: neutral().light_alpha().step_3(), - scrollbar_thumb_hover_background: neutral().light_alpha().step_4(), - scrollbar_thumb_active_background: neutral().light_alpha().step_5(), - scrollbar_thumb_border: gpui::transparent_black(), - scrollbar_track_background: gpui::transparent_black(), - scrollbar_track_border: neutral().light().step_5(), - minimap_thumb_background: neutral().light_alpha().step_3().alpha(0.7), - minimap_thumb_hover_background: neutral().light_alpha().step_4().alpha(0.7), - minimap_thumb_active_background: neutral().light_alpha().step_5().alpha(0.7), - minimap_thumb_border: gpui::transparent_black(), - editor_foreground: neutral().light().step_12(), - editor_background: neutral().light().step_1(), - editor_gutter_background: neutral().light().step_1(), - editor_subheader_background: neutral().light().step_2(), - editor_active_line_background: neutral().light_alpha().step_3(), - editor_highlighted_line_background: neutral().light_alpha().step_3(), - editor_debugger_active_line_background: yellow().dark_alpha().step_3(), - editor_line_number: neutral().light().step_10(), - editor_hover_line_number: neutral().light().step_12(), - editor_active_line_number: neutral().light().step_11(), - editor_invisible: neutral().light().step_10(), - editor_wrap_guide: neutral().light_alpha().step_7(), - editor_active_wrap_guide: neutral().light_alpha().step_8(), - editor_indent_guide: neutral().light_alpha().step_5(), - editor_indent_guide_active: neutral().light_alpha().step_6(), - editor_document_highlight_read_background: neutral().light_alpha().step_3(), - editor_document_highlight_write_background: neutral().light_alpha().step_4(), - editor_document_highlight_bracket_background: green().light_alpha().step_5(), - terminal_background: neutral().light().step_1(), - terminal_foreground: black().light().step_12(), - terminal_bright_foreground: black().light().step_11(), - terminal_dim_foreground: black().light().step_10(), - terminal_ansi_background: neutral().light().step_1(), - terminal_ansi_bright_black: black().light().step_11(), - terminal_ansi_bright_red: red().light().step_10(), - terminal_ansi_bright_green: green().light().step_10(), - terminal_ansi_bright_yellow: yellow().light().step_10(), - terminal_ansi_bright_blue: blue().light().step_10(), - terminal_ansi_bright_magenta: violet().light().step_10(), - terminal_ansi_bright_cyan: cyan().light().step_10(), - terminal_ansi_bright_white: neutral().light().step_11(), - terminal_ansi_black: black().light().step_12(), - terminal_ansi_red: red().light().step_11(), - terminal_ansi_green: green().light().step_11(), - terminal_ansi_yellow: yellow().light().step_11(), - terminal_ansi_blue: blue().light().step_11(), - terminal_ansi_magenta: violet().light().step_11(), - terminal_ansi_cyan: cyan().light().step_11(), - terminal_ansi_white: neutral().light().step_12(), - terminal_ansi_dim_black: black().light().step_11(), - terminal_ansi_dim_red: red().light().step_10(), - terminal_ansi_dim_green: green().light().step_10(), - terminal_ansi_dim_yellow: yellow().light().step_10(), - terminal_ansi_dim_blue: blue().light().step_10(), - terminal_ansi_dim_magenta: violet().light().step_10(), - terminal_ansi_dim_cyan: cyan().light().step_10(), - terminal_ansi_dim_white: neutral().light().step_11(), - link_text_hover: orange().light().step_10(), - version_control_added: ADDED_COLOR, - version_control_deleted: REMOVED_COLOR, - version_control_modified: MODIFIED_COLOR, - version_control_renamed: MODIFIED_COLOR, - version_control_conflict: orange().light().step_12(), - version_control_ignored: gray().light().step_12(), - version_control_word_added: WORD_ADDED_COLOR, - version_control_word_deleted: WORD_DELETED_COLOR, - version_control_conflict_marker_ours: green().light().step_10().alpha(0.5), - version_control_conflict_marker_theirs: blue().light().step_10().alpha(0.5), - vim_normal_background: system.transparent, - vim_insert_background: system.transparent, - vim_replace_background: system.transparent, - vim_visual_background: system.transparent, - vim_visual_line_background: system.transparent, - vim_visual_block_background: system.transparent, - vim_helix_normal_background: system.transparent, - vim_helix_select_background: system.transparent, - vim_mode_text: system.transparent, - } - } - - /// Returns the default colors for dark themes. - /// - /// Themes that do not specify all colors are refined off of these defaults. - pub fn dark() -> Self { - let system = SystemColors::default(); - - Self { - border: neutral().dark().step_6(), - border_variant: neutral().dark().step_5(), - border_focused: blue().dark().step_5(), - border_selected: blue().dark().step_5(), - border_transparent: system.transparent, - border_disabled: neutral().dark().step_3(), - elevated_surface_background: neutral().dark().step_2(), - surface_background: neutral().dark().step_2(), - background: neutral().dark().step_1(), - element_background: neutral().dark().step_3(), - element_hover: neutral().dark_alpha().step_4(), - element_active: neutral().dark_alpha().step_5(), - element_selected: neutral().dark_alpha().step_5(), - element_disabled: neutral().dark_alpha().step_3(), - element_selection_background: blue().dark().step_3().alpha(0.25), - drop_target_background: blue().dark_alpha().step_2(), - drop_target_border: neutral().dark().step_12(), - ghost_element_background: system.transparent, - ghost_element_hover: neutral().dark_alpha().step_4(), - ghost_element_active: neutral().dark_alpha().step_5(), - ghost_element_selected: neutral().dark_alpha().step_5(), - ghost_element_disabled: neutral().dark_alpha().step_3(), - text: neutral().dark().step_12(), - text_muted: neutral().dark().step_11(), - text_placeholder: neutral().dark().step_10(), - text_disabled: neutral().dark().step_9(), - text_accent: blue().dark().step_11(), - icon: neutral().dark().step_11(), - icon_muted: neutral().dark().step_10(), - icon_disabled: neutral().dark().step_9(), - icon_placeholder: neutral().dark().step_10(), - icon_accent: blue().dark().step_11(), - debugger_accent: red().light().step_10(), - status_bar_background: neutral().dark().step_2(), - title_bar_background: neutral().dark().step_2(), - title_bar_inactive_background: neutral().dark().step_3(), - toolbar_background: neutral().dark().step_1(), - tab_bar_background: neutral().dark().step_2(), - tab_inactive_background: neutral().dark().step_2(), - tab_active_background: neutral().dark().step_1(), - search_match_background: neutral().dark().step_5(), - search_active_match_background: neutral().dark().step_3(), - panel_background: neutral().dark().step_2(), - panel_focused_border: blue().dark().step_8(), - panel_indent_guide: neutral().dark_alpha().step_4(), - panel_indent_guide_hover: neutral().dark_alpha().step_6(), - panel_indent_guide_active: neutral().dark_alpha().step_6(), - panel_overlay_background: neutral().dark().step_2(), - panel_overlay_hover: neutral().dark().step_4(), - pane_focused_border: blue().dark().step_5(), - pane_group_border: neutral().dark().step_6(), - scrollbar_thumb_background: neutral().dark_alpha().step_3(), - scrollbar_thumb_hover_background: neutral().dark_alpha().step_4(), - scrollbar_thumb_active_background: neutral().dark_alpha().step_5(), - scrollbar_thumb_border: gpui::transparent_black(), - scrollbar_track_background: gpui::transparent_black(), - scrollbar_track_border: neutral().dark().step_5(), - minimap_thumb_background: neutral().dark_alpha().step_3().alpha(0.7), - minimap_thumb_hover_background: neutral().dark_alpha().step_4().alpha(0.7), - minimap_thumb_active_background: neutral().dark_alpha().step_5().alpha(0.7), - minimap_thumb_border: gpui::transparent_black(), - editor_foreground: neutral().dark().step_12(), - editor_background: neutral().dark().step_1(), - editor_gutter_background: neutral().dark().step_1(), - editor_subheader_background: neutral().dark().step_3(), - editor_active_line_background: neutral().dark_alpha().step_3(), - editor_highlighted_line_background: yellow().dark_alpha().step_4(), - editor_debugger_active_line_background: yellow().dark_alpha().step_3(), - editor_line_number: neutral().dark_alpha().step_10(), - editor_hover_line_number: neutral().dark_alpha().step_12(), - editor_active_line_number: neutral().dark_alpha().step_11(), - editor_invisible: neutral().dark_alpha().step_4(), - editor_wrap_guide: neutral().dark_alpha().step_4(), - editor_active_wrap_guide: neutral().dark_alpha().step_4(), - editor_indent_guide: neutral().dark_alpha().step_4(), - editor_indent_guide_active: neutral().dark_alpha().step_6(), - editor_document_highlight_read_background: neutral().dark_alpha().step_4(), - editor_document_highlight_write_background: neutral().dark_alpha().step_4(), - editor_document_highlight_bracket_background: green().dark_alpha().step_6(), - terminal_background: neutral().dark().step_1(), - terminal_ansi_background: neutral().dark().step_1(), - terminal_foreground: white().dark().step_12(), - terminal_bright_foreground: white().dark().step_11(), - terminal_dim_foreground: white().dark().step_10(), - terminal_ansi_black: black().dark().step_12(), - terminal_ansi_bright_black: black().dark().step_11(), - terminal_ansi_dim_black: black().dark().step_10(), - terminal_ansi_red: red().dark().step_11(), - terminal_ansi_bright_red: red().dark().step_10(), - terminal_ansi_dim_red: red().dark().step_9(), - terminal_ansi_green: green().dark().step_11(), - terminal_ansi_bright_green: green().dark().step_10(), - terminal_ansi_dim_green: green().dark().step_9(), - terminal_ansi_yellow: yellow().dark().step_11(), - terminal_ansi_bright_yellow: yellow().dark().step_10(), - terminal_ansi_dim_yellow: yellow().dark().step_9(), - terminal_ansi_blue: blue().dark().step_11(), - terminal_ansi_bright_blue: blue().dark().step_10(), - terminal_ansi_dim_blue: blue().dark().step_9(), - terminal_ansi_magenta: violet().dark().step_11(), - terminal_ansi_bright_magenta: violet().dark().step_10(), - terminal_ansi_dim_magenta: violet().dark().step_9(), - terminal_ansi_cyan: cyan().dark().step_11(), - terminal_ansi_bright_cyan: cyan().dark().step_10(), - terminal_ansi_dim_cyan: cyan().dark().step_9(), - terminal_ansi_white: neutral().dark().step_12(), - terminal_ansi_bright_white: neutral().dark().step_11(), - terminal_ansi_dim_white: neutral().dark().step_10(), - link_text_hover: orange().dark().step_10(), - version_control_added: ADDED_COLOR, - version_control_deleted: REMOVED_COLOR, - version_control_modified: MODIFIED_COLOR, - version_control_renamed: MODIFIED_COLOR, - version_control_conflict: orange().dark().step_12(), - version_control_ignored: gray().dark().step_12(), - version_control_word_added: WORD_ADDED_COLOR, - version_control_word_deleted: WORD_DELETED_COLOR, - version_control_conflict_marker_ours: green().dark().step_10().alpha(0.5), - version_control_conflict_marker_theirs: blue().dark().step_10().alpha(0.5), - vim_normal_background: system.transparent, - vim_insert_background: system.transparent, - vim_replace_background: system.transparent, - vim_visual_background: system.transparent, - vim_visual_line_background: system.transparent, - vim_visual_block_background: system.transparent, - vim_helix_normal_background: system.transparent, - vim_helix_select_background: system.transparent, - vim_mode_text: system.transparent, - } - } -} - -type StaticColorScale = [&'static str; 12]; - -struct StaticColorScaleSet { - scale: &'static str, - light: StaticColorScale, - light_alpha: StaticColorScale, - dark: StaticColorScale, - dark_alpha: StaticColorScale, -} - -impl TryFrom for ColorScaleSet { - type Error = anyhow::Error; - - fn try_from(value: StaticColorScaleSet) -> Result { - fn to_color_scale(scale: StaticColorScale) -> anyhow::Result { - scale - .into_iter() - .map(|color| Rgba::try_from(color).map(Hsla::from)) - .collect::, _>>() - .map(ColorScale::from_iter) - } - - Ok(Self::new( - value.scale, - to_color_scale(value.light)?, - to_color_scale(value.light_alpha)?, - to_color_scale(value.dark)?, - to_color_scale(value.dark_alpha)?, - )) - } -} - -/// Color scales used to build the default themes. -pub fn default_color_scales() -> ColorScales { - ColorScales { - gray: gray(), - mauve: mauve(), - slate: slate(), - sage: sage(), - olive: olive(), - sand: sand(), - gold: gold(), - bronze: bronze(), - brown: brown(), - yellow: yellow(), - amber: amber(), - orange: orange(), - tomato: tomato(), - red: red(), - ruby: ruby(), - crimson: crimson(), - pink: pink(), - plum: plum(), - purple: purple(), - violet: violet(), - iris: iris(), - indigo: indigo(), - blue: blue(), - cyan: cyan(), - teal: teal(), - jade: jade(), - green: green(), - grass: grass(), - lime: lime(), - mint: mint(), - sky: sky(), - black: black(), - white: white(), - } -} - -pub(crate) fn gray() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Gray", - light: [ - "#fcfcfcff", - "#f9f9f9ff", - "#f0f0f0ff", - "#e8e8e8ff", - "#e0e0e0ff", - "#d9d9d9ff", - "#cececeff", - "#bbbbbbff", - "#8d8d8dff", - "#838383ff", - "#646464ff", - "#202020ff", - ], - light_alpha: [ - "#00000003", - "#00000006", - "#0000000f", - "#00000017", - "#0000001f", - "#00000026", - "#00000031", - "#00000044", - "#00000072", - "#0000007c", - "#0000009b", - "#000000df", - ], - dark: [ - "#111111ff", - "#191919ff", - "#222222ff", - "#2a2a2aff", - "#313131ff", - "#3a3a3aff", - "#484848ff", - "#606060ff", - "#6e6e6eff", - "#7b7b7bff", - "#b4b4b4ff", - "#eeeeeeff", - ], - dark_alpha: [ - "#00000000", - "#ffffff09", - "#ffffff12", - "#ffffff1b", - "#ffffff22", - "#ffffff2c", - "#ffffff3b", - "#ffffff55", - "#ffffff64", - "#ffffff72", - "#ffffffaf", - "#ffffffed", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn mauve() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Mauve", - light: [ - "#fdfcfdff", - "#faf9fbff", - "#f2eff3ff", - "#eae7ecff", - "#e3dfe6ff", - "#dbd8e0ff", - "#d0cdd7ff", - "#bcbac7ff", - "#8e8c99ff", - "#84828eff", - "#65636dff", - "#211f26ff", - ], - light_alpha: [ - "#55005503", - "#2b005506", - "#30004010", - "#20003618", - "#20003820", - "#14003527", - "#10003332", - "#08003145", - "#05001d73", - "#0500197d", - "#0400119c", - "#020008e0", - ], - dark: [ - "#121113ff", - "#1a191bff", - "#232225ff", - "#2b292dff", - "#323035ff", - "#3c393fff", - "#49474eff", - "#625f69ff", - "#6f6d78ff", - "#7c7a85ff", - "#b5b2bcff", - "#eeeef0ff", - ], - dark_alpha: [ - "#00000000", - "#f5f4f609", - "#ebeaf814", - "#eee5f81d", - "#efe6fe25", - "#f1e6fd30", - "#eee9ff40", - "#eee7ff5d", - "#eae6fd6e", - "#ece9fd7c", - "#f5f1ffb7", - "#fdfdffef", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn slate() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Slate", - light: [ - "#fcfcfdff", - "#f9f9fbff", - "#f0f0f3ff", - "#e8e8ecff", - "#e0e1e6ff", - "#d9d9e0ff", - "#cdced6ff", - "#b9bbc6ff", - "#8b8d98ff", - "#80838dff", - "#60646cff", - "#1c2024ff", - ], - light_alpha: [ - "#00005503", - "#00005506", - "#0000330f", - "#00002d17", - "#0009321f", - "#00002f26", - "#00062e32", - "#00083046", - "#00051d74", - "#00071b7f", - "#0007149f", - "#000509e3", - ], - dark: [ - "#111113ff", - "#18191bff", - "#212225ff", - "#272a2dff", - "#2e3135ff", - "#363a3fff", - "#43484eff", - "#5a6169ff", - "#696e77ff", - "#777b84ff", - "#b0b4baff", - "#edeef0ff", - ], - dark_alpha: [ - "#00000000", - "#d8f4f609", - "#ddeaf814", - "#d3edf81d", - "#d9edfe25", - "#d6ebfd30", - "#d9edff40", - "#d9edff5d", - "#dfebfd6d", - "#e5edfd7b", - "#f1f7feb5", - "#fcfdffef", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn sage() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Sage", - light: [ - "#fbfdfcff", - "#f7f9f8ff", - "#eef1f0ff", - "#e6e9e8ff", - "#dfe2e0ff", - "#d7dad9ff", - "#cbcfcdff", - "#b8bcbaff", - "#868e8bff", - "#7c8481ff", - "#5f6563ff", - "#1a211eff", - ], - light_alpha: [ - "#00804004", - "#00402008", - "#002d1e11", - "#001f1519", - "#00180820", - "#00140d28", - "#00140a34", - "#000f0847", - "#00110b79", - "#00100a83", - "#000a07a0", - "#000805e5", - ], - dark: [ - "#101211ff", - "#171918ff", - "#202221ff", - "#272a29ff", - "#2e3130ff", - "#373b39ff", - "#444947ff", - "#5b625fff", - "#63706bff", - "#717d79ff", - "#adb5b2ff", - "#eceeedff", - ], - dark_alpha: [ - "#00000000", - "#f0f2f108", - "#f3f5f412", - "#f2fefd1a", - "#f1fbfa22", - "#edfbf42d", - "#edfcf73c", - "#ebfdf657", - "#dffdf266", - "#e5fdf674", - "#f4fefbb0", - "#fdfffeed", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn olive() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Olive", - light: [ - "#fcfdfcff", - "#f8faf8ff", - "#eff1efff", - "#e7e9e7ff", - "#dfe2dfff", - "#d7dad7ff", - "#cccfccff", - "#b9bcb8ff", - "#898e87ff", - "#7f847dff", - "#60655fff", - "#1d211cff", - ], - light_alpha: [ - "#00550003", - "#00490007", - "#00200010", - "#00160018", - "#00180020", - "#00140028", - "#000f0033", - "#040f0047", - "#050f0078", - "#040e0082", - "#020a00a0", - "#010600e3", - ], - dark: [ - "#111210ff", - "#181917ff", - "#212220ff", - "#282a27ff", - "#2f312eff", - "#383a36ff", - "#454843ff", - "#5c625bff", - "#687066ff", - "#767d74ff", - "#afb5adff", - "#eceeecff", - ], - dark_alpha: [ - "#00000000", - "#f1f2f008", - "#f4f5f312", - "#f3fef21a", - "#f2fbf122", - "#f4faed2c", - "#f2fced3b", - "#edfdeb57", - "#ebfde766", - "#f0fdec74", - "#f6fef4b0", - "#fdfffded", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn sand() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Sand", - light: [ - "#fdfdfcff", - "#f9f9f8ff", - "#f1f0efff", - "#e9e8e6ff", - "#e2e1deff", - "#dad9d6ff", - "#cfcecaff", - "#bcbbb5ff", - "#8d8d86ff", - "#82827cff", - "#63635eff", - "#21201cff", - ], - light_alpha: [ - "#55550003", - "#25250007", - "#20100010", - "#1f150019", - "#1f180021", - "#19130029", - "#19140035", - "#1915014a", - "#0f0f0079", - "#0c0c0083", - "#080800a1", - "#060500e3", - ], - dark: [ - "#111110ff", - "#191918ff", - "#222221ff", - "#2a2a28ff", - "#31312eff", - "#3b3a37ff", - "#494844ff", - "#62605bff", - "#6f6d66ff", - "#7c7b74ff", - "#b5b3adff", - "#eeeeecff", - ], - dark_alpha: [ - "#00000000", - "#f4f4f309", - "#f6f6f513", - "#fefef31b", - "#fbfbeb23", - "#fffaed2d", - "#fffbed3c", - "#fff9eb57", - "#fffae965", - "#fffdee73", - "#fffcf4b0", - "#fffffded", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn gold() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Gold", - light: [ - "#fdfdfcff", - "#faf9f2ff", - "#f2f0e7ff", - "#eae6dbff", - "#e1dccfff", - "#d8d0bfff", - "#cbc0aaff", - "#b9a88dff", - "#978365ff", - "#8c7a5eff", - "#71624bff", - "#3b352bff", - ], - light_alpha: [ - "#55550003", - "#9d8a000d", - "#75600018", - "#6b4e0024", - "#60460030", - "#64440040", - "#63420055", - "#633d0072", - "#5332009a", - "#492d00a1", - "#362100b4", - "#130c00d4", - ], - dark: [ - "#121211ff", - "#1b1a17ff", - "#24231fff", - "#2d2b26ff", - "#38352eff", - "#444039ff", - "#544f46ff", - "#696256ff", - "#978365ff", - "#a39073ff", - "#cbb99fff", - "#e8e2d9ff", - ], - dark_alpha: [ - "#91911102", - "#f9e29d0b", - "#f8ecbb15", - "#ffeec41e", - "#feecc22a", - "#feebcb37", - "#ffedcd48", - "#fdeaca5f", - "#ffdba690", - "#fedfb09d", - "#fee7c6c8", - "#fef7ede7", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn bronze() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Bronze", - light: [ - "#fdfcfcff", - "#fdf7f5ff", - "#f6edeaff", - "#efe4dfff", - "#e7d9d3ff", - "#dfcdc5ff", - "#d3bcb3ff", - "#c2a499ff", - "#a18072ff", - "#957468ff", - "#7d5e54ff", - "#43302bff", - ], - light_alpha: [ - "#55000003", - "#cc33000a", - "#92250015", - "#80280020", - "#7423002c", - "#7324003a", - "#6c1f004c", - "#671c0066", - "#551a008d", - "#4c150097", - "#3d0f00ab", - "#1d0600d4", - ], - dark: [ - "#141110ff", - "#1c1917ff", - "#262220ff", - "#302a27ff", - "#3b3330ff", - "#493e3aff", - "#5a4c47ff", - "#6f5f58ff", - "#a18072ff", - "#ae8c7eff", - "#d4b3a5ff", - "#ede0d9ff", - ], - dark_alpha: [ - "#d1110004", - "#fbbc910c", - "#faceb817", - "#facdb622", - "#ffd2c12d", - "#ffd1c03c", - "#fdd0c04f", - "#ffd6c565", - "#fec7b09b", - "#fecab5a9", - "#ffd7c6d1", - "#fff1e9ec", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn brown() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Brown", - light: [ - "#fefdfcff", - "#fcf9f6ff", - "#f6eee7ff", - "#f0e4d9ff", - "#ebdacaff", - "#e4cdb7ff", - "#dcbc9fff", - "#cea37eff", - "#ad7f58ff", - "#a07553ff", - "#815e46ff", - "#3e332eff", - ], - light_alpha: [ - "#aa550003", - "#aa550009", - "#a04b0018", - "#9b4a0026", - "#9f4d0035", - "#a04e0048", - "#a34e0060", - "#9f4a0081", - "#823c00a7", - "#723300ac", - "#522100b9", - "#140600d1", - ], - dark: [ - "#12110fff", - "#1c1816ff", - "#28211dff", - "#322922ff", - "#3e3128ff", - "#4d3c2fff", - "#614a39ff", - "#7c5f46ff", - "#ad7f58ff", - "#b88c67ff", - "#dbb594ff", - "#f2e1caff", - ], - dark_alpha: [ - "#91110002", - "#fba67c0c", - "#fcb58c19", - "#fbbb8a24", - "#fcb88931", - "#fdba8741", - "#ffbb8856", - "#ffbe8773", - "#feb87da8", - "#ffc18cb3", - "#fed1aad9", - "#feecd4f2", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn yellow() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Yellow", - light: [ - "#fdfdf9ff", - "#fefce9ff", - "#fffab8ff", - "#fff394ff", - "#ffe770ff", - "#f3d768ff", - "#e4c767ff", - "#d5ae39ff", - "#ffe629ff", - "#ffdc00ff", - "#9e6c00ff", - "#473b1fff", - ], - light_alpha: [ - "#aaaa0006", - "#f4dd0016", - "#ffee0047", - "#ffe3016b", - "#ffd5008f", - "#ebbc0097", - "#d2a10098", - "#c99700c6", - "#ffe100d6", - "#ffdc00ff", - "#9e6c00ff", - "#2e2000e0", - ], - dark: [ - "#14120bff", - "#1b180fff", - "#2d2305ff", - "#362b00ff", - "#433500ff", - "#524202ff", - "#665417ff", - "#836a21ff", - "#ffe629ff", - "#ffff57ff", - "#f5e147ff", - "#f6eeb4ff", - ], - dark_alpha: [ - "#d1510004", - "#f9b4000b", - "#ffaa001e", - "#fdb70028", - "#febb0036", - "#fec40046", - "#fdcb225c", - "#fdca327b", - "#ffe629ff", - "#ffff57ff", - "#fee949f5", - "#fef6baf6", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn amber() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Amber", - light: [ - "#fefdfbff", - "#fefbe9ff", - "#fff7c2ff", - "#ffee9cff", - "#fbe577ff", - "#f3d673ff", - "#e9c162ff", - "#e2a336ff", - "#ffc53dff", - "#ffba18ff", - "#ab6400ff", - "#4f3422ff", - ], - light_alpha: [ - "#c0800004", - "#f4d10016", - "#ffde003d", - "#ffd40063", - "#f8cf0088", - "#eab5008c", - "#dc9b009d", - "#da8a00c9", - "#ffb300c2", - "#ffb300e7", - "#ab6400ff", - "#341500dd", - ], - dark: [ - "#16120cff", - "#1d180fff", - "#302008ff", - "#3f2700ff", - "#4d3000ff", - "#5c3d05ff", - "#714f19ff", - "#8f6424ff", - "#ffc53dff", - "#ffd60aff", - "#ffca16ff", - "#ffe7b3ff", - ], - dark_alpha: [ - "#e63c0006", - "#fd9b000d", - "#fa820022", - "#fc820032", - "#fd8b0041", - "#fd9b0051", - "#ffab2567", - "#ffae3587", - "#ffc53dff", - "#ffd60aff", - "#ffca16ff", - "#ffe7b3ff", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn orange() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Orange", - light: [ - "#fefcfbff", - "#fff7edff", - "#ffefd6ff", - "#ffdfb5ff", - "#ffd19aff", - "#ffc182ff", - "#f5ae73ff", - "#ec9455ff", - "#f76b15ff", - "#ef5f00ff", - "#cc4e00ff", - "#582d1dff", - ], - light_alpha: [ - "#c0400004", - "#ff8e0012", - "#ff9c0029", - "#ff91014a", - "#ff8b0065", - "#ff81007d", - "#ed6c008c", - "#e35f00aa", - "#f65e00ea", - "#ef5f00ff", - "#cc4e00ff", - "#431200e2", - ], - dark: [ - "#17120eff", - "#1e160fff", - "#331e0bff", - "#462100ff", - "#562800ff", - "#66350cff", - "#7e451dff", - "#a35829ff", - "#f76b15ff", - "#ff801fff", - "#ffa057ff", - "#ffe0c2ff", - ], - dark_alpha: [ - "#ec360007", - "#fe6d000e", - "#fb6a0025", - "#ff590039", - "#ff61004a", - "#fd75045c", - "#ff832c75", - "#fe84389d", - "#fe6d15f7", - "#ff801fff", - "#ffa057ff", - "#ffe0c2ff", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn tomato() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Tomato", - light: [ - "#fffcfcff", - "#fff8f7ff", - "#feebe7ff", - "#ffdcd3ff", - "#ffcdc2ff", - "#fdbdafff", - "#f5a898ff", - "#ec8e7bff", - "#e54d2eff", - "#dd4425ff", - "#d13415ff", - "#5c271fff", - ], - light_alpha: [ - "#ff000003", - "#ff200008", - "#f52b0018", - "#ff35002c", - "#ff2e003d", - "#f92d0050", - "#e7280067", - "#db250084", - "#df2600d1", - "#d72400da", - "#cd2200ea", - "#460900e0", - ], - dark: [ - "#181111ff", - "#1f1513ff", - "#391714ff", - "#4e1511ff", - "#5e1c16ff", - "#6e2920ff", - "#853a2dff", - "#ac4d39ff", - "#e54d2eff", - "#ec6142ff", - "#ff977dff", - "#fbd3cbff", - ], - dark_alpha: [ - "#f1121208", - "#ff55330f", - "#ff35232b", - "#fd201142", - "#fe332153", - "#ff4f3864", - "#fd644a7d", - "#fe6d4ea7", - "#fe5431e4", - "#ff6847eb", - "#ff977dff", - "#ffd6cefb", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn red() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Red", - light: [ - "#fffcfcff", - "#fff7f7ff", - "#feebecff", - "#ffdbdcff", - "#ffcdceff", - "#fdbdbeff", - "#f4a9aaff", - "#eb8e90ff", - "#e5484dff", - "#dc3e42ff", - "#ce2c31ff", - "#641723ff", - ], - light_alpha: [ - "#ff000003", - "#ff000008", - "#f3000d14", - "#ff000824", - "#ff000632", - "#f8000442", - "#df000356", - "#d2000571", - "#db0007b7", - "#d10005c1", - "#c40006d3", - "#55000de8", - ], - dark: [ - "#191111ff", - "#201314ff", - "#3b1219ff", - "#500f1cff", - "#611623ff", - "#72232dff", - "#8c333aff", - "#b54548ff", - "#e5484dff", - "#ec5d5eff", - "#ff9592ff", - "#ffd1d9ff", - ], - dark_alpha: [ - "#f4121209", - "#f22f3e11", - "#ff173f2d", - "#fe0a3b44", - "#ff204756", - "#ff3e5668", - "#ff536184", - "#ff5d61b0", - "#fe4e54e4", - "#ff6465eb", - "#ff9592ff", - "#ffd1d9ff", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn ruby() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Ruby", - light: [ - "#fffcfdff", - "#fff7f8ff", - "#feeaedff", - "#ffdce1ff", - "#ffced6ff", - "#f8bfc8ff", - "#efacb8ff", - "#e592a3ff", - "#e54666ff", - "#dc3b5dff", - "#ca244dff", - "#64172bff", - ], - light_alpha: [ - "#ff005503", - "#ff002008", - "#f3002515", - "#ff002523", - "#ff002a31", - "#e4002440", - "#ce002553", - "#c300286d", - "#db002cb9", - "#d2002cc4", - "#c10030db", - "#550016e8", - ], - dark: [ - "#191113ff", - "#1e1517ff", - "#3a141eff", - "#4e1325ff", - "#5e1a2eff", - "#6f2539ff", - "#883447ff", - "#b3445aff", - "#e54666ff", - "#ec5a72ff", - "#ff949dff", - "#fed2e1ff", - ], - dark_alpha: [ - "#f4124a09", - "#fe5a7f0e", - "#ff235d2c", - "#fd195e42", - "#fe2d6b53", - "#ff447665", - "#ff577d80", - "#ff5c7cae", - "#fe4c70e4", - "#ff617beb", - "#ff949dff", - "#ffd3e2fe", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn crimson() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Crimson", - light: [ - "#fffcfdff", - "#fef7f9ff", - "#ffe9f0ff", - "#fedce7ff", - "#faceddff", - "#f3bed1ff", - "#eaacc3ff", - "#e093b2ff", - "#e93d82ff", - "#df3478ff", - "#cb1d63ff", - "#621639ff", - ], - light_alpha: [ - "#ff005503", - "#e0004008", - "#ff005216", - "#f8005123", - "#e5004f31", - "#d0004b41", - "#bf004753", - "#b6004a6c", - "#e2005bc2", - "#d70056cb", - "#c4004fe2", - "#530026e9", - ], - dark: [ - "#191114ff", - "#201318ff", - "#381525ff", - "#4d122fff", - "#5c1839ff", - "#6d2545ff", - "#873356ff", - "#b0436eff", - "#e93d82ff", - "#ee518aff", - "#ff92adff", - "#fdd3e8ff", - ], - dark_alpha: [ - "#f4126709", - "#f22f7a11", - "#fe2a8b2a", - "#fd158741", - "#fd278f51", - "#fe459763", - "#fd559b7f", - "#fe5b9bab", - "#fe418de8", - "#ff5693ed", - "#ff92adff", - "#ffd5eafd", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn pink() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Pink", - light: [ - "#fffcfeff", - "#fef7fbff", - "#fee9f5ff", - "#fbdcefff", - "#f6cee7ff", - "#efbfddff", - "#e7acd0ff", - "#dd93c2ff", - "#d6409fff", - "#cf3897ff", - "#c2298aff", - "#651249ff", - ], - light_alpha: [ - "#ff00aa03", - "#e0008008", - "#f4008c16", - "#e2008b23", - "#d1008331", - "#c0007840", - "#b6006f53", - "#af006f6c", - "#c8007fbf", - "#c2007ac7", - "#b60074d6", - "#59003bed", - ], - dark: [ - "#191117ff", - "#21121dff", - "#37172fff", - "#4b143dff", - "#591c47ff", - "#692955ff", - "#833869ff", - "#a84885ff", - "#d6409fff", - "#de51a8ff", - "#ff8dccff", - "#fdd1eaff", - ], - dark_alpha: [ - "#f412bc09", - "#f420bb12", - "#fe37cc29", - "#fc1ec43f", - "#fd35c24e", - "#fd51c75f", - "#fd62c87b", - "#ff68c8a2", - "#fe49bcd4", - "#ff5cc0dc", - "#ff8dccff", - "#ffd3ecfd", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn plum() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Plum", - light: [ - "#fefcffff", - "#fdf7fdff", - "#fbebfbff", - "#f7def8ff", - "#f2d1f3ff", - "#e9c2ecff", - "#deade3ff", - "#cf91d8ff", - "#ab4abaff", - "#a144afff", - "#953ea3ff", - "#53195dff", - ], - light_alpha: [ - "#aa00ff03", - "#c000c008", - "#cc00cc14", - "#c200c921", - "#b700bd2e", - "#a400b03d", - "#9900a852", - "#9000a56e", - "#89009eb5", - "#7f0092bb", - "#730086c1", - "#40004be6", - ], - dark: [ - "#181118ff", - "#201320ff", - "#351a35ff", - "#451d47ff", - "#512454ff", - "#5e3061ff", - "#734079ff", - "#92549cff", - "#ab4abaff", - "#b658c4ff", - "#e796f3ff", - "#f4d4f4ff", - ], - dark_alpha: [ - "#f112f108", - "#f22ff211", - "#fd4cfd27", - "#f646ff3a", - "#f455ff48", - "#f66dff56", - "#f07cfd70", - "#ee84ff95", - "#e961feb6", - "#ed70ffc0", - "#f19cfef3", - "#feddfef4", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn purple() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Purple", - light: [ - "#fefcfeff", - "#fbf7feff", - "#f7edfeff", - "#f2e2fcff", - "#ead5f9ff", - "#e0c4f4ff", - "#d1afecff", - "#be93e4ff", - "#8e4ec6ff", - "#8347b9ff", - "#8145b5ff", - "#402060ff", - ], - light_alpha: [ - "#aa00aa03", - "#8000e008", - "#8e00f112", - "#8d00e51d", - "#8000db2a", - "#7a01d03b", - "#6d00c350", - "#6600c06c", - "#5c00adb1", - "#53009eb8", - "#52009aba", - "#250049df", - ], - dark: [ - "#18111bff", - "#1e1523ff", - "#301c3bff", - "#3d224eff", - "#48295cff", - "#54346bff", - "#664282ff", - "#8457aaff", - "#8e4ec6ff", - "#9a5cd0ff", - "#d19dffff", - "#ecd9faff", - ], - dark_alpha: [ - "#b412f90b", - "#b744f714", - "#c150ff2d", - "#bb53fd42", - "#be5cfd51", - "#c16dfd61", - "#c378fd7a", - "#c47effa4", - "#b661ffc2", - "#bc6fffcd", - "#d19dffff", - "#f1ddfffa", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn violet() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Violet", - light: [ - "#fdfcfeff", - "#faf8ffff", - "#f4f0feff", - "#ebe4ffff", - "#e1d9ffff", - "#d4cafeff", - "#c2b5f5ff", - "#aa99ecff", - "#6e56cfff", - "#654dc4ff", - "#6550b9ff", - "#2f265fff", - ], - light_alpha: [ - "#5500aa03", - "#4900ff07", - "#4400ee0f", - "#4300ff1b", - "#3600ff26", - "#3100fb35", - "#2d01dd4a", - "#2b00d066", - "#2400b7a9", - "#2300abb2", - "#1f0099af", - "#0b0043d9", - ], - dark: [ - "#14121fff", - "#1b1525ff", - "#291f43ff", - "#33255bff", - "#3c2e69ff", - "#473876ff", - "#56468bff", - "#6958adff", - "#6e56cfff", - "#7d66d9ff", - "#baa7ffff", - "#e2ddfeff", - ], - dark_alpha: [ - "#4422ff0f", - "#853ff916", - "#8354fe36", - "#7d51fd50", - "#845ffd5f", - "#8f6cfd6d", - "#9879ff83", - "#977dfea8", - "#8668ffcc", - "#9176fed7", - "#baa7ffff", - "#e3defffe", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn iris() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Iris", - light: [ - "#fdfdffff", - "#f8f8ffff", - "#f0f1feff", - "#e6e7ffff", - "#dadcffff", - "#cbcdffff", - "#b8baf8ff", - "#9b9ef0ff", - "#5b5bd6ff", - "#5151cdff", - "#5753c6ff", - "#272962ff", - ], - light_alpha: [ - "#0000ff02", - "#0000ff07", - "#0011ee0f", - "#000bff19", - "#000eff25", - "#000aff34", - "#0008e647", - "#0008d964", - "#0000c0a4", - "#0000b6ae", - "#0600abac", - "#000246d8", - ], - dark: [ - "#13131eff", - "#171625ff", - "#202248ff", - "#262a65ff", - "#303374ff", - "#3d3e82ff", - "#4a4a95ff", - "#5958b1ff", - "#5b5bd6ff", - "#6e6adeff", - "#b1a9ffff", - "#e0dffeff", - ], - dark_alpha: [ - "#3636fe0e", - "#564bf916", - "#525bff3b", - "#4d58ff5a", - "#5b62fd6b", - "#6d6ffd7a", - "#7777fe8e", - "#7b7afeac", - "#6a6afed4", - "#7d79ffdc", - "#b1a9ffff", - "#e1e0fffe", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn indigo() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Indigo", - light: [ - "#fdfdfeff", - "#f7f9ffff", - "#edf2feff", - "#e1e9ffff", - "#d2deffff", - "#c1d0ffff", - "#abbdf9ff", - "#8da4efff", - "#3e63ddff", - "#3358d4ff", - "#3a5bc7ff", - "#1f2d5cff", - ], - light_alpha: [ - "#00008002", - "#0040ff08", - "#0047f112", - "#0044ff1e", - "#0044ff2d", - "#003eff3e", - "#0037ed54", - "#0034dc72", - "#0031d2c1", - "#002ec9cc", - "#002bb7c5", - "#001046e0", - ], - dark: [ - "#11131fff", - "#141726ff", - "#182449ff", - "#1d2e62ff", - "#253974ff", - "#304384ff", - "#3a4f97ff", - "#435db1ff", - "#3e63ddff", - "#5472e4ff", - "#9eb1ffff", - "#d6e1ffff", - ], - dark_alpha: [ - "#1133ff0f", - "#3354fa17", - "#2f62ff3c", - "#3566ff57", - "#4171fd6b", - "#5178fd7c", - "#5a7fff90", - "#5b81feac", - "#4671ffdb", - "#5c7efee3", - "#9eb1ffff", - "#d6e1ffff", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn blue() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Blue", - light: [ - "#fbfdffff", - "#f4faffff", - "#e6f4feff", - "#d5efffff", - "#c2e5ffff", - "#acd8fcff", - "#8ec8f6ff", - "#5eb1efff", - "#0090ffff", - "#0588f0ff", - "#0d74ceff", - "#113264ff", - ], - light_alpha: [ - "#0080ff04", - "#008cff0b", - "#008ff519", - "#009eff2a", - "#0093ff3d", - "#0088f653", - "#0083eb71", - "#0084e6a1", - "#0090ffff", - "#0086f0fa", - "#006dcbf2", - "#002359ee", - ], - dark: [ - "#0d1520ff", - "#111927ff", - "#0d2847ff", - "#003362ff", - "#004074ff", - "#104d87ff", - "#205d9eff", - "#2870bdff", - "#0090ffff", - "#3b9effff", - "#70b8ffff", - "#c2e6ffff", - ], - dark_alpha: [ - "#004df211", - "#1166fb18", - "#0077ff3a", - "#0075ff57", - "#0081fd6b", - "#0f89fd7f", - "#2a91fe98", - "#3094feb9", - "#0090ffff", - "#3b9effff", - "#70b8ffff", - "#c2e6ffff", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn cyan() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Cyan", - light: [ - "#fafdfeff", - "#f2fafbff", - "#def7f9ff", - "#caf1f6ff", - "#b5e9f0ff", - "#9ddde7ff", - "#7dcedcff", - "#3db9cfff", - "#00a2c7ff", - "#0797b9ff", - "#107d98ff", - "#0d3c48ff", - ], - light_alpha: [ - "#0099cc05", - "#009db10d", - "#00c2d121", - "#00bcd435", - "#01b4cc4a", - "#00a7c162", - "#009fbb82", - "#00a3c0c2", - "#00a2c7ff", - "#0094b7f8", - "#007491ef", - "#00323ef2", - ], - dark: [ - "#0b161aff", - "#101b20ff", - "#082c36ff", - "#003848ff", - "#004558ff", - "#045468ff", - "#12677eff", - "#11809cff", - "#00a2c7ff", - "#23afd0ff", - "#4ccce6ff", - "#b6ecf7ff", - ], - dark_alpha: [ - "#0091f70a", - "#02a7f211", - "#00befd28", - "#00baff3b", - "#00befd4d", - "#00c7fd5e", - "#14cdff75", - "#11cfff95", - "#00cfffc3", - "#28d6ffcd", - "#52e1fee5", - "#bbf3fef7", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn teal() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Teal", - light: [ - "#fafefdff", - "#f3fbf9ff", - "#e0f8f3ff", - "#ccf3eaff", - "#b8eae0ff", - "#a1ded2ff", - "#83cdc1ff", - "#53b9abff", - "#12a594ff", - "#0d9b8aff", - "#008573ff", - "#0d3d38ff", - ], - light_alpha: [ - "#00cc9905", - "#00aa800c", - "#00c69d1f", - "#00c39633", - "#00b49047", - "#00a6855e", - "#0099807c", - "#009783ac", - "#009e8ced", - "#009684f2", - "#008573ff", - "#00332df2", - ], - dark: [ - "#0d1514ff", - "#111c1bff", - "#0d2d2aff", - "#023b37ff", - "#084843ff", - "#145750ff", - "#1c6961ff", - "#207e73ff", - "#12a594ff", - "#0eb39eff", - "#0bd8b6ff", - "#adf0ddff", - ], - dark_alpha: [ - "#00deab05", - "#12fbe60c", - "#00ffe61e", - "#00ffe92d", - "#00ffea3b", - "#1cffe84b", - "#2efde85f", - "#32ffe775", - "#13ffe49f", - "#0dffe0ae", - "#0afed5d6", - "#b8ffebef", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn jade() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Jade", - light: [ - "#fbfefdff", - "#f4fbf7ff", - "#e6f7edff", - "#d6f1e3ff", - "#c3e9d7ff", - "#acdec8ff", - "#8bceb6ff", - "#56ba9fff", - "#29a383ff", - "#26997bff", - "#208368ff", - "#1d3b31ff", - ], - light_alpha: [ - "#00c08004", - "#00a3460b", - "#00ae4819", - "#00a85129", - "#00a2553c", - "#009a5753", - "#00945f74", - "#00976ea9", - "#00916bd6", - "#008764d9", - "#007152df", - "#002217e2", - ], - dark: [ - "#0d1512ff", - "#121c18ff", - "#0f2e22ff", - "#0b3b2cff", - "#114837ff", - "#1b5745ff", - "#246854ff", - "#2a7e68ff", - "#29a383ff", - "#27b08bff", - "#1fd8a4ff", - "#adf0d4ff", - ], - dark_alpha: [ - "#00de4505", - "#27fba60c", - "#02f99920", - "#00ffaa2d", - "#11ffb63b", - "#34ffc24b", - "#45fdc75e", - "#48ffcf75", - "#38feca9d", - "#31fec7ab", - "#21fec0d6", - "#b8ffe1ef", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn green() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Green", - light: [ - "#fbfefcff", - "#f4fbf6ff", - "#e6f6ebff", - "#d6f1dfff", - "#c4e8d1ff", - "#adddc0ff", - "#8eceaaff", - "#5bb98bff", - "#30a46cff", - "#2b9a66ff", - "#218358ff", - "#193b2dff", - ], - light_alpha: [ - "#00c04004", - "#00a32f0b", - "#00a43319", - "#00a83829", - "#019c393b", - "#00963c52", - "#00914071", - "#00924ba4", - "#008f4acf", - "#008647d4", - "#00713fde", - "#002616e6", - ], - dark: [ - "#0e1512ff", - "#121b17ff", - "#132d21ff", - "#113b29ff", - "#174933ff", - "#20573eff", - "#28684aff", - "#2f7c57ff", - "#30a46cff", - "#33b074ff", - "#3dd68cff", - "#b1f1cbff", - ], - dark_alpha: [ - "#00de4505", - "#29f99d0b", - "#22ff991e", - "#11ff992d", - "#2bffa23c", - "#44ffaa4b", - "#50fdac5e", - "#54ffad73", - "#44ffa49e", - "#43fea4ab", - "#46fea5d4", - "#bbffd7f0", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn grass() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Grass", - light: [ - "#fbfefbff", - "#f5fbf5ff", - "#e9f6e9ff", - "#daf1dbff", - "#c9e8caff", - "#b2ddb5ff", - "#94ce9aff", - "#65ba74ff", - "#46a758ff", - "#3e9b4fff", - "#2a7e3bff", - "#203c25ff", - ], - light_alpha: [ - "#00c00004", - "#0099000a", - "#00970016", - "#009f0725", - "#00930536", - "#008f0a4d", - "#018b0f6b", - "#008d199a", - "#008619b9", - "#007b17c1", - "#006514d5", - "#002006df", - ], - dark: [ - "#0e1511ff", - "#141a15ff", - "#1b2a1eff", - "#1d3a24ff", - "#25482dff", - "#2d5736ff", - "#366740ff", - "#3e7949ff", - "#46a758ff", - "#53b365ff", - "#71d083ff", - "#c2f0c2ff", - ], - dark_alpha: [ - "#00de1205", - "#5ef7780a", - "#70fe8c1b", - "#57ff802c", - "#68ff8b3b", - "#71ff8f4b", - "#77fd925d", - "#77fd9070", - "#65ff82a1", - "#72ff8dae", - "#89ff9fcd", - "#ceffceef", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn lime() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Lime", - light: [ - "#fcfdfaff", - "#f8faf3ff", - "#eef6d6ff", - "#e2f0bdff", - "#d3e7a6ff", - "#c2da91ff", - "#abc978ff", - "#8db654ff", - "#bdee63ff", - "#b0e64cff", - "#5c7c2fff", - "#37401cff", - ], - light_alpha: [ - "#66990005", - "#6b95000c", - "#96c80029", - "#8fc60042", - "#81bb0059", - "#72aa006e", - "#61990087", - "#559200ab", - "#93e4009c", - "#8fdc00b3", - "#375f00d0", - "#1e2900e3", - ], - dark: [ - "#11130cff", - "#151a10ff", - "#1f2917ff", - "#29371dff", - "#334423ff", - "#3d522aff", - "#496231ff", - "#577538ff", - "#bdee63ff", - "#d4ff70ff", - "#bde56cff", - "#e3f7baff", - ], - dark_alpha: [ - "#11bb0003", - "#78f7000a", - "#9bfd4c1a", - "#a7fe5c29", - "#affe6537", - "#b2fe6d46", - "#b6ff6f57", - "#b6fd6d6c", - "#caff69ed", - "#d4ff70ff", - "#d1fe77e4", - "#e9febff7", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn mint() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Mint", - light: [ - "#f9fefdff", - "#f2fbf9ff", - "#ddf9f2ff", - "#c8f4e9ff", - "#b3ecdeff", - "#9ce0d0ff", - "#7ecfbdff", - "#4cbba5ff", - "#86ead4ff", - "#7de0cbff", - "#027864ff", - "#16433cff", - ], - light_alpha: [ - "#00d5aa06", - "#00b18a0d", - "#00d29e22", - "#00cc9937", - "#00c0914c", - "#00b08663", - "#00a17d81", - "#009e7fb3", - "#00d3a579", - "#00c39982", - "#007763fd", - "#00312ae9", - ], - dark: [ - "#0e1515ff", - "#0f1b1bff", - "#092c2bff", - "#003a38ff", - "#004744ff", - "#105650ff", - "#1e685fff", - "#277f70ff", - "#86ead4ff", - "#a8f5e5ff", - "#58d5baff", - "#c4f5e1ff", - ], - dark_alpha: [ - "#00dede05", - "#00f9f90b", - "#00fff61d", - "#00fff42c", - "#00fff23a", - "#0effeb4a", - "#34fde55e", - "#41ffdf76", - "#92ffe7e9", - "#aefeedf5", - "#67ffded2", - "#cbfee9f5", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn sky() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Sky", - light: [ - "#f9feffff", - "#f1fafdff", - "#e1f6fdff", - "#d1f0faff", - "#bee7f5ff", - "#a9daedff", - "#8dcae3ff", - "#60b3d7ff", - "#7ce2feff", - "#74daf8ff", - "#00749eff", - "#1d3e56ff", - ], - light_alpha: [ - "#00d5ff06", - "#00a4db0e", - "#00b3ee1e", - "#00ace42e", - "#00a1d841", - "#0092ca56", - "#0089c172", - "#0085bf9f", - "#00c7fe83", - "#00bcf38b", - "#00749eff", - "#002540e2", - ], - dark: [ - "#0d141fff", - "#111a27ff", - "#112840ff", - "#113555ff", - "#154467ff", - "#1b537bff", - "#1f6692ff", - "#197caeff", - "#7ce2feff", - "#a8eeffff", - "#75c7f0ff", - "#c2f3ffff", - ], - dark_alpha: [ - "#0044ff0f", - "#1171fb18", - "#1184fc33", - "#128fff49", - "#1c9dfd5d", - "#28a5ff72", - "#2badfe8b", - "#1db2fea9", - "#7ce3fffe", - "#a8eeffff", - "#7cd3ffef", - "#c2f3ffff", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn black() -> ColorScaleSet { - StaticColorScaleSet { - scale: "Black", - light: [ - "#0000000d", - "#0000001a", - "#00000026", - "#00000033", - "#0000004d", - "#00000066", - "#00000080", - "#00000099", - "#000000b3", - "#000000cc", - "#000000e6", - "#000000f2", - ], - light_alpha: [ - "#0000000d", - "#0000001a", - "#00000026", - "#00000033", - "#0000004d", - "#00000066", - "#00000080", - "#00000099", - "#000000b3", - "#000000cc", - "#000000e6", - "#000000f2", - ], - dark: [ - "#0000000d", - "#0000001a", - "#00000026", - "#00000033", - "#0000004d", - "#00000066", - "#00000080", - "#00000099", - "#000000b3", - "#000000cc", - "#000000e6", - "#000000f2", - ], - dark_alpha: [ - "#0000000d", - "#0000001a", - "#00000026", - "#00000033", - "#0000004d", - "#00000066", - "#00000080", - "#00000099", - "#000000b3", - "#000000cc", - "#000000e6", - "#000000f2", - ], - } - .try_into() - .unwrap() -} - -pub(crate) fn white() -> ColorScaleSet { - StaticColorScaleSet { - scale: "White", - light: [ - "#ffffff0d", - "#ffffff1a", - "#ffffff26", - "#ffffff33", - "#ffffff4d", - "#ffffff66", - "#ffffff80", - "#ffffff99", - "#ffffffb3", - "#ffffffcc", - "#ffffffe6", - "#fffffff2", - ], - light_alpha: [ - "#ffffff0d", - "#ffffff1a", - "#ffffff26", - "#ffffff33", - "#ffffff4d", - "#ffffff66", - "#ffffff80", - "#ffffff99", - "#ffffffb3", - "#ffffffcc", - "#ffffffe6", - "#fffffff2", - ], - dark: [ - "#ffffff0d", - "#ffffff1a", - "#ffffff26", - "#ffffff33", - "#ffffff4d", - "#ffffff66", - "#ffffff80", - "#ffffff99", - "#ffffffb3", - "#ffffffcc", - "#ffffffe6", - "#fffffff2", - ], - dark_alpha: [ - "#ffffff0d", - "#ffffff1a", - "#ffffff26", - "#ffffff33", - "#ffffff4d", - "#ffffff66", - "#ffffff80", - "#ffffff99", - "#ffffffb3", - "#ffffffcc", - "#ffffffe6", - "#fffffff2", - ], - } - .try_into() - .unwrap() -} diff --git a/crates/theme/src/fallback_themes.rs b/crates/theme/src/fallback_themes.rs deleted file mode 100644 index 6bfcb1c868..0000000000 --- a/crates/theme/src/fallback_themes.rs +++ /dev/null @@ -1,373 +0,0 @@ -use std::sync::Arc; - -use gpui::{FontStyle, FontWeight, HighlightStyle, Hsla, WindowBackgroundAppearance, hsla}; - -use crate::{ - AccentColors, Appearance, DEFAULT_DARK_THEME, PlayerColors, StatusColors, - StatusColorsRefinement, SyntaxTheme, SystemColors, Theme, ThemeColors, ThemeColorsRefinement, - ThemeFamily, ThemeStyles, default_color_scales, -}; - -/// The default theme family for Zed. -/// -/// This is used to construct the default theme fallback values, as well as to -/// have a theme available at compile time for tests. -pub fn zed_default_themes() -> ThemeFamily { - ThemeFamily { - id: "zed-default".to_string(), - name: "Zed Default".into(), - author: "".into(), - themes: vec![zed_default_dark()], - scales: default_color_scales(), - } -} - -// If a theme customizes a foreground version of a status color, but does not -// customize the background color, then use a partly-transparent version of the -// foreground color for the background color. -pub(crate) fn apply_status_color_defaults(status: &mut StatusColorsRefinement) { - for (fg_color, bg_color) in [ - (&status.deleted, &mut status.deleted_background), - (&status.created, &mut status.created_background), - (&status.modified, &mut status.modified_background), - (&status.conflict, &mut status.conflict_background), - (&status.error, &mut status.error_background), - (&status.hidden, &mut status.hidden_background), - ] { - if bg_color.is_none() - && let Some(fg_color) = fg_color - { - *bg_color = Some(fg_color.opacity(0.25)); - } - } -} - -pub(crate) fn apply_theme_color_defaults( - theme_colors: &mut ThemeColorsRefinement, - player_colors: &PlayerColors, -) { - if theme_colors.element_selection_background.is_none() { - let mut selection = player_colors.local().selection; - if selection.a == 1.0 { - selection.a = 0.25; - } - theme_colors.element_selection_background = Some(selection); - } -} - -pub(crate) fn zed_default_dark() -> Theme { - let bg = hsla(215. / 360., 12. / 100., 15. / 100., 1.); - let editor = hsla(220. / 360., 12. / 100., 18. / 100., 1.); - let elevated_surface = hsla(225. / 360., 12. / 100., 17. / 100., 1.); - let hover = hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 1.0); - - let blue = hsla(207.8 / 360., 81. / 100., 66. / 100., 1.0); - let gray = hsla(218.8 / 360., 10. / 100., 40. / 100., 1.0); - let green = hsla(95. / 360., 38. / 100., 62. / 100., 1.0); - let orange = hsla(29. / 360., 54. / 100., 61. / 100., 1.0); - let purple = hsla(286. / 360., 51. / 100., 64. / 100., 1.0); - let red = hsla(355. / 360., 65. / 100., 65. / 100., 1.0); - let teal = hsla(187. / 360., 47. / 100., 55. / 100., 1.0); - let yellow = hsla(39. / 360., 67. / 100., 69. / 100., 1.0); - - const ADDED_COLOR: Hsla = Hsla { - h: 134. / 360., - s: 0.55, - l: 0.40, - a: 1.0, - }; - const WORD_ADDED_COLOR: Hsla = Hsla { - h: 134. / 360., - s: 0.55, - l: 0.40, - a: 0.35, - }; - const MODIFIED_COLOR: Hsla = Hsla { - h: 48. / 360., - s: 0.76, - l: 0.47, - a: 1.0, - }; - const REMOVED_COLOR: Hsla = Hsla { - h: 350. / 360., - s: 0.88, - l: 0.25, - a: 1.0, - }; - const WORD_DELETED_COLOR: Hsla = Hsla { - h: 350. / 360., - s: 0.88, - l: 0.25, - a: 0.80, - }; - - let player = PlayerColors::dark(); - Theme { - id: "one_dark".to_string(), - name: DEFAULT_DARK_THEME.into(), - appearance: Appearance::Dark, - styles: ThemeStyles { - window_background_appearance: WindowBackgroundAppearance::Opaque, - system: SystemColors::default(), - accents: AccentColors(vec![blue, orange, purple, teal, red, green, yellow]), - colors: ThemeColors { - border: hsla(225. / 360., 13. / 100., 12. / 100., 1.), - border_variant: hsla(228. / 360., 8. / 100., 25. / 100., 1.), - border_focused: hsla(223. / 360., 78. / 100., 65. / 100., 1.), - border_selected: hsla(222.6 / 360., 77.5 / 100., 65.1 / 100., 1.0), - border_transparent: SystemColors::default().transparent, - border_disabled: hsla(222.0 / 360., 11.6 / 100., 33.7 / 100., 1.0), - elevated_surface_background: elevated_surface, - surface_background: bg, - background: bg, - element_background: hsla(223.0 / 360., 13. / 100., 21. / 100., 1.0), - element_hover: hover, - element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0), - element_selected: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0), - element_disabled: SystemColors::default().transparent, - element_selection_background: player.local().selection.alpha(0.25), - drop_target_background: hsla(220.0 / 360., 8.3 / 100., 21.4 / 100., 1.0), - drop_target_border: hsla(221. / 360., 11. / 100., 86. / 100., 1.0), - ghost_element_background: SystemColors::default().transparent, - ghost_element_hover: hover, - ghost_element_active: hsla(220.0 / 360., 11.8 / 100., 20.0 / 100., 1.0), - ghost_element_selected: hsla(224.0 / 360., 11.3 / 100., 26.1 / 100., 1.0), - ghost_element_disabled: SystemColors::default().transparent, - text: hsla(221. / 360., 11. / 100., 86. / 100., 1.0), - text_muted: hsla(218.0 / 360., 7. / 100., 46. / 100., 1.0), - text_placeholder: hsla(220.0 / 360., 6.6 / 100., 44.5 / 100., 1.0), - text_disabled: hsla(220.0 / 360., 6.6 / 100., 44.5 / 100., 1.0), - text_accent: hsla(222.6 / 360., 77.5 / 100., 65.1 / 100., 1.0), - icon: hsla(222.9 / 360., 9.9 / 100., 86.1 / 100., 1.0), - icon_muted: hsla(220.0 / 360., 12.1 / 100., 66.1 / 100., 1.0), - icon_disabled: hsla(220.0 / 360., 6.4 / 100., 45.7 / 100., 1.0), - icon_placeholder: hsla(220.0 / 360., 6.4 / 100., 45.7 / 100., 1.0), - icon_accent: blue, - debugger_accent: red, - status_bar_background: bg, - title_bar_background: bg, - title_bar_inactive_background: bg, - toolbar_background: editor, - tab_bar_background: bg, - tab_inactive_background: bg, - tab_active_background: editor, - search_match_background: bg, - search_active_match_background: bg, - - editor_background: editor, - editor_gutter_background: editor, - editor_subheader_background: bg, - editor_active_line_background: hsla(222.9 / 360., 13.5 / 100., 20.4 / 100., 1.0), - editor_highlighted_line_background: hsla(207.8 / 360., 81. / 100., 66. / 100., 0.1), - editor_debugger_active_line_background: hsla( - 207.8 / 360., - 81. / 100., - 66. / 100., - 0.2, - ), - editor_line_number: hsla(222.0 / 360., 11.5 / 100., 34.1 / 100., 1.0), - editor_active_line_number: hsla(216.0 / 360., 5.9 / 100., 49.6 / 100., 1.0), - editor_hover_line_number: hsla(216.0 / 360., 5.9 / 100., 56.7 / 100., 1.0), - editor_invisible: hsla(222.0 / 360., 11.5 / 100., 34.1 / 100., 1.0), - editor_wrap_guide: hsla(228. / 360., 8. / 100., 25. / 100., 1.), - editor_active_wrap_guide: hsla(228. / 360., 8. / 100., 25. / 100., 1.), - editor_indent_guide: hsla(228. / 360., 8. / 100., 25. / 100., 1.), - editor_indent_guide_active: hsla(225. / 360., 13. / 100., 12. / 100., 1.), - editor_document_highlight_read_background: hsla( - 207.8 / 360., - 81. / 100., - 66. / 100., - 0.2, - ), - editor_document_highlight_write_background: gpui::red(), - editor_document_highlight_bracket_background: gpui::green(), - - terminal_background: bg, - // todo("Use one colors for terminal") - terminal_ansi_background: crate::black().dark().step_12(), - terminal_foreground: crate::white().dark().step_12(), - terminal_bright_foreground: crate::white().dark().step_11(), - terminal_dim_foreground: crate::white().dark().step_10(), - terminal_ansi_black: crate::black().dark().step_12(), - terminal_ansi_red: crate::red().dark().step_11(), - terminal_ansi_green: crate::green().dark().step_11(), - terminal_ansi_yellow: crate::yellow().dark().step_11(), - terminal_ansi_blue: crate::blue().dark().step_11(), - terminal_ansi_magenta: crate::violet().dark().step_11(), - terminal_ansi_cyan: crate::cyan().dark().step_11(), - terminal_ansi_white: crate::neutral().dark().step_12(), - terminal_ansi_bright_black: crate::black().dark().step_11(), - terminal_ansi_bright_red: crate::red().dark().step_10(), - terminal_ansi_bright_green: crate::green().dark().step_10(), - terminal_ansi_bright_yellow: crate::yellow().dark().step_10(), - terminal_ansi_bright_blue: crate::blue().dark().step_10(), - terminal_ansi_bright_magenta: crate::violet().dark().step_10(), - terminal_ansi_bright_cyan: crate::cyan().dark().step_10(), - terminal_ansi_bright_white: crate::neutral().dark().step_11(), - terminal_ansi_dim_black: crate::black().dark().step_10(), - terminal_ansi_dim_red: crate::red().dark().step_9(), - terminal_ansi_dim_green: crate::green().dark().step_9(), - terminal_ansi_dim_yellow: crate::yellow().dark().step_9(), - terminal_ansi_dim_blue: crate::blue().dark().step_9(), - terminal_ansi_dim_magenta: crate::violet().dark().step_9(), - terminal_ansi_dim_cyan: crate::cyan().dark().step_9(), - terminal_ansi_dim_white: crate::neutral().dark().step_10(), - panel_background: bg, - panel_focused_border: blue, - panel_indent_guide: hsla(228. / 360., 8. / 100., 25. / 100., 1.), - panel_indent_guide_hover: hsla(225. / 360., 13. / 100., 12. / 100., 1.), - panel_indent_guide_active: hsla(225. / 360., 13. / 100., 12. / 100., 1.), - panel_overlay_background: bg, - panel_overlay_hover: hover, - pane_focused_border: blue, - pane_group_border: hsla(225. / 360., 13. / 100., 12. / 100., 1.), - scrollbar_thumb_background: gpui::transparent_black(), - scrollbar_thumb_hover_background: hover, - scrollbar_thumb_active_background: hsla( - 225.0 / 360., - 11.8 / 100., - 26.7 / 100., - 1.0, - ), - scrollbar_thumb_border: hsla(228. / 360., 8. / 100., 25. / 100., 1.), - scrollbar_track_background: gpui::transparent_black(), - scrollbar_track_border: hsla(228. / 360., 8. / 100., 25. / 100., 1.), - minimap_thumb_background: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 0.7), - minimap_thumb_hover_background: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 0.7), - minimap_thumb_active_background: hsla(225.0 / 360., 11.8 / 100., 26.7 / 100., 0.7), - minimap_thumb_border: hsla(228. / 360., 8. / 100., 25. / 100., 1.), - editor_foreground: hsla(218. / 360., 14. / 100., 71. / 100., 1.), - link_text_hover: blue, - version_control_added: ADDED_COLOR, - version_control_deleted: REMOVED_COLOR, - version_control_modified: MODIFIED_COLOR, - version_control_renamed: MODIFIED_COLOR, - version_control_conflict: crate::orange().light().step_12(), - version_control_ignored: crate::gray().light().step_12(), - version_control_word_added: WORD_ADDED_COLOR, - version_control_word_deleted: WORD_DELETED_COLOR, - version_control_conflict_marker_ours: crate::green().light().step_12().alpha(0.5), - version_control_conflict_marker_theirs: crate::blue().light().step_12().alpha(0.5), - - vim_normal_background: SystemColors::default().transparent, - vim_insert_background: SystemColors::default().transparent, - vim_replace_background: SystemColors::default().transparent, - vim_visual_background: SystemColors::default().transparent, - vim_visual_line_background: SystemColors::default().transparent, - vim_visual_block_background: SystemColors::default().transparent, - vim_helix_normal_background: SystemColors::default().transparent, - vim_helix_select_background: SystemColors::default().transparent, - vim_mode_text: SystemColors::default().transparent, - }, - status: StatusColors { - conflict: yellow, - conflict_background: yellow, - conflict_border: yellow, - created: green, - created_background: green, - created_border: green, - deleted: red, - deleted_background: red, - deleted_border: red, - error: red, - error_background: red, - error_border: red, - hidden: gray, - hidden_background: gray, - hidden_border: gray, - hint: blue, - hint_background: blue, - hint_border: blue, - ignored: gray, - ignored_background: gray, - ignored_border: gray, - info: blue, - info_background: blue, - info_border: blue, - modified: yellow, - modified_background: yellow, - modified_border: yellow, - predictive: gray, - predictive_background: gray, - predictive_border: gray, - renamed: blue, - renamed_background: blue, - renamed_border: blue, - success: green, - success_background: green, - success_border: green, - unreachable: gray, - unreachable_background: gray, - unreachable_border: gray, - warning: yellow, - warning_background: yellow, - warning_border: yellow, - }, - player, - syntax: Arc::new(SyntaxTheme { - highlights: vec![ - ("attribute".into(), purple.into()), - ("boolean".into(), orange.into()), - ("comment".into(), gray.into()), - ("comment.doc".into(), gray.into()), - ("constant".into(), yellow.into()), - ("constructor".into(), blue.into()), - ("embedded".into(), HighlightStyle::default()), - ( - "emphasis".into(), - HighlightStyle { - font_style: Some(FontStyle::Italic), - ..HighlightStyle::default() - }, - ), - ( - "emphasis.strong".into(), - HighlightStyle { - font_weight: Some(FontWeight::BOLD), - ..HighlightStyle::default() - }, - ), - ("enum".into(), HighlightStyle::default()), - ("function".into(), blue.into()), - ("function.method".into(), blue.into()), - ("function.definition".into(), blue.into()), - ("hint".into(), blue.into()), - ("keyword".into(), purple.into()), - ("label".into(), HighlightStyle::default()), - ("link_text".into(), blue.into()), - ( - "link_uri".into(), - HighlightStyle { - color: Some(teal), - font_style: Some(FontStyle::Italic), - ..HighlightStyle::default() - }, - ), - ("number".into(), orange.into()), - ("operator".into(), HighlightStyle::default()), - ("predictive".into(), HighlightStyle::default()), - ("preproc".into(), HighlightStyle::default()), - ("primary".into(), HighlightStyle::default()), - ("property".into(), red.into()), - ("punctuation".into(), HighlightStyle::default()), - ("punctuation.bracket".into(), HighlightStyle::default()), - ("punctuation.delimiter".into(), HighlightStyle::default()), - ("punctuation.list_marker".into(), HighlightStyle::default()), - ("punctuation.special".into(), HighlightStyle::default()), - ("string".into(), green.into()), - ("string.escape".into(), HighlightStyle::default()), - ("string.regex".into(), red.into()), - ("string.special".into(), HighlightStyle::default()), - ("string.special.symbol".into(), HighlightStyle::default()), - ("tag".into(), HighlightStyle::default()), - ("text.literal".into(), HighlightStyle::default()), - ("title".into(), HighlightStyle::default()), - ("type".into(), teal.into()), - ("variable".into(), HighlightStyle::default()), - ("variable.special".into(), red.into()), - ("variant".into(), HighlightStyle::default()), - ], - }), - }, - } -} diff --git a/crates/theme/src/font_family_cache.rs b/crates/theme/src/font_family_cache.rs deleted file mode 100644 index 411cf9b4d4..0000000000 --- a/crates/theme/src/font_family_cache.rs +++ /dev/null @@ -1,95 +0,0 @@ -use std::sync::Arc; -use std::time::Instant; - -use gpui::{App, Global, ReadGlobal, SharedString}; -use parking_lot::RwLock; - -#[derive(Default)] -struct FontFamilyCacheState { - loaded_at: Option, - font_families: Vec, -} - -/// A cache for the list of font families. -/// -/// Listing the available font families from the text system is expensive, -/// so we do it once and then use the cached values each render. -#[derive(Default)] -pub struct FontFamilyCache { - state: Arc>, -} - -#[derive(Default)] -struct GlobalFontFamilyCache(Arc); - -impl Global for GlobalFontFamilyCache {} - -impl FontFamilyCache { - /// Initializes the global font family cache. - pub fn init_global(cx: &mut App) { - cx.default_global::(); - } - - /// Returns the global font family cache. - pub fn global(cx: &App) -> Arc { - GlobalFontFamilyCache::global(cx).0.clone() - } - - /// Returns the list of font families. - pub fn list_font_families(&self, cx: &App) -> Vec { - if self.state.read().loaded_at.is_some() { - return self.state.read().font_families.clone(); - } - - let mut lock = self.state.write(); - lock.font_families = cx - .text_system() - .all_font_names() - .into_iter() - .map(SharedString::from) - .collect(); - lock.loaded_at = Some(Instant::now()); - - lock.font_families.clone() - } - - /// Returns the list of font families if they have been loaded - pub fn try_list_font_families(&self) -> Option> { - self.state - .try_read() - .filter(|state| state.loaded_at.is_some()) - .map(|state| state.font_families.clone()) - } - - /// Prefetch all font names in the background - pub async fn prefetch(&self, cx: &gpui::AsyncApp) { - if self - .state - .try_read() - .is_none_or(|state| state.loaded_at.is_some()) - { - return; - } - - let Ok(text_system) = cx.update(|cx| App::text_system(cx).clone()) else { - return; - }; - - let state = self.state.clone(); - - cx.background_executor() - .spawn(async move { - // We take this lock in the background executor to ensure that synchronous calls to `list_font_families` are blocked while we are prefetching, - // while not blocking the main thread and risking deadlocks - let mut lock = state.write(); - let all_font_names = text_system - .all_font_names() - .into_iter() - .map(SharedString::from) - .collect(); - lock.font_families = all_font_names; - lock.loaded_at = Some(Instant::now()); - }) - .await; - } -} diff --git a/crates/theme/src/icon_theme.rs b/crates/theme/src/icon_theme.rs deleted file mode 100644 index 818bf1b2f1..0000000000 --- a/crates/theme/src/icon_theme.rs +++ /dev/null @@ -1,422 +0,0 @@ -use std::sync::{Arc, LazyLock}; - -use collections::HashMap; -use gpui::SharedString; - -use crate::Appearance; - -/// A family of icon themes. -pub struct IconThemeFamily { - /// The unique ID for the icon theme family. - pub id: String, - /// The name of the icon theme family. - pub name: SharedString, - /// The author of the icon theme family. - pub author: SharedString, - /// The list of icon themes in the family. - pub themes: Vec, -} - -/// An icon theme. -#[derive(Debug, PartialEq)] -pub struct IconTheme { - /// The unique ID for the icon theme. - pub id: String, - /// The name of the icon theme. - pub name: SharedString, - /// The appearance of the icon theme (e.g., light or dark). - pub appearance: Appearance, - /// The icons used for directories. - pub directory_icons: DirectoryIcons, - /// The icons used for named directories. - pub named_directory_icons: HashMap, - /// The icons used for chevrons. - pub chevron_icons: ChevronIcons, - /// The mapping of file stems to their associated icon keys. - pub file_stems: HashMap, - /// The mapping of file suffixes to their associated icon keys. - pub file_suffixes: HashMap, - /// The mapping of icon keys to icon definitions. - pub file_icons: HashMap, -} - -/// The icons used for directories. -#[derive(Debug, PartialEq, Clone)] -pub struct DirectoryIcons { - /// The path to the icon to use for a collapsed directory. - pub collapsed: Option, - /// The path to the icon to use for an expanded directory. - pub expanded: Option, -} - -/// The icons used for chevrons. -#[derive(Debug, PartialEq)] -pub struct ChevronIcons { - /// The path to the icon to use for a collapsed chevron. - pub collapsed: Option, - /// The path to the icon to use for an expanded chevron. - pub expanded: Option, -} - -/// An icon definition. -#[derive(Debug, PartialEq)] -pub struct IconDefinition { - /// The path to the icon file. - pub path: SharedString, -} - -const FILE_STEMS_BY_ICON_KEY: &[(&str, &[&str])] = &[ - ("docker", &["Dockerfile"]), - ("ruby", &["Podfile"]), - ("heroku", &["Procfile"]), -]; - -const FILE_SUFFIXES_BY_ICON_KEY: &[(&str, &[&str])] = &[ - ("astro", &["astro"]), - ( - "audio", - &[ - "aac", "flac", "m4a", "mka", "mp3", "ogg", "opus", "wav", "wma", "wv", - ], - ), - ("backup", &["bak"]), - ("bicep", &["bicep"]), - ("bun", &["lockb"]), - ("c", &["c", "h"]), - ("cairo", &["cairo"]), - ("code", &["handlebars", "metadata", "rkt", "scm"]), - ("coffeescript", &["coffee"]), - ( - "cpp", - &[ - "c++", "h++", "cc", "cpp", "cxx", "hh", "hpp", "hxx", "inl", "ixx", - ], - ), - ("crystal", &["cr", "ecr"]), - ("csharp", &["cs"]), - ("csproj", &["csproj"]), - ("css", &["css", "pcss", "postcss"]), - ("cue", &["cue"]), - ("dart", &["dart"]), - ("diff", &["diff"]), - ( - "document", - &[ - "doc", "docx", "mdx", "odp", "ods", "odt", "pdf", "ppt", "pptx", "rtf", "txt", "xls", - "xlsx", - ], - ), - ("elixir", &["eex", "ex", "exs", "heex"]), - ("elm", &["elm"]), - ( - "erlang", - &[ - "Emakefile", - "app.src", - "erl", - "escript", - "hrl", - "rebar.config", - "xrl", - "yrl", - ], - ), - ( - "eslint", - &[ - "eslint.config.cjs", - "eslint.config.cts", - "eslint.config.js", - "eslint.config.mjs", - "eslint.config.mts", - "eslint.config.ts", - "eslintrc", - "eslintrc.js", - "eslintrc.json", - ], - ), - ("font", &["otf", "ttf", "woff", "woff2"]), - ("fsharp", &["fs"]), - ("fsproj", &["fsproj"]), - ("gitlab", &["gitlab-ci.yml"]), - ("gleam", &["gleam"]), - ("go", &["go", "mod", "work"]), - ("graphql", &["gql", "graphql", "graphqls"]), - ("haskell", &["hs"]), - ("hcl", &["hcl"]), - ("html", &["htm", "html"]), - ( - "image", - &[ - "avif", "bmp", "gif", "heic", "heif", "ico", "j2k", "jfif", "jp2", "jpeg", "jpg", - "jxl", "png", "psd", "qoi", "svg", "tiff", "webp", - ], - ), - ("java", &["java"]), - ("javascript", &["cjs", "js", "mjs"]), - ("json", &["json", "jsonc"]), - ("julia", &["jl"]), - ("kdl", &["kdl"]), - ("kotlin", &["kt"]), - ("lock", &["lock"]), - ("log", &["log"]), - ("lua", &["lua"]), - ("luau", &["luau"]), - ("markdown", &["markdown", "md"]), - ("metal", &["metal"]), - ("nim", &["nim"]), - ("nix", &["nix"]), - ("ocaml", &["ml", "mli"]), - ("odin", &["odin"]), - ("php", &["php"]), - ( - "prettier", - &[ - "prettier.config.cjs", - "prettier.config.js", - "prettier.config.mjs", - "prettierignore", - "prettierrc", - "prettierrc.cjs", - "prettierrc.js", - "prettierrc.json", - "prettierrc.json5", - "prettierrc.mjs", - "prettierrc.toml", - "prettierrc.yaml", - "prettierrc.yml", - ], - ), - ("prisma", &["prisma"]), - ("puppet", &["pp"]), - ("python", &["py"]), - ("r", &["r", "R"]), - ("react", &["cjsx", "ctsx", "jsx", "mjsx", "mtsx", "tsx"]), - ("roc", &["roc"]), - ("ruby", &["rb"]), - ("rust", &["rs"]), - ("sass", &["sass", "scss"]), - ("scala", &["scala", "sc"]), - ("settings", &["conf", "ini", "yaml", "yml"]), - ("solidity", &["sol"]), - ( - "storage", - &[ - "accdb", "csv", "dat", "db", "dbf", "dll", "fmp", "fp7", "frm", "gdb", "ib", "ldf", - "mdb", "mdf", "myd", "myi", "pdb", "RData", "rdata", "sav", "sdf", "sql", "sqlite", - "tsv", - ], - ), - ( - "stylelint", - &[ - "stylelint.config.cjs", - "stylelint.config.js", - "stylelint.config.mjs", - "stylelintignore", - "stylelintrc", - "stylelintrc.cjs", - "stylelintrc.js", - "stylelintrc.json", - "stylelintrc.mjs", - "stylelintrc.yaml", - "stylelintrc.yml", - ], - ), - ("surrealql", &["surql"]), - ("svelte", &["svelte"]), - ("swift", &["swift"]), - ("tcl", &["tcl"]), - ("template", &["hbs", "plist", "xml"]), - ( - "terminal", - &[ - "bash", - "bash_aliases", - "bash_login", - "bash_logout", - "bash_profile", - "bashrc", - "fish", - "nu", - "profile", - "ps1", - "sh", - "zlogin", - "zlogout", - "zprofile", - "zsh", - "zsh_aliases", - "zsh_histfile", - "zsh_history", - "zshenv", - "zshrc", - ], - ), - ("terraform", &["tf", "tfvars"]), - ("toml", &["toml"]), - ("typescript", &["cts", "mts", "ts"]), - ("v", &["v", "vsh", "vv"]), - ( - "vcs", - &[ - "COMMIT_EDITMSG", - "EDIT_DESCRIPTION", - "MERGE_MSG", - "NOTES_EDITMSG", - "TAG_EDITMSG", - "gitattributes", - "gitignore", - "gitkeep", - "gitmodules", - ], - ), - ("vbproj", &["vbproj"]), - ("video", &["avi", "m4v", "mkv", "mov", "mp4", "webm", "wmv"]), - ("vs_sln", &["sln"]), - ("vs_suo", &["suo"]), - ("vue", &["vue"]), - ("vyper", &["vy", "vyi"]), - ("wgsl", &["wgsl"]), - ("zig", &["zig"]), -]; - -/// A mapping of a file type identifier to its corresponding icon. -const FILE_ICONS: &[(&str, &str)] = &[ - ("astro", "icons/file_icons/astro.svg"), - ("audio", "icons/file_icons/audio.svg"), - ("bicep", "icons/file_icons/file.svg"), - ("bun", "icons/file_icons/bun.svg"), - ("c", "icons/file_icons/c.svg"), - ("cairo", "icons/file_icons/cairo.svg"), - ("code", "icons/file_icons/code.svg"), - ("coffeescript", "icons/file_icons/coffeescript.svg"), - ("cpp", "icons/file_icons/cpp.svg"), - ("crystal", "icons/file_icons/file.svg"), - ("csharp", "icons/file_icons/file.svg"), - ("csproj", "icons/file_icons/file.svg"), - ("css", "icons/file_icons/css.svg"), - ("cue", "icons/file_icons/file.svg"), - ("dart", "icons/file_icons/dart.svg"), - ("default", "icons/file_icons/file.svg"), - ("diff", "icons/file_icons/diff.svg"), - ("docker", "icons/file_icons/docker.svg"), - ("document", "icons/file_icons/book.svg"), - ("elixir", "icons/file_icons/elixir.svg"), - ("elm", "icons/file_icons/elm.svg"), - ("erlang", "icons/file_icons/erlang.svg"), - ("eslint", "icons/file_icons/eslint.svg"), - ("font", "icons/file_icons/font.svg"), - ("fsharp", "icons/file_icons/fsharp.svg"), - ("fsproj", "icons/file_icons/file.svg"), - ("gitlab", "icons/file_icons/settings.svg"), - ("gleam", "icons/file_icons/gleam.svg"), - ("go", "icons/file_icons/go.svg"), - ("graphql", "icons/file_icons/graphql.svg"), - ("haskell", "icons/file_icons/haskell.svg"), - ("hcl", "icons/file_icons/hcl.svg"), - ("heroku", "icons/file_icons/heroku.svg"), - ("html", "icons/file_icons/html.svg"), - ("image", "icons/file_icons/image.svg"), - ("java", "icons/file_icons/java.svg"), - ("javascript", "icons/file_icons/javascript.svg"), - ("json", "icons/file_icons/code.svg"), - ("julia", "icons/file_icons/julia.svg"), - ("kdl", "icons/file_icons/kdl.svg"), - ("kotlin", "icons/file_icons/kotlin.svg"), - ("lock", "icons/file_icons/lock.svg"), - ("log", "icons/file_icons/info.svg"), - ("lua", "icons/file_icons/lua.svg"), - ("luau", "icons/file_icons/luau.svg"), - ("markdown", "icons/file_icons/book.svg"), - ("metal", "icons/file_icons/metal.svg"), - ("nim", "icons/file_icons/nim.svg"), - ("nix", "icons/file_icons/nix.svg"), - ("ocaml", "icons/file_icons/ocaml.svg"), - ("odin", "icons/file_icons/odin.svg"), - ("phoenix", "icons/file_icons/phoenix.svg"), - ("php", "icons/file_icons/php.svg"), - ("prettier", "icons/file_icons/prettier.svg"), - ("prisma", "icons/file_icons/prisma.svg"), - ("puppet", "icons/file_icons/puppet.svg"), - ("python", "icons/file_icons/python.svg"), - ("r", "icons/file_icons/r.svg"), - ("react", "icons/file_icons/react.svg"), - ("roc", "icons/file_icons/roc.svg"), - ("ruby", "icons/file_icons/ruby.svg"), - ("rust", "icons/file_icons/rust.svg"), - ("sass", "icons/file_icons/sass.svg"), - ("scala", "icons/file_icons/scala.svg"), - ("settings", "icons/file_icons/settings.svg"), - ("solidity", "icons/file_icons/file.svg"), - ("storage", "icons/file_icons/database.svg"), - ("stylelint", "icons/file_icons/javascript.svg"), - ("surrealql", "icons/file_icons/surrealql.svg"), - ("svelte", "icons/file_icons/html.svg"), - ("swift", "icons/file_icons/swift.svg"), - ("tcl", "icons/file_icons/tcl.svg"), - ("template", "icons/file_icons/html.svg"), - ("terminal", "icons/file_icons/terminal.svg"), - ("terraform", "icons/file_icons/terraform.svg"), - ("toml", "icons/file_icons/toml.svg"), - ("typescript", "icons/file_icons/typescript.svg"), - ("v", "icons/file_icons/v.svg"), - ("vbproj", "icons/file_icons/file.svg"), - ("vcs", "icons/file_icons/git.svg"), - ("video", "icons/file_icons/video.svg"), - ("vs_sln", "icons/file_icons/file.svg"), - ("vs_suo", "icons/file_icons/file.svg"), - ("vue", "icons/file_icons/vue.svg"), - ("vyper", "icons/file_icons/vyper.svg"), - ("wgsl", "icons/file_icons/wgsl.svg"), - ("zig", "icons/file_icons/zig.svg"), -]; - -/// Returns a mapping of file associations to icon keys. -fn icon_keys_by_association( - associations_by_icon_key: &[(&str, &[&str])], -) -> HashMap { - let mut icon_keys_by_association = HashMap::default(); - for (icon_key, associations) in associations_by_icon_key { - for association in *associations { - icon_keys_by_association.insert(association.to_string(), icon_key.to_string()); - } - } - - icon_keys_by_association -} - -/// The name of the default icon theme. -pub(crate) const DEFAULT_ICON_THEME_NAME: &str = "Zed (Default)"; - -static DEFAULT_ICON_THEME: LazyLock> = LazyLock::new(|| { - Arc::new(IconTheme { - id: "zed".into(), - name: DEFAULT_ICON_THEME_NAME.into(), - appearance: Appearance::Dark, - directory_icons: DirectoryIcons { - collapsed: Some("icons/file_icons/folder.svg".into()), - expanded: Some("icons/file_icons/folder_open.svg".into()), - }, - named_directory_icons: HashMap::default(), - chevron_icons: ChevronIcons { - collapsed: Some("icons/file_icons/chevron_right.svg".into()), - expanded: Some("icons/file_icons/chevron_down.svg".into()), - }, - file_stems: icon_keys_by_association(FILE_STEMS_BY_ICON_KEY), - file_suffixes: icon_keys_by_association(FILE_SUFFIXES_BY_ICON_KEY), - file_icons: HashMap::from_iter(FILE_ICONS.iter().map(|(ty, path)| { - ( - ty.to_string(), - IconDefinition { - path: (*path).into(), - }, - ) - })), - }) -}); - -/// Returns the default icon theme. -pub fn default_icon_theme() -> Arc { - DEFAULT_ICON_THEME.clone() -} diff --git a/crates/theme/src/icon_theme_schema.rs b/crates/theme/src/icon_theme_schema.rs deleted file mode 100644 index 45ac985ae9..0000000000 --- a/crates/theme/src/icon_theme_schema.rs +++ /dev/null @@ -1,50 +0,0 @@ -#![allow(missing_docs)] - -use gpui::SharedString; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; - -use crate::AppearanceContent; - -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub struct IconThemeFamilyContent { - pub name: String, - pub author: String, - pub themes: Vec, -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub struct IconThemeContent { - pub name: String, - pub appearance: AppearanceContent, - #[serde(default)] - pub directory_icons: DirectoryIconsContent, - #[serde(default)] - pub named_directory_icons: HashMap, - #[serde(default)] - pub chevron_icons: ChevronIconsContent, - #[serde(default)] - pub file_stems: HashMap, - #[serde(default)] - pub file_suffixes: HashMap, - #[serde(default)] - pub file_icons: HashMap, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] -pub struct DirectoryIconsContent { - pub collapsed: Option, - pub expanded: Option, -} - -#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)] -pub struct ChevronIconsContent { - pub collapsed: Option, - pub expanded: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub struct IconDefinitionContent { - pub path: SharedString, -} diff --git a/crates/theme/src/registry.rs b/crates/theme/src/registry.rs deleted file mode 100644 index c362b62704..0000000000 --- a/crates/theme/src/registry.rs +++ /dev/null @@ -1,364 +0,0 @@ -use std::sync::Arc; -use std::{fmt::Debug, path::Path}; - -use anyhow::{Context as _, Result}; -use collections::HashMap; -use derive_more::{Deref, DerefMut}; -use fs::Fs; -use futures::StreamExt; -use gpui::{App, AssetSource, Global, SharedString}; -use parking_lot::RwLock; -use thiserror::Error; -use util::ResultExt; - -use crate::{ - Appearance, AppearanceContent, ChevronIcons, DEFAULT_ICON_THEME_NAME, DirectoryIcons, - IconDefinition, IconTheme, Theme, ThemeFamily, ThemeFamilyContent, default_icon_theme, - read_icon_theme, read_user_theme, refine_theme_family, -}; - -/// The metadata for a theme. -#[derive(Debug, Clone)] -pub struct ThemeMeta { - /// The name of the theme. - pub name: SharedString, - /// The appearance of the theme. - pub appearance: Appearance, -} - -/// An error indicating that the theme with the given name was not found. -#[derive(Debug, Error, Clone)] -#[error("theme not found: {0}")] -pub struct ThemeNotFoundError(pub SharedString); - -/// An error indicating that the icon theme with the given name was not found. -#[derive(Debug, Error, Clone)] -#[error("icon theme not found: {0}")] -pub struct IconThemeNotFoundError(pub SharedString); - -/// The global [`ThemeRegistry`]. -/// -/// This newtype exists for obtaining a unique [`TypeId`](std::any::TypeId) when -/// inserting the [`ThemeRegistry`] into the context as a global. -/// -/// This should not be exposed outside of this module. -#[derive(Default, Deref, DerefMut)] -struct GlobalThemeRegistry(Arc); - -impl Global for GlobalThemeRegistry {} - -struct ThemeRegistryState { - themes: HashMap>, - icon_themes: HashMap>, - /// Whether the extensions have been loaded yet. - extensions_loaded: bool, -} - -/// The registry for themes. -pub struct ThemeRegistry { - state: RwLock, - assets: Box, -} - -impl ThemeRegistry { - /// Returns the global [`ThemeRegistry`]. - pub fn global(cx: &App) -> Arc { - cx.global::().0.clone() - } - - /// Returns the global [`ThemeRegistry`]. - /// - /// Inserts a default [`ThemeRegistry`] if one does not yet exist. - pub fn default_global(cx: &mut App) -> Arc { - cx.default_global::().0.clone() - } - - /// Returns the global [`ThemeRegistry`] if it exists. - pub fn try_global(cx: &mut App) -> Option> { - cx.try_global::().map(|t| t.0.clone()) - } - - /// Sets the global [`ThemeRegistry`]. - pub(crate) fn set_global(assets: Box, cx: &mut App) { - cx.set_global(GlobalThemeRegistry(Arc::new(ThemeRegistry::new(assets)))); - } - - /// Creates a new [`ThemeRegistry`] with the given [`AssetSource`]. - pub fn new(assets: Box) -> Self { - let registry = Self { - state: RwLock::new(ThemeRegistryState { - themes: HashMap::default(), - icon_themes: HashMap::default(), - extensions_loaded: false, - }), - assets, - }; - - // We're loading the Zed default theme, as we need a theme to be loaded - // for tests. - registry.insert_theme_families([crate::fallback_themes::zed_default_themes()]); - - let default_icon_theme = crate::default_icon_theme(); - registry - .state - .write() - .icon_themes - .insert(default_icon_theme.name.clone(), default_icon_theme); - - registry - } - - /// Returns whether the extensions have been loaded. - pub fn extensions_loaded(&self) -> bool { - self.state.read().extensions_loaded - } - - /// Sets the flag indicating that the extensions have loaded. - pub fn set_extensions_loaded(&self) { - self.state.write().extensions_loaded = true; - } - - fn insert_theme_families(&self, families: impl IntoIterator) { - for family in families.into_iter() { - self.insert_themes(family.themes); - } - } - - fn insert_themes(&self, themes: impl IntoIterator) { - let mut state = self.state.write(); - for theme in themes.into_iter() { - state.themes.insert(theme.name.clone(), Arc::new(theme)); - } - } - - #[allow(unused)] - fn insert_user_theme_families(&self, families: impl IntoIterator) { - for family in families.into_iter() { - let refined_family = refine_theme_family(family); - - self.insert_themes(refined_family.themes); - } - } - - /// Removes the themes with the given names from the registry. - pub fn remove_user_themes(&self, themes_to_remove: &[SharedString]) { - self.state - .write() - .themes - .retain(|name, _| !themes_to_remove.contains(name)) - } - - /// Removes all themes from the registry. - pub fn clear(&self) { - self.state.write().themes.clear(); - } - - /// Returns the names of all themes in the registry. - pub fn list_names(&self) -> Vec { - let mut names = self.state.read().themes.keys().cloned().collect::>(); - names.sort(); - names - } - - /// Returns the metadata of all themes in the registry. - pub fn list(&self) -> Vec { - self.state - .read() - .themes - .values() - .map(|theme| ThemeMeta { - name: theme.name.clone(), - appearance: theme.appearance(), - }) - .collect() - } - - /// Returns the theme with the given name. - pub fn get(&self, name: &str) -> Result, ThemeNotFoundError> { - self.state - .read() - .themes - .get(name) - .ok_or_else(|| ThemeNotFoundError(name.to_string().into())) - .cloned() - } - - /// Loads the themes bundled with the Zed binary and adds them to the registry. - pub fn load_bundled_themes(&self) { - let theme_paths = self - .assets - .list("themes/") - .expect("failed to list theme assets") - .into_iter() - .filter(|path| path.ends_with(".json")); - - for path in theme_paths { - let Some(theme) = self.assets.load(&path).log_err().flatten() else { - continue; - }; - - let Some(theme_family) = serde_json::from_slice(&theme) - .with_context(|| format!("failed to parse theme at path \"{path}\"")) - .log_err() - else { - continue; - }; - - self.insert_user_theme_families([theme_family]); - } - } - - /// Loads the user themes from the specified directory and adds them to the registry. - pub async fn load_user_themes(&self, themes_path: &Path, fs: Arc) -> Result<()> { - let mut theme_paths = fs - .read_dir(themes_path) - .await - .with_context(|| format!("reading themes from {themes_path:?}"))?; - - while let Some(theme_path) = theme_paths.next().await { - let Some(theme_path) = theme_path.log_err() else { - continue; - }; - - self.load_user_theme(&theme_path, fs.clone()) - .await - .log_err(); - } - - Ok(()) - } - - /// Loads the user theme from the specified path and adds it to the registry. - pub async fn load_user_theme(&self, theme_path: &Path, fs: Arc) -> Result<()> { - let theme = read_user_theme(theme_path, fs).await?; - - self.insert_user_theme_families([theme]); - - Ok(()) - } - - /// Returns the default icon theme. - pub fn default_icon_theme(&self) -> Result, IconThemeNotFoundError> { - self.get_icon_theme(DEFAULT_ICON_THEME_NAME) - } - - /// Returns the metadata of all icon themes in the registry. - pub fn list_icon_themes(&self) -> Vec { - self.state - .read() - .icon_themes - .values() - .map(|theme| ThemeMeta { - name: theme.name.clone(), - appearance: theme.appearance, - }) - .collect() - } - - /// Returns the icon theme with the specified name. - pub fn get_icon_theme(&self, name: &str) -> Result, IconThemeNotFoundError> { - self.state - .read() - .icon_themes - .get(name) - .ok_or_else(|| IconThemeNotFoundError(name.to_string().into())) - .cloned() - } - - /// Removes the icon themes with the given names from the registry. - pub fn remove_icon_themes(&self, icon_themes_to_remove: &[SharedString]) { - self.state - .write() - .icon_themes - .retain(|name, _| !icon_themes_to_remove.contains(name)) - } - - /// Loads the icon theme from the specified path and adds it to the registry. - /// - /// The `icons_root_dir` parameter indicates the root directory from which - /// the relative paths to icons in the theme should be resolved against. - pub async fn load_icon_theme( - &self, - icon_theme_path: &Path, - icons_root_dir: &Path, - fs: Arc, - ) -> Result<()> { - let icon_theme_family = read_icon_theme(icon_theme_path, fs).await?; - - let resolve_icon_path = |path: SharedString| { - icons_root_dir - .join(path.as_ref()) - .to_string_lossy() - .to_string() - .into() - }; - - let default_icon_theme = default_icon_theme(); - - let mut state = self.state.write(); - for icon_theme in icon_theme_family.themes { - let mut file_stems = default_icon_theme.file_stems.clone(); - file_stems.extend(icon_theme.file_stems); - - let mut file_suffixes = default_icon_theme.file_suffixes.clone(); - file_suffixes.extend(icon_theme.file_suffixes); - - let mut named_directory_icons = default_icon_theme.named_directory_icons.clone(); - named_directory_icons.extend(icon_theme.named_directory_icons.into_iter().map( - |(key, value)| { - ( - key, - DirectoryIcons { - collapsed: value.collapsed.map(resolve_icon_path), - expanded: value.expanded.map(resolve_icon_path), - }, - ) - }, - )); - - let icon_theme = IconTheme { - id: uuid::Uuid::new_v4().to_string(), - name: icon_theme.name.into(), - appearance: match icon_theme.appearance { - AppearanceContent::Light => Appearance::Light, - AppearanceContent::Dark => Appearance::Dark, - }, - directory_icons: DirectoryIcons { - collapsed: icon_theme.directory_icons.collapsed.map(resolve_icon_path), - expanded: icon_theme.directory_icons.expanded.map(resolve_icon_path), - }, - named_directory_icons, - chevron_icons: ChevronIcons { - collapsed: icon_theme.chevron_icons.collapsed.map(resolve_icon_path), - expanded: icon_theme.chevron_icons.expanded.map(resolve_icon_path), - }, - file_stems, - file_suffixes, - file_icons: icon_theme - .file_icons - .into_iter() - .map(|(key, icon)| { - ( - key, - IconDefinition { - path: resolve_icon_path(icon.path), - }, - ) - }) - .collect(), - }; - - state - .icon_themes - .insert(icon_theme.name.clone(), Arc::new(icon_theme)); - } - - Ok(()) - } -} - -impl Default for ThemeRegistry { - fn default() -> Self { - Self::new(Box::new(())) - } -} diff --git a/crates/theme/src/scale.rs b/crates/theme/src/scale.rs deleted file mode 100644 index 70c6114cc6..0000000000 --- a/crates/theme/src/scale.rs +++ /dev/null @@ -1,298 +0,0 @@ -#![allow(missing_docs)] -use gpui::{App, Hsla, SharedString}; - -use crate::{ActiveTheme, Appearance}; - -/// A collection of colors that are used to style the UI. -/// -/// Each step has a semantic meaning, and is used to style different parts of the UI. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -pub struct ColorScaleStep(usize); - -impl ColorScaleStep { - pub const ONE: Self = Self(1); - pub const TWO: Self = Self(2); - pub const THREE: Self = Self(3); - pub const FOUR: Self = Self(4); - pub const FIVE: Self = Self(5); - pub const SIX: Self = Self(6); - pub const SEVEN: Self = Self(7); - pub const EIGHT: Self = Self(8); - pub const NINE: Self = Self(9); - pub const TEN: Self = Self(10); - pub const ELEVEN: Self = Self(11); - pub const TWELVE: Self = Self(12); - - /// All of the steps in a [`ColorScale`]. - pub const ALL: [ColorScaleStep; 12] = [ - Self::ONE, - Self::TWO, - Self::THREE, - Self::FOUR, - Self::FIVE, - Self::SIX, - Self::SEVEN, - Self::EIGHT, - Self::NINE, - Self::TEN, - Self::ELEVEN, - Self::TWELVE, - ]; -} - -/// A scale of colors for a given [`ColorScaleSet`]. -/// -/// Each [`ColorScale`] contains exactly 12 colors. Refer to -/// [`ColorScaleStep`] for a reference of what each step is used for. -pub struct ColorScale(Vec); - -impl FromIterator for ColorScale { - fn from_iter>(iter: T) -> Self { - Self(Vec::from_iter(iter)) - } -} - -impl ColorScale { - /// Returns the specified step in the [`ColorScale`]. - #[inline] - pub fn step(&self, step: ColorScaleStep) -> Hsla { - // Steps are one-based, so we need convert to the zero-based vec index. - self.0[step.0 - 1] - } - - /// `Step 1` - Used for main application backgrounds. - /// - /// This step provides a neutral base for any overlaying components, ideal for applications' main backdrop or empty spaces such as canvas areas. - /// - #[inline] - pub fn step_1(&self) -> Hsla { - self.step(ColorScaleStep::ONE) - } - - /// `Step 2` - Used for both main application backgrounds and subtle component backgrounds. - /// - /// Like `Step 1`, this step allows variations in background styles, from striped tables, sidebar backgrounds, to card backgrounds. - #[inline] - pub fn step_2(&self) -> Hsla { - self.step(ColorScaleStep::TWO) - } - - /// `Step 3` - Used for UI component backgrounds in their normal states. - /// - /// This step maintains accessibility by guaranteeing a contrast ratio of 4.5:1 with steps 11 and 12 for text. It could also suit hover states for transparent components. - #[inline] - pub fn step_3(&self) -> Hsla { - self.step(ColorScaleStep::THREE) - } - - /// `Step 4` - Used for UI component backgrounds in their hover states. - /// - /// Also suited for pressed or selected states of components with a transparent background. - #[inline] - pub fn step_4(&self) -> Hsla { - self.step(ColorScaleStep::FOUR) - } - - /// `Step 5` - Used for UI component backgrounds in their pressed or selected states. - #[inline] - pub fn step_5(&self) -> Hsla { - self.step(ColorScaleStep::FIVE) - } - - /// `Step 6` - Used for subtle borders on non-interactive components. - /// - /// Its usage spans from sidebars' borders, headers' dividers, cards' outlines, to alerts' edges and separators. - #[inline] - pub fn step_6(&self) -> Hsla { - self.step(ColorScaleStep::SIX) - } - - /// `Step 7` - Used for subtle borders on interactive components. - /// - /// This step subtly delineates the boundary of elements users interact with. - #[inline] - pub fn step_7(&self) -> Hsla { - self.step(ColorScaleStep::SEVEN) - } - - /// `Step 8` - Used for stronger borders on interactive components and focus rings. - /// - /// It strengthens the visibility and accessibility of active elements and their focus states. - #[inline] - pub fn step_8(&self) -> Hsla { - self.step(ColorScaleStep::EIGHT) - } - - /// `Step 9` - Used for solid backgrounds. - /// - /// `Step 9` is the most saturated step, having the least mix of white or black. - /// - /// Due to its high chroma, `Step 9` is versatile and particularly useful for semantic colors such as - /// error, warning, and success indicators. - #[inline] - pub fn step_9(&self) -> Hsla { - self.step(ColorScaleStep::NINE) - } - - /// `Step 10` - Used for hovered or active solid backgrounds, particularly when `Step 9` is their normal state. - /// - /// May also be used for extremely low contrast text. This should be used sparingly, as it may be difficult to read. - #[inline] - pub fn step_10(&self) -> Hsla { - self.step(ColorScaleStep::TEN) - } - - /// `Step 11` - Used for text and icons requiring low contrast or less emphasis. - #[inline] - pub fn step_11(&self) -> Hsla { - self.step(ColorScaleStep::ELEVEN) - } - - /// `Step 12` - Used for text and icons requiring high contrast or prominence. - #[inline] - pub fn step_12(&self) -> Hsla { - self.step(ColorScaleStep::TWELVE) - } -} - -pub struct ColorScales { - pub gray: ColorScaleSet, - pub mauve: ColorScaleSet, - pub slate: ColorScaleSet, - pub sage: ColorScaleSet, - pub olive: ColorScaleSet, - pub sand: ColorScaleSet, - pub gold: ColorScaleSet, - pub bronze: ColorScaleSet, - pub brown: ColorScaleSet, - pub yellow: ColorScaleSet, - pub amber: ColorScaleSet, - pub orange: ColorScaleSet, - pub tomato: ColorScaleSet, - pub red: ColorScaleSet, - pub ruby: ColorScaleSet, - pub crimson: ColorScaleSet, - pub pink: ColorScaleSet, - pub plum: ColorScaleSet, - pub purple: ColorScaleSet, - pub violet: ColorScaleSet, - pub iris: ColorScaleSet, - pub indigo: ColorScaleSet, - pub blue: ColorScaleSet, - pub cyan: ColorScaleSet, - pub teal: ColorScaleSet, - pub jade: ColorScaleSet, - pub green: ColorScaleSet, - pub grass: ColorScaleSet, - pub lime: ColorScaleSet, - pub mint: ColorScaleSet, - pub sky: ColorScaleSet, - pub black: ColorScaleSet, - pub white: ColorScaleSet, -} - -impl IntoIterator for ColorScales { - type Item = ColorScaleSet; - - type IntoIter = std::vec::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - vec![ - self.gray, - self.mauve, - self.slate, - self.sage, - self.olive, - self.sand, - self.gold, - self.bronze, - self.brown, - self.yellow, - self.amber, - self.orange, - self.tomato, - self.red, - self.ruby, - self.crimson, - self.pink, - self.plum, - self.purple, - self.violet, - self.iris, - self.indigo, - self.blue, - self.cyan, - self.teal, - self.jade, - self.green, - self.grass, - self.lime, - self.mint, - self.sky, - self.black, - self.white, - ] - .into_iter() - } -} - -/// Provides groups of [`ColorScale`]s for light and dark themes, as well as transparent versions of each scale. -pub struct ColorScaleSet { - name: SharedString, - light: ColorScale, - dark: ColorScale, - light_alpha: ColorScale, - dark_alpha: ColorScale, -} - -impl ColorScaleSet { - pub fn new( - name: impl Into, - light: ColorScale, - light_alpha: ColorScale, - dark: ColorScale, - dark_alpha: ColorScale, - ) -> Self { - Self { - name: name.into(), - light, - light_alpha, - dark, - dark_alpha, - } - } - - pub fn name(&self) -> &SharedString { - &self.name - } - - pub fn light(&self) -> &ColorScale { - &self.light - } - - pub fn light_alpha(&self) -> &ColorScale { - &self.light_alpha - } - - pub fn dark(&self) -> &ColorScale { - &self.dark - } - - pub fn dark_alpha(&self) -> &ColorScale { - &self.dark_alpha - } - - pub fn step(&self, cx: &App, step: ColorScaleStep) -> Hsla { - match cx.theme().appearance { - Appearance::Light => self.light().step(step), - Appearance::Dark => self.dark().step(step), - } - } - - pub fn step_alpha(&self, cx: &App, step: ColorScaleStep) -> Hsla { - match cx.theme().appearance { - Appearance::Light => self.light_alpha.step(step), - Appearance::Dark => self.dark_alpha.step(step), - } - } -} diff --git a/crates/theme/src/schema.rs b/crates/theme/src/schema.rs deleted file mode 100644 index f52b2cf0e5..0000000000 --- a/crates/theme/src/schema.rs +++ /dev/null @@ -1,826 +0,0 @@ -#![allow(missing_docs)] - -use gpui::{FontStyle, FontWeight, HighlightStyle, Hsla}; -use palette::FromColor; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -pub use settings::{FontWeightContent, WindowBackgroundContent}; - -use crate::{StatusColorsRefinement, ThemeColorsRefinement}; - -fn ensure_non_opaque(color: Hsla) -> Hsla { - const MAXIMUM_OPACITY: f32 = 0.7; - if color.a <= MAXIMUM_OPACITY { - color - } else { - Hsla { - a: MAXIMUM_OPACITY, - ..color - } - } -} - -fn ensure_opaque(color: Hsla) -> Hsla { - Hsla { a: 1.0, ..color } -} - -#[derive(Debug, PartialEq, Clone, Copy, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum AppearanceContent { - Light, - Dark, -} - -/// The content of a serialized theme family. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub struct ThemeFamilyContent { - pub name: String, - pub author: String, - pub themes: Vec, -} - -/// The content of a serialized theme. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] -pub struct ThemeContent { - pub name: String, - pub appearance: AppearanceContent, - pub style: settings::ThemeStyleContent, -} - -/// Returns the syntax style overrides in the [`ThemeContent`]. -pub fn syntax_overrides(this: &settings::ThemeStyleContent) -> Vec<(String, HighlightStyle)> { - this.syntax - .iter() - .map(|(key, style)| { - ( - key.clone(), - HighlightStyle { - color: style - .color - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - background_color: style - .background_color - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - font_style: style.font_style.map(FontStyle::from), - font_weight: style.font_weight.map(FontWeight::from), - ..Default::default() - }, - ) - }) - .collect() -} - -pub fn status_colors_refinement(colors: &settings::StatusColorsContent) -> StatusColorsRefinement { - StatusColorsRefinement { - conflict: colors - .conflict - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - conflict_background: colors - .conflict_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - conflict_border: colors - .conflict_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - created: colors - .created - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - created_background: colors - .created_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - created_border: colors - .created_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - deleted: colors - .deleted - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - deleted_background: colors - .deleted_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - deleted_border: colors - .deleted_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - error: colors - .error - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - error_background: colors - .error_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - error_border: colors - .error_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - hidden: colors - .hidden - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - hidden_background: colors - .hidden_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - hidden_border: colors - .hidden_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - hint: colors - .hint - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - hint_background: colors - .hint_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - hint_border: colors - .hint_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - ignored: colors - .ignored - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - ignored_background: colors - .ignored_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - ignored_border: colors - .ignored_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - info: colors - .info - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - info_background: colors - .info_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - info_border: colors - .info_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - modified: colors - .modified - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - modified_background: colors - .modified_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - modified_border: colors - .modified_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - predictive: colors - .predictive - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - predictive_background: colors - .predictive_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - predictive_border: colors - .predictive_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - renamed: colors - .renamed - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - renamed_background: colors - .renamed_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - renamed_border: colors - .renamed_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - success: colors - .success - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - success_background: colors - .success_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - success_border: colors - .success_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - unreachable: colors - .unreachable - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - unreachable_background: colors - .unreachable_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - unreachable_border: colors - .unreachable_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - warning: colors - .warning - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - warning_background: colors - .warning_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - warning_border: colors - .warning_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - } -} - -pub fn theme_colors_refinement( - this: &settings::ThemeColorsContent, - status_colors: &StatusColorsRefinement, -) -> ThemeColorsRefinement { - let border = this - .border - .as_ref() - .and_then(|color| try_parse_color(color).ok()); - let editor_document_highlight_read_background = this - .editor_document_highlight_read_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()); - let scrollbar_thumb_background = this - .scrollbar_thumb_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - .or_else(|| { - this.deprecated_scrollbar_thumb_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - }); - let scrollbar_thumb_hover_background = this - .scrollbar_thumb_hover_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()); - let scrollbar_thumb_active_background = this - .scrollbar_thumb_active_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - .or(scrollbar_thumb_background); - let scrollbar_thumb_border = this - .scrollbar_thumb_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()); - let element_hover = this - .element_hover - .as_ref() - .and_then(|color| try_parse_color(color).ok()); - let panel_background = this - .panel_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()); - let search_match_background = this - .search_match_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()); - let search_active_match_background = this - .search_active_match_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - .or(search_match_background); - ThemeColorsRefinement { - border, - border_variant: this - .border_variant - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - border_focused: this - .border_focused - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - border_selected: this - .border_selected - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - border_transparent: this - .border_transparent - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - border_disabled: this - .border_disabled - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - elevated_surface_background: this - .elevated_surface_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - surface_background: this - .surface_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - background: this - .background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - element_background: this - .element_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - element_hover, - element_active: this - .element_active - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - element_selected: this - .element_selected - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - element_disabled: this - .element_disabled - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - element_selection_background: this - .element_selection_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - drop_target_background: this - .drop_target_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - drop_target_border: this - .drop_target_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - ghost_element_background: this - .ghost_element_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - ghost_element_hover: this - .ghost_element_hover - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - ghost_element_active: this - .ghost_element_active - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - ghost_element_selected: this - .ghost_element_selected - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - ghost_element_disabled: this - .ghost_element_disabled - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - text: this - .text - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - text_muted: this - .text_muted - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - text_placeholder: this - .text_placeholder - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - text_disabled: this - .text_disabled - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - text_accent: this - .text_accent - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - icon: this - .icon - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - icon_muted: this - .icon_muted - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - icon_disabled: this - .icon_disabled - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - icon_placeholder: this - .icon_placeholder - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - icon_accent: this - .icon_accent - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - debugger_accent: this - .debugger_accent - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - status_bar_background: this - .status_bar_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - title_bar_background: this - .title_bar_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - title_bar_inactive_background: this - .title_bar_inactive_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - toolbar_background: this - .toolbar_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - tab_bar_background: this - .tab_bar_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - tab_inactive_background: this - .tab_inactive_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - tab_active_background: this - .tab_active_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - search_match_background: search_match_background, - search_active_match_background: search_active_match_background, - panel_background, - panel_focused_border: this - .panel_focused_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - panel_indent_guide: this - .panel_indent_guide - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - panel_indent_guide_hover: this - .panel_indent_guide_hover - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - panel_indent_guide_active: this - .panel_indent_guide_active - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - panel_overlay_background: this - .panel_overlay_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - .or(panel_background.map(ensure_opaque)), - panel_overlay_hover: this - .panel_overlay_hover - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - .or(panel_background - .zip(element_hover) - .map(|(panel_bg, hover_bg)| panel_bg.blend(hover_bg)) - .map(ensure_opaque)), - pane_focused_border: this - .pane_focused_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - pane_group_border: this - .pane_group_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - .or(border), - scrollbar_thumb_background, - scrollbar_thumb_hover_background, - scrollbar_thumb_active_background, - scrollbar_thumb_border, - scrollbar_track_background: this - .scrollbar_track_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - scrollbar_track_border: this - .scrollbar_track_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - minimap_thumb_background: this - .minimap_thumb_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - .or(scrollbar_thumb_background.map(ensure_non_opaque)), - minimap_thumb_hover_background: this - .minimap_thumb_hover_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - .or(scrollbar_thumb_hover_background.map(ensure_non_opaque)), - minimap_thumb_active_background: this - .minimap_thumb_active_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - .or(scrollbar_thumb_active_background.map(ensure_non_opaque)), - minimap_thumb_border: this - .minimap_thumb_border - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - .or(scrollbar_thumb_border), - editor_foreground: this - .editor_foreground - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_background: this - .editor_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_gutter_background: this - .editor_gutter_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_subheader_background: this - .editor_subheader_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_active_line_background: this - .editor_active_line_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_highlighted_line_background: this - .editor_highlighted_line_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_debugger_active_line_background: this - .editor_debugger_active_line_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_line_number: this - .editor_line_number - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_hover_line_number: this - .editor_hover_line_number - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_active_line_number: this - .editor_active_line_number - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_invisible: this - .editor_invisible - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_wrap_guide: this - .editor_wrap_guide - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_active_wrap_guide: this - .editor_active_wrap_guide - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_indent_guide: this - .editor_indent_guide - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_indent_guide_active: this - .editor_indent_guide_active - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_document_highlight_read_background, - editor_document_highlight_write_background: this - .editor_document_highlight_write_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - editor_document_highlight_bracket_background: this - .editor_document_highlight_bracket_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - // Fall back to `editor.document_highlight.read_background`, for backwards compatibility. - .or(editor_document_highlight_read_background), - terminal_background: this - .terminal_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_background: this - .terminal_ansi_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_foreground: this - .terminal_foreground - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_bright_foreground: this - .terminal_bright_foreground - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_dim_foreground: this - .terminal_dim_foreground - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_black: this - .terminal_ansi_black - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_bright_black: this - .terminal_ansi_bright_black - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_dim_black: this - .terminal_ansi_dim_black - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_red: this - .terminal_ansi_red - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_bright_red: this - .terminal_ansi_bright_red - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_dim_red: this - .terminal_ansi_dim_red - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_green: this - .terminal_ansi_green - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_bright_green: this - .terminal_ansi_bright_green - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_dim_green: this - .terminal_ansi_dim_green - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_yellow: this - .terminal_ansi_yellow - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_bright_yellow: this - .terminal_ansi_bright_yellow - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_dim_yellow: this - .terminal_ansi_dim_yellow - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_blue: this - .terminal_ansi_blue - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_bright_blue: this - .terminal_ansi_bright_blue - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_dim_blue: this - .terminal_ansi_dim_blue - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_magenta: this - .terminal_ansi_magenta - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_bright_magenta: this - .terminal_ansi_bright_magenta - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_dim_magenta: this - .terminal_ansi_dim_magenta - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_cyan: this - .terminal_ansi_cyan - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_bright_cyan: this - .terminal_ansi_bright_cyan - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_dim_cyan: this - .terminal_ansi_dim_cyan - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_white: this - .terminal_ansi_white - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_bright_white: this - .terminal_ansi_bright_white - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - terminal_ansi_dim_white: this - .terminal_ansi_dim_white - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - link_text_hover: this - .link_text_hover - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - version_control_added: this - .version_control_added - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - // Fall back to `created`, for backwards compatibility. - .or(status_colors.created), - version_control_deleted: this - .version_control_deleted - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - // Fall back to `deleted`, for backwards compatibility. - .or(status_colors.deleted), - version_control_modified: this - .version_control_modified - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - // Fall back to `modified`, for backwards compatibility. - .or(status_colors.modified), - version_control_renamed: this - .version_control_renamed - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - // Fall back to `modified`, for backwards compatibility. - .or(status_colors.modified), - version_control_conflict: this - .version_control_conflict - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - // Fall back to `ignored`, for backwards compatibility. - .or(status_colors.ignored), - version_control_ignored: this - .version_control_ignored - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - // Fall back to `conflict`, for backwards compatibility. - .or(status_colors.ignored), - version_control_word_added: this - .version_control_word_added - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - version_control_word_deleted: this - .version_control_word_deleted - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - #[allow(deprecated)] - version_control_conflict_marker_ours: this - .version_control_conflict_marker_ours - .as_ref() - .or(this.version_control_conflict_ours_background.as_ref()) - .and_then(|color| try_parse_color(color).ok()), - #[allow(deprecated)] - version_control_conflict_marker_theirs: this - .version_control_conflict_marker_theirs - .as_ref() - .or(this.version_control_conflict_theirs_background.as_ref()) - .and_then(|color| try_parse_color(color).ok()), - vim_normal_background: this - .vim_normal_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - vim_insert_background: this - .vim_insert_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - vim_replace_background: this - .vim_replace_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - vim_visual_background: this - .vim_visual_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - vim_visual_line_background: this - .vim_visual_line_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - vim_visual_block_background: this - .vim_visual_block_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - vim_helix_normal_background: this - .vim_helix_normal_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - vim_helix_select_background: this - .vim_helix_select_background - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - vim_mode_text: this - .vim_mode_text - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - } -} - -pub(crate) fn try_parse_color(color: &str) -> anyhow::Result { - let rgba = gpui::Rgba::try_from(color)?; - let rgba = palette::rgb::Srgba::from_components((rgba.r, rgba.g, rgba.b, rgba.a)); - let hsla = palette::Hsla::from_color(rgba); - - let hsla = gpui::hsla( - hsla.hue.into_positive_degrees() / 360., - hsla.saturation, - hsla.lightness, - hsla.alpha, - ); - - Ok(hsla) -} diff --git a/crates/theme/src/settings.rs b/crates/theme/src/settings.rs deleted file mode 100644 index d60d4882a6..0000000000 --- a/crates/theme/src/settings.rs +++ /dev/null @@ -1,749 +0,0 @@ -use crate::{ - Appearance, DEFAULT_ICON_THEME_NAME, SyntaxTheme, Theme, status_colors_refinement, - syntax_overrides, theme_colors_refinement, -}; -use collections::HashMap; -use derive_more::{Deref, DerefMut}; -use gpui::{ - App, Context, Font, FontFallbacks, FontStyle, FontWeight, Global, Pixels, Subscription, Window, - px, -}; -use refineable::Refineable; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; -pub use settings::{FontFamilyName, IconThemeName, ThemeAppearanceMode, ThemeName}; -use settings::{RegisterSetting, Settings, SettingsContent}; -use std::sync::Arc; - -const MIN_FONT_SIZE: Pixels = px(6.0); -const MAX_FONT_SIZE: Pixels = px(100.0); -const MIN_LINE_HEIGHT: f32 = 1.0; - -#[derive( - Debug, - Default, - PartialEq, - Eq, - PartialOrd, - Ord, - Hash, - Clone, - Copy, - Serialize, - Deserialize, - JsonSchema, -)] - -/// Specifies the density of the UI. -/// Note: This setting is still experimental. See [this tracking issue](https://github.com/zed-industries/zed/issues/18078) -#[serde(rename_all = "snake_case")] -pub enum UiDensity { - /// A denser UI with tighter spacing and smaller elements. - #[serde(alias = "compact")] - Compact, - #[default] - #[serde(alias = "default")] - /// The default UI density. - Default, - #[serde(alias = "comfortable")] - /// A looser UI with more spacing and larger elements. - Comfortable, -} - -impl UiDensity { - /// The spacing ratio of a given density. - /// TODO: Standardize usage throughout the app or remove - pub fn spacing_ratio(self) -> f32 { - match self { - UiDensity::Compact => 0.75, - UiDensity::Default => 1.0, - UiDensity::Comfortable => 1.25, - } - } -} - -impl From for UiDensity { - fn from(s: String) -> Self { - match s.as_str() { - "compact" => Self::Compact, - "default" => Self::Default, - "comfortable" => Self::Comfortable, - _ => Self::default(), - } - } -} - -impl From for String { - fn from(val: UiDensity) -> Self { - match val { - UiDensity::Compact => "compact".to_string(), - UiDensity::Default => "default".to_string(), - UiDensity::Comfortable => "comfortable".to_string(), - } - } -} - -impl From for UiDensity { - fn from(val: settings::UiDensity) -> Self { - match val { - settings::UiDensity::Compact => Self::Compact, - settings::UiDensity::Default => Self::Default, - settings::UiDensity::Comfortable => Self::Comfortable, - } - } -} - -/// Customizable settings for the UI and theme system. -#[derive(Clone, PartialEq, RegisterSetting)] -pub struct ThemeSettings { - /// The UI font size. Determines the size of text in the UI, - /// as well as the size of a [gpui::Rems] unit. - /// - /// Changing this will impact the size of all UI elements. - ui_font_size: Pixels, - /// The font used for UI elements. - pub ui_font: Font, - /// The font size used for buffers, and the terminal. - /// - /// The terminal font size can be overridden using it's own setting. - buffer_font_size: Pixels, - /// The font used for buffers, and the terminal. - /// - /// The terminal font family can be overridden using it's own setting. - pub buffer_font: Font, - /// The agent font size. Determines the size of text in the agent panel. Falls back to the UI font size if unset. - agent_ui_font_size: Option, - /// The agent buffer font size. Determines the size of user messages in the agent panel. - agent_buffer_font_size: Option, - /// The line height for buffers, and the terminal. - /// - /// Changing this may affect the spacing of some UI elements. - /// - /// The terminal font family can be overridden using it's own setting. - pub buffer_line_height: BufferLineHeight, - /// The current theme selection. - pub theme: ThemeSelection, - /// Manual overrides for the active theme. - /// - /// Note: This setting is still experimental. See [this tracking issue](https://github.com/zed-industries/zed/issues/18078) - pub experimental_theme_overrides: Option, - /// Manual overrides per theme - pub theme_overrides: HashMap, - /// The current icon theme selection. - pub icon_theme: IconThemeSelection, - /// The density of the UI. - /// Note: This setting is still experimental. See [this tracking issue]( - pub ui_density: UiDensity, - /// The amount of fading applied to unnecessary code. - pub unnecessary_code_fade: f32, -} - -pub(crate) const DEFAULT_LIGHT_THEME: &'static str = "One Light"; -pub(crate) const DEFAULT_DARK_THEME: &'static str = "One Dark"; - -/// Returns the name of the default theme for the given [`Appearance`]. -pub fn default_theme(appearance: Appearance) -> &'static str { - match appearance { - Appearance::Light => DEFAULT_LIGHT_THEME, - Appearance::Dark => DEFAULT_DARK_THEME, - } -} - -/// The appearance of the system. -#[derive(Debug, Clone, Copy, Deref)] -pub struct SystemAppearance(pub Appearance); - -impl Default for SystemAppearance { - fn default() -> Self { - Self(Appearance::Dark) - } -} - -#[derive(Deref, DerefMut, Default)] -struct GlobalSystemAppearance(SystemAppearance); - -impl Global for GlobalSystemAppearance {} - -impl SystemAppearance { - /// Initializes the [`SystemAppearance`] for the application. - pub fn init(cx: &mut App) { - *cx.default_global::() = - GlobalSystemAppearance(SystemAppearance(cx.window_appearance().into())); - } - - /// Returns the global [`SystemAppearance`]. - pub fn global(cx: &App) -> Self { - cx.global::().0 - } - - /// Returns a mutable reference to the global [`SystemAppearance`]. - pub fn global_mut(cx: &mut App) -> &mut Self { - cx.global_mut::() - } -} - -#[derive(Default)] -struct BufferFontSize(Pixels); - -impl Global for BufferFontSize {} - -#[derive(Default)] -pub(crate) struct UiFontSize(Pixels); - -impl Global for UiFontSize {} - -/// In-memory override for the font size in the agent panel. -#[derive(Default)] -pub struct AgentFontSize(Pixels); - -impl Global for AgentFontSize {} - -/// Represents the selection of a theme, which can be either static or dynamic. -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(untagged)] -pub enum ThemeSelection { - /// A static theme selection, represented by a single theme name. - Static(ThemeName), - /// A dynamic theme selection, which can change based the [ThemeMode]. - Dynamic { - /// The mode used to determine which theme to use. - #[serde(default)] - mode: ThemeAppearanceMode, - /// The theme to use for light mode. - light: ThemeName, - /// The theme to use for dark mode. - dark: ThemeName, - }, -} - -impl From for ThemeSelection { - fn from(selection: settings::ThemeSelection) -> Self { - match selection { - settings::ThemeSelection::Static(theme) => ThemeSelection::Static(theme), - settings::ThemeSelection::Dynamic { mode, light, dark } => { - ThemeSelection::Dynamic { mode, light, dark } - } - } - } -} - -impl ThemeSelection { - /// Returns the theme name for the selected [ThemeMode]. - pub fn name(&self, system_appearance: Appearance) -> ThemeName { - match self { - Self::Static(theme) => theme.clone(), - Self::Dynamic { mode, light, dark } => match mode { - ThemeAppearanceMode::Light => light.clone(), - ThemeAppearanceMode::Dark => dark.clone(), - ThemeAppearanceMode::System => match system_appearance { - Appearance::Light => light.clone(), - Appearance::Dark => dark.clone(), - }, - }, - } - } - - /// Returns the [ThemeMode] for the [ThemeSelection]. - pub fn mode(&self) -> Option { - match self { - ThemeSelection::Static(_) => None, - ThemeSelection::Dynamic { mode, .. } => Some(*mode), - } - } -} - -/// Represents the selection of an icon theme, which can be either static or dynamic. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum IconThemeSelection { - /// A static icon theme selection, represented by a single icon theme name. - Static(IconThemeName), - /// A dynamic icon theme selection, which can change based on the [`ThemeMode`]. - Dynamic { - /// The mode used to determine which theme to use. - mode: ThemeAppearanceMode, - /// The icon theme to use for light mode. - light: IconThemeName, - /// The icon theme to use for dark mode. - dark: IconThemeName, - }, -} - -impl From for IconThemeSelection { - fn from(selection: settings::IconThemeSelection) -> Self { - match selection { - settings::IconThemeSelection::Static(theme) => IconThemeSelection::Static(theme), - settings::IconThemeSelection::Dynamic { mode, light, dark } => { - IconThemeSelection::Dynamic { mode, light, dark } - } - } - } -} - -impl IconThemeSelection { - /// Returns the icon theme name based on the given [`Appearance`]. - pub fn name(&self, system_appearance: Appearance) -> IconThemeName { - match self { - Self::Static(theme) => theme.clone(), - Self::Dynamic { mode, light, dark } => match mode { - ThemeAppearanceMode::Light => light.clone(), - ThemeAppearanceMode::Dark => dark.clone(), - ThemeAppearanceMode::System => match system_appearance { - Appearance::Light => light.clone(), - Appearance::Dark => dark.clone(), - }, - }, - } - } - - /// Returns the [`ThemeMode`] for the [`IconThemeSelection`]. - pub fn mode(&self) -> Option { - match self { - IconThemeSelection::Static(_) => None, - IconThemeSelection::Dynamic { mode, .. } => Some(*mode), - } - } -} - -/// Sets the theme for the given appearance to the theme with the specified name. -/// -/// The caller should make sure that the [`Appearance`] matches the theme associated with the name. -/// -/// If the current [`ThemeAppearanceMode`] is set to [`System`] and the user's system [`Appearance`] -/// is different than the new theme's [`Appearance`], this function will update the -/// [`ThemeAppearanceMode`] to the new theme's appearance in order to display the new theme. -/// -/// [`System`]: ThemeAppearanceMode::System -pub fn set_theme( - current: &mut SettingsContent, - theme_name: impl Into>, - theme_appearance: Appearance, - system_appearance: Appearance, -) { - let theme_name = ThemeName(theme_name.into()); - - let Some(selection) = current.theme.theme.as_mut() else { - current.theme.theme = Some(settings::ThemeSelection::Static(theme_name)); - return; - }; - - match selection { - settings::ThemeSelection::Static(theme) => { - *theme = theme_name; - } - settings::ThemeSelection::Dynamic { mode, light, dark } => { - // Update the appropriate theme slot based on appearance. - match theme_appearance { - Appearance::Light => *light = theme_name, - Appearance::Dark => *dark = theme_name, - } - - // Don't update the theme mode if it is set to system and the new theme has the same - // appearance. - let should_update_mode = - !(mode == &ThemeAppearanceMode::System && theme_appearance == system_appearance); - - if should_update_mode { - // Update the mode to the specified appearance (otherwise we might set the theme and - // nothing gets updated because the system specified the other mode appearance). - *mode = ThemeAppearanceMode::from(theme_appearance); - } - } - } -} - -/// Sets the icon theme for the given appearance to the icon theme with the specified name. -pub fn set_icon_theme( - current: &mut SettingsContent, - icon_theme_name: IconThemeName, - appearance: Appearance, -) { - if let Some(selection) = current.theme.icon_theme.as_mut() { - let icon_theme_to_update = match selection { - settings::IconThemeSelection::Static(theme) => theme, - settings::IconThemeSelection::Dynamic { mode, light, dark } => match mode { - ThemeAppearanceMode::Light => light, - ThemeAppearanceMode::Dark => dark, - ThemeAppearanceMode::System => match appearance { - Appearance::Light => light, - Appearance::Dark => dark, - }, - }, - }; - - *icon_theme_to_update = icon_theme_name; - } else { - current.theme.icon_theme = Some(settings::IconThemeSelection::Static(icon_theme_name)); - } -} - -/// Sets the mode for the theme. -pub fn set_mode(content: &mut SettingsContent, mode: ThemeAppearanceMode) { - let theme = content.theme.as_mut(); - - if let Some(selection) = theme.theme.as_mut() { - match selection { - settings::ThemeSelection::Static(theme) => { - // If the theme was previously set to a single static theme, - // we don't know whether it was a light or dark theme, so we - // just use it for both. - *selection = settings::ThemeSelection::Dynamic { - mode, - light: theme.clone(), - dark: theme.clone(), - }; - } - settings::ThemeSelection::Dynamic { - mode: mode_to_update, - .. - } => *mode_to_update = mode, - } - } else { - theme.theme = Some(settings::ThemeSelection::Dynamic { - mode, - light: ThemeName(DEFAULT_LIGHT_THEME.into()), - dark: ThemeName(DEFAULT_DARK_THEME.into()), - }); - } - - if let Some(selection) = theme.icon_theme.as_mut() { - match selection { - settings::IconThemeSelection::Static(icon_theme) => { - // If the icon theme was previously set to a single static - // theme, we don't know whether it was a light or dark - // theme, so we just use it for both. - *selection = settings::IconThemeSelection::Dynamic { - mode, - light: icon_theme.clone(), - dark: icon_theme.clone(), - }; - } - settings::IconThemeSelection::Dynamic { - mode: mode_to_update, - .. - } => *mode_to_update = mode, - } - } else { - theme.icon_theme = Some(settings::IconThemeSelection::Static(IconThemeName( - DEFAULT_ICON_THEME_NAME.into(), - ))); - } -} -// } - -/// The buffer's line height. -#[derive(Clone, Copy, Debug, PartialEq, Default)] -pub enum BufferLineHeight { - /// A less dense line height. - #[default] - Comfortable, - /// The default line height. - Standard, - /// A custom line height, where 1.0 is the font's height. Must be at least 1.0. - Custom(f32), -} - -impl From for BufferLineHeight { - fn from(value: settings::BufferLineHeight) -> Self { - match value { - settings::BufferLineHeight::Comfortable => BufferLineHeight::Comfortable, - settings::BufferLineHeight::Standard => BufferLineHeight::Standard, - settings::BufferLineHeight::Custom(line_height) => { - BufferLineHeight::Custom(line_height) - } - } - } -} - -impl BufferLineHeight { - /// Returns the value of the line height. - pub fn value(&self) -> f32 { - match self { - BufferLineHeight::Comfortable => 1.618, - BufferLineHeight::Standard => 1.3, - BufferLineHeight::Custom(line_height) => *line_height, - } - } -} - -impl ThemeSettings { - /// Returns the buffer font size. - pub fn buffer_font_size(&self, cx: &App) -> Pixels { - let font_size = cx - .try_global::() - .map(|size| size.0) - .unwrap_or(self.buffer_font_size); - clamp_font_size(font_size) - } - - /// Returns the UI font size. - pub fn ui_font_size(&self, cx: &App) -> Pixels { - let font_size = cx - .try_global::() - .map(|size| size.0) - .unwrap_or(self.ui_font_size); - clamp_font_size(font_size) - } - - /// Returns the agent panel font size. Falls back to the UI font size if unset. - pub fn agent_ui_font_size(&self, cx: &App) -> Pixels { - cx.try_global::() - .map(|size| size.0) - .or(self.agent_ui_font_size) - .map(clamp_font_size) - .unwrap_or_else(|| self.ui_font_size(cx)) - } - - /// Returns the agent panel buffer font size. - pub fn agent_buffer_font_size(&self, cx: &App) -> Pixels { - cx.try_global::() - .map(|size| size.0) - .or(self.agent_buffer_font_size) - .map(clamp_font_size) - .unwrap_or_else(|| self.buffer_font_size(cx)) - } - - /// Returns the buffer font size, read from the settings. - /// - /// The real buffer font size is stored in-memory, to support temporary font size changes. - /// Use [`Self::buffer_font_size`] to get the real font size. - pub fn buffer_font_size_settings(&self) -> Pixels { - self.buffer_font_size - } - - /// Returns the UI font size, read from the settings. - /// - /// The real UI font size is stored in-memory, to support temporary font size changes. - /// Use [`Self::ui_font_size`] to get the real font size. - pub fn ui_font_size_settings(&self) -> Pixels { - self.ui_font_size - } - - /// Returns the agent font size, read from the settings. - /// - /// The real agent font size is stored in-memory, to support temporary font size changes. - /// Use [`Self::agent_ui_font_size`] to get the real font size. - pub fn agent_ui_font_size_settings(&self) -> Option { - self.agent_ui_font_size - } - - /// Returns the agent buffer font size, read from the settings. - /// - /// The real agent buffer font size is stored in-memory, to support temporary font size changes. - /// Use [`Self::agent_buffer_font_size`] to get the real font size. - pub fn agent_buffer_font_size_settings(&self) -> Option { - self.agent_buffer_font_size - } - - // TODO: Rename: `line_height` -> `buffer_line_height` - /// Returns the buffer's line height. - pub fn line_height(&self) -> f32 { - f32::max(self.buffer_line_height.value(), MIN_LINE_HEIGHT) - } - - /// Applies the theme overrides, if there are any, to the current theme. - pub fn apply_theme_overrides(&self, mut arc_theme: Arc) -> Arc { - // Apply the old overrides setting first, so that the new setting can override those. - if let Some(experimental_theme_overrides) = &self.experimental_theme_overrides { - let mut theme = (*arc_theme).clone(); - ThemeSettings::modify_theme(&mut theme, experimental_theme_overrides); - arc_theme = Arc::new(theme); - } - - if let Some(theme_overrides) = self.theme_overrides.get(arc_theme.name.as_ref()) { - let mut theme = (*arc_theme).clone(); - ThemeSettings::modify_theme(&mut theme, theme_overrides); - arc_theme = Arc::new(theme); - } - - arc_theme - } - - fn modify_theme(base_theme: &mut Theme, theme_overrides: &settings::ThemeStyleContent) { - if let Some(window_background_appearance) = theme_overrides.window_background_appearance { - base_theme.styles.window_background_appearance = window_background_appearance.into(); - } - let status_color_refinement = status_colors_refinement(&theme_overrides.status); - - base_theme.styles.colors.refine(&theme_colors_refinement( - &theme_overrides.colors, - &status_color_refinement, - )); - base_theme.styles.status.refine(&status_color_refinement); - base_theme.styles.player.merge(&theme_overrides.players); - base_theme.styles.accents.merge(&theme_overrides.accents); - base_theme.styles.syntax = SyntaxTheme::merge( - base_theme.styles.syntax.clone(), - syntax_overrides(&theme_overrides), - ); - } -} - -/// Observe changes to the adjusted buffer font size. -pub fn observe_buffer_font_size_adjustment( - cx: &mut Context, - f: impl 'static + Fn(&mut V, &mut Context), -) -> Subscription { - cx.observe_global::(f) -} - -/// Gets the font size, adjusted by the difference between the current buffer font size and the one set in the settings. -pub fn adjusted_font_size(size: Pixels, cx: &App) -> Pixels { - let adjusted_font_size = - if let Some(BufferFontSize(adjusted_size)) = cx.try_global::() { - let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size; - let delta = *adjusted_size - buffer_font_size; - size + delta - } else { - size - }; - clamp_font_size(adjusted_font_size) -} - -/// Adjusts the buffer font size. -pub fn adjust_buffer_font_size(cx: &mut App, f: impl FnOnce(Pixels) -> Pixels) { - let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size; - let adjusted_size = cx - .try_global::() - .map_or(buffer_font_size, |adjusted_size| adjusted_size.0); - cx.set_global(BufferFontSize(clamp_font_size(f(adjusted_size)))); - cx.refresh_windows(); -} - -/// Resets the buffer font size to the default value. -pub fn reset_buffer_font_size(cx: &mut App) { - if cx.has_global::() { - cx.remove_global::(); - cx.refresh_windows(); - } -} - -// TODO: Make private, change usages to use `get_ui_font_size` instead. -#[allow(missing_docs)] -pub fn setup_ui_font(window: &mut Window, cx: &mut App) -> gpui::Font { - let (ui_font, ui_font_size) = { - let theme_settings = ThemeSettings::get_global(cx); - let font = theme_settings.ui_font.clone(); - (font, theme_settings.ui_font_size(cx)) - }; - - window.set_rem_size(ui_font_size); - ui_font -} - -/// Sets the adjusted UI font size. -pub fn adjust_ui_font_size(cx: &mut App, f: impl FnOnce(Pixels) -> Pixels) { - let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx); - let adjusted_size = cx - .try_global::() - .map_or(ui_font_size, |adjusted_size| adjusted_size.0); - cx.set_global(UiFontSize(clamp_font_size(f(adjusted_size)))); - cx.refresh_windows(); -} - -/// Resets the UI font size to the default value. -pub fn reset_ui_font_size(cx: &mut App) { - if cx.has_global::() { - cx.remove_global::(); - cx.refresh_windows(); - } -} - -/// Sets the adjusted font size of agent responses in the agent panel. -pub fn adjust_agent_ui_font_size(cx: &mut App, f: impl FnOnce(Pixels) -> Pixels) { - let agent_ui_font_size = ThemeSettings::get_global(cx).agent_ui_font_size(cx); - let adjusted_size = cx - .try_global::() - .map_or(agent_ui_font_size, |adjusted_size| adjusted_size.0); - cx.set_global(AgentFontSize(clamp_font_size(f(adjusted_size)))); - cx.refresh_windows(); -} - -/// Resets the agent response font size in the agent panel to the default value. -pub fn reset_agent_ui_font_size(cx: &mut App) { - if cx.has_global::() { - cx.remove_global::(); - cx.refresh_windows(); - } -} - -/// Sets the adjusted font size of user messages in the agent panel. -pub fn adjust_agent_buffer_font_size(cx: &mut App, f: impl FnOnce(Pixels) -> Pixels) { - let agent_buffer_font_size = ThemeSettings::get_global(cx).agent_buffer_font_size(cx); - let adjusted_size = cx - .try_global::() - .map_or(agent_buffer_font_size, |adjusted_size| adjusted_size.0); - cx.set_global(AgentFontSize(clamp_font_size(f(adjusted_size)))); - cx.refresh_windows(); -} - -/// Resets the user message font size in the agent panel to the default value. -pub fn reset_agent_buffer_font_size(cx: &mut App) { - if cx.has_global::() { - cx.remove_global::(); - cx.refresh_windows(); - } -} - -/// Ensures font size is within the valid range. -pub fn clamp_font_size(size: Pixels) -> Pixels { - size.clamp(MIN_FONT_SIZE, MAX_FONT_SIZE) -} - -fn clamp_font_weight(weight: f32) -> FontWeight { - FontWeight(weight.clamp(100., 950.)) -} - -/// font fallback from settings -pub fn font_fallbacks_from_settings( - fallbacks: Option>, -) -> Option { - fallbacks.map(|fallbacks| { - FontFallbacks::from_fonts( - fallbacks - .into_iter() - .map(|font_family| font_family.0.to_string()) - .collect(), - ) - }) -} - -impl settings::Settings for ThemeSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - let content = &content.theme; - let theme_selection: ThemeSelection = content.theme.clone().unwrap().into(); - let icon_theme_selection: IconThemeSelection = content.icon_theme.clone().unwrap().into(); - Self { - ui_font_size: clamp_font_size(content.ui_font_size.unwrap().into()), - ui_font: Font { - family: content.ui_font_family.as_ref().unwrap().0.clone().into(), - features: content.ui_font_features.clone().unwrap(), - fallbacks: font_fallbacks_from_settings(content.ui_font_fallbacks.clone()), - weight: clamp_font_weight(content.ui_font_weight.unwrap().0), - style: Default::default(), - }, - buffer_font: Font { - family: content - .buffer_font_family - .as_ref() - .unwrap() - .0 - .clone() - .into(), - features: content.buffer_font_features.clone().unwrap(), - fallbacks: font_fallbacks_from_settings(content.buffer_font_fallbacks.clone()), - weight: clamp_font_weight(content.buffer_font_weight.unwrap().0), - style: FontStyle::default(), - }, - buffer_font_size: clamp_font_size(content.buffer_font_size.unwrap().into()), - buffer_line_height: content.buffer_line_height.unwrap().into(), - agent_ui_font_size: content.agent_ui_font_size.map(Into::into), - agent_buffer_font_size: content.agent_buffer_font_size.map(Into::into), - theme: theme_selection, - experimental_theme_overrides: content.experimental_theme_overrides.clone(), - theme_overrides: content.theme_overrides.clone(), - icon_theme: icon_theme_selection, - ui_density: content.ui_density.unwrap_or_default().into(), - unnecessary_code_fade: content.unnecessary_code_fade.unwrap().0.clamp(0.0, 0.9), - } - } -} diff --git a/crates/theme/src/styles.rs b/crates/theme/src/styles.rs deleted file mode 100644 index da22f8de1f..0000000000 --- a/crates/theme/src/styles.rs +++ /dev/null @@ -1,13 +0,0 @@ -mod accents; -mod colors; -mod players; -mod status; -mod syntax; -mod system; - -pub use accents::*; -pub use colors::*; -pub use players::*; -pub use status::*; -pub use syntax::*; -pub use system::*; diff --git a/crates/theme/src/styles/accents.rs b/crates/theme/src/styles/accents.rs deleted file mode 100644 index 49ae755bf8..0000000000 --- a/crates/theme/src/styles/accents.rs +++ /dev/null @@ -1,88 +0,0 @@ -use gpui::Hsla; -use serde::Deserialize; - -use crate::{ - amber, blue, cyan, gold, grass, indigo, iris, jade, lime, orange, pink, purple, tomato, - try_parse_color, -}; - -/// A collection of colors that are used to color indent aware lines in the editor. -#[derive(Clone, Debug, Deserialize, PartialEq)] -pub struct AccentColors(pub Vec); - -impl Default for AccentColors { - /// Don't use this! - /// We have to have a default to be `[refineable::Refinable]`. - /// TODO "Find a way to not need this for Refinable" - fn default() -> Self { - Self::dark() - } -} - -impl AccentColors { - /// Returns the set of dark accent colors. - pub fn dark() -> Self { - Self(vec![ - blue().dark().step_9(), - orange().dark().step_9(), - pink().dark().step_9(), - lime().dark().step_9(), - purple().dark().step_9(), - amber().dark().step_9(), - jade().dark().step_9(), - tomato().dark().step_9(), - cyan().dark().step_9(), - gold().dark().step_9(), - grass().dark().step_9(), - indigo().dark().step_9(), - iris().dark().step_9(), - ]) - } - - /// Returns the set of light accent colors. - pub fn light() -> Self { - Self(vec![ - blue().light().step_9(), - orange().light().step_9(), - pink().light().step_9(), - lime().light().step_9(), - purple().light().step_9(), - amber().light().step_9(), - jade().light().step_9(), - tomato().light().step_9(), - cyan().light().step_9(), - gold().light().step_9(), - grass().light().step_9(), - indigo().light().step_9(), - iris().light().step_9(), - ]) - } -} - -impl AccentColors { - /// Returns the color for the given index. - pub fn color_for_index(&self, index: u32) -> Hsla { - self.0[index as usize % self.0.len()] - } - - /// Merges the given accent colors into this [`AccentColors`] instance. - pub fn merge(&mut self, accent_colors: &[settings::AccentContent]) { - if accent_colors.is_empty() { - return; - } - - let colors = accent_colors - .iter() - .filter_map(|accent_color| { - accent_color - .0 - .as_ref() - .and_then(|color| try_parse_color(color).ok()) - }) - .collect::>(); - - if !colors.is_empty() { - self.0 = colors; - } - } -} diff --git a/crates/theme/src/styles/colors.rs b/crates/theme/src/styles/colors.rs deleted file mode 100644 index 905f2245e0..0000000000 --- a/crates/theme/src/styles/colors.rs +++ /dev/null @@ -1,655 +0,0 @@ -#![allow(missing_docs)] - -use gpui::{App, Hsla, SharedString, WindowBackgroundAppearance}; -use refineable::Refineable; -use std::sync::Arc; -use strum::{AsRefStr, EnumIter, IntoEnumIterator}; - -use crate::{ - AccentColors, ActiveTheme, PlayerColors, StatusColors, StatusColorsRefinement, SyntaxTheme, - SystemColors, -}; - -#[derive(Refineable, Clone, Debug, PartialEq)] -#[refineable(Debug, serde::Deserialize)] -pub struct ThemeColors { - /// Border color. Used for most borders, is usually a high contrast color. - pub border: Hsla, - /// Border color. Used for deemphasized borders, like a visual divider between two sections - pub border_variant: Hsla, - /// Border color. Used for focused elements, like keyboard focused list item. - pub border_focused: Hsla, - /// Border color. Used for selected elements, like an active search filter or selected checkbox. - pub border_selected: Hsla, - /// Border color. Used for transparent borders. Used for placeholder borders when an element gains a border on state change. - pub border_transparent: Hsla, - /// Border color. Used for disabled elements, like a disabled input or button. - pub border_disabled: Hsla, - /// Border color. Used for elevated surfaces, like a context menu, popup, or dialog. - pub elevated_surface_background: Hsla, - /// Background Color. Used for grounded surfaces like a panel or tab. - pub surface_background: Hsla, - /// Background Color. Used for the app background and blank panels or windows. - pub background: Hsla, - /// Background Color. Used for the background of an element that should have a different background than the surface it's on. - /// - /// Elements might include: Buttons, Inputs, Checkboxes, Radio Buttons... - /// - /// For an element that should have the same background as the surface it's on, use `ghost_element_background`. - pub element_background: Hsla, - /// Background Color. Used for the hover state of an element that should have a different background than the surface it's on. - /// - /// Hover states are triggered by the mouse entering an element, or a finger touching an element on a touch screen. - pub element_hover: Hsla, - /// Background Color. Used for the active state of an element that should have a different background than the surface it's on. - /// - /// Active states are triggered by the mouse button being pressed down on an element, or the Return button or other activator being pressed. - pub element_active: Hsla, - /// Background Color. Used for the selected state of an element that should have a different background than the surface it's on. - /// - /// Selected states are triggered by the element being selected (or "activated") by the user. - /// - /// This could include a selected checkbox, a toggleable button that is toggled on, etc. - pub element_selected: Hsla, - /// Background Color. Used for the background of selections in a UI element. - pub element_selection_background: Hsla, - /// Background Color. Used for the disabled state of an element that should have a different background than the surface it's on. - /// - /// Disabled states are shown when a user cannot interact with an element, like a disabled button or input. - pub element_disabled: Hsla, - /// Background Color. Used for the area that shows where a dragged element will be dropped. - pub drop_target_background: Hsla, - /// Border Color. Used for the border that shows where a dragged element will be dropped. - pub drop_target_border: Hsla, - /// Used for the background of a ghost element that should have the same background as the surface it's on. - /// - /// Elements might include: Buttons, Inputs, Checkboxes, Radio Buttons... - /// - /// For an element that should have a different background than the surface it's on, use `element_background`. - pub ghost_element_background: Hsla, - /// Background Color. Used for the hover state of a ghost element that should have the same background as the surface it's on. - /// - /// Hover states are triggered by the mouse entering an element, or a finger touching an element on a touch screen. - pub ghost_element_hover: Hsla, - /// Background Color. Used for the active state of a ghost element that should have the same background as the surface it's on. - /// - /// Active states are triggered by the mouse button being pressed down on an element, or the Return button or other activator being pressed. - pub ghost_element_active: Hsla, - /// Background Color. Used for the selected state of a ghost element that should have the same background as the surface it's on. - /// - /// Selected states are triggered by the element being selected (or "activated") by the user. - /// - /// This could include a selected checkbox, a toggleable button that is toggled on, etc. - pub ghost_element_selected: Hsla, - /// Background Color. Used for the disabled state of a ghost element that should have the same background as the surface it's on. - /// - /// Disabled states are shown when a user cannot interact with an element, like a disabled button or input. - pub ghost_element_disabled: Hsla, - /// Text Color. Default text color used for most text. - pub text: Hsla, - /// Text Color. Color of muted or deemphasized text. It is a subdued version of the standard text color. - pub text_muted: Hsla, - /// Text Color. Color of the placeholder text typically shown in input fields to guide the user to enter valid data. - pub text_placeholder: Hsla, - /// Text Color. Color used for text denoting disabled elements. Typically, the color is faded or grayed out to emphasize the disabled state. - pub text_disabled: Hsla, - /// Text Color. Color used for emphasis or highlighting certain text, like an active filter or a matched character in a search. - pub text_accent: Hsla, - /// Fill Color. Used for the default fill color of an icon. - pub icon: Hsla, - /// Fill Color. Used for the muted or deemphasized fill color of an icon. - /// - /// This might be used to show an icon in an inactive pane, or to deemphasize a series of icons to give them less visual weight. - pub icon_muted: Hsla, - /// Fill Color. Used for the disabled fill color of an icon. - /// - /// Disabled states are shown when a user cannot interact with an element, like a icon button. - pub icon_disabled: Hsla, - /// Fill Color. Used for the placeholder fill color of an icon. - /// - /// This might be used to show an icon in an input that disappears when the user enters text. - pub icon_placeholder: Hsla, - /// Fill Color. Used for the accent fill color of an icon. - /// - /// This might be used to show when a toggleable icon button is selected. - pub icon_accent: Hsla, - /// Color used to accent some debugger elements - /// Is used by breakpoints - pub debugger_accent: Hsla, - - // === - // UI Elements - // === - pub status_bar_background: Hsla, - pub title_bar_background: Hsla, - pub title_bar_inactive_background: Hsla, - pub toolbar_background: Hsla, - pub tab_bar_background: Hsla, - pub tab_inactive_background: Hsla, - pub tab_active_background: Hsla, - pub search_match_background: Hsla, - pub search_active_match_background: Hsla, - pub panel_background: Hsla, - pub panel_focused_border: Hsla, - pub panel_indent_guide: Hsla, - pub panel_indent_guide_hover: Hsla, - pub panel_indent_guide_active: Hsla, - - /// The color of the overlay surface on top of panel. - pub panel_overlay_background: Hsla, - /// The color of the overlay surface on top of panel when hovered over. - pub panel_overlay_hover: Hsla, - - pub pane_focused_border: Hsla, - pub pane_group_border: Hsla, - /// The color of the scrollbar thumb. - pub scrollbar_thumb_background: Hsla, - /// The color of the scrollbar thumb when hovered over. - pub scrollbar_thumb_hover_background: Hsla, - /// The color of the scrollbar thumb whilst being actively dragged. - pub scrollbar_thumb_active_background: Hsla, - /// The border color of the scrollbar thumb. - pub scrollbar_thumb_border: Hsla, - /// The background color of the scrollbar track. - pub scrollbar_track_background: Hsla, - /// The border color of the scrollbar track. - pub scrollbar_track_border: Hsla, - /// The color of the minimap thumb. - pub minimap_thumb_background: Hsla, - /// The color of the minimap thumb when hovered over. - pub minimap_thumb_hover_background: Hsla, - /// The color of the minimap thumb whilst being actively dragged. - pub minimap_thumb_active_background: Hsla, - /// The border color of the minimap thumb. - pub minimap_thumb_border: Hsla, - - /// Background color for Vim Normal mode indicator. - pub vim_normal_background: Hsla, - /// Background color for Vim Insert mode indicator. - pub vim_insert_background: Hsla, - /// Background color for Vim Replace mode indicator. - pub vim_replace_background: Hsla, - /// Background color for Vim Visual mode indicator. - pub vim_visual_background: Hsla, - /// Background color for Vim Visual Line mode indicator. - pub vim_visual_line_background: Hsla, - /// Background color for Vim Visual Block mode indicator. - pub vim_visual_block_background: Hsla, - /// Background color for Vim Helix Normal mode indicator. - pub vim_helix_normal_background: Hsla, - /// Background color for Vim Helix Select mode indicator. - pub vim_helix_select_background: Hsla, - /// Text color for Vim mode indicator label. - pub vim_mode_text: Hsla, - - // === - // Editor - // === - pub editor_foreground: Hsla, - pub editor_background: Hsla, - pub editor_gutter_background: Hsla, - pub editor_subheader_background: Hsla, - pub editor_active_line_background: Hsla, - pub editor_highlighted_line_background: Hsla, - /// Line color of the line a debugger is currently stopped at - pub editor_debugger_active_line_background: Hsla, - /// Text Color. Used for the text of the line number in the editor gutter. - pub editor_line_number: Hsla, - /// Text Color. Used for the text of the line number in the editor gutter when the line is highlighted. - pub editor_active_line_number: Hsla, - /// Text Color. Used for the text of the line number in the editor gutter when the line is hovered over. - pub editor_hover_line_number: Hsla, - /// Text Color. Used to mark invisible characters in the editor. - /// - /// Example: spaces, tabs, carriage returns, etc. - pub editor_invisible: Hsla, - pub editor_wrap_guide: Hsla, - pub editor_active_wrap_guide: Hsla, - pub editor_indent_guide: Hsla, - pub editor_indent_guide_active: Hsla, - /// Read-access of a symbol, like reading a variable. - /// - /// A document highlight is a range inside a text document which deserves - /// special attention. Usually a document highlight is visualized by changing - /// the background color of its range. - pub editor_document_highlight_read_background: Hsla, - /// Read-access of a symbol, like reading a variable. - /// - /// A document highlight is a range inside a text document which deserves - /// special attention. Usually a document highlight is visualized by changing - /// the background color of its range. - pub editor_document_highlight_write_background: Hsla, - /// Highlighted brackets background color. - /// - /// Matching brackets in the cursor scope are highlighted with this background color. - pub editor_document_highlight_bracket_background: Hsla, - - // === - // Terminal - // === - /// Terminal layout background color. - pub terminal_background: Hsla, - /// Terminal foreground color. - pub terminal_foreground: Hsla, - /// Bright terminal foreground color. - pub terminal_bright_foreground: Hsla, - /// Dim terminal foreground color. - pub terminal_dim_foreground: Hsla, - /// Terminal ANSI background color. - pub terminal_ansi_background: Hsla, - /// Black ANSI terminal color. - pub terminal_ansi_black: Hsla, - /// Bright black ANSI terminal color. - pub terminal_ansi_bright_black: Hsla, - /// Dim black ANSI terminal color. - pub terminal_ansi_dim_black: Hsla, - /// Red ANSI terminal color. - pub terminal_ansi_red: Hsla, - /// Bright red ANSI terminal color. - pub terminal_ansi_bright_red: Hsla, - /// Dim red ANSI terminal color. - pub terminal_ansi_dim_red: Hsla, - /// Green ANSI terminal color. - pub terminal_ansi_green: Hsla, - /// Bright green ANSI terminal color. - pub terminal_ansi_bright_green: Hsla, - /// Dim green ANSI terminal color. - pub terminal_ansi_dim_green: Hsla, - /// Yellow ANSI terminal color. - pub terminal_ansi_yellow: Hsla, - /// Bright yellow ANSI terminal color. - pub terminal_ansi_bright_yellow: Hsla, - /// Dim yellow ANSI terminal color. - pub terminal_ansi_dim_yellow: Hsla, - /// Blue ANSI terminal color. - pub terminal_ansi_blue: Hsla, - /// Bright blue ANSI terminal color. - pub terminal_ansi_bright_blue: Hsla, - /// Dim blue ANSI terminal color. - pub terminal_ansi_dim_blue: Hsla, - /// Magenta ANSI terminal color. - pub terminal_ansi_magenta: Hsla, - /// Bright magenta ANSI terminal color. - pub terminal_ansi_bright_magenta: Hsla, - /// Dim magenta ANSI terminal color. - pub terminal_ansi_dim_magenta: Hsla, - /// Cyan ANSI terminal color. - pub terminal_ansi_cyan: Hsla, - /// Bright cyan ANSI terminal color. - pub terminal_ansi_bright_cyan: Hsla, - /// Dim cyan ANSI terminal color. - pub terminal_ansi_dim_cyan: Hsla, - /// White ANSI terminal color. - pub terminal_ansi_white: Hsla, - /// Bright white ANSI terminal color. - pub terminal_ansi_bright_white: Hsla, - /// Dim white ANSI terminal color. - pub terminal_ansi_dim_white: Hsla, - - /// Represents a link text hover color. - pub link_text_hover: Hsla, - - /// Represents an added entry or hunk in vcs, like git. - pub version_control_added: Hsla, - /// Represents a deleted entry in version control systems. - pub version_control_deleted: Hsla, - /// Represents a modified entry in version control systems. - pub version_control_modified: Hsla, - /// Represents a renamed entry in version control systems. - pub version_control_renamed: Hsla, - /// Represents a conflicting entry in version control systems. - pub version_control_conflict: Hsla, - /// Represents an ignored entry in version control systems. - pub version_control_ignored: Hsla, - /// Represents an added word in a word diff. - pub version_control_word_added: Hsla, - /// Represents a deleted word in a word diff. - pub version_control_word_deleted: Hsla, - /// Represents the "ours" region of a merge conflict. - pub version_control_conflict_marker_ours: Hsla, - /// Represents the "theirs" region of a merge conflict. - pub version_control_conflict_marker_theirs: Hsla, -} - -#[derive(EnumIter, Debug, Clone, Copy, AsRefStr)] -#[strum(serialize_all = "snake_case")] -pub enum ThemeColorField { - Border, - BorderVariant, - BorderFocused, - BorderSelected, - BorderTransparent, - BorderDisabled, - ElevatedSurfaceBackground, - SurfaceBackground, - Background, - ElementBackground, - ElementHover, - ElementActive, - ElementSelected, - ElementDisabled, - DropTargetBackground, - DropTargetBorder, - GhostElementBackground, - GhostElementHover, - GhostElementActive, - GhostElementSelected, - GhostElementDisabled, - Text, - TextMuted, - TextPlaceholder, - TextDisabled, - TextAccent, - Icon, - IconMuted, - IconDisabled, - IconPlaceholder, - IconAccent, - StatusBarBackground, - TitleBarBackground, - TitleBarInactiveBackground, - ToolbarBackground, - TabBarBackground, - TabInactiveBackground, - TabActiveBackground, - SearchMatchBackground, - SearchActiveMatchBackground, - PanelBackground, - PanelFocusedBorder, - PanelIndentGuide, - PanelIndentGuideHover, - PanelIndentGuideActive, - PanelOverlayBackground, - PanelOverlayHover, - PaneFocusedBorder, - PaneGroupBorder, - ScrollbarThumbBackground, - ScrollbarThumbHoverBackground, - ScrollbarThumbActiveBackground, - ScrollbarThumbBorder, - ScrollbarTrackBackground, - ScrollbarTrackBorder, - MinimapThumbBackground, - MinimapThumbHoverBackground, - MinimapThumbActiveBackground, - MinimapThumbBorder, - EditorForeground, - EditorBackground, - EditorGutterBackground, - EditorSubheaderBackground, - EditorActiveLineBackground, - EditorHighlightedLineBackground, - EditorLineNumber, - EditorActiveLineNumber, - EditorInvisible, - EditorWrapGuide, - EditorActiveWrapGuide, - EditorIndentGuide, - EditorIndentGuideActive, - EditorDocumentHighlightReadBackground, - EditorDocumentHighlightWriteBackground, - EditorDocumentHighlightBracketBackground, - TerminalBackground, - TerminalForeground, - TerminalBrightForeground, - TerminalDimForeground, - TerminalAnsiBackground, - TerminalAnsiBlack, - TerminalAnsiBrightBlack, - TerminalAnsiDimBlack, - TerminalAnsiRed, - TerminalAnsiBrightRed, - TerminalAnsiDimRed, - TerminalAnsiGreen, - TerminalAnsiBrightGreen, - TerminalAnsiDimGreen, - TerminalAnsiYellow, - TerminalAnsiBrightYellow, - TerminalAnsiDimYellow, - TerminalAnsiBlue, - TerminalAnsiBrightBlue, - TerminalAnsiDimBlue, - TerminalAnsiMagenta, - TerminalAnsiBrightMagenta, - TerminalAnsiDimMagenta, - TerminalAnsiCyan, - TerminalAnsiBrightCyan, - TerminalAnsiDimCyan, - TerminalAnsiWhite, - TerminalAnsiBrightWhite, - TerminalAnsiDimWhite, - LinkTextHover, - VersionControlAdded, - VersionControlDeleted, - VersionControlModified, - VersionControlRenamed, - VersionControlConflict, - VersionControlIgnored, -} - -impl ThemeColors { - pub fn color(&self, field: ThemeColorField) -> Hsla { - match field { - ThemeColorField::Border => self.border, - ThemeColorField::BorderVariant => self.border_variant, - ThemeColorField::BorderFocused => self.border_focused, - ThemeColorField::BorderSelected => self.border_selected, - ThemeColorField::BorderTransparent => self.border_transparent, - ThemeColorField::BorderDisabled => self.border_disabled, - ThemeColorField::ElevatedSurfaceBackground => self.elevated_surface_background, - ThemeColorField::SurfaceBackground => self.surface_background, - ThemeColorField::Background => self.background, - ThemeColorField::ElementBackground => self.element_background, - ThemeColorField::ElementHover => self.element_hover, - ThemeColorField::ElementActive => self.element_active, - ThemeColorField::ElementSelected => self.element_selected, - ThemeColorField::ElementDisabled => self.element_disabled, - ThemeColorField::DropTargetBackground => self.drop_target_background, - ThemeColorField::DropTargetBorder => self.drop_target_border, - ThemeColorField::GhostElementBackground => self.ghost_element_background, - ThemeColorField::GhostElementHover => self.ghost_element_hover, - ThemeColorField::GhostElementActive => self.ghost_element_active, - ThemeColorField::GhostElementSelected => self.ghost_element_selected, - ThemeColorField::GhostElementDisabled => self.ghost_element_disabled, - ThemeColorField::Text => self.text, - ThemeColorField::TextMuted => self.text_muted, - ThemeColorField::TextPlaceholder => self.text_placeholder, - ThemeColorField::TextDisabled => self.text_disabled, - ThemeColorField::TextAccent => self.text_accent, - ThemeColorField::Icon => self.icon, - ThemeColorField::IconMuted => self.icon_muted, - ThemeColorField::IconDisabled => self.icon_disabled, - ThemeColorField::IconPlaceholder => self.icon_placeholder, - ThemeColorField::IconAccent => self.icon_accent, - ThemeColorField::StatusBarBackground => self.status_bar_background, - ThemeColorField::TitleBarBackground => self.title_bar_background, - ThemeColorField::TitleBarInactiveBackground => self.title_bar_inactive_background, - ThemeColorField::ToolbarBackground => self.toolbar_background, - ThemeColorField::TabBarBackground => self.tab_bar_background, - ThemeColorField::TabInactiveBackground => self.tab_inactive_background, - ThemeColorField::TabActiveBackground => self.tab_active_background, - ThemeColorField::SearchMatchBackground => self.search_match_background, - ThemeColorField::SearchActiveMatchBackground => self.search_active_match_background, - ThemeColorField::PanelBackground => self.panel_background, - ThemeColorField::PanelFocusedBorder => self.panel_focused_border, - ThemeColorField::PanelIndentGuide => self.panel_indent_guide, - ThemeColorField::PanelIndentGuideHover => self.panel_indent_guide_hover, - ThemeColorField::PanelIndentGuideActive => self.panel_indent_guide_active, - ThemeColorField::PanelOverlayBackground => self.panel_overlay_background, - ThemeColorField::PanelOverlayHover => self.panel_overlay_hover, - ThemeColorField::PaneFocusedBorder => self.pane_focused_border, - ThemeColorField::PaneGroupBorder => self.pane_group_border, - ThemeColorField::ScrollbarThumbBackground => self.scrollbar_thumb_background, - ThemeColorField::ScrollbarThumbHoverBackground => self.scrollbar_thumb_hover_background, - ThemeColorField::ScrollbarThumbActiveBackground => { - self.scrollbar_thumb_active_background - } - ThemeColorField::ScrollbarThumbBorder => self.scrollbar_thumb_border, - ThemeColorField::ScrollbarTrackBackground => self.scrollbar_track_background, - ThemeColorField::ScrollbarTrackBorder => self.scrollbar_track_border, - ThemeColorField::MinimapThumbBackground => self.minimap_thumb_background, - ThemeColorField::MinimapThumbHoverBackground => self.minimap_thumb_hover_background, - ThemeColorField::MinimapThumbActiveBackground => self.minimap_thumb_active_background, - ThemeColorField::MinimapThumbBorder => self.minimap_thumb_border, - ThemeColorField::EditorForeground => self.editor_foreground, - ThemeColorField::EditorBackground => self.editor_background, - ThemeColorField::EditorGutterBackground => self.editor_gutter_background, - ThemeColorField::EditorSubheaderBackground => self.editor_subheader_background, - ThemeColorField::EditorActiveLineBackground => self.editor_active_line_background, - ThemeColorField::EditorHighlightedLineBackground => { - self.editor_highlighted_line_background - } - ThemeColorField::EditorLineNumber => self.editor_line_number, - ThemeColorField::EditorActiveLineNumber => self.editor_active_line_number, - ThemeColorField::EditorInvisible => self.editor_invisible, - ThemeColorField::EditorWrapGuide => self.editor_wrap_guide, - ThemeColorField::EditorActiveWrapGuide => self.editor_active_wrap_guide, - ThemeColorField::EditorIndentGuide => self.editor_indent_guide, - ThemeColorField::EditorIndentGuideActive => self.editor_indent_guide_active, - ThemeColorField::EditorDocumentHighlightReadBackground => { - self.editor_document_highlight_read_background - } - ThemeColorField::EditorDocumentHighlightWriteBackground => { - self.editor_document_highlight_write_background - } - ThemeColorField::EditorDocumentHighlightBracketBackground => { - self.editor_document_highlight_bracket_background - } - ThemeColorField::TerminalBackground => self.terminal_background, - ThemeColorField::TerminalForeground => self.terminal_foreground, - ThemeColorField::TerminalBrightForeground => self.terminal_bright_foreground, - ThemeColorField::TerminalDimForeground => self.terminal_dim_foreground, - ThemeColorField::TerminalAnsiBackground => self.terminal_ansi_background, - ThemeColorField::TerminalAnsiBlack => self.terminal_ansi_black, - ThemeColorField::TerminalAnsiBrightBlack => self.terminal_ansi_bright_black, - ThemeColorField::TerminalAnsiDimBlack => self.terminal_ansi_dim_black, - ThemeColorField::TerminalAnsiRed => self.terminal_ansi_red, - ThemeColorField::TerminalAnsiBrightRed => self.terminal_ansi_bright_red, - ThemeColorField::TerminalAnsiDimRed => self.terminal_ansi_dim_red, - ThemeColorField::TerminalAnsiGreen => self.terminal_ansi_green, - ThemeColorField::TerminalAnsiBrightGreen => self.terminal_ansi_bright_green, - ThemeColorField::TerminalAnsiDimGreen => self.terminal_ansi_dim_green, - ThemeColorField::TerminalAnsiYellow => self.terminal_ansi_yellow, - ThemeColorField::TerminalAnsiBrightYellow => self.terminal_ansi_bright_yellow, - ThemeColorField::TerminalAnsiDimYellow => self.terminal_ansi_dim_yellow, - ThemeColorField::TerminalAnsiBlue => self.terminal_ansi_blue, - ThemeColorField::TerminalAnsiBrightBlue => self.terminal_ansi_bright_blue, - ThemeColorField::TerminalAnsiDimBlue => self.terminal_ansi_dim_blue, - ThemeColorField::TerminalAnsiMagenta => self.terminal_ansi_magenta, - ThemeColorField::TerminalAnsiBrightMagenta => self.terminal_ansi_bright_magenta, - ThemeColorField::TerminalAnsiDimMagenta => self.terminal_ansi_dim_magenta, - ThemeColorField::TerminalAnsiCyan => self.terminal_ansi_cyan, - ThemeColorField::TerminalAnsiBrightCyan => self.terminal_ansi_bright_cyan, - ThemeColorField::TerminalAnsiDimCyan => self.terminal_ansi_dim_cyan, - ThemeColorField::TerminalAnsiWhite => self.terminal_ansi_white, - ThemeColorField::TerminalAnsiBrightWhite => self.terminal_ansi_bright_white, - ThemeColorField::TerminalAnsiDimWhite => self.terminal_ansi_dim_white, - ThemeColorField::LinkTextHover => self.link_text_hover, - ThemeColorField::VersionControlAdded => self.version_control_added, - ThemeColorField::VersionControlDeleted => self.version_control_deleted, - ThemeColorField::VersionControlModified => self.version_control_modified, - ThemeColorField::VersionControlRenamed => self.version_control_renamed, - ThemeColorField::VersionControlConflict => self.version_control_conflict, - ThemeColorField::VersionControlIgnored => self.version_control_ignored, - } - } - - pub fn iter(&self) -> impl Iterator + '_ { - ThemeColorField::iter().map(move |field| (field, self.color(field))) - } - - pub fn to_vec(&self) -> Vec<(ThemeColorField, Hsla)> { - self.iter().collect() - } -} - -pub fn all_theme_colors(cx: &mut App) -> Vec<(Hsla, SharedString)> { - let theme = cx.theme(); - ThemeColorField::iter() - .map(|field| { - let color = theme.colors().color(field); - let name = field.as_ref().to_string(); - (color, SharedString::from(name)) - }) - .collect() -} - -#[derive(Refineable, Clone, Debug, PartialEq)] -pub struct ThemeStyles { - /// The background appearance of the window. - pub window_background_appearance: WindowBackgroundAppearance, - pub system: SystemColors, - /// An array of colors used for theme elements that iterate through a series of colors. - /// - /// Example: Player colors, rainbow brackets and indent guides, etc. - pub accents: AccentColors, - - #[refineable] - pub colors: ThemeColors, - - #[refineable] - pub status: StatusColors, - - pub player: PlayerColors, - - pub syntax: Arc, -} - -#[cfg(test)] -mod tests { - use serde_json::json; - - use super::*; - - #[test] - fn override_a_single_theme_color() { - let mut colors = ThemeColors::light(); - - let magenta: Hsla = gpui::rgb(0xff00ff).into(); - - assert_ne!(colors.text, magenta); - - let overrides = ThemeColorsRefinement { - text: Some(magenta), - ..Default::default() - }; - - colors.refine(&overrides); - - assert_eq!(colors.text, magenta); - } - - #[test] - fn override_multiple_theme_colors() { - let mut colors = ThemeColors::light(); - - let magenta: Hsla = gpui::rgb(0xff00ff).into(); - let green: Hsla = gpui::rgb(0x00ff00).into(); - - assert_ne!(colors.text, magenta); - assert_ne!(colors.background, green); - - let overrides = ThemeColorsRefinement { - text: Some(magenta), - background: Some(green), - ..Default::default() - }; - - colors.refine(&overrides); - - assert_eq!(colors.text, magenta); - assert_eq!(colors.background, green); - } - - #[test] - fn deserialize_theme_colors_refinement_from_json() { - let colors: ThemeColorsRefinement = serde_json::from_value(json!({ - "background": "#ff00ff", - "text": "#ff0000" - })) - .unwrap(); - - assert_eq!(colors.background, Some(gpui::rgb(0xff00ff).into())); - assert_eq!(colors.text, Some(gpui::rgb(0xff0000).into())); - } -} diff --git a/crates/theme/src/styles/players.rs b/crates/theme/src/styles/players.rs deleted file mode 100644 index 439dbdd437..0000000000 --- a/crates/theme/src/styles/players.rs +++ /dev/null @@ -1,187 +0,0 @@ -#![allow(missing_docs)] - -use gpui::Hsla; -use serde::Deserialize; - -use crate::{amber, blue, jade, lime, orange, pink, purple, red, try_parse_color}; - -#[derive(Debug, Clone, Copy, Deserialize, Default, PartialEq)] -pub struct PlayerColor { - pub cursor: Hsla, - pub background: Hsla, - pub selection: Hsla, -} - -/// A collection of colors that are used to color players in the editor. -/// -/// The first color is always the local player's color, usually a blue. -/// -/// The rest of the default colors crisscross back and forth on the -/// color wheel so that the colors are as distinct as possible. -#[derive(Clone, Debug, Deserialize, PartialEq)] -pub struct PlayerColors(pub Vec); - -impl Default for PlayerColors { - /// Don't use this! - /// We have to have a default to be `[refineable::Refinable]`. - /// TODO "Find a way to not need this for Refinable" - fn default() -> Self { - Self::dark() - } -} - -impl PlayerColors { - pub fn dark() -> Self { - Self(vec![ - PlayerColor { - cursor: blue().dark().step_9(), - background: blue().dark().step_5(), - selection: blue().dark().step_3(), - }, - PlayerColor { - cursor: orange().dark().step_9(), - background: orange().dark().step_5(), - selection: orange().dark().step_3(), - }, - PlayerColor { - cursor: pink().dark().step_9(), - background: pink().dark().step_5(), - selection: pink().dark().step_3(), - }, - PlayerColor { - cursor: lime().dark().step_9(), - background: lime().dark().step_5(), - selection: lime().dark().step_3(), - }, - PlayerColor { - cursor: purple().dark().step_9(), - background: purple().dark().step_5(), - selection: purple().dark().step_3(), - }, - PlayerColor { - cursor: amber().dark().step_9(), - background: amber().dark().step_5(), - selection: amber().dark().step_3(), - }, - PlayerColor { - cursor: jade().dark().step_9(), - background: jade().dark().step_5(), - selection: jade().dark().step_3(), - }, - PlayerColor { - cursor: red().dark().step_9(), - background: red().dark().step_5(), - selection: red().dark().step_3(), - }, - ]) - } - - pub fn light() -> Self { - Self(vec![ - PlayerColor { - cursor: blue().light().step_9(), - background: blue().light().step_4(), - selection: blue().light().step_3(), - }, - PlayerColor { - cursor: orange().light().step_9(), - background: orange().light().step_4(), - selection: orange().light().step_3(), - }, - PlayerColor { - cursor: pink().light().step_9(), - background: pink().light().step_4(), - selection: pink().light().step_3(), - }, - PlayerColor { - cursor: lime().light().step_9(), - background: lime().light().step_4(), - selection: lime().light().step_3(), - }, - PlayerColor { - cursor: purple().light().step_9(), - background: purple().light().step_4(), - selection: purple().light().step_3(), - }, - PlayerColor { - cursor: amber().light().step_9(), - background: amber().light().step_4(), - selection: amber().light().step_3(), - }, - PlayerColor { - cursor: jade().light().step_9(), - background: jade().light().step_4(), - selection: jade().light().step_3(), - }, - PlayerColor { - cursor: red().light().step_9(), - background: red().light().step_4(), - selection: red().light().step_3(), - }, - ]) - } -} - -impl PlayerColors { - pub fn local(&self) -> PlayerColor { - *self.0.first().unwrap() - } - - pub fn agent(&self) -> PlayerColor { - *self.0.last().unwrap() - } - - pub fn absent(&self) -> PlayerColor { - *self.0.last().unwrap() - } - - pub fn read_only(&self) -> PlayerColor { - let local = self.local(); - PlayerColor { - cursor: local.cursor.grayscale(), - background: local.background.grayscale(), - selection: local.selection.grayscale(), - } - } - - pub fn color_for_participant(&self, participant_index: u32) -> PlayerColor { - let len = self.0.len() - 1; - self.0[(participant_index as usize % len) + 1] - } - - /// Merges the given player colors into this [`PlayerColors`] instance. - pub fn merge(&mut self, user_player_colors: &[settings::PlayerColorContent]) { - if user_player_colors.is_empty() { - return; - } - - for (idx, player) in user_player_colors.iter().enumerate() { - let cursor = player - .cursor - .as_ref() - .and_then(|color| try_parse_color(color).ok()); - let background = player - .background - .as_ref() - .and_then(|color| try_parse_color(color).ok()); - let selection = player - .selection - .as_ref() - .and_then(|color| try_parse_color(color).ok()); - - if let Some(player_color) = self.0.get_mut(idx) { - *player_color = PlayerColor { - cursor: cursor.unwrap_or(player_color.cursor), - background: background.unwrap_or(player_color.background), - selection: selection.unwrap_or(player_color.selection), - }; - } else { - self.0.push(PlayerColor { - cursor: cursor.unwrap_or_default(), - background: background.unwrap_or_default(), - selection: selection.unwrap_or_default(), - }); - } - } - } -} diff --git a/crates/theme/src/styles/status.rs b/crates/theme/src/styles/status.rs deleted file mode 100644 index ab333ecb3e..0000000000 --- a/crates/theme/src/styles/status.rs +++ /dev/null @@ -1,191 +0,0 @@ -#![allow(missing_docs)] - -use gpui::Hsla; -use refineable::Refineable; - -use crate::{blue, grass, neutral, red, yellow}; - -#[derive(Refineable, Clone, Debug, PartialEq)] -#[refineable(Debug, serde::Deserialize)] -pub struct StatusColors { - /// Indicates some kind of conflict, like a file changed on disk while it was open, or - /// merge conflicts in a Git repository. - pub conflict: Hsla, - pub conflict_background: Hsla, - pub conflict_border: Hsla, - - /// Indicates something new, like a new file added to a Git repository. - pub created: Hsla, - pub created_background: Hsla, - pub created_border: Hsla, - - /// Indicates that something no longer exists, like a deleted file. - pub deleted: Hsla, - pub deleted_background: Hsla, - pub deleted_border: Hsla, - - /// Indicates a system error, a failed operation or a diagnostic error. - pub error: Hsla, - pub error_background: Hsla, - pub error_border: Hsla, - - /// Represents a hidden status, such as a file being hidden in a file tree. - pub hidden: Hsla, - pub hidden_background: Hsla, - pub hidden_border: Hsla, - - /// Indicates a hint or some kind of additional information. - pub hint: Hsla, - pub hint_background: Hsla, - pub hint_border: Hsla, - - /// Indicates that something is deliberately ignored, such as a file or operation ignored by Git. - pub ignored: Hsla, - pub ignored_background: Hsla, - pub ignored_border: Hsla, - - /// Represents informational status updates or messages. - pub info: Hsla, - pub info_background: Hsla, - pub info_border: Hsla, - - /// Indicates a changed or altered status, like a file that has been edited. - pub modified: Hsla, - pub modified_background: Hsla, - pub modified_border: Hsla, - - /// Indicates something that is predicted, like automatic code completion, or generated code. - pub predictive: Hsla, - pub predictive_background: Hsla, - pub predictive_border: Hsla, - - /// Represents a renamed status, such as a file that has been renamed. - pub renamed: Hsla, - pub renamed_background: Hsla, - pub renamed_border: Hsla, - - /// Indicates a successful operation or task completion. - pub success: Hsla, - pub success_background: Hsla, - pub success_border: Hsla, - - /// Indicates some kind of unreachable status, like a block of code that can never be reached. - pub unreachable: Hsla, - pub unreachable_background: Hsla, - pub unreachable_border: Hsla, - - /// Represents a warning status, like an operation that is about to fail. - pub warning: Hsla, - pub warning_background: Hsla, - pub warning_border: Hsla, -} - -pub struct DiagnosticColors { - pub error: Hsla, - pub warning: Hsla, - pub info: Hsla, -} - -impl StatusColors { - pub fn dark() -> Self { - Self { - conflict: red().dark().step_9(), - conflict_background: red().dark().step_9(), - conflict_border: red().dark().step_9(), - created: grass().dark().step_9(), - created_background: grass().dark().step_9().opacity(0.25), - created_border: grass().dark().step_9(), - deleted: red().dark().step_9(), - deleted_background: red().dark().step_9().opacity(0.25), - deleted_border: red().dark().step_9(), - error: red().dark().step_9(), - error_background: red().dark().step_9(), - error_border: red().dark().step_9(), - hidden: neutral().dark().step_9(), - hidden_background: neutral().dark().step_9(), - hidden_border: neutral().dark().step_9(), - hint: blue().dark().step_9(), - hint_background: blue().dark().step_9(), - hint_border: blue().dark().step_9(), - ignored: neutral().dark().step_9(), - ignored_background: neutral().dark().step_9(), - ignored_border: neutral().dark().step_9(), - info: blue().dark().step_9(), - info_background: blue().dark().step_9(), - info_border: blue().dark().step_9(), - modified: yellow().dark().step_9(), - modified_background: yellow().dark().step_9().opacity(0.25), - modified_border: yellow().dark().step_9(), - predictive: neutral().dark_alpha().step_9(), - predictive_background: neutral().dark_alpha().step_9(), - predictive_border: neutral().dark_alpha().step_9(), - renamed: blue().dark().step_9(), - renamed_background: blue().dark().step_9(), - renamed_border: blue().dark().step_9(), - success: grass().dark().step_9(), - success_background: grass().dark().step_9(), - success_border: grass().dark().step_9(), - unreachable: neutral().dark().step_10(), - unreachable_background: neutral().dark().step_10(), - unreachable_border: neutral().dark().step_10(), - warning: yellow().dark().step_9(), - warning_background: yellow().dark().step_9(), - warning_border: yellow().dark().step_9(), - } - } - - pub fn light() -> Self { - Self { - conflict: red().light().step_9(), - conflict_background: red().light().step_9(), - conflict_border: red().light().step_9(), - created: grass().light().step_9(), - created_background: grass().light().step_9(), - created_border: grass().light().step_9(), - deleted: red().light().step_9(), - deleted_background: red().light().step_9(), - deleted_border: red().light().step_9(), - error: red().light().step_9(), - error_background: red().light().step_9(), - error_border: red().light().step_9(), - hidden: neutral().light().step_9(), - hidden_background: neutral().light().step_9(), - hidden_border: neutral().light().step_9(), - hint: blue().light().step_9(), - hint_background: blue().light().step_9(), - hint_border: blue().light().step_9(), - ignored: neutral().light().step_9(), - ignored_background: neutral().light().step_9(), - ignored_border: neutral().light().step_9(), - info: blue().light().step_9(), - info_background: blue().light().step_9(), - info_border: blue().light().step_9(), - modified: yellow().light().step_9(), - modified_background: yellow().light().step_9(), - modified_border: yellow().light().step_9(), - predictive: neutral().light_alpha().step_9(), - predictive_background: neutral().light_alpha().step_9(), - predictive_border: neutral().light_alpha().step_9(), - renamed: blue().light().step_9(), - renamed_background: blue().light().step_9(), - renamed_border: blue().light().step_9(), - success: grass().light().step_9(), - success_background: grass().light().step_9(), - success_border: grass().light().step_9(), - unreachable: neutral().light().step_10(), - unreachable_background: neutral().light().step_10(), - unreachable_border: neutral().light().step_10(), - warning: yellow().light().step_9(), - warning_background: yellow().light().step_9(), - warning_border: yellow().light().step_9(), - } - } - - pub fn diagnostic(&self) -> DiagnosticColors { - DiagnosticColors { - error: self.error, - warning: self.warning, - info: self.info, - } - } -} diff --git a/crates/theme/src/styles/syntax.rs b/crates/theme/src/styles/syntax.rs deleted file mode 100644 index 0a97ff77f2..0000000000 --- a/crates/theme/src/styles/syntax.rs +++ /dev/null @@ -1,197 +0,0 @@ -#![allow(missing_docs)] - -use std::sync::Arc; - -use gpui::{HighlightStyle, Hsla}; - -#[derive(Debug, PartialEq, Eq, Clone, Default)] -pub struct SyntaxTheme { - pub highlights: Vec<(String, HighlightStyle)>, -} - -impl SyntaxTheme { - #[cfg(any(test, feature = "test-support"))] - pub fn new_test(colors: impl IntoIterator) -> Self { - Self::new_test_styles(colors.into_iter().map(|(key, color)| { - ( - key, - HighlightStyle { - color: Some(color), - ..Default::default() - }, - ) - })) - } - - #[cfg(any(test, feature = "test-support"))] - pub fn new_test_styles( - colors: impl IntoIterator, - ) -> Self { - Self { - highlights: colors - .into_iter() - .map(|(key, style)| (key.to_owned(), style)) - .collect(), - } - } - - pub fn get(&self, name: &str) -> HighlightStyle { - self.highlights - .iter() - .find_map(|entry| if entry.0 == name { Some(entry.1) } else { None }) - .unwrap_or_default() - } - - pub fn color(&self, name: &str) -> Hsla { - self.get(name).color.unwrap_or_default() - } - - pub fn highlight_id(&self, name: &str) -> Option { - let ix = self.highlights.iter().position(|entry| entry.0 == name)?; - Some(ix as u32) - } - - /// Returns a new [`Arc`] with the given syntax styles merged in. - pub fn merge(base: Arc, user_syntax_styles: Vec<(String, HighlightStyle)>) -> Arc { - if user_syntax_styles.is_empty() { - return base; - } - - let mut merged_highlights = base.highlights.clone(); - - for (name, highlight) in user_syntax_styles { - if let Some((_, existing_highlight)) = merged_highlights - .iter_mut() - .find(|(existing_name, _)| existing_name == &name) - { - existing_highlight.color = highlight.color.or(existing_highlight.color); - existing_highlight.font_weight = - highlight.font_weight.or(existing_highlight.font_weight); - existing_highlight.font_style = - highlight.font_style.or(existing_highlight.font_style); - existing_highlight.background_color = highlight - .background_color - .or(existing_highlight.background_color); - existing_highlight.underline = highlight.underline.or(existing_highlight.underline); - existing_highlight.strikethrough = - highlight.strikethrough.or(existing_highlight.strikethrough); - existing_highlight.fade_out = highlight.fade_out.or(existing_highlight.fade_out); - } else { - merged_highlights.push((name, highlight)); - } - } - - Arc::new(Self { - highlights: merged_highlights, - }) - } -} - -#[cfg(test)] -mod tests { - use gpui::FontStyle; - - use super::*; - - #[test] - fn test_syntax_theme_merge() { - // Merging into an empty `SyntaxTheme` keeps all the user-defined styles. - let syntax_theme = SyntaxTheme::merge( - Arc::new(SyntaxTheme::new_test([])), - vec![ - ( - "foo".to_string(), - HighlightStyle { - color: Some(gpui::red()), - ..Default::default() - }, - ), - ( - "foo.bar".to_string(), - HighlightStyle { - color: Some(gpui::green()), - ..Default::default() - }, - ), - ], - ); - assert_eq!( - syntax_theme, - Arc::new(SyntaxTheme::new_test([ - ("foo", gpui::red()), - ("foo.bar", gpui::green()) - ])) - ); - - // Merging empty user-defined styles keeps all the base styles. - let syntax_theme = SyntaxTheme::merge( - Arc::new(SyntaxTheme::new_test([ - ("foo", gpui::blue()), - ("foo.bar", gpui::red()), - ])), - Vec::new(), - ); - assert_eq!( - syntax_theme, - Arc::new(SyntaxTheme::new_test([ - ("foo", gpui::blue()), - ("foo.bar", gpui::red()) - ])) - ); - - let syntax_theme = SyntaxTheme::merge( - Arc::new(SyntaxTheme::new_test([ - ("foo", gpui::red()), - ("foo.bar", gpui::green()), - ])), - vec![( - "foo.bar".to_string(), - HighlightStyle { - color: Some(gpui::yellow()), - ..Default::default() - }, - )], - ); - assert_eq!( - syntax_theme, - Arc::new(SyntaxTheme::new_test([ - ("foo", gpui::red()), - ("foo.bar", gpui::yellow()) - ])) - ); - - let syntax_theme = SyntaxTheme::merge( - Arc::new(SyntaxTheme::new_test([ - ("foo", gpui::red()), - ("foo.bar", gpui::green()), - ])), - vec![( - "foo.bar".to_string(), - HighlightStyle { - font_style: Some(FontStyle::Italic), - ..Default::default() - }, - )], - ); - assert_eq!( - syntax_theme, - Arc::new(SyntaxTheme::new_test_styles([ - ( - "foo", - HighlightStyle { - color: Some(gpui::red()), - ..Default::default() - } - ), - ( - "foo.bar", - HighlightStyle { - color: Some(gpui::green()), - font_style: Some(FontStyle::Italic), - ..Default::default() - } - ) - ])) - ); - } -} diff --git a/crates/theme/src/styles/system.rs b/crates/theme/src/styles/system.rs deleted file mode 100644 index 676577bfb4..0000000000 --- a/crates/theme/src/styles/system.rs +++ /dev/null @@ -1,22 +0,0 @@ -#![allow(missing_docs)] - -use gpui::{Hsla, hsla}; - -#[derive(Clone, Debug, PartialEq)] -pub struct SystemColors { - pub transparent: Hsla, - pub mac_os_traffic_light_red: Hsla, - pub mac_os_traffic_light_yellow: Hsla, - pub mac_os_traffic_light_green: Hsla, -} - -impl Default for SystemColors { - fn default() -> Self { - Self { - transparent: hsla(0.0, 0.0, 0.0, 0.0), - mac_os_traffic_light_red: hsla(0.0139, 0.79, 0.65, 1.0), - mac_os_traffic_light_yellow: hsla(0.114, 0.88, 0.63, 1.0), - mac_os_traffic_light_green: hsla(0.313, 0.49, 0.55, 1.0), - } - } -} diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs deleted file mode 100644 index c94e0d60bf..0000000000 --- a/crates/theme/src/theme.rs +++ /dev/null @@ -1,527 +0,0 @@ -#![deny(missing_docs)] - -//! # Theme -//! -//! This crate provides the theme system for Zed. -//! -//! ## Overview -//! -//! A theme is a collection of colors used to build a consistent appearance for UI components across the application. - -mod default_colors; -mod fallback_themes; -mod font_family_cache; -mod icon_theme; -mod icon_theme_schema; -mod registry; -mod scale; -mod schema; -mod settings; -mod styles; - -use std::path::Path; -use std::sync::Arc; - -use ::settings::Settings; -use ::settings::SettingsStore; -use anyhow::Result; -use fallback_themes::apply_status_color_defaults; -use fs::Fs; -use gpui::BorrowAppContext; -use gpui::Global; -use gpui::{ - App, AssetSource, HighlightStyle, Hsla, Pixels, Refineable, SharedString, WindowAppearance, - WindowBackgroundAppearance, px, -}; -use serde::Deserialize; -use uuid::Uuid; - -pub use crate::default_colors::*; -use crate::fallback_themes::apply_theme_color_defaults; -pub use crate::font_family_cache::*; -pub use crate::icon_theme::*; -pub use crate::icon_theme_schema::*; -pub use crate::registry::*; -pub use crate::scale::*; -pub use crate::schema::*; -pub use crate::settings::*; -pub use crate::styles::*; -pub use ::settings::{ - FontStyleContent, HighlightStyleContent, StatusColorsContent, ThemeColorsContent, - ThemeStyleContent, -}; - -/// Defines window border radius for platforms that use client side decorations. -pub const CLIENT_SIDE_DECORATION_ROUNDING: Pixels = px(10.0); -/// Defines window shadow size for platforms that use client side decorations. -pub const CLIENT_SIDE_DECORATION_SHADOW: Pixels = px(10.0); - -/// The appearance of the theme. -#[derive(Debug, PartialEq, Clone, Copy, Deserialize)] -pub enum Appearance { - /// A light appearance. - Light, - /// A dark appearance. - Dark, -} - -impl Appearance { - /// Returns whether the appearance is light. - pub fn is_light(&self) -> bool { - match self { - Self::Light => true, - Self::Dark => false, - } - } -} - -impl From for Appearance { - fn from(value: WindowAppearance) -> Self { - match value { - WindowAppearance::Dark | WindowAppearance::VibrantDark => Self::Dark, - WindowAppearance::Light | WindowAppearance::VibrantLight => Self::Light, - } - } -} - -impl From for ThemeAppearanceMode { - fn from(value: Appearance) -> Self { - match value { - Appearance::Light => Self::Light, - Appearance::Dark => Self::Dark, - } - } -} - -/// Which themes should be loaded. This is used primarily for testing. -pub enum LoadThemes { - /// Only load the base theme. - /// - /// No user themes will be loaded. - JustBase, - - /// Load all of the built-in themes. - All(Box), -} - -/// Initialize the theme system. -pub fn init(themes_to_load: LoadThemes, cx: &mut App) { - SystemAppearance::init(cx); - let (assets, load_user_themes) = match themes_to_load { - LoadThemes::JustBase => (Box::new(()) as Box, false), - LoadThemes::All(assets) => (assets, true), - }; - ThemeRegistry::set_global(assets, cx); - - if load_user_themes { - ThemeRegistry::global(cx).load_bundled_themes(); - } - - FontFamilyCache::init_global(cx); - - let theme = GlobalTheme::configured_theme(cx); - let icon_theme = GlobalTheme::configured_icon_theme(cx); - cx.set_global(GlobalTheme { theme, icon_theme }); - - let settings = ThemeSettings::get_global(cx); - - let mut prev_buffer_font_size_settings = settings.buffer_font_size_settings(); - let mut prev_ui_font_size_settings = settings.ui_font_size_settings(); - let mut prev_agent_ui_font_size_settings = settings.agent_ui_font_size_settings(); - let mut prev_agent_buffer_font_size_settings = settings.agent_buffer_font_size_settings(); - let mut prev_theme_name = settings.theme.name(SystemAppearance::global(cx).0); - let mut prev_icon_theme_name = settings.icon_theme.name(SystemAppearance::global(cx).0); - let mut prev_theme_overrides = ( - settings.experimental_theme_overrides.clone(), - settings.theme_overrides.clone(), - ); - - cx.observe_global::(move |cx| { - let settings = ThemeSettings::get_global(cx); - - let buffer_font_size_settings = settings.buffer_font_size_settings(); - let ui_font_size_settings = settings.ui_font_size_settings(); - let agent_ui_font_size_settings = settings.agent_ui_font_size_settings(); - let agent_buffer_font_size_settings = settings.agent_buffer_font_size_settings(); - let theme_name = settings.theme.name(SystemAppearance::global(cx).0); - let icon_theme_name = settings.icon_theme.name(SystemAppearance::global(cx).0); - let theme_overrides = ( - settings.experimental_theme_overrides.clone(), - settings.theme_overrides.clone(), - ); - - if buffer_font_size_settings != prev_buffer_font_size_settings { - prev_buffer_font_size_settings = buffer_font_size_settings; - reset_buffer_font_size(cx); - } - - if ui_font_size_settings != prev_ui_font_size_settings { - prev_ui_font_size_settings = ui_font_size_settings; - reset_ui_font_size(cx); - } - - if agent_ui_font_size_settings != prev_agent_ui_font_size_settings { - prev_agent_ui_font_size_settings = agent_ui_font_size_settings; - reset_agent_ui_font_size(cx); - } - - if agent_buffer_font_size_settings != prev_agent_buffer_font_size_settings { - prev_agent_buffer_font_size_settings = agent_buffer_font_size_settings; - reset_agent_buffer_font_size(cx); - } - - if theme_name != prev_theme_name || theme_overrides != prev_theme_overrides { - prev_theme_name = theme_name; - prev_theme_overrides = theme_overrides; - GlobalTheme::reload_theme(cx); - } - - if icon_theme_name != prev_icon_theme_name { - prev_icon_theme_name = icon_theme_name; - GlobalTheme::reload_icon_theme(cx); - } - }) - .detach(); -} - -/// Implementing this trait allows accessing the active theme. -pub trait ActiveTheme { - /// Returns the active theme. - fn theme(&self) -> &Arc; -} - -impl ActiveTheme for App { - fn theme(&self) -> &Arc { - GlobalTheme::theme(self) - } -} - -/// A theme family is a grouping of themes under a single name. -/// -/// For example, the "One" theme family contains the "One Light" and "One Dark" themes. -/// -/// It can also be used to package themes with many variants. -/// -/// For example, the "Atelier" theme family contains "Cave", "Dune", "Estuary", "Forest", "Heath", etc. -pub struct ThemeFamily { - /// The unique identifier for the theme family. - pub id: String, - /// The name of the theme family. This will be displayed in the UI, such as when adding or removing a theme family. - pub name: SharedString, - /// The author of the theme family. - pub author: SharedString, - /// The [Theme]s in the family. - pub themes: Vec, - /// The color scales used by the themes in the family. - /// Note: This will be removed in the future. - pub scales: ColorScales, -} - -impl ThemeFamily { - // This is on ThemeFamily because we will have variables here we will need - // in the future to resolve @references. - /// Refines ThemeContent into a theme, merging it's contents with the base theme. - pub fn refine_theme(&self, theme: &ThemeContent) -> Theme { - let appearance = match theme.appearance { - AppearanceContent::Light => Appearance::Light, - AppearanceContent::Dark => Appearance::Dark, - }; - - let mut refined_status_colors = match theme.appearance { - AppearanceContent::Light => StatusColors::light(), - AppearanceContent::Dark => StatusColors::dark(), - }; - let mut status_colors_refinement = status_colors_refinement(&theme.style.status); - apply_status_color_defaults(&mut status_colors_refinement); - refined_status_colors.refine(&status_colors_refinement); - - let mut refined_player_colors = match theme.appearance { - AppearanceContent::Light => PlayerColors::light(), - AppearanceContent::Dark => PlayerColors::dark(), - }; - refined_player_colors.merge(&theme.style.players); - - let mut refined_theme_colors = match theme.appearance { - AppearanceContent::Light => ThemeColors::light(), - AppearanceContent::Dark => ThemeColors::dark(), - }; - let mut theme_colors_refinement = - theme_colors_refinement(&theme.style.colors, &status_colors_refinement); - apply_theme_color_defaults(&mut theme_colors_refinement, &refined_player_colors); - refined_theme_colors.refine(&theme_colors_refinement); - - let mut refined_accent_colors = match theme.appearance { - AppearanceContent::Light => AccentColors::light(), - AppearanceContent::Dark => AccentColors::dark(), - }; - refined_accent_colors.merge(&theme.style.accents); - - let syntax_highlights = theme - .style - .syntax - .iter() - .map(|(syntax_token, highlight)| { - ( - syntax_token.clone(), - HighlightStyle { - color: highlight - .color - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - background_color: highlight - .background_color - .as_ref() - .and_then(|color| try_parse_color(color).ok()), - font_style: highlight.font_style.map(Into::into), - font_weight: highlight.font_weight.map(Into::into), - ..Default::default() - }, - ) - }) - .collect::>(); - let syntax_theme = SyntaxTheme::merge(Arc::new(SyntaxTheme::default()), syntax_highlights); - - let window_background_appearance = theme - .style - .window_background_appearance - .map(Into::into) - .unwrap_or_default(); - - Theme { - id: uuid::Uuid::new_v4().to_string(), - name: theme.name.clone().into(), - appearance, - styles: ThemeStyles { - system: SystemColors::default(), - window_background_appearance, - accents: refined_accent_colors, - colors: refined_theme_colors, - status: refined_status_colors, - player: refined_player_colors, - syntax: syntax_theme, - }, - } - } -} - -/// Refines a [ThemeFamilyContent] and it's [ThemeContent]s into a [ThemeFamily]. -pub fn refine_theme_family(theme_family_content: ThemeFamilyContent) -> ThemeFamily { - let id = Uuid::new_v4().to_string(); - let name = theme_family_content.name.clone(); - let author = theme_family_content.author.clone(); - - let mut theme_family = ThemeFamily { - id, - name: name.into(), - author: author.into(), - themes: vec![], - scales: default_color_scales(), - }; - - let refined_themes = theme_family_content - .themes - .iter() - .map(|theme_content| theme_family.refine_theme(theme_content)) - .collect(); - - theme_family.themes = refined_themes; - - theme_family -} - -/// A theme is the primary mechanism for defining the appearance of the UI. -#[derive(Clone, Debug, PartialEq)] -pub struct Theme { - /// The unique identifier for the theme. - pub id: String, - /// The name of the theme. - pub name: SharedString, - /// The appearance of the theme (light or dark). - pub appearance: Appearance, - /// The colors and other styles for the theme. - pub styles: ThemeStyles, -} - -impl Theme { - /// Returns the [`SystemColors`] for the theme. - #[inline(always)] - pub fn system(&self) -> &SystemColors { - &self.styles.system - } - - /// Returns the [`AccentColors`] for the theme. - #[inline(always)] - pub fn accents(&self) -> &AccentColors { - &self.styles.accents - } - - /// Returns the [`PlayerColors`] for the theme. - #[inline(always)] - pub fn players(&self) -> &PlayerColors { - &self.styles.player - } - - /// Returns the [`ThemeColors`] for the theme. - #[inline(always)] - pub fn colors(&self) -> &ThemeColors { - &self.styles.colors - } - - /// Returns the [`SyntaxTheme`] for the theme. - #[inline(always)] - pub fn syntax(&self) -> &Arc { - &self.styles.syntax - } - - /// Returns the [`StatusColors`] for the theme. - #[inline(always)] - pub fn status(&self) -> &StatusColors { - &self.styles.status - } - - /// Returns the color for the syntax node with the given name. - #[inline(always)] - pub fn syntax_color(&self, name: &str) -> Hsla { - self.syntax().color(name) - } - - /// Returns the [`Appearance`] for the theme. - #[inline(always)] - pub fn appearance(&self) -> Appearance { - self.appearance - } - - /// Returns the [`WindowBackgroundAppearance`] for the theme. - #[inline(always)] - pub fn window_background_appearance(&self) -> WindowBackgroundAppearance { - self.styles.window_background_appearance - } - - /// Darkens the color by reducing its lightness. - /// The resulting lightness is clamped to ensure it doesn't go below 0.0. - /// - /// The first value darkens light appearance mode, the second darkens appearance dark mode. - /// - /// Note: This is a tentative solution and may be replaced with a more robust color system. - pub fn darken(&self, color: Hsla, light_amount: f32, dark_amount: f32) -> Hsla { - let amount = match self.appearance { - Appearance::Light => light_amount, - Appearance::Dark => dark_amount, - }; - let mut hsla = color; - hsla.l = (hsla.l - amount).max(0.0); - hsla - } -} - -/// Asynchronously reads the user theme from the specified path. -pub async fn read_user_theme(theme_path: &Path, fs: Arc) -> Result { - let bytes = fs.load_bytes(theme_path).await?; - let theme_family: ThemeFamilyContent = serde_json_lenient::from_slice(&bytes)?; - - for theme in &theme_family.themes { - if theme - .style - .colors - .deprecated_scrollbar_thumb_background - .is_some() - { - log::warn!( - r#"Theme "{theme_name}" is using a deprecated style property: scrollbar_thumb.background. Use `scrollbar.thumb.background` instead."#, - theme_name = theme.name - ) - } - } - - Ok(theme_family) -} - -/// Asynchronously reads the icon theme from the specified path. -pub async fn read_icon_theme( - icon_theme_path: &Path, - fs: Arc, -) -> Result { - let bytes = fs.load_bytes(icon_theme_path).await?; - let icon_theme_family: IconThemeFamilyContent = serde_json_lenient::from_slice(&bytes)?; - - Ok(icon_theme_family) -} - -/// The active theme -pub struct GlobalTheme { - theme: Arc, - icon_theme: Arc, -} -impl Global for GlobalTheme {} - -impl GlobalTheme { - fn configured_theme(cx: &mut App) -> Arc { - let themes = ThemeRegistry::default_global(cx); - let theme_settings = ThemeSettings::get_global(cx); - let system_appearance = SystemAppearance::global(cx); - - let theme_name = theme_settings.theme.name(*system_appearance); - - let theme = match themes.get(&theme_name.0) { - Ok(theme) => theme, - Err(err) => { - if themes.extensions_loaded() { - log::error!("{err}"); - } - themes - .get(default_theme(*system_appearance)) - // fallback for tests. - .unwrap_or_else(|_| themes.get(DEFAULT_DARK_THEME).unwrap()) - } - }; - theme_settings.apply_theme_overrides(theme) - } - - /// Reloads the current theme. - /// - /// Reads the [`ThemeSettings`] to know which theme should be loaded, - /// taking into account the current [`SystemAppearance`]. - pub fn reload_theme(cx: &mut App) { - let theme = Self::configured_theme(cx); - cx.update_global::(|this, _| this.theme = theme); - cx.refresh_windows(); - } - - fn configured_icon_theme(cx: &mut App) -> Arc { - let themes = ThemeRegistry::default_global(cx); - let theme_settings = ThemeSettings::get_global(cx); - let system_appearance = SystemAppearance::global(cx); - - let icon_theme_name = theme_settings.icon_theme.name(*system_appearance); - - match themes.get_icon_theme(&icon_theme_name.0) { - Ok(theme) => theme, - Err(err) => { - if themes.extensions_loaded() { - log::error!("{err}"); - } - themes.get_icon_theme(DEFAULT_ICON_THEME_NAME).unwrap() - } - } - } - - /// Reloads the current icon theme. - /// - /// Reads the [`ThemeSettings`] to know which icon theme should be loaded, - /// taking into account the current [`SystemAppearance`]. - pub fn reload_icon_theme(cx: &mut App) { - let icon_theme = Self::configured_icon_theme(cx); - cx.update_global::(|this, _| this.icon_theme = icon_theme); - cx.refresh_windows(); - } - - /// the active theme - pub fn theme(cx: &App) -> &Arc { - &cx.global::().theme - } - - /// the active icon theme - pub fn icon_theme(cx: &App) -> &Arc { - &cx.global::().icon_theme - } -} diff --git a/crates/theme_extension/Cargo.toml b/crates/theme_extension/Cargo.toml deleted file mode 100644 index d94e15914b..0000000000 --- a/crates/theme_extension/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "theme_extension" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/theme_extension.rs" - -[dependencies] -anyhow.workspace = true -extension.workspace = true -fs.workspace = true -gpui.workspace = true -theme.workspace = true diff --git a/crates/theme_extension/LICENSE-GPL b/crates/theme_extension/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/theme_extension/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/theme_extension/src/theme_extension.rs b/crates/theme_extension/src/theme_extension.rs deleted file mode 100644 index 10df2349c8..0000000000 --- a/crates/theme_extension/src/theme_extension.rs +++ /dev/null @@ -1,88 +0,0 @@ -use std::path::PathBuf; -use std::sync::Arc; - -use anyhow::Result; -use extension::{ExtensionHostProxy, ExtensionThemeProxy}; -use fs::Fs; -use gpui::{App, BackgroundExecutor, SharedString, Task}; -use theme::{GlobalTheme, ThemeRegistry}; - -pub fn init( - extension_host_proxy: Arc, - theme_registry: Arc, - executor: BackgroundExecutor, -) { - extension_host_proxy.register_theme_proxy(ThemeRegistryProxy { - theme_registry, - executor, - }); -} - -struct ThemeRegistryProxy { - theme_registry: Arc, - executor: BackgroundExecutor, -} - -impl ExtensionThemeProxy for ThemeRegistryProxy { - fn set_extensions_loaded(&self) { - self.theme_registry.set_extensions_loaded(); - } - - fn list_theme_names(&self, theme_path: PathBuf, fs: Arc) -> Task>> { - self.executor.spawn(async move { - let themes = theme::read_user_theme(&theme_path, fs).await?; - Ok(themes.themes.into_iter().map(|theme| theme.name).collect()) - }) - } - - fn remove_user_themes(&self, themes: Vec) { - self.theme_registry.remove_user_themes(&themes); - } - - fn load_user_theme(&self, theme_path: PathBuf, fs: Arc) -> Task> { - let theme_registry = self.theme_registry.clone(); - self.executor - .spawn(async move { theme_registry.load_user_theme(&theme_path, fs).await }) - } - - fn reload_current_theme(&self, cx: &mut App) { - GlobalTheme::reload_theme(cx) - } - - fn list_icon_theme_names( - &self, - icon_theme_path: PathBuf, - fs: Arc, - ) -> Task>> { - self.executor.spawn(async move { - let icon_theme_family = theme::read_icon_theme(&icon_theme_path, fs).await?; - Ok(icon_theme_family - .themes - .into_iter() - .map(|theme| theme.name) - .collect()) - }) - } - - fn remove_icon_themes(&self, icon_themes: Vec) { - self.theme_registry.remove_icon_themes(&icon_themes); - } - - fn load_icon_theme( - &self, - icon_theme_path: PathBuf, - icons_root_dir: PathBuf, - fs: Arc, - ) -> Task> { - let theme_registry = self.theme_registry.clone(); - self.executor.spawn(async move { - theme_registry - .load_icon_theme(&icon_theme_path, &icons_root_dir, fs) - .await - }) - } - - fn reload_current_icon_theme(&self, cx: &mut App) { - GlobalTheme::reload_icon_theme(cx) - } -} diff --git a/crates/theme_importer/Cargo.toml b/crates/theme_importer/Cargo.toml deleted file mode 100644 index a91ffc4454..0000000000 --- a/crates/theme_importer/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "theme_importer" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[dependencies] -anyhow.workspace = true -clap = { workspace = true, features = ["derive"] } -collections.workspace = true -gpui.workspace = true -indexmap.workspace = true -log.workspace = true -palette.workspace = true -serde.workspace = true -serde_json.workspace = true -serde_json_lenient.workspace = true -simplelog.workspace= true -strum = { workspace = true, features = ["derive"] } -theme.workspace = true -vscode_theme = "0.2.0" diff --git a/crates/theme_importer/LICENSE-GPL b/crates/theme_importer/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/theme_importer/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/theme_importer/README.md b/crates/theme_importer/README.md deleted file mode 100644 index 20b7d063ad..0000000000 --- a/crates/theme_importer/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Zed Theme Importer - -```sh -cargo run -p theme_importer -- dark-plus-syntax-color-theme.json --output output-theme.json -``` diff --git a/crates/theme_importer/src/color.rs b/crates/theme_importer/src/color.rs deleted file mode 100644 index 921f7a376b..0000000000 --- a/crates/theme_importer/src/color.rs +++ /dev/null @@ -1,56 +0,0 @@ -use anyhow::Result; -use gpui::Hsla; -use palette::FromColor; - -#[allow(unused)] -pub(crate) fn try_parse_color(color: &str) -> Result { - let rgba = gpui::Rgba::try_from(color)?; - let rgba = palette::rgb::Srgba::from_components((rgba.r, rgba.g, rgba.b, rgba.a)); - let hsla = palette::Hsla::from_color(rgba); - - let hsla = gpui::hsla( - hsla.hue.into_positive_degrees() / 360., - hsla.saturation, - hsla.lightness, - hsla.alpha, - ); - - Ok(hsla) -} - -#[allow(unused)] -pub(crate) fn pack_color(color: Hsla) -> u32 { - let hsla = palette::Hsla::from_components((color.h * 360., color.s, color.l, color.a)); - let rgba = palette::rgb::Srgba::from_color(hsla); - let rgba = rgba.into_format::(); - - u32::from(rgba) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - pub fn test_serialize_color() { - let color = "#b4637aff"; - let hsla = try_parse_color(color).unwrap(); - let packed = pack_color(hsla); - - assert_eq!(format!("#{:x}", packed), color); - } - - #[test] - pub fn test_serialize_color_with_palette() { - let color = "#b4637aff"; - - let rgba = gpui::Rgba::try_from(color).unwrap(); - let rgba = palette::rgb::Srgba::from_components((rgba.r, rgba.g, rgba.b, rgba.a)); - let hsla = palette::Hsla::from_color(rgba); - - let rgba = palette::rgb::Srgba::from_color(hsla); - let rgba = rgba.into_format::(); - - assert_eq!(format!("#{:x}", rgba), color); - } -} diff --git a/crates/theme_importer/src/main.rs b/crates/theme_importer/src/main.rs deleted file mode 100644 index 24291fc511..0000000000 --- a/crates/theme_importer/src/main.rs +++ /dev/null @@ -1,130 +0,0 @@ -mod color; -mod vscode; - -use std::fs::File; -use std::io::{Read, Write}; -use std::path::PathBuf; - -use anyhow::{Context as _, Result}; -use clap::Parser; -use collections::IndexMap; -use log::LevelFilter; -use serde::Deserialize; -use simplelog::ColorChoice; -use simplelog::{TermLogger, TerminalMode}; -use theme::{Appearance, AppearanceContent}; - -use crate::vscode::VsCodeTheme; -use crate::vscode::VsCodeThemeConverter; - -const ZED_THEME_SCHEMA_URL: &str = "https://zed.dev/schema/themes/v0.2.0.json"; - -#[derive(Debug, Clone, Copy, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum ThemeAppearanceJson { - Light, - Dark, -} - -impl From for AppearanceContent { - fn from(value: ThemeAppearanceJson) -> Self { - match value { - ThemeAppearanceJson::Light => Self::Light, - ThemeAppearanceJson::Dark => Self::Dark, - } - } -} - -impl From for Appearance { - fn from(value: ThemeAppearanceJson) -> Self { - match value { - ThemeAppearanceJson::Light => Self::Light, - ThemeAppearanceJson::Dark => Self::Dark, - } - } -} - -#[derive(Debug, Deserialize)] -pub struct ThemeMetadata { - pub name: String, - pub file_name: String, - pub appearance: ThemeAppearanceJson, -} - -#[derive(Parser)] -#[command(author, version, about, long_about = None)] -struct Args { - /// The path to the theme to import. - theme_path: PathBuf, - - /// Whether to warn when values are missing from the theme. - #[arg(long)] - warn_on_missing: bool, - - /// The path to write the output to. - #[arg(long, short)] - output: Option, -} - -fn main() -> Result<()> { - let args = Args::parse(); - - let log_config = { - let mut config = simplelog::ConfigBuilder::new(); - - if !args.warn_on_missing { - config.add_filter_ignore_str("theme_printer"); - } - - config.build() - }; - - TermLogger::init( - LevelFilter::Trace, - log_config, - TerminalMode::Stderr, - ColorChoice::Auto, - ) - .expect("could not initialize logger"); - - let theme_file_path = args.theme_path; - - let mut buffer = Vec::new(); - match File::open(&theme_file_path).and_then(|mut file| file.read_to_end(&mut buffer)) { - Ok(_) => {} - Err(err) => { - log::info!("Failed to open file at path: {:?}", theme_file_path); - return Err(err)?; - } - }; - - let vscode_theme: VsCodeTheme = serde_json_lenient::from_slice(&buffer) - .context(format!("failed to parse theme {theme_file_path:?}"))?; - - let theme_metadata = ThemeMetadata { - name: vscode_theme.name.clone().unwrap_or("".to_string()), - appearance: ThemeAppearanceJson::Dark, - file_name: "".to_string(), - }; - - let converter = VsCodeThemeConverter::new(vscode_theme, theme_metadata, IndexMap::default()); - - let theme = converter.convert()?; - let mut theme = serde_json::to_value(theme).unwrap(); - theme.as_object_mut().unwrap().insert( - "$schema".to_string(), - serde_json::Value::String(ZED_THEME_SCHEMA_URL.to_string()), - ); - let theme_json = serde_json::to_string_pretty(&theme).unwrap(); - - if let Some(output) = args.output { - let mut file = File::create(output)?; - file.write_all(theme_json.as_bytes())?; - } else { - println!("{}", theme_json); - } - - log::info!("Done!"); - - Ok(()) -} diff --git a/crates/theme_importer/src/vscode.rs b/crates/theme_importer/src/vscode.rs deleted file mode 100644 index 6933bbaa8f..0000000000 --- a/crates/theme_importer/src/vscode.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod converter; -mod syntax; -mod theme; - -pub use converter::*; -pub use syntax::*; -pub use theme::*; diff --git a/crates/theme_importer/src/vscode/converter.rs b/crates/theme_importer/src/vscode/converter.rs deleted file mode 100644 index e4a9769978..0000000000 --- a/crates/theme_importer/src/vscode/converter.rs +++ /dev/null @@ -1,276 +0,0 @@ -use anyhow::Result; -use collections::IndexMap; -use strum::IntoEnumIterator; -use theme::{ - FontStyleContent, FontWeightContent, HighlightStyleContent, StatusColorsContent, - ThemeColorsContent, ThemeContent, ThemeStyleContent, WindowBackgroundContent, -}; - -use crate::ThemeMetadata; -use crate::vscode::{VsCodeTheme, VsCodeTokenScope}; - -use super::ZedSyntaxToken; - -pub(crate) fn try_parse_font_weight(font_style: &str) -> Option { - match font_style { - style if style.contains("bold") => Some(FontWeightContent::Bold), - _ => None, - } -} - -pub(crate) fn try_parse_font_style(font_style: &str) -> Option { - match font_style { - style if style.contains("italic") => Some(FontStyleContent::Italic), - style if style.contains("oblique") => Some(FontStyleContent::Oblique), - _ => None, - } -} - -pub struct VsCodeThemeConverter { - theme: VsCodeTheme, - theme_metadata: ThemeMetadata, - syntax_overrides: IndexMap>, -} - -impl VsCodeThemeConverter { - pub fn new( - theme: VsCodeTheme, - theme_metadata: ThemeMetadata, - syntax_overrides: IndexMap>, - ) -> Self { - Self { - theme, - theme_metadata, - syntax_overrides, - } - } - - pub fn convert(self) -> Result { - let appearance = self.theme_metadata.appearance.into(); - - let status_colors = self.convert_status_colors()?; - let theme_colors = self.convert_theme_colors()?; - let syntax_theme = self.convert_syntax_theme()?; - - Ok(ThemeContent { - name: self.theme_metadata.name, - appearance, - style: ThemeStyleContent { - window_background_appearance: Some(WindowBackgroundContent::Opaque), - accents: Vec::new(), //TODO can we read this from the theme? - colors: theme_colors, - status: status_colors, - players: Vec::new(), - syntax: syntax_theme, - }, - }) - } - - fn convert_status_colors(&self) -> Result { - let vscode_colors = &self.theme.colors; - - let vscode_base_status_colors = StatusColorsContent { - hint: Some("#969696ff".to_string()), - ..Default::default() - }; - - Ok(StatusColorsContent { - conflict: vscode_colors - .git_decoration - .conflicting_resource_foreground - .clone(), - created: vscode_colors.editor_gutter.added_background.clone(), - deleted: vscode_colors.editor_gutter.deleted_background.clone(), - error: vscode_colors.editor_error.foreground.clone(), - error_background: vscode_colors.editor_error.background.clone(), - error_border: vscode_colors.editor_error.border.clone(), - hidden: vscode_colors.tab.inactive_foreground.clone(), - hint: vscode_colors - .editor_inlay_hint - .foreground - .clone() - .or(vscode_base_status_colors.hint), - hint_border: vscode_colors.editor_hint.border.clone(), - ignored: vscode_colors - .git_decoration - .ignored_resource_foreground - .clone(), - info: vscode_colors.editor_info.foreground.clone(), - info_background: vscode_colors.editor_info.background.clone(), - info_border: vscode_colors.editor_info.border.clone(), - modified: vscode_colors.editor_gutter.modified_background.clone(), - // renamed: None, - // success: None, - warning: vscode_colors.editor_warning.foreground.clone(), - warning_background: vscode_colors.editor_warning.background.clone(), - warning_border: vscode_colors.editor_warning.border.clone(), - ..Default::default() - }) - } - - fn convert_theme_colors(&self) -> Result { - let vscode_colors = &self.theme.colors; - - let vscode_panel_border = vscode_colors.panel.border.clone(); - let vscode_tab_inactive_background = vscode_colors.tab.inactive_background.clone(); - let vscode_editor_foreground = vscode_colors.editor.foreground.clone(); - let vscode_editor_background = vscode_colors.editor.background.clone(); - let vscode_scrollbar_slider_background = vscode_colors.scrollbar_slider.background.clone(); - let vscode_token_colors_foreground = self - .theme - .token_colors - .iter() - .find(|token_color| token_color.scope.is_none()) - .and_then(|token_color| token_color.settings.foreground.as_ref()) - .cloned(); - - Ok(ThemeColorsContent { - border: vscode_panel_border.clone(), - border_variant: vscode_panel_border.clone(), - border_focused: vscode_colors.focus_border.clone(), - border_selected: vscode_panel_border.clone(), - border_transparent: vscode_panel_border.clone(), - border_disabled: vscode_panel_border.clone(), - elevated_surface_background: vscode_colors.dropdown.background.clone(), - surface_background: vscode_colors.panel.background.clone(), - background: vscode_editor_background.clone(), - element_background: vscode_colors.button.background.clone(), - element_hover: vscode_colors.list.hover_background.clone(), - element_selected: vscode_colors.list.active_selection_background.clone(), - drop_target_background: vscode_colors.list.drop_background.clone(), - ghost_element_hover: vscode_colors.list.hover_background.clone(), - ghost_element_selected: vscode_colors.list.active_selection_background.clone(), - text: vscode_colors - .foreground - .clone() - .or(vscode_token_colors_foreground.clone()), - text_muted: vscode_colors.tab.inactive_foreground.clone(), - status_bar_background: vscode_colors.status_bar.background.clone(), - title_bar_background: vscode_colors.title_bar.active_background.clone(), - toolbar_background: vscode_colors - .breadcrumb - .background - .clone() - .or(vscode_editor_background.clone()), - tab_bar_background: vscode_colors.editor_group_header.tabs_background.clone(), - tab_inactive_background: vscode_tab_inactive_background.clone(), - tab_active_background: vscode_colors - .tab - .active_background - .clone() - .or(vscode_tab_inactive_background), - search_match_background: vscode_colors.editor.find_match_background.clone(), - panel_background: vscode_colors.panel.background.clone(), - pane_group_border: vscode_colors.editor_group.border.clone(), - scrollbar_thumb_background: vscode_scrollbar_slider_background.clone(), - scrollbar_thumb_hover_background: vscode_colors - .scrollbar_slider - .hover_background - .clone(), - scrollbar_thumb_active_background: vscode_colors - .scrollbar_slider - .active_background - .clone(), - scrollbar_thumb_border: vscode_scrollbar_slider_background, - scrollbar_track_background: vscode_editor_background.clone(), - scrollbar_track_border: vscode_colors.editor_overview_ruler.border.clone(), - minimap_thumb_background: vscode_colors.minimap_slider.background.clone(), - minimap_thumb_hover_background: vscode_colors.minimap_slider.hover_background.clone(), - minimap_thumb_active_background: vscode_colors.minimap_slider.active_background.clone(), - editor_foreground: vscode_editor_foreground.or(vscode_token_colors_foreground), - editor_background: vscode_editor_background.clone(), - editor_gutter_background: vscode_editor_background, - editor_active_line_background: vscode_colors.editor.line_highlight_background.clone(), - editor_line_number: vscode_colors.editor_line_number.foreground.clone(), - editor_active_line_number: vscode_colors.editor.foreground.clone(), - editor_wrap_guide: vscode_panel_border.clone(), - editor_active_wrap_guide: vscode_panel_border, - editor_document_highlight_bracket_background: vscode_colors - .editor_bracket_match - .background - .clone(), - terminal_background: vscode_colors.terminal.background.clone(), - terminal_ansi_black: vscode_colors.terminal.ansi_black.clone(), - terminal_ansi_bright_black: vscode_colors.terminal.ansi_bright_black.clone(), - terminal_ansi_red: vscode_colors.terminal.ansi_red.clone(), - terminal_ansi_bright_red: vscode_colors.terminal.ansi_bright_red.clone(), - terminal_ansi_green: vscode_colors.terminal.ansi_green.clone(), - terminal_ansi_bright_green: vscode_colors.terminal.ansi_bright_green.clone(), - terminal_ansi_yellow: vscode_colors.terminal.ansi_yellow.clone(), - terminal_ansi_bright_yellow: vscode_colors.terminal.ansi_bright_yellow.clone(), - terminal_ansi_blue: vscode_colors.terminal.ansi_blue.clone(), - terminal_ansi_bright_blue: vscode_colors.terminal.ansi_bright_blue.clone(), - terminal_ansi_magenta: vscode_colors.terminal.ansi_magenta.clone(), - terminal_ansi_bright_magenta: vscode_colors.terminal.ansi_bright_magenta.clone(), - terminal_ansi_cyan: vscode_colors.terminal.ansi_cyan.clone(), - terminal_ansi_bright_cyan: vscode_colors.terminal.ansi_bright_cyan.clone(), - terminal_ansi_white: vscode_colors.terminal.ansi_white.clone(), - terminal_ansi_bright_white: vscode_colors.terminal.ansi_bright_white.clone(), - link_text_hover: vscode_colors.text_link.active_foreground.clone(), - ..Default::default() - }) - } - - fn convert_syntax_theme(&self) -> Result> { - let mut highlight_styles = IndexMap::default(); - - for syntax_token in ZedSyntaxToken::iter() { - let override_match = self - .syntax_overrides - .get(&syntax_token.to_string()) - .and_then(|scope| { - self.theme.token_colors.iter().find(|token_color| { - token_color.scope == Some(VsCodeTokenScope::Many(scope.clone())) - }) - }); - - let best_match = override_match - .or_else(|| syntax_token.find_best_token_color_match(&self.theme.token_colors)) - .or_else(|| { - syntax_token.fallbacks().iter().find_map(|fallback| { - fallback.find_best_token_color_match(&self.theme.token_colors) - }) - }); - - let Some(token_color) = best_match else { - log::warn!("No matching token color found for '{syntax_token}'"); - continue; - }; - - log::info!( - "Matched '{syntax_token}' to '{}'", - token_color - .name - .clone() - .or_else(|| token_color - .scope - .as_ref() - .map(|scope| format!("{:?}", scope))) - .unwrap_or_else(|| "no identifier".to_string()) - ); - - let highlight_style = HighlightStyleContent { - color: token_color.settings.foreground.clone(), - background_color: token_color.settings.background.clone(), - font_style: token_color - .settings - .font_style - .as_ref() - .and_then(|style| try_parse_font_style(style)), - font_weight: token_color - .settings - .font_style - .as_ref() - .and_then(|style| try_parse_font_weight(style)), - }; - - if highlight_style.is_empty() { - continue; - } - - highlight_styles.insert(syntax_token.to_string(), highlight_style); - } - - Ok(highlight_styles) - } -} diff --git a/crates/theme_importer/src/vscode/syntax.rs b/crates/theme_importer/src/vscode/syntax.rs deleted file mode 100644 index 7b134089b7..0000000000 --- a/crates/theme_importer/src/vscode/syntax.rs +++ /dev/null @@ -1,310 +0,0 @@ -use indexmap::IndexMap; -use serde::Deserialize; -use strum::EnumIter; - -#[derive(Debug, PartialEq, Eq, Deserialize)] -#[serde(untagged)] -pub enum VsCodeTokenScope { - One(String), - Many(Vec), -} - -#[derive(Debug, Deserialize)] -pub struct VsCodeTokenColor { - pub name: Option, - pub scope: Option, - pub settings: VsCodeTokenColorSettings, -} - -#[derive(Debug, Deserialize)] -pub struct VsCodeTokenColorSettings { - pub foreground: Option, - pub background: Option, - #[serde(rename = "fontStyle")] - pub font_style: Option, -} - -#[derive(Debug, PartialEq, Copy, Clone, EnumIter)] -pub enum ZedSyntaxToken { - Attribute, - Boolean, - Comment, - CommentDoc, - Constant, - Constructor, - Embedded, - Emphasis, - EmphasisStrong, - Enum, - Function, - Hint, - Keyword, - Label, - LinkText, - LinkUri, - Number, - Operator, - Predictive, - Preproc, - Primary, - Property, - Punctuation, - PunctuationBracket, - PunctuationDelimiter, - PunctuationListMarker, - PunctuationSpecial, - String, - StringEscape, - StringRegex, - StringSpecial, - StringSpecialSymbol, - Tag, - TextLiteral, - Title, - Type, - Variable, - VariableSpecial, - Variant, -} - -impl std::fmt::Display for ZedSyntaxToken { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match self { - ZedSyntaxToken::Attribute => "attribute", - ZedSyntaxToken::Boolean => "boolean", - ZedSyntaxToken::Comment => "comment", - ZedSyntaxToken::CommentDoc => "comment.doc", - ZedSyntaxToken::Constant => "constant", - ZedSyntaxToken::Constructor => "constructor", - ZedSyntaxToken::Embedded => "embedded", - ZedSyntaxToken::Emphasis => "emphasis", - ZedSyntaxToken::EmphasisStrong => "emphasis.strong", - ZedSyntaxToken::Enum => "enum", - ZedSyntaxToken::Function => "function", - ZedSyntaxToken::Hint => "hint", - ZedSyntaxToken::Keyword => "keyword", - ZedSyntaxToken::Label => "label", - ZedSyntaxToken::LinkText => "link_text", - ZedSyntaxToken::LinkUri => "link_uri", - ZedSyntaxToken::Number => "number", - ZedSyntaxToken::Operator => "operator", - ZedSyntaxToken::Predictive => "predictive", - ZedSyntaxToken::Preproc => "preproc", - ZedSyntaxToken::Primary => "primary", - ZedSyntaxToken::Property => "property", - ZedSyntaxToken::Punctuation => "punctuation", - ZedSyntaxToken::PunctuationBracket => "punctuation.bracket", - ZedSyntaxToken::PunctuationDelimiter => "punctuation.delimiter", - ZedSyntaxToken::PunctuationListMarker => "punctuation.list_marker", - ZedSyntaxToken::PunctuationSpecial => "punctuation.special", - ZedSyntaxToken::String => "string", - ZedSyntaxToken::StringEscape => "string.escape", - ZedSyntaxToken::StringRegex => "string.regex", - ZedSyntaxToken::StringSpecial => "string.special", - ZedSyntaxToken::StringSpecialSymbol => "string.special.symbol", - ZedSyntaxToken::Tag => "tag", - ZedSyntaxToken::TextLiteral => "text.literal", - ZedSyntaxToken::Title => "title", - ZedSyntaxToken::Type => "type", - ZedSyntaxToken::Variable => "variable", - ZedSyntaxToken::VariableSpecial => "variable.special", - ZedSyntaxToken::Variant => "variant", - } - ) - } -} - -impl ZedSyntaxToken { - pub fn find_best_token_color_match<'a>( - &self, - token_colors: &'a [VsCodeTokenColor], - ) -> Option<&'a VsCodeTokenColor> { - let mut ranked_matches = IndexMap::new(); - - for (ix, token_color) in token_colors.iter().enumerate() { - if token_color.settings.foreground.is_none() { - continue; - } - - let Some(rank) = self.rank_match(token_color) else { - continue; - }; - - if rank > 0 { - ranked_matches.insert(ix, rank); - } - } - - ranked_matches - .into_iter() - .max_by_key(|(_, rank)| *rank) - .map(|(ix, _)| &token_colors[ix]) - } - - fn rank_match(&self, token_color: &VsCodeTokenColor) -> Option { - let candidate_scopes = match token_color.scope.as_ref()? { - VsCodeTokenScope::One(scope) => vec![scope], - VsCodeTokenScope::Many(scopes) => scopes.iter().collect(), - } - .iter() - .flat_map(|scope| scope.split(',').map(|s| s.trim())) - .collect::>(); - - let scopes_to_match = self.to_vscode(); - let number_of_scopes_to_match = scopes_to_match.len(); - - let mut matches = 0; - - for (ix, scope) in scopes_to_match.into_iter().enumerate() { - // Assign each entry a weight that is inversely proportional to its - // position in the list. - // - // Entries towards the front are weighted higher than those towards the end. - let weight = (number_of_scopes_to_match - ix) as u32; - - if candidate_scopes.contains(&scope) { - matches += 1 + weight; - } - } - - Some(matches) - } - - pub fn fallbacks(&self) -> &[Self] { - match self { - ZedSyntaxToken::CommentDoc => &[ZedSyntaxToken::Comment], - ZedSyntaxToken::Number => &[ZedSyntaxToken::Constant], - ZedSyntaxToken::VariableSpecial => &[ZedSyntaxToken::Variable], - ZedSyntaxToken::PunctuationBracket - | ZedSyntaxToken::PunctuationDelimiter - | ZedSyntaxToken::PunctuationListMarker - | ZedSyntaxToken::PunctuationSpecial => &[ZedSyntaxToken::Punctuation], - ZedSyntaxToken::StringEscape - | ZedSyntaxToken::StringRegex - | ZedSyntaxToken::StringSpecial - | ZedSyntaxToken::StringSpecialSymbol => &[ZedSyntaxToken::String], - _ => &[], - } - } - - fn to_vscode(self) -> Vec<&'static str> { - match self { - ZedSyntaxToken::Attribute => vec!["entity.other.attribute-name"], - ZedSyntaxToken::Boolean => vec!["constant.language"], - ZedSyntaxToken::Comment => vec!["comment"], - ZedSyntaxToken::CommentDoc => vec!["comment.block.documentation"], - ZedSyntaxToken::Constant => vec!["constant", "constant.language", "constant.character"], - ZedSyntaxToken::Constructor => { - vec![ - "entity.name.tag", - "entity.name.function.definition.special.constructor", - ] - } - ZedSyntaxToken::Embedded => vec!["meta.embedded"], - ZedSyntaxToken::Emphasis => vec!["markup.italic"], - ZedSyntaxToken::EmphasisStrong => vec![ - "markup.bold", - "markup.italic markup.bold", - "markup.bold markup.italic", - ], - ZedSyntaxToken::Enum => vec!["support.type.enum"], - ZedSyntaxToken::Function => vec![ - "entity.function", - "entity.name.function", - "variable.function", - ], - ZedSyntaxToken::Hint => vec![], - ZedSyntaxToken::Keyword => vec![ - "keyword", - "keyword.other.fn.rust", - "keyword.control", - "keyword.control.fun", - "keyword.control.class", - "punctuation.accessor", - "entity.name.tag", - ], - ZedSyntaxToken::Label => vec![ - "label", - "entity.name", - "entity.name.import", - "entity.name.package", - ], - ZedSyntaxToken::LinkText => vec!["markup.underline.link", "string.other.link"], - ZedSyntaxToken::LinkUri => vec!["markup.underline.link", "string.other.link"], - ZedSyntaxToken::Number => vec!["constant.numeric", "number"], - ZedSyntaxToken::Operator => vec!["operator", "keyword.operator"], - ZedSyntaxToken::Predictive => vec![], - ZedSyntaxToken::Preproc => vec![ - "preproc", - "meta.preprocessor", - "punctuation.definition.preprocessor", - ], - ZedSyntaxToken::Primary => vec![], - ZedSyntaxToken::Property => vec![ - "variable.member", - "support.type.property-name", - "variable.object.property", - "variable.other.field", - ], - ZedSyntaxToken::Punctuation => vec![ - "punctuation", - "punctuation.section", - "punctuation.accessor", - "punctuation.separator", - "punctuation.definition.tag", - ], - ZedSyntaxToken::PunctuationBracket => vec![ - "punctuation.bracket", - "punctuation.definition.tag.begin", - "punctuation.definition.tag.end", - ], - ZedSyntaxToken::PunctuationDelimiter => vec![ - "punctuation.delimiter", - "punctuation.separator", - "punctuation.terminator", - ], - ZedSyntaxToken::PunctuationListMarker => { - vec!["markup.list punctuation.definition.list.begin"] - } - ZedSyntaxToken::PunctuationSpecial => vec!["punctuation.special"], - ZedSyntaxToken::String => vec!["string"], - ZedSyntaxToken::StringEscape => { - vec!["string.escape", "constant.character", "constant.other"] - } - ZedSyntaxToken::StringRegex => vec!["string.regex"], - ZedSyntaxToken::StringSpecial => vec!["string.special", "constant.other.symbol"], - ZedSyntaxToken::StringSpecialSymbol => { - vec!["string.special.symbol", "constant.other.symbol"] - } - ZedSyntaxToken::Tag => vec!["tag", "entity.name.tag", "meta.tag.sgml"], - ZedSyntaxToken::TextLiteral => vec!["text.literal", "string"], - ZedSyntaxToken::Title => vec!["title", "entity.name"], - ZedSyntaxToken::Type => vec![ - "entity.name.type", - "entity.name.type.primitive", - "entity.name.type.numeric", - "keyword.type", - "support.type", - "support.type.primitive", - "support.class", - ], - ZedSyntaxToken::Variable => vec![ - "variable", - "variable.language", - "variable.member", - "variable.parameter", - "variable.parameter.function-call", - ], - ZedSyntaxToken::VariableSpecial => vec![ - "variable.special", - "variable.member", - "variable.annotation", - "variable.language", - ], - ZedSyntaxToken::Variant => vec!["variant"], - } - } -} diff --git a/crates/theme_importer/src/vscode/theme.rs b/crates/theme_importer/src/vscode/theme.rs deleted file mode 100644 index 2479dd312e..0000000000 --- a/crates/theme_importer/src/vscode/theme.rs +++ /dev/null @@ -1,40 +0,0 @@ -use serde::Deserialize; -use vscode_theme::Colors; - -use crate::vscode::VsCodeTokenColor; - -#[derive(Deserialize, Debug)] -pub struct VsCodeTheme { - #[serde(rename = "$schema")] - #[expect( - unused, - reason = "This field was found to be unused with serde library bump; it's left as is due to insufficient context on PO's side, but it *may* be fine to remove" - )] - pub schema: Option, - pub name: Option, - #[expect( - unused, - reason = "This field was found to be unused with serde library bump; it's left as is due to insufficient context on PO's side, but it *may* be fine to remove" - )] - pub author: Option, - #[expect( - unused, - reason = "This field was found to be unused with serde library bump; it's left as is due to insufficient context on PO's side, but it *may* be fine to remove" - )] - pub maintainers: Option>, - #[serde(rename = "semanticClass")] - #[expect( - unused, - reason = "This field was found to be unused with serde library bump; it's left as is due to insufficient context on PO's side, but it *may* be fine to remove" - )] - pub semantic_class: Option, - #[expect( - unused, - reason = "This field was found to be unused with serde library bump; it's left as is due to insufficient context on PO's side, but it *may* be fine to remove" - )] - #[serde(rename = "semanticHighlighting")] - pub semantic_highlighting: Option, - pub colors: Colors, - #[serde(rename = "tokenColors")] - pub token_colors: Vec, -} diff --git a/crates/theme_selector/Cargo.toml b/crates/theme_selector/Cargo.toml deleted file mode 100644 index 1a563e81f2..0000000000 --- a/crates/theme_selector/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "theme_selector" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/theme_selector.rs" -doctest = false - -[dependencies] -fs.workspace = true -fuzzy.workspace = true -gpui.workspace = true -log.workspace = true -picker.workspace = true -serde.workspace = true -settings.workspace = true -telemetry.workspace = true -theme.workspace = true -ui.workspace = true -util.workspace = true -workspace.workspace = true -zed_actions.workspace = true - -[dev-dependencies] diff --git a/crates/theme_selector/LICENSE-GPL b/crates/theme_selector/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/theme_selector/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/theme_selector/src/icon_theme_selector.rs b/crates/theme_selector/src/icon_theme_selector.rs deleted file mode 100644 index 2ea3436d43..0000000000 --- a/crates/theme_selector/src/icon_theme_selector.rs +++ /dev/null @@ -1,338 +0,0 @@ -use fs::Fs; -use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; -use gpui::{ - App, Context, DismissEvent, Entity, EventEmitter, Focusable, Render, UpdateGlobal, WeakEntity, - Window, -}; -use picker::{Picker, PickerDelegate}; -use settings::{Settings as _, SettingsStore, update_settings_file}; -use std::sync::Arc; -use theme::{ - Appearance, IconThemeName, IconThemeSelection, SystemAppearance, ThemeMeta, ThemeRegistry, - ThemeSettings, -}; -use ui::{ListItem, ListItemSpacing, prelude::*, v_flex}; -use util::ResultExt; -use workspace::{ModalView, ui::HighlightedLabel}; -use zed_actions::{ExtensionCategoryFilter, Extensions}; - -pub(crate) struct IconThemeSelector { - picker: Entity>, -} - -impl EventEmitter for IconThemeSelector {} - -impl Focusable for IconThemeSelector { - fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { - self.picker.focus_handle(cx) - } -} - -impl ModalView for IconThemeSelector {} - -impl IconThemeSelector { - pub fn new( - delegate: IconThemeSelectorDelegate, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); - Self { picker } - } -} - -impl Render for IconThemeSelector { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - v_flex() - .key_context("IconThemeSelector") - .w(rems(34.)) - .child(self.picker.clone()) - } -} - -pub(crate) struct IconThemeSelectorDelegate { - fs: Arc, - themes: Vec, - matches: Vec, - original_theme: IconThemeName, - selection_completed: bool, - selected_theme: Option, - selected_index: usize, - selector: WeakEntity, -} - -impl IconThemeSelectorDelegate { - pub fn new( - selector: WeakEntity, - fs: Arc, - themes_filter: Option<&Vec>, - cx: &mut Context, - ) -> Self { - let theme_settings = ThemeSettings::get_global(cx); - let original_theme = theme_settings - .icon_theme - .name(SystemAppearance::global(cx).0); - - let registry = ThemeRegistry::global(cx); - let mut themes = registry - .list_icon_themes() - .into_iter() - .filter(|meta| { - if let Some(theme_filter) = themes_filter { - theme_filter.contains(&meta.name.to_string()) - } else { - true - } - }) - .collect::>(); - - themes.sort_unstable_by(|a, b| { - a.appearance - .is_light() - .cmp(&b.appearance.is_light()) - .then(a.name.cmp(&b.name)) - }); - let matches = themes - .iter() - .map(|meta| StringMatch { - candidate_id: 0, - score: 0.0, - positions: Default::default(), - string: meta.name.to_string(), - }) - .collect(); - let mut this = Self { - fs, - themes, - matches, - original_theme: original_theme.clone(), - selected_index: 0, - selected_theme: None, - selection_completed: false, - selector, - }; - - this.select_if_matching(&original_theme.0); - this - } - - fn show_selected_theme( - &mut self, - cx: &mut Context>, - ) -> Option { - let mat = self.matches.get(self.selected_index)?; - let name = IconThemeName(mat.string.clone().into()); - Self::set_icon_theme(name.clone(), cx); - Some(name) - } - - fn select_if_matching(&mut self, theme_name: &str) { - self.selected_index = self - .matches - .iter() - .position(|mat| mat.string == theme_name) - .unwrap_or(self.selected_index); - } - - fn set_icon_theme(name: IconThemeName, cx: &mut App) { - SettingsStore::update_global(cx, |store, _| { - let mut theme_settings = store.get::(None).clone(); - theme_settings.icon_theme = IconThemeSelection::Static(name); - store.override_global(theme_settings); - }); - } -} - -impl PickerDelegate for IconThemeSelectorDelegate { - type ListItem = ui::ListItem; - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - "Select Icon Theme...".into() - } - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn confirm( - &mut self, - _: bool, - window: &mut Window, - cx: &mut Context>, - ) { - self.selection_completed = true; - - let theme_settings = ThemeSettings::get_global(cx); - let theme_name = theme_settings - .icon_theme - .name(SystemAppearance::global(cx).0); - - telemetry::event!( - "Settings Changed", - setting = "icon_theme", - value = theme_name - ); - - let appearance = Appearance::from(window.appearance()); - - update_settings_file(self.fs.clone(), cx, move |settings, _| { - theme::set_icon_theme(settings, theme_name, appearance); - }); - - self.selector - .update(cx, |_, cx| { - cx.emit(DismissEvent); - }) - .ok(); - } - - fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { - if !self.selection_completed { - Self::set_icon_theme(self.original_theme.clone(), cx); - self.selection_completed = true; - } - - self.selector - .update(cx, |_, cx| cx.emit(DismissEvent)) - .log_err(); - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index( - &mut self, - ix: usize, - _: &mut Window, - cx: &mut Context>, - ) { - self.selected_index = ix; - self.selected_theme = self.show_selected_theme(cx); - } - - fn update_matches( - &mut self, - query: String, - window: &mut Window, - cx: &mut Context>, - ) -> gpui::Task<()> { - let background = cx.background_executor().clone(); - let candidates = self - .themes - .iter() - .enumerate() - .map(|(id, meta)| StringMatchCandidate::new(id, &meta.name)) - .collect::>(); - - cx.spawn_in(window, async move |this, cx| { - let matches = if query.is_empty() { - candidates - .into_iter() - .enumerate() - .map(|(index, candidate)| StringMatch { - candidate_id: index, - string: candidate.string, - positions: Vec::new(), - score: 0.0, - }) - .collect() - } else { - match_strings( - &candidates, - &query, - false, - true, - 100, - &Default::default(), - background, - ) - .await - }; - - this.update(cx, |this, cx| { - this.delegate.matches = matches; - if query.is_empty() && this.delegate.selected_theme.is_none() { - this.delegate.selected_index = this - .delegate - .selected_index - .min(this.delegate.matches.len().saturating_sub(1)); - } else if let Some(selected) = this.delegate.selected_theme.as_ref() { - this.delegate.selected_index = this - .delegate - .matches - .iter() - .enumerate() - .find(|(_, mtch)| mtch.string.as_str() == selected.0.as_ref()) - .map(|(ix, _)| ix) - .unwrap_or_default(); - } else { - this.delegate.selected_index = 0; - } - this.delegate.selected_theme = this.delegate.show_selected_theme(cx); - }) - .log_err(); - }) - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _window: &mut Window, - _cx: &mut Context>, - ) -> Option { - let theme_match = &self.matches.get(ix)?; - - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .child(HighlightedLabel::new( - theme_match.string.clone(), - theme_match.positions.clone(), - )), - ) - } - - fn render_footer( - &self, - _window: &mut Window, - cx: &mut Context>, - ) -> Option { - Some( - h_flex() - .p_2() - .w_full() - .justify_between() - .gap_2() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .child( - Button::new("docs", "View Icon Theme Docs") - .icon(IconName::ArrowUpRight) - .icon_position(IconPosition::End) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .on_click(|_event, _window, cx| { - cx.open_url("https://zed.dev/docs/icon-themes"); - }), - ) - .child( - Button::new("more-icon-themes", "Install Icon Themes").on_click( - move |_event, window, cx| { - window.dispatch_action( - Box::new(Extensions { - category_filter: Some(ExtensionCategoryFilter::IconThemes), - id: None, - }), - cx, - ); - }, - ), - ) - .into_any_element(), - ) - } -} diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs deleted file mode 100644 index 74b242dd0b..0000000000 --- a/crates/theme_selector/src/theme_selector.rs +++ /dev/null @@ -1,524 +0,0 @@ -mod icon_theme_selector; - -use fs::Fs; -use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; -use gpui::{ - App, Context, DismissEvent, Entity, EventEmitter, Focusable, Render, UpdateGlobal, WeakEntity, - Window, actions, -}; -use picker::{Picker, PickerDelegate}; -use settings::{Settings, SettingsStore, update_settings_file}; -use std::sync::Arc; -use theme::{ - Appearance, SystemAppearance, Theme, ThemeAppearanceMode, ThemeMeta, ThemeName, ThemeRegistry, - ThemeSelection, ThemeSettings, -}; -use ui::{ListItem, ListItemSpacing, prelude::*, v_flex}; -use util::ResultExt; -use workspace::{ModalView, Workspace, ui::HighlightedLabel, with_active_or_new_workspace}; -use zed_actions::{ExtensionCategoryFilter, Extensions}; - -use crate::icon_theme_selector::{IconThemeSelector, IconThemeSelectorDelegate}; - -actions!( - theme_selector, - [ - /// Reloads all themes from disk. - Reload - ] -); - -pub fn init(cx: &mut App) { - cx.on_action(|action: &zed_actions::theme_selector::Toggle, cx| { - let action = action.clone(); - with_active_or_new_workspace(cx, move |workspace, window, cx| { - toggle_theme_selector(workspace, &action, window, cx); - }); - }); - cx.on_action(|action: &zed_actions::icon_theme_selector::Toggle, cx| { - let action = action.clone(); - with_active_or_new_workspace(cx, move |workspace, window, cx| { - toggle_icon_theme_selector(workspace, &action, window, cx); - }); - }); -} - -fn toggle_theme_selector( - workspace: &mut Workspace, - toggle: &zed_actions::theme_selector::Toggle, - window: &mut Window, - cx: &mut Context, -) { - let fs = workspace.app_state().fs.clone(); - workspace.toggle_modal(window, cx, |window, cx| { - let delegate = ThemeSelectorDelegate::new( - cx.entity().downgrade(), - fs, - toggle.themes_filter.as_ref(), - cx, - ); - ThemeSelector::new(delegate, window, cx) - }); -} - -fn toggle_icon_theme_selector( - workspace: &mut Workspace, - toggle: &zed_actions::icon_theme_selector::Toggle, - window: &mut Window, - cx: &mut Context, -) { - let fs = workspace.app_state().fs.clone(); - workspace.toggle_modal(window, cx, |window, cx| { - let delegate = IconThemeSelectorDelegate::new( - cx.entity().downgrade(), - fs, - toggle.themes_filter.as_ref(), - cx, - ); - IconThemeSelector::new(delegate, window, cx) - }); -} - -impl ModalView for ThemeSelector {} - -struct ThemeSelector { - picker: Entity>, -} - -impl EventEmitter for ThemeSelector {} - -impl Focusable for ThemeSelector { - fn focus_handle(&self, cx: &App) -> gpui::FocusHandle { - self.picker.focus_handle(cx) - } -} - -impl Render for ThemeSelector { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - v_flex() - .key_context("ThemeSelector") - .w(rems(34.)) - .child(self.picker.clone()) - } -} - -impl ThemeSelector { - pub fn new( - delegate: ThemeSelectorDelegate, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx)); - Self { picker } - } -} - -struct ThemeSelectorDelegate { - fs: Arc, - themes: Vec, - matches: Vec, - /// The theme that was selected before the `ThemeSelector` menu was opened. - /// - /// We use this to return back to theme that was set if the user dismisses the menu. - original_theme_settings: ThemeSettings, - /// The current system appearance. - original_system_appearance: Appearance, - /// The currently selected new theme. - new_theme: Arc, - selection_completed: bool, - selected_theme: Option>, - selected_index: usize, - selector: WeakEntity, -} - -impl ThemeSelectorDelegate { - fn new( - selector: WeakEntity, - fs: Arc, - themes_filter: Option<&Vec>, - cx: &mut Context, - ) -> Self { - let original_theme = cx.theme().clone(); - let original_theme_settings = ThemeSettings::get_global(cx).clone(); - let original_system_appearance = SystemAppearance::global(cx).0; - - let registry = ThemeRegistry::global(cx); - let mut themes = registry - .list() - .into_iter() - .filter(|meta| { - if let Some(theme_filter) = themes_filter { - theme_filter.contains(&meta.name.to_string()) - } else { - true - } - }) - .collect::>(); - - // Sort by dark vs light, then by name. - themes.sort_unstable_by(|a, b| { - a.appearance - .is_light() - .cmp(&b.appearance.is_light()) - .then(a.name.cmp(&b.name)) - }); - - let matches: Vec = themes - .iter() - .map(|meta| StringMatch { - candidate_id: 0, - score: 0.0, - positions: Default::default(), - string: meta.name.to_string(), - }) - .collect(); - - // The current theme is likely in this list, so default to first showing that. - let selected_index = matches - .iter() - .position(|mat| mat.string == original_theme.name) - .unwrap_or(0); - - Self { - fs, - themes, - matches, - original_theme_settings, - original_system_appearance, - new_theme: original_theme, // Start with the original theme. - selected_index, - selection_completed: false, - selected_theme: None, - selector, - } - } - - fn show_selected_theme( - &mut self, - cx: &mut Context>, - ) -> Option> { - if let Some(mat) = self.matches.get(self.selected_index) { - let registry = ThemeRegistry::global(cx); - - match registry.get(&mat.string) { - Ok(theme) => { - self.set_theme(theme.clone(), cx); - Some(theme) - } - Err(error) => { - log::error!("error loading theme {}: {}", mat.string, error); - None - } - } - } else { - None - } - } - - fn set_theme(&mut self, new_theme: Arc, cx: &mut App) { - // Update the global (in-memory) theme settings. - SettingsStore::update_global(cx, |store, _| { - override_global_theme( - store, - &new_theme, - &self.original_theme_settings.theme, - self.original_system_appearance, - ) - }); - - self.new_theme = new_theme; - } -} - -/// Overrides the global (in-memory) theme settings. -/// -/// Note that this does **not** update the user's `settings.json` file (see the -/// [`ThemeSelectorDelegate::confirm`] method and [`theme::set_theme`] function). -fn override_global_theme( - store: &mut SettingsStore, - new_theme: &Theme, - original_theme: &ThemeSelection, - system_appearance: Appearance, -) { - let theme_name = ThemeName(new_theme.name.clone().into()); - let new_appearance = new_theme.appearance(); - let new_theme_is_light = new_appearance.is_light(); - - let mut curr_theme_settings = store.get::(None).clone(); - - match (original_theme, &curr_theme_settings.theme) { - // Override the currently selected static theme. - (ThemeSelection::Static(_), ThemeSelection::Static(_)) => { - curr_theme_settings.theme = ThemeSelection::Static(theme_name); - } - - // If the current theme selection is dynamic, then only override the global setting for the - // specific mode (light or dark). - ( - ThemeSelection::Dynamic { - mode: original_mode, - light: original_light, - dark: original_dark, - }, - ThemeSelection::Dynamic { .. }, - ) => { - let new_mode = update_mode_if_new_appearance_is_different_from_system( - original_mode, - system_appearance, - new_appearance, - ); - - let updated_theme = retain_original_opposing_theme( - new_theme_is_light, - new_mode, - theme_name, - original_light, - original_dark, - ); - - curr_theme_settings.theme = updated_theme; - } - - // The theme selection mode changed while selecting new themes (someone edited the settings - // file on disk while we had the dialogue open), so don't do anything. - _ => return, - }; - - store.override_global(curr_theme_settings); -} - -/// Helper function for determining the new [`ThemeAppearanceMode`] for the new theme. -/// -/// If the the original theme mode was [`System`] and the new theme's appearance matches the system -/// appearance, we don't need to change the mode setting. -/// -/// Otherwise, we need to change the mode in order to see the new theme. -/// -/// [`System`]: ThemeAppearanceMode::System -fn update_mode_if_new_appearance_is_different_from_system( - original_mode: &ThemeAppearanceMode, - system_appearance: Appearance, - new_appearance: Appearance, -) -> ThemeAppearanceMode { - if original_mode == &ThemeAppearanceMode::System && system_appearance == new_appearance { - ThemeAppearanceMode::System - } else { - ThemeAppearanceMode::from(new_appearance) - } -} - -/// Helper function for updating / displaying the [`ThemeSelection`] while using the theme selector. -/// -/// We want to retain the alternate theme selection of the original settings (before the menu was -/// opened), not the currently selected theme (which likely has changed multiple times while the -/// menu has been open). -fn retain_original_opposing_theme( - new_theme_is_light: bool, - new_mode: ThemeAppearanceMode, - theme_name: ThemeName, - original_light: &ThemeName, - original_dark: &ThemeName, -) -> ThemeSelection { - if new_theme_is_light { - ThemeSelection::Dynamic { - mode: new_mode, - light: theme_name, - dark: original_dark.clone(), - } - } else { - ThemeSelection::Dynamic { - mode: new_mode, - light: original_light.clone(), - dark: theme_name, - } - } -} - -impl PickerDelegate for ThemeSelectorDelegate { - type ListItem = ui::ListItem; - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - "Select Theme...".into() - } - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn confirm( - &mut self, - _secondary: bool, - _window: &mut Window, - cx: &mut Context>, - ) { - self.selection_completed = true; - - let theme_name: Arc = self.new_theme.name.as_str().into(); - let theme_appearance = self.new_theme.appearance; - let system_appearance = SystemAppearance::global(cx).0; - - telemetry::event!("Settings Changed", setting = "theme", value = theme_name); - - update_settings_file(self.fs.clone(), cx, move |settings, _| { - theme::set_theme(settings, theme_name, theme_appearance, system_appearance); - }); - - self.selector - .update(cx, |_, cx| { - cx.emit(DismissEvent); - }) - .ok(); - } - - fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { - if !self.selection_completed { - SettingsStore::update_global(cx, |store, _| { - store.override_global(self.original_theme_settings.clone()); - }); - self.selection_completed = true; - } - - self.selector - .update(cx, |_, cx| cx.emit(DismissEvent)) - .log_err(); - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index( - &mut self, - ix: usize, - _: &mut Window, - cx: &mut Context>, - ) { - self.selected_index = ix; - self.selected_theme = self.show_selected_theme(cx); - } - - fn update_matches( - &mut self, - query: String, - window: &mut Window, - cx: &mut Context>, - ) -> gpui::Task<()> { - let background = cx.background_executor().clone(); - let candidates = self - .themes - .iter() - .enumerate() - .map(|(id, meta)| StringMatchCandidate::new(id, &meta.name)) - .collect::>(); - - cx.spawn_in(window, async move |this, cx| { - let matches = if query.is_empty() { - candidates - .into_iter() - .enumerate() - .map(|(index, candidate)| StringMatch { - candidate_id: index, - string: candidate.string, - positions: Vec::new(), - score: 0.0, - }) - .collect() - } else { - match_strings( - &candidates, - &query, - false, - true, - 100, - &Default::default(), - background, - ) - .await - }; - - this.update(cx, |this, cx| { - this.delegate.matches = matches; - if query.is_empty() && this.delegate.selected_theme.is_none() { - this.delegate.selected_index = this - .delegate - .selected_index - .min(this.delegate.matches.len().saturating_sub(1)); - } else if let Some(selected) = this.delegate.selected_theme.as_ref() { - this.delegate.selected_index = this - .delegate - .matches - .iter() - .enumerate() - .find(|(_, mtch)| mtch.string == selected.name) - .map(|(ix, _)| ix) - .unwrap_or_default(); - } else { - this.delegate.selected_index = 0; - } - this.delegate.selected_theme = this.delegate.show_selected_theme(cx); - }) - .log_err(); - }) - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _window: &mut Window, - _cx: &mut Context>, - ) -> Option { - let theme_match = &self.matches.get(ix)?; - - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .child(HighlightedLabel::new( - theme_match.string.clone(), - theme_match.positions.clone(), - )), - ) - } - - fn render_footer( - &self, - _: &mut Window, - cx: &mut Context>, - ) -> Option { - Some( - h_flex() - .p_2() - .w_full() - .justify_between() - .gap_2() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .child( - Button::new("docs", "View Theme Docs") - .icon(IconName::ArrowUpRight) - .icon_position(IconPosition::End) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .on_click(cx.listener(|_, _, _, cx| { - cx.open_url("https://zed.dev/docs/themes"); - })), - ) - .child( - Button::new("more-themes", "Install Themes").on_click(cx.listener({ - move |_, _, window, cx| { - window.dispatch_action( - Box::new(Extensions { - category_filter: Some(ExtensionCategoryFilter::Themes), - id: None, - }), - cx, - ); - } - })), - ) - .into_any_element(), - ) - } -} diff --git a/crates/time_format/Cargo.toml b/crates/time_format/Cargo.toml deleted file mode 100644 index b598d19887..0000000000 --- a/crates/time_format/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "time_format" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/time_format.rs" -doctest = false - -[dependencies] -sys-locale.workspace = true -time.workspace = true - -[target.'cfg(target_os = "macos")'.dependencies] -core-foundation.workspace = true -core-foundation-sys.workspace = true diff --git a/crates/time_format/LICENSE-GPL b/crates/time_format/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/time_format/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/time_format/src/time_format.rs b/crates/time_format/src/time_format.rs deleted file mode 100644 index 37d4201d1b..0000000000 --- a/crates/time_format/src/time_format.rs +++ /dev/null @@ -1,973 +0,0 @@ -use time::{OffsetDateTime, UtcOffset}; - -/// The formatting style for a timestamp. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum TimestampFormat { - /// Formats the timestamp as an absolute time, e.g. "2021-12-31 3:00AM". - Absolute, - /// Formats the timestamp as an absolute time. - /// If the message is from today or yesterday the date will be replaced with "Today at x" or "Yesterday at x" respectively. - /// E.g. "Today at 12:00 PM", "Yesterday at 11:00 AM", "2021-12-31 3:00AM". - EnhancedAbsolute, - /// Formats the timestamp as an absolute time, using month name, day of month, year. e.g. "Feb. 24, 2024". - MediumAbsolute, - /// Formats the timestamp as a relative time, e.g. "just now", "1 minute ago", "2 hours ago", "2 months ago". - Relative, -} - -/// Formats a timestamp, which respects the user's date and time preferences/custom format. -pub fn format_localized_timestamp( - timestamp: OffsetDateTime, - reference: OffsetDateTime, - timezone: UtcOffset, - format: TimestampFormat, -) -> String { - let timestamp_local = timestamp.to_offset(timezone); - let reference_local = reference.to_offset(timezone); - format_local_timestamp(timestamp_local, reference_local, format) -} - -/// Formats a timestamp, which respects the user's date and time preferences/custom format. -pub fn format_local_timestamp( - timestamp: OffsetDateTime, - reference: OffsetDateTime, - format: TimestampFormat, -) -> String { - match format { - TimestampFormat::Absolute => format_absolute_timestamp(timestamp, reference, false), - TimestampFormat::EnhancedAbsolute => format_absolute_timestamp(timestamp, reference, true), - TimestampFormat::MediumAbsolute => format_absolute_timestamp_medium(timestamp, reference), - TimestampFormat::Relative => format_relative_time(timestamp, reference) - .unwrap_or_else(|| format_relative_date(timestamp, reference)), - } -} - -/// Formats the date component of a timestamp -pub fn format_date( - timestamp: OffsetDateTime, - reference: OffsetDateTime, - enhanced_formatting: bool, -) -> String { - format_absolute_date(timestamp, reference, enhanced_formatting) -} - -/// Formats the time component of a timestamp -pub fn format_time(timestamp: OffsetDateTime) -> String { - format_absolute_time(timestamp) -} - -/// Formats the date component of a timestamp in medium style -pub fn format_date_medium( - timestamp: OffsetDateTime, - reference: OffsetDateTime, - enhanced_formatting: bool, -) -> String { - format_absolute_date_medium(timestamp, reference, enhanced_formatting) -} - -fn format_absolute_date( - timestamp: OffsetDateTime, - reference: OffsetDateTime, - #[allow(unused_variables)] enhanced_date_formatting: bool, -) -> String { - #[cfg(target_os = "macos")] - { - if !enhanced_date_formatting { - return macos::format_date(×tamp); - } - - let timestamp_date = timestamp.date(); - let reference_date = reference.date(); - if timestamp_date == reference_date { - "Today".to_string() - } else if reference_date.previous_day() == Some(timestamp_date) { - "Yesterday".to_string() - } else { - macos::format_date(×tamp) - } - } - #[cfg(not(target_os = "macos"))] - { - // todo(linux) respect user's date/time preferences - // todo(windows) respect user's date/time preferences - let current_locale = CURRENT_LOCALE - .get_or_init(|| sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"))); - format_timestamp_naive_date( - timestamp, - reference, - is_12_hour_time_by_locale(current_locale.as_str()), - ) - } -} - -fn format_absolute_time(timestamp: OffsetDateTime) -> String { - #[cfg(target_os = "macos")] - { - macos::format_time(×tamp) - } - #[cfg(not(target_os = "macos"))] - { - // todo(linux) respect user's date/time preferences - // todo(windows) respect user's date/time preferences - let current_locale = CURRENT_LOCALE - .get_or_init(|| sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"))); - format_timestamp_naive_time( - timestamp, - is_12_hour_time_by_locale(current_locale.as_str()), - ) - } -} - -fn format_absolute_timestamp( - timestamp: OffsetDateTime, - reference: OffsetDateTime, - #[allow(unused_variables)] enhanced_date_formatting: bool, -) -> String { - #[cfg(target_os = "macos")] - { - if !enhanced_date_formatting { - return format!( - "{} {}", - format_absolute_date(timestamp, reference, enhanced_date_formatting), - format_absolute_time(timestamp) - ); - } - - let timestamp_date = timestamp.date(); - let reference_date = reference.date(); - if timestamp_date == reference_date { - format!("Today at {}", format_absolute_time(timestamp)) - } else if reference_date.previous_day() == Some(timestamp_date) { - format!("Yesterday at {}", format_absolute_time(timestamp)) - } else { - format!( - "{} {}", - format_absolute_date(timestamp, reference, enhanced_date_formatting), - format_absolute_time(timestamp) - ) - } - } - #[cfg(not(target_os = "macos"))] - { - // todo(linux) respect user's date/time preferences - // todo(windows) respect user's date/time preferences - format_timestamp_fallback(timestamp, reference) - } -} - -fn format_absolute_date_medium( - timestamp: OffsetDateTime, - reference: OffsetDateTime, - enhanced_formatting: bool, -) -> String { - #[cfg(target_os = "macos")] - { - if !enhanced_formatting { - return macos::format_date_medium(×tamp); - } - - let timestamp_date = timestamp.date(); - let reference_date = reference.date(); - if timestamp_date == reference_date { - "Today".to_string() - } else if reference_date.previous_day() == Some(timestamp_date) { - "Yesterday".to_string() - } else { - macos::format_date_medium(×tamp) - } - } - #[cfg(not(target_os = "macos"))] - { - // todo(linux) respect user's date/time preferences - // todo(windows) respect user's date/time preferences - let current_locale = CURRENT_LOCALE - .get_or_init(|| sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"))); - if !enhanced_formatting { - return format_timestamp_naive_date_medium( - timestamp, - is_12_hour_time_by_locale(current_locale.as_str()), - ); - } - - let timestamp_date = timestamp.date(); - let reference_date = reference.date(); - if timestamp_date == reference_date { - "Today".to_string() - } else if reference_date.previous_day() == Some(timestamp_date) { - "Yesterday".to_string() - } else { - format_timestamp_naive_date_medium( - timestamp, - is_12_hour_time_by_locale(current_locale.as_str()), - ) - } - } -} - -fn format_absolute_timestamp_medium( - timestamp: OffsetDateTime, - reference: OffsetDateTime, -) -> String { - #[cfg(target_os = "macos")] - { - format_absolute_date_medium(timestamp, reference, false) - } - #[cfg(not(target_os = "macos"))] - { - // todo(linux) respect user's date/time preferences - // todo(windows) respect user's date/time preferences - format_timestamp_fallback(timestamp, reference) - } -} - -fn format_relative_time(timestamp: OffsetDateTime, reference: OffsetDateTime) -> Option { - let difference = reference - timestamp; - let minutes = difference.whole_minutes(); - match minutes { - 0 => Some("Just now".to_string()), - 1 => Some("1 minute ago".to_string()), - 2..=59 => Some(format!("{} minutes ago", minutes)), - _ => { - let hours = difference.whole_hours(); - match hours { - 1 => Some("1 hour ago".to_string()), - 2..=23 => Some(format!("{} hours ago", hours)), - _ => None, - } - } - } -} - -fn format_relative_date(timestamp: OffsetDateTime, reference: OffsetDateTime) -> String { - let timestamp_date = timestamp.date(); - let reference_date = reference.date(); - let difference = reference_date - timestamp_date; - let days = difference.whole_days(); - match days { - 0 => "Today".to_string(), - 1 => "Yesterday".to_string(), - 2..=6 => format!("{} days ago", days), - _ => { - let weeks = difference.whole_weeks(); - match weeks { - 1 => "1 week ago".to_string(), - 2..=4 => format!("{} weeks ago", weeks), - _ => { - let month_diff = calculate_month_difference(timestamp, reference); - match month_diff { - 0..=1 => "1 month ago".to_string(), - 2..=11 => format!("{} months ago", month_diff), - _ => { - let timestamp_year = timestamp_date.year(); - let reference_year = reference_date.year(); - let years = reference_year - timestamp_year; - match years { - 1 => "1 year ago".to_string(), - _ => format!("{} years ago", years), - } - } - } - } - } - } - } -} - -/// Calculates the difference in months between two timestamps. -/// The reference timestamp should always be greater than the timestamp. -fn calculate_month_difference(timestamp: OffsetDateTime, reference: OffsetDateTime) -> usize { - let timestamp_year = timestamp.year(); - let reference_year = reference.year(); - let timestamp_month: u8 = timestamp.month().into(); - let reference_month: u8 = reference.month().into(); - - let month_diff = if reference_month >= timestamp_month { - reference_month as usize - timestamp_month as usize - } else { - 12 - timestamp_month as usize + reference_month as usize - }; - - let year_diff = (reference_year - timestamp_year) as usize; - if year_diff == 0 { - reference_month as usize - timestamp_month as usize - } else if month_diff == 0 { - year_diff * 12 - } else if timestamp_month > reference_month { - (year_diff - 1) * 12 + month_diff - } else { - year_diff * 12 + month_diff - } -} - -/// Formats a timestamp, which is either in 12-hour or 24-hour time format. -/// Note: -/// This function does not respect the user's date and time preferences. -/// This should only be used as a fallback mechanism when the OS time formatting fails. -fn format_timestamp_naive_time(timestamp_local: OffsetDateTime, is_12_hour_time: bool) -> String { - let timestamp_local_hour = timestamp_local.hour(); - let timestamp_local_minute = timestamp_local.minute(); - - let (hour, meridiem) = if is_12_hour_time { - let meridiem = if timestamp_local_hour >= 12 { - "PM" - } else { - "AM" - }; - - let hour_12 = match timestamp_local_hour { - 0 => 12, // Midnight - 13..=23 => timestamp_local_hour - 12, // PM hours - _ => timestamp_local_hour, // AM hours - }; - - (hour_12, Some(meridiem)) - } else { - (timestamp_local_hour, None) - }; - - match meridiem { - Some(meridiem) => format!("{}:{:02} {}", hour, timestamp_local_minute, meridiem), - None => format!("{:02}:{:02}", hour, timestamp_local_minute), - } -} - -#[cfg(not(target_os = "macos"))] -fn format_timestamp_naive_date( - timestamp_local: OffsetDateTime, - reference_local: OffsetDateTime, - is_12_hour_time: bool, -) -> String { - let reference_local_date = reference_local.date(); - let timestamp_local_date = timestamp_local.date(); - - if timestamp_local_date == reference_local_date { - "Today".to_string() - } else if reference_local_date.previous_day() == Some(timestamp_local_date) { - "Yesterday".to_string() - } else { - match is_12_hour_time { - true => format!( - "{:02}/{:02}/{}", - timestamp_local_date.month() as u32, - timestamp_local_date.day(), - timestamp_local_date.year() - ), - false => format!( - "{:02}/{:02}/{}", - timestamp_local_date.day(), - timestamp_local_date.month() as u32, - timestamp_local_date.year() - ), - } - } -} - -#[cfg(not(target_os = "macos"))] -fn format_timestamp_naive_date_medium( - timestamp_local: OffsetDateTime, - is_12_hour_time: bool, -) -> String { - let timestamp_local_date = timestamp_local.date(); - - match is_12_hour_time { - true => format!( - "{:02}/{:02}/{}", - timestamp_local_date.month() as u32, - timestamp_local_date.day(), - timestamp_local_date.year() - ), - false => format!( - "{:02}/{:02}/{}", - timestamp_local_date.day(), - timestamp_local_date.month() as u32, - timestamp_local_date.year() - ), - } -} - -pub fn format_timestamp_naive( - timestamp_local: OffsetDateTime, - reference_local: OffsetDateTime, - is_12_hour_time: bool, -) -> String { - let formatted_time = format_timestamp_naive_time(timestamp_local, is_12_hour_time); - let reference_local_date = reference_local.date(); - let timestamp_local_date = timestamp_local.date(); - - if timestamp_local_date == reference_local_date { - format!("Today at {}", formatted_time) - } else if reference_local_date.previous_day() == Some(timestamp_local_date) { - format!("Yesterday at {}", formatted_time) - } else { - let formatted_date = match is_12_hour_time { - true => format!( - "{:02}/{:02}/{}", - timestamp_local_date.month() as u32, - timestamp_local_date.day(), - timestamp_local_date.year() - ), - false => format!( - "{:02}/{:02}/{}", - timestamp_local_date.day(), - timestamp_local_date.month() as u32, - timestamp_local_date.year() - ), - }; - format!("{} {}", formatted_date, formatted_time) - } -} - -#[cfg(not(target_os = "macos"))] -static CURRENT_LOCALE: std::sync::OnceLock = std::sync::OnceLock::new(); - -#[cfg(not(target_os = "macos"))] -fn format_timestamp_fallback(timestamp: OffsetDateTime, reference: OffsetDateTime) -> String { - let current_locale = CURRENT_LOCALE - .get_or_init(|| sys_locale::get_locale().unwrap_or_else(|| String::from("en-US"))); - - let is_12_hour_time = is_12_hour_time_by_locale(current_locale.as_str()); - format_timestamp_naive(timestamp, reference, is_12_hour_time) -} - -/// Returns `true` if the locale is recognized as a 12-hour time locale. -#[cfg(not(target_os = "macos"))] -fn is_12_hour_time_by_locale(locale: &str) -> bool { - [ - "es-MX", "es-CO", "es-SV", "es-NI", - "es-HN", // Mexico, Colombia, El Salvador, Nicaragua, Honduras - "en-US", "en-CA", "en-AU", "en-NZ", // U.S, Canada, Australia, New Zealand - "ar-SA", "ar-EG", "ar-JO", // Saudi Arabia, Egypt, Jordan - "en-IN", "hi-IN", // India, Hindu - "en-PK", "ur-PK", // Pakistan, Urdu - "en-PH", "fil-PH", // Philippines, Filipino - "bn-BD", "ccp-BD", // Bangladesh, Chakma - "en-IE", "ga-IE", // Ireland, Irish - "en-MY", "ms-MY", // Malaysia, Malay - ] - .contains(&locale) -} - -#[cfg(target_os = "macos")] -mod macos { - use core_foundation::base::TCFType; - use core_foundation::date::CFAbsoluteTime; - use core_foundation::string::CFString; - use core_foundation_sys::date_formatter::CFDateFormatterCreateStringWithAbsoluteTime; - use core_foundation_sys::date_formatter::CFDateFormatterRef; - use core_foundation_sys::locale::CFLocaleRef; - use core_foundation_sys::{ - base::kCFAllocatorDefault, - date_formatter::{ - CFDateFormatterCreate, kCFDateFormatterMediumStyle, kCFDateFormatterNoStyle, - kCFDateFormatterShortStyle, - }, - locale::CFLocaleCopyCurrent, - }; - - pub fn format_time(timestamp: &time::OffsetDateTime) -> String { - format_with_date_formatter(timestamp, TIME_FORMATTER.with(|f| *f)) - } - - pub fn format_date(timestamp: &time::OffsetDateTime) -> String { - format_with_date_formatter(timestamp, DATE_FORMATTER.with(|f| *f)) - } - - pub fn format_date_medium(timestamp: &time::OffsetDateTime) -> String { - format_with_date_formatter(timestamp, MEDIUM_DATE_FORMATTER.with(|f| *f)) - } - - fn format_with_date_formatter( - timestamp: &time::OffsetDateTime, - fmt: CFDateFormatterRef, - ) -> String { - const UNIX_TO_CF_ABSOLUTE_TIME_OFFSET: i64 = 978307200; - // Convert timestamp to macOS absolute time - let timestamp_macos = timestamp.unix_timestamp() - UNIX_TO_CF_ABSOLUTE_TIME_OFFSET; - let cf_absolute_time = timestamp_macos as CFAbsoluteTime; - unsafe { - let s = CFDateFormatterCreateStringWithAbsoluteTime( - kCFAllocatorDefault, - fmt, - cf_absolute_time, - ); - CFString::wrap_under_create_rule(s).to_string() - } - } - - thread_local! { - static CURRENT_LOCALE: CFLocaleRef = unsafe { CFLocaleCopyCurrent() }; - static TIME_FORMATTER: CFDateFormatterRef = unsafe { - CFDateFormatterCreate( - kCFAllocatorDefault, - CURRENT_LOCALE.with(|locale| *locale), - kCFDateFormatterNoStyle, - kCFDateFormatterShortStyle, - ) - }; - static DATE_FORMATTER: CFDateFormatterRef = unsafe { - CFDateFormatterCreate( - kCFAllocatorDefault, - CURRENT_LOCALE.with(|locale| *locale), - kCFDateFormatterShortStyle, - kCFDateFormatterNoStyle, - ) - }; - - static MEDIUM_DATE_FORMATTER: CFDateFormatterRef = unsafe { - CFDateFormatterCreate( - kCFAllocatorDefault, - CURRENT_LOCALE.with(|locale| *locale), - kCFDateFormatterMediumStyle, - kCFDateFormatterNoStyle, - ) - }; - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_format_date() { - let reference = create_offset_datetime(1990, 4, 12, 10, 30, 0); - - // Test with same date (today) - let timestamp_today = create_offset_datetime(1990, 4, 12, 9, 30, 0); - assert_eq!(format_date(timestamp_today, reference, true), "Today"); - - // Test with previous day (yesterday) - let timestamp_yesterday = create_offset_datetime(1990, 4, 11, 9, 30, 0); - assert_eq!( - format_date(timestamp_yesterday, reference, true), - "Yesterday" - ); - - // Test with other date - let timestamp_other = create_offset_datetime(1990, 4, 10, 9, 30, 0); - let result = format_date(timestamp_other, reference, true); - assert!(!result.is_empty()); - assert_ne!(result, "Today"); - assert_ne!(result, "Yesterday"); - } - - #[test] - fn test_format_time() { - let timestamp = create_offset_datetime(1990, 4, 12, 9, 30, 0); - - // We can't assert the exact output as it depends on the platform and locale - // But we can at least confirm it doesn't panic and returns a non-empty string - let result = format_time(timestamp); - assert!(!result.is_empty()); - } - - #[test] - fn test_format_date_medium() { - let reference = create_offset_datetime(1990, 4, 12, 10, 30, 0); - let timestamp = create_offset_datetime(1990, 4, 12, 9, 30, 0); - - // Test with enhanced formatting (today) - let result_enhanced = format_date_medium(timestamp, reference, true); - assert_eq!(result_enhanced, "Today"); - - // Test with standard formatting - let result_standard = format_date_medium(timestamp, reference, false); - assert!(!result_standard.is_empty()); - - // Test yesterday with enhanced formatting - let timestamp_yesterday = create_offset_datetime(1990, 4, 11, 9, 30, 0); - let result_yesterday = format_date_medium(timestamp_yesterday, reference, true); - assert_eq!(result_yesterday, "Yesterday"); - - // Test other date with enhanced formatting - let timestamp_other = create_offset_datetime(1990, 4, 10, 9, 30, 0); - let result_other = format_date_medium(timestamp_other, reference, true); - assert!(!result_other.is_empty()); - assert_ne!(result_other, "Today"); - assert_ne!(result_other, "Yesterday"); - } - - #[test] - fn test_format_absolute_time() { - let timestamp = create_offset_datetime(1990, 4, 12, 9, 30, 0); - - // We can't assert the exact output as it depends on the platform and locale - // But we can at least confirm it doesn't panic and returns a non-empty string - let result = format_absolute_time(timestamp); - assert!(!result.is_empty()); - } - - #[test] - fn test_format_absolute_date() { - let reference = create_offset_datetime(1990, 4, 12, 10, 30, 0); - - // Test with same date (today) - let timestamp_today = create_offset_datetime(1990, 4, 12, 9, 30, 0); - assert_eq!( - format_absolute_date(timestamp_today, reference, true), - "Today" - ); - - // Test with previous day (yesterday) - let timestamp_yesterday = create_offset_datetime(1990, 4, 11, 9, 30, 0); - assert_eq!( - format_absolute_date(timestamp_yesterday, reference, true), - "Yesterday" - ); - - // Test with other date - let timestamp_other = create_offset_datetime(1990, 4, 10, 9, 30, 0); - let result = format_absolute_date(timestamp_other, reference, true); - assert!(!result.is_empty()); - assert_ne!(result, "Today"); - assert_ne!(result, "Yesterday"); - } - - #[test] - fn test_format_absolute_date_medium() { - let reference = create_offset_datetime(1990, 4, 12, 10, 30, 0); - let timestamp = create_offset_datetime(1990, 4, 12, 9, 30, 0); - - // Test with enhanced formatting (today) - let result_enhanced = format_absolute_date_medium(timestamp, reference, true); - assert_eq!(result_enhanced, "Today"); - - // Test with standard formatting - let result_standard = format_absolute_date_medium(timestamp, reference, false); - assert!(!result_standard.is_empty()); - - // Test yesterday with enhanced formatting - let timestamp_yesterday = create_offset_datetime(1990, 4, 11, 9, 30, 0); - let result_yesterday = format_absolute_date_medium(timestamp_yesterday, reference, true); - assert_eq!(result_yesterday, "Yesterday"); - } - - #[test] - fn test_format_timestamp_naive_time() { - let timestamp = create_offset_datetime(1990, 4, 12, 9, 30, 0); - assert_eq!(format_timestamp_naive_time(timestamp, true), "9:30 AM"); - assert_eq!(format_timestamp_naive_time(timestamp, false), "09:30"); - - let timestamp_pm = create_offset_datetime(1990, 4, 12, 15, 45, 0); - assert_eq!(format_timestamp_naive_time(timestamp_pm, true), "3:45 PM"); - assert_eq!(format_timestamp_naive_time(timestamp_pm, false), "15:45"); - } - - #[test] - fn test_format_24_hour_time() { - let reference = create_offset_datetime(1990, 4, 12, 16, 45, 0); - let timestamp = create_offset_datetime(1990, 4, 12, 15, 30, 0); - - assert_eq!( - format_timestamp_naive(timestamp, reference, false), - "Today at 15:30" - ); - } - - #[test] - fn test_format_today() { - let reference = create_offset_datetime(1990, 4, 12, 16, 45, 0); - let timestamp = create_offset_datetime(1990, 4, 12, 15, 30, 0); - - assert_eq!( - format_timestamp_naive(timestamp, reference, true), - "Today at 3:30 PM" - ); - } - - #[test] - fn test_format_yesterday() { - let reference = create_offset_datetime(1990, 4, 12, 10, 30, 0); - let timestamp = create_offset_datetime(1990, 4, 11, 9, 0, 0); - - assert_eq!( - format_timestamp_naive(timestamp, reference, true), - "Yesterday at 9:00 AM" - ); - } - - #[test] - fn test_format_yesterday_less_than_24_hours_ago() { - let reference = create_offset_datetime(1990, 4, 12, 19, 59, 0); - let timestamp = create_offset_datetime(1990, 4, 11, 20, 0, 0); - - assert_eq!( - format_timestamp_naive(timestamp, reference, true), - "Yesterday at 8:00 PM" - ); - } - - #[test] - fn test_format_yesterday_more_than_24_hours_ago() { - let reference = create_offset_datetime(1990, 4, 12, 19, 59, 0); - let timestamp = create_offset_datetime(1990, 4, 11, 18, 0, 0); - - assert_eq!( - format_timestamp_naive(timestamp, reference, true), - "Yesterday at 6:00 PM" - ); - } - - #[test] - fn test_format_yesterday_over_midnight() { - let reference = create_offset_datetime(1990, 4, 12, 0, 5, 0); - let timestamp = create_offset_datetime(1990, 4, 11, 23, 55, 0); - - assert_eq!( - format_timestamp_naive(timestamp, reference, true), - "Yesterday at 11:55 PM" - ); - } - - #[test] - fn test_format_yesterday_over_month() { - let reference = create_offset_datetime(1990, 4, 2, 9, 0, 0); - let timestamp = create_offset_datetime(1990, 4, 1, 20, 0, 0); - - assert_eq!( - format_timestamp_naive(timestamp, reference, true), - "Yesterday at 8:00 PM" - ); - } - - #[test] - fn test_format_before_yesterday() { - let reference = create_offset_datetime(1990, 4, 12, 10, 30, 0); - let timestamp = create_offset_datetime(1990, 4, 10, 20, 20, 0); - - assert_eq!( - format_timestamp_naive(timestamp, reference, true), - "04/10/1990 8:20 PM" - ); - } - - #[test] - fn test_relative_format_minutes() { - let reference = create_offset_datetime(1990, 4, 12, 23, 0, 0); - let mut current_timestamp = reference; - - let mut next_minute = || { - current_timestamp = if current_timestamp.minute() == 0 { - current_timestamp - .replace_hour(current_timestamp.hour() - 1) - .unwrap() - .replace_minute(59) - .unwrap() - } else { - current_timestamp - .replace_minute(current_timestamp.minute() - 1) - .unwrap() - }; - current_timestamp - }; - - assert_eq!( - format_relative_time(reference, reference), - Some("Just now".to_string()) - ); - - assert_eq!( - format_relative_time(next_minute(), reference), - Some("1 minute ago".to_string()) - ); - - for i in 2..=59 { - assert_eq!( - format_relative_time(next_minute(), reference), - Some(format!("{} minutes ago", i)) - ); - } - - assert_eq!( - format_relative_time(next_minute(), reference), - Some("1 hour ago".to_string()) - ); - } - - #[test] - fn test_relative_format_hours() { - let reference = create_offset_datetime(1990, 4, 12, 23, 0, 0); - let mut current_timestamp = reference; - - let mut next_hour = || { - current_timestamp = if current_timestamp.hour() == 0 { - let date = current_timestamp.date().previous_day().unwrap(); - current_timestamp.replace_date(date) - } else { - current_timestamp - .replace_hour(current_timestamp.hour() - 1) - .unwrap() - }; - current_timestamp - }; - - assert_eq!( - format_relative_time(next_hour(), reference), - Some("1 hour ago".to_string()) - ); - - for i in 2..=23 { - assert_eq!( - format_relative_time(next_hour(), reference), - Some(format!("{} hours ago", i)) - ); - } - - assert_eq!(format_relative_time(next_hour(), reference), None); - } - - #[test] - fn test_relative_format_days() { - let reference = create_offset_datetime(1990, 4, 12, 23, 0, 0); - let mut current_timestamp = reference; - - let mut next_day = || { - let date = current_timestamp.date().previous_day().unwrap(); - current_timestamp = current_timestamp.replace_date(date); - current_timestamp - }; - - assert_eq!( - format_relative_date(reference, reference), - "Today".to_string() - ); - - assert_eq!( - format_relative_date(next_day(), reference), - "Yesterday".to_string() - ); - - for i in 2..=6 { - assert_eq!( - format_relative_date(next_day(), reference), - format!("{} days ago", i) - ); - } - - assert_eq!(format_relative_date(next_day(), reference), "1 week ago"); - } - - #[test] - fn test_relative_format_weeks() { - let reference = create_offset_datetime(1990, 4, 12, 23, 0, 0); - let mut current_timestamp = reference; - - let mut next_week = || { - for _ in 0..7 { - let date = current_timestamp.date().previous_day().unwrap(); - current_timestamp = current_timestamp.replace_date(date); - } - current_timestamp - }; - - assert_eq!( - format_relative_date(next_week(), reference), - "1 week ago".to_string() - ); - - for i in 2..=4 { - assert_eq!( - format_relative_date(next_week(), reference), - format!("{} weeks ago", i) - ); - } - - assert_eq!(format_relative_date(next_week(), reference), "1 month ago"); - } - - #[test] - fn test_relative_format_months() { - let reference = create_offset_datetime(1990, 4, 12, 23, 0, 0); - let mut current_timestamp = reference; - - let mut next_month = || { - if current_timestamp.month() == time::Month::January { - current_timestamp = current_timestamp - .replace_month(time::Month::December) - .unwrap() - .replace_year(current_timestamp.year() - 1) - .unwrap(); - } else { - current_timestamp = current_timestamp - .replace_month(current_timestamp.month().previous()) - .unwrap(); - } - current_timestamp - }; - - assert_eq!( - format_relative_date(next_month(), reference), - "4 weeks ago".to_string() - ); - - for i in 2..=11 { - assert_eq!( - format_relative_date(next_month(), reference), - format!("{} months ago", i) - ); - } - - assert_eq!(format_relative_date(next_month(), reference), "1 year ago"); - } - - #[test] - fn test_calculate_month_difference() { - let reference = create_offset_datetime(1990, 4, 12, 23, 0, 0); - - assert_eq!(calculate_month_difference(reference, reference), 0); - - assert_eq!( - calculate_month_difference(create_offset_datetime(1990, 1, 12, 23, 0, 0), reference), - 3 - ); - - assert_eq!( - calculate_month_difference(create_offset_datetime(1989, 11, 12, 23, 0, 0), reference), - 5 - ); - - assert_eq!( - calculate_month_difference(create_offset_datetime(1989, 4, 12, 23, 0, 0), reference), - 12 - ); - - assert_eq!( - calculate_month_difference(create_offset_datetime(1989, 3, 12, 23, 0, 0), reference), - 13 - ); - - assert_eq!( - calculate_month_difference(create_offset_datetime(1987, 5, 12, 23, 0, 0), reference), - 35 - ); - - assert_eq!( - calculate_month_difference(create_offset_datetime(1987, 4, 12, 23, 0, 0), reference), - 36 - ); - - assert_eq!( - calculate_month_difference(create_offset_datetime(1987, 3, 12, 23, 0, 0), reference), - 37 - ); - } - - fn test_timezone() -> UtcOffset { - UtcOffset::from_hms(0, 0, 0).expect("Valid timezone offset") - } - - fn create_offset_datetime( - year: i32, - month: u8, - day: u8, - hour: u8, - minute: u8, - second: u8, - ) -> OffsetDateTime { - let date = time::Date::from_calendar_date(year, time::Month::try_from(month).unwrap(), day) - .unwrap(); - let time = time::Time::from_hms(hour, minute, second).unwrap(); - let date = date.with_time(time).assume_utc(); // Assume UTC for simplicity - date.to_offset(test_timezone()) - } -} diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml deleted file mode 100644 index 6d5d0ce170..0000000000 --- a/crates/title_bar/Cargo.toml +++ /dev/null @@ -1,71 +0,0 @@ -[package] -name = "title_bar" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/title_bar.rs" -doctest = false - -[features] -default = [] -stories = ["dep:story"] -test-support = [ - "call/test-support", - "client/test-support", - "collections/test-support", - "gpui/test-support", - "http_client/test-support", - "project/test-support", - "util/test-support", - "workspace/test-support", -] - -[dependencies] -anyhow.workspace = true -auto_update.workspace = true -call.workspace = true -channel.workspace = true -chrono.workspace = true -client.workspace = true -cloud_llm_client.workspace = true -db.workspace = true -gpui = { workspace = true, features = ["screen-capture"] } -notifications.workspace = true -project.workspace = true -remote.workspace = true -rpc.workspace = true -schemars.workspace = true -serde.workspace = true -settings.workspace = true -smallvec.workspace = true -story = { workspace = true, optional = true } -telemetry.workspace = true -theme.workspace = true -ui.workspace = true -util.workspace = true -workspace.workspace = true -zed_actions.workspace = true - -[target.'cfg(windows)'.dependencies] -windows.workspace = true - -[dev-dependencies] -call = { workspace = true, features = ["test-support"] } -client = { workspace = true, features = ["test-support"] } -collections = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -http_client = { workspace = true, features = ["test-support"] } -notifications = { workspace = true, features = ["test-support"] } -pretty_assertions.workspace = true -project = { workspace = true, features = ["test-support"] } -rpc = { workspace = true, features = ["test-support"] } -settings = { workspace = true, features = ["test-support"] } -tree-sitter-md.workspace = true -util = { workspace = true, features = ["test-support"] } -workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/title_bar/LICENSE-GPL b/crates/title_bar/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/title_bar/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/title_bar/src/application_menu.rs b/crates/title_bar/src/application_menu.rs deleted file mode 100644 index 817b73c45e..0000000000 --- a/crates/title_bar/src/application_menu.rs +++ /dev/null @@ -1,324 +0,0 @@ -use gpui::{Entity, OwnedMenu, OwnedMenuItem}; -use settings::Settings; - -#[cfg(not(target_os = "macos"))] -use gpui::{Action, actions}; - -#[cfg(not(target_os = "macos"))] -use schemars::JsonSchema; -#[cfg(not(target_os = "macos"))] -use serde::Deserialize; - -use smallvec::SmallVec; -use ui::{ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*}; - -use crate::title_bar_settings::TitleBarSettings; - -#[cfg(not(target_os = "macos"))] -actions!( - app_menu, - [ - /// Navigates to the menu item on the right. - ActivateMenuRight, - /// Navigates to the menu item on the left. - ActivateMenuLeft - ] -); - -#[cfg(not(target_os = "macos"))] -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Default, Action)] -#[action(namespace = app_menu)] -pub struct OpenApplicationMenu(String); - -#[cfg(not(target_os = "macos"))] -pub enum ActivateDirection { - Left, - Right, -} - -#[derive(Clone)] -struct MenuEntry { - menu: OwnedMenu, - handle: PopoverMenuHandle, -} - -pub struct ApplicationMenu { - entries: SmallVec<[MenuEntry; 8]>, - pending_menu_open: Option, -} - -impl ApplicationMenu { - pub fn new(_: &mut Window, cx: &mut Context) -> Self { - let menus = cx.get_menus().unwrap_or_default(); - Self { - entries: menus - .into_iter() - .map(|menu| MenuEntry { - menu, - handle: PopoverMenuHandle::default(), - }) - .collect(), - pending_menu_open: None, - } - } - - fn sanitize_menu_items(items: Vec) -> Vec { - let mut cleaned = Vec::new(); - let mut last_was_separator = false; - - for item in items { - match item { - OwnedMenuItem::Separator => { - if !last_was_separator { - cleaned.push(item); - last_was_separator = true; - } - } - OwnedMenuItem::Submenu(submenu) => { - // Skip empty submenus - if !submenu.items.is_empty() { - cleaned.push(OwnedMenuItem::Submenu(submenu)); - last_was_separator = false; - } - } - item => { - cleaned.push(item); - last_was_separator = false; - } - } - } - - // Remove trailing separator - if let Some(OwnedMenuItem::Separator) = cleaned.last() { - cleaned.pop(); - } - - cleaned - } - - fn build_menu_from_items( - entry: MenuEntry, - window: &mut Window, - cx: &mut App, - ) -> Entity { - ContextMenu::build(window, cx, |menu, window, cx| { - // Grab current focus handle so menu can shown items in context with the focused element - let menu = menu.when_some(window.focused(cx), |menu, focused| menu.context(focused)); - let sanitized_items = Self::sanitize_menu_items(entry.menu.items); - - sanitized_items - .into_iter() - .fold(menu, |menu, item| match item { - OwnedMenuItem::Separator => menu.separator(), - OwnedMenuItem::Action { - name, - action, - checked, - .. - } => menu.action_checked(name, action, checked), - OwnedMenuItem::Submenu(submenu) => { - submenu - .items - .into_iter() - .fold(menu, |menu, item| match item { - OwnedMenuItem::Separator => menu.separator(), - OwnedMenuItem::Action { - name, - action, - checked, - .. - } => menu.action_checked(name, action, checked), - OwnedMenuItem::Submenu(_) => menu, - OwnedMenuItem::SystemMenu(_) => { - // A system menu doesn't make sense in this context, so ignore it - menu - } - }) - } - OwnedMenuItem::SystemMenu(_) => { - // A system menu doesn't make sense in this context, so ignore it - menu - } - }) - }) - } - - fn render_application_menu(&self, entry: &MenuEntry) -> impl IntoElement { - let handle = entry.handle.clone(); - - let menu_name = entry.menu.name.clone(); - let entry = entry.clone(); - - // Application menu must have same ids as first menu item in standard menu - div() - .id(format!("{}-menu-item", menu_name)) - .occlude() - .child( - PopoverMenu::new(format!("{}-menu-popover", menu_name)) - .menu(move |window, cx| { - Self::build_menu_from_items(entry.clone(), window, cx).into() - }) - .trigger_with_tooltip( - IconButton::new( - SharedString::from(format!("{}-menu-trigger", menu_name)), - ui::IconName::Menu, - ) - .style(ButtonStyle::Subtle) - .icon_size(IconSize::Small), - Tooltip::text("Open Application Menu"), - ) - .with_handle(handle), - ) - } - - fn render_standard_menu(&self, entry: &MenuEntry) -> impl IntoElement { - let current_handle = entry.handle.clone(); - - let menu_name = entry.menu.name.clone(); - let entry = entry.clone(); - - let all_handles: Vec<_> = self - .entries - .iter() - .map(|entry| entry.handle.clone()) - .collect(); - - div() - .id(format!("{}-menu-item", menu_name)) - .occlude() - .child( - PopoverMenu::new(format!("{}-menu-popover", menu_name)) - .menu(move |window, cx| { - Self::build_menu_from_items(entry.clone(), window, cx).into() - }) - .trigger( - Button::new( - SharedString::from(format!("{}-menu-trigger", menu_name)), - menu_name, - ) - .style(ButtonStyle::Subtle) - .label_size(LabelSize::Small), - ) - .with_handle(current_handle.clone()), - ) - .on_hover(move |hover_enter, window, cx| { - if *hover_enter && !current_handle.is_deployed() { - all_handles.iter().for_each(|h| h.hide(cx)); - - // We need to defer this so that this menu handle can take focus from the previous menu - let handle = current_handle.clone(); - window.defer(cx, move |window, cx| handle.show(window, cx)); - } - }) - } - - #[cfg(not(target_os = "macos"))] - pub fn open_menu( - &mut self, - action: &OpenApplicationMenu, - _window: &mut Window, - _cx: &mut Context, - ) { - self.pending_menu_open = Some(action.0.clone()); - } - - #[cfg(not(target_os = "macos"))] - pub fn navigate_menus_in_direction( - &mut self, - direction: ActivateDirection, - window: &mut Window, - cx: &mut Context, - ) { - let current_index = self - .entries - .iter() - .position(|entry| entry.handle.is_deployed()); - let Some(current_index) = current_index else { - return; - }; - - let next_index = match direction { - ActivateDirection::Left => { - if current_index == 0 { - self.entries.len() - 1 - } else { - current_index - 1 - } - } - ActivateDirection::Right => { - if current_index == self.entries.len() - 1 { - 0 - } else { - current_index + 1 - } - } - }; - - self.entries[current_index].handle.hide(cx); - - // We need to defer this so that this menu handle can take focus from the previous menu - let next_handle = self.entries[next_index].handle.clone(); - cx.defer_in(window, move |_, window, cx| next_handle.show(window, cx)); - } - - pub fn all_menus_shown(&self, cx: &mut Context) -> bool { - show_menus(cx) - || self.entries.iter().any(|entry| entry.handle.is_deployed()) - || self.pending_menu_open.is_some() - } -} - -pub(crate) fn show_menus(cx: &mut App) -> bool { - TitleBarSettings::get_global(cx).show_menus - && (cfg!(not(target_os = "macos")) || option_env!("ZED_USE_CROSS_PLATFORM_MENU").is_some()) -} - -impl Render for ApplicationMenu { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let all_menus_shown = self.all_menus_shown(cx); - - if let Some(pending_menu_open) = self.pending_menu_open.take() - && let Some(entry) = self - .entries - .iter() - .find(|entry| entry.menu.name == pending_menu_open && !entry.handle.is_deployed()) - { - let handle_to_show = entry.handle.clone(); - let handles_to_hide: Vec<_> = self - .entries - .iter() - .filter(|e| e.menu.name != pending_menu_open && e.handle.is_deployed()) - .map(|e| e.handle.clone()) - .collect(); - - if handles_to_hide.is_empty() { - // We need to wait for the next frame to show all menus first, - // before we can handle show/hide operations - window.on_next_frame(move |window, cx| { - handles_to_hide.iter().for_each(|handle| handle.hide(cx)); - window.defer(cx, move |window, cx| handle_to_show.show(window, cx)); - }); - } else { - // Since menus are already shown, we can directly handle show/hide operations - handles_to_hide.iter().for_each(|handle| handle.hide(cx)); - cx.defer_in(window, move |_, window, cx| handle_to_show.show(window, cx)); - } - } - - div() - .key_context("ApplicationMenu") - .flex() - .flex_row() - .gap_x_1() - .when(!all_menus_shown && !self.entries.is_empty(), |this| { - this.child(self.render_application_menu(&self.entries[0])) - }) - .when(all_menus_shown, |this| { - this.children( - self.entries - .iter() - .map(|entry| self.render_standard_menu(entry)), - ) - }) - } -} diff --git a/crates/title_bar/src/collab.rs b/crates/title_bar/src/collab.rs deleted file mode 100644 index 8a2d23dd26..0000000000 --- a/crates/title_bar/src/collab.rs +++ /dev/null @@ -1,627 +0,0 @@ -use std::rc::Rc; -use std::sync::Arc; - -use call::{ActiveCall, ParticipantLocation, Room}; -use channel::ChannelStore; -use client::{User, proto::PeerId}; -use gpui::{ - AnyElement, Hsla, IntoElement, MouseButton, Path, ScreenCaptureSource, Styled, WeakEntity, - canvas, point, -}; -use gpui::{App, Task, Window}; -use project::WorktreeSettings; -use rpc::proto::{self}; -use settings::{Settings as _, SettingsLocation}; -use theme::ActiveTheme; -use ui::{ - Avatar, AvatarAudioStatusIndicator, ContextMenu, ContextMenuItem, Divider, DividerColor, - Facepile, PopoverMenu, SplitButton, SplitButtonStyle, TintColor, Tooltip, prelude::*, -}; -use util::rel_path::RelPath; -use workspace::notifications::DetachAndPromptErr; - -use crate::TitleBar; - -pub fn toggle_screen_sharing( - screen: anyhow::Result>>, - window: &mut Window, - cx: &mut App, -) { - let call = ActiveCall::global(cx).read(cx); - let toggle_screen_sharing = match screen { - Ok(screen) => { - let Some(room) = call.room().cloned() else { - return; - }; - - room.update(cx, |room, cx| { - let clicked_on_currently_shared_screen = - room.shared_screen_id().is_some_and(|screen_id| { - Some(screen_id) - == screen - .as_deref() - .and_then(|s| s.metadata().ok().map(|meta| meta.id)) - }); - let should_unshare_current_screen = room.is_sharing_screen(); - let unshared_current_screen = should_unshare_current_screen.then(|| { - telemetry::event!( - "Screen Share Disabled", - room_id = room.id(), - channel_id = room.channel_id(), - ); - room.unshare_screen(clicked_on_currently_shared_screen || screen.is_none(), cx) - }); - if let Some(screen) = screen { - if !should_unshare_current_screen { - telemetry::event!( - "Screen Share Enabled", - room_id = room.id(), - channel_id = room.channel_id(), - ); - } - cx.spawn(async move |room, cx| { - unshared_current_screen.transpose()?; - if !clicked_on_currently_shared_screen { - room.update(cx, |room, cx| room.share_screen(screen, cx))? - .await - } else { - Ok(()) - } - }) - } else { - Task::ready(Ok(())) - } - }) - } - Err(e) => Task::ready(Err(e)), - }; - toggle_screen_sharing.detach_and_prompt_err("Sharing Screen Failed", window, cx, |e, _, _| Some(format!("{:?}\n\nPlease check that you have given Zed permissions to record your screen in Settings.", e))); -} - -pub fn toggle_mute(cx: &mut App) { - let call = ActiveCall::global(cx).read(cx); - if let Some(room) = call.room().cloned() { - room.update(cx, |room, cx| { - let operation = if room.is_muted() { - "Microphone Enabled" - } else { - "Microphone Disabled" - }; - telemetry::event!( - operation, - room_id = room.id(), - channel_id = room.channel_id(), - ); - - room.toggle_mute(cx) - }); - } -} - -pub fn toggle_deafen(cx: &mut App) { - if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { - room.update(cx, |room, cx| room.toggle_deafen(cx)); - } -} - -fn render_color_ribbon(color: Hsla) -> impl Element { - canvas( - move |_, _, _| {}, - move |bounds, _, window, _| { - let height = bounds.size.height; - let horizontal_offset = height; - let vertical_offset = height / 2.0; - let mut path = Path::new(bounds.bottom_left()); - path.curve_to( - bounds.origin + point(horizontal_offset, vertical_offset), - bounds.origin + point(px(0.0), vertical_offset), - ); - path.line_to(bounds.top_right() + point(-horizontal_offset, vertical_offset)); - path.curve_to( - bounds.bottom_right(), - bounds.top_right() + point(px(0.0), vertical_offset), - ); - path.line_to(bounds.bottom_left()); - window.paint_path(path, color); - }, - ) - .h_1() - .w_full() -} - -impl TitleBar { - pub(crate) fn render_collaborator_list( - &self, - _: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let room = ActiveCall::global(cx).read(cx).room().cloned(); - let current_user = self.user_store.read(cx).current_user(); - let client = self.client.clone(); - let project_id = self.project.read(cx).remote_id(); - let workspace = self.workspace.upgrade(); - - h_flex() - .id("collaborator-list") - .w_full() - .gap_1() - .overflow_x_scroll() - .when_some( - current_user.zip(client.peer_id()).zip(room), - |this, ((current_user, peer_id), room)| { - let player_colors = cx.theme().players(); - let room = room.read(cx); - let mut remote_participants = - room.remote_participants().values().collect::>(); - remote_participants.sort_by_key(|p| p.participant_index.0); - - let current_user_face_pile = self.render_collaborator( - ¤t_user, - peer_id, - true, - room.is_speaking(), - room.is_muted(), - None, - room, - project_id, - ¤t_user, - cx, - ); - - this.children(current_user_face_pile.map(|face_pile| { - v_flex() - .on_mouse_down(MouseButton::Left, |_, window, _| { - window.prevent_default() - }) - .child(face_pile) - .child(render_color_ribbon(player_colors.local().cursor)) - })) - .children(remote_participants.iter().filter_map(|collaborator| { - let player_color = - player_colors.color_for_participant(collaborator.participant_index.0); - let is_following = workspace - .as_ref()? - .read(cx) - .is_being_followed(collaborator.peer_id); - let is_present = project_id.is_some_and(|project_id| { - collaborator.location - == ParticipantLocation::SharedProject { project_id } - }); - - let facepile = self.render_collaborator( - &collaborator.user, - collaborator.peer_id, - is_present, - collaborator.speaking, - collaborator.muted, - is_following.then_some(player_color.selection), - room, - project_id, - ¤t_user, - cx, - )?; - - Some( - v_flex() - .id(("collaborator", collaborator.user.id)) - .child(facepile) - .child(render_color_ribbon(player_color.cursor)) - .cursor_pointer() - .on_mouse_down(MouseButton::Left, |_, window, _| { - window.prevent_default() - }) - .on_click({ - let peer_id = collaborator.peer_id; - cx.listener(move |this, _, window, cx| { - cx.stop_propagation(); - - this.workspace - .update(cx, |workspace, cx| { - if is_following { - workspace.unfollow(peer_id, window, cx); - } else { - workspace.follow(peer_id, window, cx); - } - }) - .ok(); - }) - }) - .tooltip({ - let login = collaborator.user.github_login.clone(); - Tooltip::text(format!("Follow {login}")) - }), - ) - })) - }, - ) - } - - fn render_collaborator( - &self, - user: &Arc, - peer_id: PeerId, - is_present: bool, - is_speaking: bool, - is_muted: bool, - leader_selection_color: Option, - room: &Room, - project_id: Option, - current_user: &Arc, - cx: &App, - ) -> Option
{ - if room.role_for_user(user.id) == Some(proto::ChannelRole::Guest) { - return None; - } - - const FACEPILE_LIMIT: usize = 3; - let followers = project_id.map_or(&[] as &[_], |id| room.followers_for(peer_id, id)); - let extra_count = followers.len().saturating_sub(FACEPILE_LIMIT); - - Some( - div() - .m_0p5() - .p_0p5() - // When the collaborator is not followed, still draw this wrapper div, but leave - // it transparent, so that it does not shift the layout when following. - .when_some(leader_selection_color, |div, color| { - div.rounded_sm().bg(color) - }) - .child( - Facepile::empty() - .child( - Avatar::new(user.avatar_uri.clone()) - .grayscale(!is_present) - .border_color(if is_speaking { - cx.theme().status().info - } else { - // We draw the border in a transparent color rather to avoid - // the layout shift that would come with adding/removing the border. - gpui::transparent_black() - }) - .when(is_muted, |avatar| { - avatar.indicator( - AvatarAudioStatusIndicator::new(ui::AudioStatus::Muted) - .tooltip({ - let github_login = user.github_login.clone(); - Tooltip::text(format!("{} is muted", github_login)) - }), - ) - }), - ) - .children(followers.iter().take(FACEPILE_LIMIT).filter_map( - |follower_peer_id| { - let follower = room - .remote_participants() - .values() - .find_map(|p| { - (p.peer_id == *follower_peer_id).then_some(&p.user) - }) - .or_else(|| { - (self.client.peer_id() == Some(*follower_peer_id)) - .then_some(current_user) - })? - .clone(); - - Some(div().mt(-px(4.)).child( - Avatar::new(follower.avatar_uri.clone()).size(rems(0.75)), - )) - }, - )) - .children(if extra_count > 0 { - Some( - Label::new(format!("+{extra_count}")) - .ml_1() - .into_any_element(), - ) - } else { - None - }), - ), - ) - } - - pub(crate) fn render_call_controls( - &self, - window: &mut Window, - cx: &mut Context, - ) -> Vec { - let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() else { - return Vec::new(); - }; - - let is_connecting_to_project = self - .workspace - .update(cx, |workspace, cx| workspace.has_active_modal(window, cx)) - .unwrap_or(false); - - let room = room.read(cx); - let project = self.project.read(cx); - let is_local = project.is_local() || project.is_via_remote_server(); - let is_shared = is_local && project.is_shared(); - let is_muted = room.is_muted(); - let muted_by_user = room.muted_by_user(); - let is_deafened = room.is_deafened().unwrap_or(false); - let is_screen_sharing = room.is_sharing_screen(); - let can_use_microphone = room.can_use_microphone(); - let can_share_projects = room.can_share_projects(); - let screen_sharing_supported = cx.is_screen_capture_supported(); - - let channel_store = ChannelStore::global(cx); - let channel = room - .channel_id() - .and_then(|channel_id| channel_store.read(cx).channel_for_id(channel_id).cloned()); - - let mut children = Vec::new(); - - children.push( - h_flex() - .gap_1() - .child( - IconButton::new("leave-call", IconName::Exit) - .style(ButtonStyle::Subtle) - .tooltip(Tooltip::text("Leave Call")) - .icon_size(IconSize::Small) - .on_click(move |_, _window, cx| { - ActiveCall::global(cx) - .update(cx, |call, cx| call.hang_up(cx)) - .detach_and_log_err(cx); - }), - ) - .child(Divider::vertical().color(DividerColor::Border)) - .into_any_element(), - ); - - if is_local && can_share_projects && !is_connecting_to_project { - let is_sharing_disabled = channel.is_some_and(|channel| match channel.visibility { - proto::ChannelVisibility::Public => project.visible_worktrees(cx).any(|worktree| { - let worktree_id = worktree.read(cx).id(); - - let settings_location = Some(SettingsLocation { - worktree_id, - path: RelPath::empty(), - }); - - WorktreeSettings::get(settings_location, cx).prevent_sharing_in_public_channels - }), - proto::ChannelVisibility::Members => false, - }); - - children.push( - Button::new( - "toggle_sharing", - if is_shared { "Unshare" } else { "Share" }, - ) - .tooltip(Tooltip::text(if is_shared { - "Stop sharing project with call participants" - } else { - "Share project with call participants" - })) - .style(ButtonStyle::Subtle) - .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - .toggle_state(is_shared) - .label_size(LabelSize::Small) - .when(is_sharing_disabled, |parent| { - parent.disabled(true).tooltip(Tooltip::text( - "This project may not be shared in a public channel.", - )) - }) - .on_click(cx.listener(move |this, _, window, cx| { - if is_shared { - this.unshare_project(window, cx); - } else { - this.share_project(cx); - } - })) - .into_any_element(), - ); - } - - if can_use_microphone { - children.push( - IconButton::new( - "mute-microphone", - if is_muted { - IconName::MicMute - } else { - IconName::Mic - }, - ) - .tooltip(move |_window, cx| { - if is_muted { - if is_deafened { - Tooltip::with_meta( - "Unmute Microphone", - None, - "Audio will be unmuted", - cx, - ) - } else { - Tooltip::simple("Unmute Microphone", cx) - } - } else { - Tooltip::simple("Mute Microphone", cx) - } - }) - .style(ButtonStyle::Subtle) - .icon_size(IconSize::Small) - .toggle_state(is_muted) - .selected_style(ButtonStyle::Tinted(TintColor::Error)) - .on_click(move |_, _window, cx| toggle_mute(cx)) - .into_any_element(), - ); - } - - children.push( - IconButton::new( - "mute-sound", - if is_deafened { - IconName::AudioOff - } else { - IconName::AudioOn - }, - ) - .style(ButtonStyle::Subtle) - .selected_style(ButtonStyle::Tinted(TintColor::Error)) - .icon_size(IconSize::Small) - .toggle_state(is_deafened) - .tooltip(move |_window, cx| { - if is_deafened { - let label = "Unmute Audio"; - - if !muted_by_user { - Tooltip::with_meta(label, None, "Microphone will be unmuted", cx) - } else { - Tooltip::simple(label, cx) - } - } else { - let label = "Mute Audio"; - - if !muted_by_user { - Tooltip::with_meta(label, None, "Microphone will be muted", cx) - } else { - Tooltip::simple(label, cx) - } - } - }) - .on_click(move |_, _, cx| toggle_deafen(cx)) - .into_any_element(), - ); - - if can_use_microphone && screen_sharing_supported { - let trigger = IconButton::new("screen-share", IconName::Screen) - .style(ButtonStyle::Subtle) - .icon_size(IconSize::Small) - .toggle_state(is_screen_sharing) - .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - .tooltip(Tooltip::text(if is_screen_sharing { - "Stop Sharing Screen" - } else { - "Share Screen" - })) - .on_click(move |_, window, cx| { - let should_share = ActiveCall::global(cx) - .read(cx) - .room() - .is_some_and(|room| !room.read(cx).is_sharing_screen()); - - window - .spawn(cx, async move |cx| { - let screen = if should_share { - cx.update(|_, cx| pick_default_screen(cx))?.await - } else { - Ok(None) - }; - cx.update(|window, cx| toggle_screen_sharing(screen, window, cx))?; - - Result::<_, anyhow::Error>::Ok(()) - }) - .detach(); - }); - - children.push( - SplitButton::new( - trigger.render(window, cx), - self.render_screen_list().into_any_element(), - ) - .style(SplitButtonStyle::Transparent) - .into_any_element(), - ); - } - - children.push(div().pr_2().into_any_element()); - - children - } - - fn render_screen_list(&self) -> impl IntoElement { - PopoverMenu::new("screen-share-screen-list") - .with_handle(self.screen_share_popover_handle.clone()) - .trigger( - ui::ButtonLike::new_rounded_right("screen-share-screen-list-trigger") - .child( - h_flex() - .mx_neg_0p5() - .h_full() - .justify_center() - .child(Icon::new(IconName::ChevronDown).size(IconSize::XSmall)), - ) - .toggle_state(self.screen_share_popover_handle.is_deployed()), - ) - .menu(|window, cx| { - let screens = cx.screen_capture_sources(); - Some(ContextMenu::build(window, cx, |context_menu, _, cx| { - cx.spawn(async move |this: WeakEntity, cx| { - let screens = screens.await??; - this.update(cx, |this, cx| { - let active_screenshare_id = ActiveCall::global(cx) - .read(cx) - .room() - .and_then(|room| room.read(cx).shared_screen_id()); - for screen in screens { - let Ok(meta) = screen.metadata() else { - continue; - }; - - let label = meta - .label - .clone() - .unwrap_or_else(|| SharedString::from("Unknown screen")); - let resolution = SharedString::from(format!( - "{} × {}", - meta.resolution.width.0, meta.resolution.height.0 - )); - this.push_item(ContextMenuItem::CustomEntry { - entry_render: Box::new(move |_, _| { - h_flex() - .gap_2() - .child( - Icon::new(IconName::Screen) - .size(IconSize::XSmall) - .map(|this| { - if active_screenshare_id == Some(meta.id) { - this.color(Color::Accent) - } else { - this.color(Color::Muted) - } - }), - ) - .child(Label::new(label.clone())) - .child( - Label::new(resolution.clone()) - .color(Color::Muted) - .size(LabelSize::Small), - ) - .into_any() - }), - selectable: true, - documentation_aside: None, - handler: Rc::new(move |_, window, cx| { - toggle_screen_sharing(Ok(Some(screen.clone())), window, cx); - }), - }); - } - }) - }) - .detach_and_log_err(cx); - context_menu - })) - }) - } -} - -/// Picks the screen to share when clicking on the main screen sharing button. -fn pick_default_screen(cx: &App) -> Task>>> { - let source = cx.screen_capture_sources(); - cx.spawn(async move |_| { - let available_sources = source.await??; - Ok(available_sources - .iter() - .find(|it| { - it.as_ref() - .metadata() - .is_ok_and(|meta| meta.is_main.unwrap_or_default()) - }) - .or_else(|| available_sources.first()) - .cloned()) - }) -} diff --git a/crates/title_bar/src/onboarding_banner.rs b/crates/title_bar/src/onboarding_banner.rs deleted file mode 100644 index 750ef0a6cd..0000000000 --- a/crates/title_bar/src/onboarding_banner.rs +++ /dev/null @@ -1,170 +0,0 @@ -use gpui::{Action, Entity, Global, Render, SharedString}; -use ui::{ButtonLike, Tooltip, prelude::*}; -use util::ResultExt; - -/// Prompts the user to try newly released Zed's features -pub struct OnboardingBanner { - dismissed: bool, - source: String, - details: BannerDetails, - visible_when: Option bool>>, -} - -#[derive(Clone)] -struct BannerGlobal { - entity: Entity, -} -impl Global for BannerGlobal {} - -pub struct BannerDetails { - pub action: Box, - pub icon_name: IconName, - pub label: SharedString, - pub subtitle: Option, -} - -impl OnboardingBanner { - pub fn new( - source: &str, - icon_name: IconName, - label: impl Into, - subtitle: Option, - action: Box, - cx: &mut Context, - ) -> Self { - cx.set_global(BannerGlobal { - entity: cx.entity(), - }); - Self { - source: source.to_string(), - details: BannerDetails { - action, - icon_name, - label: label.into(), - subtitle: subtitle.or(Some(SharedString::from("Introducing:"))), - }, - visible_when: None, - dismissed: get_dismissed(source), - } - } - - pub fn visible_when(mut self, predicate: impl Fn(&mut App) -> bool + 'static) -> Self { - self.visible_when = Some(Box::new(predicate)); - self - } - - fn should_show(&self, cx: &mut App) -> bool { - !self.dismissed && self.visible_when.as_ref().map_or(true, |f| f(cx)) - } - - fn dismiss(&mut self, cx: &mut Context) { - persist_dismissed(&self.source, cx); - self.dismissed = true; - cx.notify(); - } -} - -fn dismissed_at_key(source: &str) -> String { - if source == "Git Onboarding" { - "zed_git_banner_dismissed_at".to_string() - } else { - format!( - "{}_banner_dismissed_at", - source.to_lowercase().trim().replace(" ", "_") - ) - } -} - -fn get_dismissed(source: &str) -> bool { - let dismissed_at = dismissed_at_key(source); - db::kvp::KEY_VALUE_STORE - .read_kvp(&dismissed_at) - .log_err() - .is_some_and(|dismissed| dismissed.is_some()) -} - -fn persist_dismissed(source: &str, cx: &mut App) { - let dismissed_at = dismissed_at_key(source); - cx.spawn(async |_| { - let time = chrono::Utc::now().to_rfc3339(); - db::kvp::KEY_VALUE_STORE.write_kvp(dismissed_at, time).await - }) - .detach_and_log_err(cx); -} - -pub fn restore_banner(cx: &mut App) { - cx.defer(|cx| { - cx.global::() - .entity - .clone() - .update(cx, |this, cx| { - this.dismissed = false; - cx.notify(); - }); - }); - - let source = &cx.global::().entity.read(cx).source; - let dismissed_at = dismissed_at_key(source); - cx.spawn(async |_| db::kvp::KEY_VALUE_STORE.delete_kvp(dismissed_at).await) - .detach_and_log_err(cx); -} - -impl Render for OnboardingBanner { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - if !self.should_show(cx) { - return div(); - } - - let border_color = cx.theme().colors().editor_foreground.opacity(0.3); - let banner = h_flex() - .rounded_sm() - .border_1() - .border_color(border_color) - .child( - ButtonLike::new("try-a-feature") - .child( - h_flex() - .h_full() - .gap_1() - .child(Icon::new(self.details.icon_name).size(IconSize::XSmall)) - .child( - h_flex() - .gap_0p5() - .when_some(self.details.subtitle.as_ref(), |this, subtitle| { - this.child( - Label::new(subtitle) - .size(LabelSize::Small) - .color(Color::Muted), - ) - }) - .child(Label::new(&self.details.label).size(LabelSize::Small)), - ), - ) - .on_click(cx.listener(|this, _, window, cx| { - telemetry::event!("Banner Clicked", source = this.source); - this.dismiss(cx); - window.dispatch_action(this.details.action.boxed_clone(), cx) - })), - ) - .child( - div().border_l_1().border_color(border_color).child( - IconButton::new("close", IconName::Close) - .icon_size(IconSize::Indicator) - .on_click(cx.listener(|this, _, _window, cx| { - telemetry::event!("Banner Dismissed", source = this.source); - this.dismiss(cx) - })) - .tooltip(|_window, cx| { - Tooltip::with_meta( - "Close Announcement Banner", - None, - "It won't show again for this feature", - cx, - ) - }), - ), - ); - - div().pr_2().child(banner) - } -} diff --git a/crates/title_bar/src/platform_title_bar.rs b/crates/title_bar/src/platform_title_bar.rs deleted file mode 100644 index 6ce7d089bb..0000000000 --- a/crates/title_bar/src/platform_title_bar.rs +++ /dev/null @@ -1,192 +0,0 @@ -use gpui::{ - AnyElement, Context, Decorations, Entity, Hsla, InteractiveElement, IntoElement, MouseButton, - ParentElement, Pixels, StatefulInteractiveElement, Styled, Window, WindowControlArea, div, px, -}; -use smallvec::SmallVec; -use std::mem; -use ui::prelude::*; - -use crate::{ - platforms::{platform_linux, platform_mac, platform_windows}, - system_window_tabs::SystemWindowTabs, -}; - -pub struct PlatformTitleBar { - id: ElementId, - platform_style: PlatformStyle, - children: SmallVec<[AnyElement; 2]>, - should_move: bool, - system_window_tabs: Entity, -} - -impl PlatformTitleBar { - pub fn new(id: impl Into, cx: &mut Context) -> Self { - let platform_style = PlatformStyle::platform(); - let system_window_tabs = cx.new(|_cx| SystemWindowTabs::new()); - - Self { - id: id.into(), - platform_style, - children: SmallVec::new(), - should_move: false, - system_window_tabs, - } - } - - #[cfg(not(target_os = "windows"))] - pub fn height(window: &mut Window) -> Pixels { - (1.75 * window.rem_size()).max(px(34.)) - } - - #[cfg(target_os = "windows")] - pub fn height(_window: &mut Window) -> Pixels { - // todo(windows) instead of hard coded size report the actual size to the Windows platform API - px(32.) - } - - pub fn title_bar_color(&self, window: &mut Window, cx: &mut Context) -> Hsla { - if cfg!(any(target_os = "linux", target_os = "freebsd")) { - if window.is_window_active() && !self.should_move { - cx.theme().colors().title_bar_background - } else { - cx.theme().colors().title_bar_inactive_background - } - } else { - cx.theme().colors().title_bar_background - } - } - - pub fn set_children(&mut self, children: T) - where - T: IntoIterator, - { - self.children = children.into_iter().collect(); - } -} - -impl Render for PlatformTitleBar { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let supported_controls = window.window_controls(); - let decorations = window.window_decorations(); - let height = Self::height(window); - let titlebar_color = self.title_bar_color(window, cx); - let close_action = Box::new(workspace::CloseWindow); - let children = mem::take(&mut self.children); - - let title_bar = h_flex() - .window_control_area(WindowControlArea::Drag) - .w_full() - .h(height) - .map(|this| { - this.on_mouse_down_out(cx.listener(move |this, _ev, _window, _cx| { - this.should_move = false; - })) - .on_mouse_up( - gpui::MouseButton::Left, - cx.listener(move |this, _ev, _window, _cx| { - this.should_move = false; - }), - ) - .on_mouse_down( - gpui::MouseButton::Left, - cx.listener(move |this, _ev, _window, _cx| { - this.should_move = true; - }), - ) - .on_mouse_move(cx.listener(move |this, _ev, window, _| { - if this.should_move { - this.should_move = false; - window.start_window_move(); - } - })) - }) - .map(|this| { - // Note: On Windows the title bar behavior is handled by the platform implementation. - this.id(self.id.clone()) - .when(self.platform_style == PlatformStyle::Mac, |this| { - this.on_click(|event, window, _| { - if event.click_count() == 2 { - window.titlebar_double_click(); - } - }) - }) - .when(self.platform_style == PlatformStyle::Linux, |this| { - this.on_click(|event, window, _| { - if event.click_count() == 2 { - window.zoom_window(); - } - }) - }) - }) - .map(|this| { - if window.is_fullscreen() { - this.pl_2() - } else if self.platform_style == PlatformStyle::Mac { - this.pl(px(platform_mac::TRAFFIC_LIGHT_PADDING)) - } else { - this.pl_2() - } - }) - .map(|el| match decorations { - Decorations::Server => el, - Decorations::Client { tiling, .. } => el - .when(!(tiling.top || tiling.right), |el| { - el.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.top || tiling.left), |el| { - el.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING) - }) - // this border is to avoid a transparent gap in the rounded corners - .mt(px(-1.)) - .mb(px(-1.)) - .border(px(1.)) - .border_color(titlebar_color), - }) - .bg(titlebar_color) - .content_stretch() - .child( - div() - .id(self.id.clone()) - .flex() - .flex_row() - .items_center() - .justify_between() - .overflow_x_hidden() - .w_full() - .children(children), - ) - .when(!window.is_fullscreen(), |title_bar| { - match self.platform_style { - PlatformStyle::Mac => title_bar, - PlatformStyle::Linux => { - if matches!(decorations, Decorations::Client { .. }) { - title_bar - .child(platform_linux::LinuxWindowControls::new(close_action)) - .when(supported_controls.window_menu, |titlebar| { - titlebar - .on_mouse_down(MouseButton::Right, move |ev, window, _| { - window.show_window_menu(ev.position) - }) - }) - } else { - title_bar - } - } - PlatformStyle::Windows => { - title_bar.child(platform_windows::WindowsWindowControls::new(height)) - } - } - }); - - v_flex() - .w_full() - .child(title_bar) - .child(self.system_window_tabs.clone().into_any_element()) - } -} - -impl ParentElement for PlatformTitleBar { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} diff --git a/crates/title_bar/src/platforms.rs b/crates/title_bar/src/platforms.rs deleted file mode 100644 index 67e87d45ea..0000000000 --- a/crates/title_bar/src/platforms.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod platform_linux; -pub mod platform_mac; -pub mod platform_windows; diff --git a/crates/title_bar/src/platforms/platform_linux.rs b/crates/title_bar/src/platforms/platform_linux.rs deleted file mode 100644 index 0e7af80f80..0000000000 --- a/crates/title_bar/src/platforms/platform_linux.rs +++ /dev/null @@ -1,208 +0,0 @@ -use gpui::{Action, Hsla, MouseButton, prelude::*, svg}; -use ui::prelude::*; - -#[derive(IntoElement)] -pub struct LinuxWindowControls { - close_window_action: Box, -} - -impl LinuxWindowControls { - pub fn new(close_window_action: Box) -> Self { - Self { - close_window_action, - } - } -} - -impl RenderOnce for LinuxWindowControls { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - h_flex() - .id("generic-window-controls") - .px_3() - .gap_3() - .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) - .child(WindowControl::new( - "minimize", - WindowControlType::Minimize, - cx, - )) - .child(WindowControl::new( - "maximize-or-restore", - if window.is_maximized() { - WindowControlType::Restore - } else { - WindowControlType::Maximize - }, - cx, - )) - .child(WindowControl::new_close( - "close", - WindowControlType::Close, - self.close_window_action, - cx, - )) - } -} - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -pub enum WindowControlType { - Minimize, - Restore, - Maximize, - Close, -} - -impl WindowControlType { - /// Returns the icon name for the window control type. - /// - /// Will take a [PlatformStyle] in the future to return a different - /// icon name based on the platform. - pub fn icon(&self) -> IconName { - match self { - WindowControlType::Minimize => IconName::GenericMinimize, - WindowControlType::Restore => IconName::GenericRestore, - WindowControlType::Maximize => IconName::GenericMaximize, - WindowControlType::Close => IconName::GenericClose, - } - } -} - -#[allow(unused)] -pub struct WindowControlStyle { - background: Hsla, - background_hover: Hsla, - icon: Hsla, - icon_hover: Hsla, -} - -impl WindowControlStyle { - pub fn default(cx: &mut App) -> Self { - let colors = cx.theme().colors(); - - Self { - background: colors.ghost_element_background, - background_hover: colors.ghost_element_hover, - icon: colors.icon, - icon_hover: colors.icon_muted, - } - } - - #[allow(unused)] - /// Sets the background color of the control. - pub fn background(mut self, color: impl Into) -> Self { - self.background = color.into(); - self - } - - #[allow(unused)] - /// Sets the background color of the control when hovered. - pub fn background_hover(mut self, color: impl Into) -> Self { - self.background_hover = color.into(); - self - } - - #[allow(unused)] - /// Sets the color of the icon. - pub fn icon(mut self, color: impl Into) -> Self { - self.icon = color.into(); - self - } - - #[allow(unused)] - /// Sets the color of the icon when hovered. - pub fn icon_hover(mut self, color: impl Into) -> Self { - self.icon_hover = color.into(); - self - } -} - -#[derive(IntoElement)] -pub struct WindowControl { - id: ElementId, - icon: WindowControlType, - style: WindowControlStyle, - close_action: Option>, -} - -impl WindowControl { - pub fn new(id: impl Into, icon: WindowControlType, cx: &mut App) -> Self { - let style = WindowControlStyle::default(cx); - - Self { - id: id.into(), - icon, - style, - close_action: None, - } - } - - pub fn new_close( - id: impl Into, - icon: WindowControlType, - close_action: Box, - cx: &mut App, - ) -> Self { - let style = WindowControlStyle::default(cx); - - Self { - id: id.into(), - icon, - style, - close_action: Some(close_action.boxed_clone()), - } - } - - #[allow(unused)] - pub fn custom_style( - id: impl Into, - icon: WindowControlType, - style: WindowControlStyle, - ) -> Self { - Self { - id: id.into(), - icon, - style, - close_action: None, - } - } -} - -impl RenderOnce for WindowControl { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let icon = svg() - .size_4() - .flex_none() - .path(self.icon.icon().path()) - .text_color(self.style.icon) - .group_hover("", |this| this.text_color(self.style.icon_hover)); - - h_flex() - .id(self.id) - .group("") - .cursor_pointer() - .justify_center() - .content_center() - .rounded_2xl() - .w_5() - .h_5() - .hover(|this| this.bg(self.style.background_hover)) - .active(|this| this.bg(self.style.background_hover)) - .child(icon) - .on_mouse_move(|_, _, cx| cx.stop_propagation()) - .on_click(move |_, window, cx| { - cx.stop_propagation(); - match self.icon { - WindowControlType::Minimize => window.minimize_window(), - WindowControlType::Restore => window.zoom_window(), - WindowControlType::Maximize => window.zoom_window(), - WindowControlType::Close => window.dispatch_action( - self.close_action - .as_ref() - .expect("Use WindowControl::new_close() for close control.") - .boxed_clone(), - cx, - ), - } - }) - } -} diff --git a/crates/title_bar/src/platforms/platform_mac.rs b/crates/title_bar/src/platforms/platform_mac.rs deleted file mode 100644 index c7becde6c1..0000000000 --- a/crates/title_bar/src/platforms/platform_mac.rs +++ /dev/null @@ -1,6 +0,0 @@ -/// Use pixels here instead of a rem-based size because the macOS traffic -/// lights are a static size, and don't scale with the rest of the UI. -/// -/// Magic number: There is one extra pixel of padding on the left side due to -/// the 1px border around the window on macOS apps. -pub const TRAFFIC_LIGHT_PADDING: f32 = 71.; diff --git a/crates/title_bar/src/platforms/platform_windows.rs b/crates/title_bar/src/platforms/platform_windows.rs deleted file mode 100644 index 1df75ee8a9..0000000000 --- a/crates/title_bar/src/platforms/platform_windows.rs +++ /dev/null @@ -1,143 +0,0 @@ -use gpui::{Hsla, Rgba, WindowControlArea, prelude::*}; - -use ui::prelude::*; - -#[derive(IntoElement)] -pub struct WindowsWindowControls { - button_height: Pixels, -} - -impl WindowsWindowControls { - pub fn new(button_height: Pixels) -> Self { - Self { button_height } - } - - #[cfg(not(target_os = "windows"))] - fn get_font() -> &'static str { - "Segoe Fluent Icons" - } - - #[cfg(target_os = "windows")] - fn get_font() -> &'static str { - use windows::Wdk::System::SystemServices::RtlGetVersion; - - let mut version = unsafe { std::mem::zeroed() }; - let status = unsafe { RtlGetVersion(&mut version) }; - - if status.is_ok() && version.dwBuildNumber >= 22000 { - "Segoe Fluent Icons" - } else { - "Segoe MDL2 Assets" - } - } -} - -impl RenderOnce for WindowsWindowControls { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let close_button_hover_color = Rgba { - r: 232.0 / 255.0, - g: 17.0 / 255.0, - b: 32.0 / 255.0, - a: 1.0, - }; - - let button_hover_color = cx.theme().colors().ghost_element_hover; - let button_active_color = cx.theme().colors().ghost_element_active; - - div() - .id("windows-window-controls") - .font_family(Self::get_font()) - .flex() - .flex_row() - .justify_center() - .content_stretch() - .max_h(self.button_height) - .min_h(self.button_height) - .child(WindowsCaptionButton::new( - "minimize", - WindowsCaptionButtonIcon::Minimize, - button_hover_color, - button_active_color, - )) - .child(WindowsCaptionButton::new( - "maximize-or-restore", - if window.is_maximized() { - WindowsCaptionButtonIcon::Restore - } else { - WindowsCaptionButtonIcon::Maximize - }, - button_hover_color, - button_active_color, - )) - .child(WindowsCaptionButton::new( - "close", - WindowsCaptionButtonIcon::Close, - close_button_hover_color, - button_active_color, - )) - } -} - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -enum WindowsCaptionButtonIcon { - Minimize, - Restore, - Maximize, - Close, -} - -#[derive(IntoElement)] -struct WindowsCaptionButton { - id: ElementId, - icon: WindowsCaptionButtonIcon, - hover_background_color: Hsla, - active_background_color: Hsla, -} - -impl WindowsCaptionButton { - pub fn new( - id: impl Into, - icon: WindowsCaptionButtonIcon, - hover_background_color: impl Into, - active_background_color: impl Into, - ) -> Self { - Self { - id: id.into(), - icon, - hover_background_color: hover_background_color.into(), - active_background_color: active_background_color.into(), - } - } -} - -impl RenderOnce for WindowsCaptionButton { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - h_flex() - .id(self.id) - .justify_center() - .content_center() - .occlude() - .w(px(36.)) - .h_full() - .text_size(px(10.0)) - .hover(|style| style.bg(self.hover_background_color)) - .active(|style| style.bg(self.active_background_color)) - .map(|this| match self.icon { - WindowsCaptionButtonIcon::Close => { - this.window_control_area(WindowControlArea::Close) - } - WindowsCaptionButtonIcon::Maximize | WindowsCaptionButtonIcon::Restore => { - this.window_control_area(WindowControlArea::Max) - } - WindowsCaptionButtonIcon::Minimize => { - this.window_control_area(WindowControlArea::Min) - } - }) - .child(match self.icon { - WindowsCaptionButtonIcon::Minimize => "\u{e921}", - WindowsCaptionButtonIcon::Restore => "\u{e923}", - WindowsCaptionButtonIcon::Maximize => "\u{e922}", - WindowsCaptionButtonIcon::Close => "\u{e8bb}", - }) - } -} diff --git a/crates/title_bar/src/stories.rs b/crates/title_bar/src/stories.rs deleted file mode 100644 index 21ed2268db..0000000000 --- a/crates/title_bar/src/stories.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod application_menu; - -pub use application_menu::*; diff --git a/crates/title_bar/src/stories/application_menu.rs b/crates/title_bar/src/stories/application_menu.rs deleted file mode 100644 index f47f2a6c76..0000000000 --- a/crates/title_bar/src/stories/application_menu.rs +++ /dev/null @@ -1,29 +0,0 @@ -use gpui::{Entity, Render}; -use story::{Story, StoryItem, StorySection}; - -use ui::prelude::*; - -use crate::application_menu::ApplicationMenu; - -pub struct ApplicationMenuStory { - menu: Entity, -} - -impl ApplicationMenuStory { - pub fn new(window: &mut Window, cx: &mut App) -> Self { - Self { - menu: cx.new(|cx| ApplicationMenu::new(window, cx)), - } - } -} - -impl Render for ApplicationMenuStory { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - Story::container(cx) - .child(Story::title_for::(cx)) - .child(StorySection::new().child(StoryItem::new( - "Application Menu", - h_flex().child(self.menu.clone()), - ))) - } -} diff --git a/crates/title_bar/src/system_window_tabs.rs b/crates/title_bar/src/system_window_tabs.rs deleted file mode 100644 index a9bf46cc4f..0000000000 --- a/crates/title_bar/src/system_window_tabs.rs +++ /dev/null @@ -1,529 +0,0 @@ -use settings::{Settings, SettingsStore}; - -use gpui::{ - AnyWindowHandle, Context, Hsla, InteractiveElement, MouseButton, ParentElement, ScrollHandle, - Styled, SystemWindowTab, SystemWindowTabController, Window, WindowId, actions, canvas, div, -}; - -use theme::ThemeSettings; -use ui::{ - Color, ContextMenu, DynamicSpacing, IconButton, IconButtonShape, IconName, IconSize, Label, - LabelSize, Tab, h_flex, prelude::*, right_click_menu, -}; -use workspace::{ - CloseWindow, ItemSettings, Workspace, WorkspaceSettings, - item::{ClosePosition, ShowCloseButton}, -}; - -actions!( - window, - [ - ShowNextWindowTab, - ShowPreviousWindowTab, - MergeAllWindows, - MoveTabToNewWindow - ] -); - -#[derive(Clone)] -pub struct DraggedWindowTab { - pub id: WindowId, - pub ix: usize, - pub handle: AnyWindowHandle, - pub title: String, - pub width: Pixels, - pub is_active: bool, - pub active_background_color: Hsla, - pub inactive_background_color: Hsla, -} - -pub struct SystemWindowTabs { - tab_bar_scroll_handle: ScrollHandle, - measured_tab_width: Pixels, - last_dragged_tab: Option, -} - -impl SystemWindowTabs { - pub fn new() -> Self { - Self { - tab_bar_scroll_handle: ScrollHandle::new(), - measured_tab_width: px(0.), - last_dragged_tab: None, - } - } - - pub fn init(cx: &mut App) { - let mut was_use_system_window_tabs = - WorkspaceSettings::get_global(cx).use_system_window_tabs; - - cx.observe_global::(move |cx| { - let use_system_window_tabs = WorkspaceSettings::get_global(cx).use_system_window_tabs; - if use_system_window_tabs == was_use_system_window_tabs { - return; - } - was_use_system_window_tabs = use_system_window_tabs; - - let tabbing_identifier = if use_system_window_tabs { - Some(String::from("zed")) - } else { - None - }; - - if use_system_window_tabs { - SystemWindowTabController::init(cx); - } - - cx.windows().iter().for_each(|handle| { - let _ = handle.update(cx, |_, window, cx| { - window.set_tabbing_identifier(tabbing_identifier.clone()); - if use_system_window_tabs { - let tabs = if let Some(tabs) = window.tabbed_windows() { - tabs - } else { - vec![SystemWindowTab::new( - SharedString::from(window.window_title()), - window.window_handle(), - )] - }; - - SystemWindowTabController::add_tab(cx, handle.window_id(), tabs); - } - }); - }); - }) - .detach(); - - cx.observe_new(|workspace: &mut Workspace, _, _| { - workspace.register_action_renderer(|div, _, window, cx| { - let window_id = window.window_handle().window_id(); - let controller = cx.global::(); - - let tab_groups = controller.tab_groups(); - let tabs = controller.tabs(window_id); - let Some(tabs) = tabs else { - return div; - }; - - div.when(tabs.len() > 1, |div| { - div.on_action(move |_: &ShowNextWindowTab, window, cx| { - SystemWindowTabController::select_next_tab( - cx, - window.window_handle().window_id(), - ); - }) - .on_action(move |_: &ShowPreviousWindowTab, window, cx| { - SystemWindowTabController::select_previous_tab( - cx, - window.window_handle().window_id(), - ); - }) - .on_action(move |_: &MoveTabToNewWindow, window, cx| { - SystemWindowTabController::move_tab_to_new_window( - cx, - window.window_handle().window_id(), - ); - window.move_tab_to_new_window(); - }) - }) - .when(tab_groups.len() > 1, |div| { - div.on_action(move |_: &MergeAllWindows, window, cx| { - SystemWindowTabController::merge_all_windows( - cx, - window.window_handle().window_id(), - ); - window.merge_all_windows(); - }) - }) - }); - }) - .detach(); - } - - fn render_tab( - &self, - ix: usize, - item: SystemWindowTab, - tabs: Vec, - active_background_color: Hsla, - inactive_background_color: Hsla, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement + use<> { - let entity = cx.entity(); - let settings = ItemSettings::get_global(cx); - let close_side = &settings.close_position; - let show_close_button = &settings.show_close_button; - - let rem_size = window.rem_size(); - let width = self.measured_tab_width.max(rem_size * 10); - let is_active = window.window_handle().window_id() == item.id; - let title = item.title.to_string(); - - let label = Label::new(&title) - .size(LabelSize::Small) - .truncate() - .color(if is_active { - Color::Default - } else { - Color::Muted - }); - - let tab = h_flex() - .id(ix) - .group("tab") - .w_full() - .overflow_hidden() - .h(Tab::content_height(cx)) - .relative() - .px(DynamicSpacing::Base16.px(cx)) - .justify_center() - .border_l_1() - .border_color(cx.theme().colors().border) - .cursor_pointer() - .on_drag( - DraggedWindowTab { - id: item.id, - ix, - handle: item.handle, - title: item.title.to_string(), - width, - is_active, - active_background_color, - inactive_background_color, - }, - move |tab, _, _, cx| { - entity.update(cx, |this, _cx| { - this.last_dragged_tab = Some(tab.clone()); - }); - cx.new(|_| tab.clone()) - }, - ) - .drag_over::({ - let tab_ix = ix; - move |element, dragged_tab: &DraggedWindowTab, _, cx| { - let mut styled_tab = element - .bg(cx.theme().colors().drop_target_background) - .border_color(cx.theme().colors().drop_target_border) - .border_0(); - - if tab_ix < dragged_tab.ix { - styled_tab = styled_tab.border_l_2(); - } else if tab_ix > dragged_tab.ix { - styled_tab = styled_tab.border_r_2(); - } - - styled_tab - } - }) - .on_drop({ - let tab_ix = ix; - cx.listener(move |this, dragged_tab: &DraggedWindowTab, _window, cx| { - this.last_dragged_tab = None; - Self::handle_tab_drop(dragged_tab, tab_ix, cx); - }) - }) - .on_click(move |_, _, cx| { - let _ = item.handle.update(cx, |_, window, _| { - window.activate_window(); - }); - }) - .on_mouse_up(MouseButton::Middle, move |_, window, cx| { - if item.handle.window_id() == window.window_handle().window_id() { - window.dispatch_action(Box::new(CloseWindow), cx); - } else { - let _ = item.handle.update(cx, |_, window, cx| { - window.dispatch_action(Box::new(CloseWindow), cx); - }); - } - }) - .child(label) - .map(|this| match show_close_button { - ShowCloseButton::Hidden => this, - _ => this.child( - div() - .absolute() - .top_2() - .w_4() - .h_4() - .map(|this| match close_side { - ClosePosition::Left => this.left_1(), - ClosePosition::Right => this.right_1(), - }) - .child( - IconButton::new("close", IconName::Close) - .shape(IconButtonShape::Square) - .icon_color(Color::Muted) - .icon_size(IconSize::XSmall) - .on_click({ - move |_, window, cx| { - if item.handle.window_id() - == window.window_handle().window_id() - { - window.dispatch_action(Box::new(CloseWindow), cx); - } else { - let _ = item.handle.update(cx, |_, window, cx| { - window.dispatch_action(Box::new(CloseWindow), cx); - }); - } - } - }) - .map(|this| match show_close_button { - ShowCloseButton::Hover => this.visible_on_hover("tab"), - _ => this, - }), - ), - ), - }) - .into_any(); - - let menu = right_click_menu(ix) - .trigger(|_, _, _| tab) - .menu(move |window, cx| { - let focus_handle = cx.focus_handle(); - let tabs = tabs.clone(); - let other_tabs = tabs.clone(); - let move_tabs = tabs.clone(); - let merge_tabs = tabs.clone(); - - ContextMenu::build(window, cx, move |mut menu, _window_, _cx| { - menu = menu.entry("Close Tab", None, move |window, cx| { - Self::handle_right_click_action( - cx, - window, - &tabs, - |tab| tab.id == item.id, - |window, cx| { - window.dispatch_action(Box::new(CloseWindow), cx); - }, - ); - }); - - menu = menu.entry("Close Other Tabs", None, move |window, cx| { - Self::handle_right_click_action( - cx, - window, - &other_tabs, - |tab| tab.id != item.id, - |window, cx| { - window.dispatch_action(Box::new(CloseWindow), cx); - }, - ); - }); - - menu = menu.entry("Move Tab to New Window", None, move |window, cx| { - Self::handle_right_click_action( - cx, - window, - &move_tabs, - |tab| tab.id == item.id, - |window, cx| { - SystemWindowTabController::move_tab_to_new_window( - cx, - window.window_handle().window_id(), - ); - window.move_tab_to_new_window(); - }, - ); - }); - - menu = menu.entry("Show All Tabs", None, move |window, cx| { - Self::handle_right_click_action( - cx, - window, - &merge_tabs, - |tab| tab.id == item.id, - |window, _cx| { - window.toggle_window_tab_overview(); - }, - ); - }); - - menu.context(focus_handle) - }) - }); - - div() - .flex_1() - .min_w(rem_size * 10) - .when(is_active, |this| this.bg(active_background_color)) - .border_t_1() - .border_color(if is_active { - active_background_color - } else { - cx.theme().colors().border - }) - .child(menu) - } - - fn handle_tab_drop(dragged_tab: &DraggedWindowTab, ix: usize, cx: &mut Context) { - SystemWindowTabController::update_tab_position(cx, dragged_tab.id, ix); - } - - fn handle_right_click_action( - cx: &mut App, - window: &mut Window, - tabs: &Vec, - predicate: P, - mut action: F, - ) where - P: Fn(&SystemWindowTab) -> bool, - F: FnMut(&mut Window, &mut App), - { - for tab in tabs { - if predicate(tab) { - if tab.id == window.window_handle().window_id() { - action(window, cx); - } else { - let _ = tab.handle.update(cx, |_view, window, cx| { - action(window, cx); - }); - } - } - } - } -} - -impl Render for SystemWindowTabs { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let use_system_window_tabs = WorkspaceSettings::get_global(cx).use_system_window_tabs; - let active_background_color = cx.theme().colors().title_bar_background; - let inactive_background_color = cx.theme().colors().tab_bar_background; - let entity = cx.entity(); - - let controller = cx.global::(); - let visible = controller.is_visible(); - let current_window_tab = vec![SystemWindowTab::new( - SharedString::from(window.window_title()), - window.window_handle(), - )]; - let tabs = controller - .tabs(window.window_handle().window_id()) - .unwrap_or(¤t_window_tab) - .clone(); - - let tab_items = tabs - .iter() - .enumerate() - .map(|(ix, item)| { - self.render_tab( - ix, - item.clone(), - tabs.clone(), - active_background_color, - inactive_background_color, - window, - cx, - ) - }) - .collect::>(); - - let number_of_tabs = tab_items.len().max(1); - if (!window.tab_bar_visible() && !visible) - || (!use_system_window_tabs && number_of_tabs == 1) - { - return h_flex().into_any_element(); - } - - h_flex() - .w_full() - .h(Tab::container_height(cx)) - .bg(inactive_background_color) - .on_mouse_up_out( - MouseButton::Left, - cx.listener(|this, _event, window, cx| { - if let Some(tab) = this.last_dragged_tab.take() { - SystemWindowTabController::move_tab_to_new_window(cx, tab.id); - if tab.id == window.window_handle().window_id() { - window.move_tab_to_new_window(); - } else { - let _ = tab.handle.update(cx, |_, window, _cx| { - window.move_tab_to_new_window(); - }); - } - } - }), - ) - .child( - h_flex() - .id("window tabs") - .w_full() - .h(Tab::container_height(cx)) - .bg(inactive_background_color) - .overflow_x_scroll() - .track_scroll(&self.tab_bar_scroll_handle) - .children(tab_items) - .child( - canvas( - |_, _, _| (), - move |bounds, _, _, cx| { - let entity = entity.clone(); - entity.update(cx, |this, cx| { - let width = bounds.size.width / number_of_tabs as f32; - if width != this.measured_tab_width { - this.measured_tab_width = width; - cx.notify(); - } - }); - }, - ) - .absolute() - .size_full(), - ), - ) - .child( - h_flex() - .h_full() - .px(DynamicSpacing::Base06.rems(cx)) - .border_t_1() - .border_l_1() - .border_color(cx.theme().colors().border) - .child( - IconButton::new("plus", IconName::Plus) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .on_click(|_event, window, cx| { - window.dispatch_action( - Box::new(zed_actions::OpenRecent { - create_new_window: true, - }), - cx, - ); - }), - ), - ) - .into_any_element() - } -} - -impl Render for DraggedWindowTab { - fn render( - &mut self, - _window: &mut gpui::Window, - cx: &mut gpui::Context, - ) -> impl gpui::IntoElement { - let ui_font = ThemeSettings::get_global(cx).ui_font.clone(); - let label = Label::new(self.title.clone()) - .size(LabelSize::Small) - .truncate() - .color(if self.is_active { - Color::Default - } else { - Color::Muted - }); - - h_flex() - .h(Tab::container_height(cx)) - .w(self.width) - .px(DynamicSpacing::Base16.px(cx)) - .justify_center() - .bg(if self.is_active { - self.active_background_color - } else { - self.inactive_background_color - }) - .border_1() - .border_color(cx.theme().colors().border) - .font(ui_font) - .child(label) - } -} diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs deleted file mode 100644 index 680c455e73..0000000000 --- a/crates/title_bar/src/title_bar.rs +++ /dev/null @@ -1,777 +0,0 @@ -mod application_menu; -pub mod collab; -mod onboarding_banner; -pub mod platform_title_bar; -mod platforms; -mod system_window_tabs; -mod title_bar_settings; - -#[cfg(feature = "stories")] -mod stories; - -use crate::{ - application_menu::{ApplicationMenu, show_menus}, - platform_title_bar::PlatformTitleBar, - system_window_tabs::SystemWindowTabs, -}; - -#[cfg(not(target_os = "macos"))] -use crate::application_menu::{ - ActivateDirection, ActivateMenuLeft, ActivateMenuRight, OpenApplicationMenu, -}; - -use auto_update::AutoUpdateStatus; -use call::ActiveCall; -use client::{Client, UserStore, zed_urls}; -use cloud_llm_client::{Plan, PlanV1, PlanV2}; -use gpui::{ - Action, AnyElement, App, Context, Corner, Element, Entity, Focusable, InteractiveElement, - IntoElement, MouseButton, ParentElement, Render, StatefulInteractiveElement, Styled, - Subscription, WeakEntity, Window, actions, div, -}; -use onboarding_banner::OnboardingBanner; -use project::{Project, WorktreeSettings, git_store::GitStoreEvent}; -use remote::RemoteConnectionOptions; -use settings::{Settings, SettingsLocation}; -use std::sync::Arc; -use theme::ActiveTheme; -use title_bar_settings::TitleBarSettings; -use ui::{ - Avatar, Button, ButtonLike, ButtonStyle, Chip, ContextMenu, Icon, IconName, IconSize, - IconWithIndicator, Indicator, PopoverMenu, PopoverMenuHandle, Tooltip, h_flex, prelude::*, -}; -use util::{ResultExt, rel_path::RelPath}; -use workspace::{Workspace, notifications::NotifyResultExt}; -use zed_actions::{OpenRecent, OpenRemote}; - -pub use onboarding_banner::restore_banner; - -#[cfg(feature = "stories")] -pub use stories::*; - -const MAX_PROJECT_NAME_LENGTH: usize = 40; -const MAX_BRANCH_NAME_LENGTH: usize = 40; -const MAX_SHORT_SHA_LENGTH: usize = 8; - -actions!( - collab, - [ - /// Toggles the user menu dropdown. - ToggleUserMenu, - /// Toggles the project menu dropdown. - ToggleProjectMenu, - /// Switches to a different git branch. - SwitchBranch - ] -); - -pub fn init(cx: &mut App) { - SystemWindowTabs::init(cx); - - cx.observe_new(|workspace: &mut Workspace, window, cx| { - let Some(window) = window else { - return; - }; - let item = cx.new(|cx| TitleBar::new("title-bar", workspace, window, cx)); - workspace.set_titlebar_item(item.into(), window, cx); - - #[cfg(not(target_os = "macos"))] - workspace.register_action(|workspace, action: &OpenApplicationMenu, window, cx| { - if let Some(titlebar) = workspace - .titlebar_item() - .and_then(|item| item.downcast::().ok()) - { - titlebar.update(cx, |titlebar, cx| { - if let Some(ref menu) = titlebar.application_menu { - menu.update(cx, |menu, cx| menu.open_menu(action, window, cx)); - } - }); - } - }); - - #[cfg(not(target_os = "macos"))] - workspace.register_action(|workspace, _: &ActivateMenuRight, window, cx| { - if let Some(titlebar) = workspace - .titlebar_item() - .and_then(|item| item.downcast::().ok()) - { - titlebar.update(cx, |titlebar, cx| { - if let Some(ref menu) = titlebar.application_menu { - menu.update(cx, |menu, cx| { - menu.navigate_menus_in_direction(ActivateDirection::Right, window, cx) - }); - } - }); - } - }); - - #[cfg(not(target_os = "macos"))] - workspace.register_action(|workspace, _: &ActivateMenuLeft, window, cx| { - if let Some(titlebar) = workspace - .titlebar_item() - .and_then(|item| item.downcast::().ok()) - { - titlebar.update(cx, |titlebar, cx| { - if let Some(ref menu) = titlebar.application_menu { - menu.update(cx, |menu, cx| { - menu.navigate_menus_in_direction(ActivateDirection::Left, window, cx) - }); - } - }); - } - }); - }) - .detach(); -} - -pub struct TitleBar { - platform_titlebar: Entity, - project: Entity, - user_store: Entity, - client: Arc, - workspace: WeakEntity, - application_menu: Option>, - _subscriptions: Vec, - banner: Entity, - screen_share_popover_handle: PopoverMenuHandle, -} - -impl Render for TitleBar { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let title_bar_settings = *TitleBarSettings::get_global(cx); - - let show_menus = show_menus(cx); - - let mut children = Vec::new(); - - children.push( - h_flex() - .gap_1() - .map(|title_bar| { - let mut render_project_items = title_bar_settings.show_branch_name - || title_bar_settings.show_project_items; - title_bar - .when_some( - self.application_menu.clone().filter(|_| !show_menus), - |title_bar, menu| { - render_project_items &= - !menu.update(cx, |menu, cx| menu.all_menus_shown(cx)); - title_bar.child(menu) - }, - ) - .when(render_project_items, |title_bar| { - title_bar - .when(title_bar_settings.show_project_items, |title_bar| { - title_bar - .children(self.render_project_host(cx)) - .child(self.render_project_name(cx)) - }) - .when(title_bar_settings.show_branch_name, |title_bar| { - title_bar.children(self.render_project_branch(cx)) - }) - }) - }) - .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) - .into_any_element(), - ); - - children.push(self.render_collaborator_list(window, cx).into_any_element()); - - if title_bar_settings.show_onboarding_banner { - children.push(self.banner.clone().into_any_element()) - } - - let status = self.client.status(); - let status = &*status.borrow(); - let user = self.user_store.read(cx).current_user(); - - let signed_in = user.is_some(); - - children.push( - h_flex() - .map(|this| { - if signed_in { - this.pr_1p5() - } else { - this.pr_1() - } - }) - .gap_1() - .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) - .children(self.render_call_controls(window, cx)) - .children(self.render_connection_status(status, cx)) - .when( - user.is_none() && TitleBarSettings::get_global(cx).show_sign_in, - |el| el.child(self.render_sign_in_button(cx)), - ) - .child(self.render_app_menu_button(cx)) - .into_any_element(), - ); - - if show_menus { - self.platform_titlebar.update(cx, |this, _| { - this.set_children( - self.application_menu - .clone() - .map(|menu| menu.into_any_element()), - ); - }); - - let height = PlatformTitleBar::height(window); - let title_bar_color = self.platform_titlebar.update(cx, |platform_titlebar, cx| { - platform_titlebar.title_bar_color(window, cx) - }); - - v_flex() - .w_full() - .child(self.platform_titlebar.clone().into_any_element()) - .child( - h_flex() - .bg(title_bar_color) - .h(height) - .pl_2() - .justify_between() - .w_full() - .children(children), - ) - .into_any_element() - } else { - self.platform_titlebar.update(cx, |this, _| { - this.set_children(children); - }); - self.platform_titlebar.clone().into_any_element() - } - } -} - -impl TitleBar { - pub fn new( - id: impl Into, - workspace: &Workspace, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let project = workspace.project().clone(); - let git_store = project.read(cx).git_store().clone(); - let user_store = workspace.app_state().user_store.clone(); - let client = workspace.app_state().client.clone(); - let active_call = ActiveCall::global(cx); - - let platform_style = PlatformStyle::platform(); - let application_menu = match platform_style { - PlatformStyle::Mac => { - if option_env!("ZED_USE_CROSS_PLATFORM_MENU").is_some() { - Some(cx.new(|cx| ApplicationMenu::new(window, cx))) - } else { - None - } - } - PlatformStyle::Linux | PlatformStyle::Windows => { - Some(cx.new(|cx| ApplicationMenu::new(window, cx))) - } - }; - - let mut subscriptions = Vec::new(); - subscriptions.push( - cx.observe(&workspace.weak_handle().upgrade().unwrap(), |_, _, cx| { - cx.notify() - }), - ); - subscriptions.push(cx.subscribe(&project, |_, _, _: &project::Event, cx| cx.notify())); - subscriptions.push(cx.observe(&active_call, |this, _, cx| this.active_call_changed(cx))); - subscriptions.push(cx.observe_window_activation(window, Self::window_activation_changed)); - subscriptions.push( - cx.subscribe(&git_store, move |_, _, event, cx| match event { - GitStoreEvent::ActiveRepositoryChanged(_) - | GitStoreEvent::RepositoryUpdated(_, _, true) => { - cx.notify(); - } - _ => {} - }), - ); - subscriptions.push(cx.observe(&user_store, |_, _, cx| cx.notify())); - - let banner = cx.new(|cx| { - OnboardingBanner::new( - "ACP Claude Code Onboarding", - IconName::AiClaude, - "Claude Code", - Some("Introducing:".into()), - zed_actions::agent::OpenClaudeCodeOnboardingModal.boxed_clone(), - cx, - ) - // When updating this to a non-AI feature release, remove this line. - .visible_when(|cx| !project::DisableAiSettings::get_global(cx).disable_ai) - }); - - let platform_titlebar = cx.new(|cx| PlatformTitleBar::new(id, cx)); - - Self { - platform_titlebar, - application_menu, - workspace: workspace.weak_handle(), - project, - user_store, - client, - _subscriptions: subscriptions, - banner, - screen_share_popover_handle: Default::default(), - } - } - - fn render_remote_project_connection(&self, cx: &mut Context) -> Option { - let options = self.project.read(cx).remote_connection_options(cx)?; - let host: SharedString = options.display_name().into(); - - let (nickname, tooltip_title, icon) = match options { - RemoteConnectionOptions::Ssh(options) => ( - options.nickname.map(|nick| nick.into()), - "Remote Project", - IconName::Server, - ), - RemoteConnectionOptions::Wsl(_) => (None, "Remote Project", IconName::Linux), - RemoteConnectionOptions::Docker(_dev_container_connection) => { - (None, "Dev Container", IconName::Box) - } - }; - - let nickname = nickname.unwrap_or_else(|| host.clone()); - - let (indicator_color, meta) = match self.project.read(cx).remote_connection_state(cx)? { - remote::ConnectionState::Connecting => (Color::Info, format!("Connecting to: {host}")), - remote::ConnectionState::Connected => (Color::Success, format!("Connected to: {host}")), - remote::ConnectionState::HeartbeatMissed => ( - Color::Warning, - format!("Connection attempt to {host} missed. Retrying..."), - ), - remote::ConnectionState::Reconnecting => ( - Color::Warning, - format!("Lost connection to {host}. Reconnecting..."), - ), - remote::ConnectionState::Disconnected => { - (Color::Error, format!("Disconnected from {host}")) - } - }; - - let icon_color = match self.project.read(cx).remote_connection_state(cx)? { - remote::ConnectionState::Connecting => Color::Info, - remote::ConnectionState::Connected => Color::Default, - remote::ConnectionState::HeartbeatMissed => Color::Warning, - remote::ConnectionState::Reconnecting => Color::Warning, - remote::ConnectionState::Disconnected => Color::Error, - }; - - let meta = SharedString::from(meta); - - Some( - ButtonLike::new("ssh-server-icon") - .child( - h_flex() - .gap_2() - .max_w_32() - .child( - IconWithIndicator::new( - Icon::new(icon).size(IconSize::Small).color(icon_color), - Some(Indicator::dot().color(indicator_color)), - ) - .indicator_border_color(Some(cx.theme().colors().title_bar_background)) - .into_any_element(), - ) - .child(Label::new(nickname).size(LabelSize::Small).truncate()), - ) - .tooltip(move |_window, cx| { - Tooltip::with_meta( - tooltip_title, - Some(&OpenRemote { - from_existing_connection: false, - create_new_window: false, - }), - meta.clone(), - cx, - ) - }) - .on_click(|_, window, cx| { - window.dispatch_action( - OpenRemote { - from_existing_connection: false, - create_new_window: false, - } - .boxed_clone(), - cx, - ); - }) - .into_any_element(), - ) - } - - pub fn render_project_host(&self, cx: &mut Context) -> Option { - if self.project.read(cx).is_via_remote_server() { - return self.render_remote_project_connection(cx); - } - - if self.project.read(cx).is_disconnected(cx) { - return Some( - Button::new("disconnected", "Disconnected") - .disabled(true) - .color(Color::Disabled) - .style(ButtonStyle::Subtle) - .label_size(LabelSize::Small) - .into_any_element(), - ); - } - - let host = self.project.read(cx).host()?; - let host_user = self.user_store.read(cx).get_cached_user(host.user_id)?; - let participant_index = self - .user_store - .read(cx) - .participant_indices() - .get(&host_user.id)?; - Some( - Button::new("project_owner_trigger", host_user.github_login.clone()) - .color(Color::Player(participant_index.0)) - .style(ButtonStyle::Subtle) - .label_size(LabelSize::Small) - .tooltip(Tooltip::text(format!( - "{} is sharing this project. Click to follow.", - host_user.github_login - ))) - .on_click({ - let host_peer_id = host.peer_id; - cx.listener(move |this, _, window, cx| { - this.workspace - .update(cx, |workspace, cx| { - workspace.follow(host_peer_id, window, cx); - }) - .log_err(); - }) - }) - .into_any_element(), - ) - } - - pub fn render_project_name(&self, cx: &mut Context) -> impl IntoElement { - let name = self - .project - .read(cx) - .visible_worktrees(cx) - .map(|worktree| { - let worktree = worktree.read(cx); - let settings_location = SettingsLocation { - worktree_id: worktree.id(), - path: RelPath::empty(), - }; - - let settings = WorktreeSettings::get(Some(settings_location), cx); - match &settings.project_name { - Some(name) => name.as_str(), - None => worktree.root_name_str(), - } - }) - .next(); - let is_project_selected = name.is_some(); - let name = if let Some(name) = name { - util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH) - } else { - "Open recent project".to_string() - }; - - Button::new("project_name_trigger", name) - .when(!is_project_selected, |b| b.color(Color::Muted)) - .style(ButtonStyle::Subtle) - .label_size(LabelSize::Small) - .tooltip(move |_window, cx| { - Tooltip::for_action( - "Recent Projects", - &zed_actions::OpenRecent { - create_new_window: false, - }, - cx, - ) - }) - .on_click(cx.listener(move |_, _, window, cx| { - window.dispatch_action( - OpenRecent { - create_new_window: false, - } - .boxed_clone(), - cx, - ); - })) - } - - pub fn render_project_branch(&self, cx: &mut Context) -> Option { - let settings = TitleBarSettings::get_global(cx); - let repository = self.project.read(cx).active_repository(cx)?; - let workspace = self.workspace.upgrade()?; - let repo = repository.read(cx); - let branch_name = repo - .branch - .as_ref() - .map(|branch| branch.name()) - .map(|name| util::truncate_and_trailoff(name, MAX_BRANCH_NAME_LENGTH)) - .or_else(|| { - repo.head_commit.as_ref().map(|commit| { - commit - .sha - .chars() - .take(MAX_SHORT_SHA_LENGTH) - .collect::() - }) - })?; - - Some( - Button::new("project_branch_trigger", branch_name) - .color(Color::Muted) - .style(ButtonStyle::Subtle) - .label_size(LabelSize::Small) - .tooltip(move |_window, cx| { - Tooltip::with_meta( - "Recent Branches", - Some(&zed_actions::git::Branch), - "Local branches only", - cx, - ) - }) - .on_click(move |_, window, cx| { - let _ = workspace.update(cx, |this, cx| { - window.focus(&this.active_pane().focus_handle(cx)); - window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx); - }); - }) - .when(settings.show_branch_icon, |branch_button| { - let (icon, icon_color) = { - let status = repo.status_summary(); - let tracked = status.index + status.worktree; - if status.conflict > 0 { - (IconName::Warning, Color::VersionControlConflict) - } else if tracked.modified > 0 { - (IconName::SquareDot, Color::VersionControlModified) - } else if tracked.added > 0 || status.untracked > 0 { - (IconName::SquarePlus, Color::VersionControlAdded) - } else if tracked.deleted > 0 { - (IconName::SquareMinus, Color::VersionControlDeleted) - } else { - (IconName::GitBranch, Color::Muted) - } - }; - - branch_button - .icon(icon) - .icon_position(IconPosition::Start) - .icon_color(icon_color) - .icon_size(IconSize::Indicator) - }), - ) - } - - fn window_activation_changed(&mut self, window: &mut Window, cx: &mut Context) { - if window.is_window_active() { - ActiveCall::global(cx) - .update(cx, |call, cx| call.set_location(Some(&self.project), cx)) - .detach_and_log_err(cx); - } else if cx.active_window().is_none() { - ActiveCall::global(cx) - .update(cx, |call, cx| call.set_location(None, cx)) - .detach_and_log_err(cx); - } - self.workspace - .update(cx, |workspace, cx| { - workspace.update_active_view_for_followers(window, cx); - }) - .ok(); - } - - fn active_call_changed(&mut self, cx: &mut Context) { - cx.notify(); - } - - fn share_project(&mut self, cx: &mut Context) { - let active_call = ActiveCall::global(cx); - let project = self.project.clone(); - active_call - .update(cx, |call, cx| call.share_project(project, cx)) - .detach_and_log_err(cx); - } - - fn unshare_project(&mut self, _: &mut Window, cx: &mut Context) { - let active_call = ActiveCall::global(cx); - let project = self.project.clone(); - active_call - .update(cx, |call, cx| call.unshare_project(project, cx)) - .log_err(); - } - - fn render_connection_status( - &self, - status: &client::Status, - cx: &mut Context, - ) -> Option { - match status { - client::Status::ConnectionError - | client::Status::ConnectionLost - | client::Status::Reauthenticating - | client::Status::Reconnecting - | client::Status::ReconnectionError { .. } => Some( - div() - .id("disconnected") - .child(Icon::new(IconName::Disconnected).size(IconSize::Small)) - .tooltip(Tooltip::text("Disconnected")) - .into_any_element(), - ), - client::Status::UpgradeRequired => { - let auto_updater = auto_update::AutoUpdater::get(cx); - let label = match auto_updater.map(|auto_update| auto_update.read(cx).status()) { - Some(AutoUpdateStatus::Updated { .. }) => "Please restart Zed to Collaborate", - Some(AutoUpdateStatus::Installing { .. }) - | Some(AutoUpdateStatus::Downloading { .. }) - | Some(AutoUpdateStatus::Checking) => "Updating...", - Some(AutoUpdateStatus::Idle) - | Some(AutoUpdateStatus::Errored { .. }) - | None => "Please update Zed to Collaborate", - }; - - Some( - Button::new("connection-status", label) - .label_size(LabelSize::Small) - .on_click(|_, window, cx| { - if let Some(auto_updater) = auto_update::AutoUpdater::get(cx) - && auto_updater.read(cx).status().is_updated() - { - workspace::reload(cx); - return; - } - auto_update::check(&Default::default(), window, cx); - }) - .into_any_element(), - ) - } - _ => None, - } - } - - pub fn render_sign_in_button(&mut self, _: &mut Context) -> Button { - let client = self.client.clone(); - Button::new("sign_in", "Sign in") - .label_size(LabelSize::Small) - .on_click(move |_, window, cx| { - let client = client.clone(); - window - .spawn(cx, async move |cx| { - client - .sign_in_with_optional_connect(true, cx) - .await - .notify_async_err(cx); - }) - .detach(); - }) - } - - pub fn render_app_menu_button(&mut self, cx: &mut Context) -> impl Element { - let user_store = self.user_store.read(cx); - let user = user_store.current_user(); - - let user_avatar = user.as_ref().map(|u| u.avatar_uri.clone()); - let user_login = user.as_ref().map(|u| u.github_login.clone()); - - let is_signed_in = user.is_some(); - - let has_subscription_period = user_store.subscription_period().is_some(); - let plan = user_store.plan().filter(|_| { - // Since the user might be on the legacy free plan we filter based on whether we have a subscription period. - has_subscription_period - }); - - let free_chip_bg = cx - .theme() - .colors() - .editor_background - .opacity(0.5) - .blend(cx.theme().colors().text_accent.opacity(0.05)); - - let pro_chip_bg = cx - .theme() - .colors() - .editor_background - .opacity(0.5) - .blend(cx.theme().colors().text_accent.opacity(0.2)); - - PopoverMenu::new("user-menu") - .anchor(Corner::TopRight) - .menu(move |window, cx| { - ContextMenu::build(window, cx, |menu, _, _cx| { - let user_login = user_login.clone(); - - let (plan_name, label_color, bg_color) = match plan { - None | Some(Plan::V1(PlanV1::ZedFree) | Plan::V2(PlanV2::ZedFree)) => { - ("Free", Color::Default, free_chip_bg) - } - Some(Plan::V1(PlanV1::ZedProTrial) | Plan::V2(PlanV2::ZedProTrial)) => { - ("Pro Trial", Color::Accent, pro_chip_bg) - } - Some(Plan::V1(PlanV1::ZedPro) | Plan::V2(PlanV2::ZedPro)) => { - ("Pro", Color::Accent, pro_chip_bg) - } - }; - - menu.when(is_signed_in, |this| { - this.custom_entry( - move |_window, _cx| { - let user_login = user_login.clone().unwrap_or_default(); - - h_flex() - .w_full() - .justify_between() - .child(Label::new(user_login)) - .child( - Chip::new(plan_name.to_string()) - .bg_color(bg_color) - .label_color(label_color), - ) - .into_any_element() - }, - move |_, cx| { - cx.open_url(&zed_urls::account_url(cx)); - }, - ) - .separator() - }) - .action("Settings", zed_actions::OpenSettings.boxed_clone()) - .action("Keymap", Box::new(zed_actions::OpenKeymap)) - .action( - "Themes…", - zed_actions::theme_selector::Toggle::default().boxed_clone(), - ) - .action( - "Icon Themes…", - zed_actions::icon_theme_selector::Toggle::default().boxed_clone(), - ) - .action( - "Extensions", - zed_actions::Extensions::default().boxed_clone(), - ) - .when(is_signed_in, |this| { - this.separator() - .action("Sign Out", client::SignOut.boxed_clone()) - }) - }) - .into() - }) - .map(|this| { - if is_signed_in && TitleBarSettings::get_global(cx).show_user_picture { - this.trigger_with_tooltip( - ButtonLike::new("user-menu") - .children(user_avatar.clone().map(|avatar| Avatar::new(avatar))), - Tooltip::text("Toggle User Menu"), - ) - } else { - this.trigger_with_tooltip( - IconButton::new("user-menu", IconName::ChevronDown) - .icon_size(IconSize::Small), - Tooltip::text("Toggle User Menu"), - ) - } - }) - .anchor(gpui::Corner::TopRight) - } -} diff --git a/crates/title_bar/src/title_bar_settings.rs b/crates/title_bar/src/title_bar_settings.rs deleted file mode 100644 index 29fae4d31e..0000000000 --- a/crates/title_bar/src/title_bar_settings.rs +++ /dev/null @@ -1,27 +0,0 @@ -use settings::{RegisterSetting, Settings, SettingsContent}; - -#[derive(Copy, Clone, Debug, RegisterSetting)] -pub struct TitleBarSettings { - pub show_branch_icon: bool, - pub show_onboarding_banner: bool, - pub show_user_picture: bool, - pub show_branch_name: bool, - pub show_project_items: bool, - pub show_sign_in: bool, - pub show_menus: bool, -} - -impl Settings for TitleBarSettings { - fn from_settings(s: &SettingsContent) -> Self { - let content = s.title_bar.clone().unwrap(); - TitleBarSettings { - show_branch_icon: content.show_branch_icon.unwrap(), - show_onboarding_banner: content.show_onboarding_banner.unwrap(), - show_user_picture: content.show_user_picture.unwrap(), - show_branch_name: content.show_branch_name.unwrap(), - show_project_items: content.show_project_items.unwrap(), - show_sign_in: content.show_sign_in.unwrap(), - show_menus: content.show_menus.unwrap(), - } - } -} diff --git a/crates/toolchain_selector/Cargo.toml b/crates/toolchain_selector/Cargo.toml deleted file mode 100644 index 94a655b727..0000000000 --- a/crates/toolchain_selector/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "toolchain_selector" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[dependencies] -anyhow.workspace = true -convert_case.workspace = true -editor.workspace = true -file_finder.workspace = true -futures.workspace = true -fuzzy.workspace = true -gpui.workspace = true -language.workspace = true -menu.workspace = true -picker.workspace = true -project.workspace = true -ui.workspace = true -util.workspace = true -workspace.workspace = true - -[lints] -workspace = true - -[lib] -path = "src/toolchain_selector.rs" -doctest = false diff --git a/crates/toolchain_selector/LICENSE-GPL b/crates/toolchain_selector/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/toolchain_selector/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/toolchain_selector/src/active_toolchain.rs b/crates/toolchain_selector/src/active_toolchain.rs deleted file mode 100644 index 03c152e3fd..0000000000 --- a/crates/toolchain_selector/src/active_toolchain.rs +++ /dev/null @@ -1,264 +0,0 @@ -use std::sync::Arc; - -use editor::Editor; -use gpui::{ - AsyncWindowContext, Context, Entity, IntoElement, ParentElement, Render, Styled, Subscription, - Task, WeakEntity, Window, div, -}; -use language::{Buffer, BufferEvent, LanguageName, Toolchain, ToolchainScope}; -use project::{Project, ProjectPath, Toolchains, WorktreeId, toolchain_store::ToolchainStoreEvent}; -use ui::{Button, ButtonCommon, Clickable, LabelSize, SharedString, Tooltip}; -use util::{maybe, rel_path::RelPath}; -use workspace::{StatusItemView, Workspace, item::ItemHandle}; - -use crate::ToolchainSelector; - -pub struct ActiveToolchain { - active_toolchain: Option, - term: SharedString, - workspace: WeakEntity, - active_buffer: Option<(WorktreeId, WeakEntity, Subscription)>, - _update_toolchain_task: Task>, -} - -impl ActiveToolchain { - pub fn new(workspace: &Workspace, window: &mut Window, cx: &mut Context) -> Self { - if let Some(store) = workspace.project().read(cx).toolchain_store() { - cx.subscribe_in( - &store, - window, - |this, _, _: &ToolchainStoreEvent, window, cx| { - let editor = this - .workspace - .update(cx, |workspace, cx| { - workspace - .active_item(cx) - .and_then(|item| item.downcast::()) - }) - .ok() - .flatten(); - if let Some(editor) = editor { - this.update_lister(editor, window, cx); - } - }, - ) - .detach(); - } - Self { - active_toolchain: None, - active_buffer: None, - term: SharedString::new_static("Toolchain"), - workspace: workspace.weak_handle(), - - _update_toolchain_task: Self::spawn_tracker_task(window, cx), - } - } - fn spawn_tracker_task(window: &mut Window, cx: &mut Context) -> Task> { - cx.spawn_in(window, async move |this, cx| { - let did_set_toolchain = maybe!(async { - let active_file = this - .read_with(cx, |this, _| { - this.active_buffer - .as_ref() - .map(|(_, buffer, _)| buffer.clone()) - }) - .ok() - .flatten()?; - let workspace = this.read_with(cx, |this, _| this.workspace.clone()).ok()?; - let language_name = active_file - .read_with(cx, |this, _| Some(this.language()?.name())) - .ok() - .flatten()?; - let meta = workspace - .update(cx, |workspace, cx| { - let languages = workspace.project().read(cx).languages(); - Project::toolchain_metadata(languages.clone(), language_name.clone()) - }) - .ok()? - .await?; - let _ = this.update(cx, |this, cx| { - this.term = meta.term; - cx.notify(); - }); - let (worktree_id, path) = active_file - .update(cx, |this, cx| { - this.file().and_then(|file| { - Some((file.worktree_id(cx), file.path().parent()?.into())) - }) - }) - .ok() - .flatten()?; - let toolchain = - Self::active_toolchain(workspace, worktree_id, path, language_name, cx).await?; - this.update(cx, |this, cx| { - this.active_toolchain = Some(toolchain); - - cx.notify(); - }) - .ok() - }) - .await - .is_some(); - if !did_set_toolchain { - this.update(cx, |this, cx| { - this.active_toolchain = None; - cx.notify(); - }) - .ok(); - } - did_set_toolchain.then_some(()) - }) - } - - fn update_lister( - &mut self, - editor: Entity, - window: &mut Window, - cx: &mut Context, - ) { - let editor = editor.read(cx); - if let Some((_, buffer, _)) = editor.active_excerpt(cx) - && let Some(worktree_id) = buffer.read(cx).file().map(|file| file.worktree_id(cx)) - { - let subscription = cx.subscribe_in( - &buffer, - window, - |this, _, event: &BufferEvent, window, cx| { - if matches!(event, BufferEvent::LanguageChanged(_)) { - this._update_toolchain_task = Self::spawn_tracker_task(window, cx); - } - }, - ); - self.active_buffer = Some((worktree_id, buffer.downgrade(), subscription)); - self._update_toolchain_task = Self::spawn_tracker_task(window, cx); - } - - cx.notify(); - } - - fn active_toolchain( - workspace: WeakEntity, - worktree_id: WorktreeId, - relative_path: Arc, - language_name: LanguageName, - cx: &mut AsyncWindowContext, - ) -> Task> { - cx.spawn(async move |cx| { - let workspace_id = workspace - .read_with(cx, |this, _| this.database_id()) - .ok() - .flatten()?; - let selected_toolchain = workspace - .update(cx, |this, cx| { - this.project().read(cx).active_toolchain( - ProjectPath { - worktree_id, - path: relative_path.clone(), - }, - language_name.clone(), - cx, - ) - }) - .ok()? - .await; - if let Some(toolchain) = selected_toolchain { - Some(toolchain) - } else { - let project = workspace - .read_with(cx, |this, _| this.project().clone()) - .ok()?; - let Toolchains { - toolchains, - root_path: relative_path, - user_toolchains, - } = cx - .update(|_, cx| { - project.read(cx).available_toolchains( - ProjectPath { - worktree_id, - path: relative_path.clone(), - }, - language_name, - cx, - ) - }) - .ok()? - .await?; - // Since we don't have a selected toolchain, pick one for user here. - let default_choice = user_toolchains - .iter() - .find_map(|(scope, toolchains)| { - if scope == &ToolchainScope::Global { - // Ignore global toolchains when making a default choice. They're unlikely to be the right choice. - None - } else { - toolchains.first() - } - }) - .or_else(|| toolchains.toolchains.first()) - .cloned(); - if let Some(toolchain) = &default_choice { - workspace::WORKSPACE_DB - .set_toolchain( - workspace_id, - worktree_id, - relative_path.clone(), - toolchain.clone(), - ) - .await - .ok()?; - project - .update(cx, |this, cx| { - this.activate_toolchain( - ProjectPath { - worktree_id, - path: relative_path, - }, - toolchain.clone(), - cx, - ) - }) - .ok()? - .await; - } - - default_choice - } - }) - } -} - -impl Render for ActiveToolchain { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let Some(active_toolchain) = self.active_toolchain.as_ref() else { - return div().hidden(); - }; - - div().child( - Button::new("change-toolchain", active_toolchain.name.clone()) - .label_size(LabelSize::Small) - .on_click(cx.listener(|this, _, window, cx| { - if let Some(workspace) = this.workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - ToolchainSelector::toggle(workspace, window, cx) - }); - } - })) - .tooltip(Tooltip::text(format!("Select {}", &self.term))), - ) - } -} - -impl StatusItemView for ActiveToolchain { - fn set_active_pane_item( - &mut self, - active_pane_item: Option<&dyn ItemHandle>, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(editor) = active_pane_item.and_then(|item| item.downcast::()) { - self.update_lister(editor, window, cx); - } - cx.notify(); - } -} diff --git a/crates/toolchain_selector/src/toolchain_selector.rs b/crates/toolchain_selector/src/toolchain_selector.rs deleted file mode 100644 index 138f99066f..0000000000 --- a/crates/toolchain_selector/src/toolchain_selector.rs +++ /dev/null @@ -1,1142 +0,0 @@ -mod active_toolchain; - -pub use active_toolchain::ActiveToolchain; -use convert_case::Casing as _; -use editor::Editor; -use file_finder::OpenPathDelegate; -use futures::channel::oneshot; -use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; -use gpui::{ - Action, Animation, AnimationExt, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, - Focusable, KeyContext, ParentElement, Render, Styled, Subscription, Task, WeakEntity, Window, - actions, pulsating_between, -}; -use language::{Language, LanguageName, Toolchain, ToolchainScope}; -use picker::{Picker, PickerDelegate}; -use project::{DirectoryLister, Project, ProjectPath, Toolchains, WorktreeId}; -use std::{ - borrow::Cow, - path::{Path, PathBuf}, - sync::Arc, - time::Duration, -}; -use ui::{ - Divider, HighlightedLabel, KeyBinding, List, ListItem, ListItemSpacing, Navigable, - NavigableEntry, prelude::*, -}; -use util::{ResultExt, maybe, paths::PathStyle, rel_path::RelPath}; -use workspace::{ModalView, Workspace}; - -actions!( - toolchain, - [ - /// Selects a toolchain for the current project. - Select, - /// Adds a new toolchain for the current project. - AddToolchain - ] -); - -pub fn init(cx: &mut App) { - cx.observe_new(ToolchainSelector::register).detach(); -} - -pub struct ToolchainSelector { - state: State, - create_search_state: Arc) -> SearchState + 'static>, - language: Option>, - project: Entity, - language_name: LanguageName, - worktree_id: WorktreeId, - relative_path: Arc, -} - -#[derive(Clone)] -struct SearchState { - picker: Entity>, -} - -struct AddToolchainState { - state: AddState, - project: Entity, - language_name: LanguageName, - root_path: ProjectPath, - weak: WeakEntity, -} - -struct ScopePickerState { - entries: [NavigableEntry; 3], - selected_scope: ToolchainScope, -} - -#[expect( - dead_code, - reason = "These tasks have to be kept alive to run to completion" -)] -enum PathInputState { - WaitingForPath(Task<()>), - Resolving(Task<()>), -} - -enum AddState { - Path { - picker: Entity>, - error: Option>, - input_state: PathInputState, - _subscription: Subscription, - }, - Name { - toolchain: Toolchain, - editor: Entity, - scope_picker: ScopePickerState, - }, -} - -impl AddToolchainState { - fn new( - project: Entity, - language_name: LanguageName, - root_path: ProjectPath, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - let weak = cx.weak_entity(); - - cx.new(|cx| { - let (lister, rx) = Self::create_path_browser_delegate(project.clone(), cx); - let picker = cx.new(|cx| Picker::uniform_list(lister, window, cx)); - Self { - state: AddState::Path { - _subscription: cx.subscribe(&picker, |_, _, _: &DismissEvent, cx| { - cx.stop_propagation(); - }), - picker, - error: None, - input_state: Self::wait_for_path(rx, window, cx), - }, - project, - language_name, - root_path, - weak, - } - }) - } - - fn create_path_browser_delegate( - project: Entity, - cx: &mut Context, - ) -> (OpenPathDelegate, oneshot::Receiver>>) { - let (tx, rx) = oneshot::channel(); - let weak = cx.weak_entity(); - let path_style = project.read(cx).path_style(cx); - let lister = - OpenPathDelegate::new(tx, DirectoryLister::Project(project), false, path_style) - .show_hidden() - .with_footer(Arc::new(move |_, cx| { - let error = weak - .read_with(cx, |this, _| { - if let AddState::Path { error, .. } = &this.state { - error.clone() - } else { - None - } - }) - .ok() - .flatten(); - let is_loading = weak - .read_with(cx, |this, _| { - matches!( - this.state, - AddState::Path { - input_state: PathInputState::Resolving(_), - .. - } - ) - }) - .unwrap_or_default(); - Some( - v_flex() - .child(Divider::horizontal()) - .child( - h_flex() - .p_1() - .justify_between() - .gap_2() - .child( - Label::new("Select Toolchain Path") - .color(Color::Muted) - .map(|this| { - if is_loading { - this.with_animation( - "select-toolchain-label", - Animation::new(Duration::from_secs(2)) - .repeat() - .with_easing(pulsating_between( - 0.4, 0.8, - )), - |label, delta| label.alpha(delta), - ) - .into_any() - } else { - this.into_any_element() - } - }), - ) - .when_some(error, |this, error| { - this.child(Label::new(error).color(Color::Error)) - }), - ) - .into_any(), - ) - })); - - (lister, rx) - } - fn resolve_path( - path: PathBuf, - root_path: ProjectPath, - language_name: LanguageName, - project: Entity, - window: &mut Window, - cx: &mut Context, - ) -> PathInputState { - PathInputState::Resolving(cx.spawn_in(window, async move |this, cx| { - _ = maybe!(async move { - let toolchain = project - .update(cx, |this, cx| { - this.resolve_toolchain(path.clone(), language_name, cx) - })? - .await; - let Ok(toolchain) = toolchain else { - // Go back to the path input state - _ = this.update_in(cx, |this, window, cx| { - if let AddState::Path { - input_state, - picker, - error, - .. - } = &mut this.state - && matches!(input_state, PathInputState::Resolving(_)) - { - let Err(e) = toolchain else { unreachable!() }; - *error = Some(Arc::from(e.to_string())); - let (delegate, rx) = - Self::create_path_browser_delegate(this.project.clone(), cx); - picker.update(cx, |picker, cx| { - *picker = Picker::uniform_list(delegate, window, cx); - picker.set_query( - Arc::from(path.to_string_lossy().as_ref()), - window, - cx, - ); - }); - *input_state = Self::wait_for_path(rx, window, cx); - this.focus_handle(cx).focus(window); - } - }); - return Err(anyhow::anyhow!("Failed to resolve toolchain")); - }; - let resolved_toolchain_path = project.read_with(cx, |this, cx| { - this.find_project_path(&toolchain.path.as_ref(), cx) - })?; - - // Suggest a default scope based on the applicability. - let scope = if let Some(project_path) = resolved_toolchain_path { - if !root_path.path.as_ref().is_empty() && project_path.starts_with(&root_path) { - ToolchainScope::Subproject(root_path.worktree_id, root_path.path) - } else { - ToolchainScope::Project - } - } else { - // This path lies outside of the project. - ToolchainScope::Global - }; - - _ = this.update_in(cx, |this, window, cx| { - let scope_picker = ScopePickerState { - entries: std::array::from_fn(|_| NavigableEntry::focusable(cx)), - selected_scope: scope, - }; - this.state = AddState::Name { - editor: cx.new(|cx| { - let mut editor = Editor::single_line(window, cx); - editor.set_text(toolchain.name.as_ref(), window, cx); - editor - }), - toolchain, - scope_picker, - }; - this.focus_handle(cx).focus(window); - }); - - Result::<_, anyhow::Error>::Ok(()) - }) - .await; - })) - } - - fn wait_for_path( - rx: oneshot::Receiver>>, - window: &mut Window, - cx: &mut Context, - ) -> PathInputState { - let task = cx.spawn_in(window, async move |this, cx| { - maybe!(async move { - let result = rx.await.log_err()?; - - let path = result - .into_iter() - .flat_map(|paths| paths.into_iter()) - .next()?; - this.update_in(cx, |this, window, cx| { - if let AddState::Path { - input_state, error, .. - } = &mut this.state - && matches!(input_state, PathInputState::WaitingForPath(_)) - { - error.take(); - *input_state = Self::resolve_path( - path, - this.root_path.clone(), - this.language_name.clone(), - this.project.clone(), - window, - cx, - ); - } - }) - .ok()?; - Some(()) - }) - .await; - }); - PathInputState::WaitingForPath(task) - } - - fn confirm_toolchain( - &mut self, - _: &menu::Confirm, - window: &mut Window, - cx: &mut Context, - ) { - let AddState::Name { - toolchain, - editor, - scope_picker, - } = &mut self.state - else { - return; - }; - - let text = editor.read(cx).text(cx); - if text.is_empty() { - return; - } - - toolchain.name = SharedString::from(text); - self.project.update(cx, |this, cx| { - this.add_toolchain(toolchain.clone(), scope_picker.selected_scope.clone(), cx); - }); - _ = self.weak.update(cx, |this, cx| { - this.state = State::Search((this.create_search_state)(window, cx)); - this.focus_handle(cx).focus(window); - cx.notify(); - }); - } -} -impl Focusable for AddToolchainState { - fn focus_handle(&self, cx: &App) -> FocusHandle { - match &self.state { - AddState::Path { picker, .. } => picker.focus_handle(cx), - AddState::Name { editor, .. } => editor.focus_handle(cx), - } - } -} - -impl AddToolchainState { - fn select_scope(&mut self, scope: ToolchainScope, cx: &mut Context) { - if let AddState::Name { scope_picker, .. } = &mut self.state { - scope_picker.selected_scope = scope; - cx.notify(); - } - } -} - -impl Focusable for State { - fn focus_handle(&self, cx: &App) -> FocusHandle { - match self { - State::Search(state) => state.picker.focus_handle(cx), - State::AddToolchain(state) => state.focus_handle(cx), - } - } -} -impl Render for AddToolchainState { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let theme = cx.theme().clone(); - let weak = self.weak.upgrade(); - let label = SharedString::new_static("Add"); - - v_flex() - .size_full() - // todo: These modal styles shouldn't be needed as the modal picker already has `elevation_3` - // They get duplicated in the middle state of adding a virtual env, but then are needed for this last state - .bg(cx.theme().colors().elevated_surface_background) - .border_1() - .border_color(cx.theme().colors().border_variant) - .rounded_lg() - .when_some(weak, |this, weak| { - this.on_action(window.listener_for( - &weak, - |this: &mut ToolchainSelector, _: &menu::Cancel, window, cx| { - this.state = State::Search((this.create_search_state)(window, cx)); - this.state.focus_handle(cx).focus(window); - cx.notify(); - }, - )) - }) - .on_action(cx.listener(Self::confirm_toolchain)) - .map(|this| match &self.state { - AddState::Path { picker, .. } => this.child(picker.clone()), - AddState::Name { - editor, - scope_picker, - .. - } => { - let scope_options = [ - ToolchainScope::Global, - ToolchainScope::Project, - ToolchainScope::Subproject( - self.root_path.worktree_id, - self.root_path.path.clone(), - ), - ]; - - let mut navigable_scope_picker = Navigable::new( - v_flex() - .child( - h_flex() - .w_full() - .p_2() - .border_b_1() - .border_color(theme.colors().border) - .child(editor.clone()), - ) - .child( - v_flex() - .child( - Label::new("Scope") - .size(LabelSize::Small) - .color(Color::Muted) - .mt_1() - .ml_2(), - ) - .child(List::new().children( - scope_options.iter().enumerate().map(|(i, scope)| { - let is_selected = *scope == scope_picker.selected_scope; - let label = scope.label(); - let description = scope.description(); - let scope_clone_for_action = scope.clone(); - let scope_clone_for_click = scope.clone(); - - div() - .id(SharedString::from(format!("scope-option-{i}"))) - .track_focus(&scope_picker.entries[i].focus_handle) - .on_action(cx.listener( - move |this, _: &menu::Confirm, _, cx| { - this.select_scope( - scope_clone_for_action.clone(), - cx, - ); - }, - )) - .child( - ListItem::new(SharedString::from(format!( - "scope-{i}" - ))) - .toggle_state( - is_selected - || scope_picker.entries[i] - .focus_handle - .contains_focused(window, cx), - ) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .child( - h_flex() - .gap_2() - .child(Label::new(label)) - .child( - Label::new(description) - .size(LabelSize::Small) - .color(Color::Muted), - ), - ) - .on_click(cx.listener(move |this, _, _, cx| { - this.select_scope( - scope_clone_for_click.clone(), - cx, - ); - })), - ) - }), - )) - .child(Divider::horizontal()) - .child(h_flex().p_1p5().justify_end().map(|this| { - let is_disabled = editor.read(cx).is_empty(cx); - let handle = self.focus_handle(cx); - this.child( - Button::new("add-toolchain", label) - .disabled(is_disabled) - .key_binding(KeyBinding::for_action_in( - &menu::Confirm, - &handle, - cx, - )) - .on_click(cx.listener(|this, _, window, cx| { - this.confirm_toolchain( - &menu::Confirm, - window, - cx, - ); - })) - .map(|this| { - if false { - this.with_animation( - "inspecting-user-toolchain", - Animation::new(Duration::from_millis( - 500, - )) - .repeat() - .with_easing(pulsating_between( - 0.4, 0.8, - )), - |label, delta| label.alpha(delta), - ) - .into_any() - } else { - this.into_any_element() - } - }), - ) - })), - ) - .into_any_element(), - ); - - for entry in &scope_picker.entries { - navigable_scope_picker = navigable_scope_picker.entry(entry.clone()); - } - - this.child(navigable_scope_picker.render(window, cx)) - } - }) - } -} - -#[derive(Clone)] -enum State { - Search(SearchState), - AddToolchain(Entity), -} - -impl RenderOnce for State { - fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement { - match self { - State::Search(state) => state.picker.into_any_element(), - State::AddToolchain(state) => state.into_any_element(), - } - } -} -impl ToolchainSelector { - fn register( - workspace: &mut Workspace, - _window: Option<&mut Window>, - _: &mut Context, - ) { - workspace.register_action(move |workspace, _: &Select, window, cx| { - Self::toggle(workspace, window, cx); - }); - workspace.register_action(move |workspace, _: &AddToolchain, window, cx| { - let Some(toolchain_selector) = workspace.active_modal::(cx) else { - Self::toggle(workspace, window, cx); - return; - }; - - toolchain_selector.update(cx, |toolchain_selector, cx| { - toolchain_selector.handle_add_toolchain(&AddToolchain, window, cx); - }); - }); - } - - fn toggle( - workspace: &mut Workspace, - window: &mut Window, - cx: &mut Context, - ) -> Option<()> { - let (_, buffer, _) = workspace - .active_item(cx)? - .act_as::(cx)? - .read(cx) - .active_excerpt(cx)?; - let project = workspace.project().clone(); - - let language_name = buffer.read(cx).language()?.name(); - let worktree_id = buffer.read(cx).file()?.worktree_id(cx); - let relative_path: Arc = buffer.read(cx).file()?.path().parent()?.into(); - let worktree_root_path = project - .read(cx) - .worktree_for_id(worktree_id, cx)? - .read(cx) - .abs_path(); - let weak = workspace.weak_handle(); - cx.spawn_in(window, async move |workspace, cx| { - let active_toolchain = project - .read_with(cx, |this, cx| { - this.active_toolchain( - ProjectPath { - worktree_id, - path: relative_path.clone(), - }, - language_name.clone(), - cx, - ) - })? - .await; - workspace - .update_in(cx, |this, window, cx| { - this.toggle_modal(window, cx, move |window, cx| { - ToolchainSelector::new( - weak, - project, - active_toolchain, - worktree_id, - worktree_root_path, - relative_path, - language_name, - window, - cx, - ) - }); - }) - .ok(); - anyhow::Ok(()) - }) - .detach(); - - Some(()) - } - - fn new( - workspace: WeakEntity, - project: Entity, - active_toolchain: Option, - worktree_id: WorktreeId, - worktree_root: Arc, - relative_path: Arc, - language_name: LanguageName, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let language_registry = project.read(cx).languages().clone(); - cx.spawn({ - let language_name = language_name.clone(); - async move |this, cx| { - let language = language_registry - .language_for_name(&language_name.0) - .await - .ok(); - this.update(cx, |this, cx| { - this.language = language; - cx.notify(); - }) - .ok(); - } - }) - .detach(); - let project_clone = project.clone(); - let language_name_clone = language_name.clone(); - let relative_path_clone = relative_path.clone(); - - let create_search_state = Arc::new(move |window: &mut Window, cx: &mut Context| { - let toolchain_selector = cx.entity().downgrade(); - let picker = cx.new(|cx| { - let delegate = ToolchainSelectorDelegate::new( - active_toolchain.clone(), - toolchain_selector, - workspace.clone(), - worktree_id, - worktree_root.clone(), - project_clone.clone(), - relative_path_clone.clone(), - language_name_clone.clone(), - window, - cx, - ); - Picker::uniform_list(delegate, window, cx) - }); - let picker_focus_handle = picker.focus_handle(cx); - picker.update(cx, |picker, _| { - picker.delegate.focus_handle = picker_focus_handle.clone(); - }); - SearchState { picker } - }); - - Self { - state: State::Search(create_search_state(window, cx)), - create_search_state, - language: None, - project, - language_name, - worktree_id, - relative_path, - } - } - - fn handle_add_toolchain( - &mut self, - _: &AddToolchain, - window: &mut Window, - cx: &mut Context, - ) { - if matches!(self.state, State::Search(_)) { - self.state = State::AddToolchain(AddToolchainState::new( - self.project.clone(), - self.language_name.clone(), - ProjectPath { - worktree_id: self.worktree_id, - path: self.relative_path.clone(), - }, - window, - cx, - )); - self.state.focus_handle(cx).focus(window); - cx.notify(); - } - } -} - -impl Render for ToolchainSelector { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let mut key_context = KeyContext::new_with_defaults(); - key_context.add("ToolchainSelector"); - - v_flex() - .key_context(key_context) - .w(rems(34.)) - .on_action(cx.listener(Self::handle_add_toolchain)) - .child(self.state.clone().render(window, cx)) - } -} - -impl Focusable for ToolchainSelector { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.state.focus_handle(cx) - } -} - -impl EventEmitter for ToolchainSelector {} -impl ModalView for ToolchainSelector {} - -pub struct ToolchainSelectorDelegate { - toolchain_selector: WeakEntity, - candidates: Arc<[(Toolchain, Option)]>, - matches: Vec, - selected_index: usize, - workspace: WeakEntity, - worktree_id: WorktreeId, - worktree_abs_path_root: Arc, - relative_path: Arc, - placeholder_text: Arc, - add_toolchain_text: Arc, - project: Entity, - focus_handle: FocusHandle, - _fetch_candidates_task: Task>, -} - -impl ToolchainSelectorDelegate { - fn new( - active_toolchain: Option, - toolchain_selector: WeakEntity, - workspace: WeakEntity, - worktree_id: WorktreeId, - worktree_abs_path_root: Arc, - project: Entity, - relative_path: Arc, - language_name: LanguageName, - window: &mut Window, - cx: &mut Context>, - ) -> Self { - let _project = project.clone(); - let path_style = project.read(cx).path_style(cx); - - let _fetch_candidates_task = cx.spawn_in(window, { - async move |this, cx| { - let meta = _project - .read_with(cx, |this, _| { - Project::toolchain_metadata(this.languages().clone(), language_name.clone()) - }) - .ok()? - .await?; - let relative_path = this - .update(cx, |this, cx| { - this.delegate.add_toolchain_text = format!( - "Add {}", - meta.term.as_ref().to_case(convert_case::Case::Title) - ) - .into(); - cx.notify(); - this.delegate.relative_path.clone() - }) - .ok()?; - - let Toolchains { - toolchains: available_toolchains, - root_path: relative_path, - user_toolchains, - } = _project - .update(cx, |this, cx| { - this.available_toolchains( - ProjectPath { - worktree_id, - path: relative_path.clone(), - }, - language_name, - cx, - ) - }) - .ok()? - .await?; - let pretty_path = { - if relative_path.is_empty() { - Cow::Borrowed("worktree root") - } else { - Cow::Owned(format!("`{}`", relative_path.display(path_style))) - } - }; - let placeholder_text = - format!("Select a {} for {pretty_path}…", meta.term.to_lowercase(),).into(); - let _ = this.update_in(cx, move |this, window, cx| { - this.delegate.relative_path = relative_path; - this.delegate.placeholder_text = placeholder_text; - this.refresh_placeholder(window, cx); - }); - - let _ = this.update_in(cx, move |this, window, cx| { - this.delegate.candidates = user_toolchains - .into_iter() - .flat_map(|(scope, toolchains)| { - toolchains - .into_iter() - .map(move |toolchain| (toolchain, Some(scope.clone()))) - }) - .chain( - available_toolchains - .toolchains - .into_iter() - .map(|toolchain| (toolchain, None)), - ) - .collect(); - - if let Some(active_toolchain) = active_toolchain - && let Some(position) = this - .delegate - .candidates - .iter() - .position(|(toolchain, _)| *toolchain == active_toolchain) - { - this.delegate.set_selected_index(position, window, cx); - } - this.update_matches(this.query(cx), window, cx); - }); - - Some(()) - } - }); - let placeholder_text = "Select a toolchain…".to_string().into(); - Self { - toolchain_selector, - candidates: Default::default(), - matches: vec![], - selected_index: 0, - workspace, - worktree_id, - worktree_abs_path_root, - placeholder_text, - relative_path, - _fetch_candidates_task, - project, - focus_handle: cx.focus_handle(), - add_toolchain_text: Arc::from("Add Toolchain"), - } - } - fn relativize_path( - path: SharedString, - worktree_root: &Path, - path_style: PathStyle, - ) -> SharedString { - Path::new(&path.as_ref()) - .strip_prefix(&worktree_root) - .ok() - .and_then(|suffix| suffix.to_str()) - .map(|suffix| format!(".{}{suffix}", path_style.primary_separator()).into()) - .unwrap_or(path) - } -} - -impl PickerDelegate for ToolchainSelectorDelegate { - type ListItem = ListItem; - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - self.placeholder_text.clone() - } - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { - if let Some(string_match) = self.matches.get(self.selected_index) { - let (toolchain, _) = self.candidates[string_match.candidate_id].clone(); - if let Some(workspace_id) = self - .workspace - .read_with(cx, |this, _| this.database_id()) - .ok() - .flatten() - { - let workspace = self.workspace.clone(); - let worktree_id = self.worktree_id; - let path = self.relative_path.clone(); - let relative_path = self.relative_path.clone(); - cx.spawn_in(window, async move |_, cx| { - workspace::WORKSPACE_DB - .set_toolchain(workspace_id, worktree_id, relative_path, toolchain.clone()) - .await - .log_err(); - workspace - .update(cx, |this, cx| { - this.project().update(cx, |this, cx| { - this.activate_toolchain( - ProjectPath { worktree_id, path }, - toolchain, - cx, - ) - }) - }) - .ok()? - .await; - Some(()) - }) - .detach(); - } - } - self.dismissed(window, cx); - } - - fn dismissed(&mut self, _: &mut Window, cx: &mut Context>) { - self.toolchain_selector - .update(cx, |_, cx| cx.emit(DismissEvent)) - .log_err(); - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index( - &mut self, - ix: usize, - _window: &mut Window, - _: &mut Context>, - ) { - self.selected_index = ix; - } - - fn update_matches( - &mut self, - query: String, - window: &mut Window, - cx: &mut Context>, - ) -> gpui::Task<()> { - let background = cx.background_executor().clone(); - let candidates = self.candidates.clone(); - let worktree_root_path = self.worktree_abs_path_root.clone(); - let path_style = self.project.read(cx).path_style(cx); - cx.spawn_in(window, async move |this, cx| { - let matches = if query.is_empty() { - candidates - .into_iter() - .enumerate() - .map(|(index, (candidate, _))| { - let path = Self::relativize_path( - candidate.path.clone(), - &worktree_root_path, - path_style, - ); - let string = format!("{}{}", candidate.name, path); - StringMatch { - candidate_id: index, - string, - positions: Vec::new(), - score: 0.0, - } - }) - .collect() - } else { - let candidates = candidates - .into_iter() - .enumerate() - .map(|(candidate_id, (toolchain, _))| { - let path = Self::relativize_path( - toolchain.path.clone(), - &worktree_root_path, - path_style, - ); - let string = format!("{}{}", toolchain.name, path); - StringMatchCandidate::new(candidate_id, &string) - }) - .collect::>(); - match_strings( - &candidates, - &query, - false, - true, - 100, - &Default::default(), - background, - ) - .await - }; - - this.update(cx, |this, cx| { - let delegate = &mut this.delegate; - delegate.matches = matches; - delegate.selected_index = delegate - .selected_index - .min(delegate.matches.len().saturating_sub(1)); - cx.notify(); - }) - .log_err(); - }) - } - - fn render_match( - &self, - ix: usize, - selected: bool, - _: &mut Window, - cx: &mut Context>, - ) -> Option { - let mat = &self.matches.get(ix)?; - let (toolchain, scope) = &self.candidates.get(mat.candidate_id)?; - - let label = toolchain.name.clone(); - let path_style = self.project.read(cx).path_style(cx); - let path = Self::relativize_path( - toolchain.path.clone(), - &self.worktree_abs_path_root, - path_style, - ); - let (name_highlights, mut path_highlights) = mat - .positions - .iter() - .cloned() - .partition::, _>(|index| *index < label.len()); - path_highlights.iter_mut().for_each(|index| { - *index -= label.len(); - }); - let id: SharedString = format!("toolchain-{ix}",).into(); - Some( - ListItem::new(id) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .toggle_state(selected) - .child(HighlightedLabel::new(label, name_highlights)) - .child( - HighlightedLabel::new(path, path_highlights) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .when_some(scope.as_ref(), |this, scope| { - let id: SharedString = format!( - "delete-custom-toolchain-{}-{}", - toolchain.name, toolchain.path - ) - .into(); - let toolchain = toolchain.clone(); - let scope = scope.clone(); - - this.end_slot(IconButton::new(id, IconName::Trash).on_click(cx.listener( - move |this, _, _, cx| { - this.delegate.project.update(cx, |this, cx| { - this.remove_toolchain(toolchain.clone(), scope.clone(), cx) - }); - - this.delegate.matches.retain_mut(|m| { - if m.candidate_id == ix { - return false; - } else if m.candidate_id > ix { - m.candidate_id -= 1; - } - true - }); - - this.delegate.candidates = this - .delegate - .candidates - .iter() - .enumerate() - .filter_map(|(i, toolchain)| (ix != i).then_some(toolchain.clone())) - .collect(); - - if this.delegate.selected_index >= ix { - this.delegate.selected_index = - this.delegate.selected_index.saturating_sub(1); - } - cx.stop_propagation(); - cx.notify(); - }, - ))) - }), - ) - } - fn render_footer( - &self, - _window: &mut Window, - cx: &mut Context>, - ) -> Option { - Some( - v_flex() - .rounded_b_md() - .child(Divider::horizontal()) - .child( - h_flex() - .p_1p5() - .gap_0p5() - .justify_end() - .child( - Button::new("xd", self.add_toolchain_text.clone()) - .key_binding(KeyBinding::for_action_in( - &AddToolchain, - &self.focus_handle, - cx, - )) - .on_click(|_, window, cx| { - window.dispatch_action(Box::new(AddToolchain), cx) - }), - ) - .child( - Button::new("select", "Select") - .key_binding(KeyBinding::for_action_in( - &menu::Confirm, - &self.focus_handle, - cx, - )) - .on_click(|_, window, cx| { - window.dispatch_action(menu::Confirm.boxed_clone(), cx) - }), - ), - ) - .into_any_element(), - ) - } -} diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml deleted file mode 100644 index 5eb58bf1da..0000000000 --- a/crates/ui/Cargo.toml +++ /dev/null @@ -1,42 +0,0 @@ -[package] -name = "ui" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -name = "ui" -path = "src/ui.rs" - -[dependencies] -chrono.workspace = true -component.workspace = true -documented.workspace = true -gpui.workspace = true -gpui_macros.workspace = true -icons.workspace = true -itertools.workspace = true -menu.workspace = true -schemars.workspace = true -serde.workspace = true -settings.workspace = true -smallvec.workspace = true -story = { workspace = true, optional = true } -strum.workspace = true -theme.workspace = true -ui_macros.workspace = true -util.workspace = true - -[target.'cfg(windows)'.dependencies] -windows.workspace = true - -[dev-dependencies] -gpui = { workspace = true, features = ["test-support"] } - -[features] -default = [] -stories = ["dep:story"] diff --git a/crates/ui/LICENSE-GPL b/crates/ui/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/ui/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/ui/src/component_prelude.rs b/crates/ui/src/component_prelude.rs deleted file mode 100644 index 0a01372970..0000000000 --- a/crates/ui/src/component_prelude.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub use component::{ - Component, ComponentId, ComponentScope, ComponentStatus, example_group, - example_group_with_title, single_example, -}; -pub use documented::Documented; -pub use ui_macros::RegisterComponent; diff --git a/crates/ui/src/components.rs b/crates/ui/src/components.rs deleted file mode 100644 index b6318f18c9..0000000000 --- a/crates/ui/src/components.rs +++ /dev/null @@ -1,89 +0,0 @@ -mod avatar; -mod banner; -mod button; -mod callout; -mod chip; -mod content_group; -mod context_menu; -mod data_table; -mod diff_stat; -mod disclosure; -mod divider; -mod dropdown_menu; -mod facepile; -mod group; -mod icon; -mod image; -mod indent_guides; -mod indicator; -mod keybinding; -mod keybinding_hint; -mod label; -mod list; -mod modal; -mod navigable; -mod notification; -mod popover; -mod popover_menu; -mod progress; -mod radio; -mod right_click_menu; -mod scrollbar; -mod settings_container; -mod settings_group; -mod stack; -mod sticky_items; -mod tab; -mod tab_bar; -mod thread_item; -mod toggle; -mod tooltip; -mod tree_view_item; - -#[cfg(feature = "stories")] -mod stories; - -pub use avatar::*; -pub use banner::*; -pub use button::*; -pub use callout::*; -pub use chip::*; -pub use content_group::*; -pub use context_menu::*; -pub use data_table::*; -pub use diff_stat::*; -pub use disclosure::*; -pub use divider::*; -pub use dropdown_menu::*; -pub use facepile::*; -pub use group::*; -pub use icon::*; -pub use image::*; -pub use indent_guides::*; -pub use indicator::*; -pub use keybinding::*; -pub use keybinding_hint::*; -pub use label::*; -pub use list::*; -pub use modal::*; -pub use navigable::*; -pub use notification::*; -pub use popover::*; -pub use popover_menu::*; -pub use progress::*; -pub use radio::*; -pub use right_click_menu::*; -pub use scrollbar::*; -pub use settings_container::*; -pub use settings_group::*; -pub use stack::*; -pub use sticky_items::*; -pub use tab::*; -pub use tab_bar::*; -pub use thread_item::*; -pub use toggle::*; -pub use tooltip::*; -pub use tree_view_item::*; - -#[cfg(feature = "stories")] -pub use stories::*; diff --git a/crates/ui/src/components/avatar.rs b/crates/ui/src/components/avatar.rs deleted file mode 100644 index 7b2ba8ce5c..0000000000 --- a/crates/ui/src/components/avatar.rs +++ /dev/null @@ -1,303 +0,0 @@ -use crate::prelude::*; - -use documented::Documented; -use gpui::{AnyElement, Hsla, ImageSource, Img, IntoElement, Styled, img}; - -/// An element that renders a user avatar with customizable appearance options. -/// -/// # Examples -/// -/// ``` -/// use ui::Avatar; -/// -/// Avatar::new("path/to/image.png") -/// .grayscale(true) -/// .border_color(gpui::red()); -/// ``` -#[derive(IntoElement, Documented, RegisterComponent)] -pub struct Avatar { - image: Img, - size: Option, - border_color: Option, - indicator: Option, -} - -impl Avatar { - /// Creates a new avatar element with the specified image source. - pub fn new(src: impl Into) -> Self { - Avatar { - image: img(src), - size: None, - border_color: None, - indicator: None, - } - } - - /// Applies a grayscale filter to the avatar image. - /// - /// # Examples - /// - /// ``` - /// use ui::Avatar; - /// - /// let avatar = Avatar::new("path/to/image.png").grayscale(true); - /// ``` - pub fn grayscale(mut self, grayscale: bool) -> Self { - self.image = self.image.grayscale(grayscale); - self - } - - /// Sets the border color of the avatar. - /// - /// This might be used to match the border to the background color of - /// the parent element to create the illusion of cropping another - /// shape underneath (for example in face piles.) - pub fn border_color(mut self, color: impl Into) -> Self { - self.border_color = Some(color.into()); - self - } - - /// Size overrides the avatar size. By default they are 1rem. - pub fn size>(mut self, size: impl Into>) -> Self { - self.size = size.into().map(Into::into); - self - } - - /// Sets the current indicator to be displayed on the avatar, if any. - pub fn indicator(mut self, indicator: impl Into>) -> Self { - self.indicator = indicator.into().map(IntoElement::into_any_element); - self - } -} - -impl RenderOnce for Avatar { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let border_width = if self.border_color.is_some() { - px(2.) - } else { - px(0.) - }; - - let image_size = self.size.unwrap_or_else(|| rems(1.).into()); - let container_size = image_size.to_pixels(window.rem_size()) + border_width * 2.; - - div() - .size(container_size) - .rounded_full() - .when_some(self.border_color, |this, color| { - this.border(border_width).border_color(color) - }) - .child( - self.image - .size(image_size) - .rounded_full() - .bg(cx.theme().colors().element_disabled) - .with_fallback(|| { - h_flex() - .size_full() - .justify_center() - .child( - Icon::new(IconName::Person) - .color(Color::Muted) - .size(IconSize::Small), - ) - .into_any_element() - }), - ) - .children(self.indicator.map(|indicator| div().child(indicator))) - } -} - -use gpui::AnyView; - -/// The audio status of an player, for use in representing -/// their status visually on their avatar. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -pub enum AudioStatus { - /// The player's microphone is muted. - Muted, - /// The player's microphone is muted, and collaboration audio is disabled. - Deafened, -} - -/// An indicator that shows the audio status of a player. -#[derive(IntoElement)] -pub struct AvatarAudioStatusIndicator { - audio_status: AudioStatus, - tooltip: Option AnyView>>, -} - -impl AvatarAudioStatusIndicator { - /// Creates a new `AvatarAudioStatusIndicator` - pub fn new(audio_status: AudioStatus) -> Self { - Self { - audio_status, - tooltip: None, - } - } - - /// Sets the tooltip for the indicator. - pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self { - self.tooltip = Some(Box::new(tooltip)); - self - } -} - -impl RenderOnce for AvatarAudioStatusIndicator { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let icon_size = IconSize::Indicator; - - let width_in_px = icon_size.rems() * window.rem_size(); - let padding_x = px(4.); - - div() - .absolute() - .bottom(rems_from_px(-3.)) - .right(rems_from_px(-6.)) - .w(width_in_px + padding_x) - .h(icon_size.rems()) - .child( - h_flex() - .id("muted-indicator") - .justify_center() - .px(padding_x) - .py(px(2.)) - .bg(cx.theme().status().error_background) - .rounded_sm() - .child( - Icon::new(match self.audio_status { - AudioStatus::Muted => IconName::MicMute, - AudioStatus::Deafened => IconName::AudioOff, - }) - .size(icon_size) - .color(Color::Error), - ) - .when_some(self.tooltip, |this, tooltip| { - this.tooltip(move |window, cx| tooltip(window, cx)) - }), - ) - } -} - -/// Represents the availability status of a collaborator. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -pub enum CollaboratorAvailability { - Free, - Busy, -} - -/// Represents the availability and presence status of a collaborator. -#[derive(IntoElement)] -pub struct AvatarAvailabilityIndicator { - availability: CollaboratorAvailability, - avatar_size: Option, -} - -impl AvatarAvailabilityIndicator { - /// Creates a new indicator - pub fn new(availability: CollaboratorAvailability) -> Self { - Self { - availability, - avatar_size: None, - } - } - - /// Sets the size of the [`Avatar`](crate::Avatar) this indicator appears on. - pub fn avatar_size(mut self, size: impl Into>) -> Self { - self.avatar_size = size.into(); - self - } -} - -impl RenderOnce for AvatarAvailabilityIndicator { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let avatar_size = self.avatar_size.unwrap_or_else(|| window.rem_size()); - - // HACK: non-integer sizes result in oval indicators. - let indicator_size = (avatar_size * 0.4).round(); - - div() - .absolute() - .bottom_0() - .right_0() - .size(indicator_size) - .rounded(indicator_size) - .bg(match self.availability { - CollaboratorAvailability::Free => cx.theme().status().created, - CollaboratorAvailability::Busy => cx.theme().status().deleted, - }) - } -} - -// View this component preview using `workspace: open component-preview` -impl Component for Avatar { - fn scope() -> ComponentScope { - ComponentScope::Collaboration - } - - fn description() -> Option<&'static str> { - Some(Avatar::DOCS) - } - - fn preview(_window: &mut Window, cx: &mut App) -> Option { - let example_avatar = "https://avatars.githubusercontent.com/u/1714999?v=4"; - - Some( - v_flex() - .gap_6() - .children(vec![ - example_group(vec![ - single_example("Default", Avatar::new(example_avatar).into_any_element()), - single_example( - "Grayscale", - Avatar::new(example_avatar) - .grayscale(true) - .into_any_element(), - ), - single_example( - "Border", - Avatar::new(example_avatar) - .border_color(cx.theme().colors().border) - .into_any_element(), - ).description("Can be used to create visual space by setting the border color to match the background, which creates the appearance of a gap around the avatar."), - ]), - example_group_with_title( - "Indicator Styles", - vec![ - single_example( - "Muted", - Avatar::new(example_avatar) - .indicator(AvatarAudioStatusIndicator::new(AudioStatus::Muted)) - .into_any_element(), - ).description("Indicates the collaborator's mic is muted."), - single_example( - "Deafened", - Avatar::new(example_avatar) - .indicator(AvatarAudioStatusIndicator::new( - AudioStatus::Deafened, - )) - .into_any_element(), - ).description("Indicates that both the collaborator's mic and audio are muted."), - single_example( - "Availability: Free", - Avatar::new(example_avatar) - .indicator(AvatarAvailabilityIndicator::new( - CollaboratorAvailability::Free, - )) - .into_any_element(), - ).description("Indicates that the person is free, usually meaning they are not in a call."), - single_example( - "Availability: Busy", - Avatar::new(example_avatar) - .indicator(AvatarAvailabilityIndicator::new( - CollaboratorAvailability::Busy, - )) - .into_any_element(), - ).description("Indicates that the person is busy, usually meaning they are in a channel or direct call."), - ], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/banner.rs b/crates/ui/src/components/banner.rs deleted file mode 100644 index a09170f5b2..0000000000 --- a/crates/ui/src/components/banner.rs +++ /dev/null @@ -1,182 +0,0 @@ -use crate::prelude::*; -use gpui::{AnyElement, IntoElement, ParentElement, Styled}; - -/// Banners provide informative and brief messages without interrupting the user. -/// This component offers four severity levels that can be used depending on the message. -/// -/// # Usage Example -/// -/// ``` -/// use ui::prelude::*; -/// use ui::{Banner, Button, IconName, IconPosition, IconSize, Label, Severity}; -/// -/// Banner::new() -/// .severity(Severity::Success) -/// .children([Label::new("This is a success message")]) -/// .action_slot( -/// Button::new("learn-more", "Learn More") -/// .icon(IconName::ArrowUpRight) -/// .icon_size(IconSize::Small) -/// .icon_position(IconPosition::End) -/// ); -/// ``` -#[derive(IntoElement, RegisterComponent)] -pub struct Banner { - severity: Severity, - children: Vec, - action_slot: Option, -} - -impl Banner { - /// Creates a new `Banner` component with default styling. - pub fn new() -> Self { - Self { - severity: Severity::Info, - children: Vec::new(), - action_slot: None, - } - } - - /// Sets the severity of the banner. - pub fn severity(mut self, severity: Severity) -> Self { - self.severity = severity; - self - } - - /// A slot for actions, such as CTA or dismissal buttons. - pub fn action_slot(mut self, element: impl IntoElement) -> Self { - self.action_slot = Some(element.into_any_element()); - self - } -} - -impl ParentElement for Banner { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl RenderOnce for Banner { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let banner = h_flex() - .py_0p5() - .gap_1p5() - .flex_wrap() - .justify_between() - .rounded_sm() - .border_1(); - - let (icon, icon_color, bg_color, border_color) = match self.severity { - Severity::Info => ( - IconName::Info, - Color::Muted, - cx.theme().status().info_background.opacity(0.5), - cx.theme().colors().border.opacity(0.5), - ), - Severity::Success => ( - IconName::Check, - Color::Success, - cx.theme().status().success.opacity(0.1), - cx.theme().status().success.opacity(0.2), - ), - Severity::Warning => ( - IconName::Warning, - Color::Warning, - cx.theme().status().warning_background.opacity(0.5), - cx.theme().status().warning_border.opacity(0.4), - ), - Severity::Error => ( - IconName::XCircle, - Color::Error, - cx.theme().status().error.opacity(0.1), - cx.theme().status().error.opacity(0.2), - ), - }; - - let mut banner = banner.bg(bg_color).border_color(border_color); - - let icon_and_child = h_flex() - .items_start() - .min_w_0() - .gap_1p5() - .child( - h_flex() - .h(window.line_height()) - .flex_shrink_0() - .child(Icon::new(icon).size(IconSize::XSmall).color(icon_color)), - ) - .child(div().min_w_0().children(self.children)); - - if let Some(action_slot) = self.action_slot { - banner = banner - .pl_2() - .pr_1() - .child(icon_and_child) - .child(action_slot); - } else { - banner = banner.px_2().child(icon_and_child); - } - - banner - } -} - -impl Component for Banner { - fn scope() -> ComponentScope { - ComponentScope::DataDisplay - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - let severity_examples = vec![ - single_example( - "Default", - Banner::new() - .child(Label::new("This is a default banner with no customization")) - .into_any_element(), - ), - single_example( - "Info", - Banner::new() - .severity(Severity::Info) - .child(Label::new("This is an informational message")) - .action_slot( - Button::new("learn-more", "Learn More") - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::Small) - .icon_position(IconPosition::End), - ) - .into_any_element(), - ), - single_example( - "Success", - Banner::new() - .severity(Severity::Success) - .child(Label::new("Operation completed successfully")) - .action_slot(Button::new("dismiss", "Dismiss")) - .into_any_element(), - ), - single_example( - "Warning", - Banner::new() - .severity(Severity::Warning) - .child(Label::new("Your settings file uses deprecated settings")) - .action_slot(Button::new("update", "Update Settings")) - .into_any_element(), - ), - single_example( - "Error", - Banner::new() - .severity(Severity::Error) - .child(Label::new("Connection error: unable to connect to server")) - .action_slot(Button::new("reconnect", "Retry")) - .into_any_element(), - ), - ]; - - Some( - example_group(severity_examples) - .vertical() - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/button.rs b/crates/ui/src/components/button.rs deleted file mode 100644 index 23e7702f62..0000000000 --- a/crates/ui/src/components/button.rs +++ /dev/null @@ -1,12 +0,0 @@ -mod button; -mod button_icon; -mod button_like; -mod icon_button; -mod split_button; -mod toggle_button; - -pub use button::*; -pub use button_like::*; -pub use icon_button::*; -pub use split_button::*; -pub use toggle_button::*; diff --git a/crates/ui/src/components/button/button.rs b/crates/ui/src/components/button/button.rs deleted file mode 100644 index 83b50b6341..0000000000 --- a/crates/ui/src/components/button/button.rs +++ /dev/null @@ -1,612 +0,0 @@ -use crate::component_prelude::*; -use gpui::{AnyElement, AnyView, DefiniteLength}; -use ui_macros::RegisterComponent; - -use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, IconName, IconSize, Label}; -use crate::{ - Color, DynamicSpacing, ElevationIndex, IconPosition, KeyBinding, KeybindingPosition, TintColor, - prelude::*, -}; - -use super::button_icon::ButtonIcon; - -/// An element that creates a button with a label and an optional icon. -/// -/// Common buttons: -/// - Label, Icon + Label: [`Button`] (this component) -/// - Icon only: [`IconButton`] -/// - Custom: [`ButtonLike`] -/// -/// To create a more complex button than what the [`Button`] or [`IconButton`] components provide, use -/// [`ButtonLike`] directly. -/// -/// # Examples -/// -/// **A button with a label**, is typically used in scenarios such as a form, where the button's label -/// indicates what action will be performed when the button is clicked. -/// -/// ``` -/// use ui::prelude::*; -/// -/// Button::new("button_id", "Click me!") -/// .on_click(|event, window, cx| { -/// // Handle click event -/// }); -/// ``` -/// -/// **A toggleable button**, is typically used in scenarios such as a toolbar, -/// where the button's state indicates whether a feature is enabled or not, or -/// a trigger for a popover menu, where clicking the button toggles the visibility of the menu. -/// -/// ``` -/// use ui::prelude::*; -/// -/// Button::new("button_id", "Click me!") -/// .icon(IconName::Check) -/// .toggle_state(true) -/// .on_click(|event, window, cx| { -/// // Handle click event -/// }); -/// ``` -/// -/// To change the style of the button when it is selected use the [`selected_style`][Button::selected_style] method. -/// -/// ``` -/// use ui::prelude::*; -/// use ui::TintColor; -/// -/// Button::new("button_id", "Click me!") -/// .toggle_state(true) -/// .selected_style(ButtonStyle::Tinted(TintColor::Accent)) -/// .on_click(|event, window, cx| { -/// // Handle click event -/// }); -/// ``` -/// This will create a button with a blue tinted background when selected. -/// -/// **A full-width button**, is typically used in scenarios such as the bottom of a modal or form, where it occupies the entire width of its container. -/// The button's content, including text and icons, is centered by default. -/// -/// ``` -/// use ui::prelude::*; -/// -/// let button = Button::new("button_id", "Click me!") -/// .full_width() -/// .on_click(|event, window, cx| { -/// // Handle click event -/// }); -/// ``` -/// -#[derive(IntoElement, Documented, RegisterComponent)] -pub struct Button { - base: ButtonLike, - label: SharedString, - label_color: Option, - label_size: Option, - selected_label: Option, - selected_label_color: Option, - icon: Option, - icon_position: Option, - icon_size: Option, - icon_color: Option, - selected_icon: Option, - selected_icon_color: Option, - key_binding: Option, - key_binding_position: KeybindingPosition, - alpha: Option, - truncate: bool, -} - -impl Button { - /// Creates a new [`Button`] with a specified identifier and label. - /// - /// This is the primary constructor for a [`Button`] component. It initializes - /// the button with the provided identifier and label text, setting all other - /// properties to their default values, which can be customized using the - /// builder pattern methods provided by this struct. - pub fn new(id: impl Into, label: impl Into) -> Self { - Self { - base: ButtonLike::new(id), - label: label.into(), - label_color: None, - label_size: None, - selected_label: None, - selected_label_color: None, - icon: None, - icon_position: None, - icon_size: None, - icon_color: None, - selected_icon: None, - selected_icon_color: None, - key_binding: None, - key_binding_position: KeybindingPosition::default(), - alpha: None, - truncate: false, - } - } - - /// Sets the color of the button's label. - pub fn color(mut self, label_color: impl Into>) -> Self { - self.label_color = label_color.into(); - self - } - - /// Defines the size of the button's label. - pub fn label_size(mut self, label_size: impl Into>) -> Self { - self.label_size = label_size.into(); - self - } - - /// Sets the label used when the button is in a selected state. - pub fn selected_label>(mut self, label: impl Into>) -> Self { - self.selected_label = label.into().map(Into::into); - self - } - - /// Sets the label color used when the button is in a selected state. - pub fn selected_label_color(mut self, color: impl Into>) -> Self { - self.selected_label_color = color.into(); - self - } - - /// Assigns an icon to the button. - pub fn icon(mut self, icon: impl Into>) -> Self { - self.icon = icon.into(); - self - } - - /// Sets the position of the icon relative to the label. - pub fn icon_position(mut self, icon_position: impl Into>) -> Self { - self.icon_position = icon_position.into(); - self - } - - /// Specifies the size of the button's icon. - pub fn icon_size(mut self, icon_size: impl Into>) -> Self { - self.icon_size = icon_size.into(); - self - } - - /// Sets the color of the button's icon. - pub fn icon_color(mut self, icon_color: impl Into>) -> Self { - self.icon_color = icon_color.into(); - self - } - - /// Chooses an icon to display when the button is in a selected state. - pub fn selected_icon(mut self, icon: impl Into>) -> Self { - self.selected_icon = icon.into(); - self - } - - /// Sets the icon color used when the button is in a selected state. - pub fn selected_icon_color(mut self, color: impl Into>) -> Self { - self.selected_icon_color = color.into(); - self - } - - /// Display the keybinding that triggers the button action. - pub fn key_binding(mut self, key_binding: impl Into>) -> Self { - self.key_binding = key_binding.into(); - self - } - - /// Sets the position of the keybinding relative to the button label. - /// - /// This method allows you to specify where the keybinding should be displayed - /// in relation to the button's label. - pub fn key_binding_position(mut self, position: KeybindingPosition) -> Self { - self.key_binding_position = position; - self - } - - /// Sets the alpha property of the color of label. - pub fn alpha(mut self, alpha: f32) -> Self { - self.alpha = Some(alpha); - self - } - - /// Truncates overflowing labels with an ellipsis (`…`) if needed. - /// - /// Buttons with static labels should _never_ be truncated, ensure - /// this is only used when the label is dynamic and may overflow. - pub fn truncate(mut self, truncate: bool) -> Self { - self.truncate = truncate; - self - } -} - -impl Toggleable for Button { - /// Sets the selected state of the button. - /// - /// This method allows the selection state of the button to be specified. - /// It modifies the button's appearance to reflect its selected state. - /// - /// # Examples - /// - /// ``` - /// use ui::prelude::*; - /// - /// Button::new("button_id", "Click me!") - /// .toggle_state(true) - /// .on_click(|event, window, cx| { - /// // Handle click event - /// }); - /// ``` - /// - /// Use [`selected_style`](Button::selected_style) to change the style of the button when it is selected. - fn toggle_state(mut self, selected: bool) -> Self { - self.base = self.base.toggle_state(selected); - self - } -} - -impl SelectableButton for Button { - /// Sets the style for the button when selected. - /// - /// # Examples - /// - /// ``` - /// use ui::prelude::*; - /// use ui::TintColor; - /// - /// Button::new("button_id", "Click me!") - /// .toggle_state(true) - /// .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - /// .on_click(|event, window, cx| { - /// // Handle click event - /// }); - /// ``` - /// This results in a button with a blue tinted background when selected. - fn selected_style(mut self, style: ButtonStyle) -> Self { - self.base = self.base.selected_style(style); - self - } -} - -impl Disableable for Button { - /// Disables the button. - /// - /// This method allows the button to be disabled. When a button is disabled, - /// it doesn't react to user interactions and its appearance is updated to reflect this. - /// - /// # Examples - /// - /// ``` - /// use ui::prelude::*; - /// - /// Button::new("button_id", "Click me!") - /// .disabled(true) - /// .on_click(|event, window, cx| { - /// // Handle click event - /// }); - /// ``` - /// - /// This results in a button that is disabled and does not respond to click events. - fn disabled(mut self, disabled: bool) -> Self { - self.base = self.base.disabled(disabled); - self.key_binding = self - .key_binding - .take() - .map(|binding| binding.disabled(disabled)); - self - } -} - -impl Clickable for Button { - /// Sets the click event handler for the button. - fn on_click( - mut self, - handler: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.base = self.base.on_click(handler); - self - } - - fn cursor_style(mut self, cursor_style: gpui::CursorStyle) -> Self { - self.base = self.base.cursor_style(cursor_style); - self - } -} - -impl FixedWidth for Button { - /// Sets a fixed width for the button. - /// - /// This function allows a button to have a fixed width instead of automatically growing or shrinking. - /// Sets a fixed width for the button. - /// - /// # Examples - /// - /// ``` - /// use ui::prelude::*; - /// - /// Button::new("button_id", "Click me!") - /// .width(px(100.)) - /// .on_click(|event, window, cx| { - /// // Handle click event - /// }); - /// ``` - /// - /// This sets the button's width to be exactly 100 pixels. - fn width(mut self, width: impl Into) -> Self { - self.base = self.base.width(width); - self - } - - /// Sets the button to occupy the full width of its container. - /// - /// # Examples - /// - /// ``` - /// use ui::prelude::*; - /// - /// Button::new("button_id", "Click me!") - /// .full_width() - /// .on_click(|event, window, cx| { - /// // Handle click event - /// }); - /// ``` - /// - /// This stretches the button to the full width of its container. - fn full_width(mut self) -> Self { - self.base = self.base.full_width(); - self - } -} - -impl ButtonCommon for Button { - /// Sets the button's id. - fn id(&self) -> &ElementId { - self.base.id() - } - - /// Sets the visual style of the button using a [`ButtonStyle`]. - fn style(mut self, style: ButtonStyle) -> Self { - self.base = self.base.style(style); - self - } - - /// Sets the button's size using a [`ButtonSize`]. - fn size(mut self, size: ButtonSize) -> Self { - self.base = self.base.size(size); - self - } - - /// Sets a tooltip for the button. - /// - /// This method allows a tooltip to be set for the button. The tooltip is a function that - /// takes a mutable references to [`Window`] and [`App`], and returns an [`AnyView`]. The - /// tooltip is displayed when the user hovers over the button. - /// - /// # Examples - /// - /// ``` - /// use ui::prelude::*; - /// use ui::Tooltip; - /// - /// Button::new("button_id", "Click me!") - /// .tooltip(Tooltip::text("This is a tooltip")) - /// .on_click(|event, window, cx| { - /// // Handle click event - /// }); - /// ``` - /// - /// This will create a button with a tooltip that displays "This is a tooltip" when hovered over. - fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self { - self.base = self.base.tooltip(tooltip); - self - } - - fn tab_index(mut self, tab_index: impl Into) -> Self { - self.base = self.base.tab_index(tab_index); - self - } - - fn layer(mut self, elevation: ElevationIndex) -> Self { - self.base = self.base.layer(elevation); - self - } - - fn track_focus(mut self, focus_handle: &gpui::FocusHandle) -> Self { - self.base = self.base.track_focus(focus_handle); - self - } -} - -impl RenderOnce for Button { - #[allow(refining_impl_trait)] - fn render(self, _window: &mut Window, cx: &mut App) -> ButtonLike { - let is_disabled = self.base.disabled; - let is_selected = self.base.selected; - - let label = self - .selected_label - .filter(|_| is_selected) - .unwrap_or(self.label); - - let label_color = if is_disabled { - Color::Disabled - } else if is_selected { - self.selected_label_color.unwrap_or(Color::Selected) - } else { - self.label_color.unwrap_or_default() - }; - - self.base.child( - h_flex() - .gap(DynamicSpacing::Base04.rems(cx)) - .when(self.icon_position == Some(IconPosition::Start), |this| { - this.children(self.icon.map(|icon| { - ButtonIcon::new(icon) - .disabled(is_disabled) - .toggle_state(is_selected) - .selected_icon(self.selected_icon) - .selected_icon_color(self.selected_icon_color) - .size(self.icon_size) - .color(self.icon_color) - })) - }) - .child( - h_flex() - .when( - self.key_binding_position == KeybindingPosition::Start, - |this| this.flex_row_reverse(), - ) - .gap(DynamicSpacing::Base06.rems(cx)) - .justify_between() - .child( - Label::new(label) - .color(label_color) - .size(self.label_size.unwrap_or_default()) - .when_some(self.alpha, |this, alpha| this.alpha(alpha)) - .when(self.truncate, |this| this.truncate()), - ) - .children(self.key_binding), - ) - .when(self.icon_position != Some(IconPosition::Start), |this| { - this.children(self.icon.map(|icon| { - ButtonIcon::new(icon) - .disabled(is_disabled) - .toggle_state(is_selected) - .selected_icon(self.selected_icon) - .selected_icon_color(self.selected_icon_color) - .size(self.icon_size) - .color(self.icon_color) - })) - }), - ) - } -} - -impl Component for Button { - fn scope() -> ComponentScope { - ComponentScope::Input - } - - fn sort_name() -> &'static str { - "ButtonA" - } - - fn description() -> Option<&'static str> { - Some("A button triggers an event or action.") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Button Styles", - vec![ - single_example( - "Default", - Button::new("default", "Default").into_any_element(), - ), - single_example( - "Filled", - Button::new("filled", "Filled") - .style(ButtonStyle::Filled) - .into_any_element(), - ), - single_example( - "Subtle", - Button::new("outline", "Subtle") - .style(ButtonStyle::Subtle) - .into_any_element(), - ), - single_example( - "Tinted", - Button::new("tinted_accent_style", "Accent") - .style(ButtonStyle::Tinted(TintColor::Accent)) - .into_any_element(), - ), - single_example( - "Transparent", - Button::new("transparent", "Transparent") - .style(ButtonStyle::Transparent) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Tint Styles", - vec![ - single_example( - "Accent", - Button::new("tinted_accent", "Accent") - .style(ButtonStyle::Tinted(TintColor::Accent)) - .into_any_element(), - ), - single_example( - "Error", - Button::new("tinted_negative", "Error") - .style(ButtonStyle::Tinted(TintColor::Error)) - .into_any_element(), - ), - single_example( - "Warning", - Button::new("tinted_warning", "Warning") - .style(ButtonStyle::Tinted(TintColor::Warning)) - .into_any_element(), - ), - single_example( - "Success", - Button::new("tinted_positive", "Success") - .style(ButtonStyle::Tinted(TintColor::Success)) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Special States", - vec![ - single_example( - "Default", - Button::new("default_state", "Default").into_any_element(), - ), - single_example( - "Disabled", - Button::new("disabled", "Disabled") - .disabled(true) - .into_any_element(), - ), - single_example( - "Selected", - Button::new("selected", "Selected") - .toggle_state(true) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Buttons with Icons", - vec![ - single_example( - "Icon Start", - Button::new("icon_start", "Icon Start") - .icon(IconName::Check) - .icon_position(IconPosition::Start) - .into_any_element(), - ), - single_example( - "Icon End", - Button::new("icon_end", "Icon End") - .icon(IconName::Check) - .icon_position(IconPosition::End) - .into_any_element(), - ), - single_example( - "Icon Color", - Button::new("icon_color", "Icon Color") - .icon(IconName::Check) - .icon_color(Color::Accent) - .into_any_element(), - ), - ], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/button/button_icon.rs b/crates/ui/src/components/button/button_icon.rs deleted file mode 100644 index 510c418714..0000000000 --- a/crates/ui/src/components/button/button_icon.rs +++ /dev/null @@ -1,199 +0,0 @@ -use crate::{Icon, IconName, IconSize, IconWithIndicator, Indicator, prelude::*}; -use gpui::Hsla; - -/// An icon that appears within a button. -/// -/// Can be used as either an icon alongside a label, like in [`Button`](crate::Button), -/// or as a standalone icon, like in [`IconButton`](crate::IconButton). -#[derive(IntoElement, RegisterComponent)] -pub(super) struct ButtonIcon { - icon: IconName, - size: IconSize, - color: Color, - disabled: bool, - selected: bool, - selected_icon: Option, - selected_icon_color: Option, - selected_style: Option, - indicator: Option, - indicator_border_color: Option, -} - -impl ButtonIcon { - pub fn new(icon: IconName) -> Self { - Self { - icon, - size: IconSize::default(), - color: Color::default(), - disabled: false, - selected: false, - selected_icon: None, - selected_icon_color: None, - selected_style: None, - indicator: None, - indicator_border_color: None, - } - } - - pub fn size(mut self, size: impl Into>) -> Self { - if let Some(size) = size.into() { - self.size = size; - } - self - } - - pub fn color(mut self, color: impl Into>) -> Self { - if let Some(color) = color.into() { - self.color = color; - } - self - } - - pub fn selected_icon(mut self, icon: impl Into>) -> Self { - self.selected_icon = icon.into(); - self - } - - pub fn selected_icon_color(mut self, color: impl Into>) -> Self { - self.selected_icon_color = color.into(); - self - } - - pub fn indicator(mut self, indicator: Indicator) -> Self { - self.indicator = Some(indicator); - self - } - - pub fn indicator_border_color(mut self, color: Option) -> Self { - self.indicator_border_color = color; - self - } -} - -impl Disableable for ButtonIcon { - fn disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self - } -} - -impl Toggleable for ButtonIcon { - fn toggle_state(mut self, selected: bool) -> Self { - self.selected = selected; - self - } -} - -impl SelectableButton for ButtonIcon { - fn selected_style(mut self, style: ButtonStyle) -> Self { - self.selected_style = Some(style); - self - } -} - -impl RenderOnce for ButtonIcon { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let icon = self - .selected_icon - .filter(|_| self.selected) - .unwrap_or(self.icon); - - let icon_color = if self.disabled { - Color::Disabled - } else if self.selected_style.is_some() && self.selected { - self.selected_style.unwrap().into() - } else if self.selected { - self.selected_icon_color.unwrap_or(Color::Selected) - } else { - self.color - }; - - let icon = Icon::new(icon).size(self.size).color(icon_color); - - match self.indicator { - Some(indicator) => IconWithIndicator::new(icon, Some(indicator)) - .indicator_border_color(self.indicator_border_color) - .into_any_element(), - None => icon.into_any_element(), - } - } -} - -impl Component for ButtonIcon { - fn scope() -> ComponentScope { - ComponentScope::Input - } - - fn name() -> &'static str { - "ButtonIcon" - } - - fn description() -> Option<&'static str> { - Some("An icon component specifically designed for use within buttons.") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Basic Usage", - vec![ - single_example( - "Default", - ButtonIcon::new(IconName::Star).into_any_element(), - ), - single_example( - "Custom Size", - ButtonIcon::new(IconName::Star) - .size(IconSize::Medium) - .into_any_element(), - ), - single_example( - "Custom Color", - ButtonIcon::new(IconName::Star) - .color(Color::Accent) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "States", - vec![ - single_example( - "Selected", - ButtonIcon::new(IconName::Star) - .toggle_state(true) - .into_any_element(), - ), - single_example( - "Disabled", - ButtonIcon::new(IconName::Star) - .disabled(true) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "With Indicator", - vec![ - single_example( - "Default Indicator", - ButtonIcon::new(IconName::Star) - .indicator(Indicator::dot()) - .into_any_element(), - ), - single_example( - "Custom Indicator", - ButtonIcon::new(IconName::Star) - .indicator(Indicator::dot().color(Color::Error)) - .into_any_element(), - ), - ], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/button/button_like.rs b/crates/ui/src/components/button/button_like.rs deleted file mode 100644 index 4ce7aeed0d..0000000000 --- a/crates/ui/src/components/button/button_like.rs +++ /dev/null @@ -1,849 +0,0 @@ -use documented::Documented; -use gpui::{ - AnyElement, AnyView, ClickEvent, CursorStyle, DefiniteLength, FocusHandle, Hsla, MouseButton, - MouseClickEvent, MouseDownEvent, MouseUpEvent, Rems, StyleRefinement, relative, - transparent_black, -}; -use smallvec::SmallVec; - -use crate::{DynamicSpacing, ElevationIndex, prelude::*}; - -/// A trait for buttons that can be Selected. Enables setting the [`ButtonStyle`] of a button when it is selected. -pub trait SelectableButton: Toggleable { - fn selected_style(self, style: ButtonStyle) -> Self; -} - -/// A common set of traits all buttons must implement. -pub trait ButtonCommon: Clickable + Disableable { - /// A unique element ID to identify the button. - fn id(&self) -> &ElementId; - - /// The visual style of the button. - /// - /// Most commonly will be [`ButtonStyle::Subtle`], or [`ButtonStyle::Filled`] - /// for an emphasized button. - fn style(self, style: ButtonStyle) -> Self; - - /// The size of the button. - /// - /// Most buttons will use the default size. - /// - /// [`ButtonSize`] can also be used to help build non-button elements - /// that are consistently sized with buttons. - fn size(self, size: ButtonSize) -> Self; - - /// The tooltip that shows when a user hovers over the button. - /// - /// Nearly all interactable elements should have a tooltip. Some example - /// exceptions might a scroll bar, or a slider. - fn tooltip(self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self; - - fn tab_index(self, tab_index: impl Into) -> Self; - - fn layer(self, elevation: ElevationIndex) -> Self; - - fn track_focus(self, focus_handle: &FocusHandle) -> Self; -} - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] -pub enum IconPosition { - #[default] - Start, - End, -} - -#[derive(Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -pub enum KeybindingPosition { - Start, - #[default] - End, -} - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] -pub enum TintColor { - #[default] - Accent, - Error, - Warning, - Success, -} - -impl TintColor { - fn button_like_style(self, cx: &mut App) -> ButtonLikeStyles { - match self { - TintColor::Accent => ButtonLikeStyles { - background: cx.theme().status().info_background, - border_color: cx.theme().status().info_border, - label_color: cx.theme().colors().text, - icon_color: cx.theme().colors().text, - }, - TintColor::Error => ButtonLikeStyles { - background: cx.theme().status().error_background, - border_color: cx.theme().status().error_border, - label_color: cx.theme().colors().text, - icon_color: cx.theme().colors().text, - }, - TintColor::Warning => ButtonLikeStyles { - background: cx.theme().status().warning_background, - border_color: cx.theme().status().warning_border, - label_color: cx.theme().colors().text, - icon_color: cx.theme().colors().text, - }, - TintColor::Success => ButtonLikeStyles { - background: cx.theme().status().success_background, - border_color: cx.theme().status().success_border, - label_color: cx.theme().colors().text, - icon_color: cx.theme().colors().text, - }, - } - } -} - -impl From for Color { - fn from(tint: TintColor) -> Self { - match tint { - TintColor::Accent => Color::Accent, - TintColor::Error => Color::Error, - TintColor::Warning => Color::Warning, - TintColor::Success => Color::Success, - } - } -} - -// Used to go from ButtonStyle -> Color through tint colors. -impl From for Color { - fn from(style: ButtonStyle) -> Self { - match style { - ButtonStyle::Tinted(tint) => tint.into(), - _ => Color::Default, - } - } -} - -/// The visual appearance of a button. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] -pub enum ButtonStyle { - /// A filled button with a solid background color. Provides emphasis versus - /// the more common subtle button. - Filled, - - /// Used to emphasize a button in some way, like a selected state, or a semantic - /// coloring like an error or success button. - Tinted(TintColor), - - /// Usually used as a secondary action that should have more emphasis than - /// a fully transparent button. - Outlined, - - /// A more de-emphasized version of the outlined button. - OutlinedGhost, - - /// The default button style, used for most buttons. Has a transparent background, - /// but has a background color to indicate states like hover and active. - #[default] - Subtle, - - /// Used for buttons that only change foreground color on hover and active states. - /// - /// TODO: Better docs for this. - Transparent, -} - -/// Rounding for a button that may have straight edges. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -pub(crate) struct ButtonLikeRounding { - /// Top-left corner rounding - pub top_left: bool, - /// Top-right corner rounding - pub top_right: bool, - /// Bottom-right corner rounding - pub bottom_right: bool, - /// Bottom-left corner rounding - pub bottom_left: bool, -} - -impl ButtonLikeRounding { - pub const ALL: Self = Self { - top_left: true, - top_right: true, - bottom_right: true, - bottom_left: true, - }; - pub const LEFT: Self = Self { - top_left: true, - top_right: false, - bottom_right: false, - bottom_left: true, - }; - pub const RIGHT: Self = Self { - top_left: false, - top_right: true, - bottom_right: true, - bottom_left: false, - }; -} - -#[derive(Debug, Clone)] -pub(crate) struct ButtonLikeStyles { - pub background: Hsla, - #[allow(unused)] - pub border_color: Hsla, - #[allow(unused)] - pub label_color: Hsla, - #[allow(unused)] - pub icon_color: Hsla, -} - -fn element_bg_from_elevation(elevation: Option, cx: &mut App) -> Hsla { - match elevation { - Some(ElevationIndex::Background) => cx.theme().colors().element_background, - Some(ElevationIndex::ElevatedSurface) => cx.theme().colors().elevated_surface_background, - Some(ElevationIndex::Surface) => cx.theme().colors().surface_background, - Some(ElevationIndex::ModalSurface) => cx.theme().colors().background, - _ => cx.theme().colors().element_background, - } -} - -impl ButtonStyle { - pub(crate) fn enabled( - self, - elevation: Option, - - cx: &mut App, - ) -> ButtonLikeStyles { - match self { - ButtonStyle::Filled => ButtonLikeStyles { - background: element_bg_from_elevation(elevation, cx), - border_color: transparent_black(), - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::Tinted(tint) => tint.button_like_style(cx), - ButtonStyle::Outlined => ButtonLikeStyles { - background: element_bg_from_elevation(elevation, cx), - border_color: cx.theme().colors().border_variant, - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::OutlinedGhost => ButtonLikeStyles { - background: transparent_black(), - border_color: cx.theme().colors().border_variant, - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::Subtle => ButtonLikeStyles { - background: cx.theme().colors().ghost_element_background, - border_color: transparent_black(), - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::Transparent => ButtonLikeStyles { - background: transparent_black(), - border_color: transparent_black(), - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - } - } - - pub(crate) fn hovered( - self, - elevation: Option, - - cx: &mut App, - ) -> ButtonLikeStyles { - match self { - ButtonStyle::Filled => { - let mut filled_background = element_bg_from_elevation(elevation, cx); - filled_background.fade_out(0.5); - - ButtonLikeStyles { - background: filled_background, - border_color: transparent_black(), - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - } - } - ButtonStyle::Tinted(tint) => { - let mut styles = tint.button_like_style(cx); - let theme = cx.theme(); - styles.background = theme.darken(styles.background, 0.05, 0.2); - styles - } - ButtonStyle::Outlined => ButtonLikeStyles { - background: cx.theme().colors().ghost_element_hover, - border_color: cx.theme().colors().border, - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::OutlinedGhost => ButtonLikeStyles { - background: cx.theme().colors().ghost_element_hover, - border_color: cx.theme().colors().border, - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::Subtle => ButtonLikeStyles { - background: cx.theme().colors().ghost_element_hover, - border_color: transparent_black(), - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::Transparent => ButtonLikeStyles { - background: transparent_black(), - border_color: transparent_black(), - // TODO: These are not great - label_color: Color::Muted.color(cx), - // TODO: These are not great - icon_color: Color::Muted.color(cx), - }, - } - } - - pub(crate) fn active(self, cx: &mut App) -> ButtonLikeStyles { - match self { - ButtonStyle::Filled => ButtonLikeStyles { - background: cx.theme().colors().element_active, - border_color: transparent_black(), - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::Tinted(tint) => tint.button_like_style(cx), - ButtonStyle::Subtle => ButtonLikeStyles { - background: cx.theme().colors().ghost_element_active, - border_color: transparent_black(), - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::Outlined => ButtonLikeStyles { - background: cx.theme().colors().element_active, - border_color: cx.theme().colors().border_variant, - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::OutlinedGhost => ButtonLikeStyles { - background: transparent_black(), - border_color: cx.theme().colors().border_variant, - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::Transparent => ButtonLikeStyles { - background: transparent_black(), - border_color: transparent_black(), - // TODO: These are not great - label_color: Color::Muted.color(cx), - // TODO: These are not great - icon_color: Color::Muted.color(cx), - }, - } - } - - #[allow(unused)] - pub(crate) fn focused(self, window: &mut Window, cx: &mut App) -> ButtonLikeStyles { - match self { - ButtonStyle::Filled => ButtonLikeStyles { - background: cx.theme().colors().element_background, - border_color: cx.theme().colors().border_focused, - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::Tinted(tint) => tint.button_like_style(cx), - ButtonStyle::Subtle => ButtonLikeStyles { - background: cx.theme().colors().ghost_element_background, - border_color: cx.theme().colors().border_focused, - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::Outlined => ButtonLikeStyles { - background: cx.theme().colors().ghost_element_background, - border_color: cx.theme().colors().border, - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::OutlinedGhost => ButtonLikeStyles { - background: transparent_black(), - border_color: cx.theme().colors().border, - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::Transparent => ButtonLikeStyles { - background: transparent_black(), - border_color: cx.theme().colors().border_focused, - label_color: Color::Accent.color(cx), - icon_color: Color::Accent.color(cx), - }, - } - } - - #[allow(unused)] - pub(crate) fn disabled( - self, - elevation: Option, - window: &mut Window, - cx: &mut App, - ) -> ButtonLikeStyles { - match self { - ButtonStyle::Filled => ButtonLikeStyles { - background: cx.theme().colors().element_disabled, - border_color: cx.theme().colors().border_disabled, - label_color: Color::Disabled.color(cx), - icon_color: Color::Disabled.color(cx), - }, - ButtonStyle::Tinted(tint) => tint.button_like_style(cx), - ButtonStyle::Subtle => ButtonLikeStyles { - background: cx.theme().colors().ghost_element_disabled, - border_color: cx.theme().colors().border_disabled, - label_color: Color::Disabled.color(cx), - icon_color: Color::Disabled.color(cx), - }, - ButtonStyle::Outlined => ButtonLikeStyles { - background: cx.theme().colors().element_disabled, - border_color: cx.theme().colors().border_disabled, - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::OutlinedGhost => ButtonLikeStyles { - background: transparent_black(), - border_color: cx.theme().colors().border_disabled, - label_color: Color::Default.color(cx), - icon_color: Color::Default.color(cx), - }, - ButtonStyle::Transparent => ButtonLikeStyles { - background: transparent_black(), - border_color: transparent_black(), - label_color: Color::Disabled.color(cx), - icon_color: Color::Disabled.color(cx), - }, - } - } -} - -/// The height of a button. -/// -/// Can also be used to size non-button elements to align with [`Button`]s. -#[derive(Default, PartialEq, Clone, Copy)] -pub enum ButtonSize { - Large, - Medium, - #[default] - Default, - Compact, - None, -} - -impl ButtonSize { - pub fn rems(self) -> Rems { - match self { - ButtonSize::Large => rems_from_px(32.), - ButtonSize::Medium => rems_from_px(28.), - ButtonSize::Default => rems_from_px(22.), - ButtonSize::Compact => rems_from_px(18.), - ButtonSize::None => rems_from_px(16.), - } - } -} - -/// A button-like element that can be used to create a custom button when -/// prebuilt buttons are not sufficient. Use this sparingly, as it is -/// unconstrained and may make the UI feel less consistent. -/// -/// This is also used to build the prebuilt buttons. -#[derive(IntoElement, Documented, RegisterComponent)] -pub struct ButtonLike { - pub(super) base: Div, - id: ElementId, - pub(super) style: ButtonStyle, - pub(super) disabled: bool, - pub(super) selected: bool, - pub(super) selected_style: Option, - pub(super) width: Option, - pub(super) height: Option, - pub(super) layer: Option, - tab_index: Option, - size: ButtonSize, - rounding: Option, - tooltip: Option AnyView>>, - hoverable_tooltip: Option AnyView>>, - cursor_style: CursorStyle, - on_click: Option>, - on_right_click: Option>, - children: SmallVec<[AnyElement; 2]>, - focus_handle: Option, -} - -impl ButtonLike { - pub fn new(id: impl Into) -> Self { - Self { - base: div(), - id: id.into(), - style: ButtonStyle::default(), - disabled: false, - selected: false, - selected_style: None, - width: None, - height: None, - size: ButtonSize::Default, - rounding: Some(ButtonLikeRounding::ALL), - tooltip: None, - hoverable_tooltip: None, - children: SmallVec::new(), - cursor_style: CursorStyle::PointingHand, - on_click: None, - on_right_click: None, - layer: None, - tab_index: None, - focus_handle: None, - } - } - - pub fn new_rounded_left(id: impl Into) -> Self { - Self::new(id).rounding(ButtonLikeRounding::LEFT) - } - - pub fn new_rounded_right(id: impl Into) -> Self { - Self::new(id).rounding(ButtonLikeRounding::RIGHT) - } - - pub fn new_rounded_all(id: impl Into) -> Self { - Self::new(id).rounding(ButtonLikeRounding::ALL) - } - - pub fn opacity(mut self, opacity: f32) -> Self { - self.base = self.base.opacity(opacity); - self - } - - pub fn height(mut self, height: DefiniteLength) -> Self { - self.height = Some(height); - self - } - - pub(crate) fn rounding(mut self, rounding: impl Into>) -> Self { - self.rounding = rounding.into(); - self - } - - pub fn on_right_click( - mut self, - handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_right_click = Some(Box::new(handler)); - self - } - - pub fn hoverable_tooltip( - mut self, - tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static, - ) -> Self { - self.hoverable_tooltip = Some(Box::new(tooltip)); - self - } -} - -impl Disableable for ButtonLike { - fn disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self - } -} - -impl Toggleable for ButtonLike { - fn toggle_state(mut self, selected: bool) -> Self { - self.selected = selected; - self - } -} - -impl SelectableButton for ButtonLike { - fn selected_style(mut self, style: ButtonStyle) -> Self { - self.selected_style = Some(style); - self - } -} - -impl Clickable for ButtonLike { - fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self { - self.on_click = Some(Box::new(handler)); - self - } - - fn cursor_style(mut self, cursor_style: CursorStyle) -> Self { - self.cursor_style = cursor_style; - self - } -} - -impl FixedWidth for ButtonLike { - fn width(mut self, width: impl Into) -> Self { - self.width = Some(width.into()); - self - } - - fn full_width(mut self) -> Self { - self.width = Some(relative(1.)); - self - } -} - -impl ButtonCommon for ButtonLike { - fn id(&self) -> &ElementId { - &self.id - } - - fn style(mut self, style: ButtonStyle) -> Self { - self.style = style; - self - } - - fn size(mut self, size: ButtonSize) -> Self { - self.size = size; - self - } - - fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self { - self.tooltip = Some(Box::new(tooltip)); - self - } - - fn tab_index(mut self, tab_index: impl Into) -> Self { - self.tab_index = Some(tab_index.into()); - self - } - - fn layer(mut self, elevation: ElevationIndex) -> Self { - self.layer = Some(elevation); - self - } - - fn track_focus(mut self, focus_handle: &gpui::FocusHandle) -> Self { - self.focus_handle = Some(focus_handle.clone()); - self - } -} - -impl VisibleOnHover for ButtonLike { - fn visible_on_hover(mut self, group_name: impl Into) -> Self { - self.base = self.base.visible_on_hover(group_name); - self - } -} - -impl ParentElement for ButtonLike { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl RenderOnce for ButtonLike { - fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - let style = self - .selected_style - .filter(|_| self.selected) - .unwrap_or(self.style); - - let is_outlined = matches!( - self.style, - ButtonStyle::Outlined | ButtonStyle::OutlinedGhost - ); - - self.base - .h_flex() - .id(self.id.clone()) - .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)) - .when_some(self.focus_handle, |this, focus_handle| { - this.track_focus(&focus_handle) - }) - .font_ui(cx) - .group("") - .flex_none() - .h(self.height.unwrap_or(self.size.rems().into())) - .when_some(self.width, |this, width| { - this.w(width).justify_center().text_center() - }) - .when(is_outlined, |this| this.border_1()) - .when_some(self.rounding, |this, rounding| { - this.when(rounding.top_left, |this| this.rounded_tl_sm()) - .when(rounding.top_right, |this| this.rounded_tr_sm()) - .when(rounding.bottom_right, |this| this.rounded_br_sm()) - .when(rounding.bottom_left, |this| this.rounded_bl_sm()) - }) - .gap(DynamicSpacing::Base04.rems(cx)) - .map(|this| match self.size { - ButtonSize::Large | ButtonSize::Medium => this.px(DynamicSpacing::Base08.rems(cx)), - ButtonSize::Default | ButtonSize::Compact => { - this.px(DynamicSpacing::Base04.rems(cx)) - } - ButtonSize::None => this.px_px(), - }) - .border_color(style.enabled(self.layer, cx).border_color) - .bg(style.enabled(self.layer, cx).background) - .when(self.disabled, |this| { - if self.cursor_style == CursorStyle::PointingHand { - this.cursor_not_allowed() - } else { - this.cursor(self.cursor_style) - } - }) - .when(!self.disabled, |this| { - let hovered_style = style.hovered(self.layer, cx); - let focus_color = - |refinement: StyleRefinement| refinement.bg(hovered_style.background); - - this.cursor(self.cursor_style) - .hover(focus_color) - .map(|this| { - if is_outlined { - this.focus_visible(|s| { - s.border_color(cx.theme().colors().border_focused) - }) - } else { - this.focus_visible(focus_color) - } - }) - .active(|active| active.bg(style.active(cx).background)) - }) - .when_some( - self.on_right_click.filter(|_| !self.disabled), - |this, on_right_click| { - this.on_mouse_down(MouseButton::Right, |_event, window, cx| { - window.prevent_default(); - cx.stop_propagation(); - }) - .on_mouse_up( - MouseButton::Right, - move |event, window, cx| { - cx.stop_propagation(); - let click_event = ClickEvent::Mouse(MouseClickEvent { - down: MouseDownEvent { - button: MouseButton::Right, - position: event.position, - modifiers: event.modifiers, - click_count: 1, - first_mouse: false, - }, - up: MouseUpEvent { - button: MouseButton::Right, - position: event.position, - modifiers: event.modifiers, - click_count: 1, - }, - }); - (on_right_click)(&click_event, window, cx) - }, - ) - }, - ) - .when_some( - self.on_click.filter(|_| !self.disabled), - |this, on_click| { - this.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default()) - .on_click(move |event, window, cx| { - cx.stop_propagation(); - (on_click)(event, window, cx) - }) - }, - ) - .when_some(self.tooltip, |this, tooltip| { - this.tooltip(move |window, cx| tooltip(window, cx)) - }) - .when_some(self.hoverable_tooltip, |this, tooltip| { - this.hoverable_tooltip(move |window, cx| tooltip(window, cx)) - }) - .children(self.children) - } -} - -impl Component for ButtonLike { - fn scope() -> ComponentScope { - ComponentScope::Input - } - - fn sort_name() -> &'static str { - // ButtonLike should be at the bottom of the button list - "ButtonZ" - } - - fn description() -> Option<&'static str> { - Some(ButtonLike::DOCS) - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group(vec![ - single_example( - "Default", - ButtonLike::new("default") - .child(Label::new("Default")) - .into_any_element(), - ), - single_example( - "Filled", - ButtonLike::new("filled") - .style(ButtonStyle::Filled) - .child(Label::new("Filled")) - .into_any_element(), - ), - single_example( - "Subtle", - ButtonLike::new("outline") - .style(ButtonStyle::Subtle) - .child(Label::new("Subtle")) - .into_any_element(), - ), - single_example( - "Tinted", - ButtonLike::new("tinted_accent_style") - .style(ButtonStyle::Tinted(TintColor::Accent)) - .child(Label::new("Accent")) - .into_any_element(), - ), - single_example( - "Transparent", - ButtonLike::new("transparent") - .style(ButtonStyle::Transparent) - .child(Label::new("Transparent")) - .into_any_element(), - ), - ]), - example_group_with_title( - "Button Group Constructors", - vec![ - single_example( - "Left Rounded", - ButtonLike::new_rounded_left("left_rounded") - .child(Label::new("Left Rounded")) - .style(ButtonStyle::Filled) - .into_any_element(), - ), - single_example( - "Right Rounded", - ButtonLike::new_rounded_right("right_rounded") - .child(Label::new("Right Rounded")) - .style(ButtonStyle::Filled) - .into_any_element(), - ), - single_example( - "Button Group", - h_flex() - .gap_px() - .child( - ButtonLike::new_rounded_left("bg_left") - .child(Label::new("Left")) - .style(ButtonStyle::Filled), - ) - .child( - ButtonLike::new_rounded_right("bg_right") - .child(Label::new("Right")) - .style(ButtonStyle::Filled), - ) - .into_any_element(), - ), - ], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/button/icon_button.rs b/crates/ui/src/components/button/icon_button.rs deleted file mode 100644 index 961176ed6c..0000000000 --- a/crates/ui/src/components/button/icon_button.rs +++ /dev/null @@ -1,388 +0,0 @@ -use gpui::{AnyView, DefiniteLength, Hsla}; - -use super::button_like::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle}; -use crate::{ElevationIndex, Indicator, SelectableButton, TintColor, prelude::*}; -use crate::{IconName, IconSize}; - -use super::button_icon::ButtonIcon; - -/// The shape of an [`IconButton`]. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -pub enum IconButtonShape { - Square, - Wide, -} - -#[derive(IntoElement, RegisterComponent)] -pub struct IconButton { - base: ButtonLike, - shape: IconButtonShape, - icon: IconName, - icon_size: IconSize, - icon_color: Color, - selected_icon: Option, - selected_icon_color: Option, - indicator: Option, - indicator_border_color: Option, - alpha: Option, -} - -impl IconButton { - pub fn new(id: impl Into, icon: IconName) -> Self { - let mut this = Self { - base: ButtonLike::new(id), - shape: IconButtonShape::Wide, - icon, - icon_size: IconSize::default(), - icon_color: Color::Default, - selected_icon: None, - selected_icon_color: None, - indicator: None, - indicator_border_color: None, - alpha: None, - }; - this.base.base = this.base.base.debug_selector(|| format!("ICON-{:?}", icon)); - this - } - - pub fn shape(mut self, shape: IconButtonShape) -> Self { - self.shape = shape; - self - } - - pub fn icon_size(mut self, icon_size: IconSize) -> Self { - self.icon_size = icon_size; - self - } - - pub fn icon_color(mut self, icon_color: Color) -> Self { - self.icon_color = icon_color; - self - } - - pub fn alpha(mut self, alpha: f32) -> Self { - self.alpha = Some(alpha); - self - } - - pub fn selected_icon(mut self, icon: impl Into>) -> Self { - self.selected_icon = icon.into(); - self - } - - pub fn on_right_click( - mut self, - handler: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.base = self.base.on_right_click(handler); - self - } - - /// Sets the icon color used when the button is in a selected state. - pub fn selected_icon_color(mut self, color: impl Into>) -> Self { - self.selected_icon_color = color.into(); - self - } - - pub fn indicator(mut self, indicator: Indicator) -> Self { - self.indicator = Some(indicator); - self - } - - pub fn indicator_border_color(mut self, color: Option) -> Self { - self.indicator_border_color = color; - - self - } -} - -impl Disableable for IconButton { - fn disabled(mut self, disabled: bool) -> Self { - self.base = self.base.disabled(disabled); - self - } -} - -impl Toggleable for IconButton { - fn toggle_state(mut self, selected: bool) -> Self { - self.base = self.base.toggle_state(selected); - self - } -} - -impl SelectableButton for IconButton { - fn selected_style(mut self, style: ButtonStyle) -> Self { - self.base = self.base.selected_style(style); - self - } -} - -impl Clickable for IconButton { - fn on_click( - mut self, - handler: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.base = self.base.on_click(handler); - self - } - - fn cursor_style(mut self, cursor_style: gpui::CursorStyle) -> Self { - self.base = self.base.cursor_style(cursor_style); - self - } -} - -impl FixedWidth for IconButton { - fn width(mut self, width: impl Into) -> Self { - self.base = self.base.width(width); - self - } - - fn full_width(mut self) -> Self { - self.base = self.base.full_width(); - self - } -} - -impl ButtonCommon for IconButton { - fn id(&self) -> &ElementId { - self.base.id() - } - - fn style(mut self, style: ButtonStyle) -> Self { - self.base = self.base.style(style); - self - } - - fn size(mut self, size: ButtonSize) -> Self { - self.base = self.base.size(size); - self - } - - fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self { - self.base = self.base.tooltip(tooltip); - self - } - - fn tab_index(mut self, tab_index: impl Into) -> Self { - self.base = self.base.tab_index(tab_index); - self - } - - fn layer(mut self, elevation: ElevationIndex) -> Self { - self.base = self.base.layer(elevation); - self - } - - fn track_focus(mut self, focus_handle: &gpui::FocusHandle) -> Self { - self.base = self.base.track_focus(focus_handle); - self - } -} - -impl VisibleOnHover for IconButton { - fn visible_on_hover(mut self, group_name: impl Into) -> Self { - self.base = self.base.visible_on_hover(group_name); - self - } -} - -impl RenderOnce for IconButton { - #[allow(refining_impl_trait)] - fn render(self, window: &mut Window, cx: &mut App) -> ButtonLike { - let is_disabled = self.base.disabled; - let is_selected = self.base.selected; - let selected_style = self.base.selected_style; - - let color = self.icon_color.color(cx).opacity(self.alpha.unwrap_or(1.0)); - self.base - .map(|this| match self.shape { - IconButtonShape::Square => { - let size = self.icon_size.square(window, cx); - this.width(size).height(size.into()) - } - IconButtonShape::Wide => this, - }) - .child( - ButtonIcon::new(self.icon) - .disabled(is_disabled) - .toggle_state(is_selected) - .selected_icon(self.selected_icon) - .selected_icon_color(self.selected_icon_color) - .when_some(selected_style, |this, style| this.selected_style(style)) - .when_some(self.indicator, |this, indicator| { - this.indicator(indicator) - .indicator_border_color(self.indicator_border_color) - }) - .size(self.icon_size) - .color(Color::Custom(color)), - ) - } -} - -impl Component for IconButton { - fn scope() -> ComponentScope { - ComponentScope::Input - } - - fn sort_name() -> &'static str { - "ButtonB" - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Icon Button Styles", - vec![ - single_example( - "Default", - IconButton::new("default", IconName::Check) - .layer(ElevationIndex::Background) - .into_any_element(), - ), - single_example( - "Filled", - IconButton::new("filled", IconName::Check) - .layer(ElevationIndex::Background) - .style(ButtonStyle::Filled) - .into_any_element(), - ), - single_example( - "Subtle", - IconButton::new("subtle", IconName::Check) - .layer(ElevationIndex::Background) - .style(ButtonStyle::Subtle) - .into_any_element(), - ), - single_example( - "Tinted", - IconButton::new("tinted", IconName::Check) - .layer(ElevationIndex::Background) - .style(ButtonStyle::Tinted(TintColor::Accent)) - .into_any_element(), - ), - single_example( - "Transparent", - IconButton::new("transparent", IconName::Check) - .layer(ElevationIndex::Background) - .style(ButtonStyle::Transparent) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Icon Button Shapes", - vec![ - single_example( - "Square", - IconButton::new("square", IconName::Check) - .shape(IconButtonShape::Square) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::Background) - .into_any_element(), - ), - single_example( - "Wide", - IconButton::new("wide", IconName::Check) - .shape(IconButtonShape::Wide) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::Background) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Icon Button Sizes", - vec![ - single_example( - "XSmall", - IconButton::new("xsmall", IconName::Check) - .icon_size(IconSize::XSmall) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::Background) - .into_any_element(), - ), - single_example( - "Small", - IconButton::new("small", IconName::Check) - .icon_size(IconSize::Small) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::Background) - .into_any_element(), - ), - single_example( - "Medium", - IconButton::new("medium", IconName::Check) - .icon_size(IconSize::Medium) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::Background) - .into_any_element(), - ), - single_example( - "XLarge", - IconButton::new("xlarge", IconName::Check) - .icon_size(IconSize::XLarge) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::Background) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Special States", - vec![ - single_example( - "Disabled", - IconButton::new("disabled", IconName::Check) - .disabled(true) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::Background) - .into_any_element(), - ), - single_example( - "Selected", - IconButton::new("selected", IconName::Check) - .toggle_state(true) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::Background) - .into_any_element(), - ), - single_example( - "With Indicator", - IconButton::new("indicator", IconName::Check) - .indicator(Indicator::dot().color(Color::Success)) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::Background) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Custom Colors", - vec![ - single_example( - "Custom Icon Color", - IconButton::new("custom_color", IconName::Check) - .icon_color(Color::Accent) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::Background) - .into_any_element(), - ), - single_example( - "With Alpha", - IconButton::new("alpha", IconName::Check) - .alpha(0.5) - .style(ButtonStyle::Filled) - .layer(ElevationIndex::Background) - .into_any_element(), - ), - ], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/button/split_button.rs b/crates/ui/src/components/button/split_button.rs deleted file mode 100644 index 48f06ff378..0000000000 --- a/crates/ui/src/components/button/split_button.rs +++ /dev/null @@ -1,97 +0,0 @@ -use gpui::{ - AnyElement, App, BoxShadow, IntoElement, ParentElement, RenderOnce, Styled, Window, div, hsla, - point, prelude::FluentBuilder, px, -}; -use theme::ActiveTheme; - -use crate::{ElevationIndex, IconButton, h_flex}; - -use super::ButtonLike; - -#[derive(Clone, Copy, PartialEq)] -pub enum SplitButtonStyle { - Filled, - Outlined, - Transparent, -} - -pub enum SplitButtonKind { - ButtonLike(ButtonLike), - IconButton(IconButton), -} - -impl From for SplitButtonKind { - fn from(icon_button: IconButton) -> Self { - Self::IconButton(icon_button) - } -} - -impl From for SplitButtonKind { - fn from(button_like: ButtonLike) -> Self { - Self::ButtonLike(button_like) - } -} - -/// /// A button with two parts: a primary action on the left and a secondary action on the right. -/// -/// The left side is a [`ButtonLike`] with the main action, while the right side can contain -/// any element (typically a dropdown trigger or similar). -/// -/// The two sections are visually separated by a divider, but presented as a unified control. -#[derive(IntoElement)] -pub struct SplitButton { - left: SplitButtonKind, - right: AnyElement, - style: SplitButtonStyle, -} - -impl SplitButton { - pub fn new(left: impl Into, right: AnyElement) -> Self { - Self { - left: left.into(), - right, - style: SplitButtonStyle::Filled, - } - } - - pub fn style(mut self, style: SplitButtonStyle) -> Self { - self.style = style; - self - } -} - -impl RenderOnce for SplitButton { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let is_filled_or_outlined = matches!( - self.style, - SplitButtonStyle::Filled | SplitButtonStyle::Outlined - ); - - h_flex() - .rounded_sm() - .when(is_filled_or_outlined, |this| { - this.border_1() - .border_color(cx.theme().colors().border.opacity(0.8)) - }) - .child(div().flex_grow().child(match self.left { - SplitButtonKind::ButtonLike(button) => button.into_any_element(), - SplitButtonKind::IconButton(icon) => icon.into_any_element(), - })) - .child( - div() - .h_full() - .w_px() - .bg(cx.theme().colors().border.opacity(0.5)), - ) - .child(self.right) - .when(self.style == SplitButtonStyle::Filled, |this| { - this.bg(ElevationIndex::Surface.on_elevation_bg(cx)) - .shadow(vec![BoxShadow { - color: hsla(0.0, 0.0, 0.0, 0.16), - offset: point(px(0.), px(1.)), - blur_radius: px(0.), - spread_radius: px(0.), - }]) - }) - } -} diff --git a/crates/ui/src/components/button/toggle_button.rs b/crates/ui/src/components/button/toggle_button.rs deleted file mode 100644 index 5cecfef062..0000000000 --- a/crates/ui/src/components/button/toggle_button.rs +++ /dev/null @@ -1,769 +0,0 @@ -use std::rc::Rc; - -use gpui::{AnyView, ClickEvent, relative}; - -use crate::{ButtonLike, ButtonLikeRounding, TintColor, Tooltip, prelude::*}; - -/// The position of a [`ToggleButton`] within a group of buttons. -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub struct ToggleButtonPosition { - /// The toggle button is one of the leftmost of the group. - leftmost: bool, - /// The toggle button is one of the rightmost of the group. - rightmost: bool, - /// The toggle button is one of the topmost of the group. - topmost: bool, - /// The toggle button is one of the bottommost of the group. - bottommost: bool, -} - -impl ToggleButtonPosition { - pub const HORIZONTAL_FIRST: Self = Self { - leftmost: true, - ..Self::HORIZONTAL_MIDDLE - }; - pub const HORIZONTAL_MIDDLE: Self = Self { - leftmost: false, - rightmost: false, - topmost: true, - bottommost: true, - }; - pub const HORIZONTAL_LAST: Self = Self { - rightmost: true, - ..Self::HORIZONTAL_MIDDLE - }; - - pub(crate) fn to_rounding(self) -> ButtonLikeRounding { - ButtonLikeRounding { - top_left: self.topmost && self.leftmost, - top_right: self.topmost && self.rightmost, - bottom_right: self.bottommost && self.rightmost, - bottom_left: self.bottommost && self.leftmost, - } - } -} - -pub struct ButtonConfiguration { - label: SharedString, - icon: Option, - on_click: Box, - selected: bool, - tooltip: Option AnyView>>, -} - -mod private { - pub trait ToggleButtonStyle {} -} - -pub trait ButtonBuilder: 'static + private::ToggleButtonStyle { - fn into_configuration(self) -> ButtonConfiguration; -} - -pub struct ToggleButtonSimple { - label: SharedString, - on_click: Box, - selected: bool, - tooltip: Option AnyView>>, -} - -impl ToggleButtonSimple { - pub fn new( - label: impl Into, - on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - Self { - label: label.into(), - on_click: Box::new(on_click), - selected: false, - tooltip: None, - } - } - - pub fn selected(mut self, selected: bool) -> Self { - self.selected = selected; - self - } - - pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self { - self.tooltip = Some(Rc::new(tooltip)); - self - } -} - -impl private::ToggleButtonStyle for ToggleButtonSimple {} - -impl ButtonBuilder for ToggleButtonSimple { - fn into_configuration(self) -> ButtonConfiguration { - ButtonConfiguration { - label: self.label, - icon: None, - on_click: self.on_click, - selected: self.selected, - tooltip: self.tooltip, - } - } -} - -pub struct ToggleButtonWithIcon { - label: SharedString, - icon: IconName, - on_click: Box, - selected: bool, - tooltip: Option AnyView>>, -} - -impl ToggleButtonWithIcon { - pub fn new( - label: impl Into, - icon: IconName, - on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - Self { - label: label.into(), - icon, - on_click: Box::new(on_click), - selected: false, - tooltip: None, - } - } - - pub fn selected(mut self, selected: bool) -> Self { - self.selected = selected; - self - } - - pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self { - self.tooltip = Some(Rc::new(tooltip)); - self - } -} - -impl private::ToggleButtonStyle for ToggleButtonWithIcon {} - -impl ButtonBuilder for ToggleButtonWithIcon { - fn into_configuration(self) -> ButtonConfiguration { - ButtonConfiguration { - label: self.label, - icon: Some(self.icon), - on_click: self.on_click, - selected: self.selected, - tooltip: self.tooltip, - } - } -} - -#[derive(Clone, Copy, PartialEq)] -pub enum ToggleButtonGroupStyle { - Transparent, - Filled, - Outlined, -} - -#[derive(Clone, Copy, PartialEq)] -pub enum ToggleButtonGroupSize { - Default, - Medium, - Large, - Custom(Rems), -} - -#[derive(IntoElement)] -pub struct ToggleButtonGroup -where - T: ButtonBuilder, -{ - group_name: SharedString, - rows: [[T; COLS]; ROWS], - style: ToggleButtonGroupStyle, - size: ToggleButtonGroupSize, - label_size: LabelSize, - group_width: Option, - auto_width: bool, - selected_index: usize, - tab_index: Option, -} - -impl ToggleButtonGroup { - pub fn single_row(group_name: impl Into, buttons: [T; COLS]) -> Self { - Self { - group_name: group_name.into(), - rows: [buttons], - style: ToggleButtonGroupStyle::Transparent, - size: ToggleButtonGroupSize::Default, - label_size: LabelSize::Small, - group_width: None, - auto_width: false, - selected_index: 0, - tab_index: None, - } - } -} - -impl ToggleButtonGroup { - pub fn two_rows( - group_name: impl Into, - first_row: [T; COLS], - second_row: [T; COLS], - ) -> Self { - Self { - group_name: group_name.into(), - rows: [first_row, second_row], - style: ToggleButtonGroupStyle::Transparent, - size: ToggleButtonGroupSize::Default, - label_size: LabelSize::Small, - group_width: None, - auto_width: false, - selected_index: 0, - tab_index: None, - } - } -} - -impl ToggleButtonGroup { - pub fn style(mut self, style: ToggleButtonGroupStyle) -> Self { - self.style = style; - self - } - - pub fn size(mut self, size: ToggleButtonGroupSize) -> Self { - self.size = size; - self - } - - pub fn selected_index(mut self, index: usize) -> Self { - self.selected_index = index; - self - } - - /// Makes the button group size itself to fit the content of the buttons, - /// rather than filling the full width of its parent. - pub fn auto_width(mut self) -> Self { - self.auto_width = true; - self - } - - pub fn label_size(mut self, label_size: LabelSize) -> Self { - self.label_size = label_size; - self - } - - /// Sets the tab index for the toggle button group. - /// The tab index is set to the initial value provided, then the - /// value is incremented by the number of buttons in the group. - pub fn tab_index(mut self, tab_index: &mut isize) -> Self { - self.tab_index = Some(*tab_index); - *tab_index += (COLS * ROWS) as isize; - self - } - - const fn button_width() -> DefiniteLength { - relative(1. / COLS as f32) - } -} - -impl FixedWidth - for ToggleButtonGroup -{ - fn width(mut self, width: impl Into) -> Self { - self.group_width = Some(width.into()); - self - } - - fn full_width(mut self) -> Self { - self.group_width = Some(relative(1.)); - self - } -} - -impl RenderOnce - for ToggleButtonGroup -{ - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let custom_height = match self.size { - ToggleButtonGroupSize::Custom(height) => Some(height), - _ => None, - }; - - let entries = - self.rows.into_iter().enumerate().map(|(row_index, row)| { - let group_name = self.group_name.clone(); - row.into_iter().enumerate().map(move |(col_index, button)| { - let ButtonConfiguration { - label, - icon, - on_click, - selected, - tooltip, - } = button.into_configuration(); - - let entry_index = row_index * COLS + col_index; - - ButtonLike::new((group_name.clone(), entry_index)) - .when(!self.auto_width, |this| this.full_width()) - .rounding(Some( - ToggleButtonPosition { - leftmost: col_index == 0, - rightmost: col_index == COLS - 1, - topmost: row_index == 0, - bottommost: row_index == ROWS - 1, - } - .to_rounding(), - )) - .when_some(self.tab_index, |this, tab_index| { - this.tab_index(tab_index + entry_index as isize) - }) - .when(entry_index == self.selected_index || selected, |this| { - this.toggle_state(true) - .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - }) - .when(self.style == ToggleButtonGroupStyle::Filled, |button| { - button.style(ButtonStyle::Filled) - }) - .when(self.size == ToggleButtonGroupSize::Medium, |button| { - button.size(ButtonSize::Medium) - }) - .when(self.size == ToggleButtonGroupSize::Large, |button| { - button.size(ButtonSize::Large) - }) - .when_some(custom_height, |button, height| button.height(height.into())) - .child( - h_flex() - .w_full() - .px_2() - .gap_1p5() - .justify_center() - .flex_none() - .when_some(icon, |this, icon| { - this.py_2() - .child(Icon::new(icon).size(IconSize::XSmall).map(|this| { - if entry_index == self.selected_index || selected { - this.color(Color::Accent) - } else { - this.color(Color::Muted) - } - })) - }) - .child(Label::new(label).size(self.label_size).when( - entry_index == self.selected_index || selected, - |this| this.color(Color::Accent), - )), - ) - .when_some(tooltip, |this, tooltip| { - this.tooltip(move |window, cx| tooltip(window, cx)) - }) - .on_click(on_click) - .into_any_element() - }) - }); - - let border_color = cx.theme().colors().border.opacity(0.6); - let is_outlined_or_filled = self.style == ToggleButtonGroupStyle::Outlined - || self.style == ToggleButtonGroupStyle::Filled; - let is_transparent = self.style == ToggleButtonGroupStyle::Transparent; - - v_flex() - .map(|this| { - if let Some(width) = self.group_width { - this.w(width) - } else if self.auto_width { - this - } else { - this.w_full() - } - }) - .rounded_md() - .overflow_hidden() - .map(|this| { - if is_transparent { - this.gap_px() - } else { - this.border_1().border_color(border_color) - } - }) - .children(entries.enumerate().map(|(row_index, row)| { - let last_row = row_index == ROWS - 1; - h_flex() - .when(!is_outlined_or_filled, |this| this.gap_px()) - .when(is_outlined_or_filled && !last_row, |this| { - this.border_b_1().border_color(border_color) - }) - .children(row.enumerate().map(|(item_index, item)| { - let last_item = item_index == COLS - 1; - div() - .when(is_outlined_or_filled && !last_item, |this| { - this.border_r_1().border_color(border_color) - }) - .when(!self.auto_width, |this| this.w(Self::button_width())) - .overflow_hidden() - .child(item) - })) - })) - } -} - -fn register_toggle_button_group() { - component::register_component::>(); -} - -component::__private::inventory::submit! { - component::ComponentFn::new(register_toggle_button_group) -} - -impl Component - for ToggleButtonGroup -{ - fn name() -> &'static str { - "ToggleButtonGroup" - } - - fn scope() -> ComponentScope { - ComponentScope::Input - } - - fn sort_name() -> &'static str { - "ButtonG" - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![example_group_with_title( - "Transparent Variant", - vec![ - single_example( - "Single Row Group", - ToggleButtonGroup::single_row( - "single_row_test", - [ - ToggleButtonSimple::new("First", |_, _, _| {}), - ToggleButtonSimple::new("Second", |_, _, _| {}), - ToggleButtonSimple::new("Third", |_, _, _| {}), - ], - ) - .selected_index(1) - .into_any_element(), - ), - single_example( - "Single Row Group with icons", - ToggleButtonGroup::single_row( - "single_row_test_icon", - [ - ToggleButtonWithIcon::new( - "First", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Second", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Third", - IconName::AiZed, - |_, _, _| {}, - ), - ], - ) - .selected_index(1) - .into_any_element(), - ), - single_example( - "Multiple Row Group", - ToggleButtonGroup::two_rows( - "multiple_row_test", - [ - ToggleButtonSimple::new("First", |_, _, _| {}), - ToggleButtonSimple::new("Second", |_, _, _| {}), - ToggleButtonSimple::new("Third", |_, _, _| {}), - ], - [ - ToggleButtonSimple::new("Fourth", |_, _, _| {}), - ToggleButtonSimple::new("Fifth", |_, _, _| {}), - ToggleButtonSimple::new("Sixth", |_, _, _| {}), - ], - ) - .selected_index(3) - .into_any_element(), - ), - single_example( - "Multiple Row Group with Icons", - ToggleButtonGroup::two_rows( - "multiple_row_test_icons", - [ - ToggleButtonWithIcon::new( - "First", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Second", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Third", - IconName::AiZed, - |_, _, _| {}, - ), - ], - [ - ToggleButtonWithIcon::new( - "Fourth", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Fifth", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Sixth", - IconName::AiZed, - |_, _, _| {}, - ), - ], - ) - .selected_index(3) - .into_any_element(), - ), - ], - )]) - .children(vec![example_group_with_title( - "Outlined Variant", - vec![ - single_example( - "Single Row Group", - ToggleButtonGroup::single_row( - "single_row_test_outline", - [ - ToggleButtonSimple::new("First", |_, _, _| {}), - ToggleButtonSimple::new("Second", |_, _, _| {}), - ToggleButtonSimple::new("Third", |_, _, _| {}), - ], - ) - .selected_index(1) - .style(ToggleButtonGroupStyle::Outlined) - .into_any_element(), - ), - single_example( - "Single Row Group with icons", - ToggleButtonGroup::single_row( - "single_row_test_icon_outlined", - [ - ToggleButtonWithIcon::new( - "First", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Second", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Third", - IconName::AiZed, - |_, _, _| {}, - ), - ], - ) - .selected_index(1) - .style(ToggleButtonGroupStyle::Outlined) - .into_any_element(), - ), - single_example( - "Multiple Row Group", - ToggleButtonGroup::two_rows( - "multiple_row_test", - [ - ToggleButtonSimple::new("First", |_, _, _| {}), - ToggleButtonSimple::new("Second", |_, _, _| {}), - ToggleButtonSimple::new("Third", |_, _, _| {}), - ], - [ - ToggleButtonSimple::new("Fourth", |_, _, _| {}), - ToggleButtonSimple::new("Fifth", |_, _, _| {}), - ToggleButtonSimple::new("Sixth", |_, _, _| {}), - ], - ) - .selected_index(3) - .style(ToggleButtonGroupStyle::Outlined) - .into_any_element(), - ), - single_example( - "Multiple Row Group with Icons", - ToggleButtonGroup::two_rows( - "multiple_row_test", - [ - ToggleButtonWithIcon::new( - "First", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Second", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Third", - IconName::AiZed, - |_, _, _| {}, - ), - ], - [ - ToggleButtonWithIcon::new( - "Fourth", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Fifth", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Sixth", - IconName::AiZed, - |_, _, _| {}, - ), - ], - ) - .selected_index(3) - .style(ToggleButtonGroupStyle::Outlined) - .into_any_element(), - ), - ], - )]) - .children(vec![example_group_with_title( - "Filled Variant", - vec![ - single_example( - "Single Row Group", - ToggleButtonGroup::single_row( - "single_row_test_outline", - [ - ToggleButtonSimple::new("First", |_, _, _| {}), - ToggleButtonSimple::new("Second", |_, _, _| {}), - ToggleButtonSimple::new("Third", |_, _, _| {}), - ], - ) - .selected_index(2) - .style(ToggleButtonGroupStyle::Filled) - .into_any_element(), - ), - single_example( - "Single Row Group with icons", - ToggleButtonGroup::single_row( - "single_row_test_icon_outlined", - [ - ToggleButtonWithIcon::new( - "First", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Second", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Third", - IconName::AiZed, - |_, _, _| {}, - ), - ], - ) - .selected_index(1) - .style(ToggleButtonGroupStyle::Filled) - .into_any_element(), - ), - single_example( - "Multiple Row Group", - ToggleButtonGroup::two_rows( - "multiple_row_test", - [ - ToggleButtonSimple::new("First", |_, _, _| {}), - ToggleButtonSimple::new("Second", |_, _, _| {}), - ToggleButtonSimple::new("Third", |_, _, _| {}), - ], - [ - ToggleButtonSimple::new("Fourth", |_, _, _| {}), - ToggleButtonSimple::new("Fifth", |_, _, _| {}), - ToggleButtonSimple::new("Sixth", |_, _, _| {}), - ], - ) - .selected_index(3) - .width(rems_from_px(100.)) - .style(ToggleButtonGroupStyle::Filled) - .into_any_element(), - ), - single_example( - "Multiple Row Group with Icons", - ToggleButtonGroup::two_rows( - "multiple_row_test", - [ - ToggleButtonWithIcon::new( - "First", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Second", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Third", - IconName::AiZed, - |_, _, _| {}, - ), - ], - [ - ToggleButtonWithIcon::new( - "Fourth", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Fifth", - IconName::AiZed, - |_, _, _| {}, - ), - ToggleButtonWithIcon::new( - "Sixth", - IconName::AiZed, - |_, _, _| {}, - ), - ], - ) - .selected_index(3) - .width(rems_from_px(100.)) - .style(ToggleButtonGroupStyle::Filled) - .into_any_element(), - ), - ], - )]) - .children(vec![single_example( - "With Tooltips", - ToggleButtonGroup::single_row( - "with_tooltips", - [ - ToggleButtonSimple::new("First", |_, _, _| {}) - .tooltip(Tooltip::text("This is a tooltip. Hello!")), - ToggleButtonSimple::new("Second", |_, _, _| {}) - .tooltip(Tooltip::text("This is a tooltip. Hey?")), - ToggleButtonSimple::new("Third", |_, _, _| {}) - .tooltip(Tooltip::text("This is a tooltip. Get out of here now!")), - ], - ) - .selected_index(1) - .into_any_element(), - )]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/callout.rs b/crates/ui/src/components/callout.rs deleted file mode 100644 index 4eb849d7f6..0000000000 --- a/crates/ui/src/components/callout.rs +++ /dev/null @@ -1,327 +0,0 @@ -use gpui::AnyElement; - -use crate::prelude::*; - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum BorderPosition { - Top, - Bottom, -} - -/// A callout component for displaying important information that requires user attention. -/// -/// # Usage Example -/// -/// ``` -/// use ui::prelude::*; -/// use ui::{Button, Callout, IconName, Label, Severity}; -/// -/// let callout = Callout::new() -/// .severity(Severity::Warning) -/// .icon(IconName::Warning) -/// .title("Be aware of your subscription!") -/// .description("Your subscription is about to expire. Renew now!") -/// .actions_slot(Button::new("renew", "Renew Now")); -/// ``` -/// -#[derive(IntoElement, RegisterComponent)] -pub struct Callout { - severity: Severity, - icon: Option, - title: Option, - description: Option, - description_slot: Option, - actions_slot: Option, - dismiss_action: Option, - line_height: Option, - border_position: BorderPosition, -} - -impl Callout { - /// Creates a new `Callout` component with default styling. - pub fn new() -> Self { - Self { - severity: Severity::Info, - icon: None, - title: None, - description: None, - description_slot: None, - actions_slot: None, - dismiss_action: None, - line_height: None, - border_position: BorderPosition::Top, - } - } - - /// Sets the severity of the callout. - pub fn severity(mut self, severity: Severity) -> Self { - self.severity = severity; - self - } - - /// Sets the icon to display in the callout. - pub fn icon(mut self, icon: IconName) -> Self { - self.icon = Some(icon); - self - } - - /// Sets the title of the callout. - pub fn title(mut self, title: impl Into) -> Self { - self.title = Some(title.into()); - self - } - - /// Sets the description of the callout. - /// The description can be single or multi-line text. - pub fn description(mut self, description: impl Into) -> Self { - self.description = Some(description.into()); - self - } - - /// Allows for any element—like markdown elements—to fill the description slot of the callout. - /// This method wins over `description` if both happen to be set. - pub fn description_slot(mut self, description: impl IntoElement) -> Self { - self.description_slot = Some(description.into_any_element()); - self - } - - /// Sets the primary call-to-action button. - pub fn actions_slot(mut self, action: impl IntoElement) -> Self { - self.actions_slot = Some(action.into_any_element()); - self - } - - /// Sets an optional dismiss button, which is usually an icon button with a close icon. - /// This button is always rendered as the last one to the far right. - pub fn dismiss_action(mut self, action: impl IntoElement) -> Self { - self.dismiss_action = Some(action.into_any_element()); - self - } - - /// Sets a custom line height for the callout content. - pub fn line_height(mut self, line_height: Pixels) -> Self { - self.line_height = Some(line_height); - self - } - - /// Sets the border position in the callout. - pub fn border_position(mut self, border_position: BorderPosition) -> Self { - self.border_position = border_position; - self - } -} - -impl RenderOnce for Callout { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let line_height = self.line_height.unwrap_or(window.line_height()); - - let has_actions = self.actions_slot.is_some() || self.dismiss_action.is_some(); - - let (icon, icon_color, bg_color) = match self.severity { - Severity::Info => ( - IconName::Info, - Color::Muted, - cx.theme().colors().panel_background.opacity(0.), - ), - Severity::Success => ( - IconName::Check, - Color::Success, - cx.theme().status().success.opacity(0.1), - ), - Severity::Warning => ( - IconName::Warning, - Color::Warning, - cx.theme().status().warning_background.opacity(0.2), - ), - Severity::Error => ( - IconName::XCircle, - Color::Error, - cx.theme().status().error.opacity(0.08), - ), - }; - - h_flex() - .min_w_0() - .w_full() - .p_2() - .gap_2() - .items_start() - .map(|this| match self.border_position { - BorderPosition::Top => this.border_t_1(), - BorderPosition::Bottom => this.border_b_1(), - }) - .border_color(cx.theme().colors().border) - .bg(bg_color) - .overflow_x_hidden() - .when(self.icon.is_some(), |this| { - this.child( - h_flex() - .h(line_height) - .justify_center() - .child(Icon::new(icon).size(IconSize::Small).color(icon_color)), - ) - }) - .child( - v_flex() - .min_w_0() - .w_full() - .child( - h_flex() - .min_h(line_height) - .w_full() - .gap_1() - .justify_between() - .flex_wrap() - .when_some(self.title, |this, title| { - this.child(h_flex().child(Label::new(title).size(LabelSize::Small))) - }) - .when(has_actions, |this| { - this.child( - h_flex() - .gap_0p5() - .when_some(self.actions_slot, |this, action| { - this.child(action) - }) - .when_some(self.dismiss_action, |this, action| { - this.child(action) - }), - ) - }), - ) - .map(|this| { - if let Some(description_slot) = self.description_slot { - this.child( - div() - .w_full() - .flex_1() - .text_ui_sm(cx) - .child(description_slot), - ) - } else if let Some(description) = self.description { - this.child( - div() - .w_full() - .flex_1() - .text_ui_sm(cx) - .text_color(cx.theme().colors().text_muted) - .child(description), - ) - } else { - this - } - }), - ) - } -} - -impl Component for Callout { - fn scope() -> ComponentScope { - ComponentScope::DataDisplay - } - - fn description() -> Option<&'static str> { - Some( - "Used to display a callout for situations where the user needs to know some information, and likely make a decision. This might be a thread running out of tokens, or running out of prompts on a plan and needing to upgrade.", - ) - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - let single_action = || Button::new("got-it", "Got it").label_size(LabelSize::Small); - let multiple_actions = || { - h_flex() - .gap_0p5() - .child(Button::new("update", "Backup & Update").label_size(LabelSize::Small)) - .child(Button::new("dismiss", "Dismiss").label_size(LabelSize::Small)) - }; - - let basic_examples = vec![ - single_example( - "Simple with Title Only", - Callout::new() - .icon(IconName::Info) - .title("System maintenance scheduled for tonight") - .actions_slot(single_action()) - .into_any_element(), - ) - .width(px(580.)), - single_example( - "With Title and Description", - Callout::new() - .icon(IconName::Warning) - .title("Your settings contain deprecated values") - .description( - "We'll backup your current settings and update them to the new format.", - ) - .actions_slot(single_action()) - .into_any_element(), - ) - .width(px(580.)), - single_example( - "Error with Multiple Actions", - Callout::new() - .icon(IconName::Close) - .title("Thread reached the token limit") - .description("Start a new thread from a summary to continue the conversation.") - .actions_slot(multiple_actions()) - .into_any_element(), - ) - .width(px(580.)), - single_example( - "Multi-line Description", - Callout::new() - .icon(IconName::Sparkle) - .title("Upgrade to Pro") - .description("• Unlimited threads\n• Priority support\n• Advanced analytics") - .actions_slot(multiple_actions()) - .into_any_element(), - ) - .width(px(580.)), - ]; - - let severity_examples = vec![ - single_example( - "Info", - Callout::new() - .icon(IconName::Info) - .title("System maintenance scheduled for tonight") - .actions_slot(single_action()) - .into_any_element(), - ), - single_example( - "Warning", - Callout::new() - .severity(Severity::Warning) - .icon(IconName::Triangle) - .title("System maintenance scheduled for tonight") - .actions_slot(single_action()) - .into_any_element(), - ), - single_example( - "Error", - Callout::new() - .severity(Severity::Error) - .icon(IconName::XCircle) - .title("System maintenance scheduled for tonight") - .actions_slot(single_action()) - .into_any_element(), - ), - single_example( - "Success", - Callout::new() - .severity(Severity::Success) - .icon(IconName::Check) - .title("System maintenance scheduled for tonight") - .actions_slot(single_action()) - .into_any_element(), - ), - ]; - - Some( - v_flex() - .gap_4() - .child(example_group(basic_examples).vertical()) - .child(example_group_with_title("Severity", severity_examples).vertical()) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/chip.rs b/crates/ui/src/components/chip.rs deleted file mode 100644 index 8d0000db94..0000000000 --- a/crates/ui/src/components/chip.rs +++ /dev/null @@ -1,106 +0,0 @@ -use crate::prelude::*; -use gpui::{AnyElement, Hsla, IntoElement, ParentElement, Styled}; - -/// Chips provide a container for an informative label. -/// -/// # Usage Example -/// -/// ``` -/// use ui::Chip; -/// -/// let chip = Chip::new("This Chip"); -/// ``` -#[derive(IntoElement, RegisterComponent)] -pub struct Chip { - label: SharedString, - label_color: Color, - label_size: LabelSize, - bg_color: Option, -} - -impl Chip { - /// Creates a new `Chip` component with the specified label. - pub fn new(label: impl Into) -> Self { - Self { - label: label.into(), - label_color: Color::Default, - label_size: LabelSize::XSmall, - bg_color: None, - } - } - - /// Sets the color of the label. - pub fn label_color(mut self, color: Color) -> Self { - self.label_color = color; - self - } - - /// Sets the size of the label. - pub fn label_size(mut self, size: LabelSize) -> Self { - self.label_size = size; - self - } - - /// Sets a custom background color for the callout content. - pub fn bg_color(mut self, color: Hsla) -> Self { - self.bg_color = Some(color); - self - } -} - -impl RenderOnce for Chip { - fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - let bg_color = self - .bg_color - .unwrap_or(cx.theme().colors().element_background); - - h_flex() - .min_w_0() - .flex_initial() - .px_1() - .border_1() - .rounded_sm() - .border_color(cx.theme().colors().border) - .bg(bg_color) - .overflow_hidden() - .child( - Label::new(self.label) - .size(self.label_size) - .color(self.label_color) - .buffer_font(cx), - ) - } -} - -impl Component for Chip { - fn scope() -> ComponentScope { - ComponentScope::DataDisplay - } - - fn preview(_window: &mut Window, cx: &mut App) -> Option { - let chip_examples = vec![ - single_example("Default", Chip::new("Chip Example").into_any_element()), - single_example( - "Customized Label Color", - Chip::new("Chip Example") - .label_color(Color::Accent) - .into_any_element(), - ), - single_example( - "Customized Label Size", - Chip::new("Chip Example") - .label_size(LabelSize::Large) - .label_color(Color::Accent) - .into_any_element(), - ), - single_example( - "Customized Background Color", - Chip::new("Chip Example") - .bg_color(cx.theme().colors().text_accent.opacity(0.1)) - .into_any_element(), - ), - ]; - - Some(example_group(chip_examples).vertical().into_any_element()) - } -} diff --git a/crates/ui/src/components/content_group.rs b/crates/ui/src/components/content_group.rs deleted file mode 100644 index f89d38d153..0000000000 --- a/crates/ui/src/components/content_group.rs +++ /dev/null @@ -1,137 +0,0 @@ -use crate::component_prelude::*; -use crate::prelude::*; -use gpui::{AnyElement, IntoElement, ParentElement, StyleRefinement, Styled}; -use smallvec::SmallVec; - -/// Creates a new [ContentGroup]. -pub fn content_group() -> ContentGroup { - ContentGroup::new() -} - -/// A [ContentGroup] that vertically stacks its children. -/// -/// This is a convenience function that simply combines [`ContentGroup`] and [`v_flex`](crate::v_flex). -pub fn v_container() -> ContentGroup { - content_group().v_flex() -} - -/// Creates a new horizontal [ContentGroup]. -/// -/// This is a convenience function that simply combines [`ContentGroup`] and [`h_flex`](crate::h_flex). -pub fn h_container() -> ContentGroup { - content_group().h_flex() -} - -/// A flexible container component that can hold other elements. -#[derive(IntoElement, Documented, RegisterComponent)] -pub struct ContentGroup { - base: Div, - border: bool, - fill: bool, - children: SmallVec<[AnyElement; 2]>, -} - -impl ContentGroup { - /// Creates a new [`ContentGroup`]. - pub fn new() -> Self { - Self { - base: div(), - border: true, - fill: true, - children: SmallVec::new(), - } - } - - /// Removes the border from the [`ContentGroup`]. - pub fn borderless(mut self) -> Self { - self.border = false; - self - } - - /// Removes the background fill from the [`ContentGroup`]. - pub fn unfilled(mut self) -> Self { - self.fill = false; - self - } -} - -impl ParentElement for ContentGroup { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl Styled for ContentGroup { - fn style(&mut self) -> &mut StyleRefinement { - self.base.style() - } -} - -impl RenderOnce for ContentGroup { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - // TODO: - // Baked in padding will make scrollable views inside of content boxes awkward. - // - // Do we make the padding optional, or do we push to use a different component? - - self.base - .when(self.fill, |this| { - this.bg(cx.theme().colors().text.opacity(0.05)) - }) - .when(self.border, |this| { - this.border_1().border_color(cx.theme().colors().border) - }) - .rounded_sm() - .children(self.children) - } -} - -impl Component for ContentGroup { - fn scope() -> ComponentScope { - ComponentScope::Layout - } - - fn description() -> Option<&'static str> { - Some(ContentGroup::DOCS) - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - example_group(vec![ - single_example( - "Default", - ContentGroup::new() - .flex_1() - .items_center() - .justify_center() - .h_48() - .child(Label::new("Default ContentGroup")) - .into_any_element(), - ).description("A contained style for laying out groups of content. Has a default background and border color."), - single_example( - "Without Border", - ContentGroup::new() - .flex_1() - .items_center() - .justify_center() - .h_48() - .borderless() - .child(Label::new("Borderless ContentGroup")) - .into_any_element(), - ), - single_example( - "Without Fill", - ContentGroup::new() - .flex_1() - .items_center() - .justify_center() - .h_48() - .unfilled() - .child(Label::new("Unfilled ContentGroup")) - .into_any_element(), - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/context_menu.rs b/crates/ui/src/components/context_menu.rs deleted file mode 100644 index a4bae64740..0000000000 --- a/crates/ui/src/components/context_menu.rs +++ /dev/null @@ -1,1392 +0,0 @@ -use crate::{ - Icon, IconButtonShape, IconName, IconSize, KeyBinding, Label, List, ListItem, ListSeparator, - ListSubHeader, h_flex, prelude::*, utils::WithRemSize, v_flex, -}; -use gpui::{ - Action, AnyElement, App, AppContext as _, DismissEvent, Entity, EventEmitter, FocusHandle, - Focusable, IntoElement, Render, Subscription, px, -}; -use menu::{SelectFirst, SelectLast, SelectNext, SelectPrevious}; -use settings::Settings; -use std::{rc::Rc, time::Duration}; -use theme::ThemeSettings; - -use super::Tooltip; - -pub enum ContextMenuItem { - Separator, - Header(SharedString), - /// title, link_label, link_url - HeaderWithLink(SharedString, SharedString, SharedString), // This could be folded into header - Label(SharedString), - Entry(ContextMenuEntry), - CustomEntry { - entry_render: Box AnyElement>, - handler: Rc, &mut Window, &mut App)>, - selectable: bool, - documentation_aside: Option, - }, -} - -impl ContextMenuItem { - pub fn custom_entry( - entry_render: impl Fn(&mut Window, &mut App) -> AnyElement + 'static, - handler: impl Fn(&mut Window, &mut App) + 'static, - documentation_aside: Option, - ) -> Self { - Self::CustomEntry { - entry_render: Box::new(entry_render), - handler: Rc::new(move |_, window, cx| handler(window, cx)), - selectable: true, - documentation_aside, - } - } -} - -pub struct ContextMenuEntry { - toggle: Option<(IconPosition, bool)>, - label: SharedString, - icon: Option, - custom_icon_path: Option, - custom_icon_svg: Option, - icon_position: IconPosition, - icon_size: IconSize, - icon_color: Option, - handler: Rc, &mut Window, &mut App)>, - action: Option>, - disabled: bool, - documentation_aside: Option, - end_slot_icon: Option, - end_slot_title: Option, - end_slot_handler: Option, &mut Window, &mut App)>>, - show_end_slot_on_hover: bool, -} - -impl ContextMenuEntry { - pub fn new(label: impl Into) -> Self { - ContextMenuEntry { - toggle: None, - label: label.into(), - icon: None, - custom_icon_path: None, - custom_icon_svg: None, - icon_position: IconPosition::Start, - icon_size: IconSize::Small, - icon_color: None, - handler: Rc::new(|_, _, _| {}), - action: None, - disabled: false, - documentation_aside: None, - end_slot_icon: None, - end_slot_title: None, - end_slot_handler: None, - show_end_slot_on_hover: false, - } - } - - pub fn toggleable(mut self, toggle_position: IconPosition, toggled: bool) -> Self { - self.toggle = Some((toggle_position, toggled)); - self - } - - pub fn icon(mut self, icon: IconName) -> Self { - self.icon = Some(icon); - self - } - - pub fn custom_icon_path(mut self, path: impl Into) -> Self { - self.custom_icon_path = Some(path.into()); - self.custom_icon_svg = None; // Clear other icon sources if custom path is set - self.icon = None; - self - } - - pub fn custom_icon_svg(mut self, svg: impl Into) -> Self { - self.custom_icon_svg = Some(svg.into()); - self.custom_icon_path = None; // Clear other icon sources if custom path is set - self.icon = None; - self - } - - pub fn icon_position(mut self, position: IconPosition) -> Self { - self.icon_position = position; - self - } - - pub fn icon_size(mut self, icon_size: IconSize) -> Self { - self.icon_size = icon_size; - self - } - - pub fn icon_color(mut self, icon_color: Color) -> Self { - self.icon_color = Some(icon_color); - self - } - - pub fn toggle(mut self, toggle_position: IconPosition, toggled: bool) -> Self { - self.toggle = Some((toggle_position, toggled)); - self - } - - pub fn action(mut self, action: Box) -> Self { - self.action = Some(action); - self - } - - pub fn handler(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self { - self.handler = Rc::new(move |_, window, cx| handler(window, cx)); - self - } - - pub fn disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self - } - - pub fn documentation_aside( - mut self, - side: DocumentationSide, - edge: DocumentationEdge, - render: impl Fn(&mut App) -> AnyElement + 'static, - ) -> Self { - self.documentation_aside = Some(DocumentationAside { - side, - edge, - render: Rc::new(render), - }); - - self - } -} - -impl FluentBuilder for ContextMenuEntry {} - -impl From for ContextMenuItem { - fn from(entry: ContextMenuEntry) -> Self { - ContextMenuItem::Entry(entry) - } -} - -pub struct ContextMenu { - builder: Option) -> Self>>, - items: Vec, - focus_handle: FocusHandle, - action_context: Option, - selected_index: Option, - delayed: bool, - clicked: bool, - end_slot_action: Option>, - key_context: SharedString, - _on_blur_subscription: Subscription, - keep_open_on_confirm: bool, - documentation_aside: Option<(usize, DocumentationAside)>, - fixed_width: Option, -} - -#[derive(Copy, Clone, PartialEq, Eq)] -pub enum DocumentationSide { - Left, - Right, -} - -#[derive(Copy, Default, Clone, PartialEq, Eq)] -pub enum DocumentationEdge { - #[default] - Top, - Bottom, -} - -#[derive(Clone)] -pub struct DocumentationAside { - pub side: DocumentationSide, - pub edge: DocumentationEdge, - pub render: Rc AnyElement>, -} - -impl DocumentationAside { - pub fn new( - side: DocumentationSide, - edge: DocumentationEdge, - render: Rc AnyElement>, - ) -> Self { - Self { side, edge, render } - } -} - -impl Focusable for ContextMenu { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl EventEmitter for ContextMenu {} - -impl FluentBuilder for ContextMenu {} - -impl ContextMenu { - pub fn new( - window: &mut Window, - cx: &mut Context, - f: impl FnOnce(Self, &mut Window, &mut Context) -> Self, - ) -> Self { - let focus_handle = cx.focus_handle(); - let _on_blur_subscription = cx.on_blur( - &focus_handle, - window, - |this: &mut ContextMenu, window, cx| this.cancel(&menu::Cancel, window, cx), - ); - window.refresh(); - - f( - Self { - builder: None, - items: Default::default(), - focus_handle, - action_context: None, - selected_index: None, - delayed: false, - clicked: false, - key_context: "menu".into(), - _on_blur_subscription, - keep_open_on_confirm: false, - documentation_aside: None, - fixed_width: None, - end_slot_action: None, - }, - window, - cx, - ) - } - - pub fn build( - window: &mut Window, - cx: &mut App, - f: impl FnOnce(Self, &mut Window, &mut Context) -> Self, - ) -> Entity { - cx.new(|cx| Self::new(window, cx, f)) - } - - /// Builds a [`ContextMenu`] that will stay open when making changes instead of closing after each confirmation. - /// - /// The main difference from [`ContextMenu::build`] is the type of the `builder`, as we need to be able to hold onto - /// it to call it again. - pub fn build_persistent( - window: &mut Window, - cx: &mut App, - builder: impl Fn(Self, &mut Window, &mut Context) -> Self + 'static, - ) -> Entity { - cx.new(|cx| { - let builder = Rc::new(builder); - - let focus_handle = cx.focus_handle(); - let _on_blur_subscription = cx.on_blur( - &focus_handle, - window, - |this: &mut ContextMenu, window, cx| this.cancel(&menu::Cancel, window, cx), - ); - window.refresh(); - - (builder.clone())( - Self { - builder: Some(builder), - items: Default::default(), - focus_handle, - action_context: None, - selected_index: None, - delayed: false, - clicked: false, - key_context: "menu".into(), - _on_blur_subscription, - keep_open_on_confirm: true, - documentation_aside: None, - fixed_width: None, - end_slot_action: None, - }, - window, - cx, - ) - }) - } - - /// Rebuilds the menu. - /// - /// This is used to refresh the menu entries when entries are toggled when the menu is configured with - /// `keep_open_on_confirm = true`. - /// - /// This only works if the [`ContextMenu`] was constructed using [`ContextMenu::build_persistent`]. Otherwise it is - /// a no-op. - pub fn rebuild(&mut self, window: &mut Window, cx: &mut Context) { - let Some(builder) = self.builder.clone() else { - return; - }; - - // The way we rebuild the menu is a bit of a hack. - let focus_handle = cx.focus_handle(); - let new_menu = (builder.clone())( - Self { - builder: Some(builder), - items: Default::default(), - focus_handle: focus_handle.clone(), - action_context: None, - selected_index: None, - delayed: false, - clicked: false, - key_context: "menu".into(), - _on_blur_subscription: cx.on_blur( - &focus_handle, - window, - |this: &mut ContextMenu, window, cx| this.cancel(&menu::Cancel, window, cx), - ), - keep_open_on_confirm: false, - documentation_aside: None, - fixed_width: None, - end_slot_action: None, - }, - window, - cx, - ); - - self.items = new_menu.items; - - cx.notify(); - } - - pub fn context(mut self, focus: FocusHandle) -> Self { - self.action_context = Some(focus); - self - } - - pub fn header(mut self, title: impl Into) -> Self { - self.items.push(ContextMenuItem::Header(title.into())); - self - } - - pub fn header_with_link( - mut self, - title: impl Into, - link_label: impl Into, - link_url: impl Into, - ) -> Self { - self.items.push(ContextMenuItem::HeaderWithLink( - title.into(), - link_label.into(), - link_url.into(), - )); - self - } - - pub fn separator(mut self) -> Self { - self.items.push(ContextMenuItem::Separator); - self - } - - pub fn extend>(mut self, items: impl IntoIterator) -> Self { - self.items.extend(items.into_iter().map(Into::into)); - self - } - - pub fn item(mut self, item: impl Into) -> Self { - self.items.push(item.into()); - self - } - - pub fn push_item(&mut self, item: impl Into) { - self.items.push(item.into()); - } - - pub fn entry( - mut self, - label: impl Into, - action: Option>, - handler: impl Fn(&mut Window, &mut App) + 'static, - ) -> Self { - self.items.push(ContextMenuItem::Entry(ContextMenuEntry { - toggle: None, - label: label.into(), - handler: Rc::new(move |_, window, cx| handler(window, cx)), - icon: None, - custom_icon_path: None, - custom_icon_svg: None, - icon_position: IconPosition::End, - icon_size: IconSize::Small, - icon_color: None, - action, - disabled: false, - documentation_aside: None, - end_slot_icon: None, - end_slot_title: None, - end_slot_handler: None, - show_end_slot_on_hover: false, - })); - self - } - - pub fn entry_with_end_slot( - mut self, - label: impl Into, - action: Option>, - handler: impl Fn(&mut Window, &mut App) + 'static, - end_slot_icon: IconName, - end_slot_title: SharedString, - end_slot_handler: impl Fn(&mut Window, &mut App) + 'static, - ) -> Self { - self.items.push(ContextMenuItem::Entry(ContextMenuEntry { - toggle: None, - label: label.into(), - handler: Rc::new(move |_, window, cx| handler(window, cx)), - icon: None, - custom_icon_path: None, - custom_icon_svg: None, - icon_position: IconPosition::End, - icon_size: IconSize::Small, - icon_color: None, - action, - disabled: false, - documentation_aside: None, - end_slot_icon: Some(end_slot_icon), - end_slot_title: Some(end_slot_title), - end_slot_handler: Some(Rc::new(move |_, window, cx| end_slot_handler(window, cx))), - show_end_slot_on_hover: false, - })); - self - } - - pub fn entry_with_end_slot_on_hover( - mut self, - label: impl Into, - action: Option>, - handler: impl Fn(&mut Window, &mut App) + 'static, - end_slot_icon: IconName, - end_slot_title: SharedString, - end_slot_handler: impl Fn(&mut Window, &mut App) + 'static, - ) -> Self { - self.items.push(ContextMenuItem::Entry(ContextMenuEntry { - toggle: None, - label: label.into(), - handler: Rc::new(move |_, window, cx| handler(window, cx)), - icon: None, - custom_icon_path: None, - custom_icon_svg: None, - icon_position: IconPosition::End, - icon_size: IconSize::Small, - icon_color: None, - action, - disabled: false, - documentation_aside: None, - end_slot_icon: Some(end_slot_icon), - end_slot_title: Some(end_slot_title), - end_slot_handler: Some(Rc::new(move |_, window, cx| end_slot_handler(window, cx))), - show_end_slot_on_hover: true, - })); - self - } - - pub fn toggleable_entry( - mut self, - label: impl Into, - toggled: bool, - position: IconPosition, - action: Option>, - handler: impl Fn(&mut Window, &mut App) + 'static, - ) -> Self { - self.items.push(ContextMenuItem::Entry(ContextMenuEntry { - toggle: Some((position, toggled)), - label: label.into(), - handler: Rc::new(move |_, window, cx| handler(window, cx)), - icon: None, - custom_icon_path: None, - custom_icon_svg: None, - icon_position: position, - icon_size: IconSize::Small, - icon_color: None, - action, - disabled: false, - documentation_aside: None, - end_slot_icon: None, - end_slot_title: None, - end_slot_handler: None, - show_end_slot_on_hover: false, - })); - self - } - - pub fn custom_row( - mut self, - entry_render: impl Fn(&mut Window, &mut App) -> AnyElement + 'static, - ) -> Self { - self.items.push(ContextMenuItem::CustomEntry { - entry_render: Box::new(entry_render), - handler: Rc::new(|_, _, _| {}), - selectable: false, - documentation_aside: None, - }); - self - } - - pub fn custom_entry( - mut self, - entry_render: impl Fn(&mut Window, &mut App) -> AnyElement + 'static, - handler: impl Fn(&mut Window, &mut App) + 'static, - ) -> Self { - self.items.push(ContextMenuItem::CustomEntry { - entry_render: Box::new(entry_render), - handler: Rc::new(move |_, window, cx| handler(window, cx)), - selectable: true, - documentation_aside: None, - }); - self - } - - pub fn label(mut self, label: impl Into) -> Self { - self.items.push(ContextMenuItem::Label(label.into())); - self - } - - pub fn action(self, label: impl Into, action: Box) -> Self { - self.action_checked(label, action, false) - } - - pub fn action_checked( - mut self, - label: impl Into, - action: Box, - checked: bool, - ) -> Self { - self.items.push(ContextMenuItem::Entry(ContextMenuEntry { - toggle: if checked { - Some((IconPosition::Start, true)) - } else { - None - }, - label: label.into(), - action: Some(action.boxed_clone()), - handler: Rc::new(move |context, window, cx| { - if let Some(context) = &context { - window.focus(context); - } - window.dispatch_action(action.boxed_clone(), cx); - }), - icon: None, - custom_icon_path: None, - custom_icon_svg: None, - icon_position: IconPosition::End, - icon_size: IconSize::Small, - icon_color: None, - disabled: false, - documentation_aside: None, - end_slot_icon: None, - end_slot_title: None, - end_slot_handler: None, - show_end_slot_on_hover: false, - })); - self - } - - pub fn action_disabled_when( - mut self, - disabled: bool, - label: impl Into, - action: Box, - ) -> Self { - self.items.push(ContextMenuItem::Entry(ContextMenuEntry { - toggle: None, - label: label.into(), - action: Some(action.boxed_clone()), - handler: Rc::new(move |context, window, cx| { - if let Some(context) = &context { - window.focus(context); - } - window.dispatch_action(action.boxed_clone(), cx); - }), - icon: None, - custom_icon_path: None, - custom_icon_svg: None, - icon_size: IconSize::Small, - icon_position: IconPosition::End, - icon_color: None, - disabled, - documentation_aside: None, - end_slot_icon: None, - end_slot_title: None, - end_slot_handler: None, - show_end_slot_on_hover: false, - })); - self - } - - pub fn link(mut self, label: impl Into, action: Box) -> Self { - self.items.push(ContextMenuItem::Entry(ContextMenuEntry { - toggle: None, - label: label.into(), - action: Some(action.boxed_clone()), - handler: Rc::new(move |_, window, cx| window.dispatch_action(action.boxed_clone(), cx)), - icon: Some(IconName::ArrowUpRight), - custom_icon_path: None, - custom_icon_svg: None, - icon_size: IconSize::XSmall, - icon_position: IconPosition::End, - icon_color: None, - disabled: false, - documentation_aside: None, - end_slot_icon: None, - end_slot_title: None, - end_slot_handler: None, - show_end_slot_on_hover: false, - })); - self - } - - pub fn keep_open_on_confirm(mut self, keep_open: bool) -> Self { - self.keep_open_on_confirm = keep_open; - self - } - - pub fn trigger_end_slot_handler(&mut self, window: &mut Window, cx: &mut Context) { - let Some(entry) = self.selected_index.and_then(|ix| self.items.get(ix)) else { - return; - }; - let ContextMenuItem::Entry(entry) = entry else { - return; - }; - let Some(handler) = entry.end_slot_handler.as_ref() else { - return; - }; - handler(None, window, cx); - } - - pub fn fixed_width(mut self, width: DefiniteLength) -> Self { - self.fixed_width = Some(width); - self - } - - pub fn end_slot_action(mut self, action: Box) -> Self { - self.end_slot_action = Some(action); - self - } - - pub fn key_context(mut self, context: impl Into) -> Self { - self.key_context = context.into(); - self - } - - pub fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context) { - let context = self.action_context.as_ref(); - if let Some( - ContextMenuItem::Entry(ContextMenuEntry { - handler, - disabled: false, - .. - }) - | ContextMenuItem::CustomEntry { handler, .. }, - ) = self.selected_index.and_then(|ix| self.items.get(ix)) - { - (handler)(context, window, cx) - } - - if self.keep_open_on_confirm { - self.rebuild(window, cx); - } else { - cx.emit(DismissEvent); - } - } - - pub fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context) { - cx.emit(DismissEvent); - cx.emit(DismissEvent); - } - - pub fn end_slot(&mut self, _: &dyn Action, window: &mut Window, cx: &mut Context) { - let Some(item) = self.selected_index.and_then(|ix| self.items.get(ix)) else { - return; - }; - let ContextMenuItem::Entry(entry) = item else { - return; - }; - let Some(handler) = entry.end_slot_handler.as_ref() else { - return; - }; - handler(None, window, cx); - self.rebuild(window, cx); - cx.notify(); - } - - pub fn clear_selected(&mut self) { - self.selected_index = None; - } - - pub fn select_first(&mut self, _: &SelectFirst, window: &mut Window, cx: &mut Context) { - if let Some(ix) = self.items.iter().position(|item| item.is_selectable()) { - self.select_index(ix, window, cx); - } - cx.notify(); - } - - pub fn select_last(&mut self, window: &mut Window, cx: &mut Context) -> Option { - for (ix, item) in self.items.iter().enumerate().rev() { - if item.is_selectable() { - return self.select_index(ix, window, cx); - } - } - None - } - - fn handle_select_last(&mut self, _: &SelectLast, window: &mut Window, cx: &mut Context) { - if self.select_last(window, cx).is_some() { - cx.notify(); - } - } - - pub fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context) { - if let Some(ix) = self.selected_index { - let next_index = ix + 1; - if self.items.len() <= next_index { - self.select_first(&SelectFirst, window, cx); - return; - } else { - for (ix, item) in self.items.iter().enumerate().skip(next_index) { - if item.is_selectable() { - self.select_index(ix, window, cx); - cx.notify(); - return; - } - } - } - } - self.select_first(&SelectFirst, window, cx); - } - - pub fn select_previous( - &mut self, - _: &SelectPrevious, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(ix) = self.selected_index { - for (ix, item) in self.items.iter().enumerate().take(ix).rev() { - if item.is_selectable() { - self.select_index(ix, window, cx); - cx.notify(); - return; - } - } - } - self.handle_select_last(&SelectLast, window, cx); - } - - fn select_index( - &mut self, - ix: usize, - _window: &mut Window, - _cx: &mut Context, - ) -> Option { - self.documentation_aside = None; - let item = self.items.get(ix)?; - if item.is_selectable() { - self.selected_index = Some(ix); - match item { - ContextMenuItem::Entry(entry) => { - if let Some(callback) = &entry.documentation_aside { - self.documentation_aside = Some((ix, callback.clone())); - } - } - ContextMenuItem::CustomEntry { - documentation_aside: Some(callback), - .. - } => { - self.documentation_aside = Some((ix, callback.clone())); - } - _ => (), - } - } - Some(ix) - } - - pub fn on_action_dispatch( - &mut self, - dispatched: &dyn Action, - window: &mut Window, - cx: &mut Context, - ) { - if self.clicked { - cx.propagate(); - return; - } - - if let Some(ix) = self.items.iter().position(|item| { - if let ContextMenuItem::Entry(ContextMenuEntry { - action: Some(action), - disabled: false, - .. - }) = item - { - action.partial_eq(dispatched) - } else { - false - } - }) { - self.select_index(ix, window, cx); - self.delayed = true; - cx.notify(); - let action = dispatched.boxed_clone(); - cx.spawn_in(window, async move |this, cx| { - cx.background_executor() - .timer(Duration::from_millis(50)) - .await; - cx.update(|window, cx| { - this.update(cx, |this, cx| { - this.cancel(&menu::Cancel, window, cx); - window.dispatch_action(action, cx); - }) - }) - }) - .detach_and_log_err(cx); - } else { - cx.propagate() - } - } - - pub fn on_blur_subscription(mut self, new_subscription: Subscription) -> Self { - self._on_blur_subscription = new_subscription; - self - } - - fn render_menu_item( - &self, - ix: usize, - item: &ContextMenuItem, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement + use<> { - match item { - ContextMenuItem::Separator => ListSeparator.into_any_element(), - ContextMenuItem::Header(header) => ListSubHeader::new(header.clone()) - .inset(true) - .into_any_element(), - ContextMenuItem::HeaderWithLink(header, label, url) => { - let url = url.clone(); - let link_id = ElementId::Name(format!("link-{}", url).into()); - ListSubHeader::new(header.clone()) - .inset(true) - .end_slot( - Button::new(link_id, label.clone()) - .color(Color::Muted) - .label_size(LabelSize::Small) - .size(ButtonSize::None) - .style(ButtonStyle::Transparent) - .on_click(move |_, _, cx| { - let url = url.clone(); - cx.open_url(&url); - }) - .into_any_element(), - ) - .into_any_element() - } - ContextMenuItem::Label(label) => ListItem::new(ix) - .inset(true) - .disabled(true) - .child(Label::new(label.clone())) - .into_any_element(), - ContextMenuItem::Entry(entry) => { - self.render_menu_entry(ix, entry, cx).into_any_element() - } - ContextMenuItem::CustomEntry { - entry_render, - handler, - selectable, - .. - } => { - let handler = handler.clone(); - let menu = cx.entity().downgrade(); - let selectable = *selectable; - ListItem::new(ix) - .inset(true) - .toggle_state(if selectable { - Some(ix) == self.selected_index - } else { - false - }) - .selectable(selectable) - .when(selectable, |item| { - item.on_click({ - let context = self.action_context.clone(); - let keep_open_on_confirm = self.keep_open_on_confirm; - move |_, window, cx| { - handler(context.as_ref(), window, cx); - menu.update(cx, |menu, cx| { - menu.clicked = true; - - if keep_open_on_confirm { - menu.rebuild(window, cx); - } else { - cx.emit(DismissEvent); - } - }) - .ok(); - } - }) - }) - .child(entry_render(window, cx)) - .into_any_element() - } - } - } - - fn render_menu_entry( - &self, - ix: usize, - entry: &ContextMenuEntry, - cx: &mut Context, - ) -> impl IntoElement { - let ContextMenuEntry { - toggle, - label, - handler, - icon, - custom_icon_path, - custom_icon_svg, - icon_position, - icon_size, - icon_color, - action, - disabled, - documentation_aside, - end_slot_icon, - end_slot_title, - end_slot_handler, - show_end_slot_on_hover, - } = entry; - let this = cx.weak_entity(); - - let handler = handler.clone(); - let menu = cx.entity().downgrade(); - - let icon_color = if *disabled { - Color::Muted - } else if toggle.is_some() { - icon_color.unwrap_or(Color::Accent) - } else { - icon_color.unwrap_or(Color::Default) - }; - - let label_color = if *disabled { - Color::Disabled - } else { - Color::Default - }; - - let label_element = if let Some(custom_path) = custom_icon_path { - h_flex() - .gap_1p5() - .when( - *icon_position == IconPosition::Start && toggle.is_none(), - |flex| { - flex.child( - Icon::from_path(custom_path.clone()) - .size(*icon_size) - .color(icon_color), - ) - }, - ) - .child(Label::new(label.clone()).color(label_color).truncate()) - .when(*icon_position == IconPosition::End, |flex| { - flex.child( - Icon::from_path(custom_path.clone()) - .size(*icon_size) - .color(icon_color), - ) - }) - .into_any_element() - } else if let Some(custom_icon_svg) = custom_icon_svg { - h_flex() - .gap_1p5() - .when( - *icon_position == IconPosition::Start && toggle.is_none(), - |flex| { - flex.child( - Icon::from_external_svg(custom_icon_svg.clone()) - .size(*icon_size) - .color(icon_color), - ) - }, - ) - .child(Label::new(label.clone()).color(label_color).truncate()) - .when(*icon_position == IconPosition::End, |flex| { - flex.child( - Icon::from_external_svg(custom_icon_svg.clone()) - .size(*icon_size) - .color(icon_color), - ) - }) - .into_any_element() - } else if let Some(icon_name) = icon { - h_flex() - .gap_1p5() - .when( - *icon_position == IconPosition::Start && toggle.is_none(), - |flex| flex.child(Icon::new(*icon_name).size(*icon_size).color(icon_color)), - ) - .child(Label::new(label.clone()).color(label_color).truncate()) - .when(*icon_position == IconPosition::End, |flex| { - flex.child(Icon::new(*icon_name).size(*icon_size).color(icon_color)) - }) - .into_any_element() - } else { - Label::new(label.clone()) - .color(label_color) - .truncate() - .into_any_element() - }; - - div() - .id(("context-menu-child", ix)) - .when_some(documentation_aside.clone(), |this, documentation_aside| { - this.occlude() - .on_hover(cx.listener(move |menu, hovered, _, cx| { - if *hovered { - menu.documentation_aside = Some((ix, documentation_aside.clone())); - } else if matches!(menu.documentation_aside, Some((id, _)) if id == ix) { - menu.documentation_aside = None; - } - cx.notify(); - })) - }) - .child( - ListItem::new(ix) - .group_name("label_container") - .inset(true) - .disabled(*disabled) - .toggle_state(Some(ix) == self.selected_index) - .when_some(*toggle, |list_item, (position, toggled)| { - let contents = div() - .flex_none() - .child( - Icon::new(icon.unwrap_or(IconName::Check)) - .color(icon_color) - .size(*icon_size), - ) - .when(!toggled, |contents| contents.invisible()); - - match position { - IconPosition::Start => list_item.start_slot(contents), - IconPosition::End => list_item.end_slot(contents), - } - }) - .child( - h_flex() - .w_full() - .justify_between() - .child(label_element) - .debug_selector(|| format!("MENU_ITEM-{}", label)) - .children(action.as_ref().map(|action| { - let binding = self - .action_context - .as_ref() - .map(|focus| KeyBinding::for_action_in(&**action, focus, cx)) - .unwrap_or_else(|| KeyBinding::for_action(&**action, cx)); - - div() - .ml_4() - .child(binding.disabled(*disabled)) - .when(*disabled && documentation_aside.is_some(), |parent| { - parent.invisible() - }) - })) - .when(*disabled && documentation_aside.is_some(), |parent| { - parent.child( - Icon::new(IconName::Info) - .size(IconSize::XSmall) - .color(Color::Muted), - ) - }), - ) - .when_some( - end_slot_icon - .as_ref() - .zip(self.end_slot_action.as_ref()) - .zip(end_slot_title.as_ref()) - .zip(end_slot_handler.as_ref()), - |el, (((icon, action), title), handler)| { - el.end_slot({ - let icon_button = IconButton::new("end-slot-icon", *icon) - .shape(IconButtonShape::Square) - .tooltip({ - let action_context = self.action_context.clone(); - let title = title.clone(); - let action = action.boxed_clone(); - move |_window, cx| { - action_context - .as_ref() - .map(|focus| { - Tooltip::for_action_in( - title.clone(), - &*action, - focus, - cx, - ) - }) - .unwrap_or_else(|| { - Tooltip::for_action(title.clone(), &*action, cx) - }) - } - }) - .on_click({ - let handler = handler.clone(); - move |_, window, cx| { - handler(None, window, cx); - this.update(cx, |this, cx| { - this.rebuild(window, cx); - cx.notify(); - }) - .ok(); - } - }); - - if *show_end_slot_on_hover { - div() - .visible_on_hover("label_container") - .child(icon_button) - .into_any_element() - } else { - icon_button.into_any_element() - } - }) - }, - ) - .on_click({ - let context = self.action_context.clone(); - let keep_open_on_confirm = self.keep_open_on_confirm; - move |_, window, cx| { - handler(context.as_ref(), window, cx); - menu.update(cx, |menu, cx| { - menu.clicked = true; - if keep_open_on_confirm { - menu.rebuild(window, cx); - } else { - cx.emit(DismissEvent); - } - }) - .ok(); - } - }), - ) - .into_any_element() - } -} - -impl ContextMenuItem { - fn is_selectable(&self) -> bool { - match self { - ContextMenuItem::Header(_) - | ContextMenuItem::HeaderWithLink(_, _, _) - | ContextMenuItem::Separator - | ContextMenuItem::Label { .. } => false, - ContextMenuItem::Entry(ContextMenuEntry { disabled, .. }) => !disabled, - ContextMenuItem::CustomEntry { selectable, .. } => *selectable, - } - } -} - -impl Render for ContextMenu { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx); - let window_size = window.viewport_size(); - let rem_size = window.rem_size(); - let is_wide_window = window_size.width / rem_size > rems_from_px(800.).0; - - let aside = self.documentation_aside.clone(); - let render_aside = |aside: DocumentationAside, cx: &mut Context| { - WithRemSize::new(ui_font_size) - .occlude() - .elevation_2(cx) - .w_full() - .p_2() - .overflow_hidden() - .when(is_wide_window, |this| this.max_w_96()) - .when(!is_wide_window, |this| this.max_w_48()) - .child((aside.render)(cx)) - }; - - let render_menu = - |cx: &mut Context, window: &mut Window| { - WithRemSize::new(ui_font_size) - .occlude() - .elevation_2(cx) - .flex() - .flex_row() - .flex_shrink_0() - .child( - v_flex() - .id("context-menu") - .max_h(vh(0.75, window)) - .flex_shrink_0() - .when_some(self.fixed_width, |this, width| { - this.w(width).overflow_x_hidden() - }) - .when(self.fixed_width.is_none(), |this| { - this.min_w(px(200.)).flex_1() - }) - .overflow_y_scroll() - .track_focus(&self.focus_handle(cx)) - .on_mouse_down_out(cx.listener(|this, _, window, cx| { - this.cancel(&menu::Cancel, window, cx) - })) - .key_context(self.key_context.as_ref()) - .on_action(cx.listener(ContextMenu::select_first)) - .on_action(cx.listener(ContextMenu::handle_select_last)) - .on_action(cx.listener(ContextMenu::select_next)) - .on_action(cx.listener(ContextMenu::select_previous)) - .on_action(cx.listener(ContextMenu::confirm)) - .on_action(cx.listener(ContextMenu::cancel)) - .when_some(self.end_slot_action.as_ref(), |el, action| { - el.on_boxed_action(&**action, cx.listener(ContextMenu::end_slot)) - }) - .when(!self.delayed, |mut el| { - for item in self.items.iter() { - if let ContextMenuItem::Entry(ContextMenuEntry { - action: Some(action), - disabled: false, - .. - }) = item - { - el = el.on_boxed_action( - &**action, - cx.listener(ContextMenu::on_action_dispatch), - ); - } - } - el - }) - .child( - List::new().children( - self.items.iter().enumerate().map(|(ix, item)| { - self.render_menu_item(ix, item, window, cx) - }), - ), - ), - ) - }; - - if is_wide_window { - div() - .relative() - .child(render_menu(cx, window)) - .children(aside.map(|(_item_index, aside)| { - h_flex() - .absolute() - .when(aside.side == DocumentationSide::Left, |this| { - this.right_full().mr_1() - }) - .when(aside.side == DocumentationSide::Right, |this| { - this.left_full().ml_1() - }) - .when(aside.edge == DocumentationEdge::Top, |this| this.top_0()) - .when(aside.edge == DocumentationEdge::Bottom, |this| { - this.bottom_0() - }) - .child(render_aside(aside, cx)) - })) - } else { - v_flex() - .w_full() - .gap_1() - .justify_end() - .children(aside.map(|(_, aside)| render_aside(aside, cx))) - .child(render_menu(cx, window)) - } - } -} - -#[cfg(test)] -mod tests { - use gpui::TestAppContext; - - use super::*; - - #[gpui::test] - fn can_navigate_back_over_headers(cx: &mut TestAppContext) { - let cx = cx.add_empty_window(); - let context_menu = cx.update(|window, cx| { - ContextMenu::build(window, cx, |menu, _, _| { - menu.header("First header") - .separator() - .entry("First entry", None, |_, _| {}) - .separator() - .separator() - .entry("Last entry", None, |_, _| {}) - .header("Last header") - }) - }); - - context_menu.update_in(cx, |context_menu, window, cx| { - assert_eq!( - None, context_menu.selected_index, - "No selection is in the menu initially" - ); - - context_menu.select_first(&SelectFirst, window, cx); - assert_eq!( - Some(2), - context_menu.selected_index, - "Should select first selectable entry, skipping the header and the separator" - ); - - context_menu.select_next(&SelectNext, window, cx); - assert_eq!( - Some(5), - context_menu.selected_index, - "Should select next selectable entry, skipping 2 separators along the way" - ); - - context_menu.select_next(&SelectNext, window, cx); - assert_eq!( - Some(2), - context_menu.selected_index, - "Should wrap around to first selectable entry" - ); - }); - - context_menu.update_in(cx, |context_menu, window, cx| { - assert_eq!( - Some(2), - context_menu.selected_index, - "Should start from the first selectable entry" - ); - - context_menu.select_previous(&SelectPrevious, window, cx); - assert_eq!( - Some(5), - context_menu.selected_index, - "Should wrap around to previous selectable entry (last)" - ); - - context_menu.select_previous(&SelectPrevious, window, cx); - assert_eq!( - Some(2), - context_menu.selected_index, - "Should go back to previous selectable entry (first)" - ); - }); - - context_menu.update_in(cx, |context_menu, window, cx| { - context_menu.select_first(&SelectFirst, window, cx); - assert_eq!( - Some(2), - context_menu.selected_index, - "Should start from the first selectable entry" - ); - - context_menu.select_previous(&SelectPrevious, window, cx); - assert_eq!( - Some(5), - context_menu.selected_index, - "Should wrap around to last selectable entry" - ); - context_menu.select_next(&SelectNext, window, cx); - assert_eq!( - Some(2), - context_menu.selected_index, - "Should wrap around to first selectable entry" - ); - }); - } -} diff --git a/crates/ui/src/components/data_table.rs b/crates/ui/src/components/data_table.rs deleted file mode 100644 index 9cd2a5cb7a..0000000000 --- a/crates/ui/src/components/data_table.rs +++ /dev/null @@ -1,1390 +0,0 @@ -use std::{ops::Range, rc::Rc}; - -use gpui::{ - AbsoluteLength, AppContext, Context, DefiniteLength, DragMoveEvent, Entity, EntityId, - FocusHandle, Length, ListHorizontalSizingBehavior, ListSizingBehavior, Point, Stateful, - UniformListScrollHandle, WeakEntity, transparent_black, uniform_list, -}; - -use crate::{ - ActiveTheme as _, AnyElement, App, Button, ButtonCommon as _, ButtonStyle, Color, Component, - ComponentScope, Div, ElementId, FixedWidth as _, FluentBuilder as _, Indicator, - InteractiveElement, IntoElement, ParentElement, Pixels, RegisterComponent, RenderOnce, - ScrollableHandle, Scrollbars, SharedString, StatefulInteractiveElement, Styled, StyledExt as _, - StyledTypography, Window, WithScrollbar, div, example_group_with_title, h_flex, px, - single_example, v_flex, -}; -use itertools::intersperse_with; - -const RESIZE_COLUMN_WIDTH: f32 = 8.0; - -#[derive(Debug)] -struct DraggedColumn(usize); - -struct UniformListData { - render_item_fn: Box, &mut Window, &mut App) -> Vec<[AnyElement; COLS]>>, - element_id: ElementId, - row_count: usize, -} - -enum TableContents { - Vec(Vec<[AnyElement; COLS]>), - UniformList(UniformListData), -} - -impl TableContents { - fn rows_mut(&mut self) -> Option<&mut Vec<[AnyElement; COLS]>> { - match self { - TableContents::Vec(rows) => Some(rows), - TableContents::UniformList(_) => None, - } - } - - fn len(&self) -> usize { - match self { - TableContents::Vec(rows) => rows.len(), - TableContents::UniformList(data) => data.row_count, - } - } - - fn is_empty(&self) -> bool { - self.len() == 0 - } -} - -pub struct TableInteractionState { - pub focus_handle: FocusHandle, - pub scroll_handle: UniformListScrollHandle, - pub custom_scrollbar: Option, -} - -impl TableInteractionState { - pub fn new(cx: &mut App) -> Self { - Self { - focus_handle: cx.focus_handle(), - scroll_handle: UniformListScrollHandle::new(), - custom_scrollbar: None, - } - } - - pub fn with_custom_scrollbar(mut self, custom_scrollbar: Scrollbars) -> Self { - self.custom_scrollbar = Some(custom_scrollbar); - self - } - - pub fn scroll_offset(&self) -> Point { - self.scroll_handle.offset() - } - - pub fn set_scroll_offset(&self, offset: Point) { - self.scroll_handle.set_offset(offset); - } - - pub fn listener( - this: &Entity, - f: impl Fn(&mut Self, &E, &mut Window, &mut Context) + 'static, - ) -> impl Fn(&E, &mut Window, &mut App) + 'static { - let view = this.downgrade(); - move |e: &E, window: &mut Window, cx: &mut App| { - view.update(cx, |view, cx| f(view, e, window, cx)).ok(); - } - } - - fn render_resize_handles( - &self, - column_widths: &[Length; COLS], - resizable_columns: &[TableResizeBehavior; COLS], - initial_sizes: [DefiniteLength; COLS], - columns: Option>>, - window: &mut Window, - cx: &mut App, - ) -> AnyElement { - let spacers = column_widths - .iter() - .map(|width| base_cell_style(Some(*width)).into_any_element()); - - let mut column_ix = 0; - let resizable_columns_slice = *resizable_columns; - let mut resizable_columns = resizable_columns.iter(); - - let dividers = intersperse_with(spacers, || { - window.with_id(column_ix, |window| { - let mut resize_divider = div() - // This is required because this is evaluated at a different time than the use_state call above - .id(column_ix) - .relative() - .top_0() - .w_px() - .h_full() - .bg(cx.theme().colors().border.opacity(0.8)); - - let mut resize_handle = div() - .id("column-resize-handle") - .absolute() - .left_neg_0p5() - .w(px(RESIZE_COLUMN_WIDTH)) - .h_full(); - - if resizable_columns - .next() - .is_some_and(TableResizeBehavior::is_resizable) - { - let hovered = window.use_state(cx, |_window, _cx| false); - - resize_divider = resize_divider.when(*hovered.read(cx), |div| { - div.bg(cx.theme().colors().border_focused) - }); - - resize_handle = resize_handle - .on_hover(move |&was_hovered, _, cx| hovered.write(cx, was_hovered)) - .cursor_col_resize() - .when_some(columns.clone(), |this, columns| { - this.on_click(move |event, window, cx| { - if event.click_count() >= 2 { - columns.update(cx, |columns, _| { - columns.on_double_click( - column_ix, - &initial_sizes, - &resizable_columns_slice, - window, - ); - }) - } - - cx.stop_propagation(); - }) - }) - .on_drag(DraggedColumn(column_ix), |_, _offset, _window, cx| { - cx.new(|_cx| gpui::Empty) - }) - } - - column_ix += 1; - resize_divider.child(resize_handle).into_any_element() - }) - }); - - h_flex() - .id("resize-handles") - .absolute() - .inset_0() - .w_full() - .children(dividers) - .into_any_element() - } -} - -#[derive(Debug, Copy, Clone, PartialEq)] -pub enum TableResizeBehavior { - None, - Resizable, - MinSize(f32), -} - -impl TableResizeBehavior { - pub fn is_resizable(&self) -> bool { - *self != TableResizeBehavior::None - } - - pub fn min_size(&self) -> Option { - match self { - TableResizeBehavior::None => None, - TableResizeBehavior::Resizable => Some(0.05), - TableResizeBehavior::MinSize(min_size) => Some(*min_size), - } - } -} - -pub struct TableColumnWidths { - widths: [DefiniteLength; COLS], - visible_widths: [DefiniteLength; COLS], - cached_bounds_width: Pixels, - initialized: bool, -} - -impl TableColumnWidths { - pub fn new(_: &mut App) -> Self { - Self { - widths: [DefiniteLength::default(); COLS], - visible_widths: [DefiniteLength::default(); COLS], - cached_bounds_width: Default::default(), - initialized: false, - } - } - - fn get_fraction(length: &DefiniteLength, bounds_width: Pixels, rem_size: Pixels) -> f32 { - match length { - DefiniteLength::Absolute(AbsoluteLength::Pixels(pixels)) => *pixels / bounds_width, - DefiniteLength::Absolute(AbsoluteLength::Rems(rems_width)) => { - rems_width.to_pixels(rem_size) / bounds_width - } - DefiniteLength::Fraction(fraction) => *fraction, - } - } - - fn on_double_click( - &mut self, - double_click_position: usize, - initial_sizes: &[DefiniteLength; COLS], - resize_behavior: &[TableResizeBehavior; COLS], - window: &mut Window, - ) { - let bounds_width = self.cached_bounds_width; - let rem_size = window.rem_size(); - let initial_sizes = - initial_sizes.map(|length| Self::get_fraction(&length, bounds_width, rem_size)); - let widths = self - .widths - .map(|length| Self::get_fraction(&length, bounds_width, rem_size)); - - let updated_widths = Self::reset_to_initial_size( - double_click_position, - widths, - initial_sizes, - resize_behavior, - ); - self.widths = updated_widths.map(DefiniteLength::Fraction); - self.visible_widths = self.widths; - } - - fn reset_to_initial_size( - col_idx: usize, - mut widths: [f32; COLS], - initial_sizes: [f32; COLS], - resize_behavior: &[TableResizeBehavior; COLS], - ) -> [f32; COLS] { - // RESET: - // Part 1: - // Figure out if we should shrink/grow the selected column - // Get diff which represents the change in column we want to make initial size delta curr_size = diff - // - // Part 2: We need to decide which side column we should move and where - // - // If we want to grow our column we should check the left/right columns diff to see what side - // has a greater delta than their initial size. Likewise, if we shrink our column we should check - // the left/right column diffs to see what side has the smallest delta. - // - // Part 3: resize - // - // col_idx represents the column handle to the right of an active column - // - // If growing and right has the greater delta { - // shift col_idx to the right - // } else if growing and left has the greater delta { - // shift col_idx - 1 to the left - // } else if shrinking and the right has the greater delta { - // shift - // } { - // - // } - // } - // - // if we need to shrink, then if the right - // - - // DRAGGING - // we get diff which represents the change in the _drag handle_ position - // -diff => dragging left -> - // grow the column to the right of the handle as much as we can shrink columns to the left of the handle - // +diff => dragging right -> growing handles column - // grow the column to the left of the handle as much as we can shrink columns to the right of the handle - // - - let diff = initial_sizes[col_idx] - widths[col_idx]; - - let left_diff = - initial_sizes[..col_idx].iter().sum::() - widths[..col_idx].iter().sum::(); - let right_diff = initial_sizes[col_idx + 1..].iter().sum::() - - widths[col_idx + 1..].iter().sum::(); - - let go_left_first = if diff < 0.0 { - left_diff > right_diff - } else { - left_diff < right_diff - }; - - if !go_left_first { - let diff_remaining = - Self::propagate_resize_diff(diff, col_idx, &mut widths, resize_behavior, 1); - - if diff_remaining != 0.0 && col_idx > 0 { - Self::propagate_resize_diff( - diff_remaining, - col_idx, - &mut widths, - resize_behavior, - -1, - ); - } - } else { - let diff_remaining = - Self::propagate_resize_diff(diff, col_idx, &mut widths, resize_behavior, -1); - - if diff_remaining != 0.0 { - Self::propagate_resize_diff( - diff_remaining, - col_idx, - &mut widths, - resize_behavior, - 1, - ); - } - } - - widths - } - - fn on_drag_move( - &mut self, - drag_event: &DragMoveEvent, - resize_behavior: &[TableResizeBehavior; COLS], - window: &mut Window, - cx: &mut Context, - ) { - let drag_position = drag_event.event.position; - let bounds = drag_event.bounds; - - let mut col_position = 0.0; - let rem_size = window.rem_size(); - let bounds_width = bounds.right() - bounds.left(); - let col_idx = drag_event.drag(cx).0; - - let column_handle_width = Self::get_fraction( - &DefiniteLength::Absolute(AbsoluteLength::Pixels(px(RESIZE_COLUMN_WIDTH))), - bounds_width, - rem_size, - ); - - let mut widths = self - .widths - .map(|length| Self::get_fraction(&length, bounds_width, rem_size)); - - for length in widths[0..=col_idx].iter() { - col_position += length + column_handle_width; - } - - let mut total_length_ratio = col_position; - for length in widths[col_idx + 1..].iter() { - total_length_ratio += length; - } - total_length_ratio += (COLS - 1 - col_idx) as f32 * column_handle_width; - - let drag_fraction = (drag_position.x - bounds.left()) / bounds_width; - let drag_fraction = drag_fraction * total_length_ratio; - let diff = drag_fraction - col_position - column_handle_width / 2.0; - - Self::drag_column_handle(diff, col_idx, &mut widths, resize_behavior); - - self.visible_widths = widths.map(DefiniteLength::Fraction); - } - - fn drag_column_handle( - diff: f32, - col_idx: usize, - widths: &mut [f32; COLS], - resize_behavior: &[TableResizeBehavior; COLS], - ) { - // if diff > 0.0 then go right - if diff > 0.0 { - Self::propagate_resize_diff(diff, col_idx, widths, resize_behavior, 1); - } else { - Self::propagate_resize_diff(-diff, col_idx + 1, widths, resize_behavior, -1); - } - } - - fn propagate_resize_diff( - diff: f32, - col_idx: usize, - widths: &mut [f32; COLS], - resize_behavior: &[TableResizeBehavior; COLS], - direction: i8, - ) -> f32 { - let mut diff_remaining = diff; - if resize_behavior[col_idx].min_size().is_none() { - return diff; - } - - let step_right; - let step_left; - if direction < 0 { - step_right = 0; - step_left = 1; - } else { - step_right = 1; - step_left = 0; - } - if col_idx == 0 && direction < 0 { - return diff; - } - let mut curr_column = col_idx + step_right - step_left; - - while diff_remaining != 0.0 && curr_column < COLS { - let Some(min_size) = resize_behavior[curr_column].min_size() else { - if curr_column == 0 { - break; - } - curr_column -= step_left; - curr_column += step_right; - continue; - }; - - let curr_width = widths[curr_column] - diff_remaining; - widths[curr_column] = curr_width; - - if min_size > curr_width { - diff_remaining = min_size - curr_width; - widths[curr_column] = min_size; - } else { - diff_remaining = 0.0; - break; - } - if curr_column == 0 { - break; - } - curr_column -= step_left; - curr_column += step_right; - } - widths[col_idx] = widths[col_idx] + (diff - diff_remaining); - - diff_remaining - } -} - -pub struct TableWidths { - initial: [DefiniteLength; COLS], - current: Option>>, - resizable: [TableResizeBehavior; COLS], -} - -impl TableWidths { - pub fn new(widths: [impl Into; COLS]) -> Self { - let widths = widths.map(Into::into); - - TableWidths { - initial: widths, - current: None, - resizable: [TableResizeBehavior::None; COLS], - } - } - - fn lengths(&self, cx: &App) -> [Length; COLS] { - self.current - .as_ref() - .map(|entity| entity.read(cx).visible_widths.map(Length::Definite)) - .unwrap_or(self.initial.map(Length::Definite)) - } -} - -/// A table component -#[derive(RegisterComponent, IntoElement)] -pub struct Table { - striped: bool, - width: Option, - headers: Option<[AnyElement; COLS]>, - rows: TableContents, - interaction_state: Option>, - col_widths: Option>, - map_row: Option), &mut Window, &mut App) -> AnyElement>>, - use_ui_font: bool, - empty_table_callback: Option AnyElement>>, -} - -impl Table { - /// number of headers provided. - pub fn new() -> Self { - Self { - striped: false, - width: None, - headers: None, - rows: TableContents::Vec(Vec::new()), - interaction_state: None, - map_row: None, - use_ui_font: true, - empty_table_callback: None, - col_widths: None, - } - } - - /// Enables uniform list rendering. - /// The provided function will be passed directly to the `uniform_list` element. - /// Therefore, if this method is called, any calls to [`Table::row`] before or after - /// this method is called will be ignored. - pub fn uniform_list( - mut self, - id: impl Into, - row_count: usize, - render_item_fn: impl Fn(Range, &mut Window, &mut App) -> Vec<[AnyElement; COLS]> - + 'static, - ) -> Self { - self.rows = TableContents::UniformList(UniformListData { - element_id: id.into(), - row_count, - render_item_fn: Box::new(render_item_fn), - }); - self - } - - /// Enables row striping. - pub fn striped(mut self) -> Self { - self.striped = true; - self - } - - /// Sets the width of the table. - /// Will enable horizontal scrolling if [`Self::interactable`] is also called. - pub fn width(mut self, width: impl Into) -> Self { - self.width = Some(width.into()); - self - } - - /// Enables interaction (primarily scrolling) with the table. - /// - /// Vertical scrolling will be enabled by default if the table is taller than its container. - /// - /// Horizontal scrolling will only be enabled if [`Self::width`] is also called, otherwise - /// the list will always shrink the table columns to fit their contents I.e. If [`Self::uniform_list`] - /// is used without a width and with [`Self::interactable`], the [`ListHorizontalSizingBehavior`] will - /// be set to [`ListHorizontalSizingBehavior::FitList`]. - pub fn interactable(mut self, interaction_state: &Entity) -> Self { - self.interaction_state = Some(interaction_state.downgrade()); - self - } - - pub fn header(mut self, headers: [impl IntoElement; COLS]) -> Self { - self.headers = Some(headers.map(IntoElement::into_any_element)); - self - } - - pub fn row(mut self, items: [impl IntoElement; COLS]) -> Self { - if let Some(rows) = self.rows.rows_mut() { - rows.push(items.map(IntoElement::into_any_element)); - } - self - } - - pub fn column_widths(mut self, widths: [impl Into; COLS]) -> Self { - if self.col_widths.is_none() { - self.col_widths = Some(TableWidths::new(widths)); - } - self - } - - pub fn resizable_columns( - mut self, - resizable: [TableResizeBehavior; COLS], - column_widths: &Entity>, - cx: &mut App, - ) -> Self { - if let Some(table_widths) = self.col_widths.as_mut() { - table_widths.resizable = resizable; - let column_widths = table_widths - .current - .get_or_insert_with(|| column_widths.clone()); - - column_widths.update(cx, |widths, _| { - if !widths.initialized { - widths.initialized = true; - widths.widths = table_widths.initial; - widths.visible_widths = widths.widths; - } - }) - } - self - } - - pub fn no_ui_font(mut self) -> Self { - self.use_ui_font = false; - self - } - - pub fn map_row( - mut self, - callback: impl Fn((usize, Stateful
), &mut Window, &mut App) -> AnyElement + 'static, - ) -> Self { - self.map_row = Some(Rc::new(callback)); - self - } - - /// Provide a callback that is invoked when the table is rendered without any rows - pub fn empty_table_callback( - mut self, - callback: impl Fn(&mut Window, &mut App) -> AnyElement + 'static, - ) -> Self { - self.empty_table_callback = Some(Rc::new(callback)); - self - } -} - -fn base_cell_style(width: Option) -> Div { - div() - .px_1p5() - .when_some(width, |this, width| this.w(width)) - .when(width.is_none(), |this| this.flex_1()) - .whitespace_nowrap() - .text_ellipsis() - .overflow_hidden() -} - -fn base_cell_style_text(width: Option, use_ui_font: bool, cx: &App) -> Div { - base_cell_style(width).when(use_ui_font, |el| el.text_ui(cx)) -} - -pub fn render_table_row( - row_index: usize, - items: [impl IntoElement; COLS], - table_context: TableRenderContext, - window: &mut Window, - cx: &mut App, -) -> AnyElement { - let is_striped = table_context.striped; - let is_last = row_index == table_context.total_row_count - 1; - let bg = if row_index % 2 == 1 && is_striped { - Some(cx.theme().colors().text.opacity(0.05)) - } else { - None - }; - let column_widths = table_context - .column_widths - .map_or([None; COLS], |widths| widths.map(Some)); - - let mut row = h_flex() - .id(("table_row", row_index)) - .size_full() - .when_some(bg, |row, bg| row.bg(bg)) - .hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.6))) - .when(!is_striped, |row| { - row.border_b_1() - .border_color(transparent_black()) - .when(!is_last, |row| row.border_color(cx.theme().colors().border)) - }); - - row = row.children( - items - .map(IntoElement::into_any_element) - .into_iter() - .zip(column_widths) - .map(|(cell, width)| { - base_cell_style_text(width, table_context.use_ui_font, cx) - .px_1() - .py_0p5() - .child(cell) - }), - ); - - let row = if let Some(map_row) = table_context.map_row { - map_row((row_index, row), window, cx) - } else { - row.into_any_element() - }; - - div().size_full().child(row).into_any_element() -} - -pub fn render_table_header( - headers: [impl IntoElement; COLS], - table_context: TableRenderContext, - columns_widths: Option<( - WeakEntity>, - [TableResizeBehavior; COLS], - [DefiniteLength; COLS], - )>, - entity_id: Option, - cx: &mut App, -) -> impl IntoElement { - let column_widths = table_context - .column_widths - .map_or([None; COLS], |widths| widths.map(Some)); - - let element_id = entity_id - .map(|entity| entity.to_string()) - .unwrap_or_default(); - - let shared_element_id: SharedString = format!("table-{}", element_id).into(); - - div() - .flex() - .flex_row() - .items_center() - .justify_between() - .w_full() - .p_2() - .border_b_1() - .border_color(cx.theme().colors().border) - .children(headers.into_iter().enumerate().zip(column_widths).map( - |((header_idx, h), width)| { - base_cell_style_text(width, table_context.use_ui_font, cx) - .child(h) - .id(ElementId::NamedInteger( - shared_element_id.clone(), - header_idx as u64, - )) - .when_some( - columns_widths.as_ref().cloned(), - |this, (column_widths, resizables, initial_sizes)| { - if resizables[header_idx].is_resizable() { - this.on_click(move |event, window, cx| { - if event.click_count() > 1 { - column_widths - .update(cx, |column, _| { - column.on_double_click( - header_idx, - &initial_sizes, - &resizables, - window, - ); - }) - .ok(); - } - }) - } else { - this - } - }, - ) - }, - )) -} - -#[derive(Clone)] -pub struct TableRenderContext { - pub striped: bool, - pub total_row_count: usize, - pub column_widths: Option<[Length; COLS]>, - pub map_row: Option), &mut Window, &mut App) -> AnyElement>>, - pub use_ui_font: bool, -} - -impl TableRenderContext { - fn new(table: &Table, cx: &App) -> Self { - Self { - striped: table.striped, - total_row_count: table.rows.len(), - column_widths: table.col_widths.as_ref().map(|widths| widths.lengths(cx)), - map_row: table.map_row.clone(), - use_ui_font: table.use_ui_font, - } - } -} - -impl RenderOnce for Table { - fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let table_context = TableRenderContext::new(&self, cx); - let interaction_state = self.interaction_state.and_then(|state| state.upgrade()); - let current_widths = self - .col_widths - .as_ref() - .and_then(|widths| Some((widths.current.as_ref()?, widths.resizable))) - .map(|(curr, resize_behavior)| (curr.downgrade(), resize_behavior)); - - let current_widths_with_initial_sizes = self - .col_widths - .as_ref() - .and_then(|widths| Some((widths.current.as_ref()?, widths.resizable, widths.initial))) - .map(|(curr, resize_behavior, initial)| (curr.downgrade(), resize_behavior, initial)); - - let width = self.width; - let no_rows_rendered = self.rows.is_empty(); - - let table = div() - .when_some(width, |this, width| this.w(width)) - .h_full() - .v_flex() - .when_some(self.headers.take(), |this, headers| { - this.child(render_table_header( - headers, - table_context.clone(), - current_widths_with_initial_sizes, - interaction_state.as_ref().map(Entity::entity_id), - cx, - )) - }) - .when_some(current_widths, { - |this, (widths, resize_behavior)| { - this.on_drag_move::({ - let widths = widths.clone(); - move |e, window, cx| { - widths - .update(cx, |widths, cx| { - widths.on_drag_move(e, &resize_behavior, window, cx); - }) - .ok(); - } - }) - .on_children_prepainted({ - let widths = widths.clone(); - move |bounds, _, cx| { - widths - .update(cx, |widths, _| { - // This works because all children x axis bounds are the same - widths.cached_bounds_width = - bounds[0].right() - bounds[0].left(); - }) - .ok(); - } - }) - .on_drop::(move |_, _, cx| { - widths - .update(cx, |widths, _| { - widths.widths = widths.visible_widths; - }) - .ok(); - // Finish the resize operation - }) - } - }) - .child({ - let content = div() - .flex_grow() - .w_full() - .relative() - .overflow_hidden() - .map(|parent| match self.rows { - TableContents::Vec(items) => { - parent.children(items.into_iter().enumerate().map(|(index, row)| { - div().child(render_table_row( - index, - row, - table_context.clone(), - window, - cx, - )) - })) - } - TableContents::UniformList(uniform_list_data) => parent.child( - uniform_list( - uniform_list_data.element_id, - uniform_list_data.row_count, - { - let render_item_fn = uniform_list_data.render_item_fn; - move |range: Range, window, cx| { - let elements = render_item_fn(range.clone(), window, cx); - elements - .into_iter() - .zip(range) - .map(|(row, row_index)| { - render_table_row( - row_index, - row, - table_context.clone(), - window, - cx, - ) - }) - .collect() - } - }, - ) - .size_full() - .flex_grow() - .with_sizing_behavior(ListSizingBehavior::Auto) - .with_horizontal_sizing_behavior(if width.is_some() { - ListHorizontalSizingBehavior::Unconstrained - } else { - ListHorizontalSizingBehavior::FitList - }) - .when_some( - interaction_state.as_ref(), - |this, state| { - this.track_scroll( - &state.read_with(cx, |s, _| s.scroll_handle.clone()), - ) - }, - ), - ), - }) - .when_some( - self.col_widths.as_ref().zip(interaction_state.as_ref()), - |parent, (table_widths, state)| { - parent.child(state.update(cx, |state, cx| { - let resizable_columns = table_widths.resizable; - let column_widths = table_widths.lengths(cx); - let columns = table_widths.current.clone(); - let initial_sizes = table_widths.initial; - state.render_resize_handles( - &column_widths, - &resizable_columns, - initial_sizes, - columns, - window, - cx, - ) - })) - }, - ); - - if let Some(state) = interaction_state.as_ref() { - let scrollbars = state - .read(cx) - .custom_scrollbar - .clone() - .unwrap_or_else(|| Scrollbars::new(super::ScrollAxes::Both)); - content - .custom_scrollbars( - scrollbars.tracked_scroll_handle(&state.read(cx).scroll_handle), - window, - cx, - ) - .into_any_element() - } else { - content.into_any_element() - } - }) - .when_some( - no_rows_rendered - .then_some(self.empty_table_callback) - .flatten(), - |this, callback| { - this.child( - h_flex() - .size_full() - .p_3() - .items_start() - .justify_center() - .child(callback(window, cx)), - ) - }, - ); - - if let Some(interaction_state) = interaction_state.as_ref() { - table - .track_focus(&interaction_state.read(cx).focus_handle) - .id(("table", interaction_state.entity_id())) - .into_any_element() - } else { - table.into_any_element() - } - } -} - -impl Component for Table<3> { - fn scope() -> ComponentScope { - ComponentScope::Layout - } - - fn description() -> Option<&'static str> { - Some("A table component for displaying data in rows and columns with optional styling.") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Basic Tables", - vec![ - single_example( - "Simple Table", - Table::new() - .width(px(400.)) - .header(["Name", "Age", "City"]) - .row(["Alice", "28", "New York"]) - .row(["Bob", "32", "San Francisco"]) - .row(["Charlie", "25", "London"]) - .into_any_element(), - ), - single_example( - "Two Column Table", - Table::new() - .header(["Category", "Value"]) - .width(px(300.)) - .row(["Revenue", "$100,000"]) - .row(["Expenses", "$75,000"]) - .row(["Profit", "$25,000"]) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Styled Tables", - vec![ - single_example( - "Default", - Table::new() - .width(px(400.)) - .header(["Product", "Price", "Stock"]) - .row(["Laptop", "$999", "In Stock"]) - .row(["Phone", "$599", "Low Stock"]) - .row(["Tablet", "$399", "Out of Stock"]) - .into_any_element(), - ), - single_example( - "Striped", - Table::new() - .width(px(400.)) - .striped() - .header(["Product", "Price", "Stock"]) - .row(["Laptop", "$999", "In Stock"]) - .row(["Phone", "$599", "Low Stock"]) - .row(["Tablet", "$399", "Out of Stock"]) - .row(["Headphones", "$199", "In Stock"]) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Mixed Content Table", - vec![single_example( - "Table with Elements", - Table::new() - .width(px(840.)) - .header(["Status", "Name", "Priority", "Deadline", "Action"]) - .row([ - Indicator::dot().color(Color::Success).into_any_element(), - "Project A".into_any_element(), - "High".into_any_element(), - "2023-12-31".into_any_element(), - Button::new("view_a", "View") - .style(ButtonStyle::Filled) - .full_width() - .into_any_element(), - ]) - .row([ - Indicator::dot().color(Color::Warning).into_any_element(), - "Project B".into_any_element(), - "Medium".into_any_element(), - "2024-03-15".into_any_element(), - Button::new("view_b", "View") - .style(ButtonStyle::Filled) - .full_width() - .into_any_element(), - ]) - .row([ - Indicator::dot().color(Color::Error).into_any_element(), - "Project C".into_any_element(), - "Low".into_any_element(), - "2024-06-30".into_any_element(), - Button::new("view_c", "View") - .style(ButtonStyle::Filled) - .full_width() - .into_any_element(), - ]) - .into_any_element(), - )], - ), - ]) - .into_any_element(), - ) - } -} - -#[cfg(test)] -mod test { - use super::*; - - fn is_almost_eq(a: &[f32], b: &[f32]) -> bool { - a.len() == b.len() && a.iter().zip(b).all(|(x, y)| (x - y).abs() < 1e-6) - } - - fn cols_to_str(cols: &[f32; COLS], total_size: f32) -> String { - cols.map(|f| "*".repeat(f32::round(f * total_size) as usize)) - .join("|") - } - - fn parse_resize_behavior( - input: &str, - total_size: f32, - ) -> [TableResizeBehavior; COLS] { - let mut resize_behavior = [TableResizeBehavior::None; COLS]; - let mut max_index = 0; - for (index, col) in input.split('|').enumerate() { - if col.starts_with('X') || col.is_empty() { - resize_behavior[index] = TableResizeBehavior::None; - } else if col.starts_with('*') { - resize_behavior[index] = - TableResizeBehavior::MinSize(col.len() as f32 / total_size); - } else { - panic!("invalid test input: unrecognized resize behavior: {}", col); - } - max_index = index; - } - - if max_index + 1 != COLS { - panic!("invalid test input: too many columns"); - } - resize_behavior - } - - mod reset_column_size { - use super::*; - - fn parse(input: &str) -> ([f32; COLS], f32, Option) { - let mut widths = [f32::NAN; COLS]; - let mut column_index = None; - for (index, col) in input.split('|').enumerate() { - widths[index] = col.len() as f32; - if col.starts_with('X') { - column_index = Some(index); - } - } - - for w in widths { - assert!(w.is_finite(), "incorrect number of columns"); - } - let total = widths.iter().sum::(); - for width in &mut widths { - *width /= total; - } - (widths, total, column_index) - } - - #[track_caller] - fn check_reset_size( - initial_sizes: &str, - widths: &str, - expected: &str, - resize_behavior: &str, - ) { - let (initial_sizes, total_1, None) = parse::(initial_sizes) else { - panic!("invalid test input: initial sizes should not be marked"); - }; - let (widths, total_2, Some(column_index)) = parse::(widths) else { - panic!("invalid test input: widths should be marked"); - }; - assert_eq!( - total_1, total_2, - "invalid test input: total width not the same {total_1}, {total_2}" - ); - let (expected, total_3, None) = parse::(expected) else { - panic!("invalid test input: expected should not be marked: {expected:?}"); - }; - assert_eq!( - total_2, total_3, - "invalid test input: total width not the same" - ); - let resize_behavior = parse_resize_behavior::(resize_behavior, total_1); - let result = TableColumnWidths::reset_to_initial_size( - column_index, - widths, - initial_sizes, - &resize_behavior, - ); - let is_eq = is_almost_eq(&result, &expected); - if !is_eq { - let result_str = cols_to_str(&result, total_1); - let expected_str = cols_to_str(&expected, total_1); - panic!( - "resize failed\ncomputed: {result_str}\nexpected: {expected_str}\n\ncomputed values: {result:?}\nexpected values: {expected:?}\n:minimum widths: {resize_behavior:?}" - ); - } - } - - macro_rules! check_reset_size { - (columns: $cols:expr, starting: $initial:expr, snapshot: $current:expr, expected: $expected:expr, resizing: $resizing:expr $(,)?) => { - check_reset_size::<$cols>($initial, $current, $expected, $resizing); - }; - ($name:ident, columns: $cols:expr, starting: $initial:expr, snapshot: $current:expr, expected: $expected:expr, minimums: $resizing:expr $(,)?) => { - #[test] - fn $name() { - check_reset_size::<$cols>($initial, $current, $expected, $resizing); - } - }; - } - - check_reset_size!( - basic_right, - columns: 5, - starting: "**|**|**|**|**", - snapshot: "**|**|X|***|**", - expected: "**|**|**|**|**", - minimums: "X|*|*|*|*", - ); - - check_reset_size!( - basic_left, - columns: 5, - starting: "**|**|**|**|**", - snapshot: "**|**|***|X|**", - expected: "**|**|**|**|**", - minimums: "X|*|*|*|**", - ); - - check_reset_size!( - squashed_left_reset_col2, - columns: 6, - starting: "*|***|**|**|****|*", - snapshot: "*|*|X|*|*|********", - expected: "*|*|**|*|*|*******", - minimums: "X|*|*|*|*|*", - ); - - check_reset_size!( - grow_cascading_right, - columns: 6, - starting: "*|***|****|**|***|*", - snapshot: "*|***|X|**|**|*****", - expected: "*|***|****|*|*|****", - minimums: "X|*|*|*|*|*", - ); - - check_reset_size!( - squashed_right_reset_col4, - columns: 6, - starting: "*|***|**|**|****|*", - snapshot: "*|********|*|*|X|*", - expected: "*|*****|*|*|****|*", - minimums: "X|*|*|*|*|*", - ); - - check_reset_size!( - reset_col6_right, - columns: 6, - starting: "*|***|**|***|***|**", - snapshot: "*|***|**|***|**|XXX", - expected: "*|***|**|***|***|**", - minimums: "X|*|*|*|*|*", - ); - - check_reset_size!( - reset_col6_left, - columns: 6, - starting: "*|***|**|***|***|**", - snapshot: "*|***|**|***|****|X", - expected: "*|***|**|***|***|**", - minimums: "X|*|*|*|*|*", - ); - - check_reset_size!( - last_column_grow_cascading, - columns: 6, - starting: "*|***|**|**|**|***", - snapshot: "*|*******|*|**|*|X", - expected: "*|******|*|*|*|***", - minimums: "X|*|*|*|*|*", - ); - - check_reset_size!( - goes_left_when_left_has_extreme_diff, - columns: 6, - starting: "*|***|****|**|**|***", - snapshot: "*|********|X|*|**|**", - expected: "*|*****|****|*|**|**", - minimums: "X|*|*|*|*|*", - ); - - check_reset_size!( - basic_shrink_right, - columns: 6, - starting: "**|**|**|**|**|**", - snapshot: "**|**|XXX|*|**|**", - expected: "**|**|**|**|**|**", - minimums: "X|*|*|*|*|*", - ); - - check_reset_size!( - shrink_should_go_left, - columns: 6, - starting: "*|***|**|*|*|*", - snapshot: "*|*|XXX|**|*|*", - expected: "*|**|**|**|*|*", - minimums: "X|*|*|*|*|*", - ); - - check_reset_size!( - shrink_should_go_right, - columns: 6, - starting: "*|***|**|**|**|*", - snapshot: "*|****|XXX|*|*|*", - expected: "*|****|**|**|*|*", - minimums: "X|*|*|*|*|*", - ); - } - - mod drag_handle { - use super::*; - - fn parse(input: &str) -> ([f32; COLS], f32, Option) { - let mut widths = [f32::NAN; COLS]; - let column_index = input.replace("*", "").find("I"); - for (index, col) in input.replace("I", "|").split('|').enumerate() { - widths[index] = col.len() as f32; - } - - for w in widths { - assert!(w.is_finite(), "incorrect number of columns"); - } - let total = widths.iter().sum::(); - for width in &mut widths { - *width /= total; - } - (widths, total, column_index) - } - - #[track_caller] - fn check( - distance: i32, - widths: &str, - expected: &str, - resize_behavior: &str, - ) { - let (mut widths, total_1, Some(column_index)) = parse::(widths) else { - panic!("invalid test input: widths should be marked"); - }; - let (expected, total_2, None) = parse::(expected) else { - panic!("invalid test input: expected should not be marked: {expected:?}"); - }; - assert_eq!( - total_1, total_2, - "invalid test input: total width not the same" - ); - let resize_behavior = parse_resize_behavior::(resize_behavior, total_1); - - let distance = distance as f32 / total_1; - - let result = TableColumnWidths::drag_column_handle( - distance, - column_index, - &mut widths, - &resize_behavior, - ); - - let is_eq = is_almost_eq(&widths, &expected); - if !is_eq { - let result_str = cols_to_str(&widths, total_1); - let expected_str = cols_to_str(&expected, total_1); - panic!( - "resize failed\ncomputed: {result_str}\nexpected: {expected_str}\n\ncomputed values: {result:?}\nexpected values: {expected:?}\n:minimum widths: {resize_behavior:?}" - ); - } - } - - macro_rules! check { - (columns: $cols:expr, distance: $dist:expr, snapshot: $current:expr, expected: $expected:expr, resizing: $resizing:expr $(,)?) => { - check!($cols, $dist, $snapshot, $expected, $resizing); - }; - ($name:ident, columns: $cols:expr, distance: $dist:expr, snapshot: $current:expr, expected: $expected:expr, minimums: $resizing:expr $(,)?) => { - #[test] - fn $name() { - check::<$cols>($dist, $current, $expected, $resizing); - } - }; - } - - check!( - basic_right_drag, - columns: 3, - distance: 1, - snapshot: "**|**I**", - expected: "**|***|*", - minimums: "X|*|*", - ); - - check!( - drag_left_against_mins, - columns: 5, - distance: -1, - snapshot: "*|*|*|*I*******", - expected: "*|*|*|*|*******", - minimums: "X|*|*|*|*", - ); - - check!( - drag_left, - columns: 5, - distance: -2, - snapshot: "*|*|*|*****I***", - expected: "*|*|*|***|*****", - minimums: "X|*|*|*|*", - ); - } -} diff --git a/crates/ui/src/components/diff_stat.rs b/crates/ui/src/components/diff_stat.rs deleted file mode 100644 index 2606963555..0000000000 --- a/crates/ui/src/components/diff_stat.rs +++ /dev/null @@ -1,85 +0,0 @@ -use crate::prelude::*; - -#[derive(IntoElement, RegisterComponent)] -pub struct DiffStat { - id: ElementId, - added: usize, - removed: usize, -} - -impl DiffStat { - pub fn new(id: impl Into, added: usize, removed: usize) -> Self { - Self { - id: id.into(), - added, - removed, - } - } -} - -impl RenderOnce for DiffStat { - fn render(self, _: &mut Window, _cx: &mut App) -> impl IntoElement { - h_flex() - .id(self.id) - .gap_1() - .child( - h_flex() - .gap_0p5() - .child( - Icon::new(IconName::Plus) - .size(IconSize::XSmall) - .color(Color::Success), - ) - .child( - Label::new(self.added.to_string()) - .color(Color::Success) - .size(LabelSize::Small), - ), - ) - .child( - h_flex() - .gap_0p5() - .child( - Icon::new(IconName::Dash) - .size(IconSize::XSmall) - .color(Color::Error), - ) - .child( - Label::new(self.removed.to_string()) - .color(Color::Error) - .size(LabelSize::Small), - ), - ) - } -} - -impl Component for DiffStat { - fn scope() -> ComponentScope { - ComponentScope::VersionControl - } - - fn preview(_window: &mut Window, cx: &mut App) -> Option { - let container = || { - h_flex() - .py_4() - .w_72() - .justify_center() - .border_1() - .border_color(cx.theme().colors().border_variant) - .bg(cx.theme().colors().panel_background) - }; - - let diff_stat_example = vec![single_example( - "Default", - container() - .child(DiffStat::new("id", 1, 2)) - .into_any_element(), - )]; - - Some( - example_group(diff_stat_example) - .vertical() - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/disclosure.rs b/crates/ui/src/components/disclosure.rs deleted file mode 100644 index 84282db2e3..0000000000 --- a/crates/ui/src/components/disclosure.rs +++ /dev/null @@ -1,152 +0,0 @@ -use std::sync::Arc; - -use gpui::{ClickEvent, CursorStyle, SharedString}; - -use crate::{Color, IconButton, IconButtonShape, IconName, IconSize, prelude::*}; - -#[derive(IntoElement, RegisterComponent)] -pub struct Disclosure { - id: ElementId, - is_open: bool, - selected: bool, - disabled: bool, - on_toggle_expanded: Option>, - cursor_style: CursorStyle, - opened_icon: IconName, - closed_icon: IconName, - visible_on_hover: Option, -} - -impl Disclosure { - pub fn new(id: impl Into, is_open: bool) -> Self { - Self { - id: id.into(), - is_open, - selected: false, - disabled: false, - on_toggle_expanded: None, - cursor_style: CursorStyle::PointingHand, - opened_icon: IconName::ChevronDown, - closed_icon: IconName::ChevronRight, - visible_on_hover: None, - } - } - - pub fn on_toggle_expanded( - mut self, - handler: impl Into>>, - ) -> Self { - self.on_toggle_expanded = handler.into(); - self - } - - pub fn opened_icon(mut self, icon: IconName) -> Self { - self.opened_icon = icon; - self - } - - pub fn closed_icon(mut self, icon: IconName) -> Self { - self.closed_icon = icon; - self - } - - pub fn disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self - } -} - -impl Toggleable for Disclosure { - fn toggle_state(mut self, selected: bool) -> Self { - self.selected = selected; - self - } -} - -impl Clickable for Disclosure { - fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self { - self.on_toggle_expanded = Some(Arc::new(handler)); - self - } - - fn cursor_style(mut self, cursor_style: gpui::CursorStyle) -> Self { - self.cursor_style = cursor_style; - self - } -} - -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( - self.id, - match self.is_open { - true => self.opened_icon, - false => self.closed_icon, - }, - ) - .shape(IconButtonShape::Square) - .icon_color(Color::Muted) - .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_expanded, move |this, on_toggle| { - this.on_click(move |event, window, cx| on_toggle(event, window, cx)) - }) - } -} - -impl Component for Disclosure { - fn scope() -> ComponentScope { - ComponentScope::Input - } - - fn description() -> Option<&'static str> { - Some( - "An interactive element used to show or hide content, typically used in expandable sections or tree-like structures.", - ) - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Disclosure States", - vec![ - single_example( - "Closed", - Disclosure::new("closed", false).into_any_element(), - ), - single_example( - "Open", - Disclosure::new("open", true).into_any_element(), - ), - ], - ), - example_group_with_title( - "Interactive Example", - vec![single_example( - "Toggleable", - v_flex() - .gap_2() - .child(Disclosure::new("interactive", false).into_any_element()) - .child(Label::new("Click to toggle")) - .into_any_element(), - )], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/divider.rs b/crates/ui/src/components/divider.rs deleted file mode 100644 index d6101f2320..0000000000 --- a/crates/ui/src/components/divider.rs +++ /dev/null @@ -1,248 +0,0 @@ -use gpui::{Hsla, IntoElement, PathBuilder, canvas, point}; - -use crate::prelude::*; - -pub fn divider() -> Divider { - Divider { - style: DividerStyle::Solid, - direction: DividerDirection::Horizontal, - color: DividerColor::default(), - inset: false, - } -} - -pub fn vertical_divider() -> Divider { - Divider { - style: DividerStyle::Solid, - direction: DividerDirection::Vertical, - color: DividerColor::default(), - inset: false, - } -} - -#[derive(Clone, Copy, PartialEq)] -enum DividerStyle { - Solid, - Dashed, -} - -#[derive(Clone, Copy, PartialEq)] -enum DividerDirection { - Horizontal, - Vertical, -} - -/// The color of a [`Divider`]. -#[derive(Default)] -pub enum DividerColor { - Border, - BorderFaded, - #[default] - BorderVariant, -} - -impl DividerColor { - pub fn hsla(self, cx: &mut App) -> Hsla { - match self { - DividerColor::Border => cx.theme().colors().border, - DividerColor::BorderFaded => cx.theme().colors().border.opacity(0.6), - DividerColor::BorderVariant => cx.theme().colors().border_variant, - } - } -} - -#[derive(IntoElement, RegisterComponent)] -pub struct Divider { - style: DividerStyle, - direction: DividerDirection, - color: DividerColor, - inset: bool, -} - -impl Divider { - pub fn horizontal() -> Self { - Self { - style: DividerStyle::Solid, - direction: DividerDirection::Horizontal, - color: DividerColor::default(), - inset: false, - } - } - - pub fn vertical() -> Self { - Self { - style: DividerStyle::Solid, - direction: DividerDirection::Vertical, - color: DividerColor::default(), - inset: false, - } - } - - pub fn horizontal_dashed() -> Self { - Self { - style: DividerStyle::Dashed, - direction: DividerDirection::Horizontal, - color: DividerColor::default(), - inset: false, - } - } - - pub fn vertical_dashed() -> Self { - Self { - style: DividerStyle::Dashed, - direction: DividerDirection::Vertical, - color: DividerColor::default(), - inset: false, - } - } - - pub fn inset(mut self) -> Self { - self.inset = true; - self - } - - pub fn color(mut self, color: DividerColor) -> Self { - self.color = color; - self - } - - pub fn render_solid(self, base: Div, cx: &mut App) -> impl IntoElement { - base.bg(self.color.hsla(cx)) - } - - pub fn render_dashed(self, base: Div) -> impl IntoElement { - base.relative().child( - canvas( - |_, _, _| {}, - move |bounds, _, window, cx| { - let mut builder = PathBuilder::stroke(px(1.)).dash_array(&[px(4.), px(2.)]); - let (start, end) = match self.direction { - DividerDirection::Horizontal => { - let x = bounds.origin.x; - let y = bounds.origin.y + px(0.5); - (point(x, y), point(x + bounds.size.width, y)) - } - DividerDirection::Vertical => { - let x = bounds.origin.x + px(0.5); - let y = bounds.origin.y; - (point(x, y), point(x, y + bounds.size.height)) - } - }; - builder.move_to(start); - builder.line_to(end); - if let Ok(line) = builder.build() { - window.paint_path(line, self.color.hsla(cx)); - } - }, - ) - .absolute() - .size_full(), - ) - } -} - -impl RenderOnce for Divider { - fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - let base = match self.direction { - DividerDirection::Horizontal => { - div().h_px().w_full().when(self.inset, |this| this.mx_1p5()) - } - DividerDirection::Vertical => { - div().w_px().h_full().when(self.inset, |this| this.my_1p5()) - } - }; - - match self.style { - DividerStyle::Solid => self.render_solid(base, cx).into_any_element(), - DividerStyle::Dashed => self.render_dashed(base).into_any_element(), - } - } -} - -impl Component for Divider { - fn scope() -> ComponentScope { - ComponentScope::Layout - } - - fn description() -> Option<&'static str> { - Some( - "Visual separator used to create divisions between groups of content or sections in a layout.", - ) - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Horizontal Dividers", - vec![ - single_example("Default", Divider::horizontal().into_any_element()), - single_example( - "Border Color", - Divider::horizontal() - .color(DividerColor::Border) - .into_any_element(), - ), - single_example( - "Inset", - Divider::horizontal().inset().into_any_element(), - ), - single_example( - "Dashed", - Divider::horizontal_dashed().into_any_element(), - ), - ], - ), - example_group_with_title( - "Vertical Dividers", - vec![ - single_example( - "Default", - div().h_16().child(Divider::vertical()).into_any_element(), - ), - single_example( - "Border Color", - div() - .h_16() - .child(Divider::vertical().color(DividerColor::Border)) - .into_any_element(), - ), - single_example( - "Inset", - div() - .h_16() - .child(Divider::vertical().inset()) - .into_any_element(), - ), - single_example( - "Dashed", - div() - .h_16() - .child(Divider::vertical_dashed()) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Example Usage", - vec![single_example( - "Between Content", - v_flex() - .w_full() - .gap_4() - .px_4() - .child(Label::new("Section One")) - .child(Divider::horizontal()) - .child(Label::new("Section Two")) - .child(Divider::horizontal_dashed()) - .child(Label::new("Section Three")) - .into_any_element(), - )], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/dropdown_menu.rs b/crates/ui/src/components/dropdown_menu.rs deleted file mode 100644 index 5b5de7a257..0000000000 --- a/crates/ui/src/components/dropdown_menu.rs +++ /dev/null @@ -1,304 +0,0 @@ -use gpui::{AnyView, Corner, Entity, Pixels, Point}; - -use crate::{ButtonLike, ContextMenu, PopoverMenu, prelude::*}; - -use super::PopoverMenuHandle; - -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] -pub enum DropdownStyle { - #[default] - Solid, - Outlined, - Subtle, - Ghost, -} - -enum LabelKind { - Text(SharedString), - Element(AnyElement), -} - -#[derive(IntoElement, RegisterComponent)] -pub struct DropdownMenu { - id: ElementId, - label: LabelKind, - trigger_size: ButtonSize, - trigger_tooltip: Option AnyView + 'static>>, - trigger_icon: Option, - style: DropdownStyle, - menu: Entity, - full_width: bool, - disabled: bool, - handle: Option>, - attach: Option, - offset: Option>, - tab_index: Option, - chevron: bool, -} - -impl DropdownMenu { - pub fn new( - id: impl Into, - label: impl Into, - menu: Entity, - ) -> Self { - Self { - id: id.into(), - label: LabelKind::Text(label.into()), - trigger_size: ButtonSize::Default, - trigger_tooltip: None, - trigger_icon: Some(IconName::ChevronUpDown), - style: DropdownStyle::default(), - menu, - full_width: false, - disabled: false, - handle: None, - attach: None, - offset: None, - tab_index: None, - chevron: true, - } - } - - pub fn new_with_element( - id: impl Into, - label: AnyElement, - menu: Entity, - ) -> Self { - Self { - id: id.into(), - label: LabelKind::Element(label), - trigger_size: ButtonSize::Default, - trigger_tooltip: None, - trigger_icon: Some(IconName::ChevronUpDown), - style: DropdownStyle::default(), - menu, - full_width: false, - disabled: false, - handle: None, - attach: None, - offset: None, - tab_index: None, - chevron: true, - } - } - - pub fn style(mut self, style: DropdownStyle) -> Self { - self.style = style; - self - } - - pub fn trigger_size(mut self, size: ButtonSize) -> Self { - self.trigger_size = size; - self - } - - pub fn trigger_tooltip( - mut self, - tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static, - ) -> Self { - self.trigger_tooltip = Some(Box::new(tooltip)); - self - } - - pub fn trigger_icon(mut self, icon: IconName) -> Self { - self.trigger_icon = Some(icon); - self - } - - pub fn full_width(mut self, full_width: bool) -> Self { - self.full_width = full_width; - self - } - - pub fn handle(mut self, handle: PopoverMenuHandle) -> Self { - self.handle = Some(handle); - self - } - - /// Defines which corner of the handle to attach the menu's anchor to. - pub fn attach(mut self, attach: Corner) -> Self { - self.attach = Some(attach); - self - } - - /// Offsets the position of the menu by that many pixels. - pub fn offset(mut self, offset: Point) -> Self { - self.offset = Some(offset); - self - } - - pub fn tab_index(mut self, arg: isize) -> Self { - self.tab_index = Some(arg); - self - } - - pub fn no_chevron(mut self) -> Self { - self.chevron = false; - self - } -} - -impl Disableable for DropdownMenu { - fn disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self - } -} - -impl RenderOnce for DropdownMenu { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let button_style = match self.style { - DropdownStyle::Solid => ButtonStyle::Filled, - DropdownStyle::Subtle => ButtonStyle::Subtle, - DropdownStyle::Outlined => ButtonStyle::Outlined, - DropdownStyle::Ghost => ButtonStyle::Transparent, - }; - - let full_width = self.full_width; - let trigger_size = self.trigger_size; - - let (text_button, element_button) = match self.label { - LabelKind::Text(text) => ( - Some( - Button::new(self.id.clone(), text) - .style(button_style) - .when(self.chevron, |this| { - this.icon(self.trigger_icon) - .icon_position(IconPosition::End) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - }) - .when(full_width, |this| this.full_width()) - .size(trigger_size) - .disabled(self.disabled) - .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)), - ), - None, - ), - LabelKind::Element(element) => ( - None, - Some( - ButtonLike::new(self.id.clone()) - .child(element) - .style(button_style) - .when(self.chevron, |this| { - this.child( - Icon::new(IconName::ChevronUpDown) - .size(IconSize::XSmall) - .color(Color::Muted), - ) - }) - .when(full_width, |this| this.full_width()) - .size(trigger_size) - .disabled(self.disabled) - .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index)), - ), - ), - }; - - let mut popover = PopoverMenu::new((self.id.clone(), "popover")) - .full_width(self.full_width) - .menu(move |_window, _cx| Some(self.menu.clone())); - - popover = match (text_button, element_button, self.trigger_tooltip) { - (Some(text_button), None, Some(tooltip)) => { - popover.trigger_with_tooltip(text_button, tooltip) - } - (Some(text_button), None, None) => popover.trigger(text_button), - (None, Some(element_button), Some(tooltip)) => { - popover.trigger_with_tooltip(element_button, tooltip) - } - (None, Some(element_button), None) => popover.trigger(element_button), - _ => popover, - }; - - popover - .attach(match self.attach { - Some(attach) => attach, - None => Corner::BottomRight, - }) - .when_some(self.offset, |this, offset| this.offset(offset)) - .when_some(self.handle, |this, handle| this.with_handle(handle)) - } -} - -impl Component for DropdownMenu { - fn scope() -> ComponentScope { - ComponentScope::Input - } - - fn name() -> &'static str { - "DropdownMenu" - } - - fn description() -> Option<&'static str> { - Some( - "A dropdown menu displays a list of actions or options. A dropdown menu is always activated by clicking a trigger (or via a keybinding).", - ) - } - - fn preview(window: &mut Window, cx: &mut App) -> Option { - let menu = ContextMenu::build(window, cx, |this, _, _| { - this.entry("Option 1", None, |_, _| {}) - .entry("Option 2", None, |_, _| {}) - .entry("Option 3", None, |_, _| {}) - .separator() - .entry("Option 4", None, |_, _| {}) - }); - - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Basic Usage", - vec![ - single_example( - "Default", - DropdownMenu::new("default", "Select an option", menu.clone()) - .into_any_element(), - ), - single_example( - "Full Width", - DropdownMenu::new( - "full-width", - "Full Width Dropdown", - menu.clone(), - ) - .full_width(true) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Styles", - vec![ - single_example( - "Outlined", - DropdownMenu::new("outlined", "Outlined Dropdown", menu.clone()) - .style(DropdownStyle::Outlined) - .into_any_element(), - ), - single_example( - "Ghost", - DropdownMenu::new("ghost", "Ghost Dropdown", menu.clone()) - .style(DropdownStyle::Ghost) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "States", - vec![single_example( - "Disabled", - DropdownMenu::new("disabled", "Disabled Dropdown", menu) - .disabled(true) - .into_any_element(), - )], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/facepile.rs b/crates/ui/src/components/facepile.rs deleted file mode 100644 index 79a36871d6..0000000000 --- a/crates/ui/src/components/facepile.rs +++ /dev/null @@ -1,135 +0,0 @@ -use crate::component_prelude::*; -use crate::prelude::*; -use gpui::{AnyElement, StyleRefinement}; -use smallvec::SmallVec; - -use super::Avatar; - -/// An element that displays a collection of (usually) faces stacked -/// horizontally, with the left-most face on top, visually descending -/// from left to right. -/// -/// Facepiles are used to display a group of people or things, -/// such as a list of participants in a collaboration session. -/// -/// # Examples -/// -/// ## Default -/// -/// A default, horizontal facepile. -/// -/// ``` -/// use gpui::IntoElement; -/// use ui::{Avatar, Facepile, EXAMPLE_FACES}; -/// -/// let facepile = Facepile::new( -/// EXAMPLE_FACES.iter().take(3).map(|&url| -/// Avatar::new(url).into_any_element()).collect() -/// ); -/// ``` -#[derive(IntoElement, Documented, RegisterComponent)] -pub struct Facepile { - base: Div, - faces: SmallVec<[AnyElement; 2]>, -} - -impl Facepile { - /// Creates a new empty facepile. - pub fn empty() -> Self { - Self::new(SmallVec::new()) - } - - /// Creates a new facepile with the given faces. - pub fn new(faces: SmallVec<[AnyElement; 2]>) -> Self { - Self { base: div(), faces } - } -} - -impl ParentElement for Facepile { - fn extend(&mut self, elements: impl IntoIterator) { - self.faces.extend(elements); - } -} - -// Style methods. -impl Facepile { - fn style(&mut self) -> &mut StyleRefinement { - self.base.style() - } - - gpui::padding_style_methods!({ - visibility: pub - }); -} - -impl RenderOnce for Facepile { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - // Lay the faces out in reverse so they overlap in the desired order (left to right, front to back) - self.base - .flex() - .flex_row_reverse() - .items_center() - .justify_start() - .children( - self.faces - .into_iter() - .enumerate() - .rev() - .map(|(ix, player)| div().when(ix > 0, |div| div.ml_neg_1()).child(player)), - ) - } -} - -pub const EXAMPLE_FACES: [&str; 6] = [ - "https://avatars.githubusercontent.com/u/326587?s=60&v=4", - "https://avatars.githubusercontent.com/u/2280405?s=60&v=4", - "https://avatars.githubusercontent.com/u/1789?s=60&v=4", - "https://avatars.githubusercontent.com/u/67129314?s=60&v=4", - "https://avatars.githubusercontent.com/u/482957?s=60&v=4", - "https://avatars.githubusercontent.com/u/1714999?s=60&v=4", -]; - -impl Component for Facepile { - fn scope() -> ComponentScope { - ComponentScope::Collaboration - } - - fn description() -> Option<&'static str> { - Some( - "Displays a collection of avatars or initials in a compact format. Often used to represent active collaborators or a subset of contributors.", - ) - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![example_group_with_title( - "Facepile Examples", - vec![ - single_example( - "Default", - Facepile::new( - EXAMPLE_FACES - .iter() - .map(|&url| Avatar::new(url).into_any_element()) - .collect(), - ) - .into_any_element(), - ), - single_example( - "Custom Size", - Facepile::new( - EXAMPLE_FACES - .iter() - .map(|&url| Avatar::new(url).size(px(24.)).into_any_element()) - .collect(), - ) - .into_any_element(), - ), - ], - )]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/group.rs b/crates/ui/src/components/group.rs deleted file mode 100644 index 12462bb24b..0000000000 --- a/crates/ui/src/components/group.rs +++ /dev/null @@ -1,57 +0,0 @@ -use gpui::{Div, div, prelude::*}; - -/// Creates a horizontal group with tight, consistent spacing. -/// -/// xs: ~2px @16px/rem -pub fn h_group_sm() -> Div { - div().flex().gap_0p5() -} - -/// Creates a horizontal group with consistent spacing. -/// -/// s: ~4px @16px/rem -pub fn h_group() -> Div { - div().flex().gap_1() -} - -/// Creates a horizontal group with consistent spacing. -/// -/// m: ~6px @16px/rem -pub fn h_group_lg() -> Div { - div().flex().gap_1p5() -} - -/// Creates a horizontal group with consistent spacing. -/// -/// l: ~8px @16px/rem -pub fn h_group_xl() -> Div { - div().flex().gap_2() -} - -/// Creates a vertical group with tight, consistent spacing. -/// -/// xs: ~2px @16px/rem -pub fn v_group_sm() -> Div { - div().flex().flex_col().gap_0p5() -} - -/// Creates a vertical group with consistent spacing. -/// -/// s: ~4px @16px/rem -pub fn v_group() -> Div { - div().flex().flex_col().gap_1() -} - -/// Creates a vertical group with consistent spacing. -/// -/// m: ~6px @16px/rem -pub fn v_group_lg() -> Div { - div().flex().flex_col().gap_1p5() -} - -/// Creates a vertical group with consistent spacing. -/// -/// l: ~8px @16px/rem -pub fn v_group_xl() -> Div { - div().flex().flex_col().gap_2() -} diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs deleted file mode 100644 index 1c8e36ec18..0000000000 --- a/crates/ui/src/components/icon.rs +++ /dev/null @@ -1,371 +0,0 @@ -mod decorated_icon; -mod icon_decoration; - -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -pub use decorated_icon::*; -use gpui::{AnimationElement, AnyElement, Hsla, IntoElement, Rems, Transformation, img, svg}; -pub use icon_decoration::*; -pub use icons::*; - -use crate::traits::transformable::Transformable; -use crate::{Indicator, prelude::*}; - -#[derive(IntoElement)] -pub enum AnyIcon { - Icon(Icon), - AnimatedIcon(AnimationElement), -} - -impl AnyIcon { - /// Returns a new [`AnyIcon`] after applying the given mapping function - /// to the contained [`Icon`]. - pub fn map(self, f: impl FnOnce(Icon) -> Icon) -> Self { - match self { - Self::Icon(icon) => Self::Icon(f(icon)), - Self::AnimatedIcon(animated_icon) => Self::AnimatedIcon(animated_icon.map_element(f)), - } - } -} - -impl From for AnyIcon { - fn from(value: Icon) -> Self { - Self::Icon(value) - } -} - -impl From> for AnyIcon { - fn from(value: AnimationElement) -> Self { - Self::AnimatedIcon(value) - } -} - -impl RenderOnce for AnyIcon { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - match self { - Self::Icon(icon) => icon.into_any_element(), - Self::AnimatedIcon(animated_icon) => animated_icon.into_any_element(), - } - } -} - -#[derive(Default, PartialEq, Copy, Clone)] -pub enum IconSize { - /// 10px - Indicator, - /// 12px - XSmall, - /// 14px - Small, - #[default] - /// 16px - Medium, - /// 48px - XLarge, - Custom(Rems), -} - -impl IconSize { - pub fn rems(self) -> Rems { - match self { - IconSize::Indicator => rems_from_px(10.), - IconSize::XSmall => rems_from_px(12.), - IconSize::Small => rems_from_px(14.), - IconSize::Medium => rems_from_px(16.), - IconSize::XLarge => rems_from_px(48.), - IconSize::Custom(size) => size, - } - } - - /// Returns the individual components of the square that contains this [`IconSize`]. - /// - /// The returned tuple contains: - /// 1. The length of one side of the square - /// 2. The padding of one side of the square - pub fn square_components(&self, window: &mut Window, cx: &mut App) -> (Pixels, Pixels) { - let icon_size = self.rems() * window.rem_size(); - let padding = match self { - IconSize::Indicator => DynamicSpacing::Base00.px(cx), - IconSize::XSmall => DynamicSpacing::Base02.px(cx), - IconSize::Small => DynamicSpacing::Base02.px(cx), - IconSize::Medium => DynamicSpacing::Base02.px(cx), - IconSize::XLarge => DynamicSpacing::Base02.px(cx), - // TODO: Wire into dynamic spacing - IconSize::Custom(size) => size.to_pixels(window.rem_size()), - }; - - (icon_size, padding) - } - - /// Returns the length of a side of the square that contains this [`IconSize`], with padding. - pub fn square(&self, window: &mut Window, cx: &mut App) -> Pixels { - let (icon_size, padding) = self.square_components(window, cx); - - icon_size + padding * 2. - } -} - -impl From for Icon { - fn from(icon: IconName) -> Self { - Icon::new(icon) - } -} - -/// The source of an icon. -enum IconSource { - /// An SVG embedded in the Zed binary. - Embedded(SharedString), - /// An image file located at the specified path. - /// - /// Currently our SVG renderer is missing support for rendering polychrome SVGs. - /// - /// In order to support icon themes, we render the icons as images instead. - External(Arc), - /// An SVG not embedded in the Zed binary. - ExternalSvg(SharedString), -} - -impl IconSource { - fn from_path(path: impl Into) -> Self { - let path = path.into(); - if path.starts_with("icons/") { - Self::Embedded(path) - } else { - Self::External(Arc::from(PathBuf::from(path.as_ref()))) - } - } -} - -#[derive(IntoElement, RegisterComponent)] -pub struct Icon { - source: IconSource, - color: Color, - size: Rems, - transformation: Transformation, -} - -impl Icon { - pub fn new(icon: IconName) -> Self { - Self { - source: IconSource::Embedded(icon.path().into()), - color: Color::default(), - size: IconSize::default().rems(), - transformation: Transformation::default(), - } - } - - pub fn from_path(path: impl Into) -> Self { - Self { - source: IconSource::from_path(path), - color: Color::default(), - size: IconSize::default().rems(), - transformation: Transformation::default(), - } - } - - pub fn from_external_svg(svg: SharedString) -> Self { - Self { - source: IconSource::ExternalSvg(svg), - color: Color::default(), - size: IconSize::default().rems(), - transformation: Transformation::default(), - } - } - - pub fn color(mut self, color: Color) -> Self { - self.color = color; - self - } - - pub fn size(mut self, size: IconSize) -> Self { - self.size = size.rems(); - self - } - - /// Sets a custom size for the icon, in [`Rems`]. - /// - /// Not to be exposed outside of the `ui` crate. - pub(crate) fn custom_size(mut self, size: Rems) -> Self { - self.size = size; - self - } -} - -impl Transformable for Icon { - fn transform(mut self, transformation: Transformation) -> Self { - self.transformation = transformation; - self - } -} - -impl RenderOnce for Icon { - fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - match self.source { - IconSource::Embedded(path) => svg() - .with_transformation(self.transformation) - .size(self.size) - .flex_none() - .path(path) - .text_color(self.color.color(cx)) - .into_any_element(), - IconSource::ExternalSvg(path) => svg() - .external_path(path) - .with_transformation(self.transformation) - .size(self.size) - .flex_none() - .text_color(self.color.color(cx)) - .into_any_element(), - IconSource::External(path) => img(path) - .size(self.size) - .flex_none() - .text_color(self.color.color(cx)) - .into_any_element(), - } - } -} - -#[derive(IntoElement)] -pub struct IconWithIndicator { - icon: Icon, - indicator: Option, - indicator_border_color: Option, -} - -impl IconWithIndicator { - pub fn new(icon: Icon, indicator: Option) -> Self { - Self { - icon, - indicator, - indicator_border_color: None, - } - } - - pub fn indicator(mut self, indicator: Option) -> Self { - self.indicator = indicator; - self - } - - pub fn indicator_color(mut self, color: Color) -> Self { - if let Some(indicator) = self.indicator.as_mut() { - indicator.color = color; - } - self - } - - pub fn indicator_border_color(mut self, color: Option) -> Self { - self.indicator_border_color = color; - self - } -} - -impl RenderOnce for IconWithIndicator { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let indicator_border_color = self - .indicator_border_color - .unwrap_or_else(|| cx.theme().colors().elevated_surface_background); - - div() - .relative() - .child(self.icon) - .when_some(self.indicator, |this, indicator| { - this.child( - div() - .absolute() - .size_2p5() - .border_2() - .border_color(indicator_border_color) - .rounded_full() - .bottom_neg_0p5() - .right_neg_0p5() - .child(indicator), - ) - }) - } -} - -impl Component for Icon { - fn scope() -> ComponentScope { - ComponentScope::Images - } - - fn description() -> Option<&'static str> { - Some( - "A versatile icon component that supports SVG and image-based icons with customizable size, color, and transformations.", - ) - } - - fn preview(_window: &mut Window, cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Sizes", - vec![single_example( - "XSmall, Small, Default, Large", - h_flex() - .gap_1() - .child( - Icon::new(IconName::Star) - .size(IconSize::XSmall) - .into_any_element(), - ) - .child( - Icon::new(IconName::Star) - .size(IconSize::Small) - .into_any_element(), - ) - .child(Icon::new(IconName::Star).into_any_element()) - .child( - Icon::new(IconName::Star) - .size(IconSize::XLarge) - .into_any_element(), - ) - .into_any_element(), - )], - ), - example_group_with_title( - "Colors", - vec![single_example( - "Default & Custom", - h_flex() - .gap_1() - .child(Icon::new(IconName::Star).into_any_element()) - .child( - Icon::new(IconName::Star) - .color(Color::Error) - .into_any_element(), - ) - .into_any_element(), - )], - ), - example_group_with_title( - "All Icons", - vec![single_example( - "All Icons", - h_flex() - .image_cache(gpui::retain_all("all icons")) - .flex_wrap() - .gap_2() - .children(::iter().map( - |icon_name| { - h_flex() - .p_1() - .gap_1() - .border_1() - .border_color(cx.theme().colors().border_variant) - .bg(cx.theme().colors().element_disabled) - .rounded_sm() - .child(Icon::new(icon_name).into_any_element()) - .child(SharedString::new_static(icon_name.into())) - }, - )) - .into_any_element(), - )], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/icon/decorated_icon.rs b/crates/ui/src/components/icon/decorated_icon.rs deleted file mode 100644 index 82ca844c38..0000000000 --- a/crates/ui/src/components/icon/decorated_icon.rs +++ /dev/null @@ -1,106 +0,0 @@ -use gpui::{AnyElement, IntoElement, Point}; - -use crate::{IconDecoration, IconDecorationKind, prelude::*}; - -#[derive(IntoElement, RegisterComponent)] -pub struct DecoratedIcon { - icon: Icon, - decoration: Option, -} - -impl DecoratedIcon { - pub fn new(icon: Icon, decoration: Option) -> Self { - Self { icon, decoration } - } -} - -impl RenderOnce for DecoratedIcon { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - div() - .relative() - .size(self.icon.size) - .child(self.icon) - .children(self.decoration) - } -} - -impl Component for DecoratedIcon { - fn scope() -> ComponentScope { - ComponentScope::Images - } - - fn description() -> Option<&'static str> { - Some( - "An icon with an optional decoration overlay (like an X, triangle, or dot) that can be positioned relative to the icon", - ) - } - - fn preview(_window: &mut Window, cx: &mut App) -> Option { - let decoration_x = IconDecoration::new( - IconDecorationKind::X, - cx.theme().colors().surface_background, - cx, - ) - .color(cx.theme().status().error) - .position(Point { - x: px(-2.), - y: px(-2.), - }); - - let decoration_triangle = IconDecoration::new( - IconDecorationKind::Triangle, - cx.theme().colors().surface_background, - cx, - ) - .color(cx.theme().status().error) - .position(Point { - x: px(-2.), - y: px(-2.), - }); - - let decoration_dot = IconDecoration::new( - IconDecorationKind::Dot, - cx.theme().colors().surface_background, - cx, - ) - .color(cx.theme().status().error) - .position(Point { - x: px(-2.), - y: px(-2.), - }); - - Some( - v_flex() - .gap_6() - .children(vec![example_group_with_title( - "Decorations", - vec![ - single_example( - "No Decoration", - DecoratedIcon::new(Icon::new(IconName::FileDoc), None) - .into_any_element(), - ), - single_example( - "X Decoration", - DecoratedIcon::new(Icon::new(IconName::FileDoc), Some(decoration_x)) - .into_any_element(), - ), - single_example( - "Triangle Decoration", - DecoratedIcon::new( - Icon::new(IconName::FileDoc), - Some(decoration_triangle), - ) - .into_any_element(), - ), - single_example( - "Dot Decoration", - DecoratedIcon::new(Icon::new(IconName::FileDoc), Some(decoration_dot)) - .into_any_element(), - ), - ], - )]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/icon/icon_decoration.rs b/crates/ui/src/components/icon/icon_decoration.rs deleted file mode 100644 index 9f84a8bcf4..0000000000 --- a/crates/ui/src/components/icon/icon_decoration.rs +++ /dev/null @@ -1,159 +0,0 @@ -use std::sync::Arc; - -use gpui::{Hsla, IntoElement, Point, svg}; -use strum::{EnumIter, EnumString, IntoStaticStr}; - -use crate::prelude::*; - -const ICON_DECORATION_SIZE: Pixels = px(11.); - -/// An icon silhouette used to knockout the background of an element for an icon -/// to sit on top of it, emulating a stroke/border. -#[derive(Debug, PartialEq, Eq, Copy, Clone, EnumIter, EnumString, IntoStaticStr)] -#[strum(serialize_all = "snake_case")] -pub enum KnockoutIconName { - XFg, - XBg, - DotFg, - DotBg, - TriangleFg, - TriangleBg, -} - -impl KnockoutIconName { - /// Returns the path to this icon. - pub fn path(&self) -> Arc { - let file_stem: &'static str = self.into(); - format!("icons/knockouts/{file_stem}.svg").into() - } -} - -#[derive(Debug, PartialEq, Eq, Copy, Clone, EnumIter, EnumString)] -pub enum IconDecorationKind { - X, - Dot, - Triangle, -} - -impl IconDecorationKind { - fn fg(&self) -> KnockoutIconName { - match self { - Self::X => KnockoutIconName::XFg, - Self::Dot => KnockoutIconName::DotFg, - Self::Triangle => KnockoutIconName::TriangleFg, - } - } - - fn bg(&self) -> KnockoutIconName { - match self { - Self::X => KnockoutIconName::XBg, - Self::Dot => KnockoutIconName::DotBg, - Self::Triangle => KnockoutIconName::TriangleBg, - } - } -} - -/// The decoration for an icon. -/// -/// For example, this can show an indicator, an "x", or a diagonal strikethrough -/// to indicate something is disabled. -#[derive(IntoElement)] -pub struct IconDecoration { - kind: IconDecorationKind, - color: Hsla, - knockout_color: Hsla, - knockout_hover_color: Hsla, - position: Point, - group_name: Option, -} - -impl IconDecoration { - /// Creates a new [`IconDecoration`]. - pub fn new(kind: IconDecorationKind, knockout_color: Hsla, cx: &App) -> Self { - let color = cx.theme().colors().icon; - let position = Point::default(); - - Self { - kind, - color, - knockout_color, - knockout_hover_color: knockout_color, - position, - group_name: None, - } - } - - /// Sets the kind of decoration. - pub fn kind(mut self, kind: IconDecorationKind) -> Self { - self.kind = kind; - self - } - - /// Sets the color of the decoration. - pub fn color(mut self, color: Hsla) -> Self { - self.color = color; - self - } - - /// Sets the color of the decoration's knockout - /// - /// Match this to the background of the element the icon will be rendered - /// on. - pub fn knockout_color(mut self, color: Hsla) -> Self { - self.knockout_color = color; - self - } - - /// Sets the color of the decoration that is used on hover. - pub fn knockout_hover_color(mut self, color: Hsla) -> Self { - self.knockout_hover_color = color; - self - } - - /// Sets the position of the decoration. - pub fn position(mut self, position: Point) -> Self { - self.position = position; - self - } - - /// Sets the name of the group the decoration belongs to - pub fn group_name(mut self, name: Option) -> Self { - self.group_name = name; - self - } -} - -impl RenderOnce for IconDecoration { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let foreground = svg() - .absolute() - .bottom_0() - .right_0() - .size(ICON_DECORATION_SIZE) - .path(self.kind.fg().path()) - .text_color(self.color); - - let background = svg() - .absolute() - .bottom_0() - .right_0() - .size(ICON_DECORATION_SIZE) - .path(self.kind.bg().path()) - .text_color(self.knockout_color) - .map(|this| match self.group_name { - Some(group_name) => this.group_hover(group_name, |style| { - style.text_color(self.knockout_hover_color) - }), - None => this.hover(|style| style.text_color(self.knockout_hover_color)), - }); - - div() - .size(ICON_DECORATION_SIZE) - .flex_none() - .absolute() - .bottom(self.position.y) - .right(self.position.x) - .child(foreground) - .child(background) - } -} diff --git a/crates/ui/src/components/image.rs b/crates/ui/src/components/image.rs deleted file mode 100644 index 3e8cbd8fff..0000000000 --- a/crates/ui/src/components/image.rs +++ /dev/null @@ -1,184 +0,0 @@ -use std::sync::Arc; - -use gpui::Transformation; -use gpui::{App, IntoElement, Rems, RenderOnce, Size, Styled, Window, svg}; -use serde::{Deserialize, Serialize}; -use strum::{EnumIter, EnumString, IntoStaticStr}; - -use crate::Color; -use crate::prelude::*; -use crate::traits::transformable::Transformable; - -#[derive( - Debug, PartialEq, Eq, Copy, Clone, EnumIter, EnumString, IntoStaticStr, Serialize, Deserialize, -)] -#[strum(serialize_all = "snake_case")] -pub enum VectorName { - AcpGrid, - AcpLogo, - AcpLogoSerif, - AiGrid, - DebuggerGrid, - Grid, - ProTrialStamp, - ProUserStamp, - ZedLogo, - ZedXCopilot, -} - -impl VectorName { - /// Returns the path to this vector image. - pub fn path(&self) -> Arc { - let file_stem: &'static str = self.into(); - format!("images/{file_stem}.svg").into() - } -} - -/// A vector image, such as an SVG. -/// -/// A [`Vector`] is different from an [`crate::Icon`] in that it is intended -/// to be displayed at a specific size, or series of sizes, rather -/// than conforming to the standard size of an icon. -#[derive(IntoElement, RegisterComponent)] -pub struct Vector { - path: Arc, - color: Color, - size: Size, - transformation: Transformation, -} - -impl Vector { - /// Creates a new [`Vector`] image with the given [`VectorName`] and size. - pub fn new(vector: VectorName, width: Rems, height: Rems) -> Self { - Self { - path: vector.path(), - color: Color::default(), - size: Size { width, height }, - transformation: Transformation::default(), - } - } - - /// Creates a new [`Vector`] image where the width and height are the same. - pub fn square(vector: VectorName, size: Rems) -> Self { - Self::new(vector, size, size) - } - - /// Sets the vector color. - pub fn color(mut self, color: Color) -> Self { - self.color = color; - self - } - - /// Sets the vector size. - pub fn size(mut self, size: impl Into>) -> Self { - let size = size.into(); - self.size = size; - self - } -} - -impl Transformable for Vector { - fn transform(mut self, transformation: Transformation) -> Self { - self.transformation = transformation; - self - } -} - -impl RenderOnce for Vector { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let width = self.size.width; - let height = self.size.height; - - svg() - // By default, prevent the SVG from stretching - // to fill its container. - .flex_none() - .w(width) - .h(height) - .path(self.path) - .text_color(self.color.color(cx)) - .with_transformation(self.transformation) - } -} - -impl Component for Vector { - fn scope() -> ComponentScope { - ComponentScope::Images - } - - fn name() -> &'static str { - "Vector" - } - - fn description() -> Option<&'static str> { - Some("A vector image component that can be displayed at specific sizes.") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - let size = rems_from_px(60.); - - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Basic Usage", - vec![ - single_example( - "Default", - Vector::square(VectorName::ZedLogo, size).into_any_element(), - ), - single_example( - "Custom Size", - h_flex() - .h(rems_from_px(120.)) - .justify_center() - .child(Vector::new( - VectorName::ZedLogo, - rems_from_px(120.), - rems_from_px(200.), - )) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Colored", - vec![ - single_example( - "Accent Color", - Vector::square(VectorName::ZedLogo, size) - .color(Color::Accent) - .into_any_element(), - ), - single_example( - "Error Color", - Vector::square(VectorName::ZedLogo, size) - .color(Color::Error) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Different Vectors", - vec![single_example( - "Zed X Copilot", - Vector::square(VectorName::ZedXCopilot, rems_from_px(100.)) - .into_any_element(), - )], - ), - ]) - .into_any_element(), - ) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn vector_path() { - assert_eq!(VectorName::ZedLogo.path().as_ref(), "images/zed_logo.svg"); - } -} diff --git a/crates/ui/src/components/indent_guides.rs b/crates/ui/src/components/indent_guides.rs deleted file mode 100644 index 60aa23b44c..0000000000 --- a/crates/ui/src/components/indent_guides.rs +++ /dev/null @@ -1,596 +0,0 @@ -use std::{cmp::Ordering, ops::Range, rc::Rc}; - -use gpui::{AnyElement, App, Bounds, Entity, Hsla, Point, fill, point, size}; -use gpui::{DispatchPhase, Hitbox, HitboxBehavior, MouseButton, MouseDownEvent, MouseMoveEvent}; -use smallvec::SmallVec; - -use crate::prelude::*; - -/// Represents the colors used for different states of indent guides. -#[derive(Debug, Clone)] -pub struct IndentGuideColors { - /// The color of the indent guide when it's neither active nor hovered. - pub default: Hsla, - /// The color of the indent guide when it's hovered. - pub hover: Hsla, - /// The color of the indent guide when it's active. - pub active: Hsla, -} - -impl IndentGuideColors { - /// Returns the indent guide colors that should be used for panels. - pub fn panel(cx: &App) -> Self { - Self { - default: cx.theme().colors().panel_indent_guide, - hover: cx.theme().colors().panel_indent_guide_hover, - active: cx.theme().colors().panel_indent_guide_active, - } - } -} - -pub struct IndentGuides { - colors: IndentGuideColors, - indent_size: Pixels, - compute_indents_fn: - Option, &mut Window, &mut App) -> SmallVec<[usize; 64]>>>, - render_fn: Option< - Box< - dyn Fn( - RenderIndentGuideParams, - &mut Window, - &mut App, - ) -> SmallVec<[RenderedIndentGuide; 12]>, - >, - >, - on_click: Option>, -} - -pub fn indent_guides(indent_size: Pixels, colors: IndentGuideColors) -> IndentGuides { - IndentGuides { - colors, - indent_size, - compute_indents_fn: None, - render_fn: None, - on_click: None, - } -} - -impl IndentGuides { - /// Sets the callback that will be called when the user clicks on an indent guide. - pub fn on_click( - mut self, - on_click: impl Fn(&IndentGuideLayout, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_click = Some(Rc::new(on_click)); - self - } - - /// Sets the function that computes indents for uniform list decoration. - pub fn with_compute_indents_fn( - mut self, - entity: Entity, - compute_indents_fn: impl Fn( - &mut V, - Range, - &mut Window, - &mut Context, - ) -> SmallVec<[usize; 64]> - + 'static, - ) -> Self { - let compute_indents_fn = Box::new(move |range, window: &mut Window, cx: &mut App| { - entity.update(cx, |this, cx| compute_indents_fn(this, range, window, cx)) - }); - self.compute_indents_fn = Some(compute_indents_fn); - self - } - - /// Sets a custom callback that will be called when the indent guides need to be rendered. - pub fn with_render_fn( - mut self, - entity: Entity, - render_fn: impl Fn( - &mut V, - RenderIndentGuideParams, - &mut Window, - &mut App, - ) -> SmallVec<[RenderedIndentGuide; 12]> - + 'static, - ) -> Self { - let render_fn = move |params, window: &mut Window, cx: &mut App| { - entity.update(cx, |this, cx| render_fn(this, params, window, cx)) - }; - self.render_fn = Some(Box::new(render_fn)); - self - } - - fn render_from_layout( - &self, - indent_guides: SmallVec<[IndentGuideLayout; 12]>, - bounds: Bounds, - item_height: Pixels, - window: &mut Window, - cx: &mut App, - ) -> AnyElement { - let mut indent_guides = if let Some(ref custom_render) = self.render_fn { - let params = RenderIndentGuideParams { - indent_guides, - indent_size: self.indent_size, - item_height, - }; - custom_render(params, window, cx) - } else { - indent_guides - .into_iter() - .map(|layout| RenderedIndentGuide { - bounds: Bounds::new( - point( - layout.offset.x * self.indent_size, - layout.offset.y * item_height, - ), - size(px(1.), layout.length * item_height), - ), - layout, - is_active: false, - hitbox: None, - }) - .collect() - }; - for guide in &mut indent_guides { - guide.bounds.origin += bounds.origin; - if let Some(hitbox) = guide.hitbox.as_mut() { - hitbox.origin += bounds.origin; - } - } - - let indent_guides = IndentGuidesElement { - indent_guides: Rc::new(indent_guides), - colors: self.colors.clone(), - on_hovered_indent_guide_click: self.on_click.clone(), - }; - indent_guides.into_any_element() - } -} - -/// Parameters for rendering indent guides. -pub struct RenderIndentGuideParams { - /// The calculated layouts for the indent guides to be rendered. - pub indent_guides: SmallVec<[IndentGuideLayout; 12]>, - /// The size of each indentation level in pixels. - pub indent_size: Pixels, - /// The height of each item in pixels. - pub item_height: Pixels, -} - -/// Represents a rendered indent guide with its visual properties and interaction areas. -pub struct RenderedIndentGuide { - /// The bounds of the rendered indent guide in pixels. - pub bounds: Bounds, - /// The layout information for the indent guide. - pub layout: IndentGuideLayout, - /// Indicates whether the indent guide is currently active. - pub is_active: bool, - /// Can be used to customize the hitbox of the indent guide, - /// if this is set to `None`, the bounds of the indent guide will be used. - pub hitbox: Option>, -} - -/// Represents the layout information for an indent guide. -#[derive(Debug, PartialEq, Eq, Hash)] -pub struct IndentGuideLayout { - /// The starting position of the indent guide, where x is the indentation level - /// and y is the starting row. - pub offset: Point, - /// The length of the indent guide in rows. - pub length: usize, - /// Indicates whether the indent guide continues beyond the visible bounds. - pub continues_offscreen: bool, -} - -/// Implements the necessary functionality for rendering indent guides inside a uniform list. -mod uniform_list { - use gpui::UniformListDecoration; - - use super::*; - - impl UniformListDecoration for IndentGuides { - fn compute( - &self, - mut visible_range: Range, - bounds: Bounds, - _scroll_offset: Point, - item_height: Pixels, - item_count: usize, - window: &mut Window, - cx: &mut App, - ) -> AnyElement { - let includes_trailing_indent = visible_range.end < item_count; - // Check if we have entries after the visible range, - // if so extend the visible range so we can fetch a trailing indent, - // which is needed to compute indent guides correctly. - if includes_trailing_indent { - visible_range.end += 1; - } - let Some(ref compute_indents_fn) = self.compute_indents_fn else { - panic!("compute_indents_fn is required for UniformListDecoration"); - }; - let visible_entries = &compute_indents_fn(visible_range.clone(), window, cx); - let indent_guides = compute_indent_guides( - visible_entries, - visible_range.start, - includes_trailing_indent, - ); - self.render_from_layout(indent_guides, bounds, item_height, window, cx) - } - } -} - -/// Implements the necessary functionality for rendering indent guides inside a sticky items. -mod sticky_items { - use crate::StickyItemsDecoration; - - use super::*; - - impl StickyItemsDecoration for IndentGuides { - fn compute( - &self, - indents: &SmallVec<[usize; 8]>, - bounds: Bounds, - _scroll_offset: Point, - item_height: Pixels, - window: &mut Window, - cx: &mut App, - ) -> AnyElement { - let indent_guides = compute_indent_guides(indents, 0, false); - self.render_from_layout(indent_guides, bounds, item_height, window, cx) - } - } -} - -struct IndentGuidesElement { - colors: IndentGuideColors, - indent_guides: Rc>, - on_hovered_indent_guide_click: Option>, -} - -enum IndentGuidesElementPrepaintState { - Static, - Interactive { - hitboxes: Rc>, - on_hovered_indent_guide_click: Rc, - }, -} - -impl Element for IndentGuidesElement { - type RequestLayoutState = (); - type PrepaintState = IndentGuidesElementPrepaintState; - - fn id(&self) -> Option { - None - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - _id: Option<&gpui::GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (gpui::LayoutId, Self::RequestLayoutState) { - (window.request_layout(gpui::Style::default(), [], cx), ()) - } - - fn prepaint( - &mut self, - _id: Option<&gpui::GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - _bounds: Bounds, - _request_layout: &mut Self::RequestLayoutState, - window: &mut Window, - _cx: &mut App, - ) -> Self::PrepaintState { - if let Some(on_hovered_indent_guide_click) = self.on_hovered_indent_guide_click.clone() { - let hitboxes = self - .indent_guides - .as_ref() - .iter() - .map(|guide| { - window - .insert_hitbox(guide.hitbox.unwrap_or(guide.bounds), HitboxBehavior::Normal) - }) - .collect(); - Self::PrepaintState::Interactive { - hitboxes: Rc::new(hitboxes), - on_hovered_indent_guide_click, - } - } else { - Self::PrepaintState::Static - } - } - - fn paint( - &mut self, - _id: Option<&gpui::GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - _bounds: Bounds, - _request_layout: &mut Self::RequestLayoutState, - prepaint: &mut Self::PrepaintState, - window: &mut Window, - _cx: &mut App, - ) { - let current_view = window.current_view(); - - match prepaint { - IndentGuidesElementPrepaintState::Static => { - for indent_guide in self.indent_guides.as_ref() { - let fill_color = if indent_guide.is_active { - self.colors.active - } else { - self.colors.default - }; - - window.paint_quad(fill(indent_guide.bounds, fill_color)); - } - } - IndentGuidesElementPrepaintState::Interactive { - hitboxes, - on_hovered_indent_guide_click, - } => { - window.on_mouse_event({ - let hitboxes = hitboxes.clone(); - let indent_guides = self.indent_guides.clone(); - let on_hovered_indent_guide_click = on_hovered_indent_guide_click.clone(); - move |event: &MouseDownEvent, phase, window, cx| { - if phase == DispatchPhase::Bubble && event.button == MouseButton::Left { - let mut active_hitbox_ix = None; - for (i, hitbox) in hitboxes.iter().enumerate() { - if hitbox.is_hovered(window) { - active_hitbox_ix = Some(i); - break; - } - } - - let Some(active_hitbox_ix) = active_hitbox_ix else { - return; - }; - - let active_indent_guide = &indent_guides[active_hitbox_ix].layout; - on_hovered_indent_guide_click(active_indent_guide, window, cx); - - cx.stop_propagation(); - window.prevent_default(); - } - } - }); - let mut hovered_hitbox_id = None; - for (i, hitbox) in hitboxes.iter().enumerate() { - window.set_cursor_style(gpui::CursorStyle::PointingHand, hitbox); - let indent_guide = &self.indent_guides[i]; - let fill_color = if hitbox.is_hovered(window) { - hovered_hitbox_id = Some(hitbox.id); - self.colors.hover - } else if indent_guide.is_active { - self.colors.active - } else { - self.colors.default - }; - - window.paint_quad(fill(indent_guide.bounds, fill_color)); - } - - window.on_mouse_event({ - let prev_hovered_hitbox_id = hovered_hitbox_id; - let hitboxes = hitboxes.clone(); - move |_: &MouseMoveEvent, phase, window, cx| { - let mut hovered_hitbox_id = None; - for hitbox in hitboxes.as_ref() { - if hitbox.is_hovered(window) { - hovered_hitbox_id = Some(hitbox.id); - break; - } - } - if phase == DispatchPhase::Capture { - // If the hovered hitbox has changed, we need to re-paint the indent guides. - match (prev_hovered_hitbox_id, hovered_hitbox_id) { - (Some(prev_id), Some(id)) => { - if prev_id != id { - cx.notify(current_view) - } - } - (None, Some(_)) => cx.notify(current_view), - (Some(_), None) => cx.notify(current_view), - (None, None) => {} - } - } - } - }); - } - } - } -} - -impl IntoElement for IndentGuidesElement { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - -fn compute_indent_guides( - indents: &[usize], - offset: usize, - includes_trailing_indent: bool, -) -> SmallVec<[IndentGuideLayout; 12]> { - let mut indent_guides = SmallVec::<[IndentGuideLayout; 12]>::new(); - let mut indent_stack = SmallVec::<[IndentGuideLayout; 8]>::new(); - - let mut min_depth = usize::MAX; - for (row, &depth) in indents.iter().enumerate() { - if includes_trailing_indent && row == indents.len() - 1 { - continue; - } - - let current_row = row + offset; - let current_depth = indent_stack.len(); - if depth < min_depth { - min_depth = depth; - } - - match depth.cmp(¤t_depth) { - Ordering::Less => { - for _ in 0..(current_depth - depth) { - if let Some(guide) = indent_stack.pop() { - indent_guides.push(guide); - } - } - } - Ordering::Greater => { - for new_depth in current_depth..depth { - indent_stack.push(IndentGuideLayout { - offset: Point::new(new_depth, current_row), - length: current_row, - continues_offscreen: false, - }); - } - } - _ => {} - } - - for indent in indent_stack.iter_mut() { - indent.length = current_row - indent.offset.y + 1; - } - } - - indent_guides.extend(indent_stack); - - for guide in indent_guides.iter_mut() { - if includes_trailing_indent - && guide.offset.y + guide.length == offset + indents.len().saturating_sub(1) - { - guide.continues_offscreen = indents - .last() - .map(|last_indent| guide.offset.x < *last_indent) - .unwrap_or(false); - } - } - - indent_guides -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_compute_indent_guides() { - fn assert_compute_indent_guides( - input: &[usize], - offset: usize, - includes_trailing_indent: bool, - expected: Vec, - ) { - use std::collections::HashSet; - assert_eq!( - compute_indent_guides(input, offset, includes_trailing_indent) - .into_vec() - .into_iter() - .collect::>(), - expected.into_iter().collect::>(), - ); - } - - assert_compute_indent_guides( - &[0, 1, 2, 2, 1, 0], - 0, - false, - vec![ - IndentGuideLayout { - offset: Point::new(0, 1), - length: 4, - continues_offscreen: false, - }, - IndentGuideLayout { - offset: Point::new(1, 2), - length: 2, - continues_offscreen: false, - }, - ], - ); - - assert_compute_indent_guides( - &[2, 2, 2, 1, 1], - 0, - false, - vec![ - IndentGuideLayout { - offset: Point::new(0, 0), - length: 5, - continues_offscreen: false, - }, - IndentGuideLayout { - offset: Point::new(1, 0), - length: 3, - continues_offscreen: false, - }, - ], - ); - - assert_compute_indent_guides( - &[1, 2, 3, 2, 1], - 0, - false, - vec![ - IndentGuideLayout { - offset: Point::new(0, 0), - length: 5, - continues_offscreen: false, - }, - IndentGuideLayout { - offset: Point::new(1, 1), - length: 3, - continues_offscreen: false, - }, - IndentGuideLayout { - offset: Point::new(2, 2), - length: 1, - continues_offscreen: false, - }, - ], - ); - - assert_compute_indent_guides( - &[0, 1, 0], - 0, - true, - vec![IndentGuideLayout { - offset: Point::new(0, 1), - length: 1, - continues_offscreen: false, - }], - ); - - assert_compute_indent_guides( - &[0, 1, 1], - 0, - true, - vec![IndentGuideLayout { - offset: Point::new(0, 1), - length: 1, - continues_offscreen: true, - }], - ); - assert_compute_indent_guides( - &[0, 1, 2], - 0, - true, - vec![IndentGuideLayout { - offset: Point::new(0, 1), - length: 1, - continues_offscreen: true, - }], - ); - } -} diff --git a/crates/ui/src/components/indicator.rs b/crates/ui/src/components/indicator.rs deleted file mode 100644 index 59d69a068b..0000000000 --- a/crates/ui/src/components/indicator.rs +++ /dev/null @@ -1,177 +0,0 @@ -use super::AnyIcon; -use crate::prelude::*; - -#[derive(Default)] -enum IndicatorKind { - #[default] - Dot, - Bar, - Icon(AnyIcon), -} - -#[derive(IntoElement, RegisterComponent)] -pub struct Indicator { - kind: IndicatorKind, - border_color: Option, - pub color: Color, -} - -impl Indicator { - pub fn dot() -> Self { - Self { - kind: IndicatorKind::Dot, - border_color: None, - color: Color::Default, - } - } - - pub fn bar() -> Self { - Self { - kind: IndicatorKind::Bar, - border_color: None, - - color: Color::Default, - } - } - - pub fn icon(icon: impl Into) -> Self { - Self { - kind: IndicatorKind::Icon(icon.into()), - border_color: None, - - color: Color::Default, - } - } - - pub fn color(mut self, color: Color) -> Self { - self.color = color; - self - } - - pub fn border_color(mut self, color: Color) -> Self { - self.border_color = Some(color); - self - } -} - -impl RenderOnce for Indicator { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let container = div().flex_none(); - let container = if let Some(border_color) = self.border_color { - if matches!(self.kind, IndicatorKind::Dot | IndicatorKind::Bar) { - container.border_1().border_color(border_color.color(cx)) - } else { - container - } - } else { - container - }; - - match self.kind { - IndicatorKind::Icon(icon) => container - .child(icon.map(|icon| icon.custom_size(rems_from_px(8.)).color(self.color))), - IndicatorKind::Dot => container - .w_1p5() - .h_1p5() - .rounded_full() - .bg(self.color.color(cx)), - IndicatorKind::Bar => container - .w_full() - .h_1p5() - .rounded_t_sm() - .bg(self.color.color(cx)), - } - } -} - -impl Component for Indicator { - fn scope() -> ComponentScope { - ComponentScope::Status - } - - fn description() -> Option<&'static str> { - Some( - "Visual indicators used to represent status, notifications, or draw attention to specific elements.", - ) - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Dot Indicators", - vec![ - single_example("Default", Indicator::dot().into_any_element()), - single_example( - "Success", - Indicator::dot().color(Color::Success).into_any_element(), - ), - single_example( - "Warning", - Indicator::dot().color(Color::Warning).into_any_element(), - ), - single_example( - "Error", - Indicator::dot().color(Color::Error).into_any_element(), - ), - single_example( - "With Border", - Indicator::dot() - .color(Color::Accent) - .border_color(Color::Default) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Bar Indicators", - vec![ - single_example("Default", Indicator::bar().into_any_element()), - single_example( - "Success", - Indicator::bar().color(Color::Success).into_any_element(), - ), - single_example( - "Warning", - Indicator::bar().color(Color::Warning).into_any_element(), - ), - single_example( - "Error", - Indicator::bar().color(Color::Error).into_any_element(), - ), - ], - ), - example_group_with_title( - "Icon Indicators", - vec![ - single_example( - "Default", - Indicator::icon(Icon::new(IconName::Circle)).into_any_element(), - ), - single_example( - "Success", - Indicator::icon(Icon::new(IconName::Check)) - .color(Color::Success) - .into_any_element(), - ), - single_example( - "Warning", - Indicator::icon(Icon::new(IconName::Warning)) - .color(Color::Warning) - .into_any_element(), - ), - single_example( - "Error", - Indicator::icon(Icon::new(IconName::Close)) - .color(Color::Error) - .into_any_element(), - ), - ], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/keybinding.rs b/crates/ui/src/components/keybinding.rs deleted file mode 100644 index e22669995d..0000000000 --- a/crates/ui/src/components/keybinding.rs +++ /dev/null @@ -1,740 +0,0 @@ -use std::rc::Rc; - -use crate::PlatformStyle; -use crate::{Icon, IconName, IconSize, h_flex, prelude::*}; -use gpui::{ - Action, AnyElement, App, FocusHandle, Global, IntoElement, KeybindingKeystroke, Keystroke, - Modifiers, Window, relative, -}; -use itertools::Itertools; -use settings::KeybindSource; - -#[derive(Debug)] -enum Source { - Action { - action: Box, - focus_handle: Option, - }, - Keystrokes { - /// A keybinding consists of a set of keystrokes, - /// where each keystroke is a key and a set of modifier keys. - /// More than one keystroke produces a chord. - /// - /// This should always contain at least one keystroke. - keystrokes: Rc<[KeybindingKeystroke]>, - }, -} - -impl Clone for Source { - fn clone(&self) -> Self { - match self { - Source::Action { - action, - focus_handle, - } => Source::Action { - action: action.boxed_clone(), - focus_handle: focus_handle.clone(), - }, - Source::Keystrokes { keystrokes } => Source::Keystrokes { - keystrokes: keystrokes.clone(), - }, - } - } -} - -#[derive(Clone, Debug, IntoElement, RegisterComponent)] -pub struct KeyBinding { - source: Source, - size: Option, - /// The [`PlatformStyle`] to use when displaying this keybinding. - platform_style: PlatformStyle, - /// Determines whether the keybinding is meant for vim mode. - vim_mode: bool, - /// Indicates whether the keybinding is currently disabled. - disabled: bool, -} - -struct VimStyle(bool); -impl Global for VimStyle {} - -impl KeyBinding { - /// Returns the highest precedence keybinding for an action. This is the last binding added to - /// the keymap. User bindings are added after built-in bindings so that they take precedence. - pub fn for_action(action: &dyn Action, cx: &App) -> Self { - Self::new(action, None, cx) - } - - /// Like `for_action`, but lets you specify the context from which keybindings are matched. - pub fn for_action_in(action: &dyn Action, focus: &FocusHandle, cx: &App) -> Self { - Self::new(action, Some(focus.clone()), cx) - } - pub fn has_binding(&self, window: &Window) -> bool { - match &self.source { - Source::Action { - action, - focus_handle: Some(focus), - } => window - .highest_precedence_binding_for_action_in(action.as_ref(), focus) - .or_else(|| window.highest_precedence_binding_for_action(action.as_ref())) - .is_some(), - _ => false, - } - } - - pub fn set_vim_mode(cx: &mut App, enabled: bool) { - cx.set_global(VimStyle(enabled)); - } - - fn is_vim_mode(cx: &App) -> bool { - cx.try_global::().is_some_and(|g| g.0) - } - - pub fn new(action: &dyn Action, focus_handle: Option, cx: &App) -> Self { - Self { - source: Source::Action { - action: action.boxed_clone(), - focus_handle, - }, - size: None, - vim_mode: KeyBinding::is_vim_mode(cx), - platform_style: PlatformStyle::platform(), - disabled: false, - } - } - - pub fn from_keystrokes(keystrokes: Rc<[KeybindingKeystroke]>, source: KeybindSource) -> Self { - Self { - source: Source::Keystrokes { keystrokes }, - size: None, - vim_mode: source == KeybindSource::Vim, - platform_style: PlatformStyle::platform(), - disabled: false, - } - } - - /// Sets the [`PlatformStyle`] for this [`KeyBinding`]. - pub fn platform_style(mut self, platform_style: PlatformStyle) -> Self { - self.platform_style = platform_style; - self - } - - /// Sets the size for this [`KeyBinding`]. - pub fn size(mut self, size: impl Into) -> Self { - self.size = Some(size.into()); - self - } - - /// Sets whether this keybinding is currently disabled. - /// Disabled keybinds will be rendered in a dimmed state. - pub fn disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self - } -} - -fn render_key( - key: &str, - color: Option, - platform_style: PlatformStyle, - size: impl Into>, -) -> AnyElement { - let key_icon = icon_for_key(key, platform_style); - match key_icon { - Some(icon) => KeyIcon::new(icon, color).size(size).into_any_element(), - None => { - let key = util::capitalize(key); - Key::new(&key, color).size(size).into_any_element() - } - } -} - -impl RenderOnce for KeyBinding { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let render_keybinding = |keystrokes: &[KeybindingKeystroke]| { - let color = self.disabled.then_some(Color::Disabled); - - h_flex() - .debug_selector(|| { - format!( - "KEY_BINDING-{}", - keystrokes - .iter() - .map(|k| k.key().to_string()) - .collect::>() - .join(" ") - ) - }) - .gap(DynamicSpacing::Base04.rems(cx)) - .flex_none() - .children(keystrokes.iter().map(|keystroke| { - h_flex() - .flex_none() - .py_0p5() - .rounded_xs() - .text_color(cx.theme().colors().text_muted) - .children(render_keybinding_keystroke( - keystroke, - color, - self.size, - PlatformStyle::platform(), - self.vim_mode, - )) - })) - .into_any_element() - }; - - match self.source { - Source::Action { - action, - focus_handle, - } => focus_handle - .or_else(|| window.focused(cx)) - .and_then(|focus| { - window.highest_precedence_binding_for_action_in(action.as_ref(), &focus) - }) - .or_else(|| window.highest_precedence_binding_for_action(action.as_ref())) - .map(|binding| render_keybinding(binding.keystrokes())), - Source::Keystrokes { keystrokes } => Some(render_keybinding(keystrokes.as_ref())), - } - .unwrap_or_else(|| gpui::Empty.into_any_element()) - } -} - -pub fn render_keybinding_keystroke( - keystroke: &KeybindingKeystroke, - color: Option, - size: impl Into>, - platform_style: PlatformStyle, - vim_mode: bool, -) -> Vec { - let use_text = vim_mode - || matches!( - platform_style, - PlatformStyle::Linux | PlatformStyle::Windows - ); - let size = size.into(); - - if use_text { - let element = Key::new( - keystroke_text( - keystroke.modifiers(), - keystroke.key(), - platform_style, - vim_mode, - ), - color, - ) - .size(size) - .into_any_element(); - vec![element] - } else { - let mut elements = Vec::new(); - elements.extend(render_modifiers( - keystroke.modifiers(), - platform_style, - color, - size, - true, - )); - elements.push(render_key(keystroke.key(), color, platform_style, size)); - elements - } -} - -fn icon_for_key(key: &str, platform_style: PlatformStyle) -> Option { - match key { - "left" => Some(IconName::ArrowLeft), - "right" => Some(IconName::ArrowRight), - "up" => Some(IconName::ArrowUp), - "down" => Some(IconName::ArrowDown), - "backspace" => Some(IconName::Backspace), - "delete" => Some(IconName::Backspace), - "return" => Some(IconName::Return), - "enter" => Some(IconName::Return), - "tab" => Some(IconName::Tab), - "space" => Some(IconName::Space), - "escape" => Some(IconName::Escape), - "pagedown" => Some(IconName::PageDown), - "pageup" => Some(IconName::PageUp), - "shift" if platform_style == PlatformStyle::Mac => Some(IconName::Shift), - "control" if platform_style == PlatformStyle::Mac => Some(IconName::Control), - "platform" if platform_style == PlatformStyle::Mac => Some(IconName::Command), - "function" if platform_style == PlatformStyle::Mac => Some(IconName::Control), - "alt" if platform_style == PlatformStyle::Mac => Some(IconName::Option), - _ => None, - } -} - -pub fn render_modifiers( - modifiers: &Modifiers, - platform_style: PlatformStyle, - color: Option, - size: Option, - trailing_separator: bool, -) -> impl Iterator { - #[derive(Clone)] - enum KeyOrIcon { - Key(&'static str), - Plus, - Icon(IconName), - } - - struct Modifier { - enabled: bool, - mac: KeyOrIcon, - linux: KeyOrIcon, - windows: KeyOrIcon, - } - - let table = { - use KeyOrIcon::*; - - [ - Modifier { - enabled: modifiers.function, - mac: Icon(IconName::Control), - linux: Key("Fn"), - windows: Key("Fn"), - }, - Modifier { - enabled: modifiers.control, - mac: Icon(IconName::Control), - linux: Key("Ctrl"), - windows: Key("Ctrl"), - }, - Modifier { - enabled: modifiers.alt, - mac: Icon(IconName::Option), - linux: Key("Alt"), - windows: Key("Alt"), - }, - Modifier { - enabled: modifiers.platform, - mac: Icon(IconName::Command), - linux: Key("Super"), - windows: Key("Win"), - }, - Modifier { - enabled: modifiers.shift, - mac: Icon(IconName::Shift), - linux: Key("Shift"), - windows: Key("Shift"), - }, - ] - }; - - let filtered = table - .into_iter() - .filter(|modifier| modifier.enabled) - .collect::>(); - - let platform_keys = filtered - .into_iter() - .map(move |modifier| match platform_style { - PlatformStyle::Mac => Some(modifier.mac), - PlatformStyle::Linux => Some(modifier.linux), - PlatformStyle::Windows => Some(modifier.windows), - }); - - let separator = match platform_style { - PlatformStyle::Mac => None, - PlatformStyle::Linux => Some(KeyOrIcon::Plus), - PlatformStyle::Windows => Some(KeyOrIcon::Plus), - }; - - let platform_keys = itertools::intersperse(platform_keys, separator.clone()); - - platform_keys - .chain(if modifiers.modified() && trailing_separator { - Some(separator) - } else { - None - }) - .flatten() - .map(move |key_or_icon| match key_or_icon { - KeyOrIcon::Key(key) => Key::new(key, color).size(size).into_any_element(), - KeyOrIcon::Icon(icon) => KeyIcon::new(icon, color).size(size).into_any_element(), - KeyOrIcon::Plus => "+".into_any_element(), - }) -} - -#[derive(IntoElement)] -pub struct Key { - key: SharedString, - color: Option, - size: Option, -} - -impl RenderOnce for Key { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let single_char = self.key.len() == 1; - let size = self - .size - .unwrap_or_else(|| TextSize::default().rems(cx).into()); - - div() - .py_0() - .map(|this| { - if single_char { - this.w(size).flex().flex_none().justify_center() - } else { - this.px_0p5() - } - }) - .h(size) - .text_size(size) - .line_height(relative(1.)) - .text_color(self.color.unwrap_or(Color::Muted).color(cx)) - .child(self.key) - } -} - -impl Key { - pub fn new(key: impl Into, color: Option) -> Self { - Self { - key: key.into(), - color, - size: None, - } - } - - pub fn size(mut self, size: impl Into>) -> Self { - self.size = size.into(); - self - } -} - -#[derive(IntoElement)] -pub struct KeyIcon { - icon: IconName, - color: Option, - size: Option, -} - -impl RenderOnce for KeyIcon { - fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement { - let size = self.size.unwrap_or(IconSize::Small.rems().into()); - - Icon::new(self.icon) - .size(IconSize::Custom(size.to_rems(window.rem_size()))) - .color(self.color.unwrap_or(Color::Muted)) - } -} - -impl KeyIcon { - pub fn new(icon: IconName, color: Option) -> Self { - Self { - icon, - color, - size: None, - } - } - - pub fn size(mut self, size: impl Into>) -> Self { - self.size = size.into(); - self - } -} - -/// 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)) -} - -pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String { - let platform_style = PlatformStyle::platform(); - let vim_enabled = KeyBinding::is_vim_mode(cx); - keystrokes - .iter() - .map(|keystroke| { - keystroke_text( - &keystroke.modifiers, - &keystroke.key, - platform_style, - vim_enabled, - ) - }) - .join(" ") -} - -pub fn text_for_keybinding_keystrokes(keystrokes: &[KeybindingKeystroke], cx: &App) -> String { - let platform_style = PlatformStyle::platform(); - let vim_enabled = KeyBinding::is_vim_mode(cx); - keystrokes - .iter() - .map(|keystroke| { - keystroke_text( - keystroke.modifiers(), - keystroke.key(), - platform_style, - vim_enabled, - ) - }) - .join(" ") -} - -pub fn text_for_keystroke(modifiers: &Modifiers, key: &str, cx: &App) -> String { - let platform_style = PlatformStyle::platform(); - keystroke_text(modifiers, key, platform_style, KeyBinding::is_vim_mode(cx)) -} - -/// Returns a textual representation of the given [`Keystroke`]. -fn keystroke_text( - modifiers: &Modifiers, - key: &str, - platform_style: PlatformStyle, - vim_mode: bool, -) -> String { - let mut text = String::new(); - let delimiter = '-'; - - if modifiers.function { - match vim_mode { - false => text.push_str("Fn"), - true => text.push_str("fn"), - } - - text.push(delimiter); - } - - if modifiers.control { - match (platform_style, vim_mode) { - (PlatformStyle::Mac, false) => text.push_str("Control"), - (PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Ctrl"), - (_, true) => text.push_str("ctrl"), - } - - text.push(delimiter); - } - - if modifiers.platform { - match (platform_style, vim_mode) { - (PlatformStyle::Mac, false) => text.push_str("Command"), - (PlatformStyle::Mac, true) => text.push_str("cmd"), - (PlatformStyle::Linux, false) => text.push_str("Super"), - (PlatformStyle::Linux, true) => text.push_str("super"), - (PlatformStyle::Windows, false) => text.push_str("Win"), - (PlatformStyle::Windows, true) => text.push_str("win"), - } - - text.push(delimiter); - } - - if modifiers.alt { - match (platform_style, vim_mode) { - (PlatformStyle::Mac, false) => text.push_str("Option"), - (PlatformStyle::Mac, true) => text.push_str("option"), - (PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Alt"), - (_, true) => text.push_str("alt"), - } - - text.push(delimiter); - } - - if modifiers.shift { - match (platform_style, vim_mode) { - (_, false) => text.push_str("Shift"), - (_, true) => text.push_str("shift"), - } - text.push(delimiter); - } - - if vim_mode { - text.push_str(key) - } else { - let key = match key { - "pageup" => "PageUp", - "pagedown" => "PageDown", - key => &util::capitalize(key), - }; - text.push_str(key); - } - - text -} - -impl Component for KeyBinding { - fn scope() -> ComponentScope { - ComponentScope::Typography - } - - fn name() -> &'static str { - "KeyBinding" - } - - fn description() -> Option<&'static str> { - Some( - "A component that displays a key binding, supporting different platform styles and vim mode.", - ) - } - - // fn preview(_window: &mut Window, cx: &mut App) -> Option { - // Some( - // v_flex() - // .gap_6() - // .children(vec![ - // example_group_with_title( - // "Basic Usage", - // vec![ - // single_example( - // "Default", - // KeyBinding::new_from_gpui( - // gpui::KeyBinding::new("ctrl-s", gpui::NoAction, None), - // cx, - // ) - // .into_any_element(), - // ), - // single_example( - // "Mac Style", - // KeyBinding::new_from_gpui( - // gpui::KeyBinding::new("cmd-s", gpui::NoAction, None), - // cx, - // ) - // .platform_style(PlatformStyle::Mac) - // .into_any_element(), - // ), - // single_example( - // "Windows Style", - // KeyBinding::new_from_gpui( - // gpui::KeyBinding::new("ctrl-s", gpui::NoAction, None), - // cx, - // ) - // .platform_style(PlatformStyle::Windows) - // .into_any_element(), - // ), - // ], - // ), - // example_group_with_title( - // "Vim Mode", - // vec![single_example( - // "Vim Mode Enabled", - // KeyBinding::new_from_gpui( - // gpui::KeyBinding::new("dd", gpui::NoAction, None), - // cx, - // ) - // .vim_mode(true) - // .into_any_element(), - // )], - // ), - // example_group_with_title( - // "Complex Bindings", - // vec![ - // single_example( - // "Multiple Keys", - // KeyBinding::new_from_gpui( - // gpui::KeyBinding::new("ctrl-k ctrl-b", gpui::NoAction, None), - // cx, - // ) - // .into_any_element(), - // ), - // single_example( - // "With Shift", - // KeyBinding::new_from_gpui( - // gpui::KeyBinding::new("shift-cmd-p", gpui::NoAction, None), - // cx, - // ) - // .into_any_element(), - // ), - // ], - // ), - // ]) - // .into_any_element(), - // ) - // } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_text_for_keystroke() { - let keystroke = Keystroke::parse("cmd-c").unwrap(); - assert_eq!( - keystroke_text( - &keystroke.modifiers, - &keystroke.key, - PlatformStyle::Mac, - false - ), - "Command-C".to_string() - ); - assert_eq!( - keystroke_text( - &keystroke.modifiers, - &keystroke.key, - PlatformStyle::Linux, - false - ), - "Super-C".to_string() - ); - assert_eq!( - keystroke_text( - &keystroke.modifiers, - &keystroke.key, - PlatformStyle::Windows, - false - ), - "Win-C".to_string() - ); - - let keystroke = Keystroke::parse("ctrl-alt-delete").unwrap(); - assert_eq!( - keystroke_text( - &keystroke.modifiers, - &keystroke.key, - PlatformStyle::Mac, - false - ), - "Control-Option-Delete".to_string() - ); - assert_eq!( - keystroke_text( - &keystroke.modifiers, - &keystroke.key, - PlatformStyle::Linux, - false - ), - "Ctrl-Alt-Delete".to_string() - ); - assert_eq!( - keystroke_text( - &keystroke.modifiers, - &keystroke.key, - PlatformStyle::Windows, - false - ), - "Ctrl-Alt-Delete".to_string() - ); - - let keystroke = Keystroke::parse("shift-pageup").unwrap(); - assert_eq!( - keystroke_text( - &keystroke.modifiers, - &keystroke.key, - PlatformStyle::Mac, - false - ), - "Shift-PageUp".to_string() - ); - assert_eq!( - keystroke_text( - &keystroke.modifiers, - &keystroke.key, - PlatformStyle::Linux, - false, - ), - "Shift-PageUp".to_string() - ); - assert_eq!( - keystroke_text( - &keystroke.modifiers, - &keystroke.key, - PlatformStyle::Windows, - false - ), - "Shift-PageUp".to_string() - ); - } -} diff --git a/crates/ui/src/components/keybinding_hint.rs b/crates/ui/src/components/keybinding_hint.rs deleted file mode 100644 index c998e29f0e..0000000000 --- a/crates/ui/src/components/keybinding_hint.rs +++ /dev/null @@ -1,341 +0,0 @@ -use crate::KeyBinding; -use crate::prelude::*; -use gpui::{AnyElement, App, BoxShadow, FontStyle, Hsla, IntoElement, Window, point}; -use theme::Appearance; - -/// Represents a hint for a keybinding, optionally with a prefix and suffix. -/// -/// This struct allows for the creation and customization of a keybinding hint, -/// which can be used to display keyboard shortcuts or commands in a user interface. -/// -/// # Examples -/// -/// ```no_run -/// use gpui::{App, Hsla, KeybindingKeystroke, Keystroke}; -/// use ui::prelude::*; -/// use ui::{KeyBinding, KeybindingHint}; -/// use settings::KeybindSource; -/// -/// # fn example(cx: &App) { -/// let hint = KeybindingHint::new( -/// KeyBinding::from_keystrokes(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-s").unwrap())].into(), KeybindSource::Base), -/// Hsla::black() -/// ) -/// .prefix("Save:") -/// .size(Pixels::from(14.0)); -/// # } -/// ``` -#[derive(Debug, IntoElement, RegisterComponent)] -pub struct KeybindingHint { - prefix: Option, - suffix: Option, - keybinding: KeyBinding, - size: Option, - background_color: Hsla, -} - -impl KeybindingHint { - /// Creates a new `KeybindingHint` with the specified keybinding. - /// - /// This method initializes a new `KeybindingHint` instance with the given keybinding, - /// setting all other fields to their default values. - /// - /// # Examples - /// - /// ```no_run - /// use gpui::{App, Hsla, KeybindingKeystroke, Keystroke}; - /// use ui::prelude::*; - /// use ui::{KeyBinding, KeybindingHint}; - /// use settings::KeybindSource; - /// - /// # fn example(cx: &App) { - /// let hint = KeybindingHint::new( - /// KeyBinding::from_keystrokes(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-c").unwrap())].into(), KeybindSource::Base), - /// Hsla::black() - /// ); - /// # } - /// ``` - pub fn new(keybinding: KeyBinding, background_color: Hsla) -> Self { - Self { - prefix: None, - suffix: None, - keybinding, - size: None, - background_color, - } - } - - /// Creates a new `KeybindingHint` with a prefix and keybinding. - /// - /// This method initializes a new `KeybindingHint` instance with the given prefix and keybinding, - /// setting all other fields to their default values. - /// - /// # Examples - /// - /// ```no_run - /// use gpui::{App, Hsla, KeybindingKeystroke, Keystroke}; - /// use ui::prelude::*; - /// use ui::{KeyBinding, KeybindingHint}; - /// use settings::KeybindSource; - /// - /// # fn example(cx: &App) { - /// let hint = KeybindingHint::with_prefix( - /// "Copy:", - /// KeyBinding::from_keystrokes(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-c").unwrap())].into(), KeybindSource::Base), - /// Hsla::black() - /// ); - /// # } - /// ``` - pub fn with_prefix( - prefix: impl Into, - keybinding: KeyBinding, - background_color: Hsla, - ) -> Self { - Self { - prefix: Some(prefix.into()), - suffix: None, - keybinding, - size: None, - background_color, - } - } - - /// Creates a new `KeybindingHint` with a keybinding and suffix. - /// - /// This method initializes a new `KeybindingHint` instance with the given keybinding and suffix, - /// setting all other fields to their default values. - /// - /// # Examples - /// - /// ```no_run - /// use gpui::{App, Hsla, KeybindingKeystroke, Keystroke}; - /// use ui::prelude::*; - /// use ui::{KeyBinding, KeybindingHint}; - /// use settings::KeybindSource; - /// - /// # fn example(cx: &App) { - /// let hint = KeybindingHint::with_suffix( - /// KeyBinding::from_keystrokes(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-v").unwrap())].into(), KeybindSource::Base), - /// "Paste", - /// Hsla::black() - /// ); - /// # } - /// ``` - pub fn with_suffix( - keybinding: KeyBinding, - suffix: impl Into, - background_color: Hsla, - ) -> Self { - Self { - prefix: None, - suffix: Some(suffix.into()), - keybinding, - size: None, - background_color, - } - } - - /// Sets the prefix for the keybinding hint. - /// - /// This method allows adding or changing the prefix text that appears before the keybinding. - /// - /// # Examples - /// - /// ```no_run - /// use gpui::{App, Hsla, KeybindingKeystroke, Keystroke}; - /// use ui::prelude::*; - /// use ui::{KeyBinding, KeybindingHint}; - /// use settings::KeybindSource; - /// - /// # fn example(cx: &App) { - /// let hint = KeybindingHint::new( - /// KeyBinding::from_keystrokes(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-x").unwrap())].into(), KeybindSource::Base), - /// Hsla::black() - /// ) - /// .prefix("Cut:"); - /// # } - /// ``` - pub fn prefix(mut self, prefix: impl Into) -> Self { - self.prefix = Some(prefix.into()); - self - } - - /// Sets the suffix for the keybinding hint. - /// - /// This method allows adding or changing the suffix text that appears after the keybinding. - /// - /// # Examples - /// - /// ```no_run - /// use gpui::{App, Hsla, KeybindingKeystroke, Keystroke}; - /// use ui::prelude::*; - /// use ui::{KeyBinding, KeybindingHint}; - /// use settings::KeybindSource; - /// - /// # fn example(cx: &App) { - /// let hint = KeybindingHint::new( - /// KeyBinding::from_keystrokes(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-f").unwrap())].into(), KeybindSource::Base), - /// Hsla::black() - /// ) - /// .suffix("Find"); - /// # } - /// ``` - pub fn suffix(mut self, suffix: impl Into) -> Self { - self.suffix = Some(suffix.into()); - self - } - - /// Sets the size of the keybinding hint. - /// - /// This method allows specifying the size of the keybinding hint in pixels. - /// - /// # Examples - /// - /// ```no_run - /// use gpui::{App, Hsla, KeybindingKeystroke, Keystroke}; - /// use ui::prelude::*; - /// use ui::{KeyBinding, KeybindingHint}; - /// use settings::KeybindSource; - /// - /// # fn example(cx: &App) { - /// let hint = KeybindingHint::new( - /// KeyBinding::from_keystrokes(vec![KeybindingKeystroke::from_keystroke(Keystroke::parse("ctrl-z").unwrap())].into(), KeybindSource::Base), - /// Hsla::black() - /// ) - /// .size(Pixels::from(16.0)); - /// # } - /// ``` - pub fn size(mut self, size: impl Into>) -> Self { - self.size = size.into(); - self - } -} - -impl RenderOnce for KeybindingHint { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let colors = cx.theme().colors(); - let is_light = cx.theme().appearance() == Appearance::Light; - - let border_color = - self.background_color - .blend(colors.text.alpha(if is_light { 0.08 } else { 0.16 })); - - let bg_color = self - .background_color - .blend(colors.text_accent.alpha(if is_light { 0.05 } else { 0.1 })); - - let shadow_color = colors.text.alpha(if is_light { 0.04 } else { 0.08 }); - - let size = self - .size - .unwrap_or(TextSize::Small.rems(cx).to_pixels(window.rem_size())); - - let kb_size = size - px(2.0); - - let mut base = h_flex(); - - base.text_style() - .get_or_insert_with(Default::default) - .font_style = Some(FontStyle::Italic); - - base.gap_1() - .font_buffer(cx) - .text_size(size) - .text_color(colors.text_disabled) - .children(self.prefix) - .child( - h_flex() - .rounded_sm() - .px_0p5() - .mr_0p5() - .border_1() - .border_color(border_color) - .bg(bg_color) - .shadow(vec![BoxShadow { - color: shadow_color, - offset: point(px(0.), px(1.)), - blur_radius: px(0.), - spread_radius: px(0.), - }]) - .child(self.keybinding.size(rems_from_px(kb_size))), - ) - .children(self.suffix) - } -} - -impl Component for KeybindingHint { - fn scope() -> ComponentScope { - ComponentScope::DataDisplay - } - - fn description() -> Option<&'static str> { - Some("Displays a keyboard shortcut hint with optional prefix and suffix text") - } - - fn preview(_window: &mut Window, cx: &mut App) -> Option { - let enter = KeyBinding::for_action(&menu::Confirm, cx); - - let bg_color = cx.theme().colors().surface_background; - - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Basic", - vec![ - single_example( - "With Prefix", - KeybindingHint::with_prefix( - "Go to Start:", - enter.clone(), - bg_color, - ) - .into_any_element(), - ), - single_example( - "With Suffix", - KeybindingHint::with_suffix(enter.clone(), "Go to End", bg_color) - .into_any_element(), - ), - single_example( - "With Prefix and Suffix", - KeybindingHint::new(enter.clone(), bg_color) - .prefix("Confirm:") - .suffix("Execute selected action") - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Sizes", - vec![ - single_example( - "Small", - KeybindingHint::new(enter.clone(), bg_color) - .size(Pixels::from(12.0)) - .prefix("Small:") - .into_any_element(), - ), - single_example( - "Medium", - KeybindingHint::new(enter.clone(), bg_color) - .size(Pixels::from(16.0)) - .suffix("Medium") - .into_any_element(), - ), - single_example( - "Large", - KeybindingHint::new(enter, bg_color) - .size(Pixels::from(20.0)) - .prefix("Large:") - .suffix("Size") - .into_any_element(), - ), - ], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/label.rs b/crates/ui/src/components/label.rs deleted file mode 100644 index dc830559ca..0000000000 --- a/crates/ui/src/components/label.rs +++ /dev/null @@ -1,11 +0,0 @@ -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/highlighted_label.rs b/crates/ui/src/components/label/highlighted_label.rs deleted file mode 100644 index 840bba7b17..0000000000 --- a/crates/ui/src/components/label/highlighted_label.rs +++ /dev/null @@ -1,242 +0,0 @@ -use std::ops::Range; - -use gpui::{FontWeight, HighlightStyle, StyledText}; - -use crate::{LabelCommon, LabelLike, LabelSize, LineHeightStyle, prelude::*}; - -#[derive(IntoElement, RegisterComponent)] -pub struct HighlightedLabel { - base: LabelLike, - label: SharedString, - highlight_indices: Vec, -} - -impl HighlightedLabel { - /// Constructs a label with the given characters highlighted. - /// Characters are identified by UTF-8 byte position. - pub fn new(label: impl Into, highlight_indices: Vec) -> Self { - let label = label.into(); - for &run in &highlight_indices { - assert!( - label.is_char_boundary(run), - "highlight index {run} is not a valid UTF-8 boundary" - ); - } - Self { - base: LabelLike::new(), - label, - highlight_indices, - } - } - - pub fn text(&self) -> &str { - self.label.as_str() - } - - pub fn highlight_indices(&self) -> &[usize] { - &self.highlight_indices - } -} - -impl LabelCommon for HighlightedLabel { - 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 - } -} - -pub fn highlight_ranges( - text: &str, - indices: &[usize], - style: HighlightStyle, -) -> Vec<(Range, HighlightStyle)> { - let mut highlight_indices = indices.iter().copied().peekable(); - let mut highlights: Vec<(Range, HighlightStyle)> = Vec::new(); - - while let Some(start_ix) = highlight_indices.next() { - let mut end_ix = start_ix; - - loop { - end_ix += text[end_ix..].chars().next().map_or(0, |c| c.len_utf8()); - if highlight_indices.next_if(|&ix| ix == end_ix).is_none() { - break; - } - } - - highlights.push((start_ix..end_ix, style)); - } - - highlights -} - -impl RenderOnce for HighlightedLabel { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let highlight_color = cx.theme().colors().text_accent; - - let highlights = highlight_ranges( - &self.label, - &self.highlight_indices, - HighlightStyle { - color: Some(highlight_color), - ..Default::default() - }, - ); - - let mut text_style = window.text_style(); - text_style.color = self.base.color.color(cx); - - self.base - .child(StyledText::new(self.label).with_default_highlights(&text_style, highlights)) - } -} - -impl Component for HighlightedLabel { - fn scope() -> ComponentScope { - ComponentScope::Typography - } - - fn name() -> &'static str { - "HighlightedLabel" - } - - fn description() -> Option<&'static str> { - Some("A label with highlighted characters based on specified indices.") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Basic Usage", - vec![ - single_example( - "Default", - HighlightedLabel::new("Highlighted Text", vec![0, 1, 2, 3]).into_any_element(), - ), - single_example( - "Custom Color", - HighlightedLabel::new("Colored Highlight", vec![0, 1, 7, 8, 9]) - .color(Color::Accent) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Styles", - vec![ - single_example( - "Bold", - HighlightedLabel::new("Bold Highlight", vec![0, 1, 2, 3]) - .weight(FontWeight::BOLD) - .into_any_element(), - ), - single_example( - "Italic", - HighlightedLabel::new("Italic Highlight", vec![0, 1, 6, 7, 8]) - .italic() - .into_any_element(), - ), - single_example( - "Underline", - HighlightedLabel::new("Underlined Highlight", vec![0, 1, 10, 11, 12]) - .underline() - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Sizes", - vec![ - single_example( - "Small", - HighlightedLabel::new("Small Highlight", vec![0, 1, 5, 6, 7]) - .size(LabelSize::Small) - .into_any_element(), - ), - single_example( - "Large", - HighlightedLabel::new("Large Highlight", vec![0, 1, 5, 6, 7]) - .size(LabelSize::Large) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Special Cases", - vec![ - single_example( - "Single Line", - HighlightedLabel::new("Single Line Highlight\nWith Newline", vec![0, 1, 7, 8, 9]) - .single_line() - .into_any_element(), - ), - single_example( - "Truncate", - HighlightedLabel::new("This is a very long text that should be truncated with highlights", vec![0, 1, 2, 3, 4, 5]) - .truncate() - .into_any_element(), - ), - ], - ), - ]) - .into_any_element() - ) - } -} diff --git a/crates/ui/src/components/label/label.rs b/crates/ui/src/components/label/label.rs deleted file mode 100644 index 49e2de94a1..0000000000 --- a/crates/ui/src/components/label/label.rs +++ /dev/null @@ -1,266 +0,0 @@ -use crate::{LabelLike, prelude::*}; -use gpui::StyleRefinement; - -/// A struct representing a label element in the UI. -/// -/// The `Label` struct stores the label text and common properties for a label element. -/// It provides methods for modifying these properties. -/// -/// # Examples -/// -/// ``` -/// use ui::prelude::*; -/// -/// Label::new("Hello, World!"); -/// ``` -/// -/// **A colored label**, for example labeling a dangerous action: -/// -/// ``` -/// use ui::prelude::*; -/// -/// let my_label = Label::new("Delete").color(Color::Error); -/// ``` -/// -/// **A label with a strikethrough**, for example labeling something that has been deleted: -/// -/// ``` -/// use ui::prelude::*; -/// -/// let my_label = Label::new("Deleted").strikethrough(); -/// ``` -#[derive(IntoElement, RegisterComponent)] -pub struct Label { - base: LabelLike, - label: SharedString, -} - -impl Label { - /// Creates a new [`Label`] with the given text. - /// - /// # Examples - /// - /// ``` - /// use ui::prelude::*; - /// - /// let my_label = Label::new("Hello, World!"); - /// ``` - pub fn new(label: impl Into) -> Self { - Self { - base: LabelLike::new(), - label: label.into(), - } - } - - /// Sets the text of the [`Label`]. - pub fn set_text(&mut self, text: impl Into) { - self.label = text.into(); - } -} - -// Style methods. -impl Label { - fn style(&mut self) -> &mut StyleRefinement { - self.base.base.style() - } - - gpui::margin_style_methods!({ - visibility: pub - }); -} - -impl LabelCommon for Label { - /// Sets the size of the label using a [`LabelSize`]. - /// - /// # Examples - /// - /// ``` - /// use ui::prelude::*; - /// - /// let my_label = Label::new("Hello, World!").size(LabelSize::Small); - /// ``` - fn size(mut self, size: LabelSize) -> Self { - self.base = self.base.size(size); - self - } - - /// Sets the weight of the label using a [`FontWeight`]. - /// - /// # Examples - /// - /// ``` - /// use gpui::FontWeight; - /// use ui::prelude::*; - /// - /// let my_label = Label::new("Hello, World!").weight(FontWeight::BOLD); - /// ``` - fn weight(mut self, weight: gpui::FontWeight) -> Self { - self.base = self.base.weight(weight); - self - } - - /// Sets the line height style of the label using a [`LineHeightStyle`]. - /// - /// # Examples - /// - /// ``` - /// use ui::prelude::*; - /// - /// let my_label = Label::new("Hello, World!").line_height_style(LineHeightStyle::UiLabel); - /// ``` - fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self { - self.base = self.base.line_height_style(line_height_style); - self - } - - /// Sets the color of the label using a [`Color`]. - /// - /// # Examples - /// - /// ``` - /// use ui::prelude::*; - /// - /// let my_label = Label::new("Hello, World!").color(Color::Accent); - /// ``` - fn color(mut self, color: Color) -> Self { - self.base = self.base.color(color); - self - } - - /// Sets the strikethrough property of the label. - /// - /// # Examples - /// - /// ``` - /// use ui::prelude::*; - /// - /// let my_label = Label::new("Hello, World!").strikethrough(); - /// ``` - fn strikethrough(mut self) -> Self { - self.base = self.base.strikethrough(); - self - } - - /// Sets the italic property of the label. - /// - /// # Examples - /// - /// ``` - /// use ui::prelude::*; - /// - /// let my_label = Label::new("Hello, World!").italic(); - /// ``` - fn italic(mut self) -> Self { - self.base = self.base.italic(); - self - } - - /// Sets the alpha property of the color of label. - /// - /// # Examples - /// - /// ``` - /// use ui::prelude::*; - /// - /// let my_label = Label::new("Hello, World!").alpha(0.5); - /// ``` - 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 - } - - /// Truncates overflowing text with an ellipsis (`…`) if needed. - fn truncate(mut self) -> Self { - self.base = self.base.truncate(); - self - } - - fn single_line(mut self) -> Self { - self.label = SharedString::from(self.label.replace('\n', "⏎")); - self.base = self.base.single_line(); - self - } - - fn buffer_font(mut self, cx: &App) -> Self { - self.base = self.base.buffer_font(cx); - self - } - - /// Styles the label to look like inline code. - fn inline_code(mut self, cx: &App) -> Self { - self.base = self.base.inline_code(cx); - self - } -} - -impl RenderOnce for Label { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - self.base.child(self.label) - } -} - -impl Component for Label { - fn scope() -> ComponentScope { - ComponentScope::Typography - } - - fn description() -> Option<&'static str> { - Some("A text label component that supports various styles, sizes, and formatting options.") - } - - fn preview(_window: &mut Window, cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Sizes", - vec![ - single_example("Default", Label::new("Project Explorer").into_any_element()), - single_example("Small", Label::new("File: main.rs").size(LabelSize::Small).into_any_element()), - single_example("Large", Label::new("Welcome to Zed").size(LabelSize::Large).into_any_element()), - ], - ), - example_group_with_title( - "Colors", - vec![ - single_example("Default", Label::new("Status: Ready").into_any_element()), - single_example("Accent", Label::new("New Update Available").color(Color::Accent).into_any_element()), - single_example("Error", Label::new("Build Failed").color(Color::Error).into_any_element()), - ], - ), - example_group_with_title( - "Styles", - vec![ - single_example("Default", Label::new("Normal Text").into_any_element()), - single_example("Bold", Label::new("Important Notice").weight(gpui::FontWeight::BOLD).into_any_element()), - single_example("Italic", Label::new("Code Comment").italic().into_any_element()), - single_example("Strikethrough", Label::new("Deprecated Feature").strikethrough().into_any_element()), - single_example("Underline", Label::new("Clickable Link").underline().into_any_element()), - single_example("Inline Code", Label::new("fn main() {}").inline_code(cx).into_any_element()), - ], - ), - example_group_with_title( - "Line Height Styles", - vec![ - single_example("Default", Label::new("Multi-line\nText\nExample").into_any_element()), - single_example("UI Label", Label::new("Compact\nUI\nLabel").line_height_style(LineHeightStyle::UiLabel).into_any_element()), - ], - ), - example_group_with_title( - "Special Cases", - vec![ - single_example("Single Line", Label::new("Line 1\nLine 2\nLine 3").single_line().into_any_element()), - single_example("Text Ellipsis", div().max_w_24().child(Label::new("This is a very long file name that should be truncated: very_long_file_name_with_many_words.rs").truncate()).into_any_element()), - ], - ), - ]) - .into_any_element() - ) - } -} diff --git a/crates/ui/src/components/label/label_like.rs b/crates/ui/src/components/label/label_like.rs deleted file mode 100644 index 1fa6b14c83..0000000000 --- a/crates/ui/src/components/label/label_like.rs +++ /dev/null @@ -1,315 +0,0 @@ -use crate::prelude::*; -use gpui::{FontWeight, StyleRefinement, UnderlineStyle}; -use settings::Settings; -use smallvec::SmallVec; -use theme::ThemeSettings; - -/// Sets the size of a label -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] -pub enum LabelSize { - /// The default size of a label. - #[default] - Default, - /// The large size of a label. - Large, - /// The small size of a label. - Small, - /// The extra small size of a label. - XSmall, -} - -/// Sets the line height of a label -#[derive(Default, PartialEq, Copy, Clone)] -pub enum LineHeightStyle { - /// The default line height style of a label, - /// set by either the UI's default line height, - /// or the developer's default buffer line height. - #[default] - TextLabel, - /// Sets the line height to 1. - UiLabel, -} - -/// A common set of traits all labels must implement. -pub trait LabelCommon { - /// Sets the size of the label using a [`LabelSize`]. - fn size(self, size: LabelSize) -> Self; - - /// Sets the font weight of the label. - fn weight(self, weight: FontWeight) -> Self; - - /// Sets the line height style of the label using a [`LineHeightStyle`]. - fn line_height_style(self, line_height_style: LineHeightStyle) -> Self; - - /// Sets the color of the label using a [`Color`]. - fn color(self, color: Color) -> Self; - - /// Sets the strikethrough property of the label. - fn strikethrough(self) -> Self; - - /// Sets the italic property of the label. - fn italic(self) -> Self; - - /// Sets the underline property of the label - fn underline(self) -> Self; - - /// Sets the alpha property of the label, overwriting the alpha value of the color. - fn alpha(self, alpha: f32) -> Self; - - /// Truncates overflowing text with an ellipsis (`…`) if needed. - fn truncate(self) -> Self; - - /// Sets the label to render as a single line. - fn single_line(self) -> Self; - - /// Sets the font to the buffer's - fn buffer_font(self, cx: &App) -> Self; - - /// Styles the label to look like inline code. - fn inline_code(self, cx: &App) -> Self; -} - -/// A label-like element that can be used to create a custom label when -/// prebuilt labels are not sufficient. Use this sparingly, as it is -/// unconstrained and may make the UI feel less consistent. -/// -/// This is also used to build the prebuilt labels. -#[derive(IntoElement)] -pub struct LabelLike { - pub(super) base: Div, - size: LabelSize, - weight: Option, - line_height_style: LineHeightStyle, - pub(crate) color: Color, - strikethrough: bool, - italic: bool, - children: SmallVec<[AnyElement; 2]>, - alpha: Option, - underline: bool, - single_line: bool, - truncate: bool, -} - -impl Default for LabelLike { - fn default() -> Self { - Self::new() - } -} - -impl LabelLike { - /// Creates a new, fully custom label. - /// Prefer using [`Label`] or [`HighlightedLabel`] where possible. - pub fn new() -> Self { - Self { - base: div(), - size: LabelSize::Default, - weight: None, - line_height_style: LineHeightStyle::default(), - color: Color::Default, - strikethrough: false, - italic: false, - children: SmallVec::new(), - alpha: None, - underline: false, - single_line: false, - truncate: false, - } - } -} - -// Style methods. -impl LabelLike { - fn style(&mut self) -> &mut StyleRefinement { - self.base.style() - } - - gpui::margin_style_methods!({ - visibility: pub - }); -} - -impl LabelCommon for LabelLike { - fn size(mut self, size: LabelSize) -> Self { - self.size = size; - self - } - - fn weight(mut self, weight: FontWeight) -> Self { - self.weight = Some(weight); - self - } - - fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self { - self.line_height_style = line_height_style; - self - } - - fn color(mut self, color: Color) -> Self { - self.color = color; - self - } - - fn strikethrough(mut self) -> Self { - self.strikethrough = true; - self - } - - fn italic(mut self) -> Self { - self.italic = true; - self - } - - fn underline(mut self) -> Self { - self.underline = true; - self - } - - fn alpha(mut self, alpha: f32) -> Self { - self.alpha = Some(alpha); - self - } - - /// Truncates overflowing text with an ellipsis (`…`) if needed. - fn truncate(mut self) -> Self { - self.truncate = true; - self - } - - fn single_line(mut self) -> Self { - self.single_line = true; - self - } - - fn buffer_font(mut self, cx: &App) -> Self { - self.base = self - .base - .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()); - self - } - - fn inline_code(mut self, cx: &App) -> Self { - self.base = self - .base - .font(theme::ThemeSettings::get_global(cx).buffer_font.clone()) - .bg(cx.theme().colors().element_background) - .rounded_sm() - .px_0p5(); - self - } -} - -impl ParentElement for LabelLike { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl RenderOnce for LabelLike { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let mut color = self.color.color(cx); - if let Some(alpha) = self.alpha { - color.fade_out(1.0 - alpha); - } - - self.base - .map(|this| match self.size { - LabelSize::Large => this.text_ui_lg(cx), - LabelSize::Default => this.text_ui(cx), - LabelSize::Small => this.text_ui_sm(cx), - LabelSize::XSmall => this.text_ui_xs(cx), - }) - .when(self.line_height_style == LineHeightStyle::UiLabel, |this| { - this.line_height(relative(1.)) - }) - .when(self.italic, |this| this.italic()) - .when(self.underline, |mut this| { - this.text_style() - .get_or_insert_with(Default::default) - .underline = Some(UnderlineStyle { - thickness: px(1.), - color: None, - wavy: false, - }); - this - }) - .when(self.strikethrough, |this| this.line_through()) - .when(self.single_line, |this| this.whitespace_nowrap()) - .when(self.truncate, |this| { - this.overflow_x_hidden().text_ellipsis() - }) - .text_color(color) - .font_weight( - self.weight - .unwrap_or(ThemeSettings::get_global(cx).ui_font.weight), - ) - .children(self.children) - } -} - -impl Component for LabelLike { - fn scope() -> ComponentScope { - ComponentScope::Typography - } - - fn name() -> &'static str { - "LabelLike" - } - - fn description() -> Option<&'static str> { - Some( - "A flexible, customizable label-like component that serves as a base for other label types.", - ) - } - - fn preview(_window: &mut Window, cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Sizes", - vec![ - single_example("Default", LabelLike::new().child("Default size").into_any_element()), - single_example("Large", LabelLike::new().size(LabelSize::Large).child("Large size").into_any_element()), - single_example("Small", LabelLike::new().size(LabelSize::Small).child("Small size").into_any_element()), - single_example("XSmall", LabelLike::new().size(LabelSize::XSmall).child("Extra small size").into_any_element()), - ], - ), - example_group_with_title( - "Styles", - vec![ - single_example("Bold", LabelLike::new().weight(FontWeight::BOLD).child("Bold text").into_any_element()), - single_example("Italic", LabelLike::new().italic().child("Italic text").into_any_element()), - single_example("Underline", LabelLike::new().underline().child("Underlined text").into_any_element()), - single_example("Strikethrough", LabelLike::new().strikethrough().child("Strikethrough text").into_any_element()), - single_example("Inline Code", LabelLike::new().inline_code(cx).child("const value = 42;").into_any_element()), - ], - ), - example_group_with_title( - "Colors", - vec![ - single_example("Default", LabelLike::new().child("Default color").into_any_element()), - single_example("Accent", LabelLike::new().color(Color::Accent).child("Accent color").into_any_element()), - single_example("Error", LabelLike::new().color(Color::Error).child("Error color").into_any_element()), - single_example("Alpha", LabelLike::new().alpha(0.5).child("50% opacity").into_any_element()), - ], - ), - example_group_with_title( - "Line Height", - vec![ - single_example("Default", LabelLike::new().child("Default line height\nMulti-line text").into_any_element()), - single_example("UI Label", LabelLike::new().line_height_style(LineHeightStyle::UiLabel).child("UI label line height\nMulti-line text").into_any_element()), - ], - ), - example_group_with_title( - "Special Cases", - vec![ - single_example("Single Line", LabelLike::new().single_line().child("This is a very long text that should be displayed in a single line").into_any_element()), - single_example("Truncate", LabelLike::new().truncate().child("This is a very long text that should be truncated with an ellipsis").into_any_element()), - ], - ), - ]) - .into_any_element() - ) - } -} diff --git a/crates/ui/src/components/label/loading_label.rs b/crates/ui/src/components/label/loading_label.rs deleted file mode 100644 index 0b6b027e47..0000000000 --- a/crates/ui/src/components/label/loading_label.rs +++ /dev/null @@ -1,112 +0,0 @@ -use crate::prelude::*; -use gpui::{Animation, AnimationExt, FontWeight}; -use std::time::Duration; - -#[derive(IntoElement)] -pub struct LoadingLabel { - base: Label, - text: SharedString, -} - -impl LoadingLabel { - pub fn new(text: impl Into) -> Self { - let text = text.into(); - LoadingLabel { - base: Label::new(text.clone()), - text, - } - } -} - -impl LabelCommon for LoadingLabel { - 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 LoadingLabel { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let text = self.text.clone(); - - self.base.color(Color::Muted).with_animations( - "loading_label", - vec![ - Animation::new(Duration::from_secs(1)), - Animation::new(Duration::from_secs(1)).repeat(), - ], - move |mut label, animation_ix, delta| { - match animation_ix { - 0 => { - let chars_to_show = (delta * text.len() as f32).ceil() as usize; - let text = SharedString::from(text[0..chars_to_show].to_string()); - label.set_text(text); - } - 1 => match delta { - d if d < 0.25 => label.set_text(text.clone()), - d if d < 0.5 => label.set_text(format!("{}.", text)), - d if d < 0.75 => label.set_text(format!("{}..", text)), - _ => label.set_text(format!("{}...", text)), - }, - _ => {} - } - 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 33eeeae125..0000000000 --- a/crates/ui/src/components/label/spinner_label.rs +++ /dev/null @@ -1,205 +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, - Sand, -} - -/// 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!["⣼", "⣹", "⢻", "⠿", "⡟", "⣏", "⣧", "⣶"], - SpinnerVariant::Sand => vec![ - "⠁", "⠂", "⠄", "⡀", "⡈", "⡐", "⡠", "⣀", "⣁", "⣂", "⣄", "⣌", "⣔", "⣤", "⣥", "⣦", - "⣮", "⣶", "⣷", "⣿", "⡿", "⠿", "⢟", "⠟", "⡛", "⠛", "⠫", "⢋", "⠋", "⠍", "⡉", "⠉", - "⠑", "⠡", "⢁", - ], - } - } - - fn duration(&self) -> Duration { - match self { - SpinnerVariant::Dots => Duration::from_millis(1000), - SpinnerVariant::DotsVariant => Duration::from_millis(1000), - SpinnerVariant::Sand => Duration::from_millis(2000), - } - } - - fn animation_id(&self) -> &'static str { - match self { - SpinnerVariant::Dots => "spinner_label_dots", - SpinnerVariant::DotsVariant => "spinner_label_dots_variant", - SpinnerVariant::Sand => "spinner_label_dots_variant_2", - } - } -} - -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]).color(Color::Muted), - variant, - frames, - duration, - } - } - - pub fn dots() -> Self { - Self::with_variant(SpinnerVariant::Dots) - } - - pub fn dots_variant() -> Self { - Self::with_variant(SpinnerVariant::DotsVariant) - } - - pub fn sand() -> Self { - Self::with_variant(SpinnerVariant::Sand) - } -} - -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.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(), - ), - single_example("Sand Variant", SpinnerLabel::sand().into_any_element()), - ]; - - Some(example_group(examples).vertical().into_any_element()) - } -} diff --git a/crates/ui/src/components/list.rs b/crates/ui/src/components/list.rs deleted file mode 100644 index 6876f290ce..0000000000 --- a/crates/ui/src/components/list.rs +++ /dev/null @@ -1,13 +0,0 @@ -mod list; -mod list_bullet_item; -mod list_header; -mod list_item; -mod list_separator; -mod list_sub_header; - -pub use list::*; -pub use list_bullet_item::*; -pub use list_header::*; -pub use list_item::*; -pub use list_separator::*; -pub use list_sub_header::*; diff --git a/crates/ui/src/components/list/list.rs b/crates/ui/src/components/list/list.rs deleted file mode 100644 index ccae5bed23..0000000000 --- a/crates/ui/src/components/list/list.rs +++ /dev/null @@ -1,142 +0,0 @@ -use component::{Component, ComponentScope, example_group_with_title, single_example}; -use gpui::AnyElement; -use smallvec::SmallVec; - -use crate::{Label, ListHeader, ListItem, prelude::*}; - -pub enum EmptyMessage { - Text(SharedString), - Element(AnyElement), -} - -#[derive(IntoElement, RegisterComponent)] -pub struct List { - /// Message to display when the list is empty - /// Defaults to "No items" - empty_message: EmptyMessage, - header: Option, - toggle: Option, - children: SmallVec<[AnyElement; 2]>, -} - -impl Default for List { - fn default() -> Self { - Self::new() - } -} - -impl List { - pub fn new() -> Self { - Self { - empty_message: EmptyMessage::Text("No items".into()), - header: None, - toggle: None, - children: SmallVec::new(), - } - } - - pub fn empty_message(mut self, message: impl Into) -> Self { - self.empty_message = message.into(); - self - } - - pub fn header(mut self, header: impl Into>) -> Self { - self.header = header.into(); - self - } - - pub fn toggle(mut self, toggle: impl Into>) -> Self { - self.toggle = toggle.into(); - self - } -} - -impl ParentElement for List { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl From for EmptyMessage { - fn from(s: String) -> Self { - EmptyMessage::Text(SharedString::from(s)) - } -} - -impl From<&str> for EmptyMessage { - fn from(s: &str) -> Self { - EmptyMessage::Text(SharedString::from(s.to_owned())) - } -} - -impl From for EmptyMessage { - fn from(e: AnyElement) -> Self { - EmptyMessage::Element(e) - } -} - -impl RenderOnce for List { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - v_flex() - .w_full() - .py(DynamicSpacing::Base04.rems(cx)) - .children(self.header) - .map(|this| match (self.children.is_empty(), self.toggle) { - (false, _) => this.children(self.children), - (true, Some(false)) => this, - (true, _) => match self.empty_message { - EmptyMessage::Text(text) => { - this.px_2().child(Label::new(text).color(Color::Muted)) - } - EmptyMessage::Element(element) => this.child(element), - }, - }) - } -} - -impl Component for List { - fn scope() -> ComponentScope { - ComponentScope::Layout - } - - fn description() -> Option<&'static str> { - Some( - "A container component for displaying a collection of list items with optional header and empty state.", - ) - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![example_group_with_title( - "Basic Lists", - vec![ - single_example( - "Simple List", - List::new() - .child(ListItem::new("item1").child(Label::new("Item 1"))) - .child(ListItem::new("item2").child(Label::new("Item 2"))) - .child(ListItem::new("item3").child(Label::new("Item 3"))) - .into_any_element(), - ), - single_example( - "With Header", - List::new() - .header(ListHeader::new("Section Header")) - .child(ListItem::new("item1").child(Label::new("Item 1"))) - .child(ListItem::new("item2").child(Label::new("Item 2"))) - .into_any_element(), - ), - single_example( - "Empty List", - List::new() - .empty_message("No items to display") - .into_any_element(), - ), - ], - )]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/list/list_bullet_item.rs b/crates/ui/src/components/list/list_bullet_item.rs deleted file mode 100644 index 17731488f7..0000000000 --- a/crates/ui/src/components/list/list_bullet_item.rs +++ /dev/null @@ -1,83 +0,0 @@ -use crate::{ListItem, prelude::*}; -use component::{Component, ComponentScope, example_group_with_title, single_example}; -use gpui::{IntoElement, ParentElement, SharedString}; - -#[derive(IntoElement, RegisterComponent)] -pub struct ListBulletItem { - label: SharedString, -} - -impl ListBulletItem { - pub fn new(label: impl Into) -> Self { - Self { - label: label.into(), - } - } -} - -impl RenderOnce for ListBulletItem { - fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement { - let line_height = window.line_height() * 0.85; - - ListItem::new("list-item") - .selectable(false) - .child( - h_flex() - .w_full() - .min_w_0() - .gap_1() - .items_start() - .child( - h_flex().h(line_height).justify_center().child( - Icon::new(IconName::Dash) - .size(IconSize::XSmall) - .color(Color::Hidden), - ), - ) - .child(div().w_full().min_w_0().child(Label::new(self.label))), - ) - .into_any_element() - } -} - -impl Component for ListBulletItem { - fn scope() -> ComponentScope { - ComponentScope::DataDisplay - } - - fn description() -> Option<&'static str> { - Some("A list item with a bullet point indicator for unordered lists.") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .child(example_group_with_title( - "Bullet Items", - vec![ - single_example( - "Simple", - ListBulletItem::new("First bullet item").into_any_element(), - ), - single_example( - "Multiple Lines", - v_flex() - .child(ListBulletItem::new("First item")) - .child(ListBulletItem::new("Second item")) - .child(ListBulletItem::new("Third item")) - .into_any_element(), - ), - single_example( - "Long Text", - ListBulletItem::new( - "A longer bullet item that demonstrates text wrapping behavior", - ) - .into_any_element(), - ), - ], - )) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/list/list_header.rs b/crates/ui/src/components/list/list_header.rs deleted file mode 100644 index 8726dca50d..0000000000 --- a/crates/ui/src/components/list/list_header.rs +++ /dev/null @@ -1,218 +0,0 @@ -use std::sync::Arc; - -use crate::{Disclosure, prelude::*}; -use component::{Component, ComponentScope, example_group_with_title, single_example}; -use gpui::{AnyElement, ClickEvent}; -use settings::Settings; -use theme::ThemeSettings; - -#[derive(IntoElement, RegisterComponent)] -pub struct ListHeader { - /// The label of the header. - label: SharedString, - /// A slot for content that appears before the label, like an icon or avatar. - start_slot: Option, - /// A slot for content that appears after the label, usually on the other side of the header. - /// This might be a button, a disclosure arrow, a face pile, etc. - end_slot: Option, - /// A slot for content that appears on hover after the label - /// It will obscure the `end_slot` when visible. - end_hover_slot: Option, - toggle: Option, - on_toggle: Option>, - inset: bool, - selected: bool, -} - -impl ListHeader { - pub fn new(label: impl Into) -> Self { - Self { - label: label.into(), - start_slot: None, - end_slot: None, - end_hover_slot: None, - inset: false, - toggle: None, - on_toggle: None, - selected: false, - } - } - - pub fn toggle(mut self, toggle: impl Into>) -> Self { - self.toggle = toggle.into(); - self - } - - pub fn on_toggle( - mut self, - on_toggle: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_toggle = Some(Arc::new(on_toggle)); - self - } - - pub fn start_slot(mut self, start_slot: impl Into>) -> Self { - self.start_slot = start_slot.into().map(IntoElement::into_any_element); - self - } - - pub fn end_slot(mut self, end_slot: impl Into>) -> Self { - self.end_slot = end_slot.into().map(IntoElement::into_any_element); - self - } - - pub fn end_hover_slot(mut self, end_hover_slot: impl Into>) -> Self { - self.end_hover_slot = end_hover_slot.into().map(IntoElement::into_any_element); - self - } - - pub fn inset(mut self, inset: bool) -> Self { - self.inset = inset; - self - } -} - -impl Toggleable for ListHeader { - fn toggle_state(mut self, selected: bool) -> Self { - self.selected = selected; - self - } -} - -impl RenderOnce for ListHeader { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let ui_density = ThemeSettings::get_global(cx).ui_density; - - h_flex() - .id(self.label.clone()) - .w_full() - .relative() - .group("list_header") - .child( - div() - .map(|this| match ui_density { - theme::UiDensity::Comfortable => this.h_5(), - _ => this.h_7(), - }) - .when(self.inset, |this| this.px_2()) - .when(self.selected, |this| { - this.bg(cx.theme().colors().ghost_element_selected) - }) - .flex() - .flex_1() - .items_center() - .justify_between() - .w_full() - .gap(DynamicSpacing::Base04.rems(cx)) - .child( - h_flex() - .gap(DynamicSpacing::Base04.rems(cx)) - .children(self.toggle.map(|is_open| { - Disclosure::new("toggle", is_open) - .on_toggle_expanded(self.on_toggle.clone()) - })) - .child( - div() - .id("label_container") - .flex() - .gap(DynamicSpacing::Base04.rems(cx)) - .items_center() - .children(self.start_slot) - .child(Label::new(self.label.clone()).color(Color::Muted)) - .when_some(self.on_toggle, |this, on_toggle| { - this.on_click(move |event, window, cx| { - on_toggle(event, window, cx) - }) - }), - ), - ) - .child(h_flex().children(self.end_slot)) - .when_some(self.end_hover_slot, |this, end_hover_slot| { - this.child( - div() - .absolute() - .right_0() - .visible_on_hover("list_header") - .child(end_hover_slot), - ) - }), - ) - } -} - -impl Component for ListHeader { - fn scope() -> ComponentScope { - ComponentScope::DataDisplay - } - - fn description() -> Option<&'static str> { - Some( - "A header component for lists with support for icons, actions, and collapsible sections.", - ) - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Basic Headers", - vec![ - single_example( - "Simple", - ListHeader::new("Section Header").into_any_element(), - ), - single_example( - "With Icon", - ListHeader::new("Files") - .start_slot(Icon::new(IconName::File)) - .into_any_element(), - ), - single_example( - "With End Slot", - ListHeader::new("Recent") - .end_slot(Label::new("5").color(Color::Muted)) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Collapsible Headers", - vec![ - single_example( - "Expanded", - ListHeader::new("Expanded Section") - .toggle(Some(true)) - .into_any_element(), - ), - single_example( - "Collapsed", - ListHeader::new("Collapsed Section") - .toggle(Some(false)) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "States", - vec![ - single_example( - "Selected", - ListHeader::new("Selected Header") - .toggle_state(true) - .into_any_element(), - ), - single_example( - "Inset", - ListHeader::new("Inset Header") - .inset(true) - .into_any_element(), - ), - ], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/list/list_item.rs b/crates/ui/src/components/list/list_item.rs deleted file mode 100644 index d581fad945..0000000000 --- a/crates/ui/src/components/list/list_item.rs +++ /dev/null @@ -1,470 +0,0 @@ -use std::sync::Arc; - -use component::{Component, ComponentScope, example_group_with_title, single_example}; -use gpui::{AnyElement, AnyView, ClickEvent, MouseButton, MouseDownEvent, Pixels, px}; -use smallvec::SmallVec; - -use crate::{Disclosure, prelude::*}; - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] -pub enum ListItemSpacing { - #[default] - Dense, - ExtraDense, - Sparse, -} - -#[derive(IntoElement, RegisterComponent)] -pub struct ListItem { - id: ElementId, - group_name: Option, - disabled: bool, - selected: bool, - spacing: ListItemSpacing, - indent_level: usize, - indent_step_size: Pixels, - /// A slot for content that appears before the children, like an icon or avatar. - start_slot: Option, - /// A slot for content that appears after the children, usually on the other side of the header. - /// This might be a button, a disclosure arrow, a face pile, etc. - end_slot: Option, - /// A slot for content that appears on hover after the children - /// It will obscure the `end_slot` when visible. - end_hover_slot: Option, - toggle: Option, - inset: bool, - on_click: Option>, - on_hover: Option>, - on_toggle: Option>, - tooltip: Option AnyView + 'static>>, - on_secondary_mouse_down: Option>, - children: SmallVec<[AnyElement; 2]>, - selectable: bool, - always_show_disclosure_icon: bool, - outlined: bool, - rounded: bool, - overflow_x: bool, - focused: Option, -} - -impl ListItem { - pub fn new(id: impl Into) -> Self { - Self { - id: id.into(), - group_name: None, - disabled: false, - selected: false, - spacing: ListItemSpacing::Dense, - indent_level: 0, - indent_step_size: px(12.), - start_slot: None, - end_slot: None, - end_hover_slot: None, - toggle: None, - inset: false, - on_click: None, - on_secondary_mouse_down: None, - on_toggle: None, - on_hover: None, - tooltip: None, - children: SmallVec::new(), - selectable: true, - always_show_disclosure_icon: false, - outlined: false, - rounded: false, - overflow_x: false, - focused: None, - } - } - - pub fn group_name(mut self, group_name: impl Into) -> Self { - self.group_name = Some(group_name.into()); - self - } - - pub fn spacing(mut self, spacing: ListItemSpacing) -> Self { - self.spacing = spacing; - self - } - - pub fn selectable(mut self, has_hover: bool) -> Self { - self.selectable = has_hover; - self - } - - pub fn always_show_disclosure_icon(mut self, show: bool) -> Self { - self.always_show_disclosure_icon = show; - self - } - - pub fn on_click( - mut self, - handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_click = Some(Box::new(handler)); - self - } - - pub fn on_hover(mut self, handler: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self { - self.on_hover = Some(Box::new(handler)); - self - } - - pub fn on_secondary_mouse_down( - mut self, - handler: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_secondary_mouse_down = Some(Box::new(handler)); - self - } - - pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self { - self.tooltip = Some(Box::new(tooltip)); - self - } - - pub fn inset(mut self, inset: bool) -> Self { - self.inset = inset; - self - } - - pub fn indent_level(mut self, indent_level: usize) -> Self { - self.indent_level = indent_level; - self - } - - pub fn indent_step_size(mut self, indent_step_size: Pixels) -> Self { - self.indent_step_size = indent_step_size; - self - } - - pub fn toggle(mut self, toggle: impl Into>) -> Self { - self.toggle = toggle.into(); - self - } - - pub fn on_toggle( - mut self, - on_toggle: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_toggle = Some(Arc::new(on_toggle)); - self - } - - pub fn start_slot(mut self, start_slot: impl Into>) -> Self { - self.start_slot = start_slot.into().map(IntoElement::into_any_element); - self - } - - pub fn end_slot(mut self, end_slot: impl Into>) -> Self { - self.end_slot = end_slot.into().map(IntoElement::into_any_element); - self - } - - pub fn end_hover_slot(mut self, end_hover_slot: impl Into>) -> Self { - self.end_hover_slot = end_hover_slot.into().map(IntoElement::into_any_element); - self - } - - pub fn outlined(mut self) -> Self { - self.outlined = true; - self - } - - pub fn rounded(mut self) -> Self { - self.rounded = true; - self - } - - pub fn overflow_x(mut self) -> Self { - self.overflow_x = true; - self - } - - pub fn focused(mut self, focused: bool) -> Self { - self.focused = Some(focused); - self - } -} - -impl Disableable for ListItem { - fn disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self - } -} - -impl Toggleable for ListItem { - fn toggle_state(mut self, selected: bool) -> Self { - self.selected = selected; - self - } -} - -impl ParentElement for ListItem { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl RenderOnce for ListItem { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - h_flex() - .id(self.id) - .when_some(self.group_name, |this, group| this.group(group)) - .w_full() - .relative() - // When an item is inset draw the indent spacing outside of the item - .when(self.inset, |this| { - this.ml(self.indent_level as f32 * self.indent_step_size) - .px(DynamicSpacing::Base04.rems(cx)) - }) - .when(!self.inset && !self.disabled, |this| { - this - // TODO: Add focus state - // .when(self.state == InteractionState::Focused, |this| { - .when_some(self.focused, |this, focused| { - if focused { - this.border_1() - .border_color(cx.theme().colors().border_focused) - } else { - this.border_1() - } - }) - .when(self.selectable, |this| { - this.hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) - .active(|style| style.bg(cx.theme().colors().ghost_element_active)) - .when(self.outlined, |this| this.rounded_sm()) - .when(self.selected, |this| { - this.bg(cx.theme().colors().ghost_element_selected) - }) - }) - }) - .when(self.rounded, |this| this.rounded_sm()) - .when_some(self.on_hover, |this, on_hover| this.on_hover(on_hover)) - .child( - h_flex() - .id("inner_list_item") - .group("list_item") - .w_full() - .relative() - .gap_1() - .px(DynamicSpacing::Base06.rems(cx)) - .map(|this| match self.spacing { - ListItemSpacing::Dense => this, - ListItemSpacing::ExtraDense => this.py_neg_px(), - ListItemSpacing::Sparse => this.py_1(), - }) - .when(self.inset && !self.disabled, |this| { - this - // TODO: Add focus state - //.when(self.state == InteractionState::Focused, |this| { - .when_some(self.focused, |this, focused| { - if focused { - this.border_1() - .border_color(cx.theme().colors().border_focused) - } else { - this.border_1() - } - }) - .when(self.selectable, |this| { - this.hover(|style| { - style.bg(cx.theme().colors().ghost_element_hover) - }) - .active(|style| style.bg(cx.theme().colors().ghost_element_active)) - .when(self.selected, |this| { - this.bg(cx.theme().colors().ghost_element_selected) - }) - }) - }) - .when_some( - self.on_click.filter(|_| !self.disabled), - |this, on_click| this.cursor_pointer().on_click(on_click), - ) - .when(self.outlined, |this| { - this.border_1() - .border_color(cx.theme().colors().border) - .rounded_sm() - .overflow_hidden() - }) - .when_some(self.on_secondary_mouse_down, |this, on_mouse_down| { - this.on_mouse_down(MouseButton::Right, move |event, window, cx| { - (on_mouse_down)(event, window, cx) - }) - }) - .when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip)) - .map(|this| { - if self.inset { - this.rounded_sm() - } else { - // When an item is not inset draw the indent spacing inside of the item - this.ml(self.indent_level as f32 * self.indent_step_size) - } - }) - .children(self.toggle.map(|is_open| { - div() - .flex() - .absolute() - .left(rems(-1.)) - .when(is_open && !self.always_show_disclosure_icon, |this| { - this.visible_on_hover("") - }) - .child( - Disclosure::new("toggle", is_open) - .on_toggle_expanded(self.on_toggle), - ) - })) - .child( - h_flex() - .flex_grow() - .flex_shrink_0() - .flex_basis(relative(0.25)) - .gap(DynamicSpacing::Base06.rems(cx)) - .map(|list_content| { - if self.overflow_x { - list_content - } else { - list_content.overflow_hidden() - } - }) - .children(self.start_slot) - .children(self.children), - ) - .when_some(self.end_slot, |this, end_slot| { - this.justify_between().child( - h_flex() - .flex_shrink() - .overflow_hidden() - .when(self.end_hover_slot.is_some(), |this| { - this.visible() - .group_hover("list_item", |this| this.invisible()) - }) - .child(end_slot), - ) - }) - .when_some(self.end_hover_slot, |this, end_hover_slot| { - this.child( - h_flex() - .h_full() - .absolute() - .right(DynamicSpacing::Base06.rems(cx)) - .top_0() - .visible_on_hover("list_item") - .child(end_hover_slot), - ) - }), - ) - } -} - -impl Component for ListItem { - fn scope() -> ComponentScope { - ComponentScope::DataDisplay - } - - fn description() -> Option<&'static str> { - Some( - "A flexible list item component with support for icons, actions, disclosure toggles, and hierarchical display.", - ) - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Basic List Items", - vec![ - single_example( - "Simple", - ListItem::new("simple") - .child(Label::new("Simple list item")) - .into_any_element(), - ), - single_example( - "With Icon", - ListItem::new("with_icon") - .start_slot(Icon::new(IconName::File)) - .child(Label::new("List item with icon")) - .into_any_element(), - ), - single_example( - "Selected", - ListItem::new("selected") - .toggle_state(true) - .start_slot(Icon::new(IconName::Check)) - .child(Label::new("Selected item")) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "List Item Spacing", - vec![ - single_example( - "Dense", - ListItem::new("dense") - .spacing(ListItemSpacing::Dense) - .child(Label::new("Dense spacing")) - .into_any_element(), - ), - single_example( - "Extra Dense", - ListItem::new("extra_dense") - .spacing(ListItemSpacing::ExtraDense) - .child(Label::new("Extra dense spacing")) - .into_any_element(), - ), - single_example( - "Sparse", - ListItem::new("sparse") - .spacing(ListItemSpacing::Sparse) - .child(Label::new("Sparse spacing")) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "With Slots", - vec![ - single_example( - "End Slot", - ListItem::new("end_slot") - .child(Label::new("Item with end slot")) - .end_slot(Icon::new(IconName::ChevronRight)) - .into_any_element(), - ), - single_example( - "With Toggle", - ListItem::new("with_toggle") - .toggle(Some(true)) - .child(Label::new("Expandable item")) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "States", - vec![ - single_example( - "Disabled", - ListItem::new("disabled") - .disabled(true) - .child(Label::new("Disabled item")) - .into_any_element(), - ), - single_example( - "Non-selectable", - ListItem::new("non_selectable") - .selectable(false) - .child(Label::new("Non-selectable item")) - .into_any_element(), - ), - ], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/list/list_separator.rs b/crates/ui/src/components/list/list_separator.rs deleted file mode 100644 index 92a7c987c7..0000000000 --- a/crates/ui/src/components/list/list_separator.rs +++ /dev/null @@ -1,14 +0,0 @@ -use crate::prelude::*; - -#[derive(IntoElement)] -pub struct ListSeparator; - -impl RenderOnce for ListSeparator { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - div() - .h_px() - .w_full() - .my(DynamicSpacing::Base06.rems(cx)) - .bg(cx.theme().colors().border_variant) - } -} diff --git a/crates/ui/src/components/list/list_sub_header.rs b/crates/ui/src/components/list/list_sub_header.rs deleted file mode 100644 index b4a82fb2ed..0000000000 --- a/crates/ui/src/components/list/list_sub_header.rs +++ /dev/null @@ -1,149 +0,0 @@ -use crate::prelude::*; -use component::{Component, ComponentScope, example_group_with_title, single_example}; - -#[derive(IntoElement, RegisterComponent)] -pub struct ListSubHeader { - label: SharedString, - start_slot: Option, - end_slot: Option, - inset: bool, - selected: bool, -} - -impl ListSubHeader { - pub fn new(label: impl Into) -> Self { - Self { - label: label.into(), - start_slot: None, - end_slot: None, - inset: false, - selected: false, - } - } - - pub fn left_icon(mut self, left_icon: Option) -> Self { - self.start_slot = left_icon; - self - } - - pub fn end_slot(mut self, end_slot: AnyElement) -> Self { - self.end_slot = Some(end_slot); - self - } - - pub fn inset(mut self, inset: bool) -> Self { - self.inset = inset; - self - } -} - -impl Toggleable for ListSubHeader { - fn toggle_state(mut self, selected: bool) -> Self { - self.selected = selected; - self - } -} - -impl RenderOnce for ListSubHeader { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - h_flex() - .flex_1() - .w_full() - .relative() - .pb(DynamicSpacing::Base04.rems(cx)) - .px(DynamicSpacing::Base02.rems(cx)) - .child( - div() - .h_5() - .when(self.inset, |this| this.px_2()) - .when(self.selected, |this| { - this.bg(cx.theme().colors().ghost_element_selected) - }) - .flex() - .flex_1() - .w_full() - .gap_1() - .items_center() - .justify_between() - .child( - div() - .flex() - .gap_1() - .items_center() - .children( - self.start_slot.map(|i| { - Icon::new(i).color(Color::Muted).size(IconSize::Small) - }), - ) - .child( - Label::new(self.label.clone()) - .color(Color::Muted) - .size(LabelSize::Small), - ), - ) - .children(self.end_slot), - ) - } -} - -impl Component for ListSubHeader { - fn scope() -> ComponentScope { - ComponentScope::DataDisplay - } - - fn description() -> Option<&'static str> { - Some( - "A sub-header component for organizing list content into subsections with optional icons and end slots.", - ) - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Basic Sub-headers", - vec![ - single_example( - "Simple", - ListSubHeader::new("Subsection").into_any_element(), - ), - single_example( - "With Icon", - ListSubHeader::new("Documents") - .left_icon(Some(IconName::File)) - .into_any_element(), - ), - single_example( - "With End Slot", - ListSubHeader::new("Recent") - .end_slot( - Label::new("3").color(Color::Muted).into_any_element(), - ) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "States", - vec![ - single_example( - "Selected", - ListSubHeader::new("Selected") - .toggle_state(true) - .into_any_element(), - ), - single_example( - "Inset", - ListSubHeader::new("Inset Sub-header") - .inset(true) - .into_any_element(), - ), - ], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/modal.rs b/crates/ui/src/components/modal.rs deleted file mode 100644 index 85565f5488..0000000000 --- a/crates/ui/src/components/modal.rs +++ /dev/null @@ -1,462 +0,0 @@ -use crate::{ - Clickable, Color, DynamicSpacing, Headline, HeadlineSize, Icon, IconButton, IconButtonShape, - IconName, Label, LabelCommon, LabelSize, h_flex, v_flex, -}; -use gpui::{prelude::FluentBuilder, *}; -use smallvec::SmallVec; -use theme::ActiveTheme; - -#[derive(IntoElement)] -pub struct Modal { - id: ElementId, - header: ModalHeader, - children: SmallVec<[AnyElement; 2]>, - footer: Option, - container_id: ElementId, - container_scroll_handler: Option, -} - -impl Modal { - pub fn new(id: impl Into, scroll_handle: Option) -> Self { - let id = id.into(); - - let container_id = ElementId::Name(format!("{}_container", id).into()); - Self { - id: ElementId::Name(id), - header: ModalHeader::new(), - children: SmallVec::new(), - footer: None, - container_id, - container_scroll_handler: scroll_handle, - } - } - - pub fn header(mut self, header: ModalHeader) -> Self { - self.header = header; - self - } - - pub fn section(mut self, section: Section) -> Self { - self.children.push(section.into_any_element()); - self - } - - pub fn footer(mut self, footer: ModalFooter) -> Self { - self.footer = Some(footer); - self - } - - pub fn show_dismiss(mut self, show: bool) -> Self { - self.header.show_dismiss_button = show; - self - } - - pub fn show_back(mut self, show: bool) -> Self { - self.header.show_back_button = show; - self - } -} - -impl ParentElement for Modal { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl RenderOnce for Modal { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - v_flex() - .id(self.id.clone()) - .size_full() - .flex_1() - .overflow_hidden() - .child(self.header) - .child( - v_flex() - .id(self.container_id.clone()) - .w_full() - .flex_1() - .gap(DynamicSpacing::Base08.rems(cx)) - .when(self.footer.is_some(), |this| this.pb_4()) - .when_some( - self.container_scroll_handler, - |this, container_scroll_handle| { - this.overflow_y_scroll() - .track_scroll(&container_scroll_handle) - }, - ) - .children(self.children), - ) - .children(self.footer) - } -} - -#[derive(IntoElement)] -pub struct ModalHeader { - icon: Option, - headline: Option, - description: Option, - children: SmallVec<[AnyElement; 2]>, - show_dismiss_button: bool, - show_back_button: bool, -} - -impl Default for ModalHeader { - fn default() -> Self { - Self::new() - } -} - -impl ModalHeader { - pub fn new() -> Self { - Self { - icon: None, - headline: None, - description: None, - children: SmallVec::new(), - show_dismiss_button: false, - show_back_button: false, - } - } - - pub fn icon(mut self, icon: Icon) -> Self { - self.icon = Some(icon); - self - } - - /// Set the headline of the modal. - /// - /// This will insert the headline as the first item - /// of `children` if it is not already present. - pub fn headline(mut self, headline: impl Into) -> Self { - self.headline = Some(headline.into()); - self - } - - pub fn description(mut self, description: impl Into) -> Self { - self.description = Some(description.into()); - self - } - - pub fn show_dismiss_button(mut self, show: bool) -> Self { - self.show_dismiss_button = show; - self - } - - pub fn show_back_button(mut self, show: bool) -> Self { - self.show_back_button = show; - self - } -} - -impl ParentElement for ModalHeader { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl RenderOnce for ModalHeader { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let mut children = self.children; - - if self.headline.is_some() { - children.insert( - 0, - Headline::new(self.headline.unwrap()) - .size(HeadlineSize::XSmall) - .color(Color::Muted) - .into_any_element(), - ); - } - - h_flex() - .flex_none() - .justify_between() - .w_full() - .px(DynamicSpacing::Base12.rems(cx)) - .pt(DynamicSpacing::Base08.rems(cx)) - .pb(DynamicSpacing::Base04.rems(cx)) - .gap(DynamicSpacing::Base08.rems(cx)) - .when(self.show_back_button, |this| { - this.child( - IconButton::new("back", IconName::ArrowLeft) - .shape(IconButtonShape::Square) - .on_click(|_, window, cx| { - window.dispatch_action(menu::Cancel.boxed_clone(), cx); - }), - ) - }) - .child( - v_flex() - .flex_1() - .child( - h_flex() - .gap_1() - .when_some(self.icon, |this, icon| this.child(icon)) - .children(children), - ) - .when_some(self.description, |this, description| { - this.child(Label::new(description).color(Color::Muted).mb_2()) - }), - ) - .when(self.show_dismiss_button, |this| { - this.child( - IconButton::new("dismiss", IconName::Close) - .shape(IconButtonShape::Square) - .on_click(|_, window, cx| { - window.dispatch_action(menu::Cancel.boxed_clone(), cx); - }), - ) - }) - } -} - -#[derive(IntoElement)] -pub struct ModalRow { - children: SmallVec<[AnyElement; 2]>, -} - -impl Default for ModalRow { - fn default() -> Self { - Self::new() - } -} - -impl ModalRow { - pub fn new() -> Self { - Self { - children: SmallVec::new(), - } - } -} - -impl ParentElement for ModalRow { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl RenderOnce for ModalRow { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - h_flex().w_full().py_1().children(self.children) - } -} - -#[derive(IntoElement)] -pub struct ModalFooter { - start_slot: Option, - end_slot: Option, -} - -impl Default for ModalFooter { - fn default() -> Self { - Self::new() - } -} - -impl ModalFooter { - pub fn new() -> Self { - Self { - start_slot: None, - end_slot: None, - } - } - - pub fn start_slot(mut self, start_slot: impl Into>) -> Self { - self.start_slot = start_slot.into().map(IntoElement::into_any_element); - self - } - - pub fn end_slot(mut self, end_slot: impl Into>) -> Self { - self.end_slot = end_slot.into().map(IntoElement::into_any_element); - self - } -} - -impl RenderOnce for ModalFooter { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - h_flex() - .w_full() - .p(DynamicSpacing::Base08.rems(cx)) - .flex_none() - .justify_between() - .gap_1() - .border_t_1() - .border_color(cx.theme().colors().border_variant) - .child(div().when_some(self.start_slot, |this, start_slot| this.child(start_slot))) - .child(div().when_some(self.end_slot, |this, end_slot| this.child(end_slot))) - } -} - -#[derive(IntoElement)] -pub struct Section { - contained: bool, - padded: bool, - header: Option, - meta: Option, - children: SmallVec<[AnyElement; 2]>, -} - -impl Default for Section { - fn default() -> Self { - Self::new() - } -} - -impl Section { - pub fn new() -> Self { - Self { - contained: false, - padded: true, - header: None, - meta: None, - children: SmallVec::new(), - } - } - - pub fn new_contained() -> Self { - Self { - contained: true, - padded: true, - header: None, - meta: None, - children: SmallVec::new(), - } - } - - pub fn contained(mut self, contained: bool) -> Self { - self.contained = contained; - self - } - - pub fn header(mut self, header: SectionHeader) -> Self { - self.header = Some(header); - self - } - - pub fn meta(mut self, meta: impl Into) -> Self { - self.meta = Some(meta.into()); - self - } - pub fn padded(mut self, padded: bool) -> Self { - self.padded = padded; - self - } -} - -impl ParentElement for Section { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl RenderOnce for Section { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let mut section_bg = cx.theme().colors().text; - section_bg.fade_out(0.96); - - let children = if self.contained { - v_flex() - .flex_1() - .when(self.padded, |this| this.px(DynamicSpacing::Base12.rems(cx))) - .child( - v_flex() - .w_full() - .rounded_sm() - .border_1() - .border_color(cx.theme().colors().border) - .bg(section_bg) - .py(DynamicSpacing::Base06.rems(cx)) - .gap_y(DynamicSpacing::Base04.rems(cx)) - .child(div().flex().flex_1().size_full().children(self.children)), - ) - } else { - v_flex() - .w_full() - .flex_1() - .gap_y(DynamicSpacing::Base04.rems(cx)) - .when(self.padded, |this| { - this.px(DynamicSpacing::Base06.rems(cx) + DynamicSpacing::Base06.rems(cx)) - }) - .children(self.children) - }; - - v_flex() - .size_full() - .flex_1() - .child( - v_flex() - .flex_none() - .px(DynamicSpacing::Base12.rems(cx)) - .children(self.header) - .when_some(self.meta, |this, meta| { - this.child(Label::new(meta).size(LabelSize::Small).color(Color::Muted)) - }), - ) - .child(children) - // fill any leftover space - .child(div().flex().flex_1()) - } -} - -#[derive(IntoElement)] -pub struct SectionHeader { - /// The label of the header. - label: SharedString, - /// A slot for content that appears after the label, usually on the other side of the header. - /// This might be a button, a disclosure arrow, a face pile, etc. - end_slot: Option, -} - -impl SectionHeader { - pub fn new(label: impl Into) -> Self { - Self { - label: label.into(), - end_slot: None, - } - } - - pub fn end_slot(mut self, end_slot: impl Into>) -> Self { - self.end_slot = end_slot.into().map(IntoElement::into_any_element); - self - } -} - -impl RenderOnce for SectionHeader { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - h_flex() - .id(self.label.clone()) - .w_full() - .px(DynamicSpacing::Base08.rems(cx)) - .child( - div() - .h_7() - .flex() - .items_center() - .justify_between() - .w_full() - .gap(DynamicSpacing::Base04.rems(cx)) - .child( - div().flex_1().child( - Label::new(self.label.clone()) - .size(LabelSize::Small) - .into_element(), - ), - ) - .child(h_flex().children(self.end_slot)), - ) - } -} - -impl From for SectionHeader { - fn from(val: SharedString) -> Self { - SectionHeader::new(val) - } -} - -impl From<&'static str> for SectionHeader { - fn from(val: &'static str) -> Self { - let label: SharedString = val.into(); - SectionHeader::new(label) - } -} diff --git a/crates/ui/src/components/navigable.rs b/crates/ui/src/components/navigable.rs deleted file mode 100644 index a592bcc36f..0000000000 --- a/crates/ui/src/components/navigable.rs +++ /dev/null @@ -1,102 +0,0 @@ -use crate::prelude::*; -use gpui::{AnyElement, FocusHandle, ScrollAnchor, ScrollHandle}; - -/// An element that can be navigated through via keyboard. Intended for use with scrollable views that want to use -#[derive(IntoElement)] -pub struct Navigable { - child: AnyElement, - selectable_children: Vec, -} - -/// An entry of [Navigable] that can be navigated to. -#[derive(Clone)] -pub struct NavigableEntry { - #[allow(missing_docs)] - pub focus_handle: FocusHandle, - #[allow(missing_docs)] - pub scroll_anchor: Option, -} - -impl NavigableEntry { - /// Creates a new [NavigableEntry] for a given scroll handle. - pub fn new(scroll_handle: &ScrollHandle, cx: &App) -> Self { - Self { - focus_handle: cx.focus_handle(), - scroll_anchor: Some(ScrollAnchor::for_handle(scroll_handle.clone())), - } - } - /// Create a new [NavigableEntry] that cannot be scrolled to. - pub fn focusable(cx: &App) -> Self { - Self { - focus_handle: cx.focus_handle(), - scroll_anchor: None, - } - } -} -impl Navigable { - /// Creates new empty [Navigable] wrapper. - pub fn new(child: AnyElement) -> Self { - Self { - child, - selectable_children: vec![], - } - } - - /// Add a new entry that can be navigated to via keyboard. - /// - /// The order of calls to [Navigable::entry] determines the order of traversal of - /// elements via successive uses of `menu:::SelectNext/SelectPrevious` - pub fn entry(mut self, child: NavigableEntry) -> Self { - self.selectable_children.push(child); - self - } - - fn find_focused( - selectable_children: &[NavigableEntry], - window: &mut Window, - cx: &mut App, - ) -> Option { - selectable_children - .iter() - .position(|entry| entry.focus_handle.contains_focused(window, cx)) - } -} - -impl RenderOnce for Navigable { - fn render(self, _window: &mut Window, _: &mut App) -> impl crate::IntoElement { - div() - .on_action({ - let children = self.selectable_children.clone(); - - move |_: &menu::SelectNext, window, cx| { - let target = Self::find_focused(&children, window, cx) - .and_then(|index| { - index.checked_add(1).filter(|index| *index < children.len()) - }) - .unwrap_or(0); - if let Some(entry) = children.get(target) { - entry.focus_handle.focus(window); - if let Some(anchor) = &entry.scroll_anchor { - anchor.scroll_to(window, cx); - } - } - } - }) - .on_action({ - let children = self.selectable_children; - move |_: &menu::SelectPrevious, window, cx| { - let target = Self::find_focused(&children, window, cx) - .and_then(|index| index.checked_sub(1)) - .or(children.len().checked_sub(1)); - if let Some(entry) = target.and_then(|target| children.get(target)) { - entry.focus_handle.focus(window); - if let Some(anchor) = &entry.scroll_anchor { - anchor.scroll_to(window, cx); - } - } - } - }) - .size_full() - .child(self.child) - } -} diff --git a/crates/ui/src/components/notification.rs b/crates/ui/src/components/notification.rs deleted file mode 100644 index 61109550f7..0000000000 --- a/crates/ui/src/components/notification.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod alert_modal; - -pub use alert_modal::*; diff --git a/crates/ui/src/components/notification/alert_modal.rs b/crates/ui/src/components/notification/alert_modal.rs deleted file mode 100644 index 9990dc1ce5..0000000000 --- a/crates/ui/src/components/notification/alert_modal.rs +++ /dev/null @@ -1,113 +0,0 @@ -use crate::component_prelude::*; -use crate::prelude::*; -use gpui::IntoElement; -use smallvec::{SmallVec, smallvec}; - -#[derive(IntoElement, RegisterComponent)] -pub struct AlertModal { - id: ElementId, - children: SmallVec<[AnyElement; 2]>, - title: SharedString, - primary_action: SharedString, - dismiss_label: SharedString, -} - -impl AlertModal { - pub fn new(id: impl Into, title: impl Into) -> Self { - Self { - id: id.into(), - children: smallvec![], - title: title.into(), - primary_action: "Ok".into(), - dismiss_label: "Cancel".into(), - } - } - - pub fn primary_action(mut self, primary_action: impl Into) -> Self { - self.primary_action = primary_action.into(); - self - } - - pub fn dismiss_label(mut self, dismiss_label: impl Into) -> Self { - self.dismiss_label = dismiss_label.into(); - self - } -} - -impl RenderOnce for AlertModal { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - v_flex() - .id(self.id) - .elevation_3(cx) - .w(px(440.)) - .p_5() - .child( - v_flex() - .text_ui(cx) - .text_color(Color::Muted.color(cx)) - .gap_1() - .child(Headline::new(self.title).size(HeadlineSize::Small)) - .children(self.children), - ) - .child( - h_flex() - .h(rems(1.75)) - .items_center() - .child(div().flex_1()) - .child( - h_flex() - .items_center() - .gap_1() - .child( - Button::new(self.dismiss_label.clone(), self.dismiss_label.clone()) - .color(Color::Muted), - ) - .child(Button::new( - self.primary_action.clone(), - self.primary_action, - )), - ), - ) - } -} - -impl ParentElement for AlertModal { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl Component for AlertModal { - fn scope() -> ComponentScope { - ComponentScope::Notification - } - - fn status() -> ComponentStatus { - ComponentStatus::WorkInProgress - } - - fn description() -> Option<&'static str> { - Some("A modal dialog that presents an alert message with primary and dismiss actions.") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .p_4() - .children(vec![example_group( - vec![ - single_example( - "Basic Alert", - AlertModal::new("simple-modal", "Do you want to leave the current call?") - .child("The current window will be closed, and connections to any shared projects will be terminated." - ) - .primary_action("Leave Call") - .into_any_element(), - ) - ], - )]) - .into_any_element() - ) - } -} diff --git a/crates/ui/src/components/popover.rs b/crates/ui/src/components/popover.rs deleted file mode 100644 index 7143514c52..0000000000 --- a/crates/ui/src/components/popover.rs +++ /dev/null @@ -1,94 +0,0 @@ -use crate::prelude::*; -use crate::v_flex; -use gpui::{ - AnyElement, App, Element, IntoElement, ParentElement, Pixels, RenderOnce, Styled, Window, div, -}; -use smallvec::SmallVec; - -/// Y height added beyond the size of the contents. -pub const POPOVER_Y_PADDING: Pixels = px(8.); - -/// A popover is used to display a menu or show some options. -/// -/// Clicking the element that launches the popover should not change the current view, -/// and the popover should be statically positioned relative to that element (not the -/// user's mouse.) -/// -/// Example: A "new" menu with options like "new file", "new folder", etc, -/// Linear's "Display" menu, a profile menu that appears when you click your avatar. -/// -/// Related elements: -/// -/// [`ContextMenu`](crate::ContextMenu): -/// -/// Used to display a popover menu that only contains a list of items. Context menus are always -/// launched by secondary clicking on an element. The menu is positioned relative to the user's cursor. -/// -/// Example: Right clicking a file in the file tree to get a list of actions, right clicking -/// a tab to in the tab bar to get a list of actions. -/// -/// `Dropdown`: -/// -/// Used to display a list of options when the user clicks an element. The menu is -/// positioned relative the element that was clicked, and clicking an item in the -/// dropdown should change the value of the element that was clicked. -/// -/// Example: A theme select control. Displays "One Dark", clicking it opens a list of themes. -/// When one is selected, the theme select control displays the selected theme. -#[derive(IntoElement)] -pub struct Popover { - children: SmallVec<[AnyElement; 2]>, - aside: Option, -} - -impl RenderOnce for Popover { - fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - div() - .flex() - .gap_1() - .child( - v_flex() - .elevation_2(cx) - .py(POPOVER_Y_PADDING / 2.) - .child(div().children(self.children)), - ) - .when_some(self.aside, |this, aside| { - this.child( - v_flex() - .elevation_2(cx) - .bg(cx.theme().colors().surface_background) - .px_1() - .child(aside), - ) - }) - } -} - -impl Default for Popover { - fn default() -> Self { - Self::new() - } -} - -impl Popover { - pub fn new() -> Self { - Self { - children: SmallVec::new(), - aside: None, - } - } - - pub fn aside(mut self, aside: impl IntoElement) -> Self - where - Self: Sized, - { - self.aside = Some(aside.into_element().into_any()); - self - } -} - -impl ParentElement for Popover { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} diff --git a/crates/ui/src/components/popover_menu.rs b/crates/ui/src/components/popover_menu.rs deleted file mode 100644 index b1a52bec8f..0000000000 --- a/crates/ui/src/components/popover_menu.rs +++ /dev/null @@ -1,488 +0,0 @@ -use std::{cell::RefCell, rc::Rc}; - -use gpui::{ - AnyElement, AnyView, App, Bounds, Corner, DismissEvent, DispatchPhase, Element, ElementId, - Entity, Focusable as _, GlobalElementId, HitboxBehavior, HitboxId, InteractiveElement, - IntoElement, LayoutId, Length, ManagedView, MouseDownEvent, ParentElement, Pixels, Point, - Style, Window, anchored, deferred, div, point, prelude::FluentBuilder, px, size, -}; - -use crate::prelude::*; - -pub trait PopoverTrigger: IntoElement + Clickable + Toggleable + 'static {} - -impl PopoverTrigger for T {} - -impl Clickable for gpui::AnimationElement -where - T: Clickable + 'static, -{ - fn on_click( - self, - handler: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.map_element(|e| e.on_click(handler)) - } - - fn cursor_style(self, cursor_style: gpui::CursorStyle) -> Self { - self.map_element(|e| e.cursor_style(cursor_style)) - } -} - -impl Toggleable for gpui::AnimationElement -where - T: Toggleable + 'static, -{ - fn toggle_state(self, selected: bool) -> Self { - self.map_element(|e| e.toggle_state(selected)) - } -} - -pub struct PopoverMenuHandle(Rc>>>); - -impl Clone for PopoverMenuHandle { - fn clone(&self) -> Self { - Self(self.0.clone()) - } -} - -impl Default for PopoverMenuHandle { - fn default() -> Self { - Self(Rc::default()) - } -} - -struct PopoverMenuHandleState { - menu_builder: Rc Option>>, - menu: Rc>>>, - on_open: Option>, -} - -impl PopoverMenuHandle { - pub fn show(&self, window: &mut Window, cx: &mut App) { - if let Some(state) = self.0.borrow().as_ref() { - show_menu( - &state.menu_builder, - &state.menu, - state.on_open.clone(), - window, - cx, - ); - } - } - - pub fn hide(&self, cx: &mut App) { - if let Some(state) = self.0.borrow().as_ref() - && let Some(menu) = state.menu.borrow().as_ref() - { - menu.update(cx, |_, cx| cx.emit(DismissEvent)); - } - } - - pub fn toggle(&self, window: &mut Window, cx: &mut App) { - if let Some(state) = self.0.borrow().as_ref() { - if state.menu.borrow().is_some() { - self.hide(cx); - } else { - self.show(window, cx); - } - } - } - - pub fn is_deployed(&self) -> bool { - self.0 - .borrow() - .as_ref() - .is_some_and(|state| state.menu.borrow().as_ref().is_some()) - } - - pub fn is_focused(&self, window: &Window, cx: &App) -> bool { - self.0.borrow().as_ref().is_some_and(|state| { - state - .menu - .borrow() - .as_ref() - .is_some_and(|model| model.focus_handle(cx).is_focused(window)) - }) - } - - pub fn refresh_menu( - &self, - window: &mut Window, - cx: &mut App, - new_menu_builder: Rc Option>>, - ) { - let show_menu = if let Some(state) = self.0.borrow_mut().as_mut() { - state.menu_builder = new_menu_builder; - state.menu.borrow().is_some() - } else { - false - }; - - if show_menu { - self.show(window, cx); - } - } -} - -pub struct PopoverMenu { - id: ElementId, - child_builder: Option< - Box< - dyn FnOnce( - Rc>>>, - Option Option> + 'static>>, - ) -> AnyElement - + 'static, - >, - >, - menu_builder: Option Option> + 'static>>, - anchor: Corner, - attach: Option, - offset: Option>, - trigger_handle: Option>, - on_open: Option>, - full_width: bool, -} - -impl PopoverMenu { - /// Returns a new [`PopoverMenu`]. - pub fn new(id: impl Into) -> Self { - Self { - id: id.into(), - child_builder: None, - menu_builder: None, - anchor: Corner::TopLeft, - attach: None, - offset: None, - trigger_handle: None, - on_open: None, - full_width: false, - } - } - - pub fn full_width(mut self, full_width: bool) -> Self { - self.full_width = full_width; - self - } - - pub fn menu( - mut self, - f: impl Fn(&mut Window, &mut App) -> Option> + 'static, - ) -> Self { - self.menu_builder = Some(Rc::new(f)); - self - } - - pub fn with_handle(mut self, handle: PopoverMenuHandle) -> Self { - self.trigger_handle = Some(handle); - self - } - - pub fn trigger(mut self, t: T) -> Self { - let on_open = self.on_open.clone(); - self.child_builder = Some(Box::new(move |menu, builder| { - let open = menu.borrow().is_some(); - t.toggle_state(open) - .when_some(builder, |el, builder| { - el.on_click(move |_event, window, cx| { - show_menu(&builder, &menu, on_open.clone(), window, cx) - }) - }) - .into_any_element() - })); - self - } - - /// This method prevents the trigger button tooltip from being seen when the menu is open. - pub fn trigger_with_tooltip( - mut self, - t: T, - tooltip_builder: impl Fn(&mut Window, &mut App) -> AnyView + 'static, - ) -> Self { - let on_open = self.on_open.clone(); - self.child_builder = Some(Box::new(move |menu, builder| { - let open = menu.borrow().is_some(); - t.toggle_state(open) - .when_some(builder, |el, builder| { - el.on_click(move |_, window, cx| { - show_menu(&builder, &menu, on_open.clone(), window, cx) - }) - .when(!open, |t| { - t.tooltip(move |window, cx| tooltip_builder(window, cx)) - }) - }) - .into_any_element() - })); - self - } - - /// Defines which corner of the menu to anchor to the attachment point. - /// By default, it uses the cursor position. Also see the `attach` method. - pub fn anchor(mut self, anchor: Corner) -> Self { - self.anchor = anchor; - self - } - - /// Defines which corner of the handle to attach the menu's anchor to. - pub fn attach(mut self, attach: Corner) -> Self { - self.attach = Some(attach); - self - } - - /// Offsets the position of the content by that many pixels. - pub fn offset(mut self, offset: Point) -> Self { - self.offset = Some(offset); - self - } - - /// Attaches something upon opening the menu. - pub fn on_open(mut self, on_open: Rc) -> Self { - self.on_open = Some(on_open); - self - } - - fn resolved_attach(&self) -> Corner { - self.attach.unwrap_or(match self.anchor { - Corner::TopLeft => Corner::BottomLeft, - Corner::TopRight => Corner::BottomRight, - Corner::BottomLeft => Corner::TopLeft, - Corner::BottomRight => Corner::TopRight, - }) - } - - fn resolved_offset(&self, window: &mut Window) -> Point { - self.offset.unwrap_or_else(|| { - // Default offset = 4px padding + 1px border - let offset = rems_from_px(5.) * window.rem_size(); - match self.anchor { - Corner::TopRight | Corner::BottomRight => point(offset, px(0.)), - Corner::TopLeft | Corner::BottomLeft => point(-offset, px(0.)), - } - }) - } -} - -fn show_menu( - builder: &Rc Option>>, - menu: &Rc>>>, - on_open: Option>, - window: &mut Window, - cx: &mut App, -) { - let previous_focus_handle = window.focused(cx); - let Some(new_menu) = (builder)(window, cx) else { - return; - }; - let menu2 = menu.clone(); - - window - .subscribe(&new_menu, cx, move |modal, _: &DismissEvent, window, cx| { - if modal.focus_handle(cx).contains_focused(window, cx) - && let Some(previous_focus_handle) = previous_focus_handle.as_ref() - { - window.focus(previous_focus_handle); - } - *menu2.borrow_mut() = None; - window.refresh(); - }) - .detach(); - window.focus(&new_menu.focus_handle(cx)); - *menu.borrow_mut() = Some(new_menu); - window.refresh(); - - if let Some(on_open) = on_open { - on_open(window, cx); - } -} - -pub struct PopoverMenuElementState { - menu: Rc>>>, - child_bounds: Option>, -} - -impl Clone for PopoverMenuElementState { - fn clone(&self) -> Self { - Self { - menu: Rc::clone(&self.menu), - child_bounds: self.child_bounds, - } - } -} - -impl Default for PopoverMenuElementState { - fn default() -> Self { - Self { - menu: Rc::default(), - child_bounds: None, - } - } -} - -pub struct PopoverMenuFrameState { - child_layout_id: Option, - child_element: Option, - menu_element: Option, - menu_handle: Rc>>>, -} - -impl Element for PopoverMenu { - type RequestLayoutState = PopoverMenuFrameState; - type PrepaintState = Option; - - fn id(&self) -> Option { - Some(self.id.clone()) - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - global_id: Option<&GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (gpui::LayoutId, Self::RequestLayoutState) { - window.with_element_state( - global_id.unwrap(), - |element_state: Option>, window| { - let element_state = element_state.unwrap_or_default(); - let mut menu_layout_id = None; - - let menu_element = element_state.menu.borrow_mut().as_mut().map(|menu| { - let offset = self.resolved_offset(window); - let mut anchored = anchored() - .snap_to_window_with_margin(px(8.)) - .anchor(self.anchor) - .offset(offset); - if let Some(child_bounds) = element_state.child_bounds { - anchored = - anchored.position(child_bounds.corner(self.resolved_attach()) + offset); - } - let mut element = deferred(anchored.child(div().occlude().child(menu.clone()))) - .with_priority(1) - .into_any(); - - menu_layout_id = Some(element.request_layout(window, cx)); - element - }); - - let mut child_element = self.child_builder.take().map(|child_builder| { - (child_builder)(element_state.menu.clone(), self.menu_builder.clone()) - }); - - if let Some(trigger_handle) = self.trigger_handle.take() - && let Some(menu_builder) = self.menu_builder.clone() - { - *trigger_handle.0.borrow_mut() = Some(PopoverMenuHandleState { - menu_builder, - menu: element_state.menu.clone(), - on_open: self.on_open.clone(), - }); - } - - let child_layout_id = child_element - .as_mut() - .map(|child_element| child_element.request_layout(window, cx)); - - let mut style = Style::default(); - if self.full_width { - style.size = size(relative(1.).into(), Length::Auto); - } - - let layout_id = window.request_layout( - style, - menu_layout_id.into_iter().chain(child_layout_id), - cx, - ); - - ( - ( - layout_id, - PopoverMenuFrameState { - child_element, - child_layout_id, - menu_element, - menu_handle: element_state.menu.clone(), - }, - ), - element_state, - ) - }, - ) - } - - fn prepaint( - &mut self, - global_id: Option<&GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - _bounds: Bounds, - request_layout: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Option { - if let Some(child) = request_layout.child_element.as_mut() { - child.prepaint(window, cx); - } - - if let Some(menu) = request_layout.menu_element.as_mut() { - menu.prepaint(window, cx); - } - - request_layout.child_layout_id.map(|layout_id| { - let bounds = window.layout_bounds(layout_id); - window.with_element_state(global_id.unwrap(), |element_state, _cx| { - let mut element_state: PopoverMenuElementState = element_state.unwrap(); - element_state.child_bounds = Some(bounds); - ((), element_state) - }); - - window.insert_hitbox(bounds, HitboxBehavior::Normal).id - }) - } - - fn paint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - _: Bounds, - request_layout: &mut Self::RequestLayoutState, - child_hitbox: &mut Option, - window: &mut Window, - cx: &mut App, - ) { - if let Some(mut child) = request_layout.child_element.take() { - child.paint(window, cx); - } - - if let Some(mut menu) = request_layout.menu_element.take() { - menu.paint(window, cx); - - if let Some(child_hitbox) = *child_hitbox { - let menu_handle = request_layout.menu_handle.clone(); - // Mouse-downing outside the menu dismisses it, so we don't - // want a click on the toggle to re-open it. - window.on_mouse_event(move |_: &MouseDownEvent, phase, window, cx| { - if phase == DispatchPhase::Bubble && child_hitbox.is_hovered(window) { - if let Some(menu) = menu_handle.borrow().as_ref() { - menu.update(cx, |_, cx| { - cx.emit(DismissEvent); - }); - } - cx.stop_propagation(); - } - }) - } - } - } -} - -impl IntoElement for PopoverMenu { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} diff --git a/crates/ui/src/components/progress.rs b/crates/ui/src/components/progress.rs deleted file mode 100644 index bfaf7f3dcf..0000000000 --- a/crates/ui/src/components/progress.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod progress_bar; -pub use progress_bar::*; diff --git a/crates/ui/src/components/progress/progress_bar.rs b/crates/ui/src/components/progress/progress_bar.rs deleted file mode 100644 index 5cc5abd36d..0000000000 --- a/crates/ui/src/components/progress/progress_bar.rs +++ /dev/null @@ -1,156 +0,0 @@ -use documented::Documented; -use gpui::{Hsla, point}; - -use crate::components::Label; -use crate::prelude::*; - -/// A progress bar is a horizontal bar that communicates the status of a process. -/// -/// A progress bar should not be used to represent indeterminate progress. -#[derive(IntoElement, RegisterComponent, Documented)] -pub struct ProgressBar { - id: ElementId, - value: f32, - max_value: f32, - bg_color: Hsla, - over_color: Hsla, - fg_color: Hsla, -} - -impl ProgressBar { - pub fn new(id: impl Into, value: f32, max_value: f32, cx: &App) -> Self { - Self { - id: id.into(), - value, - max_value, - bg_color: cx.theme().colors().background, - over_color: cx.theme().status().error, - fg_color: cx.theme().status().info, - } - } - - /// Sets the current value of the progress bar. - pub fn value(mut self, value: f32) -> Self { - self.value = value; - self - } - - /// Sets the maximum value of the progress bar. - pub fn max_value(mut self, max_value: f32) -> Self { - self.max_value = max_value; - self - } - - /// Sets the background color of the progress bar. - pub fn bg_color(mut self, color: Hsla) -> Self { - self.bg_color = color; - self - } - - /// Sets the foreground color of the progress bar. - pub fn fg_color(mut self, color: Hsla) -> Self { - self.fg_color = color; - self - } - - /// Sets the over limit color of the progress bar. - pub fn over_color(mut self, color: Hsla) -> Self { - self.over_color = color; - self - } -} - -impl RenderOnce for ProgressBar { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let fill_width = (self.value / self.max_value).clamp(0.02, 1.0); - - div() - .id(self.id.clone()) - .w_full() - .h(px(8.0)) - .rounded_full() - .p(px(2.0)) - .bg(self.bg_color) - .shadow(vec![gpui::BoxShadow { - color: gpui::black().opacity(0.08), - offset: point(px(0.), px(1.)), - blur_radius: px(0.), - spread_radius: px(0.), - }]) - .child( - div() - .h_full() - .rounded_full() - .when(self.value > self.max_value, |div| div.bg(self.over_color)) - .when(self.value <= self.max_value, |div| div.bg(self.fg_color)) - .w(relative(fill_width)), - ) - } -} - -impl Component for ProgressBar { - fn scope() -> ComponentScope { - ComponentScope::Status - } - - fn description() -> Option<&'static str> { - Some(Self::DOCS) - } - - fn preview(_window: &mut Window, cx: &mut App) -> Option { - let max_value = 180.0; - - Some( - div() - .flex() - .flex_col() - .gap_4() - .p_4() - .w(px(240.0)) - .child(div().child("Progress Bar")) - .child( - div() - .flex() - .flex_col() - .gap_2() - .child( - div() - .flex() - .justify_between() - .child(Label::new("0%")) - .child(Label::new("Empty")), - ) - .child(ProgressBar::new("empty", 0.0, max_value, cx)), - ) - .child( - div() - .flex() - .flex_col() - .gap_2() - .child( - div() - .flex() - .justify_between() - .child(Label::new("38%")) - .child(Label::new("Partial")), - ) - .child(ProgressBar::new("partial", max_value * 0.35, max_value, cx)), - ) - .child( - div() - .flex() - .flex_col() - .gap_2() - .child( - div() - .flex() - .justify_between() - .child(Label::new("100%")) - .child(Label::new("Complete")), - ) - .child(ProgressBar::new("filled", max_value, max_value, cx)), - ) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/radio.rs b/crates/ui/src/components/radio.rs deleted file mode 100644 index c7e19f5c34..0000000000 --- a/crates/ui/src/components/radio.rs +++ /dev/null @@ -1,60 +0,0 @@ -use std::sync::Arc; - -use crate::prelude::*; - -#[derive(IntoElement)] -pub struct RadioWithLabel { - id: ElementId, - label: Label, - selected: bool, - on_click: Arc, -} - -impl RadioWithLabel { - pub fn new( - id: impl Into, - label: Label, - selected: bool, - on_click: impl Fn(&bool, &mut Window, &mut App) + 'static, - ) -> Self { - Self { - id: id.into(), - label, - selected, - on_click: Arc::new(on_click), - } - } -} - -impl RenderOnce for RadioWithLabel { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let inner_diameter = rems_from_px(6.); - let outer_diameter = rems_from_px(16.); - let border_width = rems_from_px(1.); - h_flex() - .id(self.id) - .gap(DynamicSpacing::Base08.rems(cx)) - .group("") - .child( - div() - .size(outer_diameter) - .rounded(outer_diameter / 2.) - .border_color(cx.theme().colors().border) - .border(border_width) - .group_hover("", |el| el.bg(cx.theme().colors().element_hover)) - .when(self.selected, |el| { - el.child( - div() - .m((outer_diameter - inner_diameter) / 2. - border_width) - .size(inner_diameter) - .rounded(inner_diameter / 2.) - .bg(cx.theme().colors().icon_accent), - ) - }), - ) - .child(self.label) - .on_click(move |_event, window, cx| { - (self.on_click)(&true, window, cx); - }) - } -} diff --git a/crates/ui/src/components/right_click_menu.rs b/crates/ui/src/components/right_click_menu.rs deleted file mode 100644 index dff4230737..0000000000 --- a/crates/ui/src/components/right_click_menu.rs +++ /dev/null @@ -1,287 +0,0 @@ -use std::{cell::RefCell, rc::Rc}; - -use gpui::{ - AnyElement, App, Bounds, Corner, DismissEvent, DispatchPhase, Element, ElementId, Entity, - Focusable as _, GlobalElementId, Hitbox, HitboxBehavior, InteractiveElement, IntoElement, - LayoutId, ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Window, - anchored, deferred, div, px, -}; - -pub struct RightClickMenu { - id: ElementId, - child_builder: Option AnyElement + 'static>>, - menu_builder: Option Entity + 'static>>, - anchor: Option, - attach: Option, -} - -impl RightClickMenu { - pub fn menu(mut self, f: impl Fn(&mut Window, &mut App) -> Entity + 'static) -> Self { - self.menu_builder = Some(Rc::new(f)); - self - } - - pub fn trigger(mut self, e: F) -> Self - where - F: FnOnce(bool, &mut Window, &mut App) -> E + 'static, - E: IntoElement + 'static, - { - self.child_builder = Some(Box::new(move |is_menu_active, window, cx| { - e(is_menu_active, window, cx).into_any_element() - })); - self - } - - /// anchor defines which corner of the menu to anchor to the attachment point - /// (by default the cursor position, but see attach) - pub fn anchor(mut self, anchor: Corner) -> Self { - self.anchor = Some(anchor); - self - } - - /// attach defines which corner of the handle to attach the menu's anchor to - pub fn attach(mut self, attach: Corner) -> Self { - self.attach = Some(attach); - self - } - - fn with_element_state( - &mut self, - global_id: &GlobalElementId, - window: &mut Window, - cx: &mut App, - f: impl FnOnce(&mut Self, &mut MenuHandleElementState, &mut Window, &mut App) -> R, - ) -> R { - window.with_optional_element_state::, _>( - Some(global_id), - |element_state, window| { - let mut element_state = element_state.unwrap().unwrap_or_default(); - let result = f(self, &mut element_state, window, cx); - (result, Some(element_state)) - }, - ) - } -} - -/// Creates a [`RightClickMenu`] -pub fn right_click_menu(id: impl Into) -> RightClickMenu { - RightClickMenu { - id: id.into(), - child_builder: None, - menu_builder: None, - anchor: None, - attach: None, - } -} - -pub struct MenuHandleElementState { - menu: Rc>>>, - position: Rc>>, -} - -impl Clone for MenuHandleElementState { - fn clone(&self) -> Self { - Self { - menu: Rc::clone(&self.menu), - position: Rc::clone(&self.position), - } - } -} - -impl Default for MenuHandleElementState { - fn default() -> Self { - Self { - menu: Rc::default(), - position: Rc::default(), - } - } -} - -pub struct RequestLayoutState { - child_layout_id: Option, - child_element: Option, - menu_element: Option, -} - -pub struct PrepaintState { - hitbox: Hitbox, - child_bounds: Option>, -} - -impl Element for RightClickMenu { - type RequestLayoutState = RequestLayoutState; - type PrepaintState = PrepaintState; - - fn id(&self) -> Option { - Some(self.id.clone()) - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - id: Option<&GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (gpui::LayoutId, Self::RequestLayoutState) { - self.with_element_state( - id.unwrap(), - window, - cx, - |this, element_state, window, cx| { - let mut menu_layout_id = None; - - let menu_element = element_state.menu.borrow_mut().as_mut().map(|menu| { - let mut anchored = anchored().snap_to_window_with_margin(px(8.)); - if let Some(anchor) = this.anchor { - anchored = anchored.anchor(anchor); - } - anchored = anchored.position(*element_state.position.borrow()); - - let mut element = deferred(anchored.child(div().occlude().child(menu.clone()))) - .with_priority(1) - .into_any(); - - menu_layout_id = Some(element.request_layout(window, cx)); - element - }); - - let mut child_element = this.child_builder.take().map(|child_builder| { - (child_builder)(element_state.menu.borrow().is_some(), window, cx) - }); - - let child_layout_id = child_element - .as_mut() - .map(|child_element| child_element.request_layout(window, cx)); - - let layout_id = window.request_layout( - gpui::Style::default(), - menu_layout_id.into_iter().chain(child_layout_id), - cx, - ); - - ( - layout_id, - RequestLayoutState { - child_element, - child_layout_id, - menu_element, - }, - ) - }, - ) - } - - fn prepaint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - bounds: Bounds, - request_layout: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> PrepaintState { - let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal); - - if let Some(child) = request_layout.child_element.as_mut() { - child.prepaint(window, cx); - } - - if let Some(menu) = request_layout.menu_element.as_mut() { - menu.prepaint(window, cx); - } - - PrepaintState { - hitbox, - child_bounds: request_layout - .child_layout_id - .map(|layout_id| window.layout_bounds(layout_id)), - } - } - - fn paint( - &mut self, - id: Option<&GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - _bounds: Bounds, - request_layout: &mut Self::RequestLayoutState, - prepaint_state: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - self.with_element_state( - id.unwrap(), - window, - cx, - |this, element_state, window, cx| { - if let Some(mut child) = request_layout.child_element.take() { - child.paint(window, cx); - } - - if let Some(mut menu) = request_layout.menu_element.take() { - menu.paint(window, cx); - } - - let Some(builder) = this.menu_builder.take() else { - return; - }; - - let attach = this.attach; - let menu = element_state.menu.clone(); - let position = element_state.position.clone(); - let child_bounds = prepaint_state.child_bounds; - - let hitbox_id = prepaint_state.hitbox.id; - window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| { - if phase == DispatchPhase::Bubble - && event.button == MouseButton::Right - && hitbox_id.is_hovered(window) - { - cx.stop_propagation(); - window.prevent_default(); - - let new_menu = (builder)(window, cx); - let menu2 = menu.clone(); - let previous_focus_handle = window.focused(cx); - - window - .subscribe(&new_menu, cx, move |modal, _: &DismissEvent, window, cx| { - if modal.focus_handle(cx).contains_focused(window, cx) - && let Some(previous_focus_handle) = - previous_focus_handle.as_ref() - { - window.focus(previous_focus_handle); - } - *menu2.borrow_mut() = None; - window.refresh(); - }) - .detach(); - window.focus(&new_menu.focus_handle(cx)); - *menu.borrow_mut() = Some(new_menu); - *position.borrow_mut() = if let Some(child_bounds) = child_bounds { - if let Some(attach) = attach { - child_bounds.corner(attach) - } else { - window.mouse_position() - } - } else { - window.mouse_position() - }; - window.refresh(); - } - }); - }, - ) - } -} - -impl IntoElement for RightClickMenu { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} diff --git a/crates/ui/src/components/scrollbar.rs b/crates/ui/src/components/scrollbar.rs deleted file mode 100644 index 391d480fb3..0000000000 --- a/crates/ui/src/components/scrollbar.rs +++ /dev/null @@ -1,1466 +0,0 @@ -use std::{ - any::Any, - fmt::Debug, - ops::Not, - time::{Duration, Instant}, -}; - -use gpui::{ - Along, App, AppContext as _, Axis as ScrollbarAxis, BorderStyle, Bounds, ContentMask, Context, - Corner, Corners, CursorStyle, DispatchPhase, Div, Edges, Element, ElementId, Entity, EntityId, - GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, - LayoutId, ListState, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Negate, - ParentElement, Pixels, Point, Position, Render, ScrollHandle, ScrollWheelEvent, Size, Stateful, - StatefulInteractiveElement, Style, Styled, Task, UniformListDecoration, - UniformListScrollHandle, Window, ease_in_out, prelude::FluentBuilder as _, px, quad, relative, - size, -}; -use settings::SettingsStore; -use smallvec::SmallVec; -use theme::ActiveTheme as _; -use util::ResultExt; - -use std::ops::Range; - -use crate::scrollbars::{ScrollbarAutoHide, ScrollbarVisibility, ShowScrollbar}; - -const SCROLLBAR_HIDE_DELAY_INTERVAL: Duration = Duration::from_secs(1); -const SCROLLBAR_HIDE_DURATION: Duration = Duration::from_millis(400); -const SCROLLBAR_SHOW_DURATION: Duration = Duration::from_millis(50); - -const SCROLLBAR_PADDING: Pixels = px(4.); - -pub mod scrollbars { - use gpui::{App, Global}; - use schemars::JsonSchema; - use serde::{Deserialize, Serialize}; - use settings::Settings; - - /// When to show the scrollbar in the editor. - /// - /// Default: auto - #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] - #[serde(rename_all = "snake_case")] - pub enum ShowScrollbar { - /// Show the scrollbar if there's important information or - /// follow the system's configured behavior. - #[default] - Auto, - /// Match the system's configured behavior. - System, - /// Always show the scrollbar. - Always, - /// Never show the scrollbar. - Never, - } - - impl From for ShowScrollbar { - fn from(value: settings::ShowScrollbar) -> Self { - match value { - settings::ShowScrollbar::Auto => ShowScrollbar::Auto, - settings::ShowScrollbar::System => ShowScrollbar::System, - settings::ShowScrollbar::Always => ShowScrollbar::Always, - settings::ShowScrollbar::Never => ShowScrollbar::Never, - } - } - } - - pub trait GlobalSetting { - fn get_value(cx: &App) -> &Self; - } - - impl GlobalSetting for T { - fn get_value(cx: &App) -> &T { - T::get_global(cx) - } - } - - pub trait ScrollbarVisibility: GlobalSetting + 'static { - fn visibility(&self, cx: &App) -> ShowScrollbar; - } - - #[derive(Default)] - pub struct ScrollbarAutoHide(pub bool); - - impl ScrollbarAutoHide { - pub fn should_hide(&self) -> bool { - self.0 - } - } - - impl Global for ScrollbarAutoHide {} -} - -fn get_scrollbar_state( - mut config: Scrollbars, - caller_location: &'static std::panic::Location, - window: &mut Window, - cx: &mut App, -) -> Entity> -where - T: ScrollableHandle, -{ - let element_id = config.id.take().unwrap_or_else(|| caller_location.into()); - let track_color = config.track_color; - - let state = window.use_keyed_state(element_id, cx, |window, cx| { - let parent_id = cx.entity_id(); - ScrollbarStateWrapper( - cx.new(|cx| ScrollbarState::new_from_config(config, parent_id, window, cx)), - ) - }); - - state.update(cx, |state, cx| { - state - .0 - .update(cx, |state, _cx| state.update_track_color(track_color)) - }); - state -} - -pub trait WithScrollbar: Sized { - type Output; - - fn custom_scrollbars( - self, - config: Scrollbars, - window: &mut Window, - cx: &mut App, - ) -> Self::Output - where - T: ScrollableHandle; - - // TODO: account for these cases properly - // #[track_caller] - // fn horizontal_scrollbar(self, window: &mut Window, cx: &mut App) -> Self::Output { - // self.custom_scrollbars( - // Scrollbars::new(ScrollAxes::Horizontal).ensure_id(core::panic::Location::caller()), - // window, - // cx, - // ) - // } - - // #[track_caller] - // fn vertical_scrollbar(self, window: &mut Window, cx: &mut App) -> Self::Output { - // self.custom_scrollbars( - // Scrollbars::new(ScrollAxes::Vertical).ensure_id(core::panic::Location::caller()), - // window, - // cx, - // ) - // } - - #[track_caller] - fn vertical_scrollbar_for( - self, - scroll_handle: &ScrollHandle, - window: &mut Window, - cx: &mut App, - ) -> Self::Output { - self.custom_scrollbars( - Scrollbars::new(ScrollAxes::Vertical) - .tracked_scroll_handle(scroll_handle) - .ensure_id(core::panic::Location::caller()), - window, - cx, - ) - } -} - -impl WithScrollbar for Stateful
{ - type Output = Self; - - #[track_caller] - fn custom_scrollbars( - self, - config: Scrollbars, - window: &mut Window, - cx: &mut App, - ) -> Self::Output - where - T: ScrollableHandle, - { - render_scrollbar( - get_scrollbar_state(config, std::panic::Location::caller(), window, cx), - self, - cx, - ) - } -} - -impl WithScrollbar for Div { - type Output = Stateful
; - - #[track_caller] - fn custom_scrollbars( - self, - config: Scrollbars, - window: &mut Window, - cx: &mut App, - ) -> Self::Output - where - T: ScrollableHandle, - { - let scrollbar = get_scrollbar_state(config, std::panic::Location::caller(), window, cx); - // We know this ID stays consistent as long as the element is rendered for - // consecutive frames, which is sufficient for our use case here - let scrollbar_entity_id = scrollbar.entity_id(); - - render_scrollbar( - scrollbar, - self.id(("track-scroll", scrollbar_entity_id)), - cx, - ) - } -} - -fn render_scrollbar( - scrollbar: Entity>, - div: Stateful
, - cx: &App, -) -> Stateful
-where - T: ScrollableHandle, -{ - let state = &scrollbar.read(cx).0; - - div.when_some(state.read(cx).handle_to_track(), |this, handle| { - this.track_scroll(handle).when_some( - state.read(cx).visible_axes(), - |this, axes| match axes { - ScrollAxes::Horizontal => this.overflow_x_scroll(), - ScrollAxes::Vertical => this.overflow_y_scroll(), - ScrollAxes::Both => this.overflow_scroll(), - }, - ) - }) - .when_some( - state - .read(cx) - .space_to_reserve_for(ScrollbarAxis::Horizontal), - |this, space| this.pb(space), - ) - .when_some( - state.read(cx).space_to_reserve_for(ScrollbarAxis::Vertical), - |this, space| this.pr(space), - ) - .child(state.clone()) -} - -impl UniformListDecoration for ScrollbarStateWrapper { - fn compute( - &self, - _visible_range: Range, - _bounds: Bounds, - scroll_offset: Point, - _item_height: Pixels, - _item_count: usize, - _window: &mut Window, - _cx: &mut App, - ) -> gpui::AnyElement { - ScrollbarElement { - origin: scroll_offset.negate(), - state: self.0.clone(), - } - .into_any() - } -} - -// impl WithScrollbar for UniformList { -// type Output = Self; - -// #[track_caller] -// fn custom_scrollbars( -// self, -// config: Scrollbars, -// window: &mut Window, -// cx: &mut App, -// ) -> Self::Output -// where -// S: ScrollbarVisibilitySetting, -// T: ScrollableHandle, -// { -// let scrollbar = get_scrollbar_state(config, std::panic::Location::caller(), window, cx); -// self.when_some( -// scrollbar.read_with(cx, |wrapper, cx| { -// wrapper -// .0 -// .read(cx) -// .handle_to_track::() -// .cloned() -// }), -// |this, handle| this.track_scroll(handle), -// ) -// .with_decoration(scrollbar) -// } -// } - -#[derive(Copy, Clone, PartialEq, Eq)] -enum ShowBehavior { - Always, - Autohide, - Never, -} - -impl ShowBehavior { - fn from_setting(setting: ShowScrollbar, cx: &mut App) -> Self { - match setting { - ShowScrollbar::Never => Self::Never, - ShowScrollbar::Auto => Self::Autohide, - ShowScrollbar::System => { - if cx.default_global::().should_hide() { - Self::Autohide - } else { - Self::Always - } - } - ShowScrollbar::Always => Self::Always, - } - } -} - -pub enum ScrollAxes { - Horizontal, - Vertical, - Both, -} - -impl ScrollAxes { - fn apply_to(self, point: Point, value: T) -> Point - where - T: Debug + Default + PartialEq + Clone, - { - match self { - Self::Horizontal => point.apply_along(ScrollbarAxis::Horizontal, |_| value), - Self::Vertical => point.apply_along(ScrollbarAxis::Vertical, |_| value), - Self::Both => Point::new(value.clone(), value), - } - } -} - -#[derive(Clone, Debug, Default, PartialEq)] -enum ReservedSpace { - #[default] - None, - Thumb, - Track, -} - -impl ReservedSpace { - fn is_visible(&self) -> bool { - *self != ReservedSpace::None - } - - fn needs_scroll_track(&self) -> bool { - *self == ReservedSpace::Track - } -} - -#[derive(Debug, Default, Clone, Copy)] -enum ScrollbarWidth { - #[default] - Normal, - Small, - XSmall, -} - -impl ScrollbarWidth { - fn to_pixels(&self) -> Pixels { - match self { - ScrollbarWidth::Normal => px(8.), - ScrollbarWidth::Small => px(6.), - ScrollbarWidth::XSmall => px(4.), - } - } -} - -#[derive(Clone)] -enum Handle { - Tracked(T), - Untracked(fn() -> T), -} - -#[derive(Clone)] -pub struct Scrollbars { - id: Option, - get_visibility: fn(&App) -> ShowScrollbar, - tracked_entity: Option>, - scrollable_handle: Handle, - visibility: Point, - track_color: Option, - scrollbar_width: ScrollbarWidth, -} - -impl Scrollbars { - pub fn new(show_along: ScrollAxes) -> Self { - Self::new_with_setting(show_along, |_| ShowScrollbar::default()) - } - - pub fn for_settings() -> Scrollbars { - Scrollbars::new_with_setting(ScrollAxes::Both, |cx| S::get_value(cx).visibility(cx)) - } -} - -impl Scrollbars { - fn new_with_setting(show_along: ScrollAxes, get_visibility: fn(&App) -> ShowScrollbar) -> Self { - Self { - id: None, - get_visibility, - scrollable_handle: Handle::Untracked(ScrollHandle::new), - tracked_entity: None, - visibility: show_along.apply_to(Default::default(), ReservedSpace::Thumb), - track_color: None, - scrollbar_width: ScrollbarWidth::Normal, - } - } -} - -impl Scrollbars { - pub fn id(mut self, id: impl Into) -> Self { - self.id = Some(id.into()); - self - } - - fn ensure_id(mut self, id: impl Into) -> Self { - if self.id.is_none() { - self.id = Some(id.into()); - } - self - } - - /// Notify the current context whenever this scrollbar gets a scroll event - pub fn notify_content(mut self) -> Self { - self.tracked_entity = Some(None); - self - } - - /// Set a parent model which should be notified whenever this scrollbar gets a scroll event. - pub fn tracked_entity(mut self, entity_id: EntityId) -> Self { - self.tracked_entity = Some(Some(entity_id)); - self - } - - pub fn tracked_scroll_handle( - self, - tracked_scroll_handle: &TrackedHandle, - ) -> Scrollbars { - let Self { - id, - tracked_entity: tracked_entity_id, - scrollbar_width, - visibility, - get_visibility, - track_color, - .. - } = self; - - Scrollbars { - scrollable_handle: Handle::Tracked(tracked_scroll_handle.clone()), - id, - tracked_entity: tracked_entity_id, - visibility, - scrollbar_width, - track_color, - get_visibility, - } - } - - pub fn show_along(mut self, along: ScrollAxes) -> Self { - self.visibility = along.apply_to(self.visibility, ReservedSpace::Thumb); - self - } - - pub fn with_track_along(mut self, along: ScrollAxes, background_color: Hsla) -> Self { - self.visibility = along.apply_to(self.visibility, ReservedSpace::Track); - self.track_color = Some(background_color); - self - } - - pub fn width_sm(mut self) -> Self { - self.scrollbar_width = ScrollbarWidth::Small; - self - } - - pub fn width_xs(mut self) -> Self { - self.scrollbar_width = ScrollbarWidth::XSmall; - self - } -} - -#[derive(PartialEq, Clone, Debug)] -enum VisibilityState { - Visible, - Animating { showing: bool, delta: f32 }, - Hidden, - Disabled, -} - -const DELTA_MAX: f32 = 1.0; - -impl VisibilityState { - fn from_behavior(behavior: ShowBehavior) -> Self { - match behavior { - ShowBehavior::Always => Self::Visible, - ShowBehavior::Never => Self::Disabled, - ShowBehavior::Autohide => Self::for_show(), - } - } - - fn for_show() -> Self { - Self::Animating { - showing: true, - delta: Default::default(), - } - } - - fn for_autohide() -> Self { - Self::Animating { - showing: Default::default(), - delta: Default::default(), - } - } - - fn is_visible(&self) -> bool { - matches!(self, Self::Visible | Self::Animating { .. }) - } - - #[inline] - fn is_disabled(&self) -> bool { - *self == VisibilityState::Disabled - } - - fn animation_progress(&self) -> Option<(f32, Duration, bool)> { - match self { - Self::Animating { showing, delta } => Some(( - *delta, - if *showing { - SCROLLBAR_SHOW_DURATION - } else { - SCROLLBAR_HIDE_DURATION - }, - *showing, - )), - _ => None, - } - } - - fn set_delta(&mut self, new_delta: f32) { - match self { - Self::Animating { showing, .. } if new_delta >= DELTA_MAX => { - if *showing { - *self = Self::Visible; - } else { - *self = Self::Hidden; - } - } - Self::Animating { delta, .. } => *delta = new_delta, - _ => {} - } - } - - fn toggle_visible(&self, show_behavior: ShowBehavior) -> Self { - match self { - Self::Hidden => { - if show_behavior == ShowBehavior::Autohide { - Self::for_show() - } else { - Self::Visible - } - } - Self::Animating { - showing: false, - delta: progress, - } => Self::Animating { - showing: true, - delta: DELTA_MAX - progress, - }, - _ => self.clone(), - } - } -} - -enum ParentHoverEvent { - Within, - Entered, - Exited, - Outside, -} - -/// This is used to ensure notifies within the state do not notify the parent -/// unintentionally. -struct ScrollbarStateWrapper(Entity>); - -/// A scrollbar state that should be persisted across frames. -struct ScrollbarState { - thumb_state: ThumbState, - notify_id: Option, - manually_added: bool, - scroll_handle: T, - width: ScrollbarWidth, - show_behavior: ShowBehavior, - get_visibility: fn(&App) -> ShowScrollbar, - visibility: Point, - track_color: Option, - show_state: VisibilityState, - mouse_in_parent: bool, - last_prepaint_state: Option, - _auto_hide_task: Option>, -} - -impl ScrollbarState { - fn new_from_config( - config: Scrollbars, - parent_id: EntityId, - window: &mut Window, - cx: &mut Context, - ) -> Self { - cx.observe_global_in::(window, Self::settings_changed) - .detach(); - - let (manually_added, scroll_handle) = match config.scrollable_handle { - Handle::Tracked(handle) => (true, handle), - Handle::Untracked(func) => (false, func()), - }; - - let show_behavior = ShowBehavior::from_setting((config.get_visibility)(cx), cx); - ScrollbarState { - thumb_state: Default::default(), - notify_id: config.tracked_entity.map(|id| id.unwrap_or(parent_id)), - manually_added, - scroll_handle, - width: config.scrollbar_width, - visibility: config.visibility, - track_color: config.track_color, - show_behavior, - get_visibility: config.get_visibility, - show_state: VisibilityState::from_behavior(show_behavior), - mouse_in_parent: true, - last_prepaint_state: None, - _auto_hide_task: None, - } - } - - fn settings_changed(&mut self, window: &mut Window, cx: &mut Context) { - self.set_show_behavior( - ShowBehavior::from_setting((self.get_visibility)(cx), cx), - window, - cx, - ); - } - - /// Schedules a scrollbar auto hide if no auto hide is currently in progress yet. - fn schedule_auto_hide(&mut self, window: &mut Window, cx: &mut Context) { - if self._auto_hide_task.is_none() { - self._auto_hide_task = (self.visible() && self.show_behavior == ShowBehavior::Autohide) - .then(|| { - cx.spawn_in(window, async move |scrollbar_state, cx| { - cx.background_executor() - .timer(SCROLLBAR_HIDE_DELAY_INTERVAL) - .await; - scrollbar_state - .update(cx, |state, cx| { - if state.thumb_state == ThumbState::Inactive { - state.set_visibility(VisibilityState::for_autohide(), cx); - } - state._auto_hide_task.take(); - }) - .log_err(); - }) - }); - } - } - - fn show_scrollbars(&mut self, window: &mut Window, cx: &mut Context) { - let visibility = self.show_state.toggle_visible(self.show_behavior); - self.set_visibility(visibility, cx); - self._auto_hide_task.take(); - self.schedule_auto_hide(window, cx); - } - - fn set_show_behavior( - &mut self, - behavior: ShowBehavior, - window: &mut Window, - cx: &mut Context, - ) { - if self.show_behavior != behavior { - self.show_behavior = behavior; - self.set_visibility(VisibilityState::from_behavior(behavior), cx); - self.schedule_auto_hide(window, cx); - cx.notify(); - } - } - - fn set_visibility(&mut self, visibility: VisibilityState, cx: &mut Context) { - if self.show_state != visibility { - self.show_state = visibility; - cx.notify(); - } - } - - #[inline] - fn visible_axes(&self) -> Option { - match (&self.visibility.x, &self.visibility.y) { - (ReservedSpace::None, ReservedSpace::None) => None, - (ReservedSpace::None, _) => Some(ScrollAxes::Vertical), - (_, ReservedSpace::None) => Some(ScrollAxes::Horizontal), - _ => Some(ScrollAxes::Both), - } - } - - fn space_to_reserve_for(&self, axis: ScrollbarAxis) -> Option { - (self.show_state.is_disabled().not() - && self.visibility.along(axis).needs_scroll_track() - && self - .scroll_handle() - .max_offset() - .along(axis) - .is_zero() - .not()) - .then(|| self.space_to_reserve()) - } - - fn space_to_reserve(&self) -> Pixels { - self.width.to_pixels() + 2 * SCROLLBAR_PADDING - } - - fn handle_to_track(&self) -> Option<&Handle> { - (!self.manually_added) - .then(|| (self.scroll_handle() as &dyn Any).downcast_ref::()) - .flatten() - } - - fn scroll_handle(&self) -> &T { - &self.scroll_handle - } - - fn set_offset(&mut self, offset: Point, cx: &mut Context) { - self.scroll_handle.set_offset(offset); - self.notify_parent(cx); - cx.notify(); - } - - fn is_dragging(&self) -> bool { - self.thumb_state.is_dragging() - } - - fn set_dragging( - &mut self, - axis: ScrollbarAxis, - drag_offset: Pixels, - window: &mut Window, - cx: &mut Context, - ) { - self.set_thumb_state(ThumbState::Dragging(axis, drag_offset), window, cx); - self.scroll_handle().drag_started(); - } - - fn update_hovered_thumb( - &mut self, - position: &Point, - window: &mut Window, - cx: &mut Context, - ) { - self.set_thumb_state( - if let Some(&ScrollbarLayout { axis, .. }) = - self.last_prepaint_state.as_ref().and_then(|state| { - state - .thumb_for_position(position) - .filter(|thumb| thumb.cursor_hitbox.is_hovered(window)) - }) - { - ThumbState::Hover(axis) - } else { - ThumbState::Inactive - }, - window, - cx, - ); - } - - fn set_thumb_state(&mut self, state: ThumbState, window: &mut Window, cx: &mut Context) { - if self.thumb_state != state { - if state == ThumbState::Inactive { - self.schedule_auto_hide(window, cx); - } else { - self.set_visibility(self.show_state.toggle_visible(self.show_behavior), cx); - self._auto_hide_task.take(); - } - self.thumb_state = state; - cx.notify(); - } - } - - fn update_parent_hovered(&mut self, window: &Window) -> ParentHoverEvent { - let last_parent_hovered = self.mouse_in_parent; - self.mouse_in_parent = self.parent_hovered(window); - let state_changed = self.mouse_in_parent != last_parent_hovered; - match (self.mouse_in_parent, state_changed) { - (true, true) => ParentHoverEvent::Entered, - (true, false) => ParentHoverEvent::Within, - (false, true) => ParentHoverEvent::Exited, - (false, false) => ParentHoverEvent::Outside, - } - } - - fn update_track_color(&mut self, track_color: Option) { - self.track_color = track_color; - } - - fn parent_hovered(&self, window: &Window) -> bool { - self.last_prepaint_state - .as_ref() - .is_some_and(|state| state.parent_bounds_hitbox.is_hovered(window)) - } - - fn hit_for_position(&self, position: &Point) -> Option<&ScrollbarLayout> { - self.last_prepaint_state - .as_ref() - .and_then(|state| state.hit_for_position(position)) - } - - fn thumb_for_axis(&self, axis: ScrollbarAxis) -> Option<&ScrollbarLayout> { - self.last_prepaint_state - .as_ref() - .and_then(|state| state.thumbs.iter().find(|thumb| thumb.axis == axis)) - } - - fn thumb_ranges( - &self, - ) -> impl Iterator, ReservedSpace)> + '_ { - const MINIMUM_THUMB_SIZE: Pixels = px(25.); - let max_offset = self.scroll_handle().max_offset(); - let viewport_size = self.scroll_handle().viewport().size; - let current_offset = self.scroll_handle().offset(); - - [ScrollbarAxis::Horizontal, ScrollbarAxis::Vertical] - .into_iter() - .filter(|&axis| self.visibility.along(axis).is_visible()) - .flat_map(move |axis| { - let max_offset = max_offset.along(axis); - let viewport_size = viewport_size.along(axis); - if max_offset.is_zero() || viewport_size.is_zero() { - return None; - } - let content_size = viewport_size + max_offset; - let visible_percentage = viewport_size / content_size; - let thumb_size = MINIMUM_THUMB_SIZE.max(viewport_size * visible_percentage); - if thumb_size > viewport_size { - return None; - } - let current_offset = current_offset - .along(axis) - .clamp(-max_offset, Pixels::ZERO) - .abs(); - let start_offset = (current_offset / max_offset) * (viewport_size - thumb_size); - let thumb_percentage_start = start_offset / viewport_size; - let thumb_percentage_end = (start_offset + thumb_size) / viewport_size; - Some(( - axis, - thumb_percentage_start..thumb_percentage_end, - self.visibility.along(axis), - )) - }) - } - - fn visible(&self) -> bool { - self.show_state.is_visible() - } - - #[inline] - fn disabled(&self) -> bool { - self.show_state.is_disabled() - } - - fn notify_parent(&self, cx: &mut App) { - if let Some(entity_id) = self.notify_id { - cx.notify(entity_id); - } - } -} - -impl Render for ScrollbarState { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - ScrollbarElement { - state: cx.entity(), - origin: Default::default(), - } - } -} - -struct ScrollbarElement { - origin: Point, - state: Entity>, -} - -#[derive(Default, Debug, PartialEq, Eq)] -enum ThumbState { - #[default] - Inactive, - Hover(ScrollbarAxis), - Dragging(ScrollbarAxis, Pixels), -} - -impl ThumbState { - fn is_dragging(&self) -> bool { - matches!(*self, ThumbState::Dragging(..)) - } -} - -impl ScrollableHandle for UniformListScrollHandle { - fn max_offset(&self) -> Size { - self.0.borrow().base_handle.max_offset() - } - - fn set_offset(&self, point: Point) { - self.0.borrow().base_handle.set_offset(point); - } - - fn offset(&self) -> Point { - self.0.borrow().base_handle.offset() - } - - fn viewport(&self) -> Bounds { - self.0.borrow().base_handle.bounds() - } -} - -impl ScrollableHandle for ListState { - fn max_offset(&self) -> Size { - self.max_offset_for_scrollbar() - } - - fn set_offset(&self, point: Point) { - self.set_offset_from_scrollbar(point); - } - - fn offset(&self) -> Point { - self.scroll_px_offset_for_scrollbar() - } - - fn drag_started(&self) { - self.scrollbar_drag_started(); - } - - fn drag_ended(&self) { - self.scrollbar_drag_ended(); - } - - fn viewport(&self) -> Bounds { - self.viewport_bounds() - } -} - -impl ScrollableHandle for ScrollHandle { - fn max_offset(&self) -> Size { - self.max_offset() - } - - fn set_offset(&self, point: Point) { - self.set_offset(point); - } - - fn offset(&self) -> Point { - self.offset() - } - - fn viewport(&self) -> Bounds { - self.bounds() - } -} - -pub trait ScrollableHandle: 'static + Any + Sized + Clone { - fn max_offset(&self) -> Size; - fn set_offset(&self, point: Point); - fn offset(&self) -> Point; - fn viewport(&self) -> Bounds; - fn drag_started(&self) {} - fn drag_ended(&self) {} - - fn scrollable_along(&self, axis: ScrollbarAxis) -> bool { - self.max_offset().along(axis) > Pixels::ZERO - } - fn content_size(&self) -> Size { - self.viewport().size + self.max_offset() - } -} - -enum ScrollbarMouseEvent { - TrackClick, - ThumbDrag(Pixels), -} - -struct ScrollbarLayout { - thumb_bounds: Bounds, - track_bounds: Bounds, - cursor_hitbox: Hitbox, - reserved_space: ReservedSpace, - track_background: Option<(Bounds, Hsla)>, - axis: ScrollbarAxis, -} - -impl ScrollbarLayout { - fn compute_click_offset( - &self, - event_position: Point, - max_offset: Size, - event_type: ScrollbarMouseEvent, - ) -> Pixels { - let Self { - track_bounds, - thumb_bounds, - axis, - .. - } = self; - let axis = *axis; - - let viewport_size = track_bounds.size.along(axis); - let thumb_size = thumb_bounds.size.along(axis); - let thumb_offset = match event_type { - ScrollbarMouseEvent::TrackClick => thumb_size / 2., - ScrollbarMouseEvent::ThumbDrag(thumb_offset) => thumb_offset, - }; - - let thumb_start = - (event_position.along(axis) - track_bounds.origin.along(axis) - thumb_offset) - .clamp(px(0.), viewport_size - thumb_size); - - let max_offset = max_offset.along(axis); - let percentage = if viewport_size > thumb_size { - thumb_start / (viewport_size - thumb_size) - } else { - 0. - }; - - -max_offset * percentage - } -} - -impl PartialEq for ScrollbarLayout { - fn eq(&self, other: &Self) -> bool { - self.axis == other.axis && self.thumb_bounds == other.thumb_bounds - } -} - -pub struct ScrollbarPrepaintState { - parent_bounds_hitbox: Hitbox, - thumbs: SmallVec<[ScrollbarLayout; 2]>, -} - -impl ScrollbarPrepaintState { - fn thumb_for_position(&self, position: &Point) -> Option<&ScrollbarLayout> { - self.thumbs - .iter() - .find(|info| info.thumb_bounds.contains(position)) - } - - fn hit_for_position(&self, position: &Point) -> Option<&ScrollbarLayout> { - self.thumbs.iter().find(|info| { - if info.reserved_space.needs_scroll_track() { - info.track_bounds.contains(position) - } else { - info.thumb_bounds.contains(position) - } - }) - } -} - -impl PartialEq for ScrollbarPrepaintState { - fn eq(&self, other: &Self) -> bool { - self.thumbs == other.thumbs - } -} - -impl Element for ScrollbarElement { - type RequestLayoutState = (); - type PrepaintState = Option<(ScrollbarPrepaintState, Option)>; - - fn id(&self) -> Option { - Some(("scrollbar_animation", self.state.entity_id()).into()) - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState) { - let scrollbar_style = Style { - position: Position::Absolute, - inset: Edges::default(), - size: size(relative(1.), relative(1.)).map(Into::into), - ..Default::default() - }; - - (window.request_layout(scrollbar_style, None, cx), ()) - } - - fn prepaint( - &mut self, - id: Option<&GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - bounds: Bounds, - _request_layout: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Self::PrepaintState { - let prepaint_state = self - .state - .read(cx) - .disabled() - .not() - .then(|| ScrollbarPrepaintState { - thumbs: { - let state = self.state.read(cx); - let thumb_ranges = state.thumb_ranges().collect::>(); - let width = state.width.to_pixels(); - let track_color = state.track_color; - - let additional_padding = if thumb_ranges.len() == 2 { - width - } else { - Pixels::ZERO - }; - - thumb_ranges - .into_iter() - .map(|(axis, thumb_range, reserved_space)| { - let track_anchor = match axis { - ScrollbarAxis::Horizontal => Corner::BottomLeft, - ScrollbarAxis::Vertical => Corner::TopRight, - }; - let Bounds { origin, size } = Bounds::from_corner_and_size( - track_anchor, - bounds - .corner(track_anchor) - .apply_along(axis.invert(), |corner| { - corner - SCROLLBAR_PADDING - }), - bounds.size.apply_along(axis.invert(), |_| width), - ); - let scroll_track_bounds = Bounds::new(self.origin + origin, size); - - let padded_bounds = scroll_track_bounds.extend(match axis { - ScrollbarAxis::Horizontal => Edges { - right: -SCROLLBAR_PADDING, - left: -SCROLLBAR_PADDING, - ..Default::default() - }, - ScrollbarAxis::Vertical => Edges { - top: -SCROLLBAR_PADDING, - bottom: -SCROLLBAR_PADDING, - ..Default::default() - }, - }); - - let available_space = - padded_bounds.size.along(axis) - additional_padding; - - let thumb_offset = thumb_range.start * available_space; - let thumb_end = thumb_range.end * available_space; - let thumb_bounds = Bounds::new( - padded_bounds - .origin - .apply_along(axis, |origin| origin + thumb_offset), - padded_bounds - .size - .apply_along(axis, |_| thumb_end - thumb_offset), - ); - - let needs_scroll_track = reserved_space.needs_scroll_track(); - - ScrollbarLayout { - thumb_bounds, - track_bounds: padded_bounds, - axis, - cursor_hitbox: window.insert_hitbox( - if needs_scroll_track { - padded_bounds - } else { - thumb_bounds - }, - HitboxBehavior::BlockMouseExceptScroll, - ), - track_background: track_color - .filter(|_| needs_scroll_track) - .map(|color| (padded_bounds.dilate(SCROLLBAR_PADDING), color)), - reserved_space, - } - }) - .collect() - }, - parent_bounds_hitbox: window.insert_hitbox(bounds, HitboxBehavior::Normal), - }); - if prepaint_state - .as_ref() - .is_some_and(|state| Some(state) != self.state.read(cx).last_prepaint_state.as_ref()) - { - self.state - .update(cx, |state, cx| state.show_scrollbars(window, cx)); - } - - prepaint_state.map(|state| { - let autohide_delta = self.state.read(cx).show_state.animation_progress().map( - |(delta, delta_duration, should_invert)| { - window.with_element_state(id.unwrap(), |state, window| { - let state = state.unwrap_or_else(|| Instant::now()); - let current = Instant::now(); - - let new_delta = DELTA_MAX - .min(delta + (current - state).div_duration_f32(delta_duration)); - self.state - .update(cx, |state, _| state.show_state.set_delta(new_delta)); - - window.request_animation_frame(); - let delta = if should_invert { - DELTA_MAX - delta - } else { - delta - }; - (ease_in_out(delta), current) - }) - }, - ); - - (state, autohide_delta) - }) - } - - fn paint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - Bounds { origin, size }: Bounds, - _request_layout: &mut Self::RequestLayoutState, - prepaint_state: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - let Some((prepaint_state, autohide_fade)) = prepaint_state.take() else { - return; - }; - - let bounds = Bounds::new(self.origin + origin, size); - window.with_content_mask(Some(ContentMask { bounds }), |window| { - let colors = cx.theme().colors(); - - let capture_phase; - - if self.state.read(cx).visible() { - let thumb_state = &self.state.read(cx).thumb_state; - - if thumb_state.is_dragging() { - capture_phase = DispatchPhase::Capture; - } else { - capture_phase = DispatchPhase::Bubble; - } - - for ScrollbarLayout { - thumb_bounds, - cursor_hitbox, - axis, - reserved_space, - track_background, - .. - } in &prepaint_state.thumbs - { - const MAXIMUM_OPACITY: f32 = 0.7; - let (thumb_base_color, hovered) = match thumb_state { - ThumbState::Dragging(dragged_axis, _) if dragged_axis == axis => { - (colors.scrollbar_thumb_active_background, false) - } - ThumbState::Hover(hovered_axis) if hovered_axis == axis => { - (colors.scrollbar_thumb_hover_background, true) - } - _ => (colors.scrollbar_thumb_background, false), - }; - - let blending_color = if hovered || reserved_space.needs_scroll_track() { - track_background - .map(|(_, background)| background) - .unwrap_or(colors.surface_background) - } else { - let blend_color = colors.surface_background; - blend_color.min(blend_color.alpha(MAXIMUM_OPACITY)) - }; - - let mut thumb_color = blending_color.blend(thumb_base_color); - - if !hovered && let Some(fade) = autohide_fade { - thumb_color.fade_out(fade); - } - - if let Some((track_bounds, color)) = track_background { - let mut color = *color; - if let Some(fade) = autohide_fade { - color.fade_out(fade); - } - - window.paint_quad(quad( - *track_bounds, - Corners::default(), - color, - Edges::default(), - Hsla::transparent_black(), - BorderStyle::default(), - )); - } - - window.paint_quad(quad( - *thumb_bounds, - Corners::all(Pixels::MAX).clamp_radii_for_quad_size(thumb_bounds.size), - thumb_color, - Edges::default(), - Hsla::transparent_black(), - BorderStyle::default(), - )); - - if thumb_state.is_dragging() { - window.set_window_cursor_style(CursorStyle::Arrow); - } else { - window.set_cursor_style(CursorStyle::Arrow, cursor_hitbox); - } - } - } else { - capture_phase = DispatchPhase::Bubble; - } - - self.state.update(cx, |state, _| { - state.last_prepaint_state = Some(prepaint_state) - }); - - window.on_mouse_event({ - let state = self.state.clone(); - - move |event: &MouseDownEvent, phase, window, cx| { - state.update(cx, |state, cx| { - let Some(scrollbar_layout) = (phase == capture_phase - && event.button == MouseButton::Left) - .then(|| state.hit_for_position(&event.position)) - .flatten() - else { - return; - }; - - let ScrollbarLayout { - thumb_bounds, axis, .. - } = scrollbar_layout; - - if thumb_bounds.contains(&event.position) { - let offset = - event.position.along(*axis) - thumb_bounds.origin.along(*axis); - state.set_dragging(*axis, offset, window, cx); - } else { - let scroll_handle = state.scroll_handle(); - let click_offset = scrollbar_layout.compute_click_offset( - event.position, - scroll_handle.max_offset(), - ScrollbarMouseEvent::TrackClick, - ); - state.set_offset( - scroll_handle.offset().apply_along(*axis, |_| click_offset), - cx, - ); - }; - - cx.stop_propagation(); - }); - } - }); - - window.on_mouse_event({ - let state = self.state.clone(); - - move |event: &ScrollWheelEvent, phase, window, cx| { - state.update(cx, |state, cx| { - if phase.capture() && state.parent_hovered(window) { - state.update_hovered_thumb(&event.position, window, cx) - } - }); - } - }); - - window.on_mouse_event({ - let state = self.state.clone(); - - move |event: &MouseMoveEvent, phase, window, cx| { - if phase != capture_phase { - return; - } - - match state.read(cx).thumb_state { - ThumbState::Dragging(axis, drag_state) if event.dragging() => { - if let Some(scrollbar_layout) = state.read(cx).thumb_for_axis(axis) { - let scroll_handle = state.read(cx).scroll_handle(); - let drag_offset = scrollbar_layout.compute_click_offset( - event.position, - scroll_handle.max_offset(), - ScrollbarMouseEvent::ThumbDrag(drag_state), - ); - let new_offset = - scroll_handle.offset().apply_along(axis, |_| drag_offset); - - state.update(cx, |state, cx| state.set_offset(new_offset, cx)); - cx.stop_propagation(); - } - } - _ => state.update(cx, |state, cx| { - match state.update_parent_hovered(window) { - hover @ ParentHoverEvent::Entered - | hover @ ParentHoverEvent::Within - if event.pressed_button.is_none() => - { - if matches!(hover, ParentHoverEvent::Entered) { - state.show_scrollbars(window, cx); - } - state.update_hovered_thumb(&event.position, window, cx); - if state.thumb_state != ThumbState::Inactive { - cx.stop_propagation(); - } - } - ParentHoverEvent::Exited => { - state.set_thumb_state(ThumbState::Inactive, window, cx); - } - _ => {} - } - }), - } - } - }); - - window.on_mouse_event({ - let state = self.state.clone(); - move |event: &MouseUpEvent, phase, window, cx| { - if phase != capture_phase { - return; - } - - state.update(cx, |state, cx| { - if state.is_dragging() { - state.scroll_handle().drag_ended(); - } - - if !state.parent_hovered(window) { - state.schedule_auto_hide(window, cx); - return; - } - - state.update_hovered_thumb(&event.position, window, cx); - }); - } - }); - }) - } -} - -impl IntoElement for ScrollbarElement { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} diff --git a/crates/ui/src/components/settings_container.rs b/crates/ui/src/components/settings_container.rs deleted file mode 100644 index 31cb1b32f8..0000000000 --- a/crates/ui/src/components/settings_container.rs +++ /dev/null @@ -1,89 +0,0 @@ -use gpui::AnyElement; -use smallvec::SmallVec; - -use crate::prelude::*; - -use super::Checkbox; - -#[derive(IntoElement, RegisterComponent)] -pub struct SettingsContainer { - children: SmallVec<[AnyElement; 2]>, -} - -impl Default for SettingsContainer { - fn default() -> Self { - Self::new() - } -} - -impl SettingsContainer { - pub fn new() -> Self { - Self { - children: SmallVec::new(), - } - } -} - -impl ParentElement for SettingsContainer { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl RenderOnce for SettingsContainer { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - v_flex().px_2().gap_1().children(self.children) - } -} - -impl Component for SettingsContainer { - fn scope() -> ComponentScope { - ComponentScope::Layout - } - - fn name() -> &'static str { - "SettingsContainer" - } - - fn description() -> Option<&'static str> { - Some("A container for organizing and displaying settings in a structured manner.") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Basic Usage", - vec![ - single_example( - "Empty Container", - SettingsContainer::new().into_any_element(), - ), - single_example( - "With Content", - SettingsContainer::new() - .child(Label::new("Setting 1")) - .child(Label::new("Setting 2")) - .child(Label::new("Setting 3")) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "With Different Elements", - vec![single_example( - "Mixed Content", - SettingsContainer::new() - .child(Label::new("Text Setting")) - .child(Checkbox::new("checkbox", ToggleState::Unselected)) - .child(Button::new("button", "Click me")) - .into_any_element(), - )], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/settings_group.rs b/crates/ui/src/components/settings_group.rs deleted file mode 100644 index 1812a1bec4..0000000000 --- a/crates/ui/src/components/settings_group.rs +++ /dev/null @@ -1,110 +0,0 @@ -use gpui::AnyElement; -use smallvec::SmallVec; - -use crate::{ListHeader, prelude::*}; - -use super::Checkbox; - -/// A group of settings. -#[derive(IntoElement, RegisterComponent)] -pub struct SettingsGroup { - header: SharedString, - children: SmallVec<[AnyElement; 2]>, -} - -impl SettingsGroup { - pub fn new(header: impl Into) -> Self { - Self { - header: header.into(), - children: SmallVec::new(), - } - } -} - -impl ParentElement for SettingsGroup { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl RenderOnce for SettingsGroup { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - v_flex() - .p_1() - .gap_2() - .child(ListHeader::new(self.header)) - .children(self.children) - } -} - -impl Component for SettingsGroup { - fn scope() -> ComponentScope { - ComponentScope::Layout - } - - fn name() -> &'static str { - "SettingsGroup" - } - - fn description() -> Option<&'static str> { - Some("A group of settings with a header, used to organize related settings.") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Basic Usage", - vec![ - single_example( - "Empty Group", - SettingsGroup::new("General Settings").into_any_element(), - ), - single_example( - "With Children", - SettingsGroup::new("Appearance") - .child( - Checkbox::new("dark_mode", ToggleState::Unselected) - .label("Dark Mode"), - ) - .child( - Checkbox::new("high_contrast", ToggleState::Unselected) - .label("High Contrast"), - ) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Multiple Groups", - vec![single_example( - "Two Groups", - v_flex() - .gap_4() - .child( - SettingsGroup::new("General").child( - Checkbox::new("auto_update", ToggleState::Selected) - .label("Auto Update"), - ), - ) - .child( - SettingsGroup::new("Editor") - .child( - Checkbox::new("line_numbers", ToggleState::Selected) - .label("Show Line Numbers"), - ) - .child( - Checkbox::new("word_wrap", ToggleState::Unselected) - .label("Word Wrap"), - ), - ) - .into_any_element(), - )], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/stack.rs b/crates/ui/src/components/stack.rs deleted file mode 100644 index 2118757548..0000000000 --- a/crates/ui/src/components/stack.rs +++ /dev/null @@ -1,15 +0,0 @@ -use gpui::{Div, div}; - -use crate::StyledExt; - -/// Horizontally stacks elements. Sets `flex()`, `flex_row()`, `items_center()` -#[track_caller] -pub fn h_flex() -> Div { - div().h_flex() -} - -/// Vertically stacks elements. Sets `flex()`, `flex_col()` -#[track_caller] -pub fn v_flex() -> Div { - div().v_flex() -} diff --git a/crates/ui/src/components/sticky_items.rs b/crates/ui/src/components/sticky_items.rs deleted file mode 100644 index bf64622b29..0000000000 --- a/crates/ui/src/components/sticky_items.rs +++ /dev/null @@ -1,335 +0,0 @@ -use std::{ops::Range, rc::Rc}; - -use gpui::{ - AnyElement, App, AvailableSpace, Bounds, Context, Element, ElementId, Entity, GlobalElementId, - InspectorElementId, IntoElement, LayoutId, Pixels, Point, Render, Style, UniformListDecoration, - Window, point, px, size, -}; -use smallvec::SmallVec; - -pub trait StickyCandidate { - fn depth(&self) -> usize; -} - -pub struct StickyItems { - compute_fn: Rc, &mut Window, &mut App) -> SmallVec<[T; 8]>>, - render_fn: Rc SmallVec<[AnyElement; 8]>>, - decorations: Vec>, -} - -pub fn sticky_items( - entity: Entity, - compute_fn: impl Fn(&mut V, Range, &mut Window, &mut Context) -> SmallVec<[T; 8]> - + 'static, - render_fn: impl Fn(&mut V, T, &mut Window, &mut Context) -> SmallVec<[AnyElement; 8]> + 'static, -) -> StickyItems -where - V: Render, - T: StickyCandidate + Clone + 'static, -{ - let entity_compute = entity.clone(); - let entity_render = entity; - - let compute_fn = Rc::new( - move |range: Range, window: &mut Window, cx: &mut App| -> SmallVec<[T; 8]> { - entity_compute.update(cx, |view, cx| compute_fn(view, range, window, cx)) - }, - ); - let render_fn = Rc::new( - move |entry: T, window: &mut Window, cx: &mut App| -> SmallVec<[AnyElement; 8]> { - entity_render.update(cx, |view, cx| render_fn(view, entry, window, cx)) - }, - ); - - StickyItems { - compute_fn, - render_fn, - decorations: Vec::new(), - } -} - -impl StickyItems -where - T: StickyCandidate + Clone + 'static, -{ - /// Adds a decoration element to the sticky items. - pub fn with_decoration(mut self, decoration: impl StickyItemsDecoration + 'static) -> Self { - self.decorations.push(Box::new(decoration)); - self - } -} - -struct StickyItemsElement { - drifting_element: Option, - drifting_decoration: Option, - rest_elements: SmallVec<[AnyElement; 8]>, - rest_decorations: SmallVec<[AnyElement; 1]>, -} - -impl IntoElement for StickyItemsElement { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} - -impl Element for StickyItemsElement { - type RequestLayoutState = (); - type PrepaintState = (); - - fn id(&self) -> Option { - None - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState) { - (window.request_layout(Style::default(), [], cx), ()) - } - - fn prepaint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - _bounds: Bounds, - _request_layout: &mut Self::RequestLayoutState, - _window: &mut Window, - _cx: &mut App, - ) -> Self::PrepaintState { - } - - fn paint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&InspectorElementId>, - _bounds: Bounds, - _request_layout: &mut Self::RequestLayoutState, - _prepaint: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - if let Some(ref mut drifting_element) = self.drifting_element { - drifting_element.paint(window, cx); - } - if let Some(ref mut drifting_decoration) = self.drifting_decoration { - drifting_decoration.paint(window, cx); - } - for item in self.rest_elements.iter_mut().rev() { - item.paint(window, cx); - } - for item in self.rest_decorations.iter_mut() { - item.paint(window, cx); - } - } -} - -impl UniformListDecoration for StickyItems -where - T: StickyCandidate + Clone + 'static, -{ - fn compute( - &self, - visible_range: Range, - bounds: Bounds, - scroll_offset: Point, - item_height: Pixels, - _item_count: usize, - window: &mut Window, - cx: &mut App, - ) -> AnyElement { - let entries = (self.compute_fn)(visible_range.clone(), window, cx); - - let Some(sticky_anchor) = find_sticky_anchor(&entries, visible_range.start) else { - return StickyItemsElement { - drifting_element: None, - drifting_decoration: None, - rest_elements: SmallVec::new(), - rest_decorations: SmallVec::new(), - } - .into_any_element(); - }; - - let anchor_depth = sticky_anchor.entry.depth(); - let mut elements = (self.render_fn)(sticky_anchor.entry, window, cx); - let items_count = elements.len(); - - let indents: SmallVec<[usize; 8]> = (0..items_count) - .map(|ix| anchor_depth.saturating_sub(items_count.saturating_sub(ix))) - .collect(); - - let mut last_decoration_element = None; - let mut rest_decoration_elements = SmallVec::new(); - - let expanded_width = bounds.size.width + scroll_offset.x.abs(); - - let decor_available_space = size( - AvailableSpace::Definite(expanded_width), - AvailableSpace::Definite(bounds.size.height), - ); - - let drifting_y_offset = if sticky_anchor.drifting { - let scroll_top = -scroll_offset.y; - let anchor_top = item_height * (sticky_anchor.index + 1); - let sticky_area_height = item_height * items_count; - (anchor_top - scroll_top - sticky_area_height).min(Pixels::ZERO) - } else { - Pixels::ZERO - }; - - let (drifting_indent, rest_indents) = if sticky_anchor.drifting && !indents.is_empty() { - let last = indents[indents.len() - 1]; - let rest: SmallVec<[usize; 8]> = indents[..indents.len() - 1].iter().copied().collect(); - (Some(last), rest) - } else { - (None, indents) - }; - - let base_origin = bounds.origin - point(px(0.), scroll_offset.y); - - for decoration in &self.decorations { - if let Some(drifting_indent) = drifting_indent { - let drifting_indent_vec: SmallVec<[usize; 8]> = - [drifting_indent].into_iter().collect(); - - let sticky_origin = base_origin - + point(px(0.), item_height * rest_indents.len() + drifting_y_offset); - let decoration_bounds = Bounds::new(sticky_origin, bounds.size); - - let mut drifting_dec = decoration.as_ref().compute( - &drifting_indent_vec, - decoration_bounds, - scroll_offset, - item_height, - window, - cx, - ); - drifting_dec.layout_as_root(decor_available_space, window, cx); - drifting_dec.prepaint_at(sticky_origin, window, cx); - last_decoration_element = Some(drifting_dec); - } - - if !rest_indents.is_empty() { - let decoration_bounds = Bounds::new(base_origin, bounds.size); - let mut rest_dec = decoration.as_ref().compute( - &rest_indents, - decoration_bounds, - scroll_offset, - item_height, - window, - cx, - ); - rest_dec.layout_as_root(decor_available_space, window, cx); - rest_dec.prepaint_at(bounds.origin, window, cx); - rest_decoration_elements.push(rest_dec); - } - } - - let (mut drifting_element, mut rest_elements) = - if sticky_anchor.drifting && !elements.is_empty() { - let last = elements.pop().unwrap(); - (Some(last), elements) - } else { - (None, elements) - }; - - let element_available_space = size( - AvailableSpace::Definite(expanded_width), - AvailableSpace::Definite(item_height), - ); - - // order of prepaint is important here - // mouse events checks hitboxes in reverse insertion order - if let Some(ref mut drifting_element) = drifting_element { - let sticky_origin = base_origin - + point( - px(0.), - item_height * rest_elements.len() + drifting_y_offset, - ); - - drifting_element.layout_as_root(element_available_space, window, cx); - drifting_element.prepaint_at(sticky_origin, window, cx); - } - - for (ix, element) in rest_elements.iter_mut().enumerate() { - let sticky_origin = base_origin + point(px(0.), item_height * ix); - - element.layout_as_root(element_available_space, window, cx); - element.prepaint_at(sticky_origin, window, cx); - } - - StickyItemsElement { - drifting_element, - drifting_decoration: last_decoration_element, - rest_elements, - rest_decorations: rest_decoration_elements, - } - .into_any_element() - } -} - -struct StickyAnchor { - entry: T, - index: usize, - drifting: bool, -} - -fn find_sticky_anchor( - entries: &SmallVec<[T; 8]>, - visible_range_start: usize, -) -> Option> { - let mut iter = entries.iter().enumerate().peekable(); - while let Some((ix, current_entry)) = iter.next() { - let depth = current_entry.depth(); - - if depth < ix { - return Some(StickyAnchor { - entry: current_entry.clone(), - index: visible_range_start + ix, - drifting: false, - }); - } - - if let Some(&(_next_ix, next_entry)) = iter.peek() { - let next_depth = next_entry.depth(); - let next_item_outdented = next_depth + 1 == depth; - - let depth_same_as_index = depth == ix; - let depth_greater_than_index = depth == ix + 1; - - if next_item_outdented && (depth_same_as_index || depth_greater_than_index) { - return Some(StickyAnchor { - entry: current_entry.clone(), - index: visible_range_start + ix, - drifting: depth_greater_than_index, - }); - } - } - } - - None -} - -/// A decoration for a [`StickyItems`]. This can be used for various things, -/// such as rendering indent guides, or other visual effects. -pub trait StickyItemsDecoration { - /// Compute the decoration element, given the visible range of list items, - /// the bounds of the list, and the height of each item. - fn compute( - &self, - indents: &SmallVec<[usize; 8]>, - bounds: Bounds, - scroll_offset: Point, - item_height: Pixels, - window: &mut Window, - cx: &mut App, - ) -> AnyElement; -} diff --git a/crates/ui/src/components/stories.rs b/crates/ui/src/components/stories.rs deleted file mode 100644 index bcfcfd04c3..0000000000 --- a/crates/ui/src/components/stories.rs +++ /dev/null @@ -1,3 +0,0 @@ -mod context_menu; - -pub use context_menu::*; diff --git a/crates/ui/src/components/stories/context_menu.rs b/crates/ui/src/components/stories/context_menu.rs deleted file mode 100644 index 197964adc8..0000000000 --- a/crates/ui/src/components/stories/context_menu.rs +++ /dev/null @@ -1,81 +0,0 @@ -use gpui::{Corner, Entity, Render, actions}; -use story::Story; - -use crate::prelude::*; -use crate::{ContextMenu, Label, right_click_menu}; - -actions!(stories, [PrintCurrentDate, PrintBestFood]); - -fn build_menu( - window: &mut Window, - cx: &mut App, - header: impl Into, -) -> Entity { - ContextMenu::build(window, cx, |menu, _, _| { - menu.header(header) - .separator() - .action("Print current time", Box::new(PrintCurrentDate)) - .entry( - "Print best food", - Some(Box::new(PrintBestFood)), - |window, cx| window.dispatch_action(Box::new(PrintBestFood), cx), - ) - }) -} - -pub struct ContextMenuStory; - -impl Render for ContextMenuStory { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - Story::container(cx) - .on_action(|_: &PrintCurrentDate, _, _| { - println!("printing unix time!"); - if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() { - println!("Current Unix time is {:?}", unix_time.as_secs()); - } - }) - .on_action(|_: &PrintBestFood, _, _| { - println!("burrito"); - }) - .flex() - .flex_row() - .justify_between() - .child( - div() - .flex() - .flex_col() - .justify_between() - .child( - right_click_menu("test2") - .trigger(|_, _, _| Label::new("TOP LEFT")) - .menu(move |window, cx| build_menu(window, cx, "top left")), - ) - .child( - right_click_menu("test1") - .trigger(|_, _, _| Label::new("BOTTOM LEFT")) - .anchor(Corner::BottomLeft) - .attach(Corner::TopLeft) - .menu(move |window, cx| build_menu(window, cx, "bottom left")), - ), - ) - .child( - div() - .flex() - .flex_col() - .justify_between() - .child( - right_click_menu("test3") - .trigger(|_, _, _| Label::new("TOP RIGHT")) - .anchor(Corner::TopRight) - .menu(move |window, cx| build_menu(window, cx, "top right")), - ) - .child( - right_click_menu("test4") - .trigger(|_, _, _| Label::new("BOTTOM RIGHT")) - .anchor(Corner::BottomRight) - .attach(Corner::TopRight) - .menu(move |window, cx| build_menu(window, cx, "bottom right")), - ), - ) - } -} diff --git a/crates/ui/src/components/tab.rs b/crates/ui/src/components/tab.rs deleted file mode 100644 index e6823f46b7..0000000000 --- a/crates/ui/src/components/tab.rs +++ /dev/null @@ -1,238 +0,0 @@ -use std::cmp::Ordering; - -use gpui::{AnyElement, IntoElement, Stateful}; -use smallvec::SmallVec; - -use crate::prelude::*; - -const START_TAB_SLOT_SIZE: Pixels = px(12.); -const END_TAB_SLOT_SIZE: Pixels = px(14.); - -/// The position of a [`Tab`] within a list of tabs. -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum TabPosition { - /// The tab is first in the list. - First, - - /// The tab is in the middle of the list (i.e., it is not the first or last tab). - /// - /// The [`Ordering`] is where this tab is positioned with respect to the selected tab. - Middle(Ordering), - - /// The tab is last in the list. - Last, -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum TabCloseSide { - Start, - End, -} - -#[derive(IntoElement, RegisterComponent)] -pub struct Tab { - div: Stateful
, - selected: bool, - position: TabPosition, - close_side: TabCloseSide, - start_slot: Option, - end_slot: Option, - children: SmallVec<[AnyElement; 2]>, -} - -impl Tab { - pub fn new(id: impl Into) -> Self { - let id = id.into(); - Self { - div: div() - .id(id.clone()) - .debug_selector(|| format!("TAB-{}", id)), - selected: false, - position: TabPosition::First, - close_side: TabCloseSide::End, - start_slot: None, - end_slot: None, - children: SmallVec::new(), - } - } - - pub fn position(mut self, position: TabPosition) -> Self { - self.position = position; - self - } - - pub fn close_side(mut self, close_side: TabCloseSide) -> Self { - self.close_side = close_side; - self - } - - pub fn start_slot(mut self, element: impl Into>) -> Self { - self.start_slot = element.into().map(IntoElement::into_any_element); - self - } - - pub fn end_slot(mut self, element: impl Into>) -> Self { - self.end_slot = element.into().map(IntoElement::into_any_element); - self - } - - pub fn content_height(cx: &App) -> Pixels { - DynamicSpacing::Base32.px(cx) - px(1.) - } - - pub fn container_height(cx: &App) -> Pixels { - DynamicSpacing::Base32.px(cx) - } -} - -impl InteractiveElement for Tab { - fn interactivity(&mut self) -> &mut gpui::Interactivity { - self.div.interactivity() - } -} - -impl StatefulInteractiveElement for Tab {} - -impl Toggleable for Tab { - fn toggle_state(mut self, selected: bool) -> Self { - self.selected = selected; - self - } -} - -impl ParentElement for Tab { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl RenderOnce for Tab { - #[allow(refining_impl_trait)] - fn render(self, _: &mut Window, cx: &mut App) -> Stateful
{ - let (text_color, tab_bg, _tab_hover_bg, _tab_active_bg) = match self.selected { - false => ( - cx.theme().colors().text_muted, - cx.theme().colors().tab_inactive_background, - cx.theme().colors().ghost_element_hover, - cx.theme().colors().ghost_element_active, - ), - true => ( - cx.theme().colors().text, - cx.theme().colors().tab_active_background, - cx.theme().colors().element_hover, - cx.theme().colors().element_active, - ), - }; - - let (start_slot, end_slot) = { - let start_slot = h_flex() - .size(START_TAB_SLOT_SIZE) - .justify_center() - .children(self.start_slot); - - let end_slot = h_flex() - .size(END_TAB_SLOT_SIZE) - .justify_center() - .children(self.end_slot); - - match self.close_side { - TabCloseSide::End => (start_slot, end_slot), - TabCloseSide::Start => (end_slot, start_slot), - } - }; - - self.div - .h(Tab::container_height(cx)) - .bg(tab_bg) - .border_color(cx.theme().colors().border) - .map(|this| match self.position { - TabPosition::First => { - if self.selected { - this.pl_px().border_r_1().pb_px() - } else { - this.pl_px().pr_px().border_b_1() - } - } - TabPosition::Last => { - if self.selected { - this.border_l_1().border_r_1().pb_px() - } else { - this.pl_px().border_b_1().border_r_1() - } - } - TabPosition::Middle(Ordering::Equal) => this.border_l_1().border_r_1().pb_px(), - TabPosition::Middle(Ordering::Less) => this.border_l_1().pr_px().border_b_1(), - TabPosition::Middle(Ordering::Greater) => this.border_r_1().pl_px().border_b_1(), - }) - .cursor_pointer() - .child( - h_flex() - .group("") - .relative() - .h(Tab::content_height(cx)) - .px(DynamicSpacing::Base04.px(cx)) - .gap(DynamicSpacing::Base04.rems(cx)) - .text_color(text_color) - .child(start_slot) - .children(self.children) - .child(end_slot), - ) - } -} - -impl Component for Tab { - fn scope() -> ComponentScope { - ComponentScope::Navigation - } - - fn description() -> Option<&'static str> { - Some( - "A tab component that can be used in a tabbed interface, supporting different positions and states.", - ) - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![example_group_with_title( - "Variations", - vec![ - single_example( - "Default", - Tab::new("default").child("Default Tab").into_any_element(), - ), - single_example( - "Selected", - Tab::new("selected") - .toggle_state(true) - .child("Selected Tab") - .into_any_element(), - ), - single_example( - "First", - Tab::new("first") - .position(TabPosition::First) - .child("First Tab") - .into_any_element(), - ), - single_example( - "Middle", - Tab::new("middle") - .position(TabPosition::Middle(Ordering::Equal)) - .child("Middle Tab") - .into_any_element(), - ), - single_example( - "Last", - Tab::new("last") - .position(TabPosition::Last) - .child("Last Tab") - .into_any_element(), - ), - ], - )]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/tab_bar.rs b/crates/ui/src/components/tab_bar.rs deleted file mode 100644 index 5d41466e3c..0000000000 --- a/crates/ui/src/components/tab_bar.rs +++ /dev/null @@ -1,207 +0,0 @@ -use gpui::{AnyElement, ScrollHandle}; -use smallvec::SmallVec; - -use crate::Tab; -use crate::prelude::*; - -#[derive(IntoElement, RegisterComponent)] -pub struct TabBar { - id: ElementId, - start_children: SmallVec<[AnyElement; 2]>, - children: SmallVec<[AnyElement; 2]>, - end_children: SmallVec<[AnyElement; 2]>, - scroll_handle: Option, -} - -impl TabBar { - pub fn new(id: impl Into) -> Self { - Self { - id: id.into(), - start_children: SmallVec::new(), - children: SmallVec::new(), - end_children: SmallVec::new(), - scroll_handle: None, - } - } - - pub fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self { - self.scroll_handle = Some(scroll_handle.clone()); - self - } - - pub fn start_children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { - &mut self.start_children - } - - pub fn start_child(mut self, start_child: impl IntoElement) -> Self - where - Self: Sized, - { - self.start_children_mut() - .push(start_child.into_element().into_any()); - self - } - - pub fn start_children( - mut self, - start_children: impl IntoIterator, - ) -> Self - where - Self: Sized, - { - self.start_children_mut().extend( - start_children - .into_iter() - .map(|child| child.into_any_element()), - ); - self - } - - pub fn end_children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { - &mut self.end_children - } - - pub fn end_child(mut self, end_child: impl IntoElement) -> Self - where - Self: Sized, - { - self.end_children_mut() - .push(end_child.into_element().into_any()); - self - } - - pub fn end_children(mut self, end_children: impl IntoIterator) -> Self - where - Self: Sized, - { - self.end_children_mut().extend( - end_children - .into_iter() - .map(|child| child.into_any_element()), - ); - self - } -} - -impl ParentElement for TabBar { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } -} - -impl RenderOnce for TabBar { - fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - div() - .id(self.id) - .group("tab_bar") - .flex() - .flex_none() - .w_full() - .h(Tab::container_height(cx)) - .bg(cx.theme().colors().tab_bar_background) - .when(!self.start_children.is_empty(), |this| { - this.child( - h_flex() - .flex_none() - .gap(DynamicSpacing::Base04.rems(cx)) - .px(DynamicSpacing::Base06.rems(cx)) - .border_b_1() - .border_r_1() - .border_color(cx.theme().colors().border) - .children(self.start_children), - ) - }) - .child( - div() - .relative() - .flex_1() - .h_full() - .overflow_x_hidden() - .child( - div() - .absolute() - .top_0() - .left_0() - .size_full() - .border_b_1() - .border_color(cx.theme().colors().border), - ) - .child( - h_flex() - .id("tabs") - .flex_grow() - .overflow_x_scroll() - .when_some(self.scroll_handle, |cx, scroll_handle| { - cx.track_scroll(&scroll_handle) - }) - .children(self.children), - ), - ) - .when(!self.end_children.is_empty(), |this| { - this.child( - h_flex() - .flex_none() - .gap(DynamicSpacing::Base04.rems(cx)) - .px(DynamicSpacing::Base06.rems(cx)) - .border_b_1() - .border_l_1() - .border_color(cx.theme().colors().border) - .children(self.end_children), - ) - }) - } -} - -impl Component for TabBar { - fn scope() -> ComponentScope { - ComponentScope::Navigation - } - - fn name() -> &'static str { - "TabBar" - } - - fn description() -> Option<&'static str> { - Some("A horizontal bar containing tabs for navigation between different views or sections.") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Basic Usage", - vec![ - single_example( - "Empty TabBar", - TabBar::new("empty_tab_bar").into_any_element(), - ), - single_example( - "With Tabs", - TabBar::new("tab_bar_with_tabs") - .child(Tab::new("tab1")) - .child(Tab::new("tab2")) - .child(Tab::new("tab3")) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "With Start and End Children", - vec![single_example( - "Full TabBar", - TabBar::new("full_tab_bar") - .start_child(Button::new("start_button", "Start")) - .child(Tab::new("tab1")) - .child(Tab::new("tab2")) - .child(Tab::new("tab3")) - .end_child(Button::new("end_button", "End")) - .into_any_element(), - )], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/thread_item.rs b/crates/ui/src/components/thread_item.rs deleted file mode 100644 index a4f6a8a533..0000000000 --- a/crates/ui/src/components/thread_item.rs +++ /dev/null @@ -1,260 +0,0 @@ -use crate::{ - Chip, DecoratedIcon, DiffStat, IconDecoration, IconDecorationKind, SpinnerLabel, prelude::*, -}; -use gpui::{ClickEvent, SharedString}; - -#[derive(IntoElement, RegisterComponent)] -pub struct ThreadItem { - id: ElementId, - icon: IconName, - title: SharedString, - timestamp: SharedString, - running: bool, - generation_done: bool, - selected: bool, - added: Option, - removed: Option, - worktree: Option, - on_click: Option>, -} - -impl ThreadItem { - pub fn new(id: impl Into, title: impl Into) -> Self { - Self { - id: id.into(), - icon: IconName::ZedAgent, - title: title.into(), - timestamp: "".into(), - running: false, - generation_done: false, - selected: false, - added: None, - removed: None, - worktree: None, - on_click: None, - } - } - - pub fn timestamp(mut self, timestamp: impl Into) -> Self { - self.timestamp = timestamp.into(); - self - } - - pub fn icon(mut self, icon: IconName) -> Self { - self.icon = icon; - self - } - - pub fn running(mut self, running: bool) -> Self { - self.running = running; - self - } - - pub fn generation_done(mut self, generation_done: bool) -> Self { - self.generation_done = generation_done; - self - } - - pub fn selected(mut self, selected: bool) -> Self { - self.selected = selected; - self - } - - pub fn added(mut self, added: usize) -> Self { - self.added = Some(added); - self - } - - pub fn removed(mut self, removed: usize) -> Self { - self.removed = Some(removed); - self - } - - pub fn worktree(mut self, worktree: impl Into) -> Self { - self.worktree = Some(worktree.into()); - self - } - - pub fn on_click( - mut self, - handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_click = Some(Box::new(handler)); - self - } -} - -impl RenderOnce for ThreadItem { - fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - let icon_container = || h_flex().size_4().justify_center(); - let agent_icon = Icon::new(self.icon) - .color(Color::Muted) - .size(IconSize::Small); - - let icon = if self.generation_done { - DecoratedIcon::new( - agent_icon, - Some( - IconDecoration::new( - IconDecorationKind::Dot, - cx.theme().colors().surface_background, - cx, - ) - .color(cx.theme().colors().text_accent) - .position(gpui::Point { - x: px(-2.), - y: px(-2.), - }), - ), - ) - .into_any_element() - } else { - agent_icon.into_any_element() - }; - - let has_no_changes = self.added.is_none() && self.removed.is_none(); - - v_flex() - .id(self.id.clone()) - .cursor_pointer() - .p_2() - .when(self.selected, |this| { - this.bg(cx.theme().colors().element_active) - }) - .hover(|s| s.bg(cx.theme().colors().element_hover)) - .child( - h_flex() - .w_full() - .gap_1p5() - .child(icon) - .child(Label::new(self.title).truncate()) - .when(self.running, |this| { - this.child(icon_container().child(SpinnerLabel::new().color(Color::Accent))) - }), - ) - .child( - h_flex() - .gap_1p5() - .child(icon_container()) // Icon Spacing - .when_some(self.worktree, |this, name| { - this.child(Chip::new(name).label_size(LabelSize::XSmall)) - }) - .child( - Label::new(self.timestamp) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child( - Label::new("•") - .size(LabelSize::Small) - .color(Color::Muted) - .alpha(0.5), - ) - .when(has_no_changes, |this| { - this.child( - Label::new("No Changes") - .size(LabelSize::Small) - .color(Color::Muted), - ) - }) - .when(self.added.is_some() || self.removed.is_some(), |this| { - this.child(DiffStat::new( - self.id, - self.added.unwrap_or(0), - self.removed.unwrap_or(0), - )) - }), - ) - .when_some(self.on_click, |this, on_click| this.on_click(on_click)) - } -} - -impl Component for ThreadItem { - fn scope() -> ComponentScope { - ComponentScope::Agent - } - - fn preview(_window: &mut Window, cx: &mut App) -> Option { - let container = || { - v_flex() - .w_72() - .border_1() - .border_color(cx.theme().colors().border_variant) - .bg(cx.theme().colors().panel_background) - }; - - let thread_item_examples = vec![ - single_example( - "Default", - container() - .child( - ThreadItem::new("ti-1", "Linking to the Agent Panel Depending on Settings") - .icon(IconName::AiOpenAi) - .timestamp("1:33 AM"), - ) - .into_any_element(), - ), - single_example( - "Generation Done", - container() - .child( - ThreadItem::new("ti-2", "Refine thread view scrolling behavior") - .timestamp("12:12 AM") - .generation_done(true), - ) - .into_any_element(), - ), - single_example( - "Running Agent", - container() - .child( - ThreadItem::new("ti-3", "Add line numbers option to FileEditBlock") - .icon(IconName::AiClaude) - .timestamp("7:30 PM") - .running(true), - ) - .into_any_element(), - ), - single_example( - "In Worktree", - container() - .child( - ThreadItem::new("ti-4", "Add line numbers option to FileEditBlock") - .icon(IconName::AiClaude) - .timestamp("7:37 PM") - .worktree("link-agent-panel"), - ) - .into_any_element(), - ), - single_example( - "With Changes", - container() - .child( - ThreadItem::new("ti-5", "Managing user and project settings interactions") - .icon(IconName::AiClaude) - .timestamp("7:37 PM") - .added(10) - .removed(3), - ) - .into_any_element(), - ), - single_example( - "Selected Item", - container() - .child( - ThreadItem::new("ti-6", "Refine textarea interaction behavior") - .icon(IconName::AiGemini) - .timestamp("3:00 PM") - .selected(true), - ) - .into_any_element(), - ), - ]; - - Some( - example_group(thread_item_examples) - .vertical() - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/toggle.rs b/crates/ui/src/components/toggle.rs deleted file mode 100644 index 86ff1d8eff..0000000000 --- a/crates/ui/src/components/toggle.rs +++ /dev/null @@ -1,1063 +0,0 @@ -use gpui::{ - AnyElement, AnyView, ClickEvent, ElementId, Hsla, IntoElement, KeybindingKeystroke, Keystroke, - Styled, Window, div, hsla, prelude::*, -}; -use settings::KeybindSource; -use std::{rc::Rc, sync::Arc}; - -use crate::utils::is_light; -use crate::{Color, Icon, IconName, ToggleState, Tooltip}; -use crate::{ElevationIndex, KeyBinding, prelude::*}; - -// TODO: Checkbox, CheckboxWithLabel, and Switch could all be -// restructured to use a ToggleLike, similar to Button/Buttonlike, Label/Labellike - -/// Creates a new checkbox. -pub fn checkbox(id: impl Into, toggle_state: ToggleState) -> Checkbox { - Checkbox::new(id, toggle_state) -} - -/// Creates a new switch. -pub fn switch(id: impl Into, toggle_state: ToggleState) -> Switch { - Switch::new(id, toggle_state) -} - -/// The visual style of a toggle. -#[derive(Debug, Default, Clone, PartialEq, Eq)] -pub enum ToggleStyle { - /// Toggle has a transparent background - #[default] - Ghost, - /// Toggle has a filled background based on the - /// elevation index of the parent container - ElevationBased(ElevationIndex), - /// A custom style using a color to tint the toggle - Custom(Hsla), -} - -/// # Checkbox -/// -/// Checkboxes are used for multiple choices, not for mutually exclusive choices. -/// Each checkbox works independently from other checkboxes in the list, -/// therefore checking an additional box does not affect any other selections. -#[derive(IntoElement, RegisterComponent)] -pub struct Checkbox { - id: ElementId, - toggle_state: ToggleState, - style: ToggleStyle, - disabled: bool, - placeholder: bool, - filled: bool, - visualization: bool, - label: Option, - label_size: LabelSize, - label_color: Color, - tooltip: Option AnyView>>, - on_click: Option>, -} - -impl Checkbox { - /// Creates a new [`Checkbox`]. - pub fn new(id: impl Into, checked: ToggleState) -> Self { - Self { - id: id.into(), - toggle_state: checked, - style: ToggleStyle::default(), - disabled: false, - placeholder: false, - filled: false, - visualization: false, - label: None, - label_size: LabelSize::Default, - label_color: Color::Muted, - tooltip: None, - on_click: None, - } - } - - /// Sets the disabled state of the [`Checkbox`]. - pub fn disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self - } - - /// Sets the disabled state of the [`Checkbox`]. - pub fn placeholder(mut self, placeholder: bool) -> Self { - self.placeholder = placeholder; - self - } - - /// Binds a handler to the [`Checkbox`] that will be called when clicked. - pub fn on_click( - mut self, - handler: impl Fn(&ToggleState, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_click = Some(Box::new(move |state, _, window, cx| { - handler(state, window, cx) - })); - self - } - - pub fn on_click_ext( - mut self, - handler: impl Fn(&ToggleState, &ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_click = Some(Box::new(handler)); - self - } - - /// Sets the `fill` setting of the checkbox, indicating whether it should be filled. - pub fn fill(mut self) -> Self { - self.filled = true; - self - } - - /// Makes the checkbox look enabled but without pointer cursor and hover styles. - /// Primarily used for uninteractive markdown previews. - pub fn visualization_only(mut self, visualization: bool) -> Self { - self.visualization = visualization; - self - } - - /// Sets the style of the checkbox using the specified [`ToggleStyle`]. - pub fn style(mut self, style: ToggleStyle) -> Self { - self.style = style; - self - } - - /// Match the style of the checkbox to the current elevation using [`ToggleStyle::ElevationBased`]. - pub fn elevation(mut self, elevation: ElevationIndex) -> Self { - self.style = ToggleStyle::ElevationBased(elevation); - self - } - - /// Sets the tooltip for the checkbox. - pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self { - self.tooltip = Some(Box::new(tooltip)); - self - } - - /// Set the label for the checkbox. - pub fn label(mut self, label: impl Into) -> Self { - self.label = Some(label.into()); - self - } - - pub fn label_size(mut self, size: LabelSize) -> Self { - self.label_size = size; - self - } - - pub fn label_color(mut self, color: Color) -> Self { - self.label_color = color; - self - } -} - -impl Checkbox { - fn bg_color(&self, cx: &App) -> Hsla { - let style = self.style.clone(); - match (style, self.filled) { - (ToggleStyle::Ghost, false) => cx.theme().colors().ghost_element_background, - (ToggleStyle::Ghost, true) => cx.theme().colors().element_background, - (ToggleStyle::ElevationBased(_), false) => gpui::transparent_black(), - (ToggleStyle::ElevationBased(elevation), true) => elevation.darker_bg(cx), - (ToggleStyle::Custom(_), false) => gpui::transparent_black(), - (ToggleStyle::Custom(color), true) => color.opacity(0.2), - } - } - - fn border_color(&self, cx: &App) -> Hsla { - if self.disabled { - return cx.theme().colors().border_variant; - } - - match self.style.clone() { - ToggleStyle::Ghost => cx.theme().colors().border, - ToggleStyle::ElevationBased(_) => cx.theme().colors().border, - ToggleStyle::Custom(color) => color.opacity(0.3), - } - } - - pub fn container_size() -> Pixels { - px(20.0) - } -} - -impl RenderOnce for Checkbox { - fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { - let group_id = format!("checkbox_group_{:?}", self.id); - let color = if self.disabled { - Color::Disabled - } else { - Color::Selected - }; - - let icon = match self.toggle_state { - ToggleState::Selected => { - if self.placeholder { - None - } else { - Some( - Icon::new(IconName::Check) - .size(IconSize::Small) - .color(color), - ) - } - } - ToggleState::Indeterminate => { - Some(Icon::new(IconName::Dash).size(IconSize::Small).color(color)) - } - ToggleState::Unselected => None, - }; - - let bg_color = self.bg_color(cx); - let border_color = self.border_color(cx); - let hover_border_color = border_color.alpha(0.7); - - let size = Self::container_size(); - - let checkbox = h_flex() - .group(group_id.clone()) - .id(self.id.clone()) - .size(size) - .justify_center() - .child( - div() - .flex() - .flex_none() - .justify_center() - .items_center() - .m_1() - .size_4() - .rounded_xs() - .bg(bg_color) - .border_1() - .border_color(border_color) - .when(self.disabled, |this| this.cursor_not_allowed()) - .when(self.disabled, |this| { - this.bg(cx.theme().colors().element_disabled.opacity(0.6)) - }) - .when(!self.disabled && !self.visualization, |this| { - this.group_hover(group_id.clone(), |el| el.border_color(hover_border_color)) - }) - .when(self.placeholder, |this| { - this.child( - div() - .flex_none() - .rounded_full() - .bg(color.color(cx).alpha(0.5)) - .size(px(4.)), - ) - }) - .children(icon), - ); - - h_flex() - .id(self.id) - .map(|this| { - if self.disabled { - this.cursor_not_allowed() - } else if self.visualization { - this.cursor_default() - } else { - this.cursor_pointer() - } - }) - .gap(DynamicSpacing::Base06.rems(cx)) - .child(checkbox) - .when_some(self.label, |this, label| { - this.child( - Label::new(label) - .color(self.label_color) - .size(self.label_size), - ) - }) - .when_some(self.tooltip, |this, tooltip| { - this.tooltip(move |window, cx| tooltip(window, cx)) - }) - .when_some( - self.on_click.filter(|_| !self.disabled), - |this, on_click| { - this.on_click(move |click, window, cx| { - on_click(&self.toggle_state.inverse(), click, window, cx) - }) - }, - ) - } -} - -/// Defines the color for a switch component. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] -pub enum SwitchColor { - #[default] - Accent, - Custom(Hsla), -} - -impl SwitchColor { - fn get_colors(&self, is_on: bool, cx: &App) -> (Hsla, Hsla) { - if !is_on { - return ( - cx.theme().colors().element_disabled, - cx.theme().colors().border, - ); - } - - match self { - SwitchColor::Accent => { - let status = cx.theme().status(); - let colors = cx.theme().colors(); - (status.info.opacity(0.4), colors.text_accent.opacity(0.2)) - } - SwitchColor::Custom(color) => (*color, color.opacity(0.6)), - } - } -} - -impl From for Color { - fn from(color: SwitchColor) -> Self { - match color { - SwitchColor::Accent => Color::Accent, - SwitchColor::Custom(_) => Color::Default, - } - } -} - -/// Defines the color for a switch component. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] -pub enum SwitchLabelPosition { - Start, - #[default] - End, -} - -/// # Switch -/// -/// Switches are used to represent opposite states, such as enabled or disabled. -#[derive(IntoElement, RegisterComponent)] -pub struct Switch { - id: ElementId, - toggle_state: ToggleState, - disabled: bool, - on_click: Option>, - label: Option, - label_position: Option, - label_size: LabelSize, - full_width: bool, - key_binding: Option, - color: SwitchColor, - tab_index: Option, -} - -impl Switch { - /// Creates a new [`Switch`]. - pub fn new(id: impl Into, state: ToggleState) -> Self { - Self { - id: id.into(), - toggle_state: state, - disabled: false, - on_click: None, - label: None, - label_position: None, - label_size: LabelSize::Small, - full_width: false, - key_binding: None, - color: SwitchColor::default(), - tab_index: None, - } - } - - /// Sets the color of the switch using the specified [`SwitchColor`]. - pub fn color(mut self, color: SwitchColor) -> Self { - self.color = color; - self - } - - /// Sets the disabled state of the [`Switch`]. - pub fn disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self - } - - /// Binds a handler to the [`Switch`] that will be called when clicked. - pub fn on_click( - mut self, - handler: impl Fn(&ToggleState, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_click = Some(Rc::new(handler)); - self - } - - /// Sets the label of the [`Switch`]. - pub fn label(mut self, label: impl Into) -> Self { - self.label = Some(label.into()); - self - } - - pub fn label_position( - mut self, - label_position: impl Into>, - ) -> Self { - self.label_position = label_position.into(); - self - } - - pub fn label_size(mut self, size: LabelSize) -> Self { - self.label_size = size; - self - } - - pub fn full_width(mut self, full_width: bool) -> Self { - self.full_width = full_width; - self - } - - /// Display the keybinding that triggers the switch action. - pub fn key_binding(mut self, key_binding: impl Into>) -> Self { - self.key_binding = key_binding.into(); - self - } - - pub fn tab_index(mut self, tab_index: impl Into) -> Self { - self.tab_index = Some(tab_index.into()); - self - } -} - -impl RenderOnce for Switch { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let is_on = self.toggle_state == ToggleState::Selected; - let adjust_ratio = if is_light(cx) { 1.5 } else { 1.0 }; - - let base_color = cx.theme().colors().text; - let thumb_color = base_color; - let (bg_color, border_color) = self.color.get_colors(is_on, cx); - - let bg_hover_color = if is_on { - bg_color.blend(base_color.opacity(0.16 * adjust_ratio)) - } else { - bg_color.blend(base_color.opacity(0.05 * adjust_ratio)) - }; - - let thumb_opacity = match (is_on, self.disabled) { - (_, true) => 0.2, - (true, false) => 1.0, - (false, false) => 0.5, - }; - - let group_id = format!("switch_group_{:?}", self.id); - let label = self.label; - - let switch = div() - .id((self.id.clone(), "switch")) - .p(px(1.0)) - .border_2() - .border_color(cx.theme().colors().border_transparent) - .rounded_full() - .when_some( - self.tab_index.filter(|_| !self.disabled), - |this, tab_index| { - this.tab_index(tab_index) - .focus_visible(|mut style| { - style.border_color = Some(cx.theme().colors().border_focused); - style - }) - .when_some(self.on_click.clone(), |this, on_click| { - this.on_click(move |_, window, cx| { - on_click(&self.toggle_state.inverse(), window, cx) - }) - }) - }, - ) - .child( - h_flex() - .w(DynamicSpacing::Base32.rems(cx)) - .h(DynamicSpacing::Base20.rems(cx)) - .group(group_id.clone()) - .child( - h_flex() - .when(is_on, |on| on.justify_end()) - .when(!is_on, |off| off.justify_start()) - .size_full() - .rounded_full() - .px(DynamicSpacing::Base02.px(cx)) - .bg(bg_color) - .when(!self.disabled, |this| { - this.group_hover(group_id.clone(), |el| el.bg(bg_hover_color)) - }) - .border_1() - .border_color(border_color) - .child( - div() - .size(DynamicSpacing::Base12.rems(cx)) - .rounded_full() - .bg(thumb_color) - .opacity(thumb_opacity), - ), - ), - ); - - h_flex() - .id(self.id) - .cursor_pointer() - .gap(DynamicSpacing::Base06.rems(cx)) - .when(self.full_width, |this| this.w_full().justify_between()) - .when( - self.label_position == Some(SwitchLabelPosition::Start), - |this| { - this.when_some(label.clone(), |this, label| { - this.child(Label::new(label).size(self.label_size)) - }) - }, - ) - .child(switch) - .when( - self.label_position == Some(SwitchLabelPosition::End), - |this| { - this.when_some(label, |this, label| { - this.child(Label::new(label).size(self.label_size)) - }) - }, - ) - .children(self.key_binding) - .when_some( - self.on_click.filter(|_| !self.disabled), - |this, on_click| { - this.on_click(move |_, window, cx| { - on_click(&self.toggle_state.inverse(), window, cx) - }) - }, - ) - } -} - -/// # SwitchField -/// -/// A field component that combines a label, description, and switch into one reusable component. -/// -/// # Examples -/// -/// ``` -/// use ui::prelude::*; -/// use ui::{SwitchField, ToggleState}; -/// -/// let switch_field = SwitchField::new( -/// "feature-toggle", -/// Some("Enable feature"), -/// Some("This feature adds new functionality to the app.".into()), -/// ToggleState::Unselected, -/// |state, window, cx| { -/// // Logic here -/// } -/// ); -/// ``` -#[derive(IntoElement, RegisterComponent)] -pub struct SwitchField { - id: ElementId, - label: Option, - description: Option, - toggle_state: ToggleState, - on_click: Arc, - disabled: bool, - color: SwitchColor, - tooltip: Option AnyView>>, - tab_index: Option, -} - -impl SwitchField { - pub fn new( - id: impl Into, - label: Option>, - description: Option, - toggle_state: impl Into, - on_click: impl Fn(&ToggleState, &mut Window, &mut App) + 'static, - ) -> Self { - Self { - id: id.into(), - label: label.map(Into::into), - description, - toggle_state: toggle_state.into(), - on_click: Arc::new(on_click), - disabled: false, - color: SwitchColor::Accent, - tooltip: None, - tab_index: None, - } - } - - pub fn description(mut self, description: impl Into) -> Self { - self.description = Some(description.into()); - self - } - - pub fn disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self - } - - /// Sets the color of the switch using the specified [`SwitchColor`]. - /// This changes the color scheme of the switch when it's in the "on" state. - pub fn color(mut self, color: SwitchColor) -> Self { - self.color = color; - self - } - - pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self { - self.tooltip = Some(Rc::new(tooltip)); - self - } - - pub fn tab_index(mut self, tab_index: isize) -> Self { - self.tab_index = Some(tab_index); - self - } -} - -impl RenderOnce for SwitchField { - fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement { - let tooltip = self - .tooltip - .zip(self.label.clone()) - .map(|(tooltip_fn, label)| { - h_flex().gap_0p5().child(Label::new(label)).child( - IconButton::new("tooltip_button", IconName::Info) - .icon_size(IconSize::XSmall) - .icon_color(Color::Muted) - .shape(crate::IconButtonShape::Square) - .style(ButtonStyle::Transparent) - .tooltip({ - let tooltip = tooltip_fn.clone(); - move |window, cx| tooltip(window, cx) - }) - .on_click(|_, _, _| {}), // Intentional empty on click handler so that clicking on the info tooltip icon doesn't trigger the switch toggle - ) - }); - - h_flex() - .id((self.id.clone(), "container")) - .when(!self.disabled, |this| { - this.hover(|this| this.cursor_pointer()) - }) - .w_full() - .gap_4() - .justify_between() - .flex_wrap() - .child(match (&self.description, tooltip) { - (Some(description), Some(tooltip)) => v_flex() - .gap_0p5() - .max_w_5_6() - .child(tooltip) - .child(Label::new(description.clone()).color(Color::Muted)) - .into_any_element(), - (Some(description), None) => v_flex() - .gap_0p5() - .max_w_5_6() - .when_some(self.label, |this, label| this.child(Label::new(label))) - .child(Label::new(description.clone()).color(Color::Muted)) - .into_any_element(), - (None, Some(tooltip)) => tooltip.into_any_element(), - (None, None) => { - if let Some(label) = self.label.clone() { - Label::new(label).into_any_element() - } else { - gpui::Empty.into_any_element() - } - } - }) - .child( - Switch::new((self.id.clone(), "switch"), self.toggle_state) - .color(self.color) - .disabled(self.disabled) - .when_some( - self.tab_index.filter(|_| !self.disabled), - |this, tab_index| this.tab_index(tab_index), - ) - .on_click({ - let on_click = self.on_click.clone(); - move |state, window, cx| { - (on_click)(state, window, cx); - } - }), - ) - .when(!self.disabled, |this| { - this.on_click({ - let on_click = self.on_click.clone(); - let toggle_state = self.toggle_state; - move |_click, window, cx| { - (on_click)(&toggle_state.inverse(), window, cx); - } - }) - }) - } -} - -impl Component for SwitchField { - fn scope() -> ComponentScope { - ComponentScope::Input - } - - fn description() -> Option<&'static str> { - Some("A field component that combines a label, description, and switch") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "States", - vec![ - single_example( - "Unselected", - SwitchField::new( - "switch_field_unselected", - Some("Enable notifications"), - Some("Receive notifications when new messages arrive.".into()), - ToggleState::Unselected, - |_, _, _| {}, - ) - .into_any_element(), - ), - single_example( - "Selected", - SwitchField::new( - "switch_field_selected", - Some("Enable notifications"), - Some("Receive notifications when new messages arrive.".into()), - ToggleState::Selected, - |_, _, _| {}, - ) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Colors", - vec![ - single_example( - "Default", - SwitchField::new( - "switch_field_default", - Some("Default color"), - Some("This uses the default switch color.".into()), - ToggleState::Selected, - |_, _, _| {}, - ) - .into_any_element(), - ), - single_example( - "Accent", - SwitchField::new( - "switch_field_accent", - Some("Accent color"), - Some("This uses the accent color scheme.".into()), - ToggleState::Selected, - |_, _, _| {}, - ) - .color(SwitchColor::Accent) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Disabled", - vec![single_example( - "Disabled", - SwitchField::new( - "switch_field_disabled", - Some("Disabled field"), - Some("This field is disabled and cannot be toggled.".into()), - ToggleState::Selected, - |_, _, _| {}, - ) - .disabled(true) - .into_any_element(), - )], - ), - example_group_with_title( - "No Description", - vec![single_example( - "No Description", - SwitchField::new( - "switch_field_disabled", - Some("Disabled field"), - None, - ToggleState::Selected, - |_, _, _| {}, - ) - .into_any_element(), - )], - ), - example_group_with_title( - "With Tooltip", - vec![ - single_example( - "Tooltip with Description", - SwitchField::new( - "switch_field_tooltip_with_desc", - Some("Nice Feature"), - Some("Enable advanced configuration options.".into()), - ToggleState::Unselected, - |_, _, _| {}, - ) - .tooltip(Tooltip::text("This is content for this tooltip!")) - .into_any_element(), - ), - single_example( - "Tooltip without Description", - SwitchField::new( - "switch_field_tooltip_no_desc", - Some("Nice Feature"), - None, - ToggleState::Selected, - |_, _, _| {}, - ) - .tooltip(Tooltip::text("This is content for this tooltip!")) - .into_any_element(), - ), - ], - ), - ]) - .into_any_element(), - ) - } -} - -impl Component for Checkbox { - fn scope() -> ComponentScope { - ComponentScope::Input - } - - fn description() -> Option<&'static str> { - Some("A checkbox component that can be used for multiple choice selections") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "States", - vec![ - single_example( - "Unselected", - Checkbox::new("checkbox_unselected", ToggleState::Unselected) - .into_any_element(), - ), - single_example( - "Placeholder", - Checkbox::new("checkbox_indeterminate", ToggleState::Selected) - .placeholder(true) - .into_any_element(), - ), - single_example( - "Indeterminate", - Checkbox::new("checkbox_indeterminate", ToggleState::Indeterminate) - .into_any_element(), - ), - single_example( - "Selected", - Checkbox::new("checkbox_selected", ToggleState::Selected) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Styles", - vec![ - single_example( - "Default", - Checkbox::new("checkbox_default", ToggleState::Selected) - .into_any_element(), - ), - single_example( - "Filled", - Checkbox::new("checkbox_filled", ToggleState::Selected) - .fill() - .into_any_element(), - ), - single_example( - "ElevationBased", - Checkbox::new("checkbox_elevation", ToggleState::Selected) - .style(ToggleStyle::ElevationBased( - ElevationIndex::EditorSurface, - )) - .into_any_element(), - ), - single_example( - "Custom Color", - Checkbox::new("checkbox_custom", ToggleState::Selected) - .style(ToggleStyle::Custom(hsla(142.0 / 360., 0.68, 0.45, 0.7))) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Disabled", - vec![ - single_example( - "Unselected", - Checkbox::new( - "checkbox_disabled_unselected", - ToggleState::Unselected, - ) - .disabled(true) - .into_any_element(), - ), - single_example( - "Selected", - Checkbox::new("checkbox_disabled_selected", ToggleState::Selected) - .disabled(true) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "With Label", - vec![single_example( - "Default", - Checkbox::new("checkbox_with_label", ToggleState::Selected) - .label("Always save on quit") - .into_any_element(), - )], - ), - example_group_with_title( - "Extra", - vec![single_example( - "Visualization-Only", - Checkbox::new("viz_only", ToggleState::Selected) - .visualization_only(true) - .into_any_element(), - )], - ), - ]) - .into_any_element(), - ) - } -} - -impl Component for Switch { - fn scope() -> ComponentScope { - ComponentScope::Input - } - - fn description() -> Option<&'static str> { - Some("A switch component that represents binary states like on/off") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "States", - vec![ - single_example( - "Off", - Switch::new("switch_off", ToggleState::Unselected) - .on_click(|_, _, _cx| {}) - .into_any_element(), - ), - single_example( - "On", - Switch::new("switch_on", ToggleState::Selected) - .on_click(|_, _, _cx| {}) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Colors", - vec![ - single_example( - "Accent (Default)", - Switch::new("switch_accent_style", ToggleState::Selected) - .on_click(|_, _, _cx| {}) - .into_any_element(), - ), - single_example( - "Custom", - Switch::new("switch_custom_style", ToggleState::Selected) - .color(SwitchColor::Custom(hsla(300.0 / 360.0, 0.6, 0.6, 1.0))) - .on_click(|_, _, _cx| {}) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "Disabled", - vec![ - single_example( - "Off", - Switch::new("switch_disabled_off", ToggleState::Unselected) - .disabled(true) - .into_any_element(), - ), - single_example( - "On", - Switch::new("switch_disabled_on", ToggleState::Selected) - .disabled(true) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "With Label", - vec![ - single_example( - "Start Label", - Switch::new("switch_with_label_start", ToggleState::Selected) - .label("Always save on quit") - .label_position(SwitchLabelPosition::Start) - .into_any_element(), - ), - single_example( - "End Label", - Switch::new("switch_with_label_end", ToggleState::Selected) - .label("Always save on quit") - .label_position(SwitchLabelPosition::End) - .into_any_element(), - ), - single_example( - "Default Size Label", - Switch::new( - "switch_with_label_default_size", - ToggleState::Selected, - ) - .label("Always save on quit") - .label_size(LabelSize::Default) - .into_any_element(), - ), - single_example( - "Small Size Label", - Switch::new("switch_with_label_small_size", ToggleState::Selected) - .label("Always save on quit") - .label_size(LabelSize::Small) - .into_any_element(), - ), - ], - ), - example_group_with_title( - "With Keybinding", - vec![single_example( - "Keybinding", - Switch::new("switch_with_keybinding", ToggleState::Selected) - .key_binding(Some(KeyBinding::from_keystrokes( - vec![KeybindingKeystroke::from_keystroke( - Keystroke::parse("cmd-s").unwrap(), - )] - .into(), - KeybindSource::Base, - ))) - .into_any_element(), - )], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/tooltip.rs b/crates/ui/src/components/tooltip.rs deleted file mode 100644 index 8b4ff3f731..0000000000 --- a/crates/ui/src/components/tooltip.rs +++ /dev/null @@ -1,297 +0,0 @@ -use std::borrow::Borrow; -use std::rc::Rc; - -use gpui::{Action, AnyElement, AnyView, AppContext, FocusHandle, IntoElement, Render}; -use settings::Settings; -use theme::ThemeSettings; - -use crate::prelude::*; -use crate::{Color, KeyBinding, Label, LabelSize, StyledExt, h_flex, v_flex}; - -#[derive(RegisterComponent)] -pub struct Tooltip { - title: Title, - meta: Option, - key_binding: Option, -} - -#[derive(Clone, IntoElement)] -enum Title { - Str(SharedString), - Callback(Rc AnyElement>), -} - -impl From for Title { - fn from(value: SharedString) -> Self { - Title::Str(value) - } -} - -impl RenderOnce for Title { - fn render(self, window: &mut Window, cx: &mut App) -> impl gpui::IntoElement { - match self { - Title::Str(title) => title.into_any_element(), - Title::Callback(element) => element(window, cx), - } - } -} - -impl Tooltip { - pub fn simple(title: impl Into, cx: &mut App) -> AnyView { - cx.new(|_| Self { - title: Title::Str(title.into()), - meta: None, - key_binding: None, - }) - .into() - } - - pub fn text(title: impl Into) -> impl Fn(&mut Window, &mut App) -> AnyView { - let title = title.into(); - move |_, cx| { - cx.new(|_| Self { - title: title.clone().into(), - meta: None, - key_binding: None, - }) - .into() - } - } - - pub fn for_action_title>( - title: T, - action: &dyn Action, - ) -> impl Fn(&mut Window, &mut App) -> AnyView + use { - let title = title.into(); - let action = action.boxed_clone(); - move |_, cx| { - cx.new(|cx| Self { - title: Title::Str(title.clone()), - meta: None, - key_binding: Some(KeyBinding::for_action(action.as_ref(), cx)), - }) - .into() - } - } - - pub fn for_action_title_in>( - title: Str, - action: &dyn Action, - focus_handle: &FocusHandle, - ) -> impl Fn(&mut Window, &mut App) -> AnyView + use { - let title = title.into(); - let action = action.boxed_clone(); - let focus_handle = focus_handle.clone(); - move |_, cx| { - cx.new(|cx| Self { - title: Title::Str(title.clone()), - meta: None, - key_binding: Some(KeyBinding::for_action_in( - action.as_ref(), - &focus_handle, - cx, - )), - }) - .into() - } - } - - pub fn for_action( - title: impl Into, - action: &dyn Action, - cx: &mut App, - ) -> AnyView { - cx.new(|cx| Self { - title: Title::Str(title.into()), - meta: None, - key_binding: Some(KeyBinding::for_action(action, cx)), - }) - .into() - } - - pub fn for_action_in( - title: impl Into, - action: &dyn Action, - focus_handle: &FocusHandle, - cx: &mut App, - ) -> AnyView { - cx.new(|cx| Self { - title: title.into().into(), - meta: None, - key_binding: Some(KeyBinding::for_action_in(action, focus_handle, cx)), - }) - .into() - } - - pub fn with_meta( - title: impl Into, - action: Option<&dyn Action>, - meta: impl Into, - cx: &mut App, - ) -> AnyView { - cx.new(|cx| Self { - title: title.into().into(), - meta: Some(meta.into()), - key_binding: action.map(|action| KeyBinding::for_action(action, cx)), - }) - .into() - } - - pub fn with_meta_in( - title: impl Into, - action: Option<&dyn Action>, - meta: impl Into, - focus_handle: &FocusHandle, - cx: &mut App, - ) -> AnyView { - cx.new(|cx| Self { - title: title.into().into(), - meta: Some(meta.into()), - key_binding: action.map(|action| KeyBinding::for_action_in(action, focus_handle, cx)), - }) - .into() - } - - pub fn new(title: impl Into) -> Self { - Self { - title: title.into().into(), - meta: None, - key_binding: None, - } - } - - pub fn new_element(title: impl Fn(&mut Window, &mut App) -> AnyElement + 'static) -> Self { - Self { - title: Title::Callback(Rc::new(title)), - meta: None, - key_binding: None, - } - } - - pub fn element( - title: impl Fn(&mut Window, &mut App) -> AnyElement + 'static, - ) -> impl Fn(&mut Window, &mut App) -> AnyView { - let title = Title::Callback(Rc::new(title)); - move |_, cx| { - let title = title.clone(); - cx.new(|_| Self { - title, - meta: None, - key_binding: None, - }) - .into() - } - } - - pub fn meta(mut self, meta: impl Into) -> Self { - self.meta = Some(meta.into()); - self - } - - pub fn key_binding(mut self, key_binding: impl Into>) -> Self { - self.key_binding = key_binding.into(); - self - } -} - -impl Render for Tooltip { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - tooltip_container(cx, |el, _| { - el.child( - h_flex() - .gap_4() - .child(div().max_w_72().child(self.title.clone())) - .when_some(self.key_binding.clone(), |this, key_binding| { - this.justify_between().child(key_binding) - }), - ) - .when_some(self.meta.clone(), |this, meta| { - this.child( - div() - .max_w_72() - .child(Label::new(meta).size(LabelSize::Small).color(Color::Muted)), - ) - }) - }) - } -} - -pub fn tooltip_container(cx: &mut C, f: impl FnOnce(Div, &mut C) -> Div) -> impl IntoElement -where - C: AppContext + Borrow, -{ - let app = (*cx).borrow(); - let ui_font = ThemeSettings::get_global(app).ui_font.clone(); - - // padding to avoid tooltip appearing right below the mouse cursor - div().pl_2().pt_2p5().child( - v_flex() - .elevation_2(app) - .font(ui_font) - .text_ui(app) - .text_color(app.theme().colors().text) - .py_1() - .px_2() - .map(|el| f(el, cx)), - ) -} - -pub struct LinkPreview { - link: SharedString, -} - -impl LinkPreview { - pub fn new(url: &str, cx: &mut App) -> AnyView { - let mut wrapped_url = String::new(); - for (i, ch) in url.chars().enumerate() { - if i == 500 { - wrapped_url.push('…'); - break; - } - if i % 100 == 0 && i != 0 { - wrapped_url.push('\n'); - } - wrapped_url.push(ch); - } - cx.new(|_| LinkPreview { - link: wrapped_url.into(), - }) - .into() - } -} - -impl Render for LinkPreview { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - tooltip_container(cx, |el, _| { - el.child( - Label::new(self.link.clone()) - .size(LabelSize::XSmall) - .color(Color::Muted), - ) - }) - } -} - -impl Component for Tooltip { - fn scope() -> ComponentScope { - ComponentScope::DataDisplay - } - - fn description() -> Option<&'static str> { - Some( - "A tooltip that appears when hovering over an element, optionally showing a keybinding or additional metadata.", - ) - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - example_group(vec![single_example( - "Text only", - Button::new("delete-example", "Delete") - .tooltip(Tooltip::text("This is a tooltip!")) - .into_any_element(), - )]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/components/tree_view_item.rs b/crates/ui/src/components/tree_view_item.rs deleted file mode 100644 index c96800223d..0000000000 --- a/crates/ui/src/components/tree_view_item.rs +++ /dev/null @@ -1,294 +0,0 @@ -use std::sync::Arc; - -use gpui::{AnyElement, AnyView, ClickEvent, MouseButton, MouseDownEvent}; - -use crate::{Disclosure, prelude::*}; - -#[derive(IntoElement, RegisterComponent)] -pub struct TreeViewItem { - id: ElementId, - group_name: Option, - label: SharedString, - expanded: bool, - selected: bool, - disabled: bool, - focused: bool, - default_expanded: bool, - root_item: bool, - tooltip: Option AnyView + 'static>>, - on_click: Option>, - on_hover: Option>, - on_toggle: Option>, - on_secondary_mouse_down: Option>, - tab_index: Option, - focus_handle: Option, -} - -impl TreeViewItem { - pub fn new(id: impl Into, label: impl Into) -> Self { - Self { - id: id.into(), - group_name: None, - label: label.into(), - expanded: false, - selected: false, - disabled: false, - focused: false, - default_expanded: false, - root_item: false, - tooltip: None, - on_click: None, - on_hover: None, - on_toggle: None, - on_secondary_mouse_down: None, - tab_index: None, - focus_handle: None, - } - } - - pub fn group_name(mut self, group_name: impl Into) -> Self { - self.group_name = Some(group_name.into()); - self - } - - pub fn on_click( - mut self, - handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_click = Some(Box::new(handler)); - self - } - - pub fn on_hover(mut self, handler: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self { - self.on_hover = Some(Box::new(handler)); - self - } - - pub fn on_secondary_mouse_down( - mut self, - handler: impl Fn(&MouseDownEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_secondary_mouse_down = Some(Box::new(handler)); - self - } - - pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self { - self.tooltip = Some(Box::new(tooltip)); - self - } - - pub fn tab_index(mut self, tab_index: isize) -> Self { - self.tab_index = Some(tab_index); - self - } - - pub fn expanded(mut self, toggle: bool) -> Self { - self.expanded = toggle; - self - } - - pub fn default_expanded(mut self, default_expanded: bool) -> Self { - self.default_expanded = default_expanded; - self - } - - pub fn on_toggle( - mut self, - on_toggle: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_toggle = Some(Arc::new(on_toggle)); - self - } - - pub fn root_item(mut self, root_item: bool) -> Self { - self.root_item = root_item; - self - } - - pub fn focused(mut self, focused: bool) -> Self { - self.focused = focused; - self - } - - pub fn track_focus(mut self, focus_handle: &gpui::FocusHandle) -> Self { - self.focus_handle = Some(focus_handle.clone()); - self - } -} - -impl Disableable for TreeViewItem { - fn disabled(mut self, disabled: bool) -> Self { - self.disabled = disabled; - self - } -} - -impl Toggleable for TreeViewItem { - fn toggle_state(mut self, selected: bool) -> Self { - self.selected = selected; - self - } -} - -impl RenderOnce for TreeViewItem { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let selected_bg = cx.theme().colors().element_active.opacity(0.5); - - let transparent_border = cx.theme().colors().border.opacity(0.); - let selected_border = cx.theme().colors().border.opacity(0.4); - let focused_border = cx.theme().colors().border_focused; - - let item_size = rems_from_px(28.); - let indentation_line = h_flex().size(item_size).flex_none().justify_center().child( - div() - .w_px() - .h_full() - .bg(cx.theme().colors().border.opacity(0.5)), - ); - - h_flex() - .id(self.id) - .when_some(self.group_name, |this, group| this.group(group)) - .w_full() - .child( - h_flex() - .id("inner_tree_view_item") - .cursor_pointer() - .size_full() - .h(item_size) - .rounded_sm() - .border_1() - .border_color(transparent_border) - .focus_visible(|s| s.border_color(focused_border)) - .when(self.selected, |this| { - this.border_color(selected_border).bg(selected_bg) - }) - .hover(|s| s.bg(cx.theme().colors().element_hover)) - .map(|this| { - let label = self.label; - - if self.root_item { - this.px_1() - .gap_2p5() - .child( - Disclosure::new("toggle", self.expanded) - .when_some( - self.on_toggle.clone(), - |disclosure, on_toggle| { - disclosure.on_toggle_expanded(on_toggle) - }, - ) - .opened_icon(IconName::ChevronDown) - .closed_icon(IconName::ChevronRight), - ) - .child( - Label::new(label) - .when(!self.selected, |this| this.color(Color::Muted)), - ) - } else { - this.child(indentation_line).child( - h_flex() - .id("nested_inner_tree_view_item") - .w_full() - .flex_grow() - .px_1() - .child( - Label::new(label) - .when(!self.selected, |this| this.color(Color::Muted)), - ), - ) - } - }) - .when_some(self.focus_handle, |this, handle| this.track_focus(&handle)) - .when_some(self.tab_index, |this, index| this.tab_index(index)) - .when_some(self.on_hover, |this, on_hover| this.on_hover(on_hover)) - .when_some( - self.on_click.filter(|_| !self.disabled), - |this, on_click| this.on_click(on_click), - ) - .when_some(self.on_secondary_mouse_down, |this, on_mouse_down| { - this.on_mouse_down(MouseButton::Right, move |event, window, cx| { - (on_mouse_down)(event, window, cx) - }) - }) - .when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip)), - ) - } -} - -impl Component for TreeViewItem { - fn scope() -> ComponentScope { - ComponentScope::Navigation - } - - fn description() -> Option<&'static str> { - Some( - "A hierarchical list of items that may have a parent-child relationship where children can be toggled into view by expanding or collapsing their parent item.", - ) - } - - fn preview(_window: &mut Window, cx: &mut App) -> Option { - let container = || { - v_flex() - .p_2() - .w_64() - .border_1() - .border_color(cx.theme().colors().border_variant) - .bg(cx.theme().colors().panel_background) - }; - - Some( - example_group(vec![ - single_example( - "Basic Tree View", - container() - .child( - TreeViewItem::new("index-1", "Tree Item Root #1") - .root_item(true) - .toggle_state(true), - ) - .child(TreeViewItem::new("index-2", "Tree Item #2")) - .child(TreeViewItem::new("index-3", "Tree Item #3")) - .child(TreeViewItem::new("index-4", "Tree Item Root #2").root_item(true)) - .child(TreeViewItem::new("index-5", "Tree Item #5")) - .child(TreeViewItem::new("index-6", "Tree Item #6")) - .into_any_element(), - ), - single_example( - "Active Child", - container() - .child(TreeViewItem::new("index-1", "Tree Item Root #1").root_item(true)) - .child(TreeViewItem::new("index-2", "Tree Item #2").toggle_state(true)) - .child(TreeViewItem::new("index-3", "Tree Item #3")) - .into_any_element(), - ), - single_example( - "Focused Parent", - container() - .child( - TreeViewItem::new("index-1", "Tree Item Root #1") - .root_item(true) - .focused(true) - .toggle_state(true), - ) - .child(TreeViewItem::new("index-2", "Tree Item #2")) - .child(TreeViewItem::new("index-3", "Tree Item #3")) - .into_any_element(), - ), - single_example( - "Focused Child", - container() - .child( - TreeViewItem::new("index-1", "Tree Item Root #1") - .root_item(true) - .toggle_state(true), - ) - .child(TreeViewItem::new("index-2", "Tree Item #2").focused(true)) - .child(TreeViewItem::new("index-3", "Tree Item #3")) - .into_any_element(), - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/prelude.rs b/crates/ui/src/prelude.rs deleted file mode 100644 index 0357e498bb..0000000000 --- a/crates/ui/src/prelude.rs +++ /dev/null @@ -1,35 +0,0 @@ -//! The prelude of this crate. When building UI in Zed you almost always want to import this. - -pub use gpui::prelude::*; -pub use gpui::{ - AbsoluteLength, AnyElement, App, Context, DefiniteLength, Div, Element, ElementId, - InteractiveElement, ParentElement, Pixels, Rems, RenderOnce, SharedString, Styled, Window, div, - px, relative, rems, -}; - -pub use component::{ - Component, ComponentScope, example_group, example_group_with_title, single_example, -}; -pub use ui_macros::RegisterComponent; - -pub use crate::DynamicSpacing; -pub use crate::animation::{AnimationDirection, AnimationDuration, DefaultAnimations}; -pub use crate::styles::{ - PlatformStyle, Severity, StyledTypography, TextSize, rems_from_px, vh, vw, -}; -pub use crate::traits::clickable::*; -pub use crate::traits::disableable::*; -pub use crate::traits::fixed::*; -pub use crate::traits::styled_ext::*; -pub use crate::traits::toggleable::*; -pub use crate::traits::visible_on_hover::*; -pub use crate::{Button, ButtonSize, ButtonStyle, IconButton, SelectableButton}; -pub use crate::{ButtonCommon, Color}; -pub use crate::{Headline, HeadlineSize}; -pub use crate::{Icon, IconName, IconPosition, IconSize}; -pub use crate::{Label, LabelCommon, LabelSize, LineHeightStyle, LoadingLabel}; -pub use crate::{h_container, h_flex, v_container, v_flex}; -pub use crate::{ - h_group, h_group_lg, h_group_sm, h_group_xl, v_group, v_group_lg, v_group_sm, v_group_xl, -}; -pub use theme::ActiveTheme; diff --git a/crates/ui/src/styles.rs b/crates/ui/src/styles.rs deleted file mode 100644 index bc2399f54b..0000000000 --- a/crates/ui/src/styles.rs +++ /dev/null @@ -1,18 +0,0 @@ -pub mod animation; -mod appearance; -mod color; -mod elevation; -mod platform; -mod severity; -mod spacing; -mod typography; -mod units; - -pub use appearance::*; -pub use color::*; -pub use elevation::*; -pub use platform::*; -pub use severity::*; -pub use spacing::*; -pub use typography::*; -pub use units::*; diff --git a/crates/ui/src/styles/animation.rs b/crates/ui/src/styles/animation.rs deleted file mode 100644 index acea834548..0000000000 --- a/crates/ui/src/styles/animation.rs +++ /dev/null @@ -1,290 +0,0 @@ -use crate::{ContentGroup, prelude::*}; -use gpui::{AnimationElement, AnimationExt, Styled}; -use std::time::Duration; - -use gpui::ease_out_quint; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum AnimationDuration { - Instant = 50, - Fast = 150, - Slow = 300, -} - -impl AnimationDuration { - pub fn duration(&self) -> Duration { - Duration::from_millis(*self as u64) - } -} - -impl Into for AnimationDuration { - fn into(self) -> Duration { - self.duration() - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum AnimationDirection { - FromBottom, - FromLeft, - FromRight, - FromTop, -} - -pub trait DefaultAnimations: Styled + Sized + Element { - fn animate_in( - self, - animation_type: AnimationDirection, - fade_in: bool, - ) -> AnimationElement { - let animation_name = match animation_type { - AnimationDirection::FromBottom => "animate_from_bottom", - AnimationDirection::FromLeft => "animate_from_left", - AnimationDirection::FromRight => "animate_from_right", - AnimationDirection::FromTop => "animate_from_top", - }; - - let animation_id = self.id().map_or_else( - || ElementId::from(animation_name), - |id| (id, animation_name).into(), - ); - - self.with_animation( - animation_id, - gpui::Animation::new(AnimationDuration::Fast.into()).with_easing(ease_out_quint()), - move |mut this, delta| { - let start_opacity = 0.4; - let start_pos = 0.0; - let end_pos = 40.0; - - if fade_in { - this = this.opacity(start_opacity + delta * (1.0 - start_opacity)); - } - - match animation_type { - AnimationDirection::FromBottom => { - this.bottom(px(start_pos + delta * (end_pos - start_pos))) - } - AnimationDirection::FromLeft => { - this.left(px(start_pos + delta * (end_pos - start_pos))) - } - AnimationDirection::FromRight => { - this.right(px(start_pos + delta * (end_pos - start_pos))) - } - AnimationDirection::FromTop => { - this.top(px(start_pos + delta * (end_pos - start_pos))) - } - } - }, - ) - } - - fn animate_in_from_bottom(self, fade: bool) -> AnimationElement { - self.animate_in(AnimationDirection::FromBottom, fade) - } - - fn animate_in_from_left(self, fade: bool) -> AnimationElement { - self.animate_in(AnimationDirection::FromLeft, fade) - } - - fn animate_in_from_right(self, fade: bool) -> AnimationElement { - self.animate_in(AnimationDirection::FromRight, fade) - } - - fn animate_in_from_top(self, fade: bool) -> AnimationElement { - self.animate_in(AnimationDirection::FromTop, fade) - } -} - -impl DefaultAnimations for E {} - -// Don't use this directly, it only exists to show animation previews -#[derive(RegisterComponent)] -struct Animation {} - -impl Component for Animation { - fn scope() -> ComponentScope { - ComponentScope::Utilities - } - - fn description() -> Option<&'static str> { - Some("Demonstrates various animation patterns and transitions available in the UI system.") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - let container_size = 128.0; - let element_size = 32.0; - let offset = container_size / 2.0 - element_size / 2.0; - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Animate In", - vec![ - single_example( - "From Bottom", - ContentGroup::new() - .relative() - .items_center() - .justify_center() - .size(px(container_size)) - .child( - div() - .id("animate-in-from-bottom") - .absolute() - .size(px(element_size)) - .left(px(offset)) - .rounded_md() - .bg(gpui::red()) - .animate_in_from_bottom(false), - ) - .into_any_element(), - ), - single_example( - "From Top", - ContentGroup::new() - .relative() - .items_center() - .justify_center() - .size(px(container_size)) - .child( - div() - .id("animate-in-from-top") - .absolute() - .size(px(element_size)) - .left(px(offset)) - .rounded_md() - .bg(gpui::blue()) - .animate_in_from_top(false), - ) - .into_any_element(), - ), - single_example( - "From Left", - ContentGroup::new() - .relative() - .items_center() - .justify_center() - .size(px(container_size)) - .child( - div() - .id("animate-in-from-left") - .absolute() - .size(px(element_size)) - .top(px(offset)) - .rounded_md() - .bg(gpui::green()) - .animate_in_from_left(false), - ) - .into_any_element(), - ), - single_example( - "From Right", - ContentGroup::new() - .relative() - .items_center() - .justify_center() - .size(px(container_size)) - .child( - div() - .id("animate-in-from-right") - .absolute() - .size(px(element_size)) - .top(px(offset)) - .rounded_md() - .bg(gpui::yellow()) - .animate_in_from_right(false), - ) - .into_any_element(), - ), - ], - ) - .grow(), - example_group_with_title( - "Fade and Animate In", - vec![ - single_example( - "From Bottom", - ContentGroup::new() - .relative() - .items_center() - .justify_center() - .size(px(container_size)) - .child( - div() - .id("fade-animate-in-from-bottom") - .absolute() - .size(px(element_size)) - .left(px(offset)) - .rounded_md() - .bg(gpui::red()) - .animate_in_from_bottom(true), - ) - .into_any_element(), - ), - single_example( - "From Top", - ContentGroup::new() - .relative() - .items_center() - .justify_center() - .size(px(container_size)) - .child( - div() - .id("fade-animate-in-from-top") - .absolute() - .size(px(element_size)) - .left(px(offset)) - .rounded_md() - .bg(gpui::blue()) - .animate_in_from_top(true), - ) - .into_any_element(), - ), - single_example( - "From Left", - ContentGroup::new() - .relative() - .items_center() - .justify_center() - .size(px(container_size)) - .child( - div() - .id("fade-animate-in-from-left") - .absolute() - .size(px(element_size)) - .top(px(offset)) - .rounded_md() - .bg(gpui::green()) - .animate_in_from_left(true), - ) - .into_any_element(), - ), - single_example( - "From Right", - ContentGroup::new() - .relative() - .items_center() - .justify_center() - .size(px(container_size)) - .child( - div() - .id("fade-animate-in-from-right") - .absolute() - .size(px(element_size)) - .top(px(offset)) - .rounded_md() - .bg(gpui::yellow()) - .animate_in_from_right(true), - ) - .into_any_element(), - ), - ], - ) - .grow(), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/styles/appearance.rs b/crates/ui/src/styles/appearance.rs deleted file mode 100644 index 007511584e..0000000000 --- a/crates/ui/src/styles/appearance.rs +++ /dev/null @@ -1,19 +0,0 @@ -use crate::prelude::*; -use gpui::{App, WindowBackgroundAppearance}; - -/// Returns the [WindowBackgroundAppearance]. -fn window_appearance(cx: &mut App) -> WindowBackgroundAppearance { - cx.theme().styles.window_background_appearance -} - -/// Returns if the window and it's surfaces are expected -/// to be transparent. -/// -/// Helps determine if you need to take extra steps to prevent -/// transparent backgrounds. -pub fn theme_is_transparent(cx: &mut App) -> bool { - matches!( - window_appearance(cx), - WindowBackgroundAppearance::Transparent | WindowBackgroundAppearance::Blurred - ) -} diff --git a/crates/ui/src/styles/color.rs b/crates/ui/src/styles/color.rs deleted file mode 100644 index 586b2ccc57..0000000000 --- a/crates/ui/src/styles/color.rs +++ /dev/null @@ -1,244 +0,0 @@ -use crate::{Label, LabelCommon, component_prelude::*, v_flex}; -use documented::{DocumentedFields, DocumentedVariants}; -use gpui::{App, Hsla, IntoElement, ParentElement, Styled}; -use theme::ActiveTheme; - -/// Sets a color that has a consistent meaning across all themes. -#[derive( - Debug, - Default, - Eq, - PartialEq, - Copy, - Clone, - RegisterComponent, - Documented, - DocumentedFields, - DocumentedVariants, -)] -pub enum Color { - #[default] - /// The default text color. Might be known as "foreground" or "primary" in - /// some theme systems. - /// - /// For less emphasis, consider using [`Color::Muted`] or [`Color::Hidden`]. - Default, - /// A text color used for accents, such as links or highlights. - Accent, - /// A color used to indicate a conflict, such as a version control merge conflict, or a conflict between a file in the editor and the file system. - Conflict, - /// A color used to indicate a newly created item, such as a new file in - /// version control, or a new file on disk. - Created, - /// It is highly, HIGHLY recommended not to use this! Using this color - /// means detaching it from any semantic meaning across themes. - /// - /// A custom color specified by an HSLA value. - Custom(Hsla), - /// A color used for all debugger UI elements. - Debugger, - /// A color used to indicate a deleted item, such as a file removed from version control. - Deleted, - /// A color used for disabled UI elements or text, like a disabled button or menu item. - Disabled, - /// A color used to indicate an error condition, or something the user - /// cannot do. In very rare cases, it might be used to indicate dangerous or - /// destructive action. - Error, - /// A color used for elements that represent something that is hidden, like - /// a hidden file, or an element that should be visually de-emphasized. - Hidden, - /// A color used for hint or suggestion text, often a blue color. Use this - /// color to represent helpful, or semantically neutral information. - Hint, - /// A color used for items that are intentionally ignored, such as files ignored by version control. - Ignored, - /// A color used for informational messages or status indicators, often a blue color. - Info, - /// A color used to indicate a modified item, such as an edited file, or a modified entry in version control. - Modified, - /// A color used for text or UI elements that should be visually muted or de-emphasized. - /// - /// For more emphasis, consider using [`Color::Default`]. - /// - /// For less emphasis, consider using [`Color::Hidden`]. - Muted, - /// A color used for placeholder text in input fields. - Placeholder, - /// A color associated with a specific player number. - Player(u32), - /// A color used to indicate selected text or UI elements. - Selected, - /// A color used to indicate a successful operation or status. - Success, - /// A version control color used to indicate a newly added file or content in version control. - VersionControlAdded, - /// A version control color used to indicate conflicting changes that need resolution. - VersionControlConflict, - /// A version control color used to indicate a file or content that has been deleted in version control. - VersionControlDeleted, - /// A version control color used to indicate files or content that is being ignored by version control. - VersionControlIgnored, - /// A version control color used to indicate modified files or content in version control. - VersionControlModified, - /// A color used to indicate a warning condition. - Warning, -} - -impl Color { - /// Returns the Color's HSLA value. - pub fn color(&self, cx: &App) -> Hsla { - match self { - Color::Default => cx.theme().colors().text, - Color::Muted => cx.theme().colors().text_muted, - Color::Created => cx.theme().status().created, - Color::Modified => cx.theme().status().modified, - Color::Conflict => cx.theme().status().conflict, - Color::Ignored => cx.theme().status().ignored, - Color::Debugger => cx.theme().colors().debugger_accent, - Color::Deleted => cx.theme().status().deleted, - Color::Disabled => cx.theme().colors().text_disabled, - Color::Hidden => cx.theme().status().hidden, - Color::Hint => cx.theme().status().hint, - Color::Info => cx.theme().status().info, - Color::Placeholder => cx.theme().colors().text_placeholder, - Color::Accent => cx.theme().colors().text_accent, - Color::Player(i) => cx.theme().styles.player.color_for_participant(*i).cursor, - Color::Error => cx.theme().status().error, - Color::Selected => cx.theme().colors().text_accent, - Color::Success => cx.theme().status().success, - Color::VersionControlAdded => cx.theme().colors().version_control_added, - Color::VersionControlConflict => cx.theme().colors().version_control_conflict, - Color::VersionControlDeleted => cx.theme().colors().version_control_deleted, - Color::VersionControlIgnored => cx.theme().colors().version_control_ignored, - Color::VersionControlModified => cx.theme().colors().version_control_modified, - Color::Warning => cx.theme().status().warning, - Color::Custom(color) => *color, - } - } -} - -impl From for Color { - fn from(color: Hsla) -> Self { - Color::Custom(color) - } -} - -impl Component for Color { - fn scope() -> ComponentScope { - ComponentScope::Utilities - } - - fn description() -> Option<&'static str> { - Some(Color::DOCS) - } - - fn preview(_window: &mut gpui::Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_6() - .children(vec![ - example_group_with_title( - "Text Colors", - vec![ - single_example( - "Default", - Label::new("Default text color") - .color(Color::Default) - .into_any_element(), - ) - .description(Color::Default.get_variant_docs()), - single_example( - "Muted", - Label::new("Muted text color") - .color(Color::Muted) - .into_any_element(), - ) - .description(Color::Muted.get_variant_docs()), - single_example( - "Accent", - Label::new("Accent text color") - .color(Color::Accent) - .into_any_element(), - ) - .description(Color::Accent.get_variant_docs()), - single_example( - "Disabled", - Label::new("Disabled text color") - .color(Color::Disabled) - .into_any_element(), - ) - .description(Color::Disabled.get_variant_docs()), - ], - ), - example_group_with_title( - "Status Colors", - vec![ - single_example( - "Success", - Label::new("Success status") - .color(Color::Success) - .into_any_element(), - ) - .description(Color::Success.get_variant_docs()), - single_example( - "Warning", - Label::new("Warning status") - .color(Color::Warning) - .into_any_element(), - ) - .description(Color::Warning.get_variant_docs()), - single_example( - "Error", - Label::new("Error status") - .color(Color::Error) - .into_any_element(), - ) - .description(Color::Error.get_variant_docs()), - single_example( - "Info", - Label::new("Info status") - .color(Color::Info) - .into_any_element(), - ) - .description(Color::Info.get_variant_docs()), - ], - ), - example_group_with_title( - "Version Control Colors", - vec![ - single_example( - "Created", - Label::new("Created item") - .color(Color::Created) - .into_any_element(), - ) - .description(Color::Created.get_variant_docs()), - single_example( - "Modified", - Label::new("Modified item") - .color(Color::Modified) - .into_any_element(), - ) - .description(Color::Modified.get_variant_docs()), - single_example( - "Deleted", - Label::new("Deleted item") - .color(Color::Deleted) - .into_any_element(), - ) - .description(Color::Deleted.get_variant_docs()), - single_example( - "Conflict", - Label::new("Conflict item") - .color(Color::Conflict) - .into_any_element(), - ) - .description(Color::Conflict.get_variant_docs()), - ], - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/styles/elevation.rs b/crates/ui/src/styles/elevation.rs deleted file mode 100644 index 35e8e499b9..0000000000 --- a/crates/ui/src/styles/elevation.rs +++ /dev/null @@ -1,129 +0,0 @@ -use std::fmt::{self, Display, Formatter}; - -use gpui::{App, BoxShadow, Hsla, hsla, point, px}; -use theme::{ActiveTheme, Appearance}; - -/// Today, elevation is primarily used to add shadows to elements, and set the correct background for elements like buttons. -/// -/// Elevation can be thought of as the physical closeness of an element to the -/// user. Elements with lower elevations are physically further away on the -/// z-axis and appear to be underneath elements with higher elevations. -/// -/// In the future, a more complete approach to elevation may be added. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ElevationIndex { - /// On the layer of the app background. This is under panels, panes, and - /// other surfaces. - Background, - /// The primary surface – Contains panels, panes, containers, etc. - Surface, - /// The same elevation as the primary surface, but used for the editable areas, like buffers - EditorSurface, - /// A surface that is elevated above the primary surface. but below washes, models, and dragged elements. - ElevatedSurface, - /// A surface above the [ElevationIndex::ElevatedSurface] that is used for dialogs, alerts, modals, etc. - ModalSurface, -} - -impl Display for ElevationIndex { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match self { - ElevationIndex::Background => write!(f, "Background"), - ElevationIndex::Surface => write!(f, "Surface"), - ElevationIndex::EditorSurface => write!(f, "Editor Surface"), - ElevationIndex::ElevatedSurface => write!(f, "Elevated Surface"), - ElevationIndex::ModalSurface => write!(f, "Modal Surface"), - } - } -} - -impl ElevationIndex { - /// Returns an appropriate shadow for the given elevation index. - pub fn shadow(self, cx: &App) -> Vec { - let is_light = cx.theme().appearance() == Appearance::Light; - - match self { - ElevationIndex::Surface => vec![], - ElevationIndex::EditorSurface => vec![], - - ElevationIndex::ElevatedSurface => vec![ - BoxShadow { - color: hsla(0., 0., 0., 0.12), - offset: point(px(0.), px(2.)), - blur_radius: px(3.), - spread_radius: px(0.), - }, - BoxShadow { - color: hsla(0., 0., 0., if is_light { 0.03 } else { 0.06 }), - offset: point(px(1.), px(1.)), - blur_radius: px(0.), - spread_radius: px(0.), - }, - ], - - ElevationIndex::ModalSurface => vec![ - BoxShadow { - color: hsla(0., 0., 0., if is_light { 0.06 } else { 0.12 }), - offset: point(px(0.), px(2.)), - blur_radius: px(3.), - spread_radius: px(0.), - }, - BoxShadow { - color: hsla(0., 0., 0., if is_light { 0.06 } else { 0.08 }), - offset: point(px(0.), px(3.)), - blur_radius: px(6.), - spread_radius: px(0.), - }, - BoxShadow { - color: hsla(0., 0., 0., 0.04), - offset: point(px(0.), px(6.)), - blur_radius: px(12.), - spread_radius: px(0.), - }, - BoxShadow { - color: hsla(0., 0., 0., if is_light { 0.04 } else { 0.12 }), - offset: point(px(1.), px(1.)), - blur_radius: px(0.), - spread_radius: px(0.), - }, - ], - - _ => vec![], - } - } - - /// Returns the background color for the given elevation index. - pub fn bg(&self, cx: &mut App) -> Hsla { - match self { - ElevationIndex::Background => cx.theme().colors().background, - ElevationIndex::Surface => cx.theme().colors().surface_background, - ElevationIndex::EditorSurface => cx.theme().colors().editor_background, - ElevationIndex::ElevatedSurface => cx.theme().colors().elevated_surface_background, - ElevationIndex::ModalSurface => cx.theme().colors().elevated_surface_background, - } - } - - /// Returns a color that is appropriate a filled element on this elevation - pub fn on_elevation_bg(&self, cx: &App) -> Hsla { - match self { - ElevationIndex::Background => cx.theme().colors().surface_background, - ElevationIndex::Surface => cx.theme().colors().background, - ElevationIndex::EditorSurface => cx.theme().colors().surface_background, - ElevationIndex::ElevatedSurface => cx.theme().colors().background, - ElevationIndex::ModalSurface => cx.theme().colors().background, - } - } - - /// Attempts to return a darker background color than the current elevation index's background. - /// - /// If the current background color is already dark, it will return a lighter color instead. - pub fn darker_bg(&self, cx: &App) -> Hsla { - match self { - ElevationIndex::Background => cx.theme().colors().surface_background, - ElevationIndex::Surface => cx.theme().colors().editor_background, - ElevationIndex::EditorSurface => cx.theme().colors().surface_background, - ElevationIndex::ElevatedSurface => cx.theme().colors().editor_background, - ElevationIndex::ModalSurface => cx.theme().colors().editor_background, - } - } -} diff --git a/crates/ui/src/styles/platform.rs b/crates/ui/src/styles/platform.rs deleted file mode 100644 index 0d873a2ff2..0000000000 --- a/crates/ui/src/styles/platform.rs +++ /dev/null @@ -1,25 +0,0 @@ -/// The platform style to use when rendering UI. -/// -/// This can be used to abstract over platform differences. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] -pub enum PlatformStyle { - /// Display in macOS style. - Mac, - /// Display in Linux style. - Linux, - /// Display in Windows style. - Windows, -} - -impl PlatformStyle { - /// Returns the [`PlatformStyle`] for the current platform. - pub const fn platform() -> Self { - if cfg!(any(target_os = "linux", target_os = "freebsd")) { - Self::Linux - } else if cfg!(target_os = "windows") { - Self::Windows - } else { - Self::Mac - } - } -} diff --git a/crates/ui/src/styles/severity.rs b/crates/ui/src/styles/severity.rs deleted file mode 100644 index 464f835186..0000000000 --- a/crates/ui/src/styles/severity.rs +++ /dev/null @@ -1,10 +0,0 @@ -/// Severity levels that determine the style of the component. -/// Usually, it affects the background. Most of the time, -/// it also follows with an icon corresponding the severity level. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Severity { - Info, - Success, - Warning, - Error, -} diff --git a/crates/ui/src/styles/spacing.rs b/crates/ui/src/styles/spacing.rs deleted file mode 100644 index c6629f5d88..0000000000 --- a/crates/ui/src/styles/spacing.rs +++ /dev/null @@ -1,55 +0,0 @@ -use gpui::{App, Pixels, Rems, px, rems}; -use settings::Settings; -use theme::{ThemeSettings, UiDensity}; -use ui_macros::derive_dynamic_spacing; - -// Derives [DynamicSpacing]. See [ui_macros::derive_dynamic_spacing]. -// -// There are 3 UI density settings: Compact, Default, and Comfortable. -// -// When a tuple of three values is provided, the values are used directly. -// -// Example: (1, 2, 4) => Compact: 1px, Default: 2px, Comfortable: 4px -// -// When a single value is provided, the standard spacing formula is -// used to derive the of spacing values. This formula can be found in -// the macro. -// -// Example: -// -// Assuming the standard formula is (n-4, n, n+4) -// -// 24 => Compact: 20px, Default: 24px, Comfortable: 28px -// -// The [DynamicSpacing] enum variants use a BaseXX format, -// where XX = the pixel value @ default rem size and the default UI density. -// -// Example: -// -// DynamicSpacing::Base16 would return 16px at the default UI scale & density. -derive_dynamic_spacing![ - (0, 0, 0), - (1, 1, 2), - (1, 2, 4), - (2, 3, 4), - (2, 4, 6), - (3, 6, 8), - (4, 8, 10), - (10, 12, 14), - (14, 16, 18), - (18, 20, 22), - 24, - 32, - 40, - 48 -]; - -/// Returns the current [`UiDensity`] setting. Use this to -/// modify or show something in the UI other than spacing. -/// -/// Do not use this to calculate spacing values. -/// -/// Always use [DynamicSpacing] for spacing values. -pub fn ui_density(cx: &mut App) -> UiDensity { - ThemeSettings::get_global(cx).ui_density -} diff --git a/crates/ui/src/styles/typography.rs b/crates/ui/src/styles/typography.rs deleted file mode 100644 index 2bb0b35720..0000000000 --- a/crates/ui/src/styles/typography.rs +++ /dev/null @@ -1,295 +0,0 @@ -use crate::prelude::*; -use gpui::{ - AnyElement, App, IntoElement, ParentElement, Rems, RenderOnce, SharedString, Styled, Window, - div, rems, -}; -use settings::Settings; -use theme::{ActiveTheme, ThemeSettings}; - -use crate::{Color, rems_from_px}; - -/// Extends [`gpui::Styled`] with typography-related styling methods. -pub trait StyledTypography: Styled + Sized { - /// Sets the font family to the buffer font. - fn font_buffer(self, cx: &App) -> Self { - let settings = ThemeSettings::get_global(cx); - let buffer_font_family = settings.buffer_font.family.clone(); - - self.font_family(buffer_font_family) - } - - /// Sets the font family to the UI font. - fn font_ui(self, cx: &App) -> Self { - let settings = ThemeSettings::get_global(cx); - let ui_font_family = settings.ui_font.family.clone(); - - self.font_family(ui_font_family) - } - - /// Sets the text size using a [`TextSize`]. - fn text_ui_size(self, size: TextSize, cx: &App) -> Self { - self.text_size(size.rems(cx)) - } - - /// The large size for UI text. - /// - /// `1rem` or `16px` at the default scale of `1rem` = `16px`. - /// - /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. - /// - /// Use `text_ui` for regular-sized text. - fn text_ui_lg(self, cx: &App) -> Self { - self.text_size(TextSize::Large.rems(cx)) - } - - /// The default size for UI text. - /// - /// `0.825rem` or `14px` at the default scale of `1rem` = `16px`. - /// - /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. - /// - /// Use `text_ui_sm` for smaller text. - fn text_ui(self, cx: &App) -> Self { - self.text_size(TextSize::default().rems(cx)) - } - - /// The small size for UI text. - /// - /// `0.75rem` or `12px` at the default scale of `1rem` = `16px`. - /// - /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. - /// - /// Use `text_ui` for regular-sized text. - fn text_ui_sm(self, cx: &App) -> Self { - self.text_size(TextSize::Small.rems(cx)) - } - - /// The extra small size for UI text. - /// - /// `0.625rem` or `10px` at the default scale of `1rem` = `16px`. - /// - /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. - /// - /// Use `text_ui` for regular-sized text. - fn text_ui_xs(self, cx: &App) -> Self { - self.text_size(TextSize::XSmall.rems(cx)) - } - - /// The font size for buffer text. - /// - /// Retrieves the default font size, or the user's custom font size if set. - /// - /// This should only be used for text that is displayed in a buffer, - /// or other places that text needs to match the user's buffer font size. - fn text_buffer(self, cx: &App) -> Self { - let settings = ThemeSettings::get_global(cx); - self.text_size(settings.buffer_font_size(cx)) - } -} - -impl StyledTypography for E {} - -/// A utility for getting the size of various semantic text sizes. -#[derive(Debug, Default, Clone)] -pub enum TextSize { - /// The default size for UI text. - /// - /// `0.825rem` or `14px` at the default scale of `1rem` = `16px`. - /// - /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. - #[default] - Default, - /// The large size for UI text. - /// - /// `1rem` or `16px` at the default scale of `1rem` = `16px`. - /// - /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. - Large, - - /// The small size for UI text. - /// - /// `0.75rem` or `12px` at the default scale of `1rem` = `16px`. - /// - /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. - Small, - - /// The extra small size for UI text. - /// - /// `0.625rem` or `10px` at the default scale of `1rem` = `16px`. - /// - /// Note: The absolute size of this text will change based on a user's `ui_scale` setting. - XSmall, - - /// The `ui_font_size` set by the user. - Ui, - /// The `buffer_font_size` set by the user. - Editor, - // TODO: The terminal settings will need to be passed to - // ThemeSettings before we can enable this. - //// The `terminal.font_size` set by the user. - // Terminal, -} - -impl TextSize { - /// Returns the text size in rems. - pub fn rems(self, cx: &App) -> Rems { - let theme_settings = ThemeSettings::get_global(cx); - - match self { - Self::Large => rems_from_px(16.), - Self::Default => rems_from_px(14.), - Self::Small => rems_from_px(12.), - Self::XSmall => rems_from_px(10.), - Self::Ui => rems_from_px(theme_settings.ui_font_size(cx)), - Self::Editor => rems_from_px(theme_settings.buffer_font_size(cx)), - } - } - - pub fn pixels(self, cx: &App) -> Pixels { - let theme_settings = ThemeSettings::get_global(cx); - - match self { - Self::Large => px(16.), - Self::Default => px(14.), - Self::Small => px(12.), - Self::XSmall => px(10.), - Self::Ui => theme_settings.ui_font_size(cx), - Self::Editor => theme_settings.buffer_font_size(cx), - } - } -} - -/// The size of a [`Headline`] element -/// -/// Defaults to a Major Second scale. -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] -pub enum HeadlineSize { - /// An extra small headline - `~14px` @16px/rem - XSmall, - /// A small headline - `16px` @16px/rem - Small, - #[default] - /// A medium headline - `~18px` @16px/rem - Medium, - /// A large headline - `~20px` @16px/rem - Large, - /// An extra large headline - `~22px` @16px/rem - XLarge, -} - -impl HeadlineSize { - /// Returns the headline size in rems. - pub fn rems(self) -> Rems { - match self { - Self::XSmall => rems(0.88), - Self::Small => rems(1.0), - Self::Medium => rems(1.125), - Self::Large => rems(1.27), - Self::XLarge => rems(1.43), - } - } - - /// Returns the line height for the headline size. - pub fn line_height(self) -> Rems { - match self { - Self::XSmall => rems(1.6), - Self::Small => rems(1.6), - Self::Medium => rems(1.6), - Self::Large => rems(1.6), - Self::XLarge => rems(1.6), - } - } -} - -/// A headline element, used to emphasize some text and -/// create a visual hierarchy. -#[derive(IntoElement, RegisterComponent)] -pub struct Headline { - size: HeadlineSize, - text: SharedString, - color: Color, -} - -impl RenderOnce for Headline { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { - let ui_font = ThemeSettings::get_global(cx).ui_font.clone(); - - div() - .font(ui_font) - .line_height(self.size.line_height()) - .text_size(self.size.rems()) - .text_color(cx.theme().colors().text) - .child(self.text) - } -} - -impl Headline { - /// Create a new headline element. - pub fn new(text: impl Into) -> Self { - Self { - size: HeadlineSize::default(), - text: text.into(), - color: Color::default(), - } - } - - /// Set the size of the headline. - pub fn size(mut self, size: HeadlineSize) -> Self { - self.size = size; - self - } - - /// Set the color of the headline. - pub fn color(mut self, color: Color) -> Self { - self.color = color; - self - } -} - -impl Component for Headline { - fn scope() -> ComponentScope { - ComponentScope::Typography - } - - fn description() -> Option<&'static str> { - Some("A headline element used to emphasize text and create visual hierarchy in the UI.") - } - - fn preview(_window: &mut Window, _cx: &mut App) -> Option { - Some( - v_flex() - .gap_1() - .children(vec![ - single_example( - "XLarge", - Headline::new("XLarge Headline") - .size(HeadlineSize::XLarge) - .into_any_element(), - ), - single_example( - "Large", - Headline::new("Large Headline") - .size(HeadlineSize::Large) - .into_any_element(), - ), - single_example( - "Medium (Default)", - Headline::new("Medium Headline").into_any_element(), - ), - single_example( - "Small", - Headline::new("Small Headline") - .size(HeadlineSize::Small) - .into_any_element(), - ), - single_example( - "XSmall", - Headline::new("XSmall Headline") - .size(HeadlineSize::XSmall) - .into_any_element(), - ), - ]) - .into_any_element(), - ) - } -} diff --git a/crates/ui/src/styles/units.rs b/crates/ui/src/styles/units.rs deleted file mode 100644 index 3fa6520204..0000000000 --- a/crates/ui/src/styles/units.rs +++ /dev/null @@ -1,29 +0,0 @@ -use gpui::{Length, Rems, Window, rems}; - -/// The base size of a rem, in pixels. -pub const BASE_REM_SIZE_IN_PX: f32 = 16.; - -/// Returns a rem value derived from the provided pixel value and the base rem size (16px). -/// -/// This can be used to compute rem values relative to pixel sizes, without -/// needing to hard-code the rem value. -/// -/// For instance, instead of writing `rems(0.875)` you can write `rems_from_px(14.)` -#[inline(always)] -pub fn rems_from_px(px: impl Into) -> Rems { - rems(px.into() / BASE_REM_SIZE_IN_PX) -} - -/// Returns a [`Length`] corresponding to the specified percentage of the viewport's width. -/// -/// `percent` should be a value between `0.0` and `1.0`. -pub fn vw(percent: f32, window: &mut Window) -> Length { - Length::from(window.viewport_size().width * percent) -} - -/// Returns a [`Length`] corresponding to the specified percentage of the viewport's height. -/// -/// `percent` should be a value between `0.0` and `1.0`. -pub fn vh(percent: f32, window: &mut Window) -> Length { - Length::from(window.viewport_size().height * percent) -} diff --git a/crates/ui/src/traits.rs b/crates/ui/src/traits.rs deleted file mode 100644 index 9627f6d6ad..0000000000 --- a/crates/ui/src/traits.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub mod animation_ext; -pub mod clickable; -pub mod disableable; -pub mod fixed; -pub mod styled_ext; -pub mod toggleable; -pub mod transformable; -pub mod visible_on_hover; diff --git a/crates/ui/src/traits/animation_ext.rs b/crates/ui/src/traits/animation_ext.rs deleted file mode 100644 index 4907c71ff2..0000000000 --- a/crates/ui/src/traits/animation_ext.rs +++ /dev/null @@ -1,42 +0,0 @@ -use std::time::Duration; - -use gpui::{Animation, AnimationElement, AnimationExt, Transformation, percentage}; - -use crate::{prelude::*, traits::transformable::Transformable}; - -/// An extension trait for adding common animations to animatable components. -pub trait CommonAnimationExt: AnimationExt { - /// Render this component as rotating over the given duration. - /// - /// NOTE: This method uses the location of the caller to generate an ID for this state. - /// If this is not sufficient to identify your state (e.g. you're rendering a list item), - /// you can provide a custom ElementID using the `use_keyed_rotate_animation` method. - #[track_caller] - fn with_rotate_animation(self, duration: u64) -> AnimationElement - where - Self: Transformable + Sized, - { - self.with_keyed_rotate_animation( - ElementId::CodeLocation(*std::panic::Location::caller()), - duration, - ) - } - - /// Render this component as rotating with the given element ID over the given duration. - fn with_keyed_rotate_animation( - self, - id: impl Into, - duration: u64, - ) -> AnimationElement - where - Self: Transformable + Sized, - { - self.with_animation( - id, - Animation::new(Duration::from_secs(duration)).repeat(), - |component, delta| component.transform(Transformation::rotate(percentage(delta))), - ) - } -} - -impl CommonAnimationExt for T {} diff --git a/crates/ui/src/traits/clickable.rs b/crates/ui/src/traits/clickable.rs deleted file mode 100644 index 55a4986b20..0000000000 --- a/crates/ui/src/traits/clickable.rs +++ /dev/null @@ -1,9 +0,0 @@ -use gpui::{App, ClickEvent, CursorStyle, Window}; - -/// A trait for elements that can be clicked. Enables the use of the `on_click` method. -pub trait Clickable { - /// Sets the click handler that will fire whenever the element is clicked. - fn on_click(self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self; - /// Sets the cursor style when hovering over the element. - fn cursor_style(self, cursor_style: CursorStyle) -> Self; -} diff --git a/crates/ui/src/traits/disableable.rs b/crates/ui/src/traits/disableable.rs deleted file mode 100644 index 9f08ed8d12..0000000000 --- a/crates/ui/src/traits/disableable.rs +++ /dev/null @@ -1,5 +0,0 @@ -/// A trait for elements that can be disabled. Generally used to implement disabling an element's interactivity and changing its appearance to reflect that it is disabled. -pub trait Disableable { - /// Sets whether the element is disabled. - fn disabled(self, disabled: bool) -> Self; -} diff --git a/crates/ui/src/traits/fixed.rs b/crates/ui/src/traits/fixed.rs deleted file mode 100644 index 6ca9c8617f..0000000000 --- a/crates/ui/src/traits/fixed.rs +++ /dev/null @@ -1,10 +0,0 @@ -use gpui::DefiniteLength; - -/// A trait for elements that can have a fixed with. Enables the use of the `width` and `full_width` methods. -pub trait FixedWidth { - /// Sets the width of the element. - fn width(self, width: impl Into) -> Self; - - /// Sets the element's width to the full width of its container. - fn full_width(self) -> Self; -} diff --git a/crates/ui/src/traits/styled_ext.rs b/crates/ui/src/traits/styled_ext.rs deleted file mode 100644 index 849e56a024..0000000000 --- a/crates/ui/src/traits/styled_ext.rs +++ /dev/null @@ -1,134 +0,0 @@ -use gpui::{App, Styled, hsla}; - -use crate::ElevationIndex; -use crate::prelude::*; - -fn elevated(this: E, cx: &App, index: ElevationIndex) -> E { - this.bg(cx.theme().colors().elevated_surface_background) - .rounded_lg() - .border_1() - .border_color(cx.theme().colors().border_variant) - .shadow(index.shadow(cx)) -} - -fn elevated_borderless(this: E, cx: &mut App, index: ElevationIndex) -> E { - this.bg(cx.theme().colors().elevated_surface_background) - .rounded_lg() - .shadow(index.shadow(cx)) -} - -/// Extends [`gpui::Styled`] with Zed-specific styling methods. -// gate on rust-analyzer so rust-analyzer never needs to expand this macro, it takes up to 10 seconds to expand due to inefficiencies in rust-analyzers proc-macro srv -#[cfg_attr( - all(debug_assertions, not(rust_analyzer)), - gpui_macros::derive_inspector_reflection -)] -pub trait StyledExt: Styled + Sized { - /// Horizontally stacks elements. - /// - /// Sets `flex()`, `flex_row()`, `items_center()` - fn h_flex(self) -> Self { - self.flex().flex_row().items_center() - } - - /// Vertically stacks elements. - /// - /// Sets `flex()`, `flex_col()` - fn v_flex(self) -> Self { - self.flex().flex_col() - } - - /// The [`Surface`](ElevationIndex::Surface) elevation level, located above the app background, is the standard level for all elements - /// - /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()` - /// - /// Example Elements: Title Bar, Panel, Tab Bar, Editor - fn elevation_1(self, cx: &App) -> Self { - elevated(self, cx, ElevationIndex::Surface) - } - - /// See [`elevation_1`](Self::elevation_1). - /// - /// Renders a borderless version [`elevation_1`](Self::elevation_1). - fn elevation_1_borderless(self, cx: &mut App) -> Self { - elevated_borderless(self, cx, ElevationIndex::Surface) - } - - /// Non-Modal Elevated Surfaces appear above the [`Surface`](ElevationIndex::Surface) layer and is used for things that should appear above most UI elements like an editor or panel, but not elements like popovers, context menus, modals, etc. - /// - /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()` - /// - /// Examples: Notifications, Palettes, Detached/Floating Windows, Detached/Floating Panels - fn elevation_2(self, cx: &App) -> Self { - elevated(self, cx, ElevationIndex::ElevatedSurface) - } - - /// See [`elevation_2`](Self::elevation_2). - /// - /// Renders a borderless version [`elevation_2`](Self::elevation_2). - fn elevation_2_borderless(self, cx: &mut App) -> Self { - elevated_borderless(self, cx, ElevationIndex::ElevatedSurface) - } - - /// Modal Surfaces are used for elements that should appear above all other UI elements and are located above the wash layer. This is the maximum elevation at which UI elements can be rendered in their default state. - /// - /// Elements rendered at this layer should have an enforced behavior: Any interaction outside of the modal will either dismiss the modal or prompt an action (Save your progress, etc) then dismiss the modal. - /// - /// If the element does not have this behavior, it should be rendered at the [`Elevated Surface`](ElevationIndex::ElevatedSurface) layer. - /// - /// Sets `bg()`, `rounded_lg()`, `border()`, `border_color()`, `shadow()` - /// - /// Examples: Settings Modal, Channel Management, Wizards/Setup UI, Dialogs - fn elevation_3(self, cx: &App) -> Self { - elevated(self, cx, ElevationIndex::ModalSurface) - } - - /// See [`elevation_3`](Self::elevation_3). - /// - /// Renders a borderless version [`elevation_3`](Self::elevation_3). - fn elevation_3_borderless(self, cx: &mut App) -> Self { - elevated_borderless(self, cx, ElevationIndex::ModalSurface) - } - - /// The theme's primary border color. - fn border_primary(self, cx: &mut App) -> Self { - self.border_color(cx.theme().colors().border) - } - - /// The theme's secondary or muted border color. - fn border_muted(self, cx: &mut App) -> Self { - self.border_color(cx.theme().colors().border_variant) - } - - /// Sets the background color to red for debugging when building UI. - fn debug_bg_red(self) -> Self { - self.bg(hsla(0. / 360., 1., 0.5, 1.)) - } - - /// Sets the background color to green for debugging when building UI. - fn debug_bg_green(self) -> Self { - self.bg(hsla(120. / 360., 1., 0.5, 1.)) - } - - /// Sets the background color to blue for debugging when building UI. - fn debug_bg_blue(self) -> Self { - self.bg(hsla(240. / 360., 1., 0.5, 1.)) - } - - /// Sets the background color to yellow for debugging when building UI. - fn debug_bg_yellow(self) -> Self { - self.bg(hsla(60. / 360., 1., 0.5, 1.)) - } - - /// Sets the background color to cyan for debugging when building UI. - fn debug_bg_cyan(self) -> Self { - self.bg(hsla(160. / 360., 1., 0.5, 1.)) - } - - /// Sets the background color to magenta for debugging when building UI. - fn debug_bg_magenta(self) -> Self { - self.bg(hsla(300. / 360., 1., 0.5, 1.)) - } -} - -impl StyledExt for E {} diff --git a/crates/ui/src/traits/toggleable.rs b/crates/ui/src/traits/toggleable.rs deleted file mode 100644 index f731f9965e..0000000000 --- a/crates/ui/src/traits/toggleable.rs +++ /dev/null @@ -1,69 +0,0 @@ -/// A trait for elements that can be toggled. -/// -/// Implement this for elements that are visually distinct -/// when in two opposing states, like checkboxes or switches. -pub trait Toggleable { - /// Sets whether the element is selected. - fn toggle_state(self, selected: bool) -> Self; -} - -/// Represents the selection status of an element. -#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)] -pub enum ToggleState { - /// The element is not selected. - #[default] - Unselected, - /// The selection state of the element is indeterminate. - Indeterminate, - /// The element is selected. - Selected, -} - -impl ToggleState { - /// Returns the inverse of the current selection status. - /// - /// Indeterminate states become selected if inverted. - pub fn inverse(&self) -> Self { - match self { - Self::Unselected | Self::Indeterminate => Self::Selected, - Self::Selected => Self::Unselected, - } - } - - /// Creates a `ToggleState` from the given `any_checked` and `all_checked` flags. - pub fn from_any_and_all(any_checked: bool, all_checked: bool) -> Self { - match (any_checked, all_checked) { - (true, true) => Self::Selected, - (false, false) => Self::Unselected, - _ => Self::Indeterminate, - } - } - - /// Returns whether this toggle state is selected - pub fn selected(&self) -> bool { - match self { - ToggleState::Indeterminate | ToggleState::Unselected => false, - ToggleState::Selected => true, - } - } -} - -impl From for ToggleState { - fn from(selected: bool) -> Self { - if selected { - Self::Selected - } else { - Self::Unselected - } - } -} - -impl From> for ToggleState { - fn from(selected: Option) -> Self { - match selected { - Some(true) => Self::Selected, - Some(false) => Self::Unselected, - None => Self::Indeterminate, - } - } -} diff --git a/crates/ui/src/traits/transformable.rs b/crates/ui/src/traits/transformable.rs deleted file mode 100644 index f52141f304..0000000000 --- a/crates/ui/src/traits/transformable.rs +++ /dev/null @@ -1,7 +0,0 @@ -use gpui::Transformation; - -/// A trait for components that can be transformed. -pub trait Transformable { - /// Sets the transformation for the element. - fn transform(self, transformation: Transformation) -> Self; -} diff --git a/crates/ui/src/traits/visible_on_hover.rs b/crates/ui/src/traits/visible_on_hover.rs deleted file mode 100644 index fc0bb837d7..0000000000 --- a/crates/ui/src/traits/visible_on_hover.rs +++ /dev/null @@ -1,17 +0,0 @@ -use gpui::{InteractiveElement, SharedString, Styled}; - -/// A trait for elements that can be made visible on hover by -/// tracking a specific group. -pub trait VisibleOnHover { - /// Sets the element to only be visible when the specified group is hovered. - /// - /// Pass `""` as the `group_name` to use the global group. - fn visible_on_hover(self, group_name: impl Into) -> Self; -} - -impl VisibleOnHover for E { - fn visible_on_hover(self, group_name: impl Into) -> Self { - self.invisible() - .group_hover(group_name, |style| style.visible()) - } -} diff --git a/crates/ui/src/ui.rs b/crates/ui/src/ui.rs deleted file mode 100644 index 17e707f11b..0000000000 --- a/crates/ui/src/ui.rs +++ /dev/null @@ -1,20 +0,0 @@ -//! # UI – Zed UI Primitives & Components -//! -//! This crate provides a set of UI primitives and components that are used to build all of the elements in Zed's UI. -//! -//! ## Related Crates: -//! -//! - [`ui_macros`] - proc_macros support for this crate -//! - `ui_input` - the single line input component - -pub mod component_prelude; -mod components; -pub mod prelude; -mod styles; -mod traits; -pub mod utils; - -pub use components::*; -pub use prelude::*; -pub use styles::*; -pub use traits::animation_ext::*; diff --git a/crates/ui/src/utils.rs b/crates/ui/src/utils.rs deleted file mode 100644 index cd7d8eb497..0000000000 --- a/crates/ui/src/utils.rs +++ /dev/null @@ -1,23 +0,0 @@ -//! UI-related utilities - -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::*; -pub use search_input::*; -pub use with_rem_size::*; - -/// Returns true if the current theme is light or vibrant light. -pub fn is_light(cx: &mut App) -> bool { - cx.theme().appearance.is_light() -} diff --git a/crates/ui/src/utils/apca_contrast.rs b/crates/ui/src/utils/apca_contrast.rs deleted file mode 100644 index 341b44670d..0000000000 --- a/crates/ui/src/utils/apca_contrast.rs +++ /dev/null @@ -1,479 +0,0 @@ -use gpui::Hsla; - -/// APCA (Accessible Perceptual Contrast Algorithm) constants -/// Based on APCA 0.0.98G-4g W3 compatible constants -/// https://github.com/Myndex/apca-w3 -struct APCAConstants { - // Main TRC exponent for monitor perception - main_trc: f32, - - // sRGB coefficients - s_rco: f32, - s_gco: f32, - s_bco: f32, - - // G-4g constants for use with 2.4 exponent - norm_bg: f32, - norm_txt: f32, - rev_txt: f32, - rev_bg: f32, - - // G-4g Clamps and Scalers - blk_thrs: f32, - blk_clmp: f32, - scale_bow: f32, - scale_wob: f32, - lo_bow_offset: f32, - lo_wob_offset: f32, - delta_y_min: f32, - lo_clip: f32, -} - -impl Default for APCAConstants { - fn default() -> Self { - Self { - main_trc: 2.4, - s_rco: 0.2126729, - s_gco: 0.7151522, - s_bco: 0.0721750, - norm_bg: 0.56, - norm_txt: 0.57, - rev_txt: 0.62, - rev_bg: 0.65, - blk_thrs: 0.022, - blk_clmp: 1.414, - scale_bow: 1.14, - scale_wob: 1.14, - lo_bow_offset: 0.027, - lo_wob_offset: 0.027, - delta_y_min: 0.0005, - lo_clip: 0.1, - } - } -} - -/// Calculates the perceptual lightness contrast using APCA. -/// Returns a value between approximately -108 and 106. -/// Negative values indicate light text on dark background. -/// Positive values indicate dark text on light background. -/// -/// The APCA algorithm is more perceptually accurate than WCAG 2.x, -/// especially for dark mode interfaces. Key improvements include: -/// - Better accuracy for dark backgrounds -/// - Polarity-aware (direction matters) -/// - Perceptually uniform across the range -/// -/// Common APCA Lc thresholds per ARC Bronze Simple Mode: -/// https://readtech.org/ARC/tests/bronze-simple-mode/ -/// - Lc 45: Minimum for large fluent text (36px+) -/// - Lc 60: Minimum for other content text -/// - Lc 75: Minimum for body text -/// - Lc 90: Preferred for body text -/// -/// Most terminal themes use colors with APCA values of 40-70. -/// -/// https://github.com/Myndex/apca-w3 -pub fn apca_contrast(text_color: Hsla, background_color: Hsla) -> f32 { - let constants = APCAConstants::default(); - - let text_y = srgb_to_y(text_color, &constants); - let bg_y = srgb_to_y(background_color, &constants); - - // Apply soft clamp to near-black colors - let text_y_clamped = if text_y > constants.blk_thrs { - text_y - } else { - text_y + (constants.blk_thrs - text_y).powf(constants.blk_clmp) - }; - - let bg_y_clamped = if bg_y > constants.blk_thrs { - bg_y - } else { - bg_y + (constants.blk_thrs - bg_y).powf(constants.blk_clmp) - }; - - // Return 0 for extremely low delta Y - if (bg_y_clamped - text_y_clamped).abs() < constants.delta_y_min { - return 0.0; - } - - let sapc; - let output_contrast; - - if bg_y_clamped > text_y_clamped { - // Normal polarity: dark text on light background - sapc = (bg_y_clamped.powf(constants.norm_bg) - text_y_clamped.powf(constants.norm_txt)) - * constants.scale_bow; - - // Low contrast smooth rollout to prevent polarity reversal - output_contrast = if sapc < constants.lo_clip { - 0.0 - } else { - sapc - constants.lo_bow_offset - }; - } else { - // Reverse polarity: light text on dark background - sapc = (bg_y_clamped.powf(constants.rev_bg) - text_y_clamped.powf(constants.rev_txt)) - * constants.scale_wob; - - output_contrast = if sapc > -constants.lo_clip { - 0.0 - } else { - sapc + constants.lo_wob_offset - }; - } - - // Return Lc (lightness contrast) scaled to percentage - output_contrast * 100.0 -} - -/// Converts sRGB color to Y (luminance) for APCA calculation -fn srgb_to_y(color: Hsla, constants: &APCAConstants) -> f32 { - let rgba = color.to_rgb(); - - // Linearize and apply coefficients - let r_linear = (rgba.r).powf(constants.main_trc); - let g_linear = (rgba.g).powf(constants.main_trc); - let b_linear = (rgba.b).powf(constants.main_trc); - - constants.s_rco * r_linear + constants.s_gco * g_linear + constants.s_bco * b_linear -} - -/// Adjusts the foreground color to meet the minimum APCA contrast against the background. -/// The minimum_apca_contrast should be an absolute value (e.g., 75 for Lc 75). -/// -/// This implementation gradually adjusts the lightness while preserving the hue and -/// saturation as much as possible, only falling back to black/white when necessary. -pub fn ensure_minimum_contrast( - foreground: Hsla, - background: Hsla, - minimum_apca_contrast: f32, -) -> Hsla { - if minimum_apca_contrast <= 0.0 { - return foreground; - } - - let current_contrast = apca_contrast(foreground, background).abs(); - - if current_contrast >= minimum_apca_contrast { - return foreground; - } - - // First, try to adjust lightness while preserving hue and saturation - let adjusted = adjust_lightness_for_contrast(foreground, background, minimum_apca_contrast); - - let adjusted_contrast = apca_contrast(adjusted, background).abs(); - if adjusted_contrast >= minimum_apca_contrast { - return adjusted; - } - - // If that's not enough, gradually reduce saturation while adjusting lightness - let desaturated = - adjust_lightness_and_saturation_for_contrast(foreground, background, minimum_apca_contrast); - - let desaturated_contrast = apca_contrast(desaturated, background).abs(); - if desaturated_contrast >= minimum_apca_contrast { - return desaturated; - } - - // Last resort: use black or white - let black = Hsla { - h: 0.0, - s: 0.0, - l: 0.0, - a: foreground.a, - }; - - let white = Hsla { - h: 0.0, - s: 0.0, - l: 1.0, - a: foreground.a, - }; - - let black_contrast = apca_contrast(black, background).abs(); - let white_contrast = apca_contrast(white, background).abs(); - - if white_contrast > black_contrast { - white - } else { - black - } -} - -/// Adjusts only the lightness to meet the minimum contrast, preserving hue and saturation -fn adjust_lightness_for_contrast( - foreground: Hsla, - background: Hsla, - minimum_apca_contrast: f32, -) -> Hsla { - // Determine if we need to go lighter or darker - let bg_luminance = srgb_to_y(background, &APCAConstants::default()); - let should_go_darker = bg_luminance > 0.5; - - // Binary search for the optimal lightness - let mut low = if should_go_darker { 0.0 } else { foreground.l }; - let mut high = if should_go_darker { foreground.l } else { 1.0 }; - let mut best_l = foreground.l; - - for _ in 0..20 { - let mid = (low + high) / 2.0; - let test_color = Hsla { - h: foreground.h, - s: foreground.s, - l: mid, - a: foreground.a, - }; - - let contrast = apca_contrast(test_color, background).abs(); - - if contrast >= minimum_apca_contrast { - best_l = mid; - // Try to get closer to the minimum - if should_go_darker { - low = mid; - } else { - high = mid; - } - } else if should_go_darker { - high = mid; - } else { - low = mid; - } - - // If we're close enough to the target, stop - if (contrast - minimum_apca_contrast).abs() < 1.0 { - best_l = mid; - break; - } - } - - Hsla { - h: foreground.h, - s: foreground.s, - l: best_l, - a: foreground.a, - } -} - -/// Adjusts both lightness and saturation to meet the minimum contrast -fn adjust_lightness_and_saturation_for_contrast( - foreground: Hsla, - background: Hsla, - minimum_apca_contrast: f32, -) -> Hsla { - // Try different saturation levels - let saturation_steps = [1.0, 0.8, 0.6, 0.4, 0.2, 0.0]; - - for &sat_multiplier in &saturation_steps { - let test_color = Hsla { - h: foreground.h, - s: foreground.s * sat_multiplier, - l: foreground.l, - a: foreground.a, - }; - - let adjusted = adjust_lightness_for_contrast(test_color, background, minimum_apca_contrast); - let contrast = apca_contrast(adjusted, background).abs(); - - if contrast >= minimum_apca_contrast { - return adjusted; - } - } - - // If we get here, even grayscale didn't work, so return the grayscale attempt - Hsla { - h: foreground.h, - s: 0.0, - l: foreground.l, - a: foreground.a, - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla { - Hsla { h, s, l, a } - } - - fn hsla_from_hex(hex: u32) -> Hsla { - let r = ((hex >> 16) & 0xFF) as f32 / 255.0; - let g = ((hex >> 8) & 0xFF) as f32 / 255.0; - let b = (hex & 0xFF) as f32 / 255.0; - - let max = r.max(g).max(b); - let min = r.min(g).min(b); - let l = (max + min) / 2.0; - - if max == min { - // Achromatic - Hsla { - h: 0.0, - s: 0.0, - l, - a: 1.0, - } - } else { - let d = max - min; - let s = if l > 0.5 { - d / (2.0 - max - min) - } else { - d / (max + min) - }; - - let h = if max == r { - (g - b) / d + if g < b { 6.0 } else { 0.0 } - } else if max == g { - (b - r) / d + 2.0 - } else { - (r - g) / d + 4.0 - } / 6.0; - - Hsla { h, s, l, a: 1.0 } - } - } - - #[test] - fn test_apca_contrast() { - // Test black text on white background (should be positive) - let black = hsla(0.0, 0.0, 0.0, 1.0); - let white = hsla(0.0, 0.0, 1.0, 1.0); - let contrast = apca_contrast(black, white); - assert!( - contrast > 100.0, - "Black on white should have high positive contrast, got {}", - contrast - ); - - // Test white text on black background (should be negative) - let contrast_reversed = apca_contrast(white, black); - assert!( - contrast_reversed < -100.0, - "White on black should have high negative contrast, got {}", - contrast_reversed - ); - - // Same color should have zero contrast - let gray = hsla(0.0, 0.0, 0.5, 1.0); - let contrast_same = apca_contrast(gray, gray); - assert!( - contrast_same.abs() < 1.0, - "Same color should have near-zero contrast, got {}", - contrast_same - ); - - // APCA is NOT commutative - polarity matters - assert!( - (contrast + contrast_reversed).abs() > 1.0, - "APCA should not be commutative" - ); - } - - #[test] - fn test_srgb_to_y() { - let constants = APCAConstants::default(); - - // Test known Y values - let black = hsla(0.0, 0.0, 0.0, 1.0); - let y_black = srgb_to_y(black, &constants); - assert!( - y_black.abs() < 0.001, - "Black should have Y near 0, got {}", - y_black - ); - - let white = hsla(0.0, 0.0, 1.0, 1.0); - let y_white = srgb_to_y(white, &constants); - assert!( - (y_white - 1.0).abs() < 0.001, - "White should have Y near 1, got {}", - y_white - ); - } - - #[test] - fn test_srgb_to_y_nan_issue() { - let dark_red = hsla_from_hex(0x5f0000); - let y_dark_red = srgb_to_y(dark_red, &APCAConstants::default()); - assert!(!y_dark_red.is_nan()); - } - - #[test] - fn test_ensure_minimum_contrast() { - let white_bg = hsla(0.0, 0.0, 1.0, 1.0); - let light_gray = hsla(0.0, 0.0, 0.9, 1.0); - - // Light gray on white has poor contrast - let initial_contrast = apca_contrast(light_gray, white_bg).abs(); - assert!( - initial_contrast < 15.0, - "Initial contrast should be low, got {}", - initial_contrast - ); - - // Should be adjusted to black for better contrast (using APCA Lc 45 as minimum) - let adjusted = ensure_minimum_contrast(light_gray, white_bg, 45.0); - assert_eq!(adjusted.l, 0.0); // Should be black - assert_eq!(adjusted.a, light_gray.a); // Alpha preserved - - // Test with dark background - let black_bg = hsla(0.0, 0.0, 0.0, 1.0); - let dark_gray = hsla(0.0, 0.0, 0.1, 1.0); - - // Dark gray on black has poor contrast - let initial_contrast = apca_contrast(dark_gray, black_bg).abs(); - assert!( - initial_contrast < 15.0, - "Initial contrast should be low, got {}", - initial_contrast - ); - - // Should be adjusted to white for better contrast - let adjusted = ensure_minimum_contrast(dark_gray, black_bg, 45.0); - assert_eq!(adjusted.l, 1.0); // Should be white - - // Test when contrast is already sufficient - let black = hsla(0.0, 0.0, 0.0, 1.0); - let adjusted = ensure_minimum_contrast(black, white_bg, 45.0); - assert_eq!(adjusted, black); // Should remain unchanged - } - - #[test] - fn test_one_light_theme_exact_colors() { - // Test with exact colors from One Light theme - // terminal.background and terminal.ansi.white are both #fafafaff - let fafafa = hsla_from_hex(0xfafafa); - - // They should be identical - let bg = fafafa; - let fg = fafafa; - - // Contrast should be 0 (no contrast) - let contrast = apca_contrast(fg, bg); - assert!( - contrast.abs() < 1.0, - "Same color should have near-zero APCA contrast, got {}", - contrast - ); - - // With minimum APCA contrast of 15 (very low, but detectable), it should adjust - let adjusted = ensure_minimum_contrast(fg, bg, 15.0); - // The new algorithm preserves colors, so we just need to check contrast - let new_contrast = apca_contrast(adjusted, bg).abs(); - assert!( - new_contrast >= 15.0, - "Adjusted contrast {} should be >= 15.0", - new_contrast - ); - - // The adjusted color should have sufficient contrast - let new_contrast = apca_contrast(adjusted, bg).abs(); - assert!( - new_contrast >= 15.0, - "Adjusted APCA contrast {} should be >= 15.0", - new_contrast - ); - } -} diff --git a/crates/ui/src/utils/color_contrast.rs b/crates/ui/src/utils/color_contrast.rs deleted file mode 100644 index 2a6b4bf281..0000000000 --- a/crates/ui/src/utils/color_contrast.rs +++ /dev/null @@ -1,70 +0,0 @@ -use gpui::{Hsla, Rgba}; - -/// Calculates the contrast ratio between two colors according to WCAG 2.0 standards. -/// -/// The formula used is: -/// (L1 + 0.05) / (L2 + 0.05), where L1 is the lighter of the two luminances and L2 is the darker. -/// -/// Returns a float representing the contrast ratio. A higher value indicates more contrast. -/// The range of the returned value is 1 to 21 (commonly written as 1:1 to 21:1). -pub fn calculate_contrast_ratio(fg: Hsla, bg: Hsla) -> f32 { - let l1 = relative_luminance(fg); - let l2 = relative_luminance(bg); - - let (lighter, darker) = if l1 > l2 { (l1, l2) } else { (l2, l1) }; - - (lighter + 0.05) / (darker + 0.05) -} - -/// Calculates the relative luminance of a color. -/// -/// The relative luminance is the relative brightness of any point in a colorspace, -/// normalized to 0 for darkest black and 1 for lightest white. -fn relative_luminance(color: Hsla) -> f32 { - let rgba: Rgba = color.into(); - let r = linearize(rgba.r); - let g = linearize(rgba.g); - let b = linearize(rgba.b); - - 0.2126 * r + 0.7152 * g + 0.0722 * b -} - -/// Linearizes an RGB component. -fn linearize(component: f32) -> f32 { - if component <= 0.03928 { - component / 12.92 - } else { - ((component + 0.055) / 1.055).powf(2.4) - } -} - -#[cfg(test)] -mod tests { - use gpui::hsla; - - use super::*; - - // Test the contrast ratio formula with some common color combinations to - // prevent regressions in either the color conversions or the formula itself. - #[test] - fn test_contrast_ratio_formula() { - // White on Black (should be close to 21:1) - let white = hsla(0.0, 0.0, 1.0, 1.0); - let black = hsla(0.0, 0.0, 0.0, 1.0); - assert!((calculate_contrast_ratio(white, black) - 21.0).abs() < 0.1); - - // Black on White (should be close to 21:1) - assert!((calculate_contrast_ratio(black, white) - 21.0).abs() < 0.1); - - // Mid-gray on Black (should be close to 5.32:1) - let mid_gray = hsla(0.0, 0.0, 0.5, 1.0); - assert!((calculate_contrast_ratio(mid_gray, black) - 5.32).abs() < 0.1); - - // White on Mid-gray (should be close to 3.95:1) - assert!((calculate_contrast_ratio(white, mid_gray) - 3.95).abs() < 0.1); - - // Same color (should be 1:1) - let red = hsla(0.0, 1.0, 0.5, 1.0); - assert!((calculate_contrast_ratio(red, red) - 1.0).abs() < 0.01); - } -} diff --git a/crates/ui/src/utils/corner_solver.rs b/crates/ui/src/utils/corner_solver.rs deleted file mode 100644 index c49bccc445..0000000000 --- a/crates/ui/src/utils/corner_solver.rs +++ /dev/null @@ -1,61 +0,0 @@ -use gpui::Pixels; - -/// Calculates the child’s content-corner radius for a single nested level. -/// -/// child_content_radius = max(0, parent_radius - parent_border - parent_padding + self_border) -/// -/// - parent_radius: outer corner radius of the parent element -/// - parent_border: border width of the parent element -/// - parent_padding: padding of the parent element -/// - self_border: border width of this child element (for content inset) -pub fn inner_corner_radius( - parent_radius: Pixels, - parent_border: Pixels, - parent_padding: Pixels, - self_border: Pixels, -) -> Pixels { - (parent_radius - parent_border - parent_padding + self_border).max(Pixels::ZERO) -} - -/// Solver for arbitrarily deep nested corner radii. -/// -/// Each nested level’s outer border-box radius is: -/// R₀ = max(0, root_radius - root_border - root_padding) -/// Rᵢ = max(0, Rᵢ₋₁ - childᵢ₋₁_border - childᵢ₋₁_padding) for i > 0 -pub struct CornerSolver { - root_radius: Pixels, - root_border: Pixels, - root_padding: Pixels, - children: Vec<(Pixels, Pixels)>, // (border, padding) -} - -impl CornerSolver { - pub fn new(root_radius: Pixels, root_border: Pixels, root_padding: Pixels) -> Self { - Self { - root_radius, - root_border, - root_padding, - children: Vec::new(), - } - } - - pub fn add_child(mut self, border: Pixels, padding: Pixels) -> Self { - self.children.push((border, padding)); - self - } - - pub fn corner_radius(&self, level: usize) -> Pixels { - if level == 0 { - return (self.root_radius - self.root_border - self.root_padding).max(Pixels::ZERO); - } - if level >= self.children.len() { - return Pixels::ZERO; - } - let mut r = (self.root_radius - self.root_border - self.root_padding).max(Pixels::ZERO); - for i in 0..level { - let (b, p) = self.children[i]; - r = (r - b - p).max(Pixels::ZERO); - } - r - } -} diff --git a/crates/ui/src/utils/format_distance.rs b/crates/ui/src/utils/format_distance.rs deleted file mode 100644 index 6ec497edee..0000000000 --- a/crates/ui/src/utils/format_distance.rs +++ /dev/null @@ -1,396 +0,0 @@ -// This won't be documented further as it is intended to be removed, or merged with the `time_format` crate. - -use chrono::{DateTime, Local, NaiveDateTime}; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum DateTimeType { - Naive(NaiveDateTime), - Local(DateTime), -} - -impl DateTimeType { - /// Converts the [`DateTimeType`] to a [`NaiveDateTime`]. - /// - /// If the [`DateTimeType`] is already a [`NaiveDateTime`], it will be returned as is. - /// If the [`DateTimeType`] is a [`DateTime`], it will be converted to a [`NaiveDateTime`]. - pub fn to_naive(self) -> NaiveDateTime { - match self { - DateTimeType::Naive(naive) => naive, - DateTimeType::Local(local) => local.naive_local(), - } - } -} - -pub struct FormatDistance { - date: DateTimeType, - base_date: DateTimeType, - include_seconds: bool, - add_suffix: bool, - hide_prefix: bool, -} - -impl FormatDistance { - pub fn new(date: DateTimeType, base_date: DateTimeType) -> Self { - Self { - date, - base_date, - include_seconds: false, - add_suffix: false, - hide_prefix: false, - } - } - - pub fn from_now(date: DateTimeType) -> Self { - Self::new(date, DateTimeType::Local(Local::now())) - } - - pub fn include_seconds(mut self, include_seconds: bool) -> Self { - self.include_seconds = include_seconds; - self - } - - pub fn add_suffix(mut self, add_suffix: bool) -> Self { - self.add_suffix = add_suffix; - self - } - - pub fn hide_prefix(mut self, hide_prefix: bool) -> Self { - self.hide_prefix = hide_prefix; - self - } -} - -impl std::fmt::Display for FormatDistance { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - format_distance( - self.date, - self.base_date.to_naive(), - self.include_seconds, - self.add_suffix, - self.hide_prefix, - ) - ) - } -} -/// Calculates the distance in seconds between two [`NaiveDateTime`] objects. -/// It returns a signed integer denoting the difference. If `date` is earlier than `base_date`, the returned value will be negative. -/// -/// ## Arguments -/// -/// * `date` - A [NaiveDateTime`] object representing the date of interest -/// * `base_date` - A [NaiveDateTime`] object representing the base date against which the comparison is made -fn distance_in_seconds(date: NaiveDateTime, base_date: NaiveDateTime) -> i64 { - let duration = date.signed_duration_since(base_date); - -duration.num_seconds() -} - -/// Generates a string describing the time distance between two dates in a human-readable way. -fn distance_string( - distance: i64, - include_seconds: bool, - add_suffix: bool, - hide_prefix: bool, -) -> String { - let suffix = if distance < 0 { " from now" } else { " ago" }; - - let distance = distance.abs(); - - let minutes = distance / 60; - let hours = distance / 3_600; - let days = distance / 86_400; - let months = distance / 2_592_000; - - let string = if distance < 5 && include_seconds { - if hide_prefix { - "5 seconds" - } else { - "less than 5 seconds" - } - .to_string() - } else if distance < 10 && include_seconds { - if hide_prefix { - "10 seconds" - } else { - "less than 10 seconds" - } - .to_string() - } else if distance < 20 && include_seconds { - if hide_prefix { - "20 seconds" - } else { - "less than 20 seconds" - } - .to_string() - } else if distance < 40 && include_seconds { - "half a minute".to_string() - } else if distance < 60 && include_seconds { - if hide_prefix { - "a minute" - } else { - "less than a minute" - } - .to_string() - } else if distance < 90 && include_seconds { - "1 minute".to_string() - } else if distance < 30 { - if hide_prefix { - "a minute" - } else { - "less than a minute" - } - .to_string() - } else if distance < 90 { - "1 minute".to_string() - } else if distance < 2_700 { - format!("{} minutes", minutes) - } else if distance < 5_400 { - if hide_prefix { - "1 hour" - } else { - "about 1 hour" - } - .to_string() - } else if distance < 86_400 { - if hide_prefix { - format!("{} hours", hours) - } else { - format!("about {} hours", hours) - } - } else if distance < 172_800 { - "1 day".to_string() - } else if distance < 2_592_000 { - format!("{} days", days) - } else if distance < 5_184_000 { - if hide_prefix { - "1 month" - } else { - "about 1 month" - } - .to_string() - } else if distance < 7_776_000 { - if hide_prefix { - "2 months" - } else { - "about 2 months" - } - .to_string() - } else if distance < 31_540_000 { - format!("{} months", months) - } else if distance < 39_425_000 { - if hide_prefix { - "1 year" - } else { - "about 1 year" - } - .to_string() - } else if distance < 55_195_000 { - if hide_prefix { "1 year" } else { "over 1 year" }.to_string() - } else if distance < 63_080_000 { - if hide_prefix { - "2 years" - } else { - "almost 2 years" - } - .to_string() - } else { - let years = distance / 31_536_000; - let remaining_months = (distance % 31_536_000) / 2_592_000; - - if remaining_months < 3 { - if hide_prefix { - format!("{} years", years) - } else { - format!("about {} years", years) - } - } else if remaining_months < 9 { - if hide_prefix { - format!("{} years", years) - } else { - format!("over {} years", years) - } - } else if hide_prefix { - format!("{} years", years + 1) - } else { - format!("almost {} years", years + 1) - } - }; - - if add_suffix { - format!("{}{}", string, suffix) - } else { - string - } -} - -/// Get the time difference between two dates into a relative human readable string. -/// -/// For example, "less than a minute ago", "about 2 hours ago", "3 months from now", etc. -/// -/// Use [`format_distance_from_now`] to compare a NaiveDateTime against now. -pub fn format_distance( - date: DateTimeType, - base_date: NaiveDateTime, - include_seconds: bool, - add_suffix: bool, - hide_prefix: bool, -) -> String { - let distance = distance_in_seconds(date.to_naive(), base_date); - - distance_string(distance, include_seconds, add_suffix, hide_prefix) -} - -/// Get the time difference between a date and now as relative human readable string. -/// -/// For example, "less than a minute ago", "about 2 hours ago", "3 months from now", etc. -pub fn format_distance_from_now( - datetime: DateTimeType, - include_seconds: bool, - add_suffix: bool, - hide_prefix: bool, -) -> String { - let now = chrono::offset::Local::now().naive_local(); - - format_distance(datetime, now, include_seconds, add_suffix, hide_prefix) -} - -#[cfg(test)] -mod tests { - use super::*; - use chrono::NaiveDateTime; - - #[test] - fn test_format_distance() { - let date = DateTimeType::Naive( - #[allow(deprecated)] - NaiveDateTime::from_timestamp_opt(9600, 0).expect("Invalid NaiveDateTime for date"), - ); - let base_date = DateTimeType::Naive( - #[allow(deprecated)] - NaiveDateTime::from_timestamp_opt(0, 0).expect("Invalid NaiveDateTime for base_date"), - ); - - assert_eq!( - "about 2 hours", - format_distance(date, base_date.to_naive(), false, false, false) - ); - } - - #[test] - fn test_format_distance_with_suffix() { - let date = DateTimeType::Naive( - #[allow(deprecated)] - NaiveDateTime::from_timestamp_opt(9600, 0).expect("Invalid NaiveDateTime for date"), - ); - let base_date = DateTimeType::Naive( - #[allow(deprecated)] - NaiveDateTime::from_timestamp_opt(0, 0).expect("Invalid NaiveDateTime for base_date"), - ); - - assert_eq!( - "about 2 hours from now", - format_distance(date, base_date.to_naive(), false, true, false) - ); - } - - #[test] - fn test_format_distance_from_hms() { - let date = DateTimeType::Naive( - NaiveDateTime::parse_from_str("1969-07-20T11:22:33Z", "%Y-%m-%dT%H:%M:%SZ") - .expect("Invalid NaiveDateTime for date"), - ); - let base_date = DateTimeType::Naive( - NaiveDateTime::parse_from_str("2024-02-01T00:00:00Z", "%Y-%m-%dT%H:%M:%SZ") - .expect("Invalid NaiveDateTime for base_date"), - ); - - assert_eq!( - "over 54 years ago", - format_distance(date, base_date.to_naive(), false, true, false) - ); - } - - #[test] - fn test_format_distance_string() { - assert_eq!( - distance_string(3, false, false, false), - "less than a minute" - ); - assert_eq!( - distance_string(7, false, false, false), - "less than a minute" - ); - assert_eq!( - distance_string(13, false, false, false), - "less than a minute" - ); - assert_eq!( - distance_string(21, false, false, false), - "less than a minute" - ); - assert_eq!(distance_string(45, false, false, false), "1 minute"); - assert_eq!(distance_string(61, false, false, false), "1 minute"); - assert_eq!(distance_string(1920, false, false, false), "32 minutes"); - assert_eq!(distance_string(3902, false, false, false), "about 1 hour"); - assert_eq!(distance_string(18002, false, false, false), "about 5 hours"); - assert_eq!(distance_string(86470, false, false, false), "1 day"); - assert_eq!(distance_string(345880, false, false, false), "4 days"); - assert_eq!( - distance_string(2764800, false, false, false), - "about 1 month" - ); - assert_eq!( - distance_string(5184000, false, false, false), - "about 2 months" - ); - assert_eq!(distance_string(10368000, false, false, false), "4 months"); - assert_eq!( - distance_string(34694000, false, false, false), - "about 1 year" - ); - assert_eq!( - distance_string(47310000, false, false, false), - "over 1 year" - ); - assert_eq!( - distance_string(61503000, false, false, false), - "almost 2 years" - ); - assert_eq!( - distance_string(160854000, false, false, false), - "about 5 years" - ); - assert_eq!( - distance_string(236550000, false, false, false), - "over 7 years" - ); - assert_eq!( - distance_string(249166000, false, false, false), - "almost 8 years" - ); - } - - #[test] - fn test_format_distance_string_include_seconds() { - assert_eq!( - distance_string(3, true, false, false), - "less than 5 seconds" - ); - assert_eq!( - distance_string(7, true, false, false), - "less than 10 seconds" - ); - assert_eq!( - distance_string(13, true, false, false), - "less than 20 seconds" - ); - assert_eq!(distance_string(21, true, false, false), "half a minute"); - assert_eq!( - distance_string(45, true, false, false), - "less than a minute" - ); - assert_eq!(distance_string(61, true, false, false), "1 minute"); - } -} diff --git a/crates/ui/src/utils/search_input.rs b/crates/ui/src/utils/search_input.rs deleted file mode 100644 index c677b203d5..0000000000 --- a/crates/ui/src/utils/search_input.rs +++ /dev/null @@ -1,20 +0,0 @@ -use gpui::{Pixels, px}; - -pub struct SearchInputWidth; - -impl SearchInputWidth { - /// The container size in which the input stops filling the whole width. - pub const THRESHOLD_WIDTH: Pixels = px(1200.0); - - /// The maximum width for the search input when the container is larger than the threshold. - pub const MAX_WIDTH: Pixels = px(1200.0); - - /// Calculates the actual width in pixels based on the container width. - pub fn calc_width(container_width: Pixels) -> Pixels { - if container_width < Self::THRESHOLD_WIDTH { - container_width - } else { - container_width.min(Self::MAX_WIDTH) - } - } -} diff --git a/crates/ui/src/utils/with_rem_size.rs b/crates/ui/src/utils/with_rem_size.rs deleted file mode 100644 index b9770b086c..0000000000 --- a/crates/ui/src/utils/with_rem_size.rs +++ /dev/null @@ -1,114 +0,0 @@ -use gpui::{ - AnyElement, App, Bounds, Div, DivFrameState, Element, ElementId, GlobalElementId, Hitbox, - InteractiveElement as _, IntoElement, LayoutId, ParentElement, Pixels, StyleRefinement, Styled, - Window, div, -}; - -/// An element that sets a particular rem size for its children. -pub struct WithRemSize { - div: Div, - rem_size: Pixels, -} - -impl WithRemSize { - /// Create a new [WithRemSize] element, which sets a - /// particular rem size for its children. - pub fn new(rem_size: impl Into) -> Self { - Self { - div: div(), - rem_size: rem_size.into(), - } - } - - /// Block the mouse from interacting with this element or any of its children - /// The fluent API equivalent to [`Interactivity::occlude_mouse`] - /// - /// [`Interactivity::occlude_mouse`]: gpui::Interactivity::occlude_mouse - pub fn occlude(mut self) -> Self { - self.div = self.div.occlude(); - self - } -} - -impl Styled for WithRemSize { - fn style(&mut self) -> &mut StyleRefinement { - self.div.style() - } -} - -impl ParentElement for WithRemSize { - fn extend(&mut self, elements: impl IntoIterator) { - self.div.extend(elements) - } -} - -impl Element for WithRemSize { - type RequestLayoutState = DivFrameState; - type PrepaintState = Option; - - fn id(&self) -> Option { - Element::id(&self.div) - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - Element::source_location(&self.div) - } - - fn request_layout( - &mut self, - id: Option<&GlobalElementId>, - inspector_id: Option<&gpui::InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (LayoutId, Self::RequestLayoutState) { - window.with_rem_size(Some(self.rem_size), |window| { - self.div.request_layout(id, inspector_id, window, cx) - }) - } - - fn prepaint( - &mut self, - id: Option<&GlobalElementId>, - inspector_id: Option<&gpui::InspectorElementId>, - bounds: Bounds, - request_layout: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> Self::PrepaintState { - window.with_rem_size(Some(self.rem_size), |window| { - self.div - .prepaint(id, inspector_id, bounds, request_layout, window, cx) - }) - } - - fn paint( - &mut self, - id: Option<&GlobalElementId>, - inspector_id: Option<&gpui::InspectorElementId>, - bounds: Bounds, - request_layout: &mut Self::RequestLayoutState, - prepaint: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - window.with_rem_size(Some(self.rem_size), |window| { - self.div.paint( - id, - inspector_id, - bounds, - request_layout, - prepaint, - window, - cx, - ) - }) - } -} - -impl IntoElement for WithRemSize { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } -} diff --git a/crates/ui_input/Cargo.toml b/crates/ui_input/Cargo.toml deleted file mode 100644 index 4e7b08241d..0000000000 --- a/crates/ui_input/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "ui_input" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/ui_input.rs" - -[dependencies] -component.workspace = true -editor.workspace = true -gpui.workspace = true -menu.workspace = true -settings.workspace = true -theme.workspace = true -ui.workspace = true - -[features] -default = [] diff --git a/crates/ui_input/LICENSE-GPL b/crates/ui_input/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/ui_input/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/ui_input/src/input_field.rs b/crates/ui_input/src/input_field.rs deleted file mode 100644 index 2bae8c172d..0000000000 --- a/crates/ui_input/src/input_field.rs +++ /dev/null @@ -1,259 +0,0 @@ -use component::{example_group, single_example}; -use editor::{Editor, EditorElement, EditorStyle}; -use gpui::{App, Entity, FocusHandle, Focusable, FontStyle, Hsla, Length, TextStyle}; -use settings::Settings; -use std::sync::Arc; -use theme::ThemeSettings; -use ui::prelude::*; - -pub struct InputFieldStyle { - text_color: Hsla, - background_color: Hsla, - border_color: Hsla, -} - -/// An Input Field component that can be used to create text fields like search inputs, form fields, etc. -/// -/// It wraps a single line [`Editor`] and allows for common field properties like labels, placeholders, icons, etc. -#[derive(RegisterComponent)] -pub struct InputField { - /// An optional label for the text field. - /// - /// Its position is determined by the [`FieldLabelLayout`]. - label: Option, - /// The size of the label text. - label_size: LabelSize, - /// The placeholder text for the text field. - placeholder: SharedString, - /// Exposes the underlying [`Entity`] to allow for customizing the editor beyond the provided API. - /// - /// This likely will only be public in the short term, ideally the API will be expanded to cover necessary use cases. - pub editor: Entity, - /// An optional icon that is displayed at the start of the text field. - /// - /// For example, a magnifying glass icon in a search field. - start_icon: Option, - /// Whether the text field is disabled. - disabled: bool, - /// The minimum width of for the input - min_width: Length, - /// The tab index for keyboard navigation order. - tab_index: Option, - /// Whether this field is a tab stop (can be focused via Tab key). - tab_stop: bool, -} - -impl Focusable for InputField { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.editor.focus_handle(cx) - } -} - -impl InputField { - pub fn new(window: &mut Window, cx: &mut App, placeholder: impl Into) -> Self { - let placeholder_text = placeholder.into(); - - let editor = cx.new(|cx| { - let mut input = Editor::single_line(window, cx); - input.set_placeholder_text(&placeholder_text, window, cx); - input - }); - - Self { - label: None, - label_size: LabelSize::Small, - placeholder: placeholder_text, - editor, - start_icon: None, - disabled: false, - min_width: px(192.).into(), - tab_index: None, - tab_stop: true, - } - } - - pub fn start_icon(mut self, icon: IconName) -> Self { - self.start_icon = Some(icon); - self - } - - pub fn label(mut self, label: impl Into) -> Self { - self.label = Some(label.into()); - self - } - - pub fn label_size(mut self, size: LabelSize) -> Self { - self.label_size = size; - self - } - - pub fn label_min_width(mut self, width: impl Into) -> Self { - self.min_width = width.into(); - self - } - - pub fn tab_index(mut self, index: isize) -> Self { - self.tab_index = Some(index); - self - } - - pub fn tab_stop(mut self, tab_stop: bool) -> Self { - self.tab_stop = tab_stop; - self - } - - pub fn set_disabled(&mut self, disabled: bool, cx: &mut Context) { - self.disabled = disabled; - self.editor - .update(cx, |editor, _| editor.set_read_only(disabled)) - } - - pub fn is_empty(&self, cx: &App) -> bool { - self.editor().read(cx).text(cx).trim().is_empty() - } - - pub fn editor(&self) -> &Entity { - &self.editor - } - - pub fn text(&self, cx: &App) -> String { - self.editor().read(cx).text(cx) - } - - pub fn clear(&self, window: &mut Window, cx: &mut App) { - self.editor() - .update(cx, |editor, cx| editor.clear(window, cx)) - } - - pub fn set_text(&self, text: impl Into>, window: &mut Window, cx: &mut App) { - self.editor() - .update(cx, |editor, cx| editor.set_text(text, window, cx)) - } -} - -impl Render for InputField { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let editor = self.editor.clone(); - let settings = ThemeSettings::get_global(cx); - let theme_color = cx.theme().colors(); - - let mut style = InputFieldStyle { - text_color: theme_color.text, - background_color: theme_color.editor_background, - border_color: theme_color.border_variant, - }; - - if self.disabled { - style.text_color = theme_color.text_disabled; - style.background_color = theme_color.editor_background; - style.border_color = theme_color.border_disabled; - } - - // if self.error_message.is_some() { - // style.text_color = cx.theme().status().error; - // style.border_color = cx.theme().status().error_border - // } - - let text_style = TextStyle { - font_family: settings.ui_font.family.clone(), - font_features: settings.ui_font.features.clone(), - font_size: rems(0.875).into(), - font_weight: settings.buffer_font.weight, - font_style: FontStyle::Normal, - line_height: relative(1.2), - color: style.text_color, - ..Default::default() - }; - - let editor_style = EditorStyle { - background: theme_color.ghost_element_background, - local_player: cx.theme().players().local(), - syntax: cx.theme().syntax().clone(), - text: text_style, - ..Default::default() - }; - - let focus_handle = self.editor.focus_handle(cx); - - let configured_handle = if let Some(tab_index) = self.tab_index { - focus_handle.tab_index(tab_index).tab_stop(self.tab_stop) - } else if !self.tab_stop { - focus_handle.tab_stop(false) - } else { - focus_handle - }; - - v_flex() - .id(self.placeholder.clone()) - .w_full() - .gap_1() - .when_some(self.label.clone(), |this, label| { - this.child( - Label::new(label) - .size(self.label_size) - .color(if self.disabled { - Color::Disabled - } else { - Color::Default - }), - ) - }) - .child( - h_flex() - .track_focus(&configured_handle) - .min_w(self.min_width) - .min_h_8() - .w_full() - .px_2() - .py_1p5() - .flex_grow() - .text_color(style.text_color) - .rounded_md() - .bg(style.background_color) - .border_1() - .border_color(style.border_color) - .when( - editor.focus_handle(cx).contains_focused(window, cx), - |this| this.border_color(theme_color.border_focused), - ) - .when_some(self.start_icon, |this, icon| { - this.gap_1() - .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted)) - }) - .child(EditorElement::new(&self.editor, editor_style)), - ) - } -} - -impl Component for InputField { - fn scope() -> ComponentScope { - ComponentScope::Input - } - - fn preview(window: &mut Window, cx: &mut App) -> Option { - let input_small = - cx.new(|cx| InputField::new(window, cx, "placeholder").label("Small Label")); - - let input_regular = cx.new(|cx| { - InputField::new(window, cx, "placeholder") - .label("Regular Label") - .label_size(LabelSize::Default) - }); - - Some( - v_flex() - .gap_6() - .children(vec![example_group(vec![ - single_example( - "Small Label (Default)", - div().child(input_small).into_any_element(), - ), - single_example( - "Regular Label", - div().child(input_regular).into_any_element(), - ), - ])]) - .into_any_element(), - ) - } -} diff --git a/crates/ui_input/src/number_field.rs b/crates/ui_input/src/number_field.rs deleted file mode 100644 index ee5c57b43b..0000000000 --- a/crates/ui_input/src/number_field.rs +++ /dev/null @@ -1,577 +0,0 @@ -use std::{ - fmt::Display, - num::{NonZero, NonZeroU32, NonZeroU64}, - rc::Rc, - str::FromStr, -}; - -use editor::{Editor, EditorStyle}; -use gpui::{ClickEvent, Entity, FocusHandle, Focusable, FontWeight, Modifiers}; - -use settings::{CenteredPaddingSettings, CodeFade, DelayMs, InactiveOpacity, MinimumContrast}; -use ui::prelude::*; - -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash)] -pub enum NumberFieldMode { - #[default] - Read, - Edit, -} - -pub trait NumberFieldType: Display + Copy + Clone + Sized + PartialOrd + FromStr + 'static { - fn default_format(value: &Self) -> String { - format!("{}", value) - } - fn default_step() -> Self; - fn large_step() -> Self; - fn small_step() -> Self; - fn min_value() -> Self; - fn max_value() -> Self; - fn saturating_add(self, rhs: Self) -> Self; - fn saturating_sub(self, rhs: Self) -> Self; -} - -macro_rules! impl_newtype_numeric_stepper_float { - ($type:ident, $default:expr, $large:expr, $small:expr, $min:expr, $max:expr) => { - impl NumberFieldType for $type { - fn default_step() -> Self { - $default.into() - } - - fn large_step() -> Self { - $large.into() - } - - fn small_step() -> Self { - $small.into() - } - - fn min_value() -> Self { - $min.into() - } - - fn max_value() -> Self { - $max.into() - } - - fn saturating_add(self, rhs: Self) -> Self { - $type((self.0 + rhs.0).min(Self::max_value().0)) - } - - fn saturating_sub(self, rhs: Self) -> Self { - $type((self.0 - rhs.0).max(Self::min_value().0)) - } - } - }; -} - -macro_rules! impl_newtype_numeric_stepper_int { - ($type:ident, $default:expr, $large:expr, $small:expr, $min:expr, $max:expr) => { - impl NumberFieldType for $type { - fn default_step() -> Self { - $default.into() - } - - fn large_step() -> Self { - $large.into() - } - - fn small_step() -> Self { - $small.into() - } - - fn min_value() -> Self { - $min.into() - } - - fn max_value() -> Self { - $max.into() - } - - fn saturating_add(self, rhs: Self) -> Self { - $type(self.0.saturating_add(rhs.0).min(Self::max_value().0)) - } - - fn saturating_sub(self, rhs: Self) -> Self { - $type(self.0.saturating_sub(rhs.0).max(Self::min_value().0)) - } - } - }; -} - -#[rustfmt::skip] -impl_newtype_numeric_stepper_float!(FontWeight, 50., 100., 10., FontWeight::THIN, FontWeight::BLACK); -impl_newtype_numeric_stepper_float!(CodeFade, 0.1, 0.2, 0.05, 0.0, 0.9); -impl_newtype_numeric_stepper_float!(InactiveOpacity, 0.1, 0.2, 0.05, 0.0, 1.0); -impl_newtype_numeric_stepper_float!(MinimumContrast, 1., 10., 0.5, 0.0, 106.0); -impl_newtype_numeric_stepper_int!(DelayMs, 100, 500, 10, 0, 2000); -impl_newtype_numeric_stepper_float!( - CenteredPaddingSettings, - 0.05, - 0.2, - 0.1, - CenteredPaddingSettings::MIN_PADDING, - CenteredPaddingSettings::MAX_PADDING -); - -macro_rules! impl_numeric_stepper_int { - ($type:ident) => { - impl NumberFieldType for $type { - fn default_step() -> Self { - 1 - } - - fn large_step() -> Self { - 10 - } - - fn small_step() -> Self { - 1 - } - - fn min_value() -> Self { - <$type>::MIN - } - - fn max_value() -> Self { - <$type>::MAX - } - - fn saturating_add(self, rhs: Self) -> Self { - self.saturating_add(rhs) - } - - fn saturating_sub(self, rhs: Self) -> Self { - self.saturating_sub(rhs) - } - } - }; -} - -macro_rules! impl_numeric_stepper_nonzero_int { - ($nonzero:ty, $inner:ty) => { - impl NumberFieldType for $nonzero { - fn default_step() -> Self { - <$nonzero>::new(1).unwrap() - } - - fn large_step() -> Self { - <$nonzero>::new(10).unwrap() - } - - fn small_step() -> Self { - <$nonzero>::new(1).unwrap() - } - - fn min_value() -> Self { - <$nonzero>::MIN - } - - fn max_value() -> Self { - <$nonzero>::MAX - } - - fn saturating_add(self, rhs: Self) -> Self { - let result = self.get().saturating_add(rhs.get()); - <$nonzero>::new(result.max(1)).unwrap() - } - - fn saturating_sub(self, rhs: Self) -> Self { - let result = self.get().saturating_sub(rhs.get()).max(1); - <$nonzero>::new(result).unwrap() - } - } - }; -} - -macro_rules! impl_numeric_stepper_float { - ($type:ident) => { - impl NumberFieldType for $type { - fn default_format(value: &Self) -> String { - format!("{:.2}", value) - } - - fn default_step() -> Self { - 1.0 - } - - fn large_step() -> Self { - 10.0 - } - - fn small_step() -> Self { - 0.1 - } - - fn min_value() -> Self { - <$type>::MIN - } - - fn max_value() -> Self { - <$type>::MAX - } - - fn saturating_add(self, rhs: Self) -> Self { - (self + rhs).clamp(Self::min_value(), Self::max_value()) - } - - fn saturating_sub(self, rhs: Self) -> Self { - (self - rhs).clamp(Self::min_value(), Self::max_value()) - } - } - }; -} - -impl_numeric_stepper_float!(f32); -impl_numeric_stepper_float!(f64); -impl_numeric_stepper_int!(isize); -impl_numeric_stepper_int!(usize); -impl_numeric_stepper_int!(i32); -impl_numeric_stepper_int!(u32); -impl_numeric_stepper_int!(i64); -impl_numeric_stepper_int!(u64); - -impl_numeric_stepper_nonzero_int!(NonZeroU32, u32); -impl_numeric_stepper_nonzero_int!(NonZeroU64, u64); -impl_numeric_stepper_nonzero_int!(NonZero, usize); - -#[derive(RegisterComponent)] -pub struct NumberField { - id: ElementId, - value: T, - focus_handle: FocusHandle, - mode: Entity, - format: Box String>, - large_step: T, - small_step: T, - step: T, - min_value: T, - max_value: T, - on_reset: Option>, - on_change: Rc, - tab_index: Option, -} - -impl NumberField { - pub fn new(id: impl Into, value: T, window: &mut Window, cx: &mut App) -> Self { - let id = id.into(); - - let (mode, focus_handle) = window.with_id(id.clone(), |window| { - let mode = window.use_state(cx, |_, _| NumberFieldMode::default()); - let focus_handle = window.use_state(cx, |_, cx| cx.focus_handle()); - (mode, focus_handle) - }); - - Self { - id, - mode, - value, - focus_handle: focus_handle.read(cx).clone(), - format: Box::new(T::default_format), - large_step: T::large_step(), - step: T::default_step(), - small_step: T::small_step(), - min_value: T::min_value(), - max_value: T::max_value(), - on_reset: None, - on_change: Rc::new(|_, _, _| {}), - tab_index: None, - } - } - - pub fn format(mut self, format: impl FnOnce(&T) -> String + 'static) -> Self { - self.format = Box::new(format); - self - } - - pub fn small_step(mut self, step: T) -> Self { - self.small_step = step; - self - } - - pub fn normal_step(mut self, step: T) -> Self { - self.step = step; - self - } - - pub fn large_step(mut self, step: T) -> Self { - self.large_step = step; - self - } - - pub fn min(mut self, min: T) -> Self { - self.min_value = min; - self - } - - pub fn max(mut self, max: T) -> Self { - self.max_value = max; - self - } - - pub fn on_reset( - mut self, - on_reset: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - self.on_reset = Some(Box::new(on_reset)); - self - } - - pub fn tab_index(mut self, tab_index: isize) -> Self { - self.tab_index = Some(tab_index); - self - } - - pub fn on_change(mut self, on_change: impl Fn(&T, &mut Window, &mut App) + 'static) -> Self { - self.on_change = Rc::new(on_change); - self - } -} - -impl IntoElement for NumberField { - type Element = gpui::Component; - - fn into_element(self) -> Self::Element { - gpui::Component::new(self) - } -} - -impl RenderOnce for NumberField { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let mut tab_index = self.tab_index; - - let get_step = { - let large_step = self.large_step; - let step = self.step; - let small_step = self.small_step; - move |modifiers: Modifiers| -> T { - if modifiers.shift { - large_step - } else if modifiers.alt { - small_step - } else { - step - } - } - }; - - let bg_color = cx.theme().colors().surface_background; - let hover_bg_color = cx.theme().colors().element_hover; - - let border_color = cx.theme().colors().border_variant; - let focus_border_color = cx.theme().colors().border_focused; - - let base_button = |icon: IconName| { - h_flex() - .cursor_pointer() - .p_1p5() - .size_full() - .justify_center() - .overflow_hidden() - .border_1() - .border_color(border_color) - .bg(bg_color) - .hover(|s| s.bg(hover_bg_color)) - .focus_visible(|s| s.border_color(focus_border_color).bg(hover_bg_color)) - .child(Icon::new(icon).size(IconSize::Small)) - }; - - h_flex() - .id(self.id.clone()) - .track_focus(&self.focus_handle) - .gap_1() - .when_some(self.on_reset, |this, on_reset| { - this.child( - IconButton::new("reset", IconName::RotateCcw) - .icon_size(IconSize::Small) - .when_some(tab_index.as_mut(), |this, tab_index| { - *tab_index += 1; - this.tab_index(*tab_index - 1) - }) - .on_click(on_reset), - ) - }) - .child( - h_flex() - .map(|decrement| { - let decrement_handler = { - let value = self.value; - let on_change = self.on_change.clone(); - let min = self.min_value; - move |click: &ClickEvent, window: &mut Window, cx: &mut App| { - let step = get_step(click.modifiers()); - let new_value = value.saturating_sub(step); - let new_value = if new_value < min { min } else { new_value }; - on_change(&new_value, window, cx); - } - }; - - decrement.child( - base_button(IconName::Dash) - .id("decrement_button") - .rounded_tl_sm() - .rounded_bl_sm() - .tab_index( - tab_index - .as_mut() - .map(|tab_index| { - *tab_index += 1; - *tab_index - 1 - }) - .unwrap_or(0), - ) - .on_click(decrement_handler), - ) - }) - .child( - h_flex() - .min_w_16() - .size_full() - .border_y_1() - .border_color(border_color) - .bg(bg_color) - .in_focus(|this| this.border_color(focus_border_color)) - .child(match *self.mode.read(cx) { - NumberFieldMode::Read => h_flex() - .px_1() - .flex_1() - .justify_center() - .child(Label::new((self.format)(&self.value))) - .into_any_element(), - // Edit mode is disabled until we implement center text alignment for editor - // mode.write(cx, NumberFieldMode::Edit); - // - // When we get to making Edit mode work, we shouldn't even focus the decrement/increment buttons. - // Focus should go instead straight to the editor, avoiding any double-step focus. - // In this world, the buttons become a mouse-only interaction, given users should be able - // to do everything they'd do with the buttons straight in the editor anyway. - NumberFieldMode::Edit => h_flex() - .flex_1() - .child(window.use_state(cx, { - |window, cx| { - let previous_focus_handle = window.focused(cx); - let mut editor = Editor::single_line(window, cx); - let mut style = EditorStyle::default(); - style.text.text_align = gpui::TextAlign::Right; - editor.set_style(style, window, cx); - - editor.set_text(format!("{}", self.value), window, cx); - cx.on_focus_out(&editor.focus_handle(cx), window, { - let mode = self.mode.clone(); - let min = self.min_value; - let max = self.max_value; - let on_change = self.on_change.clone(); - move |this, _, window, cx| { - if let Ok(new_value) = - this.text(cx).parse::() - { - let new_value = if new_value < min { - min - } else if new_value > max { - max - } else { - new_value - }; - - if let Some(previous) = - previous_focus_handle.as_ref() - { - window.focus(previous); - } - on_change(&new_value, window, cx); - }; - mode.write(cx, NumberFieldMode::Read); - } - }) - .detach(); - - window.focus(&editor.focus_handle(cx)); - - editor - } - })) - .on_action::({ - move |_, window, _| { - window.blur(); - } - }) - .into_any_element(), - }), - ) - .map(|increment| { - let increment_handler = { - let value = self.value; - let on_change = self.on_change.clone(); - let max = self.max_value; - move |click: &ClickEvent, window: &mut Window, cx: &mut App| { - let step = get_step(click.modifiers()); - let new_value = value.saturating_add(step); - let new_value = if new_value > max { max } else { new_value }; - on_change(&new_value, window, cx); - } - }; - - increment.child( - base_button(IconName::Plus) - .id("increment_button") - .rounded_tr_sm() - .rounded_br_sm() - .tab_index( - tab_index - .as_mut() - .map(|tab_index| { - *tab_index += 1; - *tab_index - 1 - }) - .unwrap_or(0), - ) - .on_click(increment_handler), - ) - }), - ) - } -} - -impl Component for NumberField { - fn scope() -> ComponentScope { - ComponentScope::Input - } - - fn name() -> &'static str { - "Number Field" - } - - fn sort_name() -> &'static str { - Self::name() - } - - fn description() -> Option<&'static str> { - Some("A numeric input element with increment and decrement buttons.") - } - - fn preview(window: &mut Window, cx: &mut App) -> Option { - let stepper_example = window.use_state(cx, |_, _| 100.0); - - Some( - v_flex() - .gap_6() - .children(vec![single_example( - "Default Numeric Stepper", - NumberField::new( - "numeric-stepper-component-preview", - *stepper_example.read(cx), - window, - cx, - ) - .on_change({ - let stepper_example = stepper_example.clone(); - move |value, _, cx| stepper_example.write(cx, *value) - }) - .min(1.0) - .max(100.0) - .into_any_element(), - )]) - .into_any_element(), - ) - } -} diff --git a/crates/ui_input/src/ui_input.rs b/crates/ui_input/src/ui_input.rs deleted file mode 100644 index ddc0e659a2..0000000000 --- a/crates/ui_input/src/ui_input.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! This crate provides UI components that can be used for form-like scenarios, such as a input and number field. -//! -//! It can't be located in the `ui` crate because it depends on `editor`. -//! -mod input_field; -mod number_field; - -pub use input_field::*; -pub use number_field::*; diff --git a/crates/ui_macros/Cargo.toml b/crates/ui_macros/Cargo.toml deleted file mode 100644 index 74bd2186a7..0000000000 --- a/crates/ui_macros/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "ui_macros" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/ui_macros.rs" -proc-macro = true - -[dependencies] -quote.workspace = true -syn.workspace = true - -[dev-dependencies] -component.workspace = true -ui.workspace = true diff --git a/crates/ui_macros/LICENSE-GPL b/crates/ui_macros/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/ui_macros/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/ui_macros/src/derive_register_component.rs b/crates/ui_macros/src/derive_register_component.rs deleted file mode 100644 index 64ab132cc0..0000000000 --- a/crates/ui_macros/src/derive_register_component.rs +++ /dev/null @@ -1,29 +0,0 @@ -use proc_macro::TokenStream; -use quote::quote; -use syn::{DeriveInput, parse_macro_input}; - -pub fn derive_register_component(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - - let name = input.ident; - let register_fn_name = syn::Ident::new( - &format!("__component_registry_internal_register_{}", name), - name.span(), - ); - let expanded = quote! { - const _: () = { - struct AssertComponent(::std::marker::PhantomData); - let _ = AssertComponent::<#name>(::std::marker::PhantomData); - }; - - #[allow(non_snake_case)] - fn #register_fn_name() { - component::register_component::<#name>(); - } - - component::__private::inventory::submit! { - component::ComponentFn::new(#register_fn_name) - } - }; - expanded.into() -} diff --git a/crates/ui_macros/src/dynamic_spacing.rs b/crates/ui_macros/src/dynamic_spacing.rs deleted file mode 100644 index 15ba3e241e..0000000000 --- a/crates/ui_macros/src/dynamic_spacing.rs +++ /dev/null @@ -1,167 +0,0 @@ -use proc_macro::TokenStream; -use quote::{format_ident, quote}; -use syn::{ - LitInt, Token, parse::Parse, parse::ParseStream, parse_macro_input, punctuated::Punctuated, -}; - -struct DynamicSpacingInput { - values: Punctuated, -} - -// The input for the derive macro is a list of values. -// -// When a single value is provided, the standard spacing formula is -// used to derive the of spacing values. -// -// When a tuple of three values is provided, the values are used as -// the spacing values directly. -enum DynamicSpacingValue { - Single(LitInt), - Tuple(LitInt, LitInt, LitInt), -} - -impl Parse for DynamicSpacingInput { - fn parse(input: ParseStream) -> syn::Result { - Ok(DynamicSpacingInput { - values: input.parse_terminated(DynamicSpacingValue::parse, Token![,])?, - }) - } -} - -impl Parse for DynamicSpacingValue { - fn parse(input: ParseStream) -> syn::Result { - if input.peek(syn::token::Paren) { - let content; - syn::parenthesized!(content in input); - let a: LitInt = content.parse()?; - content.parse::()?; - let b: LitInt = content.parse()?; - content.parse::()?; - let c: LitInt = content.parse()?; - Ok(DynamicSpacingValue::Tuple(a, b, c)) - } else { - Ok(DynamicSpacingValue::Single(input.parse()?)) - } - } -} - -/// Derives the spacing method for the `DynamicSpacing` enum. -pub fn derive_spacing(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DynamicSpacingInput); - - let spacing_ratios: Vec<_> = input - .values - .iter() - .map(|v| { - let variant = match v { - DynamicSpacingValue::Single(n) => { - format_ident!("Base{:02}", n.base10_parse::().unwrap()) - } - DynamicSpacingValue::Tuple(_, b, _) => { - format_ident!("Base{:02}", b.base10_parse::().unwrap()) - } - }; - match v { - DynamicSpacingValue::Single(n) => { - let n = n.base10_parse::().unwrap(); - quote! { - DynamicSpacing::#variant => match ThemeSettings::get_global(cx).ui_density { - ::theme::UiDensity::Compact => (#n - 4.0).max(0.0) / BASE_REM_SIZE_IN_PX, - ::theme::UiDensity::Default => #n / BASE_REM_SIZE_IN_PX, - ::theme::UiDensity::Comfortable => (#n + 4.0) / BASE_REM_SIZE_IN_PX, - } - } - } - DynamicSpacingValue::Tuple(a, b, c) => { - let a = a.base10_parse::().unwrap(); - let b = b.base10_parse::().unwrap(); - let c = c.base10_parse::().unwrap(); - quote! { - DynamicSpacing::#variant => match ThemeSettings::get_global(cx).ui_density { - ::theme::UiDensity::Compact => #a / BASE_REM_SIZE_IN_PX, - ::theme::UiDensity::Default => #b / BASE_REM_SIZE_IN_PX, - ::theme::UiDensity::Comfortable => #c / BASE_REM_SIZE_IN_PX, - } - } - } - } - }) - .collect(); - - let (variant_names, doc_strings): (Vec<_>, Vec<_>) = input - .values - .iter() - .map(|v| { - let variant = match v { - DynamicSpacingValue::Single(n) => { - format_ident!("Base{:02}", n.base10_parse::().unwrap()) - } - DynamicSpacingValue::Tuple(_, b, _) => { - format_ident!("Base{:02}", b.base10_parse::().unwrap()) - } - }; - let doc_string = match v { - DynamicSpacingValue::Single(n) => { - let n = n.base10_parse::().unwrap(); - let compact = (n - 4.0).max(0.0); - let comfortable = n + 4.0; - format!( - "`{}px`|`{}px`|`{}px (@16px/rem)` - Scales with the user's rem size.", - compact, n, comfortable - ) - } - DynamicSpacingValue::Tuple(a, b, c) => { - let a = a.base10_parse::().unwrap(); - let b = b.base10_parse::().unwrap(); - let c = c.base10_parse::().unwrap(); - format!( - "`{}px`|`{}px`|`{}px (@16px/rem)` - Scales with the user's rem size.", - a, b, c - ) - } - }; - (quote!(#variant), quote!(#doc_string)) - }) - .unzip(); - - let expanded = quote! { - /// A dynamic spacing system that adjusts spacing based on - /// [UiDensity]. - /// - /// The number following "Base" refers to the base pixel size - /// at the default rem size and spacing settings. - /// - /// When possible, [DynamicSpacing] should be used over manual - /// or built-in spacing values in places dynamic spacing is needed. - #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] - pub enum DynamicSpacing { - #( - #[doc = #doc_strings] - #variant_names, - )* - } - - impl DynamicSpacing { - /// Returns the spacing ratio, should only be used internally. - fn spacing_ratio(&self, cx: &App) -> f32 { - const BASE_REM_SIZE_IN_PX: f32 = 16.0; - match self { - #(#spacing_ratios,)* - } - } - - /// Returns the spacing value in rems. - pub fn rems(&self, cx: &App) -> Rems { - rems(self.spacing_ratio(cx)) - } - - /// Returns the spacing value in pixels. - pub fn px(&self, cx: &App) -> Pixels { - let ui_font_size_f32: f32 = ThemeSettings::get_global(cx).ui_font_size(cx).into(); - px(ui_font_size_f32 * self.spacing_ratio(cx)) - } - } - }; - - TokenStream::from(expanded) -} diff --git a/crates/ui_macros/src/ui_macros.rs b/crates/ui_macros/src/ui_macros.rs deleted file mode 100644 index ce48a21c11..0000000000 --- a/crates/ui_macros/src/ui_macros.rs +++ /dev/null @@ -1,37 +0,0 @@ -mod derive_register_component; -mod dynamic_spacing; - -use proc_macro::TokenStream; - -/// Generates the DynamicSpacing enum used for density-aware spacing in the UI. -#[proc_macro] -pub fn derive_dynamic_spacing(input: TokenStream) -> TokenStream { - dynamic_spacing::derive_spacing(input) -} - -/// Registers components that implement the `Component` trait. -/// -/// This proc macro is used to automatically register structs that implement -/// the `Component` trait with the [`component::ComponentRegistry`]. -/// -/// If the component trait is not implemented, it will generate a compile-time error. -/// -/// # Example -/// -/// ``` -/// use ui::Component; -/// use ui_macros::RegisterComponent; -/// -/// #[derive(RegisterComponent)] -/// struct MyComponent; -/// -/// impl Component for MyComponent { -/// // Component implementation -/// } -/// ``` -/// -/// This example will add MyComponent to the ComponentRegistry. -#[proc_macro_derive(RegisterComponent)] -pub fn derive_register_component(input: TokenStream) -> TokenStream { - derive_register_component::derive_register_component(input) -} diff --git a/crates/ui_prompt/Cargo.toml b/crates/ui_prompt/Cargo.toml deleted file mode 100644 index 55a9828843..0000000000 --- a/crates/ui_prompt/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "ui_prompt" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/ui_prompt.rs" - -[features] -default = [] - -[dependencies] -gpui.workspace = true -markdown.workspace = true -menu.workspace = true -settings.workspace = true -theme.workspace = true -ui.workspace = true -workspace.workspace = true diff --git a/crates/ui_prompt/LICENSE-GPL b/crates/ui_prompt/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/ui_prompt/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/ui_prompt/src/ui_prompt.rs b/crates/ui_prompt/src/ui_prompt.rs deleted file mode 100644 index 3b2716fd92..0000000000 --- a/crates/ui_prompt/src/ui_prompt.rs +++ /dev/null @@ -1,205 +0,0 @@ -use gpui::{ - App, Entity, EventEmitter, FocusHandle, Focusable, PromptButton, PromptHandle, PromptLevel, - PromptResponse, RenderablePromptHandle, SharedString, TextStyleRefinement, Window, div, - prelude::*, -}; -use markdown::{Markdown, MarkdownElement, MarkdownStyle}; -use settings::{Settings, SettingsStore}; -use theme::ThemeSettings; -use ui::{FluentBuilder, TintColor, prelude::*}; -use workspace::WorkspaceSettings; - -pub fn init(cx: &mut App) { - process_settings(cx); - - cx.observe_global::(process_settings) - .detach(); -} - -fn process_settings(cx: &mut App) { - let settings = WorkspaceSettings::get_global(cx); - if settings.use_system_prompts && cfg!(not(any(target_os = "linux", target_os = "freebsd"))) { - cx.reset_prompt_builder(); - } else { - cx.set_prompt_builder(zed_prompt_renderer); - } -} - -/// Use this function in conjunction with [App::set_prompt_builder] to force -/// GPUI to use the internal prompt system. -fn zed_prompt_renderer( - level: PromptLevel, - message: &str, - detail: Option<&str>, - actions: &[PromptButton], - handle: PromptHandle, - window: &mut Window, - cx: &mut App, -) -> RenderablePromptHandle { - let renderer = cx.new({ - |cx| ZedPromptRenderer { - _level: level, - message: cx.new(|cx| Markdown::new(SharedString::new(message), None, None, cx)), - actions: actions.iter().map(|a| a.label().to_string()).collect(), - focus: cx.focus_handle(), - active_action_id: 0, - detail: detail - .filter(|text| !text.is_empty()) - .map(|text| cx.new(|cx| Markdown::new(SharedString::new(text), None, None, cx))), - } - }); - - handle.with_view(renderer, window, cx) -} - -pub struct ZedPromptRenderer { - _level: PromptLevel, - message: Entity, - actions: Vec, - focus: FocusHandle, - active_action_id: usize, - detail: Option>, -} - -impl ZedPromptRenderer { - fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context) { - cx.emit(PromptResponse(self.active_action_id)); - } - - fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context) { - if let Some(ix) = self.actions.iter().position(|a| a == "Cancel") { - cx.emit(PromptResponse(ix)); - } - } - - fn select_first( - &mut self, - _: &menu::SelectFirst, - _window: &mut Window, - cx: &mut Context, - ) { - self.active_action_id = self.actions.len().saturating_sub(1); - cx.notify(); - } - - fn select_last(&mut self, _: &menu::SelectLast, _window: &mut Window, cx: &mut Context) { - self.active_action_id = 0; - cx.notify(); - } - - fn select_next(&mut self, _: &menu::SelectNext, _window: &mut Window, cx: &mut Context) { - self.active_action_id = (self.active_action_id + 1) % self.actions.len(); - cx.notify(); - } - - fn select_previous( - &mut self, - _: &menu::SelectPrevious, - _window: &mut Window, - cx: &mut Context, - ) { - if self.active_action_id > 0 { - self.active_action_id -= 1; - } else { - self.active_action_id = self.actions.len().saturating_sub(1); - } - cx.notify(); - } -} - -impl Render for ZedPromptRenderer { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let settings = ThemeSettings::get_global(cx); - - let dialog = v_flex() - .key_context("Prompt") - .cursor_default() - .track_focus(&self.focus) - .on_action(cx.listener(Self::confirm)) - .on_action(cx.listener(Self::cancel)) - .on_action(cx.listener(Self::select_next)) - .on_action(cx.listener(Self::select_previous)) - .on_action(cx.listener(Self::select_first)) - .on_action(cx.listener(Self::select_last)) - .w_80() - .p_4() - .gap_4() - .elevation_3(cx) - .overflow_hidden() - .font_family(settings.ui_font.family.clone()) - .child(div().w_full().child(MarkdownElement::new( - self.message.clone(), - markdown_style(true, window, cx), - ))) - .children(self.detail.clone().map(|detail| { - div().w_full().text_xs().child(MarkdownElement::new( - detail, - markdown_style(false, window, cx), - )) - })) - .child( - v_flex() - .gap_1() - .children(self.actions.iter().enumerate().map(|(ix, action)| { - Button::new(ix, action.clone()) - .full_width() - .style(ButtonStyle::Outlined) - .when(ix == self.active_action_id, |s| { - s.style(ButtonStyle::Tinted(TintColor::Accent)) - }) - .tab_index(ix as isize) - .on_click(cx.listener(move |_, _, _window, cx| { - cx.emit(PromptResponse(ix)); - })) - })), - ); - - div() - .size_full() - .occlude() - .bg(gpui::black().opacity(0.2)) - .child( - v_flex() - .size_full() - .absolute() - .top_0() - .left_0() - .items_center() - .justify_center() - .child(dialog), - ) - } -} - -fn markdown_style(main_message: bool, window: &Window, cx: &App) -> MarkdownStyle { - let mut base_text_style = window.text_style(); - let settings = ThemeSettings::get_global(cx); - let font_size = settings.ui_font_size(cx).into(); - - let color = if main_message { - Color::Default.color(cx) - } else { - Color::Muted.color(cx) - }; - - base_text_style.refine(&TextStyleRefinement { - font_family: Some(settings.ui_font.family.clone()), - font_size: Some(font_size), - color: Some(color), - ..Default::default() - }); - - MarkdownStyle { - base_text_style, - selection_background_color: cx.theme().colors().element_selection_background, - ..Default::default() - } -} - -impl EventEmitter for ZedPromptRenderer {} - -impl Focusable for ZedPromptRenderer { - fn focus_handle(&self, _: &crate::App) -> FocusHandle { - self.focus.clone() - } -} diff --git a/crates/vercel/Cargo.toml b/crates/vercel/Cargo.toml deleted file mode 100644 index 98b26c9104..0000000000 --- a/crates/vercel/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "vercel" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/vercel.rs" - -[features] -default = [] -schemars = ["dep:schemars"] - -[dependencies] -anyhow.workspace = true -schemars = { workspace = true, optional = true } -serde.workspace = true -strum.workspace = true diff --git a/crates/vercel/LICENSE-GPL b/crates/vercel/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/vercel/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/vercel/src/vercel.rs b/crates/vercel/src/vercel.rs deleted file mode 100644 index 8686fda53f..0000000000 --- a/crates/vercel/src/vercel.rs +++ /dev/null @@ -1,78 +0,0 @@ -use anyhow::Result; -use serde::{Deserialize, Serialize}; -use strum::EnumIter; - -pub const VERCEL_API_URL: &str = "https://api.v0.dev/v1"; - -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)] -pub enum Model { - #[default] - #[serde(rename = "v0-1.5-md")] - VZeroOnePointFiveMedium, - #[serde(rename = "custom")] - Custom { - name: String, - /// The name displayed in the UI, such as in the assistant panel model dropdown menu. - display_name: Option, - max_tokens: u64, - max_output_tokens: Option, - max_completion_tokens: Option, - }, -} - -impl Model { - pub fn default_fast() -> Self { - Self::VZeroOnePointFiveMedium - } - - pub fn from_id(id: &str) -> Result { - match id { - "v0-1.5-md" => Ok(Self::VZeroOnePointFiveMedium), - invalid_id => anyhow::bail!("invalid model id '{invalid_id}'"), - } - } - - pub fn id(&self) -> &str { - match self { - Self::VZeroOnePointFiveMedium => "v0-1.5-md", - Self::Custom { name, .. } => name, - } - } - - pub fn display_name(&self) -> &str { - match self { - Self::VZeroOnePointFiveMedium => "v0-1.5-md", - Self::Custom { - name, display_name, .. - } => display_name.as_ref().unwrap_or(name), - } - } - - pub fn max_token_count(&self) -> u64 { - match self { - Self::VZeroOnePointFiveMedium => 128_000, - Self::Custom { max_tokens, .. } => *max_tokens, - } - } - - pub fn max_output_tokens(&self) -> Option { - match self { - Self::VZeroOnePointFiveMedium => Some(32_000), - Self::Custom { - max_output_tokens, .. - } => *max_output_tokens, - } - } - - pub fn supports_parallel_tool_calls(&self) -> bool { - match self { - Self::VZeroOnePointFiveMedium => true, - Model::Custom { .. } => false, - } - } - - pub fn supports_prompt_cache_key(&self) -> bool { - false - } -} diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml deleted file mode 100644 index 74409a6c25..0000000000 --- a/crates/vim/Cargo.toml +++ /dev/null @@ -1,75 +0,0 @@ -[package] -name = "vim" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/vim.rs" -doctest = false - -[features] -neovim = ["nvim-rs", "async-compat", "async-trait", "tokio"] - -[dependencies] -anyhow.workspace = true -async-compat = { workspace = true, "optional" = true } -async-trait = { workspace = true, "optional" = true } -collections.workspace = true -command_palette.workspace = true -command_palette_hooks.workspace = true -db.workspace = true -editor.workspace = true -env_logger.workspace = true -futures.workspace = true -fuzzy.workspace = true -gpui.workspace = true -itertools.workspace = true -language.workspace = true -log.workspace = true -multi_buffer.workspace = true -nvim-rs = { git = "https://github.com/KillTheMule/nvim-rs", rev = "764dd270c642f77f10f3e19d05cc178a6cbe69f3", features = ["use_tokio"], optional = true } -picker.workspace = true -project.workspace = true -regex.workspace = true -schemars.workspace = true -search.workspace = true -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -task.workspace = true -text.workspace = true -theme.workspace = true -menu.workspace = true -tokio = { version = "1.15", features = ["full"], optional = true } -ui.workspace = true -util.workspace = true -util_macros.workspace = true -vim_mode_setting.workspace = true -workspace.workspace = true -zed_actions.workspace = true - -[dev-dependencies] -assets.workspace = true -command_palette = { workspace = true, features = ["test-support"] } -editor = { workspace = true, features = ["test-support"] } -git_ui.workspace = true -gpui = { workspace = true, features = ["test-support"] } -indoc.workspace = true -language = { workspace = true, features = ["test-support"] } -project = { workspace = true, features = ["test-support"] } -lsp = { workspace = true, features = ["test-support"] } -markdown_preview.workspace = true -parking_lot.workspace = true -project_panel.workspace = true -release_channel.workspace = true -semver.workspace = true -settings_ui.workspace = true -settings.workspace = true -perf.workspace = true -util = { workspace = true, features = ["test-support"] } -workspace = { workspace = true, features = ["test-support"] } diff --git a/crates/vim/LICENSE-GPL b/crates/vim/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/vim/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/vim/README.md b/crates/vim/README.md deleted file mode 100644 index 28f1375c62..0000000000 --- a/crates/vim/README.md +++ /dev/null @@ -1,36 +0,0 @@ -This contains the code for Zed's Vim emulation mode. - -Vim mode in Zed is supposed to primarily "do what you expect": it mostly tries to copy vim exactly, but will use Zed-specific functionality when available to make things smoother. This means Zed will never be 100% vim compatible, but should be 100% vim familiar! - -The backlog is maintained in the `#vim` channel notes. - -## Testing against Neovim - -If you are making a change to make Zed's behavior more closely match vim/nvim, you can create a test using the `NeovimBackedTestContext`. - -For example, the following test checks that Zed and Neovim have the same behavior when running `*` in visual mode: - -```rust -#[gpui::test] -async fn test_visual_star_hash(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇa.c. abcd a.c. abcd").await; - cx.simulate_shared_keystrokes(["v", "3", "l", "*"]).await; - cx.assert_shared_state("a.c. abcd ˇa.c. abcd").await; -} -``` - -To keep CI runs fast, by default the neovim tests use a cached JSON file that records what neovim did (see crates/vim/test_data), -but while developing this test you'll need to run it with the neovim flag enabled: - -```sh -cargo test -p vim --features neovim test_visual_star_hash -``` - -This will run your keystrokes against a headless neovim and cache the results in the test_data directory. Note that neovim must be installed and reachable on your $PATH in order to run the feature. - - -## Testing zed-only behavior - -Zed does more than vim/neovim in their default modes. The `VimTestContext` can be used instead. This lets you test integration with the language server and other parts of zed's UI that don't have a NeoVim equivalent. diff --git a/crates/vim/src/change_list.rs b/crates/vim/src/change_list.rs deleted file mode 100644 index 21cd332800..0000000000 --- a/crates/vim/src/change_list.rs +++ /dev/null @@ -1,220 +0,0 @@ -use editor::{Bias, Direction, Editor, display_map::ToDisplayPoint, movement}; -use gpui::{Context, Window, actions}; - -use crate::{Vim, state::Mode}; - -actions!( - vim, - [ - /// Navigates to an older position in the change list. - ChangeListOlder, - /// Navigates to a newer position in the change list. - ChangeListNewer - ] -); - -pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { - Vim::action(editor, cx, |vim, _: &ChangeListOlder, window, cx| { - vim.move_to_change(Direction::Prev, window, cx); - }); - Vim::action(editor, cx, |vim, _: &ChangeListNewer, window, cx| { - vim.move_to_change(Direction::Next, window, cx); - }); -} - -impl Vim { - fn move_to_change( - &mut self, - direction: Direction, - window: &mut Window, - cx: &mut Context, - ) { - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - self.update_editor(cx, |_, editor, cx| { - if let Some(selections) = editor - .change_list - .next_change(count, direction) - .map(|s| s.to_vec()) - { - editor.change_selections(Default::default(), window, cx, |s| { - let map = s.display_snapshot(); - s.select_display_ranges(selections.iter().map(|a| { - let point = a.to_display_point(&map); - point..point - })) - }) - }; - }); - } - - pub(crate) fn push_to_change_list(&mut self, window: &mut Window, cx: &mut Context) { - let Some((new_positions, buffer)) = self.update_editor(cx, |vim, editor, cx| { - let display_map = editor.display_snapshot(cx); - let selections = editor.selections.all_adjusted_display(&display_map); - let buffer = editor.buffer().clone(); - - let pop_state = editor - .change_list - .last() - .map(|previous| { - previous.len() == selections.len() - && previous.iter().enumerate().all(|(ix, p)| { - p.to_display_point(&display_map).row() == selections[ix].head().row() - }) - }) - .unwrap_or(false); - - let new_positions = selections - .into_iter() - .map(|s| { - let point = if vim.mode == Mode::Insert { - movement::saturating_left(&display_map, s.head()) - } else { - s.head() - }; - display_map.display_point_to_anchor(point, Bias::Left) - }) - .collect::>(); - - editor - .change_list - .push_to_change_list(pop_state, new_positions.clone()); - - (new_positions, buffer) - }) else { - return; - }; - - self.set_mark(".".to_string(), new_positions, &buffer, window, cx) - } -} - -#[cfg(test)] -mod test { - use indoc::indoc; - - use crate::{state::Mode, test::NeovimBackedTestContext}; - - #[gpui::test] - async fn test_change_list_insert(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇ").await; - - cx.simulate_shared_keystrokes("i 1 1 escape shift-o 2 2 escape shift-g o 3 3 escape") - .await; - - cx.shared_state().await.assert_eq(indoc! { - "22 - 11 - 3ˇ3" - }); - - cx.simulate_shared_keystrokes("g ;").await; - // NOTE: this matches nvim when I type it into it - // but in tests, nvim always reports the column as 0... - cx.assert_state( - indoc! { - "22 - 11 - 3ˇ3" - }, - Mode::Normal, - ); - cx.simulate_shared_keystrokes("g ;").await; - cx.assert_state( - indoc! { - "2ˇ2 - 11 - 33" - }, - Mode::Normal, - ); - cx.simulate_shared_keystrokes("g ;").await; - cx.assert_state( - indoc! { - "22 - 1ˇ1 - 33" - }, - Mode::Normal, - ); - cx.simulate_shared_keystrokes("g ,").await; - cx.assert_state( - indoc! { - "2ˇ2 - 11 - 33" - }, - Mode::Normal, - ); - cx.simulate_shared_keystrokes("shift-g i 4 4 escape").await; - cx.simulate_shared_keystrokes("g ;").await; - cx.assert_state( - indoc! { - "22 - 11 - 34ˇ43" - }, - Mode::Normal, - ); - cx.simulate_shared_keystrokes("g ;").await; - cx.assert_state( - indoc! { - "2ˇ2 - 11 - 3443" - }, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_change_list_delete(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! { - "one two - three fˇour"}) - .await; - cx.simulate_shared_keystrokes("x k d i w ^ x").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇne• - three fur"}); - cx.simulate_shared_keystrokes("2 g ;").await; - cx.shared_state().await.assert_eq(indoc! { - "ne• - three fˇur"}); - cx.simulate_shared_keystrokes("g ,").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇne• - three fur"}); - } - - #[gpui::test] - async fn test_gi(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! { - "one two - three fˇr"}) - .await; - cx.simulate_shared_keystrokes("i o escape k g i").await; - cx.simulate_shared_keystrokes("u escape").await; - cx.shared_state().await.assert_eq(indoc! { - "one two - three foˇur"}); - } - - #[gpui::test] - async fn test_dot_mark(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! { - "one two - three fˇr"}) - .await; - cx.simulate_shared_keystrokes("i o escape k ` .").await; - cx.shared_state().await.assert_eq(indoc! { - "one two - three fˇor"}); - } -} diff --git a/crates/vim/src/command.rs b/crates/vim/src/command.rs deleted file mode 100644 index 5bf0fca041..0000000000 --- a/crates/vim/src/command.rs +++ /dev/null @@ -1,3211 +0,0 @@ -use anyhow::{Result, anyhow}; -use collections::{HashMap, HashSet}; -use command_palette_hooks::{CommandInterceptItem, CommandInterceptResult}; -use editor::{ - Bias, Editor, EditorSettings, SelectionEffects, ToPoint, - actions::{SortLinesCaseInsensitive, SortLinesCaseSensitive}, - display_map::ToDisplayPoint, -}; -use futures::AsyncWriteExt as _; -use gpui::{ - Action, App, AppContext as _, Context, Global, Keystroke, Task, WeakEntity, Window, actions, -}; -use itertools::Itertools; -use language::Point; -use multi_buffer::MultiBufferRow; -use project::ProjectPath; -use regex::Regex; -use schemars::JsonSchema; -use search::{BufferSearchBar, SearchOptions}; -use serde::Deserialize; -use settings::{Settings, SettingsStore}; -use std::{ - iter::Peekable, - ops::{Deref, Range}, - path::{Path, PathBuf}, - process::Stdio, - str::Chars, - sync::OnceLock, - time::Instant, -}; -use task::{HideStrategy, RevealStrategy, SpawnInTerminal, TaskId}; -use ui::ActiveTheme; -use util::{ - ResultExt, - paths::PathStyle, - rel_path::{RelPath, RelPathBuf}, -}; -use workspace::{Item, SaveIntent, Workspace, notifications::NotifyResultExt}; -use workspace::{SplitDirection, notifications::DetachAndPromptErr}; -use zed_actions::{OpenDocs, RevealTarget}; - -use crate::{ - ToggleMarksView, ToggleRegistersView, Vim, - motion::{EndOfDocument, Motion, MotionKind, StartOfDocument}, - normal::{ - JoinLines, - search::{FindCommand, ReplaceCommand, Replacement}, - }, - object::Object, - state::{Mark, Mode}, - visual::VisualDeleteLine, -}; - -/// Goes to the specified line number in the editor. -#[derive(Clone, Debug, PartialEq, Action)] -#[action(namespace = vim, no_json, no_register)] -pub struct GoToLine { - range: CommandRange, -} - -/// Yanks (copies) text based on the specified range. -#[derive(Clone, Debug, PartialEq, Action)] -#[action(namespace = vim, no_json, no_register)] -pub struct YankCommand { - range: CommandRange, -} - -/// Executes a command with the specified range. -#[derive(Clone, Debug, PartialEq, Action)] -#[action(namespace = vim, no_json, no_register)] -pub struct WithRange { - restore_selection: bool, - range: CommandRange, - action: WrappedAction, -} - -/// Executes a command with the specified count. -#[derive(Clone, Debug, PartialEq, Action)] -#[action(namespace = vim, no_json, no_register)] -pub struct WithCount { - count: u32, - action: WrappedAction, -} - -#[derive(Clone, Deserialize, JsonSchema, PartialEq)] -pub enum VimOption { - Wrap(bool), - Number(bool), - RelativeNumber(bool), - IgnoreCase(bool), -} - -impl VimOption { - fn possible_commands(query: &str) -> Vec { - let mut prefix_of_options = Vec::new(); - let mut options = query.split(" ").collect::>(); - let prefix = options.pop().unwrap_or_default(); - for option in options { - if let Some(opt) = Self::from(option) { - prefix_of_options.push(opt) - } else { - return vec![]; - } - } - - Self::possibilities(prefix) - .map(|possible| { - let mut options = prefix_of_options.clone(); - options.push(possible); - - CommandInterceptItem { - string: format!( - ":set {}", - options.iter().map(|opt| opt.to_string()).join(" ") - ), - action: VimSet { options }.boxed_clone(), - positions: vec![], - } - }) - .collect() - } - - fn possibilities(query: &str) -> impl Iterator + '_ { - [ - (None, VimOption::Wrap(true)), - (None, VimOption::Wrap(false)), - (None, VimOption::Number(true)), - (None, VimOption::Number(false)), - (None, VimOption::RelativeNumber(true)), - (None, VimOption::RelativeNumber(false)), - (Some("rnu"), VimOption::RelativeNumber(true)), - (Some("nornu"), VimOption::RelativeNumber(false)), - (None, VimOption::IgnoreCase(true)), - (None, VimOption::IgnoreCase(false)), - (Some("ic"), VimOption::IgnoreCase(true)), - (Some("noic"), VimOption::IgnoreCase(false)), - ] - .into_iter() - .filter(move |(prefix, option)| prefix.unwrap_or(option.to_string()).starts_with(query)) - .map(|(_, option)| option) - } - - fn from(option: &str) -> Option { - match option { - "wrap" => Some(Self::Wrap(true)), - "nowrap" => Some(Self::Wrap(false)), - - "number" => Some(Self::Number(true)), - "nu" => Some(Self::Number(true)), - "nonumber" => Some(Self::Number(false)), - "nonu" => Some(Self::Number(false)), - - "relativenumber" => Some(Self::RelativeNumber(true)), - "rnu" => Some(Self::RelativeNumber(true)), - "norelativenumber" => Some(Self::RelativeNumber(false)), - "nornu" => Some(Self::RelativeNumber(false)), - - "ignorecase" => Some(Self::IgnoreCase(true)), - "ic" => Some(Self::IgnoreCase(true)), - "noignorecase" => Some(Self::IgnoreCase(false)), - "noic" => Some(Self::IgnoreCase(false)), - - _ => None, - } - } - - fn to_string(&self) -> &'static str { - match self { - VimOption::Wrap(true) => "wrap", - VimOption::Wrap(false) => "nowrap", - VimOption::Number(true) => "number", - VimOption::Number(false) => "nonumber", - VimOption::RelativeNumber(true) => "relativenumber", - VimOption::RelativeNumber(false) => "norelativenumber", - VimOption::IgnoreCase(true) => "ignorecase", - VimOption::IgnoreCase(false) => "noignorecase", - } - } -} - -/// Sets vim options and configuration values. -#[derive(Clone, PartialEq, Action)] -#[action(namespace = vim, no_json, no_register)] -pub struct VimSet { - options: Vec, -} - -/// Saves the current file with optional save intent. -#[derive(Clone, PartialEq, Action)] -#[action(namespace = vim, no_json, no_register)] -struct VimSave { - pub range: Option, - pub save_intent: Option, - pub filename: String, -} - -/// Deletes the specified marks from the editor. -#[derive(Clone, PartialEq, Action)] -#[action(namespace = vim, no_json, no_register)] -struct VimSplit { - pub vertical: bool, - pub filename: String, -} - -#[derive(Clone, PartialEq, Action)] -#[action(namespace = vim, no_json, no_register)] -enum DeleteMarks { - Marks(String), - AllLocal, -} - -actions!( - vim, - [ - /// Executes a command in visual mode. - VisualCommand, - /// Executes a command with a count prefix. - CountCommand, - /// Executes a shell command. - ShellCommand, - /// Indicates that an argument is required for the command. - ArgumentRequired - ] -); - -/// Opens the specified file for editing. -#[derive(Clone, PartialEq, Action)] -#[action(namespace = vim, no_json, no_register)] -struct VimEdit { - pub filename: String, -} - -#[derive(Clone, PartialEq, Action)] -#[action(namespace = vim, no_json, no_register)] -struct VimNorm { - pub range: Option, - pub command: String, -} - -#[derive(Debug)] -struct WrappedAction(Box); - -impl PartialEq for WrappedAction { - fn eq(&self, other: &Self) -> bool { - self.0.partial_eq(&*other.0) - } -} - -impl Clone for WrappedAction { - fn clone(&self) -> Self { - Self(self.0.boxed_clone()) - } -} - -impl Deref for WrappedAction { - type Target = dyn Action; - fn deref(&self) -> &dyn Action { - &*self.0 - } -} - -pub fn register(editor: &mut Editor, cx: &mut Context) { - // Vim::action(editor, cx, |vim, action: &StartOfLine, window, cx| { - Vim::action(editor, cx, |vim, action: &VimSet, _, cx| { - for option in action.options.iter() { - vim.update_editor(cx, |_, editor, cx| match option { - VimOption::Wrap(true) => { - editor - .set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx); - } - VimOption::Wrap(false) => { - editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx); - } - VimOption::Number(enabled) => { - editor.set_show_line_numbers(*enabled, cx); - } - VimOption::RelativeNumber(enabled) => { - editor.set_relative_line_number(Some(*enabled), cx); - } - VimOption::IgnoreCase(enabled) => { - let mut settings = EditorSettings::get_global(cx).clone(); - settings.search.case_sensitive = !*enabled; - SettingsStore::update(cx, |store, _| { - store.override_global(settings); - }); - } - }); - } - }); - Vim::action(editor, cx, |vim, _: &VisualCommand, window, cx| { - let Some(workspace) = vim.workspace(window) else { - return; - }; - workspace.update(cx, |workspace, cx| { - command_palette::CommandPalette::toggle(workspace, "'<,'>", window, cx); - }) - }); - - Vim::action(editor, cx, |vim, _: &ShellCommand, window, cx| { - let Some(workspace) = vim.workspace(window) else { - return; - }; - workspace.update(cx, |workspace, cx| { - command_palette::CommandPalette::toggle(workspace, "'<,'>!", window, cx); - }) - }); - - Vim::action(editor, cx, |_, _: &ArgumentRequired, window, cx| { - let _ = window.prompt( - gpui::PromptLevel::Critical, - "Argument required", - None, - &["Cancel"], - cx, - ); - }); - - Vim::action(editor, cx, |vim, _: &ShellCommand, window, cx| { - let Some(workspace) = vim.workspace(window) else { - return; - }; - workspace.update(cx, |workspace, cx| { - command_palette::CommandPalette::toggle(workspace, "'<,'>!", window, cx); - }) - }); - - Vim::action(editor, cx, |vim, action: &VimSave, window, cx| { - if let Some(range) = &action.range { - vim.update_editor(cx, |vim, editor, cx| { - let Some(range) = range.buffer_range(vim, editor, window, cx).ok() else { - return; - }; - let Some((line_ending, text, whole_buffer)) = editor.buffer().update(cx, |multi, cx| { - Some(multi.as_singleton()?.update(cx, |buffer, _| { - ( - buffer.line_ending(), - buffer.as_rope().slice_rows(range.start.0..range.end.0 + 1), - range.start.0 == 0 && range.end.0 + 1 >= buffer.row_count(), - ) - })) - }) else { - return; - }; - - let filename = action.filename.clone(); - let filename = if filename.is_empty() { - let Some(file) = editor - .buffer() - .read(cx) - .as_singleton() - .and_then(|buffer| buffer.read(cx).file()) - else { - let _ = window.prompt( - gpui::PromptLevel::Warning, - "No file name", - Some("Partial buffer write requires file name."), - &["Cancel"], - cx, - ); - return; - }; - file.path().display(file.path_style(cx)).to_string() - } else { - filename - }; - - if action.filename.is_empty() { - if whole_buffer { - if let Some(workspace) = vim.workspace(window) { - workspace.update(cx, |workspace, cx| { - workspace - .save_active_item( - action.save_intent.unwrap_or(SaveIntent::Save), - window, - cx, - ) - .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None); - }); - } - return; - } - if Some(SaveIntent::Overwrite) != action.save_intent { - let _ = window.prompt( - gpui::PromptLevel::Warning, - "Use ! to write partial buffer", - Some("Overwriting the current file with selected buffer content requires '!'."), - &["Cancel"], - cx, - ); - return; - } - editor.buffer().update(cx, |multi, cx| { - if let Some(buffer) = multi.as_singleton() { - buffer.update(cx, |buffer, _| buffer.set_conflict()); - } - }); - }; - - editor.project().unwrap().update(cx, |project, cx| { - let worktree = project.visible_worktrees(cx).next().unwrap(); - - worktree.update(cx, |worktree, cx| { - let path_style = worktree.path_style(); - let Some(path) = RelPath::new(Path::new(&filename), path_style).ok() else { - return; - }; - - let rx = (worktree.entry_for_path(&path).is_some() && Some(SaveIntent::Overwrite) != action.save_intent).then(|| { - window.prompt( - gpui::PromptLevel::Warning, - &format!("{path:?} already exists. Do you want to replace it?"), - Some( - "A file or folder with the same name already exists. Replacing it will overwrite its current contents.", - ), - &["Replace", "Cancel"], - cx - ) - }); - let filename = filename.clone(); - cx.spawn_in(window, async move |this, cx| { - if let Some(rx) = rx - && Ok(0) != rx.await - { - return; - } - - let _ = this.update_in(cx, |worktree, window, cx| { - let Some(path) = RelPath::new(Path::new(&filename), path_style).ok() else { - return; - }; - worktree - .write_file(path.into_arc(), text.clone(), line_ending, cx) - .detach_and_prompt_err("Failed to write lines", window, cx, |_, _, _| None); - }); - }) - .detach(); - }); - }); - }); - return; - } - if action.filename.is_empty() { - if let Some(workspace) = vim.workspace(window) { - workspace.update(cx, |workspace, cx| { - workspace - .save_active_item( - action.save_intent.unwrap_or(SaveIntent::Save), - window, - cx, - ) - .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None); - }); - } - return; - } - vim.update_editor(cx, |_, editor, cx| { - let Some(project) = editor.project().cloned() else { - return; - }; - let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else { - return; - }; - let path_style = worktree.read(cx).path_style(); - let Ok(project_path) = - RelPath::new(Path::new(&action.filename), path_style).map(|path| ProjectPath { - worktree_id: worktree.read(cx).id(), - path: path.into_arc(), - }) - else { - // TODO implement save_as with absolute path - Task::ready(Err::<(), _>(anyhow!( - "Cannot save buffer with absolute path" - ))) - .detach_and_prompt_err( - "Failed to save", - window, - cx, - |_, _, _| None, - ); - return; - }; - - if project.read(cx).entry_for_path(&project_path, cx).is_some() - && action.save_intent != Some(SaveIntent::Overwrite) - { - let answer = window.prompt( - gpui::PromptLevel::Critical, - &format!( - "{} already exists. Do you want to replace it?", - project_path.path.display(path_style) - ), - Some( - "A file or folder with the same name already exists. \ - Replacing it will overwrite its current contents.", - ), - &["Replace", "Cancel"], - cx, - ); - cx.spawn_in(window, async move |editor, cx| { - if answer.await.ok() != Some(0) { - return; - } - - let _ = editor.update_in(cx, |editor, window, cx| { - editor - .save_as(project, project_path, window, cx) - .detach_and_prompt_err("Failed to :w", window, cx, |_, _, _| None); - }); - }) - .detach(); - } else { - editor - .save_as(project, project_path, window, cx) - .detach_and_prompt_err("Failed to :w", window, cx, |_, _, _| None); - } - }); - }); - - Vim::action(editor, cx, |vim, action: &VimSplit, window, cx| { - let Some(workspace) = vim.workspace(window) else { - return; - }; - - workspace.update(cx, |workspace, cx| { - let project = workspace.project().clone(); - let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else { - return; - }; - let path_style = worktree.read(cx).path_style(); - let Some(path) = RelPath::new(Path::new(&action.filename), path_style).log_err() else { - return; - }; - let project_path = ProjectPath { - worktree_id: worktree.read(cx).id(), - path: path.into_arc(), - }; - - let direction = if action.vertical { - SplitDirection::vertical(cx) - } else { - SplitDirection::horizontal(cx) - }; - - workspace - .split_path_preview(project_path, false, Some(direction), window, cx) - .detach_and_log_err(cx); - }) - }); - - Vim::action(editor, cx, |vim, action: &DeleteMarks, window, cx| { - fn err(s: String, window: &mut Window, cx: &mut Context) { - let _ = window.prompt( - gpui::PromptLevel::Critical, - &format!("Invalid argument: {}", s), - None, - &["Cancel"], - cx, - ); - } - vim.update_editor(cx, |vim, editor, cx| match action { - DeleteMarks::Marks(s) => { - if s.starts_with('-') || s.ends_with('-') || s.contains(['\'', '`']) { - err(s.clone(), window, cx); - return; - } - - let to_delete = if s.len() < 3 { - Some(s.clone()) - } else { - s.chars() - .tuple_windows::<(_, _, _)>() - .map(|(a, b, c)| { - if b == '-' { - if match a { - 'a'..='z' => a <= c && c <= 'z', - 'A'..='Z' => a <= c && c <= 'Z', - '0'..='9' => a <= c && c <= '9', - _ => false, - } { - Some((a..=c).collect_vec()) - } else { - None - } - } else if a == '-' { - if c == '-' { None } else { Some(vec![c]) } - } else if c == '-' { - if a == '-' { None } else { Some(vec![a]) } - } else { - Some(vec![a, b, c]) - } - }) - .fold_options(HashSet::::default(), |mut set, chars| { - set.extend(chars.iter().copied()); - set - }) - .map(|set| set.iter().collect::()) - }; - - let Some(to_delete) = to_delete else { - err(s.clone(), window, cx); - return; - }; - - for c in to_delete.chars().filter(|c| !c.is_whitespace()) { - vim.delete_mark(c.to_string(), editor, window, cx); - } - } - DeleteMarks::AllLocal => { - for s in 'a'..='z' { - vim.delete_mark(s.to_string(), editor, window, cx); - } - } - }); - }); - - Vim::action(editor, cx, |vim, action: &VimEdit, window, cx| { - vim.update_editor(cx, |vim, editor, cx| { - let Some(workspace) = vim.workspace(window) else { - return; - }; - let Some(project) = editor.project().cloned() else { - return; - }; - let Some(worktree) = project.read(cx).visible_worktrees(cx).next() else { - return; - }; - let path_style = worktree.read(cx).path_style(); - let Some(path) = RelPath::new(Path::new(&action.filename), path_style).log_err() else { - return; - }; - let project_path = ProjectPath { - worktree_id: worktree.read(cx).id(), - path: path.into_arc(), - }; - - let _ = workspace.update(cx, |workspace, cx| { - workspace - .open_path(project_path, None, true, window, cx) - .detach_and_log_err(cx); - }); - }); - }); - - Vim::action(editor, cx, |vim, action: &VimNorm, window, cx| { - let keystrokes = action - .command - .chars() - .map(|c| Keystroke::parse(&c.to_string()).unwrap()) - .collect(); - vim.switch_mode(Mode::Normal, true, window, cx); - let initial_selections = - vim.update_editor(cx, |_, editor, _| editor.selections.disjoint_anchors_arc()); - if let Some(range) = &action.range { - let result = vim.update_editor(cx, |vim, editor, cx| { - let range = range.buffer_range(vim, editor, window, cx)?; - editor.change_selections( - SelectionEffects::no_scroll().nav_history(false), - window, - cx, - |s| { - s.select_ranges( - (range.start.0..=range.end.0) - .map(|line| Point::new(line, 0)..Point::new(line, 0)), - ); - }, - ); - anyhow::Ok(()) - }); - if let Some(Err(err)) = result { - log::error!("Error selecting range: {}", err); - return; - } - }; - - let Some(workspace) = vim.workspace(window) else { - return; - }; - let task = workspace.update(cx, |workspace, cx| { - workspace.send_keystrokes_impl(keystrokes, window, cx) - }); - let had_range = action.range.is_some(); - - cx.spawn_in(window, async move |vim, cx| { - task.await; - vim.update_in(cx, |vim, window, cx| { - vim.update_editor(cx, |_, editor, cx| { - if had_range { - editor.change_selections(SelectionEffects::default(), window, cx, |s| { - s.select_anchor_ranges([s.newest_anchor().range()]); - }) - } - }); - if matches!(vim.mode, Mode::Insert | Mode::Replace) { - vim.normal_before(&Default::default(), window, cx); - } else { - vim.switch_mode(Mode::Normal, true, window, cx); - } - vim.update_editor(cx, |_, editor, cx| { - if let Some(first_sel) = initial_selections - && let Some(tx_id) = editor - .buffer() - .update(cx, |multi, cx| multi.last_transaction_id(cx)) - { - let last_sel = editor.selections.disjoint_anchors_arc(); - editor.modify_transaction_selection_history(tx_id, |old| { - old.0 = first_sel; - old.1 = Some(last_sel); - }); - } - }); - }) - .ok(); - }) - .detach(); - }); - - Vim::action(editor, cx, |vim, _: &CountCommand, window, cx| { - let Some(workspace) = vim.workspace(window) else { - return; - }; - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - let n = if count > 1 { - format!(".,.+{}", count.saturating_sub(1)) - } else { - ".".to_string() - }; - workspace.update(cx, |workspace, cx| { - command_palette::CommandPalette::toggle(workspace, &n, window, cx); - }) - }); - - Vim::action(editor, cx, |vim, action: &GoToLine, window, cx| { - vim.switch_mode(Mode::Normal, false, window, cx); - let result = vim.update_editor(cx, |vim, editor, cx| { - let snapshot = editor.snapshot(window, cx); - let buffer_row = action.range.head().buffer_row(vim, editor, window, cx)?; - let current = editor - .selections - .newest::(&editor.display_snapshot(cx)); - let target = snapshot - .buffer_snapshot() - .clip_point(Point::new(buffer_row.0, current.head().column), Bias::Left); - editor.change_selections(Default::default(), window, cx, |s| { - s.select_ranges([target..target]); - }); - - anyhow::Ok(()) - }); - if let Some(e @ Err(_)) = result { - let Some(workspace) = vim.workspace(window) else { - return; - }; - workspace.update(cx, |workspace, cx| { - e.notify_err(workspace, cx); - }); - } - }); - - Vim::action(editor, cx, |vim, action: &YankCommand, window, cx| { - vim.update_editor(cx, |vim, editor, cx| { - let snapshot = editor.snapshot(window, cx); - if let Ok(range) = action.range.buffer_range(vim, editor, window, cx) { - let end = if range.end < snapshot.buffer_snapshot().max_row() { - Point::new(range.end.0 + 1, 0) - } else { - snapshot.buffer_snapshot().max_point() - }; - vim.copy_ranges( - editor, - MotionKind::Linewise, - true, - vec![Point::new(range.start.0, 0)..end], - window, - cx, - ) - } - }); - }); - - Vim::action(editor, cx, |_, action: &WithCount, window, cx| { - for _ in 0..action.count { - window.dispatch_action(action.action.boxed_clone(), cx) - } - }); - - Vim::action(editor, cx, |vim, action: &WithRange, window, cx| { - let result = vim.update_editor(cx, |vim, editor, cx| { - action.range.buffer_range(vim, editor, window, cx) - }); - - let range = match result { - None => return, - Some(e @ Err(_)) => { - let Some(workspace) = vim.workspace(window) else { - return; - }; - workspace.update(cx, |workspace, cx| { - e.notify_err(workspace, cx); - }); - return; - } - Some(Ok(result)) => result, - }; - - let previous_selections = vim - .update_editor(cx, |_, editor, cx| { - let selections = action.restore_selection.then(|| { - editor - .selections - .disjoint_anchor_ranges() - .collect::>() - }); - let snapshot = editor.buffer().read(cx).snapshot(cx); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - let end = Point::new(range.end.0, snapshot.line_len(range.end)); - s.select_ranges([end..Point::new(range.start.0, 0)]); - }); - selections - }) - .flatten(); - window.dispatch_action(action.action.boxed_clone(), cx); - cx.defer_in(window, move |vim, window, cx| { - vim.update_editor(cx, |_, editor, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - if let Some(previous_selections) = previous_selections { - s.select_ranges(previous_selections); - } else { - s.select_ranges([ - Point::new(range.start.0, 0)..Point::new(range.start.0, 0) - ]); - } - }) - }); - }); - }); - - Vim::action(editor, cx, |vim, action: &OnMatchingLines, window, cx| { - action.run(vim, window, cx) - }); - - Vim::action(editor, cx, |vim, action: &ShellExec, window, cx| { - action.run(vim, window, cx) - }) -} - -#[derive(Default)] -struct VimCommand { - prefix: &'static str, - suffix: &'static str, - action: Option>, - action_name: Option<&'static str>, - bang_action: Option>, - args: Option< - Box, String) -> Option> + Send + Sync + 'static>, - >, - /// Optional range Range to use if no range is specified. - default_range: Option, - range: Option< - Box< - dyn Fn(Box, &CommandRange) -> Option> - + Send - + Sync - + 'static, - >, - >, - has_count: bool, - has_filename: bool, -} - -struct ParsedQuery { - args: String, - has_bang: bool, - has_space: bool, -} - -impl VimCommand { - fn new(pattern: (&'static str, &'static str), action: impl Action) -> Self { - Self { - prefix: pattern.0, - suffix: pattern.1, - action: Some(action.boxed_clone()), - ..Default::default() - } - } - - // from_str is used for actions in other crates. - fn str(pattern: (&'static str, &'static str), action_name: &'static str) -> Self { - Self { - prefix: pattern.0, - suffix: pattern.1, - action_name: Some(action_name), - ..Default::default() - } - } - - fn bang(mut self, bang_action: impl Action) -> Self { - self.bang_action = Some(bang_action.boxed_clone()); - self - } - - fn args( - mut self, - f: impl Fn(Box, String) -> Option> + Send + Sync + 'static, - ) -> Self { - self.args = Some(Box::new(f)); - self - } - - fn filename( - mut self, - f: impl Fn(Box, String) -> Option> + Send + Sync + 'static, - ) -> Self { - self.args = Some(Box::new(f)); - self.has_filename = true; - self - } - - fn range( - mut self, - f: impl Fn(Box, &CommandRange) -> Option> + Send + Sync + 'static, - ) -> Self { - self.range = Some(Box::new(f)); - self - } - - fn default_range(mut self, range: CommandRange) -> Self { - self.default_range = Some(range); - self - } - - fn count(mut self) -> Self { - self.has_count = true; - self - } - - fn generate_filename_completions( - parsed_query: &ParsedQuery, - workspace: WeakEntity, - cx: &mut App, - ) -> Task> { - let ParsedQuery { - args, - has_bang: _, - has_space: _, - } = parsed_query; - let Some(workspace) = workspace.upgrade() else { - return Task::ready(Vec::new()); - }; - - let (task, args_path) = workspace.update(cx, |workspace, cx| { - let prefix = workspace - .project() - .read(cx) - .visible_worktrees(cx) - .map(|worktree| worktree.read(cx).abs_path().to_path_buf()) - .next() - .or_else(std::env::home_dir) - .unwrap_or_else(|| PathBuf::from("")); - - let rel_path = match RelPath::new(Path::new(&args), PathStyle::local()) { - Ok(path) => path.to_rel_path_buf(), - Err(_) => { - return (Task::ready(Ok(Vec::new())), RelPathBuf::new()); - } - }; - - let rel_path = if args.ends_with(PathStyle::local().primary_separator()) { - rel_path - } else { - rel_path - .parent() - .map(|rel_path| rel_path.to_rel_path_buf()) - .unwrap_or(RelPathBuf::new()) - }; - - let task = workspace.project().update(cx, |project, cx| { - let path = prefix - .join(rel_path.as_std_path()) - .to_string_lossy() - .to_string(); - project.list_directory(path, cx) - }); - - (task, rel_path) - }); - - cx.background_spawn(async move { - let directories = task.await.unwrap_or_default(); - directories - .iter() - .map(|dir| { - let path = RelPath::new(dir.path.as_path(), PathStyle::local()) - .map(|cow| cow.into_owned()) - .unwrap_or(RelPathBuf::new()); - let mut path_string = args_path - .join(&path) - .display(PathStyle::local()) - .to_string(); - if dir.is_dir { - path_string.push_str(PathStyle::local().primary_separator()); - } - path_string - }) - .collect() - }) - } - - fn get_parsed_query(&self, query: String) -> Option { - let rest = query - .strip_prefix(self.prefix)? - .to_string() - .chars() - .zip_longest(self.suffix.to_string().chars()) - .skip_while(|e| e.clone().both().map(|(s, q)| s == q).unwrap_or(false)) - .filter_map(|e| e.left()) - .collect::(); - let has_bang = rest.starts_with('!'); - let has_space = rest.starts_with("! ") || rest.starts_with(' '); - let args = if has_bang { - rest.strip_prefix('!')?.trim().to_string() - } else if rest.is_empty() { - "".into() - } else { - rest.strip_prefix(' ')?.trim().to_string() - }; - Some(ParsedQuery { - args, - has_bang, - has_space, - }) - } - - fn parse( - &self, - query: &str, - range: &Option, - cx: &App, - ) -> Option> { - let ParsedQuery { - args, - has_bang, - has_space: _, - } = self.get_parsed_query(query.to_string())?; - let action = if has_bang && self.bang_action.is_some() { - self.bang_action.as_ref().unwrap().boxed_clone() - } else if let Some(action) = self.action.as_ref() { - action.boxed_clone() - } else if let Some(action_name) = self.action_name { - cx.build_action(action_name, None).log_err()? - } else { - return None; - }; - - let action = if args.is_empty() { - action - } else { - // if command does not accept args and we have args then we should do no action - self.args.as_ref()?(action, args)? - }; - - let range = range.as_ref().or(self.default_range.as_ref()); - if let Some(range) = range { - self.range.as_ref().and_then(|f| f(action, range)) - } else { - Some(action) - } - } - - // TODO: ranges with search queries - fn parse_range(query: &str) -> (Option, String) { - let mut chars = query.chars().peekable(); - - match chars.peek() { - Some('%') => { - chars.next(); - return ( - Some(CommandRange { - start: Position::Line { row: 1, offset: 0 }, - end: Some(Position::LastLine { offset: 0 }), - }), - chars.collect(), - ); - } - Some('*') => { - chars.next(); - return ( - Some(CommandRange { - start: Position::Mark { - name: '<', - offset: 0, - }, - end: Some(Position::Mark { - name: '>', - offset: 0, - }), - }), - chars.collect(), - ); - } - _ => {} - } - - let start = Self::parse_position(&mut chars); - - match chars.peek() { - Some(',' | ';') => { - chars.next(); - ( - Some(CommandRange { - start: start.unwrap_or(Position::CurrentLine { offset: 0 }), - end: Self::parse_position(&mut chars), - }), - chars.collect(), - ) - } - _ => ( - start.map(|start| CommandRange { start, end: None }), - chars.collect(), - ), - } - } - - fn parse_position(chars: &mut Peekable) -> Option { - match chars.peek()? { - '0'..='9' => { - let row = Self::parse_u32(chars); - Some(Position::Line { - row, - offset: Self::parse_offset(chars), - }) - } - '\'' => { - chars.next(); - let name = chars.next()?; - Some(Position::Mark { - name, - offset: Self::parse_offset(chars), - }) - } - '.' => { - chars.next(); - Some(Position::CurrentLine { - offset: Self::parse_offset(chars), - }) - } - '+' | '-' => Some(Position::CurrentLine { - offset: Self::parse_offset(chars), - }), - '$' => { - chars.next(); - Some(Position::LastLine { - offset: Self::parse_offset(chars), - }) - } - _ => None, - } - } - - fn parse_offset(chars: &mut Peekable) -> i32 { - let mut res: i32 = 0; - while matches!(chars.peek(), Some('+' | '-')) { - let sign = if chars.next().unwrap() == '+' { 1 } else { -1 }; - let amount = if matches!(chars.peek(), Some('0'..='9')) { - (Self::parse_u32(chars) as i32).saturating_mul(sign) - } else { - sign - }; - res = res.saturating_add(amount) - } - res - } - - fn parse_u32(chars: &mut Peekable) -> u32 { - let mut res: u32 = 0; - while matches!(chars.peek(), Some('0'..='9')) { - res = res - .saturating_mul(10) - .saturating_add(chars.next().unwrap() as u32 - '0' as u32); - } - res - } -} - -#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq)] -enum Position { - Line { row: u32, offset: i32 }, - Mark { name: char, offset: i32 }, - LastLine { offset: i32 }, - CurrentLine { offset: i32 }, -} - -impl Position { - fn buffer_row( - &self, - vim: &Vim, - editor: &mut Editor, - window: &mut Window, - cx: &mut App, - ) -> Result { - let snapshot = editor.snapshot(window, cx); - let target = match self { - Position::Line { row, offset } => { - if let Some(anchor) = editor.active_excerpt(cx).and_then(|(_, buffer, _)| { - editor.buffer().read(cx).buffer_point_to_anchor( - &buffer, - Point::new(row.saturating_sub(1), 0), - cx, - ) - }) { - anchor - .to_point(&snapshot.buffer_snapshot()) - .row - .saturating_add_signed(*offset) - } else { - row.saturating_add_signed(offset.saturating_sub(1)) - } - } - Position::Mark { name, offset } => { - let Some(Mark::Local(anchors)) = - vim.get_mark(&name.to_string(), editor, window, cx) - else { - anyhow::bail!("mark {name} not set"); - }; - let Some(mark) = anchors.last() else { - anyhow::bail!("mark {name} contains empty anchors"); - }; - mark.to_point(&snapshot.buffer_snapshot()) - .row - .saturating_add_signed(*offset) - } - Position::LastLine { offset } => snapshot - .buffer_snapshot() - .max_row() - .0 - .saturating_add_signed(*offset), - Position::CurrentLine { offset } => editor - .selections - .newest_anchor() - .head() - .to_point(&snapshot.buffer_snapshot()) - .row - .saturating_add_signed(*offset), - }; - - Ok(MultiBufferRow(target).min(snapshot.buffer_snapshot().max_row())) - } -} - -#[derive(Clone, Debug, PartialEq)] -pub(crate) struct CommandRange { - start: Position, - end: Option, -} - -impl CommandRange { - fn head(&self) -> &Position { - self.end.as_ref().unwrap_or(&self.start) - } - - /// Convert the `CommandRange` into a `Range`. - pub(crate) fn buffer_range( - &self, - vim: &Vim, - editor: &mut Editor, - window: &mut Window, - cx: &mut App, - ) -> Result> { - let start = self.start.buffer_row(vim, editor, window, cx)?; - let end = if let Some(end) = self.end.as_ref() { - end.buffer_row(vim, editor, window, cx)? - } else { - start - }; - if end < start { - anyhow::Ok(end..start) - } else { - anyhow::Ok(start..end) - } - } - - pub fn as_count(&self) -> Option { - if let CommandRange { - start: Position::Line { row, offset: 0 }, - end: None, - } = &self - { - Some(*row) - } else { - None - } - } - - /// The `CommandRange` representing the entire buffer. - fn buffer() -> Self { - Self { - start: Position::Line { row: 1, offset: 0 }, - end: Some(Position::LastLine { offset: 0 }), - } - } -} - -fn generate_commands(_: &App) -> Vec { - vec![ - VimCommand::new( - ("w", "rite"), - VimSave { - save_intent: Some(SaveIntent::Save), - filename: "".into(), - range: None, - }, - ) - .bang(VimSave { - save_intent: Some(SaveIntent::Overwrite), - filename: "".into(), - range: None, - }) - .filename(|action, filename| { - Some( - VimSave { - save_intent: action - .as_any() - .downcast_ref::() - .and_then(|action| action.save_intent), - filename, - range: None, - } - .boxed_clone(), - ) - }) - .range(|action, range| { - let mut action: VimSave = action.as_any().downcast_ref::().unwrap().clone(); - action.range.replace(range.clone()); - Some(Box::new(action)) - }), - VimCommand::new(("e", "dit"), editor::actions::ReloadFile) - .bang(editor::actions::ReloadFile) - .filename(|_, filename| Some(VimEdit { filename }.boxed_clone())), - VimCommand::new(("sp", "lit"), workspace::SplitHorizontal).filename(|_, filename| { - Some( - VimSplit { - vertical: false, - filename, - } - .boxed_clone(), - ) - }), - VimCommand::new(("vs", "plit"), workspace::SplitVertical).filename(|_, filename| { - Some( - VimSplit { - vertical: true, - filename, - } - .boxed_clone(), - ) - }), - VimCommand::new(("tabe", "dit"), workspace::NewFile) - .filename(|_action, filename| Some(VimEdit { filename }.boxed_clone())), - VimCommand::new(("tabnew", ""), workspace::NewFile) - .filename(|_action, filename| Some(VimEdit { filename }.boxed_clone())), - VimCommand::new( - ("q", "uit"), - workspace::CloseActiveItem { - save_intent: Some(SaveIntent::Close), - close_pinned: false, - }, - ) - .bang(workspace::CloseActiveItem { - save_intent: Some(SaveIntent::Skip), - close_pinned: true, - }), - VimCommand::new( - ("wq", ""), - workspace::CloseActiveItem { - save_intent: Some(SaveIntent::Save), - close_pinned: false, - }, - ) - .bang(workspace::CloseActiveItem { - save_intent: Some(SaveIntent::Overwrite), - close_pinned: true, - }), - VimCommand::new( - ("x", "it"), - workspace::CloseActiveItem { - save_intent: Some(SaveIntent::SaveAll), - close_pinned: false, - }, - ) - .bang(workspace::CloseActiveItem { - save_intent: Some(SaveIntent::Overwrite), - close_pinned: true, - }), - VimCommand::new( - ("exi", "t"), - workspace::CloseActiveItem { - save_intent: Some(SaveIntent::SaveAll), - close_pinned: false, - }, - ) - .bang(workspace::CloseActiveItem { - save_intent: Some(SaveIntent::Overwrite), - close_pinned: true, - }), - VimCommand::new( - ("up", "date"), - workspace::Save { - save_intent: Some(SaveIntent::SaveAll), - }, - ), - VimCommand::new( - ("wa", "ll"), - workspace::SaveAll { - save_intent: Some(SaveIntent::SaveAll), - }, - ) - .bang(workspace::SaveAll { - save_intent: Some(SaveIntent::Overwrite), - }), - VimCommand::new( - ("qa", "ll"), - workspace::CloseAllItemsAndPanes { - save_intent: Some(SaveIntent::Close), - }, - ) - .bang(workspace::CloseAllItemsAndPanes { - save_intent: Some(SaveIntent::Skip), - }), - VimCommand::new( - ("quita", "ll"), - workspace::CloseAllItemsAndPanes { - save_intent: Some(SaveIntent::Close), - }, - ) - .bang(workspace::CloseAllItemsAndPanes { - save_intent: Some(SaveIntent::Skip), - }), - VimCommand::new( - ("xa", "ll"), - workspace::CloseAllItemsAndPanes { - save_intent: Some(SaveIntent::SaveAll), - }, - ) - .bang(workspace::CloseAllItemsAndPanes { - save_intent: Some(SaveIntent::Overwrite), - }), - VimCommand::new( - ("wqa", "ll"), - workspace::CloseAllItemsAndPanes { - save_intent: Some(SaveIntent::SaveAll), - }, - ) - .bang(workspace::CloseAllItemsAndPanes { - save_intent: Some(SaveIntent::Overwrite), - }), - VimCommand::new(("cq", "uit"), zed_actions::Quit), - VimCommand::new( - ("bd", "elete"), - workspace::CloseActiveItem { - save_intent: Some(SaveIntent::Close), - close_pinned: false, - }, - ) - .bang(workspace::CloseActiveItem { - save_intent: Some(SaveIntent::Skip), - close_pinned: true, - }), - VimCommand::new( - ("norm", "al"), - VimNorm { - command: "".into(), - range: None, - }, - ) - .args(|_, args| { - Some( - VimNorm { - command: args, - range: None, - } - .boxed_clone(), - ) - }) - .range(|action, range| { - let mut action: VimNorm = action.as_any().downcast_ref::().unwrap().clone(); - action.range.replace(range.clone()); - Some(Box::new(action)) - }), - VimCommand::new(("bn", "ext"), workspace::ActivateNextItem).count(), - VimCommand::new(("bN", "ext"), workspace::ActivatePreviousItem).count(), - VimCommand::new(("bp", "revious"), workspace::ActivatePreviousItem).count(), - VimCommand::new(("bf", "irst"), workspace::ActivateItem(0)), - VimCommand::new(("br", "ewind"), workspace::ActivateItem(0)), - VimCommand::new(("bl", "ast"), workspace::ActivateLastItem), - VimCommand::str(("buffers", ""), "tab_switcher::ToggleAll"), - VimCommand::str(("ls", ""), "tab_switcher::ToggleAll"), - VimCommand::new(("new", ""), workspace::NewFileSplitHorizontal), - VimCommand::new(("vne", "w"), workspace::NewFileSplitVertical), - VimCommand::new(("tabn", "ext"), workspace::ActivateNextItem).count(), - VimCommand::new(("tabp", "revious"), workspace::ActivatePreviousItem).count(), - VimCommand::new(("tabN", "ext"), workspace::ActivatePreviousItem).count(), - VimCommand::new( - ("tabc", "lose"), - workspace::CloseActiveItem { - save_intent: Some(SaveIntent::Close), - close_pinned: false, - }, - ), - VimCommand::new( - ("tabo", "nly"), - workspace::CloseOtherItems { - save_intent: Some(SaveIntent::Close), - close_pinned: false, - }, - ) - .bang(workspace::CloseOtherItems { - save_intent: Some(SaveIntent::Skip), - close_pinned: false, - }), - VimCommand::new( - ("on", "ly"), - workspace::CloseInactiveTabsAndPanes { - save_intent: Some(SaveIntent::Close), - }, - ) - .bang(workspace::CloseInactiveTabsAndPanes { - save_intent: Some(SaveIntent::Skip), - }), - VimCommand::str(("cl", "ist"), "diagnostics::Deploy"), - VimCommand::new(("cc", ""), editor::actions::Hover), - VimCommand::new(("ll", ""), editor::actions::Hover), - VimCommand::new(("cn", "ext"), editor::actions::GoToDiagnostic::default()) - .range(wrap_count), - VimCommand::new( - ("cp", "revious"), - editor::actions::GoToPreviousDiagnostic::default(), - ) - .range(wrap_count), - VimCommand::new( - ("cN", "ext"), - editor::actions::GoToPreviousDiagnostic::default(), - ) - .range(wrap_count), - VimCommand::new( - ("lp", "revious"), - editor::actions::GoToPreviousDiagnostic::default(), - ) - .range(wrap_count), - VimCommand::new( - ("lN", "ext"), - editor::actions::GoToPreviousDiagnostic::default(), - ) - .range(wrap_count), - VimCommand::new(("j", "oin"), JoinLines).range(select_range), - VimCommand::new(("fo", "ld"), editor::actions::FoldSelectedRanges).range(act_on_range), - VimCommand::new(("foldo", "pen"), editor::actions::UnfoldLines) - .bang(editor::actions::UnfoldRecursive) - .range(act_on_range), - VimCommand::new(("foldc", "lose"), editor::actions::Fold) - .bang(editor::actions::FoldRecursive) - .range(act_on_range), - VimCommand::new(("dif", "fupdate"), editor::actions::ToggleSelectedDiffHunks) - .range(act_on_range), - VimCommand::str(("rev", "ert"), "git::Restore").range(act_on_range), - VimCommand::new(("d", "elete"), VisualDeleteLine).range(select_range), - VimCommand::new(("y", "ank"), gpui::NoAction).range(|_, range| { - Some( - YankCommand { - range: range.clone(), - } - .boxed_clone(), - ) - }), - VimCommand::new(("reg", "isters"), ToggleRegistersView).bang(ToggleRegistersView), - VimCommand::new(("di", "splay"), ToggleRegistersView).bang(ToggleRegistersView), - VimCommand::new(("marks", ""), ToggleMarksView).bang(ToggleMarksView), - VimCommand::new(("delm", "arks"), ArgumentRequired) - .bang(DeleteMarks::AllLocal) - .args(|_, args| Some(DeleteMarks::Marks(args).boxed_clone())), - VimCommand::new(("sor", "t"), SortLinesCaseSensitive) - .range(select_range) - .default_range(CommandRange::buffer()), - VimCommand::new(("sort i", ""), SortLinesCaseInsensitive) - .range(select_range) - .default_range(CommandRange::buffer()), - VimCommand::str(("E", "xplore"), "project_panel::ToggleFocus"), - VimCommand::str(("H", "explore"), "project_panel::ToggleFocus"), - VimCommand::str(("L", "explore"), "project_panel::ToggleFocus"), - VimCommand::str(("S", "explore"), "project_panel::ToggleFocus"), - VimCommand::str(("Ve", "xplore"), "project_panel::ToggleFocus"), - VimCommand::str(("te", "rm"), "terminal_panel::Toggle"), - VimCommand::str(("T", "erm"), "terminal_panel::Toggle"), - VimCommand::str(("C", "ollab"), "collab_panel::ToggleFocus"), - VimCommand::str(("No", "tifications"), "notification_panel::ToggleFocus"), - VimCommand::str(("A", "I"), "agent::ToggleFocus"), - VimCommand::str(("G", "it"), "git_panel::ToggleFocus"), - VimCommand::str(("D", "ebug"), "debug_panel::ToggleFocus"), - VimCommand::new(("noh", "lsearch"), search::buffer_search::Dismiss), - VimCommand::new(("$", ""), EndOfDocument), - VimCommand::new(("%", ""), EndOfDocument), - VimCommand::new(("0", ""), StartOfDocument), - VimCommand::new(("ex", ""), editor::actions::ReloadFile).bang(editor::actions::ReloadFile), - VimCommand::new(("cpp", "link"), editor::actions::CopyPermalinkToLine).range(act_on_range), - VimCommand::str(("opt", "ions"), "zed::OpenDefaultSettings"), - VimCommand::str(("map", ""), "vim::OpenDefaultKeymap"), - VimCommand::new(("h", "elp"), OpenDocs), - ] -} - -struct VimCommands(Vec); -// safety: we only ever access this from the main thread (as ensured by the cx argument) -// actions are not Sync so we can't otherwise use a OnceLock. -unsafe impl Sync for VimCommands {} -impl Global for VimCommands {} - -fn commands(cx: &App) -> &Vec { - static COMMANDS: OnceLock = OnceLock::new(); - &COMMANDS - .get_or_init(|| VimCommands(generate_commands(cx))) - .0 -} - -fn act_on_range(action: Box, range: &CommandRange) -> Option> { - Some( - WithRange { - restore_selection: true, - range: range.clone(), - action: WrappedAction(action), - } - .boxed_clone(), - ) -} - -fn select_range(action: Box, range: &CommandRange) -> Option> { - Some( - WithRange { - restore_selection: false, - range: range.clone(), - action: WrappedAction(action), - } - .boxed_clone(), - ) -} - -fn wrap_count(action: Box, range: &CommandRange) -> Option> { - range.as_count().map(|count| { - WithCount { - count, - action: WrappedAction(action), - } - .boxed_clone() - }) -} - -pub fn command_interceptor( - mut input: &str, - workspace: WeakEntity, - cx: &mut App, -) -> Task { - while input.starts_with(':') { - input = &input[1..]; - } - - let (range, query) = VimCommand::parse_range(input); - let range_prefix = input[0..(input.len() - query.len())].to_string(); - let has_trailing_space = query.ends_with(" "); - let mut query = query.as_str().trim(); - - let on_matching_lines = (query.starts_with('g') || query.starts_with('v')) - .then(|| { - let (pattern, range, search, invert) = OnMatchingLines::parse(query, &range)?; - let start_idx = query.len() - pattern.len(); - query = query[start_idx..].trim(); - Some((range, search, invert)) - }) - .flatten(); - - let mut action = if range.is_some() && query.is_empty() { - Some( - GoToLine { - range: range.clone().unwrap(), - } - .boxed_clone(), - ) - } else if query.starts_with('/') || query.starts_with('?') { - Some( - FindCommand { - query: query[1..].to_string(), - backwards: query.starts_with('?'), - } - .boxed_clone(), - ) - } else if query.starts_with("se ") || query.starts_with("set ") { - let (prefix, option) = query.split_once(' ').unwrap(); - let mut commands = VimOption::possible_commands(option); - if !commands.is_empty() { - let query = prefix.to_string() + " " + option; - for command in &mut commands { - command.positions = generate_positions(&command.string, &query); - } - } - return Task::ready(CommandInterceptResult { - results: commands, - exclusive: false, - }); - } else if query.starts_with('s') { - let mut substitute = "substitute".chars().peekable(); - let mut query = query.chars().peekable(); - while substitute - .peek() - .is_some_and(|char| Some(char) == query.peek()) - { - substitute.next(); - query.next(); - } - if let Some(replacement) = Replacement::parse(query) { - let range = range.clone().unwrap_or(CommandRange { - start: Position::CurrentLine { offset: 0 }, - end: None, - }); - Some(ReplaceCommand { replacement, range }.boxed_clone()) - } else { - None - } - } else if query.contains('!') { - ShellExec::parse(query, range.clone()) - } else if on_matching_lines.is_some() { - commands(cx) - .iter() - .find_map(|command| command.parse(query, &range, cx)) - } else { - None - }; - - if let Some((range, search, invert)) = on_matching_lines - && let Some(ref inner) = action - { - action = Some(Box::new(OnMatchingLines { - range, - search, - action: WrappedAction(inner.boxed_clone()), - invert, - })); - }; - - if let Some(action) = action { - let string = input.to_string(); - let positions = generate_positions(&string, &(range_prefix + query)); - return Task::ready(CommandInterceptResult { - results: vec![CommandInterceptItem { - action, - string, - positions, - }], - exclusive: false, - }); - } - - let Some((mut results, filenames)) = - commands(cx).iter().enumerate().find_map(|(idx, command)| { - let action = command.parse(query, &range, cx)?; - let parsed_query = command.get_parsed_query(query.into())?; - let display_string = ":".to_owned() - + &range_prefix - + command.prefix - + command.suffix - + if parsed_query.has_bang { "!" } else { "" }; - let space = if parsed_query.has_space { " " } else { "" }; - - let string = format!("{}{}{}", &display_string, &space, &parsed_query.args); - let positions = generate_positions(&string, &(range_prefix.clone() + query)); - - let results = vec![CommandInterceptItem { - action, - string, - positions, - }]; - - let no_args_positions = - generate_positions(&display_string, &(range_prefix.clone() + query)); - - // The following are valid autocomplete scenarios: - // :w!filename.txt - // :w filename.txt - // :w[space] - if !command.has_filename - || (!has_trailing_space && !parsed_query.has_bang && parsed_query.args.is_empty()) - { - return Some((results, None)); - } - - Some(( - results, - Some((idx, parsed_query, display_string, no_args_positions)), - )) - }) - else { - return Task::ready(CommandInterceptResult::default()); - }; - - if let Some((cmd_idx, parsed_query, display_string, no_args_positions)) = filenames { - let filenames = VimCommand::generate_filename_completions(&parsed_query, workspace, cx); - cx.spawn(async move |cx| { - let filenames = filenames.await; - const MAX_RESULTS: usize = 100; - let executor = cx.background_executor().clone(); - let mut candidates = Vec::with_capacity(filenames.len()); - - for (idx, filename) in filenames.iter().enumerate() { - candidates.push(fuzzy::StringMatchCandidate::new(idx, &filename)); - } - let filenames = fuzzy::match_strings( - &candidates, - &parsed_query.args, - false, - true, - MAX_RESULTS, - &Default::default(), - executor, - ) - .await; - - for fuzzy::StringMatch { - candidate_id: _, - score: _, - positions, - string, - } in filenames - { - let offset = display_string.len() + 1; - let mut positions: Vec<_> = positions.iter().map(|&pos| pos + offset).collect(); - positions.splice(0..0, no_args_positions.clone()); - let string = format!("{display_string} {string}"); - let (range, query) = VimCommand::parse_range(&string[1..]); - let action = - match cx.update(|cx| commands(cx).get(cmd_idx)?.parse(&query, &range, cx)) { - Ok(Some(action)) => action, - _ => continue, - }; - results.push(CommandInterceptItem { - action, - string, - positions, - }); - } - CommandInterceptResult { - results, - exclusive: true, - } - }) - } else { - Task::ready(CommandInterceptResult { - results, - exclusive: false, - }) - } -} - -fn generate_positions(string: &str, query: &str) -> Vec { - let mut positions = Vec::new(); - let mut chars = query.chars(); - - let Some(mut current) = chars.next() else { - return positions; - }; - - for (i, c) in string.char_indices() { - if c == current { - positions.push(i); - if let Some(c) = chars.next() { - current = c; - } else { - break; - } - } - } - - positions -} - -/// Applies a command to all lines matching a pattern. -#[derive(Debug, PartialEq, Clone, Action)] -#[action(namespace = vim, no_json, no_register)] -pub(crate) struct OnMatchingLines { - range: CommandRange, - search: String, - action: WrappedAction, - invert: bool, -} - -impl OnMatchingLines { - // convert a vim query into something more usable by zed. - // we don't attempt to fully convert between the two regex syntaxes, - // but we do flip \( and \) to ( and ) (and vice-versa) in the pattern, - // and convert \0..\9 to $0..$9 in the replacement so that common idioms work. - pub(crate) fn parse( - query: &str, - range: &Option, - ) -> Option<(String, CommandRange, String, bool)> { - let mut global = "global".chars().peekable(); - let mut query_chars = query.chars().peekable(); - let mut invert = false; - if query_chars.peek() == Some(&'v') { - invert = true; - query_chars.next(); - } - while global - .peek() - .is_some_and(|char| Some(char) == query_chars.peek()) - { - global.next(); - query_chars.next(); - } - if !invert && query_chars.peek() == Some(&'!') { - invert = true; - query_chars.next(); - } - let range = range.clone().unwrap_or(CommandRange { - start: Position::Line { row: 0, offset: 0 }, - end: Some(Position::LastLine { offset: 0 }), - }); - - let delimiter = query_chars.next().filter(|c| { - !c.is_alphanumeric() && *c != '"' && *c != '|' && *c != '\'' && *c != '!' - })?; - - let mut search = String::new(); - let mut escaped = false; - - for c in query_chars.by_ref() { - if escaped { - escaped = false; - // unescape escaped parens - if c != '(' && c != ')' && c != delimiter { - search.push('\\') - } - search.push(c) - } else if c == '\\' { - escaped = true; - } else if c == delimiter { - break; - } else { - // escape unescaped parens - if c == '(' || c == ')' { - search.push('\\') - } - search.push(c) - } - } - - Some((query_chars.collect::(), range, search, invert)) - } - - pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context) { - let result = vim.update_editor(cx, |vim, editor, cx| { - self.range.buffer_range(vim, editor, window, cx) - }); - - let range = match result { - None => return, - Some(e @ Err(_)) => { - let Some(workspace) = vim.workspace(window) else { - return; - }; - workspace.update(cx, |workspace, cx| { - e.notify_err(workspace, cx); - }); - return; - } - Some(Ok(result)) => result, - }; - - let mut action = self.action.boxed_clone(); - let mut last_pattern = self.search.clone(); - - let mut regexes = match Regex::new(&self.search) { - Ok(regex) => vec![(regex, !self.invert)], - e @ Err(_) => { - let Some(workspace) = vim.workspace(window) else { - return; - }; - workspace.update(cx, |workspace, cx| { - e.notify_err(workspace, cx); - }); - return; - } - }; - while let Some(inner) = action - .boxed_clone() - .as_any() - .downcast_ref::() - { - let Some(regex) = Regex::new(&inner.search).ok() else { - break; - }; - last_pattern = inner.search.clone(); - action = inner.action.boxed_clone(); - regexes.push((regex, !inner.invert)) - } - - if let Some(pane) = vim.pane(window, cx) { - pane.update(cx, |pane, cx| { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() - { - search_bar.update(cx, |search_bar, cx| { - if search_bar.show(window, cx) { - let _ = search_bar.search( - &last_pattern, - Some(SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE), - false, - window, - cx, - ); - } - }); - } - }); - }; - - vim.update_editor(cx, |_, editor, cx| { - let snapshot = editor.snapshot(window, cx); - let mut row = range.start.0; - - let point_range = Point::new(range.start.0, 0) - ..snapshot - .buffer_snapshot() - .clip_point(Point::new(range.end.0 + 1, 0), Bias::Left); - cx.spawn_in(window, async move |editor, cx| { - let new_selections = cx - .background_spawn(async move { - let mut line = String::new(); - let mut new_selections = Vec::new(); - let chunks = snapshot - .buffer_snapshot() - .text_for_range(point_range) - .chain(["\n"]); - - for chunk in chunks { - for (newline_ix, text) in chunk.split('\n').enumerate() { - if newline_ix > 0 { - if regexes.iter().all(|(regex, should_match)| { - regex.is_match(&line) == *should_match - }) { - new_selections - .push(Point::new(row, 0).to_display_point(&snapshot)) - } - row += 1; - line.clear(); - } - line.push_str(text) - } - } - - new_selections - }) - .await; - - if new_selections.is_empty() { - return; - } - editor - .update_in(cx, |editor, window, cx| { - editor.start_transaction_at(Instant::now(), window, cx); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.replace_cursors_with(|_| new_selections); - }); - window.dispatch_action(action, cx); - cx.defer_in(window, move |editor, window, cx| { - let newest = editor - .selections - .newest::(&editor.display_snapshot(cx)); - editor.change_selections( - SelectionEffects::no_scroll(), - window, - cx, - |s| { - s.select(vec![newest]); - }, - ); - editor.end_transaction_at(Instant::now(), cx); - }) - }) - .ok(); - }) - .detach(); - }); - } -} - -/// Executes a shell command and returns the output. -#[derive(Clone, Debug, PartialEq, Action)] -#[action(namespace = vim, no_json, no_register)] -pub struct ShellExec { - command: String, - range: Option, - is_read: bool, -} - -impl Vim { - pub fn cancel_running_command(&mut self, window: &mut Window, cx: &mut Context) { - if self.running_command.take().is_some() { - self.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, _window, _cx| { - editor.clear_row_highlights::(); - }) - }); - } - } - - fn prepare_shell_command( - &mut self, - command: &str, - _: &mut Window, - cx: &mut Context, - ) -> String { - let mut ret = String::new(); - // N.B. non-standard escaping rules: - // * !echo % => "echo README.md" - // * !echo \% => "echo %" - // * !echo \\% => echo \% - // * !echo \\\% => echo \\% - for c in command.chars() { - if c != '%' && c != '!' { - ret.push(c); - continue; - } else if ret.chars().last() == Some('\\') { - ret.pop(); - ret.push(c); - continue; - } - match c { - '%' => { - self.update_editor(cx, |_, editor, cx| { - if let Some((_, buffer, _)) = editor.active_excerpt(cx) - && let Some(file) = buffer.read(cx).file() - && let Some(local) = file.as_local() - { - ret.push_str(&local.path().display(local.path_style(cx))); - } - }); - } - '!' => { - if let Some(command) = &self.last_command { - ret.push_str(command) - } - } - _ => {} - } - } - self.last_command = Some(ret.clone()); - ret - } - - pub fn shell_command_motion( - &mut self, - motion: Motion, - times: Option, - forced_motion: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - let Some(workspace) = self.workspace(window) else { - return; - }; - let command = self.update_editor(cx, |_, editor, cx| { - let snapshot = editor.snapshot(window, cx); - let start = editor - .selections - .newest_display(&editor.display_snapshot(cx)); - let text_layout_details = editor.text_layout_details(window); - let (mut range, _) = motion - .range( - &snapshot, - start.clone(), - times, - &text_layout_details, - forced_motion, - ) - .unwrap_or((start.range(), MotionKind::Exclusive)); - if range.start != start.start { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges([ - range.start.to_point(&snapshot)..range.start.to_point(&snapshot) - ]); - }) - } - if range.end.row() > range.start.row() && range.end.column() != 0 { - *range.end.row_mut() -= 1 - } - if range.end.row() == range.start.row() { - ".!".to_string() - } else { - format!(".,.+{}!", (range.end.row() - range.start.row()).0) - } - }); - if let Some(command) = command { - workspace.update(cx, |workspace, cx| { - command_palette::CommandPalette::toggle(workspace, &command, window, cx); - }); - } - } - - pub fn shell_command_object( - &mut self, - object: Object, - around: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - let Some(workspace) = self.workspace(window) else { - return; - }; - let command = self.update_editor(cx, |_, editor, cx| { - let snapshot = editor.snapshot(window, cx); - let start = editor - .selections - .newest_display(&editor.display_snapshot(cx)); - let range = object - .range(&snapshot, start.clone(), around, None) - .unwrap_or(start.range()); - if range.start != start.start { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges([ - range.start.to_point(&snapshot)..range.start.to_point(&snapshot) - ]); - }) - } - if range.end.row() == range.start.row() { - ".!".to_string() - } else { - format!(".,.+{}!", (range.end.row() - range.start.row()).0) - } - }); - if let Some(command) = command { - workspace.update(cx, |workspace, cx| { - command_palette::CommandPalette::toggle(workspace, &command, window, cx); - }); - } - } -} - -impl ShellExec { - pub fn parse(query: &str, range: Option) -> Option> { - let (before, after) = query.split_once('!')?; - let before = before.trim(); - - if !"read".starts_with(before) { - return None; - } - - Some( - ShellExec { - command: after.trim().to_string(), - range, - is_read: !before.is_empty(), - } - .boxed_clone(), - ) - } - - pub fn run(&self, vim: &mut Vim, window: &mut Window, cx: &mut Context) { - let Some(workspace) = vim.workspace(window) else { - return; - }; - - let project = workspace.read(cx).project().clone(); - let command = vim.prepare_shell_command(&self.command, window, cx); - - if self.range.is_none() && !self.is_read { - 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 = SpawnInTerminal { - id: TaskId("vim".to_string()), - full_label: command.clone(), - label: command.clone(), - command: Some(command.clone()), - args: Vec::new(), - command_label: command.clone(), - cwd, - env: HashMap::default(), - use_new_terminal: true, - allow_concurrent_runs: true, - reveal: RevealStrategy::NoFocus, - reveal_target: RevealTarget::Dock, - hide: HideStrategy::Never, - shell, - show_summary: false, - show_command: false, - show_rerun: false, - }; - - let task_status = workspace.spawn_in_terminal(spawn_in_terminal, window, cx); - cx.background_spawn(async move { - match task_status.await { - Some(Ok(status)) => { - if status.success() { - log::debug!("Vim shell exec succeeded"); - } else { - log::debug!("Vim shell exec failed, code: {:?}", status.code()); - } - } - Some(Err(e)) => log::error!("Vim shell exec failed: {e}"), - None => log::debug!("Vim shell exec got cancelled"), - } - }) - .detach(); - }); - return; - }; - - let mut input_snapshot = None; - let mut input_range = None; - let mut needs_newline_prefix = false; - vim.update_editor(cx, |vim, editor, cx| { - let snapshot = editor.buffer().read(cx).snapshot(cx); - let range = if let Some(range) = self.range.clone() { - let Some(range) = range.buffer_range(vim, editor, window, cx).log_err() else { - return; - }; - Point::new(range.start.0, 0) - ..snapshot.clip_point(Point::new(range.end.0 + 1, 0), Bias::Right) - } else { - let mut end = editor - .selections - .newest::(&editor.display_snapshot(cx)) - .range() - .end; - end = snapshot.clip_point(Point::new(end.row + 1, 0), Bias::Right); - needs_newline_prefix = end == snapshot.max_point(); - end..end - }; - if self.is_read { - input_range = - Some(snapshot.anchor_after(range.end)..snapshot.anchor_after(range.end)); - } else { - input_range = - Some(snapshot.anchor_before(range.start)..snapshot.anchor_after(range.end)); - } - editor.highlight_rows::( - input_range.clone().unwrap(), - cx.theme().status().unreachable_background, - Default::default(), - cx, - ); - - if !self.is_read { - input_snapshot = Some(snapshot) - } - }); - - let Some(range) = input_range else { return }; - - let process_task = project.update(cx, |project, cx| project.exec_in_shell(command, cx)); - - let is_read = self.is_read; - - let task = cx.spawn_in(window, async move |vim, cx| { - let Some(mut process) = process_task.await.log_err() else { - return; - }; - process.stdout(Stdio::piped()); - process.stderr(Stdio::piped()); - - if input_snapshot.is_some() { - process.stdin(Stdio::piped()); - } else { - process.stdin(Stdio::null()); - }; - - let Some(mut running) = process.spawn().log_err() else { - vim.update_in(cx, |vim, window, cx| { - vim.cancel_running_command(window, cx); - }) - .log_err(); - return; - }; - - if let Some(mut stdin) = running.stdin.take() - && let Some(snapshot) = input_snapshot - { - let range = range.clone(); - cx.background_spawn(async move { - for chunk in snapshot.text_for_range(range) { - if stdin.write_all(chunk.as_bytes()).await.log_err().is_none() { - return; - } - } - stdin.flush().await.log_err(); - }) - .detach(); - }; - - let output = cx.background_spawn(running.output()).await; - - let Some(output) = output.log_err() else { - vim.update_in(cx, |vim, window, cx| { - vim.cancel_running_command(window, cx); - }) - .log_err(); - return; - }; - let mut text = String::new(); - if needs_newline_prefix { - text.push('\n'); - } - text.push_str(&String::from_utf8_lossy(&output.stdout)); - text.push_str(&String::from_utf8_lossy(&output.stderr)); - if !text.is_empty() && text.chars().last() != Some('\n') { - text.push('\n'); - } - - vim.update_in(cx, |vim, window, cx| { - vim.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - editor.edit([(range.clone(), text)], cx); - let snapshot = editor.buffer().read(cx).snapshot(cx); - editor.change_selections(Default::default(), window, cx, |s| { - let point = if is_read { - let point = range.end.to_point(&snapshot); - Point::new(point.row.saturating_sub(1), 0) - } else { - let point = range.start.to_point(&snapshot); - Point::new(point.row, 0) - }; - s.select_ranges([point..point]); - }) - }) - }); - vim.cancel_running_command(window, cx); - }) - .log_err(); - }); - vim.running_command.replace(task); - } -} - -#[cfg(test)] -mod test { - use std::path::{Path, PathBuf}; - - use crate::{ - VimAddon, - state::Mode, - test::{NeovimBackedTestContext, VimTestContext}, - }; - use editor::{Editor, EditorSettings}; - use gpui::{Context, TestAppContext}; - use indoc::indoc; - use settings::Settings; - use util::path; - use workspace::{OpenOptions, Workspace}; - - #[gpui::test] - async fn test_command_basics(cx: &mut TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ˇa - b - c"}) - .await; - - cx.simulate_shared_keystrokes(": j enter").await; - - // hack: our cursor positioning after a join command is wrong - cx.simulate_shared_keystrokes("^").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇa b - c" - }); - } - - #[gpui::test] - async fn test_command_goto(cx: &mut TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ˇa - b - c"}) - .await; - cx.simulate_shared_keystrokes(": 3 enter").await; - cx.shared_state().await.assert_eq(indoc! {" - a - b - ˇc"}); - } - - #[gpui::test] - async fn test_command_replace(cx: &mut TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ˇa - b - b - c"}) - .await; - cx.simulate_shared_keystrokes(": % s / b / d enter").await; - cx.shared_state().await.assert_eq(indoc! {" - a - d - ˇd - c"}); - cx.simulate_shared_keystrokes(": % s : . : \\ 0 \\ 0 enter") - .await; - cx.shared_state().await.assert_eq(indoc! {" - aa - dd - dd - ˇcc"}); - cx.simulate_shared_keystrokes("k : s / d d / e e enter") - .await; - cx.shared_state().await.assert_eq(indoc! {" - aa - dd - ˇee - cc"}); - } - - #[gpui::test] - async fn test_command_search(cx: &mut TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ˇa - b - a - c"}) - .await; - cx.simulate_shared_keystrokes(": / b enter").await; - cx.shared_state().await.assert_eq(indoc! {" - a - ˇb - a - c"}); - cx.simulate_shared_keystrokes(": ? a enter").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇa - b - a - c"}); - } - - #[gpui::test] - async fn test_command_write(cx: &mut TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - let path = Path::new(path!("/root/dir/file.rs")); - let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone()); - - cx.simulate_keystrokes("i @ escape"); - cx.simulate_keystrokes(": w enter"); - - assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@\n"); - - fs.as_fake().insert_file(path, b"oops\n".to_vec()).await; - - // conflict! - cx.simulate_keystrokes("i @ escape"); - cx.simulate_keystrokes(": w enter"); - cx.simulate_prompt_answer("Cancel"); - - assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "oops\n"); - assert!(!cx.has_pending_prompt()); - cx.simulate_keystrokes(": w !"); - cx.simulate_keystrokes("enter"); - assert!(!cx.has_pending_prompt()); - assert_eq!(fs.load(path).await.unwrap().replace("\r\n", "\n"), "@@\n"); - } - - #[gpui::test] - async fn test_command_quit(cx: &mut TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.simulate_keystrokes(": n e w enter"); - cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2)); - cx.simulate_keystrokes(": q enter"); - cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 1)); - cx.simulate_keystrokes(": n e w enter"); - cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2)); - cx.simulate_keystrokes(": q a enter"); - cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 0)); - } - - #[gpui::test] - async fn test_offsets(cx: &mut TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇ1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n") - .await; - - cx.simulate_shared_keystrokes(": + enter").await; - cx.shared_state() - .await - .assert_eq("1\nˇ2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n"); - - cx.simulate_shared_keystrokes(": 1 0 - enter").await; - cx.shared_state() - .await - .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\nˇ9\n10\n11\n"); - - cx.simulate_shared_keystrokes(": . - 2 enter").await; - cx.shared_state() - .await - .assert_eq("1\n2\n3\n4\n5\n6\nˇ7\n8\n9\n10\n11\n"); - - cx.simulate_shared_keystrokes(": % enter").await; - cx.shared_state() - .await - .assert_eq("1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\nˇ"); - } - - #[gpui::test] - async fn test_command_ranges(cx: &mut TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await; - - cx.simulate_shared_keystrokes(": 2 , 4 d enter").await; - cx.shared_state().await.assert_eq("1\nˇ4\n3\n2\n1"); - - cx.simulate_shared_keystrokes(": 2 , 4 s o r t enter").await; - cx.shared_state().await.assert_eq("1\nˇ2\n3\n4\n1"); - - cx.simulate_shared_keystrokes(": 2 , 4 j o i n enter").await; - cx.shared_state().await.assert_eq("1\nˇ2 3 4\n1"); - } - - #[gpui::test] - async fn test_command_visual_replace(cx: &mut TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇ1\n2\n3\n4\n4\n3\n2\n1").await; - - cx.simulate_shared_keystrokes("v 2 j : s / . / k enter") - .await; - cx.shared_state().await.assert_eq("k\nk\nˇk\n4\n4\n3\n2\n1"); - } - - #[track_caller] - fn assert_active_item( - workspace: &mut Workspace, - expected_path: &str, - expected_text: &str, - cx: &mut Context, - ) { - let active_editor = workspace.active_item_as::(cx).unwrap(); - - let buffer = active_editor - .read(cx) - .buffer() - .read(cx) - .as_singleton() - .unwrap(); - - let text = buffer.read(cx).text(); - let file = buffer.read(cx).file().unwrap(); - let file_path = file.as_local().unwrap().abs_path(cx); - - assert_eq!(text, expected_text); - assert_eq!(file_path, Path::new(expected_path)); - } - - #[gpui::test] - async fn test_command_gf(cx: &mut TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // Assert base state, that we're in /root/dir/file.rs - cx.workspace(|workspace, _, cx| { - assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx); - }); - - // Insert a new file - let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone()); - fs.as_fake() - .insert_file( - path!("/root/dir/file2.rs"), - "This is file2.rs".as_bytes().to_vec(), - ) - .await; - fs.as_fake() - .insert_file( - path!("/root/dir/file3.rs"), - "go to file3".as_bytes().to_vec(), - ) - .await; - - // Put the path to the second file into the currently open buffer - cx.set_state(indoc! {"go to fiˇle2.rs"}, Mode::Normal); - - // Go to file2.rs - cx.simulate_keystrokes("g f"); - - // We now have two items - cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2)); - cx.workspace(|workspace, _, cx| { - assert_active_item( - workspace, - path!("/root/dir/file2.rs"), - "This is file2.rs", - cx, - ); - }); - - // Update editor to point to `file2.rs` - cx.editor = - cx.workspace(|workspace, _, cx| workspace.active_item_as::(cx).unwrap()); - - // Put the path to the third file into the currently open buffer, - // but remove its suffix, because we want that lookup to happen automatically. - cx.set_state(indoc! {"go to fiˇle3"}, Mode::Normal); - - // Go to file3.rs - cx.simulate_keystrokes("g f"); - - // We now have three items - cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3)); - cx.workspace(|workspace, _, cx| { - assert_active_item(workspace, path!("/root/dir/file3.rs"), "go to file3", cx); - }); - } - - #[gpui::test] - async fn test_command_write_filename(cx: &mut TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.workspace(|workspace, _, cx| { - assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx); - }); - - cx.simulate_keystrokes(": w space other.rs"); - cx.simulate_keystrokes("enter"); - - cx.workspace(|workspace, _, cx| { - assert_active_item(workspace, path!("/root/other.rs"), "", cx); - }); - - cx.simulate_keystrokes(": w space dir/file.rs"); - cx.simulate_keystrokes("enter"); - - cx.simulate_prompt_answer("Replace"); - cx.run_until_parked(); - - cx.workspace(|workspace, _, cx| { - assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx); - }); - - cx.simulate_keystrokes(": w ! space other.rs"); - cx.simulate_keystrokes("enter"); - - cx.workspace(|workspace, _, cx| { - assert_active_item(workspace, path!("/root/other.rs"), "", cx); - }); - } - - #[gpui::test] - async fn test_command_write_range(cx: &mut TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.workspace(|workspace, _, cx| { - assert_active_item(workspace, path!("/root/dir/file.rs"), "", cx); - }); - - cx.set_state( - indoc! {" - The quick - brown« fox - jumpsˇ» over - the lazy dog - "}, - Mode::Visual, - ); - - cx.simulate_keystrokes(": w space dir/other.rs"); - cx.simulate_keystrokes("enter"); - - let other = path!("/root/dir/other.rs"); - - let _ = cx - .workspace(|workspace, window, cx| { - workspace.open_abs_path(PathBuf::from(other), OpenOptions::default(), window, cx) - }) - .await; - - cx.workspace(|workspace, _, cx| { - assert_active_item( - workspace, - other, - indoc! {" - brown fox - jumps over - "}, - cx, - ); - }); - } - - #[gpui::test] - async fn test_command_matching_lines(cx: &mut TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ˇa - b - a - b - a - "}) - .await; - - cx.simulate_shared_keystrokes(":").await; - cx.simulate_shared_keystrokes("g / a / d").await; - cx.simulate_shared_keystrokes("enter").await; - - cx.shared_state().await.assert_eq(indoc! {" - b - b - ˇ"}); - - cx.simulate_shared_keystrokes("u").await; - - cx.shared_state().await.assert_eq(indoc! {" - ˇa - b - a - b - a - "}); - - cx.simulate_shared_keystrokes(":").await; - cx.simulate_shared_keystrokes("v / a / d").await; - cx.simulate_shared_keystrokes("enter").await; - - cx.shared_state().await.assert_eq(indoc! {" - a - a - ˇa"}); - } - - #[gpui::test] - async fn test_del_marks(cx: &mut TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ˇa - b - a - b - a - "}) - .await; - - cx.simulate_shared_keystrokes("m a").await; - - let mark = cx.update_editor(|editor, window, cx| { - let vim = editor.addon::().unwrap().entity.clone(); - vim.update(cx, |vim, cx| vim.get_mark("a", editor, window, cx)) - }); - assert!(mark.is_some()); - - cx.simulate_shared_keystrokes(": d e l m space a").await; - cx.simulate_shared_keystrokes("enter").await; - - let mark = cx.update_editor(|editor, window, cx| { - let vim = editor.addon::().unwrap().entity.clone(); - vim.update(cx, |vim, cx| vim.get_mark("a", editor, window, cx)) - }); - assert!(mark.is_none()) - } - - #[gpui::test] - async fn test_normal_command(cx: &mut TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - The quick - brown« fox - jumpsˇ» over - the lazy dog - "}) - .await; - - cx.simulate_shared_keystrokes(": n o r m space w C w o r d") - .await; - cx.simulate_shared_keystrokes("enter").await; - - cx.shared_state().await.assert_eq(indoc! {" - The quick - brown word - jumps worˇd - the lazy dog - "}); - - cx.simulate_shared_keystrokes(": n o r m space _ w c i w t e s t") - .await; - cx.simulate_shared_keystrokes("enter").await; - - cx.shared_state().await.assert_eq(indoc! {" - The quick - brown word - jumps tesˇt - the lazy dog - "}); - - cx.simulate_shared_keystrokes("_ l v l : n o r m space s l a") - .await; - cx.simulate_shared_keystrokes("enter").await; - - cx.shared_state().await.assert_eq(indoc! {" - The quick - brown word - lˇaumps test - the lazy dog - "}); - - cx.set_shared_state(indoc! {" - ˇThe quick - brown fox - jumps over - the lazy dog - "}) - .await; - - cx.simulate_shared_keystrokes("c i w M y escape").await; - - cx.shared_state().await.assert_eq(indoc! {" - Mˇy quick - brown fox - jumps over - the lazy dog - "}); - - cx.simulate_shared_keystrokes(": n o r m space u").await; - cx.simulate_shared_keystrokes("enter").await; - - cx.shared_state().await.assert_eq(indoc! {" - ˇThe quick - brown fox - jumps over - the lazy dog - "}); - // Once ctrl-v to input character literals is added there should be a test for redo - } - - #[gpui::test] - async fn test_command_tabnew(cx: &mut TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // Create a new file to ensure that, when the filename is used with - // `:tabnew`, it opens the existing file in a new tab. - let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone()); - fs.as_fake() - .insert_file(path!("/root/dir/file_2.rs"), "file_2".as_bytes().to_vec()) - .await; - - cx.simulate_keystrokes(": tabnew"); - cx.simulate_keystrokes("enter"); - cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2)); - - // Assert that the new tab is empty and not associated with any file, as - // no file path was provided to the `:tabnew` command. - cx.workspace(|workspace, _window, cx| { - let active_editor = workspace.active_item_as::(cx).unwrap(); - let buffer = active_editor - .read(cx) - .buffer() - .read(cx) - .as_singleton() - .unwrap(); - - assert!(&buffer.read(cx).file().is_none()); - }); - - // Leverage the filename as an argument to the `:tabnew` command, - // ensuring that the file, instead of an empty buffer, is opened in a - // new tab. - cx.simulate_keystrokes(": tabnew space dir/file_2.rs"); - cx.simulate_keystrokes("enter"); - - cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3)); - cx.workspace(|workspace, _, cx| { - assert_active_item(workspace, path!("/root/dir/file_2.rs"), "file_2", cx); - }); - - // If the `filename` argument provided to the `:tabnew` command is for a - // file that doesn't yet exist, it should still associate the buffer - // with that file path, so that when the buffer contents are saved, the - // file is created. - cx.simulate_keystrokes(": tabnew space dir/file_3.rs"); - cx.simulate_keystrokes("enter"); - - cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 4)); - cx.workspace(|workspace, _, cx| { - assert_active_item(workspace, path!("/root/dir/file_3.rs"), "", cx); - }); - } - - #[gpui::test] - async fn test_command_tabedit(cx: &mut TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // Create a new file to ensure that, when the filename is used with - // `:tabedit`, it opens the existing file in a new tab. - let fs = cx.workspace(|workspace, _, cx| workspace.project().read(cx).fs().clone()); - fs.as_fake() - .insert_file(path!("/root/dir/file_2.rs"), "file_2".as_bytes().to_vec()) - .await; - - cx.simulate_keystrokes(": tabedit"); - cx.simulate_keystrokes("enter"); - cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 2)); - - // Assert that the new tab is empty and not associated with any file, as - // no file path was provided to the `:tabedit` command. - cx.workspace(|workspace, _window, cx| { - let active_editor = workspace.active_item_as::(cx).unwrap(); - let buffer = active_editor - .read(cx) - .buffer() - .read(cx) - .as_singleton() - .unwrap(); - - assert!(&buffer.read(cx).file().is_none()); - }); - - // Leverage the filename as an argument to the `:tabedit` command, - // ensuring that the file, instead of an empty buffer, is opened in a - // new tab. - cx.simulate_keystrokes(": tabedit space dir/file_2.rs"); - cx.simulate_keystrokes("enter"); - - cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 3)); - cx.workspace(|workspace, _, cx| { - assert_active_item(workspace, path!("/root/dir/file_2.rs"), "file_2", cx); - }); - - // If the `filename` argument provided to the `:tabedit` command is for a - // file that doesn't yet exist, it should still associate the buffer - // with that file path, so that when the buffer contents are saved, the - // file is created. - cx.simulate_keystrokes(": tabedit space dir/file_3.rs"); - cx.simulate_keystrokes("enter"); - - cx.workspace(|workspace, _, cx| assert_eq!(workspace.items(cx).count(), 4)); - cx.workspace(|workspace, _, cx| { - assert_active_item(workspace, path!("/root/dir/file_3.rs"), "", cx); - }); - } - - #[gpui::test] - async fn test_ignorecase_command(cx: &mut TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.read(|cx| { - assert_eq!( - EditorSettings::get_global(cx).search.case_sensitive, - false, - "The `case_sensitive` setting should be `false` by default." - ); - }); - cx.simulate_keystrokes(": set space noignorecase"); - cx.simulate_keystrokes("enter"); - cx.read(|cx| { - assert_eq!( - EditorSettings::get_global(cx).search.case_sensitive, - true, - "The `case_sensitive` setting should have been enabled with `:set noignorecase`." - ); - }); - cx.simulate_keystrokes(": set space ignorecase"); - cx.simulate_keystrokes("enter"); - cx.read(|cx| { - assert_eq!( - EditorSettings::get_global(cx).search.case_sensitive, - false, - "The `case_sensitive` setting should have been disabled with `:set ignorecase`." - ); - }); - cx.simulate_keystrokes(": set space noic"); - cx.simulate_keystrokes("enter"); - cx.read(|cx| { - assert_eq!( - EditorSettings::get_global(cx).search.case_sensitive, - true, - "The `case_sensitive` setting should have been enabled with `:set noic`." - ); - }); - cx.simulate_keystrokes(": set space ic"); - cx.simulate_keystrokes("enter"); - cx.read(|cx| { - assert_eq!( - EditorSettings::get_global(cx).search.case_sensitive, - false, - "The `case_sensitive` setting should have been disabled with `:set ic`." - ); - }); - } - - #[gpui::test] - async fn test_sort_commands(cx: &mut TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! {" - «hornet - quirrel - elderbug - cornifer - idaˇ» - "}, - Mode::Visual, - ); - - cx.simulate_keystrokes(": sort"); - cx.simulate_keystrokes("enter"); - - cx.assert_state( - indoc! {" - ˇcornifer - elderbug - hornet - ida - quirrel - "}, - Mode::Normal, - ); - - // Assert that, by default, `:sort` takes case into consideration. - cx.set_state( - indoc! {" - «hornet - quirrel - Elderbug - cornifer - idaˇ» - "}, - Mode::Visual, - ); - - cx.simulate_keystrokes(": sort"); - cx.simulate_keystrokes("enter"); - - cx.assert_state( - indoc! {" - ˇElderbug - cornifer - hornet - ida - quirrel - "}, - Mode::Normal, - ); - - // Assert that, if the `i` option is passed, `:sort` ignores case. - cx.set_state( - indoc! {" - «hornet - quirrel - Elderbug - cornifer - idaˇ» - "}, - Mode::Visual, - ); - - cx.simulate_keystrokes(": sort space i"); - cx.simulate_keystrokes("enter"); - - cx.assert_state( - indoc! {" - ˇcornifer - Elderbug - hornet - ida - quirrel - "}, - Mode::Normal, - ); - - // When no range is provided, sorts the whole buffer. - cx.set_state( - indoc! {" - ˇhornet - quirrel - elderbug - cornifer - ida - "}, - Mode::Normal, - ); - - cx.simulate_keystrokes(": sort"); - cx.simulate_keystrokes("enter"); - - cx.assert_state( - indoc! {" - ˇcornifer - elderbug - hornet - ida - quirrel - "}, - Mode::Normal, - ); - } -} diff --git a/crates/vim/src/digraph.rs b/crates/vim/src/digraph.rs deleted file mode 100644 index 39014fea5b..0000000000 --- a/crates/vim/src/digraph.rs +++ /dev/null @@ -1,365 +0,0 @@ -use std::sync::Arc; - -use collections::HashMap; -use editor::Editor; -use gpui::{Action, App, Context, Keystroke, KeystrokeEvent, Window}; -use schemars::JsonSchema; -use serde::Deserialize; -use settings::Settings; -use std::sync::LazyLock; - -use crate::{Vim, VimSettings, state::Operator}; - -mod default; - -#[derive(Debug, Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -struct Literal(String, char); - -pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { - Vim::action(editor, cx, Vim::literal) -} - -static DEFAULT_DIGRAPHS_MAP: LazyLock>> = LazyLock::new(|| { - let mut map = HashMap::default(); - for &(a, b, c) in default::DEFAULT_DIGRAPHS { - let key = format!("{a}{b}"); - let value = char::from_u32(c).unwrap().to_string().into(); - map.insert(key, value); - } - map -}); - -fn lookup_digraph(a: char, b: char, cx: &App) -> Arc { - let custom_digraphs = &VimSettings::get_global(cx).custom_digraphs; - let input = format!("{a}{b}"); - let reversed = format!("{b}{a}"); - - custom_digraphs - .get(&input) - .or_else(|| DEFAULT_DIGRAPHS_MAP.get(&input)) - .or_else(|| custom_digraphs.get(&reversed)) - .or_else(|| DEFAULT_DIGRAPHS_MAP.get(&reversed)) - .cloned() - .unwrap_or_else(|| b.to_string().into()) -} - -impl Vim { - pub fn insert_digraph( - &mut self, - first_char: char, - second_char: char, - window: &mut Window, - cx: &mut Context, - ) { - let text = lookup_digraph(first_char, second_char, cx); - - self.pop_operator(window, cx); - if self.editor_input_enabled() { - self.update_editor(cx, |_, editor, cx| editor.insert(&text, window, cx)); - } else { - self.input_ignored(text, window, cx); - } - } - - fn literal(&mut self, action: &Literal, window: &mut Window, cx: &mut Context) { - match self.active_operator() { - Some(Operator::Literal { - prefix: Some(prefix), - }) => { - if let Some(keystroke) = Keystroke::parse(&action.0).ok() { - window.defer(cx, |window, cx| { - window.dispatch_keystroke(keystroke, cx); - }); - } - return self.handle_literal_input(prefix, "", window, cx); - } - Some(_) => self.insert_literal(Some(action.1), "", window, cx), - None => log::error!( - "Literal called when no operator was on the stack. This likely means there is an invalid keymap config" - ), - } - } - - pub fn handle_literal_keystroke( - &mut self, - keystroke_event: &KeystrokeEvent, - prefix: String, - window: &mut Window, - cx: &mut Context, - ) { - // handled by handle_literal_input - if keystroke_event.keystroke.key_char.is_some() { - return; - }; - - if !prefix.is_empty() { - self.handle_literal_input(prefix, "", window, cx); - } else { - self.pop_operator(window, cx); - } - - // give another chance to handle the binding outside - // of waiting mode. - if keystroke_event.action.is_none() { - let keystroke = keystroke_event.keystroke.clone(); - window.defer(cx, |window, cx| { - window.dispatch_keystroke(keystroke, cx); - }); - } - } - - pub fn handle_literal_input( - &mut self, - mut prefix: String, - text: &str, - window: &mut Window, - cx: &mut Context, - ) { - let first = prefix.chars().next(); - let next = text.chars().next().unwrap_or(' '); - match first { - Some('o' | 'O') => { - if next.is_digit(8) { - prefix.push(next); - if prefix.len() == 4 { - let ch: char = u8::from_str_radix(&prefix[1..], 8).unwrap_or(255).into(); - return self.insert_literal(Some(ch), "", window, cx); - } - } else { - let ch = if prefix.len() > 1 { - Some(u8::from_str_radix(&prefix[1..], 8).unwrap_or(255).into()) - } else { - None - }; - return self.insert_literal(ch, text, window, cx); - } - } - Some('x' | 'X' | 'u' | 'U') => { - let max_len = match first.unwrap() { - 'x' => 3, - 'X' => 3, - 'u' => 5, - 'U' => 9, - _ => unreachable!(), - }; - if next.is_ascii_hexdigit() { - prefix.push(next); - if prefix.len() == max_len { - let ch: char = u32::from_str_radix(&prefix[1..], 16) - .ok() - .and_then(|n| n.try_into().ok()) - .unwrap_or('\u{FFFD}'); - return self.insert_literal(Some(ch), "", window, cx); - } - } else { - let ch = if prefix.len() > 1 { - Some( - u32::from_str_radix(&prefix[1..], 16) - .ok() - .and_then(|n| n.try_into().ok()) - .unwrap_or('\u{FFFD}'), - ) - } else { - None - }; - return self.insert_literal(ch, text, window, cx); - } - } - Some('0'..='9') => { - if next.is_ascii_hexdigit() { - prefix.push(next); - if prefix.len() == 3 { - let ch: char = u8::from_str_radix(&prefix, 10).unwrap_or(255).into(); - return self.insert_literal(Some(ch), "", window, cx); - } - } else { - let ch: char = u8::from_str_radix(&prefix, 10).unwrap_or(255).into(); - return self.insert_literal(Some(ch), "", window, cx); - } - } - None if matches!(next, 'o' | 'O' | 'x' | 'X' | 'u' | 'U' | '0'..='9') => { - prefix.push(next) - } - _ => { - return self.insert_literal(None, text, window, cx); - } - }; - - self.pop_operator(window, cx); - self.push_operator( - Operator::Literal { - prefix: Some(prefix), - }, - window, - cx, - ); - } - - fn insert_literal( - &mut self, - ch: Option, - suffix: &str, - window: &mut Window, - cx: &mut Context, - ) { - self.pop_operator(window, cx); - let mut text = String::new(); - if let Some(c) = ch { - if c == '\n' { - text.push('\x00') - } else { - text.push(c) - } - } - text.push_str(suffix); - - if self.editor_input_enabled() { - self.update_editor(cx, |_, editor, cx| editor.insert(&text, window, cx)); - } else { - self.input_ignored(text.into(), window, cx); - } - } -} - -#[cfg(test)] -mod test { - use collections::HashMap; - use settings::SettingsStore; - - use crate::{ - state::Mode, - test::{NeovimBackedTestContext, VimTestContext}, - }; - - #[gpui::test] - async fn test_digraph_insert_mode(cx: &mut gpui::TestAppContext) { - let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("Hellˇo").await; - cx.simulate_shared_keystrokes("a ctrl-k o : escape").await; - cx.shared_state().await.assert_eq("Helloˇö"); - - cx.set_shared_state("Hellˇo").await; - cx.simulate_shared_keystrokes("a ctrl-k : o escape").await; - cx.shared_state().await.assert_eq("Helloˇö"); - - cx.set_shared_state("Hellˇo").await; - cx.simulate_shared_keystrokes("i ctrl-k o : escape").await; - cx.shared_state().await.assert_eq("Hellˇöo"); - } - - #[gpui::test] - async fn test_digraph_insert_multicursor(cx: &mut gpui::TestAppContext) { - let mut cx: VimTestContext = VimTestContext::new(cx, true).await; - - cx.set_state("Hellˇo wˇorld", Mode::Normal); - cx.simulate_keystrokes("a ctrl-k o : escape"); - cx.assert_state("Helloˇö woˇörld", Mode::Normal); - } - - #[gpui::test] - async fn test_digraph_replace(cx: &mut gpui::TestAppContext) { - let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("Hellˇo").await; - cx.simulate_shared_keystrokes("r ctrl-k o :").await; - cx.shared_state().await.assert_eq("Hellˇö"); - } - - #[gpui::test] - async fn test_digraph_find(cx: &mut gpui::TestAppContext) { - let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇHellö world").await; - cx.simulate_shared_keystrokes("f ctrl-k o :").await; - cx.shared_state().await.assert_eq("Hellˇö world"); - - cx.set_shared_state("ˇHellö world").await; - cx.simulate_shared_keystrokes("t ctrl-k o :").await; - cx.shared_state().await.assert_eq("Helˇlö world"); - } - - #[gpui::test] - async fn test_digraph_replace_mode(cx: &mut gpui::TestAppContext) { - let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇHello").await; - cx.simulate_shared_keystrokes( - "shift-r ctrl-k a ' ctrl-k e ` ctrl-k i : ctrl-k o ~ ctrl-k u - escape", - ) - .await; - cx.shared_state().await.assert_eq("áèïõˇū"); - } - - #[gpui::test] - async fn test_digraph_custom(cx: &mut gpui::TestAppContext) { - let mut cx: VimTestContext = VimTestContext::new(cx, true).await; - - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |s| { - let mut custom_digraphs = HashMap::default(); - custom_digraphs.insert("|-".into(), "⊢".into()); - custom_digraphs.insert(":)".into(), "👨‍💻".into()); - s.vim.get_or_insert_default().custom_digraphs = Some(custom_digraphs); - }); - }); - - cx.set_state("ˇ", Mode::Normal); - cx.simulate_keystrokes("a ctrl-k | - escape"); - cx.assert_state("ˇ⊢", Mode::Normal); - - // Test support for multi-codepoint mappings - cx.set_state("ˇ", Mode::Normal); - cx.simulate_keystrokes("a ctrl-k : ) escape"); - cx.assert_state("ˇ👨‍💻", Mode::Normal); - } - - #[gpui::test] - async fn test_digraph_keymap_conflict(cx: &mut gpui::TestAppContext) { - let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("Hellˇo").await; - cx.simulate_shared_keystrokes("a ctrl-k s , escape").await; - cx.shared_state().await.assert_eq("Helloˇş"); - } - - #[gpui::test] - async fn test_ctrl_v(cx: &mut gpui::TestAppContext) { - let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇ").await; - cx.simulate_shared_keystrokes("i ctrl-v 0 0 0").await; - cx.shared_state().await.assert_eq("\x00ˇ"); - - cx.simulate_shared_keystrokes("ctrl-v j").await; - cx.shared_state().await.assert_eq("\x00jˇ"); - cx.simulate_shared_keystrokes("ctrl-v x 6 5").await; - cx.shared_state().await.assert_eq("\x00jeˇ"); - cx.simulate_shared_keystrokes("ctrl-v U 1 F 6 4 0 space") - .await; - cx.shared_state().await.assert_eq("\x00je🙀 ˇ"); - } - - #[gpui::test] - async fn test_ctrl_v_escape(cx: &mut gpui::TestAppContext) { - let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state("ˇ").await; - cx.simulate_shared_keystrokes("i ctrl-v 9 escape").await; - cx.shared_state().await.assert_eq("ˇ\t"); - cx.simulate_shared_keystrokes("i ctrl-v escape").await; - cx.shared_state().await.assert_eq("\x1bˇ\t"); - } - - #[gpui::test] - async fn test_ctrl_v_control(cx: &mut gpui::TestAppContext) { - let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state("ˇ").await; - cx.simulate_shared_keystrokes("i ctrl-v ctrl-d").await; - cx.shared_state().await.assert_eq("\x04ˇ"); - cx.simulate_shared_keystrokes("ctrl-v ctrl-j").await; - cx.shared_state().await.assert_eq("\x04\x00ˇ"); - cx.simulate_shared_keystrokes("ctrl-v tab").await; - cx.shared_state().await.assert_eq("\x04\x00\x09ˇ"); - } -} diff --git a/crates/vim/src/digraph/default.rs b/crates/vim/src/digraph/default.rs deleted file mode 100644 index 717fed3816..0000000000 --- a/crates/vim/src/digraph/default.rs +++ /dev/null @@ -1,1366 +0,0 @@ -/// Copied from https://github.com/neovim/neovim/blob/5fc25ecc7a383a4bed2199774ed2e26022456ca3/src/nvim/digraph.c#L65 -pub const DEFAULT_DIGRAPHS: &[(char, char, u32)] = &[ - ('N', 'U', 0x00), - ('S', 'H', 0x01), - ('S', 'X', 0x02), - ('E', 'X', 0x03), - ('E', 'T', 0x04), - ('E', 'Q', 0x05), - ('A', 'K', 0x06), - ('B', 'L', 0x07), - ('B', 'S', 0x08), - ('H', 'T', 0x09), - ('L', 'F', 0x0a), - ('V', 'T', 0x0b), - ('F', 'F', 0x0c), - ('C', 'R', 0x0d), - ('S', 'O', 0x0e), - ('S', 'I', 0x0f), - ('D', 'L', 0x10), - ('D', '1', 0x11), - ('D', '2', 0x12), - ('D', '3', 0x13), - ('D', '4', 0x14), - ('N', 'K', 0x15), - ('S', 'Y', 0x16), - ('E', 'B', 0x17), - ('C', 'N', 0x18), - ('E', 'M', 0x19), - ('S', 'B', 0x1a), - ('E', 'C', 0x1b), - ('F', 'S', 0x1c), - ('G', 'S', 0x1d), - ('R', 'S', 0x1e), - ('U', 'S', 0x1f), - ('S', 'P', 0x20), - ('N', 'b', 0x23), - ('D', 'O', 0x24), - ('A', 't', 0x40), - ('<', '(', 0x5b), - ('/', '/', 0x5c), - (')', '>', 0x5d), - ('\'', '>', 0x5e), - ('\'', '!', 0x60), - ('(', '!', 0x7b), - ('!', '!', 0x7c), - ('!', ')', 0x7d), - ('\'', '?', 0x7e), - ('D', 'T', 0x7f), - ('P', 'A', 0x80), - ('H', 'O', 0x81), - ('B', 'H', 0x82), - ('N', 'H', 0x83), - ('I', 'N', 0x84), - ('N', 'L', 0x85), - ('S', 'A', 0x86), - ('E', 'S', 0x87), - ('H', 'S', 0x88), - ('H', 'J', 0x89), - ('V', 'S', 0x8a), - ('P', 'D', 0x8b), - ('P', 'U', 0x8c), - ('R', 'I', 0x8d), - ('S', '2', 0x8e), - ('S', '3', 0x8f), - ('D', 'C', 0x90), - ('P', '1', 0x91), - ('P', '2', 0x92), - ('T', 'S', 0x93), - ('C', 'C', 0x94), - ('M', 'W', 0x95), - ('S', 'G', 0x96), - ('E', 'G', 0x97), - ('S', 'S', 0x98), - ('G', 'C', 0x99), - ('S', 'C', 0x9a), - ('C', 'I', 0x9b), - ('S', 'T', 0x9c), - ('O', 'C', 0x9d), - ('P', 'M', 0x9e), - ('A', 'C', 0x9f), - ('N', 'S', 0xa0), - ('!', 'I', 0xa1), - ('~', '!', 0xa1), - ('C', 't', 0xa2), - ('c', '|', 0xa2), - ('P', 'd', 0xa3), - ('$', '$', 0xa3), - ('C', 'u', 0xa4), - ('o', 'x', 0xa4), - ('Y', 'e', 0xa5), - ('Y', '-', 0xa5), - ('B', 'B', 0xa6), - ('|', '|', 0xa6), - ('S', 'E', 0xa7), - ('\'', ':', 0xa8), - ('C', 'o', 0xa9), - ('c', 'O', 0xa9), - ('-', 'a', 0xaa), - ('<', '<', 0xab), - ('N', 'O', 0xac), - ('-', ',', 0xac), - ('-', '-', 0xad), - ('R', 'g', 0xae), - ('\'', 'm', 0xaf), - ('-', '=', 0xaf), - ('D', 'G', 0xb0), - ('~', 'o', 0xb0), - ('+', '-', 0xb1), - ('2', 'S', 0xb2), - ('2', '2', 0xb2), - ('3', 'S', 0xb3), - ('3', '3', 0xb3), - ('\'', '\'', 0xb4), - ('M', 'y', 0xb5), - ('P', 'I', 0xb6), - ('p', 'p', 0xb6), - ('.', 'M', 0xb7), - ('~', '.', 0xb7), - ('\'', ',', 0xb8), - ('1', 'S', 0xb9), - ('1', '1', 0xb9), - ('-', 'o', 0xba), - ('>', '>', 0xbb), - ('1', '4', 0xbc), - ('1', '2', 0xbd), - ('3', '4', 0xbe), - ('?', 'I', 0xbf), - ('~', '?', 0xbf), - ('A', '!', 0xc0), - ('A', '`', 0xc0), - ('A', '\'', 0xc1), - ('A', '>', 0xc2), - ('A', '^', 0xc2), - ('A', '?', 0xc3), - ('A', '~', 0xc3), - ('A', ':', 0xc4), - ('A', '"', 0xc4), - ('A', 'A', 0xc5), - ('A', '@', 0xc5), - ('A', 'E', 0xc6), - ('C', ',', 0xc7), - ('E', '!', 0xc8), - ('E', '`', 0xc8), - ('E', '\'', 0xc9), - ('E', '>', 0xca), - ('E', '^', 0xca), - ('E', ':', 0xcb), - ('E', '"', 0xcb), - ('I', '!', 0xcc), - ('I', '`', 0xcc), - ('I', '\'', 0xcd), - ('I', '>', 0xce), - ('I', '^', 0xce), - ('I', ':', 0xcf), - ('I', '"', 0xcf), - ('D', '-', 0xd0), - ('N', '?', 0xd1), - ('N', '~', 0xd1), - ('O', '!', 0xd2), - ('O', '`', 0xd2), - ('O', '\'', 0xd3), - ('O', '>', 0xd4), - ('O', '^', 0xd4), - ('O', '?', 0xd5), - ('O', '~', 0xd5), - ('O', ':', 0xd6), - ('*', 'X', 0xd7), - ('/', '\\', 0xd7), - ('O', '/', 0xd8), - ('U', '!', 0xd9), - ('U', '`', 0xd9), - ('U', '\'', 0xda), - ('U', '>', 0xdb), - ('U', '^', 0xdb), - ('U', ':', 0xdc), - ('Y', '\'', 0xdd), - ('T', 'H', 0xde), - ('I', 'p', 0xde), - ('s', 's', 0xdf), - ('a', '!', 0xe0), - ('a', '`', 0xe0), - ('a', '\'', 0xe1), - ('a', '>', 0xe2), - ('a', '^', 0xe2), - ('a', '?', 0xe3), - ('a', '~', 0xe3), - ('a', ':', 0xe4), - ('a', '"', 0xe4), - ('a', 'a', 0xe5), - ('a', '@', 0xe5), - ('a', 'e', 0xe6), - ('c', ',', 0xe7), - ('e', '!', 0xe8), - ('e', '`', 0xe8), - ('e', '\'', 0xe9), - ('e', '>', 0xea), - ('e', '^', 0xea), - ('e', ':', 0xeb), - ('e', '"', 0xeb), - ('i', '!', 0xec), - ('i', '`', 0xec), - ('i', '\'', 0xed), - ('i', '>', 0xee), - ('i', '^', 0xee), - ('i', ':', 0xef), - ('d', '-', 0xf0), - ('n', '?', 0xf1), - ('n', '~', 0xf1), - ('o', '!', 0xf2), - ('o', '`', 0xf2), - ('o', '\'', 0xf3), - ('o', '>', 0xf4), - ('o', '^', 0xf4), - ('o', '?', 0xf5), - ('o', '~', 0xf5), - ('o', ':', 0xf6), - ('-', ':', 0xf7), - ('o', '/', 0xf8), - ('u', '!', 0xf9), - ('u', '`', 0xf9), - ('u', '\'', 0xfa), - ('u', '>', 0xfb), - ('u', '^', 0xfb), - ('u', ':', 0xfc), - ('y', '\'', 0xfd), - ('t', 'h', 0xfe), - ('y', ':', 0xff), - ('y', '"', 0xff), - ('A', '-', 0x0100), - ('a', '-', 0x0101), - ('A', '(', 0x0102), - ('a', '(', 0x0103), - ('A', ';', 0x0104), - ('a', ';', 0x0105), - ('C', '\'', 0x0106), - ('c', '\'', 0x0107), - ('C', '>', 0x0108), - ('c', '>', 0x0109), - ('C', '.', 0x010a), - ('c', '.', 0x010b), - ('C', '<', 0x010c), - ('c', '<', 0x010d), - ('D', '<', 0x010e), - ('d', '<', 0x010f), - ('D', '/', 0x0110), - ('d', '/', 0x0111), - ('E', '-', 0x0112), - ('e', '-', 0x0113), - ('E', '(', 0x0114), - ('e', '(', 0x0115), - ('E', '.', 0x0116), - ('e', '.', 0x0117), - ('E', ';', 0x0118), - ('e', ';', 0x0119), - ('E', '<', 0x011a), - ('e', '<', 0x011b), - ('G', '>', 0x011c), - ('g', '>', 0x011d), - ('G', '(', 0x011e), - ('g', '(', 0x011f), - ('G', '.', 0x0120), - ('g', '.', 0x0121), - ('G', ',', 0x0122), - ('g', ',', 0x0123), - ('H', '>', 0x0124), - ('h', '>', 0x0125), - ('H', '/', 0x0126), - ('h', '/', 0x0127), - ('I', '?', 0x0128), - ('i', '?', 0x0129), - ('I', '-', 0x012a), - ('i', '-', 0x012b), - ('I', '(', 0x012c), - ('i', '(', 0x012d), - ('I', ';', 0x012e), - ('i', ';', 0x012f), - ('I', '.', 0x0130), - ('i', '.', 0x0131), - ('I', 'J', 0x0132), - ('i', 'j', 0x0133), - ('J', '>', 0x0134), - ('j', '>', 0x0135), - ('K', ',', 0x0136), - ('k', ',', 0x0137), - ('k', 'k', 0x0138), - ('L', '\'', 0x0139), - ('l', '\'', 0x013a), - ('L', ',', 0x013b), - ('l', ',', 0x013c), - ('L', '<', 0x013d), - ('l', '<', 0x013e), - ('L', '.', 0x013f), - ('l', '.', 0x0140), - ('L', '/', 0x0141), - ('l', '/', 0x0142), - ('N', '\'', 0x0143), - ('n', '\'', 0x0144), - ('N', ',', 0x0145), - ('n', ',', 0x0146), - ('N', '<', 0x0147), - ('n', '<', 0x0148), - ('\'', 'n', 0x0149), - ('N', 'G', 0x014a), - ('n', 'g', 0x014b), - ('O', '-', 0x014c), - ('o', '-', 0x014d), - ('O', '(', 0x014e), - ('o', '(', 0x014f), - ('O', '"', 0x0150), - ('o', '"', 0x0151), - ('O', 'E', 0x0152), - ('o', 'e', 0x0153), - ('R', '\'', 0x0154), - ('r', '\'', 0x0155), - ('R', ',', 0x0156), - ('r', ',', 0x0157), - ('R', '<', 0x0158), - ('r', '<', 0x0159), - ('S', '\'', 0x015a), - ('s', '\'', 0x015b), - ('S', '>', 0x015c), - ('s', '>', 0x015d), - ('S', ',', 0x015e), - ('s', ',', 0x015f), - ('S', '<', 0x0160), - ('s', '<', 0x0161), - ('T', ',', 0x0162), - ('t', ',', 0x0163), - ('T', '<', 0x0164), - ('t', '<', 0x0165), - ('T', '/', 0x0166), - ('t', '/', 0x0167), - ('U', '?', 0x0168), - ('u', '?', 0x0169), - ('U', '-', 0x016a), - ('u', '-', 0x016b), - ('U', '(', 0x016c), - ('u', '(', 0x016d), - ('U', '0', 0x016e), - ('u', '0', 0x016f), - ('U', '"', 0x0170), - ('u', '"', 0x0171), - ('U', ';', 0x0172), - ('u', ';', 0x0173), - ('W', '>', 0x0174), - ('w', '>', 0x0175), - ('Y', '>', 0x0176), - ('y', '>', 0x0177), - ('Y', ':', 0x0178), - ('Z', '\'', 0x0179), - ('z', '\'', 0x017a), - ('Z', '.', 0x017b), - ('z', '.', 0x017c), - ('Z', '<', 0x017d), - ('z', '<', 0x017e), - ('O', '9', 0x01a0), - ('o', '9', 0x01a1), - ('O', 'I', 0x01a2), - ('o', 'i', 0x01a3), - ('y', 'r', 0x01a6), - ('U', '9', 0x01af), - ('u', '9', 0x01b0), - ('Z', '/', 0x01b5), - ('z', '/', 0x01b6), - ('E', 'D', 0x01b7), - ('A', '<', 0x01cd), - ('a', '<', 0x01ce), - ('I', '<', 0x01cf), - ('i', '<', 0x01d0), - ('O', '<', 0x01d1), - ('o', '<', 0x01d2), - ('U', '<', 0x01d3), - ('u', '<', 0x01d4), - ('A', '1', 0x01de), - ('a', '1', 0x01df), - ('A', '7', 0x01e0), - ('a', '7', 0x01e1), - ('A', '3', 0x01e2), - ('a', '3', 0x01e3), - ('G', '/', 0x01e4), - ('g', '/', 0x01e5), - ('G', '<', 0x01e6), - ('g', '<', 0x01e7), - ('K', '<', 0x01e8), - ('k', '<', 0x01e9), - ('O', ';', 0x01ea), - ('o', ';', 0x01eb), - ('O', '1', 0x01ec), - ('o', '1', 0x01ed), - ('E', 'Z', 0x01ee), - ('e', 'z', 0x01ef), - ('j', '<', 0x01f0), - ('G', '\'', 0x01f4), - ('g', '\'', 0x01f5), - (';', 'S', 0x02bf), - ('\'', '<', 0x02c7), - ('\'', '(', 0x02d8), - ('\'', '.', 0x02d9), - ('\'', '0', 0x02da), - ('\'', ';', 0x02db), - ('\'', '"', 0x02dd), - ('A', '%', 0x0386), - ('E', '%', 0x0388), - ('Y', '%', 0x0389), - ('I', '%', 0x038a), - ('O', '%', 0x038c), - ('U', '%', 0x038e), - ('W', '%', 0x038f), - ('i', '3', 0x0390), - ('A', '*', 0x0391), - ('B', '*', 0x0392), - ('G', '*', 0x0393), - ('D', '*', 0x0394), - ('E', '*', 0x0395), - ('Z', '*', 0x0396), - ('Y', '*', 0x0397), - ('H', '*', 0x0398), - ('I', '*', 0x0399), - ('K', '*', 0x039a), - ('L', '*', 0x039b), - ('M', '*', 0x039c), - ('N', '*', 0x039d), - ('C', '*', 0x039e), - ('O', '*', 0x039f), - ('P', '*', 0x03a0), - ('R', '*', 0x03a1), - ('S', '*', 0x03a3), - ('T', '*', 0x03a4), - ('U', '*', 0x03a5), - ('F', '*', 0x03a6), - ('X', '*', 0x03a7), - ('Q', '*', 0x03a8), - ('W', '*', 0x03a9), - ('J', '*', 0x03aa), - ('V', '*', 0x03ab), - ('a', '%', 0x03ac), - ('e', '%', 0x03ad), - ('y', '%', 0x03ae), - ('i', '%', 0x03af), - ('u', '3', 0x03b0), - ('a', '*', 0x03b1), - ('b', '*', 0x03b2), - ('g', '*', 0x03b3), - ('d', '*', 0x03b4), - ('e', '*', 0x03b5), - ('z', '*', 0x03b6), - ('y', '*', 0x03b7), - ('h', '*', 0x03b8), - ('i', '*', 0x03b9), - ('k', '*', 0x03ba), - ('l', '*', 0x03bb), - ('m', '*', 0x03bc), - ('n', '*', 0x03bd), - ('c', '*', 0x03be), - ('o', '*', 0x03bf), - ('p', '*', 0x03c0), - ('r', '*', 0x03c1), - ('*', 's', 0x03c2), - ('s', '*', 0x03c3), - ('t', '*', 0x03c4), - ('u', '*', 0x03c5), - ('f', '*', 0x03c6), - ('x', '*', 0x03c7), - ('q', '*', 0x03c8), - ('w', '*', 0x03c9), - ('j', '*', 0x03ca), - ('v', '*', 0x03cb), - ('o', '%', 0x03cc), - ('u', '%', 0x03cd), - ('w', '%', 0x03ce), - ('\'', 'G', 0x03d8), - (',', 'G', 0x03d9), - ('T', '3', 0x03da), - ('t', '3', 0x03db), - ('M', '3', 0x03dc), - ('m', '3', 0x03dd), - ('K', '3', 0x03de), - ('k', '3', 0x03df), - ('P', '3', 0x03e0), - ('p', '3', 0x03e1), - ('\'', '%', 0x03f4), - ('j', '3', 0x03f5), - ('I', 'O', 0x0401), - ('D', '%', 0x0402), - ('G', '%', 0x0403), - ('I', 'E', 0x0404), - ('D', 'S', 0x0405), - ('I', 'I', 0x0406), - ('Y', 'I', 0x0407), - ('J', '%', 0x0408), - ('L', 'J', 0x0409), - ('N', 'J', 0x040a), - ('T', 's', 0x040b), - ('K', 'J', 0x040c), - ('V', '%', 0x040e), - ('D', 'Z', 0x040f), - ('A', '=', 0x0410), - ('B', '=', 0x0411), - ('V', '=', 0x0412), - ('G', '=', 0x0413), - ('D', '=', 0x0414), - ('E', '=', 0x0415), - ('Z', '%', 0x0416), - ('Z', '=', 0x0417), - ('I', '=', 0x0418), - ('J', '=', 0x0419), - ('K', '=', 0x041a), - ('L', '=', 0x041b), - ('M', '=', 0x041c), - ('N', '=', 0x041d), - ('O', '=', 0x041e), - ('P', '=', 0x041f), - ('R', '=', 0x0420), - ('S', '=', 0x0421), - ('T', '=', 0x0422), - ('U', '=', 0x0423), - ('F', '=', 0x0424), - ('H', '=', 0x0425), - ('C', '=', 0x0426), - ('C', '%', 0x0427), - ('S', '%', 0x0428), - ('S', 'c', 0x0429), - ('=', '"', 0x042a), - ('Y', '=', 0x042b), - ('%', '"', 0x042c), - ('J', 'E', 0x042d), - ('J', 'U', 0x042e), - ('J', 'A', 0x042f), - ('a', '=', 0x0430), - ('b', '=', 0x0431), - ('v', '=', 0x0432), - ('g', '=', 0x0433), - ('d', '=', 0x0434), - ('e', '=', 0x0435), - ('z', '%', 0x0436), - ('z', '=', 0x0437), - ('i', '=', 0x0438), - ('j', '=', 0x0439), - ('k', '=', 0x043a), - ('l', '=', 0x043b), - ('m', '=', 0x043c), - ('n', '=', 0x043d), - ('o', '=', 0x043e), - ('p', '=', 0x043f), - ('r', '=', 0x0440), - ('s', '=', 0x0441), - ('t', '=', 0x0442), - ('u', '=', 0x0443), - ('f', '=', 0x0444), - ('h', '=', 0x0445), - ('c', '=', 0x0446), - ('c', '%', 0x0447), - ('s', '%', 0x0448), - ('s', 'c', 0x0449), - ('=', '\'', 0x044a), - ('y', '=', 0x044b), - ('%', '\'', 0x044c), - ('j', 'e', 0x044d), - ('j', 'u', 0x044e), - ('j', 'a', 0x044f), - ('i', 'o', 0x0451), - ('d', '%', 0x0452), - ('g', '%', 0x0453), - ('i', 'e', 0x0454), - ('d', 's', 0x0455), - ('i', 'i', 0x0456), - ('y', 'i', 0x0457), - ('j', '%', 0x0458), - ('l', 'j', 0x0459), - ('n', 'j', 0x045a), - ('t', 's', 0x045b), - ('k', 'j', 0x045c), - ('v', '%', 0x045e), - ('d', 'z', 0x045f), - ('Y', '3', 0x0462), - ('y', '3', 0x0463), - ('O', '3', 0x046a), - ('o', '3', 0x046b), - ('F', '3', 0x0472), - ('f', '3', 0x0473), - ('V', '3', 0x0474), - ('v', '3', 0x0475), - ('C', '3', 0x0480), - ('c', '3', 0x0481), - ('G', '3', 0x0490), - ('g', '3', 0x0491), - ('A', '+', 0x05d0), - ('B', '+', 0x05d1), - ('G', '+', 0x05d2), - ('D', '+', 0x05d3), - ('H', '+', 0x05d4), - ('W', '+', 0x05d5), - ('Z', '+', 0x05d6), - ('X', '+', 0x05d7), - ('T', 'j', 0x05d8), - ('J', '+', 0x05d9), - ('K', '%', 0x05da), - ('K', '+', 0x05db), - ('L', '+', 0x05dc), - ('M', '%', 0x05dd), - ('M', '+', 0x05de), - ('N', '%', 0x05df), - ('N', '+', 0x05e0), - ('S', '+', 0x05e1), - ('E', '+', 0x05e2), - ('P', '%', 0x05e3), - ('P', '+', 0x05e4), - ('Z', 'j', 0x05e5), - ('Z', 'J', 0x05e6), - ('Q', '+', 0x05e7), - ('R', '+', 0x05e8), - ('S', 'h', 0x05e9), - ('T', '+', 0x05ea), - (',', '+', 0x060c), - (';', '+', 0x061b), - ('?', '+', 0x061f), - ('H', '\'', 0x0621), - ('a', 'M', 0x0622), - ('a', 'H', 0x0623), - ('w', 'H', 0x0624), - ('a', 'h', 0x0625), - ('y', 'H', 0x0626), - ('a', '+', 0x0627), - ('b', '+', 0x0628), - ('t', 'm', 0x0629), - ('t', '+', 0x062a), - ('t', 'k', 0x062b), - ('g', '+', 0x062c), - ('h', 'k', 0x062d), - ('x', '+', 0x062e), - ('d', '+', 0x062f), - ('d', 'k', 0x0630), - ('r', '+', 0x0631), - ('z', '+', 0x0632), - ('s', '+', 0x0633), - ('s', 'n', 0x0634), - ('c', '+', 0x0635), - ('d', 'd', 0x0636), - ('t', 'j', 0x0637), - ('z', 'H', 0x0638), - ('e', '+', 0x0639), - ('i', '+', 0x063a), - ('+', '+', 0x0640), - ('f', '+', 0x0641), - ('q', '+', 0x0642), - ('k', '+', 0x0643), - ('l', '+', 0x0644), - ('m', '+', 0x0645), - ('n', '+', 0x0646), - ('h', '+', 0x0647), - ('w', '+', 0x0648), - ('j', '+', 0x0649), - ('y', '+', 0x064a), - (':', '+', 0x064b), - ('"', '+', 0x064c), - ('=', '+', 0x064d), - ('/', '+', 0x064e), - ('\'', '+', 0x064f), - ('1', '+', 0x0650), - ('3', '+', 0x0651), - ('0', '+', 0x0652), - ('a', 'S', 0x0670), - ('p', '+', 0x067e), - ('v', '+', 0x06a4), - ('g', 'f', 0x06af), - ('0', 'a', 0x06f0), - ('1', 'a', 0x06f1), - ('2', 'a', 0x06f2), - ('3', 'a', 0x06f3), - ('4', 'a', 0x06f4), - ('5', 'a', 0x06f5), - ('6', 'a', 0x06f6), - ('7', 'a', 0x06f7), - ('8', 'a', 0x06f8), - ('9', 'a', 0x06f9), - ('B', '.', 0x1e02), - ('b', '.', 0x1e03), - ('B', '_', 0x1e06), - ('b', '_', 0x1e07), - ('D', '.', 0x1e0a), - ('d', '.', 0x1e0b), - ('D', '_', 0x1e0e), - ('d', '_', 0x1e0f), - ('D', ',', 0x1e10), - ('d', ',', 0x1e11), - ('F', '.', 0x1e1e), - ('f', '.', 0x1e1f), - ('G', '-', 0x1e20), - ('g', '-', 0x1e21), - ('H', '.', 0x1e22), - ('h', '.', 0x1e23), - ('H', ':', 0x1e26), - ('h', ':', 0x1e27), - ('H', ',', 0x1e28), - ('h', ',', 0x1e29), - ('K', '\'', 0x1e30), - ('k', '\'', 0x1e31), - ('K', '_', 0x1e34), - ('k', '_', 0x1e35), - ('L', '_', 0x1e3a), - ('l', '_', 0x1e3b), - ('M', '\'', 0x1e3e), - ('m', '\'', 0x1e3f), - ('M', '.', 0x1e40), - ('m', '.', 0x1e41), - ('N', '.', 0x1e44), - ('n', '.', 0x1e45), - ('N', '_', 0x1e48), - ('n', '_', 0x1e49), - ('P', '\'', 0x1e54), - ('p', '\'', 0x1e55), - ('P', '.', 0x1e56), - ('p', '.', 0x1e57), - ('R', '.', 0x1e58), - ('r', '.', 0x1e59), - ('R', '_', 0x1e5e), - ('r', '_', 0x1e5f), - ('S', '.', 0x1e60), - ('s', '.', 0x1e61), - ('T', '.', 0x1e6a), - ('t', '.', 0x1e6b), - ('T', '_', 0x1e6e), - ('t', '_', 0x1e6f), - ('V', '?', 0x1e7c), - ('v', '?', 0x1e7d), - ('W', '!', 0x1e80), - ('W', '`', 0x1e80), - ('w', '!', 0x1e81), - ('w', '`', 0x1e81), - ('W', '\'', 0x1e82), - ('w', '\'', 0x1e83), - ('W', ':', 0x1e84), - ('w', ':', 0x1e85), - ('W', '.', 0x1e86), - ('w', '.', 0x1e87), - ('X', '.', 0x1e8a), - ('x', '.', 0x1e8b), - ('X', ':', 0x1e8c), - ('x', ':', 0x1e8d), - ('Y', '.', 0x1e8e), - ('y', '.', 0x1e8f), - ('Z', '>', 0x1e90), - ('z', '>', 0x1e91), - ('Z', '_', 0x1e94), - ('z', '_', 0x1e95), - ('h', '_', 0x1e96), - ('t', ':', 0x1e97), - ('w', '0', 0x1e98), - ('y', '0', 0x1e99), - ('A', '2', 0x1ea2), - ('a', '2', 0x1ea3), - ('E', '2', 0x1eba), - ('e', '2', 0x1ebb), - ('E', '?', 0x1ebc), - ('e', '?', 0x1ebd), - ('I', '2', 0x1ec8), - ('i', '2', 0x1ec9), - ('O', '2', 0x1ece), - ('o', '2', 0x1ecf), - ('U', '2', 0x1ee6), - ('u', '2', 0x1ee7), - ('Y', '!', 0x1ef2), - ('Y', '`', 0x1ef2), - ('y', '!', 0x1ef3), - ('y', '`', 0x1ef3), - ('Y', '2', 0x1ef6), - ('y', '2', 0x1ef7), - ('Y', '?', 0x1ef8), - ('y', '?', 0x1ef9), - (';', '\'', 0x1f00), - (',', '\'', 0x1f01), - (';', '!', 0x1f02), - (',', '!', 0x1f03), - ('?', ';', 0x1f04), - ('?', ',', 0x1f05), - ('!', ':', 0x1f06), - ('?', ':', 0x1f07), - ('1', 'N', 0x2002), - ('1', 'M', 0x2003), - ('3', 'M', 0x2004), - ('4', 'M', 0x2005), - ('6', 'M', 0x2006), - ('1', 'T', 0x2009), - ('1', 'H', 0x200a), - ('-', '1', 0x2010), - ('-', 'N', 0x2013), - ('-', 'M', 0x2014), - ('-', '3', 0x2015), - ('!', '2', 0x2016), - ('=', '2', 0x2017), - ('\'', '6', 0x2018), - ('\'', '9', 0x2019), - ('.', '9', 0x201a), - ('9', '\'', 0x201b), - ('"', '6', 0x201c), - ('"', '9', 0x201d), - (':', '9', 0x201e), - ('9', '"', 0x201f), - ('/', '-', 0x2020), - ('/', '=', 0x2021), - ('o', 'o', 0x2022), - ('.', '.', 0x2025), - (',', '.', 0x2026), - ('%', '0', 0x2030), - ('1', '\'', 0x2032), - ('2', '\'', 0x2033), - ('3', '\'', 0x2034), - ('4', '\'', 0x2057), - ('1', '"', 0x2035), - ('2', '"', 0x2036), - ('3', '"', 0x2037), - ('C', 'a', 0x2038), - ('<', '1', 0x2039), - ('>', '1', 0x203a), - (':', 'X', 0x203b), - ('\'', '-', 0x203e), - ('/', 'f', 0x2044), - ('0', 'S', 0x2070), - ('4', 'S', 0x2074), - ('5', 'S', 0x2075), - ('6', 'S', 0x2076), - ('7', 'S', 0x2077), - ('8', 'S', 0x2078), - ('9', 'S', 0x2079), - ('+', 'S', 0x207a), - ('-', 'S', 0x207b), - ('=', 'S', 0x207c), - ('(', 'S', 0x207d), - (')', 'S', 0x207e), - ('n', 'S', 0x207f), - ('0', 's', 0x2080), - ('1', 's', 0x2081), - ('2', 's', 0x2082), - ('3', 's', 0x2083), - ('4', 's', 0x2084), - ('5', 's', 0x2085), - ('6', 's', 0x2086), - ('7', 's', 0x2087), - ('8', 's', 0x2088), - ('9', 's', 0x2089), - ('+', 's', 0x208a), - ('-', 's', 0x208b), - ('=', 's', 0x208c), - ('(', 's', 0x208d), - (')', 's', 0x208e), - ('L', 'i', 0x20a4), - ('P', 't', 0x20a7), - ('W', '=', 0x20a9), - ('=', 'e', 0x20ac), - ('E', 'u', 0x20ac), - ('=', 'R', 0x20bd), - ('=', 'P', 0x20bd), - ('o', 'C', 0x2103), - ('c', 'o', 0x2105), - ('o', 'F', 0x2109), - ('N', '0', 0x2116), - ('P', 'O', 0x2117), - ('R', 'x', 0x211e), - ('S', 'M', 0x2120), - ('T', 'M', 0x2122), - ('O', 'm', 0x2126), - ('A', 'O', 0x212b), - ('1', '3', 0x2153), - ('2', '3', 0x2154), - ('1', '5', 0x2155), - ('2', '5', 0x2156), - ('3', '5', 0x2157), - ('4', '5', 0x2158), - ('1', '6', 0x2159), - ('5', '6', 0x215a), - ('1', '8', 0x215b), - ('3', '8', 0x215c), - ('5', '8', 0x215d), - ('7', '8', 0x215e), - ('1', 'R', 0x2160), - ('2', 'R', 0x2161), - ('3', 'R', 0x2162), - ('4', 'R', 0x2163), - ('5', 'R', 0x2164), - ('6', 'R', 0x2165), - ('7', 'R', 0x2166), - ('8', 'R', 0x2167), - ('9', 'R', 0x2168), - ('a', 'R', 0x2169), - ('b', 'R', 0x216a), - ('c', 'R', 0x216b), - ('1', 'r', 0x2170), - ('2', 'r', 0x2171), - ('3', 'r', 0x2172), - ('4', 'r', 0x2173), - ('5', 'r', 0x2174), - ('6', 'r', 0x2175), - ('7', 'r', 0x2176), - ('8', 'r', 0x2177), - ('9', 'r', 0x2178), - ('a', 'r', 0x2179), - ('b', 'r', 0x217a), - ('c', 'r', 0x217b), - ('<', '-', 0x2190), - ('-', '!', 0x2191), - ('-', '>', 0x2192), - ('-', 'v', 0x2193), - ('<', '>', 0x2194), - ('U', 'D', 0x2195), - ('<', '=', 0x21d0), - ('=', '>', 0x21d2), - ('=', '=', 0x21d4), - ('F', 'A', 0x2200), - ('d', 'P', 0x2202), - ('T', 'E', 0x2203), - ('/', '0', 0x2205), - ('D', 'E', 0x2206), - ('N', 'B', 0x2207), - ('(', '-', 0x2208), - ('-', ')', 0x220b), - ('*', 'P', 0x220f), - ('+', 'Z', 0x2211), - ('-', '2', 0x2212), - ('-', '+', 0x2213), - ('*', '-', 0x2217), - ('O', 'b', 0x2218), - ('S', 'b', 0x2219), - ('R', 'T', 0x221a), - ('0', '(', 0x221d), - ('0', '0', 0x221e), - ('-', 'L', 0x221f), - ('-', 'V', 0x2220), - ('P', 'P', 0x2225), - ('A', 'N', 0x2227), - ('O', 'R', 0x2228), - ('(', 'U', 0x2229), - (')', 'U', 0x222a), - ('I', 'n', 0x222b), - ('D', 'I', 0x222c), - ('I', 'o', 0x222e), - ('.', ':', 0x2234), - (':', '.', 0x2235), - (':', 'R', 0x2236), - (':', ':', 0x2237), - ('?', '1', 0x223c), - ('C', 'G', 0x223e), - ('?', '-', 0x2243), - ('?', '=', 0x2245), - ('?', '2', 0x2248), - ('=', '?', 0x224c), - ('H', 'I', 0x2253), - ('!', '=', 0x2260), - ('=', '3', 0x2261), - ('=', '<', 0x2264), - ('>', '=', 0x2265), - ('<', '*', 0x226a), - ('*', '>', 0x226b), - ('!', '<', 0x226e), - ('!', '>', 0x226f), - ('(', 'C', 0x2282), - (')', 'C', 0x2283), - ('(', '_', 0x2286), - (')', '_', 0x2287), - ('0', '.', 0x2299), - ('0', '2', 0x229a), - ('-', 'T', 0x22a5), - ('.', 'P', 0x22c5), - (':', '3', 0x22ee), - ('.', '3', 0x22ef), - ('E', 'h', 0x2302), - ('<', '7', 0x2308), - ('>', '7', 0x2309), - ('7', '<', 0x230a), - ('7', '>', 0x230b), - ('N', 'I', 0x2310), - ('(', 'A', 0x2312), - ('T', 'R', 0x2315), - ('I', 'u', 0x2320), - ('I', 'l', 0x2321), - ('<', '/', 0x2329), - ('/', '>', 0x232a), - ('V', 's', 0x2423), - ('1', 'h', 0x2440), - ('3', 'h', 0x2441), - ('2', 'h', 0x2442), - ('4', 'h', 0x2443), - ('1', 'j', 0x2446), - ('2', 'j', 0x2447), - ('3', 'j', 0x2448), - ('4', 'j', 0x2449), - ('1', '.', 0x2488), - ('2', '.', 0x2489), - ('3', '.', 0x248a), - ('4', '.', 0x248b), - ('5', '.', 0x248c), - ('6', '.', 0x248d), - ('7', '.', 0x248e), - ('8', '.', 0x248f), - ('9', '.', 0x2490), - ('h', 'h', 0x2500), - ('H', 'H', 0x2501), - ('v', 'v', 0x2502), - ('V', 'V', 0x2503), - ('3', '-', 0x2504), - ('3', '_', 0x2505), - ('3', '!', 0x2506), - ('3', '/', 0x2507), - ('4', '-', 0x2508), - ('4', '_', 0x2509), - ('4', '!', 0x250a), - ('4', '/', 0x250b), - ('d', 'r', 0x250c), - ('d', 'R', 0x250d), - ('D', 'r', 0x250e), - ('D', 'R', 0x250f), - ('d', 'l', 0x2510), - ('d', 'L', 0x2511), - ('D', 'l', 0x2512), - ('L', 'D', 0x2513), - ('u', 'r', 0x2514), - ('u', 'R', 0x2515), - ('U', 'r', 0x2516), - ('U', 'R', 0x2517), - ('u', 'l', 0x2518), - ('u', 'L', 0x2519), - ('U', 'l', 0x251a), - ('U', 'L', 0x251b), - ('v', 'r', 0x251c), - ('v', 'R', 0x251d), - ('V', 'r', 0x2520), - ('V', 'R', 0x2523), - ('v', 'l', 0x2524), - ('v', 'L', 0x2525), - ('V', 'l', 0x2528), - ('V', 'L', 0x252b), - ('d', 'h', 0x252c), - ('d', 'H', 0x252f), - ('D', 'h', 0x2530), - ('D', 'H', 0x2533), - ('u', 'h', 0x2534), - ('u', 'H', 0x2537), - ('U', 'h', 0x2538), - ('U', 'H', 0x253b), - ('v', 'h', 0x253c), - ('v', 'H', 0x253f), - ('V', 'h', 0x2542), - ('V', 'H', 0x254b), - ('F', 'D', 0x2571), - ('B', 'D', 0x2572), - ('T', 'B', 0x2580), - ('L', 'B', 0x2584), - ('F', 'B', 0x2588), - ('l', 'B', 0x258c), - ('R', 'B', 0x2590), - ('.', 'S', 0x2591), - (':', 'S', 0x2592), - ('?', 'S', 0x2593), - ('f', 'S', 0x25a0), - ('O', 'S', 0x25a1), - ('R', 'O', 0x25a2), - ('R', 'r', 0x25a3), - ('R', 'F', 0x25a4), - ('R', 'Y', 0x25a5), - ('R', 'H', 0x25a6), - ('R', 'Z', 0x25a7), - ('R', 'K', 0x25a8), - ('R', 'X', 0x25a9), - ('s', 'B', 0x25aa), - ('S', 'R', 0x25ac), - ('O', 'r', 0x25ad), - ('U', 'T', 0x25b2), - ('u', 'T', 0x25b3), - ('P', 'R', 0x25b6), - ('T', 'r', 0x25b7), - ('D', 't', 0x25bc), - ('d', 'T', 0x25bd), - ('P', 'L', 0x25c0), - ('T', 'l', 0x25c1), - ('D', 'b', 0x25c6), - ('D', 'w', 0x25c7), - ('L', 'Z', 0x25ca), - ('0', 'm', 0x25cb), - ('0', 'o', 0x25ce), - ('0', 'M', 0x25cf), - ('0', 'L', 0x25d0), - ('0', 'R', 0x25d1), - ('S', 'n', 0x25d8), - ('I', 'c', 0x25d9), - ('F', 'd', 0x25e2), - ('B', 'd', 0x25e3), - ('*', '2', 0x2605), - ('*', '1', 0x2606), - ('<', 'H', 0x261c), - ('>', 'H', 0x261e), - ('0', 'u', 0x263a), - ('0', 'U', 0x263b), - ('S', 'U', 0x263c), - ('F', 'm', 0x2640), - ('M', 'l', 0x2642), - ('c', 'S', 0x2660), - ('c', 'H', 0x2661), - ('c', 'D', 0x2662), - ('c', 'C', 0x2663), - ('M', 'd', 0x2669), - ('M', '8', 0x266a), - ('M', '2', 0x266b), - ('M', 'b', 0x266d), - ('M', 'x', 0x266e), - ('M', 'X', 0x266f), - ('O', 'K', 0x2713), - ('X', 'X', 0x2717), - ('-', 'X', 0x2720), - ('I', 'S', 0x3000), - (',', '_', 0x3001), - ('.', '_', 0x3002), - ('+', '"', 0x3003), - ('+', '_', 0x3004), - ('*', '_', 0x3005), - (';', '_', 0x3006), - ('0', '_', 0x3007), - ('<', '+', 0x300a), - ('>', '+', 0x300b), - ('<', '\'', 0x300c), - ('>', '\'', 0x300d), - ('<', '"', 0x300e), - ('>', '"', 0x300f), - ('(', '"', 0x3010), - (')', '"', 0x3011), - ('=', 'T', 0x3012), - ('=', '_', 0x3013), - ('(', '\'', 0x3014), - (')', '\'', 0x3015), - ('(', 'I', 0x3016), - (')', 'I', 0x3017), - ('-', '?', 0x301c), - ('A', '5', 0x3041), - ('a', '5', 0x3042), - ('I', '5', 0x3043), - ('i', '5', 0x3044), - ('U', '5', 0x3045), - ('u', '5', 0x3046), - ('E', '5', 0x3047), - ('e', '5', 0x3048), - ('O', '5', 0x3049), - ('o', '5', 0x304a), - ('k', 'a', 0x304b), - ('g', 'a', 0x304c), - ('k', 'i', 0x304d), - ('g', 'i', 0x304e), - ('k', 'u', 0x304f), - ('g', 'u', 0x3050), - ('k', 'e', 0x3051), - ('g', 'e', 0x3052), - ('k', 'o', 0x3053), - ('g', 'o', 0x3054), - ('s', 'a', 0x3055), - ('z', 'a', 0x3056), - ('s', 'i', 0x3057), - ('z', 'i', 0x3058), - ('s', 'u', 0x3059), - ('z', 'u', 0x305a), - ('s', 'e', 0x305b), - ('z', 'e', 0x305c), - ('s', 'o', 0x305d), - ('z', 'o', 0x305e), - ('t', 'a', 0x305f), - ('d', 'a', 0x3060), - ('t', 'i', 0x3061), - ('d', 'i', 0x3062), - ('t', 'U', 0x3063), - ('t', 'u', 0x3064), - ('d', 'u', 0x3065), - ('t', 'e', 0x3066), - ('d', 'e', 0x3067), - ('t', 'o', 0x3068), - ('d', 'o', 0x3069), - ('n', 'a', 0x306a), - ('n', 'i', 0x306b), - ('n', 'u', 0x306c), - ('n', 'e', 0x306d), - ('n', 'o', 0x306e), - ('h', 'a', 0x306f), - ('b', 'a', 0x3070), - ('p', 'a', 0x3071), - ('h', 'i', 0x3072), - ('b', 'i', 0x3073), - ('p', 'i', 0x3074), - ('h', 'u', 0x3075), - ('b', 'u', 0x3076), - ('p', 'u', 0x3077), - ('h', 'e', 0x3078), - ('b', 'e', 0x3079), - ('p', 'e', 0x307a), - ('h', 'o', 0x307b), - ('b', 'o', 0x307c), - ('p', 'o', 0x307d), - ('m', 'a', 0x307e), - ('m', 'i', 0x307f), - ('m', 'u', 0x3080), - ('m', 'e', 0x3081), - ('m', 'o', 0x3082), - ('y', 'A', 0x3083), - ('y', 'a', 0x3084), - ('y', 'U', 0x3085), - ('y', 'u', 0x3086), - ('y', 'O', 0x3087), - ('y', 'o', 0x3088), - ('r', 'a', 0x3089), - ('r', 'i', 0x308a), - ('r', 'u', 0x308b), - ('r', 'e', 0x308c), - ('r', 'o', 0x308d), - ('w', 'A', 0x308e), - ('w', 'a', 0x308f), - ('w', 'i', 0x3090), - ('w', 'e', 0x3091), - ('w', 'o', 0x3092), - ('n', '5', 0x3093), - ('v', 'u', 0x3094), - ('"', '5', 0x309b), - ('0', '5', 0x309c), - ('*', '5', 0x309d), - ('+', '5', 0x309e), - ('a', '6', 0x30a1), - ('A', '6', 0x30a2), - ('i', '6', 0x30a3), - ('I', '6', 0x30a4), - ('u', '6', 0x30a5), - ('U', '6', 0x30a6), - ('e', '6', 0x30a7), - ('E', '6', 0x30a8), - ('o', '6', 0x30a9), - ('O', '6', 0x30aa), - ('K', 'a', 0x30ab), - ('G', 'a', 0x30ac), - ('K', 'i', 0x30ad), - ('G', 'i', 0x30ae), - ('K', 'u', 0x30af), - ('G', 'u', 0x30b0), - ('K', 'e', 0x30b1), - ('G', 'e', 0x30b2), - ('K', 'o', 0x30b3), - ('G', 'o', 0x30b4), - ('S', 'a', 0x30b5), - ('Z', 'a', 0x30b6), - ('S', 'i', 0x30b7), - ('Z', 'i', 0x30b8), - ('S', 'u', 0x30b9), - ('Z', 'u', 0x30ba), - ('S', 'e', 0x30bb), - ('Z', 'e', 0x30bc), - ('S', 'o', 0x30bd), - ('Z', 'o', 0x30be), - ('T', 'a', 0x30bf), - ('D', 'a', 0x30c0), - ('T', 'i', 0x30c1), - ('D', 'i', 0x30c2), - ('T', 'U', 0x30c3), - ('T', 'u', 0x30c4), - ('D', 'u', 0x30c5), - ('T', 'e', 0x30c6), - ('D', 'e', 0x30c7), - ('T', 'o', 0x30c8), - ('D', 'o', 0x30c9), - ('N', 'a', 0x30ca), - ('N', 'i', 0x30cb), - ('N', 'u', 0x30cc), - ('N', 'e', 0x30cd), - ('N', 'o', 0x30ce), - ('H', 'a', 0x30cf), - ('B', 'a', 0x30d0), - ('P', 'a', 0x30d1), - ('H', 'i', 0x30d2), - ('B', 'i', 0x30d3), - ('P', 'i', 0x30d4), - ('H', 'u', 0x30d5), - ('B', 'u', 0x30d6), - ('P', 'u', 0x30d7), - ('H', 'e', 0x30d8), - ('B', 'e', 0x30d9), - ('P', 'e', 0x30da), - ('H', 'o', 0x30db), - ('B', 'o', 0x30dc), - ('P', 'o', 0x30dd), - ('M', 'a', 0x30de), - ('M', 'i', 0x30df), - ('M', 'u', 0x30e0), - ('M', 'e', 0x30e1), - ('M', 'o', 0x30e2), - ('Y', 'A', 0x30e3), - ('Y', 'a', 0x30e4), - ('Y', 'U', 0x30e5), - ('Y', 'u', 0x30e6), - ('Y', 'O', 0x30e7), - ('Y', 'o', 0x30e8), - ('R', 'a', 0x30e9), - ('R', 'i', 0x30ea), - ('R', 'u', 0x30eb), - ('R', 'e', 0x30ec), - ('R', 'o', 0x30ed), - ('W', 'A', 0x30ee), - ('W', 'a', 0x30ef), - ('W', 'i', 0x30f0), - ('W', 'e', 0x30f1), - ('W', 'o', 0x30f2), - ('N', '6', 0x30f3), - ('V', 'u', 0x30f4), - ('K', 'A', 0x30f5), - ('K', 'E', 0x30f6), - ('V', 'a', 0x30f7), - ('V', 'i', 0x30f8), - ('V', 'e', 0x30f9), - ('V', 'o', 0x30fa), - ('.', '6', 0x30fb), - ('-', '6', 0x30fc), - ('*', '6', 0x30fd), - ('+', '6', 0x30fe), - ('b', '4', 0x3105), - ('p', '4', 0x3106), - ('m', '4', 0x3107), - ('f', '4', 0x3108), - ('d', '4', 0x3109), - ('t', '4', 0x310a), - ('n', '4', 0x310b), - ('l', '4', 0x310c), - ('g', '4', 0x310d), - ('k', '4', 0x310e), - ('h', '4', 0x310f), - ('j', '4', 0x3110), - ('q', '4', 0x3111), - ('x', '4', 0x3112), - ('z', 'h', 0x3113), - ('c', 'h', 0x3114), - ('s', 'h', 0x3115), - ('r', '4', 0x3116), - ('z', '4', 0x3117), - ('c', '4', 0x3118), - ('s', '4', 0x3119), - ('a', '4', 0x311a), - ('o', '4', 0x311b), - ('e', '4', 0x311c), - ('a', 'i', 0x311e), - ('e', 'i', 0x311f), - ('a', 'u', 0x3120), - ('o', 'u', 0x3121), - ('a', 'n', 0x3122), - ('e', 'n', 0x3123), - ('a', 'N', 0x3124), - ('e', 'N', 0x3125), - ('e', 'r', 0x3126), - ('i', '4', 0x3127), - ('u', '4', 0x3128), - ('i', 'u', 0x3129), - ('v', '4', 0x312a), - ('n', 'G', 0x312b), - ('g', 'n', 0x312c), - ('1', 'c', 0x3220), - ('2', 'c', 0x3221), - ('3', 'c', 0x3222), - ('4', 'c', 0x3223), - ('5', 'c', 0x3224), - ('6', 'c', 0x3225), - ('7', 'c', 0x3226), - ('8', 'c', 0x3227), - ('9', 'c', 0x3228), - ('f', 'f', 0xfb00), - ('f', 'i', 0xfb01), - ('f', 'l', 0xfb02), - ('f', 't', 0xfb05), - ('s', 't', 0xfb06), -]; diff --git a/crates/vim/src/helix.rs b/crates/vim/src/helix.rs deleted file mode 100644 index fae2bda578..0000000000 --- a/crates/vim/src/helix.rs +++ /dev/null @@ -1,1669 +0,0 @@ -mod boundary; -mod duplicate; -mod object; -mod paste; -mod select; - -use editor::display_map::DisplaySnapshot; -use editor::{ - DisplayPoint, Editor, EditorSettings, HideMouseCursorOrigin, MultiBufferOffset, - SelectionEffects, ToOffset, ToPoint, movement, -}; -use gpui::actions; -use gpui::{Context, Window}; -use language::{CharClassifier, CharKind, Point}; -use search::{BufferSearchBar, SearchOptions}; -use settings::Settings; -use text::{Bias, SelectionGoal}; -use workspace::searchable::FilteredSearchRange; -use workspace::searchable::{self, Direction}; - -use crate::motion::{self, MotionKind}; -use crate::state::SearchState; -use crate::{ - Vim, - motion::{Motion, right}, - state::Mode, -}; - -actions!( - vim, - [ - /// Yanks the current selection or character if no selection. - HelixYank, - /// Inserts at the beginning of the selection. - HelixInsert, - /// Appends at the end of the selection. - HelixAppend, - /// Goes to the location of the last modification. - HelixGotoLastModification, - /// Select entire line or multiple lines, extending downwards. - HelixSelectLine, - /// Select all matches of a given pattern within the current selection. - HelixSelectRegex, - /// Removes all but the one selection that was created last. - /// `Newest` can eventually be `Primary`. - HelixKeepNewestSelection, - /// Copies all selections below. - HelixDuplicateBelow, - /// Copies all selections above. - HelixDuplicateAbove, - /// Delete the selection and enter edit mode. - HelixSubstitute, - /// Delete the selection and enter edit mode, without yanking the selection. - HelixSubstituteNoYank, - /// Delete the selection and enter edit mode. - HelixSelectNext, - /// Delete the selection and enter edit mode, without yanking the selection. - HelixSelectPrevious, - ] -); - -pub fn register(editor: &mut Editor, cx: &mut Context) { - Vim::action(editor, cx, Vim::helix_select_lines); - 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); - Vim::action(editor, cx, Vim::helix_paste); - Vim::action(editor, cx, Vim::helix_select_regex); - Vim::action(editor, cx, Vim::helix_keep_newest_selection); - Vim::action(editor, cx, |vim, _: &HelixDuplicateBelow, window, cx| { - let times = Vim::take_count(cx); - vim.helix_duplicate_selections_below(times, window, cx); - }); - Vim::action(editor, cx, |vim, _: &HelixDuplicateAbove, window, cx| { - let times = Vim::take_count(cx); - vim.helix_duplicate_selections_above(times, window, cx); - }); - Vim::action(editor, cx, Vim::helix_substitute); - Vim::action(editor, cx, Vim::helix_substitute_no_yank); - Vim::action(editor, cx, Vim::helix_select_next); - Vim::action(editor, cx, Vim::helix_select_previous); -} - -impl Vim { - pub fn helix_normal_motion( - &mut self, - motion: Motion, - times: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.helix_move_cursor(motion, times, window, cx); - } - - pub fn helix_select_motion( - &mut self, - motion: Motion, - times: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.update_editor(cx, |_, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - editor.change_selections(Default::default(), window, cx, |s| { - if let Motion::ZedSearchResult { new_selections, .. } = &motion { - s.select_anchor_ranges(new_selections.clone()); - return; - }; - - s.move_with(|map, selection| { - let was_reversed = selection.reversed; - let mut current_head = selection.head(); - - // our motions assume the current character is after the cursor, - // but in (forward) visual mode the current character is just - // before the end of the selection. - - // If the file ends with a newline (which is common) we don't do this. - // so that if you go to the end of such a file you can use "up" to go - // to the previous line and have it work somewhat as expected. - if !selection.reversed - && !selection.is_empty() - && !(selection.end.column() == 0 && selection.end == map.max_point()) - { - current_head = movement::left(map, selection.end) - } - - let (new_head, goal) = match motion { - // Going to next word start is special cased - // since Vim differs from Helix in that motion - // Vim: `w` goes to the first character of a word - // Helix: `w` goes to the character before a word - Motion::NextWordStart { ignore_punctuation } => { - let mut head = movement::right(map, current_head); - let classifier = - map.buffer_snapshot().char_classifier_at(head.to_point(map)); - for _ in 0..times.unwrap_or(1) { - let (_, new_head) = - movement::find_boundary_trail(map, head, |left, right| { - Self::is_boundary_right(ignore_punctuation)( - left, - right, - &classifier, - ) - }); - head = new_head; - } - head = movement::left(map, head); - (head, SelectionGoal::None) - } - _ => motion - .move_point( - map, - current_head, - selection.goal, - times, - &text_layout_details, - ) - .unwrap_or((current_head, selection.goal)), - }; - - selection.set_head(new_head, goal); - - // ensure the current character is included in the selection. - if !selection.reversed { - let next_point = movement::right(map, selection.end); - - if !(next_point.column() == 0 && next_point == map.max_point()) { - selection.end = next_point; - } - } - - // vim always ensures the anchor character stays selected. - // if our selection has reversed, we need to move the opposite end - // to ensure the anchor is still selected. - if was_reversed && !selection.reversed { - selection.start = movement::left(map, selection.start); - } else if !was_reversed && selection.reversed { - selection.end = movement::right(map, selection.end); - } - }) - }); - }); - } - - /// Updates all selections based on where the cursors are. - fn helix_new_selections( - &mut self, - window: &mut Window, - cx: &mut Context, - mut change: impl FnMut( - // the start of the cursor - DisplayPoint, - &DisplaySnapshot, - ) -> Option<(DisplayPoint, DisplayPoint)>, - ) { - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let cursor_start = if selection.reversed || selection.is_empty() { - selection.head() - } else { - movement::left(map, selection.head()) - }; - let Some((head, tail)) = change(cursor_start, map) else { - return; - }; - - selection.set_head_tail(head, tail, SelectionGoal::None); - }); - }); - }); - } - - fn helix_find_range_forward( - &mut self, - times: Option, - window: &mut Window, - cx: &mut Context, - mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool, - ) { - let times = times.unwrap_or(1); - self.helix_new_selections(window, cx, |cursor, map| { - let mut head = movement::right(map, cursor); - let mut tail = cursor; - let classifier = map.buffer_snapshot().char_classifier_at(head.to_point(map)); - if head == map.max_point() { - return None; - } - for _ in 0..times { - let (maybe_next_tail, next_head) = - movement::find_boundary_trail(map, head, |left, right| { - is_boundary(left, right, &classifier) - }); - - if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail { - break; - } - - head = next_head; - if let Some(next_tail) = maybe_next_tail { - tail = next_tail; - } - } - Some((head, tail)) - }); - } - - fn helix_find_range_backward( - &mut self, - times: Option, - window: &mut Window, - cx: &mut Context, - mut is_boundary: impl FnMut(char, char, &CharClassifier) -> bool, - ) { - let times = times.unwrap_or(1); - self.helix_new_selections(window, cx, |cursor, map| { - let mut head = cursor; - // The original cursor was one character wide, - // but the search starts from the left side of it, - // so to include that space the selection must end one character to the right. - let mut tail = movement::right(map, cursor); - let classifier = map.buffer_snapshot().char_classifier_at(head.to_point(map)); - if head == DisplayPoint::zero() { - return None; - } - for _ in 0..times { - let (maybe_next_tail, next_head) = - movement::find_preceding_boundary_trail(map, head, |left, right| { - is_boundary(left, right, &classifier) - }); - - if next_head == head && maybe_next_tail.unwrap_or(next_head) == tail { - break; - } - - head = next_head; - if let Some(next_tail) = maybe_next_tail { - tail = next_tail; - } - } - Some((head, tail)) - }); - } - - pub fn helix_move_and_collapse( - &mut self, - motion: Motion, - times: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.update_editor(cx, |_, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let goal = selection.goal; - let cursor = if selection.is_empty() || selection.reversed { - selection.head() - } else { - movement::left(map, selection.head()) - }; - - let (point, goal) = motion - .move_point(map, cursor, selection.goal, times, &text_layout_details) - .unwrap_or((cursor, goal)); - - selection.collapse_to(point, goal) - }) - }); - }); - } - - fn is_boundary_right( - ignore_punctuation: bool, - ) -> impl FnMut(char, char, &CharClassifier) -> bool { - move |left, right, classifier| { - let left_kind = classifier.kind_with(left, ignore_punctuation); - let right_kind = classifier.kind_with(right, ignore_punctuation); - let at_newline = (left == '\n') ^ (right == '\n'); - - (left_kind != right_kind && right_kind != CharKind::Whitespace) || at_newline - } - } - - fn is_boundary_left( - ignore_punctuation: bool, - ) -> impl FnMut(char, char, &CharClassifier) -> bool { - move |left, right, classifier| { - let left_kind = classifier.kind_with(left, ignore_punctuation); - let right_kind = classifier.kind_with(right, ignore_punctuation); - let at_newline = (left == '\n') ^ (right == '\n'); - - (left_kind != right_kind && left_kind != CharKind::Whitespace) || at_newline - } - } - - pub fn helix_move_cursor( - &mut self, - motion: Motion, - times: Option, - window: &mut Window, - cx: &mut Context, - ) { - match motion { - Motion::NextWordStart { ignore_punctuation } => self.helix_find_range_forward( - times, - window, - cx, - Self::is_boundary_right(ignore_punctuation), - ), - Motion::NextWordEnd { ignore_punctuation } => self.helix_find_range_forward( - times, - window, - cx, - Self::is_boundary_left(ignore_punctuation), - ), - Motion::PreviousWordStart { ignore_punctuation } => self.helix_find_range_backward( - times, - window, - cx, - Self::is_boundary_left(ignore_punctuation), - ), - Motion::PreviousWordEnd { ignore_punctuation } => self.helix_find_range_backward( - times, - window, - cx, - Self::is_boundary_right(ignore_punctuation), - ), - Motion::EndOfLine { .. } => { - // In Helix mode, EndOfLine should position cursor ON the last character, - // not after it. We therefore need special handling for it. - self.update_editor(cx, |_, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let goal = selection.goal; - let cursor = if selection.is_empty() || selection.reversed { - selection.head() - } else { - movement::left(map, selection.head()) - }; - - let (point, _goal) = motion - .move_point(map, cursor, goal, times, &text_layout_details) - .unwrap_or((cursor, goal)); - - // Move left by one character to position on the last character - let adjusted_point = movement::saturating_left(map, point); - selection.collapse_to(adjusted_point, SelectionGoal::None) - }) - }); - }); - } - Motion::FindForward { - before, - char, - mode, - smartcase, - } => { - self.helix_new_selections(window, cx, |cursor, map| { - let start = cursor; - let mut last_boundary = start; - for _ in 0..times.unwrap_or(1) { - last_boundary = movement::find_boundary( - map, - movement::right(map, last_boundary), - mode, - |left, right| { - let current_char = if before { right } else { left }; - motion::is_character_match(char, current_char, smartcase) - }, - ); - } - Some((last_boundary, start)) - }); - } - Motion::FindBackward { - after, - char, - mode, - smartcase, - } => { - self.helix_new_selections(window, cx, |cursor, map| { - let start = cursor; - let mut last_boundary = start; - for _ in 0..times.unwrap_or(1) { - last_boundary = movement::find_preceding_boundary_display_point( - map, - last_boundary, - mode, - |left, right| { - let current_char = if after { left } else { right }; - motion::is_character_match(char, current_char, smartcase) - }, - ); - } - // The original cursor was one character wide, - // but the search started from the left side of it, - // so to include that space the selection must end one character to the right. - Some((last_boundary, movement::right(map, start))) - }); - } - _ => self.helix_move_and_collapse(motion, times, window, cx), - } - } - - pub fn helix_yank(&mut self, _: &HelixYank, window: &mut Window, cx: &mut Context) { - self.update_editor(cx, |vim, editor, cx| { - let has_selection = editor - .selections - .all_adjusted(&editor.display_snapshot(cx)) - .iter() - .any(|selection| !selection.is_empty()); - - if !has_selection { - // If no selection, expand to current character (like 'v' does) - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let head = selection.head(); - let new_head = movement::saturating_right(map, head); - selection.set_tail(head, SelectionGoal::None); - selection.set_head(new_head, SelectionGoal::None); - }); - }); - vim.yank_selections_content( - editor, - crate::motion::MotionKind::Exclusive, - window, - cx, - ); - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|_map, selection| { - selection.collapse_to(selection.start, SelectionGoal::None); - }); - }); - } else { - // Yank the selection(s) - vim.yank_selections_content( - editor, - crate::motion::MotionKind::Exclusive, - window, - cx, - ); - } - }); - - // Drop back to normal mode after yanking - self.switch_mode(Mode::HelixNormal, true, window, cx); - } - - fn helix_insert(&mut self, _: &HelixInsert, window: &mut Window, cx: &mut Context) { - self.start_recording(cx); - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|_map, selection| { - // In helix normal mode, move cursor to start of selection and collapse - if !selection.is_empty() { - selection.collapse_to(selection.start, SelectionGoal::None); - } - }); - }); - }); - self.switch_mode(Mode::Insert, false, window, cx); - } - - fn helix_select_regex( - &mut self, - _: &HelixSelectRegex, - window: &mut Window, - cx: &mut Context, - ) { - Vim::take_forced_motion(cx); - let Some(pane) = self.pane(window, cx) else { - return; - }; - let prior_selections = self.editor_selections(window, cx); - pane.update(cx, |pane, cx| { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - search_bar.update(cx, |search_bar, cx| { - if !search_bar.show(window, cx) { - return; - } - - search_bar.select_query(window, cx); - cx.focus_self(window); - - search_bar.set_replacement(None, cx); - let mut options = SearchOptions::NONE; - options |= SearchOptions::REGEX; - if EditorSettings::get_global(cx).search.case_sensitive { - options |= SearchOptions::CASE_SENSITIVE; - } - search_bar.set_search_options(options, cx); - if let Some(search) = search_bar.set_search_within_selection( - Some(FilteredSearchRange::Selection), - window, - cx, - ) { - cx.spawn_in(window, async move |search_bar, cx| { - if search.await.is_ok() { - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.activate_current_match(window, cx) - }) - } else { - Ok(()) - } - }) - .detach_and_log_err(cx); - } - self.search = SearchState { - direction: searchable::Direction::Next, - count: 1, - prior_selections, - prior_operator: self.operator_stack.last().cloned(), - prior_mode: self.mode, - helix_select: true, - } - }); - } - }); - self.start_recording(cx); - } - - fn helix_append(&mut self, _: &HelixAppend, window: &mut Window, cx: &mut Context) { - self.start_recording(cx); - self.switch_mode(Mode::Insert, false, window, cx); - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let point = if selection.is_empty() { - right(map, selection.head(), 1) - } else { - selection.end - }; - selection.collapse_to(point, SelectionGoal::None); - }); - }); - }); - } - - pub fn helix_replace(&mut self, text: &str, window: &mut Window, cx: &mut Context) { - self.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - let display_map = editor.display_snapshot(cx); - let selections = editor.selections.all_display(&display_map); - - // Store selection info for positioning after edit - let selection_info: Vec<_> = selections - .iter() - .map(|selection| { - let range = selection.range(); - let start_offset = range.start.to_offset(&display_map, Bias::Left); - let end_offset = range.end.to_offset(&display_map, Bias::Left); - let was_empty = range.is_empty(); - let was_reversed = selection.reversed; - ( - display_map.buffer_snapshot().anchor_before(start_offset), - end_offset - start_offset, - was_empty, - was_reversed, - ) - }) - .collect(); - - let mut edits = Vec::new(); - for selection in &selections { - let mut range = selection.range(); - - // For empty selections, extend to replace one character - if range.is_empty() { - range.end = movement::saturating_right(&display_map, range.start); - } - - let byte_range = range.start.to_offset(&display_map, Bias::Left) - ..range.end.to_offset(&display_map, Bias::Left); - - if !byte_range.is_empty() { - let replacement_text = text.repeat(byte_range.end - byte_range.start); - edits.push((byte_range, replacement_text)); - } - } - - editor.edit(edits, cx); - - // Restore selections based on original info - let snapshot = editor.buffer().read(cx).snapshot(cx); - let ranges: Vec<_> = selection_info - .into_iter() - .map(|(start_anchor, original_len, was_empty, was_reversed)| { - let start_point = start_anchor.to_point(&snapshot); - if was_empty { - // For cursor-only, collapse to start - start_point..start_point - } else { - // For selections, span the replaced text - let replacement_len = text.len() * original_len; - let end_offset = start_anchor.to_offset(&snapshot) + replacement_len; - let end_point = snapshot.offset_to_point(end_offset); - if was_reversed { - end_point..start_point - } else { - start_point..end_point - } - } - }) - .collect(); - - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges(ranges); - }); - }); - }); - 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); - } - - pub fn helix_select_lines( - &mut self, - _: &HelixSelectLine, - window: &mut Window, - cx: &mut Context, - ) { - let count = Vim::take_count(cx).unwrap_or(1); - self.update_editor(cx, |_, editor, cx| { - editor.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx); - let display_map = editor.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = editor.selections.all::(&display_map); - let max_point = display_map.buffer_snapshot().max_point(); - let buffer_snapshot = &display_map.buffer_snapshot(); - - for selection in &mut selections { - // Start always goes to column 0 of the first selected line - let start_row = selection.start.row; - let current_end_row = selection.end.row; - - // Check if cursor is on empty line by checking first character - let line_start_offset = buffer_snapshot.point_to_offset(Point::new(start_row, 0)); - let first_char = buffer_snapshot.chars_at(line_start_offset).next(); - let extra_line = if first_char == Some('\n') { 1 } else { 0 }; - - let end_row = current_end_row + count as u32 + extra_line; - - selection.start = Point::new(start_row, 0); - selection.end = if end_row > max_point.row { - max_point - } else { - Point::new(end_row, 0) - }; - selection.reversed = false; - } - - editor.change_selections(Default::default(), window, cx, |s| { - s.select(selections); - }); - }); - } - - fn helix_keep_newest_selection( - &mut self, - _: &HelixKeepNewestSelection, - window: &mut Window, - cx: &mut Context, - ) { - self.update_editor(cx, |_, editor, cx| { - let newest = editor - .selections - .newest::(&editor.display_snapshot(cx)); - editor.change_selections(Default::default(), window, cx, |s| s.select(vec![newest])); - }); - } - - fn do_helix_substitute(&mut self, yank: bool, window: &mut Window, cx: &mut Context) { - self.update_editor(cx, |vim, editor, cx| { - editor.set_clip_at_line_ends(false, cx); - editor.transact(window, cx, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - if selection.start == selection.end { - selection.end = movement::right(map, selection.end); - } - - // If the selection starts and ends on a newline, we exclude the last one. - if !selection.is_empty() - && selection.start.column() == 0 - && selection.end.column() == 0 - { - selection.end = movement::left(map, selection.end); - } - }) - }); - if yank { - vim.copy_selections_content(editor, MotionKind::Exclusive, window, cx); - } - let selections = editor - .selections - .all::(&editor.display_snapshot(cx)) - .into_iter(); - let edits = selections.map(|selection| (selection.start..selection.end, "")); - editor.edit(edits, cx); - }); - }); - self.switch_mode(Mode::Insert, true, window, cx); - } - - fn helix_substitute( - &mut self, - _: &HelixSubstitute, - window: &mut Window, - cx: &mut Context, - ) { - self.do_helix_substitute(true, window, cx); - } - - fn helix_substitute_no_yank( - &mut self, - _: &HelixSubstituteNoYank, - window: &mut Window, - cx: &mut Context, - ) { - self.do_helix_substitute(false, window, cx); - } - - fn helix_select_next( - &mut self, - _: &HelixSelectNext, - window: &mut Window, - cx: &mut Context, - ) { - self.do_helix_select(Direction::Next, window, cx); - } - - fn helix_select_previous( - &mut self, - _: &HelixSelectPrevious, - window: &mut Window, - cx: &mut Context, - ) { - self.do_helix_select(Direction::Prev, window, cx); - } - - fn do_helix_select( - &mut self, - direction: searchable::Direction, - window: &mut Window, - cx: &mut Context, - ) { - let Some(pane) = self.pane(window, cx) else { - return; - }; - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - let prior_selections = self.editor_selections(window, cx); - - let success = pane.update(cx, |pane, cx| { - let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() else { - return false; - }; - search_bar.update(cx, |search_bar, cx| { - if !search_bar.has_active_match() || !search_bar.show(window, cx) { - return false; - } - search_bar.select_match(direction, count, window, cx); - true - }) - }); - - if !success { - return; - } - if self.mode == Mode::HelixSelect { - self.update_editor(cx, |_vim, editor, cx| { - let snapshot = editor.snapshot(window, cx); - editor.change_selections(SelectionEffects::default(), window, cx, |s| { - s.select_anchor_ranges( - prior_selections - .iter() - .cloned() - .chain(s.all_anchors(&snapshot).iter().map(|s| s.range())), - ); - }) - }); - } - } -} - -#[cfg(test)] -mod test { - use indoc::indoc; - - use crate::{state::Mode, test::VimTestContext}; - - #[gpui::test] - async fn test_word_motions(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - // « - // ˇ - // » - cx.set_state( - indoc! {" - Th«e quiˇ»ck brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("w"); - - cx.assert_state( - indoc! {" - The qu«ick ˇ»brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("w"); - - cx.assert_state( - indoc! {" - The quick «brownˇ» - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("2 b"); - - cx.assert_state( - indoc! {" - The «ˇquick »brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("down e up"); - - cx.assert_state( - indoc! {" - The quicˇk brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - cx.set_state("aa\n «ˇbb»", Mode::HelixNormal); - - cx.simulate_keystroke("b"); - - cx.assert_state("aa\n«ˇ »bb", Mode::HelixNormal); - } - - #[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( - indoc! {" - The qu«ick ˇ»brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("d"); - - cx.assert_state( - indoc! {" - The quˇbrown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - // test deleting a single character - cx.simulate_keystrokes("d"); - - cx.assert_state( - indoc! {" - The quˇrown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - } - - #[gpui::test] - async fn test_delete_character_end_of_line(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! {" - The quick brownˇ - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("d"); - - cx.assert_state( - indoc! {" - The quick brownˇfox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - } - - // #[gpui::test] - // async fn test_delete_character_end_of_buffer(cx: &mut gpui::TestAppContext) { - // let mut cx = VimTestContext::new(cx, true).await; - - // cx.set_state( - // indoc! {" - // The quick brown - // fox jumps over - // the lazy dog.ˇ"}, - // Mode::HelixNormal, - // ); - - // cx.simulate_keystrokes("d"); - - // cx.assert_state( - // indoc! {" - // The quick brown - // fox jumps over - // the lazy dog.ˇ"}, - // Mode::HelixNormal, - // ); - // } - - #[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! {" - The quˇick brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("f z"); - - cx.assert_state( - indoc! {" - The qu«ick brown - fox jumps over - the lazˇ»y dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("F e F e"); - - cx.assert_state( - indoc! {" - The quick brown - fox jumps ov«ˇer - the» lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("e 2 F e"); - - cx.assert_state( - indoc! {" - Th«ˇe quick brown - fox jumps over» - the lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("t r t r"); - - cx.assert_state( - indoc! {" - The quick «brown - fox jumps oveˇ»r - the lazy dog."}, - Mode::HelixNormal, - ); - } - - #[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); - - cx.simulate_keystroke("w"); - - cx.assert_state("aa\n«bb ˇ»cc", Mode::HelixNormal); - - cx.set_state("aa«\nˇ»", Mode::HelixNormal); - - cx.simulate_keystroke("b"); - - cx.assert_state("«ˇaa»\n", Mode::HelixNormal); - } - - #[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 - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("i"); - - cx.assert_state( - indoc! {" - ˇThe quick brown - fox jumps over - the lazy dog."}, - Mode::Insert, - ); - } - - #[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! {" - «Theˇ» quick brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("a"); - - cx.assert_state( - indoc! {" - Theˇ quick brown - fox jumps over - the lazy dog."}, - Mode::Insert, - ); - - // test from the beginning of the selection - cx.set_state( - indoc! {" - «ˇThe» quick brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("a"); - - cx.assert_state( - indoc! {" - Theˇ quick brown - fox jumps over - the lazy dog."}, - Mode::Insert, - ); - } - - #[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); - - cx.simulate_keystrokes("r x"); - - cx.assert_state("ˇxa", Mode::HelixNormal); - - // Cursor at the beginning - cx.set_state("«ˇaa»", Mode::HelixNormal); - - cx.simulate_keystrokes("r x"); - - cx.assert_state("«ˇxx»", Mode::HelixNormal); - - // Cursor at the end - cx.set_state("«aaˇ»", Mode::HelixNormal); - - cx.simulate_keystrokes("r x"); - - cx.assert_state("«xxˇ»", Mode::HelixNormal); - } - - #[gpui::test] - async fn test_helix_yank(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - // Test yanking current character with no selection - cx.set_state("hello ˇworld", Mode::HelixNormal); - cx.simulate_keystrokes("y"); - - // Test cursor remains at the same position after yanking single character - cx.assert_state("hello ˇworld", Mode::HelixNormal); - cx.shared_clipboard().assert_eq("w"); - - // Move cursor and yank another character - cx.simulate_keystrokes("l"); - cx.simulate_keystrokes("y"); - cx.shared_clipboard().assert_eq("o"); - - // Test yanking with existing selection - cx.set_state("hello «worlˇ»d", Mode::HelixNormal); - cx.simulate_keystrokes("y"); - cx.shared_clipboard().assert_eq("worl"); - cx.assert_state("hello «worlˇ»d", Mode::HelixNormal); - - // Test yanking in select mode character by character - cx.set_state("hello ˇworld", Mode::HelixNormal); - cx.simulate_keystroke("v"); - cx.assert_state("hello «wˇ»orld", Mode::HelixSelect); - cx.simulate_keystroke("y"); - cx.assert_state("hello «wˇ»orld", Mode::HelixNormal); - cx.shared_clipboard().assert_eq("w"); - } - - #[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_helix_select_mode(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - assert_eq!(cx.mode(), Mode::Normal); - cx.enable_helix(); - - cx.simulate_keystrokes("v"); - assert_eq!(cx.mode(), Mode::HelixSelect); - cx.simulate_keystrokes("escape"); - assert_eq!(cx.mode(), 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, - ); - } - - #[gpui::test] - async fn test_helix_select_lines(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.set_state( - "line one\nline ˇtwo\nline three\nline four", - Mode::HelixNormal, - ); - cx.simulate_keystrokes("2 x"); - cx.assert_state( - "line one\n«line two\nline three\nˇ»line four", - Mode::HelixNormal, - ); - - // Test extending existing line selection - cx.set_state( - indoc! {" - li«ˇne one - li»ne two - line three - line four"}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("x"); - cx.assert_state( - indoc! {" - «line one - line two - ˇ»line three - line four"}, - Mode::HelixNormal, - ); - - // Pressing x in empty line, select next line (because helix considers cursor a selection) - cx.set_state( - indoc! {" - line one - ˇ - line three - line four"}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("x"); - cx.assert_state( - indoc! {" - line one - « - line three - ˇ»line four"}, - Mode::HelixNormal, - ); - - // Empty line with count selects extra + count lines - cx.set_state( - indoc! {" - line one - ˇ - line three - line four - line five"}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("2 x"); - cx.assert_state( - indoc! {" - line one - « - line three - line four - ˇ»line five"}, - Mode::HelixNormal, - ); - - // Compare empty vs non-empty line behavior - cx.set_state( - indoc! {" - ˇnon-empty line - line two - line three"}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("x"); - cx.assert_state( - indoc! {" - «non-empty line - ˇ»line two - line three"}, - Mode::HelixNormal, - ); - - // Same test but with empty line - should select one extra - cx.set_state( - indoc! {" - ˇ - line two - line three"}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("x"); - cx.assert_state( - indoc! {" - « - line two - ˇ»line three"}, - Mode::HelixNormal, - ); - - // Test selecting multiple lines with count - cx.set_state( - indoc! {" - ˇline one - line two - line threeˇ - line four - line five"}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("x"); - cx.assert_state( - indoc! {" - «line one - ˇ»line two - «line three - ˇ»line four - line five"}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("x"); - cx.assert_state( - indoc! {" - «line one - line two - line three - line four - ˇ»line five"}, - Mode::HelixNormal, - ); - } - - #[gpui::test] - async fn test_helix_select_mode_motion(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - assert_eq!(cx.mode(), Mode::Normal); - cx.enable_helix(); - - cx.set_state("ˇhello", Mode::HelixNormal); - cx.simulate_keystrokes("l v l l"); - cx.assert_state("h«ellˇ»o", Mode::HelixSelect); - } - - #[gpui::test] - async fn test_helix_select_mode_motion_multiple_cursors(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - assert_eq!(cx.mode(), Mode::Normal); - cx.enable_helix(); - - // Start with multiple cursors (no selections) - cx.set_state("ˇhello\nˇworld", Mode::HelixNormal); - - // Enter select mode and move right twice - cx.simulate_keystrokes("v l l"); - - // Each cursor should independently create and extend its own selection - cx.assert_state("«helˇ»lo\n«worˇ»ld", Mode::HelixSelect); - } - - #[gpui::test] - async fn test_helix_select_word_motions(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("ˇone two", Mode::Normal); - cx.simulate_keystrokes("v w"); - cx.assert_state("«one tˇ»wo", Mode::Visual); - - // In Vim, this selects "t". In helix selections stops just before "t" - - cx.enable_helix(); - cx.set_state("ˇone two", Mode::HelixNormal); - cx.simulate_keystrokes("v w"); - cx.assert_state("«one ˇ»two", Mode::HelixSelect); - } - - #[gpui::test] - async fn test_exit_visual_mode(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("ˇone two", Mode::Normal); - cx.simulate_keystrokes("v w"); - cx.assert_state("«one tˇ»wo", Mode::Visual); - cx.simulate_keystrokes("escape"); - cx.assert_state("one ˇtwo", Mode::Normal); - - cx.enable_helix(); - cx.set_state("ˇone two", Mode::HelixNormal); - cx.simulate_keystrokes("v w"); - cx.assert_state("«one ˇ»two", Mode::HelixSelect); - cx.simulate_keystrokes("escape"); - cx.assert_state("«one ˇ»two", Mode::HelixNormal); - } - - #[gpui::test] - async fn test_helix_select_motion(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - cx.set_state("«ˇ»one two three", Mode::HelixSelect); - cx.simulate_keystrokes("w"); - cx.assert_state("«one ˇ»two three", Mode::HelixSelect); - - cx.set_state("«ˇ»one two three", Mode::HelixSelect); - cx.simulate_keystrokes("e"); - cx.assert_state("«oneˇ» two three", Mode::HelixSelect); - } - - #[gpui::test] - async fn test_helix_full_cursor_selection(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - cx.set_state("ˇone two three", Mode::HelixNormal); - cx.simulate_keystrokes("l l v h h h"); - cx.assert_state("«ˇone» two three", Mode::HelixSelect); - } - - #[gpui::test] - async fn test_helix_select_regex(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - cx.set_state("ˇone two one", Mode::HelixNormal); - cx.simulate_keystrokes("x"); - cx.assert_state("«one two oneˇ»", Mode::HelixNormal); - cx.simulate_keystrokes("s o n e"); - cx.run_until_parked(); - cx.simulate_keystrokes("enter"); - cx.assert_state("«oneˇ» two «oneˇ»", Mode::HelixNormal); - - cx.simulate_keystrokes("x"); - cx.simulate_keystrokes("s"); - cx.run_until_parked(); - cx.simulate_keystrokes("enter"); - cx.assert_state("«oneˇ» two «oneˇ»", Mode::HelixNormal); - - // TODO: change "search_in_selection" to not perform any search when in helix select mode with no selection - // cx.set_state("ˇstuff one two one", Mode::HelixNormal); - // cx.simulate_keystrokes("s o n e enter"); - // cx.assert_state("ˇstuff one two one", Mode::HelixNormal); - } - - #[gpui::test] - async fn test_helix_select_next_match(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("ˇhello two one two one two one", Mode::Visual); - cx.simulate_keystrokes("/ o n e"); - cx.simulate_keystrokes("enter"); - cx.simulate_keystrokes("n n"); - cx.assert_state("«hello two one two one two oˇ»ne", Mode::Visual); - - cx.set_state("ˇhello two one two one two one", Mode::Normal); - cx.simulate_keystrokes("/ o n e"); - cx.simulate_keystrokes("enter"); - cx.simulate_keystrokes("n n"); - cx.assert_state("hello two one two one two ˇone", Mode::Normal); - - cx.set_state("ˇhello two one two one two one", Mode::Normal); - cx.simulate_keystrokes("/ o n e"); - cx.simulate_keystrokes("enter"); - cx.simulate_keystrokes("n g n g n"); - cx.assert_state("hello two one two «one two oneˇ»", Mode::Visual); - - cx.enable_helix(); - - cx.set_state("ˇhello two one two one two one", Mode::HelixNormal); - cx.simulate_keystrokes("/ o n e"); - cx.simulate_keystrokes("enter"); - cx.simulate_keystrokes("n n"); - cx.assert_state("hello two one two one two «oneˇ»", Mode::HelixNormal); - - cx.set_state("ˇhello two one two one two one", Mode::HelixSelect); - cx.simulate_keystrokes("/ o n e"); - cx.simulate_keystrokes("enter"); - cx.simulate_keystrokes("n n"); - cx.assert_state("hello two «oneˇ» two «oneˇ» two «oneˇ»", Mode::HelixSelect); - } - - #[gpui::test] - async fn test_helix_substitute(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("ˇone two", Mode::HelixNormal); - cx.simulate_keystrokes("c"); - cx.assert_state("ˇne two", Mode::Insert); - - cx.set_state("«oneˇ» two", Mode::HelixNormal); - cx.simulate_keystrokes("c"); - cx.assert_state("ˇ two", Mode::Insert); - - cx.set_state( - indoc! {" - oneˇ two - three - "}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("x c"); - cx.assert_state( - indoc! {" - ˇ - three - "}, - Mode::Insert, - ); - - cx.set_state( - indoc! {" - one twoˇ - three - "}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("c"); - cx.assert_state( - indoc! {" - one twoˇthree - "}, - Mode::Insert, - ); - - // Helix doesn't set the cursor to the first non-blank one when - // replacing lines: it uses language-dependent indent queries instead. - cx.set_state( - indoc! {" - one two - « indented - three not indentedˇ» - "}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("c"); - cx.set_state( - indoc! {" - one two - ˇ - "}, - Mode::Insert, - ); - } - - #[gpui::test] - async fn test_g_l_end_of_line(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - // Test g l moves to last character, not after it - cx.set_state("hello ˇworld!", Mode::HelixNormal); - cx.simulate_keystrokes("g l"); - cx.assert_state("hello worldˇ!", Mode::HelixNormal); - - // Test with Chinese characters, test if work with UTF-8? - cx.set_state("ˇ你好世界", Mode::HelixNormal); - cx.simulate_keystrokes("g l"); - cx.assert_state("你好世ˇ界", Mode::HelixNormal); - - // Test with end of line - cx.set_state("endˇ", Mode::HelixNormal); - cx.simulate_keystrokes("g l"); - cx.assert_state("enˇd", Mode::HelixNormal); - - // Test with empty line - cx.set_state( - indoc! {" - hello - ˇ - world"}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("g l"); - cx.assert_state( - indoc! {" - hello - ˇ - world"}, - Mode::HelixNormal, - ); - - // Test with multiple lines - cx.set_state( - indoc! {" - ˇfirst line - second line - third line"}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("g l"); - cx.assert_state( - indoc! {" - first linˇe - second line - third line"}, - Mode::HelixNormal, - ); - } -} diff --git a/crates/vim/src/helix/boundary.rs b/crates/vim/src/helix/boundary.rs deleted file mode 100644 index 0c2ebbeef0..0000000000 --- a/crates/vim/src/helix/boundary.rs +++ /dev/null @@ -1,739 +0,0 @@ -use std::{cmp::Ordering, ops::Range}; - -use editor::{ - DisplayPoint, MultiBufferOffset, - display_map::{DisplaySnapshot, ToDisplayPoint}, - movement, -}; -use language::{CharClassifier, CharKind}; -use text::Bias; - -use crate::helix::object::HelixTextObject; - -/// Text objects (after helix definition) that can easily be -/// found by reading a buffer and comparing two neighboring chars -/// until a start / end is found -trait BoundedObject { - /// The next start since `from` (inclusive). - /// If outer is true it is the start of "a" object (m a) rather than "inner" object (m i). - fn next_start(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option; - /// The next end since `from` (inclusive). - /// If outer is true it is the end of "a" object (m a) rather than "inner" object (m i). - fn next_end(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option; - /// The previous start since `from` (inclusive). - /// If outer is true it is the start of "a" object (m a) rather than "inner" object (m i). - fn previous_start(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option; - /// The previous end since `from` (inclusive). - /// If outer is true it is the end of "a" object (m a) rather than "inner" object (m i). - fn previous_end(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option; - - /// Whether the range inside the object can be zero characters wide. - /// If so, the trait assumes that these ranges can't be directly adjacent to each other. - fn inner_range_can_be_zero_width(&self) -> bool; - /// Whether the "ma" can exceed the "mi" range on both sides at the same time - fn surround_on_both_sides(&self) -> bool; - /// Whether the outer range of an object could overlap with the outer range of the neighboring - /// object. If so, they can't be nested. - fn ambiguous_outer(&self) -> bool; - - fn can_be_zero_width(&self, around: bool) -> bool { - if around { - false - } else { - self.inner_range_can_be_zero_width() - } - } - - /// Switches from an "mi" range to an "ma" one. - /// Assumes the inner range is valid. - fn around(&self, map: &DisplaySnapshot, inner_range: Range) -> Range { - if self.surround_on_both_sides() { - let start = self - .previous_start(map, inner_range.start, true) - .unwrap_or(inner_range.start); - let end = self - .next_end(map, inner_range.end, true) - .unwrap_or(inner_range.end); - - return start..end; - } - - let mut start = inner_range.start; - let end = self - .next_end(map, inner_range.end, true) - .unwrap_or(inner_range.end); - if end == inner_range.end { - start = self - .previous_start(map, inner_range.start, true) - .unwrap_or(inner_range.start) - } - - start..end - } - /// Switches from an "ma" range to an "mi" one. - /// Assumes the inner range is valid. - fn inside(&self, map: &DisplaySnapshot, outer_range: Range) -> Range { - let inner_start = self - .next_start(map, outer_range.start, false) - .unwrap_or_else(|| { - log::warn!("The motion might not have found the text object correctly"); - outer_range.start - }); - let inner_end = self - .previous_end(map, outer_range.end, false) - .unwrap_or_else(|| { - log::warn!("The motion might not have found the text object correctly"); - outer_range.end - }); - inner_start..inner_end - } - - /// The next end since `start` (inclusive) on the same nesting level. - fn close_at_end(&self, start: Offset, map: &DisplaySnapshot, outer: bool) -> Option { - let mut end_search_start = if self.can_be_zero_width(outer) { - start - } else { - start.next(map)? - }; - let mut start_search_start = start.next(map)?; - - loop { - let next_end = self.next_end(map, end_search_start, outer)?; - let maybe_next_start = self.next_start(map, start_search_start, outer); - if let Some(next_start) = maybe_next_start - && (next_start.0 < next_end.0 - || next_start.0 == next_end.0 && self.can_be_zero_width(outer)) - && !self.ambiguous_outer() - { - let closing = self.close_at_end(next_start, map, outer)?; - end_search_start = closing.next(map)?; - start_search_start = if self.can_be_zero_width(outer) { - closing.next(map)? - } else { - closing - }; - } else { - return Some(next_end); - } - } - } - /// The previous start since `end` (inclusive) on the same nesting level. - fn close_at_start(&self, end: Offset, map: &DisplaySnapshot, outer: bool) -> Option { - let mut start_search_end = if self.can_be_zero_width(outer) { - end - } else { - end.previous(map)? - }; - let mut end_search_end = end.previous(map)?; - - loop { - let previous_start = self.previous_start(map, start_search_end, outer)?; - let maybe_previous_end = self.previous_end(map, end_search_end, outer); - if let Some(previous_end) = maybe_previous_end - && (previous_end.0 > previous_start.0 - || previous_end.0 == previous_start.0 && self.can_be_zero_width(outer)) - && !self.ambiguous_outer() - { - let closing = self.close_at_start(previous_end, map, outer)?; - start_search_end = closing.previous(map)?; - end_search_end = if self.can_be_zero_width(outer) { - closing.previous(map)? - } else { - closing - }; - } else { - return Some(previous_start); - } - } - } -} - -#[derive(Clone, Copy, PartialEq, Debug, PartialOrd, Ord, Eq)] -struct Offset(MultiBufferOffset); -impl Offset { - fn next(self, map: &DisplaySnapshot) -> Option { - let next = Self( - map.buffer_snapshot() - .clip_offset(self.0 + 1usize, Bias::Right), - ); - (next.0 > self.0).then(|| next) - } - fn previous(self, map: &DisplaySnapshot) -> Option { - if self.0 == MultiBufferOffset(0) { - return None; - } - Some(Self( - map.buffer_snapshot().clip_offset(self.0 - 1, Bias::Left), - )) - } - fn range( - start: (DisplayPoint, Bias), - end: (DisplayPoint, Bias), - map: &DisplaySnapshot, - ) -> Range { - Self(start.0.to_offset(map, start.1))..Self(end.0.to_offset(map, end.1)) - } -} - -impl HelixTextObject for B { - fn range( - &self, - map: &DisplaySnapshot, - relative_to: Range, - around: bool, - ) -> Option> { - let relative_to = Offset::range( - (relative_to.start, Bias::Left), - (relative_to.end, Bias::Left), - map, - ); - - relative_range(self, around, map, |find_outer| { - let search_start = if self.can_be_zero_width(find_outer) { - relative_to.end - } else { - // If the objects can be directly next to each other an object end the - // cursor (relative_to) end would not count for close_at_end, so the search - // needs to start one character to the left. - relative_to.end.previous(map)? - }; - let max_end = self.close_at_end(search_start, map, find_outer)?; - let min_start = self.close_at_start(max_end, map, find_outer)?; - - (min_start <= relative_to.start).then(|| min_start..max_end) - }) - } - - fn next_range( - &self, - map: &DisplaySnapshot, - relative_to: Range, - around: bool, - ) -> Option> { - let relative_to = Offset::range( - (relative_to.start, Bias::Left), - (relative_to.end, Bias::Left), - map, - ); - - relative_range(self, around, map, |find_outer| { - let min_start = self.next_start(map, relative_to.end, find_outer)?; - let max_end = self.close_at_end(min_start, map, find_outer)?; - - Some(min_start..max_end) - }) - } - - fn previous_range( - &self, - map: &DisplaySnapshot, - relative_to: Range, - around: bool, - ) -> Option> { - let relative_to = Offset::range( - (relative_to.start, Bias::Left), - (relative_to.end, Bias::Left), - map, - ); - - relative_range(self, around, map, |find_outer| { - let max_end = self.previous_end(map, relative_to.start, find_outer)?; - let min_start = self.close_at_start(max_end, map, find_outer)?; - - Some(min_start..max_end) - }) - } -} - -fn relative_range( - object: &B, - outer: bool, - map: &DisplaySnapshot, - find_range: impl Fn(bool) -> Option>, -) -> Option> { - // The cursor could be inside the outer range, but not the inner range. - // Whether that should count as found. - let find_outer = object.surround_on_both_sides() && !object.ambiguous_outer(); - let range = find_range(find_outer)?; - let min_start = range.start; - let max_end = range.end; - - let wanted_range = if outer && !find_outer { - // max_end is not yet the outer end - object.around(map, min_start..max_end) - } else if !outer && find_outer { - // max_end is the outer end, but the final result should have the inner end - object.inside(map, min_start..max_end) - } else { - min_start..max_end - }; - - let start = wanted_range.start.0.to_display_point(map); - let end = wanted_range.end.0.to_display_point(map); - - Some(start..end) -} - -/// A textobject whose boundaries can easily be found between two chars -pub enum ImmediateBoundary { - Word { ignore_punctuation: bool }, - Subword { ignore_punctuation: bool }, - AngleBrackets, - BackQuotes, - CurlyBrackets, - DoubleQuotes, - Parentheses, - SingleQuotes, - SquareBrackets, - VerticalBars, -} - -/// A textobject whose start and end can be found from an easy-to-find -/// boundary between two chars by following a simple path from there -pub enum FuzzyBoundary { - Sentence, - Paragraph, -} - -impl ImmediateBoundary { - fn is_inner_start(&self, left: char, right: char, classifier: CharClassifier) -> bool { - match self { - Self::Word { ignore_punctuation } => { - let classifier = classifier.ignore_punctuation(*ignore_punctuation); - is_word_start(left, right, &classifier) - || (is_buffer_start(left) && classifier.kind(right) != CharKind::Whitespace) - } - Self::Subword { ignore_punctuation } => { - let classifier = classifier.ignore_punctuation(*ignore_punctuation); - movement::is_subword_start(left, right, &classifier) - || (is_buffer_start(left) && classifier.kind(right) != CharKind::Whitespace) - } - Self::AngleBrackets => left == '<', - Self::BackQuotes => left == '`', - Self::CurlyBrackets => left == '{', - Self::DoubleQuotes => left == '"', - Self::Parentheses => left == '(', - Self::SingleQuotes => left == '\'', - Self::SquareBrackets => left == '[', - Self::VerticalBars => left == '|', - } - } - fn is_inner_end(&self, left: char, right: char, classifier: CharClassifier) -> bool { - match self { - Self::Word { ignore_punctuation } => { - let classifier = classifier.ignore_punctuation(*ignore_punctuation); - is_word_end(left, right, &classifier) - || (is_buffer_end(right) && classifier.kind(left) != CharKind::Whitespace) - } - Self::Subword { ignore_punctuation } => { - let classifier = classifier.ignore_punctuation(*ignore_punctuation); - movement::is_subword_start(left, right, &classifier) - || (is_buffer_end(right) && classifier.kind(left) != CharKind::Whitespace) - } - Self::AngleBrackets => right == '>', - Self::BackQuotes => right == '`', - Self::CurlyBrackets => right == '}', - Self::DoubleQuotes => right == '"', - Self::Parentheses => right == ')', - Self::SingleQuotes => right == '\'', - Self::SquareBrackets => right == ']', - Self::VerticalBars => right == '|', - } - } - fn is_outer_start(&self, left: char, right: char, classifier: CharClassifier) -> bool { - match self { - word @ Self::Word { .. } => word.is_inner_end(left, right, classifier) || left == '\n', - subword @ Self::Subword { .. } => { - subword.is_inner_end(left, right, classifier) || left == '\n' - } - Self::AngleBrackets => right == '<', - Self::BackQuotes => right == '`', - Self::CurlyBrackets => right == '{', - Self::DoubleQuotes => right == '"', - Self::Parentheses => right == '(', - Self::SingleQuotes => right == '\'', - Self::SquareBrackets => right == '[', - Self::VerticalBars => right == '|', - } - } - fn is_outer_end(&self, left: char, right: char, classifier: CharClassifier) -> bool { - match self { - word @ Self::Word { .. } => { - word.is_inner_start(left, right, classifier) || right == '\n' - } - subword @ Self::Subword { .. } => { - subword.is_inner_start(left, right, classifier) || right == '\n' - } - Self::AngleBrackets => left == '>', - Self::BackQuotes => left == '`', - Self::CurlyBrackets => left == '}', - Self::DoubleQuotes => left == '"', - Self::Parentheses => left == ')', - Self::SingleQuotes => left == '\'', - Self::SquareBrackets => left == ']', - Self::VerticalBars => left == '|', - } - } -} - -impl BoundedObject for ImmediateBoundary { - fn next_start(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option { - try_find_boundary(map, from, |left, right| { - let classifier = map.buffer_snapshot().char_classifier_at(from.0); - if outer { - self.is_outer_start(left, right, classifier) - } else { - self.is_inner_start(left, right, classifier) - } - }) - } - fn next_end(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option { - try_find_boundary(map, from, |left, right| { - let classifier = map.buffer_snapshot().char_classifier_at(from.0); - if outer { - self.is_outer_end(left, right, classifier) - } else { - self.is_inner_end(left, right, classifier) - } - }) - } - fn previous_start(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option { - try_find_preceding_boundary(map, from, |left, right| { - let classifier = map.buffer_snapshot().char_classifier_at(from.0); - if outer { - self.is_outer_start(left, right, classifier) - } else { - self.is_inner_start(left, right, classifier) - } - }) - } - fn previous_end(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option { - try_find_preceding_boundary(map, from, |left, right| { - let classifier = map.buffer_snapshot().char_classifier_at(from.0); - if outer { - self.is_outer_end(left, right, classifier) - } else { - self.is_inner_end(left, right, classifier) - } - }) - } - fn inner_range_can_be_zero_width(&self) -> bool { - match self { - Self::Subword { .. } | Self::Word { .. } => false, - _ => true, - } - } - fn surround_on_both_sides(&self) -> bool { - match self { - Self::Subword { .. } | Self::Word { .. } => false, - _ => true, - } - } - fn ambiguous_outer(&self) -> bool { - match self { - Self::BackQuotes - | Self::DoubleQuotes - | Self::SingleQuotes - | Self::VerticalBars - | Self::Subword { .. } - | Self::Word { .. } => true, - _ => false, - } - } -} - -impl FuzzyBoundary { - /// When between two chars that form an easy-to-find identifier boundary, - /// what's the way to get to the actual start of the object, if any - fn is_near_potential_inner_start<'a>( - &self, - left: char, - right: char, - classifier: &CharClassifier, - ) -> Option Option>> { - if is_buffer_start(left) { - return Some(Box::new(|identifier, _| Some(identifier))); - } - match self { - Self::Paragraph => { - if left != '\n' || right != '\n' { - return None; - } - Some(Box::new(|identifier, map| { - try_find_boundary(map, identifier, |left, right| left == '\n' && right != '\n') - })) - } - Self::Sentence => { - if let Some(find_paragraph_start) = - Self::Paragraph.is_near_potential_inner_start(left, right, classifier) - { - return Some(find_paragraph_start); - } else if !is_sentence_end(left, right, classifier) { - return None; - } - Some(Box::new(|identifier, map| { - let word = ImmediateBoundary::Word { - ignore_punctuation: false, - }; - word.next_start(map, identifier, false) - })) - } - } - } - /// When between two chars that form an easy-to-find identifier boundary, - /// what's the way to get to the actual end of the object, if any - fn is_near_potential_inner_end<'a>( - &self, - left: char, - right: char, - classifier: &CharClassifier, - ) -> Option Option>> { - if is_buffer_end(right) { - return Some(Box::new(|identifier, _| Some(identifier))); - } - match self { - Self::Paragraph => { - if left != '\n' || right != '\n' { - return None; - } - Some(Box::new(|identifier, map| { - try_find_preceding_boundary(map, identifier, |left, right| { - left != '\n' && right == '\n' - }) - })) - } - Self::Sentence => { - if let Some(find_paragraph_end) = - Self::Paragraph.is_near_potential_inner_end(left, right, classifier) - { - return Some(find_paragraph_end); - } else if !is_sentence_end(left, right, classifier) { - return None; - } - Some(Box::new(|identifier, _| Some(identifier))) - } - } - } - /// When between two chars that form an easy-to-find identifier boundary, - /// what's the way to get to the actual end of the object, if any - fn is_near_potential_outer_start<'a>( - &self, - left: char, - right: char, - classifier: &CharClassifier, - ) -> Option Option>> { - match self { - paragraph @ Self::Paragraph => { - paragraph.is_near_potential_inner_end(left, right, classifier) - } - sentence @ Self::Sentence => { - sentence.is_near_potential_inner_end(left, right, classifier) - } - } - } - /// When between two chars that form an easy-to-find identifier boundary, - /// what's the way to get to the actual end of the object, if any - fn is_near_potential_outer_end<'a>( - &self, - left: char, - right: char, - classifier: &CharClassifier, - ) -> Option Option>> { - match self { - paragraph @ Self::Paragraph => { - paragraph.is_near_potential_inner_start(left, right, classifier) - } - sentence @ Self::Sentence => { - sentence.is_near_potential_inner_start(left, right, classifier) - } - } - } - - // The boundary can be on the other side of `from` than the identifier, so the search needs to go both ways. - // Also, the distance (and direction) between identifier and boundary could vary, so a few ones need to be - // compared, even if one boundary was already found on the right side of `from`. - fn to_boundary( - &self, - map: &DisplaySnapshot, - from: Offset, - outer: bool, - backward: bool, - boundary_kind: Boundary, - ) -> Option { - let generate_boundary_data = |left, right, point: Offset| { - let classifier = map.buffer_snapshot().char_classifier_at(from.0); - let reach_boundary = if outer && boundary_kind == Boundary::Start { - self.is_near_potential_outer_start(left, right, &classifier) - } else if !outer && boundary_kind == Boundary::Start { - self.is_near_potential_inner_start(left, right, &classifier) - } else if outer && boundary_kind == Boundary::End { - self.is_near_potential_outer_end(left, right, &classifier) - } else { - self.is_near_potential_inner_end(left, right, &classifier) - }; - - reach_boundary.map(|reach_start| (point, reach_start)) - }; - - let forwards = try_find_boundary_data(map, from, generate_boundary_data); - let backwards = try_find_preceding_boundary_data(map, from, generate_boundary_data); - let boundaries = [forwards, backwards] - .into_iter() - .flatten() - .filter_map(|(identifier, reach_boundary)| reach_boundary(identifier, map)) - .filter(|boundary| match boundary.cmp(&from) { - Ordering::Equal => true, - Ordering::Less => backward, - Ordering::Greater => !backward, - }); - if backward { - boundaries.max_by_key(|boundary| *boundary) - } else { - boundaries.min_by_key(|boundary| *boundary) - } - } -} - -#[derive(PartialEq)] -enum Boundary { - Start, - End, -} - -impl BoundedObject for FuzzyBoundary { - fn next_start(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option { - self.to_boundary(map, from, outer, false, Boundary::Start) - } - fn next_end(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option { - self.to_boundary(map, from, outer, false, Boundary::End) - } - fn previous_start(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option { - self.to_boundary(map, from, outer, true, Boundary::Start) - } - fn previous_end(&self, map: &DisplaySnapshot, from: Offset, outer: bool) -> Option { - self.to_boundary(map, from, outer, true, Boundary::End) - } - fn inner_range_can_be_zero_width(&self) -> bool { - false - } - fn surround_on_both_sides(&self) -> bool { - false - } - fn ambiguous_outer(&self) -> bool { - false - } -} - -/// Returns the first boundary after or at `from` in text direction. -/// The start and end of the file are the chars `'\0'`. -fn try_find_boundary( - map: &DisplaySnapshot, - from: Offset, - is_boundary: impl Fn(char, char) -> bool, -) -> Option { - let boundary = try_find_boundary_data(map, from, |left, right, point| { - if is_boundary(left, right) { - Some(point) - } else { - None - } - })?; - Some(boundary) -} - -/// Returns some information about it (of type `T`) as soon as -/// there is a boundary after or at `from` in text direction -/// The start and end of the file are the chars `'\0'`. -fn try_find_boundary_data( - map: &DisplaySnapshot, - mut from: Offset, - boundary_information: impl Fn(char, char, Offset) -> Option, -) -> Option { - let mut prev_ch = map - .buffer_snapshot() - .reversed_chars_at(from.0) - .next() - .unwrap_or('\0'); - - for ch in map.buffer_snapshot().chars_at(from.0).chain(['\0']) { - if let Some(boundary_information) = boundary_information(prev_ch, ch, from) { - return Some(boundary_information); - } - from.0 += ch.len_utf8(); - prev_ch = ch; - } - - None -} - -/// Returns the first boundary after or at `from` in text direction. -/// The start and end of the file are the chars `'\0'`. -fn try_find_preceding_boundary( - map: &DisplaySnapshot, - from: Offset, - is_boundary: impl Fn(char, char) -> bool, -) -> Option { - let boundary = try_find_preceding_boundary_data(map, from, |left, right, point| { - if is_boundary(left, right) { - Some(point) - } else { - None - } - })?; - Some(boundary) -} - -/// Returns some information about it (of type `T`) as soon as -/// there is a boundary before or at `from` in opposite text direction -/// The start and end of the file are the chars `'\0'`. -fn try_find_preceding_boundary_data( - map: &DisplaySnapshot, - mut from: Offset, - is_boundary: impl Fn(char, char, Offset) -> Option, -) -> Option { - let mut prev_ch = map - .buffer_snapshot() - .chars_at(from.0) - .next() - .unwrap_or('\0'); - - for ch in map - .buffer_snapshot() - .reversed_chars_at(from.0) - .chain(['\0']) - { - if let Some(boundary_information) = is_boundary(ch, prev_ch, from) { - return Some(boundary_information); - } - from.0.0 = from.0.0.saturating_sub(ch.len_utf8()); - prev_ch = ch; - } - - None -} - -fn is_buffer_start(left: char) -> bool { - left == '\0' -} - -fn is_buffer_end(right: char) -> bool { - right == '\0' -} - -fn is_word_start(left: char, right: char, classifier: &CharClassifier) -> bool { - classifier.kind(left) != classifier.kind(right) - && classifier.kind(right) != CharKind::Whitespace -} - -fn is_word_end(left: char, right: char, classifier: &CharClassifier) -> bool { - classifier.kind(left) != classifier.kind(right) && classifier.kind(left) != CharKind::Whitespace -} - -fn is_sentence_end(left: char, right: char, classifier: &CharClassifier) -> bool { - const ENDS: [char; 1] = ['.']; - - if classifier.kind(right) != CharKind::Whitespace { - return false; - } - ENDS.into_iter().any(|end| left == end) -} diff --git a/crates/vim/src/helix/duplicate.rs b/crates/vim/src/helix/duplicate.rs deleted file mode 100644 index 37796c57aa..0000000000 --- a/crates/vim/src/helix/duplicate.rs +++ /dev/null @@ -1,234 +0,0 @@ -use std::ops::Range; - -use editor::{DisplayPoint, MultiBufferOffset, display_map::DisplaySnapshot}; -use gpui::Context; -use text::Bias; -use ui::Window; - -use crate::Vim; - -impl Vim { - /// Creates a duplicate of every selection below it in the first place that has both its start - /// and end - pub(super) fn helix_duplicate_selections_below( - &mut self, - times: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.duplicate_selections( - times, - window, - cx, - |prev_point| *prev_point.row_mut() += 1, - |prev_range, map| prev_range.end.row() >= map.max_point().row(), - false, - ); - } - - /// Creates a duplicate of every selection above it in the first place that has both its start - /// and end - pub(super) fn helix_duplicate_selections_above( - &mut self, - times: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.duplicate_selections( - times, - window, - cx, - |prev_point| *prev_point.row_mut() = prev_point.row().0.saturating_sub(1), - |prev_range, _| prev_range.start.row() == DisplayPoint::zero().row(), - true, - ); - } - - fn duplicate_selections( - &mut self, - times: Option, - window: &mut Window, - cx: &mut Context, - advance_search: impl Fn(&mut DisplayPoint), - end_search: impl Fn(&Range, &DisplaySnapshot) -> bool, - above: bool, - ) { - let times = times.unwrap_or(1); - self.update_editor(cx, |_, editor, cx| { - let mut selections = Vec::new(); - let map = editor.display_snapshot(cx); - let mut original_selections = editor.selections.all_display(&map); - // The order matters, because it is recorded when the selections are added. - if above { - original_selections.reverse(); - } - - for origin in original_selections { - let origin = origin.tail()..origin.head(); - selections.push(display_point_range_to_offset_range(&origin, &map)); - let mut last_origin = origin; - for _ in 1..=times { - if let Some(duplicate) = find_next_valid_duplicate_space( - last_origin.clone(), - &map, - &advance_search, - &end_search, - ) { - selections.push(display_point_range_to_offset_range(&duplicate, &map)); - last_origin = duplicate; - } else { - break; - } - } - } - - editor.change_selections(Default::default(), window, cx, |s| { - s.select_ranges(selections); - }); - }); - } -} - -fn find_next_valid_duplicate_space( - mut origin: Range, - map: &DisplaySnapshot, - advance_search: &impl Fn(&mut DisplayPoint), - end_search: &impl Fn(&Range, &DisplaySnapshot) -> bool, -) -> Option> { - while !end_search(&origin, map) { - advance_search(&mut origin.start); - advance_search(&mut origin.end); - - if map.clip_point(origin.start, Bias::Left) == origin.start - && map.clip_point(origin.end, Bias::Right) == origin.end - { - return Some(origin); - } - } - None -} - -fn display_point_range_to_offset_range( - range: &Range, - map: &DisplaySnapshot, -) -> Range { - range.start.to_offset(map, Bias::Left)..range.end.to_offset(map, Bias::Right) -} - -#[cfg(test)] -mod tests { - use db::indoc; - - use crate::{state::Mode, test::VimTestContext}; - - #[gpui::test] - async fn test_selection_duplication(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - cx.set_state( - indoc! {" - The quick brown - fox «jumpsˇ» - over the - lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("C"); - - cx.assert_state( - indoc! {" - The quick brown - fox «jumpsˇ» - over the - lazy« dog.ˇ»"}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("C"); - - cx.assert_state( - indoc! {" - The quick brown - fox «jumpsˇ» - over the - lazy« dog.ˇ»"}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("alt-C"); - - cx.assert_state( - indoc! {" - The «quickˇ» brown - fox «jumpsˇ» - over the - lazy« dog.ˇ»"}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes(","); - - cx.assert_state( - indoc! {" - The «quickˇ» brown - fox jumps - over the - lazy dog."}, - Mode::HelixNormal, - ); - } - - #[gpui::test] - async fn test_selection_duplication_backwards(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - cx.set_state( - indoc! {" - The quick brown - «ˇfox» jumps - over the - lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("C C alt-C"); - - cx.assert_state( - indoc! {" - «ˇThe» quick brown - «ˇfox» jumps - «ˇove»r the - «ˇlaz»y dog."}, - Mode::HelixNormal, - ); - } - - #[gpui::test] - async fn test_selection_duplication_count(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - cx.set_state( - indoc! {" - The «qˇ»uick brown - fox jumps - over the - lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("9 C"); - - cx.assert_state( - indoc! {" - The «qˇ»uick brown - fox «jˇ»umps - over« ˇ»the - lazy« ˇ»dog."}, - Mode::HelixNormal, - ); - } -} diff --git a/crates/vim/src/helix/object.rs b/crates/vim/src/helix/object.rs deleted file mode 100644 index 798cd7162e..0000000000 --- a/crates/vim/src/helix/object.rs +++ /dev/null @@ -1,182 +0,0 @@ -use std::{ - error::Error, - fmt::{self, Display}, - ops::Range, -}; - -use editor::{DisplayPoint, display_map::DisplaySnapshot, movement}; -use text::Selection; - -use crate::{ - helix::boundary::{FuzzyBoundary, ImmediateBoundary}, - object::Object as VimObject, -}; - -/// A text object from helix or an extra one -pub trait HelixTextObject { - fn range( - &self, - map: &DisplaySnapshot, - relative_to: Range, - around: bool, - ) -> Option>; - - fn next_range( - &self, - map: &DisplaySnapshot, - relative_to: Range, - around: bool, - ) -> Option>; - - fn previous_range( - &self, - map: &DisplaySnapshot, - relative_to: Range, - around: bool, - ) -> Option>; -} - -impl VimObject { - /// Returns the range of the object the cursor is over. - /// Follows helix convention. - pub fn helix_range( - self, - map: &DisplaySnapshot, - selection: Selection, - around: bool, - ) -> Result>, VimToHelixError> { - let cursor = cursor_range(&selection, map); - if let Some(helix_object) = self.to_helix_object() { - Ok(helix_object.range(map, cursor, around)) - } else { - Err(VimToHelixError) - } - } - /// Returns the range of the next object the cursor is not over. - /// Follows helix convention. - pub fn helix_next_range( - self, - map: &DisplaySnapshot, - selection: Selection, - around: bool, - ) -> Result>, VimToHelixError> { - let cursor = cursor_range(&selection, map); - if let Some(helix_object) = self.to_helix_object() { - Ok(helix_object.next_range(map, cursor, around)) - } else { - Err(VimToHelixError) - } - } - /// Returns the range of the previous object the cursor is not over. - /// Follows helix convention. - pub fn helix_previous_range( - self, - map: &DisplaySnapshot, - selection: Selection, - around: bool, - ) -> Result>, VimToHelixError> { - let cursor = cursor_range(&selection, map); - if let Some(helix_object) = self.to_helix_object() { - Ok(helix_object.previous_range(map, cursor, around)) - } else { - Err(VimToHelixError) - } - } -} - -#[derive(Debug)] -pub struct VimToHelixError; -impl Display for VimToHelixError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "Not all vim text objects have an implemented helix equivalent" - ) - } -} -impl Error for VimToHelixError {} - -impl VimObject { - fn to_helix_object(self) -> Option> { - Some(match self { - Self::AngleBrackets => Box::new(ImmediateBoundary::AngleBrackets), - Self::BackQuotes => Box::new(ImmediateBoundary::BackQuotes), - Self::CurlyBrackets => Box::new(ImmediateBoundary::CurlyBrackets), - Self::DoubleQuotes => Box::new(ImmediateBoundary::DoubleQuotes), - Self::Paragraph => Box::new(FuzzyBoundary::Paragraph), - Self::Parentheses => Box::new(ImmediateBoundary::Parentheses), - Self::Quotes => Box::new(ImmediateBoundary::SingleQuotes), - Self::Sentence => Box::new(FuzzyBoundary::Sentence), - Self::SquareBrackets => Box::new(ImmediateBoundary::SquareBrackets), - Self::Subword { ignore_punctuation } => { - Box::new(ImmediateBoundary::Subword { ignore_punctuation }) - } - Self::VerticalBars => Box::new(ImmediateBoundary::VerticalBars), - Self::Word { ignore_punctuation } => { - Box::new(ImmediateBoundary::Word { ignore_punctuation }) - } - _ => return None, - }) - } -} - -/// Returns the start of the cursor of a selection, whether that is collapsed or not. -pub(crate) fn cursor_range( - selection: &Selection, - map: &DisplaySnapshot, -) -> Range { - if selection.is_empty() | selection.reversed { - selection.head()..movement::right(map, selection.head()) - } else { - movement::left(map, selection.head())..selection.head() - } -} - -#[cfg(test)] -mod test { - use db::indoc; - - use crate::{state::Mode, test::VimTestContext}; - - #[gpui::test] - async fn test_select_word_object(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - let start = indoc! {" - The quick brˇowˇnˇ - fox «ˇjumps» ov«er - the laˇ»zy dogˇ - - " - }; - - cx.set_state(start, Mode::HelixNormal); - - cx.simulate_keystrokes("m i w"); - - cx.assert_state( - indoc! {" - The quick «brownˇ» - fox «jumpsˇ» over - the «lazyˇ» dogˇ - - " - }, - Mode::HelixNormal, - ); - - cx.set_state(start, Mode::HelixNormal); - - cx.simulate_keystrokes("m a w"); - - cx.assert_state( - indoc! {" - The quick« brownˇ» - fox «jumps ˇ»over - the «lazy ˇ»dogˇ - - " - }, - Mode::HelixNormal, - ); - } -} diff --git a/crates/vim/src/helix/paste.rs b/crates/vim/src/helix/paste.rs deleted file mode 100644 index d91b138853..0000000000 --- a/crates/vim/src/helix/paste.rs +++ /dev/null @@ -1,455 +0,0 @@ -use editor::{ToOffset, movement}; -use gpui::{Action, Context, Window}; -use schemars::JsonSchema; -use serde::Deserialize; - -use crate::{Vim, state::Mode}; - -/// Pastes text from the specified register at the cursor position. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -pub struct HelixPaste { - #[serde(default)] - before: bool, -} - -impl Vim { - pub fn helix_paste( - &mut self, - action: &HelixPaste, - window: &mut Window, - cx: &mut Context, - ) { - self.record_current_action(cx); - self.store_visual_marks(window, cx); - let count = Vim::take_count(cx).unwrap_or(1); - // TODO: vim paste calls take_forced_motion here, but I don't know what that does - // (none of the other helix_ methods call it) - - self.update_editor(cx, |vim, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - editor.set_clip_at_line_ends(false, cx); - - let selected_register = vim.selected_register.take(); - - let Some((text, clipboard_selections)) = Vim::update_globals(cx, |globals, cx| { - globals.read_register(selected_register, Some(editor), cx) - }) - .and_then(|reg| { - (!reg.text.is_empty()) - .then_some(reg.text) - .zip(reg.clipboard_selections) - }) else { - return; - }; - - let display_map = editor.display_snapshot(cx); - let current_selections = editor.selections.all_adjusted_display(&display_map); - - // The clipboard can have multiple selections, and there can - // be multiple selections. Helix zips them together, so the first - // clipboard entry gets pasted at the first selection, the second - // entry gets pasted at the second selection, and so on. If there - // are more clipboard selections than selections, the extra ones - // don't get pasted anywhere. If there are more selections than - // clipboard selections, the last clipboard selection gets - // pasted at all remaining selections. - - let mut edits = Vec::new(); - let mut new_selections = Vec::new(); - let mut start_offset = 0; - - let mut replacement_texts: Vec = Vec::new(); - - for ix in 0..current_selections.len() { - let to_insert = if let Some(clip_sel) = clipboard_selections.get(ix) { - let end_offset = start_offset + clip_sel.len; - let text = text[start_offset..end_offset].to_string(); - start_offset = end_offset + 1; - text - } else if let Some(last_text) = replacement_texts.last() { - // We have more current selections than clipboard selections: repeat the last one. - last_text.to_owned() - } else { - text.to_string() - }; - replacement_texts.push(to_insert); - } - - let line_mode = replacement_texts.iter().any(|text| text.ends_with('\n')); - - for (to_insert, sel) in replacement_texts.into_iter().zip(current_selections) { - // Helix doesn't care about the head/tail of the selection. - // Pasting before means pasting before the whole selection. - let display_point = if line_mode { - if action.before { - movement::line_beginning(&display_map, sel.start, false) - } else { - if sel.start == sel.end { - movement::right( - &display_map, - movement::line_end(&display_map, sel.end, false), - ) - } else { - sel.end - } - } - } else if action.before { - sel.start - } else if sel.start == sel.end { - // Helix and Zed differ in how they understand - // single-point cursors. In Helix, a single-point cursor - // is "on top" of some character, and pasting after that - // cursor means that the pasted content should go after - // that character. (If the cursor is at the end of a - // line, the pasted content goes on the next line.) - movement::right(&display_map, sel.end) - } else { - sel.end - }; - let point = display_point.to_point(&display_map); - let anchor = if action.before { - display_map.buffer_snapshot().anchor_after(point) - } else { - display_map.buffer_snapshot().anchor_before(point) - }; - edits.push((point..point, to_insert.repeat(count))); - new_selections.push((anchor, to_insert.len() * count)); - } - - editor.edit(edits, cx); - - let snapshot = editor.buffer().read(cx).snapshot(cx); - editor.change_selections(Default::default(), window, cx, |s| { - s.select_ranges(new_selections.into_iter().map(|(anchor, len)| { - let offset = anchor.to_offset(&snapshot); - if action.before { - offset.saturating_sub_usize(len)..offset - } else { - offset..(offset + len) - } - })); - }) - }); - }); - - self.switch_mode(Mode::HelixNormal, true, window, cx); - } -} - -#[cfg(test)] -mod test { - use indoc::indoc; - - use crate::{state::Mode, test::VimTestContext}; - - #[gpui::test] - async fn test_paste(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - cx.set_state( - indoc! {" - The «quiˇ»ck brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("y w p"); - - cx.assert_state( - indoc! {" - The quick «quiˇ»brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - // Pasting before the selection: - cx.set_state( - indoc! {" - The quick brown - fox «jumpsˇ» over - the lazy dog."}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("shift-p"); - cx.assert_state( - indoc! {" - The quick brown - fox «quiˇ»jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - } - - #[gpui::test] - async fn test_point_selection_paste(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - cx.set_state( - indoc! {" - The quiˇck brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("y"); - - // Pasting before the selection: - cx.set_state( - indoc! {" - The quick brown - fox jumpsˇ over - the lazy dog."}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("shift-p"); - cx.assert_state( - indoc! {" - The quick brown - fox jumps«cˇ» over - the lazy dog."}, - Mode::HelixNormal, - ); - - // Pasting after the selection: - cx.set_state( - indoc! {" - The quick brown - fox jumpsˇ over - the lazy dog."}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("p"); - cx.assert_state( - indoc! {" - The quick brown - fox jumps «cˇ»over - the lazy dog."}, - Mode::HelixNormal, - ); - - // Pasting after the selection at the end of a line: - cx.set_state( - indoc! {" - The quick brown - fox jumps overˇ - the lazy dog."}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("p"); - cx.assert_state( - indoc! {" - The quick brown - fox jumps over - «cˇ»the lazy dog."}, - Mode::HelixNormal, - ); - } - - #[gpui::test] - async fn test_multi_cursor_paste(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - // Select two blocks of text. - cx.set_state( - indoc! {" - The «quiˇ»ck brown - fox ju«mpsˇ» over - the lazy dog."}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("y"); - - // Only one cursor: only the first block gets pasted. - cx.set_state( - indoc! {" - ˇThe quick brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("shift-p"); - cx.assert_state( - indoc! {" - «quiˇ»The quick brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - // Two cursors: both get pasted. - cx.set_state( - indoc! {" - ˇThe ˇquick brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("shift-p"); - cx.assert_state( - indoc! {" - «quiˇ»The «mpsˇ»quick brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - // Three cursors: the second yanked block is duplicated. - cx.set_state( - indoc! {" - ˇThe ˇquick brown - fox jumpsˇ over - the lazy dog."}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("shift-p"); - cx.assert_state( - indoc! {" - «quiˇ»The «mpsˇ»quick brown - fox jumps«mpsˇ» over - the lazy dog."}, - Mode::HelixNormal, - ); - - // Again with three cursors. All three should be pasted twice. - cx.set_state( - indoc! {" - ˇThe ˇquick brown - fox jumpsˇ over - the lazy dog."}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("2 shift-p"); - cx.assert_state( - indoc! {" - «quiquiˇ»The «mpsmpsˇ»quick brown - fox jumps«mpsmpsˇ» over - the lazy dog."}, - Mode::HelixNormal, - ); - } - - #[gpui::test] - async fn test_line_mode_paste(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - cx.set_state( - indoc! {" - The quick brow«n - ˇ»fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - cx.simulate_keystrokes("y shift-p"); - - cx.assert_state( - indoc! {" - «n - ˇ»The quick brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - // In line mode, if we're in the middle of a line then pasting before pastes on - // the line before. - cx.set_state( - indoc! {" - The quick brown - fox jumpsˇ over - the lazy dog."}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("shift-p"); - cx.assert_state( - indoc! {" - The quick brown - «n - ˇ»fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - // In line mode, if we're in the middle of a line then pasting after pastes on - // the line after. - cx.set_state( - indoc! {" - The quick brown - fox jumpsˇ over - the lazy dog."}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("p"); - cx.assert_state( - indoc! {" - The quick brown - fox jumps over - «n - ˇ»the lazy dog."}, - Mode::HelixNormal, - ); - - // If we're currently at the end of a line, "the line after" - // means right after the cursor. - cx.set_state( - indoc! {" - The quick brown - fox jumps overˇ - the lazy dog."}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("p"); - cx.assert_state( - indoc! {" - The quick brown - fox jumps over - «n - ˇ»the lazy dog."}, - Mode::HelixNormal, - ); - - cx.set_state( - indoc! {" - - The quick brown - fox jumps overˇ - the lazy dog."}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("x y up up p"); - cx.assert_state( - indoc! {" - - «fox jumps over - ˇ»The quick brown - fox jumps over - the lazy dog."}, - Mode::HelixNormal, - ); - - cx.set_state( - indoc! {" - «The quick brown - fox jumps over - ˇ»the lazy dog."}, - Mode::HelixNormal, - ); - cx.simulate_keystrokes("y p p"); - cx.assert_state( - indoc! {" - The quick brown - fox jumps over - The quick brown - fox jumps over - «The quick brown - fox jumps over - ˇ»the lazy dog."}, - Mode::HelixNormal, - ); - } -} diff --git a/crates/vim/src/helix/select.rs b/crates/vim/src/helix/select.rs deleted file mode 100644 index d782e8b450..0000000000 --- a/crates/vim/src/helix/select.rs +++ /dev/null @@ -1,84 +0,0 @@ -use text::SelectionGoal; -use ui::{Context, Window}; - -use crate::{Vim, helix::object::cursor_range, object::Object}; - -impl Vim { - /// Selects the object each cursor is over. - /// Follows helix convention. - pub fn select_current_object( - &mut self, - object: Object, - around: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let Some(range) = object - .helix_range(map, selection.clone(), around) - .unwrap_or({ - let vim_range = object.range(map, selection.clone(), around, None); - vim_range.filter(|r| r.start <= cursor_range(selection, map).start) - }) - else { - return; - }; - - selection.set_head_tail(range.end, range.start, SelectionGoal::None); - }); - }); - }); - } - - /// Selects the next object from each cursor which the cursor is not over. - /// Follows helix convention. - pub fn select_next_object( - &mut self, - object: Object, - around: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let Ok(Some(range)) = object.helix_next_range(map, selection.clone(), around) - else { - return; - }; - - selection.set_head_tail(range.end, range.start, SelectionGoal::None); - }); - }); - }); - } - - /// Selects the previous object from each cursor which the cursor is not over. - /// Follows helix convention. - pub fn select_previous_object( - &mut self, - object: Object, - around: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let Ok(Some(range)) = - object.helix_previous_range(map, selection.clone(), around) - else { - return; - }; - - selection.set_head_tail(range.start, range.end, SelectionGoal::None); - }); - }); - }); - } -} diff --git a/crates/vim/src/indent.rs b/crates/vim/src/indent.rs deleted file mode 100644 index 927edf4d9a..0000000000 --- a/crates/vim/src/indent.rs +++ /dev/null @@ -1,247 +0,0 @@ -use crate::{Vim, motion::Motion, object::Object, state::Mode}; -use collections::HashMap; -use editor::SelectionEffects; -use editor::{Bias, Editor, display_map::ToDisplayPoint}; -use gpui::actions; -use gpui::{Context, Window}; -use language::SelectionGoal; -use settings::Settings; -use vim_mode_setting::HelixModeSetting; - -#[derive(PartialEq, Eq)] -pub(crate) enum IndentDirection { - In, - Out, - Auto, -} - -actions!( - vim, - [ - /// Increases indentation of selected lines. - Indent, - /// Decreases indentation of selected lines. - Outdent, - /// Automatically adjusts indentation based on syntax. - AutoIndent - ] -); - -pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { - Vim::action(editor, cx, |vim, _: &Indent, window, cx| { - vim.record_current_action(cx); - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - vim.store_visual_marks(window, cx); - vim.update_editor(cx, |vim, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - let original_positions = vim.save_selection_starts(editor, cx); - for _ in 0..count { - editor.indent(&Default::default(), window, cx); - } - if !HelixModeSetting::get_global(cx).0 { - vim.restore_selection_cursors(editor, window, cx, original_positions); - } - }); - }); - if vim.mode.is_visual() { - vim.switch_mode(Mode::Normal, true, window, cx) - } - }); - - Vim::action(editor, cx, |vim, _: &Outdent, window, cx| { - vim.record_current_action(cx); - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - vim.store_visual_marks(window, cx); - vim.update_editor(cx, |vim, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - let original_positions = vim.save_selection_starts(editor, cx); - for _ in 0..count { - editor.outdent(&Default::default(), window, cx); - } - if !HelixModeSetting::get_global(cx).0 { - vim.restore_selection_cursors(editor, window, cx, original_positions); - } - }); - }); - if vim.mode.is_visual() { - vim.switch_mode(Mode::Normal, true, window, cx) - } - }); - - Vim::action(editor, cx, |vim, _: &AutoIndent, window, cx| { - vim.record_current_action(cx); - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - vim.store_visual_marks(window, cx); - vim.update_editor(cx, |vim, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - let original_positions = vim.save_selection_starts(editor, cx); - for _ in 0..count { - editor.autoindent(&Default::default(), window, cx); - } - vim.restore_selection_cursors(editor, window, cx, original_positions); - }); - }); - if vim.mode.is_visual() { - vim.switch_mode(Mode::Normal, true, window, cx) - } - }); -} - -impl Vim { - pub(crate) fn indent_motion( - &mut self, - motion: Motion, - times: Option, - forced_motion: bool, - dir: IndentDirection, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - self.update_editor(cx, |_, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - editor.transact(window, cx, |editor, window, cx| { - let mut selection_starts: HashMap<_, _> = Default::default(); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let anchor = map.display_point_to_anchor(selection.head(), Bias::Right); - selection_starts.insert(selection.id, anchor); - motion.expand_selection( - map, - selection, - times, - &text_layout_details, - forced_motion, - ); - }); - }); - match dir { - IndentDirection::In => editor.indent(&Default::default(), window, cx), - IndentDirection::Out => editor.outdent(&Default::default(), window, cx), - IndentDirection::Auto => editor.autoindent(&Default::default(), window, cx), - } - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let anchor = selection_starts.remove(&selection.id).unwrap(); - selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); - }); - }); - }); - }); - } - - pub(crate) fn indent_object( - &mut self, - object: Object, - around: bool, - dir: IndentDirection, - times: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - self.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - let mut original_positions: HashMap<_, _> = Default::default(); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let anchor = map.display_point_to_anchor(selection.head(), Bias::Right); - original_positions.insert(selection.id, anchor); - object.expand_selection(map, selection, around, times); - }); - }); - match dir { - IndentDirection::In => editor.indent(&Default::default(), window, cx), - IndentDirection::Out => editor.outdent(&Default::default(), window, cx), - IndentDirection::Auto => editor.autoindent(&Default::default(), window, cx), - } - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let anchor = original_positions.remove(&selection.id).unwrap(); - selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); - }); - }); - }); - }); - } -} - -#[cfg(test)] -mod test { - use crate::{ - state::Mode, - test::{NeovimBackedTestContext, VimTestContext}, - }; - use indoc::indoc; - - #[gpui::test] - async fn test_indent_gv(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_neovim_option("shiftwidth=4").await; - - cx.set_shared_state("ˇhello\nworld\n").await; - cx.simulate_shared_keystrokes("v j > g v").await; - cx.shared_state() - .await - .assert_eq("« hello\n ˇ» world\n"); - } - - #[gpui::test] - async fn test_indent_hx(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.enable_helix(); - - cx.set_state("«Hello\nWorldˇ»\n", Mode::HelixNormal); - - cx.simulate_keystrokes(">"); - cx.assert_state(" «Hello\n Worldˇ»\n", Mode::HelixNormal); - - cx.simulate_keystrokes("<"); - cx.assert_state("«Hello\nWorldˇ»\n", Mode::HelixNormal); - } - - #[gpui::test] - async fn test_autoindent_op(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc!( - " - fn a() { - b(); - c(); - - d(); - ˇe(); - f(); - - g(); - } - " - ), - Mode::Normal, - ); - - cx.simulate_keystrokes("= a p"); - cx.assert_state( - indoc!( - " - fn a() { - b(); - c(); - - d(); - ˇe(); - f(); - - g(); - } - " - ), - Mode::Normal, - ); - } -} diff --git a/crates/vim/src/insert.rs b/crates/vim/src/insert.rs deleted file mode 100644 index d5323f31dc..0000000000 --- a/crates/vim/src/insert.rs +++ /dev/null @@ -1,205 +0,0 @@ -use crate::{Vim, state::Mode}; -use editor::{Bias, Editor}; -use gpui::{Action, Context, Window, actions}; -use language::SelectionGoal; -use settings::Settings; -use text::Point; -use vim_mode_setting::HelixModeSetting; -use workspace::searchable::Direction; - -actions!( - vim, - [ - /// Switches to normal mode with cursor positioned before the current character. - NormalBefore, - /// Temporarily switches to normal mode for one command. - TemporaryNormal, - /// Inserts the next character from the line above into the current line. - InsertFromAbove, - /// Inserts the next character from the line below into the current line. - InsertFromBelow - ] -); - -pub fn register(editor: &mut Editor, cx: &mut Context) { - Vim::action(editor, cx, Vim::normal_before); - Vim::action(editor, cx, Vim::temporary_normal); - Vim::action(editor, cx, |vim, _: &InsertFromAbove, window, cx| { - vim.insert_around(Direction::Prev, window, cx) - }); - Vim::action(editor, cx, |vim, _: &InsertFromBelow, window, cx| { - vim.insert_around(Direction::Next, window, cx) - }) -} - -impl Vim { - pub(crate) fn normal_before( - &mut self, - action: &NormalBefore, - window: &mut Window, - cx: &mut Context, - ) { - if self.active_operator().is_some() { - self.operator_stack.clear(); - self.sync_vim_settings(window, cx); - return; - } - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - self.stop_recording_immediately(action.boxed_clone(), cx); - if count <= 1 || Vim::globals(cx).dot_replaying { - self.create_mark("^".into(), window, cx); - - if HelixModeSetting::get_global(cx).0 { - self.update_editor(cx, |_, editor, cx| { - editor.dismiss_menus_and_popups(false, window, cx); - }); - self.switch_mode(Mode::HelixNormal, false, window, cx); - return; - } - - self.update_editor(cx, |_, editor, cx| { - editor.dismiss_menus_and_popups(false, window, cx); - - editor.change_selections(Default::default(), window, cx, |s| { - s.move_cursors_with(|map, mut cursor, _| { - *cursor.column_mut() = cursor.column().saturating_sub(1); - (map.clip_point(cursor, Bias::Left), SelectionGoal::None) - }); - }); - }); - - self.switch_mode(Mode::Normal, false, window, cx); - return; - } - - self.repeat(true, window, cx) - } - - fn temporary_normal( - &mut self, - _: &TemporaryNormal, - window: &mut Window, - cx: &mut Context, - ) { - self.switch_mode(Mode::Normal, true, window, cx); - self.temp_mode = true; - } - - fn insert_around(&mut self, direction: Direction, _: &mut Window, cx: &mut Context) { - self.update_editor(cx, |_, editor, cx| { - let snapshot = editor.buffer().read(cx).snapshot(cx); - let mut edits = Vec::new(); - for selection in editor.selections.all::(&editor.display_snapshot(cx)) { - let point = selection.head(); - let new_row = match direction { - Direction::Next => point.row + 1, - Direction::Prev if point.row > 0 => point.row - 1, - _ => continue, - }; - let source = snapshot.clip_point(Point::new(new_row, point.column), Bias::Left); - if let Some(c) = snapshot.chars_at(source).next() - && c != '\n' - { - edits.push((point..point, c.to_string())) - } - } - - editor.edit(edits, cx); - }); - } -} - -#[cfg(test)] -mod test { - use crate::{ - state::Mode, - test::{NeovimBackedTestContext, VimTestContext}, - }; - - #[gpui::test] - async fn test_enter_and_exit_insert_mode(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.simulate_keystrokes("i"); - assert_eq!(cx.mode(), Mode::Insert); - cx.simulate_keystrokes("T e s t"); - cx.assert_editor_state("Testˇ"); - cx.simulate_keystrokes("escape"); - assert_eq!(cx.mode(), Mode::Normal); - cx.assert_editor_state("Tesˇt"); - } - - #[gpui::test] - async fn test_insert_with_counts(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇhello\n").await; - cx.simulate_shared_keystrokes("5 i - escape").await; - cx.shared_state().await.assert_eq("----ˇ-hello\n"); - - cx.set_shared_state("ˇhello\n").await; - cx.simulate_shared_keystrokes("5 a - escape").await; - cx.shared_state().await.assert_eq("h----ˇ-ello\n"); - - cx.simulate_shared_keystrokes("4 shift-i - escape").await; - cx.shared_state().await.assert_eq("---ˇ-h-----ello\n"); - - cx.simulate_shared_keystrokes("3 shift-a - escape").await; - cx.shared_state().await.assert_eq("----h-----ello--ˇ-\n"); - - cx.set_shared_state("ˇhello\n").await; - cx.simulate_shared_keystrokes("3 o o i escape").await; - cx.shared_state().await.assert_eq("hello\noi\noi\noˇi\n"); - - cx.set_shared_state("ˇhello\n").await; - cx.simulate_shared_keystrokes("3 shift-o o i escape").await; - cx.shared_state().await.assert_eq("oi\noi\noˇi\nhello\n"); - } - - #[gpui::test] - async fn test_insert_with_repeat(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇhello\n").await; - cx.simulate_shared_keystrokes("3 i - escape").await; - cx.shared_state().await.assert_eq("--ˇ-hello\n"); - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq("----ˇ--hello\n"); - cx.simulate_shared_keystrokes("2 .").await; - cx.shared_state().await.assert_eq("-----ˇ---hello\n"); - - cx.set_shared_state("ˇhello\n").await; - cx.simulate_shared_keystrokes("2 o k k escape").await; - cx.shared_state().await.assert_eq("hello\nkk\nkˇk\n"); - cx.simulate_shared_keystrokes(".").await; - cx.shared_state() - .await - .assert_eq("hello\nkk\nkk\nkk\nkˇk\n"); - cx.simulate_shared_keystrokes("1 .").await; - cx.shared_state() - .await - .assert_eq("hello\nkk\nkk\nkk\nkk\nkˇk\n"); - } - - #[gpui::test] - async fn test_insert_ctrl_r(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("heˇllo\n").await; - cx.simulate_shared_keystrokes("y y i ctrl-r \"").await; - cx.shared_state().await.assert_eq("hehello\nˇllo\n"); - - cx.simulate_shared_keystrokes("ctrl-r x ctrl-r escape") - .await; - cx.shared_state().await.assert_eq("hehello\nˇllo\n"); - } - - #[gpui::test] - async fn test_insert_ctrl_y(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("hello\nˇ\nworld").await; - cx.simulate_shared_keystrokes("i ctrl-y ctrl-e").await; - cx.shared_state().await.assert_eq("hello\nhoˇ\nworld"); - } -} diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs deleted file mode 100644 index 42d4915fc5..0000000000 --- a/crates/vim/src/mode_indicator.rs +++ /dev/null @@ -1,180 +0,0 @@ -use gpui::{Context, Element, Entity, FontWeight, Render, Subscription, WeakEntity, Window, div}; -use ui::text_for_keystrokes; -use workspace::{StatusItemView, item::ItemHandle, ui::prelude::*}; - -use crate::{Vim, VimEvent, VimGlobals}; - -/// The ModeIndicator displays the current mode in the status bar. -pub struct ModeIndicator { - vim: Option>, - pending_keys: Option, - vim_subscription: Option, -} - -impl ModeIndicator { - /// Construct a new mode indicator in this window. - pub fn new(window: &mut Window, cx: &mut Context) -> Self { - cx.observe_pending_input(window, |this: &mut Self, window, cx| { - this.update_pending_keys(window, cx); - cx.notify(); - }) - .detach(); - - let handle = cx.entity(); - let window_handle = window.window_handle(); - cx.observe_new::(move |_, window, cx| { - let Some(window) = window else { - return; - }; - if window.window_handle() != window_handle { - return; - } - let vim = cx.entity(); - handle.update(cx, |_, cx| { - cx.subscribe(&vim, |mode_indicator, vim, event, cx| match event { - VimEvent::Focused => { - mode_indicator.vim_subscription = - Some(cx.observe(&vim, |_, _, cx| cx.notify())); - mode_indicator.vim = Some(vim.downgrade()); - } - }) - .detach() - }) - }) - .detach(); - - Self { - vim: None, - pending_keys: None, - vim_subscription: None, - } - } - - fn update_pending_keys(&mut self, window: &mut Window, cx: &App) { - self.pending_keys = window - .pending_input_keystrokes() - .map(|keystrokes| text_for_keystrokes(keystrokes, cx)); - } - - fn vim(&self) -> Option> { - self.vim.as_ref().and_then(|vim| vim.upgrade()) - } - - fn current_operators_description(&self, vim: Entity, cx: &mut Context) -> String { - let recording = Vim::globals(cx) - .recording_register - .map(|reg| format!("recording @{reg} ")) - .into_iter(); - - let vim = vim.read(cx); - recording - .chain( - cx.global::() - .pre_count - .map(|count| format!("{}", count)), - ) - .chain(vim.selected_register.map(|reg| format!("\"{reg}"))) - .chain(vim.operator_stack.iter().map(|item| item.status())) - .chain( - cx.global::() - .post_count - .map(|count| format!("{}", count)), - ) - .collect::>() - .join("") - } -} - -impl Render for ModeIndicator { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let vim = self.vim(); - let Some(vim) = vim else { - return div().hidden().into_any_element(); - }; - - let vim_readable = vim.read(cx); - let status_label = vim_readable.status_label.clone(); - let temp_mode = vim_readable.temp_mode; - let mode = vim_readable.mode; - - let theme = cx.theme(); - let colors = theme.colors(); - let system_transparent = gpui::hsla(0.0, 0.0, 0.0, 0.0); - let vim_mode_text = colors.vim_mode_text; - let bg_color = match mode { - crate::state::Mode::Normal => colors.vim_normal_background, - crate::state::Mode::Insert => colors.vim_insert_background, - crate::state::Mode::Replace => colors.vim_replace_background, - crate::state::Mode::Visual => colors.vim_visual_background, - crate::state::Mode::VisualLine => colors.vim_visual_line_background, - crate::state::Mode::VisualBlock => colors.vim_visual_block_background, - crate::state::Mode::HelixNormal => colors.vim_helix_normal_background, - crate::state::Mode::HelixSelect => colors.vim_helix_select_background, - }; - - let (label, mode): (SharedString, Option) = if let Some(label) = status_label - { - (label, None) - } else { - let mode_str = if temp_mode { - format!("(insert) {}", mode) - } else { - mode.to_string() - }; - - let current_operators_description = self.current_operators_description(vim.clone(), cx); - let pending = self - .pending_keys - .as_ref() - .unwrap_or(¤t_operators_description); - let mode = if bg_color != system_transparent { - mode_str.into() - } else { - format!("-- {} --", mode_str).into() - }; - (pending.into(), Some(mode)) - }; - h_flex() - .gap_1() - .when(!label.is_empty(), |el| { - el.child( - Label::new(label) - .line_height_style(LineHeightStyle::UiLabel) - .weight(FontWeight::MEDIUM), - ) - }) - .when_some(mode, |el, mode| { - el.child( - v_flex() - .when(bg_color != system_transparent, |el| el.px_2()) - // match with other icons at the bottom that use default buttons - .h(ButtonSize::Default.rems()) - .justify_center() - .rounded_sm() - .bg(bg_color) - .child( - Label::new(mode) - .size(LabelSize::Small) - .line_height_style(LineHeightStyle::UiLabel) - .weight(FontWeight::MEDIUM) - .when( - bg_color != system_transparent - && vim_mode_text != system_transparent, - |el| el.color(Color::Custom(vim_mode_text)), - ), - ), - ) - }) - .into_any() - } -} - -impl StatusItemView for ModeIndicator { - fn set_active_pane_item( - &mut self, - _active_pane_item: Option<&dyn ItemHandle>, - _window: &mut Window, - _cx: &mut Context, - ) { - } -} diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs deleted file mode 100644 index 6ba28a1c23..0000000000 --- a/crates/vim/src/motion.rs +++ /dev/null @@ -1,4484 +0,0 @@ -use editor::{ - Anchor, Bias, BufferOffset, DisplayPoint, Editor, MultiBufferOffset, RowExt, ToOffset, ToPoint, - display_map::{DisplayRow, DisplaySnapshot, FoldPoint, ToDisplayPoint}, - movement::{ - self, FindRange, TextLayoutDetails, find_boundary, find_preceding_boundary_display_point, - }, -}; -use gpui::{Action, Context, Window, actions, px}; -use language::{CharKind, Point, Selection, SelectionGoal}; -use multi_buffer::MultiBufferRow; -use schemars::JsonSchema; -use serde::Deserialize; -use std::ops::Range; -use workspace::searchable::Direction; - -use crate::{ - Vim, - normal::mark, - state::{Mode, Operator}, - surrounds::SurroundsType, -}; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) enum MotionKind { - Linewise, - Exclusive, - Inclusive, -} - -impl MotionKind { - pub(crate) fn for_mode(mode: Mode) -> Self { - match mode { - Mode::VisualLine => MotionKind::Linewise, - _ => MotionKind::Exclusive, - } - } - - pub(crate) fn linewise(&self) -> bool { - matches!(self, MotionKind::Linewise) - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum Motion { - Left, - WrappingLeft, - Down { - display_lines: bool, - }, - Up { - display_lines: bool, - }, - Right, - WrappingRight, - NextWordStart { - ignore_punctuation: bool, - }, - NextWordEnd { - ignore_punctuation: bool, - }, - PreviousWordStart { - ignore_punctuation: bool, - }, - PreviousWordEnd { - ignore_punctuation: bool, - }, - NextSubwordStart { - ignore_punctuation: bool, - }, - NextSubwordEnd { - ignore_punctuation: bool, - }, - PreviousSubwordStart { - ignore_punctuation: bool, - }, - PreviousSubwordEnd { - ignore_punctuation: bool, - }, - FirstNonWhitespace { - display_lines: bool, - }, - CurrentLine, - StartOfLine { - display_lines: bool, - }, - MiddleOfLine { - display_lines: bool, - }, - EndOfLine { - display_lines: bool, - }, - SentenceBackward, - SentenceForward, - StartOfParagraph, - EndOfParagraph, - StartOfDocument, - EndOfDocument, - Matching, - GoToPercentage, - UnmatchedForward { - char: char, - }, - UnmatchedBackward { - char: char, - }, - FindForward { - before: bool, - char: char, - mode: FindRange, - smartcase: bool, - }, - FindBackward { - after: bool, - char: char, - mode: FindRange, - smartcase: bool, - }, - Sneak { - first_char: char, - second_char: char, - smartcase: bool, - }, - SneakBackward { - first_char: char, - second_char: char, - smartcase: bool, - }, - RepeatFind { - last_find: Box, - }, - RepeatFindReversed { - last_find: Box, - }, - NextLineStart, - PreviousLineStart, - StartOfLineDownward, - EndOfLineDownward, - GoToColumn, - WindowTop, - WindowMiddle, - WindowBottom, - NextSectionStart, - NextSectionEnd, - PreviousSectionStart, - PreviousSectionEnd, - NextMethodStart, - NextMethodEnd, - PreviousMethodStart, - PreviousMethodEnd, - NextComment, - PreviousComment, - PreviousLesserIndent, - PreviousGreaterIndent, - PreviousSameIndent, - NextLesserIndent, - NextGreaterIndent, - NextSameIndent, - - // we don't have a good way to run a search synchronously, so - // we handle search motions by running the search async and then - // calling back into motion with this - ZedSearchResult { - prior_selections: Vec>, - new_selections: Vec>, - }, - Jump { - anchor: Anchor, - line: bool, - }, -} - -#[derive(Clone, Copy)] -enum IndentType { - Lesser, - Greater, - Same, -} - -/// Moves to the start of the next word. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct NextWordStart { - #[serde(default)] - ignore_punctuation: bool, -} - -/// Moves to the end of the next word. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct NextWordEnd { - #[serde(default)] - ignore_punctuation: bool, -} - -/// Moves to the start of the previous word. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct PreviousWordStart { - #[serde(default)] - ignore_punctuation: bool, -} - -/// Moves to the end of the previous word. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct PreviousWordEnd { - #[serde(default)] - ignore_punctuation: bool, -} - -/// Moves to the start of the next subword. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -pub(crate) struct NextSubwordStart { - #[serde(default)] - pub(crate) ignore_punctuation: bool, -} - -/// Moves to the end of the next subword. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -pub(crate) struct NextSubwordEnd { - #[serde(default)] - pub(crate) ignore_punctuation: bool, -} - -/// Moves to the start of the previous subword. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -pub(crate) struct PreviousSubwordStart { - #[serde(default)] - pub(crate) ignore_punctuation: bool, -} - -/// Moves to the end of the previous subword. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -pub(crate) struct PreviousSubwordEnd { - #[serde(default)] - pub(crate) ignore_punctuation: bool, -} - -/// Moves cursor up by the specified number of lines. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -pub(crate) struct Up { - #[serde(default)] - pub(crate) display_lines: bool, -} - -/// Moves cursor down by the specified number of lines. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -pub(crate) struct Down { - #[serde(default)] - pub(crate) display_lines: bool, -} - -/// Moves to the first non-whitespace character on the current line. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct FirstNonWhitespace { - #[serde(default)] - display_lines: bool, -} - -/// Moves to the end of the current line. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct EndOfLine { - #[serde(default)] - display_lines: bool, -} - -/// Moves to the start of the current line. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -pub struct StartOfLine { - #[serde(default)] - pub(crate) display_lines: bool, -} - -/// Moves to the middle of the current line. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct MiddleOfLine { - #[serde(default)] - display_lines: bool, -} - -/// Finds the next unmatched bracket or delimiter. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct UnmatchedForward { - #[serde(default)] - char: char, -} - -/// Finds the previous unmatched bracket or delimiter. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct UnmatchedBackward { - #[serde(default)] - char: char, -} - -actions!( - vim, - [ - /// Moves cursor left one character. - Left, - /// Moves cursor left one character, wrapping to previous line. - #[action(deprecated_aliases = ["vim::Backspace"])] - WrappingLeft, - /// Moves cursor right one character. - Right, - /// Moves cursor right one character, wrapping to next line. - #[action(deprecated_aliases = ["vim::Space"])] - WrappingRight, - /// Selects the current line. - CurrentLine, - /// Moves to the start of the next sentence. - SentenceForward, - /// Moves to the start of the previous sentence. - SentenceBackward, - /// Moves to the start of the paragraph. - StartOfParagraph, - /// Moves to the end of the paragraph. - EndOfParagraph, - /// Moves to the start of the document. - StartOfDocument, - /// Moves to the end of the document. - EndOfDocument, - /// Moves to the matching bracket or delimiter. - Matching, - /// Goes to a percentage position in the file. - GoToPercentage, - /// Moves to the start of the next line. - NextLineStart, - /// Moves to the start of the previous line. - PreviousLineStart, - /// Moves to the start of a line downward. - StartOfLineDownward, - /// Moves to the end of a line downward. - EndOfLineDownward, - /// Goes to a specific column number. - GoToColumn, - /// Repeats the last character find. - RepeatFind, - /// Repeats the last character find in reverse. - RepeatFindReversed, - /// Moves to the top of the window. - WindowTop, - /// Moves to the middle of the window. - WindowMiddle, - /// Moves to the bottom of the window. - WindowBottom, - /// Moves to the start of the next section. - NextSectionStart, - /// Moves to the end of the next section. - NextSectionEnd, - /// Moves to the start of the previous section. - PreviousSectionStart, - /// Moves to the end of the previous section. - PreviousSectionEnd, - /// Moves to the start of the next method. - NextMethodStart, - /// Moves to the end of the next method. - NextMethodEnd, - /// Moves to the start of the previous method. - PreviousMethodStart, - /// Moves to the end of the previous method. - PreviousMethodEnd, - /// Moves to the next comment. - NextComment, - /// Moves to the previous comment. - PreviousComment, - /// Moves to the previous line with lesser indentation. - PreviousLesserIndent, - /// Moves to the previous line with greater indentation. - PreviousGreaterIndent, - /// Moves to the previous line with the same indentation. - PreviousSameIndent, - /// Moves to the next line with lesser indentation. - NextLesserIndent, - /// Moves to the next line with greater indentation. - NextGreaterIndent, - /// Moves to the next line with the same indentation. - NextSameIndent, - ] -); - -pub fn register(editor: &mut Editor, cx: &mut Context) { - Vim::action(editor, cx, |vim, _: &Left, window, cx| { - vim.motion(Motion::Left, window, cx) - }); - Vim::action(editor, cx, |vim, _: &WrappingLeft, window, cx| { - vim.motion(Motion::WrappingLeft, window, cx) - }); - Vim::action(editor, cx, |vim, action: &Down, window, cx| { - vim.motion( - Motion::Down { - display_lines: action.display_lines, - }, - window, - cx, - ) - }); - Vim::action(editor, cx, |vim, action: &Up, window, cx| { - vim.motion( - Motion::Up { - display_lines: action.display_lines, - }, - window, - cx, - ) - }); - Vim::action(editor, cx, |vim, _: &Right, window, cx| { - vim.motion(Motion::Right, window, cx) - }); - Vim::action(editor, cx, |vim, _: &WrappingRight, window, cx| { - vim.motion(Motion::WrappingRight, window, cx) - }); - Vim::action( - editor, - cx, - |vim, action: &FirstNonWhitespace, window, cx| { - vim.motion( - Motion::FirstNonWhitespace { - display_lines: action.display_lines, - }, - window, - cx, - ) - }, - ); - Vim::action(editor, cx, |vim, action: &StartOfLine, window, cx| { - vim.motion( - Motion::StartOfLine { - display_lines: action.display_lines, - }, - window, - cx, - ) - }); - Vim::action(editor, cx, |vim, action: &MiddleOfLine, window, cx| { - vim.motion( - Motion::MiddleOfLine { - display_lines: action.display_lines, - }, - window, - cx, - ) - }); - Vim::action(editor, cx, |vim, action: &EndOfLine, window, cx| { - vim.motion( - Motion::EndOfLine { - display_lines: action.display_lines, - }, - window, - cx, - ) - }); - Vim::action(editor, cx, |vim, _: &CurrentLine, window, cx| { - vim.motion(Motion::CurrentLine, window, cx) - }); - Vim::action(editor, cx, |vim, _: &StartOfParagraph, window, cx| { - vim.motion(Motion::StartOfParagraph, window, cx) - }); - Vim::action(editor, cx, |vim, _: &EndOfParagraph, window, cx| { - vim.motion(Motion::EndOfParagraph, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &SentenceForward, window, cx| { - vim.motion(Motion::SentenceForward, window, cx) - }); - Vim::action(editor, cx, |vim, _: &SentenceBackward, window, cx| { - vim.motion(Motion::SentenceBackward, window, cx) - }); - Vim::action(editor, cx, |vim, _: &StartOfDocument, window, cx| { - vim.motion(Motion::StartOfDocument, window, cx) - }); - Vim::action(editor, cx, |vim, _: &EndOfDocument, window, cx| { - vim.motion(Motion::EndOfDocument, window, cx) - }); - Vim::action(editor, cx, |vim, _: &Matching, window, cx| { - vim.motion(Motion::Matching, window, cx) - }); - Vim::action(editor, cx, |vim, _: &GoToPercentage, window, cx| { - vim.motion(Motion::GoToPercentage, window, cx) - }); - Vim::action( - editor, - cx, - |vim, &UnmatchedForward { char }: &UnmatchedForward, window, cx| { - vim.motion(Motion::UnmatchedForward { char }, window, cx) - }, - ); - Vim::action( - editor, - cx, - |vim, &UnmatchedBackward { char }: &UnmatchedBackward, window, cx| { - vim.motion(Motion::UnmatchedBackward { char }, window, cx) - }, - ); - Vim::action( - editor, - cx, - |vim, &NextWordStart { ignore_punctuation }: &NextWordStart, window, cx| { - vim.motion(Motion::NextWordStart { ignore_punctuation }, window, cx) - }, - ); - Vim::action( - editor, - cx, - |vim, &NextWordEnd { ignore_punctuation }: &NextWordEnd, window, cx| { - vim.motion(Motion::NextWordEnd { ignore_punctuation }, window, cx) - }, - ); - Vim::action( - editor, - cx, - |vim, &PreviousWordStart { ignore_punctuation }: &PreviousWordStart, window, cx| { - vim.motion(Motion::PreviousWordStart { ignore_punctuation }, window, cx) - }, - ); - Vim::action( - editor, - cx, - |vim, &PreviousWordEnd { ignore_punctuation }, window, cx| { - vim.motion(Motion::PreviousWordEnd { ignore_punctuation }, window, cx) - }, - ); - Vim::action( - editor, - cx, - |vim, &NextSubwordStart { ignore_punctuation }: &NextSubwordStart, window, cx| { - vim.motion(Motion::NextSubwordStart { ignore_punctuation }, window, cx) - }, - ); - Vim::action( - editor, - cx, - |vim, &NextSubwordEnd { ignore_punctuation }: &NextSubwordEnd, window, cx| { - vim.motion(Motion::NextSubwordEnd { ignore_punctuation }, window, cx) - }, - ); - Vim::action( - editor, - cx, - |vim, &PreviousSubwordStart { ignore_punctuation }: &PreviousSubwordStart, window, cx| { - vim.motion( - Motion::PreviousSubwordStart { ignore_punctuation }, - window, - cx, - ) - }, - ); - Vim::action( - editor, - cx, - |vim, &PreviousSubwordEnd { ignore_punctuation }, window, cx| { - vim.motion( - Motion::PreviousSubwordEnd { ignore_punctuation }, - window, - cx, - ) - }, - ); - Vim::action(editor, cx, |vim, &NextLineStart, window, cx| { - vim.motion(Motion::NextLineStart, window, cx) - }); - Vim::action(editor, cx, |vim, &PreviousLineStart, window, cx| { - vim.motion(Motion::PreviousLineStart, window, cx) - }); - Vim::action(editor, cx, |vim, &StartOfLineDownward, window, cx| { - vim.motion(Motion::StartOfLineDownward, window, cx) - }); - Vim::action(editor, cx, |vim, &EndOfLineDownward, window, cx| { - vim.motion(Motion::EndOfLineDownward, window, cx) - }); - Vim::action(editor, cx, |vim, &GoToColumn, window, cx| { - vim.motion(Motion::GoToColumn, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &RepeatFind, window, cx| { - if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) { - vim.motion(Motion::RepeatFind { last_find }, window, cx); - } - }); - - Vim::action(editor, cx, |vim, _: &RepeatFindReversed, window, cx| { - if let Some(last_find) = Vim::globals(cx).last_find.clone().map(Box::new) { - vim.motion(Motion::RepeatFindReversed { last_find }, window, cx); - } - }); - Vim::action(editor, cx, |vim, &WindowTop, window, cx| { - vim.motion(Motion::WindowTop, window, cx) - }); - Vim::action(editor, cx, |vim, &WindowMiddle, window, cx| { - vim.motion(Motion::WindowMiddle, window, cx) - }); - Vim::action(editor, cx, |vim, &WindowBottom, window, cx| { - vim.motion(Motion::WindowBottom, window, cx) - }); - - Vim::action(editor, cx, |vim, &PreviousSectionStart, window, cx| { - vim.motion(Motion::PreviousSectionStart, window, cx) - }); - Vim::action(editor, cx, |vim, &NextSectionStart, window, cx| { - vim.motion(Motion::NextSectionStart, window, cx) - }); - Vim::action(editor, cx, |vim, &PreviousSectionEnd, window, cx| { - vim.motion(Motion::PreviousSectionEnd, window, cx) - }); - Vim::action(editor, cx, |vim, &NextSectionEnd, window, cx| { - vim.motion(Motion::NextSectionEnd, window, cx) - }); - Vim::action(editor, cx, |vim, &PreviousMethodStart, window, cx| { - vim.motion(Motion::PreviousMethodStart, window, cx) - }); - Vim::action(editor, cx, |vim, &NextMethodStart, window, cx| { - vim.motion(Motion::NextMethodStart, window, cx) - }); - Vim::action(editor, cx, |vim, &PreviousMethodEnd, window, cx| { - vim.motion(Motion::PreviousMethodEnd, window, cx) - }); - Vim::action(editor, cx, |vim, &NextMethodEnd, window, cx| { - vim.motion(Motion::NextMethodEnd, window, cx) - }); - Vim::action(editor, cx, |vim, &NextComment, window, cx| { - vim.motion(Motion::NextComment, window, cx) - }); - Vim::action(editor, cx, |vim, &PreviousComment, window, cx| { - vim.motion(Motion::PreviousComment, window, cx) - }); - Vim::action(editor, cx, |vim, &PreviousLesserIndent, window, cx| { - vim.motion(Motion::PreviousLesserIndent, window, cx) - }); - Vim::action(editor, cx, |vim, &PreviousGreaterIndent, window, cx| { - vim.motion(Motion::PreviousGreaterIndent, window, cx) - }); - Vim::action(editor, cx, |vim, &PreviousSameIndent, window, cx| { - vim.motion(Motion::PreviousSameIndent, window, cx) - }); - Vim::action(editor, cx, |vim, &NextLesserIndent, window, cx| { - vim.motion(Motion::NextLesserIndent, window, cx) - }); - Vim::action(editor, cx, |vim, &NextGreaterIndent, window, cx| { - vim.motion(Motion::NextGreaterIndent, window, cx) - }); - Vim::action(editor, cx, |vim, &NextSameIndent, window, cx| { - vim.motion(Motion::NextSameIndent, window, cx) - }); -} - -impl Vim { - pub(crate) fn search_motion(&mut self, m: Motion, window: &mut Window, cx: &mut Context) { - if let Motion::ZedSearchResult { - prior_selections, .. - } = &m - { - match self.mode { - Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { - if !prior_selections.is_empty() { - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.select_ranges(prior_selections.iter().cloned()) - }) - }); - } - } - Mode::Normal | Mode::Replace | Mode::Insert => { - if self.active_operator().is_none() { - return; - } - } - Mode::HelixNormal | Mode::HelixSelect => {} - } - } - - self.motion(m, window, cx) - } - - pub(crate) fn motion(&mut self, motion: Motion, window: &mut Window, cx: &mut Context) { - if let Some(Operator::FindForward { .. }) - | Some(Operator::Sneak { .. }) - | Some(Operator::SneakBackward { .. }) - | Some(Operator::FindBackward { .. }) = self.active_operator() - { - self.pop_operator(window, cx); - } - - let count = Vim::take_count(cx); - let forced_motion = Vim::take_forced_motion(cx); - let active_operator = self.active_operator(); - let mut waiting_operator: Option = None; - match self.mode { - Mode::Normal | Mode::Replace | Mode::Insert => { - if active_operator == Some(Operator::AddSurrounds { target: None }) { - waiting_operator = Some(Operator::AddSurrounds { - target: Some(SurroundsType::Motion(motion)), - }); - } else { - self.normal_motion(motion, active_operator, count, forced_motion, window, cx) - } - } - Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { - self.visual_motion(motion, count, window, cx) - } - - Mode::HelixNormal => self.helix_normal_motion(motion, count, window, cx), - Mode::HelixSelect => self.helix_select_motion(motion, count, window, cx), - } - self.clear_operator(window, cx); - if let Some(operator) = waiting_operator { - self.push_operator(operator, window, cx); - Vim::globals(cx).pre_count = count - } - } -} - -// Motion handling is specified here: -// https://github.com/vim/vim/blob/master/runtime/doc/motion.txt -impl Motion { - fn default_kind(&self) -> MotionKind { - use Motion::*; - match self { - Down { .. } - | Up { .. } - | StartOfDocument - | EndOfDocument - | CurrentLine - | NextLineStart - | PreviousLineStart - | StartOfLineDownward - | WindowTop - | WindowMiddle - | WindowBottom - | NextSectionStart - | NextSectionEnd - | PreviousSectionStart - | PreviousSectionEnd - | NextMethodStart - | NextMethodEnd - | PreviousMethodStart - | PreviousMethodEnd - | NextComment - | PreviousComment - | PreviousLesserIndent - | PreviousGreaterIndent - | PreviousSameIndent - | NextLesserIndent - | NextGreaterIndent - | NextSameIndent - | GoToPercentage - | Jump { line: true, .. } => MotionKind::Linewise, - EndOfLine { .. } - | EndOfLineDownward - | Matching - | FindForward { .. } - | NextWordEnd { .. } - | PreviousWordEnd { .. } - | NextSubwordEnd { .. } - | PreviousSubwordEnd { .. } => MotionKind::Inclusive, - Left - | WrappingLeft - | Right - | WrappingRight - | StartOfLine { .. } - | StartOfParagraph - | EndOfParagraph - | SentenceBackward - | SentenceForward - | GoToColumn - | MiddleOfLine { .. } - | UnmatchedForward { .. } - | UnmatchedBackward { .. } - | NextWordStart { .. } - | PreviousWordStart { .. } - | NextSubwordStart { .. } - | PreviousSubwordStart { .. } - | FirstNonWhitespace { .. } - | FindBackward { .. } - | Sneak { .. } - | SneakBackward { .. } - | Jump { .. } - | ZedSearchResult { .. } => MotionKind::Exclusive, - RepeatFind { last_find: motion } | RepeatFindReversed { last_find: motion } => { - motion.default_kind() - } - } - } - - fn skip_exclusive_special_case(&self) -> bool { - matches!(self, Motion::WrappingLeft | Motion::WrappingRight) - } - - pub(crate) fn push_to_jump_list(&self) -> bool { - use Motion::*; - match self { - CurrentLine - | Down { .. } - | EndOfLine { .. } - | EndOfLineDownward - | FindBackward { .. } - | FindForward { .. } - | FirstNonWhitespace { .. } - | GoToColumn - | Left - | MiddleOfLine { .. } - | NextLineStart - | NextSubwordEnd { .. } - | NextSubwordStart { .. } - | NextWordEnd { .. } - | NextWordStart { .. } - | PreviousLineStart - | PreviousSubwordEnd { .. } - | PreviousSubwordStart { .. } - | PreviousWordEnd { .. } - | PreviousWordStart { .. } - | RepeatFind { .. } - | RepeatFindReversed { .. } - | Right - | StartOfLine { .. } - | StartOfLineDownward - | Up { .. } - | WrappingLeft - | WrappingRight => false, - EndOfDocument - | EndOfParagraph - | GoToPercentage - | Jump { .. } - | Matching - | NextComment - | NextGreaterIndent - | NextLesserIndent - | NextMethodEnd - | NextMethodStart - | NextSameIndent - | NextSectionEnd - | NextSectionStart - | PreviousComment - | PreviousGreaterIndent - | PreviousLesserIndent - | PreviousMethodEnd - | PreviousMethodStart - | PreviousSameIndent - | PreviousSectionEnd - | PreviousSectionStart - | SentenceBackward - | SentenceForward - | Sneak { .. } - | SneakBackward { .. } - | StartOfDocument - | StartOfParagraph - | UnmatchedBackward { .. } - | UnmatchedForward { .. } - | WindowBottom - | WindowMiddle - | WindowTop - | ZedSearchResult { .. } => true, - } - } - - pub fn infallible(&self) -> bool { - use Motion::*; - match self { - StartOfDocument | EndOfDocument | CurrentLine => true, - Down { .. } - | Up { .. } - | EndOfLine { .. } - | MiddleOfLine { .. } - | Matching - | UnmatchedForward { .. } - | UnmatchedBackward { .. } - | FindForward { .. } - | RepeatFind { .. } - | Left - | WrappingLeft - | Right - | WrappingRight - | StartOfLine { .. } - | StartOfParagraph - | EndOfParagraph - | SentenceBackward - | SentenceForward - | StartOfLineDownward - | EndOfLineDownward - | GoToColumn - | GoToPercentage - | NextWordStart { .. } - | NextWordEnd { .. } - | PreviousWordStart { .. } - | PreviousWordEnd { .. } - | NextSubwordStart { .. } - | NextSubwordEnd { .. } - | PreviousSubwordStart { .. } - | PreviousSubwordEnd { .. } - | FirstNonWhitespace { .. } - | FindBackward { .. } - | Sneak { .. } - | SneakBackward { .. } - | RepeatFindReversed { .. } - | WindowTop - | WindowMiddle - | WindowBottom - | NextLineStart - | PreviousLineStart - | ZedSearchResult { .. } - | NextSectionStart - | NextSectionEnd - | PreviousSectionStart - | PreviousSectionEnd - | NextMethodStart - | NextMethodEnd - | PreviousMethodStart - | PreviousMethodEnd - | NextComment - | PreviousComment - | PreviousLesserIndent - | PreviousGreaterIndent - | PreviousSameIndent - | NextLesserIndent - | NextGreaterIndent - | NextSameIndent - | Jump { .. } => false, - } - } - - pub fn move_point( - &self, - map: &DisplaySnapshot, - point: DisplayPoint, - goal: SelectionGoal, - maybe_times: Option, - text_layout_details: &TextLayoutDetails, - ) -> Option<(DisplayPoint, SelectionGoal)> { - let times = maybe_times.unwrap_or(1); - use Motion::*; - let infallible = self.infallible(); - let (new_point, goal) = match self { - Left => (left(map, point, times), SelectionGoal::None), - WrappingLeft => (wrapping_left(map, point, times), SelectionGoal::None), - Down { - display_lines: false, - } => up_down_buffer_rows(map, point, goal, times as isize, text_layout_details), - Down { - display_lines: true, - } => down_display(map, point, goal, times, text_layout_details), - Up { - display_lines: false, - } => up_down_buffer_rows(map, point, goal, 0 - times as isize, text_layout_details), - Up { - display_lines: true, - } => up_display(map, point, goal, times, text_layout_details), - Right => (right(map, point, times), SelectionGoal::None), - WrappingRight => (wrapping_right(map, point, times), SelectionGoal::None), - NextWordStart { ignore_punctuation } => ( - next_word_start(map, point, *ignore_punctuation, times), - SelectionGoal::None, - ), - NextWordEnd { ignore_punctuation } => ( - next_word_end(map, point, *ignore_punctuation, times, true, true), - SelectionGoal::None, - ), - PreviousWordStart { ignore_punctuation } => ( - previous_word_start(map, point, *ignore_punctuation, times), - SelectionGoal::None, - ), - PreviousWordEnd { ignore_punctuation } => ( - previous_word_end(map, point, *ignore_punctuation, times), - SelectionGoal::None, - ), - NextSubwordStart { ignore_punctuation } => ( - next_subword_start(map, point, *ignore_punctuation, times), - SelectionGoal::None, - ), - NextSubwordEnd { ignore_punctuation } => ( - next_subword_end(map, point, *ignore_punctuation, times, true), - SelectionGoal::None, - ), - PreviousSubwordStart { ignore_punctuation } => ( - previous_subword_start(map, point, *ignore_punctuation, times), - SelectionGoal::None, - ), - PreviousSubwordEnd { ignore_punctuation } => ( - previous_subword_end(map, point, *ignore_punctuation, times), - SelectionGoal::None, - ), - FirstNonWhitespace { display_lines } => ( - first_non_whitespace(map, *display_lines, point), - SelectionGoal::None, - ), - StartOfLine { display_lines } => ( - start_of_line(map, *display_lines, point), - SelectionGoal::None, - ), - MiddleOfLine { display_lines } => ( - middle_of_line(map, *display_lines, point, maybe_times), - SelectionGoal::None, - ), - EndOfLine { display_lines } => ( - end_of_line(map, *display_lines, point, times), - SelectionGoal::None, - ), - SentenceBackward => (sentence_backwards(map, point, times), SelectionGoal::None), - SentenceForward => (sentence_forwards(map, point, times), SelectionGoal::None), - StartOfParagraph => ( - movement::start_of_paragraph(map, point, times), - SelectionGoal::None, - ), - EndOfParagraph => ( - map.clip_at_line_end(movement::end_of_paragraph(map, point, times)), - SelectionGoal::None, - ), - CurrentLine => (next_line_end(map, point, times), SelectionGoal::None), - StartOfDocument => ( - start_of_document(map, point, maybe_times), - SelectionGoal::None, - ), - EndOfDocument => ( - end_of_document(map, point, maybe_times), - SelectionGoal::None, - ), - Matching => (matching(map, point), SelectionGoal::None), - GoToPercentage => (go_to_percentage(map, point, times), SelectionGoal::None), - UnmatchedForward { char } => ( - unmatched_forward(map, point, *char, times), - SelectionGoal::None, - ), - UnmatchedBackward { char } => ( - unmatched_backward(map, point, *char, times), - SelectionGoal::None, - ), - // t f - FindForward { - before, - char, - mode, - smartcase, - } => { - return find_forward(map, point, *before, *char, times, *mode, *smartcase) - .map(|new_point| (new_point, SelectionGoal::None)); - } - // T F - FindBackward { - after, - char, - mode, - smartcase, - } => ( - find_backward(map, point, *after, *char, times, *mode, *smartcase), - SelectionGoal::None, - ), - Sneak { - first_char, - second_char, - smartcase, - } => { - return sneak(map, point, *first_char, *second_char, times, *smartcase) - .map(|new_point| (new_point, SelectionGoal::None)); - } - SneakBackward { - first_char, - second_char, - smartcase, - } => { - return sneak_backward(map, point, *first_char, *second_char, times, *smartcase) - .map(|new_point| (new_point, SelectionGoal::None)); - } - // ; -- repeat the last find done with t, f, T, F - RepeatFind { last_find } => match **last_find { - Motion::FindForward { - before, - char, - mode, - smartcase, - } => { - let mut new_point = - find_forward(map, point, before, char, times, mode, smartcase); - if new_point == Some(point) { - new_point = - find_forward(map, point, before, char, times + 1, mode, smartcase); - } - - return new_point.map(|new_point| (new_point, SelectionGoal::None)); - } - - Motion::FindBackward { - after, - char, - mode, - smartcase, - } => { - let mut new_point = - find_backward(map, point, after, char, times, mode, smartcase); - if new_point == point { - new_point = - find_backward(map, point, after, char, times + 1, mode, smartcase); - } - - (new_point, SelectionGoal::None) - } - Motion::Sneak { - first_char, - second_char, - smartcase, - } => { - let mut new_point = - sneak(map, point, first_char, second_char, times, smartcase); - if new_point == Some(point) { - new_point = - sneak(map, point, first_char, second_char, times + 1, smartcase); - } - - return new_point.map(|new_point| (new_point, SelectionGoal::None)); - } - - Motion::SneakBackward { - first_char, - second_char, - smartcase, - } => { - let mut new_point = - sneak_backward(map, point, first_char, second_char, times, smartcase); - if new_point == Some(point) { - new_point = sneak_backward( - map, - point, - first_char, - second_char, - times + 1, - smartcase, - ); - } - - return new_point.map(|new_point| (new_point, SelectionGoal::None)); - } - _ => return None, - }, - // , -- repeat the last find done with t, f, T, F, s, S, in opposite direction - RepeatFindReversed { last_find } => match **last_find { - Motion::FindForward { - before, - char, - mode, - smartcase, - } => { - let mut new_point = - find_backward(map, point, before, char, times, mode, smartcase); - if new_point == point { - new_point = - find_backward(map, point, before, char, times + 1, mode, smartcase); - } - - (new_point, SelectionGoal::None) - } - - Motion::FindBackward { - after, - char, - mode, - smartcase, - } => { - let mut new_point = - find_forward(map, point, after, char, times, mode, smartcase); - if new_point == Some(point) { - new_point = - find_forward(map, point, after, char, times + 1, mode, smartcase); - } - - return new_point.map(|new_point| (new_point, SelectionGoal::None)); - } - - Motion::Sneak { - first_char, - second_char, - smartcase, - } => { - let mut new_point = - sneak_backward(map, point, first_char, second_char, times, smartcase); - if new_point == Some(point) { - new_point = sneak_backward( - map, - point, - first_char, - second_char, - times + 1, - smartcase, - ); - } - - return new_point.map(|new_point| (new_point, SelectionGoal::None)); - } - - Motion::SneakBackward { - first_char, - second_char, - smartcase, - } => { - let mut new_point = - sneak(map, point, first_char, second_char, times, smartcase); - if new_point == Some(point) { - new_point = - sneak(map, point, first_char, second_char, times + 1, smartcase); - } - - return new_point.map(|new_point| (new_point, SelectionGoal::None)); - } - _ => return None, - }, - NextLineStart => (next_line_start(map, point, times), SelectionGoal::None), - PreviousLineStart => (previous_line_start(map, point, times), SelectionGoal::None), - StartOfLineDownward => (next_line_start(map, point, times - 1), SelectionGoal::None), - EndOfLineDownward => (last_non_whitespace(map, point, times), SelectionGoal::None), - GoToColumn => (go_to_column(map, point, times), SelectionGoal::None), - WindowTop => window_top(map, point, text_layout_details, times - 1), - WindowMiddle => window_middle(map, point, text_layout_details), - WindowBottom => window_bottom(map, point, text_layout_details, times - 1), - Jump { line, anchor } => mark::jump_motion(map, *anchor, *line), - ZedSearchResult { new_selections, .. } => { - // There will be only one selection, as - // Search::SelectNextMatch selects a single match. - if let Some(new_selection) = new_selections.first() { - ( - new_selection.start.to_display_point(map), - SelectionGoal::None, - ) - } else { - return None; - } - } - NextSectionStart => ( - section_motion(map, point, times, Direction::Next, true), - SelectionGoal::None, - ), - NextSectionEnd => ( - section_motion(map, point, times, Direction::Next, false), - SelectionGoal::None, - ), - PreviousSectionStart => ( - section_motion(map, point, times, Direction::Prev, true), - SelectionGoal::None, - ), - PreviousSectionEnd => ( - section_motion(map, point, times, Direction::Prev, false), - SelectionGoal::None, - ), - - NextMethodStart => ( - method_motion(map, point, times, Direction::Next, true), - SelectionGoal::None, - ), - NextMethodEnd => ( - method_motion(map, point, times, Direction::Next, false), - SelectionGoal::None, - ), - PreviousMethodStart => ( - method_motion(map, point, times, Direction::Prev, true), - SelectionGoal::None, - ), - PreviousMethodEnd => ( - method_motion(map, point, times, Direction::Prev, false), - SelectionGoal::None, - ), - NextComment => ( - comment_motion(map, point, times, Direction::Next), - SelectionGoal::None, - ), - PreviousComment => ( - comment_motion(map, point, times, Direction::Prev), - SelectionGoal::None, - ), - PreviousLesserIndent => ( - indent_motion(map, point, times, Direction::Prev, IndentType::Lesser), - SelectionGoal::None, - ), - PreviousGreaterIndent => ( - indent_motion(map, point, times, Direction::Prev, IndentType::Greater), - SelectionGoal::None, - ), - PreviousSameIndent => ( - indent_motion(map, point, times, Direction::Prev, IndentType::Same), - SelectionGoal::None, - ), - NextLesserIndent => ( - indent_motion(map, point, times, Direction::Next, IndentType::Lesser), - SelectionGoal::None, - ), - NextGreaterIndent => ( - indent_motion(map, point, times, Direction::Next, IndentType::Greater), - SelectionGoal::None, - ), - NextSameIndent => ( - indent_motion(map, point, times, Direction::Next, IndentType::Same), - SelectionGoal::None, - ), - }; - (new_point != point || infallible).then_some((new_point, goal)) - } - - // Get the range value after self is applied to the specified selection. - pub fn range( - &self, - map: &DisplaySnapshot, - mut selection: Selection, - times: Option, - text_layout_details: &TextLayoutDetails, - forced_motion: bool, - ) -> Option<(Range, MotionKind)> { - if let Motion::ZedSearchResult { - prior_selections, - new_selections, - } = self - { - if let Some((prior_selection, new_selection)) = - prior_selections.first().zip(new_selections.first()) - { - let start = prior_selection - .start - .to_display_point(map) - .min(new_selection.start.to_display_point(map)); - let end = new_selection - .end - .to_display_point(map) - .max(prior_selection.end.to_display_point(map)); - - if start < end { - return Some((start..end, MotionKind::Exclusive)); - } else { - return Some((end..start, MotionKind::Exclusive)); - } - } else { - return None; - } - } - let maybe_new_point = self.move_point( - map, - selection.head(), - selection.goal, - times, - text_layout_details, - ); - - let (new_head, goal) = match (maybe_new_point, forced_motion) { - (Some((p, g)), _) => Some((p, g)), - (None, false) => None, - (None, true) => Some((selection.head(), selection.goal)), - }?; - - selection.set_head(new_head, goal); - - let mut kind = match (self.default_kind(), forced_motion) { - (MotionKind::Linewise, true) => MotionKind::Exclusive, - (MotionKind::Exclusive, true) => MotionKind::Inclusive, - (MotionKind::Inclusive, true) => MotionKind::Exclusive, - (kind, false) => kind, - }; - - if let Motion::NextWordStart { - ignore_punctuation: _, - } = self - { - // Another special case: When using the "w" motion in combination with an - // operator and the last word moved over is at the end of a line, the end of - // that word becomes the end of the operated text, not the first word in the - // next line. - let start = selection.start.to_point(map); - let end = selection.end.to_point(map); - let start_row = MultiBufferRow(selection.start.to_point(map).row); - if end.row > start.row { - selection.end = Point::new(start_row.0, map.buffer_snapshot().line_len(start_row)) - .to_display_point(map); - - // a bit of a hack, we need `cw` on a blank line to not delete the newline, - // but dw on a blank line should. The `Linewise` returned from this method - // causes the `d` operator to include the trailing newline. - if selection.start == selection.end { - return Some((selection.start..selection.end, MotionKind::Linewise)); - } - } - } else if kind == MotionKind::Exclusive && !self.skip_exclusive_special_case() { - let start_point = selection.start.to_point(map); - let mut end_point = selection.end.to_point(map); - let mut next_point = selection.end; - *next_point.column_mut() += 1; - next_point = map.clip_point(next_point, Bias::Right); - if next_point.to_point(map) == end_point && forced_motion { - selection.end = movement::saturating_left(map, selection.end); - } - - if end_point.row > start_point.row { - let first_non_blank_of_start_row = map - .line_indent_for_buffer_row(MultiBufferRow(start_point.row)) - .raw_len(); - // https://github.com/neovim/neovim/blob/ee143aaf65a0e662c42c636aa4a959682858b3e7/src/nvim/ops.c#L6178-L6203 - if end_point.column == 0 { - // If the motion is exclusive and the end of the motion is in column 1, the - // end of the motion is moved to the end of the previous line and the motion - // becomes inclusive. Example: "}" moves to the first line after a paragraph, - // but "d}" will not include that line. - // - // If the motion is exclusive, the end of the motion is in column 1 and the - // start of the motion was at or before the first non-blank in the line, the - // motion becomes linewise. Example: If a paragraph begins with some blanks - // and you do "d}" while standing on the first non-blank, all the lines of - // the paragraph are deleted, including the blanks. - if start_point.column <= first_non_blank_of_start_row { - kind = MotionKind::Linewise; - } else { - kind = MotionKind::Inclusive; - } - end_point.row -= 1; - end_point.column = 0; - selection.end = map.clip_point(map.next_line_boundary(end_point).1, Bias::Left); - } else if let Motion::EndOfParagraph = self { - // Special case: When using the "}" motion, it's possible - // that there's no blank lines after the paragraph the - // cursor is currently on. - // In this situation the `end_point.column` value will be - // greater than 0, so the selection doesn't actually end on - // the first character of a blank line. In that case, we'll - // want to move one column to the right, to actually include - // all characters of the last non-blank line. - selection.end = movement::saturating_right(map, selection.end) - } - } - } else if kind == MotionKind::Inclusive { - selection.end = movement::saturating_right(map, selection.end) - } - - if kind == MotionKind::Linewise { - selection.start = map.prev_line_boundary(selection.start.to_point(map)).1; - selection.end = map.next_line_boundary(selection.end.to_point(map)).1; - } - Some((selection.start..selection.end, kind)) - } - - // Expands a selection using self for an operator - pub fn expand_selection( - &self, - map: &DisplaySnapshot, - selection: &mut Selection, - times: Option, - text_layout_details: &TextLayoutDetails, - forced_motion: bool, - ) -> Option { - let (range, kind) = self.range( - map, - selection.clone(), - times, - text_layout_details, - forced_motion, - )?; - selection.start = range.start; - selection.end = range.end; - Some(kind) - } -} - -fn left(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint { - for _ in 0..times { - point = movement::saturating_left(map, point); - if point.column() == 0 { - break; - } - } - point -} - -pub(crate) fn wrapping_left( - map: &DisplaySnapshot, - mut point: DisplayPoint, - times: usize, -) -> DisplayPoint { - for _ in 0..times { - point = movement::left(map, point); - if point.is_zero() { - break; - } - } - point -} - -fn wrapping_right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint { - for _ in 0..times { - point = wrapping_right_single(map, point); - if point == map.max_point() { - break; - } - } - point -} - -fn wrapping_right_single(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { - let mut next_point = point; - *next_point.column_mut() += 1; - next_point = map.clip_point(next_point, Bias::Right); - if next_point == point { - if next_point.row() == map.max_point().row() { - next_point - } else { - DisplayPoint::new(next_point.row().next_row(), 0) - } - } else { - next_point - } -} - -fn up_down_buffer_rows( - map: &DisplaySnapshot, - mut point: DisplayPoint, - mut goal: SelectionGoal, - mut times: isize, - text_layout_details: &TextLayoutDetails, -) -> (DisplayPoint, SelectionGoal) { - let bias = if times < 0 { Bias::Left } else { Bias::Right }; - - while map.is_folded_buffer_header(point.row()) { - if times < 0 { - (point, _) = movement::up(map, point, goal, true, text_layout_details); - times += 1; - } else if times > 0 { - (point, _) = movement::down(map, point, goal, true, text_layout_details); - times -= 1; - } else { - break; - } - } - - let start = map.display_point_to_fold_point(point, Bias::Left); - let begin_folded_line = map.fold_point_to_display_point( - map.fold_snapshot() - .clip_point(FoldPoint::new(start.row(), 0), Bias::Left), - ); - let select_nth_wrapped_row = point.row().0 - begin_folded_line.row().0; - - let (goal_wrap, goal_x) = match goal { - SelectionGoal::WrappedHorizontalPosition((row, x)) => (row, x), - SelectionGoal::HorizontalRange { end, .. } => (select_nth_wrapped_row, end as f32), - SelectionGoal::HorizontalPosition(x) => (select_nth_wrapped_row, x as f32), - _ => { - let x = map.x_for_display_point(point, text_layout_details); - goal = SelectionGoal::WrappedHorizontalPosition((select_nth_wrapped_row, x.into())); - (select_nth_wrapped_row, x.into()) - } - }; - - let target = start.row() as isize + times; - let new_row = (target.max(0) as u32).min(map.fold_snapshot().max_point().row()); - - let mut begin_folded_line = map.fold_point_to_display_point( - map.fold_snapshot() - .clip_point(FoldPoint::new(new_row, 0), bias), - ); - - let mut i = 0; - while i < goal_wrap && begin_folded_line.row() < map.max_point().row() { - let next_folded_line = DisplayPoint::new(begin_folded_line.row().next_row(), 0); - if map - .display_point_to_fold_point(next_folded_line, bias) - .row() - == new_row - { - i += 1; - begin_folded_line = next_folded_line; - } else { - break; - } - } - - let new_col = if i == goal_wrap { - map.display_column_for_x(begin_folded_line.row(), px(goal_x), text_layout_details) - } else { - 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) -} - -fn down_display( - map: &DisplaySnapshot, - mut point: DisplayPoint, - mut goal: SelectionGoal, - times: usize, - text_layout_details: &TextLayoutDetails, -) -> (DisplayPoint, SelectionGoal) { - for _ in 0..times { - (point, goal) = movement::down(map, point, goal, true, text_layout_details); - } - - (point, goal) -} - -fn up_display( - map: &DisplaySnapshot, - mut point: DisplayPoint, - mut goal: SelectionGoal, - times: usize, - text_layout_details: &TextLayoutDetails, -) -> (DisplayPoint, SelectionGoal) { - for _ in 0..times { - (point, goal) = movement::up(map, point, goal, true, text_layout_details); - } - - (point, goal) -} - -pub(crate) fn right(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> DisplayPoint { - for _ in 0..times { - let new_point = movement::saturating_right(map, point); - if point == new_point { - break; - } - point = new_point; - } - point -} - -pub(crate) fn next_char( - map: &DisplaySnapshot, - point: DisplayPoint, - allow_cross_newline: bool, -) -> DisplayPoint { - let mut new_point = point; - let mut max_column = map.line_len(new_point.row()); - if !allow_cross_newline { - max_column -= 1; - } - if new_point.column() < max_column { - *new_point.column_mut() += 1; - } else if new_point < map.max_point() && allow_cross_newline { - *new_point.row_mut() += 1; - *new_point.column_mut() = 0; - } - map.clip_ignoring_line_ends(new_point, Bias::Right) -} - -pub(crate) fn next_word_start( - map: &DisplaySnapshot, - mut point: DisplayPoint, - ignore_punctuation: bool, - times: usize, -) -> DisplayPoint { - let classifier = map - .buffer_snapshot() - .char_classifier_at(point.to_point(map)) - .ignore_punctuation(ignore_punctuation); - for _ in 0..times { - let mut crossed_newline = false; - let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| { - let left_kind = classifier.kind(left); - let right_kind = classifier.kind(right); - let at_newline = right == '\n'; - - let found = (left_kind != right_kind && right_kind != CharKind::Whitespace) - || at_newline && crossed_newline - || at_newline && left == '\n'; // Prevents skipping repeated empty lines - - crossed_newline |= at_newline; - found - }); - if point == new_point { - break; - } - point = new_point; - } - point -} - -pub(crate) fn next_word_end( - map: &DisplaySnapshot, - mut point: DisplayPoint, - ignore_punctuation: bool, - times: usize, - allow_cross_newline: bool, - always_advance: bool, -) -> DisplayPoint { - let classifier = map - .buffer_snapshot() - .char_classifier_at(point.to_point(map)) - .ignore_punctuation(ignore_punctuation); - for _ in 0..times { - let mut need_next_char = false; - let new_point = if always_advance { - next_char(map, point, allow_cross_newline) - } else { - point - }; - let new_point = movement::find_boundary_exclusive( - map, - new_point, - FindRange::MultiLine, - |left, right| { - let left_kind = classifier.kind(left); - let right_kind = classifier.kind(right); - let at_newline = right == '\n'; - - if !allow_cross_newline && at_newline { - need_next_char = true; - return true; - } - - left_kind != right_kind && left_kind != CharKind::Whitespace - }, - ); - let new_point = if need_next_char { - next_char(map, new_point, true) - } else { - new_point - }; - let new_point = map.clip_point(new_point, Bias::Left); - if point == new_point { - break; - } - point = new_point; - } - point -} - -fn previous_word_start( - map: &DisplaySnapshot, - mut point: DisplayPoint, - ignore_punctuation: bool, - times: usize, -) -> DisplayPoint { - let classifier = map - .buffer_snapshot() - .char_classifier_at(point.to_point(map)) - .ignore_punctuation(ignore_punctuation); - for _ in 0..times { - // This works even though find_preceding_boundary is called for every character in the line containing - // cursor because the newline is checked only once. - let new_point = movement::find_preceding_boundary_display_point( - map, - point, - FindRange::MultiLine, - |left, right| { - let left_kind = classifier.kind(left); - let right_kind = classifier.kind(right); - - (left_kind != right_kind && !right.is_whitespace()) || left == '\n' - }, - ); - if point == new_point { - break; - } - point = new_point; - } - point -} - -fn previous_word_end( - map: &DisplaySnapshot, - point: DisplayPoint, - ignore_punctuation: bool, - times: usize, -) -> DisplayPoint { - let classifier = map - .buffer_snapshot() - .char_classifier_at(point.to_point(map)) - .ignore_punctuation(ignore_punctuation); - let mut point = point.to_point(map); - - if point.column < map.buffer_snapshot().line_len(MultiBufferRow(point.row)) - && let Some(ch) = map.buffer_snapshot().chars_at(point).next() - { - point.column += ch.len_utf8() as u32; - } - for _ in 0..times { - let new_point = movement::find_preceding_boundary_point( - &map.buffer_snapshot(), - point, - FindRange::MultiLine, - |left, right| { - let left_kind = classifier.kind(left); - let right_kind = classifier.kind(right); - match (left_kind, right_kind) { - (CharKind::Punctuation, CharKind::Whitespace) - | (CharKind::Punctuation, CharKind::Word) - | (CharKind::Word, CharKind::Whitespace) - | (CharKind::Word, CharKind::Punctuation) => true, - (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n', - _ => false, - } - }, - ); - if new_point == point { - break; - } - point = new_point; - } - movement::saturating_left(map, point.to_display_point(map)) -} - -fn next_subword_start( - map: &DisplaySnapshot, - mut point: DisplayPoint, - ignore_punctuation: bool, - times: usize, -) -> DisplayPoint { - let classifier = map - .buffer_snapshot() - .char_classifier_at(point.to_point(map)) - .ignore_punctuation(ignore_punctuation); - for _ in 0..times { - let mut crossed_newline = false; - let new_point = movement::find_boundary(map, point, FindRange::MultiLine, |left, right| { - let left_kind = classifier.kind(left); - let right_kind = classifier.kind(right); - let at_newline = right == '\n'; - - let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric(); - let is_subword_start = - left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase(); - - let found = (!right.is_whitespace() && (is_word_start || is_subword_start)) - || at_newline && crossed_newline - || at_newline && left == '\n'; // Prevents skipping repeated empty lines - - crossed_newline |= at_newline; - found - }); - if point == new_point { - break; - } - point = new_point; - } - point -} - -pub(crate) fn next_subword_end( - map: &DisplaySnapshot, - mut point: DisplayPoint, - ignore_punctuation: bool, - times: usize, - allow_cross_newline: bool, -) -> DisplayPoint { - let classifier = map - .buffer_snapshot() - .char_classifier_at(point.to_point(map)) - .ignore_punctuation(ignore_punctuation); - for _ in 0..times { - let new_point = next_char(map, point, allow_cross_newline); - - let mut crossed_newline = false; - let mut need_backtrack = false; - let new_point = - movement::find_boundary(map, new_point, FindRange::MultiLine, |left, right| { - let left_kind = classifier.kind(left); - let right_kind = classifier.kind(right); - let at_newline = right == '\n'; - - if !allow_cross_newline && at_newline { - return true; - } - - let is_word_end = (left_kind != right_kind) && !right.is_alphanumeric(); - let is_subword_end = - left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase(); - - let found = !left.is_whitespace() && !at_newline && (is_word_end || is_subword_end); - - if found && (is_word_end || is_subword_end) { - need_backtrack = true; - } - - crossed_newline |= at_newline; - found - }); - let mut new_point = map.clip_point(new_point, Bias::Left); - if need_backtrack { - *new_point.column_mut() -= 1; - } - let new_point = map.clip_point(new_point, Bias::Left); - if point == new_point { - break; - } - point = new_point; - } - point -} - -fn previous_subword_start( - map: &DisplaySnapshot, - mut point: DisplayPoint, - ignore_punctuation: bool, - times: usize, -) -> DisplayPoint { - let classifier = map - .buffer_snapshot() - .char_classifier_at(point.to_point(map)) - .ignore_punctuation(ignore_punctuation); - for _ in 0..times { - let mut crossed_newline = false; - // This works even though find_preceding_boundary is called for every character in the line containing - // cursor because the newline is checked only once. - let new_point = movement::find_preceding_boundary_display_point( - map, - point, - FindRange::MultiLine, - |left, right| { - let left_kind = classifier.kind(left); - let right_kind = classifier.kind(right); - let at_newline = right == '\n'; - - let is_word_start = (left_kind != right_kind) && !left.is_alphanumeric(); - let is_subword_start = - left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase(); - - let found = (!right.is_whitespace() && (is_word_start || is_subword_start)) - || at_newline && crossed_newline - || at_newline && left == '\n'; // Prevents skipping repeated empty lines - - crossed_newline |= at_newline; - - found - }, - ); - if point == new_point { - break; - } - point = new_point; - } - point -} - -fn previous_subword_end( - map: &DisplaySnapshot, - point: DisplayPoint, - ignore_punctuation: bool, - times: usize, -) -> DisplayPoint { - let classifier = map - .buffer_snapshot() - .char_classifier_at(point.to_point(map)) - .ignore_punctuation(ignore_punctuation); - let mut point = point.to_point(map); - - if point.column < map.buffer_snapshot().line_len(MultiBufferRow(point.row)) - && let Some(ch) = map.buffer_snapshot().chars_at(point).next() - { - point.column += ch.len_utf8() as u32; - } - for _ in 0..times { - let new_point = movement::find_preceding_boundary_point( - &map.buffer_snapshot(), - point, - FindRange::MultiLine, - |left, right| { - let left_kind = classifier.kind(left); - let right_kind = classifier.kind(right); - - let is_subword_end = - left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase(); - - if is_subword_end { - return true; - } - - match (left_kind, right_kind) { - (CharKind::Word, CharKind::Whitespace) - | (CharKind::Word, CharKind::Punctuation) => true, - (CharKind::Whitespace, CharKind::Whitespace) => left == '\n' && right == '\n', - _ => false, - } - }, - ); - if new_point == point { - break; - } - point = new_point; - } - movement::saturating_left(map, point.to_display_point(map)) -} - -pub(crate) fn first_non_whitespace( - map: &DisplaySnapshot, - display_lines: bool, - from: DisplayPoint, -) -> DisplayPoint { - let mut start_offset = start_of_line(map, display_lines, from).to_offset(map, Bias::Left); - let classifier = map.buffer_snapshot().char_classifier_at(from.to_point(map)); - for (ch, offset) in map.buffer_chars_at(start_offset) { - if ch == '\n' { - return from; - } - - start_offset = offset; - - if classifier.kind(ch) != CharKind::Whitespace { - break; - } - } - - start_offset.to_display_point(map) -} - -pub(crate) fn last_non_whitespace( - map: &DisplaySnapshot, - from: DisplayPoint, - count: usize, -) -> DisplayPoint { - let mut end_of_line = end_of_line(map, false, from, count).to_offset(map, Bias::Left); - let classifier = map.buffer_snapshot().char_classifier_at(from.to_point(map)); - - // NOTE: depending on clip_at_line_end we may already be one char back from the end. - if let Some((ch, _)) = map.buffer_chars_at(end_of_line).next() - && classifier.kind(ch) != CharKind::Whitespace - { - return end_of_line.to_display_point(map); - } - - for (ch, offset) in map.reverse_buffer_chars_at(end_of_line) { - if ch == '\n' { - break; - } - end_of_line = offset; - if classifier.kind(ch) != CharKind::Whitespace || ch == '\n' { - break; - } - } - - end_of_line.to_display_point(map) -} - -pub(crate) fn start_of_line( - map: &DisplaySnapshot, - display_lines: bool, - point: DisplayPoint, -) -> DisplayPoint { - if display_lines { - map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right) - } else { - map.prev_line_boundary(point.to_point(map)).1 - } -} - -pub(crate) fn middle_of_line( - map: &DisplaySnapshot, - display_lines: bool, - point: DisplayPoint, - times: Option, -) -> DisplayPoint { - let percent = if let Some(times) = times.filter(|&t| t <= 100) { - times as f64 / 100. - } else { - 0.5 - }; - if display_lines { - map.clip_point( - DisplayPoint::new( - point.row(), - (map.line_len(point.row()) as f64 * percent) as u32, - ), - Bias::Left, - ) - } else { - let mut buffer_point = point.to_point(map); - buffer_point.column = (map - .buffer_snapshot() - .line_len(MultiBufferRow(buffer_point.row)) as f64 - * percent) as u32; - - map.clip_point(buffer_point.to_display_point(map), Bias::Left) - } -} - -pub(crate) fn end_of_line( - map: &DisplaySnapshot, - display_lines: bool, - mut point: DisplayPoint, - times: usize, -) -> DisplayPoint { - if times > 1 { - point = map.start_of_relative_buffer_row(point, times as isize - 1); - } - if display_lines { - map.clip_point( - DisplayPoint::new(point.row(), map.line_len(point.row())), - Bias::Left, - ) - } else { - map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left) - } -} - -pub(crate) fn sentence_backwards( - map: &DisplaySnapshot, - point: DisplayPoint, - mut times: usize, -) -> DisplayPoint { - let mut start = point.to_point(map).to_offset(&map.buffer_snapshot()); - let mut chars = map.reverse_buffer_chars_at(start).peekable(); - - let mut was_newline = map - .buffer_chars_at(start) - .next() - .is_some_and(|(c, _)| c == '\n'); - - while let Some((ch, offset)) = chars.next() { - let start_of_next_sentence = if was_newline && ch == '\n' { - Some(offset + ch.len_utf8()) - } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') { - Some(next_non_blank(map, offset + ch.len_utf8())) - } else if ch == '.' || ch == '?' || ch == '!' { - start_of_next_sentence(map, offset + ch.len_utf8()) - } else { - None - }; - - if let Some(start_of_next_sentence) = start_of_next_sentence { - if start_of_next_sentence < start { - times = times.saturating_sub(1); - } - if times == 0 || offset.0 == 0 { - return map.clip_point( - start_of_next_sentence - .to_offset(&map.buffer_snapshot()) - .to_display_point(map), - Bias::Left, - ); - } - } - if was_newline { - start = offset; - } - was_newline = ch == '\n'; - } - - DisplayPoint::zero() -} - -pub(crate) fn sentence_forwards( - map: &DisplaySnapshot, - point: DisplayPoint, - mut times: usize, -) -> DisplayPoint { - let start = point.to_point(map).to_offset(&map.buffer_snapshot()); - let mut chars = map.buffer_chars_at(start).peekable(); - - let mut was_newline = map - .reverse_buffer_chars_at(start) - .next() - .is_some_and(|(c, _)| c == '\n') - && chars.peek().is_some_and(|(c, _)| *c == '\n'); - - while let Some((ch, offset)) = chars.next() { - if was_newline && ch == '\n' { - continue; - } - let start_of_next_sentence = if was_newline { - Some(next_non_blank(map, offset)) - } else if ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n') { - Some(next_non_blank(map, offset + ch.len_utf8())) - } else if ch == '.' || ch == '?' || ch == '!' { - start_of_next_sentence(map, offset + ch.len_utf8()) - } else { - None - }; - - if let Some(start_of_next_sentence) = start_of_next_sentence { - times = times.saturating_sub(1); - if times == 0 { - return map.clip_point( - start_of_next_sentence - .to_offset(&map.buffer_snapshot()) - .to_display_point(map), - Bias::Right, - ); - } - } - - was_newline = ch == '\n' && chars.peek().is_some_and(|(c, _)| *c == '\n'); - } - - map.max_point() -} - -fn next_non_blank(map: &DisplaySnapshot, start: MultiBufferOffset) -> MultiBufferOffset { - for (c, o) in map.buffer_chars_at(start) { - if c == '\n' || !c.is_whitespace() { - return o; - } - } - - map.buffer_snapshot().len() -} - -// given the offset after a ., !, or ? find the start of the next sentence. -// if this is not a sentence boundary, returns None. -fn start_of_next_sentence( - map: &DisplaySnapshot, - end_of_sentence: MultiBufferOffset, -) -> Option { - let chars = map.buffer_chars_at(end_of_sentence); - let mut seen_space = false; - - for (char, offset) in chars { - if !seen_space && (char == ')' || char == ']' || char == '"' || char == '\'') { - continue; - } - - if char == '\n' && seen_space { - return Some(offset); - } else if char.is_whitespace() { - seen_space = true; - } else if seen_space { - return Some(offset); - } else { - return None; - } - } - - Some(map.buffer_snapshot().len()) -} - -fn go_to_line(map: &DisplaySnapshot, display_point: DisplayPoint, line: usize) -> DisplayPoint { - let point = map.display_point_to_point(display_point, Bias::Left); - let Some(mut excerpt) = map.buffer_snapshot().excerpt_containing(point..point) else { - return display_point; - }; - let offset = excerpt.buffer().point_to_offset( - excerpt - .buffer() - .clip_point(Point::new((line - 1) as u32, point.column), Bias::Left), - ); - let buffer_range = excerpt.buffer_range(); - if offset >= buffer_range.start.0 && offset <= buffer_range.end.0 { - let point = map - .buffer_snapshot() - .offset_to_point(excerpt.map_offset_from_buffer(BufferOffset(offset))); - return map.clip_point(map.point_to_display_point(point, Bias::Left), Bias::Left); - } - let mut last_position = None; - for (excerpt, buffer, range) in map.buffer_snapshot().excerpts() { - let excerpt_range = language::ToOffset::to_offset(&range.context.start, buffer) - ..language::ToOffset::to_offset(&range.context.end, buffer); - if offset >= excerpt_range.start && offset <= excerpt_range.end { - let text_anchor = buffer.anchor_after(offset); - let anchor = Anchor::in_buffer(excerpt, text_anchor); - return anchor.to_display_point(map); - } else if offset <= excerpt_range.start { - let anchor = Anchor::in_buffer(excerpt, range.context.start); - return anchor.to_display_point(map); - } else { - last_position = Some(Anchor::in_buffer(excerpt, range.context.end)); - } - } - - let mut last_point = last_position.unwrap().to_point(&map.buffer_snapshot()); - last_point.column = point.column; - - map.clip_point( - map.point_to_display_point( - map.buffer_snapshot().clip_point(point, Bias::Left), - Bias::Left, - ), - Bias::Left, - ) -} - -fn start_of_document( - map: &DisplaySnapshot, - display_point: DisplayPoint, - maybe_times: Option, -) -> DisplayPoint { - if let Some(times) = maybe_times { - return go_to_line(map, display_point, times); - } - - let point = map.display_point_to_point(display_point, Bias::Left); - let mut first_point = Point::zero(); - first_point.column = point.column; - - map.clip_point( - map.point_to_display_point( - map.buffer_snapshot().clip_point(first_point, Bias::Left), - Bias::Left, - ), - Bias::Left, - ) -} - -fn end_of_document( - map: &DisplaySnapshot, - display_point: DisplayPoint, - maybe_times: Option, -) -> DisplayPoint { - if let Some(times) = maybe_times { - return go_to_line(map, display_point, times); - }; - let point = map.display_point_to_point(display_point, Bias::Left); - let mut last_point = map.buffer_snapshot().max_point(); - last_point.column = point.column; - - map.clip_point( - map.point_to_display_point( - map.buffer_snapshot().clip_point(last_point, Bias::Left), - Bias::Left, - ), - Bias::Left, - ) -} - -fn matching_tag(map: &DisplaySnapshot, head: DisplayPoint) -> Option { - let inner = crate::object::surrounding_html_tag(map, head, head..head, false)?; - let outer = crate::object::surrounding_html_tag(map, head, head..head, true)?; - - if head > outer.start && head < inner.start { - let mut offset = inner.end.to_offset(map, Bias::Left); - for c in map.buffer_snapshot().chars_at(offset) { - if c == '/' || c == '\n' || c == '>' { - return Some(offset.to_display_point(map)); - } - offset += c.len_utf8(); - } - } else { - let mut offset = outer.start.to_offset(map, Bias::Left); - for c in map.buffer_snapshot().chars_at(offset) { - offset += c.len_utf8(); - if c == '<' || c == '\n' { - return Some(offset.to_display_point(map)); - } - } - } - - None -} - -fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint { - if !map.is_singleton() { - return display_point; - } - // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1200 - let display_point = map.clip_at_line_end(display_point); - let point = display_point.to_point(map); - let offset = point.to_offset(&map.buffer_snapshot()); - let snapshot = map.buffer_snapshot(); - - // Ensure the range is contained by the current line. - let mut line_end = map.next_line_boundary(point).0; - if line_end == point { - line_end = map.max_point().to_point(map); - } - - // Attempt to find the smallest enclosing bracket range that also contains - // the offset, which only happens if the cursor is currently in a bracket. - let range_filter = |_buffer: &language::BufferSnapshot, - opening_range: Range, - closing_range: Range| { - opening_range.contains(&BufferOffset(offset.0)) - || closing_range.contains(&BufferOffset(offset.0)) - }; - - let bracket_ranges = snapshot - .innermost_enclosing_bracket_ranges(offset..offset, Some(&range_filter)) - .or_else(|| snapshot.innermost_enclosing_bracket_ranges(offset..offset, None)); - - if let Some((opening_range, closing_range)) = bracket_ranges { - let mut chars = map.buffer_snapshot().chars_at(offset); - match chars.next() { - Some('/') => {} - _ => { - if opening_range.contains(&offset) { - return closing_range.start.to_display_point(map); - } else if closing_range.contains(&offset) { - return opening_range.start.to_display_point(map); - } - } - } - } - - let line_range = map.prev_line_boundary(point).0..line_end; - let visible_line_range = - line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1)); - let ranges = map.buffer_snapshot().bracket_ranges(visible_line_range); - if let Some(ranges) = ranges { - let line_range = line_range.start.to_offset(&map.buffer_snapshot()) - ..line_range.end.to_offset(&map.buffer_snapshot()); - let mut closest_pair_destination = None; - let mut closest_distance = usize::MAX; - - for (open_range, close_range) in ranges { - if map.buffer_snapshot().chars_at(open_range.start).next() == Some('<') { - if offset > open_range.start && offset < close_range.start { - let mut chars = map.buffer_snapshot().chars_at(close_range.start); - if (Some('/'), Some('>')) == (chars.next(), chars.next()) { - return display_point; - } - if let Some(tag) = matching_tag(map, display_point) { - return tag; - } - } else if close_range.contains(&offset) { - return open_range.start.to_display_point(map); - } else if open_range.contains(&offset) { - return (close_range.end - 1).to_display_point(map); - } - } - - if (open_range.contains(&offset) || open_range.start >= offset) - && line_range.contains(&open_range.start) - { - let distance = open_range.start.saturating_sub(offset); - if distance < closest_distance { - closest_pair_destination = Some(close_range.start); - closest_distance = distance; - } - } - - if (close_range.contains(&offset) || close_range.start >= offset) - && line_range.contains(&close_range.start) - { - let distance = close_range.start.saturating_sub(offset); - if distance < closest_distance { - closest_pair_destination = Some(open_range.start); - closest_distance = distance; - } - } - - continue; - } - - closest_pair_destination - .map(|destination| destination.to_display_point(map)) - .unwrap_or(display_point) - } else { - display_point - } -} - -// Go to {count} percentage in the file, on the first -// non-blank in the line linewise. To compute the new -// line number this formula is used: -// ({count} * number-of-lines + 99) / 100 -// -// https://neovim.io/doc/user/motion.html#N%25 -fn go_to_percentage(map: &DisplaySnapshot, point: DisplayPoint, count: usize) -> DisplayPoint { - let total_lines = map.buffer_snapshot().max_point().row + 1; - let target_line = (count * total_lines as usize).div_ceil(100); - let target_point = DisplayPoint::new( - DisplayRow(target_line.saturating_sub(1) as u32), - point.column(), - ); - map.clip_point(target_point, Bias::Left) -} - -fn unmatched_forward( - map: &DisplaySnapshot, - mut display_point: DisplayPoint, - char: char, - times: usize, -) -> DisplayPoint { - for _ in 0..times { - // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1245 - let point = display_point.to_point(map); - let offset = point.to_offset(&map.buffer_snapshot()); - - let ranges = map.buffer_snapshot().enclosing_bracket_ranges(point..point); - let Some(ranges) = ranges else { break }; - let mut closest_closing_destination = None; - let mut closest_distance = usize::MAX; - - for (_, close_range) in ranges { - if close_range.start > offset { - let mut chars = map.buffer_snapshot().chars_at(close_range.start); - if Some(char) == chars.next() { - let distance = close_range.start - offset; - if distance < closest_distance { - closest_closing_destination = Some(close_range.start); - closest_distance = distance; - continue; - } - } - } - } - - let new_point = closest_closing_destination - .map(|destination| destination.to_display_point(map)) - .unwrap_or(display_point); - if new_point == display_point { - break; - } - display_point = new_point; - } - display_point -} - -fn unmatched_backward( - map: &DisplaySnapshot, - mut display_point: DisplayPoint, - char: char, - times: usize, -) -> DisplayPoint { - for _ in 0..times { - // https://github.com/vim/vim/blob/1d87e11a1ef201b26ed87585fba70182ad0c468a/runtime/doc/motion.txt#L1239 - let point = display_point.to_point(map); - let offset = point.to_offset(&map.buffer_snapshot()); - - let ranges = map.buffer_snapshot().enclosing_bracket_ranges(point..point); - let Some(ranges) = ranges else { - break; - }; - - let mut closest_starting_destination = None; - let mut closest_distance = usize::MAX; - - for (start_range, _) in ranges { - if start_range.start < offset { - let mut chars = map.buffer_snapshot().chars_at(start_range.start); - if Some(char) == chars.next() { - let distance = offset - start_range.start; - if distance < closest_distance { - closest_starting_destination = Some(start_range.start); - closest_distance = distance; - continue; - } - } - } - } - - let new_point = closest_starting_destination - .map(|destination| destination.to_display_point(map)) - .unwrap_or(display_point); - if new_point == display_point { - break; - } else { - display_point = new_point; - } - } - display_point -} - -fn find_forward( - map: &DisplaySnapshot, - from: DisplayPoint, - before: bool, - target: char, - times: usize, - mode: FindRange, - smartcase: bool, -) -> Option { - let mut to = from; - let mut found = false; - - for _ in 0..times { - found = false; - let new_to = find_boundary(map, to, mode, |_, right| { - found = is_character_match(target, right, smartcase); - found - }); - if to == new_to { - break; - } - to = new_to; - } - - if found { - if before && to.column() > 0 { - *to.column_mut() -= 1; - Some(map.clip_point(to, Bias::Left)) - } else if before && to.row().0 > 0 { - *to.row_mut() -= 1; - *to.column_mut() = map.line(to.row()).len() as u32; - Some(map.clip_point(to, Bias::Left)) - } else { - Some(to) - } - } else { - None - } -} - -fn find_backward( - map: &DisplaySnapshot, - from: DisplayPoint, - after: bool, - target: char, - times: usize, - mode: FindRange, - smartcase: bool, -) -> DisplayPoint { - let mut to = from; - - for _ in 0..times { - let new_to = find_preceding_boundary_display_point(map, to, mode, |_, right| { - is_character_match(target, right, smartcase) - }); - if to == new_to { - break; - } - to = new_to; - } - - let next = map.buffer_snapshot().chars_at(to.to_point(map)).next(); - if next.is_some() && is_character_match(target, next.unwrap(), smartcase) { - if after { - *to.column_mut() += 1; - map.clip_point(to, Bias::Right) - } else { - to - } - } else { - from - } -} - -/// Returns true if one char is equal to the other or its uppercase variant (if smartcase is true). -pub fn is_character_match(target: char, other: char, smartcase: bool) -> bool { - if smartcase { - if target.is_uppercase() { - target == other - } else { - target == other.to_ascii_lowercase() - } - } else { - target == other - } -} - -fn sneak( - map: &DisplaySnapshot, - from: DisplayPoint, - first_target: char, - second_target: char, - times: usize, - smartcase: bool, -) -> Option { - let mut to = from; - let mut found = false; - - for _ in 0..times { - found = false; - let new_to = find_boundary( - map, - movement::right(map, to), - FindRange::MultiLine, - |left, right| { - found = is_character_match(first_target, left, smartcase) - && is_character_match(second_target, right, smartcase); - found - }, - ); - if to == new_to { - break; - } - to = new_to; - } - - if found { - Some(movement::left(map, to)) - } else { - None - } -} - -fn sneak_backward( - map: &DisplaySnapshot, - from: DisplayPoint, - first_target: char, - second_target: char, - times: usize, - smartcase: bool, -) -> Option { - let mut to = from; - let mut found = false; - - for _ in 0..times { - found = false; - let new_to = - find_preceding_boundary_display_point(map, to, FindRange::MultiLine, |left, right| { - found = is_character_match(first_target, left, smartcase) - && is_character_match(second_target, right, smartcase); - found - }); - if to == new_to { - break; - } - to = new_to; - } - - if found { - Some(movement::left(map, to)) - } else { - None - } -} - -fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint { - let correct_line = map.start_of_relative_buffer_row(point, times as isize); - first_non_whitespace(map, false, correct_line) -} - -fn previous_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint { - let correct_line = map.start_of_relative_buffer_row(point, -(times as isize)); - first_non_whitespace(map, false, correct_line) -} - -fn go_to_column(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint { - let correct_line = map.start_of_relative_buffer_row(point, 0); - right(map, correct_line, times.saturating_sub(1)) -} - -pub(crate) fn next_line_end( - map: &DisplaySnapshot, - mut point: DisplayPoint, - times: usize, -) -> DisplayPoint { - if times > 1 { - point = map.start_of_relative_buffer_row(point, times as isize - 1); - } - end_of_line(map, false, point, 1) -} - -fn window_top( - map: &DisplaySnapshot, - point: DisplayPoint, - text_layout_details: &TextLayoutDetails, - mut times: usize, -) -> (DisplayPoint, SelectionGoal) { - let first_visible_line = text_layout_details - .scroll_anchor - .anchor - .to_display_point(map); - - if first_visible_line.row() != DisplayRow(0) - && text_layout_details.vertical_scroll_margin as usize > times - { - times = text_layout_details.vertical_scroll_margin.ceil() as usize; - } - - if let Some(visible_rows) = text_layout_details.visible_rows { - let bottom_row = first_visible_line.row().0 + visible_rows as u32; - let new_row = (first_visible_line.row().0 + (times as u32)) - .min(bottom_row) - .min(map.max_point().row().0); - let new_col = point.column().min(map.line_len(first_visible_line.row())); - - let new_point = DisplayPoint::new(DisplayRow(new_row), new_col); - (map.clip_point(new_point, Bias::Left), SelectionGoal::None) - } else { - let new_row = - DisplayRow((first_visible_line.row().0 + (times as u32)).min(map.max_point().row().0)); - let new_col = point.column().min(map.line_len(first_visible_line.row())); - - let new_point = DisplayPoint::new(new_row, new_col); - (map.clip_point(new_point, Bias::Left), SelectionGoal::None) - } -} - -fn window_middle( - map: &DisplaySnapshot, - point: DisplayPoint, - text_layout_details: &TextLayoutDetails, -) -> (DisplayPoint, SelectionGoal) { - if let Some(visible_rows) = text_layout_details.visible_rows { - let first_visible_line = text_layout_details - .scroll_anchor - .anchor - .to_display_point(map); - - let max_visible_rows = - (visible_rows as u32).min(map.max_point().row().0 - first_visible_line.row().0); - - let new_row = - (first_visible_line.row().0 + (max_visible_rows / 2)).min(map.max_point().row().0); - let new_row = DisplayRow(new_row); - let new_col = point.column().min(map.line_len(new_row)); - let new_point = DisplayPoint::new(new_row, new_col); - (map.clip_point(new_point, Bias::Left), SelectionGoal::None) - } else { - (point, SelectionGoal::None) - } -} - -fn window_bottom( - map: &DisplaySnapshot, - point: DisplayPoint, - text_layout_details: &TextLayoutDetails, - mut times: usize, -) -> (DisplayPoint, SelectionGoal) { - if let Some(visible_rows) = text_layout_details.visible_rows { - let first_visible_line = text_layout_details - .scroll_anchor - .anchor - .to_display_point(map); - let bottom_row = first_visible_line.row().0 - + (visible_rows + text_layout_details.scroll_anchor.offset.y - 1.).floor() as u32; - if bottom_row < map.max_point().row().0 - && text_layout_details.vertical_scroll_margin as usize > times - { - times = text_layout_details.vertical_scroll_margin.ceil() as usize; - } - let bottom_row_capped = bottom_row.min(map.max_point().row().0); - let new_row = if bottom_row_capped.saturating_sub(times as u32) < first_visible_line.row().0 - { - first_visible_line.row() - } else { - DisplayRow(bottom_row_capped.saturating_sub(times as u32)) - }; - let new_col = point.column().min(map.line_len(new_row)); - let new_point = DisplayPoint::new(new_row, new_col); - (map.clip_point(new_point, Bias::Left), SelectionGoal::None) - } else { - (point, SelectionGoal::None) - } -} - -fn method_motion( - map: &DisplaySnapshot, - mut display_point: DisplayPoint, - times: usize, - direction: Direction, - is_start: bool, -) -> DisplayPoint { - let Some((_, _, buffer)) = map.buffer_snapshot().as_singleton() else { - return display_point; - }; - - for _ in 0..times { - let point = map.display_point_to_point(display_point, Bias::Left); - let offset = point.to_offset(&map.buffer_snapshot()).0; - let range = if direction == Direction::Prev { - 0..offset - } else { - offset..buffer.len() - }; - - let possibilities = buffer - .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(4)) - .filter_map(|(range, object)| { - if !matches!(object, language::TextObject::AroundFunction) { - return None; - } - - let relevant = if is_start { range.start } else { range.end }; - if direction == Direction::Prev && relevant < offset { - Some(relevant) - } else if direction == Direction::Next && relevant > offset + 1 { - Some(relevant) - } else { - None - } - }); - - let dest = if direction == Direction::Prev { - possibilities.max().unwrap_or(offset) - } else { - possibilities.min().unwrap_or(offset) - }; - let new_point = map.clip_point(MultiBufferOffset(dest).to_display_point(map), Bias::Left); - if new_point == display_point { - break; - } - display_point = new_point; - } - display_point -} - -fn comment_motion( - map: &DisplaySnapshot, - mut display_point: DisplayPoint, - times: usize, - direction: Direction, -) -> DisplayPoint { - let Some((_, _, buffer)) = map.buffer_snapshot().as_singleton() else { - return display_point; - }; - - for _ in 0..times { - let point = map.display_point_to_point(display_point, Bias::Left); - let offset = point.to_offset(&map.buffer_snapshot()).0; - let range = if direction == Direction::Prev { - 0..offset - } else { - offset..buffer.len() - }; - - let possibilities = buffer - .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(6)) - .filter_map(|(range, object)| { - if !matches!(object, language::TextObject::AroundComment) { - return None; - } - - let relevant = if direction == Direction::Prev { - range.start - } else { - range.end - }; - if direction == Direction::Prev && relevant < offset { - Some(relevant) - } else if direction == Direction::Next && relevant > offset + 1 { - Some(relevant) - } else { - None - } - }); - - let dest = if direction == Direction::Prev { - possibilities.max().unwrap_or(offset) - } else { - possibilities.min().unwrap_or(offset) - }; - let new_point = map.clip_point(MultiBufferOffset(dest).to_display_point(map), Bias::Left); - if new_point == display_point { - break; - } - display_point = new_point; - } - - display_point -} - -fn section_motion( - map: &DisplaySnapshot, - mut display_point: DisplayPoint, - times: usize, - direction: Direction, - is_start: bool, -) -> DisplayPoint { - if map.buffer_snapshot().as_singleton().is_some() { - for _ in 0..times { - let offset = map - .display_point_to_point(display_point, Bias::Left) - .to_offset(&map.buffer_snapshot()); - let range = if direction == Direction::Prev { - MultiBufferOffset(0)..offset - } else { - offset..map.buffer_snapshot().len() - }; - - // we set a max start depth here because we want a section to only be "top level" - // similar to vim's default of '{' in the first column. - // (and without it, ]] at the start of editor.rs is -very- slow) - let mut possibilities = map - .buffer_snapshot() - .text_object_ranges(range, language::TreeSitterOptions::max_start_depth(3)) - .filter(|(_, object)| { - matches!( - object, - language::TextObject::AroundClass | language::TextObject::AroundFunction - ) - }) - .collect::>(); - possibilities.sort_by_key(|(range_a, _)| range_a.start); - let mut prev_end = None; - let possibilities = possibilities.into_iter().filter_map(|(range, t)| { - if t == language::TextObject::AroundFunction - && prev_end.is_some_and(|prev_end| prev_end > range.start) - { - return None; - } - prev_end = Some(range.end); - - let relevant = if is_start { range.start } else { range.end }; - if direction == Direction::Prev && relevant < offset { - Some(relevant) - } else if direction == Direction::Next && relevant > offset + 1usize { - Some(relevant) - } else { - None - } - }); - - let offset = if direction == Direction::Prev { - possibilities.max().unwrap_or(MultiBufferOffset(0)) - } else { - possibilities.min().unwrap_or(map.buffer_snapshot().len()) - }; - - let new_point = map.clip_point(offset.to_display_point(map), Bias::Left); - if new_point == display_point { - break; - } - display_point = new_point; - } - return display_point; - }; - - for _ in 0..times { - let next_point = if is_start { - movement::start_of_excerpt(map, display_point, direction) - } else { - movement::end_of_excerpt(map, display_point, direction) - }; - if next_point == display_point { - break; - } - display_point = next_point; - } - - display_point -} - -fn matches_indent_type( - target_indent: &text::LineIndent, - current_indent: &text::LineIndent, - indent_type: IndentType, -) -> bool { - match indent_type { - IndentType::Lesser => { - target_indent.spaces < current_indent.spaces || target_indent.tabs < current_indent.tabs - } - IndentType::Greater => { - target_indent.spaces > current_indent.spaces || target_indent.tabs > current_indent.tabs - } - IndentType::Same => { - target_indent.spaces == current_indent.spaces - && target_indent.tabs == current_indent.tabs - } - } -} - -fn indent_motion( - map: &DisplaySnapshot, - mut display_point: DisplayPoint, - times: usize, - direction: Direction, - indent_type: IndentType, -) -> DisplayPoint { - let buffer_point = map.display_point_to_point(display_point, Bias::Left); - let current_row = MultiBufferRow(buffer_point.row); - let current_indent = map.line_indent_for_buffer_row(current_row); - if current_indent.is_line_empty() { - return display_point; - } - let max_row = map.max_point().to_point(map).row; - - for _ in 0..times { - let current_buffer_row = map.display_point_to_point(display_point, Bias::Left).row; - - let target_row = match direction { - Direction::Next => (current_buffer_row + 1..=max_row).find(|&row| { - let indent = map.line_indent_for_buffer_row(MultiBufferRow(row)); - !indent.is_line_empty() - && matches_indent_type(&indent, ¤t_indent, indent_type) - }), - Direction::Prev => (0..current_buffer_row).rev().find(|&row| { - let indent = map.line_indent_for_buffer_row(MultiBufferRow(row)); - !indent.is_line_empty() - && matches_indent_type(&indent, ¤t_indent, indent_type) - }), - } - .unwrap_or(current_buffer_row); - - let new_point = map.point_to_display_point(Point::new(target_row, 0), Bias::Right); - let new_point = first_non_whitespace(map, false, new_point); - if new_point == display_point { - break; - } - display_point = new_point; - } - display_point -} - -#[cfg(test)] -mod test { - - use crate::{ - state::Mode, - test::{NeovimBackedTestContext, VimTestContext}, - }; - use editor::Inlay; - use indoc::indoc; - use language::Point; - use multi_buffer::MultiBufferRow; - - #[gpui::test] - async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - let initial_state = indoc! {r"ˇabc - def - - paragraph - the second - - - - third and - final"}; - - // goes down once - cx.set_shared_state(initial_state).await; - cx.simulate_shared_keystrokes("}").await; - cx.shared_state().await.assert_eq(indoc! {r"abc - def - ˇ - paragraph - the second - - - - third and - final"}); - - // goes up once - cx.simulate_shared_keystrokes("{").await; - cx.shared_state().await.assert_eq(initial_state); - - // goes down twice - cx.simulate_shared_keystrokes("2 }").await; - cx.shared_state().await.assert_eq(indoc! {r"abc - def - - paragraph - the second - ˇ - - - third and - final"}); - - // goes down over multiple blanks - cx.simulate_shared_keystrokes("}").await; - cx.shared_state().await.assert_eq(indoc! {r"abc - def - - paragraph - the second - - - - third and - finaˇl"}); - - // goes up twice - cx.simulate_shared_keystrokes("2 {").await; - cx.shared_state().await.assert_eq(indoc! {r"abc - def - ˇ - paragraph - the second - - - - third and - final"}); - } - - #[gpui::test] - async fn test_matching(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {r"func ˇ(a string) { - do(something(with.and_arrays[0, 2])) - }"}) - .await; - cx.simulate_shared_keystrokes("%").await; - cx.shared_state() - .await - .assert_eq(indoc! {r"func (a stringˇ) { - do(something(with.and_arrays[0, 2])) - }"}); - - // test it works on the last character of the line - cx.set_shared_state(indoc! {r"func (a string) ˇ{ - do(something(with.and_arrays[0, 2])) - }"}) - .await; - cx.simulate_shared_keystrokes("%").await; - cx.shared_state() - .await - .assert_eq(indoc! {r"func (a string) { - do(something(with.and_arrays[0, 2])) - ˇ}"}); - - // test it works on immediate nesting - cx.set_shared_state("ˇ{()}").await; - cx.simulate_shared_keystrokes("%").await; - cx.shared_state().await.assert_eq("{()ˇ}"); - cx.simulate_shared_keystrokes("%").await; - cx.shared_state().await.assert_eq("ˇ{()}"); - - // test it works on immediate nesting inside braces - cx.set_shared_state("{\n ˇ{()}\n}").await; - cx.simulate_shared_keystrokes("%").await; - cx.shared_state().await.assert_eq("{\n {()ˇ}\n}"); - - // test it jumps to the next paren on a line - cx.set_shared_state("func ˇboop() {\n}").await; - cx.simulate_shared_keystrokes("%").await; - cx.shared_state().await.assert_eq("func boop(ˇ) {\n}"); - } - - #[gpui::test] - async fn test_unmatched_forward(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - // test it works with curly braces - cx.set_shared_state(indoc! {r"func (a string) { - do(something(with.anˇd_arrays[0, 2])) - }"}) - .await; - cx.simulate_shared_keystrokes("] }").await; - cx.shared_state() - .await - .assert_eq(indoc! {r"func (a string) { - do(something(with.and_arrays[0, 2])) - ˇ}"}); - - // test it works with brackets - cx.set_shared_state(indoc! {r"func (a string) { - do(somethiˇng(with.and_arrays[0, 2])) - }"}) - .await; - cx.simulate_shared_keystrokes("] )").await; - cx.shared_state() - .await - .assert_eq(indoc! {r"func (a string) { - do(something(with.and_arrays[0, 2])ˇ) - }"}); - - cx.set_shared_state(indoc! {r"func (a string) { a((b, cˇ))}"}) - .await; - cx.simulate_shared_keystrokes("] )").await; - cx.shared_state() - .await - .assert_eq(indoc! {r"func (a string) { a((b, c)ˇ)}"}); - - // test it works on immediate nesting - cx.set_shared_state("{ˇ {}{}}").await; - cx.simulate_shared_keystrokes("] }").await; - cx.shared_state().await.assert_eq("{ {}{}ˇ}"); - cx.set_shared_state("(ˇ ()())").await; - cx.simulate_shared_keystrokes("] )").await; - cx.shared_state().await.assert_eq("( ()()ˇ)"); - - // test it works on immediate nesting inside braces - cx.set_shared_state("{\n ˇ {()}\n}").await; - cx.simulate_shared_keystrokes("] }").await; - cx.shared_state().await.assert_eq("{\n {()}\nˇ}"); - cx.set_shared_state("(\n ˇ {()}\n)").await; - cx.simulate_shared_keystrokes("] )").await; - cx.shared_state().await.assert_eq("(\n {()}\nˇ)"); - } - - #[gpui::test] - async fn test_unmatched_backward(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - // test it works with curly braces - cx.set_shared_state(indoc! {r"func (a string) { - do(something(with.anˇd_arrays[0, 2])) - }"}) - .await; - cx.simulate_shared_keystrokes("[ {").await; - cx.shared_state() - .await - .assert_eq(indoc! {r"func (a string) ˇ{ - do(something(with.and_arrays[0, 2])) - }"}); - - // test it works with brackets - cx.set_shared_state(indoc! {r"func (a string) { - do(somethiˇng(with.and_arrays[0, 2])) - }"}) - .await; - cx.simulate_shared_keystrokes("[ (").await; - cx.shared_state() - .await - .assert_eq(indoc! {r"func (a string) { - doˇ(something(with.and_arrays[0, 2])) - }"}); - - // test it works on immediate nesting - cx.set_shared_state("{{}{} ˇ }").await; - cx.simulate_shared_keystrokes("[ {").await; - cx.shared_state().await.assert_eq("ˇ{{}{} }"); - cx.set_shared_state("(()() ˇ )").await; - cx.simulate_shared_keystrokes("[ (").await; - cx.shared_state().await.assert_eq("ˇ(()() )"); - - // test it works on immediate nesting inside braces - cx.set_shared_state("{\n {()} ˇ\n}").await; - cx.simulate_shared_keystrokes("[ {").await; - cx.shared_state().await.assert_eq("ˇ{\n {()} \n}"); - cx.set_shared_state("(\n {()} ˇ\n)").await; - cx.simulate_shared_keystrokes("[ (").await; - cx.shared_state().await.assert_eq("ˇ(\n {()} \n)"); - } - - #[gpui::test] - async fn test_unmatched_forward_markdown(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new_markdown_with_rust(cx).await; - - cx.neovim.exec("set filetype=markdown").await; - - cx.set_shared_state(indoc! {r" - ```rs - impl Worktree { - pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> { - ˇ } - } - ``` - "}) - .await; - cx.simulate_shared_keystrokes("] }").await; - cx.shared_state().await.assert_eq(indoc! {r" - ```rs - impl Worktree { - pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> { - ˇ} - } - ``` - "}); - - cx.set_shared_state(indoc! {r" - ```rs - impl Worktree { - pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> { - } ˇ - } - ``` - "}) - .await; - cx.simulate_shared_keystrokes("] }").await; - cx.shared_state().await.assert_eq(indoc! {r" - ```rs - impl Worktree { - pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> { - } • - ˇ} - ``` - "}); - } - - #[gpui::test] - async fn test_unmatched_backward_markdown(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new_markdown_with_rust(cx).await; - - cx.neovim.exec("set filetype=markdown").await; - - cx.set_shared_state(indoc! {r" - ```rs - impl Worktree { - pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> { - ˇ } - } - ``` - "}) - .await; - cx.simulate_shared_keystrokes("[ {").await; - cx.shared_state().await.assert_eq(indoc! {r" - ```rs - impl Worktree { - pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{ - } - } - ``` - "}); - - cx.set_shared_state(indoc! {r" - ```rs - impl Worktree { - pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> { - } ˇ - } - ``` - "}) - .await; - cx.simulate_shared_keystrokes("[ {").await; - cx.shared_state().await.assert_eq(indoc! {r" - ```rs - impl Worktree ˇ{ - pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> { - } • - } - ``` - "}); - } - - #[gpui::test] - async fn test_matching_tags(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new_html(cx).await; - - cx.neovim.exec("set filetype=html").await; - - cx.set_shared_state(indoc! {r""}).await; - cx.simulate_shared_keystrokes("%").await; - cx.shared_state() - .await - .assert_eq(indoc! {r"<ˇ/body>"}); - cx.simulate_shared_keystrokes("%").await; - - // test jumping backwards - cx.shared_state() - .await - .assert_eq(indoc! {r"<ˇbody>"}); - - // test self-closing tags - cx.set_shared_state(indoc! {r""}).await; - cx.simulate_shared_keystrokes("%").await; - cx.shared_state().await.assert_eq(indoc! {r""}); - - // test tag with attributes - cx.set_shared_state(indoc! {r"
-
- "}) - .await; - cx.simulate_shared_keystrokes("%").await; - cx.shared_state() - .await - .assert_eq(indoc! {r"
- <ˇ/div> - "}); - - // test multi-line self-closing tag - cx.set_shared_state(indoc! {r#" -
-
"#}) - .await; - cx.simulate_shared_keystrokes("%").await; - cx.shared_state().await.assert_eq(indoc! {r#" - ˇ
-
"#}); - - // test nested closing tag - cx.set_shared_state(indoc! {r#" - - - "#}) - .await; - cx.simulate_shared_keystrokes("%").await; - cx.shared_state().await.assert_eq(indoc! {r#" - - <ˇ/body> - "#}); - cx.simulate_shared_keystrokes("%").await; - cx.shared_state().await.assert_eq(indoc! {r#" - <ˇbody> - - "#}); - } - - #[gpui::test] - async fn test_matching_braces_in_tag(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new_typescript(cx).await; - - // test brackets within tags - cx.set_shared_state(indoc! {r"function f() { - return ( -
-

test

-
- ); - }"}) - .await; - cx.simulate_shared_keystrokes("%").await; - cx.shared_state().await.assert_eq(indoc! {r"function f() { - return ( -
-

test

-
- ); - }"}); - } - - #[gpui::test] - async fn test_matching_nested_brackets(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new_tsx(cx).await; - - cx.set_shared_state(indoc! {r""}) - .await; - cx.simulate_shared_keystrokes("%").await; - cx.shared_state() - .await - .assert_eq(indoc! {r""}); - cx.simulate_shared_keystrokes("%").await; - cx.shared_state() - .await - .assert_eq(indoc! {r""}); - } - - #[gpui::test] - async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - // f and F - cx.set_shared_state("ˇone two three four").await; - cx.simulate_shared_keystrokes("f o").await; - cx.shared_state().await.assert_eq("one twˇo three four"); - cx.simulate_shared_keystrokes(",").await; - cx.shared_state().await.assert_eq("ˇone two three four"); - cx.simulate_shared_keystrokes("2 ;").await; - cx.shared_state().await.assert_eq("one two three fˇour"); - cx.simulate_shared_keystrokes("shift-f e").await; - cx.shared_state().await.assert_eq("one two threˇe four"); - cx.simulate_shared_keystrokes("2 ;").await; - cx.shared_state().await.assert_eq("onˇe two three four"); - cx.simulate_shared_keystrokes(",").await; - cx.shared_state().await.assert_eq("one two thrˇee four"); - - // t and T - cx.set_shared_state("ˇone two three four").await; - cx.simulate_shared_keystrokes("t o").await; - cx.shared_state().await.assert_eq("one tˇwo three four"); - cx.simulate_shared_keystrokes(",").await; - cx.shared_state().await.assert_eq("oˇne two three four"); - cx.simulate_shared_keystrokes("2 ;").await; - cx.shared_state().await.assert_eq("one two three ˇfour"); - cx.simulate_shared_keystrokes("shift-t e").await; - cx.shared_state().await.assert_eq("one two threeˇ four"); - cx.simulate_shared_keystrokes("3 ;").await; - cx.shared_state().await.assert_eq("oneˇ two three four"); - cx.simulate_shared_keystrokes(",").await; - cx.shared_state().await.assert_eq("one two thˇree four"); - } - - #[gpui::test] - async fn test_next_word_end_newline_last_char(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - let initial_state = indoc! {r"something(ˇfoo)"}; - cx.set_shared_state(initial_state).await; - cx.simulate_shared_keystrokes("}").await; - cx.shared_state().await.assert_eq("something(fooˇ)"); - } - - #[gpui::test] - async fn test_next_line_start(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state("ˇone\n two\nthree").await; - cx.simulate_shared_keystrokes("enter").await; - cx.shared_state().await.assert_eq("one\n ˇtwo\nthree"); - } - - #[gpui::test] - async fn test_end_of_line_downward(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state("ˇ one\n two \nthree").await; - cx.simulate_shared_keystrokes("g _").await; - cx.shared_state().await.assert_eq(" onˇe\n two \nthree"); - - cx.set_shared_state("ˇ one \n two \nthree").await; - cx.simulate_shared_keystrokes("g _").await; - cx.shared_state().await.assert_eq(" onˇe \n two \nthree"); - cx.simulate_shared_keystrokes("2 g _").await; - cx.shared_state().await.assert_eq(" one \n twˇo \nthree"); - } - - #[gpui::test] - async fn test_window_top(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - let initial_state = indoc! {r"abc - def - paragraph - the second - third ˇand - final"}; - - cx.set_shared_state(initial_state).await; - cx.simulate_shared_keystrokes("shift-h").await; - cx.shared_state().await.assert_eq(indoc! {r"abˇc - def - paragraph - the second - third and - final"}); - - // clip point - cx.set_shared_state(indoc! {r" - 1 2 3 - 4 5 6 - 7 8 ˇ9 - "}) - .await; - cx.simulate_shared_keystrokes("shift-h").await; - cx.shared_state().await.assert_eq(indoc! {" - 1 2 ˇ3 - 4 5 6 - 7 8 9 - "}); - - cx.set_shared_state(indoc! {r" - 1 2 3 - 4 5 6 - ˇ7 8 9 - "}) - .await; - cx.simulate_shared_keystrokes("shift-h").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇ1 2 3 - 4 5 6 - 7 8 9 - "}); - - cx.set_shared_state(indoc! {r" - 1 2 3 - 4 5 ˇ6 - 7 8 9"}) - .await; - cx.simulate_shared_keystrokes("9 shift-h").await; - cx.shared_state().await.assert_eq(indoc! {" - 1 2 3 - 4 5 6 - 7 8 ˇ9"}); - } - - #[gpui::test] - async fn test_window_middle(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - let initial_state = indoc! {r"abˇc - def - paragraph - the second - third and - final"}; - - cx.set_shared_state(initial_state).await; - cx.simulate_shared_keystrokes("shift-m").await; - cx.shared_state().await.assert_eq(indoc! {r"abc - def - paˇragraph - the second - third and - final"}); - - cx.set_shared_state(indoc! {r" - 1 2 3 - 4 5 6 - 7 8 ˇ9 - "}) - .await; - cx.simulate_shared_keystrokes("shift-m").await; - cx.shared_state().await.assert_eq(indoc! {" - 1 2 3 - 4 5 ˇ6 - 7 8 9 - "}); - cx.set_shared_state(indoc! {r" - 1 2 3 - 4 5 6 - ˇ7 8 9 - "}) - .await; - cx.simulate_shared_keystrokes("shift-m").await; - cx.shared_state().await.assert_eq(indoc! {" - 1 2 3 - ˇ4 5 6 - 7 8 9 - "}); - cx.set_shared_state(indoc! {r" - ˇ1 2 3 - 4 5 6 - 7 8 9 - "}) - .await; - cx.simulate_shared_keystrokes("shift-m").await; - cx.shared_state().await.assert_eq(indoc! {" - 1 2 3 - ˇ4 5 6 - 7 8 9 - "}); - cx.set_shared_state(indoc! {r" - 1 2 3 - ˇ4 5 6 - 7 8 9 - "}) - .await; - cx.simulate_shared_keystrokes("shift-m").await; - cx.shared_state().await.assert_eq(indoc! {" - 1 2 3 - ˇ4 5 6 - 7 8 9 - "}); - cx.set_shared_state(indoc! {r" - 1 2 3 - 4 5 ˇ6 - 7 8 9 - "}) - .await; - cx.simulate_shared_keystrokes("shift-m").await; - cx.shared_state().await.assert_eq(indoc! {" - 1 2 3 - 4 5 ˇ6 - 7 8 9 - "}); - } - - #[gpui::test] - async fn test_window_bottom(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - let initial_state = indoc! {r"abc - deˇf - paragraph - the second - third and - final"}; - - cx.set_shared_state(initial_state).await; - cx.simulate_shared_keystrokes("shift-l").await; - cx.shared_state().await.assert_eq(indoc! {r"abc - def - paragraph - the second - third and - fiˇnal"}); - - cx.set_shared_state(indoc! {r" - 1 2 3 - 4 5 ˇ6 - 7 8 9 - "}) - .await; - cx.simulate_shared_keystrokes("shift-l").await; - cx.shared_state().await.assert_eq(indoc! {" - 1 2 3 - 4 5 6 - 7 8 9 - ˇ"}); - - cx.set_shared_state(indoc! {r" - 1 2 3 - ˇ4 5 6 - 7 8 9 - "}) - .await; - cx.simulate_shared_keystrokes("shift-l").await; - cx.shared_state().await.assert_eq(indoc! {" - 1 2 3 - 4 5 6 - 7 8 9 - ˇ"}); - - cx.set_shared_state(indoc! {r" - 1 2 ˇ3 - 4 5 6 - 7 8 9 - "}) - .await; - cx.simulate_shared_keystrokes("shift-l").await; - cx.shared_state().await.assert_eq(indoc! {" - 1 2 3 - 4 5 6 - 7 8 9 - ˇ"}); - - cx.set_shared_state(indoc! {r" - ˇ1 2 3 - 4 5 6 - 7 8 9 - "}) - .await; - cx.simulate_shared_keystrokes("shift-l").await; - cx.shared_state().await.assert_eq(indoc! {" - 1 2 3 - 4 5 6 - 7 8 9 - ˇ"}); - - cx.set_shared_state(indoc! {r" - 1 2 3 - 4 5 ˇ6 - 7 8 9 - "}) - .await; - cx.simulate_shared_keystrokes("9 shift-l").await; - cx.shared_state().await.assert_eq(indoc! {" - 1 2 ˇ3 - 4 5 6 - 7 8 9 - "}); - } - - #[gpui::test] - async fn test_previous_word_end(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! {r" - 456 5ˇ67 678 - "}) - .await; - cx.simulate_shared_keystrokes("g e").await; - cx.shared_state().await.assert_eq(indoc! {" - 45ˇ6 567 678 - "}); - - // Test times - cx.set_shared_state(indoc! {r" - 123 234 345 - 456 5ˇ67 678 - "}) - .await; - cx.simulate_shared_keystrokes("4 g e").await; - cx.shared_state().await.assert_eq(indoc! {" - 12ˇ3 234 345 - 456 567 678 - "}); - - // With punctuation - cx.set_shared_state(indoc! {r" - 123 234 345 - 4;5.6 5ˇ67 678 - 789 890 901 - "}) - .await; - cx.simulate_shared_keystrokes("g e").await; - cx.shared_state().await.assert_eq(indoc! {" - 123 234 345 - 4;5.ˇ6 567 678 - 789 890 901 - "}); - - // With punctuation and count - cx.set_shared_state(indoc! {r" - 123 234 345 - 4;5.6 5ˇ67 678 - 789 890 901 - "}) - .await; - cx.simulate_shared_keystrokes("5 g e").await; - cx.shared_state().await.assert_eq(indoc! {" - 123 234 345 - ˇ4;5.6 567 678 - 789 890 901 - "}); - - // newlines - cx.set_shared_state(indoc! {r" - 123 234 345 - - 78ˇ9 890 901 - "}) - .await; - cx.simulate_shared_keystrokes("g e").await; - cx.shared_state().await.assert_eq(indoc! {" - 123 234 345 - ˇ - 789 890 901 - "}); - cx.simulate_shared_keystrokes("g e").await; - cx.shared_state().await.assert_eq(indoc! {" - 123 234 34ˇ5 - - 789 890 901 - "}); - - // With punctuation - cx.set_shared_state(indoc! {r" - 123 234 345 - 4;5.ˇ6 567 678 - 789 890 901 - "}) - .await; - cx.simulate_shared_keystrokes("g shift-e").await; - cx.shared_state().await.assert_eq(indoc! {" - 123 234 34ˇ5 - 4;5.6 567 678 - 789 890 901 - "}); - - // With multi byte char - cx.set_shared_state(indoc! {r" - bar ˇó - "}) - .await; - cx.simulate_shared_keystrokes("g e").await; - cx.shared_state().await.assert_eq(indoc! {" - baˇr ó - "}); - } - - #[gpui::test] - async fn test_visual_match_eol(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - fn aˇ() { - return - } - "}) - .await; - cx.simulate_shared_keystrokes("v $ %").await; - cx.shared_state().await.assert_eq(indoc! {" - fn a«() { - return - }ˇ» - "}); - } - - #[gpui::test] - async fn test_clipping_with_inlay_hints(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! {" - struct Foo { - ˇ - } - "}, - Mode::Normal, - ); - - cx.update_editor(|editor, _window, cx| { - let range = editor.selections.newest_anchor().range(); - let inlay_text = " field: int,\n field2: string\n field3: float"; - let inlay = Inlay::edit_prediction(1, range.start, inlay_text); - editor.splice_inlays(&[], vec![inlay], cx); - }); - - cx.simulate_keystrokes("j"); - cx.assert_state( - indoc! {" - struct Foo { - - ˇ} - "}, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_clipping_with_inlay_hints_end_of_line(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! {" - ˇstruct Foo { - - } - "}, - Mode::Normal, - ); - cx.update_editor(|editor, _window, cx| { - let snapshot = editor.buffer().read(cx).snapshot(cx); - let end_of_line = - snapshot.anchor_after(Point::new(0, snapshot.line_len(MultiBufferRow(0)))); - let inlay_text = " hint"; - let inlay = Inlay::edit_prediction(1, end_of_line, inlay_text); - editor.splice_inlays(&[], vec![inlay], cx); - }); - cx.simulate_keystrokes("$"); - cx.assert_state( - indoc! {" - struct Foo ˇ{ - - } - "}, - Mode::Normal, - ); - } - - #[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; - // Normal mode - cx.set_shared_state(indoc! {" - The ˇquick brown - fox jumps over - the lazy dog - The quick brown - fox jumps over - the lazy dog - The quick brown - fox jumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("2 0 %").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox ˇjumps over - the lazy dog - The quick brown - fox jumps over - the lazy dog - The quick brown - fox jumps over - the lazy dog"}); - - cx.simulate_shared_keystrokes("2 5 %").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox jumps over - the ˇlazy dog - The quick brown - fox jumps over - the lazy dog - The quick brown - fox jumps over - the lazy dog"}); - - cx.simulate_shared_keystrokes("7 5 %").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox jumps over - the lazy dog - The quick brown - fox jumps over - the lazy dog - The ˇquick brown - fox jumps over - the lazy dog"}); - - // Visual mode - cx.set_shared_state(indoc! {" - The ˇquick brown - fox jumps over - the lazy dog - The quick brown - fox jumps over - the lazy dog - The quick brown - fox jumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("v 5 0 %").await; - cx.shared_state().await.assert_eq(indoc! {" - The «quick brown - fox jumps over - the lazy dog - The quick brown - fox jˇ»umps over - the lazy dog - The quick brown - fox jumps over - the lazy dog"}); - - cx.set_shared_state(indoc! {" - The ˇquick brown - fox jumps over - the lazy dog - The quick brown - fox jumps over - the lazy dog - The quick brown - fox jumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("v 1 0 0 %").await; - cx.shared_state().await.assert_eq(indoc! {" - The «quick brown - fox jumps over - the lazy dog - The quick brown - fox jumps over - the lazy dog - The quick brown - fox jumps over - the lˇ»azy dog"}); - } - - #[gpui::test] - async fn test_space_non_ascii(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇπππππ").await; - cx.simulate_shared_keystrokes("3 space").await; - cx.shared_state().await.assert_eq("πππˇππ"); - } - - #[gpui::test] - async fn test_space_non_ascii_eol(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ππππˇπ - πanotherline"}) - .await; - cx.simulate_shared_keystrokes("4 space").await; - cx.shared_state().await.assert_eq(indoc! {" - πππππ - πanˇotherline"}); - } - - #[gpui::test] - async fn test_backspace_non_ascii_bol(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ππππ - πanˇotherline"}) - .await; - cx.simulate_shared_keystrokes("4 backspace").await; - cx.shared_state().await.assert_eq(indoc! {" - πππˇπ - πanotherline"}); - } - - #[gpui::test] - async fn test_go_to_indent(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.set_state( - indoc! { - "func empty(a string) bool { - ˇif a == \"\" { - return true - } - return false - }" - }, - Mode::Normal, - ); - cx.simulate_keystrokes("[ -"); - cx.assert_state( - indoc! { - "ˇfunc empty(a string) bool { - if a == \"\" { - return true - } - return false - }" - }, - Mode::Normal, - ); - cx.simulate_keystrokes("] ="); - cx.assert_state( - indoc! { - "func empty(a string) bool { - if a == \"\" { - return true - } - return false - ˇ}" - }, - Mode::Normal, - ); - cx.simulate_keystrokes("[ +"); - cx.assert_state( - indoc! { - "func empty(a string) bool { - if a == \"\" { - return true - } - ˇreturn false - }" - }, - Mode::Normal, - ); - cx.simulate_keystrokes("2 [ ="); - cx.assert_state( - indoc! { - "func empty(a string) bool { - ˇif a == \"\" { - return true - } - return false - }" - }, - Mode::Normal, - ); - cx.simulate_keystrokes("] +"); - cx.assert_state( - indoc! { - "func empty(a string) bool { - if a == \"\" { - ˇreturn true - } - return false - }" - }, - Mode::Normal, - ); - cx.simulate_keystrokes("] -"); - cx.assert_state( - indoc! { - "func empty(a string) bool { - if a == \"\" { - return true - ˇ} - return false - }" - }, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_delete_key_can_remove_last_character(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state("abˇc").await; - cx.simulate_shared_keystrokes("delete").await; - cx.shared_state().await.assert_eq("aˇb"); - } - - #[gpui::test] - async fn test_forced_motion_delete_to_start_of_line(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ˇthe quick brown fox - jumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("d v 0").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇhe quick brown fox - jumped over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - - cx.set_shared_state(indoc! {" - the quick bˇrown fox - jumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("d v 0").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇown fox - jumped over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - - cx.set_shared_state(indoc! {" - the quick brown foˇx - jumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("d v 0").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇ - jumped over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - } - - #[gpui::test] - async fn test_forced_motion_delete_to_middle_of_line(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ˇthe quick brown fox - jumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("d v g shift-m").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇbrown fox - jumped over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - - cx.set_shared_state(indoc! {" - the quick bˇrown fox - jumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("d v g shift-m").await; - cx.shared_state().await.assert_eq(indoc! {" - the quickˇown fox - jumped over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - - cx.set_shared_state(indoc! {" - the quick brown foˇx - jumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("d v g shift-m").await; - cx.shared_state().await.assert_eq(indoc! {" - the quicˇk - jumped over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - - cx.set_shared_state(indoc! {" - ˇthe quick brown fox - jumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("d v 7 5 g shift-m").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇ fox - jumped over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - - cx.set_shared_state(indoc! {" - ˇthe quick brown fox - jumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("d v 2 3 g shift-m").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇuick brown fox - jumped over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - } - - #[gpui::test] - async fn test_forced_motion_delete_to_end_of_line(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - the quick brown foˇx - jumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("d v $").await; - cx.shared_state().await.assert_eq(indoc! {" - the quick brown foˇx - jumped over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - - cx.set_shared_state(indoc! {" - ˇthe quick brown fox - jumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("d v $").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇx - jumped over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - } - - #[gpui::test] - async fn test_forced_motion_yank(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ˇthe quick brown fox - jumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("y v j p").await; - cx.shared_state().await.assert_eq(indoc! {" - the quick brown fox - ˇthe quick brown fox - jumped over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - - cx.set_shared_state(indoc! {" - the quick bˇrown fox - jumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("y v j p").await; - cx.shared_state().await.assert_eq(indoc! {" - the quick brˇrown fox - jumped overown fox - jumped over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - - cx.set_shared_state(indoc! {" - the quick brown foˇx - jumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("y v j p").await; - cx.shared_state().await.assert_eq(indoc! {" - the quick brown foxˇx - jumped over the la - jumped over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - - cx.set_shared_state(indoc! {" - the quick brown fox - jˇumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("y v k p").await; - cx.shared_state().await.assert_eq(indoc! {" - thˇhe quick brown fox - je quick brown fox - jumped over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - } - - #[gpui::test] - async fn test_inclusive_to_exclusive_delete(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ˇthe quick brown fox - jumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("d v e").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇe quick brown fox - jumped over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - - cx.set_shared_state(indoc! {" - the quick bˇrown fox - jumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("d v e").await; - cx.shared_state().await.assert_eq(indoc! {" - the quick bˇn fox - jumped over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - - cx.set_shared_state(indoc! {" - the quick brown foˇx - jumped over the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("d v e").await; - cx.shared_state().await.assert_eq(indoc! {" - the quick brown foˇd over the lazy dog"}); - assert!(!cx.cx.forced_motion()); - } -} diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs deleted file mode 100644 index aee0b424f0..0000000000 --- a/crates/vim/src/normal.rs +++ /dev/null @@ -1,2320 +0,0 @@ -mod change; -mod convert; -mod delete; -mod increment; -pub(crate) mod mark; -pub(crate) mod paste; -pub(crate) mod repeat; -mod scroll; -pub(crate) mod search; -pub mod substitute; -mod toggle_comments; -pub(crate) mod yank; - -use std::collections::HashMap; -use std::sync::Arc; - -use crate::{ - Vim, - indent::IndentDirection, - motion::{self, Motion, first_non_whitespace, next_line_end, right}, - object::Object, - state::{Mark, Mode, Operator}, - surrounds::SurroundsType, -}; -use collections::BTreeSet; -use convert::ConvertTarget; -use editor::Editor; -use editor::{Anchor, SelectionEffects}; -use editor::{Bias, ToPoint}; -use editor::{display_map::ToDisplayPoint, movement}; -use gpui::{Context, Window, actions}; -use language::{Point, SelectionGoal}; -use log::error; -use multi_buffer::MultiBufferRow; - -actions!( - vim, - [ - /// Inserts text after the cursor. - InsertAfter, - /// Inserts text before the cursor. - InsertBefore, - /// Inserts at the first non-whitespace character. - InsertFirstNonWhitespace, - /// Inserts at the end of the line. - InsertEndOfLine, - /// Inserts a new line above the current line. - InsertLineAbove, - /// Inserts a new line below the current line. - InsertLineBelow, - /// Inserts an empty line above without entering insert mode. - InsertEmptyLineAbove, - /// Inserts an empty line below without entering insert mode. - InsertEmptyLineBelow, - /// Inserts at the previous insert position. - InsertAtPrevious, - /// Joins the current line with the next line. - JoinLines, - /// Joins lines without adding whitespace. - JoinLinesNoWhitespace, - /// Deletes character to the left. - DeleteLeft, - /// Deletes character to the right. - DeleteRight, - /// Deletes using Helix-style behavior. - HelixDelete, - /// Collapse the current selection - HelixCollapseSelection, - /// Changes from cursor to end of line. - ChangeToEndOfLine, - /// Deletes from cursor to end of line. - DeleteToEndOfLine, - /// Yanks (copies) the selected text. - Yank, - /// Yanks the entire line. - YankLine, - /// Yanks from cursor to end of line. - YankToEndOfLine, - /// Toggles the case of selected text. - ChangeCase, - /// Converts selected text to uppercase. - ConvertToUpperCase, - /// Converts selected text to lowercase. - ConvertToLowerCase, - /// Applies ROT13 cipher to selected text. - ConvertToRot13, - /// Applies ROT47 cipher to selected text. - ConvertToRot47, - /// Toggles comments for selected lines. - ToggleComments, - /// Shows the current location in the file. - ShowLocation, - /// Undoes the last change. - Undo, - /// Redoes the last undone change. - Redo, - /// Undoes all changes to the most recently changed line. - UndoLastLine, - /// Go to tab page (with count support). - GoToTab, - /// Go to previous tab page (with count support). - GoToPreviousTab, - /// Go to tab page (with count support). - GoToPreviousReference, - /// Go to previous tab page (with count support). - GoToNextReference, - ] -); - -pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { - Vim::action(editor, cx, Vim::insert_after); - Vim::action(editor, cx, Vim::insert_before); - Vim::action(editor, cx, Vim::insert_first_non_whitespace); - Vim::action(editor, cx, Vim::insert_end_of_line); - Vim::action(editor, cx, Vim::insert_line_above); - Vim::action(editor, cx, Vim::insert_line_below); - Vim::action(editor, cx, Vim::insert_empty_line_above); - Vim::action(editor, cx, Vim::insert_empty_line_below); - Vim::action(editor, cx, Vim::insert_at_previous); - Vim::action(editor, cx, Vim::change_case); - Vim::action(editor, cx, Vim::convert_to_upper_case); - Vim::action(editor, cx, Vim::convert_to_lower_case); - Vim::action(editor, cx, Vim::convert_to_rot13); - Vim::action(editor, cx, Vim::convert_to_rot47); - Vim::action(editor, cx, Vim::yank_line); - Vim::action(editor, cx, Vim::yank_to_end_of_line); - Vim::action(editor, cx, Vim::toggle_comments); - Vim::action(editor, cx, Vim::paste); - Vim::action(editor, cx, Vim::show_location); - - Vim::action(editor, cx, |vim, _: &DeleteLeft, window, cx| { - vim.record_current_action(cx); - let times = Vim::take_count(cx); - let forced_motion = Vim::take_forced_motion(cx); - vim.delete_motion(Motion::Left, times, forced_motion, window, cx); - }); - Vim::action(editor, cx, |vim, _: &DeleteRight, window, cx| { - vim.record_current_action(cx); - let times = Vim::take_count(cx); - let forced_motion = Vim::take_forced_motion(cx); - vim.delete_motion(Motion::Right, times, forced_motion, window, cx); - }); - - Vim::action(editor, cx, |vim, _: &HelixDelete, window, cx| { - vim.record_current_action(cx); - vim.update_editor(cx, |_, editor, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - if selection.is_empty() { - selection.end = movement::right(map, selection.end) - } - }) - }) - }); - vim.visual_delete(false, window, cx); - vim.switch_mode(Mode::HelixNormal, true, window, cx); - }); - - Vim::action(editor, cx, |vim, _: &HelixCollapseSelection, window, cx| { - vim.update_editor(cx, |_, editor, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let mut point = selection.head(); - if !selection.reversed && !selection.is_empty() { - point = movement::left(map, selection.head()); - } - selection.collapse_to(point, selection.goal) - }); - }); - }); - }); - - Vim::action(editor, cx, |vim, _: &ChangeToEndOfLine, window, cx| { - vim.start_recording(cx); - let times = Vim::take_count(cx); - let forced_motion = Vim::take_forced_motion(cx); - vim.change_motion( - Motion::EndOfLine { - display_lines: false, - }, - times, - forced_motion, - window, - cx, - ); - }); - Vim::action(editor, cx, |vim, _: &DeleteToEndOfLine, window, cx| { - vim.record_current_action(cx); - let times = Vim::take_count(cx); - let forced_motion = Vim::take_forced_motion(cx); - vim.delete_motion( - Motion::EndOfLine { - display_lines: false, - }, - times, - forced_motion, - window, - cx, - ); - }); - Vim::action(editor, cx, |vim, _: &JoinLines, window, cx| { - vim.join_lines_impl(true, window, cx); - }); - - Vim::action(editor, cx, |vim, _: &JoinLinesNoWhitespace, window, cx| { - vim.join_lines_impl(false, window, cx); - }); - - Vim::action(editor, cx, |vim, _: &GoToPreviousReference, window, cx| { - let count = Vim::take_count(cx); - vim.update_editor(cx, |_, editor, cx| { - let task = editor.go_to_reference_before_or_after_position( - editor::Direction::Prev, - count.unwrap_or(1), - window, - cx, - ); - if let Some(task) = task { - task.detach_and_log_err(cx); - }; - }); - }); - - Vim::action(editor, cx, |vim, _: &GoToNextReference, window, cx| { - let count = Vim::take_count(cx); - vim.update_editor(cx, |_, editor, cx| { - let task = editor.go_to_reference_before_or_after_position( - editor::Direction::Next, - count.unwrap_or(1), - window, - cx, - ); - if let Some(task) = task { - task.detach_and_log_err(cx); - }; - }); - }); - - Vim::action(editor, cx, |vim, _: &Undo, window, cx| { - let times = Vim::take_count(cx); - Vim::take_forced_motion(cx); - vim.update_editor(cx, |_, editor, cx| { - for _ in 0..times.unwrap_or(1) { - editor.undo(&editor::actions::Undo, window, cx); - } - }); - }); - Vim::action(editor, cx, |vim, _: &Redo, window, cx| { - let times = Vim::take_count(cx); - Vim::take_forced_motion(cx); - vim.update_editor(cx, |_, editor, cx| { - for _ in 0..times.unwrap_or(1) { - editor.redo(&editor::actions::Redo, window, cx); - } - }); - }); - Vim::action(editor, cx, |vim, _: &UndoLastLine, window, cx| { - Vim::take_forced_motion(cx); - vim.update_editor(cx, |vim, editor, cx| { - let snapshot = editor.buffer().read(cx).snapshot(cx); - let Some(last_change) = editor.change_list.last_before_grouping() else { - return; - }; - - let anchors = last_change.to_vec(); - let mut last_row = None; - let ranges: Vec<_> = anchors - .iter() - .filter_map(|anchor| { - let point = anchor.to_point(&snapshot); - if last_row == Some(point.row) { - return None; - } - last_row = Some(point.row); - let line_range = Point::new(point.row, 0) - ..Point::new(point.row, snapshot.line_len(MultiBufferRow(point.row))); - Some(( - snapshot.anchor_before(line_range.start) - ..snapshot.anchor_after(line_range.end), - line_range, - )) - }) - .collect(); - - let edits = editor.buffer().update(cx, |buffer, cx| { - let current_content = ranges - .iter() - .map(|(anchors, _)| { - buffer - .snapshot(cx) - .text_for_range(anchors.clone()) - .collect::() - }) - .collect::>(); - let mut content_before_undo = current_content.clone(); - let mut undo_count = 0; - - loop { - let undone_tx = buffer.undo(cx); - undo_count += 1; - let mut content_after_undo = Vec::new(); - - let mut line_changed = false; - for ((anchors, _), text_before_undo) in - ranges.iter().zip(content_before_undo.iter()) - { - let snapshot = buffer.snapshot(cx); - let text_after_undo = - snapshot.text_for_range(anchors.clone()).collect::(); - - if &text_after_undo != text_before_undo { - line_changed = true; - } - content_after_undo.push(text_after_undo); - } - - content_before_undo = content_after_undo; - if !line_changed { - break; - } - if undone_tx == vim.undo_last_line_tx { - break; - } - } - - let edits = ranges - .into_iter() - .zip(content_before_undo.into_iter().zip(current_content)) - .filter_map(|((_, mut points), (mut old_text, new_text))| { - if new_text == old_text { - return None; - } - let common_suffix_starts_at = old_text - .char_indices() - .rev() - .zip(new_text.chars().rev()) - .find_map( - |((i, a), b)| { - if a != b { Some(i + a.len_utf8()) } else { None } - }, - ) - .unwrap_or(old_text.len()); - points.end.column -= (old_text.len() - common_suffix_starts_at) as u32; - old_text = old_text.split_at(common_suffix_starts_at).0.to_string(); - let common_prefix_len = old_text - .char_indices() - .zip(new_text.chars()) - .find_map(|((i, a), b)| if a != b { Some(i) } else { None }) - .unwrap_or(0); - points.start.column = common_prefix_len as u32; - old_text = old_text.split_at(common_prefix_len).1.to_string(); - - Some((points, old_text)) - }) - .collect::>(); - - for _ in 0..undo_count { - buffer.redo(cx); - } - edits - }); - vim.undo_last_line_tx = editor.transact(window, cx, |editor, window, cx| { - editor.change_list.invert_last_group(); - editor.edit(edits, cx); - editor.change_selections(SelectionEffects::default(), window, cx, |s| { - s.select_anchor_ranges(anchors.into_iter().map(|a| a..a)); - }) - }); - }); - }); - - repeat::register(editor, cx); - scroll::register(editor, cx); - search::register(editor, cx); - substitute::register(editor, cx); - increment::register(editor, cx); -} - -impl Vim { - pub fn normal_motion( - &mut self, - motion: Motion, - operator: Option, - times: Option, - forced_motion: bool, - window: &mut Window, - cx: &mut Context, - ) { - match operator { - None => self.move_cursor(motion, times, window, cx), - Some(Operator::Change) => self.change_motion(motion, times, forced_motion, window, cx), - Some(Operator::Delete) => self.delete_motion(motion, times, forced_motion, window, cx), - Some(Operator::Yank) => self.yank_motion(motion, times, forced_motion, window, cx), - Some(Operator::AddSurrounds { target: None }) => {} - Some(Operator::Indent) => self.indent_motion( - motion, - times, - forced_motion, - IndentDirection::In, - window, - cx, - ), - Some(Operator::Rewrap) => self.rewrap_motion(motion, times, forced_motion, window, cx), - Some(Operator::Outdent) => self.indent_motion( - motion, - times, - forced_motion, - IndentDirection::Out, - window, - cx, - ), - Some(Operator::AutoIndent) => self.indent_motion( - motion, - times, - forced_motion, - IndentDirection::Auto, - window, - cx, - ), - Some(Operator::ShellCommand) => { - self.shell_command_motion(motion, times, forced_motion, window, cx) - } - Some(Operator::Lowercase) => self.convert_motion( - motion, - times, - forced_motion, - ConvertTarget::LowerCase, - window, - cx, - ), - Some(Operator::Uppercase) => self.convert_motion( - motion, - times, - forced_motion, - ConvertTarget::UpperCase, - window, - cx, - ), - Some(Operator::OppositeCase) => self.convert_motion( - motion, - times, - forced_motion, - ConvertTarget::OppositeCase, - window, - cx, - ), - Some(Operator::Rot13) => self.convert_motion( - motion, - times, - forced_motion, - ConvertTarget::Rot13, - window, - cx, - ), - Some(Operator::Rot47) => self.convert_motion( - motion, - times, - forced_motion, - ConvertTarget::Rot47, - window, - cx, - ), - Some(Operator::ToggleComments) => { - self.toggle_comments_motion(motion, times, forced_motion, window, cx) - } - Some(Operator::ReplaceWithRegister) => { - self.replace_with_register_motion(motion, times, forced_motion, window, cx) - } - Some(Operator::Exchange) => { - self.exchange_motion(motion, times, forced_motion, window, cx) - } - Some(operator) => { - // Can't do anything for text objects, Ignoring - error!("Unexpected normal mode motion operator: {:?}", operator) - } - } - // Exit temporary normal mode (if active). - self.exit_temporary_normal(window, cx); - } - - pub fn normal_object( - &mut self, - object: Object, - times: Option, - opening: bool, - window: &mut Window, - cx: &mut Context, - ) { - let mut waiting_operator: Option = None; - match self.maybe_pop_operator() { - Some(Operator::Object { around }) => match self.maybe_pop_operator() { - Some(Operator::Change) => self.change_object(object, around, times, window, cx), - Some(Operator::Delete) => self.delete_object(object, around, times, window, cx), - Some(Operator::Yank) => self.yank_object(object, around, times, window, cx), - Some(Operator::Indent) => { - self.indent_object(object, around, IndentDirection::In, times, window, cx) - } - Some(Operator::Outdent) => { - self.indent_object(object, around, IndentDirection::Out, times, window, cx) - } - Some(Operator::AutoIndent) => { - self.indent_object(object, around, IndentDirection::Auto, times, window, cx) - } - Some(Operator::ShellCommand) => { - self.shell_command_object(object, around, window, cx); - } - Some(Operator::Rewrap) => self.rewrap_object(object, around, times, window, cx), - Some(Operator::Lowercase) => { - self.convert_object(object, around, ConvertTarget::LowerCase, times, window, cx) - } - Some(Operator::Uppercase) => { - self.convert_object(object, around, ConvertTarget::UpperCase, times, window, cx) - } - Some(Operator::OppositeCase) => self.convert_object( - object, - around, - ConvertTarget::OppositeCase, - times, - window, - cx, - ), - Some(Operator::Rot13) => { - self.convert_object(object, around, ConvertTarget::Rot13, times, window, cx) - } - Some(Operator::Rot47) => { - self.convert_object(object, around, ConvertTarget::Rot47, times, window, cx) - } - Some(Operator::AddSurrounds { target: None }) => { - waiting_operator = Some(Operator::AddSurrounds { - target: Some(SurroundsType::Object(object, around)), - }); - } - Some(Operator::ToggleComments) => { - self.toggle_comments_object(object, around, times, window, cx) - } - Some(Operator::ReplaceWithRegister) => { - self.replace_with_register_object(object, around, window, cx) - } - Some(Operator::Exchange) => self.exchange_object(object, around, window, cx), - Some(Operator::HelixMatch) => { - self.select_current_object(object, around, window, cx) - } - _ => { - // Can't do anything for namespace operators. Ignoring - } - }, - Some(Operator::HelixNext { around }) => { - self.select_next_object(object, around, window, cx); - } - Some(Operator::HelixPrevious { around }) => { - self.select_previous_object(object, around, window, cx); - } - Some(Operator::DeleteSurrounds) => { - waiting_operator = Some(Operator::DeleteSurrounds); - } - Some(Operator::ChangeSurrounds { target: None, .. }) => { - if self.check_and_move_to_valid_bracket_pair(object, window, cx) { - waiting_operator = Some(Operator::ChangeSurrounds { - target: Some(object), - opening, - }); - } - } - _ => { - // Can't do anything with change/delete/yank/surrounds and text objects. Ignoring - } - } - self.clear_operator(window, cx); - if let Some(operator) = waiting_operator { - self.push_operator(operator, window, cx); - } - } - - pub(crate) fn move_cursor( - &mut self, - motion: Motion, - times: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.update_editor(cx, |vim, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - - // If vim is in temporary mode and the motion being used is - // `EndOfLine` ($), we'll want to disable clipping at line ends so - // that the newline character can be selected so that, when moving - // back to visual mode, the cursor will be placed after the last - // character and not before it. - let clip_at_line_ends = editor.clip_at_line_ends(cx); - let should_disable_clip = matches!(motion, Motion::EndOfLine { .. }) && vim.temp_mode; - - if should_disable_clip { - editor.set_clip_at_line_ends(false, cx) - }; - - editor.change_selections( - SelectionEffects::default().nav_history(motion.push_to_jump_list()), - window, - cx, - |s| { - s.move_cursors_with(|map, cursor, goal| { - motion - .move_point(map, cursor, goal, times, &text_layout_details) - .unwrap_or((cursor, goal)) - }) - }, - ); - - if should_disable_clip { - editor.set_clip_at_line_ends(clip_at_line_ends, cx); - }; - }); - } - - fn insert_after(&mut self, _: &InsertAfter, window: &mut Window, cx: &mut Context) { - self.start_recording(cx); - self.switch_mode(Mode::Insert, false, window, cx); - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_cursors_with(|map, cursor, _| (right(map, cursor, 1), SelectionGoal::None)); - }); - }); - } - - fn insert_before(&mut self, _: &InsertBefore, window: &mut Window, cx: &mut Context) { - self.start_recording(cx); - if self.mode.is_visual() { - let current_mode = self.mode; - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - if current_mode == Mode::VisualLine { - let start_of_line = motion::start_of_line(map, false, selection.start); - selection.collapse_to(start_of_line, SelectionGoal::None) - } else { - selection.collapse_to(selection.start, SelectionGoal::None) - } - }); - }); - }); - } - self.switch_mode(Mode::Insert, false, window, cx); - } - - fn insert_first_non_whitespace( - &mut self, - _: &InsertFirstNonWhitespace, - window: &mut Window, - cx: &mut Context, - ) { - self.start_recording(cx); - self.switch_mode(Mode::Insert, false, window, cx); - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_cursors_with(|map, cursor, _| { - ( - first_non_whitespace(map, false, cursor), - SelectionGoal::None, - ) - }); - }); - }); - } - - fn insert_end_of_line( - &mut self, - _: &InsertEndOfLine, - window: &mut Window, - cx: &mut Context, - ) { - self.start_recording(cx); - self.switch_mode(Mode::Insert, false, window, cx); - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_cursors_with(|map, cursor, _| { - (next_line_end(map, cursor, 1), SelectionGoal::None) - }); - }); - }); - } - - fn insert_at_previous( - &mut self, - _: &InsertAtPrevious, - window: &mut Window, - cx: &mut Context, - ) { - self.start_recording(cx); - self.switch_mode(Mode::Insert, false, window, cx); - self.update_editor(cx, |vim, editor, cx| { - if let Some(Mark::Local(marks)) = vim.get_mark("^", editor, window, cx) - && !marks.is_empty() - { - editor.change_selections(Default::default(), window, cx, |s| { - s.select_anchor_ranges(marks.iter().map(|mark| *mark..*mark)) - }); - } - }); - } - - fn insert_line_above( - &mut self, - _: &InsertLineAbove, - window: &mut Window, - cx: &mut Context, - ) { - self.start_recording(cx); - self.switch_mode(Mode::Insert, false, window, cx); - self.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - let selections = editor.selections.all::(&editor.display_snapshot(cx)); - let snapshot = editor.buffer().read(cx).snapshot(cx); - - let selection_start_rows: BTreeSet = selections - .into_iter() - .map(|selection| selection.start.row) - .collect(); - let edits = selection_start_rows - .into_iter() - .map(|row| { - let indent = snapshot - .indent_and_comment_for_line(MultiBufferRow(row), cx) - .chars() - .collect::(); - - let start_of_line = Point::new(row, 0); - (start_of_line..start_of_line, indent + "\n") - }) - .collect::>(); - editor.edit_with_autoindent(edits, cx); - editor.change_selections(Default::default(), window, cx, |s| { - s.move_cursors_with(|map, cursor, _| { - let previous_line = map.start_of_relative_buffer_row(cursor, -1); - let insert_point = motion::end_of_line(map, false, previous_line, 1); - (insert_point, SelectionGoal::None) - }); - }); - }); - }); - } - - fn insert_line_below( - &mut self, - _: &InsertLineBelow, - window: &mut Window, - cx: &mut Context, - ) { - self.start_recording(cx); - self.switch_mode(Mode::Insert, false, window, cx); - self.update_editor(cx, |_, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - editor.transact(window, cx, |editor, window, cx| { - let selections = editor.selections.all::(&editor.display_snapshot(cx)); - let snapshot = editor.buffer().read(cx).snapshot(cx); - - let selection_end_rows: BTreeSet = selections - .into_iter() - .map(|selection| selection.end.row) - .collect(); - let edits = selection_end_rows - .into_iter() - .map(|row| { - let indent = snapshot - .indent_and_comment_for_line(MultiBufferRow(row), cx) - .chars() - .collect::(); - - let end_of_line = Point::new(row, snapshot.line_len(MultiBufferRow(row))); - (end_of_line..end_of_line, "\n".to_string() + &indent) - }) - .collect::>(); - editor.change_selections(Default::default(), window, cx, |s| { - s.maybe_move_cursors_with(|map, cursor, goal| { - Motion::CurrentLine.move_point( - map, - cursor, - goal, - None, - &text_layout_details, - ) - }); - }); - editor.edit_with_autoindent(edits, cx); - }); - }); - } - - fn insert_empty_line_above( - &mut self, - _: &InsertEmptyLineAbove, - window: &mut Window, - cx: &mut Context, - ) { - self.record_current_action(cx); - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - self.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, _, cx| { - let selections = editor.selections.all::(&editor.display_snapshot(cx)); - - let selection_start_rows: BTreeSet = selections - .into_iter() - .map(|selection| selection.start.row) - .collect(); - let edits = selection_start_rows - .into_iter() - .map(|row| { - let start_of_line = Point::new(row, 0); - (start_of_line..start_of_line, "\n".repeat(count)) - }) - .collect::>(); - editor.edit(edits, cx); - }); - }); - } - - fn insert_empty_line_below( - &mut self, - _: &InsertEmptyLineBelow, - window: &mut Window, - cx: &mut Context, - ) { - self.record_current_action(cx); - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - self.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - let display_map = editor.display_snapshot(cx); - let selections = editor.selections.all::(&display_map); - let snapshot = editor.buffer().read(cx).snapshot(cx); - let display_selections = editor.selections.all_display(&display_map); - let original_positions = display_selections - .iter() - .map(|s| (s.id, s.head())) - .collect::>(); - - let selection_end_rows: BTreeSet = selections - .into_iter() - .map(|selection| selection.end.row) - .collect(); - let edits = selection_end_rows - .into_iter() - .map(|row| { - let end_of_line = Point::new(row, snapshot.line_len(MultiBufferRow(row))); - (end_of_line..end_of_line, "\n".repeat(count)) - }) - .collect::>(); - editor.edit(edits, cx); - - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|_, selection| { - if let Some(position) = original_positions.get(&selection.id) { - selection.collapse_to(*position, SelectionGoal::None); - } - }); - }); - }); - }); - } - - fn join_lines_impl( - &mut self, - insert_whitespace: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.record_current_action(cx); - let mut times = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - if self.mode.is_visual() { - times = 1; - } else if times > 1 { - // 2J joins two lines together (same as J or 1J) - times -= 1; - } - - self.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - for _ in 0..times { - editor.join_lines_impl(insert_whitespace, window, cx) - } - }) - }); - if self.mode.is_visual() { - self.switch_mode(Mode::Normal, true, window, cx) - } - } - - fn yank_line(&mut self, _: &YankLine, window: &mut Window, cx: &mut Context) { - let count = Vim::take_count(cx); - let forced_motion = Vim::take_forced_motion(cx); - self.yank_motion( - motion::Motion::CurrentLine, - count, - forced_motion, - window, - cx, - ) - } - - fn yank_to_end_of_line( - &mut self, - _: &YankToEndOfLine, - window: &mut Window, - cx: &mut Context, - ) { - let count = Vim::take_count(cx); - let forced_motion = Vim::take_forced_motion(cx); - self.yank_motion( - motion::Motion::EndOfLine { - display_lines: false, - }, - count, - forced_motion, - window, - cx, - ) - } - - fn show_location(&mut self, _: &ShowLocation, _: &mut Window, cx: &mut Context) { - let count = Vim::take_count(cx); - Vim::take_forced_motion(cx); - self.update_editor(cx, |vim, editor, cx| { - let selection = editor.selections.newest_anchor(); - let Some((buffer, point, _)) = editor - .buffer() - .read(cx) - .point_to_buffer_point(selection.head(), cx) - else { - return; - }; - let filename = if let Some(file) = buffer.read(cx).file() { - if count.is_some() { - if let Some(local) = file.as_local() { - local.abs_path(cx).to_string_lossy().into_owned() - } else { - file.full_path(cx).to_string_lossy().into_owned() - } - } else { - file.path().display(file.path_style(cx)).into_owned() - } - } else { - "[No Name]".into() - }; - let buffer = buffer.read(cx); - let lines = buffer.max_point().row + 1; - let current_line = point.row; - let percentage = current_line as f32 / lines as f32; - let modified = if buffer.is_dirty() { " [modified]" } else { "" }; - vim.status_label = Some( - format!( - "{}{} {} lines --{:.0}%--", - filename, - modified, - lines, - percentage * 100.0, - ) - .into(), - ); - cx.notify(); - }); - } - - fn toggle_comments(&mut self, _: &ToggleComments, window: &mut Window, cx: &mut Context) { - self.record_current_action(cx); - self.store_visual_marks(window, cx); - self.update_editor(cx, |vim, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - let original_positions = vim.save_selection_starts(editor, cx); - editor.toggle_comments(&Default::default(), window, cx); - vim.restore_selection_cursors(editor, window, cx, original_positions); - }); - }); - if self.mode.is_visual() { - self.switch_mode(Mode::Normal, true, window, cx) - } - } - - pub(crate) fn normal_replace( - &mut self, - text: Arc, - window: &mut Window, - cx: &mut Context, - ) { - // We need to use `text.chars().count()` instead of `text.len()` here as - // `len()` counts bytes, not characters. - let char_count = text.chars().count(); - let count = Vim::take_count(cx).unwrap_or(char_count); - let is_return_char = text == "\n".into() || text == "\r".into(); - let repeat_count = match (is_return_char, char_count) { - (true, _) => 0, - (_, 1) => count, - (_, _) => 1, - }; - - Vim::take_forced_motion(cx); - self.stop_recording(cx); - self.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - editor.set_clip_at_line_ends(false, cx); - let display_map = editor.display_snapshot(cx); - let display_selections = editor.selections.all_display(&display_map); - - let mut edits = Vec::with_capacity(display_selections.len()); - for selection in &display_selections { - let mut range = selection.range(); - for _ in 0..count { - let new_point = movement::saturating_right(&display_map, range.end); - if range.end == new_point { - return; - } - range.end = new_point; - } - - edits.push(( - range.start.to_offset(&display_map, Bias::Left) - ..range.end.to_offset(&display_map, Bias::Left), - text.repeat(repeat_count), - )); - } - - editor.edit(edits, cx); - if is_return_char { - editor.newline(&editor::actions::Newline, window, cx); - } - editor.set_clip_at_line_ends(true, cx); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let point = movement::saturating_left(map, selection.head()); - selection.collapse_to(point, SelectionGoal::None) - }); - }); - }); - }); - self.pop_operator(window, cx); - } - - pub fn save_selection_starts( - &self, - editor: &Editor, - cx: &mut Context, - ) -> HashMap { - let display_map = editor.display_snapshot(cx); - let selections = editor.selections.all_display(&display_map); - selections - .iter() - .map(|selection| { - ( - selection.id, - display_map.display_point_to_anchor(selection.start, Bias::Right), - ) - }) - .collect::>() - } - - pub fn restore_selection_cursors( - &self, - editor: &mut Editor, - window: &mut Window, - cx: &mut Context, - mut positions: HashMap, - ) { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - if let Some(anchor) = positions.remove(&selection.id) { - selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); - } - }); - }); - } - - fn exit_temporary_normal(&mut self, window: &mut Window, cx: &mut Context) { - if self.temp_mode { - self.switch_mode(Mode::Insert, true, window, cx); - } - } -} - -#[cfg(test)] -mod test { - use gpui::{KeyBinding, TestAppContext, UpdateGlobal}; - use indoc::indoc; - use settings::SettingsStore; - - use crate::{ - motion, - state::Mode::{self}, - test::{NeovimBackedTestContext, VimTestContext}, - }; - - #[gpui::test] - async fn test_h(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset( - "h", - indoc! {" - ˇThe qˇuick - ˇbrown" - }, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_backspace(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset( - "backspace", - indoc! {" - ˇThe qˇuick - ˇbrown" - }, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_j(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - aaˇaa - 😃😃" - }) - .await; - cx.simulate_shared_keystrokes("j").await; - cx.shared_state().await.assert_eq(indoc! {" - aaaa - 😃ˇ😃" - }); - - cx.simulate_at_each_offset( - "j", - indoc! {" - ˇThe qˇuick broˇwn - ˇfox jumps" - }, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_enter(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset( - "enter", - indoc! {" - ˇThe qˇuick broˇwn - ˇfox jumps" - }, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_k(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset( - "k", - indoc! {" - ˇThe qˇuick - ˇbrown fˇox jumˇps" - }, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_l(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset( - "l", - indoc! {" - ˇThe qˇuicˇk - ˇbrowˇn"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_jump_to_line_boundaries(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset( - "$", - indoc! {" - ˇThe qˇuicˇk - ˇbrowˇn"}, - ) - .await - .assert_matches(); - cx.simulate_at_each_offset( - "0", - indoc! {" - ˇThe qˇuicˇk - ˇbrowˇn"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_jump_to_end(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.simulate_at_each_offset( - "shift-g", - indoc! {" - The ˇquick - - brown fox jumps - overˇ the lazy doˇg"}, - ) - .await - .assert_matches(); - cx.simulate( - "shift-g", - indoc! {" - The quiˇck - - brown"}, - ) - .await - .assert_matches(); - cx.simulate( - "shift-g", - indoc! {" - The quiˇck - - "}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_w(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset( - "w", - indoc! {" - The ˇquickˇ-ˇbrown - ˇ - ˇ - ˇfox_jumps ˇover - ˇthˇe"}, - ) - .await - .assert_matches(); - cx.simulate_at_each_offset( - "shift-w", - indoc! {" - The ˇquickˇ-ˇbrown - ˇ - ˇ - ˇfox_jumps ˇover - ˇthˇe"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_end_of_word(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset( - "e", - indoc! {" - Thˇe quicˇkˇ-browˇn - - - fox_jumpˇs oveˇr - thˇe"}, - ) - .await - .assert_matches(); - cx.simulate_at_each_offset( - "shift-e", - indoc! {" - Thˇe quicˇkˇ-browˇn - - - fox_jumpˇs oveˇr - thˇe"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_b(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset( - "b", - indoc! {" - ˇThe ˇquickˇ-ˇbrown - ˇ - ˇ - ˇfox_jumps ˇover - ˇthe"}, - ) - .await - .assert_matches(); - cx.simulate_at_each_offset( - "shift-b", - indoc! {" - ˇThe ˇquickˇ-ˇbrown - ˇ - ˇ - ˇfox_jumps ˇover - ˇthe"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_gg(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset( - "g g", - indoc! {" - The qˇuick - - brown fox jumps - over ˇthe laˇzy dog"}, - ) - .await - .assert_matches(); - cx.simulate( - "g g", - indoc! {" - - - brown fox jumps - over the laˇzy dog"}, - ) - .await - .assert_matches(); - cx.simulate( - "2 g g", - indoc! {" - ˇ - - brown fox jumps - over the lazydog"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_end_of_document(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset( - "shift-g", - indoc! {" - The qˇuick - - brown fox jumps - over ˇthe laˇzy dog"}, - ) - .await - .assert_matches(); - cx.simulate( - "shift-g", - indoc! {" - - - brown fox jumps - over the laˇzy dog"}, - ) - .await - .assert_matches(); - cx.simulate( - "2 shift-g", - indoc! {" - ˇ - - brown fox jumps - over the lazydog"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_a(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset("a", "The qˇuicˇk") - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_insert_end_of_line(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset( - "shift-a", - indoc! {" - ˇ - The qˇuick - brown ˇfox "}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_jump_to_first_non_whitespace(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("^", "The qˇuick").await.assert_matches(); - cx.simulate("^", " The qˇuick").await.assert_matches(); - cx.simulate("^", "ˇ").await.assert_matches(); - cx.simulate( - "^", - indoc! {" - The qˇuick - brown fox"}, - ) - .await - .assert_matches(); - cx.simulate( - "^", - indoc! {" - ˇ - The quick"}, - ) - .await - .assert_matches(); - // Indoc disallows trailing whitespace. - cx.simulate("^", " ˇ \nThe quick").await.assert_matches(); - } - - #[gpui::test] - async fn test_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("shift-i", "The qˇuick").await.assert_matches(); - cx.simulate("shift-i", " The qˇuick").await.assert_matches(); - cx.simulate("shift-i", "ˇ").await.assert_matches(); - cx.simulate( - "shift-i", - indoc! {" - The qˇuick - brown fox"}, - ) - .await - .assert_matches(); - cx.simulate( - "shift-i", - indoc! {" - ˇ - The quick"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_to_end_of_line(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate( - "shift-d", - indoc! {" - The qˇuick - brown fox"}, - ) - .await - .assert_matches(); - cx.simulate( - "shift-d", - indoc! {" - The quick - ˇ - brown fox"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_x(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset("x", "ˇTeˇsˇt") - .await - .assert_matches(); - cx.simulate( - "x", - indoc! {" - Tesˇt - test"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_left(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset("shift-x", "ˇTˇeˇsˇt") - .await - .assert_matches(); - cx.simulate( - "shift-x", - indoc! {" - Test - ˇtest"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_o(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("o", "ˇ").await.assert_matches(); - cx.simulate("o", "The ˇquick").await.assert_matches(); - cx.simulate_at_each_offset( - "o", - indoc! {" - The qˇuick - brown ˇfox - jumps ˇover"}, - ) - .await - .assert_matches(); - cx.simulate( - "o", - indoc! {" - The quick - ˇ - brown fox"}, - ) - .await - .assert_matches(); - - cx.assert_binding( - "o", - indoc! {" - fn test() { - println!(ˇ); - }"}, - Mode::Normal, - indoc! {" - fn test() { - println!(); - ˇ - }"}, - Mode::Insert, - ); - - cx.assert_binding( - "o", - indoc! {" - fn test(ˇ) { - println!(); - }"}, - Mode::Normal, - indoc! {" - fn test() { - ˇ - println!(); - }"}, - Mode::Insert, - ); - } - - #[gpui::test] - async fn test_insert_line_above(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("shift-o", "ˇ").await.assert_matches(); - cx.simulate("shift-o", "The ˇquick").await.assert_matches(); - cx.simulate_at_each_offset( - "shift-o", - indoc! {" - The qˇuick - brown ˇfox - jumps ˇover"}, - ) - .await - .assert_matches(); - cx.simulate( - "shift-o", - indoc! {" - The quick - ˇ - brown fox"}, - ) - .await - .assert_matches(); - - // Our indentation is smarter than vims. So we don't match here - cx.assert_binding( - "shift-o", - indoc! {" - fn test() { - println!(ˇ); - }"}, - Mode::Normal, - indoc! {" - fn test() { - ˇ - println!(); - }"}, - Mode::Insert, - ); - cx.assert_binding( - "shift-o", - indoc! {" - fn test(ˇ) { - println!(); - }"}, - Mode::Normal, - indoc! {" - ˇ - fn test() { - println!(); - }"}, - Mode::Insert, - ); - } - - #[gpui::test] - async fn test_insert_empty_line(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("[ space", "ˇ").await.assert_matches(); - cx.simulate("[ space", "The ˇquick").await.assert_matches(); - cx.simulate_at_each_offset( - "3 [ space", - indoc! {" - The qˇuick - brown ˇfox - jumps ˇover"}, - ) - .await - .assert_matches(); - cx.simulate_at_each_offset( - "[ space", - indoc! {" - The qˇuick - brown ˇfox - jumps ˇover"}, - ) - .await - .assert_matches(); - cx.simulate( - "[ space", - indoc! {" - The quick - ˇ - brown fox"}, - ) - .await - .assert_matches(); - - cx.simulate("] space", "ˇ").await.assert_matches(); - cx.simulate("] space", "The ˇquick").await.assert_matches(); - cx.simulate_at_each_offset( - "3 ] space", - indoc! {" - The qˇuick - brown ˇfox - jumps ˇover"}, - ) - .await - .assert_matches(); - cx.simulate_at_each_offset( - "] space", - indoc! {" - The qˇuick - brown ˇfox - jumps ˇover"}, - ) - .await - .assert_matches(); - cx.simulate( - "] space", - indoc! {" - The quick - ˇ - brown fox"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_dd(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("d d", "ˇ").await.assert_matches(); - cx.simulate("d d", "The ˇquick").await.assert_matches(); - cx.simulate_at_each_offset( - "d d", - indoc! {" - The qˇuick - brown ˇfox - jumps ˇover"}, - ) - .await - .assert_matches(); - cx.simulate( - "d d", - indoc! {" - The quick - ˇ - brown fox"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_cc(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("c c", "ˇ").await.assert_matches(); - cx.simulate("c c", "The ˇquick").await.assert_matches(); - cx.simulate_at_each_offset( - "c c", - indoc! {" - The quˇick - brown ˇfox - jumps ˇover"}, - ) - .await - .assert_matches(); - cx.simulate( - "c c", - indoc! {" - The quick - ˇ - brown fox"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_repeated_word(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - for count in 1..=5 { - cx.simulate_at_each_offset( - &format!("{count} w"), - indoc! {" - ˇThe quˇickˇ browˇn - ˇ - ˇfox ˇjumpsˇ-ˇoˇver - ˇthe lazy dog - "}, - ) - .await - .assert_matches(); - } - } - - #[gpui::test] - async fn test_h_through_unicode(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset("h", "Testˇ├ˇ──ˇ┐ˇTest") - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_f_and_t(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - for count in 1..=3 { - let test_case = indoc! {" - ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa - ˇ ˇbˇaaˇa ˇbˇbˇb - ˇ - ˇb - "}; - - cx.simulate_at_each_offset(&format!("{count} f b"), test_case) - .await - .assert_matches(); - - cx.simulate_at_each_offset(&format!("{count} t b"), test_case) - .await - .assert_matches(); - } - } - - #[gpui::test] - async fn test_capital_f_and_capital_t(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - let test_case = indoc! {" - ˇaaaˇbˇ ˇbˇ ˇbˇbˇ aˇaaˇbaaa - ˇ ˇbˇaaˇa ˇbˇbˇb - ˇ••• - ˇb - " - }; - - for count in 1..=3 { - cx.simulate_at_each_offset(&format!("{count} shift-f b"), test_case) - .await - .assert_matches(); - - cx.simulate_at_each_offset(&format!("{count} shift-t b"), test_case) - .await - .assert_matches(); - } - } - - #[gpui::test] - async fn test_f_and_t_smartcase(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |s| { - s.vim.get_or_insert_default().use_smartcase_find = Some(true); - }); - }); - - cx.assert_binding( - "f p", - indoc! {"ˇfmt.Println(\"Hello, World!\")"}, - Mode::Normal, - indoc! {"fmt.ˇPrintln(\"Hello, World!\")"}, - Mode::Normal, - ); - - cx.assert_binding( - "shift-f p", - indoc! {"fmt.Printlnˇ(\"Hello, World!\")"}, - Mode::Normal, - indoc! {"fmt.ˇPrintln(\"Hello, World!\")"}, - Mode::Normal, - ); - - cx.assert_binding( - "t p", - indoc! {"ˇfmt.Println(\"Hello, World!\")"}, - Mode::Normal, - indoc! {"fmtˇ.Println(\"Hello, World!\")"}, - Mode::Normal, - ); - - cx.assert_binding( - "shift-t p", - indoc! {"fmt.Printlnˇ(\"Hello, World!\")"}, - Mode::Normal, - indoc! {"fmt.Pˇrintln(\"Hello, World!\")"}, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_percent(cx: &mut TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇvaˇrˇ)ˇ;") - .await - .assert_matches(); - cx.simulate_at_each_offset("%", "ˇconsole.logˇ(ˇ'var', ˇ[ˇ1, ˇ2, 3ˇ]ˇ)ˇ;") - .await - .assert_matches(); - cx.simulate_at_each_offset("%", "let result = curried_funˇ(ˇ)ˇ(ˇ)ˇ;") - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_end_of_line_with_neovim(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - // goes to current line end - cx.set_shared_state(indoc! {"ˇaa\nbb\ncc"}).await; - cx.simulate_shared_keystrokes("$").await; - cx.shared_state().await.assert_eq("aˇa\nbb\ncc"); - - // goes to next line end - cx.simulate_shared_keystrokes("2 $").await; - cx.shared_state().await.assert_eq("aa\nbˇb\ncc"); - - // try to exceed the final line. - cx.simulate_shared_keystrokes("4 $").await; - cx.shared_state().await.assert_eq("aa\nbb\ncˇc"); - } - - #[gpui::test] - async fn test_subword_motions(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.update(|_, cx| { - cx.bind_keys(vec![ - KeyBinding::new( - "w", - motion::NextSubwordStart { - ignore_punctuation: false, - }, - Some("Editor && VimControl && !VimWaiting && !menu"), - ), - KeyBinding::new( - "b", - motion::PreviousSubwordStart { - ignore_punctuation: false, - }, - Some("Editor && VimControl && !VimWaiting && !menu"), - ), - KeyBinding::new( - "e", - motion::NextSubwordEnd { - ignore_punctuation: false, - }, - Some("Editor && VimControl && !VimWaiting && !menu"), - ), - KeyBinding::new( - "g e", - motion::PreviousSubwordEnd { - ignore_punctuation: false, - }, - Some("Editor && VimControl && !VimWaiting && !menu"), - ), - ]); - }); - - cx.assert_binding_normal("w", indoc! {"ˇassert_binding"}, indoc! {"assert_ˇbinding"}); - // Special case: In 'cw', 'w' acts like 'e' - cx.assert_binding( - "c w", - indoc! {"ˇassert_binding"}, - Mode::Normal, - indoc! {"ˇ_binding"}, - Mode::Insert, - ); - - cx.assert_binding_normal("e", indoc! {"ˇassert_binding"}, indoc! {"asserˇt_binding"}); - - cx.assert_binding_normal("b", indoc! {"assert_ˇbinding"}, indoc! {"ˇassert_binding"}); - - cx.assert_binding_normal( - "g e", - indoc! {"assert_bindinˇg"}, - indoc! {"asserˇt_binding"}, - ); - } - - #[gpui::test] - async fn test_r(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇhello\n").await; - cx.simulate_shared_keystrokes("r -").await; - cx.shared_state().await.assert_eq("ˇ-ello\n"); - - cx.set_shared_state("ˇhello\n").await; - cx.simulate_shared_keystrokes("3 r -").await; - cx.shared_state().await.assert_eq("--ˇ-lo\n"); - - cx.set_shared_state("ˇhello\n").await; - cx.simulate_shared_keystrokes("r - 2 l .").await; - cx.shared_state().await.assert_eq("-eˇ-lo\n"); - - cx.set_shared_state("ˇhello world\n").await; - cx.simulate_shared_keystrokes("2 r - f w .").await; - cx.shared_state().await.assert_eq("--llo -ˇ-rld\n"); - - cx.set_shared_state("ˇhello world\n").await; - cx.simulate_shared_keystrokes("2 0 r - ").await; - cx.shared_state().await.assert_eq("ˇhello world\n"); - - cx.set_shared_state(" helloˇ world\n").await; - cx.simulate_shared_keystrokes("r enter").await; - cx.shared_state().await.assert_eq(" hello\n ˇ world\n"); - - cx.set_shared_state(" helloˇ world\n").await; - cx.simulate_shared_keystrokes("2 r enter").await; - cx.shared_state().await.assert_eq(" hello\n ˇ orld\n"); - } - - #[gpui::test] - async fn test_gq(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_neovim_option("textwidth=5").await; - - cx.update(|_, cx| { - SettingsStore::update_global(cx, |settings, cx| { - settings.update_user_settings(cx, |settings| { - settings - .project - .all_languages - .defaults - .preferred_line_length = Some(5); - }); - }) - }); - - cx.set_shared_state("ˇth th th th th th\n").await; - cx.simulate_shared_keystrokes("g q q").await; - cx.shared_state().await.assert_eq("th th\nth th\nˇth th\n"); - - cx.set_shared_state("ˇth th th th th th\nth th th th th th\n") - .await; - cx.simulate_shared_keystrokes("v j g q").await; - cx.shared_state() - .await - .assert_eq("th th\nth th\nth th\nth th\nth th\nˇth th\n"); - } - - #[gpui::test] - async fn test_o_comment(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_neovim_option("filetype=rust").await; - - cx.set_shared_state("// helloˇ\n").await; - cx.simulate_shared_keystrokes("o").await; - cx.shared_state().await.assert_eq("// hello\n// ˇ\n"); - cx.simulate_shared_keystrokes("x escape shift-o").await; - cx.shared_state().await.assert_eq("// hello\n// ˇ\n// x\n"); - } - - #[gpui::test] - async fn test_yank_line_with_trailing_newline(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state("heˇllo\n").await; - cx.simulate_shared_keystrokes("y y p").await; - cx.shared_state().await.assert_eq("hello\nˇhello\n"); - } - - #[gpui::test] - async fn test_yank_line_without_trailing_newline(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state("heˇllo").await; - cx.simulate_shared_keystrokes("y y p").await; - cx.shared_state().await.assert_eq("hello\nˇhello"); - } - - #[gpui::test] - async fn test_yank_multiline_without_trailing_newline(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state("heˇllo\nhello").await; - cx.simulate_shared_keystrokes("2 y y p").await; - cx.shared_state() - .await - .assert_eq("hello\nˇhello\nhello\nhello"); - } - - #[gpui::test] - async fn test_dd_then_paste_without_trailing_newline(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state("heˇllo").await; - cx.simulate_shared_keystrokes("d d").await; - cx.shared_state().await.assert_eq("ˇ"); - cx.simulate_shared_keystrokes("p p").await; - cx.shared_state().await.assert_eq("\nhello\nˇhello"); - } - - #[gpui::test] - async fn test_visual_mode_insert_before_after(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("heˇllo").await; - cx.simulate_shared_keystrokes("v i w shift-i").await; - cx.shared_state().await.assert_eq("ˇhello"); - - cx.set_shared_state(indoc! {" - The quick brown - fox ˇjumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("shift-v shift-i").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - ˇfox jumps over - the lazy dog"}); - - cx.set_shared_state(indoc! {" - The quick brown - fox ˇjumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("shift-v shift-a").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox jˇumps over - the lazy dog"}); - } - - #[gpui::test] - async fn test_jump_list(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ˇfn a() { } - - - - - - fn b() { } - - - - - - fn b() { }"}) - .await; - cx.simulate_shared_keystrokes("3 }").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("ctrl-o").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("ctrl-i").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("1 1 k").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("ctrl-o").await; - cx.shared_state().await.assert_matches(); - } - - #[gpui::test] - async fn test_undo_last_line(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ˇfn a() { } - fn a() { } - fn a() { } - "}) - .await; - // do a jump to reset vim's undo grouping - cx.simulate_shared_keystrokes("shift-g").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("r a").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("shift-u").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("shift-u").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("g g shift-u").await; - cx.shared_state().await.assert_matches(); - } - - #[gpui::test] - async fn test_undo_last_line_newline(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ˇfn a() { } - fn a() { } - fn a() { } - "}) - .await; - // do a jump to reset vim's undo grouping - cx.simulate_shared_keystrokes("shift-g k").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("o h e l l o escape").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("shift-u").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("shift-u").await; - } - - #[gpui::test] - async fn test_undo_last_line_newline_many_changes(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ˇfn a() { } - fn a() { } - fn a() { } - "}) - .await; - // do a jump to reset vim's undo grouping - cx.simulate_shared_keystrokes("x shift-g k").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("x f a x f { x").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("shift-u").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("shift-u").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("shift-u").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("shift-u").await; - cx.shared_state().await.assert_matches(); - } - - #[gpui::test] - async fn test_undo_last_line_multicursor(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! {" - ˇone two ˇone - two ˇone two - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("3 r a"); - cx.assert_state( - indoc! {" - aaˇa two aaˇa - two aaˇa two - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("escape escape"); - cx.simulate_keystrokes("shift-u"); - cx.set_state( - indoc! {" - onˇe two onˇe - two onˇe two - "}, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_go_to_tab_with_count(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // Open 4 tabs. - cx.simulate_keystrokes(": tabnew"); - cx.simulate_keystrokes("enter"); - cx.simulate_keystrokes(": tabnew"); - cx.simulate_keystrokes("enter"); - cx.simulate_keystrokes(": tabnew"); - cx.simulate_keystrokes("enter"); - cx.workspace(|workspace, _, cx| { - assert_eq!(workspace.items(cx).count(), 4); - assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3); - }); - - cx.simulate_keystrokes("1 g t"); - cx.workspace(|workspace, _, cx| { - assert_eq!(workspace.active_pane().read(cx).active_item_index(), 0); - }); - - cx.simulate_keystrokes("3 g t"); - cx.workspace(|workspace, _, cx| { - assert_eq!(workspace.active_pane().read(cx).active_item_index(), 2); - }); - - cx.simulate_keystrokes("4 g t"); - cx.workspace(|workspace, _, cx| { - assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3); - }); - - cx.simulate_keystrokes("1 g t"); - cx.simulate_keystrokes("g t"); - cx.workspace(|workspace, _, cx| { - assert_eq!(workspace.active_pane().read(cx).active_item_index(), 1); - }); - } - - #[gpui::test] - async fn test_go_to_previous_tab_with_count(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // Open 4 tabs. - cx.simulate_keystrokes(": tabnew"); - cx.simulate_keystrokes("enter"); - cx.simulate_keystrokes(": tabnew"); - cx.simulate_keystrokes("enter"); - cx.simulate_keystrokes(": tabnew"); - cx.simulate_keystrokes("enter"); - cx.workspace(|workspace, _, cx| { - assert_eq!(workspace.items(cx).count(), 4); - assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3); - }); - - cx.simulate_keystrokes("2 g shift-t"); - cx.workspace(|workspace, _, cx| { - assert_eq!(workspace.active_pane().read(cx).active_item_index(), 1); - }); - - cx.simulate_keystrokes("g shift-t"); - cx.workspace(|workspace, _, cx| { - assert_eq!(workspace.active_pane().read(cx).active_item_index(), 0); - }); - - // Wraparound: gT from first tab should go to last. - cx.simulate_keystrokes("g shift-t"); - cx.workspace(|workspace, _, cx| { - assert_eq!(workspace.active_pane().read(cx).active_item_index(), 3); - }); - - cx.simulate_keystrokes("6 g shift-t"); - cx.workspace(|workspace, _, cx| { - assert_eq!(workspace.active_pane().read(cx).active_item_index(), 1); - }); - } - - #[gpui::test] - async fn test_temporary_mode(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - // Test jumping to the end of the line ($). - cx.set_shared_state(indoc! {"lorem ˇipsum"}).await; - cx.simulate_shared_keystrokes("i").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("ctrl-o $").await; - cx.shared_state().await.assert_eq(indoc! {"lorem ipsumˇ"}); - - // Test jumping to the next word. - cx.set_shared_state(indoc! {"loremˇ ipsum dolor"}).await; - cx.simulate_shared_keystrokes("a").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("a n d space ctrl-o w").await; - cx.shared_state() - .await - .assert_eq(indoc! {"lorem and ipsum ˇdolor"}); - - // Test yanking to end of line ($). - cx.set_shared_state(indoc! {"lorem ˇipsum dolor"}).await; - cx.simulate_shared_keystrokes("i").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("a n d space ctrl-o y $") - .await; - cx.shared_state() - .await - .assert_eq(indoc! {"lorem and ˇipsum dolor"}); - } -} diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs deleted file mode 100644 index b0b0bddae1..0000000000 --- a/crates/vim/src/normal/change.rs +++ /dev/null @@ -1,706 +0,0 @@ -use crate::{ - Vim, - motion::{self, Motion, MotionKind}, - object::Object, - state::Mode, -}; -use editor::{ - Bias, DisplayPoint, - display_map::{DisplaySnapshot, ToDisplayPoint}, - movement::TextLayoutDetails, -}; -use gpui::{Context, Window}; -use language::Selection; - -impl Vim { - pub fn change_motion( - &mut self, - motion: Motion, - times: Option, - forced_motion: bool, - window: &mut Window, - cx: &mut Context, - ) { - // Some motions ignore failure when switching to normal mode - let mut motion_kind = if matches!( - motion, - Motion::Left - | Motion::Right - | Motion::EndOfLine { .. } - | Motion::WrappingLeft - | Motion::StartOfLine { .. } - ) { - Some(MotionKind::Exclusive) - } else { - None - }; - self.update_editor(cx, |vim, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - editor.transact(window, cx, |editor, window, cx| { - // We are swapping to insert mode anyway. Just set the line end clipping behavior now - editor.set_clip_at_line_ends(false, cx); - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let kind = match motion { - Motion::NextWordStart { ignore_punctuation } - | Motion::NextSubwordStart { ignore_punctuation } => { - expand_changed_word_selection( - map, - selection, - times, - ignore_punctuation, - &text_layout_details, - motion == Motion::NextSubwordStart { ignore_punctuation }, - !matches!(motion, Motion::NextWordStart { .. }), - ) - } - _ => { - let kind = motion.expand_selection( - map, - selection, - times, - &text_layout_details, - forced_motion, - ); - if matches!( - motion, - Motion::CurrentLine | Motion::Down { .. } | Motion::Up { .. } - ) { - let mut start_offset = - selection.start.to_offset(map, Bias::Left); - let classifier = map - .buffer_snapshot() - .char_classifier_at(selection.start.to_point(map)); - for (ch, offset) in map.buffer_chars_at(start_offset) { - if ch == '\n' || !classifier.is_whitespace(ch) { - break; - } - start_offset = offset + ch.len_utf8(); - } - selection.start = start_offset.to_display_point(map); - } - kind - } - }; - if let Some(kind) = kind { - motion_kind.get_or_insert(kind); - } - }); - }); - if let Some(kind) = motion_kind { - vim.copy_selections_content(editor, kind, window, cx); - editor.insert("", window, cx); - editor.refresh_edit_prediction(true, false, window, cx); - } - }); - }); - - if motion_kind.is_some() { - self.switch_mode(Mode::Insert, false, window, cx) - } else { - self.switch_mode(Mode::Normal, false, window, cx) - } - } - - pub fn change_object( - &mut self, - object: Object, - around: bool, - times: Option, - window: &mut Window, - cx: &mut Context, - ) { - let mut objects_found = false; - self.update_editor(cx, |vim, editor, cx| { - // We are swapping to insert mode anyway. Just set the line end clipping behavior now - editor.set_clip_at_line_ends(false, cx); - editor.transact(window, cx, |editor, window, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - objects_found |= object.expand_selection(map, selection, around, times); - }); - }); - if objects_found { - let kind = match object.target_visual_mode(vim.mode, around) { - Mode::VisualLine => MotionKind::Linewise, - _ => MotionKind::Exclusive, - }; - vim.copy_selections_content(editor, kind, window, cx); - editor.insert("", window, cx); - editor.refresh_edit_prediction(true, false, window, cx); - } - }); - }); - - if objects_found { - self.switch_mode(Mode::Insert, false, window, cx); - } else { - self.switch_mode(Mode::Normal, false, window, cx); - } - } -} - -// From the docs https://vimdoc.sourceforge.net/htmldoc/motion.html -// Special case: "cw" and "cW" are treated like "ce" and "cE" if the cursor is -// on a non-blank. This is because "cw" is interpreted as change-word, and a -// word does not include the following white space. {Vi: "cw" when on a blank -// followed by other blanks changes only the first blank; this is probably a -// bug, because "dw" deletes all the blanks} -fn expand_changed_word_selection( - map: &DisplaySnapshot, - selection: &mut Selection, - times: Option, - ignore_punctuation: bool, - text_layout_details: &TextLayoutDetails, - use_subword: bool, - always_advance: bool, -) -> Option { - let is_in_word = || { - let classifier = map - .buffer_snapshot() - .char_classifier_at(selection.start.to_point(map)); - - map.buffer_chars_at(selection.head().to_offset(map, Bias::Left)) - .next() - .map(|(c, _)| !classifier.is_whitespace(c)) - .unwrap_or_default() - }; - if (times.is_none() || times.unwrap() == 1) && is_in_word() { - let next_char = map - .buffer_chars_at( - motion::next_char(map, selection.end, false).to_offset(map, Bias::Left), - ) - .next(); - match next_char { - Some((' ', _)) => selection.end = motion::next_char(map, selection.end, false), - _ => { - if use_subword { - selection.end = - motion::next_subword_end(map, selection.end, ignore_punctuation, 1, false); - } else { - selection.end = motion::next_word_end( - map, - selection.end, - ignore_punctuation, - 1, - false, - always_advance, - ); - } - selection.end = motion::next_char(map, selection.end, false); - } - } - Some(MotionKind::Inclusive) - } else { - let motion = if use_subword { - Motion::NextSubwordStart { ignore_punctuation } - } else { - Motion::NextWordStart { ignore_punctuation } - }; - motion.expand_selection(map, selection, times, text_layout_details, false) - } -} - -#[cfg(test)] -mod test { - use indoc::indoc; - - use crate::test::NeovimBackedTestContext; - - #[gpui::test] - async fn test_change_h(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("c h", "Teˇst").await.assert_matches(); - cx.simulate("c h", "Tˇest").await.assert_matches(); - cx.simulate("c h", "ˇTest").await.assert_matches(); - cx.simulate( - "c h", - indoc! {" - Test - ˇtest"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_change_backspace(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("c backspace", "Teˇst").await.assert_matches(); - cx.simulate("c backspace", "Tˇest").await.assert_matches(); - cx.simulate("c backspace", "ˇTest").await.assert_matches(); - cx.simulate( - "c backspace", - indoc! {" - Test - ˇtest"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_change_l(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("c l", "Teˇst").await.assert_matches(); - cx.simulate("c l", "Tesˇt").await.assert_matches(); - } - - #[gpui::test] - async fn test_change_w(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("c w", "Teˇst").await.assert_matches(); - cx.simulate("c w", "Tˇest test").await.assert_matches(); - cx.simulate("c w", "Testˇ test").await.assert_matches(); - cx.simulate("c w", "Tesˇt test").await.assert_matches(); - cx.simulate( - "c w", - indoc! {" - Test teˇst - test"}, - ) - .await - .assert_matches(); - cx.simulate( - "c w", - indoc! {" - Test tesˇt - test"}, - ) - .await - .assert_matches(); - cx.simulate( - "c w", - indoc! {" - Test test - ˇ - test"}, - ) - .await - .assert_matches(); - - cx.simulate("c shift-w", "Test teˇst-test test") - .await - .assert_matches(); - - // on last character of word, `cw` doesn't eat subsequent punctuation - // see https://github.com/zed-industries/zed/issues/35269 - cx.simulate("c w", "tesˇt-test").await.assert_matches(); - } - - #[gpui::test] - async fn test_change_e(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("c e", "Teˇst Test").await.assert_matches(); - cx.simulate("c e", "Tˇest test").await.assert_matches(); - cx.simulate( - "c e", - indoc! {" - Test teˇst - test"}, - ) - .await - .assert_matches(); - cx.simulate( - "c e", - indoc! {" - Test tesˇt - test"}, - ) - .await - .assert_matches(); - cx.simulate( - "c e", - indoc! {" - Test test - ˇ - test"}, - ) - .await - .assert_matches(); - - cx.simulate("c shift-e", "Test teˇst-test test") - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_change_b(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("c b", "Teˇst Test").await.assert_matches(); - cx.simulate("c b", "Test ˇtest").await.assert_matches(); - cx.simulate("c b", "Test1 test2 ˇtest3") - .await - .assert_matches(); - cx.simulate( - "c b", - indoc! {" - Test test - ˇtest"}, - ) - .await - .assert_matches(); - cx.simulate( - "c b", - indoc! {" - Test test - ˇ - test"}, - ) - .await - .assert_matches(); - - cx.simulate("c shift-b", "Test test-test ˇtest") - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_change_end_of_line(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate( - "c $", - indoc! {" - The qˇuick - brown fox"}, - ) - .await - .assert_matches(); - cx.simulate( - "c $", - indoc! {" - The quick - ˇ - brown fox"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_change_0(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.simulate( - "c 0", - indoc! {" - The qˇuick - brown fox"}, - ) - .await - .assert_matches(); - cx.simulate( - "c 0", - indoc! {" - The quick - ˇ - brown fox"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_change_k(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.simulate( - "c k", - indoc! {" - The quick - brown ˇfox - jumps over"}, - ) - .await - .assert_matches(); - cx.simulate( - "c k", - indoc! {" - The quick - brown fox - jumps ˇover"}, - ) - .await - .assert_matches(); - cx.simulate( - "c k", - indoc! {" - The qˇuick - brown fox - jumps over"}, - ) - .await - .assert_matches(); - cx.simulate( - "c k", - indoc! {" - ˇ - brown fox - jumps over"}, - ) - .await - .assert_matches(); - cx.simulate( - "c k", - indoc! {" - The quick - brown fox - ˇjumps over"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_change_j(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate( - "c j", - indoc! {" - The quick - brown ˇfox - jumps over"}, - ) - .await - .assert_matches(); - cx.simulate( - "c j", - indoc! {" - The quick - brown fox - jumps ˇover"}, - ) - .await - .assert_matches(); - cx.simulate( - "c j", - indoc! {" - The qˇuick - brown fox - jumps over"}, - ) - .await - .assert_matches(); - cx.simulate( - "c j", - indoc! {" - The quick - brown fox - ˇ"}, - ) - .await - .assert_matches(); - cx.simulate( - "c j", - indoc! {" - The quick - ˇbrown fox - jumps over"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_change_end_of_document(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate( - "c shift-g", - indoc! {" - The quick - brownˇ fox - jumps over - the lazy"}, - ) - .await - .assert_matches(); - cx.simulate( - "c shift-g", - indoc! {" - The quick - brownˇ fox - jumps over - the lazy"}, - ) - .await - .assert_matches(); - cx.simulate( - "c shift-g", - indoc! {" - The quick - brown fox - jumps over - the lˇazy"}, - ) - .await - .assert_matches(); - cx.simulate( - "c shift-g", - indoc! {" - The quick - brown fox - jumps over - ˇ"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_change_cc(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate( - "c c", - indoc! {" - The quick - brownˇ fox - jumps over - the lazy"}, - ) - .await - .assert_matches(); - - cx.simulate( - "c c", - indoc! {" - ˇThe quick - brown fox - jumps over - the lazy"}, - ) - .await - .assert_matches(); - - cx.simulate( - "c c", - indoc! {" - The quick - broˇwn fox - jumps over - the lazy"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_change_gg(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate( - "c g g", - indoc! {" - The quick - brownˇ fox - jumps over - the lazy"}, - ) - .await - .assert_matches(); - cx.simulate( - "c g g", - indoc! {" - The quick - brown fox - jumps over - the lˇazy"}, - ) - .await - .assert_matches(); - cx.simulate( - "c g g", - indoc! {" - The qˇuick - brown fox - jumps over - the lazy"}, - ) - .await - .assert_matches(); - cx.simulate( - "c g g", - indoc! {" - ˇ - brown fox - jumps over - the lazy"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_repeated_cj(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - for count in 1..=5 { - cx.simulate_at_each_offset( - &format!("c {count} j"), - indoc! {" - ˇThe quˇickˇ browˇn - ˇ - ˇfox ˇjumpsˇ-ˇoˇver - ˇthe lazy dog - "}, - ) - .await - .assert_matches(); - } - } - - #[gpui::test] - async fn test_repeated_cl(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - for count in 1..=5 { - cx.simulate_at_each_offset( - &format!("c {count} l"), - indoc! {" - ˇThe quˇickˇ browˇn - ˇ - ˇfox ˇjumpsˇ-ˇoˇver - ˇthe lazy dog - "}, - ) - .await - .assert_matches(); - } - } - - #[gpui::test] - async fn test_repeated_cb(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - for count in 1..=5 { - cx.simulate_at_each_offset( - &format!("c {count} b"), - indoc! {" - ˇThe quˇickˇ browˇn - ˇ - ˇfox ˇjumpsˇ-ˇoˇver - ˇthe lazy dog - "}, - ) - .await - .assert_matches() - } - } - - #[gpui::test] - async fn test_repeated_ce(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - for count in 1..=5 { - cx.simulate_at_each_offset( - &format!("c {count} e"), - indoc! {" - ˇThe quˇickˇ browˇn - ˇ - ˇfox ˇjumpsˇ-ˇoˇver - ˇthe lazy dog - "}, - ) - .await - .assert_matches(); - } - } -} diff --git a/crates/vim/src/normal/convert.rs b/crates/vim/src/normal/convert.rs deleted file mode 100644 index 0ee132a44d..0000000000 --- a/crates/vim/src/normal/convert.rs +++ /dev/null @@ -1,469 +0,0 @@ -use collections::HashMap; -use editor::{SelectionEffects, display_map::ToDisplayPoint}; -use gpui::{Context, Window}; -use language::{Bias, Point, SelectionGoal}; -use multi_buffer::MultiBufferRow; - -use crate::{ - Vim, - motion::Motion, - normal::{ChangeCase, ConvertToLowerCase, ConvertToRot13, ConvertToRot47, ConvertToUpperCase}, - object::Object, - state::Mode, -}; - -pub enum ConvertTarget { - LowerCase, - UpperCase, - OppositeCase, - Rot13, - Rot47, -} - -impl Vim { - pub fn convert_motion( - &mut self, - motion: Motion, - times: Option, - forced_motion: bool, - mode: ConvertTarget, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - self.update_editor(cx, |_, editor, cx| { - editor.set_clip_at_line_ends(false, cx); - let text_layout_details = editor.text_layout_details(window); - editor.transact(window, cx, |editor, window, cx| { - let mut selection_starts: HashMap<_, _> = Default::default(); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let anchor = map.display_point_to_anchor(selection.head(), Bias::Left); - selection_starts.insert(selection.id, anchor); - motion.expand_selection( - map, - selection, - times, - &text_layout_details, - forced_motion, - ); - }); - }); - match mode { - ConvertTarget::LowerCase => { - editor.convert_to_lower_case(&Default::default(), window, cx) - } - ConvertTarget::UpperCase => { - editor.convert_to_upper_case(&Default::default(), window, cx) - } - ConvertTarget::OppositeCase => { - editor.convert_to_opposite_case(&Default::default(), window, cx) - } - ConvertTarget::Rot13 => { - editor.convert_to_rot13(&Default::default(), window, cx) - } - ConvertTarget::Rot47 => { - editor.convert_to_rot47(&Default::default(), window, cx) - } - } - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let anchor = selection_starts.remove(&selection.id).unwrap(); - selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); - }); - }); - }); - editor.set_clip_at_line_ends(true, cx); - }); - } - - pub fn convert_object( - &mut self, - object: Object, - around: bool, - mode: ConvertTarget, - times: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - self.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - editor.set_clip_at_line_ends(false, cx); - let mut original_positions: HashMap<_, _> = Default::default(); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - object.expand_selection(map, selection, around, times); - original_positions.insert( - selection.id, - map.display_point_to_anchor(selection.start, Bias::Left), - ); - }); - }); - match mode { - ConvertTarget::LowerCase => { - editor.convert_to_lower_case(&Default::default(), window, cx) - } - ConvertTarget::UpperCase => { - editor.convert_to_upper_case(&Default::default(), window, cx) - } - ConvertTarget::OppositeCase => { - editor.convert_to_opposite_case(&Default::default(), window, cx) - } - ConvertTarget::Rot13 => { - editor.convert_to_rot13(&Default::default(), window, cx) - } - ConvertTarget::Rot47 => { - editor.convert_to_rot47(&Default::default(), window, cx) - } - } - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let anchor = original_positions.remove(&selection.id).unwrap(); - selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); - }); - }); - editor.set_clip_at_line_ends(true, cx); - }); - }); - } - - pub fn change_case(&mut self, _: &ChangeCase, window: &mut Window, cx: &mut Context) { - self.manipulate_text(window, cx, |c| { - if c.is_lowercase() { - c.to_uppercase().collect::>() - } else { - c.to_lowercase().collect::>() - } - }) - } - - pub fn convert_to_upper_case( - &mut self, - _: &ConvertToUpperCase, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_text(window, cx, |c| c.to_uppercase().collect::>()) - } - - pub fn convert_to_lower_case( - &mut self, - _: &ConvertToLowerCase, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_text(window, cx, |c| c.to_lowercase().collect::>()) - } - - pub fn convert_to_rot13( - &mut self, - _: &ConvertToRot13, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_text(window, cx, |c| { - vec![match c { - 'A'..='M' | 'a'..='m' => ((c as u8) + 13) as char, - 'N'..='Z' | 'n'..='z' => ((c as u8) - 13) as char, - _ => c, - }] - }) - } - - pub fn convert_to_rot47( - &mut self, - _: &ConvertToRot47, - window: &mut Window, - cx: &mut Context, - ) { - self.manipulate_text(window, cx, |c| { - let code_point = c as u32; - if code_point >= 33 && code_point <= 126 { - return vec![char::from_u32(33 + ((code_point + 14) % 94)).unwrap()]; - } - vec![c] - }) - } - - fn manipulate_text(&mut self, window: &mut Window, cx: &mut Context, transform: F) - where - F: Fn(char) -> Vec + Copy, - { - self.record_current_action(cx); - self.store_visual_marks(window, cx); - let count = Vim::take_count(cx).unwrap_or(1) as u32; - Vim::take_forced_motion(cx); - - self.update_editor(cx, |vim, editor, cx| { - let mut ranges = Vec::new(); - let mut cursor_positions = Vec::new(); - let snapshot = editor.buffer().read(cx).snapshot(cx); - for selection in editor.selections.all_adjusted(&editor.display_snapshot(cx)) { - match vim.mode { - Mode::Visual | Mode::VisualLine => { - ranges.push(selection.start..selection.end); - cursor_positions.push(selection.start..selection.start); - } - Mode::VisualBlock => { - ranges.push(selection.start..selection.end); - if cursor_positions.is_empty() { - cursor_positions.push(selection.start..selection.start); - } - } - - Mode::HelixNormal | Mode::HelixSelect => { - if selection.is_empty() { - // Handle empty selection by operating on single character - let start = selection.start; - let end = snapshot.clip_point(start + Point::new(0, 1), Bias::Right); - ranges.push(start..end); - cursor_positions.push(selection.start..selection.start); - } else { - ranges.push(selection.start..selection.end); - cursor_positions.push(selection.start..selection.end); - } - } - Mode::Insert | Mode::Normal | Mode::Replace => { - let start = selection.start; - let mut end = start; - for _ in 0..count { - end = snapshot.clip_point(end + Point::new(0, 1), Bias::Right); - } - ranges.push(start..end); - - if end.column == snapshot.line_len(MultiBufferRow(end.row)) - && end.column > 0 - { - end = snapshot.clip_point(end - Point::new(0, 1), Bias::Left); - } - cursor_positions.push(end..end) - } - } - } - editor.transact(window, cx, |editor, window, cx| { - for range in ranges.into_iter().rev() { - let snapshot = editor.buffer().read(cx).snapshot(cx); - let text = snapshot - .text_for_range(range.start..range.end) - .flat_map(|s| s.chars()) - .flat_map(transform) - .collect::(); - editor.edit([(range, text)], cx) - } - editor.change_selections(Default::default(), window, cx, |s| { - s.select_ranges(cursor_positions) - }) - }); - }); - if self.mode != Mode::HelixNormal { - self.switch_mode(Mode::Normal, true, window, cx) - } - } -} - -#[cfg(test)] -mod test { - use crate::test::VimTestContext; - - use crate::{state::Mode, test::NeovimBackedTestContext}; - - #[gpui::test] - async fn test_change_case(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state("ˇabC\n").await; - cx.simulate_shared_keystrokes("~").await; - cx.shared_state().await.assert_eq("AˇbC\n"); - cx.simulate_shared_keystrokes("2 ~").await; - cx.shared_state().await.assert_eq("ABˇc\n"); - - // works in visual mode - cx.set_shared_state("a😀C«dÉ1*fˇ»\n").await; - cx.simulate_shared_keystrokes("~").await; - cx.shared_state().await.assert_eq("a😀CˇDé1*F\n"); - - // works with multibyte characters - cx.simulate_shared_keystrokes("~").await; - cx.set_shared_state("aˇC😀é1*F\n").await; - cx.simulate_shared_keystrokes("4 ~").await; - cx.shared_state().await.assert_eq("ac😀É1ˇ*F\n"); - - // works with line selections - cx.set_shared_state("abˇC\n").await; - cx.simulate_shared_keystrokes("shift-v ~").await; - cx.shared_state().await.assert_eq("ˇABc\n"); - - // works in visual block mode - cx.set_shared_state("ˇaa\nbb\ncc").await; - cx.simulate_shared_keystrokes("ctrl-v j ~").await; - cx.shared_state().await.assert_eq("ˇAa\nBb\ncc"); - - // works with multiple cursors (zed only) - cx.set_state("aˇßcdˇe\n", Mode::Normal); - cx.simulate_keystrokes("~"); - cx.assert_state("aSSˇcdˇE\n", Mode::Normal); - } - - #[gpui::test] - async fn test_convert_to_upper_case(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - // works in visual mode - cx.set_shared_state("a😀C«dÉ1*fˇ»\n").await; - cx.simulate_shared_keystrokes("shift-u").await; - cx.shared_state().await.assert_eq("a😀CˇDÉ1*F\n"); - - // works with line selections - cx.set_shared_state("abˇC\n").await; - cx.simulate_shared_keystrokes("shift-v shift-u").await; - cx.shared_state().await.assert_eq("ˇABC\n"); - - // works in visual block mode - cx.set_shared_state("ˇaa\nbb\ncc").await; - cx.simulate_shared_keystrokes("ctrl-v j shift-u").await; - cx.shared_state().await.assert_eq("ˇAa\nBb\ncc"); - } - - #[gpui::test] - async fn test_convert_to_lower_case(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - // works in visual mode - cx.set_shared_state("A😀c«DÉ1*fˇ»\n").await; - cx.simulate_shared_keystrokes("u").await; - cx.shared_state().await.assert_eq("A😀cˇdé1*f\n"); - - // works with line selections - cx.set_shared_state("ABˇc\n").await; - cx.simulate_shared_keystrokes("shift-v u").await; - cx.shared_state().await.assert_eq("ˇabc\n"); - - // works in visual block mode - cx.set_shared_state("ˇAa\nBb\nCc").await; - cx.simulate_shared_keystrokes("ctrl-v j u").await; - cx.shared_state().await.assert_eq("ˇaa\nbb\nCc"); - } - - #[gpui::test] - async fn test_change_case_motion(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇabc def").await; - cx.simulate_shared_keystrokes("g shift-u w").await; - cx.shared_state().await.assert_eq("ˇABC def"); - - cx.simulate_shared_keystrokes("g u w").await; - cx.shared_state().await.assert_eq("ˇabc def"); - - cx.simulate_shared_keystrokes("g ~ w").await; - cx.shared_state().await.assert_eq("ˇABC def"); - - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq("ˇabc def"); - - cx.set_shared_state("abˇc def").await; - cx.simulate_shared_keystrokes("g ~ i w").await; - cx.shared_state().await.assert_eq("ˇABC def"); - - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq("ˇabc def"); - - cx.simulate_shared_keystrokes("g shift-u $").await; - cx.shared_state().await.assert_eq("ˇABC DEF"); - } - - #[gpui::test] - async fn test_change_case_motion_object(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("abc dˇef\n").await; - cx.simulate_shared_keystrokes("g shift-u i w").await; - cx.shared_state().await.assert_eq("abc ˇDEF\n"); - } - - #[gpui::test] - async fn test_convert_to_rot13(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - // works in visual mode - cx.set_shared_state("a😀C«dÉ1*fˇ»\n").await; - cx.simulate_shared_keystrokes("g ?").await; - cx.shared_state().await.assert_eq("a😀CˇqÉ1*s\n"); - - // works with line selections - cx.set_shared_state("abˇC\n").await; - cx.simulate_shared_keystrokes("shift-v g ?").await; - cx.shared_state().await.assert_eq("ˇnoP\n"); - - // works in visual block mode - cx.set_shared_state("ˇaa\nbb\ncc").await; - cx.simulate_shared_keystrokes("ctrl-v j g ?").await; - cx.shared_state().await.assert_eq("ˇna\nob\ncc"); - } - - #[gpui::test] - async fn test_change_rot13_motion(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇabc def").await; - cx.simulate_shared_keystrokes("g ? w").await; - cx.shared_state().await.assert_eq("ˇnop def"); - - cx.simulate_shared_keystrokes("g ? w").await; - cx.shared_state().await.assert_eq("ˇabc def"); - - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq("ˇnop def"); - - cx.set_shared_state("abˇc def").await; - cx.simulate_shared_keystrokes("g ? i w").await; - cx.shared_state().await.assert_eq("ˇnop def"); - - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq("ˇabc def"); - - cx.simulate_shared_keystrokes("g ? $").await; - cx.shared_state().await.assert_eq("ˇnop qrs"); - } - - #[gpui::test] - async fn test_change_rot13_object(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") - .await; - cx.simulate_shared_keystrokes("g ? i w").await; - cx.shared_state() - .await - .assert_eq("ˇnopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM"); - } - - #[gpui::test] - async fn test_change_case_helix_mode(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // Explicit selection - cx.set_state("«hello worldˇ»", Mode::HelixNormal); - cx.simulate_keystrokes("~"); - cx.assert_state("«HELLO WORLDˇ»", Mode::HelixNormal); - - // Cursor-only (empty) selection - switch case - cx.set_state("The ˇquick brown", Mode::HelixNormal); - cx.simulate_keystrokes("~"); - cx.assert_state("The ˇQuick brown", Mode::HelixNormal); - cx.simulate_keystrokes("~"); - cx.assert_state("The ˇquick brown", Mode::HelixNormal); - - // Cursor-only (empty) selection - switch to uppercase and lowercase explicitly - cx.set_state("The ˇquick brown", Mode::HelixNormal); - cx.simulate_keystrokes("alt-`"); - cx.assert_state("The ˇQuick brown", Mode::HelixNormal); - cx.simulate_keystrokes("`"); - cx.assert_state("The ˇquick brown", Mode::HelixNormal); - - // With `e` motion (which extends selection to end of word in Helix) - cx.set_state("The ˇquick brown fox", Mode::HelixNormal); - cx.simulate_keystrokes("e"); - cx.simulate_keystrokes("~"); - cx.assert_state("The «QUICKˇ» brown fox", Mode::HelixNormal); - - // Cursor-only - } -} diff --git a/crates/vim/src/normal/delete.rs b/crates/vim/src/normal/delete.rs deleted file mode 100644 index b1c41315a8..0000000000 --- a/crates/vim/src/normal/delete.rs +++ /dev/null @@ -1,763 +0,0 @@ -use crate::{ - Vim, - motion::{Motion, MotionKind}, - object::Object, - state::Mode, -}; -use collections::{HashMap, HashSet}; -use editor::{ - Bias, DisplayPoint, - display_map::{DisplaySnapshot, ToDisplayPoint}, -}; -use gpui::{Context, Window}; -use language::{Point, Selection}; -use multi_buffer::MultiBufferRow; - -impl Vim { - pub fn delete_motion( - &mut self, - motion: Motion, - times: Option, - forced_motion: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - self.update_editor(cx, |vim, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - editor.transact(window, cx, |editor, window, cx| { - editor.set_clip_at_line_ends(false, cx); - let mut original_columns: HashMap<_, _> = Default::default(); - let mut motion_kind = None; - let mut ranges_to_copy = Vec::new(); - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let original_head = selection.head(); - original_columns.insert(selection.id, original_head.column()); - let kind = motion.expand_selection( - map, - selection, - times, - &text_layout_details, - forced_motion, - ); - ranges_to_copy - .push(selection.start.to_point(map)..selection.end.to_point(map)); - - // When deleting line-wise, we always want to delete a newline. - // If there is one after the current line, it goes; otherwise we - // pick the one before. - if kind == Some(MotionKind::Linewise) { - let start = selection.start.to_point(map); - let end = selection.end.to_point(map); - if end.row < map.buffer_snapshot().max_point().row { - selection.end = Point::new(end.row + 1, 0).to_display_point(map) - } else if start.row > 0 { - selection.start = Point::new( - start.row - 1, - map.buffer_snapshot() - .line_len(MultiBufferRow(start.row - 1)), - ) - .to_display_point(map) - } - } - if let Some(kind) = kind { - motion_kind.get_or_insert(kind); - } - }); - }); - let Some(kind) = motion_kind else { return }; - vim.copy_ranges(editor, kind, false, ranges_to_copy, window, cx); - editor.insert("", window, cx); - - // Fixup cursor position after the deletion - editor.set_clip_at_line_ends(true, cx); - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let mut cursor = selection.head(); - if kind.linewise() - && let Some(column) = original_columns.get(&selection.id) - { - *cursor.column_mut() = *column - } - cursor = map.clip_point(cursor, Bias::Left); - selection.collapse_to(cursor, selection.goal) - }); - }); - editor.refresh_edit_prediction(true, false, window, cx); - }); - }); - } - - pub fn delete_object( - &mut self, - object: Object, - around: bool, - times: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - self.update_editor(cx, |vim, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - editor.set_clip_at_line_ends(false, cx); - // Emulates behavior in vim where if we expanded backwards to include a newline - // the cursor gets set back to the start of the line - let mut should_move_to_start: HashSet<_> = Default::default(); - - // Emulates behavior in vim where after deletion the cursor should try to move - // to the same column it was before deletion if the line is not empty or only - // contains whitespace - let mut column_before_move: HashMap<_, _> = Default::default(); - let target_mode = object.target_visual_mode(vim.mode, around); - - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let cursor_point = selection.head().to_point(map); - if target_mode == Mode::VisualLine { - column_before_move.insert(selection.id, cursor_point.column); - } - - object.expand_selection(map, selection, around, times); - let offset_range = selection.map(|p| p.to_offset(map, Bias::Left)).range(); - let mut move_selection_start_to_previous_line = - |map: &DisplaySnapshot, selection: &mut Selection| { - let start = selection.start.to_offset(map, Bias::Left); - if selection.start.row().0 > 0 { - should_move_to_start.insert(selection.id); - selection.start = - (start - '\n'.len_utf8()).to_display_point(map); - } - }; - let range = selection.start.to_offset(map, Bias::Left) - ..selection.end.to_offset(map, Bias::Right); - let contains_only_newlines = map - .buffer_chars_at(range.start) - .take_while(|(_, p)| p < &range.end) - .all(|(char, _)| char == '\n') - && !offset_range.is_empty(); - let end_at_newline = map - .buffer_chars_at(range.end) - .next() - .map(|(c, _)| c == '\n') - .unwrap_or(false); - - // If expanded range contains only newlines and - // the object is around or sentence, expand to include a newline - // at the end or start - if (around || object == Object::Sentence) && contains_only_newlines { - if end_at_newline { - move_selection_end_to_next_line(map, selection); - } else { - move_selection_start_to_previous_line(map, selection); - } - } - - // Does post-processing for the trailing newline and EOF - // when not cancelled. - let cancelled = around && selection.start == selection.end; - if object == Object::Paragraph && !cancelled { - // EOF check should be done before including a trailing newline. - if ends_at_eof(map, selection) { - move_selection_start_to_previous_line(map, selection); - } - - if end_at_newline { - move_selection_end_to_next_line(map, selection); - } - } - }); - }); - vim.copy_selections_content(editor, MotionKind::Exclusive, window, cx); - editor.insert("", window, cx); - - // Fixup cursor position after the deletion - editor.set_clip_at_line_ends(true, cx); - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let mut cursor = selection.head(); - if should_move_to_start.contains(&selection.id) { - *cursor.column_mut() = 0; - } else if let Some(column) = column_before_move.get(&selection.id) - && *column > 0 - { - let mut cursor_point = cursor.to_point(map); - cursor_point.column = *column; - cursor = map - .buffer_snapshot() - .clip_point(cursor_point, Bias::Left) - .to_display_point(map); - } - cursor = map.clip_point(cursor, Bias::Left); - selection.collapse_to(cursor, selection.goal) - }); - }); - editor.refresh_edit_prediction(true, false, window, cx); - }); - }); - } -} - -fn move_selection_end_to_next_line(map: &DisplaySnapshot, selection: &mut Selection) { - let end = selection.end.to_offset(map, Bias::Left); - selection.end = (end + '\n'.len_utf8()).to_display_point(map); -} - -fn ends_at_eof(map: &DisplaySnapshot, selection: &mut Selection) -> bool { - selection.end.to_point(map) == map.buffer_snapshot().max_point() -} - -#[cfg(test)] -mod test { - use indoc::indoc; - - use crate::{ - state::Mode, - test::{NeovimBackedTestContext, VimTestContext}, - }; - - #[gpui::test] - async fn test_delete_h(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("d h", "Teˇst").await.assert_matches(); - cx.simulate("d h", "Tˇest").await.assert_matches(); - cx.simulate("d h", "ˇTest").await.assert_matches(); - cx.simulate( - "d h", - indoc! {" - Test - ˇtest"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_l(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("d l", "ˇTest").await.assert_matches(); - cx.simulate("d l", "Teˇst").await.assert_matches(); - cx.simulate("d l", "Tesˇt").await.assert_matches(); - cx.simulate( - "d l", - indoc! {" - Tesˇt - test"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_w(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate( - "d w", - indoc! {" - Test tesˇt - test"}, - ) - .await - .assert_matches(); - - cx.simulate("d w", "Teˇst").await.assert_matches(); - cx.simulate("d w", "Tˇest test").await.assert_matches(); - cx.simulate( - "d w", - indoc! {" - Test teˇst - test"}, - ) - .await - .assert_matches(); - cx.simulate( - "d w", - indoc! {" - Test tesˇt - test"}, - ) - .await - .assert_matches(); - - cx.simulate( - "d w", - indoc! {" - Test test - ˇ - test"}, - ) - .await - .assert_matches(); - - cx.simulate("d shift-w", "Test teˇst-test test") - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_next_word_end(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("d e", "Teˇst Test\n").await.assert_matches(); - cx.simulate("d e", "Tˇest test\n").await.assert_matches(); - cx.simulate( - "d e", - indoc! {" - Test teˇst - test"}, - ) - .await - .assert_matches(); - cx.simulate( - "d e", - indoc! {" - Test tesˇt - test"}, - ) - .await - .assert_matches(); - - cx.simulate("d e", "Test teˇst-test test") - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_b(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("d b", "Teˇst Test").await.assert_matches(); - cx.simulate("d b", "Test ˇtest").await.assert_matches(); - cx.simulate("d b", "Test1 test2 ˇtest3") - .await - .assert_matches(); - cx.simulate( - "d b", - indoc! {" - Test test - ˇtest"}, - ) - .await - .assert_matches(); - cx.simulate( - "d b", - indoc! {" - Test test - ˇ - test"}, - ) - .await - .assert_matches(); - - cx.simulate("d shift-b", "Test test-test ˇtest") - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_end_of_line(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate( - "d $", - indoc! {" - The qˇuick - brown fox"}, - ) - .await - .assert_matches(); - cx.simulate( - "d $", - indoc! {" - The quick - ˇ - brown fox"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_end_of_paragraph(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate( - "d }", - indoc! {" - ˇhello world. - - hello world."}, - ) - .await - .assert_matches(); - - cx.simulate( - "d }", - indoc! {" - ˇhello world. - hello world."}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_0(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate( - "d 0", - indoc! {" - The qˇuick - brown fox"}, - ) - .await - .assert_matches(); - cx.simulate( - "d 0", - indoc! {" - The quick - ˇ - brown fox"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_k(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate( - "d k", - indoc! {" - The quick - brown ˇfox - jumps over"}, - ) - .await - .assert_matches(); - cx.simulate( - "d k", - indoc! {" - The quick - brown fox - jumps ˇover"}, - ) - .await - .assert_matches(); - cx.simulate( - "d k", - indoc! {" - The qˇuick - brown fox - jumps over"}, - ) - .await - .assert_matches(); - cx.simulate( - "d k", - indoc! {" - ˇbrown fox - jumps over"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_j(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate( - "d j", - indoc! {" - The quick - brown ˇfox - jumps over"}, - ) - .await - .assert_matches(); - cx.simulate( - "d j", - indoc! {" - The quick - brown fox - jumps ˇover"}, - ) - .await - .assert_matches(); - cx.simulate( - "d j", - indoc! {" - The qˇuick - brown fox - jumps over"}, - ) - .await - .assert_matches(); - cx.simulate( - "d j", - indoc! {" - The quick - brown fox - ˇ"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_end_of_document(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate( - "d shift-g", - indoc! {" - The quick - brownˇ fox - jumps over - the lazy"}, - ) - .await - .assert_matches(); - cx.simulate( - "d shift-g", - indoc! {" - The quick - brownˇ fox - jumps over - the lazy"}, - ) - .await - .assert_matches(); - cx.simulate( - "d shift-g", - indoc! {" - The quick - brown fox - jumps over - the lˇazy"}, - ) - .await - .assert_matches(); - cx.simulate( - "d shift-g", - indoc! {" - The quick - brown fox - jumps over - ˇ"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_to_line(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate( - "d 3 shift-g", - indoc! {" - The quick - brownˇ fox - jumps over - the lazy"}, - ) - .await - .assert_matches(); - cx.simulate( - "d 3 shift-g", - indoc! {" - The quick - brown fox - jumps over - the lˇazy"}, - ) - .await - .assert_matches(); - cx.simulate( - "d 2 shift-g", - indoc! {" - The quick - brown fox - jumps over - ˇ"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_gg(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate( - "d g g", - indoc! {" - The quick - brownˇ fox - jumps over - the lazy"}, - ) - .await - .assert_matches(); - cx.simulate( - "d g g", - indoc! {" - The quick - brown fox - jumps over - the lˇazy"}, - ) - .await - .assert_matches(); - cx.simulate( - "d g g", - indoc! {" - The qˇuick - brown fox - jumps over - the lazy"}, - ) - .await - .assert_matches(); - cx.simulate( - "d g g", - indoc! {" - ˇ - brown fox - jumps over - the lazy"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_cancel_delete_operator(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.set_state( - indoc! {" - The quick brown - fox juˇmps over - the lazy dog"}, - Mode::Normal, - ); - - // Canceling operator twice reverts to normal mode with no active operator - cx.simulate_keystrokes("d escape k"); - assert_eq!(cx.active_operator(), None); - assert_eq!(cx.mode(), Mode::Normal); - cx.assert_editor_state(indoc! {" - The quˇick brown - fox jumps over - the lazy dog"}); - } - - #[gpui::test] - async fn test_unbound_command_cancels_pending_operator(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.set_state( - indoc! {" - The quick brown - fox juˇmps over - the lazy dog"}, - Mode::Normal, - ); - - // Canceling operator twice reverts to normal mode with no active operator - cx.simulate_keystrokes("d y"); - assert_eq!(cx.active_operator(), None); - assert_eq!(cx.mode(), Mode::Normal); - } - - #[gpui::test] - async fn test_delete_with_counts(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! {" - The ˇquick brown - fox jumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("d 2 d").await; - cx.shared_state().await.assert_eq(indoc! {" - the ˇlazy dog"}); - - cx.set_shared_state(indoc! {" - The ˇquick brown - fox jumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("2 d d").await; - cx.shared_state().await.assert_eq(indoc! {" - the ˇlazy dog"}); - - cx.set_shared_state(indoc! {" - The ˇquick brown - fox jumps over - the moon, - a star, and - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("2 d 2 d").await; - cx.shared_state().await.assert_eq(indoc! {" - the ˇlazy dog"}); - } - - #[gpui::test] - async fn test_delete_to_adjacent_character(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate("d t x", "ˇax").await.assert_matches(); - cx.simulate("d t x", "aˇx").await.assert_matches(); - } - - #[gpui::test] - async fn test_delete_sentence(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - // cx.simulate( - // "d )", - // indoc! {" - // Fiˇrst. Second. Third. - // Fourth. - // "}, - // ) - // .await - // .assert_matches(); - - // cx.simulate( - // "d )", - // indoc! {" - // First. Secˇond. Third. - // Fourth. - // "}, - // ) - // .await - // .assert_matches(); - - // // Two deletes - // cx.simulate( - // "d ) d )", - // indoc! {" - // First. Second. Thirˇd. - // Fourth. - // "}, - // ) - // .await - // .assert_matches(); - - // Should delete whole line if done on first column - cx.simulate( - "d )", - indoc! {" - ˇFirst. - Fourth. - "}, - ) - .await - .assert_matches(); - - // Backwards it should also delete the whole first line - cx.simulate( - "d (", - indoc! {" - First. - ˇSecond. - Fourth. - "}, - ) - .await - .assert_matches(); - } -} diff --git a/crates/vim/src/normal/increment.rs b/crates/vim/src/normal/increment.rs deleted file mode 100644 index d9ef32deba..0000000000 --- a/crates/vim/src/normal/increment.rs +++ /dev/null @@ -1,849 +0,0 @@ -use editor::{Editor, MultiBufferSnapshot, ToOffset, ToPoint}; -use gpui::{Action, Context, Window}; -use language::{Bias, Point}; -use schemars::JsonSchema; -use serde::Deserialize; -use std::ops::Range; - -use crate::{Vim, state::Mode}; - -const BOOLEAN_PAIRS: &[(&str, &str)] = &[("true", "false"), ("yes", "no"), ("on", "off")]; - -/// Increments the number under the cursor or toggles boolean values. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct Increment { - #[serde(default)] - step: bool, -} - -/// Decrements the number under the cursor or toggles boolean values. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct Decrement { - #[serde(default)] - step: bool, -} - -pub fn register(editor: &mut Editor, cx: &mut Context) { - Vim::action(editor, cx, |vim, action: &Increment, window, cx| { - vim.record_current_action(cx); - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - let step = if action.step { count as i32 } else { 0 }; - vim.increment(count as i64, step, window, cx) - }); - Vim::action(editor, cx, |vim, action: &Decrement, window, cx| { - vim.record_current_action(cx); - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - let step = if action.step { -1 * (count as i32) } else { 0 }; - vim.increment(-(count as i64), step, window, cx) - }); -} - -impl Vim { - fn increment( - &mut self, - mut delta: i64, - step: i32, - window: &mut Window, - cx: &mut Context, - ) { - self.store_visual_marks(window, cx); - self.update_editor(cx, |vim, editor, cx| { - let mut edits = Vec::new(); - let mut new_anchors = Vec::new(); - - let snapshot = editor.buffer().read(cx).snapshot(cx); - for selection in editor.selections.all_adjusted(&editor.display_snapshot(cx)) { - if !selection.is_empty() - && (vim.mode != Mode::VisualBlock || new_anchors.is_empty()) - { - new_anchors.push((true, snapshot.anchor_before(selection.start))) - } - for row in selection.start.row..=selection.end.row { - let start = if row == selection.start.row { - selection.start - } 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 find_result = if !selection.is_empty() { - find_target(&snapshot, start, end, true) - } else { - find_target(&snapshot, start, end, false) - }; - - if let Some((range, target, radix)) = find_result { - let replace = match radix { - 10 => increment_decimal_string(&target, delta), - 16 => increment_hex_string(&target, delta), - 2 => increment_binary_string(&target, delta), - 0 => increment_toggle_string(&target), - _ => unreachable!(), - }; - delta += step as i64; - edits.push((range.clone(), replace)); - if selection.is_empty() { - new_anchors.push((false, snapshot.anchor_after(range.end))) - } - } else if selection.is_empty() { - new_anchors.push((true, snapshot.anchor_after(start))) - } - } - } - editor.transact(window, cx, |editor, window, cx| { - editor.edit(edits, cx); - - let snapshot = editor.buffer().read(cx).snapshot(cx); - editor.change_selections(Default::default(), window, cx, |s| { - let mut new_ranges = Vec::new(); - for (visual, anchor) in new_anchors.iter() { - let mut point = anchor.to_point(&snapshot); - if !*visual && point.column > 0 { - point.column -= 1; - point = snapshot.clip_point(point, Bias::Left) - } - new_ranges.push(point..point); - } - s.select_ranges(new_ranges) - }) - }); - }); - self.switch_mode(Mode::Normal, true, window, cx) - } -} - -fn increment_decimal_string(num: &str, delta: i64) -> String { - let (negative, delta, num_str) = match num.strip_prefix('-') { - Some(n) => (true, -delta, n), - None => (false, delta, num), - }; - let num_length = num_str.len(); - let leading_zero = num_str.starts_with('0'); - - let (result, new_negative) = match u64::from_str_radix(num_str, 10) { - Ok(value) => { - let wrapped = value.wrapping_add_signed(delta); - if delta < 0 && wrapped > value { - ((u64::MAX - wrapped).wrapping_add(1), !negative) - } else if delta > 0 && wrapped < value { - (u64::MAX - wrapped, !negative) - } else { - (wrapped, negative) - } - } - Err(_) => (u64::MAX, negative), - }; - - let formatted = format!("{}", result); - let new_significant_digits = formatted.len(); - let padding = if leading_zero { - num_length.saturating_sub(new_significant_digits) - } else { - 0 - }; - - if new_negative && result != 0 { - format!("-{}{}", "0".repeat(padding), formatted) - } else { - format!("{}{}", "0".repeat(padding), formatted) - } -} - -fn increment_hex_string(num: &str, delta: i64) -> String { - let result = if let Ok(val) = u64::from_str_radix(num, 16) { - val.wrapping_add_signed(delta) - } else { - u64::MAX - }; - if should_use_lowercase(num) { - format!("{:0width$x}", result, width = num.len()) - } else { - format!("{:0width$X}", result, width = num.len()) - } -} - -fn should_use_lowercase(num: &str) -> bool { - let mut use_uppercase = false; - for ch in num.chars() { - if ch.is_ascii_lowercase() { - return true; - } - if ch.is_ascii_uppercase() { - use_uppercase = true; - } - } - !use_uppercase -} - -fn increment_binary_string(num: &str, delta: i64) -> String { - let result = if let Ok(val) = u64::from_str_radix(num, 2) { - val.wrapping_add_signed(delta) - } else { - u64::MAX - }; - format!("{:0width$b}", result, width = num.len()) -} - -fn find_target( - snapshot: &MultiBufferSnapshot, - start: Point, - end: Point, - need_range: bool, -) -> Option<(Range, String, u32)> { - let start_offset = start.to_offset(snapshot); - let end_offset = end.to_offset(snapshot); - - let mut offset = start_offset; - let mut first_char_is_num = snapshot - .chars_at(offset) - .next() - .map_or(false, |ch| ch.is_ascii_hexdigit()); - let mut pre_char = String::new(); - - let next_offset = offset - + snapshot - .chars_at(start_offset) - .next() - .map_or(0, |ch| ch.len_utf8()); - // Backward scan to find the start of the number, but stop at start_offset - for ch in snapshot.reversed_chars_at(next_offset) { - // Search boundaries - if offset.0 == 0 || ch.is_whitespace() || (need_range && offset <= start_offset) { - break; - } - - // Avoid the influence of hexadecimal letters - if first_char_is_num - && !ch.is_ascii_hexdigit() - && (ch != 'b' && ch != 'B') - && (ch != 'x' && ch != 'X') - && ch != '-' - { - // Used to determine if the initial character is a number. - if is_numeric_string(&pre_char) { - break; - } else { - first_char_is_num = false; - } - } - - pre_char.insert(0, ch); - offset -= ch.len_utf8(); - } - - let mut begin = None; - let mut end = None; - let mut target = String::new(); - let mut radix = 10; - let mut is_num = false; - - let mut chars = snapshot.chars_at(offset).peekable(); - - while let Some(ch) = chars.next() { - if need_range && offset >= end_offset { - break; // stop at end of selection - } - - if target == "0" - && (ch == 'b' || ch == 'B') - && chars.peek().is_some() - && chars.peek().unwrap().is_digit(2) - { - radix = 2; - begin = None; - target = String::new(); - } else if target == "0" - && (ch == 'x' || ch == 'X') - && chars.peek().is_some() - && chars.peek().unwrap().is_ascii_hexdigit() - { - radix = 16; - begin = None; - target = String::new(); - } else if ch == '.' { - is_num = false; - begin = None; - target = String::new(); - } else if ch.is_digit(radix) - || ((begin.is_none() || !is_num) - && ch == '-' - && chars.peek().is_some() - && chars.peek().unwrap().is_digit(radix)) - { - if !is_num { - is_num = true; - begin = Some(offset); - target = String::new(); - } else if begin.is_none() { - begin = Some(offset); - } - target.push(ch); - } else if ch.is_ascii_alphabetic() && !is_num { - if begin.is_none() { - begin = Some(offset); - } - target.push(ch); - } else if begin.is_some() && (is_num || !is_num && is_toggle_word(&target)) { - // End of matching - end = Some(offset); - break; - } else if ch == '\n' { - break; - } else { - // To match the next word - is_num = false; - begin = None; - target = String::new(); - } - - offset += ch.len_utf8(); - } - - if let Some(begin) = begin - && (is_num || !is_num && is_toggle_word(&target)) - { - if !is_num { - radix = 0; - } - - let end = end.unwrap_or(offset); - Some(( - begin.to_point(snapshot)..end.to_point(snapshot), - target, - radix, - )) - } else { - None - } -} - -fn is_numeric_string(s: &str) -> bool { - if s.is_empty() { - return false; - } - - let (_, rest) = if let Some(r) = s.strip_prefix('-') { - (true, r) - } else { - (false, s) - }; - - if rest.is_empty() { - return false; - } - - if let Some(digits) = rest.strip_prefix("0b").or_else(|| rest.strip_prefix("0B")) { - digits.is_empty() || digits.chars().all(|c| c == '0' || c == '1') - } else if let Some(digits) = rest.strip_prefix("0x").or_else(|| rest.strip_prefix("0X")) { - digits.is_empty() || digits.chars().all(|c| c.is_ascii_hexdigit()) - } else { - !rest.is_empty() && rest.chars().all(|c| c.is_ascii_digit()) - } -} - -fn is_toggle_word(word: &str) -> bool { - let lower = word.to_lowercase(); - BOOLEAN_PAIRS - .iter() - .any(|(a, b)| lower == *a || lower == *b) -} - -fn increment_toggle_string(boolean: &str) -> String { - let lower = boolean.to_lowercase(); - - let target = BOOLEAN_PAIRS - .iter() - .find_map(|(a, b)| { - if lower == *a { - Some(b) - } else if lower == *b { - Some(a) - } else { - None - } - }) - .unwrap_or(&boolean); - - if boolean.chars().all(|c| c.is_uppercase()) { - // Upper case - target.to_uppercase() - } else if boolean.chars().next().unwrap_or(' ').is_uppercase() { - // Title case - let mut chars = target.chars(); - match chars.next() { - None => String::new(), - Some(c) => c.to_uppercase().collect::() + chars.as_str(), - } - } else { - target.to_string() - } -} - -#[cfg(test)] -mod test { - use indoc::indoc; - - use crate::{ - state::Mode, - test::{NeovimBackedTestContext, VimTestContext}, - }; - - #[gpui::test] - async fn test_increment(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - 1ˇ2 - "}) - .await; - - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 1ˇ3 - "}); - cx.simulate_shared_keystrokes("ctrl-x").await; - cx.shared_state().await.assert_eq(indoc! {" - 1ˇ2 - "}); - - cx.simulate_shared_keystrokes("9 9 ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 11ˇ1 - "}); - cx.simulate_shared_keystrokes("1 1 1 ctrl-x").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇ0 - "}); - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq(indoc! {" - -11ˇ1 - "}); - } - - #[gpui::test] - async fn test_increment_with_dot(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - 1ˇ.2 - "}) - .await; - - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 1.ˇ3 - "}); - cx.simulate_shared_keystrokes("ctrl-x").await; - cx.shared_state().await.assert_eq(indoc! {" - 1.ˇ2 - "}); - } - - #[gpui::test] - async fn test_increment_with_leading_zeros(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - 000ˇ9 - "}) - .await; - - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 001ˇ0 - "}); - cx.simulate_shared_keystrokes("2 ctrl-x").await; - cx.shared_state().await.assert_eq(indoc! {" - 000ˇ8 - "}); - } - - #[gpui::test] - async fn test_increment_with_leading_zeros_and_zero(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - 01ˇ1 - "}) - .await; - - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 01ˇ2 - "}); - cx.simulate_shared_keystrokes("1 2 ctrl-x").await; - cx.shared_state().await.assert_eq(indoc! {" - 00ˇ0 - "}); - } - - #[gpui::test] - async fn test_increment_with_changing_leading_zeros(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - 099ˇ9 - "}) - .await; - - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 100ˇ0 - "}); - cx.simulate_shared_keystrokes("2 ctrl-x").await; - cx.shared_state().await.assert_eq(indoc! {" - 99ˇ8 - "}); - } - - #[gpui::test] - async fn test_increment_with_two_dots(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - 111.ˇ.2 - "}) - .await; - - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 111..ˇ3 - "}); - cx.simulate_shared_keystrokes("ctrl-x").await; - cx.shared_state().await.assert_eq(indoc! {" - 111..ˇ2 - "}); - } - - #[gpui::test] - async fn test_increment_sign_change(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! {" - ˇ0 - "}) - .await; - cx.simulate_shared_keystrokes("ctrl-x").await; - cx.shared_state().await.assert_eq(indoc! {" - -ˇ1 - "}); - cx.simulate_shared_keystrokes("2 ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇ1 - "}); - } - - #[gpui::test] - async fn test_increment_sign_change_with_leading_zeros(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! {" - 00ˇ1 - "}) - .await; - cx.simulate_shared_keystrokes("ctrl-x").await; - cx.shared_state().await.assert_eq(indoc! {" - 00ˇ0 - "}); - cx.simulate_shared_keystrokes("ctrl-x").await; - cx.shared_state().await.assert_eq(indoc! {" - -00ˇ1 - "}); - cx.simulate_shared_keystrokes("2 ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 00ˇ1 - "}); - } - - #[gpui::test] - async fn test_increment_bin_wrapping_and_padding(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! {" - 0b111111111111111111111111111111111111111111111111111111111111111111111ˇ1 - "}) - .await; - - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 0b000000111111111111111111111111111111111111111111111111111111111111111ˇ1 - "}); - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 0b000000000000000000000000000000000000000000000000000000000000000000000ˇ0 - "}); - - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 0b000000000000000000000000000000000000000000000000000000000000000000000ˇ1 - "}); - cx.simulate_shared_keystrokes("2 ctrl-x").await; - cx.shared_state().await.assert_eq(indoc! {" - 0b000000111111111111111111111111111111111111111111111111111111111111111ˇ1 - "}); - } - - #[gpui::test] - async fn test_increment_hex_wrapping_and_padding(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! {" - 0xfffffffffffffffffffˇf - "}) - .await; - - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 0x0000fffffffffffffffˇf - "}); - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 0x0000000000000000000ˇ0 - "}); - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 0x0000000000000000000ˇ1 - "}); - cx.simulate_shared_keystrokes("2 ctrl-x").await; - cx.shared_state().await.assert_eq(indoc! {" - 0x0000fffffffffffffffˇf - "}); - } - - #[gpui::test] - async fn test_increment_wrapping(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! {" - 1844674407370955161ˇ9 - "}) - .await; - - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 1844674407370955161ˇ5 - "}); - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - -1844674407370955161ˇ5 - "}); - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - -1844674407370955161ˇ4 - "}); - cx.simulate_shared_keystrokes("3 ctrl-x").await; - cx.shared_state().await.assert_eq(indoc! {" - 1844674407370955161ˇ4 - "}); - cx.simulate_shared_keystrokes("2 ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - -1844674407370955161ˇ5 - "}); - } - - #[gpui::test] - async fn test_increment_inline(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! {" - inline0x3ˇ9u32 - "}) - .await; - - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - inline0x3ˇau32 - "}); - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - inline0x3ˇbu32 - "}); - cx.simulate_shared_keystrokes("l l l ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - inline0x3bu3ˇ3 - "}); - } - - #[gpui::test] - async fn test_increment_hex_casing(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! {" - 0xFˇa - "}) - .await; - - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 0xfˇb - "}); - cx.simulate_shared_keystrokes("ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 0xfˇc - "}); - } - - #[gpui::test] - async fn test_increment_radix(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.simulate("ctrl-a", "ˇ total: 0xff") - .await - .assert_matches(); - cx.simulate("ctrl-x", "ˇ total: 0xff") - .await - .assert_matches(); - cx.simulate("ctrl-x", "ˇ total: 0xFF") - .await - .assert_matches(); - cx.simulate("ctrl-a", "(ˇ0b10f)").await.assert_matches(); - cx.simulate("ctrl-a", "ˇ-1").await.assert_matches(); - cx.simulate("ctrl-a", "banˇana").await.assert_matches(); - } - - #[gpui::test] - async fn test_increment_steps(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ˇ1 - 1 - 1 2 - 1 - 1"}) - .await; - - cx.simulate_shared_keystrokes("j v shift-g g ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - 1 - ˇ2 - 3 2 - 4 - 5"}); - - cx.simulate_shared_keystrokes("shift-g ctrl-v g g").await; - cx.shared_state().await.assert_eq(indoc! {" - «1ˇ» - «2ˇ» - «3ˇ» 2 - «4ˇ» - «5ˇ»"}); - - cx.simulate_shared_keystrokes("g ctrl-x").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇ0 - 0 - 0 2 - 0 - 0"}); - cx.simulate_shared_keystrokes("v shift-g g ctrl-a").await; - cx.simulate_shared_keystrokes("v shift-g 5 g ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇ6 - 12 - 18 2 - 24 - 30"}); - } - - #[gpui::test] - async fn test_increment_toggle(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("let enabled = trˇue;", Mode::Normal); - cx.simulate_keystrokes("ctrl-a"); - cx.assert_state("let enabled = falsˇe;", Mode::Normal); - - cx.simulate_keystrokes("0 ctrl-a"); - cx.assert_state("let enabled = truˇe;", Mode::Normal); - - cx.set_state( - indoc! {" - ˇlet enabled = TRUE; - let enabled = TRUE; - let enabled = TRUE; - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("shift-v j j ctrl-x"); - cx.assert_state( - indoc! {" - ˇlet enabled = FALSE; - let enabled = FALSE; - let enabled = FALSE; - "}, - Mode::Normal, - ); - - cx.set_state( - indoc! {" - let enabled = ˇYes; - let enabled = Yes; - let enabled = Yes; - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("ctrl-v j j e ctrl-x"); - cx.assert_state( - indoc! {" - let enabled = ˇNo; - let enabled = No; - let enabled = No; - "}, - Mode::Normal, - ); - - cx.set_state("ˇlet enabled = True;", Mode::Normal); - cx.simulate_keystrokes("ctrl-a"); - cx.assert_state("let enabled = Falsˇe;", Mode::Normal); - - cx.simulate_keystrokes("ctrl-a"); - cx.assert_state("let enabled = Truˇe;", Mode::Normal); - - cx.set_state("let enabled = Onˇ;", Mode::Normal); - cx.simulate_keystrokes("v b ctrl-a"); - cx.assert_state("let enabled = ˇOff;", Mode::Normal); - } - - #[gpui::test] - async fn test_increment_order(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("aaˇa false 1 2 3", Mode::Normal); - cx.simulate_keystrokes("ctrl-a"); - cx.assert_state("aaa truˇe 1 2 3", Mode::Normal); - - cx.set_state("aaˇa 1 false 2 3", Mode::Normal); - cx.simulate_keystrokes("ctrl-a"); - cx.assert_state("aaa ˇ2 false 2 3", Mode::Normal); - - cx.set_state("trueˇ 1 2 3", Mode::Normal); - cx.simulate_keystrokes("ctrl-a"); - cx.assert_state("true ˇ2 2 3", Mode::Normal); - - cx.set_state("falseˇ", Mode::Normal); - cx.simulate_keystrokes("ctrl-a"); - cx.assert_state("truˇe", Mode::Normal); - - cx.set_state("⚡️ˇ⚡️", Mode::Normal); - cx.simulate_keystrokes("ctrl-a"); - cx.assert_state("⚡️ˇ⚡️", 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/mark.rs b/crates/vim/src/normal/mark.rs deleted file mode 100644 index 3bb040511f..0000000000 --- a/crates/vim/src/normal/mark.rs +++ /dev/null @@ -1,397 +0,0 @@ -use std::{ops::Range, path::Path, sync::Arc}; - -use editor::{ - Anchor, Bias, DisplayPoint, Editor, MultiBuffer, - display_map::{DisplaySnapshot, ToDisplayPoint}, - movement, -}; -use gpui::{Context, Entity, EntityId, UpdateGlobal, Window}; -use language::SelectionGoal; -use text::Point; -use ui::App; -use workspace::OpenOptions; - -use crate::{ - Vim, - motion::{self, Motion}, - state::{Mark, Mode, VimGlobals}, -}; - -impl Vim { - pub fn create_mark(&mut self, text: Arc, window: &mut Window, cx: &mut Context) { - self.update_editor(cx, |vim, editor, cx| { - let anchors = editor - .selections - .disjoint_anchors_arc() - .iter() - .map(|s| s.head()) - .collect::>(); - vim.set_mark(text.to_string(), anchors, editor.buffer(), window, cx); - }); - self.clear_operator(window, cx); - } - - // When handling an action, you must create visual marks if you will switch to normal - // mode without the default selection behavior. - pub(crate) fn store_visual_marks(&mut self, window: &mut Window, cx: &mut Context) { - if self.mode.is_visual() { - self.create_visual_marks(self.mode, window, cx); - } - } - - pub(crate) fn create_visual_marks( - &mut self, - mode: Mode, - window: &mut Window, - cx: &mut Context, - ) { - let mut starts = vec![]; - let mut ends = vec![]; - let mut reversed = vec![]; - - self.update_editor(cx, |vim, editor, cx| { - let display_map = editor.display_snapshot(cx); - let selections = editor.selections.all_display(&display_map); - for selection in selections { - let end = movement::saturating_left(&display_map, selection.end); - ends.push( - display_map - .buffer_snapshot() - .anchor_before(end.to_offset(&display_map, Bias::Left)), - ); - starts.push( - display_map - .buffer_snapshot() - .anchor_before(selection.start.to_offset(&display_map, Bias::Left)), - ); - reversed.push(selection.reversed) - } - vim.set_mark("<".to_string(), starts, editor.buffer(), window, cx); - vim.set_mark(">".to_string(), ends, editor.buffer(), window, cx); - }); - - self.stored_visual_mode.replace((mode, reversed)); - } - - fn open_buffer_mark( - &mut self, - line: bool, - entity_id: EntityId, - anchors: Vec, - window: &mut Window, - cx: &mut Context, - ) { - let Some(workspace) = self.workspace(window) else { - return; - }; - workspace.update(cx, |workspace, cx| { - let item = workspace.items(cx).find(|item| { - item.act_as::(cx) - .is_some_and(|editor| editor.read(cx).buffer().entity_id() == entity_id) - }); - let Some(item) = item.cloned() else { - return; - }; - if let Some(pane) = workspace.pane_for(item.as_ref()) { - pane.update(cx, |pane, cx| { - if let Some(index) = pane.index_for_item(item.as_ref()) { - pane.activate_item(index, true, true, window, cx); - } - }); - }; - - item.act_as::(cx).unwrap().update(cx, |editor, cx| { - let map = editor.snapshot(window, cx); - let mut ranges: Vec> = Vec::new(); - for mut anchor in anchors { - if line { - let mut point = anchor.to_display_point(&map.display_snapshot); - point = motion::first_non_whitespace(&map.display_snapshot, false, point); - anchor = map - .display_snapshot - .buffer_snapshot() - .anchor_before(point.to_point(&map.display_snapshot)); - } - - if ranges.last() != Some(&(anchor..anchor)) { - ranges.push(anchor..anchor); - } - } - - editor.change_selections(Default::default(), window, cx, |s| { - s.select_anchor_ranges(ranges) - }); - }) - }); - } - - fn open_path_mark( - &mut self, - line: bool, - path: Arc, - points: Vec, - window: &mut Window, - cx: &mut Context, - ) { - let Some(workspace) = self.workspace(window) else { - return; - }; - let task = workspace.update(cx, |workspace, cx| { - workspace.open_abs_path( - path.to_path_buf(), - OpenOptions { - visible: Some(workspace::OpenVisible::All), - focus: Some(true), - ..Default::default() - }, - window, - cx, - ) - }); - cx.spawn_in(window, async move |this, cx| { - let editor = task.await?; - this.update_in(cx, |_, window, cx| { - if let Some(editor) = editor.act_as::(cx) { - editor.update(cx, |editor, cx| { - let map = editor.snapshot(window, cx); - let points: Vec<_> = points - .into_iter() - .map(|p| { - if line { - let point = p.to_display_point(&map.display_snapshot); - motion::first_non_whitespace( - &map.display_snapshot, - false, - point, - ) - .to_point(&map.display_snapshot) - } else { - p - } - }) - .collect(); - editor.change_selections(Default::default(), window, cx, |s| { - s.select_ranges(points.into_iter().map(|p| p..p)) - }) - }) - } - }) - }) - .detach_and_log_err(cx); - } - - pub fn jump( - &mut self, - text: Arc, - line: bool, - should_pop_operator: bool, - window: &mut Window, - cx: &mut Context, - ) { - if should_pop_operator { - self.pop_operator(window, cx); - } - let mark = self - .update_editor(cx, |vim, editor, cx| { - vim.get_mark(&text, editor, window, cx) - }) - .flatten(); - let anchors = match mark { - None => None, - Some(Mark::Local(anchors)) => Some(anchors), - Some(Mark::Buffer(entity_id, anchors)) => { - self.open_buffer_mark(line, entity_id, anchors, window, cx); - return; - } - Some(Mark::Path(path, points)) => { - self.open_path_mark(line, path, points, window, cx); - return; - } - }; - - let Some(mut anchors) = anchors else { return }; - - self.update_editor(cx, |_, editor, cx| { - editor.create_nav_history_entry(cx); - }); - let is_active_operator = self.active_operator().is_some(); - if is_active_operator { - if let Some(anchor) = anchors.last() { - self.motion( - Motion::Jump { - anchor: *anchor, - line, - }, - window, - cx, - ) - } - } else { - // Save the last anchor so as to jump to it later. - let anchor: Option = anchors.last_mut().map(|anchor| *anchor); - let should_jump = self.mode == Mode::Visual - || self.mode == Mode::VisualLine - || self.mode == Mode::VisualBlock; - - self.update_editor(cx, |_, editor, cx| { - let map = editor.snapshot(window, cx); - let mut ranges: Vec> = Vec::new(); - for mut anchor in anchors { - if line { - let mut point = anchor.to_display_point(&map.display_snapshot); - point = motion::first_non_whitespace(&map.display_snapshot, false, point); - anchor = map - .display_snapshot - .buffer_snapshot() - .anchor_before(point.to_point(&map.display_snapshot)); - } - - if ranges.last() != Some(&(anchor..anchor)) { - ranges.push(anchor..anchor); - } - } - - if !should_jump && !ranges.is_empty() { - editor.change_selections(Default::default(), window, cx, |s| { - s.select_anchor_ranges(ranges) - }); - } - }); - - if should_jump && let Some(anchor) = anchor { - self.motion(Motion::Jump { anchor, line }, window, cx) - } - } - } - - pub fn set_mark( - &mut self, - mut name: String, - anchors: Vec, - buffer_entity: &Entity, - window: &mut Window, - cx: &mut App, - ) { - let Some(workspace) = self.workspace(window) else { - return; - }; - if name == "`" { - name = "'".to_string(); - } - if matches!(&name[..], "-" | " ") { - // Not allowed marks - return; - } - let entity_id = workspace.entity_id(); - Vim::update_globals(cx, |vim_globals, cx| { - let Some(marks_state) = vim_globals.marks.get(&entity_id) else { - return; - }; - marks_state.update(cx, |ms, cx| { - ms.set_mark(name.clone(), buffer_entity, anchors, cx); - }); - }); - } - - pub fn get_mark( - &self, - mut name: &str, - editor: &mut Editor, - window: &mut Window, - cx: &mut App, - ) -> Option { - if name == "`" { - name = "'"; - } - if matches!(name, "{" | "}" | "(" | ")") { - let display_map = editor.display_snapshot(cx); - let selections = editor.selections.all_display(&display_map); - let anchors = selections - .into_iter() - .map(|selection| { - let point = match name { - "{" => movement::start_of_paragraph(&display_map, selection.head(), 1), - "}" => movement::end_of_paragraph(&display_map, selection.head(), 1), - "(" => motion::sentence_backwards(&display_map, selection.head(), 1), - ")" => motion::sentence_forwards(&display_map, selection.head(), 1), - _ => unreachable!(), - }; - display_map - .buffer_snapshot() - .anchor_before(point.to_offset(&display_map, Bias::Left)) - }) - .collect::>(); - return Some(Mark::Local(anchors)); - } - VimGlobals::update_global(cx, |globals, cx| { - let workspace_id = self.workspace(window)?.entity_id(); - globals - .marks - .get_mut(&workspace_id)? - .update(cx, |ms, cx| ms.get_mark(name, editor.buffer(), cx)) - }) - } - - pub fn delete_mark( - &self, - name: String, - editor: &mut Editor, - window: &mut Window, - cx: &mut App, - ) { - let Some(workspace) = self.workspace(window) else { - return; - }; - if name == "`" || name == "'" { - return; - } - let entity_id = workspace.entity_id(); - Vim::update_globals(cx, |vim_globals, cx| { - let Some(marks_state) = vim_globals.marks.get(&entity_id) else { - return; - }; - marks_state.update(cx, |ms, cx| { - ms.delete_mark(name.clone(), editor.buffer(), cx); - }); - }); - } -} - -pub fn jump_motion( - map: &DisplaySnapshot, - anchor: Anchor, - line: bool, -) -> (DisplayPoint, SelectionGoal) { - let mut point = anchor.to_display_point(map); - if line { - point = motion::first_non_whitespace(map, false, point) - } - - (point, SelectionGoal::None) -} - -#[cfg(test)] -mod test { - use gpui::TestAppContext; - - use crate::test::NeovimBackedTestContext; - - #[gpui::test] - async fn test_quote_mark(cx: &mut TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇHello, world!").await; - cx.simulate_shared_keystrokes("w m o").await; - cx.shared_state().await.assert_eq("Helloˇ, world!"); - cx.simulate_shared_keystrokes("$ ` o").await; - cx.shared_state().await.assert_eq("Helloˇ, world!"); - cx.simulate_shared_keystrokes("` `").await; - cx.shared_state().await.assert_eq("Hello, worldˇ!"); - cx.simulate_shared_keystrokes("` `").await; - cx.shared_state().await.assert_eq("Helloˇ, world!"); - cx.simulate_shared_keystrokes("$ m '").await; - cx.shared_state().await.assert_eq("Hello, worldˇ!"); - cx.simulate_shared_keystrokes("^ ` `").await; - cx.shared_state().await.assert_eq("Hello, worldˇ!"); - } -} diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs deleted file mode 100644 index 82af828deb..0000000000 --- a/crates/vim/src/normal/paste.rs +++ /dev/null @@ -1,1086 +0,0 @@ -use editor::{ - DisplayPoint, MultiBufferOffset, RowExt, SelectionEffects, display_map::ToDisplayPoint, - movement, -}; -use gpui::{Action, Context, Window}; -use language::{Bias, SelectionGoal}; -use schemars::JsonSchema; -use serde::Deserialize; -use settings::Settings; -use std::cmp; -use vim_mode_setting::HelixModeSetting; - -use crate::{ - Vim, - motion::{Motion, MotionKind}, - object::Object, - state::{Mode, Register}, -}; - -/// Pastes text from the specified register at the cursor position. -#[derive(Clone, Default, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -pub struct Paste { - #[serde(default)] - before: bool, - #[serde(default)] - preserve_clipboard: bool, -} - -impl Vim { - pub fn paste(&mut self, action: &Paste, window: &mut Window, cx: &mut Context) { - self.record_current_action(cx); - self.store_visual_marks(window, cx); - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - - self.update_editor(cx, |vim, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - editor.transact(window, cx, |editor, window, cx| { - editor.set_clip_at_line_ends(false, cx); - - let selected_register = vim.selected_register.take(); - - let Some(Register { - text, - clipboard_selections, - }) = Vim::update_globals(cx, |globals, cx| { - globals.read_register(selected_register, Some(editor), cx) - }) - .filter(|reg| !reg.text.is_empty()) - else { - return; - }; - let clipboard_selections = clipboard_selections - .filter(|sel| sel.len() > 1 && vim.mode != Mode::VisualLine); - - if !action.preserve_clipboard && vim.mode.is_visual() { - vim.copy_selections_content(editor, MotionKind::for_mode(vim.mode), window, cx); - } - - let display_map = editor.display_snapshot(cx); - let current_selections = editor.selections.all_adjusted_display(&display_map); - - // unlike zed, if you have a multi-cursor selection from vim block mode, - // pasting it will paste it on subsequent lines, even if you don't yet - // have a cursor there. - let mut selections_to_process = Vec::new(); - let mut i = 0; - while i < current_selections.len() { - selections_to_process - .push((current_selections[i].start..current_selections[i].end, true)); - i += 1; - } - if let Some(clipboard_selections) = clipboard_selections.as_ref() { - let left = current_selections - .iter() - .map(|selection| cmp::min(selection.start.column(), selection.end.column())) - .min() - .unwrap(); - let mut row = current_selections.last().unwrap().end.row().next_row(); - while i < clipboard_selections.len() { - let cursor = - display_map.clip_point(DisplayPoint::new(row, left), Bias::Left); - selections_to_process.push((cursor..cursor, false)); - i += 1; - row.0 += 1; - } - } - - let first_selection_indent_column = - clipboard_selections.as_ref().and_then(|zed_selections| { - zed_selections - .first() - .map(|selection| selection.first_line_indent) - }); - let before = action.before || vim.mode == Mode::VisualLine; - - let mut edits = Vec::new(); - let mut new_selections = Vec::new(); - let mut original_indent_columns = Vec::new(); - let mut start_offset = 0; - - for (ix, (selection, preserve)) in selections_to_process.iter().enumerate() { - let (mut to_insert, original_indent_column) = - if let Some(clipboard_selections) = &clipboard_selections { - if let Some(clipboard_selection) = clipboard_selections.get(ix) { - let end_offset = start_offset + clipboard_selection.len; - let text = text[start_offset..end_offset].to_string(); - start_offset = end_offset + 1; - (text, Some(clipboard_selection.first_line_indent)) - } else { - ("".to_string(), first_selection_indent_column) - } - } else { - (text.to_string(), first_selection_indent_column) - }; - let line_mode = to_insert.ends_with('\n'); - let is_multiline = to_insert.contains('\n'); - - if line_mode && !before { - if selection.is_empty() { - to_insert = - "\n".to_owned() + &to_insert[..to_insert.len() - "\n".len()]; - } else { - to_insert = "\n".to_owned() + &to_insert; - } - } else if line_mode && vim.mode == Mode::VisualLine { - to_insert.pop(); - } - - let display_range = if !selection.is_empty() { - // If vim is in VISUAL LINE mode and the column for the - // selection's end point is 0, that means that the - // cursor is at the newline character (\n) at the end of - // the line. In this situation we'll want to move one - // position to the left, ensuring we don't join the last - // line of the selection with the line directly below. - let end_point = - if vim.mode == Mode::VisualLine && selection.end.column() == 0 { - movement::left(&display_map, selection.end) - } else { - selection.end - }; - - selection.start..end_point - } else if line_mode { - let point = if before { - movement::line_beginning(&display_map, selection.start, false) - } else { - movement::line_end(&display_map, selection.start, false) - }; - point..point - } else { - let point = if before { - selection.start - } else { - movement::saturating_right(&display_map, selection.start) - }; - point..point - }; - - let point_range = display_range.start.to_point(&display_map) - ..display_range.end.to_point(&display_map); - let anchor = if is_multiline || vim.mode == Mode::VisualLine { - display_map - .buffer_snapshot() - .anchor_before(point_range.start) - } else { - display_map.buffer_snapshot().anchor_after(point_range.end) - }; - - if *preserve { - new_selections.push((anchor, line_mode, is_multiline)); - } - edits.push((point_range, to_insert.repeat(count))); - original_indent_columns.push(original_indent_column); - } - - let cursor_offset = editor - .selections - .last::(&display_map) - .head(); - if editor - .buffer() - .read(cx) - .snapshot(cx) - .language_settings_at(cursor_offset, cx) - .auto_indent_on_paste - { - editor.edit_with_block_indent(edits, original_indent_columns, cx); - } else { - editor.edit(edits, cx); - } - - // in line_mode vim will insert the new text on the next (or previous if before) line - // and put the cursor on the first non-blank character of the first inserted line (or at the end if the first line is blank). - // otherwise vim will insert the next text at (or before) the current cursor position, - // the cursor will go to the last (or first, if is_multiline) inserted character. - editor.change_selections(Default::default(), window, cx, |s| { - s.replace_cursors_with(|map| { - let mut cursors = Vec::new(); - for (anchor, line_mode, is_multiline) in &new_selections { - let mut cursor = anchor.to_display_point(map); - if *line_mode { - if !before { - cursor = movement::down( - map, - cursor, - SelectionGoal::None, - false, - &text_layout_details, - ) - .0; - } - cursor = movement::indented_line_beginning(map, cursor, true, true); - } else if !is_multiline && !vim.temp_mode { - cursor = movement::saturating_left(map, cursor) - } - cursors.push(cursor); - if vim.mode == Mode::VisualBlock { - break; - } - } - - cursors - }); - }) - }); - }); - - if HelixModeSetting::get_global(cx).0 { - self.switch_mode(Mode::HelixNormal, true, window, cx); - } else { - self.switch_mode(Mode::Normal, true, window, cx); - } - } - - pub fn replace_with_register_object( - &mut self, - object: Object, - around: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - let selected_register = self.selected_register.take(); - self.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - editor.set_clip_at_line_ends(false, cx); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - object.expand_selection(map, selection, around, None); - }); - }); - - let Some(Register { text, .. }) = Vim::update_globals(cx, |globals, cx| { - globals.read_register(selected_register, Some(editor), cx) - }) - .filter(|reg| !reg.text.is_empty()) else { - return; - }; - editor.insert(&text, window, cx); - editor.set_clip_at_line_ends(true, cx); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - selection.start = map.clip_point(selection.start, Bias::Left); - selection.end = selection.start - }) - }) - }); - }); - } - - pub fn replace_with_register_motion( - &mut self, - motion: Motion, - times: Option, - forced_motion: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - let selected_register = self.selected_register.take(); - self.update_editor(cx, |_, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - editor.transact(window, cx, |editor, window, cx| { - editor.set_clip_at_line_ends(false, cx); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - motion.expand_selection( - map, - selection, - times, - &text_layout_details, - forced_motion, - ); - }); - }); - - let Some(Register { text, .. }) = Vim::update_globals(cx, |globals, cx| { - globals.read_register(selected_register, Some(editor), cx) - }) - .filter(|reg| !reg.text.is_empty()) else { - return; - }; - editor.insert(&text, window, cx); - editor.set_clip_at_line_ends(true, cx); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - selection.start = map.clip_point(selection.start, Bias::Left); - selection.end = selection.start - }) - }) - }); - }); - } -} - -#[cfg(test)] -mod test { - use crate::{ - state::{Mode, Register}, - test::{NeovimBackedTestContext, VimTestContext}, - }; - use gpui::ClipboardItem; - use indoc::indoc; - use language::{LanguageName, language_settings::LanguageSettingsContent}; - use settings::{SettingsStore, UseSystemClipboard}; - - #[gpui::test] - async fn test_paste(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - // single line - cx.set_shared_state(indoc! {" - The quick brown - fox ˇjumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("v w y").await; - cx.shared_clipboard().await.assert_eq("jumps o"); - cx.set_shared_state(indoc! {" - The quick brown - fox jumps oveˇr - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("p").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox jumps overjumps ˇo - the lazy dog"}); - - cx.set_shared_state(indoc! {" - The quick brown - fox jumps oveˇr - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("shift-p").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox jumps ovejumps ˇor - the lazy dog"}); - - // line mode - cx.set_shared_state(indoc! {" - The quick brown - fox juˇmps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("d d").await; - cx.shared_clipboard().await.assert_eq("fox jumps over\n"); - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - the laˇzy dog"}); - cx.simulate_shared_keystrokes("p").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - the lazy dog - ˇfox jumps over"}); - cx.simulate_shared_keystrokes("k shift-p").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - ˇfox jumps over - the lazy dog - fox jumps over"}); - - // multiline, cursor to first character of pasted text. - cx.set_shared_state(indoc! {" - The quick brown - fox jumps ˇover - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("v j y").await; - cx.shared_clipboard().await.assert_eq("over\nthe lazy do"); - - cx.simulate_shared_keystrokes("p").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox jumps oˇover - the lazy dover - the lazy dog"}); - cx.simulate_shared_keystrokes("u shift-p").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox jumps ˇover - the lazy doover - the lazy dog"}); - } - - #[gpui::test] - async fn test_yank_system_clipboard_never(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |s| { - s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never) - }); - }); - - cx.set_state( - indoc! {" - The quick brown - fox jˇumps over - the lazy dog"}, - Mode::Normal, - ); - cx.simulate_keystrokes("v i w y"); - cx.assert_state( - indoc! {" - The quick brown - fox ˇjumps over - the lazy dog"}, - Mode::Normal, - ); - cx.simulate_keystrokes("p"); - cx.assert_state( - indoc! {" - The quick brown - fox jjumpˇsumps over - the lazy dog"}, - Mode::Normal, - ); - assert_eq!(cx.read_from_clipboard(), None); - } - - #[gpui::test] - async fn test_yank_system_clipboard_on_yank(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |s| { - s.vim.get_or_insert_default().use_system_clipboard = - Some(UseSystemClipboard::OnYank) - }); - }); - - // copy in visual mode - cx.set_state( - indoc! {" - The quick brown - fox jˇumps over - the lazy dog"}, - Mode::Normal, - ); - cx.simulate_keystrokes("v i w y"); - cx.assert_state( - indoc! {" - The quick brown - fox ˇjumps over - the lazy dog"}, - Mode::Normal, - ); - cx.simulate_keystrokes("p"); - cx.assert_state( - indoc! {" - The quick brown - fox jjumpˇsumps over - the lazy dog"}, - Mode::Normal, - ); - assert_eq!( - cx.read_from_clipboard().map(|item| item.text().unwrap()), - Some("jumps".into()) - ); - cx.simulate_keystrokes("d d p"); - cx.assert_state( - indoc! {" - The quick brown - the lazy dog - ˇfox jjumpsumps over"}, - Mode::Normal, - ); - assert_eq!( - cx.read_from_clipboard().map(|item| item.text().unwrap()), - Some("jumps".into()) - ); - cx.write_to_clipboard(ClipboardItem::new_string("test-copy".to_string())); - cx.simulate_keystrokes("shift-p"); - cx.assert_state( - indoc! {" - The quick brown - the lazy dog - test-copˇyfox jjumpsumps over"}, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_paste_visual(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - // copy in visual mode - cx.set_shared_state(indoc! {" - The quick brown - fox jˇumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("v i w y").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox ˇjumps over - the lazy dog"}); - // paste in visual mode - cx.simulate_shared_keystrokes("w v i w p").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox jumps jumpˇs - the lazy dog"}); - cx.shared_clipboard().await.assert_eq("over"); - // paste in visual line mode - cx.simulate_shared_keystrokes("up shift-v shift-p").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇover - fox jumps jumps - the lazy dog"}); - cx.shared_clipboard().await.assert_eq("over"); - // paste in visual block mode - cx.simulate_shared_keystrokes("ctrl-v down down p").await; - cx.shared_state().await.assert_eq(indoc! {" - oveˇrver - overox jumps jumps - overhe lazy dog"}); - - // copy in visual line mode - cx.set_shared_state(indoc! {" - The quick brown - fox juˇmps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("shift-v d").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - the laˇzy dog"}); - // paste in visual mode - cx.simulate_shared_keystrokes("v i w p").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - the• - ˇfox jumps over - dog"}); - cx.shared_clipboard().await.assert_eq("lazy"); - cx.set_shared_state(indoc! {" - The quick brown - fox juˇmps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("shift-v d").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - the laˇzy dog"}); - cx.shared_clipboard().await.assert_eq("fox jumps over\n"); - // paste in visual line mode - cx.simulate_shared_keystrokes("k shift-v p").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇfox jumps over - the lazy dog"}); - cx.shared_clipboard().await.assert_eq("The quick brown\n"); - - // Copy line and paste in visual mode, with cursor on newline character. - cx.set_shared_state(indoc! {" - ˇThe quick brown - fox jumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("y y shift-v j $ p").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇThe quick brown - the lazy dog"}); - } - - #[gpui::test] - async fn test_paste_visual_block(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - // copy in visual block mode - cx.set_shared_state(indoc! {" - The ˇquick brown - fox jumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("ctrl-v 2 j y").await; - cx.shared_clipboard().await.assert_eq("q\nj\nl"); - cx.simulate_shared_keystrokes("p").await; - cx.shared_state().await.assert_eq(indoc! {" - The qˇquick brown - fox jjumps over - the llazy dog"}); - cx.simulate_shared_keystrokes("v i w shift-p").await; - cx.shared_state().await.assert_eq(indoc! {" - The ˇq brown - fox jjjumps over - the lllazy dog"}); - cx.simulate_shared_keystrokes("v i w shift-p").await; - - cx.set_shared_state(indoc! {" - The ˇquick brown - fox jumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("ctrl-v j y").await; - cx.shared_clipboard().await.assert_eq("q\nj"); - cx.simulate_shared_keystrokes("l ctrl-v 2 j shift-p").await; - cx.shared_state().await.assert_eq(indoc! {" - The qˇqick brown - fox jjmps over - the lzy dog"}); - - cx.simulate_shared_keystrokes("shift-v p").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇq - j - fox jjmps over - the lzy dog"}); - } - - #[gpui::test] - async fn test_paste_indent(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new_typescript(cx).await; - - cx.set_state( - indoc! {" - class A {ˇ - } - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("o a ( ) { escape"); - cx.assert_state( - indoc! {" - class A { - a()ˇ{} - } - "}, - Mode::Normal, - ); - // cursor goes to the first non-blank character in the line; - cx.simulate_keystrokes("y y p"); - cx.assert_state( - indoc! {" - class A { - a(){} - ˇa(){} - } - "}, - Mode::Normal, - ); - // indentation is preserved when pasting - cx.simulate_keystrokes("u shift-v up y shift-p"); - cx.assert_state( - indoc! {" - ˇclass A { - a(){} - class A { - a(){} - } - "}, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_paste_auto_indent(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! {" - mod some_module { - ˇfn main() { - } - } - "}, - Mode::Normal, - ); - // default auto indentation - cx.simulate_keystrokes("y y p"); - cx.assert_state( - indoc! {" - mod some_module { - fn main() { - ˇfn main() { - } - } - "}, - Mode::Normal, - ); - // back to previous state - cx.simulate_keystrokes("u u"); - cx.assert_state( - indoc! {" - mod some_module { - ˇfn main() { - } - } - "}, - Mode::Normal, - ); - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |settings| { - settings.project.all_languages.languages.0.insert( - LanguageName::new_static("Rust").0, - LanguageSettingsContent { - auto_indent_on_paste: Some(false), - ..Default::default() - }, - ); - }); - }); - // auto indentation turned off - cx.simulate_keystrokes("y y p"); - cx.assert_state( - indoc! {" - mod some_module { - fn main() { - ˇfn main() { - } - } - "}, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_paste_count(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - onˇe - two - three - "}) - .await; - cx.simulate_shared_keystrokes("y y 3 p").await; - cx.shared_state().await.assert_eq(indoc! {" - one - ˇone - one - one - two - three - "}); - - cx.set_shared_state(indoc! {" - one - ˇtwo - three - "}) - .await; - cx.simulate_shared_keystrokes("y $ $ 3 p").await; - cx.shared_state().await.assert_eq(indoc! {" - one - twotwotwotwˇo - three - "}); - } - - #[gpui::test] - async fn test_paste_system_clipboard_never(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |s| { - s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never) - }); - }); - - cx.set_state( - indoc! {" - ˇThe quick brown - fox jumps over - the lazy dog"}, - Mode::Normal, - ); - - cx.write_to_clipboard(ClipboardItem::new_string("something else".to_string())); - - cx.simulate_keystrokes("d d"); - cx.assert_state( - indoc! {" - ˇfox jumps over - the lazy dog"}, - Mode::Normal, - ); - - cx.simulate_keystrokes("shift-v p"); - cx.assert_state( - indoc! {" - ˇThe quick brown - the lazy dog"}, - Mode::Normal, - ); - - cx.simulate_keystrokes("shift-v"); - cx.dispatch_action(editor::actions::Paste); - cx.assert_state( - indoc! {" - ˇsomething else - the lazy dog"}, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_numbered_registers(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |s| { - s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never) - }); - }); - - cx.set_shared_state(indoc! {" - The quick brown - fox jˇumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("y y \" 0 p").await; - cx.shared_register('0').await.assert_eq("fox jumps over\n"); - cx.shared_register('"').await.assert_eq("fox jumps over\n"); - - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox jumps over - ˇfox jumps over - the lazy dog"}); - cx.simulate_shared_keystrokes("k k d d").await; - cx.shared_register('0').await.assert_eq("fox jumps over\n"); - cx.shared_register('1').await.assert_eq("The quick brown\n"); - cx.shared_register('"').await.assert_eq("The quick brown\n"); - - cx.simulate_shared_keystrokes("d d shift-g d d").await; - cx.shared_register('0').await.assert_eq("fox jumps over\n"); - cx.shared_register('3').await.assert_eq("The quick brown\n"); - cx.shared_register('2').await.assert_eq("fox jumps over\n"); - cx.shared_register('1').await.assert_eq("the lazy dog\n"); - - cx.shared_state().await.assert_eq(indoc! {" - ˇfox jumps over"}); - - cx.simulate_shared_keystrokes("d d \" 3 p p \" 1 p").await; - cx.set_shared_state(indoc! {" - The quick brown - fox jumps over - ˇthe lazy dog"}) - .await; - } - - #[gpui::test] - async fn test_named_registers(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |s| { - s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never) - }); - }); - - cx.set_shared_state(indoc! {" - The quick brown - fox jˇumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("\" a d a w").await; - cx.shared_register('a').await.assert_eq("jumps "); - cx.simulate_shared_keystrokes("\" shift-a d i w").await; - cx.shared_register('a').await.assert_eq("jumps over"); - cx.shared_register('"').await.assert_eq("jumps over"); - cx.simulate_shared_keystrokes("\" a p").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox jumps oveˇr - the lazy dog"}); - cx.simulate_shared_keystrokes("\" a d a w").await; - cx.shared_register('a').await.assert_eq(" over"); - } - - #[gpui::test] - async fn test_special_registers(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |s| { - s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never) - }); - }); - - cx.set_shared_state(indoc! {" - The quick brown - fox jˇumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("d i w").await; - cx.shared_register('-').await.assert_eq("jumps"); - cx.simulate_shared_keystrokes("\" _ d d").await; - cx.shared_register('_').await.assert_eq(""); - - cx.simulate_shared_keystrokes("shift-v \" _ y w").await; - cx.shared_register('"').await.assert_eq("jumps"); - - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - the ˇlazy dog"}); - cx.simulate_shared_keystrokes("\" \" d ^").await; - cx.shared_register('0').await.assert_eq("the "); - cx.shared_register('"').await.assert_eq("the "); - - cx.simulate_shared_keystrokes("^ \" + d $").await; - cx.shared_clipboard().await.assert_eq("lazy dog"); - cx.shared_register('"').await.assert_eq("lazy dog"); - - cx.simulate_shared_keystrokes("/ d o g enter").await; - cx.shared_register('/').await.assert_eq("dog"); - cx.simulate_shared_keystrokes("\" / shift-p").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - doˇg"}); - - // not testing nvim as it doesn't have a filename - cx.simulate_keystrokes("\" % p"); - #[cfg(not(target_os = "windows"))] - cx.assert_state( - indoc! {" - The quick brown - dogdir/file.rˇs"}, - Mode::Normal, - ); - #[cfg(target_os = "windows")] - cx.assert_state( - indoc! {" - The quick brown - dogdir\\file.rˇs"}, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_multicursor_paste(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |s| { - s.vim.get_or_insert_default().use_system_clipboard = Some(UseSystemClipboard::Never) - }); - }); - - cx.set_state( - indoc! {" - ˇfish one - fish two - fish red - fish blue - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("4 g l w escape d i w 0 shift-p"); - cx.assert_state( - indoc! {" - onˇefish• - twˇofish• - reˇdfish• - bluˇefish• - "}, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_replace_with_register(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! {" - ˇfish one - two three - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("y i w"); - cx.simulate_keystrokes("w"); - cx.simulate_keystrokes("g shift-r i w"); - cx.assert_state( - indoc! {" - fish fisˇh - two three - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("j b g shift-r e"); - cx.assert_state( - indoc! {" - fish fish - two fisˇh - "}, - Mode::Normal, - ); - let clipboard: Register = cx.read_from_clipboard().unwrap().into(); - assert_eq!(clipboard.text, "fish"); - - cx.set_state( - indoc! {" - ˇfish one - two three - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("y i w"); - cx.simulate_keystrokes("w"); - cx.simulate_keystrokes("v i w g shift-r"); - cx.assert_state( - indoc! {" - fish fisˇh - two three - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("g shift-r r"); - cx.assert_state( - indoc! {" - fisˇh - two three - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("j w g shift-r $"); - cx.assert_state( - indoc! {" - fish - two fisˇh - "}, - Mode::Normal, - ); - let clipboard: Register = cx.read_from_clipboard().unwrap().into(); - assert_eq!(clipboard.text, "fish"); - } - - #[gpui::test] - async fn test_replace_with_register_dot_repeat(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! {" - ˇfish one - two three - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("y i w"); - cx.simulate_keystrokes("w"); - cx.simulate_keystrokes("g shift-r i w"); - cx.assert_state( - indoc! {" - fish fisˇh - two three - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("j ."); - cx.assert_state( - indoc! {" - fish fish - two fisˇh - "}, - Mode::Normal, - ); - } -} diff --git a/crates/vim/src/normal/repeat.rs b/crates/vim/src/normal/repeat.rs deleted file mode 100644 index e47b2b350f..0000000000 --- a/crates/vim/src/normal/repeat.rs +++ /dev/null @@ -1,911 +0,0 @@ -use std::{cell::RefCell, rc::Rc}; - -use crate::{ - Vim, - insert::NormalBefore, - motion::Motion, - normal::InsertBefore, - state::{Mode, Operator, RecordedSelection, ReplayableAction, VimGlobals}, -}; -use editor::Editor; -use gpui::{Action, App, Context, Window, actions}; -use workspace::Workspace; - -actions!( - vim, - [ - /// Repeats the last change. - Repeat, - /// Ends the repeat recording. - EndRepeat, - /// Toggles macro recording. - ToggleRecord, - /// Replays the last recorded macro. - ReplayLastRecording - ] -); - -fn should_replay(action: &dyn Action) -> bool { - // skip so that we don't leave the character palette open - if editor::actions::ShowCharacterPalette.partial_eq(action) { - return false; - } - true -} - -fn repeatable_insert(action: &ReplayableAction) -> Option> { - match action { - ReplayableAction::Action(action) => { - if super::InsertBefore.partial_eq(&**action) - || super::InsertAfter.partial_eq(&**action) - || super::InsertFirstNonWhitespace.partial_eq(&**action) - || super::InsertEndOfLine.partial_eq(&**action) - { - Some(super::InsertBefore.boxed_clone()) - } else if super::InsertLineAbove.partial_eq(&**action) - || super::InsertLineBelow.partial_eq(&**action) - { - Some(super::InsertLineBelow.boxed_clone()) - } else if crate::replace::ToggleReplace.partial_eq(&**action) { - Some(crate::replace::ToggleReplace.boxed_clone()) - } else { - None - } - } - ReplayableAction::Insertion { .. } => None, - } -} - -pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { - Vim::action(editor, cx, |vim, _: &EndRepeat, window, cx| { - Vim::globals(cx).dot_replaying = false; - vim.switch_mode(Mode::Normal, false, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &Repeat, window, cx| { - vim.repeat(false, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &ToggleRecord, window, cx| { - let globals = Vim::globals(cx); - if let Some(char) = globals.recording_register.take() { - globals.last_recorded_register = Some(char) - } else { - vim.push_operator(Operator::RecordRegister, window, cx); - } - }); - - Vim::action(editor, cx, |vim, _: &ReplayLastRecording, window, cx| { - let Some(register) = Vim::globals(cx).last_recorded_register else { - return; - }; - vim.replay_register(register, window, cx) - }); -} - -pub struct ReplayerState { - actions: Vec, - running: bool, - ix: usize, -} - -#[derive(Clone)] -pub struct Replayer(Rc>); - -impl Replayer { - pub fn new() -> Self { - Self(Rc::new(RefCell::new(ReplayerState { - actions: vec![], - running: false, - ix: 0, - }))) - } - - pub fn replay(&mut self, actions: Vec, window: &mut Window, cx: &mut App) { - let mut lock = self.0.borrow_mut(); - let range = lock.ix..lock.ix; - lock.actions.splice(range, actions); - if lock.running { - return; - } - lock.running = true; - let this = self.clone(); - window.defer(cx, move |window, cx| { - this.next(window, cx); - let Some(Some(workspace)) = window.root::() else { - return; - }; - let Some(editor) = workspace - .read(cx) - .active_item(cx) - .and_then(|item| item.act_as::(cx)) - else { - return; - }; - editor.update(cx, |editor, cx| { - editor - .buffer() - .update(cx, |multi, cx| multi.finalize_last_transaction(cx)) - }); - }) - } - - pub fn stop(self) { - self.0.borrow_mut().actions.clear() - } - - pub fn next(self, window: &mut Window, cx: &mut App) { - let mut lock = self.0.borrow_mut(); - let action = if lock.ix < 10000 { - lock.actions.get(lock.ix).cloned() - } else { - log::error!("Aborting replay after 10000 actions"); - None - }; - lock.ix += 1; - drop(lock); - let Some(action) = action else { - Vim::globals(cx).replayer.take(); - return; - }; - match action { - ReplayableAction::Action(action) => { - if should_replay(&*action) { - window.dispatch_action(action.boxed_clone(), cx); - cx.defer(move |cx| Vim::globals(cx).observe_action(action.boxed_clone())); - } - } - ReplayableAction::Insertion { - text, - utf16_range_to_replace, - } => { - let Some(Some(workspace)) = window.root::() else { - return; - }; - let Some(editor) = workspace - .read(cx) - .active_item(cx) - .and_then(|item| item.act_as::(cx)) - else { - return; - }; - editor.update(cx, |editor, cx| { - editor.replay_insert_event(&text, utf16_range_to_replace.clone(), window, cx) - }) - } - } - window.defer(cx, move |window, cx| self.next(window, cx)); - } -} - -impl Vim { - pub(crate) fn record_register( - &mut self, - register: char, - window: &mut Window, - cx: &mut Context, - ) { - let globals = Vim::globals(cx); - globals.recording_register = Some(register); - globals.recordings.remove(®ister); - globals.ignore_current_insertion = true; - self.clear_operator(window, cx) - } - - pub(crate) fn replay_register( - &mut self, - mut register: char, - window: &mut Window, - cx: &mut Context, - ) { - let mut count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - self.clear_operator(window, cx); - - let globals = Vim::globals(cx); - if register == '@' { - let Some(last) = globals.last_replayed_register else { - return; - }; - register = last; - } - let Some(actions) = globals.recordings.get(®ister) else { - return; - }; - - let mut repeated_actions = vec![]; - while count > 0 { - repeated_actions.extend(actions.iter().cloned()); - count -= 1 - } - - globals.last_replayed_register = Some(register); - let mut replayer = globals.replayer.get_or_insert_with(Replayer::new).clone(); - replayer.replay(repeated_actions, window, cx); - } - - pub(crate) fn repeat( - &mut self, - from_insert_mode: bool, - window: &mut Window, - cx: &mut Context, - ) { - if self.active_operator().is_some() { - Vim::update_globals(cx, |globals, _| { - globals.recording_actions.clear(); - globals.recording_count = None; - globals.dot_recording = false; - globals.stop_recording_after_next_action = false; - }); - self.clear_operator(window, cx); - return; - } - - Vim::take_forced_motion(cx); - let count = Vim::take_count(cx); - - let Some((mut actions, selection, mode)) = Vim::update_globals(cx, |globals, _| { - let actions = globals.recorded_actions.clone(); - if actions.is_empty() { - return None; - } - if globals.replayer.is_none() - && let Some(recording_register) = globals.recording_register - { - globals - .recordings - .entry(recording_register) - .or_default() - .push(ReplayableAction::Action(Repeat.boxed_clone())); - } - - let mut mode = None; - let selection = globals.recorded_selection.clone(); - match selection { - RecordedSelection::SingleLine { .. } | RecordedSelection::Visual { .. } => { - globals.recorded_count = None; - mode = Some(Mode::Visual); - } - RecordedSelection::VisualLine { .. } => { - globals.recorded_count = None; - mode = Some(Mode::VisualLine) - } - RecordedSelection::VisualBlock { .. } => { - globals.recorded_count = None; - mode = Some(Mode::VisualBlock) - } - RecordedSelection::None => { - if let Some(count) = count { - globals.recorded_count = Some(count); - } - } - } - - Some((actions, selection, mode)) - }) else { - return; - }; - if mode != Some(self.mode) { - if let Some(mode) = mode { - self.switch_mode(mode, false, window, cx) - } - - match selection { - RecordedSelection::SingleLine { cols } => { - if cols > 1 { - self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx) - } - } - RecordedSelection::Visual { rows, cols } => { - self.visual_motion( - Motion::Down { - display_lines: false, - }, - Some(rows as usize), - window, - cx, - ); - self.visual_motion( - Motion::StartOfLine { - display_lines: false, - }, - None, - window, - cx, - ); - if cols > 1 { - self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx) - } - } - RecordedSelection::VisualBlock { rows, cols } => { - self.visual_motion( - Motion::Down { - display_lines: false, - }, - Some(rows as usize), - window, - cx, - ); - if cols > 1 { - self.visual_motion(Motion::Right, Some(cols as usize - 1), window, cx); - } - } - RecordedSelection::VisualLine { rows } => { - self.visual_motion( - Motion::Down { - display_lines: false, - }, - Some(rows as usize), - window, - cx, - ); - } - RecordedSelection::None => {} - } - } - - // insert internally uses repeat to handle counts - // vim doesn't treat 3a1 as though you literally repeated a1 - // 3 times, instead it inserts the content thrice at the insert position. - if let Some(to_repeat) = repeatable_insert(&actions[0]) { - if let Some(ReplayableAction::Action(action)) = actions.last() - && NormalBefore.partial_eq(&**action) - { - actions.pop(); - } - - let mut new_actions = actions.clone(); - actions[0] = ReplayableAction::Action(to_repeat.boxed_clone()); - - let mut count = cx.global::().recorded_count.unwrap_or(1); - - // if we came from insert mode we're just doing repetitions 2 onwards. - if from_insert_mode { - count -= 1; - new_actions[0] = actions[0].clone(); - } - - for _ in 1..count { - new_actions.append(actions.clone().as_mut()); - } - new_actions.push(ReplayableAction::Action(NormalBefore.boxed_clone())); - actions = new_actions; - } - - actions.push(ReplayableAction::Action(EndRepeat.boxed_clone())); - - if self.temp_mode { - self.temp_mode = false; - actions.push(ReplayableAction::Action(InsertBefore.boxed_clone())); - } - - let globals = Vim::globals(cx); - globals.dot_replaying = true; - let mut replayer = globals.replayer.get_or_insert_with(Replayer::new).clone(); - - replayer.replay(actions, window, cx); - } -} - -#[cfg(test)] -mod test { - use editor::test::editor_lsp_test_context::EditorLspTestContext; - use futures::StreamExt; - use indoc::indoc; - - use gpui::EntityInputHandler; - - use crate::{ - state::Mode, - test::{NeovimBackedTestContext, VimTestContext}, - }; - - #[gpui::test] - async fn test_dot_repeat(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - // "o" - cx.set_shared_state("ˇhello").await; - cx.simulate_shared_keystrokes("o w o r l d escape").await; - cx.shared_state().await.assert_eq("hello\nworlˇd"); - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq("hello\nworld\nworlˇd"); - - // "d" - cx.simulate_shared_keystrokes("^ d f o").await; - cx.simulate_shared_keystrokes("g g .").await; - cx.shared_state().await.assert_eq("ˇ\nworld\nrld"); - - // "p" (note that it pastes the current clipboard) - cx.simulate_shared_keystrokes("j y y p").await; - cx.simulate_shared_keystrokes("shift-g y y .").await; - cx.shared_state() - .await - .assert_eq("\nworld\nworld\nrld\nˇrld"); - - // "~" (note that counts apply to the action taken, not . itself) - cx.set_shared_state("ˇthe quick brown fox").await; - cx.simulate_shared_keystrokes("2 ~ .").await; - cx.set_shared_state("THE ˇquick brown fox").await; - cx.simulate_shared_keystrokes("3 .").await; - cx.set_shared_state("THE QUIˇck brown fox").await; - cx.run_until_parked(); - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq("THE QUICK ˇbrown fox"); - } - - #[gpui::test] - async fn test_repeat_ime(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("hˇllo", Mode::Normal); - cx.simulate_keystrokes("i"); - - // simulate brazilian input for ä. - cx.update_editor(|editor, window, cx| { - editor.replace_and_mark_text_in_range(None, "\"", Some(1..1), window, cx); - editor.replace_text_in_range(None, "ä", window, cx); - }); - cx.simulate_keystrokes("escape"); - cx.assert_state("hˇällo", Mode::Normal); - cx.simulate_keystrokes("."); - cx.assert_state("hˇäällo", Mode::Normal); - } - - #[gpui::test] - async fn test_repeat_completion(cx: &mut gpui::TestAppContext) { - VimTestContext::init(cx); - let cx = EditorLspTestContext::new_rust( - lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string(), ":".to_string()]), - resolve_provider: Some(true), - ..Default::default() - }), - ..Default::default() - }, - cx, - ) - .await; - let mut cx = VimTestContext::new_with_lsp(cx, true); - - cx.set_state( - indoc! {" - onˇe - two - three - "}, - Mode::Normal, - ); - - let mut request = cx.set_request_handler::( - move |_, params, _| async move { - let position = params.text_document_position.position; - Ok(Some(lsp::CompletionResponse::Array(vec![ - lsp::CompletionItem { - label: "first".to_string(), - text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: lsp::Range::new(position, position), - new_text: "first".to_string(), - })), - ..Default::default() - }, - lsp::CompletionItem { - label: "second".to_string(), - text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: lsp::Range::new(position, position), - new_text: "second".to_string(), - })), - ..Default::default() - }, - ]))) - }, - ); - cx.simulate_keystrokes("a ."); - request.next().await; - cx.condition(|editor, _| editor.context_menu_visible()) - .await; - cx.simulate_keystrokes("down enter ! escape"); - - cx.assert_state( - indoc! {" - one.secondˇ! - two - three - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("j ."); - cx.assert_state( - indoc! {" - one.second! - two.secondˇ! - three - "}, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_repeat_completion_unicode_bug(cx: &mut gpui::TestAppContext) { - VimTestContext::init(cx); - let cx = EditorLspTestContext::new_rust( - lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string(), ":".to_string()]), - resolve_provider: Some(true), - ..Default::default() - }), - ..Default::default() - }, - cx, - ) - .await; - let mut cx = VimTestContext::new_with_lsp(cx, true); - - cx.set_state( - indoc! {" - ĩлˇк - ĩлк - "}, - Mode::Normal, - ); - - let mut request = cx.set_request_handler::( - move |_, params, _| async move { - let position = params.text_document_position.position; - let mut to_the_left = position; - to_the_left.character -= 2; - Ok(Some(lsp::CompletionResponse::Array(vec![ - lsp::CompletionItem { - label: "oops".to_string(), - text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: lsp::Range::new(to_the_left, position), - new_text: "к!".to_string(), - })), - ..Default::default() - }, - ]))) - }, - ); - cx.simulate_keystrokes("i ."); - request.next().await; - cx.condition(|editor, _| editor.context_menu_visible()) - .await; - cx.simulate_keystrokes("enter escape"); - cx.assert_state( - indoc! {" - ĩкˇ!к - ĩлк - "}, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_repeat_visual(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - // single-line (3 columns) - cx.set_shared_state(indoc! { - "ˇthe quick brown - fox jumps over - the lazy dog" - }) - .await; - cx.simulate_shared_keystrokes("v i w s o escape").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇo quick brown - fox jumps over - the lazy dog" - }); - cx.simulate_shared_keystrokes("j w .").await; - cx.shared_state().await.assert_eq(indoc! { - "o quick brown - fox ˇops over - the lazy dog" - }); - cx.simulate_shared_keystrokes("f r .").await; - cx.shared_state().await.assert_eq(indoc! { - "o quick brown - fox ops oveˇothe lazy dog" - }); - - // visual - cx.set_shared_state(indoc! { - "the ˇquick brown - fox jumps over - fox jumps over - fox jumps over - the lazy dog" - }) - .await; - cx.simulate_shared_keystrokes("v j x").await; - cx.shared_state().await.assert_eq(indoc! { - "the ˇumps over - fox jumps over - fox jumps over - the lazy dog" - }); - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq(indoc! { - "the ˇumps over - fox jumps over - the lazy dog" - }); - cx.simulate_shared_keystrokes("w .").await; - cx.shared_state().await.assert_eq(indoc! { - "the umps ˇumps over - the lazy dog" - }); - cx.simulate_shared_keystrokes("j .").await; - cx.shared_state().await.assert_eq(indoc! { - "the umps umps over - the ˇog" - }); - - // block mode (3 rows) - cx.set_shared_state(indoc! { - "ˇthe quick brown - fox jumps over - the lazy dog" - }) - .await; - cx.simulate_shared_keystrokes("ctrl-v j j shift-i o escape") - .await; - cx.shared_state().await.assert_eq(indoc! { - "ˇothe quick brown - ofox jumps over - othe lazy dog" - }); - cx.simulate_shared_keystrokes("j 4 l .").await; - cx.shared_state().await.assert_eq(indoc! { - "othe quick brown - ofoxˇo jumps over - otheo lazy dog" - }); - - // line mode - cx.set_shared_state(indoc! { - "ˇthe quick brown - fox jumps over - the lazy dog" - }) - .await; - cx.simulate_shared_keystrokes("shift-v shift-r o escape") - .await; - cx.shared_state().await.assert_eq(indoc! { - "ˇo - fox jumps over - the lazy dog" - }); - cx.simulate_shared_keystrokes("j .").await; - cx.shared_state().await.assert_eq(indoc! { - "o - ˇo - the lazy dog" - }); - } - - #[gpui::test] - async fn test_repeat_motion_counts(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "ˇthe quick brown - fox jumps over - the lazy dog" - }) - .await; - cx.simulate_shared_keystrokes("3 d 3 l").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇ brown - fox jumps over - the lazy dog" - }); - cx.simulate_shared_keystrokes("j .").await; - cx.shared_state().await.assert_eq(indoc! { - " brown - ˇ over - the lazy dog" - }); - cx.simulate_shared_keystrokes("j 2 .").await; - cx.shared_state().await.assert_eq(indoc! { - " brown - over - ˇe lazy dog" - }); - } - - #[gpui::test] - async fn test_record_interrupted(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("ˇhello\n", Mode::Normal); - cx.simulate_keystrokes("4 i j cmd-shift-p escape"); - cx.simulate_keystrokes("escape"); - cx.assert_state("ˇjhello\n", Mode::Normal); - } - - #[gpui::test] - async fn test_repeat_over_blur(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇhello hello hello\n").await; - cx.simulate_shared_keystrokes("c f o x escape").await; - cx.shared_state().await.assert_eq("ˇx hello hello\n"); - cx.simulate_shared_keystrokes(": escape").await; - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq("ˇx hello\n"); - } - - #[gpui::test] - async fn test_undo_repeated_insert(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("hellˇo").await; - cx.simulate_shared_keystrokes("3 a . escape").await; - cx.shared_state().await.assert_eq("hello..ˇ."); - cx.simulate_shared_keystrokes("u").await; - cx.shared_state().await.assert_eq("hellˇo"); - } - - #[gpui::test] - async fn test_record_replay(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇhello world").await; - cx.simulate_shared_keystrokes("q w c w j escape q").await; - cx.shared_state().await.assert_eq("ˇj world"); - cx.simulate_shared_keystrokes("2 l @ w").await; - cx.shared_state().await.assert_eq("j ˇj"); - } - - #[gpui::test] - async fn test_record_replay_count(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇhello world!!").await; - cx.simulate_shared_keystrokes("q a v 3 l s 0 escape l q") - .await; - cx.shared_state().await.assert_eq("0ˇo world!!"); - cx.simulate_shared_keystrokes("2 @ a").await; - cx.shared_state().await.assert_eq("000ˇ!"); - } - - #[gpui::test] - async fn test_record_replay_dot(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇhello world").await; - cx.simulate_shared_keystrokes("q a r a l r b l q").await; - cx.shared_state().await.assert_eq("abˇllo world"); - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq("abˇblo world"); - cx.simulate_shared_keystrokes("shift-q").await; - cx.shared_state().await.assert_eq("ababˇo world"); - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq("ababˇb world"); - } - - #[gpui::test] - async fn test_record_replay_of_dot(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇhello world").await; - cx.simulate_shared_keystrokes("r o q w . q").await; - cx.shared_state().await.assert_eq("ˇoello world"); - cx.simulate_shared_keystrokes("d l").await; - cx.shared_state().await.assert_eq("ˇello world"); - cx.simulate_shared_keystrokes("@ w").await; - cx.shared_state().await.assert_eq("ˇllo world"); - } - - #[gpui::test] - async fn test_record_replay_interleaved(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇhello world").await; - cx.simulate_shared_keystrokes("q z r a l q").await; - cx.shared_state().await.assert_eq("aˇello world"); - cx.simulate_shared_keystrokes("q b @ z @ z q").await; - cx.shared_state().await.assert_eq("aaaˇlo world"); - cx.simulate_shared_keystrokes("@ @").await; - cx.shared_state().await.assert_eq("aaaaˇo world"); - cx.simulate_shared_keystrokes("@ b").await; - cx.shared_state().await.assert_eq("aaaaaaˇworld"); - cx.simulate_shared_keystrokes("@ @").await; - cx.shared_state().await.assert_eq("aaaaaaaˇorld"); - cx.simulate_shared_keystrokes("q z r b l q").await; - cx.shared_state().await.assert_eq("aaaaaaabˇrld"); - cx.simulate_shared_keystrokes("@ b").await; - cx.shared_state().await.assert_eq("aaaaaaabbbˇd"); - } - - #[gpui::test] - async fn test_repeat_clear(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // Check that, when repeat is preceded by something other than a number, - // the current operator is cleared, in order to prevent infinite loops. - cx.set_state("ˇhello world", Mode::Normal); - cx.simulate_keystrokes("d ."); - assert_eq!(cx.active_operator(), None); - } - - #[gpui::test] - async fn test_repeat_clear_repeat(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "ˇthe quick brown - fox jumps over - the lazy dog" - }) - .await; - cx.simulate_shared_keystrokes("d d").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇfox jumps over - the lazy dog" - }); - cx.simulate_shared_keystrokes("d . .").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇthe lazy dog" - }); - } - - #[gpui::test] - async fn test_repeat_clear_count(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "ˇthe quick brown - fox jumps over - the lazy dog" - }) - .await; - cx.simulate_shared_keystrokes("d d").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇfox jumps over - the lazy dog" - }); - cx.simulate_shared_keystrokes("2 d .").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇfox jumps over - the lazy dog" - }); - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇthe lazy dog" - }); - - cx.set_shared_state(indoc! { - "ˇthe quick brown - fox jumps over - the lazy dog - the quick brown - fox jumps over - the lazy dog" - }) - .await; - cx.simulate_shared_keystrokes("2 d d").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇthe lazy dog - the quick brown - fox jumps over - the lazy dog" - }); - cx.simulate_shared_keystrokes("5 d .").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇthe lazy dog - the quick brown - fox jumps over - the lazy dog" - }); - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇfox jumps over - the lazy dog" - }); - } -} diff --git a/crates/vim/src/normal/scroll.rs b/crates/vim/src/normal/scroll.rs deleted file mode 100644 index 73209c8873..0000000000 --- a/crates/vim/src/normal/scroll.rs +++ /dev/null @@ -1,568 +0,0 @@ -use crate::Vim; -use editor::{ - DisplayPoint, Editor, EditorSettings, SelectionEffects, - display_map::{DisplayRow, ToDisplayPoint}, - scroll::ScrollAmount, -}; -use gpui::{Context, Window, actions}; -use language::Bias; -use settings::Settings; -use text::SelectionGoal; - -actions!( - vim, - [ - /// Scrolls up by one line. - LineUp, - /// Scrolls down by one line. - LineDown, - /// Scrolls right by one column. - ColumnRight, - /// Scrolls left by one column. - ColumnLeft, - /// Scrolls up by half a page. - ScrollUp, - /// Scrolls down by half a page. - ScrollDown, - /// Scrolls up by one page. - PageUp, - /// Scrolls down by one page. - PageDown, - /// Scrolls right by half a page's width. - HalfPageRight, - /// Scrolls left by half a page's width. - HalfPageLeft, - ] -); - -pub fn register(editor: &mut Editor, cx: &mut Context) { - Vim::action(editor, cx, |vim, _: &LineDown, window, cx| { - vim.scroll(false, window, cx, |c| ScrollAmount::Line(c.unwrap_or(1.))) - }); - Vim::action(editor, cx, |vim, _: &LineUp, window, cx| { - vim.scroll(false, window, cx, |c| ScrollAmount::Line(-c.unwrap_or(1.))) - }); - Vim::action(editor, cx, |vim, _: &ColumnRight, window, cx| { - vim.scroll(false, window, cx, |c| ScrollAmount::Column(c.unwrap_or(1.))) - }); - Vim::action(editor, cx, |vim, _: &ColumnLeft, window, cx| { - vim.scroll(false, window, cx, |c| { - ScrollAmount::Column(-c.unwrap_or(1.)) - }) - }); - Vim::action(editor, cx, |vim, _: &PageDown, window, cx| { - vim.scroll(false, window, cx, |c| ScrollAmount::Page(c.unwrap_or(1.))) - }); - Vim::action(editor, cx, |vim, _: &PageUp, window, cx| { - vim.scroll(false, window, cx, |c| ScrollAmount::Page(-c.unwrap_or(1.))) - }); - Vim::action(editor, cx, |vim, _: &HalfPageRight, window, cx| { - vim.scroll(false, window, cx, |c| { - ScrollAmount::PageWidth(c.unwrap_or(0.5)) - }) - }); - Vim::action(editor, cx, |vim, _: &HalfPageLeft, window, cx| { - vim.scroll(false, window, cx, |c| { - ScrollAmount::PageWidth(-c.unwrap_or(0.5)) - }) - }); - Vim::action(editor, cx, |vim, _: &ScrollDown, window, cx| { - vim.scroll(true, window, cx, |c| { - if let Some(c) = c { - ScrollAmount::Line(c) - } else { - ScrollAmount::Page(0.5) - } - }) - }); - Vim::action(editor, cx, |vim, _: &ScrollUp, window, cx| { - vim.scroll(true, window, cx, |c| { - if let Some(c) = c { - ScrollAmount::Line(-c) - } else { - ScrollAmount::Page(-0.5) - } - }) - }); -} - -impl Vim { - fn scroll( - &mut self, - move_cursor: bool, - window: &mut Window, - cx: &mut Context, - by: fn(c: Option) -> ScrollAmount, - ) { - let amount = by(Vim::take_count(cx).map(|c| c as f32)); - Vim::take_forced_motion(cx); - self.exit_temporary_normal(window, cx); - self.update_editor(cx, |_, editor, cx| { - scroll_editor(editor, move_cursor, amount, window, cx) - }); - } -} - -fn scroll_editor( - editor: &mut Editor, - preserve_cursor_position: bool, - amount: ScrollAmount, - window: &mut Window, - cx: &mut Context, -) { - let should_move_cursor = editor.newest_selection_on_screen(cx).is_eq(); - let old_top_anchor = editor.scroll_manager.anchor().anchor; - - if editor.scroll_hover(amount, window, cx) { - return; - } - - let full_page_up = amount.is_full_page() && amount.direction().is_upwards(); - let amount = match (amount.is_full_page(), editor.visible_line_count()) { - (true, Some(visible_line_count)) => { - if amount.direction().is_upwards() { - ScrollAmount::Line((amount.lines(visible_line_count) + 1.0) as f32) - } else { - ScrollAmount::Line((amount.lines(visible_line_count) - 1.0) as f32) - } - } - _ => amount, - }; - - editor.scroll_screen(&amount, window, cx); - if !should_move_cursor { - return; - } - - let Some(visible_line_count) = editor.visible_line_count() else { - return; - }; - - let Some(visible_column_count) = editor.visible_column_count() else { - return; - }; - - let top_anchor = editor.scroll_manager.anchor().anchor; - let vertical_scroll_margin = EditorSettings::get_global(cx).vertical_scroll_margin; - - editor.change_selections( - SelectionEffects::no_scroll().nav_history(false), - window, - cx, - |s| { - s.move_with(|map, selection| { - // TODO: Improve the logic and function calls below to be dependent on - // the `amount`. If the amount is vertical, we don't care about - // columns, while if it's horizontal, we don't care about rows, - // so we don't need to calculate both and deal with logic for - // both. - let mut head = selection.head(); - let top = top_anchor.to_display_point(map); - let max_point = map.max_point(); - let starting_column = head.column(); - - let vertical_scroll_margin = - (vertical_scroll_margin as u32).min(visible_line_count as u32 / 2); - - if preserve_cursor_position { - let old_top = old_top_anchor.to_display_point(map); - let new_row = if old_top.row() == top.row() { - DisplayRow( - head.row() - .0 - .saturating_add_signed(amount.lines(visible_line_count) as i32), - ) - } else { - DisplayRow(top.row().0 + selection.head().row().0 - old_top.row().0) - }; - head = map.clip_point(DisplayPoint::new(new_row, head.column()), Bias::Left) - } - - let min_row = if top.row().0 == 0 { - DisplayRow(0) - } else { - DisplayRow(top.row().0 + vertical_scroll_margin) - }; - - let max_visible_row = top.row().0.saturating_add( - (visible_line_count as u32).saturating_sub(1 + vertical_scroll_margin), - ); - // scroll off the end. - let max_row = if top.row().0 + visible_line_count as u32 >= max_point.row().0 { - max_point.row() - } else { - DisplayRow( - (top.row().0 + visible_line_count as u32) - .saturating_sub(1 + vertical_scroll_margin), - ) - }; - - let new_row = if full_page_up { - // Special-casing ctrl-b/page-up, which is special-cased by Vim, it seems - // to always put the cursor on the last line of the page, even if the cursor - // was before that. - DisplayRow(max_visible_row) - } else if head.row() < min_row { - min_row - } else if head.row() > max_row { - max_row - } else { - head.row() - }; - - // The minimum column position that the cursor position can be - // at is either the scroll manager's anchor column, which is the - // left-most column in the visible area, or the scroll manager's - // old anchor column, in case the cursor position is being - // preserved. This is necessary for motions like `ctrl-d` in - // case there's not enough content to scroll half page down, in - // which case the scroll manager's anchor column will be the - // maximum column for the current line, so the minimum column - // would end up being the same as the maximum column. - let min_column = match preserve_cursor_position { - true => old_top_anchor.to_display_point(map).column(), - false => top.column(), - }; - - // As for the maximum column position, that should be either the - // right-most column in the visible area, which we can easily - // calculate by adding the visible column count to the minimum - // column position, or the right-most column in the current - // line, seeing as the cursor might be in a short line, in which - // case we don't want to go past its last column. - let max_row_column = if new_row <= map.max_point().row() { - map.line_len(new_row) - } else { - 0 - }; - let max_column = match min_column + visible_column_count as u32 { - max_column if max_column >= max_row_column => max_row_column, - max_column => max_column, - }; - - // Ensure that the cursor's column stays within the visible - // area, otherwise clip it at either the left or right edge of - // the visible area. - let new_column = match (min_column, max_column) { - (min_column, _) if starting_column < min_column => min_column, - (_, max_column) if starting_column > max_column => max_column, - _ => starting_column, - }; - - let new_head = map.clip_point(DisplayPoint::new(new_row, new_column), Bias::Left); - let goal = match amount { - ScrollAmount::Column(_) | ScrollAmount::PageWidth(_) => SelectionGoal::None, - _ => selection.goal, - }; - - if selection.is_empty() { - selection.collapse_to(new_head, goal) - } else { - selection.set_head(new_head, goal) - }; - }) - }, - ); -} - -#[cfg(test)] -mod test { - use crate::{ - state::Mode, - test::{NeovimBackedTestContext, VimTestContext}, - }; - use editor::ScrollBeyondLastLine; - use gpui::{AppContext as _, point, px, size}; - use indoc::indoc; - use language::Point; - use settings::SettingsStore; - - pub fn sample_text(rows: usize, cols: usize, start_char: char) -> String { - let mut text = String::new(); - for row in 0..rows { - let c: char = (start_char as u32 + row as u32) as u8 as char; - let mut line = c.to_string().repeat(cols); - if row < rows - 1 { - line.push('\n'); - } - text += &line; - } - text - } - - #[gpui::test] - async fn test_scroll(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - let (line_height, visible_line_count) = cx.update_editor(|editor, window, cx| { - ( - editor - .style(cx) - .text - .line_height_in_pixels(window.rem_size()), - editor.visible_line_count().unwrap(), - ) - }); - - let window = cx.window; - let margin = cx - .update_window(window, |_, window, _cx| { - window.viewport_size().height - line_height * visible_line_count as f32 - }) - .unwrap(); - cx.simulate_window_resize( - cx.window, - size(px(1000.), margin + 8. * line_height - px(1.0)), - ); - - cx.set_state( - indoc!( - "ˇone - two - three - four - five - six - seven - eight - nine - ten - eleven - twelve - " - ), - Mode::Normal, - ); - - cx.update_editor(|editor, window, cx| { - assert_eq!(editor.snapshot(window, cx).scroll_position(), point(0., 0.)) - }); - cx.simulate_keystrokes("ctrl-e"); - cx.update_editor(|editor, window, cx| { - assert_eq!(editor.snapshot(window, cx).scroll_position(), point(0., 1.)) - }); - cx.simulate_keystrokes("2 ctrl-e"); - cx.update_editor(|editor, window, cx| { - assert_eq!(editor.snapshot(window, cx).scroll_position(), point(0., 3.)) - }); - cx.simulate_keystrokes("ctrl-y"); - cx.update_editor(|editor, window, cx| { - assert_eq!(editor.snapshot(window, cx).scroll_position(), point(0., 2.)) - }); - - // does not select in normal mode - cx.simulate_keystrokes("g g"); - cx.update_editor(|editor, window, cx| { - assert_eq!(editor.snapshot(window, cx).scroll_position(), point(0., 0.)) - }); - cx.simulate_keystrokes("ctrl-d"); - cx.update_editor(|editor, window, cx| { - assert_eq!( - editor.snapshot(window, cx).scroll_position(), - point(0., 3.0) - ); - assert_eq!( - editor - .selections - .newest(&editor.display_snapshot(cx)) - .range(), - Point::new(6, 0)..Point::new(6, 0) - ) - }); - - // does select in visual mode - cx.simulate_keystrokes("g g"); - cx.update_editor(|editor, window, cx| { - assert_eq!(editor.snapshot(window, cx).scroll_position(), point(0., 0.)) - }); - cx.simulate_keystrokes("v ctrl-d"); - cx.update_editor(|editor, window, cx| { - assert_eq!( - editor.snapshot(window, cx).scroll_position(), - point(0., 3.0) - ); - assert_eq!( - editor - .selections - .newest(&editor.display_snapshot(cx)) - .range(), - Point::new(0, 0)..Point::new(6, 1) - ) - }); - } - - #[gpui::test] - async fn test_ctrl_d_u(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_scroll_height(10).await; - - let content = "ˇ".to_owned() + &sample_text(26, 2, 'a'); - cx.set_shared_state(&content).await; - - // skip over the scrolloff at the top - // test ctrl-d - cx.simulate_shared_keystrokes("4 j ctrl-d").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("ctrl-d").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("g g ctrl-d").await; - cx.shared_state().await.assert_matches(); - - // test ctrl-u - cx.simulate_shared_keystrokes("ctrl-u").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("ctrl-d ctrl-d 4 j ctrl-u ctrl-u") - .await; - cx.shared_state().await.assert_matches(); - - // test returning to top - cx.simulate_shared_keystrokes("g g ctrl-d ctrl-u ctrl-u") - .await; - cx.shared_state().await.assert_matches(); - } - - #[gpui::test] - async fn test_ctrl_f_b(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - let visible_lines = 10; - cx.set_scroll_height(visible_lines).await; - - // First test without vertical scroll margin - cx.neovim.set_option(&format!("scrolloff={}", 0)).await; - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |s| s.editor.vertical_scroll_margin = Some(0.0)); - }); - - let content = "ˇ".to_owned() + &sample_text(26, 2, 'a'); - cx.set_shared_state(&content).await; - - // scroll down: ctrl-f - cx.simulate_shared_keystrokes("ctrl-f").await; - cx.shared_state().await.assert_matches(); - - cx.simulate_shared_keystrokes("ctrl-f").await; - cx.shared_state().await.assert_matches(); - - // scroll up: ctrl-b - cx.simulate_shared_keystrokes("ctrl-b").await; - cx.shared_state().await.assert_matches(); - - cx.simulate_shared_keystrokes("ctrl-b").await; - cx.shared_state().await.assert_matches(); - - // Now go back to start of file, and test with vertical scroll margin - cx.simulate_shared_keystrokes("g g").await; - cx.shared_state().await.assert_matches(); - - cx.neovim.set_option(&format!("scrolloff={}", 3)).await; - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |s| s.editor.vertical_scroll_margin = Some(3.0)); - }); - - // scroll down: ctrl-f - cx.simulate_shared_keystrokes("ctrl-f").await; - cx.shared_state().await.assert_matches(); - - cx.simulate_shared_keystrokes("ctrl-f").await; - cx.shared_state().await.assert_matches(); - - // scroll up: ctrl-b - cx.simulate_shared_keystrokes("ctrl-b").await; - cx.shared_state().await.assert_matches(); - - cx.simulate_shared_keystrokes("ctrl-b").await; - cx.shared_state().await.assert_matches(); - } - - #[gpui::test] - async fn test_scroll_beyond_last_line(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_scroll_height(10).await; - - let content = "ˇ".to_owned() + &sample_text(26, 2, 'a'); - cx.set_shared_state(&content).await; - - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |s| { - s.editor.scroll_beyond_last_line = Some(ScrollBeyondLastLine::Off); - }); - }); - - // ctrl-d can reach the end and the cursor stays in the first column - cx.simulate_shared_keystrokes("shift-g k").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("ctrl-d").await; - cx.shared_state().await.assert_matches(); - - // ctrl-u from the last line - cx.simulate_shared_keystrokes("shift-g").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("ctrl-u").await; - cx.shared_state().await.assert_matches(); - } - - #[gpui::test] - async fn test_ctrl_y_e(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_scroll_height(10).await; - - let content = "ˇ".to_owned() + &sample_text(26, 2, 'a'); - cx.set_shared_state(&content).await; - - for _ in 0..8 { - cx.simulate_shared_keystrokes("ctrl-e").await; - cx.shared_state().await.assert_matches(); - } - - for _ in 0..8 { - cx.simulate_shared_keystrokes("ctrl-y").await; - cx.shared_state().await.assert_matches(); - } - } - - #[gpui::test] - async fn test_scroll_jumps(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_scroll_height(20).await; - - let content = "ˇ".to_owned() + &sample_text(52, 2, 'a'); - cx.set_shared_state(&content).await; - - cx.simulate_shared_keystrokes("shift-g g g").await; - cx.simulate_shared_keystrokes("ctrl-d ctrl-d ctrl-o").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("ctrl-o").await; - cx.shared_state().await.assert_matches(); - } - - #[gpui::test] - async fn test_horizontal_scroll(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_scroll_height(20).await; - cx.set_shared_wrap(12).await; - cx.set_neovim_option("nowrap").await; - - let content = "ˇ01234567890123456789"; - cx.set_shared_state(content).await; - - cx.simulate_shared_keystrokes("z shift-l").await; - cx.shared_state().await.assert_eq("012345ˇ67890123456789"); - - // At this point, `z h` should not move the cursor as it should still be - // visible within the 12 column width. - cx.simulate_shared_keystrokes("z h").await; - cx.shared_state().await.assert_eq("012345ˇ67890123456789"); - - let content = "ˇ01234567890123456789"; - cx.set_shared_state(content).await; - - cx.simulate_shared_keystrokes("z l").await; - cx.shared_state().await.assert_eq("0ˇ1234567890123456789"); - } -} diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs deleted file mode 100644 index 36a529da5d..0000000000 --- a/crates/vim/src/normal/search.rs +++ /dev/null @@ -1,1176 +0,0 @@ -use editor::{Editor, EditorSettings}; -use gpui::{Action, Context, Window, actions}; -use language::Point; -use schemars::JsonSchema; -use search::{BufferSearchBar, SearchOptions, buffer_search}; -use serde::Deserialize; -use settings::Settings; -use std::{iter::Peekable, str::Chars}; -use util::serde::default_true; -use workspace::{notifications::NotifyResultExt, searchable::Direction}; - -use crate::{ - Vim, - command::CommandRange, - motion::Motion, - state::{Mode, SearchState}, -}; - -/// Moves to the next search match. -#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -pub(crate) struct MoveToNext { - #[serde(default = "default_true")] - case_sensitive: bool, - #[serde(default)] - partial_word: bool, - #[serde(default = "default_true")] - regex: bool, -} - -/// Moves to the previous search match. -#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -pub(crate) struct MoveToPrevious { - #[serde(default = "default_true")] - case_sensitive: bool, - #[serde(default)] - partial_word: bool, - #[serde(default = "default_true")] - regex: bool, -} - -/// Initiates a search operation with the specified parameters. -#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -pub(crate) struct Search { - #[serde(default)] - backwards: bool, - #[serde(default = "default_true")] - regex: bool, -} - -/// Executes a find command to search for patterns in the buffer. -#[derive(Clone, Debug, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -pub struct FindCommand { - pub query: String, - pub backwards: bool, -} - -/// Executes a search and replace command within the specified range. -#[derive(Clone, Debug, PartialEq, Action)] -#[action(namespace = vim, no_json, no_register)] -pub struct ReplaceCommand { - pub(crate) range: CommandRange, - pub(crate) replacement: Replacement, -} - -#[derive(Clone, Debug, PartialEq)] -pub struct Replacement { - search: String, - replacement: String, - case_sensitive: Option, - flag_n: bool, - flag_g: bool, - flag_c: bool, -} - -actions!( - vim, - [ - /// Submits the current search query. - SearchSubmit, - /// Moves to the next search match. - MoveToNextMatch, - /// Moves to the previous search match. - MoveToPreviousMatch - ] -); - -pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { - Vim::action(editor, cx, Vim::move_to_next); - Vim::action(editor, cx, Vim::move_to_previous); - Vim::action(editor, cx, Vim::move_to_next_match); - Vim::action(editor, cx, Vim::move_to_previous_match); - Vim::action(editor, cx, Vim::search); - Vim::action(editor, cx, Vim::search_deploy); - Vim::action(editor, cx, Vim::find_command); - Vim::action(editor, cx, Vim::replace_command); -} - -impl Vim { - fn move_to_next(&mut self, action: &MoveToNext, window: &mut Window, cx: &mut Context) { - self.move_to_internal( - Direction::Next, - action.case_sensitive, - !action.partial_word, - action.regex, - window, - cx, - ) - } - - fn move_to_previous( - &mut self, - action: &MoveToPrevious, - window: &mut Window, - cx: &mut Context, - ) { - self.move_to_internal( - Direction::Prev, - action.case_sensitive, - !action.partial_word, - action.regex, - window, - cx, - ) - } - - fn move_to_next_match( - &mut self, - _: &MoveToNextMatch, - window: &mut Window, - cx: &mut Context, - ) { - self.move_to_match_internal(self.search.direction, window, cx) - } - - fn move_to_previous_match( - &mut self, - _: &MoveToPreviousMatch, - window: &mut Window, - cx: &mut Context, - ) { - self.move_to_match_internal(self.search.direction.opposite(), window, cx) - } - - fn search(&mut self, action: &Search, window: &mut Window, cx: &mut Context) { - let Some(pane) = self.pane(window, cx) else { - return; - }; - let direction = if action.backwards { - Direction::Prev - } else { - Direction::Next - }; - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - let prior_selections = self.editor_selections(window, cx); - pane.update(cx, |pane, cx| { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - search_bar.update(cx, |search_bar, cx| { - if !search_bar.show(window, cx) { - return; - } - - search_bar.select_query(window, cx); - cx.focus_self(window); - - search_bar.set_replacement(None, cx); - let mut options = SearchOptions::NONE; - if action.regex { - options |= SearchOptions::REGEX; - } - if action.backwards { - options |= SearchOptions::BACKWARDS; - } - if EditorSettings::get_global(cx).search.case_sensitive { - options |= SearchOptions::CASE_SENSITIVE; - } - search_bar.set_search_options(options, cx); - let prior_mode = if self.temp_mode { - Mode::Insert - } else { - self.mode - }; - - self.search = SearchState { - direction, - count, - prior_selections, - prior_operator: self.operator_stack.last().cloned(), - prior_mode, - helix_select: false, - } - }); - } - }) - } - - // 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(); - } - - pub fn search_submit(&mut self, window: &mut Window, cx: &mut Context) { - self.store_visual_marks(window, cx); - let Some(pane) = self.pane(window, cx) else { - return; - }; - let new_selections = self.editor_selections(window, cx); - let result = pane.update(cx, |pane, cx| { - let search_bar = pane.toolbar().read(cx).item_of_type::()?; - if self.search.helix_select { - search_bar.update(cx, |search_bar, cx| { - search_bar.select_all_matches(&Default::default(), window, cx) - }); - return None; - } - search_bar.update(cx, |search_bar, cx| { - let mut count = self.search.count; - let direction = self.search.direction; - search_bar.has_active_match(); - let new_head = new_selections.last()?.start; - let is_different_head = self - .search - .prior_selections - .last() - .is_none_or(|range| range.start != new_head); - - if is_different_head { - count = count.saturating_sub(1) - } - self.search.count = 1; - search_bar.select_match(direction, count, window, cx); - search_bar.focus_editor(&Default::default(), window, cx); - - let prior_selections: Vec<_> = self.search.prior_selections.drain(..).collect(); - let prior_mode = self.search.prior_mode; - let prior_operator = self.search.prior_operator.take(); - - let query = search_bar.query(cx).into(); - Vim::globals(cx).registers.insert('/', query); - Some((prior_selections, prior_mode, prior_operator)) - }) - }); - - let Some((mut prior_selections, prior_mode, prior_operator)) = result else { - return; - }; - - let new_selections = self.editor_selections(window, cx); - - // If the active editor has changed during a search, don't panic. - if prior_selections.iter().any(|s| { - self.update_editor(cx, |_, editor, cx| { - !s.start - .is_valid(&editor.snapshot(window, cx).buffer_snapshot()) - }) - .unwrap_or(true) - }) { - prior_selections.clear(); - } - - if prior_mode != self.mode { - self.switch_mode(prior_mode, true, window, cx); - } - if let Some(operator) = prior_operator { - self.push_operator(operator, window, cx); - }; - self.search_motion( - Motion::ZedSearchResult { - prior_selections, - new_selections, - }, - window, - cx, - ); - } - - pub fn move_to_match_internal( - &mut self, - direction: Direction, - window: &mut Window, - cx: &mut Context, - ) { - let Some(pane) = self.pane(window, cx) else { - return; - }; - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - let prior_selections = self.editor_selections(window, cx); - - let success = pane.update(cx, |pane, cx| { - let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() else { - return false; - }; - search_bar.update(cx, |search_bar, cx| { - if !search_bar.has_active_match() || !search_bar.show(window, cx) { - return false; - } - search_bar.select_match(direction, count, window, cx); - true - }) - }); - if !success { - return; - } - - let new_selections = self.editor_selections(window, cx); - self.search_motion( - Motion::ZedSearchResult { - prior_selections, - new_selections, - }, - window, - cx, - ); - } - - pub fn move_to_internal( - &mut self, - direction: Direction, - case_sensitive: bool, - whole_word: bool, - regex: bool, - window: &mut Window, - cx: &mut Context, - ) { - let Some(pane) = self.pane(window, cx) else { - return; - }; - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - let prior_selections = self.editor_selections(window, cx); - let cursor_word = self.editor_cursor_word(window, cx); - let vim = cx.entity(); - - let searched = pane.update(cx, |pane, cx| { - self.search.direction = direction; - let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() else { - return false; - }; - let search = search_bar.update(cx, |search_bar, cx| { - let mut options = SearchOptions::NONE; - if case_sensitive { - options |= SearchOptions::CASE_SENSITIVE; - } - if regex { - options |= SearchOptions::REGEX; - } - if whole_word { - options |= SearchOptions::WHOLE_WORD; - } - if !search_bar.show(window, cx) { - return None; - } - let Some(query) = search_bar - .query_suggestion(window, cx) - .or_else(|| cursor_word) - else { - drop(search_bar.search("", None, false, window, cx)); - return None; - }; - - let query = regex::escape(&query); - Some(search_bar.search(&query, Some(options), true, window, cx)) - }); - - let Some(search) = search else { return false }; - - let search_bar = search_bar.downgrade(); - cx.spawn_in(window, async move |_, cx| { - search.await?; - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.select_match(direction, count, window, cx); - - vim.update(cx, |vim, cx| { - let new_selections = vim.editor_selections(window, cx); - vim.search_motion( - Motion::ZedSearchResult { - prior_selections, - new_selections, - }, - window, - cx, - ) - }); - })?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - true - }); - if !searched { - self.clear_operator(window, cx) - } - - if self.mode.is_visual() { - self.switch_mode(Mode::Normal, false, window, cx) - } - } - - fn find_command(&mut self, action: &FindCommand, window: &mut Window, cx: &mut Context) { - let Some(pane) = self.pane(window, cx) else { - return; - }; - pane.update(cx, |pane, cx| { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - let search = search_bar.update(cx, |search_bar, cx| { - if !search_bar.show(window, cx) { - return None; - } - let mut query = action.query.clone(); - if query.is_empty() { - query = search_bar.query(cx); - }; - - let mut options = SearchOptions::REGEX | SearchOptions::CASE_SENSITIVE; - if search_bar.should_use_smartcase_search(cx) { - options.set( - SearchOptions::CASE_SENSITIVE, - search_bar.is_contains_uppercase(&query), - ); - } - - Some(search_bar.search(&query, Some(options), true, window, cx)) - }); - let Some(search) = search else { return }; - let search_bar = search_bar.downgrade(); - let direction = if action.backwards { - Direction::Prev - } else { - Direction::Next - }; - cx.spawn_in(window, async move |_, cx| { - search.await?; - search_bar.update_in(cx, |search_bar, window, cx| { - search_bar.select_match(direction, 1, window, cx) - })?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - }) - } - - fn replace_command( - &mut self, - action: &ReplaceCommand, - window: &mut Window, - cx: &mut Context, - ) { - let replacement = action.replacement.clone(); - let Some(((pane, workspace), editor)) = self - .pane(window, cx) - .zip(self.workspace(window)) - .zip(self.editor()) - else { - return; - }; - if let Some(result) = self.update_editor(cx, |vim, editor, cx| { - let range = action.range.buffer_range(vim, editor, window, cx)?; - let snapshot = editor.snapshot(window, cx); - let snapshot = snapshot.buffer_snapshot(); - let end_point = Point::new(range.end.0, snapshot.line_len(range.end)); - let range = snapshot.anchor_before(Point::new(range.start.0, 0)) - ..snapshot.anchor_after(end_point); - editor.set_search_within_ranges(&[range], cx); - anyhow::Ok(()) - }) { - workspace.update(cx, |workspace, cx| { - result.notify_err(workspace, cx); - }) - } - let Some(search_bar) = pane.update(cx, |pane, cx| { - pane.toolbar().read(cx).item_of_type::() - }) else { - return; - }; - let mut options = SearchOptions::REGEX; - let search = search_bar.update(cx, |search_bar, cx| { - if !search_bar.show(window, cx) { - return None; - } - - let search = if replacement.search.is_empty() { - search_bar.query(cx) - } else { - replacement.search - }; - - if let Some(case) = replacement.case_sensitive { - options.set(SearchOptions::CASE_SENSITIVE, case) - } else if search_bar.should_use_smartcase_search(cx) { - options.set( - SearchOptions::CASE_SENSITIVE, - search_bar.is_contains_uppercase(&search), - ); - } else { - // Fallback: no explicit i/I flags and smartcase disabled; - // use global editor.search.case_sensitive. - options.set( - SearchOptions::CASE_SENSITIVE, - EditorSettings::get_global(cx).search.case_sensitive, - ) - } - - if !replacement.flag_g { - options.set(SearchOptions::ONE_MATCH_PER_LINE, true); - } - - search_bar.set_replacement(Some(&replacement.replacement), cx); - if replacement.flag_c { - search_bar.focus_replace(window, cx); - } - Some(search_bar.search(&search, Some(options), true, window, cx)) - }); - if replacement.flag_n { - self.move_cursor( - Motion::StartOfLine { - display_lines: false, - }, - None, - window, - cx, - ); - return; - } - let Some(search) = search else { return }; - let search_bar = search_bar.downgrade(); - cx.spawn_in(window, async move |vim, cx| { - search.await?; - search_bar.update_in(cx, |search_bar, window, cx| { - if replacement.flag_c { - search_bar.select_first_match(window, cx); - return; - } - search_bar.select_last_match(window, cx); - search_bar.replace_all(&Default::default(), window, cx); - editor.update(cx, |editor, cx| editor.clear_search_within_ranges(cx)); - let _ = search_bar.search(&search_bar.query(cx), None, false, window, cx); - vim.update(cx, |vim, cx| { - vim.move_cursor( - Motion::StartOfLine { - display_lines: false, - }, - None, - window, - cx, - ) - }) - .ok(); - - // Disable the `ONE_MATCH_PER_LINE` search option when finished, as - // this is not properly supported outside of vim mode, and - // not disabling it makes the "Replace All Matches" button - // actually replace only the first match on each line. - options.set(SearchOptions::ONE_MATCH_PER_LINE, false); - search_bar.set_search_options(options, cx); - }) - }) - .detach_and_log_err(cx); - } -} - -impl Replacement { - // convert a vim query into something more usable by zed. - // we don't attempt to fully convert between the two regex syntaxes, - // but we do flip \( and \) to ( and ) (and vice-versa) in the pattern, - // and convert \0..\9 to $0..$9 in the replacement so that common idioms work. - pub(crate) fn parse(mut chars: Peekable) -> Option { - let delimiter = chars - .next() - .filter(|c| !c.is_alphanumeric() && *c != '"' && *c != '|' && *c != '\'')?; - - let mut search = String::new(); - let mut replacement = String::new(); - let mut flags = String::new(); - - let mut buffer = &mut search; - - let mut escaped = false; - // 0 - parsing search - // 1 - parsing replacement - // 2 - parsing flags - let mut phase = 0; - - for c in chars { - if escaped { - escaped = false; - if phase == 1 && c.is_ascii_digit() { - buffer.push('$') - // unescape escaped parens - } else if phase == 0 && (c == '(' || c == ')') { - } else if c != delimiter { - buffer.push('\\') - } - buffer.push(c) - } else if c == '\\' { - escaped = true; - } else if c == delimiter { - if phase == 0 { - buffer = &mut replacement; - phase = 1; - } else if phase == 1 { - buffer = &mut flags; - phase = 2; - } else { - break; - } - } else { - // escape unescaped parens - if phase == 0 && (c == '(' || c == ')') { - buffer.push('\\') - } - buffer.push(c) - } - } - - let mut replacement = Replacement { - search, - replacement, - case_sensitive: None, - flag_g: false, - flag_n: false, - flag_c: false, - }; - - for c in flags.chars() { - match c { - 'g' => replacement.flag_g = true, - 'n' => replacement.flag_n = true, - 'c' => replacement.flag_c = true, - 'i' => replacement.case_sensitive = Some(false), - 'I' => replacement.case_sensitive = Some(true), - _ => {} - } - } - - Some(replacement) - } -} - -#[cfg(test)] -mod test { - use std::time::Duration; - - use crate::{ - state::Mode, - test::{NeovimBackedTestContext, VimTestContext}, - }; - use editor::{DisplayPoint, display_map::DisplayRow}; - - use indoc::indoc; - use search::BufferSearchBar; - use settings::SettingsStore; - - #[gpui::test] - async fn test_move_to_next(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal); - - cx.simulate_keystrokes("*"); - cx.run_until_parked(); - cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal); - - cx.simulate_keystrokes("*"); - cx.run_until_parked(); - cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal); - - cx.simulate_keystrokes("#"); - cx.run_until_parked(); - cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal); - - cx.simulate_keystrokes("#"); - cx.run_until_parked(); - cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal); - - cx.simulate_keystrokes("2 *"); - cx.run_until_parked(); - cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal); - - cx.simulate_keystrokes("g *"); - cx.run_until_parked(); - cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal); - - cx.simulate_keystrokes("n"); - cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal); - - cx.simulate_keystrokes("g #"); - cx.run_until_parked(); - cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal); - } - - #[gpui::test] - async fn test_move_to_next_with_no_search_wrap(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |s| s.editor.search_wrap = Some(false)); - }); - - cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal); - - cx.simulate_keystrokes("*"); - cx.run_until_parked(); - cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal); - - cx.simulate_keystrokes("*"); - cx.run_until_parked(); - cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal); - - cx.simulate_keystrokes("#"); - cx.run_until_parked(); - cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal); - - cx.simulate_keystrokes("3 *"); - cx.run_until_parked(); - cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal); - - cx.simulate_keystrokes("g *"); - cx.run_until_parked(); - cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal); - - cx.simulate_keystrokes("n"); - cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal); - - cx.simulate_keystrokes("g #"); - cx.run_until_parked(); - cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal); - } - - #[gpui::test] - async fn test_search(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal); - cx.simulate_keystrokes("/ c c"); - - let search_bar = cx.workspace(|workspace, _, cx| { - workspace - .active_pane() - .read(cx) - .toolbar() - .read(cx) - .item_of_type::() - .expect("Buffer search bar should be deployed") - }); - - cx.update_entity(search_bar, |bar, _window, cx| { - assert_eq!(bar.query(cx), "cc"); - }); - - cx.run_until_parked(); - - cx.update_editor(|editor, window, cx| { - let highlights = editor.all_text_background_highlights(window, cx); - assert_eq!(3, highlights.len()); - assert_eq!( - DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 2), - highlights[0].0 - ) - }); - - cx.simulate_keystrokes("enter"); - cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal); - - // n to go to next/N to go to previous - cx.simulate_keystrokes("n"); - cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal); - cx.simulate_keystrokes("shift-n"); - cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal); - - // ? to go to previous - cx.simulate_keystrokes("? enter"); - cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal); - cx.simulate_keystrokes("? enter"); - cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal); - - // / to go to next - cx.simulate_keystrokes("/ enter"); - cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal); - - // ?{search} to search backwards - cx.simulate_keystrokes("? b enter"); - cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal); - - // works with counts - cx.simulate_keystrokes("4 / c"); - cx.simulate_keystrokes("enter"); - cx.assert_state("aa\nbb\ncc\ncˇc\ncc\n", Mode::Normal); - - // check that searching resumes from cursor, not previous match - cx.set_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal); - cx.simulate_keystrokes("/ d"); - cx.simulate_keystrokes("enter"); - cx.assert_state("aa\nbb\nˇdd\ncc\nbb\n", Mode::Normal); - cx.update_editor(|editor, window, cx| { - editor.move_to_beginning(&Default::default(), window, cx) - }); - cx.assert_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal); - cx.simulate_keystrokes("/ b"); - cx.simulate_keystrokes("enter"); - cx.assert_state("aa\nˇbb\ndd\ncc\nbb\n", Mode::Normal); - - // check that searching switches to normal mode if in visual mode - cx.set_state("ˇone two one", Mode::Normal); - cx.simulate_keystrokes("v l l"); - cx.assert_editor_state("«oneˇ» two one"); - cx.simulate_keystrokes("*"); - cx.assert_state("one two ˇone", Mode::Normal); - - // check that a backward search after last match works correctly - cx.set_state("aa\naa\nbbˇ", Mode::Normal); - cx.simulate_keystrokes("? a a"); - cx.simulate_keystrokes("enter"); - cx.assert_state("aa\nˇaa\nbb", Mode::Normal); - - // check that searching with unable search wrap - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |s| s.editor.search_wrap = Some(false)); - }); - cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal); - cx.simulate_keystrokes("/ c c enter"); - - cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal); - - // n to go to next/N to go to previous - cx.simulate_keystrokes("n"); - cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal); - cx.simulate_keystrokes("shift-n"); - cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal); - - // ? to go to previous - cx.simulate_keystrokes("? enter"); - cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal); - cx.simulate_keystrokes("? enter"); - cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal); - } - - #[gpui::test] - async fn test_non_vim_search(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, false).await; - cx.cx.set_state("ˇone one one one"); - cx.run_until_parked(); - cx.simulate_keystrokes("cmd-f"); - cx.run_until_parked(); - - cx.assert_editor_state("«oneˇ» one one one"); - cx.simulate_keystrokes("enter"); - cx.assert_editor_state("one «oneˇ» one one"); - cx.simulate_keystrokes("shift-enter"); - cx.assert_editor_state("«oneˇ» one one one"); - } - - #[gpui::test] - async fn test_visual_star_hash(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇa.c. abcd a.c. abcd").await; - cx.simulate_shared_keystrokes("v 3 l *").await; - cx.shared_state().await.assert_eq("a.c. abcd ˇa.c. abcd"); - } - - #[gpui::test] - async fn test_d_search(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇa.c. abcd a.c. abcd").await; - cx.simulate_shared_keystrokes("d / c d").await; - cx.simulate_shared_keystrokes("enter").await; - cx.shared_state().await.assert_eq("ˇcd a.c. abcd"); - } - - #[gpui::test] - async fn test_backwards_n(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇa b a b a b a").await; - cx.simulate_shared_keystrokes("*").await; - cx.simulate_shared_keystrokes("n").await; - cx.shared_state().await.assert_eq("a b a b ˇa b a"); - cx.simulate_shared_keystrokes("#").await; - cx.shared_state().await.assert_eq("a b ˇa b a b a"); - cx.simulate_shared_keystrokes("n").await; - cx.shared_state().await.assert_eq("ˇa b a b a b a"); - } - - #[gpui::test] - async fn test_v_search(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇa.c. abcd a.c. abcd").await; - cx.simulate_shared_keystrokes("v / c d").await; - cx.simulate_shared_keystrokes("enter").await; - cx.shared_state().await.assert_eq("«a.c. abcˇ»d a.c. abcd"); - - cx.set_shared_state("a a aˇ a a a").await; - cx.simulate_shared_keystrokes("v / a").await; - cx.simulate_shared_keystrokes("enter").await; - cx.shared_state().await.assert_eq("a a a« aˇ» a a"); - cx.simulate_shared_keystrokes("/ enter").await; - cx.shared_state().await.assert_eq("a a a« a aˇ» a"); - cx.simulate_shared_keystrokes("? enter").await; - cx.shared_state().await.assert_eq("a a a« aˇ» a a"); - cx.simulate_shared_keystrokes("? enter").await; - cx.shared_state().await.assert_eq("a a «ˇa »a a a"); - cx.simulate_shared_keystrokes("/ enter").await; - cx.shared_state().await.assert_eq("a a a« aˇ» a a"); - cx.simulate_shared_keystrokes("/ enter").await; - cx.shared_state().await.assert_eq("a a a« a aˇ» a"); - } - - #[gpui::test] - async fn test_v_search_aa(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇaa aa").await; - cx.simulate_shared_keystrokes("v / a a").await; - cx.simulate_shared_keystrokes("enter").await; - cx.shared_state().await.assert_eq("«aa aˇ»a"); - } - - #[gpui::test] - async fn test_visual_block_search(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "ˇone two - three four - five six - " - }) - .await; - cx.simulate_shared_keystrokes("ctrl-v j / f").await; - cx.simulate_shared_keystrokes("enter").await; - cx.shared_state().await.assert_eq(indoc! { - "«one twoˇ» - «three fˇ»our - five six - " - }); - } - - #[gpui::test] - async fn test_replace_with_range_at_start(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "ˇa - a - a - a - a - a - a - " - }) - .await; - cx.simulate_shared_keystrokes(": 2 , 5 s / ^ / b").await; - cx.simulate_shared_keystrokes("enter").await; - cx.shared_state().await.assert_eq(indoc! { - "a - ba - ba - ba - ˇba - a - a - " - }); - - cx.simulate_shared_keystrokes("/ a").await; - cx.simulate_shared_keystrokes("enter").await; - cx.shared_state().await.assert_eq(indoc! { - "a - ba - ba - ba - bˇa - a - a - " - }); - } - - #[gpui::test] - async fn test_search_skipping(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! { - "ˇaa aa aa" - }) - .await; - - cx.simulate_shared_keystrokes("/ a a").await; - cx.simulate_shared_keystrokes("enter").await; - - cx.shared_state().await.assert_eq(indoc! { - "aa ˇaa aa" - }); - - cx.simulate_shared_keystrokes("left / a a").await; - cx.simulate_shared_keystrokes("enter").await; - - cx.shared_state().await.assert_eq(indoc! { - "aa ˇaa aa" - }); - } - - #[gpui::test] - async fn test_replace_n(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! { - "ˇaa - bb - aa" - }) - .await; - - cx.simulate_shared_keystrokes(": s / b b / d d / n").await; - cx.simulate_shared_keystrokes("enter").await; - - cx.shared_state().await.assert_eq(indoc! { - "ˇaa - bb - aa" - }); - - let search_bar = cx.update_workspace(|workspace, _, cx| { - workspace.active_pane().update(cx, |pane, cx| { - pane.toolbar() - .read(cx) - .item_of_type::() - .unwrap() - }) - }); - cx.update_entity(search_bar, |search_bar, _, cx| { - assert!(!search_bar.is_dismissed()); - assert_eq!(search_bar.query(cx), "bb".to_string()); - assert_eq!(search_bar.replacement(cx), "dd".to_string()); - }) - } - - #[gpui::test] - async fn test_replace_g(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! { - "ˇaa aa aa aa - aa - aa" - }) - .await; - - cx.simulate_shared_keystrokes(": s / a a / b b").await; - cx.simulate_shared_keystrokes("enter").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇbb aa aa aa - aa - aa" - }); - cx.simulate_shared_keystrokes(": s / a a / b b / g").await; - cx.simulate_shared_keystrokes("enter").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇbb bb bb bb - aa - aa" - }); - } - - #[gpui::test] - async fn test_replace_c(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.set_state( - indoc! { - "ˇaa - aa - aa" - }, - Mode::Normal, - ); - - cx.simulate_keystrokes("v j : s / a a / d d / c"); - cx.simulate_keystrokes("enter"); - - cx.assert_state( - indoc! { - "ˇaa - aa - aa" - }, - Mode::Normal, - ); - - cx.simulate_keystrokes("enter"); - - cx.assert_state( - indoc! { - "dd - ˇaa - aa" - }, - Mode::Normal, - ); - - cx.simulate_keystrokes("enter"); - cx.assert_state( - indoc! { - "dd - ddˇ - aa" - }, - Mode::Normal, - ); - cx.simulate_keystrokes("enter"); - cx.assert_state( - indoc! { - "dd - ddˇ - aa" - }, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_replace_with_range(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "ˇa - a - a - a - a - a - a - " - }) - .await; - cx.simulate_shared_keystrokes(": 2 , 5 s / a / b").await; - cx.simulate_shared_keystrokes("enter").await; - cx.shared_state().await.assert_eq(indoc! { - "a - b - b - b - ˇb - a - a - " - }); - cx.executor().advance_clock(Duration::from_millis(250)); - cx.run_until_parked(); - - cx.simulate_shared_keystrokes("/ a enter").await; - cx.shared_state().await.assert_eq(indoc! { - "a - b - b - b - b - ˇa - a - " - }); - } -} diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs deleted file mode 100644 index df8d7b4879..0000000000 --- a/crates/vim/src/normal/substitute.rs +++ /dev/null @@ -1,312 +0,0 @@ -use editor::{Editor, SelectionEffects, movement}; -use gpui::{Context, Window, actions}; -use language::Point; - -use crate::{ - Mode, Vim, - motion::{Motion, MotionKind}, -}; - -actions!( - vim, - [ - /// Substitutes characters in the current selection. - Substitute, - /// Substitutes the entire line. - SubstituteLine - ] -); - -pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { - Vim::action(editor, cx, |vim, _: &Substitute, window, cx| { - vim.start_recording(cx); - let count = Vim::take_count(cx); - Vim::take_forced_motion(cx); - vim.substitute(count, vim.mode == Mode::VisualLine, window, cx); - }); - - Vim::action(editor, cx, |vim, _: &SubstituteLine, window, cx| { - vim.start_recording(cx); - if matches!(vim.mode, Mode::VisualBlock | Mode::Visual) { - vim.switch_mode(Mode::VisualLine, false, window, cx) - } - let count = Vim::take_count(cx); - Vim::take_forced_motion(cx); - vim.substitute(count, true, window, cx) - }); -} - -impl Vim { - pub fn substitute( - &mut self, - count: Option, - line_mode: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.store_visual_marks(window, cx); - self.update_editor(cx, |vim, editor, cx| { - editor.set_clip_at_line_ends(false, cx); - editor.transact(window, cx, |editor, window, cx| { - let text_layout_details = editor.text_layout_details(window); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - if selection.start == selection.end { - Motion::Right.expand_selection( - map, - selection, - count, - &text_layout_details, - false, - ); - } - if line_mode { - // in Visual mode when the selection contains the newline at the end - // of the line, we should exclude it. - if !selection.is_empty() && selection.end.column() == 0 { - selection.end = movement::left(map, selection.end); - } - Motion::CurrentLine.expand_selection( - map, - selection, - None, - &text_layout_details, - false, - ); - if let Some((point, _)) = (Motion::FirstNonWhitespace { - display_lines: false, - }) - .move_point( - map, - selection.start, - selection.goal, - None, - &text_layout_details, - ) { - selection.start = point; - } - } - }) - }); - let kind = if line_mode { - MotionKind::Linewise - } else { - MotionKind::Exclusive - }; - vim.copy_selections_content(editor, kind, window, cx); - let selections = editor - .selections - .all::(&editor.display_snapshot(cx)) - .into_iter(); - let edits = selections.map(|selection| (selection.start..selection.end, "")); - editor.edit(edits, cx); - }); - }); - self.switch_mode(Mode::Insert, true, window, cx); - } -} - -#[cfg(test)] -mod test { - use crate::{ - state::Mode, - test::{NeovimBackedTestContext, VimTestContext}, - }; - use indoc::indoc; - - #[gpui::test] - async fn test_substitute(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // supports a single cursor - cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal); - cx.simulate_keystrokes("s x"); - cx.assert_editor_state("xˇbc\n"); - - // supports a selection - cx.set_state(indoc! {"a«bcˇ»\n"}, Mode::Visual); - cx.assert_editor_state("a«bcˇ»\n"); - cx.simulate_keystrokes("s x"); - cx.assert_editor_state("axˇ\n"); - - // supports counts - cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal); - cx.simulate_keystrokes("2 s x"); - cx.assert_editor_state("xˇc\n"); - - // supports multiple cursors - cx.set_state(indoc! {"a«bcˇ»deˇffg\n"}, Mode::Normal); - cx.simulate_keystrokes("2 s x"); - cx.assert_editor_state("axˇdexˇg\n"); - - // does not read beyond end of line - cx.set_state(indoc! {"ˇabc\n"}, Mode::Normal); - cx.simulate_keystrokes("5 s x"); - cx.assert_editor_state("xˇ\n"); - - // it handles multibyte characters - cx.set_state(indoc! {"ˇcàfé\n"}, Mode::Normal); - cx.simulate_keystrokes("4 s"); - cx.assert_editor_state("ˇ\n"); - - // should transactionally undo selection changes - cx.simulate_keystrokes("escape u"); - cx.assert_editor_state("ˇcàfé\n"); - - // it handles visual line mode - cx.set_state( - indoc! {" - alpha - beˇta - gamma"}, - Mode::Normal, - ); - cx.simulate_keystrokes("shift-v s"); - cx.assert_editor_state(indoc! {" - alpha - ˇ - gamma"}); - } - - #[gpui::test] - async fn test_visual_change(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("The quick ˇbrown").await; - cx.simulate_shared_keystrokes("v w c").await; - cx.shared_state().await.assert_eq("The quick ˇ"); - - cx.set_shared_state(indoc! {" - The ˇquick brown - fox jumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("v w j c").await; - cx.shared_state().await.assert_eq(indoc! {" - The ˇver - the lazy dog"}); - - cx.simulate_at_each_offset( - "v w j c", - indoc! {" - The ˇquick brown - fox jumps ˇover - the ˇlazy dog"}, - ) - .await - .assert_matches(); - cx.simulate_at_each_offset( - "v w k c", - indoc! {" - The ˇquick brown - fox jumps ˇover - the ˇlazy dog"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_visual_line_change(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.simulate( - "shift-v c", - indoc! {" - The quˇick brown - fox jumps over - the lazy dog"}, - ) - .await - .assert_matches(); - // Test pasting code copied on change - cx.simulate_shared_keystrokes("escape j p").await; - cx.shared_state().await.assert_matches(); - - cx.simulate_at_each_offset( - "shift-v c", - indoc! {" - The quick brown - fox juˇmps over - the laˇzy dog"}, - ) - .await - .assert_matches(); - cx.simulate( - "shift-v j c", - indoc! {" - The quˇick brown - fox jumps over - the lazy dog"}, - ) - .await - .assert_matches(); - // Test pasting code copied on delete - cx.simulate_shared_keystrokes("escape j p").await; - cx.shared_state().await.assert_matches(); - - cx.simulate_at_each_offset( - "shift-v j c", - indoc! {" - The quick brown - fox juˇmps over - the laˇzy dog"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_substitute_line(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - let initial_state = indoc! {" - The quick brown - fox juˇmps over - the lazy dog - "}; - - // normal mode - cx.set_shared_state(initial_state).await; - cx.simulate_shared_keystrokes("shift-s o").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - oˇ - the lazy dog - "}); - - // visual mode - cx.set_shared_state(initial_state).await; - cx.simulate_shared_keystrokes("v k shift-s o").await; - cx.shared_state().await.assert_eq(indoc! {" - oˇ - the lazy dog - "}); - - // visual block mode - cx.set_shared_state(initial_state).await; - cx.simulate_shared_keystrokes("ctrl-v j shift-s o").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - oˇ - "}); - - // visual mode including newline - cx.set_shared_state(initial_state).await; - cx.simulate_shared_keystrokes("v $ shift-s o").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - oˇ - the lazy dog - "}); - - // indentation - cx.set_neovim_option("shiftwidth=4").await; - cx.set_shared_state(initial_state).await; - cx.simulate_shared_keystrokes("> > shift-s o").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - oˇ - the lazy dog - "}); - } -} diff --git a/crates/vim/src/normal/toggle_comments.rs b/crates/vim/src/normal/toggle_comments.rs deleted file mode 100644 index 17c3b2d363..0000000000 --- a/crates/vim/src/normal/toggle_comments.rs +++ /dev/null @@ -1,74 +0,0 @@ -use crate::{Vim, motion::Motion, object::Object}; -use collections::HashMap; -use editor::{Bias, SelectionEffects, display_map::ToDisplayPoint}; -use gpui::{Context, Window}; -use language::SelectionGoal; - -impl Vim { - pub fn toggle_comments_motion( - &mut self, - motion: Motion, - times: Option, - forced_motion: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - self.update_editor(cx, |_, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - editor.transact(window, cx, |editor, window, cx| { - let mut selection_starts: HashMap<_, _> = Default::default(); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let anchor = map.display_point_to_anchor(selection.head(), Bias::Right); - selection_starts.insert(selection.id, anchor); - motion.expand_selection( - map, - selection, - times, - &text_layout_details, - forced_motion, - ); - }); - }); - editor.toggle_comments(&Default::default(), window, cx); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let anchor = selection_starts.remove(&selection.id).unwrap(); - selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); - }); - }); - }); - }); - } - - pub fn toggle_comments_object( - &mut self, - object: Object, - around: bool, - times: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - self.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - let mut original_positions: HashMap<_, _> = Default::default(); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let anchor = map.display_point_to_anchor(selection.head(), Bias::Right); - original_positions.insert(selection.id, anchor); - object.expand_selection(map, selection, around, times); - }); - }); - editor.toggle_comments(&Default::default(), window, cx); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let anchor = original_positions.remove(&selection.id).unwrap(); - selection.collapse_to(anchor.to_display_point(map), SelectionGoal::None); - }); - }); - }); - }); - } -} diff --git a/crates/vim/src/normal/yank.rs b/crates/vim/src/normal/yank.rs deleted file mode 100644 index 9920b8fc88..0000000000 --- a/crates/vim/src/normal/yank.rs +++ /dev/null @@ -1,246 +0,0 @@ -use std::{ops::Range, time::Duration}; - -use crate::{ - Vim, VimSettings, - motion::{Motion, MotionKind}, - object::Object, - state::{Mode, Register}, -}; -use collections::HashMap; -use editor::{ClipboardSelection, Editor, SelectionEffects}; -use gpui::Context; -use gpui::Window; -use language::Point; -use settings::Settings; - -struct HighlightOnYank; - -impl Vim { - pub fn yank_motion( - &mut self, - motion: Motion, - times: Option, - forced_motion: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.update_editor(cx, |vim, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - editor.transact(window, cx, |editor, window, cx| { - editor.set_clip_at_line_ends(false, cx); - let mut original_positions: HashMap<_, _> = Default::default(); - let mut kind = None; - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let original_position = (selection.head(), selection.goal); - kind = motion.expand_selection( - map, - selection, - times, - &text_layout_details, - forced_motion, - ); - if kind == Some(MotionKind::Exclusive) { - original_positions - .insert(selection.id, (selection.start, selection.goal)); - } else { - original_positions.insert(selection.id, original_position); - } - }) - }); - let Some(kind) = kind else { return }; - vim.yank_selections_content(editor, kind, window, cx); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|_, selection| { - let (head, goal) = original_positions.remove(&selection.id).unwrap(); - selection.collapse_to(head, goal); - }); - }); - }); - }); - self.exit_temporary_normal(window, cx); - } - - pub fn yank_object( - &mut self, - object: Object, - around: bool, - times: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.update_editor(cx, |vim, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - editor.set_clip_at_line_ends(false, cx); - let mut start_positions: HashMap<_, _> = Default::default(); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - object.expand_selection(map, selection, around, times); - let start_position = (selection.start, selection.goal); - start_positions.insert(selection.id, start_position); - }); - }); - let kind = match object.target_visual_mode(vim.mode, around) { - Mode::VisualLine => MotionKind::Linewise, - _ => MotionKind::Exclusive, - }; - vim.yank_selections_content(editor, kind, window, cx); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|_, selection| { - let (head, goal) = start_positions.remove(&selection.id).unwrap(); - selection.collapse_to(head, goal); - }); - }); - }); - }); - self.exit_temporary_normal(window, cx); - } - - pub fn yank_selections_content( - &mut self, - editor: &mut Editor, - kind: MotionKind, - window: &mut Window, - cx: &mut Context, - ) { - self.copy_ranges( - editor, - kind, - true, - editor - .selections - .all_adjusted(&editor.display_snapshot(cx)) - .iter() - .map(|s| s.range()) - .collect(), - window, - cx, - ) - } - - pub fn copy_selections_content( - &mut self, - editor: &mut Editor, - kind: MotionKind, - window: &mut Window, - cx: &mut Context, - ) { - self.copy_ranges( - editor, - kind, - false, - editor - .selections - .all_adjusted(&editor.display_snapshot(cx)) - .iter() - .map(|s| s.range()) - .collect(), - window, - cx, - ) - } - - pub(crate) fn copy_ranges( - &mut self, - editor: &mut Editor, - kind: MotionKind, - is_yank: bool, - selections: Vec>, - window: &mut Window, - cx: &mut Context, - ) { - let buffer = editor.buffer().read(cx).snapshot(cx); - self.set_mark( - "[".to_string(), - selections - .iter() - .map(|s| buffer.anchor_before(s.start)) - .collect(), - editor.buffer(), - window, - cx, - ); - self.set_mark( - "]".to_string(), - selections - .iter() - .map(|s| buffer.anchor_after(s.end)) - .collect(), - editor.buffer(), - window, - cx, - ); - - let mut text = String::new(); - let mut clipboard_selections = Vec::with_capacity(selections.len()); - let mut ranges_to_highlight = Vec::new(); - - { - let mut is_first = true; - for selection in selections.iter() { - let start = selection.start; - let end = selection.end; - if is_first { - is_first = false; - } else { - text.push('\n'); - } - let initial_len = text.len(); - - let start_anchor = buffer.anchor_after(start); - let end_anchor = buffer.anchor_before(end); - ranges_to_highlight.push(start_anchor..end_anchor); - - for chunk in buffer.text_for_range(start..end) { - text.push_str(chunk); - } - if kind.linewise() { - text.push('\n'); - } - clipboard_selections.push(ClipboardSelection::for_buffer( - text.len() - initial_len, - false, - start..end, - &buffer, - editor.project(), - cx, - )); - } - } - - let selected_register = self.selected_register.take(); - Vim::update_globals(cx, |globals, cx| { - globals.write_registers( - Register { - text: text.into(), - clipboard_selections: Some(clipboard_selections), - }, - selected_register, - is_yank, - kind, - cx, - ) - }); - - let highlight_duration = VimSettings::get_global(cx).highlight_on_yank_duration; - if !is_yank || self.mode == Mode::Visual || highlight_duration == 0 { - return; - } - - editor.highlight_background::( - &ranges_to_highlight, - |_, colors| colors.colors().editor_document_highlight_read_background, - cx, - ); - cx.spawn(async move |this, cx| { - cx.background_executor() - .timer(Duration::from_millis(highlight_duration)) - .await; - this.update(cx, |editor, cx| { - editor.clear_background_highlights::(cx) - }) - .ok(); - }) - .detach(); - } -} diff --git a/crates/vim/src/object.rs b/crates/vim/src/object.rs deleted file mode 100644 index f11386d02d..0000000000 --- a/crates/vim/src/object.rs +++ /dev/null @@ -1,3414 +0,0 @@ -use std::ops::Range; - -use crate::{ - Vim, - motion::right, - state::{Mode, Operator}, -}; -use editor::{ - Bias, BufferOffset, DisplayPoint, Editor, MultiBufferOffset, ToOffset, - display_map::{DisplaySnapshot, ToDisplayPoint}, - movement::{self, FindRange}, -}; -use gpui::{Action, Window, actions}; -use itertools::Itertools; -use language::{BufferSnapshot, CharKind, Point, Selection, TextObject, TreeSitterOptions}; -use multi_buffer::MultiBufferRow; -use schemars::JsonSchema; -use serde::Deserialize; -use ui::Context; - -#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum Object { - Word { ignore_punctuation: bool }, - Subword { ignore_punctuation: bool }, - Sentence, - Paragraph, - Quotes, - BackQuotes, - AnyQuotes, - MiniQuotes, - DoubleQuotes, - VerticalBars, - AnyBrackets, - MiniBrackets, - Parentheses, - SquareBrackets, - CurlyBrackets, - AngleBrackets, - Argument, - IndentObj { include_below: bool }, - Tag, - Method, - Class, - Comment, - EntireFile, -} - -/// Selects a word text object. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct Word { - #[serde(default)] - ignore_punctuation: bool, -} - -/// Selects a subword text object. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct Subword { - #[serde(default)] - ignore_punctuation: bool, -} -/// Selects text at the same indentation level. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct IndentObj { - #[serde(default)] - include_below: bool, -} - -#[derive(Debug, Clone)] -pub struct CandidateRange { - pub start: DisplayPoint, - pub end: DisplayPoint, -} - -#[derive(Debug, Clone)] -pub struct CandidateWithRanges { - candidate: CandidateRange, - open_range: Range, - close_range: Range, -} - -/// Selects text at the same indentation level. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct Parentheses { - #[serde(default)] - opening: bool, -} - -/// Selects text at the same indentation level. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct SquareBrackets { - #[serde(default)] - opening: bool, -} - -/// Selects text at the same indentation level. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct AngleBrackets { - #[serde(default)] - opening: bool, -} -/// Selects text at the same indentation level. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct CurlyBrackets { - #[serde(default)] - opening: bool, -} - -fn cover_or_next, Range)>>( - candidates: Option, - caret: DisplayPoint, - map: &DisplaySnapshot, -) -> Option { - let caret_offset = caret.to_offset(map, Bias::Left); - let mut covering = vec![]; - let mut next_ones = vec![]; - let snapshot = map.buffer_snapshot(); - - if let Some(ranges) = candidates { - for (open_range, close_range) in ranges { - let start_off = open_range.start; - let end_off = close_range.end; - let candidate = CandidateWithRanges { - candidate: CandidateRange { - start: start_off.to_display_point(map), - end: end_off.to_display_point(map), - }, - open_range: open_range.clone(), - close_range: close_range.clone(), - }; - - if open_range - .start - .to_offset(snapshot) - .to_display_point(map) - .row() - == caret_offset.to_display_point(map).row() - { - if start_off <= caret_offset && caret_offset < end_off { - covering.push(candidate); - } else if start_off >= caret_offset { - next_ones.push(candidate); - } - } - } - } - - // 1) covering -> smallest width - if !covering.is_empty() { - return covering.into_iter().min_by_key(|r| { - r.candidate.end.to_offset(map, Bias::Right) - - r.candidate.start.to_offset(map, Bias::Left) - }); - } - - // 2) next -> closest by start - if !next_ones.is_empty() { - return next_ones.into_iter().min_by_key(|r| { - let start = r.candidate.start.to_offset(map, Bias::Left); - (start.0 as isize - caret_offset.0 as isize).abs() - }); - } - - None -} - -type DelimiterPredicate = dyn Fn(&BufferSnapshot, usize, usize) -> bool; - -struct DelimiterRange { - open: Range, - close: Range, -} - -impl DelimiterRange { - fn to_display_range(&self, map: &DisplaySnapshot, around: bool) -> Range { - if around { - self.open.start.to_display_point(map)..self.close.end.to_display_point(map) - } else { - self.open.end.to_display_point(map)..self.close.start.to_display_point(map) - } - } -} - -fn find_mini_delimiters( - map: &DisplaySnapshot, - display_point: DisplayPoint, - around: bool, - is_valid_delimiter: &DelimiterPredicate, -) -> Option> { - let point = map.clip_at_line_end(display_point).to_point(map); - let offset = point.to_offset(&map.buffer_snapshot()); - - let line_range = get_line_range(map, point); - let visible_line_range = get_visible_line_range(&line_range); - - let snapshot = &map.buffer_snapshot(); - let mut excerpt = snapshot.excerpt_containing(offset..offset)?; - let buffer = excerpt.buffer(); - let buffer_offset = excerpt.map_offset_to_buffer(offset); - - let bracket_filter = |open: Range, close: Range| { - is_valid_delimiter(buffer, open.start, close.start) - }; - - // Try to find delimiters in visible range first - let ranges = map - .buffer_snapshot() - .bracket_ranges(visible_line_range) - .map(|ranges| { - ranges.filter_map(|(open, close)| { - // Convert the ranges from multibuffer space to buffer space as - // that is what `is_valid_delimiter` expects, otherwise it might - // panic as the values might be out of bounds. - let buffer_open = excerpt.map_range_to_buffer(open.clone()); - let buffer_close = excerpt.map_range_to_buffer(close.clone()); - - if is_valid_delimiter(buffer, buffer_open.start.0, buffer_close.start.0) { - Some((open, close)) - } else { - None - } - }) - }); - - if let Some(candidate) = cover_or_next(ranges, display_point, map) { - return Some( - DelimiterRange { - open: candidate.open_range, - close: candidate.close_range, - } - .to_display_range(map, around), - ); - } - - // Fall back to innermost enclosing brackets - let (open_bracket, close_bracket) = buffer - .innermost_enclosing_bracket_ranges(buffer_offset..buffer_offset, Some(&bracket_filter))?; - - Some( - DelimiterRange { - open: excerpt.map_range_from_buffer( - BufferOffset(open_bracket.start)..BufferOffset(open_bracket.end), - ), - close: excerpt.map_range_from_buffer( - BufferOffset(close_bracket.start)..BufferOffset(close_bracket.end), - ), - } - .to_display_range(map, around), - ) -} - -fn get_line_range(map: &DisplaySnapshot, point: Point) -> Range { - let (start, mut end) = ( - map.prev_line_boundary(point).0, - map.next_line_boundary(point).0, - ); - - if end == point { - end = map.max_point().to_point(map); - } - - start..end -} - -fn get_visible_line_range(line_range: &Range) -> Range { - let end_column = line_range.end.column.saturating_sub(1); - line_range.start..Point::new(line_range.end.row, end_column) -} - -fn is_quote_delimiter(buffer: &BufferSnapshot, _start: usize, end: usize) -> bool { - matches!(buffer.chars_at(end).next(), Some('\'' | '"' | '`')) -} - -fn is_bracket_delimiter(buffer: &BufferSnapshot, start: usize, _end: usize) -> bool { - matches!( - buffer.chars_at(start).next(), - Some('(' | '[' | '{' | '<' | '|') - ) -} - -fn find_mini_quotes( - map: &DisplaySnapshot, - display_point: DisplayPoint, - around: bool, -) -> Option> { - find_mini_delimiters(map, display_point, around, &is_quote_delimiter) -} - -fn find_mini_brackets( - map: &DisplaySnapshot, - display_point: DisplayPoint, - around: bool, -) -> Option> { - find_mini_delimiters(map, display_point, around, &is_bracket_delimiter) -} - -actions!( - vim, - [ - /// Selects a sentence text object. - Sentence, - /// Selects a paragraph text object. - Paragraph, - /// Selects text within single quotes. - Quotes, - /// Selects text within backticks. - BackQuotes, - /// Selects text within the nearest quotes (single or double). - MiniQuotes, - /// Selects text within any type of quotes. - AnyQuotes, - /// Selects text within double quotes. - DoubleQuotes, - /// Selects text within vertical bars (pipes). - VerticalBars, - /// Selects text within the nearest brackets. - MiniBrackets, - /// Selects text within any type of brackets. - AnyBrackets, - /// Selects a function argument. - Argument, - /// Selects an HTML/XML tag. - Tag, - /// Selects a method or function. - Method, - /// Selects a class definition. - Class, - /// Selects a comment block. - Comment, - /// Selects the entire file. - EntireFile - ] -); - -pub fn register(editor: &mut Editor, cx: &mut Context) { - Vim::action( - editor, - cx, - |vim, &Word { ignore_punctuation }: &Word, window, cx| { - vim.object(Object::Word { ignore_punctuation }, window, cx) - }, - ); - Vim::action( - editor, - cx, - |vim, &Subword { ignore_punctuation }: &Subword, window, cx| { - vim.object(Object::Subword { ignore_punctuation }, window, cx) - }, - ); - Vim::action(editor, cx, |vim, _: &Tag, window, cx| { - vim.object(Object::Tag, window, cx) - }); - Vim::action(editor, cx, |vim, _: &Sentence, window, cx| { - vim.object(Object::Sentence, window, cx) - }); - Vim::action(editor, cx, |vim, _: &Paragraph, window, cx| { - vim.object(Object::Paragraph, window, cx) - }); - Vim::action(editor, cx, |vim, _: &Quotes, window, cx| { - vim.object(Object::Quotes, window, cx) - }); - Vim::action(editor, cx, |vim, _: &BackQuotes, window, cx| { - vim.object(Object::BackQuotes, window, cx) - }); - Vim::action(editor, cx, |vim, _: &MiniQuotes, window, cx| { - vim.object(Object::MiniQuotes, window, cx) - }); - Vim::action(editor, cx, |vim, _: &MiniBrackets, window, cx| { - vim.object(Object::MiniBrackets, window, cx) - }); - Vim::action(editor, cx, |vim, _: &AnyQuotes, window, cx| { - vim.object(Object::AnyQuotes, window, cx) - }); - Vim::action(editor, cx, |vim, _: &AnyBrackets, window, cx| { - vim.object(Object::AnyBrackets, window, cx) - }); - Vim::action(editor, cx, |vim, _: &BackQuotes, window, cx| { - vim.object(Object::BackQuotes, window, cx) - }); - Vim::action(editor, cx, |vim, _: &DoubleQuotes, window, cx| { - vim.object(Object::DoubleQuotes, window, cx) - }); - Vim::action(editor, cx, |vim, action: &Parentheses, window, cx| { - vim.object_impl(Object::Parentheses, action.opening, window, cx) - }); - Vim::action(editor, cx, |vim, action: &SquareBrackets, window, cx| { - vim.object_impl(Object::SquareBrackets, action.opening, window, cx) - }); - Vim::action(editor, cx, |vim, action: &CurlyBrackets, window, cx| { - vim.object_impl(Object::CurlyBrackets, action.opening, window, cx) - }); - Vim::action(editor, cx, |vim, action: &AngleBrackets, window, cx| { - vim.object_impl(Object::AngleBrackets, action.opening, window, cx) - }); - Vim::action(editor, cx, |vim, _: &VerticalBars, window, cx| { - vim.object(Object::VerticalBars, window, cx) - }); - Vim::action(editor, cx, |vim, _: &Argument, window, cx| { - vim.object(Object::Argument, window, cx) - }); - Vim::action(editor, cx, |vim, _: &Method, window, cx| { - vim.object(Object::Method, window, cx) - }); - Vim::action(editor, cx, |vim, _: &Class, window, cx| { - vim.object(Object::Class, window, cx) - }); - Vim::action(editor, cx, |vim, _: &EntireFile, window, cx| { - vim.object(Object::EntireFile, window, cx) - }); - Vim::action(editor, cx, |vim, _: &Comment, window, cx| { - if !matches!(vim.active_operator(), Some(Operator::Object { .. })) { - vim.push_operator(Operator::Object { around: true }, window, cx); - } - vim.object(Object::Comment, window, cx) - }); - Vim::action( - editor, - cx, - |vim, &IndentObj { include_below }: &IndentObj, window, cx| { - vim.object(Object::IndentObj { include_below }, window, cx) - }, - ); -} - -impl Vim { - fn object(&mut self, object: Object, window: &mut Window, cx: &mut Context) { - self.object_impl(object, false, window, cx); - } - - fn object_impl( - &mut self, - object: Object, - opening: bool, - window: &mut Window, - cx: &mut Context, - ) { - let count = Self::take_count(cx); - - match self.mode { - Mode::Normal | Mode::HelixNormal => { - self.normal_object(object, count, opening, window, cx) - } - Mode::Visual | Mode::VisualLine | Mode::VisualBlock | Mode::HelixSelect => { - self.visual_object(object, count, window, cx) - } - Mode::Insert | Mode::Replace => { - // Shouldn't execute a text object in insert mode. Ignoring - } - } - } -} - -impl Object { - pub fn is_multiline(self) -> bool { - match self { - Object::Word { .. } - | Object::Subword { .. } - | Object::Quotes - | Object::BackQuotes - | Object::AnyQuotes - | Object::MiniQuotes - | Object::VerticalBars - | Object::DoubleQuotes => false, - Object::Sentence - | Object::Paragraph - | Object::AnyBrackets - | Object::MiniBrackets - | Object::Parentheses - | Object::Tag - | Object::AngleBrackets - | Object::CurlyBrackets - | Object::SquareBrackets - | Object::Argument - | Object::Method - | Object::Class - | Object::EntireFile - | Object::Comment - | Object::IndentObj { .. } => true, - } - } - - pub fn always_expands_both_ways(self) -> bool { - match self { - Object::Word { .. } - | Object::Subword { .. } - | Object::Sentence - | Object::Paragraph - | Object::Argument - | Object::IndentObj { .. } => false, - Object::Quotes - | Object::BackQuotes - | Object::AnyQuotes - | Object::MiniQuotes - | Object::DoubleQuotes - | Object::VerticalBars - | Object::AnyBrackets - | Object::MiniBrackets - | Object::Parentheses - | Object::SquareBrackets - | Object::Tag - | Object::Method - | Object::Class - | Object::Comment - | Object::EntireFile - | Object::CurlyBrackets - | Object::AngleBrackets => true, - } - } - - pub fn target_visual_mode(self, current_mode: Mode, around: bool) -> Mode { - match self { - Object::Word { .. } - | Object::Subword { .. } - | Object::Sentence - | Object::Quotes - | Object::AnyQuotes - | Object::MiniQuotes - | Object::BackQuotes - | Object::DoubleQuotes => { - if current_mode == Mode::VisualBlock { - Mode::VisualBlock - } else { - Mode::Visual - } - } - Object::Parentheses - | Object::AnyBrackets - | Object::MiniBrackets - | Object::SquareBrackets - | Object::CurlyBrackets - | Object::AngleBrackets - | Object::VerticalBars - | Object::Tag - | Object::Comment - | Object::Argument - | Object::IndentObj { .. } => Mode::Visual, - Object::Method | Object::Class => { - if around { - Mode::VisualLine - } else { - Mode::Visual - } - } - Object::Paragraph | Object::EntireFile => Mode::VisualLine, - } - } - - pub fn range( - self, - map: &DisplaySnapshot, - selection: Selection, - around: bool, - times: Option, - ) -> Option> { - let relative_to = selection.head(); - match self { - Object::Word { ignore_punctuation } => { - if around { - around_word(map, relative_to, ignore_punctuation) - } else { - in_word(map, relative_to, ignore_punctuation) - } - } - Object::Subword { ignore_punctuation } => { - if around { - around_subword(map, relative_to, ignore_punctuation) - } else { - in_subword(map, relative_to, ignore_punctuation) - } - } - Object::Sentence => sentence(map, relative_to, around), - //change others later - Object::Paragraph => paragraph(map, relative_to, around, times.unwrap_or(1)), - Object::Quotes => { - surrounding_markers(map, relative_to, around, self.is_multiline(), '\'', '\'') - } - Object::BackQuotes => { - surrounding_markers(map, relative_to, around, self.is_multiline(), '`', '`') - } - Object::AnyQuotes => { - let quote_types = ['\'', '"', '`']; - let cursor_offset = relative_to.to_offset(map, Bias::Left); - - // Find innermost range directly without collecting all ranges - let mut innermost = None; - let mut min_size = usize::MAX; - - // First pass: find innermost enclosing range - for quote in quote_types { - if let Some(range) = surrounding_markers( - map, - relative_to, - around, - self.is_multiline(), - quote, - quote, - ) { - let start_offset = range.start.to_offset(map, Bias::Left); - let end_offset = range.end.to_offset(map, Bias::Right); - - if cursor_offset >= start_offset && cursor_offset <= end_offset { - let size = end_offset - start_offset; - if size < min_size { - min_size = size; - innermost = Some(range); - } - } - } - } - - if let Some(range) = innermost { - return Some(range); - } - - // Fallback: find nearest pair if not inside any quotes - quote_types - .iter() - .flat_map(|"e| { - surrounding_markers( - map, - relative_to, - around, - self.is_multiline(), - quote, - quote, - ) - }) - .min_by_key(|range| { - let start_offset = range.start.to_offset(map, Bias::Left); - let end_offset = range.end.to_offset(map, Bias::Right); - if cursor_offset < start_offset { - (start_offset - cursor_offset) as isize - } else if cursor_offset > end_offset { - (cursor_offset - end_offset) as isize - } else { - 0 - } - }) - } - Object::MiniQuotes => find_mini_quotes(map, relative_to, around), - Object::DoubleQuotes => { - surrounding_markers(map, relative_to, around, self.is_multiline(), '"', '"') - } - Object::VerticalBars => { - surrounding_markers(map, relative_to, around, self.is_multiline(), '|', '|') - } - Object::Parentheses => { - surrounding_markers(map, relative_to, around, self.is_multiline(), '(', ')') - } - Object::Tag => { - let head = selection.head(); - let range = selection.range(); - surrounding_html_tag(map, head, range, around) - } - Object::AnyBrackets => { - let bracket_pairs = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')]; - let cursor_offset = relative_to.to_offset(map, Bias::Left); - - // Find innermost enclosing bracket range - let mut innermost = None; - let mut min_size = usize::MAX; - - for &(open, close) in bracket_pairs.iter() { - if let Some(range) = surrounding_markers( - map, - relative_to, - around, - self.is_multiline(), - open, - close, - ) { - let start_offset = range.start.to_offset(map, Bias::Left); - let end_offset = range.end.to_offset(map, Bias::Right); - - if cursor_offset >= start_offset && cursor_offset <= end_offset { - let size = end_offset - start_offset; - if size < min_size { - min_size = size; - innermost = Some(range); - } - } - } - } - - if let Some(range) = innermost { - return Some(range); - } - - // Fallback: find nearest bracket pair if not inside any - bracket_pairs - .iter() - .flat_map(|&(open, close)| { - surrounding_markers( - map, - relative_to, - around, - self.is_multiline(), - open, - close, - ) - }) - .min_by_key(|range| { - let start_offset = range.start.to_offset(map, Bias::Left); - let end_offset = range.end.to_offset(map, Bias::Right); - if cursor_offset < start_offset { - (start_offset - cursor_offset) as isize - } else if cursor_offset > end_offset { - (cursor_offset - end_offset) as isize - } else { - 0 - } - }) - } - Object::MiniBrackets => find_mini_brackets(map, relative_to, around), - Object::SquareBrackets => { - surrounding_markers(map, relative_to, around, self.is_multiline(), '[', ']') - } - Object::CurlyBrackets => { - surrounding_markers(map, relative_to, around, self.is_multiline(), '{', '}') - } - Object::AngleBrackets => { - surrounding_markers(map, relative_to, around, self.is_multiline(), '<', '>') - } - Object::Method => text_object( - map, - relative_to, - if around { - TextObject::AroundFunction - } else { - TextObject::InsideFunction - }, - ), - Object::Comment => text_object( - map, - relative_to, - if around { - TextObject::AroundComment - } else { - TextObject::InsideComment - }, - ), - Object::Class => text_object( - map, - relative_to, - if around { - TextObject::AroundClass - } else { - TextObject::InsideClass - }, - ), - Object::Argument => argument(map, relative_to, around), - Object::IndentObj { include_below } => indent(map, relative_to, around, include_below), - Object::EntireFile => entire_file(map), - } - } - - pub fn expand_selection( - self, - map: &DisplaySnapshot, - selection: &mut Selection, - around: bool, - times: Option, - ) -> bool { - if let Some(range) = self.range(map, selection.clone(), around, times) { - selection.start = range.start; - selection.end = range.end; - true - } else { - false - } - } -} - -/// Returns a range that surrounds the word `relative_to` is in. -/// -/// If `relative_to` is at the start of a word, return the word. -/// If `relative_to` is between words, return the space between. -fn in_word( - map: &DisplaySnapshot, - relative_to: DisplayPoint, - ignore_punctuation: bool, -) -> Option> { - // Use motion::right so that we consider the character under the cursor when looking for the start - let classifier = map - .buffer_snapshot() - .char_classifier_at(relative_to.to_point(map)) - .ignore_punctuation(ignore_punctuation); - let start = movement::find_preceding_boundary_display_point( - map, - right(map, relative_to, 1), - movement::FindRange::SingleLine, - |left, right| classifier.kind(left) != classifier.kind(right), - ); - - let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| { - classifier.kind(left) != classifier.kind(right) - }); - - Some(start..end) -} - -fn in_subword( - map: &DisplaySnapshot, - relative_to: DisplayPoint, - ignore_punctuation: bool, -) -> Option> { - let offset = relative_to.to_offset(map, Bias::Left); - // Use motion::right so that we consider the character under the cursor when looking for the start - let classifier = map - .buffer_snapshot() - .char_classifier_at(relative_to.to_point(map)) - .ignore_punctuation(ignore_punctuation); - let in_subword = map - .buffer_chars_at(offset) - .next() - .map(|(c, _)| { - if classifier.is_word('-') { - !classifier.is_whitespace(c) && c != '_' && c != '-' - } else { - !classifier.is_whitespace(c) && c != '_' - } - }) - .unwrap_or(false); - - let start = if in_subword { - movement::find_preceding_boundary_display_point( - map, - right(map, relative_to, 1), - movement::FindRange::SingleLine, - |left, right| { - let is_word_start = classifier.kind(left) != classifier.kind(right); - let is_subword_start = classifier.is_word('-') && left == '-' && right != '-' - || left == '_' && right != '_' - || left.is_lowercase() && right.is_uppercase(); - is_word_start || is_subword_start - }, - ) - } else { - movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| { - let is_word_start = classifier.kind(left) != classifier.kind(right); - let is_subword_start = classifier.is_word('-') && left == '-' && right != '-' - || left == '_' && right != '_' - || left.is_lowercase() && right.is_uppercase(); - is_word_start || is_subword_start - }) - }; - - let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| { - let is_word_end = classifier.kind(left) != classifier.kind(right); - let is_subword_end = classifier.is_word('-') && left != '-' && right == '-' - || left != '_' && right == '_' - || left.is_lowercase() && right.is_uppercase(); - is_word_end || is_subword_end - }); - - Some(start..end) -} - -pub fn surrounding_html_tag( - map: &DisplaySnapshot, - head: DisplayPoint, - range: Range, - around: bool, -) -> Option> { - fn read_tag(chars: impl Iterator) -> String { - chars - .take_while(|c| c.is_alphanumeric() || *c == ':' || *c == '-' || *c == '_' || *c == '.') - .collect() - } - fn open_tag(mut chars: impl Iterator) -> Option { - if Some('<') != chars.next() { - return None; - } - Some(read_tag(chars)) - } - fn close_tag(mut chars: impl Iterator) -> Option { - if (Some('<'), Some('/')) != (chars.next(), chars.next()) { - return None; - } - Some(read_tag(chars)) - } - - let snapshot = &map.buffer_snapshot(); - let offset = head.to_offset(map, Bias::Left); - let mut excerpt = snapshot.excerpt_containing(offset..offset)?; - let buffer = excerpt.buffer(); - let offset = excerpt.map_offset_to_buffer(offset); - - // Find the most closest to current offset - let mut cursor = buffer.syntax_layer_at(offset)?.node().walk(); - let mut last_child_node = cursor.node(); - while cursor.goto_first_child_for_byte(offset.0).is_some() { - last_child_node = cursor.node(); - } - - let mut last_child_node = Some(last_child_node); - while let Some(cur_node) = last_child_node { - if cur_node.child_count() >= 2 { - let first_child = cur_node.child(0); - let last_child = cur_node.child(cur_node.child_count() - 1); - if let (Some(first_child), Some(last_child)) = (first_child, last_child) { - let open_tag = open_tag(buffer.chars_for_range(first_child.byte_range())); - let close_tag = close_tag(buffer.chars_for_range(last_child.byte_range())); - // It needs to be handled differently according to the selection length - let is_valid = if range.end.to_offset(map, Bias::Left) - - range.start.to_offset(map, Bias::Left) - <= 1 - { - offset.0 <= last_child.end_byte() - } else { - excerpt - .map_offset_to_buffer(range.start.to_offset(map, Bias::Left)) - .0 - >= first_child.start_byte() - && excerpt - .map_offset_to_buffer(range.end.to_offset(map, Bias::Left)) - .0 - <= last_child.start_byte() + 1 - }; - if open_tag.is_some() && open_tag == close_tag && is_valid { - let range = if around { - first_child.byte_range().start..last_child.byte_range().end - } else { - first_child.byte_range().end..last_child.byte_range().start - }; - let range = BufferOffset(range.start)..BufferOffset(range.end); - if excerpt.contains_buffer_range(range.clone()) { - let result = excerpt.map_range_from_buffer(range); - return Some( - result.start.to_display_point(map)..result.end.to_display_point(map), - ); - } - } - } - } - last_child_node = cur_node.parent(); - } - None -} - -/// Returns a range that surrounds the word and following whitespace -/// relative_to is in. -/// -/// If `relative_to` is at the start of a word, return the word and following whitespace. -/// If `relative_to` is between words, return the whitespace back and the following word. -/// -/// if in word -/// delete that word -/// if there is whitespace following the word, delete that as well -/// otherwise, delete any preceding whitespace -/// otherwise -/// delete whitespace around cursor -/// delete word following the cursor -fn around_word( - map: &DisplaySnapshot, - relative_to: DisplayPoint, - ignore_punctuation: bool, -) -> Option> { - let offset = relative_to.to_offset(map, Bias::Left); - let classifier = map - .buffer_snapshot() - .char_classifier_at(offset) - .ignore_punctuation(ignore_punctuation); - let in_word = map - .buffer_chars_at(offset) - .next() - .map(|(c, _)| !classifier.is_whitespace(c)) - .unwrap_or(false); - - if in_word { - around_containing_word(map, relative_to, ignore_punctuation) - } else { - around_next_word(map, relative_to, ignore_punctuation) - } -} - -fn around_subword( - map: &DisplaySnapshot, - relative_to: DisplayPoint, - ignore_punctuation: bool, -) -> Option> { - // Use motion::right so that we consider the character under the cursor when looking for the start - let classifier = map - .buffer_snapshot() - .char_classifier_at(relative_to.to_point(map)) - .ignore_punctuation(ignore_punctuation); - let start = movement::find_preceding_boundary_display_point( - map, - right(map, relative_to, 1), - movement::FindRange::SingleLine, - |left, right| { - let is_word_start = classifier.kind(left) != classifier.kind(right); - let is_subword_start = classifier.is_word('-') && left != '-' && right == '-' - || left != '_' && right == '_' - || left.is_lowercase() && right.is_uppercase(); - is_word_start || is_subword_start - }, - ); - - let end = movement::find_boundary(map, relative_to, FindRange::SingleLine, |left, right| { - let is_word_end = classifier.kind(left) != classifier.kind(right); - let is_subword_end = classifier.is_word('-') && left != '-' && right == '-' - || left != '_' && right == '_' - || left.is_lowercase() && right.is_uppercase(); - is_word_end || is_subword_end - }); - - Some(start..end).map(|range| expand_to_include_whitespace(map, range, true)) -} - -fn around_containing_word( - map: &DisplaySnapshot, - relative_to: DisplayPoint, - ignore_punctuation: bool, -) -> Option> { - in_word(map, relative_to, ignore_punctuation).map(|range| { - let line_start = DisplayPoint::new(range.start.row(), 0); - let is_first_word = map - .buffer_chars_at(line_start.to_offset(map, Bias::Left)) - .take_while(|(ch, offset)| { - offset < &range.start.to_offset(map, Bias::Left) && ch.is_whitespace() - }) - .count() - > 0; - - if is_first_word { - // For first word on line, trim indentation - let mut expanded = expand_to_include_whitespace(map, range.clone(), true); - expanded.start = range.start; - expanded - } else { - expand_to_include_whitespace(map, range, true) - } - }) -} - -fn around_next_word( - map: &DisplaySnapshot, - relative_to: DisplayPoint, - ignore_punctuation: bool, -) -> Option> { - let classifier = map - .buffer_snapshot() - .char_classifier_at(relative_to.to_point(map)) - .ignore_punctuation(ignore_punctuation); - // Get the start of the word - let start = movement::find_preceding_boundary_display_point( - map, - right(map, relative_to, 1), - FindRange::SingleLine, - |left, right| classifier.kind(left) != classifier.kind(right), - ); - - let mut word_found = false; - let end = movement::find_boundary(map, relative_to, FindRange::MultiLine, |left, right| { - let left_kind = classifier.kind(left); - let right_kind = classifier.kind(right); - - let found = (word_found && left_kind != right_kind) || right == '\n' && left == '\n'; - - if right_kind != CharKind::Whitespace { - word_found = true; - } - - found - }); - - Some(start..end) -} - -fn entire_file(map: &DisplaySnapshot) -> Option> { - Some(DisplayPoint::zero()..map.max_point()) -} - -fn text_object( - map: &DisplaySnapshot, - relative_to: DisplayPoint, - target: TextObject, -) -> Option> { - let snapshot = &map.buffer_snapshot(); - let offset = relative_to.to_offset(map, Bias::Left); - - let mut excerpt = snapshot.excerpt_containing(offset..offset)?; - let buffer = excerpt.buffer(); - let offset = excerpt.map_offset_to_buffer(offset); - - let mut matches: Vec> = buffer - .text_object_ranges(offset..offset, TreeSitterOptions::default()) - .filter_map(|(r, m)| if m == target { Some(r) } else { None }) - .collect(); - matches.sort_by_key(|r| r.end - r.start); - if let Some(buffer_range) = matches.first() { - let buffer_range = BufferOffset(buffer_range.start)..BufferOffset(buffer_range.end); - let range = excerpt.map_range_from_buffer(buffer_range); - return Some(range.start.to_display_point(map)..range.end.to_display_point(map)); - } - - let around = target.around()?; - let mut matches: Vec> = buffer - .text_object_ranges(offset..offset, TreeSitterOptions::default()) - .filter_map(|(r, m)| if m == around { Some(r) } else { None }) - .collect(); - matches.sort_by_key(|r| r.end - r.start); - let around_range = matches.first()?; - - let mut matches: Vec> = buffer - .text_object_ranges(around_range.clone(), TreeSitterOptions::default()) - .filter_map(|(r, m)| if m == target { Some(r) } else { None }) - .collect(); - matches.sort_by_key(|r| r.start); - if let Some(buffer_range) = matches.first() - && !buffer_range.is_empty() - { - let buffer_range = BufferOffset(buffer_range.start)..BufferOffset(buffer_range.end); - let range = excerpt.map_range_from_buffer(buffer_range); - return Some(range.start.to_display_point(map)..range.end.to_display_point(map)); - } - let around_range = BufferOffset(around_range.start)..BufferOffset(around_range.end); - let buffer_range = excerpt.map_range_from_buffer(around_range); - return Some(buffer_range.start.to_display_point(map)..buffer_range.end.to_display_point(map)); -} - -fn argument( - map: &DisplaySnapshot, - relative_to: DisplayPoint, - around: bool, -) -> Option> { - let snapshot = &map.buffer_snapshot(); - let offset = relative_to.to_offset(map, Bias::Left); - - // The `argument` vim text object uses the syntax tree, so we operate at the buffer level and map back to the display level - let mut excerpt = snapshot.excerpt_containing(offset..offset)?; - let buffer = excerpt.buffer(); - - fn comma_delimited_range_at( - buffer: &BufferSnapshot, - mut offset: BufferOffset, - include_comma: bool, - ) -> Option> { - // Seek to the first non-whitespace character - offset += buffer - .chars_at(offset) - .take_while(|c| c.is_whitespace()) - .map(char::len_utf8) - .sum::(); - - let bracket_filter = |open: Range, close: Range| { - // Filter out empty ranges - if open.end == close.start { - return false; - } - - // If the cursor is outside the brackets, ignore them - if open.start == offset.0 || close.end == offset.0 { - return false; - } - - // TODO: Is there any better way to filter out string brackets? - // Used to filter out string brackets - matches!( - buffer.chars_at(open.start).next(), - Some('(' | '[' | '{' | '<' | '|') - ) - }; - - // Find the brackets containing the cursor - let (open_bracket, close_bracket) = - buffer.innermost_enclosing_bracket_ranges(offset..offset, Some(&bracket_filter))?; - - let inner_bracket_range = BufferOffset(open_bracket.end)..BufferOffset(close_bracket.start); - - let layer = buffer.syntax_layer_at(offset)?; - let node = layer.node(); - let mut cursor = node.walk(); - - // Loop until we find the smallest node whose parent covers the bracket range. This node is the argument in the parent argument list - let mut parent_covers_bracket_range = false; - loop { - let node = cursor.node(); - let range = node.byte_range(); - let covers_bracket_range = - range.start == open_bracket.start && range.end == close_bracket.end; - if parent_covers_bracket_range && !covers_bracket_range { - break; - } - parent_covers_bracket_range = covers_bracket_range; - - // Unable to find a child node with a parent that covers the bracket range, so no argument to select - cursor.goto_first_child_for_byte(offset.0)?; - } - - let mut argument_node = cursor.node(); - - // If the child node is the open bracket, move to the next sibling. - if argument_node.byte_range() == open_bracket { - if !cursor.goto_next_sibling() { - return Some(inner_bracket_range); - } - argument_node = cursor.node(); - } - // While the child node is the close bracket or a comma, move to the previous sibling - while argument_node.byte_range() == close_bracket || argument_node.kind() == "," { - if !cursor.goto_previous_sibling() { - return Some(inner_bracket_range); - } - argument_node = cursor.node(); - if argument_node.byte_range() == open_bracket { - return Some(inner_bracket_range); - } - } - - // The start and end of the argument range, defaulting to the start and end of the argument node - let mut start = argument_node.start_byte(); - let mut end = argument_node.end_byte(); - - let mut needs_surrounding_comma = include_comma; - - // Seek backwards to find the start of the argument - either the previous comma or the opening bracket. - // We do this because multiple nodes can represent a single argument, such as with rust `vec![a.b.c, d.e.f]` - while cursor.goto_previous_sibling() { - let prev = cursor.node(); - - if prev.start_byte() < open_bracket.end { - start = open_bracket.end; - break; - } else if prev.kind() == "," { - if needs_surrounding_comma { - start = prev.start_byte(); - needs_surrounding_comma = false; - } - break; - } else if prev.start_byte() < start { - start = prev.start_byte(); - } - } - - // Do the same for the end of the argument, extending to next comma or the end of the argument list - while cursor.goto_next_sibling() { - let next = cursor.node(); - - if next.end_byte() > close_bracket.start { - end = close_bracket.start; - break; - } else if next.kind() == "," { - if needs_surrounding_comma { - // Select up to the beginning of the next argument if there is one, otherwise to the end of the comma - if let Some(next_arg) = next.next_sibling() { - end = next_arg.start_byte(); - } else { - end = next.end_byte(); - } - } - break; - } else if next.end_byte() > end { - end = next.end_byte(); - } - } - - Some(BufferOffset(start)..BufferOffset(end)) - } - - let result = comma_delimited_range_at(buffer, excerpt.map_offset_to_buffer(offset), around)?; - - if excerpt.contains_buffer_range(result.clone()) { - let result = excerpt.map_range_from_buffer(result); - Some(result.start.to_display_point(map)..result.end.to_display_point(map)) - } else { - None - } -} - -fn indent( - map: &DisplaySnapshot, - relative_to: DisplayPoint, - around: bool, - include_below: bool, -) -> Option> { - let point = relative_to.to_point(map); - let row = point.row; - - let desired_indent = map.line_indent_for_buffer_row(MultiBufferRow(row)); - - // Loop backwards until we find a non-blank line with less indent - let mut start_row = row; - for prev_row in (0..row).rev() { - let indent = map.line_indent_for_buffer_row(MultiBufferRow(prev_row)); - if indent.is_line_empty() { - continue; - } - if indent.spaces < desired_indent.spaces || indent.tabs < desired_indent.tabs { - if around { - // When around is true, include the first line with less indent - start_row = prev_row; - } - break; - } - start_row = prev_row; - } - - // Loop forwards until we find a non-blank line with less indent - let mut end_row = row; - let max_rows = map.buffer_snapshot().max_row().0; - for next_row in (row + 1)..=max_rows { - let indent = map.line_indent_for_buffer_row(MultiBufferRow(next_row)); - if indent.is_line_empty() { - continue; - } - if indent.spaces < desired_indent.spaces || indent.tabs < desired_indent.tabs { - if around && include_below { - // When around is true and including below, include this line - end_row = next_row; - } - break; - } - end_row = next_row; - } - - let end_len = map.buffer_snapshot().line_len(MultiBufferRow(end_row)); - let start = map.point_to_display_point(Point::new(start_row, 0), Bias::Right); - let end = map.point_to_display_point(Point::new(end_row, end_len), Bias::Left); - Some(start..end) -} - -fn sentence( - map: &DisplaySnapshot, - relative_to: DisplayPoint, - around: bool, -) -> Option> { - let mut start = None; - let relative_offset = relative_to.to_offset(map, Bias::Left); - let mut previous_end = relative_offset; - - let mut chars = map.buffer_chars_at(previous_end).peekable(); - - // Search backwards for the previous sentence end or current sentence start. Include the character under relative_to - for (char, offset) in chars - .peek() - .cloned() - .into_iter() - .chain(map.reverse_buffer_chars_at(previous_end)) - { - if is_sentence_end(map, offset) { - break; - } - - if is_possible_sentence_start(char) { - start = Some(offset); - } - - previous_end = offset; - } - - // Search forward for the end of the current sentence or if we are between sentences, the start of the next one - let mut end = relative_offset; - for (char, offset) in chars { - if start.is_none() && is_possible_sentence_start(char) { - if around { - start = Some(offset); - continue; - } else { - end = offset; - break; - } - } - - if char != '\n' { - end = offset + char.len_utf8(); - } - - if is_sentence_end(map, end) { - break; - } - } - - let mut range = start.unwrap_or(previous_end).to_display_point(map)..end.to_display_point(map); - if around { - range = expand_to_include_whitespace(map, range, false); - } - - Some(range) -} - -fn is_possible_sentence_start(character: char) -> bool { - !character.is_whitespace() && character != '.' -} - -const SENTENCE_END_PUNCTUATION: &[char] = &['.', '!', '?']; -const SENTENCE_END_FILLERS: &[char] = &[')', ']', '"', '\'']; -const SENTENCE_END_WHITESPACE: &[char] = &[' ', '\t', '\n']; -fn is_sentence_end(map: &DisplaySnapshot, offset: MultiBufferOffset) -> bool { - let mut next_chars = map.buffer_chars_at(offset).peekable(); - if let Some((char, _)) = next_chars.next() { - // We are at a double newline. This position is a sentence end. - if char == '\n' && next_chars.peek().map(|(c, _)| c == &'\n').unwrap_or(false) { - return true; - } - - // The next text is not a valid whitespace. This is not a sentence end - if !SENTENCE_END_WHITESPACE.contains(&char) { - return false; - } - } - - for (char, _) in map.reverse_buffer_chars_at(offset) { - if SENTENCE_END_PUNCTUATION.contains(&char) { - return true; - } - - if !SENTENCE_END_FILLERS.contains(&char) { - return false; - } - } - - false -} - -/// Expands the passed range to include whitespace on one side or the other in a line. Attempts to add the -/// whitespace to the end first and falls back to the start if there was none. -pub fn expand_to_include_whitespace( - map: &DisplaySnapshot, - range: Range, - stop_at_newline: bool, -) -> Range { - let mut range = range.start.to_offset(map, Bias::Left)..range.end.to_offset(map, Bias::Right); - let mut whitespace_included = false; - - let chars = map.buffer_chars_at(range.end).peekable(); - for (char, offset) in chars { - if char == '\n' && stop_at_newline { - break; - } - - if char.is_whitespace() { - if char != '\n' { - range.end = offset + char.len_utf8(); - whitespace_included = true; - } - } else { - // Found non whitespace. Quit out. - break; - } - } - - if !whitespace_included { - for (char, point) in map.reverse_buffer_chars_at(range.start) { - if char == '\n' && stop_at_newline { - break; - } - - if !char.is_whitespace() { - break; - } - - range.start = point; - } - } - - range.start.to_display_point(map)..range.end.to_display_point(map) -} - -/// If not `around` (i.e. inner), returns a range that surrounds the paragraph -/// where `relative_to` is in. If `around`, principally returns the range ending -/// at the end of the next paragraph. -/// -/// Here, the "paragraph" is defined as a block of non-blank lines or a block of -/// blank lines. If the paragraph ends with a trailing newline (i.e. not with -/// EOF), the returned range ends at the trailing newline of the paragraph (i.e. -/// the trailing newline is not subject to subsequent operations). -/// -/// Edge cases: -/// - If `around` and if the current paragraph is the last paragraph of the -/// file and is blank, then the selection results in an error. -/// - If `around` and if the current paragraph is the last paragraph of the -/// file and is not blank, then the returned range starts at the start of the -/// previous paragraph, if it exists. -fn paragraph( - map: &DisplaySnapshot, - relative_to: DisplayPoint, - around: bool, - times: usize, -) -> Option> { - let mut paragraph_start = start_of_paragraph(map, relative_to); - let mut paragraph_end = end_of_paragraph(map, relative_to); - - for i in 0..times { - let paragraph_end_row = paragraph_end.row(); - let paragraph_ends_with_eof = paragraph_end_row == map.max_point().row(); - let point = relative_to.to_point(map); - let current_line_is_empty = map - .buffer_snapshot() - .is_line_blank(MultiBufferRow(point.row)); - - if around { - if paragraph_ends_with_eof { - if current_line_is_empty { - return None; - } - - let paragraph_start_buffer_point = paragraph_start.to_point(map); - if paragraph_start_buffer_point.row != 0 { - let previous_paragraph_last_line_start = - Point::new(paragraph_start_buffer_point.row - 1, 0).to_display_point(map); - paragraph_start = start_of_paragraph(map, previous_paragraph_last_line_start); - } - } else { - let paragraph_end_buffer_point = paragraph_end.to_point(map); - let mut start_row = paragraph_end_buffer_point.row + 1; - if i > 0 { - start_row += 1; - } - let next_paragraph_start = Point::new(start_row, 0).to_display_point(map); - paragraph_end = end_of_paragraph(map, next_paragraph_start); - } - } - } - - let range = paragraph_start..paragraph_end; - Some(range) -} - -/// Returns a position of the start of the current paragraph, where a paragraph -/// is defined as a run of non-blank lines or a run of blank lines. -pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint { - let point = display_point.to_point(map); - if point.row == 0 { - return DisplayPoint::zero(); - } - - let is_current_line_blank = map - .buffer_snapshot() - .is_line_blank(MultiBufferRow(point.row)); - - for row in (0..point.row).rev() { - let blank = map.buffer_snapshot().is_line_blank(MultiBufferRow(row)); - if blank != is_current_line_blank { - return Point::new(row + 1, 0).to_display_point(map); - } - } - - DisplayPoint::zero() -} - -/// Returns a position of the end of the current paragraph, where a paragraph -/// is defined as a run of non-blank lines or a run of blank lines. -/// The trailing newline is excluded from the paragraph. -pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint { - let point = display_point.to_point(map); - if point.row == map.buffer_snapshot().max_row().0 { - return map.max_point(); - } - - let is_current_line_blank = map - .buffer_snapshot() - .is_line_blank(MultiBufferRow(point.row)); - - for row in point.row + 1..map.buffer_snapshot().max_row().0 + 1 { - let blank = map.buffer_snapshot().is_line_blank(MultiBufferRow(row)); - if blank != is_current_line_blank { - let previous_row = row - 1; - return Point::new( - previous_row, - map.buffer_snapshot().line_len(MultiBufferRow(previous_row)), - ) - .to_display_point(map); - } - } - - map.max_point() -} - -pub fn surrounding_markers( - map: &DisplaySnapshot, - relative_to: DisplayPoint, - around: bool, - search_across_lines: bool, - open_marker: char, - close_marker: char, -) -> Option> { - let point = relative_to.to_offset(map, Bias::Left); - - let mut matched_closes = 0; - let mut opening = None; - - let mut before_ch = match movement::chars_before(map, point).next() { - Some((ch, _)) => ch, - _ => '\0', - }; - if let Some((ch, range)) = movement::chars_after(map, point).next() - && ch == open_marker - && before_ch != '\\' - { - if open_marker == close_marker { - let mut total = 0; - for ((ch, _), (before_ch, _)) in movement::chars_before(map, point).tuple_windows() { - if ch == '\n' { - break; - } - if ch == open_marker && before_ch != '\\' { - total += 1; - } - } - if total % 2 == 0 { - opening = Some(range) - } - } else { - opening = Some(range) - } - } - - if opening.is_none() { - let mut chars_before = movement::chars_before(map, point).peekable(); - while let Some((ch, range)) = chars_before.next() { - if ch == '\n' && !search_across_lines { - break; - } - - if let Some((before_ch, _)) = chars_before.peek() - && *before_ch == '\\' - { - continue; - } - - if ch == open_marker { - if matched_closes == 0 { - opening = Some(range); - break; - } - matched_closes -= 1; - } else if ch == close_marker { - matched_closes += 1 - } - } - } - if opening.is_none() { - for (ch, range) in movement::chars_after(map, point) { - if before_ch != '\\' { - if ch == open_marker { - opening = Some(range); - break; - } else if ch == close_marker { - break; - } - } - - before_ch = ch; - } - } - - let mut opening = opening?; - - let mut matched_opens = 0; - let mut closing = None; - before_ch = match movement::chars_before(map, opening.end).next() { - Some((ch, _)) => ch, - _ => '\0', - }; - for (ch, range) in movement::chars_after(map, opening.end) { - if ch == '\n' && !search_across_lines { - break; - } - - if before_ch != '\\' { - if ch == close_marker { - if matched_opens == 0 { - closing = Some(range); - break; - } - matched_opens -= 1; - } else if ch == open_marker { - matched_opens += 1; - } - } - - before_ch = ch; - } - - let mut closing = closing?; - - if around && !search_across_lines { - let mut found = false; - - for (ch, range) in movement::chars_after(map, closing.end) { - if ch.is_whitespace() && ch != '\n' { - found = true; - closing.end = range.end; - } else { - break; - } - } - - if !found { - for (ch, range) in movement::chars_before(map, opening.start) { - if ch.is_whitespace() && ch != '\n' { - opening.start = range.start - } else { - break; - } - } - } - } - - // Adjust selection to remove leading and trailing whitespace for multiline inner brackets - if !around && open_marker != close_marker { - let start_point = opening.end.to_display_point(map); - let end_point = closing.start.to_display_point(map); - let start_offset = start_point.to_offset(map, Bias::Left); - let end_offset = end_point.to_offset(map, Bias::Left); - - if start_point.row() != end_point.row() - && map - .buffer_chars_at(start_offset) - .take_while(|(_, offset)| offset < &end_offset) - .any(|(ch, _)| !ch.is_whitespace()) - { - let mut first_non_ws = None; - let mut last_non_ws = None; - for (ch, offset) in map.buffer_chars_at(start_offset) { - if !ch.is_whitespace() { - first_non_ws = Some(offset); - break; - } - } - for (ch, offset) in map.reverse_buffer_chars_at(end_offset) { - if !ch.is_whitespace() { - last_non_ws = Some(offset + ch.len_utf8()); - break; - } - } - if let Some(start) = first_non_ws { - opening.end = start; - } - if let Some(end) = last_non_ws { - closing.start = end; - } - } - } - - let result = if around { - opening.start..closing.end - } else { - opening.end..closing.start - }; - - Some( - map.clip_point(result.start.to_display_point(map), Bias::Left) - ..map.clip_point(result.end.to_display_point(map), Bias::Right), - ) -} - -#[cfg(test)] -mod test { - use editor::{Editor, EditorMode, MultiBuffer, test::editor_test_context::EditorTestContext}; - use gpui::KeyBinding; - use indoc::indoc; - use text::Point; - - use crate::{ - object::{AnyBrackets, AnyQuotes, MiniBrackets}, - state::Mode, - test::{NeovimBackedTestContext, VimTestContext}, - }; - - const WORD_LOCATIONS: &str = indoc! {" - The quick ˇbrowˇnˇ••• - fox ˇjuˇmpsˇ over - the lazy dogˇ•• - ˇ - ˇ - ˇ - Thˇeˇ-ˇquˇickˇ ˇbrownˇ• - ˇ•• - ˇ•• - ˇ fox-jumpˇs over - the lazy dogˇ• - ˇ - " - }; - - #[gpui::test] - async fn test_change_word_object(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.simulate_at_each_offset("c i w", WORD_LOCATIONS) - .await - .assert_matches(); - cx.simulate_at_each_offset("c i shift-w", WORD_LOCATIONS) - .await - .assert_matches(); - cx.simulate_at_each_offset("c a w", WORD_LOCATIONS) - .await - .assert_matches(); - cx.simulate_at_each_offset("c a shift-w", WORD_LOCATIONS) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_word_object(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.simulate_at_each_offset("d i w", WORD_LOCATIONS) - .await - .assert_matches(); - cx.simulate_at_each_offset("d i shift-w", WORD_LOCATIONS) - .await - .assert_matches(); - cx.simulate_at_each_offset("d a w", WORD_LOCATIONS) - .await - .assert_matches(); - cx.simulate_at_each_offset("d a shift-w", WORD_LOCATIONS) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_visual_word_object(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - /* - cx.set_shared_state("The quick ˇbrown\nfox").await; - cx.simulate_shared_keystrokes(["v"]).await; - cx.assert_shared_state("The quick «bˇ»rown\nfox").await; - cx.simulate_shared_keystrokes(["i", "w"]).await; - cx.assert_shared_state("The quick «brownˇ»\nfox").await; - */ - cx.set_shared_state("The quick brown\nˇ\nfox").await; - cx.simulate_shared_keystrokes("v").await; - cx.shared_state() - .await - .assert_eq("The quick brown\n«\nˇ»fox"); - cx.simulate_shared_keystrokes("i w").await; - cx.shared_state() - .await - .assert_eq("The quick brown\n«\nˇ»fox"); - - cx.simulate_at_each_offset("v i w", WORD_LOCATIONS) - .await - .assert_matches(); - cx.simulate_at_each_offset("v i shift-w", WORD_LOCATIONS) - .await - .assert_matches(); - } - - const PARAGRAPH_EXAMPLES: &[&str] = &[ - // Single line - "ˇThe quick brown fox jumpˇs over the lazy dogˇ.ˇ", - // Multiple lines without empty lines - indoc! {" - ˇThe quick brownˇ - ˇfox jumps overˇ - the lazy dog.ˇ - "}, - // Heading blank paragraph and trailing normal paragraph - indoc! {" - ˇ - ˇ - ˇThe quick brown fox jumps - ˇover the lazy dog. - ˇ - ˇ - ˇThe quick brown fox jumpsˇ - ˇover the lazy dog.ˇ - "}, - // Inserted blank paragraph and trailing blank paragraph - indoc! {" - ˇThe quick brown fox jumps - ˇover the lazy dog. - ˇ - ˇ - ˇ - ˇThe quick brown fox jumpsˇ - ˇover the lazy dog.ˇ - ˇ - ˇ - ˇ - "}, - // "Blank" paragraph with whitespace characters - indoc! {" - ˇThe quick brown fox jumps - over the lazy dog. - - ˇ \t - - ˇThe quick brown fox jumps - over the lazy dog.ˇ - ˇ - ˇ \t - \t \t - "}, - // Single line "paragraphs", where selection size might be zero. - indoc! {" - ˇThe quick brown fox jumps over the lazy dog. - ˇ - ˇThe quick brown fox jumpˇs over the lazy dog.ˇ - ˇ - "}, - ]; - - #[gpui::test] - async fn test_change_paragraph_object(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - for paragraph_example in PARAGRAPH_EXAMPLES { - cx.simulate_at_each_offset("c i p", paragraph_example) - .await - .assert_matches(); - cx.simulate_at_each_offset("c a p", paragraph_example) - .await - .assert_matches(); - } - } - - #[gpui::test] - async fn test_delete_paragraph_object(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - for paragraph_example in PARAGRAPH_EXAMPLES { - cx.simulate_at_each_offset("d i p", paragraph_example) - .await - .assert_matches(); - cx.simulate_at_each_offset("d a p", paragraph_example) - .await - .assert_matches(); - } - } - - #[gpui::test] - async fn test_visual_paragraph_object(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - const EXAMPLES: &[&str] = &[ - indoc! {" - ˇThe quick brown - fox jumps over - the lazy dog. - "}, - indoc! {" - ˇ - - ˇThe quick brown fox jumps - over the lazy dog. - ˇ - - ˇThe quick brown fox jumps - over the lazy dog. - "}, - indoc! {" - ˇThe quick brown fox jumps over the lazy dog. - ˇ - ˇThe quick brown fox jumps over the lazy dog. - - "}, - ]; - - for paragraph_example in EXAMPLES { - cx.simulate_at_each_offset("v i p", paragraph_example) - .await - .assert_matches(); - cx.simulate_at_each_offset("v a p", paragraph_example) - .await - .assert_matches(); - } - } - - #[gpui::test] - async fn test_change_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - const WRAPPING_EXAMPLE: &str = indoc! {" - ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines. - - ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly. - - ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ - "}; - - cx.set_shared_wrap(20).await; - - cx.simulate_at_each_offset("c i p", WRAPPING_EXAMPLE) - .await - .assert_matches(); - cx.simulate_at_each_offset("c a p", WRAPPING_EXAMPLE) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - const WRAPPING_EXAMPLE: &str = indoc! {" - ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines. - - ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly. - - ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ - "}; - - cx.set_shared_wrap(20).await; - - cx.simulate_at_each_offset("d i p", WRAPPING_EXAMPLE) - .await - .assert_matches(); - cx.simulate_at_each_offset("d a p", WRAPPING_EXAMPLE) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_delete_paragraph_whitespace(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - a - ˇ• - aaaaaaaaaaaaa - "}) - .await; - - cx.simulate_shared_keystrokes("d i p").await; - cx.shared_state().await.assert_eq(indoc! {" - a - aaaaaaaˇaaaaaa - "}); - } - - #[gpui::test] - async fn test_visual_paragraph_object_with_soft_wrap(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - const WRAPPING_EXAMPLE: &str = indoc! {" - ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines. - - ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly. - - ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.ˇ - "}; - - cx.set_shared_wrap(20).await; - - cx.simulate_at_each_offset("v i p", WRAPPING_EXAMPLE) - .await - .assert_matches(); - cx.simulate_at_each_offset("v a p", WRAPPING_EXAMPLE) - .await - .assert_matches(); - } - - // Test string with "`" for opening surrounders and "'" for closing surrounders - const SURROUNDING_MARKER_STRING: &str = indoc! {" - ˇTh'ˇe ˇ`ˇ'ˇquˇi`ˇck broˇ'wn` - 'ˇfox juˇmps ov`ˇer - the ˇlazy d'o`ˇg"}; - - const SURROUNDING_OBJECTS: &[(char, char)] = &[ - ('"', '"'), // Double Quote - ('(', ')'), // Parentheses - ]; - - #[gpui::test] - async fn test_change_surrounding_character_objects(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - for (start, end) in SURROUNDING_OBJECTS { - let marked_string = SURROUNDING_MARKER_STRING - .replace('`', &start.to_string()) - .replace('\'', &end.to_string()); - - cx.simulate_at_each_offset(&format!("c i {start}"), &marked_string) - .await - .assert_matches(); - cx.simulate_at_each_offset(&format!("c i {end}"), &marked_string) - .await - .assert_matches(); - cx.simulate_at_each_offset(&format!("c a {start}"), &marked_string) - .await - .assert_matches(); - cx.simulate_at_each_offset(&format!("c a {end}"), &marked_string) - .await - .assert_matches(); - } - } - #[gpui::test] - async fn test_singleline_surrounding_character_objects(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_wrap(12).await; - - cx.set_shared_state(indoc! { - "\"ˇhello world\"!" - }) - .await; - cx.simulate_shared_keystrokes("v i \"").await; - cx.shared_state().await.assert_eq(indoc! { - "\"«hello worldˇ»\"!" - }); - - cx.set_shared_state(indoc! { - "\"hˇello world\"!" - }) - .await; - cx.simulate_shared_keystrokes("v i \"").await; - cx.shared_state().await.assert_eq(indoc! { - "\"«hello worldˇ»\"!" - }); - - cx.set_shared_state(indoc! { - "helˇlo \"world\"!" - }) - .await; - cx.simulate_shared_keystrokes("v i \"").await; - cx.shared_state().await.assert_eq(indoc! { - "hello \"«worldˇ»\"!" - }); - - cx.set_shared_state(indoc! { - "hello \"wˇorld\"!" - }) - .await; - cx.simulate_shared_keystrokes("v i \"").await; - cx.shared_state().await.assert_eq(indoc! { - "hello \"«worldˇ»\"!" - }); - - cx.set_shared_state(indoc! { - "hello \"wˇorld\"!" - }) - .await; - cx.simulate_shared_keystrokes("v a \"").await; - cx.shared_state().await.assert_eq(indoc! { - "hello« \"world\"ˇ»!" - }); - - cx.set_shared_state(indoc! { - "hello \"wˇorld\" !" - }) - .await; - cx.simulate_shared_keystrokes("v a \"").await; - cx.shared_state().await.assert_eq(indoc! { - "hello «\"world\" ˇ»!" - }); - - cx.set_shared_state(indoc! { - "hello \"wˇorld\"• - goodbye" - }) - .await; - cx.simulate_shared_keystrokes("v a \"").await; - cx.shared_state().await.assert_eq(indoc! { - "hello «\"world\" ˇ» - goodbye" - }); - } - - #[gpui::test] - async fn test_multiline_surrounding_character_objects(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! { - "func empty(a string) bool { - if a == \"\" { - return true - } - ˇreturn false - }" - }, - Mode::Normal, - ); - cx.simulate_keystrokes("v i {"); - cx.assert_state( - indoc! { - "func empty(a string) bool { - «if a == \"\" { - return true - } - return falseˇ» - }" - }, - Mode::Visual, - ); - - cx.set_state( - indoc! { - "func empty(a string) bool { - if a == \"\" { - ˇreturn true - } - return false - }" - }, - Mode::Normal, - ); - cx.simulate_keystrokes("v i {"); - cx.assert_state( - indoc! { - "func empty(a string) bool { - if a == \"\" { - «return trueˇ» - } - return false - }" - }, - Mode::Visual, - ); - - cx.set_state( - indoc! { - "func empty(a string) bool { - if a == \"\" ˇ{ - return true - } - return false - }" - }, - Mode::Normal, - ); - cx.simulate_keystrokes("v i {"); - cx.assert_state( - indoc! { - "func empty(a string) bool { - if a == \"\" { - «return trueˇ» - } - return false - }" - }, - Mode::Visual, - ); - - cx.set_state( - indoc! { - "func empty(a string) bool { - if a == \"\" { - return true - } - return false - ˇ}" - }, - Mode::Normal, - ); - cx.simulate_keystrokes("v i {"); - cx.assert_state( - indoc! { - "func empty(a string) bool { - «if a == \"\" { - return true - } - return falseˇ» - }" - }, - Mode::Visual, - ); - - cx.set_state( - indoc! { - "func empty(a string) bool { - if a == \"\" { - ˇ - - }" - }, - Mode::Normal, - ); - cx.simulate_keystrokes("c i {"); - cx.assert_state( - indoc! { - "func empty(a string) bool { - if a == \"\" {ˇ}" - }, - Mode::Insert, - ); - } - - #[gpui::test] - async fn test_singleline_surrounding_character_objects_with_escape( - cx: &mut gpui::TestAppContext, - ) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! { - "h\"e\\\"lˇlo \\\"world\"!" - }) - .await; - cx.simulate_shared_keystrokes("v i \"").await; - cx.shared_state().await.assert_eq(indoc! { - "h\"«e\\\"llo \\\"worldˇ»\"!" - }); - - cx.set_shared_state(indoc! { - "hello \"teˇst \\\"inside\\\" world\"" - }) - .await; - cx.simulate_shared_keystrokes("v i \"").await; - cx.shared_state().await.assert_eq(indoc! { - "hello \"«test \\\"inside\\\" worldˇ»\"" - }); - } - - #[gpui::test] - async fn test_vertical_bars(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.set_state( - indoc! {" - fn boop() { - baz(ˇ|a, b| { bar(|j, k| { })}) - }" - }, - Mode::Normal, - ); - cx.simulate_keystrokes("c i |"); - cx.assert_state( - indoc! {" - fn boop() { - baz(|ˇ| { bar(|j, k| { })}) - }" - }, - Mode::Insert, - ); - cx.simulate_keystrokes("escape 1 8 |"); - cx.assert_state( - indoc! {" - fn boop() { - baz(|| { bar(ˇ|j, k| { })}) - }" - }, - Mode::Normal, - ); - - cx.simulate_keystrokes("v a |"); - cx.assert_state( - indoc! {" - fn boop() { - baz(|| { bar(«|j, k| ˇ»{ })}) - }" - }, - Mode::Visual, - ); - } - - #[gpui::test] - async fn test_argument_object(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // Generic arguments - cx.set_state("fn boop() {}", Mode::Normal); - cx.simulate_keystrokes("v i a"); - cx.assert_state("fn boop<«A: Debugˇ», B>() {}", Mode::Visual); - - // Function arguments - cx.set_state( - "fn boop(ˇarg_a: (Tuple, Of, Types), arg_b: String) {}", - Mode::Normal, - ); - cx.simulate_keystrokes("d a a"); - cx.assert_state("fn boop(ˇarg_b: String) {}", Mode::Normal); - - cx.set_state("std::namespace::test(\"strinˇg\", a.b.c())", Mode::Normal); - cx.simulate_keystrokes("v a a"); - cx.assert_state("std::namespace::test(«\"string\", ˇ»a.b.c())", Mode::Visual); - - // Tuple, vec, and array arguments - cx.set_state( - "fn boop(arg_a: (Tuple, Ofˇ, Types), arg_b: String) {}", - Mode::Normal, - ); - cx.simulate_keystrokes("c i a"); - cx.assert_state( - "fn boop(arg_a: (Tuple, ˇ, Types), arg_b: String) {}", - Mode::Insert, - ); - - // TODO regressed with the up-to-date Rust grammar. - // cx.set_state("let a = (test::call(), 'p', my_macro!{ˇ});", Mode::Normal); - // cx.simulate_keystrokes("c a a"); - // cx.assert_state("let a = (test::call(), 'p'ˇ);", Mode::Insert); - - cx.set_state("let a = [test::call(ˇ), 300];", Mode::Normal); - cx.simulate_keystrokes("c i a"); - cx.assert_state("let a = [ˇ, 300];", Mode::Insert); - - cx.set_state( - "let a = vec![Vec::new(), vecˇ![test::call(), 300]];", - Mode::Normal, - ); - cx.simulate_keystrokes("c a a"); - cx.assert_state("let a = vec![Vec::new()ˇ];", Mode::Insert); - - // Cursor immediately before / after brackets - cx.set_state("let a = [test::call(first_arg)ˇ]", Mode::Normal); - cx.simulate_keystrokes("v i a"); - cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual); - - cx.set_state("let a = [test::callˇ(first_arg)]", Mode::Normal); - cx.simulate_keystrokes("v i a"); - cx.assert_state("let a = [«test::call(first_arg)ˇ»]", Mode::Visual); - } - - #[gpui::test] - async fn test_indent_object(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // Base use case - cx.set_state( - indoc! {" - fn boop() { - // Comment - baz();ˇ - - loop { - bar(1); - bar(2); - } - - result - } - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("v i i"); - cx.assert_state( - indoc! {" - fn boop() { - « // Comment - baz(); - - loop { - bar(1); - bar(2); - } - - resultˇ» - } - "}, - Mode::Visual, - ); - - // Around indent (include line above) - cx.set_state( - indoc! {" - const ABOVE: str = true; - fn boop() { - - hello(); - worˇld() - } - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("v a i"); - cx.assert_state( - indoc! {" - const ABOVE: str = true; - «fn boop() { - - hello(); - world()ˇ» - } - "}, - Mode::Visual, - ); - - // Around indent (include line above & below) - cx.set_state( - indoc! {" - const ABOVE: str = true; - fn boop() { - hellˇo(); - world() - - } - const BELOW: str = true; - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("c a shift-i"); - cx.assert_state( - indoc! {" - const ABOVE: str = true; - ˇ - const BELOW: str = true; - "}, - Mode::Insert, - ); - } - - #[gpui::test] - async fn test_delete_surrounding_character_objects(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - for (start, end) in SURROUNDING_OBJECTS { - let marked_string = SURROUNDING_MARKER_STRING - .replace('`', &start.to_string()) - .replace('\'', &end.to_string()); - - cx.simulate_at_each_offset(&format!("d i {start}"), &marked_string) - .await - .assert_matches(); - cx.simulate_at_each_offset(&format!("d i {end}"), &marked_string) - .await - .assert_matches(); - cx.simulate_at_each_offset(&format!("d a {start}"), &marked_string) - .await - .assert_matches(); - cx.simulate_at_each_offset(&format!("d a {end}"), &marked_string) - .await - .assert_matches(); - } - } - - #[gpui::test] - async fn test_anyquotes_object(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new( - "q", - AnyQuotes, - Some("vim_operator == a || vim_operator == i || vim_operator == cs"), - )]); - }); - - const TEST_CASES: &[(&str, &str, &str, Mode)] = &[ - // the false string in the middle should be considered - ( - "c i q", - "'first' false ˇstring 'second'", - "'first'ˇ'second'", - Mode::Insert, - ), - // Single quotes - ( - "c i q", - "Thisˇ is a 'quote' example.", - "This is a 'ˇ' example.", - Mode::Insert, - ), - ( - "c a q", - "Thisˇ is a 'quote' example.", - "This is a ˇexample.", - Mode::Insert, - ), - ( - "c i q", - "This is a \"simple 'qˇuote'\" example.", - "This is a \"simple 'ˇ'\" example.", - Mode::Insert, - ), - ( - "c a q", - "This is a \"simple 'qˇuote'\" example.", - "This is a \"simpleˇ\" example.", - Mode::Insert, - ), - ( - "c i q", - "This is a 'qˇuote' example.", - "This is a 'ˇ' example.", - Mode::Insert, - ), - ( - "c a q", - "This is a 'qˇuote' example.", - "This is a ˇexample.", - Mode::Insert, - ), - ( - "d i q", - "This is a 'qˇuote' example.", - "This is a 'ˇ' example.", - Mode::Normal, - ), - ( - "d a q", - "This is a 'qˇuote' example.", - "This is a ˇexample.", - Mode::Normal, - ), - // Double quotes - ( - "c i q", - "This is a \"qˇuote\" example.", - "This is a \"ˇ\" example.", - Mode::Insert, - ), - ( - "c a q", - "This is a \"qˇuote\" example.", - "This is a ˇexample.", - Mode::Insert, - ), - ( - "d i q", - "This is a \"qˇuote\" example.", - "This is a \"ˇ\" example.", - Mode::Normal, - ), - ( - "d a q", - "This is a \"qˇuote\" example.", - "This is a ˇexample.", - Mode::Normal, - ), - // Back quotes - ( - "c i q", - "This is a `qˇuote` example.", - "This is a `ˇ` example.", - Mode::Insert, - ), - ( - "c a q", - "This is a `qˇuote` example.", - "This is a ˇexample.", - Mode::Insert, - ), - ( - "d i q", - "This is a `qˇuote` example.", - "This is a `ˇ` example.", - Mode::Normal, - ), - ( - "d a q", - "This is a `qˇuote` example.", - "This is a ˇexample.", - Mode::Normal, - ), - ]; - - for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES { - cx.set_state(initial_state, Mode::Normal); - - cx.simulate_keystrokes(keystrokes); - - cx.assert_state(expected_state, *expected_mode); - } - - const INVALID_CASES: &[(&str, &str, Mode)] = &[ - ("c i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote - ("c a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote - ("d i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote - ("d a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote - ("c i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote - ("c a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote - ("d i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote - ("d a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing back quote - ("c i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote - ("c a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote - ("d i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote - ("d a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote - ]; - - for (keystrokes, initial_state, mode) in INVALID_CASES { - cx.set_state(initial_state, Mode::Normal); - - cx.simulate_keystrokes(keystrokes); - - cx.assert_state(initial_state, *mode); - } - } - - #[gpui::test] - async fn test_miniquotes_object(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new_typescript(cx).await; - - const TEST_CASES: &[(&str, &str, &str, Mode)] = &[ - // Special cases from mini.ai plugin - // the false string in the middle should not be considered - ( - "c i q", - "'first' false ˇstring 'second'", - "'first' false string 'ˇ'", - Mode::Insert, - ), - // Multiline support :)! Same behavior as mini.ai plugin - ( - "c i q", - indoc! {" - ` - first - middle ˇstring - second - ` - "}, - indoc! {" - `ˇ` - "}, - Mode::Insert, - ), - // If you are in the close quote and it is the only quote in the buffer, it should replace inside the quote - // This is not working with the core motion ci' for this special edge case, so I am happy to fix it in MiniQuotes :) - // Bug reference: https://github.com/zed-industries/zed/issues/23889 - ("c i q", "'quote«'ˇ»", "'ˇ'", Mode::Insert), - // Single quotes - ( - "c i q", - "Thisˇ is a 'quote' example.", - "This is a 'ˇ' example.", - Mode::Insert, - ), - ( - "c a q", - "Thisˇ is a 'quote' example.", - "This is a ˇ example.", // same mini.ai plugin behavior - Mode::Insert, - ), - ( - "c i q", - "This is a \"simple 'qˇuote'\" example.", - "This is a \"ˇ\" example.", // Not supported by Tree-sitter queries for now - Mode::Insert, - ), - ( - "c a q", - "This is a \"simple 'qˇuote'\" example.", - "This is a ˇ example.", // Not supported by Tree-sitter queries for now - Mode::Insert, - ), - ( - "c i q", - "This is a 'qˇuote' example.", - "This is a 'ˇ' example.", - Mode::Insert, - ), - ( - "c a q", - "This is a 'qˇuote' example.", - "This is a ˇ example.", // same mini.ai plugin behavior - Mode::Insert, - ), - ( - "d i q", - "This is a 'qˇuote' example.", - "This is a 'ˇ' example.", - Mode::Normal, - ), - ( - "d a q", - "This is a 'qˇuote' example.", - "This is a ˇ example.", // same mini.ai plugin behavior - Mode::Normal, - ), - // Double quotes - ( - "c i q", - "This is a \"qˇuote\" example.", - "This is a \"ˇ\" example.", - Mode::Insert, - ), - ( - "c a q", - "This is a \"qˇuote\" example.", - "This is a ˇ example.", // same mini.ai plugin behavior - Mode::Insert, - ), - ( - "d i q", - "This is a \"qˇuote\" example.", - "This is a \"ˇ\" example.", - Mode::Normal, - ), - ( - "d a q", - "This is a \"qˇuote\" example.", - "This is a ˇ example.", // same mini.ai plugin behavior - Mode::Normal, - ), - // Back quotes - ( - "c i q", - "This is a `qˇuote` example.", - "This is a `ˇ` example.", - Mode::Insert, - ), - ( - "c a q", - "This is a `qˇuote` example.", - "This is a ˇ example.", // same mini.ai plugin behavior - Mode::Insert, - ), - ( - "d i q", - "This is a `qˇuote` example.", - "This is a `ˇ` example.", - Mode::Normal, - ), - ( - "d a q", - "This is a `qˇuote` example.", - "This is a ˇ example.", // same mini.ai plugin behavior - Mode::Normal, - ), - ]; - - for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES { - cx.set_state(initial_state, Mode::Normal); - - cx.simulate_keystrokes(keystrokes); - - cx.assert_state(expected_state, *expected_mode); - } - - const INVALID_CASES: &[(&str, &str, Mode)] = &[ - ("c i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote - ("c a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote - ("d i q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote - ("d a q", "this is a 'qˇuote example.", Mode::Normal), // Missing closing simple quote - ("c i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote - ("c a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote - ("d i q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing double quote - ("d a q", "this is a \"qˇuote example.", Mode::Normal), // Missing closing back quote - ("c i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote - ("c a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote - ("d i q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote - ("d a q", "this is a `qˇuote example.", Mode::Normal), // Missing closing back quote - ]; - - for (keystrokes, initial_state, mode) in INVALID_CASES { - cx.set_state(initial_state, Mode::Normal); - - cx.simulate_keystrokes(keystrokes); - - cx.assert_state(initial_state, *mode); - } - } - - #[gpui::test] - async fn test_anybrackets_object(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new( - "b", - AnyBrackets, - Some("vim_operator == a || vim_operator == i || vim_operator == cs"), - )]); - }); - - const TEST_CASES: &[(&str, &str, &str, Mode)] = &[ - ( - "c i b", - indoc! {" - { - { - ˇprint('hello') - } - } - "}, - indoc! {" - { - { - ˇ - } - } - "}, - Mode::Insert, - ), - // Bracket (Parentheses) - ( - "c i b", - "Thisˇ is a (simple [quote]) example.", - "This is a (ˇ) example.", - Mode::Insert, - ), - ( - "c i b", - "This is a [simple (qˇuote)] example.", - "This is a [simple (ˇ)] example.", - Mode::Insert, - ), - ( - "c a b", - "This is a [simple (qˇuote)] example.", - "This is a [simple ˇ] example.", - Mode::Insert, - ), - ( - "c a b", - "Thisˇ is a (simple [quote]) example.", - "This is a ˇ example.", - Mode::Insert, - ), - ( - "c i b", - "This is a (qˇuote) example.", - "This is a (ˇ) example.", - Mode::Insert, - ), - ( - "c a b", - "This is a (qˇuote) example.", - "This is a ˇ example.", - Mode::Insert, - ), - ( - "d i b", - "This is a (qˇuote) example.", - "This is a (ˇ) example.", - Mode::Normal, - ), - ( - "d a b", - "This is a (qˇuote) example.", - "This is a ˇ example.", - Mode::Normal, - ), - // Square brackets - ( - "c i b", - "This is a [qˇuote] example.", - "This is a [ˇ] example.", - Mode::Insert, - ), - ( - "c a b", - "This is a [qˇuote] example.", - "This is a ˇ example.", - Mode::Insert, - ), - ( - "d i b", - "This is a [qˇuote] example.", - "This is a [ˇ] example.", - Mode::Normal, - ), - ( - "d a b", - "This is a [qˇuote] example.", - "This is a ˇ example.", - Mode::Normal, - ), - // Curly brackets - ( - "c i b", - "This is a {qˇuote} example.", - "This is a {ˇ} example.", - Mode::Insert, - ), - ( - "c a b", - "This is a {qˇuote} example.", - "This is a ˇ example.", - Mode::Insert, - ), - ( - "d i b", - "This is a {qˇuote} example.", - "This is a {ˇ} example.", - Mode::Normal, - ), - ( - "d a b", - "This is a {qˇuote} example.", - "This is a ˇ example.", - Mode::Normal, - ), - ]; - - for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES { - cx.set_state(initial_state, Mode::Normal); - - cx.simulate_keystrokes(keystrokes); - - cx.assert_state(expected_state, *expected_mode); - } - - const INVALID_CASES: &[(&str, &str, Mode)] = &[ - ("c i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket - ("c a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket - ("d i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket - ("d a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket - ("c i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket - ("c a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket - ("d i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket - ("d a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket - ("c i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket - ("c a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket - ("d i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket - ("d a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket - ]; - - for (keystrokes, initial_state, mode) in INVALID_CASES { - cx.set_state(initial_state, Mode::Normal); - - cx.simulate_keystrokes(keystrokes); - - cx.assert_state(initial_state, *mode); - } - } - - #[gpui::test] - async fn test_minibrackets_object(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new( - "b", - MiniBrackets, - Some("vim_operator == a || vim_operator == i || vim_operator == cs"), - )]); - }); - - const TEST_CASES: &[(&str, &str, &str, Mode)] = &[ - // Special cases from mini.ai plugin - // Current line has more priority for the cover or next algorithm, to avoid changing curly brackets which is supper anoying - // Same behavior as mini.ai plugin - ( - "c i b", - indoc! {" - { - { - ˇprint('hello') - } - } - "}, - indoc! {" - { - { - print(ˇ) - } - } - "}, - Mode::Insert, - ), - // If the current line doesn't have brackets then it should consider if the caret is inside an external bracket - // Same behavior as mini.ai plugin - ( - "c i b", - indoc! {" - { - { - ˇ - print('hello') - } - } - "}, - indoc! {" - { - {ˇ} - } - "}, - Mode::Insert, - ), - // If you are in the open bracket then it has higher priority - ( - "c i b", - indoc! {" - «{ˇ» - { - print('hello') - } - } - "}, - indoc! {" - {ˇ} - "}, - Mode::Insert, - ), - // If you are in the close bracket then it has higher priority - ( - "c i b", - indoc! {" - { - { - print('hello') - } - «}ˇ» - "}, - indoc! {" - {ˇ} - "}, - Mode::Insert, - ), - // Bracket (Parentheses) - ( - "c i b", - "Thisˇ is a (simple [quote]) example.", - "This is a (ˇ) example.", - Mode::Insert, - ), - ( - "c i b", - "This is a [simple (qˇuote)] example.", - "This is a [simple (ˇ)] example.", - Mode::Insert, - ), - ( - "c a b", - "This is a [simple (qˇuote)] example.", - "This is a [simple ˇ] example.", - Mode::Insert, - ), - ( - "c a b", - "Thisˇ is a (simple [quote]) example.", - "This is a ˇ example.", - Mode::Insert, - ), - ( - "c i b", - "This is a (qˇuote) example.", - "This is a (ˇ) example.", - Mode::Insert, - ), - ( - "c a b", - "This is a (qˇuote) example.", - "This is a ˇ example.", - Mode::Insert, - ), - ( - "d i b", - "This is a (qˇuote) example.", - "This is a (ˇ) example.", - Mode::Normal, - ), - ( - "d a b", - "This is a (qˇuote) example.", - "This is a ˇ example.", - Mode::Normal, - ), - // Square brackets - ( - "c i b", - "This is a [qˇuote] example.", - "This is a [ˇ] example.", - Mode::Insert, - ), - ( - "c a b", - "This is a [qˇuote] example.", - "This is a ˇ example.", - Mode::Insert, - ), - ( - "d i b", - "This is a [qˇuote] example.", - "This is a [ˇ] example.", - Mode::Normal, - ), - ( - "d a b", - "This is a [qˇuote] example.", - "This is a ˇ example.", - Mode::Normal, - ), - // Curly brackets - ( - "c i b", - "This is a {qˇuote} example.", - "This is a {ˇ} example.", - Mode::Insert, - ), - ( - "c a b", - "This is a {qˇuote} example.", - "This is a ˇ example.", - Mode::Insert, - ), - ( - "d i b", - "This is a {qˇuote} example.", - "This is a {ˇ} example.", - Mode::Normal, - ), - ( - "d a b", - "This is a {qˇuote} example.", - "This is a ˇ example.", - Mode::Normal, - ), - ]; - - for (keystrokes, initial_state, expected_state, expected_mode) in TEST_CASES { - cx.set_state(initial_state, Mode::Normal); - - cx.simulate_keystrokes(keystrokes); - - cx.assert_state(expected_state, *expected_mode); - } - - const INVALID_CASES: &[(&str, &str, Mode)] = &[ - ("c i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket - ("c a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket - ("d i b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket - ("d a b", "this is a (qˇuote example.", Mode::Normal), // Missing closing bracket - ("c i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket - ("c a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket - ("d i b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket - ("d a b", "this is a [qˇuote example.", Mode::Normal), // Missing closing square bracket - ("c i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket - ("c a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket - ("d i b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket - ("d a b", "this is a {qˇuote example.", Mode::Normal), // Missing closing curly bracket - ]; - - for (keystrokes, initial_state, mode) in INVALID_CASES { - cx.set_state(initial_state, Mode::Normal); - - cx.simulate_keystrokes(keystrokes); - - cx.assert_state(initial_state, *mode); - } - } - - #[gpui::test] - async fn test_minibrackets_multibuffer(cx: &mut gpui::TestAppContext) { - // Initialize test context with the TypeScript language loaded, so we - // can actually get brackets definition. - let mut cx = VimTestContext::new(cx, true).await; - - // Update `b` to `MiniBrackets` so we can later use it when simulating - // keystrokes. - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new("b", MiniBrackets, None)]); - }); - - let (editor, cx) = cx.add_window_view(|window, cx| { - let multi_buffer = MultiBuffer::build_multi( - [ - ("111\n222\n333\n444\n", vec![Point::row_range(0..2)]), - ("111\na {bracket} example\n", vec![Point::row_range(0..2)]), - ], - cx, - ); - - // In order for the brackets to actually be found, we need to update - // the language used for the second buffer. This is something that - // is handled automatically when simply using `VimTestContext::new` - // but, since this is being set manually, the language isn't - // automatically set. - let editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx); - let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids(); - if let Some(buffer) = multi_buffer.read(cx).buffer(buffer_ids[1]) { - buffer.update(cx, |buffer, cx| { - buffer.set_language(Some(language::rust_lang()), cx); - }) - }; - - editor - }); - - let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await; - - cx.assert_excerpts_with_selections(indoc! {" - [EXCERPT] - ˇ111 - 222 - [EXCERPT] - 111 - a {bracket} example - " - }); - - cx.simulate_keystrokes("j j j j f r"); - cx.assert_excerpts_with_selections(indoc! {" - [EXCERPT] - 111 - 222 - [EXCERPT] - 111 - a {bˇracket} example - " - }); - - cx.simulate_keystrokes("d i b"); - cx.assert_excerpts_with_selections(indoc! {" - [EXCERPT] - 111 - 222 - [EXCERPT] - 111 - a {ˇ} example - " - }); - } - - #[gpui::test] - async fn test_minibrackets_trailing_space(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state("(trailingˇ whitespace )") - .await; - cx.simulate_shared_keystrokes("v i b").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("escape y i b").await; - cx.shared_clipboard() - .await - .assert_eq("trailing whitespace "); - } - - #[gpui::test] - async fn test_tags(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new_html(cx).await; - - cx.set_state("hˇi!", Mode::Normal); - cx.simulate_keystrokes("v i t"); - cx.assert_state( - "«hi!ˇ»", - Mode::Visual, - ); - cx.simulate_keystrokes("a t"); - cx.assert_state( - "«hi!ˇ»", - Mode::Visual, - ); - cx.simulate_keystrokes("a t"); - cx.assert_state( - "«hi!ˇ»", - Mode::Visual, - ); - - // The cursor is before the tag - cx.set_state( - " ˇ hi!", - Mode::Normal, - ); - cx.simulate_keystrokes("v i t"); - cx.assert_state( - " «hi!ˇ»", - Mode::Visual, - ); - cx.simulate_keystrokes("a t"); - cx.assert_state( - " «hi!ˇ»", - Mode::Visual, - ); - - // The cursor is in the open tag - cx.set_state( - "hi!hello!", - Mode::Normal, - ); - cx.simulate_keystrokes("v a t"); - cx.assert_state( - "«hi!ˇ»hello!", - Mode::Visual, - ); - cx.simulate_keystrokes("i t"); - cx.assert_state( - "«hi!hello!ˇ»", - Mode::Visual, - ); - - // current selection length greater than 1 - cx.set_state( - "<«b>hi!ˇ»", - Mode::Visual, - ); - cx.simulate_keystrokes("i t"); - cx.assert_state( - "«hi!ˇ»", - Mode::Visual, - ); - cx.simulate_keystrokes("a t"); - cx.assert_state( - "«hi!ˇ»", - Mode::Visual, - ); - - cx.set_state( - "<«b>hi!", - Mode::Visual, - ); - cx.simulate_keystrokes("a t"); - cx.assert_state( - "«hi!ˇ»", - Mode::Visual, - ); - } - #[gpui::test] - async fn test_around_containing_word_indent(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(" ˇconst f = (x: unknown) => {") - .await; - cx.simulate_shared_keystrokes("v a w").await; - cx.shared_state() - .await - .assert_eq(" «const ˇ»f = (x: unknown) => {"); - - cx.set_shared_state(" ˇconst f = (x: unknown) => {") - .await; - cx.simulate_shared_keystrokes("y a w").await; - cx.shared_clipboard().await.assert_eq("const "); - - cx.set_shared_state(" ˇconst f = (x: unknown) => {") - .await; - cx.simulate_shared_keystrokes("d a w").await; - cx.shared_state() - .await - .assert_eq(" ˇf = (x: unknown) => {"); - cx.shared_clipboard().await.assert_eq("const "); - - cx.set_shared_state(" ˇconst f = (x: unknown) => {") - .await; - cx.simulate_shared_keystrokes("c a w").await; - cx.shared_state() - .await - .assert_eq(" ˇf = (x: unknown) => {"); - cx.shared_clipboard().await.assert_eq("const "); - } -} diff --git a/crates/vim/src/replace.rs b/crates/vim/src/replace.rs deleted file mode 100644 index 63d452f84b..0000000000 --- a/crates/vim/src/replace.rs +++ /dev/null @@ -1,559 +0,0 @@ -use crate::{ - Operator, Vim, - motion::{self, Motion}, - object::Object, - state::Mode, -}; -use editor::{ - Anchor, Bias, Editor, EditorSnapshot, SelectionEffects, ToOffset, ToPoint, - display_map::ToDisplayPoint, -}; -use gpui::{ClipboardEntry, Context, Window, actions}; -use language::{Point, SelectionGoal}; -use std::ops::Range; -use std::sync::Arc; - -actions!( - vim, - [ - /// Toggles replace mode. - ToggleReplace, - /// Undoes the last replacement. - UndoReplace - ] -); - -pub fn register(editor: &mut Editor, cx: &mut Context) { - Vim::action(editor, cx, |vim, _: &ToggleReplace, window, cx| { - vim.replacements = vec![]; - vim.start_recording(cx); - vim.switch_mode(Mode::Replace, false, window, cx); - }); - - Vim::action(editor, cx, |vim, _: &UndoReplace, window, cx| { - if vim.mode != Mode::Replace { - return; - } - let count = Vim::take_count(cx); - Vim::take_forced_motion(cx); - vim.undo_replace(count, window, cx) - }); -} - -struct VimExchange; - -impl Vim { - pub(crate) fn multi_replace( - &mut self, - text: Arc, - window: &mut Window, - cx: &mut Context, - ) { - self.update_editor(cx, |vim, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - editor.set_clip_at_line_ends(false, cx); - let map = editor.snapshot(window, cx); - let display_selections = editor.selections.all::(&map.display_snapshot); - - // Handles all string that require manipulation, including inserts and replaces - let edits = display_selections - .into_iter() - .map(|selection| { - let is_new_line = text.as_ref() == "\n"; - let mut range = selection.range(); - // "\n" need to be handled separately, because when a "\n" is typing, - // we don't do a replace, we need insert a "\n" - if !is_new_line { - range.end.column += 1; - range.end = map.buffer_snapshot().clip_point(range.end, Bias::Right); - } - let replace_range = map.buffer_snapshot().anchor_before(range.start) - ..map.buffer_snapshot().anchor_after(range.end); - let current_text = map - .buffer_snapshot() - .text_for_range(replace_range.clone()) - .collect(); - vim.replacements.push((replace_range.clone(), current_text)); - (replace_range, text.clone()) - }) - .collect::>(); - - editor.edit_with_block_indent(edits.clone(), Vec::new(), cx); - - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_anchor_ranges(edits.iter().map(|(range, _)| range.end..range.end)); - }); - editor.set_clip_at_line_ends(true, cx); - }); - }); - } - - fn undo_replace( - &mut self, - maybe_times: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.update_editor(cx, |vim, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - editor.set_clip_at_line_ends(false, cx); - let map = editor.snapshot(window, cx); - let selections = editor.selections.all::(&map.display_snapshot); - let mut new_selections = vec![]; - let edits: Vec<(Range, String)> = selections - .into_iter() - .filter_map(|selection| { - let end = selection.head(); - let start = motion::wrapping_left( - &map, - end.to_display_point(&map), - maybe_times.unwrap_or(1), - ) - .to_point(&map); - new_selections.push( - map.buffer_snapshot().anchor_before(start) - ..map.buffer_snapshot().anchor_before(start), - ); - - let mut undo = None; - let edit_range = start..end; - for (i, (range, inverse)) in vim.replacements.iter().rev().enumerate() { - if range.start.to_point(&map.buffer_snapshot()) <= edit_range.start - && range.end.to_point(&map.buffer_snapshot()) >= edit_range.end - { - undo = Some(inverse.clone()); - vim.replacements.remove(vim.replacements.len() - i - 1); - break; - } - } - Some((edit_range, undo?)) - }) - .collect::>(); - - editor.edit(edits, cx); - - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges(new_selections); - }); - editor.set_clip_at_line_ends(true, cx); - }); - }); - } - - pub fn exchange_object( - &mut self, - object: Object, - around: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - self.update_editor(cx, |vim, editor, cx| { - editor.set_clip_at_line_ends(false, cx); - let mut selection = editor - .selections - .newest_display(&editor.display_snapshot(cx)); - let snapshot = editor.snapshot(window, cx); - object.expand_selection(&snapshot, &mut selection, around, None); - let start = snapshot - .buffer_snapshot() - .anchor_before(selection.start.to_point(&snapshot)); - let end = snapshot - .buffer_snapshot() - .anchor_before(selection.end.to_point(&snapshot)); - let new_range = start..end; - vim.exchange_impl(new_range, editor, &snapshot, window, cx); - editor.set_clip_at_line_ends(true, cx); - }); - } - - pub fn exchange_visual(&mut self, window: &mut Window, cx: &mut Context) { - self.stop_recording(cx); - self.update_editor(cx, |vim, editor, cx| { - let selection = editor.selections.newest_anchor(); - let new_range = selection.start..selection.end; - let snapshot = editor.snapshot(window, cx); - vim.exchange_impl(new_range, editor, &snapshot, window, cx); - }); - self.switch_mode(Mode::Normal, false, window, cx); - } - - pub fn clear_exchange(&mut self, window: &mut Window, cx: &mut Context) { - self.stop_recording(cx); - self.update_editor(cx, |_, editor, cx| { - editor.clear_background_highlights::(cx); - }); - self.clear_operator(window, cx); - } - - pub fn exchange_motion( - &mut self, - motion: Motion, - times: Option, - forced_motion: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - self.update_editor(cx, |vim, editor, cx| { - editor.set_clip_at_line_ends(false, cx); - let text_layout_details = editor.text_layout_details(window); - let mut selection = editor - .selections - .newest_display(&editor.display_snapshot(cx)); - let snapshot = editor.snapshot(window, cx); - motion.expand_selection( - &snapshot, - &mut selection, - times, - &text_layout_details, - forced_motion, - ); - let start = snapshot - .buffer_snapshot() - .anchor_before(selection.start.to_point(&snapshot)); - let end = snapshot - .buffer_snapshot() - .anchor_before(selection.end.to_point(&snapshot)); - let new_range = start..end; - vim.exchange_impl(new_range, editor, &snapshot, window, cx); - editor.set_clip_at_line_ends(true, cx); - }); - } - - pub fn exchange_impl( - &self, - new_range: Range, - editor: &mut Editor, - snapshot: &EditorSnapshot, - window: &mut Window, - cx: &mut Context, - ) { - if let Some((_, ranges)) = editor.clear_background_highlights::(cx) { - let previous_range = ranges[0].clone(); - - let new_range_start = new_range.start.to_offset(&snapshot.buffer_snapshot()); - let new_range_end = new_range.end.to_offset(&snapshot.buffer_snapshot()); - let previous_range_end = previous_range.end.to_offset(&snapshot.buffer_snapshot()); - let previous_range_start = previous_range.start.to_offset(&snapshot.buffer_snapshot()); - - let text_for = |range: Range| { - snapshot - .buffer_snapshot() - .text_for_range(range) - .collect::() - }; - - let mut final_cursor_position = None; - - if previous_range_end < new_range_start || new_range_end < previous_range_start { - let previous_text = text_for(previous_range.clone()); - let new_text = text_for(new_range.clone()); - final_cursor_position = Some(new_range.start.to_display_point(snapshot)); - - editor.edit([(previous_range, new_text), (new_range, previous_text)], cx); - } else if new_range_start <= previous_range_start && new_range_end >= previous_range_end - { - final_cursor_position = Some(new_range.start.to_display_point(snapshot)); - editor.edit([(new_range, text_for(previous_range))], cx); - } else if previous_range_start <= new_range_start && previous_range_end >= new_range_end - { - final_cursor_position = Some(previous_range.start.to_display_point(snapshot)); - editor.edit([(previous_range, text_for(new_range))], cx); - } - - if let Some(position) = final_cursor_position { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|_map, selection| { - selection.collapse_to(position, SelectionGoal::None); - }); - }) - } - } else { - let ranges = [new_range]; - editor.highlight_background::( - &ranges, - |_, theme| theme.colors().editor_document_highlight_read_background, - cx, - ); - } - } - - /// Pastes the clipboard contents, replacing the same number of characters - /// as the clipboard's contents. - pub fn paste_replace(&mut self, window: &mut Window, cx: &mut Context) { - let clipboard_text = - cx.read_from_clipboard() - .and_then(|item| match item.entries().first() { - Some(ClipboardEntry::String(text)) => Some(text.text().to_string()), - _ => None, - }); - - if let Some(text) = clipboard_text { - self.push_operator(Operator::Replace, window, cx); - self.normal_replace(Arc::from(text), window, cx); - } - } -} - -#[cfg(test)] -mod test { - use gpui::ClipboardItem; - use indoc::indoc; - - use crate::{ - state::Mode, - test::{NeovimBackedTestContext, VimTestContext}, - }; - - #[gpui::test] - async fn test_enter_and_exit_replace_mode(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.simulate_keystrokes("shift-r"); - assert_eq!(cx.mode(), Mode::Replace); - cx.simulate_keystrokes("escape"); - assert_eq!(cx.mode(), Mode::Normal); - } - - #[gpui::test] - #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] - async fn test_replace_mode(cx: &mut gpui::TestAppContext) { - let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await; - - // test normal replace - cx.set_shared_state(indoc! {" - ˇThe quick brown - fox jumps over - the lazy dog."}) - .await; - cx.simulate_shared_keystrokes("shift-r O n e").await; - cx.shared_state().await.assert_eq(indoc! {" - Oneˇ quick brown - fox jumps over - the lazy dog."}); - - // test replace with line ending - cx.set_shared_state(indoc! {" - The quick browˇn - fox jumps over - the lazy dog."}) - .await; - cx.simulate_shared_keystrokes("shift-r O n e").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick browOneˇ - fox jumps over - the lazy dog."}); - - // test replace with blank line - cx.set_shared_state(indoc! {" - The quick brown - ˇ - fox jumps over - the lazy dog."}) - .await; - cx.simulate_shared_keystrokes("shift-r O n e").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - Oneˇ - fox jumps over - the lazy dog."}); - - // test replace with newline - cx.set_shared_state(indoc! {" - The quˇick brown - fox jumps over - the lazy dog."}) - .await; - cx.simulate_shared_keystrokes("shift-r enter O n e").await; - cx.shared_state().await.assert_eq(indoc! {" - The qu - Oneˇ brown - fox jumps over - the lazy dog."}); - - // test replace with multi cursor and newline - cx.set_state( - indoc! {" - ˇThe quick brown - fox jumps over - the lazy ˇdog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("shift-r O n e"); - cx.assert_state( - indoc! {" - Oneˇ quick brown - fox jumps over - the lazy Oneˇ."}, - Mode::Replace, - ); - cx.simulate_keystrokes("enter T w o"); - cx.assert_state( - indoc! {" - One - Twoˇck brown - fox jumps over - the lazy One - Twoˇ"}, - Mode::Replace, - ); - } - - #[gpui::test] - async fn test_replace_mode_with_counts(cx: &mut gpui::TestAppContext) { - let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇhello\n").await; - cx.simulate_shared_keystrokes("3 shift-r - escape").await; - cx.shared_state().await.assert_eq("--ˇ-lo\n"); - - cx.set_shared_state("ˇhello\n").await; - cx.simulate_shared_keystrokes("3 shift-r a b c escape") - .await; - cx.shared_state().await.assert_eq("abcabcabˇc\n"); - } - - #[gpui::test] - async fn test_replace_mode_repeat(cx: &mut gpui::TestAppContext) { - let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇhello world\n").await; - cx.simulate_shared_keystrokes("shift-r - - - escape 4 l .") - .await; - cx.shared_state().await.assert_eq("---lo --ˇ-ld\n"); - } - - #[gpui::test] - async fn test_replace_mode_undo(cx: &mut gpui::TestAppContext) { - let mut cx: NeovimBackedTestContext = NeovimBackedTestContext::new(cx).await; - - const UNDO_REPLACE_EXAMPLES: &[&str] = &[ - // replace undo with single line - "ˇThe quick brown fox jumps over the lazy dog.", - // replace undo with ending line - indoc! {" - The quick browˇn - fox jumps over - the lazy dog." - }, - // replace undo with empty line - indoc! {" - The quick brown - ˇ - fox jumps over - the lazy dog." - }, - ]; - - for example in UNDO_REPLACE_EXAMPLES { - // normal undo - cx.simulate("shift-r O n e backspace backspace backspace", example) - .await - .assert_matches(); - // undo with new line - cx.simulate("shift-r O enter e backspace backspace backspace", example) - .await - .assert_matches(); - cx.simulate( - "shift-r O enter n enter e backspace backspace backspace backspace backspace", - example, - ) - .await - .assert_matches(); - } - } - - #[gpui::test] - async fn test_replace_multicursor(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.set_state("ˇabcˇabcabc", Mode::Normal); - cx.simulate_keystrokes("shift-r 1 2 3 4"); - cx.assert_state("1234ˇ234ˇbc", Mode::Replace); - assert_eq!(cx.mode(), Mode::Replace); - cx.simulate_keystrokes("backspace backspace backspace backspace backspace"); - cx.assert_state("ˇabˇcabcabc", Mode::Replace); - } - - #[gpui::test] - async fn test_replace_undo(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("ˇaaaa", Mode::Normal); - cx.simulate_keystrokes("0 shift-r b b b escape u"); - cx.assert_state("ˇaaaa", Mode::Normal); - } - - #[gpui::test] - async fn test_exchange_separate_range(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("ˇhello world", Mode::Normal); - cx.simulate_keystrokes("c x i w w c x i w"); - cx.assert_state("world ˇhello", Mode::Normal); - } - - #[gpui::test] - async fn test_exchange_complete_overlap(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("ˇhello world", Mode::Normal); - cx.simulate_keystrokes("c x x w c x i w"); - cx.assert_state("ˇworld", Mode::Normal); - - // the focus should still be at the start of the word if we reverse the - // order of selections (smaller -> larger) - cx.set_state("ˇhello world", Mode::Normal); - cx.simulate_keystrokes("c x i w c x x"); - cx.assert_state("ˇhello", Mode::Normal); - } - - #[gpui::test] - async fn test_exchange_partial_overlap(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("ˇhello world", Mode::Normal); - cx.simulate_keystrokes("c x t r w c x i w"); - cx.assert_state("hello ˇworld", Mode::Normal); - } - - #[gpui::test] - async fn test_clear_exchange_clears_operator(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("ˇirrelevant", Mode::Normal); - cx.simulate_keystrokes("c x c"); - - assert_eq!(cx.active_operator(), None); - } - - #[gpui::test] - async fn test_clear_exchange(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("ˇhello world", Mode::Normal); - cx.simulate_keystrokes("c x i w c x c"); - - cx.update_editor(|editor, window, cx| { - let highlights = editor.all_text_background_highlights(window, cx); - assert_eq!(0, highlights.len()); - }); - } - - #[gpui::test] - async fn test_paste_replace(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state(indoc! {"ˇ123"}, Mode::Replace); - cx.write_to_clipboard(ClipboardItem::new_string("456".to_string())); - cx.dispatch_action(editor::actions::Paste); - cx.assert_state(indoc! {"45ˇ6"}, Mode::Replace); - - // If the clipboard's contents length is greater than the remaining text - // length, nothing sould be replace and cursor should remain in the same - // position. - cx.set_state(indoc! {"ˇ123"}, Mode::Replace); - cx.write_to_clipboard(ClipboardItem::new_string("4567".to_string())); - cx.dispatch_action(editor::actions::Paste); - cx.assert_state(indoc! {"ˇ123"}, Mode::Replace); - } -} diff --git a/crates/vim/src/rewrap.rs b/crates/vim/src/rewrap.rs deleted file mode 100644 index 85e1967af0..0000000000 --- a/crates/vim/src/rewrap.rs +++ /dev/null @@ -1,148 +0,0 @@ -use crate::{Vim, motion::Motion, object::Object, state::Mode}; -use collections::HashMap; -use editor::{Bias, Editor, RewrapOptions, SelectionEffects, display_map::ToDisplayPoint}; -use gpui::{Context, Window, actions}; -use language::SelectionGoal; - -actions!( - vim, - [ - /// Rewraps the selected text to fit within the line width. - Rewrap - ] -); - -pub(crate) fn register(editor: &mut Editor, cx: &mut Context) { - Vim::action(editor, cx, |vim, _: &Rewrap, window, cx| { - vim.record_current_action(cx); - Vim::take_count(cx); - Vim::take_forced_motion(cx); - vim.store_visual_marks(window, cx); - vim.update_editor(cx, |vim, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - let mut positions = vim.save_selection_starts(editor, cx); - editor.rewrap_impl( - RewrapOptions { - override_language_settings: true, - ..Default::default() - }, - cx, - ); - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - if let Some(anchor) = positions.remove(&selection.id) { - let mut point = anchor.to_display_point(map); - *point.column_mut() = 0; - selection.collapse_to(point, SelectionGoal::None); - } - }); - }); - }); - }); - if vim.mode.is_visual() { - vim.switch_mode(Mode::Normal, true, window, cx) - } - }); -} - -impl Vim { - pub(crate) fn rewrap_motion( - &mut self, - motion: Motion, - times: Option, - forced_motion: bool, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - self.update_editor(cx, |_, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - editor.transact(window, cx, |editor, window, cx| { - let mut selection_starts: HashMap<_, _> = Default::default(); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let anchor = map.display_point_to_anchor(selection.head(), Bias::Right); - selection_starts.insert(selection.id, anchor); - motion.expand_selection( - map, - selection, - times, - &text_layout_details, - forced_motion, - ); - }); - }); - editor.rewrap_impl( - RewrapOptions { - override_language_settings: true, - ..Default::default() - }, - cx, - ); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let anchor = selection_starts.remove(&selection.id).unwrap(); - let mut point = anchor.to_display_point(map); - *point.column_mut() = 0; - selection.collapse_to(point, SelectionGoal::None); - }); - }); - }); - }); - } - - pub(crate) fn rewrap_object( - &mut self, - object: Object, - around: bool, - times: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - self.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - let mut original_positions: HashMap<_, _> = Default::default(); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let anchor = map.display_point_to_anchor(selection.head(), Bias::Right); - original_positions.insert(selection.id, anchor); - object.expand_selection(map, selection, around, times); - }); - }); - editor.rewrap_impl( - RewrapOptions { - override_language_settings: true, - ..Default::default() - }, - cx, - ); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let anchor = original_positions.remove(&selection.id).unwrap(); - let mut point = anchor.to_display_point(map); - *point.column_mut() = 0; - selection.collapse_to(point, SelectionGoal::None); - }); - }); - }); - }); - } -} - -#[cfg(test)] -mod test { - use crate::test::NeovimBackedTestContext; - - #[gpui::test] - async fn test_indent_gv(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_neovim_option("shiftwidth=4").await; - - cx.set_shared_state("ˇhello\nworld\n").await; - cx.simulate_shared_keystrokes("v j > g v").await; - cx.shared_state() - .await - .assert_eq("« hello\n ˇ» world\n"); - } -} diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs deleted file mode 100644 index e96fd3a329..0000000000 --- a/crates/vim/src/state.rs +++ /dev/null @@ -1,1835 +0,0 @@ -use crate::command::command_interceptor; -use crate::motion::MotionKind; -use crate::normal::repeat::Replayer; -use crate::surrounds::SurroundsType; -use crate::{ToggleMarksView, ToggleRegistersView, UseSystemClipboard, Vim, VimAddon, VimSettings}; -use crate::{motion::Motion, object::Object}; -use anyhow::Result; -use collections::HashMap; -use command_palette_hooks::{CommandPaletteFilter, GlobalCommandPaletteInterceptor}; -use db::{ - sqlez::{domain::Domain, thread_safe_connection::ThreadSafeConnection}, - sqlez_macros::sql, -}; -use editor::display_map::{is_invisible, replacement}; -use editor::{Anchor, ClipboardSelection, Editor, MultiBuffer, ToPoint as EditorToPoint}; -use gpui::{ - Action, App, AppContext, BorrowAppContext, ClipboardEntry, ClipboardItem, DismissEvent, Entity, - EntityId, Global, HighlightStyle, StyledText, Subscription, Task, TextStyle, WeakEntity, -}; -use language::{Buffer, BufferEvent, BufferId, Chunk, Point}; -use multi_buffer::MultiBufferRow; -use picker::{Picker, PickerDelegate}; -use project::{Project, ProjectItem, ProjectPath}; -use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsStore}; -use std::borrow::BorrowMut; -use std::collections::HashSet; -use std::path::Path; -use std::{fmt::Display, ops::Range, sync::Arc}; -use text::{Bias, ToPoint}; -use theme::ThemeSettings; -use ui::{ - ActiveTheme, Context, Div, FluentBuilder, KeyBinding, ParentElement, SharedString, Styled, - StyledTypography, Window, h_flex, rems, -}; -use util::ResultExt; -use util::rel_path::RelPath; -use workspace::searchable::Direction; -use workspace::{Workspace, WorkspaceDb, WorkspaceId}; - -#[derive(Clone, Copy, Default, Debug, PartialEq, Serialize, Deserialize)] -pub enum Mode { - #[default] - Normal, - Insert, - Replace, - Visual, - VisualLine, - VisualBlock, - HelixNormal, - HelixSelect, -} - -impl Display for Mode { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Mode::Normal => write!(f, "NORMAL"), - Mode::Insert => write!(f, "INSERT"), - Mode::Replace => write!(f, "REPLACE"), - Mode::Visual => write!(f, "VISUAL"), - Mode::VisualLine => write!(f, "VISUAL LINE"), - Mode::VisualBlock => write!(f, "VISUAL BLOCK"), - Mode::HelixNormal => write!(f, "NORMAL"), - Mode::HelixSelect => write!(f, "SELECT"), - } - } -} - -impl Mode { - pub fn is_visual(&self) -> bool { - match self { - Self::Visual | Self::VisualLine | Self::VisualBlock | Self::HelixSelect => true, - Self::Normal | Self::Insert | Self::Replace | Self::HelixNormal => false, - } - } -} - -#[derive(Clone, Debug, PartialEq)] -pub enum Operator { - Change, - Delete, - Yank, - Replace, - Object { - around: bool, - }, - FindForward { - before: bool, - multiline: bool, - }, - FindBackward { - after: bool, - multiline: bool, - }, - Sneak { - first_char: Option, - }, - SneakBackward { - first_char: Option, - }, - AddSurrounds { - // Typically no need to configure this as `SendKeystrokes` can be used - see #23088. - target: Option, - }, - ChangeSurrounds { - target: Option, - /// Represents whether the opening bracket was used for the target - /// object. - opening: bool, - }, - DeleteSurrounds, - Mark, - Jump { - line: bool, - }, - Indent, - Outdent, - AutoIndent, - Rewrap, - ShellCommand, - Lowercase, - Uppercase, - OppositeCase, - Rot13, - Rot47, - Digraph { - first_char: Option, - }, - Literal { - prefix: Option, - }, - Register, - RecordRegister, - ReplayRegister, - ToggleComments, - ReplaceWithRegister, - Exchange, - HelixMatch, - HelixNext { - around: bool, - }, - HelixPrevious { - around: bool, - }, -} - -#[derive(Default, Clone, Debug)] -pub enum RecordedSelection { - #[default] - None, - Visual { - rows: u32, - cols: u32, - }, - SingleLine { - cols: u32, - }, - VisualBlock { - rows: u32, - cols: u32, - }, - VisualLine { - rows: u32, - }, -} - -#[derive(Default, Clone, Debug)] -pub struct Register { - pub(crate) text: SharedString, - pub(crate) clipboard_selections: Option>, -} - -impl From for ClipboardItem { - fn from(register: Register) -> Self { - if let Some(clipboard_selections) = register.clipboard_selections { - ClipboardItem::new_string_with_json_metadata(register.text.into(), clipboard_selections) - } else { - ClipboardItem::new_string(register.text.into()) - } - } -} - -impl From for Register { - fn from(item: ClipboardItem) -> Self { - // For now, we don't store metadata for multiple entries. - match item.entries().first() { - Some(ClipboardEntry::String(value)) if item.entries().len() == 1 => Register { - text: value.text().to_owned().into(), - clipboard_selections: value.metadata_json::>(), - }, - // For now, registers can't store images. This could change in the future. - _ => Register::default(), - } - } -} - -impl From for Register { - fn from(text: String) -> Self { - Register { - text: text.into(), - clipboard_selections: None, - } - } -} - -#[derive(Default)] -pub struct VimGlobals { - pub last_find: Option, - - pub dot_recording: bool, - pub dot_replaying: bool, - - /// pre_count is the number before an operator is specified (3 in 3d2d) - pub pre_count: Option, - /// post_count is the number after an operator is specified (2 in 3d2d) - pub post_count: Option, - pub forced_motion: bool, - pub stop_recording_after_next_action: bool, - pub ignore_current_insertion: bool, - pub recording_count: Option, - pub recorded_count: Option, - pub recording_actions: Vec, - pub recorded_actions: Vec, - pub recorded_selection: RecordedSelection, - - pub recording_register: Option, - pub last_recorded_register: Option, - pub last_replayed_register: Option, - pub replayer: Option, - - pub last_yank: Option, - pub registers: HashMap, - pub recordings: HashMap>, - - pub focused_vim: Option>, - - pub marks: HashMap>, -} - -pub struct MarksState { - workspace: WeakEntity, - - multibuffer_marks: HashMap>>, - buffer_marks: HashMap>>, - watched_buffers: HashMap, - - serialized_marks: HashMap, HashMap>>, - global_marks: HashMap, - - _subscription: Subscription, -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum MarkLocation { - Buffer(EntityId), - Path(Arc), -} - -pub enum Mark { - Local(Vec), - Buffer(EntityId, Vec), - Path(Arc, Vec), -} - -impl MarksState { - pub fn new(workspace: &Workspace, cx: &mut App) -> Entity { - cx.new(|cx| { - let buffer_store = workspace.project().read(cx).buffer_store().clone(); - let subscription = cx.subscribe(&buffer_store, move |this: &mut Self, _, event, cx| { - if let project::buffer_store::BufferStoreEvent::BufferAdded(buffer) = event { - this.on_buffer_loaded(buffer, cx); - } - }); - - let mut this = Self { - workspace: workspace.weak_handle(), - multibuffer_marks: HashMap::default(), - buffer_marks: HashMap::default(), - watched_buffers: HashMap::default(), - serialized_marks: HashMap::default(), - global_marks: HashMap::default(), - _subscription: subscription, - }; - - this.load(cx); - this - }) - } - - fn workspace_id(&self, cx: &App) -> Option { - self.workspace - .read_with(cx, |workspace, _| workspace.database_id()) - .ok() - .flatten() - } - - fn project(&self, cx: &App) -> Option> { - self.workspace - .read_with(cx, |workspace, _| workspace.project().clone()) - .ok() - } - - fn load(&mut self, cx: &mut Context) { - cx.spawn(async move |this, cx| { - let Some(workspace_id) = this.update(cx, |this, cx| this.workspace_id(cx)).ok()? else { - return None; - }; - let (marks, paths) = cx - .background_spawn(async move { - let marks = DB.get_marks(workspace_id)?; - let paths = DB.get_global_marks_paths(workspace_id)?; - anyhow::Ok((marks, paths)) - }) - .await - .log_err()?; - this.update(cx, |this, cx| this.loaded(marks, paths, cx)) - .ok() - }) - .detach(); - } - - fn loaded( - &mut self, - marks: Vec, - global_mark_paths: Vec<(String, Arc)>, - cx: &mut Context, - ) { - let Some(project) = self.project(cx) else { - return; - }; - - for mark in marks { - self.serialized_marks - .entry(mark.path) - .or_default() - .insert(mark.name, mark.points); - } - - for (name, path) in global_mark_paths { - self.global_marks - .insert(name, MarkLocation::Path(path.clone())); - - let project_path = project - .read(cx) - .worktrees(cx) - .filter_map(|worktree| { - let relative = path.strip_prefix(worktree.read(cx).abs_path()).ok()?; - let path = RelPath::new(relative, worktree.read(cx).path_style()).log_err()?; - Some(ProjectPath { - worktree_id: worktree.read(cx).id(), - path: path.into_arc(), - }) - }) - .next(); - if let Some(buffer) = project_path - .and_then(|project_path| project.read(cx).get_open_buffer(&project_path, cx)) - { - self.on_buffer_loaded(&buffer, cx) - } - } - } - - pub fn on_buffer_loaded(&mut self, buffer_handle: &Entity, cx: &mut Context) { - let Some(project) = self.project(cx) else { - return; - }; - let Some(project_path) = buffer_handle.read(cx).project_path(cx) else { - return; - }; - let Some(abs_path) = project.read(cx).absolute_path(&project_path, cx) else { - return; - }; - let abs_path: Arc = abs_path.into(); - - let Some(serialized_marks) = self.serialized_marks.get(&abs_path) else { - return; - }; - - let mut loaded_marks = HashMap::default(); - let buffer = buffer_handle.read(cx); - for (name, points) in serialized_marks.iter() { - loaded_marks.insert( - name.clone(), - points - .iter() - .map(|point| buffer.anchor_before(buffer.clip_point(*point, Bias::Left))) - .collect(), - ); - } - self.buffer_marks.insert(buffer.remote_id(), loaded_marks); - self.watch_buffer(MarkLocation::Path(abs_path), buffer_handle, cx) - } - - fn serialize_buffer_marks( - &mut self, - path: Arc, - buffer: &Entity, - cx: &mut Context, - ) { - let new_points: HashMap> = - if let Some(anchors) = self.buffer_marks.get(&buffer.read(cx).remote_id()) { - anchors - .iter() - .map(|(name, anchors)| { - ( - name.clone(), - buffer - .read(cx) - .summaries_for_anchors::(anchors) - .collect(), - ) - }) - .collect() - } else { - HashMap::default() - }; - let old_points = self.serialized_marks.get(&path); - if old_points == Some(&new_points) { - return; - } - let mut to_write = HashMap::default(); - - for (key, value) in &new_points { - if self.is_global_mark(key) - && self.global_marks.get(key) != Some(&MarkLocation::Path(path.clone())) - { - if let Some(workspace_id) = self.workspace_id(cx) { - let path = path.clone(); - let key = key.clone(); - cx.background_spawn(async move { - DB.set_global_mark_path(workspace_id, key, path).await - }) - .detach_and_log_err(cx); - } - - self.global_marks - .insert(key.clone(), MarkLocation::Path(path.clone())); - } - if old_points.and_then(|o| o.get(key)) != Some(value) { - to_write.insert(key.clone(), value.clone()); - } - } - - self.serialized_marks.insert(path.clone(), new_points); - - if let Some(workspace_id) = self.workspace_id(cx) { - cx.background_spawn(async move { - DB.set_marks(workspace_id, path.clone(), to_write).await?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - } - - fn is_global_mark(&self, key: &str) -> bool { - key.chars() - .next() - .is_some_and(|c| c.is_uppercase() || c.is_digit(10)) - } - - fn rename_buffer( - &mut self, - old_path: MarkLocation, - new_path: Arc, - buffer: &Entity, - cx: &mut Context, - ) { - if let MarkLocation::Buffer(entity_id) = old_path - && let Some(old_marks) = self.multibuffer_marks.remove(&entity_id) - { - let buffer_marks = old_marks - .into_iter() - .map(|(k, v)| (k, v.into_iter().map(|anchor| anchor.text_anchor).collect())) - .collect(); - self.buffer_marks - .insert(buffer.read(cx).remote_id(), buffer_marks); - } - self.watch_buffer(MarkLocation::Path(new_path.clone()), buffer, cx); - self.serialize_buffer_marks(new_path, buffer, cx); - } - - fn path_for_buffer(&self, buffer: &Entity, cx: &App) -> Option> { - let project_path = buffer.read(cx).project_path(cx)?; - let project = self.project(cx)?; - let abs_path = project.read(cx).absolute_path(&project_path, cx)?; - Some(abs_path.into()) - } - - fn points_at( - &self, - location: &MarkLocation, - multi_buffer: &Entity, - cx: &App, - ) -> bool { - match location { - MarkLocation::Buffer(entity_id) => entity_id == &multi_buffer.entity_id(), - MarkLocation::Path(path) => { - let Some(singleton) = multi_buffer.read(cx).as_singleton() else { - return false; - }; - self.path_for_buffer(&singleton, cx).as_ref() == Some(path) - } - } - } - - pub fn watch_buffer( - &mut self, - mark_location: MarkLocation, - buffer_handle: &Entity, - cx: &mut Context, - ) { - let on_change = cx.subscribe(buffer_handle, move |this, buffer, event, cx| match event { - BufferEvent::Edited => { - if let Some(path) = this.path_for_buffer(&buffer, cx) { - this.serialize_buffer_marks(path, &buffer, cx); - } - } - BufferEvent::FileHandleChanged => { - let buffer_id = buffer.read(cx).remote_id(); - if let Some(old_path) = this - .watched_buffers - .get(&buffer_id.clone()) - .map(|(path, _, _)| path.clone()) - && let Some(new_path) = this.path_for_buffer(&buffer, cx) - { - this.rename_buffer(old_path, new_path, &buffer, cx) - } - } - _ => {} - }); - - let on_release = cx.observe_release(buffer_handle, |this, buffer, _| { - this.watched_buffers.remove(&buffer.remote_id()); - this.buffer_marks.remove(&buffer.remote_id()); - }); - - self.watched_buffers.insert( - buffer_handle.read(cx).remote_id(), - (mark_location, on_change, on_release), - ); - } - - pub fn set_mark( - &mut self, - name: String, - multibuffer: &Entity, - anchors: Vec, - cx: &mut Context, - ) { - let buffer = multibuffer.read(cx).as_singleton(); - let abs_path = buffer.as_ref().and_then(|b| self.path_for_buffer(b, cx)); - - let Some(abs_path) = abs_path else { - self.multibuffer_marks - .entry(multibuffer.entity_id()) - .or_default() - .insert(name.clone(), anchors); - if self.is_global_mark(&name) { - self.global_marks - .insert(name, MarkLocation::Buffer(multibuffer.entity_id())); - } - if let Some(buffer) = buffer { - let buffer_id = buffer.read(cx).remote_id(); - if !self.watched_buffers.contains_key(&buffer_id) { - self.watch_buffer(MarkLocation::Buffer(multibuffer.entity_id()), &buffer, cx) - } - } - return; - }; - let Some(buffer) = buffer else { - return; - }; - - let buffer_id = buffer.read(cx).remote_id(); - self.buffer_marks.entry(buffer_id).or_default().insert( - name, - anchors - .into_iter() - .map(|anchor| anchor.text_anchor) - .collect(), - ); - if !self.watched_buffers.contains_key(&buffer_id) { - self.watch_buffer(MarkLocation::Path(abs_path.clone()), &buffer, cx) - } - self.serialize_buffer_marks(abs_path, &buffer, cx) - } - - pub fn get_mark( - &self, - name: &str, - multi_buffer: &Entity, - cx: &App, - ) -> Option { - let target = self.global_marks.get(name); - - if !self.is_global_mark(name) || target.is_some_and(|t| self.points_at(t, multi_buffer, cx)) - { - if let Some(anchors) = self.multibuffer_marks.get(&multi_buffer.entity_id()) { - return Some(Mark::Local(anchors.get(name)?.clone())); - } - - let singleton = multi_buffer.read(cx).as_singleton()?; - let excerpt_id = *multi_buffer.read(cx).excerpt_ids().first()?; - let buffer_id = singleton.read(cx).remote_id(); - if let Some(anchors) = self.buffer_marks.get(&buffer_id) { - let text_anchors = anchors.get(name)?; - let anchors = text_anchors - .iter() - .map(|anchor| Anchor::in_buffer(excerpt_id, *anchor)) - .collect(); - return Some(Mark::Local(anchors)); - } - } - - match target? { - MarkLocation::Buffer(entity_id) => { - let anchors = self.multibuffer_marks.get(entity_id)?; - Some(Mark::Buffer(*entity_id, anchors.get(name)?.clone())) - } - MarkLocation::Path(path) => { - let points = self.serialized_marks.get(path)?; - Some(Mark::Path(path.clone(), points.get(name)?.clone())) - } - } - } - pub fn delete_mark( - &mut self, - mark_name: String, - multi_buffer: &Entity, - cx: &mut Context, - ) { - let path = if let Some(target) = self.global_marks.get(&mark_name.clone()) { - let name = mark_name.clone(); - if let Some(workspace_id) = self.workspace_id(cx) { - cx.background_spawn(async move { - DB.delete_global_marks_path(workspace_id, name).await - }) - .detach_and_log_err(cx); - } - self.buffer_marks.iter_mut().for_each(|(_, m)| { - m.remove(&mark_name.clone()); - }); - - match target { - MarkLocation::Buffer(entity_id) => { - self.multibuffer_marks - .get_mut(entity_id) - .map(|m| m.remove(&mark_name.clone())); - return; - } - MarkLocation::Path(path) => path.clone(), - } - } else { - self.multibuffer_marks - .get_mut(&multi_buffer.entity_id()) - .map(|m| m.remove(&mark_name.clone())); - - if let Some(singleton) = multi_buffer.read(cx).as_singleton() { - let buffer_id = singleton.read(cx).remote_id(); - self.buffer_marks - .get_mut(&buffer_id) - .map(|m| m.remove(&mark_name.clone())); - let Some(path) = self.path_for_buffer(&singleton, cx) else { - return; - }; - path - } else { - return; - } - }; - self.global_marks.remove(&mark_name); - self.serialized_marks - .get_mut(&path) - .map(|m| m.remove(&mark_name.clone())); - if let Some(workspace_id) = self.workspace_id(cx) { - cx.background_spawn(async move { DB.delete_mark(workspace_id, path, mark_name).await }) - .detach_and_log_err(cx); - } - } -} - -impl Global for VimGlobals {} - -impl VimGlobals { - pub(crate) fn register(cx: &mut App) { - cx.set_global(VimGlobals::default()); - - cx.observe_keystrokes(|event, _, cx| { - let Some(action) = event.action.as_ref().map(|action| action.boxed_clone()) else { - return; - }; - Vim::globals(cx).observe_action(action.boxed_clone()) - }) - .detach(); - - cx.observe_new(|workspace: &mut Workspace, window, _| { - RegistersView::register(workspace, window); - }) - .detach(); - - cx.observe_new(move |workspace: &mut Workspace, window, _| { - MarksView::register(workspace, window); - }) - .detach(); - - let mut was_enabled = None; - - cx.observe_global::(move |cx| { - let is_enabled = Vim::enabled(cx); - if was_enabled == Some(is_enabled) { - return; - } - was_enabled = Some(is_enabled); - if is_enabled { - KeyBinding::set_vim_mode(cx, true); - CommandPaletteFilter::update_global(cx, |filter, _| { - filter.show_namespace(Vim::NAMESPACE); - }); - GlobalCommandPaletteInterceptor::set(cx, command_interceptor); - for window in cx.windows() { - if let Some(workspace) = window.downcast::() { - workspace - .update(cx, |workspace, _, cx| { - Vim::update_globals(cx, |globals, cx| { - globals.register_workspace(workspace, cx) - }); - }) - .ok(); - } - } - } else { - KeyBinding::set_vim_mode(cx, false); - *Vim::globals(cx) = VimGlobals::default(); - GlobalCommandPaletteInterceptor::clear(cx); - CommandPaletteFilter::update_global(cx, |filter, _| { - filter.hide_namespace(Vim::NAMESPACE); - }); - } - }) - .detach(); - cx.observe_new(|workspace: &mut Workspace, _, cx| { - Vim::update_globals(cx, |globals, cx| globals.register_workspace(workspace, cx)); - }) - .detach() - } - - fn register_workspace(&mut self, workspace: &Workspace, cx: &mut Context) { - let entity_id = cx.entity_id(); - self.marks.insert(entity_id, MarksState::new(workspace, cx)); - cx.observe_release(&cx.entity(), move |_, _, cx| { - Vim::update_globals(cx, |globals, _| { - globals.marks.remove(&entity_id); - }) - }) - .detach(); - } - - pub(crate) fn write_registers( - &mut self, - content: Register, - register: Option, - is_yank: bool, - kind: MotionKind, - cx: &mut Context, - ) { - if let Some(register) = register { - let lower = register.to_lowercase().next().unwrap_or(register); - if lower != register { - let current = self.registers.entry(lower).or_default(); - current.text = (current.text.to_string() + &content.text).into(); - // not clear how to support appending to registers with multiple cursors - current.clipboard_selections.take(); - let yanked = current.clone(); - self.registers.insert('"', yanked); - } else { - match lower { - '_' | ':' | '.' | '%' | '#' | '=' | '/' => {} - '+' => { - self.registers.insert('"', content.clone()); - cx.write_to_clipboard(content.into()); - } - '*' => { - self.registers.insert('"', content.clone()); - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - cx.write_to_primary(content.into()); - #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] - cx.write_to_clipboard(content.into()); - } - '"' => { - self.registers.insert('"', content.clone()); - self.registers.insert('0', content); - } - _ => { - self.registers.insert('"', content.clone()); - self.registers.insert(lower, content); - } - } - } - } else { - let setting = VimSettings::get_global(cx).use_system_clipboard; - if setting == UseSystemClipboard::Always - || setting == UseSystemClipboard::OnYank && is_yank - { - self.last_yank.replace(content.text.clone()); - cx.write_to_clipboard(content.clone().into()); - } else { - if let Some(text) = cx.read_from_clipboard().and_then(|i| i.text()) { - self.last_yank.replace(text.into()); - } - } - self.registers.insert('"', content.clone()); - if is_yank { - self.registers.insert('0', content); - } else { - let contains_newline = content.text.contains('\n'); - if !contains_newline { - self.registers.insert('-', content.clone()); - } - if kind.linewise() || contains_newline { - let mut content = content; - for i in '1'..='9' { - if let Some(moved) = self.registers.insert(i, content) { - content = moved; - } else { - break; - } - } - } - } - } - } - - pub(crate) fn read_register( - &self, - register: Option, - editor: Option<&mut Editor>, - cx: &mut App, - ) -> Option { - let Some(register) = register.filter(|reg| *reg != '"') else { - let setting = VimSettings::get_global(cx).use_system_clipboard; - return match setting { - UseSystemClipboard::Always => cx.read_from_clipboard().map(|item| item.into()), - UseSystemClipboard::OnYank if self.system_clipboard_is_newer(cx) => { - cx.read_from_clipboard().map(|item| item.into()) - } - _ => self.registers.get(&'"').cloned(), - }; - }; - let lower = register.to_lowercase().next().unwrap_or(register); - match lower { - '_' | ':' | '.' | '#' | '=' => None, - '+' => cx.read_from_clipboard().map(|item| item.into()), - '*' => { - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - { - cx.read_from_primary().map(|item| item.into()) - } - #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] - { - cx.read_from_clipboard().map(|item| item.into()) - } - } - '%' => editor.and_then(|editor| { - let selection = editor - .selections - .newest::(&editor.display_snapshot(cx)); - if let Some((_, buffer, _)) = editor - .buffer() - .read(cx) - .excerpt_containing(selection.head(), cx) - { - buffer - .read(cx) - .file() - .map(|file| file.path().display(file.path_style(cx)).into_owned().into()) - } else { - None - } - }), - _ => self.registers.get(&lower).cloned(), - } - } - - fn system_clipboard_is_newer(&self, cx: &App) -> bool { - cx.read_from_clipboard().is_some_and(|item| { - match (item.text().as_deref(), &self.last_yank) { - (Some(new), Some(last)) => last.as_ref() != new, - (Some(_), None) => true, - (None, _) => false, - } - }) - } - - pub fn observe_action(&mut self, action: Box) { - if self.dot_recording { - self.recording_actions - .push(ReplayableAction::Action(action.boxed_clone())); - - if self.stop_recording_after_next_action { - self.dot_recording = false; - self.recorded_actions = std::mem::take(&mut self.recording_actions); - self.recorded_count = self.recording_count.take(); - self.stop_recording_after_next_action = false; - } - } - if self.replayer.is_none() - && let Some(recording_register) = self.recording_register - { - self.recordings - .entry(recording_register) - .or_default() - .push(ReplayableAction::Action(action)); - } - } - - pub fn observe_insertion(&mut self, text: &Arc, range_to_replace: Option>) { - if self.ignore_current_insertion { - self.ignore_current_insertion = false; - return; - } - if self.dot_recording { - self.recording_actions.push(ReplayableAction::Insertion { - text: text.clone(), - utf16_range_to_replace: range_to_replace.clone(), - }); - if self.stop_recording_after_next_action { - self.dot_recording = false; - self.recorded_actions = std::mem::take(&mut self.recording_actions); - self.recorded_count = self.recording_count.take(); - self.stop_recording_after_next_action = false; - } - } - if let Some(recording_register) = self.recording_register { - self.recordings.entry(recording_register).or_default().push( - ReplayableAction::Insertion { - text: text.clone(), - utf16_range_to_replace: range_to_replace, - }, - ); - } - } - - pub fn focused_vim(&self) -> Option> { - self.focused_vim.as_ref().and_then(|vim| vim.upgrade()) - } -} - -impl Vim { - pub fn globals(cx: &mut App) -> &mut VimGlobals { - cx.global_mut::() - } - - pub fn update_globals(cx: &mut C, f: impl FnOnce(&mut VimGlobals, &mut C) -> R) -> R - where - C: BorrowMut, - { - cx.update_global(f) - } -} - -#[derive(Debug)] -pub enum ReplayableAction { - Action(Box), - Insertion { - text: Arc, - utf16_range_to_replace: Option>, - }, -} - -impl Clone for ReplayableAction { - fn clone(&self) -> Self { - match self { - Self::Action(action) => Self::Action(action.boxed_clone()), - Self::Insertion { - text, - utf16_range_to_replace, - } => Self::Insertion { - text: text.clone(), - utf16_range_to_replace: utf16_range_to_replace.clone(), - }, - } - } -} - -#[derive(Clone, Default, Debug)] -pub struct SearchState { - pub direction: Direction, - pub count: usize, - - pub prior_selections: Vec>, - pub prior_operator: Option, - pub prior_mode: Mode, - pub helix_select: bool, -} - -impl Operator { - pub fn id(&self) -> &'static str { - match self { - Operator::Object { around: false } => "i", - Operator::Object { around: true } => "a", - Operator::Change => "c", - Operator::Delete => "d", - Operator::Yank => "y", - Operator::Replace => "r", - Operator::Digraph { .. } => "^K", - Operator::Literal { .. } => "^V", - Operator::FindForward { before: false, .. } => "f", - Operator::FindForward { before: true, .. } => "t", - Operator::Sneak { .. } => "s", - Operator::SneakBackward { .. } => "S", - Operator::FindBackward { after: false, .. } => "F", - Operator::FindBackward { after: true, .. } => "T", - Operator::AddSurrounds { .. } => "ys", - Operator::ChangeSurrounds { .. } => "cs", - Operator::DeleteSurrounds => "ds", - Operator::Mark => "m", - Operator::Jump { line: true } => "'", - Operator::Jump { line: false } => "`", - Operator::Indent => ">", - Operator::AutoIndent => "eq", - Operator::ShellCommand => "sh", - Operator::Rewrap => "gq", - Operator::ReplaceWithRegister => "gR", - Operator::Exchange => "cx", - Operator::Outdent => "<", - Operator::Uppercase => "gU", - Operator::Lowercase => "gu", - Operator::OppositeCase => "g~", - Operator::Rot13 => "g?", - Operator::Rot47 => "g?", - Operator::Register => "\"", - Operator::RecordRegister => "q", - Operator::ReplayRegister => "@", - Operator::ToggleComments => "gc", - Operator::HelixMatch => "helix_m", - Operator::HelixNext { .. } => "helix_next", - Operator::HelixPrevious { .. } => "helix_previous", - } - } - - pub fn status(&self) -> String { - fn make_visible(c: &str) -> &str { - match c { - "\n" => "enter", - "\t" => "tab", - " " => "space", - c => c, - } - } - match self { - Operator::Digraph { - first_char: Some(first_char), - } => format!("^K{}", make_visible(&first_char.to_string())), - Operator::Literal { - prefix: Some(prefix), - } => format!("^V{}", make_visible(prefix)), - Operator::AutoIndent => "=".to_string(), - Operator::ShellCommand => "=".to_string(), - Operator::HelixMatch => "m".to_string(), - Operator::HelixNext { .. } => "]".to_string(), - Operator::HelixPrevious { .. } => "[".to_string(), - _ => self.id().to_string(), - } - } - - pub fn is_waiting(&self, mode: Mode) -> bool { - match self { - Operator::AddSurrounds { target } => target.is_some() || mode.is_visual(), - Operator::FindForward { .. } - | Operator::Mark - | Operator::Jump { .. } - | Operator::FindBackward { .. } - | Operator::Sneak { .. } - | Operator::SneakBackward { .. } - | Operator::Register - | Operator::RecordRegister - | Operator::ReplayRegister - | Operator::Replace - | Operator::Digraph { .. } - | Operator::Literal { .. } - | Operator::ChangeSurrounds { - target: Some(_), .. - } - | Operator::DeleteSurrounds => true, - Operator::Change - | Operator::Delete - | Operator::Yank - | Operator::Rewrap - | Operator::Indent - | Operator::Outdent - | Operator::AutoIndent - | Operator::ShellCommand - | Operator::Lowercase - | Operator::Uppercase - | Operator::Rot13 - | Operator::Rot47 - | Operator::ReplaceWithRegister - | Operator::Exchange - | Operator::Object { .. } - | Operator::ChangeSurrounds { target: None, .. } - | Operator::OppositeCase - | Operator::ToggleComments - | Operator::HelixMatch - | Operator::HelixNext { .. } - | Operator::HelixPrevious { .. } => false, - } - } - - pub fn starts_dot_recording(&self) -> bool { - match self { - Operator::Change - | Operator::Delete - | Operator::Replace - | Operator::Indent - | Operator::Outdent - | Operator::AutoIndent - | Operator::Lowercase - | Operator::Uppercase - | Operator::OppositeCase - | Operator::Rot13 - | Operator::Rot47 - | Operator::ToggleComments - | Operator::ReplaceWithRegister - | Operator::Rewrap - | Operator::ShellCommand - | Operator::AddSurrounds { target: None } - | Operator::ChangeSurrounds { target: None, .. } - | Operator::DeleteSurrounds - | Operator::Exchange - | Operator::HelixNext { .. } - | Operator::HelixPrevious { .. } => true, - Operator::Yank - | Operator::Object { .. } - | Operator::FindForward { .. } - | Operator::FindBackward { .. } - | Operator::Sneak { .. } - | Operator::SneakBackward { .. } - | Operator::Mark - | Operator::Digraph { .. } - | Operator::Literal { .. } - | Operator::AddSurrounds { .. } - | Operator::ChangeSurrounds { .. } - | Operator::Jump { .. } - | Operator::Register - | Operator::RecordRegister - | Operator::ReplayRegister - | Operator::HelixMatch => false, - } - } -} - -struct RegisterMatch { - name: char, - contents: SharedString, -} - -pub struct RegistersViewDelegate { - selected_index: usize, - matches: Vec, -} - -impl PickerDelegate for RegistersViewDelegate { - type ListItem = Div; - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context>) { - self.selected_index = ix; - cx.notify(); - } - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - Arc::default() - } - - fn update_matches( - &mut self, - _: String, - _: &mut Window, - _: &mut Context>, - ) -> gpui::Task<()> { - Task::ready(()) - } - - fn confirm(&mut self, _: bool, _: &mut Window, _: &mut Context>) {} - - fn dismissed(&mut self, _: &mut Window, _: &mut Context>) {} - - fn render_match( - &self, - ix: usize, - selected: bool, - _: &mut Window, - cx: &mut Context>, - ) -> Option { - let register_match = self.matches.get(ix)?; - - let mut output = String::new(); - let mut runs = Vec::new(); - output.push('"'); - output.push(register_match.name); - runs.push(( - 0..output.len(), - HighlightStyle::color(cx.theme().colors().text_accent), - )); - output.push(' '); - output.push(' '); - let mut base = output.len(); - for (ix, c) in register_match.contents.char_indices() { - if ix > 100 { - break; - } - let replace = match c { - '\t' => Some("\\t".to_string()), - '\n' => Some("\\n".to_string()), - '\r' => Some("\\r".to_string()), - c if is_invisible(c) => { - if c <= '\x1f' { - replacement(c).map(|s| s.to_string()) - } else { - Some(format!("\\u{:04X}", c as u32)) - } - } - _ => None, - }; - let Some(replace) = replace else { - output.push(c); - continue; - }; - output.push_str(&replace); - runs.push(( - base + ix..base + ix + replace.len(), - HighlightStyle::color(cx.theme().colors().text_muted), - )); - base += replace.len() - c.len_utf8(); - } - - let theme = ThemeSettings::get_global(cx); - let text_style = TextStyle { - color: cx.theme().colors().editor_foreground, - font_family: theme.buffer_font.family.clone(), - font_features: theme.buffer_font.features.clone(), - font_fallbacks: theme.buffer_font.fallbacks.clone(), - font_size: theme.buffer_font_size(cx).into(), - line_height: (theme.line_height() * theme.buffer_font_size(cx)).into(), - font_weight: theme.buffer_font.weight, - font_style: theme.buffer_font.style, - ..Default::default() - }; - - Some( - h_flex() - .when(selected, |el| el.bg(cx.theme().colors().element_selected)) - .font_buffer(cx) - .text_buffer(cx) - .h(theme.buffer_font_size(cx) * theme.line_height()) - .px_2() - .gap_1() - .child(StyledText::new(output).with_default_highlights(&text_style, runs)), - ) - } -} - -pub struct RegistersView {} - -impl RegistersView { - fn register(workspace: &mut Workspace, _window: Option<&mut Window>) { - workspace.register_action(|workspace, _: &ToggleRegistersView, window, cx| { - Self::toggle(workspace, window, cx); - }); - } - - pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context) { - let editor = workspace - .active_item(cx) - .and_then(|item| item.act_as::(cx)); - workspace.toggle_modal(window, cx, move |window, cx| { - RegistersView::new(editor, window, cx) - }); - } - - fn new( - editor: Option>, - window: &mut Window, - cx: &mut Context>, - ) -> Picker { - let mut matches = Vec::default(); - cx.update_global(|globals: &mut VimGlobals, cx| { - for name in ['"', '+', '*'] { - if let Some(register) = globals.read_register(Some(name), None, cx) { - matches.push(RegisterMatch { - name, - contents: register.text.clone(), - }) - } - } - if let Some(editor) = editor { - let register = editor.update(cx, |editor, cx| { - globals.read_register(Some('%'), Some(editor), cx) - }); - if let Some(register) = register { - matches.push(RegisterMatch { - name: '%', - contents: register.text, - }) - } - } - for (name, register) in globals.registers.iter() { - if ['"', '+', '*', '%'].contains(name) { - continue; - }; - matches.push(RegisterMatch { - name: *name, - contents: register.text.clone(), - }) - } - }); - matches.sort_by(|a, b| a.name.cmp(&b.name)); - let delegate = RegistersViewDelegate { - selected_index: 0, - matches, - }; - - Picker::nonsearchable_uniform_list(delegate, window, cx) - .width(rems(36.)) - .modal(true) - } -} - -enum MarksMatchInfo { - Path(Arc), - Title(String), - Content { - line: String, - highlights: Vec<(Range, HighlightStyle)>, - }, -} - -impl MarksMatchInfo { - fn from_chunks<'a>(chunks: impl Iterator>, cx: &App) -> Self { - let mut line = String::new(); - let mut highlights = Vec::new(); - let mut offset = 0; - for chunk in chunks { - line.push_str(chunk.text); - if let Some(highlight_style) = chunk.syntax_highlight_id - && let Some(highlight) = highlight_style.style(cx.theme().syntax()) - { - highlights.push((offset..offset + chunk.text.len(), highlight)) - } - offset += chunk.text.len(); - } - MarksMatchInfo::Content { line, highlights } - } -} - -struct MarksMatch { - name: String, - position: Point, - info: MarksMatchInfo, -} - -pub struct MarksViewDelegate { - selected_index: usize, - matches: Vec, - point_column_width: usize, - workspace: WeakEntity, -} - -impl PickerDelegate for MarksViewDelegate { - type ListItem = Div; - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context>) { - self.selected_index = ix; - cx.notify(); - } - - fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc { - Arc::default() - } - - fn update_matches( - &mut self, - _: String, - _: &mut Window, - cx: &mut Context>, - ) -> gpui::Task<()> { - let Some(workspace) = self.workspace.upgrade() else { - return Task::ready(()); - }; - cx.spawn(async move |picker, cx| { - let mut matches = Vec::new(); - let _ = workspace.update(cx, |workspace, cx| { - let entity_id = cx.entity_id(); - let Some(editor) = workspace - .active_item(cx) - .and_then(|item| item.act_as::(cx)) - else { - return; - }; - let editor = editor.read(cx); - let mut has_seen = HashSet::new(); - let Some(marks_state) = cx.global::().marks.get(&entity_id) else { - return; - }; - let marks_state = marks_state.read(cx); - - if let Some(map) = marks_state - .multibuffer_marks - .get(&editor.buffer().entity_id()) - { - for (name, anchors) in map { - if has_seen.contains(name) { - continue; - } - has_seen.insert(name.clone()); - let Some(anchor) = anchors.first() else { - continue; - }; - - let snapshot = editor.buffer().read(cx).snapshot(cx); - let position = anchor.to_point(&snapshot); - - let chunks = snapshot.chunks( - Point::new(position.row, 0) - ..Point::new( - position.row, - snapshot.line_len(MultiBufferRow(position.row)), - ), - true, - ); - matches.push(MarksMatch { - name: name.clone(), - position, - info: MarksMatchInfo::from_chunks(chunks, cx), - }) - } - } - - if let Some(buffer) = editor.buffer().read(cx).as_singleton() { - let buffer = buffer.read(cx); - if let Some(map) = marks_state.buffer_marks.get(&buffer.remote_id()) { - for (name, anchors) in map { - if has_seen.contains(name) { - continue; - } - has_seen.insert(name.clone()); - let Some(anchor) = anchors.first() else { - continue; - }; - let snapshot = buffer.snapshot(); - let position = anchor.to_point(&snapshot); - let chunks = snapshot.chunks( - Point::new(position.row, 0) - ..Point::new(position.row, snapshot.line_len(position.row)), - true, - ); - - matches.push(MarksMatch { - name: name.clone(), - position, - info: MarksMatchInfo::from_chunks(chunks, cx), - }) - } - } - } - - for (name, mark_location) in marks_state.global_marks.iter() { - if has_seen.contains(name) { - continue; - } - has_seen.insert(name.clone()); - - match mark_location { - MarkLocation::Buffer(entity_id) => { - if let Some(&anchor) = marks_state - .multibuffer_marks - .get(entity_id) - .and_then(|map| map.get(name)) - .and_then(|anchors| anchors.first()) - { - let Some((info, snapshot)) = workspace - .items(cx) - .filter_map(|item| item.act_as::(cx)) - .map(|entity| entity.read(cx).buffer()) - .find(|buffer| buffer.entity_id().eq(entity_id)) - .map(|buffer| { - ( - MarksMatchInfo::Title( - buffer.read(cx).title(cx).to_string(), - ), - buffer.read(cx).snapshot(cx), - ) - }) - else { - continue; - }; - matches.push(MarksMatch { - name: name.clone(), - position: anchor.to_point(&snapshot), - info, - }); - } - } - MarkLocation::Path(path) => { - if let Some(&position) = marks_state - .serialized_marks - .get(path.as_ref()) - .and_then(|map| map.get(name)) - .and_then(|points| points.first()) - { - let info = MarksMatchInfo::Path(path.clone()); - matches.push(MarksMatch { - name: name.clone(), - position, - info, - }); - } - } - } - } - }); - let _ = picker.update(cx, |picker, cx| { - matches.sort_by_key(|a| { - ( - a.name.chars().next().map(|c| c.is_ascii_uppercase()), - a.name.clone(), - ) - }); - let digits = matches - .iter() - .map(|m| (m.position.row + 1).ilog10() + (m.position.column + 1).ilog10()) - .max() - .unwrap_or_default(); - picker.delegate.matches = matches; - picker.delegate.point_column_width = (digits + 4) as usize; - cx.notify(); - }); - }) - } - - fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context>) { - let Some(vim) = self - .workspace - .upgrade() - .map(|w| w.read(cx)) - .and_then(|w| w.focused_pane(window, cx).read(cx).active_item()) - .and_then(|item| item.act_as::(cx)) - .and_then(|editor| editor.read(cx).addon::().cloned()) - .map(|addon| addon.entity) - else { - return; - }; - let Some(text): Option> = self - .matches - .get(self.selected_index) - .map(|m| Arc::from(m.name.to_string().into_boxed_str())) - else { - return; - }; - vim.update(cx, |vim, cx| { - vim.jump(text, false, false, window, cx); - }); - - cx.emit(DismissEvent); - } - - fn dismissed(&mut self, _: &mut Window, _: &mut Context>) {} - - fn render_match( - &self, - ix: usize, - selected: bool, - _: &mut Window, - cx: &mut Context>, - ) -> Option { - let mark_match = self.matches.get(ix)?; - - let mut left_output = String::new(); - let mut left_runs = Vec::new(); - left_output.push('`'); - left_output.push_str(&mark_match.name); - left_runs.push(( - 0..left_output.len(), - HighlightStyle::color(cx.theme().colors().text_accent), - )); - left_output.push(' '); - left_output.push(' '); - let point_column = format!( - "{},{}", - mark_match.position.row + 1, - mark_match.position.column + 1 - ); - left_output.push_str(&point_column); - if let Some(padding) = self.point_column_width.checked_sub(point_column.len()) { - left_output.push_str(&" ".repeat(padding)); - } - - let (right_output, right_runs): (String, Vec<_>) = match &mark_match.info { - MarksMatchInfo::Path(path) => { - let s = path.to_string_lossy().into_owned(); - ( - s.clone(), - vec![(0..s.len(), HighlightStyle::color(cx.theme().colors().text))], - ) - } - MarksMatchInfo::Title(title) => ( - title.clone(), - vec![( - 0..title.len(), - HighlightStyle::color(cx.theme().colors().text), - )], - ), - MarksMatchInfo::Content { line, highlights } => (line.clone(), highlights.clone()), - }; - - let theme = ThemeSettings::get_global(cx); - let text_style = TextStyle { - color: cx.theme().colors().editor_foreground, - font_family: theme.buffer_font.family.clone(), - font_features: theme.buffer_font.features.clone(), - font_fallbacks: theme.buffer_font.fallbacks.clone(), - font_size: theme.buffer_font_size(cx).into(), - line_height: (theme.line_height() * theme.buffer_font_size(cx)).into(), - font_weight: theme.buffer_font.weight, - font_style: theme.buffer_font.style, - ..Default::default() - }; - - Some( - h_flex() - .when(selected, |el| el.bg(cx.theme().colors().element_selected)) - .font_buffer(cx) - .text_buffer(cx) - .h(theme.buffer_font_size(cx) * theme.line_height()) - .px_2() - .child(StyledText::new(left_output).with_default_highlights(&text_style, left_runs)) - .child( - StyledText::new(right_output).with_default_highlights(&text_style, right_runs), - ), - ) - } -} - -pub struct MarksView {} - -impl MarksView { - fn register(workspace: &mut Workspace, _window: Option<&mut Window>) { - workspace.register_action(|workspace, _: &ToggleMarksView, window, cx| { - Self::toggle(workspace, window, cx); - }); - } - - pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context) { - let handle = cx.weak_entity(); - workspace.toggle_modal(window, cx, move |window, cx| { - MarksView::new(handle, window, cx) - }); - } - - fn new( - workspace: WeakEntity, - window: &mut Window, - cx: &mut Context>, - ) -> Picker { - let matches = Vec::default(); - let delegate = MarksViewDelegate { - selected_index: 0, - point_column_width: 0, - matches, - workspace, - }; - Picker::nonsearchable_uniform_list(delegate, window, cx) - .width(rems(36.)) - .modal(true) - } -} - -pub struct VimDb(ThreadSafeConnection); - -impl Domain for VimDb { - const NAME: &str = stringify!(VimDb); - - const MIGRATIONS: &[&str] = &[ - sql! ( - CREATE TABLE vim_marks ( - workspace_id INTEGER, - mark_name TEXT, - path BLOB, - value TEXT - ); - CREATE UNIQUE INDEX idx_vim_marks ON vim_marks (workspace_id, mark_name, path); - ), - sql! ( - CREATE TABLE vim_global_marks_paths( - workspace_id INTEGER, - mark_name TEXT, - path BLOB - ); - CREATE UNIQUE INDEX idx_vim_global_marks_paths - ON vim_global_marks_paths(workspace_id, mark_name); - ), - ]; -} - -db::static_connection!(DB, VimDb, [WorkspaceDb]); - -struct SerializedMark { - path: Arc, - name: String, - points: Vec, -} - -impl VimDb { - pub(crate) async fn set_marks( - &self, - workspace_id: WorkspaceId, - path: Arc, - marks: HashMap>, - ) -> Result<()> { - log::debug!("Setting path {path:?} for {} marks", marks.len()); - - self.write(move |conn| { - let mut query = conn.exec_bound(sql!( - INSERT OR REPLACE INTO vim_marks - (workspace_id, mark_name, path, value) - VALUES - (?, ?, ?, ?) - ))?; - for (mark_name, value) in marks { - let pairs: Vec<(u32, u32)> = value - .into_iter() - .map(|point| (point.row, point.column)) - .collect(); - let serialized = serde_json::to_string(&pairs)?; - query((workspace_id, mark_name, path.clone(), serialized))?; - } - Ok(()) - }) - .await - } - - fn get_marks(&self, workspace_id: WorkspaceId) -> Result> { - let result: Vec<(Arc, String, String)> = self.select_bound(sql!( - SELECT path, mark_name, value FROM vim_marks - WHERE workspace_id = ? - ))?(workspace_id)?; - - Ok(result - .into_iter() - .filter_map(|(path, name, value)| { - let pairs: Vec<(u32, u32)> = serde_json::from_str(&value).log_err()?; - Some(SerializedMark { - path, - name, - points: pairs - .into_iter() - .map(|(row, column)| Point { row, column }) - .collect(), - }) - }) - .collect()) - } - - pub(crate) async fn delete_mark( - &self, - workspace_id: WorkspaceId, - path: Arc, - mark_name: String, - ) -> Result<()> { - self.write(move |conn| { - conn.exec_bound(sql!( - DELETE FROM vim_marks - WHERE workspace_id = ? AND mark_name = ? AND path = ? - ))?((workspace_id, mark_name, path)) - }) - .await - } - - pub(crate) async fn set_global_mark_path( - &self, - workspace_id: WorkspaceId, - mark_name: String, - path: Arc, - ) -> Result<()> { - log::debug!("Setting global mark path {path:?} for {mark_name}"); - self.write(move |conn| { - conn.exec_bound(sql!( - INSERT OR REPLACE INTO vim_global_marks_paths - (workspace_id, mark_name, path) - VALUES - (?, ?, ?) - ))?((workspace_id, mark_name, path)) - }) - .await - } - - pub fn get_global_marks_paths( - &self, - workspace_id: WorkspaceId, - ) -> Result)>> { - self.select_bound(sql!( - SELECT mark_name, path FROM vim_global_marks_paths - WHERE workspace_id = ? - ))?(workspace_id) - } - - pub(crate) async fn delete_global_marks_path( - &self, - workspace_id: WorkspaceId, - mark_name: String, - ) -> Result<()> { - self.write(move |conn| { - conn.exec_bound(sql!( - DELETE FROM vim_global_marks_paths - WHERE workspace_id = ? AND mark_name = ? - ))?((workspace_id, mark_name)) - }) - .await - } -} diff --git a/crates/vim/src/surrounds.rs b/crates/vim/src/surrounds.rs deleted file mode 100644 index b3f9307aac..0000000000 --- a/crates/vim/src/surrounds.rs +++ /dev/null @@ -1,1705 +0,0 @@ -use crate::{ - Vim, - motion::{self, Motion}, - object::{Object, surrounding_markers}, - state::Mode, -}; -use editor::{Bias, MultiBufferOffset, movement}; -use gpui::{Context, Window}; -use language::BracketPair; - -use std::sync::Arc; - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum SurroundsType { - Motion(Motion), - Object(Object, bool), - Selection, -} - -impl Vim { - pub fn add_surrounds( - &mut self, - text: Arc, - target: SurroundsType, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - let count = Vim::take_count(cx); - let forced_motion = Vim::take_forced_motion(cx); - let mode = self.mode; - self.update_editor(cx, |_, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - editor.transact(window, cx, |editor, window, cx| { - editor.set_clip_at_line_ends(false, cx); - - let pair = match find_surround_pair(&all_support_surround_pair(), &text) { - Some(pair) => pair.clone(), - None => BracketPair { - start: text.to_string(), - end: text.to_string(), - close: true, - surround: true, - newline: false, - }, - }; - let surround = pair.end != surround_alias((*text).as_ref()); - let display_map = editor.display_snapshot(cx); - let display_selections = editor.selections.all_adjusted_display(&display_map); - let mut edits = Vec::new(); - let mut anchors = Vec::new(); - - for selection in &display_selections { - let range = match &target { - SurroundsType::Object(object, around) => { - object.range(&display_map, selection.clone(), *around, None) - } - SurroundsType::Motion(motion) => { - motion - .range( - &display_map, - selection.clone(), - count, - &text_layout_details, - forced_motion, - ) - .map(|(mut range, _)| { - // The Motion::CurrentLine operation will contain the newline of the current line and leading/trailing whitespace - if let Motion::CurrentLine = motion { - range.start = motion::first_non_whitespace( - &display_map, - false, - range.start, - ); - range.end = movement::saturating_right( - &display_map, - motion::last_non_whitespace(&display_map, range.end, 1), - ); - } - range - }) - } - SurroundsType::Selection => Some(selection.range()), - }; - - if let Some(range) = range { - let start = range.start.to_offset(&display_map, Bias::Right); - let end = range.end.to_offset(&display_map, Bias::Left); - let (start_cursor_str, end_cursor_str) = if mode == Mode::VisualLine { - (format!("{}\n", pair.start), format!("\n{}", pair.end)) - } else { - let maybe_space = if surround { " " } else { "" }; - ( - format!("{}{}", pair.start, maybe_space), - format!("{}{}", maybe_space, pair.end), - ) - }; - let start_anchor = display_map.buffer_snapshot().anchor_before(start); - - edits.push((start..start, start_cursor_str)); - edits.push((end..end, end_cursor_str)); - anchors.push(start_anchor..start_anchor); - } else { - let start_anchor = display_map - .buffer_snapshot() - .anchor_before(selection.head().to_offset(&display_map, Bias::Left)); - anchors.push(start_anchor..start_anchor); - } - } - - editor.edit(edits, cx); - editor.set_clip_at_line_ends(true, cx); - editor.change_selections(Default::default(), window, cx, |s| { - if mode == Mode::VisualBlock { - s.select_anchor_ranges(anchors.into_iter().take(1)) - } else { - s.select_anchor_ranges(anchors) - } - }); - }); - }); - self.switch_mode(Mode::Normal, false, window, cx); - } - - pub fn delete_surrounds( - &mut self, - text: Arc, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - - // only legitimate surrounds can be removed - let pair = match find_surround_pair(&all_support_surround_pair(), &text) { - Some(pair) => pair.clone(), - None => return, - }; - let pair_object = match pair_to_object(&pair) { - Some(pair_object) => pair_object, - None => return, - }; - let surround = pair.end != *text; - - self.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - editor.set_clip_at_line_ends(false, cx); - - let display_map = editor.display_snapshot(cx); - let display_selections = editor.selections.all_display(&display_map); - let mut edits = Vec::new(); - let mut anchors = Vec::new(); - - for selection in &display_selections { - let start = selection.start.to_offset(&display_map, Bias::Left); - if let Some(range) = - pair_object.range(&display_map, selection.clone(), true, None) - { - // If the current parenthesis object is single-line, - // then we need to filter whether it is the current line or not - if !pair_object.is_multiline() { - let is_same_row = selection.start.row() == range.start.row() - && selection.end.row() == range.end.row(); - if !is_same_row { - anchors.push(start..start); - continue; - } - } - // This is a bit cumbersome, and it is written to deal with some special cases, as shown below - // hello«ˇ "hello in a word" »again. - // Sometimes the expand_selection will not be matched at both ends, and there will be extra spaces - // In order to be able to accurately match and replace in this case, some cumbersome methods are used - let mut chars_and_offset = display_map - .buffer_chars_at(range.start.to_offset(&display_map, Bias::Left)) - .peekable(); - while let Some((ch, offset)) = chars_and_offset.next() { - if ch.to_string() == pair.start { - let start = offset; - let mut end = start + 1usize; - if surround - && let Some((next_ch, _)) = chars_and_offset.peek() - && next_ch.eq(&' ') - { - end += 1; - } - edits.push((start..end, "")); - anchors.push(start..start); - break; - } - } - let mut reverse_chars_and_offsets = display_map - .reverse_buffer_chars_at(range.end.to_offset(&display_map, Bias::Left)) - .peekable(); - while let Some((ch, offset)) = reverse_chars_and_offsets.next() { - if ch.to_string() == pair.end { - let mut start = offset; - let end = start + 1usize; - if surround - && let Some((next_ch, _)) = reverse_chars_and_offsets.peek() - && next_ch.eq(&' ') - { - start -= 1; - } - edits.push((start..end, "")); - break; - } - } - } else { - anchors.push(start..start); - } - } - - editor.change_selections(Default::default(), window, cx, |s| { - s.select_ranges(anchors); - }); - edits.sort_by_key(|(range, _)| range.start); - editor.edit(edits, cx); - editor.set_clip_at_line_ends(true, cx); - }); - }); - } - - pub fn change_surrounds( - &mut self, - text: Arc, - target: Object, - opening: bool, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(will_replace_pair) = self.object_to_bracket_pair(target, cx) { - self.stop_recording(cx); - self.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - editor.set_clip_at_line_ends(false, cx); - - let pair = match find_surround_pair(&all_support_surround_pair(), &text) { - Some(pair) => pair.clone(), - None => BracketPair { - start: text.to_string(), - end: text.to_string(), - close: true, - surround: true, - newline: false, - }, - }; - - // A single space should be added if the new surround is a - // bracket and not a quote (pair.start != pair.end) and if - // the bracket used is the opening bracket. - let add_space = - !(pair.start == pair.end) && (pair.end != surround_alias((*text).as_ref())); - - // Space should be preserved if either the surrounding - // characters being updated are quotes - // (will_replace_pair.start == will_replace_pair.end) or if - // the bracket used in the command is not an opening - // bracket. - let preserve_space = - will_replace_pair.start == will_replace_pair.end || !opening; - - let display_map = editor.display_snapshot(cx); - let selections = editor.selections.all_adjusted_display(&display_map); - let mut edits = Vec::new(); - let mut anchors = Vec::new(); - - for selection in &selections { - let start = selection.start.to_offset(&display_map, Bias::Left); - if let Some(range) = - target.range(&display_map, selection.clone(), true, None) - { - if !target.is_multiline() { - let is_same_row = selection.start.row() == range.start.row() - && selection.end.row() == range.end.row(); - if !is_same_row { - anchors.push(start..start); - continue; - } - } - - // Keeps track of the length of the string that is - // going to be edited on the start so we can ensure - // that the end replacement string does not exceed - // this value. Helpful when dealing with newlines. - let mut edit_len = 0; - let mut open_range_end = MultiBufferOffset(0); - let mut chars_and_offset = display_map - .buffer_chars_at(range.start.to_offset(&display_map, Bias::Left)) - .peekable(); - - while let Some((ch, offset)) = chars_and_offset.next() { - if ch.to_string() == will_replace_pair.start { - let mut open_str = pair.start.clone(); - let start = offset; - open_range_end = start + 1usize; - while let Some((next_ch, _)) = chars_and_offset.next() - && next_ch == ' ' - { - open_range_end += 1; - - if preserve_space { - open_str.push(next_ch); - } - } - - if add_space { - open_str.push(' '); - }; - - edit_len = open_range_end - start; - edits.push((start..open_range_end, open_str)); - anchors.push(start..start); - break; - } - } - - let mut reverse_chars_and_offsets = display_map - .reverse_buffer_chars_at( - range.end.to_offset(&display_map, Bias::Left), - ) - .peekable(); - while let Some((ch, offset)) = reverse_chars_and_offsets.next() { - if ch.to_string() == will_replace_pair.end { - let mut close_str = String::new(); - let mut start = offset; - let end = start + 1usize; - while let Some((next_ch, _)) = reverse_chars_and_offsets.next() - && next_ch == ' ' - && close_str.len() < edit_len - 1 - && start > open_range_end - { - start -= 1; - - if preserve_space { - close_str.push(next_ch); - } - } - - if add_space { - close_str.push(' '); - }; - - close_str.push_str(&pair.end); - edits.push((start..end, close_str)); - break; - } - } - } else { - anchors.push(start..start); - } - } - - let stable_anchors = editor - .selections - .disjoint_anchors_arc() - .iter() - .map(|selection| { - let start = selection.start.bias_left(&display_map.buffer_snapshot()); - start..start - }) - .collect::>(); - edits.sort_by_key(|(range, _)| range.start); - editor.edit(edits, cx); - editor.set_clip_at_line_ends(true, cx); - editor.change_selections(Default::default(), window, cx, |s| { - s.select_anchor_ranges(stable_anchors); - }); - }); - }); - } - } - - /// Checks if any of the current cursors are surrounded by a valid pair of brackets. - /// - /// This method supports multiple cursors and checks each cursor for a valid pair of brackets. - /// A pair of brackets is considered valid if it is well-formed and properly closed. - /// - /// If a valid pair of brackets is found, the method returns `true` and the cursor is automatically moved to the start of the bracket pair. - /// If no valid pair of brackets is found for any cursor, the method returns `false`. - pub fn check_and_move_to_valid_bracket_pair( - &mut self, - object: Object, - window: &mut Window, - cx: &mut Context, - ) -> bool { - let mut valid = false; - if let Some(pair) = self.object_to_bracket_pair(object, cx) { - self.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - editor.set_clip_at_line_ends(false, cx); - let display_map = editor.display_snapshot(cx); - let selections = editor.selections.all_adjusted_display(&display_map); - let mut anchors = Vec::new(); - - for selection in &selections { - let start = selection.start.to_offset(&display_map, Bias::Left); - if let Some(range) = - object.range(&display_map, selection.clone(), true, None) - { - // If the current parenthesis object is single-line, - // then we need to filter whether it is the current line or not - if object.is_multiline() - || (!object.is_multiline() - && selection.start.row() == range.start.row() - && selection.end.row() == range.end.row()) - { - valid = true; - let chars_and_offset = display_map - .buffer_chars_at( - range.start.to_offset(&display_map, Bias::Left), - ) - .peekable(); - for (ch, offset) in chars_and_offset { - if ch.to_string() == pair.start { - anchors.push(offset..offset); - break; - } - } - } else { - anchors.push(start..start) - } - } else { - anchors.push(start..start) - } - } - editor.change_selections(Default::default(), window, cx, |s| { - s.select_ranges(anchors); - }); - editor.set_clip_at_line_ends(true, cx); - }); - }); - } - valid - } - - fn object_to_bracket_pair( - &self, - object: Object, - cx: &mut Context, - ) -> Option { - match object { - Object::Quotes => Some(BracketPair { - start: "'".to_string(), - end: "'".to_string(), - close: true, - surround: true, - newline: false, - }), - Object::BackQuotes => Some(BracketPair { - start: "`".to_string(), - end: "`".to_string(), - close: true, - surround: true, - newline: false, - }), - Object::DoubleQuotes => Some(BracketPair { - start: "\"".to_string(), - end: "\"".to_string(), - close: true, - surround: true, - newline: false, - }), - Object::VerticalBars => Some(BracketPair { - start: "|".to_string(), - end: "|".to_string(), - close: true, - surround: true, - newline: false, - }), - Object::Parentheses => Some(BracketPair { - start: "(".to_string(), - end: ")".to_string(), - close: true, - surround: true, - newline: false, - }), - Object::SquareBrackets => Some(BracketPair { - start: "[".to_string(), - end: "]".to_string(), - close: true, - surround: true, - newline: false, - }), - Object::CurlyBrackets { .. } => Some(BracketPair { - start: "{".to_string(), - end: "}".to_string(), - close: true, - surround: true, - newline: false, - }), - Object::AngleBrackets => Some(BracketPair { - start: "<".to_string(), - end: ">".to_string(), - close: true, - surround: true, - newline: false, - }), - Object::AnyBrackets => { - // If we're dealing with `AnyBrackets`, which can map to multiple - // bracket pairs, we'll need to first determine which `BracketPair` to - // target. - // As such, we keep track of the smallest range size, so - // that in cases like `({ name: "John" })` if the cursor is - // inside the curly brackets, we target the curly brackets - // instead of the parentheses. - let mut bracket_pair = None; - let mut min_range_size = usize::MAX; - - let _ = self.editor.update(cx, |editor, cx| { - let display_map = editor.display_snapshot(cx); - let selections = editor.selections.all_adjusted_display(&display_map); - // Even if there's multiple cursors, we'll simply rely on - // the first one to understand what bracket pair to map to. - // I believe we could, if worth it, go one step above and - // have a `BracketPair` per selection, so that `AnyBracket` - // could work in situations where the transformation below - // could be done. - // - // ``` - // (< name:ˇ'Zed' >) - // <[ name:ˇ'DeltaDB' ]> - // ``` - // - // After using `csb{`: - // - // ``` - // (ˇ{ name:'Zed' }) - // <ˇ{ name:'DeltaDB' }> - // ``` - if let Some(selection) = selections.first() { - let relative_to = selection.head(); - let bracket_pairs = [('(', ')'), ('[', ']'), ('{', '}'), ('<', '>')]; - let cursor_offset = relative_to.to_offset(&display_map, Bias::Left); - - for &(open, close) in bracket_pairs.iter() { - if let Some(range) = surrounding_markers( - &display_map, - relative_to, - true, - false, - open, - close, - ) { - let start_offset = range.start.to_offset(&display_map, Bias::Left); - let end_offset = range.end.to_offset(&display_map, Bias::Right); - - if cursor_offset >= start_offset && cursor_offset <= end_offset { - let size = end_offset - start_offset; - if size < min_range_size { - min_range_size = size; - bracket_pair = Some(BracketPair { - start: open.to_string(), - end: close.to_string(), - close: true, - surround: true, - newline: false, - }) - } - } - } - } - } - }); - - bracket_pair - } - _ => None, - } - } -} - -fn find_surround_pair<'a>(pairs: &'a [BracketPair], ch: &str) -> Option<&'a BracketPair> { - pairs - .iter() - .find(|pair| pair.start == surround_alias(ch) || pair.end == surround_alias(ch)) -} - -fn surround_alias(ch: &str) -> &str { - match ch { - "b" => ")", - "B" => "}", - "a" => ">", - "r" => "]", - _ => ch, - } -} - -fn all_support_surround_pair() -> Vec { - vec![ - BracketPair { - start: "{".into(), - end: "}".into(), - close: true, - surround: true, - newline: false, - }, - BracketPair { - start: "'".into(), - end: "'".into(), - close: true, - surround: true, - newline: false, - }, - BracketPair { - start: "`".into(), - end: "`".into(), - close: true, - surround: true, - newline: false, - }, - BracketPair { - start: "\"".into(), - end: "\"".into(), - close: true, - surround: true, - newline: false, - }, - BracketPair { - start: "(".into(), - end: ")".into(), - close: true, - surround: true, - newline: false, - }, - BracketPair { - start: "|".into(), - end: "|".into(), - close: true, - surround: true, - newline: false, - }, - BracketPair { - start: "[".into(), - end: "]".into(), - close: true, - surround: true, - newline: false, - }, - BracketPair { - start: "<".into(), - end: ">".into(), - close: true, - surround: true, - newline: false, - }, - ] -} - -fn pair_to_object(pair: &BracketPair) -> Option { - match pair.start.as_str() { - "'" => Some(Object::Quotes), - "`" => Some(Object::BackQuotes), - "\"" => Some(Object::DoubleQuotes), - "|" => Some(Object::VerticalBars), - "(" => Some(Object::Parentheses), - "[" => Some(Object::SquareBrackets), - "{" => Some(Object::CurlyBrackets), - "<" => Some(Object::AngleBrackets), - _ => None, - } -} - -#[cfg(test)] -mod test { - use gpui::KeyBinding; - use indoc::indoc; - - use crate::{PushAddSurrounds, object::AnyBrackets, state::Mode, test::VimTestContext}; - - #[gpui::test] - async fn test_add_surrounds(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // test add surrounds with around - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("y s i w {"); - cx.assert_state( - indoc! {" - The ˇ{ quick } brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - // test add surrounds not with around - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("y s i w }"); - cx.assert_state( - indoc! {" - The ˇ{quick} brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - // test add surrounds with motion - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("y s $ }"); - cx.assert_state( - indoc! {" - The quˇ{ick brown} - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - // test add surrounds with multi cursor - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the laˇzy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("y s i w '"); - cx.assert_state( - indoc! {" - The ˇ'quick' brown - fox jumps over - the ˇ'lazy' dog."}, - Mode::Normal, - ); - - // test multi cursor add surrounds with motion - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the laˇzy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("y s $ '"); - cx.assert_state( - indoc! {" - The quˇ'ick brown' - fox jumps over - the laˇ'zy dog.'"}, - Mode::Normal, - ); - - // test multi cursor add surrounds with motion and custom string - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the laˇzy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("y s $ 1"); - cx.assert_state( - indoc! {" - The quˇ1ick brown1 - fox jumps over - the laˇ1zy dog.1"}, - Mode::Normal, - ); - - // test add surrounds with motion current line - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("y s s {"); - cx.assert_state( - indoc! {" - ˇ{ The quick brown } - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - cx.set_state( - indoc! {" - The quˇick brown• - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("y s s {"); - cx.assert_state( - indoc! {" - ˇ{ The quick brown }• - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("2 y s s )"); - cx.assert_state( - indoc! {" - ˇ({ The quick brown }• - fox jumps over) - the lazy dog."}, - Mode::Normal, - ); - - // test add surrounds around object - cx.set_state( - indoc! {" - The [quˇick] brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("y s a ] )"); - cx.assert_state( - indoc! {" - The ˇ([quick]) brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - // test add surrounds inside object - cx.set_state( - indoc! {" - The [quˇick] brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("y s i ] )"); - cx.assert_state( - indoc! {" - The [ˇ(quick)] brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_add_surrounds_visual(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new( - "shift-s", - PushAddSurrounds {}, - Some("vim_mode == visual"), - )]) - }); - - // test add surrounds with around - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("v i w shift-s {"); - cx.assert_state( - indoc! {" - The ˇ{ quick } brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - // test add surrounds not with around - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("v i w shift-s }"); - cx.assert_state( - indoc! {" - The ˇ{quick} brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - // test add surrounds with motion - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("v e shift-s }"); - cx.assert_state( - indoc! {" - The quˇ{ick} brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - // test add surrounds with multi cursor - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the laˇzy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("v i w shift-s '"); - cx.assert_state( - indoc! {" - The ˇ'quick' brown - fox jumps over - the ˇ'lazy' dog."}, - Mode::Normal, - ); - - // test add surrounds with visual block - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("ctrl-v i w j j shift-s '"); - cx.assert_state( - indoc! {" - The ˇ'quick' brown - fox 'jumps' over - the 'lazy 'dog."}, - Mode::Normal, - ); - - // test add surrounds with visual line - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("j shift-v shift-s '"); - cx.assert_state( - indoc! {" - The quick brown - ˇ' - fox jumps over - ' - the lazy dog."}, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_delete_surrounds(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // test delete surround - cx.set_state( - indoc! {" - The {quˇick} brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("d s {"); - cx.assert_state( - indoc! {" - The ˇquick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - // test delete not exist surrounds - cx.set_state( - indoc! {" - The {quˇick} brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("d s ["); - cx.assert_state( - indoc! {" - The {quˇick} brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - // test delete surround forward exist, in the surrounds plugin of other editors, - // the bracket pair in front of the current line will be deleted here, which is not implemented at the moment - cx.set_state( - indoc! {" - The {quick} brˇown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("d s {"); - cx.assert_state( - indoc! {" - The {quick} brˇown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - // test cursor delete inner surrounds - cx.set_state( - indoc! {" - The { quick brown - fox jumˇps over } - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("d s {"); - cx.assert_state( - indoc! {" - The ˇquick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - // test multi cursor delete surrounds - cx.set_state( - indoc! {" - The [quˇick] brown - fox jumps over - the [laˇzy] dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("d s ]"); - cx.assert_state( - indoc! {" - The ˇquick brown - fox jumps over - the ˇlazy dog."}, - Mode::Normal, - ); - - // test multi cursor delete surrounds with around - cx.set_state( - indoc! {" - Tˇhe [ quick ] brown - fox jumps over - the [laˇzy] dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("d s ["); - cx.assert_state( - indoc! {" - The ˇquick brown - fox jumps over - the ˇlazy dog."}, - Mode::Normal, - ); - - cx.set_state( - indoc! {" - Tˇhe [ quick ] brown - fox jumps over - the [laˇzy ] dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("d s ["); - cx.assert_state( - indoc! {" - The ˇquick brown - fox jumps over - the ˇlazy dog."}, - Mode::Normal, - ); - - // test multi cursor delete different surrounds - // the pair corresponding to the two cursors is the same, - // so they are combined into one cursor - cx.set_state( - indoc! {" - The [quˇick] brown - fox jumps over - the {laˇzy} dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("d s {"); - cx.assert_state( - indoc! {" - The [quick] brown - fox jumps over - the ˇlazy dog."}, - Mode::Normal, - ); - - // test delete surround with multi cursor and nest surrounds - cx.set_state( - indoc! {" - fn test_surround() { - ifˇ 2 > 1 { - ˇprintln!(\"it is fine\"); - }; - }"}, - Mode::Normal, - ); - cx.simulate_keystrokes("d s }"); - cx.assert_state( - indoc! {" - fn test_surround() ˇ - if 2 > 1 ˇ - println!(\"it is fine\"); - ; - "}, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_change_surrounds(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! {" - The {quˇick} brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("c s { ["); - cx.assert_state( - indoc! {" - The ˇ[ quick ] brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - // test multi cursor change surrounds - cx.set_state( - indoc! {" - The {quˇick} brown - fox jumps over - the {laˇzy} dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("c s { ["); - cx.assert_state( - indoc! {" - The ˇ[ quick ] brown - fox jumps over - the ˇ[ lazy ] dog."}, - Mode::Normal, - ); - - // test multi cursor delete different surrounds with after cursor - cx.set_state( - indoc! {" - Thˇe {quick} brown - fox jumps over - the {laˇzy} dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("c s { ["); - cx.assert_state( - indoc! {" - The ˇ[ quick ] brown - fox jumps over - the ˇ[ lazy ] dog."}, - Mode::Normal, - ); - - // test multi cursor change surrount with not around - cx.set_state( - indoc! {" - Thˇe { quick } brown - fox jumps over - the {laˇzy} dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("c s { ]"); - cx.assert_state( - indoc! {" - The ˇ[quick] brown - fox jumps over - the ˇ[lazy] dog."}, - Mode::Normal, - ); - - // test multi cursor change with not exist surround - cx.set_state( - indoc! {" - The {quˇick} brown - fox jumps over - the [laˇzy] dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("c s [ '"); - cx.assert_state( - indoc! {" - The {quick} brown - fox jumps over - the ˇ'lazy' dog."}, - Mode::Normal, - ); - - // test change nesting surrounds - cx.set_state( - indoc! {" - fn test_surround() { - ifˇ 2 > 1 { - ˇprintln!(\"it is fine\"); - } - };"}, - Mode::Normal, - ); - cx.simulate_keystrokes("c s } ]"); - cx.assert_state( - indoc! {" - fn test_surround() ˇ[ - if 2 > 1 ˇ[ - println!(\"it is fine\"); - ] - ];"}, - Mode::Normal, - ); - - // test spaces with quote change surrounds - cx.set_state( - indoc! {" - fn test_surround() { - \"ˇ \" - };"}, - Mode::Normal, - ); - cx.simulate_keystrokes("c s \" '"); - cx.assert_state( - indoc! {" - fn test_surround() { - ˇ' ' - };"}, - Mode::Normal, - ); - - // Currently, the same test case but using the closing bracket `]` - // actually removes a whitespace before the closing bracket, something - // that might need to be fixed? - cx.set_state( - indoc! {" - fn test_surround() { - ifˇ 2 > 1 { - ˇprintln!(\"it is fine\"); - } - };"}, - Mode::Normal, - ); - cx.simulate_keystrokes("c s { ]"); - cx.assert_state( - indoc! {" - fn test_surround() ˇ[ - if 2 > 1 ˇ[ - println!(\"it is fine\"); - ] - ];"}, - Mode::Normal, - ); - - // test change quotes. - cx.set_state(indoc! {"' ˇstr '"}, Mode::Normal); - cx.simulate_keystrokes("c s ' \""); - cx.assert_state(indoc! {"ˇ\" str \""}, Mode::Normal); - - // test multi cursor change quotes - cx.set_state( - indoc! {" - ' ˇstr ' - some example text here - ˇ' str ' - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("c s ' \""); - cx.assert_state( - indoc! {" - ˇ\" str \" - some example text here - ˇ\" str \" - "}, - Mode::Normal, - ); - - // test quote to bracket spacing. - cx.set_state(indoc! {"'ˇfoobar'"}, Mode::Normal); - cx.simulate_keystrokes("c s ' {"); - cx.assert_state(indoc! {"ˇ{ foobar }"}, Mode::Normal); - - cx.set_state(indoc! {"'ˇfoobar'"}, Mode::Normal); - cx.simulate_keystrokes("c s ' }"); - cx.assert_state(indoc! {"ˇ{foobar}"}, Mode::Normal); - } - - #[gpui::test] - async fn test_change_surrounds_any_brackets(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // Update keybindings so that using `csb` triggers Vim's `AnyBrackets` - // action. - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new( - "b", - AnyBrackets, - Some("vim_operator == a || vim_operator == i || vim_operator == cs"), - )]); - }); - - cx.set_state(indoc! {"{braˇcketed}"}, Mode::Normal); - cx.simulate_keystrokes("c s b ["); - cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal); - - cx.set_state(indoc! {"[braˇcketed]"}, Mode::Normal); - cx.simulate_keystrokes("c s b {"); - cx.assert_state(indoc! {"ˇ{ bracketed }"}, Mode::Normal); - - cx.set_state(indoc! {""}, Mode::Normal); - cx.simulate_keystrokes("c s b ["); - cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal); - - cx.set_state(indoc! {"(braˇcketed)"}, Mode::Normal); - cx.simulate_keystrokes("c s b ["); - cx.assert_state(indoc! {"ˇ[ bracketed ]"}, Mode::Normal); - - cx.set_state(indoc! {"(< name: ˇ'Zed' >)"}, Mode::Normal); - cx.simulate_keystrokes("c s b }"); - cx.assert_state(indoc! {"(ˇ{ name: 'Zed' })"}, Mode::Normal); - - cx.set_state( - indoc! {" - (< name: ˇ'Zed' >) - (< nˇame: 'DeltaDB' >) - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("c s b {"); - cx.set_state( - indoc! {" - (ˇ{ name: 'Zed' }) - (ˇ{ name: 'DeltaDB' }) - "}, - Mode::Normal, - ); - } - - // The following test cases all follow tpope/vim-surround's behaviour - // and are more focused on how whitespace is handled. - #[gpui::test] - async fn test_change_surrounds_vim(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // Changing quote to quote should never change the surrounding - // whitespace. - cx.set_state(indoc! {"' ˇa '"}, Mode::Normal); - cx.simulate_keystrokes("c s ' \""); - cx.assert_state(indoc! {"ˇ\" a \""}, Mode::Normal); - - cx.set_state(indoc! {"\" ˇa \""}, Mode::Normal); - cx.simulate_keystrokes("c s \" '"); - cx.assert_state(indoc! {"ˇ' a '"}, Mode::Normal); - - // Changing quote to bracket adds one more space when the opening - // bracket is used, does not affect whitespace when the closing bracket - // is used. - cx.set_state(indoc! {"' ˇa '"}, Mode::Normal); - cx.simulate_keystrokes("c s ' {"); - cx.assert_state(indoc! {"ˇ{ a }"}, Mode::Normal); - - cx.set_state(indoc! {"' ˇa '"}, Mode::Normal); - cx.simulate_keystrokes("c s ' }"); - cx.assert_state(indoc! {"ˇ{ a }"}, Mode::Normal); - - // Changing bracket to quote should remove all space when the - // opening bracket is used and preserve all space when the - // closing one is used. - cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal); - cx.simulate_keystrokes("c s { '"); - cx.assert_state(indoc! {"ˇ'a'"}, Mode::Normal); - - cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal); - cx.simulate_keystrokes("c s } '"); - cx.assert_state(indoc! {"ˇ' a '"}, Mode::Normal); - - // Changing bracket to bracket follows these rules: - // * opening → opening – keeps only one space. - // * opening → closing – removes all space. - // * closing → opening – adds one space. - // * closing → closing – does not change space. - cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal); - cx.simulate_keystrokes("c s { ["); - cx.assert_state(indoc! {"ˇ[ a ]"}, Mode::Normal); - - cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal); - cx.simulate_keystrokes("c s { ]"); - cx.assert_state(indoc! {"ˇ[a]"}, Mode::Normal); - - cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal); - cx.simulate_keystrokes("c s } ["); - cx.assert_state(indoc! {"ˇ[ a ]"}, Mode::Normal); - - cx.set_state(indoc! {"{ ˇa }"}, Mode::Normal); - cx.simulate_keystrokes("c s } ]"); - cx.assert_state(indoc! {"ˇ[ a ]"}, Mode::Normal); - } - - #[gpui::test] - async fn test_surrounds(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("y s i w ["); - cx.assert_state( - indoc! {" - The ˇ[ quick ] brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - cx.simulate_keystrokes("c s [ }"); - cx.assert_state( - indoc! {" - The ˇ{quick} brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - cx.simulate_keystrokes("d s {"); - cx.assert_state( - indoc! {" - The ˇquick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - cx.simulate_keystrokes("u"); - cx.assert_state( - indoc! {" - The ˇ{quick} brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - } - - #[gpui::test] - async fn test_surround_aliases(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // add aliases - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("y s i w b"); - cx.assert_state( - indoc! {" - The ˇ(quick) brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("y s i w B"); - cx.assert_state( - indoc! {" - The ˇ{quick} brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("y s i w a"); - cx.assert_state( - indoc! {" - The ˇ brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - cx.set_state( - indoc! {" - The quˇick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("y s i w r"); - cx.assert_state( - indoc! {" - The ˇ[quick] brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - // change aliases - cx.set_state( - indoc! {" - The {quˇick} brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("c s { b"); - cx.assert_state( - indoc! {" - The ˇ(quick) brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - cx.set_state( - indoc! {" - The (quˇick) brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("c s ( B"); - cx.assert_state( - indoc! {" - The ˇ{quick} brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - cx.set_state( - indoc! {" - The (quˇick) brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("c s ( a"); - cx.assert_state( - indoc! {" - The ˇ brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - cx.set_state( - indoc! {" - The brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("c s < b"); - cx.assert_state( - indoc! {" - The ˇ(quick) brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - cx.set_state( - indoc! {" - The (quˇick) brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("c s ( r"); - cx.assert_state( - indoc! {" - The ˇ[quick] brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - cx.set_state( - indoc! {" - The [quˇick] brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("c s [ b"); - cx.assert_state( - indoc! {" - The ˇ(quick) brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - // delete alias - cx.set_state( - indoc! {" - The {quˇick} brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("d s B"); - cx.assert_state( - indoc! {" - The ˇquick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - cx.set_state( - indoc! {" - The (quˇick) brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("d s b"); - cx.assert_state( - indoc! {" - The ˇquick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - cx.set_state( - indoc! {" - The [quˇick] brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("d s r"); - cx.assert_state( - indoc! {" - The ˇquick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - - cx.set_state( - indoc! {" - The brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - cx.simulate_keystrokes("d s a"); - cx.assert_state( - indoc! {" - The ˇquick brown - fox jumps over - the lazy dog."}, - Mode::Normal, - ); - } -} diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs deleted file mode 100644 index 4c61479157..0000000000 --- a/crates/vim/src/test.rs +++ /dev/null @@ -1,2503 +0,0 @@ -mod neovim_backed_test_context; -mod neovim_connection; -mod vim_test_context; - -use std::{sync::Arc, time::Duration}; - -use collections::HashMap; -use command_palette::CommandPalette; -use editor::{ - AnchorRangeExt, DisplayPoint, Editor, EditorMode, MultiBuffer, MultiBufferOffset, - actions::{DeleteLine, WrapSelectionsInTag}, - code_context_menus::CodeContextMenu, - display_map::DisplayRow, - test::editor_test_context::EditorTestContext, -}; -use futures::StreamExt; -use gpui::{KeyBinding, Modifiers, MouseButton, TestAppContext, px}; -use itertools::Itertools; -use language::{CursorShape, Language, LanguageConfig, Point}; -pub use neovim_backed_test_context::*; -use settings::SettingsStore; -use ui::Pixels; -use util::test::marked_text_ranges; -pub use vim_test_context::*; - -use indoc::indoc; -use search::BufferSearchBar; - -use crate::{PushSneak, PushSneakBackward, insert::NormalBefore, motion, state::Mode}; - -use util_macros::perf; - -#[perf] -#[gpui::test] -async fn test_initially_disabled(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, false).await; - cx.simulate_keystrokes("h j k l"); - cx.assert_editor_state("hjklˇ"); -} - -#[perf] -#[gpui::test] -async fn test_neovim(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.simulate_shared_keystrokes("i").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("shift-t e s t space t e s t escape 0 d w") - .await; - cx.shared_state().await.assert_matches(); - cx.assert_editor_state("ˇtest"); -} - -#[perf] -#[gpui::test] -async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.simulate_keystrokes("i"); - assert_eq!(cx.mode(), Mode::Insert); - - // Editor acts as though vim is disabled - cx.disable_vim(); - cx.simulate_keystrokes("h j k l"); - cx.assert_editor_state("hjklˇ"); - - // Selections aren't changed if editor is blurred but vim-mode is still disabled. - cx.cx.set_state("«hjklˇ»"); - cx.assert_editor_state("«hjklˇ»"); - cx.update_editor(|_, window, _cx| window.blur()); - cx.assert_editor_state("«hjklˇ»"); - cx.update_editor(|_, window, cx| cx.focus_self(window)); - cx.assert_editor_state("«hjklˇ»"); - - // Enabling dynamically sets vim mode again and restores normal mode - cx.enable_vim(); - assert_eq!(cx.mode(), Mode::Normal); - cx.simulate_keystrokes("h h h l"); - assert_eq!(cx.buffer_text(), "hjkl".to_owned()); - cx.assert_editor_state("hˇjkl"); - cx.simulate_keystrokes("i T e s t"); - cx.assert_editor_state("hTestˇjkl"); - - // Disabling and enabling resets to normal mode - assert_eq!(cx.mode(), Mode::Insert); - cx.disable_vim(); - cx.enable_vim(); - assert_eq!(cx.mode(), Mode::Normal); -} - -#[perf] -#[gpui::test] -async fn test_cancel_selection(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! {"The quick brown fox juˇmps over the lazy dog"}, - Mode::Normal, - ); - // jumps - cx.simulate_keystrokes("v l l"); - cx.assert_editor_state("The quick brown fox ju«mpsˇ» over the lazy dog"); - - cx.simulate_keystrokes("escape"); - cx.assert_editor_state("The quick brown fox jumpˇs over the lazy dog"); - - // go back to the same selection state - cx.simulate_keystrokes("v h h"); - cx.assert_editor_state("The quick brown fox ju«ˇmps» over the lazy dog"); - - // Ctrl-[ should behave like Esc - cx.simulate_keystrokes("ctrl-["); - cx.assert_editor_state("The quick brown fox juˇmps over the lazy dog"); -} - -#[perf] -#[gpui::test] -async fn test_buffer_search(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! {" - The quick brown - fox juˇmps over - the lazy dog"}, - Mode::Normal, - ); - cx.simulate_keystrokes("/"); - - let search_bar = cx.workspace(|workspace, _, cx| { - workspace - .active_pane() - .read(cx) - .toolbar() - .read(cx) - .item_of_type::() - .expect("Buffer search bar should be deployed") - }); - - cx.update_entity(search_bar, |bar, _, cx| { - assert_eq!(bar.query(cx), ""); - }) -} - -#[perf] -#[gpui::test] -async fn test_count_down(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state(indoc! {"aˇa\nbb\ncc\ndd\nee"}, Mode::Normal); - cx.simulate_keystrokes("2 down"); - cx.assert_editor_state("aa\nbb\ncˇc\ndd\nee"); - cx.simulate_keystrokes("9 down"); - cx.assert_editor_state("aa\nbb\ncc\ndd\neˇe"); -} - -#[perf] -#[gpui::test] -async fn test_end_of_document_710(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // goes to end by default - cx.set_state(indoc! {"aˇa\nbb\ncc"}, Mode::Normal); - cx.simulate_keystrokes("shift-g"); - cx.assert_editor_state("aa\nbb\ncˇc"); - - // can go to line 1 (https://github.com/zed-industries/zed/issues/5812) - cx.simulate_keystrokes("1 shift-g"); - cx.assert_editor_state("aˇa\nbb\ncc"); -} - -#[perf] -#[gpui::test] -async fn test_end_of_line_with_times(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // goes to current line end - cx.set_state(indoc! {"ˇaa\nbb\ncc"}, Mode::Normal); - cx.simulate_keystrokes("$"); - cx.assert_editor_state("aˇa\nbb\ncc"); - - // goes to next line end - cx.simulate_keystrokes("2 $"); - cx.assert_editor_state("aa\nbˇb\ncc"); - - // try to exceed the final line. - cx.simulate_keystrokes("4 $"); - cx.assert_editor_state("aa\nbb\ncˇc"); -} - -#[perf] -#[gpui::test] -async fn test_indent_outdent(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // works in normal mode - cx.set_state(indoc! {"aa\nbˇb\ncc"}, Mode::Normal); - cx.simulate_keystrokes("> >"); - cx.assert_editor_state("aa\n bˇb\ncc"); - cx.simulate_keystrokes("< <"); - cx.assert_editor_state("aa\nbˇb\ncc"); - - // works in visual mode - cx.simulate_keystrokes("shift-v down >"); - cx.assert_editor_state("aa\n bˇb\n cc"); - - // works as operator - cx.set_state("aa\nbˇb\ncc\n", Mode::Normal); - cx.simulate_keystrokes("> j"); - cx.assert_editor_state("aa\n bˇb\n cc\n"); - cx.simulate_keystrokes("< k"); - cx.assert_editor_state("aa\nbˇb\n cc\n"); - cx.simulate_keystrokes("> i p"); - cx.assert_editor_state(" aa\n bˇb\n cc\n"); - cx.simulate_keystrokes("< i p"); - cx.assert_editor_state("aa\nbˇb\n cc\n"); - cx.simulate_keystrokes("< i p"); - cx.assert_editor_state("aa\nbˇb\ncc\n"); - - cx.set_state("ˇaa\nbb\ncc\n", Mode::Normal); - cx.simulate_keystrokes("> 2 j"); - cx.assert_editor_state(" ˇaa\n bb\n cc\n"); - - cx.set_state("aa\nbb\nˇcc\n", Mode::Normal); - cx.simulate_keystrokes("> 2 k"); - cx.assert_editor_state(" aa\n bb\n ˇcc\n"); - - // works with repeat - cx.set_state("a\nb\nccˇc\n", Mode::Normal); - cx.simulate_keystrokes("> 2 k"); - cx.assert_editor_state(" a\n b\n ccˇc\n"); - cx.simulate_keystrokes("."); - cx.assert_editor_state(" a\n b\n ccˇc\n"); - cx.simulate_keystrokes("v k <"); - cx.assert_editor_state(" a\n bˇ\n ccc\n"); - cx.simulate_keystrokes("."); - cx.assert_editor_state(" a\nbˇ\nccc\n"); -} - -#[perf] -#[gpui::test] -async fn test_escape_command_palette(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("aˇbc\n", Mode::Normal); - cx.simulate_keystrokes("i cmd-shift-p"); - - assert!( - cx.workspace(|workspace, _, cx| workspace.active_modal::(cx).is_some()) - ); - cx.simulate_keystrokes("escape"); - cx.run_until_parked(); - assert!( - !cx.workspace(|workspace, _, cx| workspace.active_modal::(cx).is_some()) - ); - cx.assert_state("aˇbc\n", Mode::Insert); -} - -#[perf] -#[gpui::test] -async fn test_escape_cancels(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("aˇbˇc", Mode::Normal); - cx.simulate_keystrokes("escape"); - - cx.assert_state("aˇbc", Mode::Normal); -} - -#[perf] -#[gpui::test] -async fn test_selection_on_search(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state(indoc! {"aa\nbˇb\ncc\ncc\ncc\n"}, Mode::Normal); - cx.simulate_keystrokes("/ c c"); - - let search_bar = cx.workspace(|workspace, _, cx| { - workspace - .active_pane() - .read(cx) - .toolbar() - .read(cx) - .item_of_type::() - .expect("Buffer search bar should be deployed") - }); - - cx.update_entity(search_bar, |bar, _, cx| { - assert_eq!(bar.query(cx), "cc"); - }); - - cx.update_editor(|editor, window, cx| { - let highlights = editor.all_text_background_highlights(window, cx); - assert_eq!(3, highlights.len()); - assert_eq!( - DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 2), - highlights[0].0 - ) - }); - cx.simulate_keystrokes("enter"); - - cx.assert_state(indoc! {"aa\nbb\nˇcc\ncc\ncc\n"}, Mode::Normal); - cx.simulate_keystrokes("n"); - cx.assert_state(indoc! {"aa\nbb\ncc\nˇcc\ncc\n"}, Mode::Normal); - cx.simulate_keystrokes("shift-n"); - cx.assert_state(indoc! {"aa\nbb\nˇcc\ncc\ncc\n"}, Mode::Normal); -} - -#[perf] -#[gpui::test] -async fn test_word_characters(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new_typescript(cx).await; - cx.set_state( - indoc! { " - class A { - #ˇgoop = 99; - $ˇgoop () { return this.#gˇoop }; - }; - console.log(new A().$gooˇp()) - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("v i w"); - cx.assert_state( - indoc! {" - class A { - «#goopˇ» = 99; - «$goopˇ» () { return this.«#goopˇ» }; - }; - console.log(new A().«$goopˇ»()) - "}, - Mode::Visual, - ) -} - -#[perf] -#[gpui::test] -async fn test_kebab_case(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new_html(cx).await; - cx.set_state( - indoc! { r#" -
- "#}, - Mode::Normal, - ); - cx.simulate_keystrokes("v i w"); - cx.assert_state( - indoc! { r#" -
- "# - }, - Mode::Visual, - ) -} - -#[perf] -#[gpui::test] -async fn test_join_lines(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ˇone - two - three - four - five - six - "}) - .await; - cx.simulate_shared_keystrokes("shift-j").await; - cx.shared_state().await.assert_eq(indoc! {" - oneˇ two - three - four - five - six - "}); - cx.simulate_shared_keystrokes("3 shift-j").await; - cx.shared_state().await.assert_eq(indoc! {" - one two threeˇ four - five - six - "}); - - cx.set_shared_state(indoc! {" - ˇone - two - three - four - five - six - "}) - .await; - cx.simulate_shared_keystrokes("j v 3 j shift-j").await; - cx.shared_state().await.assert_eq(indoc! {" - one - two three fourˇ five - six - "}); - - cx.set_shared_state(indoc! {" - ˇone - two - three - four - five - six - "}) - .await; - cx.simulate_shared_keystrokes("g shift-j").await; - cx.shared_state().await.assert_eq(indoc! {" - oneˇtwo - three - four - five - six - "}); - cx.simulate_shared_keystrokes("3 g shift-j").await; - cx.shared_state().await.assert_eq(indoc! {" - onetwothreeˇfour - five - six - "}); - - cx.set_shared_state(indoc! {" - ˇone - two - three - four - five - six - "}) - .await; - cx.simulate_shared_keystrokes("j v 3 j g shift-j").await; - cx.shared_state().await.assert_eq(indoc! {" - one - twothreefourˇfive - six - "}); -} - -#[cfg(target_os = "macos")] -#[perf] -#[gpui::test] -async fn test_wrapped_lines(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_wrap(12).await; - // tests line wrap as follows: - // 1: twelve char - // twelve char - // 2: twelve char - cx.set_shared_state(indoc! { " - tˇwelve char twelve char - twelve char - "}) - .await; - cx.simulate_shared_keystrokes("j").await; - cx.shared_state().await.assert_eq(indoc! {" - twelve char twelve char - tˇwelve char - "}); - cx.simulate_shared_keystrokes("k").await; - cx.shared_state().await.assert_eq(indoc! {" - tˇwelve char twelve char - twelve char - "}); - cx.simulate_shared_keystrokes("g j").await; - cx.shared_state().await.assert_eq(indoc! {" - twelve char tˇwelve char - twelve char - "}); - cx.simulate_shared_keystrokes("g j").await; - cx.shared_state().await.assert_eq(indoc! {" - twelve char twelve char - tˇwelve char - "}); - - cx.simulate_shared_keystrokes("g k").await; - cx.shared_state().await.assert_eq(indoc! {" - twelve char tˇwelve char - twelve char - "}); - - cx.simulate_shared_keystrokes("g ^").await; - cx.shared_state().await.assert_eq(indoc! {" - twelve char ˇtwelve char - twelve char - "}); - - cx.simulate_shared_keystrokes("^").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇtwelve char twelve char - twelve char - "}); - - cx.simulate_shared_keystrokes("g $").await; - cx.shared_state().await.assert_eq(indoc! {" - twelve charˇ twelve char - twelve char - "}); - cx.simulate_shared_keystrokes("$").await; - cx.shared_state().await.assert_eq(indoc! {" - twelve char twelve chaˇr - twelve char - "}); - - cx.set_shared_state(indoc! { " - tˇwelve char twelve char - twelve char - "}) - .await; - cx.simulate_shared_keystrokes("enter").await; - cx.shared_state().await.assert_eq(indoc! {" - twelve char twelve char - ˇtwelve char - "}); - - cx.set_shared_state(indoc! { " - twelve char - tˇwelve char twelve char - twelve char - "}) - .await; - cx.simulate_shared_keystrokes("o o escape").await; - cx.shared_state().await.assert_eq(indoc! {" - twelve char - twelve char twelve char - ˇo - twelve char - "}); - - cx.set_shared_state(indoc! { " - twelve char - tˇwelve char twelve char - twelve char - "}) - .await; - cx.simulate_shared_keystrokes("shift-a a escape").await; - cx.shared_state().await.assert_eq(indoc! {" - twelve char - twelve char twelve charˇa - twelve char - "}); - cx.simulate_shared_keystrokes("shift-i i escape").await; - cx.shared_state().await.assert_eq(indoc! {" - twelve char - ˇitwelve char twelve chara - twelve char - "}); - cx.simulate_shared_keystrokes("shift-d").await; - cx.shared_state().await.assert_eq(indoc! {" - twelve char - ˇ - twelve char - "}); - - cx.set_shared_state(indoc! { " - twelve char - twelve char tˇwelve char - twelve char - "}) - .await; - cx.simulate_shared_keystrokes("shift-o o escape").await; - cx.shared_state().await.assert_eq(indoc! {" - twelve char - ˇo - twelve char twelve char - twelve char - "}); - - // line wraps as: - // fourteen ch - // ar - // fourteen ch - // ar - cx.set_shared_state(indoc! { " - fourteen chaˇr - fourteen char - "}) - .await; - - cx.simulate_shared_keystrokes("d i w").await; - cx.shared_state().await.assert_eq(indoc! {" - fourteenˇ• - fourteen char - "}); - cx.simulate_shared_keystrokes("j shift-f e f r").await; - cx.shared_state().await.assert_eq(indoc! {" - fourteen• - fourteen chaˇr - "}); -} - -#[perf] -#[gpui::test] -async fn test_folds(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_neovim_option("foldmethod=manual").await; - - cx.set_shared_state(indoc! { " - fn boop() { - ˇbarp() - bazp() - } - "}) - .await; - cx.simulate_shared_keystrokes("shift-v j z f").await; - - // visual display is now: - // fn boop () { - // [FOLDED] - // } - - // TODO: this should not be needed but currently zf does not - // return to normal mode. - cx.simulate_shared_keystrokes("escape").await; - - // skip over fold downward - cx.simulate_shared_keystrokes("g g").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇfn boop() { - barp() - bazp() - } - "}); - - cx.simulate_shared_keystrokes("j j").await; - cx.shared_state().await.assert_eq(indoc! {" - fn boop() { - barp() - bazp() - ˇ} - "}); - - // skip over fold upward - cx.simulate_shared_keystrokes("2 k").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇfn boop() { - barp() - bazp() - } - "}); - - // yank the fold - cx.simulate_shared_keystrokes("down y y").await; - cx.shared_clipboard() - .await - .assert_eq(" barp()\n bazp()\n"); - - // re-open - cx.simulate_shared_keystrokes("z o").await; - cx.shared_state().await.assert_eq(indoc! {" - fn boop() { - ˇ barp() - bazp() - } - "}); -} - -#[perf] -#[gpui::test] -async fn test_folds_panic(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_neovim_option("foldmethod=manual").await; - - cx.set_shared_state(indoc! { " - fn boop() { - ˇbarp() - bazp() - } - "}) - .await; - cx.simulate_shared_keystrokes("shift-v j z f").await; - cx.simulate_shared_keystrokes("escape").await; - cx.simulate_shared_keystrokes("g g").await; - cx.simulate_shared_keystrokes("5 d j").await; - cx.shared_state().await.assert_eq("ˇ"); - cx.set_shared_state(indoc! {" - fn boop() { - ˇbarp() - bazp() - } - "}) - .await; - cx.simulate_shared_keystrokes("shift-v j j z f").await; - cx.simulate_shared_keystrokes("escape").await; - cx.simulate_shared_keystrokes("shift-g shift-v").await; - cx.shared_state().await.assert_eq(indoc! {" - fn boop() { - barp() - bazp() - } - ˇ"}); -} - -#[perf] -#[gpui::test] -async fn test_clear_counts(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - The quick brown - fox juˇmps over - the lazy dog"}) - .await; - - cx.simulate_shared_keystrokes("4 escape 3 d l").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox juˇ over - the lazy dog"}); -} - -#[perf] -#[gpui::test] -async fn test_zero(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - The quˇick brown - fox jumps over - the lazy dog"}) - .await; - - cx.simulate_shared_keystrokes("0").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇThe quick brown - fox jumps over - the lazy dog"}); - - cx.simulate_shared_keystrokes("1 0 l").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick ˇbrown - fox jumps over - the lazy dog"}); -} - -#[perf] -#[gpui::test] -async fn test_selection_goal(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - ;;ˇ; - Lorem Ipsum"}) - .await; - - cx.simulate_shared_keystrokes("a down up ; down up").await; - cx.shared_state().await.assert_eq(indoc! {" - ;;;;ˇ - Lorem Ipsum"}); -} - -#[cfg(target_os = "macos")] -#[perf] -#[gpui::test] -async fn test_wrapped_motions(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_wrap(12).await; - - cx.set_shared_state(indoc! {" - aaˇaa - 😃😃" - }) - .await; - cx.simulate_shared_keystrokes("j").await; - cx.shared_state().await.assert_eq(indoc! {" - aaaa - 😃ˇ😃" - }); - - cx.set_shared_state(indoc! {" - 123456789012aaˇaa - 123456789012😃😃" - }) - .await; - cx.simulate_shared_keystrokes("j").await; - cx.shared_state().await.assert_eq(indoc! {" - 123456789012aaaa - 123456789012😃ˇ😃" - }); - - cx.set_shared_state(indoc! {" - 123456789012aaˇaa - 123456789012😃😃" - }) - .await; - cx.simulate_shared_keystrokes("j").await; - cx.shared_state().await.assert_eq(indoc! {" - 123456789012aaaa - 123456789012😃ˇ😃" - }); - - cx.set_shared_state(indoc! {" - 123456789012aaaaˇaaaaaaaa123456789012 - wow - 123456789012😃😃😃😃😃😃123456789012" - }) - .await; - cx.simulate_shared_keystrokes("j j").await; - cx.shared_state().await.assert_eq(indoc! {" - 123456789012aaaaaaaaaaaa123456789012 - wow - 123456789012😃😃ˇ😃😃😃😃123456789012" - }); -} - -#[perf] -#[gpui::test] -async fn test_wrapped_delete_end_document(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_wrap(12).await; - - cx.set_shared_state(indoc! {" - aaˇaaaaaaaaaaaaaaaaaa - bbbbbbbbbbbbbbbbbbbb - cccccccccccccccccccc" - }) - .await; - cx.simulate_shared_keystrokes("d shift-g i z z z").await; - cx.shared_state().await.assert_eq(indoc! {" - zzzˇ" - }); -} - -#[perf] -#[gpui::test] -async fn test_paragraphs_dont_wrap(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - one - ˇ - two"}) - .await; - - cx.simulate_shared_keystrokes("} }").await; - cx.shared_state().await.assert_eq(indoc! {" - one - - twˇo"}); - - cx.simulate_shared_keystrokes("{ { {").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇone - - two"}); -} - -#[perf] -#[gpui::test] -async fn test_select_all_issue_2170(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! {" - defmodule Test do - def test(a, ˇ[_, _] = b), do: IO.puts('hi') - end - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("g a"); - cx.assert_state( - indoc! {" - defmodule Test do - def test(a, «[ˇ»_, _] = b), do: IO.puts('hi') - end - "}, - Mode::Visual, - ); -} - -#[perf] -#[gpui::test] -async fn test_jk(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new( - "j k", - NormalBefore, - Some("vim_mode == insert"), - )]) - }); - cx.neovim.exec("imap jk ").await; - - cx.set_shared_state("ˇhello").await; - cx.simulate_shared_keystrokes("i j o j k").await; - cx.shared_state().await.assert_eq("jˇohello"); -} - -fn assert_pending_input(cx: &mut VimTestContext, expected: &str) { - cx.update_editor(|editor, window, cx| { - let snapshot = editor.snapshot(window, cx); - let highlights = editor - .text_highlights::(cx) - .unwrap() - .1; - let (_, ranges) = marked_text_ranges(expected, false); - - assert_eq!( - highlights - .iter() - .map(|highlight| highlight.to_offset(&snapshot.buffer_snapshot())) - .collect::>(), - ranges - .iter() - .map(|range| MultiBufferOffset(range.start)..MultiBufferOffset(range.end)) - .collect::>() - ) - }); -} - -#[perf] -#[gpui::test] -async fn test_jk_multi(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new( - "j k l", - NormalBefore, - Some("vim_mode == insert"), - )]) - }); - - cx.set_state("ˇone ˇone ˇone", Mode::Normal); - cx.simulate_keystrokes("i j"); - cx.simulate_keystrokes("k"); - cx.assert_state("ˇjkone ˇjkone ˇjkone", Mode::Insert); - assert_pending_input(&mut cx, "«jk»one «jk»one «jk»one"); - cx.simulate_keystrokes("o j k"); - cx.assert_state("jkoˇjkone jkoˇjkone jkoˇjkone", Mode::Insert); - assert_pending_input(&mut cx, "jko«jk»one jko«jk»one jko«jk»one"); - cx.simulate_keystrokes("l"); - cx.assert_state("jkˇoone jkˇoone jkˇoone", Mode::Normal); -} - -#[perf] -#[gpui::test] -async fn test_jk_delay(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new( - "j k", - NormalBefore, - Some("vim_mode == insert"), - )]) - }); - - cx.set_state("ˇhello", Mode::Normal); - cx.simulate_keystrokes("i j"); - cx.executor().advance_clock(Duration::from_millis(500)); - cx.run_until_parked(); - cx.assert_state("ˇjhello", Mode::Insert); - cx.update_editor(|editor, window, cx| { - let snapshot = editor.snapshot(window, cx); - let highlights = editor - .text_highlights::(cx) - .unwrap() - .1; - - assert_eq!( - highlights - .iter() - .map(|highlight| highlight.to_offset(&snapshot.buffer_snapshot())) - .collect::>(), - vec![MultiBufferOffset(0)..MultiBufferOffset(1)] - ) - }); - cx.executor().advance_clock(Duration::from_millis(500)); - cx.run_until_parked(); - cx.assert_state("jˇhello", Mode::Insert); - cx.simulate_keystrokes("k j k"); - cx.assert_state("jˇkhello", Mode::Normal); -} - -#[perf] -#[gpui::test] -async fn test_jk_max_count(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("1\nˇ2\n3").await; - cx.simulate_shared_keystrokes("9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 9 j") - .await; - cx.shared_state().await.assert_eq("1\n2\nˇ3"); - - let number: String = usize::MAX.to_string().split("").join(" "); - cx.simulate_shared_keystrokes(&format!("{number} k")).await; - cx.shared_state().await.assert_eq("ˇ1\n2\n3"); -} - -#[perf] -#[gpui::test] -async fn test_comma_w(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new( - ", w", - motion::Down { - display_lines: false, - }, - Some("vim_mode == normal"), - )]) - }); - cx.neovim.exec("map ,w j").await; - - cx.set_shared_state("ˇhello hello\nhello hello").await; - cx.simulate_shared_keystrokes("f o ; , w").await; - cx.shared_state() - .await - .assert_eq("hello hello\nhello hellˇo"); - - cx.set_shared_state("ˇhello hello\nhello hello").await; - cx.simulate_shared_keystrokes("f o ; , i").await; - cx.shared_state() - .await - .assert_eq("hellˇo hello\nhello hello"); -} - -#[perf] -#[gpui::test] -async fn test_completion_menu_scroll_aside(cx: &mut TestAppContext) { - let mut cx = VimTestContext::new_typescript(cx).await; - - cx.lsp - .set_request_handler::(move |_, _| async move { - Ok(Some(lsp::CompletionResponse::Array(vec![ - lsp::CompletionItem { - label: "Test Item".to_string(), - documentation: Some(lsp::Documentation::String( - "This is some very long documentation content that will be displayed in the aside panel for scrolling.\n".repeat(50) - )), - ..Default::default() - }, - ]))) - }); - - cx.set_state("variableˇ", Mode::Insert); - cx.simulate_keystroke("."); - cx.executor().run_until_parked(); - - let mut initial_offset: Pixels = px(0.0); - - cx.update_editor(|editor, _, _| { - let binding = editor.context_menu().borrow(); - let Some(CodeContextMenu::Completions(menu)) = binding.as_ref() else { - panic!("Should have completions menu open"); - }; - - initial_offset = menu.scroll_handle_aside.offset().y; - }); - - // The `ctrl-e` shortcut should scroll the completion menu's aside content - // down, so the updated offset should be lower than the initial offset. - cx.simulate_keystroke("ctrl-e"); - cx.update_editor(|editor, _, _| { - let binding = editor.context_menu().borrow(); - let Some(CodeContextMenu::Completions(menu)) = binding.as_ref() else { - panic!("Should have completions menu open"); - }; - - assert!(menu.scroll_handle_aside.offset().y < initial_offset); - }); - - // The `ctrl-y` shortcut should do the inverse scrolling as `ctrl-e`, so the - // offset should now be the same as the initial offset. - cx.simulate_keystroke("ctrl-y"); - cx.update_editor(|editor, _, _| { - let binding = editor.context_menu().borrow(); - let Some(CodeContextMenu::Completions(menu)) = binding.as_ref() else { - panic!("Should have completions menu open"); - }; - - assert_eq!(menu.scroll_handle_aside.offset().y, initial_offset); - }); - - // The `ctrl-d` shortcut should scroll the completion menu's aside content - // down, so the updated offset should be lower than the initial offset. - cx.simulate_keystroke("ctrl-d"); - cx.update_editor(|editor, _, _| { - let binding = editor.context_menu().borrow(); - let Some(CodeContextMenu::Completions(menu)) = binding.as_ref() else { - panic!("Should have completions menu open"); - }; - - assert!(menu.scroll_handle_aside.offset().y < initial_offset); - }); - - // The `ctrl-u` shortcut should do the inverse scrolling as `ctrl-u`, so the - // offset should now be the same as the initial offset. - cx.simulate_keystroke("ctrl-u"); - cx.update_editor(|editor, _, _| { - let binding = editor.context_menu().borrow(); - let Some(CodeContextMenu::Completions(menu)) = binding.as_ref() else { - panic!("Should have completions menu open"); - }; - - assert_eq!(menu.scroll_handle_aside.offset().y, initial_offset); - }); -} - -#[perf] -#[gpui::test] -async fn test_rename(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new_typescript(cx).await; - - cx.set_state("const beˇfore = 2; console.log(before)", Mode::Normal); - let def_range = cx.lsp_range("const «beforeˇ» = 2; console.log(before)"); - let tgt_range = cx.lsp_range("const before = 2; console.log(«beforeˇ»)"); - let mut prepare_request = cx.set_request_handler::( - move |_, _, _| async move { Ok(Some(lsp::PrepareRenameResponse::Range(def_range))) }, - ); - let mut rename_request = - cx.set_request_handler::(move |url, params, _| async move { - Ok(Some(lsp::WorkspaceEdit { - changes: Some( - [( - url.clone(), - vec![ - lsp::TextEdit::new(def_range, params.new_name.clone()), - lsp::TextEdit::new(tgt_range, params.new_name), - ], - )] - .into(), - ), - ..Default::default() - })) - }); - - cx.simulate_keystrokes("c d"); - prepare_request.next().await.unwrap(); - cx.simulate_input("after"); - cx.simulate_keystrokes("enter"); - rename_request.next().await.unwrap(); - cx.assert_state("const afterˇ = 2; console.log(after)", Mode::Normal) -} - -#[gpui::test] -async fn test_go_to_definition(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new_typescript(cx).await; - - cx.set_state("const before = 2; console.log(beforˇe)", Mode::Normal); - let def_range = cx.lsp_range("const «beforeˇ» = 2; console.log(before)"); - let mut go_to_request = - cx.set_request_handler::(move |url, _, _| async move { - Ok(Some(lsp::GotoDefinitionResponse::Scalar( - lsp::Location::new(url.clone(), def_range), - ))) - }); - - cx.simulate_keystrokes("g d"); - go_to_request.next().await.unwrap(); - cx.run_until_parked(); - - cx.assert_state("const ˇbefore = 2; console.log(before)", Mode::Normal); -} - -#[perf] -#[gpui::test] -async fn test_remap(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // test moving the cursor - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new( - "g z", - workspace::SendKeystrokes("l l l l".to_string()), - None, - )]) - }); - cx.set_state("ˇ123456789", Mode::Normal); - cx.simulate_keystrokes("g z"); - cx.assert_state("1234ˇ56789", Mode::Normal); - - // test switching modes - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new( - "g y", - workspace::SendKeystrokes("i f o o escape l".to_string()), - None, - )]) - }); - cx.set_state("ˇ123456789", Mode::Normal); - cx.simulate_keystrokes("g y"); - cx.assert_state("fooˇ123456789", Mode::Normal); - - // test recursion - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new( - "g x", - workspace::SendKeystrokes("g z g y".to_string()), - None, - )]) - }); - cx.set_state("ˇ123456789", Mode::Normal); - cx.simulate_keystrokes("g x"); - cx.assert_state("1234fooˇ56789", Mode::Normal); - - // test command - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new( - "g w", - workspace::SendKeystrokes(": j enter".to_string()), - None, - )]) - }); - cx.set_state("ˇ1234\n56789", Mode::Normal); - cx.simulate_keystrokes("g w"); - cx.assert_state("1234ˇ 56789", Mode::Normal); - - // test leaving command - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new( - "g u", - workspace::SendKeystrokes("g w g z".to_string()), - None, - )]) - }); - cx.set_state("ˇ1234\n56789", Mode::Normal); - cx.simulate_keystrokes("g u"); - cx.assert_state("1234 567ˇ89", Mode::Normal); - - // test leaving command - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new( - "g t", - workspace::SendKeystrokes("i space escape".to_string()), - None, - )]) - }); - cx.set_state("12ˇ34", Mode::Normal); - cx.simulate_keystrokes("g t"); - cx.assert_state("12ˇ 34", Mode::Normal); -} - -#[perf] -#[gpui::test] -async fn test_undo(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("hello quˇoel world").await; - cx.simulate_shared_keystrokes("v i w s c o escape u").await; - cx.shared_state().await.assert_eq("hello ˇquoel world"); - cx.simulate_shared_keystrokes("ctrl-r").await; - cx.shared_state().await.assert_eq("hello ˇco world"); - cx.simulate_shared_keystrokes("a o right l escape").await; - cx.shared_state().await.assert_eq("hello cooˇl world"); - cx.simulate_shared_keystrokes("u").await; - cx.shared_state().await.assert_eq("hello cooˇ world"); - cx.simulate_shared_keystrokes("u").await; - cx.shared_state().await.assert_eq("hello cˇo world"); - cx.simulate_shared_keystrokes("u").await; - cx.shared_state().await.assert_eq("hello ˇquoel world"); - - cx.set_shared_state("hello quˇoel world").await; - cx.simulate_shared_keystrokes("v i w ~ u").await; - cx.shared_state().await.assert_eq("hello ˇquoel world"); - - cx.set_shared_state("\nhello quˇoel world\n").await; - cx.simulate_shared_keystrokes("shift-v s c escape u").await; - cx.shared_state().await.assert_eq("\nˇhello quoel world\n"); - - cx.set_shared_state(indoc! {" - ˇ1 - 2 - 3"}) - .await; - - cx.simulate_shared_keystrokes("ctrl-v shift-g ctrl-a").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇ2 - 3 - 4"}); - - cx.simulate_shared_keystrokes("u").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇ1 - 2 - 3"}); -} - -#[perf] -#[gpui::test] -async fn test_mouse_selection(cx: &mut TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("ˇone two three", Mode::Normal); - - let start_point = cx.pixel_position("one twˇo three"); - let end_point = cx.pixel_position("one ˇtwo three"); - - cx.simulate_mouse_down(start_point, MouseButton::Left, Modifiers::none()); - cx.simulate_mouse_move(end_point, MouseButton::Left, Modifiers::none()); - cx.simulate_mouse_up(end_point, MouseButton::Left, Modifiers::none()); - - cx.assert_state("one «ˇtwo» three", Mode::Visual) -} - -#[perf] -#[gpui::test] -async fn test_lowercase_marks(cx: &mut TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("line one\nline ˇtwo\nline three").await; - cx.simulate_shared_keystrokes("m a l ' a").await; - cx.shared_state() - .await - .assert_eq("line one\nˇline two\nline three"); - cx.simulate_shared_keystrokes("` a").await; - cx.shared_state() - .await - .assert_eq("line one\nline ˇtwo\nline three"); - - cx.simulate_shared_keystrokes("^ d ` a").await; - cx.shared_state() - .await - .assert_eq("line one\nˇtwo\nline three"); -} - -#[perf] -#[gpui::test] -async fn test_lt_gt_marks(cx: &mut TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc!( - " - Line one - Line two - Line ˇthree - Line four - Line five - " - )) - .await; - - cx.simulate_shared_keystrokes("v j escape k k").await; - - cx.simulate_shared_keystrokes("' <").await; - cx.shared_state().await.assert_eq(indoc! {" - Line one - Line two - ˇLine three - Line four - Line five - "}); - - cx.simulate_shared_keystrokes("` <").await; - cx.shared_state().await.assert_eq(indoc! {" - Line one - Line two - Line ˇthree - Line four - Line five - "}); - - cx.simulate_shared_keystrokes("' >").await; - cx.shared_state().await.assert_eq(indoc! {" - Line one - Line two - Line three - ˇLine four - Line five - " - }); - - cx.simulate_shared_keystrokes("` >").await; - cx.shared_state().await.assert_eq(indoc! {" - Line one - Line two - Line three - Line ˇfour - Line five - " - }); - - cx.simulate_shared_keystrokes("v i w o escape").await; - cx.simulate_shared_keystrokes("` >").await; - cx.shared_state().await.assert_eq(indoc! {" - Line one - Line two - Line three - Line fouˇr - Line five - " - }); - cx.simulate_shared_keystrokes("` <").await; - cx.shared_state().await.assert_eq(indoc! {" - Line one - Line two - Line three - Line ˇfour - Line five - " - }); -} - -#[perf] -#[gpui::test] -async fn test_caret_mark(cx: &mut TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc!( - " - Line one - Line two - Line three - ˇLine four - Line five - " - )) - .await; - - cx.simulate_shared_keystrokes("c w shift-s t r a i g h t space t h i n g escape j j") - .await; - - cx.simulate_shared_keystrokes("' ^").await; - cx.shared_state().await.assert_eq(indoc! {" - Line one - Line two - Line three - ˇStraight thing four - Line five - " - }); - - cx.simulate_shared_keystrokes("` ^").await; - cx.shared_state().await.assert_eq(indoc! {" - Line one - Line two - Line three - Straight thingˇ four - Line five - " - }); - - cx.simulate_shared_keystrokes("k a ! escape k g i ?").await; - cx.shared_state().await.assert_eq(indoc! {" - Line one - Line two - Line three!?ˇ - Straight thing four - Line five - " - }); -} - -#[cfg(target_os = "macos")] -#[perf] -#[gpui::test] -async fn test_dw_eol(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_wrap(12).await; - cx.set_shared_state("twelve ˇchar twelve char\ntwelve char") - .await; - cx.simulate_shared_keystrokes("d w").await; - cx.shared_state() - .await - .assert_eq("twelve ˇtwelve char\ntwelve char"); -} - -#[perf] -#[gpui::test] -async fn test_toggle_comments(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - let language = std::sync::Arc::new(language::Language::new( - language::LanguageConfig { - line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()], - ..Default::default() - }, - Some(language::tree_sitter_rust::LANGUAGE.into()), - )); - cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); - - // works in normal model - cx.set_state( - indoc! {" - ˇone - two - three - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("g c c"); - cx.assert_state( - indoc! {" - // ˇone - two - three - "}, - Mode::Normal, - ); - - // works in visual mode - cx.simulate_keystrokes("v j g c"); - cx.assert_state( - indoc! {" - // // ˇone - // two - three - "}, - Mode::Normal, - ); - - // works in visual line mode - cx.simulate_keystrokes("shift-v j g c"); - cx.assert_state( - indoc! {" - // ˇone - two - three - "}, - Mode::Normal, - ); - - // works with count - cx.simulate_keystrokes("g c 2 j"); - cx.assert_state( - indoc! {" - // // ˇone - // two - // three - "}, - Mode::Normal, - ); - - // works with motion object - cx.simulate_keystrokes("shift-g"); - cx.simulate_keystrokes("g c g g"); - cx.assert_state( - indoc! {" - // one - two - three - ˇ"}, - Mode::Normal, - ); -} - -#[perf] -#[gpui::test] -async fn test_find_multibyte(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(r#""#) - .await; - - cx.simulate_shared_keystrokes("c t < o escape").await; - cx.shared_state() - .await - .assert_eq(r#""#); -} - -#[perf] -#[gpui::test] -async fn test_sneak(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.update(|_window, cx| { - cx.bind_keys([ - KeyBinding::new( - "s", - PushSneak { first_char: None }, - Some("vim_mode == normal"), - ), - KeyBinding::new( - "shift-s", - PushSneakBackward { first_char: None }, - Some("vim_mode == normal"), - ), - KeyBinding::new( - "shift-s", - PushSneakBackward { first_char: None }, - Some("vim_mode == visual"), - ), - ]) - }); - - // Sneak forwards multibyte & multiline - cx.set_state( - indoc! { - r#" - Počet hostů - "# - }, - Mode::Normal, - ); - cx.simulate_keystrokes("s t ů"); - cx.assert_state( - indoc! { - r#""# - }, - Mode::Normal, - ); - - // Visual sneak backwards multibyte & multiline - cx.simulate_keystrokes("v S < l"); - cx.assert_state( - indoc! { - r#"«ˇ"# - }, - Mode::Visual, - ); - - // Sneak backwards repeated - cx.set_state(r#"11 12 13 ˇ14"#, Mode::Normal); - cx.simulate_keystrokes("S space 1"); - cx.assert_state(r#"11 12ˇ 13 14"#, Mode::Normal); - cx.simulate_keystrokes(";"); - cx.assert_state(r#"11ˇ 12 13 14"#, Mode::Normal); -} - -#[perf] -#[gpui::test] -async fn test_plus_minus(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "one - two - thrˇee - "}) - .await; - - cx.simulate_shared_keystrokes("-").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("-").await; - cx.shared_state().await.assert_matches(); - cx.simulate_shared_keystrokes("+").await; - cx.shared_state().await.assert_matches(); -} - -#[perf] -#[gpui::test] -async fn test_command_alias(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |s| { - let mut aliases = HashMap::default(); - aliases.insert("Q".to_string(), "upper".to_string()); - s.workspace.command_aliases = aliases - }); - }); - - cx.set_state("ˇhello world", Mode::Normal); - cx.simulate_keystrokes(": Q"); - cx.set_state("ˇHello world", Mode::Normal); -} - -#[perf] -#[gpui::test] -async fn test_remap_adjacent_dog_cat(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.update(|_, cx| { - cx.bind_keys([ - KeyBinding::new( - "d o g", - workspace::SendKeystrokes("🐶".to_string()), - Some("vim_mode == insert"), - ), - KeyBinding::new( - "c a t", - workspace::SendKeystrokes("🐱".to_string()), - Some("vim_mode == insert"), - ), - ]) - }); - cx.neovim.exec("imap dog 🐶").await; - cx.neovim.exec("imap cat 🐱").await; - - cx.set_shared_state("ˇ").await; - cx.simulate_shared_keystrokes("i d o g").await; - cx.shared_state().await.assert_eq("🐶ˇ"); - - cx.set_shared_state("ˇ").await; - cx.simulate_shared_keystrokes("i d o d o g").await; - cx.shared_state().await.assert_eq("do🐶ˇ"); - - cx.set_shared_state("ˇ").await; - cx.simulate_shared_keystrokes("i d o c a t").await; - cx.shared_state().await.assert_eq("do🐱ˇ"); -} - -#[perf] -#[gpui::test] -async fn test_remap_nested_pineapple(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.update(|_, cx| { - cx.bind_keys([ - KeyBinding::new( - "p i n", - workspace::SendKeystrokes("📌".to_string()), - Some("vim_mode == insert"), - ), - KeyBinding::new( - "p i n e", - workspace::SendKeystrokes("🌲".to_string()), - Some("vim_mode == insert"), - ), - KeyBinding::new( - "p i n e a p p l e", - workspace::SendKeystrokes("🍍".to_string()), - Some("vim_mode == insert"), - ), - ]) - }); - cx.neovim.exec("imap pin 📌").await; - cx.neovim.exec("imap pine 🌲").await; - cx.neovim.exec("imap pineapple 🍍").await; - - cx.set_shared_state("ˇ").await; - cx.simulate_shared_keystrokes("i p i n").await; - cx.executor().advance_clock(Duration::from_millis(1000)); - cx.run_until_parked(); - cx.shared_state().await.assert_eq("📌ˇ"); - - cx.set_shared_state("ˇ").await; - cx.simulate_shared_keystrokes("i p i n e").await; - cx.executor().advance_clock(Duration::from_millis(1000)); - cx.run_until_parked(); - cx.shared_state().await.assert_eq("🌲ˇ"); - - cx.set_shared_state("ˇ").await; - cx.simulate_shared_keystrokes("i p i n e a p p l e").await; - cx.shared_state().await.assert_eq("🍍ˇ"); -} - -#[perf] -#[gpui::test] -async fn test_remap_recursion(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new( - "x", - workspace::SendKeystrokes("\" _ x".to_string()), - Some("VimControl"), - )]); - cx.bind_keys([KeyBinding::new( - "y", - workspace::SendKeystrokes("2 x".to_string()), - Some("VimControl"), - )]) - }); - cx.neovim.exec("noremap x \"_x").await; - cx.neovim.exec("map y 2x").await; - - cx.set_shared_state("ˇhello").await; - cx.simulate_shared_keystrokes("d l").await; - cx.shared_clipboard().await.assert_eq("h"); - cx.simulate_shared_keystrokes("y").await; - cx.shared_clipboard().await.assert_eq("h"); - cx.shared_state().await.assert_eq("ˇlo"); -} - -#[perf] -#[gpui::test] -async fn test_escape_while_waiting(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state("ˇhi").await; - cx.simulate_shared_keystrokes("\" + escape x").await; - cx.shared_state().await.assert_eq("ˇi"); -} - -#[perf] -#[gpui::test] -async fn test_ctrl_w_override(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.update(|_, cx| { - cx.bind_keys([KeyBinding::new("ctrl-w", DeleteLine, None)]); - }); - cx.neovim.exec("map D").await; - cx.set_shared_state("ˇhi").await; - cx.simulate_shared_keystrokes("ctrl-w").await; - cx.shared_state().await.assert_eq("ˇ"); -} - -#[perf] -#[gpui::test] -async fn test_visual_indent_count(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.set_state("ˇhi", Mode::Normal); - cx.simulate_keystrokes("shift-v 3 >"); - cx.assert_state(" ˇhi", Mode::Normal); - cx.simulate_keystrokes("shift-v 2 <"); - cx.assert_state(" ˇhi", Mode::Normal); -} - -#[perf] -#[gpui::test] -async fn test_record_replay_recursion(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇhello world").await; - cx.simulate_shared_keystrokes(">").await; - cx.simulate_shared_keystrokes(".").await; - cx.simulate_shared_keystrokes(".").await; - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq("ˇhello world"); -} - -#[perf] -#[gpui::test] -async fn test_blackhole_register(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("ˇhello world").await; - cx.simulate_shared_keystrokes("d i w \" _ d a w").await; - cx.simulate_shared_keystrokes("p").await; - cx.shared_state().await.assert_eq("hellˇo"); -} - -#[perf] -#[gpui::test] -async fn test_sentence_backwards(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("one\n\ntwo\nthree\nˇ\nfour").await; - cx.simulate_shared_keystrokes("(").await; - cx.shared_state() - .await - .assert_eq("one\n\nˇtwo\nthree\n\nfour"); - - cx.set_shared_state("hello.\n\n\nworˇld.").await; - cx.simulate_shared_keystrokes("(").await; - cx.shared_state().await.assert_eq("hello.\n\n\nˇworld."); - cx.simulate_shared_keystrokes("(").await; - cx.shared_state().await.assert_eq("hello.\n\nˇ\nworld."); - cx.simulate_shared_keystrokes("(").await; - cx.shared_state().await.assert_eq("ˇhello.\n\n\nworld."); - - cx.set_shared_state("hello. worlˇd.").await; - cx.simulate_shared_keystrokes("(").await; - cx.shared_state().await.assert_eq("hello. ˇworld."); - cx.simulate_shared_keystrokes("(").await; - cx.shared_state().await.assert_eq("ˇhello. world."); - - cx.set_shared_state(". helˇlo.").await; - cx.simulate_shared_keystrokes("(").await; - cx.shared_state().await.assert_eq(". ˇhello."); - cx.simulate_shared_keystrokes("(").await; - cx.shared_state().await.assert_eq(". ˇhello."); - - cx.set_shared_state(indoc! { - "{ - hello_world(); - ˇ}" - }) - .await; - cx.simulate_shared_keystrokes("(").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇ{ - hello_world(); - }" - }); - - cx.set_shared_state(indoc! { - "Hello! World..? - - \tHello! World... ˇ" - }) - .await; - cx.simulate_shared_keystrokes("(").await; - cx.shared_state().await.assert_eq(indoc! { - "Hello! World..? - - \tHello! ˇWorld... " - }); - cx.simulate_shared_keystrokes("(").await; - cx.shared_state().await.assert_eq(indoc! { - "Hello! World..? - - \tˇHello! World... " - }); - cx.simulate_shared_keystrokes("(").await; - cx.shared_state().await.assert_eq(indoc! { - "Hello! World..? - ˇ - \tHello! World... " - }); - cx.simulate_shared_keystrokes("(").await; - cx.shared_state().await.assert_eq(indoc! { - "Hello! ˇWorld..? - - \tHello! World... " - }); -} - -#[perf] -#[gpui::test] -async fn test_sentence_forwards(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("helˇlo.\n\n\nworld.").await; - cx.simulate_shared_keystrokes(")").await; - cx.shared_state().await.assert_eq("hello.\nˇ\n\nworld."); - cx.simulate_shared_keystrokes(")").await; - cx.shared_state().await.assert_eq("hello.\n\n\nˇworld."); - cx.simulate_shared_keystrokes(")").await; - cx.shared_state().await.assert_eq("hello.\n\n\nworldˇ."); - - cx.set_shared_state("helˇlo.\n\n\nworld.").await; -} - -#[perf] -#[gpui::test] -async fn test_ctrl_o_visual(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("helloˇ world.").await; - cx.simulate_shared_keystrokes("i ctrl-o v b r l").await; - cx.shared_state().await.assert_eq("ˇllllllworld."); - cx.simulate_shared_keystrokes("ctrl-o v f w d").await; - cx.shared_state().await.assert_eq("ˇorld."); -} - -#[perf] -#[gpui::test] -async fn test_ctrl_o_position(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("helˇlo world.").await; - cx.simulate_shared_keystrokes("i ctrl-o d i w").await; - cx.shared_state().await.assert_eq("ˇ world."); - cx.simulate_shared_keystrokes("ctrl-o p").await; - cx.shared_state().await.assert_eq(" helloˇworld."); -} - -#[perf] -#[gpui::test] -async fn test_ctrl_o_dot(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("heˇllo world.").await; - cx.simulate_shared_keystrokes("x i ctrl-o .").await; - cx.shared_state().await.assert_eq("heˇo world."); - cx.simulate_shared_keystrokes("l l escape .").await; - cx.shared_state().await.assert_eq("hellˇllo world."); -} - -#[perf(iterations = 1)] -#[gpui::test] -async fn test_folded_multibuffer_excerpts(cx: &mut gpui::TestAppContext) { - VimTestContext::init(cx); - cx.update(|cx| { - VimTestContext::init_keybindings(true, cx); - }); - let (editor, cx) = cx.add_window_view(|window, cx| { - let multi_buffer = MultiBuffer::build_multi( - [ - ("111\n222\n333\n444\n", vec![Point::row_range(0..2)]), - ("aaa\nbbb\nccc\nddd\n", vec![Point::row_range(0..2)]), - ("AAA\nBBB\nCCC\nDDD\n", vec![Point::row_range(0..2)]), - ("one\ntwo\nthr\nfou\n", vec![Point::row_range(0..2)]), - ], - cx, - ); - let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx); - - let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids(); - // fold all but the second buffer, so that we test navigating between two - // adjacent folded buffers, as well as folded buffers at the start and - // end the multibuffer - editor.fold_buffer(buffer_ids[0], cx); - editor.fold_buffer(buffer_ids[2], cx); - editor.fold_buffer(buffer_ids[3], cx); - - editor - }); - let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await; - - cx.assert_excerpts_with_selections(indoc! {" - [EXCERPT] - ˇ[FOLDED] - [EXCERPT] - aaa - bbb - [EXCERPT] - [FOLDED] - [EXCERPT] - [FOLDED] - " - }); - cx.simulate_keystroke("j"); - cx.assert_excerpts_with_selections(indoc! {" - [EXCERPT] - [FOLDED] - [EXCERPT] - ˇaaa - bbb - [EXCERPT] - [FOLDED] - [EXCERPT] - [FOLDED] - " - }); - cx.simulate_keystroke("j"); - cx.simulate_keystroke("j"); - cx.assert_excerpts_with_selections(indoc! {" - [EXCERPT] - [FOLDED] - [EXCERPT] - aaa - bbb - ˇ[EXCERPT] - [FOLDED] - [EXCERPT] - [FOLDED] - " - }); - cx.simulate_keystroke("j"); - cx.assert_excerpts_with_selections(indoc! {" - [EXCERPT] - [FOLDED] - [EXCERPT] - aaa - bbb - [EXCERPT] - ˇ[FOLDED] - [EXCERPT] - [FOLDED] - " - }); - cx.simulate_keystroke("j"); - cx.assert_excerpts_with_selections(indoc! {" - [EXCERPT] - [FOLDED] - [EXCERPT] - aaa - bbb - [EXCERPT] - [FOLDED] - [EXCERPT] - ˇ[FOLDED] - " - }); - cx.simulate_keystroke("k"); - cx.assert_excerpts_with_selections(indoc! {" - [EXCERPT] - [FOLDED] - [EXCERPT] - aaa - bbb - [EXCERPT] - ˇ[FOLDED] - [EXCERPT] - [FOLDED] - " - }); - cx.simulate_keystroke("k"); - cx.simulate_keystroke("k"); - cx.simulate_keystroke("k"); - cx.assert_excerpts_with_selections(indoc! {" - [EXCERPT] - [FOLDED] - [EXCERPT] - ˇaaa - bbb - [EXCERPT] - [FOLDED] - [EXCERPT] - [FOLDED] - " - }); - cx.simulate_keystroke("k"); - cx.assert_excerpts_with_selections(indoc! {" - [EXCERPT] - ˇ[FOLDED] - [EXCERPT] - aaa - bbb - [EXCERPT] - [FOLDED] - [EXCERPT] - [FOLDED] - " - }); - cx.simulate_keystroke("shift-g"); - cx.assert_excerpts_with_selections(indoc! {" - [EXCERPT] - [FOLDED] - [EXCERPT] - aaa - bbb - [EXCERPT] - [FOLDED] - [EXCERPT] - ˇ[FOLDED] - " - }); - cx.simulate_keystrokes("g g"); - cx.assert_excerpts_with_selections(indoc! {" - [EXCERPT] - ˇ[FOLDED] - [EXCERPT] - aaa - bbb - [EXCERPT] - [FOLDED] - [EXCERPT] - [FOLDED] - " - }); - cx.update_editor(|editor, _, cx| { - let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids(); - editor.fold_buffer(buffer_ids[1], cx); - }); - - cx.assert_excerpts_with_selections(indoc! {" - [EXCERPT] - ˇ[FOLDED] - [EXCERPT] - [FOLDED] - [EXCERPT] - [FOLDED] - [EXCERPT] - [FOLDED] - " - }); - cx.simulate_keystrokes("2 j"); - cx.assert_excerpts_with_selections(indoc! {" - [EXCERPT] - [FOLDED] - [EXCERPT] - [FOLDED] - [EXCERPT] - ˇ[FOLDED] - [EXCERPT] - [FOLDED] - " - }); -} - -#[perf] -#[gpui::test] -async fn test_delete_paragraph_motion(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! { - "ˇhello world. - - hello world. - " - }) - .await; - cx.simulate_shared_keystrokes("y }").await; - cx.shared_clipboard().await.assert_eq("hello world.\n"); - cx.simulate_shared_keystrokes("d }").await; - cx.shared_state().await.assert_eq("ˇ\nhello world.\n"); - cx.shared_clipboard().await.assert_eq("hello world.\n"); - - cx.set_shared_state(indoc! { - "helˇlo world. - - hello world. - " - }) - .await; - cx.simulate_shared_keystrokes("y }").await; - cx.shared_clipboard().await.assert_eq("lo world."); - cx.simulate_shared_keystrokes("d }").await; - cx.shared_state().await.assert_eq("heˇl\n\nhello world.\n"); - cx.shared_clipboard().await.assert_eq("lo world."); -} - -#[perf] -#[gpui::test] -async fn test_delete_unmatched_brace(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! { - "fn o(wow: i32) { - othˇ(wow) - oth(wow) - } - " - }) - .await; - cx.simulate_shared_keystrokes("d ] }").await; - cx.shared_state().await.assert_eq(indoc! { - "fn o(wow: i32) { - otˇh - } - " - }); - cx.shared_clipboard().await.assert_eq("(wow)\n oth(wow)"); - cx.set_shared_state(indoc! { - "fn o(wow: i32) { - ˇoth(wow) - oth(wow) - } - " - }) - .await; - cx.simulate_shared_keystrokes("d ] }").await; - cx.shared_state().await.assert_eq(indoc! { - "fn o(wow: i32) { - ˇ} - " - }); - cx.shared_clipboard() - .await - .assert_eq(" oth(wow)\n oth(wow)\n"); -} - -#[perf] -#[gpui::test] -async fn test_paragraph_multi_delete(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! { - " - Emacs is - ˇa great - - operating system - - all it lacks - is a - - decent text editor - " - }) - .await; - - cx.simulate_shared_keystrokes("2 d a p").await; - cx.shared_state().await.assert_eq(indoc! { - " - ˇall it lacks - is a - - decent text editor - " - }); - - cx.simulate_shared_keystrokes("d a p").await; - cx.shared_clipboard() - .await - .assert_eq("all it lacks\nis a\n\n"); - - //reset to initial state - cx.simulate_shared_keystrokes("2 u").await; - - cx.simulate_shared_keystrokes("4 d a p").await; - cx.shared_state().await.assert_eq(indoc! {"ˇ"}); -} - -#[perf] -#[gpui::test] -async fn test_yank_paragraph_with_paste(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! { - " - first paragraph - ˇstill first - - second paragraph - still second - - third paragraph - " - }) - .await; - - cx.simulate_shared_keystrokes("y a p").await; - cx.shared_clipboard() - .await - .assert_eq("first paragraph\nstill first\n\n"); - - cx.simulate_shared_keystrokes("j j p").await; - cx.shared_state().await.assert_eq(indoc! { - " - first paragraph - still first - - ˇfirst paragraph - still first - - second paragraph - still second - - third paragraph - " - }); -} - -#[perf] -#[gpui::test] -async fn test_change_paragraph(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! { - " - first paragraph - ˇstill first - - second paragraph - still second - - third paragraph - " - }) - .await; - - cx.simulate_shared_keystrokes("c a p").await; - cx.shared_clipboard() - .await - .assert_eq("first paragraph\nstill first\n\n"); - - cx.simulate_shared_keystrokes("escape").await; - cx.shared_state().await.assert_eq(indoc! { - " - ˇ - second paragraph - still second - - third paragraph - " - }); -} - -#[perf] -#[gpui::test] -async fn test_multi_cursor_replay(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - cx.set_state( - indoc! { - " - oˇne one one - - two two two - " - }, - Mode::Normal, - ); - - cx.simulate_keystrokes("3 g l s wow escape escape"); - cx.assert_state( - indoc! { - " - woˇw wow wow - - two two two - " - }, - Mode::Normal, - ); - - cx.simulate_keystrokes("2 j 3 g l ."); - cx.assert_state( - indoc! { - " - wow wow wow - - woˇw woˇw woˇw - " - }, - Mode::Normal, - ); -} - -#[gpui::test] -async fn test_clipping_on_mode_change(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! { - " - ˇverylongline - andsomelinebelow - " - }, - Mode::Normal, - ); - - cx.simulate_keystrokes("v e"); - cx.assert_state( - indoc! { - " - «verylonglineˇ» - andsomelinebelow - " - }, - Mode::Visual, - ); - - let mut pixel_position = cx.update_editor(|editor, window, cx| { - let snapshot = editor.snapshot(window, cx); - let current_head = editor - .selections - .newest_display(&snapshot.display_snapshot) - .end; - editor.last_bounds().unwrap().origin - + editor - .display_to_pixel_point(current_head, &snapshot, window, cx) - .unwrap() - }); - pixel_position.x += px(100.); - // click beyond end of the line - cx.simulate_click(pixel_position, Modifiers::default()); - cx.run_until_parked(); - - cx.assert_state( - indoc! { - " - verylonglinˇe - andsomelinebelow - " - }, - Mode::Normal, - ); -} - -#[gpui::test] -async fn test_wrap_selections_in_tag_line_mode(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - let js_language = Arc::new(Language::new( - LanguageConfig { - name: "JavaScript".into(), - wrap_characters: Some(language::WrapCharactersConfig { - start_prefix: "<".into(), - start_suffix: ">".into(), - end_prefix: "".into(), - }), - ..LanguageConfig::default() - }, - None, - )); - - cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx)); - - cx.set_state( - indoc! { - " - ˇaaaaa - bbbbb - " - }, - Mode::Normal, - ); - - cx.simulate_keystrokes("shift-v j"); - cx.dispatch_action(WrapSelectionsInTag); - - cx.assert_state( - indoc! { - " - <ˇ>aaaaa - bbbbb - " - }, - Mode::VisualLine, - ); -} - -#[gpui::test] -async fn test_repeat_grouping_41735(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - // typically transaction gropuing is disabled in tests, but here we need to test it. - cx.update_buffer(|buffer, _cx| buffer.set_group_interval(Duration::from_millis(300))); - - cx.set_shared_state("ˇ").await; - - cx.simulate_shared_keystrokes("i a escape").await; - cx.simulate_shared_keystrokes(". . .").await; - cx.shared_state().await.assert_eq("ˇaaaa"); - cx.simulate_shared_keystrokes("u").await; - cx.shared_state().await.assert_eq("ˇaaa"); -} - -#[gpui::test] -async fn test_deactivate(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |settings| { - settings.editor.cursor_shape = Some(settings::CursorShape::Underline); - }); - }); - - // Assert that, while in `Normal` mode, the cursor shape is `Block` but, - // after deactivating vim mode, it should revert to the one specified in the - // user's settings, if set. - cx.update_editor(|editor, _window, _cx| { - assert_eq!(editor.cursor_shape(), CursorShape::Block); - }); - - cx.disable_vim(); - - cx.update_editor(|editor, _window, _cx| { - assert_eq!(editor.cursor_shape(), CursorShape::Underline); - }); -} diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs deleted file mode 100644 index d20464ccc4..0000000000 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ /dev/null @@ -1,427 +0,0 @@ -use gpui::{AppContext as _, UpdateGlobal, px, size}; -use indoc::indoc; -use settings::SettingsStore; -use std::{ - ops::{Deref, DerefMut}, - panic, thread, -}; - -use language::language_settings::SoftWrap; -use util::test::marked_text_offsets; - -use super::{VimTestContext, neovim_connection::NeovimConnection}; -use crate::state::{Mode, VimGlobals}; - -pub struct NeovimBackedTestContext { - pub(crate) cx: VimTestContext, - pub(crate) neovim: NeovimConnection, - - last_set_state: Option, - recent_keystrokes: Vec, -} - -#[derive(Default)] -pub struct SharedState { - neovim: String, - editor: String, - initial: String, - neovim_mode: Mode, - editor_mode: Mode, - recent_keystrokes: String, -} - -impl SharedState { - /// Assert that both Zed and NeoVim have the same content and mode. - #[track_caller] - pub fn assert_matches(&self) { - if self.neovim != self.editor || self.neovim_mode != self.editor_mode { - panic!( - indoc! {"Test failed (zed does not match nvim behavior) - # initial state: - {} - # keystrokes: - {} - # neovim ({}): - {} - # zed ({}): - {}"}, - self.initial, - self.recent_keystrokes, - self.neovim_mode, - self.neovim, - self.editor_mode, - self.editor, - ) - } - } - - #[track_caller] - pub fn assert_eq(&mut self, marked_text: &str) { - let marked_text = marked_text.replace('•', " "); - if self.neovim == marked_text - && self.neovim == self.editor - && self.neovim_mode == self.editor_mode - { - return; - } - - let message = if self.neovim != marked_text { - "Test is incorrect (currently expected != neovim_state)" - } else { - "Editor does not match nvim behavior" - }; - panic!( - indoc! {"{} - # initial state: - {} - # keystrokes: - {} - # currently expected: - {} - # neovim ({}): - {} - # zed ({}): - {}"}, - message, - self.initial, - self.recent_keystrokes, - marked_text.replace(" \n", "•\n"), - self.neovim_mode, - self.neovim.replace(" \n", "•\n"), - self.editor_mode, - self.editor.replace(" \n", "•\n"), - ) - } -} - -pub struct SharedClipboard { - register: char, - neovim: String, - editor: String, - state: SharedState, -} - -impl SharedClipboard { - #[track_caller] - pub fn assert_eq(&self, expected: &str) { - if expected == self.neovim && self.neovim == self.editor { - return; - } - - let message = if expected != self.neovim { - "Test is incorrect (currently expected != neovim_state)" - } else { - "Editor does not match nvim behavior" - }; - - panic!( - indoc! {"{} - # initial state: - {} - # keystrokes: - {} - # currently expected: {:?} - # neovim register \"{}: {:?} - # zed register \"{}: {:?}"}, - message, - self.state.initial, - self.state.recent_keystrokes, - expected, - self.register, - self.neovim, - self.register, - self.editor - ) - } -} - -impl NeovimBackedTestContext { - pub async fn new(cx: &mut gpui::TestAppContext) -> NeovimBackedTestContext { - #[cfg(feature = "neovim")] - cx.executor().allow_parking(); - // rust stores the name of the test on the current thread. - // We use this to automatically name a file that will store - // the neovim connection's requests/responses so that we can - // run without neovim on CI. - let thread = thread::current(); - let test_name = thread - .name() - .expect("thread is not named") - .split(':') - .next_back() - .unwrap() - .to_string(); - Self { - cx: VimTestContext::new(cx, true).await, - neovim: NeovimConnection::new(test_name).await, - - last_set_state: None, - recent_keystrokes: Default::default(), - } - } - - pub async fn new_html(cx: &mut gpui::TestAppContext) -> NeovimBackedTestContext { - #[cfg(feature = "neovim")] - cx.executor().allow_parking(); - // rust stores the name of the test on the current thread. - // We use this to automatically name a file that will store - // the neovim connection's requests/responses so that we can - // run without neovim on CI. - let thread = thread::current(); - let test_name = thread - .name() - .expect("thread is not named") - .split(':') - .next_back() - .unwrap() - .to_string(); - Self { - cx: VimTestContext::new_html(cx).await, - neovim: NeovimConnection::new(test_name).await, - - last_set_state: None, - recent_keystrokes: Default::default(), - } - } - - pub async fn new_markdown_with_rust(cx: &mut gpui::TestAppContext) -> NeovimBackedTestContext { - #[cfg(feature = "neovim")] - cx.executor().allow_parking(); - let thread = thread::current(); - let test_name = thread - .name() - .expect("thread is not named") - .split(':') - .next_back() - .unwrap() - .to_string(); - Self { - cx: VimTestContext::new_markdown_with_rust(cx).await, - neovim: NeovimConnection::new(test_name).await, - - last_set_state: None, - recent_keystrokes: Default::default(), - } - } - - pub async fn new_typescript(cx: &mut gpui::TestAppContext) -> NeovimBackedTestContext { - #[cfg(feature = "neovim")] - cx.executor().allow_parking(); - // rust stores the name of the test on the current thread. - // We use this to automatically name a file that will store - // the neovim connection's requests/responses so that we can - // run without neovim on CI. - let thread = thread::current(); - let test_name = thread - .name() - .expect("thread is not named") - .split(':') - .next_back() - .unwrap() - .to_string(); - Self { - cx: VimTestContext::new_typescript(cx).await, - neovim: NeovimConnection::new(test_name).await, - - last_set_state: None, - recent_keystrokes: Default::default(), - } - } - - pub async fn new_tsx(cx: &mut gpui::TestAppContext) -> NeovimBackedTestContext { - #[cfg(feature = "neovim")] - cx.executor().allow_parking(); - let thread = thread::current(); - let test_name = thread - .name() - .expect("thread is not named") - .split(':') - .next_back() - .unwrap() - .to_string(); - Self { - cx: VimTestContext::new_tsx(cx).await, - neovim: NeovimConnection::new(test_name).await, - - last_set_state: None, - recent_keystrokes: Default::default(), - } - } - - pub async fn set_shared_state(&mut self, marked_text: &str) { - let mode = if marked_text.contains('»') { - Mode::Visual - } else { - Mode::Normal - }; - self.set_state(marked_text, mode); - self.last_set_state = Some(marked_text.to_string()); - self.recent_keystrokes = Vec::new(); - self.neovim.set_state(marked_text).await; - } - - pub async fn simulate_shared_keystrokes(&mut self, keystroke_texts: &str) { - for keystroke_text in keystroke_texts.split(' ') { - self.recent_keystrokes.push(keystroke_text.to_string()); - self.neovim.send_keystroke(keystroke_text).await; - } - self.simulate_keystrokes(keystroke_texts); - } - - #[must_use] - pub async fn simulate(&mut self, keystrokes: &str, initial_state: &str) -> SharedState { - self.set_shared_state(initial_state).await; - self.simulate_shared_keystrokes(keystrokes).await; - self.shared_state().await - } - - pub async fn set_shared_wrap(&mut self, columns: u32) { - if columns < 12 { - panic!("nvim doesn't support columns < 12") - } - self.neovim.set_option("wrap").await; - self.neovim - .set_option(&format!("columns={}", columns)) - .await; - - self.update(|_, cx| { - SettingsStore::update_global(cx, |settings, cx| { - settings.update_user_settings(cx, |settings| { - settings.project.all_languages.defaults.soft_wrap = - Some(SoftWrap::PreferredLineLength); - settings - .project - .all_languages - .defaults - .preferred_line_length = Some(columns); - }); - }) - }) - } - - pub async fn set_scroll_height(&mut self, rows: u32) { - // match Zed's scrolling behavior - self.neovim.set_option(&format!("scrolloff={}", 3)).await; - // +2 to account for the vim command UI at the bottom. - self.neovim.set_option(&format!("lines={}", rows + 2)).await; - let (line_height, visible_line_count) = self.update_editor(|editor, window, cx| { - ( - editor - .style(cx) - .text - .line_height_in_pixels(window.rem_size()), - editor.visible_line_count().unwrap(), - ) - }); - - let window = self.window; - let margin = self - .update_window(window, |_, window, _cx| { - window.viewport_size().height - line_height * (visible_line_count as f32) - }) - .unwrap(); - - self.simulate_window_resize( - self.window, - size(px(1000.), margin + (rows as f32) * line_height), - ); - } - - pub async fn set_neovim_option(&mut self, option: &str) { - self.neovim.set_option(option).await; - } - - #[must_use] - pub async fn shared_clipboard(&mut self) -> SharedClipboard { - SharedClipboard { - register: '"', - state: self.shared_state().await, - neovim: self.neovim.read_register('"').await, - editor: self.read_from_clipboard().unwrap().text().unwrap(), - } - } - - #[must_use] - pub async fn shared_register(&mut self, register: char) -> SharedClipboard { - SharedClipboard { - register, - state: self.shared_state().await, - neovim: self.neovim.read_register(register).await, - editor: self.update(|_, cx| { - cx.global::() - .registers - .get(®ister) - .cloned() - .unwrap_or_default() - .text - .into() - }), - } - } - - #[must_use] - pub async fn shared_state(&mut self) -> SharedState { - let (mode, marked_text) = self.neovim.state().await; - SharedState { - neovim: marked_text, - neovim_mode: mode, - editor: self.editor_state(), - editor_mode: self.mode(), - initial: self - .last_set_state - .as_ref() - .cloned() - .unwrap_or("N/A".to_string()), - recent_keystrokes: self.recent_keystrokes.join(" "), - } - } - - #[must_use] - pub async fn simulate_at_each_offset( - &mut self, - keystrokes: &str, - marked_positions: &str, - ) -> SharedState { - let (unmarked_text, cursor_offsets) = marked_text_offsets(marked_positions); - - for cursor_offset in cursor_offsets.iter() { - let mut marked_text = unmarked_text.clone(); - marked_text.insert(*cursor_offset, 'ˇ'); - - let state = self.simulate(keystrokes, &marked_text).await; - if state.neovim != state.editor || state.neovim_mode != state.editor_mode { - return state; - } - } - - SharedState::default() - } -} - -impl Deref for NeovimBackedTestContext { - type Target = VimTestContext; - - fn deref(&self) -> &Self::Target { - &self.cx - } -} - -impl DerefMut for NeovimBackedTestContext { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.cx - } -} - -#[cfg(test)] -mod test { - use crate::test::NeovimBackedTestContext; - use gpui::TestAppContext; - - #[gpui::test] - async fn neovim_backed_test_context_works(cx: &mut TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.shared_state().await.assert_matches(); - cx.set_shared_state("This is a tesˇt").await; - cx.shared_state().await.assert_matches(); - } -} diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs deleted file mode 100644 index dbc4068507..0000000000 --- a/crates/vim/src/test/neovim_connection.rs +++ /dev/null @@ -1,624 +0,0 @@ -use std::path::PathBuf; -#[cfg(feature = "neovim")] -use std::{ - cmp, - ops::{Deref, DerefMut, Range}, -}; - -#[cfg(feature = "neovim")] -use async_compat::Compat; -#[cfg(feature = "neovim")] -use async_trait::async_trait; -#[cfg(feature = "neovim")] -use gpui::Keystroke; - -#[cfg(feature = "neovim")] -use language::Point; - -#[cfg(feature = "neovim")] -use nvim_rs::{ - Handler, Neovim, UiAttachOptions, Value, create::tokio::new_child_cmd, error::LoopError, -}; -#[cfg(feature = "neovim")] -use parking_lot::ReentrantMutex; -use serde::{Deserialize, Serialize}; -#[cfg(feature = "neovim")] -use tokio::{ - process::{Child, ChildStdin, Command}, - task::JoinHandle, -}; - -use crate::state::Mode; -use collections::VecDeque; - -// Neovim doesn't like to be started simultaneously from multiple threads. We use this lock -// to ensure we are only constructing one neovim connection at a time. -#[cfg(feature = "neovim")] -static NEOVIM_LOCK: ReentrantMutex<()> = ReentrantMutex::new(()); - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub enum NeovimData { - Put { state: String }, - Key(String), - Get { state: String, mode: Mode }, - ReadRegister { name: char, value: String }, - Exec { command: String }, - SetOption { value: String }, -} - -pub struct NeovimConnection { - data: VecDeque, - #[cfg(feature = "neovim")] - test_case_id: String, - #[cfg(feature = "neovim")] - nvim: Neovim>, - #[cfg(feature = "neovim")] - _join_handle: JoinHandle>>, - #[cfg(feature = "neovim")] - _child: Child, -} - -impl NeovimConnection { - pub async fn new(mut test_case_id: String) -> Self { - // When running under perf, don't create duplicate files. - if cfg!(perf_enabled) { - if test_case_id.ends_with(perf::consts::SUF_NORMAL) { - test_case_id.truncate(test_case_id.len() - perf::consts::SUF_NORMAL.len()); - } - } - #[cfg(feature = "neovim")] - let handler = NvimHandler {}; - #[cfg(feature = "neovim")] - let (nvim, join_handle, child) = Compat::new(async { - // Ensure we don't create neovim connections in parallel - let _lock = NEOVIM_LOCK.lock(); - let (nvim, join_handle, child) = new_child_cmd( - Command::new("nvim") - .arg("--embed") - .arg("--clean") - // disable swap (otherwise after about 1000 test runs you run out of swap file names) - .arg("-n") - // disable writing files (just in case) - .arg("-m"), - handler, - ) - .await - .expect("Could not connect to neovim process"); - - nvim.ui_attach(100, 100, &UiAttachOptions::default()) - .await - .expect("Could not attach to ui"); - - // Makes system act a little more like zed in terms of indentation - nvim.set_option("smartindent", nvim_rs::Value::Boolean(true)) - .await - .expect("Could not set smartindent on startup"); - - (nvim, join_handle, child) - }) - .await; - - Self { - #[cfg(feature = "neovim")] - data: Default::default(), - #[cfg(not(feature = "neovim"))] - data: Self::read_test_data(&test_case_id), - #[cfg(feature = "neovim")] - test_case_id, - #[cfg(feature = "neovim")] - nvim, - #[cfg(feature = "neovim")] - _join_handle: join_handle, - #[cfg(feature = "neovim")] - _child: child, - } - } - - // Sends a keystroke to the neovim process. - #[cfg(feature = "neovim")] - pub async fn send_keystroke(&mut self, keystroke_text: &str) { - let mut keystroke = Keystroke::parse(keystroke_text).unwrap(); - - if keystroke.key == "<" { - keystroke.key = "lt".to_string() - } - - let special = keystroke.modifiers.shift - || keystroke.modifiers.control - || keystroke.modifiers.alt - || keystroke.modifiers.platform - || keystroke.key.len() > 1; - let start = if special { "<" } else { "" }; - let shift = if keystroke.modifiers.shift { "S-" } else { "" }; - let ctrl = if keystroke.modifiers.control { - "C-" - } else { - "" - }; - let alt = if keystroke.modifiers.alt { "M-" } else { "" }; - let cmd = if keystroke.modifiers.platform { - "D-" - } else { - "" - }; - let end = if special { ">" } else { "" }; - - let key = format!("{start}{shift}{ctrl}{alt}{cmd}{}{end}", keystroke.key); - - self.data - .push_back(NeovimData::Key(keystroke_text.to_string())); - self.nvim - .input(&key) - .await - .expect("Could not input keystroke"); - } - - #[cfg(not(feature = "neovim"))] - pub async fn send_keystroke(&mut self, keystroke_text: &str) { - if matches!(self.data.front(), Some(NeovimData::Get { .. })) { - self.data.pop_front(); - } - assert_eq!( - self.data.pop_front(), - Some(NeovimData::Key(keystroke_text.to_string())), - "operation does not match recorded script. re-record with --features=neovim" - ); - } - - #[cfg(feature = "neovim")] - pub async fn set_state(&mut self, marked_text: &str) { - let (text, selections) = parse_state(marked_text); - - let nvim_buffer = self - .nvim - .get_current_buf() - .await - .expect("Could not get neovim buffer"); - let lines = text - .split('\n') - .map(|line| line.to_string()) - .collect::>(); - - nvim_buffer - .set_lines(0, -1, false, lines) - .await - .expect("Could not set nvim buffer text"); - - self.nvim - .input("") - .await - .expect("Could not send escape to nvim"); - self.nvim - .input("") - .await - .expect("Could not send escape to nvim"); - - let nvim_window = self - .nvim - .get_current_win() - .await - .expect("Could not get neovim window"); - - if selections.len() != 1 { - panic!("must have one selection"); - } - let selection = &selections[0]; - - let cursor = selection.start; - nvim_window - .set_cursor((cursor.row as i64 + 1, cursor.column as i64)) - .await - .expect("Could not set nvim cursor position"); - - if !selection.is_empty() { - self.nvim - .input("v") - .await - .expect("could not enter visual mode"); - - let cursor = selection.end; - nvim_window - .set_cursor((cursor.row as i64 + 1, cursor.column as i64)) - .await - .expect("Could not set nvim cursor position"); - } - - if let Some(NeovimData::Get { mode, state }) = self.data.back() - && *mode == Mode::Normal - && *state == marked_text - { - return; - } - self.data.push_back(NeovimData::Put { - state: marked_text.to_string(), - }) - } - - #[cfg(not(feature = "neovim"))] - pub async fn set_state(&mut self, marked_text: &str) { - if let Some(NeovimData::Get { mode, state: text }) = self.data.front() { - if *mode == Mode::Normal && *text == marked_text { - return; - } - self.data.pop_front(); - } - assert_eq!( - self.data.pop_front(), - Some(NeovimData::Put { - state: marked_text.to_string() - }), - "operation does not match recorded script. re-record with --features=neovim" - ); - } - - #[cfg(feature = "neovim")] - pub async fn set_option(&mut self, value: &str) { - self.nvim - .command_output(format!("set {}", value).as_str()) - .await - .unwrap(); - - self.data.push_back(NeovimData::SetOption { - value: value.to_string(), - }) - } - - #[cfg(not(feature = "neovim"))] - pub async fn set_option(&mut self, value: &str) { - if let Some(NeovimData::Get { .. }) = self.data.front() { - self.data.pop_front(); - }; - assert_eq!( - self.data.pop_front(), - Some(NeovimData::SetOption { - value: value.to_string(), - }), - "operation does not match recorded script. re-record with --features=neovim" - ); - } - - #[cfg(feature = "neovim")] - pub async fn exec(&mut self, value: &str) { - self.nvim.command_output(value).await.unwrap(); - - self.data.push_back(NeovimData::Exec { - command: value.to_string(), - }) - } - - #[cfg(not(feature = "neovim"))] - pub async fn exec(&mut self, value: &str) { - if let Some(NeovimData::Get { .. }) = self.data.front() { - self.data.pop_front(); - }; - assert_eq!( - self.data.pop_front(), - Some(NeovimData::Exec { - command: value.to_string(), - }), - "operation does not match recorded script. re-record with --features=neovim" - ); - } - - #[cfg(not(feature = "neovim"))] - pub async fn read_register(&mut self, register: char) -> String { - if let Some(NeovimData::Get { .. }) = self.data.front() { - self.data.pop_front(); - }; - if let Some(NeovimData::ReadRegister { name, value }) = self.data.pop_front() - && name == register - { - return value; - } - - panic!("operation does not match recorded script. re-record with --features=neovim") - } - - #[cfg(feature = "neovim")] - pub async fn read_register(&mut self, name: char) -> String { - let value = self - .nvim - .command_output(format!("echo getreg('{}')", name).as_str()) - .await - .unwrap(); - - self.data.push_back(NeovimData::ReadRegister { - name, - value: value.clone(), - }); - - value - } - - #[cfg(feature = "neovim")] - async fn read_position(&mut self, cmd: &str) -> u32 { - self.nvim - .command_output(cmd) - .await - .unwrap() - .parse::() - .unwrap() - } - - #[cfg(feature = "neovim")] - pub async fn state(&mut self) -> (Mode, String) { - let nvim_buffer = self - .nvim - .get_current_buf() - .await - .expect("Could not get neovim buffer"); - let text = nvim_buffer - .get_lines(0, -1, false) - .await - .expect("Could not get buffer text") - .join("\n"); - - // nvim columns are 1-based, so -1. - let mut cursor_row = self.read_position("echo line('.')").await - 1; - let mut cursor_col = self.read_position("echo col('.')").await - 1; - let mut selection_row = self.read_position("echo line('v')").await - 1; - let mut selection_col = self.read_position("echo col('v')").await - 1; - let total_rows = self.read_position("echo line('$')").await - 1; - - let nvim_mode_text = self - .nvim - .get_mode() - .await - .expect("Could not get mode") - .into_iter() - .find_map(|(key, value)| { - if key.as_str() == Some("mode") { - Some(value.as_str().unwrap().to_owned()) - } else { - None - } - }) - .expect("Could not find mode value"); - - let mode = match nvim_mode_text.as_ref() { - "i" => Mode::Insert, - "n" => Mode::Normal, - "v" => Mode::Visual, - "V" => Mode::VisualLine, - "R" => Mode::Replace, - "\x16" => Mode::VisualBlock, - _ => panic!("unexpected vim mode: {nvim_mode_text}"), - }; - - let mut selections = Vec::new(); - // Vim uses the index of the first and last character in the selection - // Zed uses the index of the positions between the characters, so we need - // to add one to the end in visual mode. - match mode { - Mode::VisualBlock if selection_row != cursor_row => { - // in zed we fake a block selection by using multiple cursors (one per line) - // this code emulates that. - // to deal with casees where the selection is not perfectly rectangular we extract - // the content of the selection via the "a register to get the shape correctly. - self.nvim.input("\"aygv").await.unwrap(); - let content = self.nvim.command_output("echo getreg('a')").await.unwrap(); - let lines = content.split('\n').collect::>(); - let top = cmp::min(selection_row, cursor_row); - let left = cmp::min(selection_col, cursor_col); - for row in top..=cmp::max(selection_row, cursor_row) { - let content = if row - top >= lines.len() as u32 { - "" - } else { - lines[(row - top) as usize] - }; - let line_len = self - .read_position(format!("echo strlen(getline({}))", row + 1).as_str()) - .await; - - if left > line_len { - continue; - } - - let start = Point::new(row, left); - let end = Point::new(row, left + content.len() as u32); - if cursor_col >= selection_col { - selections.push(start..end) - } else { - selections.push(end..start) - } - } - } - Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { - if (selection_row, selection_col) > (cursor_row, cursor_col) { - let selection_line_length = - self.read_position("echo strlen(getline(line('v')))").await; - if selection_line_length > selection_col { - selection_col += 1; - } else if selection_row < total_rows { - selection_col = 0; - selection_row += 1; - } - } else { - let cursor_line_length = - self.read_position("echo strlen(getline(line('.')))").await; - if cursor_line_length > cursor_col { - cursor_col += 1; - } else if cursor_row < total_rows { - cursor_col = 0; - cursor_row += 1; - } - } - selections.push( - Point::new(selection_row, selection_col)..Point::new(cursor_row, cursor_col), - ) - } - Mode::Insert | Mode::Normal | Mode::Replace => selections - .push(Point::new(selection_row, selection_col)..Point::new(cursor_row, cursor_col)), - Mode::HelixNormal | Mode::HelixSelect => unreachable!(), - } - - let ranges = encode_ranges(&text, &selections); - let state = NeovimData::Get { - mode, - state: ranges.clone(), - }; - - if self.data.back() != Some(&state) { - self.data.push_back(state); - } - - (mode, ranges) - } - - #[cfg(not(feature = "neovim"))] - pub async fn state(&mut self) -> (Mode, String) { - if let Some(NeovimData::Get { state: raw, mode }) = self.data.front() { - (*mode, raw.to_string()) - } else { - panic!("operation does not match recorded script. re-record with --features=neovim"); - } - } - - fn test_data_path(test_case_id: &str) -> PathBuf { - let mut data_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - data_path.push("test_data"); - data_path.push(format!("{}.json", test_case_id)); - data_path - } - - #[cfg(not(feature = "neovim"))] - fn read_test_data(test_case_id: &str) -> VecDeque { - let path = Self::test_data_path(test_case_id); - let json = std::fs::read_to_string(path).expect( - "Could not read test data. Is it generated? Try running test with '--features neovim'", - ); - - let mut result = VecDeque::new(); - for line in json.lines() { - result.push_back( - serde_json::from_str(line) - .expect("invalid test data. regenerate it with '--features neovim'"), - ); - } - result - } - - #[cfg(feature = "neovim")] - fn write_test_data(test_case_id: &str, data: &VecDeque) { - let path = Self::test_data_path(test_case_id); - let mut json = Vec::new(); - for entry in data { - serde_json::to_writer(&mut json, entry).unwrap(); - json.push(b'\n'); - } - std::fs::create_dir_all(path.parent().unwrap()) - .expect("could not create test data directory"); - std::fs::write(path, json).expect("could not write out test data"); - } -} - -#[cfg(feature = "neovim")] -impl Deref for NeovimConnection { - type Target = Neovim>; - - fn deref(&self) -> &Self::Target { - &self.nvim - } -} - -#[cfg(feature = "neovim")] -impl DerefMut for NeovimConnection { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.nvim - } -} - -#[cfg(feature = "neovim")] -impl Drop for NeovimConnection { - fn drop(&mut self) { - Self::write_test_data(&self.test_case_id, &self.data); - } -} - -#[cfg(feature = "neovim")] -#[derive(Clone)] -struct NvimHandler {} - -#[cfg(feature = "neovim")] -#[async_trait] -impl Handler for NvimHandler { - type Writer = nvim_rs::compat::tokio::Compat; - - async fn handle_request( - &self, - _event_name: String, - _arguments: Vec, - _neovim: Neovim, - ) -> Result { - unimplemented!(); - } - - async fn handle_notify( - &self, - _event_name: String, - _arguments: Vec, - _neovim: Neovim, - ) { - } -} - -#[cfg(feature = "neovim")] -fn parse_state(marked_text: &str) -> (String, Vec>) { - let (text, ranges) = util::test::marked_text_ranges(marked_text, true); - let point_ranges = ranges - .into_iter() - .map(|byte_range| { - let mut point_range = Point::zero()..Point::zero(); - let mut ix = 0; - let mut position = Point::zero(); - for c in text.chars().chain(['\0']) { - if ix == byte_range.start { - point_range.start = position; - } - if ix == byte_range.end { - point_range.end = position; - } - let len_utf8 = c.len_utf8(); - ix += len_utf8; - if c == '\n' { - position.row += 1; - position.column = 0; - } else { - position.column += len_utf8 as u32; - } - } - point_range - }) - .collect::>(); - (text, point_ranges) -} - -#[cfg(feature = "neovim")] -fn encode_ranges(text: &str, point_ranges: &Vec>) -> String { - let byte_ranges = point_ranges - .iter() - .map(|range| { - let mut byte_range = 0..0; - let mut ix = 0; - let mut position = Point::zero(); - for c in text.chars().chain(['\0']) { - if position == range.start { - byte_range.start = ix; - } - if position == range.end { - byte_range.end = ix; - } - let len_utf8 = c.len_utf8(); - ix += len_utf8; - if c == '\n' { - position.row += 1; - position.column = 0; - } else { - position.column += len_utf8 as u32; - } - } - byte_range - }) - .collect::>(); - util::test::generate_marked_text(text, &byte_ranges[..], true) -} diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs deleted file mode 100644 index acd77839f2..0000000000 --- a/crates/vim/src/test/vim_test_context.rs +++ /dev/null @@ -1,289 +0,0 @@ -use std::ops::{Deref, DerefMut}; - -use editor::test::editor_lsp_test_context::EditorLspTestContext; -use gpui::{Context, Entity, UpdateGlobal}; -use search::{BufferSearchBar, project_search::ProjectSearchBar}; -use semver::Version; - -use crate::{state::Operator, *}; - -pub struct VimTestContext { - cx: EditorLspTestContext, -} - -impl VimTestContext { - pub fn init(cx: &mut gpui::TestAppContext) { - if cx.has_global::() { - return; - } - env_logger::try_init().ok(); - cx.update(|cx| { - let settings = SettingsStore::test(cx); - cx.set_global(settings); - release_channel::init(Version::new(0, 0, 0), cx); - command_palette::init(cx); - project_panel::init(cx); - git_ui::init(cx); - crate::init(cx); - search::init(cx); - theme::init(theme::LoadThemes::JustBase, cx); - settings_ui::init(cx); - markdown_preview::init(cx); - }); - } - - pub async fn new(cx: &mut gpui::TestAppContext, enabled: bool) -> VimTestContext { - Self::init(cx); - let lsp = EditorLspTestContext::new_rust(Default::default(), cx).await; - Self::new_with_lsp(lsp, enabled) - } - - pub async fn new_html(cx: &mut gpui::TestAppContext) -> VimTestContext { - Self::init(cx); - Self::new_with_lsp(EditorLspTestContext::new_html(cx).await, true) - } - - pub async fn new_markdown_with_rust(cx: &mut gpui::TestAppContext) -> VimTestContext { - Self::init(cx); - Self::new_with_lsp(EditorLspTestContext::new_markdown_with_rust(cx).await, true) - } - - pub async fn new_typescript(cx: &mut gpui::TestAppContext) -> VimTestContext { - Self::init(cx); - Self::new_with_lsp( - EditorLspTestContext::new_typescript( - lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string()]), - ..Default::default() - }), - rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions { - prepare_provider: Some(true), - work_done_progress_options: Default::default(), - })), - definition_provider: Some(lsp::OneOf::Left(true)), - ..Default::default() - }, - cx, - ) - .await, - true, - ) - } - - pub async fn new_tsx(cx: &mut gpui::TestAppContext) -> VimTestContext { - Self::init(cx); - Self::new_with_lsp( - EditorLspTestContext::new_tsx( - lsp::ServerCapabilities { - completion_provider: Some(lsp::CompletionOptions { - trigger_characters: Some(vec![".".to_string()]), - ..Default::default() - }), - rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions { - prepare_provider: Some(true), - work_done_progress_options: Default::default(), - })), - ..Default::default() - }, - cx, - ) - .await, - true, - ) - } - - pub fn init_keybindings(enabled: bool, cx: &mut App) { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |s| s.vim_mode = Some(enabled)); - }); - let mut default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure( - "keymaps/default-macos.json", - cx, - ) - .unwrap(); - for key_binding in &mut default_key_bindings { - key_binding.set_meta(settings::KeybindSource::Default.meta()); - } - cx.bind_keys(default_key_bindings); - if enabled { - let vim_key_bindings = settings::KeymapFile::load_asset( - "keymaps/vim.json", - Some(settings::KeybindSource::Vim), - cx, - ) - .unwrap(); - cx.bind_keys(vim_key_bindings); - } - } - - pub fn new_with_lsp(mut cx: EditorLspTestContext, enabled: bool) -> VimTestContext { - cx.update(|_, cx| { - Self::init_keybindings(enabled, cx); - }); - - // Setup search toolbars and keypress hook - cx.update_workspace(|workspace, window, cx| { - workspace.active_pane().update(cx, |pane, cx| { - pane.toolbar().update(cx, |toolbar, cx| { - let buffer_search_bar = cx.new(|cx| BufferSearchBar::new(None, window, cx)); - toolbar.add_item(buffer_search_bar, window, cx); - - let project_search_bar = cx.new(|_| ProjectSearchBar::new()); - toolbar.add_item(project_search_bar, window, cx); - }) - }); - workspace.status_bar().update(cx, |status_bar, cx| { - let vim_mode_indicator = cx.new(|cx| ModeIndicator::new(window, cx)); - status_bar.add_right_item(vim_mode_indicator, window, cx); - }); - }); - - Self { cx } - } - - pub fn update_entity(&mut self, entity: Entity, update: F) -> R - where - T: 'static, - F: FnOnce(&mut T, &mut Window, &mut Context) -> R + 'static, - { - let window = self.window; - self.update_window(window, move |_, window, cx| { - entity.update(cx, |t, cx| update(t, window, cx)) - }) - .unwrap() - } - - pub fn workspace(&mut self, update: F) -> T - where - F: FnOnce(&mut Workspace, &mut Window, &mut Context) -> T, - { - self.cx.update_workspace(update) - } - - pub fn enable_vim(&mut self) { - self.cx.update(|_, cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |s| s.vim_mode = Some(true)); - }); - }) - } - - pub fn disable_vim(&mut self) { - self.cx.update(|_, cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |s| s.vim_mode = Some(false)); - }); - }) - } - - pub fn enable_helix(&mut self) { - self.cx.update(|_, cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |s| s.helix_mode = Some(true)); - }); - }) - } - - pub fn mode(&mut self) -> Mode { - self.update_editor(|editor, _, cx| editor.addon::().unwrap().entity.read(cx).mode) - } - - pub fn forced_motion(&mut self) -> bool { - self.update_editor(|_, _, cx| cx.global::().forced_motion) - } - - pub fn active_operator(&mut self) -> Option { - self.update_editor(|editor, _, cx| { - editor - .addon::() - .unwrap() - .entity - .read(cx) - .operator_stack - .last() - .cloned() - }) - } - - pub fn set_state(&mut self, text: &str, mode: Mode) { - self.cx.set_state(text); - let vim = - self.update_editor(|editor, _window, _cx| editor.addon::().cloned().unwrap()); - - self.update(|window, cx| { - vim.entity.update(cx, |vim, cx| { - vim.switch_mode(mode, true, window, cx); - }); - }); - self.cx.cx.cx.run_until_parked(); - } - - #[track_caller] - pub fn assert_state(&mut self, text: &str, mode: Mode) { - self.assert_editor_state(text); - assert_eq!(self.mode(), mode, "{}", self.assertion_context()); - } - - pub fn assert_binding( - &mut self, - keystrokes: &str, - initial_state: &str, - initial_mode: Mode, - state_after: &str, - mode_after: Mode, - ) { - self.set_state(initial_state, initial_mode); - self.cx.simulate_keystrokes(keystrokes); - self.cx.assert_editor_state(state_after); - assert_eq!(self.mode(), mode_after, "{}", self.assertion_context()); - assert_eq!(self.active_operator(), None, "{}", self.assertion_context()); - } - - pub fn assert_binding_normal( - &mut self, - keystrokes: &str, - initial_state: &str, - state_after: &str, - ) { - self.set_state(initial_state, Mode::Normal); - self.cx.simulate_keystrokes(keystrokes); - self.cx.assert_editor_state(state_after); - assert_eq!(self.mode(), Mode::Normal, "{}", self.assertion_context()); - assert_eq!(self.active_operator(), None, "{}", self.assertion_context()); - } - - pub fn shared_clipboard(&mut self) -> VimClipboard { - VimClipboard { - editor: self - .read_from_clipboard() - .map(|item| item.text().unwrap()) - .unwrap_or_default(), - } - } -} - -pub struct VimClipboard { - editor: String, -} - -impl VimClipboard { - #[track_caller] - pub fn assert_eq(&self, expected: &str) { - assert_eq!(self.editor, expected); - } -} - -impl Deref for VimTestContext { - type Target = EditorLspTestContext; - - fn deref(&self) -> &Self::Target { - &self.cx - } -} - -impl DerefMut for VimTestContext { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.cx - } -} diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs deleted file mode 100644 index 9a9a1a001c..0000000000 --- a/crates/vim/src/vim.rs +++ /dev/null @@ -1,2022 +0,0 @@ -//! Vim support for Zed. - -#[cfg(test)] -mod test; - -mod change_list; -mod command; -mod digraph; -mod helix; -mod indent; -mod insert; -mod mode_indicator; -mod motion; -mod normal; -mod object; -mod replace; -mod rewrap; -mod state; -mod surrounds; -mod visual; - -use crate::normal::paste::Paste as VimPaste; -use collections::HashMap; -use editor::{ - Anchor, Bias, Editor, EditorEvent, EditorSettings, HideMouseCursorOrigin, MultiBufferOffset, - SelectionEffects, ToPoint, - actions::Paste, - movement::{self, FindRange}, -}; -use gpui::{ - Action, App, AppContext, Axis, Context, Entity, EventEmitter, KeyContext, KeystrokeEvent, - Render, Subscription, Task, WeakEntity, Window, actions, -}; -use insert::{NormalBefore, TemporaryNormal}; -use language::{ - CharKind, CharScopeContext, CursorShape, Point, Selection, SelectionGoal, TransactionId, -}; -pub use mode_indicator::ModeIndicator; -use motion::Motion; -use normal::search::SearchSubmit; -use object::Object; -use schemars::JsonSchema; -use serde::Deserialize; -use settings::RegisterSetting; -pub use settings::{ - ModeContent, Settings, SettingsStore, UseSystemClipboard, update_settings_file, -}; -use state::{Mode, Operator, RecordedSelection, SearchState, VimGlobals}; -use std::{mem, ops::Range, sync::Arc}; -use surrounds::SurroundsType; -use theme::ThemeSettings; -use ui::{IntoElement, SharedString, px}; -use vim_mode_setting::HelixModeSetting; -use vim_mode_setting::VimModeSetting; -use workspace::{self, Pane, Workspace}; - -use crate::{ - normal::{GoToPreviousTab, GoToTab}, - state::ReplayableAction, -}; - -/// Number is used to manage vim's count. Pushing a digit -/// multiplies the current value by 10 and adds the digit. -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -struct Number(usize); - -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -struct SelectRegister(String); - -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct PushObject { - around: bool, -} - -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct PushFindForward { - before: bool, - multiline: bool, -} - -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct PushFindBackward { - after: bool, - multiline: bool, -} - -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -/// Selects the next object. -struct PushHelixNext { - around: bool, -} - -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -/// Selects the previous object. -struct PushHelixPrevious { - around: bool, -} - -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct PushSneak { - first_char: Option, -} - -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct PushSneakBackward { - first_char: Option, -} - -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct PushAddSurrounds; - -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct PushChangeSurrounds { - target: Option, -} - -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct PushJump { - line: bool, -} - -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct PushDigraph { - first_char: Option, -} - -#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)] -#[action(namespace = vim)] -#[serde(deny_unknown_fields)] -struct PushLiteral { - prefix: Option, -} - -actions!( - vim, - [ - /// Switches to normal mode. - SwitchToNormalMode, - /// Switches to insert mode. - SwitchToInsertMode, - /// Switches to replace mode. - SwitchToReplaceMode, - /// Switches to visual mode. - SwitchToVisualMode, - /// Switches to visual line mode. - SwitchToVisualLineMode, - /// Switches to visual block mode. - SwitchToVisualBlockMode, - /// Switches to Helix-style normal mode. - SwitchToHelixNormalMode, - /// Clears any pending operators. - ClearOperators, - /// Clears the exchange register. - ClearExchange, - /// Inserts a tab character. - Tab, - /// Inserts a newline. - Enter, - /// Selects inner text object. - InnerObject, - /// Maximizes the current pane. - MaximizePane, - /// Resets all pane sizes to default. - ResetPaneSizes, - /// Resizes the pane to the right. - ResizePaneRight, - /// Resizes the pane to the left. - ResizePaneLeft, - /// Resizes the pane upward. - ResizePaneUp, - /// Resizes the pane downward. - ResizePaneDown, - /// Starts a change operation. - PushChange, - /// Starts a delete operation. - PushDelete, - /// Exchanges text regions. - Exchange, - /// Starts a yank operation. - PushYank, - /// Starts a replace operation. - PushReplace, - /// Deletes surrounding characters. - PushDeleteSurrounds, - /// Sets a mark at the current position. - PushMark, - /// Toggles the marks view. - ToggleMarksView, - /// Starts a forced motion. - PushForcedMotion, - /// Starts an indent operation. - PushIndent, - /// Starts an outdent operation. - PushOutdent, - /// Starts an auto-indent operation. - PushAutoIndent, - /// Starts a rewrap operation. - PushRewrap, - /// Starts a shell command operation. - PushShellCommand, - /// Converts to lowercase. - PushLowercase, - /// Converts to uppercase. - PushUppercase, - /// Toggles case. - PushOppositeCase, - /// Applies ROT13 encoding. - PushRot13, - /// Applies ROT47 encoding. - PushRot47, - /// Toggles the registers view. - ToggleRegistersView, - /// Selects a register. - PushRegister, - /// Starts recording to a register. - PushRecordRegister, - /// Replays a register. - PushReplayRegister, - /// Replaces with register contents. - PushReplaceWithRegister, - /// Toggles comments. - PushToggleComments, - /// Selects (count) next menu item - MenuSelectNext, - /// Selects (count) previous menu item - MenuSelectPrevious, - /// Clears count or toggles project panel focus - ToggleProjectPanelFocus, - /// Starts a match operation. - PushHelixMatch, - ] -); - -// in the workspace namespace so it's not filtered out when vim is disabled. -actions!( - workspace, - [ - /// Toggles Vim mode on or off. - ToggleVimMode, - /// Toggles Helix mode on or off. - ToggleHelixMode, - ] -); - -/// Initializes the `vim` crate. -pub fn init(cx: &mut App) { - VimGlobals::register(cx); - - cx.observe_new(Vim::register).detach(); - - cx.observe_new(|workspace: &mut Workspace, _, _| { - workspace.register_action(|workspace, _: &ToggleVimMode, _, cx| { - let fs = workspace.app_state().fs.clone(); - let currently_enabled = VimModeSetting::get_global(cx).0; - update_settings_file(fs, cx, move |setting, _| { - setting.vim_mode = Some(!currently_enabled); - if let Some(helix_mode) = &mut setting.helix_mode { - *helix_mode = false; - } - }) - }); - - workspace.register_action(|workspace, _: &ToggleHelixMode, _, cx| { - let fs = workspace.app_state().fs.clone(); - let currently_enabled = HelixModeSetting::get_global(cx).0; - update_settings_file(fs, cx, move |setting, _| { - setting.helix_mode = Some(!currently_enabled); - if let Some(vim_mode) = &mut setting.vim_mode { - *vim_mode = false; - } - }) - }); - - workspace.register_action(|_, _: &MenuSelectNext, window, cx| { - let count = Vim::take_count(cx).unwrap_or(1); - - for _ in 0..count { - window.dispatch_action(menu::SelectNext.boxed_clone(), cx); - } - }); - - workspace.register_action(|_, _: &MenuSelectPrevious, window, cx| { - let count = Vim::take_count(cx).unwrap_or(1); - - for _ in 0..count { - window.dispatch_action(menu::SelectPrevious.boxed_clone(), cx); - } - }); - - workspace.register_action(|_, _: &ToggleProjectPanelFocus, window, cx| { - if Vim::take_count(cx).is_none() { - window.dispatch_action(zed_actions::project_panel::ToggleFocus.boxed_clone(), cx); - } - }); - - workspace.register_action(|workspace, n: &Number, window, cx| { - let vim = workspace - .focused_pane(window, cx) - .read(cx) - .active_item() - .and_then(|item| item.act_as::(cx)) - .and_then(|editor| editor.read(cx).addon::().cloned()); - if let Some(vim) = vim { - let digit = n.0; - vim.entity.update(cx, |_, cx| { - cx.defer_in(window, move |vim, window, cx| { - vim.push_count_digit(digit, window, cx) - }) - }); - } else { - let count = Vim::globals(cx).pre_count.unwrap_or(0); - Vim::globals(cx).pre_count = Some( - count - .checked_mul(10) - .and_then(|c| c.checked_add(n.0)) - .unwrap_or(count), - ); - }; - }); - - workspace.register_action(|_, _: &zed_actions::vim::OpenDefaultKeymap, _, cx| { - cx.emit(workspace::Event::OpenBundledFile { - text: settings::vim_keymap(), - title: "Default Vim Bindings", - language: "JSON", - }); - }); - - workspace.register_action(|workspace, _: &ResetPaneSizes, _, cx| { - workspace.reset_pane_sizes(cx); - }); - - workspace.register_action(|workspace, _: &MaximizePane, window, cx| { - let pane = workspace.active_pane(); - let Some(size) = workspace.bounding_box_for_pane(pane) else { - return; - }; - - let theme = ThemeSettings::get_global(cx); - let height = theme.buffer_font_size(cx) * theme.buffer_line_height.value(); - - let desired_size = if let Some(count) = Vim::take_count(cx) { - height * count - } else { - px(10000.) - }; - workspace.resize_pane(Axis::Vertical, desired_size - size.size.height, window, cx) - }); - - workspace.register_action(|workspace, _: &ResizePaneRight, window, cx| { - let count = Vim::take_count(cx).unwrap_or(1) as f32; - Vim::take_forced_motion(cx); - let theme = ThemeSettings::get_global(cx); - let font_id = window.text_system().resolve_font(&theme.buffer_font); - let Ok(width) = window - .text_system() - .advance(font_id, theme.buffer_font_size(cx), 'm') - else { - return; - }; - workspace.resize_pane(Axis::Horizontal, width.width * count, window, cx); - }); - - workspace.register_action(|workspace, _: &ResizePaneLeft, window, cx| { - let count = Vim::take_count(cx).unwrap_or(1) as f32; - Vim::take_forced_motion(cx); - let theme = ThemeSettings::get_global(cx); - let font_id = window.text_system().resolve_font(&theme.buffer_font); - let Ok(width) = window - .text_system() - .advance(font_id, theme.buffer_font_size(cx), 'm') - else { - return; - }; - workspace.resize_pane(Axis::Horizontal, -width.width * count, window, cx); - }); - - workspace.register_action(|workspace, _: &ResizePaneUp, window, cx| { - let count = Vim::take_count(cx).unwrap_or(1) as f32; - Vim::take_forced_motion(cx); - let theme = ThemeSettings::get_global(cx); - let height = theme.buffer_font_size(cx) * theme.buffer_line_height.value(); - workspace.resize_pane(Axis::Vertical, height * count, window, cx); - }); - - workspace.register_action(|workspace, _: &ResizePaneDown, window, cx| { - let count = Vim::take_count(cx).unwrap_or(1) as f32; - Vim::take_forced_motion(cx); - let theme = ThemeSettings::get_global(cx); - let height = theme.buffer_font_size(cx) * theme.buffer_line_height.value(); - workspace.resize_pane(Axis::Vertical, -height * count, window, cx); - }); - - workspace.register_action(|workspace, _: &SearchSubmit, window, cx| { - let vim = workspace - .focused_pane(window, cx) - .read(cx) - .active_item() - .and_then(|item| item.act_as::(cx)) - .and_then(|editor| editor.read(cx).addon::().cloned()); - let Some(vim) = vim else { return }; - vim.entity.update(cx, |_, cx| { - cx.defer_in(window, |vim, window, cx| vim.search_submit(window, cx)) - }) - }); - workspace.register_action(|_, _: &GoToTab, window, cx| { - let count = Vim::take_count(cx); - Vim::take_forced_motion(cx); - - if let Some(tab_index) = count { - // gt goes to tab (1-based). - let zero_based_index = tab_index.saturating_sub(1); - window.dispatch_action( - workspace::pane::ActivateItem(zero_based_index).boxed_clone(), - cx, - ); - } else { - // If no count is provided, go to the next tab. - window.dispatch_action(workspace::pane::ActivateNextItem.boxed_clone(), cx); - } - }); - - workspace.register_action(|workspace, _: &GoToPreviousTab, window, cx| { - let count = Vim::take_count(cx); - Vim::take_forced_motion(cx); - - if let Some(count) = count { - // gT with count goes back that many tabs with wraparound (not the same as gt!). - let pane = workspace.active_pane().read(cx); - let item_count = pane.items().count(); - if item_count > 0 { - let current_index = pane.active_item_index(); - let target_index = (current_index as isize - count as isize) - .rem_euclid(item_count as isize) - as usize; - window.dispatch_action( - workspace::pane::ActivateItem(target_index).boxed_clone(), - cx, - ); - } - } else { - // No count provided, go to the previous tab. - window.dispatch_action(workspace::pane::ActivatePreviousItem.boxed_clone(), cx); - } - }); - }) - .detach(); -} - -#[derive(Clone)] -pub(crate) struct VimAddon { - pub(crate) entity: Entity, -} - -impl editor::Addon for VimAddon { - fn extend_key_context(&self, key_context: &mut KeyContext, cx: &App) { - self.entity.read(cx).extend_key_context(key_context, cx) - } - - fn to_any(&self) -> &dyn std::any::Any { - self - } -} - -/// The state pertaining to Vim mode. -pub(crate) struct Vim { - pub(crate) mode: Mode, - pub last_mode: Mode, - pub temp_mode: bool, - pub status_label: Option, - pub exit_temporary_mode: bool, - - operator_stack: Vec, - pub(crate) replacements: Vec<(Range, String)>, - - pub(crate) stored_visual_mode: Option<(Mode, Vec)>, - - pub(crate) current_tx: Option, - pub(crate) current_anchor: Option>, - pub(crate) undo_modes: HashMap, - pub(crate) undo_last_line_tx: Option, - - selected_register: Option, - pub search: SearchState, - - editor: WeakEntity, - - last_command: Option, - running_command: Option>, - _subscriptions: Vec, -} - -// Hack: Vim intercepts events dispatched to a window and updates the view in response. -// This means it needs a VisualContext. The easiest way to satisfy that constraint is -// to make Vim a "View" that is just never actually rendered. -impl Render for Vim { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - gpui::Empty - } -} - -enum VimEvent { - Focused, -} -impl EventEmitter for Vim {} - -impl Vim { - /// The namespace for Vim actions. - const NAMESPACE: &'static str = "vim"; - - pub fn new(window: &mut Window, cx: &mut Context) -> Entity { - let editor = cx.entity(); - - let initial_vim_mode = VimSettings::get_global(cx).default_mode; - let (mode, last_mode) = if HelixModeSetting::get_global(cx).0 { - let initial_helix_mode = match initial_vim_mode { - Mode::Normal => Mode::HelixNormal, - Mode::Insert => Mode::Insert, - // Otherwise, we panic with a note that we should never get there due to the - // possible values of VimSettings::get_global(cx).default_mode being either Mode::Normal or Mode::Insert. - _ => unreachable!("Invalid default mode"), - }; - (initial_helix_mode, Mode::HelixNormal) - } else { - (initial_vim_mode, Mode::Normal) - }; - - cx.new(|cx| Vim { - mode, - last_mode, - temp_mode: false, - exit_temporary_mode: false, - operator_stack: Vec::new(), - replacements: Vec::new(), - - stored_visual_mode: None, - current_tx: None, - undo_last_line_tx: None, - current_anchor: None, - undo_modes: HashMap::default(), - - status_label: None, - selected_register: None, - search: SearchState::default(), - - last_command: None, - running_command: None, - - editor: editor.downgrade(), - _subscriptions: vec![ - cx.observe_keystrokes(Self::observe_keystrokes), - cx.subscribe_in(&editor, window, |this, _, event, window, cx| { - this.handle_editor_event(event, window, cx) - }), - ], - }) - } - - fn register(editor: &mut Editor, window: Option<&mut Window>, cx: &mut Context) { - let Some(window) = window else { - return; - }; - - if !editor.use_modal_editing() { - return; - } - - let mut was_enabled = Vim::enabled(cx); - let mut was_toggle = VimSettings::get_global(cx).toggle_relative_line_numbers; - cx.observe_global_in::(window, move |editor, window, cx| { - let enabled = Vim::enabled(cx); - let toggle = VimSettings::get_global(cx).toggle_relative_line_numbers; - if enabled && was_enabled && (toggle != was_toggle) { - if toggle { - let is_relative = editor - .addon::() - .map(|vim| vim.entity.read(cx).mode != Mode::Insert); - editor.set_relative_line_number(is_relative, cx) - } else { - editor.set_relative_line_number(None, cx) - } - } - was_toggle = VimSettings::get_global(cx).toggle_relative_line_numbers; - if was_enabled == enabled { - return; - } - was_enabled = enabled; - if enabled { - Self::activate(editor, window, cx) - } else { - Self::deactivate(editor, cx) - } - }) - .detach(); - if was_enabled { - Self::activate(editor, window, cx) - } - } - - fn activate(editor: &mut Editor, window: &mut Window, cx: &mut Context) { - let vim = Vim::new(window, cx); - - if !editor.mode().is_full() { - vim.update(cx, |vim, _| { - vim.mode = Mode::Insert; - }); - } - - editor.register_addon(VimAddon { - entity: vim.clone(), - }); - - vim.update(cx, |_, cx| { - Vim::action(editor, cx, |vim, _: &SwitchToNormalMode, window, cx| { - vim.switch_mode(Mode::Normal, false, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &SwitchToInsertMode, window, cx| { - vim.switch_mode(Mode::Insert, false, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &SwitchToReplaceMode, window, cx| { - vim.switch_mode(Mode::Replace, false, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &SwitchToVisualMode, window, cx| { - vim.switch_mode(Mode::Visual, false, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &SwitchToVisualLineMode, window, cx| { - vim.switch_mode(Mode::VisualLine, false, window, cx) - }); - - Vim::action( - editor, - cx, - |vim, _: &SwitchToVisualBlockMode, window, cx| { - vim.switch_mode(Mode::VisualBlock, false, window, cx) - }, - ); - - Vim::action( - editor, - cx, - |vim, _: &SwitchToHelixNormalMode, window, cx| { - vim.switch_mode(Mode::HelixNormal, true, window, cx) - }, - ); - Vim::action(editor, cx, |_, _: &PushForcedMotion, _, cx| { - Vim::globals(cx).forced_motion = true; - }); - Vim::action(editor, cx, |vim, action: &PushObject, window, cx| { - vim.push_operator( - Operator::Object { - around: action.around, - }, - window, - cx, - ) - }); - - Vim::action(editor, cx, |vim, action: &PushFindForward, window, cx| { - vim.push_operator( - Operator::FindForward { - before: action.before, - multiline: action.multiline, - }, - window, - cx, - ) - }); - - Vim::action(editor, cx, |vim, action: &PushFindBackward, window, cx| { - vim.push_operator( - Operator::FindBackward { - after: action.after, - multiline: action.multiline, - }, - window, - cx, - ) - }); - - Vim::action(editor, cx, |vim, action: &PushSneak, window, cx| { - vim.push_operator( - Operator::Sneak { - first_char: action.first_char, - }, - window, - cx, - ) - }); - - Vim::action(editor, cx, |vim, action: &PushSneakBackward, window, cx| { - vim.push_operator( - Operator::SneakBackward { - first_char: action.first_char, - }, - window, - cx, - ) - }); - - Vim::action(editor, cx, |vim, _: &PushAddSurrounds, window, cx| { - vim.push_operator(Operator::AddSurrounds { target: None }, window, cx) - }); - - Vim::action( - editor, - cx, - |vim, action: &PushChangeSurrounds, window, cx| { - vim.push_operator( - Operator::ChangeSurrounds { - target: action.target, - opening: false, - }, - window, - cx, - ) - }, - ); - - Vim::action(editor, cx, |vim, action: &PushJump, window, cx| { - vim.push_operator(Operator::Jump { line: action.line }, window, cx) - }); - - Vim::action(editor, cx, |vim, action: &PushDigraph, window, cx| { - vim.push_operator( - Operator::Digraph { - first_char: action.first_char, - }, - window, - cx, - ) - }); - - Vim::action(editor, cx, |vim, action: &PushLiteral, window, cx| { - vim.push_operator( - Operator::Literal { - prefix: action.prefix.clone(), - }, - window, - cx, - ) - }); - - Vim::action(editor, cx, |vim, _: &PushChange, window, cx| { - vim.push_operator(Operator::Change, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushDelete, window, cx| { - vim.push_operator(Operator::Delete, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushYank, window, cx| { - vim.push_operator(Operator::Yank, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushReplace, window, cx| { - vim.push_operator(Operator::Replace, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushDeleteSurrounds, window, cx| { - vim.push_operator(Operator::DeleteSurrounds, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushMark, window, cx| { - vim.push_operator(Operator::Mark, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushIndent, window, cx| { - vim.push_operator(Operator::Indent, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushOutdent, window, cx| { - vim.push_operator(Operator::Outdent, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushAutoIndent, window, cx| { - vim.push_operator(Operator::AutoIndent, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushRewrap, window, cx| { - vim.push_operator(Operator::Rewrap, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushShellCommand, window, cx| { - vim.push_operator(Operator::ShellCommand, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushLowercase, window, cx| { - vim.push_operator(Operator::Lowercase, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushUppercase, window, cx| { - vim.push_operator(Operator::Uppercase, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushOppositeCase, window, cx| { - vim.push_operator(Operator::OppositeCase, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushRot13, window, cx| { - vim.push_operator(Operator::Rot13, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushRot47, window, cx| { - vim.push_operator(Operator::Rot47, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushRegister, window, cx| { - vim.push_operator(Operator::Register, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushRecordRegister, window, cx| { - vim.push_operator(Operator::RecordRegister, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushReplayRegister, window, cx| { - vim.push_operator(Operator::ReplayRegister, window, cx) - }); - - Vim::action( - editor, - cx, - |vim, _: &PushReplaceWithRegister, window, cx| { - vim.push_operator(Operator::ReplaceWithRegister, window, cx) - }, - ); - - Vim::action(editor, cx, |vim, _: &Exchange, window, cx| { - if vim.mode.is_visual() { - vim.exchange_visual(window, cx) - } else { - vim.push_operator(Operator::Exchange, window, cx) - } - }); - - Vim::action(editor, cx, |vim, _: &ClearExchange, window, cx| { - vim.clear_exchange(window, cx) - }); - - Vim::action(editor, cx, |vim, _: &PushToggleComments, window, cx| { - vim.push_operator(Operator::ToggleComments, window, cx) - }); - - Vim::action(editor, cx, |vim, _: &ClearOperators, window, cx| { - vim.clear_operator(window, cx) - }); - Vim::action(editor, cx, |vim, n: &Number, window, cx| { - vim.push_count_digit(n.0, window, cx); - }); - Vim::action(editor, cx, |vim, _: &Tab, window, cx| { - vim.input_ignored(" ".into(), window, cx) - }); - Vim::action( - editor, - cx, - |vim, action: &editor::actions::AcceptEditPrediction, window, cx| { - vim.update_editor(cx, |_, editor, cx| { - editor.accept_edit_prediction(action, window, cx); - }); - // In non-insertion modes, predictions will be hidden and instead a jump will be - // displayed (and performed by `accept_edit_prediction`). This switches to - // insert mode so that the prediction is displayed after the jump. - match vim.mode { - Mode::Replace => {} - _ => vim.switch_mode(Mode::Insert, true, window, cx), - }; - }, - ); - Vim::action(editor, cx, |vim, _: &Enter, window, cx| { - vim.input_ignored("\n".into(), window, cx) - }); - Vim::action(editor, cx, |vim, _: &PushHelixMatch, window, cx| { - vim.push_operator(Operator::HelixMatch, window, cx) - }); - Vim::action(editor, cx, |vim, action: &PushHelixNext, window, cx| { - vim.push_operator( - Operator::HelixNext { - around: action.around, - }, - window, - cx, - ); - }); - Vim::action(editor, cx, |vim, action: &PushHelixPrevious, window, cx| { - vim.push_operator( - Operator::HelixPrevious { - around: action.around, - }, - window, - cx, - ); - }); - - Vim::action( - editor, - cx, - |vim, _: &editor::actions::Paste, window, cx| match vim.mode { - Mode::Replace => vim.paste_replace(window, cx), - Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { - vim.selected_register.replace('+'); - vim.paste(&VimPaste::default(), window, cx); - } - _ => { - vim.update_editor(cx, |_, editor, cx| editor.paste(&Paste, window, cx)); - } - }, - ); - - normal::register(editor, cx); - insert::register(editor, cx); - helix::register(editor, cx); - motion::register(editor, cx); - command::register(editor, cx); - replace::register(editor, cx); - indent::register(editor, cx); - rewrap::register(editor, cx); - object::register(editor, cx); - visual::register(editor, cx); - change_list::register(editor, cx); - digraph::register(editor, cx); - - if editor.is_focused(window) { - cx.defer_in(window, |vim, window, cx| { - vim.focused(false, window, cx); - }) - } - }) - } - - fn deactivate(editor: &mut Editor, cx: &mut Context) { - editor.set_cursor_shape( - EditorSettings::get_global(cx) - .cursor_shape - .unwrap_or_default(), - cx, - ); - editor.set_clip_at_line_ends(false, cx); - editor.set_collapse_matches(false); - editor.set_input_enabled(true); - editor.set_autoindent(true); - editor.selections.set_line_mode(false); - editor.unregister_addon::(); - editor.set_relative_line_number(None, cx); - if let Some(vim) = Vim::globals(cx).focused_vim() - && vim.entity_id() == cx.entity().entity_id() - { - Vim::globals(cx).focused_vim = None; - } - } - - /// Register an action on the editor. - pub fn action( - editor: &mut Editor, - cx: &mut Context, - f: impl Fn(&mut Vim, &A, &mut Window, &mut Context) + 'static, - ) { - let subscription = editor.register_action(cx.listener(f)); - cx.on_release(|_, _| drop(subscription)).detach(); - } - - pub fn editor(&self) -> Option> { - self.editor.upgrade() - } - - pub fn workspace(&self, window: &mut Window) -> Option> { - window.root::().flatten() - } - - pub fn pane(&self, window: &mut Window, cx: &mut Context) -> Option> { - self.workspace(window) - .map(|workspace| workspace.read(cx).focused_pane(window, cx)) - } - - pub fn enabled(cx: &mut App) -> bool { - VimModeSetting::get_global(cx).0 || HelixModeSetting::get_global(cx).0 - } - - /// Called whenever an keystroke is typed so vim can observe all actions - /// and keystrokes accordingly. - fn observe_keystrokes( - &mut self, - keystroke_event: &KeystrokeEvent, - window: &mut Window, - cx: &mut Context, - ) { - if self.exit_temporary_mode { - self.exit_temporary_mode = false; - // Don't switch to insert mode if the action is temporary_normal. - if let Some(action) = keystroke_event.action.as_ref() - && action.as_any().downcast_ref::().is_some() - { - return; - } - self.switch_mode(Mode::Insert, false, window, cx) - } - if let Some(action) = keystroke_event.action.as_ref() { - // Keystroke is handled by the vim system, so continue forward - if action.name().starts_with("vim::") { - self.update_editor(cx, |_, editor, cx| { - editor.hide_mouse_cursor(HideMouseCursorOrigin::MovementAction, cx) - }); - - return; - } - } else if window.has_pending_keystrokes() || keystroke_event.keystroke.is_ime_in_progress() - { - return; - } - - if let Some(operator) = self.active_operator() { - match operator { - Operator::Literal { prefix } => { - self.handle_literal_keystroke( - keystroke_event, - prefix.unwrap_or_default(), - window, - cx, - ); - } - _ if !operator.is_waiting(self.mode) => { - self.clear_operator(window, cx); - self.stop_recording_immediately(Box::new(ClearOperators), cx) - } - _ => {} - } - } - } - - fn handle_editor_event( - &mut self, - event: &EditorEvent, - window: &mut Window, - cx: &mut Context, - ) { - match event { - EditorEvent::Focused => self.focused(true, window, cx), - EditorEvent::Blurred => self.blurred(window, cx), - EditorEvent::SelectionsChanged { local: true } => { - self.local_selections_changed(window, cx); - } - EditorEvent::InputIgnored { text } => { - self.input_ignored(text.clone(), window, cx); - Vim::globals(cx).observe_insertion(text, None) - } - EditorEvent::InputHandled { - text, - utf16_range_to_replace: range_to_replace, - } => Vim::globals(cx).observe_insertion(text, range_to_replace.clone()), - EditorEvent::TransactionBegun { transaction_id } => { - self.transaction_begun(*transaction_id, window, cx) - } - EditorEvent::TransactionUndone { transaction_id } => { - self.transaction_undone(transaction_id, window, cx) - } - EditorEvent::Edited { .. } => self.push_to_change_list(window, cx), - EditorEvent::FocusedIn => self.sync_vim_settings(window, cx), - EditorEvent::CursorShapeChanged => self.cursor_shape_changed(window, cx), - EditorEvent::PushedToNavHistory { - anchor, - is_deactivate, - } => { - self.update_editor(cx, |vim, editor, cx| { - let mark = if *is_deactivate { - "\"".to_string() - } else { - "'".to_string() - }; - vim.set_mark(mark, vec![*anchor], editor.buffer(), window, cx); - }); - } - _ => {} - } - } - - fn push_operator(&mut self, operator: Operator, window: &mut Window, cx: &mut Context) { - if operator.starts_dot_recording() { - self.start_recording(cx); - } - // Since these operations can only be entered with pre-operators, - // we need to clear the previous operators when pushing, - // so that the current stack is the most correct - if matches!( - operator, - Operator::AddSurrounds { .. } - | Operator::ChangeSurrounds { .. } - | Operator::DeleteSurrounds - | Operator::Exchange - ) { - self.operator_stack.clear(); - }; - self.operator_stack.push(operator); - self.sync_vim_settings(window, cx); - } - - pub fn switch_mode( - &mut self, - mode: Mode, - leave_selections: bool, - window: &mut Window, - cx: &mut Context, - ) { - if self.temp_mode && mode == Mode::Normal { - self.temp_mode = false; - self.switch_mode(Mode::Normal, leave_selections, window, cx); - self.switch_mode(Mode::Insert, false, window, cx); - return; - } else if self.temp_mode - && !matches!(mode, Mode::Visual | Mode::VisualLine | Mode::VisualBlock) - { - self.temp_mode = false; - } - - let last_mode = self.mode; - let prior_mode = self.last_mode; - let prior_tx = self.current_tx; - self.status_label.take(); - self.last_mode = last_mode; - self.mode = mode; - self.operator_stack.clear(); - self.selected_register.take(); - self.cancel_running_command(window, cx); - if mode == Mode::Normal || mode != last_mode { - self.current_tx.take(); - self.current_anchor.take(); - self.update_editor(cx, |_, editor, _| { - editor.clear_selection_drag_state(); - }); - } - Vim::take_forced_motion(cx); - if mode != Mode::Insert && mode != Mode::Replace { - Vim::take_count(cx); - } - - // Sync editor settings like clip mode - self.sync_vim_settings(window, cx); - - if VimSettings::get_global(cx).toggle_relative_line_numbers - && self.mode != self.last_mode - && (self.mode == Mode::Insert || self.last_mode == Mode::Insert) - { - self.update_editor(cx, |vim, editor, cx| { - let is_relative = vim.mode != Mode::Insert; - editor.set_relative_line_number(Some(is_relative), cx) - }); - } - if HelixModeSetting::get_global(cx).0 { - if self.mode == Mode::Normal { - self.mode = Mode::HelixNormal - } else if self.mode == Mode::Visual { - self.mode = Mode::HelixSelect - } - } - - if leave_selections { - return; - } - - if !mode.is_visual() && last_mode.is_visual() { - self.create_visual_marks(last_mode, window, cx); - } - - // Adjust selections - self.update_editor(cx, |vim, editor, cx| { - if last_mode != Mode::VisualBlock && last_mode.is_visual() && mode == Mode::VisualBlock - { - vim.visual_block_motion(true, editor, window, cx, |_, point, goal| { - Some((point, goal)) - }) - } - if (last_mode == Mode::Insert || last_mode == Mode::Replace) - && let Some(prior_tx) = prior_tx - { - editor.group_until_transaction(prior_tx, cx) - } - - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - // we cheat with visual block mode and use multiple cursors. - // the cost of this cheat is we need to convert back to a single - // cursor whenever vim would. - if last_mode == Mode::VisualBlock - && (mode != Mode::VisualBlock && mode != Mode::Insert) - { - let tail = s.oldest_anchor().tail(); - let head = s.newest_anchor().head(); - s.select_anchor_ranges(vec![tail..head]); - } else if last_mode == Mode::Insert - && prior_mode == Mode::VisualBlock - && mode != Mode::VisualBlock - { - let pos = s.first_anchor().head(); - s.select_anchor_ranges(vec![pos..pos]) - } - - let snapshot = s.display_snapshot(); - if let Some(pending) = s.pending_anchor_mut() - && pending.reversed - && mode.is_visual() - && !last_mode.is_visual() - { - let mut end = pending.end.to_point(&snapshot.buffer_snapshot()); - end = snapshot - .buffer_snapshot() - .clip_point(end + Point::new(0, 1), Bias::Right); - pending.end = snapshot.buffer_snapshot().anchor_before(end); - } - - s.move_with(|map, selection| { - if last_mode.is_visual() && !mode.is_visual() { - let mut point = selection.head(); - if !selection.reversed && !selection.is_empty() { - point = movement::left(map, selection.head()); - } else if selection.is_empty() { - point = map.clip_point(point, Bias::Left); - } - selection.collapse_to(point, selection.goal) - } else if !last_mode.is_visual() && mode.is_visual() && selection.is_empty() { - selection.end = movement::right(map, selection.start); - } - }); - }) - }); - } - - pub fn take_count(cx: &mut App) -> Option { - let global_state = cx.global_mut::(); - if global_state.dot_replaying { - return global_state.recorded_count; - } - - let count = if global_state.post_count.is_none() && global_state.pre_count.is_none() { - return None; - } else { - Some( - global_state.post_count.take().unwrap_or(1) - * global_state.pre_count.take().unwrap_or(1), - ) - }; - - if global_state.dot_recording { - global_state.recording_count = count; - } - count - } - - pub fn take_forced_motion(cx: &mut App) -> bool { - let global_state = cx.global_mut::(); - let forced_motion = global_state.forced_motion; - global_state.forced_motion = false; - forced_motion - } - - pub fn cursor_shape(&self, cx: &mut App) -> CursorShape { - let cursor_shape = VimSettings::get_global(cx).cursor_shape; - match self.mode { - Mode::Normal => { - if let Some(operator) = self.operator_stack.last() { - match operator { - // Navigation operators -> Block cursor - Operator::FindForward { .. } - | Operator::FindBackward { .. } - | Operator::Mark - | Operator::Jump { .. } - | Operator::Register - | Operator::RecordRegister - | Operator::ReplayRegister => CursorShape::Block, - - // All other operators -> Underline cursor - _ => CursorShape::Underline, - } - } else { - cursor_shape.normal.unwrap_or(CursorShape::Block) - } - } - Mode::HelixNormal => cursor_shape.normal.unwrap_or(CursorShape::Block), - Mode::Replace => cursor_shape.replace.unwrap_or(CursorShape::Underline), - Mode::Visual | Mode::VisualLine | Mode::VisualBlock | Mode::HelixSelect => { - cursor_shape.visual.unwrap_or(CursorShape::Block) - } - Mode::Insert => cursor_shape.insert.unwrap_or({ - let editor_settings = EditorSettings::get_global(cx); - editor_settings.cursor_shape.unwrap_or_default() - }), - } - } - - pub fn editor_input_enabled(&self) -> bool { - match self.mode { - Mode::Insert => { - if let Some(operator) = self.operator_stack.last() { - !operator.is_waiting(self.mode) - } else { - true - } - } - Mode::Normal - | Mode::HelixNormal - | Mode::Replace - | Mode::Visual - | Mode::VisualLine - | Mode::VisualBlock - | Mode::HelixSelect => false, - } - } - - pub fn should_autoindent(&self) -> bool { - !(self.mode == Mode::Insert && self.last_mode == Mode::VisualBlock) - } - - pub fn clip_at_line_ends(&self) -> bool { - match self.mode { - Mode::Insert - | Mode::Visual - | Mode::VisualLine - | Mode::VisualBlock - | Mode::Replace - | Mode::HelixNormal - | Mode::HelixSelect => false, - Mode::Normal => true, - } - } - - pub fn extend_key_context(&self, context: &mut KeyContext, cx: &App) { - let mut mode = match self.mode { - Mode::Normal => "normal", - Mode::Visual | Mode::VisualLine | Mode::VisualBlock => "visual", - Mode::Insert => "insert", - Mode::Replace => "replace", - Mode::HelixNormal => "helix_normal", - Mode::HelixSelect => "helix_select", - } - .to_string(); - - let mut operator_id = "none"; - - let active_operator = self.active_operator(); - if active_operator.is_none() && cx.global::().pre_count.is_some() - || active_operator.is_some() && cx.global::().post_count.is_some() - { - context.add("VimCount"); - } - - if let Some(active_operator) = active_operator { - if active_operator.is_waiting(self.mode) { - if matches!(active_operator, Operator::Literal { .. }) { - mode = "literal".to_string(); - } else { - mode = "waiting".to_string(); - } - } else { - operator_id = active_operator.id(); - mode = "operator".to_string(); - } - } - - if mode == "normal" - || mode == "visual" - || mode == "operator" - || mode == "helix_normal" - || mode == "helix_select" - { - context.add("VimControl"); - } - context.set("vim_mode", mode); - context.set("vim_operator", operator_id); - } - - fn focused(&mut self, preserve_selection: bool, window: &mut Window, cx: &mut Context) { - let Some(editor) = self.editor() else { - return; - }; - let newest_selection_empty = editor.update(cx, |editor, cx| { - editor - .selections - .newest::(&editor.display_snapshot(cx)) - .is_empty() - }); - let editor = editor.read(cx); - let editor_mode = editor.mode(); - - if editor_mode.is_full() - && !newest_selection_empty - && self.mode == Mode::Normal - // When following someone, don't switch vim mode. - && editor.leader_id().is_none() - { - if preserve_selection { - self.switch_mode(Mode::Visual, true, window, cx); - } else { - self.update_editor(cx, |_, editor, cx| { - editor.set_clip_at_line_ends(false, cx); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|_, selection| { - selection.collapse_to(selection.start, selection.goal) - }) - }); - }); - } - } - - cx.emit(VimEvent::Focused); - self.sync_vim_settings(window, cx); - - if VimSettings::get_global(cx).toggle_relative_line_numbers { - if let Some(old_vim) = Vim::globals(cx).focused_vim() { - if old_vim.entity_id() != cx.entity().entity_id() { - old_vim.update(cx, |vim, cx| { - vim.update_editor(cx, |_, editor, cx| { - editor.set_relative_line_number(None, cx) - }); - }); - - self.update_editor(cx, |vim, editor, cx| { - let is_relative = vim.mode != Mode::Insert; - editor.set_relative_line_number(Some(is_relative), cx) - }); - } - } else { - self.update_editor(cx, |vim, editor, cx| { - let is_relative = vim.mode != Mode::Insert; - editor.set_relative_line_number(Some(is_relative), cx) - }); - } - } - Vim::globals(cx).focused_vim = Some(cx.entity().downgrade()); - } - - fn blurred(&mut self, window: &mut Window, cx: &mut Context) { - self.stop_recording_immediately(NormalBefore.boxed_clone(), cx); - self.store_visual_marks(window, cx); - self.clear_operator(window, cx); - self.update_editor(cx, |vim, editor, cx| { - if vim.cursor_shape(cx) == CursorShape::Block { - editor.set_cursor_shape(CursorShape::Hollow, cx); - } - }); - } - - fn cursor_shape_changed(&mut self, _: &mut Window, cx: &mut Context) { - self.update_editor(cx, |vim, editor, cx| { - editor.set_cursor_shape(vim.cursor_shape(cx), cx); - }); - } - - fn update_editor( - &mut self, - cx: &mut Context, - update: impl FnOnce(&mut Self, &mut Editor, &mut Context) -> S, - ) -> Option { - let editor = self.editor.upgrade()?; - Some(editor.update(cx, |editor, cx| update(self, editor, cx))) - } - - fn editor_selections(&mut self, _: &mut Window, cx: &mut Context) -> Vec> { - self.update_editor(cx, |_, editor, _| { - editor - .selections - .disjoint_anchors_arc() - .iter() - .map(|selection| selection.tail()..selection.head()) - .collect() - }) - .unwrap_or_default() - } - - fn editor_cursor_word( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> Option { - self.update_editor(cx, |_, editor, cx| { - let snapshot = &editor.snapshot(window, cx); - let selection = editor - .selections - .newest::(&snapshot.display_snapshot); - - let snapshot = snapshot.buffer_snapshot(); - let (range, kind) = - snapshot.surrounding_word(selection.start, Some(CharScopeContext::Completion)); - if kind == Some(CharKind::Word) { - let text: String = snapshot.text_for_range(range).collect(); - if !text.trim().is_empty() { - return Some(text); - } - } - - None - }) - .unwrap_or_default() - } - - /// When doing an action that modifies the buffer, we start recording so that `.` - /// will replay the action. - pub fn start_recording(&mut self, cx: &mut Context) { - Vim::update_globals(cx, |globals, cx| { - if !globals.dot_replaying { - globals.dot_recording = true; - globals.recording_actions = Default::default(); - globals.recording_count = None; - - let selections = self.editor().map(|editor| { - editor.update(cx, |editor, cx| { - let snapshot = editor.display_snapshot(cx); - - ( - editor.selections.oldest::(&snapshot), - editor.selections.newest::(&snapshot), - ) - }) - }); - - if let Some((oldest, newest)) = selections { - globals.recorded_selection = match self.mode { - Mode::Visual if newest.end.row == newest.start.row => { - RecordedSelection::SingleLine { - cols: newest.end.column - newest.start.column, - } - } - Mode::Visual => RecordedSelection::Visual { - rows: newest.end.row - newest.start.row, - cols: newest.end.column, - }, - Mode::VisualLine => RecordedSelection::VisualLine { - rows: newest.end.row - newest.start.row, - }, - Mode::VisualBlock => RecordedSelection::VisualBlock { - rows: newest.end.row.abs_diff(oldest.start.row), - cols: newest.end.column.abs_diff(oldest.start.column), - }, - _ => RecordedSelection::None, - } - } else { - globals.recorded_selection = RecordedSelection::None; - } - } - }) - } - - pub fn stop_replaying(&mut self, cx: &mut Context) { - let globals = Vim::globals(cx); - globals.dot_replaying = false; - if let Some(replayer) = globals.replayer.take() { - replayer.stop(); - } - } - - /// When finishing an action that modifies the buffer, stop recording. - /// as you usually call this within a keystroke handler we also ensure that - /// the current action is recorded. - pub fn stop_recording(&mut self, cx: &mut Context) { - let globals = Vim::globals(cx); - if globals.dot_recording { - globals.stop_recording_after_next_action = true; - } - self.exit_temporary_mode = self.temp_mode; - } - - /// Stops recording actions immediately rather than waiting until after the - /// next action to stop recording. - /// - /// This doesn't include the current action. - pub fn stop_recording_immediately(&mut self, action: Box, cx: &mut Context) { - let globals = Vim::globals(cx); - if globals.dot_recording { - globals - .recording_actions - .push(ReplayableAction::Action(action.boxed_clone())); - globals.recorded_actions = mem::take(&mut globals.recording_actions); - globals.recorded_count = globals.recording_count.take(); - globals.dot_recording = false; - globals.stop_recording_after_next_action = false; - } - self.exit_temporary_mode = self.temp_mode; - } - - /// Explicitly record one action (equivalents to start_recording and stop_recording) - pub fn record_current_action(&mut self, cx: &mut Context) { - self.start_recording(cx); - self.stop_recording(cx); - } - - fn push_count_digit(&mut self, number: usize, window: &mut Window, cx: &mut Context) { - if self.active_operator().is_some() { - let post_count = Vim::globals(cx).post_count.unwrap_or(0); - - Vim::globals(cx).post_count = Some( - post_count - .checked_mul(10) - .and_then(|post_count| post_count.checked_add(number)) - .filter(|post_count| *post_count < isize::MAX as usize) - .unwrap_or(post_count), - ) - } else { - let pre_count = Vim::globals(cx).pre_count.unwrap_or(0); - - Vim::globals(cx).pre_count = Some( - pre_count - .checked_mul(10) - .and_then(|pre_count| pre_count.checked_add(number)) - .filter(|pre_count| *pre_count < isize::MAX as usize) - .unwrap_or(pre_count), - ) - } - // update the keymap so that 0 works - self.sync_vim_settings(window, cx) - } - - fn select_register(&mut self, register: Arc, window: &mut Window, cx: &mut Context) { - if register.chars().count() == 1 { - self.selected_register - .replace(register.chars().next().unwrap()); - } - self.operator_stack.clear(); - self.sync_vim_settings(window, cx); - } - - fn maybe_pop_operator(&mut self) -> Option { - self.operator_stack.pop() - } - - fn pop_operator(&mut self, window: &mut Window, cx: &mut Context) -> Operator { - let popped_operator = self.operator_stack.pop() - .expect("Operator popped when no operator was on the stack. This likely means there is an invalid keymap config"); - self.sync_vim_settings(window, cx); - popped_operator - } - - fn clear_operator(&mut self, window: &mut Window, cx: &mut Context) { - Vim::take_count(cx); - Vim::take_forced_motion(cx); - self.selected_register.take(); - self.operator_stack.clear(); - self.sync_vim_settings(window, cx); - } - - fn active_operator(&self) -> Option { - self.operator_stack.last().cloned() - } - - fn transaction_begun( - &mut self, - transaction_id: TransactionId, - _window: &mut Window, - _: &mut Context, - ) { - let mode = if (self.mode == Mode::Insert - || self.mode == Mode::Replace - || self.mode == Mode::Normal) - && self.current_tx.is_none() - { - self.current_tx = Some(transaction_id); - self.last_mode - } else { - self.mode - }; - if mode == Mode::VisualLine || mode == Mode::VisualBlock { - self.undo_modes.insert(transaction_id, mode); - } - } - - fn transaction_undone( - &mut self, - transaction_id: &TransactionId, - window: &mut Window, - cx: &mut Context, - ) { - match self.mode { - Mode::VisualLine | Mode::VisualBlock | Mode::Visual | Mode::HelixSelect => { - self.update_editor(cx, |vim, editor, cx| { - let original_mode = vim.undo_modes.get(transaction_id); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - match original_mode { - Some(Mode::VisualLine) => { - s.move_with(|map, selection| { - selection.collapse_to( - map.prev_line_boundary(selection.start.to_point(map)).1, - SelectionGoal::None, - ) - }); - } - Some(Mode::VisualBlock) => { - let mut first = s.first_anchor(); - first.collapse_to(first.start, first.goal); - s.select_anchors(vec![first]); - } - _ => { - s.move_with(|map, selection| { - selection.collapse_to( - map.clip_at_line_end(selection.start), - selection.goal, - ); - }); - } - } - }); - }); - self.switch_mode(Mode::Normal, true, window, cx) - } - Mode::Normal => { - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - selection - .collapse_to(map.clip_at_line_end(selection.end), selection.goal) - }) - }) - }); - } - Mode::Insert | Mode::Replace | Mode::HelixNormal => {} - } - } - - fn local_selections_changed(&mut self, window: &mut Window, cx: &mut Context) { - let Some(editor) = self.editor() else { return }; - - if editor.read(cx).leader_id().is_some() { - return; - } - - let newest = editor.read(cx).selections.newest_anchor().clone(); - let is_multicursor = editor.read(cx).selections.count() > 1; - if self.mode == Mode::Insert && self.current_tx.is_some() { - if self.current_anchor.is_none() { - self.current_anchor = Some(newest); - } else if self.current_anchor.as_ref().unwrap() != &newest - && let Some(tx_id) = self.current_tx.take() - { - self.update_editor(cx, |_, editor, cx| { - editor.group_until_transaction(tx_id, cx) - }); - } - } else if self.mode == Mode::Normal && newest.start != newest.end { - if matches!(newest.goal, SelectionGoal::HorizontalRange { .. }) { - self.switch_mode(Mode::VisualBlock, false, window, cx); - } else { - self.switch_mode(Mode::Visual, false, window, cx) - } - } else if newest.start == newest.end - && !is_multicursor - && [Mode::Visual, Mode::VisualLine, Mode::VisualBlock].contains(&self.mode) - { - self.switch_mode(Mode::Normal, false, window, cx); - } - } - - fn input_ignored(&mut self, text: Arc, window: &mut Window, cx: &mut Context) { - if text.is_empty() { - return; - } - - match self.active_operator() { - Some(Operator::FindForward { before, multiline }) => { - let find = Motion::FindForward { - before, - char: text.chars().next().unwrap(), - mode: if multiline { - FindRange::MultiLine - } else { - FindRange::SingleLine - }, - smartcase: VimSettings::get_global(cx).use_smartcase_find, - }; - Vim::globals(cx).last_find = Some(find.clone()); - self.motion(find, window, cx) - } - Some(Operator::FindBackward { after, multiline }) => { - let find = Motion::FindBackward { - after, - char: text.chars().next().unwrap(), - mode: if multiline { - FindRange::MultiLine - } else { - FindRange::SingleLine - }, - smartcase: VimSettings::get_global(cx).use_smartcase_find, - }; - Vim::globals(cx).last_find = Some(find.clone()); - self.motion(find, window, cx) - } - Some(Operator::Sneak { first_char }) => { - if let Some(first_char) = first_char { - if let Some(second_char) = text.chars().next() { - let sneak = Motion::Sneak { - first_char, - second_char, - smartcase: VimSettings::get_global(cx).use_smartcase_find, - }; - Vim::globals(cx).last_find = Some(sneak.clone()); - self.motion(sneak, window, cx) - } - } else { - let first_char = text.chars().next(); - self.pop_operator(window, cx); - self.push_operator(Operator::Sneak { first_char }, window, cx); - } - } - Some(Operator::SneakBackward { first_char }) => { - if let Some(first_char) = first_char { - if let Some(second_char) = text.chars().next() { - let sneak = Motion::SneakBackward { - first_char, - second_char, - smartcase: VimSettings::get_global(cx).use_smartcase_find, - }; - Vim::globals(cx).last_find = Some(sneak.clone()); - self.motion(sneak, window, cx) - } - } else { - let first_char = text.chars().next(); - self.pop_operator(window, cx); - self.push_operator(Operator::SneakBackward { first_char }, window, cx); - } - } - Some(Operator::Replace) => match self.mode { - Mode::Normal => self.normal_replace(text, window, cx), - Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { - self.visual_replace(text, window, cx) - } - Mode::HelixNormal => self.helix_replace(&text, window, cx), - _ => self.clear_operator(window, cx), - }, - Some(Operator::Digraph { first_char }) => { - if let Some(first_char) = first_char { - if let Some(second_char) = text.chars().next() { - self.insert_digraph(first_char, second_char, window, cx); - } - } else { - let first_char = text.chars().next(); - self.pop_operator(window, cx); - self.push_operator(Operator::Digraph { first_char }, window, cx); - } - } - Some(Operator::Literal { prefix }) => { - self.handle_literal_input(prefix.unwrap_or_default(), &text, window, cx) - } - Some(Operator::AddSurrounds { target }) => match self.mode { - Mode::Normal => { - if let Some(target) = target { - self.add_surrounds(text, target, window, cx); - self.clear_operator(window, cx); - } - } - Mode::Visual | Mode::VisualLine | Mode::VisualBlock => { - self.add_surrounds(text, SurroundsType::Selection, window, cx); - self.clear_operator(window, cx); - } - _ => self.clear_operator(window, cx), - }, - Some(Operator::ChangeSurrounds { target, opening }) => match self.mode { - Mode::Normal => { - if let Some(target) = target { - self.change_surrounds(text, target, opening, window, cx); - self.clear_operator(window, cx); - } - } - _ => self.clear_operator(window, cx), - }, - Some(Operator::DeleteSurrounds) => match self.mode { - Mode::Normal => { - self.delete_surrounds(text, window, cx); - self.clear_operator(window, cx); - } - _ => self.clear_operator(window, cx), - }, - Some(Operator::Mark) => self.create_mark(text, window, cx), - Some(Operator::RecordRegister) => { - self.record_register(text.chars().next().unwrap(), window, cx) - } - Some(Operator::ReplayRegister) => { - self.replay_register(text.chars().next().unwrap(), window, cx) - } - Some(Operator::Register) => match self.mode { - Mode::Insert => { - self.update_editor(cx, |_, editor, cx| { - if let Some(register) = Vim::update_globals(cx, |globals, cx| { - globals.read_register(text.chars().next(), Some(editor), cx) - }) { - editor.do_paste( - ®ister.text.to_string(), - register.clipboard_selections, - false, - window, - cx, - ) - } - }); - self.clear_operator(window, cx); - } - _ => { - self.select_register(text, window, cx); - } - }, - Some(Operator::Jump { line }) => self.jump(text, line, true, window, cx), - _ => { - if self.mode == Mode::Replace { - self.multi_replace(text, window, cx) - } - - if self.mode == Mode::Normal { - self.update_editor(cx, |_, editor, cx| { - editor.accept_edit_prediction( - &editor::actions::AcceptEditPrediction {}, - window, - cx, - ); - }); - } - } - } - } - - fn sync_vim_settings(&mut self, window: &mut Window, cx: &mut Context) { - self.update_editor(cx, |vim, editor, cx| { - editor.set_cursor_shape(vim.cursor_shape(cx), cx); - editor.set_clip_at_line_ends(vim.clip_at_line_ends(), cx); - let collapse_matches = !HelixModeSetting::get_global(cx).0; - editor.set_collapse_matches(collapse_matches); - editor.set_input_enabled(vim.editor_input_enabled()); - editor.set_autoindent(vim.should_autoindent()); - editor - .selections - .set_line_mode(matches!(vim.mode, Mode::VisualLine)); - - let hide_edit_predictions = !matches!(vim.mode, Mode::Insert | Mode::Replace); - editor.set_edit_predictions_hidden_for_vim_mode(hide_edit_predictions, window, cx); - }); - cx.notify() - } -} - -#[derive(RegisterSetting)] -struct VimSettings { - pub default_mode: Mode, - pub toggle_relative_line_numbers: bool, - pub use_system_clipboard: settings::UseSystemClipboard, - pub use_smartcase_find: bool, - pub custom_digraphs: HashMap>, - pub highlight_on_yank_duration: u64, - pub cursor_shape: CursorShapeSettings, -} - -/// The settings for cursor shape. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct CursorShapeSettings { - /// Cursor shape for the normal mode. - /// - /// Default: block - pub normal: Option, - /// Cursor shape for the replace mode. - /// - /// Default: underline - pub replace: Option, - /// Cursor shape for the visual mode. - /// - /// Default: block - pub visual: Option, - /// Cursor shape for the insert mode. - /// - /// The default value follows the primary cursor_shape. - pub insert: Option, -} - -impl From for CursorShapeSettings { - fn from(settings: settings::CursorShapeSettings) -> Self { - Self { - normal: settings.normal.map(Into::into), - replace: settings.replace.map(Into::into), - visual: settings.visual.map(Into::into), - insert: settings.insert.map(Into::into), - } - } -} - -impl From for Mode { - fn from(mode: ModeContent) -> Self { - match mode { - ModeContent::Normal => Self::Normal, - ModeContent::Insert => Self::Insert, - } - } -} - -impl Settings for VimSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - let vim = content.vim.clone().unwrap(); - Self { - default_mode: vim.default_mode.unwrap().into(), - toggle_relative_line_numbers: vim.toggle_relative_line_numbers.unwrap(), - use_system_clipboard: vim.use_system_clipboard.unwrap(), - use_smartcase_find: vim.use_smartcase_find.unwrap(), - custom_digraphs: vim.custom_digraphs.unwrap(), - highlight_on_yank_duration: vim.highlight_on_yank_duration.unwrap(), - cursor_shape: vim.cursor_shape.unwrap().into(), - } - } -} diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs deleted file mode 100644 index 3c6f237435..0000000000 --- a/crates/vim/src/visual.rs +++ /dev/null @@ -1,1978 +0,0 @@ -use std::sync::Arc; - -use collections::HashMap; -use editor::{ - Bias, DisplayPoint, Editor, MultiBufferOffset, SelectionEffects, - display_map::{DisplaySnapshot, ToDisplayPoint}, - movement, -}; -use gpui::{Context, Window, actions}; -use language::{Point, Selection, SelectionGoal}; -use multi_buffer::MultiBufferRow; -use search::BufferSearchBar; -use util::ResultExt; -use workspace::searchable::Direction; - -use crate::{ - Vim, - motion::{Motion, MotionKind, first_non_whitespace, next_line_end, start_of_line}, - object::Object, - state::{Mark, Mode, Operator}, -}; - -actions!( - vim, - [ - /// Toggles visual mode. - ToggleVisual, - /// Toggles visual line mode. - ToggleVisualLine, - /// Toggles visual block mode. - ToggleVisualBlock, - /// Deletes the visual selection. - VisualDelete, - /// Deletes entire lines in visual selection. - VisualDeleteLine, - /// Yanks (copies) the visual selection. - VisualYank, - /// Yanks entire lines in visual selection. - VisualYankLine, - /// Moves cursor to the other end of the selection. - OtherEnd, - /// Moves cursor to the other end of the selection (row-aware). - OtherEndRowAware, - /// Selects the next occurrence of the current selection. - SelectNext, - /// Selects the previous occurrence of the current selection. - SelectPrevious, - /// Selects the next match of the current selection. - SelectNextMatch, - /// Selects the previous match of the current selection. - SelectPreviousMatch, - /// Selects the next smaller syntax node. - SelectSmallerSyntaxNode, - /// Selects the next larger syntax node. - SelectLargerSyntaxNode, - /// Selects the next syntax node sibling. - SelectNextSyntaxNode, - /// Selects the previous syntax node sibling. - SelectPreviousSyntaxNode, - /// Restores the previous visual selection. - RestoreVisualSelection, - /// Inserts at the end of each line in visual selection. - VisualInsertEndOfLine, - /// Inserts at the first non-whitespace character of each line. - VisualInsertFirstNonWhiteSpace, - ] -); - -pub fn register(editor: &mut Editor, cx: &mut Context) { - Vim::action(editor, cx, |vim, _: &ToggleVisual, window, cx| { - vim.toggle_mode(Mode::Visual, window, cx) - }); - Vim::action(editor, cx, |vim, _: &ToggleVisualLine, window, cx| { - vim.toggle_mode(Mode::VisualLine, window, cx) - }); - Vim::action(editor, cx, |vim, _: &ToggleVisualBlock, window, cx| { - vim.toggle_mode(Mode::VisualBlock, window, cx) - }); - Vim::action(editor, cx, Vim::other_end); - Vim::action(editor, cx, Vim::other_end_row_aware); - Vim::action(editor, cx, Vim::visual_insert_end_of_line); - Vim::action(editor, cx, Vim::visual_insert_first_non_white_space); - Vim::action(editor, cx, |vim, _: &VisualDelete, window, cx| { - vim.record_current_action(cx); - vim.visual_delete(false, window, cx); - }); - Vim::action(editor, cx, |vim, _: &VisualDeleteLine, window, cx| { - vim.record_current_action(cx); - vim.visual_delete(true, window, cx); - }); - Vim::action(editor, cx, |vim, _: &VisualYank, window, cx| { - vim.visual_yank(false, window, cx) - }); - Vim::action(editor, cx, |vim, _: &VisualYankLine, window, cx| { - vim.visual_yank(true, window, cx) - }); - - Vim::action(editor, cx, Vim::select_next); - Vim::action(editor, cx, Vim::select_previous); - Vim::action(editor, cx, |vim, _: &SelectNextMatch, window, cx| { - vim.select_match(Direction::Next, window, cx); - }); - Vim::action(editor, cx, |vim, _: &SelectPreviousMatch, window, cx| { - vim.select_match(Direction::Prev, window, cx); - }); - - Vim::action(editor, cx, |vim, _: &SelectLargerSyntaxNode, window, cx| { - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - for _ in 0..count { - vim.update_editor(cx, |_, editor, cx| { - editor.select_larger_syntax_node(&Default::default(), window, cx); - }); - } - }); - - Vim::action(editor, cx, |vim, _: &SelectNextSyntaxNode, window, cx| { - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - for _ in 0..count { - vim.update_editor(cx, |_, editor, cx| { - editor.select_next_syntax_node(&Default::default(), window, cx); - }); - } - }); - - Vim::action( - editor, - cx, - |vim, _: &SelectPreviousSyntaxNode, window, cx| { - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - for _ in 0..count { - vim.update_editor(cx, |_, editor, cx| { - editor.select_prev_syntax_node(&Default::default(), window, cx); - }); - } - }, - ); - - Vim::action( - editor, - cx, - |vim, _: &SelectSmallerSyntaxNode, window, cx| { - let count = Vim::take_count(cx).unwrap_or(1); - Vim::take_forced_motion(cx); - for _ in 0..count { - vim.update_editor(cx, |_, editor, cx| { - editor.select_smaller_syntax_node(&Default::default(), window, cx); - }); - } - }, - ); - - Vim::action(editor, cx, |vim, _: &RestoreVisualSelection, window, cx| { - let Some((stored_mode, reversed)) = vim.stored_visual_mode.take() else { - return; - }; - let marks = vim - .update_editor(cx, |vim, editor, cx| { - vim.get_mark("<", editor, window, cx) - .zip(vim.get_mark(">", editor, window, cx)) - }) - .flatten(); - let Some((Mark::Local(start), Mark::Local(end))) = marks else { - return; - }; - let ranges = start - .iter() - .zip(end) - .zip(reversed) - .map(|((start, end), reversed)| (*start, end, reversed)) - .collect::>(); - - if vim.mode.is_visual() { - vim.create_visual_marks(vim.mode, window, cx); - } - - vim.update_editor(cx, |_, editor, cx| { - editor.set_clip_at_line_ends(false, cx); - editor.change_selections(Default::default(), window, cx, |s| { - let map = s.display_snapshot(); - let ranges = ranges - .into_iter() - .map(|(start, end, reversed)| { - let mut new_end = - movement::saturating_right(&map, end.to_display_point(&map)); - let mut new_start = start.to_display_point(&map); - if new_start >= new_end { - if new_end.column() == 0 { - new_end = movement::right(&map, new_end) - } else { - new_start = movement::saturating_left(&map, new_end); - } - } - Selection { - id: s.new_selection_id(), - start: new_start.to_point(&map), - end: new_end.to_point(&map), - reversed, - goal: SelectionGoal::None, - } - }) - .collect(); - s.select(ranges); - }) - }); - vim.switch_mode(stored_mode, true, window, cx) - }); -} - -impl Vim { - pub fn visual_motion( - &mut self, - motion: Motion, - times: Option, - window: &mut Window, - cx: &mut Context, - ) { - self.update_editor(cx, |vim, editor, cx| { - let text_layout_details = editor.text_layout_details(window); - if vim.mode == Mode::VisualBlock - && !matches!( - motion, - Motion::EndOfLine { - display_lines: false - } - ) - { - let is_up_or_down = matches!(motion, Motion::Up { .. } | Motion::Down { .. }); - vim.visual_block_motion(is_up_or_down, editor, window, cx, |map, point, goal| { - motion.move_point(map, point, goal, times, &text_layout_details) - }) - } else { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let was_reversed = selection.reversed; - let mut current_head = selection.head(); - - // our motions assume the current character is after the cursor, - // but in (forward) visual mode the current character is just - // before the end of the selection. - - // If the file ends with a newline (which is common) we don't do this. - // so that if you go to the end of such a file you can use "up" to go - // to the previous line and have it work somewhat as expected. - if !selection.reversed - && !selection.is_empty() - && !(selection.end.column() == 0 && selection.end == map.max_point()) - { - current_head = movement::left(map, selection.end) - } - - let Some((new_head, goal)) = motion.move_point( - map, - current_head, - selection.goal, - times, - &text_layout_details, - ) else { - return; - }; - - selection.set_head(new_head, goal); - - // ensure the current character is included in the selection. - if !selection.reversed { - let next_point = if vim.mode == Mode::VisualBlock { - movement::saturating_right(map, selection.end) - } else { - movement::right(map, selection.end) - }; - - if !(next_point.column() == 0 && next_point == map.max_point()) { - selection.end = next_point; - } - } - - // vim always ensures the anchor character stays selected. - // if our selection has reversed, we need to move the opposite end - // to ensure the anchor is still selected. - if was_reversed && !selection.reversed { - selection.start = movement::left(map, selection.start); - } else if !was_reversed && selection.reversed { - selection.end = movement::right(map, selection.end); - } - }) - }); - } - }); - } - - pub fn visual_block_motion( - &mut self, - preserve_goal: bool, - editor: &mut Editor, - window: &mut Window, - cx: &mut Context, - mut move_selection: impl FnMut( - &DisplaySnapshot, - DisplayPoint, - SelectionGoal, - ) -> Option<(DisplayPoint, SelectionGoal)>, - ) { - let text_layout_details = editor.text_layout_details(window); - editor.change_selections(Default::default(), window, cx, |s| { - let map = &s.display_snapshot(); - let mut head = s.newest_anchor().head().to_display_point(map); - let mut tail = s.oldest_anchor().tail().to_display_point(map); - - let mut head_x = map.x_for_display_point(head, &text_layout_details); - let mut tail_x = map.x_for_display_point(tail, &text_layout_details); - - let (start, end) = match s.newest_anchor().goal { - SelectionGoal::HorizontalRange { start, end } if preserve_goal => (start, end), - SelectionGoal::HorizontalPosition(start) if preserve_goal => (start, start), - _ => (tail_x.into(), head_x.into()), - }; - let mut goal = SelectionGoal::HorizontalRange { start, end }; - - let was_reversed = tail_x > head_x; - if !was_reversed && !preserve_goal { - head = movement::saturating_left(map, head); - } - - let reverse_aware_goal = if was_reversed { - SelectionGoal::HorizontalRange { - start: end, - end: start, - } - } else { - goal - }; - - let Some((new_head, _)) = move_selection(map, head, reverse_aware_goal) else { - return; - }; - head = new_head; - head_x = map.x_for_display_point(head, &text_layout_details); - - let is_reversed = tail_x > head_x; - if was_reversed && !is_reversed { - tail = movement::saturating_left(map, tail); - tail_x = map.x_for_display_point(tail, &text_layout_details); - } else if !was_reversed && is_reversed { - tail = movement::saturating_right(map, tail); - tail_x = map.x_for_display_point(tail, &text_layout_details); - } - if !is_reversed && !preserve_goal { - head = movement::saturating_right(map, head); - head_x = map.x_for_display_point(head, &text_layout_details); - } - - let positions = if is_reversed { - head_x..tail_x - } else { - tail_x..head_x - }; - - if !preserve_goal { - goal = SelectionGoal::HorizontalRange { - start: f64::from(positions.start), - end: f64::from(positions.end), - }; - } - - let mut selections = Vec::new(); - let mut row = tail.row(); - let going_up = tail.row() > head.row(); - let direction = if going_up { -1 } else { 1 }; - - loop { - let laid_out_line = map.layout_row(row, &text_layout_details); - let start = DisplayPoint::new( - row, - laid_out_line.closest_index_for_x(positions.start) as u32, - ); - let mut end = - DisplayPoint::new(row, laid_out_line.closest_index_for_x(positions.end) as u32); - if end <= start { - if start.column() == map.line_len(start.row()) { - end = start; - } else { - end = movement::saturating_right(map, start); - } - } - - if positions.start <= laid_out_line.width { - let selection = Selection { - id: s.new_selection_id(), - start: start.to_point(map), - end: end.to_point(map), - reversed: is_reversed && - // For neovim parity: cursor is not reversed when column is a single character - end.column() - start.column() > 1, - goal, - }; - - selections.push(selection); - } - - // When dealing with soft wrapped lines, it's possible that - // `row` ends up being set to a value other than `head.row()` as - // `head.row()` might be a `DisplayPoint` mapped to a soft - // wrapped line, hence the need for `<=` and `>=` instead of - // `==`. - if going_up && row <= head.row() || !going_up && row >= head.row() { - break; - } - - // Find the next or previous buffer row where the `row` should - // be moved to, so that wrapped lines are skipped. - row = map - .start_of_relative_buffer_row(DisplayPoint::new(row, 0), direction) - .row(); - } - - s.select(selections); - }) - } - - pub fn visual_object( - &mut self, - object: Object, - count: Option, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(Operator::Object { around }) = self.active_operator() { - self.pop_operator(window, cx); - let current_mode = self.mode; - let target_mode = object.target_visual_mode(current_mode, around); - if target_mode != current_mode { - self.switch_mode(target_mode, true, window, cx); - } - - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let mut mut_selection = selection.clone(); - - // all our motions assume that the current character is - // after the cursor; however in the case of a visual selection - // the current character is before the cursor. - // But this will affect the judgment of the html tag - // so the html tag needs to skip this logic. - if !selection.reversed && object != Object::Tag { - mut_selection.set_head( - movement::left(map, mut_selection.head()), - mut_selection.goal, - ); - } - - let original_point = selection.tail().to_point(map); - - if let Some(range) = object.range(map, mut_selection, around, count) { - if !range.is_empty() { - let expand_both_ways = object.always_expands_both_ways() - || selection.is_empty() - || movement::right(map, selection.start) == selection.end; - - if expand_both_ways { - if selection.start == range.start - && selection.end == range.end - && object.always_expands_both_ways() - { - if let Some(range) = - object.range(map, selection.clone(), around, count) - { - selection.start = range.start; - selection.end = range.end; - } - } else { - selection.start = range.start; - selection.end = range.end; - } - } else if selection.reversed { - selection.start = range.start; - } else { - selection.end = range.end; - } - } - - // In the visual selection result of a paragraph object, the cursor is - // placed at the start of the last line. And in the visual mode, the - // selection end is located after the end character. So, adjustment of - // selection end is needed. - // - // We don't do this adjustment for a one-line blank paragraph since the - // trailing newline is included in its selection from the beginning. - if object == Object::Paragraph && range.start != range.end { - let row_of_selection_end_line = selection.end.to_point(map).row; - let new_selection_end = if map - .buffer_snapshot() - .line_len(MultiBufferRow(row_of_selection_end_line)) - == 0 - { - Point::new(row_of_selection_end_line + 1, 0) - } else { - Point::new(row_of_selection_end_line, 1) - }; - selection.end = new_selection_end.to_display_point(map); - } - - // To match vim, if the range starts of the same line as it originally - // did, we keep the tail of the selection in the same place instead of - // snapping it to the start of the line - if target_mode == Mode::VisualLine { - let new_start_point = selection.start.to_point(map); - if new_start_point.row == original_point.row { - if selection.end.to_point(map).row > new_start_point.row { - if original_point.column - == map - .buffer_snapshot() - .line_len(MultiBufferRow(original_point.row)) - { - selection.start = movement::saturating_left( - map, - original_point.to_display_point(map), - ) - } else { - selection.start = original_point.to_display_point(map) - } - } else { - selection.end = movement::saturating_right( - map, - original_point.to_display_point(map), - ); - if original_point.column > 0 { - selection.reversed = true - } - } - } - } - } - }); - }); - }); - } - } - - fn visual_insert_end_of_line( - &mut self, - _: &VisualInsertEndOfLine, - window: &mut Window, - cx: &mut Context, - ) { - self.update_editor(cx, |_, editor, cx| { - editor.split_selection_into_lines(&Default::default(), window, cx); - editor.change_selections(Default::default(), window, cx, |s| { - s.move_cursors_with(|map, cursor, _| { - (next_line_end(map, cursor, 1), SelectionGoal::None) - }); - }); - }); - - self.switch_mode(Mode::Insert, false, window, cx); - } - - fn visual_insert_first_non_white_space( - &mut self, - _: &VisualInsertFirstNonWhiteSpace, - window: &mut Window, - cx: &mut Context, - ) { - self.update_editor(cx, |_, editor, cx| { - editor.split_selection_into_lines(&Default::default(), window, cx); - editor.change_selections(Default::default(), window, cx, |s| { - s.move_cursors_with(|map, cursor, _| { - ( - first_non_whitespace(map, false, cursor), - SelectionGoal::None, - ) - }); - }); - }); - - self.switch_mode(Mode::Insert, false, window, cx); - } - - fn toggle_mode(&mut self, mode: Mode, window: &mut Window, cx: &mut Context) { - if self.mode == mode { - self.switch_mode(Mode::Normal, false, window, cx); - } else { - self.switch_mode(mode, false, window, cx); - } - } - - pub fn other_end(&mut self, _: &OtherEnd, window: &mut Window, cx: &mut Context) { - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|_, selection| { - selection.reversed = !selection.reversed; - }); - }) - }); - } - - pub fn other_end_row_aware( - &mut self, - _: &OtherEndRowAware, - window: &mut Window, - cx: &mut Context, - ) { - let mode = self.mode; - self.update_editor(cx, |_, editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|_, selection| { - selection.reversed = !selection.reversed; - }); - if mode == Mode::VisualBlock { - s.reverse_selections(); - } - }) - }); - } - - pub fn visual_delete(&mut self, line_mode: bool, window: &mut Window, cx: &mut Context) { - self.store_visual_marks(window, cx); - self.update_editor(cx, |vim, editor, cx| { - let mut original_columns: HashMap<_, _> = Default::default(); - let line_mode = line_mode || editor.selections.line_mode(); - editor.selections.set_line_mode(false); - - editor.transact(window, cx, |editor, window, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - if line_mode { - let mut position = selection.head(); - if !selection.reversed { - position = movement::left(map, position); - } - original_columns.insert(selection.id, position.to_point(map).column); - if vim.mode == Mode::VisualBlock { - *selection.end.column_mut() = map.line_len(selection.end.row()) - } else { - let start = selection.start.to_point(map); - let end = selection.end.to_point(map); - selection.start = map.prev_line_boundary(start).1; - if end.column == 0 && end > start { - let row = end.row.saturating_sub(1); - selection.end = Point::new( - row, - map.buffer_snapshot().line_len(MultiBufferRow(row)), - ) - .to_display_point(map) - } else { - selection.end = map.next_line_boundary(end).1; - } - } - } - selection.goal = SelectionGoal::None; - }); - }); - let kind = if line_mode { - MotionKind::Linewise - } else { - MotionKind::Exclusive - }; - vim.copy_selections_content(editor, kind, window, cx); - - if line_mode && vim.mode != Mode::VisualBlock { - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let end = selection.end.to_point(map); - let start = selection.start.to_point(map); - if end.row < map.buffer_snapshot().max_point().row { - selection.end = Point::new(end.row + 1, 0).to_display_point(map) - } else if start.row > 0 { - selection.start = Point::new( - start.row - 1, - map.buffer_snapshot() - .line_len(MultiBufferRow(start.row - 1)), - ) - .to_display_point(map) - } - }); - }); - } - editor.insert("", window, cx); - - // Fixup cursor position after the deletion - editor.set_clip_at_line_ends(true, cx); - editor.change_selections(Default::default(), window, cx, |s| { - s.move_with(|map, selection| { - let mut cursor = selection.head().to_point(map); - - if let Some(column) = original_columns.get(&selection.id) { - cursor.column = *column - } - let cursor = map.clip_point(cursor.to_display_point(map), Bias::Left); - selection.collapse_to(cursor, selection.goal) - }); - if vim.mode == Mode::VisualBlock { - s.select_anchors(vec![s.first_anchor()]) - } - }); - }) - }); - self.switch_mode(Mode::Normal, true, window, cx); - } - - pub fn visual_yank(&mut self, line_mode: bool, window: &mut Window, cx: &mut Context) { - self.store_visual_marks(window, cx); - self.update_editor(cx, |vim, editor, cx| { - let line_mode = line_mode || editor.selections.line_mode(); - - // For visual line mode, adjust selections to avoid yanking the next line when on \n - if line_mode && vim.mode != Mode::VisualBlock { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - let start = selection.start.to_point(map); - let end = selection.end.to_point(map); - if end.column == 0 && end > start { - let row = end.row.saturating_sub(1); - selection.end = Point::new( - row, - map.buffer_snapshot().line_len(MultiBufferRow(row)), - ) - .to_display_point(map); - } - }); - }); - } - - editor.selections.set_line_mode(line_mode); - let kind = if line_mode { - MotionKind::Linewise - } else { - MotionKind::Exclusive - }; - vim.yank_selections_content(editor, kind, window, cx); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.move_with(|map, selection| { - if line_mode { - selection.start = start_of_line(map, false, selection.start); - }; - selection.collapse_to(selection.start, SelectionGoal::None) - }); - if vim.mode == Mode::VisualBlock { - s.select_anchors(vec![s.first_anchor()]) - } - }); - }); - self.switch_mode(Mode::Normal, true, window, cx); - } - - pub(crate) fn visual_replace( - &mut self, - text: Arc, - window: &mut Window, - cx: &mut Context, - ) { - self.stop_recording(cx); - self.update_editor(cx, |_, editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - let display_map = editor.display_snapshot(cx); - let selections = editor.selections.all_adjusted_display(&display_map); - - // Selections are biased right at the start. So we need to store - // anchors that are biased left so that we can restore the selections - // after the change - let stable_anchors = editor - .selections - .disjoint_anchors_arc() - .iter() - .map(|selection| { - let start = selection.start.bias_left(&display_map.buffer_snapshot()); - start..start - }) - .collect::>(); - - let mut edits = Vec::new(); - for selection in selections.iter() { - let selection = selection.clone(); - for row_range in - movement::split_display_range_by_lines(&display_map, selection.range()) - { - let range = row_range.start.to_offset(&display_map, Bias::Right) - ..row_range.end.to_offset(&display_map, Bias::Right); - let text = text.repeat(range.end - range.start); - edits.push((range, text)); - } - } - - editor.edit(edits, cx); - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_ranges(stable_anchors) - }); - }); - }); - self.switch_mode(Mode::Normal, false, window, cx); - } - - pub fn select_next(&mut self, _: &SelectNext, window: &mut Window, cx: &mut Context) { - Vim::take_forced_motion(cx); - let count = - Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 }); - self.update_editor(cx, |_, editor, cx| { - editor.set_clip_at_line_ends(false, cx); - for _ in 0..count { - if editor - .select_next(&Default::default(), window, cx) - .log_err() - .is_none() - { - break; - } - } - }); - } - - pub fn select_previous( - &mut self, - _: &SelectPrevious, - window: &mut Window, - cx: &mut Context, - ) { - Vim::take_forced_motion(cx); - let count = - Vim::take_count(cx).unwrap_or_else(|| if self.mode.is_visual() { 1 } else { 2 }); - self.update_editor(cx, |_, editor, cx| { - for _ in 0..count { - if editor - .select_previous(&Default::default(), window, cx) - .log_err() - .is_none() - { - break; - } - } - }); - } - - pub fn select_match( - &mut self, - direction: Direction, - window: &mut Window, - cx: &mut Context, - ) { - Vim::take_forced_motion(cx); - let count = Vim::take_count(cx).unwrap_or(1); - let Some(pane) = self.pane(window, cx) else { - return; - }; - let vim_is_normal = self.mode == Mode::Normal; - let mut start_selection = MultiBufferOffset(0); - let mut end_selection = MultiBufferOffset(0); - - self.update_editor(cx, |_, editor, _| { - editor.set_collapse_matches(false); - }); - if vim_is_normal { - pane.update(cx, |pane, cx| { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() - { - search_bar.update(cx, |search_bar, cx| { - if !search_bar.has_active_match() || !search_bar.show(window, cx) { - return; - } - // without update_match_index there is a bug when the cursor is before the first match - search_bar.update_match_index(window, cx); - search_bar.select_match(direction.opposite(), 1, window, cx); - }); - } - }); - } - self.update_editor(cx, |_, editor, cx| { - let latest = editor - .selections - .newest::(&editor.display_snapshot(cx)); - start_selection = latest.start; - end_selection = latest.end; - }); - - let mut match_exists = false; - pane.update(cx, |pane, cx| { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - search_bar.update(cx, |search_bar, cx| { - search_bar.update_match_index(window, cx); - search_bar.select_match(direction, count, window, cx); - match_exists = search_bar.match_exists(window, cx); - }); - } - }); - if !match_exists { - self.clear_operator(window, cx); - self.stop_replaying(cx); - return; - } - self.update_editor(cx, |_, editor, cx| { - let latest = editor - .selections - .newest::(&editor.display_snapshot(cx)); - if vim_is_normal { - start_selection = latest.start; - end_selection = latest.end; - } else { - start_selection = start_selection.min(latest.start); - end_selection = end_selection.max(latest.end); - } - if direction == Direction::Prev { - std::mem::swap(&mut start_selection, &mut end_selection); - } - editor.change_selections(Default::default(), window, cx, |s| { - s.select_ranges([start_selection..end_selection]); - }); - editor.set_collapse_matches(true); - }); - - match self.maybe_pop_operator() { - Some(Operator::Change) => self.substitute(None, false, window, cx), - Some(Operator::Delete) => { - self.stop_recording(cx); - self.visual_delete(false, window, cx) - } - Some(Operator::Yank) => self.visual_yank(false, window, cx), - _ => {} // Ignoring other operators - } - } -} -#[cfg(test)] -mod test { - use indoc::indoc; - use workspace::item::Item; - - use crate::{ - state::Mode, - test::{NeovimBackedTestContext, VimTestContext}, - }; - - #[gpui::test] - async fn test_enter_visual_mode(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "The ˇquick brown - fox jumps over - the lazy dog" - }) - .await; - let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx)); - - // entering visual mode should select the character - // under cursor - cx.simulate_shared_keystrokes("v").await; - cx.shared_state() - .await - .assert_eq(indoc! { "The «qˇ»uick brown - fox jumps over - the lazy dog"}); - cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx))); - - // forwards motions should extend the selection - cx.simulate_shared_keystrokes("w j").await; - cx.shared_state().await.assert_eq(indoc! { "The «quick brown - fox jumps oˇ»ver - the lazy dog"}); - - cx.simulate_shared_keystrokes("escape").await; - cx.shared_state().await.assert_eq(indoc! { "The quick brown - fox jumps ˇover - the lazy dog"}); - - // motions work backwards - cx.simulate_shared_keystrokes("v k b").await; - cx.shared_state() - .await - .assert_eq(indoc! { "The «ˇquick brown - fox jumps o»ver - the lazy dog"}); - - // works on empty lines - cx.set_shared_state(indoc! {" - a - ˇ - b - "}) - .await; - let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx)); - cx.simulate_shared_keystrokes("v").await; - cx.shared_state().await.assert_eq(indoc! {" - a - « - ˇ»b - "}); - cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx))); - - // toggles off again - cx.simulate_shared_keystrokes("v").await; - cx.shared_state().await.assert_eq(indoc! {" - a - ˇ - b - "}); - - // works at the end of a document - cx.set_shared_state(indoc! {" - a - b - ˇ"}) - .await; - - cx.simulate_shared_keystrokes("v").await; - cx.shared_state().await.assert_eq(indoc! {" - a - b - ˇ"}); - } - - #[gpui::test] - async fn test_visual_insert_first_non_whitespace(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! { - "«The quick brown - fox jumps over - the lazy dogˇ»" - }, - Mode::Visual, - ); - cx.simulate_keystrokes("g shift-i"); - cx.assert_state( - indoc! { - "ˇThe quick brown - ˇfox jumps over - ˇthe lazy dog" - }, - Mode::Insert, - ); - } - - #[gpui::test] - async fn test_visual_insert_end_of_line(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! { - "«The quick brown - fox jumps over - the lazy dogˇ»" - }, - Mode::Visual, - ); - cx.simulate_keystrokes("g shift-a"); - cx.assert_state( - indoc! { - "The quick brownˇ - fox jumps overˇ - the lazy dogˇ" - }, - Mode::Insert, - ); - } - - #[gpui::test] - async fn test_enter_visual_line_mode(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "The ˇquick brown - fox jumps over - the lazy dog" - }) - .await; - cx.simulate_shared_keystrokes("shift-v").await; - cx.shared_state() - .await - .assert_eq(indoc! { "The «qˇ»uick brown - fox jumps over - the lazy dog"}); - cx.simulate_shared_keystrokes("x").await; - cx.shared_state().await.assert_eq(indoc! { "fox ˇjumps over - the lazy dog"}); - - // it should work on empty lines - cx.set_shared_state(indoc! {" - a - ˇ - b"}) - .await; - cx.simulate_shared_keystrokes("shift-v").await; - cx.shared_state().await.assert_eq(indoc! {" - a - « - ˇ»b"}); - cx.simulate_shared_keystrokes("x").await; - cx.shared_state().await.assert_eq(indoc! {" - a - ˇb"}); - - // it should work at the end of the document - cx.set_shared_state(indoc! {" - a - b - ˇ"}) - .await; - let cursor = cx.update_editor(|editor, _, cx| editor.pixel_position_of_cursor(cx)); - cx.simulate_shared_keystrokes("shift-v").await; - cx.shared_state().await.assert_eq(indoc! {" - a - b - ˇ"}); - cx.update_editor(|editor, _, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx))); - cx.simulate_shared_keystrokes("x").await; - cx.shared_state().await.assert_eq(indoc! {" - a - ˇb"}); - } - - #[gpui::test] - async fn test_visual_delete(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.simulate("v w", "The quick ˇbrown") - .await - .assert_matches(); - - cx.simulate("v w x", "The quick ˇbrown") - .await - .assert_matches(); - cx.simulate( - "v w j x", - indoc! {" - The ˇquick brown - fox jumps over - the lazy dog"}, - ) - .await - .assert_matches(); - // Test pasting code copied on delete - cx.simulate_shared_keystrokes("j p").await; - cx.shared_state().await.assert_matches(); - - cx.simulate_at_each_offset( - "v w j x", - indoc! {" - The ˇquick brown - fox jumps over - the ˇlazy dog"}, - ) - .await - .assert_matches(); - cx.simulate_at_each_offset( - "v b k x", - indoc! {" - The ˇquick brown - fox jumps ˇover - the ˇlazy dog"}, - ) - .await - .assert_matches(); - } - - #[gpui::test] - async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! {" - The quˇick brown - fox jumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("shift-v x").await; - cx.shared_state().await.assert_matches(); - - // Test pasting code copied on delete - cx.simulate_shared_keystrokes("p").await; - cx.shared_state().await.assert_matches(); - - cx.set_shared_state(indoc! {" - The quick brown - fox jumps over - the laˇzy dog"}) - .await; - cx.simulate_shared_keystrokes("shift-v x").await; - cx.shared_state().await.assert_matches(); - cx.shared_clipboard().await.assert_eq("the lazy dog\n"); - - cx.set_shared_state(indoc! {" - The quˇick brown - fox jumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("shift-v j x").await; - cx.shared_state().await.assert_matches(); - // Test pasting code copied on delete - cx.simulate_shared_keystrokes("p").await; - cx.shared_state().await.assert_matches(); - - cx.set_shared_state(indoc! {" - The ˇlong line - should not - crash - "}) - .await; - cx.simulate_shared_keystrokes("shift-v $ x").await; - cx.shared_state().await.assert_matches(); - } - - #[gpui::test] - async fn test_visual_yank(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("The quick ˇbrown").await; - cx.simulate_shared_keystrokes("v w y").await; - cx.shared_state().await.assert_eq("The quick ˇbrown"); - cx.shared_clipboard().await.assert_eq("brown"); - - cx.set_shared_state(indoc! {" - The ˇquick brown - fox jumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("v w j y").await; - cx.shared_state().await.assert_eq(indoc! {" - The ˇquick brown - fox jumps over - the lazy dog"}); - cx.shared_clipboard().await.assert_eq(indoc! {" - quick brown - fox jumps o"}); - - cx.set_shared_state(indoc! {" - The quick brown - fox jumps over - the ˇlazy dog"}) - .await; - cx.simulate_shared_keystrokes("v w j y").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox jumps over - the ˇlazy dog"}); - cx.shared_clipboard().await.assert_eq("lazy d"); - cx.simulate_shared_keystrokes("shift-v y").await; - cx.shared_clipboard().await.assert_eq("the lazy dog\n"); - - cx.set_shared_state(indoc! {" - The ˇquick brown - fox jumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("v b k y").await; - cx.shared_state().await.assert_eq(indoc! {" - ˇThe quick brown - fox jumps over - the lazy dog"}); - assert_eq!( - cx.read_from_clipboard() - .map(|item| item.text().unwrap()) - .unwrap(), - "The q" - ); - - cx.set_shared_state(indoc! {" - The quick brown - fox ˇjumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("shift-v shift-g shift-y") - .await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - ˇfox jumps over - the lazy dog"}); - cx.shared_clipboard() - .await - .assert_eq("fox jumps over\nthe lazy dog\n"); - - cx.set_shared_state(indoc! {" - The quick brown - fox ˇjumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("shift-v $ shift-y").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - ˇfox jumps over - the lazy dog"}); - cx.shared_clipboard().await.assert_eq("fox jumps over\n"); - } - - #[gpui::test] - async fn test_visual_block_mode(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "The ˇquick brown - fox jumps over - the lazy dog" - }) - .await; - cx.simulate_shared_keystrokes("ctrl-v").await; - cx.shared_state().await.assert_eq(indoc! { - "The «qˇ»uick brown - fox jumps over - the lazy dog" - }); - cx.simulate_shared_keystrokes("2 down").await; - cx.shared_state().await.assert_eq(indoc! { - "The «qˇ»uick brown - fox «jˇ»umps over - the «lˇ»azy dog" - }); - cx.simulate_shared_keystrokes("e").await; - cx.shared_state().await.assert_eq(indoc! { - "The «quicˇ»k brown - fox «jumpˇ»s over - the «lazyˇ» dog" - }); - cx.simulate_shared_keystrokes("^").await; - cx.shared_state().await.assert_eq(indoc! { - "«ˇThe q»uick brown - «ˇfox j»umps over - «ˇthe l»azy dog" - }); - cx.simulate_shared_keystrokes("$").await; - cx.shared_state().await.assert_eq(indoc! { - "The «quick brownˇ» - fox «jumps overˇ» - the «lazy dogˇ»" - }); - cx.simulate_shared_keystrokes("shift-f space").await; - cx.shared_state().await.assert_eq(indoc! { - "The «quickˇ» brown - fox «jumpsˇ» over - the «lazy ˇ»dog" - }); - - // toggling through visual mode works as expected - cx.simulate_shared_keystrokes("v").await; - cx.shared_state().await.assert_eq(indoc! { - "The «quick brown - fox jumps over - the lazy ˇ»dog" - }); - cx.simulate_shared_keystrokes("ctrl-v").await; - cx.shared_state().await.assert_eq(indoc! { - "The «quickˇ» brown - fox «jumpsˇ» over - the «lazy ˇ»dog" - }); - - cx.set_shared_state(indoc! { - "The ˇquick - brown - fox - jumps over the - - lazy dog - " - }) - .await; - cx.simulate_shared_keystrokes("ctrl-v down down").await; - cx.shared_state().await.assert_eq(indoc! { - "The«ˇ q»uick - bro«ˇwn» - foxˇ - jumps over the - - lazy dog - " - }); - cx.simulate_shared_keystrokes("down").await; - cx.shared_state().await.assert_eq(indoc! { - "The «qˇ»uick - brow«nˇ» - fox - jump«sˇ» over the - - lazy dog - " - }); - cx.simulate_shared_keystrokes("left").await; - cx.shared_state().await.assert_eq(indoc! { - "The«ˇ q»uick - bro«ˇwn» - foxˇ - jum«ˇps» over the - - lazy dog - " - }); - cx.simulate_shared_keystrokes("s o escape").await; - cx.shared_state().await.assert_eq(indoc! { - "Theˇouick - broo - foxo - jumo over the - - lazy dog - " - }); - - // https://github.com/zed-industries/zed/issues/6274 - cx.set_shared_state(indoc! { - "Theˇ quick brown - - fox jumps over - the lazy dog - " - }) - .await; - cx.simulate_shared_keystrokes("l ctrl-v j j").await; - cx.shared_state().await.assert_eq(indoc! { - "The «qˇ»uick brown - - fox «jˇ»umps over - the lazy dog - " - }); - } - - #[gpui::test] - async fn test_visual_block_issue_2123(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "The ˇquick brown - fox jumps over - the lazy dog - " - }) - .await; - cx.simulate_shared_keystrokes("ctrl-v right down").await; - cx.shared_state().await.assert_eq(indoc! { - "The «quˇ»ick brown - fox «juˇ»mps over - the lazy dog - " - }); - } - #[gpui::test] - async fn test_visual_block_mode_down_right(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! {" - The ˇquick brown - fox jumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("ctrl-v l l l l l j").await; - cx.shared_state().await.assert_eq(indoc! {" - The «quick ˇ»brown - fox «jumps ˇ»over - the lazy dog"}); - } - - #[gpui::test] - async fn test_visual_block_mode_up_left(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! {" - The quick brown - fox jumpsˇ over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("ctrl-v h h h h h k").await; - cx.shared_state().await.assert_eq(indoc! {" - The «ˇquick »brown - fox «ˇjumps »over - the lazy dog"}); - } - - #[gpui::test] - async fn test_visual_block_mode_other_end(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! {" - The quick brown - fox jˇumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("ctrl-v l l l l j").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox j«umps ˇ»over - the l«azy dˇ»og"}); - cx.simulate_shared_keystrokes("o k").await; - cx.shared_state().await.assert_eq(indoc! {" - The q«ˇuick »brown - fox j«ˇumps »over - the l«ˇazy d»og"}); - } - - #[gpui::test] - async fn test_visual_block_mode_shift_other_end(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - cx.set_shared_state(indoc! {" - The quick brown - fox jˇumps over - the lazy dog"}) - .await; - cx.simulate_shared_keystrokes("ctrl-v l l l l j").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox j«umps ˇ»over - the l«azy dˇ»og"}); - cx.simulate_shared_keystrokes("shift-o k").await; - cx.shared_state().await.assert_eq(indoc! {" - The quick brown - fox j«ˇumps »over - the lazy dog"}); - } - - #[gpui::test] - async fn test_visual_block_insert(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "ˇThe quick brown - fox jumps over - the lazy dog - " - }) - .await; - cx.simulate_shared_keystrokes("ctrl-v 9 down").await; - cx.shared_state().await.assert_eq(indoc! { - "«Tˇ»he quick brown - «fˇ»ox jumps over - «tˇ»he lazy dog - ˇ" - }); - - cx.simulate_shared_keystrokes("shift-i k escape").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇkThe quick brown - kfox jumps over - kthe lazy dog - k" - }); - - cx.set_shared_state(indoc! { - "ˇThe quick brown - fox jumps over - the lazy dog - " - }) - .await; - cx.simulate_shared_keystrokes("ctrl-v 9 down").await; - cx.shared_state().await.assert_eq(indoc! { - "«Tˇ»he quick brown - «fˇ»ox jumps over - «tˇ»he lazy dog - ˇ" - }); - cx.simulate_shared_keystrokes("c k escape").await; - cx.shared_state().await.assert_eq(indoc! { - "ˇkhe quick brown - kox jumps over - khe lazy dog - k" - }); - } - - #[gpui::test] - async fn test_visual_block_wrapping_selection(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - // Ensure that the editor is wrapping lines at 12 columns so that each - // of the lines ends up being wrapped. - cx.set_shared_wrap(12).await; - cx.set_shared_state(indoc! { - "ˇ12345678901234567890 - 12345678901234567890 - 12345678901234567890 - " - }) - .await; - cx.simulate_shared_keystrokes("ctrl-v j").await; - cx.shared_state().await.assert_eq(indoc! { - "«1ˇ»2345678901234567890 - «1ˇ»2345678901234567890 - 12345678901234567890 - " - }); - - // Test with lines taking up different amounts of display rows to ensure - // that, even in that case, only the buffer rows are taken into account. - cx.set_shared_state(indoc! { - "ˇ123456789012345678901234567890123456789012345678901234567890 - 1234567890123456789012345678901234567890 - 12345678901234567890 - " - }) - .await; - cx.simulate_shared_keystrokes("ctrl-v 2 j").await; - cx.shared_state().await.assert_eq(indoc! { - "«1ˇ»23456789012345678901234567890123456789012345678901234567890 - «1ˇ»234567890123456789012345678901234567890 - «1ˇ»2345678901234567890 - " - }); - - // Same scenario as above, but using the up motion to ensure that the - // result is the same. - cx.set_shared_state(indoc! { - "123456789012345678901234567890123456789012345678901234567890 - 1234567890123456789012345678901234567890 - ˇ12345678901234567890 - " - }) - .await; - cx.simulate_shared_keystrokes("ctrl-v 2 k").await; - cx.shared_state().await.assert_eq(indoc! { - "«1ˇ»23456789012345678901234567890123456789012345678901234567890 - «1ˇ»234567890123456789012345678901234567890 - «1ˇ»2345678901234567890 - " - }); - } - - #[gpui::test] - async fn test_visual_object(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("hello (in [parˇens] o)").await; - cx.simulate_shared_keystrokes("ctrl-v l").await; - cx.simulate_shared_keystrokes("a ]").await; - cx.shared_state() - .await - .assert_eq("hello (in «[parens]ˇ» o)"); - cx.simulate_shared_keystrokes("i (").await; - cx.shared_state() - .await - .assert_eq("hello («in [parens] oˇ»)"); - - cx.set_shared_state("hello in a wˇord again.").await; - cx.simulate_shared_keystrokes("ctrl-v l i w").await; - cx.shared_state() - .await - .assert_eq("hello in a w«ordˇ» again."); - assert_eq!(cx.mode(), Mode::VisualBlock); - cx.simulate_shared_keystrokes("o a s").await; - cx.shared_state() - .await - .assert_eq("«ˇhello in a word» again."); - } - - #[gpui::test] - async fn test_visual_object_expands(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "{ - { - ˇ } - } - { - } - " - }) - .await; - cx.simulate_shared_keystrokes("v l").await; - cx.shared_state().await.assert_eq(indoc! { - "{ - { - « }ˇ» - } - { - } - " - }); - cx.simulate_shared_keystrokes("a {").await; - cx.shared_state().await.assert_eq(indoc! { - "{ - «{ - }ˇ» - } - { - } - " - }); - cx.simulate_shared_keystrokes("a {").await; - cx.shared_state().await.assert_eq(indoc! { - "«{ - { - } - }ˇ» - { - } - " - }); - // cx.simulate_shared_keystrokes("a {").await; - // cx.shared_state().await.assert_eq(indoc! { - // "{ - // «{ - // }ˇ» - // } - // { - // } - // " - // }); - } - - #[gpui::test] - async fn test_mode_across_command(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("aˇbc", Mode::Normal); - cx.simulate_keystrokes("ctrl-v"); - assert_eq!(cx.mode(), Mode::VisualBlock); - cx.simulate_keystrokes("cmd-shift-p escape"); - assert_eq!(cx.mode(), Mode::VisualBlock); - } - - #[gpui::test] - async fn test_gn(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("aaˇ aa aa aa aa").await; - cx.simulate_shared_keystrokes("/ a a enter").await; - cx.shared_state().await.assert_eq("aa ˇaa aa aa aa"); - cx.simulate_shared_keystrokes("g n").await; - cx.shared_state().await.assert_eq("aa «aaˇ» aa aa aa"); - cx.simulate_shared_keystrokes("g n").await; - cx.shared_state().await.assert_eq("aa «aa aaˇ» aa aa"); - cx.simulate_shared_keystrokes("escape d g n").await; - cx.shared_state().await.assert_eq("aa aa ˇ aa aa"); - - cx.set_shared_state("aaˇ aa aa aa aa").await; - cx.simulate_shared_keystrokes("/ a a enter").await; - cx.shared_state().await.assert_eq("aa ˇaa aa aa aa"); - cx.simulate_shared_keystrokes("3 g n").await; - cx.shared_state().await.assert_eq("aa aa aa «aaˇ» aa"); - - cx.set_shared_state("aaˇ aa aa aa aa").await; - cx.simulate_shared_keystrokes("/ a a enter").await; - cx.shared_state().await.assert_eq("aa ˇaa aa aa aa"); - cx.simulate_shared_keystrokes("g shift-n").await; - cx.shared_state().await.assert_eq("aa «ˇaa» aa aa aa"); - cx.simulate_shared_keystrokes("g shift-n").await; - cx.shared_state().await.assert_eq("«ˇaa aa» aa aa aa"); - } - - #[gpui::test] - async fn test_gl(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state("aaˇ aa\naa", Mode::Normal); - cx.simulate_keystrokes("g l"); - cx.assert_state("«aaˇ» «aaˇ»\naa", Mode::Visual); - cx.simulate_keystrokes("g >"); - cx.assert_state("«aaˇ» aa\n«aaˇ»", Mode::Visual); - } - - #[gpui::test] - async fn test_dgn_repeat(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("aaˇ aa aa aa aa").await; - cx.simulate_shared_keystrokes("/ a a enter").await; - cx.shared_state().await.assert_eq("aa ˇaa aa aa aa"); - cx.simulate_shared_keystrokes("d g n").await; - - cx.shared_state().await.assert_eq("aa ˇ aa aa aa"); - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq("aa ˇ aa aa"); - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq("aa ˇ aa"); - } - - #[gpui::test] - async fn test_cgn_repeat(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("aaˇ aa aa aa aa").await; - cx.simulate_shared_keystrokes("/ a a enter").await; - cx.shared_state().await.assert_eq("aa ˇaa aa aa aa"); - cx.simulate_shared_keystrokes("c g n x escape").await; - cx.shared_state().await.assert_eq("aa ˇx aa aa aa"); - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq("aa x ˇx aa aa"); - } - - #[gpui::test] - async fn test_cgn_nomatch(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state("aaˇ aa aa aa aa").await; - cx.simulate_shared_keystrokes("/ b b enter").await; - cx.shared_state().await.assert_eq("aaˇ aa aa aa aa"); - cx.simulate_shared_keystrokes("c g n x escape").await; - cx.shared_state().await.assert_eq("aaˇaa aa aa aa"); - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq("aaˇa aa aa aa"); - - cx.set_shared_state("aaˇ bb aa aa aa").await; - cx.simulate_shared_keystrokes("/ b b enter").await; - cx.shared_state().await.assert_eq("aa ˇbb aa aa aa"); - cx.simulate_shared_keystrokes("c g n x escape").await; - cx.shared_state().await.assert_eq("aa ˇx aa aa aa"); - cx.simulate_shared_keystrokes(".").await; - cx.shared_state().await.assert_eq("aa ˇx aa aa aa"); - } - - #[gpui::test] - async fn test_visual_shift_d(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "The ˇquick brown - fox jumps over - the lazy dog - " - }) - .await; - cx.simulate_shared_keystrokes("v down shift-d").await; - cx.shared_state().await.assert_eq(indoc! { - "the ˇlazy dog\n" - }); - - cx.set_shared_state(indoc! { - "The ˇquick brown - fox jumps over - the lazy dog - " - }) - .await; - cx.simulate_shared_keystrokes("ctrl-v down shift-d").await; - cx.shared_state().await.assert_eq(indoc! { - "Theˇ• - fox• - the lazy dog - " - }); - } - - #[gpui::test] - async fn test_shift_y(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "The ˇquick brown\n" - }) - .await; - cx.simulate_shared_keystrokes("v i w shift-y").await; - cx.shared_clipboard().await.assert_eq(indoc! { - "The quick brown\n" - }); - } - - #[gpui::test] - async fn test_gv(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "The ˇquick brown" - }) - .await; - cx.simulate_shared_keystrokes("v i w escape g v").await; - cx.shared_state().await.assert_eq(indoc! { - "The «quickˇ» brown" - }); - - cx.simulate_shared_keystrokes("o escape g v").await; - cx.shared_state().await.assert_eq(indoc! { - "The «ˇquick» brown" - }); - - cx.simulate_shared_keystrokes("escape ^ ctrl-v l").await; - cx.shared_state().await.assert_eq(indoc! { - "«Thˇ»e quick brown" - }); - cx.simulate_shared_keystrokes("g v").await; - cx.shared_state().await.assert_eq(indoc! { - "The «ˇquick» brown" - }); - cx.simulate_shared_keystrokes("g v").await; - cx.shared_state().await.assert_eq(indoc! { - "«Thˇ»e quick brown" - }); - - cx.set_state( - indoc! {" - fiˇsh one - fish two - fish red - fish blue - "}, - Mode::Normal, - ); - cx.simulate_keystrokes("4 g l escape escape g v"); - cx.assert_state( - indoc! {" - «fishˇ» one - «fishˇ» two - «fishˇ» red - «fishˇ» blue - "}, - Mode::Visual, - ); - cx.simulate_keystrokes("y g v"); - cx.assert_state( - indoc! {" - «fishˇ» one - «fishˇ» two - «fishˇ» red - «fishˇ» blue - "}, - Mode::Visual, - ); - } - - #[gpui::test] - async fn test_p_g_v_y(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "The - quicˇk - brown - fox" - }) - .await; - cx.simulate_shared_keystrokes("y y j shift-v p g v y").await; - cx.shared_state().await.assert_eq(indoc! { - "The - quick - ˇquick - fox" - }); - cx.shared_clipboard().await.assert_eq("quick\n"); - } - - #[gpui::test] - async fn test_v2ap(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx).await; - - cx.set_shared_state(indoc! { - "The - quicˇk - - brown - fox" - }) - .await; - cx.simulate_shared_keystrokes("v 2 a p").await; - cx.shared_state().await.assert_eq(indoc! { - "«The - quick - - brown - fˇ»ox" - }); - } - - #[gpui::test] - async fn test_visual_syntax_sibling_selection(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - cx.set_state( - indoc! {" - fn test() { - let ˇa = 1; - let b = 2; - let c = 3; - } - "}, - Mode::Normal, - ); - - // Enter visual mode and select the statement - cx.simulate_keystrokes("v w w w"); - cx.assert_state( - indoc! {" - fn test() { - let «a = 1;ˇ» - let b = 2; - let c = 3; - } - "}, - Mode::Visual, - ); - - // The specific behavior of syntax sibling selection in vim mode - // would depend on the key bindings configured, but the actions - // are now available for use - } -} diff --git a/crates/vim/test_data/neovim_backed_test_context_works.json b/crates/vim/test_data/neovim_backed_test_context_works.json deleted file mode 100644 index 3ed9f40f88..0000000000 --- a/crates/vim/test_data/neovim_backed_test_context_works.json +++ /dev/null @@ -1,3 +0,0 @@ -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"This is a tesˇt"}} -{"Get":{"state":"This is a tesˇt","mode":"Normal"}} diff --git a/crates/vim/test_data/test_a.json b/crates/vim/test_data/test_a.json deleted file mode 100644 index 8094974f98..0000000000 --- a/crates/vim/test_data/test_a.json +++ /dev/null @@ -1,6 +0,0 @@ -{"Put":{"state":"The qˇuick"}} -{"Key":"a"} -{"Get":{"state":"The quˇick","mode":"Insert"}} -{"Put":{"state":"The quicˇk"}} -{"Key":"a"} -{"Get":{"state":"The quickˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_around_containing_word_indent.json b/crates/vim/test_data/test_around_containing_word_indent.json deleted file mode 100644 index 6707ff6804..0000000000 --- a/crates/vim/test_data/test_around_containing_word_indent.json +++ /dev/null @@ -1,23 +0,0 @@ -{"Put":{"state":" ˇconst f = (x: unknown) => {"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":" «const ˇ»f = (x: unknown) => {","mode":"Visual"}} -{"Put":{"state":" ˇconst f = (x: unknown) => {"}} -{"Key":"y"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":" ˇconst f = (x: unknown) => {","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"const "}} -{"Put":{"state":" ˇconst f = (x: unknown) => {"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":" ˇf = (x: unknown) => {","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"const "}} -{"Put":{"state":" ˇconst f = (x: unknown) => {"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":" ˇf = (x: unknown) => {","mode":"Insert"}} -{"ReadRegister":{"name":"\"","value":"const "}} diff --git a/crates/vim/test_data/test_b.json b/crates/vim/test_data/test_b.json deleted file mode 100644 index 4324f9610d..0000000000 --- a/crates/vim/test_data/test_b.json +++ /dev/null @@ -1,54 +0,0 @@ -{"Put":{"state":"ˇThe quick-brown\n\n\nfox_jumps over\nthe"}} -{"Key":"b"} -{"Get":{"state":"ˇThe quick-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The ˇquick-brown\n\n\nfox_jumps over\nthe"}} -{"Key":"b"} -{"Get":{"state":"ˇThe quick-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe"}} -{"Key":"b"} -{"Get":{"state":"The ˇquick-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quick-ˇbrown\n\n\nfox_jumps over\nthe"}} -{"Key":"b"} -{"Get":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quick-brown\nˇ\n\nfox_jumps over\nthe"}} -{"Key":"b"} -{"Get":{"state":"The quick-ˇbrown\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quick-brown\n\nˇ\nfox_jumps over\nthe"}} -{"Key":"b"} -{"Get":{"state":"The quick-brown\nˇ\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quick-brown\n\n\nˇfox_jumps over\nthe"}} -{"Key":"b"} -{"Get":{"state":"The quick-brown\n\nˇ\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quick-brown\n\n\nfox_jumps ˇover\nthe"}} -{"Key":"b"} -{"Get":{"state":"The quick-brown\n\n\nˇfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quick-brown\n\n\nfox_jumps over\nˇthe"}} -{"Key":"b"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps ˇover\nthe","mode":"Normal"}} -{"Put":{"state":"ˇThe quick-brown\n\n\nfox_jumps over\nthe"}} -{"Key":"shift-b"} -{"Get":{"state":"ˇThe quick-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The ˇquick-brown\n\n\nfox_jumps over\nthe"}} -{"Key":"shift-b"} -{"Get":{"state":"ˇThe quick-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe"}} -{"Key":"shift-b"} -{"Get":{"state":"The ˇquick-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quick-ˇbrown\n\n\nfox_jumps over\nthe"}} -{"Key":"shift-b"} -{"Get":{"state":"The ˇquick-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quick-brown\nˇ\n\nfox_jumps over\nthe"}} -{"Key":"shift-b"} -{"Get":{"state":"The ˇquick-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quick-brown\n\nˇ\nfox_jumps over\nthe"}} -{"Key":"shift-b"} -{"Get":{"state":"The quick-brown\nˇ\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quick-brown\n\n\nˇfox_jumps over\nthe"}} -{"Key":"shift-b"} -{"Get":{"state":"The quick-brown\n\nˇ\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quick-brown\n\n\nfox_jumps ˇover\nthe"}} -{"Key":"shift-b"} -{"Get":{"state":"The quick-brown\n\n\nˇfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quick-brown\n\n\nfox_jumps over\nˇthe"}} -{"Key":"shift-b"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps ˇover\nthe","mode":"Normal"}} diff --git a/crates/vim/test_data/test_backspace.json b/crates/vim/test_data/test_backspace.json deleted file mode 100644 index b11a2562db..0000000000 --- a/crates/vim/test_data/test_backspace.json +++ /dev/null @@ -1,9 +0,0 @@ -{"Put":{"state":"ˇThe quick\nbrown"}} -{"Key":"backspace"} -{"Get":{"state":"ˇThe quick\nbrown","mode":"Normal"}} -{"Put":{"state":"The qˇuick\nbrown"}} -{"Key":"backspace"} -{"Get":{"state":"The ˇquick\nbrown","mode":"Normal"}} -{"Put":{"state":"The quick\nˇbrown"}} -{"Key":"backspace"} -{"Get":{"state":"The quicˇk\nbrown","mode":"Normal"}} diff --git a/crates/vim/test_data/test_backspace_non_ascii_bol.json b/crates/vim/test_data/test_backspace_non_ascii_bol.json deleted file mode 100644 index 88536bc50d..0000000000 --- a/crates/vim/test_data/test_backspace_non_ascii_bol.json +++ /dev/null @@ -1,4 +0,0 @@ -{"Put":{"state":"ππππ\nπanˇotherline"}} -{"Key":"4"} -{"Key":"backspace"} -{"Get":{"state":"πππˇπ\nπanotherline","mode":"Normal"}} diff --git a/crates/vim/test_data/test_backwards_n.json b/crates/vim/test_data/test_backwards_n.json deleted file mode 100644 index a08cc84d21..0000000000 --- a/crates/vim/test_data/test_backwards_n.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Put":{"state":"ˇa b a b a b a"}} -{"Key":"*"} -{"Key":"n"} -{"Get":{"state":"a b a b ˇa b a","mode":"Normal"}} -{"Key":"#"} -{"Get":{"state":"a b ˇa b a b a","mode":"Normal"}} -{"Key":"n"} -{"Get":{"state":"ˇa b a b a b a","mode":"Normal"}} diff --git a/crates/vim/test_data/test_blackhole_register.json b/crates/vim/test_data/test_blackhole_register.json deleted file mode 100644 index e16bb3abe3..0000000000 --- a/crates/vim/test_data/test_blackhole_register.json +++ /dev/null @@ -1,11 +0,0 @@ -{"Put":{"state":"ˇhello world"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Key":"\""} -{"Key":"_"} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Key":"p"} -{"Get":{"state":"hellˇo","mode":"Normal"}} diff --git a/crates/vim/test_data/test_builtin_marks.json b/crates/vim/test_data/test_builtin_marks.json deleted file mode 100644 index 0d05385960..0000000000 --- a/crates/vim/test_data/test_builtin_marks.json +++ /dev/null @@ -1,36 +0,0 @@ -{"Put":{"state":"Line one\nLine two\nLine ˇthree\nLine four\nLine five\n"}} -{"Key":"v"} -{"Key":"j"} -{"Key":"escape"} -{"Key":"k"} -{"Key":"k"} -{"Key":"'"} -{"Key":"<"} -{"Get":{"state":"Line one\nLine two\nˇLine three\nLine four\nLine five\n","mode":"Normal"}} -{"Key":"`"} -{"Key":"<"} -{"Get":{"state":"Line one\nLine two\nLine ˇthree\nLine four\nLine five\n","mode":"Normal"}} -{"Key":"'"} -{"Key":">"} -{"Get":{"state":"Line one\nLine two\nLine three\nˇLine four\nLine five\n","mode":"Normal"}} -{"Key":"`"} -{"Key":">"} -{"Get":{"state":"Line one\nLine two\nLine three\nLine ˇfour\nLine five\n","mode":"Normal"}} -{"Key":"g"} -{"Key":"g"} -{"Key":"^"} -{"Key":"j"} -{"Key":"j"} -{"Key":"l"} -{"Key":"l"} -{"Key":"c"} -{"Key":"e"} -{"Key":"k"} -{"Key":"e"} -{"Key":"escape"} -{"Key":"'"} -{"Key":"."} -{"Get":{"state":"Line one\nLine two\nˇLike three\nLine four\nLine five\n","mode":"Normal"}} -{"Key":"`"} -{"Key":"."} -{"Get":{"state":"Line one\nLine two\nLiˇke three\nLine four\nLine five\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_capital_f_and_capital_t.json b/crates/vim/test_data/test_capital_f_and_capital_t.json deleted file mode 100644 index 8ef45ec623..0000000000 --- a/crates/vim/test_data/test_capital_f_and_capital_t.json +++ /dev/null @@ -1,570 +0,0 @@ -{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n"}} -{"Key":"1"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n","mode":"Normal"}} -{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n"}} -{"Key":"1"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n","mode":"Normal"}} -{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n"}} -{"Key":"2"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n","mode":"Normal"}} -{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n"}} -{"Key":"2"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n","mode":"Normal"}} -{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n"}} -{"Key":"3"} -{"Key":"shift-f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n","mode":"Normal"}} -{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ \nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n"}} -{"Key":"3"} -{"Key":"shift-t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n \nˇb\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_caret_mark.json b/crates/vim/test_data/test_caret_mark.json deleted file mode 100644 index 6a117e9968..0000000000 --- a/crates/vim/test_data/test_caret_mark.json +++ /dev/null @@ -1,35 +0,0 @@ -{"Put":{"state":"Line one\nLine two\nLine three\nˇLine four\nLine five\n"}} -{"Key":"c"} -{"Key":"w"} -{"Key":"shift-s"} -{"Key":"t"} -{"Key":"r"} -{"Key":"a"} -{"Key":"i"} -{"Key":"g"} -{"Key":"h"} -{"Key":"t"} -{"Key":"space"} -{"Key":"t"} -{"Key":"h"} -{"Key":"i"} -{"Key":"n"} -{"Key":"g"} -{"Key":"escape"} -{"Key":"j"} -{"Key":"j"} -{"Key":"'"} -{"Key":"^"} -{"Get":{"state":"Line one\nLine two\nLine three\nˇStraight thing four\nLine five\n","mode":"Normal"}} -{"Key":"`"} -{"Key":"^"} -{"Get":{"state":"Line one\nLine two\nLine three\nStraight thingˇ four\nLine five\n","mode":"Normal"}} -{"Key":"k"} -{"Key":"a"} -{"Key":"!"} -{"Key":"escape"} -{"Key":"k"} -{"Key":"g"} -{"Key":"i"} -{"Key":"?"} -{"Get":{"state":"Line one\nLine two\nLine three!?ˇ\nStraight thing four\nLine five\n","mode":"Insert"}} diff --git a/crates/vim/test_data/test_cc.json b/crates/vim/test_data/test_cc.json deleted file mode 100644 index d4b4a499bb..0000000000 --- a/crates/vim/test_data/test_cc.json +++ /dev/null @@ -1,24 +0,0 @@ -{"Put":{"state":"ˇ"}} -{"Key":"c"} -{"Key":"c"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The ˇquick"}} -{"Key":"c"} -{"Key":"c"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quˇick\nbrown fox\njumps over"}} -{"Key":"c"} -{"Key":"c"} -{"Get":{"state":"ˇ\nbrown fox\njumps over","mode":"Insert"}} -{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}} -{"Key":"c"} -{"Key":"c"} -{"Get":{"state":"The quick\nˇ\njumps over","mode":"Insert"}} -{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}} -{"Key":"c"} -{"Key":"c"} -{"Get":{"state":"The quick\nbrown fox\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick\nˇ\nbrown fox"}} -{"Key":"c"} -{"Key":"c"} -{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Insert"}} diff --git a/crates/vim/test_data/test_cgn_nomatch.json b/crates/vim/test_data/test_cgn_nomatch.json deleted file mode 100644 index 9c2f02bb85..0000000000 --- a/crates/vim/test_data/test_cgn_nomatch.json +++ /dev/null @@ -1,28 +0,0 @@ -{"Put":{"state":"aaˇ aa aa aa aa"}} -{"Key":"/"} -{"Key":"b"} -{"Key":"b"} -{"Key":"enter"} -{"Get":{"state":"aaˇ aa aa aa aa","mode":"Normal"}} -{"Key":"c"} -{"Key":"g"} -{"Key":"n"} -{"Key":"x"} -{"Key":"escape"} -{"Get":{"state":"aaˇaa aa aa aa","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"aaˇa aa aa aa","mode":"Normal"}} -{"Put":{"state":"aaˇ bb aa aa aa"}} -{"Key":"/"} -{"Key":"b"} -{"Key":"b"} -{"Key":"enter"} -{"Get":{"state":"aa ˇbb aa aa aa","mode":"Normal"}} -{"Key":"c"} -{"Key":"g"} -{"Key":"n"} -{"Key":"x"} -{"Key":"escape"} -{"Get":{"state":"aa ˇx aa aa aa","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"aa ˇx aa aa aa","mode":"Normal"}} diff --git a/crates/vim/test_data/test_cgn_repeat.json b/crates/vim/test_data/test_cgn_repeat.json deleted file mode 100644 index 4683a83d07..0000000000 --- a/crates/vim/test_data/test_cgn_repeat.json +++ /dev/null @@ -1,14 +0,0 @@ -{"Put":{"state":"aaˇ aa aa aa aa"}} -{"Key":"/"} -{"Key":"a"} -{"Key":"a"} -{"Key":"enter"} -{"Get":{"state":"aa ˇaa aa aa aa","mode":"Normal"}} -{"Key":"c"} -{"Key":"g"} -{"Key":"n"} -{"Key":"x"} -{"Key":"escape"} -{"Get":{"state":"aa ˇx aa aa aa","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"aa x ˇx aa aa","mode":"Normal"}} diff --git a/crates/vim/test_data/test_change_0.json b/crates/vim/test_data/test_change_0.json deleted file mode 100644 index 90668f4a17..0000000000 --- a/crates/vim/test_data/test_change_0.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Put":{"state":"The qˇuick\nbrown fox"}} -{"Key":"c"} -{"Key":"0"} -{"Get":{"state":"ˇuick\nbrown fox","mode":"Insert"}} -{"Put":{"state":"The quick\nˇ\nbrown fox"}} -{"Key":"c"} -{"Key":"0"} -{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Insert"}} diff --git a/crates/vim/test_data/test_change_b.json b/crates/vim/test_data/test_change_b.json deleted file mode 100644 index d43cc04c45..0000000000 --- a/crates/vim/test_data/test_change_b.json +++ /dev/null @@ -1,24 +0,0 @@ -{"Put":{"state":"Teˇst Test"}} -{"Key":"c"} -{"Key":"b"} -{"Get":{"state":"ˇst Test","mode":"Insert"}} -{"Put":{"state":"Test ˇtest"}} -{"Key":"c"} -{"Key":"b"} -{"Get":{"state":"ˇtest","mode":"Insert"}} -{"Put":{"state":"Test1 test2 ˇtest3"}} -{"Key":"c"} -{"Key":"b"} -{"Get":{"state":"Test1 ˇtest3","mode":"Insert"}} -{"Put":{"state":"Test test\nˇtest"}} -{"Key":"c"} -{"Key":"b"} -{"Get":{"state":"Test ˇ\ntest","mode":"Insert"}} -{"Put":{"state":"Test test\nˇ\ntest"}} -{"Key":"c"} -{"Key":"b"} -{"Get":{"state":"Test ˇ\n\ntest","mode":"Insert"}} -{"Put":{"state":"Test test-test ˇtest"}} -{"Key":"c"} -{"Key":"shift-b"} -{"Get":{"state":"Test ˇtest","mode":"Insert"}} diff --git a/crates/vim/test_data/test_change_backspace.json b/crates/vim/test_data/test_change_backspace.json deleted file mode 100644 index 508500163b..0000000000 --- a/crates/vim/test_data/test_change_backspace.json +++ /dev/null @@ -1,16 +0,0 @@ -{"Put":{"state":"Teˇst"}} -{"Key":"c"} -{"Key":"backspace"} -{"Get":{"state":"Tˇst","mode":"Insert"}} -{"Put":{"state":"Tˇest"}} -{"Key":"c"} -{"Key":"backspace"} -{"Get":{"state":"ˇest","mode":"Insert"}} -{"Put":{"state":"ˇTest"}} -{"Key":"c"} -{"Key":"backspace"} -{"Get":{"state":"ˇTest","mode":"Insert"}} -{"Put":{"state":"Test\nˇtest"}} -{"Key":"c"} -{"Key":"backspace"} -{"Get":{"state":"Testˇtest","mode":"Insert"}} diff --git a/crates/vim/test_data/test_change_case.json b/crates/vim/test_data/test_change_case.json deleted file mode 100644 index 10eb93b227..0000000000 --- a/crates/vim/test_data/test_change_case.json +++ /dev/null @@ -1,23 +0,0 @@ -{"Put":{"state":"ˇabC\n"}} -{"Key":"~"} -{"Get":{"state":"AˇbC\n","mode":"Normal"}} -{"Key":"2"} -{"Key":"~"} -{"Get":{"state":"ABˇc\n","mode":"Normal"}} -{"Put":{"state":"a😀C«dÉ1*fˇ»\n"}} -{"Key":"~"} -{"Get":{"state":"a😀CˇDé1*F\n","mode":"Normal"}} -{"Key":"~"} -{"Put":{"state":"aˇC😀é1*F\n"}} -{"Key":"4"} -{"Key":"~"} -{"Get":{"state":"ac😀É1ˇ*F\n","mode":"Normal"}} -{"Put":{"state":"abˇC\n"}} -{"Key":"shift-v"} -{"Key":"~"} -{"Get":{"state":"ˇABc\n","mode":"Normal"}} -{"Put":{"state":"ˇaa\nbb\ncc"}} -{"Key":"ctrl-v"} -{"Key":"j"} -{"Key":"~"} -{"Get":{"state":"ˇAa\nBb\ncc","mode":"Normal"}} diff --git a/crates/vim/test_data/test_change_case_motion.json b/crates/vim/test_data/test_change_case_motion.json deleted file mode 100644 index 18921f08a2..0000000000 --- a/crates/vim/test_data/test_change_case_motion.json +++ /dev/null @@ -1,27 +0,0 @@ -{"Put":{"state":"ˇabc def"}} -{"Key":"g"} -{"Key":"shift-u"} -{"Key":"w"} -{"Get":{"state":"ˇABC def","mode":"Normal"}} -{"Key":"g"} -{"Key":"u"} -{"Key":"w"} -{"Get":{"state":"ˇabc def","mode":"Normal"}} -{"Key":"g"} -{"Key":"~"} -{"Key":"w"} -{"Get":{"state":"ˇABC def","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"ˇabc def","mode":"Normal"}} -{"Put":{"state":"abˇc def"}} -{"Key":"g"} -{"Key":"~"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"ˇABC def","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"ˇabc def","mode":"Normal"}} -{"Key":"g"} -{"Key":"shift-u"} -{"Key":"$"} -{"Get":{"state":"ˇABC DEF","mode":"Normal"}} diff --git a/crates/vim/test_data/test_change_case_motion_object.json b/crates/vim/test_data/test_change_case_motion_object.json deleted file mode 100644 index b2157975bd..0000000000 --- a/crates/vim/test_data/test_change_case_motion_object.json +++ /dev/null @@ -1,6 +0,0 @@ -{"Put":{"state":"abc dˇef\n"}} -{"Key":"g"} -{"Key":"shift-u"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"abc ˇDEF\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_change_cc.json b/crates/vim/test_data/test_change_cc.json deleted file mode 100644 index e2d1cb8d21..0000000000 --- a/crates/vim/test_data/test_change_cc.json +++ /dev/null @@ -1,12 +0,0 @@ -{"Put":{"state":"The quick\n brownˇ fox\njumps over\nthe lazy"}} -{"Key":"c"} -{"Key":"c"} -{"Get":{"state":"The quick\n ˇ\njumps over\nthe lazy","mode":"Insert"}} -{"Put":{"state":"ˇThe quick\nbrown fox\njumps over\nthe lazy"}} -{"Key":"c"} -{"Key":"c"} -{"Get":{"state":"ˇ\nbrown fox\njumps over\nthe lazy","mode":"Insert"}} -{"Put":{"state":"The quick\n broˇwn fox\njumps over\nthe lazy"}} -{"Key":"c"} -{"Key":"c"} -{"Get":{"state":"The quick\n ˇ\njumps over\nthe lazy","mode":"Insert"}} diff --git a/crates/vim/test_data/test_change_e.json b/crates/vim/test_data/test_change_e.json deleted file mode 100644 index fc9f91fe02..0000000000 --- a/crates/vim/test_data/test_change_e.json +++ /dev/null @@ -1,24 +0,0 @@ -{"Put":{"state":"Teˇst Test"}} -{"Key":"c"} -{"Key":"e"} -{"Get":{"state":"Teˇ Test","mode":"Insert"}} -{"Put":{"state":"Tˇest test"}} -{"Key":"c"} -{"Key":"e"} -{"Get":{"state":"Tˇ test","mode":"Insert"}} -{"Put":{"state":"Test teˇst\ntest"}} -{"Key":"c"} -{"Key":"e"} -{"Get":{"state":"Test teˇ\ntest","mode":"Insert"}} -{"Put":{"state":"Test tesˇt\ntest"}} -{"Key":"c"} -{"Key":"e"} -{"Get":{"state":"Test tesˇ","mode":"Insert"}} -{"Put":{"state":"Test test\nˇ\ntest"}} -{"Key":"c"} -{"Key":"e"} -{"Get":{"state":"Test test\nˇ","mode":"Insert"}} -{"Put":{"state":"Test teˇst-test test"}} -{"Key":"c"} -{"Key":"shift-e"} -{"Get":{"state":"Test teˇ test","mode":"Insert"}} diff --git a/crates/vim/test_data/test_change_end_of_document.json b/crates/vim/test_data/test_change_end_of_document.json deleted file mode 100644 index a3f09e4453..0000000000 --- a/crates/vim/test_data/test_change_end_of_document.json +++ /dev/null @@ -1,16 +0,0 @@ -{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}} -{"Key":"c"} -{"Key":"shift-g"} -{"Get":{"state":"The quick\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}} -{"Key":"c"} -{"Key":"shift-g"} -{"Get":{"state":"The quick\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick\nbrown fox\njumps over\nthe lˇazy"}} -{"Key":"c"} -{"Key":"shift-g"} -{"Get":{"state":"The quick\nbrown fox\njumps over\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick\nbrown fox\njumps over\nˇ"}} -{"Key":"c"} -{"Key":"shift-g"} -{"Get":{"state":"The quick\nbrown fox\njumps over\nˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_change_end_of_line.json b/crates/vim/test_data/test_change_end_of_line.json deleted file mode 100644 index 44766df85e..0000000000 --- a/crates/vim/test_data/test_change_end_of_line.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Put":{"state":"The qˇuick\nbrown fox"}} -{"Key":"c"} -{"Key":"$"} -{"Get":{"state":"The qˇ\nbrown fox","mode":"Insert"}} -{"Put":{"state":"The quick\nˇ\nbrown fox"}} -{"Key":"c"} -{"Key":"$"} -{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Insert"}} diff --git a/crates/vim/test_data/test_change_gg.json b/crates/vim/test_data/test_change_gg.json deleted file mode 100644 index f4271f7120..0000000000 --- a/crates/vim/test_data/test_change_gg.json +++ /dev/null @@ -1,20 +0,0 @@ -{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}} -{"Key":"c"} -{"Key":"g"} -{"Key":"g"} -{"Get":{"state":"ˇ\njumps over\nthe lazy","mode":"Insert"}} -{"Put":{"state":"The quick\nbrown fox\njumps over\nthe lˇazy"}} -{"Key":"c"} -{"Key":"g"} -{"Key":"g"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The qˇuick\nbrown fox\njumps over\nthe lazy"}} -{"Key":"c"} -{"Key":"g"} -{"Key":"g"} -{"Get":{"state":"ˇ\nbrown fox\njumps over\nthe lazy","mode":"Insert"}} -{"Put":{"state":"ˇ\nbrown fox\njumps over\nthe lazy"}} -{"Key":"c"} -{"Key":"g"} -{"Key":"g"} -{"Get":{"state":"ˇ\nbrown fox\njumps over\nthe lazy","mode":"Insert"}} diff --git a/crates/vim/test_data/test_change_h.json b/crates/vim/test_data/test_change_h.json deleted file mode 100644 index 6acfb5d080..0000000000 --- a/crates/vim/test_data/test_change_h.json +++ /dev/null @@ -1,16 +0,0 @@ -{"Put":{"state":"Teˇst"}} -{"Key":"c"} -{"Key":"h"} -{"Get":{"state":"Tˇst","mode":"Insert"}} -{"Put":{"state":"Tˇest"}} -{"Key":"c"} -{"Key":"h"} -{"Get":{"state":"ˇest","mode":"Insert"}} -{"Put":{"state":"ˇTest"}} -{"Key":"c"} -{"Key":"h"} -{"Get":{"state":"ˇTest","mode":"Insert"}} -{"Put":{"state":"Test\nˇtest"}} -{"Key":"c"} -{"Key":"h"} -{"Get":{"state":"Test\nˇtest","mode":"Insert"}} diff --git a/crates/vim/test_data/test_change_j.json b/crates/vim/test_data/test_change_j.json deleted file mode 100644 index 3808bb21e2..0000000000 --- a/crates/vim/test_data/test_change_j.json +++ /dev/null @@ -1,20 +0,0 @@ -{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}} -{"Key":"c"} -{"Key":"j"} -{"Get":{"state":"The quick\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}} -{"Key":"c"} -{"Key":"j"} -{"Get":{"state":"The quick\nbrown fox\njumps ˇover","mode":"Normal"}} -{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}} -{"Key":"c"} -{"Key":"j"} -{"Get":{"state":"ˇ\njumps over","mode":"Insert"}} -{"Put":{"state":"The quick\nbrown fox\nˇ"}} -{"Key":"c"} -{"Key":"j"} -{"Get":{"state":"The quick\nbrown fox\nˇ","mode":"Normal"}} -{"Put":{"state":"The quick\n ˇbrown fox\n jumps over"}} -{"Key":"c"} -{"Key":"j"} -{"Get":{"state":"The quick\n ˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_change_k.json b/crates/vim/test_data/test_change_k.json deleted file mode 100644 index 5ac92744d1..0000000000 --- a/crates/vim/test_data/test_change_k.json +++ /dev/null @@ -1,20 +0,0 @@ -{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}} -{"Key":"c"} -{"Key":"k"} -{"Get":{"state":"ˇ\njumps over","mode":"Insert"}} -{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}} -{"Key":"c"} -{"Key":"k"} -{"Get":{"state":"The quick\nˇ","mode":"Insert"}} -{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}} -{"Key":"c"} -{"Key":"k"} -{"Get":{"state":"The qˇuick\nbrown fox\njumps over","mode":"Normal"}} -{"Put":{"state":"ˇ\nbrown fox\njumps over"}} -{"Key":"c"} -{"Key":"k"} -{"Get":{"state":"ˇ\nbrown fox\njumps over","mode":"Normal"}} -{"Put":{"state":"The quick\n brown fox\n ˇjumps over"}} -{"Key":"c"} -{"Key":"k"} -{"Get":{"state":"The quick\n ˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_change_l.json b/crates/vim/test_data/test_change_l.json deleted file mode 100644 index 378c5dc7ca..0000000000 --- a/crates/vim/test_data/test_change_l.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Put":{"state":"Teˇst"}} -{"Key":"c"} -{"Key":"l"} -{"Get":{"state":"Teˇt","mode":"Insert"}} -{"Put":{"state":"Tesˇt"}} -{"Key":"c"} -{"Key":"l"} -{"Get":{"state":"Tesˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_change_list_delete.json b/crates/vim/test_data/test_change_list_delete.json deleted file mode 100644 index ad8b1cbd9e..0000000000 --- a/crates/vim/test_data/test_change_list_delete.json +++ /dev/null @@ -1,16 +0,0 @@ -{"Put":{"state":"one two\nthree fˇour"}} -{"Key":"x"} -{"Key":"k"} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Key":"^"} -{"Key":"x"} -{"Get":{"state":"ˇne \nthree fur","mode":"Normal"}} -{"Key":"2"} -{"Key":"g"} -{"Key":";"} -{"Get":{"state":"ne \nthree fˇur","mode":"Normal"}} -{"Key":"g"} -{"Key":","} -{"Get":{"state":"ˇne \nthree fur","mode":"Normal"}} diff --git a/crates/vim/test_data/test_change_list_insert.json b/crates/vim/test_data/test_change_list_insert.json deleted file mode 100644 index d72878d255..0000000000 --- a/crates/vim/test_data/test_change_list_insert.json +++ /dev/null @@ -1,32 +0,0 @@ -{"Put":{"state":"ˇ"}} -{"Key":"i"} -{"Key":"1"} -{"Key":"1"} -{"Key":"escape"} -{"Key":"shift-o"} -{"Key":"2"} -{"Key":"2"} -{"Key":"escape"} -{"Key":"shift-g"} -{"Key":"o"} -{"Key":"3"} -{"Key":"3"} -{"Key":"escape"} -{"Get":{"state":"22\n11\n3ˇ3","mode":"Normal"}} -{"Key":"g"} -{"Key":";"} -{"Key":"g"} -{"Key":";"} -{"Key":"g"} -{"Key":";"} -{"Key":"g"} -{"Key":","} -{"Key":"shift-g"} -{"Key":"i"} -{"Key":"4"} -{"Key":"4"} -{"Key":"escape"} -{"Key":"g"} -{"Key":";"} -{"Key":"g"} -{"Key":";"} diff --git a/crates/vim/test_data/test_change_paragraph.json b/crates/vim/test_data/test_change_paragraph.json deleted file mode 100644 index 6d235d9f36..0000000000 --- a/crates/vim/test_data/test_change_paragraph.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Put":{"state":"first paragraph\nˇstill first\n\nsecond paragraph\nstill second\n\nthird paragraph\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ\nsecond paragraph\nstill second\n\nthird paragraph\n","mode":"Insert"}} -{"ReadRegister":{"name":"\"","value":"first paragraph\nstill first\n\n"}} -{"Key":"escape"} -{"Get":{"state":"ˇ\nsecond paragraph\nstill second\n\nthird paragraph\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_change_paragraph_object.json b/crates/vim/test_data/test_change_paragraph_object.json deleted file mode 100644 index 7de16dac5b..0000000000 --- a/crates/vim/test_data/test_change_paragraph_object.json +++ /dev/null @@ -1,430 +0,0 @@ -{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog."}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumpˇs over the lazy dog."}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dogˇ."}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.ˇ"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog."}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumpˇs over the lazy dog."}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dogˇ."}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.ˇ"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brownˇ\nfox jumps over\nthe lazy dog.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇfox jumps over\nthe lazy dog.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps overˇ\nthe lazy dog.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\n","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brownˇ\nfox jumps over\nthe lazy dog.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇfox jumps over\nthe lazy dog.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps overˇ\nthe lazy dog.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"ˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}} -{"Put":{"state":"\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}} -{"Put":{"state":"\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nˇ\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nˇover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nˇ\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ\n","mode":"Insert"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumpsˇ\nover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ\n","mode":"Insert"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nˇover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ\n","mode":"Insert"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ\n","mode":"Insert"}} -{"Put":{"state":"ˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}} -{"Put":{"state":"\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}} -{"Put":{"state":"\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nˇover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Insert"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n","mode":"Insert"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n","mode":"Insert"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Insert"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumpsˇ\nover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Insert"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nˇover the lazy dog.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Insert"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nˇover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ\n\n\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumpsˇ\nover the lazy dog.\n\n\n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ\n\n\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nˇover the lazy dog.\n\n\n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ\n\n\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n\n\n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ\n\n\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ\n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nˇover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumpsˇ\nover the lazy dog.\n\n\n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nˇover the lazy dog.\n\n\n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n\n\n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ\n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ\n","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\nˇ \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nˇ\n\n \t\n\t \t\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n\n \t\n\t \t\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nˇ\n\n \t\n\t \t\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n \t\n\t \t\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ \t\n\t \t\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\nˇ \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n \t\n\t \t\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n\n \t\n\t \t\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n \t\n\t \t\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n \t\n\t \t\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ \t\n\t \t\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ \t\n\t \t\n","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\n\nThe quick brown fox jumps over the lazy dog.\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\nˇ\nThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\nˇ\nThe quick brown fox jumps over the lazy dog.\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇ\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumpˇs over the lazy dog.\n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇ\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.ˇ\n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇ\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\nˇ\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\nˇ","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ\nThe quick brown fox jumps over the lazy dog.\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\nˇ\nThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\nˇ\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumpˇs over the lazy dog.\n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.ˇ\n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\nˇ\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\nˇ\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_change_paragraph_object_with_soft_wrap.json b/crates/vim/test_data/test_change_paragraph_object_with_soft_wrap.json deleted file mode 100644 index 47d68e13a6..0000000000 --- a/crates/vim/test_data/test_change_paragraph_object_with_soft_wrap.json +++ /dev/null @@ -1,72 +0,0 @@ -{"SetOption":{"value":"wrap"}} -{"SetOption":{"value":"columns=20"}} -{"Put":{"state":"ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ\n","mode":"Insert"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ\n","mode":"Insert"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.ˇ\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ\n","mode":"Insert"}} -{"Put":{"state":"ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Insert"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Insert"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Insert"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.ˇ\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_change_rot13_motion.json b/crates/vim/test_data/test_change_rot13_motion.json deleted file mode 100644 index 62c39887ff..0000000000 --- a/crates/vim/test_data/test_change_rot13_motion.json +++ /dev/null @@ -1,23 +0,0 @@ -{"Put":{"state":"ˇabc def"}} -{"Key":"g"} -{"Key":"?"} -{"Key":"w"} -{"Get":{"state":"ˇnop def","mode":"Normal"}} -{"Key":"g"} -{"Key":"?"} -{"Key":"w"} -{"Get":{"state":"ˇabc def","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"ˇnop def","mode":"Normal"}} -{"Put":{"state":"abˇc def"}} -{"Key":"g"} -{"Key":"?"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"ˇnop def","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"ˇabc def","mode":"Normal"}} -{"Key":"g"} -{"Key":"?"} -{"Key":"$"} -{"Get":{"state":"ˇnop qrs","mode":"Normal"}} diff --git a/crates/vim/test_data/test_change_rot13_object.json b/crates/vim/test_data/test_change_rot13_object.json deleted file mode 100644 index 19db51b946..0000000000 --- a/crates/vim/test_data/test_change_rot13_object.json +++ /dev/null @@ -1,6 +0,0 @@ -{"Put":{"state":"ˇabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"}} -{"Key":"g"} -{"Key":"?"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"ˇnopqrstuvwxyzabcdefghijklmNOPQRSTUVWXYZABCDEFGHIJKLM","mode":"Normal"}} diff --git a/crates/vim/test_data/test_change_sentence_object.json b/crates/vim/test_data/test_change_sentence_object.json deleted file mode 100644 index 4afbae2713..0000000000 --- a/crates/vim/test_data/test_change_sentence_object.json +++ /dev/null @@ -1,270 +0,0 @@ -{"Put":{"state":"ˇThe quick brown? Fox Jumps! Over the lazy."}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Fox Jumps! Over the lazy.","mode":"Insert"}} -{"Put":{"state":"The quick ˇbrown? Fox Jumps! Over the lazy."}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Fox Jumps! Over the lazy.","mode":"Insert"}} -{"Put":{"state":"The quick brownˇ? Fox Jumps! Over the lazy."}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Fox Jumps! Over the lazy.","mode":"Insert"}} -{"Put":{"state":"The quick brown?ˇ Fox Jumps! Over the lazy."}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown?ˇFox Jumps! Over the lazy.","mode":"Insert"}} -{"Put":{"state":"The quick brown? ˇFox Jumps! Over the lazy."}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown? ˇ Over the lazy.","mode":"Insert"}} -{"Put":{"state":"The quick brown? Fox Jˇumps! Over the lazy."}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown? ˇ Over the lazy.","mode":"Insert"}} -{"Put":{"state":"The quick brown? Fox Jumpsˇ! Over the lazy."}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown? ˇ Over the lazy.","mode":"Insert"}} -{"Put":{"state":"The quick brown? Fox Jumps!ˇ Over the lazy."}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown? Fox Jumps!ˇOver the lazy.","mode":"Insert"}} -{"Put":{"state":"The quick brown? Fox Jumps! Ovˇer the lazy."}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown? Fox Jumps! ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown? Fox Jumps! Over theˇ lazy."}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown? Fox Jumps! ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown? Fox Jumps! Over the lazyˇ."}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown? Fox Jumps! ˇ","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ The quick \nbrown fox jumps over\n","mode":"Insert"}} -{"Put":{"state":"The quick ˇbrown\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ The quick \nbrown fox jumps over\n","mode":"Insert"}} -{"Put":{"state":"The quick brownˇ\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ The quick \nbrown fox jumps over\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy doˇg. The quick \nbrown fox jumps over\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ The quick \nbrown fox jumps over\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dogˇ. The quick \nbrown fox jumps over\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ The quick \nbrown fox jumps over\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ The quick \nbrown fox jumps over\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇThe quick \nbrown fox jumps over\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog. ˇThe quick \nbrown fox jumps over\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog. ˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog. The quick ˇ\nbrown fox jumps over\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog. ˇ\n","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown.)]'\" Brown fox jumps. "}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Insert"}} -{"Put":{"state":"The ˇquick brown.)]'\" Brown fox jumps. "}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Insert"}} -{"Put":{"state":"The quick brownˇ.)]'\" Brown fox jumps. "}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Insert"}} -{"Put":{"state":"The quick brown.)ˇ]'\" Brown fox jumps. "}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Insert"}} -{"Put":{"state":"The quick brown.)]ˇ'\" Brown fox jumps. "}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Insert"}} -{"Put":{"state":"The quick brown.)]'ˇ\" Brown fox jumps. "}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Insert"}} -{"Put":{"state":"The quick brown.)]'\" Brown ˇfox jumps. "}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown.)]'\" ˇ ","mode":"Insert"}} -{"Put":{"state":"The quick brown.)]'\" Brown fox jumpsˇ. "}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown.)]'\" ˇ ","mode":"Insert"}} -{"Put":{"state":"The quick brown.)]'\" Brown fox jumps.ˇ "}} -{"Key":"c"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown.)]'\" Brown fox jumps.ˇ","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown? Fox Jumps! Over the lazy."}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇFox Jumps! Over the lazy.","mode":"Insert"}} -{"Put":{"state":"The quick ˇbrown? Fox Jumps! Over the lazy."}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇFox Jumps! Over the lazy.","mode":"Insert"}} -{"Put":{"state":"The quick brownˇ? Fox Jumps! Over the lazy."}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇFox Jumps! Over the lazy.","mode":"Insert"}} -{"Put":{"state":"The quick brown? ˇFox Jumps! Over the lazy."}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown? ˇOver the lazy.","mode":"Insert"}} -{"Put":{"state":"The quick brown? Fox Jˇumps! Over the lazy."}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown? ˇOver the lazy.","mode":"Insert"}} -{"Put":{"state":"The quick brown? Fox Jumpsˇ! Over the lazy."}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown? ˇOver the lazy.","mode":"Insert"}} -{"Put":{"state":"The quick brown? Fox Jumps!ˇ Over the lazy."}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown? Fox Jumps!ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown? Fox Jumps! Ovˇer the lazy."}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown? Fox Jumps!ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown? Fox Jumps! Over theˇ lazy."}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown? Fox Jumps!ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown? Fox Jumps! Over the lazyˇ."}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown? Fox Jumps!ˇ","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇThe quick \nbrown fox jumps over\n","mode":"Insert"}} -{"Put":{"state":"The quick ˇbrown\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇThe quick \nbrown fox jumps over\n","mode":"Insert"}} -{"Put":{"state":"The quick brownˇ\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇThe quick \nbrown fox jumps over\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy doˇg. The quick \nbrown fox jumps over\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇThe quick \nbrown fox jumps over\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dogˇ. The quick \nbrown fox jumps over\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇThe quick \nbrown fox jumps over\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ The quick \nbrown fox jumps over\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog. ˇThe quick \nbrown fox jumps over\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog. The quick ˇ\nbrown fox jumps over\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ\n","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown.)]'\" Brown fox jumps. "}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇBrown fox jumps. ","mode":"Insert"}} -{"Put":{"state":"The ˇquick brown.)]'\" Brown fox jumps. "}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇBrown fox jumps. ","mode":"Insert"}} -{"Put":{"state":"The quick brownˇ.)]'\" Brown fox jumps. "}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇBrown fox jumps. ","mode":"Insert"}} -{"Put":{"state":"The quick brown.)ˇ]'\" Brown fox jumps. "}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇBrown fox jumps. ","mode":"Insert"}} -{"Put":{"state":"The quick brown.)]ˇ'\" Brown fox jumps. "}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇBrown fox jumps. ","mode":"Insert"}} -{"Put":{"state":"The quick brown.)]'ˇ\" Brown fox jumps. "}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇBrown fox jumps. ","mode":"Insert"}} -{"Put":{"state":"The quick brown.)]'\" Brown ˇfox jumps. "}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown.)]'\" ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown.)]'\" Brown fox jumpsˇ. "}} -{"Key":"c"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown.)]'\" ˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_change_surrounding_character_objects.json b/crates/vim/test_data/test_change_surrounding_character_objects.json deleted file mode 100644 index 4710e228a6..0000000000 --- a/crates/vim/test_data/test_change_surrounding_character_objects.json +++ /dev/null @@ -1,520 +0,0 @@ -{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"ˇ\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}} -{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"ˇ\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}} -{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"quiˇwn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck broˇ\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}} -{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"quiˇwn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck broˇ\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Insert"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}} -{"Put":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)ˇe ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ˇ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()quˇi(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ˇck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck broˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)ˇfox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox juˇmps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇer\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe ˇlazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg","mode":"Normal"}} -{"Put":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)ˇe ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ˇ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()quˇi(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ˇck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck broˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)ˇfox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox juˇmps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇer\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe ˇlazy d)o(g"}} -{"Key":"c"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg"}} -{"Key":"c"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg","mode":"Normal"}} -{"Put":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)ˇe ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ˇ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()quˇi(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ˇck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck broˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)ˇfox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox juˇmps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇer\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe ˇlazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg","mode":"Normal"}} -{"Put":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)ˇe ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ˇ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()quˇi(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ˇck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck broˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)ˇfox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox juˇmps ov(er\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇer\nthe lazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe ˇlazy d)o(g"}} -{"Key":"c"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Insert"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg"}} -{"Key":"c"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg","mode":"Normal"}} diff --git a/crates/vim/test_data/test_change_w.json b/crates/vim/test_data/test_change_w.json deleted file mode 100644 index 149dac8420..0000000000 --- a/crates/vim/test_data/test_change_w.json +++ /dev/null @@ -1,36 +0,0 @@ -{"Put":{"state":"Teˇst"}} -{"Key":"c"} -{"Key":"w"} -{"Get":{"state":"Teˇ","mode":"Insert"}} -{"Put":{"state":"Tˇest test"}} -{"Key":"c"} -{"Key":"w"} -{"Get":{"state":"Tˇ test","mode":"Insert"}} -{"Put":{"state":"Testˇ test"}} -{"Key":"c"} -{"Key":"w"} -{"Get":{"state":"Testˇtest","mode":"Insert"}} -{"Put":{"state":"Tesˇt test"}} -{"Key":"c"} -{"Key":"w"} -{"Get":{"state":"Tesˇ test","mode":"Insert"}} -{"Put":{"state":"Test teˇst\ntest"}} -{"Key":"c"} -{"Key":"w"} -{"Get":{"state":"Test teˇ\ntest","mode":"Insert"}} -{"Put":{"state":"Test tesˇt\ntest"}} -{"Key":"c"} -{"Key":"w"} -{"Get":{"state":"Test tesˇ\ntest","mode":"Insert"}} -{"Put":{"state":"Test test\nˇ\ntest"}} -{"Key":"c"} -{"Key":"w"} -{"Get":{"state":"Test test\nˇ\ntest","mode":"Insert"}} -{"Put":{"state":"Test teˇst-test test"}} -{"Key":"c"} -{"Key":"shift-w"} -{"Get":{"state":"Test teˇ test","mode":"Insert"}} -{"Put":{"state":"tesˇt-test"}} -{"Key":"c"} -{"Key":"w"} -{"Get":{"state":"tesˇ-test","mode":"Insert"}} diff --git a/crates/vim/test_data/test_change_word_object.json b/crates/vim/test_data/test_change_word_object.json deleted file mode 100644 index 18baf78c6e..0000000000 --- a/crates/vim/test_data/test_change_word_object.json +++ /dev/null @@ -1,460 +0,0 @@ -{"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick ˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick ˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brownˇ\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox ˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox ˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumpsˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ\n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇ \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ\n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ\n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇfox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-ˇ over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick ˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick ˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brownˇ\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox ˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox ˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumpsˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ\n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇ \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ\n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ\n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇfox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n ˇ over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ\n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick ˇ\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick ˇ\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brownˇ jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox ˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox ˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumpsˇ\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇ\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-ˇover\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ","mode":"Insert"}} -{"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick ˇ\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick ˇ\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brownˇ jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox ˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox ˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumpsˇ\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇ\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ over\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n ˇover\nthe lazy dog \n\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}} -{"Key":"c"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_clear_counts.json b/crates/vim/test_data/test_clear_counts.json deleted file mode 100644 index 6ef6b36017..0000000000 --- a/crates/vim/test_data/test_clear_counts.json +++ /dev/null @@ -1,7 +0,0 @@ -{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}} -{"Key":"4"} -{"Key":"escape"} -{"Key":"3"} -{"Key":"d"} -{"Key":"l"} -{"Get":{"state":"The quick brown\nfox juˇ over\nthe lazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_comma_semicolon.json b/crates/vim/test_data/test_comma_semicolon.json deleted file mode 100644 index e08b6963fa..0000000000 --- a/crates/vim/test_data/test_comma_semicolon.json +++ /dev/null @@ -1,34 +0,0 @@ -{"Put":{"state":"ˇone two three four"}} -{"Key":"f"} -{"Key":"o"} -{"Get":{"state":"one twˇo three four","mode":"Normal"}} -{"Key":","} -{"Get":{"state":"ˇone two three four","mode":"Normal"}} -{"Key":"2"} -{"Key":";"} -{"Get":{"state":"one two three fˇour","mode":"Normal"}} -{"Key":"shift-f"} -{"Key":"e"} -{"Get":{"state":"one two threˇe four","mode":"Normal"}} -{"Key":"2"} -{"Key":";"} -{"Get":{"state":"onˇe two three four","mode":"Normal"}} -{"Key":","} -{"Get":{"state":"one two thrˇee four","mode":"Normal"}} -{"Put":{"state":"ˇone two three four"}} -{"Key":"t"} -{"Key":"o"} -{"Get":{"state":"one tˇwo three four","mode":"Normal"}} -{"Key":","} -{"Get":{"state":"oˇne two three four","mode":"Normal"}} -{"Key":"2"} -{"Key":";"} -{"Get":{"state":"one two three ˇfour","mode":"Normal"}} -{"Key":"shift-t"} -{"Key":"e"} -{"Get":{"state":"one two threeˇ four","mode":"Normal"}} -{"Key":"3"} -{"Key":";"} -{"Get":{"state":"oneˇ two three four","mode":"Normal"}} -{"Key":","} -{"Get":{"state":"one two thˇree four","mode":"Normal"}} diff --git a/crates/vim/test_data/test_comma_w.json b/crates/vim/test_data/test_comma_w.json deleted file mode 100644 index ac7a91c80c..0000000000 --- a/crates/vim/test_data/test_comma_w.json +++ /dev/null @@ -1,15 +0,0 @@ -{"Exec":{"command":"map ,w j"}} -{"Put":{"state":"ˇhello hello\nhello hello"}} -{"Key":"f"} -{"Key":"o"} -{"Key":";"} -{"Key":","} -{"Key":"w"} -{"Get":{"state":"hello hello\nhello hellˇo","mode":"Normal"}} -{"Put":{"state":"ˇhello hello\nhello hello"}} -{"Key":"f"} -{"Key":"o"} -{"Key":";"} -{"Key":","} -{"Key":"i"} -{"Get":{"state":"hellˇo hello\nhello hello","mode":"Insert"}} diff --git a/crates/vim/test_data/test_command_basics.json b/crates/vim/test_data/test_command_basics.json deleted file mode 100644 index 669d34409f..0000000000 --- a/crates/vim/test_data/test_command_basics.json +++ /dev/null @@ -1,6 +0,0 @@ -{"Put":{"state":"ˇa\nb\nc"}} -{"Key":":"} -{"Key":"j"} -{"Key":"enter"} -{"Key":"^"} -{"Get":{"state":"ˇa b\nc","mode":"Normal"}} diff --git a/crates/vim/test_data/test_command_goto.json b/crates/vim/test_data/test_command_goto.json deleted file mode 100644 index 2f7ed10eeb..0000000000 --- a/crates/vim/test_data/test_command_goto.json +++ /dev/null @@ -1,5 +0,0 @@ -{"Put":{"state":"ˇa\nb\nc"}} -{"Key":":"} -{"Key":"3"} -{"Key":"enter"} -{"Get":{"state":"a\nb\nˇc","mode":"Normal"}} diff --git a/crates/vim/test_data/test_command_matching_lines.json b/crates/vim/test_data/test_command_matching_lines.json deleted file mode 100644 index 450aae0de0..0000000000 --- a/crates/vim/test_data/test_command_matching_lines.json +++ /dev/null @@ -1,19 +0,0 @@ -{"Put":{"state":"ˇa\nb\na\nb\na\n"}} -{"Key":":"} -{"Key":"g"} -{"Key":"/"} -{"Key":"a"} -{"Key":"/"} -{"Key":"d"} -{"Key":"enter"} -{"Get":{"state":"b\nb\nˇ","mode":"Normal"}} -{"Key":"u"} -{"Get":{"state":"ˇa\nb\na\nb\na\n","mode":"Normal"}} -{"Key":":"} -{"Key":"v"} -{"Key":"/"} -{"Key":"a"} -{"Key":"/"} -{"Key":"d"} -{"Key":"enter"} -{"Get":{"state":"a\na\nˇa","mode":"Normal"}} diff --git a/crates/vim/test_data/test_command_ranges.json b/crates/vim/test_data/test_command_ranges.json deleted file mode 100644 index d0e4928c6a..0000000000 --- a/crates/vim/test_data/test_command_ranges.json +++ /dev/null @@ -1,28 +0,0 @@ -{"Put":{"state":"ˇ1\n2\n3\n4\n4\n3\n2\n1"}} -{"Key":":"} -{"Key":"2"} -{"Key":","} -{"Key":"4"} -{"Key":"d"} -{"Key":"enter"} -{"Get":{"state":"1\nˇ4\n3\n2\n1","mode":"Normal"}} -{"Key":":"} -{"Key":"2"} -{"Key":","} -{"Key":"4"} -{"Key":"s"} -{"Key":"o"} -{"Key":"r"} -{"Key":"t"} -{"Key":"enter"} -{"Get":{"state":"1\nˇ2\n3\n4\n1","mode":"Normal"}} -{"Key":":"} -{"Key":"2"} -{"Key":","} -{"Key":"4"} -{"Key":"j"} -{"Key":"o"} -{"Key":"i"} -{"Key":"n"} -{"Key":"enter"} -{"Get":{"state":"1\nˇ2 3 4\n1","mode":"Normal"}} diff --git a/crates/vim/test_data/test_command_replace.json b/crates/vim/test_data/test_command_replace.json deleted file mode 100644 index d14a8a78ce..0000000000 --- a/crates/vim/test_data/test_command_replace.json +++ /dev/null @@ -1,33 +0,0 @@ -{"Put":{"state":"ˇa\nb\nb\nc"}} -{"Key":":"} -{"Key":"%"} -{"Key":"s"} -{"Key":"/"} -{"Key":"b"} -{"Key":"/"} -{"Key":"d"} -{"Key":"enter"} -{"Get":{"state":"a\nd\nˇd\nc","mode":"Normal"}} -{"Key":":"} -{"Key":"%"} -{"Key":"s"} -{"Key":":"} -{"Key":"."} -{"Key":":"} -{"Key":"\\"} -{"Key":"0"} -{"Key":"\\"} -{"Key":"0"} -{"Key":"enter"} -{"Get":{"state":"aa\ndd\ndd\nˇcc","mode":"Normal"}} -{"Key":"k"} -{"Key":":"} -{"Key":"s"} -{"Key":"/"} -{"Key":"d"} -{"Key":"d"} -{"Key":"/"} -{"Key":"e"} -{"Key":"e"} -{"Key":"enter"} -{"Get":{"state":"aa\ndd\nˇee\ncc","mode":"Normal"}} diff --git a/crates/vim/test_data/test_command_search.json b/crates/vim/test_data/test_command_search.json deleted file mode 100644 index 705ce51fb7..0000000000 --- a/crates/vim/test_data/test_command_search.json +++ /dev/null @@ -1,11 +0,0 @@ -{"Put":{"state":"ˇa\nb\na\nc"}} -{"Key":":"} -{"Key":"/"} -{"Key":"b"} -{"Key":"enter"} -{"Get":{"state":"a\nˇb\na\nc","mode":"Normal"}} -{"Key":":"} -{"Key":"?"} -{"Key":"a"} -{"Key":"enter"} -{"Get":{"state":"ˇa\nb\na\nc","mode":"Normal"}} diff --git a/crates/vim/test_data/test_command_visual_replace.json b/crates/vim/test_data/test_command_visual_replace.json deleted file mode 100644 index 69713b19ee..0000000000 --- a/crates/vim/test_data/test_command_visual_replace.json +++ /dev/null @@ -1,12 +0,0 @@ -{"Put":{"state":"ˇ1\n2\n3\n4\n4\n3\n2\n1"}} -{"Key":"v"} -{"Key":"2"} -{"Key":"j"} -{"Key":":"} -{"Key":"s"} -{"Key":"/"} -{"Key":"."} -{"Key":"/"} -{"Key":"k"} -{"Key":"enter"} -{"Get":{"state":"k\nk\nˇk\n4\n4\n3\n2\n1","mode":"Normal"}} diff --git a/crates/vim/test_data/test_convert_to_lower_case.json b/crates/vim/test_data/test_convert_to_lower_case.json deleted file mode 100644 index 83d7435c89..0000000000 --- a/crates/vim/test_data/test_convert_to_lower_case.json +++ /dev/null @@ -1,12 +0,0 @@ -{"Put":{"state":"A😀c«DÉ1*fˇ»\n"}} -{"Key":"u"} -{"Get":{"state":"A😀cˇdé1*f\n","mode":"Normal"}} -{"Put":{"state":"ABˇc\n"}} -{"Key":"shift-v"} -{"Key":"u"} -{"Get":{"state":"ˇabc\n","mode":"Normal"}} -{"Put":{"state":"ˇAa\nBb\nCc"}} -{"Key":"ctrl-v"} -{"Key":"j"} -{"Key":"u"} -{"Get":{"state":"ˇaa\nbb\nCc","mode":"Normal"}} diff --git a/crates/vim/test_data/test_convert_to_rot13.json b/crates/vim/test_data/test_convert_to_rot13.json deleted file mode 100644 index 7ac67c885c..0000000000 --- a/crates/vim/test_data/test_convert_to_rot13.json +++ /dev/null @@ -1,15 +0,0 @@ -{"Put":{"state":"a😀C«dÉ1*fˇ»\n"}} -{"Key":"g"} -{"Key":"?"} -{"Get":{"state":"a😀CˇqÉ1*s\n","mode":"Normal"}} -{"Put":{"state":"abˇC\n"}} -{"Key":"shift-v"} -{"Key":"g"} -{"Key":"?"} -{"Get":{"state":"ˇnoP\n","mode":"Normal"}} -{"Put":{"state":"ˇaa\nbb\ncc"}} -{"Key":"ctrl-v"} -{"Key":"j"} -{"Key":"g"} -{"Key":"?"} -{"Get":{"state":"ˇna\nob\ncc","mode":"Normal"}} diff --git a/crates/vim/test_data/test_convert_to_upper_case.json b/crates/vim/test_data/test_convert_to_upper_case.json deleted file mode 100644 index ffc9771073..0000000000 --- a/crates/vim/test_data/test_convert_to_upper_case.json +++ /dev/null @@ -1,12 +0,0 @@ -{"Put":{"state":"a😀C«dÉ1*fˇ»\n"}} -{"Key":"shift-u"} -{"Get":{"state":"a😀CˇDÉ1*F\n","mode":"Normal"}} -{"Put":{"state":"abˇC\n"}} -{"Key":"shift-v"} -{"Key":"shift-u"} -{"Get":{"state":"ˇABC\n","mode":"Normal"}} -{"Put":{"state":"ˇaa\nbb\ncc"}} -{"Key":"ctrl-v"} -{"Key":"j"} -{"Key":"shift-u"} -{"Get":{"state":"ˇAa\nBb\ncc","mode":"Normal"}} diff --git a/crates/vim/test_data/test_ctrl_d_u.json b/crates/vim/test_data/test_ctrl_d_u.json deleted file mode 100644 index 77c35f24b9..0000000000 --- a/crates/vim/test_data/test_ctrl_d_u.json +++ /dev/null @@ -1,28 +0,0 @@ -{"SetOption":{"value":"scrolloff=3"}} -{"SetOption":{"value":"lines=12"}} -{"Put":{"state":"ˇaa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz"}} -{"Key":"4"} -{"Key":"j"} -{"Key":"ctrl-d"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\nˇjj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-d"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\nˇoo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"g"} -{"Key":"g"} -{"Key":"ctrl-d"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nˇii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-u"} -{"Get":{"state":"aa\nbb\ncc\nˇdd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-d"} -{"Key":"ctrl-d"} -{"Key":"4"} -{"Key":"j"} -{"Key":"ctrl-u"} -{"Key":"ctrl-u"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nˇhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"g"} -{"Key":"g"} -{"Key":"ctrl-d"} -{"Key":"ctrl-u"} -{"Key":"ctrl-u"} -{"Get":{"state":"ˇaa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} diff --git a/crates/vim/test_data/test_ctrl_f_b.json b/crates/vim/test_data/test_ctrl_f_b.json deleted file mode 100644 index 19c94d8b6e..0000000000 --- a/crates/vim/test_data/test_ctrl_f_b.json +++ /dev/null @@ -1,24 +0,0 @@ -{"SetOption":{"value":"scrolloff=3"}} -{"SetOption":{"value":"lines=12"}} -{"SetOption":{"value":"scrolloff=0"}} -{"Put":{"state":"ˇaa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz"}} -{"Key":"ctrl-f"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nˇii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-f"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nˇqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-b"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nˇrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-b"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\nˇjj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"g"} -{"Key":"g"} -{"Get":{"state":"ˇaa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"SetOption":{"value":"scrolloff=3"}} -{"Key":"ctrl-f"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nˇll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-f"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\nˇtt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-b"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\nˇoo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-b"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\nˇgg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} diff --git a/crates/vim/test_data/test_ctrl_o_dot.json b/crates/vim/test_data/test_ctrl_o_dot.json deleted file mode 100644 index e414d785bf..0000000000 --- a/crates/vim/test_data/test_ctrl_o_dot.json +++ /dev/null @@ -1,11 +0,0 @@ -{"Put":{"state":"heˇllo world."}} -{"Key":"x"} -{"Key":"i"} -{"Key":"ctrl-o"} -{"Key":"."} -{"Get":{"state":"heˇo world.","mode":"Insert"}} -{"Key":"l"} -{"Key":"l"} -{"Key":"escape"} -{"Key":"."} -{"Get":{"state":"hellˇllo world.","mode":"Normal"}} diff --git a/crates/vim/test_data/test_ctrl_o_position.json b/crates/vim/test_data/test_ctrl_o_position.json deleted file mode 100644 index d8d76ac188..0000000000 --- a/crates/vim/test_data/test_ctrl_o_position.json +++ /dev/null @@ -1,10 +0,0 @@ -{"Put":{"state":"helˇlo world."}} -{"Key":"i"} -{"Key":"ctrl-o"} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"ˇ world.","mode":"Insert"}} -{"Key":"ctrl-o"} -{"Key":"p"} -{"Get":{"state":" helloˇworld.","mode":"Insert"}} diff --git a/crates/vim/test_data/test_ctrl_o_visual.json b/crates/vim/test_data/test_ctrl_o_visual.json deleted file mode 100644 index 23ec11d766..0000000000 --- a/crates/vim/test_data/test_ctrl_o_visual.json +++ /dev/null @@ -1,14 +0,0 @@ -{"Put":{"state":"helloˇ world."}} -{"Key":"i"} -{"Key":"ctrl-o"} -{"Key":"v"} -{"Key":"b"} -{"Key":"r"} -{"Key":"l"} -{"Get":{"state":"ˇllllllworld.","mode":"Insert"}} -{"Key":"ctrl-o"} -{"Key":"v"} -{"Key":"f"} -{"Key":"w"} -{"Key":"d"} -{"Get":{"state":"ˇorld.","mode":"Insert"}} diff --git a/crates/vim/test_data/test_ctrl_v.json b/crates/vim/test_data/test_ctrl_v.json deleted file mode 100644 index dfc090ab18..0000000000 --- a/crates/vim/test_data/test_ctrl_v.json +++ /dev/null @@ -1,24 +0,0 @@ -{"Put":{"state":"ˇ"}} -{"Key":"i"} -{"Key":"ctrl-v"} -{"Key":"0"} -{"Key":"0"} -{"Key":"0"} -{"Get":{"state":"\u0000ˇ","mode":"Insert"}} -{"Key":"ctrl-v"} -{"Key":"j"} -{"Get":{"state":"\u0000jˇ","mode":"Insert"}} -{"Key":"ctrl-v"} -{"Key":"x"} -{"Key":"6"} -{"Key":"5"} -{"Get":{"state":"\u0000jeˇ","mode":"Insert"}} -{"Key":"ctrl-v"} -{"Key":"U"} -{"Key":"1"} -{"Key":"F"} -{"Key":"6"} -{"Key":"4"} -{"Key":"0"} -{"Key":"space"} -{"Get":{"state":"\u0000je🙀 ˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_ctrl_v_control.json b/crates/vim/test_data/test_ctrl_v_control.json deleted file mode 100644 index a5a55cf6d5..0000000000 --- a/crates/vim/test_data/test_ctrl_v_control.json +++ /dev/null @@ -1,11 +0,0 @@ -{"Put":{"state":"ˇ"}} -{"Key":"i"} -{"Key":"ctrl-v"} -{"Key":"ctrl-d"} -{"Get":{"state":"\u0004ˇ","mode":"Insert"}} -{"Key":"ctrl-v"} -{"Key":"ctrl-j"} -{"Get":{"state":"\u0004\u0000ˇ","mode":"Insert"}} -{"Key":"ctrl-v"} -{"Key":"tab"} -{"Get":{"state":"\u0004\u0000\tˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_ctrl_v_escape.json b/crates/vim/test_data/test_ctrl_v_escape.json deleted file mode 100644 index 8c0397ef0a..0000000000 --- a/crates/vim/test_data/test_ctrl_v_escape.json +++ /dev/null @@ -1,10 +0,0 @@ -{"Put":{"state":"ˇ"}} -{"Key":"i"} -{"Key":"ctrl-v"} -{"Key":"9"} -{"Key":"escape"} -{"Get":{"state":"ˇ\t","mode":"Normal"}} -{"Key":"i"} -{"Key":"ctrl-v"} -{"Key":"escape"} -{"Get":{"state":"\u001bˇ\t","mode":"Insert"}} diff --git a/crates/vim/test_data/test_ctrl_w_override.json b/crates/vim/test_data/test_ctrl_w_override.json deleted file mode 100644 index fe8ae94a77..0000000000 --- a/crates/vim/test_data/test_ctrl_w_override.json +++ /dev/null @@ -1,4 +0,0 @@ -{"Exec":{"command":"map D"}} -{"Put":{"state":"ˇhi"}} -{"Key":"ctrl-w"} -{"Get":{"state":"ˇ","mode":"Normal"}} diff --git a/crates/vim/test_data/test_ctrl_y_e.json b/crates/vim/test_data/test_ctrl_y_e.json deleted file mode 100644 index c43977a6ad..0000000000 --- a/crates/vim/test_data/test_ctrl_y_e.json +++ /dev/null @@ -1,35 +0,0 @@ -{"SetOption":{"value":"scrolloff=3"}} -{"SetOption":{"value":"lines=12"}} -{"Put":{"state":"ˇaa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz"}} -{"Key":"ctrl-e"} -{"Get":{"state":"aa\nbb\ncc\ndd\nˇee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-e"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nˇff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-e"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\nˇgg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-e"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nˇhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-e"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nˇii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-e"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\nˇjj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-e"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nˇkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-e"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nˇll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-y"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nˇll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-y"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nˇll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-y"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nˇll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-y"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nˇkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-y"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\nˇjj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-y"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nˇii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-y"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nˇhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} -{"Key":"ctrl-y"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\nˇgg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} diff --git a/crates/vim/test_data/test_d_search.json b/crates/vim/test_data/test_d_search.json deleted file mode 100644 index 9cdc855dbf..0000000000 --- a/crates/vim/test_data/test_d_search.json +++ /dev/null @@ -1,7 +0,0 @@ -{"Put":{"state":"ˇa.c. abcd a.c. abcd"}} -{"Key":"d"} -{"Key":"/"} -{"Key":"c"} -{"Key":"d"} -{"Key":"enter"} -{"Get":{"state":"ˇcd a.c. abcd","mode":"Normal"}} diff --git a/crates/vim/test_data/test_dd.json b/crates/vim/test_data/test_dd.json deleted file mode 100644 index c6dc30882e..0000000000 --- a/crates/vim/test_data/test_dd.json +++ /dev/null @@ -1,24 +0,0 @@ -{"Put":{"state":"ˇ"}} -{"Key":"d"} -{"Key":"d"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The ˇquick"}} -{"Key":"d"} -{"Key":"d"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}} -{"Key":"d"} -{"Key":"d"} -{"Get":{"state":"brownˇ fox\njumps over","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}} -{"Key":"d"} -{"Key":"d"} -{"Get":{"state":"The quick\njumps ˇover","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}} -{"Key":"d"} -{"Key":"d"} -{"Get":{"state":"The quick\nbrown ˇfox","mode":"Normal"}} -{"Put":{"state":"The quick\nˇ\nbrown fox"}} -{"Key":"d"} -{"Key":"d"} -{"Get":{"state":"The quick\nˇbrown fox","mode":"Normal"}} diff --git a/crates/vim/test_data/test_dd_then_paste_without_trailing_newline.json b/crates/vim/test_data/test_dd_then_paste_without_trailing_newline.json deleted file mode 100644 index 5b10a2fe28..0000000000 --- a/crates/vim/test_data/test_dd_then_paste_without_trailing_newline.json +++ /dev/null @@ -1,7 +0,0 @@ -{"Put":{"state":"heˇllo"}} -{"Key":"d"} -{"Key":"d"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Key":"p"} -{"Key":"p"} -{"Get":{"state":"\nhello\nˇhello","mode":"Normal"}} diff --git a/crates/vim/test_data/test_del_marks.json b/crates/vim/test_data/test_del_marks.json deleted file mode 100644 index c326c6d61e..0000000000 --- a/crates/vim/test_data/test_del_marks.json +++ /dev/null @@ -1,11 +0,0 @@ -{"Put":{"state":"ˇa\nb\na\nb\na\n"}} -{"Key":"m"} -{"Key":"a"} -{"Key":":"} -{"Key":"d"} -{"Key":"e"} -{"Key":"l"} -{"Key":"m"} -{"Key":"space"} -{"Key":"a"} -{"Key":"enter"} diff --git a/crates/vim/test_data/test_delete_0.json b/crates/vim/test_data/test_delete_0.json deleted file mode 100644 index 10095cefbb..0000000000 --- a/crates/vim/test_data/test_delete_0.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Put":{"state":"The qˇuick\nbrown fox"}} -{"Key":"d"} -{"Key":"0"} -{"Get":{"state":"ˇuick\nbrown fox","mode":"Normal"}} -{"Put":{"state":"The quick\nˇ\nbrown fox"}} -{"Key":"d"} -{"Key":"0"} -{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_b.json b/crates/vim/test_data/test_delete_b.json deleted file mode 100644 index 932a4c1967..0000000000 --- a/crates/vim/test_data/test_delete_b.json +++ /dev/null @@ -1,24 +0,0 @@ -{"Put":{"state":"Teˇst Test"}} -{"Key":"d"} -{"Key":"b"} -{"Get":{"state":"ˇst Test","mode":"Normal"}} -{"Put":{"state":"Test ˇtest"}} -{"Key":"d"} -{"Key":"b"} -{"Get":{"state":"ˇtest","mode":"Normal"}} -{"Put":{"state":"Test1 test2 ˇtest3"}} -{"Key":"d"} -{"Key":"b"} -{"Get":{"state":"Test1 ˇtest3","mode":"Normal"}} -{"Put":{"state":"Test test\nˇtest"}} -{"Key":"d"} -{"Key":"b"} -{"Get":{"state":"Testˇ \ntest","mode":"Normal"}} -{"Put":{"state":"Test test\nˇ\ntest"}} -{"Key":"d"} -{"Key":"b"} -{"Get":{"state":"Testˇ \n\ntest","mode":"Normal"}} -{"Put":{"state":"Test test-test ˇtest"}} -{"Key":"d"} -{"Key":"shift-b"} -{"Get":{"state":"Test ˇtest","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_end_of_document.json b/crates/vim/test_data/test_delete_end_of_document.json deleted file mode 100644 index 863d15aca1..0000000000 --- a/crates/vim/test_data/test_delete_end_of_document.json +++ /dev/null @@ -1,16 +0,0 @@ -{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}} -{"Key":"d"} -{"Key":"shift-g"} -{"Get":{"state":"The qˇuick","mode":"Normal"}} -{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}} -{"Key":"d"} -{"Key":"shift-g"} -{"Get":{"state":"The qˇuick","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown fox\njumps over\nthe lˇazy"}} -{"Key":"d"} -{"Key":"shift-g"} -{"Get":{"state":"The quick\nbrown fox\njumpsˇ over","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown fox\njumps over\nˇ"}} -{"Key":"d"} -{"Key":"shift-g"} -{"Get":{"state":"The quick\nbrown fox\nˇjumps over","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_end_of_line.json b/crates/vim/test_data/test_delete_end_of_line.json deleted file mode 100644 index 93f0cc2459..0000000000 --- a/crates/vim/test_data/test_delete_end_of_line.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Put":{"state":"The qˇuick\nbrown fox"}} -{"Key":"d"} -{"Key":"$"} -{"Get":{"state":"The ˇq\nbrown fox","mode":"Normal"}} -{"Put":{"state":"The quick\nˇ\nbrown fox"}} -{"Key":"d"} -{"Key":"$"} -{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_end_of_paragraph.json b/crates/vim/test_data/test_delete_end_of_paragraph.json deleted file mode 100644 index 860fed7a68..0000000000 --- a/crates/vim/test_data/test_delete_end_of_paragraph.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Put":{"state":"ˇhello world.\n\nhello world."}} -{"Key":"d"} -{"Key":"}"} -{"Get":{"state":"ˇ\nhello world.","mode":"Normal"}} -{"Put":{"state":"ˇhello world.\nhello world."}} -{"Key":"d"} -{"Key":"}"} -{"Get":{"state":"ˇ","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_gg.json b/crates/vim/test_data/test_delete_gg.json deleted file mode 100644 index de6aca9665..0000000000 --- a/crates/vim/test_data/test_delete_gg.json +++ /dev/null @@ -1,20 +0,0 @@ -{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}} -{"Key":"d"} -{"Key":"g"} -{"Key":"g"} -{"Get":{"state":"jumpsˇ over\nthe lazy","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown fox\njumps over\nthe lˇazy"}} -{"Key":"d"} -{"Key":"g"} -{"Key":"g"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The qˇuick\nbrown fox\njumps over\nthe lazy"}} -{"Key":"d"} -{"Key":"g"} -{"Key":"g"} -{"Get":{"state":"brownˇ fox\njumps over\nthe lazy","mode":"Normal"}} -{"Put":{"state":"ˇ\nbrown fox\njumps over\nthe lazy"}} -{"Key":"d"} -{"Key":"g"} -{"Key":"g"} -{"Get":{"state":"ˇbrown fox\njumps over\nthe lazy","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_h.json b/crates/vim/test_data/test_delete_h.json deleted file mode 100644 index cf842a386e..0000000000 --- a/crates/vim/test_data/test_delete_h.json +++ /dev/null @@ -1,16 +0,0 @@ -{"Put":{"state":"Teˇst"}} -{"Key":"d"} -{"Key":"h"} -{"Get":{"state":"Tˇst","mode":"Normal"}} -{"Put":{"state":"Tˇest"}} -{"Key":"d"} -{"Key":"h"} -{"Get":{"state":"ˇest","mode":"Normal"}} -{"Put":{"state":"ˇTest"}} -{"Key":"d"} -{"Key":"h"} -{"Get":{"state":"ˇTest","mode":"Normal"}} -{"Put":{"state":"Test\nˇtest"}} -{"Key":"d"} -{"Key":"h"} -{"Get":{"state":"Test\nˇtest","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_j.json b/crates/vim/test_data/test_delete_j.json deleted file mode 100644 index 76c2f098d3..0000000000 --- a/crates/vim/test_data/test_delete_j.json +++ /dev/null @@ -1,16 +0,0 @@ -{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}} -{"Key":"d"} -{"Key":"j"} -{"Get":{"state":"The quˇick","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}} -{"Key":"d"} -{"Key":"j"} -{"Get":{"state":"The quick\nbrown fox\njumps ˇover","mode":"Normal"}} -{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}} -{"Key":"d"} -{"Key":"j"} -{"Get":{"state":"jumpsˇ over","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown fox\nˇ"}} -{"Key":"d"} -{"Key":"j"} -{"Get":{"state":"The quick\nbrown fox\nˇ","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_k.json b/crates/vim/test_data/test_delete_k.json deleted file mode 100644 index 75c032430c..0000000000 --- a/crates/vim/test_data/test_delete_k.json +++ /dev/null @@ -1,16 +0,0 @@ -{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}} -{"Key":"d"} -{"Key":"k"} -{"Get":{"state":"jumps ˇover","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}} -{"Key":"d"} -{"Key":"k"} -{"Get":{"state":"The quˇick","mode":"Normal"}} -{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}} -{"Key":"d"} -{"Key":"k"} -{"Get":{"state":"The qˇuick\nbrown fox\njumps over","mode":"Normal"}} -{"Put":{"state":"ˇbrown fox\njumps over"}} -{"Key":"d"} -{"Key":"k"} -{"Get":{"state":"ˇbrown fox\njumps over","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_key_can_remove_last_character.json b/crates/vim/test_data/test_delete_key_can_remove_last_character.json deleted file mode 100644 index ea4cd71e9f..0000000000 --- a/crates/vim/test_data/test_delete_key_can_remove_last_character.json +++ /dev/null @@ -1,3 +0,0 @@ -{"Put":{"state":"abˇc"}} -{"Key":"delete"} -{"Get":{"state":"aˇb","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_l.json b/crates/vim/test_data/test_delete_l.json deleted file mode 100644 index 60b3a6c9b1..0000000000 --- a/crates/vim/test_data/test_delete_l.json +++ /dev/null @@ -1,16 +0,0 @@ -{"Put":{"state":"ˇTest"}} -{"Key":"d"} -{"Key":"l"} -{"Get":{"state":"ˇest","mode":"Normal"}} -{"Put":{"state":"Teˇst"}} -{"Key":"d"} -{"Key":"l"} -{"Get":{"state":"Teˇt","mode":"Normal"}} -{"Put":{"state":"Tesˇt"}} -{"Key":"d"} -{"Key":"l"} -{"Get":{"state":"Teˇs","mode":"Normal"}} -{"Put":{"state":"Tesˇt\ntest"}} -{"Key":"d"} -{"Key":"l"} -{"Get":{"state":"Teˇs\ntest","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_left.json b/crates/vim/test_data/test_delete_left.json deleted file mode 100644 index a8a242f1f6..0000000000 --- a/crates/vim/test_data/test_delete_left.json +++ /dev/null @@ -1,15 +0,0 @@ -{"Put":{"state":"ˇTest"}} -{"Key":"shift-x"} -{"Get":{"state":"ˇTest","mode":"Normal"}} -{"Put":{"state":"Tˇest"}} -{"Key":"shift-x"} -{"Get":{"state":"ˇest","mode":"Normal"}} -{"Put":{"state":"Teˇst"}} -{"Key":"shift-x"} -{"Get":{"state":"Tˇst","mode":"Normal"}} -{"Put":{"state":"Tesˇt"}} -{"Key":"shift-x"} -{"Get":{"state":"Teˇt","mode":"Normal"}} -{"Put":{"state":"Test\nˇtest"}} -{"Key":"shift-x"} -{"Get":{"state":"Test\nˇtest","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_next_word_end.json b/crates/vim/test_data/test_delete_next_word_end.json deleted file mode 100644 index 2467813b49..0000000000 --- a/crates/vim/test_data/test_delete_next_word_end.json +++ /dev/null @@ -1,20 +0,0 @@ -{"Put":{"state":"Teˇst Test\n"}} -{"Key":"d"} -{"Key":"e"} -{"Get":{"state":"Teˇ Test\n","mode":"Normal"}} -{"Put":{"state":"Tˇest test\n"}} -{"Key":"d"} -{"Key":"e"} -{"Get":{"state":"Tˇ test\n","mode":"Normal"}} -{"Put":{"state":"Test teˇst\ntest"}} -{"Key":"d"} -{"Key":"e"} -{"Get":{"state":"Test tˇe\ntest","mode":"Normal"}} -{"Put":{"state":"Test tesˇt\ntest"}} -{"Key":"d"} -{"Key":"e"} -{"Get":{"state":"Test teˇs","mode":"Normal"}} -{"Put":{"state":"Test teˇst-test test"}} -{"Key":"d"} -{"Key":"e"} -{"Get":{"state":"Test teˇ-test test","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_paragraph.json b/crates/vim/test_data/test_delete_paragraph.json deleted file mode 100644 index 3b09749bab..0000000000 --- a/crates/vim/test_data/test_delete_paragraph.json +++ /dev/null @@ -1,14 +0,0 @@ -{"Put":{"state":"helˇlo world.\n\nhello world.\n"}} -{"Key":"y"} -{"Key":"}"} -{"Key":"d"} -{"Key":"}"} -{"Get":{"state":"heˇl\n\nhello world.\n","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"lo world."}} -{"Get":{"state":"heˇl\n\nhello world.\n","mode":"Normal"}} -{"Put":{"state":"ˇhello world.\n\nhello world.\n"}} -{"Key":"y"} -{"Key":"}"} -{"Key":"d"} -{"Key":"}"} -{"Get":{"state":"ˇ\nhello world.\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_paragraph_motion.json b/crates/vim/test_data/test_delete_paragraph_motion.json deleted file mode 100644 index d4086a8ca5..0000000000 --- a/crates/vim/test_data/test_delete_paragraph_motion.json +++ /dev/null @@ -1,18 +0,0 @@ -{"Put":{"state":"ˇhello world.\n\nhello world.\n"}} -{"Key":"y"} -{"Key":"}"} -{"Get":{"state":"ˇhello world.\n\nhello world.\n","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"hello world.\n"}} -{"Key":"d"} -{"Key":"}"} -{"Get":{"state":"ˇ\nhello world.\n","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"hello world.\n"}} -{"Put":{"state":"helˇlo world.\n\nhello world.\n"}} -{"Key":"y"} -{"Key":"}"} -{"Get":{"state":"helˇlo world.\n\nhello world.\n","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"lo world."}} -{"Key":"d"} -{"Key":"}"} -{"Get":{"state":"heˇl\n\nhello world.\n","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"lo world."}} diff --git a/crates/vim/test_data/test_delete_paragraph_object.json b/crates/vim/test_data/test_delete_paragraph_object.json deleted file mode 100644 index 2cf1402ae3..0000000000 --- a/crates/vim/test_data/test_delete_paragraph_object.json +++ /dev/null @@ -1,430 +0,0 @@ -{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog."}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumpˇs over the lazy dog."}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dogˇ."}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.ˇ"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog."}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumpˇs over the lazy dog."}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dogˇ."}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.ˇ"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The quick brownˇ\nfox jumps over\nthe lazy dog.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown\nˇfox jumps over\nthe lazy dog.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps overˇ\nthe lazy dog.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The quick brownˇ\nfox jumps over\nthe lazy dog.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown\nˇfox jumps over\nthe lazy dog.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps overˇ\nthe lazy dog.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"ˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Normal"}} -{"Put":{"state":"\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Normal"}} -{"Put":{"state":"\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Normal"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nˇover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Normal"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇThe quick brown fox jumps\nover the lazy dog.\n","mode":"Normal"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇThe quick brown fox jumps\nover the lazy dog.\n","mode":"Normal"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Normal"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumpsˇ\nover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Normal"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nˇover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Normal"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Normal"}} -{"Put":{"state":"ˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Normal"}} -{"Put":{"state":"\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"Normal"}} -{"Put":{"state":"\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nˇThe quick brown fox jumps\nover the lazy dog.\n","mode":"Normal"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nˇover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nˇThe quick brown fox jumps\nover the lazy dog.\n","mode":"Normal"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ","mode":"Normal"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ","mode":"Normal"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ","mode":"Normal"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumpsˇ\nover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ","mode":"Normal"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nˇover the lazy dog.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ","mode":"Normal"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nˇover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ\n\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumpsˇ\nover the lazy dog.\n\n\n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ\n\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nˇover the lazy dog.\n\n\n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ\n\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n\n\n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇ\n\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nˇover the lazy dog.","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ\n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nˇover the lazy dog.","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nˇover the lazy dog.","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nˇover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\nˇ\nThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumpsˇ\nover the lazy dog.\n\n\n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nˇover the lazy dog.\n\n\n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n\n\n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\nˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ\n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇ\n","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\nˇ \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nˇ\n \t\n\t \t\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n\n \t\n\t \t\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nˇ\n \t\n\t \t\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n \t\n\t \t\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nˇover the lazy dog.","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ \t\n\t \t\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nˇover the lazy dog.","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\nˇ \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\nˇ\n \t\n\t \t\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n \t\n\t \t\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\nˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.ˇ\n\n \t\n\t \t\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\nˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n \t\n\t \t\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n \t\n\t \t\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ \t\n\t \t\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps\nover the lazy dog.\n\n \t\n\nThe quick brown fox jumps\nover the lazy dog.\n\nˇ \t\n\t \t\n","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\nThe quick brown fox jumps over the lazy dog.\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\nˇ\nThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\nˇThe quick brown fox jumps over the lazy dog.\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇ\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumpˇs over the lazy dog.\n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇ\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.ˇ\n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇ\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\nˇ\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇThe quick brown fox jumps over the lazy dog.","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇThe quick brown fox jumps over the lazy dog.\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\nˇ\nThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\nˇ\n","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\nˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumpˇs over the lazy dog.\n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\nˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.ˇ\n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\nˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\nˇ\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\nˇ\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_paragraph_object_with_soft_wrap.json b/crates/vim/test_data/test_delete_paragraph_object_with_soft_wrap.json deleted file mode 100644 index 19dcd175b3..0000000000 --- a/crates/vim/test_data/test_delete_paragraph_object_with_soft_wrap.json +++ /dev/null @@ -1,72 +0,0 @@ -{"SetOption":{"value":"wrap"}} -{"SetOption":{"value":"columns=20"}} -{"Put":{"state":"ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"ˇ\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Normal"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Normal"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.ˇ\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ","mode":"Normal"}} -{"Put":{"state":"ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"Second paragraph that is also quite long and will definitely wrap under soft wrap conditions andˇ should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"Normal"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nThird paragraph with additional long text content that will also wrap when line length is constraˇined by the wrapping settings.\n","mode":"Normal"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\nˇ","mode":"Normal"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\nˇ","mode":"Normal"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.ˇ\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\nˇ","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_paragraph_whitespace.json b/crates/vim/test_data/test_delete_paragraph_whitespace.json deleted file mode 100644 index e07b18eaa3..0000000000 --- a/crates/vim/test_data/test_delete_paragraph_whitespace.json +++ /dev/null @@ -1,5 +0,0 @@ -{"Put":{"state":"a\n ˇ•\naaaaaaaaaaaaa\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"a\naaaaaaaˇaaaaaa\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_sentence.json b/crates/vim/test_data/test_delete_sentence.json deleted file mode 100644 index 6056b207e4..0000000000 --- a/crates/vim/test_data/test_delete_sentence.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Put":{"state":"ˇFirst.\nFourth.\n"}} -{"Key":"d"} -{"Key":")"} -{"Get":{"state":"ˇFourth.\n","mode":"Normal"}} -{"Put":{"state":"First.\nˇSecond.\nFourth.\n"}} -{"Key":"d"} -{"Key":"("} -{"Get":{"state":"ˇSecond.\nFourth.\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_sentence_object.json b/crates/vim/test_data/test_delete_sentence_object.json deleted file mode 100644 index e45ebd5c4e..0000000000 --- a/crates/vim/test_data/test_delete_sentence_object.json +++ /dev/null @@ -1,270 +0,0 @@ -{"Put":{"state":"ˇThe quick brown? Fox Jumps! Over the lazy."}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Fox Jumps! Over the lazy.","mode":"Normal"}} -{"Put":{"state":"The quick ˇbrown? Fox Jumps! Over the lazy."}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Fox Jumps! Over the lazy.","mode":"Normal"}} -{"Put":{"state":"The quick brownˇ? Fox Jumps! Over the lazy."}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Fox Jumps! Over the lazy.","mode":"Normal"}} -{"Put":{"state":"The quick brown?ˇ Fox Jumps! Over the lazy."}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown?ˇFox Jumps! Over the lazy.","mode":"Normal"}} -{"Put":{"state":"The quick brown? ˇFox Jumps! Over the lazy."}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown? ˇ Over the lazy.","mode":"Normal"}} -{"Put":{"state":"The quick brown? Fox Jˇumps! Over the lazy."}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown? ˇ Over the lazy.","mode":"Normal"}} -{"Put":{"state":"The quick brown? Fox Jumpsˇ! Over the lazy."}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown? ˇ Over the lazy.","mode":"Normal"}} -{"Put":{"state":"The quick brown? Fox Jumps!ˇ Over the lazy."}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown? Fox Jumps!ˇOver the lazy.","mode":"Normal"}} -{"Put":{"state":"The quick brown? Fox Jumps! Ovˇer the lazy."}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown? Fox Jumps!ˇ ","mode":"Normal"}} -{"Put":{"state":"The quick brown? Fox Jumps! Over theˇ lazy."}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown? Fox Jumps!ˇ ","mode":"Normal"}} -{"Put":{"state":"The quick brown? Fox Jumps! Over the lazyˇ."}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown? Fox Jumps!ˇ ","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ The quick \nbrown fox jumps over\n","mode":"Normal"}} -{"Put":{"state":"The quick ˇbrown\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ The quick \nbrown fox jumps over\n","mode":"Normal"}} -{"Put":{"state":"The quick brownˇ\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ The quick \nbrown fox jumps over\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy doˇg. The quick \nbrown fox jumps over\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ The quick \nbrown fox jumps over\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dogˇ. The quick \nbrown fox jumps over\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ The quick \nbrown fox jumps over\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ The quick \nbrown fox jumps over\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇThe quick \nbrown fox jumps over\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog. ˇThe quick \nbrown fox jumps over\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ \n","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog. The quick ˇ\nbrown fox jumps over\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ \n","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown.)]'\" Brown fox jumps. "}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Normal"}} -{"Put":{"state":"The ˇquick brown.)]'\" Brown fox jumps. "}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Normal"}} -{"Put":{"state":"The quick brownˇ.)]'\" Brown fox jumps. "}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Normal"}} -{"Put":{"state":"The quick brown.)ˇ]'\" Brown fox jumps. "}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Normal"}} -{"Put":{"state":"The quick brown.)]ˇ'\" Brown fox jumps. "}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Normal"}} -{"Put":{"state":"The quick brown.)]'ˇ\" Brown fox jumps. "}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"ˇ Brown fox jumps. ","mode":"Normal"}} -{"Put":{"state":"The quick brown.)]'\" Brown ˇfox jumps. "}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown.)]'\" ˇ ","mode":"Normal"}} -{"Put":{"state":"The quick brown.)]'\" Brown fox jumpsˇ. "}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown.)]'\" ˇ ","mode":"Normal"}} -{"Put":{"state":"The quick brown.)]'\" Brown fox jumps.ˇ "}} -{"Key":"d"} -{"Key":"i"} -{"Key":"s"} -{"Get":{"state":"The quick brown.)]'\" Brown fox jumpsˇ.","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown? Fox Jumps! Over the lazy."}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇFox Jumps! Over the lazy.","mode":"Normal"}} -{"Put":{"state":"The quick ˇbrown? Fox Jumps! Over the lazy."}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇFox Jumps! Over the lazy.","mode":"Normal"}} -{"Put":{"state":"The quick brownˇ? Fox Jumps! Over the lazy."}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇFox Jumps! Over the lazy.","mode":"Normal"}} -{"Put":{"state":"The quick brown? ˇFox Jumps! Over the lazy."}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown? ˇOver the lazy.","mode":"Normal"}} -{"Put":{"state":"The quick brown? Fox Jˇumps! Over the lazy."}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown? ˇOver the lazy.","mode":"Normal"}} -{"Put":{"state":"The quick brown? Fox Jumpsˇ! Over the lazy."}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown? ˇOver the lazy.","mode":"Normal"}} -{"Put":{"state":"The quick brown? Fox Jumps!ˇ Over the lazy."}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown? Fox Jumpsˇ!","mode":"Normal"}} -{"Put":{"state":"The quick brown? Fox Jumps! Ovˇer the lazy."}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown? Fox Jumpsˇ!","mode":"Normal"}} -{"Put":{"state":"The quick brown? Fox Jumps! Over theˇ lazy."}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown? Fox Jumpsˇ!","mode":"Normal"}} -{"Put":{"state":"The quick brown? Fox Jumps! Over the lazyˇ."}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown? Fox Jumpsˇ!","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇThe quick \nbrown fox jumps over\n","mode":"Normal"}} -{"Put":{"state":"The quick ˇbrown\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇThe quick \nbrown fox jumps over\n","mode":"Normal"}} -{"Put":{"state":"The quick brownˇ\nfox jumps over\nthe lazy dog. The quick \nbrown fox jumps over\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇThe quick \nbrown fox jumps over\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy doˇg. The quick \nbrown fox jumps over\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇThe quick \nbrown fox jumps over\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dogˇ. The quick \nbrown fox jumps over\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇThe quick \nbrown fox jumps over\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog.ˇ The quick \nbrown fox jumps over\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dogˇ.\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog. ˇThe quick \nbrown fox jumps over\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dogˇ.\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe lazy dog. The quick ˇ\nbrown fox jumps over\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dogˇ.\n","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown.)]'\" Brown fox jumps. "}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇBrown fox jumps. ","mode":"Normal"}} -{"Put":{"state":"The ˇquick brown.)]'\" Brown fox jumps. "}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇBrown fox jumps. ","mode":"Normal"}} -{"Put":{"state":"The quick brownˇ.)]'\" Brown fox jumps. "}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇBrown fox jumps. ","mode":"Normal"}} -{"Put":{"state":"The quick brown.)ˇ]'\" Brown fox jumps. "}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇBrown fox jumps. ","mode":"Normal"}} -{"Put":{"state":"The quick brown.)]ˇ'\" Brown fox jumps. "}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇBrown fox jumps. ","mode":"Normal"}} -{"Put":{"state":"The quick brown.)]'ˇ\" Brown fox jumps. "}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"ˇBrown fox jumps. ","mode":"Normal"}} -{"Put":{"state":"The quick brown.)]'\" Brown ˇfox jumps. "}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown.)]'\"ˇ ","mode":"Normal"}} -{"Put":{"state":"The quick brown.)]'\" Brown fox jumpsˇ. "}} -{"Key":"d"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"The quick brown.)]'\"ˇ ","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_surrounding_character_objects.json b/crates/vim/test_data/test_delete_surrounding_character_objects.json deleted file mode 100644 index 3d4e6aeec0..0000000000 --- a/crates/vim/test_data/test_delete_surrounding_character_objects.json +++ /dev/null @@ -1,518 +0,0 @@ -{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"ˇ\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}} -{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"ˇ\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"ˇ\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇ\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"ˇ\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}} -{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"quiˇwn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck brˇo\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}} -{"Put":{"state":"ˇTh\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"ˇe \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e ˇ\"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Thˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"ˇ\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"ˇqui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"quˇi\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ˇck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"quiˇwn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck broˇ\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck brˇo\n\"fox jumps ov\"er\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"ˇfox jumps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox juˇmps ov\"er\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\nˇer\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"ˇer\nthe lazy d\"o\"g","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe ˇlazy d\"o\"g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy dˇg","mode":"Normal"}} -{"Put":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"Th\"e \"\"qui\"ck bro\"wn\"\n\"fox jumps ov\"er\nthe lazy d\"o\"ˇg","mode":"Normal"}} -{"Put":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)ˇe ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ˇ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()quˇi(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ˇck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck broˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)ˇfox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox juˇmps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇer\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe ˇlazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg","mode":"Normal"}} -{"Put":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)ˇe ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ˇ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Key":"d"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()quˇi(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ˇck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck broˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)ˇfox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox juˇmps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇer\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe ˇlazy d)o(g"}} -{"Key":"d"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇ)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg"}} -{"Key":"d"} -{"Key":"i"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg","mode":"Normal"}} -{"Put":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)ˇe ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ˇ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()quˇi(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ˇck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck broˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)ˇfox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox juˇmps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇer\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe ˇlazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"("} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg","mode":"Normal"}} -{"Put":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"ˇTh)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)ˇe ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ˇ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e (ˇ)qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()ˇqui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()quˇi(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ˇck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck broˇ)wn(\n)fox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()quiˇwn(\n)fox jumps ov(er\nthe lazy d)o(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)ˇfox jumps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox juˇmps ov(er\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(ˇer\nthe lazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe ˇlazy d)o(g"}} -{"Key":"d"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ovˇo(g","mode":"Normal"}} -{"Put":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg"}} -{"Key":"d"} -{"Key":"a"} -{"Key":")"} -{"Get":{"state":"Th)e ()qui(ck bro)wn(\n)fox jumps ov(er\nthe lazy d)o(ˇg","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_to_adjacent_character.json b/crates/vim/test_data/test_delete_to_adjacent_character.json deleted file mode 100644 index 130719c890..0000000000 --- a/crates/vim/test_data/test_delete_to_adjacent_character.json +++ /dev/null @@ -1,10 +0,0 @@ -{"Put":{"state":"ˇax"}} -{"Key":"d"} -{"Key":"t"} -{"Key":"x"} -{"Get":{"state":"ˇx","mode":"Normal"}} -{"Put":{"state":"aˇx"}} -{"Key":"d"} -{"Key":"t"} -{"Key":"x"} -{"Get":{"state":"aˇx","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_to_end_of_line.json b/crates/vim/test_data/test_delete_to_end_of_line.json deleted file mode 100644 index e15d5d0c4a..0000000000 --- a/crates/vim/test_data/test_delete_to_end_of_line.json +++ /dev/null @@ -1,6 +0,0 @@ -{"Put":{"state":"The qˇuick\nbrown fox"}} -{"Key":"shift-d"} -{"Get":{"state":"The ˇq\nbrown fox","mode":"Normal"}} -{"Put":{"state":"The quick\nˇ\nbrown fox"}} -{"Key":"shift-d"} -{"Get":{"state":"The quick\nˇ\nbrown fox","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_to_line.json b/crates/vim/test_data/test_delete_to_line.json deleted file mode 100644 index eae919b039..0000000000 --- a/crates/vim/test_data/test_delete_to_line.json +++ /dev/null @@ -1,15 +0,0 @@ -{"Put":{"state":"The quick\nbrownˇ fox\njumps over\nthe lazy"}} -{"Key":"d"} -{"Key":"3"} -{"Key":"shift-g"} -{"Get":{"state":"The quick\nthe lˇazy","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown fox\njumps over\nthe lˇazy"}} -{"Key":"d"} -{"Key":"3"} -{"Key":"shift-g"} -{"Get":{"state":"The quick\nbrownˇ fox","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown fox\njumps over\nˇ"}} -{"Key":"d"} -{"Key":"2"} -{"Key":"shift-g"} -{"Get":{"state":"ˇThe quick","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_unmatched_brace.json b/crates/vim/test_data/test_delete_unmatched_brace.json deleted file mode 100644 index e9308edc8b..0000000000 --- a/crates/vim/test_data/test_delete_unmatched_brace.json +++ /dev/null @@ -1,12 +0,0 @@ -{"Put":{"state":"fn o(wow: i32) {\n othˇ(wow)\n oth(wow)\n}\n"}} -{"Key":"d"} -{"Key":"]"} -{"Key":"}"} -{"Get":{"state":"fn o(wow: i32) {\n otˇh\n}\n","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"(wow)\n oth(wow)"}} -{"Put":{"state":"fn o(wow: i32) {\n ˇoth(wow)\n oth(wow)\n}\n"}} -{"Key":"d"} -{"Key":"]"} -{"Key":"}"} -{"Get":{"state":"fn o(wow: i32) {\nˇ}\n","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":" oth(wow)\n oth(wow)\n"}} diff --git a/crates/vim/test_data/test_delete_w.json b/crates/vim/test_data/test_delete_w.json deleted file mode 100644 index e5e48ca444..0000000000 --- a/crates/vim/test_data/test_delete_w.json +++ /dev/null @@ -1,28 +0,0 @@ -{"Put":{"state":"Test tesˇt\n test"}} -{"Key":"d"} -{"Key":"w"} -{"Get":{"state":"Test teˇs\n test","mode":"Normal"}} -{"Put":{"state":"Teˇst"}} -{"Key":"d"} -{"Key":"w"} -{"Get":{"state":"Tˇe","mode":"Normal"}} -{"Put":{"state":"Tˇest test"}} -{"Key":"d"} -{"Key":"w"} -{"Get":{"state":"Tˇtest","mode":"Normal"}} -{"Put":{"state":"Test teˇst\ntest"}} -{"Key":"d"} -{"Key":"w"} -{"Get":{"state":"Test tˇe\ntest","mode":"Normal"}} -{"Put":{"state":"Test tesˇt\ntest"}} -{"Key":"d"} -{"Key":"w"} -{"Get":{"state":"Test teˇs\ntest","mode":"Normal"}} -{"Put":{"state":"Test test\nˇ\ntest"}} -{"Key":"d"} -{"Key":"w"} -{"Get":{"state":"Test test\nˇtest","mode":"Normal"}} -{"Put":{"state":"Test teˇst-test test"}} -{"Key":"d"} -{"Key":"shift-w"} -{"Get":{"state":"Test teˇtest","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_with_counts.json b/crates/vim/test_data/test_delete_with_counts.json deleted file mode 100644 index de19c5d29d..0000000000 --- a/crates/vim/test_data/test_delete_with_counts.json +++ /dev/null @@ -1,16 +0,0 @@ -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"d"} -{"Key":"2"} -{"Key":"d"} -{"Get":{"state":"the ˇlazy dog","mode":"Normal"}} -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"2"} -{"Key":"d"} -{"Key":"d"} -{"Get":{"state":"the ˇlazy dog","mode":"Normal"}} -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe moon,\na star, and\nthe lazy dog"}} -{"Key":"2"} -{"Key":"d"} -{"Key":"2"} -{"Key":"d"} -{"Get":{"state":"the ˇlazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_delete_word_object.json b/crates/vim/test_data/test_delete_word_object.json deleted file mode 100644 index 9c11d89a12..0000000000 --- a/crates/vim/test_data/test_delete_word_object.json +++ /dev/null @@ -1,460 +0,0 @@ -{"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick ˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick ˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick browˇn\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox ˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox ˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumpsˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy doˇg\n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇ \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick browˇn\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ\n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ\n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇfox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-ˇ over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy doˇg\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n","mode":"Normal"}} -{"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick ˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick ˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick browˇn\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox ˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox ˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumpsˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy doˇg\n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇ \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick browˇn\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ\n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ\n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇfox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n ˇ over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy doˇg\n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n","mode":"Normal"}} -{"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quickˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quickˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brownˇ jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox ˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox ˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumpˇs\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy doˇg\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-ˇover\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy doˇg\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nˇthe lazy dog ","mode":"Normal"}} -{"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quickˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quickˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brownˇ jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox ˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox ˇover\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumpˇs\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy doˇg\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ over\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n ˇover\nthe lazy dog \n\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy doˇg\n","mode":"Normal"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nˇthe lazy dog ","mode":"Normal"}} diff --git a/crates/vim/test_data/test_dgn_repeat.json b/crates/vim/test_data/test_dgn_repeat.json deleted file mode 100644 index fc1db9e778..0000000000 --- a/crates/vim/test_data/test_dgn_repeat.json +++ /dev/null @@ -1,14 +0,0 @@ -{"Put":{"state":"aaˇ aa aa aa aa"}} -{"Key":"/"} -{"Key":"a"} -{"Key":"a"} -{"Key":"enter"} -{"Get":{"state":"aa ˇaa aa aa aa","mode":"Normal"}} -{"Key":"d"} -{"Key":"g"} -{"Key":"n"} -{"Get":{"state":"aa ˇ aa aa aa","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"aa ˇ aa aa","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"aa ˇ aa","mode":"Normal"}} diff --git a/crates/vim/test_data/test_digraph_find.json b/crates/vim/test_data/test_digraph_find.json deleted file mode 100644 index 7d5811bd4d..0000000000 --- a/crates/vim/test_data/test_digraph_find.json +++ /dev/null @@ -1,12 +0,0 @@ -{"Put":{"state":"ˇHellö world"}} -{"Key":"f"} -{"Key":"ctrl-k"} -{"Key":"o"} -{"Key":":"} -{"Get":{"state":"Hellˇö world","mode":"Normal"}} -{"Put":{"state":"ˇHellö world"}} -{"Key":"t"} -{"Key":"ctrl-k"} -{"Key":"o"} -{"Key":":"} -{"Get":{"state":"Helˇlö world","mode":"Normal"}} diff --git a/crates/vim/test_data/test_digraph_insert_mode.json b/crates/vim/test_data/test_digraph_insert_mode.json deleted file mode 100644 index e7cebb262e..0000000000 --- a/crates/vim/test_data/test_digraph_insert_mode.json +++ /dev/null @@ -1,21 +0,0 @@ -{"Put":{"state":"Hellˇo"}} -{"Key":"a"} -{"Key":"ctrl-k"} -{"Key":"o"} -{"Key":":"} -{"Key":"escape"} -{"Get":{"state":"Helloˇö","mode":"Normal"}} -{"Put":{"state":"Hellˇo"}} -{"Key":"a"} -{"Key":"ctrl-k"} -{"Key":":"} -{"Key":"o"} -{"Key":"escape"} -{"Get":{"state":"Helloˇö","mode":"Normal"}} -{"Put":{"state":"Hellˇo"}} -{"Key":"i"} -{"Key":"ctrl-k"} -{"Key":"o"} -{"Key":":"} -{"Key":"escape"} -{"Get":{"state":"Hellˇöo","mode":"Normal"}} diff --git a/crates/vim/test_data/test_digraph_insert_multicursor.json b/crates/vim/test_data/test_digraph_insert_multicursor.json deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/crates/vim/test_data/test_digraph_keymap_conflict.json b/crates/vim/test_data/test_digraph_keymap_conflict.json deleted file mode 100644 index 0481fbb724..0000000000 --- a/crates/vim/test_data/test_digraph_keymap_conflict.json +++ /dev/null @@ -1,7 +0,0 @@ -{"Put":{"state":"Hellˇo"}} -{"Key":"a"} -{"Key":"ctrl-k"} -{"Key":"s"} -{"Key":","} -{"Key":"escape"} -{"Get":{"state":"Helloˇş","mode":"Normal"}} diff --git a/crates/vim/test_data/test_digraph_replace.json b/crates/vim/test_data/test_digraph_replace.json deleted file mode 100644 index 7615263339..0000000000 --- a/crates/vim/test_data/test_digraph_replace.json +++ /dev/null @@ -1,6 +0,0 @@ -{"Put":{"state":"Hellˇo"}} -{"Key":"r"} -{"Key":"ctrl-k"} -{"Key":"o"} -{"Key":":"} -{"Get":{"state":"Hellˇö","mode":"Normal"}} diff --git a/crates/vim/test_data/test_digraph_replace_mode.json b/crates/vim/test_data/test_digraph_replace_mode.json deleted file mode 100644 index 0030fde850..0000000000 --- a/crates/vim/test_data/test_digraph_replace_mode.json +++ /dev/null @@ -1,19 +0,0 @@ -{"Put":{"state":"ˇHello"}} -{"Key":"shift-r"} -{"Key":"ctrl-k"} -{"Key":"a"} -{"Key":"'"} -{"Key":"ctrl-k"} -{"Key":"e"} -{"Key":"`"} -{"Key":"ctrl-k"} -{"Key":"i"} -{"Key":":"} -{"Key":"ctrl-k"} -{"Key":"o"} -{"Key":"~"} -{"Key":"ctrl-k"} -{"Key":"u"} -{"Key":"-"} -{"Key":"escape"} -{"Get":{"state":"áèïõˇū","mode":"Normal"}} diff --git a/crates/vim/test_data/test_dot_mark.json b/crates/vim/test_data/test_dot_mark.json deleted file mode 100644 index 27edd9d2ab..0000000000 --- a/crates/vim/test_data/test_dot_mark.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Put":{"state":"one two\nthree fˇr"}} -{"Key":"i"} -{"Key":"o"} -{"Key":"escape"} -{"Key":"k"} -{"Key":"`"} -{"Key":"."} -{"Get":{"state":"one two\nthree fˇor","mode":"Normal"}} diff --git a/crates/vim/test_data/test_dot_repeat.json b/crates/vim/test_data/test_dot_repeat.json deleted file mode 100644 index 331ef52ecb..0000000000 --- a/crates/vim/test_data/test_dot_repeat.json +++ /dev/null @@ -1,38 +0,0 @@ -{"Put":{"state":"ˇhello"}} -{"Key":"o"} -{"Key":"w"} -{"Key":"o"} -{"Key":"r"} -{"Key":"l"} -{"Key":"d"} -{"Key":"escape"} -{"Get":{"state":"hello\nworlˇd","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"hello\nworld\nworlˇd","mode":"Normal"}} -{"Key":"^"} -{"Key":"d"} -{"Key":"f"} -{"Key":"o"} -{"Key":"g"} -{"Key":"g"} -{"Key":"."} -{"Get":{"state":"ˇ\nworld\nrld","mode":"Normal"}} -{"Key":"j"} -{"Key":"y"} -{"Key":"y"} -{"Key":"p"} -{"Key":"shift-g"} -{"Key":"y"} -{"Key":"y"} -{"Key":"."} -{"Get":{"state":"\nworld\nworld\nrld\nˇrld","mode":"Normal"}} -{"Put":{"state":"ˇthe quick brown fox"}} -{"Key":"2"} -{"Key":"~"} -{"Key":"."} -{"Put":{"state":"THE ˇquick brown fox"}} -{"Key":"3"} -{"Key":"."} -{"Put":{"state":"THE QUIˇck brown fox"}} -{"Key":"."} -{"Get":{"state":"THE QUICK ˇbrown fox","mode":"Normal"}} diff --git a/crates/vim/test_data/test_dw_eol.json b/crates/vim/test_data/test_dw_eol.json deleted file mode 100644 index 93ec9b1066..0000000000 --- a/crates/vim/test_data/test_dw_eol.json +++ /dev/null @@ -1,6 +0,0 @@ -{"SetOption":{"value":"wrap"}} -{"SetOption":{"value":"columns=12"}} -{"Put":{"state":"twelve ˇchar twelve char\ntwelve char"}} -{"Key":"d"} -{"Key":"w"} -{"Get":{"state":"twelve ˇtwelve char\ntwelve char","mode":"Normal"}} diff --git a/crates/vim/test_data/test_end_of_document.json b/crates/vim/test_data/test_end_of_document.json deleted file mode 100644 index 2ad056170c..0000000000 --- a/crates/vim/test_data/test_end_of_document.json +++ /dev/null @@ -1,15 +0,0 @@ -{"Put":{"state":"The qˇuick\n\nbrown fox jumps\nover the lazy dog"}} -{"Key":"shift-g"} -{"Get":{"state":"The quick\n\nbrown fox jumps\nover ˇthe lazy dog","mode":"Normal"}} -{"Key":"shift-g"} -{"Get":{"state":"The quick\n\nbrown fox jumps\nover ˇthe lazy dog","mode":"Normal"}} -{"Put":{"state":"The quick\n\nbrown fox jumps\nover the laˇzy dog"}} -{"Key":"shift-g"} -{"Get":{"state":"The quick\n\nbrown fox jumps\nover the laˇzy dog","mode":"Normal"}} -{"Put":{"state":"\n\nbrown fox jumps\nover the laˇzy dog"}} -{"Key":"shift-g"} -{"Get":{"state":"\n\nbrown fox jumps\nover the laˇzy dog","mode":"Normal"}} -{"Put":{"state":"ˇ\n\nbrown fox jumps\nover the lazydog"}} -{"Key":"2"} -{"Key":"shift-g"} -{"Get":{"state":"\nˇ\nbrown fox jumps\nover the lazydog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_end_of_line_downward.json b/crates/vim/test_data/test_end_of_line_downward.json deleted file mode 100644 index e3563f6b3f..0000000000 --- a/crates/vim/test_data/test_end_of_line_downward.json +++ /dev/null @@ -1,12 +0,0 @@ -{"Put":{"state":"ˇ one\n two \nthree"}} -{"Key":"g"} -{"Key":"_"} -{"Get":{"state":" onˇe\n two \nthree","mode":"Normal"}} -{"Put":{"state":"ˇ one \n two \nthree"}} -{"Key":"g"} -{"Key":"_"} -{"Get":{"state":" onˇe \n two \nthree","mode":"Normal"}} -{"Key":"2"} -{"Key":"g"} -{"Key":"_"} -{"Get":{"state":" one \n twˇo \nthree","mode":"Normal"}} diff --git a/crates/vim/test_data/test_end_of_line_with_neovim.json b/crates/vim/test_data/test_end_of_line_with_neovim.json deleted file mode 100644 index 58ac05134a..0000000000 --- a/crates/vim/test_data/test_end_of_line_with_neovim.json +++ /dev/null @@ -1,9 +0,0 @@ -{"Put":{"state":"ˇaa\nbb\ncc"}} -{"Key":"$"} -{"Get":{"state":"aˇa\nbb\ncc","mode":"Normal"}} -{"Key":"2"} -{"Key":"$"} -{"Get":{"state":"aa\nbˇb\ncc","mode":"Normal"}} -{"Key":"4"} -{"Key":"$"} -{"Get":{"state":"aa\nbb\ncˇc","mode":"Normal"}} diff --git a/crates/vim/test_data/test_end_of_word.json b/crates/vim/test_data/test_end_of_word.json deleted file mode 100644 index 06f80dc245..0000000000 --- a/crates/vim/test_data/test_end_of_word.json +++ /dev/null @@ -1,32 +0,0 @@ -{"Put":{"state":"Thˇe quick-brown\n\n\nfox_jumps over\nthe"}} -{"Key":"e"} -{"Get":{"state":"The quicˇk-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Key":"e"} -{"Get":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Key":"e"} -{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Key":"e"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumpˇs over\nthe","mode":"Normal"}} -{"Key":"e"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps oveˇr\nthe","mode":"Normal"}} -{"Key":"e"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}} -{"Key":"e"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}} -{"Put":{"state":"Thˇe quick-brown\n\n\nfox_jumps over\nthe"}} -{"Key":"shift-e"} -{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quicˇk-brown\n\n\nfox_jumps over\nthe"}} -{"Key":"shift-e"} -{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe"}} -{"Key":"shift-e"} -{"Get":{"state":"The quick-browˇn\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Key":"shift-e"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumpˇs over\nthe","mode":"Normal"}} -{"Key":"shift-e"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps oveˇr\nthe","mode":"Normal"}} -{"Key":"shift-e"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}} -{"Key":"shift-e"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}} diff --git a/crates/vim/test_data/test_enter.json b/crates/vim/test_data/test_enter.json deleted file mode 100644 index c010a1ffd2..0000000000 --- a/crates/vim/test_data/test_enter.json +++ /dev/null @@ -1,11 +0,0 @@ -{"Put":{"state":"ˇThe quick brown\nfox jumps"}} -{"Key":"enter"} -{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}} -{"Put":{"state":"The qˇuick brown\nfox jumps"}} -{"Key":"enter"} -{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}} -{"Put":{"state":"The quick broˇwn\nfox jumps"}} -{"Key":"enter"} -{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}} -{"Key":"enter"} -{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}} diff --git a/crates/vim/test_data/test_enter_visual_line_mode.json b/crates/vim/test_data/test_enter_visual_line_mode.json deleted file mode 100644 index bf14ae2495..0000000000 --- a/crates/vim/test_data/test_enter_visual_line_mode.json +++ /dev/null @@ -1,15 +0,0 @@ -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Get":{"state":"The «qˇ»uick brown\nfox jumps over\nthe lazy dog","mode":"VisualLine"}} -{"Key":"x"} -{"Get":{"state":"fox ˇjumps over\nthe lazy dog","mode":"Normal"}} -{"Put":{"state":"a\nˇ\nb"}} -{"Key":"shift-v"} -{"Get":{"state":"a\n«\nˇ»b","mode":"VisualLine"}} -{"Key":"x"} -{"Get":{"state":"a\nˇb","mode":"Normal"}} -{"Put":{"state":"a\nb\nˇ"}} -{"Key":"shift-v"} -{"Get":{"state":"a\nb\nˇ","mode":"VisualLine"}} -{"Key":"x"} -{"Get":{"state":"a\nˇb","mode":"Normal"}} diff --git a/crates/vim/test_data/test_enter_visual_mode.json b/crates/vim/test_data/test_enter_visual_mode.json deleted file mode 100644 index 090e35cc5d..0000000000 --- a/crates/vim/test_data/test_enter_visual_mode.json +++ /dev/null @@ -1,20 +0,0 @@ -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"v"} -{"Get":{"state":"The «qˇ»uick brown\nfox jumps over\nthe lazy dog","mode":"Visual"}} -{"Key":"w"} -{"Key":"j"} -{"Get":{"state":"The «quick brown\nfox jumps oˇ»ver\nthe lazy dog","mode":"Visual"}} -{"Key":"escape"} -{"Get":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog","mode":"Normal"}} -{"Key":"v"} -{"Key":"k"} -{"Key":"b"} -{"Get":{"state":"The «ˇquick brown\nfox jumps o»ver\nthe lazy dog","mode":"Visual"}} -{"Put":{"state":"a\nˇ\nb\n"}} -{"Key":"v"} -{"Get":{"state":"a\n«\nˇ»b\n","mode":"Visual"}} -{"Key":"v"} -{"Get":{"state":"a\nˇ\nb\n","mode":"Normal"}} -{"Put":{"state":"a\nb\nˇ"}} -{"Key":"v"} -{"Get":{"state":"a\nb\nˇ","mode":"Visual"}} diff --git a/crates/vim/test_data/test_escape_while_waiting.json b/crates/vim/test_data/test_escape_while_waiting.json deleted file mode 100644 index d81822cf79..0000000000 --- a/crates/vim/test_data/test_escape_while_waiting.json +++ /dev/null @@ -1,6 +0,0 @@ -{"Put":{"state":"ˇhi"}} -{"Key":"\""} -{"Key":"+"} -{"Key":"escape"} -{"Key":"x"} -{"Get":{"state":"ˇi","mode":"Normal"}} diff --git a/crates/vim/test_data/test_f_and_t.json b/crates/vim/test_data/test_f_and_t.json deleted file mode 100644 index 2d2a358452..0000000000 --- a/crates/vim/test_data/test_f_and_t.json +++ /dev/null @@ -1,557 +0,0 @@ -{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n\nb\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n\nb\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n\nb\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ\nb\n"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n\nˇb\n"}} -{"Key":"1"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n\nˇb\n","mode":"Normal"}} -{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaˇab b bb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇ bb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇ bb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaˇabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaˇabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaˇabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n ˇ baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaaˇ bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaaˇ bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaaˇ bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ\nb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n\nˇb\n"}} -{"Key":"1"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n\nˇb\n","mode":"Normal"}} -{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ\nb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n\nˇb\n"}} -{"Key":"2"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n\nˇb\n","mode":"Normal"}} -{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇ bb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇ bb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaˇabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaaˇ bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n\nb\n"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n\nb\n","mode":"Normal"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n","mode":"Normal"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n\nb\n"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ\nb\n"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n\nˇb\n"}} -{"Key":"2"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n\nˇb\n","mode":"Normal"}} -{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ\nb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n\nˇb\n"}} -{"Key":"3"} -{"Key":"f"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n\nˇb\n","mode":"Normal"}} -{"Put":{"state":"ˇaaab b bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇ bb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaaˇb b bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaabˇ b bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab ˇb bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaˇabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab bˇ bb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaˇabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b ˇbb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bˇb aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bbˇ aaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aˇaabaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaaˇbaaa\n baaa bbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\nˇ baaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n ˇbaaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n bˇaaa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaˇa bbb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa ˇbbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bˇbb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbˇb\n\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ\nb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\nˇ\nb\n","mode":"Normal"}} -{"Put":{"state":"aaab b bb aaabaaa\n baaa bbb\n\nˇb\n"}} -{"Key":"3"} -{"Key":"t"} -{"Key":"b"} -{"Get":{"state":"aaab b bb aaabaaa\n baaa bbb\n\nˇb\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_find_multibyte.json b/crates/vim/test_data/test_find_multibyte.json deleted file mode 100644 index 710a89aede..0000000000 --- a/crates/vim/test_data/test_find_multibyte.json +++ /dev/null @@ -1,7 +0,0 @@ -{"Put":{"state":""}} -{"Key":"c"} -{"Key":"t"} -{"Key":"<"} -{"Key":"o"} -{"Key":"escape"} -{"Get":{"state":"","mode":"Normal"}} diff --git a/crates/vim/test_data/test_folds.json b/crates/vim/test_data/test_folds.json deleted file mode 100644 index c1972480d1..0000000000 --- a/crates/vim/test_data/test_folds.json +++ /dev/null @@ -1,24 +0,0 @@ -{"SetOption":{"value":"foldmethod=manual"}} -{"Put":{"state":"fn boop() {\n ˇbarp()\n bazp()\n}\n"}} -{"Key":"shift-v"} -{"Key":"j"} -{"Key":"z"} -{"Key":"f"} -{"Key":"escape"} -{"Key":"g"} -{"Key":"g"} -{"Get":{"state":"ˇfn boop() {\n barp()\n bazp()\n}\n","mode":"Normal"}} -{"Key":"j"} -{"Key":"j"} -{"Get":{"state":"fn boop() {\n barp()\n bazp()\nˇ}\n","mode":"Normal"}} -{"Key":"2"} -{"Key":"k"} -{"Get":{"state":"ˇfn boop() {\n barp()\n bazp()\n}\n","mode":"Normal"}} -{"Key":"down"} -{"Key":"y"} -{"Key":"y"} -{"Get":{"state":"fn boop() {\nˇ barp()\n bazp()\n}\n","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":" barp()\n bazp()\n"}} -{"Key":"z"} -{"Key":"o"} -{"Get":{"state":"fn boop() {\nˇ barp()\n bazp()\n}\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_folds_panic.json b/crates/vim/test_data/test_folds_panic.json deleted file mode 100644 index a215765b04..0000000000 --- a/crates/vim/test_data/test_folds_panic.json +++ /dev/null @@ -1,23 +0,0 @@ -{"SetOption":{"value":"foldmethod=manual"}} -{"Put":{"state":"fn boop() {\n ˇbarp()\n bazp()\n}\n"}} -{"Key":"shift-v"} -{"Key":"j"} -{"Key":"z"} -{"Key":"f"} -{"Key":"escape"} -{"Key":"g"} -{"Key":"g"} -{"Key":"5"} -{"Key":"d"} -{"Key":"j"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"fn boop() {\n ˇbarp()\n bazp()\n}\n"}} -{"Key":"shift-v"} -{"Key":"j"} -{"Key":"j"} -{"Key":"z"} -{"Key":"f"} -{"Key":"escape"} -{"Key":"shift-g"} -{"Key":"shift-v"} -{"Get":{"state":"fn boop() {\n barp()\n bazp()\n}\nˇ","mode":"VisualLine"}} diff --git a/crates/vim/test_data/test_forced_motion_delete_to_end_of_line.json b/crates/vim/test_data/test_forced_motion_delete_to_end_of_line.json deleted file mode 100644 index 4df916befb..0000000000 --- a/crates/vim/test_data/test_forced_motion_delete_to_end_of_line.json +++ /dev/null @@ -1,10 +0,0 @@ -{"Put":{"state":"the quick brown foˇx\njumped over the lazy dog"}} -{"Key":"d"} -{"Key":"v"} -{"Key":"$"} -{"Get":{"state":"the quick brown foˇx\njumped over the lazy dog","mode":"Normal"}} -{"Put":{"state":"ˇthe quick brown fox\njumped over the lazy dog"}} -{"Key":"d"} -{"Key":"v"} -{"Key":"$"} -{"Get":{"state":"ˇx\njumped over the lazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_forced_motion_delete_to_middle_of_line.json b/crates/vim/test_data/test_forced_motion_delete_to_middle_of_line.json deleted file mode 100644 index ca6aa52804..0000000000 --- a/crates/vim/test_data/test_forced_motion_delete_to_middle_of_line.json +++ /dev/null @@ -1,34 +0,0 @@ -{"Put":{"state":"ˇthe quick brown fox\njumped over the lazy dog"}} -{"Key":"d"} -{"Key":"v"} -{"Key":"g"} -{"Key":"shift-m"} -{"Get":{"state":"ˇbrown fox\njumped over the lazy dog","mode":"Normal"}} -{"Put":{"state":"the quick bˇrown fox\njumped over the lazy dog"}} -{"Key":"d"} -{"Key":"v"} -{"Key":"g"} -{"Key":"shift-m"} -{"Get":{"state":"the quickˇown fox\njumped over the lazy dog","mode":"Normal"}} -{"Put":{"state":"the quick brown foˇx\njumped over the lazy dog"}} -{"Key":"d"} -{"Key":"v"} -{"Key":"g"} -{"Key":"shift-m"} -{"Get":{"state":"the quicˇk\njumped over the lazy dog","mode":"Normal"}} -{"Put":{"state":"ˇthe quick brown fox\njumped over the lazy dog"}} -{"Key":"d"} -{"Key":"v"} -{"Key":"7"} -{"Key":"5"} -{"Key":"g"} -{"Key":"shift-m"} -{"Get":{"state":"ˇ fox\njumped over the lazy dog","mode":"Normal"}} -{"Put":{"state":"ˇthe quick brown fox\njumped over the lazy dog"}} -{"Key":"d"} -{"Key":"v"} -{"Key":"2"} -{"Key":"3"} -{"Key":"g"} -{"Key":"shift-m"} -{"Get":{"state":"ˇuick brown fox\njumped over the lazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_forced_motion_delete_to_start_of_line.json b/crates/vim/test_data/test_forced_motion_delete_to_start_of_line.json deleted file mode 100644 index 8aae77c8de..0000000000 --- a/crates/vim/test_data/test_forced_motion_delete_to_start_of_line.json +++ /dev/null @@ -1,15 +0,0 @@ -{"Put":{"state":"ˇthe quick brown fox\njumped over the lazy dog"}} -{"Key":"d"} -{"Key":"v"} -{"Key":"0"} -{"Get":{"state":"ˇhe quick brown fox\njumped over the lazy dog","mode":"Normal"}} -{"Put":{"state":"the quick bˇrown fox\njumped over the lazy dog"}} -{"Key":"d"} -{"Key":"v"} -{"Key":"0"} -{"Get":{"state":"ˇown fox\njumped over the lazy dog","mode":"Normal"}} -{"Put":{"state":"the quick brown foˇx\njumped over the lazy dog"}} -{"Key":"d"} -{"Key":"v"} -{"Key":"0"} -{"Get":{"state":"ˇ\njumped over the lazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_forced_motion_yank.json b/crates/vim/test_data/test_forced_motion_yank.json deleted file mode 100644 index 208c22d689..0000000000 --- a/crates/vim/test_data/test_forced_motion_yank.json +++ /dev/null @@ -1,24 +0,0 @@ -{"Put":{"state":"ˇthe quick brown fox\njumped over the lazy dog"}} -{"Key":"y"} -{"Key":"v"} -{"Key":"j"} -{"Key":"p"} -{"Get":{"state":"the quick brown fox\nˇthe quick brown fox\njumped over the lazy dog","mode":"Normal"}} -{"Put":{"state":"the quick bˇrown fox\njumped over the lazy dog"}} -{"Key":"y"} -{"Key":"v"} -{"Key":"j"} -{"Key":"p"} -{"Get":{"state":"the quick brˇrown fox\njumped overown fox\njumped over the lazy dog","mode":"Normal"}} -{"Put":{"state":"the quick brown foˇx\njumped over the lazy dog"}} -{"Key":"y"} -{"Key":"v"} -{"Key":"j"} -{"Key":"p"} -{"Get":{"state":"the quick brown foxˇx\njumped over the la\njumped over the lazy dog","mode":"Normal"}} -{"Put":{"state":"the quick brown fox\njˇumped over the lazy dog"}} -{"Key":"y"} -{"Key":"v"} -{"Key":"k"} -{"Key":"p"} -{"Get":{"state":"thˇhe quick brown fox\nje quick brown fox\njumped over the lazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_gg.json b/crates/vim/test_data/test_gg.json deleted file mode 100644 index 7cc8291b13..0000000000 --- a/crates/vim/test_data/test_gg.json +++ /dev/null @@ -1,21 +0,0 @@ -{"Put":{"state":"The qˇuick\n\nbrown fox jumps\nover the lazy dog"}} -{"Key":"g"} -{"Key":"g"} -{"Get":{"state":"The qˇuick\n\nbrown fox jumps\nover the lazy dog","mode":"Normal"}} -{"Put":{"state":"The quick\n\nbrown fox jumps\nover ˇthe lazy dog"}} -{"Key":"g"} -{"Key":"g"} -{"Get":{"state":"The qˇuick\n\nbrown fox jumps\nover the lazy dog","mode":"Normal"}} -{"Put":{"state":"The quick\n\nbrown fox jumps\nover the laˇzy dog"}} -{"Key":"g"} -{"Key":"g"} -{"Get":{"state":"The quicˇk\n\nbrown fox jumps\nover the lazy dog","mode":"Normal"}} -{"Put":{"state":"\n\nbrown fox jumps\nover the laˇzy dog"}} -{"Key":"g"} -{"Key":"g"} -{"Get":{"state":"ˇ\n\nbrown fox jumps\nover the lazy dog","mode":"Normal"}} -{"Put":{"state":"ˇ\n\nbrown fox jumps\nover the lazydog"}} -{"Key":"2"} -{"Key":"g"} -{"Key":"g"} -{"Get":{"state":"\nˇ\nbrown fox jumps\nover the lazydog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_gi.json b/crates/vim/test_data/test_gi.json deleted file mode 100644 index a36a919751..0000000000 --- a/crates/vim/test_data/test_gi.json +++ /dev/null @@ -1,10 +0,0 @@ -{"Put":{"state":"one two\nthree fˇr"}} -{"Key":"i"} -{"Key":"o"} -{"Key":"escape"} -{"Key":"k"} -{"Key":"g"} -{"Key":"i"} -{"Key":"u"} -{"Key":"escape"} -{"Get":{"state":"one two\nthree foˇur","mode":"Normal"}} diff --git a/crates/vim/test_data/test_gn.json b/crates/vim/test_data/test_gn.json deleted file mode 100644 index b9e0558fca..0000000000 --- a/crates/vim/test_data/test_gn.json +++ /dev/null @@ -1,39 +0,0 @@ -{"Put":{"state":"aaˇ aa aa aa aa"}} -{"Key":"/"} -{"Key":"a"} -{"Key":"a"} -{"Key":"enter"} -{"Get":{"state":"aa ˇaa aa aa aa","mode":"Normal"}} -{"Key":"g"} -{"Key":"n"} -{"Get":{"state":"aa «aaˇ» aa aa aa","mode":"Visual"}} -{"Key":"g"} -{"Key":"n"} -{"Get":{"state":"aa «aa aaˇ» aa aa","mode":"Visual"}} -{"Key":"escape"} -{"Key":"d"} -{"Key":"g"} -{"Key":"n"} -{"Get":{"state":"aa aa ˇ aa aa","mode":"Normal"}} -{"Put":{"state":"aaˇ aa aa aa aa"}} -{"Key":"/"} -{"Key":"a"} -{"Key":"a"} -{"Key":"enter"} -{"Get":{"state":"aa ˇaa aa aa aa","mode":"Normal"}} -{"Key":"3"} -{"Key":"g"} -{"Key":"n"} -{"Get":{"state":"aa aa aa «aaˇ» aa","mode":"Visual"}} -{"Put":{"state":"aaˇ aa aa aa aa"}} -{"Key":"/"} -{"Key":"a"} -{"Key":"a"} -{"Key":"enter"} -{"Get":{"state":"aa ˇaa aa aa aa","mode":"Normal"}} -{"Key":"g"} -{"Key":"shift-n"} -{"Get":{"state":"aa «ˇaa» aa aa aa","mode":"Visual"}} -{"Key":"g"} -{"Key":"shift-n"} -{"Get":{"state":"«ˇaa aa» aa aa aa","mode":"Visual"}} diff --git a/crates/vim/test_data/test_go_to_percentage.json b/crates/vim/test_data/test_go_to_percentage.json deleted file mode 100644 index d7efd170f2..0000000000 --- a/crates/vim/test_data/test_go_to_percentage.json +++ /dev/null @@ -1,26 +0,0 @@ -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"2"} -{"Key":"0"} -{"Key":"%"} -{"Get":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"2"} -{"Key":"5"} -{"Key":"%"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog\nThe quick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"7"} -{"Key":"5"} -{"Key":"%"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog\nThe ˇquick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"5"} -{"Key":"0"} -{"Key":"%"} -{"Get":{"state":"The «quick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jˇ»umps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog","mode":"Visual"}} -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"1"} -{"Key":"0"} -{"Key":"0"} -{"Key":"%"} -{"Get":{"state":"The «quick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lazy dog\nThe quick brown\nfox jumps over\nthe lˇ»azy dog","mode":"Visual"}} diff --git a/crates/vim/test_data/test_gq.json b/crates/vim/test_data/test_gq.json deleted file mode 100644 index 08cdb12315..0000000000 --- a/crates/vim/test_data/test_gq.json +++ /dev/null @@ -1,12 +0,0 @@ -{"SetOption":{"value":"textwidth=5"}} -{"Put":{"state":"ˇth th th th th th\n"}} -{"Key":"g"} -{"Key":"q"} -{"Key":"q"} -{"Get":{"state":"th th\nth th\nˇth th\n","mode":"Normal"}} -{"Put":{"state":"ˇth th th th th th\nth th th th th th\n"}} -{"Key":"v"} -{"Key":"j"} -{"Key":"g"} -{"Key":"q"} -{"Get":{"state":"th th\nth th\nth th\nth th\nth th\nˇth th\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_gv.json b/crates/vim/test_data/test_gv.json deleted file mode 100644 index 7d33535098..0000000000 --- a/crates/vim/test_data/test_gv.json +++ /dev/null @@ -1,24 +0,0 @@ -{"Put":{"state":"The ˇquick brown"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Key":"escape"} -{"Key":"g"} -{"Key":"v"} -{"Get":{"state":"The «quickˇ» brown","mode":"Visual"}} -{"Key":"o"} -{"Key":"escape"} -{"Key":"g"} -{"Key":"v"} -{"Get":{"state":"The «ˇquick» brown","mode":"Visual"}} -{"Key":"escape"} -{"Key":"^"} -{"Key":"ctrl-v"} -{"Key":"l"} -{"Get":{"state":"«Thˇ»e quick brown","mode":"VisualBlock"}} -{"Key":"g"} -{"Key":"v"} -{"Get":{"state":"The «ˇquick» brown","mode":"Visual"}} -{"Key":"g"} -{"Key":"v"} -{"Get":{"state":"«Thˇ»e quick brown","mode":"VisualBlock"}} diff --git a/crates/vim/test_data/test_h.json b/crates/vim/test_data/test_h.json deleted file mode 100644 index 4cdf210326..0000000000 --- a/crates/vim/test_data/test_h.json +++ /dev/null @@ -1,9 +0,0 @@ -{"Put":{"state":"ˇThe quick\nbrown"}} -{"Key":"h"} -{"Get":{"state":"ˇThe quick\nbrown","mode":"Normal"}} -{"Put":{"state":"The qˇuick\nbrown"}} -{"Key":"h"} -{"Get":{"state":"The ˇquick\nbrown","mode":"Normal"}} -{"Put":{"state":"The quick\nˇbrown"}} -{"Key":"h"} -{"Get":{"state":"The quick\nˇbrown","mode":"Normal"}} diff --git a/crates/vim/test_data/test_h_through_unicode.json b/crates/vim/test_data/test_h_through_unicode.json deleted file mode 100644 index 95b18396d0..0000000000 --- a/crates/vim/test_data/test_h_through_unicode.json +++ /dev/null @@ -1,12 +0,0 @@ -{"Put":{"state":"Testˇ├──┐Test"}} -{"Key":"h"} -{"Get":{"state":"Tesˇt├──┐Test","mode":"Normal"}} -{"Put":{"state":"Test├ˇ──┐Test"}} -{"Key":"h"} -{"Get":{"state":"Testˇ├──┐Test","mode":"Normal"}} -{"Put":{"state":"Test├──ˇ┐Test"}} -{"Key":"h"} -{"Get":{"state":"Test├─ˇ─┐Test","mode":"Normal"}} -{"Put":{"state":"Test├──┐ˇTest"}} -{"Key":"h"} -{"Get":{"state":"Test├──ˇ┐Test","mode":"Normal"}} diff --git a/crates/vim/test_data/test_horizontal_scroll.json b/crates/vim/test_data/test_horizontal_scroll.json deleted file mode 100644 index c6cbac8be5..0000000000 --- a/crates/vim/test_data/test_horizontal_scroll.json +++ /dev/null @@ -1,16 +0,0 @@ -{"SetOption":{"value":"scrolloff=3"}} -{"SetOption":{"value":"lines=22"}} -{"SetOption":{"value":"wrap"}} -{"SetOption":{"value":"columns=12"}} -{"SetOption":{"value":"nowrap"}} -{"Put":{"state":"ˇ01234567890123456789"}} -{"Key":"z"} -{"Key":"shift-l"} -{"Get":{"state":"012345ˇ67890123456789","mode":"Normal"}} -{"Key":"z"} -{"Key":"h"} -{"Get":{"state":"012345ˇ67890123456789","mode":"Normal"}} -{"Put":{"state":"ˇ01234567890123456789"}} -{"Key":"z"} -{"Key":"l"} -{"Get":{"state":"0ˇ1234567890123456789","mode":"Normal"}} diff --git a/crates/vim/test_data/test_inclusive_to_exclusive_delete.json b/crates/vim/test_data/test_inclusive_to_exclusive_delete.json deleted file mode 100644 index 3d25b9fc67..0000000000 --- a/crates/vim/test_data/test_inclusive_to_exclusive_delete.json +++ /dev/null @@ -1,15 +0,0 @@ -{"Put":{"state":"ˇthe quick brown fox\njumped over the lazy dog"}} -{"Key":"d"} -{"Key":"v"} -{"Key":"e"} -{"Get":{"state":"ˇe quick brown fox\njumped over the lazy dog","mode":"Normal"}} -{"Put":{"state":"the quick bˇrown fox\njumped over the lazy dog"}} -{"Key":"d"} -{"Key":"v"} -{"Key":"e"} -{"Get":{"state":"the quick bˇn fox\njumped over the lazy dog","mode":"Normal"}} -{"Put":{"state":"the quick brown foˇx\njumped over the lazy dog"}} -{"Key":"d"} -{"Key":"v"} -{"Key":"e"} -{"Get":{"state":"the quick brown foˇd over the lazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment.json b/crates/vim/test_data/test_increment.json deleted file mode 100644 index fe893bca97..0000000000 --- a/crates/vim/test_data/test_increment.json +++ /dev/null @@ -1,16 +0,0 @@ -{"Put":{"state":"1ˇ2\n"}} -{"Key":"ctrl-a"} -{"Get":{"state":"1ˇ3\n","mode":"Normal"}} -{"Key":"ctrl-x"} -{"Get":{"state":"1ˇ2\n","mode":"Normal"}} -{"Key":"9"} -{"Key":"9"} -{"Key":"ctrl-a"} -{"Get":{"state":"11ˇ1\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"1"} -{"Key":"1"} -{"Key":"ctrl-x"} -{"Get":{"state":"ˇ0\n","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"-11ˇ1\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_bin_wrapping_and_padding.json b/crates/vim/test_data/test_increment_bin_wrapping_and_padding.json deleted file mode 100644 index 69c118c0ad..0000000000 --- a/crates/vim/test_data/test_increment_bin_wrapping_and_padding.json +++ /dev/null @@ -1,10 +0,0 @@ -{"Put":{"state":"0b111111111111111111111111111111111111111111111111111111111111111111111ˇ1\n"}} -{"Key":"ctrl-a"} -{"Get":{"state":"0b000000111111111111111111111111111111111111111111111111111111111111111ˇ1\n","mode":"Normal"}} -{"Key":"ctrl-a"} -{"Get":{"state":"0b000000000000000000000000000000000000000000000000000000000000000000000ˇ0\n","mode":"Normal"}} -{"Key":"ctrl-a"} -{"Get":{"state":"0b000000000000000000000000000000000000000000000000000000000000000000000ˇ1\n","mode":"Normal"}} -{"Key":"2"} -{"Key":"ctrl-x"} -{"Get":{"state":"0b000000111111111111111111111111111111111111111111111111111111111111111ˇ1\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_hex_casing.json b/crates/vim/test_data/test_increment_hex_casing.json deleted file mode 100644 index 951906fa25..0000000000 --- a/crates/vim/test_data/test_increment_hex_casing.json +++ /dev/null @@ -1,5 +0,0 @@ -{"Put":{"state":"0xFˇa\n"}} -{"Key":"ctrl-a"} -{"Get":{"state":"0xfˇb\n","mode":"Normal"}} -{"Key":"ctrl-a"} -{"Get":{"state":"0xfˇc\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_hex_wrapping_and_padding.json b/crates/vim/test_data/test_increment_hex_wrapping_and_padding.json deleted file mode 100644 index 562b368812..0000000000 --- a/crates/vim/test_data/test_increment_hex_wrapping_and_padding.json +++ /dev/null @@ -1,10 +0,0 @@ -{"Put":{"state":"0xfffffffffffffffffffˇf\n"}} -{"Key":"ctrl-a"} -{"Get":{"state":"0x0000fffffffffffffffˇf\n","mode":"Normal"}} -{"Key":"ctrl-a"} -{"Get":{"state":"0x0000000000000000000ˇ0\n","mode":"Normal"}} -{"Key":"ctrl-a"} -{"Get":{"state":"0x0000000000000000000ˇ1\n","mode":"Normal"}} -{"Key":"2"} -{"Key":"ctrl-x"} -{"Get":{"state":"0x0000fffffffffffffffˇf\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_inline.json b/crates/vim/test_data/test_increment_inline.json deleted file mode 100644 index 1e3d8fbd90..0000000000 --- a/crates/vim/test_data/test_increment_inline.json +++ /dev/null @@ -1,10 +0,0 @@ -{"Put":{"state":"inline0x3ˇ9u32\n"}} -{"Key":"ctrl-a"} -{"Get":{"state":"inline0x3ˇau32\n","mode":"Normal"}} -{"Key":"ctrl-a"} -{"Get":{"state":"inline0x3ˇbu32\n","mode":"Normal"}} -{"Key":"l"} -{"Key":"l"} -{"Key":"l"} -{"Key":"ctrl-a"} -{"Get":{"state":"inline0x3bu3ˇ3\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_radix.json b/crates/vim/test_data/test_increment_radix.json deleted file mode 100644 index 0f41c01599..0000000000 --- a/crates/vim/test_data/test_increment_radix.json +++ /dev/null @@ -1,18 +0,0 @@ -{"Put":{"state":"ˇ total: 0xff"}} -{"Key":"ctrl-a"} -{"Get":{"state":" total: 0x10ˇ0","mode":"Normal"}} -{"Put":{"state":"ˇ total: 0xff"}} -{"Key":"ctrl-x"} -{"Get":{"state":" total: 0xfˇe","mode":"Normal"}} -{"Put":{"state":"ˇ total: 0xFF"}} -{"Key":"ctrl-x"} -{"Get":{"state":" total: 0xFˇE","mode":"Normal"}} -{"Put":{"state":"(ˇ0b10f)"}} -{"Key":"ctrl-a"} -{"Get":{"state":"(0b1ˇ1f)","mode":"Normal"}} -{"Put":{"state":"ˇ-1"}} -{"Key":"ctrl-a"} -{"Get":{"state":"ˇ0","mode":"Normal"}} -{"Put":{"state":"banˇana"}} -{"Key":"ctrl-a"} -{"Get":{"state":"banˇana","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_sign_change.json b/crates/vim/test_data/test_increment_sign_change.json deleted file mode 100644 index 8f2ee7f2f3..0000000000 --- a/crates/vim/test_data/test_increment_sign_change.json +++ /dev/null @@ -1,6 +0,0 @@ -{"Put":{"state":"ˇ0\n"}} -{"Key":"ctrl-x"} -{"Get":{"state":"-ˇ1\n","mode":"Normal"}} -{"Key":"2"} -{"Key":"ctrl-a"} -{"Get":{"state":"ˇ1\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_sign_change_with_leading_zeros.json b/crates/vim/test_data/test_increment_sign_change_with_leading_zeros.json deleted file mode 100644 index c1257012b5..0000000000 --- a/crates/vim/test_data/test_increment_sign_change_with_leading_zeros.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Put":{"state":"00ˇ1\n"}} -{"Key":"ctrl-x"} -{"Get":{"state":"00ˇ0\n","mode":"Normal"}} -{"Key":"ctrl-x"} -{"Get":{"state":"-00ˇ1\n","mode":"Normal"}} -{"Key":"2"} -{"Key":"ctrl-a"} -{"Get":{"state":"00ˇ1\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_steps.json b/crates/vim/test_data/test_increment_steps.json deleted file mode 100644 index 5e16efed53..0000000000 --- a/crates/vim/test_data/test_increment_steps.json +++ /dev/null @@ -1,25 +0,0 @@ -{"Put":{"state":"ˇ1\n1\n1 2\n1\n1"}} -{"Key":"j"} -{"Key":"v"} -{"Key":"shift-g"} -{"Key":"g"} -{"Key":"ctrl-a"} -{"Get":{"state":"1\nˇ2\n3 2\n4\n5","mode":"Normal"}} -{"Key":"shift-g"} -{"Key":"ctrl-v"} -{"Key":"g"} -{"Key":"g"} -{"Get":{"state":"«1ˇ»\n«2ˇ»\n«3ˇ» 2\n«4ˇ»\n«5ˇ»","mode":"VisualBlock"}} -{"Key":"g"} -{"Key":"ctrl-x"} -{"Get":{"state":"ˇ0\n0\n0 2\n0\n0","mode":"Normal"}} -{"Key":"v"} -{"Key":"shift-g"} -{"Key":"g"} -{"Key":"ctrl-a"} -{"Key":"v"} -{"Key":"shift-g"} -{"Key":"5"} -{"Key":"g"} -{"Key":"ctrl-a"} -{"Get":{"state":"ˇ6\n12\n18 2\n24\n30","mode":"Normal"}} 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/vim/test_data/test_increment_with_changing_leading_zeros.json b/crates/vim/test_data/test_increment_with_changing_leading_zeros.json deleted file mode 100644 index dce392e25f..0000000000 --- a/crates/vim/test_data/test_increment_with_changing_leading_zeros.json +++ /dev/null @@ -1,6 +0,0 @@ -{"Put":{"state":"099ˇ9\n"}} -{"Key":"ctrl-a"} -{"Get":{"state":"100ˇ0\n","mode":"Normal"}} -{"Key":"2"} -{"Key":"ctrl-x"} -{"Get":{"state":"99ˇ8\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_with_dot.json b/crates/vim/test_data/test_increment_with_dot.json deleted file mode 100644 index b5c5b0914e..0000000000 --- a/crates/vim/test_data/test_increment_with_dot.json +++ /dev/null @@ -1,5 +0,0 @@ -{"Put":{"state":"1ˇ.2\n"}} -{"Key":"ctrl-a"} -{"Get":{"state":"1.ˇ3\n","mode":"Normal"}} -{"Key":"ctrl-x"} -{"Get":{"state":"1.ˇ2\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_with_leading_zeros.json b/crates/vim/test_data/test_increment_with_leading_zeros.json deleted file mode 100644 index bab262463f..0000000000 --- a/crates/vim/test_data/test_increment_with_leading_zeros.json +++ /dev/null @@ -1,6 +0,0 @@ -{"Put":{"state":"000ˇ9\n"}} -{"Key":"ctrl-a"} -{"Get":{"state":"001ˇ0\n","mode":"Normal"}} -{"Key":"2"} -{"Key":"ctrl-x"} -{"Get":{"state":"000ˇ8\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_with_leading_zeros_and_zero.json b/crates/vim/test_data/test_increment_with_leading_zeros_and_zero.json deleted file mode 100644 index 94a1a1a715..0000000000 --- a/crates/vim/test_data/test_increment_with_leading_zeros_and_zero.json +++ /dev/null @@ -1,7 +0,0 @@ -{"Put":{"state":"01ˇ1\n"}} -{"Key":"ctrl-a"} -{"Get":{"state":"01ˇ2\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"2"} -{"Key":"ctrl-x"} -{"Get":{"state":"00ˇ0\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_with_two_dots.json b/crates/vim/test_data/test_increment_with_two_dots.json deleted file mode 100644 index 38b38f1005..0000000000 --- a/crates/vim/test_data/test_increment_with_two_dots.json +++ /dev/null @@ -1,5 +0,0 @@ -{"Put":{"state":"111.ˇ.2\n"}} -{"Key":"ctrl-a"} -{"Get":{"state":"111..ˇ3\n","mode":"Normal"}} -{"Key":"ctrl-x"} -{"Get":{"state":"111..ˇ2\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_wrapping.json b/crates/vim/test_data/test_increment_wrapping.json deleted file mode 100644 index 9f189991a6..0000000000 --- a/crates/vim/test_data/test_increment_wrapping.json +++ /dev/null @@ -1,13 +0,0 @@ -{"Put":{"state":"1844674407370955161ˇ9\n"}} -{"Key":"ctrl-a"} -{"Get":{"state":"1844674407370955161ˇ5\n","mode":"Normal"}} -{"Key":"ctrl-a"} -{"Get":{"state":"-1844674407370955161ˇ5\n","mode":"Normal"}} -{"Key":"ctrl-a"} -{"Get":{"state":"-1844674407370955161ˇ4\n","mode":"Normal"}} -{"Key":"3"} -{"Key":"ctrl-x"} -{"Get":{"state":"1844674407370955161ˇ4\n","mode":"Normal"}} -{"Key":"2"} -{"Key":"ctrl-a"} -{"Get":{"state":"-1844674407370955161ˇ5\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_increment_zero_leading_zeros.json b/crates/vim/test_data/test_increment_zero_leading_zeros.json deleted file mode 100644 index 5361fcbc7f..0000000000 --- a/crates/vim/test_data/test_increment_zero_leading_zeros.json +++ /dev/null @@ -1,4 +0,0 @@ -{"Put":{"state":"001ˇ0\n"}} -{"Key":"10"} -{"Key":"ctrl-x"} -{"Get":{"state":"000ˇ9\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_indent_gv.json b/crates/vim/test_data/test_indent_gv.json deleted file mode 100644 index 2c24406aee..0000000000 --- a/crates/vim/test_data/test_indent_gv.json +++ /dev/null @@ -1,8 +0,0 @@ -{"SetOption":{"value":"shiftwidth=4"}} -{"Put":{"state":"ˇhello\nworld\n"}} -{"Key":"v"} -{"Key":"j"} -{"Key":">"} -{"Key":"g"} -{"Key":"v"} -{"Get":{"state":"« hello\n ˇ» world\n","mode":"Visual"}} diff --git a/crates/vim/test_data/test_insert_ctrl_r.json b/crates/vim/test_data/test_insert_ctrl_r.json deleted file mode 100644 index 6167bc3f33..0000000000 --- a/crates/vim/test_data/test_insert_ctrl_r.json +++ /dev/null @@ -1,12 +0,0 @@ -{"Put":{"state":"heˇllo\n"}} -{"Key":"y"} -{"Key":"y"} -{"Key":"i"} -{"Key":"ctrl-r"} -{"Key":"\""} -{"Get":{"state":"hehello\nˇllo\n","mode":"Insert"}} -{"Key":"ctrl-r"} -{"Key":"x"} -{"Key":"ctrl-r"} -{"Key":"escape"} -{"Get":{"state":"hehello\nˇllo\n","mode":"Insert"}} diff --git a/crates/vim/test_data/test_insert_ctrl_y.json b/crates/vim/test_data/test_insert_ctrl_y.json deleted file mode 100644 index 09b707a198..0000000000 --- a/crates/vim/test_data/test_insert_ctrl_y.json +++ /dev/null @@ -1,5 +0,0 @@ -{"Put":{"state":"hello\nˇ\nworld"}} -{"Key":"i"} -{"Key":"ctrl-y"} -{"Key":"ctrl-e"} -{"Get":{"state":"hello\nhoˇ\nworld","mode":"Insert"}} diff --git a/crates/vim/test_data/test_insert_empty_line.json b/crates/vim/test_data/test_insert_empty_line.json deleted file mode 100644 index 534db2c457..0000000000 --- a/crates/vim/test_data/test_insert_empty_line.json +++ /dev/null @@ -1,78 +0,0 @@ -{"Put":{"state":"ˇ"}} -{"Key":"["} -{"Key":"space"} -{"Get":{"state":"\nˇ","mode":"Normal"}} -{"Put":{"state":"The ˇquick"}} -{"Key":"["} -{"Key":"space"} -{"Get":{"state":"\nThe ˇquick","mode":"Normal"}} -{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}} -{"Key":"3"} -{"Key":"["} -{"Key":"space"} -{"Get":{"state":"\n\n\nThe qˇuick\nbrown fox\njumps over","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}} -{"Key":"3"} -{"Key":"["} -{"Key":"space"} -{"Get":{"state":"The quick\n\n\n\nbrown ˇfox\njumps over","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}} -{"Key":"3"} -{"Key":"["} -{"Key":"space"} -{"Get":{"state":"The quick\nbrown fox\n\n\n\njumps ˇover","mode":"Normal"}} -{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}} -{"Key":"["} -{"Key":"space"} -{"Get":{"state":"\nThe qˇuick\nbrown fox\njumps over","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}} -{"Key":"["} -{"Key":"space"} -{"Get":{"state":"The quick\n\nbrown ˇfox\njumps over","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}} -{"Key":"["} -{"Key":"space"} -{"Get":{"state":"The quick\nbrown fox\n\njumps ˇover","mode":"Normal"}} -{"Put":{"state":"The quick\nˇ\nbrown fox"}} -{"Key":"["} -{"Key":"space"} -{"Get":{"state":"The quick\n\nˇ\nbrown fox","mode":"Normal"}} -{"Put":{"state":"ˇ"}} -{"Key":"]"} -{"Key":"space"} -{"Get":{"state":"ˇ\n","mode":"Normal"}} -{"Put":{"state":"The ˇquick"}} -{"Key":"]"} -{"Key":"space"} -{"Get":{"state":"The ˇquick\n","mode":"Normal"}} -{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}} -{"Key":"3"} -{"Key":"]"} -{"Key":"space"} -{"Get":{"state":"The qˇuick\n\n\n\nbrown fox\njumps over","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}} -{"Key":"3"} -{"Key":"]"} -{"Key":"space"} -{"Get":{"state":"The quick\nbrown ˇfox\n\n\n\njumps over","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}} -{"Key":"3"} -{"Key":"]"} -{"Key":"space"} -{"Get":{"state":"The quick\nbrown fox\njumps ˇover\n\n\n","mode":"Normal"}} -{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}} -{"Key":"]"} -{"Key":"space"} -{"Get":{"state":"The qˇuick\n\nbrown fox\njumps over","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}} -{"Key":"]"} -{"Key":"space"} -{"Get":{"state":"The quick\nbrown ˇfox\n\njumps over","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}} -{"Key":"]"} -{"Key":"space"} -{"Get":{"state":"The quick\nbrown fox\njumps ˇover\n","mode":"Normal"}} -{"Put":{"state":"The quick\nˇ\nbrown fox"}} -{"Key":"]"} -{"Key":"space"} -{"Get":{"state":"The quick\nˇ\n\nbrown fox","mode":"Normal"}} diff --git a/crates/vim/test_data/test_insert_end_of_line.json b/crates/vim/test_data/test_insert_end_of_line.json deleted file mode 100644 index a37ad24d39..0000000000 --- a/crates/vim/test_data/test_insert_end_of_line.json +++ /dev/null @@ -1,9 +0,0 @@ -{"Put":{"state":"ˇ\nThe quick\nbrown fox "}} -{"Key":"shift-a"} -{"Get":{"state":"ˇ\nThe quick\nbrown fox ","mode":"Insert"}} -{"Put":{"state":"\nThe qˇuick\nbrown fox "}} -{"Key":"shift-a"} -{"Get":{"state":"\nThe quickˇ\nbrown fox ","mode":"Insert"}} -{"Put":{"state":"\nThe quick\nbrown ˇfox "}} -{"Key":"shift-a"} -{"Get":{"state":"\nThe quick\nbrown fox ˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_insert_first_non_whitespace.json b/crates/vim/test_data/test_insert_first_non_whitespace.json deleted file mode 100644 index 4d13cdb81c..0000000000 --- a/crates/vim/test_data/test_insert_first_non_whitespace.json +++ /dev/null @@ -1,15 +0,0 @@ -{"Put":{"state":"The qˇuick"}} -{"Key":"shift-i"} -{"Get":{"state":"ˇThe quick","mode":"Insert"}} -{"Put":{"state":" The qˇuick"}} -{"Key":"shift-i"} -{"Get":{"state":" ˇThe quick","mode":"Insert"}} -{"Put":{"state":"ˇ"}} -{"Key":"shift-i"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The qˇuick\nbrown fox"}} -{"Key":"shift-i"} -{"Get":{"state":"ˇThe quick\nbrown fox","mode":"Insert"}} -{"Put":{"state":"ˇ\nThe quick"}} -{"Key":"shift-i"} -{"Get":{"state":"ˇ\nThe quick","mode":"Insert"}} diff --git a/crates/vim/test_data/test_insert_line_above.json b/crates/vim/test_data/test_insert_line_above.json deleted file mode 100644 index ce5d0d7ac1..0000000000 --- a/crates/vim/test_data/test_insert_line_above.json +++ /dev/null @@ -1,18 +0,0 @@ -{"Put":{"state":"ˇ"}} -{"Key":"shift-o"} -{"Get":{"state":"ˇ\n","mode":"Insert"}} -{"Put":{"state":"The ˇquick"}} -{"Key":"shift-o"} -{"Get":{"state":"ˇ\nThe quick","mode":"Insert"}} -{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}} -{"Key":"shift-o"} -{"Get":{"state":"ˇ\nThe quick\nbrown fox\njumps over","mode":"Insert"}} -{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}} -{"Key":"shift-o"} -{"Get":{"state":"The quick\nˇ\nbrown fox\njumps over","mode":"Insert"}} -{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}} -{"Key":"shift-o"} -{"Get":{"state":"The quick\nbrown fox\nˇ\njumps over","mode":"Insert"}} -{"Put":{"state":"The quick\nˇ\nbrown fox"}} -{"Key":"shift-o"} -{"Get":{"state":"The quick\nˇ\n\nbrown fox","mode":"Insert"}} diff --git a/crates/vim/test_data/test_insert_with_counts.json b/crates/vim/test_data/test_insert_with_counts.json deleted file mode 100644 index 470888cf6e..0000000000 --- a/crates/vim/test_data/test_insert_with_counts.json +++ /dev/null @@ -1,36 +0,0 @@ -{"Put":{"state":"ˇhello\n"}} -{"Key":"5"} -{"Key":"i"} -{"Key":"-"} -{"Key":"escape"} -{"Get":{"state":"----ˇ-hello\n","mode":"Normal"}} -{"Put":{"state":"ˇhello\n"}} -{"Key":"5"} -{"Key":"a"} -{"Key":"-"} -{"Key":"escape"} -{"Get":{"state":"h----ˇ-ello\n","mode":"Normal"}} -{"Key":"4"} -{"Key":"shift-i"} -{"Key":"-"} -{"Key":"escape"} -{"Get":{"state":"---ˇ-h-----ello\n","mode":"Normal"}} -{"Key":"3"} -{"Key":"shift-a"} -{"Key":"-"} -{"Key":"escape"} -{"Get":{"state":"----h-----ello--ˇ-\n","mode":"Normal"}} -{"Put":{"state":"ˇhello\n"}} -{"Key":"3"} -{"Key":"o"} -{"Key":"o"} -{"Key":"i"} -{"Key":"escape"} -{"Get":{"state":"hello\noi\noi\noˇi\n","mode":"Normal"}} -{"Put":{"state":"ˇhello\n"}} -{"Key":"3"} -{"Key":"shift-o"} -{"Key":"o"} -{"Key":"i"} -{"Key":"escape"} -{"Get":{"state":"oi\noi\noˇi\nhello\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_insert_with_repeat.json b/crates/vim/test_data/test_insert_with_repeat.json deleted file mode 100644 index ac6637633c..0000000000 --- a/crates/vim/test_data/test_insert_with_repeat.json +++ /dev/null @@ -1,23 +0,0 @@ -{"Put":{"state":"ˇhello\n"}} -{"Key":"3"} -{"Key":"i"} -{"Key":"-"} -{"Key":"escape"} -{"Get":{"state":"--ˇ-hello\n","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"----ˇ--hello\n","mode":"Normal"}} -{"Key":"2"} -{"Key":"."} -{"Get":{"state":"-----ˇ---hello\n","mode":"Normal"}} -{"Put":{"state":"ˇhello\n"}} -{"Key":"2"} -{"Key":"o"} -{"Key":"k"} -{"Key":"k"} -{"Key":"escape"} -{"Get":{"state":"hello\nkk\nkˇk\n","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"hello\nkk\nkk\nkk\nkˇk\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"."} -{"Get":{"state":"hello\nkk\nkk\nkk\nkk\nkˇk\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_j.json b/crates/vim/test_data/test_j.json deleted file mode 100644 index 703f69d22c..0000000000 --- a/crates/vim/test_data/test_j.json +++ /dev/null @@ -1,15 +0,0 @@ -{"Put":{"state":"aaˇaa\n😃😃"}} -{"Key":"j"} -{"Get":{"state":"aaaa\n😃ˇ😃","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown\nfox jumps"}} -{"Key":"j"} -{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}} -{"Put":{"state":"The qˇuick brown\nfox jumps"}} -{"Key":"j"} -{"Get":{"state":"The quick brown\nfox jˇumps","mode":"Normal"}} -{"Put":{"state":"The quick broˇwn\nfox jumps"}} -{"Key":"j"} -{"Get":{"state":"The quick brown\nfox jumpˇs","mode":"Normal"}} -{"Put":{"state":"The quick brown\nˇfox jumps"}} -{"Key":"j"} -{"Get":{"state":"The quick brown\nˇfox jumps","mode":"Normal"}} diff --git a/crates/vim/test_data/test_jk.json b/crates/vim/test_data/test_jk.json deleted file mode 100644 index bc1a6a4ba5..0000000000 --- a/crates/vim/test_data/test_jk.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Exec":{"command":"imap jk "}} -{"Put":{"state":"ˇhello"}} -{"Key":"i"} -{"Key":"j"} -{"Key":"o"} -{"Key":"j"} -{"Key":"k"} -{"Get":{"state":"jˇohello","mode":"Normal"}} diff --git a/crates/vim/test_data/test_jk_max_count.json b/crates/vim/test_data/test_jk_max_count.json deleted file mode 100644 index 83eab46a18..0000000000 --- a/crates/vim/test_data/test_jk_max_count.json +++ /dev/null @@ -1,47 +0,0 @@ -{"Put":{"state":"1\nˇ2\n3"}} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"9"} -{"Key":"j"} -{"Get":{"state":"1\n2\nˇ3","mode":"Normal"}} -{"Key":""} -{"Key":"1"} -{"Key":"8"} -{"Key":"4"} -{"Key":"4"} -{"Key":"6"} -{"Key":"7"} -{"Key":"4"} -{"Key":"4"} -{"Key":"0"} -{"Key":"7"} -{"Key":"3"} -{"Key":"7"} -{"Key":"0"} -{"Key":"9"} -{"Key":"5"} -{"Key":"5"} -{"Key":"1"} -{"Key":"6"} -{"Key":"1"} -{"Key":"5"} -{"Key":""} -{"Key":"k"} -{"Get":{"state":"ˇ1\n2\n3","mode":"Normal"}} diff --git a/crates/vim/test_data/test_join_lines.json b/crates/vim/test_data/test_join_lines.json deleted file mode 100644 index 55aa8b1dcb..0000000000 --- a/crates/vim/test_data/test_join_lines.json +++ /dev/null @@ -1,29 +0,0 @@ -{"Put":{"state":"ˇone\ntwo\nthree\nfour\nfive\nsix\n"}} -{"Key":"shift-j"} -{"Get":{"state":"oneˇ two\nthree\nfour\nfive\nsix\n","mode":"Normal"}} -{"Key":"3"} -{"Key":"shift-j"} -{"Get":{"state":"one two threeˇ four\nfive\nsix\n","mode":"Normal"}} -{"Put":{"state":"ˇone\ntwo\nthree\nfour\nfive\nsix\n"}} -{"Key":"j"} -{"Key":"v"} -{"Key":"3"} -{"Key":"j"} -{"Key":"shift-j"} -{"Get":{"state":"one\ntwo three fourˇ five\nsix\n","mode":"Normal"}} -{"Put":{"state":"ˇone\ntwo\nthree\nfour\nfive\nsix\n"}} -{"Key":"g"} -{"Key":"shift-j"} -{"Get":{"state":"oneˇtwo\nthree\nfour\nfive\nsix\n","mode":"Normal"}} -{"Key":"3"} -{"Key":"g"} -{"Key":"shift-j"} -{"Get":{"state":"onetwothreeˇfour\nfive\nsix\n","mode":"Normal"}} -{"Put":{"state":"ˇone\ntwo\nthree\nfour\nfive\nsix\n"}} -{"Key":"j"} -{"Key":"v"} -{"Key":"3"} -{"Key":"j"} -{"Key":"g"} -{"Key":"shift-j"} -{"Get":{"state":"one\ntwothreefourˇfive\nsix\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_jump_list.json b/crates/vim/test_data/test_jump_list.json deleted file mode 100644 index 833d1adadb..0000000000 --- a/crates/vim/test_data/test_jump_list.json +++ /dev/null @@ -1,14 +0,0 @@ -{"Put":{"state":"ˇfn a() { }\n\n\n\n\n\nfn b() { }\n\n\n\n\n\nfn b() { }"}} -{"Key":"3"} -{"Key":"}"} -{"Get":{"state":"fn a() { }\n\n\n\n\n\nfn b() { }\n\n\n\n\n\nfn b() { ˇ}","mode":"Normal"}} -{"Key":"ctrl-o"} -{"Get":{"state":"ˇfn a() { }\n\n\n\n\n\nfn b() { }\n\n\n\n\n\nfn b() { }","mode":"Normal"}} -{"Key":"ctrl-i"} -{"Get":{"state":"fn a() { }\n\n\n\n\n\nfn b() { }\n\n\n\n\n\nfn b() { ˇ}","mode":"Normal"}} -{"Key":"1"} -{"Key":"1"} -{"Key":"k"} -{"Get":{"state":"fn a() { }\nˇ\n\n\n\n\nfn b() { }\n\n\n\n\n\nfn b() { }","mode":"Normal"}} -{"Key":"ctrl-o"} -{"Get":{"state":"ˇfn a() { }\n\n\n\n\n\nfn b() { }\n\n\n\n\n\nfn b() { }","mode":"Normal"}} diff --git a/crates/vim/test_data/test_jump_to_end.json b/crates/vim/test_data/test_jump_to_end.json deleted file mode 100644 index fe9a948d43..0000000000 --- a/crates/vim/test_data/test_jump_to_end.json +++ /dev/null @@ -1,14 +0,0 @@ -{"Put":{"state":"The ˇquick\n\nbrown fox jumps\nover the lazy dog"}} -{"Key":"shift-g"} -{"Get":{"state":"The quick\n\nbrown fox jumps\noverˇ the lazy dog","mode":"Normal"}} -{"Key":"shift-g"} -{"Get":{"state":"The quick\n\nbrown fox jumps\noverˇ the lazy dog","mode":"Normal"}} -{"Put":{"state":"The quick\n\nbrown fox jumps\nover the lazy doˇg"}} -{"Key":"shift-g"} -{"Get":{"state":"The quick\n\nbrown fox jumps\nover the lazy doˇg","mode":"Normal"}} -{"Put":{"state":"The quiˇck\n\nbrown"}} -{"Key":"shift-g"} -{"Get":{"state":"The quick\n\nbrowˇn","mode":"Normal"}} -{"Put":{"state":"The quiˇck\n\n"}} -{"Key":"shift-g"} -{"Get":{"state":"The quick\n\nˇ","mode":"Normal"}} diff --git a/crates/vim/test_data/test_jump_to_first_non_whitespace.json b/crates/vim/test_data/test_jump_to_first_non_whitespace.json deleted file mode 100644 index c992662195..0000000000 --- a/crates/vim/test_data/test_jump_to_first_non_whitespace.json +++ /dev/null @@ -1,18 +0,0 @@ -{"Put":{"state":"The qˇuick"}} -{"Key":"^"} -{"Get":{"state":"ˇThe quick","mode":"Normal"}} -{"Put":{"state":" The qˇuick"}} -{"Key":"^"} -{"Get":{"state":" ˇThe quick","mode":"Normal"}} -{"Put":{"state":"ˇ"}} -{"Key":"^"} -{"Get":{"state":"ˇ","mode":"Normal"}} -{"Put":{"state":"The qˇuick\nbrown fox"}} -{"Key":"^"} -{"Get":{"state":"ˇThe quick\nbrown fox","mode":"Normal"}} -{"Put":{"state":"ˇ\nThe quick"}} -{"Key":"^"} -{"Get":{"state":"ˇ\nThe quick","mode":"Normal"}} -{"Put":{"state":" ˇ \nThe quick"}} -{"Key":"^"} -{"Get":{"state":" ˇ \nThe quick","mode":"Normal"}} diff --git a/crates/vim/test_data/test_jump_to_line_boundaries.json b/crates/vim/test_data/test_jump_to_line_boundaries.json deleted file mode 100644 index 6d4a76ffd6..0000000000 --- a/crates/vim/test_data/test_jump_to_line_boundaries.json +++ /dev/null @@ -1,28 +0,0 @@ -{"Put":{"state":"ˇThe quick\nbrown"}} -{"Key":"$"} -{"Get":{"state":"The quicˇk\nbrown","mode":"Normal"}} -{"Put":{"state":"The qˇuick\nbrown"}} -{"Key":"$"} -{"Get":{"state":"The quicˇk\nbrown","mode":"Normal"}} -{"Key":"$"} -{"Get":{"state":"The quicˇk\nbrown","mode":"Normal"}} -{"Put":{"state":"The quick\nˇbrown"}} -{"Key":"$"} -{"Get":{"state":"The quick\nbrowˇn","mode":"Normal"}} -{"Key":"$"} -{"Get":{"state":"The quick\nbrowˇn","mode":"Normal"}} -{"Put":{"state":"ˇThe quick\nbrown"}} -{"Key":"0"} -{"Get":{"state":"ˇThe quick\nbrown","mode":"Normal"}} -{"Put":{"state":"The qˇuick\nbrown"}} -{"Key":"0"} -{"Get":{"state":"ˇThe quick\nbrown","mode":"Normal"}} -{"Put":{"state":"The quicˇk\nbrown"}} -{"Key":"0"} -{"Get":{"state":"ˇThe quick\nbrown","mode":"Normal"}} -{"Put":{"state":"The quick\nˇbrown"}} -{"Key":"0"} -{"Get":{"state":"The quick\nˇbrown","mode":"Normal"}} -{"Put":{"state":"The quick\nbrowˇn"}} -{"Key":"0"} -{"Get":{"state":"The quick\nˇbrown","mode":"Normal"}} diff --git a/crates/vim/test_data/test_k.json b/crates/vim/test_data/test_k.json deleted file mode 100644 index f064fa9fd9..0000000000 --- a/crates/vim/test_data/test_k.json +++ /dev/null @@ -1,15 +0,0 @@ -{"Put":{"state":"ˇThe quick\nbrown fox jumps"}} -{"Key":"k"} -{"Get":{"state":"ˇThe quick\nbrown fox jumps","mode":"Normal"}} -{"Put":{"state":"The qˇuick\nbrown fox jumps"}} -{"Key":"k"} -{"Get":{"state":"The qˇuick\nbrown fox jumps","mode":"Normal"}} -{"Put":{"state":"The quick\nˇbrown fox jumps"}} -{"Key":"k"} -{"Get":{"state":"ˇThe quick\nbrown fox jumps","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown fˇox jumps"}} -{"Key":"k"} -{"Get":{"state":"The quiˇck\nbrown fox jumps","mode":"Normal"}} -{"Put":{"state":"The quick\nbrown fox jumˇps"}} -{"Key":"k"} -{"Get":{"state":"The quicˇk\nbrown fox jumps","mode":"Normal"}} diff --git a/crates/vim/test_data/test_l.json b/crates/vim/test_data/test_l.json deleted file mode 100644 index b0db891bce..0000000000 --- a/crates/vim/test_data/test_l.json +++ /dev/null @@ -1,15 +0,0 @@ -{"Put":{"state":"ˇThe quick\nbrown"}} -{"Key":"l"} -{"Get":{"state":"Tˇhe quick\nbrown","mode":"Normal"}} -{"Put":{"state":"The qˇuick\nbrown"}} -{"Key":"l"} -{"Get":{"state":"The quˇick\nbrown","mode":"Normal"}} -{"Put":{"state":"The quicˇk\nbrown"}} -{"Key":"l"} -{"Get":{"state":"The quicˇk\nbrown","mode":"Normal"}} -{"Put":{"state":"The quick\nˇbrown"}} -{"Key":"l"} -{"Get":{"state":"The quick\nbˇrown","mode":"Normal"}} -{"Put":{"state":"The quick\nbrowˇn"}} -{"Key":"l"} -{"Get":{"state":"The quick\nbrowˇn","mode":"Normal"}} diff --git a/crates/vim/test_data/test_lowercase_marks.json b/crates/vim/test_data/test_lowercase_marks.json deleted file mode 100644 index ff02c58728..0000000000 --- a/crates/vim/test_data/test_lowercase_marks.json +++ /dev/null @@ -1,15 +0,0 @@ -{"Put":{"state":"line one\nline ˇtwo\nline three"}} -{"Key":"m"} -{"Key":"a"} -{"Key":"l"} -{"Key":"'"} -{"Key":"a"} -{"Get":{"state":"line one\nˇline two\nline three","mode":"Normal"}} -{"Key":"`"} -{"Key":"a"} -{"Get":{"state":"line one\nline ˇtwo\nline three","mode":"Normal"}} -{"Key":"^"} -{"Key":"d"} -{"Key":"`"} -{"Key":"a"} -{"Get":{"state":"line one\nˇtwo\nline three","mode":"Normal"}} diff --git a/crates/vim/test_data/test_lt_gt_marks.json b/crates/vim/test_data/test_lt_gt_marks.json deleted file mode 100644 index 142ceb9b95..0000000000 --- a/crates/vim/test_data/test_lt_gt_marks.json +++ /dev/null @@ -1,29 +0,0 @@ -{"Put":{"state":"Line one\nLine two\nLine ˇthree\nLine four\nLine five\n"}} -{"Key":"v"} -{"Key":"j"} -{"Key":"escape"} -{"Key":"k"} -{"Key":"k"} -{"Key":"'"} -{"Key":"<"} -{"Get":{"state":"Line one\nLine two\nˇLine three\nLine four\nLine five\n","mode":"Normal"}} -{"Key":"`"} -{"Key":"<"} -{"Get":{"state":"Line one\nLine two\nLine ˇthree\nLine four\nLine five\n","mode":"Normal"}} -{"Key":"'"} -{"Key":">"} -{"Get":{"state":"Line one\nLine two\nLine three\nˇLine four\nLine five\n","mode":"Normal"}} -{"Key":"`"} -{"Key":">"} -{"Get":{"state":"Line one\nLine two\nLine three\nLine ˇfour\nLine five\n","mode":"Normal"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Key":"o"} -{"Key":"escape"} -{"Key":"`"} -{"Key":">"} -{"Get":{"state":"Line one\nLine two\nLine three\nLine fouˇr\nLine five\n","mode":"Normal"}} -{"Key":"`"} -{"Key":"<"} -{"Get":{"state":"Line one\nLine two\nLine three\nLine ˇfour\nLine five\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_marks.json b/crates/vim/test_data/test_marks.json deleted file mode 100644 index ff02c58728..0000000000 --- a/crates/vim/test_data/test_marks.json +++ /dev/null @@ -1,15 +0,0 @@ -{"Put":{"state":"line one\nline ˇtwo\nline three"}} -{"Key":"m"} -{"Key":"a"} -{"Key":"l"} -{"Key":"'"} -{"Key":"a"} -{"Get":{"state":"line one\nˇline two\nline three","mode":"Normal"}} -{"Key":"`"} -{"Key":"a"} -{"Get":{"state":"line one\nline ˇtwo\nline three","mode":"Normal"}} -{"Key":"^"} -{"Key":"d"} -{"Key":"`"} -{"Key":"a"} -{"Get":{"state":"line one\nˇtwo\nline three","mode":"Normal"}} diff --git a/crates/vim/test_data/test_matching.json b/crates/vim/test_data/test_matching.json deleted file mode 100644 index 5c8d7529b9..0000000000 --- a/crates/vim/test_data/test_matching.json +++ /dev/null @@ -1,17 +0,0 @@ -{"Put":{"state":"func ˇ(a string) {\n do(something(with.and_arrays[0, 2]))\n}"}} -{"Key":"%"} -{"Get":{"state":"func (a stringˇ) {\n do(something(with.and_arrays[0, 2]))\n}","mode":"Normal"}} -{"Put":{"state":"func (a string) ˇ{\ndo(something(with.and_arrays[0, 2]))\n}"}} -{"Key":"%"} -{"Get":{"state":"func (a string) {\ndo(something(with.and_arrays[0, 2]))\nˇ}","mode":"Normal"}} -{"Put":{"state":"ˇ{()}"}} -{"Key":"%"} -{"Get":{"state":"{()ˇ}","mode":"Normal"}} -{"Key":"%"} -{"Get":{"state":"ˇ{()}","mode":"Normal"}} -{"Put":{"state":"{\n ˇ{()}\n}"}} -{"Key":"%"} -{"Get":{"state":"{\n {()ˇ}\n}","mode":"Normal"}} -{"Put":{"state":"func ˇboop() {\n}"}} -{"Key":"%"} -{"Get":{"state":"func boop(ˇ) {\n}","mode":"Normal"}} diff --git a/crates/vim/test_data/test_matching_braces_in_tag.json b/crates/vim/test_data/test_matching_braces_in_tag.json deleted file mode 100644 index 44201548a7..0000000000 --- a/crates/vim/test_data/test_matching_braces_in_tag.json +++ /dev/null @@ -1,3 +0,0 @@ -{"Put":{"state":"function f() {\n return (\n
\n

test

\n
\n );\n}"}} -{"Key":"%"} -{"Get":{"state":"function f() {\n return (\n
\n

test

\n
\n );\n}","mode":"Normal"}} diff --git a/crates/vim/test_data/test_matching_nested_brackets.json b/crates/vim/test_data/test_matching_nested_brackets.json deleted file mode 100644 index d90b38416e..0000000000 --- a/crates/vim/test_data/test_matching_nested_brackets.json +++ /dev/null @@ -1,5 +0,0 @@ -{"Put":{"state":""}} -{"Key":"%"} -{"Get":{"state":"","mode":"Normal"}} -{"Key":"%"} -{"Get":{"state":"","mode":"Normal"}} diff --git a/crates/vim/test_data/test_matching_tags.json b/crates/vim/test_data/test_matching_tags.json deleted file mode 100644 index b401033a94..0000000000 --- a/crates/vim/test_data/test_matching_tags.json +++ /dev/null @@ -1,20 +0,0 @@ -{"Exec":{"command":"set filetype=html"}} -{"Put":{"state":""}} -{"Key":"%"} -{"Get":{"state":"<ˇ/body>","mode":"Normal"}} -{"Key":"%"} -{"Get":{"state":"<ˇbody>","mode":"Normal"}} -{"Put":{"state":""}} -{"Key":"%"} -{"Get":{"state":"","mode":"Normal"}} -{"Put":{"state":"
\n
\n"}} -{"Key":"%"} -{"Get":{"state":"
\n<ˇ/div>\n","mode":"Normal"}} -{"Put":{"state":"\n \n"}} -{"Key":"%"} -{"Get":{"state":"\n ˇ\n","mode":"Normal"}} -{"Put":{"state":"\n \n \n"}} -{"Key":"%"} -{"Get":{"state":"\n \n <ˇ/body>\n","mode":"Normal"}} -{"Key":"%"} -{"Get":{"state":"\n <ˇbody>\n \n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_minibrackets_trailing_space.json b/crates/vim/test_data/test_minibrackets_trailing_space.json deleted file mode 100644 index ed3f47df6c..0000000000 --- a/crates/vim/test_data/test_minibrackets_trailing_space.json +++ /dev/null @@ -1,11 +0,0 @@ -{"Put":{"state":"(trailingˇ whitespace )"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"b"} -{"Get":{"state":"(«trailing whitespace ˇ»)","mode":"Visual"}} -{"Key":"escape"} -{"Key":"y"} -{"Key":"i"} -{"Key":"b"} -{"Get":{"state":"(ˇtrailing whitespace )","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"trailing whitespace "}} diff --git a/crates/vim/test_data/test_named_registers.json b/crates/vim/test_data/test_named_registers.json deleted file mode 100644 index 789f17f93f..0000000000 --- a/crates/vim/test_data/test_named_registers.json +++ /dev/null @@ -1,28 +0,0 @@ -{"Put":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog"}} -{"Key":"\""} -{"Key":"a"} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown\nfox ˇover\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"a","value":"jumps "}} -{"Key":"\""} -{"Key":"shift-a"} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown\nfoxˇ \nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"a","value":"jumps over"}} -{"Get":{"state":"The quick brown\nfoxˇ \nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"jumps over"}} -{"Key":"\""} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown\nfox jumps oveˇr\nthe lazy dog","mode":"Normal"}} -{"Key":"\""} -{"Key":"a"} -{"Key":"d"} -{"Key":"a"} -{"Key":"w"} -{"Get":{"state":"The quick brown\nfox jumpˇs\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"a","value":" over"}} diff --git a/crates/vim/test_data/test_neovim.json b/crates/vim/test_data/test_neovim.json deleted file mode 100644 index 3fb5d6b3f4..0000000000 --- a/crates/vim/test_data/test_neovim.json +++ /dev/null @@ -1,16 +0,0 @@ -{"Key":"i"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Key":"shift-t"} -{"Key":"e"} -{"Key":"s"} -{"Key":"t"} -{"Key":"space"} -{"Key":"t"} -{"Key":"e"} -{"Key":"s"} -{"Key":"t"} -{"Key":"escape"} -{"Key":"0"} -{"Key":"d"} -{"Key":"w"} -{"Get":{"state":"ˇtest","mode":"Normal"}} diff --git a/crates/vim/test_data/test_next_line_start.json b/crates/vim/test_data/test_next_line_start.json deleted file mode 100644 index 90ed4a4f03..0000000000 --- a/crates/vim/test_data/test_next_line_start.json +++ /dev/null @@ -1,3 +0,0 @@ -{"Put":{"state":"ˇone\n two\nthree"}} -{"Key":"enter"} -{"Get":{"state":"one\n ˇtwo\nthree","mode":"Normal"}} diff --git a/crates/vim/test_data/test_next_word_end_newline_last_char.json b/crates/vim/test_data/test_next_word_end_newline_last_char.json deleted file mode 100644 index 9dac2979f5..0000000000 --- a/crates/vim/test_data/test_next_word_end_newline_last_char.json +++ /dev/null @@ -1,3 +0,0 @@ -{"Put":{"state":"something(ˇfoo)"}} -{"Key":"}"} -{"Get":{"state":"something(fooˇ)","mode":"Normal"}} diff --git a/crates/vim/test_data/test_normal_command.json b/crates/vim/test_data/test_normal_command.json deleted file mode 100644 index efd1d532c4..0000000000 --- a/crates/vim/test_data/test_normal_command.json +++ /dev/null @@ -1,64 +0,0 @@ -{"Put":{"state":"The quick\nbrown« fox\njumpsˇ» over\nthe lazy dog\n"}} -{"Key":":"} -{"Key":"n"} -{"Key":"o"} -{"Key":"r"} -{"Key":"m"} -{"Key":"space"} -{"Key":"w"} -{"Key":"C"} -{"Key":"w"} -{"Key":"o"} -{"Key":"r"} -{"Key":"d"} -{"Key":"enter"} -{"Get":{"state":"The quick\nbrown word\njumps worˇd\nthe lazy dog\n","mode":"Normal"}} -{"Key":":"} -{"Key":"n"} -{"Key":"o"} -{"Key":"r"} -{"Key":"m"} -{"Key":"space"} -{"Key":"_"} -{"Key":"w"} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Key":"t"} -{"Key":"e"} -{"Key":"s"} -{"Key":"t"} -{"Key":"enter"} -{"Get":{"state":"The quick\nbrown word\njumps tesˇt\nthe lazy dog\n","mode":"Normal"}} -{"Key":"_"} -{"Key":"l"} -{"Key":"v"} -{"Key":"l"} -{"Key":":"} -{"Key":"n"} -{"Key":"o"} -{"Key":"r"} -{"Key":"m"} -{"Key":"space"} -{"Key":"s"} -{"Key":"l"} -{"Key":"a"} -{"Key":"enter"} -{"Get":{"state":"The quick\nbrown word\nlˇaumps test\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"ˇThe quick\nbrown fox\njumps over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"i"} -{"Key":"w"} -{"Key":"M"} -{"Key":"y"} -{"Key":"escape"} -{"Get":{"state":"Mˇy quick\nbrown fox\njumps over\nthe lazy dog\n","mode":"Normal"}} -{"Key":":"} -{"Key":"n"} -{"Key":"o"} -{"Key":"r"} -{"Key":"m"} -{"Key":"space"} -{"Key":"u"} -{"Key":"enter"} -{"Get":{"state":"ˇThe quick\nbrown fox\njumps over\nthe lazy dog\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_numbered_registers.json b/crates/vim/test_data/test_numbered_registers.json deleted file mode 100644 index 191a58e01f..0000000000 --- a/crates/vim/test_data/test_numbered_registers.json +++ /dev/null @@ -1,45 +0,0 @@ -{"Put":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog"}} -{"Key":"y"} -{"Key":"y"} -{"Key":"\""} -{"Key":"0"} -{"Key":"p"} -{"Get":{"state":"The quick brown\nfox jumps over\nˇfox jumps over\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"0","value":"fox jumps over\n"}} -{"Get":{"state":"The quick brown\nfox jumps over\nˇfox jumps over\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"fox jumps over\n"}} -{"Get":{"state":"The quick brown\nfox jumps over\nˇfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"k"} -{"Key":"k"} -{"Key":"d"} -{"Key":"d"} -{"Get":{"state":"ˇfox jumps over\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"0","value":"fox jumps over\n"}} -{"Get":{"state":"ˇfox jumps over\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"1","value":"The quick brown\n"}} -{"Get":{"state":"ˇfox jumps over\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"The quick brown\n"}} -{"Key":"d"} -{"Key":"d"} -{"Key":"shift-g"} -{"Key":"d"} -{"Key":"d"} -{"Get":{"state":"ˇfox jumps over","mode":"Normal"}} -{"ReadRegister":{"name":"0","value":"fox jumps over\n"}} -{"Get":{"state":"ˇfox jumps over","mode":"Normal"}} -{"ReadRegister":{"name":"3","value":"The quick brown\n"}} -{"Get":{"state":"ˇfox jumps over","mode":"Normal"}} -{"ReadRegister":{"name":"2","value":"fox jumps over\n"}} -{"Get":{"state":"ˇfox jumps over","mode":"Normal"}} -{"ReadRegister":{"name":"1","value":"the lazy dog\n"}} -{"Get":{"state":"ˇfox jumps over","mode":"Normal"}} -{"Key":"d"} -{"Key":"d"} -{"Key":"\""} -{"Key":"3"} -{"Key":"p"} -{"Key":"p"} -{"Key":"\""} -{"Key":"1"} -{"Key":"p"} -{"Put":{"state":"The quick brown\nfox jumps over\nˇthe lazy dog"}} diff --git a/crates/vim/test_data/test_o.json b/crates/vim/test_data/test_o.json deleted file mode 100644 index 015890ac36..0000000000 --- a/crates/vim/test_data/test_o.json +++ /dev/null @@ -1,18 +0,0 @@ -{"Put":{"state":"ˇ"}} -{"Key":"o"} -{"Get":{"state":"\nˇ","mode":"Insert"}} -{"Put":{"state":"The ˇquick"}} -{"Key":"o"} -{"Get":{"state":"The quick\nˇ","mode":"Insert"}} -{"Put":{"state":"The qˇuick\nbrown fox\njumps over"}} -{"Key":"o"} -{"Get":{"state":"The quick\nˇ\nbrown fox\njumps over","mode":"Insert"}} -{"Put":{"state":"The quick\nbrown ˇfox\njumps over"}} -{"Key":"o"} -{"Get":{"state":"The quick\nbrown fox\nˇ\njumps over","mode":"Insert"}} -{"Put":{"state":"The quick\nbrown fox\njumps ˇover"}} -{"Key":"o"} -{"Get":{"state":"The quick\nbrown fox\njumps over\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick\nˇ\nbrown fox"}} -{"Key":"o"} -{"Get":{"state":"The quick\n\nˇ\nbrown fox","mode":"Insert"}} diff --git a/crates/vim/test_data/test_o_comment.json b/crates/vim/test_data/test_o_comment.json deleted file mode 100644 index b0b84da0e6..0000000000 --- a/crates/vim/test_data/test_o_comment.json +++ /dev/null @@ -1,8 +0,0 @@ -{"SetOption":{"value":"filetype=rust"}} -{"Put":{"state":"// helloˇ\n"}} -{"Key":"o"} -{"Get":{"state":"// hello\n// ˇ\n","mode":"Insert"}} -{"Key":"x"} -{"Key":"escape"} -{"Key":"shift-o"} -{"Get":{"state":"// hello\n// ˇ\n// x\n","mode":"Insert"}} diff --git a/crates/vim/test_data/test_offsets.json b/crates/vim/test_data/test_offsets.json deleted file mode 100644 index 065a234817..0000000000 --- a/crates/vim/test_data/test_offsets.json +++ /dev/null @@ -1,21 +0,0 @@ -{"Put":{"state":"ˇ1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n"}} -{"Key":":"} -{"Key":"+"} -{"Key":"enter"} -{"Get":{"state":"1\nˇ2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n","mode":"Normal"}} -{"Key":":"} -{"Key":"1"} -{"Key":"0"} -{"Key":"-"} -{"Key":"enter"} -{"Get":{"state":"1\n2\n3\n4\n5\n6\n7\n8\nˇ9\n10\n11\n","mode":"Normal"}} -{"Key":":"} -{"Key":"."} -{"Key":"-"} -{"Key":"2"} -{"Key":"enter"} -{"Get":{"state":"1\n2\n3\n4\n5\n6\nˇ7\n8\n9\n10\n11\n","mode":"Normal"}} -{"Key":":"} -{"Key":"%"} -{"Key":"enter"} -{"Get":{"state":"1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\nˇ","mode":"Normal"}} diff --git a/crates/vim/test_data/test_p_g_v_y.json b/crates/vim/test_data/test_p_g_v_y.json deleted file mode 100644 index a275c333a3..0000000000 --- a/crates/vim/test_data/test_p_g_v_y.json +++ /dev/null @@ -1,11 +0,0 @@ -{"Put":{"state":"The\nquicˇk\nbrown\nfox"}} -{"Key":"y"} -{"Key":"y"} -{"Key":"j"} -{"Key":"shift-v"} -{"Key":"p"} -{"Key":"g"} -{"Key":"v"} -{"Key":"y"} -{"Get":{"state":"The\nquick\nˇquick\nfox","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"quick\n"}} diff --git a/crates/vim/test_data/test_paragraph_multi_delete.json b/crates/vim/test_data/test_paragraph_multi_delete.json deleted file mode 100644 index f706827a24..0000000000 --- a/crates/vim/test_data/test_paragraph_multi_delete.json +++ /dev/null @@ -1,18 +0,0 @@ -{"Put":{"state":"Emacs is\nˇa great\n\noperating system\n\nall it lacks\nis a\n\ndecent text editor\n"}} -{"Key":"2"} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇall it lacks\nis a\n\ndecent text editor\n","mode":"Normal"}} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇdecent text editor\n","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"all it lacks\nis a\n\n"}} -{"Key":"2"} -{"Key":"u"} -{"Key":"4"} -{"Key":"d"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇ","mode":"Normal"}} diff --git a/crates/vim/test_data/test_paragraph_object_with_landing_positions_not_at_beginning_of_line.json b/crates/vim/test_data/test_paragraph_object_with_landing_positions_not_at_beginning_of_line.json deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/crates/vim/test_data/test_paragraphs_dont_wrap.json b/crates/vim/test_data/test_paragraphs_dont_wrap.json deleted file mode 100644 index 9e729651be..0000000000 --- a/crates/vim/test_data/test_paragraphs_dont_wrap.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Put":{"state":"one\nˇ\ntwo"}} -{"Key":"}"} -{"Key":"}"} -{"Get":{"state":"one\n\ntwˇo","mode":"Normal"}} -{"Key":"{"} -{"Key":"{"} -{"Key":"{"} -{"Get":{"state":"ˇone\n\ntwo","mode":"Normal"}} diff --git a/crates/vim/test_data/test_paste.json b/crates/vim/test_data/test_paste.json deleted file mode 100644 index 21b7e9346a..0000000000 --- a/crates/vim/test_data/test_paste.json +++ /dev/null @@ -1,34 +0,0 @@ -{"Put":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"y"} -{"Get":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"jumps o"}} -{"Put":{"state":"The quick brown\nfox jumps oveˇr\nthe lazy dog"}} -{"Key":"p"} -{"Get":{"state":"The quick brown\nfox jumps overjumps ˇo\nthe lazy dog","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps oveˇr\nthe lazy dog"}} -{"Key":"shift-p"} -{"Get":{"state":"The quick brown\nfox jumps ovejumps ˇor\nthe lazy dog","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}} -{"Key":"d"} -{"Key":"d"} -{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"fox jumps over\n"}} -{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}} -{"Key":"p"} -{"Get":{"state":"The quick brown\nthe lazy dog\nˇfox jumps over","mode":"Normal"}} -{"Key":"k"} -{"Key":"shift-p"} -{"Get":{"state":"The quick brown\nˇfox jumps over\nthe lazy dog\nfox jumps over","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}} -{"Key":"v"} -{"Key":"j"} -{"Key":"y"} -{"Get":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"over\nthe lazy do"}} -{"Key":"p"} -{"Get":{"state":"The quick brown\nfox jumps oˇover\nthe lazy dover\nthe lazy dog","mode":"Normal"}} -{"Key":"u"} -{"Key":"shift-p"} -{"Get":{"state":"The quick brown\nfox jumps ˇover\nthe lazy doover\nthe lazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_paste_count.json b/crates/vim/test_data/test_paste_count.json deleted file mode 100644 index 7dd4dc4d91..0000000000 --- a/crates/vim/test_data/test_paste_count.json +++ /dev/null @@ -1,13 +0,0 @@ -{"Put":{"state":"onˇe\ntwo\nthree\n"}} -{"Key":"y"} -{"Key":"y"} -{"Key":"3"} -{"Key":"p"} -{"Get":{"state":"one\nˇone\none\none\ntwo\nthree\n","mode":"Normal"}} -{"Put":{"state":"one\nˇtwo\nthree\n"}} -{"Key":"y"} -{"Key":"$"} -{"Key":"$"} -{"Key":"3"} -{"Key":"p"} -{"Get":{"state":"one\ntwotwotwotwˇo\nthree\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_paste_visual.json b/crates/vim/test_data/test_paste_visual.json deleted file mode 100644 index fb10f94782..0000000000 --- a/crates/vim/test_data/test_paste_visual.json +++ /dev/null @@ -1,51 +0,0 @@ -{"Put":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Key":"y"} -{"Get":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"w"} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Key":"p"} -{"Get":{"state":"The quick brown\nfox jumps jumpˇs\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"over"}} -{"Key":"up"} -{"Key":"shift-v"} -{"Key":"shift-p"} -{"Get":{"state":"ˇover\nfox jumps jumps\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"over"}} -{"Key":"ctrl-v"} -{"Key":"down"} -{"Key":"down"} -{"Key":"p"} -{"Get":{"state":"oveˇrver\noverox jumps jumps\noverhe lazy dog","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"d"} -{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Key":"p"} -{"Get":{"state":"The quick brown\nthe \nˇfox jumps over\n dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"lazy"}} -{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"d"} -{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"fox jumps over\n"}} -{"Key":"k"} -{"Key":"shift-v"} -{"Key":"p"} -{"Get":{"state":"ˇfox jumps over\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"The quick brown\n"}} -{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"y"} -{"Key":"y"} -{"Key":"shift-v"} -{"Key":"j"} -{"Key":"$"} -{"Key":"p"} -{"Get":{"state":"ˇThe quick brown\nthe lazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_paste_visual_block.json b/crates/vim/test_data/test_paste_visual_block.json deleted file mode 100644 index 5560b7529d..0000000000 --- a/crates/vim/test_data/test_paste_visual_block.json +++ /dev/null @@ -1,33 +0,0 @@ -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"ctrl-v"} -{"Key":"2"} -{"Key":"j"} -{"Key":"y"} -{"Get":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"q\nj\nl"}} -{"Key":"p"} -{"Get":{"state":"The qˇquick brown\nfox jjumps over\nthe llazy dog","mode":"Normal"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Key":"shift-p"} -{"Get":{"state":"The ˇq brown\nfox jjjumps over\nthe lllazy dog","mode":"Normal"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Key":"shift-p"} -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"ctrl-v"} -{"Key":"j"} -{"Key":"y"} -{"Get":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"q\nj"}} -{"Key":"l"} -{"Key":"ctrl-v"} -{"Key":"2"} -{"Key":"j"} -{"Key":"shift-p"} -{"Get":{"state":"The qˇqick brown\nfox jjmps over\nthe lzy dog","mode":"Normal"}} -{"Key":"shift-v"} -{"Key":"p"} -{"Get":{"state":"ˇq\nj\nfox jjmps over\nthe lzy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_percent.json b/crates/vim/test_data/test_percent.json deleted file mode 100644 index 2382cc9e49..0000000000 --- a/crates/vim/test_data/test_percent.json +++ /dev/null @@ -1,58 +0,0 @@ -{"Put":{"state":"ˇconsole.log(var);"}} -{"Key":"%"} -{"Get":{"state":"console.log(varˇ);","mode":"Normal"}} -{"Put":{"state":"console.logˇ(var);"}} -{"Key":"%"} -{"Get":{"state":"console.log(varˇ);","mode":"Normal"}} -{"Put":{"state":"console.log(ˇvar);"}} -{"Key":"%"} -{"Get":{"state":"console.logˇ(var);","mode":"Normal"}} -{"Put":{"state":"console.log(vaˇr);"}} -{"Key":"%"} -{"Get":{"state":"console.logˇ(var);","mode":"Normal"}} -{"Put":{"state":"console.log(varˇ);"}} -{"Key":"%"} -{"Get":{"state":"console.logˇ(var);","mode":"Normal"}} -{"Put":{"state":"console.log(var)ˇ;"}} -{"Key":"%"} -{"Get":{"state":"console.log(var)ˇ;","mode":"Normal"}} -{"Put":{"state":"ˇconsole.log('var', [1, 2, 3]);"}} -{"Key":"%"} -{"Get":{"state":"console.log('var', [1, 2, 3]ˇ);","mode":"Normal"}} -{"Put":{"state":"console.logˇ('var', [1, 2, 3]);"}} -{"Key":"%"} -{"Get":{"state":"console.log('var', [1, 2, 3]ˇ);","mode":"Normal"}} -{"Put":{"state":"console.log(ˇ'var', [1, 2, 3]);"}} -{"Key":"%"} -{"Get":{"state":"console.log('var', [1, 2, 3ˇ]);","mode":"Normal"}} -{"Put":{"state":"console.log('var', ˇ[1, 2, 3]);"}} -{"Key":"%"} -{"Get":{"state":"console.log('var', [1, 2, 3ˇ]);","mode":"Normal"}} -{"Put":{"state":"console.log('var', [ˇ1, 2, 3]);"}} -{"Key":"%"} -{"Get":{"state":"console.log('var', ˇ[1, 2, 3]);","mode":"Normal"}} -{"Put":{"state":"console.log('var', [1, ˇ2, 3]);"}} -{"Key":"%"} -{"Get":{"state":"console.log('var', ˇ[1, 2, 3]);","mode":"Normal"}} -{"Put":{"state":"console.log('var', [1, 2, 3ˇ]);"}} -{"Key":"%"} -{"Get":{"state":"console.log('var', ˇ[1, 2, 3]);","mode":"Normal"}} -{"Put":{"state":"console.log('var', [1, 2, 3]ˇ);"}} -{"Key":"%"} -{"Get":{"state":"console.logˇ('var', [1, 2, 3]);","mode":"Normal"}} -{"Put":{"state":"console.log('var', [1, 2, 3])ˇ;"}} -{"Key":"%"} -{"Get":{"state":"console.log('var', [1, 2, 3])ˇ;","mode":"Normal"}} -{"Put":{"state":"let result = curried_funˇ()();"}} -{"Key":"%"} -{"Get":{"state":"let result = curried_fun(ˇ)();","mode":"Normal"}} -{"Key":"%"} -{"Get":{"state":"let result = curried_funˇ()();","mode":"Normal"}} -{"Put":{"state":"let result = curried_fun()ˇ();"}} -{"Key":"%"} -{"Get":{"state":"let result = curried_fun()(ˇ);","mode":"Normal"}} -{"Key":"%"} -{"Get":{"state":"let result = curried_fun()ˇ();","mode":"Normal"}} -{"Put":{"state":"let result = curried_fun()()ˇ;"}} -{"Key":"%"} -{"Get":{"state":"let result = curried_fun()()ˇ;","mode":"Normal"}} diff --git a/crates/vim/test_data/test_period_mark.json b/crates/vim/test_data/test_period_mark.json deleted file mode 100644 index 6d3acea83c..0000000000 --- a/crates/vim/test_data/test_period_mark.json +++ /dev/null @@ -1,14 +0,0 @@ -{"Put":{"state":"Line one\nLine two\nLiˇne three\nLine four\nLine five\n"}} -{"Key":"c"} -{"Key":"e"} -{"Key":"k"} -{"Key":"e"} -{"Key":"escape"} -{"Key":"j"} -{"Key":"j"} -{"Key":"'"} -{"Key":"."} -{"Get":{"state":"Line one\nLine two\nˇLike three\nLine four\nLine five\n","mode":"Normal"}} -{"Key":"`"} -{"Key":"."} -{"Get":{"state":"Line one\nLine two\nLiˇke three\nLine four\nLine five\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_plus_minus.json b/crates/vim/test_data/test_plus_minus.json deleted file mode 100644 index 277d92ef6a..0000000000 --- a/crates/vim/test_data/test_plus_minus.json +++ /dev/null @@ -1,7 +0,0 @@ -{"Put":{"state":"one\n two\nthrˇee\n"}} -{"Key":"-"} -{"Get":{"state":"one\n ˇtwo\nthree\n","mode":"Normal"}} -{"Key":"-"} -{"Get":{"state":"ˇone\n two\nthree\n","mode":"Normal"}} -{"Key":"+"} -{"Get":{"state":"one\n ˇtwo\nthree\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_previous_word_end.json b/crates/vim/test_data/test_previous_word_end.json deleted file mode 100644 index f1e3540240..0000000000 --- a/crates/vim/test_data/test_previous_word_end.json +++ /dev/null @@ -1,33 +0,0 @@ -{"Put":{"state":"456 5ˇ67 678\n"}} -{"Key":"g"} -{"Key":"e"} -{"Get":{"state":"45ˇ6 567 678\n","mode":"Normal"}} -{"Put":{"state":"123 234 345\n456 5ˇ67 678\n"}} -{"Key":"4"} -{"Key":"g"} -{"Key":"e"} -{"Get":{"state":"12ˇ3 234 345\n456 567 678\n","mode":"Normal"}} -{"Put":{"state":"123 234 345\n4;5.6 5ˇ67 678\n789 890 901\n"}} -{"Key":"g"} -{"Key":"e"} -{"Get":{"state":"123 234 345\n4;5.ˇ6 567 678\n789 890 901\n","mode":"Normal"}} -{"Put":{"state":"123 234 345\n4;5.6 5ˇ67 678\n789 890 901\n"}} -{"Key":"5"} -{"Key":"g"} -{"Key":"e"} -{"Get":{"state":"123 234 345\nˇ4;5.6 567 678\n789 890 901\n","mode":"Normal"}} -{"Put":{"state":"123 234 345\n\n78ˇ9 890 901\n"}} -{"Key":"g"} -{"Key":"e"} -{"Get":{"state":"123 234 345\nˇ\n789 890 901\n","mode":"Normal"}} -{"Key":"g"} -{"Key":"e"} -{"Get":{"state":"123 234 34ˇ5\n\n789 890 901\n","mode":"Normal"}} -{"Put":{"state":"123 234 345\n4;5.ˇ6 567 678\n789 890 901\n"}} -{"Key":"g"} -{"Key":"shift-e"} -{"Get":{"state":"123 234 34ˇ5\n4;5.6 567 678\n789 890 901\n","mode":"Normal"}} -{"Put":{"state":"bar ˇó\n"}} -{"Key":"g"} -{"Key":"e"} -{"Get":{"state":"baˇr ó\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_quote_mark.json b/crates/vim/test_data/test_quote_mark.json deleted file mode 100644 index 6eb85580e8..0000000000 --- a/crates/vim/test_data/test_quote_mark.json +++ /dev/null @@ -1,23 +0,0 @@ -{"Put":{"state":"ˇHello, world!"}} -{"Key":"w"} -{"Key":"m"} -{"Key":"o"} -{"Get":{"state":"Helloˇ, world!","mode":"Normal"}} -{"Key":"$"} -{"Key":"`"} -{"Key":"o"} -{"Get":{"state":"Helloˇ, world!","mode":"Normal"}} -{"Key":"`"} -{"Key":"`"} -{"Get":{"state":"Hello, worldˇ!","mode":"Normal"}} -{"Key":"`"} -{"Key":"`"} -{"Get":{"state":"Helloˇ, world!","mode":"Normal"}} -{"Key":"$"} -{"Key":"m"} -{"Key":"'"} -{"Get":{"state":"Hello, worldˇ!","mode":"Normal"}} -{"Key":"^"} -{"Key":"`"} -{"Key":"`"} -{"Get":{"state":"Hello, worldˇ!","mode":"Normal"}} diff --git a/crates/vim/test_data/test_r.json b/crates/vim/test_data/test_r.json deleted file mode 100644 index 6cb0aec3f5..0000000000 --- a/crates/vim/test_data/test_r.json +++ /dev/null @@ -1,40 +0,0 @@ -{"Put":{"state":"ˇhello\n"}} -{"Key":"r"} -{"Key":"-"} -{"Get":{"state":"ˇ-ello\n","mode":"Normal"}} -{"Put":{"state":"ˇhello\n"}} -{"Key":"3"} -{"Key":"r"} -{"Key":"-"} -{"Get":{"state":"--ˇ-lo\n","mode":"Normal"}} -{"Put":{"state":"ˇhello\n"}} -{"Key":"r"} -{"Key":"-"} -{"Key":"2"} -{"Key":"l"} -{"Key":"."} -{"Get":{"state":"-eˇ-lo\n","mode":"Normal"}} -{"Put":{"state":"ˇhello world\n"}} -{"Key":"2"} -{"Key":"r"} -{"Key":"-"} -{"Key":"f"} -{"Key":"w"} -{"Key":"."} -{"Get":{"state":"--llo -ˇ-rld\n","mode":"Normal"}} -{"Put":{"state":"ˇhello world\n"}} -{"Key":"2"} -{"Key":"0"} -{"Key":"r"} -{"Key":"-"} -{"Key":""} -{"Get":{"state":"ˇhello world\n","mode":"Normal"}} -{"Put":{"state":" helloˇ world\n"}} -{"Key":"r"} -{"Key":"enter"} -{"Get":{"state":" hello\n ˇ world\n","mode":"Normal"}} -{"Put":{"state":" helloˇ world\n"}} -{"Key":"2"} -{"Key":"r"} -{"Key":"enter"} -{"Get":{"state":" hello\n ˇ orld\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_record_replay.json b/crates/vim/test_data/test_record_replay.json deleted file mode 100644 index 8346d9ad8b..0000000000 --- a/crates/vim/test_data/test_record_replay.json +++ /dev/null @@ -1,14 +0,0 @@ -{"Put":{"state":"ˇhello world"}} -{"Key":"q"} -{"Key":"w"} -{"Key":"c"} -{"Key":"w"} -{"Key":"j"} -{"Key":"escape"} -{"Key":"q"} -{"Get":{"state":"ˇj world","mode":"Normal"}} -{"Key":"2"} -{"Key":"l"} -{"Key":"@"} -{"Key":"w"} -{"Get":{"state":"j ˇj","mode":"Normal"}} diff --git a/crates/vim/test_data/test_record_replay_count.json b/crates/vim/test_data/test_record_replay_count.json deleted file mode 100644 index 78023ef350..0000000000 --- a/crates/vim/test_data/test_record_replay_count.json +++ /dev/null @@ -1,16 +0,0 @@ -{"Put":{"state":"ˇhello world!!"}} -{"Key":"q"} -{"Key":"a"} -{"Key":"v"} -{"Key":"3"} -{"Key":"l"} -{"Key":"s"} -{"Key":"0"} -{"Key":"escape"} -{"Key":"l"} -{"Key":"q"} -{"Get":{"state":"0ˇo world!!","mode":"Normal"}} -{"Key":"2"} -{"Key":"@"} -{"Key":"a"} -{"Get":{"state":"000ˇ!","mode":"Normal"}} diff --git a/crates/vim/test_data/test_record_replay_dot.json b/crates/vim/test_data/test_record_replay_dot.json deleted file mode 100644 index 9cc565f160..0000000000 --- a/crates/vim/test_data/test_record_replay_dot.json +++ /dev/null @@ -1,17 +0,0 @@ -{"Put":{"state":"ˇhello world"}} -{"Key":"q"} -{"Key":"a"} -{"Key":"r"} -{"Key":"a"} -{"Key":"l"} -{"Key":"r"} -{"Key":"b"} -{"Key":"l"} -{"Key":"q"} -{"Get":{"state":"abˇllo world","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"abˇblo world","mode":"Normal"}} -{"Key":"shift-q"} -{"Get":{"state":"ababˇo world","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"ababˇb world","mode":"Normal"}} diff --git a/crates/vim/test_data/test_record_replay_interleaved.json b/crates/vim/test_data/test_record_replay_interleaved.json deleted file mode 100644 index aefb5eac2a..0000000000 --- a/crates/vim/test_data/test_record_replay_interleaved.json +++ /dev/null @@ -1,35 +0,0 @@ -{"Put":{"state":"ˇhello world"}} -{"Key":"q"} -{"Key":"z"} -{"Key":"r"} -{"Key":"a"} -{"Key":"l"} -{"Key":"q"} -{"Get":{"state":"aˇello world","mode":"Normal"}} -{"Key":"q"} -{"Key":"b"} -{"Key":"@"} -{"Key":"z"} -{"Key":"@"} -{"Key":"z"} -{"Key":"q"} -{"Get":{"state":"aaaˇlo world","mode":"Normal"}} -{"Key":"@"} -{"Key":"@"} -{"Get":{"state":"aaaaˇo world","mode":"Normal"}} -{"Key":"@"} -{"Key":"b"} -{"Get":{"state":"aaaaaaˇworld","mode":"Normal"}} -{"Key":"@"} -{"Key":"@"} -{"Get":{"state":"aaaaaaaˇorld","mode":"Normal"}} -{"Key":"q"} -{"Key":"z"} -{"Key":"r"} -{"Key":"b"} -{"Key":"l"} -{"Key":"q"} -{"Get":{"state":"aaaaaaabˇrld","mode":"Normal"}} -{"Key":"@"} -{"Key":"b"} -{"Get":{"state":"aaaaaaabbbˇd","mode":"Normal"}} diff --git a/crates/vim/test_data/test_record_replay_of_dot.json b/crates/vim/test_data/test_record_replay_of_dot.json deleted file mode 100644 index f4cce4bb3d..0000000000 --- a/crates/vim/test_data/test_record_replay_of_dot.json +++ /dev/null @@ -1,14 +0,0 @@ -{"Put":{"state":"ˇhello world"}} -{"Key":"r"} -{"Key":"o"} -{"Key":"q"} -{"Key":"w"} -{"Key":"."} -{"Key":"q"} -{"Get":{"state":"ˇoello world","mode":"Normal"}} -{"Key":"d"} -{"Key":"l"} -{"Get":{"state":"ˇello world","mode":"Normal"}} -{"Key":"@"} -{"Key":"w"} -{"Get":{"state":"ˇllo world","mode":"Normal"}} diff --git a/crates/vim/test_data/test_record_replay_recursion.json b/crates/vim/test_data/test_record_replay_recursion.json deleted file mode 100644 index d02817db57..0000000000 --- a/crates/vim/test_data/test_record_replay_recursion.json +++ /dev/null @@ -1,6 +0,0 @@ -{"Put":{"state":"ˇhello world"}} -{"Key":">"} -{"Key":"."} -{"Key":"."} -{"Key":"."} -{"Get":{"state":"ˇhello world","mode":"Normal"}} diff --git a/crates/vim/test_data/test_remap_adjacent_dog_cat.json b/crates/vim/test_data/test_remap_adjacent_dog_cat.json deleted file mode 100644 index 91af9ccac6..0000000000 --- a/crates/vim/test_data/test_remap_adjacent_dog_cat.json +++ /dev/null @@ -1,24 +0,0 @@ -{"Exec":{"command":"imap dog 🐶"}} -{"Exec":{"command":"imap cat 🐱"}} -{"Put":{"state":"ˇ"}} -{"Key":"i"} -{"Key":"d"} -{"Key":"o"} -{"Key":"g"} -{"Get":{"state":"🐶ˇ","mode":"Insert"}} -{"Put":{"state":"ˇ"}} -{"Key":"i"} -{"Key":"d"} -{"Key":"o"} -{"Key":"d"} -{"Key":"o"} -{"Key":"g"} -{"Get":{"state":"do🐶ˇ","mode":"Insert"}} -{"Put":{"state":"ˇ"}} -{"Key":"i"} -{"Key":"d"} -{"Key":"o"} -{"Key":"c"} -{"Key":"a"} -{"Key":"t"} -{"Get":{"state":"do🐱ˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_remap_nested_pineapple.json b/crates/vim/test_data/test_remap_nested_pineapple.json deleted file mode 100644 index b4a4acdd2b..0000000000 --- a/crates/vim/test_data/test_remap_nested_pineapple.json +++ /dev/null @@ -1,28 +0,0 @@ -{"Exec":{"command":"imap pin 📌"}} -{"Exec":{"command":"imap pine 🌲"}} -{"Exec":{"command":"imap pineapple 🍍"}} -{"Put":{"state":"ˇ"}} -{"Key":"i"} -{"Key":"p"} -{"Key":"i"} -{"Key":"n"} -{"Get":{"state":"📌ˇ","mode":"Insert"}} -{"Put":{"state":"ˇ"}} -{"Key":"i"} -{"Key":"p"} -{"Key":"i"} -{"Key":"n"} -{"Key":"e"} -{"Get":{"state":"🌲ˇ","mode":"Insert"}} -{"Put":{"state":"ˇ"}} -{"Key":"i"} -{"Key":"p"} -{"Key":"i"} -{"Key":"n"} -{"Key":"e"} -{"Key":"a"} -{"Key":"p"} -{"Key":"p"} -{"Key":"l"} -{"Key":"e"} -{"Get":{"state":"🍍ˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_remap_recursion.json b/crates/vim/test_data/test_remap_recursion.json deleted file mode 100644 index 27e3c64b16..0000000000 --- a/crates/vim/test_data/test_remap_recursion.json +++ /dev/null @@ -1,11 +0,0 @@ -{"Exec":{"command":"noremap x \"_x"}} -{"Exec":{"command":"map y 2x"}} -{"Put":{"state":"ˇhello"}} -{"Key":"d"} -{"Key":"l"} -{"Get":{"state":"ˇello","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"h"}} -{"Key":"y"} -{"Get":{"state":"ˇlo","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"h"}} -{"Get":{"state":"ˇlo","mode":"Normal"}} diff --git a/crates/vim/test_data/test_repeat_clear_count.json b/crates/vim/test_data/test_repeat_clear_count.json deleted file mode 100644 index 352c6ca4a8..0000000000 --- a/crates/vim/test_data/test_repeat_clear_count.json +++ /dev/null @@ -1,21 +0,0 @@ -{"Put":{"state":"ˇthe quick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"d"} -{"Key":"d"} -{"Get":{"state":"ˇfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"2"} -{"Key":"d"} -{"Key":"."} -{"Get":{"state":"ˇfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"ˇthe lazy dog","mode":"Normal"}} -{"Put":{"state":"ˇthe quick brown\nfox jumps over\nthe lazy dog\nthe quick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"2"} -{"Key":"d"} -{"Key":"d"} -{"Get":{"state":"ˇthe lazy dog\nthe quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"5"} -{"Key":"d"} -{"Key":"."} -{"Get":{"state":"ˇthe lazy dog\nthe quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"ˇfox jumps over\nthe lazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_repeat_clear_repeat.json b/crates/vim/test_data/test_repeat_clear_repeat.json deleted file mode 100644 index 39d96e2a37..0000000000 --- a/crates/vim/test_data/test_repeat_clear_repeat.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Put":{"state":"ˇthe quick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"d"} -{"Key":"d"} -{"Get":{"state":"ˇfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"d"} -{"Key":"."} -{"Key":"."} -{"Get":{"state":"ˇthe lazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_repeat_grouping_41735.json b/crates/vim/test_data/test_repeat_grouping_41735.json deleted file mode 100644 index 6523be6e4b..0000000000 --- a/crates/vim/test_data/test_repeat_grouping_41735.json +++ /dev/null @@ -1,10 +0,0 @@ -{"Put":{"state":"ˇ"}} -{"Key":"i"} -{"Key":"a"} -{"Key":"escape"} -{"Key":"."} -{"Key":"."} -{"Key":"."} -{"Get":{"state":"ˇaaaa","mode":"Normal"}} -{"Key":"u"} -{"Get":{"state":"ˇaaa","mode":"Normal"}} diff --git a/crates/vim/test_data/test_repeat_motion_counts.json b/crates/vim/test_data/test_repeat_motion_counts.json deleted file mode 100644 index c39b8b09c0..0000000000 --- a/crates/vim/test_data/test_repeat_motion_counts.json +++ /dev/null @@ -1,13 +0,0 @@ -{"Put":{"state":"ˇthe quick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"3"} -{"Key":"d"} -{"Key":"3"} -{"Key":"l"} -{"Get":{"state":"ˇ brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"j"} -{"Key":"."} -{"Get":{"state":" brown\nˇ over\nthe lazy dog","mode":"Normal"}} -{"Key":"j"} -{"Key":"2"} -{"Key":"."} -{"Get":{"state":" brown\n over\nˇe lazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_repeat_over_blur.json b/crates/vim/test_data/test_repeat_over_blur.json deleted file mode 100644 index 3929711e49..0000000000 --- a/crates/vim/test_data/test_repeat_over_blur.json +++ /dev/null @@ -1,11 +0,0 @@ -{"Put":{"state":"ˇhello hello hello\n"}} -{"Key":"c"} -{"Key":"f"} -{"Key":"o"} -{"Key":"x"} -{"Key":"escape"} -{"Get":{"state":"ˇx hello hello\n","mode":"Normal"}} -{"Key":":"} -{"Key":"escape"} -{"Key":"."} -{"Get":{"state":"ˇx hello\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_repeat_visual.json b/crates/vim/test_data/test_repeat_visual.json deleted file mode 100644 index cb83addcfb..0000000000 --- a/crates/vim/test_data/test_repeat_visual.json +++ /dev/null @@ -1,51 +0,0 @@ -{"Put":{"state":"ˇthe quick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Key":"s"} -{"Key":"o"} -{"Key":"escape"} -{"Get":{"state":"ˇo quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"j"} -{"Key":"w"} -{"Key":"."} -{"Get":{"state":"o quick brown\nfox ˇops over\nthe lazy dog","mode":"Normal"}} -{"Key":"f"} -{"Key":"r"} -{"Key":"."} -{"Get":{"state":"o quick brown\nfox ops oveˇothe lazy dog","mode":"Normal"}} -{"Put":{"state":"the ˇquick brown\nfox jumps over\nfox jumps over\nfox jumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"j"} -{"Key":"x"} -{"Get":{"state":"the ˇumps over\nfox jumps over\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"."} -{"Get":{"state":"the ˇumps over\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"w"} -{"Key":"."} -{"Get":{"state":"the umps ˇumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"j"} -{"Key":"."} -{"Get":{"state":"the umps umps over\nthe ˇog","mode":"Normal"}} -{"Put":{"state":"ˇthe quick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"ctrl-v"} -{"Key":"j"} -{"Key":"j"} -{"Key":"shift-i"} -{"Key":"o"} -{"Key":"escape"} -{"Get":{"state":"ˇothe quick brown\nofox jumps over\nothe lazy dog","mode":"Normal"}} -{"Key":"j"} -{"Key":"4"} -{"Key":"l"} -{"Key":"."} -{"Get":{"state":"othe quick brown\nofoxˇo jumps over\notheo lazy dog","mode":"Normal"}} -{"Put":{"state":"ˇthe quick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"shift-r"} -{"Key":"o"} -{"Key":"escape"} -{"Get":{"state":"ˇo\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"j"} -{"Key":"."} -{"Get":{"state":"o\nˇo\nthe lazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_repeated_cb.json b/crates/vim/test_data/test_repeated_cb.json deleted file mode 100644 index 437b55ef29..0000000000 --- a/crates/vim/test_data/test_repeated_cb.json +++ /dev/null @@ -1,275 +0,0 @@ -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"b"} -{"Get":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"b"} -{"Get":{"state":"The ˇick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"b"} -{"Get":{"state":"The ˇ brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"b"} -{"Get":{"state":"The quick ˇn\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"b"} -{"Get":{"state":"The quick ˇ\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"b"} -{"Get":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"b"} -{"Get":{"state":"The quick brown\n\nˇjumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"b"} -{"Get":{"state":"The quick brown\n\nfox ˇ-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"b"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇover\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"b"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇver\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"b"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"b"} -{"Get":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"b"} -{"Get":{"state":"ˇick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"b"} -{"Get":{"state":"ˇ brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"b"} -{"Get":{"state":"The ˇn\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"b"} -{"Get":{"state":"The ˇ\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"b"} -{"Get":{"state":"The quick ˇ\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"b"} -{"Get":{"state":"The quick brown\nˇjumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"b"} -{"Get":{"state":"The quick brown\n\nˇ-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"b"} -{"Get":{"state":"The quick brown\n\nfox ˇover\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"b"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇver\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"b"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"b"} -{"Get":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"b"} -{"Get":{"state":"ˇick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"b"} -{"Get":{"state":"ˇ brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"b"} -{"Get":{"state":"ˇn\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"b"} -{"Get":{"state":"ˇ\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"b"} -{"Get":{"state":"The ˇ\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"b"} -{"Get":{"state":"The quick ˇjumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"b"} -{"Get":{"state":"The quick brown\nˇ-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"b"} -{"Get":{"state":"The quick brown\n\nˇover\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"b"} -{"Get":{"state":"The quick brown\n\nfox ˇver\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"b"} -{"Get":{"state":"The quick brown\n\nfox ˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"b"} -{"Get":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"b"} -{"Get":{"state":"ˇick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"b"} -{"Get":{"state":"ˇ brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"b"} -{"Get":{"state":"ˇn\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"b"} -{"Get":{"state":"ˇ\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"b"} -{"Get":{"state":"ˇ\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"b"} -{"Get":{"state":"The ˇjumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"b"} -{"Get":{"state":"The quick ˇ-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"b"} -{"Get":{"state":"The quick brown\nˇover\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"b"} -{"Get":{"state":"The quick brown\n\nˇver\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"b"} -{"Get":{"state":"The quick brown\n\nˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"b"} -{"Get":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"b"} -{"Get":{"state":"ˇick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"b"} -{"Get":{"state":"ˇ brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"b"} -{"Get":{"state":"ˇn\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"b"} -{"Get":{"state":"ˇ\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"b"} -{"Get":{"state":"ˇ\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"b"} -{"Get":{"state":"ˇjumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"b"} -{"Get":{"state":"The ˇ-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"b"} -{"Get":{"state":"The quick ˇover\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"b"} -{"Get":{"state":"The quick brown\nˇver\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"b"} -{"Get":{"state":"The quick brown\nˇ\nthe lazy dog\n","mode":"Insert"}} diff --git a/crates/vim/test_data/test_repeated_ce.json b/crates/vim/test_data/test_repeated_ce.json deleted file mode 100644 index 3032185d64..0000000000 --- a/crates/vim/test_data/test_repeated_ce.json +++ /dev/null @@ -1,275 +0,0 @@ -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"e"} -{"Get":{"state":"ˇ quick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"e"} -{"Get":{"state":"The quˇ brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"e"} -{"Get":{"state":"The quickˇ\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"e"} -{"Get":{"state":"The quick browˇ jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"e"} -{"Get":{"state":"The quick brown\nˇ jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nˇ jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox ˇ-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumps-oˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇ lazy dog\n","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"e"} -{"Get":{"state":"ˇ brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"e"} -{"Get":{"state":"The quˇ\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"e"} -{"Get":{"state":"The quickˇ jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"e"} -{"Get":{"state":"The quick browˇ-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"e"} -{"Get":{"state":"The quick brown\nˇ-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nˇ-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox ˇover\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇ lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇ lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumps-oˇ lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇ dog\n","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"e"} -{"Get":{"state":"ˇ\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"e"} -{"Get":{"state":"The quˇ jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"e"} -{"Get":{"state":"The quickˇ-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"e"} -{"Get":{"state":"The quick browˇover\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"e"} -{"Get":{"state":"The quick brown\nˇover\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nˇover\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox ˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇ dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇ dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumps-oˇ dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇ\n","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"e"} -{"Get":{"state":"ˇ jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"e"} -{"Get":{"state":"The quˇ-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"e"} -{"Get":{"state":"The quickˇover\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"e"} -{"Get":{"state":"The quick browˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"e"} -{"Get":{"state":"The quick brown\nˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox ˇ lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumps-oˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇ","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"e"} -{"Get":{"state":"ˇ-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"e"} -{"Get":{"state":"The quˇover\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"e"} -{"Get":{"state":"The quickˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"e"} -{"Get":{"state":"The quick browˇ lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"e"} -{"Get":{"state":"The quick brown\nˇ lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nˇ lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox ˇ dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumps-oˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"e"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_repeated_cj.json b/crates/vim/test_data/test_repeated_cj.json deleted file mode 100644 index e20270f97c..0000000000 --- a/crates/vim/test_data/test_repeated_cj.json +++ /dev/null @@ -1,275 +0,0 @@ -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"j"} -{"Get":{"state":"ˇ\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"j"} -{"Get":{"state":"ˇ\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"j"} -{"Get":{"state":"ˇ\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"j"} -{"Get":{"state":"ˇ\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"j"} -{"Get":{"state":"The quick brown\nˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇ","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"j"} -{"Get":{"state":"ˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"j"} -{"Get":{"state":"ˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"j"} -{"Get":{"state":"ˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"j"} -{"Get":{"state":"ˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"j"} -{"Get":{"state":"The quick brown\nˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇ","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"j"} -{"Get":{"state":"ˇ\n","mode":"Insert"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"j"} -{"Get":{"state":"ˇ\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"j"} -{"Get":{"state":"ˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"j"} -{"Get":{"state":"ˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"j"} -{"Get":{"state":"The quick brown\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇ","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"j"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"j"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"j"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"j"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"j"} -{"Get":{"state":"The quick brown\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇ","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"j"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"j"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"j"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"j"} -{"Get":{"state":"ˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"j"} -{"Get":{"state":"The quick brown\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"j"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_repeated_cl.json b/crates/vim/test_data/test_repeated_cl.json deleted file mode 100644 index 397a364e5a..0000000000 --- a/crates/vim/test_data/test_repeated_cl.json +++ /dev/null @@ -1,275 +0,0 @@ -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"l"} -{"Get":{"state":"ˇhe quick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"l"} -{"Get":{"state":"The quˇck brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"l"} -{"Get":{"state":"The quickˇbrown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"l"} -{"Get":{"state":"The quick browˇ\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"l"} -{"Get":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nˇox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox ˇumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇover\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇver\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumps-oˇer\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"1"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇhe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"l"} -{"Get":{"state":"ˇe quick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"l"} -{"Get":{"state":"The quˇk brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"l"} -{"Get":{"state":"The quickˇrown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"l"} -{"Get":{"state":"The quick browˇ\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"l"} -{"Get":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nˇx jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox ˇmps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇver\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇer\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumps-oˇr\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"2"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"l"} -{"Get":{"state":"ˇ quick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"l"} -{"Get":{"state":"The quˇ brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"l"} -{"Get":{"state":"The quickˇown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"l"} -{"Get":{"state":"The quick browˇ\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"l"} -{"Get":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nˇ jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox ˇps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇer\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇr\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumps-oˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"3"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇ lazy dog\n","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"l"} -{"Get":{"state":"ˇquick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"l"} -{"Get":{"state":"The quˇbrown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"l"} -{"Get":{"state":"The quickˇwn\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"l"} -{"Get":{"state":"The quick browˇ\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"l"} -{"Get":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nˇjumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox ˇs-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇr\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumps-oˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"4"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇlazy dog\n","mode":"Insert"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"l"} -{"Get":{"state":"ˇuick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"l"} -{"Get":{"state":"The quˇrown\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"l"} -{"Get":{"state":"The quickˇn\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"l"} -{"Get":{"state":"The quick browˇ\n\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"l"} -{"Get":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nˇumps-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox ˇ-over\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumps-oˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"c"} -{"Key":"5"} -{"Key":"l"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇazy dog\n","mode":"Insert"}} diff --git a/crates/vim/test_data/test_repeated_word.json b/crates/vim/test_data/test_repeated_word.json deleted file mode 100644 index caa7ed7367..0000000000 --- a/crates/vim/test_data/test_repeated_word.json +++ /dev/null @@ -1,214 +0,0 @@ -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"1"} -{"Key":"w"} -{"Get":{"state":"The ˇquick brown\n\nfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"1"} -{"Key":"w"} -{"Get":{"state":"The quick ˇbrown\n\nfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"1"} -{"Key":"w"} -{"Get":{"state":"The quick ˇbrown\n\nfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"1"} -{"Key":"w"} -{"Get":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"1"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n","mode":"Normal"}} -{"Key":"1"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe ˇlazy dog\n","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"2"} -{"Key":"w"} -{"Get":{"state":"The quick ˇbrown\n\nfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"2"} -{"Key":"w"} -{"Get":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"2"} -{"Key":"w"} -{"Get":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"2"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"2"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"2"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"2"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"2"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"2"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe ˇlazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"2"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe ˇlazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"2"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe lazy ˇdog\n","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"3"} -{"Key":"w"} -{"Get":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"3"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"3"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"3"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"3"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"3"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"3"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"3"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe ˇlazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"3"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe lazy ˇdog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"3"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe lazy ˇdog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"3"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe lazy dog\nˇ","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"4"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"4"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"4"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"4"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"4"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"4"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"4"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe ˇlazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"4"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe lazy ˇdog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"4"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe lazy dog\nˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"4"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe lazy dog\nˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"4"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe lazy dog\nˇ","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"5"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quˇick brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"5"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quickˇ brown\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"5"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick browˇn\n\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"5"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps-over\nthe lazy dog\n"}} -{"Key":"5"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nˇfox jumps-over\nthe lazy dog\n"}} -{"Key":"5"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe ˇlazy dog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox ˇjumps-over\nthe lazy dog\n"}} -{"Key":"5"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe lazy ˇdog\n","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumpsˇ-over\nthe lazy dog\n"}} -{"Key":"5"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe lazy dog\nˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumps-ˇover\nthe lazy dog\n"}} -{"Key":"5"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe lazy dog\nˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumps-oˇver\nthe lazy dog\n"}} -{"Key":"5"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe lazy dog\nˇ","mode":"Normal"}} -{"Put":{"state":"The quick brown\n\nfox jumps-over\nˇthe lazy dog\n"}} -{"Key":"5"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n\nfox jumps-over\nthe lazy dog\nˇ","mode":"Normal"}} diff --git a/crates/vim/test_data/test_replace_g.json b/crates/vim/test_data/test_replace_g.json deleted file mode 100644 index 583d1f89bc..0000000000 --- a/crates/vim/test_data/test_replace_g.json +++ /dev/null @@ -1,23 +0,0 @@ -{"Put":{"state":"ˇaa aa aa aa\naa\naa"}} -{"Key":":"} -{"Key":"s"} -{"Key":"/"} -{"Key":"a"} -{"Key":"a"} -{"Key":"/"} -{"Key":"b"} -{"Key":"b"} -{"Key":"enter"} -{"Get":{"state":"ˇbb aa aa aa\naa\naa","mode":"Normal"}} -{"Key":":"} -{"Key":"s"} -{"Key":"/"} -{"Key":"a"} -{"Key":"a"} -{"Key":"/"} -{"Key":"b"} -{"Key":"b"} -{"Key":"/"} -{"Key":"g"} -{"Key":"enter"} -{"Get":{"state":"ˇbb bb bb bb\naa\naa","mode":"Normal"}} diff --git a/crates/vim/test_data/test_replace_mode.json b/crates/vim/test_data/test_replace_mode.json deleted file mode 100644 index 32b2cc64d3..0000000000 --- a/crates/vim/test_data/test_replace_mode.json +++ /dev/null @@ -1,25 +0,0 @@ -{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog."}} -{"Key":"shift-r"} -{"Key":"O"} -{"Key":"n"} -{"Key":"e"} -{"Get":{"state":"Oneˇ quick brown\nfox jumps over\nthe lazy dog.","mode":"Replace"}} -{"Put":{"state":"The quick browˇn\nfox jumps over\nthe lazy dog."}} -{"Key":"shift-r"} -{"Key":"O"} -{"Key":"n"} -{"Key":"e"} -{"Get":{"state":"The quick browOneˇ\nfox jumps over\nthe lazy dog.","mode":"Replace"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps over\nthe lazy dog."}} -{"Key":"shift-r"} -{"Key":"O"} -{"Key":"n"} -{"Key":"e"} -{"Get":{"state":"The quick brown\nOneˇ\nfox jumps over\nthe lazy dog.","mode":"Replace"}} -{"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog."}} -{"Key":"shift-r"} -{"Key":"enter"} -{"Key":"O"} -{"Key":"n"} -{"Key":"e"} -{"Get":{"state":"The qu\nOneˇ brown\nfox jumps over\nthe lazy dog.","mode":"Replace"}} diff --git a/crates/vim/test_data/test_replace_mode_repeat.json b/crates/vim/test_data/test_replace_mode_repeat.json deleted file mode 100644 index ab7cace51c..0000000000 --- a/crates/vim/test_data/test_replace_mode_repeat.json +++ /dev/null @@ -1,10 +0,0 @@ -{"Put":{"state":"ˇhello world\n"}} -{"Key":"shift-r"} -{"Key":"-"} -{"Key":"-"} -{"Key":"-"} -{"Key":"escape"} -{"Key":"4"} -{"Key":"l"} -{"Key":"."} -{"Get":{"state":"---lo --ˇ-ld\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_replace_mode_undo.json b/crates/vim/test_data/test_replace_mode_undo.json deleted file mode 100644 index fbad2f5042..0000000000 --- a/crates/vim/test_data/test_replace_mode_undo.json +++ /dev/null @@ -1,93 +0,0 @@ -{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog."}} -{"Key":"shift-r"} -{"Key":"O"} -{"Key":"n"} -{"Key":"e"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Get":{"state":"ˇThe quick brown fox jumps over the lazy dog.","mode":"Replace"}} -{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog."}} -{"Key":"shift-r"} -{"Key":"O"} -{"Key":"enter"} -{"Key":"e"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Get":{"state":"ˇThe quick brown fox jumps over the lazy dog.","mode":"Replace"}} -{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog."}} -{"Key":"shift-r"} -{"Key":"O"} -{"Key":"enter"} -{"Key":"n"} -{"Key":"enter"} -{"Key":"e"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Get":{"state":"ˇThe quick brown fox jumps over the lazy dog.","mode":"Replace"}} -{"Put":{"state":"The quick browˇn\nfox jumps over\nthe lazy dog."}} -{"Key":"shift-r"} -{"Key":"O"} -{"Key":"n"} -{"Key":"e"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Get":{"state":"The quick browˇn\nfox jumps over\nthe lazy dog.","mode":"Replace"}} -{"Put":{"state":"The quick browˇn\nfox jumps over\nthe lazy dog."}} -{"Key":"shift-r"} -{"Key":"O"} -{"Key":"enter"} -{"Key":"e"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Get":{"state":"The quick browˇn\nfox jumps over\nthe lazy dog.","mode":"Replace"}} -{"Put":{"state":"The quick browˇn\nfox jumps over\nthe lazy dog."}} -{"Key":"shift-r"} -{"Key":"O"} -{"Key":"enter"} -{"Key":"n"} -{"Key":"enter"} -{"Key":"e"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Get":{"state":"The quick browˇn\nfox jumps over\nthe lazy dog.","mode":"Replace"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps over\nthe lazy dog."}} -{"Key":"shift-r"} -{"Key":"O"} -{"Key":"n"} -{"Key":"e"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Get":{"state":"The quick brown\nˇ\nfox jumps over\nthe lazy dog.","mode":"Replace"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps over\nthe lazy dog."}} -{"Key":"shift-r"} -{"Key":"O"} -{"Key":"enter"} -{"Key":"e"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Get":{"state":"The quick brown\nˇ\nfox jumps over\nthe lazy dog.","mode":"Replace"}} -{"Put":{"state":"The quick brown\nˇ\nfox jumps over\nthe lazy dog."}} -{"Key":"shift-r"} -{"Key":"O"} -{"Key":"enter"} -{"Key":"n"} -{"Key":"enter"} -{"Key":"e"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Key":"backspace"} -{"Get":{"state":"The quick brown\nˇ\nfox jumps over\nthe lazy dog.","mode":"Replace"}} diff --git a/crates/vim/test_data/test_replace_mode_with_counts.json b/crates/vim/test_data/test_replace_mode_with_counts.json deleted file mode 100644 index d88c856fe3..0000000000 --- a/crates/vim/test_data/test_replace_mode_with_counts.json +++ /dev/null @@ -1,14 +0,0 @@ -{"Put":{"state":"ˇhello\n"}} -{"Key":"3"} -{"Key":"shift-r"} -{"Key":"-"} -{"Key":"escape"} -{"Get":{"state":"--ˇ-lo\n","mode":"Normal"}} -{"Put":{"state":"ˇhello\n"}} -{"Key":"3"} -{"Key":"shift-r"} -{"Key":"a"} -{"Key":"b"} -{"Key":"c"} -{"Key":"escape"} -{"Get":{"state":"abcabcabˇc\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_replace_n.json b/crates/vim/test_data/test_replace_n.json deleted file mode 100644 index a03c69e9b2..0000000000 --- a/crates/vim/test_data/test_replace_n.json +++ /dev/null @@ -1,13 +0,0 @@ -{"Put":{"state":"ˇaa\nbb\naa"}} -{"Key":":"} -{"Key":"s"} -{"Key":"/"} -{"Key":"b"} -{"Key":"b"} -{"Key":"/"} -{"Key":"d"} -{"Key":"d"} -{"Key":"/"} -{"Key":"n"} -{"Key":"enter"} -{"Get":{"state":"ˇaa\nbb\naa","mode":"Normal"}} diff --git a/crates/vim/test_data/test_replace_with_range.json b/crates/vim/test_data/test_replace_with_range.json deleted file mode 100644 index 8385af9a7f..0000000000 --- a/crates/vim/test_data/test_replace_with_range.json +++ /dev/null @@ -1,16 +0,0 @@ -{"Put":{"state":"ˇa\na\na\na\na\na\na\n "}} -{"Key":":"} -{"Key":"2"} -{"Key":","} -{"Key":"5"} -{"Key":"s"} -{"Key":"/"} -{"Key":"a"} -{"Key":"/"} -{"Key":"b"} -{"Key":"enter"} -{"Get":{"state":"a\nb\nb\nb\nˇb\na\na\n ","mode":"Normal"}} -{"Key":"/"} -{"Key":"a"} -{"Key":"enter"} -{"Get":{"state":"a\nb\nb\nb\nb\nˇa\na\n ","mode":"Normal"}} diff --git a/crates/vim/test_data/test_replace_with_range_at_start.json b/crates/vim/test_data/test_replace_with_range_at_start.json deleted file mode 100644 index e3810a7ba2..0000000000 --- a/crates/vim/test_data/test_replace_with_range_at_start.json +++ /dev/null @@ -1,16 +0,0 @@ -{"Put":{"state":"ˇa\na\na\na\na\na\na\n "}} -{"Key":":"} -{"Key":"2"} -{"Key":","} -{"Key":"5"} -{"Key":"s"} -{"Key":"/"} -{"Key":"^"} -{"Key":"/"} -{"Key":"b"} -{"Key":"enter"} -{"Get":{"state":"a\nba\nba\nba\nˇba\na\na\n ","mode":"Normal"}} -{"Key":"/"} -{"Key":"a"} -{"Key":"enter"} -{"Get":{"state":"a\nba\nba\nba\nbˇa\na\na\n ","mode":"Normal"}} diff --git a/crates/vim/test_data/test_scroll_beyond_last_line.json b/crates/vim/test_data/test_scroll_beyond_last_line.json deleted file mode 100644 index e983bda3f2..0000000000 --- a/crates/vim/test_data/test_scroll_beyond_last_line.json +++ /dev/null @@ -1,12 +0,0 @@ -{"SetOption":{"value":"scrolloff=3"}} -{"SetOption":{"value":"lines=12"}} -{"Put":{"state":"ˇaa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz"}} -{"Key":"shift-g"} -{"Key":"k"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nˇyy\nzz","mode":"Normal"}} -{"Key":"ctrl-d"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nˇzz","mode":"Normal"}} -{"Key":"shift-g"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nˇzz","mode":"Normal"}} -{"Key":"ctrl-u"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nˇrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz","mode":"Normal"}} diff --git a/crates/vim/test_data/test_scroll_jumps.json b/crates/vim/test_data/test_scroll_jumps.json deleted file mode 100644 index 8d44457bf9..0000000000 --- a/crates/vim/test_data/test_scroll_jumps.json +++ /dev/null @@ -1,12 +0,0 @@ -{"SetOption":{"value":"scrolloff=3"}} -{"SetOption":{"value":"lines=22"}} -{"Put":{"state":"ˇaa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz\n{{\n||\n}}\n~~\n\n€€\n\n‚‚\nƒƒ\n„„\n……\n††\n‡‡\nˆˆ\n‰‰\nŠŠ\n‹‹\nŒŒ\n\nŽŽ\n\n\n‘‘\n’’\n““\n””"}} -{"Key":"shift-g"} -{"Key":"g"} -{"Key":"g"} -{"Key":"ctrl-d"} -{"Key":"ctrl-d"} -{"Key":"ctrl-o"} -{"Get":{"state":"aa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz\n{{\n||\n}}\n~~\n\n€€\n\n‚‚\nƒƒ\n„„\n……\n††\n‡‡\nˆˆ\n‰‰\nŠŠ\n‹‹\nŒŒ\n\nŽŽ\n\n\n‘‘\n’’\n““\nˇ””","mode":"Normal"}} -{"Key":"ctrl-o"} -{"Get":{"state":"ˇaa\nbb\ncc\ndd\nee\nff\ngg\nhh\nii\njj\nkk\nll\nmm\nnn\noo\npp\nqq\nrr\nss\ntt\nuu\nvv\nww\nxx\nyy\nzz\n{{\n||\n}}\n~~\n\n€€\n\n‚‚\nƒƒ\n„„\n……\n††\n‡‡\nˆˆ\n‰‰\nŠŠ\n‹‹\nŒŒ\n\nŽŽ\n\n\n‘‘\n’’\n““\n””","mode":"Normal"}} diff --git a/crates/vim/test_data/test_search_skipping.json b/crates/vim/test_data/test_search_skipping.json deleted file mode 100644 index 6dae5e909f..0000000000 --- a/crates/vim/test_data/test_search_skipping.json +++ /dev/null @@ -1,12 +0,0 @@ -{"Put":{"state":"ˇaa aa aa"}} -{"Key":"/"} -{"Key":"a"} -{"Key":"a"} -{"Key":"enter"} -{"Get":{"state":"aa ˇaa aa","mode":"Normal"}} -{"Key":"left"} -{"Key":"/"} -{"Key":"a"} -{"Key":"a"} -{"Key":"enter"} -{"Get":{"state":"aa ˇaa aa","mode":"Normal"}} diff --git a/crates/vim/test_data/test_selection_goal.json b/crates/vim/test_data/test_selection_goal.json deleted file mode 100644 index ada2ab4c8e..0000000000 --- a/crates/vim/test_data/test_selection_goal.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Put":{"state":";;ˇ;\nLorem Ipsum"}} -{"Key":"a"} -{"Key":"down"} -{"Key":"up"} -{"Key":";"} -{"Key":"down"} -{"Key":"up"} -{"Get":{"state":";;;;ˇ\nLorem Ipsum","mode":"Insert"}} diff --git a/crates/vim/test_data/test_sentence_backwards.json b/crates/vim/test_data/test_sentence_backwards.json deleted file mode 100644 index 3126c3d39c..0000000000 --- a/crates/vim/test_data/test_sentence_backwards.json +++ /dev/null @@ -1,32 +0,0 @@ -{"Put":{"state":"one\n\ntwo\nthree\nˇ\nfour"}} -{"Key":"("} -{"Get":{"state":"one\n\nˇtwo\nthree\n\nfour","mode":"Normal"}} -{"Put":{"state":"hello.\n\n\nworˇld."}} -{"Key":"("} -{"Get":{"state":"hello.\n\n\nˇworld.","mode":"Normal"}} -{"Key":"("} -{"Get":{"state":"hello.\n\nˇ\nworld.","mode":"Normal"}} -{"Key":"("} -{"Get":{"state":"ˇhello.\n\n\nworld.","mode":"Normal"}} -{"Put":{"state":"hello. worlˇd."}} -{"Key":"("} -{"Get":{"state":"hello. ˇworld.","mode":"Normal"}} -{"Key":"("} -{"Get":{"state":"ˇhello. world.","mode":"Normal"}} -{"Put":{"state":". helˇlo."}} -{"Key":"("} -{"Get":{"state":". ˇhello.","mode":"Normal"}} -{"Key":"("} -{"Get":{"state":". ˇhello.","mode":"Normal"}} -{"Put":{"state":"{\n hello_world();\nˇ}"}} -{"Key":"("} -{"Get":{"state":"ˇ{\n hello_world();\n}","mode":"Normal"}} -{"Put":{"state":"Hello! World..?\n\n\tHello! World... ˇ"}} -{"Key":"("} -{"Get":{"state":"Hello! World..?\n\n\tHello! ˇWorld... ","mode":"Normal"}} -{"Key":"("} -{"Get":{"state":"Hello! World..?\n\n\tˇHello! World... ","mode":"Normal"}} -{"Key":"("} -{"Get":{"state":"Hello! World..?\nˇ\n\tHello! World... ","mode":"Normal"}} -{"Key":"("} -{"Get":{"state":"Hello! ˇWorld..?\n\n\tHello! World... ","mode":"Normal"}} diff --git a/crates/vim/test_data/test_sentence_forwards.json b/crates/vim/test_data/test_sentence_forwards.json deleted file mode 100644 index 47939aae37..0000000000 --- a/crates/vim/test_data/test_sentence_forwards.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Put":{"state":"helˇlo.\n\n\nworld."}} -{"Key":")"} -{"Get":{"state":"hello.\nˇ\n\nworld.","mode":"Normal"}} -{"Key":")"} -{"Get":{"state":"hello.\n\n\nˇworld.","mode":"Normal"}} -{"Key":")"} -{"Get":{"state":"hello.\n\n\nworldˇ.","mode":"Normal"}} -{"Put":{"state":"helˇlo.\n\n\nworld."}} diff --git a/crates/vim/test_data/test_shift_y.json b/crates/vim/test_data/test_shift_y.json deleted file mode 100644 index f68f1df18d..0000000000 --- a/crates/vim/test_data/test_shift_y.json +++ /dev/null @@ -1,7 +0,0 @@ -{"Put":{"state":"The ˇquick brown\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Key":"shift-y"} -{"Get":{"state":"ˇThe quick brown\n","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"The quick brown\n"}} diff --git a/crates/vim/test_data/test_singleline_surrounding_character_objects.json b/crates/vim/test_data/test_singleline_surrounding_character_objects.json deleted file mode 100644 index 2499786c72..0000000000 --- a/crates/vim/test_data/test_singleline_surrounding_character_objects.json +++ /dev/null @@ -1,37 +0,0 @@ -{"SetOption":{"value":"wrap"}} -{"SetOption":{"value":"columns=12"}} -{"Put":{"state":"\"ˇhello world\"!"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"\"«hello worldˇ»\"!","mode":"Visual"}} -{"Put":{"state":"\"hˇello world\"!"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"\"«hello worldˇ»\"!","mode":"Visual"}} -{"Put":{"state":"helˇlo \"world\"!"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"hello \"«worldˇ»\"!","mode":"Visual"}} -{"Put":{"state":"hello \"wˇorld\"!"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"hello \"«worldˇ»\"!","mode":"Visual"}} -{"Put":{"state":"hello \"wˇorld\"!"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"hello« \"world\"ˇ»!","mode":"Visual"}} -{"Put":{"state":"hello \"wˇorld\" !"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"hello «\"world\" ˇ»!","mode":"Visual"}} -{"Put":{"state":"hello \"wˇorld\"•\ngoodbye"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"\""} -{"Get":{"state":"hello «\"world\" ˇ»\ngoodbye","mode":"Visual"}} diff --git a/crates/vim/test_data/test_singleline_surrounding_character_objects_with_escape.json b/crates/vim/test_data/test_singleline_surrounding_character_objects_with_escape.json deleted file mode 100644 index 0de952ac91..0000000000 --- a/crates/vim/test_data/test_singleline_surrounding_character_objects_with_escape.json +++ /dev/null @@ -1,10 +0,0 @@ -{"Put":{"state":"h\"e\\\"lˇlo \\\"world\"!"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"h\"«e\\\"llo \\\"worldˇ»\"!","mode":"Visual"}} -{"Put":{"state":"hello \"teˇst \\\"inside\\\" world\""}} -{"Key":"v"} -{"Key":"i"} -{"Key":"\""} -{"Get":{"state":"hello \"«test \\\"inside\\\" worldˇ»\"","mode":"Visual"}} diff --git a/crates/vim/test_data/test_space_non_ascii.json b/crates/vim/test_data/test_space_non_ascii.json deleted file mode 100644 index 9caed6cb59..0000000000 --- a/crates/vim/test_data/test_space_non_ascii.json +++ /dev/null @@ -1,4 +0,0 @@ -{"Put":{"state":"ˇπππππ"}} -{"Key":"3"} -{"Key":"space"} -{"Get":{"state":"πππˇππ","mode":"Normal"}} diff --git a/crates/vim/test_data/test_space_non_ascii_eol.json b/crates/vim/test_data/test_space_non_ascii_eol.json deleted file mode 100644 index 6c1bb73fa5..0000000000 --- a/crates/vim/test_data/test_space_non_ascii_eol.json +++ /dev/null @@ -1,4 +0,0 @@ -{"Put":{"state":"ππππˇπ\nπanotherline"}} -{"Key":"4"} -{"Key":"space"} -{"Get":{"state":"πππππ\nπanˇotherline","mode":"Normal"}} diff --git a/crates/vim/test_data/test_space_only_ascii_eol.json b/crates/vim/test_data/test_space_only_ascii_eol.json deleted file mode 100644 index 43a3af7945..0000000000 --- a/crates/vim/test_data/test_space_only_ascii_eol.json +++ /dev/null @@ -1,4 +0,0 @@ -{"Put":{"state":"aaaaˇaa\nanotherline"}} -{"Key":"4"} -{"Key":"space"} -{"Get":{"state":"aaaaaa\nanˇotherline","mode":"Normal"}} diff --git a/crates/vim/test_data/test_special_registers.json b/crates/vim/test_data/test_special_registers.json deleted file mode 100644 index 35f181a05c..0000000000 --- a/crates/vim/test_data/test_special_registers.json +++ /dev/null @@ -1,48 +0,0 @@ -{"Put":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown\nfox ˇ over\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"-","value":"jumps"}} -{"Key":"\""} -{"Key":"_"} -{"Key":"d"} -{"Key":"d"} -{"Get":{"state":"The quick brown\nthe ˇlazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"_","value":""}} -{"Key":"shift-v"} -{"Key":"\""} -{"Key":"_"} -{"Key":"y"} -{"Key":"w"} -{"Get":{"state":"The quick brown\nthe ˇlazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"jumps"}} -{"Get":{"state":"The quick brown\nthe ˇlazy dog","mode":"Normal"}} -{"Key":"\""} -{"Key":"\""} -{"Key":"d"} -{"Key":"^"} -{"Get":{"state":"The quick brown\nˇlazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"0","value":"the "}} -{"Get":{"state":"The quick brown\nˇlazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"the "}} -{"Key":"^"} -{"Key":"\""} -{"Key":"+"} -{"Key":"d"} -{"Key":"$"} -{"Get":{"state":"The quick brown\nˇ","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"lazy dog"}} -{"Get":{"state":"The quick brown\nˇ","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"lazy dog"}} -{"Key":"/"} -{"Key":"d"} -{"Key":"o"} -{"Key":"g"} -{"Key":"enter"} -{"Get":{"state":"The quick brown\nˇ","mode":"Normal"}} -{"ReadRegister":{"name":"/","value":"dog"}} -{"Key":"\""} -{"Key":"/"} -{"Key":"shift-p"} -{"Get":{"state":"The quick brown\ndoˇg","mode":"Normal"}} diff --git a/crates/vim/test_data/test_start_end_of_paragraph.json b/crates/vim/test_data/test_start_end_of_paragraph.json deleted file mode 100644 index 0de4d84f50..0000000000 --- a/crates/vim/test_data/test_start_end_of_paragraph.json +++ /dev/null @@ -1,13 +0,0 @@ -{"Put":{"state":"ˇabc\ndef\n\nparagraph\nthe second\n\n\n\nthird and\nfinal"}} -{"Key":"}"} -{"Get":{"state":"abc\ndef\nˇ\nparagraph\nthe second\n\n\n\nthird and\nfinal","mode":"Normal"}} -{"Key":"{"} -{"Get":{"state":"ˇabc\ndef\n\nparagraph\nthe second\n\n\n\nthird and\nfinal","mode":"Normal"}} -{"Key":"2"} -{"Key":"}"} -{"Get":{"state":"abc\ndef\n\nparagraph\nthe second\nˇ\n\n\nthird and\nfinal","mode":"Normal"}} -{"Key":"}"} -{"Get":{"state":"abc\ndef\n\nparagraph\nthe second\n\n\n\nthird and\nfinaˇl","mode":"Normal"}} -{"Key":"2"} -{"Key":"{"} -{"Get":{"state":"abc\ndef\nˇ\nparagraph\nthe second\n\n\n\nthird and\nfinal","mode":"Normal"}} diff --git a/crates/vim/test_data/test_substitute_line.json b/crates/vim/test_data/test_substitute_line.json deleted file mode 100644 index eb0a9825f8..0000000000 --- a/crates/vim/test_data/test_substitute_line.json +++ /dev/null @@ -1,29 +0,0 @@ -{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog\n"}} -{"Key":"shift-s"} -{"Key":"o"} -{"Get":{"state":"The quick brown\noˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog\n"}} -{"Key":"v"} -{"Key":"k"} -{"Key":"shift-s"} -{"Key":"o"} -{"Get":{"state":"oˇ\nthe lazy dog\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog\n"}} -{"Key":"ctrl-v"} -{"Key":"j"} -{"Key":"shift-s"} -{"Key":"o"} -{"Get":{"state":"The quick brown\noˇ\n","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog\n"}} -{"Key":"v"} -{"Key":"$"} -{"Key":"shift-s"} -{"Key":"o"} -{"Get":{"state":"The quick brown\noˇ\nthe lazy dog\n","mode":"Insert"}} -{"SetOption":{"value":"shiftwidth=4"}} -{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog\n"}} -{"Key":">"} -{"Key":">"} -{"Key":"shift-s"} -{"Key":"o"} -{"Get":{"state":"The quick brown\n oˇ\nthe lazy dog\n","mode":"Insert"}} diff --git a/crates/vim/test_data/test_temporary_mode.json b/crates/vim/test_data/test_temporary_mode.json deleted file mode 100644 index be370cf744..0000000000 --- a/crates/vim/test_data/test_temporary_mode.json +++ /dev/null @@ -1,27 +0,0 @@ -{"Put":{"state":"lorem ˇipsum"}} -{"Key":"i"} -{"Get":{"state":"lorem ˇipsum","mode":"Insert"}} -{"Key":"ctrl-o"} -{"Key":"$"} -{"Get":{"state":"lorem ipsumˇ","mode":"Insert"}} -{"Put":{"state":"loremˇ ipsum dolor"}} -{"Key":"a"} -{"Get":{"state":"lorem ˇipsum dolor","mode":"Insert"}} -{"Key":"a"} -{"Key":"n"} -{"Key":"d"} -{"Key":"space"} -{"Key":"ctrl-o"} -{"Key":"w"} -{"Get":{"state":"lorem and ipsum ˇdolor","mode":"Insert"}} -{"Put":{"state":"lorem ˇipsum dolor"}} -{"Key":"i"} -{"Get":{"state":"lorem ˇipsum dolor","mode":"Insert"}} -{"Key":"a"} -{"Key":"n"} -{"Key":"d"} -{"Key":"space"} -{"Key":"ctrl-o"} -{"Key":"y"} -{"Key":"$"} -{"Get":{"state":"lorem and ˇipsum dolor","mode":"Insert"}} diff --git a/crates/vim/test_data/test_undo.json b/crates/vim/test_data/test_undo.json deleted file mode 100644 index e7f275743e..0000000000 --- a/crates/vim/test_data/test_undo.json +++ /dev/null @@ -1,45 +0,0 @@ -{"Put":{"state":"hello quˇoel world"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Key":"s"} -{"Key":"c"} -{"Key":"o"} -{"Key":"escape"} -{"Key":"u"} -{"Get":{"state":"hello ˇquoel world","mode":"Normal"}} -{"Key":"ctrl-r"} -{"Get":{"state":"hello ˇco world","mode":"Normal"}} -{"Key":"a"} -{"Key":"o"} -{"Key":"right"} -{"Key":"l"} -{"Key":"escape"} -{"Get":{"state":"hello cooˇl world","mode":"Normal"}} -{"Key":"u"} -{"Get":{"state":"hello cooˇ world","mode":"Normal"}} -{"Key":"u"} -{"Get":{"state":"hello cˇo world","mode":"Normal"}} -{"Key":"u"} -{"Get":{"state":"hello ˇquoel world","mode":"Normal"}} -{"Put":{"state":"hello quˇoel world"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Key":"~"} -{"Key":"u"} -{"Get":{"state":"hello ˇquoel world","mode":"Normal"}} -{"Put":{"state":"\nhello quˇoel world\n"}} -{"Key":"shift-v"} -{"Key":"s"} -{"Key":"c"} -{"Key":"escape"} -{"Key":"u"} -{"Get":{"state":"\nˇhello quoel world\n","mode":"Normal"}} -{"Put":{"state":"ˇ1\n2\n3"}} -{"Key":"ctrl-v"} -{"Key":"shift-g"} -{"Key":"ctrl-a"} -{"Get":{"state":"ˇ2\n3\n4","mode":"Normal"}} -{"Key":"u"} -{"Get":{"state":"ˇ1\n2\n3","mode":"Normal"}} diff --git a/crates/vim/test_data/test_undo_last_line.json b/crates/vim/test_data/test_undo_last_line.json deleted file mode 100644 index a2f6fc0995..0000000000 --- a/crates/vim/test_data/test_undo_last_line.json +++ /dev/null @@ -1,14 +0,0 @@ -{"Put":{"state":"ˇfn a() { }\nfn a() { }\nfn a() { }\n"}} -{"Key":"shift-g"} -{"Get":{"state":"fn a() { }\nfn a() { }\nfn a() { }\nˇ","mode":"Normal"}} -{"Key":"r"} -{"Key":"a"} -{"Get":{"state":"fn a() { }\nfn a() { }\nfn a() { }\nˇ","mode":"Normal"}} -{"Key":"shift-u"} -{"Get":{"state":"ˇ\nfn a() { }\nfn a() { }\n","mode":"Normal"}} -{"Key":"shift-u"} -{"Get":{"state":"ˇfn a() { }\nfn a() { }\nfn a() { }\n","mode":"Normal"}} -{"Key":"g"} -{"Key":"g"} -{"Key":"shift-u"} -{"Get":{"state":"ˇ\nfn a() { }\nfn a() { }\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_undo_last_line_newline.json b/crates/vim/test_data/test_undo_last_line_newline.json deleted file mode 100644 index 2b21ccef09..0000000000 --- a/crates/vim/test_data/test_undo_last_line_newline.json +++ /dev/null @@ -1,15 +0,0 @@ -{"Put":{"state":"ˇfn a() { }\nfn a() { }\nfn a() { }\n"}} -{"Key":"shift-g"} -{"Key":"k"} -{"Get":{"state":"fn a() { }\nfn a() { }\nˇfn a() { }\n","mode":"Normal"}} -{"Key":"o"} -{"Key":"h"} -{"Key":"e"} -{"Key":"l"} -{"Key":"l"} -{"Key":"o"} -{"Key":"escape"} -{"Get":{"state":"fn a() { }\nfn a() { }\nfn a() { }\nhellˇo\n","mode":"Normal"}} -{"Key":"shift-u"} -{"Get":{"state":"fn a() { }\nfn a() { }\nfn a() { }\nˇ\n","mode":"Normal"}} -{"Key":"shift-u"} diff --git a/crates/vim/test_data/test_undo_last_line_newline_many_changes.json b/crates/vim/test_data/test_undo_last_line_newline_many_changes.json deleted file mode 100644 index 6615e8d79a..0000000000 --- a/crates/vim/test_data/test_undo_last_line_newline_many_changes.json +++ /dev/null @@ -1,21 +0,0 @@ -{"Put":{"state":"ˇfn a() { }\nfn a() { }\nfn a() { }\n"}} -{"Key":"x"} -{"Key":"shift-g"} -{"Key":"k"} -{"Get":{"state":"n a() { }\nfn a() { }\nˇfn a() { }\n","mode":"Normal"}} -{"Key":"x"} -{"Key":"f"} -{"Key":"a"} -{"Key":"x"} -{"Key":"f"} -{"Key":"{"} -{"Key":"x"} -{"Get":{"state":"n a() { }\nfn a() { }\nn () ˇ }\n","mode":"Normal"}} -{"Key":"shift-u"} -{"Get":{"state":"n a() { }\nfn a() { }\nˇfn a() { }\n","mode":"Normal"}} -{"Key":"shift-u"} -{"Get":{"state":"n a() { }\nfn a() { }\nn () ˇ }\n","mode":"Normal"}} -{"Key":"shift-u"} -{"Get":{"state":"n a() { }\nfn a() { }\nˇfn a() { }\n","mode":"Normal"}} -{"Key":"shift-u"} -{"Get":{"state":"n a() { }\nfn a() { }\nn () ˇ }\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_undo_repeated_insert.json b/crates/vim/test_data/test_undo_repeated_insert.json deleted file mode 100644 index 98a00968c1..0000000000 --- a/crates/vim/test_data/test_undo_repeated_insert.json +++ /dev/null @@ -1,8 +0,0 @@ -{"Put":{"state":"hellˇo"}} -{"Key":"3"} -{"Key":"a"} -{"Key":"."} -{"Key":"escape"} -{"Get":{"state":"hello..ˇ.","mode":"Normal"}} -{"Key":"u"} -{"Get":{"state":"hellˇo","mode":"Normal"}} diff --git a/crates/vim/test_data/test_unmatched_backward.json b/crates/vim/test_data/test_unmatched_backward.json deleted file mode 100644 index bb3825dcd2..0000000000 --- a/crates/vim/test_data/test_unmatched_backward.json +++ /dev/null @@ -1,24 +0,0 @@ -{"Put":{"state":"func (a string) {\n do(something(with.anˇd_arrays[0, 2]))\n}"}} -{"Key":"["} -{"Key":"{"} -{"Get":{"state":"func (a string) ˇ{\n do(something(with.and_arrays[0, 2]))\n}","mode":"Normal"}} -{"Put":{"state":"func (a string) {\n do(somethiˇng(with.and_arrays[0, 2]))\n}"}} -{"Key":"["} -{"Key":"("} -{"Get":{"state":"func (a string) {\n doˇ(something(with.and_arrays[0, 2]))\n}","mode":"Normal"}} -{"Put":{"state":"{{}{} ˇ }"}} -{"Key":"["} -{"Key":"{"} -{"Get":{"state":"ˇ{{}{} }","mode":"Normal"}} -{"Put":{"state":"(()() ˇ )"}} -{"Key":"["} -{"Key":"("} -{"Get":{"state":"ˇ(()() )","mode":"Normal"}} -{"Put":{"state":"{\n {()} ˇ\n}"}} -{"Key":"["} -{"Key":"{"} -{"Get":{"state":"ˇ{\n {()} \n}","mode":"Normal"}} -{"Put":{"state":"(\n {()} ˇ\n)"}} -{"Key":"["} -{"Key":"("} -{"Get":{"state":"ˇ(\n {()} \n)","mode":"Normal"}} diff --git a/crates/vim/test_data/test_unmatched_backward_markdown.json b/crates/vim/test_data/test_unmatched_backward_markdown.json deleted file mode 100644 index c2df848b81..0000000000 --- a/crates/vim/test_data/test_unmatched_backward_markdown.json +++ /dev/null @@ -1,9 +0,0 @@ -{"Exec":{"command":"set filetype=markdown"}} -{"Put":{"state":"```rs\nimpl Worktree {\n pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {\nˇ }\n}\n```\n"}} -{"Key":"["} -{"Key":"{"} -{"Get":{"state":"```rs\nimpl Worktree {\n pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> ˇ{\n }\n}\n```\n","mode":"Normal"}} -{"Put":{"state":"```rs\nimpl Worktree {\n pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {\n } ˇ\n}\n```\n"}} -{"Key":"["} -{"Key":"{"} -{"Get":{"state":"```rs\nimpl Worktree ˇ{\n pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {\n } \n}\n```\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_unmatched_forward.json b/crates/vim/test_data/test_unmatched_forward.json deleted file mode 100644 index a6b4a38f29..0000000000 --- a/crates/vim/test_data/test_unmatched_forward.json +++ /dev/null @@ -1,28 +0,0 @@ -{"Put":{"state":"func (a string) {\n do(something(with.anˇd_arrays[0, 2]))\n}"}} -{"Key":"]"} -{"Key":"}"} -{"Get":{"state":"func (a string) {\n do(something(with.and_arrays[0, 2]))\nˇ}","mode":"Normal"}} -{"Put":{"state":"func (a string) {\n do(somethiˇng(with.and_arrays[0, 2]))\n}"}} -{"Key":"]"} -{"Key":")"} -{"Get":{"state":"func (a string) {\n do(something(with.and_arrays[0, 2])ˇ)\n}","mode":"Normal"}} -{"Put":{"state":"func (a string) { a((b, cˇ))}"}} -{"Key":"]"} -{"Key":")"} -{"Get":{"state":"func (a string) { a((b, c)ˇ)}","mode":"Normal"}} -{"Put":{"state":"{ˇ {}{}}"}} -{"Key":"]"} -{"Key":"}"} -{"Get":{"state":"{ {}{}ˇ}","mode":"Normal"}} -{"Put":{"state":"(ˇ ()())"}} -{"Key":"]"} -{"Key":")"} -{"Get":{"state":"( ()()ˇ)","mode":"Normal"}} -{"Put":{"state":"{\n ˇ {()}\n}"}} -{"Key":"]"} -{"Key":"}"} -{"Get":{"state":"{\n {()}\nˇ}","mode":"Normal"}} -{"Put":{"state":"(\n ˇ {()}\n)"}} -{"Key":"]"} -{"Key":")"} -{"Get":{"state":"(\n {()}\nˇ)","mode":"Normal"}} diff --git a/crates/vim/test_data/test_unmatched_forward_markdown.json b/crates/vim/test_data/test_unmatched_forward_markdown.json deleted file mode 100644 index 753f68d04f..0000000000 --- a/crates/vim/test_data/test_unmatched_forward_markdown.json +++ /dev/null @@ -1,9 +0,0 @@ -{"Exec":{"command":"set filetype=markdown"}} -{"Put":{"state":"```rs\nimpl Worktree {\n pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {\nˇ }\n}\n```\n"}} -{"Key":"]"} -{"Key":"}"} -{"Get":{"state":"```rs\nimpl Worktree {\n pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {\n ˇ}\n}\n```\n","mode":"Normal"}} -{"Put":{"state":"```rs\nimpl Worktree {\n pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {\n } ˇ\n}\n```\n"}} -{"Key":"]"} -{"Key":"}"} -{"Get":{"state":"```rs\nimpl Worktree {\n pub async fn open_buffers(&self, path: &Path) -> impl Iterator<&Buffer> {\n } \nˇ}\n```\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_v2ap.json b/crates/vim/test_data/test_v2ap.json deleted file mode 100644 index 7b4d31a5dc..0000000000 --- a/crates/vim/test_data/test_v2ap.json +++ /dev/null @@ -1,6 +0,0 @@ -{"Put":{"state":"The\nquicˇk\n\nbrown\nfox"}} -{"Key":"v"} -{"Key":"2"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"«The\nquick\n\nbrown\nfˇ»ox","mode":"VisualLine"}} diff --git a/crates/vim/test_data/test_v_search.json b/crates/vim/test_data/test_v_search.json deleted file mode 100644 index b8af915519..0000000000 --- a/crates/vim/test_data/test_v_search.json +++ /dev/null @@ -1,28 +0,0 @@ -{"Put":{"state":"ˇa.c. abcd a.c. abcd"}} -{"Key":"v"} -{"Key":"/"} -{"Key":"c"} -{"Key":"d"} -{"Key":"enter"} -{"Get":{"state":"«a.c. abcˇ»d a.c. abcd","mode":"Visual"}} -{"Put":{"state":"a a aˇ a a a"}} -{"Key":"v"} -{"Key":"/"} -{"Key":"a"} -{"Key":"enter"} -{"Get":{"state":"a a a« aˇ» a a","mode":"Visual"}} -{"Key":"/"} -{"Key":"enter"} -{"Get":{"state":"a a a« a aˇ» a","mode":"Visual"}} -{"Key":"?"} -{"Key":"enter"} -{"Get":{"state":"a a a« aˇ» a a","mode":"Visual"}} -{"Key":"?"} -{"Key":"enter"} -{"Get":{"state":"a a «ˇa »a a a","mode":"Visual"}} -{"Key":"/"} -{"Key":"enter"} -{"Get":{"state":"a a a« aˇ» a a","mode":"Visual"}} -{"Key":"/"} -{"Key":"enter"} -{"Get":{"state":"a a a« a aˇ» a","mode":"Visual"}} diff --git a/crates/vim/test_data/test_v_search_aa.json b/crates/vim/test_data/test_v_search_aa.json deleted file mode 100644 index 12d4f51601..0000000000 --- a/crates/vim/test_data/test_v_search_aa.json +++ /dev/null @@ -1,7 +0,0 @@ -{"Put":{"state":"ˇaa aa"}} -{"Key":"v"} -{"Key":"/"} -{"Key":"a"} -{"Key":"a"} -{"Key":"enter"} -{"Get":{"state":"«aa aˇ»a","mode":"Visual"}} diff --git a/crates/vim/test_data/test_visual_block_insert.json b/crates/vim/test_data/test_visual_block_insert.json deleted file mode 100644 index d3d2689bd3..0000000000 --- a/crates/vim/test_data/test_visual_block_insert.json +++ /dev/null @@ -1,18 +0,0 @@ -{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog\n"}} -{"Key":"ctrl-v"} -{"Key":"9"} -{"Key":"down"} -{"Get":{"state":"«Tˇ»he quick brown\n«fˇ»ox jumps over\n«tˇ»he lazy dog\nˇ","mode":"VisualBlock"}} -{"Key":"shift-i"} -{"Key":"k"} -{"Key":"escape"} -{"Get":{"state":"ˇkThe quick brown\nkfox jumps over\nkthe lazy dog\nk","mode":"Normal"}} -{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog\n"}} -{"Key":"ctrl-v"} -{"Key":"9"} -{"Key":"down"} -{"Get":{"state":"«Tˇ»he quick brown\n«fˇ»ox jumps over\n«tˇ»he lazy dog\nˇ","mode":"VisualBlock"}} -{"Key":"c"} -{"Key":"k"} -{"Key":"escape"} -{"Get":{"state":"ˇkhe quick brown\nkox jumps over\nkhe lazy dog\nk","mode":"Normal"}} diff --git a/crates/vim/test_data/test_visual_block_issue_2123.json b/crates/vim/test_data/test_visual_block_issue_2123.json deleted file mode 100644 index 0f48bcc890..0000000000 --- a/crates/vim/test_data/test_visual_block_issue_2123.json +++ /dev/null @@ -1,5 +0,0 @@ -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog\n"}} -{"Key":"ctrl-v"} -{"Key":"right"} -{"Key":"down"} -{"Get":{"state":"The «quˇ»ick brown\nfox «juˇ»mps over\nthe lazy dog\n","mode":"VisualBlock"}} diff --git a/crates/vim/test_data/test_visual_block_mode.json b/crates/vim/test_data/test_visual_block_mode.json deleted file mode 100644 index 2c5ad576c4..0000000000 --- a/crates/vim/test_data/test_visual_block_mode.json +++ /dev/null @@ -1,38 +0,0 @@ -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"ctrl-v"} -{"Get":{"state":"The «qˇ»uick brown\nfox jumps over\nthe lazy dog","mode":"VisualBlock"}} -{"Key":"2"} -{"Key":"down"} -{"Get":{"state":"The «qˇ»uick brown\nfox «jˇ»umps over\nthe «lˇ»azy dog","mode":"VisualBlock"}} -{"Key":"e"} -{"Get":{"state":"The «quicˇ»k brown\nfox «jumpˇ»s over\nthe «lazyˇ» dog","mode":"VisualBlock"}} -{"Key":"^"} -{"Get":{"state":"«ˇThe q»uick brown\n«ˇfox j»umps over\n«ˇthe l»azy dog","mode":"VisualBlock"}} -{"Key":"$"} -{"Get":{"state":"The «quick brownˇ»\nfox «jumps overˇ»\nthe «lazy dogˇ»","mode":"VisualBlock"}} -{"Key":"shift-f"} -{"Key":"space"} -{"Get":{"state":"The «quickˇ» brown\nfox «jumpsˇ» over\nthe «lazy ˇ»dog","mode":"VisualBlock"}} -{"Key":"v"} -{"Get":{"state":"The «quick brown\nfox jumps over\nthe lazy ˇ»dog","mode":"Visual"}} -{"Key":"ctrl-v"} -{"Get":{"state":"The «quickˇ» brown\nfox «jumpsˇ» over\nthe «lazy ˇ»dog","mode":"VisualBlock"}} -{"Put":{"state":"The ˇquick\nbrown\nfox\njumps over the\n\nlazy dog\n"}} -{"Key":"ctrl-v"} -{"Key":"down"} -{"Key":"down"} -{"Get":{"state":"The«ˇ q»uick\nbro«ˇwn»\nfoxˇ\njumps over the\n\nlazy dog\n","mode":"VisualBlock"}} -{"Key":"down"} -{"Get":{"state":"The «qˇ»uick\nbrow«nˇ»\nfox\njump«sˇ» over the\n\nlazy dog\n","mode":"VisualBlock"}} -{"Key":"left"} -{"Get":{"state":"The«ˇ q»uick\nbro«ˇwn»\nfoxˇ\njum«ˇps» over the\n\nlazy dog\n","mode":"VisualBlock"}} -{"Key":"s"} -{"Key":"o"} -{"Key":"escape"} -{"Get":{"state":"Theˇouick\nbroo\nfoxo\njumo over the\n\nlazy dog\n","mode":"Normal"}} -{"Put":{"state":"Theˇ quick brown\n\nfox jumps over\nthe lazy dog\n"}} -{"Key":"l"} -{"Key":"ctrl-v"} -{"Key":"j"} -{"Key":"j"} -{"Get":{"state":"The «qˇ»uick brown\n\nfox «jˇ»umps over\nthe lazy dog\n","mode":"VisualBlock"}} diff --git a/crates/vim/test_data/test_visual_block_mode_down_right.json b/crates/vim/test_data/test_visual_block_mode_down_right.json deleted file mode 100644 index fbb0558a89..0000000000 --- a/crates/vim/test_data/test_visual_block_mode_down_right.json +++ /dev/null @@ -1,9 +0,0 @@ -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"ctrl-v"} -{"Key":"l"} -{"Key":"l"} -{"Key":"l"} -{"Key":"l"} -{"Key":"l"} -{"Key":"j"} -{"Get":{"state":"The «quick ˇ»brown\nfox «jumps ˇ»over\nthe lazy dog","mode":"VisualBlock"}} diff --git a/crates/vim/test_data/test_visual_block_mode_other_end.json b/crates/vim/test_data/test_visual_block_mode_other_end.json deleted file mode 100644 index bf7956f0e8..0000000000 --- a/crates/vim/test_data/test_visual_block_mode_other_end.json +++ /dev/null @@ -1,11 +0,0 @@ -{"Put":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog"}} -{"Key":"ctrl-v"} -{"Key":"l"} -{"Key":"l"} -{"Key":"l"} -{"Key":"l"} -{"Key":"j"} -{"Get":{"state":"The quick brown\nfox j«umps ˇ»over\nthe l«azy dˇ»og","mode":"VisualBlock"}} -{"Key":"o"} -{"Key":"k"} -{"Get":{"state":"The q«ˇuick »brown\nfox j«ˇumps »over\nthe l«ˇazy d»og","mode":"VisualBlock"}} diff --git a/crates/vim/test_data/test_visual_block_mode_shift_other_end.json b/crates/vim/test_data/test_visual_block_mode_shift_other_end.json deleted file mode 100644 index 03882167b4..0000000000 --- a/crates/vim/test_data/test_visual_block_mode_shift_other_end.json +++ /dev/null @@ -1,11 +0,0 @@ -{"Put":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog"}} -{"Key":"ctrl-v"} -{"Key":"l"} -{"Key":"l"} -{"Key":"l"} -{"Key":"l"} -{"Key":"j"} -{"Get":{"state":"The quick brown\nfox j«umps ˇ»over\nthe l«azy dˇ»og","mode":"VisualBlock"}} -{"Key":"shift-o"} -{"Key":"k"} -{"Get":{"state":"The quick brown\nfox j«ˇumps »over\nthe lazy dog","mode":"VisualBlock"}} diff --git a/crates/vim/test_data/test_visual_block_mode_up_left.json b/crates/vim/test_data/test_visual_block_mode_up_left.json deleted file mode 100644 index 00bbd577c3..0000000000 --- a/crates/vim/test_data/test_visual_block_mode_up_left.json +++ /dev/null @@ -1,9 +0,0 @@ -{"Put":{"state":"The quick brown\nfox jumpsˇ over\nthe lazy dog"}} -{"Key":"ctrl-v"} -{"Key":"h"} -{"Key":"h"} -{"Key":"h"} -{"Key":"h"} -{"Key":"h"} -{"Key":"k"} -{"Get":{"state":"The «ˇquick »brown\nfox «ˇjumps »over\nthe lazy dog","mode":"VisualBlock"}} diff --git a/crates/vim/test_data/test_visual_block_search.json b/crates/vim/test_data/test_visual_block_search.json deleted file mode 100644 index 1943ce906f..0000000000 --- a/crates/vim/test_data/test_visual_block_search.json +++ /dev/null @@ -1,7 +0,0 @@ -{"Put":{"state":"ˇone two\nthree four\nfive six\n"}} -{"Key":"ctrl-v"} -{"Key":"j"} -{"Key":"/"} -{"Key":"f"} -{"Key":"enter"} -{"Get":{"state":"«one twoˇ»\n«three fˇ»our\nfive six\n","mode":"VisualBlock"}} diff --git a/crates/vim/test_data/test_visual_block_wrapping_selection.json b/crates/vim/test_data/test_visual_block_wrapping_selection.json deleted file mode 100644 index bbe945cfef..0000000000 --- a/crates/vim/test_data/test_visual_block_wrapping_selection.json +++ /dev/null @@ -1,16 +0,0 @@ -{"SetOption":{"value":"wrap"}} -{"SetOption":{"value":"columns=12"}} -{"Put":{"state":"ˇ12345678901234567890\n12345678901234567890\n12345678901234567890\n"}} -{"Key":"ctrl-v"} -{"Key":"j"} -{"Get":{"state":"«1ˇ»2345678901234567890\n«1ˇ»2345678901234567890\n12345678901234567890\n","mode":"VisualBlock"}} -{"Put":{"state":"ˇ123456789012345678901234567890123456789012345678901234567890\n1234567890123456789012345678901234567890\n12345678901234567890\n"}} -{"Key":"ctrl-v"} -{"Key":"2"} -{"Key":"j"} -{"Get":{"state":"«1ˇ»23456789012345678901234567890123456789012345678901234567890\n«1ˇ»234567890123456789012345678901234567890\n«1ˇ»2345678901234567890\n","mode":"VisualBlock"}} -{"Put":{"state":"123456789012345678901234567890123456789012345678901234567890\n1234567890123456789012345678901234567890\nˇ12345678901234567890\n"}} -{"Key":"ctrl-v"} -{"Key":"2"} -{"Key":"k"} -{"Get":{"state":"«1ˇ»23456789012345678901234567890123456789012345678901234567890\n«1ˇ»234567890123456789012345678901234567890\n«1ˇ»2345678901234567890\n","mode":"VisualBlock"}} diff --git a/crates/vim/test_data/test_visual_change.json b/crates/vim/test_data/test_visual_change.json deleted file mode 100644 index 297ad0d0d6..0000000000 --- a/crates/vim/test_data/test_visual_change.json +++ /dev/null @@ -1,47 +0,0 @@ -{"Put":{"state":"The quick ˇbrown"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"c"} -{"Get":{"state":"The quick ˇ","mode":"Insert"}} -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"j"} -{"Key":"c"} -{"Get":{"state":"The ˇver\nthe lazy dog","mode":"Insert"}} -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"j"} -{"Key":"c"} -{"Get":{"state":"The ˇver\nthe lazy dog","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"j"} -{"Key":"c"} -{"Get":{"state":"The quick brown\nfox jumps ˇhe lazy dog","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"j"} -{"Key":"c"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe ˇog","mode":"Insert"}} -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"k"} -{"Key":"c"} -{"Get":{"state":"The ˇrown\nfox jumps over\nthe lazy dog","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"k"} -{"Key":"c"} -{"Get":{"state":"The quick brown\nˇver\nthe lazy dog","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"k"} -{"Key":"c"} -{"Get":{"state":"The quick brown\nfox jumpsˇazy dog","mode":"Insert"}} diff --git a/crates/vim/test_data/test_visual_delete.json b/crates/vim/test_data/test_visual_delete.json deleted file mode 100644 index d9f8055600..0000000000 --- a/crates/vim/test_data/test_visual_delete.json +++ /dev/null @@ -1,48 +0,0 @@ -{"Put":{"state":"The quick ˇbrown"}} -{"Key":"v"} -{"Key":"w"} -{"Get":{"state":"The quick «brownˇ»","mode":"Visual"}} -{"Put":{"state":"The quick ˇbrown"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"x"} -{"Get":{"state":"The quickˇ ","mode":"Normal"}} -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"j"} -{"Key":"x"} -{"Get":{"state":"The ˇver\nthe lazy dog","mode":"Normal"}} -{"Key":"j"} -{"Key":"p"} -{"Get":{"state":"The ver\nthe lˇquick brown\nfox jumps oazy dog","mode":"Normal"}} -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"j"} -{"Key":"x"} -{"Get":{"state":"The ˇver\nthe lazy dog","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"j"} -{"Key":"x"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe ˇog","mode":"Normal"}} -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"b"} -{"Key":"k"} -{"Key":"x"} -{"Get":{"state":"ˇuick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps ˇover\nthe lazy dog"}} -{"Key":"v"} -{"Key":"b"} -{"Key":"k"} -{"Key":"x"} -{"Get":{"state":"The ˇver\nthe lazy dog","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}} -{"Key":"v"} -{"Key":"b"} -{"Key":"k"} -{"Key":"x"} -{"Get":{"state":"The quick brown\nˇazy dog","mode":"Normal"}} diff --git a/crates/vim/test_data/test_visual_line_change.json b/crates/vim/test_data/test_visual_line_change.json deleted file mode 100644 index 336108c6cb..0000000000 --- a/crates/vim/test_data/test_visual_line_change.json +++ /dev/null @@ -1,35 +0,0 @@ -{"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"c"} -{"Get":{"state":"ˇ\nfox jumps over\nthe lazy dog","mode":"Insert"}} -{"Key":"escape"} -{"Key":"j"} -{"Key":"p"} -{"Get":{"state":"\nfox jumps over\nˇThe quick brown\nthe lazy dog","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"c"} -{"Get":{"state":"The quick brown\nˇ\nthe lazy dog","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe laˇzy dog"}} -{"Key":"shift-v"} -{"Key":"c"} -{"Get":{"state":"The quick brown\nfox jumps over\nˇ","mode":"Insert"}} -{"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"j"} -{"Key":"c"} -{"Get":{"state":"ˇ\nthe lazy dog","mode":"Insert"}} -{"Key":"escape"} -{"Key":"j"} -{"Key":"p"} -{"Get":{"state":"\nthe lazy dog\nˇThe quick brown\nfox jumps over","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"j"} -{"Key":"c"} -{"Get":{"state":"The quick brown\nˇ","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe laˇzy dog"}} -{"Key":"shift-v"} -{"Key":"j"} -{"Key":"c"} -{"Get":{"state":"The quick brown\nfox jumps over\nˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_visual_line_delete.json b/crates/vim/test_data/test_visual_line_delete.json deleted file mode 100644 index e221a4ad5f..0000000000 --- a/crates/vim/test_data/test_visual_line_delete.json +++ /dev/null @@ -1,23 +0,0 @@ -{"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"x"} -{"Get":{"state":"fox juˇmps over\nthe lazy dog","mode":"Normal"}} -{"Key":"p"} -{"Get":{"state":"fox jumps over\nˇThe quick brown\nthe lazy dog","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe laˇzy dog"}} -{"Key":"shift-v"} -{"Key":"x"} -{"Get":{"state":"The quick brown\nfox juˇmps over","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"the lazy dog\n"}} -{"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"j"} -{"Key":"x"} -{"Get":{"state":"the laˇzy dog","mode":"Normal"}} -{"Key":"p"} -{"Get":{"state":"the lazy dog\nˇThe quick brown\nfox jumps over","mode":"Normal"}} -{"Put":{"state":"The ˇlong line\nshould not\ncrash\n"}} -{"Key":"shift-v"} -{"Key":"$"} -{"Key":"x"} -{"Get":{"state":"should noˇt\ncrash\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_visual_match_eol.json b/crates/vim/test_data/test_visual_match_eol.json deleted file mode 100644 index 9909638e3f..0000000000 --- a/crates/vim/test_data/test_visual_match_eol.json +++ /dev/null @@ -1,5 +0,0 @@ -{"Put":{"state":"fn aˇ() {\n return\n}\n"}} -{"Key":"v"} -{"Key":"$"} -{"Key":"%"} -{"Get":{"state":"fn a«() {\n return\n}ˇ»\n","mode":"Visual"}} diff --git a/crates/vim/test_data/test_visual_mode_insert_before_after.json b/crates/vim/test_data/test_visual_mode_insert_before_after.json deleted file mode 100644 index 065f05dea4..0000000000 --- a/crates/vim/test_data/test_visual_mode_insert_before_after.json +++ /dev/null @@ -1,14 +0,0 @@ -{"Put":{"state":"heˇllo"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Key":"shift-i"} -{"Get":{"state":"ˇhello","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"shift-i"} -{"Get":{"state":"The quick brown\nˇfox jumps over\nthe lazy dog","mode":"Insert"}} -{"Put":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"shift-a"} -{"Get":{"state":"The quick brown\nfox jˇumps over\nthe lazy dog","mode":"Insert"}} diff --git a/crates/vim/test_data/test_visual_object.json b/crates/vim/test_data/test_visual_object.json deleted file mode 100644 index 7c95a8dc73..0000000000 --- a/crates/vim/test_data/test_visual_object.json +++ /dev/null @@ -1,19 +0,0 @@ -{"Put":{"state":"hello (in [parˇens] o)"}} -{"Key":"ctrl-v"} -{"Key":"l"} -{"Key":"a"} -{"Key":"]"} -{"Get":{"state":"hello (in «[parens]ˇ» o)","mode":"Visual"}} -{"Key":"i"} -{"Key":"("} -{"Get":{"state":"hello («in [parens] oˇ»)","mode":"Visual"}} -{"Put":{"state":"hello in a wˇord again."}} -{"Key":"ctrl-v"} -{"Key":"l"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"hello in a w«ordˇ» again.","mode":"VisualBlock"}} -{"Key":"o"} -{"Key":"a"} -{"Key":"s"} -{"Get":{"state":"«ˇhello in a word» again.","mode":"VisualBlock"}} diff --git a/crates/vim/test_data/test_visual_object_expands.json b/crates/vim/test_data/test_visual_object_expands.json deleted file mode 100644 index ea285ea5be..0000000000 --- a/crates/vim/test_data/test_visual_object_expands.json +++ /dev/null @@ -1,10 +0,0 @@ -{"Put":{"state":"{\n {\n ˇ }\n}\n{\n}\n"}} -{"Key":"v"} -{"Key":"l"} -{"Get":{"state":"{\n {\n « }ˇ»\n}\n{\n}\n","mode":"Visual"}} -{"Key":"a"} -{"Key":"{"} -{"Get":{"state":"{\n «{\n }ˇ»\n}\n{\n}\n","mode":"Visual"}} -{"Key":"a"} -{"Key":"{"} -{"Get":{"state":"«{\n {\n }\n}ˇ»\n{\n}\n","mode":"Visual"}} diff --git a/crates/vim/test_data/test_visual_paragraph_object.json b/crates/vim/test_data/test_visual_paragraph_object.json deleted file mode 100644 index 604d6dc93f..0000000000 --- a/crates/vim/test_data/test_visual_paragraph_object.json +++ /dev/null @@ -1,80 +0,0 @@ -{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog.\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"«The quick brown\nfox jumps over\ntˇ»he lazy dog.\n","mode":"VisualLine"}} -{"Put":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog.\n"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"«The quick brown\nfox jumps over\nthe lazy dog.\nˇ»","mode":"VisualLine"}} -{"Put":{"state":"ˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"«\n\nˇ»The quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"VisualLine"}} -{"Put":{"state":"\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\n«The quick brown fox jumps\noˇ»ver the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"VisualLine"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n«\n\nˇ»The quick brown fox jumps\nover the lazy dog.\n","mode":"VisualLine"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n«The quick brown fox jumps\noˇ»ver the lazy dog.\n","mode":"VisualLine"}} -{"Put":{"state":"ˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"«\n\nThe quick brown fox jumps\noˇ»ver the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n","mode":"VisualLine"}} -{"Put":{"state":"\n\nˇThe quick brown fox jumps\nover the lazy dog.\n\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\n«The quick brown fox jumps\nover the lazy dog.\n\n\nˇ»The quick brown fox jumps\nover the lazy dog.\n","mode":"VisualLine"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\nˇ\n\nThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n«\n\nThe quick brown fox jumps\noˇ»ver the lazy dog.\n","mode":"VisualLine"}} -{"Put":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\nˇThe quick brown fox jumps\nover the lazy dog.\n"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"\n\nThe quick brown fox jumps\nover the lazy dog.\n\n\n«The quick brown fox jumps\nover the lazy dog.\nˇ»","mode":"VisualLine"}} -{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"«Tˇ»he quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\n\n","mode":"VisualLine"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\nˇ\nThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n«\nˇ»The quick brown fox jumps over the lazy dog.\n\n","mode":"VisualLine"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\n«Tˇ»he quick brown fox jumps over the lazy dog.\n\n","mode":"VisualLine"}} -{"Put":{"state":"ˇThe quick brown fox jumps over the lazy dog.\n\nThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"«The quick brown fox jumps over the lazy dog.\n\nˇ»The quick brown fox jumps over the lazy dog.\n\n","mode":"VisualLine"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\nˇ\nThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n«\nTˇ»he quick brown fox jumps over the lazy dog.\n\n","mode":"VisualLine"}} -{"Put":{"state":"The quick brown fox jumps over the lazy dog.\n\nˇThe quick brown fox jumps over the lazy dog.\n\n"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"The quick brown fox jumps over the lazy dog.\n\n«The quick brown fox jumps over the lazy dog.\n\nˇ»","mode":"VisualLine"}} diff --git a/crates/vim/test_data/test_visual_paragraph_object_with_soft_wrap.json b/crates/vim/test_data/test_visual_paragraph_object_with_soft_wrap.json deleted file mode 100644 index 6bfce2f955..0000000000 --- a/crates/vim/test_data/test_visual_paragraph_object_with_soft_wrap.json +++ /dev/null @@ -1,72 +0,0 @@ -{"SetOption":{"value":"wrap"}} -{"SetOption":{"value":"columns=20"}} -{"Put":{"state":"ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"«Fˇ»irst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"«ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is l»imited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\n«Sˇ»econd paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\n«ˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and s»hould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\n«Tˇ»hird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\n«ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping s»ettings.\n","mode":"VisualLine"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.ˇ\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\n«ˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.»\n","mode":"VisualLine"}} -{"Put":{"state":"ˇFirst paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"«First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇ»Second paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is ˇlimited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is «limited making it span multiple display lines.\n\nˇ»Second paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nˇSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\n«Second paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇ»Third paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and ˇshould be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and «should be handled correctly.\n\nˇ»Third paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n","mode":"VisualLine"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nˇThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\n"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\n«Third paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.\nˇ»","mode":"VisualLine"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping ˇsettings.\n"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping «settings.\nˇ»","mode":"VisualLine"}} -{"Put":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings.ˇ\n"}} -{"Key":"v"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"First paragraph with very long text that will wrap when soft wrap is enabled and line length is limited making it span multiple display lines.\n\nSecond paragraph that is also quite long and will definitely wrap under soft wrap conditions and should be handled correctly.\n\nThird paragraph with additional long text content that will also wrap when line length is constrained by the wrapping settings«.\nˇ»","mode":"VisualLine"}} diff --git a/crates/vim/test_data/test_visual_sentence_object.json b/crates/vim/test_data/test_visual_sentence_object.json deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/crates/vim/test_data/test_visual_shift_d.json b/crates/vim/test_data/test_visual_shift_d.json deleted file mode 100644 index de037d788c..0000000000 --- a/crates/vim/test_data/test_visual_shift_d.json +++ /dev/null @@ -1,10 +0,0 @@ -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog\n"}} -{"Key":"v"} -{"Key":"down"} -{"Key":"shift-d"} -{"Get":{"state":"the ˇlazy dog\n","mode":"Normal"}} -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog\n"}} -{"Key":"ctrl-v"} -{"Key":"down"} -{"Key":"shift-d"} -{"Get":{"state":"Theˇ \nfox \nthe lazy dog\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_visual_star_hash.json b/crates/vim/test_data/test_visual_star_hash.json deleted file mode 100644 index d6523c4a45..0000000000 --- a/crates/vim/test_data/test_visual_star_hash.json +++ /dev/null @@ -1,6 +0,0 @@ -{"Put":{"state":"ˇa.c. abcd a.c. abcd"}} -{"Key":"v"} -{"Key":"3"} -{"Key":"l"} -{"Key":"*"} -{"Get":{"state":"a.c. abcd ˇa.c. abcd","mode":"Normal"}} diff --git a/crates/vim/test_data/test_visual_word_object.json b/crates/vim/test_data/test_visual_word_object.json deleted file mode 100644 index 5e1a9839e9..0000000000 --- a/crates/vim/test_data/test_visual_word_object.json +++ /dev/null @@ -1,236 +0,0 @@ -{"Put":{"state":"The quick brown\nˇ\nfox"}} -{"Key":"v"} -{"Get":{"state":"The quick brown\n«\nˇ»fox","mode":"Visual"}} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown\n«\nˇ»fox","mode":"Visual"}} -{"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick «brownˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick «brownˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown« ˇ»\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox «jumpsˇ» over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox «jumpsˇ» over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps« ˇ»over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog« ˇ»\n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n«\nˇ»\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n«\nˇ»\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n«\nˇ»The-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«Theˇ»-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe«-ˇ»quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-«quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-«quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick« ˇ»brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick «brownˇ» \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown« ˇ»\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n« ˇ»\n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n« ˇ»\n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n« ˇ»fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-«jumpsˇ» over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog« ˇ»\n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n«\nˇ»","mode":"Visual"}} -{"Put":{"state":"The quick ˇbrown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick «brownˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick browˇn \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick «brownˇ» \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brownˇ \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown« ˇ»\nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox ˇjumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox «jumpsˇ» over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox juˇmps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox «jumpsˇ» over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumpsˇ over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps« ˇ»over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dogˇ \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog« ˇ»\n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \nˇ\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n«\nˇ»\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\nˇ\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n«\nˇ»\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\nˇ\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n«\nˇ»The-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThˇe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nTheˇ-quick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-ˇquick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quˇick brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\n«The-quickˇ» brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quickˇ brown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick« ˇ»brown \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick ˇbrown \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick «brownˇ» \n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brownˇ \n \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown« ˇ»\n \n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \nˇ \n \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n« ˇ»\n \n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \nˇ \n fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n« ˇ»\n fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \nˇ fox-jumps over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n« ˇ»fox-jumps over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumpˇs over\nthe lazy dog \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n «fox-jumpsˇ» over\nthe lazy dog \n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dogˇ \n\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog« ˇ»\n\n","mode":"Visual"}} -{"Put":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \nˇ\n"}} -{"Key":"v"} -{"Key":"i"} -{"Key":"shift-w"} -{"Get":{"state":"The quick brown \nfox jumps over\nthe lazy dog \n\n\n\nThe-quick brown \n \n \n fox-jumps over\nthe lazy dog \n«\nˇ»","mode":"Visual"}} diff --git a/crates/vim/test_data/test_visual_yank.json b/crates/vim/test_data/test_visual_yank.json deleted file mode 100644 index ed1ff2eb3f..0000000000 --- a/crates/vim/test_data/test_visual_yank.json +++ /dev/null @@ -1,42 +0,0 @@ -{"Put":{"state":"The quick ˇbrown"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"y"} -{"Get":{"state":"The quick ˇbrown","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"brown"}} -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"j"} -{"Key":"y"} -{"Get":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"quick brown\nfox jumps o"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}} -{"Key":"v"} -{"Key":"w"} -{"Key":"j"} -{"Key":"y"} -{"Get":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"lazy d"}} -{"Key":"shift-v"} -{"Key":"y"} -{"Get":{"state":"The quick brown\nfox jumps over\nˇthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"the lazy dog\n"}} -{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"v"} -{"Key":"b"} -{"Key":"k"} -{"Key":"y"} -{"Get":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"shift-g"} -{"Key":"shift-y"} -{"Get":{"state":"The quick brown\nˇfox jumps over\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"fox jumps over\nthe lazy dog\n"}} -{"Put":{"state":"The quick brown\nfox ˇjumps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"$"} -{"Key":"shift-y"} -{"Get":{"state":"The quick brown\nˇfox jumps over\nthe lazy dog","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"fox jumps over\n"}} diff --git a/crates/vim/test_data/test_w.json b/crates/vim/test_data/test_w.json deleted file mode 100644 index b7b3bd0e63..0000000000 --- a/crates/vim/test_data/test_w.json +++ /dev/null @@ -1,40 +0,0 @@ -{"Put":{"state":"The ˇquick-brown\n\n\nfox_jumps over\nthe"}} -{"Key":"w"} -{"Get":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Key":"w"} -{"Get":{"state":"The quick-ˇbrown\n\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Key":"w"} -{"Get":{"state":"The quick-brown\nˇ\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Key":"w"} -{"Get":{"state":"The quick-brown\n\nˇ\nfox_jumps over\nthe","mode":"Normal"}} -{"Key":"w"} -{"Get":{"state":"The quick-brown\n\n\nˇfox_jumps over\nthe","mode":"Normal"}} -{"Key":"w"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps ˇover\nthe","mode":"Normal"}} -{"Key":"w"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nˇthe","mode":"Normal"}} -{"Key":"w"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}} -{"Key":"w"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}} -{"Put":{"state":"The ˇquick-brown\n\n\nfox_jumps over\nthe"}} -{"Key":"shift-w"} -{"Get":{"state":"The quick-brown\nˇ\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quickˇ-brown\n\n\nfox_jumps over\nthe"}} -{"Key":"shift-w"} -{"Get":{"state":"The quick-brown\nˇ\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Put":{"state":"The quick-ˇbrown\n\n\nfox_jumps over\nthe"}} -{"Key":"shift-w"} -{"Get":{"state":"The quick-brown\nˇ\n\nfox_jumps over\nthe","mode":"Normal"}} -{"Key":"shift-w"} -{"Get":{"state":"The quick-brown\n\nˇ\nfox_jumps over\nthe","mode":"Normal"}} -{"Key":"shift-w"} -{"Get":{"state":"The quick-brown\n\n\nˇfox_jumps over\nthe","mode":"Normal"}} -{"Key":"shift-w"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps ˇover\nthe","mode":"Normal"}} -{"Key":"shift-w"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nˇthe","mode":"Normal"}} -{"Key":"shift-w"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}} -{"Key":"shift-w"} -{"Get":{"state":"The quick-brown\n\n\nfox_jumps over\nthˇe","mode":"Normal"}} diff --git a/crates/vim/test_data/test_window_bottom.json b/crates/vim/test_data/test_window_bottom.json deleted file mode 100644 index a4855ccb96..0000000000 --- a/crates/vim/test_data/test_window_bottom.json +++ /dev/null @@ -1,19 +0,0 @@ -{"Put":{"state":"abc\ndeˇf\nparagraph\nthe second\nthird and\nfinal"}} -{"Key":"shift-l"} -{"Get":{"state":"abc\ndef\nparagraph\nthe second\nthird and\nfiˇnal","mode":"Normal"}} -{"Put":{"state":"1 2 3\n4 5 ˇ6\n7 8 9\n"}} -{"Key":"shift-l"} -{"Get":{"state":"1 2 3\n4 5 6\n7 8 9\nˇ","mode":"Normal"}} -{"Put":{"state":"1 2 3\nˇ4 5 6\n7 8 9\n"}} -{"Key":"shift-l"} -{"Get":{"state":"1 2 3\n4 5 6\n7 8 9\nˇ","mode":"Normal"}} -{"Put":{"state":"1 2 ˇ3\n4 5 6\n7 8 9\n"}} -{"Key":"shift-l"} -{"Get":{"state":"1 2 3\n4 5 6\n7 8 9\nˇ","mode":"Normal"}} -{"Put":{"state":"ˇ1 2 3\n4 5 6\n7 8 9\n"}} -{"Key":"shift-l"} -{"Get":{"state":"1 2 3\n4 5 6\n7 8 9\nˇ","mode":"Normal"}} -{"Put":{"state":"1 2 3\n4 5 ˇ6\n7 8 9\n"}} -{"Key":"9"} -{"Key":"shift-l"} -{"Get":{"state":"1 2 ˇ3\n4 5 6\n7 8 9\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_window_middle.json b/crates/vim/test_data/test_window_middle.json deleted file mode 100644 index 91154923e4..0000000000 --- a/crates/vim/test_data/test_window_middle.json +++ /dev/null @@ -1,17 +0,0 @@ -{"Put":{"state":"abˇc\ndef\nparagraph\nthe second\nthird and\nfinal"}} -{"Key":"shift-m"} -{"Get":{"state":"abc\ndef\npaˇragraph\nthe second\nthird and\nfinal","mode":"Normal"}} -{"Put":{"state":"1 2 3\n4 5 6\n7 8 ˇ9\n"}} -{"Key":"shift-m"} -{"Get":{"state":"1 2 3\n4 5 ˇ6\n7 8 9\n","mode":"Normal"}} -{"Put":{"state":"1 2 3\n4 5 6\nˇ7 8 9\n"}} -{"Key":"shift-m"} -{"Get":{"state":"1 2 3\nˇ4 5 6\n7 8 9\n","mode":"Normal"}} -{"Put":{"state":"ˇ1 2 3\n4 5 6\n7 8 9\n"}} -{"Key":"shift-m"} -{"Get":{"state":"1 2 3\nˇ4 5 6\n7 8 9\n","mode":"Normal"}} -{"Key":"shift-m"} -{"Get":{"state":"1 2 3\nˇ4 5 6\n7 8 9\n","mode":"Normal"}} -{"Put":{"state":"1 2 3\n4 5 ˇ6\n7 8 9\n"}} -{"Key":"shift-m"} -{"Get":{"state":"1 2 3\n4 5 ˇ6\n7 8 9\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_window_top.json b/crates/vim/test_data/test_window_top.json deleted file mode 100644 index aab6bd6280..0000000000 --- a/crates/vim/test_data/test_window_top.json +++ /dev/null @@ -1,13 +0,0 @@ -{"Put":{"state":"abc\ndef\nparagraph\nthe second\nthird ˇand\nfinal"}} -{"Key":"shift-h"} -{"Get":{"state":"abˇc\ndef\nparagraph\nthe second\nthird and\nfinal","mode":"Normal"}} -{"Put":{"state":"1 2 3\n4 5 6\n7 8 ˇ9\n"}} -{"Key":"shift-h"} -{"Get":{"state":"1 2 ˇ3\n4 5 6\n7 8 9\n","mode":"Normal"}} -{"Put":{"state":"1 2 3\n4 5 6\nˇ7 8 9\n"}} -{"Key":"shift-h"} -{"Get":{"state":"ˇ1 2 3\n4 5 6\n7 8 9\n","mode":"Normal"}} -{"Put":{"state":"1 2 3\n4 5 ˇ6\n7 8 9"}} -{"Key":"9"} -{"Key":"shift-h"} -{"Get":{"state":"1 2 3\n4 5 6\n7 8 ˇ9","mode":"Normal"}} diff --git a/crates/vim/test_data/test_wrapped_delete_end_document.json b/crates/vim/test_data/test_wrapped_delete_end_document.json deleted file mode 100644 index f1dc0cb238..0000000000 --- a/crates/vim/test_data/test_wrapped_delete_end_document.json +++ /dev/null @@ -1,10 +0,0 @@ -{"SetOption":{"value":"wrap"}} -{"SetOption":{"value":"columns=12"}} -{"Put":{"state":"aaˇaaaaaaaaaaaaaaaaaa\nbbbbbbbbbbbbbbbbbbbb\ncccccccccccccccccccc"}} -{"Key":"d"} -{"Key":"shift-g"} -{"Key":"i"} -{"Key":"z"} -{"Key":"z"} -{"Key":"z"} -{"Get":{"state":"zzzˇ","mode":"Insert"}} diff --git a/crates/vim/test_data/test_wrapped_lines.json b/crates/vim/test_data/test_wrapped_lines.json deleted file mode 100644 index e5b5d0eac0..0000000000 --- a/crates/vim/test_data/test_wrapped_lines.json +++ /dev/null @@ -1,61 +0,0 @@ -{"SetOption":{"value":"wrap"}} -{"SetOption":{"value":"columns=12"}} -{"Put":{"state":"tˇwelve char twelve char\ntwelve char\n"}} -{"Key":"j"} -{"Get":{"state":"twelve char twelve char\ntˇwelve char\n","mode":"Normal"}} -{"Key":"k"} -{"Get":{"state":"tˇwelve char twelve char\ntwelve char\n","mode":"Normal"}} -{"Key":"g"} -{"Key":"j"} -{"Get":{"state":"twelve char tˇwelve char\ntwelve char\n","mode":"Normal"}} -{"Key":"g"} -{"Key":"j"} -{"Get":{"state":"twelve char twelve char\ntˇwelve char\n","mode":"Normal"}} -{"Key":"g"} -{"Key":"k"} -{"Get":{"state":"twelve char tˇwelve char\ntwelve char\n","mode":"Normal"}} -{"Key":"g"} -{"Key":"^"} -{"Get":{"state":"twelve char ˇtwelve char\ntwelve char\n","mode":"Normal"}} -{"Key":"^"} -{"Get":{"state":"ˇtwelve char twelve char\ntwelve char\n","mode":"Normal"}} -{"Key":"g"} -{"Key":"$"} -{"Get":{"state":"twelve charˇ twelve char\ntwelve char\n","mode":"Normal"}} -{"Key":"$"} -{"Get":{"state":"twelve char twelve chaˇr\ntwelve char\n","mode":"Normal"}} -{"Put":{"state":"tˇwelve char twelve char\ntwelve char\n"}} -{"Key":"enter"} -{"Get":{"state":"twelve char twelve char\nˇtwelve char\n","mode":"Normal"}} -{"Put":{"state":"twelve char\ntˇwelve char twelve char\ntwelve char\n"}} -{"Key":"o"} -{"Key":"o"} -{"Key":"escape"} -{"Get":{"state":"twelve char\ntwelve char twelve char\nˇo\ntwelve char\n","mode":"Normal"}} -{"Put":{"state":"twelve char\ntˇwelve char twelve char\ntwelve char\n"}} -{"Key":"shift-a"} -{"Key":"a"} -{"Key":"escape"} -{"Get":{"state":"twelve char\ntwelve char twelve charˇa\ntwelve char\n","mode":"Normal"}} -{"Key":"shift-i"} -{"Key":"i"} -{"Key":"escape"} -{"Get":{"state":"twelve char\nˇitwelve char twelve chara\ntwelve char\n","mode":"Normal"}} -{"Key":"shift-d"} -{"Get":{"state":"twelve char\nˇ\ntwelve char\n","mode":"Normal"}} -{"Put":{"state":"twelve char\ntwelve char tˇwelve char\ntwelve char\n"}} -{"Key":"shift-o"} -{"Key":"o"} -{"Key":"escape"} -{"Get":{"state":"twelve char\nˇo\ntwelve char twelve char\ntwelve char\n","mode":"Normal"}} -{"Put":{"state":"fourteen chaˇr\nfourteen char\n"}} -{"Key":"d"} -{"Key":"i"} -{"Key":"w"} -{"Get":{"state":"fourteenˇ \nfourteen char\n","mode":"Normal"}} -{"Key":"j"} -{"Key":"shift-f"} -{"Key":"e"} -{"Key":"f"} -{"Key":"r"} -{"Get":{"state":"fourteen \nfourteen chaˇr\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_wrapped_motions.json b/crates/vim/test_data/test_wrapped_motions.json deleted file mode 100644 index 195a58f6b5..0000000000 --- a/crates/vim/test_data/test_wrapped_motions.json +++ /dev/null @@ -1,15 +0,0 @@ -{"SetOption":{"value":"wrap"}} -{"SetOption":{"value":"columns=12"}} -{"Put":{"state":"aaˇaa\n😃😃"}} -{"Key":"j"} -{"Get":{"state":"aaaa\n😃ˇ😃","mode":"Normal"}} -{"Put":{"state":"123456789012aaˇaa\n123456789012😃😃"}} -{"Key":"j"} -{"Get":{"state":"123456789012aaaa\n123456789012😃ˇ😃","mode":"Normal"}} -{"Put":{"state":"123456789012aaˇaa\n123456789012😃😃"}} -{"Key":"j"} -{"Get":{"state":"123456789012aaaa\n123456789012😃ˇ😃","mode":"Normal"}} -{"Put":{"state":"123456789012aaaaˇaaaaaaaa123456789012\nwow\n123456789012😃😃😃😃😃😃123456789012"}} -{"Key":"j"} -{"Key":"j"} -{"Get":{"state":"123456789012aaaaaaaaaaaa123456789012\nwow\n123456789012😃😃ˇ😃😃😃😃123456789012","mode":"Normal"}} diff --git a/crates/vim/test_data/test_x.json b/crates/vim/test_data/test_x.json deleted file mode 100644 index cb7eb53472..0000000000 --- a/crates/vim/test_data/test_x.json +++ /dev/null @@ -1,12 +0,0 @@ -{"Put":{"state":"ˇTest"}} -{"Key":"x"} -{"Get":{"state":"ˇest","mode":"Normal"}} -{"Put":{"state":"Teˇst"}} -{"Key":"x"} -{"Get":{"state":"Teˇt","mode":"Normal"}} -{"Put":{"state":"Tesˇt"}} -{"Key":"x"} -{"Get":{"state":"Teˇs","mode":"Normal"}} -{"Put":{"state":"Tesˇt\ntest"}} -{"Key":"x"} -{"Get":{"state":"Teˇs\ntest","mode":"Normal"}} diff --git a/crates/vim/test_data/test_yank_line_with_trailing_newline.json b/crates/vim/test_data/test_yank_line_with_trailing_newline.json deleted file mode 100644 index 8b4438737a..0000000000 --- a/crates/vim/test_data/test_yank_line_with_trailing_newline.json +++ /dev/null @@ -1,5 +0,0 @@ -{"Put":{"state":"heˇllo\n"}} -{"Key":"y"} -{"Key":"y"} -{"Key":"p"} -{"Get":{"state":"hello\nˇhello\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_yank_line_without_trailing_newline.json b/crates/vim/test_data/test_yank_line_without_trailing_newline.json deleted file mode 100644 index a1158ff2d5..0000000000 --- a/crates/vim/test_data/test_yank_line_without_trailing_newline.json +++ /dev/null @@ -1,5 +0,0 @@ -{"Put":{"state":"heˇllo"}} -{"Key":"y"} -{"Key":"y"} -{"Key":"p"} -{"Get":{"state":"hello\nˇhello","mode":"Normal"}} diff --git a/crates/vim/test_data/test_yank_multiline_without_trailing_newline.json b/crates/vim/test_data/test_yank_multiline_without_trailing_newline.json deleted file mode 100644 index ec38e81f2e..0000000000 --- a/crates/vim/test_data/test_yank_multiline_without_trailing_newline.json +++ /dev/null @@ -1,6 +0,0 @@ -{"Put":{"state":"heˇllo\nhello"}} -{"Key":"2"} -{"Key":"y"} -{"Key":"y"} -{"Key":"p"} -{"Get":{"state":"hello\nˇhello\nhello\nhello","mode":"Normal"}} diff --git a/crates/vim/test_data/test_yank_paragraph_with_paste.json b/crates/vim/test_data/test_yank_paragraph_with_paste.json deleted file mode 100644 index d73d1f6d3b..0000000000 --- a/crates/vim/test_data/test_yank_paragraph_with_paste.json +++ /dev/null @@ -1,10 +0,0 @@ -{"Put":{"state":"first paragraph\nˇstill first\n\nsecond paragraph\nstill second\n\nthird paragraph\n"}} -{"Key":"y"} -{"Key":"a"} -{"Key":"p"} -{"Get":{"state":"ˇfirst paragraph\nstill first\n\nsecond paragraph\nstill second\n\nthird paragraph\n","mode":"Normal"}} -{"ReadRegister":{"name":"\"","value":"first paragraph\nstill first\n\n"}} -{"Key":"j"} -{"Key":"j"} -{"Key":"p"} -{"Get":{"state":"first paragraph\nstill first\n\nˇfirst paragraph\nstill first\n\nsecond paragraph\nstill second\n\nthird paragraph\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_zero.json b/crates/vim/test_data/test_zero.json deleted file mode 100644 index bc1253deb5..0000000000 --- a/crates/vim/test_data/test_zero.json +++ /dev/null @@ -1,7 +0,0 @@ -{"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}} -{"Key":"0"} -{"Get":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} -{"Key":"1"} -{"Key":"0"} -{"Key":"l"} -{"Get":{"state":"The quick ˇbrown\nfox jumps over\nthe lazy dog","mode":"Normal"}} diff --git a/crates/vim_mode_setting/Cargo.toml b/crates/vim_mode_setting/Cargo.toml deleted file mode 100644 index 0ae75d9d55..0000000000 --- a/crates/vim_mode_setting/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "vim_mode_setting" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/vim_mode_setting.rs" - -[dependencies] -settings.workspace = true diff --git a/crates/vim_mode_setting/LICENSE-GPL b/crates/vim_mode_setting/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/vim_mode_setting/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/vim_mode_setting/src/vim_mode_setting.rs b/crates/vim_mode_setting/src/vim_mode_setting.rs deleted file mode 100644 index e229913a80..0000000000 --- a/crates/vim_mode_setting/src/vim_mode_setting.rs +++ /dev/null @@ -1,25 +0,0 @@ -//! Contains the [`VimModeSetting`] and [`HelixModeSetting`] used to enable/disable Vim and Helix modes. -//! -//! This is in its own crate as we want other crates to be able to enable or -//! disable Vim/Helix modes without having to depend on the `vim` crate in its -//! entirety. - -use settings::{RegisterSetting, Settings, SettingsContent}; - -#[derive(RegisterSetting)] -pub struct VimModeSetting(pub bool); - -impl Settings for VimModeSetting { - fn from_settings(content: &SettingsContent) -> Self { - Self(content.vim_mode.unwrap()) - } -} - -#[derive(RegisterSetting)] -pub struct HelixModeSetting(pub bool); - -impl Settings for HelixModeSetting { - fn from_settings(content: &SettingsContent) -> Self { - Self(content.helix_mode.unwrap()) - } -} diff --git a/crates/watch/Cargo.toml b/crates/watch/Cargo.toml index 9d77eaedde..8f61d1f031 100644 --- a/crates/watch/Cargo.toml +++ b/crates/watch/Cargo.toml @@ -17,7 +17,8 @@ parking_lot.workspace = true [dev-dependencies] ctor.workspace = true +env_logger.workspace = true futures.workspace = true gpui = { workspace = true, features = ["test-support"] } +log.workspace = true rand.workspace = true -zlog.workspace = true diff --git a/crates/watch/src/watch.rs b/crates/watch/src/watch.rs index 71dab74820..aee602db86 100644 --- a/crates/watch/src/watch.rs +++ b/crates/watch/src/watch.rs @@ -238,7 +238,7 @@ mod tests { for _ in 0..16 { executor.simulate_random_delay().await; let id = next_id.fetch_add(1, SeqCst); - zlog::info!("sending {}", id); + log::info!("sending {}", id); tx.send(id).ok(); } closed.store(true, SeqCst); @@ -255,23 +255,23 @@ mod tests { for _ in 0..16 { executor.simulate_random_delay().await; - zlog::info!("{}: receiving", receiver_id); + log::info!("{}: receiving", receiver_id); let mut timeout = executor.simulate_random_delay().fuse(); let mut recv = pin!(rx.recv().fuse()); select_biased! { _ = timeout => { - zlog::info!("{}: dropping recv future", receiver_id); + log::info!("{}: dropping recv future", receiver_id); } result = recv => { match result { Ok(value) => { - zlog::info!("{}: received {}", receiver_id, value); + log::info!("{}: received {}", receiver_id, value); assert_eq!(value, next_id.load(SeqCst) - 1); assert_ne!(value, prev_observed_value); prev_observed_value = value; } Err(NoSenderError) => { - zlog::info!("{}: closed", receiver_id); + log::info!("{}: closed", receiver_id); assert!(closed.load(SeqCst)); break; } @@ -287,6 +287,6 @@ mod tests { #[ctor::ctor] fn init_logger() { - zlog::init_test(); + let _ = env_logger::try_init(); } } diff --git a/crates/web_search/Cargo.toml b/crates/web_search/Cargo.toml deleted file mode 100644 index d0e32e71f0..0000000000 --- a/crates/web_search/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "web_search" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/web_search.rs" - -[dependencies] -anyhow.workspace = true -cloud_llm_client.workspace = true -collections.workspace = true -gpui.workspace = true -serde.workspace = true diff --git a/crates/web_search/LICENSE-GPL b/crates/web_search/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/web_search/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/web_search/src/web_search.rs b/crates/web_search/src/web_search.rs deleted file mode 100644 index c381b91f39..0000000000 --- a/crates/web_search/src/web_search.rs +++ /dev/null @@ -1,72 +0,0 @@ -use std::sync::Arc; - -use anyhow::Result; -use cloud_llm_client::WebSearchResponse; -use collections::HashMap; -use gpui::{App, AppContext as _, Context, Entity, Global, SharedString, Task}; - -pub fn init(cx: &mut App) { - let registry = cx.new(|_cx| WebSearchRegistry::default()); - cx.set_global(GlobalWebSearchRegistry(registry)); -} - -#[derive(Clone, Eq, PartialEq, Hash, Debug, Ord, PartialOrd)] -pub struct WebSearchProviderId(pub SharedString); - -pub trait WebSearchProvider { - fn id(&self) -> WebSearchProviderId; - fn search(&self, query: String, cx: &mut App) -> Task>; -} - -struct GlobalWebSearchRegistry(Entity); - -impl Global for GlobalWebSearchRegistry {} - -#[derive(Default)] -pub struct WebSearchRegistry { - providers: HashMap>, - active_provider: Option>, -} - -impl WebSearchRegistry { - pub fn global(cx: &App) -> Entity { - cx.global::().0.clone() - } - - pub fn read_global(cx: &App) -> &Self { - cx.global::().0.read(cx) - } - - pub fn providers(&self) -> impl Iterator> { - self.providers.values() - } - - pub fn active_provider(&self) -> Option> { - self.active_provider.clone() - } - - pub fn set_active_provider(&mut self, provider: Arc) { - self.active_provider = Some(provider.clone()); - self.providers.insert(provider.id(), provider); - } - - pub fn register_provider( - &mut self, - provider: T, - _cx: &mut Context, - ) { - let id = provider.id(); - let provider = Arc::new(provider); - self.providers.insert(id, provider.clone()); - if self.active_provider.is_none() { - self.active_provider = Some(provider); - } - } - - pub fn unregister_provider(&mut self, id: WebSearchProviderId) { - self.providers.remove(&id); - if self.active_provider.as_ref().map(|provider| provider.id()) == Some(id) { - self.active_provider = None; - } - } -} diff --git a/crates/web_search_providers/Cargo.toml b/crates/web_search_providers/Cargo.toml deleted file mode 100644 index ecdca5883f..0000000000 --- a/crates/web_search_providers/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "web_search_providers" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/web_search_providers.rs" - -[dependencies] -anyhow.workspace = true -client.workspace = true -cloud_llm_client.workspace = true -futures.workspace = true -gpui.workspace = true -http_client.workspace = true -language_model.workspace = true -serde.workspace = true -serde_json.workspace = true -web_search.workspace = true diff --git a/crates/web_search_providers/LICENSE-GPL b/crates/web_search_providers/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/web_search_providers/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/web_search_providers/src/cloud.rs b/crates/web_search_providers/src/cloud.rs deleted file mode 100644 index 75ffb1da63..0000000000 --- a/crates/web_search_providers/src/cloud.rs +++ /dev/null @@ -1,120 +0,0 @@ -use std::sync::Arc; - -use anyhow::{Context as _, Result}; -use client::Client; -use cloud_llm_client::{EXPIRED_LLM_TOKEN_HEADER_NAME, WebSearchBody, WebSearchResponse}; -use futures::AsyncReadExt as _; -use gpui::{App, AppContext, Context, Entity, Subscription, Task}; -use http_client::{HttpClient, Method}; -use language_model::{LlmApiToken, RefreshLlmTokenListener}; -use web_search::{WebSearchProvider, WebSearchProviderId}; - -pub struct CloudWebSearchProvider { - state: Entity, -} - -impl CloudWebSearchProvider { - pub fn new(client: Arc, cx: &mut App) -> Self { - let state = cx.new(|cx| State::new(client, cx)); - - Self { state } - } -} - -pub struct State { - client: Arc, - llm_api_token: LlmApiToken, - _llm_token_subscription: Subscription, -} - -impl State { - pub fn new(client: Arc, cx: &mut Context) -> Self { - let refresh_llm_token_listener = RefreshLlmTokenListener::global(cx); - - Self { - client, - llm_api_token: LlmApiToken::default(), - _llm_token_subscription: cx.subscribe( - &refresh_llm_token_listener, - |this, _, _event, cx| { - let client = this.client.clone(); - let llm_api_token = this.llm_api_token.clone(); - cx.spawn(async move |_this, _cx| { - llm_api_token.refresh(&client).await?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - }, - ), - } - } -} - -pub const ZED_WEB_SEARCH_PROVIDER_ID: &str = "zed.dev"; - -impl WebSearchProvider for CloudWebSearchProvider { - fn id(&self) -> WebSearchProviderId { - WebSearchProviderId(ZED_WEB_SEARCH_PROVIDER_ID.into()) - } - - fn search(&self, query: String, cx: &mut App) -> Task> { - let state = self.state.read(cx); - let client = state.client.clone(); - let llm_api_token = state.llm_api_token.clone(); - let body = WebSearchBody { query }; - cx.background_spawn(async move { perform_web_search(client, llm_api_token, body).await }) - } -} - -async fn perform_web_search( - client: Arc, - llm_api_token: LlmApiToken, - body: WebSearchBody, -) -> Result { - const MAX_RETRIES: usize = 3; - - let http_client = &client.http_client(); - let mut retries_remaining = MAX_RETRIES; - let mut token = llm_api_token.acquire(&client).await?; - - loop { - if retries_remaining == 0 { - return Err(anyhow::anyhow!( - "error performing web search, max retries exceeded" - )); - } - - let request = http_client::Request::builder() - .method(Method::POST) - .uri(http_client.build_zed_llm_url("/web_search", &[])?.as_ref()) - .header("Content-Type", "application/json") - .header("Authorization", format!("Bearer {token}")) - .body(serde_json::to_string(&body)?.into())?; - let mut response = http_client - .send(request) - .await - .context("failed to send web search request")?; - - if response.status().is_success() { - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - return Ok(serde_json::from_str(&body)?); - } else if response - .headers() - .get(EXPIRED_LLM_TOKEN_HEADER_NAME) - .is_some() - { - token = llm_api_token.refresh(&client).await?; - retries_remaining -= 1; - } else { - // For now we will only retry if the LLM token is expired, - // not if the request failed for any other reason. - let mut body = String::new(); - response.body_mut().read_to_string(&mut body).await?; - anyhow::bail!( - "error performing web search.\nStatus: {:?}\nBody: {body}", - response.status(), - ); - } - } -} diff --git a/crates/web_search_providers/src/web_search_providers.rs b/crates/web_search_providers/src/web_search_providers.rs deleted file mode 100644 index 8ab0aee47a..0000000000 --- a/crates/web_search_providers/src/web_search_providers.rs +++ /dev/null @@ -1,56 +0,0 @@ -mod cloud; - -use client::Client; -use gpui::{App, Context, Entity}; -use language_model::LanguageModelRegistry; -use std::sync::Arc; -use web_search::{WebSearchProviderId, WebSearchRegistry}; - -pub fn init(client: Arc, cx: &mut App) { - let registry = WebSearchRegistry::global(cx); - registry.update(cx, |registry, cx| { - register_web_search_providers(registry, client, cx); - }); -} - -fn register_web_search_providers( - registry: &mut WebSearchRegistry, - client: Arc, - cx: &mut Context, -) { - register_zed_web_search_provider( - registry, - client.clone(), - &LanguageModelRegistry::global(cx), - cx, - ); - - cx.subscribe( - &LanguageModelRegistry::global(cx), - move |this, registry, event, cx| { - if let language_model::Event::DefaultModelChanged = event { - register_zed_web_search_provider(this, client.clone(), ®istry, cx) - } - }, - ) - .detach(); -} - -fn register_zed_web_search_provider( - registry: &mut WebSearchRegistry, - client: Arc, - language_model_registry: &Entity, - cx: &mut Context, -) { - let using_zed_provider = language_model_registry - .read(cx) - .default_model() - .is_some_and(|default| default.is_provided_by_zed()); - if using_zed_provider { - registry.register_provider(cloud::CloudWebSearchProvider::new(client, cx), cx) - } else { - registry.unregister_provider(WebSearchProviderId( - cloud::ZED_WEB_SEARCH_PROVIDER_ID.into(), - )); - } -} diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml deleted file mode 100644 index d5d3016ab2..0000000000 --- a/crates/workspace/Cargo.toml +++ /dev/null @@ -1,83 +0,0 @@ -[package] -name = "workspace" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/workspace.rs" -doctest = false - -[features] -test-support = [ - "call/test-support", - "client/test-support", - "http_client/test-support", - "db/test-support", - "project/test-support", - "session/test-support", - "settings/test-support", - "gpui/test-support", - "fs/test-support", -] - -[dependencies] -any_vec.workspace = true -anyhow.workspace = true -async-recursion.workspace = true -call.workspace = true -client.workspace = true -clock.workspace = true -collections.workspace = true -component.workspace = true -db.workspace = true -fs.workspace = true -futures.workspace = true -gpui.workspace = true -http_client.workspace = true -itertools.workspace = true -language.workspace = true -log.workspace = true -menu.workspace = true -node_runtime.workspace = true -parking_lot.workspace = true -postage.workspace = true -project.workspace = true -remote.workspace = true -schemars.workspace = true -serde.workspace = true -serde_json.workspace = true -session.workspace = true -settings.workspace = true -smallvec.workspace = true -sqlez.workspace = true -strum.workspace = true -task.workspace = true -telemetry.workspace = true -theme.workspace = true -ui.workspace = true -util.workspace = true -uuid.workspace = true -zed_actions.workspace = true - -[target.'cfg(target_os = "windows")'.dependencies] -windows.workspace = true - -[dev-dependencies] -call = { workspace = true, features = ["test-support"] } -client = { workspace = true, features = ["test-support"] } -dap = { workspace = true, features = ["test-support"] } -db = { workspace = true, features = ["test-support"] } -fs = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -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/LICENSE-GPL b/crates/workspace/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/workspace/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs deleted file mode 100644 index dfc341db9c..0000000000 --- a/crates/workspace/src/dock.rs +++ /dev/null @@ -1,1095 +0,0 @@ -use crate::persistence::model::DockData; -use crate::{DraggedDock, Event, ModalLayer, Pane}; -use crate::{Workspace, status_bar::StatusItemView}; -use anyhow::Context as _; -use client::proto; -use gpui::{ - Action, AnyView, App, Axis, Context, Corner, Entity, EntityId, EventEmitter, FocusHandle, - Focusable, IntoElement, KeyContext, MouseButton, MouseDownEvent, MouseUpEvent, ParentElement, - Render, SharedString, StyleRefinement, Styled, Subscription, WeakEntity, Window, deferred, div, - px, -}; -use settings::SettingsStore; -use std::sync::Arc; -use ui::{ContextMenu, Divider, DividerColor, IconButton, Tooltip, h_flex}; -use ui::{prelude::*, right_click_menu}; - -pub(crate) const RESIZE_HANDLE_SIZE: Pixels = px(6.); - -pub enum PanelEvent { - ZoomIn, - ZoomOut, - Activate, - Close, -} - -pub use proto::PanelId; - -pub trait Panel: Focusable + EventEmitter + Render + Sized { - fn persistent_name() -> &'static str; - fn panel_key() -> &'static str; - fn position(&self, window: &Window, cx: &App) -> DockPosition; - fn position_is_valid(&self, position: DockPosition) -> bool; - fn set_position(&mut self, position: DockPosition, window: &mut Window, cx: &mut Context); - fn size(&self, window: &Window, cx: &App) -> Pixels; - fn set_size(&mut self, size: Option, window: &mut Window, cx: &mut Context); - fn icon(&self, window: &Window, cx: &App) -> Option; - fn icon_tooltip(&self, window: &Window, cx: &App) -> Option<&'static str>; - fn toggle_action(&self) -> Box; - fn icon_label(&self, _window: &Window, _: &App) -> Option { - None - } - fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool { - false - } - fn starts_open(&self, _window: &Window, _cx: &App) -> bool { - false - } - fn set_zoomed(&mut self, _zoomed: bool, _window: &mut Window, _cx: &mut Context) {} - fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context) {} - fn pane(&self) -> Option> { - None - } - fn remote_id() -> Option { - None - } - fn activation_priority(&self) -> u32; - fn enabled(&self, _cx: &App) -> bool { - true - } -} - -pub trait PanelHandle: Send + Sync { - fn panel_id(&self) -> EntityId; - fn persistent_name(&self) -> &'static str; - fn panel_key(&self) -> &'static str; - fn position(&self, window: &Window, cx: &App) -> DockPosition; - fn position_is_valid(&self, position: DockPosition, cx: &App) -> bool; - fn set_position(&self, position: DockPosition, window: &mut Window, cx: &mut App); - fn is_zoomed(&self, window: &Window, cx: &App) -> bool; - fn set_zoomed(&self, zoomed: bool, window: &mut Window, cx: &mut App); - fn set_active(&self, active: bool, window: &mut Window, cx: &mut App); - fn remote_id(&self) -> Option; - fn pane(&self, cx: &App) -> Option>; - fn size(&self, window: &Window, cx: &App) -> Pixels; - fn set_size(&self, size: Option, window: &mut Window, cx: &mut App); - fn icon(&self, window: &Window, cx: &App) -> Option; - fn icon_tooltip(&self, window: &Window, cx: &App) -> Option<&'static str>; - fn toggle_action(&self, window: &Window, cx: &App) -> Box; - fn icon_label(&self, window: &Window, cx: &App) -> Option; - fn panel_focus_handle(&self, cx: &App) -> FocusHandle; - fn to_any(&self) -> AnyView; - fn activation_priority(&self, cx: &App) -> u32; - fn enabled(&self, cx: &App) -> bool; - fn move_to_next_position(&self, window: &mut Window, cx: &mut App) { - let current_position = self.position(window, cx); - let next_position = [ - DockPosition::Left, - DockPosition::Bottom, - DockPosition::Right, - ] - .into_iter() - .filter(|position| self.position_is_valid(*position, cx)) - .skip_while(|valid_position| *valid_position != current_position) - .nth(1) - .unwrap_or(DockPosition::Left); - - self.set_position(next_position, window, cx); - } -} - -impl PanelHandle for Entity -where - T: Panel, -{ - fn panel_id(&self) -> EntityId { - Entity::entity_id(self) - } - - fn persistent_name(&self) -> &'static str { - T::persistent_name() - } - - fn panel_key(&self) -> &'static str { - T::panel_key() - } - - fn position(&self, window: &Window, cx: &App) -> DockPosition { - self.read(cx).position(window, cx) - } - - fn position_is_valid(&self, position: DockPosition, cx: &App) -> bool { - self.read(cx).position_is_valid(position) - } - - fn set_position(&self, position: DockPosition, window: &mut Window, cx: &mut App) { - self.update(cx, |this, cx| this.set_position(position, window, cx)) - } - - fn is_zoomed(&self, window: &Window, cx: &App) -> bool { - self.read(cx).is_zoomed(window, cx) - } - - fn set_zoomed(&self, zoomed: bool, window: &mut Window, cx: &mut App) { - self.update(cx, |this, cx| this.set_zoomed(zoomed, window, cx)) - } - - fn set_active(&self, active: bool, window: &mut Window, cx: &mut App) { - self.update(cx, |this, cx| this.set_active(active, window, cx)) - } - - fn pane(&self, cx: &App) -> Option> { - self.read(cx).pane() - } - - fn remote_id(&self) -> Option { - T::remote_id() - } - - fn size(&self, window: &Window, cx: &App) -> Pixels { - self.read(cx).size(window, cx) - } - - fn set_size(&self, size: Option, window: &mut Window, cx: &mut App) { - self.update(cx, |this, cx| this.set_size(size, window, cx)) - } - - fn icon(&self, window: &Window, cx: &App) -> Option { - self.read(cx).icon(window, cx) - } - - fn icon_tooltip(&self, window: &Window, cx: &App) -> Option<&'static str> { - self.read(cx).icon_tooltip(window, cx) - } - - fn toggle_action(&self, _: &Window, cx: &App) -> Box { - self.read(cx).toggle_action() - } - - fn icon_label(&self, window: &Window, cx: &App) -> Option { - self.read(cx).icon_label(window, cx) - } - - fn to_any(&self) -> AnyView { - self.clone().into() - } - - fn panel_focus_handle(&self, cx: &App) -> FocusHandle { - self.read(cx).focus_handle(cx) - } - - fn activation_priority(&self, cx: &App) -> u32 { - self.read(cx).activation_priority() - } - - fn enabled(&self, cx: &App) -> bool { - self.read(cx).enabled(cx) - } -} - -impl From<&dyn PanelHandle> for AnyView { - fn from(val: &dyn PanelHandle) -> Self { - val.to_any() - } -} - -/// A container with a fixed [`DockPosition`] adjacent to a certain widown edge. -/// Can contain multiple panels and show/hide itself with all contents. -pub struct Dock { - position: DockPosition, - panel_entries: Vec, - workspace: WeakEntity, - is_open: bool, - active_panel_index: Option, - focus_handle: FocusHandle, - pub(crate) serialized_dock: Option, - zoom_layer_open: bool, - modal_layer: Entity, - _subscriptions: [Subscription; 2], -} - -impl Focusable for Dock { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum DockPosition { - Left, - Bottom, - Right, -} - -impl From for DockPosition { - fn from(value: settings::DockPosition) -> Self { - match value { - settings::DockPosition::Left => Self::Left, - settings::DockPosition::Bottom => Self::Bottom, - settings::DockPosition::Right => Self::Right, - } - } -} - -impl Into for DockPosition { - fn into(self) -> settings::DockPosition { - match self { - Self::Left => settings::DockPosition::Left, - Self::Bottom => settings::DockPosition::Bottom, - Self::Right => settings::DockPosition::Right, - } - } -} - -impl DockPosition { - fn label(&self) -> &'static str { - match self { - Self::Left => "Left", - Self::Bottom => "Bottom", - Self::Right => "Right", - } - } - - pub fn axis(&self) -> Axis { - match self { - Self::Left | Self::Right => Axis::Horizontal, - Self::Bottom => Axis::Vertical, - } - } -} - -struct PanelEntry { - panel: Arc, - _subscriptions: [Subscription; 3], -} - -pub struct PanelButtons { - dock: Entity, - _settings_subscription: Subscription, -} - -impl Dock { - pub fn new( - position: DockPosition, - modal_layer: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - let focus_handle = cx.focus_handle(); - let workspace = cx.entity(); - let dock = cx.new(|cx| { - let focus_subscription = - cx.on_focus(&focus_handle, window, |dock: &mut Dock, window, cx| { - if let Some(active_entry) = dock.active_panel_entry() { - active_entry.panel.panel_focus_handle(cx).focus(window) - } - }); - let zoom_subscription = cx.subscribe(&workspace, |dock, workspace, e: &Event, cx| { - if matches!(e, Event::ZoomChanged) { - let is_zoomed = workspace.read(cx).zoomed.is_some(); - dock.zoom_layer_open = is_zoomed; - } - }); - Self { - position, - workspace: workspace.downgrade(), - panel_entries: Default::default(), - active_panel_index: None, - is_open: false, - focus_handle: focus_handle.clone(), - _subscriptions: [focus_subscription, zoom_subscription], - serialized_dock: None, - zoom_layer_open: false, - modal_layer, - } - }); - - cx.on_focus_in(&focus_handle, window, { - let dock = dock.downgrade(); - move |workspace, window, cx| { - let Some(dock) = dock.upgrade() else { - return; - }; - let Some(panel) = dock.read(cx).active_panel() else { - return; - }; - if panel.is_zoomed(window, cx) { - workspace.zoomed = Some(panel.to_any().downgrade()); - workspace.zoomed_position = Some(position); - } else { - workspace.zoomed = None; - workspace.zoomed_position = None; - } - cx.emit(Event::ZoomChanged); - workspace.dismiss_zoomed_items_to_reveal(Some(position), window, cx); - workspace.update_active_view_for_followers(window, cx) - } - }) - .detach(); - - cx.observe_in(&dock, window, move |workspace, dock, window, cx| { - if dock.read(cx).is_open() - && let Some(panel) = dock.read(cx).active_panel() - && panel.is_zoomed(window, cx) - { - workspace.zoomed = Some(panel.to_any().downgrade()); - workspace.zoomed_position = Some(position); - cx.emit(Event::ZoomChanged); - return; - } - if workspace.zoomed_position == Some(position) { - workspace.zoomed = None; - workspace.zoomed_position = None; - cx.emit(Event::ZoomChanged); - } - }) - .detach(); - - dock - } - - pub fn position(&self) -> DockPosition { - self.position - } - - pub fn is_open(&self) -> bool { - self.is_open - } - - fn resizable(&self, cx: &App) -> bool { - !(self.zoom_layer_open || self.modal_layer.read(cx).has_active_modal()) - } - - pub fn panel(&self) -> Option> { - self.panel_entries - .iter() - .find_map(|entry| entry.panel.to_any().downcast().ok()) - } - - pub fn panel_index_for_type(&self) -> Option { - self.panel_entries - .iter() - .position(|entry| entry.panel.to_any().downcast::().is_ok()) - } - - pub fn panel_index_for_persistent_name(&self, ui_name: &str, _cx: &App) -> Option { - self.panel_entries - .iter() - .position(|entry| entry.panel.persistent_name() == ui_name) - } - - pub fn panel_index_for_proto_id(&self, panel_id: PanelId) -> Option { - self.panel_entries - .iter() - .position(|entry| entry.panel.remote_id() == Some(panel_id)) - } - - pub fn first_enabled_panel_idx(&mut self, cx: &mut Context) -> anyhow::Result { - self.panel_entries - .iter() - .position(|entry| entry.panel.enabled(cx)) - .with_context(|| { - format!( - "Couldn't find any enabled panel for the {} dock.", - self.position.label() - ) - }) - } - - fn active_panel_entry(&self) -> Option<&PanelEntry> { - self.active_panel_index - .and_then(|index| self.panel_entries.get(index)) - } - - pub fn active_panel_index(&self) -> Option { - self.active_panel_index - } - - pub fn set_open(&mut self, open: bool, window: &mut Window, cx: &mut Context) { - if open != self.is_open { - self.is_open = open; - if let Some(active_panel) = self.active_panel_entry() { - active_panel.panel.set_active(open, window, cx); - } - - cx.notify(); - } - } - - pub fn set_panel_zoomed( - &mut self, - panel: &AnyView, - zoomed: bool, - window: &mut Window, - cx: &mut Context, - ) { - for entry in &mut self.panel_entries { - if entry.panel.panel_id() == panel.entity_id() { - if zoomed != entry.panel.is_zoomed(window, cx) { - entry.panel.set_zoomed(zoomed, window, cx); - } - } else if entry.panel.is_zoomed(window, cx) { - entry.panel.set_zoomed(false, window, cx); - } - } - - self.workspace - .update(cx, |workspace, cx| { - workspace.serialize_workspace(window, cx); - }) - .ok(); - cx.notify(); - } - - pub fn zoom_out(&mut self, window: &mut Window, cx: &mut Context) { - for entry in &mut self.panel_entries { - if entry.panel.is_zoomed(window, cx) { - entry.panel.set_zoomed(false, window, cx); - } - } - } - - pub(crate) fn add_panel( - &mut self, - panel: Entity, - workspace: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) -> usize { - let subscriptions = [ - cx.observe(&panel, |_, _, cx| cx.notify()), - cx.observe_global_in::(window, { - let workspace = workspace.clone(); - let panel = panel.clone(); - - move |this, window, cx| { - let new_position = panel.read(cx).position(window, cx); - if new_position == this.position { - return; - } - - let Ok(new_dock) = workspace.update(cx, |workspace, cx| { - if panel.is_zoomed(window, cx) { - workspace.zoomed_position = Some(new_position); - } - match new_position { - DockPosition::Left => &workspace.left_dock, - DockPosition::Bottom => &workspace.bottom_dock, - DockPosition::Right => &workspace.right_dock, - } - .clone() - }) else { - return; - }; - - let was_visible = this.is_open() - && this.visible_panel().is_some_and(|active_panel| { - active_panel.panel_id() == Entity::entity_id(&panel) - }); - - this.remove_panel(&panel, window, cx); - - new_dock.update(cx, |new_dock, cx| { - new_dock.remove_panel(&panel, window, cx); - let index = - new_dock.add_panel(panel.clone(), workspace.clone(), window, cx); - if was_visible { - new_dock.set_open(true, window, cx); - new_dock.activate_panel(index, window, cx); - } - }); - } - }), - cx.subscribe_in( - &panel, - window, - move |this, panel, event, window, cx| match event { - PanelEvent::ZoomIn => { - this.set_panel_zoomed(&panel.to_any(), true, window, cx); - if !PanelHandle::panel_focus_handle(panel, cx).contains_focused(window, cx) - { - window.focus(&panel.focus_handle(cx)); - } - workspace - .update(cx, |workspace, cx| { - workspace.zoomed = Some(panel.downgrade().into()); - workspace.zoomed_position = - Some(panel.read(cx).position(window, cx)); - cx.emit(Event::ZoomChanged); - }) - .ok(); - } - PanelEvent::ZoomOut => { - this.set_panel_zoomed(&panel.to_any(), false, window, cx); - workspace - .update(cx, |workspace, cx| { - if workspace.zoomed_position == Some(this.position) { - workspace.zoomed = None; - workspace.zoomed_position = None; - cx.emit(Event::ZoomChanged); - } - cx.notify(); - }) - .ok(); - } - PanelEvent::Activate => { - if let Some(ix) = this - .panel_entries - .iter() - .position(|entry| entry.panel.panel_id() == Entity::entity_id(panel)) - { - this.set_open(true, window, cx); - this.activate_panel(ix, window, cx); - window.focus(&panel.read(cx).focus_handle(cx)); - } - } - PanelEvent::Close => { - if this - .visible_panel() - .is_some_and(|p| p.panel_id() == Entity::entity_id(panel)) - { - this.set_open(false, window, cx); - } - } - }, - ), - ]; - - let index = match self - .panel_entries - .binary_search_by_key(&panel.read(cx).activation_priority(), |entry| { - entry.panel.activation_priority(cx) - }) { - Ok(ix) => { - if cfg!(debug_assertions) { - panic!( - "Panels `{}` and `{}` have the same activation priority. Each panel must have a unique priority so the status bar order is deterministic.", - T::panel_key(), - self.panel_entries[ix].panel.panel_key() - ); - } - ix - } - Err(ix) => ix, - }; - if let Some(active_index) = self.active_panel_index.as_mut() - && *active_index >= index - { - *active_index += 1; - } - self.panel_entries.insert( - index, - PanelEntry { - panel: Arc::new(panel.clone()), - _subscriptions: subscriptions, - }, - ); - - self.restore_state(window, cx); - if panel.read(cx).starts_open(window, cx) { - self.activate_panel(index, window, cx); - self.set_open(true, window, cx); - } - - cx.notify(); - index - } - - pub fn restore_state(&mut self, window: &mut Window, cx: &mut Context) -> bool { - if let Some(serialized) = self.serialized_dock.clone() { - if let Some(active_panel) = serialized.active_panel.filter(|_| serialized.visible) - && let Some(idx) = self.panel_index_for_persistent_name(active_panel.as_str(), cx) - { - self.activate_panel(idx, window, cx); - } - - if serialized.zoom - && let Some(panel) = self.active_panel() - { - panel.set_zoomed(true, window, cx) - } - self.set_open(serialized.visible, window, cx); - return true; - } - false - } - - pub fn remove_panel( - &mut self, - panel: &Entity, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(panel_ix) = self - .panel_entries - .iter() - .position(|entry| entry.panel.panel_id() == Entity::entity_id(panel)) - { - if let Some(active_panel_index) = self.active_panel_index.as_mut() { - match panel_ix.cmp(active_panel_index) { - std::cmp::Ordering::Less => { - *active_panel_index -= 1; - } - std::cmp::Ordering::Equal => { - self.active_panel_index = None; - self.set_open(false, window, cx); - } - std::cmp::Ordering::Greater => {} - } - } - self.panel_entries.remove(panel_ix); - cx.notify(); - } - } - - pub fn panels_len(&self) -> usize { - self.panel_entries.len() - } - - pub fn activate_panel(&mut self, panel_ix: usize, window: &mut Window, cx: &mut Context) { - if Some(panel_ix) != self.active_panel_index { - if let Some(active_panel) = self.active_panel_entry() { - active_panel.panel.set_active(false, window, cx); - } - - self.active_panel_index = Some(panel_ix); - if let Some(active_panel) = self.active_panel_entry() { - active_panel.panel.set_active(true, window, cx); - } - - cx.notify(); - } - } - - pub fn visible_panel(&self) -> Option<&Arc> { - let entry = self.visible_entry()?; - Some(&entry.panel) - } - - pub fn active_panel(&self) -> Option<&Arc> { - let panel_entry = self.active_panel_entry()?; - Some(&panel_entry.panel) - } - - fn visible_entry(&self) -> Option<&PanelEntry> { - if self.is_open { - self.active_panel_entry() - } else { - None - } - } - - pub fn zoomed_panel(&self, window: &Window, cx: &App) -> Option> { - let entry = self.visible_entry()?; - if entry.panel.is_zoomed(window, cx) { - Some(entry.panel.clone()) - } else { - None - } - } - - pub fn panel_size(&self, panel: &dyn PanelHandle, window: &Window, cx: &App) -> Option { - self.panel_entries - .iter() - .find(|entry| entry.panel.panel_id() == panel.panel_id()) - .map(|entry| entry.panel.size(window, cx)) - } - - pub fn active_panel_size(&self, window: &Window, cx: &App) -> Option { - if self.is_open { - self.active_panel_entry() - .map(|entry| entry.panel.size(window, cx)) - } else { - None - } - } - - pub fn resize_active_panel( - &mut self, - size: Option, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(entry) = self.active_panel_entry() { - let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE).round()); - - entry.panel.set_size(size, window, cx); - cx.notify(); - } - } - - pub fn resize_all_panels( - &mut self, - size: Option, - window: &mut Window, - cx: &mut Context, - ) { - for entry in &mut self.panel_entries { - let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE).round()); - entry.panel.set_size(size, window, cx); - } - cx.notify(); - } - - pub fn toggle_action(&self) -> Box { - match self.position { - DockPosition::Left => crate::ToggleLeftDock.boxed_clone(), - DockPosition::Bottom => crate::ToggleBottomDock.boxed_clone(), - DockPosition::Right => crate::ToggleRightDock.boxed_clone(), - } - } - - fn dispatch_context() -> KeyContext { - let mut dispatch_context = KeyContext::new_with_defaults(); - dispatch_context.add("Dock"); - - dispatch_context - } - - pub fn clamp_panel_size(&mut self, max_size: Pixels, window: &mut Window, cx: &mut App) { - let max_size = (max_size - RESIZE_HANDLE_SIZE).abs(); - for panel in self.panel_entries.iter().map(|entry| &entry.panel) { - if panel.size(window, cx) > max_size { - panel.set_size(Some(max_size.max(RESIZE_HANDLE_SIZE)), window, cx); - } - } - } -} - -impl Render for Dock { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let dispatch_context = Self::dispatch_context(); - if let Some(entry) = self.visible_entry() { - let size = entry.panel.size(window, cx); - - let position = self.position; - let create_resize_handle = || { - let handle = div() - .id("resize-handle") - .on_drag(DraggedDock(position), |dock, _, _, cx| { - cx.stop_propagation(); - cx.new(|_| dock.clone()) - }) - .on_mouse_down( - MouseButton::Left, - cx.listener(|_, _: &MouseDownEvent, _, cx| { - cx.stop_propagation(); - }), - ) - .on_mouse_up( - MouseButton::Left, - cx.listener(|dock, e: &MouseUpEvent, window, cx| { - if e.click_count == 2 { - dock.resize_active_panel(None, window, cx); - dock.workspace - .update(cx, |workspace, cx| { - workspace.serialize_workspace(window, cx); - }) - .ok(); - cx.stop_propagation(); - } - }), - ) - .occlude(); - match self.position() { - DockPosition::Left => deferred( - handle - .absolute() - .right(-RESIZE_HANDLE_SIZE / 2.) - .top(px(0.)) - .h_full() - .w(RESIZE_HANDLE_SIZE) - .cursor_col_resize(), - ), - DockPosition::Bottom => deferred( - handle - .absolute() - .top(-RESIZE_HANDLE_SIZE / 2.) - .left(px(0.)) - .w_full() - .h(RESIZE_HANDLE_SIZE) - .cursor_row_resize(), - ), - DockPosition::Right => deferred( - handle - .absolute() - .top(px(0.)) - .left(-RESIZE_HANDLE_SIZE / 2.) - .h_full() - .w(RESIZE_HANDLE_SIZE) - .cursor_col_resize(), - ), - } - }; - - div() - .key_context(dispatch_context) - .track_focus(&self.focus_handle(cx)) - .flex() - .bg(cx.theme().colors().panel_background) - .border_color(cx.theme().colors().border) - .overflow_hidden() - .map(|this| match self.position().axis() { - Axis::Horizontal => this.w(size).h_full().flex_row(), - Axis::Vertical => this.h(size).w_full().flex_col(), - }) - .map(|this| match self.position() { - DockPosition::Left => this.border_r_1(), - DockPosition::Right => this.border_l_1(), - DockPosition::Bottom => this.border_t_1(), - }) - .child( - div() - .map(|this| match self.position().axis() { - Axis::Horizontal => this.min_w(size).h_full(), - Axis::Vertical => this.min_h(size).w_full(), - }) - .child( - entry - .panel - .to_any() - .cached(StyleRefinement::default().v_flex().size_full()), - ), - ) - .when(self.resizable(cx), |this| { - this.child(create_resize_handle()) - }) - } else { - div() - .key_context(dispatch_context) - .track_focus(&self.focus_handle(cx)) - } - } -} - -impl PanelButtons { - pub fn new(dock: Entity, cx: &mut Context) -> Self { - cx.observe(&dock, |_, _, cx| cx.notify()).detach(); - let settings_subscription = cx.observe_global::(|_, cx| cx.notify()); - Self { - dock, - _settings_subscription: settings_subscription, - } - } -} - -impl Render for PanelButtons { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let dock = self.dock.read(cx); - let active_index = dock.active_panel_index; - let is_open = dock.is_open; - let dock_position = dock.position; - - let (menu_anchor, menu_attach) = match dock.position { - DockPosition::Left => (Corner::BottomLeft, Corner::TopLeft), - DockPosition::Bottom | DockPosition::Right => (Corner::BottomRight, Corner::TopRight), - }; - - let buttons: Vec<_> = dock - .panel_entries - .iter() - .enumerate() - .filter_map(|(i, entry)| { - let icon = entry.panel.icon(window, cx)?; - let icon_tooltip = entry.panel.icon_tooltip(window, cx)?; - let name = entry.panel.persistent_name(); - let panel = entry.panel.clone(); - - let is_active_button = Some(i) == active_index && is_open; - let (action, tooltip) = if is_active_button { - let action = dock.toggle_action(); - - let tooltip: SharedString = - format!("Close {} Dock", dock.position.label()).into(); - - (action, tooltip) - } else { - let action = entry.panel.toggle_action(window, cx); - - (action, icon_tooltip.into()) - }; - - let focus_handle = dock.focus_handle(cx); - - Some( - right_click_menu(name) - .menu(move |window, cx| { - const POSITIONS: [DockPosition; 3] = [ - DockPosition::Left, - DockPosition::Right, - DockPosition::Bottom, - ]; - - ContextMenu::build(window, cx, |mut menu, _, cx| { - for position in POSITIONS { - if position != dock_position - && panel.position_is_valid(position, cx) - { - let panel = panel.clone(); - menu = menu.entry( - format!("Dock {}", position.label()), - None, - move |window, cx| { - panel.set_position(position, window, cx); - }, - ) - } - } - menu - }) - }) - .anchor(menu_anchor) - .attach(menu_attach) - .trigger(move |is_active, _window, _cx| { - IconButton::new(name, icon) - .icon_size(IconSize::Small) - .toggle_state(is_active_button) - .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) - } - }) - .when(!is_active, |this| { - this.tooltip(move |_window, cx| { - Tooltip::for_action(tooltip.clone(), &*action, cx) - }) - }) - }), - ) - }) - .collect(); - - let has_buttons = !buttons.is_empty(); - - h_flex() - .gap_1() - .when( - has_buttons && dock.position == DockPosition::Bottom, - |this| this.child(Divider::vertical().color(DividerColor::Border)), - ) - .children(buttons) - .when(has_buttons && dock.position == DockPosition::Left, |this| { - this.child(Divider::vertical().color(DividerColor::Border)) - }) - } -} - -impl StatusItemView for PanelButtons { - fn set_active_pane_item( - &mut self, - _active_pane_item: Option<&dyn crate::ItemHandle>, - _window: &mut Window, - _cx: &mut Context, - ) { - // Nothing to do, panel buttons don't depend on the active center item - } -} - -#[cfg(any(test, feature = "test-support"))] -pub mod test { - use super::*; - use gpui::{App, Context, Window, actions, div}; - - pub struct TestPanel { - pub position: DockPosition, - pub zoomed: bool, - pub active: bool, - pub focus_handle: FocusHandle, - pub size: Pixels, - pub activation_priority: u32, - } - actions!(test_only, [ToggleTestPanel]); - - impl EventEmitter for TestPanel {} - - impl TestPanel { - pub fn new(position: DockPosition, activation_priority: u32, cx: &mut App) -> Self { - Self { - position, - zoomed: false, - active: false, - focus_handle: cx.focus_handle(), - size: px(300.), - activation_priority, - } - } - } - - impl Render for TestPanel { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - div().id("test").track_focus(&self.focus_handle(cx)) - } - } - - impl Panel for TestPanel { - fn persistent_name() -> &'static str { - "TestPanel" - } - - fn panel_key() -> &'static str { - "TestPanel" - } - - fn position(&self, _window: &Window, _: &App) -> super::DockPosition { - self.position - } - - fn position_is_valid(&self, _: super::DockPosition) -> bool { - true - } - - fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context) { - self.position = position; - cx.update_global::(|_, _| {}); - } - - fn size(&self, _window: &Window, _: &App) -> Pixels { - self.size - } - - fn set_size(&mut self, size: Option, _window: &mut Window, _: &mut Context) { - self.size = size.unwrap_or(px(300.)); - } - - fn icon(&self, _window: &Window, _: &App) -> Option { - None - } - - fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> { - None - } - - fn toggle_action(&self) -> Box { - ToggleTestPanel.boxed_clone() - } - - fn is_zoomed(&self, _window: &Window, _: &App) -> bool { - self.zoomed - } - - fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, _cx: &mut Context) { - self.zoomed = zoomed; - } - - fn set_active(&mut self, active: bool, _window: &mut Window, _cx: &mut Context) { - self.active = active; - } - - fn activation_priority(&self) -> u32 { - self.activation_priority - } - } - - impl Focusable for TestPanel { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } - } -} diff --git a/crates/workspace/src/history_manager.rs b/crates/workspace/src/history_manager.rs deleted file mode 100644 index 1b80e7c012..0000000000 --- a/crates/workspace/src/history_manager.rs +++ /dev/null @@ -1,136 +0,0 @@ -use std::path::PathBuf; - -use gpui::{AppContext, Entity, Global, MenuItem}; -use smallvec::SmallVec; -use ui::App; -use util::{ResultExt, paths::PathExt}; - -use crate::{ - NewWindow, SerializedWorkspaceLocation, WORKSPACE_DB, WorkspaceId, path_list::PathList, -}; - -pub fn init(cx: &mut App) { - let manager = cx.new(|_| HistoryManager::new()); - HistoryManager::set_global(manager.clone(), cx); - HistoryManager::init(manager, cx); -} - -pub struct HistoryManager { - /// The history of workspaces that have been opened in the past, in reverse order. - /// The most recent workspace is at the end of the vector. - history: Vec, -} - -#[derive(Debug)] -pub struct HistoryManagerEntry { - pub id: WorkspaceId, - pub path: SmallVec<[PathBuf; 2]>, -} - -struct GlobalHistoryManager(Entity); - -impl Global for GlobalHistoryManager {} - -impl HistoryManager { - fn new() -> Self { - Self { - history: Vec::new(), - } - } - - fn init(this: Entity, cx: &App) { - cx.spawn(async move |cx| { - let recent_folders = WORKSPACE_DB - .recent_workspaces_on_disk() - .await - .unwrap_or_default() - .into_iter() - .rev() - .filter_map(|(id, location, paths)| { - if matches!(location, SerializedWorkspaceLocation::Local) { - Some(HistoryManagerEntry::new(id, &paths)) - } else { - None - } - }) - .collect::>(); - this.update(cx, |this, cx| { - this.history = recent_folders; - this.update_jump_list(cx); - }) - }) - .detach(); - } - - pub fn global(cx: &App) -> Option> { - cx.try_global::() - .map(|model| model.0.clone()) - } - - fn set_global(history_manager: Entity, cx: &mut App) { - cx.set_global(GlobalHistoryManager(history_manager)); - } - - pub fn update_history(&mut self, id: WorkspaceId, entry: HistoryManagerEntry, cx: &App) { - if let Some(pos) = self.history.iter().position(|e| e.id == id) { - self.history.remove(pos); - } - self.history.push(entry); - self.update_jump_list(cx); - } - - pub fn delete_history(&mut self, id: WorkspaceId, cx: &App) { - let Some(pos) = self.history.iter().position(|e| e.id == id) else { - return; - }; - self.history.remove(pos); - self.update_jump_list(cx); - } - - fn update_jump_list(&mut self, cx: &App) { - let menus = vec![MenuItem::action("New Window", NewWindow)]; - let entries = self - .history - .iter() - .rev() - .map(|entry| entry.path.clone()) - .collect::>(); - let user_removed = cx.update_jump_list(menus, entries); - self.remove_user_removed_workspaces(user_removed, cx); - } - - pub fn remove_user_removed_workspaces( - &mut self, - user_removed: Vec>, - cx: &App, - ) { - if user_removed.is_empty() { - return; - } - let mut deleted_ids = Vec::new(); - for idx in (0..self.history.len()).rev() { - if let Some(entry) = self.history.get(idx) - && user_removed.contains(&entry.path) - { - deleted_ids.push(entry.id); - self.history.remove(idx); - } - } - cx.spawn(async move |_| { - for id in deleted_ids.iter() { - WORKSPACE_DB.delete_workspace_by_id(*id).await.log_err(); - } - }) - .detach(); - } -} - -impl HistoryManagerEntry { - pub fn new(id: WorkspaceId, paths: &PathList) -> Self { - let path = paths - .ordered_paths() - .map(|path| path.compact()) - .collect::>(); - Self { id, path } - } -} diff --git a/crates/workspace/src/invalid_item_view.rs b/crates/workspace/src/invalid_item_view.rs deleted file mode 100644 index 08242a1ed0..0000000000 --- a/crates/workspace/src/invalid_item_view.rs +++ /dev/null @@ -1,114 +0,0 @@ -use std::{path::Path, sync::Arc}; - -use gpui::{EventEmitter, FocusHandle, Focusable}; -use ui::{ - App, Button, ButtonCommon, ButtonStyle, Clickable, Context, FluentBuilder, InteractiveElement, - KeyBinding, Label, LabelCommon, LabelSize, 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/image/other item fails to open. -#[derive(Debug)] -pub struct InvalidItemView { - /// Which path was attempted to open. - pub abs_path: Arc, - /// An error message, happened when opening the item. - pub error: SharedString, - is_local: bool, - focus_handle: FocusHandle, -} - -impl InvalidItemView { - 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.root_cause()).into(), - focus_handle: cx.focus_handle(), - } - } -} - -impl Item for InvalidItemView { - 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 InvalidItemView {} - -impl Focusable for InvalidItemView { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Render for InvalidItemView { - 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("InvalidItem") - .child( - h_flex().size_full().justify_center().child( - v_flex() - .justify_center() - .gap_2() - .child(h_flex().justify_center().child("Could not open file")) - .child( - h_flex() - .justify_center() - .child(Label::new(self.error.clone()).size(LabelSize::Small)), - ) - .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, cx)), - ), - ) - }), - ), - ) - } -} diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs deleted file mode 100644 index 42eb754c21..0000000000 --- a/crates/workspace/src/item.rs +++ /dev/null @@ -1,1694 +0,0 @@ -use crate::{ - CollaboratorId, DelayedDebouncedEditAction, FollowableViewRegistry, ItemNavHistory, - SerializableItemRegistry, ToolbarItemLocation, ViewId, Workspace, WorkspaceId, - invalid_item_view::InvalidItemView, - pane::{self, Pane}, - persistence::model::ItemId, - searchable::SearchableItemHandle, - workspace_settings::{AutosaveSetting, WorkspaceSettings}, -}; -use anyhow::Result; -use client::{Client, proto}; -use futures::{StreamExt, channel::mpsc}; -use gpui::{ - Action, AnyElement, AnyEntity, AnyView, App, AppContext, Context, Entity, EntityId, - EventEmitter, FocusHandle, Focusable, Font, HighlightStyle, Pixels, Point, Render, - SharedString, Task, WeakEntity, Window, -}; -use project::{Project, ProjectEntryId, ProjectPath}; -pub use settings::{ - ActivateOnClose, ClosePosition, RegisterSetting, Settings, SettingsLocation, ShowCloseButton, - ShowDiagnostics, -}; -use smallvec::SmallVec; -use std::{ - any::{Any, TypeId}, - cell::RefCell, - ops::Range, - path::Path, - rc::Rc, - sync::Arc, - time::Duration, -}; -use theme::Theme; -use ui::{Color, Icon, IntoElement, Label, LabelCommon}; -use util::ResultExt; - -pub const LEADER_UPDATE_THROTTLE: Duration = Duration::from_millis(200); - -#[derive(Clone, Copy, Debug)] -pub struct SaveOptions { - pub format: bool, - pub autosave: bool, -} - -impl Default for SaveOptions { - fn default() -> Self { - Self { - format: true, - autosave: false, - } - } -} - -#[derive(RegisterSetting)] -pub struct ItemSettings { - pub git_status: bool, - pub close_position: ClosePosition, - pub activate_on_close: ActivateOnClose, - pub file_icons: bool, - pub show_diagnostics: ShowDiagnostics, - pub show_close_button: ShowCloseButton, -} - -#[derive(RegisterSetting)] -pub struct PreviewTabsSettings { - pub enabled: bool, - pub enable_preview_from_project_panel: bool, - pub enable_preview_from_file_finder: bool, - pub enable_preview_from_multibuffer: bool, - pub enable_preview_multibuffer_from_code_navigation: bool, - pub enable_preview_file_from_code_navigation: bool, - pub enable_keep_preview_on_code_navigation: bool, -} - -impl Settings for ItemSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - let tabs = content.tabs.as_ref().unwrap(); - Self { - git_status: tabs.git_status.unwrap(), - close_position: tabs.close_position.unwrap(), - activate_on_close: tabs.activate_on_close.unwrap(), - file_icons: tabs.file_icons.unwrap(), - show_diagnostics: tabs.show_diagnostics.unwrap(), - show_close_button: tabs.show_close_button.unwrap(), - } - } -} - -impl Settings for PreviewTabsSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - let preview_tabs = content.preview_tabs.as_ref().unwrap(); - Self { - enabled: preview_tabs.enabled.unwrap(), - enable_preview_from_project_panel: preview_tabs - .enable_preview_from_project_panel - .unwrap(), - enable_preview_from_file_finder: preview_tabs.enable_preview_from_file_finder.unwrap(), - enable_preview_from_multibuffer: preview_tabs.enable_preview_from_multibuffer.unwrap(), - enable_preview_multibuffer_from_code_navigation: preview_tabs - .enable_preview_multibuffer_from_code_navigation - .unwrap(), - enable_preview_file_from_code_navigation: preview_tabs - .enable_preview_file_from_code_navigation - .unwrap(), - enable_keep_preview_on_code_navigation: preview_tabs - .enable_keep_preview_on_code_navigation - .unwrap(), - } - } -} - -#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)] -pub enum ItemEvent { - CloseItem, - UpdateTab, - UpdateBreadcrumbs, - Edit, -} - -// TODO: Combine this with existing HighlightedText struct? -pub struct BreadcrumbText { - pub text: String, - pub highlights: Option, HighlightStyle)>>, - pub font: Option, -} - -#[derive(Clone, Copy, Default, Debug)] -pub struct TabContentParams { - pub detail: Option, - pub selected: bool, - pub preview: bool, - /// Tab content should be deemphasized when active pane does not have focus. - pub deemphasized: bool, -} - -impl TabContentParams { - /// Returns the text color to be used for the tab content. - pub fn text_color(&self) -> Color { - if self.deemphasized { - if self.selected { - Color::Muted - } else { - Color::Hidden - } - } else if self.selected { - Color::Default - } else { - Color::Muted - } - } -} - -pub enum TabTooltipContent { - Text(SharedString), - Custom(Box AnyView>), -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum ItemBufferKind { - Multibuffer, - Singleton, - None, -} - -pub trait Item: Focusable + EventEmitter + Render + Sized { - type Event; - - /// Returns the tab contents. - /// - /// By default this returns a [`Label`] that displays that text from - /// `tab_content_text`. - fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement { - let text = self.tab_content_text(params.detail.unwrap_or_default(), cx); - - Label::new(text) - .color(params.text_color()) - .into_any_element() - } - - /// Returns the textual contents of the tab. - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString; - - /// Returns the suggested filename for saving this item. - /// By default, returns the tab content text. - fn suggested_filename(&self, cx: &App) -> SharedString { - self.tab_content_text(0, cx) - } - - fn tab_icon(&self, _window: &Window, _cx: &App) -> Option { - None - } - - /// Returns the tab tooltip text. - /// - /// Use this if you don't need to customize the tab tooltip content. - fn tab_tooltip_text(&self, _: &App) -> Option { - None - } - - /// Returns the tab tooltip content. - /// - /// By default this returns a Tooltip text from - /// `tab_tooltip_text`. - fn tab_tooltip_content(&self, cx: &App) -> Option { - self.tab_tooltip_text(cx).map(TabTooltipContent::Text) - } - - fn to_item_events(_event: &Self::Event, _f: impl FnMut(ItemEvent)) {} - - fn deactivated(&mut self, _window: &mut Window, _: &mut Context) {} - fn discarded(&self, _project: Entity, _window: &mut Window, _cx: &mut Context) {} - fn on_removed(&self, _cx: &App) {} - fn workspace_deactivated(&mut self, _window: &mut Window, _: &mut Context) {} - fn navigate(&mut self, _: Box, _window: &mut Window, _: &mut Context) -> bool { - false - } - - fn telemetry_event_text(&self) -> Option<&'static str> { - None - } - - /// (model id, Item) - fn for_each_project_item( - &self, - _: &App, - _: &mut dyn FnMut(EntityId, &dyn project::ProjectItem), - ) { - } - fn buffer_kind(&self, _cx: &App) -> ItemBufferKind { - ItemBufferKind::None - } - fn set_nav_history(&mut self, _: ItemNavHistory, _window: &mut Window, _: &mut Context) {} - - fn can_split(&self) -> bool { - false - } - fn clone_on_split( - &self, - workspace_id: Option, - window: &mut Window, - cx: &mut Context, - ) -> Task>> - where - Self: Sized, - { - _ = (workspace_id, window, cx); - unimplemented!("clone_on_split() must be implemented if can_split() returns true") - } - fn is_dirty(&self, _: &App) -> bool { - false - } - fn has_deleted_file(&self, _: &App) -> bool { - false - } - fn has_conflict(&self, _: &App) -> bool { - false - } - fn can_save(&self, _cx: &App) -> bool { - false - } - fn can_save_as(&self, _: &App) -> bool { - false - } - fn save( - &mut self, - _options: SaveOptions, - _project: Entity, - _window: &mut Window, - _cx: &mut Context, - ) -> Task> { - unimplemented!("save() must be implemented if can_save() returns true") - } - fn save_as( - &mut self, - _project: Entity, - _path: ProjectPath, - _window: &mut Window, - _cx: &mut Context, - ) -> Task> { - unimplemented!("save_as() must be implemented if can_save() returns true") - } - fn reload( - &mut self, - _project: Entity, - _window: &mut Window, - _cx: &mut Context, - ) -> Task> { - unimplemented!("reload() must be implemented if can_save() returns true") - } - - fn act_as_type<'a>( - &'a self, - type_id: TypeId, - self_handle: &'a Entity, - _: &'a App, - ) -> Option { - if TypeId::of::() == type_id { - Some(self_handle.clone().into()) - } else { - None - } - } - - fn as_searchable(&self, _: &Entity, _: &App) -> Option> { - None - } - - fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation { - ToolbarItemLocation::Hidden - } - - fn breadcrumbs(&self, _theme: &Theme, _cx: &App) -> Option> { - None - } - - /// Returns optional elements to render to the left of the breadcrumb. - fn breadcrumb_prefix( - &self, - _window: &mut Window, - _cx: &mut Context, - ) -> Option { - None - } - - fn added_to_workspace( - &mut self, - _workspace: &mut Workspace, - _window: &mut Window, - _cx: &mut Context, - ) { - } - - fn show_toolbar(&self) -> bool { - true - } - - fn pixel_position_of_cursor(&self, _: &App) -> Option> { - None - } - - fn preserve_preview(&self, _cx: &App) -> bool { - false - } - - fn include_in_nav_history() -> bool { - true - } -} - -pub trait SerializableItem: Item { - fn serialized_item_kind() -> &'static str; - - fn cleanup( - workspace_id: WorkspaceId, - alive_items: Vec, - window: &mut Window, - cx: &mut App, - ) -> Task>; - - fn deserialize( - _project: Entity, - _workspace: WeakEntity, - _workspace_id: WorkspaceId, - _item_id: ItemId, - _window: &mut Window, - _cx: &mut App, - ) -> Task>>; - - fn serialize( - &mut self, - workspace: &mut Workspace, - item_id: ItemId, - closing: bool, - window: &mut Window, - cx: &mut Context, - ) -> Option>>; - - fn should_serialize(&self, event: &Self::Event) -> bool; -} - -pub trait SerializableItemHandle: ItemHandle { - fn serialized_item_kind(&self) -> &'static str; - fn serialize( - &self, - workspace: &mut Workspace, - closing: bool, - window: &mut Window, - cx: &mut App, - ) -> Option>>; - fn should_serialize(&self, event: &dyn Any, cx: &App) -> bool; -} - -impl SerializableItemHandle for Entity -where - T: SerializableItem, -{ - fn serialized_item_kind(&self) -> &'static str { - T::serialized_item_kind() - } - - fn serialize( - &self, - workspace: &mut Workspace, - closing: bool, - window: &mut Window, - cx: &mut App, - ) -> Option>> { - self.update(cx, |this, cx| { - this.serialize(workspace, cx.entity_id().as_u64(), closing, window, cx) - }) - } - - fn should_serialize(&self, event: &dyn Any, cx: &App) -> bool { - event - .downcast_ref::() - .is_some_and(|event| self.read(cx).should_serialize(event)) - } -} - -pub trait ItemHandle: 'static + Send { - fn item_focus_handle(&self, cx: &App) -> FocusHandle; - fn subscribe_to_item_events( - &self, - window: &mut Window, - cx: &mut App, - handler: Box, - ) -> gpui::Subscription; - fn tab_content(&self, params: TabContentParams, window: &Window, cx: &App) -> AnyElement; - fn tab_content_text(&self, detail: usize, cx: &App) -> SharedString; - fn suggested_filename(&self, cx: &App) -> SharedString; - fn tab_icon(&self, window: &Window, cx: &App) -> Option; - fn tab_tooltip_text(&self, cx: &App) -> Option; - fn tab_tooltip_content(&self, cx: &App) -> Option; - fn telemetry_event_text(&self, cx: &App) -> Option<&'static str>; - fn dragged_tab_content( - &self, - params: TabContentParams, - window: &Window, - cx: &App, - ) -> AnyElement; - fn project_path(&self, cx: &App) -> Option; - fn project_entry_ids(&self, cx: &App) -> SmallVec<[ProjectEntryId; 3]>; - fn project_paths(&self, cx: &App) -> SmallVec<[ProjectPath; 3]>; - fn project_item_model_ids(&self, cx: &App) -> SmallVec<[EntityId; 3]>; - fn for_each_project_item( - &self, - _: &App, - _: &mut dyn FnMut(EntityId, &dyn project::ProjectItem), - ); - fn buffer_kind(&self, cx: &App) -> ItemBufferKind; - fn boxed_clone(&self) -> Box; - fn can_split(&self, cx: &App) -> bool; - fn clone_on_split( - &self, - workspace_id: Option, - window: &mut Window, - cx: &mut App, - ) -> Task>>; - fn added_to_pane( - &self, - workspace: &mut Workspace, - pane: Entity, - window: &mut Window, - cx: &mut Context, - ); - fn deactivated(&self, window: &mut Window, cx: &mut App); - fn on_removed(&self, cx: &App); - fn workspace_deactivated(&self, window: &mut Window, cx: &mut App); - fn navigate(&self, data: Box, window: &mut Window, cx: &mut App) -> bool; - fn item_id(&self) -> EntityId; - fn to_any_view(&self) -> AnyView; - fn is_dirty(&self, cx: &App) -> bool; - fn has_deleted_file(&self, cx: &App) -> bool; - fn has_conflict(&self, cx: &App) -> bool; - fn can_save(&self, cx: &App) -> bool; - fn can_save_as(&self, cx: &App) -> bool; - fn save( - &self, - options: SaveOptions, - project: Entity, - window: &mut Window, - cx: &mut App, - ) -> Task>; - fn save_as( - &self, - project: Entity, - path: ProjectPath, - window: &mut Window, - cx: &mut App, - ) -> Task>; - fn reload( - &self, - project: Entity, - window: &mut Window, - cx: &mut App, - ) -> Task>; - fn act_as_type(&self, type_id: TypeId, cx: &App) -> Option; - fn to_followable_item_handle(&self, cx: &App) -> Option>; - fn to_serializable_item_handle(&self, cx: &App) -> Option>; - fn on_release( - &self, - cx: &mut App, - callback: Box, - ) -> gpui::Subscription; - fn to_searchable_item_handle(&self, cx: &App) -> Option>; - fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation; - fn breadcrumbs(&self, theme: &Theme, cx: &App) -> Option>; - fn breadcrumb_prefix(&self, window: &mut Window, cx: &mut App) -> Option; - fn show_toolbar(&self, cx: &App) -> bool; - fn pixel_position_of_cursor(&self, cx: &App) -> Option>; - fn downgrade_item(&self) -> Box; - fn workspace_settings<'a>(&self, cx: &'a App) -> &'a WorkspaceSettings; - fn preserve_preview(&self, cx: &App) -> bool; - fn include_in_nav_history(&self) -> bool; - fn relay_action(&self, action: Box, window: &mut Window, cx: &mut App); - fn can_autosave(&self, cx: &App) -> bool { - let is_deleted = self.project_entry_ids(cx).is_empty(); - self.is_dirty(cx) && !self.has_conflict(cx) && self.can_save(cx) && !is_deleted - } -} - -pub trait WeakItemHandle: Send + Sync { - fn id(&self) -> EntityId; - fn boxed_clone(&self) -> Box; - fn upgrade(&self) -> Option>; -} - -impl dyn ItemHandle { - pub fn downcast(&self) -> Option> { - self.to_any_view().downcast().ok() - } - - pub fn act_as(&self, cx: &App) -> Option> { - self.act_as_type(TypeId::of::(), cx) - .and_then(|t| t.downcast().ok()) - } -} - -impl ItemHandle for Entity { - fn subscribe_to_item_events( - &self, - window: &mut Window, - cx: &mut App, - handler: Box, - ) -> gpui::Subscription { - window.subscribe(self, cx, move |_, event, window, cx| { - T::to_item_events(event, |item_event| handler(item_event, window, cx)); - }) - } - - fn item_focus_handle(&self, cx: &App) -> FocusHandle { - self.read(cx).focus_handle(cx) - } - - fn telemetry_event_text(&self, cx: &App) -> Option<&'static str> { - self.read(cx).telemetry_event_text() - } - - fn tab_content(&self, params: TabContentParams, window: &Window, cx: &App) -> AnyElement { - self.read(cx).tab_content(params, window, cx) - } - fn tab_content_text(&self, detail: usize, cx: &App) -> SharedString { - self.read(cx).tab_content_text(detail, cx) - } - - fn suggested_filename(&self, cx: &App) -> SharedString { - self.read(cx).suggested_filename(cx) - } - - fn tab_icon(&self, window: &Window, cx: &App) -> Option { - self.read(cx).tab_icon(window, cx) - } - - fn tab_tooltip_content(&self, cx: &App) -> Option { - self.read(cx).tab_tooltip_content(cx) - } - - fn tab_tooltip_text(&self, cx: &App) -> Option { - self.read(cx).tab_tooltip_text(cx) - } - - fn dragged_tab_content( - &self, - params: TabContentParams, - window: &Window, - cx: &App, - ) -> AnyElement { - self.read(cx).tab_content( - TabContentParams { - selected: true, - ..params - }, - window, - cx, - ) - } - - fn project_path(&self, cx: &App) -> Option { - let this = self.read(cx); - let mut result = None; - if this.buffer_kind(cx) == ItemBufferKind::Singleton { - this.for_each_project_item(cx, &mut |_, item| { - result = item.project_path(cx); - }); - } - result - } - - fn workspace_settings<'a>(&self, cx: &'a App) -> &'a WorkspaceSettings { - if let Some(project_path) = self.project_path(cx) { - WorkspaceSettings::get( - Some(SettingsLocation { - worktree_id: project_path.worktree_id, - path: &project_path.path, - }), - cx, - ) - } else { - WorkspaceSettings::get_global(cx) - } - } - - fn project_entry_ids(&self, cx: &App) -> SmallVec<[ProjectEntryId; 3]> { - let mut result = SmallVec::new(); - self.read(cx).for_each_project_item(cx, &mut |_, item| { - if let Some(id) = item.entry_id(cx) { - result.push(id); - } - }); - result - } - - fn project_paths(&self, cx: &App) -> SmallVec<[ProjectPath; 3]> { - let mut result = SmallVec::new(); - self.read(cx).for_each_project_item(cx, &mut |_, item| { - if let Some(id) = item.project_path(cx) { - result.push(id); - } - }); - result - } - - fn project_item_model_ids(&self, cx: &App) -> SmallVec<[EntityId; 3]> { - let mut result = SmallVec::new(); - self.read(cx).for_each_project_item(cx, &mut |id, _| { - result.push(id); - }); - result - } - - fn for_each_project_item( - &self, - cx: &App, - f: &mut dyn FnMut(EntityId, &dyn project::ProjectItem), - ) { - self.read(cx).for_each_project_item(cx, f) - } - - fn buffer_kind(&self, cx: &App) -> ItemBufferKind { - self.read(cx).buffer_kind(cx) - } - - fn boxed_clone(&self) -> Box { - Box::new(self.clone()) - } - - fn can_split(&self, cx: &App) -> bool { - self.read(cx).can_split() - } - - fn clone_on_split( - &self, - workspace_id: Option, - window: &mut Window, - cx: &mut App, - ) -> Task>> { - let task = self.update(cx, |item, cx| item.clone_on_split(workspace_id, window, cx)); - cx.background_spawn(async move { - task.await - .map(|handle| Box::new(handle) as Box) - }) - } - - fn added_to_pane( - &self, - workspace: &mut Workspace, - pane: Entity, - window: &mut Window, - cx: &mut Context, - ) { - let weak_item = self.downgrade(); - let history = pane.read(cx).nav_history_for_item(self); - self.update(cx, |this, cx| { - this.set_nav_history(history, window, cx); - this.added_to_workspace(workspace, window, cx); - }); - - if let Some(serializable_item) = self.to_serializable_item_handle(cx) { - workspace - .enqueue_item_serialization(serializable_item) - .log_err(); - } - - if workspace - .panes_by_item - .insert(self.item_id(), pane.downgrade()) - .is_none() - { - let mut pending_autosave = DelayedDebouncedEditAction::new(); - let (pending_update_tx, mut pending_update_rx) = mpsc::unbounded(); - let pending_update = Rc::new(RefCell::new(None)); - - let mut send_follower_updates = None; - if let Some(item) = self.to_followable_item_handle(cx) { - let is_project_item = item.is_project_item(window, cx); - let item = item.downgrade(); - - send_follower_updates = Some(cx.spawn_in(window, { - let pending_update = pending_update.clone(); - async move |workspace, cx| { - while let Some(mut leader_id) = pending_update_rx.next().await { - while let Ok(Some(id)) = pending_update_rx.try_next() { - leader_id = id; - } - - workspace.update_in(cx, |workspace, window, cx| { - let Some(item) = item.upgrade() else { return }; - workspace.update_followers( - is_project_item, - proto::update_followers::Variant::UpdateView( - proto::UpdateView { - id: item - .remote_id(workspace.client(), window, cx) - .and_then(|id| id.to_proto()), - variant: pending_update.borrow_mut().take(), - leader_id, - }, - ), - window, - cx, - ); - })?; - cx.background_executor().timer(LEADER_UPDATE_THROTTLE).await; - } - anyhow::Ok(()) - } - })); - } - - let mut event_subscription = Some(cx.subscribe_in( - self, - window, - move |workspace, item: &Entity, event, window, cx| { - let pane = if let Some(pane) = workspace - .panes_by_item - .get(&item.item_id()) - .and_then(|pane| pane.upgrade()) - { - pane - } else { - return; - }; - - if let Some(item) = item.to_followable_item_handle(cx) { - let leader_id = workspace.leader_for_pane(&pane); - - if let Some(leader_id) = leader_id - && let Some(FollowEvent::Unfollow) = item.to_follow_event(event) - { - workspace.unfollow(leader_id, window, cx); - } - - if item.item_focus_handle(cx).contains_focused(window, cx) { - match leader_id { - Some(CollaboratorId::Agent) => {} - Some(CollaboratorId::PeerId(leader_peer_id)) => { - item.add_event_to_update_proto( - event, - &mut pending_update.borrow_mut(), - window, - cx, - ); - pending_update_tx.unbounded_send(Some(leader_peer_id)).ok(); - } - None => { - item.add_event_to_update_proto( - event, - &mut pending_update.borrow_mut(), - window, - cx, - ); - pending_update_tx.unbounded_send(None).ok(); - } - } - } - } - - if let Some(item) = item.to_serializable_item_handle(cx) - && item.should_serialize(event, cx) - { - workspace.enqueue_item_serialization(item).ok(); - } - - T::to_item_events(event, |event| match event { - ItemEvent::CloseItem => { - pane.update(cx, |pane, cx| { - pane.close_item_by_id( - item.item_id(), - crate::SaveIntent::Close, - window, - cx, - ) - }) - .detach_and_log_err(cx); - } - - ItemEvent::UpdateTab => { - workspace.update_item_dirty_state(item, window, cx); - - if item.has_deleted_file(cx) - && !item.is_dirty(cx) - && item.workspace_settings(cx).close_on_file_delete - { - let item_id = item.item_id(); - let close_item_task = pane.update(cx, |pane, cx| { - pane.close_item_by_id( - item_id, - crate::SaveIntent::Close, - window, - cx, - ) - }); - cx.spawn_in(window, { - let pane = pane.clone(); - async move |_workspace, cx| { - close_item_task.await?; - pane.update(cx, |pane, _cx| { - pane.nav_history_mut().remove_item(item_id); - }) - } - }) - .detach_and_log_err(cx); - } else { - pane.update(cx, |_, cx| { - cx.emit(pane::Event::ChangeItemTitle); - cx.notify(); - }); - } - } - - ItemEvent::Edit => { - let autosave = item.workspace_settings(cx).autosave; - - if let AutosaveSetting::AfterDelay { milliseconds } = autosave { - let delay = Duration::from_millis(milliseconds.0); - let item = item.clone(); - pending_autosave.fire_new( - delay, - window, - cx, - move |workspace, window, cx| { - Pane::autosave_item( - &item, - workspace.project().clone(), - window, - cx, - ) - }, - ); - } - pane.update(cx, |pane, cx| pane.handle_item_edit(item.item_id(), cx)); - } - - _ => {} - }); - }, - )); - - cx.on_blur( - &self.read(cx).focus_handle(cx), - window, - move |workspace, window, cx| { - if let Some(item) = weak_item.upgrade() - && item.workspace_settings(cx).autosave == AutosaveSetting::OnFocusChange - { - Pane::autosave_item(&item, workspace.project.clone(), window, cx) - .detach_and_log_err(cx); - } - }, - ) - .detach(); - - let item_id = self.item_id(); - workspace.update_item_dirty_state(self, window, cx); - cx.observe_release_in(self, window, move |workspace, _, _, _| { - workspace.panes_by_item.remove(&item_id); - event_subscription.take(); - send_follower_updates.take(); - }) - .detach(); - } - - cx.defer_in(window, |workspace, window, cx| { - workspace.serialize_workspace(window, cx); - }); - } - - fn deactivated(&self, window: &mut Window, cx: &mut App) { - self.update(cx, |this, cx| this.deactivated(window, cx)); - } - - fn on_removed(&self, cx: &App) { - self.read(cx).on_removed(cx); - } - - fn workspace_deactivated(&self, window: &mut Window, cx: &mut App) { - self.update(cx, |this, cx| this.workspace_deactivated(window, cx)); - } - - fn navigate(&self, data: Box, window: &mut Window, cx: &mut App) -> bool { - self.update(cx, |this, cx| this.navigate(data, window, cx)) - } - - fn item_id(&self) -> EntityId { - self.entity_id() - } - - fn to_any_view(&self) -> AnyView { - self.clone().into() - } - - fn is_dirty(&self, cx: &App) -> bool { - self.read(cx).is_dirty(cx) - } - - fn has_deleted_file(&self, cx: &App) -> bool { - self.read(cx).has_deleted_file(cx) - } - - fn has_conflict(&self, cx: &App) -> bool { - self.read(cx).has_conflict(cx) - } - - fn can_save(&self, cx: &App) -> bool { - self.read(cx).can_save(cx) - } - - fn can_save_as(&self, cx: &App) -> bool { - self.read(cx).can_save_as(cx) - } - - fn save( - &self, - options: SaveOptions, - project: Entity, - window: &mut Window, - cx: &mut App, - ) -> Task> { - self.update(cx, |item, cx| item.save(options, project, window, cx)) - } - - fn save_as( - &self, - project: Entity, - path: ProjectPath, - window: &mut Window, - cx: &mut App, - ) -> Task> { - self.update(cx, |item, cx| item.save_as(project, path, window, cx)) - } - - fn reload( - &self, - project: Entity, - window: &mut Window, - cx: &mut App, - ) -> Task> { - self.update(cx, |item, cx| item.reload(project, window, cx)) - } - - fn act_as_type<'a>(&'a self, type_id: TypeId, cx: &'a App) -> Option { - self.read(cx).act_as_type(type_id, self, cx) - } - - fn to_followable_item_handle(&self, cx: &App) -> Option> { - FollowableViewRegistry::to_followable_view(self.clone(), cx) - } - - fn on_release( - &self, - cx: &mut App, - callback: Box, - ) -> gpui::Subscription { - cx.observe_release(self, move |_, cx| callback(cx)) - } - - fn to_searchable_item_handle(&self, cx: &App) -> Option> { - self.read(cx).as_searchable(self, cx) - } - - fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation { - self.read(cx).breadcrumb_location(cx) - } - - fn breadcrumbs(&self, theme: &Theme, cx: &App) -> Option> { - self.read(cx).breadcrumbs(theme, cx) - } - - fn breadcrumb_prefix(&self, window: &mut Window, cx: &mut App) -> Option { - self.update(cx, |item, cx| item.breadcrumb_prefix(window, cx)) - } - - fn show_toolbar(&self, cx: &App) -> bool { - self.read(cx).show_toolbar() - } - - fn pixel_position_of_cursor(&self, cx: &App) -> Option> { - self.read(cx).pixel_position_of_cursor(cx) - } - - fn downgrade_item(&self) -> Box { - Box::new(self.downgrade()) - } - - fn to_serializable_item_handle(&self, cx: &App) -> Option> { - SerializableItemRegistry::view_to_serializable_item_handle(self.to_any_view(), cx) - } - - fn preserve_preview(&self, cx: &App) -> bool { - self.read(cx).preserve_preview(cx) - } - - fn include_in_nav_history(&self) -> bool { - T::include_in_nav_history() - } - - fn relay_action(&self, action: Box, window: &mut Window, cx: &mut App) { - self.update(cx, |this, cx| { - this.focus_handle(cx).focus(window); - window.dispatch_action(action, cx); - }) - } -} - -impl From> for AnyView { - fn from(val: Box) -> Self { - val.to_any_view() - } -} - -impl From<&Box> for AnyView { - fn from(val: &Box) -> Self { - val.to_any_view() - } -} - -impl Clone for Box { - fn clone(&self) -> Box { - self.boxed_clone() - } -} - -impl WeakItemHandle for WeakEntity { - fn id(&self) -> EntityId { - self.entity_id() - } - - fn boxed_clone(&self) -> Box { - Box::new(self.clone()) - } - - fn upgrade(&self) -> Option> { - self.upgrade().map(|v| Box::new(v) as Box) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct ProjectItemKind(pub &'static str); - -pub trait ProjectItem: Item { - type Item: project::ProjectItem; - - fn project_item_kind() -> Option { - None - } - - fn for_project_item( - project: Entity, - pane: Option<&Pane>, - item: Entity, - window: &mut Window, - cx: &mut Context, - ) -> 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)] -pub enum FollowEvent { - Unfollow, -} - -pub enum Dedup { - KeepExisting, - ReplaceExisting, -} - -pub trait FollowableItem: Item { - fn remote_id(&self) -> Option; - fn to_state_proto(&self, window: &Window, cx: &App) -> Option; - fn from_state_proto( - project: Entity, - id: ViewId, - state: &mut Option, - window: &mut Window, - cx: &mut App, - ) -> Option>>>; - fn to_follow_event(event: &Self::Event) -> Option; - fn add_event_to_update_proto( - &self, - event: &Self::Event, - update: &mut Option, - window: &Window, - cx: &App, - ) -> bool; - fn apply_update_proto( - &mut self, - project: &Entity, - message: proto::update_view::Variant, - window: &mut Window, - cx: &mut Context, - ) -> Task>; - fn is_project_item(&self, window: &Window, cx: &App) -> bool; - fn set_leader_id( - &mut self, - leader_peer_id: Option, - window: &mut Window, - cx: &mut Context, - ); - fn dedup(&self, existing: &Self, window: &Window, cx: &App) -> Option; - fn update_agent_location( - &mut self, - _location: language::Anchor, - _window: &mut Window, - _cx: &mut Context, - ) { - } -} - -pub trait FollowableItemHandle: ItemHandle { - fn remote_id(&self, client: &Arc, window: &mut Window, cx: &mut App) -> Option; - fn downgrade(&self) -> Box; - fn set_leader_id( - &self, - leader_peer_id: Option, - window: &mut Window, - cx: &mut App, - ); - fn to_state_proto(&self, window: &mut Window, cx: &mut App) -> Option; - fn add_event_to_update_proto( - &self, - event: &dyn Any, - update: &mut Option, - window: &mut Window, - cx: &mut App, - ) -> bool; - fn to_follow_event(&self, event: &dyn Any) -> Option; - fn apply_update_proto( - &self, - project: &Entity, - message: proto::update_view::Variant, - window: &mut Window, - cx: &mut App, - ) -> Task>; - fn is_project_item(&self, window: &mut Window, cx: &mut App) -> bool; - fn dedup( - &self, - existing: &dyn FollowableItemHandle, - window: &mut Window, - cx: &mut App, - ) -> Option; - fn update_agent_location(&self, location: language::Anchor, window: &mut Window, cx: &mut App); -} - -impl FollowableItemHandle for Entity { - fn remote_id(&self, client: &Arc, _: &mut Window, cx: &mut App) -> Option { - self.read(cx).remote_id().or_else(|| { - client.peer_id().map(|creator| ViewId { - creator: CollaboratorId::PeerId(creator), - id: self.item_id().as_u64(), - }) - }) - } - - fn downgrade(&self) -> Box { - Box::new(self.downgrade()) - } - - fn set_leader_id(&self, leader_id: Option, window: &mut Window, cx: &mut App) { - self.update(cx, |this, cx| this.set_leader_id(leader_id, window, cx)) - } - - fn to_state_proto(&self, window: &mut Window, cx: &mut App) -> Option { - self.read(cx).to_state_proto(window, cx) - } - - fn add_event_to_update_proto( - &self, - event: &dyn Any, - update: &mut Option, - window: &mut Window, - cx: &mut App, - ) -> bool { - if let Some(event) = event.downcast_ref() { - self.read(cx) - .add_event_to_update_proto(event, update, window, cx) - } else { - false - } - } - - fn to_follow_event(&self, event: &dyn Any) -> Option { - T::to_follow_event(event.downcast_ref()?) - } - - fn apply_update_proto( - &self, - project: &Entity, - message: proto::update_view::Variant, - window: &mut Window, - cx: &mut App, - ) -> Task> { - self.update(cx, |this, cx| { - this.apply_update_proto(project, message, window, cx) - }) - } - - fn is_project_item(&self, window: &mut Window, cx: &mut App) -> bool { - self.read(cx).is_project_item(window, cx) - } - - fn dedup( - &self, - existing: &dyn FollowableItemHandle, - window: &mut Window, - cx: &mut App, - ) -> Option { - let existing = existing.to_any_view().downcast::().ok()?; - self.read(cx).dedup(existing.read(cx), window, cx) - } - - fn update_agent_location(&self, location: language::Anchor, window: &mut Window, cx: &mut App) { - self.update(cx, |this, cx| { - this.update_agent_location(location, window, cx) - }) - } -} - -pub trait WeakFollowableItemHandle: Send + Sync { - fn upgrade(&self) -> Option>; -} - -impl WeakFollowableItemHandle for WeakEntity { - fn upgrade(&self) -> Option> { - Some(Box::new(self.upgrade()?)) - } -} - -#[cfg(any(test, feature = "test-support"))] -pub mod test { - use super::{Item, ItemEvent, SerializableItem, TabContentParams}; - use crate::{ - ItemId, ItemNavHistory, Workspace, WorkspaceId, - item::{ItemBufferKind, SaveOptions}, - }; - use gpui::{ - AnyElement, App, AppContext as _, Context, Entity, EntityId, EventEmitter, Focusable, - InteractiveElement, IntoElement, Render, SharedString, Task, WeakEntity, Window, - }; - use project::{Project, ProjectEntryId, ProjectPath, WorktreeId}; - use std::{any::Any, cell::Cell}; - use util::rel_path::rel_path; - - pub struct TestProjectItem { - pub entry_id: Option, - pub project_path: Option, - pub is_dirty: bool, - } - - pub struct TestItem { - pub workspace_id: Option, - pub state: String, - pub label: String, - pub save_count: usize, - pub save_as_count: usize, - pub reload_count: usize, - pub is_dirty: bool, - pub buffer_kind: ItemBufferKind, - pub has_conflict: bool, - pub has_deleted_file: bool, - pub project_items: Vec>, - pub nav_history: Option, - pub tab_descriptions: Option>, - pub tab_detail: Cell>, - serialize: Option Option>>>>, - focus_handle: gpui::FocusHandle, - } - - impl project::ProjectItem for TestProjectItem { - fn try_open( - _project: &Entity, - _path: &ProjectPath, - _cx: &mut App, - ) -> Option>>> { - None - } - fn entry_id(&self, _: &App) -> Option { - self.entry_id - } - - fn project_path(&self, _: &App) -> Option { - self.project_path.clone() - } - - fn is_dirty(&self) -> bool { - self.is_dirty - } - } - - pub enum TestItemEvent { - Edit, - } - - impl TestProjectItem { - pub fn new(id: u64, path: &str, cx: &mut App) -> Entity { - let entry_id = Some(ProjectEntryId::from_proto(id)); - let project_path = Some(ProjectPath { - worktree_id: WorktreeId::from_usize(0), - path: rel_path(path).into(), - }); - cx.new(|_| Self { - entry_id, - project_path, - is_dirty: false, - }) - } - - pub fn new_untitled(cx: &mut App) -> Entity { - cx.new(|_| Self { - project_path: None, - entry_id: None, - is_dirty: false, - }) - } - - pub fn new_dirty(id: u64, path: &str, cx: &mut App) -> Entity { - let entry_id = Some(ProjectEntryId::from_proto(id)); - let project_path = Some(ProjectPath { - worktree_id: WorktreeId::from_usize(0), - path: rel_path(path).into(), - }); - cx.new(|_| Self { - entry_id, - project_path, - is_dirty: true, - }) - } - } - - impl TestItem { - pub fn new(cx: &mut Context) -> Self { - Self { - state: String::new(), - label: String::new(), - save_count: 0, - save_as_count: 0, - reload_count: 0, - is_dirty: false, - has_conflict: false, - has_deleted_file: false, - project_items: Vec::new(), - buffer_kind: ItemBufferKind::Singleton, - nav_history: None, - tab_descriptions: None, - tab_detail: Default::default(), - workspace_id: Default::default(), - focus_handle: cx.focus_handle(), - serialize: None, - } - } - - pub fn new_deserialized(id: WorkspaceId, cx: &mut Context) -> Self { - let mut this = Self::new(cx); - this.workspace_id = Some(id); - this - } - - pub fn with_label(mut self, state: &str) -> Self { - self.label = state.to_string(); - self - } - - pub fn with_buffer_kind(mut self, buffer_kind: ItemBufferKind) -> Self { - self.buffer_kind = buffer_kind; - self - } - - pub fn set_has_deleted_file(&mut self, deleted: bool) { - self.has_deleted_file = deleted; - } - - pub fn with_dirty(mut self, dirty: bool) -> Self { - self.is_dirty = dirty; - self - } - - pub fn with_conflict(mut self, has_conflict: bool) -> Self { - self.has_conflict = has_conflict; - self - } - - pub fn with_project_items(mut self, items: &[Entity]) -> Self { - self.project_items.clear(); - self.project_items.extend(items.iter().cloned()); - self - } - - pub fn with_serialize( - mut self, - serialize: impl Fn() -> Option>> + 'static, - ) -> Self { - self.serialize = Some(Box::new(serialize)); - self - } - - pub fn set_state(&mut self, state: String, cx: &mut Context) { - self.push_to_nav_history(cx); - self.state = state; - } - - fn push_to_nav_history(&mut self, cx: &mut Context) { - if let Some(history) = &mut self.nav_history { - history.push(Some(Box::new(self.state.clone())), cx); - } - } - } - - impl Render for TestItem { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - gpui::div().track_focus(&self.focus_handle(cx)) - } - } - - impl EventEmitter for TestItem {} - - impl Focusable for TestItem { - fn focus_handle(&self, _: &App) -> gpui::FocusHandle { - self.focus_handle.clone() - } - } - - impl Item for TestItem { - type Event = ItemEvent; - - fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { - f(*event) - } - - fn tab_content_text(&self, detail: usize, _cx: &App) -> SharedString { - self.tab_descriptions - .as_ref() - .and_then(|descriptions| { - let description = *descriptions.get(detail).or_else(|| descriptions.last())?; - description.into() - }) - .unwrap_or_default() - .into() - } - - fn telemetry_event_text(&self) -> Option<&'static str> { - None - } - - fn tab_content(&self, params: TabContentParams, _window: &Window, _cx: &App) -> AnyElement { - self.tab_detail.set(params.detail); - gpui::div().into_any_element() - } - - fn for_each_project_item( - &self, - cx: &App, - f: &mut dyn FnMut(EntityId, &dyn project::ProjectItem), - ) { - self.project_items - .iter() - .for_each(|item| f(item.entity_id(), item.read(cx))) - } - - fn buffer_kind(&self, _: &App) -> ItemBufferKind { - self.buffer_kind - } - - fn set_nav_history( - &mut self, - history: ItemNavHistory, - _window: &mut Window, - _: &mut Context, - ) { - self.nav_history = Some(history); - } - - fn navigate( - &mut self, - state: Box, - _window: &mut Window, - _: &mut Context, - ) -> bool { - let state = *state.downcast::().unwrap_or_default(); - if state != self.state { - self.state = state; - true - } else { - false - } - } - - fn deactivated(&mut self, _window: &mut Window, cx: &mut Context) { - self.push_to_nav_history(cx); - } - - fn can_split(&self) -> bool { - true - } - - fn clone_on_split( - &self, - _workspace_id: Option, - _: &mut Window, - cx: &mut Context, - ) -> Task>> - where - Self: Sized, - { - Task::ready(Some(cx.new(|cx| Self { - state: self.state.clone(), - label: self.label.clone(), - save_count: self.save_count, - save_as_count: self.save_as_count, - reload_count: self.reload_count, - is_dirty: self.is_dirty, - buffer_kind: self.buffer_kind, - has_conflict: self.has_conflict, - has_deleted_file: self.has_deleted_file, - project_items: self.project_items.clone(), - nav_history: None, - tab_descriptions: None, - tab_detail: Default::default(), - workspace_id: self.workspace_id, - focus_handle: cx.focus_handle(), - serialize: None, - }))) - } - - fn is_dirty(&self, _: &App) -> bool { - self.is_dirty - } - - fn has_conflict(&self, _: &App) -> bool { - self.has_conflict - } - - fn has_deleted_file(&self, _: &App) -> bool { - self.has_deleted_file - } - - fn can_save(&self, cx: &App) -> bool { - !self.project_items.is_empty() - && self - .project_items - .iter() - .all(|item| item.read(cx).entry_id.is_some()) - } - - fn can_save_as(&self, _cx: &App) -> bool { - self.buffer_kind == ItemBufferKind::Singleton - } - - fn save( - &mut self, - _: SaveOptions, - _: Entity, - _window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.save_count += 1; - self.is_dirty = false; - for item in &self.project_items { - item.update(cx, |item, _| { - if item.is_dirty { - item.is_dirty = false; - } - }) - } - Task::ready(Ok(())) - } - - fn save_as( - &mut self, - _: Entity, - _: ProjectPath, - _window: &mut Window, - _: &mut Context, - ) -> Task> { - self.save_as_count += 1; - self.is_dirty = false; - Task::ready(Ok(())) - } - - fn reload( - &mut self, - _: Entity, - _window: &mut Window, - _: &mut Context, - ) -> Task> { - self.reload_count += 1; - self.is_dirty = false; - Task::ready(Ok(())) - } - } - - impl SerializableItem for TestItem { - fn serialized_item_kind() -> &'static str { - "TestItem" - } - - fn deserialize( - _project: Entity, - _workspace: WeakEntity, - workspace_id: WorkspaceId, - _item_id: ItemId, - _window: &mut Window, - cx: &mut App, - ) -> Task>> { - let entity = cx.new(|cx| Self::new_deserialized(workspace_id, cx)); - Task::ready(Ok(entity)) - } - - fn cleanup( - _workspace_id: WorkspaceId, - _alive_items: Vec, - _window: &mut Window, - _cx: &mut App, - ) -> Task> { - Task::ready(Ok(())) - } - - fn serialize( - &mut self, - _workspace: &mut Workspace, - _item_id: ItemId, - _closing: bool, - _window: &mut Window, - _cx: &mut Context, - ) -> Option>> { - if let Some(serialize) = self.serialize.take() { - let result = serialize(); - self.serialize = Some(serialize); - result - } else { - None - } - } - - fn should_serialize(&self, _event: &Self::Event) -> bool { - false - } - } -} diff --git a/crates/workspace/src/modal_layer.rs b/crates/workspace/src/modal_layer.rs deleted file mode 100644 index bcd7db3a82..0000000000 --- a/crates/workspace/src/modal_layer.rs +++ /dev/null @@ -1,208 +0,0 @@ -use gpui::{ - AnyView, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable as _, ManagedView, - MouseButton, Subscription, -}; -use ui::prelude::*; - -#[derive(Debug)] -pub enum DismissDecision { - Dismiss(bool), - Pending, -} - -pub trait ModalView: ManagedView { - fn on_before_dismiss( - &mut self, - _window: &mut Window, - _: &mut Context, - ) -> DismissDecision { - DismissDecision::Dismiss(true) - } - - fn fade_out_background(&self) -> bool { - false - } -} - -trait ModalViewHandle { - fn on_before_dismiss(&mut self, window: &mut Window, cx: &mut App) -> DismissDecision; - fn view(&self) -> AnyView; - fn fade_out_background(&self, cx: &mut App) -> bool; -} - -impl ModalViewHandle for Entity { - fn on_before_dismiss(&mut self, window: &mut Window, cx: &mut App) -> DismissDecision { - self.update(cx, |this, cx| this.on_before_dismiss(window, cx)) - } - - fn view(&self) -> AnyView { - self.clone().into() - } - - fn fade_out_background(&self, cx: &mut App) -> bool { - self.read(cx).fade_out_background() - } -} - -pub struct ActiveModal { - modal: Box, - _subscriptions: [Subscription; 2], - previous_focus_handle: Option, - focus_handle: FocusHandle, -} - -pub struct ModalLayer { - active_modal: Option, - dismiss_on_focus_lost: bool, -} - -pub(crate) struct ModalOpenedEvent; - -impl EventEmitter for ModalLayer {} - -impl Default for ModalLayer { - fn default() -> Self { - Self::new() - } -} - -impl ModalLayer { - pub fn new() -> Self { - Self { - active_modal: None, - dismiss_on_focus_lost: false, - } - } - - pub fn toggle_modal(&mut self, window: &mut Window, cx: &mut Context, build_view: B) - where - V: ModalView, - B: FnOnce(&mut Window, &mut Context) -> V, - { - if let Some(active_modal) = &self.active_modal { - let is_close = active_modal.modal.view().downcast::().is_ok(); - let did_close = self.hide_modal(window, cx); - if is_close || !did_close { - return; - } - } - let new_modal = cx.new(|cx| build_view(window, cx)); - self.show_modal(new_modal, window, cx); - cx.emit(ModalOpenedEvent); - } - - fn show_modal(&mut self, new_modal: Entity, window: &mut Window, cx: &mut Context) - where - V: ModalView, - { - let focus_handle = cx.focus_handle(); - self.active_modal = Some(ActiveModal { - modal: Box::new(new_modal.clone()), - _subscriptions: [ - cx.subscribe_in( - &new_modal, - window, - |this, _, _: &DismissEvent, window, cx| { - this.hide_modal(window, cx); - }, - ), - cx.on_focus_out(&focus_handle, window, |this, _event, window, cx| { - if this.dismiss_on_focus_lost { - this.hide_modal(window, cx); - } - }), - ], - previous_focus_handle: window.focused(cx), - focus_handle, - }); - cx.defer_in(window, move |_, window, cx| { - window.focus(&new_modal.focus_handle(cx)); - }); - cx.notify(); - } - - pub fn hide_modal(&mut self, window: &mut Window, cx: &mut Context) -> bool { - let Some(active_modal) = self.active_modal.as_mut() else { - self.dismiss_on_focus_lost = false; - return false; - }; - - match active_modal.modal.on_before_dismiss(window, cx) { - DismissDecision::Dismiss(dismiss) => { - self.dismiss_on_focus_lost = !dismiss; - if !dismiss { - return false; - } - } - DismissDecision::Pending => { - self.dismiss_on_focus_lost = false; - return false; - } - } - - if let Some(active_modal) = self.active_modal.take() { - if let Some(previous_focus) = active_modal.previous_focus_handle - && active_modal.focus_handle.contains_focused(window, cx) - { - previous_focus.focus(window); - } - cx.notify(); - } - true - } - - pub fn active_modal(&self) -> Option> - where - V: 'static, - { - let active_modal = self.active_modal.as_ref()?; - active_modal.modal.view().downcast::().ok() - } - - pub fn has_active_modal(&self) -> bool { - self.active_modal.is_some() - } -} - -impl Render for ModalLayer { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let Some(active_modal) = &self.active_modal else { - return div(); - }; - - div() - .occlude() - .absolute() - .size_full() - .top_0() - .left_0() - .when(active_modal.modal.fade_out_background(cx), |el| { - let mut background = cx.theme().colors().elevated_surface_background; - background.fade_out(0.2); - el.bg(background) - }) - .on_mouse_down( - MouseButton::Left, - cx.listener(|this, _, window, cx| { - this.hide_modal(window, cx); - }), - ) - .child( - v_flex() - .h(px(0.0)) - .top_20() - .flex() - .flex_col() - .items_center() - .track_focus(&active_modal.focus_handle) - .child( - h_flex() - .occlude() - .child(active_modal.modal.view()) - .on_mouse_down(MouseButton::Left, |_, _, cx| { - cx.stop_propagation(); - }), - ), - ) - } -} diff --git a/crates/workspace/src/notifications.rs b/crates/workspace/src/notifications.rs deleted file mode 100644 index cfdc730b4d..0000000000 --- a/crates/workspace/src/notifications.rs +++ /dev/null @@ -1,1115 +0,0 @@ -use crate::{SuppressNotification, Toast, Workspace}; -use anyhow::Context as _; -use gpui::{ - AnyView, App, AppContext as _, AsyncWindowContext, ClickEvent, ClipboardItem, Context, - DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, PromptLevel, Render, ScrollHandle, - Task, svg, -}; -use parking_lot::Mutex; - -use std::ops::Deref; -use std::sync::{Arc, LazyLock}; -use std::{any::TypeId, time::Duration}; -use ui::{Tooltip, prelude::*}; -use util::ResultExt; - -#[derive(Default)] -pub struct Notifications { - notifications: Vec<(NotificationId, AnyView)>, -} - -impl Deref for Notifications { - type Target = Vec<(NotificationId, AnyView)>; - - fn deref(&self) -> &Self::Target { - &self.notifications - } -} - -impl std::ops::DerefMut for Notifications { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.notifications - } -} - -#[derive(Debug, Eq, PartialEq, Clone, Hash)] -pub enum NotificationId { - Unique(TypeId), - Composite(TypeId, ElementId), - Named(SharedString), -} - -impl NotificationId { - /// Returns a unique [`NotificationId`] for the given type. - pub fn unique() -> Self { - Self::Unique(TypeId::of::()) - } - - /// Returns a [`NotificationId`] for the given type that is also identified - /// by the provided ID. - pub fn composite(id: impl Into) -> Self { - Self::Composite(TypeId::of::(), id.into()) - } - - /// Builds a `NotificationId` out of the given string. - pub fn named(id: SharedString) -> Self { - Self::Named(id) - } -} - -pub trait Notification: - EventEmitter + EventEmitter + Focusable + Render -{ -} - -pub struct SuppressEvent; - -impl Workspace { - #[cfg(any(test, feature = "test-support"))] - pub fn notification_ids(&self) -> Vec { - self.notifications - .iter() - .map(|(id, _)| id) - .cloned() - .collect() - } - - pub fn show_notification( - &mut self, - id: NotificationId, - cx: &mut Context, - build_notification: impl FnOnce(&mut Context) -> Entity, - ) { - self.show_notification_without_handling_dismiss_events(&id, cx, |cx| { - let notification = build_notification(cx); - cx.subscribe(¬ification, { - let id = id.clone(); - move |this, _, _: &DismissEvent, cx| { - this.dismiss_notification(&id, cx); - } - }) - .detach(); - cx.subscribe(¬ification, { - let id = id.clone(); - move |workspace: &mut Workspace, _, _: &SuppressEvent, cx| { - workspace.suppress_notification(&id, cx); - } - }) - .detach(); - notification.into() - }); - } - - /// Shows a notification in this workspace's window. Caller must handle dismiss. - /// - /// This exists so that the `build_notification` closures stored for app notifications can - /// return `AnyView`. Subscribing to events from an `AnyView` is not supported, so instead that - /// responsibility is pushed to the caller where the `V` type is known. - pub(crate) fn show_notification_without_handling_dismiss_events( - &mut self, - id: &NotificationId, - cx: &mut Context, - build_notification: impl FnOnce(&mut Context) -> AnyView, - ) { - if self.suppressed_notifications.contains(id) { - return; - } - self.dismiss_notification(id, cx); - self.notifications - .push((id.clone(), build_notification(cx))); - cx.notify(); - } - - pub fn show_error(&mut self, err: &E, cx: &mut Context) - where - E: std::fmt::Debug + std::fmt::Display, - { - self.show_notification(workspace_error_notification_id(), cx, |cx| { - cx.new(|cx| ErrorMessagePrompt::new(format!("Error: {err}"), cx)) - }); - } - - pub fn show_portal_error(&mut self, err: String, cx: &mut Context) { - struct PortalError; - - self.show_notification(NotificationId::unique::(), cx, |cx| { - cx.new(|cx| { - ErrorMessagePrompt::new(err.to_string(), cx).with_link_button( - "See docs", - "https://zed.dev/docs/linux#i-cant-open-any-files", - ) - }) - }); - } - - pub fn dismiss_notification(&mut self, id: &NotificationId, cx: &mut Context) { - self.notifications.retain(|(existing_id, _)| { - if existing_id == id { - cx.notify(); - false - } else { - true - } - }); - } - - pub fn show_toast(&mut self, toast: Toast, cx: &mut Context) { - self.dismiss_notification(&toast.id, cx); - self.show_notification(toast.id.clone(), cx, |cx| { - cx.new(|cx| match toast.on_click.as_ref() { - Some((click_msg, on_click)) => { - let on_click = on_click.clone(); - simple_message_notification::MessageNotification::new(toast.msg.clone(), cx) - .primary_message(click_msg.clone()) - .primary_on_click(move |window, cx| on_click(window, cx)) - } - None => { - simple_message_notification::MessageNotification::new(toast.msg.clone(), cx) - } - }) - }); - if toast.autohide { - cx.spawn(async move |workspace, cx| { - cx.background_executor() - .timer(Duration::from_millis(5000)) - .await; - workspace - .update(cx, |workspace, cx| workspace.dismiss_toast(&toast.id, cx)) - .ok(); - }) - .detach(); - } - } - - pub fn dismiss_toast(&mut self, id: &NotificationId, cx: &mut Context) { - self.dismiss_notification(id, cx); - } - - pub fn clear_all_notifications(&mut self, cx: &mut Context) { - self.notifications.clear(); - cx.notify(); - } - - /// Hide all notifications matching the given ID - pub fn suppress_notification(&mut self, id: &NotificationId, cx: &mut Context) { - self.dismiss_notification(id, cx); - self.suppressed_notifications.insert(id.clone()); - } - - pub fn show_initial_notifications(&mut self, cx: &mut Context) { - // Allow absence of the global so that tests don't need to initialize it. - let app_notifications = GLOBAL_APP_NOTIFICATIONS - .lock() - .app_notifications - .iter() - .cloned() - .collect::>(); - for (id, build_notification) in app_notifications { - self.show_notification_without_handling_dismiss_events(&id, cx, |cx| { - build_notification(cx) - }); - } - } -} - -pub struct LanguageServerPrompt { - focus_handle: FocusHandle, - request: Option, - scroll_handle: ScrollHandle, -} - -impl Focusable for LanguageServerPrompt { - fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle { - self.focus_handle.clone() - } -} - -impl Notification for LanguageServerPrompt {} - -impl LanguageServerPrompt { - pub fn new(request: project::LanguageServerPromptRequest, cx: &mut App) -> Self { - Self { - focus_handle: cx.focus_handle(), - request: Some(request), - scroll_handle: ScrollHandle::new(), - } - } - - async fn select_option(this: Entity, ix: usize, cx: &mut AsyncWindowContext) { - util::maybe!(async move { - let potential_future = this.update(cx, |this, _| { - this.request.take().map(|request| request.respond(ix)) - }); - - potential_future? // App Closed - .context("Response already sent")? - .await - .context("Stream already closed")?; - - this.update(cx, |_, cx| cx.emit(DismissEvent))?; - - anyhow::Ok(()) - }) - .await - .log_err(); - } -} - -impl Render for LanguageServerPrompt { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let Some(request) = &self.request else { - return div().id("language_server_prompt_notification"); - }; - - let (icon, color) = match request.level { - PromptLevel::Info => (IconName::Info, Color::Accent), - PromptLevel::Warning => (IconName::Warning, Color::Warning), - PromptLevel::Critical => (IconName::XCircle, Color::Error), - }; - - let suppress = window.modifiers().shift; - let (close_id, close_icon) = if suppress { - ("suppress", IconName::Minimize) - } else { - ("close", IconName::Close) - }; - - div() - .id("language_server_prompt_notification") - .group("language_server_prompt_notification") - .occlude() - .w_full() - .max_h(vh(0.8, window)) - .elevation_3(cx) - .overflow_y_scroll() - .track_scroll(&self.scroll_handle) - .on_modifiers_changed(cx.listener(|_, _, _, cx| cx.notify())) - .child( - v_flex() - .p_3() - .overflow_hidden() - .child( - h_flex() - .justify_between() - .items_start() - .child( - h_flex() - .gap_2() - .child(Icon::new(icon).color(color)) - .child(Label::new(request.lsp_name.clone())), - ) - .child( - h_flex() - .gap_2() - .child( - IconButton::new("copy", IconName::Copy) - .on_click({ - let message = request.message.clone(); - move |_, _, cx| { - cx.write_to_clipboard( - ClipboardItem::new_string(message.clone()), - ) - } - }) - .tooltip(Tooltip::text("Copy Description")), - ) - .child( - IconButton::new(close_id, close_icon) - .tooltip(move |_window, cx| { - if suppress { - Tooltip::for_action( - "Suppress.\nClose with click.", - &SuppressNotification, - cx, - ) - } else { - Tooltip::for_action( - "Close.\nSuppress with shift-click.", - &menu::Cancel, - cx, - ) - } - }) - .on_click(cx.listener( - move |_, _: &ClickEvent, _, cx| { - if suppress { - cx.emit(SuppressEvent); - } else { - cx.emit(DismissEvent); - } - }, - )), - ), - ), - ) - .child(Label::new(request.message.to_string()).size(LabelSize::Small)) - .children(request.actions.iter().enumerate().map(|(ix, action)| { - let this_handle = cx.entity(); - Button::new(ix, action.title.clone()) - .size(ButtonSize::Large) - .on_click(move |_, window, cx| { - let this_handle = this_handle.clone(); - window - .spawn(cx, async move |cx| { - LanguageServerPrompt::select_option(this_handle, ix, cx) - .await - }) - .detach() - }) - })), - ) - } -} - -impl EventEmitter for LanguageServerPrompt {} -impl EventEmitter for LanguageServerPrompt {} - -fn workspace_error_notification_id() -> NotificationId { - struct WorkspaceErrorNotification; - NotificationId::unique::() -} - -#[derive(Debug, Clone)] -pub struct ErrorMessagePrompt { - message: SharedString, - focus_handle: gpui::FocusHandle, - label_and_url_button: Option<(SharedString, SharedString)>, -} - -impl ErrorMessagePrompt { - pub fn new(message: S, cx: &mut App) -> Self - where - S: Into, - { - Self { - message: message.into(), - focus_handle: cx.focus_handle(), - label_and_url_button: None, - } - } - - pub fn with_link_button(mut self, label: S, url: S) -> Self - where - S: Into, - { - self.label_and_url_button = Some((label.into(), url.into())); - self - } -} - -impl Render for ErrorMessagePrompt { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - h_flex() - .id("error_message_prompt_notification") - .occlude() - .elevation_3(cx) - .items_start() - .justify_between() - .p_2() - .gap_2() - .w_full() - .child( - v_flex() - .w_full() - .child( - h_flex() - .w_full() - .justify_between() - .child( - svg() - .size(window.text_style().font_size) - .flex_none() - .mr_2() - .mt(px(-2.0)) - .map(|icon| { - icon.path(IconName::Warning.path()) - .text_color(Color::Error.color(cx)) - }), - ) - .child( - ui::IconButton::new("close", ui::IconName::Close) - .on_click(cx.listener(|_, _, _, cx| cx.emit(DismissEvent))), - ), - ) - .child( - div() - .id("error_message") - .max_w_96() - .max_h_40() - .overflow_y_scroll() - .child(Label::new(self.message.clone()).size(LabelSize::Small)), - ) - .when_some(self.label_and_url_button.clone(), |elm, (label, url)| { - elm.child( - div().mt_2().child( - ui::Button::new("error_message_prompt_notification_button", label) - .on_click(move |_, _, cx| cx.open_url(&url)), - ), - ) - }), - ) - } -} - -impl Focusable for ErrorMessagePrompt { - fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle { - self.focus_handle.clone() - } -} - -impl EventEmitter for ErrorMessagePrompt {} -impl EventEmitter for ErrorMessagePrompt {} - -impl Notification for ErrorMessagePrompt {} - -#[derive(IntoElement, RegisterComponent)] -pub struct NotificationFrame { - title: Option, - show_suppress_button: bool, - show_close_button: bool, - close: Option>, - contents: Option, - suffix: Option, -} - -impl NotificationFrame { - pub fn new() -> Self { - Self { - title: None, - contents: None, - suffix: None, - show_suppress_button: true, - show_close_button: true, - close: None, - } - } - - pub fn with_title(mut self, title: Option>) -> Self { - self.title = title.map(Into::into); - self - } - - pub fn with_content(self, content: impl IntoElement) -> Self { - Self { - contents: Some(content.into_any_element()), - ..self - } - } - - /// Determines whether the given notification ID should be suppressible - /// Suppressed notifications will not be shown anymore - pub fn show_suppress_button(mut self, show: bool) -> Self { - self.show_suppress_button = show; - self - } - - pub fn show_close_button(mut self, show: bool) -> Self { - self.show_close_button = show; - self - } - - pub fn on_close(self, on_close: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self { - Self { - close: Some(Box::new(on_close)), - ..self - } - } - - pub fn with_suffix(mut self, suffix: impl IntoElement) -> Self { - self.suffix = Some(suffix.into_any_element()); - self - } -} - -impl RenderOnce for NotificationFrame { - fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let entity = window.current_view(); - let show_suppress_button = self.show_suppress_button; - let suppress = show_suppress_button && window.modifiers().shift; - let (close_id, close_icon) = if suppress { - ("suppress", IconName::Minimize) - } else { - ("close", IconName::Close) - }; - - v_flex() - .occlude() - .p_3() - .gap_2() - .elevation_3(cx) - .child( - h_flex() - .gap_4() - .justify_between() - .items_start() - .child( - v_flex() - .gap_0p5() - .when_some(self.title.clone(), |div, title| { - div.child(Label::new(title)) - }) - .child(div().max_w_96().children(self.contents)), - ) - .when(self.show_close_button, |this| { - this.on_modifiers_changed(move |_, _, cx| cx.notify(entity)) - .child( - IconButton::new(close_id, close_icon) - .tooltip(move |_window, cx| { - if suppress { - Tooltip::for_action( - "Suppress.\nClose with click.", - &SuppressNotification, - cx, - ) - } else if show_suppress_button { - Tooltip::for_action( - "Close.\nSuppress with shift-click.", - &menu::Cancel, - cx, - ) - } else { - Tooltip::for_action("Close", &menu::Cancel, cx) - } - }) - .on_click({ - let close = self.close.take(); - move |_, window, cx| { - if let Some(close) = &close { - close(&suppress, window, cx) - } - } - }), - ) - }), - ) - .children(self.suffix) - } -} - -impl Component for NotificationFrame {} - -pub mod simple_message_notification { - use std::sync::Arc; - - use gpui::{ - AnyElement, DismissEvent, EventEmitter, FocusHandle, Focusable, ParentElement, Render, - ScrollHandle, SharedString, Styled, - }; - use ui::{WithScrollbar, prelude::*}; - - use crate::notifications::NotificationFrame; - - use super::{Notification, SuppressEvent}; - - pub struct MessageNotification { - focus_handle: FocusHandle, - build_content: Box) -> AnyElement>, - primary_message: Option, - primary_icon: Option, - primary_icon_color: Option, - primary_on_click: Option)>>, - secondary_message: Option, - secondary_icon: Option, - secondary_icon_color: Option, - secondary_on_click: Option)>>, - more_info_message: Option, - more_info_url: Option>, - show_close_button: bool, - show_suppress_button: bool, - title: Option, - scroll_handle: ScrollHandle, - } - - impl Focusable for MessageNotification { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.focus_handle.clone() - } - } - - impl EventEmitter for MessageNotification {} - impl EventEmitter for MessageNotification {} - - impl Notification for MessageNotification {} - - impl MessageNotification { - pub fn new(message: S, cx: &mut App) -> MessageNotification - where - S: Into, - { - let message = message.into(); - Self::new_from_builder(cx, move |_, _| { - Label::new(message.clone()).into_any_element() - }) - } - - pub fn new_from_builder(cx: &mut App, content: F) -> MessageNotification - where - F: 'static + Fn(&mut Window, &mut Context) -> AnyElement, - { - Self { - build_content: Box::new(content), - primary_message: None, - primary_icon: None, - primary_icon_color: None, - primary_on_click: None, - secondary_message: None, - secondary_icon: None, - secondary_icon_color: None, - secondary_on_click: None, - more_info_message: None, - more_info_url: None, - show_close_button: true, - show_suppress_button: true, - title: None, - focus_handle: cx.focus_handle(), - scroll_handle: ScrollHandle::new(), - } - } - - pub fn primary_message(mut self, message: S) -> Self - where - S: Into, - { - self.primary_message = Some(message.into()); - self - } - - pub fn primary_icon(mut self, icon: IconName) -> Self { - self.primary_icon = Some(icon); - self - } - - pub fn primary_icon_color(mut self, color: Color) -> Self { - self.primary_icon_color = Some(color); - self - } - - pub fn primary_on_click(mut self, on_click: F) -> Self - where - F: 'static + Fn(&mut Window, &mut Context), - { - self.primary_on_click = Some(Arc::new(on_click)); - self - } - - pub fn primary_on_click_arc(mut self, on_click: Arc) -> Self - where - F: 'static + Fn(&mut Window, &mut Context), - { - self.primary_on_click = Some(on_click); - self - } - - pub fn secondary_message(mut self, message: S) -> Self - where - S: Into, - { - self.secondary_message = Some(message.into()); - self - } - - pub fn secondary_icon(mut self, icon: IconName) -> Self { - self.secondary_icon = Some(icon); - self - } - - pub fn secondary_icon_color(mut self, color: Color) -> Self { - self.secondary_icon_color = Some(color); - self - } - - pub fn secondary_on_click(mut self, on_click: F) -> Self - where - F: 'static + Fn(&mut Window, &mut Context), - { - self.secondary_on_click = Some(Arc::new(on_click)); - self - } - - pub fn secondary_on_click_arc(mut self, on_click: Arc) -> Self - where - F: 'static + Fn(&mut Window, &mut Context), - { - self.secondary_on_click = Some(on_click); - self - } - - pub fn more_info_message(mut self, message: S) -> Self - where - S: Into, - { - self.more_info_message = Some(message.into()); - self - } - - pub fn more_info_url(mut self, url: S) -> Self - where - S: Into>, - { - self.more_info_url = Some(url.into()); - self - } - - pub fn dismiss(&mut self, cx: &mut Context) { - cx.emit(DismissEvent); - } - - pub fn show_close_button(mut self, show: bool) -> Self { - self.show_close_button = show; - self - } - - /// Determines whether the given notification ID should be suppressible - /// Suppressed notifications will not be shown anymor - pub fn show_suppress_button(mut self, show: bool) -> Self { - self.show_suppress_button = show; - self - } - - pub fn with_title(mut self, title: S) -> Self - where - S: Into, - { - self.title = Some(title.into()); - self - } - } - - impl Render for MessageNotification { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - NotificationFrame::new() - .with_title(self.title.clone()) - .with_content( - div() - .child( - div() - .id("message-notification-content") - .max_h(vh(0.6, window)) - .overflow_y_scroll() - .track_scroll(&self.scroll_handle.clone()) - .child((self.build_content)(window, cx)), - ) - .vertical_scrollbar_for(&self.scroll_handle, window, cx), - ) - .show_close_button(self.show_close_button) - .show_suppress_button(self.show_suppress_button) - .on_close(cx.listener(|_, suppress, _, cx| { - if *suppress { - cx.emit(SuppressEvent); - } else { - cx.emit(DismissEvent); - } - })) - .with_suffix( - h_flex() - .gap_1() - .children(self.primary_message.iter().map(|message| { - let mut button = Button::new(message.clone(), message.clone()) - .label_size(LabelSize::Small) - .on_click(cx.listener(|this, _, window, cx| { - if let Some(on_click) = this.primary_on_click.as_ref() { - (on_click)(window, cx) - }; - this.dismiss(cx) - })); - - if let Some(icon) = self.primary_icon { - button = button - .icon(icon) - .icon_color(self.primary_icon_color.unwrap_or(Color::Muted)) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small); - } - - button - })) - .children(self.secondary_message.iter().map(|message| { - let mut button = Button::new(message.clone(), message.clone()) - .label_size(LabelSize::Small) - .on_click(cx.listener(|this, _, window, cx| { - if let Some(on_click) = this.secondary_on_click.as_ref() { - (on_click)(window, cx) - }; - this.dismiss(cx) - })); - - if let Some(icon) = self.secondary_icon { - button = button - .icon(icon) - .icon_position(IconPosition::Start) - .icon_size(IconSize::Small) - .icon_color(self.secondary_icon_color.unwrap_or(Color::Muted)); - } - - button - })) - .child( - h_flex().w_full().justify_end().children( - self.more_info_message - .iter() - .zip(self.more_info_url.iter()) - .map(|(message, url)| { - let url = url.clone(); - Button::new(message.clone(), message.clone()) - .label_size(LabelSize::Small) - .icon(IconName::ArrowUpRight) - .icon_size(IconSize::Indicator) - .icon_color(Color::Muted) - .on_click(cx.listener(move |_, _, _, cx| { - cx.open_url(&url); - })) - }), - ), - ), - ) - } - } -} - -static GLOBAL_APP_NOTIFICATIONS: LazyLock> = LazyLock::new(|| { - Mutex::new(AppNotifications { - app_notifications: Vec::new(), - }) -}); - -/// Stores app notifications so that they can be shown in new workspaces. -struct AppNotifications { - app_notifications: Vec<( - NotificationId, - Arc) -> AnyView + Send + Sync>, - )>, -} - -impl AppNotifications { - pub fn insert( - &mut self, - id: NotificationId, - build_notification: Arc) -> AnyView + Send + Sync>, - ) { - self.remove(&id); - self.app_notifications.push((id, build_notification)) - } - - pub fn remove(&mut self, id: &NotificationId) { - self.app_notifications - .retain(|(existing_id, _)| existing_id != id); - } -} - -/// Shows a notification in all workspaces. New workspaces will also receive the notification - this -/// is particularly to handle notifications that occur on initialization before any workspaces -/// exist. If the notification is dismissed within any workspace, it will be removed from all. -pub fn show_app_notification( - id: NotificationId, - cx: &mut App, - build_notification: impl Fn(&mut Context) -> Entity + 'static + Send + Sync, -) { - // Defer notification creation so that windows on the stack can be returned to GPUI - cx.defer(move |cx| { - // Handle dismiss events by removing the notification from all workspaces. - let build_notification: Arc) -> AnyView + Send + Sync> = - Arc::new({ - let id = id.clone(); - move |cx| { - let notification = build_notification(cx); - cx.subscribe(¬ification, { - let id = id.clone(); - move |_, _, _: &DismissEvent, cx| { - dismiss_app_notification(&id, cx); - } - }) - .detach(); - cx.subscribe(¬ification, { - let id = id.clone(); - move |workspace: &mut Workspace, _, _: &SuppressEvent, cx| { - workspace.suppress_notification(&id, cx); - } - }) - .detach(); - notification.into() - } - }); - - // Store the notification so that new workspaces also receive it. - GLOBAL_APP_NOTIFICATIONS - .lock() - .insert(id.clone(), build_notification.clone()); - - for window in cx.windows() { - if let Some(workspace_window) = window.downcast::() { - workspace_window - .update(cx, |workspace, _window, cx| { - workspace.show_notification_without_handling_dismiss_events( - &id, - cx, - |cx| build_notification(cx), - ); - }) - .ok(); // Doesn't matter if the windows are dropped - } - } - }); -} - -pub fn dismiss_app_notification(id: &NotificationId, cx: &mut App) { - let id = id.clone(); - // Defer notification dismissal so that windows on the stack can be returned to GPUI - cx.defer(move |cx| { - GLOBAL_APP_NOTIFICATIONS.lock().remove(&id); - for window in cx.windows() { - if let Some(workspace_window) = window.downcast::() { - let id = id.clone(); - workspace_window - .update(cx, |workspace, _window, cx| { - workspace.dismiss_notification(&id, cx) - }) - .ok(); - } - } - }); -} - -pub trait NotifyResultExt { - type Ok; - - fn notify_err(self, workspace: &mut Workspace, cx: &mut Context) - -> Option; - - fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option; - - /// Notifies the active workspace if there is one, otherwise notifies all workspaces. - fn notify_app_err(self, cx: &mut App) -> Option; -} - -impl NotifyResultExt for std::result::Result -where - E: std::fmt::Debug + std::fmt::Display, -{ - type Ok = T; - - fn notify_err(self, workspace: &mut Workspace, cx: &mut Context) -> Option { - match self { - Ok(value) => Some(value), - Err(err) => { - log::error!("Showing error notification in workspace: {err:?}"); - workspace.show_error(&err, cx); - None - } - } - } - - fn notify_async_err(self, cx: &mut AsyncWindowContext) -> Option { - match self { - Ok(value) => Some(value), - Err(err) => { - log::error!("{err:?}"); - cx.update_root(|view, _, cx| { - if let Ok(workspace) = view.downcast::() { - workspace.update(cx, |workspace, cx| workspace.show_error(&err, cx)) - } - }) - .ok(); - None - } - } - } - - fn notify_app_err(self, cx: &mut App) -> Option { - match self { - Ok(value) => Some(value), - Err(err) => { - let message: SharedString = format!("Error: {err}").into(); - log::error!("Showing error notification in app: {message}"); - show_app_notification(workspace_error_notification_id(), cx, { - move |cx| { - cx.new({ - let message = message.clone(); - move |cx| ErrorMessagePrompt::new(message, cx) - }) - } - }); - - None - } - } - } -} - -pub trait NotifyTaskExt { - fn detach_and_notify_err(self, window: &mut Window, cx: &mut App); -} - -impl NotifyTaskExt for Task> -where - E: std::fmt::Debug + std::fmt::Display + Sized + 'static, - R: 'static, -{ - fn detach_and_notify_err(self, window: &mut Window, cx: &mut App) { - window - .spawn(cx, async move |cx| self.await.notify_async_err(cx)) - .detach(); - } -} - -pub trait DetachAndPromptErr { - fn prompt_err( - self, - msg: &str, - window: &Window, - cx: &App, - f: impl FnOnce(&anyhow::Error, &mut Window, &mut App) -> Option + 'static, - ) -> Task>; - - fn detach_and_prompt_err( - self, - msg: &str, - window: &Window, - cx: &App, - f: impl FnOnce(&anyhow::Error, &mut Window, &mut App) -> Option + 'static, - ); -} - -impl DetachAndPromptErr for Task> -where - R: 'static, -{ - fn prompt_err( - self, - msg: &str, - window: &Window, - cx: &App, - f: impl FnOnce(&anyhow::Error, &mut Window, &mut App) -> Option + 'static, - ) -> Task> { - let msg = msg.to_owned(); - window.spawn(cx, async move |cx| { - let result = self.await; - if let Err(err) = result.as_ref() { - log::error!("{err:#}"); - if let Ok(prompt) = cx.update(|window, cx| { - let mut display = format!("{err:#}"); - if !display.ends_with('\n') { - display.push('.'); - display.push(' ') - } - let detail = - f(err, window, cx).unwrap_or_else(|| format!("{display}Please try again.")); - window.prompt(PromptLevel::Critical, &msg, Some(&detail), &["Ok"], cx) - }) { - prompt.await.ok(); - } - return None; - } - Some(result.unwrap()) - }) - } - - fn detach_and_prompt_err( - self, - msg: &str, - window: &Window, - cx: &App, - f: impl FnOnce(&anyhow::Error, &mut Window, &mut App) -> Option + 'static, - ) { - self.prompt_err(msg, window, cx, f).detach(); - } -} diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs deleted file mode 100644 index e99f8d1dc9..0000000000 --- a/crates/workspace/src/pane.rs +++ /dev/null @@ -1,7010 +0,0 @@ -use crate::{ - CloseWindow, NewFile, NewTerminal, OpenInTerminal, OpenOptions, OpenTerminal, OpenVisible, - SplitDirection, ToggleFileFinder, ToggleProjectSymbols, ToggleZoom, Workspace, - WorkspaceItemBuilder, - invalid_item_view::InvalidItemView, - item::{ - ActivateOnClose, ClosePosition, Item, ItemBufferKind, ItemHandle, ItemSettings, - PreviewTabsSettings, ProjectItemKind, SaveOptions, ShowCloseButton, ShowDiagnostics, - TabContentParams, TabTooltipContent, WeakItemHandle, - }, - move_item, - notifications::NotifyResultExt, - toolbar::Toolbar, - workspace_settings::{AutosaveSetting, TabBarSettings, WorkspaceSettings}, -}; -use anyhow::Result; -use collections::{BTreeSet, HashMap, HashSet, VecDeque}; -use futures::{StreamExt, stream::FuturesUnordered}; -use gpui::{ - Action, AnyElement, App, AsyncWindowContext, ClickEvent, ClipboardItem, Context, Corner, Div, - DragMoveEvent, Entity, EntityId, EventEmitter, ExternalPaths, FocusHandle, FocusOutEvent, - Focusable, KeyContext, MouseButton, MouseDownEvent, NavigationDirection, Pixels, Point, - PromptLevel, Render, ScrollHandle, Subscription, Task, WeakEntity, WeakFocusHandle, Window, - actions, anchored, deferred, prelude::*, -}; -use itertools::Itertools; -use language::DiagnosticSeverity; -use parking_lot::Mutex; -use project::{DirectoryLister, Project, ProjectEntryId, ProjectPath, WorktreeId}; -use schemars::JsonSchema; -use serde::Deserialize; -use settings::{Settings, SettingsStore}; -use std::{ - any::Any, - cmp, fmt, mem, - num::NonZeroUsize, - ops::ControlFlow, - path::PathBuf, - rc::Rc, - sync::{ - Arc, - atomic::{AtomicUsize, Ordering}, - }, - time::Duration, -}; -use theme::ThemeSettings; -use ui::{ - ButtonSize, Color, ContextMenu, ContextMenuEntry, ContextMenuItem, DecoratedIcon, IconButton, - IconButtonShape, IconDecoration, IconDecorationKind, IconName, IconSize, Indicator, Label, - PopoverMenu, PopoverMenuHandle, Tab, TabBar, TabPosition, Tooltip, prelude::*, - right_click_menu, -}; -use util::{ResultExt, debug_panic, maybe, paths::PathStyle, truncate_and_remove_front}; - -/// A selected entry in e.g. project panel. -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct SelectedEntry { - pub worktree_id: WorktreeId, - pub entry_id: ProjectEntryId, -} - -/// A group of selected entries from project panel. -#[derive(Debug)] -pub struct DraggedSelection { - pub active_selection: SelectedEntry, - pub marked_selections: Arc<[SelectedEntry]>, -} - -impl DraggedSelection { - pub fn items<'a>(&'a self) -> Box + 'a> { - if self.marked_selections.contains(&self.active_selection) { - Box::new(self.marked_selections.iter()) - } else { - Box::new(std::iter::once(&self.active_selection)) - } - } -} - -#[derive(Clone, Copy, PartialEq, Debug, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum SaveIntent { - /// write all files (even if unchanged) - /// prompt before overwriting on-disk changes - Save, - /// same as Save, but without auto formatting - SaveWithoutFormat, - /// write any files that have local changes - /// prompt before overwriting on-disk changes - SaveAll, - /// always prompt for a new path - SaveAs, - /// prompt "you have unsaved changes" before writing - Close, - /// write all dirty files, don't prompt on conflict - Overwrite, - /// skip all save-related behavior - Skip, -} - -/// Activates a specific item in the pane by its index. -#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] -#[action(namespace = pane)] -pub struct ActivateItem(pub usize); - -/// Closes the currently active item in the pane. -#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] -#[action(namespace = pane)] -#[serde(deny_unknown_fields)] -pub struct CloseActiveItem { - #[serde(default)] - pub save_intent: Option, - #[serde(default)] - pub close_pinned: bool, -} - -/// Closes all inactive items in the pane. -#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] -#[action(namespace = pane)] -#[serde(deny_unknown_fields)] -#[action(deprecated_aliases = ["pane::CloseInactiveItems"])] -pub struct CloseOtherItems { - #[serde(default)] - pub save_intent: Option, - #[serde(default)] - pub close_pinned: bool, -} - -/// Closes all multibuffers in the pane. -#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] -#[action(namespace = pane)] -#[serde(deny_unknown_fields)] -pub struct CloseMultibufferItems { - #[serde(default)] - pub save_intent: Option, - #[serde(default)] - pub close_pinned: bool, -} - -/// Closes all items in the pane. -#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] -#[action(namespace = pane)] -#[serde(deny_unknown_fields)] -pub struct CloseAllItems { - #[serde(default)] - pub save_intent: Option, - #[serde(default)] - pub close_pinned: bool, -} - -/// Closes all items that have no unsaved changes. -#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] -#[action(namespace = pane)] -#[serde(deny_unknown_fields)] -pub struct CloseCleanItems { - #[serde(default)] - pub close_pinned: bool, -} - -/// Closes all items to the right of the current item. -#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] -#[action(namespace = pane)] -#[serde(deny_unknown_fields)] -pub struct CloseItemsToTheRight { - #[serde(default)] - pub close_pinned: bool, -} - -/// Closes all items to the left of the current item. -#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] -#[action(namespace = pane)] -#[serde(deny_unknown_fields)] -pub struct CloseItemsToTheLeft { - #[serde(default)] - pub close_pinned: bool, -} - -/// Reveals the current item in the project panel. -#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] -#[action(namespace = pane)] -#[serde(deny_unknown_fields)] -pub struct RevealInProjectPanel { - #[serde(skip)] - pub entry_id: Option, -} - -/// Opens the search interface with the specified configuration. -#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Default, Action)] -#[action(namespace = pane)] -#[serde(deny_unknown_fields)] -pub struct DeploySearch { - #[serde(default)] - pub replace_enabled: bool, - #[serde(default)] - pub included_files: Option, - #[serde(default)] - pub excluded_files: Option, -} - -actions!( - pane, - [ - /// Activates the previous item in the pane. - ActivatePreviousItem, - /// Activates the next item in the pane. - ActivateNextItem, - /// Activates the last item in the pane. - ActivateLastItem, - /// Switches to the alternate file. - AlternateFile, - /// Navigates back in history. - GoBack, - /// Navigates forward in history. - GoForward, - /// Joins this pane into the next pane. - JoinIntoNext, - /// Joins all panes into one. - JoinAll, - /// Reopens the most recently closed item. - ReopenClosedItem, - /// Splits the pane to the left, cloning the current item. - SplitLeft, - /// Splits the pane upward, cloning the current item. - SplitUp, - /// Splits the pane to the right, cloning the current item. - SplitRight, - /// Splits the pane downward, cloning the current item. - SplitDown, - /// Splits the pane to the left, moving the current item. - SplitAndMoveLeft, - /// Splits the pane upward, moving the current item. - SplitAndMoveUp, - /// Splits the pane to the right, moving the current item. - SplitAndMoveRight, - /// Splits the pane downward, moving the current item. - SplitAndMoveDown, - /// Splits the pane horizontally. - SplitHorizontal, - /// Splits the pane vertically. - SplitVertical, - /// Swaps the current item with the one to the left. - SwapItemLeft, - /// Swaps the current item with the one to the right. - SwapItemRight, - /// Toggles preview mode for the current tab. - TogglePreviewTab, - /// Toggles pin status for the current tab. - TogglePinTab, - /// Unpins all tabs in the pane. - UnpinAllTabs, - ] -); - -impl DeploySearch { - pub fn find() -> Self { - Self { - replace_enabled: false, - included_files: None, - excluded_files: None, - } - } -} - -const MAX_NAVIGATION_HISTORY_LEN: usize = 1024; - -pub enum Event { - AddItem { - item: Box, - }, - ActivateItem { - local: bool, - focus_changed: bool, - }, - Remove { - focus_on_pane: Option>, - }, - RemovedItem { - item: Box, - }, - Split { - direction: SplitDirection, - clone_active_item: bool, - }, - ItemPinned, - ItemUnpinned, - JoinAll, - JoinIntoNext, - ChangeItemTitle, - Focus, - ZoomIn, - ZoomOut, - UserSavedItem { - item: Box, - save_intent: SaveIntent, - }, -} - -impl fmt::Debug for Event { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Event::AddItem { item } => f - .debug_struct("AddItem") - .field("item", &item.item_id()) - .finish(), - Event::ActivateItem { local, .. } => f - .debug_struct("ActivateItem") - .field("local", local) - .finish(), - Event::Remove { .. } => f.write_str("Remove"), - Event::RemovedItem { item } => f - .debug_struct("RemovedItem") - .field("item", &item.item_id()) - .finish(), - Event::Split { - direction, - clone_active_item, - } => f - .debug_struct("Split") - .field("direction", direction) - .field("clone_active_item", clone_active_item) - .finish(), - Event::JoinAll => f.write_str("JoinAll"), - Event::JoinIntoNext => f.write_str("JoinIntoNext"), - Event::ChangeItemTitle => f.write_str("ChangeItemTitle"), - Event::Focus => f.write_str("Focus"), - Event::ZoomIn => f.write_str("ZoomIn"), - Event::ZoomOut => f.write_str("ZoomOut"), - Event::UserSavedItem { item, save_intent } => f - .debug_struct("UserSavedItem") - .field("item", &item.id()) - .field("save_intent", save_intent) - .finish(), - Event::ItemPinned => f.write_str("ItemPinned"), - Event::ItemUnpinned => f.write_str("ItemUnpinned"), - } - } -} - -/// A container for 0 to many items that are open in the workspace. -/// Treats all items uniformly via the [`ItemHandle`] trait, whether it's an editor, search results multibuffer, terminal or something else, -/// responsible for managing item tabs, focus and zoom states and drag and drop features. -/// Can be split, see `PaneGroup` for more details. -pub struct Pane { - alternate_file_items: ( - Option>, - Option>, - ), - focus_handle: FocusHandle, - items: Vec>, - activation_history: Vec, - next_activation_timestamp: Arc, - zoomed: bool, - was_focused: bool, - active_item_index: usize, - preview_item_id: Option, - last_focus_handle_by_item: HashMap, - nav_history: NavHistory, - toolbar: Entity, - pub(crate) workspace: WeakEntity, - project: WeakEntity, - pub drag_split_direction: Option, - can_drop_predicate: Option bool>>, - custom_drop_handle: Option< - Arc) -> ControlFlow<(), ()>>, - >, - can_split_predicate: - Option) -> bool>>, - can_toggle_zoom: bool, - should_display_tab_bar: Rc) -> bool>, - render_tab_bar_buttons: Rc< - dyn Fn( - &mut Pane, - &mut Window, - &mut Context, - ) -> (Option, Option), - >, - render_tab_bar: Rc) -> AnyElement>, - show_tab_bar_buttons: bool, - max_tabs: Option, - use_max_tabs: bool, - _subscriptions: Vec, - tab_bar_scroll_handle: ScrollHandle, - /// This is set to true if a user scroll has occurred more recently than a system scroll - /// We want to suppress certain system scrolls when the user has intentionally scrolled - suppress_scroll: bool, - /// Is None if navigation buttons are permanently turned off (and should not react to setting changes). - /// Otherwise, when `display_nav_history_buttons` is Some, it determines whether nav buttons should be displayed. - display_nav_history_buttons: Option, - double_click_dispatch_action: Box, - save_modals_spawned: HashSet, - close_pane_if_empty: bool, - pub new_item_context_menu_handle: PopoverMenuHandle, - pub split_item_context_menu_handle: PopoverMenuHandle, - pinned_tab_count: usize, - diagnostics: HashMap, - zoom_out_on_close: bool, - diagnostic_summary_update: Task<()>, - /// If a certain project item wants to get recreated with specific data, it can persist its data before the recreation here. - pub project_item_restoration_data: HashMap>, -} - -pub struct ActivationHistoryEntry { - pub entity_id: EntityId, - pub timestamp: usize, -} - -pub struct ItemNavHistory { - history: NavHistory, - item: Arc, - is_preview: bool, -} - -#[derive(Clone)] -pub struct NavHistory(Arc>); - -struct NavHistoryState { - mode: NavigationMode, - backward_stack: VecDeque, - forward_stack: VecDeque, - closed_stack: VecDeque, - paths_by_item: HashMap)>, - pane: WeakEntity, - next_timestamp: Arc, -} - -#[derive(Debug, Default, Copy, Clone)] -pub enum NavigationMode { - #[default] - Normal, - GoingBack, - GoingForward, - ClosingItem, - ReopeningClosedItem, - Disabled, -} - -pub struct NavigationEntry { - pub item: Arc, - pub data: Option>, - pub timestamp: usize, - pub is_preview: bool, -} - -#[derive(Clone)] -pub struct DraggedTab { - pub pane: Entity, - pub item: Box, - pub ix: usize, - pub detail: usize, - pub is_active: bool, -} - -impl EventEmitter for Pane {} - -pub enum Side { - Left, - Right, -} - -#[derive(Copy, Clone)] -enum PinOperation { - Pin, - Unpin, -} - -impl Pane { - pub fn new( - workspace: WeakEntity, - project: Entity, - next_timestamp: Arc, - can_drop_predicate: Option bool + 'static>>, - double_click_dispatch_action: Box, - use_max_tabs: bool, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let focus_handle = cx.focus_handle(); - let max_tabs = if use_max_tabs { - WorkspaceSettings::get_global(cx).max_tabs - } else { - None - }; - - let subscriptions = vec![ - cx.on_focus(&focus_handle, window, Pane::focus_in), - cx.on_focus_in(&focus_handle, window, Pane::focus_in), - cx.on_focus_out(&focus_handle, window, Pane::focus_out), - cx.observe_global_in::(window, Self::settings_changed), - cx.subscribe(&project, Self::project_events), - ]; - - let handle = cx.entity().downgrade(); - - Self { - alternate_file_items: (None, None), - focus_handle, - items: Vec::new(), - activation_history: Vec::new(), - next_activation_timestamp: next_timestamp.clone(), - was_focused: false, - zoomed: false, - active_item_index: 0, - preview_item_id: None, - max_tabs, - use_max_tabs, - last_focus_handle_by_item: Default::default(), - nav_history: NavHistory(Arc::new(Mutex::new(NavHistoryState { - mode: NavigationMode::Normal, - backward_stack: Default::default(), - forward_stack: Default::default(), - closed_stack: Default::default(), - paths_by_item: Default::default(), - pane: handle, - next_timestamp, - }))), - toolbar: cx.new(|_| Toolbar::new()), - tab_bar_scroll_handle: ScrollHandle::new(), - suppress_scroll: false, - drag_split_direction: None, - workspace, - project: project.downgrade(), - can_drop_predicate, - custom_drop_handle: None, - can_split_predicate: None, - can_toggle_zoom: true, - should_display_tab_bar: Rc::new(|_, cx| TabBarSettings::get_global(cx).show), - render_tab_bar_buttons: Rc::new(default_render_tab_bar_buttons), - render_tab_bar: Rc::new(Self::render_tab_bar), - show_tab_bar_buttons: TabBarSettings::get_global(cx).show_tab_bar_buttons, - display_nav_history_buttons: Some( - TabBarSettings::get_global(cx).show_nav_history_buttons, - ), - _subscriptions: subscriptions, - double_click_dispatch_action, - save_modals_spawned: HashSet::default(), - close_pane_if_empty: true, - split_item_context_menu_handle: Default::default(), - new_item_context_menu_handle: Default::default(), - pinned_tab_count: 0, - diagnostics: Default::default(), - zoom_out_on_close: true, - diagnostic_summary_update: Task::ready(()), - project_item_restoration_data: HashMap::default(), - } - } - - fn alternate_file(&mut self, _: &AlternateFile, window: &mut Window, cx: &mut Context) { - let (_, alternative) = &self.alternate_file_items; - if let Some(alternative) = alternative { - let existing = self - .items() - .find_position(|item| item.item_id() == alternative.id()); - if let Some((ix, _)) = existing { - self.activate_item(ix, true, true, window, cx); - } else if let Some(upgraded) = alternative.upgrade() { - self.add_item(upgraded, true, true, None, window, cx); - } - } - } - - pub fn track_alternate_file_items(&mut self) { - if let Some(item) = self.active_item().map(|item| item.downgrade_item()) { - let (current, _) = &self.alternate_file_items; - match current { - Some(current) => { - if current.id() != item.id() { - self.alternate_file_items = - (Some(item), self.alternate_file_items.0.take()); - } - } - None => { - self.alternate_file_items = (Some(item), None); - } - } - } - } - - pub fn has_focus(&self, window: &Window, cx: &App) -> bool { - // We not only check whether our focus handle contains focus, but also - // whether the active item might have focus, because we might have just activated an item - // that hasn't rendered yet. - // Before the next render, we might transfer focus - // to the item, and `focus_handle.contains_focus` returns false because the `active_item` - // is not hooked up to us in the dispatch tree. - self.focus_handle.contains_focused(window, cx) - || self - .active_item() - .is_some_and(|item| item.item_focus_handle(cx).contains_focused(window, cx)) - } - - fn focus_in(&mut self, window: &mut Window, cx: &mut Context) { - if !self.was_focused { - self.was_focused = true; - self.update_history(self.active_item_index); - if !self.suppress_scroll && self.items.get(self.active_item_index).is_some() { - self.update_active_tab(self.active_item_index); - } - cx.emit(Event::Focus); - cx.notify(); - } - - self.toolbar.update(cx, |toolbar, cx| { - toolbar.focus_changed(true, window, cx); - }); - - if let Some(active_item) = self.active_item() { - if self.focus_handle.is_focused(window) { - // Schedule a redraw next frame, so that the focus changes below take effect - cx.on_next_frame(window, |_, _, cx| { - cx.notify(); - }); - - // Pane was focused directly. We need to either focus a view inside the active item, - // or focus the active item itself - if let Some(weak_last_focus_handle) = - self.last_focus_handle_by_item.get(&active_item.item_id()) - && let Some(focus_handle) = weak_last_focus_handle.upgrade() - { - focus_handle.focus(window); - return; - } - - active_item.item_focus_handle(cx).focus(window); - } else if let Some(focused) = window.focused(cx) - && !self.context_menu_focused(window, cx) - { - self.last_focus_handle_by_item - .insert(active_item.item_id(), focused.downgrade()); - } - } - } - - pub fn context_menu_focused(&self, window: &mut Window, cx: &mut Context) -> bool { - self.new_item_context_menu_handle.is_focused(window, cx) - || self.split_item_context_menu_handle.is_focused(window, cx) - } - - fn focus_out(&mut self, _event: FocusOutEvent, window: &mut Window, cx: &mut Context) { - self.was_focused = false; - self.toolbar.update(cx, |toolbar, cx| { - toolbar.focus_changed(false, window, cx); - }); - - cx.notify(); - } - - fn project_events( - &mut self, - _project: Entity, - event: &project::Event, - cx: &mut Context, - ) { - match event { - project::Event::DiskBasedDiagnosticsFinished { .. } - | project::Event::DiagnosticsUpdated { .. } => { - if ItemSettings::get_global(cx).show_diagnostics != ShowDiagnostics::Off { - self.diagnostic_summary_update = cx.spawn(async move |this, cx| { - cx.background_executor() - .timer(Duration::from_millis(30)) - .await; - this.update(cx, |this, cx| { - this.update_diagnostics(cx); - cx.notify(); - }) - .log_err(); - }); - } - } - _ => {} - } - } - - fn update_diagnostics(&mut self, cx: &mut Context) { - let Some(project) = self.project.upgrade() else { - return; - }; - let show_diagnostics = ItemSettings::get_global(cx).show_diagnostics; - self.diagnostics = if show_diagnostics != ShowDiagnostics::Off { - project - .read(cx) - .diagnostic_summaries(false, cx) - .filter_map(|(project_path, _, diagnostic_summary)| { - if diagnostic_summary.error_count > 0 { - Some((project_path, DiagnosticSeverity::ERROR)) - } else if diagnostic_summary.warning_count > 0 - && show_diagnostics != ShowDiagnostics::Errors - { - Some((project_path, DiagnosticSeverity::WARNING)) - } else { - None - } - }) - .collect() - } else { - HashMap::default() - } - } - - fn settings_changed(&mut self, window: &mut Window, cx: &mut Context) { - let tab_bar_settings = TabBarSettings::get_global(cx); - let new_max_tabs = WorkspaceSettings::get_global(cx).max_tabs; - - if let Some(display_nav_history_buttons) = self.display_nav_history_buttons.as_mut() { - *display_nav_history_buttons = tab_bar_settings.show_nav_history_buttons; - } - - self.show_tab_bar_buttons = tab_bar_settings.show_tab_bar_buttons; - - if !PreviewTabsSettings::get_global(cx).enabled { - self.preview_item_id = None; - } - - if self.use_max_tabs && new_max_tabs != self.max_tabs { - self.max_tabs = new_max_tabs; - self.close_items_on_settings_change(window, cx); - } - - self.update_diagnostics(cx); - cx.notify(); - } - - pub fn active_item_index(&self) -> usize { - self.active_item_index - } - - pub fn activation_history(&self) -> &[ActivationHistoryEntry] { - &self.activation_history - } - - pub fn set_should_display_tab_bar(&mut self, should_display_tab_bar: F) - where - F: 'static + Fn(&Window, &mut Context) -> bool, - { - self.should_display_tab_bar = Rc::new(should_display_tab_bar); - } - - pub fn set_can_split( - &mut self, - can_split_predicate: Option< - Arc) -> bool + 'static>, - >, - ) { - self.can_split_predicate = can_split_predicate; - } - - pub fn set_can_toggle_zoom(&mut self, can_toggle_zoom: bool, cx: &mut Context) { - self.can_toggle_zoom = can_toggle_zoom; - cx.notify(); - } - - pub fn set_close_pane_if_empty(&mut self, close_pane_if_empty: bool, cx: &mut Context) { - self.close_pane_if_empty = close_pane_if_empty; - cx.notify(); - } - - pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut Context) { - self.toolbar.update(cx, |toolbar, cx| { - toolbar.set_can_navigate(can_navigate, cx); - }); - cx.notify(); - } - - pub fn set_render_tab_bar(&mut self, cx: &mut Context, render: F) - where - F: 'static + Fn(&mut Pane, &mut Window, &mut Context) -> AnyElement, - { - self.render_tab_bar = Rc::new(render); - cx.notify(); - } - - pub fn set_render_tab_bar_buttons(&mut self, cx: &mut Context, render: F) - where - F: 'static - + Fn( - &mut Pane, - &mut Window, - &mut Context, - ) -> (Option, Option), - { - self.render_tab_bar_buttons = Rc::new(render); - cx.notify(); - } - - pub fn set_custom_drop_handle(&mut self, cx: &mut Context, handle: F) - where - F: 'static - + Fn(&mut Pane, &dyn Any, &mut Window, &mut Context) -> ControlFlow<(), ()>, - { - self.custom_drop_handle = Some(Arc::new(handle)); - cx.notify(); - } - - pub fn nav_history_for_item(&self, item: &Entity) -> ItemNavHistory { - ItemNavHistory { - history: self.nav_history.clone(), - item: Arc::new(item.downgrade()), - is_preview: self.preview_item_id == Some(item.item_id()), - } - } - - pub fn nav_history(&self) -> &NavHistory { - &self.nav_history - } - - pub fn nav_history_mut(&mut self) -> &mut NavHistory { - &mut self.nav_history - } - - pub fn disable_history(&mut self) { - self.nav_history.disable(); - } - - pub fn enable_history(&mut self) { - self.nav_history.enable(); - } - - pub fn can_navigate_backward(&self) -> bool { - !self.nav_history.0.lock().backward_stack.is_empty() - } - - pub fn can_navigate_forward(&self) -> bool { - !self.nav_history.0.lock().forward_stack.is_empty() - } - - pub fn navigate_backward(&mut self, _: &GoBack, window: &mut Window, cx: &mut Context) { - if let Some(workspace) = self.workspace.upgrade() { - let pane = cx.entity().downgrade(); - window.defer(cx, move |window, cx| { - workspace.update(cx, |workspace, cx| { - workspace.go_back(pane, window, cx).detach_and_log_err(cx) - }) - }) - } - } - - fn navigate_forward(&mut self, _: &GoForward, window: &mut Window, cx: &mut Context) { - if let Some(workspace) = self.workspace.upgrade() { - let pane = cx.entity().downgrade(); - window.defer(cx, move |window, cx| { - workspace.update(cx, |workspace, cx| { - workspace - .go_forward(pane, window, cx) - .detach_and_log_err(cx) - }) - }) - } - } - - fn history_updated(&mut self, cx: &mut Context) { - self.toolbar.update(cx, |_, cx| cx.notify()); - } - - pub fn preview_item_id(&self) -> Option { - self.preview_item_id - } - - pub fn preview_item(&self) -> Option> { - self.preview_item_id - .and_then(|id| self.items.iter().find(|item| item.item_id() == id)) - .cloned() - } - - pub fn preview_item_idx(&self) -> Option { - if let Some(preview_item_id) = self.preview_item_id { - self.items - .iter() - .position(|item| item.item_id() == preview_item_id) - } else { - None - } - } - - pub fn is_active_preview_item(&self, item_id: EntityId) -> bool { - self.preview_item_id == Some(item_id) - } - - /// Promotes the item with the given ID to not be a preview item. - /// This does nothing if it wasn't already a preview item. - pub fn unpreview_item_if_preview(&mut self, item_id: EntityId) { - if self.is_active_preview_item(item_id) { - self.preview_item_id = None; - } - } - - /// Marks the item with the given ID as the preview item. - /// This will be ignored if the global setting `preview_tabs` is disabled. - /// - /// The old preview item (if there was one) is closed and its index is returned. - pub fn replace_preview_item_id( - &mut self, - item_id: EntityId, - window: &mut Window, - cx: &mut Context, - ) -> Option { - let idx = self.close_current_preview_item(window, cx); - self.set_preview_item_id(Some(item_id), cx); - idx - } - - /// Marks the item with the given ID as the preview item. - /// This will be ignored if the global setting `preview_tabs` is disabled. - /// - /// This is a low-level method. Prefer `unpreview_item_if_preview()` or `set_new_preview_item()`. - pub(crate) fn set_preview_item_id(&mut self, item_id: Option, cx: &App) { - if item_id.is_none() || PreviewTabsSettings::get_global(cx).enabled { - self.preview_item_id = item_id; - } - } - - /// Should only be used when deserializing a pane. - pub fn set_pinned_count(&mut self, count: usize) { - self.pinned_tab_count = count; - } - - pub fn pinned_count(&self) -> usize { - self.pinned_tab_count - } - - pub fn handle_item_edit(&mut self, item_id: EntityId, cx: &App) { - if let Some(preview_item) = self.preview_item() - && preview_item.item_id() == item_id - && !preview_item.preserve_preview(cx) - { - self.unpreview_item_if_preview(item_id); - } - } - - pub(crate) fn open_item( - &mut self, - project_entry_id: Option, - project_path: ProjectPath, - focus_item: bool, - allow_preview: bool, - activate: bool, - suggested_position: Option, - window: &mut Window, - cx: &mut Context, - build_item: WorkspaceItemBuilder, - ) -> Box { - let mut existing_item = None; - if let Some(project_entry_id) = project_entry_id { - for (index, item) in self.items.iter().enumerate() { - if item.buffer_kind(cx) == ItemBufferKind::Singleton - && item.project_entry_ids(cx).as_slice() == [project_entry_id] - { - let item = item.boxed_clone(); - existing_item = Some((index, item)); - break; - } - } - } else { - for (index, item) in self.items.iter().enumerate() { - if item.buffer_kind(cx) == ItemBufferKind::Singleton - && item.project_path(cx).as_ref() == Some(&project_path) - { - let item = item.boxed_clone(); - existing_item = Some((index, item)); - break; - } - } - } - - let set_up_existing_item = - |index: usize, pane: &mut Self, window: &mut Window, cx: &mut Context| { - if !allow_preview && let Some(item) = pane.items.get(index) { - pane.unpreview_item_if_preview(item.item_id()); - } - 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.replace_preview_item_id(new_item.item_id(), window, cx); - } - - if let Some(text) = new_item.telemetry_event_text(cx) { - telemetry::event!(text); - } - - 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); - existing_item - } else { - // If the item is being opened as preview and we have an existing preview tab, - // open the new item in the position of the existing preview tab. - let destination_index = if allow_preview { - self.close_current_preview_item(window, cx) - } else { - suggested_position - }; - - 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 - } - } - } - - pub fn close_current_preview_item( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> Option { - let item_idx = self.preview_item_idx()?; - let id = self.preview_item_id()?; - self.set_preview_item_id(None, cx); - - let prev_active_item_index = self.active_item_index; - self.remove_item(id, false, false, window, cx); - self.active_item_index = prev_active_item_index; - - if item_idx < self.items.len() { - Some(item_idx) - } else { - None - } - } - - pub fn add_item_inner( - &mut self, - item: Box, - activate_pane: bool, - focus_item: bool, - activate: bool, - destination_index: Option, - window: &mut Window, - cx: &mut Context, - ) { - let item_already_exists = self - .items - .iter() - .any(|existing_item| existing_item.item_id() == item.item_id()); - - if !item_already_exists { - self.close_items_on_item_open(window, cx); - } - - if item.buffer_kind(cx) == ItemBufferKind::Singleton - && let Some(&entry_id) = item.project_entry_ids(cx).first() - { - let Some(project) = self.project.upgrade() else { - return; - }; - - let project = project.read(cx); - if let Some(project_path) = project.path_for_entry(entry_id, cx) { - let abs_path = project.absolute_path(&project_path, cx); - self.nav_history - .0 - .lock() - .paths_by_item - .insert(item.item_id(), (project_path, abs_path)); - } - } - // If no destination index is specified, add or move the item after the - // active item (or at the start of tab bar, if the active item is pinned) - let mut insertion_index = { - cmp::min( - if let Some(destination_index) = destination_index { - destination_index - } else { - cmp::max(self.active_item_index + 1, self.pinned_count()) - }, - self.items.len(), - ) - }; - - // Does the item already exist? - let project_entry_id = if item.buffer_kind(cx) == ItemBufferKind::Singleton { - item.project_entry_ids(cx).first().copied() - } else { - None - }; - - let existing_item_index = self.items.iter().position(|existing_item| { - if existing_item.item_id() == item.item_id() { - true - } else if existing_item.buffer_kind(cx) == ItemBufferKind::Singleton { - existing_item - .project_entry_ids(cx) - .first() - .is_some_and(|existing_entry_id| { - Some(existing_entry_id) == project_entry_id.as_ref() - }) - } else { - false - } - }); - if let Some(existing_item_index) = existing_item_index { - // If the item already exists, move it to the desired destination and activate it - - if existing_item_index != insertion_index { - let existing_item_is_active = existing_item_index == self.active_item_index; - - // If the caller didn't specify a destination and the added item is already - // the active one, don't move it - if existing_item_is_active && destination_index.is_none() { - insertion_index = existing_item_index; - } else { - self.items.remove(existing_item_index); - if existing_item_index < self.active_item_index { - self.active_item_index -= 1; - } - insertion_index = insertion_index.min(self.items.len()); - - self.items.insert(insertion_index, item.clone()); - - if existing_item_is_active { - self.active_item_index = insertion_index; - } else if insertion_index <= self.active_item_index { - self.active_item_index += 1; - } - } - - cx.notify(); - } - - if activate { - self.activate_item(insertion_index, activate_pane, focus_item, window, cx); - } - } else { - self.items.insert(insertion_index, item.clone()); - cx.notify(); - - if activate { - if insertion_index <= self.active_item_index - && self.preview_item_idx() != Some(self.active_item_index) - { - self.active_item_index += 1; - } - - self.activate_item(insertion_index, activate_pane, focus_item, window, cx); - } - } - - cx.emit(Event::AddItem { item }); - } - - pub fn add_item( - &mut self, - item: Box, - activate_pane: bool, - focus_item: bool, - destination_index: Option, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(text) = item.telemetry_event_text(cx) { - telemetry::event!(text); - } - - self.add_item_inner( - item, - activate_pane, - focus_item, - true, - destination_index, - window, - cx, - ) - } - - pub fn items_len(&self) -> usize { - self.items.len() - } - - pub fn items(&self) -> impl DoubleEndedIterator> { - self.items.iter() - } - - pub fn items_of_type(&self) -> impl '_ + Iterator> { - self.items - .iter() - .filter_map(|item| item.to_any_view().downcast().ok()) - } - - pub fn active_item(&self) -> Option> { - self.items.get(self.active_item_index).cloned() - } - - fn active_item_id(&self) -> EntityId { - self.items[self.active_item_index].item_id() - } - - pub fn pixel_position_of_cursor(&self, cx: &App) -> Option> { - self.items - .get(self.active_item_index)? - .pixel_position_of_cursor(cx) - } - - pub fn item_for_entry( - &self, - entry_id: ProjectEntryId, - cx: &App, - ) -> Option> { - self.items.iter().find_map(|item| { - if item.buffer_kind(cx) == ItemBufferKind::Singleton - && (item.project_entry_ids(cx).as_slice() == [entry_id]) - { - Some(item.boxed_clone()) - } else { - None - } - }) - } - - pub fn item_for_path( - &self, - project_path: ProjectPath, - cx: &App, - ) -> Option> { - self.items.iter().find_map(move |item| { - if item.buffer_kind(cx) == ItemBufferKind::Singleton - && (item.project_path(cx).as_slice() == [project_path.clone()]) - { - Some(item.boxed_clone()) - } else { - None - } - }) - } - - pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option { - self.index_for_item_id(item.item_id()) - } - - fn index_for_item_id(&self, item_id: EntityId) -> Option { - self.items.iter().position(|i| i.item_id() == item_id) - } - - pub fn item_for_index(&self, ix: usize) -> Option<&dyn ItemHandle> { - self.items.get(ix).map(|i| i.as_ref()) - } - - pub fn toggle_zoom(&mut self, _: &ToggleZoom, window: &mut Window, cx: &mut Context) { - if !self.can_toggle_zoom { - cx.propagate(); - } else if self.zoomed { - cx.emit(Event::ZoomOut); - } else if !self.items.is_empty() { - if !self.focus_handle.contains_focused(window, cx) { - cx.focus_self(window); - } - cx.emit(Event::ZoomIn); - } - } - - pub fn activate_item( - &mut self, - index: usize, - activate_pane: bool, - focus_item: bool, - window: &mut Window, - cx: &mut Context, - ) { - use NavigationMode::{GoingBack, GoingForward}; - if index < self.items.len() { - let prev_active_item_ix = mem::replace(&mut self.active_item_index, index); - if (prev_active_item_ix != self.active_item_index - || matches!(self.nav_history.mode(), GoingBack | GoingForward)) - && let Some(prev_item) = self.items.get(prev_active_item_ix) - { - prev_item.deactivated(window, cx); - } - self.update_history(index); - self.update_toolbar(window, cx); - self.update_status_bar(window, cx); - - if focus_item { - self.focus_active_item(window, cx); - } - - cx.emit(Event::ActivateItem { - local: activate_pane, - focus_changed: focus_item, - }); - - self.update_active_tab(index); - cx.notify(); - } - } - - fn update_active_tab(&mut self, index: usize) { - if !self.is_tab_pinned(index) { - self.suppress_scroll = false; - self.tab_bar_scroll_handle.scroll_to_item(index); - } - } - - fn update_history(&mut self, index: usize) { - if let Some(newly_active_item) = self.items.get(index) { - self.activation_history - .retain(|entry| entry.entity_id != newly_active_item.item_id()); - self.activation_history.push(ActivationHistoryEntry { - entity_id: newly_active_item.item_id(), - timestamp: self - .next_activation_timestamp - .fetch_add(1, Ordering::SeqCst), - }); - } - } - - pub fn activate_previous_item( - &mut self, - _: &ActivatePreviousItem, - window: &mut Window, - cx: &mut Context, - ) { - let mut index = self.active_item_index; - if index > 0 { - index -= 1; - } else if !self.items.is_empty() { - index = self.items.len() - 1; - } - self.activate_item(index, true, true, window, cx); - } - - pub fn activate_next_item( - &mut self, - _: &ActivateNextItem, - window: &mut Window, - cx: &mut Context, - ) { - let mut index = self.active_item_index; - if index + 1 < self.items.len() { - index += 1; - } else { - index = 0; - } - self.activate_item(index, true, true, window, cx); - } - - pub fn swap_item_left( - &mut self, - _: &SwapItemLeft, - window: &mut Window, - cx: &mut Context, - ) { - let index = self.active_item_index; - if index == 0 { - return; - } - - self.items.swap(index, index - 1); - self.activate_item(index - 1, true, true, window, cx); - } - - pub fn swap_item_right( - &mut self, - _: &SwapItemRight, - window: &mut Window, - cx: &mut Context, - ) { - let index = self.active_item_index; - if index + 1 >= self.items.len() { - return; - } - - self.items.swap(index, index + 1); - 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, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - if self.items.is_empty() { - // Close the window when there's no active items to close, if configured - if WorkspaceSettings::get_global(cx) - .when_closing_with_no_tabs - .should_close() - { - window.dispatch_action(Box::new(CloseWindow), cx); - } - - return Task::ready(Ok(())); - } - if self.is_tab_pinned(self.active_item_index) && !action.close_pinned { - // Activate any non-pinned tab in same pane - let non_pinned_tab_index = self - .items() - .enumerate() - .find(|(index, _item)| !self.is_tab_pinned(*index)) - .map(|(index, _item)| index); - if let Some(index) = non_pinned_tab_index { - self.activate_item(index, false, false, window, cx); - return Task::ready(Ok(())); - } - - // Activate any non-pinned tab in different pane - let current_pane = cx.entity(); - self.workspace - .update(cx, |workspace, cx| { - let panes = workspace.center.panes(); - let pane_with_unpinned_tab = panes.iter().find(|pane| { - if **pane == ¤t_pane { - return false; - } - pane.read(cx).has_unpinned_tabs() - }); - if let Some(pane) = pane_with_unpinned_tab { - pane.update(cx, |pane, cx| pane.activate_unpinned_tab(window, cx)); - } - }) - .ok(); - - return Task::ready(Ok(())); - }; - - let active_item_id = self.active_item_id(); - - self.close_item_by_id( - active_item_id, - action.save_intent.unwrap_or(SaveIntent::Close), - window, - cx, - ) - } - - pub fn close_item_by_id( - &mut self, - item_id_to_close: EntityId, - save_intent: SaveIntent, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.close_items(window, cx, save_intent, move |view_id| { - view_id == item_id_to_close - }) - } - - pub fn close_other_items( - &mut self, - action: &CloseOtherItems, - target_item_id: Option, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - if self.items.is_empty() { - return Task::ready(Ok(())); - } - - let active_item_id = match target_item_id { - Some(result) => result, - None => self.active_item_id(), - }; - - let pinned_item_ids = self.pinned_item_ids(); - - self.close_items( - window, - cx, - action.save_intent.unwrap_or(SaveIntent::Close), - move |item_id| { - item_id != active_item_id - && (action.close_pinned || !pinned_item_ids.contains(&item_id)) - }, - ) - } - - pub fn close_multibuffer_items( - &mut self, - action: &CloseMultibufferItems, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - if self.items.is_empty() { - return Task::ready(Ok(())); - } - - let pinned_item_ids = self.pinned_item_ids(); - let multibuffer_items = self.multibuffer_item_ids(cx); - - self.close_items( - window, - cx, - action.save_intent.unwrap_or(SaveIntent::Close), - move |item_id| { - (action.close_pinned || !pinned_item_ids.contains(&item_id)) - && multibuffer_items.contains(&item_id) - }, - ) - } - - pub fn close_clean_items( - &mut self, - action: &CloseCleanItems, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - if self.items.is_empty() { - return Task::ready(Ok(())); - } - - let clean_item_ids = self.clean_item_ids(cx); - let pinned_item_ids = self.pinned_item_ids(); - - self.close_items(window, cx, SaveIntent::Close, move |item_id| { - clean_item_ids.contains(&item_id) - && (action.close_pinned || !pinned_item_ids.contains(&item_id)) - }) - } - - pub fn close_items_to_the_left_by_id( - &mut self, - item_id: Option, - action: &CloseItemsToTheLeft, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.close_items_to_the_side_by_id(item_id, Side::Left, action.close_pinned, window, cx) - } - - pub fn close_items_to_the_right_by_id( - &mut self, - item_id: Option, - action: &CloseItemsToTheRight, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.close_items_to_the_side_by_id(item_id, Side::Right, action.close_pinned, window, cx) - } - - pub fn close_items_to_the_side_by_id( - &mut self, - item_id: Option, - side: Side, - close_pinned: bool, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - if self.items.is_empty() { - return Task::ready(Ok(())); - } - - let item_id = item_id.unwrap_or_else(|| self.active_item_id()); - let to_the_side_item_ids = self.to_the_side_item_ids(item_id, side); - let pinned_item_ids = self.pinned_item_ids(); - - self.close_items(window, cx, SaveIntent::Close, move |item_id| { - to_the_side_item_ids.contains(&item_id) - && (close_pinned || !pinned_item_ids.contains(&item_id)) - }) - } - - pub fn close_all_items( - &mut self, - action: &CloseAllItems, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - if self.items.is_empty() { - return Task::ready(Ok(())); - } - - let pinned_item_ids = self.pinned_item_ids(); - - self.close_items( - window, - cx, - action.save_intent.unwrap_or(SaveIntent::Close), - |item_id| action.close_pinned || !pinned_item_ids.contains(&item_id), - ) - } - - fn close_items_on_item_open(&mut self, window: &mut Window, cx: &mut Context) { - let target = self.max_tabs.map(|m| m.get()); - let protect_active_item = false; - self.close_items_to_target_count(target, protect_active_item, window, cx); - } - - fn close_items_on_settings_change(&mut self, window: &mut Window, cx: &mut Context) { - let target = self.max_tabs.map(|m| m.get() + 1); - // The active item in this case is the settings.json file, which should be protected from being closed - let protect_active_item = true; - self.close_items_to_target_count(target, protect_active_item, window, cx); - } - - fn close_items_to_target_count( - &mut self, - target_count: Option, - protect_active_item: bool, - window: &mut Window, - cx: &mut Context, - ) { - let Some(target_count) = target_count else { - return; - }; - - let mut index_list = Vec::new(); - let mut items_len = self.items_len(); - let mut indexes: HashMap = HashMap::default(); - let active_ix = self.active_item_index(); - - for (index, item) in self.items.iter().enumerate() { - indexes.insert(item.item_id(), index); - } - - // Close least recently used items to reach target count. - // The target count is allowed to be exceeded, as we protect pinned - // items, dirty items, and sometimes, the active item. - for entry in self.activation_history.iter() { - if items_len < target_count { - break; - } - - let Some(&index) = indexes.get(&entry.entity_id) else { - continue; - }; - - if protect_active_item && index == active_ix { - continue; - } - - if let Some(true) = self.items.get(index).map(|item| item.is_dirty(cx)) { - continue; - } - - if self.is_tab_pinned(index) { - continue; - } - - index_list.push(index); - items_len -= 1; - } - // The sort and reverse is necessary since we remove items - // using their index position, hence removing from the end - // of the list first to avoid changing indexes. - index_list.sort_unstable(); - index_list - .iter() - .rev() - .for_each(|&index| self._remove_item(index, false, false, None, window, cx)); - } - - // Usually when you close an item that has unsaved changes, we prompt you to - // save it. That said, if you still have the buffer open in a different pane - // we can close this one without fear of losing data. - pub fn skip_save_on_close(item: &dyn ItemHandle, workspace: &Workspace, cx: &App) -> bool { - let mut dirty_project_item_ids = Vec::new(); - item.for_each_project_item(cx, &mut |project_item_id, project_item| { - if project_item.is_dirty() { - dirty_project_item_ids.push(project_item_id); - } - }); - if dirty_project_item_ids.is_empty() { - return !(item.buffer_kind(cx) == ItemBufferKind::Singleton && item.is_dirty(cx)); - } - - for open_item in workspace.items(cx) { - if open_item.item_id() == item.item_id() { - continue; - } - if open_item.buffer_kind(cx) != ItemBufferKind::Singleton { - continue; - } - let other_project_item_ids = open_item.project_item_model_ids(cx); - dirty_project_item_ids.retain(|id| !other_project_item_ids.contains(id)); - } - dirty_project_item_ids.is_empty() - } - - pub(super) fn file_names_for_prompt( - items: &mut dyn Iterator>, - cx: &App, - ) -> String { - let mut file_names = BTreeSet::default(); - for item in items { - item.for_each_project_item(cx, &mut |_, project_item| { - if !project_item.is_dirty() { - return; - } - let filename = project_item - .project_path(cx) - .and_then(|path| path.path.file_name().map(ToOwned::to_owned)); - file_names.insert(filename.unwrap_or("untitled".to_string())); - }); - } - if file_names.len() > 6 { - format!( - "{}\n.. and {} more", - file_names.iter().take(5).join("\n"), - file_names.len() - 5 - ) - } else { - file_names.into_iter().join("\n") - } - } - - pub fn close_items( - &self, - window: &mut Window, - cx: &mut Context, - mut save_intent: SaveIntent, - should_close: impl Fn(EntityId) -> bool, - ) -> Task> { - // Find the items to close. - let mut items_to_close = Vec::new(); - for item in &self.items { - if should_close(item.item_id()) { - items_to_close.push(item.boxed_clone()); - } - } - - let active_item_id = self.active_item().map(|item| item.item_id()); - - items_to_close.sort_by_key(|item| { - let path = item.project_path(cx); - // Put the currently active item at the end, because if the currently active item is not closed last - // closing the currently active item will cause the focus to switch to another item - // This will cause Zed to expand the content of the currently active item - // - // Beyond that sort in order of project path, with untitled files and multibuffers coming last. - (active_item_id == Some(item.item_id()), path.is_none(), path) - }); - - let workspace = self.workspace.clone(); - let Some(project) = self.project.upgrade() else { - return Task::ready(Ok(())); - }; - cx.spawn_in(window, async move |pane, cx| { - let dirty_items = workspace.update(cx, |workspace, cx| { - items_to_close - .iter() - .filter(|item| { - item.is_dirty(cx) && !Self::skip_save_on_close(item.as_ref(), workspace, cx) - }) - .map(|item| item.boxed_clone()) - .collect::>() - })?; - - if save_intent == SaveIntent::Close && dirty_items.len() > 1 { - let answer = pane.update_in(cx, |_, window, cx| { - let detail = Self::file_names_for_prompt(&mut dirty_items.iter(), cx); - window.prompt( - PromptLevel::Warning, - "Do you want to save changes to the following files?", - Some(&detail), - &["Save all", "Discard all", "Cancel"], - cx, - ) - })?; - match answer.await { - Ok(0) => save_intent = SaveIntent::SaveAll, - Ok(1) => save_intent = SaveIntent::Skip, - Ok(2) => return Ok(()), - _ => {} - } - } - - for item_to_close in items_to_close { - let mut should_save = true; - if save_intent == SaveIntent::Close { - workspace.update(cx, |workspace, cx| { - if Self::skip_save_on_close(item_to_close.as_ref(), workspace, cx) { - should_save = false; - } - })?; - } - - if should_save { - match Self::save_item(project.clone(), &pane, &*item_to_close, save_intent, cx) - .await - { - Ok(success) => { - if !success { - break; - } - } - Err(err) => { - let answer = pane.update_in(cx, |_, window, cx| { - let detail = Self::file_names_for_prompt( - &mut [&item_to_close].into_iter(), - cx, - ); - window.prompt( - PromptLevel::Warning, - &format!("Unable to save file: {}", &err), - Some(&detail), - &["Close Without Saving", "Cancel"], - cx, - ) - })?; - match answer.await { - Ok(0) => {} - Ok(1..) | Err(_) => break, - } - } - } - } - - // Remove the item from the pane. - pane.update_in(cx, |pane, window, cx| { - pane.remove_item( - item_to_close.item_id(), - false, - pane.close_pane_if_empty, - window, - cx, - ); - }) - .ok(); - } - - pane.update(cx, |_, cx| cx.notify()).ok(); - Ok(()) - }) - } - - pub fn take_active_item( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> Option> { - let item = self.active_item()?; - self.remove_item(item.item_id(), false, false, window, cx); - Some(item) - } - - pub fn remove_item( - &mut self, - item_id: EntityId, - activate_pane: bool, - close_pane_if_empty: bool, - window: &mut Window, - cx: &mut Context, - ) { - let Some(item_index) = self.index_for_item_id(item_id) else { - return; - }; - self._remove_item( - item_index, - activate_pane, - close_pane_if_empty, - None, - window, - cx, - ) - } - - pub fn remove_item_and_focus_on_pane( - &mut self, - item_index: usize, - activate_pane: bool, - focus_on_pane_if_closed: Entity, - window: &mut Window, - cx: &mut Context, - ) { - self._remove_item( - item_index, - activate_pane, - true, - Some(focus_on_pane_if_closed), - window, - cx, - ) - } - - fn _remove_item( - &mut self, - item_index: usize, - activate_pane: bool, - close_pane_if_empty: bool, - focus_on_pane_if_closed: Option>, - window: &mut Window, - cx: &mut Context, - ) { - let activate_on_close = &ItemSettings::get_global(cx).activate_on_close; - self.activation_history - .retain(|entry| entry.entity_id != self.items[item_index].item_id()); - - if self.is_tab_pinned(item_index) { - self.pinned_tab_count -= 1; - } - if item_index == self.active_item_index { - let left_neighbour_index = || item_index.min(self.items.len()).saturating_sub(1); - let index_to_activate = match activate_on_close { - ActivateOnClose::History => self - .activation_history - .pop() - .and_then(|last_activated_item| { - self.items.iter().enumerate().find_map(|(index, item)| { - (item.item_id() == last_activated_item.entity_id).then_some(index) - }) - }) - // We didn't have a valid activation history entry, so fallback - // to activating the item to the left - .unwrap_or_else(left_neighbour_index), - ActivateOnClose::Neighbour => { - self.activation_history.pop(); - if item_index + 1 < self.items.len() { - item_index + 1 - } else { - item_index.saturating_sub(1) - } - } - ActivateOnClose::LeftNeighbour => { - self.activation_history.pop(); - left_neighbour_index() - } - }; - - let should_activate = activate_pane || self.has_focus(window, cx); - if self.items.len() == 1 && should_activate { - self.focus_handle.focus(window); - } else { - self.activate_item( - index_to_activate, - should_activate, - should_activate, - window, - cx, - ); - } - } - - let item = self.items.remove(item_index); - - cx.emit(Event::RemovedItem { item: item.clone() }); - if self.items.is_empty() { - item.deactivated(window, cx); - if close_pane_if_empty { - self.update_toolbar(window, cx); - cx.emit(Event::Remove { - focus_on_pane: focus_on_pane_if_closed, - }); - } - } - - if item_index < self.active_item_index { - self.active_item_index -= 1; - } - - let mode = self.nav_history.mode(); - self.nav_history.set_mode(NavigationMode::ClosingItem); - item.deactivated(window, cx); - item.on_removed(cx); - self.nav_history.set_mode(mode); - - self.unpreview_item_if_preview(item.item_id()); - - if let Some(path) = item.project_path(cx) { - let abs_path = self - .nav_history - .0 - .lock() - .paths_by_item - .get(&item.item_id()) - .and_then(|(_, abs_path)| abs_path.clone()); - - self.nav_history - .0 - .lock() - .paths_by_item - .insert(item.item_id(), (path, abs_path)); - } else { - self.nav_history - .0 - .lock() - .paths_by_item - .remove(&item.item_id()); - } - - if self.zoom_out_on_close && self.items.is_empty() && close_pane_if_empty && self.zoomed { - cx.emit(Event::ZoomOut); - } - - cx.notify(); - } - - pub async fn save_item( - project: Entity, - pane: &WeakEntity, - item: &dyn ItemHandle, - save_intent: SaveIntent, - cx: &mut AsyncWindowContext, - ) -> Result { - const CONFLICT_MESSAGE: &str = "This file has changed on disk since you started editing it. Do you want to overwrite it?"; - - const DELETED_MESSAGE: &str = "This file has been deleted on disk since you started editing it. Do you want to recreate it?"; - - let path_style = project.read_with(cx, |project, cx| project.path_style(cx))?; - if save_intent == SaveIntent::Skip { - return Ok(true); - }; - let Some(item_ix) = pane - .read_with(cx, |pane, _| pane.index_for_item(item)) - .ok() - .flatten() - else { - return Ok(true); - }; - - let ( - mut has_conflict, - mut is_dirty, - mut can_save, - can_save_as, - is_singleton, - has_deleted_file, - ) = cx.update(|_window, cx| { - ( - item.has_conflict(cx), - item.is_dirty(cx), - item.can_save(cx), - item.can_save_as(cx), - item.buffer_kind(cx) == ItemBufferKind::Singleton, - item.has_deleted_file(cx), - ) - })?; - - // when saving a single buffer, we ignore whether or not it's dirty. - if save_intent == SaveIntent::Save || save_intent == SaveIntent::SaveWithoutFormat { - is_dirty = true; - } - - if save_intent == SaveIntent::SaveAs { - is_dirty = true; - has_conflict = false; - can_save = false; - } - - if save_intent == SaveIntent::Overwrite { - has_conflict = false; - } - - let should_format = save_intent != SaveIntent::SaveWithoutFormat; - - if has_conflict && can_save { - if has_deleted_file && is_singleton { - let answer = pane.update_in(cx, |pane, window, cx| { - pane.activate_item(item_ix, true, true, window, cx); - window.prompt( - PromptLevel::Warning, - DELETED_MESSAGE, - None, - &["Save", "Close", "Cancel"], - cx, - ) - })?; - match answer.await { - Ok(0) => { - pane.update_in(cx, |_, window, cx| { - item.save( - SaveOptions { - format: should_format, - autosave: false, - }, - project, - window, - cx, - ) - })? - .await? - } - Ok(1) => { - pane.update_in(cx, |pane, window, cx| { - pane.remove_item(item.item_id(), false, true, window, cx) - })?; - } - _ => return Ok(false), - } - return Ok(true); - } else { - let answer = pane.update_in(cx, |pane, window, cx| { - pane.activate_item(item_ix, true, true, window, cx); - window.prompt( - PromptLevel::Warning, - CONFLICT_MESSAGE, - None, - &["Overwrite", "Discard", "Cancel"], - cx, - ) - })?; - match answer.await { - Ok(0) => { - pane.update_in(cx, |_, window, cx| { - item.save( - SaveOptions { - format: should_format, - autosave: false, - }, - project, - window, - cx, - ) - })? - .await? - } - Ok(1) => { - pane.update_in(cx, |_, window, cx| item.reload(project, window, cx))? - .await? - } - _ => return Ok(false), - } - } - } else if is_dirty && (can_save || can_save_as) { - if save_intent == SaveIntent::Close { - let will_autosave = cx.update(|_window, cx| { - item.can_autosave(cx) - && item.workspace_settings(cx).autosave.should_save_on_close() - })?; - if !will_autosave { - let item_id = item.item_id(); - let answer_task = pane.update_in(cx, |pane, window, cx| { - if pane.save_modals_spawned.insert(item_id) { - pane.activate_item(item_ix, true, true, window, cx); - let prompt = dirty_message_for(item.project_path(cx), path_style); - Some(window.prompt( - PromptLevel::Warning, - &prompt, - None, - &["Save", "Don't Save", "Cancel"], - cx, - )) - } else { - None - } - })?; - if let Some(answer_task) = answer_task { - let answer = answer_task.await; - pane.update(cx, |pane, _| { - if !pane.save_modals_spawned.remove(&item_id) { - debug_panic!( - "save modal was not present in spawned modals after awaiting for its answer" - ) - } - })?; - match answer { - Ok(0) => {} - Ok(1) => { - // Don't save this file - pane.update_in(cx, |pane, _, cx| { - if pane.is_tab_pinned(item_ix) && !item.can_save(cx) { - pane.pinned_tab_count -= 1; - } - }) - .log_err(); - return Ok(true); - } - _ => return Ok(false), // Cancel - } - } else { - return Ok(false); - } - } - } - - if can_save { - pane.update_in(cx, |pane, window, cx| { - pane.unpreview_item_if_preview(item.item_id()); - item.save( - SaveOptions { - format: should_format, - autosave: false, - }, - project, - window, - cx, - ) - })? - .await?; - } else if can_save_as && is_singleton { - let suggested_name = - cx.update(|_window, cx| item.suggested_filename(cx).to_string())?; - let new_path = pane.update_in(cx, |pane, window, cx| { - pane.activate_item(item_ix, true, true, window, cx); - pane.workspace.update(cx, |workspace, cx| { - let lister = if workspace.project().read(cx).is_local() { - DirectoryLister::Local( - workspace.project().clone(), - workspace.app_state().fs.clone(), - ) - } else { - DirectoryLister::Project(workspace.project().clone()) - }; - workspace.prompt_for_new_path(lister, Some(suggested_name), window, cx) - }) - })??; - let Some(new_path) = new_path.await.ok().flatten().into_iter().flatten().next() - else { - return Ok(false); - }; - - let project_path = pane - .update(cx, |pane, cx| { - pane.project - .update(cx, |project, cx| { - project.find_or_create_worktree(new_path, true, cx) - }) - .ok() - }) - .ok() - .flatten(); - let save_task = if let Some(project_path) = project_path { - let (worktree, path) = project_path.await?; - let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?; - let new_path = ProjectPath { - worktree_id, - path: path, - }; - - pane.update_in(cx, |pane, window, cx| { - if let Some(item) = pane.item_for_path(new_path.clone(), cx) { - pane.remove_item(item.item_id(), false, false, window, cx); - } - - item.save_as(project, new_path, window, cx) - })? - } else { - return Ok(false); - }; - - save_task.await?; - return Ok(true); - } - } - - pane.update(cx, |_, cx| { - cx.emit(Event::UserSavedItem { - item: item.downgrade_item(), - save_intent, - }); - true - }) - } - - pub fn autosave_item( - item: &dyn ItemHandle, - project: Entity, - window: &mut Window, - cx: &mut App, - ) -> Task> { - let format = !matches!( - item.workspace_settings(cx).autosave, - AutosaveSetting::AfterDelay { .. } - ); - if item.can_autosave(cx) { - item.save( - SaveOptions { - format, - autosave: true, - }, - project, - window, - cx, - ) - } else { - Task::ready(Ok(())) - } - } - - pub fn focus_active_item(&mut self, window: &mut Window, cx: &mut Context) { - if let Some(active_item) = self.active_item() { - let focus_handle = active_item.item_focus_handle(cx); - window.focus(&focus_handle); - } - } - - pub fn split(&mut self, direction: SplitDirection, cx: &mut Context) { - cx.emit(Event::Split { - direction, - clone_active_item: true, - }); - } - - pub fn split_and_move(&mut self, direction: SplitDirection, cx: &mut Context) { - if self.items.len() > 1 { - cx.emit(Event::Split { - direction, - clone_active_item: false, - }); - } - } - - pub fn toolbar(&self) -> &Entity { - &self.toolbar - } - - pub fn handle_deleted_project_item( - &mut self, - entry_id: ProjectEntryId, - window: &mut Window, - cx: &mut Context, - ) -> Option<()> { - let item_id = self.items().find_map(|item| { - if item.buffer_kind(cx) == ItemBufferKind::Singleton - && item.project_entry_ids(cx).as_slice() == [entry_id] - { - Some(item.item_id()) - } else { - None - } - })?; - - self.remove_item(item_id, false, true, window, cx); - self.nav_history.remove_item(item_id); - - Some(()) - } - - fn update_toolbar(&mut self, window: &mut Window, cx: &mut Context) { - let active_item = self - .items - .get(self.active_item_index) - .map(|item| item.as_ref()); - self.toolbar.update(cx, |toolbar, cx| { - toolbar.set_active_item(active_item, window, cx); - }); - } - - fn update_status_bar(&mut self, window: &mut Window, cx: &mut Context) { - let workspace = self.workspace.clone(); - let pane = cx.entity(); - - window.defer(cx, move |window, cx| { - let Ok(status_bar) = - workspace.read_with(cx, |workspace, _| workspace.status_bar.clone()) - else { - return; - }; - - status_bar.update(cx, move |status_bar, cx| { - status_bar.set_active_pane(&pane, window, cx); - }); - }); - } - - fn entry_abs_path(&self, entry: ProjectEntryId, cx: &App) -> Option { - let worktree = self - .workspace - .upgrade()? - .read(cx) - .project() - .read(cx) - .worktree_for_entry(entry, cx)? - .read(cx); - let entry = worktree.entry_for_id(entry)?; - Some(match &entry.canonical_path { - Some(canonical_path) => canonical_path.to_path_buf(), - None => worktree.absolutize(&entry.path), - }) - } - - pub fn icon_color(selected: bool) -> Color { - if selected { - Color::Default - } else { - Color::Muted - } - } - - fn toggle_pin_tab(&mut self, _: &TogglePinTab, window: &mut Window, cx: &mut Context) { - if self.items.is_empty() { - return; - } - let active_tab_ix = self.active_item_index(); - if self.is_tab_pinned(active_tab_ix) { - self.unpin_tab_at(active_tab_ix, window, cx); - } else { - self.pin_tab_at(active_tab_ix, window, cx); - } - } - - fn unpin_all_tabs(&mut self, _: &UnpinAllTabs, window: &mut Window, cx: &mut Context) { - if self.items.is_empty() { - return; - } - - let pinned_item_ids = self.pinned_item_ids().into_iter().rev(); - - for pinned_item_id in pinned_item_ids { - if let Some(ix) = self.index_for_item_id(pinned_item_id) { - self.unpin_tab_at(ix, window, cx); - } - } - } - - fn pin_tab_at(&mut self, ix: usize, window: &mut Window, cx: &mut Context) { - self.change_tab_pin_state(ix, PinOperation::Pin, window, cx); - } - - fn unpin_tab_at(&mut self, ix: usize, window: &mut Window, cx: &mut Context) { - self.change_tab_pin_state(ix, PinOperation::Unpin, window, cx); - } - - fn change_tab_pin_state( - &mut self, - ix: usize, - operation: PinOperation, - window: &mut Window, - cx: &mut Context, - ) { - maybe!({ - let pane = cx.entity(); - - let destination_index = match operation { - PinOperation::Pin => self.pinned_tab_count.min(ix), - PinOperation::Unpin => self.pinned_tab_count.checked_sub(1)?, - }; - - let id = self.item_for_index(ix)?.item_id(); - let should_activate = ix == self.active_item_index; - - if matches!(operation, PinOperation::Pin) { - self.unpreview_item_if_preview(id); - } - - match operation { - PinOperation::Pin => self.pinned_tab_count += 1, - PinOperation::Unpin => self.pinned_tab_count -= 1, - } - - if ix == destination_index { - cx.notify(); - } else { - self.workspace - .update(cx, |_, cx| { - cx.defer_in(window, move |_, window, cx| { - move_item( - &pane, - &pane, - id, - destination_index, - should_activate, - window, - cx, - ); - }); - }) - .ok()?; - } - - let event = match operation { - PinOperation::Pin => Event::ItemPinned, - PinOperation::Unpin => Event::ItemUnpinned, - }; - - cx.emit(event); - - Some(()) - }); - } - - fn is_tab_pinned(&self, ix: usize) -> bool { - self.pinned_tab_count > ix - } - - fn has_unpinned_tabs(&self) -> bool { - self.pinned_tab_count < self.items.len() - } - - fn activate_unpinned_tab(&mut self, window: &mut Window, cx: &mut Context) { - if self.items.is_empty() { - return; - } - let Some(index) = self - .items() - .enumerate() - .find_map(|(index, _item)| (!self.is_tab_pinned(index)).then_some(index)) - else { - return; - }; - self.activate_item(index, true, true, window, cx); - } - - fn render_tab( - &self, - ix: usize, - item: &dyn ItemHandle, - detail: usize, - focus_handle: &FocusHandle, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement + use<> { - let is_active = ix == self.active_item_index; - let is_preview = self - .preview_item_id - .map(|id| id == item.item_id()) - .unwrap_or(false); - - let label = item.tab_content( - TabContentParams { - detail: Some(detail), - selected: is_active, - preview: is_preview, - deemphasized: !self.has_focus(window, cx), - }, - window, - cx, - ); - - let item_diagnostic = item - .project_path(cx) - .map_or(None, |project_path| self.diagnostics.get(&project_path)); - - let decorated_icon = item_diagnostic.map_or(None, |diagnostic| { - let icon = match item.tab_icon(window, cx) { - Some(icon) => icon, - None => return None, - }; - - let knockout_item_color = if is_active { - cx.theme().colors().tab_active_background - } else { - cx.theme().colors().tab_bar_background - }; - - let (icon_decoration, icon_color) = if matches!(diagnostic, &DiagnosticSeverity::ERROR) - { - (IconDecorationKind::X, Color::Error) - } else { - (IconDecorationKind::Triangle, Color::Warning) - }; - - Some(DecoratedIcon::new( - icon.size(IconSize::Small).color(Color::Muted), - Some( - IconDecoration::new(icon_decoration, knockout_item_color, cx) - .color(icon_color.color(cx)) - .position(Point { - x: px(-2.), - y: px(-2.), - }), - ), - )) - }); - - let icon = if decorated_icon.is_none() { - match item_diagnostic { - Some(&DiagnosticSeverity::ERROR) => None, - Some(&DiagnosticSeverity::WARNING) => None, - _ => item - .tab_icon(window, cx) - .map(|icon| icon.color(Color::Muted)), - } - .map(|icon| icon.size(IconSize::Small)) - } else { - None - }; - - let settings = ItemSettings::get_global(cx); - let close_side = &settings.close_position; - let show_close_button = &settings.show_close_button; - let indicator = render_item_indicator(item.boxed_clone(), cx); - let tab_tooltip_content = item.tab_tooltip_content(cx); - let item_id = item.item_id(); - let is_first_item = ix == 0; - let is_last_item = ix == self.items.len() - 1; - let is_pinned = self.is_tab_pinned(ix); - let position_relative_to_active_item = ix.cmp(&self.active_item_index); - - let tab = Tab::new(ix) - .position(if is_first_item { - TabPosition::First - } else if is_last_item { - TabPosition::Last - } else { - TabPosition::Middle(position_relative_to_active_item) - }) - .close_side(match close_side { - ClosePosition::Left => ui::TabCloseSide::Start, - ClosePosition::Right => ui::TabCloseSide::End, - }) - .toggle_state(is_active) - .on_click(cx.listener(move |pane: &mut Self, _, window, cx| { - pane.activate_item(ix, true, true, window, cx) - })) - // TODO: This should be a click listener with the middle mouse button instead of a mouse down listener. - .on_mouse_down( - MouseButton::Middle, - cx.listener(move |pane, _event, window, cx| { - pane.close_item_by_id(item_id, SaveIntent::Close, window, cx) - .detach_and_log_err(cx); - }), - ) - .on_mouse_down( - MouseButton::Left, - cx.listener(move |pane, event: &MouseDownEvent, _, _| { - if event.click_count > 1 { - pane.unpreview_item_if_preview(item_id); - } - }), - ) - .on_drag( - DraggedTab { - item: item.boxed_clone(), - pane: cx.entity(), - detail, - is_active, - ix, - }, - |tab, _, _, cx| cx.new(|_| tab.clone()), - ) - .drag_over::(move |tab, dragged_tab: &DraggedTab, _, cx| { - let mut styled_tab = tab - .bg(cx.theme().colors().drop_target_background) - .border_color(cx.theme().colors().drop_target_border) - .border_0(); - - if ix < dragged_tab.ix { - styled_tab = styled_tab.border_l_2(); - } else if ix > dragged_tab.ix { - styled_tab = styled_tab.border_r_2(); - } - - styled_tab - }) - .drag_over::(|tab, _, _, cx| { - tab.bg(cx.theme().colors().drop_target_background) - }) - .when_some(self.can_drop_predicate.clone(), |this, p| { - this.can_drop(move |a, window, cx| p(a, window, cx)) - }) - .on_drop( - cx.listener(move |this, dragged_tab: &DraggedTab, window, cx| { - this.drag_split_direction = None; - this.handle_tab_drop(dragged_tab, ix, window, cx) - }), - ) - .on_drop( - cx.listener(move |this, selection: &DraggedSelection, window, cx| { - this.drag_split_direction = None; - this.handle_dragged_selection_drop(selection, Some(ix), window, cx) - }), - ) - .on_drop(cx.listener(move |this, paths, window, cx| { - this.drag_split_direction = None; - this.handle_external_paths_drop(paths, window, cx) - })) - .start_slot::(indicator) - .map(|this| { - let end_slot_action: &'static dyn Action; - let end_slot_tooltip_text: &'static str; - let end_slot = if is_pinned { - end_slot_action = &TogglePinTab; - end_slot_tooltip_text = "Unpin Tab"; - IconButton::new("unpin tab", IconName::Pin) - .shape(IconButtonShape::Square) - .icon_color(Color::Muted) - .size(ButtonSize::None) - .icon_size(IconSize::Small) - .on_click(cx.listener(move |pane, _, window, cx| { - pane.unpin_tab_at(ix, window, cx); - })) - } else { - end_slot_action = &CloseActiveItem { - save_intent: None, - close_pinned: false, - }; - end_slot_tooltip_text = "Close Tab"; - match show_close_button { - ShowCloseButton::Always => IconButton::new("close tab", IconName::Close), - ShowCloseButton::Hover => { - IconButton::new("close tab", IconName::Close).visible_on_hover("") - } - ShowCloseButton::Hidden => return this, - } - .shape(IconButtonShape::Square) - .icon_color(Color::Muted) - .size(ButtonSize::None) - .icon_size(IconSize::Small) - .on_click(cx.listener(move |pane, _, window, cx| { - pane.close_item_by_id(item_id, SaveIntent::Close, window, cx) - .detach_and_log_err(cx); - })) - } - .map(|this| { - if is_active { - let focus_handle = focus_handle.clone(); - this.tooltip(move |window, cx| { - Tooltip::for_action_in( - end_slot_tooltip_text, - end_slot_action, - &window.focused(cx).unwrap_or_else(|| focus_handle.clone()), - cx, - ) - }) - } else { - this.tooltip(Tooltip::text(end_slot_tooltip_text)) - } - }); - this.end_slot(end_slot) - }) - .child( - h_flex() - .gap_1() - .items_center() - .children( - std::iter::once(if let Some(decorated_icon) = decorated_icon { - Some(div().child(decorated_icon.into_any_element())) - } else { - icon.map(|icon| div().child(icon.into_any_element())) - }) - .flatten(), - ) - .child(label) - .id(("pane-tab-content", ix)) - .map(|this| match tab_tooltip_content { - Some(TabTooltipContent::Text(text)) => this.tooltip(Tooltip::text(text)), - Some(TabTooltipContent::Custom(element_fn)) => { - this.tooltip(move |window, cx| element_fn(window, cx)) - } - None => this, - }), - ); - - let single_entry_to_resolve = (self.items[ix].buffer_kind(cx) == ItemBufferKind::Singleton) - .then(|| self.items[ix].project_entry_ids(cx).get(0).copied()) - .flatten(); - - let total_items = self.items.len(); - let has_multibuffer_items = self - .items - .iter() - .any(|item| item.buffer_kind(cx) == ItemBufferKind::Multibuffer); - let has_items_to_left = ix > 0; - let has_items_to_right = ix < total_items - 1; - let has_clean_items = self.items.iter().any(|item| !item.is_dirty(cx)); - let is_pinned = self.is_tab_pinned(ix); - let pane = cx.entity().downgrade(); - let menu_context = item.item_focus_handle(cx); - right_click_menu(ix) - .trigger(|_, _, _| tab) - .menu(move |window, cx| { - let pane = pane.clone(); - let menu_context = menu_context.clone(); - ContextMenu::build(window, cx, move |mut menu, window, cx| { - let close_active_item_action = CloseActiveItem { - save_intent: None, - close_pinned: true, - }; - let close_inactive_items_action = CloseOtherItems { - save_intent: None, - close_pinned: false, - }; - let close_multibuffers_action = CloseMultibufferItems { - save_intent: None, - close_pinned: false, - }; - let close_items_to_the_left_action = CloseItemsToTheLeft { - close_pinned: false, - }; - let close_items_to_the_right_action = CloseItemsToTheRight { - close_pinned: false, - }; - let close_clean_items_action = CloseCleanItems { - close_pinned: false, - }; - let close_all_items_action = CloseAllItems { - save_intent: None, - close_pinned: false, - }; - if let Some(pane) = pane.upgrade() { - menu = menu - .entry( - "Close", - Some(Box::new(close_active_item_action)), - window.handler_for(&pane, move |pane, window, cx| { - pane.close_item_by_id(item_id, SaveIntent::Close, window, cx) - .detach_and_log_err(cx); - }), - ) - .item(ContextMenuItem::Entry( - ContextMenuEntry::new("Close Others") - .action(Box::new(close_inactive_items_action.clone())) - .disabled(total_items == 1) - .handler(window.handler_for(&pane, move |pane, window, cx| { - pane.close_other_items( - &close_inactive_items_action, - Some(item_id), - window, - cx, - ) - .detach_and_log_err(cx); - })), - )) - // We make this optional, instead of using disabled as to not overwhelm the context menu unnecessarily - .extend(has_multibuffer_items.then(|| { - ContextMenuItem::Entry( - ContextMenuEntry::new("Close Multibuffers") - .action(Box::new(close_multibuffers_action.clone())) - .handler(window.handler_for( - &pane, - move |pane, window, cx| { - pane.close_multibuffer_items( - &close_multibuffers_action, - window, - cx, - ) - .detach_and_log_err(cx); - }, - )), - ) - })) - .separator() - .item(ContextMenuItem::Entry( - ContextMenuEntry::new("Close Left") - .action(Box::new(close_items_to_the_left_action.clone())) - .disabled(!has_items_to_left) - .handler(window.handler_for(&pane, move |pane, window, cx| { - pane.close_items_to_the_left_by_id( - Some(item_id), - &close_items_to_the_left_action, - window, - cx, - ) - .detach_and_log_err(cx); - })), - )) - .item(ContextMenuItem::Entry( - ContextMenuEntry::new("Close Right") - .action(Box::new(close_items_to_the_right_action.clone())) - .disabled(!has_items_to_right) - .handler(window.handler_for(&pane, move |pane, window, cx| { - pane.close_items_to_the_right_by_id( - Some(item_id), - &close_items_to_the_right_action, - window, - cx, - ) - .detach_and_log_err(cx); - })), - )) - .separator() - .item(ContextMenuItem::Entry( - ContextMenuEntry::new("Close Clean") - .action(Box::new(close_clean_items_action.clone())) - .disabled(!has_clean_items) - .handler(window.handler_for(&pane, move |pane, window, cx| { - pane.close_clean_items( - &close_clean_items_action, - window, - cx, - ) - .detach_and_log_err(cx) - })), - )) - .entry( - "Close All", - Some(Box::new(close_all_items_action.clone())), - window.handler_for(&pane, move |pane, window, cx| { - pane.close_all_items(&close_all_items_action, window, cx) - .detach_and_log_err(cx) - }), - ); - - let pin_tab_entries = |menu: ContextMenu| { - menu.separator().map(|this| { - if is_pinned { - this.entry( - "Unpin Tab", - Some(TogglePinTab.boxed_clone()), - window.handler_for(&pane, move |pane, window, cx| { - pane.unpin_tab_at(ix, window, cx); - }), - ) - } else { - this.entry( - "Pin Tab", - Some(TogglePinTab.boxed_clone()), - window.handler_for(&pane, move |pane, window, cx| { - pane.pin_tab_at(ix, window, cx); - }), - ) - } - }) - }; - if let Some(entry) = single_entry_to_resolve { - let project_path = pane - .read(cx) - .item_for_entry(entry, cx) - .and_then(|item| item.project_path(cx)); - let worktree = project_path.as_ref().and_then(|project_path| { - pane.read(cx) - .project - .upgrade()? - .read(cx) - .worktree_for_id(project_path.worktree_id, cx) - }); - let has_relative_path = worktree.as_ref().is_some_and(|worktree| { - worktree - .read(cx) - .root_entry() - .is_some_and(|entry| entry.is_dir()) - }); - - let entry_abs_path = pane.read(cx).entry_abs_path(entry, cx); - let parent_abs_path = entry_abs_path - .as_deref() - .and_then(|abs_path| Some(abs_path.parent()?.to_path_buf())); - let relative_path = project_path - .map(|project_path| project_path.path) - .filter(|_| has_relative_path); - - let visible_in_project_panel = relative_path.is_some() - && worktree.is_some_and(|worktree| worktree.read(cx).is_visible()); - - let entry_id = entry.to_proto(); - menu = menu - .separator() - .when_some(entry_abs_path, |menu, abs_path| { - menu.entry( - "Copy Path", - Some(Box::new(zed_actions::workspace::CopyPath)), - window.handler_for(&pane, move |_, _, cx| { - cx.write_to_clipboard(ClipboardItem::new_string( - abs_path.to_string_lossy().into_owned(), - )); - }), - ) - }) - .when_some(relative_path, |menu, relative_path| { - menu.entry( - "Copy Relative Path", - Some(Box::new(zed_actions::workspace::CopyRelativePath)), - window.handler_for(&pane, move |this, _, cx| { - let Some(project) = this.project.upgrade() else { - return; - }; - let path_style = project - .update(cx, |project, cx| project.path_style(cx)); - cx.write_to_clipboard(ClipboardItem::new_string( - relative_path.display(path_style).to_string(), - )); - }), - ) - }) - .map(pin_tab_entries) - .separator() - .when(visible_in_project_panel, |menu| { - menu.entry( - "Reveal In Project Panel", - Some(Box::new(RevealInProjectPanel::default())), - window.handler_for(&pane, move |pane, _, cx| { - pane.project - .update(cx, |_, cx| { - cx.emit(project::Event::RevealInProjectPanel( - ProjectEntryId::from_proto(entry_id), - )) - }) - .ok(); - }), - ) - }) - .when_some(parent_abs_path, |menu, parent_abs_path| { - menu.entry( - "Open in Terminal", - Some(Box::new(OpenInTerminal)), - window.handler_for(&pane, move |_, window, cx| { - window.dispatch_action( - OpenTerminal { - working_directory: parent_abs_path.clone(), - } - .boxed_clone(), - cx, - ); - }), - ) - }); - } else { - menu = menu.map(pin_tab_entries); - } - } - - menu.context(menu_context) - }) - }) - } - - fn render_tab_bar(&mut self, window: &mut Window, cx: &mut Context) -> AnyElement { - let focus_handle = self.focus_handle.clone(); - let navigate_backward = IconButton::new("navigate_backward", IconName::ArrowLeft) - .icon_size(IconSize::Small) - .on_click({ - let entity = cx.entity(); - move |_, window, cx| { - entity.update(cx, |pane, cx| { - pane.navigate_backward(&Default::default(), window, cx) - }) - } - }) - .disabled(!self.can_navigate_backward()) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Go Back", - &GoBack, - &window.focused(cx).unwrap_or_else(|| focus_handle.clone()), - cx, - ) - } - }); - - let navigate_forward = IconButton::new("navigate_forward", IconName::ArrowRight) - .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) - }) - } - }) - .disabled(!self.can_navigate_forward()) - .tooltip({ - let focus_handle = focus_handle.clone(); - move |window, cx| { - Tooltip::for_action_in( - "Go Forward", - &GoForward, - &window.focused(cx).unwrap_or_else(|| focus_handle.clone()), - cx, - ) - } - }); - - let mut tab_items = self - .items - .iter() - .enumerate() - .zip(tab_details(&self.items, window, cx)) - .map(|((ix, item), detail)| { - self.render_tab(ix, &**item, detail, &focus_handle, window, cx) - }) - .collect::>(); - let tab_count = tab_items.len(); - if self.is_tab_pinned(tab_count) { - log::warn!( - "Pinned tab count ({}) exceeds actual tab count ({}). \ - This should not happen. If possible, add reproduction steps, \ - in a comment, to https://github.com/zed-industries/zed/issues/33342", - self.pinned_tab_count, - tab_count - ); - self.pinned_tab_count = tab_count; - } - let unpinned_tabs = tab_items.split_off(self.pinned_tab_count); - let pinned_tabs = tab_items; - - TabBar::new("tab_bar") - .when( - self.display_nav_history_buttons.unwrap_or_default(), - |tab_bar| { - tab_bar - .start_child(navigate_backward) - .start_child(navigate_forward) - }, - ) - .map(|tab_bar| { - if self.show_tab_bar_buttons { - let render_tab_buttons = self.render_tab_bar_buttons.clone(); - let (left_children, right_children) = render_tab_buttons(self, window, cx); - tab_bar - .start_children(left_children) - .end_children(right_children) - } else { - tab_bar - } - }) - .children(pinned_tabs.len().ne(&0).then(|| { - let max_scroll = self.tab_bar_scroll_handle.max_offset().width; - // We need to check both because offset returns delta values even when the scroll handle is not scrollable - let is_scrolled = self.tab_bar_scroll_handle.offset().x < px(0.); - // Avoid flickering when max_offset is very small (< 2px). - // The border adds 1-2px which can push max_offset back to 0, creating a loop. - let is_scrollable = max_scroll > px(2.0); - let has_active_unpinned_tab = self.active_item_index >= self.pinned_tab_count; - h_flex() - .children(pinned_tabs) - .when(is_scrollable && is_scrolled, |this| { - this.when(has_active_unpinned_tab, |this| this.border_r_2()) - .when(!has_active_unpinned_tab, |this| this.border_r_1()) - .border_color(cx.theme().colors().border) - }) - })) - .child( - h_flex() - .id("unpinned tabs") - .overflow_x_scroll() - .w_full() - .track_scroll(&self.tab_bar_scroll_handle) - .on_scroll_wheel(cx.listener(|this, _, _, _| { - this.suppress_scroll = true; - })) - .children(unpinned_tabs) - .child( - div() - .id("tab_bar_drop_target") - .min_w_6() - // HACK: This empty child is currently necessary to force the drop target to appear - // despite us setting a min width above. - .child("") - // HACK: h_full doesn't occupy the complete height, using fixed height instead - .h(Tab::container_height(cx)) - .flex_grow() - .drag_over::(|bar, _, _, cx| { - bar.bg(cx.theme().colors().drop_target_background) - }) - .drag_over::(|bar, _, _, cx| { - bar.bg(cx.theme().colors().drop_target_background) - }) - .on_drop(cx.listener( - move |this, dragged_tab: &DraggedTab, window, cx| { - this.drag_split_direction = None; - this.handle_tab_drop(dragged_tab, this.items.len(), window, cx) - }, - )) - .on_drop(cx.listener( - move |this, selection: &DraggedSelection, window, cx| { - this.drag_split_direction = None; - this.handle_project_entry_drop( - &selection.active_selection.entry_id, - Some(tab_count), - window, - cx, - ) - }, - )) - .on_drop(cx.listener(move |this, paths, window, cx| { - this.drag_split_direction = None; - this.handle_external_paths_drop(paths, window, cx) - })) - .on_click(cx.listener(move |this, event: &ClickEvent, window, cx| { - if event.click_count() == 2 { - window.dispatch_action( - this.double_click_dispatch_action.boxed_clone(), - cx, - ); - } - })), - ), - ) - .into_any_element() - } - - pub fn render_menu_overlay(menu: &Entity) -> Div { - div().absolute().bottom_0().right_0().size_0().child( - deferred(anchored().anchor(Corner::TopRight).child(menu.clone())).with_priority(1), - ) - } - - pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut Context) { - self.zoomed = zoomed; - cx.notify(); - } - - pub fn is_zoomed(&self) -> bool { - self.zoomed - } - - fn handle_drag_move( - &mut self, - event: &DragMoveEvent, - window: &mut Window, - cx: &mut Context, - ) { - let can_split_predicate = self.can_split_predicate.take(); - let can_split = match &can_split_predicate { - Some(can_split_predicate) => { - can_split_predicate(self, event.dragged_item(), window, cx) - } - None => false, - }; - self.can_split_predicate = can_split_predicate; - if !can_split { - return; - } - - let rect = event.bounds.size; - - let size = event.bounds.size.width.min(event.bounds.size.height) - * WorkspaceSettings::get_global(cx).drop_target_size; - - let relative_cursor = Point::new( - event.event.position.x - event.bounds.left(), - event.event.position.y - event.bounds.top(), - ); - - let direction = if relative_cursor.x < size - || relative_cursor.x > rect.width - size - || relative_cursor.y < size - || relative_cursor.y > rect.height - size - { - [ - SplitDirection::Up, - SplitDirection::Right, - SplitDirection::Down, - SplitDirection::Left, - ] - .iter() - .min_by_key(|side| match side { - SplitDirection::Up => relative_cursor.y, - SplitDirection::Right => rect.width - relative_cursor.x, - SplitDirection::Down => rect.height - relative_cursor.y, - SplitDirection::Left => relative_cursor.x, - }) - .cloned() - } else { - None - }; - - if direction != self.drag_split_direction { - self.drag_split_direction = direction; - } - } - - pub fn handle_tab_drop( - &mut self, - dragged_tab: &DraggedTab, - ix: usize, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(custom_drop_handle) = self.custom_drop_handle.clone() - && let ControlFlow::Break(()) = custom_drop_handle(self, dragged_tab, window, cx) - { - return; - } - let mut to_pane = cx.entity(); - let split_direction = self.drag_split_direction; - let item_id = dragged_tab.item.item_id(); - self.unpreview_item_if_preview(item_id); - - let is_clone = cfg!(target_os = "macos") && window.modifiers().alt - || cfg!(not(target_os = "macos")) && window.modifiers().control; - - let from_pane = dragged_tab.pane.clone(); - - self.workspace - .update(cx, |_, cx| { - cx.defer_in(window, move |workspace, window, cx| { - if let Some(split_direction) = split_direction { - to_pane = workspace.split_pane(to_pane, split_direction, window, cx); - } - let database_id = workspace.database_id(); - let was_pinned_in_from_pane = from_pane.read_with(cx, |pane, _| { - pane.index_for_item_id(item_id) - .is_some_and(|ix| pane.is_tab_pinned(ix)) - }); - let to_pane_old_length = to_pane.read(cx).items.len(); - if is_clone { - let Some(item) = from_pane - .read(cx) - .items() - .find(|item| item.item_id() == item_id) - .cloned() - else { - return; - }; - if item.can_split(cx) { - let task = item.clone_on_split(database_id, window, cx); - let to_pane = to_pane.downgrade(); - cx.spawn_in(window, async move |_, cx| { - if let Some(item) = task.await { - to_pane - .update_in(cx, |pane, window, cx| { - pane.add_item(item, true, true, None, window, cx) - }) - .ok(); - } - }) - .detach(); - } else { - move_item(&from_pane, &to_pane, item_id, ix, true, window, cx); - } - } else { - move_item(&from_pane, &to_pane, item_id, ix, true, window, cx); - } - to_pane.update(cx, |this, _| { - if to_pane == from_pane { - let actual_ix = this - .items - .iter() - .position(|item| item.item_id() == item_id) - .unwrap_or(0); - - let is_pinned_in_to_pane = this.is_tab_pinned(actual_ix); - - if !was_pinned_in_from_pane && is_pinned_in_to_pane { - this.pinned_tab_count += 1; - } else if was_pinned_in_from_pane && !is_pinned_in_to_pane { - this.pinned_tab_count -= 1; - } - } else if this.items.len() >= to_pane_old_length { - let is_pinned_in_to_pane = this.is_tab_pinned(ix); - let item_created_pane = to_pane_old_length == 0; - let is_first_position = ix == 0; - let was_dropped_at_beginning = item_created_pane || is_first_position; - let should_remain_pinned = is_pinned_in_to_pane - || (was_pinned_in_from_pane && was_dropped_at_beginning); - - if should_remain_pinned { - this.pinned_tab_count += 1; - } - } - }); - }); - }) - .log_err(); - } - - fn handle_dragged_selection_drop( - &mut self, - dragged_selection: &DraggedSelection, - dragged_onto: Option, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(custom_drop_handle) = self.custom_drop_handle.clone() - && let ControlFlow::Break(()) = custom_drop_handle(self, dragged_selection, window, cx) - { - return; - } - self.handle_project_entry_drop( - &dragged_selection.active_selection.entry_id, - dragged_onto, - window, - cx, - ); - } - - fn handle_project_entry_drop( - &mut self, - project_entry_id: &ProjectEntryId, - target: Option, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(custom_drop_handle) = self.custom_drop_handle.clone() - && let ControlFlow::Break(()) = custom_drop_handle(self, project_entry_id, window, cx) - { - return; - } - let mut to_pane = cx.entity(); - let split_direction = self.drag_split_direction; - let project_entry_id = *project_entry_id; - self.workspace - .update(cx, |_, cx| { - cx.defer_in(window, move |workspace, window, cx| { - if let Some(project_path) = workspace - .project() - .read(cx) - .path_for_entry(project_entry_id, cx) - { - let load_path_task = workspace.load_path(project_path.clone(), window, cx); - cx.spawn_in(window, async move |workspace, cx| { - if let Some((project_entry_id, build_item)) = - load_path_task.await.notify_async_err(cx) - { - let (to_pane, new_item_handle) = workspace - .update_in(cx, |workspace, window, cx| { - if let Some(split_direction) = split_direction { - to_pane = workspace.split_pane( - to_pane, - split_direction, - window, - cx, - ); - } - let new_item_handle = to_pane.update(cx, |pane, cx| { - pane.open_item( - project_entry_id, - project_path, - true, - false, - true, - target, - window, - cx, - build_item, - ) - }); - (to_pane, new_item_handle) - }) - .log_err()?; - to_pane - .update_in(cx, |this, window, cx| { - let Some(index) = this.index_for_item(&*new_item_handle) - else { - return; - }; - - if target.is_some_and(|target| this.is_tab_pinned(target)) { - this.pin_tab_at(index, window, cx); - } - }) - .ok()? - } - Some(()) - }) - .detach(); - }; - }); - }) - .log_err(); - } - - fn handle_external_paths_drop( - &mut self, - paths: &ExternalPaths, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(custom_drop_handle) = self.custom_drop_handle.clone() - && let ControlFlow::Break(()) = custom_drop_handle(self, paths, window, cx) - { - return; - } - let mut to_pane = cx.entity(); - let mut split_direction = self.drag_split_direction; - let paths = paths.paths().to_vec(); - let is_remote = self - .workspace - .update(cx, |workspace, cx| { - if workspace.project().read(cx).is_via_collab() { - workspace.show_error( - &anyhow::anyhow!("Cannot drop files on a remote project"), - cx, - ); - true - } else { - false - } - }) - .unwrap_or(true); - if is_remote { - return; - } - - self.workspace - .update(cx, |workspace, cx| { - let fs = Arc::clone(workspace.project().read(cx).fs()); - cx.spawn_in(window, async move |workspace, cx| { - let mut is_file_checks = FuturesUnordered::new(); - for path in &paths { - is_file_checks.push(fs.is_file(path)) - } - let mut has_files_to_open = false; - while let Some(is_file) = is_file_checks.next().await { - if is_file { - has_files_to_open = true; - break; - } - } - drop(is_file_checks); - if !has_files_to_open { - split_direction = None; - } - - if let Ok((open_task, to_pane)) = - workspace.update_in(cx, |workspace, window, cx| { - if let Some(split_direction) = split_direction { - to_pane = - workspace.split_pane(to_pane, split_direction, window, cx); - } - ( - workspace.open_paths( - paths, - OpenOptions { - visible: Some(OpenVisible::OnlyDirectories), - ..Default::default() - }, - Some(to_pane.downgrade()), - window, - cx, - ), - to_pane, - ) - }) - { - let opened_items: Vec<_> = open_task.await; - _ = workspace.update_in(cx, |workspace, window, cx| { - for item in opened_items.into_iter().flatten() { - if let Err(e) = item { - workspace.show_error(&e, cx); - } - } - if to_pane.read(cx).items_len() == 0 { - workspace.remove_pane(to_pane, None, window, cx); - } - }); - } - }) - .detach(); - }) - .log_err(); - } - - pub fn display_nav_history_buttons(&mut self, display: Option) { - self.display_nav_history_buttons = display; - } - - fn pinned_item_ids(&self) -> Vec { - self.items - .iter() - .enumerate() - .filter_map(|(index, item)| { - if self.is_tab_pinned(index) { - return Some(item.item_id()); - } - - None - }) - .collect() - } - - fn clean_item_ids(&self, cx: &mut Context) -> Vec { - self.items() - .filter_map(|item| { - if !item.is_dirty(cx) { - return Some(item.item_id()); - } - - None - }) - .collect() - } - - fn to_the_side_item_ids(&self, item_id: EntityId, side: Side) -> Vec { - match side { - Side::Left => self - .items() - .take_while(|item| item.item_id() != item_id) - .map(|item| item.item_id()) - .collect(), - Side::Right => self - .items() - .rev() - .take_while(|item| item.item_id() != item_id) - .map(|item| item.item_id()) - .collect(), - } - } - - fn multibuffer_item_ids(&self, cx: &mut Context) -> Vec { - self.items() - .filter(|item| item.buffer_kind(cx) == ItemBufferKind::Multibuffer) - .map(|item| item.item_id()) - .collect() - } - - pub fn drag_split_direction(&self) -> Option { - self.drag_split_direction - } - - pub fn set_zoom_out_on_close(&mut self, zoom_out_on_close: bool) { - self.zoom_out_on_close = zoom_out_on_close; - } -} - -fn default_render_tab_bar_buttons( - pane: &mut Pane, - window: &mut Window, - cx: &mut Context, -) -> (Option, Option) { - if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) { - return (None, None); - } - let (can_clone, can_split_move) = match pane.active_item() { - Some(active_item) if active_item.can_split(cx) => (true, false), - Some(_) => (false, pane.items_len() > 1), - None => (false, false), - }; - // Ideally we would return a vec of elements here to pass directly to the [TabBar]'s - // `end_slot`, but due to needing a view here that isn't possible. - let right_children = h_flex() - // Instead we need to replicate the spacing from the [TabBar]'s `end_slot` here. - .gap(DynamicSpacing::Base04.rems(cx)) - .child( - PopoverMenu::new("pane-tab-bar-popover-menu") - .trigger_with_tooltip( - IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small), - Tooltip::text("New..."), - ) - .anchor(Corner::TopRight) - .with_handle(pane.new_item_context_menu_handle.clone()) - .menu(move |window, cx| { - Some(ContextMenu::build(window, cx, |menu, _, _| { - menu.action("New File", NewFile.boxed_clone()) - .action("Open File", ToggleFileFinder::default().boxed_clone()) - .separator() - .action( - "Search Project", - DeploySearch { - replace_enabled: false, - included_files: None, - excluded_files: None, - } - .boxed_clone(), - ) - .action("Search Symbols", ToggleProjectSymbols.boxed_clone()) - .separator() - .action("New Terminal", NewTerminal.boxed_clone()) - })) - }), - ) - .child( - PopoverMenu::new("pane-tab-bar-split") - .trigger_with_tooltip( - IconButton::new("split", IconName::Split) - .icon_size(IconSize::Small) - .disabled(!can_clone && !can_split_move), - Tooltip::text("Split Pane"), - ) - .anchor(Corner::TopRight) - .with_handle(pane.split_item_context_menu_handle.clone()) - .menu(move |window, cx| { - ContextMenu::build(window, cx, |menu, _, _| { - if can_split_move { - menu.action("Split Right", SplitAndMoveRight.boxed_clone()) - .action("Split Left", SplitAndMoveLeft.boxed_clone()) - .action("Split Up", SplitAndMoveUp.boxed_clone()) - .action("Split Down", SplitAndMoveDown.boxed_clone()) - } else { - menu.action("Split Right", SplitRight.boxed_clone()) - .action("Split Left", SplitLeft.boxed_clone()) - .action("Split Up", SplitUp.boxed_clone()) - .action("Split Down", SplitDown.boxed_clone()) - } - }) - .into() - }), - ) - .child({ - let zoomed = pane.is_zoomed(); - IconButton::new("toggle_zoom", IconName::Maximize) - .icon_size(IconSize::Small) - .toggle_state(zoomed) - .selected_icon(IconName::Minimize) - .on_click(cx.listener(|pane, _, window, cx| { - pane.toggle_zoom(&crate::ToggleZoom, window, cx); - })) - .tooltip(move |_window, cx| { - Tooltip::for_action( - if zoomed { "Zoom Out" } else { "Zoom In" }, - &ToggleZoom, - cx, - ) - }) - }) - .into_any_element() - .into(); - (None, right_children) -} - -impl Focusable for Pane { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Render for Pane { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let mut key_context = KeyContext::new_with_defaults(); - key_context.add("Pane"); - if self.active_item().is_none() { - key_context.add("EmptyPane"); - } - - self.toolbar - .read(cx) - .contribute_context(&mut key_context, cx); - - let should_display_tab_bar = self.should_display_tab_bar.clone(); - let display_tab_bar = should_display_tab_bar(window, cx); - let Some(project) = self.project.upgrade() else { - return div().track_focus(&self.focus_handle(cx)); - }; - let is_local = project.read(cx).is_local(); - - v_flex() - .key_context(key_context) - .track_focus(&self.focus_handle(cx)) - .size_full() - .flex_none() - .overflow_hidden() - .on_action( - cx.listener(|pane, _: &SplitLeft, _, cx| pane.split(SplitDirection::Left, cx)), - ) - .on_action(cx.listener(|pane, _: &SplitUp, _, cx| pane.split(SplitDirection::Up, cx))) - .on_action(cx.listener(|pane, _: &SplitHorizontal, _, cx| { - pane.split(SplitDirection::horizontal(cx), cx) - })) - .on_action(cx.listener(|pane, _: &SplitVertical, _, cx| { - pane.split(SplitDirection::vertical(cx), cx) - })) - .on_action( - cx.listener(|pane, _: &SplitRight, _, cx| pane.split(SplitDirection::Right, cx)), - ) - .on_action( - cx.listener(|pane, _: &SplitDown, _, cx| pane.split(SplitDirection::Down, cx)), - ) - .on_action(cx.listener(|pane, _: &SplitAndMoveUp, _, cx| { - pane.split_and_move(SplitDirection::Up, cx) - })) - .on_action(cx.listener(|pane, _: &SplitAndMoveDown, _, cx| { - pane.split_and_move(SplitDirection::Down, cx) - })) - .on_action(cx.listener(|pane, _: &SplitAndMoveLeft, _, cx| { - pane.split_and_move(SplitDirection::Left, cx) - })) - .on_action(cx.listener(|pane, _: &SplitAndMoveRight, _, cx| { - pane.split_and_move(SplitDirection::Right, cx) - })) - .on_action(cx.listener(|_, _: &JoinIntoNext, _, cx| { - cx.emit(Event::JoinIntoNext); - })) - .on_action(cx.listener(|_, _: &JoinAll, _, cx| { - 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( - action.0.min(pane.items.len().saturating_sub(1)), - true, - true, - window, - cx, - ); - }), - ) - .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)) - .when(PreviewTabsSettings::get_global(cx).enabled, |this| { - this.on_action( - cx.listener(|pane: &mut Pane, _: &TogglePreviewTab, window, cx| { - if let Some(active_item_id) = pane.active_item().map(|i| i.item_id()) { - if pane.is_active_preview_item(active_item_id) { - pane.unpreview_item_if_preview(active_item_id); - } else { - pane.replace_preview_item_id(active_item_id, window, cx); - } - } - }), - ) - }) - .on_action( - cx.listener(|pane: &mut Self, action: &CloseActiveItem, window, cx| { - pane.close_active_item(action, window, cx) - .detach_and_log_err(cx) - }), - ) - .on_action( - cx.listener(|pane: &mut Self, action: &CloseOtherItems, window, cx| { - pane.close_other_items(action, None, window, cx) - .detach_and_log_err(cx); - }), - ) - .on_action( - cx.listener(|pane: &mut Self, action: &CloseCleanItems, window, cx| { - pane.close_clean_items(action, window, cx) - .detach_and_log_err(cx) - }), - ) - .on_action(cx.listener( - |pane: &mut Self, action: &CloseItemsToTheLeft, window, cx| { - pane.close_items_to_the_left_by_id(None, action, window, cx) - .detach_and_log_err(cx) - }, - )) - .on_action(cx.listener( - |pane: &mut Self, action: &CloseItemsToTheRight, window, cx| { - pane.close_items_to_the_right_by_id(None, action, window, cx) - .detach_and_log_err(cx) - }, - )) - .on_action( - cx.listener(|pane: &mut Self, action: &CloseAllItems, window, cx| { - pane.close_all_items(action, window, cx) - .detach_and_log_err(cx) - }), - ) - .on_action(cx.listener( - |pane: &mut Self, action: &CloseMultibufferItems, window, cx| { - pane.close_multibuffer_items(action, window, cx) - .detach_and_log_err(cx) - }, - )) - .on_action( - cx.listener(|pane: &mut Self, action: &RevealInProjectPanel, _, cx| { - let entry_id = action - .entry_id - .map(ProjectEntryId::from_proto) - .or_else(|| pane.active_item()?.project_entry_ids(cx).first().copied()); - if let Some(entry_id) = entry_id { - pane.project - .update(cx, |_, cx| { - cx.emit(project::Event::RevealInProjectPanel(entry_id)) - }) - .ok(); - } - }), - ) - .on_action(cx.listener(|_, _: &menu::Cancel, window, cx| { - if cx.stop_active_drag(window) { - } else { - cx.propagate(); - } - })) - .when(self.active_item().is_some() && display_tab_bar, |pane| { - pane.child((self.render_tab_bar.clone())(self, window, cx)) - }) - .child({ - let has_worktrees = project.read(cx).visible_worktrees(cx).next().is_some(); - // main content - div() - .flex_1() - .relative() - .group("") - .overflow_hidden() - .on_drag_move::(cx.listener(Self::handle_drag_move)) - .on_drag_move::(cx.listener(Self::handle_drag_move)) - .when(is_local, |div| { - div.on_drag_move::(cx.listener(Self::handle_drag_move)) - }) - .map(|div| { - if let Some(item) = self.active_item() { - div.id("pane_placeholder") - .v_flex() - .size_full() - .overflow_hidden() - .child(self.toolbar.clone()) - .child(item.to_any_view()) - } else { - let placeholder = div - .id("pane_placeholder") - .h_flex() - .size_full() - .justify_center() - .on_click(cx.listener( - move |this, event: &ClickEvent, window, cx| { - if event.click_count() == 2 { - window.dispatch_action( - this.double_click_dispatch_action.boxed_clone(), - cx, - ); - } - }, - )); - if has_worktrees { - placeholder - } else { - placeholder.child( - Label::new("Open a file or project to get started.") - .color(Color::Muted), - ) - } - } - }) - .child( - // drag target - div() - .invisible() - .absolute() - .bg(cx.theme().colors().drop_target_background) - .group_drag_over::("", |style| style.visible()) - .group_drag_over::("", |style| style.visible()) - .when(is_local, |div| { - div.group_drag_over::("", |style| style.visible()) - }) - .when_some(self.can_drop_predicate.clone(), |this, p| { - this.can_drop(move |a, window, cx| p(a, window, cx)) - }) - .on_drop(cx.listener(move |this, dragged_tab, window, cx| { - this.handle_tab_drop( - dragged_tab, - this.active_item_index(), - window, - cx, - ) - })) - .on_drop(cx.listener( - move |this, selection: &DraggedSelection, window, cx| { - this.handle_dragged_selection_drop(selection, None, window, cx) - }, - )) - .on_drop(cx.listener(move |this, paths, window, cx| { - this.handle_external_paths_drop(paths, window, cx) - })) - .map(|div| { - let size = DefiniteLength::Fraction(0.5); - match self.drag_split_direction { - None => div.top_0().right_0().bottom_0().left_0(), - Some(SplitDirection::Up) => { - div.top_0().left_0().right_0().h(size) - } - Some(SplitDirection::Down) => { - div.left_0().bottom_0().right_0().h(size) - } - Some(SplitDirection::Left) => { - div.top_0().left_0().bottom_0().w(size) - } - Some(SplitDirection::Right) => { - div.top_0().bottom_0().right_0().w(size) - } - } - }), - ) - }) - .on_mouse_down( - MouseButton::Navigate(NavigationDirection::Back), - cx.listener(|pane, _, window, cx| { - if let Some(workspace) = pane.workspace.upgrade() { - let pane = cx.entity().downgrade(); - window.defer(cx, move |window, cx| { - workspace.update(cx, |workspace, cx| { - workspace.go_back(pane, window, cx).detach_and_log_err(cx) - }) - }) - } - }), - ) - .on_mouse_down( - MouseButton::Navigate(NavigationDirection::Forward), - cx.listener(|pane, _, window, cx| { - if let Some(workspace) = pane.workspace.upgrade() { - let pane = cx.entity().downgrade(); - window.defer(cx, move |window, cx| { - workspace.update(cx, |workspace, cx| { - workspace - .go_forward(pane, window, cx) - .detach_and_log_err(cx) - }) - }) - } - }), - ) - } -} - -impl ItemNavHistory { - pub fn push(&mut self, data: Option, cx: &mut App) { - if self - .item - .upgrade() - .is_some_and(|item| item.include_in_nav_history()) - { - self.history - .push(data, self.item.clone(), self.is_preview, cx); - } - } - - pub fn pop_backward(&mut self, cx: &mut App) -> Option { - self.history.pop(NavigationMode::GoingBack, cx) - } - - pub fn pop_forward(&mut self, cx: &mut App) -> Option { - self.history.pop(NavigationMode::GoingForward, cx) - } -} - -impl NavHistory { - pub fn for_each_entry( - &self, - cx: &App, - mut f: impl FnMut(&NavigationEntry, (ProjectPath, Option)), - ) { - let borrowed_history = self.0.lock(); - borrowed_history - .forward_stack - .iter() - .chain(borrowed_history.backward_stack.iter()) - .chain(borrowed_history.closed_stack.iter()) - .for_each(|entry| { - if let Some(project_and_abs_path) = - borrowed_history.paths_by_item.get(&entry.item.id()) - { - f(entry, project_and_abs_path.clone()); - } else if let Some(item) = entry.item.upgrade() - && let Some(path) = item.project_path(cx) - { - f(entry, (path, None)); - } - }) - } - - pub fn set_mode(&mut self, mode: NavigationMode) { - self.0.lock().mode = mode; - } - - pub fn mode(&self) -> NavigationMode { - self.0.lock().mode - } - - pub fn disable(&mut self) { - self.0.lock().mode = NavigationMode::Disabled; - } - - pub fn enable(&mut self) { - self.0.lock().mode = NavigationMode::Normal; - } - - pub fn clear(&mut self, cx: &mut App) { - let mut state = self.0.lock(); - - if state.backward_stack.is_empty() - && state.forward_stack.is_empty() - && state.closed_stack.is_empty() - && state.paths_by_item.is_empty() - { - return; - } - - state.mode = NavigationMode::Normal; - state.backward_stack.clear(); - state.forward_stack.clear(); - state.closed_stack.clear(); - state.paths_by_item.clear(); - state.did_update(cx); - } - - pub fn pop(&mut self, mode: NavigationMode, cx: &mut App) -> Option { - let mut state = self.0.lock(); - let entry = match mode { - NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => { - return None; - } - NavigationMode::GoingBack => &mut state.backward_stack, - NavigationMode::GoingForward => &mut state.forward_stack, - NavigationMode::ReopeningClosedItem => &mut state.closed_stack, - } - .pop_back(); - if entry.is_some() { - state.did_update(cx); - } - entry - } - - pub fn push( - &mut self, - data: Option, - item: Arc, - is_preview: bool, - cx: &mut App, - ) { - let state = &mut *self.0.lock(); - match state.mode { - NavigationMode::Disabled => {} - NavigationMode::Normal | NavigationMode::ReopeningClosedItem => { - if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { - state.backward_stack.pop_front(); - } - state.backward_stack.push_back(NavigationEntry { - item, - data: data.map(|data| Box::new(data) as Box), - timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), - is_preview, - }); - state.forward_stack.clear(); - } - NavigationMode::GoingBack => { - if state.forward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { - state.forward_stack.pop_front(); - } - state.forward_stack.push_back(NavigationEntry { - item, - data: data.map(|data| Box::new(data) as Box), - timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), - is_preview, - }); - } - NavigationMode::GoingForward => { - if state.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { - state.backward_stack.pop_front(); - } - state.backward_stack.push_back(NavigationEntry { - item, - data: data.map(|data| Box::new(data) as Box), - timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), - is_preview, - }); - } - NavigationMode::ClosingItem if is_preview => return, - NavigationMode::ClosingItem => { - if state.closed_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { - state.closed_stack.pop_front(); - } - state.closed_stack.push_back(NavigationEntry { - item, - data: data.map(|data| Box::new(data) as Box), - timestamp: state.next_timestamp.fetch_add(1, Ordering::SeqCst), - is_preview, - }); - } - } - state.did_update(cx); - } - - pub fn remove_item(&mut self, item_id: EntityId) { - let mut state = self.0.lock(); - state.paths_by_item.remove(&item_id); - state - .backward_stack - .retain(|entry| entry.item.id() != item_id); - state - .forward_stack - .retain(|entry| entry.item.id() != item_id); - state - .closed_stack - .retain(|entry| entry.item.id() != item_id); - } - - pub fn rename_item( - &mut self, - item_id: EntityId, - project_path: ProjectPath, - abs_path: Option, - ) { - let mut state = self.0.lock(); - let path_for_item = state.paths_by_item.get_mut(&item_id); - if let Some(path_for_item) = path_for_item { - path_for_item.0 = project_path; - path_for_item.1 = abs_path; - } - } - - pub fn path_for_item(&self, item_id: EntityId) -> Option<(ProjectPath, Option)> { - self.0.lock().paths_by_item.get(&item_id).cloned() - } -} - -impl NavHistoryState { - pub fn did_update(&self, cx: &mut App) { - if let Some(pane) = self.pane.upgrade() { - cx.defer(move |cx| { - pane.update(cx, |pane, cx| pane.history_updated(cx)); - }); - } - } -} - -fn dirty_message_for(buffer_path: Option, path_style: PathStyle) -> String { - let path = buffer_path - .as_ref() - .and_then(|p| { - let path = p.path.display(path_style); - if path.is_empty() { None } else { Some(path) } - }) - .unwrap_or("This buffer".into()); - let path = truncate_and_remove_front(&path, 80); - format!("{path} contains unsaved edits. Do you want to save it?") -} - -pub fn tab_details(items: &[Box], _window: &Window, cx: &App) -> Vec { - let mut tab_details = items.iter().map(|_| 0).collect::>(); - let mut tab_descriptions = HashMap::default(); - let mut done = false; - while !done { - done = true; - - // Store item indices by their tab description. - for (ix, (item, detail)) in items.iter().zip(&tab_details).enumerate() { - let description = item.tab_content_text(*detail, cx); - if *detail == 0 || description != item.tab_content_text(detail - 1, cx) { - tab_descriptions - .entry(description) - .or_insert(Vec::new()) - .push(ix); - } - } - - // If two or more items have the same tab description, increase their level - // of detail and try again. - for (_, item_ixs) in tab_descriptions.drain() { - if item_ixs.len() > 1 { - done = false; - for ix in item_ixs { - tab_details[ix] += 1; - } - } - } - } - - tab_details -} - -pub fn render_item_indicator(item: Box, cx: &App) -> Option { - maybe!({ - let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) { - (true, _) => Color::Warning, - (_, true) => Color::Accent, - (false, false) => return None, - }; - - Some(Indicator::dot().color(indicator_color)) - }) -} - -impl Render for DraggedTab { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let ui_font = ThemeSettings::get_global(cx).ui_font.clone(); - let label = self.item.tab_content( - TabContentParams { - detail: Some(self.detail), - selected: false, - preview: false, - deemphasized: false, - }, - window, - cx, - ); - Tab::new("") - .toggle_state(self.is_active) - .child(label) - .render(window, cx) - .font(ui_font) - } -} - -#[cfg(test)] -mod tests { - use std::num::NonZero; - - use super::*; - use crate::item::test::{TestItem, TestProjectItem}; - use gpui::{TestAppContext, VisualTestContext, size}; - use project::FakeFs; - use settings::SettingsStore; - use theme::LoadThemes; - use util::TryFutureExt; - - #[gpui::test] - async fn test_add_item_capped_to_max_tabs(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.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - for i in 0..7 { - add_labeled_item(&pane, format!("{}", i).as_str(), false, cx); - } - - set_max_tabs(cx, Some(5)); - add_labeled_item(&pane, "7", false, cx); - // Remove items to respect the max tab cap. - assert_item_labels(&pane, ["3", "4", "5", "6", "7*"], cx); - pane.update_in(cx, |pane, window, cx| { - pane.activate_item(0, false, false, window, cx); - }); - add_labeled_item(&pane, "X", false, cx); - // Respect activation order. - assert_item_labels(&pane, ["3", "X*", "5", "6", "7"], cx); - - for i in 0..7 { - add_labeled_item(&pane, format!("D{}", i).as_str(), true, cx); - } - // Keeps dirty items, even over max tab cap. - assert_item_labels( - &pane, - ["D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6*^"], - cx, - ); - - set_max_tabs(cx, None); - for i in 0..7 { - add_labeled_item(&pane, format!("N{}", i).as_str(), false, cx); - } - // No cap when max tabs is None. - assert_item_labels( - &pane, - [ - "D0^", "D1^", "D2^", "D3^", "D4^", "D5^", "D6^", "N0", "N1", "N2", "N3", "N4", - "N5", "N6*", - ], - cx, - ); - } - - #[gpui::test] - async fn test_reduce_max_tabs_closes_existing_items(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.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - add_labeled_item(&pane, "A", false, cx); - add_labeled_item(&pane, "B", false, cx); - let item_c = add_labeled_item(&pane, "C", false, cx); - let item_d = add_labeled_item(&pane, "D", false, cx); - add_labeled_item(&pane, "E", false, cx); - add_labeled_item(&pane, "Settings", false, cx); - assert_item_labels(&pane, ["A", "B", "C", "D", "E", "Settings*"], cx); - - set_max_tabs(cx, Some(5)); - assert_item_labels(&pane, ["B", "C", "D", "E", "Settings*"], cx); - - set_max_tabs(cx, Some(4)); - assert_item_labels(&pane, ["C", "D", "E", "Settings*"], cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_c.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - - let ix = pane.index_for_item_id(item_d.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["C!", "D!", "E", "Settings*"], cx); - - set_max_tabs(cx, Some(2)); - assert_item_labels(&pane, ["C!", "D!", "Settings*"], cx); - } - - #[gpui::test] - async fn test_allow_pinning_dirty_item_at_max_tabs(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.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - set_max_tabs(cx, Some(1)); - let item_a = add_labeled_item(&pane, "A", true, cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A*^!"], cx); - } - - #[gpui::test] - async fn test_allow_pinning_non_dirty_item_at_max_tabs(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.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - set_max_tabs(cx, Some(1)); - let item_a = add_labeled_item(&pane, "A", false, cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A*!"], cx); - } - - #[gpui::test] - async fn test_pin_tabs_incrementally_at_max_capacity(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.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - set_max_tabs(cx, Some(3)); - - let item_a = add_labeled_item(&pane, "A", false, cx); - assert_item_labels(&pane, ["A*"], cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A*!"], cx); - - let item_b = add_labeled_item(&pane, "B", false, cx); - assert_item_labels(&pane, ["A!", "B*"], cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A!", "B*!"], cx); - - let item_c = add_labeled_item(&pane, "C", false, cx); - assert_item_labels(&pane, ["A!", "B!", "C*"], cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_c.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A!", "B!", "C*!"], cx); - } - - #[gpui::test] - async fn test_pin_tabs_left_to_right_after_opening_at_max_capacity(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.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - set_max_tabs(cx, Some(3)); - - let item_a = add_labeled_item(&pane, "A", false, cx); - assert_item_labels(&pane, ["A*"], cx); - - let item_b = add_labeled_item(&pane, "B", false, cx); - assert_item_labels(&pane, ["A", "B*"], cx); - - let item_c = add_labeled_item(&pane, "C", false, cx); - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A!", "B", "C*"], cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A!", "B!", "C*"], cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_c.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A!", "B!", "C*!"], cx); - } - - #[gpui::test] - async fn test_pin_tabs_right_to_left_after_opening_at_max_capacity(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.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - set_max_tabs(cx, Some(3)); - - let item_a = add_labeled_item(&pane, "A", false, cx); - assert_item_labels(&pane, ["A*"], cx); - - let item_b = add_labeled_item(&pane, "B", false, cx); - assert_item_labels(&pane, ["A", "B*"], cx); - - let item_c = add_labeled_item(&pane, "C", false, cx); - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_c.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["C*!", "A", "B"], cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["C*!", "B!", "A"], cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["C*!", "B!", "A!"], cx); - } - - #[gpui::test] - async fn test_pinned_tabs_never_closed_at_max_tabs(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.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - let item_a = add_labeled_item(&pane, "A", false, cx); - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - - let item_b = add_labeled_item(&pane, "B", false, cx); - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - - add_labeled_item(&pane, "C", false, cx); - add_labeled_item(&pane, "D", false, cx); - add_labeled_item(&pane, "E", false, cx); - assert_item_labels(&pane, ["A!", "B!", "C", "D", "E*"], cx); - - set_max_tabs(cx, Some(3)); - add_labeled_item(&pane, "F", false, cx); - assert_item_labels(&pane, ["A!", "B!", "F*"], cx); - - add_labeled_item(&pane, "G", false, cx); - assert_item_labels(&pane, ["A!", "B!", "G*"], cx); - - add_labeled_item(&pane, "H", false, cx); - assert_item_labels(&pane, ["A!", "B!", "H*"], cx); - } - - #[gpui::test] - async fn test_always_allows_one_unpinned_item_over_max_tabs_regardless_of_pinned_count( - 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.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - set_max_tabs(cx, Some(3)); - - let item_a = add_labeled_item(&pane, "A", false, cx); - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - - let item_b = add_labeled_item(&pane, "B", false, cx); - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - - let item_c = add_labeled_item(&pane, "C", false, cx); - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_c.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - - assert_item_labels(&pane, ["A!", "B!", "C*!"], cx); - - let item_d = add_labeled_item(&pane, "D", false, cx); - assert_item_labels(&pane, ["A!", "B!", "C!", "D*"], cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_d.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A!", "B!", "C!", "D*!"], cx); - - add_labeled_item(&pane, "E", false, cx); - assert_item_labels(&pane, ["A!", "B!", "C!", "D!", "E*"], cx); - - add_labeled_item(&pane, "F", false, cx); - assert_item_labels(&pane, ["A!", "B!", "C!", "D!", "F*"], cx); - } - - #[gpui::test] - async fn test_can_open_one_item_when_all_tabs_are_dirty_at_max(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.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - set_max_tabs(cx, Some(3)); - - add_labeled_item(&pane, "A", true, cx); - assert_item_labels(&pane, ["A*^"], cx); - - add_labeled_item(&pane, "B", true, cx); - assert_item_labels(&pane, ["A^", "B*^"], cx); - - add_labeled_item(&pane, "C", true, cx); - assert_item_labels(&pane, ["A^", "B^", "C*^"], cx); - - add_labeled_item(&pane, "D", false, cx); - assert_item_labels(&pane, ["A^", "B^", "C^", "D*"], cx); - - add_labeled_item(&pane, "E", false, cx); - assert_item_labels(&pane, ["A^", "B^", "C^", "E*"], cx); - - add_labeled_item(&pane, "F", false, cx); - assert_item_labels(&pane, ["A^", "B^", "C^", "F*"], cx); - - add_labeled_item(&pane, "G", true, cx); - assert_item_labels(&pane, ["A^", "B^", "C^", "G*^"], cx); - } - - #[gpui::test] - async fn test_toggle_pin_tab(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.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - set_labeled_items(&pane, ["A", "B*", "C"], cx); - assert_item_labels(&pane, ["A", "B*", "C"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.toggle_pin_tab(&TogglePinTab, window, cx); - }); - assert_item_labels(&pane, ["B*!", "A", "C"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.toggle_pin_tab(&TogglePinTab, window, cx); - }); - assert_item_labels(&pane, ["B*", "A", "C"], cx); - } - - #[gpui::test] - async fn test_unpin_all_tabs(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.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Unpin all, in an empty pane - pane.update_in(cx, |pane, window, cx| { - pane.unpin_all_tabs(&UnpinAllTabs, window, cx); - }); - - assert_item_labels(&pane, [], cx); - - let item_a = add_labeled_item(&pane, "A", false, cx); - let item_b = add_labeled_item(&pane, "B", false, cx); - let item_c = add_labeled_item(&pane, "C", false, cx); - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - // Unpin all, when no tabs are pinned - pane.update_in(cx, |pane, window, cx| { - pane.unpin_all_tabs(&UnpinAllTabs, window, cx); - }); - - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - // Pin inactive tabs only - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A!", "B!", "C*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.unpin_all_tabs(&UnpinAllTabs, window, cx); - }); - - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - // Pin all tabs - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - - let ix = pane.index_for_item_id(item_c.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A!", "B!", "C*!"], cx); - - // Activate middle tab - pane.update_in(cx, |pane, window, cx| { - pane.activate_item(1, false, false, window, cx); - }); - assert_item_labels(&pane, ["A!", "B*!", "C!"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.unpin_all_tabs(&UnpinAllTabs, window, cx); - }); - - // Order has not changed - assert_item_labels(&pane, ["A", "B*", "C"], cx); - } - - #[gpui::test] - async fn test_pinning_active_tab_without_position_change_maintains_focus( - 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.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A - let item_a = add_labeled_item(&pane, "A", false, cx); - assert_item_labels(&pane, ["A*"], cx); - - // Add B - add_labeled_item(&pane, "B", false, cx); - assert_item_labels(&pane, ["A", "B*"], cx); - - // Activate A again - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.activate_item(ix, true, true, window, cx); - }); - assert_item_labels(&pane, ["A*", "B"], cx); - - // Pin A - remains active - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A*!", "B"], cx); - - // Unpin A - remain active - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.unpin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A*", "B"], cx); - } - - #[gpui::test] - async fn test_pinning_active_tab_with_position_change_maintains_focus(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.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A, B, C - add_labeled_item(&pane, "A", false, cx); - add_labeled_item(&pane, "B", false, cx); - let item_c = add_labeled_item(&pane, "C", false, cx); - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - // Pin C - moves to pinned area, remains active - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_c.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["C*!", "A", "B"], cx); - - // Unpin C - moves after pinned area, remains active - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_c.item_id()).unwrap(); - pane.unpin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["C*", "A", "B"], cx); - } - - #[gpui::test] - async fn test_pinning_inactive_tab_without_position_change_preserves_existing_focus( - 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.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A, B - let item_a = add_labeled_item(&pane, "A", false, cx); - add_labeled_item(&pane, "B", false, cx); - assert_item_labels(&pane, ["A", "B*"], cx); - - // Pin A - already in pinned area, B remains active - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A!", "B*"], cx); - - // Unpin A - stays in place, B remains active - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.unpin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A", "B*"], cx); - } - - #[gpui::test] - async fn test_pinning_inactive_tab_with_position_change_preserves_existing_focus( - 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.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A, B, C - add_labeled_item(&pane, "A", false, cx); - let item_b = add_labeled_item(&pane, "B", false, cx); - let item_c = add_labeled_item(&pane, "C", false, cx); - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - // Activate B - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.activate_item(ix, true, true, window, cx); - }); - assert_item_labels(&pane, ["A", "B*", "C"], cx); - - // Pin C - moves to pinned area, B remains active - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_c.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["C!", "A", "B*"], cx); - - // Unpin C - moves after pinned area, B remains active - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_c.item_id()).unwrap(); - pane.unpin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["C", "A", "B*"], cx); - } - - #[gpui::test] - async fn test_drag_unpinned_tab_to_split_creates_pane_with_unpinned_tab( - 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.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A, B. Pin B. Activate A - let item_a = add_labeled_item(&pane_a, "A", false, cx); - let item_b = add_labeled_item(&pane_a, "B", false, cx); - - pane_a.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.activate_item(ix, true, true, window, cx); - }); - - // Drag A to create new split - pane_a.update_in(cx, |pane, window, cx| { - pane.drag_split_direction = Some(SplitDirection::Right); - - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 0, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 0, window, cx); - }); - - // A should be moved to new pane. B should remain pinned, A should not be pinned - let (pane_a, pane_b) = workspace.read_with(cx, |workspace, _| { - let panes = workspace.panes(); - (panes[0].clone(), panes[1].clone()) - }); - assert_item_labels(&pane_a, ["B*!"], cx); - assert_item_labels(&pane_b, ["A*"], cx); - } - - #[gpui::test] - async fn test_drag_pinned_tab_to_split_creates_pane_with_pinned_tab(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.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A, B. Pin both. Activate A - let item_a = add_labeled_item(&pane_a, "A", false, cx); - let item_b = add_labeled_item(&pane_a, "B", false, cx); - - pane_a.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.activate_item(ix, true, true, window, cx); - }); - assert_item_labels(&pane_a, ["A*!", "B!"], cx); - - // Drag A to create new split - pane_a.update_in(cx, |pane, window, cx| { - pane.drag_split_direction = Some(SplitDirection::Right); - - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 0, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 0, window, cx); - }); - - // A should be moved to new pane. Both A and B should still be pinned - let (pane_a, pane_b) = workspace.read_with(cx, |workspace, _| { - let panes = workspace.panes(); - (panes[0].clone(), panes[1].clone()) - }); - assert_item_labels(&pane_a, ["B*!"], cx); - assert_item_labels(&pane_b, ["A*!"], cx); - } - - #[gpui::test] - async fn test_drag_pinned_tab_into_existing_panes_pinned_region(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.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A to pane A and pin - let item_a = add_labeled_item(&pane_a, "A", false, cx); - pane_a.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane_a, ["A*!"], cx); - - // Add B to pane B and pin - let pane_b = workspace.update_in(cx, |workspace, window, cx| { - workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx) - }); - let item_b = add_labeled_item(&pane_b, "B", false, cx); - pane_b.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane_b, ["B*!"], cx); - - // Move A from pane A to pane B's pinned region - pane_b.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 0, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 0, window, cx); - }); - - // A should stay pinned - assert_item_labels(&pane_a, [], cx); - assert_item_labels(&pane_b, ["A*!", "B!"], cx); - } - - #[gpui::test] - async fn test_drag_pinned_tab_into_existing_panes_unpinned_region(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.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A to pane A and pin - let item_a = add_labeled_item(&pane_a, "A", false, cx); - pane_a.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane_a, ["A*!"], cx); - - // Create pane B with pinned item B - let pane_b = workspace.update_in(cx, |workspace, window, cx| { - workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx) - }); - let item_b = add_labeled_item(&pane_b, "B", false, cx); - assert_item_labels(&pane_b, ["B*"], cx); - - pane_b.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane_b, ["B*!"], cx); - - // Move A from pane A to pane B's unpinned region - pane_b.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 0, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 1, window, cx); - }); - - // A should become pinned - assert_item_labels(&pane_a, [], cx); - assert_item_labels(&pane_b, ["B!", "A*"], cx); - } - - #[gpui::test] - async fn test_drag_pinned_tab_into_existing_panes_first_position_with_no_pinned_tabs( - 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.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A to pane A and pin - let item_a = add_labeled_item(&pane_a, "A", false, cx); - pane_a.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane_a, ["A*!"], cx); - - // Add B to pane B - let pane_b = workspace.update_in(cx, |workspace, window, cx| { - workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx) - }); - add_labeled_item(&pane_b, "B", false, cx); - assert_item_labels(&pane_b, ["B*"], cx); - - // Move A from pane A to position 0 in pane B, indicating it should stay pinned - pane_b.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 0, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 0, window, cx); - }); - - // A should stay pinned - assert_item_labels(&pane_a, [], cx); - assert_item_labels(&pane_b, ["A*!", "B"], cx); - } - - #[gpui::test] - async fn test_drag_pinned_tab_into_existing_pane_at_max_capacity_closes_unpinned_tabs( - 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.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - set_max_tabs(cx, Some(2)); - - // Add A, B to pane A. Pin both - let item_a = add_labeled_item(&pane_a, "A", false, cx); - let item_b = add_labeled_item(&pane_a, "B", false, cx); - pane_a.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane_a, ["A!", "B*!"], cx); - - // Add C, D to pane B. Pin both - let pane_b = workspace.update_in(cx, |workspace, window, cx| { - workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx) - }); - let item_c = add_labeled_item(&pane_b, "C", false, cx); - let item_d = add_labeled_item(&pane_b, "D", false, cx); - pane_b.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_c.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - - let ix = pane.index_for_item_id(item_d.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane_b, ["C!", "D*!"], cx); - - // Add a third unpinned item to pane B (exceeds max tabs), but is allowed, - // as we allow 1 tab over max if the others are pinned or dirty - add_labeled_item(&pane_b, "E", false, cx); - assert_item_labels(&pane_b, ["C!", "D!", "E*"], cx); - - // Drag pinned A from pane A to position 0 in pane B - pane_b.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 0, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 0, window, cx); - }); - - // E (unpinned) should be closed, leaving 3 pinned items - assert_item_labels(&pane_a, ["B*!"], cx); - assert_item_labels(&pane_b, ["A*!", "C!", "D!"], cx); - } - - #[gpui::test] - async fn test_drag_last_pinned_tab_to_same_position_stays_pinned(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.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A to pane A and pin it - let item_a = add_labeled_item(&pane_a, "A", false, cx); - pane_a.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane_a, ["A*!"], cx); - - // Drag pinned A to position 1 (directly to the right) in the same pane - pane_a.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 0, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 1, window, cx); - }); - - // A should still be pinned and active - assert_item_labels(&pane_a, ["A*!"], cx); - } - - #[gpui::test] - async fn test_drag_pinned_tab_beyond_last_pinned_tab_in_same_pane_stays_pinned( - 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.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A, B to pane A and pin both - let item_a = add_labeled_item(&pane_a, "A", false, cx); - let item_b = add_labeled_item(&pane_a, "B", false, cx); - pane_a.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane_a, ["A!", "B*!"], cx); - - // Drag pinned A right of B in the same pane - pane_a.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 0, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 2, window, cx); - }); - - // A stays pinned - assert_item_labels(&pane_a, ["B!", "A*!"], cx); - } - - #[gpui::test] - async fn test_dragging_pinned_tab_onto_unpinned_tab_reduces_unpinned_tab_count( - 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.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A, B to pane A and pin A - let item_a = add_labeled_item(&pane_a, "A", false, cx); - add_labeled_item(&pane_a, "B", false, cx); - pane_a.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane_a, ["A!", "B*"], cx); - - // Drag pinned A on top of B in the same pane, which changes tab order to B, A - pane_a.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 0, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 1, window, cx); - }); - - // Neither are pinned - assert_item_labels(&pane_a, ["B", "A*"], cx); - } - - #[gpui::test] - async fn test_drag_pinned_tab_beyond_unpinned_tab_in_same_pane_becomes_unpinned( - 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.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A, B to pane A and pin A - let item_a = add_labeled_item(&pane_a, "A", false, cx); - add_labeled_item(&pane_a, "B", false, cx); - pane_a.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane_a, ["A!", "B*"], cx); - - // Drag pinned A right of B in the same pane - pane_a.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 0, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 2, window, cx); - }); - - // A becomes unpinned - assert_item_labels(&pane_a, ["B", "A*"], cx); - } - - #[gpui::test] - async fn test_drag_unpinned_tab_in_front_of_pinned_tab_in_same_pane_becomes_pinned( - 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.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A, B to pane A and pin A - let item_a = add_labeled_item(&pane_a, "A", false, cx); - let item_b = add_labeled_item(&pane_a, "B", false, cx); - pane_a.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane_a, ["A!", "B*"], cx); - - // Drag pinned B left of A in the same pane - pane_a.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_b.boxed_clone(), - ix: 1, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 0, window, cx); - }); - - // A becomes unpinned - assert_item_labels(&pane_a, ["B*!", "A!"], cx); - } - - #[gpui::test] - async fn test_drag_unpinned_tab_to_the_pinned_region_stays_pinned(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.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A, B, C to pane A and pin A - let item_a = add_labeled_item(&pane_a, "A", false, cx); - add_labeled_item(&pane_a, "B", false, cx); - let item_c = add_labeled_item(&pane_a, "C", false, cx); - pane_a.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane_a, ["A!", "B", "C*"], cx); - - // Drag pinned C left of B in the same pane - pane_a.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_c.boxed_clone(), - ix: 2, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 1, window, cx); - }); - - // A stays pinned, B and C remain unpinned - assert_item_labels(&pane_a, ["A!", "C*", "B"], cx); - } - - #[gpui::test] - async fn test_drag_unpinned_tab_into_existing_panes_pinned_region(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.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add unpinned item A to pane A - let item_a = add_labeled_item(&pane_a, "A", false, cx); - assert_item_labels(&pane_a, ["A*"], cx); - - // Create pane B with pinned item B - let pane_b = workspace.update_in(cx, |workspace, window, cx| { - workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx) - }); - let item_b = add_labeled_item(&pane_b, "B", false, cx); - pane_b.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane_b, ["B*!"], cx); - - // Move A from pane A to pane B's pinned region - pane_b.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 0, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 0, window, cx); - }); - - // A should become pinned since it was dropped in the pinned region - assert_item_labels(&pane_a, [], cx); - assert_item_labels(&pane_b, ["A*!", "B!"], cx); - } - - #[gpui::test] - async fn test_drag_unpinned_tab_into_existing_panes_unpinned_region(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.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add unpinned item A to pane A - let item_a = add_labeled_item(&pane_a, "A", false, cx); - assert_item_labels(&pane_a, ["A*"], cx); - - // Create pane B with one pinned item B - let pane_b = workspace.update_in(cx, |workspace, window, cx| { - workspace.split_pane(pane_a.clone(), SplitDirection::Right, window, cx) - }); - let item_b = add_labeled_item(&pane_b, "B", false, cx); - pane_b.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane_b, ["B*!"], cx); - - // Move A from pane A to pane B's unpinned region - pane_b.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 0, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 1, window, cx); - }); - - // A should remain unpinned since it was dropped outside the pinned region - assert_item_labels(&pane_a, [], cx); - assert_item_labels(&pane_b, ["B!", "A*"], cx); - } - - #[gpui::test] - async fn test_drag_pinned_tab_throughout_entire_range_of_pinned_tabs_both_directions( - 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.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A, B, C and pin all - let item_a = add_labeled_item(&pane_a, "A", false, cx); - let item_b = add_labeled_item(&pane_a, "B", false, cx); - let item_c = add_labeled_item(&pane_a, "C", false, cx); - assert_item_labels(&pane_a, ["A", "B", "C*"], cx); - - pane_a.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - - let ix = pane.index_for_item_id(item_c.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane_a, ["A!", "B!", "C*!"], cx); - - // Move A to right of B - pane_a.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 0, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 1, window, cx); - }); - - // A should be after B and all are pinned - assert_item_labels(&pane_a, ["B!", "A*!", "C!"], cx); - - // Move A to right of C - pane_a.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 1, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 2, window, cx); - }); - - // A should be after C and all are pinned - assert_item_labels(&pane_a, ["B!", "C!", "A*!"], cx); - - // Move A to left of C - pane_a.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 2, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 1, window, cx); - }); - - // A should be before C and all are pinned - assert_item_labels(&pane_a, ["B!", "A*!", "C!"], cx); - - // Move A to left of B - pane_a.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 1, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 0, window, cx); - }); - - // A should be before B and all are pinned - assert_item_labels(&pane_a, ["A*!", "B!", "C!"], cx); - } - - #[gpui::test] - async fn test_drag_first_tab_to_last_position(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.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A, B, C - let item_a = add_labeled_item(&pane_a, "A", false, cx); - add_labeled_item(&pane_a, "B", false, cx); - add_labeled_item(&pane_a, "C", false, cx); - assert_item_labels(&pane_a, ["A", "B", "C*"], cx); - - // Move A to the end - pane_a.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_a.boxed_clone(), - ix: 0, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 2, window, cx); - }); - - // A should be at the end - assert_item_labels(&pane_a, ["B", "C", "A*"], cx); - } - - #[gpui::test] - async fn test_drag_last_tab_to_first_position(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.clone(), window, cx)); - let pane_a = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // Add A, B, C - add_labeled_item(&pane_a, "A", false, cx); - add_labeled_item(&pane_a, "B", false, cx); - let item_c = add_labeled_item(&pane_a, "C", false, cx); - assert_item_labels(&pane_a, ["A", "B", "C*"], cx); - - // Move C to the beginning - pane_a.update_in(cx, |pane, window, cx| { - let dragged_tab = DraggedTab { - pane: pane_a.clone(), - item: item_c.boxed_clone(), - ix: 2, - detail: 0, - is_active: true, - }; - pane.handle_tab_drop(&dragged_tab, 0, window, cx); - }); - - // C should be at the beginning - assert_item_labels(&pane_a, ["C*", "A", "B"], cx); - } - - #[gpui::test] - async fn test_add_item_with_new_item(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.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // 1. Add with a destination index - // a. Add before the active item - set_labeled_items(&pane, ["A", "B*", "C"], cx); - pane.update_in(cx, |pane, window, cx| { - pane.add_item( - Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))), - false, - false, - Some(0), - window, - cx, - ); - }); - assert_item_labels(&pane, ["D*", "A", "B", "C"], cx); - - // b. Add after the active item - set_labeled_items(&pane, ["A", "B*", "C"], cx); - pane.update_in(cx, |pane, window, cx| { - pane.add_item( - Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))), - false, - false, - Some(2), - window, - cx, - ); - }); - assert_item_labels(&pane, ["A", "B", "D*", "C"], cx); - - // c. Add at the end of the item list (including off the length) - set_labeled_items(&pane, ["A", "B*", "C"], cx); - pane.update_in(cx, |pane, window, cx| { - pane.add_item( - Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))), - false, - false, - Some(5), - window, - cx, - ); - }); - assert_item_labels(&pane, ["A", "B", "C", "D*"], cx); - - // 2. Add without a destination index - // a. Add with active item at the start of the item list - set_labeled_items(&pane, ["A*", "B", "C"], cx); - pane.update_in(cx, |pane, window, cx| { - pane.add_item( - Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))), - false, - false, - None, - window, - cx, - ); - }); - set_labeled_items(&pane, ["A", "D*", "B", "C"], cx); - - // b. Add with active item at the end of the item list - set_labeled_items(&pane, ["A", "B", "C*"], cx); - pane.update_in(cx, |pane, window, cx| { - pane.add_item( - Box::new(cx.new(|cx| TestItem::new(cx).with_label("D"))), - false, - false, - None, - window, - cx, - ); - }); - assert_item_labels(&pane, ["A", "B", "C", "D*"], cx); - } - - #[gpui::test] - async fn test_add_item_with_existing_item(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.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // 1. Add with a destination index - // 1a. Add before the active item - let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx); - pane.update_in(cx, |pane, window, cx| { - pane.add_item(d, false, false, Some(0), window, cx); - }); - assert_item_labels(&pane, ["D*", "A", "B", "C"], cx); - - // 1b. Add after the active item - let [_, _, _, d] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx); - pane.update_in(cx, |pane, window, cx| { - pane.add_item(d, false, false, Some(2), window, cx); - }); - assert_item_labels(&pane, ["A", "B", "D*", "C"], cx); - - // 1c. Add at the end of the item list (including off the length) - let [a, _, _, _] = set_labeled_items(&pane, ["A", "B*", "C", "D"], cx); - pane.update_in(cx, |pane, window, cx| { - pane.add_item(a, false, false, Some(5), window, cx); - }); - assert_item_labels(&pane, ["B", "C", "D", "A*"], cx); - - // 1d. Add same item to active index - let [_, b, _] = set_labeled_items(&pane, ["A", "B*", "C"], cx); - pane.update_in(cx, |pane, window, cx| { - pane.add_item(b, false, false, Some(1), window, cx); - }); - assert_item_labels(&pane, ["A", "B*", "C"], cx); - - // 1e. Add item to index after same item in last position - let [_, _, c] = set_labeled_items(&pane, ["A", "B*", "C"], cx); - pane.update_in(cx, |pane, window, cx| { - pane.add_item(c, false, false, Some(2), window, cx); - }); - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - // 2. Add without a destination index - // 2a. Add with active item at the start of the item list - let [_, _, _, d] = set_labeled_items(&pane, ["A*", "B", "C", "D"], cx); - pane.update_in(cx, |pane, window, cx| { - pane.add_item(d, false, false, None, window, cx); - }); - assert_item_labels(&pane, ["A", "D*", "B", "C"], cx); - - // 2b. Add with active item at the end of the item list - let [a, _, _, _] = set_labeled_items(&pane, ["A", "B", "C", "D*"], cx); - pane.update_in(cx, |pane, window, cx| { - pane.add_item(a, false, false, None, window, cx); - }); - assert_item_labels(&pane, ["B", "C", "D", "A*"], cx); - - // 2c. Add active item to active item at end of list - let [_, _, c] = set_labeled_items(&pane, ["A", "B", "C*"], cx); - pane.update_in(cx, |pane, window, cx| { - pane.add_item(c, false, false, None, window, cx); - }); - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - // 2d. Add active item to active item at start of list - let [a, _, _] = set_labeled_items(&pane, ["A*", "B", "C"], cx); - pane.update_in(cx, |pane, window, cx| { - pane.add_item(a, false, false, None, window, cx); - }); - assert_item_labels(&pane, ["A*", "B", "C"], cx); - } - - #[gpui::test] - async fn test_add_item_with_same_project_entries(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.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - // singleton view - pane.update_in(cx, |pane, window, cx| { - pane.add_item( - Box::new(cx.new(|cx| { - TestItem::new(cx) - .with_buffer_kind(ItemBufferKind::Singleton) - .with_label("buffer 1") - .with_project_items(&[TestProjectItem::new(1, "one.txt", cx)]) - })), - false, - false, - None, - window, - cx, - ); - }); - assert_item_labels(&pane, ["buffer 1*"], cx); - - // new singleton view with the same project entry - pane.update_in(cx, |pane, window, cx| { - pane.add_item( - Box::new(cx.new(|cx| { - TestItem::new(cx) - .with_buffer_kind(ItemBufferKind::Singleton) - .with_label("buffer 1") - .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) - })), - false, - false, - None, - window, - cx, - ); - }); - assert_item_labels(&pane, ["buffer 1*"], cx); - - // new singleton view with different project entry - pane.update_in(cx, |pane, window, cx| { - pane.add_item( - Box::new(cx.new(|cx| { - TestItem::new(cx) - .with_buffer_kind(ItemBufferKind::Singleton) - .with_label("buffer 2") - .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)]) - })), - false, - false, - None, - window, - cx, - ); - }); - assert_item_labels(&pane, ["buffer 1", "buffer 2*"], cx); - - // new multibuffer view with the same project entry - pane.update_in(cx, |pane, window, cx| { - pane.add_item( - Box::new(cx.new(|cx| { - TestItem::new(cx) - .with_buffer_kind(ItemBufferKind::Multibuffer) - .with_label("multibuffer 1") - .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) - })), - false, - false, - None, - window, - cx, - ); - }); - assert_item_labels(&pane, ["buffer 1", "buffer 2", "multibuffer 1*"], cx); - - // another multibuffer view with the same project entry - pane.update_in(cx, |pane, window, cx| { - pane.add_item( - Box::new(cx.new(|cx| { - TestItem::new(cx) - .with_buffer_kind(ItemBufferKind::Multibuffer) - .with_label("multibuffer 1b") - .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) - })), - false, - false, - None, - window, - cx, - ); - }); - assert_item_labels( - &pane, - ["buffer 1", "buffer 2", "multibuffer 1", "multibuffer 1b*"], - cx, - ); - } - - #[gpui::test] - async fn test_remove_item_ordering_history(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.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - add_labeled_item(&pane, "A", false, cx); - add_labeled_item(&pane, "B", false, cx); - add_labeled_item(&pane, "C", false, cx); - add_labeled_item(&pane, "D", false, cx); - assert_item_labels(&pane, ["A", "B", "C", "D*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.activate_item(1, false, false, window, cx) - }); - add_labeled_item(&pane, "1", false, cx); - assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["A", "B*", "C", "D"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.activate_item(3, false, false, window, cx) - }); - assert_item_labels(&pane, ["A", "B", "C", "D*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["A", "B*", "C"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["A", "C*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["A*"], cx); - } - - #[gpui::test] - async fn test_remove_item_ordering_neighbour(cx: &mut TestAppContext) { - init_test(cx); - cx.update_global::(|s, cx| { - s.update_user_settings(cx, |s| { - s.tabs.get_or_insert_default().activate_on_close = Some(ActivateOnClose::Neighbour); - }); - }); - 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.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - add_labeled_item(&pane, "A", false, cx); - add_labeled_item(&pane, "B", false, cx); - add_labeled_item(&pane, "C", false, cx); - add_labeled_item(&pane, "D", false, cx); - assert_item_labels(&pane, ["A", "B", "C", "D*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.activate_item(1, false, false, window, cx) - }); - add_labeled_item(&pane, "1", false, cx); - assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["A", "B", "C*", "D"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.activate_item(3, false, false, window, cx) - }); - assert_item_labels(&pane, ["A", "B", "C", "D*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["A", "B*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["A*"], cx); - } - - #[gpui::test] - async fn test_remove_item_ordering_left_neighbour(cx: &mut TestAppContext) { - init_test(cx); - cx.update_global::(|s, cx| { - s.update_user_settings(cx, |s| { - s.tabs.get_or_insert_default().activate_on_close = - Some(ActivateOnClose::LeftNeighbour); - }); - }); - 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.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - add_labeled_item(&pane, "A", false, cx); - add_labeled_item(&pane, "B", false, cx); - add_labeled_item(&pane, "C", false, cx); - add_labeled_item(&pane, "D", false, cx); - assert_item_labels(&pane, ["A", "B", "C", "D*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.activate_item(1, false, false, window, cx) - }); - add_labeled_item(&pane, "1", false, cx); - assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["A", "B*", "C", "D"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.activate_item(3, false, false, window, cx) - }); - assert_item_labels(&pane, ["A", "B", "C", "D*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.activate_item(0, false, false, window, cx) - }); - assert_item_labels(&pane, ["A*", "B", "C"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["B*", "C"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["C*"], cx); - } - - #[gpui::test] - async fn test_close_inactive_items(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.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - let item_a = add_labeled_item(&pane, "A", false, cx); - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A*!"], cx); - - let item_b = add_labeled_item(&pane, "B", false, cx); - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_b.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - }); - assert_item_labels(&pane, ["A!", "B*!"], cx); - - add_labeled_item(&pane, "C", false, cx); - assert_item_labels(&pane, ["A!", "B!", "C*"], cx); - - add_labeled_item(&pane, "D", false, cx); - add_labeled_item(&pane, "E", false, cx); - assert_item_labels(&pane, ["A!", "B!", "C", "D", "E*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_other_items( - &CloseOtherItems { - save_intent: None, - close_pinned: false, - }, - None, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["A!", "B!", "E*"], cx); - } - - #[gpui::test] - async fn test_running_close_inactive_items_via_an_inactive_item(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.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - add_labeled_item(&pane, "A", false, cx); - assert_item_labels(&pane, ["A*"], cx); - - let item_b = add_labeled_item(&pane, "B", false, cx); - assert_item_labels(&pane, ["A", "B*"], cx); - - add_labeled_item(&pane, "C", false, cx); - add_labeled_item(&pane, "D", false, cx); - add_labeled_item(&pane, "E", false, cx); - assert_item_labels(&pane, ["A", "B", "C", "D", "E*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_other_items( - &CloseOtherItems { - save_intent: None, - close_pinned: false, - }, - Some(item_b.item_id()), - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["B*"], cx); - } - - #[gpui::test] - async fn test_close_clean_items(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.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - add_labeled_item(&pane, "A", true, cx); - add_labeled_item(&pane, "B", false, cx); - add_labeled_item(&pane, "C", true, cx); - add_labeled_item(&pane, "D", false, cx); - add_labeled_item(&pane, "E", false, cx); - assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_clean_items( - &CloseCleanItems { - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["A^", "C*^"], cx); - } - - #[gpui::test] - async fn test_close_items_to_the_left(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.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_items_to_the_left_by_id( - None, - &CloseItemsToTheLeft { - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["C*", "D", "E"], cx); - } - - #[gpui::test] - async fn test_close_items_to_the_right(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.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_items_to_the_right_by_id( - None, - &CloseItemsToTheRight { - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["A", "B", "C*"], cx); - } - - #[gpui::test] - async fn test_close_all_items(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.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - let item_a = 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| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - pane.close_all_items( - &CloseAllItems { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["A*!"], cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.unpin_tab_at(ix, window, cx); - pane.close_all_items( - &CloseAllItems { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - - assert_item_labels(&pane, [], cx); - - add_labeled_item(&pane, "A", true, cx).update(cx, |item, cx| { - item.project_items - .push(TestProjectItem::new_dirty(1, "A.txt", cx)) - }); - add_labeled_item(&pane, "B", true, cx).update(cx, |item, cx| { - item.project_items - .push(TestProjectItem::new_dirty(2, "B.txt", cx)) - }); - add_labeled_item(&pane, "C", true, cx).update(cx, |item, cx| { - item.project_items - .push(TestProjectItem::new_dirty(3, "C.txt", cx)) - }); - assert_item_labels(&pane, ["A^", "B^", "C*^"], cx); - - let save = pane.update_in(cx, |pane, window, cx| { - pane.close_all_items( - &CloseAllItems { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }); - - cx.executor().run_until_parked(); - cx.simulate_prompt_answer("Save all"); - save.await.unwrap(); - assert_item_labels(&pane, [], cx); - - add_labeled_item(&pane, "A", true, cx); - add_labeled_item(&pane, "B", true, cx); - add_labeled_item(&pane, "C", true, cx); - assert_item_labels(&pane, ["A^", "B^", "C*^"], cx); - let save = pane.update_in(cx, |pane, window, cx| { - pane.close_all_items( - &CloseAllItems { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }); - - cx.executor().run_until_parked(); - cx.simulate_prompt_answer("Discard all"); - save.await.unwrap(); - assert_item_labels(&pane, [], cx); - } - - #[gpui::test] - async fn test_close_multibuffer_items(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.clone(), window, cx)); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - - let add_labeled_item = |pane: &Entity, - label, - is_dirty, - kind: ItemBufferKind, - cx: &mut VisualTestContext| { - pane.update_in(cx, |pane, window, cx| { - let labeled_item = Box::new(cx.new(|cx| { - TestItem::new(cx) - .with_label(label) - .with_dirty(is_dirty) - .with_buffer_kind(kind) - })); - pane.add_item(labeled_item.clone(), false, false, None, window, cx); - labeled_item - }) - }; - - let item_a = add_labeled_item(&pane, "A", false, ItemBufferKind::Multibuffer, cx); - add_labeled_item(&pane, "B", false, ItemBufferKind::Multibuffer, cx); - add_labeled_item(&pane, "C", false, ItemBufferKind::Singleton, cx); - assert_item_labels(&pane, ["A", "B", "C*"], cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - pane.close_multibuffer_items( - &CloseMultibufferItems { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, ["A!", "C*"], cx); - - pane.update_in(cx, |pane, window, cx| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.unpin_tab_at(ix, window, cx); - pane.close_multibuffer_items( - &CloseMultibufferItems { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - - assert_item_labels(&pane, ["C*"], cx); - - add_labeled_item(&pane, "A", true, ItemBufferKind::Singleton, cx).update(cx, |item, cx| { - item.project_items - .push(TestProjectItem::new_dirty(1, "A.txt", cx)) - }); - add_labeled_item(&pane, "B", true, ItemBufferKind::Multibuffer, cx).update( - cx, - |item, cx| { - item.project_items - .push(TestProjectItem::new_dirty(2, "B.txt", cx)) - }, - ); - add_labeled_item(&pane, "D", true, ItemBufferKind::Multibuffer, cx).update( - cx, - |item, cx| { - item.project_items - .push(TestProjectItem::new_dirty(3, "D.txt", cx)) - }, - ); - assert_item_labels(&pane, ["C", "A^", "B^", "D*^"], cx); - - let save = pane.update_in(cx, |pane, window, cx| { - pane.close_multibuffer_items( - &CloseMultibufferItems { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }); - - cx.executor().run_until_parked(); - cx.simulate_prompt_answer("Save all"); - save.await.unwrap(); - assert_item_labels(&pane, ["C", "A*^"], cx); - - add_labeled_item(&pane, "B", true, ItemBufferKind::Multibuffer, cx).update( - cx, - |item, cx| { - item.project_items - .push(TestProjectItem::new_dirty(2, "B.txt", cx)) - }, - ); - add_labeled_item(&pane, "D", true, ItemBufferKind::Multibuffer, cx).update( - cx, - |item, cx| { - item.project_items - .push(TestProjectItem::new_dirty(3, "D.txt", cx)) - }, - ); - assert_item_labels(&pane, ["C", "A^", "B^", "D*^"], cx); - let save = pane.update_in(cx, |pane, window, cx| { - pane.close_multibuffer_items( - &CloseMultibufferItems { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }); - - cx.executor().run_until_parked(); - cx.simulate_prompt_answer("Discard all"); - save.await.unwrap(); - assert_item_labels(&pane, ["C", "A*^"], cx); - } - - #[gpui::test] - async fn test_close_with_save_intent(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()); - - let a = cx.update(|_, cx| TestProjectItem::new_dirty(1, "A.txt", cx)); - let b = cx.update(|_, cx| TestProjectItem::new_dirty(1, "B.txt", cx)); - let c = cx.update(|_, cx| TestProjectItem::new_dirty(1, "C.txt", cx)); - - add_labeled_item(&pane, "AB", true, cx).update(cx, |item, _| { - item.project_items.push(a.clone()); - item.project_items.push(b.clone()); - }); - add_labeled_item(&pane, "C", true, cx) - .update(cx, |item, _| item.project_items.push(c.clone())); - assert_item_labels(&pane, ["AB^", "C*^"], cx); - - pane.update_in(cx, |pane, window, cx| { - pane.close_all_items( - &CloseAllItems { - save_intent: Some(SaveIntent::Save), - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - - assert_item_labels(&pane, [], cx); - cx.update(|_, cx| { - assert!(!a.read(cx).is_dirty); - assert!(!b.read(cx).is_dirty); - assert!(!c.read(cx).is_dirty); - }); - } - - #[gpui::test] - async fn test_new_tab_scrolls_into_view_completely(cx: &mut TestAppContext) { - // Arrange - 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()); - - cx.simulate_resize(size(px(300.), px(300.))); - - add_labeled_item(&pane, "untitled", false, cx); - add_labeled_item(&pane, "untitled", false, cx); - add_labeled_item(&pane, "untitled", false, cx); - add_labeled_item(&pane, "untitled", false, cx); - // Act: this should trigger a scroll - add_labeled_item(&pane, "untitled", false, cx); - // Assert - let tab_bar_scroll_handle = - pane.update_in(cx, |pane, _window, _cx| pane.tab_bar_scroll_handle.clone()); - assert_eq!(tab_bar_scroll_handle.children_count(), 6); - let tab_bounds = cx.debug_bounds("TAB-3").unwrap(); - let new_tab_button_bounds = cx.debug_bounds("ICON-Plus").unwrap(); - let scroll_bounds = tab_bar_scroll_handle.bounds(); - let scroll_offset = tab_bar_scroll_handle.offset(); - assert!(tab_bounds.right() <= scroll_bounds.right() + scroll_offset.x); - // -39.5 is the magic number for this setup - assert_eq!(scroll_offset.x, px(-39.5)); - assert!( - !tab_bounds.intersects(&new_tab_button_bounds), - "Tab should not overlap with the new tab button, if this is failing check if there's been a redesign!" - ); - } - - #[gpui::test] - async fn test_close_all_items_including_pinned(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()); - - let item_a = 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| { - let ix = pane.index_for_item_id(item_a.item_id()).unwrap(); - pane.pin_tab_at(ix, window, cx); - pane.close_all_items( - &CloseAllItems { - save_intent: None, - close_pinned: true, - }, - window, - cx, - ) - }) - .await - .unwrap(); - assert_item_labels(&pane, [], cx); - } - - #[gpui::test] - async fn test_close_pinned_tab_with_non_pinned_in_same_pane(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)); - - // Non-pinned tabs in same pane - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - add_labeled_item(&pane, "A", false, cx); - add_labeled_item(&pane, "B", false, cx); - add_labeled_item(&pane, "C", false, cx); - pane.update_in(cx, |pane, window, cx| { - pane.pin_tab_at(0, window, cx); - }); - set_labeled_items(&pane, ["A*", "B", "C"], cx); - pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - .unwrap(); - }); - // Non-pinned tab should be active - assert_item_labels(&pane, ["A!", "B*", "C"], cx); - } - - #[gpui::test] - async fn test_close_pinned_tab_with_non_pinned_in_different_pane(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)); - - // No non-pinned tabs in same pane, non-pinned tabs in another pane - let pane1 = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - let pane2 = workspace.update_in(cx, |workspace, window, cx| { - workspace.split_pane(pane1.clone(), SplitDirection::Right, window, cx) - }); - add_labeled_item(&pane1, "A", false, cx); - pane1.update_in(cx, |pane, window, cx| { - pane.pin_tab_at(0, window, cx); - }); - set_labeled_items(&pane1, ["A*"], cx); - add_labeled_item(&pane2, "B", false, cx); - set_labeled_items(&pane2, ["B"], cx); - pane1.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - .unwrap(); - }); - // Non-pinned tab of other pane should be active - assert_item_labels(&pane2, ["B*"], cx); - } - - #[gpui::test] - async fn ensure_item_closing_actions_do_not_panic_when_no_items_exist(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); - - pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - - pane.update_in(cx, |pane, window, cx| { - pane.close_other_items( - &CloseOtherItems { - save_intent: None, - close_pinned: false, - }, - None, - window, - cx, - ) - }) - .await - .unwrap(); - - pane.update_in(cx, |pane, window, cx| { - pane.close_all_items( - &CloseAllItems { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - - pane.update_in(cx, |pane, window, cx| { - pane.close_clean_items( - &CloseCleanItems { - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - - pane.update_in(cx, |pane, window, cx| { - pane.close_items_to_the_right_by_id( - None, - &CloseItemsToTheRight { - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .unwrap(); - - pane.update_in(cx, |pane, window, cx| { - pane.close_items_to_the_left_by_id( - None, - &CloseItemsToTheLeft { - close_pinned: false, - }, - window, - cx, - ) - }) - .await - .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); - cx.set_global(settings_store); - theme::init(LoadThemes::JustBase, cx); - }); - } - - fn set_max_tabs(cx: &mut TestAppContext, value: Option) { - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |settings| { - settings.workspace.max_tabs = value.map(|v| NonZero::new(v).unwrap()) - }); - }); - } - - fn add_labeled_item( - pane: &Entity, - label: &str, - is_dirty: bool, - cx: &mut VisualTestContext, - ) -> Box> { - pane.update_in(cx, |pane, window, cx| { - let labeled_item = - Box::new(cx.new(|cx| TestItem::new(cx).with_label(label).with_dirty(is_dirty))); - pane.add_item(labeled_item.clone(), false, false, None, window, cx); - labeled_item - }) - } - - fn set_labeled_items( - pane: &Entity, - labels: [&str; COUNT], - cx: &mut VisualTestContext, - ) -> [Box>; COUNT] { - pane.update_in(cx, |pane, window, cx| { - pane.items.clear(); - let mut active_item_index = 0; - - let mut index = 0; - let items = labels.map(|mut label| { - if label.ends_with('*') { - label = label.trim_end_matches('*'); - active_item_index = index; - } - - let labeled_item = Box::new(cx.new(|cx| TestItem::new(cx).with_label(label))); - pane.add_item(labeled_item.clone(), false, false, None, window, cx); - index += 1; - labeled_item - }); - - pane.activate_item(active_item_index, false, false, window, cx); - - items - }) - } - - // Assert the item label, with the active item label suffixed with a '*' - #[track_caller] - fn assert_item_labels( - pane: &Entity, - expected_states: [&str; COUNT], - cx: &mut VisualTestContext, - ) { - let actual_states = pane.update(cx, |pane, cx| { - pane.items - .iter() - .enumerate() - .map(|(ix, item)| { - let mut state = item - .to_any_view() - .downcast::() - .unwrap() - .read(cx) - .label - .clone(); - if ix == pane.active_item_index { - state.push('*'); - } - if item.is_dirty(cx) { - state.push('^'); - } - if pane.is_tab_pinned(ix) { - state.push('!'); - } - state - }) - .collect::>() - }); - assert_eq!( - actual_states, expected_states, - "pane items do not match expectation" - ); - } -} diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs deleted file mode 100644 index c9d9897713..0000000000 --- a/crates/workspace/src/pane_group.rs +++ /dev/null @@ -1,1456 +0,0 @@ -use crate::{ - AppState, CollaboratorId, FollowerState, Pane, Workspace, WorkspaceSettings, - pane_group::element::pane_axis, - workspace_settings::{PaneSplitDirectionHorizontal, PaneSplitDirectionVertical}, -}; -use anyhow::Result; -use call::{ActiveCall, ParticipantLocation}; -use collections::HashMap; -use gpui::{ - Along, AnyView, AnyWeakView, Axis, Bounds, Entity, Hsla, IntoElement, MouseButton, Pixels, - Point, StyleRefinement, WeakEntity, Window, point, size, -}; -use parking_lot::Mutex; -use project::Project; -use schemars::JsonSchema; -use serde::Deserialize; -use settings::Settings; -use std::sync::Arc; -use ui::prelude::*; - -pub const HANDLE_HITBOX_SIZE: f32 = 4.0; -const HORIZONTAL_MIN_SIZE: f32 = 80.; -const VERTICAL_MIN_SIZE: f32 = 100.; - -/// One or many panes, arranged in a horizontal or vertical axis due to a split. -/// Panes have all their tabs and capabilities preserved, and can be split again or resized. -/// Single-pane group is a regular pane. -#[derive(Clone)] -pub struct PaneGroup { - pub root: Member, -} - -pub struct PaneRenderResult { - pub element: gpui::AnyElement, - pub contains_active_pane: bool, -} - -impl PaneGroup { - pub fn with_root(root: Member) -> Self { - Self { root } - } - - pub fn new(pane: Entity) -> Self { - Self { - root: Member::Pane(pane), - } - } - - pub fn split( - &mut self, - old_pane: &Entity, - new_pane: &Entity, - direction: SplitDirection, - ) -> Result<()> { - match &mut self.root { - Member::Pane(pane) => { - if pane == old_pane { - self.root = Member::new_axis(old_pane.clone(), new_pane.clone(), direction); - Ok(()) - } else { - anyhow::bail!("Pane not found"); - } - } - Member::Axis(axis) => axis.split(old_pane, new_pane, direction), - } - } - - pub fn bounding_box_for_pane(&self, pane: &Entity) -> Option> { - match &self.root { - Member::Pane(_) => None, - Member::Axis(axis) => axis.bounding_box_for_pane(pane), - } - } - - pub fn pane_at_pixel_position(&self, coordinate: Point) -> Option<&Entity> { - match &self.root { - Member::Pane(pane) => Some(pane), - Member::Axis(axis) => axis.pane_at_pixel_position(coordinate), - } - } - - /// Moves active pane to span the entire border in the given direction, - /// similar to Vim ctrl+w shift-[hjkl] motion. - /// - /// Returns: - /// - Ok(true) if it found and moved a pane - /// - Ok(false) if it found but did not move the pane - /// - Err(_) if it did not find the pane - pub fn move_to_border( - &mut self, - active_pane: &Entity, - direction: SplitDirection, - ) -> Result { - if let Some(pane) = self.find_pane_at_border(direction) - && pane == active_pane - { - return Ok(false); - } - - if !self.remove(active_pane)? { - return Ok(false); - } - - if let Member::Axis(root) = &mut self.root - && direction.axis() == root.axis - { - let idx = if direction.increasing() { - root.members.len() - } else { - 0 - }; - root.insert_pane(idx, active_pane); - return Ok(true); - } - - let members = if direction.increasing() { - vec![self.root.clone(), Member::Pane(active_pane.clone())] - } else { - vec![Member::Pane(active_pane.clone()), self.root.clone()] - }; - self.root = Member::Axis(PaneAxis::new(direction.axis(), members)); - Ok(true) - } - - fn find_pane_at_border(&self, direction: SplitDirection) -> Option<&Entity> { - match &self.root { - Member::Pane(pane) => Some(pane), - Member::Axis(axis) => axis.find_pane_at_border(direction), - } - } - - /// Returns: - /// - Ok(true) if it found and removed a pane - /// - Ok(false) if it found but did not remove the pane - /// - Err(_) if it did not find the pane - pub fn remove(&mut self, pane: &Entity) -> Result { - match &mut self.root { - Member::Pane(_) => Ok(false), - Member::Axis(axis) => { - if let Some(last_pane) = axis.remove(pane)? { - self.root = last_pane; - } - Ok(true) - } - } - } - - pub fn resize( - &mut self, - pane: &Entity, - direction: Axis, - amount: Pixels, - bounds: &Bounds, - ) { - match &mut self.root { - Member::Pane(_) => {} - Member::Axis(axis) => { - let _ = axis.resize(pane, direction, amount, bounds); - } - }; - } - - pub fn reset_pane_sizes(&mut self) { - match &mut self.root { - Member::Pane(_) => {} - Member::Axis(axis) => { - let _ = axis.reset_pane_sizes(); - } - }; - } - - pub fn swap(&mut self, from: &Entity, to: &Entity) { - match &mut self.root { - Member::Pane(_) => {} - Member::Axis(axis) => axis.swap(from, to), - }; - } - - pub fn render( - &self, - zoomed: Option<&AnyWeakView>, - render_cx: &dyn PaneLeaderDecorator, - window: &mut Window, - cx: &mut App, - ) -> impl IntoElement { - self.root.render(0, zoomed, render_cx, window, cx).element - } - - pub fn panes(&self) -> Vec<&Entity> { - let mut panes = Vec::new(); - self.root.collect_panes(&mut panes); - panes - } - - pub fn first_pane(&self) -> Entity { - self.root.first_pane() - } - - pub fn last_pane(&self) -> Entity { - self.root.last_pane() - } - - pub fn find_pane_in_direction( - &mut self, - active_pane: &Entity, - direction: SplitDirection, - cx: &App, - ) -> Option<&Entity> { - let bounding_box = self.bounding_box_for_pane(active_pane)?; - let cursor = active_pane.read(cx).pixel_position_of_cursor(cx); - let center = match cursor { - Some(cursor) if bounding_box.contains(&cursor) => cursor, - _ => bounding_box.center(), - }; - - let distance_to_next = crate::HANDLE_HITBOX_SIZE; - - let target = match direction { - SplitDirection::Left => { - Point::new(bounding_box.left() - distance_to_next.into(), center.y) - } - SplitDirection::Right => { - Point::new(bounding_box.right() + distance_to_next.into(), center.y) - } - SplitDirection::Up => { - Point::new(center.x, bounding_box.top() - distance_to_next.into()) - } - SplitDirection::Down => { - Point::new(center.x, bounding_box.bottom() + distance_to_next.into()) - } - }; - self.pane_at_pixel_position(target) - } - - pub fn invert_axies(&mut self) { - self.root.invert_pane_axies(); - } -} - -#[derive(Debug, Clone)] -pub enum Member { - Axis(PaneAxis), - Pane(Entity), -} - -#[derive(Clone, Copy)] -pub struct PaneRenderContext<'a> { - pub project: &'a Entity, - pub follower_states: &'a HashMap, - pub active_call: Option<&'a Entity>, - pub active_pane: &'a Entity, - pub app_state: &'a Arc, - pub workspace: &'a WeakEntity, -} - -#[derive(Default)] -pub struct LeaderDecoration { - border: Option, - status_box: Option, -} - -pub trait PaneLeaderDecorator { - fn decorate(&self, pane: &Entity, cx: &App) -> LeaderDecoration; - fn active_pane(&self) -> &Entity; - fn workspace(&self) -> &WeakEntity; -} - -pub struct ActivePaneDecorator<'a> { - active_pane: &'a Entity, - workspace: &'a WeakEntity, -} - -impl<'a> ActivePaneDecorator<'a> { - pub fn new(active_pane: &'a Entity, workspace: &'a WeakEntity) -> Self { - Self { - active_pane, - workspace, - } - } -} - -impl PaneLeaderDecorator for ActivePaneDecorator<'_> { - fn decorate(&self, _: &Entity, _: &App) -> LeaderDecoration { - LeaderDecoration::default() - } - fn active_pane(&self) -> &Entity { - self.active_pane - } - - fn workspace(&self) -> &WeakEntity { - self.workspace - } -} - -impl PaneLeaderDecorator for PaneRenderContext<'_> { - fn decorate(&self, pane: &Entity, cx: &App) -> LeaderDecoration { - let follower_state = self.follower_states.iter().find_map(|(leader_id, state)| { - if state.center_pane == *pane { - Some((*leader_id, state)) - } else { - None - } - }); - let Some((leader_id, follower_state)) = follower_state else { - return LeaderDecoration::default(); - }; - - let mut leader_color; - let status_box; - match leader_id { - CollaboratorId::PeerId(peer_id) => { - let Some(leader) = self.active_call.as_ref().and_then(|call| { - let room = call.read(cx).room()?.read(cx); - room.remote_participant_for_peer_id(peer_id) - }) else { - return LeaderDecoration::default(); - }; - - let is_in_unshared_view = follower_state.active_view_id.is_some_and(|view_id| { - !follower_state - .items_by_leader_view_id - .contains_key(&view_id) - }); - - let mut leader_join_data = None; - let leader_status_box = match leader.location { - ParticipantLocation::SharedProject { - project_id: leader_project_id, - } => { - if Some(leader_project_id) == self.project.read(cx).remote_id() { - is_in_unshared_view.then(|| { - Label::new(format!( - "{} is in an unshared pane", - leader.user.github_login - )) - }) - } else { - leader_join_data = Some((leader_project_id, leader.user.id)); - Some(Label::new(format!( - "Follow {} to their active project", - leader.user.github_login, - ))) - } - } - ParticipantLocation::UnsharedProject => Some(Label::new(format!( - "{} is viewing an unshared Zed project", - leader.user.github_login - ))), - ParticipantLocation::External => Some(Label::new(format!( - "{} is viewing a window outside of Zed", - leader.user.github_login - ))), - }; - status_box = leader_status_box.map(|status| { - div() - .absolute() - .w_96() - .bottom_3() - .right_3() - .elevation_2(cx) - .p_1() - .child(status) - .when_some( - leader_join_data, - |this, (leader_project_id, leader_user_id)| { - let app_state = self.app_state.clone(); - this.cursor_pointer().on_mouse_down( - MouseButton::Left, - move |_, _, cx| { - crate::join_in_room_project( - leader_project_id, - leader_user_id, - app_state.clone(), - cx, - ) - .detach_and_log_err(cx); - }, - ) - }, - ) - .into_any_element() - }); - leader_color = cx - .theme() - .players() - .color_for_participant(leader.participant_index.0) - .cursor; - } - CollaboratorId::Agent => { - status_box = None; - leader_color = cx.theme().players().agent().cursor; - } - } - - let is_in_panel = follower_state.dock_pane.is_some(); - if is_in_panel { - leader_color.fade_out(0.75); - } else { - leader_color.fade_out(0.3); - } - - LeaderDecoration { - status_box, - border: Some(leader_color), - } - } - - fn active_pane(&self) -> &Entity { - self.active_pane - } - - fn workspace(&self) -> &WeakEntity { - self.workspace - } -} - -impl Member { - fn new_axis(old_pane: Entity, new_pane: Entity, direction: SplitDirection) -> Self { - use Axis::*; - use SplitDirection::*; - - let axis = match direction { - Up | Down => Vertical, - Left | Right => Horizontal, - }; - - let members = match direction { - Up | Left => vec![Member::Pane(new_pane), Member::Pane(old_pane)], - Down | Right => vec![Member::Pane(old_pane), Member::Pane(new_pane)], - }; - - Member::Axis(PaneAxis::new(axis, members)) - } - - fn first_pane(&self) -> Entity { - match self { - Member::Axis(axis) => axis.members[0].first_pane(), - Member::Pane(pane) => pane.clone(), - } - } - - fn last_pane(&self) -> Entity { - match self { - Member::Axis(axis) => axis.members.last().unwrap().last_pane(), - Member::Pane(pane) => pane.clone(), - } - } - - pub fn render( - &self, - basis: usize, - zoomed: Option<&AnyWeakView>, - render_cx: &dyn PaneLeaderDecorator, - window: &mut Window, - cx: &mut App, - ) -> PaneRenderResult { - match self { - Member::Pane(pane) => { - if zoomed == Some(&pane.downgrade().into()) { - return PaneRenderResult { - element: div().into_any(), - contains_active_pane: false, - }; - } - - let decoration = render_cx.decorate(pane, cx); - let is_active = pane == render_cx.active_pane(); - - PaneRenderResult { - element: div() - .relative() - .flex_1() - .size_full() - .child( - AnyView::from(pane.clone()) - .cached(StyleRefinement::default().v_flex().size_full()), - ) - .when_some(decoration.border, |this, color| { - this.child( - div() - .absolute() - .size_full() - .left_0() - .top_0() - .border_2() - .border_color(color), - ) - }) - .children(decoration.status_box) - .into_any(), - contains_active_pane: is_active, - } - } - Member::Axis(axis) => axis.render(basis + 1, zoomed, render_cx, window, cx), - } - } - - fn collect_panes<'a>(&'a self, panes: &mut Vec<&'a Entity>) { - match self { - Member::Axis(axis) => { - for member in &axis.members { - member.collect_panes(panes); - } - } - Member::Pane(pane) => panes.push(pane), - } - } - - fn invert_pane_axies(&mut self) { - match self { - Self::Axis(axis) => { - axis.axis = axis.axis.invert(); - for member in axis.members.iter_mut() { - member.invert_pane_axies(); - } - } - Self::Pane(_) => {} - } - } -} - -#[derive(Debug, Clone)] -pub struct PaneAxis { - pub axis: Axis, - pub members: Vec, - pub flexes: Arc>>, - pub bounding_boxes: Arc>>>>, -} - -impl PaneAxis { - pub fn new(axis: Axis, members: Vec) -> Self { - let flexes = Arc::new(Mutex::new(vec![1.; members.len()])); - let bounding_boxes = Arc::new(Mutex::new(vec![None; members.len()])); - Self { - axis, - members, - flexes, - bounding_boxes, - } - } - - pub fn load(axis: Axis, members: Vec, flexes: Option>) -> Self { - let mut flexes = flexes.unwrap_or_else(|| vec![1.; members.len()]); - if flexes.len() != members.len() - || (flexes.iter().copied().sum::() - flexes.len() as f32).abs() >= 0.001 - { - flexes = vec![1.; members.len()]; - } - - let flexes = Arc::new(Mutex::new(flexes)); - let bounding_boxes = Arc::new(Mutex::new(vec![None; members.len()])); - Self { - axis, - members, - flexes, - bounding_boxes, - } - } - - fn split( - &mut self, - old_pane: &Entity, - new_pane: &Entity, - direction: SplitDirection, - ) -> Result<()> { - for (mut idx, member) in self.members.iter_mut().enumerate() { - match member { - Member::Axis(axis) => { - if axis.split(old_pane, new_pane, direction).is_ok() { - return Ok(()); - } - } - Member::Pane(pane) => { - if pane == old_pane { - if direction.axis() == self.axis { - if direction.increasing() { - idx += 1; - } - self.insert_pane(idx, new_pane); - } else { - *member = - Member::new_axis(old_pane.clone(), new_pane.clone(), direction); - } - return Ok(()); - } - } - } - } - anyhow::bail!("Pane not found"); - } - - fn insert_pane(&mut self, idx: usize, new_pane: &Entity) { - self.members.insert(idx, Member::Pane(new_pane.clone())); - *self.flexes.lock() = vec![1.; self.members.len()]; - } - - fn find_pane_at_border(&self, direction: SplitDirection) -> Option<&Entity> { - if self.axis != direction.axis() { - return None; - } - let member = if direction.increasing() { - self.members.last() - } else { - self.members.first() - }; - member.and_then(|e| match e { - Member::Pane(pane) => Some(pane), - Member::Axis(_) => None, - }) - } - - fn remove(&mut self, pane_to_remove: &Entity) -> Result> { - let mut found_pane = false; - let mut remove_member = None; - for (idx, member) in self.members.iter_mut().enumerate() { - match member { - Member::Axis(axis) => { - if let Ok(last_pane) = axis.remove(pane_to_remove) { - if let Some(last_pane) = last_pane { - *member = last_pane; - } - found_pane = true; - break; - } - } - Member::Pane(pane) => { - if pane == pane_to_remove { - found_pane = true; - remove_member = Some(idx); - break; - } - } - } - } - - if found_pane { - if let Some(idx) = remove_member { - self.members.remove(idx); - *self.flexes.lock() = vec![1.; self.members.len()]; - } - - if self.members.len() == 1 { - let result = self.members.pop(); - *self.flexes.lock() = vec![1.; self.members.len()]; - Ok(result) - } else { - Ok(None) - } - } else { - anyhow::bail!("Pane not found"); - } - } - - fn reset_pane_sizes(&self) { - *self.flexes.lock() = vec![1.; self.members.len()]; - for member in self.members.iter() { - if let Member::Axis(axis) = member { - axis.reset_pane_sizes(); - } - } - } - - fn resize( - &mut self, - pane: &Entity, - axis: Axis, - amount: Pixels, - bounds: &Bounds, - ) -> Option { - let container_size = self - .bounding_boxes - .lock() - .iter() - .filter_map(|e| *e) - .reduce(|acc, e| acc.union(&e)) - .unwrap_or(*bounds) - .size; - - let found_pane = self - .members - .iter() - .any(|member| matches!(member, Member::Pane(p) if p == pane)); - - if found_pane && self.axis != axis { - return Some(false); // pane found but this is not the correct axis direction - } - let mut found_axis_index: Option = None; - if !found_pane { - for (i, pa) in self.members.iter_mut().enumerate() { - if let Member::Axis(pa) = pa - && let Some(done) = pa.resize(pane, axis, amount, bounds) - { - if done { - return Some(true); // pane found and operations already done - } else if self.axis != axis { - return Some(false); // pane found but this is not the correct axis direction - } else { - found_axis_index = Some(i); // pane found and this is correct direction - } - } - } - found_axis_index?; // no pane found - } - - let min_size = match axis { - Axis::Horizontal => px(HORIZONTAL_MIN_SIZE), - Axis::Vertical => px(VERTICAL_MIN_SIZE), - }; - let mut flexes = self.flexes.lock(); - - let ix = if found_pane { - self.members.iter().position(|m| { - if let Member::Pane(p) = m { - p == pane - } else { - false - } - }) - } else { - found_axis_index - }; - - if ix.is_none() { - return Some(true); - } - - let ix = ix.unwrap_or(0); - - let size = move |ix, flexes: &[f32]| { - container_size.along(axis) * (flexes[ix] / flexes.len() as f32) - }; - - // Don't allow resizing to less than the minimum size, if elements are already too small - if min_size - px(1.) > size(ix, flexes.as_slice()) { - return Some(true); - } - - let flex_changes = |pixel_dx, target_ix, next: isize, flexes: &[f32]| { - let flex_change = flexes.len() as f32 * pixel_dx / container_size.along(axis); - let current_target_flex = flexes[target_ix] + flex_change; - let next_target_flex = flexes[(target_ix as isize + next) as usize] - flex_change; - (current_target_flex, next_target_flex) - }; - - let apply_changes = - |current_ix: usize, proposed_current_pixel_change: Pixels, flexes: &mut [f32]| { - let next_target_size = Pixels::max( - size(current_ix + 1, flexes) - proposed_current_pixel_change, - min_size, - ); - let current_target_size = Pixels::max( - size(current_ix, flexes) + size(current_ix + 1, flexes) - next_target_size, - min_size, - ); - - let current_pixel_change = current_target_size - size(current_ix, flexes); - - let (current_target_flex, next_target_flex) = - flex_changes(current_pixel_change, current_ix, 1, flexes); - - flexes[current_ix] = current_target_flex; - flexes[current_ix + 1] = next_target_flex; - }; - - if ix + 1 == flexes.len() { - apply_changes(ix - 1, -1.0 * amount, flexes.as_mut_slice()); - } else { - apply_changes(ix, amount, flexes.as_mut_slice()); - } - Some(true) - } - - fn swap(&mut self, from: &Entity, to: &Entity) { - for member in self.members.iter_mut() { - match member { - Member::Axis(axis) => axis.swap(from, to), - Member::Pane(pane) => { - if pane == from { - *member = Member::Pane(to.clone()); - } else if pane == to { - *member = Member::Pane(from.clone()) - } - } - } - } - } - - fn bounding_box_for_pane(&self, pane: &Entity) -> Option> { - debug_assert!(self.members.len() == self.bounding_boxes.lock().len()); - - for (idx, member) in self.members.iter().enumerate() { - match member { - Member::Pane(found) => { - if pane == found { - return self.bounding_boxes.lock()[idx]; - } - } - Member::Axis(axis) => { - if let Some(rect) = axis.bounding_box_for_pane(pane) { - return Some(rect); - } - } - } - } - None - } - - fn pane_at_pixel_position(&self, coordinate: Point) -> Option<&Entity> { - debug_assert!(self.members.len() == self.bounding_boxes.lock().len()); - - let bounding_boxes = self.bounding_boxes.lock(); - - for (idx, member) in self.members.iter().enumerate() { - if let Some(coordinates) = bounding_boxes[idx] - && coordinates.contains(&coordinate) - { - return match member { - Member::Pane(found) => Some(found), - Member::Axis(axis) => axis.pane_at_pixel_position(coordinate), - }; - } - } - None - } - - fn render( - &self, - basis: usize, - zoomed: Option<&AnyWeakView>, - render_cx: &dyn PaneLeaderDecorator, - window: &mut Window, - cx: &mut App, - ) -> PaneRenderResult { - debug_assert!(self.members.len() == self.flexes.lock().len()); - let mut active_pane_ix = None; - let mut contains_active_pane = false; - let mut is_leaf_pane = vec![false; self.members.len()]; - - let rendered_children = self - .members - .iter() - .enumerate() - .map(|(ix, member)| { - match member { - Member::Pane(pane) => { - is_leaf_pane[ix] = true; - if pane == render_cx.active_pane() { - active_pane_ix = Some(ix); - contains_active_pane = true; - } - } - Member::Axis(_) => { - is_leaf_pane[ix] = false; - } - } - - let result = member.render((basis + ix) * 10, zoomed, render_cx, window, cx); - if result.contains_active_pane { - contains_active_pane = true; - } - result.element.into_any_element() - }) - .collect::>(); - - let element = pane_axis( - self.axis, - basis, - self.flexes.clone(), - self.bounding_boxes.clone(), - render_cx.workspace().clone(), - ) - .with_is_leaf_pane_mask(is_leaf_pane) - .children(rendered_children) - .with_active_pane(active_pane_ix) - .into_any_element(); - - PaneRenderResult { - element, - contains_active_pane, - } - } -} - -#[derive(Clone, Copy, Debug, Deserialize, PartialEq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum SplitDirection { - Up, - Down, - Left, - Right, -} - -impl std::fmt::Display for SplitDirection { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - SplitDirection::Up => write!(f, "up"), - SplitDirection::Down => write!(f, "down"), - SplitDirection::Left => write!(f, "left"), - SplitDirection::Right => write!(f, "right"), - } - } -} - -impl SplitDirection { - pub fn all() -> [Self; 4] { - [Self::Up, Self::Down, Self::Left, Self::Right] - } - - pub fn vertical(cx: &mut App) -> Self { - match WorkspaceSettings::get_global(cx).pane_split_direction_vertical { - PaneSplitDirectionVertical::Left => SplitDirection::Left, - PaneSplitDirectionVertical::Right => SplitDirection::Right, - } - } - - pub fn horizontal(cx: &mut App) -> Self { - match WorkspaceSettings::get_global(cx).pane_split_direction_horizontal { - PaneSplitDirectionHorizontal::Down => SplitDirection::Down, - PaneSplitDirectionHorizontal::Up => SplitDirection::Up, - } - } - - pub fn edge(&self, rect: Bounds) -> Pixels { - match self { - Self::Up => rect.origin.y, - Self::Down => rect.bottom_left().y, - Self::Left => rect.bottom_left().x, - Self::Right => rect.bottom_right().x, - } - } - - pub fn along_edge(&self, bounds: Bounds, length: Pixels) -> Bounds { - match self { - Self::Up => Bounds { - origin: bounds.origin, - size: size(bounds.size.width, length), - }, - Self::Down => Bounds { - origin: point(bounds.bottom_left().x, bounds.bottom_left().y - length), - size: size(bounds.size.width, length), - }, - Self::Left => Bounds { - origin: bounds.origin, - size: size(length, bounds.size.height), - }, - Self::Right => Bounds { - origin: point(bounds.bottom_right().x - length, bounds.bottom_left().y), - size: size(length, bounds.size.height), - }, - } - } - - pub fn axis(&self) -> Axis { - match self { - Self::Up | Self::Down => Axis::Vertical, - Self::Left | Self::Right => Axis::Horizontal, - } - } - - pub fn increasing(&self) -> bool { - match self { - Self::Left | Self::Up => false, - Self::Down | Self::Right => true, - } - } - - pub fn opposite(&self) -> SplitDirection { - match self { - Self::Down => Self::Up, - Self::Up => Self::Down, - Self::Left => Self::Right, - Self::Right => Self::Left, - } - } -} - -mod element { - use std::mem; - use std::{cell::RefCell, iter, rc::Rc, sync::Arc}; - - use gpui::{ - Along, AnyElement, App, Axis, BorderStyle, Bounds, Element, GlobalElementId, - HitboxBehavior, IntoElement, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, - Pixels, Point, Size, Style, WeakEntity, Window, px, relative, size, - }; - use gpui::{CursorStyle, Hitbox}; - use parking_lot::Mutex; - use settings::Settings; - use smallvec::SmallVec; - use ui::prelude::*; - use util::ResultExt; - - use crate::Workspace; - - use crate::WorkspaceSettings; - - use super::{HANDLE_HITBOX_SIZE, HORIZONTAL_MIN_SIZE, VERTICAL_MIN_SIZE}; - - const DIVIDER_SIZE: f32 = 1.0; - - pub(super) fn pane_axis( - axis: Axis, - basis: usize, - flexes: Arc>>, - bounding_boxes: Arc>>>>, - workspace: WeakEntity, - ) -> PaneAxisElement { - PaneAxisElement { - axis, - basis, - flexes, - bounding_boxes, - children: SmallVec::new(), - active_pane_ix: None, - workspace, - is_leaf_pane_mask: Vec::new(), - } - } - - pub struct PaneAxisElement { - axis: Axis, - basis: usize, - /// Equivalent to ColumnWidths (but in terms of flexes instead of percentages) - /// For example, flexes "1.33, 1, 1", instead of "40%, 30%, 30%" - flexes: Arc>>, - bounding_boxes: Arc>>>>, - children: SmallVec<[AnyElement; 2]>, - active_pane_ix: Option, - workspace: WeakEntity, - // Track which children are leaf panes (Member::Pane) vs axes (Member::Axis) - is_leaf_pane_mask: Vec, - } - - pub struct PaneAxisLayout { - dragged_handle: Rc>>, - children: Vec, - } - - struct PaneAxisChildLayout { - bounds: Bounds, - element: AnyElement, - handle: Option, - is_leaf_pane: bool, - } - - struct PaneAxisHandleLayout { - hitbox: Hitbox, - divider_bounds: Bounds, - } - - impl PaneAxisElement { - pub fn with_active_pane(mut self, active_pane_ix: Option) -> Self { - self.active_pane_ix = active_pane_ix; - self - } - - pub fn with_is_leaf_pane_mask(mut self, mask: Vec) -> Self { - self.is_leaf_pane_mask = mask; - self - } - - fn compute_resize( - flexes: &Arc>>, - e: &MouseMoveEvent, - ix: usize, - axis: Axis, - child_start: Point, - container_size: Size, - workspace: WeakEntity, - window: &mut Window, - cx: &mut App, - ) { - let min_size = match axis { - Axis::Horizontal => px(HORIZONTAL_MIN_SIZE), - Axis::Vertical => px(VERTICAL_MIN_SIZE), - }; - let mut flexes = flexes.lock(); - debug_assert!(flex_values_in_bounds(flexes.as_slice())); - - // Math to convert a flex value to a pixel value - let size = move |ix, flexes: &[f32]| { - container_size.along(axis) * (flexes[ix] / flexes.len() as f32) - }; - - // Don't allow resizing to less than the minimum size, if elements are already too small - if min_size - px(1.) > size(ix, flexes.as_slice()) { - return; - } - - // This is basically a "bucket" of pixel changes that need to be applied in response to this - // mouse event. Probably a small, fractional number like 0.5 or 1.5 pixels - let mut proposed_current_pixel_change = - (e.position - child_start).along(axis) - size(ix, flexes.as_slice()); - - // This takes a pixel change, and computes the flex changes that correspond to this pixel change - // as well as the next one, for some reason - let flex_changes = |pixel_dx, target_ix, next: isize, flexes: &[f32]| { - let flex_change = pixel_dx / container_size.along(axis); - let current_target_flex = flexes[target_ix] + flex_change; - let next_target_flex = flexes[(target_ix as isize + next) as usize] - flex_change; - (current_target_flex, next_target_flex) - }; - - // Generate the list of flex successors, from the current index. - // If you're dragging column 3 forward, out of 6 columns, then this code will produce [4, 5, 6] - // If you're dragging column 3 backward, out of 6 columns, then this code will produce [2, 1, 0] - let mut successors = iter::from_fn({ - let forward = proposed_current_pixel_change > px(0.); - let mut ix_offset = 0; - let len = flexes.len(); - move || { - let result = if forward { - (ix + 1 + ix_offset < len).then(|| ix + ix_offset) - } else { - (ix as isize - ix_offset as isize >= 0).then(|| ix - ix_offset) - }; - - ix_offset += 1; - - result - } - }); - - // Now actually loop over these, and empty our bucket of pixel changes - while proposed_current_pixel_change.abs() > px(0.) { - let Some(current_ix) = successors.next() else { - break; - }; - - let next_target_size = Pixels::max( - size(current_ix + 1, flexes.as_slice()) - proposed_current_pixel_change, - min_size, - ); - - let current_target_size = Pixels::max( - size(current_ix, flexes.as_slice()) + size(current_ix + 1, flexes.as_slice()) - - next_target_size, - min_size, - ); - - let current_pixel_change = - current_target_size - size(current_ix, flexes.as_slice()); - - let (current_target_flex, next_target_flex) = - flex_changes(current_pixel_change, current_ix, 1, flexes.as_slice()); - - flexes[current_ix] = current_target_flex; - flexes[current_ix + 1] = next_target_flex; - - proposed_current_pixel_change -= current_pixel_change; - } - - workspace - .update(cx, |this, cx| this.serialize_workspace(window, cx)) - .log_err(); - cx.stop_propagation(); - window.refresh(); - } - - fn layout_handle( - axis: Axis, - pane_bounds: Bounds, - window: &mut Window, - _cx: &mut App, - ) -> PaneAxisHandleLayout { - let handle_bounds = Bounds { - origin: pane_bounds.origin.apply_along(axis, |origin| { - origin + pane_bounds.size.along(axis) - px(HANDLE_HITBOX_SIZE / 2.) - }), - size: pane_bounds - .size - .apply_along(axis, |_| px(HANDLE_HITBOX_SIZE)), - }; - let divider_bounds = Bounds { - origin: pane_bounds - .origin - .apply_along(axis, |origin| origin + pane_bounds.size.along(axis)), - size: pane_bounds.size.apply_along(axis, |_| px(DIVIDER_SIZE)), - }; - - PaneAxisHandleLayout { - hitbox: window.insert_hitbox(handle_bounds, HitboxBehavior::BlockMouse), - divider_bounds, - } - } - } - - impl IntoElement for PaneAxisElement { - type Element = Self; - - fn into_element(self) -> Self::Element { - self - } - } - - impl Element for PaneAxisElement { - type RequestLayoutState = (); - type PrepaintState = PaneAxisLayout; - - fn id(&self) -> Option { - Some(self.basis.into()) - } - - fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { - None - } - - fn request_layout( - &mut self, - _global_id: Option<&GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - window: &mut Window, - cx: &mut App, - ) -> (gpui::LayoutId, Self::RequestLayoutState) { - let style = Style { - flex_grow: 1., - flex_shrink: 1., - flex_basis: relative(0.).into(), - size: size(relative(1.).into(), relative(1.).into()), - ..Style::default() - }; - (window.request_layout(style, None, cx), ()) - } - - fn prepaint( - &mut self, - global_id: Option<&GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - bounds: Bounds, - _state: &mut Self::RequestLayoutState, - window: &mut Window, - cx: &mut App, - ) -> PaneAxisLayout { - let dragged_handle = window.with_element_state::>>, _>( - global_id.unwrap(), - |state, _cx| { - let state = state.unwrap_or_else(|| Rc::new(RefCell::new(None))); - (state.clone(), state) - }, - ); - let flexes = self.flexes.lock().clone(); - let len = self.children.len(); - debug_assert!(flexes.len() == len); - debug_assert!(flex_values_in_bounds(flexes.as_slice())); - - let total_flex = len as f32; - - let mut origin = bounds.origin; - let space_per_flex = bounds.size.along(self.axis) / total_flex; - - let mut bounding_boxes = self.bounding_boxes.lock(); - bounding_boxes.clear(); - - let mut layout = PaneAxisLayout { - dragged_handle, - children: Vec::new(), - }; - for (ix, mut child) in mem::take(&mut self.children).into_iter().enumerate() { - let child_flex = flexes[ix]; - - let child_size = bounds - .size - .apply_along(self.axis, |_| space_per_flex * child_flex) - .map(|d| d.round()); - - let child_bounds = Bounds { - origin, - size: child_size, - }; - - bounding_boxes.push(Some(child_bounds)); - child.layout_as_root(child_size.into(), window, cx); - child.prepaint_at(origin, window, cx); - - origin = origin.apply_along(self.axis, |val| val + child_size.along(self.axis)); - - let is_leaf_pane = self.is_leaf_pane_mask.get(ix).copied().unwrap_or(true); - - layout.children.push(PaneAxisChildLayout { - bounds: child_bounds, - element: child, - handle: None, - is_leaf_pane, - }) - } - - for (ix, child_layout) in layout.children.iter_mut().enumerate() { - if ix < len - 1 { - child_layout.handle = Some(Self::layout_handle( - self.axis, - child_layout.bounds, - window, - cx, - )); - } - } - - layout - } - - fn paint( - &mut self, - _id: Option<&GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - bounds: gpui::Bounds, - _: &mut Self::RequestLayoutState, - layout: &mut Self::PrepaintState, - window: &mut Window, - cx: &mut App, - ) { - for child in &mut layout.children { - child.element.paint(window, cx); - } - - let overlay_opacity = WorkspaceSettings::get(None, cx) - .active_pane_modifiers - .inactive_opacity - .map(|val| val.0.clamp(0.0, 1.0)) - .and_then(|val| (val <= 1.).then_some(val)); - - let mut overlay_background = cx.theme().colors().editor_background; - if let Some(opacity) = overlay_opacity { - overlay_background.fade_out(opacity); - } - - let overlay_border = WorkspaceSettings::get(None, cx) - .active_pane_modifiers - .border_size - .and_then(|val| (val >= 0.).then_some(val)); - - for (ix, child) in &mut layout.children.iter_mut().enumerate() { - if overlay_opacity.is_some() || overlay_border.is_some() { - // the overlay has to be painted in origin+1px with size width-1px - // in order to accommodate the divider between panels - let overlay_bounds = Bounds { - origin: child - .bounds - .origin - .apply_along(Axis::Horizontal, |val| val + px(1.)), - size: child - .bounds - .size - .apply_along(Axis::Horizontal, |val| val - px(1.)), - }; - - if overlay_opacity.is_some() - && child.is_leaf_pane - && self.active_pane_ix != Some(ix) - { - window.paint_quad(gpui::fill(overlay_bounds, overlay_background)); - } - - if let Some(border) = overlay_border - && self.active_pane_ix == Some(ix) - && child.is_leaf_pane - { - window.paint_quad(gpui::quad( - overlay_bounds, - 0., - gpui::transparent_black(), - border, - cx.theme().colors().border_selected, - BorderStyle::Solid, - )); - } - } - - if let Some(handle) = child.handle.as_mut() { - let cursor_style = match self.axis { - Axis::Vertical => CursorStyle::ResizeRow, - Axis::Horizontal => CursorStyle::ResizeColumn, - }; - - if layout - .dragged_handle - .borrow() - .is_some_and(|dragged_ix| dragged_ix == ix) - { - window.set_window_cursor_style(cursor_style); - } else { - window.set_cursor_style(cursor_style, &handle.hitbox); - } - - window.paint_quad(gpui::fill( - handle.divider_bounds, - cx.theme().colors().pane_group_border, - )); - - window.on_mouse_event({ - let dragged_handle = layout.dragged_handle.clone(); - let flexes = self.flexes.clone(); - let workspace = self.workspace.clone(); - let handle_hitbox = handle.hitbox.clone(); - move |e: &MouseDownEvent, phase, window, cx| { - if phase.bubble() && handle_hitbox.is_hovered(window) { - dragged_handle.replace(Some(ix)); - if e.click_count >= 2 { - let mut borrow = flexes.lock(); - *borrow = vec![1.; borrow.len()]; - workspace - .update(cx, |this, cx| this.serialize_workspace(window, cx)) - .log_err(); - - window.refresh(); - } - cx.stop_propagation(); - } - } - }); - window.on_mouse_event({ - let workspace = self.workspace.clone(); - let dragged_handle = layout.dragged_handle.clone(); - let flexes = self.flexes.clone(); - let child_bounds = child.bounds; - let axis = self.axis; - move |e: &MouseMoveEvent, phase, window, cx| { - let dragged_handle = dragged_handle.borrow(); - if phase.bubble() && *dragged_handle == Some(ix) { - Self::compute_resize( - &flexes, - e, - ix, - axis, - child_bounds.origin, - bounds.size, - workspace.clone(), - window, - cx, - ) - } - } - }); - } - } - - window.on_mouse_event({ - let dragged_handle = layout.dragged_handle.clone(); - move |_: &MouseUpEvent, phase, _window, _cx| { - if phase.bubble() { - dragged_handle.replace(None); - } - } - }); - } - } - - impl ParentElement for PaneAxisElement { - fn extend(&mut self, elements: impl IntoIterator) { - self.children.extend(elements) - } - } - - fn flex_values_in_bounds(flexes: &[f32]) -> bool { - (flexes.iter().copied().sum::() - flexes.len() as f32).abs() < 0.001 - } -} diff --git a/crates/workspace/src/path_list.rs b/crates/workspace/src/path_list.rs deleted file mode 100644 index 035f9e44fc..0000000000 --- a/crates/workspace/src/path_list.rs +++ /dev/null @@ -1,196 +0,0 @@ -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; - -use itertools::Itertools; -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. -/// -/// The paths can be retrieved in the original order using `ordered_paths()`. -#[derive(Default, PartialEq, Eq, Debug, Clone)] -pub struct PathList { - /// The paths, in lexicographic order. - paths: Arc<[PathBuf]>, - /// The order in which the paths were provided. - /// - /// See `ordered_paths()` for a way to get the paths in the original order. - 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::new(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() - } - - /// Get the paths in lexicographic order. - pub fn paths(&self) -> &[PathBuf] { - self.paths.as_ref() - } - - /// Get the order in which the paths were provided. - pub fn order(&self) -> &[usize] { - self.order.as_ref() - } - - /// Get the paths in the original order. - pub fn ordered_paths(&self) -> impl Iterator { - self.order - .iter() - .zip(self.paths.iter()) - .sorted_by_key(|(i, _)| **i) - .map(|(_, path)| path) - } - - 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(), "paths differ"); - assert_eq!(list1.order(), &[1, 0], "list1 order incorrect"); - assert_eq!(list2.order(), &[0, 1], "list2 order incorrect"); - - let list1_deserialized = PathList::deserialize(&list1.serialize()); - assert_eq!(list1_deserialized, list1, "list1 deserialization failed"); - - let list2_deserialized = PathList::deserialize(&list2.serialize()); - assert_eq!(list2_deserialized, list2, "list2 deserialization failed"); - - assert_eq!( - list1.ordered_paths().collect_array().unwrap(), - [&PathBuf::from("a/d"), &PathBuf::from("a/c")], - "list1 ordered paths incorrect" - ); - assert_eq!( - list2.ordered_paths().collect_array().unwrap(), - [&PathBuf::from("a/c"), &PathBuf::from("a/d")], - "list2 ordered paths incorrect" - ); - } - - #[test] - fn test_path_list_ordering() { - let list = PathList::new(&["b", "a", "c"]); - assert_eq!( - list.paths(), - &[PathBuf::from("a"), PathBuf::from("b"), PathBuf::from("c")] - ); - assert_eq!(list.order(), &[1, 0, 2]); - assert!(!list.is_lexicographically_ordered()); - - let serialized = list.serialize(); - let deserialized = PathList::deserialize(&serialized); - assert_eq!(deserialized, list); - - assert_eq!( - deserialized.ordered_paths().collect_array().unwrap(), - [ - &PathBuf::from("b"), - &PathBuf::from("a"), - &PathBuf::from("c") - ] - ); - - let list = PathList::new(&["b", "c", "a"]); - assert_eq!( - list.paths(), - &[PathBuf::from("a"), PathBuf::from("b"), PathBuf::from("c")] - ); - assert_eq!(list.order(), &[2, 0, 1]); - assert!(!list.is_lexicographically_ordered()); - - let serialized = list.serialize(); - let deserialized = PathList::deserialize(&serialized); - assert_eq!(deserialized, list); - - assert_eq!( - deserialized.ordered_paths().collect_array().unwrap(), - [ - &PathBuf::from("b"), - &PathBuf::from("c"), - &PathBuf::from("a"), - ] - ); - } -} diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs deleted file mode 100644 index f1835caf8d..0000000000 --- a/crates/workspace/src/persistence.rs +++ /dev/null @@ -1,3051 +0,0 @@ -pub mod model; - -use std::{ - borrow::Cow, - collections::BTreeMap, - path::{Path, PathBuf}, - str::FromStr, - sync::Arc, -}; - -use anyhow::{Context as _, Result, bail}; -use collections::{HashMap, IndexSet}; -use db::{ - query, - sqlez::{connection::Connection, domain::Domain}, - sqlez_macros::sql, -}; -use gpui::{Axis, Bounds, Task, WindowBounds, WindowId, point, size}; -use project::debugger::breakpoint_store::{BreakpointState, SourceBreakpoint}; - -use language::{LanguageName, Toolchain, ToolchainScope}; -use project::WorktreeId; -use remote::{ - DockerConnectionOptions, RemoteConnectionOptions, SshConnectionOptions, WslConnectionOptions, -}; -use sqlez::{ - bindable::{Bind, Column, StaticColumnCount}, - statement::Statement, - thread_safe_connection::ThreadSafeConnection, -}; - -use ui::{App, SharedString, px}; -use util::{ResultExt, maybe, rel_path::RelPath}; -use uuid::Uuid; - -use crate::{ - WorkspaceId, - path_list::{PathList, SerializedPathList}, - persistence::model::RemoteConnectionKind, -}; - -use model::{ - GroupId, ItemId, PaneId, RemoteConnectionId, SerializedItem, SerializedPane, - SerializedPaneGroup, SerializedWorkspace, -}; - -use self::model::{DockStructure, SerializedWorkspaceLocation}; - -#[derive(Copy, Clone, Debug, PartialEq)] -pub(crate) struct SerializedAxis(pub(crate) gpui::Axis); -impl sqlez::bindable::StaticColumnCount for SerializedAxis {} -impl sqlez::bindable::Bind for SerializedAxis { - fn bind( - &self, - statement: &sqlez::statement::Statement, - start_index: i32, - ) -> anyhow::Result { - match self.0 { - gpui::Axis::Horizontal => "Horizontal", - gpui::Axis::Vertical => "Vertical", - } - .bind(statement, start_index) - } -} - -impl sqlez::bindable::Column for SerializedAxis { - fn column( - statement: &mut sqlez::statement::Statement, - start_index: i32, - ) -> anyhow::Result<(Self, i32)> { - String::column(statement, start_index).and_then(|(axis_text, next_index)| { - Ok(( - match axis_text.as_str() { - "Horizontal" => Self(Axis::Horizontal), - "Vertical" => Self(Axis::Vertical), - _ => anyhow::bail!("Stored serialized item kind is incorrect"), - }, - next_index, - )) - }) - } -} - -#[derive(Copy, Clone, Debug, PartialEq, Default)] -pub(crate) struct SerializedWindowBounds(pub(crate) WindowBounds); - -impl StaticColumnCount for SerializedWindowBounds { - fn column_count() -> usize { - 5 - } -} - -impl Bind for SerializedWindowBounds { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - match self.0 { - WindowBounds::Windowed(bounds) => { - let next_index = statement.bind(&"Windowed", start_index)?; - statement.bind( - &( - SerializedPixels(bounds.origin.x), - SerializedPixels(bounds.origin.y), - SerializedPixels(bounds.size.width), - SerializedPixels(bounds.size.height), - ), - next_index, - ) - } - WindowBounds::Maximized(bounds) => { - let next_index = statement.bind(&"Maximized", start_index)?; - statement.bind( - &( - SerializedPixels(bounds.origin.x), - SerializedPixels(bounds.origin.y), - SerializedPixels(bounds.size.width), - SerializedPixels(bounds.size.height), - ), - next_index, - ) - } - WindowBounds::Fullscreen(bounds) => { - let next_index = statement.bind(&"FullScreen", start_index)?; - statement.bind( - &( - SerializedPixels(bounds.origin.x), - SerializedPixels(bounds.origin.y), - SerializedPixels(bounds.size.width), - SerializedPixels(bounds.size.height), - ), - next_index, - ) - } - } - } -} - -impl Column for SerializedWindowBounds { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let (window_state, next_index) = String::column(statement, start_index)?; - let ((x, y, width, height), _): ((i32, i32, i32, i32), _) = - Column::column(statement, next_index)?; - let bounds = Bounds { - origin: point(px(x as f32), px(y as f32)), - size: size(px(width as f32), px(height as f32)), - }; - - let status = match window_state.as_str() { - "Windowed" | "Fixed" => SerializedWindowBounds(WindowBounds::Windowed(bounds)), - "Maximized" => SerializedWindowBounds(WindowBounds::Maximized(bounds)), - "FullScreen" => SerializedWindowBounds(WindowBounds::Fullscreen(bounds)), - _ => bail!("Window State did not have a valid string"), - }; - - Ok((status, next_index + 4)) - } -} - -#[derive(Debug)] -pub struct Breakpoint { - pub position: u32, - pub message: Option>, - pub condition: Option>, - pub hit_condition: Option>, - pub state: BreakpointState, -} - -/// Wrapper for DB type of a breakpoint -struct BreakpointStateWrapper<'a>(Cow<'a, BreakpointState>); - -impl From for BreakpointStateWrapper<'static> { - fn from(kind: BreakpointState) -> Self { - BreakpointStateWrapper(Cow::Owned(kind)) - } -} - -impl StaticColumnCount for BreakpointStateWrapper<'_> { - fn column_count() -> usize { - 1 - } -} - -impl Bind for BreakpointStateWrapper<'_> { - fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result { - statement.bind(&self.0.to_int(), start_index) - } -} - -impl Column for BreakpointStateWrapper<'_> { - fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> { - let state = statement.column_int(start_index)?; - - match state { - 0 => Ok((BreakpointState::Enabled.into(), start_index + 1)), - 1 => Ok((BreakpointState::Disabled.into(), start_index + 1)), - _ => anyhow::bail!("Invalid BreakpointState discriminant {state}"), - } - } -} - -impl sqlez::bindable::StaticColumnCount for Breakpoint { - fn column_count() -> usize { - // Position, log message, condition message, and hit condition message - 4 + BreakpointStateWrapper::column_count() - } -} - -impl sqlez::bindable::Bind for Breakpoint { - fn bind( - &self, - statement: &sqlez::statement::Statement, - start_index: i32, - ) -> anyhow::Result { - let next_index = statement.bind(&self.position, start_index)?; - let next_index = statement.bind(&self.message, next_index)?; - let next_index = statement.bind(&self.condition, next_index)?; - let next_index = statement.bind(&self.hit_condition, next_index)?; - statement.bind( - &BreakpointStateWrapper(Cow::Borrowed(&self.state)), - next_index, - ) - } -} - -impl Column for Breakpoint { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let position = statement - .column_int(start_index) - .with_context(|| format!("Failed to read BreakPoint at index {start_index}"))? - as u32; - let (message, next_index) = Option::::column(statement, start_index + 1)?; - let (condition, next_index) = Option::::column(statement, next_index)?; - let (hit_condition, next_index) = Option::::column(statement, next_index)?; - let (state, next_index) = BreakpointStateWrapper::column(statement, next_index)?; - - Ok(( - Breakpoint { - position, - message: message.map(Arc::from), - condition: condition.map(Arc::from), - hit_condition: hit_condition.map(Arc::from), - state: state.0.into_owned(), - }, - next_index, - )) - } -} - -#[derive(Clone, Debug, PartialEq)] -struct SerializedPixels(gpui::Pixels); -impl sqlez::bindable::StaticColumnCount for SerializedPixels {} - -impl sqlez::bindable::Bind for SerializedPixels { - fn bind( - &self, - statement: &sqlez::statement::Statement, - start_index: i32, - ) -> anyhow::Result { - let this: i32 = u32::from(self.0) as _; - this.bind(statement, start_index) - } -} - -pub struct WorkspaceDb(ThreadSafeConnection); - -impl Domain for WorkspaceDb { - const NAME: &str = stringify!(WorkspaceDb); - - 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 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 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 breakpoints ( - workspace_id INTEGER NOT NULL, - path TEXT NOT NULL, - breakpoint_location INTEGER NOT NULL, - kind INTEGER NOT NULL, - log_message TEXT, - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - 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!( - CREATE TABLE remote_connections( - id INTEGER PRIMARY KEY, - kind TEXT NOT NULL, - host TEXT, - port INTEGER, - user TEXT, - distro TEXT - ); - - CREATE TABLE workspaces_2( - workspace_id INTEGER PRIMARY KEY, - paths TEXT, - paths_order TEXT, - remote_connection_id INTEGER REFERENCES remote_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 remote_connections - SELECT - id, - "ssh" as kind, - host, - port, - user, - NULL as distro - FROM ssh_connections; - - INSERT - INTO workspaces_2 - SELECT - workspace_id, - paths, - paths_order, - ssh_connection_id as remote_connection_id, - timestamp, - window_state, - window_x, - window_y, - window_width, - window_height, - display, - left_dock_visible, - left_dock_active_panel, - right_dock_visible, - right_dock_active_panel, - bottom_dock_visible, - bottom_dock_active_panel, - left_dock_zoom, - right_dock_zoom, - bottom_dock_zoom, - fullscreen, - centered_layout, - session_id, - window_id - FROM - workspaces; - - DROP TABLE workspaces; - ALTER TABLE workspaces_2 RENAME TO workspaces; - - CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(remote_connection_id, paths); - ), - sql!(CREATE TABLE user_toolchains ( - remote_connection_id INTEGER, - workspace_id INTEGER NOT NULL, - worktree_id INTEGER NOT NULL, - relative_worktree_path TEXT NOT NULL, - language_name TEXT NOT NULL, - name TEXT NOT NULL, - path TEXT NOT NULL, - raw_json TEXT NOT NULL, - - PRIMARY KEY (workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json) - ) STRICT;), - sql!( - DROP TABLE ssh_connections; - ), - sql!( - ALTER TABLE remote_connections ADD COLUMN name TEXT; - ALTER TABLE remote_connections ADD COLUMN container_id TEXT; - ), - ]; - - // 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 - /// passed roots is stored, returns none. - pub(crate) fn workspace_for_roots>( - &self, - worktree_roots: &[P], - ) -> Option { - self.workspace_for_roots_internal(worktree_roots, None) - } - - pub(crate) fn remote_workspace_for_roots>( - &self, - worktree_roots: &[P], - remote_project_id: RemoteConnectionId, - ) -> Option { - self.workspace_for_roots_internal(worktree_roots, Some(remote_project_id)) - } - - pub(crate) fn workspace_for_roots_internal>( - &self, - worktree_roots: &[P], - remote_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); - - // 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, - window_bounds, - display, - centered_layout, - docks, - window_id, - ): ( - WorkspaceId, - String, - String, - Option, - Option, - Option, - DockStructure, - Option, - ) = self - .select_row_bound(sql! { - SELECT - workspace_id, - paths, - paths_order, - 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 - paths IS ? AND - remote_connection_id IS ? - LIMIT 1 - }) - .and_then(|mut prepared_statement| { - (prepared_statement)(( - root_paths.serialize().paths, - remote_connection_id.map(|id| id.0 as i32), - )) - }) - .context("No workspaces found") - .warn_on_err() - .flatten()?; - - let paths = PathList::deserialize(&SerializedPathList { - paths, - order: paths_order, - }); - - let remote_connection_options = if let Some(remote_connection_id) = remote_connection_id { - self.remote_connection(remote_connection_id) - .context("Get remote connection") - .log_err() - } else { - None - }; - - Some(SerializedWorkspace { - id: workspace_id, - location: match remote_connection_options { - Some(options) => SerializedWorkspaceLocation::Remote(options), - None => SerializedWorkspaceLocation::Local, - }, - paths, - center_group: self - .get_center_pane_group(workspace_id) - .context("Getting center group") - .log_err()?, - window_bounds, - centered_layout: centered_layout.unwrap_or(false), - display, - docks, - session_id: None, - breakpoints: self.breakpoints(workspace_id), - window_id, - user_toolchains: self.user_toolchains(workspace_id, remote_connection_id), - }) - } - - fn breakpoints(&self, workspace_id: WorkspaceId) -> BTreeMap, Vec> { - let breakpoints: Result> = self - .select_bound(sql! { - SELECT path, breakpoint_location, log_message, condition, hit_condition, state - FROM breakpoints - WHERE workspace_id = ? - }) - .and_then(|mut prepared_statement| (prepared_statement)(workspace_id)); - - match breakpoints { - Ok(bp) => { - if bp.is_empty() { - log::debug!("Breakpoints are empty after querying database for them"); - } - - let mut map: BTreeMap, Vec> = Default::default(); - - for (path, breakpoint) in bp { - let path: Arc = path.into(); - map.entry(path.clone()).or_default().push(SourceBreakpoint { - row: breakpoint.position, - path, - message: breakpoint.message, - condition: breakpoint.condition, - hit_condition: breakpoint.hit_condition, - state: breakpoint.state, - }); - } - - for (path, bps) in map.iter() { - log::info!( - "Got {} breakpoints from database at path: {}", - bps.len(), - path.to_string_lossy() - ); - } - - map - } - Err(msg) => { - log::error!("Breakpoints query failed with msg: {msg}"); - Default::default() - } - } - } - - fn user_toolchains( - &self, - workspace_id: WorkspaceId, - remote_connection_id: Option, - ) -> BTreeMap> { - type RowKind = (WorkspaceId, u64, String, String, String, String, String); - - let toolchains: Vec = self - .select_bound(sql! { - SELECT workspace_id, worktree_id, relative_worktree_path, - language_name, name, path, raw_json - FROM user_toolchains WHERE remote_connection_id IS ?1 AND ( - workspace_id IN (0, ?2) - ) - }) - .and_then(|mut statement| { - (statement)((remote_connection_id.map(|id| id.0), workspace_id)) - }) - .unwrap_or_default(); - let mut ret = BTreeMap::<_, IndexSet<_>>::default(); - - for ( - _workspace_id, - worktree_id, - relative_worktree_path, - language_name, - name, - path, - raw_json, - ) in toolchains - { - // INTEGER's that are primary keys (like workspace ids, remote connection ids and such) start at 1, so we're safe to - let scope = if _workspace_id == WorkspaceId(0) { - debug_assert_eq!(worktree_id, u64::MAX); - debug_assert_eq!(relative_worktree_path, String::default()); - ToolchainScope::Global - } else { - debug_assert_eq!(workspace_id, _workspace_id); - debug_assert_eq!( - worktree_id == u64::MAX, - relative_worktree_path == String::default() - ); - - let Some(relative_path) = RelPath::unix(&relative_worktree_path).log_err() else { - continue; - }; - if worktree_id != u64::MAX && relative_worktree_path != String::default() { - ToolchainScope::Subproject( - WorktreeId::from_usize(worktree_id as usize), - relative_path.into(), - ) - } else { - ToolchainScope::Project - } - }; - let Ok(as_json) = serde_json::from_str(&raw_json) else { - continue; - }; - let toolchain = Toolchain { - name: SharedString::from(name), - path: SharedString::from(path), - language_name: LanguageName::from_proto(language_name), - as_json, - }; - ret.entry(scope).or_default().insert(toolchain); - } - - ret - } - - /// 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 remote_connection_id = match workspace.location.clone() { - SerializedWorkspaceLocation::Local => None, - SerializedWorkspaceLocation::Remote(connection_options) => { - Some(Self::get_or_create_remote_connection_internal( - conn, - connection_options - )?.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; - ) - )?(workspace.id).context("Clearing old breakpoints")?; - - for (path, breakpoints) in workspace.breakpoints { - for bp in breakpoints { - let state = BreakpointStateWrapper::from(bp.state); - match conn.exec_bound(sql!( - INSERT INTO breakpoints (workspace_id, path, breakpoint_location, log_message, condition, hit_condition, state) - VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7);))? - - (( - workspace.id, - path.as_ref(), - bp.row, - bp.message, - bp.condition, - bp.hit_condition, - state, - )) { - Ok(_) => { - log::debug!("Stored breakpoint at row: {} in path: {}", bp.row, path.to_string_lossy()) - } - Err(err) => { - log::error!("{err}"); - continue; - } - } - } - } - - conn.exec_bound( - sql!( - DELETE FROM user_toolchains WHERE workspace_id = ?1; - ) - )?(workspace.id).context("Clearing old user toolchains")?; - - for (scope, toolchains) in workspace.user_toolchains { - for toolchain in toolchains { - let query = sql!(INSERT OR REPLACE INTO user_toolchains(remote_connection_id, workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)); - let (workspace_id, worktree_id, relative_worktree_path) = match scope { - ToolchainScope::Subproject(worktree_id, ref path) => (Some(workspace.id), Some(worktree_id), Some(path.as_unix_str().to_owned())), - ToolchainScope::Project => (Some(workspace.id), None, None), - ToolchainScope::Global => (None, None, None), - }; - let args = (remote_connection_id, workspace_id.unwrap_or(WorkspaceId(0)), worktree_id.map_or(usize::MAX,|id| id.to_usize()), relative_worktree_path.unwrap_or_default(), - toolchain.language_name.as_ref().to_owned(), toolchain.name.to_string(), toolchain.path.to_string(), toolchain.as_json.to_string()); - if let Err(err) = conn.exec_bound(query)?(args) { - log::error!("{err}"); - continue; - } - } - } - - conn.exec_bound(sql!( - DELETE - FROM workspaces - WHERE - workspace_id != ?1 AND - paths IS ?2 AND - remote_connection_id IS ?3 - ))?(( - workspace.id, - paths.paths.clone(), - remote_connection_id, - )) - .context("clearing out old locations")?; - - // Upsert - let query = sql!( - INSERT INTO workspaces( - workspace_id, - paths, - paths_order, - remote_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, - remote_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(), - remote_connection_id, - workspace.docks, - workspace.session_id, - workspace.window_id, - ); - - prepared_query(args).context("Updating workspace")?; - - // Save center pane group - Self::save_pane_group(conn, workspace.id, &workspace.center_group, None) - .context("save pane group in save workspace")?; - - Ok(()) - }) - .log_err(); - }) - .await; - } - - pub(crate) async fn get_or_create_remote_connection( - &self, - options: RemoteConnectionOptions, - ) -> Result { - self.write(move |conn| Self::get_or_create_remote_connection_internal(conn, options)) - .await - } - - fn get_or_create_remote_connection_internal( - this: &Connection, - options: RemoteConnectionOptions, - ) -> Result { - let kind; - let mut user = None; - let mut host = None; - let mut port = None; - let mut distro = None; - let mut name = None; - let mut container_id = None; - match options { - RemoteConnectionOptions::Ssh(options) => { - kind = RemoteConnectionKind::Ssh; - host = Some(options.host); - port = options.port; - user = options.username; - } - RemoteConnectionOptions::Wsl(options) => { - kind = RemoteConnectionKind::Wsl; - distro = Some(options.distro_name); - user = options.user; - } - RemoteConnectionOptions::Docker(options) => { - kind = RemoteConnectionKind::Docker; - container_id = Some(options.container_id); - name = Some(options.name); - } - } - Self::get_or_create_remote_connection_query( - this, - kind, - host, - port, - user, - distro, - name, - container_id, - ) - } - - fn get_or_create_remote_connection_query( - this: &Connection, - kind: RemoteConnectionKind, - host: Option, - port: Option, - user: Option, - distro: Option, - name: Option, - container_id: Option, - ) -> Result { - if let Some(id) = this.select_row_bound(sql!( - SELECT id - FROM remote_connections - WHERE - kind IS ? AND - host IS ? AND - port IS ? AND - user IS ? AND - distro IS ? AND - name IS ? AND - container_id IS ? - LIMIT 1 - ))?(( - kind.serialize(), - host.clone(), - port, - user.clone(), - distro.clone(), - name.clone(), - container_id.clone(), - ))? { - Ok(RemoteConnectionId(id)) - } else { - let id = this.select_row_bound(sql!( - INSERT INTO remote_connections ( - kind, - host, - port, - user, - distro, - name, - container_id - ) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7) - RETURNING id - ))?(( - kind.serialize(), - host, - port, - user, - distro, - name, - container_id, - ))? - .context("failed to insert remote project")?; - Ok(RemoteConnectionId(id)) - } - } - - 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, remote_connection_id)| { - ( - id, - PathList::deserialize(&SerializedPathList { paths, order }), - remote_connection_id.map(RemoteConnectionId), - ) - }) - .collect()) - } - - query! { - fn recent_workspaces_query() -> Result)>> { - SELECT workspace_id, paths, paths_order, remote_connection_id - FROM workspaces - WHERE - paths IS NOT NULL OR - remote_connection_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, remote_connection_id)| { - ( - PathList::deserialize(&SerializedPathList { paths, order }), - window_id, - remote_connection_id.map(RemoteConnectionId), - ) - }) - .collect()) - } - - query! { - fn session_workspaces_query(session_id: String) -> Result, Option)>> { - SELECT paths, paths_order, window_id, remote_connection_id - FROM workspaces - WHERE session_id = ?1 - ORDER BY timestamp DESC - } - } - - query! { - pub fn breakpoints_for_file(workspace_id: WorkspaceId, file_path: &Path) -> Result> { - SELECT breakpoint_location - FROM breakpoints - WHERE workspace_id= ?1 AND path = ?2 - } - } - - query! { - pub fn clear_breakpoints(file_path: &Path) -> Result<()> { - DELETE FROM breakpoints - WHERE file_path = ?2 - } - } - - fn remote_connections(&self) -> Result> { - Ok(self.select(sql!( - SELECT - id, kind, host, port, user, distro, container_id, name - FROM - remote_connections - ))?()? - .into_iter() - .filter_map(|(id, kind, host, port, user, distro, container_id, name)| { - Some(( - RemoteConnectionId(id), - Self::remote_connection_from_row( - kind, - host, - port, - user, - distro, - container_id, - name, - )?, - )) - }) - .collect()) - } - - pub(crate) fn remote_connection( - &self, - id: RemoteConnectionId, - ) -> Result { - let (kind, host, port, user, distro, container_id, name) = self.select_row_bound(sql!( - SELECT kind, host, port, user, distro, container_id, name - FROM remote_connections - WHERE id = ? - ))?(id.0)? - .context("no such remote connection")?; - Self::remote_connection_from_row(kind, host, port, user, distro, container_id, name) - .context("invalid remote_connection row") - } - - fn remote_connection_from_row( - kind: String, - host: Option, - port: Option, - user: Option, - distro: Option, - container_id: Option, - name: Option, - ) -> Option { - match RemoteConnectionKind::deserialize(&kind)? { - RemoteConnectionKind::Wsl => Some(RemoteConnectionOptions::Wsl(WslConnectionOptions { - distro_name: distro?, - user: user, - })), - RemoteConnectionKind::Ssh => Some(RemoteConnectionOptions::Ssh(SshConnectionOptions { - host: host?, - port, - username: user, - ..Default::default() - })), - RemoteConnectionKind::Docker => { - Some(RemoteConnectionOptions::Docker(DockerConnectionOptions { - container_id: container_id?, - name: name?, - upload_binary_over_docker_exec: false, - })) - } - } - } - - pub(crate) fn last_window( - &self, - ) -> anyhow::Result<(Option, Option)> { - let mut prepared_query = - self.select::<(Option, Option)>(sql!( - SELECT - display, - window_state, window_x, window_y, window_width, window_height - FROM workspaces - WHERE paths - IS NOT NULL - ORDER BY timestamp DESC - LIMIT 1 - ))?; - let result = prepared_query()?; - Ok(result.into_iter().next().unwrap_or((None, None))) - } - - query! { - pub async fn delete_workspace_by_id(id: WorkspaceId) -> Result<()> { - DELETE FROM workspaces - WHERE workspace_id IS ? - } - } - - // 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> { - let mut result = Vec::new(); - let mut delete_tasks = Vec::new(); - let remote_connections = self.remote_connections()?; - - for (id, paths, remote_connection_id) in self.recent_workspaces()? { - if let Some(remote_connection_id) = remote_connection_id { - if let Some(connection_options) = remote_connections.get(&remote_connection_id) { - result.push(( - id, - SerializedWorkspaceLocation::Remote(connection_options.clone()), - paths, - )); - } else { - delete_tasks.push(self.delete_workspace_by_id(id)); - } - continue; - } - - let has_wsl_path = if cfg!(windows) { - paths - .paths() - .iter() - .any(|path| util::paths::WslPath::from_path(path).is_some()) - } else { - false - }; - - // Delete the workspace if any of the paths are WSL paths. - // If a local workspace points to WSL, this check will cause us to wait for the - // WSL VM and file server to boot up. This can block for many seconds. - // Supported scenarios use remote workspaces. - if !has_wsl_path && paths.paths().iter().all(|path| path.exists()) { - // Only show directories in recent projects - if paths.paths().iter().any(|path| path.is_dir()) { - result.push((id, SerializedWorkspaceLocation::Local, paths)); - } - } else { - delete_tasks.push(self.delete_workspace_by_id(id)); - } - } - - futures::future::join_all(delete_tasks).await; - Ok(result) - } - - pub async fn last_workspace(&self) -> Result> { - Ok(self - .recent_workspaces_on_disk() - .await? - .into_iter() - .next() - .map(|(_, location, paths)| (location, paths))) - } - - // Returns the locations of the workspaces that were still opened when the last - // session was closed (i.e. when Zed was quit). - // If `last_session_window_order` is provided, the returned locations are ordered - // according to that. - pub fn last_session_workspace_locations( - &self, - last_session_id: &str, - last_session_window_stack: Option>, - ) -> Result> { - let mut workspaces = Vec::new(); - - for (paths, window_id, remote_connection_id) in - self.session_workspaces(last_session_id.to_owned())? - { - if let Some(remote_connection_id) = remote_connection_id { - workspaces.push(( - SerializedWorkspaceLocation::Remote( - self.remote_connection(remote_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()) - { - workspaces.push(( - SerializedWorkspaceLocation::Local, - paths, - window_id.map(WindowId::from), - )); - } - } - - if let Some(stack) = last_session_window_stack { - workspaces.sort_by_key(|(_, _, window_id)| { - window_id - .and_then(|id| stack.iter().position(|&order_id| order_id == id)) - .unwrap_or(usize::MAX) - }); - } - - Ok(workspaces - .into_iter() - .map(|(location, paths, _)| (location, paths)) - .collect::>()) - } - - fn get_center_pane_group(&self, workspace_id: WorkspaceId) -> Result { - Ok(self - .get_pane_group(workspace_id, None)? - .into_iter() - .next() - .unwrap_or_else(|| { - SerializedPaneGroup::Pane(SerializedPane { - active: true, - children: vec![], - pinned_count: 0, - }) - })) - } - - fn get_pane_group( - &self, - workspace_id: WorkspaceId, - group_id: Option, - ) -> Result> { - type GroupKey = (Option, WorkspaceId); - type GroupOrPane = ( - Option, - Option, - Option, - Option, - Option, - Option, - ); - self.select_bound::(sql!( - SELECT group_id, axis, pane_id, active, pinned_count, flexes - FROM (SELECT - group_id, - axis, - NULL as pane_id, - NULL as active, - NULL as pinned_count, - position, - parent_group_id, - workspace_id, - flexes - FROM pane_groups - UNION - SELECT - NULL, - NULL, - center_panes.pane_id, - panes.active as active, - pinned_count, - position, - parent_group_id, - panes.workspace_id as workspace_id, - NULL - FROM center_panes - JOIN panes ON center_panes.pane_id = panes.pane_id) - WHERE parent_group_id IS ? AND workspace_id = ? - ORDER BY position - ))?((group_id, workspace_id))? - .into_iter() - .map(|(group_id, axis, pane_id, active, pinned_count, flexes)| { - let maybe_pane = maybe!({ Some((pane_id?, active?, pinned_count?)) }); - if let Some((group_id, axis)) = group_id.zip(axis) { - let flexes = flexes - .map(|flexes: String| serde_json::from_str::>(&flexes)) - .transpose()?; - - Ok(SerializedPaneGroup::Group { - axis, - children: self.get_pane_group(workspace_id, Some(group_id))?, - flexes, - }) - } else if let Some((pane_id, active, pinned_count)) = maybe_pane { - Ok(SerializedPaneGroup::Pane(SerializedPane::new( - self.get_items(pane_id)?, - active, - pinned_count, - ))) - } else { - bail!("Pane Group Child was neither a pane group or a pane"); - } - }) - // Filter out panes and pane groups which don't have any children or items - .filter(|pane_group| match pane_group { - Ok(SerializedPaneGroup::Group { children, .. }) => !children.is_empty(), - Ok(SerializedPaneGroup::Pane(pane)) => !pane.children.is_empty(), - _ => true, - }) - .collect::>() - } - - fn save_pane_group( - conn: &Connection, - workspace_id: WorkspaceId, - pane_group: &SerializedPaneGroup, - parent: Option<(GroupId, usize)>, - ) -> Result<()> { - if parent.is_none() { - log::debug!("Saving a pane group for workspace {workspace_id:?}"); - } - match pane_group { - SerializedPaneGroup::Group { - axis, - children, - flexes, - } => { - let (parent_id, position) = parent.unzip(); - - let flex_string = flexes - .as_ref() - .map(|flexes| serde_json::json!(flexes).to_string()); - - let group_id = conn.select_row_bound::<_, i64>(sql!( - INSERT INTO pane_groups( - workspace_id, - parent_group_id, - position, - axis, - flexes - ) - VALUES (?, ?, ?, ?, ?) - RETURNING group_id - ))?(( - workspace_id, - parent_id, - position, - *axis, - flex_string, - ))? - .context("Couldn't retrieve group_id from inserted pane_group")?; - - for (position, group) in children.iter().enumerate() { - Self::save_pane_group(conn, workspace_id, group, Some((group_id, position)))? - } - - Ok(()) - } - SerializedPaneGroup::Pane(pane) => { - Self::save_pane(conn, workspace_id, pane, parent)?; - Ok(()) - } - } - } - - fn save_pane( - conn: &Connection, - workspace_id: WorkspaceId, - pane: &SerializedPane, - parent: Option<(GroupId, usize)>, - ) -> Result { - let pane_id = conn.select_row_bound::<_, i64>(sql!( - INSERT INTO panes(workspace_id, active, pinned_count) - VALUES (?, ?, ?) - RETURNING pane_id - ))?((workspace_id, pane.active, pane.pinned_count))? - .context("Could not retrieve inserted pane_id")?; - - let (parent_id, order) = parent.unzip(); - conn.exec_bound(sql!( - INSERT INTO center_panes(pane_id, parent_group_id, position) - VALUES (?, ?, ?) - ))?((pane_id, parent_id, order))?; - - Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?; - - Ok(pane_id) - } - - fn get_items(&self, pane_id: PaneId) -> Result> { - self.select_bound(sql!( - SELECT kind, item_id, active, preview FROM items - WHERE pane_id = ? - ORDER BY position - ))?(pane_id) - } - - fn save_items( - conn: &Connection, - workspace_id: WorkspaceId, - pane_id: PaneId, - items: &[SerializedItem], - ) -> Result<()> { - let mut insert = conn.exec_bound(sql!( - INSERT INTO items(workspace_id, pane_id, position, kind, item_id, active, preview) VALUES (?, ?, ?, ?, ?, ?, ?) - )).context("Preparing insertion")?; - for (position, item) in items.iter().enumerate() { - insert((workspace_id, pane_id, position, item))?; - } - - Ok(()) - } - - query! { - pub async fn update_timestamp(workspace_id: WorkspaceId) -> Result<()> { - UPDATE workspaces - SET timestamp = CURRENT_TIMESTAMP - WHERE workspace_id = ? - } - } - - query! { - pub(crate) async fn set_window_open_status(workspace_id: WorkspaceId, bounds: SerializedWindowBounds, display: Uuid) -> Result<()> { - UPDATE workspaces - SET window_state = ?2, - window_x = ?3, - window_y = ?4, - window_width = ?5, - window_height = ?6, - display = ?7 - WHERE workspace_id = ?1 - } - } - - query! { - pub(crate) async fn set_centered_layout(workspace_id: WorkspaceId, centered_layout: bool) -> Result<()> { - UPDATE workspaces - SET centered_layout = ?2 - WHERE workspace_id = ?1 - } - } - - query! { - pub(crate) async fn set_session_id(workspace_id: WorkspaceId, session_id: Option) -> Result<()> { - UPDATE workspaces - SET session_id = ?2 - WHERE workspace_id = ?1 - } - } - - pub(crate) async fn toolchains( - &self, - workspace_id: WorkspaceId, - ) -> Result)>> { - self.write(move |this| { - let mut select = this - .select_bound(sql!( - SELECT - name, path, worktree_id, relative_worktree_path, language_name, raw_json - FROM toolchains - WHERE workspace_id = ? - )) - .context("select toolchains")?; - - let toolchain: Vec<(String, String, u64, String, String, String)> = - select(workspace_id)?; - - Ok(toolchain - .into_iter() - .filter_map( - |(name, path, worktree_id, relative_worktree_path, language, json)| { - Some(( - Toolchain { - name: name.into(), - path: path.into(), - language_name: LanguageName::new(&language), - as_json: serde_json::Value::from_str(&json).ok()?, - }, - WorktreeId::from_proto(worktree_id), - RelPath::from_proto(&relative_worktree_path).log_err()?, - )) - }, - ) - .collect()) - }) - .await - } - - pub async fn set_toolchain( - &self, - workspace_id: WorkspaceId, - worktree_id: WorktreeId, - relative_worktree_path: Arc, - toolchain: Toolchain, - ) -> Result<()> { - log::debug!( - "Setting toolchain for workspace, worktree: {worktree_id:?}, relative path: {relative_worktree_path:?}, toolchain: {}", - toolchain.name - ); - self.write(move |conn| { - let mut insert = conn - .exec_bound(sql!( - INSERT INTO toolchains(workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json) VALUES (?, ?, ?, ?, ?, ?, ?) - ON CONFLICT DO - UPDATE SET - name = ?5, - path = ?6, - raw_json = ?7 - )) - .context("Preparing insertion")?; - - insert(( - workspace_id, - worktree_id.to_usize(), - relative_worktree_path.as_unix_str(), - toolchain.language_name.as_ref(), - toolchain.name.as_ref(), - toolchain.path.as_ref(), - toolchain.as_json.to_string(), - ))?; - - Ok(()) - }).await - } -} - -pub fn delete_unloaded_items( - alive_items: Vec, - workspace_id: WorkspaceId, - table: &'static str, - db: &ThreadSafeConnection, - cx: &mut App, -) -> Task> { - let db = db.clone(); - cx.spawn(async move |_| { - let placeholders = alive_items - .iter() - .map(|_| "?") - .collect::>() - .join(", "); - - let query = format!( - "DELETE FROM {table} WHERE workspace_id = ? AND item_id NOT IN ({placeholders})" - ); - - db.write(move |conn| { - let mut statement = Statement::prepare(conn, query)?; - let mut next_index = statement.bind(&workspace_id, 1)?; - for id in alive_items { - next_index = statement.bind(&id, next_index)?; - } - statement.exec() - }) - .await - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::persistence::model::{ - SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace, - }; - use gpui; - use pretty_assertions::assert_eq; - use remote::SshConnectionOptions; - use std::{thread, time::Duration}; - - #[gpui::test] - async fn test_breakpoints() { - zlog::init_test(); - - let db = WorkspaceDb::open_test_db("test_breakpoints").await; - let id = db.next_id().await.unwrap(); - - let path = Path::new("/tmp/test.rs"); - - let breakpoint = Breakpoint { - position: 123, - message: None, - state: BreakpointState::Enabled, - condition: None, - hit_condition: None, - }; - - let log_breakpoint = Breakpoint { - position: 456, - message: Some("Test log message".into()), - state: BreakpointState::Enabled, - condition: None, - hit_condition: None, - }; - - let disable_breakpoint = Breakpoint { - position: 578, - message: None, - state: BreakpointState::Disabled, - condition: None, - hit_condition: None, - }; - - let condition_breakpoint = Breakpoint { - position: 789, - message: None, - state: BreakpointState::Enabled, - condition: Some("x > 5".into()), - hit_condition: None, - }; - - let hit_condition_breakpoint = Breakpoint { - position: 999, - message: None, - state: BreakpointState::Enabled, - condition: None, - hit_condition: Some(">= 3".into()), - }; - - let workspace = SerializedWorkspace { - id, - paths: PathList::new(&["/tmp"]), - location: SerializedWorkspaceLocation::Local, - center_group: Default::default(), - window_bounds: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - breakpoints: { - let mut map = collections::BTreeMap::default(); - map.insert( - Arc::from(path), - vec![ - SourceBreakpoint { - row: breakpoint.position, - path: Arc::from(path), - message: breakpoint.message.clone(), - state: breakpoint.state, - condition: breakpoint.condition.clone(), - hit_condition: breakpoint.hit_condition.clone(), - }, - SourceBreakpoint { - row: log_breakpoint.position, - path: Arc::from(path), - message: log_breakpoint.message.clone(), - state: log_breakpoint.state, - condition: log_breakpoint.condition.clone(), - hit_condition: log_breakpoint.hit_condition.clone(), - }, - SourceBreakpoint { - row: disable_breakpoint.position, - path: Arc::from(path), - message: disable_breakpoint.message.clone(), - state: disable_breakpoint.state, - condition: disable_breakpoint.condition.clone(), - hit_condition: disable_breakpoint.hit_condition.clone(), - }, - SourceBreakpoint { - row: condition_breakpoint.position, - path: Arc::from(path), - message: condition_breakpoint.message.clone(), - state: condition_breakpoint.state, - condition: condition_breakpoint.condition.clone(), - hit_condition: condition_breakpoint.hit_condition.clone(), - }, - SourceBreakpoint { - row: hit_condition_breakpoint.position, - path: Arc::from(path), - message: hit_condition_breakpoint.message.clone(), - state: hit_condition_breakpoint.state, - condition: hit_condition_breakpoint.condition.clone(), - hit_condition: hit_condition_breakpoint.hit_condition.clone(), - }, - ], - ); - map - }, - session_id: None, - window_id: None, - user_toolchains: Default::default(), - }; - - db.save_workspace(workspace.clone()).await; - - let loaded = db.workspace_for_roots(&["/tmp"]).unwrap(); - let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(path)).unwrap(); - - assert_eq!(loaded_breakpoints.len(), 5); - - // normal breakpoint - assert_eq!(loaded_breakpoints[0].row, breakpoint.position); - assert_eq!(loaded_breakpoints[0].message, breakpoint.message); - assert_eq!(loaded_breakpoints[0].condition, breakpoint.condition); - assert_eq!( - loaded_breakpoints[0].hit_condition, - breakpoint.hit_condition - ); - assert_eq!(loaded_breakpoints[0].state, breakpoint.state); - assert_eq!(loaded_breakpoints[0].path, Arc::from(path)); - - // enabled breakpoint - assert_eq!(loaded_breakpoints[1].row, log_breakpoint.position); - assert_eq!(loaded_breakpoints[1].message, log_breakpoint.message); - assert_eq!(loaded_breakpoints[1].condition, log_breakpoint.condition); - assert_eq!( - loaded_breakpoints[1].hit_condition, - log_breakpoint.hit_condition - ); - assert_eq!(loaded_breakpoints[1].state, log_breakpoint.state); - assert_eq!(loaded_breakpoints[1].path, Arc::from(path)); - - // disable breakpoint - assert_eq!(loaded_breakpoints[2].row, disable_breakpoint.position); - assert_eq!(loaded_breakpoints[2].message, disable_breakpoint.message); - assert_eq!( - loaded_breakpoints[2].condition, - disable_breakpoint.condition - ); - assert_eq!( - loaded_breakpoints[2].hit_condition, - disable_breakpoint.hit_condition - ); - assert_eq!(loaded_breakpoints[2].state, disable_breakpoint.state); - assert_eq!(loaded_breakpoints[2].path, Arc::from(path)); - - // condition breakpoint - assert_eq!(loaded_breakpoints[3].row, condition_breakpoint.position); - assert_eq!(loaded_breakpoints[3].message, condition_breakpoint.message); - assert_eq!( - loaded_breakpoints[3].condition, - condition_breakpoint.condition - ); - assert_eq!( - loaded_breakpoints[3].hit_condition, - condition_breakpoint.hit_condition - ); - assert_eq!(loaded_breakpoints[3].state, condition_breakpoint.state); - assert_eq!(loaded_breakpoints[3].path, Arc::from(path)); - - // hit condition breakpoint - assert_eq!(loaded_breakpoints[4].row, hit_condition_breakpoint.position); - assert_eq!( - loaded_breakpoints[4].message, - hit_condition_breakpoint.message - ); - assert_eq!( - loaded_breakpoints[4].condition, - hit_condition_breakpoint.condition - ); - assert_eq!( - loaded_breakpoints[4].hit_condition, - hit_condition_breakpoint.hit_condition - ); - assert_eq!(loaded_breakpoints[4].state, hit_condition_breakpoint.state); - assert_eq!(loaded_breakpoints[4].path, Arc::from(path)); - } - - #[gpui::test] - async fn test_remove_last_breakpoint() { - zlog::init_test(); - - let db = WorkspaceDb::open_test_db("test_remove_last_breakpoint").await; - let id = db.next_id().await.unwrap(); - - let singular_path = Path::new("/tmp/test_remove_last_breakpoint.rs"); - - let breakpoint_to_remove = Breakpoint { - position: 100, - message: None, - state: BreakpointState::Enabled, - condition: None, - hit_condition: None, - }; - - let workspace = SerializedWorkspace { - id, - paths: PathList::new(&["/tmp"]), - location: SerializedWorkspaceLocation::Local, - center_group: Default::default(), - window_bounds: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - breakpoints: { - let mut map = collections::BTreeMap::default(); - map.insert( - Arc::from(singular_path), - vec![SourceBreakpoint { - row: breakpoint_to_remove.position, - path: Arc::from(singular_path), - message: None, - state: BreakpointState::Enabled, - condition: None, - hit_condition: None, - }], - ); - map - }, - session_id: None, - window_id: None, - user_toolchains: Default::default(), - }; - - db.save_workspace(workspace.clone()).await; - - let loaded = db.workspace_for_roots(&["/tmp"]).unwrap(); - let loaded_breakpoints = loaded.breakpoints.get(&Arc::from(singular_path)).unwrap(); - - assert_eq!(loaded_breakpoints.len(), 1); - assert_eq!(loaded_breakpoints[0].row, breakpoint_to_remove.position); - assert_eq!(loaded_breakpoints[0].message, breakpoint_to_remove.message); - assert_eq!( - loaded_breakpoints[0].condition, - breakpoint_to_remove.condition - ); - assert_eq!( - loaded_breakpoints[0].hit_condition, - breakpoint_to_remove.hit_condition - ); - assert_eq!(loaded_breakpoints[0].state, breakpoint_to_remove.state); - assert_eq!(loaded_breakpoints[0].path, Arc::from(singular_path)); - - let workspace_without_breakpoint = SerializedWorkspace { - id, - paths: PathList::new(&["/tmp"]), - location: SerializedWorkspaceLocation::Local, - center_group: Default::default(), - window_bounds: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - breakpoints: collections::BTreeMap::default(), - session_id: None, - window_id: None, - user_toolchains: Default::default(), - }; - - db.save_workspace(workspace_without_breakpoint.clone()) - .await; - - let loaded_after_remove = db.workspace_for_roots(&["/tmp"]).unwrap(); - let empty_breakpoints = loaded_after_remove - .breakpoints - .get(&Arc::from(singular_path)); - - assert!(empty_breakpoints.is_none()); - } - - #[gpui::test] - async fn test_next_id_stability() { - zlog::init_test(); - - let db = WorkspaceDb::open_test_db("test_next_id_stability").await; - - db.write(|conn| { - conn.migrate( - "test_table", - &[sql!( - CREATE TABLE test_table( - text TEXT, - workspace_id INTEGER, - FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ) STRICT; - )], - |_, _, _| false, - ) - .unwrap(); - }) - .await; - - let id = db.next_id().await.unwrap(); - // Assert the empty row got inserted - assert_eq!( - Some(id), - db.select_row_bound::(sql!( - SELECT workspace_id FROM workspaces WHERE workspace_id = ? - )) - .unwrap()(id) - .unwrap() - ); - - db.write(move |conn| { - conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?))) - .unwrap()(("test-text-1", id)) - .unwrap() - }) - .await; - - let test_text_1 = db - .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?)) - .unwrap()(1) - .unwrap() - .unwrap(); - assert_eq!(test_text_1, "test-text-1"); - } - - #[gpui::test] - async fn test_workspace_id_stability() { - zlog::init_test(); - - let db = WorkspaceDb::open_test_db("test_workspace_id_stability").await; - - db.write(|conn| { - conn.migrate( - "test_table", - &[sql!( - CREATE TABLE test_table( - text TEXT, - workspace_id INTEGER, - FOREIGN KEY(workspace_id) - REFERENCES workspaces(workspace_id) - ON DELETE CASCADE - ) STRICT;)], - |_, _, _| false, - ) - }) - .await - .unwrap(); - - let mut workspace_1 = SerializedWorkspace { - id: WorkspaceId(1), - paths: PathList::new(&["/tmp", "/tmp2"]), - location: SerializedWorkspaceLocation::Local, - center_group: Default::default(), - window_bounds: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - breakpoints: Default::default(), - session_id: None, - window_id: None, - user_toolchains: Default::default(), - }; - - let workspace_2 = SerializedWorkspace { - id: WorkspaceId(2), - paths: PathList::new(&["/tmp"]), - location: SerializedWorkspaceLocation::Local, - center_group: Default::default(), - window_bounds: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - breakpoints: Default::default(), - session_id: None, - window_id: None, - user_toolchains: Default::default(), - }; - - db.save_workspace(workspace_1.clone()).await; - - db.write(|conn| { - conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?))) - .unwrap()(("test-text-1", 1)) - .unwrap(); - }) - .await; - - db.save_workspace(workspace_2.clone()).await; - - db.write(|conn| { - conn.exec_bound(sql!(INSERT INTO test_table(text, workspace_id) VALUES (?, ?))) - .unwrap()(("test-text-2", 2)) - .unwrap(); - }) - .await; - - workspace_1.paths = PathList::new(&["/tmp", "/tmp3"]); - db.save_workspace(workspace_1.clone()).await; - db.save_workspace(workspace_1).await; - db.save_workspace(workspace_2).await; - - let test_text_2 = db - .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?)) - .unwrap()(2) - .unwrap() - .unwrap(); - assert_eq!(test_text_2, "test-text-2"); - - let test_text_1 = db - .select_row_bound::<_, String>(sql!(SELECT text FROM test_table WHERE workspace_id = ?)) - .unwrap()(1) - .unwrap() - .unwrap(); - assert_eq!(test_text_1, "test-text-1"); - } - - fn group(axis: Axis, children: Vec) -> SerializedPaneGroup { - SerializedPaneGroup::Group { - axis: SerializedAxis(axis), - flexes: None, - children, - } - } - - #[gpui::test] - async fn test_full_workspace_serialization() { - zlog::init_test(); - - let db = WorkspaceDb::open_test_db("test_full_workspace_serialization").await; - - // ----------------- - // | 1,2 | 5,6 | - // | - - - | | - // | 3,4 | | - // ----------------- - let center_group = group( - Axis::Horizontal, - vec![ - group( - Axis::Vertical, - vec![ - SerializedPaneGroup::Pane(SerializedPane::new( - vec![ - SerializedItem::new("Terminal", 5, false, false), - SerializedItem::new("Terminal", 6, true, false), - ], - false, - 0, - )), - SerializedPaneGroup::Pane(SerializedPane::new( - vec![ - SerializedItem::new("Terminal", 7, true, false), - SerializedItem::new("Terminal", 8, false, false), - ], - false, - 0, - )), - ], - ), - SerializedPaneGroup::Pane(SerializedPane::new( - vec![ - SerializedItem::new("Terminal", 9, false, false), - SerializedItem::new("Terminal", 10, true, false), - ], - false, - 0, - )), - ], - ); - - let workspace = SerializedWorkspace { - id: WorkspaceId(5), - paths: PathList::new(&["/tmp", "/tmp2"]), - location: SerializedWorkspaceLocation::Local, - center_group, - window_bounds: Default::default(), - breakpoints: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - session_id: None, - window_id: Some(999), - user_toolchains: Default::default(), - }; - - db.save_workspace(workspace.clone()).await; - - let round_trip_workspace = db.workspace_for_roots(&["/tmp2", "/tmp"]); - assert_eq!(workspace, round_trip_workspace.unwrap()); - - // Test guaranteed duplicate IDs - db.save_workspace(workspace.clone()).await; - db.save_workspace(workspace.clone()).await; - - let round_trip_workspace = db.workspace_for_roots(&["/tmp", "/tmp2"]); - assert_eq!(workspace, round_trip_workspace.unwrap()); - } - - #[gpui::test] - async fn test_workspace_assignment() { - zlog::init_test(); - - let db = WorkspaceDb::open_test_db("test_basic_functionality").await; - - let workspace_1 = SerializedWorkspace { - id: WorkspaceId(1), - paths: PathList::new(&["/tmp", "/tmp2"]), - location: SerializedWorkspaceLocation::Local, - center_group: Default::default(), - window_bounds: Default::default(), - breakpoints: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - session_id: None, - window_id: Some(1), - user_toolchains: Default::default(), - }; - - let mut workspace_2 = SerializedWorkspace { - id: WorkspaceId(2), - paths: PathList::new(&["/tmp"]), - location: SerializedWorkspaceLocation::Local, - center_group: Default::default(), - window_bounds: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - breakpoints: Default::default(), - session_id: None, - window_id: Some(2), - user_toolchains: Default::default(), - }; - - db.save_workspace(workspace_1.clone()).await; - db.save_workspace(workspace_2.clone()).await; - - // Test that paths are treated as a set - assert_eq!( - db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(), - workspace_1 - ); - assert_eq!( - db.workspace_for_roots(&["/tmp2", "/tmp"]).unwrap(), - workspace_1 - ); - - // Make sure that other keys work - assert_eq!(db.workspace_for_roots(&["/tmp"]).unwrap(), workspace_2); - 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"]); - - db.save_workspace(workspace_2.clone()).await; - assert_eq!( - db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(), - workspace_2 - ); - - // Test other mechanism for mutating - let mut workspace_3 = SerializedWorkspace { - id: WorkspaceId(3), - paths: PathList::new(&["/tmp2", "/tmp"]), - location: SerializedWorkspaceLocation::Local, - center_group: Default::default(), - window_bounds: Default::default(), - breakpoints: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - session_id: None, - window_id: Some(3), - user_toolchains: Default::default(), - }; - - db.save_workspace(workspace_3.clone()).await; - assert_eq!( - db.workspace_for_roots(&["/tmp", "/tmp2"]).unwrap(), - workspace_3 - ); - - // Make sure that updating paths differently also works - workspace_3.paths = PathList::new(&["/tmp3", "/tmp4", "/tmp2"]); - db.save_workspace(workspace_3.clone()).await; - assert_eq!(db.workspace_for_roots(&["/tmp2", "tmp"]), None); - assert_eq!( - db.workspace_for_roots(&["/tmp2", "/tmp3", "/tmp4"]) - .unwrap(), - workspace_3 - ); - } - - #[gpui::test] - async fn test_session_workspaces() { - zlog::init_test(); - - let db = WorkspaceDb::open_test_db("test_serializing_workspaces_session_id").await; - - let workspace_1 = SerializedWorkspace { - id: WorkspaceId(1), - paths: PathList::new(&["/tmp1"]), - location: SerializedWorkspaceLocation::Local, - center_group: Default::default(), - window_bounds: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - breakpoints: Default::default(), - session_id: Some("session-id-1".to_owned()), - window_id: Some(10), - user_toolchains: Default::default(), - }; - - let workspace_2 = SerializedWorkspace { - id: WorkspaceId(2), - paths: PathList::new(&["/tmp2"]), - location: SerializedWorkspaceLocation::Local, - center_group: Default::default(), - window_bounds: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - breakpoints: Default::default(), - session_id: Some("session-id-1".to_owned()), - window_id: Some(20), - user_toolchains: Default::default(), - }; - - let workspace_3 = SerializedWorkspace { - id: WorkspaceId(3), - paths: PathList::new(&["/tmp3"]), - location: SerializedWorkspaceLocation::Local, - center_group: Default::default(), - window_bounds: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - breakpoints: Default::default(), - session_id: Some("session-id-2".to_owned()), - window_id: Some(30), - user_toolchains: Default::default(), - }; - - let workspace_4 = SerializedWorkspace { - id: WorkspaceId(4), - paths: PathList::new(&["/tmp4"]), - location: SerializedWorkspaceLocation::Local, - center_group: Default::default(), - window_bounds: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - breakpoints: Default::default(), - session_id: None, - window_id: None, - user_toolchains: Default::default(), - }; - - let connection_id = db - .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions { - host: "my-host".to_string(), - port: Some(1234), - ..Default::default() - })) - .await - .unwrap(); - - let workspace_5 = SerializedWorkspace { - id: WorkspaceId(5), - paths: PathList::default(), - location: SerializedWorkspaceLocation::Remote( - db.remote_connection(connection_id).unwrap(), - ), - center_group: Default::default(), - window_bounds: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - breakpoints: Default::default(), - session_id: Some("session-id-2".to_owned()), - window_id: Some(50), - user_toolchains: Default::default(), - }; - - let workspace_6 = SerializedWorkspace { - id: WorkspaceId(6), - paths: PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]), - location: SerializedWorkspaceLocation::Local, - center_group: Default::default(), - window_bounds: Default::default(), - breakpoints: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - session_id: Some("session-id-3".to_owned()), - window_id: Some(60), - user_toolchains: Default::default(), - }; - - db.save_workspace(workspace_1.clone()).await; - thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment - db.save_workspace(workspace_2.clone()).await; - db.save_workspace(workspace_3.clone()).await; - thread::sleep(Duration::from_millis(1000)); // Force timestamps to increment - db.save_workspace(workspace_4.clone()).await; - db.save_workspace(workspace_5.clone()).await; - db.save_workspace(workspace_6.clone()).await; - - 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)); - - 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 locations = db.session_workspaces("session-id-3".to_owned()).unwrap(); - assert_eq!(locations.len(), 1); - assert_eq!( - locations[0].0, - PathList::new(&["/tmp6c", "/tmp6b", "/tmp6a"]), - ); - assert_eq!(locations[0].1, Some(60)); - } - - fn default_workspace>( - paths: &[P], - center_group: &SerializedPaneGroup, - ) -> SerializedWorkspace { - SerializedWorkspace { - id: WorkspaceId(4), - paths: PathList::new(paths), - location: SerializedWorkspaceLocation::Local, - center_group: center_group.clone(), - window_bounds: Default::default(), - display: Default::default(), - docks: Default::default(), - breakpoints: Default::default(), - centered_layout: false, - session_id: None, - window_id: None, - user_toolchains: Default::default(), - } - } - - #[gpui::test] - async fn test_last_session_workspace_locations() { - let dir1 = tempfile::TempDir::with_prefix("dir1").unwrap(); - let dir2 = tempfile::TempDir::with_prefix("dir2").unwrap(); - let dir3 = tempfile::TempDir::with_prefix("dir3").unwrap(); - let dir4 = tempfile::TempDir::with_prefix("dir4").unwrap(); - - let db = - 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), - ] - .into_iter() - .map(|(id, paths, window_id)| SerializedWorkspace { - id: WorkspaceId(id), - paths: PathList::new(paths.as_slice()), - location: SerializedWorkspaceLocation::Local, - center_group: Default::default(), - window_bounds: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - session_id: Some("one-session".to_owned()), - breakpoints: Default::default(), - window_id: Some(window_id), - user_toolchains: Default::default(), - }) - .collect::>(); - - for workspace in workspaces.iter() { - db.save_workspace(workspace.clone()).await; - } - - let stack = Some(Vec::from([ - WindowId::from(2), // Top - WindowId::from(8), - WindowId::from(5), - WindowId::from(9), - WindowId::from(3), - WindowId::from(4), // Bottom - ])); - - let locations = db - .last_session_workspace_locations("one-session", stack) - .unwrap(); - 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()]) - ), - ] - ); - } - - #[gpui::test] - async fn test_last_session_workspace_locations_remote() { - let db = - WorkspaceDb::open_test_db("test_serializing_workspaces_last_session_workspaces_remote") - .await; - - let remote_connections = [ - ("host-1", "my-user-1"), - ("host-2", "my-user-2"), - ("host-3", "my-user-3"), - ("host-4", "my-user-4"), - ] - .into_iter() - .map(|(host, user)| async { - let options = RemoteConnectionOptions::Ssh(SshConnectionOptions { - host: host.to_string(), - username: Some(user.to_string()), - ..Default::default() - }); - db.get_or_create_remote_connection(options.clone()) - .await - .unwrap(); - options - }) - .collect::>(); - - let remote_connections = futures::future::join_all(remote_connections).await; - - let workspaces = [ - (1, remote_connections[0].clone(), 9), - (2, remote_connections[1].clone(), 5), - (3, remote_connections[2].clone(), 8), - (4, remote_connections[3].clone(), 2), - ] - .into_iter() - .map(|(id, remote_connection, window_id)| SerializedWorkspace { - id: WorkspaceId(id), - paths: PathList::default(), - location: SerializedWorkspaceLocation::Remote(remote_connection), - center_group: Default::default(), - window_bounds: Default::default(), - display: Default::default(), - docks: Default::default(), - centered_layout: false, - session_id: Some("one-session".to_owned()), - breakpoints: Default::default(), - window_id: Some(window_id), - user_toolchains: Default::default(), - }) - .collect::>(); - - for workspace in workspaces.iter() { - db.save_workspace(workspace.clone()).await; - } - - let stack = Some(Vec::from([ - WindowId::from(2), // Top - WindowId::from(8), - WindowId::from(5), - WindowId::from(9), // Bottom - ])); - - let have = db - .last_session_workspace_locations("one-session", stack) - .unwrap(); - assert_eq!(have.len(), 4); - assert_eq!( - have[0], - ( - SerializedWorkspaceLocation::Remote(remote_connections[3].clone()), - PathList::default() - ) - ); - assert_eq!( - have[1], - ( - SerializedWorkspaceLocation::Remote(remote_connections[2].clone()), - PathList::default() - ) - ); - assert_eq!( - have[2], - ( - SerializedWorkspaceLocation::Remote(remote_connections[1].clone()), - PathList::default() - ) - ); - assert_eq!( - have[3], - ( - SerializedWorkspaceLocation::Remote(remote_connections[0].clone()), - PathList::default() - ) - ); - } - - #[gpui::test] - 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 connection_id = db - .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions { - host: host.clone(), - port, - username: user.clone(), - ..Default::default() - })) - .await - .unwrap(); - - // Test that calling the function again with the same parameters returns the same project - let same_connection = db - .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions { - host: host.clone(), - port, - username: user.clone(), - ..Default::default() - })) - .await - .unwrap(); - - assert_eq!(connection_id, same_connection); - - // Test with different parameters - let host2 = "otherexample.com".to_string(); - let port2 = None; - let user2 = Some("otheruser".to_string()); - - let different_connection = db - .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions { - host: host2.clone(), - port: port2, - username: user2.clone(), - ..Default::default() - })) - .await - .unwrap(); - - assert_ne!(connection_id, different_connection); - } - - #[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 connection_id = db - .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions { - host: host.clone(), - port, - username: None, - ..Default::default() - })) - .await - .unwrap(); - - let same_connection_id = db - .get_or_create_remote_connection(RemoteConnectionOptions::Ssh(SshConnectionOptions { - host: host.clone(), - port, - username: user.clone(), - ..Default::default() - })) - .await - .unwrap(); - - assert_eq!(connection_id, same_connection_id); - } - - #[gpui::test] - async fn test_get_remote_connections() { - let db = WorkspaceDb::open_test_db("test_get_remote_connections").await; - - let connections = [ - ("example.com".to_string(), None, None), - ( - "anotherexample.com".to_string(), - Some(123_u16), - Some("user2".to_string()), - ), - ("yetanother.com".to_string(), Some(345_u16), None), - ]; - - let mut ids = Vec::new(); - for (host, port, user) in connections.iter() { - ids.push( - db.get_or_create_remote_connection(RemoteConnectionOptions::Ssh( - SshConnectionOptions { - host: host.clone(), - port: *port, - username: user.clone(), - ..Default::default() - }, - )) - .await - .unwrap(), - ); - } - - let stored_connections = db.remote_connections().unwrap(); - assert_eq!( - stored_connections, - [ - ( - ids[0], - RemoteConnectionOptions::Ssh(SshConnectionOptions { - host: "example.com".into(), - port: None, - username: None, - ..Default::default() - }), - ), - ( - ids[1], - RemoteConnectionOptions::Ssh(SshConnectionOptions { - host: "anotherexample.com".into(), - port: Some(123), - username: Some("user2".into()), - ..Default::default() - }), - ), - ( - ids[2], - RemoteConnectionOptions::Ssh(SshConnectionOptions { - host: "yetanother.com".into(), - port: Some(345), - username: None, - ..Default::default() - }), - ), - ] - .into_iter() - .collect::>(), - ); - } - - #[gpui::test] - async fn test_simple_split() { - zlog::init_test(); - - let db = WorkspaceDb::open_test_db("simple_split").await; - - // ----------------- - // | 1,2 | 5,6 | - // | - - - | | - // | 3,4 | | - // ----------------- - let center_pane = group( - Axis::Horizontal, - vec![ - group( - Axis::Vertical, - vec![ - SerializedPaneGroup::Pane(SerializedPane::new( - vec![ - SerializedItem::new("Terminal", 1, false, false), - SerializedItem::new("Terminal", 2, true, false), - ], - false, - 0, - )), - SerializedPaneGroup::Pane(SerializedPane::new( - vec![ - SerializedItem::new("Terminal", 4, false, false), - SerializedItem::new("Terminal", 3, true, false), - ], - true, - 0, - )), - ], - ), - SerializedPaneGroup::Pane(SerializedPane::new( - vec![ - SerializedItem::new("Terminal", 5, true, false), - SerializedItem::new("Terminal", 6, false, false), - ], - false, - 0, - )), - ], - ); - - let workspace = default_workspace(&["/tmp"], ¢er_pane); - - db.save_workspace(workspace.clone()).await; - - let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap(); - - assert_eq!(workspace.center_group, new_workspace.center_group); - } - - #[gpui::test] - async fn test_cleanup_panes() { - zlog::init_test(); - - let db = WorkspaceDb::open_test_db("test_cleanup_panes").await; - - let center_pane = group( - Axis::Horizontal, - vec![ - group( - Axis::Vertical, - vec![ - SerializedPaneGroup::Pane(SerializedPane::new( - vec![ - SerializedItem::new("Terminal", 1, false, false), - SerializedItem::new("Terminal", 2, true, false), - ], - false, - 0, - )), - SerializedPaneGroup::Pane(SerializedPane::new( - vec![ - SerializedItem::new("Terminal", 4, false, false), - SerializedItem::new("Terminal", 3, true, false), - ], - true, - 0, - )), - ], - ), - SerializedPaneGroup::Pane(SerializedPane::new( - vec![ - SerializedItem::new("Terminal", 5, false, false), - SerializedItem::new("Terminal", 6, true, false), - ], - false, - 0, - )), - ], - ); - - let id = &["/tmp"]; - - let mut workspace = default_workspace(id, ¢er_pane); - - db.save_workspace(workspace.clone()).await; - - workspace.center_group = group( - Axis::Vertical, - vec![ - SerializedPaneGroup::Pane(SerializedPane::new( - vec![ - SerializedItem::new("Terminal", 1, false, false), - SerializedItem::new("Terminal", 2, true, false), - ], - false, - 0, - )), - SerializedPaneGroup::Pane(SerializedPane::new( - vec![ - SerializedItem::new("Terminal", 4, true, false), - SerializedItem::new("Terminal", 3, false, false), - ], - true, - 0, - )), - ], - ); - - db.save_workspace(workspace.clone()).await; - - let new_workspace = db.workspace_for_roots(id).unwrap(); - - assert_eq!(workspace.center_group, new_workspace.center_group); - } -} diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs deleted file mode 100644 index 08a3adf9eb..0000000000 --- a/crates/workspace/src/persistence/model.rs +++ /dev/null @@ -1,399 +0,0 @@ -use super::{SerializedAxis, SerializedWindowBounds}; -use crate::{ - Member, Pane, PaneAxis, SerializableItemRegistry, Workspace, WorkspaceId, item::ItemHandle, - path_list::PathList, -}; -use anyhow::{Context, Result}; -use async_recursion::async_recursion; -use collections::IndexSet; -use db::sqlez::{ - bindable::{Bind, Column, StaticColumnCount}, - statement::Statement, -}; -use gpui::{AsyncWindowContext, Entity, WeakEntity}; - -use language::{Toolchain, ToolchainScope}; -use project::{Project, debugger::breakpoint_store::SourceBreakpoint}; -use remote::RemoteConnectionOptions; -use std::{ - collections::BTreeMap, - path::{Path, PathBuf}, - sync::Arc, -}; -use util::ResultExt; -use uuid::Uuid; - -#[derive( - Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, serde::Serialize, serde::Deserialize, -)] -pub(crate) struct RemoteConnectionId(pub u64); - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub(crate) enum RemoteConnectionKind { - Ssh, - Wsl, - Docker, -} - -#[derive(Debug, PartialEq, Clone)] -pub enum SerializedWorkspaceLocation { - Local, - Remote(RemoteConnectionOptions), -} - -impl SerializedWorkspaceLocation { - /// Get sorted paths - pub fn sorted_paths(&self) -> Arc> { - unimplemented!() - } -} - -#[derive(Debug, PartialEq, Clone)] -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, - pub(crate) display: Option, - pub(crate) docks: DockStructure, - pub(crate) session_id: Option, - pub(crate) breakpoints: BTreeMap, Vec>, - pub(crate) user_toolchains: BTreeMap>, - pub(crate) window_id: Option, -} - -#[derive(Debug, PartialEq, Clone, Default)] -pub struct DockStructure { - pub(crate) left: DockData, - pub(crate) right: DockData, - pub(crate) bottom: DockData, -} - -impl RemoteConnectionKind { - pub(crate) fn serialize(&self) -> &'static str { - match self { - RemoteConnectionKind::Ssh => "ssh", - RemoteConnectionKind::Wsl => "wsl", - RemoteConnectionKind::Docker => "docker", - } - } - - pub(crate) fn deserialize(text: &str) -> Option { - match text { - "ssh" => Some(Self::Ssh), - "wsl" => Some(Self::Wsl), - "docker" => Some(Self::Docker), - _ => None, - } - } -} - -impl Column for DockStructure { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let (left, next_index) = DockData::column(statement, start_index)?; - let (right, next_index) = DockData::column(statement, next_index)?; - let (bottom, next_index) = DockData::column(statement, next_index)?; - Ok(( - DockStructure { - left, - right, - bottom, - }, - next_index, - )) - } -} - -impl Bind for DockStructure { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - let next_index = statement.bind(&self.left, start_index)?; - let next_index = statement.bind(&self.right, next_index)?; - statement.bind(&self.bottom, next_index) - } -} - -#[derive(Debug, PartialEq, Clone, Default)] -pub struct DockData { - pub(crate) visible: bool, - pub(crate) active_panel: Option, - pub(crate) zoom: bool, -} - -impl Column for DockData { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let (visible, next_index) = Option::::column(statement, start_index)?; - let (active_panel, next_index) = Option::::column(statement, next_index)?; - let (zoom, next_index) = Option::::column(statement, next_index)?; - Ok(( - DockData { - visible: visible.unwrap_or(false), - active_panel, - zoom: zoom.unwrap_or(false), - }, - next_index, - )) - } -} - -impl Bind for DockData { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - let next_index = statement.bind(&self.visible, start_index)?; - let next_index = statement.bind(&self.active_panel, next_index)?; - statement.bind(&self.zoom, next_index) - } -} - -#[derive(Debug, PartialEq, Clone)] -pub(crate) enum SerializedPaneGroup { - Group { - axis: SerializedAxis, - flexes: Option>, - children: Vec, - }, - Pane(SerializedPane), -} - -#[cfg(test)] -impl Default for SerializedPaneGroup { - fn default() -> Self { - Self::Pane(SerializedPane { - children: vec![SerializedItem::default()], - active: false, - pinned_count: 0, - }) - } -} - -impl SerializedPaneGroup { - #[async_recursion(?Send)] - pub(crate) async fn deserialize( - self, - project: &Entity, - workspace_id: WorkspaceId, - workspace: WeakEntity, - cx: &mut AsyncWindowContext, - ) -> Option<( - Member, - Option>, - Vec>>, - )> { - match self { - SerializedPaneGroup::Group { - axis, - children, - flexes, - } => { - let mut current_active_pane = None; - let mut members = Vec::new(); - let mut items = Vec::new(); - for child in children { - if let Some((new_member, active_pane, new_items)) = child - .deserialize(project, workspace_id, workspace.clone(), cx) - .await - { - members.push(new_member); - items.extend(new_items); - current_active_pane = current_active_pane.or(active_pane); - } - } - - if members.is_empty() { - return None; - } - - if members.len() == 1 { - return Some((members.remove(0), current_active_pane, items)); - } - - Some(( - Member::Axis(PaneAxis::load(axis.0, members, flexes)), - current_active_pane, - items, - )) - } - SerializedPaneGroup::Pane(serialized_pane) => { - let pane = workspace - .update_in(cx, |workspace, window, cx| { - workspace.add_pane(window, cx).downgrade() - }) - .log_err()?; - let active = serialized_pane.active; - let new_items = serialized_pane - .deserialize_to(project, &pane, workspace_id, workspace.clone(), cx) - .await - .context("Could not deserialize pane)") - .log_err()?; - - if pane - .read_with(cx, |pane, _| pane.items_len() != 0) - .log_err()? - { - let pane = pane.upgrade()?; - Some(( - Member::Pane(pane.clone()), - active.then_some(pane), - new_items, - )) - } else { - let pane = pane.upgrade()?; - workspace - .update_in(cx, |workspace, window, cx| { - workspace.force_remove_pane(&pane, &None, window, cx) - }) - .log_err()?; - None - } - } - } - } -} - -#[derive(Debug, PartialEq, Eq, Default, Clone)] -pub struct SerializedPane { - pub(crate) active: bool, - pub(crate) children: Vec, - pub(crate) pinned_count: usize, -} - -impl SerializedPane { - pub fn new(children: Vec, active: bool, pinned_count: usize) -> Self { - SerializedPane { - children, - active, - pinned_count, - } - } - - pub async fn deserialize_to( - &self, - project: &Entity, - pane: &WeakEntity, - workspace_id: WorkspaceId, - workspace: WeakEntity, - cx: &mut AsyncWindowContext, - ) -> Result>>> { - let mut item_tasks = Vec::new(); - let mut active_item_index = None; - let mut preview_item_index = None; - for (index, item) in self.children.iter().enumerate() { - let project = project.clone(); - item_tasks.push(pane.update_in(cx, |_, window, cx| { - SerializableItemRegistry::deserialize( - &item.kind, - project, - workspace.clone(), - workspace_id, - item.item_id, - window, - cx, - ) - })?); - if item.active { - active_item_index = Some(index); - } - if item.preview { - preview_item_index = Some(index); - } - } - - let mut items = Vec::new(); - for item_handle in futures::future::join_all(item_tasks).await { - let item_handle = item_handle.log_err(); - items.push(item_handle.clone()); - - if let Some(item_handle) = item_handle { - pane.update_in(cx, |pane, window, cx| { - pane.add_item(item_handle.clone(), true, true, None, window, cx); - })?; - } - } - - if let Some(active_item_index) = active_item_index { - pane.update_in(cx, |pane, window, cx| { - pane.activate_item(active_item_index, false, false, window, cx); - })?; - } - - if let Some(preview_item_index) = preview_item_index { - pane.update(cx, |pane, cx| { - if let Some(item) = pane.item_for_index(preview_item_index) { - pane.set_preview_item_id(Some(item.item_id()), cx); - } - })?; - } - pane.update(cx, |pane, _| { - pane.set_pinned_count(self.pinned_count.min(items.len())); - })?; - - anyhow::Ok(items) - } -} - -pub type GroupId = i64; -pub type PaneId = i64; -pub type ItemId = u64; - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct SerializedItem { - pub kind: Arc, - pub item_id: ItemId, - pub active: bool, - pub preview: bool, -} - -impl SerializedItem { - pub fn new(kind: impl AsRef, item_id: ItemId, active: bool, preview: bool) -> Self { - Self { - kind: Arc::from(kind.as_ref()), - item_id, - active, - preview, - } - } -} - -#[cfg(test)] -impl Default for SerializedItem { - fn default() -> Self { - SerializedItem { - kind: Arc::from("Terminal"), - item_id: 100000, - active: false, - preview: false, - } - } -} - -impl StaticColumnCount for SerializedItem { - fn column_count() -> usize { - 4 - } -} -impl Bind for &SerializedItem { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - let next_index = statement.bind(&self.kind, start_index)?; - let next_index = statement.bind(&self.item_id, next_index)?; - let next_index = statement.bind(&self.active, next_index)?; - statement.bind(&self.preview, next_index) - } -} - -impl Column for SerializedItem { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let (kind, next_index) = Arc::::column(statement, start_index)?; - let (item_id, next_index) = ItemId::column(statement, next_index)?; - let (active, next_index) = bool::column(statement, next_index)?; - let (preview, next_index) = bool::column(statement, next_index)?; - Ok(( - SerializedItem { - kind, - item_id, - active, - preview, - }, - next_index, - )) - } -} diff --git a/crates/workspace/src/searchable.rs b/crates/workspace/src/searchable.rs deleted file mode 100644 index badfe7d243..0000000000 --- a/crates/workspace/src/searchable.rs +++ /dev/null @@ -1,461 +0,0 @@ -use std::{any::Any, sync::Arc}; - -use any_vec::AnyVec; -use gpui::{ - AnyView, AnyWeakEntity, App, Context, Entity, EventEmitter, Subscription, Task, WeakEntity, - Window, -}; -use project::search::SearchQuery; - -use crate::{ - ItemHandle, - item::{Item, WeakItemHandle}, -}; - -#[derive(Clone, Debug)] -pub enum SearchEvent { - MatchesInvalidated, - ActiveMatchChanged, -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)] -pub enum Direction { - Prev, - #[default] - Next, -} - -impl Direction { - pub fn opposite(&self) -> Self { - match self { - Direction::Prev => Direction::Next, - Direction::Next => Direction::Prev, - } - } -} - -#[derive(Clone, Copy, Debug, Default)] -pub struct SearchOptions { - pub case: bool, - pub word: bool, - pub regex: bool, - /// Specifies whether the supports search & replace. - pub replacement: bool, - pub selection: bool, - pub find_in_results: bool, -} - -// Whether to always select the current selection (even if empty) -// or to use the default (restoring the previous search ranges if some, -// otherwise using the whole file). -#[derive(Clone, Copy, Debug, Default, PartialEq)] -pub enum FilteredSearchRange { - Selection, - #[default] - Default, -} - -pub trait SearchableItem: Item + EventEmitter { - type Match: Any + Sync + Send + Clone; - - fn supported_options(&self) -> SearchOptions { - SearchOptions { - case: true, - word: true, - regex: true, - replacement: true, - selection: true, - find_in_results: false, - } - } - - fn search_bar_visibility_changed( - &mut self, - _visible: bool, - _window: &mut Window, - _cx: &mut Context, - ) { - } - - fn has_filtered_search_ranges(&mut self) -> bool { - self.supported_options().selection - } - - fn toggle_filtered_search_ranges( - &mut self, - _enabled: Option, - _window: &mut Window, - _cx: &mut Context, - ) { - } - - fn get_matches(&self, _window: &mut Window, _: &mut App) -> Vec { - Vec::new() - } - fn clear_matches(&mut self, window: &mut Window, cx: &mut Context); - fn update_matches( - &mut self, - matches: &[Self::Match], - active_match_index: Option, - window: &mut Window, - cx: &mut Context, - ); - fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context) -> String; - fn activate_match( - &mut self, - index: usize, - matches: &[Self::Match], - window: &mut Window, - cx: &mut Context, - ); - fn select_matches( - &mut self, - matches: &[Self::Match], - window: &mut Window, - cx: &mut Context, - ); - fn replace( - &mut self, - _: &Self::Match, - _: &SearchQuery, - _window: &mut Window, - _: &mut Context, - ); - fn replace_all( - &mut self, - matches: &mut dyn Iterator, - query: &SearchQuery, - window: &mut Window, - cx: &mut Context, - ) { - for item in matches { - self.replace(item, query, window, cx); - } - } - fn match_index_for_direction( - &mut self, - matches: &[Self::Match], - current_index: usize, - direction: Direction, - count: usize, - _window: &mut Window, - _: &mut Context, - ) -> usize { - match direction { - Direction::Prev => { - let count = count % matches.len(); - if current_index >= count { - current_index - count - } else { - matches.len() - (count - current_index) - } - } - Direction::Next => (current_index + count) % matches.len(), - } - } - fn find_matches( - &mut self, - query: Arc, - window: &mut Window, - cx: &mut Context, - ) -> Task>; - fn active_match_index( - &mut self, - direction: Direction, - matches: &[Self::Match], - window: &mut Window, - cx: &mut Context, - ) -> Option; - fn set_search_is_case_sensitive(&mut self, _: Option, _: &mut Context) {} -} - -pub trait SearchableItemHandle: ItemHandle { - fn downgrade(&self) -> Box; - fn boxed_clone(&self) -> Box; - fn supported_options(&self, cx: &App) -> SearchOptions; - fn subscribe_to_search_events( - &self, - window: &mut Window, - cx: &mut App, - handler: Box, - ) -> Subscription; - fn clear_matches(&self, window: &mut Window, cx: &mut App); - fn update_matches( - &self, - matches: &AnyVec, - active_match_index: Option, - window: &mut Window, - cx: &mut App, - ); - fn query_suggestion(&self, window: &mut Window, cx: &mut App) -> String; - fn activate_match( - &self, - index: usize, - matches: &AnyVec, - window: &mut Window, - cx: &mut App, - ); - fn select_matches(&self, matches: &AnyVec, window: &mut Window, cx: &mut App); - fn replace( - &self, - _: any_vec::element::ElementRef<'_, dyn Send>, - _: &SearchQuery, - _window: &mut Window, - _: &mut App, - ); - fn replace_all( - &self, - matches: &mut dyn Iterator>, - query: &SearchQuery, - window: &mut Window, - cx: &mut App, - ); - fn match_index_for_direction( - &self, - matches: &AnyVec, - current_index: usize, - direction: Direction, - count: usize, - window: &mut Window, - cx: &mut App, - ) -> usize; - fn find_matches( - &self, - query: Arc, - window: &mut Window, - cx: &mut App, - ) -> Task>; - fn active_match_index( - &self, - direction: Direction, - matches: &AnyVec, - window: &mut Window, - cx: &mut App, - ) -> Option; - fn search_bar_visibility_changed(&self, visible: bool, window: &mut Window, cx: &mut App); - - fn toggle_filtered_search_ranges( - &mut self, - enabled: Option, - window: &mut Window, - cx: &mut App, - ); - - fn set_search_is_case_sensitive(&self, is_case_sensitive: Option, cx: &mut App); -} - -impl SearchableItemHandle for Entity { - fn downgrade(&self) -> Box { - Box::new(self.downgrade()) - } - - fn boxed_clone(&self) -> Box { - Box::new(self.clone()) - } - - fn supported_options(&self, cx: &App) -> SearchOptions { - self.read(cx).supported_options() - } - - fn subscribe_to_search_events( - &self, - window: &mut Window, - cx: &mut App, - handler: Box, - ) -> Subscription { - window.subscribe(self, cx, move |_, event: &SearchEvent, window, cx| { - handler(event, window, cx) - }) - } - - fn clear_matches(&self, window: &mut Window, cx: &mut App) { - self.update(cx, |this, cx| this.clear_matches(window, cx)); - } - fn update_matches( - &self, - matches: &AnyVec, - active_match_index: Option, - window: &mut Window, - cx: &mut App, - ) { - let matches = matches.downcast_ref().unwrap(); - self.update(cx, |this, cx| { - this.update_matches(matches.as_slice(), active_match_index, window, cx) - }); - } - fn query_suggestion(&self, window: &mut Window, cx: &mut App) -> String { - self.update(cx, |this, cx| this.query_suggestion(window, cx)) - } - fn activate_match( - &self, - index: usize, - matches: &AnyVec, - window: &mut Window, - cx: &mut App, - ) { - let matches = matches.downcast_ref().unwrap(); - self.update(cx, |this, cx| { - this.activate_match(index, matches.as_slice(), window, cx) - }); - } - - fn select_matches(&self, matches: &AnyVec, window: &mut Window, cx: &mut App) { - let matches = matches.downcast_ref().unwrap(); - self.update(cx, |this, cx| { - this.select_matches(matches.as_slice(), window, cx) - }); - } - - fn match_index_for_direction( - &self, - matches: &AnyVec, - current_index: usize, - direction: Direction, - count: usize, - window: &mut Window, - cx: &mut App, - ) -> usize { - let matches = matches.downcast_ref().unwrap(); - self.update(cx, |this, cx| { - this.match_index_for_direction( - matches.as_slice(), - current_index, - direction, - count, - window, - cx, - ) - }) - } - fn find_matches( - &self, - query: Arc, - window: &mut Window, - cx: &mut App, - ) -> Task> { - let matches = self.update(cx, |this, cx| this.find_matches(query, window, cx)); - window.spawn(cx, async |_| { - let matches = matches.await; - let mut any_matches = AnyVec::with_capacity::(matches.len()); - { - let mut any_matches = any_matches.downcast_mut::().unwrap(); - for mat in matches { - any_matches.push(mat); - } - } - any_matches - }) - } - fn active_match_index( - &self, - direction: Direction, - matches: &AnyVec, - window: &mut Window, - cx: &mut App, - ) -> Option { - let matches = matches.downcast_ref()?; - self.update(cx, |this, cx| { - this.active_match_index(direction, matches.as_slice(), window, cx) - }) - } - - fn replace( - &self, - mat: any_vec::element::ElementRef<'_, dyn Send>, - query: &SearchQuery, - window: &mut Window, - cx: &mut App, - ) { - let mat = mat.downcast_ref().unwrap(); - self.update(cx, |this, cx| this.replace(mat, query, window, cx)) - } - - fn replace_all( - &self, - matches: &mut dyn Iterator>, - query: &SearchQuery, - window: &mut Window, - cx: &mut App, - ) { - self.update(cx, |this, cx| { - this.replace_all( - &mut matches.map(|m| m.downcast_ref().unwrap()), - query, - window, - cx, - ); - }) - } - - fn search_bar_visibility_changed(&self, visible: bool, window: &mut Window, cx: &mut App) { - self.update(cx, |this, cx| { - this.search_bar_visibility_changed(visible, window, cx) - }); - } - - fn toggle_filtered_search_ranges( - &mut self, - enabled: Option, - window: &mut Window, - cx: &mut App, - ) { - self.update(cx, |this, cx| { - this.toggle_filtered_search_ranges(enabled, window, cx) - }); - } - fn set_search_is_case_sensitive(&self, enabled: Option, cx: &mut App) { - self.update(cx, |this, cx| { - this.set_search_is_case_sensitive(enabled, cx) - }); - } -} - -impl From> for AnyView { - fn from(this: Box) -> Self { - this.to_any_view() - } -} - -impl From<&Box> for AnyView { - fn from(this: &Box) -> Self { - this.to_any_view() - } -} - -impl PartialEq for Box { - fn eq(&self, other: &Self) -> bool { - self.item_id() == other.item_id() - } -} - -impl Eq for Box {} - -pub trait WeakSearchableItemHandle: WeakItemHandle { - fn upgrade(&self, cx: &App) -> Option>; - - fn into_any(self) -> AnyWeakEntity; -} - -impl WeakSearchableItemHandle for WeakEntity { - fn upgrade(&self, _cx: &App) -> Option> { - Some(Box::new(self.upgrade()?)) - } - - fn into_any(self) -> AnyWeakEntity { - self.into() - } -} - -impl PartialEq for Box { - fn eq(&self, other: &Self) -> bool { - self.id() == other.id() - } -} - -impl Eq for Box {} - -impl std::hash::Hash for Box { - fn hash(&self, state: &mut H) { - self.id().hash(state) - } -} diff --git a/crates/workspace/src/shared_screen.rs b/crates/workspace/src/shared_screen.rs deleted file mode 100644 index 3c009f613e..0000000000 --- a/crates/workspace/src/shared_screen.rs +++ /dev/null @@ -1,136 +0,0 @@ -use crate::{ - ItemNavHistory, WorkspaceId, - item::{Item, ItemEvent}, -}; -use call::{RemoteVideoTrack, RemoteVideoTrackView, Room}; -use client::{User, proto::PeerId}; -use gpui::{ - AppContext as _, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, - ParentElement, Render, SharedString, Styled, Task, div, -}; -use std::sync::Arc; -use ui::{Icon, IconName, prelude::*}; - -pub enum Event { - Close, -} - -pub struct SharedScreen { - pub peer_id: PeerId, - user: Arc, - nav_history: Option, - view: Entity, - focus: FocusHandle, -} - -impl SharedScreen { - pub fn new( - track: RemoteVideoTrack, - peer_id: PeerId, - user: Arc, - room: Entity, - window: &mut Window, - cx: &mut Context, - ) -> Self { - let my_sid = track.sid(); - cx.subscribe(&room, move |_, _, ev, cx| { - if let call::room::Event::RemoteVideoTrackUnsubscribed { sid } = ev - && sid == &my_sid - { - cx.emit(Event::Close) - } - }) - .detach(); - - let view = cx.new(|cx| RemoteVideoTrackView::new(track.clone(), window, cx)); - cx.subscribe(&view, |_, _, ev, cx| match ev { - call::RemoteVideoTrackViewEvent::Close => cx.emit(Event::Close), - }) - .detach(); - Self { - view, - peer_id, - user, - nav_history: Default::default(), - focus: cx.focus_handle(), - } - } -} - -impl EventEmitter for SharedScreen {} - -impl Focusable for SharedScreen { - fn focus_handle(&self, _: &App) -> FocusHandle { - self.focus.clone() - } -} -impl Render for SharedScreen { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - div() - .bg(cx.theme().colors().editor_background) - .track_focus(&self.focus) - .key_context("SharedScreen") - .size_full() - .child(self.view.clone()) - } -} - -impl Item for SharedScreen { - type Event = Event; - - fn tab_tooltip_text(&self, _: &App) -> Option { - Some(format!("{}'s screen", self.user.github_login).into()) - } - - fn deactivated(&mut self, _window: &mut Window, cx: &mut Context) { - if let Some(nav_history) = self.nav_history.as_mut() { - nav_history.push::<()>(None, cx); - } - } - - fn tab_icon(&self, _window: &Window, _cx: &App) -> Option { - Some(Icon::new(IconName::Screen)) - } - - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { - format!("{}'s screen", self.user.github_login).into() - } - - fn telemetry_event_text(&self) -> Option<&'static str> { - None - } - - fn set_nav_history( - &mut self, - history: ItemNavHistory, - _window: &mut Window, - _cx: &mut Context, - ) { - self.nav_history = Some(history); - } - - fn can_split(&self) -> bool { - true - } - - fn clone_on_split( - &self, - _workspace_id: Option, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - Task::ready(Some(cx.new(|cx| Self { - view: self.view.update(cx, |view, cx| view.clone(window, cx)), - peer_id: self.peer_id, - user: self.user.clone(), - nav_history: Default::default(), - focus: cx.focus_handle(), - }))) - } - - fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { - match event { - Event::Close => f(ItemEvent::CloseItem), - } - } -} diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs deleted file mode 100644 index 9087cbba42..0000000000 --- a/crates/workspace/src/status_bar.rs +++ /dev/null @@ -1,223 +0,0 @@ -use crate::{ItemHandle, Pane}; -use gpui::{ - AnyView, App, Context, Decorations, Entity, IntoElement, ParentElement, Render, Styled, - Subscription, Window, -}; -use std::any::TypeId; -use theme::CLIENT_SIDE_DECORATION_ROUNDING; -use ui::{h_flex, prelude::*}; -use util::ResultExt; - -pub trait StatusItemView: Render { - /// Event callback that is triggered when the active pane item changes. - fn set_active_pane_item( - &mut self, - active_pane_item: Option<&dyn crate::ItemHandle>, - window: &mut Window, - cx: &mut Context, - ); -} - -trait StatusItemViewHandle: Send { - fn to_any(&self) -> AnyView; - fn set_active_pane_item( - &self, - active_pane_item: Option<&dyn ItemHandle>, - window: &mut Window, - cx: &mut App, - ); - fn item_type(&self) -> TypeId; -} - -pub struct StatusBar { - left_items: Vec>, - right_items: Vec>, - active_pane: Entity, - _observe_active_pane: Subscription, -} - -impl Render for StatusBar { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - h_flex() - .w_full() - .justify_between() - .gap(DynamicSpacing::Base08.rems(cx)) - .py(DynamicSpacing::Base04.rems(cx)) - .px(DynamicSpacing::Base06.rems(cx)) - .bg(cx.theme().colors().status_bar_background) - .map(|el| match window.window_decorations() { - Decorations::Server => el, - Decorations::Client { tiling, .. } => el - .when(!(tiling.bottom || tiling.right), |el| { - el.rounded_br(CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.bottom || tiling.left), |el| { - el.rounded_bl(CLIENT_SIDE_DECORATION_ROUNDING) - }) - // This border is to avoid a transparent gap in the rounded corners - .mb(px(-1.)) - .border_b(px(1.0)) - .border_color(cx.theme().colors().status_bar_background), - }) - .child(self.render_left_tools()) - .child(self.render_right_tools()) - } -} - -impl StatusBar { - fn render_left_tools(&self) -> impl IntoElement { - h_flex() - .gap_1() - .overflow_x_hidden() - .children(self.left_items.iter().map(|item| item.to_any())) - } - - fn render_right_tools(&self) -> impl IntoElement { - h_flex() - .gap_1() - .overflow_x_hidden() - .children(self.right_items.iter().rev().map(|item| item.to_any())) - } -} - -impl StatusBar { - pub fn new(active_pane: &Entity, window: &mut Window, cx: &mut Context) -> Self { - let mut this = Self { - left_items: Default::default(), - right_items: Default::default(), - active_pane: active_pane.clone(), - _observe_active_pane: cx.observe_in(active_pane, window, |this, _, window, cx| { - this.update_active_pane_item(window, cx) - }), - }; - this.update_active_pane_item(window, cx); - this - } - - pub fn add_left_item(&mut self, item: Entity, window: &mut Window, cx: &mut Context) - where - T: 'static + StatusItemView, - { - let active_pane_item = self.active_pane.read(cx).active_item(); - item.set_active_pane_item(active_pane_item.as_deref(), window, cx); - - self.left_items.push(Box::new(item)); - cx.notify(); - } - - pub fn item_of_type(&self) -> Option> { - self.left_items - .iter() - .chain(self.right_items.iter()) - .find_map(|item| item.to_any().downcast().log_err()) - } - - pub fn position_of_item(&self) -> Option - where - T: StatusItemView, - { - for (index, item) in self.left_items.iter().enumerate() { - if item.item_type() == TypeId::of::() { - return Some(index); - } - } - for (index, item) in self.right_items.iter().enumerate() { - if item.item_type() == TypeId::of::() { - return Some(index + self.left_items.len()); - } - } - None - } - - pub fn insert_item_after( - &mut self, - position: usize, - item: Entity, - window: &mut Window, - cx: &mut Context, - ) where - T: 'static + StatusItemView, - { - let active_pane_item = self.active_pane.read(cx).active_item(); - item.set_active_pane_item(active_pane_item.as_deref(), window, cx); - - if position < self.left_items.len() { - self.left_items.insert(position + 1, Box::new(item)) - } else { - self.right_items - .insert(position + 1 - self.left_items.len(), Box::new(item)) - } - cx.notify() - } - - pub fn remove_item_at(&mut self, position: usize, cx: &mut Context) { - if position < self.left_items.len() { - self.left_items.remove(position); - } else { - self.right_items.remove(position - self.left_items.len()); - } - cx.notify(); - } - - pub fn add_right_item( - &mut self, - item: Entity, - window: &mut Window, - cx: &mut Context, - ) where - T: 'static + StatusItemView, - { - let active_pane_item = self.active_pane.read(cx).active_item(); - item.set_active_pane_item(active_pane_item.as_deref(), window, cx); - - self.right_items.push(Box::new(item)); - cx.notify(); - } - - pub fn set_active_pane( - &mut self, - active_pane: &Entity, - window: &mut Window, - cx: &mut Context, - ) { - self.active_pane = active_pane.clone(); - self._observe_active_pane = cx.observe_in(active_pane, window, |this, _, window, cx| { - this.update_active_pane_item(window, cx) - }); - self.update_active_pane_item(window, cx); - } - - fn update_active_pane_item(&mut self, window: &mut Window, cx: &mut Context) { - let active_pane_item = self.active_pane.read(cx).active_item(); - for item in self.left_items.iter().chain(&self.right_items) { - item.set_active_pane_item(active_pane_item.as_deref(), window, cx); - } - } -} - -impl StatusItemViewHandle for Entity { - fn to_any(&self) -> AnyView { - self.clone().into() - } - - fn set_active_pane_item( - &self, - active_pane_item: Option<&dyn ItemHandle>, - window: &mut Window, - cx: &mut App, - ) { - self.update(cx, |this, cx| { - this.set_active_pane_item(active_pane_item, window, cx) - }); - } - - fn item_type(&self) -> TypeId { - TypeId::of::() - } -} - -impl From<&dyn StatusItemViewHandle> for AnyView { - fn from(val: &dyn StatusItemViewHandle) -> Self { - val.to_any() - } -} diff --git a/crates/workspace/src/tasks.rs b/crates/workspace/src/tasks.rs deleted file mode 100644 index 5f52cb49e7..0000000000 --- a/crates/workspace/src/tasks.rs +++ /dev/null @@ -1,134 +0,0 @@ -use std::process::ExitStatus; - -use anyhow::Result; -use gpui::{AppContext, Context, Entity, Task}; -use language::Buffer; -use project::{TaskSourceKind, WorktreeId}; -use remote::ConnectionState; -use task::{DebugScenario, ResolvedTask, SpawnInTerminal, TaskContext, TaskTemplate}; -use ui::Window; - -use crate::{Toast, Workspace, notifications::NotificationId}; - -impl Workspace { - pub fn schedule_task( - self: &mut Workspace, - task_source_kind: TaskSourceKind, - task_to_resolve: &TaskTemplate, - task_cx: &TaskContext, - omit_history: bool, - window: &mut Window, - cx: &mut Context, - ) { - match self.project.read(cx).remote_connection_state(cx) { - None | Some(ConnectionState::Connected) => {} - Some( - ConnectionState::Connecting - | ConnectionState::Disconnected - | ConnectionState::HeartbeatMissed - | ConnectionState::Reconnecting, - ) => { - log::warn!("Cannot schedule tasks when disconnected from a remote host"); - return; - } - } - - if let Some(spawn_in_terminal) = - task_to_resolve.resolve_task(&task_source_kind.to_id_base(), task_cx) - { - self.schedule_resolved_task( - task_source_kind, - spawn_in_terminal, - omit_history, - window, - cx, - ); - } - } - - pub fn schedule_resolved_task( - self: &mut Workspace, - task_source_kind: TaskSourceKind, - resolved_task: ResolvedTask, - omit_history: bool, - window: &mut Window, - cx: &mut Context, - ) { - let spawn_in_terminal = resolved_task.resolved.clone(); - if !omit_history { - if let Some(debugger_provider) = self.debugger_provider.as_ref() { - debugger_provider.task_scheduled(cx); - } - - self.project().update(cx, |project, cx| { - if let Some(task_inventory) = - project.task_store().read(cx).task_inventory().cloned() - { - task_inventory.update(cx, |inventory, _| { - inventory.task_scheduled(task_source_kind, resolved_task); - }) - } - }); - } - - if let Some(terminal_provider) = self.terminal_provider.as_ref() { - let task_status = terminal_provider.spawn(spawn_in_terminal, window, cx); - - let task = cx.spawn(async |w, cx| { - let res = cx.background_spawn(task_status).await; - match res { - Some(Ok(status)) => { - if status.success() { - log::debug!("Task spawn succeeded"); - } else { - log::debug!("Task spawn failed, code: {:?}", status.code()); - } - } - Some(Err(e)) => { - log::error!("Task spawn failed: {e:#}"); - _ = w.update(cx, |w, cx| { - let id = NotificationId::unique::(); - w.show_toast(Toast::new(id, format!("Task spawn failed: {e}")), cx); - }) - } - None => log::debug!("Task spawn got cancelled"), - }; - }); - self.scheduled_tasks.push(task); - } - } - - pub fn start_debug_session( - &mut self, - scenario: DebugScenario, - task_context: TaskContext, - active_buffer: Option>, - worktree_id: Option, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(provider) = self.debugger_provider.as_mut() { - provider.start_session( - scenario, - task_context, - active_buffer, - worktree_id, - window, - cx, - ) - } - } - - pub fn spawn_in_terminal( - self: &mut Workspace, - spawn_in_terminal: SpawnInTerminal, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - if let Some(terminal_provider) = self.terminal_provider.as_ref() { - terminal_provider.spawn(spawn_in_terminal, window, cx) - } else { - Task::ready(None) - } - } -} diff --git a/crates/workspace/src/theme_preview.rs b/crates/workspace/src/theme_preview.rs deleted file mode 100644 index f978da706b..0000000000 --- a/crates/workspace/src/theme_preview.rs +++ /dev/null @@ -1,429 +0,0 @@ -#![allow(unused, dead_code)] -use gpui::{ - AnyElement, App, Entity, EventEmitter, FocusHandle, Focusable, Hsla, Task, actions, hsla, -}; -use strum::IntoEnumIterator; -use theme::all_theme_colors; -use ui::{ - AudioStatus, Avatar, AvatarAudioStatusIndicator, AvatarAvailabilityIndicator, ButtonLike, - Checkbox, CollaboratorAvailability, ContentGroup, DecoratedIcon, ElevationIndex, Facepile, - IconDecoration, Indicator, KeybindingHint, Switch, TintColor, Tooltip, prelude::*, - utils::calculate_contrast_ratio, -}; - -use crate::{Item, Workspace}; - -actions!( - dev, - [ - /// Opens the theme preview window. - OpenThemePreview - ] -); - -pub fn init(cx: &mut App) { - cx.observe_new(|workspace: &mut Workspace, _, _| { - workspace.register_action(|workspace, _: &OpenThemePreview, window, cx| { - let theme_preview = cx.new(|cx| ThemePreview::new(window, cx)); - workspace.add_item_to_active_pane(Box::new(theme_preview), None, true, window, cx) - }); - }) - .detach(); -} - -#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, strum::EnumIter)] -enum ThemePreviewPage { - Overview, - Typography, -} - -impl ThemePreviewPage { - pub fn name(&self) -> &'static str { - match self { - Self::Overview => "Overview", - Self::Typography => "Typography", - } - } -} - -struct ThemePreview { - current_page: ThemePreviewPage, - focus_handle: FocusHandle, -} - -impl ThemePreview { - pub fn new(window: &mut Window, cx: &mut Context) -> Self { - Self { - current_page: ThemePreviewPage::Overview, - focus_handle: cx.focus_handle(), - } - } - - pub fn view( - &self, - page: ThemePreviewPage, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - match page { - ThemePreviewPage::Overview => self.render_overview_page(window, cx).into_any_element(), - ThemePreviewPage::Typography => { - self.render_typography_page(window, cx).into_any_element() - } - } - } -} - -impl EventEmitter<()> for ThemePreview {} - -impl Focusable for ThemePreview { - fn focus_handle(&self, _: &App) -> gpui::FocusHandle { - self.focus_handle.clone() - } -} -impl ThemePreview {} - -impl Item for ThemePreview { - type Event = (); - - fn to_item_events(_: &Self::Event, _: impl FnMut(crate::item::ItemEvent)) {} - - fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString { - let name = cx.theme().name.clone(); - format!("{} Preview", name).into() - } - - fn telemetry_event_text(&self) -> Option<&'static str> { - None - } - - fn can_split(&self) -> bool { - true - } - - fn clone_on_split( - &self, - _workspace_id: Option, - window: &mut Window, - cx: &mut Context, - ) -> Task>> - where - Self: Sized, - { - Task::ready(Some(cx.new(|cx| Self::new(window, cx)))) - } -} - -const AVATAR_URL: &str = "https://avatars.githubusercontent.com/u/1714999?v=4"; - -impl ThemePreview { - fn preview_bg(window: &mut Window, cx: &mut App) -> Hsla { - cx.theme().colors().editor_background - } - - fn render_text( - &self, - layer: ElevationIndex, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let bg = layer.bg(cx); - - let label_with_contrast = |label: &str, fg: Hsla| { - let contrast = calculate_contrast_ratio(fg, bg); - format!("{} ({:.2})", label, contrast) - }; - - v_flex() - .gap_1() - .child(Headline::new("Text").size(HeadlineSize::Small).color(Color::Muted)) - .child( - h_flex() - .items_start() - .gap_4() - .child( - v_flex() - .gap_1() - .child(Headline::new("Headline Sizes").size(HeadlineSize::Small).color(Color::Muted)) - .child(Headline::new("XLarge Headline").size(HeadlineSize::XLarge)) - .child(Headline::new("Large Headline").size(HeadlineSize::Large)) - .child(Headline::new("Medium Headline").size(HeadlineSize::Medium)) - .child(Headline::new("Small Headline").size(HeadlineSize::Small)) - .child(Headline::new("XSmall Headline").size(HeadlineSize::XSmall)), - ) - .child( - v_flex() - .gap_1() - .child(Headline::new("Text Colors").size(HeadlineSize::Small).color(Color::Muted)) - .child( - Label::new(label_with_contrast( - "Default Text", - Color::Default.color(cx), - )) - .color(Color::Default), - ) - .child( - Label::new(label_with_contrast( - "Accent Text", - Color::Accent.color(cx), - )) - .color(Color::Accent), - ) - .child( - Label::new(label_with_contrast( - "Conflict Text", - Color::Conflict.color(cx), - )) - .color(Color::Conflict), - ) - .child( - Label::new(label_with_contrast( - "Created Text", - Color::Created.color(cx), - )) - .color(Color::Created), - ) - .child( - Label::new(label_with_contrast( - "Deleted Text", - Color::Deleted.color(cx), - )) - .color(Color::Deleted), - ) - .child( - Label::new(label_with_contrast( - "Disabled Text", - Color::Disabled.color(cx), - )) - .color(Color::Disabled), - ) - .child( - Label::new(label_with_contrast( - "Error Text", - Color::Error.color(cx), - )) - .color(Color::Error), - ) - .child( - Label::new(label_with_contrast( - "Hidden Text", - Color::Hidden.color(cx), - )) - .color(Color::Hidden), - ) - .child( - Label::new(label_with_contrast( - "Hint Text", - Color::Hint.color(cx), - )) - .color(Color::Hint), - ) - .child( - Label::new(label_with_contrast( - "Ignored Text", - Color::Ignored.color(cx), - )) - .color(Color::Ignored), - ) - .child( - Label::new(label_with_contrast( - "Info Text", - Color::Info.color(cx), - )) - .color(Color::Info), - ) - .child( - Label::new(label_with_contrast( - "Modified Text", - Color::Modified.color(cx), - )) - .color(Color::Modified), - ) - .child( - Label::new(label_with_contrast( - "Muted Text", - Color::Muted.color(cx), - )) - .color(Color::Muted), - ) - .child( - Label::new(label_with_contrast( - "Placeholder Text", - Color::Placeholder.color(cx), - )) - .color(Color::Placeholder), - ) - .child( - Label::new(label_with_contrast( - "Selected Text", - Color::Selected.color(cx), - )) - .color(Color::Selected), - ) - .child( - Label::new(label_with_contrast( - "Success Text", - Color::Success.color(cx), - )) - .color(Color::Success), - ) - .child( - Label::new(label_with_contrast( - "Warning Text", - Color::Warning.color(cx), - )) - .color(Color::Warning), - ) - ) - .child( - v_flex() - .gap_1() - .child(Headline::new("Wrapping Text").size(HeadlineSize::Small).color(Color::Muted)) - .child( - div().max_w(px(200.)).child( - "This is a longer piece of text that should wrap to multiple lines. It demonstrates how text behaves when it exceeds the width of its container." - )) - ) - ) - } - - fn render_colors( - &self, - layer: ElevationIndex, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - let bg = layer.bg(cx); - let all_colors = all_theme_colors(cx); - - v_flex() - .gap_1() - .child( - Headline::new("Colors") - .size(HeadlineSize::Small) - .color(Color::Muted), - ) - .child( - h_flex() - .flex_wrap() - .gap_1() - .children(all_colors.into_iter().map(|(color, name)| { - let id = ElementId::Name(format!("{:?}-preview", color).into()); - div().size_8().flex_none().child( - ButtonLike::new(id) - .child( - div() - .size_8() - .bg(color) - .border_1() - .border_color(cx.theme().colors().border) - .overflow_hidden(), - ) - .size(ButtonSize::None) - .style(ButtonStyle::Transparent) - .tooltip(move |window, cx| { - let name = name.clone(); - Tooltip::with_meta(name, None, format!("{:?}", color), cx) - }), - ) - })), - ) - } - - fn render_theme_layer( - &self, - layer: ElevationIndex, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - v_flex() - .p_4() - .bg(layer.bg(cx)) - .text_color(cx.theme().colors().text) - .gap_2() - .child(Headline::new(layer.clone().to_string()).size(HeadlineSize::Medium)) - .child(self.render_text(layer, window, cx)) - .child(self.render_colors(layer, window, cx)) - } - - fn render_overview_page( - &self, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - v_flex() - .id("theme-preview-overview") - .overflow_scroll() - .size_full() - .child( - v_flex() - .child(Headline::new("Theme Preview").size(HeadlineSize::Large)) - .child(div().w_full().text_color(cx.theme().colors().text_muted).child("This view lets you preview a range of UI elements across a theme. Use it for testing out changes to the theme.")) - ) - .child(self.render_theme_layer(ElevationIndex::Background, window, cx)) - .child(self.render_theme_layer(ElevationIndex::Surface, window, cx)) - .child(self.render_theme_layer(ElevationIndex::EditorSurface, window, cx)) - .child(self.render_theme_layer(ElevationIndex::ElevatedSurface, window, cx)) - } - - fn render_typography_page( - &self, - window: &mut Window, - cx: &mut Context, - ) -> impl IntoElement { - v_flex() - .id("theme-preview-typography") - .overflow_scroll() - .size_full() - .child(v_flex() - .gap_4() - .child(Headline::new("Headline 1").size(HeadlineSize::XLarge)) - .child(Label::new("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.")) - .child(Headline::new("Headline 2").size(HeadlineSize::Large)) - .child(Label::new("Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.")) - .child(Headline::new("Headline 3").size(HeadlineSize::Medium)) - .child(Label::new("Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.")) - .child(Headline::new("Headline 4").size(HeadlineSize::Small)) - .child(Label::new("Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")) - .child(Headline::new("Headline 5").size(HeadlineSize::XSmall)) - .child(Label::new("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.")) - .child(Headline::new("Body Text").size(HeadlineSize::Small)) - .child(Label::new("Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.")) - ) - } - - fn render_page_nav(&self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - h_flex() - .id("theme-preview-nav") - .items_center() - .gap_4() - .py_2() - .bg(Self::preview_bg(window, cx)) - .children(ThemePreviewPage::iter().map(|p| { - Button::new(ElementId::Name(p.name().into()), p.name()) - .on_click(cx.listener(move |this, _, window, cx| { - this.current_page = p; - cx.notify(); - })) - .toggle_state(p == self.current_page) - .selected_style(ButtonStyle::Tinted(TintColor::Accent)) - })) - } -} - -impl Render for ThemePreview { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl ui::IntoElement { - v_flex() - .id("theme-preview") - .key_context("ThemePreview") - .items_start() - .overflow_hidden() - .size_full() - .max_h_full() - .track_focus(&self.focus_handle) - .px_2() - .bg(Self::preview_bg(window, cx)) - .child(self.render_page_nav(window, cx)) - .child(self.view(self.current_page, window, cx)) - } -} diff --git a/crates/workspace/src/toast_layer.rs b/crates/workspace/src/toast_layer.rs deleted file mode 100644 index 5157945548..0000000000 --- a/crates/workspace/src/toast_layer.rs +++ /dev/null @@ -1,252 +0,0 @@ -use std::{ - rc::Rc, - time::{Duration, Instant}, -}; - -use gpui::{AnyView, DismissEvent, Entity, EntityId, FocusHandle, ManagedView, Subscription, Task}; -use ui::{animation::DefaultAnimations, prelude::*}; -use zed_actions::toast; - -use crate::Workspace; - -const DEFAULT_TOAST_DURATION: Duration = Duration::from_secs(10); -const MINIMUM_RESUME_DURATION: Duration = Duration::from_millis(800); - -pub fn init(cx: &mut App) { - cx.observe_new(|workspace: &mut Workspace, _window, _cx| { - workspace.register_action(|_workspace, _: &toast::RunAction, window, cx| { - let workspace = cx.entity(); - let window = window.window_handle(); - cx.defer(move |cx| { - let action = workspace - .read(cx) - .toast_layer - .read(cx) - .active_toast - .as_ref() - .and_then(|active_toast| active_toast.action.clone()); - - if let Some(on_click) = action.and_then(|action| action.on_click) { - window - .update(cx, |_, window, cx| { - on_click(window, cx); - }) - .ok(); - } - }); - }); - }) - .detach(); -} - -pub trait ToastView: ManagedView { - fn action(&self) -> Option; -} - -#[derive(Clone)] -pub struct ToastAction { - pub id: ElementId, - pub label: SharedString, - pub on_click: Option>, -} - -impl ToastAction { - pub fn new( - label: SharedString, - on_click: Option>, - ) -> Self { - let id = ElementId::Name(label.clone()); - - Self { - id, - label, - on_click, - } - } -} - -trait ToastViewHandle { - fn view(&self) -> AnyView; -} - -impl ToastViewHandle for Entity { - fn view(&self) -> AnyView { - self.clone().into() - } -} - -pub struct ActiveToast { - id: EntityId, - toast: Box, - action: Option, - _subscriptions: [Subscription; 1], - focus_handle: FocusHandle, -} - -struct DismissTimer { - instant_started: Instant, - _task: Task<()>, -} - -pub struct ToastLayer { - active_toast: Option, - duration_remaining: Option, - dismiss_timer: Option, -} - -impl Default for ToastLayer { - fn default() -> Self { - Self::new() - } -} - -impl ToastLayer { - pub fn new() -> Self { - Self { - active_toast: None, - duration_remaining: None, - dismiss_timer: None, - } - } - - pub fn toggle_toast(&mut self, cx: &mut Context, new_toast: Entity) - where - V: ToastView, - { - if let Some(active_toast) = &self.active_toast { - let show_new = active_toast.id != new_toast.entity_id(); - self.hide_toast(cx); - if !show_new { - return; - } - } - self.show_toast(new_toast, cx); - } - - pub fn show_toast(&mut self, new_toast: Entity, cx: &mut Context) - where - V: ToastView, - { - let action = new_toast.read(cx).action(); - let focus_handle = cx.focus_handle(); - - self.active_toast = Some(ActiveToast { - _subscriptions: [cx.subscribe(&new_toast, |this, _, _: &DismissEvent, cx| { - this.hide_toast(cx); - })], - id: new_toast.entity_id(), - toast: Box::new(new_toast), - action, - focus_handle, - }); - - self.start_dismiss_timer(DEFAULT_TOAST_DURATION, cx); - - cx.notify(); - } - - pub fn hide_toast(&mut self, cx: &mut Context) { - self.active_toast.take(); - cx.notify(); - } - - pub fn active_toast(&self) -> Option> - where - V: 'static, - { - let active_toast = self.active_toast.as_ref()?; - active_toast.toast.view().downcast::().ok() - } - - pub fn has_active_toast(&self) -> bool { - self.active_toast.is_some() - } - - fn pause_dismiss_timer(&mut self) { - let Some(dismiss_timer) = self.dismiss_timer.take() else { - return; - }; - let Some(duration_remaining) = self.duration_remaining.as_mut() else { - return; - }; - *duration_remaining = - duration_remaining.saturating_sub(dismiss_timer.instant_started.elapsed()); - if *duration_remaining < MINIMUM_RESUME_DURATION { - *duration_remaining = MINIMUM_RESUME_DURATION; - } - } - - /// Starts a timer to automatically dismiss the toast after the specified duration - pub fn start_dismiss_timer(&mut self, duration: Duration, cx: &mut Context) { - self.clear_dismiss_timer(cx); - - let instant_started = std::time::Instant::now(); - let task = cx.spawn(async move |this, cx| { - cx.background_executor().timer(duration).await; - - if let Some(this) = this.upgrade() { - this.update(cx, |this, cx| this.hide_toast(cx)).ok(); - } - }); - - self.duration_remaining = Some(duration); - self.dismiss_timer = Some(DismissTimer { - instant_started, - _task: task, - }); - cx.notify(); - } - - /// Restarts the dismiss timer with a new duration - pub fn restart_dismiss_timer(&mut self, cx: &mut Context) { - let Some(duration) = self.duration_remaining else { - return; - }; - self.start_dismiss_timer(duration, cx); - cx.notify(); - } - - /// Clears the dismiss timer if one exists - pub fn clear_dismiss_timer(&mut self, cx: &mut Context) { - self.dismiss_timer.take(); - cx.notify(); - } -} - -impl Render for ToastLayer { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let Some(active_toast) = &self.active_toast else { - return div(); - }; - - div().absolute().size_full().bottom_0().left_0().child( - v_flex() - .id(("toast-layer-container", active_toast.id)) - .absolute() - .w_full() - .bottom(px(0.)) - .flex() - .flex_col() - .items_center() - .track_focus(&active_toast.focus_handle) - .child( - h_flex() - .id("active-toast-container") - .occlude() - .on_hover(cx.listener(|this, hover_start, _window, cx| { - if *hover_start { - this.pause_dismiss_timer(); - } else { - this.restart_dismiss_timer(cx); - } - cx.stop_propagation(); - })) - .on_click(|_, _, cx| { - cx.stop_propagation(); - }) - .child(active_toast.toast.view()), - ) - .animate_in(AnimationDirection::FromBottom, true), - ) - } -} diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs deleted file mode 100644 index 6e26be6dc7..0000000000 --- a/crates/workspace/src/toolbar.rs +++ /dev/null @@ -1,282 +0,0 @@ -use crate::ItemHandle; -use gpui::{ - AnyView, App, Context, Entity, EntityId, EventEmitter, KeyContext, ParentElement as _, Render, - Styled, Window, -}; -use ui::prelude::*; -use ui::{h_flex, v_flex}; - -pub enum ToolbarItemEvent { - ChangeLocation(ToolbarItemLocation), -} - -pub trait ToolbarItemView: Render + EventEmitter { - fn set_active_pane_item( - &mut self, - active_pane_item: Option<&dyn crate::ItemHandle>, - window: &mut Window, - cx: &mut Context, - ) -> ToolbarItemLocation; - - fn pane_focus_update( - &mut self, - _pane_focused: bool, - _window: &mut Window, - _cx: &mut Context, - ) { - } - - fn contribute_context(&self, _context: &mut KeyContext, _cx: &App) {} -} - -trait ToolbarItemViewHandle: Send { - fn id(&self) -> EntityId; - fn to_any(&self) -> AnyView; - fn set_active_pane_item( - &self, - active_pane_item: Option<&dyn ItemHandle>, - window: &mut Window, - cx: &mut App, - ) -> ToolbarItemLocation; - fn focus_changed(&mut self, pane_focused: bool, window: &mut Window, cx: &mut App); - fn contribute_context(&self, context: &mut KeyContext, cx: &App); -} - -#[derive(Copy, Clone, Debug, PartialEq)] -pub enum ToolbarItemLocation { - Hidden, - PrimaryLeft, - PrimaryRight, - Secondary, -} - -pub struct Toolbar { - active_item: Option>, - hidden: bool, - can_navigate: bool, - items: Vec<(Box, ToolbarItemLocation)>, -} - -impl Toolbar { - fn has_any_visible_items(&self) -> bool { - self.items - .iter() - .any(|(_item, location)| *location != ToolbarItemLocation::Hidden) - } - - fn left_items(&self) -> impl Iterator { - self.items.iter().filter_map(|(item, location)| { - if *location == ToolbarItemLocation::PrimaryLeft { - Some(item.as_ref()) - } else { - None - } - }) - } - - fn right_items(&self) -> impl Iterator { - self.items.iter().filter_map(|(item, location)| { - if *location == ToolbarItemLocation::PrimaryRight { - Some(item.as_ref()) - } else { - None - } - }) - } - - fn secondary_items(&self) -> impl Iterator { - self.items.iter().rev().filter_map(|(item, location)| { - if *location == ToolbarItemLocation::Secondary { - Some(item.as_ref()) - } else { - None - } - }) - } -} - -impl Render for Toolbar { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - if !self.has_any_visible_items() { - return div(); - } - - let secondary_items = self.secondary_items().map(|item| item.to_any()); - - let has_left_items = self.left_items().count() > 0; - let has_right_items = self.right_items().count() > 0; - - v_flex() - .group("toolbar") - .relative() - .p(DynamicSpacing::Base08.rems(cx)) - .when(has_left_items || has_right_items, |this| { - this.gap(DynamicSpacing::Base08.rems(cx)) - }) - .border_b_1() - .border_color(cx.theme().colors().border_variant) - .bg(cx.theme().colors().toolbar_background) - .when(has_left_items || has_right_items, |this| { - this.child( - h_flex() - .min_h_6() - .justify_between() - .gap(DynamicSpacing::Base08.rems(cx)) - .when(has_left_items, |this| { - this.child( - h_flex() - .flex_auto() - .justify_start() - .overflow_x_hidden() - .children(self.left_items().map(|item| item.to_any())), - ) - }) - .when(has_right_items, |this| { - this.child( - h_flex() - .h_full() - .flex_row_reverse() - .map(|el| { - if has_left_items { - // We're using `flex_none` here to prevent some flickering that can occur when the - // size of the left items container changes. - el.flex_none() - } else { - el.flex_auto() - } - }) - .justify_end() - .children(self.right_items().map(|item| item.to_any())), - ) - }), - ) - }) - .children(secondary_items) - } -} - -impl Default for Toolbar { - fn default() -> Self { - Self::new() - } -} - -impl Toolbar { - pub fn new() -> Self { - Self { - active_item: None, - items: Default::default(), - hidden: false, - can_navigate: true, - } - } - - pub fn set_can_navigate(&mut self, can_navigate: bool, cx: &mut Context) { - self.can_navigate = can_navigate; - cx.notify(); - } - - pub fn add_item(&mut self, item: Entity, window: &mut Window, cx: &mut Context) - where - T: 'static + ToolbarItemView, - { - let location = item.set_active_pane_item(self.active_item.as_deref(), window, cx); - cx.subscribe(&item, |this, item, event, cx| { - if let Some((_, current_location)) = this - .items - .iter_mut() - .find(|(i, _)| i.id() == item.entity_id()) - { - match event { - ToolbarItemEvent::ChangeLocation(new_location) => { - if new_location != current_location { - *current_location = *new_location; - cx.notify(); - } - } - } - } - }) - .detach(); - self.items.push((Box::new(item), location)); - cx.notify(); - } - - pub fn set_active_item( - &mut self, - item: Option<&dyn ItemHandle>, - window: &mut Window, - cx: &mut Context, - ) { - self.active_item = item.map(|item| item.boxed_clone()); - self.hidden = self - .active_item - .as_ref() - .map(|item| !item.show_toolbar(cx)) - .unwrap_or(false); - - for (toolbar_item, current_location) in self.items.iter_mut() { - let new_location = toolbar_item.set_active_pane_item(item, window, cx); - if new_location != *current_location { - *current_location = new_location; - cx.notify(); - } - } - } - - pub fn focus_changed(&mut self, focused: bool, window: &mut Window, cx: &mut Context) { - for (toolbar_item, _) in self.items.iter_mut() { - toolbar_item.focus_changed(focused, window, cx); - } - } - - pub fn item_of_type(&self) -> Option> { - self.items - .iter() - .find_map(|(item, _)| item.to_any().downcast().ok()) - } - - pub fn hidden(&self) -> bool { - self.hidden - } - - pub fn contribute_context(&self, context: &mut KeyContext, cx: &App) { - for (item, location) in &self.items { - if *location != ToolbarItemLocation::Hidden { - item.contribute_context(context, cx); - } - } - } -} - -impl ToolbarItemViewHandle for Entity { - fn id(&self) -> EntityId { - self.entity_id() - } - - fn to_any(&self) -> AnyView { - self.clone().into() - } - - fn set_active_pane_item( - &self, - active_pane_item: Option<&dyn ItemHandle>, - window: &mut Window, - cx: &mut App, - ) -> ToolbarItemLocation { - self.update(cx, |this, cx| { - this.set_active_pane_item(active_pane_item, window, cx) - }) - } - - fn focus_changed(&mut self, pane_focused: bool, window: &mut Window, cx: &mut App) { - self.update(cx, |this, cx| { - this.pane_focus_update(pane_focused, window, cx); - cx.notify(); - }); - } - - fn contribute_context(&self, context: &mut KeyContext, cx: &App) { - self.read(cx).contribute_context(context, cx) - } -} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs deleted file mode 100644 index d2a9ef71fc..0000000000 --- a/crates/workspace/src/workspace.rs +++ /dev/null @@ -1,11487 +0,0 @@ -pub mod dock; -pub mod history_manager; -pub mod invalid_item_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; -mod status_bar; -pub mod tasks; -mod theme_preview; -mod toast_layer; -mod toolbar; -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}; -use call::{ActiveCall, call_settings::CallSettings}; -use client::{ - ChannelId, Client, ErrorExt, Status, TypedEnvelope, UserStore, - proto::{self, ErrorCode, PanelId, PeerId}, -}; -use collections::{HashMap, HashSet, hash_map}; -use dock::{Dock, DockPosition, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE}; -use futures::{ - Future, FutureExt, StreamExt, - channel::{ - mpsc::{self, UnboundedReceiver, UnboundedSender}, - oneshot, - }, - future::{Shared, try_join_all}, -}; -use gpui::{ - Action, AnyEntity, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Bounds, Context, - CursorStyle, Decorations, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, - Focusable, Global, HitboxBehavior, Hsla, KeyContext, Keystroke, ManagedView, MouseButton, - PathPromptOptions, Point, PromptLevel, Render, ResizeEdge, Size, Stateful, Subscription, - SystemWindowTabController, Task, Tiling, WeakEntity, WindowBounds, WindowHandle, WindowId, - WindowOptions, actions, canvas, point, relative, size, transparent_black, -}; -pub use history_manager::*; -pub use item::{ - FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings, - ProjectItem, SerializableItem, SerializableItemHandle, WeakItemHandle, -}; -use itertools::Itertools; -use language::{Buffer, LanguageRegistry, Rope, language_settings::all_language_settings}; -pub use modal_layer::*; -use node_runtime::NodeRuntime; -use notifications::{ - DetachAndPromptErr, Notifications, dismiss_app_notification, - simple_message_notification::MessageNotification, -}; -pub use pane::*; -pub use pane_group::{ - ActivePaneDecorator, HANDLE_HITBOX_SIZE, Member, PaneAxis, PaneGroup, PaneRenderContext, - SplitDirection, -}; -use persistence::{DB, SerializedWindowBounds, model::SerializedWorkspace}; -pub use persistence::{ - DB as WORKSPACE_DB, WorkspaceDb, delete_unloaded_items, - model::{ItemId, SerializedWorkspaceLocation}, -}; -use postage::stream::Stream; -use project::{ - DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId, - WorktreeSettings, - debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus}, - toolchain_store::ToolchainStoreEvent, -}; -use remote::{ - RemoteClientDelegate, RemoteConnection, RemoteConnectionOptions, - remote_client::ConnectionIdentifier, -}; -use schemars::JsonSchema; -use serde::Deserialize; -use session::AppSession; -use settings::{CenteredPaddingSettings, Settings, SettingsLocation, update_settings_file}; -use shared_screen::SharedScreen; -use sqlez::{ - bindable::{Bind, Column, StaticColumnCount}, - statement::Statement, -}; -use status_bar::StatusBar; -pub use status_bar::StatusItemView; -use std::{ - any::TypeId, - borrow::Cow, - cell::RefCell, - cmp, - collections::{VecDeque, hash_map::DefaultHasher}, - env, - hash::{Hash, Hasher}, - path::{Path, PathBuf}, - process::ExitStatus, - rc::Rc, - sync::{ - Arc, LazyLock, Weak, - atomic::{AtomicBool, AtomicUsize}, - }, - time::Duration, -}; -use task::{DebugScenario, SpawnInTerminal, TaskContext}; -use theme::{ActiveTheme, GlobalTheme, SystemAppearance, ThemeSettings}; -pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; -pub use ui; -use ui::{Window, prelude::*}; -use util::{ - ResultExt, TryFutureExt, - paths::{PathStyle, SanitizedPath}, - rel_path::RelPath, - serde::default_true, -}; -use uuid::Uuid; -pub use workspace_settings::{ - AutosaveSetting, BottomDockLayout, RestoreOnStartupBehavior, StatusBarSettings, TabBarSettings, - WorkspaceSettings, -}; -use zed_actions::{Spawn, feedback::FileBugReport}; - -use crate::persistence::{ - SerializedAxis, - model::{DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup}, -}; -use crate::{item::ItemBufferKind, notifications::NotificationId}; - -pub const SERIALIZATION_THROTTLE_TIME: Duration = Duration::from_millis(200); - -static ZED_WINDOW_SIZE: LazyLock>> = LazyLock::new(|| { - env::var("ZED_WINDOW_SIZE") - .ok() - .as_deref() - .and_then(parse_pixel_size_env_var) -}); - -static ZED_WINDOW_POSITION: LazyLock>> = LazyLock::new(|| { - env::var("ZED_WINDOW_POSITION") - .ok() - .as_deref() - .and_then(parse_pixel_position_env_var) -}); - -pub trait TerminalProvider { - fn spawn( - &self, - task: SpawnInTerminal, - window: &mut Window, - cx: &mut App, - ) -> Task>>; -} - -pub trait DebuggerProvider { - // `active_buffer` is used to resolve build task's name against language-specific tasks. - fn start_session( - &self, - definition: DebugScenario, - task_context: TaskContext, - active_buffer: Option>, - worktree_id: Option, - window: &mut Window, - cx: &mut App, - ); - - fn spawn_task_or_modal( - &self, - workspace: &mut Workspace, - action: &Spawn, - window: &mut Window, - cx: &mut Context, - ); - - fn task_scheduled(&self, cx: &mut App); - fn debug_scenario_scheduled(&self, cx: &mut App); - fn debug_scenario_scheduled_last(&self, cx: &App) -> bool; - - fn active_thread_state(&self, cx: &App) -> Option; -} - -actions!( - workspace, - [ - /// Activates the next pane in the workspace. - ActivateNextPane, - /// Activates the previous pane in the workspace. - ActivatePreviousPane, - /// Switches to the next window. - ActivateNextWindow, - /// Switches to the previous window. - ActivatePreviousWindow, - /// Adds a folder to the current project. - AddFolderToProject, - /// Clears all notifications. - ClearAllNotifications, - /// Clears all navigation history, including forward/backward navigation, recently opened files, and recently closed tabs. **This action is irreversible**. - ClearNavigationHistory, - /// Closes the active dock. - CloseActiveDock, - /// Closes all docks. - CloseAllDocks, - /// Toggles all docks. - ToggleAllDocks, - /// Closes the current window. - CloseWindow, - /// Opens the feedback dialog. - Feedback, - /// Follows the next collaborator in the session. - FollowNextCollaborator, - /// Moves the focused panel to the next position. - MoveFocusedPanelToNextPosition, - /// Opens a new terminal in the center. - NewCenterTerminal, - /// Creates a new file. - NewFile, - /// Creates a new file in a vertical split. - NewFileSplitVertical, - /// Creates a new file in a horizontal split. - NewFileSplitHorizontal, - /// Opens a new search. - NewSearch, - /// Opens a new terminal. - NewTerminal, - /// Opens a new window. - NewWindow, - /// Opens a file or directory. - Open, - /// Opens multiple files. - OpenFiles, - /// Opens the current location in terminal. - OpenInTerminal, - /// Opens the component preview. - OpenComponentPreview, - /// Reloads the active item. - ReloadActiveItem, - /// Resets the active dock to its default size. - ResetActiveDockSize, - /// Resets all open docks to their default sizes. - ResetOpenDocksSize, - /// Reloads the application - Reload, - /// Saves the current file with a new name. - SaveAs, - /// Saves without formatting. - SaveWithoutFormat, - /// Shuts down all debug adapters. - ShutdownDebugAdapters, - /// Suppresses the current notification. - SuppressNotification, - /// Toggles the bottom dock. - ToggleBottomDock, - /// Toggles centered layout mode. - ToggleCenteredLayout, - /// Toggles edit prediction feature globally for all files. - ToggleEditPrediction, - /// Toggles the left dock. - ToggleLeftDock, - /// Toggles the right dock. - ToggleRightDock, - /// Toggles zoom on the active pane. - ToggleZoom, - /// Stops following a collaborator. - Unfollow, - /// Restores the banner. - RestoreBanner, - /// Toggles expansion of the selected item. - ToggleExpandItem, - ] -); - -/// Activates a specific pane by its index. -#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)] -#[action(namespace = workspace)] -pub struct ActivatePane(pub usize); - -/// Moves an item to a specific pane by index. -#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)] -#[action(namespace = workspace)] -#[serde(deny_unknown_fields)] -pub struct MoveItemToPane { - #[serde(default = "default_1")] - pub destination: usize, - #[serde(default = "default_true")] - pub focus: bool, - #[serde(default)] - pub clone: bool, -} - -fn default_1() -> usize { - 1 -} - -/// Moves an item to a pane in the specified direction. -#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)] -#[action(namespace = workspace)] -#[serde(deny_unknown_fields)] -pub struct MoveItemToPaneInDirection { - #[serde(default = "default_right")] - pub direction: SplitDirection, - #[serde(default = "default_true")] - pub focus: bool, - #[serde(default)] - pub clone: bool, -} - -/// Creates a new file in a split of the desired direction. -#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)] -#[action(namespace = workspace)] -#[serde(deny_unknown_fields)] -pub struct NewFileSplit(pub SplitDirection); - -fn default_right() -> SplitDirection { - SplitDirection::Right -} - -/// Saves all open files in the workspace. -#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Action)] -#[action(namespace = workspace)] -#[serde(deny_unknown_fields)] -pub struct SaveAll { - #[serde(default)] - pub save_intent: Option, -} - -/// Saves the current file with the specified options. -#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Action)] -#[action(namespace = workspace)] -#[serde(deny_unknown_fields)] -pub struct Save { - #[serde(default)] - pub save_intent: Option, -} - -/// Closes all items and panes in the workspace. -#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)] -#[action(namespace = workspace)] -#[serde(deny_unknown_fields)] -pub struct CloseAllItemsAndPanes { - #[serde(default)] - pub save_intent: Option, -} - -/// Closes all inactive tabs and panes in the workspace. -#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)] -#[action(namespace = workspace)] -#[serde(deny_unknown_fields)] -pub struct CloseInactiveTabsAndPanes { - #[serde(default)] - pub save_intent: Option, -} - -/// Sends a sequence of keystrokes to the active element. -#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)] -#[action(namespace = workspace)] -pub struct SendKeystrokes(pub String); - -actions!( - project_symbols, - [ - /// Toggles the project symbols search. - #[action(name = "Toggle")] - ToggleProjectSymbols - ] -); - -/// Toggles the file finder interface. -#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema, Action)] -#[action(namespace = file_finder, name = "Toggle")] -#[serde(deny_unknown_fields)] -pub struct ToggleFileFinder { - #[serde(default)] - pub separate_history: bool, -} - -/// Increases size of a currently focused dock by a given amount of pixels. -#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)] -#[action(namespace = workspace)] -#[serde(deny_unknown_fields)] -pub struct IncreaseActiveDockSize { - /// For 0px parameter, uses UI font size value. - #[serde(default)] - pub px: u32, -} - -/// Decreases size of a currently focused dock by a given amount of pixels. -#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)] -#[action(namespace = workspace)] -#[serde(deny_unknown_fields)] -pub struct DecreaseActiveDockSize { - /// For 0px parameter, uses UI font size value. - #[serde(default)] - pub px: u32, -} - -/// Increases size of all currently visible docks uniformly, by a given amount of pixels. -#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)] -#[action(namespace = workspace)] -#[serde(deny_unknown_fields)] -pub struct IncreaseOpenDocksSize { - /// For 0px parameter, uses UI font size value. - #[serde(default)] - pub px: u32, -} - -/// Decreases size of all currently visible docks uniformly, by a given amount of pixels. -#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)] -#[action(namespace = workspace)] -#[serde(deny_unknown_fields)] -pub struct DecreaseOpenDocksSize { - /// For 0px parameter, uses UI font size value. - #[serde(default)] - pub px: u32, -} - -actions!( - workspace, - [ - /// Activates the pane to the left. - ActivatePaneLeft, - /// Activates the pane to the right. - ActivatePaneRight, - /// Activates the pane above. - ActivatePaneUp, - /// Activates the pane below. - ActivatePaneDown, - /// Swaps the current pane with the one to the left. - SwapPaneLeft, - /// Swaps the current pane with the one to the right. - SwapPaneRight, - /// Swaps the current pane with the one above. - SwapPaneUp, - /// Swaps the current pane with the one below. - SwapPaneDown, - // Swaps the current pane with the first available adjacent pane (searching in order: below, above, right, left) and activates that pane. - SwapPaneAdjacent, - /// Move the current pane to be at the far left. - MovePaneLeft, - /// Move the current pane to be at the far right. - MovePaneRight, - /// Move the current pane to be at the very top. - MovePaneUp, - /// Move the current pane to be at the very bottom. - MovePaneDown, - ] -); - -#[derive(PartialEq, Eq, Debug)] -pub enum CloseIntent { - /// Quit the program entirely. - Quit, - /// Close a window. - CloseWindow, - /// Replace the workspace in an existing window. - ReplaceWindow, -} - -#[derive(Clone)] -pub struct Toast { - id: NotificationId, - msg: Cow<'static, str>, - autohide: bool, - on_click: Option<(Cow<'static, str>, Arc)>, -} - -impl Toast { - pub fn new>>(id: NotificationId, msg: I) -> Self { - Toast { - id, - msg: msg.into(), - on_click: None, - autohide: false, - } - } - - pub fn on_click(mut self, message: M, on_click: F) -> Self - where - M: Into>, - F: Fn(&mut Window, &mut App) + 'static, - { - self.on_click = Some((message.into(), Arc::new(on_click))); - self - } - - pub fn autohide(mut self) -> Self { - self.autohide = true; - self - } -} - -impl PartialEq for Toast { - fn eq(&self, other: &Self) -> bool { - self.id == other.id - && self.msg == other.msg - && self.on_click.is_some() == other.on_click.is_some() - } -} - -/// Opens a new terminal with the specified working directory. -#[derive(Debug, Default, Clone, Deserialize, PartialEq, JsonSchema, Action)] -#[action(namespace = workspace)] -#[serde(deny_unknown_fields)] -pub struct OpenTerminal { - pub working_directory: PathBuf, -} - -#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)] -pub struct WorkspaceId(i64); - -impl StaticColumnCount for WorkspaceId {} -impl Bind for WorkspaceId { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - self.0.bind(statement, start_index) - } -} -impl Column for WorkspaceId { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - i64::column(statement, start_index) - .map(|(i, next_index)| (Self(i), next_index)) - .with_context(|| format!("Failed to read WorkspaceId at index {start_index}")) - } -} -impl From for i64 { - fn from(val: WorkspaceId) -> Self { - val.0 - } -} - -fn prompt_and_open_paths(app_state: Arc, options: PathPromptOptions, cx: &mut App) { - let paths = cx.prompt_for_paths(options); - cx.spawn( - async move |cx| match paths.await.anyhow().and_then(|res| res) { - Ok(Some(paths)) => { - cx.update(|cx| { - open_paths(&paths, app_state, OpenOptions::default(), cx).detach_and_log_err(cx) - }) - .ok(); - } - Ok(None) => {} - Err(err) => { - util::log_err(&err); - cx.update(|cx| { - if let Some(workspace_window) = cx - .active_window() - .and_then(|window| window.downcast::()) - { - workspace_window - .update(cx, |workspace, _, cx| { - workspace.show_portal_error(err.to_string(), cx); - }) - .ok(); - } - }) - .ok(); - } - }, - ) - .detach(); -} - -pub fn init(app_state: Arc, cx: &mut App) { - component::init(); - theme_preview::init(cx); - toast_layer::init(cx); - history_manager::init(cx); - - cx.on_action(|_: &CloseWindow, cx| Workspace::close_global(cx)); - cx.on_action(|_: &Reload, cx| reload(cx)); - - cx.on_action({ - let app_state = Arc::downgrade(&app_state); - move |_: &Open, cx: &mut App| { - if let Some(app_state) = app_state.upgrade() { - prompt_and_open_paths( - app_state, - PathPromptOptions { - files: true, - directories: true, - multiple: true, - prompt: None, - }, - cx, - ); - } - } - }); - cx.on_action({ - let app_state = Arc::downgrade(&app_state); - move |_: &OpenFiles, cx: &mut App| { - let directories = cx.can_select_mixed_files_and_dirs(); - if let Some(app_state) = app_state.upgrade() { - prompt_and_open_paths( - app_state, - PathPromptOptions { - files: true, - directories, - multiple: true, - prompt: None, - }, - cx, - ); - } - } - }); -} - -type BuildProjectItemFn = - fn(AnyEntity, Entity, Option<&Pane>, &mut Window, &mut App) -> Box; - -type BuildProjectItemForPathFn = - fn( - &Entity, - &ProjectPath, - &mut Window, - &mut App, - ) -> Option, WorkspaceItemBuilder)>>>; - -#[derive(Clone, Default)] -struct ProjectItemRegistry { - build_project_item_fns_by_type: HashMap, - build_project_item_for_path_fns: Vec, -} - -impl ProjectItemRegistry { - fn register(&mut self) { - self.build_project_item_fns_by_type.insert( - TypeId::of::(), - |item, project, pane, window, cx| { - let item = item.downcast().unwrap(); - Box::new(cx.new(|cx| T::for_project_item(project, pane, item, window, cx))) - as Box - }, - ); - 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)?; - 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.as_std_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) => { - log::warn!("Failed to open a project item: {e:#}"); - if e.error_code() == ErrorCode::Internal { - if let Some(abs_path) = - entry_abs_path.as_deref().filter(|_| is_file) - { - if let Some(broken_project_item_view) = - cx.update(|window, cx| { - T::for_broken_project_item( - abs_path, is_local, &e, window, cx, - ) - })? - { - let build_workspace_item = Box::new( - move |_: &mut Pane, _: &mut Window, cx: &mut Context| { - cx.new(|_| broken_project_item_view).boxed_clone() - }, - ) - as Box<_>; - return Ok((None, build_workspace_item)); - } - } - } - Err(e) - } - } - })) - }); - } - - fn open_path( - &self, - project: &Entity, - path: &ProjectPath, - window: &mut Window, - cx: &mut App, - ) -> Task, WorkspaceItemBuilder)>> { - let Some(open_project_item) = self - .build_project_item_for_path_fns - .iter() - .rev() - .find_map(|open_project_item| open_project_item(project, path, window, cx)) - else { - return Task::ready(Err(anyhow!("cannot open file {:?}", path.path))); - }; - open_project_item - } - - fn build_item( - &self, - item: Entity, - project: Entity, - pane: Option<&Pane>, - window: &mut Window, - cx: &mut App, - ) -> Option> { - let build = self - .build_project_item_fns_by_type - .get(&TypeId::of::())?; - Some(build(item.into_any(), project, pane, window, cx)) - } -} - -type WorkspaceItemBuilder = - Box) -> Box>; - -impl Global for ProjectItemRegistry {} - -/// Registers a [ProjectItem] for the app. When opening a file, all the registered -/// items will get a chance to open the file, starting from the project item that -/// was added last. -pub fn register_project_item(cx: &mut App) { - cx.default_global::().register::(); -} - -#[derive(Default)] -pub struct FollowableViewRegistry(HashMap); - -struct FollowableViewDescriptor { - from_state_proto: fn( - Entity, - ViewId, - &mut Option, - &mut Window, - &mut App, - ) -> Option>>>, - to_followable_view: fn(&AnyView) -> Box, -} - -impl Global for FollowableViewRegistry {} - -impl FollowableViewRegistry { - pub fn register(cx: &mut App) { - cx.default_global::().0.insert( - TypeId::of::(), - FollowableViewDescriptor { - from_state_proto: |workspace, id, state, window, cx| { - I::from_state_proto(workspace, id, state, window, cx).map(|task| { - cx.foreground_executor() - .spawn(async move { Ok(Box::new(task.await?) as Box<_>) }) - }) - }, - to_followable_view: |view| Box::new(view.clone().downcast::().unwrap()), - }, - ); - } - - pub fn from_state_proto( - workspace: Entity, - view_id: ViewId, - mut state: Option, - window: &mut Window, - cx: &mut App, - ) -> Option>>> { - cx.update_default_global(|this: &mut Self, cx| { - this.0.values().find_map(|descriptor| { - (descriptor.from_state_proto)(workspace.clone(), view_id, &mut state, window, cx) - }) - }) - } - - pub fn to_followable_view( - view: impl Into, - cx: &App, - ) -> Option> { - let this = cx.try_global::()?; - let view = view.into(); - let descriptor = this.0.get(&view.entity_type())?; - Some((descriptor.to_followable_view)(&view)) - } -} - -#[derive(Copy, Clone)] -struct SerializableItemDescriptor { - deserialize: fn( - Entity, - WeakEntity, - WorkspaceId, - ItemId, - &mut Window, - &mut Context, - ) -> Task>>, - cleanup: fn(WorkspaceId, Vec, &mut Window, &mut App) -> Task>, - view_to_serializable_item: fn(AnyView) -> Box, -} - -#[derive(Default)] -struct SerializableItemRegistry { - descriptors_by_kind: HashMap, SerializableItemDescriptor>, - descriptors_by_type: HashMap, -} - -impl Global for SerializableItemRegistry {} - -impl SerializableItemRegistry { - fn deserialize( - item_kind: &str, - project: Entity, - workspace: WeakEntity, - workspace_id: WorkspaceId, - item_item: ItemId, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - let Some(descriptor) = Self::descriptor(item_kind, cx) else { - return Task::ready(Err(anyhow!( - "cannot deserialize {}, descriptor not found", - item_kind - ))); - }; - - (descriptor.deserialize)(project, workspace, workspace_id, item_item, window, cx) - } - - fn cleanup( - item_kind: &str, - workspace_id: WorkspaceId, - loaded_items: Vec, - window: &mut Window, - cx: &mut App, - ) -> Task> { - let Some(descriptor) = Self::descriptor(item_kind, cx) else { - return Task::ready(Err(anyhow!( - "cannot cleanup {}, descriptor not found", - item_kind - ))); - }; - - (descriptor.cleanup)(workspace_id, loaded_items, window, cx) - } - - fn view_to_serializable_item_handle( - view: AnyView, - cx: &App, - ) -> Option> { - let this = cx.try_global::()?; - let descriptor = this.descriptors_by_type.get(&view.entity_type())?; - Some((descriptor.view_to_serializable_item)(view)) - } - - fn descriptor(item_kind: &str, cx: &App) -> Option { - let this = cx.try_global::()?; - this.descriptors_by_kind.get(item_kind).copied() - } -} - -pub fn register_serializable_item(cx: &mut App) { - let serialized_item_kind = I::serialized_item_kind(); - - let registry = cx.default_global::(); - let descriptor = SerializableItemDescriptor { - deserialize: |project, workspace, workspace_id, item_id, window, cx| { - let task = I::deserialize(project, workspace, workspace_id, item_id, window, cx); - cx.foreground_executor() - .spawn(async { Ok(Box::new(task.await?) as Box<_>) }) - }, - cleanup: |workspace_id, loaded_items, window, cx| { - I::cleanup(workspace_id, loaded_items, window, cx) - }, - view_to_serializable_item: |view| Box::new(view.downcast::().unwrap()), - }; - registry - .descriptors_by_kind - .insert(Arc::from(serialized_item_kind), descriptor); - registry - .descriptors_by_type - .insert(TypeId::of::(), descriptor); -} - -pub struct AppState { - pub languages: Arc, - pub client: Arc, - pub user_store: Entity, - pub workspace_store: Entity, - pub fs: Arc, - pub build_window_options: fn(Option, &mut App) -> WindowOptions, - pub node_runtime: NodeRuntime, - pub session: Entity, -} - -struct GlobalAppState(Weak); - -impl Global for GlobalAppState {} - -pub struct WorkspaceStore { - workspaces: HashSet>, - client: Arc, - _subscriptions: Vec, -} - -#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)] -pub enum CollaboratorId { - PeerId(PeerId), - Agent, -} - -impl From for CollaboratorId { - fn from(peer_id: PeerId) -> Self { - CollaboratorId::PeerId(peer_id) - } -} - -impl From<&PeerId> for CollaboratorId { - fn from(peer_id: &PeerId) -> Self { - CollaboratorId::PeerId(*peer_id) - } -} - -#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)] -struct Follower { - project_id: Option, - peer_id: PeerId, -} - -impl AppState { - #[track_caller] - pub fn global(cx: &App) -> Weak { - cx.global::().0.clone() - } - pub fn try_global(cx: &App) -> Option> { - cx.try_global::() - .map(|state| state.0.clone()) - } - pub fn set_global(state: Weak, cx: &mut App) { - cx.set_global(GlobalAppState(state)); - } - - #[cfg(any(test, feature = "test-support"))] - pub fn test(cx: &mut App) -> Arc { - use node_runtime::NodeRuntime; - use session::Session; - use settings::SettingsStore; - - if !cx.has_global::() { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - } - - let fs = fs::FakeFs::new(cx.background_executor().clone()); - let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone())); - let clock = Arc::new(clock::FakeSystemClock::new()); - let http_client = http_client::FakeHttpClient::with_404_response(); - let client = Client::new(clock, http_client, cx); - let session = cx.new(|cx| AppSession::new(Session::test(), cx)); - let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); - let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx)); - - theme::init(theme::LoadThemes::JustBase, cx); - client::init(&client, cx); - - Arc::new(Self { - client, - fs, - languages, - user_store, - workspace_store, - node_runtime: NodeRuntime::unavailable(), - build_window_options: |_, _| Default::default(), - session, - }) - } -} - -struct DelayedDebouncedEditAction { - task: Option>, - cancel_channel: Option>, -} - -impl DelayedDebouncedEditAction { - fn new() -> DelayedDebouncedEditAction { - DelayedDebouncedEditAction { - task: None, - cancel_channel: None, - } - } - - fn fire_new( - &mut self, - delay: Duration, - window: &mut Window, - cx: &mut Context, - func: F, - ) where - F: 'static - + Send - + FnOnce(&mut Workspace, &mut Window, &mut Context) -> Task>, - { - if let Some(channel) = self.cancel_channel.take() { - _ = channel.send(()); - } - - let (sender, mut receiver) = oneshot::channel::<()>(); - self.cancel_channel = Some(sender); - - let previous_task = self.task.take(); - self.task = Some(cx.spawn_in(window, async move |workspace, cx| { - let mut timer = cx.background_executor().timer(delay).fuse(); - if let Some(previous_task) = previous_task { - previous_task.await; - } - - futures::select_biased! { - _ = receiver => return, - _ = timer => {} - } - - if let Some(result) = workspace - .update_in(cx, |workspace, window, cx| (func)(workspace, window, cx)) - .log_err() - { - result.await.log_err(); - } - })); - } -} - -pub enum Event { - PaneAdded(Entity), - PaneRemoved, - ItemAdded { - item: Box, - }, - ActiveItemChanged, - ItemRemoved { - item_id: EntityId, - }, - UserSavedItem { - pane: WeakEntity, - item: Box, - save_intent: SaveIntent, - }, - ContactRequestedJoin(u64), - WorkspaceCreated(WeakEntity), - OpenBundledFile { - text: Cow<'static, str>, - title: &'static str, - language: &'static str, - }, - ZoomChanged, - ModalOpened, -} - -#[derive(Debug)] -pub enum OpenVisible { - All, - None, - OnlyFiles, - OnlyDirectories, -} - -enum WorkspaceLocation { - // Valid local paths or SSH project to serialize - Location(SerializedWorkspaceLocation, PathList), - // No valid location found hence clear session id - DetachFromSession, - // No valid location found to serialize - None, -} - -type PromptForNewPath = Box< - dyn Fn( - &mut Workspace, - DirectoryLister, - &mut Window, - &mut Context, - ) -> oneshot::Receiver>>, ->; - -type PromptForOpenPath = Box< - dyn Fn( - &mut Workspace, - DirectoryLister, - &mut Window, - &mut Context, - ) -> oneshot::Receiver>>, ->; - -#[derive(Default)] -struct DispatchingKeystrokes { - dispatched: HashSet>, - queue: VecDeque, - task: Option>>, -} - -/// Collects everything project-related for a certain window opened. -/// In some way, is a counterpart of a window, as the [`WindowHandle`] could be downcast into `Workspace`. -/// -/// A `Workspace` usually consists of 1 or more projects, a central pane group, 3 docks and a status bar. -/// The `Workspace` owns everybody's state and serves as a default, "global context", -/// that can be used to register a global action to be triggered from any place in the window. -pub struct Workspace { - weak_self: WeakEntity, - workspace_actions: Vec) -> Div>>, - zoomed: Option, - previous_dock_drag_coordinates: Option>, - zoomed_position: Option, - center: PaneGroup, - left_dock: Entity, - bottom_dock: Entity, - right_dock: Entity, - panes: Vec>, - panes_by_item: HashMap>, - active_pane: Entity, - last_active_center_pane: Option>, - last_active_view_id: Option, - status_bar: Entity, - modal_layer: Entity, - toast_layer: Entity, - titlebar_item: Option, - notifications: Notifications, - suppressed_notifications: HashSet, - project: Entity, - follower_states: HashMap, - last_leaders_by_pane: HashMap, CollaboratorId>, - window_edited: bool, - last_window_title: Option, - dirty_items: HashMap, - active_call: Option<(Entity, Vec)>, - leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>, - database_id: Option, - app_state: Arc, - dispatching_keystrokes: Rc>, - _subscriptions: Vec, - _apply_leader_updates: Task>, - _observe_current_user: Task>, - _schedule_serialize_workspace: Option>, - _schedule_serialize_ssh_paths: Option>, - pane_history_timestamp: Arc, - bounds: Bounds, - pub centered_layout: bool, - bounds_save_task_queued: Option>, - on_prompt_for_new_path: Option, - on_prompt_for_open_path: Option, - terminal_provider: Option>, - debugger_provider: Option>, - serializable_items_tx: UnboundedSender>, - _items_serializer: Task>, - session_id: Option, - scheduled_tasks: Vec>, - last_open_dock_positions: Vec, - removing: bool, -} - -impl EventEmitter for Workspace {} - -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] -pub struct ViewId { - pub creator: CollaboratorId, - pub id: u64, -} - -pub struct FollowerState { - center_pane: Entity, - dock_pane: Option>, - active_view_id: Option, - items_by_leader_view_id: HashMap, -} - -struct FollowerView { - view: Box, - location: Option, -} - -impl Workspace { - pub fn new( - workspace_id: Option, - project: Entity, - app_state: Arc, - window: &mut Window, - cx: &mut Context, - ) -> Self { - cx.subscribe_in(&project, window, move |this, _, event, window, cx| { - match event { - project::Event::RemoteIdChanged(_) => { - this.update_window_title(window, cx); - } - - project::Event::CollaboratorLeft(peer_id) => { - this.collaborator_left(*peer_id, window, cx); - } - - project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded(_) => { - this.update_window_title(window, cx); - this.serialize_workspace(window, cx); - // This event could be triggered by `AddFolderToProject` or `RemoveFromProject`. - this.update_history(cx); - } - - project::Event::DisconnectedFromHost => { - this.update_window_edited(window, cx); - let leaders_to_unfollow = - this.follower_states.keys().copied().collect::>(); - for leader_id in leaders_to_unfollow { - this.unfollow(leader_id, window, cx); - } - } - - project::Event::DisconnectedFromSshRemote => { - this.update_window_edited(window, cx); - } - - project::Event::Closed => { - window.remove_window(); - } - - project::Event::DeletedEntry(_, entry_id) => { - for pane in this.panes.iter() { - pane.update(cx, |pane, cx| { - pane.handle_deleted_project_item(*entry_id, window, cx) - }); - } - } - - project::Event::Toast { - notification_id, - message, - } => this.show_notification( - NotificationId::named(notification_id.clone()), - cx, - |cx| cx.new(|cx| MessageNotification::new(message.clone(), cx)), - ), - - project::Event::HideToast { notification_id } => { - this.dismiss_notification(&NotificationId::named(notification_id.clone()), cx) - } - - project::Event::LanguageServerPrompt(request) => { - struct LanguageServerPrompt; - - let mut hasher = DefaultHasher::new(); - request.lsp_name.as_str().hash(&mut hasher); - let id = hasher.finish(); - - this.show_notification( - NotificationId::composite::(id as usize), - cx, - |cx| { - cx.new(|cx| { - notifications::LanguageServerPrompt::new(request.clone(), cx) - }) - }, - ); - } - - project::Event::AgentLocationChanged => { - this.handle_agent_location_changed(window, cx) - } - - _ => {} - } - cx.notify() - }) - .detach(); - - cx.subscribe_in( - &project.read(cx).breakpoint_store(), - window, - |workspace, _, event, window, cx| match event { - BreakpointStoreEvent::BreakpointsUpdated(_, _) - | BreakpointStoreEvent::BreakpointsCleared(_) => { - workspace.serialize_workspace(window, cx); - } - BreakpointStoreEvent::SetDebugLine | BreakpointStoreEvent::ClearDebugLines => {} - }, - ) - .detach(); - if let Some(toolchain_store) = project.read(cx).toolchain_store() { - cx.subscribe_in( - &toolchain_store, - window, - |workspace, _, event, window, cx| match event { - ToolchainStoreEvent::CustomToolchainsModified => { - workspace.serialize_workspace(window, cx); - } - _ => {} - }, - ) - .detach(); - } - - cx.on_focus_lost(window, |this, window, cx| { - let focus_handle = this.focus_handle(cx); - window.focus(&focus_handle); - }) - .detach(); - - let weak_handle = cx.entity().downgrade(); - let pane_history_timestamp = Arc::new(AtomicUsize::new(0)); - - let center_pane = cx.new(|cx| { - let mut center_pane = Pane::new( - weak_handle.clone(), - project.clone(), - pane_history_timestamp.clone(), - None, - NewFile.boxed_clone(), - true, - window, - cx, - ); - center_pane.set_can_split(Some(Arc::new(|_, _, _, _| true))); - center_pane - }); - cx.subscribe_in(¢er_pane, window, Self::handle_pane_event) - .detach(); - - window.focus(¢er_pane.focus_handle(cx)); - - cx.emit(Event::PaneAdded(center_pane.clone())); - - let window_handle = window.window_handle().downcast::().unwrap(); - app_state.workspace_store.update(cx, |store, _| { - store.workspaces.insert(window_handle); - }); - - let mut current_user = app_state.user_store.read(cx).watch_current_user(); - let mut connection_status = app_state.client.status(); - let _observe_current_user = cx.spawn_in(window, async move |this, cx| { - current_user.next().await; - connection_status.next().await; - let mut stream = - Stream::map(current_user, drop).merge(Stream::map(connection_status, drop)); - - while stream.recv().await.is_some() { - this.update(cx, |_, cx| cx.notify())?; - } - anyhow::Ok(()) - }); - - // All leader updates are enqueued and then processed in a single task, so - // that each asynchronous operation can be run in order. - let (leader_updates_tx, mut leader_updates_rx) = - mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>(); - let _apply_leader_updates = cx.spawn_in(window, async move |this, cx| { - while let Some((leader_id, update)) = leader_updates_rx.next().await { - Self::process_leader_update(&this, leader_id, update, cx) - .await - .log_err(); - } - - Ok(()) - }); - - cx.emit(Event::WorkspaceCreated(weak_handle.clone())); - let modal_layer = cx.new(|_| ModalLayer::new()); - let toast_layer = cx.new(|_| ToastLayer::new()); - cx.subscribe( - &modal_layer, - |_, _, _: &modal_layer::ModalOpenedEvent, cx| { - cx.emit(Event::ModalOpened); - }, - ) - .detach(); - - let left_dock = Dock::new(DockPosition::Left, modal_layer.clone(), window, cx); - let bottom_dock = Dock::new(DockPosition::Bottom, modal_layer.clone(), window, cx); - let right_dock = Dock::new(DockPosition::Right, modal_layer.clone(), window, cx); - let left_dock_buttons = cx.new(|cx| PanelButtons::new(left_dock.clone(), cx)); - let bottom_dock_buttons = cx.new(|cx| PanelButtons::new(bottom_dock.clone(), cx)); - let right_dock_buttons = cx.new(|cx| PanelButtons::new(right_dock.clone(), cx)); - let status_bar = cx.new(|cx| { - let mut status_bar = StatusBar::new(¢er_pane.clone(), window, cx); - status_bar.add_left_item(left_dock_buttons, window, cx); - status_bar.add_right_item(right_dock_buttons, window, cx); - status_bar.add_right_item(bottom_dock_buttons, window, cx); - status_bar - }); - - let session_id = app_state.session.read(cx).id().to_owned(); - - let mut active_call = None; - if let Some(call) = ActiveCall::try_global(cx) { - let subscriptions = vec![cx.subscribe_in(&call, window, Self::on_active_call_event)]; - active_call = Some((call, subscriptions)); - } - - let (serializable_items_tx, serializable_items_rx) = - mpsc::unbounded::>(); - let _items_serializer = cx.spawn_in(window, async move |this, cx| { - Self::serialize_items(&this, serializable_items_rx, cx).await - }); - - let subscriptions = vec![ - cx.observe_window_activation(window, Self::on_window_activation_changed), - cx.observe_window_bounds(window, move |this, window, cx| { - if this.bounds_save_task_queued.is_some() { - return; - } - this.bounds_save_task_queued = Some(cx.spawn_in(window, async move |this, cx| { - cx.background_executor() - .timer(Duration::from_millis(100)) - .await; - this.update_in(cx, |this, window, cx| { - if let Some(display) = window.display(cx) - && let Ok(display_uuid) = display.uuid() - { - let window_bounds = window.inner_window_bounds(); - if let Some(database_id) = workspace_id { - cx.background_executor() - .spawn(DB.set_window_open_status( - database_id, - SerializedWindowBounds(window_bounds), - display_uuid, - )) - .detach_and_log_err(cx); - } - } - this.bounds_save_task_queued.take(); - }) - .ok(); - })); - cx.notify(); - }), - cx.observe_window_appearance(window, |_, window, cx| { - let window_appearance = window.appearance(); - - *SystemAppearance::global_mut(cx) = SystemAppearance(window_appearance.into()); - - GlobalTheme::reload_theme(cx); - GlobalTheme::reload_icon_theme(cx); - }), - cx.on_release(move |this, cx| { - this.app_state.workspace_store.update(cx, move |store, _| { - store.workspaces.remove(&window_handle); - }) - }), - ]; - - cx.defer_in(window, |this, window, cx| { - this.update_window_title(window, cx); - this.show_initial_notifications(cx); - }); - Workspace { - weak_self: weak_handle.clone(), - zoomed: None, - zoomed_position: None, - previous_dock_drag_coordinates: None, - center: PaneGroup::new(center_pane.clone()), - panes: vec![center_pane.clone()], - panes_by_item: Default::default(), - active_pane: center_pane.clone(), - last_active_center_pane: Some(center_pane.downgrade()), - last_active_view_id: None, - status_bar, - modal_layer, - toast_layer, - titlebar_item: None, - notifications: Notifications::default(), - suppressed_notifications: HashSet::default(), - left_dock, - bottom_dock, - right_dock, - project: project.clone(), - follower_states: Default::default(), - last_leaders_by_pane: Default::default(), - dispatching_keystrokes: Default::default(), - window_edited: false, - last_window_title: None, - dirty_items: Default::default(), - active_call, - database_id: workspace_id, - app_state, - _observe_current_user, - _apply_leader_updates, - _schedule_serialize_workspace: None, - _schedule_serialize_ssh_paths: None, - leader_updates_tx, - _subscriptions: subscriptions, - pane_history_timestamp, - workspace_actions: Default::default(), - // This data will be incorrect, but it will be overwritten by the time it needs to be used. - bounds: Default::default(), - centered_layout: false, - bounds_save_task_queued: None, - on_prompt_for_new_path: None, - on_prompt_for_open_path: None, - terminal_provider: None, - debugger_provider: None, - serializable_items_tx, - _items_serializer, - session_id: Some(session_id), - - scheduled_tasks: Vec::new(), - last_open_dock_positions: Vec::new(), - removing: false, - } - } - - pub fn new_local( - abs_paths: Vec, - app_state: Arc, - requesting_window: Option>, - env: Option>, - cx: &mut App, - ) -> Task< - anyhow::Result<( - WindowHandle, - Vec>>>, - )>, - > { - let project_handle = Project::local( - app_state.client.clone(), - app_state.node_runtime.clone(), - app_state.user_store.clone(), - app_state.languages.clone(), - app_state.fs.clone(), - env, - cx, - ); - - cx.spawn(async move |cx| { - let mut paths_to_open = Vec::with_capacity(abs_paths.len()); - for path in abs_paths.into_iter() { - if let Some(canonical) = app_state.fs.canonicalize(&path).await.ok() { - paths_to_open.push(canonical) - } else { - paths_to_open.push(path) - } - } - - 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.ordered_paths().cloned().collect(); - if !paths.is_lexicographically_ordered() { - project_handle - .update(cx, |project, cx| { - project.set_worktrees_reordered(true, cx); - }) - .log_err(); - } - } - - // Get project paths for all of the abs_paths - let mut project_paths: Vec<(PathBuf, Option)> = - Vec::with_capacity(paths_to_open.len()); - - for path in paths_to_open.into_iter() { - if let Some((_, project_entry)) = cx - .update(|cx| { - Workspace::project_path_for_path(project_handle.clone(), &path, true, cx) - })? - .await - .log_err() - { - project_paths.push((path, Some(project_entry))); - } else { - project_paths.push((path, None)); - } - } - - let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() { - serialized_workspace.id - } else { - DB.next_id().await.unwrap_or_else(|_| Default::default()) - }; - - let toolchains = DB.toolchains(workspace_id).await?; - - for (toolchain, worktree_id, path) in toolchains { - let toolchain_path = PathBuf::from(toolchain.path.clone().to_string()); - if !app_state.fs.is_file(toolchain_path.as_path()).await { - continue; - } - - project_handle - .update(cx, |this, cx| { - this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx) - })? - .await; - } - if let Some(workspace) = serialized_workspace.as_ref() { - project_handle.update(cx, |this, cx| { - for (scope, toolchains) in &workspace.user_toolchains { - for toolchain in toolchains { - this.add_toolchain(toolchain.clone(), scope.clone(), cx); - } - } - })?; - } - - let window = if let Some(window) = requesting_window { - let centered_layout = serialized_workspace - .as_ref() - .map(|w| w.centered_layout) - .unwrap_or(false); - - cx.update_window(window.into(), |_, window, cx| { - window.replace_root(cx, |window, cx| { - let mut workspace = Workspace::new( - Some(workspace_id), - project_handle.clone(), - app_state.clone(), - window, - cx, - ); - - workspace.centered_layout = centered_layout; - workspace - }); - })?; - window - } else { - let window_bounds_override = window_bounds_env_override(); - - let (window_bounds, display) = if let Some(bounds) = window_bounds_override { - (Some(WindowBounds::Windowed(bounds)), None) - } else if let Some(workspace) = serialized_workspace.as_ref() { - // Reopening an existing workspace - restore its saved bounds - if let (Some(display), Some(bounds)) = - (workspace.display, workspace.window_bounds.as_ref()) - { - (Some(bounds.0), Some(display)) - } else { - (None, None) - } - } else { - // New window - let GPUI's default_bounds() handle cascading - (None, None) - }; - - // Use the serialized workspace to construct the new window - let mut options = cx.update(|cx| (app_state.build_window_options)(display, cx))?; - options.window_bounds = window_bounds; - let centered_layout = serialized_workspace - .as_ref() - .map(|w| w.centered_layout) - .unwrap_or(false); - cx.open_window(options, { - let app_state = app_state.clone(); - let project_handle = project_handle.clone(); - move |window, cx| { - cx.new(|cx| { - let mut workspace = Workspace::new( - Some(workspace_id), - project_handle, - app_state, - window, - cx, - ); - workspace.centered_layout = centered_layout; - workspace - }) - } - })? - }; - - notify_if_database_failed(window, cx); - let opened_items = window - .update(cx, |_workspace, window, cx| { - open_items(serialized_workspace, project_paths, window, cx) - })? - .await - .unwrap_or_default(); - - window - .update(cx, |workspace, window, cx| { - window.activate_window(); - workspace.update_history(cx); - }) - .log_err(); - Ok((window, opened_items)) - }) - } - - pub fn weak_handle(&self) -> WeakEntity { - self.weak_self.clone() - } - - pub fn left_dock(&self) -> &Entity { - &self.left_dock - } - - pub fn bottom_dock(&self) -> &Entity { - &self.bottom_dock - } - - pub fn set_bottom_dock_layout( - &mut self, - layout: BottomDockLayout, - window: &mut Window, - cx: &mut Context, - ) { - let fs = self.project().read(cx).fs(); - settings::update_settings_file(fs.clone(), cx, move |content, _cx| { - content.workspace.bottom_dock_layout = Some(layout); - }); - - cx.notify(); - self.serialize_workspace(window, cx); - } - - pub fn right_dock(&self) -> &Entity { - &self.right_dock - } - - pub fn all_docks(&self) -> [&Entity; 3] { - [&self.left_dock, &self.bottom_dock, &self.right_dock] - } - - pub fn dock_at_position(&self, position: DockPosition) -> &Entity { - match position { - DockPosition::Left => &self.left_dock, - DockPosition::Bottom => &self.bottom_dock, - DockPosition::Right => &self.right_dock, - } - } - - pub fn is_edited(&self) -> bool { - self.window_edited - } - - pub fn add_panel( - &mut self, - panel: Entity, - window: &mut Window, - cx: &mut Context, - ) { - let focus_handle = panel.panel_focus_handle(cx); - cx.on_focus_in(&focus_handle, window, Self::handle_panel_focused) - .detach(); - - let dock_position = panel.position(window, cx); - let dock = self.dock_at_position(dock_position); - - dock.update(cx, |dock, cx| { - dock.add_panel(panel, self.weak_self.clone(), window, cx) - }); - } - - pub fn remove_panel( - &mut self, - panel: &Entity, - window: &mut Window, - cx: &mut Context, - ) { - for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] { - dock.update(cx, |dock, cx| { - dock.remove_panel(panel, window, cx); - }) - } - } - - pub fn status_bar(&self) -> &Entity { - &self.status_bar - } - - pub fn status_bar_visible(&self, cx: &App) -> bool { - StatusBarSettings::get_global(cx).show - } - - pub fn app_state(&self) -> &Arc { - &self.app_state - } - - pub fn user_store(&self) -> &Entity { - &self.app_state.user_store - } - - pub fn project(&self) -> &Entity { - &self.project - } - - pub fn path_style(&self, cx: &App) -> PathStyle { - self.project.read(cx).path_style(cx) - } - - pub fn recently_activated_items(&self, cx: &App) -> HashMap { - let mut history: HashMap = HashMap::default(); - - for pane_handle in &self.panes { - let pane = pane_handle.read(cx); - - for entry in pane.activation_history() { - history.insert( - entry.entity_id, - history - .get(&entry.entity_id) - .cloned() - .unwrap_or(0) - .max(entry.timestamp), - ); - } - } - - history - } - - pub fn recent_active_item_by_type(&self, cx: &App) -> Option> { - let mut recent_item: Option> = None; - let mut recent_timestamp = 0; - for pane_handle in &self.panes { - let pane = pane_handle.read(cx); - let item_map: HashMap> = - pane.items().map(|item| (item.item_id(), item)).collect(); - for entry in pane.activation_history() { - if entry.timestamp > recent_timestamp - && let Some(&item) = item_map.get(&entry.entity_id) - && let Some(typed_item) = item.act_as::(cx) - { - recent_timestamp = entry.timestamp; - recent_item = Some(typed_item); - } - } - } - recent_item - } - - pub fn recent_navigation_history_iter( - &self, - cx: &App, - ) -> impl Iterator)> + use<> { - let mut abs_paths_opened: HashMap> = HashMap::default(); - let mut history: HashMap, usize)> = HashMap::default(); - - for pane in &self.panes { - let pane = pane.read(cx); - - pane.nav_history() - .for_each_entry(cx, |entry, (project_path, fs_path)| { - if let Some(fs_path) = &fs_path { - abs_paths_opened - .entry(fs_path.clone()) - .or_default() - .insert(project_path.clone()); - } - let timestamp = entry.timestamp; - match history.entry(project_path) { - hash_map::Entry::Occupied(mut entry) => { - let (_, old_timestamp) = entry.get(); - if ×tamp > old_timestamp { - entry.insert((fs_path, timestamp)); - } - } - hash_map::Entry::Vacant(entry) => { - entry.insert((fs_path, timestamp)); - } - } - }); - - if let Some(item) = pane.active_item() - && let Some(project_path) = item.project_path(cx) - { - let fs_path = self.project.read(cx).absolute_path(&project_path, cx); - - if let Some(fs_path) = &fs_path { - abs_paths_opened - .entry(fs_path.clone()) - .or_default() - .insert(project_path.clone()); - } - - history.insert(project_path, (fs_path, std::usize::MAX)); - } - } - - history - .into_iter() - .sorted_by_key(|(_, (_, order))| *order) - .map(|(project_path, (fs_path, _))| (project_path, fs_path)) - .rev() - .filter(move |(history_path, abs_path)| { - let latest_project_path_opened = abs_path - .as_ref() - .and_then(|abs_path| abs_paths_opened.get(abs_path)) - .and_then(|project_paths| { - project_paths - .iter() - .max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id)) - }); - - latest_project_path_opened.is_none_or(|path| path == history_path) - }) - } - - pub fn recent_navigation_history( - &self, - limit: Option, - cx: &App, - ) -> Vec<(ProjectPath, Option)> { - self.recent_navigation_history_iter(cx) - .take(limit.unwrap_or(usize::MAX)) - .collect() - } - - pub fn clear_navigation_history(&mut self, _window: &mut Window, cx: &mut Context) { - for pane in &self.panes { - pane.update(cx, |pane, cx| pane.nav_history_mut().clear(cx)); - } - } - - fn navigate_history( - &mut self, - pane: WeakEntity, - mode: NavigationMode, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - let to_load = if let Some(pane) = pane.upgrade() { - pane.update(cx, |pane, cx| { - window.focus(&pane.focus_handle(cx)); - loop { - // Retrieve the weak item handle from the history. - let entry = pane.nav_history_mut().pop(mode, cx)?; - - // If the item is still present in this pane, then activate it. - if let Some(index) = entry - .item - .upgrade() - .and_then(|v| pane.index_for_item(v.as_ref())) - { - let prev_active_item_index = pane.active_item_index(); - pane.nav_history_mut().set_mode(mode); - pane.activate_item(index, true, true, window, cx); - pane.nav_history_mut().set_mode(NavigationMode::Normal); - - let mut navigated = prev_active_item_index != pane.active_item_index(); - if let Some(data) = entry.data { - navigated |= pane.active_item()?.navigate(data, window, cx); - } - - if navigated { - break None; - } - } else { - // If the item is no longer present in this pane, then retrieve its - // path info in order to reopen it. - break pane - .nav_history() - .path_for_item(entry.item.id()) - .map(|(project_path, abs_path)| (project_path, abs_path, entry)); - } - } - }) - } else { - None - }; - - if let Some((project_path, abs_path, entry)) = to_load { - // If the item was no longer present, then load it again from its previous path, first try the local path - let open_by_project_path = self.load_path(project_path.clone(), window, cx); - - cx.spawn_in(window, async move |workspace, cx| { - let open_by_project_path = open_by_project_path.await; - let mut navigated = false; - match open_by_project_path - .with_context(|| format!("Navigating to {project_path:?}")) - { - Ok((project_entry_id, build_item)) => { - let prev_active_item_id = pane.update(cx, |pane, _| { - pane.nav_history_mut().set_mode(mode); - pane.active_item().map(|p| p.item_id()) - })?; - - pane.update_in(cx, |pane, window, cx| { - let item = pane.open_item( - project_entry_id, - project_path, - true, - entry.is_preview, - true, - None, - window, cx, - build_item, - ); - navigated |= Some(item.item_id()) != prev_active_item_id; - pane.nav_history_mut().set_mode(NavigationMode::Normal); - if let Some(data) = entry.data { - navigated |= item.navigate(data, window, cx); - } - })?; - } - Err(open_by_project_path_e) => { - // Fall back to opening by abs path, in case an external file was opened and closed, - // and its worktree is now dropped - if let Some(abs_path) = abs_path { - let prev_active_item_id = pane.update(cx, |pane, _| { - pane.nav_history_mut().set_mode(mode); - pane.active_item().map(|p| p.item_id()) - })?; - let open_by_abs_path = workspace.update_in(cx, |workspace, window, cx| { - workspace.open_abs_path(abs_path.clone(), OpenOptions { visible: Some(OpenVisible::None), ..Default::default() }, window, cx) - })?; - match open_by_abs_path - .await - .with_context(|| format!("Navigating to {abs_path:?}")) - { - Ok(item) => { - pane.update_in(cx, |pane, window, cx| { - navigated |= Some(item.item_id()) != prev_active_item_id; - pane.nav_history_mut().set_mode(NavigationMode::Normal); - if let Some(data) = entry.data { - navigated |= item.navigate(data, window, cx); - } - })?; - } - Err(open_by_abs_path_e) => { - log::error!("Failed to navigate history: {open_by_project_path_e:#} and {open_by_abs_path_e:#}"); - } - } - } - } - } - - if !navigated { - workspace - .update_in(cx, |workspace, window, cx| { - Self::navigate_history(workspace, pane, mode, window, cx) - })? - .await?; - } - - Ok(()) - }) - } else { - Task::ready(Ok(())) - } - } - - pub fn go_back( - &mut self, - pane: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.navigate_history(pane, NavigationMode::GoingBack, window, cx) - } - - pub fn go_forward( - &mut self, - pane: WeakEntity, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.navigate_history(pane, NavigationMode::GoingForward, window, cx) - } - - pub fn reopen_closed_item( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - self.navigate_history( - self.active_pane().downgrade(), - NavigationMode::ReopeningClosedItem, - window, - cx, - ) - } - - pub fn client(&self) -> &Arc { - &self.app_state.client - } - - pub fn set_titlebar_item(&mut self, item: AnyView, _: &mut Window, cx: &mut Context) { - self.titlebar_item = Some(item); - cx.notify(); - } - - pub fn set_prompt_for_new_path(&mut self, prompt: PromptForNewPath) { - self.on_prompt_for_new_path = Some(prompt) - } - - pub fn set_prompt_for_open_path(&mut self, prompt: PromptForOpenPath) { - self.on_prompt_for_open_path = Some(prompt) - } - - pub fn set_terminal_provider(&mut self, provider: impl TerminalProvider + 'static) { - self.terminal_provider = Some(Box::new(provider)); - } - - pub fn set_debugger_provider(&mut self, provider: impl DebuggerProvider + 'static) { - self.debugger_provider = Some(Arc::new(provider)); - } - - pub fn debugger_provider(&self) -> Option> { - self.debugger_provider.clone() - } - - pub fn prompt_for_open_path( - &mut self, - path_prompt_options: PathPromptOptions, - lister: DirectoryLister, - window: &mut Window, - cx: &mut Context, - ) -> oneshot::Receiver>> { - if !lister.is_local(cx) || !WorkspaceSettings::get_global(cx).use_system_path_prompts { - let prompt = self.on_prompt_for_open_path.take().unwrap(); - let rx = prompt(self, lister, window, cx); - self.on_prompt_for_open_path = Some(prompt); - rx - } else { - let (tx, rx) = oneshot::channel(); - let abs_path = cx.prompt_for_paths(path_prompt_options); - - cx.spawn_in(window, async move |workspace, cx| { - let Ok(result) = abs_path.await else { - return Ok(()); - }; - - match result { - Ok(result) => { - tx.send(result).ok(); - } - Err(err) => { - let rx = workspace.update_in(cx, |workspace, window, cx| { - workspace.show_portal_error(err.to_string(), cx); - let prompt = workspace.on_prompt_for_open_path.take().unwrap(); - let rx = prompt(workspace, lister, window, cx); - workspace.on_prompt_for_open_path = Some(prompt); - rx - })?; - if let Ok(path) = rx.await { - tx.send(path).ok(); - } - } - }; - anyhow::Ok(()) - }) - .detach(); - - rx - } - } - - pub fn prompt_for_new_path( - &mut self, - lister: DirectoryLister, - suggested_name: Option, - window: &mut Window, - cx: &mut Context, - ) -> oneshot::Receiver>> { - if self.project.read(cx).is_via_collab() - || self.project.read(cx).is_via_remote_server() - || !WorkspaceSettings::get_global(cx).use_system_path_prompts - { - let prompt = self.on_prompt_for_new_path.take().unwrap(); - let rx = prompt(self, lister, window, cx); - self.on_prompt_for_new_path = Some(prompt); - return rx; - } - - let (tx, rx) = oneshot::channel(); - cx.spawn_in(window, async move |workspace, cx| { - let abs_path = workspace.update(cx, |workspace, cx| { - let relative_to = workspace - .most_recent_active_path(cx) - .and_then(|p| p.parent().map(|p| p.to_path_buf())) - .or_else(|| { - let project = workspace.project.read(cx); - project.visible_worktrees(cx).find_map(|worktree| { - Some(worktree.read(cx).as_local()?.abs_path().to_path_buf()) - }) - }) - .or_else(std::env::home_dir) - .unwrap_or_else(|| PathBuf::from("")); - cx.prompt_for_new_path(&relative_to, suggested_name.as_deref()) - })?; - let abs_path = match abs_path.await? { - Ok(path) => path, - Err(err) => { - let rx = workspace.update_in(cx, |workspace, window, cx| { - workspace.show_portal_error(err.to_string(), cx); - - let prompt = workspace.on_prompt_for_new_path.take().unwrap(); - let rx = prompt(workspace, lister, window, cx); - workspace.on_prompt_for_new_path = Some(prompt); - rx - })?; - if let Ok(path) = rx.await { - tx.send(path).ok(); - } - return anyhow::Ok(()); - } - }; - - tx.send(abs_path.map(|path| vec![path])).ok(); - anyhow::Ok(()) - }) - .detach(); - - rx - } - - pub fn titlebar_item(&self) -> Option { - self.titlebar_item.clone() - } - - /// Call the given callback with a workspace whose project is local. - /// - /// If the given workspace has a local project, then it will be passed - /// to the callback. Otherwise, a new empty window will be created. - pub fn with_local_workspace( - &mut self, - window: &mut Window, - cx: &mut Context, - callback: F, - ) -> Task> - where - T: 'static, - F: 'static + FnOnce(&mut Workspace, &mut Window, &mut Context) -> T, - { - if self.project.read(cx).is_local() { - Task::ready(Ok(callback(self, window, cx))) - } else { - let env = self.project.read(cx).cli_environment(cx); - let task = Self::new_local(Vec::new(), self.app_state.clone(), None, env, cx); - cx.spawn_in(window, async move |_vh, cx| { - let (workspace, _) = task.await?; - workspace.update(cx, callback) - }) - } - } - - pub fn worktrees<'a>(&self, cx: &'a App) -> impl 'a + Iterator> { - self.project.read(cx).worktrees(cx) - } - - pub fn visible_worktrees<'a>( - &self, - cx: &'a App, - ) -> impl 'a + Iterator> { - self.project.read(cx).visible_worktrees(cx) - } - - #[cfg(any(test, feature = "test-support"))] - pub fn worktree_scans_complete(&self, cx: &App) -> impl Future + 'static + use<> { - let futures = self - .worktrees(cx) - .filter_map(|worktree| worktree.read(cx).as_local()) - .map(|worktree| worktree.scan_complete()) - .collect::>(); - async move { - for future in futures { - future.await; - } - } - } - - pub fn close_global(cx: &mut App) { - cx.defer(|cx| { - cx.windows().iter().find(|window| { - window - .update(cx, |_, window, _| { - if window.is_window_active() { - //This can only get called when the window's project connection has been lost - //so we don't need to prompt the user for anything and instead just close the window - window.remove_window(); - true - } else { - false - } - }) - .unwrap_or(false) - }); - }); - } - - pub fn close_window(&mut self, _: &CloseWindow, window: &mut Window, cx: &mut Context) { - let prepare = self.prepare_to_close(CloseIntent::CloseWindow, window, cx); - cx.spawn_in(window, async move |_, cx| { - if prepare.await? { - cx.update(|window, _cx| window.remove_window())?; - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx) - } - - pub fn move_focused_panel_to_next_position( - &mut self, - _: &MoveFocusedPanelToNextPosition, - window: &mut Window, - cx: &mut Context, - ) { - let docks = self.all_docks(); - let active_dock = docks - .into_iter() - .find(|dock| dock.focus_handle(cx).contains_focused(window, cx)); - - if let Some(dock) = active_dock { - dock.update(cx, |dock, cx| { - let active_panel = dock - .active_panel() - .filter(|panel| panel.panel_focus_handle(cx).contains_focused(window, cx)); - - if let Some(panel) = active_panel { - panel.move_to_next_position(window, cx); - } - }) - } - } - - pub fn prepare_to_close( - &mut self, - close_intent: CloseIntent, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - let active_call = self.active_call().cloned(); - - cx.spawn_in(window, async move |this, cx| { - this.update(cx, |this, _| { - if close_intent == CloseIntent::CloseWindow { - this.removing = true; - } - })?; - - let workspace_count = cx.update(|_window, cx| { - cx.windows() - .iter() - .filter(|window| window.downcast::().is_some()) - .count() - })?; - - #[cfg(target_os = "macos")] - let save_last_workspace = false; - - // On Linux and Windows, closing the last window should restore the last workspace. - #[cfg(not(target_os = "macos"))] - let save_last_workspace = { - let remaining_workspaces = cx.update(|_window, cx| { - cx.windows() - .iter() - .filter_map(|window| window.downcast::()) - .filter_map(|workspace| { - workspace - .update(cx, |workspace, _, _| workspace.removing) - .ok() - }) - .filter(|removing| !removing) - .count() - })?; - - close_intent != CloseIntent::ReplaceWindow && remaining_workspaces == 0 - }; - - if let Some(active_call) = active_call - && 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, - ) - })?; - - 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>(()) - })?; - } - } - - let save_result = this - .update_in(cx, |this, window, cx| { - this.save_all_internal(SaveIntent::Close, window, cx) - })? - .await; - - // If we're not quitting, but closing, we remove the workspace from - // the current session. - if close_intent != CloseIntent::Quit - && !save_last_workspace - && save_result.as_ref().is_ok_and(|&res| res) - { - this.update_in(cx, |this, window, cx| this.remove_from_session(window, cx))? - .await; - } - - save_result - }) - } - - fn save_all(&mut self, action: &SaveAll, window: &mut Window, cx: &mut Context) { - self.save_all_internal( - action.save_intent.unwrap_or(SaveIntent::SaveAll), - window, - cx, - ) - .detach_and_log_err(cx); - } - - fn send_keystrokes( - &mut self, - action: &SendKeystrokes, - window: &mut Window, - cx: &mut Context, - ) { - let keystrokes: Vec = action - .0 - .split(' ') - .flat_map(|k| Keystroke::parse(k).log_err()) - .map(|k| { - cx.keyboard_mapper() - .map_key_equivalent(k, true) - .inner() - .clone() - }) - .collect(); - let _ = self.send_keystrokes_impl(keystrokes, window, cx); - } - - pub fn send_keystrokes_impl( - &mut self, - keystrokes: Vec, - window: &mut Window, - cx: &mut Context, - ) -> Shared> { - let mut state = self.dispatching_keystrokes.borrow_mut(); - if !state.dispatched.insert(keystrokes.clone()) { - cx.propagate(); - return state.task.clone().unwrap(); - } - - state.queue.extend(keystrokes); - - let keystrokes = self.dispatching_keystrokes.clone(); - if state.task.is_none() { - state.task = Some( - window - .spawn(cx, async move |cx| { - // limit to 100 keystrokes to avoid infinite recursion. - for _ in 0..100 { - let mut state = keystrokes.borrow_mut(); - let Some(keystroke) = state.queue.pop_front() else { - state.dispatched.clear(); - state.task.take(); - return; - }; - drop(state); - cx.update(|window, cx| { - let focused = window.focused(cx); - window.dispatch_keystroke(keystroke.clone(), cx); - if window.focused(cx) != focused { - // dispatch_keystroke may cause the focus to change. - // draw's side effect is to schedule the FocusChanged events in the current flush effect cycle - // And we need that to happen before the next keystroke to keep vim mode happy... - // (Note that the tests always do this implicitly, so you must manually test with something like: - // "bindings": { "g z": ["workspace::SendKeystrokes", ": j u"]} - // ) - window.draw(cx).clear(); - } - }) - .ok(); - } - - *keystrokes.borrow_mut() = Default::default(); - log::error!("over 100 keystrokes passed to send_keystrokes"); - }) - .shared(), - ); - } - state.task.clone().unwrap() - } - - fn save_all_internal( - &mut self, - mut save_intent: SaveIntent, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - if self.project.read(cx).is_disconnected(cx) { - return Task::ready(Ok(true)); - } - let dirty_items = self - .panes - .iter() - .flat_map(|pane| { - pane.read(cx).items().filter_map(|item| { - if item.is_dirty(cx) { - item.tab_content_text(0, cx); - Some((pane.downgrade(), item.boxed_clone())) - } else { - None - } - }) - }) - .collect::>(); - - let project = self.project.clone(); - cx.spawn_in(window, async move |workspace, cx| { - let dirty_items = if save_intent == SaveIntent::Close && !dirty_items.is_empty() { - let (serialize_tasks, remaining_dirty_items) = - workspace.update_in(cx, |workspace, window, cx| { - let mut remaining_dirty_items = Vec::new(); - let mut serialize_tasks = Vec::new(); - for (pane, item) in dirty_items { - if let Some(task) = item - .to_serializable_item_handle(cx) - .and_then(|handle| handle.serialize(workspace, true, window, cx)) - { - serialize_tasks.push(task); - } else { - remaining_dirty_items.push((pane, item)); - } - } - (serialize_tasks, remaining_dirty_items) - })?; - - futures::future::try_join_all(serialize_tasks).await?; - - if remaining_dirty_items.len() > 1 { - let answer = workspace.update_in(cx, |_, window, cx| { - let detail = Pane::file_names_for_prompt( - &mut remaining_dirty_items.iter().map(|(_, handle)| handle), - cx, - ); - window.prompt( - PromptLevel::Warning, - "Do you want to save all changes in the following files?", - Some(&detail), - &["Save all", "Discard all", "Cancel"], - cx, - ) - })?; - match answer.await.log_err() { - Some(0) => save_intent = SaveIntent::SaveAll, - Some(1) => save_intent = SaveIntent::Skip, - Some(2) => return Ok(false), - _ => {} - } - } - - remaining_dirty_items - } else { - dirty_items - }; - - for (pane, item) in dirty_items { - let (singleton, project_entry_ids) = cx.update(|_, cx| { - ( - item.buffer_kind(cx) == ItemBufferKind::Singleton, - item.project_entry_ids(cx), - ) - })?; - if (singleton || !project_entry_ids.is_empty()) - && !Pane::save_item(project.clone(), &pane, &*item, save_intent, cx).await? - { - return Ok(false); - } - } - Ok(true) - }) - } - - pub fn open_workspace_for_paths( - &mut self, - replace_current_window: bool, - paths: Vec, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - let window_handle = window.window_handle().downcast::(); - let is_remote = self.project.read(cx).is_via_collab(); - let has_worktree = self.project.read(cx).worktrees(cx).next().is_some(); - let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx)); - - let window_to_replace = if replace_current_window { - window_handle - } else if is_remote || has_worktree || has_dirty_items { - None - } else { - window_handle - }; - let app_state = self.app_state.clone(); - - cx.spawn(async move |_, cx| { - cx.update(|cx| { - open_paths( - &paths, - app_state, - OpenOptions { - replace_window: window_to_replace, - ..Default::default() - }, - cx, - ) - })? - .await?; - Ok(()) - }) - } - - #[allow(clippy::type_complexity)] - pub fn open_paths( - &mut self, - mut abs_paths: Vec, - options: OpenOptions, - pane: Option>, - window: &mut Window, - cx: &mut Context, - ) -> Task>>>> { - let fs = self.app_state.fs.clone(); - - // Sort the paths to ensure we add worktrees for parents before their children. - abs_paths.sort_unstable(); - cx.spawn_in(window, async move |this, cx| { - let mut tasks = Vec::with_capacity(abs_paths.len()); - - for abs_path in &abs_paths { - let visible = match options.visible.as_ref().unwrap_or(&OpenVisible::None) { - OpenVisible::All => Some(true), - OpenVisible::None => Some(false), - OpenVisible::OnlyFiles => match fs.metadata(abs_path).await.log_err() { - Some(Some(metadata)) => Some(!metadata.is_dir), - Some(None) => Some(true), - None => None, - }, - OpenVisible::OnlyDirectories => match fs.metadata(abs_path).await.log_err() { - Some(Some(metadata)) => Some(metadata.is_dir), - Some(None) => Some(false), - None => None, - }, - }; - let project_path = match visible { - Some(visible) => match this - .update(cx, |this, cx| { - Workspace::project_path_for_path( - this.project.clone(), - abs_path, - visible, - cx, - ) - }) - .log_err() - { - Some(project_path) => project_path.await.log_err(), - None => None, - }, - None => None, - }; - - let this = this.clone(); - let abs_path: Arc = SanitizedPath::new(&abs_path).as_path().into(); - let fs = fs.clone(); - let pane = pane.clone(); - let task = cx.spawn(async move |cx| { - let (worktree, project_path) = project_path?; - if fs.is_dir(&abs_path).await { - this.update(cx, |workspace, cx| { - let worktree = worktree.read(cx); - let worktree_abs_path = worktree.abs_path(); - let entry_id = if abs_path.as_ref() == worktree_abs_path.as_ref() { - worktree.root_entry() - } else { - abs_path - .strip_prefix(worktree_abs_path.as_ref()) - .ok() - .and_then(|relative_path| { - let relative_path = - RelPath::new(relative_path, PathStyle::local()) - .log_err()?; - worktree.entry_for_path(&relative_path) - }) - } - .map(|entry| entry.id); - if let Some(entry_id) = entry_id { - workspace.project.update(cx, |_, cx| { - cx.emit(project::Event::ActiveEntryChanged(Some(entry_id))); - }) - } - }) - .ok()?; - None - } else { - Some( - this.update_in(cx, |this, window, cx| { - this.open_path( - project_path, - pane, - options.focus.unwrap_or(true), - window, - cx, - ) - }) - .ok()? - .await, - ) - } - }); - tasks.push(task); - } - - futures::future::join_all(tasks).await - }) - } - - pub fn open_resolved_path( - &mut self, - path: ResolvedPath, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - match path { - ResolvedPath::ProjectPath { project_path, .. } => { - self.open_path(project_path, None, true, window, cx) - } - ResolvedPath::AbsPath { path, .. } => self.open_abs_path( - PathBuf::from(path), - OpenOptions { - visible: Some(OpenVisible::None), - ..Default::default() - }, - window, - cx, - ), - } - } - - pub fn absolute_path_of_worktree( - &self, - worktree_id: WorktreeId, - cx: &mut Context, - ) -> Option { - self.project - .read(cx) - .worktree_for_id(worktree_id, cx) - // TODO: use `abs_path` or `root_dir` - .map(|wt| wt.read(cx).abs_path().as_ref().to_path_buf()) - } - - fn add_folder_to_project( - &mut self, - _: &AddFolderToProject, - window: &mut Window, - cx: &mut Context, - ) { - let project = self.project.read(cx); - if project.is_via_collab() { - self.show_error( - &anyhow!("You cannot add folders to someone else's project"), - cx, - ); - return; - } - let paths = self.prompt_for_open_path( - PathPromptOptions { - files: false, - directories: true, - multiple: true, - prompt: None, - }, - DirectoryLister::Project(self.project.clone()), - window, - cx, - ); - cx.spawn_in(window, async move |this, cx| { - if let Some(paths) = paths.await.log_err().flatten() { - let results = this - .update_in(cx, |this, window, cx| { - this.open_paths( - paths, - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - None, - window, - cx, - ) - })? - .await; - for result in results.into_iter().flatten() { - result.log_err(); - } - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - - pub fn project_path_for_path( - project: Entity, - abs_path: &Path, - visible: bool, - cx: &mut App, - ) -> Task, ProjectPath)>> { - let entry = project.update(cx, |project, cx| { - project.find_or_create_worktree(abs_path, visible, cx) - }); - cx.spawn(async move |cx| { - let (worktree, path) = entry.await?; - let worktree_id = worktree.read_with(cx, |t, _| t.id())?; - Ok(( - worktree, - ProjectPath { - worktree_id, - path: path, - }, - )) - }) - } - - pub fn items<'a>(&'a self, cx: &'a App) -> impl 'a + Iterator> { - self.panes.iter().flat_map(|pane| pane.read(cx).items()) - } - - pub fn item_of_type(&self, cx: &App) -> Option> { - self.items_of_type(cx).max_by_key(|item| item.item_id()) - } - - pub fn items_of_type<'a, T: Item>( - &'a self, - cx: &'a App, - ) -> impl 'a + Iterator> { - self.panes - .iter() - .flat_map(|pane| pane.read(cx).items_of_type()) - } - - pub fn active_item(&self, cx: &App) -> Option> { - self.active_pane().read(cx).active_item() - } - - pub fn active_item_as(&self, cx: &App) -> Option> { - let item = self.active_item(cx)?; - item.to_any_view().downcast::().ok() - } - - fn active_project_path(&self, cx: &App) -> Option { - self.active_item(cx).and_then(|item| item.project_path(cx)) - } - - pub fn most_recent_active_path(&self, cx: &App) -> Option { - self.recent_navigation_history_iter(cx) - .filter_map(|(path, abs_path)| { - let worktree = self - .project - .read(cx) - .worktree_for_id(path.worktree_id, cx)?; - if worktree.read(cx).is_visible() { - abs_path - } else { - None - } - }) - .next() - } - - pub fn save_active_item( - &mut self, - save_intent: SaveIntent, - window: &mut Window, - cx: &mut App, - ) -> Task> { - let project = self.project.clone(); - let pane = self.active_pane(); - let item = pane.read(cx).active_item(); - let pane = pane.downgrade(); - - window.spawn(cx, async move |cx| { - if let Some(item) = item { - Pane::save_item(project, &pane, item.as_ref(), save_intent, cx) - .await - .map(|_| ()) - } else { - Ok(()) - } - }) - } - - pub fn close_inactive_items_and_panes( - &mut self, - action: &CloseInactiveTabsAndPanes, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(task) = self.close_all_internal( - true, - action.save_intent.unwrap_or(SaveIntent::Close), - window, - cx, - ) { - task.detach_and_log_err(cx) - } - } - - pub fn close_all_items_and_panes( - &mut self, - action: &CloseAllItemsAndPanes, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(task) = self.close_all_internal( - false, - action.save_intent.unwrap_or(SaveIntent::Close), - window, - cx, - ) { - task.detach_and_log_err(cx) - } - } - - fn close_all_internal( - &mut self, - retain_active_pane: bool, - save_intent: SaveIntent, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - let current_pane = self.active_pane(); - - let mut tasks = Vec::new(); - - if retain_active_pane { - let current_pane_close = current_pane.update(cx, |pane, cx| { - pane.close_other_items( - &CloseOtherItems { - save_intent: None, - close_pinned: false, - }, - None, - window, - cx, - ) - }); - - tasks.push(current_pane_close); - } - - for pane in self.panes() { - if retain_active_pane && pane.entity_id() == current_pane.entity_id() { - continue; - } - - let close_pane_items = pane.update(cx, |pane: &mut Pane, cx| { - pane.close_all_items( - &CloseAllItems { - save_intent: Some(save_intent), - close_pinned: false, - }, - window, - cx, - ) - }); - - tasks.push(close_pane_items) - } - - if tasks.is_empty() { - None - } else { - Some(cx.spawn_in(window, async move |_, _| { - for task in tasks { - task.await? - } - Ok(()) - })) - } - } - - pub fn is_dock_at_position_open(&self, position: DockPosition, cx: &mut Context) -> bool { - self.dock_at_position(position).read(cx).is_open() - } - - pub fn toggle_dock( - &mut self, - dock_side: DockPosition, - window: &mut Window, - cx: &mut Context, - ) { - let mut focus_center = false; - let mut reveal_dock = false; - - let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side); - let was_visible = self.is_dock_at_position_open(dock_side, cx) && !other_is_zoomed; - if was_visible { - self.save_open_dock_positions(cx); - } - - let dock = self.dock_at_position(dock_side); - dock.update(cx, |dock, cx| { - dock.set_open(!was_visible, window, cx); - - if dock.active_panel().is_none() { - let Some(panel_ix) = dock - .first_enabled_panel_idx(cx) - .log_with_level(log::Level::Info) - else { - return; - }; - dock.activate_panel(panel_ix, window, cx); - } - - if let Some(active_panel) = dock.active_panel() { - if was_visible { - if active_panel - .panel_focus_handle(cx) - .contains_focused(window, cx) - { - focus_center = true; - } - } else { - let focus_handle = &active_panel.panel_focus_handle(cx); - window.focus(focus_handle); - reveal_dock = true; - } - } - }); - - if reveal_dock { - self.dismiss_zoomed_items_to_reveal(Some(dock_side), window, cx); - } - - if focus_center { - self.active_pane - .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx))) - } - - cx.notify(); - self.serialize_workspace(window, cx); - } - - fn active_dock(&self, window: &Window, cx: &Context) -> Option<&Entity> { - self.all_docks().into_iter().find(|&dock| { - dock.read(cx).is_open() && dock.focus_handle(cx).contains_focused(window, cx) - }) - } - - fn close_active_dock(&mut self, window: &mut Window, cx: &mut Context) -> bool { - if let Some(dock) = self.active_dock(window, cx).cloned() { - self.save_open_dock_positions(cx); - dock.update(cx, |dock, cx| { - dock.set_open(false, window, cx); - }); - return true; - } - false - } - - pub fn close_all_docks(&mut self, window: &mut Window, cx: &mut Context) { - self.save_open_dock_positions(cx); - for dock in self.all_docks() { - dock.update(cx, |dock, cx| { - dock.set_open(false, window, cx); - }); - } - - cx.focus_self(window); - cx.notify(); - self.serialize_workspace(window, cx); - } - - fn get_open_dock_positions(&self, cx: &Context) -> Vec { - self.all_docks() - .into_iter() - .filter_map(|dock| { - let dock_ref = dock.read(cx); - if dock_ref.is_open() { - Some(dock_ref.position()) - } else { - None - } - }) - .collect() - } - - /// Saves the positions of currently open docks. - /// - /// Updates `last_open_dock_positions` with positions of all currently open - /// docks, to later be restored by the 'Toggle All Docks' action. - fn save_open_dock_positions(&mut self, cx: &mut Context) { - let open_dock_positions = self.get_open_dock_positions(cx); - if !open_dock_positions.is_empty() { - self.last_open_dock_positions = open_dock_positions; - } - } - - /// Toggles all docks between open and closed states. - /// - /// If any docks are open, closes all and remembers their positions. If all - /// docks are closed, restores the last remembered dock configuration. - fn toggle_all_docks( - &mut self, - _: &ToggleAllDocks, - window: &mut Window, - cx: &mut Context, - ) { - let open_dock_positions = self.get_open_dock_positions(cx); - - if !open_dock_positions.is_empty() { - self.close_all_docks(window, cx); - } else if !self.last_open_dock_positions.is_empty() { - self.restore_last_open_docks(window, cx); - } - } - - /// Reopens docks from the most recently remembered configuration. - /// - /// Opens all docks whose positions are stored in `last_open_dock_positions` - /// and clears the stored positions. - fn restore_last_open_docks(&mut self, window: &mut Window, cx: &mut Context) { - let positions_to_open = std::mem::take(&mut self.last_open_dock_positions); - - for position in positions_to_open { - let dock = self.dock_at_position(position); - dock.update(cx, |dock, cx| dock.set_open(true, window, cx)); - } - - cx.focus_self(window); - cx.notify(); - self.serialize_workspace(window, cx); - } - - /// Transfer focus to the panel of the given type. - pub fn focus_panel( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> Option> { - let panel = self.focus_or_unfocus_panel::(window, cx, |_, _, _| true)?; - panel.to_any().downcast().ok() - } - - /// Focus the panel of the given type if it isn't already focused. If it is - /// already focused, then transfer focus back to the workspace center. - pub fn toggle_panel_focus( - &mut self, - window: &mut Window, - cx: &mut Context, - ) -> bool { - let mut did_focus_panel = false; - self.focus_or_unfocus_panel::(window, cx, |panel, window, cx| { - did_focus_panel = !panel.panel_focus_handle(cx).contains_focused(window, cx); - did_focus_panel - }); - did_focus_panel - } - - pub fn activate_panel_for_proto_id( - &mut self, - panel_id: PanelId, - window: &mut Window, - cx: &mut Context, - ) -> Option> { - let mut panel = None; - for dock in self.all_docks() { - if let Some(panel_index) = dock.read(cx).panel_index_for_proto_id(panel_id) { - panel = dock.update(cx, |dock, cx| { - dock.activate_panel(panel_index, window, cx); - dock.set_open(true, window, cx); - dock.active_panel().cloned() - }); - break; - } - } - - if panel.is_some() { - cx.notify(); - self.serialize_workspace(window, cx); - } - - panel - } - - /// Focus or unfocus the given panel type, depending on the given callback. - fn focus_or_unfocus_panel( - &mut self, - window: &mut Window, - cx: &mut Context, - mut should_focus: impl FnMut(&dyn PanelHandle, &mut Window, &mut Context) -> bool, - ) -> Option> { - let mut result_panel = None; - let mut serialize = false; - for dock in self.all_docks() { - if let Some(panel_index) = dock.read(cx).panel_index_for_type::() { - let mut focus_center = false; - let panel = dock.update(cx, |dock, cx| { - dock.activate_panel(panel_index, window, cx); - - let panel = dock.active_panel().cloned(); - if let Some(panel) = panel.as_ref() { - if should_focus(&**panel, window, cx) { - dock.set_open(true, window, cx); - panel.panel_focus_handle(cx).focus(window); - } else { - focus_center = true; - } - } - panel - }); - - if focus_center { - self.active_pane - .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx))) - } - - result_panel = panel; - serialize = true; - break; - } - } - - if serialize { - self.serialize_workspace(window, cx); - } - - cx.notify(); - result_panel - } - - /// Open the panel of the given type - pub fn open_panel(&mut self, window: &mut Window, cx: &mut Context) { - for dock in self.all_docks() { - if let Some(panel_index) = dock.read(cx).panel_index_for_type::() { - dock.update(cx, |dock, cx| { - dock.activate_panel(panel_index, window, cx); - dock.set_open(true, window, cx); - }); - } - } - } - - pub fn close_panel(&self, window: &mut Window, cx: &mut Context) { - for dock in self.all_docks().iter() { - dock.update(cx, |dock, cx| { - if dock.panel::().is_some() { - dock.set_open(false, window, cx) - } - }) - } - } - - pub fn panel(&self, cx: &App) -> Option> { - self.all_docks() - .iter() - .find_map(|dock| dock.read(cx).panel::()) - } - - fn dismiss_zoomed_items_to_reveal( - &mut self, - dock_to_reveal: Option, - window: &mut Window, - cx: &mut Context, - ) { - // If a center pane is zoomed, unzoom it. - for pane in &self.panes { - if pane != &self.active_pane || dock_to_reveal.is_some() { - pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); - } - } - - // If another dock is zoomed, hide it. - let mut focus_center = false; - for dock in self.all_docks() { - dock.update(cx, |dock, cx| { - if Some(dock.position()) != dock_to_reveal - && let Some(panel) = dock.active_panel() - && panel.is_zoomed(window, cx) - { - focus_center |= panel.panel_focus_handle(cx).contains_focused(window, cx); - dock.set_open(false, window, cx); - } - }); - } - - if focus_center { - self.active_pane - .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx))) - } - - if self.zoomed_position != dock_to_reveal { - self.zoomed = None; - self.zoomed_position = None; - cx.emit(Event::ZoomChanged); - } - - cx.notify(); - } - - fn add_pane(&mut self, window: &mut Window, cx: &mut Context) -> Entity { - let pane = cx.new(|cx| { - let mut pane = Pane::new( - self.weak_handle(), - self.project.clone(), - self.pane_history_timestamp.clone(), - None, - NewFile.boxed_clone(), - true, - window, - cx, - ); - pane.set_can_split(Some(Arc::new(|_, _, _, _| true))); - pane - }); - cx.subscribe_in(&pane, window, Self::handle_pane_event) - .detach(); - self.panes.push(pane.clone()); - - window.focus(&pane.focus_handle(cx)); - - cx.emit(Event::PaneAdded(pane.clone())); - pane - } - - pub fn add_item_to_center( - &mut self, - item: Box, - window: &mut Window, - cx: &mut Context, - ) -> bool { - if let Some(center_pane) = self.last_active_center_pane.clone() { - if let Some(center_pane) = center_pane.upgrade() { - center_pane.update(cx, |pane, cx| { - pane.add_item(item, true, true, None, window, cx) - }); - true - } else { - false - } - } else { - false - } - } - - pub fn add_item_to_active_pane( - &mut self, - item: Box, - destination_index: Option, - focus_item: bool, - window: &mut Window, - cx: &mut App, - ) { - self.add_item( - self.active_pane.clone(), - item, - destination_index, - false, - focus_item, - window, - cx, - ) - } - - pub fn add_item( - &mut self, - pane: Entity, - item: Box, - destination_index: Option, - activate_pane: bool, - focus_item: bool, - window: &mut Window, - cx: &mut App, - ) { - pane.update(cx, |pane, cx| { - pane.add_item( - item, - activate_pane, - focus_item, - destination_index, - window, - cx, - ) - }); - } - - pub fn split_item( - &mut self, - split_direction: SplitDirection, - item: Box, - window: &mut Window, - cx: &mut Context, - ) { - let new_pane = self.split_pane(self.active_pane.clone(), split_direction, window, cx); - self.add_item(new_pane, item, None, true, true, window, cx); - } - - pub fn open_abs_path( - &mut self, - abs_path: PathBuf, - options: OpenOptions, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - cx.spawn_in(window, async move |workspace, cx| { - let open_paths_task_result = workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_paths(vec![abs_path.clone()], options, None, window, cx) - }) - .with_context(|| format!("open abs path {abs_path:?} task spawn"))? - .await; - anyhow::ensure!( - open_paths_task_result.len() == 1, - "open abs path {abs_path:?} task returned incorrect number of results" - ); - match open_paths_task_result - .into_iter() - .next() - .expect("ensured single task result") - { - Some(open_result) => { - open_result.with_context(|| format!("open abs path {abs_path:?} task join")) - } - None => anyhow::bail!("open abs path {abs_path:?} task returned None"), - } - }) - } - - pub fn split_abs_path( - &mut self, - abs_path: PathBuf, - visible: bool, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - let project_path_task = - Workspace::project_path_for_path(self.project.clone(), &abs_path, visible, cx); - cx.spawn_in(window, async move |this, cx| { - let (_, path) = project_path_task.await?; - this.update_in(cx, |this, window, cx| this.split_path(path, window, cx))? - .await - }) - } - - pub fn open_path( - &mut self, - path: impl Into, - pane: Option>, - focus_item: bool, - window: &mut Window, - cx: &mut App, - ) -> Task>> { - self.open_path_preview(path, pane, focus_item, false, true, window, cx) - } - - pub fn open_path_preview( - &mut self, - path: impl Into, - pane: Option>, - focus_item: bool, - allow_preview: bool, - activate: bool, - window: &mut Window, - cx: &mut App, - ) -> Task>> { - let pane = pane.unwrap_or_else(|| { - self.last_active_center_pane.clone().unwrap_or_else(|| { - self.panes - .first() - .expect("There must be an active pane") - .downgrade() - }) - }); - - let project_path = path.into(); - let task = self.load_path(project_path.clone(), window, cx); - window.spawn(cx, async move |cx| { - let (project_entry_id, build_item) = task.await?; - - pane.update_in(cx, |pane, window, cx| { - pane.open_item( - project_entry_id, - project_path, - focus_item, - allow_preview, - activate, - None, - window, - cx, - build_item, - ) - }) - }) - } - - pub fn split_path( - &mut self, - path: impl Into, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - self.split_path_preview(path, false, None, window, cx) - } - - pub fn split_path_preview( - &mut self, - path: impl Into, - allow_preview: bool, - split_direction: Option, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - let pane = self.last_active_center_pane.clone().unwrap_or_else(|| { - self.panes - .first() - .expect("There must be an active pane") - .downgrade() - }); - - if let Member::Pane(center_pane) = &self.center.root - && center_pane.read(cx).items_len() == 0 - { - return self.open_path(path, Some(pane), true, window, cx); - } - - let project_path = path.into(); - let task = self.load_path(project_path.clone(), window, cx); - cx.spawn_in(window, async move |this, cx| { - let (project_entry_id, build_item) = task.await?; - this.update_in(cx, move |this, window, cx| -> Option<_> { - let pane = pane.upgrade()?; - let new_pane = this.split_pane( - pane, - split_direction.unwrap_or(SplitDirection::Right), - window, - cx, - ); - new_pane.update(cx, |new_pane, cx| { - Some(new_pane.open_item( - project_entry_id, - project_path, - true, - allow_preview, - true, - None, - window, - cx, - build_item, - )) - }) - }) - .map(|option| option.context("pane was dropped"))? - }) - } - - fn load_path( - &mut self, - path: ProjectPath, - window: &mut Window, - cx: &mut App, - ) -> Task, WorkspaceItemBuilder)>> { - let registry = cx.default_global::().clone(); - registry.open_path(self.project(), &path, window, cx) - } - - pub fn find_project_item( - &self, - pane: &Entity, - project_item: &Entity, - cx: &App, - ) -> Option> - where - T: ProjectItem, - { - use project::ProjectItem as _; - let project_item = project_item.read(cx); - let entry_id = project_item.entry_id(cx); - let project_path = project_item.project_path(cx); - - let mut item = None; - if let Some(entry_id) = entry_id { - item = pane.read(cx).item_for_entry(entry_id, cx); - } - if item.is_none() - && let Some(project_path) = project_path - { - item = pane.read(cx).item_for_path(project_path, cx); - } - - item.and_then(|item| item.downcast::()) - } - - pub fn is_project_item_open( - &self, - pane: &Entity, - project_item: &Entity, - cx: &App, - ) -> bool - where - T: ProjectItem, - { - self.find_project_item::(pane, project_item, cx) - .is_some() - } - - pub fn open_project_item( - &mut self, - pane: Entity, - project_item: Entity, - activate_pane: bool, - focus_item: bool, - keep_old_preview: bool, - allow_new_preview: bool, - window: &mut Window, - cx: &mut Context, - ) -> Entity - where - T: ProjectItem, - { - let old_item_id = pane.read(cx).active_item().map(|item| item.item_id()); - - if let Some(item) = self.find_project_item(&pane, &project_item, cx) { - if !keep_old_preview - && let Some(old_id) = old_item_id - && old_id != item.item_id() - { - // switching to a different item, so unpreview old active item - pane.update(cx, |pane, _| { - pane.unpreview_item_if_preview(old_id); - }); - } - - self.activate_item(&item, activate_pane, focus_item, window, cx); - if !allow_new_preview { - pane.update(cx, |pane, _| { - pane.unpreview_item_if_preview(item.item_id()); - }); - } - return item; - } - - let item = pane.update(cx, |pane, cx| { - cx.new(|cx| { - T::for_project_item(self.project().clone(), Some(pane), project_item, window, cx) - }) - }); - let mut destination_index = None; - pane.update(cx, |pane, cx| { - if !keep_old_preview && let Some(old_id) = old_item_id { - pane.unpreview_item_if_preview(old_id); - } - if allow_new_preview { - destination_index = pane.replace_preview_item_id(item.item_id(), window, cx); - } - }); - - self.add_item( - pane, - Box::new(item.clone()), - destination_index, - activate_pane, - focus_item, - window, - cx, - ); - item - } - - pub fn open_shared_screen( - &mut self, - peer_id: PeerId, - window: &mut Window, - cx: &mut Context, - ) { - if let Some(shared_screen) = - self.shared_screen_for_peer(peer_id, &self.active_pane, window, cx) - { - self.active_pane.update(cx, |pane, cx| { - pane.add_item(Box::new(shared_screen), false, true, None, window, cx) - }); - } - } - - pub fn activate_item( - &mut self, - item: &dyn ItemHandle, - activate_pane: bool, - focus_item: bool, - window: &mut Window, - cx: &mut App, - ) -> bool { - let result = self.panes.iter().find_map(|pane| { - pane.read(cx) - .index_for_item(item) - .map(|ix| (pane.clone(), ix)) - }); - if let Some((pane, ix)) = result { - pane.update(cx, |pane, cx| { - pane.activate_item(ix, activate_pane, focus_item, window, cx) - }); - true - } else { - false - } - } - - fn activate_pane_at_index( - &mut self, - action: &ActivatePane, - window: &mut Window, - cx: &mut Context, - ) { - let panes = self.center.panes(); - if let Some(pane) = panes.get(action.0).map(|p| (*p).clone()) { - window.focus(&pane.focus_handle(cx)); - } else { - self.split_and_clone(self.active_pane.clone(), SplitDirection::Right, window, cx) - .detach(); - } - } - - fn move_item_to_pane_at_index( - &mut self, - action: &MoveItemToPane, - window: &mut Window, - cx: &mut Context, - ) { - let panes = self.center.panes(); - let destination = match panes.get(action.destination) { - Some(&destination) => destination.clone(), - None => { - if !action.clone && self.active_pane.read(cx).items_len() < 2 { - return; - } - let direction = SplitDirection::Right; - let split_off_pane = self - .find_pane_in_direction(direction, cx) - .unwrap_or_else(|| self.active_pane.clone()); - let new_pane = self.add_pane(window, cx); - if self - .center - .split(&split_off_pane, &new_pane, direction) - .log_err() - .is_none() - { - return; - }; - new_pane - } - }; - - if action.clone { - if self - .active_pane - .read(cx) - .active_item() - .is_some_and(|item| item.can_split(cx)) - { - clone_active_item( - self.database_id(), - &self.active_pane, - &destination, - action.focus, - window, - cx, - ); - return; - } - } - move_active_item( - &self.active_pane, - &destination, - action.focus, - true, - window, - cx, - ) - } - - pub fn activate_next_pane(&mut self, window: &mut Window, cx: &mut App) { - let panes = self.center.panes(); - if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) { - let next_ix = (ix + 1) % panes.len(); - let next_pane = panes[next_ix].clone(); - window.focus(&next_pane.focus_handle(cx)); - } - } - - pub fn activate_previous_pane(&mut self, window: &mut Window, cx: &mut App) { - let panes = self.center.panes(); - if let Some(ix) = panes.iter().position(|pane| **pane == self.active_pane) { - let prev_ix = cmp::min(ix.wrapping_sub(1), panes.len() - 1); - let prev_pane = panes[prev_ix].clone(); - window.focus(&prev_pane.focus_handle(cx)); - } - } - - pub fn activate_pane_in_direction( - &mut self, - direction: SplitDirection, - window: &mut Window, - cx: &mut App, - ) { - use ActivateInDirectionTarget as Target; - enum Origin { - LeftDock, - RightDock, - BottomDock, - Center, - } - - let origin: Origin = [ - (&self.left_dock, Origin::LeftDock), - (&self.right_dock, Origin::RightDock), - (&self.bottom_dock, Origin::BottomDock), - ] - .into_iter() - .find_map(|(dock, origin)| { - if dock.focus_handle(cx).contains_focused(window, cx) && dock.read(cx).is_open() { - Some(origin) - } else { - None - } - }) - .unwrap_or(Origin::Center); - - let get_last_active_pane = || { - let pane = self - .last_active_center_pane - .clone() - .unwrap_or_else(|| { - self.panes - .first() - .expect("There must be an active pane") - .downgrade() - }) - .upgrade()?; - (pane.read(cx).items_len() != 0).then_some(pane) - }; - - let try_dock = - |dock: &Entity| dock.read(cx).is_open().then(|| Target::Dock(dock.clone())); - - let target = match (origin, direction) { - // We're in the center, so we first try to go to a different pane, - // otherwise try to go to a dock. - (Origin::Center, direction) => { - if let Some(pane) = self.find_pane_in_direction(direction, cx) { - Some(Target::Pane(pane)) - } else { - match direction { - SplitDirection::Up => None, - SplitDirection::Down => try_dock(&self.bottom_dock), - SplitDirection::Left => try_dock(&self.left_dock), - SplitDirection::Right => try_dock(&self.right_dock), - } - } - } - - (Origin::LeftDock, SplitDirection::Right) => { - if let Some(last_active_pane) = get_last_active_pane() { - Some(Target::Pane(last_active_pane)) - } else { - try_dock(&self.bottom_dock).or_else(|| try_dock(&self.right_dock)) - } - } - - (Origin::LeftDock, SplitDirection::Down) - | (Origin::RightDock, SplitDirection::Down) => try_dock(&self.bottom_dock), - - (Origin::BottomDock, SplitDirection::Up) => get_last_active_pane().map(Target::Pane), - (Origin::BottomDock, SplitDirection::Left) => try_dock(&self.left_dock), - (Origin::BottomDock, SplitDirection::Right) => try_dock(&self.right_dock), - - (Origin::RightDock, SplitDirection::Left) => { - if let Some(last_active_pane) = get_last_active_pane() { - Some(Target::Pane(last_active_pane)) - } else { - try_dock(&self.bottom_dock).or_else(|| try_dock(&self.left_dock)) - } - } - - _ => None, - }; - - match target { - Some(ActivateInDirectionTarget::Pane(pane)) => { - let pane = pane.read(cx); - if let Some(item) = pane.active_item() { - item.item_focus_handle(cx).focus(window); - } else { - log::error!( - "Could not find a focus target when in switching focus in {direction} direction for a pane", - ); - } - } - Some(ActivateInDirectionTarget::Dock(dock)) => { - // Defer this to avoid a panic when the dock's active panel is already on the stack. - window.defer(cx, move |window, cx| { - let dock = dock.read(cx); - if let Some(panel) = dock.active_panel() { - panel.panel_focus_handle(cx).focus(window); - } else { - log::error!("Could not find a focus target when in switching focus in {direction} direction for a {:?} dock", dock.position()); - } - }) - } - None => {} - } - } - - pub fn move_item_to_pane_in_direction( - &mut self, - action: &MoveItemToPaneInDirection, - window: &mut Window, - cx: &mut Context, - ) { - let destination = match self.find_pane_in_direction(action.direction, cx) { - Some(destination) => destination, - None => { - if !action.clone && self.active_pane.read(cx).items_len() < 2 { - return; - } - let new_pane = self.add_pane(window, cx); - if self - .center - .split(&self.active_pane, &new_pane, action.direction) - .log_err() - .is_none() - { - return; - }; - new_pane - } - }; - - if action.clone { - if self - .active_pane - .read(cx) - .active_item() - .is_some_and(|item| item.can_split(cx)) - { - clone_active_item( - self.database_id(), - &self.active_pane, - &destination, - action.focus, - window, - cx, - ); - return; - } - } - move_active_item( - &self.active_pane, - &destination, - action.focus, - true, - window, - cx, - ); - } - - pub fn bounding_box_for_pane(&self, pane: &Entity) -> Option> { - self.center.bounding_box_for_pane(pane) - } - - pub fn find_pane_in_direction( - &mut self, - direction: SplitDirection, - cx: &App, - ) -> Option> { - self.center - .find_pane_in_direction(&self.active_pane, direction, cx) - .cloned() - } - - pub fn swap_pane_in_direction(&mut self, direction: SplitDirection, cx: &mut Context) { - if let Some(to) = self.find_pane_in_direction(direction, cx) { - self.center.swap(&self.active_pane, &to); - cx.notify(); - } - } - - pub fn move_pane_to_border(&mut self, direction: SplitDirection, cx: &mut Context) { - if self - .center - .move_to_border(&self.active_pane, direction) - .unwrap() - { - cx.notify(); - } - } - - pub fn resize_pane( - &mut self, - axis: gpui::Axis, - amount: Pixels, - window: &mut Window, - cx: &mut Context, - ) { - let docks = self.all_docks(); - let active_dock = docks - .into_iter() - .find(|dock| dock.focus_handle(cx).contains_focused(window, cx)); - - if let Some(dock) = active_dock { - let Some(panel_size) = dock.read(cx).active_panel_size(window, cx) else { - return; - }; - match dock.read(cx).position() { - DockPosition::Left => self.resize_left_dock(panel_size + amount, window, cx), - DockPosition::Bottom => self.resize_bottom_dock(panel_size + amount, window, cx), - DockPosition::Right => self.resize_right_dock(panel_size + amount, window, cx), - } - } else { - self.center - .resize(&self.active_pane, axis, amount, &self.bounds); - } - cx.notify(); - } - - pub fn reset_pane_sizes(&mut self, cx: &mut Context) { - self.center.reset_pane_sizes(); - cx.notify(); - } - - fn handle_pane_focused( - &mut self, - pane: Entity, - window: &mut Window, - cx: &mut Context, - ) { - // This is explicitly hoisted out of the following check for pane identity as - // terminal panel panes are not registered as a center panes. - self.status_bar.update(cx, |status_bar, cx| { - status_bar.set_active_pane(&pane, window, cx); - }); - if self.active_pane != pane { - self.set_active_pane(&pane, window, cx); - } - - if self.last_active_center_pane.is_none() { - self.last_active_center_pane = Some(pane.downgrade()); - } - - self.dismiss_zoomed_items_to_reveal(None, window, cx); - if pane.read(cx).is_zoomed() { - self.zoomed = Some(pane.downgrade().into()); - } else { - self.zoomed = None; - } - self.zoomed_position = None; - cx.emit(Event::ZoomChanged); - self.update_active_view_for_followers(window, cx); - pane.update(cx, |pane, _| { - pane.track_alternate_file_items(); - }); - - cx.notify(); - } - - fn set_active_pane( - &mut self, - pane: &Entity, - window: &mut Window, - cx: &mut Context, - ) { - self.active_pane = pane.clone(); - self.active_item_path_changed(window, cx); - self.last_active_center_pane = Some(pane.downgrade()); - } - - fn handle_panel_focused(&mut self, window: &mut Window, cx: &mut Context) { - self.update_active_view_for_followers(window, cx); - } - - fn handle_pane_event( - &mut self, - pane: &Entity, - event: &pane::Event, - window: &mut Window, - cx: &mut Context, - ) { - let mut serialize_workspace = true; - match event { - pane::Event::AddItem { item } => { - item.added_to_pane(self, pane.clone(), window, cx); - cx.emit(Event::ItemAdded { - item: item.boxed_clone(), - }); - } - pane::Event::Split { - direction, - clone_active_item, - } => { - if *clone_active_item { - self.split_and_clone(pane.clone(), *direction, window, cx) - .detach(); - } else { - self.split_and_move(pane.clone(), *direction, window, cx); - } - } - pane::Event::JoinIntoNext => { - self.join_pane_into_next(pane.clone(), window, cx); - } - pane::Event::JoinAll => { - self.join_all_panes(window, cx); - } - pane::Event::Remove { focus_on_pane } => { - self.remove_pane(pane.clone(), focus_on_pane.clone(), window, cx); - } - pane::Event::ActivateItem { - local, - focus_changed, - } => { - window.invalidate_character_coordinates(); - - pane.update(cx, |pane, _| { - pane.track_alternate_file_items(); - }); - if *local { - self.unfollow_in_pane(pane, window, cx); - } - serialize_workspace = *focus_changed || pane != self.active_pane(); - if pane == self.active_pane() { - self.active_item_path_changed(window, cx); - self.update_active_view_for_followers(window, cx); - } else if *local { - self.set_active_pane(pane, window, cx); - } - } - pane::Event::UserSavedItem { item, save_intent } => { - cx.emit(Event::UserSavedItem { - pane: pane.downgrade(), - item: item.boxed_clone(), - save_intent: *save_intent, - }); - serialize_workspace = false; - } - pane::Event::ChangeItemTitle => { - if *pane == self.active_pane { - self.active_item_path_changed(window, cx); - } - serialize_workspace = false; - } - pane::Event::RemovedItem { item } => { - cx.emit(Event::ActiveItemChanged); - self.update_window_edited(window, cx); - if let hash_map::Entry::Occupied(entry) = self.panes_by_item.entry(item.item_id()) - && entry.get().entity_id() == pane.entity_id() - { - entry.remove(); - } - cx.emit(Event::ItemRemoved { - item_id: item.item_id(), - }); - } - pane::Event::Focus => { - window.invalidate_character_coordinates(); - self.handle_pane_focused(pane.clone(), window, cx); - } - pane::Event::ZoomIn => { - if *pane == self.active_pane { - pane.update(cx, |pane, cx| pane.set_zoomed(true, cx)); - if pane.read(cx).has_focus(window, cx) { - self.zoomed = Some(pane.downgrade().into()); - self.zoomed_position = None; - cx.emit(Event::ZoomChanged); - } - cx.notify(); - } - } - pane::Event::ZoomOut => { - pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); - if self.zoomed_position.is_none() { - self.zoomed = None; - cx.emit(Event::ZoomChanged); - } - cx.notify(); - } - pane::Event::ItemPinned | pane::Event::ItemUnpinned => {} - } - - if serialize_workspace { - self.serialize_workspace(window, cx); - } - } - - pub fn unfollow_in_pane( - &mut self, - pane: &Entity, - window: &mut Window, - cx: &mut Context, - ) -> Option { - let leader_id = self.leader_for_pane(pane)?; - self.unfollow(leader_id, window, cx); - Some(leader_id) - } - - pub fn split_pane( - &mut self, - pane_to_split: Entity, - split_direction: SplitDirection, - window: &mut Window, - cx: &mut Context, - ) -> Entity { - let new_pane = self.add_pane(window, cx); - self.center - .split(&pane_to_split, &new_pane, split_direction) - .unwrap(); - cx.notify(); - new_pane - } - - pub fn split_and_move( - &mut self, - pane: Entity, - direction: SplitDirection, - window: &mut Window, - cx: &mut Context, - ) { - let Some(item) = pane.update(cx, |pane, cx| pane.take_active_item(window, cx)) else { - return; - }; - let new_pane = self.add_pane(window, cx); - new_pane.update(cx, |pane, cx| { - pane.add_item(item, true, true, None, window, cx) - }); - self.center.split(&pane, &new_pane, direction).unwrap(); - cx.notify(); - } - - pub fn split_and_clone( - &mut self, - pane: Entity, - direction: SplitDirection, - window: &mut Window, - cx: &mut Context, - ) -> Task>> { - let Some(item) = pane.read(cx).active_item() else { - return Task::ready(None); - }; - if !item.can_split(cx) { - return Task::ready(None); - } - let task = item.clone_on_split(self.database_id(), window, cx); - cx.spawn_in(window, async move |this, cx| { - if let Some(clone) = task.await { - this.update_in(cx, |this, window, cx| { - let new_pane = this.add_pane(window, cx); - new_pane.update(cx, |pane, cx| { - pane.add_item(clone, true, true, None, window, cx) - }); - this.center.split(&pane, &new_pane, direction).unwrap(); - cx.notify(); - new_pane - }) - .ok() - } else { - None - } - }) - } - - 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 { - join_pane_into_active(&self.active_pane, pane, window, cx); - } - if let Some(active_item) = active_item { - self.activate_item(active_item.as_ref(), true, true, window, cx); - } - cx.notify(); - } - - pub fn join_pane_into_next( - &mut self, - pane: Entity, - window: &mut Window, - cx: &mut Context, - ) { - let next_pane = self - .find_pane_in_direction(SplitDirection::Right, cx) - .or_else(|| self.find_pane_in_direction(SplitDirection::Down, cx)) - .or_else(|| self.find_pane_in_direction(SplitDirection::Left, cx)) - .or_else(|| self.find_pane_in_direction(SplitDirection::Up, cx)); - let Some(next_pane) = next_pane else { - return; - }; - move_all_items(&pane, &next_pane, window, cx); - cx.notify(); - } - - fn remove_pane( - &mut self, - pane: Entity, - focus_on: Option>, - window: &mut Window, - cx: &mut Context, - ) { - if self.center.remove(&pane).unwrap() { - self.force_remove_pane(&pane, &focus_on, window, cx); - self.unfollow_in_pane(&pane, window, cx); - self.last_leaders_by_pane.remove(&pane.downgrade()); - for removed_item in pane.read(cx).items() { - self.panes_by_item.remove(&removed_item.item_id()); - } - - cx.notify(); - } else { - self.active_item_path_changed(window, cx); - } - cx.emit(Event::PaneRemoved); - } - - pub fn panes_mut(&mut self) -> &mut [Entity] { - &mut self.panes - } - - pub fn panes(&self) -> &[Entity] { - &self.panes - } - - pub fn active_pane(&self) -> &Entity { - &self.active_pane - } - - pub fn focused_pane(&self, window: &Window, cx: &App) -> Entity { - for dock in self.all_docks() { - if dock.focus_handle(cx).contains_focused(window, cx) - && let Some(pane) = dock - .read(cx) - .active_panel() - .and_then(|panel| panel.pane(cx)) - { - return pane; - } - } - self.active_pane().clone() - } - - pub fn adjacent_pane(&mut self, window: &mut Window, cx: &mut Context) -> Entity { - self.find_pane_in_direction(SplitDirection::Right, cx) - .unwrap_or_else(|| { - self.split_pane(self.active_pane.clone(), SplitDirection::Right, window, cx) - }) - } - - pub fn pane_for(&self, handle: &dyn ItemHandle) -> Option> { - let weak_pane = self.panes_by_item.get(&handle.item_id())?; - weak_pane.upgrade() - } - - fn collaborator_left(&mut self, peer_id: PeerId, window: &mut Window, cx: &mut Context) { - self.follower_states.retain(|leader_id, state| { - if *leader_id == CollaboratorId::PeerId(peer_id) { - for item in state.items_by_leader_view_id.values() { - item.view.set_leader_id(None, window, cx); - } - false - } else { - true - } - }); - cx.notify(); - } - - pub fn start_following( - &mut self, - leader_id: impl Into, - window: &mut Window, - cx: &mut Context, - ) -> Option>> { - let leader_id = leader_id.into(); - let pane = self.active_pane().clone(); - - self.last_leaders_by_pane - .insert(pane.downgrade(), leader_id); - self.unfollow(leader_id, window, cx); - self.unfollow_in_pane(&pane, window, cx); - self.follower_states.insert( - leader_id, - FollowerState { - center_pane: pane.clone(), - dock_pane: None, - active_view_id: None, - items_by_leader_view_id: Default::default(), - }, - ); - cx.notify(); - - match leader_id { - CollaboratorId::PeerId(leader_peer_id) => { - let room_id = self.active_call()?.read(cx).room()?.read(cx).id(); - let project_id = self.project.read(cx).remote_id(); - let request = self.app_state.client.request(proto::Follow { - room_id, - project_id, - leader_id: Some(leader_peer_id), - }); - - Some(cx.spawn_in(window, async move |this, cx| { - let response = request.await?; - this.update(cx, |this, _| { - let state = this - .follower_states - .get_mut(&leader_id) - .context("following interrupted")?; - state.active_view_id = response - .active_view - .as_ref() - .and_then(|view| ViewId::from_proto(view.id.clone()?).ok()); - anyhow::Ok(()) - })??; - if let Some(view) = response.active_view { - Self::add_view_from_leader(this.clone(), leader_peer_id, &view, cx).await?; - } - this.update_in(cx, |this, window, cx| { - this.leader_updated(leader_id, window, cx) - })?; - Ok(()) - })) - } - CollaboratorId::Agent => { - self.leader_updated(leader_id, window, cx)?; - Some(Task::ready(Ok(()))) - } - } - } - - pub fn follow_next_collaborator( - &mut self, - _: &FollowNextCollaborator, - window: &mut Window, - cx: &mut Context, - ) { - let collaborators = self.project.read(cx).collaborators(); - let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) { - let mut collaborators = collaborators.keys().copied(); - for peer_id in collaborators.by_ref() { - if CollaboratorId::PeerId(peer_id) == leader_id { - break; - } - } - collaborators.next().map(CollaboratorId::PeerId) - } else if let Some(last_leader_id) = - self.last_leaders_by_pane.get(&self.active_pane.downgrade()) - { - match last_leader_id { - CollaboratorId::PeerId(peer_id) => { - if collaborators.contains_key(peer_id) { - Some(*last_leader_id) - } else { - None - } - } - CollaboratorId::Agent => Some(CollaboratorId::Agent), - } - } else { - None - }; - - let pane = self.active_pane.clone(); - let Some(leader_id) = next_leader_id.or_else(|| { - Some(CollaboratorId::PeerId( - collaborators.keys().copied().next()?, - )) - }) else { - return; - }; - if self.unfollow_in_pane(&pane, window, cx) == Some(leader_id) { - return; - } - if let Some(task) = self.start_following(leader_id, window, cx) { - task.detach_and_log_err(cx) - } - } - - pub fn follow( - &mut self, - leader_id: impl Into, - window: &mut Window, - cx: &mut Context, - ) { - let leader_id = leader_id.into(); - - if let CollaboratorId::PeerId(peer_id) = leader_id { - let Some(room) = ActiveCall::global(cx).read(cx).room() else { - return; - }; - let room = room.read(cx); - let Some(remote_participant) = room.remote_participant_for_peer_id(peer_id) else { - return; - }; - - let project = self.project.read(cx); - - let other_project_id = match remote_participant.location { - call::ParticipantLocation::External => None, - call::ParticipantLocation::UnsharedProject => None, - call::ParticipantLocation::SharedProject { project_id } => { - if Some(project_id) == project.remote_id() { - None - } else { - Some(project_id) - } - } - }; - - // if they are active in another project, follow there. - if let Some(project_id) = other_project_id { - let app_state = self.app_state.clone(); - crate::join_in_room_project(project_id, remote_participant.user.id, app_state, cx) - .detach_and_log_err(cx); - } - } - - // if you're already following, find the right pane and focus it. - if let Some(follower_state) = self.follower_states.get(&leader_id) { - window.focus(&follower_state.pane().focus_handle(cx)); - - return; - } - - // Otherwise, follow. - if let Some(task) = self.start_following(leader_id, window, cx) { - task.detach_and_log_err(cx) - } - } - - pub fn unfollow( - &mut self, - leader_id: impl Into, - window: &mut Window, - cx: &mut Context, - ) -> Option<()> { - cx.notify(); - - let leader_id = leader_id.into(); - let state = self.follower_states.remove(&leader_id)?; - for (_, item) in state.items_by_leader_view_id { - item.view.set_leader_id(None, window, cx); - } - - if let CollaboratorId::PeerId(leader_peer_id) = leader_id { - let project_id = self.project.read(cx).remote_id(); - let room_id = self.active_call()?.read(cx).room()?.read(cx).id(); - self.app_state - .client - .send(proto::Unfollow { - room_id, - project_id, - leader_id: Some(leader_peer_id), - }) - .log_err(); - } - - Some(()) - } - - pub fn is_being_followed(&self, id: impl Into) -> bool { - self.follower_states.contains_key(&id.into()) - } - - fn active_item_path_changed(&mut self, window: &mut Window, cx: &mut Context) { - cx.emit(Event::ActiveItemChanged); - let active_entry = self.active_project_path(cx); - self.project.update(cx, |project, cx| { - project.set_active_path(active_entry.clone(), cx) - }); - - if let Some(project_path) = &active_entry { - let git_store_entity = self.project.read(cx).git_store().clone(); - git_store_entity.update(cx, |git_store, cx| { - git_store.set_active_repo_for_path(project_path, cx); - }); - } - - self.update_window_title(window, cx); - } - - fn update_window_title(&mut self, window: &mut Window, cx: &mut App) { - let project = self.project().read(cx); - let mut title = String::new(); - - for (i, worktree) in project.visible_worktrees(cx).enumerate() { - let name = { - let settings_location = SettingsLocation { - worktree_id: worktree.read(cx).id(), - path: RelPath::empty(), - }; - - let settings = WorktreeSettings::get(Some(settings_location), cx); - match &settings.project_name { - Some(name) => name.as_str(), - None => worktree.read(cx).root_name_str(), - } - }; - if i > 0 { - title.push_str(", "); - } - title.push_str(name); - } - - if title.is_empty() { - title = "empty project".to_string(); - } - - if let Some(path) = self.active_item(cx).and_then(|item| item.project_path(cx)) { - let filename = path.path.file_name().or_else(|| { - Some( - project - .worktree_for_id(path.worktree_id, cx)? - .read(cx) - .root_name_str(), - ) - }); - - if let Some(filename) = filename { - title.push_str(" — "); - title.push_str(filename.as_ref()); - } - } - - if project.is_via_collab() { - title.push_str(" ↙"); - } else if project.is_shared() { - title.push_str(" ↗"); - } - - if let Some(last_title) = self.last_window_title.as_ref() - && &title == last_title - { - return; - } - window.set_window_title(&title); - SystemWindowTabController::update_tab_title( - cx, - window.window_handle().window_id(), - SharedString::from(&title), - ); - self.last_window_title = Some(title); - } - - fn update_window_edited(&mut self, window: &mut Window, cx: &mut App) { - let is_edited = !self.project.read(cx).is_disconnected(cx) && !self.dirty_items.is_empty(); - if is_edited != self.window_edited { - self.window_edited = is_edited; - window.set_window_edited(self.window_edited) - } - } - - fn update_item_dirty_state( - &mut self, - item: &dyn ItemHandle, - window: &mut Window, - cx: &mut App, - ) { - let is_dirty = item.is_dirty(cx); - let item_id = item.item_id(); - let was_dirty = self.dirty_items.contains_key(&item_id); - if is_dirty == was_dirty { - return; - } - if was_dirty { - self.dirty_items.remove(&item_id); - self.update_window_edited(window, cx); - return; - } - if let Some(window_handle) = window.window_handle().downcast::() { - let s = item.on_release( - cx, - Box::new(move |cx| { - window_handle - .update(cx, |this, window, cx| { - this.dirty_items.remove(&item_id); - this.update_window_edited(window, cx) - }) - .ok(); - }), - ); - self.dirty_items.insert(item_id, s); - self.update_window_edited(window, cx); - } - } - - fn render_notifications(&self, _window: &mut Window, _cx: &mut Context) -> Option
{ - if self.notifications.is_empty() { - None - } else { - Some( - div() - .absolute() - .right_3() - .bottom_3() - .w_112() - .h_full() - .flex() - .flex_col() - .justify_end() - .gap_2() - .children( - self.notifications - .iter() - .map(|(_, notification)| notification.clone().into_any()), - ), - ) - } - } - - // RPC handlers - - fn active_view_for_follower( - &self, - follower_project_id: Option, - window: &mut Window, - cx: &mut Context, - ) -> Option { - let (item, panel_id) = self.active_item_for_followers(window, cx); - let item = item?; - let leader_id = self - .pane_for(&*item) - .and_then(|pane| self.leader_for_pane(&pane)); - let leader_peer_id = match leader_id { - Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id), - Some(CollaboratorId::Agent) | None => None, - }; - - let item_handle = item.to_followable_item_handle(cx)?; - let id = item_handle.remote_id(&self.app_state.client, window, cx)?; - let variant = item_handle.to_state_proto(window, cx)?; - - if item_handle.is_project_item(window, cx) - && (follower_project_id.is_none() - || follower_project_id != self.project.read(cx).remote_id()) - { - return None; - } - - Some(proto::View { - id: id.to_proto(), - leader_id: leader_peer_id, - variant: Some(variant), - panel_id: panel_id.map(|id| id as i32), - }) - } - - fn handle_follow( - &mut self, - follower_project_id: Option, - window: &mut Window, - cx: &mut Context, - ) -> proto::FollowResponse { - let active_view = self.active_view_for_follower(follower_project_id, window, cx); - - cx.notify(); - proto::FollowResponse { - // TODO: Remove after version 0.145.x stabilizes. - active_view_id: active_view.as_ref().and_then(|view| view.id.clone()), - views: active_view.iter().cloned().collect(), - active_view, - } - } - - fn handle_update_followers( - &mut self, - leader_id: PeerId, - message: proto::UpdateFollowers, - _window: &mut Window, - _cx: &mut Context, - ) { - self.leader_updates_tx - .unbounded_send((leader_id, message)) - .ok(); - } - - async fn process_leader_update( - this: &WeakEntity, - leader_id: PeerId, - update: proto::UpdateFollowers, - cx: &mut AsyncWindowContext, - ) -> Result<()> { - match update.variant.context("invalid update")? { - proto::update_followers::Variant::CreateView(view) => { - let view_id = ViewId::from_proto(view.id.clone().context("invalid view id")?)?; - let should_add_view = this.update(cx, |this, _| { - if let Some(state) = this.follower_states.get_mut(&leader_id.into()) { - anyhow::Ok(!state.items_by_leader_view_id.contains_key(&view_id)) - } else { - anyhow::Ok(false) - } - })??; - - if should_add_view { - Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await? - } - } - proto::update_followers::Variant::UpdateActiveView(update_active_view) => { - let should_add_view = this.update(cx, |this, _| { - if let Some(state) = this.follower_states.get_mut(&leader_id.into()) { - state.active_view_id = update_active_view - .view - .as_ref() - .and_then(|view| ViewId::from_proto(view.id.clone()?).ok()); - - if state.active_view_id.is_some_and(|view_id| { - !state.items_by_leader_view_id.contains_key(&view_id) - }) { - anyhow::Ok(true) - } else { - anyhow::Ok(false) - } - } else { - anyhow::Ok(false) - } - })??; - - if should_add_view && let Some(view) = update_active_view.view { - Self::add_view_from_leader(this.clone(), leader_id, &view, cx).await? - } - } - proto::update_followers::Variant::UpdateView(update_view) => { - let variant = update_view.variant.context("missing update view variant")?; - let id = update_view.id.context("missing update view id")?; - let mut tasks = Vec::new(); - this.update_in(cx, |this, window, cx| { - let project = this.project.clone(); - if let Some(state) = this.follower_states.get(&leader_id.into()) { - let view_id = ViewId::from_proto(id.clone())?; - if let Some(item) = state.items_by_leader_view_id.get(&view_id) { - tasks.push(item.view.apply_update_proto( - &project, - variant.clone(), - window, - cx, - )); - } - } - anyhow::Ok(()) - })??; - try_join_all(tasks).await.log_err(); - } - } - this.update_in(cx, |this, window, cx| { - this.leader_updated(leader_id, window, cx) - })?; - Ok(()) - } - - async fn add_view_from_leader( - this: WeakEntity, - leader_id: PeerId, - view: &proto::View, - cx: &mut AsyncWindowContext, - ) -> Result<()> { - let this = this.upgrade().context("workspace dropped")?; - - let Some(id) = view.id.clone() else { - anyhow::bail!("no id for view"); - }; - let id = ViewId::from_proto(id)?; - let panel_id = view.panel_id.and_then(proto::PanelId::from_i32); - - let pane = this.update(cx, |this, _cx| { - let state = this - .follower_states - .get(&leader_id.into()) - .context("stopped following")?; - anyhow::Ok(state.pane().clone()) - })??; - let existing_item = pane.update_in(cx, |pane, window, cx| { - let client = this.read(cx).client().clone(); - pane.items().find_map(|item| { - let item = item.to_followable_item_handle(cx)?; - if item.remote_id(&client, window, cx) == Some(id) { - Some(item) - } else { - None - } - }) - })?; - let item = if let Some(existing_item) = existing_item { - existing_item - } else { - let variant = view.variant.clone(); - anyhow::ensure!(variant.is_some(), "missing view variant"); - - let task = cx.update(|window, cx| { - FollowableViewRegistry::from_state_proto(this.clone(), id, variant, window, cx) - })?; - - let Some(task) = task else { - anyhow::bail!( - "failed to construct view from leader (maybe from a different version of zed?)" - ); - }; - - let mut new_item = task.await?; - pane.update_in(cx, |pane, window, cx| { - let mut item_to_remove = None; - for (ix, item) in pane.items().enumerate() { - if let Some(item) = item.to_followable_item_handle(cx) { - match new_item.dedup(item.as_ref(), window, cx) { - Some(item::Dedup::KeepExisting) => { - new_item = - item.boxed_clone().to_followable_item_handle(cx).unwrap(); - break; - } - Some(item::Dedup::ReplaceExisting) => { - item_to_remove = Some((ix, item.item_id())); - break; - } - None => {} - } - } - } - - if let Some((ix, id)) = item_to_remove { - pane.remove_item(id, false, false, window, cx); - pane.add_item(new_item.boxed_clone(), false, false, Some(ix), window, cx); - } - })?; - - new_item - }; - - this.update_in(cx, |this, window, cx| { - let state = this.follower_states.get_mut(&leader_id.into())?; - item.set_leader_id(Some(leader_id.into()), window, cx); - state.items_by_leader_view_id.insert( - id, - FollowerView { - view: item, - location: panel_id, - }, - ); - - Some(()) - })?; - - Ok(()) - } - - fn handle_agent_location_changed(&mut self, window: &mut Window, cx: &mut Context) { - let Some(follower_state) = self.follower_states.get_mut(&CollaboratorId::Agent) else { - return; - }; - - if let Some(agent_location) = self.project.read(cx).agent_location() { - let buffer_entity_id = agent_location.buffer.entity_id(); - let view_id = ViewId { - creator: CollaboratorId::Agent, - id: buffer_entity_id.as_u64(), - }; - follower_state.active_view_id = Some(view_id); - - let item = match follower_state.items_by_leader_view_id.entry(view_id) { - hash_map::Entry::Occupied(entry) => Some(entry.into_mut()), - hash_map::Entry::Vacant(entry) => { - let existing_view = - follower_state - .center_pane - .read(cx) - .items() - .find_map(|item| { - let item = item.to_followable_item_handle(cx)?; - if item.buffer_kind(cx) == ItemBufferKind::Singleton - && item.project_item_model_ids(cx).as_slice() - == [buffer_entity_id] - { - Some(item) - } else { - None - } - }); - let view = existing_view.or_else(|| { - agent_location.buffer.upgrade().and_then(|buffer| { - cx.update_default_global(|registry: &mut ProjectItemRegistry, cx| { - registry.build_item(buffer, self.project.clone(), None, window, cx) - })? - .to_followable_item_handle(cx) - }) - }); - - view.map(|view| { - entry.insert(FollowerView { - view, - location: None, - }) - }) - } - }; - - if let Some(item) = item { - item.view - .set_leader_id(Some(CollaboratorId::Agent), window, cx); - item.view - .update_agent_location(agent_location.position, window, cx); - } - } else { - follower_state.active_view_id = None; - } - - self.leader_updated(CollaboratorId::Agent, window, cx); - } - - pub fn update_active_view_for_followers(&mut self, window: &mut Window, cx: &mut App) { - let mut is_project_item = true; - let mut update = proto::UpdateActiveView::default(); - if window.is_window_active() { - let (active_item, panel_id) = self.active_item_for_followers(window, cx); - - if let Some(item) = active_item - && item.item_focus_handle(cx).contains_focused(window, cx) - { - let leader_id = self - .pane_for(&*item) - .and_then(|pane| self.leader_for_pane(&pane)); - let leader_peer_id = match leader_id { - Some(CollaboratorId::PeerId(peer_id)) => Some(peer_id), - Some(CollaboratorId::Agent) | None => None, - }; - - if let Some(item) = item.to_followable_item_handle(cx) { - let id = item - .remote_id(&self.app_state.client, window, cx) - .map(|id| id.to_proto()); - - if let Some(id) = id - && let Some(variant) = item.to_state_proto(window, cx) - { - let view = Some(proto::View { - id: id.clone(), - leader_id: leader_peer_id, - variant: Some(variant), - panel_id: panel_id.map(|id| id as i32), - }); - - is_project_item = item.is_project_item(window, cx); - update = proto::UpdateActiveView { - view, - // TODO: Remove after version 0.145.x stabilizes. - id, - leader_id: leader_peer_id, - }; - }; - } - } - } - - let active_view_id = update.view.as_ref().and_then(|view| view.id.as_ref()); - if active_view_id != self.last_active_view_id.as_ref() { - self.last_active_view_id = active_view_id.cloned(); - self.update_followers( - is_project_item, - proto::update_followers::Variant::UpdateActiveView(update), - window, - cx, - ); - } - } - - fn active_item_for_followers( - &self, - window: &mut Window, - cx: &mut App, - ) -> (Option>, Option) { - let mut active_item = None; - let mut panel_id = None; - for dock in self.all_docks() { - if dock.focus_handle(cx).contains_focused(window, cx) - && let Some(panel) = dock.read(cx).active_panel() - && let Some(pane) = panel.pane(cx) - && let Some(item) = pane.read(cx).active_item() - { - active_item = Some(item); - panel_id = panel.remote_id(); - break; - } - } - - if active_item.is_none() { - active_item = self.active_pane().read(cx).active_item(); - } - (active_item, panel_id) - } - - fn update_followers( - &self, - project_only: bool, - update: proto::update_followers::Variant, - _: &mut Window, - cx: &mut App, - ) -> Option<()> { - // If this update only applies to for followers in the current project, - // then skip it unless this project is shared. If it applies to all - // followers, regardless of project, then set `project_id` to none, - // indicating that it goes to all followers. - let project_id = if project_only { - Some(self.project.read(cx).remote_id()?) - } else { - None - }; - self.app_state().workspace_store.update(cx, |store, cx| { - store.update_followers(project_id, update, cx) - }) - } - - pub fn leader_for_pane(&self, pane: &Entity) -> Option { - self.follower_states.iter().find_map(|(leader_id, state)| { - if state.center_pane == *pane || state.dock_pane.as_ref() == Some(pane) { - Some(*leader_id) - } else { - None - } - }) - } - - fn leader_updated( - &mut self, - leader_id: impl Into, - window: &mut Window, - cx: &mut Context, - ) -> Option> { - cx.notify(); - - let leader_id = leader_id.into(); - let (panel_id, item) = match leader_id { - CollaboratorId::PeerId(peer_id) => self.active_item_for_peer(peer_id, window, cx)?, - CollaboratorId::Agent => (None, self.active_item_for_agent()?), - }; - - let state = self.follower_states.get(&leader_id)?; - let mut transfer_focus = state.center_pane.read(cx).has_focus(window, cx); - let pane; - if let Some(panel_id) = panel_id { - pane = self - .activate_panel_for_proto_id(panel_id, window, cx)? - .pane(cx)?; - let state = self.follower_states.get_mut(&leader_id)?; - state.dock_pane = Some(pane.clone()); - } else { - pane = state.center_pane.clone(); - let state = self.follower_states.get_mut(&leader_id)?; - if let Some(dock_pane) = state.dock_pane.take() { - transfer_focus |= dock_pane.focus_handle(cx).contains_focused(window, cx); - } - } - - pane.update(cx, |pane, cx| { - let focus_active_item = pane.has_focus(window, cx) || transfer_focus; - if let Some(index) = pane.index_for_item(item.as_ref()) { - pane.activate_item(index, false, false, window, cx); - } else { - pane.add_item(item.boxed_clone(), false, false, None, window, cx) - } - - if focus_active_item { - pane.focus_active_item(window, cx) - } - }); - - Some(item) - } - - fn active_item_for_agent(&self) -> Option> { - let state = self.follower_states.get(&CollaboratorId::Agent)?; - let active_view_id = state.active_view_id?; - Some( - state - .items_by_leader_view_id - .get(&active_view_id)? - .view - .boxed_clone(), - ) - } - - fn active_item_for_peer( - &self, - peer_id: PeerId, - window: &mut Window, - cx: &mut Context, - ) -> Option<(Option, Box)> { - let call = self.active_call()?; - let room = call.read(cx).room()?.read(cx); - let participant = room.remote_participant_for_peer_id(peer_id)?; - let leader_in_this_app; - let leader_in_this_project; - match participant.location { - call::ParticipantLocation::SharedProject { project_id } => { - leader_in_this_app = true; - leader_in_this_project = Some(project_id) == self.project.read(cx).remote_id(); - } - call::ParticipantLocation::UnsharedProject => { - leader_in_this_app = true; - leader_in_this_project = false; - } - call::ParticipantLocation::External => { - leader_in_this_app = false; - leader_in_this_project = false; - } - }; - let state = self.follower_states.get(&peer_id.into())?; - let mut item_to_activate = None; - if let (Some(active_view_id), true) = (state.active_view_id, leader_in_this_app) { - if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) - && (leader_in_this_project || !item.view.is_project_item(window, cx)) - { - item_to_activate = Some((item.location, item.view.boxed_clone())); - } - } else if let Some(shared_screen) = - self.shared_screen_for_peer(peer_id, &state.center_pane, window, cx) - { - item_to_activate = Some((None, Box::new(shared_screen))); - } - item_to_activate - } - - fn shared_screen_for_peer( - &self, - peer_id: PeerId, - pane: &Entity, - window: &mut Window, - cx: &mut App, - ) -> Option> { - let call = self.active_call()?; - let room = call.read(cx).room()?.clone(); - let participant = room.read(cx).remote_participant_for_peer_id(peer_id)?; - let track = participant.video_tracks.values().next()?.clone(); - let user = participant.user.clone(); - - for item in pane.read(cx).items_of_type::() { - if item.read(cx).peer_id == peer_id { - return Some(item); - } - } - - Some(cx.new(|cx| SharedScreen::new(track, peer_id, user.clone(), room.clone(), window, cx))) - } - - pub fn on_window_activation_changed(&mut self, window: &mut Window, cx: &mut Context) { - if window.is_window_active() { - self.update_active_view_for_followers(window, cx); - - if let Some(database_id) = self.database_id { - cx.background_spawn(persistence::DB.update_timestamp(database_id)) - .detach(); - } - } else { - for pane in &self.panes { - pane.update(cx, |pane, cx| { - if let Some(item) = pane.active_item() { - item.workspace_deactivated(window, cx); - } - for item in pane.items() { - if matches!( - item.workspace_settings(cx).autosave, - AutosaveSetting::OnWindowChange | AutosaveSetting::OnFocusChange - ) { - Pane::autosave_item(item.as_ref(), self.project.clone(), window, cx) - .detach_and_log_err(cx); - } - } - }); - } - } - } - - pub fn active_call(&self) -> Option<&Entity> { - self.active_call.as_ref().map(|(call, _)| call) - } - - fn on_active_call_event( - &mut self, - _: &Entity, - event: &call::room::Event, - window: &mut Window, - cx: &mut Context, - ) { - match event { - call::room::Event::ParticipantLocationChanged { participant_id } - | call::room::Event::RemoteVideoTracksChanged { participant_id } => { - self.leader_updated(participant_id, window, cx); - } - _ => {} - } - } - - pub fn database_id(&self) -> Option { - self.database_id - } - - pub fn session_id(&self) -> Option { - self.session_id.clone() - } - - pub fn root_paths(&self, cx: &App) -> Vec> { - let project = self.project().read(cx); - project - .visible_worktrees(cx) - .map(|worktree| worktree.read(cx).abs_path()) - .collect::>() - } - - fn remove_panes(&mut self, member: Member, window: &mut Window, cx: &mut Context) { - match member { - Member::Axis(PaneAxis { members, .. }) => { - for child in members.iter() { - self.remove_panes(child.clone(), window, cx) - } - } - Member::Pane(pane) => { - self.force_remove_pane(&pane, &None, window, cx); - } - } - } - - fn remove_from_session(&mut self, window: &mut Window, cx: &mut App) -> Task<()> { - self.session_id.take(); - self.serialize_workspace_internal(window, cx) - } - - fn force_remove_pane( - &mut self, - pane: &Entity, - focus_on: &Option>, - window: &mut Window, - cx: &mut Context, - ) { - self.panes.retain(|p| p != pane); - if let Some(focus_on) = focus_on { - focus_on.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx))); - } else if self.active_pane() == pane { - self.panes - .last() - .unwrap() - .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx))); - } - if self.last_active_center_pane == Some(pane.downgrade()) { - self.last_active_center_pane = None; - } - cx.notify(); - } - - fn serialize_workspace(&mut self, window: &mut Window, cx: &mut Context) { - if self._schedule_serialize_workspace.is_none() { - self._schedule_serialize_workspace = - Some(cx.spawn_in(window, async move |this, cx| { - cx.background_executor() - .timer(SERIALIZATION_THROTTLE_TIME) - .await; - this.update_in(cx, |this, window, cx| { - this.serialize_workspace_internal(window, cx).detach(); - this._schedule_serialize_workspace.take(); - }) - .log_err(); - })); - } - } - - fn serialize_workspace_internal(&self, window: &mut Window, cx: &mut App) -> Task<()> { - let Some(database_id) = self.database_id() else { - return Task::ready(()); - }; - - fn serialize_pane_handle( - pane_handle: &Entity, - window: &mut Window, - cx: &mut App, - ) -> SerializedPane { - let (items, active, pinned_count) = { - let pane = pane_handle.read(cx); - let active_item_id = pane.active_item().map(|item| item.item_id()); - ( - pane.items() - .filter_map(|handle| { - let handle = handle.to_serializable_item_handle(cx)?; - - Some(SerializedItem { - kind: Arc::from(handle.serialized_item_kind()), - item_id: handle.item_id().as_u64(), - active: Some(handle.item_id()) == active_item_id, - preview: pane.is_active_preview_item(handle.item_id()), - }) - }) - .collect::>(), - pane.has_focus(window, cx), - pane.pinned_count(), - ) - }; - - SerializedPane::new(items, active, pinned_count) - } - - fn build_serialized_pane_group( - pane_group: &Member, - window: &mut Window, - cx: &mut App, - ) -> SerializedPaneGroup { - match pane_group { - Member::Axis(PaneAxis { - axis, - members, - flexes, - bounding_boxes: _, - }) => SerializedPaneGroup::Group { - axis: SerializedAxis(*axis), - children: members - .iter() - .map(|member| build_serialized_pane_group(member, window, cx)) - .collect::>(), - flexes: Some(flexes.lock().clone()), - }, - Member::Pane(pane_handle) => { - SerializedPaneGroup::Pane(serialize_pane_handle(pane_handle, window, cx)) - } - } - } - - fn build_serialized_docks( - this: &Workspace, - window: &mut Window, - cx: &mut App, - ) -> DockStructure { - let left_dock = this.left_dock.read(cx); - let left_visible = left_dock.is_open(); - let left_active_panel = left_dock - .active_panel() - .map(|panel| panel.persistent_name().to_string()); - let left_dock_zoom = left_dock - .active_panel() - .map(|panel| panel.is_zoomed(window, cx)) - .unwrap_or(false); - - let right_dock = this.right_dock.read(cx); - let right_visible = right_dock.is_open(); - let right_active_panel = right_dock - .active_panel() - .map(|panel| panel.persistent_name().to_string()); - let right_dock_zoom = right_dock - .active_panel() - .map(|panel| panel.is_zoomed(window, cx)) - .unwrap_or(false); - - let bottom_dock = this.bottom_dock.read(cx); - let bottom_visible = bottom_dock.is_open(); - let bottom_active_panel = bottom_dock - .active_panel() - .map(|panel| panel.persistent_name().to_string()); - let bottom_dock_zoom = bottom_dock - .active_panel() - .map(|panel| panel.is_zoomed(window, cx)) - .unwrap_or(false); - - DockStructure { - left: DockData { - visible: left_visible, - active_panel: left_active_panel, - zoom: left_dock_zoom, - }, - right: DockData { - visible: right_visible, - active_panel: right_active_panel, - zoom: right_dock_zoom, - }, - bottom: DockData { - visible: bottom_visible, - active_panel: bottom_active_panel, - zoom: bottom_dock_zoom, - }, - } - } - - match self.serialize_workspace_location(cx) { - WorkspaceLocation::Location(location, paths) => { - let breakpoints = self.project.update(cx, |project, cx| { - project - .breakpoint_store() - .read(cx) - .all_source_breakpoints(cx) - }); - let user_toolchains = self - .project - .read(cx) - .user_toolchains(cx) - .unwrap_or_default(); - - let center_group = build_serialized_pane_group(&self.center.root, window, cx); - let docks = build_serialized_docks(self, window, cx); - let window_bounds = Some(SerializedWindowBounds(window.window_bounds())); - - let serialized_workspace = SerializedWorkspace { - id: database_id, - location, - paths, - center_group, - window_bounds, - display: Default::default(), - docks, - centered_layout: self.centered_layout, - session_id: self.session_id.clone(), - breakpoints, - window_id: Some(window.window_handle().window_id().as_u64()), - user_toolchains, - }; - - window.spawn(cx, async move |_| { - persistence::DB.save_workspace(serialized_workspace).await; - }) - } - WorkspaceLocation::DetachFromSession => window.spawn(cx, async move |_| { - persistence::DB - .set_session_id(database_id, None) - .await - .log_err(); - }), - WorkspaceLocation::None => Task::ready(()), - } - } - - fn serialize_workspace_location(&self, cx: &App) -> WorkspaceLocation { - let paths = PathList::new(&self.root_paths(cx)); - if let Some(connection) = self.project.read(cx).remote_connection_options(cx) { - WorkspaceLocation::Location(SerializedWorkspaceLocation::Remote(connection), paths) - } else if self.project.read(cx).is_local() { - if !paths.is_empty() { - WorkspaceLocation::Location(SerializedWorkspaceLocation::Local, paths) - } else { - WorkspaceLocation::DetachFromSession - } - } else { - WorkspaceLocation::None - } - } - - fn update_history(&self, cx: &mut App) { - let Some(id) = self.database_id() else { - return; - }; - if !self.project.read(cx).is_local() { - 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); - }); - } - } - - async fn serialize_items( - this: &WeakEntity, - items_rx: UnboundedReceiver>, - cx: &mut AsyncWindowContext, - ) -> Result<()> { - const CHUNK_SIZE: usize = 200; - - let mut serializable_items = items_rx.ready_chunks(CHUNK_SIZE); - - while let Some(items_received) = serializable_items.next().await { - let unique_items = - items_received - .into_iter() - .fold(HashMap::default(), |mut acc, item| { - acc.entry(item.item_id()).or_insert(item); - acc - }); - - // We use into_iter() here so that the references to the items are moved into - // the tasks and not kept alive while we're sleeping. - for (_, item) in unique_items.into_iter() { - if let Ok(Some(task)) = this.update_in(cx, |workspace, window, cx| { - item.serialize(workspace, false, window, cx) - }) { - cx.background_spawn(async move { task.await.log_err() }) - .detach(); - } - } - - cx.background_executor() - .timer(SERIALIZATION_THROTTLE_TIME) - .await; - } - - Ok(()) - } - - pub(crate) fn enqueue_item_serialization( - &mut self, - item: Box, - ) -> Result<()> { - self.serializable_items_tx - .unbounded_send(item) - .map_err(|err| anyhow!("failed to send serializable item over channel: {err}")) - } - - pub(crate) fn load_workspace( - serialized_workspace: SerializedWorkspace, - paths_to_open: Vec>, - window: &mut Window, - cx: &mut Context, - ) -> Task>>>> { - cx.spawn_in(window, async move |workspace, cx| { - let project = workspace.read_with(cx, |workspace, _| workspace.project().clone())?; - - let mut center_group = None; - let mut center_items = None; - - // Traverse the splits tree and add to things - if let Some((group, active_pane, items)) = serialized_workspace - .center_group - .deserialize(&project, serialized_workspace.id, workspace.clone(), cx) - .await - { - center_items = Some(items); - center_group = Some((group, active_pane)) - } - - let mut items_by_project_path = HashMap::default(); - let mut item_ids_by_kind = HashMap::default(); - let mut all_deserialized_items = Vec::default(); - cx.update(|_, cx| { - for item in center_items.unwrap_or_default().into_iter().flatten() { - if let Some(serializable_item_handle) = item.to_serializable_item_handle(cx) { - item_ids_by_kind - .entry(serializable_item_handle.serialized_item_kind()) - .or_insert(Vec::new()) - .push(item.item_id().as_u64() as ItemId); - } - - if let Some(project_path) = item.project_path(cx) { - items_by_project_path.insert(project_path, item.clone()); - } - all_deserialized_items.push(item); - } - })?; - - let opened_items = paths_to_open - .into_iter() - .map(|path_to_open| { - path_to_open - .and_then(|path_to_open| items_by_project_path.remove(&path_to_open)) - }) - .collect::>(); - - // Remove old panes from workspace panes list - workspace.update_in(cx, |workspace, window, cx| { - if let Some((center_group, active_pane)) = center_group { - workspace.remove_panes(workspace.center.root.clone(), window, cx); - - // Swap workspace center group - workspace.center = PaneGroup::with_root(center_group); - if let Some(active_pane) = active_pane { - workspace.set_active_pane(&active_pane, window, cx); - cx.focus_self(window); - } else { - workspace.set_active_pane(&workspace.center.first_pane(), window, cx); - } - } - - let docks = serialized_workspace.docks; - - for (dock, serialized_dock) in [ - (&mut workspace.right_dock, docks.right), - (&mut workspace.left_dock, docks.left), - (&mut workspace.bottom_dock, docks.bottom), - ] - .iter_mut() - { - dock.update(cx, |dock, cx| { - dock.serialized_dock = Some(serialized_dock.clone()); - dock.restore_state(window, cx); - }); - } - - cx.notify(); - })?; - - let _ = project - .update(cx, |project, cx| { - project - .breakpoint_store() - .update(cx, |breakpoint_store, cx| { - breakpoint_store - .with_serialized_breakpoints(serialized_workspace.breakpoints, cx) - }) - })? - .await; - - // Clean up all the items that have _not_ been loaded. Our ItemIds aren't stable. That means - // after loading the items, we might have different items and in order to avoid - // the database filling up, we delete items that haven't been loaded now. - // - // The items that have been loaded, have been saved after they've been added to the workspace. - let clean_up_tasks = workspace.update_in(cx, |_, window, cx| { - item_ids_by_kind - .into_iter() - .map(|(item_kind, loaded_items)| { - SerializableItemRegistry::cleanup( - item_kind, - serialized_workspace.id, - loaded_items, - window, - cx, - ) - .log_err() - }) - .collect::>() - })?; - - futures::future::join_all(clean_up_tasks).await; - - workspace - .update_in(cx, |workspace, window, cx| { - // Serialize ourself to make sure our timestamps and any pane / item changes are replicated - workspace.serialize_workspace_internal(window, cx).detach(); - - // Ensure that we mark the window as edited if we did load dirty items - workspace.update_window_edited(window, cx); - }) - .ok(); - - Ok(opened_items) - }) - } - - fn actions(&self, div: Div, window: &mut Window, cx: &mut Context) -> Div { - self.add_workspace_actions_listeners(div, window, cx) - .on_action(cx.listener( - |_workspace, action_sequence: &settings::ActionSequence, window, cx| { - for action in &action_sequence.0 { - window.dispatch_action(action.boxed_clone(), cx); - } - }, - )) - .on_action(cx.listener(Self::close_inactive_items_and_panes)) - .on_action(cx.listener(Self::close_all_items_and_panes)) - .on_action(cx.listener(Self::save_all)) - .on_action(cx.listener(Self::send_keystrokes)) - .on_action(cx.listener(Self::add_folder_to_project)) - .on_action(cx.listener(Self::follow_next_collaborator)) - .on_action(cx.listener(Self::close_window)) - .on_action(cx.listener(Self::activate_pane_at_index)) - .on_action(cx.listener(Self::move_item_to_pane_at_index)) - .on_action(cx.listener(Self::move_focused_panel_to_next_position)) - .on_action(cx.listener(Self::toggle_edit_predictions_all_files)) - .on_action(cx.listener(|workspace, _: &Unfollow, window, cx| { - let pane = workspace.active_pane().clone(); - workspace.unfollow_in_pane(&pane, window, cx); - })) - .on_action(cx.listener(|workspace, action: &Save, window, cx| { - workspace - .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), window, cx) - .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None); - })) - .on_action(cx.listener(|workspace, _: &SaveWithoutFormat, window, cx| { - workspace - .save_active_item(SaveIntent::SaveWithoutFormat, window, cx) - .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None); - })) - .on_action(cx.listener(|workspace, _: &SaveAs, window, cx| { - workspace - .save_active_item(SaveIntent::SaveAs, window, cx) - .detach_and_prompt_err("Failed to save", window, cx, |_, _, _| None); - })) - .on_action( - cx.listener(|workspace, _: &ActivatePreviousPane, window, cx| { - workspace.activate_previous_pane(window, cx) - }), - ) - .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| { - workspace.activate_next_pane(window, cx) - })) - .on_action( - cx.listener(|workspace, _: &ActivateNextWindow, _window, cx| { - workspace.activate_next_window(cx) - }), - ) - .on_action( - cx.listener(|workspace, _: &ActivatePreviousWindow, _window, cx| { - workspace.activate_previous_window(cx) - }), - ) - .on_action(cx.listener(|workspace, _: &ActivatePaneLeft, window, cx| { - workspace.activate_pane_in_direction(SplitDirection::Left, window, cx) - })) - .on_action(cx.listener(|workspace, _: &ActivatePaneRight, window, cx| { - workspace.activate_pane_in_direction(SplitDirection::Right, window, cx) - })) - .on_action(cx.listener(|workspace, _: &ActivatePaneUp, window, cx| { - workspace.activate_pane_in_direction(SplitDirection::Up, window, cx) - })) - .on_action(cx.listener(|workspace, _: &ActivatePaneDown, window, cx| { - workspace.activate_pane_in_direction(SplitDirection::Down, window, cx) - })) - .on_action(cx.listener(|workspace, _: &ActivateNextPane, window, cx| { - workspace.activate_next_pane(window, cx) - })) - .on_action(cx.listener( - |workspace, action: &MoveItemToPaneInDirection, window, cx| { - workspace.move_item_to_pane_in_direction(action, window, cx) - }, - )) - .on_action(cx.listener(|workspace, _: &SwapPaneLeft, _, cx| { - workspace.swap_pane_in_direction(SplitDirection::Left, cx) - })) - .on_action(cx.listener(|workspace, _: &SwapPaneRight, _, cx| { - workspace.swap_pane_in_direction(SplitDirection::Right, cx) - })) - .on_action(cx.listener(|workspace, _: &SwapPaneUp, _, cx| { - workspace.swap_pane_in_direction(SplitDirection::Up, cx) - })) - .on_action(cx.listener(|workspace, _: &SwapPaneDown, _, cx| { - workspace.swap_pane_in_direction(SplitDirection::Down, cx) - })) - .on_action(cx.listener(|workspace, _: &SwapPaneAdjacent, window, cx| { - const DIRECTION_PRIORITY: [SplitDirection; 4] = [ - SplitDirection::Down, - SplitDirection::Up, - SplitDirection::Right, - SplitDirection::Left, - ]; - for dir in DIRECTION_PRIORITY { - if workspace.find_pane_in_direction(dir, cx).is_some() { - workspace.swap_pane_in_direction(dir, cx); - workspace.activate_pane_in_direction(dir.opposite(), window, cx); - break; - } - } - })) - .on_action(cx.listener(|workspace, _: &MovePaneLeft, _, cx| { - workspace.move_pane_to_border(SplitDirection::Left, cx) - })) - .on_action(cx.listener(|workspace, _: &MovePaneRight, _, cx| { - workspace.move_pane_to_border(SplitDirection::Right, cx) - })) - .on_action(cx.listener(|workspace, _: &MovePaneUp, _, cx| { - workspace.move_pane_to_border(SplitDirection::Up, cx) - })) - .on_action(cx.listener(|workspace, _: &MovePaneDown, _, cx| { - workspace.move_pane_to_border(SplitDirection::Down, cx) - })) - .on_action(cx.listener(|this, _: &ToggleLeftDock, window, cx| { - this.toggle_dock(DockPosition::Left, window, cx); - })) - .on_action(cx.listener( - |workspace: &mut Workspace, _: &ToggleRightDock, window, cx| { - workspace.toggle_dock(DockPosition::Right, window, cx); - }, - )) - .on_action(cx.listener( - |workspace: &mut Workspace, _: &ToggleBottomDock, window, cx| { - workspace.toggle_dock(DockPosition::Bottom, window, cx); - }, - )) - .on_action(cx.listener( - |workspace: &mut Workspace, _: &CloseActiveDock, window, cx| { - if !workspace.close_active_dock(window, cx) { - cx.propagate(); - } - }, - )) - .on_action( - cx.listener(|workspace: &mut Workspace, _: &CloseAllDocks, window, cx| { - workspace.close_all_docks(window, cx); - }), - ) - .on_action(cx.listener(Self::toggle_all_docks)) - .on_action(cx.listener( - |workspace: &mut Workspace, _: &ClearAllNotifications, _, cx| { - workspace.clear_all_notifications(cx); - }, - )) - .on_action(cx.listener( - |workspace: &mut Workspace, _: &ClearNavigationHistory, window, cx| { - workspace.clear_navigation_history(window, cx); - }, - )) - .on_action(cx.listener( - |workspace: &mut Workspace, _: &SuppressNotification, _, cx| { - if let Some((notification_id, _)) = workspace.notifications.pop() { - workspace.suppress_notification(¬ification_id, cx); - } - }, - )) - .on_action(cx.listener( - |workspace: &mut Workspace, _: &ReopenClosedItem, window, cx| { - workspace.reopen_closed_item(window, cx).detach(); - }, - )) - .on_action(cx.listener( - |workspace: &mut Workspace, _: &ResetActiveDockSize, window, cx| { - for dock in workspace.all_docks() { - if dock.focus_handle(cx).contains_focused(window, cx) { - let Some(panel) = dock.read(cx).active_panel() else { - return; - }; - - // Set to `None`, then the size will fall back to the default. - panel.clone().set_size(None, window, cx); - - return; - } - } - }, - )) - .on_action(cx.listener( - |workspace: &mut Workspace, _: &ResetOpenDocksSize, window, cx| { - for dock in workspace.all_docks() { - if let Some(panel) = dock.read(cx).visible_panel() { - // Set to `None`, then the size will fall back to the default. - panel.clone().set_size(None, window, cx); - } - } - }, - )) - .on_action(cx.listener( - |workspace: &mut Workspace, act: &IncreaseActiveDockSize, window, cx| { - adjust_active_dock_size_by_px( - px_with_ui_font_fallback(act.px, cx), - workspace, - window, - cx, - ); - }, - )) - .on_action(cx.listener( - |workspace: &mut Workspace, act: &DecreaseActiveDockSize, window, cx| { - adjust_active_dock_size_by_px( - px_with_ui_font_fallback(act.px, cx) * -1., - workspace, - window, - cx, - ); - }, - )) - .on_action(cx.listener( - |workspace: &mut Workspace, act: &IncreaseOpenDocksSize, window, cx| { - adjust_open_docks_size_by_px( - px_with_ui_font_fallback(act.px, cx), - workspace, - window, - cx, - ); - }, - )) - .on_action(cx.listener( - |workspace: &mut Workspace, act: &DecreaseOpenDocksSize, window, cx| { - adjust_open_docks_size_by_px( - px_with_ui_font_fallback(act.px, cx) * -1., - workspace, - window, - cx, - ); - }, - )) - .on_action(cx.listener(Workspace::toggle_centered_layout)) - .on_action(cx.listener( - |workspace: &mut Workspace, _action: &pane::ActivateNextItem, window, cx| { - if let Some(active_dock) = workspace.active_dock(window, cx) { - let dock = active_dock.read(cx); - if let Some(active_panel) = dock.active_panel() { - if active_panel.pane(cx).is_none() { - let mut recent_pane: Option> = None; - let mut recent_timestamp = 0; - for pane_handle in workspace.panes() { - let pane = pane_handle.read(cx); - for entry in pane.activation_history() { - if entry.timestamp > recent_timestamp { - recent_timestamp = entry.timestamp; - recent_pane = Some(pane_handle.clone()); - } - } - } - - if let Some(pane) = recent_pane { - pane.update(cx, |pane, cx| { - let current_index = pane.active_item_index(); - let items_len = pane.items_len(); - if items_len > 0 { - let next_index = if current_index + 1 < items_len { - current_index + 1 - } else { - 0 - }; - pane.activate_item( - next_index, false, false, window, cx, - ); - } - }); - return; - } - } - } - } - cx.propagate(); - }, - )) - .on_action(cx.listener( - |workspace: &mut Workspace, _action: &pane::ActivatePreviousItem, window, cx| { - if let Some(active_dock) = workspace.active_dock(window, cx) { - let dock = active_dock.read(cx); - if let Some(active_panel) = dock.active_panel() { - if active_panel.pane(cx).is_none() { - let mut recent_pane: Option> = None; - let mut recent_timestamp = 0; - for pane_handle in workspace.panes() { - let pane = pane_handle.read(cx); - for entry in pane.activation_history() { - if entry.timestamp > recent_timestamp { - recent_timestamp = entry.timestamp; - recent_pane = Some(pane_handle.clone()); - } - } - } - - if let Some(pane) = recent_pane { - pane.update(cx, |pane, cx| { - let current_index = pane.active_item_index(); - let items_len = pane.items_len(); - if items_len > 0 { - let prev_index = if current_index > 0 { - current_index - 1 - } else { - items_len.saturating_sub(1) - }; - pane.activate_item( - prev_index, false, false, window, cx, - ); - } - }); - return; - } - } - } - } - cx.propagate(); - }, - )) - .on_action(cx.listener(Workspace::cancel)) - } - - #[cfg(any(test, feature = "test-support"))] - pub fn set_random_database_id(&mut self) { - self.database_id = Some(WorkspaceId(Uuid::new_v4().as_u64_pair().0 as i64)); - } - - #[cfg(any(test, feature = "test-support"))] - pub fn test_new(project: Entity, window: &mut Window, cx: &mut Context) -> Self { - use node_runtime::NodeRuntime; - use session::Session; - - let client = project.read(cx).client(); - let user_store = project.read(cx).user_store(); - let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx)); - let session = cx.new(|cx| AppSession::new(Session::test(), cx)); - window.activate_window(); - let app_state = Arc::new(AppState { - languages: project.read(cx).languages().clone(), - workspace_store, - client, - user_store, - fs: project.read(cx).fs().clone(), - build_window_options: |_, _| Default::default(), - node_runtime: NodeRuntime::unavailable(), - session, - }); - let workspace = Self::new(Default::default(), project, app_state, window, cx); - workspace - .active_pane - .update(cx, |pane, cx| window.focus(&pane.focus_handle(cx))); - workspace - } - - pub fn register_action( - &mut self, - callback: impl Fn(&mut Self, &A, &mut Window, &mut Context) + 'static, - ) -> &mut Self { - let callback = Arc::new(callback); - - self.workspace_actions.push(Box::new(move |div, _, _, cx| { - let callback = callback.clone(); - div.on_action(cx.listener(move |workspace, event, window, cx| { - (callback)(workspace, event, window, cx) - })) - })); - self - } - pub fn register_action_renderer( - &mut self, - callback: impl Fn(Div, &Workspace, &mut Window, &mut Context) -> Div + 'static, - ) -> &mut Self { - self.workspace_actions.push(Box::new(callback)); - self - } - - fn add_workspace_actions_listeners( - &self, - mut div: Div, - window: &mut Window, - cx: &mut Context, - ) -> Div { - for action in self.workspace_actions.iter() { - div = (action)(div, self, window, cx) - } - div - } - - pub fn has_active_modal(&self, _: &mut Window, cx: &mut App) -> bool { - self.modal_layer.read(cx).has_active_modal() - } - - pub fn active_modal(&self, cx: &App) -> Option> { - self.modal_layer.read(cx).active_modal() - } - - pub fn toggle_modal(&mut self, window: &mut Window, cx: &mut App, build: B) - where - B: FnOnce(&mut Window, &mut Context) -> V, - { - self.modal_layer.update(cx, |modal_layer, cx| { - modal_layer.toggle_modal(window, cx, build) - }) - } - - pub fn hide_modal(&mut self, window: &mut Window, cx: &mut App) -> bool { - self.modal_layer - .update(cx, |modal_layer, cx| modal_layer.hide_modal(window, cx)) - } - - pub fn toggle_status_toast(&mut self, entity: Entity, cx: &mut App) { - self.toast_layer - .update(cx, |toast_layer, cx| toast_layer.toggle_toast(cx, entity)) - } - - pub fn toggle_centered_layout( - &mut self, - _: &ToggleCenteredLayout, - _: &mut Window, - cx: &mut Context, - ) { - self.centered_layout = !self.centered_layout; - if let Some(database_id) = self.database_id() { - cx.background_spawn(DB.set_centered_layout(database_id, self.centered_layout)) - .detach_and_log_err(cx); - } - cx.notify(); - } - - fn adjust_padding(padding: Option) -> f32 { - padding - .unwrap_or(CenteredPaddingSettings::default().0) - .clamp( - CenteredPaddingSettings::MIN_PADDING, - CenteredPaddingSettings::MAX_PADDING, - ) - } - - fn render_dock( - &self, - position: DockPosition, - dock: &Entity, - window: &mut Window, - cx: &mut App, - ) -> Option
{ - if self.zoomed_position == Some(position) { - return None; - } - - let leader_border = dock.read(cx).active_panel().and_then(|panel| { - let pane = panel.pane(cx)?; - let follower_states = &self.follower_states; - leader_border_for_pane(follower_states, &pane, window, cx) - }); - - Some( - div() - .flex() - .flex_none() - .overflow_hidden() - .child(dock.clone()) - .children(leader_border), - ) - } - - pub fn for_window(window: &mut Window, _: &mut App) -> Option> { - window.root().flatten() - } - - pub fn zoomed_item(&self) -> Option<&AnyWeakView> { - self.zoomed.as_ref() - } - - pub fn activate_next_window(&mut self, cx: &mut Context) { - let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else { - return; - }; - let windows = cx.windows(); - let next_window = - SystemWindowTabController::get_next_tab_group_window(cx, current_window_id).or_else( - || { - windows - .iter() - .cycle() - .skip_while(|window| window.window_id() != current_window_id) - .nth(1) - }, - ); - - if let Some(window) = next_window { - window - .update(cx, |_, window, _| window.activate_window()) - .ok(); - } - } - - pub fn activate_previous_window(&mut self, cx: &mut Context) { - let Some(current_window_id) = cx.active_window().map(|a| a.window_id()) else { - return; - }; - let windows = cx.windows(); - let prev_window = - SystemWindowTabController::get_prev_tab_group_window(cx, current_window_id).or_else( - || { - windows - .iter() - .rev() - .cycle() - .skip_while(|window| window.window_id() != current_window_id) - .nth(1) - }, - ); - - if let Some(window) = prev_window { - window - .update(cx, |_, window, _| window.activate_window()) - .ok(); - } - } - - pub fn cancel(&mut self, _: &menu::Cancel, window: &mut Window, cx: &mut Context) { - if cx.stop_active_drag(window) { - } else if let Some((notification_id, _)) = self.notifications.pop() { - dismiss_app_notification(¬ification_id, cx); - } else { - cx.propagate(); - } - } - - fn adjust_dock_size_by_px( - &mut self, - panel_size: Pixels, - dock_pos: DockPosition, - px: Pixels, - window: &mut Window, - cx: &mut Context, - ) { - match dock_pos { - DockPosition::Left => self.resize_left_dock(panel_size + px, window, cx), - DockPosition::Right => self.resize_right_dock(panel_size + px, window, cx), - DockPosition::Bottom => self.resize_bottom_dock(panel_size + px, window, cx), - } - } - - fn resize_left_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) { - let size = new_size.min(self.bounds.right() - RESIZE_HANDLE_SIZE); - - self.left_dock.update(cx, |left_dock, cx| { - if WorkspaceSettings::get_global(cx) - .resize_all_panels_in_dock - .contains(&DockPosition::Left) - { - left_dock.resize_all_panels(Some(size), window, cx); - } else { - left_dock.resize_active_panel(Some(size), window, cx); - } - }); - } - - fn resize_right_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) { - let mut size = new_size.max(self.bounds.left() - RESIZE_HANDLE_SIZE); - self.left_dock.read_with(cx, |left_dock, cx| { - let left_dock_size = left_dock - .active_panel_size(window, cx) - .unwrap_or(Pixels::ZERO); - if left_dock_size + size > self.bounds.right() { - size = self.bounds.right() - left_dock_size - } - }); - self.right_dock.update(cx, |right_dock, cx| { - if WorkspaceSettings::get_global(cx) - .resize_all_panels_in_dock - .contains(&DockPosition::Right) - { - right_dock.resize_all_panels(Some(size), window, cx); - } else { - right_dock.resize_active_panel(Some(size), window, cx); - } - }); - } - - fn resize_bottom_dock(&mut self, new_size: Pixels, window: &mut Window, cx: &mut App) { - let size = new_size.min(self.bounds.bottom() - RESIZE_HANDLE_SIZE - self.bounds.top()); - self.bottom_dock.update(cx, |bottom_dock, cx| { - if WorkspaceSettings::get_global(cx) - .resize_all_panels_in_dock - .contains(&DockPosition::Bottom) - { - bottom_dock.resize_all_panels(Some(size), window, cx); - } else { - bottom_dock.resize_active_panel(Some(size), window, cx); - } - }); - } - - fn toggle_edit_predictions_all_files( - &mut self, - _: &ToggleEditPrediction, - _window: &mut Window, - cx: &mut Context, - ) { - let fs = self.project().read(cx).fs().clone(); - let show_edit_predictions = all_language_settings(None, cx).show_edit_predictions(None, cx); - update_settings_file(fs, cx, move |file, _| { - file.project.all_languages.defaults.show_edit_predictions = Some(!show_edit_predictions) - }); - } -} - -fn leader_border_for_pane( - follower_states: &HashMap, - pane: &Entity, - _: &Window, - cx: &App, -) -> Option
{ - let (leader_id, _follower_state) = follower_states.iter().find_map(|(leader_id, state)| { - if state.pane() == pane { - Some((*leader_id, state)) - } else { - None - } - })?; - - let mut leader_color = match leader_id { - CollaboratorId::PeerId(leader_peer_id) => { - let room = ActiveCall::try_global(cx)?.read(cx).room()?.read(cx); - let leader = room.remote_participant_for_peer_id(leader_peer_id)?; - - cx.theme() - .players() - .color_for_participant(leader.participant_index.0) - .cursor - } - CollaboratorId::Agent => cx.theme().players().agent().cursor, - }; - leader_color.fade_out(0.3); - Some( - div() - .absolute() - .size_full() - .left_0() - .top_0() - .border_2() - .border_color(leader_color), - ) -} - -fn window_bounds_env_override() -> Option> { - ZED_WINDOW_POSITION - .zip(*ZED_WINDOW_SIZE) - .map(|(position, size)| Bounds { - origin: position, - size, - }) -} - -fn open_items( - serialized_workspace: Option, - mut project_paths_to_open: Vec<(PathBuf, Option)>, - window: &mut Window, - cx: &mut Context, -) -> impl 'static + Future>>>>> + use<> { - let restored_items = serialized_workspace.map(|serialized_workspace| { - Workspace::load_workspace( - serialized_workspace, - project_paths_to_open - .iter() - .map(|(_, project_path)| project_path) - .cloned() - .collect(), - window, - cx, - ) - }); - - cx.spawn_in(window, async move |workspace, cx| { - let mut opened_items = Vec::with_capacity(project_paths_to_open.len()); - - if let Some(restored_items) = restored_items { - let restored_items = restored_items.await?; - - let restored_project_paths = restored_items - .iter() - .filter_map(|item| { - cx.update(|_, cx| item.as_ref()?.project_path(cx)) - .ok() - .flatten() - }) - .collect::>(); - - for restored_item in restored_items { - opened_items.push(restored_item.map(Ok)); - } - - project_paths_to_open - .iter_mut() - .for_each(|(_, project_path)| { - if let Some(project_path_to_open) = project_path - && restored_project_paths.contains(project_path_to_open) - { - *project_path = None; - } - }); - } else { - for _ in 0..project_paths_to_open.len() { - opened_items.push(None); - } - } - assert!(opened_items.len() == project_paths_to_open.len()); - - let tasks = - project_paths_to_open - .into_iter() - .enumerate() - .map(|(ix, (abs_path, project_path))| { - let workspace = workspace.clone(); - cx.spawn(async move |cx| { - let file_project_path = project_path?; - let abs_path_task = workspace.update(cx, |workspace, cx| { - workspace.project().update(cx, |project, cx| { - project.resolve_abs_path(abs_path.to_string_lossy().as_ref(), cx) - }) - }); - - // We only want to open file paths here. If one of the items - // here is a directory, it was already opened further above - // with a `find_or_create_worktree`. - if let Ok(task) = abs_path_task - && task.await.is_none_or(|p| p.is_file()) - { - return Some(( - ix, - workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_path( - file_project_path, - None, - true, - window, - cx, - ) - }) - .log_err()? - .await, - )); - } - None - }) - }); - - let tasks = tasks.collect::>(); - - let tasks = futures::future::join_all(tasks); - for (ix, path_open_result) in tasks.await.into_iter().flatten() { - opened_items[ix] = Some(path_open_result); - } - - Ok(opened_items) - }) -} - -enum ActivateInDirectionTarget { - Pane(Entity), - Dock(Entity), -} - -fn notify_if_database_failed(workspace: WindowHandle, cx: &mut AsyncApp) { - workspace - .update(cx, |workspace, _, cx| { - if (*db::ALL_FILE_DB_FAILED).load(std::sync::atomic::Ordering::Acquire) { - struct DatabaseFailedNotification; - - workspace.show_notification( - NotificationId::unique::(), - cx, - |cx| { - cx.new(|cx| { - MessageNotification::new("Failed to load the database file.", cx) - .primary_message("File an Issue") - .primary_icon(IconName::Plus) - .primary_on_click(|window, cx| { - window.dispatch_action(Box::new(FileBugReport), cx) - }) - }) - }, - ); - } - }) - .log_err(); -} - -fn px_with_ui_font_fallback(val: u32, cx: &Context) -> Pixels { - if val == 0 { - ThemeSettings::get_global(cx).ui_font_size(cx) - } else { - px(val as f32) - } -} - -fn adjust_active_dock_size_by_px( - px: Pixels, - workspace: &mut Workspace, - window: &mut Window, - cx: &mut Context, -) { - let Some(active_dock) = workspace - .all_docks() - .into_iter() - .find(|dock| dock.focus_handle(cx).contains_focused(window, cx)) - else { - return; - }; - let dock = active_dock.read(cx); - let Some(panel_size) = dock.active_panel_size(window, cx) else { - return; - }; - let dock_pos = dock.position(); - workspace.adjust_dock_size_by_px(panel_size, dock_pos, px, window, cx); -} - -fn adjust_open_docks_size_by_px( - px: Pixels, - workspace: &mut Workspace, - window: &mut Window, - cx: &mut Context, -) { - let docks = workspace - .all_docks() - .into_iter() - .filter_map(|dock| { - if dock.read(cx).is_open() { - let dock = dock.read(cx); - let panel_size = dock.active_panel_size(window, cx)?; - let dock_pos = dock.position(); - Some((panel_size, dock_pos, px)) - } else { - None - } - }) - .collect::>(); - - docks - .into_iter() - .for_each(|(panel_size, dock_pos, offset)| { - workspace.adjust_dock_size_by_px(panel_size, dock_pos, offset, window, cx); - }); -} - -impl Focusable for Workspace { - fn focus_handle(&self, cx: &App) -> FocusHandle { - self.active_pane.focus_handle(cx) - } -} - -#[derive(Clone)] -struct DraggedDock(DockPosition); - -impl Render for DraggedDock { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - gpui::Empty - } -} - -impl Render for Workspace { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - static FIRST_PAINT: AtomicBool = AtomicBool::new(true); - if FIRST_PAINT.swap(false, std::sync::atomic::Ordering::Relaxed) { - log::info!("Rendered first frame"); - } - let mut context = KeyContext::new_with_defaults(); - context.add("Workspace"); - context.set("keyboard_layout", cx.keyboard_layout().name().to_string()); - if let Some(status) = self - .debugger_provider - .as_ref() - .and_then(|provider| provider.active_thread_state(cx)) - { - match status { - ThreadStatus::Running | ThreadStatus::Stepping => { - context.add("debugger_running"); - } - ThreadStatus::Stopped => context.add("debugger_stopped"), - ThreadStatus::Exited | ThreadStatus::Ended => {} - } - } - - if self.left_dock.read(cx).is_open() { - if let Some(active_panel) = self.left_dock.read(cx).active_panel() { - context.set("left_dock", active_panel.panel_key()); - } - } - - if self.right_dock.read(cx).is_open() { - if let Some(active_panel) = self.right_dock.read(cx).active_panel() { - context.set("right_dock", active_panel.panel_key()); - } - } - - if self.bottom_dock.read(cx).is_open() { - if let Some(active_panel) = self.bottom_dock.read(cx).active_panel() { - context.set("bottom_dock", active_panel.panel_key()); - } - } - - let centered_layout = self.centered_layout - && self.center.panes().len() == 1 - && self.active_item(cx).is_some(); - let render_padding = |size| { - (size > 0.0).then(|| { - div() - .h_full() - .w(relative(size)) - .bg(cx.theme().colors().editor_background) - .border_color(cx.theme().colors().pane_group_border) - }) - }; - let paddings = if centered_layout { - let settings = WorkspaceSettings::get_global(cx).centered_layout; - ( - render_padding(Self::adjust_padding( - settings.left_padding.map(|padding| padding.0), - )), - render_padding(Self::adjust_padding( - settings.right_padding.map(|padding| padding.0), - )), - ) - } else { - (None, None) - }; - let ui_font = theme::setup_ui_font(window, cx); - - let theme = cx.theme().clone(); - let colors = theme.colors(); - let notification_entities = self - .notifications - .iter() - .map(|(_, notification)| notification.entity_id()) - .collect::>(); - let bottom_dock_layout = WorkspaceSettings::get_global(cx).bottom_dock_layout; - - client_side_decorations( - self.actions(div(), window, cx) - .key_context(context) - .relative() - .size_full() - .flex() - .flex_col() - .font(ui_font) - .gap_0() - .justify_start() - .items_start() - .text_color(colors.text) - .overflow_hidden() - .children(self.titlebar_item.clone()) - .on_modifiers_changed(move |_, _, cx| { - for &id in ¬ification_entities { - cx.notify(id); - } - }) - .child( - div() - .size_full() - .relative() - .flex_1() - .flex() - .flex_col() - .child( - div() - .id("workspace") - .bg(colors.background) - .relative() - .flex_1() - .w_full() - .flex() - .flex_col() - .overflow_hidden() - .border_t_1() - .border_b_1() - .border_color(colors.border) - .child({ - let this = cx.entity(); - canvas( - move |bounds, window, cx| { - this.update(cx, |this, cx| { - let bounds_changed = this.bounds != bounds; - this.bounds = bounds; - - if bounds_changed { - this.left_dock.update(cx, |dock, cx| { - dock.clamp_panel_size( - bounds.size.width, - window, - cx, - ) - }); - - this.right_dock.update(cx, |dock, cx| { - dock.clamp_panel_size( - bounds.size.width, - window, - cx, - ) - }); - - this.bottom_dock.update(cx, |dock, cx| { - dock.clamp_panel_size( - bounds.size.height, - window, - cx, - ) - }); - } - }) - }, - |_, _, _, _| {}, - ) - .absolute() - .size_full() - }) - .when(self.zoomed.is_none(), |this| { - this.on_drag_move(cx.listener( - move |workspace, - e: &DragMoveEvent, - window, - cx| { - if workspace.previous_dock_drag_coordinates - != Some(e.event.position) - { - workspace.previous_dock_drag_coordinates = - Some(e.event.position); - match e.drag(cx).0 { - DockPosition::Left => { - workspace.resize_left_dock( - e.event.position.x - - workspace.bounds.left(), - window, - cx, - ); - } - DockPosition::Right => { - workspace.resize_right_dock( - workspace.bounds.right() - - e.event.position.x, - window, - cx, - ); - } - DockPosition::Bottom => { - workspace.resize_bottom_dock( - workspace.bounds.bottom() - - e.event.position.y, - window, - cx, - ); - } - }; - workspace.serialize_workspace(window, cx); - } - }, - )) - }) - .child({ - match bottom_dock_layout { - BottomDockLayout::Full => div() - .flex() - .flex_col() - .h_full() - .child( - div() - .flex() - .flex_row() - .flex_1() - .overflow_hidden() - .children(self.render_dock( - DockPosition::Left, - &self.left_dock, - window, - cx, - )) - .child( - div() - .flex() - .flex_col() - .flex_1() - .overflow_hidden() - .child( - h_flex() - .flex_1() - .when_some( - paddings.0, - |this, p| { - this.child( - p.border_r_1(), - ) - }, - ) - .child(self.center.render( - self.zoomed.as_ref(), - &PaneRenderContext { - follower_states: - &self.follower_states, - active_call: self.active_call(), - active_pane: &self.active_pane, - app_state: &self.app_state, - project: &self.project, - workspace: &self.weak_self, - }, - window, - cx, - )) - .when_some( - paddings.1, - |this, p| { - this.child( - p.border_l_1(), - ) - }, - ), - ), - ) - .children(self.render_dock( - DockPosition::Right, - &self.right_dock, - window, - cx, - )), - ) - .child(div().w_full().children(self.render_dock( - DockPosition::Bottom, - &self.bottom_dock, - window, - cx - ))), - - BottomDockLayout::LeftAligned => div() - .flex() - .flex_row() - .h_full() - .child( - div() - .flex() - .flex_col() - .flex_1() - .h_full() - .child( - div() - .flex() - .flex_row() - .flex_1() - .children(self.render_dock(DockPosition::Left, &self.left_dock, window, cx)) - .child( - div() - .flex() - .flex_col() - .flex_1() - .overflow_hidden() - .child( - h_flex() - .flex_1() - .when_some(paddings.0, |this, p| this.child(p.border_r_1())) - .child(self.center.render( - self.zoomed.as_ref(), - &PaneRenderContext { - follower_states: - &self.follower_states, - active_call: self.active_call(), - active_pane: &self.active_pane, - app_state: &self.app_state, - project: &self.project, - workspace: &self.weak_self, - }, - window, - cx, - )) - .when_some(paddings.1, |this, p| this.child(p.border_l_1())), - ) - ) - ) - .child( - div() - .w_full() - .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx)) - ), - ) - .children(self.render_dock( - DockPosition::Right, - &self.right_dock, - window, - cx, - )), - - BottomDockLayout::RightAligned => div() - .flex() - .flex_row() - .h_full() - .children(self.render_dock( - DockPosition::Left, - &self.left_dock, - window, - cx, - )) - .child( - div() - .flex() - .flex_col() - .flex_1() - .h_full() - .child( - div() - .flex() - .flex_row() - .flex_1() - .child( - div() - .flex() - .flex_col() - .flex_1() - .overflow_hidden() - .child( - h_flex() - .flex_1() - .when_some(paddings.0, |this, p| this.child(p.border_r_1())) - .child(self.center.render( - self.zoomed.as_ref(), - &PaneRenderContext { - follower_states: - &self.follower_states, - active_call: self.active_call(), - active_pane: &self.active_pane, - app_state: &self.app_state, - project: &self.project, - workspace: &self.weak_self, - }, - window, - cx, - )) - .when_some(paddings.1, |this, p| this.child(p.border_l_1())), - ) - ) - .children(self.render_dock(DockPosition::Right, &self.right_dock, window, cx)) - ) - .child( - div() - .w_full() - .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx)) - ), - ), - - BottomDockLayout::Contained => div() - .flex() - .flex_row() - .h_full() - .children(self.render_dock( - DockPosition::Left, - &self.left_dock, - window, - cx, - )) - .child( - div() - .flex() - .flex_col() - .flex_1() - .overflow_hidden() - .child( - h_flex() - .flex_1() - .when_some(paddings.0, |this, p| { - this.child(p.border_r_1()) - }) - .child(self.center.render( - self.zoomed.as_ref(), - &PaneRenderContext { - follower_states: - &self.follower_states, - active_call: self.active_call(), - active_pane: &self.active_pane, - app_state: &self.app_state, - project: &self.project, - workspace: &self.weak_self, - }, - window, - cx, - )) - .when_some(paddings.1, |this, p| { - this.child(p.border_l_1()) - }), - ) - .children(self.render_dock( - DockPosition::Bottom, - &self.bottom_dock, - window, - cx, - )), - ) - .children(self.render_dock( - DockPosition::Right, - &self.right_dock, - window, - cx, - )), - } - }) - .children(self.zoomed.as_ref().and_then(|view| { - let zoomed_view = view.upgrade()?; - let div = div() - .occlude() - .absolute() - .overflow_hidden() - .border_color(colors.border) - .bg(colors.background) - .child(zoomed_view) - .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() - } - }) - })) - .children(self.render_notifications(window, cx)), - ) - .when(self.status_bar_visible(cx), |parent| { - parent.child(self.status_bar.clone()) - }) - .child(self.modal_layer.clone()) - .child(self.toast_layer.clone()), - ), - window, - cx, - ) - } -} - -impl WorkspaceStore { - pub fn new(client: Arc, cx: &mut Context) -> Self { - Self { - workspaces: Default::default(), - _subscriptions: vec![ - client.add_request_handler(cx.weak_entity(), Self::handle_follow), - client.add_message_handler(cx.weak_entity(), Self::handle_update_followers), - ], - client, - } - } - - pub fn update_followers( - &self, - project_id: Option, - update: proto::update_followers::Variant, - cx: &App, - ) -> Option<()> { - let active_call = ActiveCall::try_global(cx)?; - let room_id = active_call.read(cx).room()?.read(cx).id(); - self.client - .send(proto::UpdateFollowers { - room_id, - project_id, - variant: Some(update), - }) - .log_err() - } - - pub async fn handle_follow( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result { - this.update(&mut cx, |this, cx| { - let follower = Follower { - project_id: envelope.payload.project_id, - peer_id: envelope.original_sender_id()?, - }; - - let mut response = proto::FollowResponse::default(); - this.workspaces.retain(|workspace| { - workspace - .update(cx, |workspace, window, cx| { - let handler_response = - workspace.handle_follow(follower.project_id, window, cx); - if let Some(active_view) = handler_response.active_view - && workspace.project.read(cx).remote_id() == follower.project_id - { - response.active_view = Some(active_view) - } - }) - .is_ok() - }); - - Ok(response) - })? - } - - async fn handle_update_followers( - this: Entity, - envelope: TypedEnvelope, - mut cx: AsyncApp, - ) -> Result<()> { - let leader_id = envelope.original_sender_id()?; - let update = envelope.payload; - - this.update(&mut cx, |this, cx| { - this.workspaces.retain(|workspace| { - workspace - .update(cx, |workspace, window, cx| { - let project_id = workspace.project.read(cx).remote_id(); - if update.project_id != project_id && update.project_id.is_some() { - return; - } - workspace.handle_update_followers(leader_id, update.clone(), window, cx); - }) - .is_ok() - }); - Ok(()) - })? - } - - pub fn workspaces(&self) -> &HashSet> { - &self.workspaces - } -} - -impl ViewId { - pub(crate) fn from_proto(message: proto::ViewId) -> Result { - Ok(Self { - creator: message - .creator - .map(CollaboratorId::PeerId) - .context("creator is missing")?, - id: message.id, - }) - } - - pub(crate) fn to_proto(self) -> Option { - if let CollaboratorId::PeerId(peer_id) = self.creator { - Some(proto::ViewId { - creator: Some(peer_id), - id: self.id, - }) - } else { - None - } - } -} - -impl FollowerState { - fn pane(&self) -> &Entity { - self.dock_pane.as_ref().unwrap_or(&self.center_pane) - } -} - -pub trait WorkspaceHandle { - fn file_project_paths(&self, cx: &App) -> Vec; -} - -impl WorkspaceHandle for Entity { - fn file_project_paths(&self, cx: &App) -> Vec { - self.read(cx) - .worktrees(cx) - .flat_map(|worktree| { - let worktree_id = worktree.read(cx).id(); - worktree.read(cx).files(true, 0).map(move |f| ProjectPath { - worktree_id, - path: f.path.clone(), - }) - }) - .collect::>() - } -} - -pub async fn last_opened_workspace_location() -> Option<(SerializedWorkspaceLocation, PathList)> { - DB.last_workspace().await.log_err().flatten() -} - -pub fn last_session_workspace_locations( - last_session_id: &str, - last_session_window_stack: Option>, -) -> Option> { - DB.last_session_workspace_locations(last_session_id, last_session_window_stack) - .log_err() -} - -actions!( - collab, - [ - /// Opens the channel notes for the current call. - /// - /// Use `collab_panel::OpenSelectedChannelNotes` to open the channel notes for the selected - /// channel in the collab panel. - /// - /// If you want to open a specific channel, use `zed::OpenZedUrl` with a channel notes URL - - /// can be copied via "Copy link to section" in the context menu of the channel notes - /// buffer. These URLs look like `https://zed.dev/channel/channel-name-CHANNEL_ID/notes`. - OpenChannelNotes, - /// Mutes your microphone. - Mute, - /// Deafens yourself (mute both microphone and speakers). - Deafen, - /// Leaves the current call. - LeaveCall, - /// Shares the current project with collaborators. - ShareProject, - /// Shares your screen with collaborators. - ScreenShare, - /// Copies the current room name and session id for debugging purposes. - CopyRoomId, - ] -); -actions!( - zed, - [ - /// Opens the Zed log file. - OpenLog, - /// Reveals the Zed log file in the system file manager. - RevealLogInFileManager - ] -); - -async fn join_channel_internal( - channel_id: ChannelId, - app_state: &Arc, - requesting_window: Option>, - active_call: &Entity, - cx: &mut AsyncApp, -) -> Result { - let (should_prompt, open_room) = active_call.update(cx, |active_call, cx| { - let Some(room) = active_call.room().map(|room| room.read(cx)) else { - return (false, None); - }; - - let already_in_channel = room.channel_id() == Some(channel_id); - let should_prompt = room.is_sharing_project() - && !room.remote_participants().is_empty() - && !already_in_channel; - let open_room = if already_in_channel { - active_call.room().cloned() - } else { - None - }; - (should_prompt, open_room) - })?; - - if let Some(room) = open_room { - let task = room.update(cx, |room, cx| { - if let Some((project, host)) = room.most_active_project(cx) { - return Some(join_in_room_project(project, host, app_state.clone(), cx)); - } - - None - })?; - if let Some(task) = task { - task.await?; - } - return anyhow::Ok(true); - } - - if should_prompt { - if let Some(workspace) = requesting_window { - let answer = workspace - .update(cx, |_, window, cx| { - window.prompt( - PromptLevel::Warning, - "Do you want to switch channels?", - Some("Leaving this call will unshare your current project."), - &["Yes, Join Channel", "Cancel"], - cx, - ) - })? - .await; - - if answer == Ok(1) { - return Ok(false); - } - } else { - return Ok(false); // unreachable!() hopefully - } - } - - let client = cx.update(|cx| active_call.read(cx).client())?; - - let mut client_status = client.status(); - - // this loop will terminate within client::CONNECTION_TIMEOUT seconds. - 'outer: loop { - let Some(status) = client_status.recv().await else { - anyhow::bail!("error connecting"); - }; - - match status { - Status::Connecting - | Status::Authenticating - | Status::Authenticated - | Status::Reconnecting - | Status::Reauthenticating - | Status::Reauthenticated => continue, - Status::Connected { .. } => break 'outer, - Status::SignedOut | Status::AuthenticationError => { - return Err(ErrorCode::SignedOut.into()); - } - Status::UpgradeRequired => return Err(ErrorCode::UpgradeRequired.into()), - Status::ConnectionError | Status::ConnectionLost | Status::ReconnectionError { .. } => { - return Err(ErrorCode::Disconnected.into()); - } - } - } - - let room = active_call - .update(cx, |active_call, cx| { - active_call.join_channel(channel_id, cx) - })? - .await?; - - let Some(room) = room else { - return anyhow::Ok(true); - }; - - room.update(cx, |room, _| room.room_update_completed())? - .await; - - let task = room.update(cx, |room, cx| { - if let Some((project, host)) = room.most_active_project(cx) { - return Some(join_in_room_project(project, host, app_state.clone(), cx)); - } - - // If you are the first to join a channel, see if you should share your project. - if room.remote_participants().is_empty() - && !room.local_participant_is_guest() - && let Some(workspace) = requesting_window - { - let project = workspace.update(cx, |workspace, _, cx| { - let project = workspace.project.read(cx); - - if !CallSettings::get_global(cx).share_on_join { - return None; - } - - if (project.is_local() || project.is_via_remote_server()) - && project.visible_worktrees(cx).any(|tree| { - tree.read(cx) - .root_entry() - .is_some_and(|entry| entry.is_dir()) - }) - { - Some(workspace.project.clone()) - } else { - None - } - }); - if let Ok(Some(project)) = project { - return Some(cx.spawn(async move |room, cx| { - room.update(cx, |room, cx| room.share_project(project, cx))? - .await?; - Ok(()) - })); - } - } - - None - })?; - if let Some(task) = task { - task.await?; - return anyhow::Ok(true); - } - anyhow::Ok(false) -} - -pub fn join_channel( - channel_id: ChannelId, - app_state: Arc, - requesting_window: Option>, - cx: &mut App, -) -> Task> { - let active_call = ActiveCall::global(cx); - cx.spawn(async move |cx| { - let result = - join_channel_internal(channel_id, &app_state, requesting_window, &active_call, cx) - .await; - - // join channel succeeded, and opened a window - if matches!(result, Ok(true)) { - return anyhow::Ok(()); - } - - // find an existing workspace to focus and show call controls - let mut active_window = requesting_window.or_else(|| activate_any_workspace_window(cx)); - if active_window.is_none() { - // no open workspaces, make one to show the error in (blergh) - let (window_handle, _) = cx - .update(|cx| { - Workspace::new_local(vec![], app_state.clone(), requesting_window, None, cx) - })? - .await?; - - if result.is_ok() { - cx.update(|cx| { - cx.dispatch_action(&OpenChannelNotes); - }) - .log_err(); - } - - active_window = Some(window_handle); - } - - if let Err(err) = result { - log::error!("failed to join channel: {}", err); - if let Some(active_window) = active_window { - active_window - .update(cx, |_, window, cx| { - let detail: SharedString = match err.error_code() { - ErrorCode::SignedOut => "Please sign in to continue.".into(), - ErrorCode::UpgradeRequired => concat!( - "Your are running an unsupported version of Zed. ", - "Please update to continue." - ) - .into(), - ErrorCode::NoSuchChannel => concat!( - "No matching channel was found. ", - "Please check the link and try again." - ) - .into(), - ErrorCode::Forbidden => concat!( - "This channel is private, and you do not have access. ", - "Please ask someone to add you and try again." - ) - .into(), - ErrorCode::Disconnected => { - "Please check your internet connection and try again.".into() - } - _ => format!("{}\n\nPlease try again.", err).into(), - }; - window.prompt( - PromptLevel::Critical, - "Failed to join channel", - Some(&detail), - &["Ok"], - cx, - ) - })? - .await - .ok(); - } - } - - // return ok, we showed the error to the user. - anyhow::Ok(()) - }) -} - -pub async fn get_any_active_workspace( - app_state: Arc, - mut cx: AsyncApp, -) -> anyhow::Result> { - // find an existing workspace to focus and show call controls - let active_window = activate_any_workspace_window(&mut cx); - if active_window.is_none() { - cx.update(|cx| Workspace::new_local(vec![], app_state.clone(), None, None, cx))? - .await?; - } - activate_any_workspace_window(&mut cx).context("could not open zed") -} - -fn activate_any_workspace_window(cx: &mut AsyncApp) -> Option> { - cx.update(|cx| { - if let Some(workspace_window) = cx - .active_window() - .and_then(|window| window.downcast::()) - { - return Some(workspace_window); - } - - for window in cx.windows() { - if let Some(workspace_window) = window.downcast::() { - workspace_window - .update(cx, |_, window, _| window.activate_window()) - .ok(); - return Some(workspace_window); - } - } - None - }) - .ok() - .flatten() -} - -pub fn local_workspace_windows(cx: &App) -> Vec> { - cx.windows() - .into_iter() - .filter_map(|window| window.downcast::()) - .filter(|workspace| { - workspace - .read(cx) - .is_ok_and(|workspace| workspace.project.read(cx).is_local()) - }) - .collect() -} - -#[derive(Default)] -pub struct OpenOptions { - pub visible: Option, - pub focus: Option, - pub open_new_workspace: Option, - pub prefer_focused_window: bool, - pub replace_window: Option>, - pub env: Option>, -} - -#[allow(clippy::type_complexity)] -pub fn open_paths( - abs_paths: &[PathBuf], - app_state: Arc, - open_options: OpenOptions, - cx: &mut App, -) -> Task< - anyhow::Result<( - WindowHandle, - Vec>>>, - )>, -> { - let abs_paths = abs_paths.to_vec(); - let mut existing = None; - let mut best_match = None; - let mut open_visible = OpenVisible::All; - #[cfg(target_os = "windows")] - let wsl_path = abs_paths - .iter() - .find_map(|p| util::paths::WslPath::from_path(p)); - - cx.spawn(async move |cx| { - if open_options.open_new_workspace != Some(true) { - let all_paths = abs_paths.iter().map(|path| app_state.fs.metadata(path)); - let all_metadatas = futures::future::join_all(all_paths) - .await - .into_iter() - .filter_map(|result| result.ok().flatten()) - .collect::>(); - - cx.update(|cx| { - for window in local_workspace_windows(cx) { - if let Ok(workspace) = window.read(cx) { - let m = workspace.project.read(cx).visibility_for_paths( - &abs_paths, - &all_metadatas, - open_options.open_new_workspace == None, - cx, - ); - if m > best_match { - existing = Some(window); - best_match = m; - } else if best_match.is_none() - && open_options.open_new_workspace == Some(false) - { - existing = Some(window) - } - } - } - })?; - - if open_options.open_new_workspace.is_none() - && (existing.is_none() || open_options.prefer_focused_window) - && all_metadatas.iter().all(|file| !file.is_dir) - { - cx.update(|cx| { - if let Some(window) = cx - .active_window() - .and_then(|window| window.downcast::()) - && let Ok(workspace) = window.read(cx) - { - let project = workspace.project().read(cx); - if project.is_local() && !project.is_via_collab() { - existing = Some(window); - open_visible = OpenVisible::None; - return; - } - } - for window in local_workspace_windows(cx) { - if let Ok(workspace) = window.read(cx) { - let project = workspace.project().read(cx); - if project.is_via_collab() { - continue; - } - existing = Some(window); - open_visible = OpenVisible::None; - break; - } - } - })?; - } - } - - let result = if let Some(existing) = existing { - let open_task = existing - .update(cx, |workspace, window, cx| { - window.activate_window(); - workspace.open_paths( - abs_paths, - OpenOptions { - visible: Some(open_visible), - ..Default::default() - }, - None, - window, - cx, - ) - })? - .await; - - _ = existing.update(cx, |workspace, _, cx| { - for item in open_task.iter().flatten() { - if let Err(e) = item { - workspace.show_error(&e, cx); - } - } - }); - - Ok((existing, open_task)) - } else { - cx.update(move |cx| { - Workspace::new_local( - abs_paths, - app_state.clone(), - open_options.replace_window, - open_options.env, - cx, - ) - })? - .await - }; - - #[cfg(target_os = "windows")] - if let Some(util::paths::WslPath{distro, path}) = wsl_path - && let Ok((workspace, _)) = &result - { - workspace - .update(cx, move |workspace, _window, cx| { - struct OpenInWsl; - workspace.show_notification(NotificationId::unique::(), cx, move |cx| { - let display_path = util::markdown::MarkdownInlineCode(&path.to_string_lossy()); - let msg = format!("{display_path} is inside a WSL filesystem, some features may not work unless you open it with WSL remote"); - cx.new(move |cx| { - MessageNotification::new(msg, cx) - .primary_message("Open in WSL") - .primary_icon(IconName::FolderOpen) - .primary_on_click(move |window, cx| { - window.dispatch_action(Box::new(remote::OpenWslPath { - distro: remote::WslConnectionOptions { - distro_name: distro.clone(), - user: None, - }, - paths: vec![path.clone().into()], - }), cx) - }) - }) - }); - }) - .unwrap(); - }; - result - }) -} - -pub fn open_new( - open_options: OpenOptions, - app_state: Arc, - cx: &mut App, - init: impl FnOnce(&mut Workspace, &mut Window, &mut Context) + 'static + Send, -) -> Task> { - let task = Workspace::new_local(Vec::new(), app_state, None, open_options.env, cx); - cx.spawn(async move |cx| { - let (workspace, opened_paths) = task.await?; - workspace.update(cx, |workspace, window, cx| { - if opened_paths.is_empty() { - init(workspace, window, cx) - } - })?; - Ok(()) - }) -} - -pub fn create_and_open_local_file( - path: &'static Path, - window: &mut Window, - cx: &mut Context, - default_content: impl 'static + Send + FnOnce() -> Rope, -) -> Task>> { - cx.spawn_in(window, async move |workspace, cx| { - let fs = workspace.read_with(cx, |workspace, _| workspace.app_state().fs.clone())?; - if !fs.is_file(path).await { - fs.create_file(path, Default::default()).await?; - fs.save(path, &default_content(), Default::default()) - .await?; - } - - let mut items = workspace - .update_in(cx, |workspace, window, cx| { - workspace.with_local_workspace(window, cx, |workspace, window, cx| { - workspace.open_paths( - vec![path.to_path_buf()], - OpenOptions { - visible: Some(OpenVisible::None), - ..Default::default() - }, - None, - window, - cx, - ) - }) - })? - .await? - .await; - - let item = items.pop().flatten(); - item.with_context(|| format!("path {path:?} is not a file"))? - }) -} - -pub fn open_remote_project_with_new_connection( - window: WindowHandle, - remote_connection: Arc, - cancel_rx: oneshot::Receiver<()>, - delegate: Arc, - app_state: Arc, - paths: Vec, - cx: &mut App, -) -> Task>>>> { - cx.spawn(async move |cx| { - let (workspace_id, serialized_workspace) = - deserialize_remote_project(remote_connection.connection_options(), paths.clone(), cx) - .await?; - - let session = match cx - .update(|cx| { - remote::RemoteClient::new( - ConnectionIdentifier::Workspace(workspace_id.0), - remote_connection, - cancel_rx, - delegate, - cx, - ) - })? - .await? - { - Some(result) => result, - None => return Ok(Vec::new()), - }; - - let project = cx.update(|cx| { - project::Project::remote( - session, - app_state.client.clone(), - app_state.node_runtime.clone(), - app_state.user_store.clone(), - app_state.languages.clone(), - app_state.fs.clone(), - cx, - ) - })?; - - open_remote_project_inner( - project, - paths, - workspace_id, - serialized_workspace, - app_state, - window, - cx, - ) - .await - }) -} - -pub fn open_remote_project_with_existing_connection( - connection_options: RemoteConnectionOptions, - project: Entity, - paths: Vec, - app_state: Arc, - window: WindowHandle, - cx: &mut AsyncApp, -) -> Task>>>> { - cx.spawn(async move |cx| { - let (workspace_id, serialized_workspace) = - deserialize_remote_project(connection_options.clone(), paths.clone(), cx).await?; - - open_remote_project_inner( - project, - paths, - workspace_id, - serialized_workspace, - app_state, - window, - cx, - ) - .await - }) -} - -async fn open_remote_project_inner( - project: Entity, - paths: Vec, - workspace_id: WorkspaceId, - serialized_workspace: Option, - app_state: Arc, - window: WindowHandle, - cx: &mut AsyncApp, -) -> Result>>> { - let toolchains = DB.toolchains(workspace_id).await?; - for (toolchain, worktree_id, path) in toolchains { - project - .update(cx, |this, cx| { - this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx) - })? - .await; - } - let mut project_paths_to_open = vec![]; - let mut project_path_errors = vec![]; - - for path in paths { - let result = cx - .update(|cx| Workspace::project_path_for_path(project.clone(), &path, true, cx))? - .await; - match result { - Ok((_, project_path)) => { - project_paths_to_open.push((path.clone(), Some(project_path))); - } - Err(error) => { - project_path_errors.push(error); - } - }; - } - - if project_paths_to_open.is_empty() { - return Err(project_path_errors.pop().context("no paths given")?); - } - - if let Some(detach_session_task) = window - .update(cx, |_workspace, window, cx| { - cx.spawn_in(window, async move |this, cx| { - this.update_in(cx, |this, window, cx| this.remove_from_session(window, cx)) - }) - }) - .ok() - { - detach_session_task.await.ok(); - } - - cx.update_window(window.into(), |_, window, cx| { - window.replace_root(cx, |window, cx| { - telemetry::event!("SSH Project Opened"); - - let mut workspace = - Workspace::new(Some(workspace_id), project, app_state.clone(), window, cx); - workspace.update_history(cx); - - if let Some(ref serialized) = serialized_workspace { - workspace.centered_layout = serialized.centered_layout; - } - - workspace - }); - })?; - - let items = window - .update(cx, |_, window, cx| { - window.activate_window(); - open_items(serialized_workspace, project_paths_to_open, window, cx) - })? - .await?; - - window.update(cx, |workspace, _, cx| { - for error in project_path_errors { - if error.error_code() == proto::ErrorCode::DevServerProjectPathDoesNotExist { - if let Some(path) = error.error_tag("path") { - workspace.show_error(&anyhow!("'{path}' does not exist"), cx) - } - } else { - workspace.show_error(&error, cx) - } - } - })?; - - Ok(items.into_iter().map(|item| item?.ok()).collect()) -} - -fn deserialize_remote_project( - connection_options: RemoteConnectionOptions, - paths: Vec, - cx: &AsyncApp, -) -> Task)>> { - cx.background_spawn(async move { - let remote_connection_id = persistence::DB - .get_or_create_remote_connection(connection_options) - .await?; - - let serialized_workspace = - persistence::DB.remote_workspace_for_roots(&paths, remote_connection_id); - - let workspace_id = if let Some(workspace_id) = - serialized_workspace.as_ref().map(|workspace| workspace.id) - { - workspace_id - } else { - persistence::DB.next_id().await? - }; - - Ok((workspace_id, serialized_workspace)) - }) -} - -pub fn join_in_room_project( - project_id: u64, - follow_user_id: u64, - app_state: Arc, - cx: &mut App, -) -> Task> { - let windows = cx.windows(); - cx.spawn(async move |cx| { - let existing_workspace = windows.into_iter().find_map(|window_handle| { - window_handle - .downcast::() - .and_then(|window_handle| { - window_handle - .update(cx, |workspace, _window, cx| { - if workspace.project().read(cx).remote_id() == Some(project_id) { - Some(window_handle) - } else { - None - } - }) - .unwrap_or(None) - }) - }); - - let workspace = if let Some(existing_workspace) = existing_workspace { - existing_workspace - } else { - let active_call = cx.update(|cx| ActiveCall::global(cx))?; - let room = active_call - .read_with(cx, |call, _| call.room().cloned())? - .context("not in a call")?; - let project = room - .update(cx, |room, cx| { - room.join_project( - project_id, - app_state.languages.clone(), - app_state.fs.clone(), - cx, - ) - })? - .await?; - - let window_bounds_override = window_bounds_env_override(); - cx.update(|cx| { - let mut options = (app_state.build_window_options)(None, cx); - options.window_bounds = window_bounds_override.map(WindowBounds::Windowed); - cx.open_window(options, |window, cx| { - cx.new(|cx| { - Workspace::new(Default::default(), project, app_state.clone(), window, cx) - }) - }) - })?? - }; - - workspace.update(cx, |workspace, window, cx| { - cx.activate(true); - window.activate_window(); - - if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { - let follow_peer_id = room - .read(cx) - .remote_participants() - .iter() - .find(|(_, participant)| participant.user.id == follow_user_id) - .map(|(_, p)| p.peer_id) - .or_else(|| { - // If we couldn't follow the given user, follow the host instead. - let collaborator = workspace - .project() - .read(cx) - .collaborators() - .values() - .find(|collaborator| collaborator.is_host)?; - Some(collaborator.peer_id) - }); - - if let Some(follow_peer_id) = follow_peer_id { - workspace.follow(follow_peer_id, window, cx); - } - } - })?; - - anyhow::Ok(()) - }) -} - -pub fn reload(cx: &mut App) { - let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit; - let mut workspace_windows = cx - .windows() - .into_iter() - .filter_map(|window| window.downcast::()) - .collect::>(); - - // If multiple windows have unsaved changes, and need a save prompt, - // prompt in the active window before switching to a different window. - workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false)); - - let mut prompt = None; - if let (true, Some(window)) = (should_confirm, workspace_windows.first()) { - prompt = window - .update(cx, |_, window, cx| { - window.prompt( - PromptLevel::Info, - "Are you sure you want to restart?", - None, - &["Restart", "Cancel"], - cx, - ) - }) - .ok(); - } - - cx.spawn(async move |cx| { - if let Some(prompt) = prompt { - let answer = prompt.await?; - if answer != 0 { - return Ok(()); - } - } - - // If the user cancels any save prompt, then keep the app open. - for window in workspace_windows { - if let Ok(should_close) = window.update(cx, |workspace, window, cx| { - workspace.prepare_to_close(CloseIntent::Quit, window, cx) - }) && !should_close.await? - { - return Ok(()); - } - } - cx.update(|cx| cx.restart()) - }) - .detach_and_log_err(cx); -} - -fn parse_pixel_position_env_var(value: &str) -> Option> { - let mut parts = value.split(','); - let x: usize = parts.next()?.parse().ok()?; - let y: usize = parts.next()?.parse().ok()?; - Some(point(px(x as f32), px(y as f32))) -} - -fn parse_pixel_size_env_var(value: &str) -> Option> { - let mut parts = value.split(','); - let width: usize = parts.next()?.parse().ok()?; - let height: usize = parts.next()?.parse().ok()?; - Some(size(px(width as f32), px(height as f32))) -} - -/// Add client-side decorations (rounded corners, shadows, resize handling) when appropriate. -pub fn client_side_decorations( - element: impl IntoElement, - window: &mut Window, - cx: &mut App, -) -> Stateful
{ - const BORDER_SIZE: Pixels = px(1.0); - let decorations = window.window_decorations(); - - match decorations { - Decorations::Client { .. } => window.set_client_inset(theme::CLIENT_SIDE_DECORATION_SHADOW), - Decorations::Server => window.set_client_inset(px(0.0)), - } - - struct GlobalResizeEdge(ResizeEdge); - impl Global for GlobalResizeEdge {} - - div() - .id("window-backdrop") - .bg(transparent_black()) - .map(|div| match decorations { - Decorations::Server => div, - Decorations::Client { tiling, .. } => div - .when(!(tiling.top || tiling.right), |div| { - div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.top || tiling.left), |div| { - div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.bottom || tiling.right), |div| { - div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.bottom || tiling.left), |div| { - div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!tiling.top, |div| { - div.pt(theme::CLIENT_SIDE_DECORATION_SHADOW) - }) - .when(!tiling.bottom, |div| { - div.pb(theme::CLIENT_SIDE_DECORATION_SHADOW) - }) - .when(!tiling.left, |div| { - div.pl(theme::CLIENT_SIDE_DECORATION_SHADOW) - }) - .when(!tiling.right, |div| { - div.pr(theme::CLIENT_SIDE_DECORATION_SHADOW) - }) - .on_mouse_move(move |e, window, cx| { - let size = window.window_bounds().get_bounds().size; - let pos = e.position; - - let new_edge = - resize_edge(pos, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling); - - let edge = cx.try_global::(); - if new_edge != edge.map(|edge| edge.0) { - window - .window_handle() - .update(cx, |workspace, _, cx| { - cx.notify(workspace.entity_id()); - }) - .ok(); - } - }) - .on_mouse_down(MouseButton::Left, move |e, window, _| { - let size = window.window_bounds().get_bounds().size; - let pos = e.position; - - let edge = match resize_edge( - pos, - theme::CLIENT_SIDE_DECORATION_SHADOW, - size, - tiling, - ) { - Some(value) => value, - None => return, - }; - - window.start_window_resize(edge); - }), - }) - .size_full() - .child( - div() - .cursor(CursorStyle::Arrow) - .map(|div| match decorations { - Decorations::Server => div, - Decorations::Client { tiling } => div - .border_color(cx.theme().colors().border) - .when(!(tiling.top || tiling.right), |div| { - div.rounded_tr(theme::CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.top || tiling.left), |div| { - div.rounded_tl(theme::CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.bottom || tiling.right), |div| { - div.rounded_br(theme::CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!(tiling.bottom || tiling.left), |div| { - div.rounded_bl(theme::CLIENT_SIDE_DECORATION_ROUNDING) - }) - .when(!tiling.top, |div| div.border_t(BORDER_SIZE)) - .when(!tiling.bottom, |div| div.border_b(BORDER_SIZE)) - .when(!tiling.left, |div| div.border_l(BORDER_SIZE)) - .when(!tiling.right, |div| div.border_r(BORDER_SIZE)) - .when(!tiling.is_tiled(), |div| { - div.shadow(vec![gpui::BoxShadow { - color: Hsla { - h: 0., - s: 0., - l: 0., - a: 0.4, - }, - blur_radius: theme::CLIENT_SIDE_DECORATION_SHADOW / 2., - spread_radius: px(0.), - offset: point(px(0.0), px(0.0)), - }]) - }), - }) - .on_mouse_move(|_e, _, cx| { - cx.stop_propagation(); - }) - .size_full() - .child(element), - ) - .map(|div| match decorations { - Decorations::Server => div, - Decorations::Client { tiling, .. } => div.child( - canvas( - |_bounds, window, _| { - window.insert_hitbox( - Bounds::new( - point(px(0.0), px(0.0)), - window.window_bounds().get_bounds().size, - ), - HitboxBehavior::Normal, - ) - }, - move |_bounds, hitbox, window, cx| { - let mouse = window.mouse_position(); - let size = window.window_bounds().get_bounds().size; - let Some(edge) = - resize_edge(mouse, theme::CLIENT_SIDE_DECORATION_SHADOW, size, tiling) - else { - return; - }; - cx.set_global(GlobalResizeEdge(edge)); - window.set_cursor_style( - match edge { - ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown, - ResizeEdge::Left | ResizeEdge::Right => { - CursorStyle::ResizeLeftRight - } - ResizeEdge::TopLeft | ResizeEdge::BottomRight => { - CursorStyle::ResizeUpLeftDownRight - } - ResizeEdge::TopRight | ResizeEdge::BottomLeft => { - CursorStyle::ResizeUpRightDownLeft - } - }, - &hitbox, - ); - }, - ) - .size_full() - .absolute(), - ), - }) -} - -fn resize_edge( - pos: Point, - shadow_size: Pixels, - window_size: Size, - tiling: Tiling, -) -> Option { - let bounds = Bounds::new(Point::default(), window_size).inset(shadow_size * 1.5); - if bounds.contains(&pos) { - return None; - } - - let corner_size = size(shadow_size * 1.5, shadow_size * 1.5); - let top_left_bounds = Bounds::new(Point::new(px(0.), px(0.)), corner_size); - if !tiling.top && top_left_bounds.contains(&pos) { - return Some(ResizeEdge::TopLeft); - } - - let top_right_bounds = Bounds::new( - Point::new(window_size.width - corner_size.width, px(0.)), - corner_size, - ); - if !tiling.top && top_right_bounds.contains(&pos) { - return Some(ResizeEdge::TopRight); - } - - let bottom_left_bounds = Bounds::new( - Point::new(px(0.), window_size.height - corner_size.height), - corner_size, - ); - if !tiling.bottom && bottom_left_bounds.contains(&pos) { - return Some(ResizeEdge::BottomLeft); - } - - let bottom_right_bounds = Bounds::new( - Point::new( - window_size.width - corner_size.width, - window_size.height - corner_size.height, - ), - corner_size, - ); - if !tiling.bottom && bottom_right_bounds.contains(&pos) { - return Some(ResizeEdge::BottomRight); - } - - if !tiling.top && pos.y < shadow_size { - Some(ResizeEdge::Top) - } else if !tiling.bottom && pos.y > window_size.height - shadow_size { - Some(ResizeEdge::Bottom) - } else if !tiling.left && pos.x < shadow_size { - Some(ResizeEdge::Left) - } else if !tiling.right && pos.x > window_size.width - shadow_size { - Some(ResizeEdge::Right) - } else { - None - } -} - -fn join_pane_into_active( - active_pane: &Entity, - pane: &Entity, - window: &mut Window, - cx: &mut App, -) { - if pane == active_pane { - } else if pane.read(cx).items_len() == 0 { - pane.update(cx, |_, cx| { - cx.emit(pane::Event::Remove { - focus_on_pane: None, - }); - }) - } else { - move_all_items(pane, active_pane, window, cx); - } -} - -fn move_all_items( - from_pane: &Entity, - to_pane: &Entity, - window: &mut Window, - cx: &mut App, -) { - let destination_is_different = from_pane != to_pane; - let mut moved_items = 0; - for (item_ix, item_handle) in from_pane - .read(cx) - .items() - .enumerate() - .map(|(ix, item)| (ix, item.clone())) - .collect::>() - { - let ix = item_ix - moved_items; - if destination_is_different { - // Close item from previous pane - from_pane.update(cx, |source, cx| { - source.remove_item_and_focus_on_pane(ix, false, to_pane.clone(), window, cx); - }); - moved_items += 1; - } - - // This automatically removes duplicate items in the pane - to_pane.update(cx, |destination, cx| { - destination.add_item(item_handle, true, true, None, window, cx); - window.focus(&destination.focus_handle(cx)) - }); - } -} - -pub fn move_item( - source: &Entity, - destination: &Entity, - item_id_to_move: EntityId, - destination_index: usize, - activate: bool, - window: &mut Window, - cx: &mut App, -) { - let Some((item_ix, item_handle)) = source - .read(cx) - .items() - .enumerate() - .find(|(_, item_handle)| item_handle.item_id() == item_id_to_move) - .map(|(ix, item)| (ix, item.clone())) - else { - // Tab was closed during drag - return; - }; - - if source != destination { - // Close item from previous pane - source.update(cx, |source, cx| { - source.remove_item_and_focus_on_pane(item_ix, false, destination.clone(), window, cx); - }); - } - - // This automatically removes duplicate items in the pane - destination.update(cx, |destination, cx| { - destination.add_item_inner( - item_handle, - activate, - activate, - activate, - Some(destination_index), - window, - cx, - ); - if activate { - window.focus(&destination.focus_handle(cx)) - } - }); -} - -pub fn move_active_item( - source: &Entity, - destination: &Entity, - focus_destination: bool, - close_if_empty: bool, - window: &mut Window, - cx: &mut App, -) { - if source == destination { - return; - } - let Some(active_item) = source.read(cx).active_item() else { - return; - }; - source.update(cx, |source_pane, cx| { - let item_id = active_item.item_id(); - source_pane.remove_item(item_id, false, close_if_empty, window, cx); - destination.update(cx, |target_pane, cx| { - target_pane.add_item( - active_item, - focus_destination, - focus_destination, - Some(target_pane.items_len()), - window, - cx, - ); - }); - }); -} - -pub fn clone_active_item( - workspace_id: Option, - source: &Entity, - destination: &Entity, - focus_destination: bool, - window: &mut Window, - cx: &mut App, -) { - if source == destination { - return; - } - let Some(active_item) = source.read(cx).active_item() else { - return; - }; - if !active_item.can_split(cx) { - return; - } - let destination = destination.downgrade(); - let task = active_item.clone_on_split(workspace_id, window, cx); - window - .spawn(cx, async move |cx| { - let Some(clone) = task.await else { - return; - }; - destination - .update_in(cx, |target_pane, window, cx| { - target_pane.add_item( - clone, - focus_destination, - focus_destination, - Some(target_pane.items_len()), - window, - cx, - ); - }) - .log_err(); - }) - .detach(); -} - -#[derive(Debug)] -pub struct WorkspacePosition { - pub window_bounds: Option, - pub display: Option, - pub centered_layout: bool, -} - -pub fn remote_workspace_position_from_db( - connection_options: RemoteConnectionOptions, - paths_to_open: &[PathBuf], - cx: &App, -) -> Task> { - let paths = paths_to_open.to_vec(); - - cx.background_spawn(async move { - let remote_connection_id = persistence::DB - .get_or_create_remote_connection(connection_options) - .await - .context("fetching serialized ssh project")?; - let serialized_workspace = - persistence::DB.remote_workspace_for_roots(&paths, remote_connection_id); - - let (window_bounds, display) = if let Some(bounds) = window_bounds_env_override() { - (Some(WindowBounds::Windowed(bounds)), None) - } else { - let restorable_bounds = serialized_workspace - .as_ref() - .and_then(|workspace| Some((workspace.display?, workspace.window_bounds?))) - .or_else(|| { - let (display, window_bounds) = DB.last_window().log_err()?; - Some((display?, window_bounds?)) - }); - - if let Some((serialized_display, serialized_status)) = restorable_bounds { - (Some(serialized_status.0), Some(serialized_display)) - } else { - (None, None) - } - }; - - let centered_layout = serialized_workspace - .as_ref() - .map(|w| w.centered_layout) - .unwrap_or(false); - - Ok(WorkspacePosition { - window_bounds, - display, - centered_layout, - }) - }) -} - -pub fn with_active_or_new_workspace( - cx: &mut App, - f: impl FnOnce(&mut Workspace, &mut Window, &mut Context) + Send + 'static, -) { - match cx.active_window().and_then(|w| w.downcast::()) { - Some(workspace) => { - cx.defer(move |cx| { - workspace - .update(cx, |workspace, window, cx| f(workspace, window, cx)) - .log_err(); - }); - } - None => { - let app_state = AppState::global(cx); - if let Some(app_state) = app_state.upgrade() { - open_new( - OpenOptions::default(), - app_state, - cx, - move |workspace, window, cx| f(workspace, window, cx), - ) - .detach_and_log_err(cx); - } - } - } -} - -#[cfg(test)] -mod tests { - use std::{cell::RefCell, rc::Rc}; - - use super::*; - use crate::{ - dock::{PanelEvent, test::TestPanel}, - item::{ - ItemBufferKind, ItemEvent, - test::{TestItem, TestProjectItem}, - }, - }; - use fs::FakeFs; - use gpui::{ - DismissEvent, Empty, EventEmitter, FocusHandle, Focusable, Render, TestAppContext, - UpdateGlobal, VisualTestContext, px, - }; - use project::{Project, ProjectEntryId}; - use serde_json::json; - use settings::SettingsStore; - use util::rel_path::rel_path; - - #[gpui::test] - async fn test_tab_disambiguation(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - // Adding an item with no ambiguity renders the tab without detail. - let item1 = cx.new(|cx| { - let mut item = TestItem::new(cx); - item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]); - item - }); - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx); - }); - item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(0))); - - // Adding an item that creates ambiguity increases the level of detail on - // both tabs. - let item2 = cx.new_window_entity(|_window, cx| { - let mut item = TestItem::new(cx); - item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]); - item - }); - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx); - }); - item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1))); - item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1))); - - // Adding an item that creates ambiguity increases the level of detail only - // on the ambiguous tabs. In this case, the ambiguity can't be resolved so - // we stop at the highest detail available. - let item3 = cx.new(|cx| { - let mut item = TestItem::new(cx); - item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]); - item - }); - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx); - }); - item1.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(1))); - item2.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3))); - item3.read_with(cx, |item, _| assert_eq!(item.tab_detail.get(), Some(3))); - } - - #[gpui::test] - async fn test_tracking_active_path(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root1", - json!({ - "one.txt": "", - "two.txt": "", - }), - ) - .await; - fs.insert_tree( - "/root2", - json!({ - "three.txt": "", - }), - ) - .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 pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - let worktree_id = project.update(cx, |project, cx| { - project.worktrees(cx).next().unwrap().read(cx).id() - }); - - let item1 = cx.new(|cx| { - TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)]) - }); - let item2 = cx.new(|cx| { - TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "two.txt", cx)]) - }); - - // Add an item to an empty pane - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane(Box::new(item1), None, true, window, cx) - }); - project.update(cx, |project, cx| { - assert_eq!( - project.active_entry(), - project - .entry_for_path(&(worktree_id, rel_path("one.txt")).into(), cx) - .map(|e| e.id) - ); - }); - assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt")); - - // Add a second item to a non-empty pane - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane(Box::new(item2), None, true, window, cx) - }); - assert_eq!(cx.window_title().as_deref(), Some("root1 — two.txt")); - project.update(cx, |project, cx| { - assert_eq!( - project.active_entry(), - project - .entry_for_path(&(worktree_id, rel_path("two.txt")).into(), cx) - .map(|e| e.id) - ); - }); - - // Close the active item - pane.update_in(cx, |pane, window, cx| { - pane.close_active_item(&Default::default(), window, cx) - }) - .await - .unwrap(); - assert_eq!(cx.window_title().as_deref(), Some("root1 — one.txt")); - project.update(cx, |project, cx| { - assert_eq!( - project.active_entry(), - project - .entry_for_path(&(worktree_id, rel_path("one.txt")).into(), cx) - .map(|e| e.id) - ); - }); - - // Add a project folder - project - .update(cx, |project, cx| { - project.find_or_create_worktree("root2", true, cx) - }) - .await - .unwrap(); - assert_eq!(cx.window_title().as_deref(), Some("root1, root2 — one.txt")); - - // Remove a project folder - project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx)); - assert_eq!(cx.window_title().as_deref(), Some("root2 — one.txt")); - } - - #[gpui::test] - async fn test_close_window(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree("/root", json!({ "one": "" })).await; - - let project = Project::test(fs, ["root".as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - // When there are no dirty items, there's nothing to do. - let item1 = cx.new(TestItem::new); - workspace.update_in(cx, |w, window, cx| { - w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx) - }); - let task = workspace.update_in(cx, |w, window, cx| { - w.prepare_to_close(CloseIntent::CloseWindow, window, cx) - }); - assert!(task.await.unwrap()); - - // When there are dirty untitled items, prompt to save each one. If the user - // cancels any prompt, then abort. - let item2 = cx.new(|cx| TestItem::new(cx).with_dirty(true)); - let item3 = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) - }); - workspace.update_in(cx, |w, window, cx| { - w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx); - w.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx); - }); - let task = workspace.update_in(cx, |w, window, cx| { - w.prepare_to_close(CloseIntent::CloseWindow, window, cx) - }); - cx.executor().run_until_parked(); - cx.simulate_prompt_answer("Cancel"); // cancel save all - cx.executor().run_until_parked(); - assert!(!cx.has_pending_prompt()); - assert!(!task.await.unwrap()); - } - - #[gpui::test] - async fn test_close_window_with_serializable_items(cx: &mut TestAppContext) { - init_test(cx); - - // Register TestItem as a serializable item - cx.update(|cx| { - register_serializable_item::(cx); - }); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree("/root", json!({ "one": "" })).await; - - let project = Project::test(fs, ["root".as_ref()], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - // When there are dirty untitled items, but they can serialize, then there is no prompt. - let item1 = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_serialize(|| Some(Task::ready(Ok(())))) - }); - let item2 = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) - .with_serialize(|| Some(Task::ready(Ok(())))) - }); - workspace.update_in(cx, |w, window, cx| { - w.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx); - w.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx); - }); - let task = workspace.update_in(cx, |w, window, cx| { - w.prepare_to_close(CloseIntent::CloseWindow, window, cx) - }); - assert!(task.await.unwrap()); - } - - #[gpui::test] - async fn test_close_pane_items(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 item1 = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_project_items(&[dirty_project_item(1, "1.txt", cx)]) - }); - let item2 = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_conflict(true) - .with_project_items(&[dirty_project_item(2, "2.txt", cx)]) - }); - let item3 = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_conflict(true) - .with_project_items(&[dirty_project_item(3, "3.txt", cx)]) - }); - let item4 = cx.new(|cx| { - TestItem::new(cx).with_dirty(true).with_project_items(&[{ - let project_item = TestProjectItem::new_untitled(cx); - project_item.update(cx, |project_item, _| project_item.is_dirty = true); - project_item - }]) - }); - let pane = workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane(Box::new(item1.clone()), None, true, window, cx); - workspace.add_item_to_active_pane(Box::new(item2.clone()), None, true, window, cx); - workspace.add_item_to_active_pane(Box::new(item3.clone()), None, true, window, cx); - workspace.add_item_to_active_pane(Box::new(item4.clone()), None, true, window, cx); - workspace.active_pane().clone() - }); - - let close_items = pane.update_in(cx, |pane, window, cx| { - pane.activate_item(1, true, true, window, cx); - assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id()); - let item1_id = item1.item_id(); - let item3_id = item3.item_id(); - let item4_id = item4.item_id(); - pane.close_items(window, cx, SaveIntent::Close, move |id| { - [item1_id, item3_id, item4_id].contains(&id) - }) - }); - cx.executor().run_until_parked(); - - assert!(cx.has_pending_prompt()); - cx.simulate_prompt_answer("Save all"); - - cx.executor().run_until_parked(); - - // Item 1 is saved. There's a prompt to save item 3. - pane.update(cx, |pane, cx| { - assert_eq!(item1.read(cx).save_count, 1); - assert_eq!(item1.read(cx).save_as_count, 0); - assert_eq!(item1.read(cx).reload_count, 0); - assert_eq!(pane.items_len(), 3); - assert_eq!(pane.active_item().unwrap().item_id(), item3.item_id()); - }); - assert!(cx.has_pending_prompt()); - - // Cancel saving item 3. - cx.simulate_prompt_answer("Discard"); - cx.executor().run_until_parked(); - - // Item 3 is reloaded. There's a prompt to save item 4. - pane.update(cx, |pane, cx| { - assert_eq!(item3.read(cx).save_count, 0); - assert_eq!(item3.read(cx).save_as_count, 0); - assert_eq!(item3.read(cx).reload_count, 1); - assert_eq!(pane.items_len(), 2); - assert_eq!(pane.active_item().unwrap().item_id(), item4.item_id()); - }); - - // There's a prompt for a path for item 4. - cx.simulate_new_path_selection(|_| Some(Default::default())); - close_items.await.unwrap(); - - // The requested items are closed. - pane.update(cx, |pane, cx| { - assert_eq!(item4.read(cx).save_count, 0); - assert_eq!(item4.read(cx).save_as_count, 1); - assert_eq!(item4.read(cx).reload_count, 0); - assert_eq!(pane.items_len(), 1); - assert_eq!(pane.active_item().unwrap().item_id(), item2.item_id()); - }); - } - - #[gpui::test] - async fn test_prompting_to_save_only_on_last_item_for_entry(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - - // Create several workspace items with single project entries, and two - // workspace items with multiple project entries. - let single_entry_items = (0..=4) - .map(|project_entry_id| { - cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_project_items(&[dirty_project_item( - project_entry_id, - &format!("{project_entry_id}.txt"), - cx, - )]) - }) - }) - .collect::>(); - let item_2_3 = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_buffer_kind(ItemBufferKind::Multibuffer) - .with_project_items(&[ - single_entry_items[2].read(cx).project_items[0].clone(), - single_entry_items[3].read(cx).project_items[0].clone(), - ]) - }); - let item_3_4 = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_buffer_kind(ItemBufferKind::Multibuffer) - .with_project_items(&[ - single_entry_items[3].read(cx).project_items[0].clone(), - single_entry_items[4].read(cx).project_items[0].clone(), - ]) - }); - - // Create two panes that contain the following project entries: - // left pane: - // multi-entry items: (2, 3) - // single-entry items: 0, 2, 3, 4 - // right pane: - // single-entry items: 4, 1 - // multi-entry items: (3, 4) - let (left_pane, right_pane) = workspace.update_in(cx, |workspace, window, cx| { - let left_pane = workspace.active_pane().clone(); - workspace.add_item_to_active_pane(Box::new(item_2_3.clone()), None, true, window, cx); - workspace.add_item_to_active_pane( - single_entry_items[0].boxed_clone(), - None, - true, - window, - cx, - ); - workspace.add_item_to_active_pane( - single_entry_items[2].boxed_clone(), - None, - true, - window, - cx, - ); - workspace.add_item_to_active_pane( - single_entry_items[3].boxed_clone(), - None, - true, - window, - cx, - ); - workspace.add_item_to_active_pane( - single_entry_items[4].boxed_clone(), - None, - true, - window, - cx, - ); - - let right_pane = - workspace.split_and_clone(left_pane.clone(), SplitDirection::Right, window, cx); - - let boxed_clone = single_entry_items[1].boxed_clone(); - let right_pane = window.spawn(cx, async move |cx| { - right_pane.await.inspect(|right_pane| { - right_pane - .update_in(cx, |pane, window, cx| { - pane.add_item(boxed_clone, true, true, None, window, cx); - pane.add_item(Box::new(item_3_4.clone()), true, true, None, window, cx); - }) - .unwrap(); - }) - }); - - (left_pane, right_pane) - }); - let right_pane = right_pane.await.unwrap(); - cx.focus(&right_pane); - - let mut close = right_pane.update_in(cx, |pane, window, cx| { - pane.close_all_items(&CloseAllItems::default(), window, cx) - .unwrap() - }); - cx.executor().run_until_parked(); - - let msg = cx.pending_prompt().unwrap().0; - assert!(msg.contains("1.txt")); - assert!(!msg.contains("2.txt")); - assert!(!msg.contains("3.txt")); - assert!(!msg.contains("4.txt")); - - cx.simulate_prompt_answer("Cancel"); - close.await; - - left_pane - .update_in(cx, |left_pane, window, cx| { - left_pane.close_item_by_id( - single_entry_items[3].entity_id(), - SaveIntent::Skip, - window, - cx, - ) - }) - .await - .unwrap(); - - close = right_pane.update_in(cx, |pane, window, cx| { - pane.close_all_items(&CloseAllItems::default(), window, cx) - .unwrap() - }); - cx.executor().run_until_parked(); - - let details = cx.pending_prompt().unwrap().1; - assert!(details.contains("1.txt")); - assert!(!details.contains("2.txt")); - assert!(details.contains("3.txt")); - // ideally this assertion could be made, but today we can only - // save whole items not project items, so the orphaned item 3 causes - // 4 to be saved too. - // assert!(!details.contains("4.txt")); - - cx.simulate_prompt_answer("Save all"); - - cx.executor().run_until_parked(); - close.await; - right_pane.read_with(cx, |pane, _| { - assert_eq!(pane.items_len(), 0); - }); - } - - #[gpui::test] - async fn test_autosave(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], 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()); - - let item = cx.new(|cx| { - TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) - }); - let item_id = item.entity_id(); - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx); - }); - - // Autosave on window change. - item.update(cx, |item, cx| { - SettingsStore::update_global(cx, |settings, cx| { - settings.update_user_settings(cx, |settings| { - settings.workspace.autosave = Some(AutosaveSetting::OnWindowChange); - }) - }); - item.is_dirty = true; - }); - - // Deactivating the window saves the file. - cx.deactivate_window(); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 1)); - - // Re-activating the window doesn't save the file. - cx.update(|window, _| window.activate_window()); - cx.executor().run_until_parked(); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 1)); - - // Autosave on focus change. - item.update_in(cx, |item, window, cx| { - cx.focus_self(window); - SettingsStore::update_global(cx, |settings, cx| { - settings.update_user_settings(cx, |settings| { - settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange); - }) - }); - item.is_dirty = true; - }); - // Blurring the item saves the file. - item.update_in(cx, |_, window, _| window.blur()); - cx.executor().run_until_parked(); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 2)); - - // Deactivating the window still saves the file. - item.update_in(cx, |item, window, cx| { - cx.focus_self(window); - item.is_dirty = true; - }); - cx.deactivate_window(); - item.update(cx, |item, _| assert_eq!(item.save_count, 3)); - - // Autosave after delay. - item.update(cx, |item, cx| { - SettingsStore::update_global(cx, |settings, cx| { - settings.update_user_settings(cx, |settings| { - settings.workspace.autosave = Some(AutosaveSetting::AfterDelay { - milliseconds: 500.into(), - }); - }) - }); - item.is_dirty = true; - cx.emit(ItemEvent::Edit); - }); - - // Delay hasn't fully expired, so the file is still dirty and unsaved. - cx.executor().advance_clock(Duration::from_millis(250)); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 3)); - - // After delay expires, the file is saved. - cx.executor().advance_clock(Duration::from_millis(250)); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 4)); - - // Autosave after delay, should save earlier than delay if tab is closed - item.update(cx, |item, cx| { - item.is_dirty = true; - cx.emit(ItemEvent::Edit); - }); - cx.executor().advance_clock(Duration::from_millis(250)); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 4)); - - // // Ensure auto save with delay saves the item on close, even if the timer hasn't yet run out. - pane.update_in(cx, |pane, window, cx| { - pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id) - }) - .await - .unwrap(); - assert!(!cx.has_pending_prompt()); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); - - // Add the item again, ensuring autosave is prevented if the underlying file has been deleted. - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx); - }); - item.update_in(cx, |item, _window, cx| { - item.is_dirty = true; - for project_item in &mut item.project_items { - project_item.update(cx, |project_item, _| project_item.is_dirty = true); - } - }); - cx.run_until_parked(); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); - - // Autosave on focus change, ensuring closing the tab counts as such. - item.update(cx, |item, cx| { - SettingsStore::update_global(cx, |settings, cx| { - settings.update_user_settings(cx, |settings| { - settings.workspace.autosave = Some(AutosaveSetting::OnFocusChange); - }) - }); - item.is_dirty = true; - for project_item in &mut item.project_items { - project_item.update(cx, |project_item, _| project_item.is_dirty = true); - } - }); - - pane.update_in(cx, |pane, window, cx| { - pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id) - }) - .await - .unwrap(); - assert!(!cx.has_pending_prompt()); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 6)); - - // Add the item again, ensuring autosave is prevented if the underlying file has been deleted. - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx); - }); - item.update_in(cx, |item, window, cx| { - item.project_items[0].update(cx, |item, _| { - item.entry_id = None; - }); - item.is_dirty = true; - window.blur(); - }); - cx.run_until_parked(); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 6)); - - // Ensure autosave is prevented for deleted files also when closing the buffer. - let _close_items = pane.update_in(cx, |pane, window, cx| { - pane.close_items(window, cx, SaveIntent::Close, move |id| id == item_id) - }); - cx.run_until_parked(); - assert!(cx.has_pending_prompt()); - item.read_with(cx, |item, _| assert_eq!(item.save_count, 6)); - } - - #[gpui::test] - async fn test_pane_navigation(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - - let item = cx.new(|cx| { - TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) - }); - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone()); - let toolbar_notify_count = Rc::new(RefCell::new(0)); - - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane(Box::new(item.clone()), None, true, window, cx); - let toolbar_notification_count = toolbar_notify_count.clone(); - cx.observe_in(&toolbar, window, move |_, _, _, _| { - *toolbar_notification_count.borrow_mut() += 1 - }) - .detach(); - }); - - pane.read_with(cx, |pane, _| { - assert!(!pane.can_navigate_backward()); - assert!(!pane.can_navigate_forward()); - }); - - item.update_in(cx, |item, _, cx| { - item.set_state("one".to_string(), cx); - }); - - // Toolbar must be notified to re-render the navigation buttons - assert_eq!(*toolbar_notify_count.borrow(), 1); - - pane.read_with(cx, |pane, _| { - assert!(pane.can_navigate_backward()); - assert!(!pane.can_navigate_forward()); - }); - - workspace - .update_in(cx, |workspace, window, cx| { - workspace.go_back(pane.downgrade(), window, cx) - }) - .await - .unwrap(); - - assert_eq!(*toolbar_notify_count.borrow(), 2); - pane.read_with(cx, |pane, _| { - assert!(!pane.can_navigate_backward()); - assert!(pane.can_navigate_forward()); - }); - } - - #[gpui::test] - async fn test_toggle_docks_and_panels(cx: &mut gpui::TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - - let panel = workspace.update_in(cx, |workspace, window, cx| { - let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx)); - workspace.add_panel(panel.clone(), window, cx); - - workspace - .right_dock() - .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx)); - - panel - }); - - let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - pane.update_in(cx, |pane, window, cx| { - let item = cx.new(TestItem::new); - pane.add_item(Box::new(item), true, true, None, window, cx); - }); - - // Transfer focus from center to panel - workspace.update_in(cx, |workspace, window, cx| { - workspace.toggle_panel_focus::(window, cx); - }); - - workspace.update_in(cx, |workspace, window, cx| { - assert!(workspace.right_dock().read(cx).is_open()); - assert!(!panel.is_zoomed(window, cx)); - assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx)); - }); - - // Transfer focus from panel to center - workspace.update_in(cx, |workspace, window, cx| { - workspace.toggle_panel_focus::(window, cx); - }); - - workspace.update_in(cx, |workspace, window, cx| { - assert!(workspace.right_dock().read(cx).is_open()); - assert!(!panel.is_zoomed(window, cx)); - assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx)); - }); - - // Close the dock - workspace.update_in(cx, |workspace, window, cx| { - workspace.toggle_dock(DockPosition::Right, window, cx); - }); - - workspace.update_in(cx, |workspace, window, cx| { - assert!(!workspace.right_dock().read(cx).is_open()); - assert!(!panel.is_zoomed(window, cx)); - assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx)); - }); - - // Open the dock - workspace.update_in(cx, |workspace, window, cx| { - workspace.toggle_dock(DockPosition::Right, window, cx); - }); - - workspace.update_in(cx, |workspace, window, cx| { - assert!(workspace.right_dock().read(cx).is_open()); - assert!(!panel.is_zoomed(window, cx)); - assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx)); - }); - - // Focus and zoom panel - panel.update_in(cx, |panel, window, cx| { - cx.focus_self(window); - panel.set_zoomed(true, window, cx) - }); - - workspace.update_in(cx, |workspace, window, cx| { - assert!(workspace.right_dock().read(cx).is_open()); - assert!(panel.is_zoomed(window, cx)); - assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx)); - }); - - // Transfer focus to the center closes the dock - workspace.update_in(cx, |workspace, window, cx| { - workspace.toggle_panel_focus::(window, cx); - }); - - workspace.update_in(cx, |workspace, window, cx| { - assert!(!workspace.right_dock().read(cx).is_open()); - assert!(panel.is_zoomed(window, cx)); - assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx)); - }); - - // Transferring focus back to the panel keeps it zoomed - workspace.update_in(cx, |workspace, window, cx| { - workspace.toggle_panel_focus::(window, cx); - }); - - workspace.update_in(cx, |workspace, window, cx| { - assert!(workspace.right_dock().read(cx).is_open()); - assert!(panel.is_zoomed(window, cx)); - assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx)); - }); - - // Close the dock while it is zoomed - workspace.update_in(cx, |workspace, window, cx| { - workspace.toggle_dock(DockPosition::Right, window, cx) - }); - - workspace.update_in(cx, |workspace, window, cx| { - assert!(!workspace.right_dock().read(cx).is_open()); - assert!(panel.is_zoomed(window, cx)); - assert!(workspace.zoomed.is_none()); - assert!(!panel.read(cx).focus_handle(cx).contains_focused(window, cx)); - }); - - // Opening the dock, when it's zoomed, retains focus - workspace.update_in(cx, |workspace, window, cx| { - workspace.toggle_dock(DockPosition::Right, window, cx) - }); - - workspace.update_in(cx, |workspace, window, cx| { - assert!(workspace.right_dock().read(cx).is_open()); - assert!(panel.is_zoomed(window, cx)); - assert!(workspace.zoomed.is_some()); - assert!(panel.read(cx).focus_handle(cx).contains_focused(window, cx)); - }); - - // Unzoom and close the panel, zoom the active pane. - panel.update_in(cx, |panel, window, cx| panel.set_zoomed(false, window, cx)); - workspace.update_in(cx, |workspace, window, cx| { - workspace.toggle_dock(DockPosition::Right, window, cx) - }); - pane.update_in(cx, |pane, window, cx| { - pane.toggle_zoom(&Default::default(), window, cx) - }); - - // Opening a dock unzooms the pane. - workspace.update_in(cx, |workspace, window, cx| { - workspace.toggle_dock(DockPosition::Right, window, cx) - }); - workspace.update_in(cx, |workspace, window, cx| { - let pane = pane.read(cx); - assert!(!pane.is_zoomed()); - assert!(!pane.focus_handle(cx).is_focused(window)); - assert!(workspace.right_dock().read(cx).is_open()); - assert!(workspace.zoomed.is_none()); - }); - } - - #[gpui::test] - async fn test_toggle_all_docks(cx: &mut gpui::TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - workspace.update_in(cx, |workspace, window, cx| { - // Open two docks - let left_dock = workspace.dock_at_position(DockPosition::Left); - let right_dock = workspace.dock_at_position(DockPosition::Right); - - left_dock.update(cx, |dock, cx| dock.set_open(true, window, cx)); - right_dock.update(cx, |dock, cx| dock.set_open(true, window, cx)); - - assert!(left_dock.read(cx).is_open()); - assert!(right_dock.read(cx).is_open()); - }); - - workspace.update_in(cx, |workspace, window, cx| { - // Toggle all docks - should close both - workspace.toggle_all_docks(&ToggleAllDocks, window, cx); - - let left_dock = workspace.dock_at_position(DockPosition::Left); - let right_dock = workspace.dock_at_position(DockPosition::Right); - assert!(!left_dock.read(cx).is_open()); - assert!(!right_dock.read(cx).is_open()); - }); - - workspace.update_in(cx, |workspace, window, cx| { - // Toggle again - should reopen both - workspace.toggle_all_docks(&ToggleAllDocks, window, cx); - - let left_dock = workspace.dock_at_position(DockPosition::Left); - let right_dock = workspace.dock_at_position(DockPosition::Right); - assert!(left_dock.read(cx).is_open()); - assert!(right_dock.read(cx).is_open()); - }); - } - - #[gpui::test] - async fn test_toggle_all_with_manual_close(cx: &mut gpui::TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - workspace.update_in(cx, |workspace, window, cx| { - // Open two docks - let left_dock = workspace.dock_at_position(DockPosition::Left); - let right_dock = workspace.dock_at_position(DockPosition::Right); - - left_dock.update(cx, |dock, cx| dock.set_open(true, window, cx)); - right_dock.update(cx, |dock, cx| dock.set_open(true, window, cx)); - - assert!(left_dock.read(cx).is_open()); - assert!(right_dock.read(cx).is_open()); - }); - - workspace.update_in(cx, |workspace, window, cx| { - // Close them manually - workspace.toggle_dock(DockPosition::Left, window, cx); - workspace.toggle_dock(DockPosition::Right, window, cx); - - let left_dock = workspace.dock_at_position(DockPosition::Left); - let right_dock = workspace.dock_at_position(DockPosition::Right); - assert!(!left_dock.read(cx).is_open()); - assert!(!right_dock.read(cx).is_open()); - }); - - workspace.update_in(cx, |workspace, window, cx| { - // Toggle all docks - only last closed (right dock) should reopen - workspace.toggle_all_docks(&ToggleAllDocks, window, cx); - - let left_dock = workspace.dock_at_position(DockPosition::Left); - let right_dock = workspace.dock_at_position(DockPosition::Right); - assert!(!left_dock.read(cx).is_open()); - assert!(right_dock.read(cx).is_open()); - }); - } - - #[gpui::test] - async fn test_toggle_all_docks_after_dock_move(cx: &mut gpui::TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - - // Open two docks (left and right) with one panel each - let (left_panel, right_panel) = workspace.update_in(cx, |workspace, window, cx| { - let left_panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx)); - workspace.add_panel(left_panel.clone(), window, cx); - - let right_panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx)); - workspace.add_panel(right_panel.clone(), window, cx); - - workspace.toggle_dock(DockPosition::Left, window, cx); - workspace.toggle_dock(DockPosition::Right, window, cx); - - // Verify initial state - assert!( - workspace.left_dock().read(cx).is_open(), - "Left dock should be open" - ); - assert_eq!( - workspace - .left_dock() - .read(cx) - .visible_panel() - .unwrap() - .panel_id(), - left_panel.panel_id(), - "Left panel should be visible in left dock" - ); - assert!( - workspace.right_dock().read(cx).is_open(), - "Right dock should be open" - ); - assert_eq!( - workspace - .right_dock() - .read(cx) - .visible_panel() - .unwrap() - .panel_id(), - right_panel.panel_id(), - "Right panel should be visible in right dock" - ); - assert!( - !workspace.bottom_dock().read(cx).is_open(), - "Bottom dock should be closed" - ); - - (left_panel, right_panel) - }); - - // Focus the left panel and move it to the next position (bottom dock) - workspace.update_in(cx, |workspace, window, cx| { - workspace.toggle_panel_focus::(window, cx); // Focus left panel - assert!( - left_panel.read(cx).focus_handle(cx).is_focused(window), - "Left panel should be focused" - ); - }); - - cx.dispatch_action(MoveFocusedPanelToNextPosition); - - // Verify the left panel has moved to the bottom dock, and the bottom dock is now open - workspace.update(cx, |workspace, cx| { - assert!( - !workspace.left_dock().read(cx).is_open(), - "Left dock should be closed" - ); - assert!( - workspace.bottom_dock().read(cx).is_open(), - "Bottom dock should now be open" - ); - assert_eq!( - left_panel.read(cx).position, - DockPosition::Bottom, - "Left panel should now be in the bottom dock" - ); - assert_eq!( - workspace - .bottom_dock() - .read(cx) - .visible_panel() - .unwrap() - .panel_id(), - left_panel.panel_id(), - "Left panel should be the visible panel in the bottom dock" - ); - }); - - // Toggle all docks off - workspace.update_in(cx, |workspace, window, cx| { - workspace.toggle_all_docks(&ToggleAllDocks, window, cx); - assert!( - !workspace.left_dock().read(cx).is_open(), - "Left dock should be closed" - ); - assert!( - !workspace.right_dock().read(cx).is_open(), - "Right dock should be closed" - ); - assert!( - !workspace.bottom_dock().read(cx).is_open(), - "Bottom dock should be closed" - ); - }); - - // Toggle all docks back on and verify positions are restored - workspace.update_in(cx, |workspace, window, cx| { - workspace.toggle_all_docks(&ToggleAllDocks, window, cx); - assert!( - !workspace.left_dock().read(cx).is_open(), - "Left dock should remain closed" - ); - assert!( - workspace.right_dock().read(cx).is_open(), - "Right dock should remain open" - ); - assert!( - workspace.bottom_dock().read(cx).is_open(), - "Bottom dock should remain open" - ); - assert_eq!( - left_panel.read(cx).position, - DockPosition::Bottom, - "Left panel should remain in the bottom dock" - ); - assert_eq!( - right_panel.read(cx).position, - DockPosition::Right, - "Right panel should remain in the right dock" - ); - assert_eq!( - workspace - .bottom_dock() - .read(cx) - .visible_panel() - .unwrap() - .panel_id(), - left_panel.panel_id(), - "Left panel should be the visible panel in the right dock" - ); - }); - } - - #[gpui::test] - async fn test_join_pane_into_next(cx: &mut gpui::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's arrange the panes like this: - // - // +-----------------------+ - // | top | - // +------+--------+-------+ - // | left | center | right | - // +------+--------+-------+ - // | bottom | - // +-----------------------+ - - let top_item = cx.new(|cx| { - TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "top.txt", cx)]) - }); - let bottom_item = cx.new(|cx| { - TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "bottom.txt", cx)]) - }); - let left_item = cx.new(|cx| { - TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "left.txt", cx)]) - }); - let right_item = cx.new(|cx| { - TestItem::new(cx).with_project_items(&[TestProjectItem::new(4, "right.txt", cx)]) - }); - let center_item = cx.new(|cx| { - TestItem::new(cx).with_project_items(&[TestProjectItem::new(5, "center.txt", cx)]) - }); - - let top_pane_id = workspace.update_in(cx, |workspace, window, cx| { - let top_pane_id = workspace.active_pane().entity_id(); - workspace.add_item_to_active_pane(Box::new(top_item.clone()), None, false, window, cx); - workspace.split_pane( - workspace.active_pane().clone(), - SplitDirection::Down, - window, - cx, - ); - top_pane_id - }); - let bottom_pane_id = workspace.update_in(cx, |workspace, window, cx| { - let bottom_pane_id = workspace.active_pane().entity_id(); - workspace.add_item_to_active_pane( - Box::new(bottom_item.clone()), - None, - false, - window, - cx, - ); - workspace.split_pane( - workspace.active_pane().clone(), - SplitDirection::Up, - window, - cx, - ); - bottom_pane_id - }); - let left_pane_id = workspace.update_in(cx, |workspace, window, cx| { - let left_pane_id = workspace.active_pane().entity_id(); - workspace.add_item_to_active_pane(Box::new(left_item.clone()), None, false, window, cx); - workspace.split_pane( - workspace.active_pane().clone(), - SplitDirection::Right, - window, - cx, - ); - left_pane_id - }); - let right_pane_id = workspace.update_in(cx, |workspace, window, cx| { - let right_pane_id = workspace.active_pane().entity_id(); - workspace.add_item_to_active_pane( - Box::new(right_item.clone()), - None, - false, - window, - cx, - ); - workspace.split_pane( - workspace.active_pane().clone(), - SplitDirection::Left, - window, - cx, - ); - right_pane_id - }); - let center_pane_id = workspace.update_in(cx, |workspace, window, cx| { - let center_pane_id = workspace.active_pane().entity_id(); - workspace.add_item_to_active_pane( - Box::new(center_item.clone()), - None, - false, - window, - cx, - ); - center_pane_id - }); - cx.executor().run_until_parked(); - - workspace.update_in(cx, |workspace, window, cx| { - assert_eq!(center_pane_id, workspace.active_pane().entity_id()); - - // Join into next from center pane into right - workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx); - }); - - workspace.update_in(cx, |workspace, window, cx| { - let active_pane = workspace.active_pane(); - assert_eq!(right_pane_id, active_pane.entity_id()); - assert_eq!(2, active_pane.read(cx).items_len()); - let item_ids_in_pane = - HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id())); - assert!(item_ids_in_pane.contains(¢er_item.item_id())); - assert!(item_ids_in_pane.contains(&right_item.item_id())); - - // Join into next from right pane into bottom - workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx); - }); - - workspace.update_in(cx, |workspace, window, cx| { - let active_pane = workspace.active_pane(); - assert_eq!(bottom_pane_id, active_pane.entity_id()); - assert_eq!(3, active_pane.read(cx).items_len()); - let item_ids_in_pane = - HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id())); - assert!(item_ids_in_pane.contains(¢er_item.item_id())); - assert!(item_ids_in_pane.contains(&right_item.item_id())); - assert!(item_ids_in_pane.contains(&bottom_item.item_id())); - - // Join into next from bottom pane into left - workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx); - }); - - workspace.update_in(cx, |workspace, window, cx| { - let active_pane = workspace.active_pane(); - assert_eq!(left_pane_id, active_pane.entity_id()); - assert_eq!(4, active_pane.read(cx).items_len()); - let item_ids_in_pane = - HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id())); - assert!(item_ids_in_pane.contains(¢er_item.item_id())); - assert!(item_ids_in_pane.contains(&right_item.item_id())); - assert!(item_ids_in_pane.contains(&bottom_item.item_id())); - assert!(item_ids_in_pane.contains(&left_item.item_id())); - - // Join into next from left pane into top - workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx); - }); - - workspace.update_in(cx, |workspace, window, cx| { - let active_pane = workspace.active_pane(); - assert_eq!(top_pane_id, active_pane.entity_id()); - assert_eq!(5, active_pane.read(cx).items_len()); - let item_ids_in_pane = - HashSet::from_iter(active_pane.read(cx).items().map(|item| item.item_id())); - assert!(item_ids_in_pane.contains(¢er_item.item_id())); - assert!(item_ids_in_pane.contains(&right_item.item_id())); - assert!(item_ids_in_pane.contains(&bottom_item.item_id())); - assert!(item_ids_in_pane.contains(&left_item.item_id())); - assert!(item_ids_in_pane.contains(&top_item.item_id())); - - // Single pane left: no-op - workspace.join_pane_into_next(workspace.active_pane().clone(), window, cx) - }); - - workspace.update(cx, |workspace, _cx| { - let active_pane = workspace.active_pane(); - assert_eq!(top_pane_id, active_pane.entity_id()); - }); - } - - fn add_an_item_to_active_pane( - cx: &mut VisualTestContext, - workspace: &Entity, - item_id: u64, - ) -> Entity { - let item = cx.new(|cx| { - TestItem::new(cx).with_project_items(&[TestProjectItem::new( - item_id, - "item{item_id}.txt", - cx, - )]) - }); - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane(Box::new(item.clone()), None, false, window, cx); - }); - item - } - - fn split_pane(cx: &mut VisualTestContext, workspace: &Entity) -> Entity { - workspace.update_in(cx, |workspace, window, cx| { - workspace.split_pane( - workspace.active_pane().clone(), - SplitDirection::Right, - window, - cx, - ) - }) - } - - #[gpui::test] - async fn test_join_all_panes(cx: &mut gpui::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)); - - add_an_item_to_active_pane(cx, &workspace, 1); - split_pane(cx, &workspace); - add_an_item_to_active_pane(cx, &workspace, 2); - split_pane(cx, &workspace); // empty pane - split_pane(cx, &workspace); - let last_item = add_an_item_to_active_pane(cx, &workspace, 3); - - cx.executor().run_until_parked(); - - workspace.update(cx, |workspace, cx| { - let num_panes = workspace.panes().len(); - let num_items_in_current_pane = workspace.active_pane().read(cx).items().count(); - let active_item = workspace - .active_pane() - .read(cx) - .active_item() - .expect("item is in focus"); - - assert_eq!(num_panes, 4); - assert_eq!(num_items_in_current_pane, 1); - assert_eq!(active_item.item_id(), last_item.item_id()); - }); - - workspace.update_in(cx, |workspace, window, cx| { - workspace.join_all_panes(window, cx); - }); - - workspace.update(cx, |workspace, cx| { - let num_panes = workspace.panes().len(); - let num_items_in_current_pane = workspace.active_pane().read(cx).items().count(); - let active_item = workspace - .active_pane() - .read(cx) - .active_item() - .expect("item is in focus"); - - assert_eq!(num_panes, 1); - assert_eq!(num_items_in_current_pane, 3); - assert_eq!(active_item.item_id(), last_item.item_id()); - }); - } - struct TestModal(FocusHandle); - - impl TestModal { - fn new(_: &mut Window, cx: &mut Context) -> Self { - Self(cx.focus_handle()) - } - } - - impl EventEmitter for TestModal {} - - impl Focusable for TestModal { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.0.clone() - } - } - - impl ModalView for TestModal {} - - impl Render for TestModal { - fn render( - &mut self, - _window: &mut Window, - _cx: &mut Context, - ) -> impl IntoElement { - div().track_focus(&self.0) - } - } - - #[gpui::test] - async fn test_panels(cx: &mut gpui::TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - - let project = Project::test(fs, [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - - let (panel_1, panel_2) = workspace.update_in(cx, |workspace, window, cx| { - let panel_1 = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx)); - workspace.add_panel(panel_1.clone(), window, cx); - workspace.toggle_dock(DockPosition::Left, window, cx); - let panel_2 = cx.new(|cx| TestPanel::new(DockPosition::Right, 101, cx)); - workspace.add_panel(panel_2.clone(), window, cx); - workspace.toggle_dock(DockPosition::Right, window, cx); - - let left_dock = workspace.left_dock(); - assert_eq!( - left_dock.read(cx).visible_panel().unwrap().panel_id(), - panel_1.panel_id() - ); - assert_eq!( - left_dock.read(cx).active_panel_size(window, cx).unwrap(), - panel_1.size(window, cx) - ); - - left_dock.update(cx, |left_dock, cx| { - left_dock.resize_active_panel(Some(px(1337.)), window, cx) - }); - assert_eq!( - workspace - .right_dock() - .read(cx) - .visible_panel() - .unwrap() - .panel_id(), - panel_2.panel_id(), - ); - - (panel_1, panel_2) - }); - - // Move panel_1 to the right - panel_1.update_in(cx, |panel_1, window, cx| { - panel_1.set_position(DockPosition::Right, window, cx) - }); - - workspace.update_in(cx, |workspace, window, cx| { - // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right. - // Since it was the only panel on the left, the left dock should now be closed. - assert!(!workspace.left_dock().read(cx).is_open()); - assert!(workspace.left_dock().read(cx).visible_panel().is_none()); - let right_dock = workspace.right_dock(); - assert_eq!( - right_dock.read(cx).visible_panel().unwrap().panel_id(), - panel_1.panel_id() - ); - assert_eq!( - right_dock.read(cx).active_panel_size(window, cx).unwrap(), - px(1337.) - ); - - // Now we move panel_2 to the left - panel_2.set_position(DockPosition::Left, window, cx); - }); - - workspace.update(cx, |workspace, cx| { - // Since panel_2 was not visible on the right, we don't open the left dock. - assert!(!workspace.left_dock().read(cx).is_open()); - // And the right dock is unaffected in its displaying of panel_1 - assert!(workspace.right_dock().read(cx).is_open()); - assert_eq!( - workspace - .right_dock() - .read(cx) - .visible_panel() - .unwrap() - .panel_id(), - panel_1.panel_id(), - ); - }); - - // Move panel_1 back to the left - panel_1.update_in(cx, |panel_1, window, cx| { - panel_1.set_position(DockPosition::Left, window, cx) - }); - - workspace.update_in(cx, |workspace, window, cx| { - // Since panel_1 was visible on the right, we open the left dock and make panel_1 active. - let left_dock = workspace.left_dock(); - assert!(left_dock.read(cx).is_open()); - assert_eq!( - left_dock.read(cx).visible_panel().unwrap().panel_id(), - panel_1.panel_id() - ); - assert_eq!( - left_dock.read(cx).active_panel_size(window, cx).unwrap(), - px(1337.) - ); - // And the right dock should be closed as it no longer has any panels. - assert!(!workspace.right_dock().read(cx).is_open()); - - // Now we move panel_1 to the bottom - panel_1.set_position(DockPosition::Bottom, window, cx); - }); - - workspace.update_in(cx, |workspace, window, cx| { - // Since panel_1 was visible on the left, we close the left dock. - assert!(!workspace.left_dock().read(cx).is_open()); - // The bottom dock is sized based on the panel's default size, - // since the panel orientation changed from vertical to horizontal. - let bottom_dock = workspace.bottom_dock(); - assert_eq!( - bottom_dock.read(cx).active_panel_size(window, cx).unwrap(), - panel_1.size(window, cx), - ); - // Close bottom dock and move panel_1 back to the left. - bottom_dock.update(cx, |bottom_dock, cx| { - bottom_dock.set_open(false, window, cx) - }); - panel_1.set_position(DockPosition::Left, window, cx); - }); - - // Emit activated event on panel 1 - panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Activate)); - - // Now the left dock is open and panel_1 is active and focused. - workspace.update_in(cx, |workspace, window, cx| { - let left_dock = workspace.left_dock(); - assert!(left_dock.read(cx).is_open()); - assert_eq!( - left_dock.read(cx).visible_panel().unwrap().panel_id(), - panel_1.panel_id(), - ); - assert!(panel_1.focus_handle(cx).is_focused(window)); - }); - - // Emit closed event on panel 2, which is not active - panel_2.update(cx, |_, cx| cx.emit(PanelEvent::Close)); - - // Wo don't close the left dock, because panel_2 wasn't the active panel - workspace.update(cx, |workspace, cx| { - let left_dock = workspace.left_dock(); - assert!(left_dock.read(cx).is_open()); - assert_eq!( - left_dock.read(cx).visible_panel().unwrap().panel_id(), - panel_1.panel_id(), - ); - }); - - // Emitting a ZoomIn event shows the panel as zoomed. - panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomIn)); - workspace.read_with(cx, |workspace, _| { - assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade())); - assert_eq!(workspace.zoomed_position, Some(DockPosition::Left)); - }); - - // Move panel to another dock while it is zoomed - panel_1.update_in(cx, |panel, window, cx| { - panel.set_position(DockPosition::Right, window, cx) - }); - workspace.read_with(cx, |workspace, _| { - assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade())); - - assert_eq!(workspace.zoomed_position, Some(DockPosition::Right)); - }); - - // This is a helper for getting a: - // - valid focus on an element, - // - that isn't a part of the panes and panels system of the Workspace, - // - and doesn't trigger the 'on_focus_lost' API. - let focus_other_view = { - let workspace = workspace.clone(); - move |cx: &mut VisualTestContext| { - workspace.update_in(cx, |workspace, window, cx| { - if workspace.active_modal::(cx).is_some() { - workspace.toggle_modal(window, cx, TestModal::new); - workspace.toggle_modal(window, cx, TestModal::new); - } else { - workspace.toggle_modal(window, cx, TestModal::new); - } - }) - } - }; - - // If focus is transferred to another view that's not a panel or another pane, we still show - // the panel as zoomed. - focus_other_view(cx); - workspace.read_with(cx, |workspace, _| { - assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade())); - assert_eq!(workspace.zoomed_position, Some(DockPosition::Right)); - }); - - // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed. - workspace.update_in(cx, |_workspace, window, cx| { - cx.focus_self(window); - }); - workspace.read_with(cx, |workspace, _| { - assert_eq!(workspace.zoomed, None); - assert_eq!(workspace.zoomed_position, None); - }); - - // If focus is transferred again to another view that's not a panel or a pane, we won't - // show the panel as zoomed because it wasn't zoomed before. - focus_other_view(cx); - workspace.read_with(cx, |workspace, _| { - assert_eq!(workspace.zoomed, None); - assert_eq!(workspace.zoomed_position, None); - }); - - // When the panel is activated, it is zoomed again. - cx.dispatch_action(ToggleRightDock); - workspace.read_with(cx, |workspace, _| { - assert_eq!(workspace.zoomed, Some(panel_1.to_any().downgrade())); - assert_eq!(workspace.zoomed_position, Some(DockPosition::Right)); - }); - - // Emitting a ZoomOut event unzooms the panel. - panel_1.update(cx, |_, cx| cx.emit(PanelEvent::ZoomOut)); - workspace.read_with(cx, |workspace, _| { - assert_eq!(workspace.zoomed, None); - assert_eq!(workspace.zoomed_position, None); - }); - - // Emit closed event on panel 1, which is active - panel_1.update(cx, |_, cx| cx.emit(PanelEvent::Close)); - - // Now the left dock is closed, because panel_1 was the active panel - workspace.update(cx, |workspace, cx| { - let right_dock = workspace.right_dock(); - assert!(!right_dock.read(cx).is_open()); - }); - } - - #[gpui::test] - async fn test_no_save_prompt_when_multi_buffer_dirty_items_closed(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - let project = Project::test(fs, [], 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()); - - let dirty_regular_buffer = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_label("1.txt") - .with_project_items(&[dirty_project_item(1, "1.txt", cx)]) - }); - let dirty_regular_buffer_2 = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_label("2.txt") - .with_project_items(&[dirty_project_item(2, "2.txt", cx)]) - }); - let dirty_multi_buffer_with_both = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_buffer_kind(ItemBufferKind::Multibuffer) - .with_label("Fake Project Search") - .with_project_items(&[ - dirty_regular_buffer.read(cx).project_items[0].clone(), - dirty_regular_buffer_2.read(cx).project_items[0].clone(), - ]) - }); - let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id(); - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item( - pane.clone(), - Box::new(dirty_regular_buffer.clone()), - None, - false, - false, - window, - cx, - ); - workspace.add_item( - pane.clone(), - Box::new(dirty_regular_buffer_2.clone()), - None, - false, - false, - window, - cx, - ); - workspace.add_item( - pane.clone(), - Box::new(dirty_multi_buffer_with_both.clone()), - None, - false, - false, - window, - cx, - ); - }); - - pane.update_in(cx, |pane, window, cx| { - pane.activate_item(2, true, true, window, cx); - assert_eq!( - pane.active_item().unwrap().item_id(), - multi_buffer_with_both_files_id, - "Should select the multi buffer in the pane" - ); - }); - let close_all_but_multi_buffer_task = pane.update_in(cx, |pane, window, cx| { - pane.close_other_items( - &CloseOtherItems { - save_intent: Some(SaveIntent::Save), - close_pinned: true, - }, - None, - window, - cx, - ) - }); - cx.background_executor.run_until_parked(); - assert!(!cx.has_pending_prompt()); - close_all_but_multi_buffer_task - .await - .expect("Closing all buffers but the multi buffer failed"); - pane.update(cx, |pane, cx| { - assert_eq!(dirty_regular_buffer.read(cx).save_count, 1); - assert_eq!(dirty_multi_buffer_with_both.read(cx).save_count, 0); - assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 1); - assert_eq!(pane.items_len(), 1); - assert_eq!( - pane.active_item().unwrap().item_id(), - multi_buffer_with_both_files_id, - "Should have only the multi buffer left in the pane" - ); - assert!( - dirty_multi_buffer_with_both.read(cx).is_dirty, - "The multi buffer containing the unsaved buffer should still be dirty" - ); - }); - - dirty_regular_buffer.update(cx, |buffer, cx| { - buffer.project_items[0].update(cx, |pi, _| pi.is_dirty = true) - }); - - let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: Some(SaveIntent::Close), - close_pinned: false, - }, - window, - cx, - ) - }); - cx.background_executor.run_until_parked(); - assert!( - cx.has_pending_prompt(), - "Dirty multi buffer should prompt a save dialog" - ); - cx.simulate_prompt_answer("Save"); - cx.background_executor.run_until_parked(); - close_multi_buffer_task - .await - .expect("Closing the multi buffer failed"); - pane.update(cx, |pane, cx| { - assert_eq!( - dirty_multi_buffer_with_both.read(cx).save_count, - 1, - "Multi buffer item should get be saved" - ); - // Test impl does not save inner items, so we do not assert them - assert_eq!( - pane.items_len(), - 0, - "No more items should be left in the pane" - ); - assert!(pane.active_item().is_none()); - }); - } - - #[gpui::test] - async fn test_save_prompt_when_dirty_multi_buffer_closed_with_some_of_its_dirty_items_not_present_in_the_pane( - cx: &mut TestAppContext, - ) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - let project = Project::test(fs, [], 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()); - - let dirty_regular_buffer = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_label("1.txt") - .with_project_items(&[dirty_project_item(1, "1.txt", cx)]) - }); - let dirty_regular_buffer_2 = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_label("2.txt") - .with_project_items(&[dirty_project_item(2, "2.txt", cx)]) - }); - let clear_regular_buffer = cx.new(|cx| { - TestItem::new(cx) - .with_label("3.txt") - .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)]) - }); - - let dirty_multi_buffer_with_both = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_buffer_kind(ItemBufferKind::Multibuffer) - .with_label("Fake Project Search") - .with_project_items(&[ - dirty_regular_buffer.read(cx).project_items[0].clone(), - dirty_regular_buffer_2.read(cx).project_items[0].clone(), - clear_regular_buffer.read(cx).project_items[0].clone(), - ]) - }); - let multi_buffer_with_both_files_id = dirty_multi_buffer_with_both.item_id(); - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item( - pane.clone(), - Box::new(dirty_regular_buffer.clone()), - None, - false, - false, - window, - cx, - ); - workspace.add_item( - pane.clone(), - Box::new(dirty_multi_buffer_with_both.clone()), - None, - false, - false, - window, - cx, - ); - }); - - pane.update_in(cx, |pane, window, cx| { - pane.activate_item(1, true, true, window, cx); - assert_eq!( - pane.active_item().unwrap().item_id(), - multi_buffer_with_both_files_id, - "Should select the multi buffer in the pane" - ); - }); - let _close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }); - cx.background_executor.run_until_parked(); - assert!( - cx.has_pending_prompt(), - "With one dirty item from the multi buffer not being in the pane, a save prompt should be shown" - ); - } - - /// Tests that when `close_on_file_delete` is enabled, files are automatically - /// closed when they are deleted from disk. - #[gpui::test] - async fn test_close_on_disk_deletion_enabled(cx: &mut TestAppContext) { - init_test(cx); - - // Enable the close_on_disk_deletion setting - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |settings| { - settings.workspace.close_on_file_delete = Some(true); - }); - }); - - let fs = FakeFs::new(cx.background_executor.clone()); - let project = Project::test(fs, [], 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()); - - // Create a test item that simulates a file - let item = cx.new(|cx| { - TestItem::new(cx) - .with_label("test.txt") - .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)]) - }); - - // Add item to workspace - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item( - pane.clone(), - Box::new(item.clone()), - None, - false, - false, - window, - cx, - ); - }); - - // Verify the item is in the pane - pane.read_with(cx, |pane, _| { - assert_eq!(pane.items().count(), 1); - }); - - // Simulate file deletion by setting the item's deleted state - item.update(cx, |item, _| { - item.set_has_deleted_file(true); - }); - - // Emit UpdateTab event to trigger the close behavior - cx.run_until_parked(); - item.update(cx, |_, cx| { - cx.emit(ItemEvent::UpdateTab); - }); - - // Allow the close operation to complete - cx.run_until_parked(); - - // Verify the item was automatically closed - pane.read_with(cx, |pane, _| { - assert_eq!( - pane.items().count(), - 0, - "Item should be automatically closed when file is deleted" - ); - }); - } - - /// Tests that when `close_on_file_delete` is disabled (default), files remain - /// open with a strikethrough when they are deleted from disk. - #[gpui::test] - async fn test_close_on_disk_deletion_disabled(cx: &mut TestAppContext) { - init_test(cx); - - // Ensure close_on_disk_deletion is disabled (default) - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |settings| { - settings.workspace.close_on_file_delete = Some(false); - }); - }); - - let fs = FakeFs::new(cx.background_executor.clone()); - let project = Project::test(fs, [], 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()); - - // Create a test item that simulates a file - let item = cx.new(|cx| { - TestItem::new(cx) - .with_label("test.txt") - .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)]) - }); - - // Add item to workspace - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item( - pane.clone(), - Box::new(item.clone()), - None, - false, - false, - window, - cx, - ); - }); - - // Verify the item is in the pane - pane.read_with(cx, |pane, _| { - assert_eq!(pane.items().count(), 1); - }); - - // Simulate file deletion - item.update(cx, |item, _| { - item.set_has_deleted_file(true); - }); - - // Emit UpdateTab event - cx.run_until_parked(); - item.update(cx, |_, cx| { - cx.emit(ItemEvent::UpdateTab); - }); - - // Allow any potential close operation to complete - cx.run_until_parked(); - - // Verify the item remains open (with strikethrough) - pane.read_with(cx, |pane, _| { - assert_eq!( - pane.items().count(), - 1, - "Item should remain open when close_on_disk_deletion is disabled" - ); - }); - - // Verify the item shows as deleted - item.read_with(cx, |item, _| { - assert!( - item.has_deleted_file, - "Item should be marked as having deleted file" - ); - }); - } - - /// Tests that dirty files are not automatically closed when deleted from disk, - /// even when `close_on_file_delete` is enabled. This ensures users don't lose - /// unsaved changes without being prompted. - #[gpui::test] - async fn test_close_on_disk_deletion_with_dirty_file(cx: &mut TestAppContext) { - init_test(cx); - - // Enable the close_on_file_delete setting - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |settings| { - settings.workspace.close_on_file_delete = Some(true); - }); - }); - - let fs = FakeFs::new(cx.background_executor.clone()); - let project = Project::test(fs, [], 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()); - - // Create a dirty test item - let item = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_label("test.txt") - .with_project_items(&[TestProjectItem::new(1, "test.txt", cx)]) - }); - - // Add item to workspace - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item( - pane.clone(), - Box::new(item.clone()), - None, - false, - false, - window, - cx, - ); - }); - - // Simulate file deletion - item.update(cx, |item, _| { - item.set_has_deleted_file(true); - }); - - // Emit UpdateTab event to trigger the close behavior - cx.run_until_parked(); - item.update(cx, |_, cx| { - cx.emit(ItemEvent::UpdateTab); - }); - - // Allow any potential close operation to complete - cx.run_until_parked(); - - // Verify the item remains open (dirty files are not auto-closed) - pane.read_with(cx, |pane, _| { - assert_eq!( - pane.items().count(), - 1, - "Dirty items should not be automatically closed even when file is deleted" - ); - }); - - // Verify the item is marked as deleted and still dirty - item.read_with(cx, |item, _| { - assert!( - item.has_deleted_file, - "Item should be marked as having deleted file" - ); - assert!(item.is_dirty, "Item should still be dirty"); - }); - } - - /// Tests that navigation history is cleaned up when files are auto-closed - /// due to deletion from disk. - #[gpui::test] - async fn test_close_on_disk_deletion_cleans_navigation_history(cx: &mut TestAppContext) { - init_test(cx); - - // Enable the close_on_file_delete setting - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |settings| { - settings.workspace.close_on_file_delete = Some(true); - }); - }); - - let fs = FakeFs::new(cx.background_executor.clone()); - let project = Project::test(fs, [], 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()); - - // Create test items - let item1 = cx.new(|cx| { - TestItem::new(cx) - .with_label("test1.txt") - .with_project_items(&[TestProjectItem::new(1, "test1.txt", cx)]) - }); - let item1_id = item1.item_id(); - - let item2 = cx.new(|cx| { - TestItem::new(cx) - .with_label("test2.txt") - .with_project_items(&[TestProjectItem::new(2, "test2.txt", cx)]) - }); - - // Add items to workspace - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item( - pane.clone(), - Box::new(item1.clone()), - None, - false, - false, - window, - cx, - ); - workspace.add_item( - pane.clone(), - Box::new(item2.clone()), - None, - false, - false, - window, - cx, - ); - }); - - // Activate item1 to ensure it gets navigation entries - pane.update_in(cx, |pane, window, cx| { - pane.activate_item(0, true, true, window, cx); - }); - - // Switch to item2 and back to create navigation history - pane.update_in(cx, |pane, window, cx| { - pane.activate_item(1, true, true, window, cx); - }); - cx.run_until_parked(); - - pane.update_in(cx, |pane, window, cx| { - pane.activate_item(0, true, true, window, cx); - }); - cx.run_until_parked(); - - // Simulate file deletion for item1 - item1.update(cx, |item, _| { - item.set_has_deleted_file(true); - }); - - // Emit UpdateTab event to trigger the close behavior - item1.update(cx, |_, cx| { - cx.emit(ItemEvent::UpdateTab); - }); - cx.run_until_parked(); - - // Verify item1 was closed - pane.read_with(cx, |pane, _| { - assert_eq!( - pane.items().count(), - 1, - "Should have 1 item remaining after auto-close" - ); - }); - - // Check navigation history after close - let has_item = pane.read_with(cx, |pane, cx| { - let mut has_item = false; - pane.nav_history().for_each_entry(cx, |entry, _| { - if entry.item.id() == item1_id { - has_item = true; - } - }); - has_item - }); - - assert!( - !has_item, - "Navigation history should not contain closed item entries" - ); - } - - #[gpui::test] - async fn test_no_save_prompt_when_dirty_multi_buffer_closed_with_all_of_its_dirty_items_present_in_the_pane( - cx: &mut TestAppContext, - ) { - init_test(cx); - - let fs = FakeFs::new(cx.background_executor.clone()); - let project = Project::test(fs, [], 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()); - - let dirty_regular_buffer = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_label("1.txt") - .with_project_items(&[dirty_project_item(1, "1.txt", cx)]) - }); - let dirty_regular_buffer_2 = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_label("2.txt") - .with_project_items(&[dirty_project_item(2, "2.txt", cx)]) - }); - let clear_regular_buffer = cx.new(|cx| { - TestItem::new(cx) - .with_label("3.txt") - .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)]) - }); - - let dirty_multi_buffer = cx.new(|cx| { - TestItem::new(cx) - .with_dirty(true) - .with_buffer_kind(ItemBufferKind::Multibuffer) - .with_label("Fake Project Search") - .with_project_items(&[ - dirty_regular_buffer.read(cx).project_items[0].clone(), - dirty_regular_buffer_2.read(cx).project_items[0].clone(), - clear_regular_buffer.read(cx).project_items[0].clone(), - ]) - }); - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item( - pane.clone(), - Box::new(dirty_regular_buffer.clone()), - None, - false, - false, - window, - cx, - ); - workspace.add_item( - pane.clone(), - Box::new(dirty_regular_buffer_2.clone()), - None, - false, - false, - window, - cx, - ); - workspace.add_item( - pane.clone(), - Box::new(dirty_multi_buffer.clone()), - None, - false, - false, - window, - cx, - ); - }); - - pane.update_in(cx, |pane, window, cx| { - pane.activate_item(2, true, true, window, cx); - assert_eq!( - pane.active_item().unwrap().item_id(), - dirty_multi_buffer.item_id(), - "Should select the multi buffer in the pane" - ); - }); - let close_multi_buffer_task = pane.update_in(cx, |pane, window, cx| { - pane.close_active_item( - &CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - window, - cx, - ) - }); - cx.background_executor.run_until_parked(); - assert!( - !cx.has_pending_prompt(), - "All dirty items from the multi buffer are in the pane still, no save prompts should be shown" - ); - close_multi_buffer_task - .await - .expect("Closing multi buffer failed"); - pane.update(cx, |pane, cx| { - assert_eq!(dirty_regular_buffer.read(cx).save_count, 0); - assert_eq!(dirty_multi_buffer.read(cx).save_count, 0); - assert_eq!(dirty_regular_buffer_2.read(cx).save_count, 0); - assert_eq!( - pane.items() - .map(|item| item.item_id()) - .sorted() - .collect::>(), - vec![ - dirty_regular_buffer.item_id(), - dirty_regular_buffer_2.item_id(), - ], - "Should have no multi buffer left in the pane" - ); - assert!(dirty_regular_buffer.read(cx).is_dirty); - assert!(dirty_regular_buffer_2.read(cx).is_dirty); - }); - } - - #[gpui::test] - async fn test_move_focused_panel_to_next_position(cx: &mut gpui::TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx)); - - // Add a new panel to the right dock, opening the dock and setting the - // focus to the new panel. - let panel = workspace.update_in(cx, |workspace, window, cx| { - let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 100, cx)); - workspace.add_panel(panel.clone(), window, cx); - - workspace - .right_dock() - .update(cx, |right_dock, cx| right_dock.set_open(true, window, cx)); - - workspace.toggle_panel_focus::(window, cx); - - panel - }); - - // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the - // panel to the next valid position which, in this case, is the left - // dock. - cx.dispatch_action(MoveFocusedPanelToNextPosition); - workspace.update(cx, |workspace, cx| { - assert!(workspace.left_dock().read(cx).is_open()); - assert_eq!(panel.read(cx).position, DockPosition::Left); - }); - - // Dispatch the `MoveFocusedPanelToNextPosition` action, moving the - // panel to the next valid position which, in this case, is the bottom - // dock. - cx.dispatch_action(MoveFocusedPanelToNextPosition); - workspace.update(cx, |workspace, cx| { - assert!(workspace.bottom_dock().read(cx).is_open()); - assert_eq!(panel.read(cx).position, DockPosition::Bottom); - }); - - // Dispatch the `MoveFocusedPanelToNextPosition` action again, this time - // around moving the panel to its initial position, the right dock. - cx.dispatch_action(MoveFocusedPanelToNextPosition); - workspace.update(cx, |workspace, cx| { - assert!(workspace.right_dock().read(cx).is_open()); - assert_eq!(panel.read(cx).position, DockPosition::Right); - }); - - // Remove focus from the panel, ensuring that, if the panel is not - // focused, the `MoveFocusedPanelToNextPosition` action does not update - // the panel's position, so the panel is still in the right dock. - workspace.update_in(cx, |workspace, window, cx| { - workspace.toggle_panel_focus::(window, cx); - }); - - cx.dispatch_action(MoveFocusedPanelToNextPosition); - workspace.update(cx, |workspace, cx| { - assert!(workspace.right_dock().read(cx).is_open()); - assert_eq!(panel.read(cx).position, DockPosition::Right); - }); - } - - #[gpui::test] - async fn test_moving_items_create_panes(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let item_1 = cx.new(|cx| { - TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)]) - }); - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx); - workspace.move_item_to_pane_in_direction( - &MoveItemToPaneInDirection { - direction: SplitDirection::Right, - focus: true, - clone: false, - }, - window, - cx, - ); - workspace.move_item_to_pane_at_index( - &MoveItemToPane { - destination: 3, - focus: true, - clone: false, - }, - window, - cx, - ); - - assert_eq!(workspace.panes.len(), 1, "No new panes were created"); - assert_eq!( - pane_items_paths(&workspace.active_pane, cx), - vec!["first.txt".to_string()], - "Single item was not moved anywhere" - ); - }); - - let item_2 = cx.new(|cx| { - TestItem::new(cx).with_project_items(&[TestProjectItem::new(2, "second.txt", cx)]) - }); - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane(Box::new(item_2), None, true, window, cx); - assert_eq!( - pane_items_paths(&workspace.panes[0], cx), - vec!["first.txt".to_string(), "second.txt".to_string()], - ); - workspace.move_item_to_pane_in_direction( - &MoveItemToPaneInDirection { - direction: SplitDirection::Right, - focus: true, - clone: false, - }, - window, - cx, - ); - - assert_eq!(workspace.panes.len(), 2, "A new pane should be created"); - assert_eq!( - pane_items_paths(&workspace.panes[0], cx), - vec!["first.txt".to_string()], - "After moving, one item should be left in the original pane" - ); - assert_eq!( - pane_items_paths(&workspace.panes[1], cx), - vec!["second.txt".to_string()], - "New item should have been moved to the new pane" - ); - }); - - let item_3 = cx.new(|cx| { - TestItem::new(cx).with_project_items(&[TestProjectItem::new(3, "third.txt", cx)]) - }); - workspace.update_in(cx, |workspace, window, cx| { - let original_pane = workspace.panes[0].clone(); - workspace.set_active_pane(&original_pane, window, cx); - workspace.add_item_to_active_pane(Box::new(item_3), None, true, window, cx); - assert_eq!(workspace.panes.len(), 2, "No new panes were created"); - assert_eq!( - pane_items_paths(&workspace.active_pane, cx), - vec!["first.txt".to_string(), "third.txt".to_string()], - "New pane should be ready to move one item out" - ); - - workspace.move_item_to_pane_at_index( - &MoveItemToPane { - destination: 3, - focus: true, - clone: false, - }, - window, - cx, - ); - assert_eq!(workspace.panes.len(), 3, "A new pane should be created"); - assert_eq!( - pane_items_paths(&workspace.active_pane, cx), - vec!["first.txt".to_string()], - "After moving, one item should be left in the original pane" - ); - assert_eq!( - pane_items_paths(&workspace.panes[1], cx), - vec!["second.txt".to_string()], - "Previously created pane should be unchanged" - ); - assert_eq!( - pane_items_paths(&workspace.panes[2], cx), - vec!["third.txt".to_string()], - "New item should have been moved to the new pane" - ); - }); - } - - #[gpui::test] - async fn test_moving_items_can_clone_panes(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - let (workspace, cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - let item_1 = cx.new(|cx| { - TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "first.txt", cx)]) - }); - workspace.update_in(cx, |workspace, window, cx| { - workspace.add_item_to_active_pane(Box::new(item_1), None, true, window, cx); - workspace.move_item_to_pane_in_direction( - &MoveItemToPaneInDirection { - direction: SplitDirection::Right, - focus: true, - clone: true, - }, - window, - cx, - ); - workspace.move_item_to_pane_at_index( - &MoveItemToPane { - destination: 3, - focus: true, - clone: true, - }, - window, - cx, - ); - }); - cx.run_until_parked(); - - workspace.update(cx, |workspace, cx| { - assert_eq!(workspace.panes.len(), 3, "Two new panes were created"); - for pane in workspace.panes() { - assert_eq!( - pane_items_paths(pane, cx), - vec!["first.txt".to_string()], - "Single item exists in all panes" - ); - } - }); - - // verify that the active pane has been updated after waiting for the - // pane focus event to fire and resolve - workspace.read_with(cx, |workspace, _app| { - assert_eq!( - workspace.active_pane(), - &workspace.panes[2], - "The third pane should be the active one: {:?}", - workspace.panes - ); - }) - } - - mod register_project_item_tests { - - use super::*; - - // View - struct TestPngItemView { - focus_handle: FocusHandle, - } - // Model - struct TestPngItem {} - - impl project::ProjectItem for TestPngItem { - fn try_open( - _project: &Entity, - path: &ProjectPath, - cx: &mut App, - ) -> Option>>> { - if path.path.extension().unwrap() == "png" { - Some(cx.spawn(async move |cx| cx.new(|_| TestPngItem {}))) - } else { - None - } - } - - fn entry_id(&self, _: &App) -> Option { - None - } - - fn project_path(&self, _: &App) -> Option { - None - } - - fn is_dirty(&self) -> bool { - false - } - } - - impl Item for TestPngItemView { - type Event = (); - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { - "".into() - } - } - impl EventEmitter<()> for TestPngItemView {} - impl Focusable for TestPngItemView { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } - } - - impl Render for TestPngItemView { - fn render( - &mut self, - _window: &mut Window, - _cx: &mut Context, - ) -> impl IntoElement { - Empty - } - } - - impl ProjectItem for TestPngItemView { - type Item = TestPngItem; - - fn for_project_item( - _project: Entity, - _pane: Option<&Pane>, - _item: Entity, - _: &mut Window, - cx: &mut Context, - ) -> Self - where - Self: Sized, - { - Self { - focus_handle: cx.focus_handle(), - } - } - } - - // View - struct TestIpynbItemView { - focus_handle: FocusHandle, - } - // Model - struct TestIpynbItem {} - - impl project::ProjectItem for TestIpynbItem { - fn try_open( - _project: &Entity, - path: &ProjectPath, - cx: &mut App, - ) -> Option>>> { - if path.path.extension().unwrap() == "ipynb" { - Some(cx.spawn(async move |cx| cx.new(|_| TestIpynbItem {}))) - } else { - None - } - } - - fn entry_id(&self, _: &App) -> Option { - None - } - - fn project_path(&self, _: &App) -> Option { - None - } - - fn is_dirty(&self) -> bool { - false - } - } - - impl Item for TestIpynbItemView { - type Event = (); - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { - "".into() - } - } - impl EventEmitter<()> for TestIpynbItemView {} - impl Focusable for TestIpynbItemView { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } - } - - impl Render for TestIpynbItemView { - fn render( - &mut self, - _window: &mut Window, - _cx: &mut Context, - ) -> impl IntoElement { - Empty - } - } - - impl ProjectItem for TestIpynbItemView { - type Item = TestIpynbItem; - - fn for_project_item( - _project: Entity, - _pane: Option<&Pane>, - _item: Entity, - _: &mut Window, - cx: &mut Context, - ) -> Self - where - Self: Sized, - { - Self { - focus_handle: cx.focus_handle(), - } - } - } - - struct TestAlternatePngItemView { - focus_handle: FocusHandle, - } - - impl Item for TestAlternatePngItemView { - type Event = (); - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { - "".into() - } - } - - impl EventEmitter<()> for TestAlternatePngItemView {} - impl Focusable for TestAlternatePngItemView { - fn focus_handle(&self, _cx: &App) -> FocusHandle { - self.focus_handle.clone() - } - } - - impl Render for TestAlternatePngItemView { - fn render( - &mut self, - _window: &mut Window, - _cx: &mut Context, - ) -> impl IntoElement { - Empty - } - } - - impl ProjectItem for TestAlternatePngItemView { - type Item = TestPngItem; - - fn for_project_item( - _project: Entity, - _pane: Option<&Pane>, - _item: Entity, - _: &mut Window, - cx: &mut Context, - ) -> Self - where - Self: Sized, - { - Self { - focus_handle: cx.focus_handle(), - } - } - } - - #[gpui::test] - async fn test_register_project_item(cx: &mut TestAppContext) { - init_test(cx); - - cx.update(|cx| { - register_project_item::(cx); - register_project_item::(cx); - }); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root1", - json!({ - "one.png": "BINARYDATAHERE", - "two.ipynb": "{ totally a notebook }", - "three.txt": "editing text, sure why not?" - }), - ) - .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, rel_path("one.png")); - workspace.open_path(project_path, None, true, window, cx) - }) - .await - .unwrap(); - - // Now we can check if the handle we got back errored or not - assert_eq!( - handle.to_any_view().entity_type(), - TypeId::of::() - ); - - let handle = workspace - .update_in(cx, |workspace, window, cx| { - let project_path = (worktree_id, rel_path("two.ipynb")); - workspace.open_path(project_path, None, true, window, cx) - }) - .await - .unwrap(); - - assert_eq!( - handle.to_any_view().entity_type(), - TypeId::of::() - ); - - let handle = workspace - .update_in(cx, |workspace, window, cx| { - let project_path = (worktree_id, rel_path("three.txt")); - workspace.open_path(project_path, None, true, window, cx) - }) - .await; - assert!(handle.is_err()); - } - - #[gpui::test] - async fn test_register_project_item_two_enter_one_leaves(cx: &mut TestAppContext) { - init_test(cx); - - cx.update(|cx| { - register_project_item::(cx); - register_project_item::(cx); - }); - - let fs = FakeFs::new(cx.executor()); - fs.insert_tree( - "/root1", - json!({ - "one.png": "BINARYDATAHERE", - "two.ipynb": "{ totally a notebook }", - "three.txt": "editing text, sure why not?" - }), - ) - .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, rel_path("one.png")); - workspace.open_path(project_path, None, true, window, cx) - }) - .await - .unwrap(); - - // This _must_ be the second item registered - assert_eq!( - handle.to_any_view().entity_type(), - TypeId::of::() - ); - - let handle = workspace - .update_in(cx, |workspace, window, cx| { - let project_path = (worktree_id, rel_path("three.txt")); - workspace.open_path(project_path, None, true, window, cx) - }) - .await; - assert!(handle.is_err()); - } - } - - #[gpui::test] - async fn test_status_bar_visibility(cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor()); - let project = Project::test(fs, [], cx).await; - let (workspace, _cx) = - cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - // Test with status bar shown (default) - workspace.read_with(cx, |workspace, cx| { - let visible = workspace.status_bar_visible(cx); - assert!(visible, "Status bar should be visible by default"); - }); - - // Test with status bar hidden - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |settings| { - settings.status_bar.get_or_insert_default().show = Some(false); - }); - }); - - workspace.read_with(cx, |workspace, cx| { - let visible = workspace.status_bar_visible(cx); - assert!(!visible, "Status bar should be hidden when show is false"); - }); - - // Test with status bar shown explicitly - cx.update_global(|store: &mut SettingsStore, cx| { - store.update_user_settings(cx, |settings| { - settings.status_bar.get_or_insert_default().show = Some(true); - }); - }); - - workspace.read_with(cx, |workspace, cx| { - let visible = workspace.status_bar_visible(cx); - assert!(visible, "Status bar should be visible when show is true"); - }); - } - - fn pane_items_paths(pane: &Entity, cx: &App) -> Vec { - pane.read(cx) - .items() - .flat_map(|item| { - item.project_paths(cx) - .into_iter() - .map(|path| path.path.display(PathStyle::local()).into_owned()) - }) - .collect() - } - - pub fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - theme::init(theme::LoadThemes::JustBase, cx); - }); - } - - fn dirty_project_item(id: u64, path: &str, cx: &mut App) -> Entity { - let item = TestProjectItem::new(id, path, cx); - item.update(cx, |item, _| { - item.is_dirty = true; - }); - item - } -} diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs deleted file mode 100644 index 4ce0394fe5..0000000000 --- a/crates/workspace/src/workspace_settings.rs +++ /dev/null @@ -1,143 +0,0 @@ -use std::num::NonZeroUsize; - -use crate::DockPosition; -use collections::HashMap; -use serde::Deserialize; -pub use settings::{ - AutosaveSetting, BottomDockLayout, InactiveOpacity, PaneSplitDirectionHorizontal, - PaneSplitDirectionVertical, RegisterSetting, RestoreOnStartupBehavior, Settings, -}; - -#[derive(RegisterSetting)] -pub struct WorkspaceSettings { - pub active_pane_modifiers: ActivePanelModifiers, - pub bottom_dock_layout: settings::BottomDockLayout, - pub pane_split_direction_horizontal: settings::PaneSplitDirectionHorizontal, - pub pane_split_direction_vertical: settings::PaneSplitDirectionVertical, - pub centered_layout: settings::CenteredLayoutSettings, - pub confirm_quit: bool, - pub show_call_status_icon: bool, - pub autosave: AutosaveSetting, - pub restore_on_startup: settings::RestoreOnStartupBehavior, - pub restore_on_file_reopen: bool, - pub drop_target_size: f32, - pub use_system_path_prompts: bool, - pub use_system_prompts: bool, - pub command_aliases: HashMap, - pub max_tabs: Option, - pub when_closing_with_no_tabs: settings::CloseWindowWhenNoItems, - pub on_last_window_closed: settings::OnLastWindowClosed, - pub resize_all_panels_in_dock: Vec, - pub close_on_file_delete: bool, - pub use_system_window_tabs: bool, - pub zoomed_padding: bool, - pub window_decorations: settings::WindowDecorations, -} - -#[derive(Copy, Clone, PartialEq, Debug, Default)] -pub struct ActivePanelModifiers { - /// Size of the border surrounding the active pane. - /// When set to 0, the active pane doesn't have any border. - /// The border is drawn inset. - /// - /// Default: `0.0` - // TODO: make this not an option, it is never None - pub border_size: Option, - /// Opacity of inactive panels. - /// When set to 1.0, the inactive panes have the same opacity as the active one. - /// If set to 0, the inactive panes content will not be visible at all. - /// Values are clamped to the [0.0, 1.0] range. - /// - /// Default: `1.0` - // TODO: make this not an option, it is never None - pub inactive_opacity: Option, -} - -#[derive(Deserialize, RegisterSetting)] -pub struct TabBarSettings { - pub show: bool, - pub show_nav_history_buttons: bool, - pub show_tab_bar_buttons: bool, -} - -impl Settings for WorkspaceSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - let workspace = &content.workspace; - Self { - active_pane_modifiers: ActivePanelModifiers { - border_size: Some( - workspace - .active_pane_modifiers - .unwrap() - .border_size - .unwrap(), - ), - inactive_opacity: Some( - workspace - .active_pane_modifiers - .unwrap() - .inactive_opacity - .unwrap(), - ), - }, - bottom_dock_layout: workspace.bottom_dock_layout.unwrap(), - pane_split_direction_horizontal: workspace.pane_split_direction_horizontal.unwrap(), - pane_split_direction_vertical: workspace.pane_split_direction_vertical.unwrap(), - centered_layout: workspace.centered_layout.unwrap(), - confirm_quit: workspace.confirm_quit.unwrap(), - show_call_status_icon: workspace.show_call_status_icon.unwrap(), - autosave: workspace.autosave.unwrap(), - restore_on_startup: workspace.restore_on_startup.unwrap(), - restore_on_file_reopen: workspace.restore_on_file_reopen.unwrap(), - drop_target_size: workspace.drop_target_size.unwrap(), - use_system_path_prompts: workspace.use_system_path_prompts.unwrap(), - use_system_prompts: workspace.use_system_prompts.unwrap(), - command_aliases: workspace.command_aliases.clone(), - max_tabs: workspace.max_tabs, - when_closing_with_no_tabs: workspace.when_closing_with_no_tabs.unwrap(), - on_last_window_closed: workspace.on_last_window_closed.unwrap(), - resize_all_panels_in_dock: workspace - .resize_all_panels_in_dock - .clone() - .unwrap() - .into_iter() - .map(Into::into) - .collect(), - close_on_file_delete: workspace.close_on_file_delete.unwrap(), - use_system_window_tabs: workspace.use_system_window_tabs.unwrap(), - zoomed_padding: workspace.zoomed_padding.unwrap(), - window_decorations: workspace.window_decorations.unwrap(), - } - } -} - -impl Settings for TabBarSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - let tab_bar = content.tab_bar.clone().unwrap(); - TabBarSettings { - show: tab_bar.show.unwrap(), - show_nav_history_buttons: tab_bar.show_nav_history_buttons.unwrap(), - show_tab_bar_buttons: tab_bar.show_tab_bar_buttons.unwrap(), - } - } -} - -#[derive(Deserialize, RegisterSetting)] -pub struct StatusBarSettings { - pub show: bool, - pub active_language_button: bool, - pub cursor_position_button: bool, - pub line_endings_button: bool, -} - -impl Settings for StatusBarSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - let status_bar = content.status_bar.clone().unwrap(); - StatusBarSettings { - show: status_bar.show.unwrap(), - active_language_button: status_bar.active_language_button.unwrap(), - cursor_position_button: status_bar.cursor_position_button.unwrap(), - line_endings_button: status_bar.line_endings_button.unwrap(), - } - } -} diff --git a/crates/worktree/Cargo.toml b/crates/worktree/Cargo.toml deleted file mode 100644 index 6d132fbd2c..0000000000 --- a/crates/worktree/Cargo.toml +++ /dev/null @@ -1,63 +0,0 @@ -[package] -name = "worktree" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lib] -path = "src/worktree.rs" -doctest = false - -[lints] -workspace = true - -[features] -test-support = [ - "gpui/test-support", - "http_client/test-support", - "language/test-support", - "settings/test-support", - "text/test-support", - "util/test-support", -] - -[dependencies] -anyhow.workspace = true -async-lock.workspace = true -clock.workspace = true -collections.workspace = true -fs.workspace = true -futures.workspace = true -fuzzy.workspace = true -git.workspace = true -gpui.workspace = true -ignore.workspace = true -language.workspace = true -log.workspace = true -parking_lot.workspace = true -paths.workspace = true -postage.workspace = true -rpc = { workspace = true, features = ["gpui"] } -serde.workspace = true -serde_json.workspace = true -settings.workspace = true -smallvec.workspace = true -smol.workspace = true -sum_tree.workspace = true -text.workspace = true -util.workspace = true - -[dev-dependencies] -clock = { workspace = true, features = ["test-support"] } -collections = { workspace = true, features = ["test-support"] } -git2.workspace = true -gpui = { workspace = true, features = ["test-support"] } -http_client.workspace = true -paths = { workspace = true, features = ["test-support"] } -pretty_assertions.workspace = true -rand.workspace = true -rpc = { workspace = true, features = ["test-support"] } -settings = { workspace = true, features = ["test-support"] } -util = { workspace = true, features = ["test-support"] } -zlog.workspace = true diff --git a/crates/worktree/LICENSE-GPL b/crates/worktree/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/worktree/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/worktree/src/ignore.rs b/crates/worktree/src/ignore.rs deleted file mode 100644 index 17c362e2d7..0000000000 --- a/crates/worktree/src/ignore.rs +++ /dev/null @@ -1,102 +0,0 @@ -use ignore::gitignore::Gitignore; -use std::{ffi::OsStr, path::Path, sync::Arc}; - -#[derive(Clone, Debug)] -pub struct IgnoreStack { - pub repo_root: Option>, - pub top: Arc, -} - -#[derive(Debug)] -pub enum IgnoreStackEntry { - None, - Global { - ignore: Arc, - }, - Some { - abs_base_path: Arc, - ignore: Arc, - parent: Arc, - }, - All, -} - -impl IgnoreStack { - pub fn none() -> Self { - Self { - repo_root: None, - top: Arc::new(IgnoreStackEntry::None), - } - } - - pub fn all() -> Self { - Self { - repo_root: None, - top: Arc::new(IgnoreStackEntry::All), - } - } - - pub fn global(ignore: Arc) -> Self { - Self { - repo_root: None, - top: Arc::new(IgnoreStackEntry::Global { ignore }), - } - } - - pub fn append(self, abs_base_path: Arc, ignore: Arc) -> Self { - let top = match self.top.as_ref() { - IgnoreStackEntry::All => self.top.clone(), - _ => Arc::new(IgnoreStackEntry::Some { - abs_base_path, - ignore, - parent: self.top.clone(), - }), - }; - Self { - repo_root: self.repo_root, - top, - } - } - - pub fn is_abs_path_ignored(&self, abs_path: &Path, is_dir: bool) -> bool { - if is_dir && abs_path.file_name() == Some(OsStr::new(".git")) { - return true; - } - - match self.top.as_ref() { - IgnoreStackEntry::None => false, - IgnoreStackEntry::All => true, - IgnoreStackEntry::Global { ignore } => { - let combined_path; - let abs_path = if let Some(repo_root) = self.repo_root.as_ref() { - combined_path = ignore.path().join( - abs_path - .strip_prefix(repo_root) - .expect("repo root should be a parent of matched path"), - ); - &combined_path - } else { - abs_path - }; - match ignore.matched(abs_path, is_dir) { - ignore::Match::None => false, - ignore::Match::Ignore(_) => true, - ignore::Match::Whitelist(_) => false, - } - } - IgnoreStackEntry::Some { - abs_base_path, - ignore, - parent: prev, - } => match ignore.matched(abs_path.strip_prefix(abs_base_path).unwrap(), is_dir) { - ignore::Match::None => IgnoreStack { - repo_root: self.repo_root.clone(), - top: prev.clone(), - } - .is_abs_path_ignored(abs_path, is_dir), - ignore::Match::Ignore(_) => true, - ignore::Match::Whitelist(_) => false, - }, - } - } -} diff --git a/crates/worktree/src/worktree.rs b/crates/worktree/src/worktree.rs deleted file mode 100644 index e1ce31c038..0000000000 --- a/crates/worktree/src/worktree.rs +++ /dev/null @@ -1,5677 +0,0 @@ -mod ignore; -mod worktree_settings; -#[cfg(test)] -mod worktree_tests; - -use ::ignore::gitignore::{Gitignore, GitignoreBuilder}; -use anyhow::{Context as _, Result, anyhow}; -use clock::ReplicaId; -use collections::{HashMap, HashSet, VecDeque}; -use fs::{Fs, MTime, PathEvent, RemoveOptions, Watcher, copy_recursive, read_dir_items}; -use futures::{ - FutureExt as _, Stream, StreamExt, - channel::{ - mpsc::{self, UnboundedSender}, - oneshot, - }, - select_biased, stream, - task::Poll, -}; -use fuzzy::CharBag; -use git::{ - COMMIT_MESSAGE, DOT_GIT, FSMONITOR_DAEMON, GITIGNORE, INDEX_LOCK, LFS_DIR, status::GitSummary, -}; -use gpui::{ - App, AppContext as _, AsyncApp, BackgroundExecutor, Context, Entity, EventEmitter, Priority, - Task, -}; -use ignore::IgnoreStack; -use language::DiskState; - -use parking_lot::Mutex; -use paths::{local_settings_folder_name, local_vscode_folder_name}; -use postage::{ - barrier, - prelude::{Sink as _, Stream as _}, - watch, -}; -use rpc::{ - AnyProtoClient, - proto::{self, split_worktree_update}, -}; -pub use settings::WorktreeId; -use settings::{Settings, SettingsLocation, SettingsStore}; -use smallvec::{SmallVec, smallvec}; -use smol::channel::{self, Sender}; -use std::{ - any::Any, - borrow::Borrow as _, - cmp::Ordering, - collections::hash_map, - convert::TryFrom, - ffi::OsStr, - fmt, - future::Future, - mem::{self}, - ops::{Deref, DerefMut, Range}, - path::{Path, PathBuf}, - pin::Pin, - sync::{ - Arc, - atomic::{AtomicUsize, Ordering::SeqCst}, - }, - time::{Duration, Instant}, -}; -use sum_tree::{Bias, Dimensions, Edit, KeyedItem, SeekTarget, SumTree, Summary, TreeMap, TreeSet}; -use text::{LineEnding, Rope}; -use util::{ - ResultExt, debug_panic, maybe, - paths::{PathMatcher, PathStyle, SanitizedPath, home_dir}, - rel_path::RelPath, -}; -pub use worktree_settings::WorktreeSettings; - -pub const FS_WATCH_LATENCY: Duration = Duration::from_millis(100); - -/// A set of local or remote files that are being opened as part of a project. -/// Responsible for tracking related FS (for local)/collab (for remote) events and corresponding updates. -/// Stores git repositories data and the diagnostics for the file(s). -/// -/// Has an absolute path, and may be set to be visible in Zed UI or not. -/// May correspond to a directory or a single file. -/// Possible examples: -/// * a drag and dropped file — may be added as an invisible, "ephemeral" entry to the current worktree -/// * a directory opened in Zed — may be added as a visible entry to the current worktree -/// -/// Uses [`Entry`] to track the state of each file/directory, can look up absolute paths for entries. -pub enum Worktree { - Local(LocalWorktree), - Remote(RemoteWorktree), -} - -/// An entry, created in the worktree. -#[derive(Debug)] -pub enum CreatedEntry { - /// Got created and indexed by the worktree, receiving a corresponding entry. - Included(Entry), - /// Got created, but not indexed due to falling under exclusion filters. - Excluded { abs_path: PathBuf }, -} - -#[derive(Debug)] -pub struct LoadedFile { - pub file: Arc, - pub text: String, -} - -pub struct LoadedBinaryFile { - pub file: Arc, - pub content: Vec, -} - -impl fmt::Debug for LoadedBinaryFile { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("LoadedBinaryFile") - .field("file", &self.file) - .field("content_bytes", &self.content.len()) - .finish() - } -} - -pub struct LocalWorktree { - snapshot: LocalSnapshot, - scan_requests_tx: channel::Sender, - path_prefixes_to_scan_tx: channel::Sender, - is_scanning: (watch::Sender, watch::Receiver), - _background_scanner_tasks: Vec>, - update_observer: Option, - fs: Arc, - fs_case_sensitive: bool, - visible: bool, - next_entry_id: Arc, - settings: WorktreeSettings, - share_private_files: bool, - scanning_enabled: bool, -} - -pub struct PathPrefixScanRequest { - path: Arc, - done: SmallVec<[barrier::Sender; 1]>, -} - -struct ScanRequest { - relative_paths: Vec>, - done: SmallVec<[barrier::Sender; 1]>, -} - -pub struct RemoteWorktree { - snapshot: Snapshot, - background_snapshot: Arc)>>, - project_id: u64, - client: AnyProtoClient, - file_scan_inclusions: PathMatcher, - updates_tx: Option>, - update_observer: Option>, - snapshot_subscriptions: VecDeque<(usize, oneshot::Sender<()>)>, - replica_id: ReplicaId, - visible: bool, - disconnected: bool, -} - -#[derive(Clone)] -pub struct Snapshot { - id: WorktreeId, - /// The absolute path of the worktree root. - abs_path: Arc, - path_style: PathStyle, - root_name: Arc, - root_char_bag: CharBag, - entries_by_path: SumTree, - entries_by_id: SumTree, - always_included_entries: Vec>, - - /// A number that increases every time the worktree begins scanning - /// a set of paths from the filesystem. This scanning could be caused - /// by some operation performed on the worktree, such as reading or - /// writing a file, or by an event reported by the filesystem. - scan_id: usize, - - /// The latest scan id that has completed, and whose preceding scans - /// have all completed. The current `scan_id` could be more than one - /// greater than the `completed_scan_id` if operations are performed - /// on the worktree while it is processing a file-system event. - completed_scan_id: usize, -} - -/// This path corresponds to the 'content path' of a repository in relation -/// to Zed's project root. -/// In the majority of the cases, this is the folder that contains the .git folder. -/// But if a sub-folder of a git repository is opened, this corresponds to the -/// project root and the .git folder is located in a parent directory. -#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)] -pub enum WorkDirectory { - InProject { - relative_path: Arc, - }, - AboveProject { - absolute_path: Arc, - location_in_repo: Arc, - }, -} - -impl WorkDirectory { - fn path_key(&self) -> PathKey { - match self { - WorkDirectory::InProject { relative_path } => PathKey(relative_path.clone()), - WorkDirectory::AboveProject { .. } => PathKey(RelPath::empty().into()), - } - } - - /// Returns true if the given path is a child of the work directory. - /// - /// Note that the path may not be a member of this repository, if there - /// is a repository in a directory between these two paths - /// external .git folder in a parent folder of the project root. - #[track_caller] - pub fn directory_contains(&self, path: &RelPath) -> bool { - match self { - WorkDirectory::InProject { relative_path } => path.starts_with(relative_path), - WorkDirectory::AboveProject { .. } => true, - } - } -} - -impl Default for WorkDirectory { - fn default() -> Self { - Self::InProject { - relative_path: Arc::from(RelPath::empty()), - } - } -} - -#[derive(Clone)] -pub struct LocalSnapshot { - snapshot: Snapshot, - global_gitignore: Option>, - /// All of the gitignore files in the worktree, indexed by their absolute path. - /// The boolean indicates whether the gitignore needs to be updated. - ignores_by_parent_abs_path: HashMap, (Arc, bool)>, - /// All of the git repositories in the worktree, indexed by the project entry - /// id of their parent directory. - git_repositories: TreeMap, - /// The file handle of the worktree root - /// (so we can find it after it's been moved) - root_file_handle: Option>, - executor: BackgroundExecutor, -} - -struct BackgroundScannerState { - snapshot: LocalSnapshot, - scanned_dirs: HashSet, - path_prefixes_to_scan: HashSet>, - paths_to_scan: HashSet>, - /// The ids of all of the entries that were removed from the snapshot - /// as part of the current update. These entry ids may be re-used - /// if the same inode is discovered at a new path, or if the given - /// path is re-created after being deleted. - removed_entries: HashMap, - changed_paths: Vec>, - prev_snapshot: Snapshot, -} - -#[derive(Debug, Clone)] -struct LocalRepositoryEntry { - work_directory_id: ProjectEntryId, - work_directory: WorkDirectory, - work_directory_abs_path: Arc, - git_dir_scan_id: usize, - /// Absolute path to the original .git entry that caused us to create this repository. - /// - /// This is normally a directory, but may be a "gitfile" that points to a directory elsewhere - /// (whose path we then store in `repository_dir_abs_path`). - dot_git_abs_path: Arc, - /// Absolute path to the "commondir" for this repository. - /// - /// This is always a directory. For a normal repository, this is the same as dot_git_abs_path, - /// but in the case of a submodule or a worktree it is the path to the "parent" .git directory - /// from which the submodule/worktree was derived. - common_dir_abs_path: Arc, - /// Absolute path to the directory holding the repository's state. - /// - /// For a normal repository, this is a directory and coincides with `dot_git_abs_path` and - /// `common_dir_abs_path`. For a submodule or worktree, this is some subdirectory of the - /// commondir like `/project/.git/modules/foo`. - repository_dir_abs_path: Arc, -} - -impl sum_tree::Item for LocalRepositoryEntry { - type Summary = PathSummary; - - fn summary(&self, _: ::Context<'_>) -> Self::Summary { - PathSummary { - max_path: self.work_directory.path_key().0, - item_summary: sum_tree::NoSummary, - } - } -} - -impl KeyedItem for LocalRepositoryEntry { - type Key = PathKey; - - fn key(&self) -> Self::Key { - self.work_directory.path_key() - } -} - -impl Deref for LocalRepositoryEntry { - type Target = WorkDirectory; - - fn deref(&self) -> &Self::Target { - &self.work_directory - } -} - -impl Deref for LocalSnapshot { - type Target = Snapshot; - - fn deref(&self) -> &Self::Target { - &self.snapshot - } -} - -impl DerefMut for LocalSnapshot { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.snapshot - } -} - -enum ScanState { - Started, - Updated { - snapshot: LocalSnapshot, - changes: UpdatedEntriesSet, - barrier: SmallVec<[barrier::Sender; 1]>, - scanning: bool, - }, - RootUpdated { - new_path: Arc, - }, -} - -struct UpdateObservationState { - snapshots_tx: mpsc::UnboundedSender<(LocalSnapshot, UpdatedEntriesSet)>, - resume_updates: watch::Sender<()>, - _maintain_remote_snapshot: Task>, -} - -#[derive(Clone)] -pub enum Event { - UpdatedEntries(UpdatedEntriesSet), - UpdatedGitRepositories(UpdatedGitRepositoriesSet), - DeletedEntry(ProjectEntryId), -} - -impl EventEmitter for Worktree {} - -impl Worktree { - pub async fn local( - path: impl Into>, - visible: bool, - fs: Arc, - next_entry_id: Arc, - scanning_enabled: bool, - cx: &mut AsyncApp, - ) -> Result> { - let abs_path = path.into(); - let metadata = fs - .metadata(&abs_path) - .await - .context("failed to stat worktree path")?; - - let fs_case_sensitive = fs.is_case_sensitive().await.unwrap_or_else(|e| { - log::error!( - "Failed to determine whether filesystem is case sensitive (falling back to true) due to error: {e:#}" - ); - true - }); - - let root_file_handle = if metadata.as_ref().is_some() { - fs.open_handle(&abs_path) - .await - .with_context(|| { - format!( - "failed to open local worktree root at {}", - abs_path.display() - ) - }) - .log_err() - } else { - None - }; - - cx.new(move |cx: &mut Context| { - let mut snapshot = LocalSnapshot { - ignores_by_parent_abs_path: Default::default(), - global_gitignore: Default::default(), - git_repositories: Default::default(), - snapshot: Snapshot::new( - cx.entity_id().as_u64(), - abs_path - .file_name() - .and_then(|f| f.to_str()) - .map_or(RelPath::empty().into(), |f| { - RelPath::unix(f).unwrap().into() - }), - abs_path.clone(), - PathStyle::local(), - ), - root_file_handle, - executor: cx.background_executor().clone(), - }; - - let worktree_id = snapshot.id(); - let settings_location = Some(SettingsLocation { - worktree_id, - path: RelPath::empty(), - }); - - let settings = WorktreeSettings::get(settings_location, cx).clone(); - cx.observe_global::(move |this, cx| { - if let Self::Local(this) = this { - let settings = WorktreeSettings::get(settings_location, cx).clone(); - if this.settings != settings { - this.settings = settings; - this.restart_background_scanners(cx); - } - } - }) - .detach(); - - let share_private_files = false; - if let Some(metadata) = metadata { - let mut entry = Entry::new( - RelPath::empty().into(), - &metadata, - ProjectEntryId::new(&next_entry_id), - snapshot.root_char_bag, - None, - ); - if !metadata.is_dir { - if let Some(file_name) = abs_path.file_name() - && let Some(file_name) = file_name.to_str() - && let Ok(path) = RelPath::unix(file_name) - { - entry.is_private = !share_private_files && settings.is_path_private(path); - entry.is_hidden = settings.is_path_hidden(path); - } - } - snapshot.insert_entry(entry, fs.as_ref()); - } - - let (scan_requests_tx, scan_requests_rx) = channel::unbounded(); - let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded(); - let mut worktree = LocalWorktree { - share_private_files, - next_entry_id, - snapshot, - is_scanning: watch::channel_with(true), - update_observer: None, - scan_requests_tx, - path_prefixes_to_scan_tx, - _background_scanner_tasks: Vec::new(), - fs, - fs_case_sensitive, - visible, - settings, - scanning_enabled, - }; - worktree.start_background_scanner(scan_requests_rx, path_prefixes_to_scan_rx, cx); - Worktree::Local(worktree) - }) - } - - pub fn remote( - project_id: u64, - replica_id: ReplicaId, - worktree: proto::WorktreeMetadata, - client: AnyProtoClient, - path_style: PathStyle, - cx: &mut App, - ) -> Entity { - cx.new(|cx: &mut Context| { - let snapshot = Snapshot::new( - worktree.id, - RelPath::from_proto(&worktree.root_name) - .unwrap_or_else(|_| RelPath::empty().into()), - Path::new(&worktree.abs_path).into(), - path_style, - ); - - let background_snapshot = Arc::new(Mutex::new(( - snapshot.clone(), - Vec::::new(), - ))); - let (background_updates_tx, mut background_updates_rx) = - mpsc::unbounded::(); - let (mut snapshot_updated_tx, mut snapshot_updated_rx) = watch::channel(); - - let worktree_id = snapshot.id(); - let settings_location = Some(SettingsLocation { - worktree_id, - path: RelPath::empty(), - }); - - let settings = WorktreeSettings::get(settings_location, cx).clone(); - let worktree = RemoteWorktree { - client, - project_id, - replica_id, - snapshot, - file_scan_inclusions: settings.parent_dir_scan_inclusions.clone(), - background_snapshot: background_snapshot.clone(), - updates_tx: Some(background_updates_tx), - update_observer: None, - snapshot_subscriptions: Default::default(), - visible: worktree.visible, - disconnected: false, - }; - - // Apply updates to a separate snapshot in a background task, then - // send them to a foreground task which updates the model. - cx.background_spawn(async move { - while let Some(update) = background_updates_rx.next().await { - { - let mut lock = background_snapshot.lock(); - lock.0.apply_remote_update( - update.clone(), - &settings.parent_dir_scan_inclusions, - ); - lock.1.push(update); - } - snapshot_updated_tx.send(()).await.ok(); - } - }) - .detach(); - - // On the foreground task, update to the latest snapshot and notify - // any update observer of all updates that led to that snapshot. - cx.spawn(async move |this, cx| { - while (snapshot_updated_rx.recv().await).is_some() { - this.update(cx, |this, cx| { - let mut entries_changed = false; - let this = this.as_remote_mut().unwrap(); - { - let mut lock = this.background_snapshot.lock(); - this.snapshot = lock.0.clone(); - for update in lock.1.drain(..) { - entries_changed |= !update.updated_entries.is_empty() - || !update.removed_entries.is_empty(); - if let Some(tx) = &this.update_observer { - tx.unbounded_send(update).ok(); - } - } - }; - - if entries_changed { - cx.emit(Event::UpdatedEntries(Arc::default())); - } - cx.notify(); - while let Some((scan_id, _)) = this.snapshot_subscriptions.front() { - if this.observed_snapshot(*scan_id) { - let (_, tx) = this.snapshot_subscriptions.pop_front().unwrap(); - let _ = tx.send(()); - } else { - break; - } - } - })?; - } - anyhow::Ok(()) - }) - .detach(); - - Worktree::Remote(worktree) - }) - } - - pub fn as_local(&self) -> Option<&LocalWorktree> { - if let Worktree::Local(worktree) = self { - Some(worktree) - } else { - None - } - } - - pub fn as_remote(&self) -> Option<&RemoteWorktree> { - if let Worktree::Remote(worktree) = self { - Some(worktree) - } else { - None - } - } - - pub fn as_local_mut(&mut self) -> Option<&mut LocalWorktree> { - if let Worktree::Local(worktree) = self { - Some(worktree) - } else { - None - } - } - - pub fn as_remote_mut(&mut self) -> Option<&mut RemoteWorktree> { - if let Worktree::Remote(worktree) = self { - Some(worktree) - } else { - None - } - } - - pub fn is_local(&self) -> bool { - matches!(self, Worktree::Local(_)) - } - - pub fn is_remote(&self) -> bool { - !self.is_local() - } - - pub fn settings_location(&self, _: &Context) -> SettingsLocation<'static> { - SettingsLocation { - worktree_id: self.id(), - path: RelPath::empty(), - } - } - - pub fn snapshot(&self) -> Snapshot { - match self { - Worktree::Local(worktree) => worktree.snapshot.snapshot.clone(), - Worktree::Remote(worktree) => worktree.snapshot.clone(), - } - } - - pub fn scan_id(&self) -> usize { - match self { - Worktree::Local(worktree) => worktree.snapshot.scan_id, - Worktree::Remote(worktree) => worktree.snapshot.scan_id, - } - } - - pub fn metadata_proto(&self) -> proto::WorktreeMetadata { - proto::WorktreeMetadata { - id: self.id().to_proto(), - root_name: self.root_name().to_proto(), - visible: self.is_visible(), - abs_path: self.abs_path().to_string_lossy().into_owned(), - } - } - - pub fn completed_scan_id(&self) -> usize { - match self { - Worktree::Local(worktree) => worktree.snapshot.completed_scan_id, - Worktree::Remote(worktree) => worktree.snapshot.completed_scan_id, - } - } - - pub fn is_visible(&self) -> bool { - match self { - Worktree::Local(worktree) => worktree.visible, - Worktree::Remote(worktree) => worktree.visible, - } - } - - pub fn replica_id(&self) -> ReplicaId { - match self { - Worktree::Local(_) => ReplicaId::LOCAL, - Worktree::Remote(worktree) => worktree.replica_id, - } - } - - pub fn abs_path(&self) -> Arc { - match self { - Worktree::Local(worktree) => SanitizedPath::cast_arc(worktree.abs_path.clone()), - Worktree::Remote(worktree) => SanitizedPath::cast_arc(worktree.abs_path.clone()), - } - } - - pub fn root_file(&self, cx: &Context) -> Option> { - let entry = self.root_entry()?; - Some(File::for_entry(entry.clone(), cx.entity())) - } - - pub fn observe_updates(&mut self, project_id: u64, cx: &Context, callback: F) - where - F: 'static + Send + Fn(proto::UpdateWorktree) -> Fut, - Fut: 'static + Send + Future, - { - match self { - Worktree::Local(this) => this.observe_updates(project_id, cx, callback), - Worktree::Remote(this) => this.observe_updates(project_id, cx, callback), - } - } - - pub fn stop_observing_updates(&mut self) { - match self { - Worktree::Local(this) => { - this.update_observer.take(); - } - Worktree::Remote(this) => { - this.update_observer.take(); - } - } - } - - #[cfg(any(test, feature = "test-support"))] - pub fn has_update_observer(&self) -> bool { - match self { - Worktree::Local(this) => this.update_observer.is_some(), - Worktree::Remote(this) => this.update_observer.is_some(), - } - } - - pub fn load_file(&self, path: &RelPath, cx: &Context) -> Task> { - match self { - Worktree::Local(this) => this.load_file(path, cx), - Worktree::Remote(_) => { - Task::ready(Err(anyhow!("remote worktrees can't yet load files"))) - } - } - } - - pub fn load_binary_file( - &self, - path: &RelPath, - cx: &Context, - ) -> Task> { - match self { - Worktree::Local(this) => this.load_binary_file(path, cx), - Worktree::Remote(_) => { - Task::ready(Err(anyhow!("remote worktrees can't yet load binary files"))) - } - } - } - - pub fn write_file( - &self, - path: Arc, - text: Rope, - line_ending: LineEnding, - cx: &Context, - ) -> Task>> { - match self { - Worktree::Local(this) => this.write_file(path, text, line_ending, cx), - Worktree::Remote(_) => { - Task::ready(Err(anyhow!("remote worktree can't yet write files"))) - } - } - } - - pub fn create_entry( - &mut self, - path: Arc, - is_directory: bool, - content: Option>, - cx: &Context, - ) -> Task> { - let worktree_id = self.id(); - match self { - Worktree::Local(this) => this.create_entry(path, is_directory, content, cx), - Worktree::Remote(this) => { - let project_id = this.project_id; - let request = this.client.request(proto::CreateProjectEntry { - worktree_id: worktree_id.to_proto(), - project_id, - path: path.as_ref().to_proto(), - content, - is_directory, - }); - cx.spawn(async move |this, cx| { - let response = request.await?; - match response.entry { - Some(entry) => this - .update(cx, |worktree, cx| { - worktree.as_remote_mut().unwrap().insert_entry( - entry, - response.worktree_scan_id as usize, - cx, - ) - })? - .await - .map(CreatedEntry::Included), - None => { - let abs_path = - this.read_with(cx, |worktree, _| worktree.absolutize(&path))?; - Ok(CreatedEntry::Excluded { abs_path }) - } - } - }) - } - } - } - - pub fn delete_entry( - &mut self, - entry_id: ProjectEntryId, - trash: bool, - cx: &mut Context, - ) -> Option>> { - let task = match self { - Worktree::Local(this) => this.delete_entry(entry_id, trash, cx), - Worktree::Remote(this) => this.delete_entry(entry_id, trash, cx), - }?; - - let entry = match &*self { - Worktree::Local(this) => this.entry_for_id(entry_id), - Worktree::Remote(this) => this.entry_for_id(entry_id), - }?; - - let mut ids = vec![entry_id]; - let path = &*entry.path; - - self.get_children_ids_recursive(path, &mut ids); - - for id in ids { - cx.emit(Event::DeletedEntry(id)); - } - Some(task) - } - - fn get_children_ids_recursive(&self, path: &RelPath, ids: &mut Vec) { - let children_iter = self.child_entries(path); - for child in children_iter { - ids.push(child.id); - self.get_children_ids_recursive(&child.path, ids); - } - } - - // pub fn rename_entry( - // &mut self, - // entry_id: ProjectEntryId, - // new_path: Arc, - // cx: &Context, - // ) -> Task> { - // match self { - // Worktree::Local(this) => this.rename_entry(entry_id, new_path, cx), - // Worktree::Remote(this) => this.rename_entry(entry_id, new_path, cx), - // } - // } - - pub fn copy_external_entries( - &mut self, - target_directory: Arc, - paths: Vec>, - fs: Arc, - cx: &Context, - ) -> Task>> { - match self { - Worktree::Local(this) => this.copy_external_entries(target_directory, paths, cx), - Worktree::Remote(this) => this.copy_external_entries(target_directory, paths, fs, cx), - } - } - - pub fn expand_entry( - &mut self, - entry_id: ProjectEntryId, - cx: &Context, - ) -> Option>> { - match self { - Worktree::Local(this) => this.expand_entry(entry_id, cx), - Worktree::Remote(this) => { - let response = this.client.request(proto::ExpandProjectEntry { - project_id: this.project_id, - entry_id: entry_id.to_proto(), - }); - Some(cx.spawn(async move |this, cx| { - let response = response.await?; - this.update(cx, |this, _| { - this.as_remote_mut() - .unwrap() - .wait_for_snapshot(response.worktree_scan_id as usize) - })? - .await?; - Ok(()) - })) - } - } - } - - pub fn expand_all_for_entry( - &mut self, - entry_id: ProjectEntryId, - cx: &Context, - ) -> Option>> { - match self { - Worktree::Local(this) => this.expand_all_for_entry(entry_id, cx), - Worktree::Remote(this) => { - let response = this.client.request(proto::ExpandAllForProjectEntry { - project_id: this.project_id, - entry_id: entry_id.to_proto(), - }); - Some(cx.spawn(async move |this, cx| { - let response = response.await?; - this.update(cx, |this, _| { - this.as_remote_mut() - .unwrap() - .wait_for_snapshot(response.worktree_scan_id as usize) - })? - .await?; - Ok(()) - })) - } - } - } - - pub async fn handle_create_entry( - this: Entity, - request: proto::CreateProjectEntry, - mut cx: AsyncApp, - ) -> Result { - let (scan_id, entry) = this.update(&mut cx, |this, cx| { - anyhow::Ok(( - this.scan_id(), - this.create_entry( - RelPath::from_proto(&request.path).with_context(|| { - format!("received invalid relative path {:?}", request.path) - })?, - request.is_directory, - request.content, - cx, - ), - )) - })??; - Ok(proto::ProjectEntryResponse { - entry: match &entry.await? { - CreatedEntry::Included(entry) => Some(entry.into()), - CreatedEntry::Excluded { .. } => None, - }, - worktree_scan_id: scan_id as u64, - }) - } - - pub async fn handle_delete_entry( - this: Entity, - request: proto::DeleteProjectEntry, - mut cx: AsyncApp, - ) -> Result { - let (scan_id, task) = this.update(&mut cx, |this, cx| { - ( - this.scan_id(), - this.delete_entry( - ProjectEntryId::from_proto(request.entry_id), - request.use_trash, - cx, - ), - ) - })?; - task.context("invalid entry")?.await?; - Ok(proto::ProjectEntryResponse { - entry: None, - worktree_scan_id: scan_id as u64, - }) - } - - pub async fn handle_expand_entry( - this: Entity, - request: proto::ExpandProjectEntry, - mut cx: AsyncApp, - ) -> Result { - let task = this.update(&mut cx, |this, cx| { - this.expand_entry(ProjectEntryId::from_proto(request.entry_id), cx) - })?; - task.context("no such entry")?.await?; - let scan_id = this.read_with(&cx, |this, _| this.scan_id())?; - Ok(proto::ExpandProjectEntryResponse { - worktree_scan_id: scan_id as u64, - }) - } - - pub async fn handle_expand_all_for_entry( - this: Entity, - request: proto::ExpandAllForProjectEntry, - mut cx: AsyncApp, - ) -> Result { - let task = this.update(&mut cx, |this, cx| { - this.expand_all_for_entry(ProjectEntryId::from_proto(request.entry_id), cx) - })?; - task.context("no such entry")?.await?; - let scan_id = this.read_with(&cx, |this, _| this.scan_id())?; - Ok(proto::ExpandAllForProjectEntryResponse { - worktree_scan_id: scan_id as u64, - }) - } - - pub fn is_single_file(&self) -> bool { - self.root_dir().is_none() - } - - /// For visible worktrees, returns the path with the worktree name as the first component. - /// Otherwise, returns an absolute path. - pub fn full_path(&self, worktree_relative_path: &RelPath) -> PathBuf { - if self.is_visible() { - self.root_name() - .join(worktree_relative_path) - .display(self.path_style) - .to_string() - .into() - } else { - let full_path = self.abs_path(); - let mut full_path_string = if self.is_local() - && let Ok(stripped) = full_path.strip_prefix(home_dir()) - { - self.path_style - .join("~", &*stripped.to_string_lossy()) - .unwrap() - } else { - full_path.to_string_lossy().into_owned() - }; - - if worktree_relative_path.components().next().is_some() { - full_path_string.push_str(self.path_style.primary_separator()); - full_path_string.push_str(&worktree_relative_path.display(self.path_style)); - } - - full_path_string.into() - } - } -} - -impl LocalWorktree { - pub fn fs(&self) -> &Arc { - &self.fs - } - - pub fn is_path_private(&self, path: &RelPath) -> bool { - !self.share_private_files && self.settings.is_path_private(path) - } - - pub fn fs_is_case_sensitive(&self) -> bool { - self.fs_case_sensitive - } - - fn restart_background_scanners(&mut self, cx: &Context) { - let (scan_requests_tx, scan_requests_rx) = channel::unbounded(); - let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded(); - self.scan_requests_tx = scan_requests_tx; - self.path_prefixes_to_scan_tx = path_prefixes_to_scan_tx; - - self.start_background_scanner(scan_requests_rx, path_prefixes_to_scan_rx, cx); - let always_included_entries = mem::take(&mut self.snapshot.always_included_entries); - log::debug!( - "refreshing entries for the following always included paths: {:?}", - always_included_entries - ); - - // Cleans up old always included entries to ensure they get updated properly. Otherwise, - // nested always included entries may not get updated and will result in out-of-date info. - self.refresh_entries_for_paths(always_included_entries); - } - - fn start_background_scanner( - &mut self, - scan_requests_rx: channel::Receiver, - path_prefixes_to_scan_rx: channel::Receiver, - cx: &Context, - ) { - let snapshot = self.snapshot(); - let share_private_files = self.share_private_files; - let next_entry_id = self.next_entry_id.clone(); - let fs = self.fs.clone(); - let scanning_enabled = self.scanning_enabled; - let settings = self.settings.clone(); - let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded(); - let background_scanner = cx.background_spawn({ - let abs_path = snapshot.abs_path.as_path().to_path_buf(); - let background = cx.background_executor().clone(); - async move { - let (events, watcher) = if scanning_enabled { - fs.watch(&abs_path, FS_WATCH_LATENCY).await - } else { - (Box::pin(stream::pending()) as _, Arc::new(NullWatcher) as _) - }; - let fs_case_sensitive = fs.is_case_sensitive().await.unwrap_or_else(|e| { - log::error!("Failed to determine whether filesystem is case sensitive: {e:#}"); - true - }); - - let mut scanner = BackgroundScanner { - fs, - fs_case_sensitive, - status_updates_tx: scan_states_tx, - executor: background, - scan_requests_rx, - path_prefixes_to_scan_rx, - next_entry_id, - state: async_lock::Mutex::new(BackgroundScannerState { - prev_snapshot: snapshot.snapshot.clone(), - snapshot, - scanned_dirs: Default::default(), - path_prefixes_to_scan: Default::default(), - paths_to_scan: Default::default(), - removed_entries: Default::default(), - changed_paths: Default::default(), - }), - phase: BackgroundScannerPhase::InitialScan, - share_private_files, - scanning_enabled, - settings, - watcher, - }; - - scanner - .run(Box::pin(events.map(|events| events.into_iter().collect()))) - .await; - } - }); - let scan_state_updater = cx.spawn(async move |this, cx| { - while let Some((state, this)) = scan_states_rx.next().await.zip(this.upgrade()) { - this.update(cx, |this, cx| { - let this = this.as_local_mut().unwrap(); - match state { - ScanState::Started => { - *this.is_scanning.0.borrow_mut() = true; - } - ScanState::Updated { - snapshot, - changes, - barrier, - scanning, - } => { - *this.is_scanning.0.borrow_mut() = scanning; - this.set_snapshot(snapshot, changes, cx); - drop(barrier); - } - ScanState::RootUpdated { new_path } => { - this.update_abs_path_and_refresh(new_path, cx); - } - } - }) - .ok(); - } - }); - self._background_scanner_tasks = vec![background_scanner, scan_state_updater]; - *self.is_scanning.0.borrow_mut() = true; - } - - fn set_snapshot( - &mut self, - mut new_snapshot: LocalSnapshot, - entry_changes: UpdatedEntriesSet, - cx: &mut Context, - ) { - let repo_changes = self.changed_repos(&self.snapshot, &mut new_snapshot); - self.snapshot = new_snapshot; - - if let Some(share) = self.update_observer.as_mut() { - share - .snapshots_tx - .unbounded_send((self.snapshot.clone(), entry_changes.clone())) - .ok(); - } - - if !entry_changes.is_empty() { - cx.emit(Event::UpdatedEntries(entry_changes)); - } - if !repo_changes.is_empty() { - cx.emit(Event::UpdatedGitRepositories(repo_changes)); - } - } - - fn changed_repos( - &self, - old_snapshot: &LocalSnapshot, - new_snapshot: &mut LocalSnapshot, - ) -> UpdatedGitRepositoriesSet { - let mut changes = Vec::new(); - let mut old_repos = old_snapshot.git_repositories.iter().peekable(); - let new_repos = new_snapshot.git_repositories.clone(); - let mut new_repos = new_repos.iter().peekable(); - - loop { - match (new_repos.peek().map(clone), old_repos.peek().map(clone)) { - (Some((new_entry_id, new_repo)), Some((old_entry_id, old_repo))) => { - match Ord::cmp(&new_entry_id, &old_entry_id) { - Ordering::Less => { - changes.push(UpdatedGitRepository { - work_directory_id: new_entry_id, - old_work_directory_abs_path: None, - new_work_directory_abs_path: Some( - new_repo.work_directory_abs_path.clone(), - ), - dot_git_abs_path: Some(new_repo.dot_git_abs_path.clone()), - repository_dir_abs_path: Some( - new_repo.repository_dir_abs_path.clone(), - ), - common_dir_abs_path: Some(new_repo.common_dir_abs_path.clone()), - }); - new_repos.next(); - } - Ordering::Equal => { - if new_repo.git_dir_scan_id != old_repo.git_dir_scan_id - || new_repo.work_directory_abs_path - != old_repo.work_directory_abs_path - { - changes.push(UpdatedGitRepository { - work_directory_id: new_entry_id, - old_work_directory_abs_path: Some( - old_repo.work_directory_abs_path.clone(), - ), - new_work_directory_abs_path: Some( - new_repo.work_directory_abs_path.clone(), - ), - dot_git_abs_path: Some(new_repo.dot_git_abs_path.clone()), - repository_dir_abs_path: Some( - new_repo.repository_dir_abs_path.clone(), - ), - common_dir_abs_path: Some(new_repo.common_dir_abs_path.clone()), - }); - } - new_repos.next(); - old_repos.next(); - } - Ordering::Greater => { - changes.push(UpdatedGitRepository { - work_directory_id: old_entry_id, - old_work_directory_abs_path: Some( - old_repo.work_directory_abs_path.clone(), - ), - new_work_directory_abs_path: None, - dot_git_abs_path: None, - repository_dir_abs_path: None, - common_dir_abs_path: None, - }); - old_repos.next(); - } - } - } - (Some((entry_id, repo)), None) => { - changes.push(UpdatedGitRepository { - work_directory_id: entry_id, - old_work_directory_abs_path: None, - new_work_directory_abs_path: Some(repo.work_directory_abs_path.clone()), - dot_git_abs_path: Some(repo.dot_git_abs_path.clone()), - repository_dir_abs_path: Some(repo.repository_dir_abs_path.clone()), - common_dir_abs_path: Some(repo.common_dir_abs_path.clone()), - }); - new_repos.next(); - } - (None, Some((entry_id, repo))) => { - changes.push(UpdatedGitRepository { - work_directory_id: entry_id, - old_work_directory_abs_path: Some(repo.work_directory_abs_path.clone()), - new_work_directory_abs_path: None, - dot_git_abs_path: Some(repo.dot_git_abs_path.clone()), - repository_dir_abs_path: Some(repo.repository_dir_abs_path.clone()), - common_dir_abs_path: Some(repo.common_dir_abs_path.clone()), - }); - old_repos.next(); - } - (None, None) => break, - } - } - - fn clone(value: &(&T, &U)) -> (T, U) { - (value.0.clone(), value.1.clone()) - } - - changes.into() - } - - pub fn scan_complete(&self) -> impl Future + use<> { - let mut is_scanning_rx = self.is_scanning.1.clone(); - async move { - let mut is_scanning = *is_scanning_rx.borrow(); - while is_scanning { - if let Some(value) = is_scanning_rx.recv().await { - is_scanning = value; - } else { - break; - } - } - } - } - - pub fn snapshot(&self) -> LocalSnapshot { - self.snapshot.clone() - } - - pub fn settings(&self) -> WorktreeSettings { - self.settings.clone() - } - - fn load_binary_file( - &self, - path: &RelPath, - cx: &Context, - ) -> Task> { - let path = Arc::from(path); - let abs_path = self.absolutize(&path); - let fs = self.fs.clone(); - let entry = self.refresh_entry(path.clone(), None, cx); - let is_private = self.is_path_private(&path); - - let worktree = cx.weak_entity(); - cx.background_spawn(async move { - let content = fs.load_bytes(&abs_path).await?; - - let worktree = worktree.upgrade().context("worktree was dropped")?; - let file = match entry.await? { - Some(entry) => File::for_entry(entry, worktree), - None => { - let metadata = fs - .metadata(&abs_path) - .await - .with_context(|| { - format!("Loading metadata for excluded file {abs_path:?}") - })? - .with_context(|| { - format!("Excluded file {abs_path:?} got removed during loading") - })?; - Arc::new(File { - entry_id: None, - worktree, - path, - disk_state: DiskState::Present { - mtime: metadata.mtime, - }, - is_local: true, - is_private, - }) - } - }; - - Ok(LoadedBinaryFile { file, content }) - }) - } - - fn load_file(&self, path: &RelPath, cx: &Context) -> Task> { - let path = Arc::from(path); - let abs_path = self.absolutize(&path); - let fs = self.fs.clone(); - let entry = self.refresh_entry(path.clone(), None, cx); - let is_private = self.is_path_private(path.as_ref()); - - let this = cx.weak_entity(); - cx.background_spawn(async move { - // WARN: Temporary workaround for #27283. - // We are not efficient with our memory usage per file, and use in excess of 64GB for a 10GB file - // Therefore, as a temporary workaround to prevent system freezes, we just bail before opening a file - // if it is too large - // 5GB seems to be more reasonable, peaking at ~16GB, while 6GB jumps up to >24GB which seems like a - // reasonable limit - { - const FILE_SIZE_MAX: u64 = 6 * 1024 * 1024 * 1024; // 6GB - if let Ok(Some(metadata)) = fs.metadata(&abs_path).await - && metadata.len >= FILE_SIZE_MAX - { - anyhow::bail!("File is too large to load"); - } - } - let text = fs.load(&abs_path).await?; - - let worktree = this.upgrade().context("worktree was dropped")?; - let file = match entry.await? { - Some(entry) => File::for_entry(entry, worktree), - None => { - let metadata = fs - .metadata(&abs_path) - .await - .with_context(|| { - format!("Loading metadata for excluded file {abs_path:?}") - })? - .with_context(|| { - format!("Excluded file {abs_path:?} got removed during loading") - })?; - Arc::new(File { - entry_id: None, - worktree, - path, - disk_state: DiskState::Present { - mtime: metadata.mtime, - }, - is_local: true, - is_private, - }) - } - }; - - Ok(LoadedFile { file, text }) - }) - } - - /// Find the lowest path in the worktree's datastructures that is an ancestor - fn lowest_ancestor(&self, path: &RelPath) -> Arc { - let mut lowest_ancestor = None; - for path in path.ancestors() { - if self.entry_for_path(path).is_some() { - lowest_ancestor = Some(path.into()); - break; - } - } - - lowest_ancestor.unwrap_or_else(|| RelPath::empty().into()) - } - - fn create_entry( - &self, - path: Arc, - is_dir: bool, - content: Option>, - cx: &Context, - ) -> Task> { - let abs_path = self.absolutize(&path); - let path_excluded = self.settings.is_path_excluded(&path); - let fs = self.fs.clone(); - let task_abs_path = abs_path.clone(); - let write = cx.background_spawn(async move { - if is_dir { - fs.create_dir(&task_abs_path) - .await - .with_context(|| format!("creating directory {task_abs_path:?}")) - } else { - fs.write(&task_abs_path, content.as_deref().unwrap_or(&[])) - .await - .with_context(|| format!("creating file {task_abs_path:?}")) - } - }); - - let lowest_ancestor = self.lowest_ancestor(&path); - cx.spawn(async move |this, cx| { - write.await?; - if path_excluded { - return Ok(CreatedEntry::Excluded { abs_path }); - } - - let (result, refreshes) = this.update(cx, |this, cx| { - let mut refreshes = Vec::new(); - let refresh_paths = path.strip_prefix(&lowest_ancestor).unwrap(); - for refresh_path in refresh_paths.ancestors() { - if refresh_path == RelPath::empty() { - continue; - } - let refresh_full_path = lowest_ancestor.join(refresh_path); - - refreshes.push(this.as_local_mut().unwrap().refresh_entry( - refresh_full_path, - None, - cx, - )); - } - ( - this.as_local_mut().unwrap().refresh_entry(path, None, cx), - refreshes, - ) - })?; - for refresh in refreshes { - refresh.await.log_err(); - } - - Ok(result - .await? - .map(CreatedEntry::Included) - .unwrap_or_else(|| CreatedEntry::Excluded { abs_path })) - }) - } - - fn write_file( - &self, - path: Arc, - text: Rope, - line_ending: LineEnding, - cx: &Context, - ) -> Task>> { - let fs = self.fs.clone(); - let is_private = self.is_path_private(&path); - let abs_path = self.absolutize(&path); - - let write = cx.background_spawn({ - let fs = fs.clone(); - let abs_path = abs_path.clone(); - async move { fs.save(&abs_path, &text, line_ending).await } - }); - - cx.spawn(async move |this, cx| { - write.await?; - let entry = this - .update(cx, |this, cx| { - this.as_local_mut() - .unwrap() - .refresh_entry(path.clone(), None, cx) - })? - .await?; - let worktree = this.upgrade().context("worktree dropped")?; - if let Some(entry) = entry { - Ok(File::for_entry(entry, worktree)) - } else { - let metadata = fs - .metadata(&abs_path) - .await - .with_context(|| { - format!("Fetching metadata after saving the excluded buffer {abs_path:?}") - })? - .with_context(|| { - format!("Excluded buffer {path:?} got removed during saving") - })?; - Ok(Arc::new(File { - worktree, - path, - disk_state: DiskState::Present { - mtime: metadata.mtime, - }, - entry_id: None, - is_local: true, - is_private, - })) - } - }) - } - - fn delete_entry( - &self, - entry_id: ProjectEntryId, - trash: bool, - cx: &Context, - ) -> Option>> { - let entry = self.entry_for_id(entry_id)?.clone(); - let abs_path = self.absolutize(&entry.path); - let fs = self.fs.clone(); - - let delete = cx.background_spawn(async move { - if entry.is_file() { - if trash { - fs.trash_file(&abs_path, Default::default()).await?; - } else { - fs.remove_file(&abs_path, Default::default()).await?; - } - } else if trash { - fs.trash_dir( - &abs_path, - RemoveOptions { - recursive: true, - ignore_if_not_exists: false, - }, - ) - .await?; - } else { - fs.remove_dir( - &abs_path, - RemoveOptions { - recursive: true, - ignore_if_not_exists: false, - }, - ) - .await?; - } - anyhow::Ok(entry.path) - }); - - Some(cx.spawn(async move |this, cx| { - let path = delete.await?; - this.update(cx, |this, _| { - this.as_local_mut() - .unwrap() - .refresh_entries_for_paths(vec![path]) - })? - .recv() - .await; - Ok(()) - })) - } - - pub fn copy_external_entries( - &self, - target_directory: Arc, - paths: Vec>, - cx: &Context, - ) -> Task>> { - let target_directory = self.absolutize(&target_directory); - let worktree_path = self.abs_path().clone(); - let fs = self.fs.clone(); - let paths = paths - .into_iter() - .filter_map(|source| { - let file_name = source.file_name()?; - let mut target = target_directory.clone(); - target.push(file_name); - - // Do not allow copying the same file to itself. - if source.as_ref() != target.as_path() { - Some((source, target)) - } else { - None - } - }) - .collect::>(); - - let paths_to_refresh = paths - .iter() - .filter_map(|(_, target)| { - RelPath::new( - target.strip_prefix(&worktree_path).ok()?, - PathStyle::local(), - ) - .ok() - .map(|path| path.into_arc()) - }) - .collect::>(); - - cx.spawn(async move |this, cx| { - cx.background_spawn(async move { - for (source, target) in paths { - copy_recursive( - fs.as_ref(), - &source, - &target, - fs::CopyOptions { - overwrite: true, - ..Default::default() - }, - ) - .await - .with_context(|| { - format!("Failed to copy file from {source:?} to {target:?}") - })?; - } - anyhow::Ok(()) - }) - .await - .log_err(); - let mut refresh = cx.read_entity( - &this.upgrade().with_context(|| "Dropped worktree")?, - |this, _| { - anyhow::Ok::( - this.as_local() - .with_context(|| "Worktree is not local")? - .refresh_entries_for_paths(paths_to_refresh.clone()), - ) - }, - )??; - - cx.background_spawn(async move { - refresh.next().await; - anyhow::Ok(()) - }) - .await - .log_err(); - - let this = this.upgrade().with_context(|| "Dropped worktree")?; - cx.read_entity(&this, |this, _| { - paths_to_refresh - .iter() - .filter_map(|path| Some(this.entry_for_path(path)?.id)) - .collect() - }) - }) - } - - fn expand_entry( - &self, - entry_id: ProjectEntryId, - cx: &Context, - ) -> Option>> { - let path = self.entry_for_id(entry_id)?.path.clone(); - let mut refresh = self.refresh_entries_for_paths(vec![path]); - Some(cx.background_spawn(async move { - refresh.next().await; - Ok(()) - })) - } - - fn expand_all_for_entry( - &self, - entry_id: ProjectEntryId, - cx: &Context, - ) -> Option>> { - let path = self.entry_for_id(entry_id).unwrap().path.clone(); - let mut rx = self.add_path_prefix_to_scan(path); - Some(cx.background_spawn(async move { - rx.next().await; - Ok(()) - })) - } - - pub fn refresh_entries_for_paths(&self, paths: Vec>) -> barrier::Receiver { - let (tx, rx) = barrier::channel(); - self.scan_requests_tx - .try_send(ScanRequest { - relative_paths: paths, - done: smallvec![tx], - }) - .ok(); - rx - } - - #[cfg(feature = "test-support")] - pub fn manually_refresh_entries_for_paths( - &self, - paths: Vec>, - ) -> barrier::Receiver { - self.refresh_entries_for_paths(paths) - } - - pub fn add_path_prefix_to_scan(&self, path_prefix: Arc) -> barrier::Receiver { - let (tx, rx) = barrier::channel(); - self.path_prefixes_to_scan_tx - .try_send(PathPrefixScanRequest { - path: path_prefix, - done: smallvec![tx], - }) - .ok(); - rx - } - - pub fn refresh_entry( - &self, - path: Arc, - old_path: Option>, - cx: &Context, - ) -> Task>> { - if self.settings.is_path_excluded(&path) { - return Task::ready(Ok(None)); - } - let paths = if let Some(old_path) = old_path.as_ref() { - vec![old_path.clone(), path.clone()] - } else { - vec![path.clone()] - }; - let t0 = Instant::now(); - let mut refresh = self.refresh_entries_for_paths(paths); - // todo(lw): Hot foreground spawn - cx.spawn(async move |this, cx| { - refresh.recv().await; - log::trace!("refreshed entry {path:?} in {:?}", t0.elapsed()); - let new_entry = this.read_with(cx, |this, _| { - this.entry_for_path(&path).cloned().with_context(|| { - format!("Could not find entry in worktree for {path:?} after refresh") - }) - })??; - Ok(Some(new_entry)) - }) - } - - fn observe_updates(&mut self, project_id: u64, cx: &Context, callback: F) - where - F: 'static + Send + Fn(proto::UpdateWorktree) -> Fut, - Fut: 'static + Send + Future, - { - if let Some(observer) = self.update_observer.as_mut() { - *observer.resume_updates.borrow_mut() = (); - return; - } - - let (resume_updates_tx, mut resume_updates_rx) = watch::channel::<()>(); - let (snapshots_tx, mut snapshots_rx) = - mpsc::unbounded::<(LocalSnapshot, UpdatedEntriesSet)>(); - snapshots_tx - .unbounded_send((self.snapshot(), Arc::default())) - .ok(); - - let worktree_id = cx.entity_id().as_u64(); - let _maintain_remote_snapshot = cx.background_spawn(async move { - let mut is_first = true; - while let Some((snapshot, entry_changes)) = snapshots_rx.next().await { - let update = if is_first { - is_first = false; - snapshot.build_initial_update(project_id, worktree_id) - } else { - snapshot.build_update(project_id, worktree_id, entry_changes) - }; - - for update in proto::split_worktree_update(update) { - let _ = resume_updates_rx.try_recv(); - loop { - let result = callback(update.clone()); - if result.await { - break; - } else { - log::info!("waiting to resume updates"); - if resume_updates_rx.next().await.is_none() { - return Some(()); - } - } - } - } - } - Some(()) - }); - - self.update_observer = Some(UpdateObservationState { - snapshots_tx, - resume_updates: resume_updates_tx, - _maintain_remote_snapshot, - }); - } - - pub fn share_private_files(&mut self, cx: &Context) { - self.share_private_files = true; - self.restart_background_scanners(cx); - } - - pub fn update_abs_path_and_refresh( - &mut self, - new_path: Arc, - cx: &Context, - ) { - self.snapshot.git_repositories = Default::default(); - self.snapshot.ignores_by_parent_abs_path = Default::default(); - let root_name = new_path - .as_path() - .file_name() - .and_then(|f| f.to_str()) - .map_or(RelPath::empty().into(), |f| { - RelPath::unix(f).unwrap().into() - }); - self.snapshot.update_abs_path(new_path, root_name); - self.restart_background_scanners(cx); - } -} - -impl RemoteWorktree { - pub fn project_id(&self) -> u64 { - self.project_id - } - - pub fn client(&self) -> AnyProtoClient { - self.client.clone() - } - - pub fn disconnected_from_host(&mut self) { - self.updates_tx.take(); - self.snapshot_subscriptions.clear(); - self.disconnected = true; - } - - pub fn update_from_remote(&self, update: proto::UpdateWorktree) { - if let Some(updates_tx) = &self.updates_tx { - updates_tx - .unbounded_send(update) - .expect("consumer runs to completion"); - } - } - - fn observe_updates(&mut self, project_id: u64, cx: &Context, callback: F) - where - F: 'static + Send + Fn(proto::UpdateWorktree) -> Fut, - Fut: 'static + Send + Future, - { - let (tx, mut rx) = mpsc::unbounded(); - let initial_update = self - .snapshot - .build_initial_update(project_id, self.id().to_proto()); - self.update_observer = Some(tx); - cx.spawn(async move |this, cx| { - let mut update = initial_update; - 'outer: loop { - // SSH projects use a special project ID of 0, and we need to - // remap it to the correct one here. - update.project_id = project_id; - - for chunk in split_worktree_update(update) { - if !callback(chunk).await { - break 'outer; - } - } - - if let Some(next_update) = rx.next().await { - update = next_update; - } else { - break; - } - } - this.update(cx, |this, _| { - let this = this.as_remote_mut().unwrap(); - this.update_observer.take(); - }) - }) - .detach(); - } - - fn observed_snapshot(&self, scan_id: usize) -> bool { - self.completed_scan_id >= scan_id - } - - pub fn wait_for_snapshot( - &mut self, - scan_id: usize, - ) -> impl Future> + use<> { - let (tx, rx) = oneshot::channel(); - if self.observed_snapshot(scan_id) { - let _ = tx.send(()); - } else if self.disconnected { - drop(tx); - } else { - match self - .snapshot_subscriptions - .binary_search_by_key(&scan_id, |probe| probe.0) - { - Ok(ix) | Err(ix) => self.snapshot_subscriptions.insert(ix, (scan_id, tx)), - } - } - - async move { - rx.await?; - Ok(()) - } - } - - pub fn insert_entry( - &mut self, - entry: proto::Entry, - scan_id: usize, - cx: &Context, - ) -> Task> { - let wait_for_snapshot = self.wait_for_snapshot(scan_id); - cx.spawn(async move |this, cx| { - wait_for_snapshot.await?; - this.update(cx, |worktree, _| { - let worktree = worktree.as_remote_mut().unwrap(); - let snapshot = &mut worktree.background_snapshot.lock().0; - let entry = snapshot.insert_entry(entry, &worktree.file_scan_inclusions); - worktree.snapshot = snapshot.clone(); - entry - })? - }) - } - - fn delete_entry( - &self, - entry_id: ProjectEntryId, - trash: bool, - cx: &Context, - ) -> Option>> { - let response = self.client.request(proto::DeleteProjectEntry { - project_id: self.project_id, - entry_id: entry_id.to_proto(), - use_trash: trash, - }); - Some(cx.spawn(async move |this, cx| { - let response = response.await?; - let scan_id = response.worktree_scan_id as usize; - - this.update(cx, move |this, _| { - this.as_remote_mut().unwrap().wait_for_snapshot(scan_id) - })? - .await?; - - this.update(cx, |this, _| { - let this = this.as_remote_mut().unwrap(); - let snapshot = &mut this.background_snapshot.lock().0; - snapshot.delete_entry(entry_id); - this.snapshot = snapshot.clone(); - }) - })) - } - - // fn rename_entry( - // &self, - // entry_id: ProjectEntryId, - // new_path: impl Into>, - // cx: &Context, - // ) -> Task> { - // let new_path: Arc = new_path.into(); - // let response = self.client.request(proto::RenameProjectEntry { - // project_id: self.project_id, - // entry_id: entry_id.to_proto(), - // new_worktree_id: new_path.worktree_id, - // new_path: new_path.as_ref().to_proto(), - // }); - // cx.spawn(async move |this, cx| { - // let response = response.await?; - // match response.entry { - // Some(entry) => this - // .update(cx, |this, cx| { - // this.as_remote_mut().unwrap().insert_entry( - // entry, - // response.worktree_scan_id as usize, - // cx, - // ) - // })? - // .await - // .map(CreatedEntry::Included), - // None => { - // let abs_path = - // this.read_with(cx, |worktree, _| worktree.absolutize(&new_path))?; - // Ok(CreatedEntry::Excluded { abs_path }) - // } - // } - // }) - // } - - fn copy_external_entries( - &self, - target_directory: Arc, - paths_to_copy: Vec>, - local_fs: Arc, - cx: &Context, - ) -> Task>> { - let client = self.client.clone(); - let worktree_id = self.id().to_proto(); - let project_id = self.project_id; - - cx.background_spawn(async move { - let mut requests = Vec::new(); - for root_path_to_copy in paths_to_copy { - let Some(filename) = root_path_to_copy - .file_name() - .and_then(|name| name.to_str()) - .and_then(|filename| RelPath::unix(filename).ok()) - else { - continue; - }; - for (abs_path, is_directory) in - read_dir_items(local_fs.as_ref(), &root_path_to_copy).await? - { - let Some(relative_path) = abs_path - .strip_prefix(&root_path_to_copy) - .map_err(|e| anyhow::Error::from(e)) - .and_then(|relative_path| RelPath::new(relative_path, PathStyle::local())) - .log_err() - else { - continue; - }; - let content = if is_directory { - None - } else { - Some(local_fs.load_bytes(&abs_path).await?) - }; - - let mut target_path = target_directory.join(filename); - if relative_path.file_name().is_some() { - target_path = target_path.join(&relative_path); - } - - requests.push(proto::CreateProjectEntry { - project_id, - worktree_id, - path: target_path.to_proto(), - is_directory, - content, - }); - } - } - requests.sort_unstable_by(|a, b| a.path.cmp(&b.path)); - requests.dedup(); - - let mut copied_entry_ids = Vec::new(); - for request in requests { - let response = client.request(request).await?; - copied_entry_ids.extend(response.entry.map(|e| ProjectEntryId::from_proto(e.id))); - } - - Ok(copied_entry_ids) - }) - } -} - -impl Snapshot { - pub fn new( - id: u64, - root_name: Arc, - abs_path: Arc, - path_style: PathStyle, - ) -> Self { - Snapshot { - id: WorktreeId::from_usize(id as usize), - abs_path: SanitizedPath::from_arc(abs_path), - path_style, - root_char_bag: root_name - .as_unix_str() - .chars() - .map(|c| c.to_ascii_lowercase()) - .collect(), - root_name, - always_included_entries: Default::default(), - entries_by_path: Default::default(), - entries_by_id: Default::default(), - scan_id: 1, - completed_scan_id: 0, - } - } - - pub fn id(&self) -> WorktreeId { - self.id - } - - // TODO: - // Consider the following: - // - // ```rust - // let abs_path: Arc = snapshot.abs_path(); // e.g. "C:\Users\user\Desktop\project" - // let some_non_trimmed_path = Path::new("\\\\?\\C:\\Users\\user\\Desktop\\project\\main.rs"); - // // The caller perform some actions here: - // some_non_trimmed_path.strip_prefix(abs_path); // This fails - // some_non_trimmed_path.starts_with(abs_path); // This fails too - // ``` - // - // This is definitely a bug, but it's not clear if we should handle it here or not. - pub fn abs_path(&self) -> &Arc { - SanitizedPath::cast_arc_ref(&self.abs_path) - } - - fn build_initial_update(&self, project_id: u64, worktree_id: u64) -> proto::UpdateWorktree { - let mut updated_entries = self - .entries_by_path - .iter() - .map(proto::Entry::from) - .collect::>(); - updated_entries.sort_unstable_by_key(|e| e.id); - - proto::UpdateWorktree { - project_id, - worktree_id, - abs_path: self.abs_path().to_string_lossy().into_owned(), - root_name: self.root_name().to_proto(), - updated_entries, - removed_entries: Vec::new(), - scan_id: self.scan_id as u64, - is_last_update: self.completed_scan_id == self.scan_id, - // Sent in separate messages. - updated_repositories: Vec::new(), - removed_repositories: Vec::new(), - } - } - - pub fn work_directory_abs_path(&self, work_directory: &WorkDirectory) -> PathBuf { - match work_directory { - WorkDirectory::InProject { relative_path } => self.absolutize(relative_path), - WorkDirectory::AboveProject { absolute_path, .. } => absolute_path.as_ref().to_owned(), - } - } - - pub fn absolutize(&self, path: &RelPath) -> PathBuf { - if path.file_name().is_some() { - let mut abs_path = self.abs_path.to_string(); - for component in path.components() { - if !abs_path.ends_with(self.path_style.primary_separator()) { - abs_path.push_str(self.path_style.primary_separator()); - } - abs_path.push_str(component); - } - PathBuf::from(abs_path) - } else { - self.abs_path.as_path().to_path_buf() - } - } - - pub fn contains_entry(&self, entry_id: ProjectEntryId) -> bool { - self.entries_by_id.get(&entry_id, ()).is_some() - } - - fn insert_entry( - &mut self, - entry: proto::Entry, - always_included_paths: &PathMatcher, - ) -> Result { - let entry = Entry::try_from((&self.root_char_bag, always_included_paths, entry))?; - let old_entry = self.entries_by_id.insert_or_replace( - PathEntry { - id: entry.id, - path: entry.path.clone(), - is_ignored: entry.is_ignored, - scan_id: 0, - }, - (), - ); - if let Some(old_entry) = old_entry { - self.entries_by_path.remove(&PathKey(old_entry.path), ()); - } - self.entries_by_path.insert_or_replace(entry.clone(), ()); - Ok(entry) - } - - fn delete_entry(&mut self, entry_id: ProjectEntryId) -> Option> { - let removed_entry = self.entries_by_id.remove(&entry_id, ())?; - self.entries_by_path = { - let mut cursor = self.entries_by_path.cursor::(()); - let mut new_entries_by_path = - cursor.slice(&TraversalTarget::path(&removed_entry.path), Bias::Left); - while let Some(entry) = cursor.item() { - if entry.path.starts_with(&removed_entry.path) { - self.entries_by_id.remove(&entry.id, ()); - cursor.next(); - } else { - break; - } - } - new_entries_by_path.append(cursor.suffix(), ()); - new_entries_by_path - }; - - Some(removed_entry.path) - } - - fn update_abs_path(&mut self, abs_path: Arc, root_name: Arc) { - self.abs_path = abs_path; - if root_name != self.root_name { - self.root_char_bag = root_name - .as_unix_str() - .chars() - .map(|c| c.to_ascii_lowercase()) - .collect(); - self.root_name = root_name; - } - } - - fn apply_remote_update( - &mut self, - update: proto::UpdateWorktree, - always_included_paths: &PathMatcher, - ) { - log::debug!( - "applying remote worktree update. {} entries updated, {} removed", - update.updated_entries.len(), - update.removed_entries.len() - ); - if let Some(root_name) = RelPath::from_proto(&update.root_name).log_err() { - self.update_abs_path( - SanitizedPath::new_arc(&Path::new(&update.abs_path)), - root_name, - ); - } - - let mut entries_by_path_edits = Vec::new(); - let mut entries_by_id_edits = Vec::new(); - - for entry_id in update.removed_entries { - let entry_id = ProjectEntryId::from_proto(entry_id); - entries_by_id_edits.push(Edit::Remove(entry_id)); - if let Some(entry) = self.entry_for_id(entry_id) { - entries_by_path_edits.push(Edit::Remove(PathKey(entry.path.clone()))); - } - } - - for entry in update.updated_entries { - let Some(entry) = - Entry::try_from((&self.root_char_bag, always_included_paths, entry)).log_err() - else { - continue; - }; - if let Some(PathEntry { path, .. }) = self.entries_by_id.get(&entry.id, ()) { - entries_by_path_edits.push(Edit::Remove(PathKey(path.clone()))); - } - if let Some(old_entry) = self.entries_by_path.get(&PathKey(entry.path.clone()), ()) - && old_entry.id != entry.id - { - entries_by_id_edits.push(Edit::Remove(old_entry.id)); - } - entries_by_id_edits.push(Edit::Insert(PathEntry { - id: entry.id, - path: entry.path.clone(), - is_ignored: entry.is_ignored, - scan_id: 0, - })); - entries_by_path_edits.push(Edit::Insert(entry)); - } - - self.entries_by_path.edit(entries_by_path_edits, ()); - self.entries_by_id.edit(entries_by_id_edits, ()); - - self.scan_id = update.scan_id as usize; - if update.is_last_update { - self.completed_scan_id = update.scan_id as usize; - } - } - - pub fn entry_count(&self) -> usize { - self.entries_by_path.summary().count - } - - pub fn visible_entry_count(&self) -> usize { - self.entries_by_path.summary().non_ignored_count - } - - pub fn dir_count(&self) -> usize { - let summary = self.entries_by_path.summary(); - summary.count - summary.file_count - } - - pub fn visible_dir_count(&self) -> usize { - let summary = self.entries_by_path.summary(); - summary.non_ignored_count - summary.non_ignored_file_count - } - - pub fn file_count(&self) -> usize { - self.entries_by_path.summary().file_count - } - - pub fn visible_file_count(&self) -> usize { - self.entries_by_path.summary().non_ignored_file_count - } - - fn traverse_from_offset( - &self, - include_files: bool, - include_dirs: bool, - include_ignored: bool, - start_offset: usize, - ) -> Traversal<'_> { - let mut cursor = self.entries_by_path.cursor(()); - cursor.seek( - &TraversalTarget::Count { - count: start_offset, - include_files, - include_dirs, - include_ignored, - }, - Bias::Right, - ); - Traversal { - snapshot: self, - cursor, - include_files, - include_dirs, - include_ignored, - } - } - - pub fn traverse_from_path( - &self, - include_files: bool, - include_dirs: bool, - include_ignored: bool, - path: &RelPath, - ) -> Traversal<'_> { - Traversal::new(self, include_files, include_dirs, include_ignored, path) - } - - pub fn files(&self, include_ignored: bool, start: usize) -> Traversal<'_> { - self.traverse_from_offset(true, false, include_ignored, start) - } - - pub fn directories(&self, include_ignored: bool, start: usize) -> Traversal<'_> { - self.traverse_from_offset(false, true, include_ignored, start) - } - - pub fn entries(&self, include_ignored: bool, start: usize) -> Traversal<'_> { - self.traverse_from_offset(true, true, include_ignored, start) - } - - pub fn paths(&self) -> impl Iterator { - self.entries_by_path - .cursor::<()>(()) - .filter(move |entry| !entry.path.is_empty()) - .map(|entry| entry.path.as_ref()) - } - - pub fn child_entries<'a>(&'a self, parent_path: &'a RelPath) -> ChildEntriesIter<'a> { - let options = ChildEntriesOptions { - include_files: true, - include_dirs: true, - include_ignored: true, - }; - self.child_entries_with_options(parent_path, options) - } - - pub fn child_entries_with_options<'a>( - &'a self, - parent_path: &'a RelPath, - options: ChildEntriesOptions, - ) -> ChildEntriesIter<'a> { - let mut cursor = self.entries_by_path.cursor(()); - cursor.seek(&TraversalTarget::path(parent_path), Bias::Right); - let traversal = Traversal { - snapshot: self, - cursor, - include_files: options.include_files, - include_dirs: options.include_dirs, - include_ignored: options.include_ignored, - }; - ChildEntriesIter { - traversal, - parent_path, - } - } - - pub fn root_entry(&self) -> Option<&Entry> { - self.entries_by_path.first() - } - - /// Returns `None` for a single file worktree, or `Some(self.abs_path())` if - /// it is a directory. - pub fn root_dir(&self) -> Option> { - self.root_entry() - .filter(|entry| entry.is_dir()) - .map(|_| self.abs_path().clone()) - } - - pub fn root_name(&self) -> &RelPath { - &self.root_name - } - - pub fn root_name_str(&self) -> &str { - self.root_name.as_unix_str() - } - - pub fn scan_id(&self) -> usize { - self.scan_id - } - - pub fn entry_for_path(&self, path: &RelPath) -> Option<&Entry> { - self.traverse_from_path(true, true, true, path) - .entry() - .and_then(|entry| { - if entry.path.as_ref() == path { - Some(entry) - } else { - None - } - }) - } - - /// Resolves a path to an executable using the following heuristics: - /// - /// 1. If the path starts with `~`, it is expanded to the user's home directory. - /// 2. If the path is relative and contains more than one component, - /// it is joined to the worktree root path. - /// 3. If the path is relative and exists in the worktree - /// (even if falls under an exclusion filter), - /// it is joined to the worktree root path. - /// 4. Otherwise the path is returned unmodified. - /// - /// Relative paths that do not exist in the worktree may - /// still be found using the `PATH` environment variable. - pub fn resolve_executable_path(&self, path: PathBuf) -> PathBuf { - if let Some(path_str) = path.to_str() { - if let Some(remaining_path) = path_str.strip_prefix("~/") { - return home_dir().join(remaining_path); - } else if path_str == "~" { - return home_dir().to_path_buf(); - } - } - - if let Ok(rel_path) = RelPath::new(&path, self.path_style) - && (path.components().count() > 1 || self.entry_for_path(&rel_path).is_some()) - { - self.abs_path().join(path) - } else { - path - } - } - - pub fn entry_for_id(&self, id: ProjectEntryId) -> Option<&Entry> { - let entry = self.entries_by_id.get(&id, ())?; - self.entry_for_path(&entry.path) - } - - pub fn path_style(&self) -> PathStyle { - self.path_style - } -} - -impl LocalSnapshot { - fn local_repo_for_work_directory_path(&self, path: &RelPath) -> Option<&LocalRepositoryEntry> { - self.git_repositories - .iter() - .map(|(_, entry)| entry) - .find(|entry| entry.work_directory.path_key() == PathKey(path.into())) - } - - fn build_update( - &self, - project_id: u64, - worktree_id: u64, - entry_changes: UpdatedEntriesSet, - ) -> proto::UpdateWorktree { - let mut updated_entries = Vec::new(); - let mut removed_entries = Vec::new(); - - for (_, entry_id, path_change) in entry_changes.iter() { - if let PathChange::Removed = path_change { - removed_entries.push(entry_id.0 as u64); - } else if let Some(entry) = self.entry_for_id(*entry_id) { - updated_entries.push(proto::Entry::from(entry)); - } - } - - removed_entries.sort_unstable(); - updated_entries.sort_unstable_by_key(|e| e.id); - - // TODO - optimize, knowing that removed_entries are sorted. - removed_entries.retain(|id| updated_entries.binary_search_by_key(id, |e| e.id).is_err()); - - proto::UpdateWorktree { - project_id, - worktree_id, - abs_path: self.abs_path().to_string_lossy().into_owned(), - root_name: self.root_name().to_proto(), - updated_entries, - removed_entries, - scan_id: self.scan_id as u64, - is_last_update: self.completed_scan_id == self.scan_id, - // Sent in separate messages. - updated_repositories: Vec::new(), - removed_repositories: Vec::new(), - } - } - - fn insert_entry(&mut self, mut entry: Entry, fs: &dyn Fs) -> Entry { - log::trace!("insert entry {:?}", entry.path); - if entry.is_file() && entry.path.file_name() == Some(&GITIGNORE) { - let abs_path = self.absolutize(&entry.path); - match self.executor.block(build_gitignore(&abs_path, fs)) { - Ok(ignore) => { - self.ignores_by_parent_abs_path - .insert(abs_path.parent().unwrap().into(), (Arc::new(ignore), true)); - } - Err(error) => { - log::error!( - "error loading .gitignore file {:?} - {:?}", - &entry.path, - error - ); - } - } - } - - if entry.kind == EntryKind::PendingDir - && let Some(existing_entry) = self.entries_by_path.get(&PathKey(entry.path.clone()), ()) - { - entry.kind = existing_entry.kind; - } - - let scan_id = self.scan_id; - let removed = self.entries_by_path.insert_or_replace(entry.clone(), ()); - if let Some(removed) = removed - && removed.id != entry.id - { - self.entries_by_id.remove(&removed.id, ()); - } - self.entries_by_id.insert_or_replace( - PathEntry { - id: entry.id, - path: entry.path.clone(), - is_ignored: entry.is_ignored, - scan_id, - }, - (), - ); - - entry - } - - fn ancestor_inodes_for_path(&self, path: &RelPath) -> TreeSet { - let mut inodes = TreeSet::default(); - for ancestor in path.ancestors().skip(1) { - if let Some(entry) = self.entry_for_path(ancestor) { - inodes.insert(entry.inode); - } - } - inodes - } - - async fn ignore_stack_for_abs_path( - &self, - abs_path: &Path, - is_dir: bool, - fs: &dyn Fs, - ) -> IgnoreStack { - let mut new_ignores = Vec::new(); - let mut repo_root = None; - for (index, ancestor) in abs_path.ancestors().enumerate() { - if index > 0 { - if let Some((ignore, _)) = self.ignores_by_parent_abs_path.get(ancestor) { - new_ignores.push((ancestor, Some(ignore.clone()))); - } else { - new_ignores.push((ancestor, None)); - } - } - - let metadata = fs.metadata(&ancestor.join(DOT_GIT)).await.ok().flatten(); - if metadata.is_some() { - repo_root = Some(Arc::from(ancestor)); - break; - } - } - - let mut ignore_stack = if let Some(global_gitignore) = self.global_gitignore.clone() { - IgnoreStack::global(global_gitignore) - } else { - IgnoreStack::none() - }; - ignore_stack.repo_root = repo_root; - for (parent_abs_path, ignore) in new_ignores.into_iter().rev() { - if ignore_stack.is_abs_path_ignored(parent_abs_path, true) { - ignore_stack = IgnoreStack::all(); - break; - } else if let Some(ignore) = ignore { - ignore_stack = ignore_stack.append(parent_abs_path.into(), ignore); - } - } - - if ignore_stack.is_abs_path_ignored(abs_path, is_dir) { - ignore_stack = IgnoreStack::all(); - } - - ignore_stack - } - - #[cfg(test)] - fn expanded_entries(&self) -> impl Iterator { - self.entries_by_path - .cursor::<()>(()) - .filter(|entry| entry.kind == EntryKind::Dir && (entry.is_external || entry.is_ignored)) - } - - #[cfg(test)] - pub fn check_invariants(&self, git_state: bool) { - use pretty_assertions::assert_eq; - - assert_eq!( - self.entries_by_path - .cursor::<()>(()) - .map(|e| (&e.path, e.id)) - .collect::>(), - self.entries_by_id - .cursor::<()>(()) - .map(|e| (&e.path, e.id)) - .collect::>() - .into_iter() - .collect::>(), - "entries_by_path and entries_by_id are inconsistent" - ); - - let mut files = self.files(true, 0); - let mut visible_files = self.files(false, 0); - for entry in self.entries_by_path.cursor::<()>(()) { - if entry.is_file() { - assert_eq!(files.next().unwrap().inode, entry.inode); - if (!entry.is_ignored && !entry.is_external) || entry.is_always_included { - assert_eq!(visible_files.next().unwrap().inode, entry.inode); - } - } - } - - assert!(files.next().is_none()); - assert!(visible_files.next().is_none()); - - let mut bfs_paths = Vec::new(); - let mut stack = self - .root_entry() - .map(|e| e.path.as_ref()) - .into_iter() - .collect::>(); - while let Some(path) = stack.pop() { - bfs_paths.push(path); - let ix = stack.len(); - for child_entry in self.child_entries(path) { - stack.insert(ix, &child_entry.path); - } - } - - let dfs_paths_via_iter = self - .entries_by_path - .cursor::<()>(()) - .map(|e| e.path.as_ref()) - .collect::>(); - assert_eq!(bfs_paths, dfs_paths_via_iter); - - let dfs_paths_via_traversal = self - .entries(true, 0) - .map(|e| e.path.as_ref()) - .collect::>(); - - assert_eq!(dfs_paths_via_traversal, dfs_paths_via_iter); - - if git_state { - for ignore_parent_abs_path in self.ignores_by_parent_abs_path.keys() { - let ignore_parent_path = &RelPath::new( - ignore_parent_abs_path - .strip_prefix(self.abs_path.as_path()) - .unwrap(), - PathStyle::local(), - ) - .unwrap(); - assert!(self.entry_for_path(ignore_parent_path).is_some()); - assert!( - self.entry_for_path( - &ignore_parent_path.join(RelPath::unix(GITIGNORE).unwrap()) - ) - .is_some() - ); - } - } - } - - #[cfg(test)] - pub fn entries_without_ids(&self, include_ignored: bool) -> Vec<(&RelPath, u64, bool)> { - let mut paths = Vec::new(); - for entry in self.entries_by_path.cursor::<()>(()) { - if include_ignored || !entry.is_ignored { - paths.push((entry.path.as_ref(), entry.inode, entry.is_ignored)); - } - } - paths.sort_by(|a, b| a.0.cmp(b.0)); - paths - } -} - -impl BackgroundScannerState { - fn should_scan_directory(&self, entry: &Entry) -> bool { - (!entry.is_external && (!entry.is_ignored || entry.is_always_included)) - || entry.path.file_name() == Some(DOT_GIT) - || entry.path.file_name() == Some(local_settings_folder_name()) - || entry.path.file_name() == Some(local_vscode_folder_name()) - || self.scanned_dirs.contains(&entry.id) // If we've ever scanned it, keep scanning - || self - .paths_to_scan - .iter() - .any(|p| p.starts_with(&entry.path)) - || self - .path_prefixes_to_scan - .iter() - .any(|p| entry.path.starts_with(p)) - } - - async fn enqueue_scan_dir( - &self, - abs_path: Arc, - entry: &Entry, - scan_job_tx: &Sender, - fs: &dyn Fs, - ) { - let path = entry.path.clone(); - let ignore_stack = self - .snapshot - .ignore_stack_for_abs_path(&abs_path, true, fs) - .await; - let mut ancestor_inodes = self.snapshot.ancestor_inodes_for_path(&path); - - if !ancestor_inodes.contains(&entry.inode) { - ancestor_inodes.insert(entry.inode); - scan_job_tx - .try_send(ScanJob { - abs_path, - path, - ignore_stack, - scan_queue: scan_job_tx.clone(), - ancestor_inodes, - is_external: entry.is_external, - }) - .unwrap(); - } - } - - fn reuse_entry_id(&mut self, entry: &mut Entry) { - if let Some(mtime) = entry.mtime { - // If an entry with the same inode was removed from the worktree during this scan, - // then it *might* represent the same file or directory. But the OS might also have - // re-used the inode for a completely different file or directory. - // - // Conditionally reuse the old entry's id: - // * if the mtime is the same, the file was probably been renamed. - // * if the path is the same, the file may just have been updated - if let Some(removed_entry) = self.removed_entries.remove(&entry.inode) { - if removed_entry.mtime == Some(mtime) || removed_entry.path == entry.path { - entry.id = removed_entry.id; - } - } else if let Some(existing_entry) = self.snapshot.entry_for_path(&entry.path) { - entry.id = existing_entry.id; - } - } - } - - fn entry_id_for( - &mut self, - next_entry_id: &AtomicUsize, - path: &RelPath, - metadata: &fs::Metadata, - ) -> ProjectEntryId { - // If an entry with the same inode was removed from the worktree during this scan, - // then it *might* represent the same file or directory. But the OS might also have - // re-used the inode for a completely different file or directory. - // - // Conditionally reuse the old entry's id: - // * if the mtime is the same, the file was probably been renamed. - // * if the path is the same, the file may just have been updated - if let Some(removed_entry) = self.removed_entries.remove(&metadata.inode) { - if removed_entry.mtime == Some(metadata.mtime) || *removed_entry.path == *path { - return removed_entry.id; - } - } else if let Some(existing_entry) = self.snapshot.entry_for_path(path) { - return existing_entry.id; - } - ProjectEntryId::new(next_entry_id) - } - - async fn insert_entry(&mut self, entry: Entry, fs: &dyn Fs, watcher: &dyn Watcher) -> Entry { - let entry = self.snapshot.insert_entry(entry, fs); - if entry.path.file_name() == Some(&DOT_GIT) { - self.insert_git_repository(entry.path.clone(), fs, watcher) - .await; - } - - #[cfg(test)] - self.snapshot.check_invariants(false); - - entry - } - - fn populate_dir( - &mut self, - parent_path: Arc, - entries: impl IntoIterator, - ignore: Option>, - ) { - let mut parent_entry = if let Some(parent_entry) = self - .snapshot - .entries_by_path - .get(&PathKey(parent_path.clone()), ()) - { - parent_entry.clone() - } else { - log::warn!( - "populating a directory {:?} that has been removed", - parent_path - ); - return; - }; - - match parent_entry.kind { - EntryKind::PendingDir | EntryKind::UnloadedDir => parent_entry.kind = EntryKind::Dir, - EntryKind::Dir => {} - _ => return, - } - - if let Some(ignore) = ignore { - let abs_parent_path = self - .snapshot - .abs_path - .as_path() - .join(parent_path.as_std_path()) - .into(); - self.snapshot - .ignores_by_parent_abs_path - .insert(abs_parent_path, (ignore, false)); - } - - let parent_entry_id = parent_entry.id; - self.scanned_dirs.insert(parent_entry_id); - let mut entries_by_path_edits = vec![Edit::Insert(parent_entry)]; - let mut entries_by_id_edits = Vec::new(); - - for entry in entries { - entries_by_id_edits.push(Edit::Insert(PathEntry { - id: entry.id, - path: entry.path.clone(), - is_ignored: entry.is_ignored, - scan_id: self.snapshot.scan_id, - })); - entries_by_path_edits.push(Edit::Insert(entry)); - } - - self.snapshot - .entries_by_path - .edit(entries_by_path_edits, ()); - self.snapshot.entries_by_id.edit(entries_by_id_edits, ()); - - if let Err(ix) = self.changed_paths.binary_search(&parent_path) { - self.changed_paths.insert(ix, parent_path.clone()); - } - - #[cfg(test)] - self.snapshot.check_invariants(false); - } - - fn remove_path(&mut self, path: &RelPath) { - log::trace!("background scanner removing path {path:?}"); - let mut new_entries; - let removed_entries; - { - let mut cursor = self - .snapshot - .entries_by_path - .cursor::(()); - new_entries = cursor.slice(&TraversalTarget::path(path), Bias::Left); - removed_entries = cursor.slice(&TraversalTarget::successor(path), Bias::Left); - new_entries.append(cursor.suffix(), ()); - } - self.snapshot.entries_by_path = new_entries; - - let mut removed_ids = Vec::with_capacity(removed_entries.summary().count); - for entry in removed_entries.cursor::<()>(()) { - match self.removed_entries.entry(entry.inode) { - hash_map::Entry::Occupied(mut e) => { - let prev_removed_entry = e.get_mut(); - if entry.id > prev_removed_entry.id { - *prev_removed_entry = entry.clone(); - } - } - hash_map::Entry::Vacant(e) => { - e.insert(entry.clone()); - } - } - - if entry.path.file_name() == Some(GITIGNORE) { - let abs_parent_path = self.snapshot.absolutize(&entry.path.parent().unwrap()); - if let Some((_, needs_update)) = self - .snapshot - .ignores_by_parent_abs_path - .get_mut(abs_parent_path.as_path()) - { - *needs_update = true; - } - } - - if let Err(ix) = removed_ids.binary_search(&entry.id) { - removed_ids.insert(ix, entry.id); - } - } - - self.snapshot - .entries_by_id - .edit(removed_ids.iter().map(|&id| Edit::Remove(id)).collect(), ()); - self.snapshot - .git_repositories - .retain(|id, _| removed_ids.binary_search(id).is_err()); - - #[cfg(test)] - self.snapshot.check_invariants(false); - } - - async fn insert_git_repository( - &mut self, - dot_git_path: Arc, - fs: &dyn Fs, - watcher: &dyn Watcher, - ) { - let work_dir_path: Arc = match dot_git_path.parent() { - Some(parent_dir) => { - // Guard against repositories inside the repository metadata - if parent_dir - .components() - .any(|component| component == DOT_GIT) - { - log::debug!( - "not building git repository for nested `.git` directory, `.git` path in the worktree: {dot_git_path:?}" - ); - return; - }; - - parent_dir.into() - } - None => { - // `dot_git_path.parent().is_none()` means `.git` directory is the opened worktree itself, - // no files inside that directory are tracked by git, so no need to build the repo around it - log::debug!( - "not building git repository for the worktree itself, `.git` path in the worktree: {dot_git_path:?}" - ); - return; - } - }; - - let dot_git_abs_path = Arc::from(self.snapshot.absolutize(&dot_git_path).as_ref()); - - self.insert_git_repository_for_path( - WorkDirectory::InProject { - relative_path: work_dir_path, - }, - dot_git_abs_path, - fs, - watcher, - ) - .await - .log_err(); - } - - async fn insert_git_repository_for_path( - &mut self, - work_directory: WorkDirectory, - dot_git_abs_path: Arc, - fs: &dyn Fs, - watcher: &dyn Watcher, - ) -> Result { - let work_dir_entry = self - .snapshot - .entry_for_path(&work_directory.path_key().0) - .with_context(|| { - format!( - "working directory `{}` not indexed", - work_directory - .path_key() - .0 - .display(self.snapshot.path_style) - ) - })?; - let work_directory_abs_path = self.snapshot.work_directory_abs_path(&work_directory); - - let (repository_dir_abs_path, common_dir_abs_path) = - discover_git_paths(&dot_git_abs_path, fs).await; - watcher - .add(&common_dir_abs_path) - .context("failed to add common directory to watcher") - .log_err(); - if !repository_dir_abs_path.starts_with(&common_dir_abs_path) { - watcher - .add(&repository_dir_abs_path) - .context("failed to add repository directory to watcher") - .log_err(); - } - - let work_directory_id = work_dir_entry.id; - - let local_repository = LocalRepositoryEntry { - work_directory_id, - work_directory, - work_directory_abs_path: work_directory_abs_path.as_path().into(), - git_dir_scan_id: 0, - dot_git_abs_path, - common_dir_abs_path, - repository_dir_abs_path, - }; - - self.snapshot - .git_repositories - .insert(work_directory_id, local_repository.clone()); - - log::trace!("inserting new local git repository"); - Ok(local_repository) - } -} - -async fn is_git_dir(path: &Path, fs: &dyn Fs) -> bool { - if let Some(file_name) = path.file_name() - && file_name == DOT_GIT - { - return true; - } - - // If we're in a bare repository, we are not inside a `.git` folder. In a - // bare repository, the root folder contains what would normally be in the - // `.git` folder. - let head_metadata = fs.metadata(&path.join("HEAD")).await; - if !matches!(head_metadata, Ok(Some(_))) { - return false; - } - let config_metadata = fs.metadata(&path.join("config")).await; - matches!(config_metadata, Ok(Some(_))) -} - -async fn build_gitignore(abs_path: &Path, fs: &dyn Fs) -> Result { - let contents = fs - .load(abs_path) - .await - .with_context(|| format!("failed to load gitignore file at {}", abs_path.display()))?; - let parent = abs_path.parent().unwrap_or_else(|| Path::new("/")); - let mut builder = GitignoreBuilder::new(parent); - for line in contents.lines() { - builder.add_line(Some(abs_path.into()), line)?; - } - Ok(builder.build()?) -} - -impl Deref for Worktree { - type Target = Snapshot; - - fn deref(&self) -> &Self::Target { - match self { - Worktree::Local(worktree) => &worktree.snapshot, - Worktree::Remote(worktree) => &worktree.snapshot, - } - } -} - -impl Deref for LocalWorktree { - type Target = LocalSnapshot; - - fn deref(&self) -> &Self::Target { - &self.snapshot - } -} - -impl Deref for RemoteWorktree { - type Target = Snapshot; - - fn deref(&self) -> &Self::Target { - &self.snapshot - } -} - -impl fmt::Debug for LocalWorktree { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.snapshot.fmt(f) - } -} - -impl fmt::Debug for Snapshot { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - struct EntriesById<'a>(&'a SumTree); - struct EntriesByPath<'a>(&'a SumTree); - - impl fmt::Debug for EntriesByPath<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_map() - .entries(self.0.iter().map(|entry| (&entry.path, entry.id))) - .finish() - } - } - - impl fmt::Debug for EntriesById<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_list().entries(self.0.iter()).finish() - } - } - - f.debug_struct("Snapshot") - .field("id", &self.id) - .field("root_name", &self.root_name) - .field("entries_by_path", &EntriesByPath(&self.entries_by_path)) - .field("entries_by_id", &EntriesById(&self.entries_by_id)) - .finish() - } -} - -#[derive(Debug, Clone, PartialEq)] -pub struct File { - pub worktree: Entity, - pub path: Arc, - pub disk_state: DiskState, - pub entry_id: Option, - pub is_local: bool, - pub is_private: bool, -} - -impl language::File for File { - fn as_local(&self) -> Option<&dyn language::LocalFile> { - if self.is_local { Some(self) } else { None } - } - - fn disk_state(&self) -> DiskState { - self.disk_state - } - - fn path(&self) -> &Arc { - &self.path - } - - fn full_path(&self, cx: &App) -> PathBuf { - self.worktree.read(cx).full_path(&self.path) - } - - /// Returns the last component of this handle's absolute path. If this handle refers to the root - /// of its worktree, then this method will return the name of the worktree itself. - fn file_name<'a>(&'a self, cx: &'a App) -> &'a str { - self.path - .file_name() - .unwrap_or_else(|| self.worktree.read(cx).root_name_str()) - } - - fn worktree_id(&self, cx: &App) -> WorktreeId { - self.worktree.read(cx).id() - } - - fn to_proto(&self, cx: &App) -> rpc::proto::File { - rpc::proto::File { - worktree_id: self.worktree.read(cx).id().to_proto(), - entry_id: self.entry_id.map(|id| id.to_proto()), - path: self.path.as_ref().to_proto(), - mtime: self.disk_state.mtime().map(|time| time.into()), - is_deleted: self.disk_state == DiskState::Deleted, - } - } - - fn is_private(&self) -> bool { - self.is_private - } - - fn path_style(&self, cx: &App) -> PathStyle { - self.worktree.read(cx).path_style() - } -} - -impl language::LocalFile for File { - fn abs_path(&self, cx: &App) -> PathBuf { - self.worktree.read(cx).absolutize(&self.path) - } - - fn load(&self, cx: &App) -> Task> { - let worktree = self.worktree.read(cx).as_local().unwrap(); - let abs_path = worktree.absolutize(&self.path); - let fs = worktree.fs.clone(); - cx.background_spawn(async move { fs.load(&abs_path).await }) - } - - fn load_bytes(&self, cx: &App) -> Task>> { - let worktree = self.worktree.read(cx).as_local().unwrap(); - let abs_path = worktree.absolutize(&self.path); - let fs = worktree.fs.clone(); - cx.background_spawn(async move { fs.load_bytes(&abs_path).await }) - } -} - -impl File { - pub fn for_entry(entry: Entry, worktree: Entity) -> Arc { - Arc::new(Self { - worktree, - path: entry.path.clone(), - disk_state: if let Some(mtime) = entry.mtime { - DiskState::Present { mtime } - } else { - DiskState::New - }, - entry_id: Some(entry.id), - is_local: true, - is_private: entry.is_private, - }) - } - - pub fn from_proto( - proto: rpc::proto::File, - worktree: Entity, - cx: &App, - ) -> Result { - let worktree_id = worktree.read(cx).as_remote().context("not remote")?.id(); - - anyhow::ensure!( - worktree_id.to_proto() == proto.worktree_id, - "worktree id does not match file" - ); - - let disk_state = if proto.is_deleted { - DiskState::Deleted - } else if let Some(mtime) = proto.mtime.map(&Into::into) { - DiskState::Present { mtime } - } else { - DiskState::New - }; - - Ok(Self { - worktree, - path: RelPath::from_proto(&proto.path).context("invalid path in file protobuf")?, - disk_state, - entry_id: proto.entry_id.map(ProjectEntryId::from_proto), - is_local: false, - is_private: false, - }) - } - - pub fn from_dyn(file: Option<&Arc>) -> Option<&Self> { - file.and_then(|f| { - let f: &dyn language::File = f.borrow(); - let f: &dyn Any = f; - f.downcast_ref() - }) - } - - pub fn worktree_id(&self, cx: &App) -> WorktreeId { - self.worktree.read(cx).id() - } - - pub fn project_entry_id(&self) -> Option { - match self.disk_state { - DiskState::Deleted => None, - _ => self.entry_id, - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct Entry { - pub id: ProjectEntryId, - pub kind: EntryKind, - pub path: Arc, - pub inode: u64, - pub mtime: Option, - - pub canonical_path: Option>, - /// Whether this entry is ignored by Git. - /// - /// We only scan ignored entries once the directory is expanded and - /// exclude them from searches. - pub is_ignored: bool, - - /// Whether this entry is hidden or inside hidden directory. - /// - /// We only scan hidden entries once the directory is expanded. - pub is_hidden: bool, - - /// Whether this entry is always included in searches. - /// - /// This is used for entries that are always included in searches, even - /// if they are ignored by git. Overridden by file_scan_exclusions. - pub is_always_included: bool, - - /// Whether this entry's canonical path is outside of the worktree. - /// This means the entry is only accessible from the worktree root via a - /// symlink. - /// - /// We only scan entries outside of the worktree once the symlinked - /// directory is expanded. External entries are treated like gitignored - /// entries in that they are not included in searches. - pub is_external: bool, - - /// Whether this entry is considered to be a `.env` file. - pub is_private: bool, - /// The entry's size on disk, in bytes. - pub size: u64, - pub char_bag: CharBag, - pub is_fifo: bool, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum EntryKind { - UnloadedDir, - PendingDir, - Dir, - File, -} - -#[derive(Clone, Copy, Debug, PartialEq)] -pub enum PathChange { - /// A filesystem entry was was created. - Added, - /// A filesystem entry was removed. - Removed, - /// A filesystem entry was updated. - Updated, - /// A filesystem entry was either updated or added. We don't know - /// whether or not it already existed, because the path had not - /// been loaded before the event. - AddedOrUpdated, - /// A filesystem entry was found during the initial scan of the worktree. - Loaded, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct UpdatedGitRepository { - /// ID of the repository's working directory. - /// - /// For a repo that's above the worktree root, this is the ID of the worktree root, and hence not unique. - /// It's included here to aid the GitStore in detecting when a repository's working directory is renamed. - pub work_directory_id: ProjectEntryId, - pub old_work_directory_abs_path: Option>, - pub new_work_directory_abs_path: Option>, - /// For a normal git repository checkout, the absolute path to the .git directory. - /// For a worktree, the absolute path to the worktree's subdirectory inside the .git directory. - pub dot_git_abs_path: Option>, - pub repository_dir_abs_path: Option>, - pub common_dir_abs_path: Option>, -} - -pub type UpdatedEntriesSet = Arc<[(Arc, ProjectEntryId, PathChange)]>; -pub type UpdatedGitRepositoriesSet = Arc<[UpdatedGitRepository]>; - -#[derive(Clone, Debug)] -pub struct PathProgress<'a> { - pub max_path: &'a RelPath, -} - -#[derive(Clone, Debug)] -pub struct PathSummary { - pub max_path: Arc, - pub item_summary: S, -} - -impl Summary for PathSummary { - type Context<'a> = S::Context<'a>; - - fn zero(cx: Self::Context<'_>) -> Self { - Self { - max_path: RelPath::empty().into(), - item_summary: S::zero(cx), - } - } - - fn add_summary(&mut self, rhs: &Self, cx: Self::Context<'_>) { - self.max_path = rhs.max_path.clone(); - self.item_summary.add_summary(&rhs.item_summary, cx); - } -} - -impl<'a, S: Summary> sum_tree::Dimension<'a, PathSummary> for PathProgress<'a> { - fn zero(_: as Summary>::Context<'_>) -> Self { - Self { - max_path: RelPath::empty(), - } - } - - fn add_summary( - &mut self, - summary: &'a PathSummary, - _: as Summary>::Context<'_>, - ) { - self.max_path = summary.max_path.as_ref() - } -} - -impl<'a> sum_tree::Dimension<'a, PathSummary> for GitSummary { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a PathSummary, _: ()) { - *self += summary.item_summary - } -} - -impl<'a> - sum_tree::SeekTarget<'a, PathSummary, Dimensions, GitSummary>> - for PathTarget<'_> -{ - fn cmp( - &self, - cursor_location: &Dimensions, GitSummary>, - _: (), - ) -> Ordering { - self.cmp_path(cursor_location.0.max_path) - } -} - -impl<'a, S: Summary> sum_tree::Dimension<'a, PathSummary> for PathKey { - fn zero(_: S::Context<'_>) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a PathSummary, _: S::Context<'_>) { - self.0 = summary.max_path.clone(); - } -} - -impl<'a, S: Summary> sum_tree::Dimension<'a, PathSummary> for TraversalProgress<'a> { - fn zero(_cx: S::Context<'_>) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a PathSummary, _: S::Context<'_>) { - self.max_path = summary.max_path.as_ref(); - } -} - -impl Entry { - fn new( - path: Arc, - metadata: &fs::Metadata, - id: ProjectEntryId, - root_char_bag: CharBag, - canonical_path: Option>, - ) -> Self { - let char_bag = char_bag_for_path(root_char_bag, &path); - Self { - id, - kind: if metadata.is_dir { - EntryKind::PendingDir - } else { - EntryKind::File - }, - path, - inode: metadata.inode, - mtime: Some(metadata.mtime), - size: metadata.len, - canonical_path, - is_ignored: false, - is_hidden: false, - is_always_included: false, - is_external: false, - is_private: false, - char_bag, - is_fifo: metadata.is_fifo, - } - } - - pub fn is_created(&self) -> bool { - self.mtime.is_some() - } - - pub fn is_dir(&self) -> bool { - self.kind.is_dir() - } - - pub fn is_file(&self) -> bool { - self.kind.is_file() - } -} - -impl EntryKind { - pub fn is_dir(&self) -> bool { - matches!( - self, - EntryKind::Dir | EntryKind::PendingDir | EntryKind::UnloadedDir - ) - } - - pub fn is_unloaded(&self) -> bool { - matches!(self, EntryKind::UnloadedDir) - } - - pub fn is_file(&self) -> bool { - matches!(self, EntryKind::File) - } -} - -impl sum_tree::Item for Entry { - type Summary = EntrySummary; - - fn summary(&self, _cx: ()) -> Self::Summary { - let non_ignored_count = if (self.is_ignored || self.is_external) && !self.is_always_included - { - 0 - } else { - 1 - }; - let file_count; - let non_ignored_file_count; - if self.is_file() { - file_count = 1; - non_ignored_file_count = non_ignored_count; - } else { - file_count = 0; - non_ignored_file_count = 0; - } - - EntrySummary { - max_path: self.path.clone(), - count: 1, - non_ignored_count, - file_count, - non_ignored_file_count, - } - } -} - -impl sum_tree::KeyedItem for Entry { - type Key = PathKey; - - fn key(&self) -> Self::Key { - PathKey(self.path.clone()) - } -} - -#[derive(Clone, Debug)] -pub struct EntrySummary { - max_path: Arc, - count: usize, - non_ignored_count: usize, - file_count: usize, - non_ignored_file_count: usize, -} - -impl Default for EntrySummary { - fn default() -> Self { - Self { - max_path: Arc::from(RelPath::empty()), - count: 0, - non_ignored_count: 0, - file_count: 0, - non_ignored_file_count: 0, - } - } -} - -impl sum_tree::ContextLessSummary for EntrySummary { - fn zero() -> Self { - Default::default() - } - - fn add_summary(&mut self, rhs: &Self) { - self.max_path = rhs.max_path.clone(); - self.count += rhs.count; - self.non_ignored_count += rhs.non_ignored_count; - self.file_count += rhs.file_count; - self.non_ignored_file_count += rhs.non_ignored_file_count; - } -} - -#[derive(Clone, Debug)] -struct PathEntry { - id: ProjectEntryId, - path: Arc, - is_ignored: bool, - scan_id: usize, -} - -impl sum_tree::Item for PathEntry { - type Summary = PathEntrySummary; - - fn summary(&self, _cx: ()) -> Self::Summary { - PathEntrySummary { max_id: self.id } - } -} - -impl sum_tree::KeyedItem for PathEntry { - type Key = ProjectEntryId; - - fn key(&self) -> Self::Key { - self.id - } -} - -#[derive(Clone, Debug, Default)] -struct PathEntrySummary { - max_id: ProjectEntryId, -} - -impl sum_tree::ContextLessSummary for PathEntrySummary { - fn zero() -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &Self) { - self.max_id = summary.max_id; - } -} - -impl<'a> sum_tree::Dimension<'a, PathEntrySummary> for ProjectEntryId { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a PathEntrySummary, _: ()) { - *self = summary.max_id; - } -} - -#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] -pub struct PathKey(pub Arc); - -impl Default for PathKey { - fn default() -> Self { - Self(RelPath::empty().into()) - } -} - -impl<'a> sum_tree::Dimension<'a, EntrySummary> for PathKey { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a EntrySummary, _: ()) { - self.0 = summary.max_path.clone(); - } -} - -struct BackgroundScanner { - state: async_lock::Mutex, - fs: Arc, - fs_case_sensitive: bool, - status_updates_tx: UnboundedSender, - executor: BackgroundExecutor, - scan_requests_rx: channel::Receiver, - path_prefixes_to_scan_rx: channel::Receiver, - next_entry_id: Arc, - phase: BackgroundScannerPhase, - watcher: Arc, - settings: WorktreeSettings, - share_private_files: bool, - scanning_enabled: bool, -} - -#[derive(Copy, Clone, PartialEq)] -enum BackgroundScannerPhase { - InitialScan, - EventsReceivedDuringInitialScan, - Events, -} - -impl BackgroundScanner { - async fn run(&mut self, mut fs_events_rx: Pin>>>) { - // If the worktree root does not contain a git repository, then find - // the git repository in an ancestor directory. Find any gitignore files - // in ancestor directories. - let root_abs_path = self.state.lock().await.snapshot.abs_path.clone(); - - let repo = if self.scanning_enabled { - let (ignores, repo) = discover_ancestor_git_repo(self.fs.clone(), &root_abs_path).await; - self.state - .lock() - .await - .snapshot - .ignores_by_parent_abs_path - .extend(ignores); - repo - } else { - None - }; - - let containing_git_repository = if let Some((ancestor_dot_git, work_directory)) = repo - && self.scanning_enabled - { - maybe!(async { - self.state - .lock() - .await - .insert_git_repository_for_path( - work_directory, - ancestor_dot_git.clone().into(), - self.fs.as_ref(), - self.watcher.as_ref(), - ) - .await - .log_err()?; - Some(ancestor_dot_git) - }) - .await - } else { - None - }; - - log::trace!("containing git repository: {containing_git_repository:?}"); - - let mut global_gitignore_events = if let Some(global_gitignore_path) = - &paths::global_gitignore_path() - && self.scanning_enabled - { - let is_file = self.fs.is_file(&global_gitignore_path).await; - self.state.lock().await.snapshot.global_gitignore = if is_file { - build_gitignore(global_gitignore_path, self.fs.as_ref()) - .await - .ok() - .map(Arc::new) - } else { - None - }; - if is_file - || matches!(global_gitignore_path.parent(), Some(path) if self.fs.is_dir(path).await) - { - self.fs - .watch(global_gitignore_path, FS_WATCH_LATENCY) - .await - .0 - } else { - Box::pin(futures::stream::pending()) - } - } else { - self.state.lock().await.snapshot.global_gitignore = None; - Box::pin(futures::stream::pending()) - }; - - let (scan_job_tx, scan_job_rx) = channel::unbounded(); - { - let mut state = self.state.lock().await; - state.snapshot.scan_id += 1; - if let Some(mut root_entry) = state.snapshot.root_entry().cloned() { - let ignore_stack = state - .snapshot - .ignore_stack_for_abs_path(root_abs_path.as_path(), true, self.fs.as_ref()) - .await; - if ignore_stack.is_abs_path_ignored(root_abs_path.as_path(), true) { - root_entry.is_ignored = true; - let mut root_entry = root_entry.clone(); - state.reuse_entry_id(&mut root_entry); - state - .insert_entry(root_entry, self.fs.as_ref(), self.watcher.as_ref()) - .await; - } - if root_entry.is_dir() && self.scanning_enabled { - state - .enqueue_scan_dir( - root_abs_path.as_path().into(), - &root_entry, - &scan_job_tx, - self.fs.as_ref(), - ) - .await; - } - } - }; - - // Perform an initial scan of the directory. - drop(scan_job_tx); - self.scan_dirs(true, scan_job_rx).await; - { - let mut state = self.state.lock().await; - state.snapshot.completed_scan_id = state.snapshot.scan_id; - } - - self.send_status_update(false, SmallVec::new()).await; - - // Process any any FS events that occurred while performing the initial scan. - // For these events, update events cannot be as precise, because we didn't - // have the previous state loaded yet. - self.phase = BackgroundScannerPhase::EventsReceivedDuringInitialScan; - if let Poll::Ready(Some(mut paths)) = futures::poll!(fs_events_rx.next()) { - while let Poll::Ready(Some(more_paths)) = futures::poll!(fs_events_rx.next()) { - paths.extend(more_paths); - } - self.process_events( - paths - .into_iter() - .filter(|e| e.kind.is_some()) - .map(Into::into) - .collect(), - ) - .await; - } - if let Some(abs_path) = containing_git_repository { - self.process_events(vec![abs_path]).await; - } - - // Continue processing events until the worktree is dropped. - self.phase = BackgroundScannerPhase::Events; - - loop { - select_biased! { - // Process any path refresh requests from the worktree. Prioritize - // these before handling changes reported by the filesystem. - request = self.next_scan_request().fuse() => { - let Ok(request) = request else { break }; - if !self.process_scan_request(request, false).await { - return; - } - } - - path_prefix_request = self.path_prefixes_to_scan_rx.recv().fuse() => { - let Ok(request) = path_prefix_request else { break }; - log::trace!("adding path prefix {:?}", request.path); - - let did_scan = self.forcibly_load_paths(std::slice::from_ref(&request.path)).await; - if did_scan { - let abs_path = - { - let mut state = self.state.lock().await; - state.path_prefixes_to_scan.insert(request.path.clone()); - state.snapshot.absolutize(&request.path) - }; - - if let Some(abs_path) = self.fs.canonicalize(&abs_path).await.log_err() { - self.process_events(vec![abs_path]).await; - } - } - self.send_status_update(false, request.done).await; - } - - paths = fs_events_rx.next().fuse() => { - let Some(mut paths) = paths else { break }; - while let Poll::Ready(Some(more_paths)) = futures::poll!(fs_events_rx.next()) { - paths.extend(more_paths); - } - self.process_events(paths.into_iter().filter(|e| e.kind.is_some()).map(Into::into).collect()).await; - } - - paths = global_gitignore_events.next().fuse() => { - match paths.as_deref() { - Some([event, ..]) => { - self.update_global_gitignore(&event.path).await; - } - _ => (), - } - } - } - } - } - - async fn process_scan_request(&self, mut request: ScanRequest, scanning: bool) -> bool { - log::debug!("rescanning paths {:?}", request.relative_paths); - - request.relative_paths.sort_unstable(); - self.forcibly_load_paths(&request.relative_paths).await; - - let root_path = self.state.lock().await.snapshot.abs_path.clone(); - let root_canonical_path = self.fs.canonicalize(root_path.as_path()).await; - let root_canonical_path = match &root_canonical_path { - Ok(path) => SanitizedPath::new(path), - Err(err) => { - log::error!("failed to canonicalize root path {root_path:?}: {err:#}"); - return true; - } - }; - let abs_paths = request - .relative_paths - .iter() - .map(|path| { - if path.file_name().is_some() { - root_canonical_path.as_path().join(path.as_std_path()) - } else { - root_canonical_path.as_path().to_path_buf() - } - }) - .collect::>(); - - { - let mut state = self.state.lock().await; - let is_idle = state.snapshot.completed_scan_id == state.snapshot.scan_id; - state.snapshot.scan_id += 1; - if is_idle { - state.snapshot.completed_scan_id = state.snapshot.scan_id; - } - } - - self.reload_entries_for_paths( - &root_path, - &root_canonical_path, - &request.relative_paths, - abs_paths, - None, - ) - .await; - - self.send_status_update(scanning, request.done).await - } - - async fn process_events(&self, mut abs_paths: Vec) { - log::trace!("process events: {abs_paths:?}"); - let root_path = self.state.lock().await.snapshot.abs_path.clone(); - let root_canonical_path = self.fs.canonicalize(root_path.as_path()).await; - let root_canonical_path = match &root_canonical_path { - Ok(path) => SanitizedPath::new(path), - Err(err) => { - let new_path = self - .state - .lock() - .await - .snapshot - .root_file_handle - .clone() - .and_then(|handle| handle.current_path(&self.fs).log_err()) - .map(|path| SanitizedPath::new_arc(&path)) - .filter(|new_path| *new_path != root_path); - - if let Some(new_path) = new_path { - log::info!( - "root renamed from {} to {}", - root_path.as_path().display(), - new_path.as_path().display() - ); - self.status_updates_tx - .unbounded_send(ScanState::RootUpdated { new_path }) - .ok(); - } else { - log::warn!("root path could not be canonicalized: {:#}", err); - } - return; - } - }; - - // Certain directories may have FS changes, but do not lead to git data changes that Zed cares about. - // Ignore these, to avoid Zed unnecessarily rescanning git metadata. - let skipped_files_in_dot_git = [COMMIT_MESSAGE, INDEX_LOCK]; - let skipped_dirs_in_dot_git = [FSMONITOR_DAEMON, LFS_DIR]; - - let mut relative_paths = Vec::with_capacity(abs_paths.len()); - let mut dot_git_abs_paths = Vec::new(); - abs_paths.sort_unstable(); - abs_paths.dedup_by(|a, b| a.starts_with(b)); - { - let snapshot = &self.state.lock().await.snapshot; - - let mut ranges_to_drop = SmallVec::<[Range; 4]>::new(); - - fn skip_ix(ranges: &mut SmallVec<[Range; 4]>, ix: usize) { - if let Some(last_range) = ranges.last_mut() - && last_range.end == ix - { - last_range.end += 1; - } else { - ranges.push(ix..ix + 1); - } - } - - for (ix, abs_path) in abs_paths.iter().enumerate() { - let abs_path = &SanitizedPath::new(&abs_path); - - let mut is_git_related = false; - let mut dot_git_paths = None; - - for ancestor in abs_path.as_path().ancestors() { - if is_git_dir(ancestor, self.fs.as_ref()).await { - let path_in_git_dir = abs_path - .as_path() - .strip_prefix(ancestor) - .expect("stripping off the ancestor"); - dot_git_paths = Some((ancestor.to_owned(), path_in_git_dir.to_owned())); - break; - } - } - - if let Some((dot_git_abs_path, path_in_git_dir)) = dot_git_paths { - if skipped_files_in_dot_git - .iter() - .any(|skipped| OsStr::new(skipped) == path_in_git_dir.as_path().as_os_str()) - || skipped_dirs_in_dot_git.iter().any(|skipped_git_subdir| { - path_in_git_dir.starts_with(skipped_git_subdir) - }) - { - log::debug!( - "ignoring event {abs_path:?} as it's in the .git directory among skipped files or directories" - ); - skip_ix(&mut ranges_to_drop, ix); - continue; - } - - is_git_related = true; - if !dot_git_abs_paths.contains(&dot_git_abs_path) { - dot_git_abs_paths.push(dot_git_abs_path); - } - } - - let relative_path = if let Ok(path) = abs_path.strip_prefix(&root_canonical_path) - && let Ok(path) = RelPath::new(path, PathStyle::local()) - { - path - } else { - if is_git_related { - log::debug!( - "ignoring event {abs_path:?}, since it's in git dir outside of root path {root_canonical_path:?}", - ); - } else { - log::error!( - "ignoring event {abs_path:?} outside of root path {root_canonical_path:?}", - ); - } - skip_ix(&mut ranges_to_drop, ix); - continue; - }; - - if abs_path.file_name() == Some(OsStr::new(GITIGNORE)) { - for (_, repo) in snapshot - .git_repositories - .iter() - .filter(|(_, repo)| repo.directory_contains(&relative_path)) - { - if !dot_git_abs_paths.iter().any(|dot_git_abs_path| { - dot_git_abs_path == repo.common_dir_abs_path.as_ref() - }) { - dot_git_abs_paths.push(repo.common_dir_abs_path.to_path_buf()); - } - } - } - - let parent_dir_is_loaded = relative_path.parent().is_none_or(|parent| { - snapshot - .entry_for_path(parent) - .is_some_and(|entry| entry.kind == EntryKind::Dir) - }); - if !parent_dir_is_loaded { - log::debug!("ignoring event {relative_path:?} within unloaded directory"); - skip_ix(&mut ranges_to_drop, ix); - continue; - } - - if self.settings.is_path_excluded(&relative_path) { - if !is_git_related { - log::debug!("ignoring FS event for excluded path {relative_path:?}"); - } - skip_ix(&mut ranges_to_drop, ix); - continue; - } - - relative_paths.push(relative_path.into_arc()); - } - - for range_to_drop in ranges_to_drop.into_iter().rev() { - abs_paths.drain(range_to_drop); - } - } - - if relative_paths.is_empty() && dot_git_abs_paths.is_empty() { - return; - } - - self.state.lock().await.snapshot.scan_id += 1; - - let (scan_job_tx, scan_job_rx) = channel::unbounded(); - log::debug!("received fs events {:?}", relative_paths); - self.reload_entries_for_paths( - &root_path, - &root_canonical_path, - &relative_paths, - abs_paths, - Some(scan_job_tx.clone()), - ) - .await; - - let affected_repo_roots = if !dot_git_abs_paths.is_empty() { - self.update_git_repositories(dot_git_abs_paths).await - } else { - Vec::new() - }; - - { - let mut ignores_to_update = self.ignores_needing_update().await; - ignores_to_update.extend(affected_repo_roots); - let ignores_to_update = self.order_ignores(ignores_to_update).await; - let snapshot = self.state.lock().await.snapshot.clone(); - self.update_ignore_statuses_for_paths(scan_job_tx, snapshot, ignores_to_update) - .await; - self.scan_dirs(false, scan_job_rx).await; - } - - { - let mut state = self.state.lock().await; - state.snapshot.completed_scan_id = state.snapshot.scan_id; - for (_, entry) in mem::take(&mut state.removed_entries) { - state.scanned_dirs.remove(&entry.id); - } - } - self.send_status_update(false, SmallVec::new()).await; - } - - async fn update_global_gitignore(&self, abs_path: &Path) { - let ignore = build_gitignore(abs_path, self.fs.as_ref()) - .await - .log_err() - .map(Arc::new); - let (prev_snapshot, ignore_stack, abs_path) = { - let mut state = self.state.lock().await; - state.snapshot.global_gitignore = ignore; - let abs_path = state.snapshot.abs_path().clone(); - let ignore_stack = state - .snapshot - .ignore_stack_for_abs_path(&abs_path, true, self.fs.as_ref()) - .await; - (state.snapshot.clone(), ignore_stack, abs_path) - }; - let (scan_job_tx, scan_job_rx) = channel::unbounded(); - self.update_ignore_statuses_for_paths( - scan_job_tx, - prev_snapshot, - vec![(abs_path, ignore_stack)], - ) - .await; - self.scan_dirs(false, scan_job_rx).await; - self.send_status_update(false, SmallVec::new()).await; - } - - async fn forcibly_load_paths(&self, paths: &[Arc]) -> bool { - let (scan_job_tx, scan_job_rx) = channel::unbounded(); - { - let mut state = self.state.lock().await; - let root_path = state.snapshot.abs_path.clone(); - for path in paths { - for ancestor in path.ancestors() { - if let Some(entry) = state.snapshot.entry_for_path(ancestor) - && entry.kind == EntryKind::UnloadedDir - { - let abs_path = root_path.join(ancestor.as_std_path()); - state - .enqueue_scan_dir( - abs_path.into(), - entry, - &scan_job_tx, - self.fs.as_ref(), - ) - .await; - state.paths_to_scan.insert(path.clone()); - break; - } - } - } - drop(scan_job_tx); - } - while let Ok(job) = scan_job_rx.recv().await { - self.scan_dir(&job).await.log_err(); - } - - !mem::take(&mut self.state.lock().await.paths_to_scan).is_empty() - } - - async fn scan_dirs( - &self, - enable_progress_updates: bool, - scan_jobs_rx: channel::Receiver, - ) { - if self - .status_updates_tx - .unbounded_send(ScanState::Started) - .is_err() - { - return; - } - - let progress_update_count = AtomicUsize::new(0); - self.executor - .scoped_priority(Priority::Low, |scope| { - for _ in 0..self.executor.num_cpus() { - scope.spawn(async { - let mut last_progress_update_count = 0; - let progress_update_timer = self.progress_timer(enable_progress_updates).fuse(); - futures::pin_mut!(progress_update_timer); - - loop { - select_biased! { - // Process any path refresh requests before moving on to process - // the scan queue, so that user operations are prioritized. - request = self.next_scan_request().fuse() => { - let Ok(request) = request else { break }; - if !self.process_scan_request(request, true).await { - return; - } - } - - // Send periodic progress updates to the worktree. Use an atomic counter - // to ensure that only one of the workers sends a progress update after - // the update interval elapses. - _ = progress_update_timer => { - match progress_update_count.compare_exchange( - last_progress_update_count, - last_progress_update_count + 1, - SeqCst, - SeqCst - ) { - Ok(_) => { - last_progress_update_count += 1; - self.send_status_update(true, SmallVec::new()).await; - } - Err(count) => { - last_progress_update_count = count; - } - } - progress_update_timer.set(self.progress_timer(enable_progress_updates).fuse()); - } - - // Recursively load directories from the file system. - job = scan_jobs_rx.recv().fuse() => { - let Ok(job) = job else { break }; - if let Err(err) = self.scan_dir(&job).await - && job.path.is_empty() { - log::error!("error scanning directory {:?}: {}", job.abs_path, err); - } - } - } - } - }); - } - }) - .await; - } - - async fn send_status_update( - &self, - scanning: bool, - barrier: SmallVec<[barrier::Sender; 1]>, - ) -> bool { - let mut state = self.state.lock().await; - if state.changed_paths.is_empty() && scanning { - return true; - } - - let new_snapshot = state.snapshot.clone(); - let old_snapshot = mem::replace(&mut state.prev_snapshot, new_snapshot.snapshot.clone()); - let changes = build_diff( - self.phase, - &old_snapshot, - &new_snapshot, - &state.changed_paths, - ); - state.changed_paths.clear(); - - self.status_updates_tx - .unbounded_send(ScanState::Updated { - snapshot: new_snapshot, - changes, - scanning, - barrier, - }) - .is_ok() - } - - async fn scan_dir(&self, job: &ScanJob) -> Result<()> { - let root_abs_path; - let root_char_bag; - { - let snapshot = &self.state.lock().await.snapshot; - if self.settings.is_path_excluded(&job.path) { - log::error!("skipping excluded directory {:?}", job.path); - return Ok(()); - } - log::trace!("scanning directory {:?}", job.path); - root_abs_path = snapshot.abs_path().clone(); - root_char_bag = snapshot.root_char_bag; - } - - let next_entry_id = self.next_entry_id.clone(); - let mut ignore_stack = job.ignore_stack.clone(); - let mut new_ignore = None; - let mut root_canonical_path = None; - let mut new_entries: Vec = Vec::new(); - let mut new_jobs: Vec> = Vec::new(); - let mut child_paths = self - .fs - .read_dir(&job.abs_path) - .await? - .filter_map(|entry| async { - match entry { - Ok(entry) => Some(entry), - Err(error) => { - log::error!("error processing entry {:?}", error); - None - } - } - }) - .collect::>() - .await; - - // Ensure that .git and .gitignore are processed first. - swap_to_front(&mut child_paths, GITIGNORE); - swap_to_front(&mut child_paths, DOT_GIT); - - if let Some(path) = child_paths.first() - && path.ends_with(DOT_GIT) - { - ignore_stack.repo_root = Some(job.abs_path.clone()); - } - - for child_abs_path in child_paths { - let child_abs_path: Arc = child_abs_path.into(); - let child_name = child_abs_path.file_name().unwrap(); - let Some(child_path) = child_name - .to_str() - .and_then(|name| Some(job.path.join(RelPath::unix(name).ok()?))) - else { - continue; - }; - - if child_name == DOT_GIT { - let mut state = self.state.lock().await; - state - .insert_git_repository( - child_path.clone(), - self.fs.as_ref(), - self.watcher.as_ref(), - ) - .await; - } else if child_name == GITIGNORE { - match build_gitignore(&child_abs_path, self.fs.as_ref()).await { - Ok(ignore) => { - let ignore = Arc::new(ignore); - ignore_stack = ignore_stack.append(job.abs_path.clone(), ignore.clone()); - new_ignore = Some(ignore); - } - Err(error) => { - log::error!( - "error loading .gitignore file {:?} - {:?}", - child_name, - error - ); - } - } - } - - if self.settings.is_path_excluded(&child_path) { - log::debug!("skipping excluded child entry {child_path:?}"); - self.state.lock().await.remove_path(&child_path); - continue; - } - - let child_metadata = match self.fs.metadata(&child_abs_path).await { - Ok(Some(metadata)) => metadata, - Ok(None) => continue, - Err(err) => { - log::error!("error processing {child_abs_path:?}: {err:?}"); - continue; - } - }; - - let mut child_entry = Entry::new( - child_path.clone(), - &child_metadata, - ProjectEntryId::new(&next_entry_id), - root_char_bag, - None, - ); - - if job.is_external { - child_entry.is_external = true; - } else if child_metadata.is_symlink { - let canonical_path = match self.fs.canonicalize(&child_abs_path).await { - Ok(path) => path, - Err(err) => { - log::error!("error reading target of symlink {child_abs_path:?}: {err:#}",); - continue; - } - }; - - // lazily canonicalize the root path in order to determine if - // symlinks point outside of the worktree. - let root_canonical_path = match &root_canonical_path { - Some(path) => path, - None => match self.fs.canonicalize(&root_abs_path).await { - Ok(path) => root_canonical_path.insert(path), - Err(err) => { - log::error!("error canonicalizing root {:?}: {:?}", root_abs_path, err); - continue; - } - }, - }; - - if !canonical_path.starts_with(root_canonical_path) { - child_entry.is_external = true; - } - - child_entry.canonical_path = Some(canonical_path.into()); - } - - if child_entry.is_dir() { - child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, true); - child_entry.is_always_included = - self.settings.is_path_always_included(&child_path, true); - - // Avoid recursing until crash in the case of a recursive symlink - if job.ancestor_inodes.contains(&child_entry.inode) { - new_jobs.push(None); - } else { - let mut ancestor_inodes = job.ancestor_inodes.clone(); - ancestor_inodes.insert(child_entry.inode); - - new_jobs.push(Some(ScanJob { - abs_path: child_abs_path.clone(), - path: child_path, - is_external: child_entry.is_external, - ignore_stack: if child_entry.is_ignored { - IgnoreStack::all() - } else { - ignore_stack.clone() - }, - ancestor_inodes, - scan_queue: job.scan_queue.clone(), - })); - } - } else { - child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, false); - child_entry.is_always_included = - self.settings.is_path_always_included(&child_path, false); - } - - { - let relative_path = job - .path - .join(RelPath::unix(child_name.to_str().unwrap()).unwrap()); - if self.is_path_private(&relative_path) { - log::debug!("detected private file: {relative_path:?}"); - child_entry.is_private = true; - } - if self.settings.is_path_hidden(&relative_path) { - log::debug!("detected hidden file: {relative_path:?}"); - child_entry.is_hidden = true; - } - } - - new_entries.push(child_entry); - } - - let mut state = self.state.lock().await; - - // Identify any subdirectories that should not be scanned. - let mut job_ix = 0; - for entry in &mut new_entries { - state.reuse_entry_id(entry); - if entry.is_dir() { - if state.should_scan_directory(entry) { - job_ix += 1; - } else { - log::debug!("defer scanning directory {:?}", entry.path); - entry.kind = EntryKind::UnloadedDir; - new_jobs.remove(job_ix); - } - } - if entry.is_always_included { - state - .snapshot - .always_included_entries - .push(entry.path.clone()); - } - } - - state.populate_dir(job.path.clone(), new_entries, new_ignore); - self.watcher.add(job.abs_path.as_ref()).log_err(); - - for new_job in new_jobs.into_iter().flatten() { - job.scan_queue - .try_send(new_job) - .expect("channel is unbounded"); - } - - Ok(()) - } - - /// All list arguments should be sorted before calling this function - async fn reload_entries_for_paths( - &self, - root_abs_path: &SanitizedPath, - root_canonical_path: &SanitizedPath, - relative_paths: &[Arc], - abs_paths: Vec, - scan_queue_tx: Option>, - ) { - // grab metadata for all requested paths - let metadata = futures::future::join_all( - abs_paths - .iter() - .map(|abs_path| async move { - let metadata = self.fs.metadata(abs_path).await?; - if let Some(metadata) = metadata { - let canonical_path = self.fs.canonicalize(abs_path).await?; - - // If we're on a case-insensitive filesystem (default on macOS), we want - // to only ignore metadata for non-symlink files if their absolute-path matches - // the canonical-path. - // Because if not, this might be a case-only-renaming (`mv test.txt TEST.TXT`) - // and we want to ignore the metadata for the old path (`test.txt`) so it's - // treated as removed. - if !self.fs_case_sensitive && !metadata.is_symlink { - let canonical_file_name = canonical_path.file_name(); - let file_name = abs_path.file_name(); - if canonical_file_name != file_name { - return Ok(None); - } - } - - anyhow::Ok(Some((metadata, SanitizedPath::new_arc(&canonical_path)))) - } else { - Ok(None) - } - }) - .collect::>(), - ) - .await; - - let mut new_ancestor_repo = if relative_paths.iter().any(|path| path.is_empty()) { - Some(discover_ancestor_git_repo(self.fs.clone(), &root_abs_path).await) - } else { - None - }; - - let mut state = self.state.lock().await; - let doing_recursive_update = scan_queue_tx.is_some(); - - // Remove any entries for paths that no longer exist or are being recursively - // refreshed. Do this before adding any new entries, so that renames can be - // detected regardless of the order of the paths. - for (path, metadata) in relative_paths.iter().zip(metadata.iter()) { - if matches!(metadata, Ok(None)) || doing_recursive_update { - state.remove_path(path); - } - } - - for (path, metadata) in relative_paths.iter().zip(metadata.into_iter()) { - let abs_path: Arc = root_abs_path.join(path.as_std_path()).into(); - match metadata { - Ok(Some((metadata, canonical_path))) => { - let ignore_stack = state - .snapshot - .ignore_stack_for_abs_path(&abs_path, metadata.is_dir, self.fs.as_ref()) - .await; - let is_external = !canonical_path.starts_with(&root_canonical_path); - let entry_id = state.entry_id_for(self.next_entry_id.as_ref(), path, &metadata); - let mut fs_entry = Entry::new( - path.clone(), - &metadata, - entry_id, - state.snapshot.root_char_bag, - if metadata.is_symlink { - Some(canonical_path.as_path().to_path_buf().into()) - } else { - None - }, - ); - - let is_dir = fs_entry.is_dir(); - fs_entry.is_ignored = ignore_stack.is_abs_path_ignored(&abs_path, is_dir); - fs_entry.is_external = is_external; - fs_entry.is_private = self.is_path_private(path); - fs_entry.is_always_included = - self.settings.is_path_always_included(path, is_dir); - fs_entry.is_hidden = self.settings.is_path_hidden(path); - - if let (Some(scan_queue_tx), true) = (&scan_queue_tx, is_dir) { - if state.should_scan_directory(&fs_entry) - || (fs_entry.path.is_empty() - && abs_path.file_name() == Some(OsStr::new(DOT_GIT))) - { - state - .enqueue_scan_dir( - abs_path, - &fs_entry, - scan_queue_tx, - self.fs.as_ref(), - ) - .await; - } else { - fs_entry.kind = EntryKind::UnloadedDir; - } - } - - state - .insert_entry(fs_entry.clone(), self.fs.as_ref(), self.watcher.as_ref()) - .await; - - if path.is_empty() - && let Some((ignores, repo)) = new_ancestor_repo.take() - { - log::trace!("updating ancestor git repository"); - state.snapshot.ignores_by_parent_abs_path.extend(ignores); - if let Some((ancestor_dot_git, work_directory)) = repo { - state - .insert_git_repository_for_path( - work_directory, - ancestor_dot_git.into(), - self.fs.as_ref(), - self.watcher.as_ref(), - ) - .await - .log_err(); - } - } - } - Ok(None) => { - self.remove_repo_path(path.clone(), &mut state.snapshot); - } - Err(err) => { - log::error!("error reading file {abs_path:?} on event: {err:#}"); - } - } - } - - util::extend_sorted( - &mut state.changed_paths, - relative_paths.iter().cloned(), - usize::MAX, - Ord::cmp, - ); - } - - fn remove_repo_path(&self, path: Arc, snapshot: &mut LocalSnapshot) -> Option<()> { - if !path.components().any(|component| component == DOT_GIT) - && let Some(local_repo) = snapshot.local_repo_for_work_directory_path(&path) - { - let id = local_repo.work_directory_id; - log::debug!("remove repo path: {:?}", path); - snapshot.git_repositories.remove(&id); - return Some(()); - } - - Some(()) - } - - async fn update_ignore_statuses_for_paths( - &self, - scan_job_tx: Sender, - prev_snapshot: LocalSnapshot, - ignores_to_update: Vec<(Arc, IgnoreStack)>, - ) { - let (ignore_queue_tx, ignore_queue_rx) = channel::unbounded(); - { - for (parent_abs_path, ignore_stack) in ignores_to_update { - ignore_queue_tx - .send_blocking(UpdateIgnoreStatusJob { - abs_path: parent_abs_path, - ignore_stack, - ignore_queue: ignore_queue_tx.clone(), - scan_queue: scan_job_tx.clone(), - }) - .unwrap(); - } - } - drop(ignore_queue_tx); - - self.executor - .scoped(|scope| { - for _ in 0..self.executor.num_cpus() { - scope.spawn(async { - loop { - select_biased! { - // Process any path refresh requests before moving on to process - // the queue of ignore statuses. - request = self.next_scan_request().fuse() => { - let Ok(request) = request else { break }; - if !self.process_scan_request(request, true).await { - return; - } - } - - // Recursively process directories whose ignores have changed. - job = ignore_queue_rx.recv().fuse() => { - let Ok(job) = job else { break }; - self.update_ignore_status(job, &prev_snapshot).await; - } - } - } - }); - } - }) - .await; - } - - async fn ignores_needing_update(&self) -> Vec> { - let mut ignores_to_update = Vec::new(); - - { - let snapshot = &mut self.state.lock().await.snapshot; - let abs_path = snapshot.abs_path.clone(); - snapshot - .ignores_by_parent_abs_path - .retain(|parent_abs_path, (_, needs_update)| { - if let Ok(parent_path) = parent_abs_path.strip_prefix(abs_path.as_path()) - && let Some(parent_path) = - RelPath::new(&parent_path, PathStyle::local()).log_err() - { - if *needs_update { - *needs_update = false; - if snapshot.snapshot.entry_for_path(&parent_path).is_some() { - ignores_to_update.push(parent_abs_path.clone()); - } - } - - let ignore_path = parent_path.join(RelPath::unix(GITIGNORE).unwrap()); - if snapshot.snapshot.entry_for_path(&ignore_path).is_none() { - return false; - } - } - true - }); - } - - ignores_to_update - } - - async fn order_ignores(&self, mut ignores: Vec>) -> Vec<(Arc, IgnoreStack)> { - let fs = self.fs.clone(); - let snapshot = self.state.lock().await.snapshot.clone(); - ignores.sort_unstable(); - let mut ignores_to_update = ignores.into_iter().peekable(); - - let mut result = vec![]; - while let Some(parent_abs_path) = ignores_to_update.next() { - while ignores_to_update - .peek() - .map_or(false, |p| p.starts_with(&parent_abs_path)) - { - ignores_to_update.next().unwrap(); - } - let ignore_stack = snapshot - .ignore_stack_for_abs_path(&parent_abs_path, true, fs.as_ref()) - .await; - result.push((parent_abs_path, ignore_stack)); - } - - result - } - - async fn update_ignore_status(&self, job: UpdateIgnoreStatusJob, snapshot: &LocalSnapshot) { - log::trace!("update ignore status {:?}", job.abs_path); - - let mut ignore_stack = job.ignore_stack; - if let Some((ignore, _)) = snapshot.ignores_by_parent_abs_path.get(&job.abs_path) { - ignore_stack = ignore_stack.append(job.abs_path.clone(), ignore.clone()); - } - - let mut entries_by_id_edits = Vec::new(); - let mut entries_by_path_edits = Vec::new(); - let Some(path) = job - .abs_path - .strip_prefix(snapshot.abs_path.as_path()) - .map_err(|_| { - anyhow::anyhow!( - "Failed to strip prefix '{}' from path '{}'", - snapshot.abs_path.as_path().display(), - job.abs_path.display() - ) - }) - .log_err() - else { - return; - }; - - let Some(path) = RelPath::new(&path, PathStyle::local()).log_err() else { - return; - }; - - if let Ok(Some(metadata)) = self.fs.metadata(&job.abs_path.join(DOT_GIT)).await - && metadata.is_dir - { - ignore_stack.repo_root = Some(job.abs_path.clone()); - } - - for mut entry in snapshot.child_entries(&path).cloned() { - let was_ignored = entry.is_ignored; - let abs_path: Arc = snapshot.absolutize(&entry.path).into(); - entry.is_ignored = ignore_stack.is_abs_path_ignored(&abs_path, entry.is_dir()); - - if entry.is_dir() { - let child_ignore_stack = if entry.is_ignored { - IgnoreStack::all() - } else { - ignore_stack.clone() - }; - - // Scan any directories that were previously ignored and weren't previously scanned. - if was_ignored && !entry.is_ignored && entry.kind.is_unloaded() { - let state = self.state.lock().await; - if state.should_scan_directory(&entry) { - state - .enqueue_scan_dir( - abs_path.clone(), - &entry, - &job.scan_queue, - self.fs.as_ref(), - ) - .await; - } - } - - job.ignore_queue - .send(UpdateIgnoreStatusJob { - abs_path: abs_path.clone(), - ignore_stack: child_ignore_stack, - ignore_queue: job.ignore_queue.clone(), - scan_queue: job.scan_queue.clone(), - }) - .await - .unwrap(); - } - - if entry.is_ignored != was_ignored { - let mut path_entry = snapshot.entries_by_id.get(&entry.id, ()).unwrap().clone(); - path_entry.scan_id = snapshot.scan_id; - path_entry.is_ignored = entry.is_ignored; - entries_by_id_edits.push(Edit::Insert(path_entry)); - entries_by_path_edits.push(Edit::Insert(entry)); - } - } - - let state = &mut self.state.lock().await; - for edit in &entries_by_path_edits { - if let Edit::Insert(entry) = edit - && let Err(ix) = state.changed_paths.binary_search(&entry.path) - { - state.changed_paths.insert(ix, entry.path.clone()); - } - } - - state - .snapshot - .entries_by_path - .edit(entries_by_path_edits, ()); - state.snapshot.entries_by_id.edit(entries_by_id_edits, ()); - } - - async fn update_git_repositories(&self, dot_git_paths: Vec) -> Vec> { - log::trace!("reloading repositories: {dot_git_paths:?}"); - let mut state = self.state.lock().await; - let scan_id = state.snapshot.scan_id; - let mut affected_repo_roots = Vec::new(); - for dot_git_dir in dot_git_paths { - let existing_repository_entry = - state - .snapshot - .git_repositories - .iter() - .find_map(|(_, repo)| { - let dot_git_dir = SanitizedPath::new(&dot_git_dir); - if SanitizedPath::new(repo.common_dir_abs_path.as_ref()) == dot_git_dir - || SanitizedPath::new(repo.repository_dir_abs_path.as_ref()) - == dot_git_dir - { - Some(repo.clone()) - } else { - None - } - }); - - match existing_repository_entry { - None => { - let Ok(relative) = dot_git_dir.strip_prefix(state.snapshot.abs_path()) else { - debug_panic!( - "update_git_repositories called with .git directory outside the worktree root" - ); - return Vec::new(); - }; - affected_repo_roots.push(dot_git_dir.parent().unwrap().into()); - state - .insert_git_repository( - RelPath::new(relative, PathStyle::local()) - .unwrap() - .into_arc(), - self.fs.as_ref(), - self.watcher.as_ref(), - ) - .await; - } - Some(local_repository) => { - state.snapshot.git_repositories.update( - &local_repository.work_directory_id, - |entry| { - entry.git_dir_scan_id = scan_id; - }, - ); - } - }; - } - - // Remove any git repositories whose .git entry no longer exists. - let snapshot = &mut state.snapshot; - let mut ids_to_preserve = HashSet::default(); - for (&work_directory_id, entry) in snapshot.git_repositories.iter() { - let exists_in_snapshot = - snapshot - .entry_for_id(work_directory_id) - .is_some_and(|entry| { - snapshot - .entry_for_path(&entry.path.join(RelPath::unix(DOT_GIT).unwrap())) - .is_some() - }); - - if exists_in_snapshot - || matches!( - self.fs.metadata(&entry.common_dir_abs_path).await, - Ok(Some(_)) - ) - { - ids_to_preserve.insert(work_directory_id); - } - } - - snapshot - .git_repositories - .retain(|work_directory_id, entry| { - let preserve = ids_to_preserve.contains(work_directory_id); - if !preserve { - affected_repo_roots.push(entry.dot_git_abs_path.parent().unwrap().into()); - } - preserve - }); - - affected_repo_roots - } - - async fn progress_timer(&self, running: bool) { - if !running { - return futures::future::pending().await; - } - - #[cfg(any(test, feature = "test-support"))] - if self.fs.is_fake() { - return self.executor.simulate_random_delay().await; - } - - smol::Timer::after(FS_WATCH_LATENCY).await; - } - - fn is_path_private(&self, path: &RelPath) -> bool { - !self.share_private_files && self.settings.is_path_private(path) - } - - async fn next_scan_request(&self) -> Result { - let mut request = self.scan_requests_rx.recv().await?; - while let Ok(next_request) = self.scan_requests_rx.try_recv() { - request.relative_paths.extend(next_request.relative_paths); - request.done.extend(next_request.done); - } - Ok(request) - } -} - -async fn discover_ancestor_git_repo( - fs: Arc, - root_abs_path: &SanitizedPath, -) -> ( - HashMap, (Arc, bool)>, - Option<(PathBuf, WorkDirectory)>, -) { - let mut ignores = HashMap::default(); - for (index, ancestor) in root_abs_path.as_path().ancestors().enumerate() { - if index != 0 { - if ancestor == paths::home_dir() { - // Unless $HOME is itself the worktree root, don't consider it as a - // containing git repository---expensive and likely unwanted. - break; - } else if let Ok(ignore) = build_gitignore(&ancestor.join(GITIGNORE), fs.as_ref()).await - { - ignores.insert(ancestor.into(), (ignore.into(), false)); - } - } - - let ancestor_dot_git = ancestor.join(DOT_GIT); - log::trace!("considering ancestor: {ancestor_dot_git:?}"); - // Check whether the directory or file called `.git` exists (in the - // case of worktrees it's a file.) - if fs - .metadata(&ancestor_dot_git) - .await - .is_ok_and(|metadata| metadata.is_some()) - { - if index != 0 { - // We canonicalize, since the FS events use the canonicalized path. - if let Some(ancestor_dot_git) = fs.canonicalize(&ancestor_dot_git).await.log_err() { - let location_in_repo = root_abs_path - .as_path() - .strip_prefix(ancestor) - .unwrap() - .into(); - log::info!("inserting parent git repo for this worktree: {location_in_repo:?}"); - // We associate the external git repo with our root folder and - // also mark where in the git repo the root folder is located. - return ( - ignores, - Some(( - ancestor_dot_git, - WorkDirectory::AboveProject { - absolute_path: ancestor.into(), - location_in_repo, - }, - )), - ); - }; - } - - // Reached root of git repository. - break; - } - } - - (ignores, None) -} - -fn build_diff( - phase: BackgroundScannerPhase, - old_snapshot: &Snapshot, - new_snapshot: &Snapshot, - event_paths: &[Arc], -) -> UpdatedEntriesSet { - use BackgroundScannerPhase::*; - use PathChange::{Added, AddedOrUpdated, Loaded, Removed, Updated}; - - // Identify which paths have changed. Use the known set of changed - // parent paths to optimize the search. - let mut changes = Vec::new(); - let mut old_paths = old_snapshot.entries_by_path.cursor::(()); - let mut new_paths = new_snapshot.entries_by_path.cursor::(()); - let mut last_newly_loaded_dir_path = None; - old_paths.next(); - new_paths.next(); - for path in event_paths { - let path = PathKey(path.clone()); - if old_paths.item().is_some_and(|e| e.path < path.0) { - old_paths.seek_forward(&path, Bias::Left); - } - if new_paths.item().is_some_and(|e| e.path < path.0) { - new_paths.seek_forward(&path, Bias::Left); - } - loop { - match (old_paths.item(), new_paths.item()) { - (Some(old_entry), Some(new_entry)) => { - if old_entry.path > path.0 - && new_entry.path > path.0 - && !old_entry.path.starts_with(&path.0) - && !new_entry.path.starts_with(&path.0) - { - break; - } - - match Ord::cmp(&old_entry.path, &new_entry.path) { - Ordering::Less => { - changes.push((old_entry.path.clone(), old_entry.id, Removed)); - old_paths.next(); - } - Ordering::Equal => { - if phase == EventsReceivedDuringInitialScan { - if old_entry.id != new_entry.id { - changes.push((old_entry.path.clone(), old_entry.id, Removed)); - } - // If the worktree was not fully initialized when this event was generated, - // we can't know whether this entry was added during the scan or whether - // it was merely updated. - changes.push(( - new_entry.path.clone(), - new_entry.id, - AddedOrUpdated, - )); - } else if old_entry.id != new_entry.id { - changes.push((old_entry.path.clone(), old_entry.id, Removed)); - changes.push((new_entry.path.clone(), new_entry.id, Added)); - } else if old_entry != new_entry { - if old_entry.kind.is_unloaded() { - last_newly_loaded_dir_path = Some(&new_entry.path); - changes.push((new_entry.path.clone(), new_entry.id, Loaded)); - } else { - changes.push((new_entry.path.clone(), new_entry.id, Updated)); - } - } - old_paths.next(); - new_paths.next(); - } - Ordering::Greater => { - let is_newly_loaded = phase == InitialScan - || last_newly_loaded_dir_path - .as_ref() - .is_some_and(|dir| new_entry.path.starts_with(dir)); - changes.push(( - new_entry.path.clone(), - new_entry.id, - if is_newly_loaded { Loaded } else { Added }, - )); - new_paths.next(); - } - } - } - (Some(old_entry), None) => { - changes.push((old_entry.path.clone(), old_entry.id, Removed)); - old_paths.next(); - } - (None, Some(new_entry)) => { - let is_newly_loaded = phase == InitialScan - || last_newly_loaded_dir_path - .as_ref() - .is_some_and(|dir| new_entry.path.starts_with(dir)); - changes.push(( - new_entry.path.clone(), - new_entry.id, - if is_newly_loaded { Loaded } else { Added }, - )); - new_paths.next(); - } - (None, None) => break, - } - } - } - - changes.into() -} - -fn swap_to_front(child_paths: &mut Vec, file: &str) { - let position = child_paths - .iter() - .position(|path| path.file_name().unwrap() == file); - if let Some(position) = position { - let temp = child_paths.remove(position); - child_paths.insert(0, temp); - } -} - -fn char_bag_for_path(root_char_bag: CharBag, path: &RelPath) -> CharBag { - let mut result = root_char_bag; - result.extend(path.as_unix_str().chars().map(|c| c.to_ascii_lowercase())); - result -} - -#[derive(Debug)] -struct ScanJob { - abs_path: Arc, - path: Arc, - ignore_stack: IgnoreStack, - scan_queue: Sender, - ancestor_inodes: TreeSet, - is_external: bool, -} - -struct UpdateIgnoreStatusJob { - abs_path: Arc, - ignore_stack: IgnoreStack, - ignore_queue: Sender, - scan_queue: Sender, -} - -pub trait WorktreeModelHandle { - #[cfg(any(test, feature = "test-support"))] - fn flush_fs_events<'a>( - &self, - cx: &'a mut gpui::TestAppContext, - ) -> futures::future::LocalBoxFuture<'a, ()>; - - #[cfg(any(test, feature = "test-support"))] - fn flush_fs_events_in_root_git_repository<'a>( - &self, - cx: &'a mut gpui::TestAppContext, - ) -> futures::future::LocalBoxFuture<'a, ()>; -} - -impl WorktreeModelHandle for Entity { - // When the worktree's FS event stream sometimes delivers "redundant" events for FS changes that - // occurred before the worktree was constructed. These events can cause the worktree to perform - // extra directory scans, and emit extra scan-state notifications. - // - // This function mutates the worktree's directory and waits for those mutations to be picked up, - // to ensure that all redundant FS events have already been processed. - #[cfg(any(test, feature = "test-support"))] - fn flush_fs_events<'a>( - &self, - cx: &'a mut gpui::TestAppContext, - ) -> futures::future::LocalBoxFuture<'a, ()> { - let file_name = "fs-event-sentinel"; - - let tree = self.clone(); - let (fs, root_path) = self.read_with(cx, |tree, _| { - let tree = tree.as_local().unwrap(); - (tree.fs.clone(), tree.abs_path.clone()) - }); - - async move { - fs.create_file(&root_path.join(file_name), Default::default()) - .await - .unwrap(); - - let mut events = cx.events(&tree); - while events.next().await.is_some() { - if tree.read_with(cx, |tree, _| { - tree.entry_for_path(RelPath::unix(file_name).unwrap()) - .is_some() - }) { - break; - } - } - - fs.remove_file(&root_path.join(file_name), Default::default()) - .await - .unwrap(); - while events.next().await.is_some() { - if tree.read_with(cx, |tree, _| { - tree.entry_for_path(RelPath::unix(file_name).unwrap()) - .is_none() - }) { - break; - } - } - - cx.update(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - } - .boxed_local() - } - - // This function is similar to flush_fs_events, except that it waits for events to be flushed in - // the .git folder of the root repository. - // The reason for its existence is that a repository's .git folder might live *outside* of the - // worktree and thus its FS events might go through a different path. - // In order to flush those, we need to create artificial events in the .git folder and wait - // for the repository to be reloaded. - #[cfg(any(test, feature = "test-support"))] - fn flush_fs_events_in_root_git_repository<'a>( - &self, - cx: &'a mut gpui::TestAppContext, - ) -> futures::future::LocalBoxFuture<'a, ()> { - let file_name = "fs-event-sentinel"; - - let tree = self.clone(); - let (fs, root_path, mut git_dir_scan_id) = self.read_with(cx, |tree, _| { - let tree = tree.as_local().unwrap(); - let local_repo_entry = tree - .git_repositories - .values() - .min_by_key(|local_repo_entry| local_repo_entry.work_directory.clone()) - .unwrap(); - ( - tree.fs.clone(), - local_repo_entry.common_dir_abs_path.clone(), - local_repo_entry.git_dir_scan_id, - ) - }); - - let scan_id_increased = |tree: &mut Worktree, git_dir_scan_id: &mut usize| { - let tree = tree.as_local().unwrap(); - // let repository = tree.repositories.first().unwrap(); - let local_repo_entry = tree - .git_repositories - .values() - .min_by_key(|local_repo_entry| local_repo_entry.work_directory.clone()) - .unwrap(); - - if local_repo_entry.git_dir_scan_id > *git_dir_scan_id { - *git_dir_scan_id = local_repo_entry.git_dir_scan_id; - true - } else { - false - } - }; - - async move { - fs.create_file(&root_path.join(file_name), Default::default()) - .await - .unwrap(); - - let mut events = cx.events(&tree); - while events.next().await.is_some() { - if tree.update(cx, |tree, _| scan_id_increased(tree, &mut git_dir_scan_id)) { - break; - } - } - - fs.remove_file(&root_path.join(file_name), Default::default()) - .await - .unwrap(); - - while events.next().await.is_some() { - if tree.update(cx, |tree, _| scan_id_increased(tree, &mut git_dir_scan_id)) { - break; - } - } - - cx.update(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - } - .boxed_local() - } -} - -#[derive(Clone, Debug)] -struct TraversalProgress<'a> { - max_path: &'a RelPath, - count: usize, - non_ignored_count: usize, - file_count: usize, - non_ignored_file_count: usize, -} - -impl TraversalProgress<'_> { - fn count(&self, include_files: bool, include_dirs: bool, include_ignored: bool) -> usize { - match (include_files, include_dirs, include_ignored) { - (true, true, true) => self.count, - (true, true, false) => self.non_ignored_count, - (true, false, true) => self.file_count, - (true, false, false) => self.non_ignored_file_count, - (false, true, true) => self.count - self.file_count, - (false, true, false) => self.non_ignored_count - self.non_ignored_file_count, - (false, false, _) => 0, - } - } -} - -impl<'a> sum_tree::Dimension<'a, EntrySummary> for TraversalProgress<'a> { - fn zero(_cx: ()) -> Self { - Default::default() - } - - fn add_summary(&mut self, summary: &'a EntrySummary, _: ()) { - self.max_path = summary.max_path.as_ref(); - self.count += summary.count; - self.non_ignored_count += summary.non_ignored_count; - self.file_count += summary.file_count; - self.non_ignored_file_count += summary.non_ignored_file_count; - } -} - -impl Default for TraversalProgress<'_> { - fn default() -> Self { - Self { - max_path: RelPath::empty(), - count: 0, - non_ignored_count: 0, - file_count: 0, - non_ignored_file_count: 0, - } - } -} - -#[derive(Debug)] -pub struct Traversal<'a> { - snapshot: &'a Snapshot, - cursor: sum_tree::Cursor<'a, 'static, Entry, TraversalProgress<'a>>, - include_ignored: bool, - include_files: bool, - include_dirs: bool, -} - -impl<'a> Traversal<'a> { - fn new( - snapshot: &'a Snapshot, - include_files: bool, - include_dirs: bool, - include_ignored: bool, - start_path: &RelPath, - ) -> Self { - let mut cursor = snapshot.entries_by_path.cursor(()); - cursor.seek(&TraversalTarget::path(start_path), Bias::Left); - let mut traversal = Self { - snapshot, - cursor, - include_files, - include_dirs, - include_ignored, - }; - if traversal.end_offset() == traversal.start_offset() { - traversal.next(); - } - traversal - } - - pub fn advance(&mut self) -> bool { - self.advance_by(1) - } - - pub fn advance_by(&mut self, count: usize) -> bool { - self.cursor.seek_forward( - &TraversalTarget::Count { - count: self.end_offset() + count, - include_dirs: self.include_dirs, - include_files: self.include_files, - include_ignored: self.include_ignored, - }, - Bias::Left, - ) - } - - pub fn advance_to_sibling(&mut self) -> bool { - while let Some(entry) = self.cursor.item() { - self.cursor - .seek_forward(&TraversalTarget::successor(&entry.path), Bias::Left); - if let Some(entry) = self.cursor.item() - && (self.include_files || !entry.is_file()) - && (self.include_dirs || !entry.is_dir()) - && (self.include_ignored || !entry.is_ignored || entry.is_always_included) - { - return true; - } - } - false - } - - pub fn back_to_parent(&mut self) -> bool { - let Some(parent_path) = self.cursor.item().and_then(|entry| entry.path.parent()) else { - return false; - }; - self.cursor - .seek(&TraversalTarget::path(parent_path), Bias::Left) - } - - pub fn entry(&self) -> Option<&'a Entry> { - self.cursor.item() - } - - pub fn snapshot(&self) -> &'a Snapshot { - self.snapshot - } - - pub fn start_offset(&self) -> usize { - self.cursor - .start() - .count(self.include_files, self.include_dirs, self.include_ignored) - } - - pub fn end_offset(&self) -> usize { - self.cursor - .end() - .count(self.include_files, self.include_dirs, self.include_ignored) - } -} - -impl<'a> Iterator for Traversal<'a> { - type Item = &'a Entry; - - fn next(&mut self) -> Option { - if let Some(item) = self.entry() { - self.advance(); - Some(item) - } else { - None - } - } -} - -#[derive(Debug, Clone, Copy)] -pub enum PathTarget<'a> { - Path(&'a RelPath), - Successor(&'a RelPath), -} - -impl PathTarget<'_> { - fn cmp_path(&self, other: &RelPath) -> Ordering { - match self { - PathTarget::Path(path) => path.cmp(&other), - PathTarget::Successor(path) => { - if other.starts_with(path) { - Ordering::Greater - } else { - Ordering::Equal - } - } - } - } -} - -impl<'a, S: Summary> SeekTarget<'a, PathSummary, PathProgress<'a>> for PathTarget<'_> { - fn cmp(&self, cursor_location: &PathProgress<'a>, _: S::Context<'_>) -> Ordering { - self.cmp_path(cursor_location.max_path) - } -} - -impl<'a, S: Summary> SeekTarget<'a, PathSummary, TraversalProgress<'a>> for PathTarget<'_> { - fn cmp(&self, cursor_location: &TraversalProgress<'a>, _: S::Context<'_>) -> Ordering { - self.cmp_path(cursor_location.max_path) - } -} - -#[derive(Debug)] -enum TraversalTarget<'a> { - Path(PathTarget<'a>), - Count { - count: usize, - include_files: bool, - include_ignored: bool, - include_dirs: bool, - }, -} - -impl<'a> TraversalTarget<'a> { - fn path(path: &'a RelPath) -> Self { - Self::Path(PathTarget::Path(path)) - } - - fn successor(path: &'a RelPath) -> Self { - Self::Path(PathTarget::Successor(path)) - } - - fn cmp_progress(&self, progress: &TraversalProgress) -> Ordering { - match self { - TraversalTarget::Path(path) => path.cmp_path(progress.max_path), - TraversalTarget::Count { - count, - include_files, - include_dirs, - include_ignored, - } => Ord::cmp( - count, - &progress.count(*include_files, *include_dirs, *include_ignored), - ), - } - } -} - -impl<'a> SeekTarget<'a, EntrySummary, TraversalProgress<'a>> for TraversalTarget<'_> { - fn cmp(&self, cursor_location: &TraversalProgress<'a>, _: ()) -> Ordering { - self.cmp_progress(cursor_location) - } -} - -impl<'a> SeekTarget<'a, PathSummary, TraversalProgress<'a>> - for TraversalTarget<'_> -{ - fn cmp(&self, cursor_location: &TraversalProgress<'a>, _: ()) -> Ordering { - self.cmp_progress(cursor_location) - } -} - -pub struct ChildEntriesOptions { - pub include_files: bool, - pub include_dirs: bool, - pub include_ignored: bool, -} - -pub struct ChildEntriesIter<'a> { - parent_path: &'a RelPath, - traversal: Traversal<'a>, -} - -impl<'a> Iterator for ChildEntriesIter<'a> { - type Item = &'a Entry; - - fn next(&mut self) -> Option { - if let Some(item) = self.traversal.entry() - && item.path.starts_with(self.parent_path) - { - self.traversal.advance_to_sibling(); - return Some(item); - } - None - } -} - -impl<'a> From<&'a Entry> for proto::Entry { - fn from(entry: &'a Entry) -> Self { - Self { - id: entry.id.to_proto(), - is_dir: entry.is_dir(), - path: entry.path.as_ref().to_proto(), - inode: entry.inode, - mtime: entry.mtime.map(|time| time.into()), - is_ignored: entry.is_ignored, - is_hidden: entry.is_hidden, - is_external: entry.is_external, - is_fifo: entry.is_fifo, - size: Some(entry.size), - canonical_path: entry - .canonical_path - .as_ref() - .map(|path| path.to_string_lossy().into_owned()), - } - } -} - -impl TryFrom<(&CharBag, &PathMatcher, proto::Entry)> for Entry { - type Error = anyhow::Error; - - fn try_from( - (root_char_bag, always_included, entry): (&CharBag, &PathMatcher, proto::Entry), - ) -> Result { - let kind = if entry.is_dir { - EntryKind::Dir - } else { - EntryKind::File - }; - - let path = - RelPath::from_proto(&entry.path).context("invalid relative path in proto message")?; - let char_bag = char_bag_for_path(*root_char_bag, &path); - let is_always_included = always_included.is_match(&path); - Ok(Entry { - id: ProjectEntryId::from_proto(entry.id), - kind, - path, - inode: entry.inode, - mtime: entry.mtime.map(|time| time.into()), - size: entry.size.unwrap_or(0), - canonical_path: entry - .canonical_path - .map(|path_string| Arc::from(PathBuf::from(path_string))), - is_ignored: entry.is_ignored, - is_hidden: entry.is_hidden, - is_always_included, - is_external: entry.is_external, - is_private: false, - char_bag, - is_fifo: entry.is_fifo, - }) - } -} - -#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)] -pub struct ProjectEntryId(usize); - -impl ProjectEntryId { - pub const MAX: Self = Self(usize::MAX); - pub const MIN: Self = Self(usize::MIN); - - pub fn new(counter: &AtomicUsize) -> Self { - Self(counter.fetch_add(1, SeqCst)) - } - - pub fn from_proto(id: u64) -> Self { - Self(id as usize) - } - - pub fn to_proto(self) -> u64 { - self.0 as u64 - } - - pub fn from_usize(id: usize) -> Self { - ProjectEntryId(id) - } - - pub fn to_usize(self) -> usize { - self.0 - } -} - -#[cfg(any(test, feature = "test-support"))] -impl CreatedEntry { - pub fn into_included(self) -> Option { - match self { - CreatedEntry::Included(entry) => Some(entry), - CreatedEntry::Excluded { .. } => None, - } - } -} - -fn parse_gitfile(content: &str) -> anyhow::Result<&Path> { - let path = content - .strip_prefix("gitdir:") - .with_context(|| format!("parsing gitfile content {content:?}"))?; - Ok(Path::new(path.trim())) -} - -async fn discover_git_paths(dot_git_abs_path: &Arc, fs: &dyn Fs) -> (Arc, Arc) { - let mut repository_dir_abs_path = dot_git_abs_path.clone(); - let mut common_dir_abs_path = dot_git_abs_path.clone(); - - if let Some(path) = fs - .load(dot_git_abs_path) - .await - .ok() - .as_ref() - .and_then(|contents| parse_gitfile(contents).log_err()) - { - let path = dot_git_abs_path - .parent() - .unwrap_or(Path::new("")) - .join(path); - if let Some(path) = fs.canonicalize(&path).await.log_err() { - repository_dir_abs_path = Path::new(&path).into(); - common_dir_abs_path = repository_dir_abs_path.clone(); - - if let Some(commondir_contents) = fs.load(&path.join("commondir")).await.ok() - && let Some(commondir_path) = fs - .canonicalize(&path.join(commondir_contents.trim())) - .await - .log_err() - { - common_dir_abs_path = commondir_path.as_path().into(); - } - } - }; - (repository_dir_abs_path, common_dir_abs_path) -} - -struct NullWatcher; - -impl fs::Watcher for NullWatcher { - fn add(&self, _path: &Path) -> Result<()> { - Ok(()) - } - - fn remove(&self, _path: &Path) -> Result<()> { - Ok(()) - } -} diff --git a/crates/worktree/src/worktree_settings.rs b/crates/worktree/src/worktree_settings.rs deleted file mode 100644 index a86720184e..0000000000 --- a/crates/worktree/src/worktree_settings.rs +++ /dev/null @@ -1,95 +0,0 @@ -use std::path::Path; - -use anyhow::Context as _; -use settings::{RegisterSetting, Settings}; -use util::{ - ResultExt, - paths::{PathMatcher, PathStyle}, - rel_path::RelPath, -}; - -#[derive(Clone, PartialEq, Eq, RegisterSetting)] -pub struct WorktreeSettings { - pub project_name: Option, - /// Whether to prevent this project from being shared in public channels. - pub prevent_sharing_in_public_channels: bool, - pub file_scan_exclusions: PathMatcher, - pub file_scan_inclusions: PathMatcher, - /// This field contains all ancestors of the `file_scan_inclusions`. It's used to - /// determine whether to terminate worktree scanning for a given dir. - pub parent_dir_scan_inclusions: PathMatcher, - pub private_files: PathMatcher, - pub hidden_files: PathMatcher, -} - -impl WorktreeSettings { - pub fn is_path_private(&self, path: &RelPath) -> bool { - path.ancestors() - .any(|ancestor| self.private_files.is_match(ancestor)) - } - - pub fn is_path_excluded(&self, path: &RelPath) -> bool { - path.ancestors() - .any(|ancestor| self.file_scan_exclusions.is_match(ancestor)) - } - - pub fn is_path_always_included(&self, path: &RelPath, is_dir: bool) -> bool { - if is_dir { - self.parent_dir_scan_inclusions.is_match(path) - } else { - self.file_scan_inclusions.is_match(path) - } - } - - pub fn is_path_hidden(&self, path: &RelPath) -> bool { - path.ancestors() - .any(|ancestor| self.hidden_files.is_match(ancestor)) - } -} - -impl Settings for WorktreeSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - let worktree = content.project.worktree.clone(); - let file_scan_exclusions = worktree.file_scan_exclusions.unwrap(); - let file_scan_inclusions = worktree.file_scan_inclusions.unwrap(); - let private_files = worktree.private_files.unwrap().0; - let hidden_files = worktree.hidden_files.unwrap(); - let parsed_file_scan_inclusions: Vec = file_scan_inclusions - .iter() - .flat_map(|glob| { - Path::new(glob) - .ancestors() - .skip(1) - .map(|a| a.to_string_lossy().into()) - }) - .filter(|p: &String| !p.is_empty()) - .collect(); - - Self { - project_name: worktree.project_name, - prevent_sharing_in_public_channels: worktree.prevent_sharing_in_public_channels, - file_scan_exclusions: path_matchers(file_scan_exclusions, "file_scan_exclusions") - .log_err() - .unwrap_or_default(), - parent_dir_scan_inclusions: path_matchers( - parsed_file_scan_inclusions, - "file_scan_inclusions", - ) - .unwrap(), - file_scan_inclusions: path_matchers(file_scan_inclusions, "file_scan_inclusions") - .unwrap(), - private_files: path_matchers(private_files, "private_files") - .log_err() - .unwrap_or_default(), - hidden_files: path_matchers(hidden_files, "hidden_files") - .log_err() - .unwrap_or_default(), - } - } -} - -fn path_matchers(mut values: Vec, context: &'static str) -> anyhow::Result { - values.sort(); - PathMatcher::new(values, PathStyle::local()) - .with_context(|| format!("Failed to parse globs from {}", context)) -} diff --git a/crates/worktree/src/worktree_tests.rs b/crates/worktree/src/worktree_tests.rs deleted file mode 100644 index e58e99ea68..0000000000 --- a/crates/worktree/src/worktree_tests.rs +++ /dev/null @@ -1,2466 +0,0 @@ -use crate::{Entry, EntryKind, Event, PathChange, Worktree, WorktreeModelHandle}; -use anyhow::Result; -use fs::{FakeFs, Fs, RealFs, RemoveOptions}; -use git::GITIGNORE; -use gpui::{AppContext as _, BackgroundExecutor, BorrowAppContext, Context, Task, TestAppContext}; -use parking_lot::Mutex; -use postage::stream::Stream; -use pretty_assertions::assert_eq; -use rand::prelude::*; - -use serde_json::json; -use settings::SettingsStore; -use std::{ - env, - fmt::Write, - mem, - path::{Path, PathBuf}, - sync::Arc, -}; -use util::{ - ResultExt, path, - rel_path::{RelPath, rel_path}, - test::TempTree, -}; - -#[gpui::test] -async fn test_traversal(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/root", - json!({ - ".gitignore": "a/b\n", - "a": { - "b": "", - "c": "", - } - }), - ) - .await; - - let tree = Worktree::local( - Path::new("/root"), - true, - fs, - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - - tree.read_with(cx, |tree, _| { - assert_eq!( - tree.entries(false, 0) - .map(|entry| entry.path.as_ref()) - .collect::>(), - vec![ - rel_path(""), - rel_path(".gitignore"), - rel_path("a"), - rel_path("a/c"), - ] - ); - assert_eq!( - tree.entries(true, 0) - .map(|entry| entry.path.as_ref()) - .collect::>(), - vec![ - rel_path(""), - rel_path(".gitignore"), - rel_path("a"), - rel_path("a/b"), - rel_path("a/c"), - ] - ); - }) -} - -#[gpui::test(iterations = 10)] -async fn test_circular_symlinks(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/root", - json!({ - "lib": { - "a": { - "a.txt": "" - }, - "b": { - "b.txt": "" - } - } - }), - ) - .await; - fs.create_symlink("/root/lib/a/lib".as_ref(), "..".into()) - .await - .unwrap(); - fs.create_symlink("/root/lib/b/lib".as_ref(), "..".into()) - .await - .unwrap(); - - let tree = Worktree::local( - Path::new("/root"), - true, - fs.clone(), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - - tree.read_with(cx, |tree, _| { - assert_eq!( - tree.entries(false, 0) - .map(|entry| entry.path.as_ref()) - .collect::>(), - vec![ - rel_path(""), - rel_path("lib"), - rel_path("lib/a"), - rel_path("lib/a/a.txt"), - rel_path("lib/a/lib"), - rel_path("lib/b"), - rel_path("lib/b/b.txt"), - rel_path("lib/b/lib"), - ] - ); - }); - - fs.rename( - Path::new("/root/lib/a/lib"), - Path::new("/root/lib/a/lib-2"), - Default::default(), - ) - .await - .unwrap(); - cx.executor().run_until_parked(); - tree.read_with(cx, |tree, _| { - assert_eq!( - tree.entries(false, 0) - .map(|entry| entry.path.as_ref()) - .collect::>(), - vec![ - rel_path(""), - rel_path("lib"), - rel_path("lib/a"), - rel_path("lib/a/a.txt"), - rel_path("lib/a/lib-2"), - rel_path("lib/b"), - rel_path("lib/b/b.txt"), - rel_path("lib/b/lib"), - ] - ); - }); -} - -#[gpui::test] -async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/root", - json!({ - "dir1": { - "deps": { - // symlinks here - }, - "src": { - "a.rs": "", - "b.rs": "", - }, - }, - "dir2": { - "src": { - "c.rs": "", - "d.rs": "", - } - }, - "dir3": { - "deps": {}, - "src": { - "e.rs": "", - "f.rs": "", - }, - } - }), - ) - .await; - - // These symlinks point to directories outside of the worktree's root, dir1. - fs.create_symlink("/root/dir1/deps/dep-dir2".as_ref(), "../../dir2".into()) - .await - .unwrap(); - fs.create_symlink("/root/dir1/deps/dep-dir3".as_ref(), "../../dir3".into()) - .await - .unwrap(); - - let tree = Worktree::local( - Path::new("/root/dir1"), - true, - fs.clone(), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - - let tree_updates = Arc::new(Mutex::new(Vec::new())); - tree.update(cx, |_, cx| { - let tree_updates = tree_updates.clone(); - cx.subscribe(&tree, move |_, _, event, _| { - if let Event::UpdatedEntries(update) = event { - tree_updates.lock().extend( - update - .iter() - .map(|(path, _, change)| (path.clone(), *change)), - ); - } - }) - .detach(); - }); - - // The symlinked directories are not scanned by default. - tree.read_with(cx, |tree, _| { - assert_eq!( - tree.entries(true, 0) - .map(|entry| (entry.path.as_ref(), entry.is_external)) - .collect::>(), - vec![ - (rel_path(""), false), - (rel_path("deps"), false), - (rel_path("deps/dep-dir2"), true), - (rel_path("deps/dep-dir3"), true), - (rel_path("src"), false), - (rel_path("src/a.rs"), false), - (rel_path("src/b.rs"), false), - ] - ); - - assert_eq!( - tree.entry_for_path(rel_path("deps/dep-dir2")).unwrap().kind, - EntryKind::UnloadedDir - ); - }); - - // Expand one of the symlinked directories. - tree.read_with(cx, |tree, _| { - tree.as_local() - .unwrap() - .refresh_entries_for_paths(vec![rel_path("deps/dep-dir3").into()]) - }) - .recv() - .await; - - // The expanded directory's contents are loaded. Subdirectories are - // not scanned yet. - tree.read_with(cx, |tree, _| { - assert_eq!( - tree.entries(true, 0) - .map(|entry| (entry.path.as_ref(), entry.is_external)) - .collect::>(), - vec![ - (rel_path(""), false), - (rel_path("deps"), false), - (rel_path("deps/dep-dir2"), true), - (rel_path("deps/dep-dir3"), true), - (rel_path("deps/dep-dir3/deps"), true), - (rel_path("deps/dep-dir3/src"), true), - (rel_path("src"), false), - (rel_path("src/a.rs"), false), - (rel_path("src/b.rs"), false), - ] - ); - }); - assert_eq!( - mem::take(&mut *tree_updates.lock()), - &[ - (rel_path("deps/dep-dir3").into(), PathChange::Loaded), - (rel_path("deps/dep-dir3/deps").into(), PathChange::Loaded), - (rel_path("deps/dep-dir3/src").into(), PathChange::Loaded) - ] - ); - - // Expand a subdirectory of one of the symlinked directories. - tree.read_with(cx, |tree, _| { - tree.as_local() - .unwrap() - .refresh_entries_for_paths(vec![rel_path("deps/dep-dir3/src").into()]) - }) - .recv() - .await; - - // The expanded subdirectory's contents are loaded. - tree.read_with(cx, |tree, _| { - assert_eq!( - tree.entries(true, 0) - .map(|entry| (entry.path.as_ref(), entry.is_external)) - .collect::>(), - vec![ - (rel_path(""), false), - (rel_path("deps"), false), - (rel_path("deps/dep-dir2"), true), - (rel_path("deps/dep-dir3"), true), - (rel_path("deps/dep-dir3/deps"), true), - (rel_path("deps/dep-dir3/src"), true), - (rel_path("deps/dep-dir3/src/e.rs"), true), - (rel_path("deps/dep-dir3/src/f.rs"), true), - (rel_path("src"), false), - (rel_path("src/a.rs"), false), - (rel_path("src/b.rs"), false), - ] - ); - }); - - assert_eq!( - mem::take(&mut *tree_updates.lock()), - &[ - (rel_path("deps/dep-dir3/src").into(), PathChange::Loaded), - ( - rel_path("deps/dep-dir3/src/e.rs").into(), - PathChange::Loaded - ), - ( - rel_path("deps/dep-dir3/src/f.rs").into(), - PathChange::Loaded - ) - ] - ); -} - -#[cfg(target_os = "macos")] -#[gpui::test] -async fn test_renaming_case_only(cx: &mut TestAppContext) { - cx.executor().allow_parking(); - init_test(cx); - - const OLD_NAME: &str = "aaa.rs"; - const NEW_NAME: &str = "AAA.rs"; - - let fs = Arc::new(RealFs::new(None, cx.executor())); - let temp_root = TempTree::new(json!({ - OLD_NAME: "", - })); - - let tree = Worktree::local( - temp_root.path(), - true, - fs.clone(), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - tree.read_with(cx, |tree, _| { - assert_eq!( - tree.entries(true, 0) - .map(|entry| entry.path.as_ref()) - .collect::>(), - vec![rel_path(""), rel_path(OLD_NAME)] - ); - }); - - fs.rename( - &temp_root.path().join(OLD_NAME), - &temp_root.path().join(NEW_NAME), - fs::RenameOptions { - overwrite: true, - ignore_if_exists: true, - create_parents: false, - }, - ) - .await - .unwrap(); - - tree.flush_fs_events(cx).await; - - tree.read_with(cx, |tree, _| { - assert_eq!( - tree.entries(true, 0) - .map(|entry| entry.path.as_ref()) - .collect::>(), - vec![rel_path(""), rel_path(NEW_NAME)] - ); - }); -} - -#[gpui::test] -async fn test_open_gitignored_files(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/root", - json!({ - ".gitignore": "node_modules\n", - "one": { - "node_modules": { - "a": { - "a1.js": "a1", - "a2.js": "a2", - }, - "b": { - "b1.js": "b1", - "b2.js": "b2", - }, - "c": { - "c1.js": "c1", - "c2.js": "c2", - } - }, - }, - "two": { - "x.js": "", - "y.js": "", - }, - }), - ) - .await; - - let tree = Worktree::local( - Path::new("/root"), - true, - fs.clone(), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - - tree.read_with(cx, |tree, _| { - assert_eq!( - tree.entries(true, 0) - .map(|entry| (entry.path.as_ref(), entry.is_ignored)) - .collect::>(), - vec![ - (rel_path(""), false), - (rel_path(".gitignore"), false), - (rel_path("one"), false), - (rel_path("one/node_modules"), true), - (rel_path("two"), false), - (rel_path("two/x.js"), false), - (rel_path("two/y.js"), false), - ] - ); - }); - - // Open a file that is nested inside of a gitignored directory that - // has not yet been expanded. - let prev_read_dir_count = fs.read_dir_call_count(); - let loaded = tree - .update(cx, |tree, cx| { - tree.load_file(rel_path("one/node_modules/b/b1.js"), cx) - }) - .await - .unwrap(); - - tree.read_with(cx, |tree, _| { - assert_eq!( - tree.entries(true, 0) - .map(|entry| (entry.path.as_ref(), entry.is_ignored)) - .collect::>(), - vec![ - (rel_path(""), false), - (rel_path(".gitignore"), false), - (rel_path("one"), false), - (rel_path("one/node_modules"), true), - (rel_path("one/node_modules/a"), true), - (rel_path("one/node_modules/b"), true), - (rel_path("one/node_modules/b/b1.js"), true), - (rel_path("one/node_modules/b/b2.js"), true), - (rel_path("one/node_modules/c"), true), - (rel_path("two"), false), - (rel_path("two/x.js"), false), - (rel_path("two/y.js"), false), - ] - ); - - assert_eq!( - loaded.file.path.as_ref(), - rel_path("one/node_modules/b/b1.js") - ); - - // Only the newly-expanded directories are scanned. - assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 2); - }); - - // Open another file in a different subdirectory of the same - // gitignored directory. - let prev_read_dir_count = fs.read_dir_call_count(); - let loaded = tree - .update(cx, |tree, cx| { - tree.load_file(rel_path("one/node_modules/a/a2.js"), cx) - }) - .await - .unwrap(); - - tree.read_with(cx, |tree, _| { - assert_eq!( - tree.entries(true, 0) - .map(|entry| (entry.path.as_ref(), entry.is_ignored)) - .collect::>(), - vec![ - (rel_path(""), false), - (rel_path(".gitignore"), false), - (rel_path("one"), false), - (rel_path("one/node_modules"), true), - (rel_path("one/node_modules/a"), true), - (rel_path("one/node_modules/a/a1.js"), true), - (rel_path("one/node_modules/a/a2.js"), true), - (rel_path("one/node_modules/b"), true), - (rel_path("one/node_modules/b/b1.js"), true), - (rel_path("one/node_modules/b/b2.js"), true), - (rel_path("one/node_modules/c"), true), - (rel_path("two"), false), - (rel_path("two/x.js"), false), - (rel_path("two/y.js"), false), - ] - ); - - assert_eq!( - loaded.file.path.as_ref(), - rel_path("one/node_modules/a/a2.js") - ); - - // Only the newly-expanded directory is scanned. - assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 1); - }); - - let path = PathBuf::from("/root/one/node_modules/c/lib"); - - // No work happens when files and directories change within an unloaded directory. - let prev_fs_call_count = fs.read_dir_call_count() + fs.metadata_call_count(); - // When we open a directory, we check each ancestor whether it's a git - // repository. That means we have an fs.metadata call per ancestor that we - // need to subtract here. - let ancestors = path.ancestors().count(); - - fs.create_dir(path.as_ref()).await.unwrap(); - cx.executor().run_until_parked(); - - assert_eq!( - fs.read_dir_call_count() + fs.metadata_call_count() - prev_fs_call_count - ancestors, - 0 - ); -} - -#[gpui::test] -async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/root", - json!({ - ".gitignore": "node_modules\n", - "a": { - "a.js": "", - }, - "b": { - "b.js": "", - }, - "node_modules": { - "c": { - "c.js": "", - }, - "d": { - "d.js": "", - "e": { - "e1.js": "", - "e2.js": "", - }, - "f": { - "f1.js": "", - "f2.js": "", - } - }, - }, - }), - ) - .await; - - let tree = Worktree::local( - Path::new("/root"), - true, - fs.clone(), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - - // Open a file within the gitignored directory, forcing some of its - // subdirectories to be read, but not all. - let read_dir_count_1 = fs.read_dir_call_count(); - tree.read_with(cx, |tree, _| { - tree.as_local() - .unwrap() - .refresh_entries_for_paths(vec![rel_path("node_modules/d/d.js").into()]) - }) - .recv() - .await; - - // Those subdirectories are now loaded. - tree.read_with(cx, |tree, _| { - assert_eq!( - tree.entries(true, 0) - .map(|e| (e.path.as_ref(), e.is_ignored)) - .collect::>(), - &[ - (rel_path(""), false), - (rel_path(".gitignore"), false), - (rel_path("a"), false), - (rel_path("a/a.js"), false), - (rel_path("b"), false), - (rel_path("b/b.js"), false), - (rel_path("node_modules"), true), - (rel_path("node_modules/c"), true), - (rel_path("node_modules/d"), true), - (rel_path("node_modules/d/d.js"), true), - (rel_path("node_modules/d/e"), true), - (rel_path("node_modules/d/f"), true), - ] - ); - }); - let read_dir_count_2 = fs.read_dir_call_count(); - assert_eq!(read_dir_count_2 - read_dir_count_1, 2); - - // Update the gitignore so that node_modules is no longer ignored, - // but a subdirectory is ignored - fs.save("/root/.gitignore".as_ref(), &"e".into(), Default::default()) - .await - .unwrap(); - cx.executor().run_until_parked(); - - // All of the directories that are no longer ignored are now loaded. - tree.read_with(cx, |tree, _| { - assert_eq!( - tree.entries(true, 0) - .map(|e| (e.path.as_ref(), e.is_ignored)) - .collect::>(), - &[ - (rel_path(""), false), - (rel_path(".gitignore"), false), - (rel_path("a"), false), - (rel_path("a/a.js"), false), - (rel_path("b"), false), - (rel_path("b/b.js"), false), - // This directory is no longer ignored - (rel_path("node_modules"), false), - (rel_path("node_modules/c"), false), - (rel_path("node_modules/c/c.js"), false), - (rel_path("node_modules/d"), false), - (rel_path("node_modules/d/d.js"), false), - // This subdirectory is now ignored - (rel_path("node_modules/d/e"), true), - (rel_path("node_modules/d/f"), false), - (rel_path("node_modules/d/f/f1.js"), false), - (rel_path("node_modules/d/f/f2.js"), false), - ] - ); - }); - - // Each of the newly-loaded directories is scanned only once. - let read_dir_count_3 = fs.read_dir_call_count(); - assert_eq!(read_dir_count_3 - read_dir_count_2, 2); -} - -#[gpui::test] -async fn test_write_file(cx: &mut TestAppContext) { - init_test(cx); - cx.executor().allow_parking(); - let dir = TempTree::new(json!({ - ".git": {}, - ".gitignore": "ignored-dir\n", - "tracked-dir": {}, - "ignored-dir": {} - })); - - let worktree = Worktree::local( - dir.path(), - true, - Arc::new(RealFs::new(None, cx.executor())), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - - #[cfg(not(target_os = "macos"))] - fs::fs_watcher::global(|_| {}).unwrap(); - - cx.read(|cx| worktree.read(cx).as_local().unwrap().scan_complete()) - .await; - worktree.flush_fs_events(cx).await; - - worktree - .update(cx, |tree, cx| { - tree.write_file( - rel_path("tracked-dir/file.txt").into(), - "hello".into(), - Default::default(), - cx, - ) - }) - .await - .unwrap(); - worktree - .update(cx, |tree, cx| { - tree.write_file( - rel_path("ignored-dir/file.txt").into(), - "world".into(), - Default::default(), - cx, - ) - }) - .await - .unwrap(); - worktree.read_with(cx, |tree, _| { - let tracked = tree - .entry_for_path(rel_path("tracked-dir/file.txt")) - .unwrap(); - let ignored = tree - .entry_for_path(rel_path("ignored-dir/file.txt")) - .unwrap(); - assert!(!tracked.is_ignored); - assert!(ignored.is_ignored); - }); -} - -#[gpui::test] -async fn test_file_scan_inclusions(cx: &mut TestAppContext) { - init_test(cx); - cx.executor().allow_parking(); - let dir = TempTree::new(json!({ - ".gitignore": "**/target\n/node_modules\ntop_level.txt\n", - "target": { - "index": "blah2" - }, - "node_modules": { - ".DS_Store": "", - "prettier": { - "package.json": "{}", - }, - "package.json": "//package.json" - }, - "src": { - ".DS_Store": "", - "foo": { - "foo.rs": "mod another;\n", - "another.rs": "// another", - }, - "bar": { - "bar.rs": "// bar", - }, - "lib.rs": "mod foo;\nmod bar;\n", - }, - "top_level.txt": "top level file", - ".DS_Store": "", - })); - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(vec![]); - settings.project.worktree.file_scan_inclusions = Some(vec![ - "node_modules/**/package.json".to_string(), - "**/.DS_Store".to_string(), - ]); - }); - }); - }); - - let tree = Worktree::local( - dir.path(), - true, - Arc::new(RealFs::new(None, cx.executor())), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - tree.flush_fs_events(cx).await; - tree.read_with(cx, |tree, _| { - // Assert that file_scan_inclusions overrides file_scan_exclusions. - check_worktree_entries( - tree, - &[], - &["target", "node_modules"], - &["src/lib.rs", "src/bar/bar.rs", ".gitignore"], - &[ - "node_modules/prettier/package.json", - ".DS_Store", - "node_modules/.DS_Store", - "src/.DS_Store", - ], - ) - }); -} - -#[gpui::test] -async fn test_file_scan_exclusions_overrules_inclusions(cx: &mut TestAppContext) { - init_test(cx); - cx.executor().allow_parking(); - let dir = TempTree::new(json!({ - ".gitignore": "**/target\n/node_modules\n", - "target": { - "index": "blah2" - }, - "node_modules": { - ".DS_Store": "", - "prettier": { - "package.json": "{}", - }, - }, - "src": { - ".DS_Store": "", - "foo": { - "foo.rs": "mod another;\n", - "another.rs": "// another", - }, - }, - ".DS_Store": "", - })); - - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = - Some(vec!["**/.DS_Store".to_string()]); - settings.project.worktree.file_scan_inclusions = - Some(vec!["**/.DS_Store".to_string()]); - }); - }); - }); - - let tree = Worktree::local( - dir.path(), - true, - Arc::new(RealFs::new(None, cx.executor())), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - tree.flush_fs_events(cx).await; - tree.read_with(cx, |tree, _| { - // Assert that file_scan_inclusions overrides file_scan_exclusions. - check_worktree_entries( - tree, - &[".DS_Store, src/.DS_Store"], - &["target", "node_modules"], - &["src/foo/another.rs", "src/foo/foo.rs", ".gitignore"], - &[], - ) - }); -} - -#[gpui::test] -async fn test_file_scan_inclusions_reindexes_on_setting_change(cx: &mut TestAppContext) { - init_test(cx); - cx.executor().allow_parking(); - let dir = TempTree::new(json!({ - ".gitignore": "**/target\n/node_modules/\n", - "target": { - "index": "blah2" - }, - "node_modules": { - ".DS_Store": "", - "prettier": { - "package.json": "{}", - }, - }, - "src": { - ".DS_Store": "", - "foo": { - "foo.rs": "mod another;\n", - "another.rs": "// another", - }, - }, - ".DS_Store": "", - })); - - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(vec![]); - settings.project.worktree.file_scan_inclusions = - Some(vec!["node_modules/**".to_string()]); - }); - }); - }); - let tree = Worktree::local( - dir.path(), - true, - Arc::new(RealFs::new(None, cx.executor())), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - tree.flush_fs_events(cx).await; - - tree.read_with(cx, |tree, _| { - assert!( - tree.entry_for_path(rel_path("node_modules")) - .is_some_and(|f| f.is_always_included) - ); - assert!( - tree.entry_for_path(rel_path("node_modules/prettier/package.json")) - .is_some_and(|f| f.is_always_included) - ); - }); - - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(vec![]); - settings.project.worktree.file_scan_inclusions = Some(vec![]); - }); - }); - }); - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - tree.flush_fs_events(cx).await; - - tree.read_with(cx, |tree, _| { - assert!( - tree.entry_for_path(rel_path("node_modules")) - .is_some_and(|f| !f.is_always_included) - ); - assert!( - tree.entry_for_path(rel_path("node_modules/prettier/package.json")) - .is_some_and(|f| !f.is_always_included) - ); - }); -} - -#[gpui::test] -async fn test_file_scan_exclusions(cx: &mut TestAppContext) { - init_test(cx); - cx.executor().allow_parking(); - let dir = TempTree::new(json!({ - ".gitignore": "**/target\n/node_modules\n", - "target": { - "index": "blah2" - }, - "node_modules": { - ".DS_Store": "", - "prettier": { - "package.json": "{}", - }, - }, - "src": { - ".DS_Store": "", - "foo": { - "foo.rs": "mod another;\n", - "another.rs": "// another", - }, - "bar": { - "bar.rs": "// bar", - }, - "lib.rs": "mod foo;\nmod bar;\n", - }, - ".DS_Store": "", - })); - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = - Some(vec!["**/foo/**".to_string(), "**/.DS_Store".to_string()]); - }); - }); - }); - - let tree = Worktree::local( - dir.path(), - true, - Arc::new(RealFs::new(None, cx.executor())), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - tree.flush_fs_events(cx).await; - tree.read_with(cx, |tree, _| { - check_worktree_entries( - tree, - &[ - "src/foo/foo.rs", - "src/foo/another.rs", - "node_modules/.DS_Store", - "src/.DS_Store", - ".DS_Store", - ], - &["target", "node_modules"], - &["src/lib.rs", "src/bar/bar.rs", ".gitignore"], - &[], - ) - }); - - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = - Some(vec!["**/node_modules/**".to_string()]); - }); - }); - }); - tree.flush_fs_events(cx).await; - cx.executor().run_until_parked(); - tree.read_with(cx, |tree, _| { - check_worktree_entries( - tree, - &[ - "node_modules/prettier/package.json", - "node_modules/.DS_Store", - "node_modules", - ], - &["target"], - &[ - ".gitignore", - "src/lib.rs", - "src/bar/bar.rs", - "src/foo/foo.rs", - "src/foo/another.rs", - "src/.DS_Store", - ".DS_Store", - ], - &[], - ) - }); -} - -#[gpui::test] -async fn test_hidden_files(cx: &mut TestAppContext) { - init_test(cx); - cx.executor().allow_parking(); - let dir = TempTree::new(json!({ - ".gitignore": "**/target\n", - ".hidden_file": "content", - ".hidden_dir": { - "nested.rs": "code", - }, - "src": { - "visible.rs": "code", - }, - "logs": { - "app.log": "logs", - "debug.log": "logs", - }, - "visible.txt": "content", - })); - - let tree = Worktree::local( - dir.path(), - true, - Arc::new(RealFs::new(None, cx.executor())), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - tree.flush_fs_events(cx).await; - - tree.read_with(cx, |tree, _| { - assert_eq!( - tree.entries(true, 0) - .map(|entry| (entry.path.as_ref(), entry.is_hidden)) - .collect::>(), - vec![ - (rel_path(""), false), - (rel_path(".gitignore"), true), - (rel_path(".hidden_dir"), true), - (rel_path(".hidden_dir/nested.rs"), true), - (rel_path(".hidden_file"), true), - (rel_path("logs"), false), - (rel_path("logs/app.log"), false), - (rel_path("logs/debug.log"), false), - (rel_path("src"), false), - (rel_path("src/visible.rs"), false), - (rel_path("visible.txt"), false), - ] - ); - }); - - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.hidden_files = Some(vec!["**/*.log".to_string()]); - }); - }); - }); - tree.flush_fs_events(cx).await; - cx.executor().run_until_parked(); - - tree.read_with(cx, |tree, _| { - assert_eq!( - tree.entries(true, 0) - .map(|entry| (entry.path.as_ref(), entry.is_hidden)) - .collect::>(), - vec![ - (rel_path(""), false), - (rel_path(".gitignore"), false), - (rel_path(".hidden_dir"), false), - (rel_path(".hidden_dir/nested.rs"), false), - (rel_path(".hidden_file"), false), - (rel_path("logs"), false), - (rel_path("logs/app.log"), true), - (rel_path("logs/debug.log"), true), - (rel_path("src"), false), - (rel_path("src/visible.rs"), false), - (rel_path("visible.txt"), false), - ] - ); - }); -} - -#[gpui::test] -async fn test_fs_events_in_exclusions(cx: &mut TestAppContext) { - init_test(cx); - cx.executor().allow_parking(); - let dir = TempTree::new(json!({ - ".git": { - "HEAD": "ref: refs/heads/main\n", - "foo": "bar", - }, - ".gitignore": "**/target\n/node_modules\ntest_output\n", - "target": { - "index": "blah2" - }, - "node_modules": { - ".DS_Store": "", - "prettier": { - "package.json": "{}", - }, - }, - "src": { - ".DS_Store": "", - "foo": { - "foo.rs": "mod another;\n", - "another.rs": "// another", - }, - "bar": { - "bar.rs": "// bar", - }, - "lib.rs": "mod foo;\nmod bar;\n", - }, - ".DS_Store": "", - })); - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |settings| { - settings.project.worktree.file_scan_exclusions = Some(vec![ - "**/.git".to_string(), - "node_modules/".to_string(), - "build_output".to_string(), - ]); - }); - }); - }); - - let tree = Worktree::local( - dir.path(), - true, - Arc::new(RealFs::new(None, cx.executor())), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - tree.flush_fs_events(cx).await; - tree.read_with(cx, |tree, _| { - check_worktree_entries( - tree, - &[ - ".git/HEAD", - ".git/foo", - "node_modules", - "node_modules/.DS_Store", - "node_modules/prettier", - "node_modules/prettier/package.json", - ], - &["target"], - &[ - ".DS_Store", - "src/.DS_Store", - "src/lib.rs", - "src/foo/foo.rs", - "src/foo/another.rs", - "src/bar/bar.rs", - ".gitignore", - ], - &[], - ) - }); - - let new_excluded_dir = dir.path().join("build_output"); - let new_ignored_dir = dir.path().join("test_output"); - std::fs::create_dir_all(&new_excluded_dir) - .unwrap_or_else(|e| panic!("Failed to create a {new_excluded_dir:?} directory: {e}")); - std::fs::create_dir_all(&new_ignored_dir) - .unwrap_or_else(|e| panic!("Failed to create a {new_ignored_dir:?} directory: {e}")); - let node_modules_dir = dir.path().join("node_modules"); - let dot_git_dir = dir.path().join(".git"); - let src_dir = dir.path().join("src"); - for existing_dir in [&node_modules_dir, &dot_git_dir, &src_dir] { - assert!( - existing_dir.is_dir(), - "Expect {existing_dir:?} to be present in the FS already" - ); - } - - for directory_for_new_file in [ - new_excluded_dir, - new_ignored_dir, - node_modules_dir, - dot_git_dir, - src_dir, - ] { - std::fs::write(directory_for_new_file.join("new_file"), "new file contents") - .unwrap_or_else(|e| { - panic!("Failed to create in {directory_for_new_file:?} a new file: {e}") - }); - } - tree.flush_fs_events(cx).await; - - tree.read_with(cx, |tree, _| { - check_worktree_entries( - tree, - &[ - ".git/HEAD", - ".git/foo", - ".git/new_file", - "node_modules", - "node_modules/.DS_Store", - "node_modules/prettier", - "node_modules/prettier/package.json", - "node_modules/new_file", - "build_output", - "build_output/new_file", - "test_output/new_file", - ], - &["target", "test_output"], - &[ - ".DS_Store", - "src/.DS_Store", - "src/lib.rs", - "src/foo/foo.rs", - "src/foo/another.rs", - "src/bar/bar.rs", - "src/new_file", - ".gitignore", - ], - &[], - ) - }); -} - -#[gpui::test] -async fn test_fs_events_in_dot_git_worktree(cx: &mut TestAppContext) { - init_test(cx); - cx.executor().allow_parking(); - let dir = TempTree::new(json!({ - ".git": { - "HEAD": "ref: refs/heads/main\n", - "foo": "foo contents", - }, - })); - let dot_git_worktree_dir = dir.path().join(".git"); - - let tree = Worktree::local( - dot_git_worktree_dir.clone(), - true, - Arc::new(RealFs::new(None, cx.executor())), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - tree.flush_fs_events(cx).await; - tree.read_with(cx, |tree, _| { - check_worktree_entries(tree, &[], &["HEAD", "foo"], &[], &[]) - }); - - std::fs::write(dot_git_worktree_dir.join("new_file"), "new file contents") - .unwrap_or_else(|e| panic!("Failed to create in {dot_git_worktree_dir:?} a new file: {e}")); - tree.flush_fs_events(cx).await; - tree.read_with(cx, |tree, _| { - check_worktree_entries(tree, &[], &["HEAD", "foo", "new_file"], &[], &[]) - }); -} - -#[gpui::test(iterations = 30)] -async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/root", - json!({ - "b": {}, - "c": {}, - "d": {}, - }), - ) - .await; - - let tree = Worktree::local( - "/root".as_ref(), - true, - fs, - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - - let snapshot1 = tree.update(cx, |tree, cx| { - let tree = tree.as_local_mut().unwrap(); - let snapshot = Arc::new(Mutex::new(tree.snapshot())); - tree.observe_updates(0, cx, { - let snapshot = snapshot.clone(); - let settings = tree.settings(); - move |update| { - snapshot - .lock() - .apply_remote_update(update, &settings.file_scan_inclusions); - async { true } - } - }); - snapshot - }); - - let entry = tree - .update(cx, |tree, cx| { - tree.as_local_mut() - .unwrap() - .create_entry(rel_path("a/e").into(), true, None, cx) - }) - .await - .unwrap() - .into_included() - .unwrap(); - assert!(entry.is_dir()); - - cx.executor().run_until_parked(); - tree.read_with(cx, |tree, _| { - assert_eq!( - tree.entry_for_path(rel_path("a/e")).unwrap().kind, - EntryKind::Dir - ); - }); - - let snapshot2 = tree.update(cx, |tree, _| tree.as_local().unwrap().snapshot()); - assert_eq!( - snapshot1.lock().entries(true, 0).collect::>(), - snapshot2.entries(true, 0).collect::>() - ); -} - -#[gpui::test] -async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) { - init_test(cx); - cx.executor().allow_parking(); - - let fs_fake = FakeFs::new(cx.background_executor.clone()); - fs_fake - .insert_tree( - "/root", - json!({ - "a": {}, - }), - ) - .await; - - let tree_fake = Worktree::local( - "/root".as_ref(), - true, - fs_fake, - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - - let entry = tree_fake - .update(cx, |tree, cx| { - tree.as_local_mut().unwrap().create_entry( - rel_path("a/b/c/d.txt").into(), - false, - None, - cx, - ) - }) - .await - .unwrap() - .into_included() - .unwrap(); - assert!(entry.is_file()); - - cx.executor().run_until_parked(); - tree_fake.read_with(cx, |tree, _| { - assert!( - tree.entry_for_path(rel_path("a/b/c/d.txt")) - .unwrap() - .is_file() - ); - assert!(tree.entry_for_path(rel_path("a/b/c")).unwrap().is_dir()); - assert!(tree.entry_for_path(rel_path("a/b")).unwrap().is_dir()); - }); - - let fs_real = Arc::new(RealFs::new(None, cx.executor())); - let temp_root = TempTree::new(json!({ - "a": {} - })); - - let tree_real = Worktree::local( - temp_root.path(), - true, - fs_real, - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - - let entry = tree_real - .update(cx, |tree, cx| { - tree.as_local_mut().unwrap().create_entry( - rel_path("a/b/c/d.txt").into(), - false, - None, - cx, - ) - }) - .await - .unwrap() - .into_included() - .unwrap(); - assert!(entry.is_file()); - - cx.executor().run_until_parked(); - tree_real.read_with(cx, |tree, _| { - assert!( - tree.entry_for_path(rel_path("a/b/c/d.txt")) - .unwrap() - .is_file() - ); - assert!(tree.entry_for_path(rel_path("a/b/c")).unwrap().is_dir()); - assert!(tree.entry_for_path(rel_path("a/b")).unwrap().is_dir()); - }); - - // Test smallest change - let entry = tree_real - .update(cx, |tree, cx| { - tree.as_local_mut().unwrap().create_entry( - rel_path("a/b/c/e.txt").into(), - false, - None, - cx, - ) - }) - .await - .unwrap() - .into_included() - .unwrap(); - assert!(entry.is_file()); - - cx.executor().run_until_parked(); - tree_real.read_with(cx, |tree, _| { - assert!( - tree.entry_for_path(rel_path("a/b/c/e.txt")) - .unwrap() - .is_file() - ); - }); - - // Test largest change - let entry = tree_real - .update(cx, |tree, cx| { - tree.as_local_mut().unwrap().create_entry( - rel_path("d/e/f/g.txt").into(), - false, - None, - cx, - ) - }) - .await - .unwrap() - .into_included() - .unwrap(); - assert!(entry.is_file()); - - cx.executor().run_until_parked(); - tree_real.read_with(cx, |tree, _| { - assert!( - tree.entry_for_path(rel_path("d/e/f/g.txt")) - .unwrap() - .is_file() - ); - assert!(tree.entry_for_path(rel_path("d/e/f")).unwrap().is_dir()); - assert!(tree.entry_for_path(rel_path("d/e")).unwrap().is_dir()); - assert!(tree.entry_for_path(rel_path("d")).unwrap().is_dir()); - }); -} - -#[gpui::test] -async fn test_create_file_in_expanded_gitignored_dir(cx: &mut TestAppContext) { - // Tests the behavior of our worktree refresh when a file in a gitignored directory - // is created. - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/root", - json!({ - ".gitignore": "ignored_dir\n", - "ignored_dir": { - "existing_file.txt": "existing content", - "another_file.txt": "another content", - }, - }), - ) - .await; - - let tree = Worktree::local( - Path::new("/root"), - true, - fs.clone(), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - - tree.read_with(cx, |tree, _| { - let ignored_dir = tree.entry_for_path(rel_path("ignored_dir")).unwrap(); - assert!(ignored_dir.is_ignored); - assert_eq!(ignored_dir.kind, EntryKind::UnloadedDir); - }); - - tree.update(cx, |tree, cx| { - tree.load_file(rel_path("ignored_dir/existing_file.txt"), cx) - }) - .await - .unwrap(); - - tree.read_with(cx, |tree, _| { - let ignored_dir = tree.entry_for_path(rel_path("ignored_dir")).unwrap(); - assert!(ignored_dir.is_ignored); - assert_eq!(ignored_dir.kind, EntryKind::Dir); - - assert!( - tree.entry_for_path(rel_path("ignored_dir/existing_file.txt")) - .is_some() - ); - assert!( - tree.entry_for_path(rel_path("ignored_dir/another_file.txt")) - .is_some() - ); - }); - - let entry = tree - .update(cx, |tree, cx| { - tree.create_entry(rel_path("ignored_dir/new_file.txt").into(), false, None, cx) - }) - .await - .unwrap(); - assert!(entry.into_included().is_some()); - - cx.executor().run_until_parked(); - - tree.read_with(cx, |tree, _| { - let ignored_dir = tree.entry_for_path(rel_path("ignored_dir")).unwrap(); - assert!(ignored_dir.is_ignored); - assert_eq!( - ignored_dir.kind, - EntryKind::Dir, - "ignored_dir should still be loaded, not UnloadedDir" - ); - - assert!( - tree.entry_for_path(rel_path("ignored_dir/existing_file.txt")) - .is_some(), - "existing_file.txt should still be visible" - ); - assert!( - tree.entry_for_path(rel_path("ignored_dir/another_file.txt")) - .is_some(), - "another_file.txt should still be visible" - ); - assert!( - tree.entry_for_path(rel_path("ignored_dir/new_file.txt")) - .is_some(), - "new_file.txt should be visible" - ); - }); -} - -#[gpui::test] -async fn test_fs_event_for_gitignored_dir_does_not_lose_contents(cx: &mut TestAppContext) { - // Tests the behavior of our worktree refresh when a directory modification for a gitignored directory - // is triggered. - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/root", - json!({ - ".gitignore": "ignored_dir\n", - "ignored_dir": { - "file1.txt": "content1", - "file2.txt": "content2", - }, - }), - ) - .await; - - let tree = Worktree::local( - Path::new("/root"), - true, - fs.clone(), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - - // Load a file to expand the ignored directory - tree.update(cx, |tree, cx| { - tree.load_file(rel_path("ignored_dir/file1.txt"), cx) - }) - .await - .unwrap(); - - tree.read_with(cx, |tree, _| { - let ignored_dir = tree.entry_for_path(rel_path("ignored_dir")).unwrap(); - assert_eq!(ignored_dir.kind, EntryKind::Dir); - assert!( - tree.entry_for_path(rel_path("ignored_dir/file1.txt")) - .is_some() - ); - assert!( - tree.entry_for_path(rel_path("ignored_dir/file2.txt")) - .is_some() - ); - }); - - fs.emit_fs_event("/root/ignored_dir", Some(fs::PathEventKind::Changed)); - tree.flush_fs_events(cx).await; - - tree.read_with(cx, |tree, _| { - let ignored_dir = tree.entry_for_path(rel_path("ignored_dir")).unwrap(); - assert_eq!( - ignored_dir.kind, - EntryKind::Dir, - "ignored_dir should still be loaded (Dir), not UnloadedDir" - ); - assert!( - tree.entry_for_path(rel_path("ignored_dir/file1.txt")) - .is_some(), - "file1.txt should still be visible after directory fs event" - ); - assert!( - tree.entry_for_path(rel_path("ignored_dir/file2.txt")) - .is_some(), - "file2.txt should still be visible after directory fs event" - ); - }); -} - -#[gpui::test(iterations = 100)] -async fn test_random_worktree_operations_during_initial_scan( - cx: &mut TestAppContext, - mut rng: StdRng, -) { - init_test(cx); - let operations = env::var("OPERATIONS") - .map(|o| o.parse().unwrap()) - .unwrap_or(5); - let initial_entries = env::var("INITIAL_ENTRIES") - .map(|o| o.parse().unwrap()) - .unwrap_or(20); - - let root_dir = Path::new(path!("/test")); - let fs = FakeFs::new(cx.background_executor.clone()) as Arc; - fs.as_fake().insert_tree(root_dir, json!({})).await; - for _ in 0..initial_entries { - randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await; - } - log::info!("generated initial tree"); - - let worktree = Worktree::local( - root_dir, - true, - fs.clone(), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - - let mut snapshots = vec![worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())]; - let updates = Arc::new(Mutex::new(Vec::new())); - worktree.update(cx, |tree, cx| { - check_worktree_change_events(tree, cx); - - tree.as_local_mut().unwrap().observe_updates(0, cx, { - let updates = updates.clone(); - move |update| { - updates.lock().push(update); - async { true } - } - }); - }); - - for _ in 0..operations { - worktree - .update(cx, |worktree, cx| { - randomly_mutate_worktree(worktree, &mut rng, cx) - }) - .await - .log_err(); - worktree.read_with(cx, |tree, _| { - tree.as_local().unwrap().snapshot().check_invariants(true) - }); - - if rng.random_bool(0.6) { - snapshots.push(worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot())); - } - } - - worktree - .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete()) - .await; - - cx.executor().run_until_parked(); - - let final_snapshot = worktree.read_with(cx, |tree, _| { - let tree = tree.as_local().unwrap(); - let snapshot = tree.snapshot(); - snapshot.check_invariants(true); - snapshot - }); - - let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings()); - - for (i, snapshot) in snapshots.into_iter().enumerate().rev() { - let mut updated_snapshot = snapshot.clone(); - for update in updates.lock().iter() { - if update.scan_id >= updated_snapshot.scan_id() as u64 { - updated_snapshot - .apply_remote_update(update.clone(), &settings.file_scan_inclusions); - } - } - - assert_eq!( - updated_snapshot.entries(true, 0).collect::>(), - final_snapshot.entries(true, 0).collect::>(), - "wrong updates after snapshot {i}: {updates:#?}", - ); - } -} - -#[gpui::test(iterations = 100)] -async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) { - init_test(cx); - let operations = env::var("OPERATIONS") - .map(|o| o.parse().unwrap()) - .unwrap_or(40); - let initial_entries = env::var("INITIAL_ENTRIES") - .map(|o| o.parse().unwrap()) - .unwrap_or(20); - - let root_dir = Path::new(path!("/test")); - let fs = FakeFs::new(cx.background_executor.clone()) as Arc; - fs.as_fake().insert_tree(root_dir, json!({})).await; - for _ in 0..initial_entries { - randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await; - } - log::info!("generated initial tree"); - - let worktree = Worktree::local( - root_dir, - true, - fs.clone(), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - - let updates = Arc::new(Mutex::new(Vec::new())); - worktree.update(cx, |tree, cx| { - check_worktree_change_events(tree, cx); - - tree.as_local_mut().unwrap().observe_updates(0, cx, { - let updates = updates.clone(); - move |update| { - updates.lock().push(update); - async { true } - } - }); - }); - - worktree - .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete()) - .await; - - fs.as_fake().pause_events(); - let mut snapshots = Vec::new(); - let mut mutations_len = operations; - while mutations_len > 1 { - if rng.random_bool(0.2) { - worktree - .update(cx, |worktree, cx| { - randomly_mutate_worktree(worktree, &mut rng, cx) - }) - .await - .log_err(); - } else { - randomly_mutate_fs(&fs, root_dir, 1.0, &mut rng).await; - } - - let buffered_event_count = fs.as_fake().buffered_event_count(); - if buffered_event_count > 0 && rng.random_bool(0.3) { - let len = rng.random_range(0..=buffered_event_count); - log::info!("flushing {} events", len); - fs.as_fake().flush_events(len); - } else { - randomly_mutate_fs(&fs, root_dir, 0.6, &mut rng).await; - mutations_len -= 1; - } - - cx.executor().run_until_parked(); - if rng.random_bool(0.2) { - log::info!("storing snapshot {}", snapshots.len()); - let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()); - snapshots.push(snapshot); - } - } - - log::info!("quiescing"); - fs.as_fake().flush_events(usize::MAX); - cx.executor().run_until_parked(); - - let snapshot = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()); - snapshot.check_invariants(true); - let expanded_paths = snapshot - .expanded_entries() - .map(|e| e.path.clone()) - .collect::>(); - - { - let new_worktree = Worktree::local( - root_dir, - true, - fs.clone(), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - new_worktree - .update(cx, |tree, _| tree.as_local_mut().unwrap().scan_complete()) - .await; - new_worktree - .update(cx, |tree, _| { - tree.as_local_mut() - .unwrap() - .refresh_entries_for_paths(expanded_paths) - }) - .recv() - .await; - let new_snapshot = - new_worktree.read_with(cx, |tree, _| tree.as_local().unwrap().snapshot()); - assert_eq!( - snapshot.entries_without_ids(true), - new_snapshot.entries_without_ids(true) - ); - } - - let settings = worktree.read_with(cx, |tree, _| tree.as_local().unwrap().settings()); - - for (i, mut prev_snapshot) in snapshots.into_iter().enumerate().rev() { - for update in updates.lock().iter() { - if update.scan_id >= prev_snapshot.scan_id() as u64 { - prev_snapshot.apply_remote_update(update.clone(), &settings.file_scan_inclusions); - } - } - - assert_eq!( - prev_snapshot - .entries(true, 0) - .map(ignore_pending_dir) - .collect::>(), - snapshot - .entries(true, 0) - .map(ignore_pending_dir) - .collect::>(), - "wrong updates after snapshot {i}: {updates:#?}", - ); - } - - fn ignore_pending_dir(entry: &Entry) -> Entry { - let mut entry = entry.clone(); - if entry.kind.is_dir() { - entry.kind = EntryKind::Dir - } - entry - } -} - -// The worktree's `UpdatedEntries` event can be used to follow along with -// all changes to the worktree's snapshot. -fn check_worktree_change_events(tree: &mut Worktree, cx: &mut Context) { - let mut entries = tree.entries(true, 0).cloned().collect::>(); - cx.subscribe(&cx.entity(), move |tree, _, event, _| { - if let Event::UpdatedEntries(changes) = event { - for (path, _, change_type) in changes.iter() { - let entry = tree.entry_for_path(path).cloned(); - let ix = match entries.binary_search_by_key(&path, |e| &e.path) { - Ok(ix) | Err(ix) => ix, - }; - match change_type { - PathChange::Added => entries.insert(ix, entry.unwrap()), - PathChange::Removed => drop(entries.remove(ix)), - PathChange::Updated => { - let entry = entry.unwrap(); - let existing_entry = entries.get_mut(ix).unwrap(); - assert_eq!(existing_entry.path, entry.path); - *existing_entry = entry; - } - PathChange::AddedOrUpdated | PathChange::Loaded => { - let entry = entry.unwrap(); - if entries.get(ix).map(|e| &e.path) == Some(&entry.path) { - *entries.get_mut(ix).unwrap() = entry; - } else { - entries.insert(ix, entry); - } - } - } - } - - let new_entries = tree.entries(true, 0).cloned().collect::>(); - assert_eq!(entries, new_entries, "incorrect changes: {:?}", changes); - } - }) - .detach(); -} - -fn randomly_mutate_worktree( - worktree: &mut Worktree, - rng: &mut impl Rng, - cx: &mut Context, -) -> Task> { - log::info!("mutating worktree"); - let worktree = worktree.as_local_mut().unwrap(); - let snapshot = worktree.snapshot(); - let entry = snapshot.entries(false, 0).choose(rng).unwrap(); - - match rng.random_range(0_u32..100) { - 0..=33 if entry.path.as_ref() != RelPath::empty() => { - log::info!("deleting entry {:?} ({})", entry.path, entry.id.0); - worktree.delete_entry(entry.id, false, cx).unwrap() - } - _ => { - if entry.is_dir() { - let child_path = entry.path.join(rel_path(&random_filename(rng))); - let is_dir = rng.random_bool(0.3); - log::info!( - "creating {} at {:?}", - if is_dir { "dir" } else { "file" }, - child_path, - ); - let task = worktree.create_entry(child_path, is_dir, None, cx); - cx.background_spawn(async move { - task.await?; - Ok(()) - }) - } else { - log::info!("overwriting file {:?} ({})", &entry.path, entry.id.0); - let task = - worktree.write_file(entry.path.clone(), "".into(), Default::default(), cx); - cx.background_spawn(async move { - task.await?; - Ok(()) - }) - } - } - } -} - -async fn randomly_mutate_fs( - fs: &Arc, - root_path: &Path, - insertion_probability: f64, - rng: &mut impl Rng, -) { - log::info!("mutating fs"); - let mut files = Vec::new(); - let mut dirs = Vec::new(); - for path in fs.as_fake().paths(false) { - if path.starts_with(root_path) { - if fs.is_file(&path).await { - files.push(path); - } else { - dirs.push(path); - } - } - } - - if (files.is_empty() && dirs.len() == 1) || rng.random_bool(insertion_probability) { - let path = dirs.choose(rng).unwrap(); - let new_path = path.join(random_filename(rng)); - - if rng.random() { - log::info!( - "creating dir {:?}", - new_path.strip_prefix(root_path).unwrap() - ); - fs.create_dir(&new_path).await.unwrap(); - } else { - log::info!( - "creating file {:?}", - new_path.strip_prefix(root_path).unwrap() - ); - fs.create_file(&new_path, Default::default()).await.unwrap(); - } - } else if rng.random_bool(0.05) { - let ignore_dir_path = dirs.choose(rng).unwrap(); - let ignore_path = ignore_dir_path.join(GITIGNORE); - - let subdirs = dirs - .iter() - .filter(|d| d.starts_with(ignore_dir_path)) - .cloned() - .collect::>(); - let subfiles = files - .iter() - .filter(|d| d.starts_with(ignore_dir_path)) - .cloned() - .collect::>(); - let files_to_ignore = { - let len = rng.random_range(0..=subfiles.len()); - subfiles.choose_multiple(rng, len) - }; - let dirs_to_ignore = { - let len = rng.random_range(0..subdirs.len()); - subdirs.choose_multiple(rng, len) - }; - - let mut ignore_contents = String::new(); - for path_to_ignore in files_to_ignore.chain(dirs_to_ignore) { - writeln!( - ignore_contents, - "{}", - path_to_ignore - .strip_prefix(ignore_dir_path) - .unwrap() - .to_str() - .unwrap() - ) - .unwrap(); - } - log::info!( - "creating gitignore {:?} with contents:\n{}", - ignore_path.strip_prefix(root_path).unwrap(), - ignore_contents - ); - fs.save( - &ignore_path, - &ignore_contents.as_str().into(), - Default::default(), - ) - .await - .unwrap(); - } else { - let old_path = { - let file_path = files.choose(rng); - let dir_path = dirs[1..].choose(rng); - file_path.into_iter().chain(dir_path).choose(rng).unwrap() - }; - - let is_rename = rng.random(); - if is_rename { - let new_path_parent = dirs - .iter() - .filter(|d| !d.starts_with(old_path)) - .choose(rng) - .unwrap(); - - let overwrite_existing_dir = - !old_path.starts_with(new_path_parent) && rng.random_bool(0.3); - let new_path = if overwrite_existing_dir { - fs.remove_dir( - new_path_parent, - RemoveOptions { - recursive: true, - ignore_if_not_exists: true, - }, - ) - .await - .unwrap(); - new_path_parent.to_path_buf() - } else { - new_path_parent.join(random_filename(rng)) - }; - - log::info!( - "renaming {:?} to {}{:?}", - old_path.strip_prefix(root_path).unwrap(), - if overwrite_existing_dir { - "overwrite " - } else { - "" - }, - new_path.strip_prefix(root_path).unwrap() - ); - fs.rename( - old_path, - &new_path, - fs::RenameOptions { - overwrite: true, - ignore_if_exists: true, - create_parents: false, - }, - ) - .await - .unwrap(); - } else if fs.is_file(old_path).await { - log::info!( - "deleting file {:?}", - old_path.strip_prefix(root_path).unwrap() - ); - fs.remove_file(old_path, Default::default()).await.unwrap(); - } else { - log::info!( - "deleting dir {:?}", - old_path.strip_prefix(root_path).unwrap() - ); - fs.remove_dir( - old_path, - RemoveOptions { - recursive: true, - ignore_if_not_exists: true, - }, - ) - .await - .unwrap(); - } - } -} - -fn random_filename(rng: &mut impl Rng) -> String { - (0..6) - .map(|_| rng.sample(rand::distr::Alphanumeric)) - .map(char::from) - .collect() -} - -#[gpui::test] -async fn test_private_single_file_worktree(cx: &mut TestAppContext) { - init_test(cx); - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree("/", json!({".env": "PRIVATE=secret\n"})) - .await; - let tree = Worktree::local( - Path::new("/.env"), - true, - fs.clone(), - Default::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) - .await; - tree.read_with(cx, |tree, _| { - let entry = tree.entry_for_path(rel_path("")).unwrap(); - assert!(entry.is_private); - }); -} - -#[gpui::test] -async fn test_repository_above_root(executor: BackgroundExecutor, cx: &mut TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(executor); - fs.insert_tree( - path!("/root"), - json!({ - ".git": {}, - "subproject": { - "a.txt": "A" - } - }), - ) - .await; - let worktree = Worktree::local( - path!("/root/subproject").as_ref(), - true, - fs.clone(), - Arc::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - worktree - .update(cx, |worktree, _| { - worktree.as_local().unwrap().scan_complete() - }) - .await; - cx.run_until_parked(); - let repos = worktree.update(cx, |worktree, _| { - worktree - .as_local() - .unwrap() - .git_repositories - .values() - .map(|entry| entry.work_directory_abs_path.clone()) - .collect::>() - }); - pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]); - - fs.touch_path(path!("/root/subproject")).await; - worktree - .update(cx, |worktree, _| { - worktree.as_local().unwrap().scan_complete() - }) - .await; - cx.run_until_parked(); - - let repos = worktree.update(cx, |worktree, _| { - worktree - .as_local() - .unwrap() - .git_repositories - .values() - .map(|entry| entry.work_directory_abs_path.clone()) - .collect::>() - }); - pretty_assertions::assert_eq!(repos, [Path::new(path!("/root")).into()]); -} - -#[gpui::test] -async fn test_global_gitignore(executor: BackgroundExecutor, cx: &mut TestAppContext) { - init_test(cx); - - let home = paths::home_dir(); - let fs = FakeFs::new(executor); - fs.insert_tree( - home, - json!({ - ".config": { - "git": { - "ignore": "foo\n/bar\nbaz\n" - } - }, - "project": { - ".git": {}, - ".gitignore": "!baz", - "foo": "", - "bar": "", - "sub": { - "bar": "", - }, - "subrepo": { - ".git": {}, - "bar": "" - }, - "baz": "" - } - }), - ) - .await; - let worktree = Worktree::local( - home.join("project"), - true, - fs.clone(), - Arc::default(), - true, - &mut cx.to_async(), - ) - .await - .unwrap(); - worktree - .update(cx, |worktree, _| { - worktree.as_local().unwrap().scan_complete() - }) - .await; - cx.run_until_parked(); - - // .gitignore overrides excludesFile, and anchored paths in excludesFile are resolved - // relative to the nearest containing repository - worktree.update(cx, |worktree, _cx| { - check_worktree_entries( - worktree, - &[], - &["foo", "bar", "subrepo/bar"], - &["sub/bar", "baz"], - &[], - ); - }); - - // Ignore statuses are updated when excludesFile changes - fs.write( - &home.join(".config").join("git").join("ignore"), - "/bar\nbaz\n".as_bytes(), - ) - .await - .unwrap(); - worktree - .update(cx, |worktree, _| { - worktree.as_local().unwrap().scan_complete() - }) - .await; - cx.run_until_parked(); - - worktree.update(cx, |worktree, _cx| { - check_worktree_entries( - worktree, - &[], - &["bar", "subrepo/bar"], - &["foo", "sub/bar", "baz"], - &[], - ); - }); - - // Statuses are updated when .git added/removed - fs.remove_dir( - &home.join("project").join("subrepo").join(".git"), - RemoveOptions { - recursive: true, - ..Default::default() - }, - ) - .await - .unwrap(); - worktree - .update(cx, |worktree, _| { - worktree.as_local().unwrap().scan_complete() - }) - .await; - cx.run_until_parked(); - - worktree.update(cx, |worktree, _cx| { - check_worktree_entries( - worktree, - &[], - &["bar"], - &["foo", "sub/bar", "baz", "subrepo/bar"], - &[], - ); - }); -} - -#[track_caller] -fn check_worktree_entries( - tree: &Worktree, - expected_excluded_paths: &[&str], - expected_ignored_paths: &[&str], - expected_tracked_paths: &[&str], - expected_included_paths: &[&str], -) { - for path in expected_excluded_paths { - let entry = tree.entry_for_path(rel_path(path)); - assert!( - entry.is_none(), - "expected path '{path}' to be excluded, but got entry: {entry:?}", - ); - } - for path in expected_ignored_paths { - let entry = tree - .entry_for_path(rel_path(path)) - .unwrap_or_else(|| panic!("Missing entry for expected ignored path '{path}'")); - assert!( - entry.is_ignored, - "expected path '{path}' to be ignored, but got entry: {entry:?}", - ); - } - for path in expected_tracked_paths { - let entry = tree - .entry_for_path(rel_path(path)) - .unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'")); - assert!( - !entry.is_ignored || entry.is_always_included, - "expected path '{path}' to be tracked, but got entry: {entry:?}", - ); - } - for path in expected_included_paths { - let entry = tree - .entry_for_path(rel_path(path)) - .unwrap_or_else(|| panic!("Missing entry for expected included path '{path}'")); - assert!( - entry.is_always_included, - "expected path '{path}' to always be included, but got entry: {entry:?}", - ); - } -} - -fn init_test(cx: &mut gpui::TestAppContext) { - zlog::init_test(); - - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - }); -} diff --git a/crates/worktree_benchmarks/Cargo.toml b/crates/worktree_benchmarks/Cargo.toml deleted file mode 100644 index 29681573ad..0000000000 --- a/crates/worktree_benchmarks/Cargo.toml +++ /dev/null @@ -1,14 +0,0 @@ -[package] -name = "worktree_benchmarks" -version = "0.1.0" -publish.workspace = true -edition.workspace = true - -[dependencies] -fs.workspace = true -gpui = { workspace = true, features = ["windows-manifest"] } -settings.workspace = true -worktree.workspace = true - -[lints] -workspace = true diff --git a/crates/worktree_benchmarks/LICENSE-GPL b/crates/worktree_benchmarks/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/worktree_benchmarks/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/worktree_benchmarks/src/main.rs b/crates/worktree_benchmarks/src/main.rs deleted file mode 100644 index 00f268b75f..0000000000 --- a/crates/worktree_benchmarks/src/main.rs +++ /dev/null @@ -1,53 +0,0 @@ -use std::{ - path::Path, - sync::{Arc, atomic::AtomicUsize}, -}; - -use fs::RealFs; -use gpui::Application; -use settings::Settings; -use worktree::{Worktree, WorktreeSettings}; - -fn main() { - let Some(worktree_root_path) = std::env::args().nth(1) else { - println!( - "Missing path to worktree root\nUsage: bench_background_scan PATH_TO_WORKTREE_ROOT" - ); - return; - }; - let app = Application::headless(); - - app.run(|cx| { - settings::init(cx); - let fs = Arc::new(RealFs::new(None, cx.background_executor().clone())); - - cx.spawn(async move |cx| { - let worktree = Worktree::local( - Path::new(&worktree_root_path), - true, - fs, - Arc::new(AtomicUsize::new(0)), - cx, - ) - .await - .expect("Worktree initialization to succeed"); - let did_finish_scan = worktree - .update(cx, |this, _| this.as_local().unwrap().scan_complete()) - .unwrap(); - let start = std::time::Instant::now(); - did_finish_scan.await; - let elapsed = start.elapsed(); - let (files, directories) = worktree - .read_with(cx, |this, _| (this.file_count(), this.dir_count())) - .unwrap(); - println!( - "{:?} for {directories} directories and {files} files", - elapsed - ); - cx.update(|cx| { - cx.quit(); - }) - }) - .detach(); - }) -} diff --git a/crates/x_ai/Cargo.toml b/crates/x_ai/Cargo.toml deleted file mode 100644 index 8ff020df8c..0000000000 --- a/crates/x_ai/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "x_ai" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/x_ai.rs" - -[features] -default = [] -schemars = ["dep:schemars"] - -[dependencies] -anyhow.workspace = true -schemars = { workspace = true, optional = true } -serde.workspace = true -strum.workspace = true diff --git a/crates/x_ai/LICENSE-GPL b/crates/x_ai/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/x_ai/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/x_ai/src/x_ai.rs b/crates/x_ai/src/x_ai.rs deleted file mode 100644 index 072a893a6a..0000000000 --- a/crates/x_ai/src/x_ai.rs +++ /dev/null @@ -1,208 +0,0 @@ -use anyhow::Result; -use serde::{Deserialize, Serialize}; -use strum::EnumIter; - -pub const XAI_API_URL: &str = "https://api.x.ai/v1"; - -#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] -#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, EnumIter)] -pub enum Model { - #[serde(rename = "grok-2-vision-latest")] - Grok2Vision, - #[default] - #[serde(rename = "grok-3-latest")] - Grok3, - #[serde(rename = "grok-3-mini-latest")] - Grok3Mini, - #[serde(rename = "grok-3-fast-latest")] - Grok3Fast, - #[serde(rename = "grok-3-mini-fast-latest")] - Grok3MiniFast, - #[serde(rename = "grok-4", alias = "grok-4-latest")] - Grok4, - #[serde( - rename = "grok-4-fast-reasoning", - alias = "grok-4-fast-reasoning-latest" - )] - Grok4FastReasoning, - #[serde( - rename = "grok-4-fast-non-reasoning", - alias = "grok-4-fast-non-reasoning-latest" - )] - Grok4FastNonReasoning, - #[serde( - rename = "grok-4-1-fast-non-reasoning", - alias = "grok-4-1-fast-non-reasoning-latest" - )] - Grok41FastNonReasoning, - #[serde( - rename = "grok-4-1-fast-reasoning", - alias = "grok-4-1-fast-reasoning-latest", - alias = "grok-4-1-fast" - )] - Grok41FastReasoning, - #[serde(rename = "grok-code-fast-1", alias = "grok-code-fast-1-0825")] - GrokCodeFast1, - #[serde(rename = "custom")] - Custom { - name: String, - /// The name displayed in the UI, such as in the assistant panel model dropdown menu. - display_name: Option, - max_tokens: u64, - max_output_tokens: Option, - max_completion_tokens: Option, - supports_images: Option, - supports_tools: Option, - parallel_tool_calls: Option, - }, -} - -impl Model { - pub fn default_fast() -> Self { - Self::Grok3Fast - } - - pub fn from_id(id: &str) -> Result { - match id { - "grok-4" => Ok(Self::Grok4), - "grok-4-fast-reasoning" => Ok(Self::Grok4FastReasoning), - "grok-4-fast-non-reasoning" => Ok(Self::Grok4FastNonReasoning), - "grok-4-1-fast-non-reasoning" => Ok(Self::Grok41FastNonReasoning), - "grok-4-1-fast-reasoning" => Ok(Self::Grok41FastReasoning), - "grok-4-1-fast" => Ok(Self::Grok41FastReasoning), - "grok-2-vision" => Ok(Self::Grok2Vision), - "grok-3" => Ok(Self::Grok3), - "grok-3-mini" => Ok(Self::Grok3Mini), - "grok-3-fast" => Ok(Self::Grok3Fast), - "grok-3-mini-fast" => Ok(Self::Grok3MiniFast), - "grok-code-fast-1" => Ok(Self::GrokCodeFast1), - _ => anyhow::bail!("invalid model id '{id}'"), - } - } - - pub fn id(&self) -> &str { - match self { - Self::Grok2Vision => "grok-2-vision", - Self::Grok3 => "grok-3", - Self::Grok3Mini => "grok-3-mini", - Self::Grok3Fast => "grok-3-fast", - Self::Grok3MiniFast => "grok-3-mini-fast", - Self::Grok4 => "grok-4", - Self::Grok4FastReasoning => "grok-4-fast-reasoning", - Self::Grok4FastNonReasoning => "grok-4-fast-non-reasoning", - Self::Grok41FastNonReasoning => "grok-4-1-fast-non-reasoning", - Self::Grok41FastReasoning => "grok-4-1-fast-reasoning", - Self::GrokCodeFast1 => "grok-code-fast-1", - Self::Custom { name, .. } => name, - } - } - - pub fn display_name(&self) -> &str { - match self { - Self::Grok2Vision => "Grok 2 Vision", - Self::Grok3 => "Grok 3", - Self::Grok3Mini => "Grok 3 Mini", - Self::Grok3Fast => "Grok 3 Fast", - Self::Grok3MiniFast => "Grok 3 Mini Fast", - Self::Grok4 => "Grok 4", - Self::Grok4FastReasoning => "Grok 4 Fast", - Self::Grok4FastNonReasoning => "Grok 4 Fast (Non-Reasoning)", - Self::Grok41FastNonReasoning => "Grok 4.1 Fast (Non-Reasoning)", - Self::Grok41FastReasoning => "Grok 4.1 Fast", - Self::GrokCodeFast1 => "Grok Code Fast 1", - Self::Custom { - name, display_name, .. - } => display_name.as_ref().unwrap_or(name), - } - } - - pub fn max_token_count(&self) -> u64 { - match self { - Self::Grok3 | Self::Grok3Mini | Self::Grok3Fast | Self::Grok3MiniFast => 131_072, - Self::Grok4 | Self::GrokCodeFast1 => 256_000, - Self::Grok4FastReasoning - | Self::Grok4FastNonReasoning - | Self::Grok41FastNonReasoning - | Self::Grok41FastReasoning => 2_000_000, - Self::Grok2Vision => 8_192, - Self::Custom { max_tokens, .. } => *max_tokens, - } - } - - pub fn max_output_tokens(&self) -> Option { - match self { - Self::Grok3 | Self::Grok3Mini | Self::Grok3Fast | Self::Grok3MiniFast => Some(8_192), - Self::Grok4 - | Self::Grok4FastReasoning - | Self::Grok4FastNonReasoning - | Self::Grok41FastNonReasoning - | Self::Grok41FastReasoning - | Self::GrokCodeFast1 => Some(64_000), - Self::Grok2Vision => Some(4_096), - Self::Custom { - max_output_tokens, .. - } => *max_output_tokens, - } - } - - pub fn supports_parallel_tool_calls(&self) -> bool { - match self { - Self::Grok2Vision - | Self::Grok3 - | Self::Grok3Mini - | Self::Grok3Fast - | Self::Grok3MiniFast - | Self::Grok4 - | Self::Grok4FastReasoning - | Self::Grok4FastNonReasoning - | Self::Grok41FastNonReasoning - | Self::Grok41FastReasoning => true, - Self::Custom { - parallel_tool_calls: Some(support), - .. - } => *support, - Self::GrokCodeFast1 | Model::Custom { .. } => false, - } - } - - pub fn supports_prompt_cache_key(&self) -> bool { - false - } - - pub fn supports_tool(&self) -> bool { - match self { - Self::Grok2Vision - | Self::Grok3 - | Self::Grok3Mini - | Self::Grok3Fast - | Self::Grok3MiniFast - | Self::Grok4 - | Self::Grok4FastReasoning - | Self::Grok4FastNonReasoning - | Self::Grok41FastNonReasoning - | Self::Grok41FastReasoning - | Self::GrokCodeFast1 => true, - Self::Custom { - supports_tools: Some(support), - .. - } => *support, - Model::Custom { .. } => false, - } - } - - pub fn supports_images(&self) -> bool { - match self { - Self::Grok2Vision - | Self::Grok4 - | Self::Grok4FastReasoning - | Self::Grok4FastNonReasoning - | Self::Grok41FastNonReasoning - | Self::Grok41FastReasoning => true, - Self::Custom { - supports_images: Some(support), - .. - } => *support, - _ => false, - } - } -} diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml deleted file mode 100644 index 92a274da96..0000000000 --- a/crates/zed/Cargo.toml +++ /dev/null @@ -1,231 +0,0 @@ -[package] -description = "The fast, collaborative code editor." -edition.workspace = true -name = "zed" -version = "0.218.0" -publish.workspace = true -license = "GPL-3.0-or-later" -authors = ["Zed Team "] - -[lints] -workspace = true - -[features] -tracy = ["ztracing/tracy"] - -[[bin]] -name = "zed" -path = "src/zed-main.rs" - -[lib] -name = "zed" -path = "src/main.rs" - -[dependencies] -acp_tools.workspace = true -activity_indicator.workspace = true -agent_settings.workspace = true -agent_ui.workspace = true -anyhow.workspace = true -askpass.workspace = true -assets.workspace = true -audio.workspace = true -auto_update.workspace = true -auto_update_ui.workspace = true -bincode.workspace = true -breadcrumbs.workspace = true -call.workspace = true -channel.workspace = true -clap.workspace = true -cli.workspace = true -client.workspace = true -codestral.workspace = true -collab_ui.workspace = true -collections.workspace = true -command_palette.workspace = true -component.workspace = true -copilot.workspace = true -crashes.workspace = true -dap_adapters.workspace = true -db.workspace = true -debug_adapter_extension.workspace = true -debugger_tools.workspace = true -debugger_ui.workspace = true -diagnostics.workspace = true -editor.workspace = true -env_logger.workspace = true -extension.workspace = true -extension_host.workspace = true -extensions_ui.workspace = true -feature_flags.workspace = true -feedback.workspace = true -file_finder.workspace = true -fs.workspace = true -futures.workspace = true -git.workspace = true -git_hosting_providers.workspace = true -git_ui.workspace = true -go_to_line.workspace = true -system_specs.workspace = true -gpui = { workspace = true, features = [ - "wayland", - "x11", - "font-kit", - "windows-manifest", -] } -gpui_tokio.workspace = true -rayon.workspace = true - -edit_prediction.workspace = true -edit_prediction_ui.workspace = true -http_client.workspace = true -image_viewer.workspace = true -inspector_ui.workspace = true -install_cli.workspace = true -journal.workspace = true -json_schema_store.workspace = true -keymap_editor.workspace = true -language.workspace = true -language_extension.workspace = true -language_model.workspace = true -language_models.workspace = true -language_onboarding.workspace = true -language_selector.workspace = true -language_tools.workspace = true -languages = { workspace = true, features = ["load-grammars"] } -line_ending_selector.workspace = true -log.workspace = true -markdown.workspace = true -markdown_preview.workspace = true -menu.workspace = true -migrator.workspace = true -miniprofiler_ui.workspace = true -mimalloc = { version = "0.1", optional = true } -nc.workspace = true -node_runtime.workspace = true -notifications.workspace = true -onboarding.workspace = true -outline.workspace = true -outline_panel.workspace = true -parking_lot.workspace = true -paths.workspace = true -picker.workspace = true -profiling.workspace = true -project.workspace = true -project_panel.workspace = true -project_symbols.workspace = true -prompt_store.workspace = true -proto.workspace = true -recent_projects.workspace = true -release_channel.workspace = true -remote.workspace = true -repl.workspace = true -reqwest.workspace = true -reqwest_client.workspace = true -rope.workspace = true -search.workspace = true -serde.workspace = true -serde_json.workspace = true -session.workspace = true -settings.workspace = true -settings_profile_selector.workspace = true -settings_ui.workspace = true -shellexpand.workspace = true -smol.workspace = true -snippet_provider.workspace = true -snippets_ui.workspace = true -supermaven.workspace = true -svg_preview.workspace = true -sysinfo.workspace = true -tab_switcher.workspace = true -task.workspace = true -tasks_ui.workspace = true -telemetry.workspace = true -terminal_view.workspace = true -theme.workspace = true -theme_extension.workspace = true -theme_selector.workspace = true -time.workspace = true -title_bar.workspace = true -ztracing.workspace = true -tracing.workspace = true -toolchain_selector.workspace = true -ui.workspace = true -ui_input.workspace = true -ui_prompt.workspace = true -url.workspace = true -urlencoding.workspace = true -util.workspace = true -uuid.workspace = true -vim.workspace = true -vim_mode_setting.workspace = true -watch.workspace = true -web_search.workspace = true -web_search_providers.workspace = true -workspace.workspace = true -zed_actions.workspace = true -zed_env_vars.workspace = true -zlog.workspace = true -zlog_settings.workspace = true -chrono.workspace = true - -[target.'cfg(target_os = "windows")'.dependencies] -windows.workspace = true -chrono.workspace = true - -[target.'cfg(target_os = "windows")'.build-dependencies] -winresource = "0.1" - -[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies] -ashpd.workspace = true - -[dev-dependencies] -call = { workspace = true, features = ["test-support"] } -dap = { workspace = true, features = ["test-support"] } -editor = { workspace = true, features = ["test-support"] } -gpui = { workspace = true, features = ["test-support"] } -image_viewer = { workspace = true, features = ["test-support"] } -itertools.workspace = true -language = { workspace = true, features = ["test-support"] } -pretty_assertions.workspace = true -project = { workspace = true, features = ["test-support"] } -semver.workspace = true -terminal_view = { workspace = true, features = ["test-support"] } -tree-sitter-md.workspace = true -tree-sitter-rust.workspace = true -workspace = { workspace = true, features = ["test-support"] } - -[package.metadata.bundle-dev] -icon = ["resources/app-icon-dev@2x.png", "resources/app-icon-dev.png"] -identifier = "dev.zed.Zed-Dev" -name = "Zed Dev" -osx_minimum_system_version = "10.15.7" -osx_info_plist_exts = ["resources/info/*"] -osx_url_schemes = ["zed"] - -[package.metadata.bundle-nightly] -icon = ["resources/app-icon-nightly@2x.png", "resources/app-icon-nightly.png"] -identifier = "dev.zed.Zed-Nightly" -name = "Zed Nightly" -osx_minimum_system_version = "10.15.7" -osx_info_plist_exts = ["resources/info/*"] -osx_url_schemes = ["zed"] - -[package.metadata.bundle-preview] -icon = ["resources/app-icon-preview@2x.png", "resources/app-icon-preview.png"] -identifier = "dev.zed.Zed-Preview" -name = "Zed Preview" -osx_minimum_system_version = "10.15.7" -osx_info_plist_exts = ["resources/info/*"] -osx_url_schemes = ["zed"] - -[package.metadata.bundle-stable] -icon = ["resources/app-icon@2x.png", "resources/app-icon.png"] -identifier = "dev.zed.Zed" -name = "Zed" -osx_minimum_system_version = "10.15.7" -osx_info_plist_exts = ["resources/info/*"] -osx_url_schemes = ["zed"] - -[package.metadata.cargo-machete] -ignored = ["profiling", "zstd", "tracing"] diff --git a/crates/zed/LICENSE-GPL b/crates/zed/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/zed/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/zed/RELEASE_CHANNEL b/crates/zed/RELEASE_CHANNEL deleted file mode 100644 index 38f8e886e1..0000000000 --- a/crates/zed/RELEASE_CHANNEL +++ /dev/null @@ -1 +0,0 @@ -dev diff --git a/crates/zed/build.rs b/crates/zed/build.rs deleted file mode 100644 index dd26a1152e..0000000000 --- a/crates/zed/build.rs +++ /dev/null @@ -1,105 +0,0 @@ -#![allow(clippy::disallowed_methods, reason = "build scripts are exempt")] -use std::process::Command; - -fn main() { - if cfg!(target_os = "macos") { - println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.15.7"); - - // Weakly link ReplayKit to ensure Zed can be used on macOS 10.15+. - println!("cargo:rustc-link-arg=-Wl,-weak_framework,ReplayKit"); - - // Seems to be required to enable Swift concurrency - println!("cargo:rustc-link-arg=-Wl,-rpath,/usr/lib/swift"); - - // Register exported Objective-C selectors, protocols, etc - println!("cargo:rustc-link-arg=-Wl,-ObjC"); - - // weak link to support Catalina - println!("cargo:rustc-link-arg=-Wl,-weak_framework,ScreenCaptureKit"); - } - - // Populate git sha environment variable if git is available - println!("cargo:rerun-if-changed=../../.git/logs/HEAD"); - println!( - "cargo:rustc-env=TARGET={}", - std::env::var("TARGET").unwrap() - ); - if let Ok(output) = Command::new("git").args(["rev-parse", "HEAD"]).output() - && output.status.success() - { - let git_sha = String::from_utf8_lossy(&output.stdout); - let git_sha = git_sha.trim(); - - println!("cargo:rustc-env=ZED_COMMIT_SHA={git_sha}"); - - if let Some(build_identifier) = option_env!("GITHUB_RUN_NUMBER") { - println!("cargo:rustc-env=ZED_BUILD_ID={build_identifier}"); - } - - if let Ok(build_profile) = std::env::var("PROFILE") - && build_profile == "release" - { - // This is currently the best way to make `cargo build ...`'s build script - // to print something to stdout without extra verbosity. - println!("cargo::warning=Info: using '{git_sha}' hash for ZED_COMMIT_SHA env var"); - } - } - - #[cfg(target_os = "windows")] - { - #[cfg(target_env = "msvc")] - { - // todo(windows): This is to avoid stack overflow. Remove it when solved. - println!("cargo:rustc-link-arg=/stack:{}", 8 * 1024 * 1024); - } - - if cfg!(target_arch = "x86_64") { - println!("cargo::rerun-if-changed=resources\\windows\\bin\\x64\\conpty.dll"); - println!("cargo::rerun-if-changed=resources\\windows\\bin\\x64\\OpenConsole.exe"); - let conpty_target = std::env::var("OUT_DIR").unwrap() + "\\..\\..\\..\\conpty.dll"; - match std::fs::copy("resources/windows/bin/x64/conpty.dll", &conpty_target) { - Ok(_) => println!("Copied conpty.dll to {conpty_target}"), - Err(e) => println!("cargo::warning=Failed to copy conpty.dll: {}", e), - } - let open_console_target = - std::env::var("OUT_DIR").unwrap() + "\\..\\..\\..\\OpenConsole.exe"; - match std::fs::copy( - "resources/windows/bin/x64/OpenConsole.exe", - &open_console_target, - ) { - Ok(_) => println!("Copied OpenConsole.exe to {open_console_target}"), - Err(e) => println!("cargo::warning=Failed to copy OpenConsole.exe: {}", e), - } - } - - let release_channel = option_env!("RELEASE_CHANNEL").unwrap_or("dev"); - let icon = match release_channel { - "stable" => "resources/windows/app-icon.ico", - "preview" => "resources/windows/app-icon-preview.ico", - "nightly" => "resources/windows/app-icon-nightly.ico", - "dev" => "resources/windows/app-icon-dev.ico", - _ => "resources/windows/app-icon-dev.ico", - }; - let icon = std::path::Path::new(icon); - - println!("cargo:rerun-if-env-changed=RELEASE_CHANNEL"); - println!("cargo:rerun-if-changed={}", icon.display()); - - let mut res = winresource::WindowsResource::new(); - - // Depending on the security applied to the computer, winresource might fail - // fetching the RC path. Therefore, we add a way to explicitly specify the - // toolkit path, allowing winresource to use a valid RC path. - if let Some(explicit_rc_toolkit_path) = std::env::var("ZED_RC_TOOLKIT_PATH").ok() { - res.set_toolkit_path(explicit_rc_toolkit_path.as_str()); - } - res.set_icon(icon.to_str().unwrap()); - res.set("FileDescription", "Zed"); - res.set("ProductName", "Zed"); - - if let Err(e) = res.compile() { - eprintln!("{}", e); - std::process::exit(1); - } - } -} diff --git a/crates/zed/contents/dev/embedded.provisionprofile b/crates/zed/contents/dev/embedded.provisionprofile deleted file mode 100644 index 8979e1fb9fb72e9f5adbbc05d3498e6218016581..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12512 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 dW%r!D=*yLl-1Fw1WXXKmQE$9ByP|uj)3dn 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 a*7NeWFDQ&(klM8L$X$DlFNp3R9ByoeQ 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 diff --git a/crates/zed/resources/app-icon-dev.png b/crates/zed/resources/app-icon-dev.png deleted file mode 100644 index cbe2ac3ef717584f15188bc6992355ba72836582..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 194029 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

Uy)8Y+ZWOOK&3m}`lp%BYD&L^WjrW)4|vfrfx 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

{6!9nNV#ec!tH~L|nPbBT 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 z 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

X1r8RH?jiJ&nsGK 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�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%0AAXD_@~2XDI$+xWb20$kY`ys|ErIt4cgvl+pa@_ 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

K>c8jA7^DD+GgJmag-CMB0^8sp49 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�Apx#&>gmUnjktFXP&^kw5 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 diff --git a/crates/zed/resources/app-icon-nightly.png b/crates/zed/resources/app-icon-nightly.png deleted file mode 100644 index 776cd06b1bca36c74257dafbc4bffebbbc8f55ad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 242801 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

+J*DfI{u88oY`M7}&E}PHLAU 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

    t)j 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

    WBw05_@Lj1+0w&@5BvWA{_f<~U9Q%BmSl^7 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%0AiVshv@A0Q*0M3C)}` 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 ~Wiw*g?F2q_RP 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 aN1cpG-kC~9V4{Tn5)faFDS4#RmcNC|^H;LMH} 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≠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

    wibT(J_@q= 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 bd;t68F!!G*O<;hQ%9nV!d%My8eKYMZ9A(F| diff --git a/crates/zed/resources/app-icon@2x.png b/crates/zed/resources/app-icon@2x.png deleted file mode 100644 index 5bb5754bc19c1ae81a7f1c1e48e0aa1c739486e3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 452124 zcmeEu1ydYR(=HZVLLj&XPq2mHiv)KFB)Bi`5P}323Bf&haNXd(K=1&IyYJ#|i@V;u z->t9ikGOA5O`X#@fo0|guN-w;6i zF8JSo`bA4Y8l`fSX78UNYo(`Tt)_e-@zrv#9>vC@AQ;s3_?FM$~^7 zN-o;}KE*)EMgM>E|GYT0v`>qIB88$P`{9!}>S3EhxwDc19kiqEy4lpX7^?@x&9}H} zYHDd}sk;-un-_|Sum{1B6gD<0BLl{dz?c6u{<{bN^@IP$!hiALznJ)6UidE^{x>D~|1T|C z&5G_^&oc6vF~-IY!om#dRgk=!sGF-a7He#2@jW`SaPvPnIT^m{>N}_4^jVU?Hlks4jbsMC_p;V^O&Rjrgd`SA-uY7gbv?K%+9Vart`}$xGg_*En6HP z8Dph>GgMF*<+qqsa2&MuMc{g^Rdk08wUqnV5<1a0KdkpZr9W64?7n>QYw@r53D-dL z60<|QGbPVJcj!5pHQPPPN@EORhxyQ=E8Xr%aH|SA54U=b$Ohm2y)Rz(%B`_Dy7OFa zgPBfNT>a;Pw_odGOuQA?hu8 z1qB5aVqVVV1*try%c558OpAZO4hP#K86o7ad{aUZ*x^Gtw_uN_2cPXcmm_*UQ;^t8 zBkDGhUBk(w;c7jEfcfveg^8hrG`F+E3JO?Q3Srxz^({WL&dsqbF)vG*%ot_`1z)eL*rUbf z4bRM|+QXbnl&JVznda`n&OK)8sNsoH)H|p-u*$Qf*Sm(vo*B88`G{0i$Px5FQ zk#ahSS?BHQn4uERcjwc4+Osz1KKHgO<%dhn2f)NyZ^Sdpy({284g58_oXwDNHUNtJ zM2pq=FoYk0+dd zO8kDhUIy*#`~V}3rj-d#_hAS!LZO9qbAWlg22X37CC5F*a$bmXm9~6KIoR&g#@hN> zKkF`bQ>s>_7w;w3n8Sq3YG6f@B9;z>Tp@nOt?vG`7kLA}o zha~gGZ_)O#rv?kYX&PSGvD&M)_c45|fxJHq1Om6~dtCAHMqa5azQ!IXBz633GCKgI zmuKHWk1MfWBV7}UQ&Z<%b05|MBoj$Sd32*O#c&2C{4n-O3mfvq_a8~y{3af=`9t^j zU2syIe(@9~;g{Z}NQgOU*Zw{?>;0a;>UyE4l&CP_iqY$Dsxr(my%M7ZX7|TQnXrW( z7gMUGQBeulhE8Y>={TIL3CHApN~)M(gVL;gDB4Dw`=Q>{71# zNUlt>V8j$B`f~tnD~M<^{#?ms_KV6Ff1TtU&^ZO4NmaXs#0Q(KIP<4=O{DJe<`Ud8 z9R!V*fWcK?Xw2SO`_a^(&ACzV*ME)x_Qz|8Z!i8bIo2o%>+Sbxi5}C;P3Jp z6=PjNSs#$c!DA9$Q?~wzoz4mmUsO6Qggyt%sgqaDgHv@qJ-tjt+S(o?D}LNAjrm{I zef1&0)%~B{#AgU}lmGA^!2tyWn&W-7AZbw-%XS#?xFKkM%j(;7YU0~}x*SE$)8^%C zgWx`E3+7f;JOOLisof@ssB6#YN zDb-ylFt`vo&u-x874Bf2U)5<(XyQQjc^~Vs#mBerEp&9AjQD~Dqh8HupO6l)f#ESTJC6&Ee3+Eu*JI8v<3|slciRBVd(X@q}GH_EW*QY#Q}ecSZ4HUdN8K>lUs` zuF;Ooz`tEjK~66y{atDL@ayFq<5=YIYk7?SSQ=_ho);nnw-*P53q}x6E%qys;J#Q_ zM0Ty8st23MH0+AzMB-|wxjjqVcb2mVtvoWTUCp=p>$$bK$)5Vzh)!j5{?@RhW4xJE z%Q0M8=tm1Q86p|##o27s(~sY7Z`ENDy&{09ny)^IblSeLftnnZSzkJsKCF{JOX^O> z{Y$HlZ87T6R%w?WN*~0CgZk(L9@dak5c%7c51X_=^Jr$gbKBvy0zqJZBF^Ig?DKdn~DTNN%nrkqyJAd|D?uj-;WfM zw~HRGwdo3g!WOilFNW5XiLy88=ap-Aw#6u56U`e8ibP&79P*z@wSN$D6l{(0D-BVO z^Xj4{6pDy#%{iNzqe8^_q#BxD@n`hHX)P^Wk`9CSS8j7g?}mVWZ^%v(+rQ7|0JR)| zp>)VKi?=35F?M;XMvjb}WKH4S%kE}N*YqSHc-VuR^u=pA)Rjcs1EUKC({U(70}^%n zxcwLZhM2@`G)iyWTZRozrQAoYK0ycPFuB>K?Aa+d|oD4hucl+L>KJ6{%oAt2jFXpzuwwW!8hle zUr;}euHM=#m^?HsKy%==xX)k7_qdUHFn=XvmcN*3QG7RA=wB`|l_%-uZD2DT?nKgO zdegljJj1_)VpelO?$hqlEk^gqFS;D|Vc2yMjwCIOfe|wWKN+)zcM#>r`orsHPtN}k z=lC4Sc+P!06=-2R5op0V{rv9E!4L~!+U9!=n`^(DsWPGGlEweWvdcQyyO^DbQjyP; zjGDyNTX)B`C6@|^Z=POcyZ}1$lXQ$XUbIx_EL{&G4$b;fvq1TQ3usRu!S7L>K%eUQ zr!hQv))&IaO>Y~u(#s!F2)h=s{FCFU+g~GjChDlD_pGM=Gj_>Ez$t$$VA{S zLncFJ1*Ax!@JUIC8KV%epUhLf&5G}NIG&*4n5A@X_2R;!_j5}r_Vy$OqUrBe&I!Rgm{8WR2QJsu77_QO2ezxA?bsfq1pmZ5X=URm- zxM%^zyT52t9?{6lvhM)Si`J97RZul?(tkT73Kksi7pd1cQBt5eNYu$^=H^{X+&{w* z6#Dx2-9jJr4P|ae7~AGM%+*0Cs5L+0cKu%zF9_!4@2Je8SMi40hAgN}LkcOp2r*Lw z`nINwk@fgbM{mHy=Cw|Sazi`Cl&ix`z4o)&i7*^O%%|50uF(^Gn;WNcD|RFgjL$R$ zj&b1fWQTK361PnsIPTv>5Qg6>NUVyxb6zj*v{kwbs>hJ4?r&sB<=^*B^|E)=jfA%g za8y>gNEaKn=0tF+1RP|vKCyf2`Vbg>P`IyN;;~QIQE;3XnU3SQ;YQb&3pNy&TVaCD zI6D?8%ayt+=MTk++zj)42_!TkXmqLG59t0^`mm9EO}c&AAFc@Ak+^tp`E@y#zA%>Y z<#|`d^lnlTkSwYx*R70)%6KBhzzh2A%dpSL7Sbt(6VwZ$e^^bta&L2KAUSKCx4U5y zRPY@|VRspX6%uHiO>J_CPidg6#cN#j$F+W&2-U}sp6%+*Ncj6z(;{qTbXhB-`pdfy zua%&8K9en{GDPB^San_6eL!Z%0^o8o?NSHGR;{}6w;7Ruu9cYQ;6Y1h?W-FsO4N+xe- zDIdz8Lkl+fZCs5T<9YgRZA}<$%#pWE8znu2p8wRMK-XF&7xaGiE@{~FAlWHXJgjYk zV}6JJojfyPkFAS~@5wI-x3?YqsGN%)5sxqG6Y*>A4wBHp(&YD}y?Erx^nJbzSBKxz zH&qqlD6V z=Jgyk3fkB&f`pbS=)7Esk$u^p356t&7XOhP9nvC4W_YXky3+f@7Zmrdnw)twroL}F zFFbXtW&yi*8FOMn6rn1+@HoB@?NBMBV52>mbU#?zcjL{z< z{f~qK&d;BZ@#P}^C04FE47smSu9al^2^e{UI@eDjIZtH1e2-wyhznXl!-r~PeNzsS zKS3mdJTE5YBv=_D=x07q=POVa zbMy(Pyr7)qkd$YlKYNY-kNYgE+!N5`J;57*^Q3T($>qvZipW-1DHy7GG9P11! z(&(#%*xL^w?a_^O34MTmAlw>g9T2$O5R(BuH{vRW^QGJ1ccpPh9X&;O?p*AMoI|fs17qhOd17yRSq1rv~*mvc@3W#zn)iJ7q_~D z4JZ-4&1}CPmff2wp%Yoc33SVhVDs=CV!&QU6GSF zNnH4VtS(tTP;!$9!ujm!L}k9Uo`fxV7hv1~#whr%9Vp6HN}piKES)~)^9$uo6{#%7=3jy3?fu+GzDOU2I&6ol*0r)$K08?I*`mKo~ zif--H;JkH@q+LiEl9&Zf`K8jOR&#aCyCRaJ^=LNEN+%&`nu<_B$9~B$bjv0^tl;zb ztczbT>NC3hJ*ux)XTbg9InP5%QNY&8?y9cF@uA;UQL4$LF9y}`9i|_cUixwsOClZ5~etL3c z;K&gr$3oDn6lrByI+3y-J^xa3I*hky1^>d^LMHv2&(S5e1K-X5#B zp+pJ>I#Daj@-<`|=W;RPQjb>C#o4O!p0={>6qdF-=t1*Ex6%mE_9-vp3P%)MbZyLFyffIvpsP z0RxlR`{{OPB{BPqr~P`i0kHV*X3YEHkIGSLg&uOk#s@`z*l14Qb1li5gN4^D{7C>0w#DFs3~_am zK5i5uU`}`s0r}%7niLN2nAnJa?9>;tTj9Ij1d}(ns(fE+TGK*}G9X8vjC{3{E%cMM zw4ErO+LgIg)RJpey(FG3;l(K{vmF(mIXjX=lKbQAP7DkSKg72FPP`N0Pm+Tzev-r=jy}u~U=S$^i+e@dyw6rUEN(5fp`PhS|!I z+dRnUTKd)n{RI@IENrXbxU^vKZ2FjIpr~H5#qeDlZ-Gb%MH+t)3APK1P|17y12zVE z&GZ)3Pz){X(`7UF#Xo_7pFGRxtw7%t$=?q$sOR}oD!g5XmiL$4B^V&RN(2{CZQ{!CfYqZ@ z*OS;yV-bnsx7uoupGEw#{6{QXaN&hq&p|l^n7ZbZ0d;;*BW_mhk|8mt`D2eu^tsLq!ak?5K?FJd{y-buUZoI z46*!oG#6BaUGBEX-N5l5-R><~)OFi2I8`G@?7GiM=d>H@aoKXn-L&Mr@q-KJEK#lB zJpFB|%|`x9mqRH2@MtlZEW*zW_ z(?(#QztP}If+$@XWi zvh5=rlP^iew|5IVuRe+6SWH+mpx6AutFBK1g`;#T4lZg;J)hDU8@9D^SXaH3Gj$N@ zOPGV*4WT54$}G8gtZ`jn*Z6IMmPm45?bW&E_M#lc9~a7`>^T|c`-yO-UKb5-cMMTf zD#GsAV(WaZ#;w<9O(zYFdAj}c!ILi&)xqPup}d=8fj(rQ@#Y8lfK``wN$lImVJHj` zUK1sFS|S|!#im=ZP#h6z>0$+;v7(5ZXwG`!?zRVjhM_oMBn3Eaj+}76^xqXytRC?* z7|cxZZEWX_lL*;4#81vC0~5>?S{%G=!Ga7-X&LwEH4gObue2h{lw80z$?Qc0Upq6s%TNk0AK zxW=rv&q+lcE2D{mplUp>Lyq^AIS*#NJcMr;jdNYs5$c?|e*+VF)r+x5SPWgHrD=Ow zZojAiMHJ|!fE$wy!0|xZDuQYqpH272A$1Njoeu6|-TD z^RAaCHTR5JD9w(wG99|<^L;0y;q#bZ{G_sM(a8tz?=NXtC|2e}z30?~>Kj-sj7!p} zTVBTTDNfNBlHbDcC$WcA0F)&8E85mL)fPT)0Byb&#A?8T zD(lK^g`#A&loWj6;cnA0wF4;39LB1mA+@V~#w=Qqzc`s-Nwkv?8XqTBV_1?+HeeSp zo+BG>Q)y%k|6IKbE5nRl?HgK~kO zYG(I+|7zW+4rjy*|0`IDw&b;8&OJ>E{_t-TgkE9X^SbW`Eia+?0zM8!{2U2>n?!pR?7 zIZzDe1*OVs9NU2*0uLM7fZ4C*44Nbm&so~y0)tnLu z7G~su>?XL3Ickz<>;H6yiQUjFaCx}Iu$$x zJQt;!J{hItNL4rdZv7XbvSyBpZ%qFat6+?WCZC7XzLmCyw)8h z9j=UehQF5RzG=j+ZzMV;4u|Cr8Edjk&Q>XDNs7yKPj3MrY9##9Vb5Zc;}VOcB#QjN zCh6psQ(e}TF`ZR8EpuKC8__8C+Q5s!ck67+eG(QE^!`>Q@xmLR++qTrP3)PVVA;ks zj7*dcuOn1kAwnhjc&H&4=u#)4E#K$xT8RU|YPk2RA-+`{{MWHi+=%p!ZuQ6KW=gYG z;k{D$yTbAsZ*6pgA?IJYRM?IUNo5^Q&y+(2swP0``{)3BZY9w3u5FvV=vQ*E1v3@3%D;{rioL5D>K ze!$0LTncH;_l&$TQf=ZuF1E^lp0oF<-uqcwL;Ue3QR8u@B1`*uSn3w%UWNP_*<;6H z;mJomHQ=GD7=IzjYD?~e0pqYC$;vd7l>ki=d)W@xW9Cb%Y5&pED%CjF{=GrM-D&p5 z?M%*JsF)T8lXGvjm>XtwH*x0e@pI++jQg}D8R=z-7NA&}u+=N+jBzrc(nt`7%jl%- zc3@j8CL1HpQM`v!Ce&NtC`9+R;$2O-X|*bB+L{jVw9901#%{Q=NrZq*rmu_f*m6yP zDqY0f8FS7q$_!Y;l4i#Wu~sZ7In)fe-c2S{X4=z3?Ty2jOUohoimE~uOXWDDDMcSs^`0STWRm8s?ULHFL zZDqdnRv|w5vb~`pZZ&jI9spyN4j68;;{n=a>FX0^_UFcH*jh(HplQl{N^HL%f)!na zId>uiXz#Jnrntn<{@S&9k)XPxqi#!cwNm))k3@aQimx_0w$m;nbtIW}qyOnXW?k)O zp;V<&O?SSn)66cEpyic|nhFa4mN4qENmMwYaI%T}94OQEcS^^Io5(!_&l>Lv3tygU zlD)lxZ{`iHUB5OYri29K9Hr!$TfxB8wgjy0r(49vZ?kRq?Yj8^J z;23i+t0*K_iEUl0U-(#G{%omac>*>??{`SNnJ^n`Da4qL0;v=sjQ&`x^?bQCzX~b*bvO|()`WyA9 zlP|4|EIaFl(|nUuD6~uA!IFj2g77#y_=o5_Srb)V5xuOFa&rPiZiBpcAkBf)l^Kw6 z-YOd}n%3^1AAw=J@HLs^bnT;B@OB z^mfz@#(O%b`7oZu%9apQ6X}p z>&}RsBYJ!#nnP2ok#UNKZFNxuQGwO|IVT@Y73otmobBH_*OT7MvEV1sEA>Xg8`^7F zUYOJ(Upxt#PxVYnaAx&TQrJh|%eRmY{v3?pIe$!}0`F+9+)=)5HDq*gNOQ*oV*CP0 zd{hYFG5Mn>u@SZv>1N0DV8*aKOwYE>To+?L#irBcZG?&UJT91lOPr5w=t$Xb#vyXe z0t$Es<(v*w5^T^8KqVS-ono7aw#J&LhL+PoOD!9C#a`16DcAS?Sv@!08Ou4Ztv(j_ z$K~;tATboL?x9^9-}ea4LB5x{;UsIkTFed-@oO?u*=h!3I9-H-+Y{Pr)Rq)--uu=( z$*ArIY<()v$9IyB^(ZdEqlAM*_dhz%+$@Sv2fKHAH}3<2DwK`x2$wcunlG69YW@;9 z*g;?LeQy|co|gNN)VRrFs61*l<>iRuYqZ=x4SD^dX_3wz>uQh8#3SD?n?CS%_10Wn zpE!{-4rw#L{5Sh9tm?^pkAyw(YYrR@C1>}5Mkv&B`Lfb0+3oxfn2ZN$NB@(i*qpl) z$tMilXyFbOkg5or{uEEgjOouN!r)klYOhdc{gZi{-%1@s>$WoXIwEy6?=X`og{}XF zFCH<0>QLGjOc(x8mY=*0#KqJfFUa4C>A7n$ zdCA!EQO46Q{EteA5K|`(kA(>hpFB?PxeEyk>@DoQZ&m67DX9-2tH+Gxk;TM+A2fa8 z(IVkP{jRDZ@$4psDp^#RT5w`hZlCJng@GcGe&qYvA-e?PsRc48Ei9iOf;qpF>C`@w zy?;s|0~H$@=4unQrYVT-14hme6nAAbEHH_rcpgq}}@ZMmYa^|dkfV%2{RZ6D!$X9p%s zn(8n8*Pf@b+8s5rC7mDynLyxbeYfPxLA(A}{z13~jMk`nPImJ9Shy1e+rgVC9IiWT z<~Afy+c1E5HPB5enhTZ1lFoiWN`D(cv7e*>-xB|zWcjjonkM*@Qlv|ismboSH3~?f zSYIA>Su_Z_Dj1Av5cM!U&>!U5)&zOGMJXd%R6tim89eut5S9kivk)Es=oO!pM8ay| z#}N01EHPDyPLAp`Zp@G0@l$fJGgbawx*xRLLjv(?eq45*_JlfaoTH~{py9;chy)w(Iv>WlrY4{Lw7$gvq2uy zwmmCGQhF~(h@6g{+x|h{O>D>QS{(S$Zb-%l8sywOh!fOEjwbqY*#ubvFOQWH!OZqM zOLr0TJsLKJi#MDjd4n70&ymX&3;9WudRN)sjW;)iT<`5dcjVa_mfl)(wbrC-ID0vY zFfaQZmU`BFZ^ZKDT^f~mE|0?gv*T349L&>p9o2e)A-vzawX0Xz?_R^4q2lHi6#h^e zx7PiZrc^lz^?imsGktA0f8Qr=Hp zdD=mP0*Z(I7C(hMVberx6(91{1$#z&^N%&v#|&iEYK)&w>DlfywTdSvq6E|2F`-Fh zq@)C}8i|4Xyg8{K>F-Sx#GeIIM*tg3fq-LPC&nWIP)OK>`YfGoJ6cdUtCG@m45|2H8GkN~Lb++qu2~QdgU~z6=QAza=mWrMim-63!$Kp(U3TS1? zKroN5G}EG7RW5i2TA^NzEdXAaz8tqEt~;|6-yjz2UppYaczp{BgXVWJoT8Ong)vk! z?;(TH7cxe8x6)L^KhbV!gkWfB{Og8FzbPX18!*Qvzj>KD1Ol1|67mN;*c#lz!o%fbU_q%gK-8WHVT<)D0 z8ayedLNQpJzAI2%{1>~joKw0sr5e*8bSn%SqV0VIHEf2cc^i@R_@_62Q#Ef`@TWPh z%2ZOhZ&TgPNO!cOU)K}4#LGu(Xz`(J#$!;h4nKBM1xejlI3mrr_@?7ot_qMja$s{;C?q=(C>vLzN38b~vQ6rQ_D zba{$kK9{0X2gyc#RCko|OAbPPi~;d(8W?eeIk_zUH1AvOLK-#(%o^-<+&t_^KKtqy zuTYkh>tVN!5dSV%LbT?WX1}z3$-cvKpSeOWZj^Z!5nK^0X-*RUg6+*|e)9x!n9H2q zpSqx_PwzWa1`lWbZqN177IWrBFdXq#R2kWL!g!XEtFT!Kjs4Jc6 zn0gQGg&?+j4IHJ%E}ay&~FG-6%*3q4KqJ3uLv5inV$rQ4)dBoxJ8pufoPc3PgNP49QPM33|S z(B|=OFj!EB)lpCCes#q*&Fpm+F=f3M(Rpz|6Tx``$fhQpq=|y%xZjgG?nS>DQGJ*j zfWB_$!sW&UKKHPZhGM|chpQq)`l>(P+?R$9j&PPXc1sMrl8Te4_CEpFP_!%{`f3H6 z=HLOE%-n;NCnXvwQDZT+MoB)i%W}M=ywFoHHPXqr9un|&#?tj1odwmIAzoAQbY6Ih zEACfBB>0(T!?iL(h;v;pklL+0ktn{4{qln}b*)Rc&9YHA^3^A-pG#gP_}I}ucj>t+ z!$Q-|d%5XejB_r!(xOKvCUsNneLaWb2>tncpl*&!F(J9=c+l{gnok(V=Sn-OV&3WA z@%)M?U=z`fWFxw|@*k9-?}re$YJb3PEqJ*K8uI#7^m)Mxh~{7Fy_uj;3B2l=lJu!> zyVoF#DhQa;q!bQO1vS*7sq>!_ZJYmizxic1D*-o;ncaYu-!sNgT9K+EYPs{g5wNjf;2?c->etoARgMEsh-^m9I6 z`Zhe~_9~X5>-2})lL%n*OBdZo)+teSIl9~G5Q{XMUp^-YUTZA(3X=>Mr>}2cUzCs! zMP4bzy)WK5v85L+|DxI`m=D}sR%iTL7c-4rJS#;0A$G61vR6K%j9=TC)=RW_L<;H# zj5fQ#I9NCpA@6--Xp$)D-&WeyaIa|#F%S4I#l;ON_7-1L)*D$+x#C?NLooe#KEK_O zd^k=c{?q&`+2D_s^lFgoKgpV|-#T&1;ms=gLljSYlkX!8N#CGS@I0x$Np#-~BzpY1 z=Xb4itLGyFd6$Mp3pkQEuN1H-Hm?Gn@|pu*U$V&+*MqB#v%OEG3D0=L*>LUlipA}! z#deHR@SDZF|E_+&DxArF^B3nPsO5HRy^{H7E>B2L0STVp;7{R=jwX^ZHZpJb=@ycl z&jmC(isQP~Ydp#!0qwvmb}px$DUx`|9OsvmCpOtXiOo2+< z+N^$q1VvS3O@}e+c`0heT|73&>n7;RUY=QV@VA8yPnTUy6A#_ZQHZ!ZYmjt_-ECWB6Kz z{J)6(daSK2VxN@-R<`&Xb_itPQ;Lp}nGR$|2FJ-chRbhL#CgEtc^A4DJ|wNwI=IXM#+>3lV_<6NSnd(8IrJEQ<~yb* z5&J^v+`FTQU<4m7-nsM0HS=bU$&5^83DG?p$4q&Ek&Y3Zb>0-o^EUMTkWwK$bRvgg zS>u<>I2G_ekFA8thX~7 zp^Z;r{~UFVzVYL_Rxr!V;xnhQV1{KAU$jMmp5QdmKgWAxzly?_z* z-vJZWuQWWE-C1MrG%eaG`rk`(BvKMseb~1s5nyu=j&PS0PJevc@*bO>+ z)-UJE0+cMWOocPy#rBD0ZxhpRq9=Q!$bn%IM-2{h6{Aa0Va&=I1DKYS!p&#|0f(up zAUp-PX2;W;dhp-3OIPQW(aAfGkm%_#*#FY{_pL=oOw_H$buKD}%ur zT@*bUljbxO_sp~s8;T;)A~!!ai_w;qUR^id?{L`En|D0;p}ddgw>jwb^)tnRJ-ho? zx9hK@ON*7zf57xzAs!zy1z*k-|Jc&M__%D_bwfKQ2ZG1%G{w0eG|?04Xv>QSGNNss zjAj)Dv3mdJC6d!w`FXN7DOVZ;-~VfEuh7M)rNk9zILyX zDyO-^$3h-BL>F0n_w4WCeUjt9&7X5?uv!Os@$;wjDl$l_t>XIXcbq*V#v3$Yf}4`n z$K!yrkiKm1{o@7jMTfZ!Wyeh%lh`DHhzX$KpQoQ+$moblPETh*hcRTTUQZ$yPt2a` zUeH0RF$3y)KVJHp!T=`TUnlLf$n)!KX#-;s4?;n5;N|QjnM|p;!9_*i;!DPqiQK8iot<&K4|^DxOpV>kDx?)L(2dD3t+IYOyw$=1x}mz&K|@~7Q~uh{I{D^V*CtR z+nRBflN$_tRxDNxjd4=Sm)i;JO~))xD3gjz%f)`Z^DWQaBdUW=}CMJBEpW= zJ`(PDjP%%F;BPY+C{wz|Js_T~ovX6wb8d*3{^9!iaF=I{^Xanp>L^w6>Qv)FD6ZpX zy`>IVUg6~Clm7E1nhP!3AJmgs^Qxa3gh70^0grP5k3KCY&n3M_S5*k%C#6h5-iVz& zrmdD(O&|N|xVz~iBO}Z1+_g6Z&(*E80p8yTsJJU{6sV((S%+TR#NddpaGPnMYhr+l zmP>curgU6LSCPg0RkI0te_WV;@%!HXYiF%SAu`|=Z(DitRQ_?8I~yA{c(%h5OMsqG z0_Q@=95RD8b!m_63{II%;fG;?_p2I6&g%z+&qAkg3>|~b(!1S3(}k&bB$natOwC>_ zvXdYfic_TpG`%4mubj)I!?MjgUpcr)@)E;K$(nl#C?1s&vhK9BkWaCfvd5;opqnVv zN36VgW&PvE>|rad&c`9XXi{twxFW`H!6YKM$sCxFKe*37%6_IvQRP52eN4 z)Z`HQi0W3(T)Op$qTb7F)sYkfxS-~9M4m~dBH8(l(TQM}oypmy+K(n&iGLcB^scSI zoEC;$g>2{LAg*`H_>etBz4}?lU^QVww()^{qv*!GM}YXKQ_^SsxI!Uoh=aiP?>KK~ z#f?wjP#Nz>jF#-0H#;`y;qU{=3R@#Ii-FtEB|6kuTgdbw!@4GelT6Yt12qO@IGE{D+&6ieAZvYxLDNO?;*oKP&XGf!^4^wI#isSmPkX?BKhazU(_f-^*hS z#QTk!N@ZdIp9U+3dX9Kq;|LX&ipud_Z4W!T%IeQhAQ~&{gmeDYR&DtRX{AxKG@~gz zOz4^j!EM2j5Y@G;!#&cC15Y*ZGzw4O0#*BH?)py8(m#)2irW3{hn9_Z0rJ5 zjobY=j4=$Tav+iSD4>t?kU_Fjjpunj7Yt5Bd)Gg_&A#u=TDbqPSItLPU7wu8^!JCk z(o|x9+p6k5%y+%}n1bm-a`h<8ZzE zbN>PseEw0myP-3G*J(ifwJv&G(-wwG$3Nw;4;U+UgY{5do<3Ch5$z7^Zs76NcmR)S zi6PO`>h<%gzgxu)7JO}gMpWM7Sm>T5_Tb_u>NkuyuqLM1Q?breX6{KSs5bg^IVA-A zt;RnEIa`rYv?%XWpg)~Dgf#c|F1eJC@&F0LM2b-hnu><58?QegT1gh(D6D$ZoumNJ z7NvYWWmtM@-Zlul*G{YZiu#jC{6ce&N6%RjLQpN6gA8@Tr=~4azSPRM{L%({6T7hL zOC5`?4nGJMw_r&SJI}%C7N(0Je=lYzDSr_<&^^7r`N}gPLz%|uvC8Xs@s6nDEV=5W zE-1c|)+I7>af#{fZ;g` z`F-rMPmxn1ER(KlD96S>>vthOAaa&!#nM z*={p~FSuEA9nJ66WsOBJt|G%eSePE77OT$0&*xHmuzu0LEb5}*CumbI zP^Gl@nNS>R@$&HoY(>QIJQ2~Mc3`oTFL7Rfez71*>{UGAK+Bp1xTzRLpl3n1uN~-Y z&xs_UyS~f6et!`k+zc9|1K);_w{K}sB+RI)X%!{CLL*}S58_D)W@I|QWJf>O9s{1!-YZ1$ z&~TAGC#W6H?yqP=k{Rjj!Z%DeHqJN+8h%zxxaSeS=0q+C76S(PDyK}8JwStuu>tXK zcKg{sGFO0#ZZGYhG3R7 z&4qy2R>7q~-ng`YamKb8`oXF1&hcEg3fJgsyfhBpTfA{(d9rmAnag*hnSTlya20k$&ikYF~4DGKvh}>bZ29iKii~rUgU|{jI@B z26DF09Po6f9Bb@DX_b3O^xR5#TfvW)b5T>afO>M!I!aJ@z(>8GQt35DoW=@tNTCbX zLzZppI#4tFyQ@@NjLU)mrw+@U?|z5$9|PXTxP4dg&ZAEy6%(hQoX_TsbrN4bC40D7 zQ1rVy%bnsWKyH%6&FwPUalrJ#kVM!gjQ$4vu^$GcXGXfi%yHF4ixY=$TzdFszm_@$ z5+g8gDyA^|kR2NWzqZF+6xA-puZa3pxwIi|07G^xzH*+)dV*!zdn3aK4|>}#=mvCo zS3ODecXG`Xl9bLH{BAW-o6Z=`OVuGg!y4hn&)ruc`6_$1xq(3HH-Z*#3 z)`ZB?;4#E`T}*2>aA!82kCa?&Ic2&yFO}{*zs=E-E6cH!6Q@V;uA{J;|KR9P*2wxh zSA$eCapqVYvJJRfl)_Qk&C@ugiQ(I5cJm_dbEu6SBX5+LARR+Zqg+)HFcwi3Fu~-5 z{rm!Lz(-B%Imd<)xR-aGzMl~6a6#TfI&RK}I+8{@Y0%8oMILd3uuy{DQ;DO*4$3i( zGj!&GPZj8W_cz3R!z4jFAqodL85@9JDrAD=Oe68qsPZ0K|Kxk2LM)LM2D4SKL~$2j z>D^|px->SsjjaXl!me8e4=mH1?F3!L^2NND|9ef+R=p%6HUWjG3UCX9>uD^ZmBS$k z)_mi5`Z7fX+vr(ILBi#U6gcb>)2hU)0R6X-<^DRrEv3SC@kw+)QSBlVgSB>H=W1f0^hWb4Mq#O z!E)PRJmN*&-rn%IL(SpiG_^5#xPahzceou#1Ikrv)Tw@^zA-tT2K6y4gGet4oax`X z$Kh1__6pn}p>6LW)3JVYuD zK;z9s=SKsUX4^V1bR_f@wp;3>RT1Md$EfY?JFYtPLma?^cC|1hooKk=U)Z@6f#YG~ zBnqv-2Oft92l2Q`(y+jz*(q}hPpU~gj|<2U2ULLdrYNvZfiXGr<5e|LCeMv1rS?S} ztL9<$X<;`2Vu2%ws>qBGpJvJx&h38-V@GtG%+4`M-ugA~#*nzWg+0yqsyh4mTJH$&SccnWL>wojrf_<&S>+;DL)5TJ<(f2mkwj}sCwNA-FW9-5ecenAR z8xsQfinnPmGl;w8r_a=F4UTJmXc5bSy||3{l&x?m(+0zj>19~5Zj6`J7f%DwrhZ{% z!nLHdRU`TcY=#1s`thp(Y`>_7G;qLE8$NaNCC?+P641WM|M917Lw+o!!6<=bnz6C) zc7Jvb_)zLV5VJ0yKA}AH(bzYOeK14tr!HYjOWY>Gi+w(oakX4#S~i?A_|gQfSrl-F3DbrDBnJEDBzG88~r#Os625HcVme=N~L^>OXWg$-W zM~sksN$K=U-PAT**4@RbJ05j;(H88(hZHc@Q5G@NeX&T6P=hk9pYpLM|IaIdlLH8jQD*Y5TMCYOwoiXs%0QvXd@+J!=(cUi^k8_! zfbo_-9#}c(=aeY^l8_axTfXOD@8HuP`N&6q@$TJQ|Na-g@P#)rYpe>fNTr3qTaJKk z0KDZ*AO~h35Kc*3i}yeNLQ$=4`I?~h#UL4yGkvO`={F9`=W$xIV_n=Y7(u~Jj2&3R{7v=xGJ z&@9G80iU%32`4$f0l>mDNG|R+k|8XQKY#9gDeH}EZ=?wy^|Ec$Jx;b&q#-2(Bv?)WJxW)D)pD`wgV~atlhZuVgPuV_2ggbZd zZ0+9~UgGT&;#6V3(ZX^z5)-7D1Wgy>by7L;NZIV!FbHo%CM5I>F(nP?a4lddS1R?NG4_?@NKQi2h0xrbSmk?mrc5AO3SPm3X7X3qC z^BX*d4f`)V8VF@o;dI`XXrYGqyKw-Y9(sV>J8Kfo*MzW7Ro)*3lb-C8R92_>~)@#*a4x zaDIsWuWGKB@*5H(C!Bap+UPl}033ttFF3~i0-2hju9*AqldACH_U&~3N}s#T?<6uk z95?MT08b$2H&C)|+`MSzSnOB~+hfNi&R3m2Xub#ci^=1*1HR!x z)%5e^fJ49_n+Z4YB^lpm?$@BP|mAgCS>-kYkVRr;uZMKp%3O2sUv_{)D1u+Kn#6 z^q@Kg7!L{=(f9;-OvLH5gSbsI?}Dv0GIPvqE{xKQGx_*YQ_i{uIWa5!%C%Ps5~eZ<$*N$QD)34>8r?6XVdYJ z4=oK?k;Zt_emuDL?7q>wap}RW;(X61x(5$4#QdXS=35!?No&*8y&vM_zg2#s{&W0{ zb)|fwHHMokWhdhy<3S%}zV)-dH-I`cNKN$4#= zAP$_1M;>|dU)nvrg*UIh7zb@kqIoykmkGv!nrVw6!#mKBT8uEoE6+D`3TgIKI)65`gp#D<&=(J_8b1gmwHi_ zEF@g(6F~QS$T(@)F@GV_Vc#QQ$ER&j;a&Bqdeih!41e&Hhk<pRyYkVec#NedOGPsZkvJo!4-jQrARXyQlNaf&9P(o!wi9f$cExC%z6IP-2mh@lqg-Os>XW{z53DcrBNn9B z(pO8tKa9D4q%`9Nj+XWaqV-rWPY(!EKOJ?=*lqHp)8HB#!D|P%qi&(u77-z9g z1T5Jekeib2D8@|g1ChjZY#Wx7cq|K*2p5&c#JK`5>T~RJ9iU?x9si>3DZe{?1?SWp z*2TV4))FIL2l$Tu#l98l1XtquFT2)@P-d(z(7=ti!49;~g+IO&=W=P-@>+S&Bkv$SL%OJ(Cy(09M<-DWq{vzO9hqKU}> z>XE!o?iKn(CD13|_0U5}-{fAi&*%9veZc4d6ql0G6aQs9#)8tNN{G+Yv@xN5H2wc5-wJamXCoS=J5&^z)xiajdpAPjU?{k|1XrLmfFT;%+ zH)bCX)WI=*I`0og+s37|lvyQ4`w0>5NV-GborEtdPDkM|USm!FG^O?jQ+rRHy5sGw8xa9%!s81XxUVH7eiLuoT z4A`ipq*PxdQ~HcnbxD37qNxF$s(XF04IyDtgns$PXUM*k3Rn7pRtSoU$Jx@t(V!v5E~|A zY5avbFlZh|BCQ>lnP&|7B>|%CA5>2lEDFFZ<-POleu ze9}vPE|O-Db^S=lGBF|>G1HMhl5R>JkxFEkjq)oLe4ptGmd#8j1H4L0~R67{Z2Yc336+UcHXr= zghSi+>RVGP`&cQIe*s^4$e7&5wwGMzwEVX2g`HCneC6Av(GFOK$v!qme_feaF0g#_ z%{PlF^i%o(pV*He5Yt6r%>>FhZ(p%s_j=Qo5z{{UKi}G&+Io0ZwId7p7#ZL6&(CKW|E$@j9zI8hzQ`WTIt-OHMI#Pq8!jJ*t+)k)N6L^L z_{&hoCQ_Ei=P#TueGy-fz^A}iUO80F%x&9Vz-ZL6<;fovg^@~YV zGGMVEB~tnY-FcQp8J)hEb&H!N5w`Cc4aX?41dh>nSIG^G`cV!toG%r#y@WY1>O)U> zlyLFl#lpipW2{_`VGu?iCFVtWNdh>WJ(s?sfNeL|->k=t_w!sC+#oR9KlA&)_s@Uq zcYf!0>Pgd@U`Y{T3xN|wKraD2QFvv11_B)NJ$~Zpr+?61*7LJAXopQ~uoA~gh;o)d zI?coqDHCZ-(?-`axiPL*n8JWwUkn#zk&Y*pbONN0S<vIu(|HM++G zsdhXC|5$g}fIfU<-{#FW+NDgjR8x^he=4`8Sohc#;7;kZvenw^DXV(9IQDvB9J7Cp zsNu>G+mN)OcojL6eVhPww#Ag4ogKVuAuX(f9Q*lkaAZGWJXAjF$FlH=P(Sq<4L+Bu zm&A~TE-jV?yR_YvO6^G=`&TOy^VNmmSta_-)hORh=d-~?L*V%0F$C!`pWrijz$&2B zEwW*;9pQ3y!|=h_Vr`E=8P9~edA|9H;{lO`X9L{hLeresC#EE0 zou=gayo4yQ+_m>3ARI2@%qnABfrrSSdiXtG_uQ2$U;jg2`N~VbWBFLqt|X)tgup#O zpx!pW2ZgB!A6O3Us%P~%8%c2yykRHD{ ziZ)%c`5~XKH|*-9GxAiR6|y|-WR^Ram!4+Uu&xXC^?N0BwW-w;8zZc&V1_We%YiAx zOp}M0^d7HAh+0U~F~lo|Wh#;1rjh4gDb@(q&!Wo^iFt^oJe6Zy3~e~UGB#}X9-G5N zZti669=o=Y;?OVJzS0`zQA|0HiAx#dx>}SaHRh3Kp6Q{Ox{RX^X^2~0_Wzb1^_h-# z?$0!j?R4>v0bsfBKw#alOn3OWj#$@{X^U~?%ug-T9pVqsYM*P%<|$)2uC3)$GdQ7kGk~D6{@60iXt)-H6qYP9OkJ47 zrRkl!>{OnJQB6DGRvO&R%ci40jcUhin~6&; zaif9DR-?UE2kpe}f>kP;46uFk=B;9q{(!JfS+Ez674~J=Lwlm0Q+0)0>|=A}X`AIv z*^j)Kh7BxJXWvFW@aF+Or_|&xw&KL&746FaTPd6B&#uN8awQEx^gWB|5BPrULde+h zc!f+nzB)Q#=`rYh%GcH%Z9|)--YC>U@hfFVf6tviS9sW_LVJk0y`?R;D6Ejcn8W4w z3G+IZ;t3x9A)rpTO5d(+0Up~T$<^iF==0br&=0NtVP~0i90&1vI{1ZfWbdU(;}i6W zB-1hO+Q*6~hKd>W!RgAwHZZPZCdTPxp^lH#iRD5Dg5)6|)AYKYaY=!sYZ{pJkui9i z2(ZU~)Y`^2Klb&u%_35Rehdi2Erb{cS?-{ICMGip&(hDc?RXqT-{CX;9<$B18{@~>yn)G4Zi zjRj?ez{wz>8vrMRtE`SA5Jz96ckJT-zq7=TVmT=Y7!5I~`#wA5abhnMY$G6WunpFr zbM|bVSl+#B6HC)gfw4`puzY`?`OF?N*gds{lToWn8!3+O6zFp1<#z2t9QDA79TNq7 zy2nGPEAqHt;yz+v7cVGqLgYjr7oxDysC$ho?a1YMBE?B>-%j-YvP3H@PEg#Jk)LWW zx1qHO0(^-Vc%Y?|J2ZeN;xrNUcK}!pA*R#b%{bv^J1jbr*>2~c46LOW&mlD~leo#B zs7rc|TNeA7-@IBlUffgd6Ew(UGUw`^u@vKMvx%^Wk{4lkZ^It1>>Ih5MLW%HtEe|8 zjlLN7WY5u=l7XK*R)9--$41h)X=Q=Wi6*E>r#_Qp@QbZ2xVZ%Q;oqFPLuQT~?6>8C zhJXtI9$z@_D2EsqbnMkhI{l`$Pie@ZO(}<%{6pO6vcKux5iGOs8Hs+x$#;dH?90+TY9nlaRm z{R=lt?sA+po*%QhzyPmNGH^&)vfGa;FROY4{NhFDS^H!m%RRbl=eAq`qIx4O+G3h< zv<@HQn3#?eNMy5JVb67M77qwCq*!iDS26ZoQffZP4;;Ug-$V?Y+rCP>egl9Ri>rTu zIFxij)S;`vcb7l~eV6^0Cc4t3B2FC4L2y%kPT(;b%NVymZ4v!$+a8p7fRQx}`3->S zVw^iR292v?D@1(=f%@VdBg+DgW6XmXUVZgdCP(7XCmn#8H%JU4-lRi+8XJ$`+rHgc z3PZwRdARsLYoA0$DL6M6e($zw{+;KZd+vw6_{A^&T_d9v*vY!i3|bUX2y6xcZ2{N} zjqdQIPUiE~lFZ8-nYATva)K)r6z&^`p<7{pUl|GN<#F**oUq1yI{*fE?#?p|b z-jvLcP14et8g*fP7(>G54LSHa7G|0lWm7Q6#IuonM`sNapUh`$c)>(W;zYl~7u%JR z_dbrTi!}dQPoiAQL!SI0-VhC#04MxKe1>F4of;H=aEYs8I9$`AabsBWc+^ez-%Z;~ zwPSyd@npRR8{uSp8}fsG4)I25Yba4=u)3untM{}(ksM2O)H|6mjpNf<3}U8xR!12< z$qB;h{bJImiINg)wemXB$aDx}h1t0*vr_E&i(8AGU;g$_eBx7?Mf4bt853OyYzzUd z0N5CmGPDE%4s0Gj`skw=0DsX2#P6nF93&UK05B#dv*6JHUa>lTVbIia8;NjMt+rJ1EDC98TaF2eFdGQv|QCxyfauWa935u{TjB z3VvDD(yb>Kogz)WKw@uoy5kNaE8JFo7A}fM0D@ z$b&5TNFz{|aUmkXN>Yi2*bdIGn&qX3#-}LE#3w#hd5}Cn9k`If;=lw(q`PqQLzfzI zsoyGmFBg5l!Y10LQ(v>0EW7!heeV~*#D%)c-;>7hFKxztg}w8kGf0cB^Kc-=0u5I>R#w;H%tFy|azTNst;TU_N_S4dkFK8n}0nnX^L+qCeOZua-u!bSFr~h)WiHC2KHCb zAcwycQoUJ6^fC6cB4f;w&RSCu^&!*hG0%9)4)xg{^mv)h6d%=Q*^IrMDWwS>Y!uAx zyb;1_TQK-@pZLTlANmLX;IluS=?UzV*+g|haY_n-E&@EJyHw?bz-AF(Kl9iw{@-n{ zqy1$&ZGP)eF%XKQV3g_5bLM!XBumSsV8SyaP^#n4fZnA`7j5Z#+Q*Ki!Z+-5Uqb;) z-EkoZyYVgHICN7aDm&W_Iyzi?}Da<1%J8O<2}{8}Uwd2Yog>>t&k#ehD2F&omIMhd&Co z^02>~eGJRy*~g~pg{>%ctIJ{8hS2wP9tE~&59M=P!#wTHNf8HhFdip79@}GcPN{5L z$PRQJH-Y}^`=Px#?6ZCL+0$;A(<7jfdIgpM^X>HTJ$};bu5C#|JrJ;mo$uIiLij=Z zvffyR%*l5US(5Db(70qD<;LawV}{^2_lZLXpSV1!5Vuo!8M)!Gg)43g125JL>1E-< z#S7(4=XY#N9p9Z%^w_*}jJZXjQ*XBQoGMw64q(v6T& z-2ukCAAYz{uU&g1Z=abgWOO{l{NSO79t!rZg{^OeJdtS1ZL(J`SIlqSYB8_E=x-cr zaQ@_&i$Q|NC!9!UgQSOJkUT$s*)ht1<6}6OZY_3L&|k6^AeU17{PybxT;113)a);)EA| zQ;Mu5RsuWeIbn6>;z}^MRuV z?;lcZf#emA!+0!a>SkTu2!`~1v#zaY?SArQ^&0TlYBkwRWBgWrR(@q;z?PoG5{UT_ zFUobtj^)x_-O=2VdMsfNn_bblwaOSSd78Sxf@o^Is%+P3mqxkAR3Ejw0;9NqM9HjR zAS#X6F#P_)bI*O~FPT+blSMdVdX+*YD+HDzpc?>7A(Fr`5a8hDv3>sJ@iS-6{vS5J zeSnOPZ4Ry@gCu05NX&QakU?SQdqORN1zSvAk6Uk3K1p{jCt8C!4@v0IXkUyLr=|LO z9ZE9NA+3{1_3CSmdv7(%7aDW{7ikF0_pPl)!S&oawy48+wNg(=8bFRXgx*9f*DXDo zW}5N3Hiz}PJpyIKa@zc`jn$~crhie3vSaC~PvV#xl3lITA?a@2i$vFjLY|ixu{lpU zOTosl9b%URquj9kbg|T7OzBP8rM*+WG{`$PPd3YaS#3YuSjP+Fs$vGHiRsW_G_mp%!J(o;)f z)l!n8|1Pt!*#3{7{m_Sg`st^i#@1{$4+>nP0ao=>^6x|h)Un=)t$uIn%)aIE6Hk2b zh22xzf6JC+e%;`kU6%}wGzKwz8jcs3d-nF`vIK3aUN8XVY{>Oh}Mky?jqnP_P_%sr40k2En&W5*&W7%qa)Aq_2Ff2Sf6@A0fy~~#_`_gT=b?y4K zQeVgsS3tvZqdCxb?X9*uyE|U>Bio<37v5&P8QJ&?fluDv~+flIKFN1AeYbADWs&83Tw>$Wo}!@-xC=Y7B^cdEb9*S5Yr z%NbP4lTSU+nFbF_ty!n`mJ^rXL$0%ZBP^ zKt8^eIewmU2tQoxw1-^x0w`~pNM2-FR3Q141`05lN17D`jk1*nADv^Q+HT}SztwB= zkck0&1lox5iHl#gX5#j6ipEj4)a`kQ&_ zG32b?xjms)_vMci2n}nDxNIF?mWcfzwrMK|1qyCpERSPQEDQEnN7&<=)7jdTWu7tUNF!Xjd?_XD_m*uJtK$ExxMbN0|Qy3c&aGp*5DxD8vf18VUEzQ2{VfOen9<*iLBEo)% za${SgBHWe(UfcqJwuzr9#@v8m-?r>=97g?EF8XKO(!u@(y}>&1V5Z?E@ztwWiv;Es z?6cS&7wcPu1`_Z)6}#BFa^-T7xNc)#J-!NgB$3*X7(fVhJ$v?Skw@DS_uKYKLEvdx zf8!7S#qa%Fue|aK_kJcr^@uI{N(h1DAW)Ci<5-cd5a=SnbB@Q4e&7So?JO35#|Gl7 z9E@F@!&+B&Fc(FAuY5i!q$qL+)$7n{-&#g+z3e3hWuAl>S1XO}ChNBB1c}L2wp<2u z=1m+RV=E=yAMd%nJMz2HdJmf=kmZVeM-nK;GzN3 zk65<$Xd5y(=@Dzjral;2JqTff6pM~FV4FA}SULu_MWiiD!!|E+7Hx%>DUb0psH|HN z%at0U(+A8DB0s(Wa*_Kt>_tq9{bM9xkCPRci9XCWYxOIB`F@_|nudPSiETc|2lQ1; z!0Lp~wcPhgqt)Cf#ice6xt5mgS$`bjSZ%WoxnprDG$$xWI`fRvC;Uo3_lpb2g3mIk ziNSPD&%dJ|mU(KIvSAEtYQjU|3{={P- z(qSLG(zih{AS;3(^h0Yewo*((lX=Fp)+^I)BzQ2v^2s+NwEh-@G(=}Z0nq8`W%)Q3 z(8Q`Si$b5mCp{TRXMXLNhS+%r3{hl&wLU2uW&d4PM#0hqG2XG~bo&P`kok(yWfp6H ziIs@<+H+x>ryklS_A#Vmp8W%U$AEE!0q?2VvCp+4;3gq;kdO9|jy&+oz~h^zJiCEz<=+THuf>ZdXv_V8)J7^{`PPG zHl7l>J0Wf?Dk}umL!geh>*-5N2($?BEP#0NzW2TFQF|8hw=G?7%VK=&jggVabQ_~6 zqk)XTIOS&kCY$MZ$iz&e!2nJ&taP$S=1i9$7SEO|s7;EFR>c^{E|10;_dOvdeiF{* z@dO&R{(tt~Huke5yYK3MKh9%!&#WD!^?E(CGxmCS9mfWVSjaX?BqBvZI1&MoS9uW$ zMKKTw5fT!Ff<&B{7eV4h5JDjeLJ1P)DImyOz`QUBh`jbXUT+l33YLt_JZ$&$O!rJr zb3W(Px9U{gy7zZ~|9|()bkDE4|G%m_58rd@RNcByb?=qj5}{G{$jo}PAzZ9<;cy*e zp5W|j@DjD?Db7oQPnUvw`a6#&F>dncnWJ#vHYW9zt>I z9obTzam3HONsvs1*yR|3A}!^Ol5L#Iha#Y{ul$hwLH?`@&kKHDy@O6R0hv(VILne` zI32!l0&GaaAIKN0CQub8rsp;e3a$%eTPiP%tgA0XKU;*y(Emp^Xkih;f;fWqLd>D# zJUVLh^>Wz9CyCaL#V7^Va|BUt5C46>?TDcryL`Ct)c_}p_h0?d9t_~5?9uVIRTZoZP<`D1O+2_~8>^;aTx;?a&7HnJ>z zLk>RJYj+=zRXgIV0#QTvioG#hi8q8R5li(U*!gh^$sCh+ZQ|}l>Vq{L*U{2w6WV&j zp~FyPhb|GG?y}^#=GYMz#h=58J-&AKH!9$287E@vaW%Wi|5(Brz&ysGzCph&1-0$V z?hvW@Y}ZQI_Ul@~4=^mkmOYxaP6lDetVX^i|ljH*ZHf zAy&oc3vK`4j@$pLVq)ZF+$@ikfAU$P`Oy!SscZasjC99Y!aMT17Cq3~rC#lw9qgPRyAUKq*qWnQI*&f1p)ZrEIW^=iBk zow(k=Z>xUu(8hs&825Z*U$;l%lo(^`8)%v@C^H{iB(dFUi8ok^37DhZY)4Ii77@n6&l1~`dv$X7 z9iDNFRksh5&m$4LNI_#xlUz5asl$r1922Qe@gdI(dCtHG^)F>TVpCuu%gz{Os9t(L zE!l@BSLhEVjzR0=1%;m267xLoLuqTYgGVXt(NFr<6ru5`tBuaT_J91+Fa2w#D7Z22 zuuqj!1dfTo=@EcqqIHtpia^ZtG5w#}+y1{`C;$JG%~r3)vu~Qgn7=gLZ^5nec(&}q zDb3#gOmT7Pxf{=QpKq!c08IXj$+{Rm^Bs#$QbaYTHP3alm*^Zd=3)tX==#L~JNRvv z7@DG={1m)Lwgiv}l2}s5PJ+bFO$V*ilSfo*VrR&6qK6VYAnk~e%?G-%W1k~z@v?>v z7_}3(y)=8hEV39S{!W2%t%2wAIe1oTCz2L_i{s-=d)t(4vRq#Wb;Nx41pqpiFz~02 zwxWNtISqT_rUUx!Am421fXCDxV+!25J(z=9(m5KTE5nCqKz{*%13cZ--~yD!qJ7fO zwlAf)P#D`6c=>{WWY+T(xEe%gZG7n>1Q_yHW1fR#q&Zl892Wp&%g4cE9Gh%IYX|ML zYfs|&`#p(gQcO&saxp+LVuvcIwYh&{7BrtEs=t82>Ha6XT)`q$aa#q6`=BbzXQ5+(odBAF#_UoR^BfV zDEQFFx}j_PgyJ_1%Dj5x`i;P_KoCbhJ*g=-$A>Z~fvg{x5&AxF_$G&rFT-{gt-zWA&%&J(_f}j-qPE{AMsi}wR2`1$P6!6Q{B=#Wpzu zvx@=$_K*G8kNvAglzU^|7BjLyFJ#V_7CyK?mx?11<8 zOKLnp5uIFjU3@~_I7l8R)v=y2Rpu%xf26&4skr;3tMTZtELmwhh!=M3kR~2gk`;;{ zamVQxpVG;LVFYCPwRq|7Mfk=^wYo!IpDzcmwYN6pAXtuhjSpIe=Y3G(5PQC$S5TAb zbSh&~FqfKt6aEeocj-M|7BZH~p9gPkl>}p6W9~e--qCdB7<%gJVq#DP9`Qmw`ycUF z;}IrFA!^&dkL=Eh?!@=~K|9nYFxJ{N(jV3rSK0Rd(8jRgkTiC^Vn3o&_r7@)+aC1B zQ=3AIyFhDWX2cJT_-%O9TRS6u7B3s8W7KrVDdVUtAHdBq5}&-c8GR-G$P@jO%)Pz* zc!=$%1^|L8);{{r=Eg{jm(owDR(f!yRfIhgO*waAtD0demh^$u8YE?vSm*Vp}OR-Qk3_NXU3xEl(KO(l}MV(0u}g zyxnY#S($d1eQeW+7~`oKJ_gipv}x~nmJW8Z4l4{UUn(nrYT$}DX4q|K2MTK~%h+e- z<)aK^(wYy>vmeB!{g1$bl=lxUE*p*;ixra(&tjDcIvdjK)>oC7OJPRi!QV;&)6Z#& zrK4Cs>F)cu?Qh|(+4a#?I!1On9C&Q2mW!{vbd;LrYV@u8H|unmRID-du}4J%qX>2oZP{{cXF|x?toT>I@uNTb_OJf(FaPIWb&qE)-m-h*og&aj zAVzObJJn7R*c$)r%hJ%0^Hh$i^b*r-O+_|%BjJ1Pr4y@XNOaIO(C0_hh?2kb;!E|yEpdIo=T>Du+s!oj>6Yy$@owL`?RF8l zckjKr)R+1E=FM9r5|=K$_4ZrQhw?{QPdFb>Q1*-?#-)~-6Ql7_kktHL-rpRzn;nmq z@XS9kBr$el+fWBJvs^US_GLrDV!VCQ+B6TB#_dqsd-K9dt#TM}kodwEzECOO{qA>d z$@k>(k%XU$Rj)7f5-l!e0K=Y+zh1A$T-JK&R7=IB){JtT0&^mtfFv=O=6RoS9j#25 z6xH4m0#`0Sj#nPB;TS~oZ&2Rn>MiXbullZB4iokXR+U?D4xfAykfiVJ_g~|Qz z%{PDOuUWA>`5%=Ycb(bqiOhf*_-LoKS6cThAEYUON1I6sV9b;G;CF||295R5er&#| z>2s$xslKBxd%j#=RXV3F9Zsy`J~74+`}NN2JwR^OR_T}A(d~y2>HtD{e>5vF0Tgp0 zQ_bbVepWKZmWR~xygh!}?Ne`zdG<@ZOjB7ra{Y-n=8~+`m(l-URU2M zH=^Q{x1FFNR0K0DOaPOQ}kXtJsK8*lvJ&)MwxCq|9cscaOS)DP*(cveN{kY1KQ zs=OOG3sNqgwzII#EC8zFb zPs~4nx9n{3o~Tn#FE}B)XxM~t5geB1JI(}zRVNejxq|LCW^ERuqCPH*0qF4dQ|E+g zXHQ}9C08AIi6L^1T~=Fa3={`AN@D;xUlogaZq6v-%^EjLUZku^aAIdcS`Y@qJzx9d zL8w%l?26p;m#n2nzDI@m?efvckbS4&q%fT&DmQgcU80>(I9QIVzS*u~59o;a@hXSfA=#hE4f zy1g-1%ZTykop;}Hp<{7`%p(+h8qf8utfkBF`J7aJM)rV#IUA}1pCumIa=GjE&DAWm z{=i>d9bOTyDs2VTSlXK(r^w5sLv#2oy2J@gPBy?&j&$u`bF=#tmMstVx8}?J0oolj zUVR6E@%3by}Z*!deNBvl*MB(FT_Ll31$gZ>zR4_&jc)gsoU&Y-7#(MrHWOMvE?8 zdcoqN<)5?g9KFWb6nUGrJ`u`Wu@5dMl_NOT%u$Qv z&ZuH}uBWozxQ!(JtL4=!iS@avU~8^IjxiN)Qm58G#^Li$8Pj9T_l`eMXNgbHDQ5U^ z?Rr}(oF6Ze3>f2BYduKVW`SE;_I|g%C!sHV^fNx=ZI+O~RzCFy-{*-UxOaP);f^#s zWLdq&IeA@TZ+&w2ZagN19NUn_uQjOZuoOSXGPiHEm+P_#Ew-DD*p~4}NNX%zoFAA^ zHqmbR%XnYwHw#%!jJFqGe4*6O4nAHa#r$OxmbS|-{mz_0A_lk9GIREg^X)%A4JT|q zeLMEgEP5Pw$gage7O|{Q3<4fMh^MwunVotGhFMWB1?Nxp-ZfyVc|K7j< z@jw0TZ-4VQ3bHKZoboB1BCr~P(@OwXLvvzGBA^*o`Q_JNfAf#n1k2t3A|5OqTm)6( zOT=-qtuQ7kt-#>7r!3Iqf-!i{jbj_l5Bj9KP}(Le{FAtB9f=z|B!NWEfn+`~?YSE@ z1JlkDH}pxXXmOk@yLJxU23*fl1EKZ+*!1sQ!)bfeP`7&3w1R5rXa|NM^@VOA)L-dJ zPA}<_fYXaH_el(LIYD4l&U~laGx+AOxw1&2dGk!)>k~PD?%L?cEo8F10CC4;<&xq+ zP8xG3mLHdJTL5J6GJXIxa^ujpR&$u@Xhsg5q8<(=cE-qPYC{tBBJ!=T+Zct3e%4c6 zxblHI7r1gAeUeU;UDw*9gEC=+REq=j@F^s`JrcIOZ8zqdod7u=ar0Q2vgPP-vLN44 z7X*CtoYygmCq}EX6bgusU+Q+=n7e* zlc4(x?yRis&l&#H`c(Q69~lZsXc`BSYZ%9uIzP-;VRv+c9qS8AibL=2LQDkkgh@=) z7HArKlAYxit*Qh6tlM^H8t0>VbF6Lh&tIBYE_&gsnhknxlV3=dobjYDIr2kkPaLWP zIBL$gzZ!6E01pfEkinh&86q^bkr(2rZ-La9qMYf-x0j>BohPt!%5e#dxCoi)$m@8C zvV5c5B?Heq+44A=pz56tbn{($%14<+y#u>QpB(aTVY!R@lDkEbEo7n`!%O~Wev?7g zIU~E6^3PZkkAY7t>v%Ci>+_b#prebfj>pAf=)Jbv30v%o9P9U7yMFoa zeCbQy_=$h?kA9Vx-3;Ch0H^9H0+R@w9s!sH_0VW(I$$_ zOFQPKDi#H%Wc_RURcK;oASIKI!=6BY?oY-_!WCvltIXEm5oTMh0eC46$|872DGoTL zT7B7vh24#g8*Ef?r}uini_^71?Y!0ATy>O#X(jqqsZouhigJWX*>}uM&3C54Sj{ue zC?wpGzEO`Izq#bPadO$lIP^FuU79t=Z4AeOhLsU;A%197@EcZ-P(>vdM@I@x%}3Ys z$wKCVr$6YuHA&)EpQIysrT`Y z-{2pTk?c{qfD^dSe^YympCovcp~Lw(W{)hCVb@%G}`Hy=QFhiF`7CbdeT}m zltexY#pB#upPSY>)=Dvzj}3=D@VaW@ceD;8ajckXom5ncKoPR-=m&4#zWO(f$9Y^H z2FHbFV(CQsd_>^10^sw}&lhc_CX||g=}TX-*Pmbh1p{tt{mZVFgF)VwtjR3ibdC$6 zP!<&AOow+k#YqsV>$*2=iB=@8fK##``_E@`MXfy?4)jk$5Ohga_SRl{7xElrYyN$I{QBpZQX>k4#QU@~SIm z*>7Glo@4vL2lq`Zy#DB;wp5tM3>sc52~#$F&3f^rL&ex7Ji0W$je?~xE)*4PhyDrN`a`^FLa@)jNBt% zhV5d;kEQL%k%|G&3Ju^wjz&qgXc+IRy~SV2R*Z&O5egPP*xG4C!vzWU1HPV7mW=X= z%$i^(a}H%cS`q*TNJW1xYT{@)@y#*35S;p)``vf5= z-+jX$UzYD2L$L9@noJ#BE<({hjbAO-g-&7_wo;9Q5%QNW(9PrAF-m`C|Acdqob6iM z2kp)H#6>Ch4R^hKc`8iP>E{LeOxZ_eELfAzKIVc|^t~|1S@Qppy-}N}0G2>$zhM)a zVkVo%EaR;*K*4LN7GbJkEv2XAZUP4r0EXmn-KGs9%mbUL5FbwRyRJ*F8#A#|B)a4^WXZFU;azrm4ukY zMLXrE2rMFSx&W{U$%!yT!1q6iM*WH%ymIINtBG#KnCh6kIAJU`qmK}188-x|9F_78 zZJa1G5YMV(8tYkyiS_{(0I+XN4tU^`IQdWKD^Al-KG=I?LZKmU$qQy}TN2{J!+pRD zO)T0F=0y=^2AlAVjV@B8d{FVi_2=t>9Op~A05GD2X8f|F(@6=q8IO}B?VOAh zS+SF+OI)O)74YW#!8vbpw2dXBj&1Gu$WIRDvTnqG_>@pPA!~F>ie;fLoH2d}&zQRG z%T`m98)VVvpfl7&IP;U7pvOs^Kf9gnQI;)olF@|4HuT{GJ3-8HA=QGI^g66!=R@Kk znS!r+J;5_sUy{vOGMWB@3F-y_hB3Q?CgG7=s zeLWMOpuEUj!gK$M(f#}1jT1}rfpPgb6aH`!fW&tD&`$1^-hcm|ie1i$uAg8ODxc;f zJS0B9m*<9{E`Q@-xt&}gLE99orN%ZT$AvMVaR8pjvZ&{Sbmn${uGV<&a|&&KM*$dl zevc6qfcGp$vQ4fsI{9Qd$K(nzw?8-MI>k8)VwNk^nkmL2jCIl1a<+jppKBu4?ZRWP zk+%ohk$u8<*;R~t?NURC1{Pt1StRBl?Nea-MWsjSf}S=2rxdxs-d$uDfVGcz)+7yaJhw-9{+%EGkstX#|NgK1%HK^4#G`SEcq*PE zkRxz<1RzJ_RNjgJlZpS?8~^{T6@O++I3_DiS>!60Vyccf8>~_EKs}Jw@MuhS%d2a` zQ%eZvzao?|#LoIgbK8a1_Tv)H00@P&2fyNEgi2f~*g;`MSrOSFho$DzKCC9+f{dox z7^n-gMNKCsZXpNkb+ z<>xfhsR?UoyAxZjbb7g>GI=Sp?@HAPW|~KeP6*{nE>9p5Nw)TDB;{1}eeMpB$7L^> zo)%@NV^-qes8u!Rbn?emz%d>D9jpH7%HwqzTNiRkuJqM9oZP$+SEZyZFO>lL*%BjVpkk!|0qFe7k+k_o=*l274 zn_woc&+%N1mJhKC!S1m8Q3{SoS~;}sl&^@_WoFwdes?0@@u9uAj#bj68No=u;d5y9 z@e)c1v(#VhBlkO3*f7=XLxT3Z7}5bdw=wIu-Lm6sPKWn?CnVgv^w92dY6r_nZ(xU? z;d|)Bul$c%_G?zgyk91enhSN6nOaB7<1#bA?(dMb`#RWcsl}-8izY$IHIkYzhzEL# zd9SssNpHzHo|4<1@4Adgxm_ObuKlC*f%?9#br6jgwOMhBzG-*Z5tfg{QL%(Jj#bDUy*GG3Y>lRCFDPGI+}NaW9fRy zQRZjiC-NpdkG_#YUfSI8Z*Sv=-yXfA)~8KdFo8`%Y@c~7W*%+Twx_WX))IgbcsZiR z0XiO|A`&se3Z`3|0%!c~amAfdr^ME{-rpEA;N|`kzH)pu_dU(!8oqSMqivaTk@DOI zh~7`7-8@b)_p!IlYD$*id$X)@TAkQSZP)TR-?HhC@09p~xY^>(hrnq`O_BZC-grhi4))qoKU?~4OVCp! ztIj7`;1ff7^E6{b?I1>Z{N~T2!SfTh$LJ__HO}P2V}zeVJ#U}pQ-9Fc%P}!k`^1QB z8;S)dIE-J#N%`oEr%f-t^ipjCbJ*JUk5HLf7}!3(4#i~HtgD@|aNQpL z(lhs4bI`3@x9ZYpe9iA`$e6TAU!=HiOSKt~Pa-ki*-t6$`Q)Z#FO(~=wL_L@J4BXB z3yM%+ocU+<)L#1O?YGbQS=@==#Pa6LZLY6l%Ve25TVFTjRtm@2m+`m&iE*5t-42Pp zzj2}9p!DcKr?6zwM#d!@5;q<4ww5m>wF`a;&~&cYjgMQmZdc;F_E}=>hbyrbc-$#% zvxTn1iz;@oux!WeS8mt&@V&e5?T+*DwwKN=Fa`qS=H{(7ju<<!?EG~{&5MV+yzIjGHP@xG3mH|=#?CB5_ZJAeC|-~4-j+^Spv5d4@+ z{FHyrBXD{I;B!6zR+_I_y5@Gm_JW;^{FgQ%9z6N)FqwL6GTWQ>OYTKHUJiHtOHJ9+G{n(FQtIi!S@`?ZgRx>=1Mkc1T`)Klwk1FX_)xKEz6=3Pz?m(WFr(`Cnq2 zC{|pPhi-oc+OT`+EzRD>><{r1)<|^R(g{gC;z?R2Iht{8!Q2iW>gRUydR{S z^O-4Ojd|8<5?(*GspFf0-{ zp8u>0S%0-rYPWKeK~GgTrb^+FksxQ56Ya-!qB3Jo#6)_`xg6Z`VlbI}A2^S#%86_K zdu+hZ5D@XnC;v1}7f?i34)0mK7%&b+>iVP2Bfm~?X)=|}sF&$rbxF)Z9I8vZopP&4 zPkG~fhD$iBKir&69coaLQ&FtP+rk;@lD4E2FJP=P&*y1Ca&kTZXIlg7kfp5Jcfz3A z3l7Y&y59-GE;KSj1JXqmHyybA{3K zoA%|iNB{m;zxs`zG#Atj>SX2ByHnxw9f8w;_ENl1rCHQPwBWyzF$BuutywZ?4wK z21^53TCT*BHpRj}k3=6>^0$DO&#g&zPyX`DFV|(Yyp1<6n+(qar7lr4VvHXm*Ox=G z{W3my>pO|JV}x$J6jQdS(vc7t1FEb4k312|ve9l{!(S^VfS%U;!f-XN{# z7a8xoiCKxIgX%YU>L>WQEcrt@c7?|hSe6pb(hD!VSht%sIdJ?Dkpe)BH@SHr{&c zt=fUi?abqJ26OM4-S9G_&dYqlkN35Z7^8VUpKFZNxh1HyRqZ8~H|*;#BzXDLi%vYp zkM_x^@?`@h`XhhJ(diR6iKV_SLA&q1cUKUdN^S@J5&b>^oR9>V#}RXGJND_7a_yxw zry(yL@_X;SM}TAFOiJ-(nCMT&xyBKGykqlpevClAMczPO@JG`EW6vWonjeJ2cH+zG zVHm6}Td&&+UnS7Km+k%Dz4v=JhFXsuOy)ZhJTSB!(q6v&;XLc7jQ#uz_Q;v>Ntz>t z&9+RUE-WXASSwMGUT7qyF&O9cn|OEb+^zlQw5D9=)r3$CgpfqF4Z-yHXc)u(|IYjO zKKlLt@E`u#{{uPwxfnmEbc#Tm6;H7^MIiRQ#DUq~|6Y6TwSUTXq(ARdNPVE3iN*xl zKB1MFYFw2(CkZ2^fUT*abn!ucRaCzUQ|JlZ>J^Gq&uTD%r_MiAJM@JTV3xVK4_(zfcC)qSOc z_**%$bR$u zwUl!`G>-eteKYn^Mv9A3EoS1OBOg3B5see_){pJ8Q_@|_H5P@N8DH z84M@!fwQcF!L}1=OA{WBljd@g5Vyv)Fo>m@du%E98GTUWAvf1ubsyMLq*L>WcM+rJ zcp0WmvauR%CTzsg@?hwhydQZL=t(+-KFFG-xKU zMbkKinPpo0f?s(5*mKWc{Vy)P_Szq~^qarQm43+Hq?8xjQ}%g{fM)j3Yx_^_ZV}Kt z?enIQ&u-to{nFJd*Z#Uq8+J*RgCP4YvrAL+xiYGirvmx-hp6~tnfjQr?8rM@zmWEY zlw-D4P&-c-xDfPLBo{Ir!mdi&F&q|eeilC-gMAJz9QQF`9d5yvgd9*VS*web26l}_ zEo}6qpe~SOCYibb(&JHWp&&TTvqR&l=PR&}$ET+arA2Mxc?g31cOI&HZZh);3TQ`gtoGAryt$cwTt(%8FCX+Z`Ae}Z zuQ-YZ7J!Q%fx4f)Wn9!6Z%u}{ z`7~b~((-O%=vTx`&eiq&CVZA%v&0P<{&e*ny<_fNDGt}Kr?;cq^1D44^ploDBfqvB z8sBvAf4qML-D2Ut@^-9Iw`7^(Ky^b+W}Wf7OBZa>s$bOg}!}KcX*O z%YHN{UCyOLT>O$_OL1XakiX;hgWZ|j0Ow1vXL9sWUaIQLi_3s?o7PTzCU#f-E%||* zmknp1Lar@B%gb`}BL$;>=c(s6!xJNI9~mo;2K7NiJvOae&Uz&TnN-UjNA;t{Cu8XG z9fWKAsF=L$Q@v;+&irB9w;OA}lHS5j{R5UBC!S=+oMpONQqA)H_D1zla=l(Zn`$Dw zAshcl^i7`6Jzk6}OBy@m#8tU7(+@n2U&gHV1H^cbulVv@dNk#I`$B+hCo^tC+c`Ota8HQuq3Q23mu5{T}RS3H&!~kLZXCE8CFI}^Jt@_&S%J|SG z#*t!+eAs0h>GyN-`QotguzcAhYJF6R7jeT|-iR;k-n;eMnLWmr$ApKkh=ZkOP@c`x z8F?0N#RNIp>T`h!5>x(>u+gv|`5YlV?5a&jtBfA~hQH9+U*&!hHgd&`7xfdC?fCVE z9T$|!0~<$qzo&hI`>H@7EafS5VfJFh?4$c1KDhrAzwsNt@vBDAmjQ&PTrQsS&shZ8 z(bwm!;iu-VM!@%G_Q}7WwteQGl9Xl&<|IwnUN!;WJwUgzC;+QlyQ z8bv!GD0~k+A22qlJi%vbwjXpR{%Di!)C{lgf#haZe}d)S96Ohw=$r33_G@nV&d!+Z zKq#x8<+Y=OCY#!kt;I6yk7Z#1ws!J9>f|~*={N{G^PTbfM)xX1?^S;Cfi4GA(xIFWxSzM@jn#t> zZ99})1-d|i1FhU9l}E^tp6j`s_15UfcpFlGNRPNavT>0U+p6Rh_u2NFb@r5rb3Ta8 zI{w@>{+VsIyFDj{*X+WU4&dqK^PH?@J6CNx(MkG!8#j4OWM92ZuC8U~@9N z)-L#(_=uT?DJL%SZ|XxamTfYA6@O6iPj=~>8#iK(WIWutdk2worEIecKtYqu5gh+C z_Q24m*N~%c)xXSPtsVUZfO_JmjdnCL)Um@w71_nNHZB3_S~F-j_S$}ui!}M!{ybJ_ zZ#-(o34DJMhrZB$gqtOLEOB&T&F(0-Rv*XC&6_XR;}o9De;oL-&2feHKlJ5;`{O4( zqtn#S(j!ib?lb~7spva+JihnHZWxu>NU8_(`}cM6$aGOJ$@|9U021+kSg<9NM2m(? zM!APd{7W~kUcK?xzxc&3{?u>(_HWCu6d7o9*o<&0KeG`yJp%B|cJ1zlX>UW$-{miV z`O7caJ){4m)E6@Z$BX=t^6WubV9xA*M#)@ng6sNC=Hex6=RYA1J`Gh#?a3ccJHkf} z5Qw+!lRQ`KXMTiiIIc4JR$|9oLV}H<&$KTXKV(ebf7biyiUCGU#B#$G@Mo%CB-4vA2Kt# z_{uYug<8!Vu@+vD| z!x;IJ`xJAzr2OKpu;Ug3N6YjeLh{7GEtNz&XHPg?)Ss#cJVPNR4(3B>_>a)q1?q%Zv!F(J77+p>N&&kKep`d+kwxPro}xw@>v?ECQz$0H0W> zKa+;E@srm-d%O3~*_F=M@%Yd|CrZpgQl`9}B)u)Q#f_!>aqTK!HRYruzl`q+S5w*9 zS>md5Zp(O(t=ZSbiw?AX(|hl2SnO#RuW=E8H-{g;48HnPPRjBXc4ER`j>nd-!lDU+gw=EF#%UV3PR>*_ui;HmvTeD||cy-?-KRa=7EBIM)8eYW`IyH4~W zu_5O#wv~tk+lKhf8MVAo#!tXT23QvV)Mx7&Y`Y)XI0zlKq4U`7@C_>FSt|r= zWz%k@JQqit*at+6@-~*o7<9$i=NQ2$2RxUl8)U|VeHt!%BQ%WtOt9 zBcJgQcW%u+!_NNg?YCIMj^oVvqXpH=V_<5l?($NTrY&H zodsu=0Ct|I)m9WbyC#>D`;%VutaxSpEWapcUn%$SP26}_%h!Am;p zJLvp8^u}7gvWXX7z*Hdmz09T8jWh;Q36kU%2)=@C#6Wp^ZCKGRAB4C zT$4Kw{r0P`IxhaREMEt8jM2O-Ia)IeJQ<_`mN>D?&%`tKw#IsB6_A2Ke_-XzU^J1xHw_kas%DrdtUmH7)Rs8b<5h38K{XLeX_OF0>tR$0Sv?6|x%ro6_ z5g?W!Z!R-Gh_+wg)~#EYc;2135m&Yk{^+B3-+5;W#A6m(Zp{3{`y;fSvb;CSaRQw)l-hIeu_T^%3Mgew(VkRZZPmie(mIpv zP~(H`DKGV(A+e3{In);O2mZ)YFB|sgn;YeQFU+Z1`?g-6+QjDxX{i?!waJFTV@=Fi znlIVD>L--(ZL~G>ca%xq@&1tfDdO*bw|!6AV%5onjCKQAplwNnwu(>CR^@B6;* zyJ>eS{$t7#dc3njo^NDrnczB zVP-`K*};>+KIb^seNUYuiE<3QfUcq@sIqbitFB!1$CV40T;(~oS~GBI94u!SnQfp7 zP}K&@QdeFTb`*})Nkd=Z05~m#h=NGmvdn#&%AkA$jawZM)ji)a1Uc2#F`j6 zjM|g}ziI5TH=I@hEvb)>1hr(NF zuExuNyuG^tKIs}8` zlTzd)ORffHTG6kULB%sg(N<54W&7B?+BjD6Q<7g9)SBmNygw!Pjxnpyljrh23~8<9 zpa``fj-ms(R6n___(PYjbZhcO&L`5bML{TE)o%i=l!Qej-<3@8Xq^C-8UU@_!-=k3 zE>S0m?s+15(E=}0NMGRB4II+f#CJ$;jgNl*Ta(*IcW(rTN-h@&mpK5}WJ;;x)IyKo z`@lwO`l1;{`;oN{N=m6{0MzXG?ef{b^_8!D^+&Cedu9zyhEMr38i7;CKcgLcpjmn2 z=OWPM*Is+=5886e|Iy0bm|%%hyrDf1eR(iNllthxk8Dcet_#cj^V|4`CHD5rXol5} zos-aOSFiij1Z2E9oQW!2=qE5FmI;hKC$kv~c`a|_C?h`tP%U&(5EWwcPgGyxX`kd# zy8l6Y13F9g6+hl^t_mSFO>^_omkyXV}d$}fef`^Ll9SPmKc z5=z`Z@+!P*i@*#Ff@`S+%G);g3HhsckX`I%7>!TBo0gV=8vk$*V#s9Jo-S}A zTLv4;XV=<_-0fSp>yq2|4O??5yqs>>BMJUFEgMtdYKc5{!6!d&Vt+{AlfV-879R8Y z_~})(lgEzq4F7eLd(rk6Bs_fEw!@s{x0$uX&ynreA$YTxaTUDzAy*OJ<}iOsm9^DGDVMZ+hd z^RDfOXKB5K);=4HvdeasKPNZCavFdu^HkI6tx8<{oA$_qJ_pI8C>+-uOd75QOny^s zG3)JaVRDS3JpubKKb9)k%qL2GDpAYyAKJSH9@w#lQvRY%KK_dBOH$+o*h=|b64=!v zBE(;hejv?p-&K1&io_#L@29at+l3fyj~e?E@u0W3fMPOIqRLpZ)MN^?&s-*cI{m-* z)?2^(lcz@kQg}Y6#nVdwKYtycXIaW;_KA?2cJkjX{a09+kTh9CQ<-_j%U+PQLLLu<3x3vxoa-y* zjwZJq=FzHyf_aDEIH~0A*za{#1!B}Bb~S#oK~C&S%&0EM1Jq?&ZF{kkF_+p$Qf3{f z2E(&N6ROQZE1byucZ^C4d4^j`C5U5+ogbz+)(aM6SH7Rw%Q?LdR%BP>zTwv%9b~Ex z7EIM+3TnI!GrL)`gF-H60NHP}c|g$bPR@wlFmE=DR?2!lcQjUCVmw1(fGtvs(}J~= zp!VgRxkaN648?DO(GgVXm96$;w{O4tXYB&Ozh@HJIRvTq3W7Q1&s+pf zj{rP#UAoyIZS3Uy(f?<5#s9BZ{$=~ym5!esLPL`l$9k*Bdx#bT?rat;^o5g2XTci? z3cx@wsZF?+7gb;BVV#zUM#>`^G>YJ8G;KA7oE?=h{|fP*7DK zs=fuWn+MqVCuPxR^^uolhzD;?b{V{u=gJYzWhu9Ogs;-ZfJgGYs|0=Vi_Gi~!8LjT z@q;02k8h)r&3Z-7Gx2A6mYbE)$w?Ts;S};;7PhkFAr~yE+IjdT{;G`1l2u(fbdu`i zPeqc$)?V*3a%)D}i1W4#T{54!rX3q^HCy>sgC^Lq|7jOSh|QBqGJn`rNzObGPdPHV zJm`o;uD?O#6R*lA86N9LyE*|V*y7||WtrcXXD!OPzJVaooL6xXE1?yP1^2i#(U6Ip z9|yWBJ+ZbbyU7@UhN`KuQEJ1SmC+DCS(=a%cV97^o7w14B-{+^3sl}`(xvT(aIjLtqe$Jx8+Q&OoM+u9{o+$@fzPr;{z$}i?QQog zLAB3)x}0y#cM(mj(yhF;{amxRzpKyar@RgH*7nqB6@!|@V^;Q=vhsO3#zs8K6aL_z zLWO_wSG2&`|C`_1&5~9<0^rMFt*<;8czs*A5%MQXKeWfi#{QLc#^_cICC74G&9jV& zJeNv_oH0>??(vSVk@OS%0fc99t}H%ORjCsfTjT;|P2qAo}ji-y|-TylmU& z{cCM|s9!)ZFy1cjF~=O>Pap|akDp;;t8e&Xj*`zoITQkuz$9N6fwQln6~hX|Pve7b zsb6(5-q=rahxv!E3Mt*S$6JJ%ANOFp)tGl#g!Z{;f+8il z;Sgt+p3}Bwm~2nFo6LNkm!q#e9y8kN>q|f3Gv4##ifni4YL?YMw8uYpB`FQUE960{ zuVx+m(>@Q!*hee*cwob~#2CnN)&2z@bU&W0ErU8qZ2zS{vCAAsV!UBjN38UR=4Rr> z*vUS};6c&V>rY>`T2C0N%Y^OeS$6&eXR4%;V*2 zm+N!uoB;ET=9tPn+{jGG{tZ)`$c%c$c8E->$nga^+qD=q&t5Q$D z9PBY=y^&YzgCf3>$m6+R=FC`9+8rFYRQxdcpPeBWNe3v@)(&9V8GWb|vZCkN;E!0^ zA*sHV)9(BC?JF>bwE{O+C(hn6B}d9=(O(Uz#>m(_*PlNlm4Wk)IMXT9k0A1=K6RQ1 zGABWk>yr4&JLd5fp3P;4PV)HedFHXU;y8P15~0oqB-Htf#CF&YazrKPphmtjCf6%l}(4N2L;-~y0Ug@}e^%Im^KDDvj{F=$fXKV3BaxK^V>vK4BEMi$y zfT&Co?HK!>`ImiT%)@|@g6e%?7w^_Sxf!x9?)#JnWp0N4!tEFBH}E*Q@O{jRU&tSu z)}RjhYKDkkocsjJLz_d0!QQmAJyOQ{3UU&oZ#CYSJMqcWs#|g4-Y<9*ocz8W;yvB3 z%ck#J7l3CR2pZe-ozy347{_EfR zZ%sQoF<{V|^j0uWzt2ttPLBXQJAJv;5^db%{Q1)V*Is+=kJ#k>Q={U_oH2@R*A1lX zo2p!`V?ajiz*^2#K`(>=hf%@LQbk!9y`sw_t-pbN?y7cl(J&b&SNYt~onmgOGZutS z+$!>6pC1k%#DzjHMq1D^}vyh>_C)?aUp zb0#Wn3S89}v-ADoRaGt(oSb-ZxXKRY5|P@Yo7n)6RvuaCQ?#EooXy$|iuArp9p8SQa^Q zVfNI%hD;7A=|z?TW;@frnW zP#mfgOH0yp+)jy0-6_R&w*d}uJAPLQi$UVIFbA2w+SlZBqm~j`BTB60iPws~l3m36 zx$pU&?_miSU7_w!-mp%kXEFlq7Rxi)yi=o61hi56hR#NBOAP;#__{Mfor5FYp{4BC zHros$l`co#uV|YbWr=fu8T%~TDY^&2;Wt$si@U{-ObB{+6|>0VhjUF?*IQ$mX}*qC zk-IDGZeZo-ioLBhtlCXJmTI}vOJUmOz8Ne0L&jUN6fUfgYnStZb{6f7fOuVaQ?%Bk zP)@%gUw-Wl2$7*J>c1q~iY7j_(Sidm*(k;D^RYv4E20 zv8b4b?)(=W-Y6d)1dz<4->%Zn^=DMxz^|0`DHm6xd0nl=KzF8L6Q;Oh-)jngmZQU6 zdLFMJV)2y?8-GA7LTEi+HUaK@X&E*;Yl(Jf6+v#_n*8IU)3+M;jGMJ_%3Kh!=I-^Q zGA5$&S;tr6l+7zI(VzL7!zLe))3!I$$+l*EXkf7vGra5D#A6)l!)$X6YnvW-Eb}PR z9j@6*8^%yep#@Fb2E0~|IOlrkM%3bNYP8S?bO8oWK0uFEE5T$OsvcuZo)=}$3TC$* zH94(jF;9I-x0*zswSH^z$i<^%u|cr>A0J7~i@HNho#o371AZiLap#se%uV(cfD$C+ z;_-r_XWO3AWj&r@s>HyzMF*C|;XE&8X6(rK7;oVd{^z+lqh=lGS+2DxYb?uodnG^` z!{p9estgpOu7)cgM7F9pn$))ZCeCMPG3ROAtwG}ziS2uOw>BHfkTkvky%s=Epo1d;d(5Z80ls zm5ZSKMs$IdRI83aa^6&&+E=aC6DBf7Dl)|GLtAAU zNpoV4(coGM{nBf1eBle<`zL?*cYpUkGz|8n3J@%tZ-~Db|jw<wyhR=lHzgV6>|BzErXmmYjAlo#k4& zKDTBhkC(-!OH0VgUa-s<&j5=4z?PupIE(H8U8O>H1^P^%{(`}$b?O02mZXvReWdyGkNc838R=Gj-rh%1 z$F@B?pXb>_xLwFBX4E}dImT5Gh_P%O!sMV=&mM#F{`;-%hjb6|iQBE)x4iYp^6mk) zB@!nqy3pl>^b_w2h_>+V0mTSkZ`xe1#3xYo9st@;$}tLxeglu^ZF#Q)$(M1b=Nf$Z zei47(KJliwaPoBTUiy@4kvS$~|E&E5eWNmcHGVEoxY*ab8<2bFop+q2ZkOCNFG6gu zTIx@Kt@6O#jHO@f~fL*pS5akV}4c3S<)SG52t%?kcWi{^`*{->mbSN<<)Nd^F z|A1}f0?sg=wVkqEXxljChxlB~dg&z|Gl_Kf?t4dLXuA={!Q2$^IbUGU7Z*l-&7II` z-_+ie7<1Z)SL^6-92x&BnF4*JPXw~v@Lq^5!wzL_^2a!+SYk}5Kgx-AFbY2^BdIWx zOe&i2k`KZ{CJ|DxN%lnA`}WsKhcpVaMC5fZ?tJJuagM4@JY z7+!*N@#&8+U}l_F_mG%a_!=@9HzQ0E?SeC0DQ zMn!qR2B1zPs!-M2QwZRSyGLcGjGBvuS*!NI2&2hDu|ES8CUcIJ|6U=-#=Mo69y{Jv_n zo)D@TzfhDF@mFr$dgU(|jq}0J{$P;vr~JYZI9&j^a0s6o0BzWpZ{NK67wkmpl}*p5 z6UnAVVoI4@*pTK{GG3zQfXO;rpAjUJ?bh; z7+R0sWZ;pqtm?|e6E1YML%g8nUvaWlE;rerhKKUBa0Q~OcUU7*jXn&F5L3CyHCvyN zl3c`{&dtK81Jmk0M(l@yUqStVE>|T&k}YnW_GqnDuRc@GpP;>tBDvB=bc8wMcmaIu)OV2%I|hS?I-i8`QVt z{IOwQ{{HX({vW(?{l@=g<#?@mnN{j1hm66JNu7|hRO_mJng{?sIdlJmwuJSDEr--w zvRRTDOS$ZD+Lj9DM+)+CD1(_Nn~n6jK?G!f6z0M~ED^_}c;0#aS!ZOFnC^02^psE7 z6H-zbiy{5|^X;=jJeQ}XV579^qYVzud4@FZcDKhVkejz+$#KjC><7?RXGxuwg<}8i z-MiI4czMZPey%vTfxgva7ixp&<~K{-qwlnP2`7>(or)^D1E~u`(NNP$JL`C^R!HQs z!I&)%f(tO^5wm&ZlGrC!^zEsfjS$xwTXT`v$y1ggsy}$7gJ<@|i<4GrHmVg0g{HP7 z6Qo(0R>-u#%G@VcmcxfE$p7qTuM_V83AsFeiQ|3S_LP|4G{zKjp(>Z`TCTo;U;AuL zctU^Sg|^)=U*5U1_Grppk0wTTlFINa($ODbRWXMJg%tc&j?&})%~l+{xFUL~PR(Xd z5l{q0hqk2;e{a$_VvMQ0R~NZ)`S{wi*T-P4$$FdZhxz|T`rO?E`;y82F${_#x@M`- zyEt1VKK03rRo-Jz1ZcM&&uH4M)ZsMpMros-@HjuFE%wXiilV&8wrU`!y82~2ctu1!xwDo_uFI=%jyvNg*Q?N^ zU~-7^Cw~=C&tp7=&E_lhP>bCzztoiiN`Rt%nz=_hVqF{B{24u+wM8*yoYaGq2ttct zj)HE3HZxWlEsga~_%T*;i!yA)CH%_1RrYN(W8Im8~J+L;;n1#;ka3YR~sXbU&c~Pl7N!SO`jY)Wjn@3g#%}uh)8U#i@ug?9IWvl3q3WS8BsR3$KGgJ(NjM1 zTRoX;L@UL17uzMBQT?br(}{cKur=w(>Y(9vFfO^Ar#WKEmn>UEldXQ|ZCCorrMfFe z>0F8xT|Rk`EA^8t9Mys(yuR&4+zCyNKdlN*0vVb*!>ky{7J->uhJs)BtIlW|8qe!7 zZ?bMLRhtdB5Y;KaNy=wYlBSWYHt4r%E7cg7^Zbj{_+S|8XTI{4ANon-A)vn^Ta{El z<L0dR1ZOCV@zyA79Tm7G#$e)XprVrEaYYqsBzwAw}&-kv`|MaJG z0}qT{2`411JSiq0YrqnFPce=Q8g_zQs84w#vD8O$4_53UWPk3%wnDF7YP(g6oheAT z;H(g9yP787;?roduG}~-#t(l+w=8j+YAVT^aImV~Il^IG>Th)BHS@9!~^L z9s79V_*p|4H(oCETz&J6H-FLc9}j2ynP7TX01?|;#sMUE8plsu>62dZ`KSE$aevFJ z>!(lL^d^0Ib6%F@2T*u!ax9(qM+(Xa&8J8_Vh}#z9{z<;J{Y5odC4CB=&j<=`_Hl= zZOLGG%dwXFBDb|fF_hYG_`xU3%G-G@UiA}MP0UNBSwZb8g`@^j?ap3;U!-PP7MPaX zr?sqa(p#(%RxByk1KPnq-ahP)j2PWzQor(cm_aD?#gOK$?2!~<^g?!R_&#T`)El5#LdjPeE|Tymk6B@{$>i-nw@LAq*Z8dY zCA<$5ALlkkNa*lJd8Pd50gs<#Tap=b#+&9Kcr`{=h$Y2gy&pSx*E}uZ?VuI)j+h#9 z!`=oT%dlm$b^Lqf?evWjDD)A1#W=wJJ>G;(!fAqAoj2N8cftKX*?;$Az*bZE;)}Ld z<{5qVOqL`^oib^G9c1UTT{Cu&VPCVh{TCI_Vh(aIz1?`6G!qZHf6i_qBXzFd;Y2Em=|9tz+6Do&9Oc;+iC-WUkcL-`k5<8sr zPv3BYHn$Yo{4!x)zY(8QqMmmcd?M^p*{9+U9w;}#ik(m5XhX&TQ;D5GeDjv)#knYv z_kIyF50ssV`c5ZE-GPFzW_j!rqQw!H0x%KoI9;U)t3OuQO|S?YlxrGsVWreZJ=v|| zqku7p@rRG%WYXpPJ!Vy^uV_#(?y_ud`i3kJ^yu zF(|fZ*sKS|M$89=vO~_nPdxzQ!c=CaG8d-eMAO`0yRidYx9Mw}+qMF4Z97bG^%G^8 zwnvX`*OTS7eMUGl&pl+Yi%19#zoyr*%k#3dFJC-r7XY$!l&cR0oyQaxBy{3{RKGLk^92;fj@rn(QTz~TbW*T1$jXc_ zjWP6}PtRn>=jcK(&tdK}{wglQOpQZpk&+xI|JSbDcBFP1Pvy1G)8QZQS|Jg;dzb72 z0ON|b<{W{s4(-TP-KAf^vlM;SfEatZKkSlL$Q2Q^B7#k4jE(BZf<-$?xo@1X@l*Xn zhIp(bjYY{zY&_@}eKS|%Oo$Udy@SPVtbAcGqS#R9?@1Y@n*3@B73(AQ1#+}W?H%>v zGlD_=_?50%|$$b2+^snNNhvS8)Yd3`F2srwMpwkxATIy`2}!MIW}9-Rg5K?bMe;339@6% zHD9{96`R>cl$)S$*PO>T(o#HAs$4B(Zo8o~VXJ+vkofF_HC~wK^LRl=@n|YmYL#j~ zNQse(RcNJvjdlUMC=U8j62Q1@&gBckC%{gj^a2*lp+DR0Ed z{Tczy{N(zhk8K}(#XkEtZ1Ghs6|{*LfVsw;nozvx&SX(}C&A1UZIUy2>3|h>J&+&q zCc2e0l?@`t1?AYl#l2n?4;Qj-NKcK`4ylN9vAyhi6mbBi;^hK5n|cQY0&R{lr)K+$ zvc`h)PGAF1@}mg#!V`-sW&Q0n(aC*gDrz{n%Dr=HcSoHPJBek5BgTn|aokViYG|To z(|!qq9@EYM=6|lnT;A zM&-or;tqf2LzX|Lyy;hp!FW=$+lHK#A=M86(7&M~1?2}653w%{6JE{D3_RIVK3D3f z1475km4%#dphk`VC3ackBIRa{lK!TM5|xnhsqju>!JvssYHoE#c9ctSHBVqkW=>54 zsW{tK6T2%OBRii38r&w9F@wJ;C7}lDT2(pdGl&_jpfpx1B?HUiAl9m-MAnF@U5U6r zExfa{FTMJ`-}}dntu9r*&^QsE?g*S#06g9Ox8*$>t^d9D+G~H*W`j3JVSj_aNyKp+ zt9(kbc|dobx>>~8TN8)?Qf~OyeEArz*qs(zH4t~Zu=}XH8cJQ0mS6eBl(!)K(P0fZ z_(vTbT?yYh(9-fRbXiUvcO?wL@O70rcr6a#=?+oIH6F4g=z9}b`7TE+bDV1jEWg|F zUNg_#8C{Vk-g?t9@@kXEHo<%Ru$O34?<_8dM6~aqOShBVr5@s9>+GwF$H>E=mKvZH zKaz~d(#Z8v&MxFT89sLS;)UsQS6=Y~3Z5so)$VRV*IjU;@G;9tN+4O%`tiqM)MKeQ zWe4Qn9@-%pJof>Z1)J|s^VlAwir+x=j|x1r*mgZ!YG&Ynn;n7z8D4B8X-TJ zGioDD47$dP#)?p~4VA~70P#2pbdoS=ga1nbEZ(@R9-lzR$6k_ymXomQn>=@V|Cpfo zSx05sm+w@G-gT@2fuT4cM)rTj=;4T`lS z_f5njx7xJ5YCj5A0nB&o@vn_JS)Akpbd^Xkhqi@P=W1780xl`l%I<^S$-!1>T{nf-lk)G#elBmya zPd95#QdRw6f#t$VFTeCsJ-FwSfFGn!2}))&SId$R6BvFghm|MoBwCB?x0v(TG4Tl< zrF-}8RV>lKj-^}IjTd_FzprPWL#oW5QmJy4Q0L6(5SBi_WU(M|p#07|Z#U6Y^3E&q z!*+KdSYwe7>63CPzWnmdxhJ~;G}x{IoN7?X)aOrNhJmUJk1S% z6vP&(+kqONCXAG^7?@ViqztlAMx|(9dfq<8ro{4d9oP^TJ)ccX{Dfj+i|46dc;Q8d zKrWV0hak_@<#@`@CzGp{vNmyEEYB5@SPK2}%P}rtz4K1_l%pUE6c-Gmp91K)=bo?I z+_(vv~RBR4lg5@7iZYN#NbQd8^39_8Ax7VlI78?%M();mQ*8oAy1_@z3WtDmlqq?ZC`zWr;P)p3+61Vnv)*LpsH%7=@&Z!3lB7P$R?_JO_8QX%N zNEfUfkW14mN{!q~)t5v{#mZKJ_~`;Pd5-0sJ9omD1wbRzQY{F1ICfs}6}!SX#0NIX zv|^N=v-fRS&|QdlGe^RSeQlApjP}ttc*lONdz`n3+5GS)|G_`_jlXH?TmaCY6$6sO zDLvg0XlH3pcRNnrM+CH2Aosu5UVDwV{&y$;eXx9b@GZKhs}{3?fJHqfxBb!askUIQ zs)GaNKA9abw6!zVj%u_9Waw>2Aj_?~VGu18tu)#!T^HmPvn%8Qs@d2lKd6@~y4(8V zDxc%5QeBOo;x9#~SzTp1f$}1_t&MB=NI2YzzycC8{7s;HQeRtv|Aff$Wo)VFqZ^~# z!1q{BXagx%N5nPDSx4nVvcewaREM8sav2+{k7as#I7BP5LMG4XAe1Cfa*0W;kR+P( ztOi~6nT%*N8F(@WUovuK%lHl(VsUBFImPv2r=nmz3U zgtX>ck0a@Z*eH*&R69l||6rH0@d(X&c)BQF$f+?#Q?P6UvE)y{&k+wekj zN(lG(oj5unZGcQ!BKF&8Z?BrEg{swiwcz7*!77%QiVF>kDwuU@|VZ?3BO(MNCv(9bdR ziD4BlF@%>$#}#`=4(g5vIAi@Fz#yOt0xQQ2i{ptb7rH|PN;OlpQ&HZ#P-1VHmt4_) zJT4sc_R{OM5Uk?Wfm(3YzShvz=u3&il1-tex)vUzWoE34POi3CP7zU&V6UdY%*dk}^+Rct z$#}Ue9BBNl*^^Ggpz^{d6DjAE1Hai8UpiwQIw{X?9@qbj+~7fo12t;SvTP8p(3x~hN`v=J%KydCAy2fT-NEL_G>S?%26#=1z_ z0SLG4B<2?Iy@hHnvu<#W15W-ca%=Dt(KX~uUxS<4&Wb9_uIlH^J3mq%q`)3T?l2BY zBT$@|J+-9(d7tjW*(;Pi%;mlM7m(Xwu8#LTW=wdme3he6E2EJWMj~&jJ9HBs=M(BZ zt;ym^t<{43Yp2&vS@6-F5T#zR*JXl}_+I2E(Gp%yJ}hI$v2T3+>%aG3FbK{Q83`H< z$}{?@{1isu)R9kNr%hbW8z&npe^-9s2Y%oW+w=GTmCNgBgGw$tGk!GDz4X$Hbvg9? z2lp@Cub=x`omvvgyeRBBH>fPV(usFGm$WiTZGjXXI67DsW>(9zSPm83SPm7>t7RFu zw|t3b$g$*9&xQp>zh{?j*)m;E=0ld#@d7->!#BXZ71HEfw`wd+H--Ta7ueFbq95y! zw}O?60J1!ep{1q7BtC(N!X;l8s%Q0BcKN`T6qAr+De~Zo^L!b%KW`|x!~NusT|CcY z7lHUOZxSwvWz2dWTFqB3nU>3=Ug9N|u+wIX5sy3Y9J>*$-SG(?jfeN{uDmkarAr76 z26U45Su#~j$W#bTei>8kE$X)P*p}QLn^+PX%My7sqAa020t8+E{8iDpckg}g*Jz#M zxB-YrYWs%dl$9-vFg3n|rs$a*k6l>*8X%7hy!*~OwSV{=rJjA|Jr`QmJi0F2>{GTu zx%tQ)+RzAjp-E#4MKYF4Z4eh(l9??(#Uq#em}egES$VNMUIbk+61H%2U1j7+S%z|+ z|CkRXFFABXqg<11G%oKOPoZkVu_`(|KH|wL`B5C^sq5F`%P>49#p6rICYHg*b`x!o z8$43N+@Li2?rj$Hm>dJCeNuJ&!V+yOXU@o=XePW@zLV=JplXH=3%YZ5GZipw~X)zH| zPSq$b2-(CC!O&;^efj3gA!q6~-oEwiZ~y&&(g+-|=`jF+1+F}FPQOo81Wqdeo~rJe zI-B=7lYi@#%UE9{tc2?9-j|)0W168Czs=lPkF5Bwg z9UWMGI$(F5oe^@`#wgp&`c7sY@^Nx@1?+&q*r*Q(_1mNiFZ33KRm@$ITSA35JC=0T zLU%#XWLABQzBIX&n8uuya8j{?v{`f!un?ar6?H7E^W8tO&n|ua2i|z&51Ha=6+jFA zGgCTU0C+aKQd5cc9GAcJr7!(n+d=+xclM_pnR41b>{tXeP7#_9&K0f#>X|$|7tnsC zZl_t>xY3HjihX`tJtfBh?3Cgv|3$ciR4_W@~yT9#(9#A;gwof@`^2<$y%|JH_QoD zA~}rEGDhZQx^yEC#4OwMpL|WxY5w4oV$*Y1y^)ymRl%MiD`Q-#i>4gS5ngg3lO@B> zOg=cRg{tRVNW!1_88lfyQstZrDWkWta|0;U2XRP9JJZVr=*{@{NkHJAS`eegne@w@mo6?)^@9lFUmt_7Ws*9 z6TdWcyA$m-xIuqnVWK%zs&WH2=mXJ@|4RU_U%v9QCd9eoco6{I<$#5HN>4=uPM-mM zD!OUa$-MDVu75AT_~Os9^qbF*bcxxg&1}9yZQFiKg)I5c2X6V(XGv+Sf zX*2Ae^CsY$GOrSs1(qyn2|LRYwS*l>H(_mcRNAbW*LZm{&r7=U@-bxmfWsX<>y~k^ zzY*Nt4FA3@_r`xS{Lq)Wr*?^fgOnd7UMj+kYWoyf(D>XBrM?Xa-?)A)81_pX)JEF$ z(W6*$ubo|S@6w0%rr678@!36pYpls)7k;F20Bse~u3oi|#MshnrHE7TKjiJr#!ypK z=54gyqa+%GChMPA6WFd?o-|KshZ3K^iFD>m7vI-1Vv!{{=5iW(mQD0Dgz9D_n9O6` z=&_6L4Z9NFk^5s2WjSLWmpfElgt+FI$96))A};w2{DG-Pw+8dsN7wmh*q7kAghvB?sn}F2$GLAa~xtRx)oIXa2PijbX+Y%XY(ivz_)Gc{}`welKG-+S6LA*6BiR*ozt% z)Wh>?#tMdnle|2smZ>rfnkaGWxqbQN5ecN`5;Vw8EZz? zr}>Z=`%uloM*AvJv5P`yhss%fj0U!kqawo`#3x5d;PEJ)2K2&i59~Vnkx8~seT|3x zP2R4Ve{~$qJd^hcZC;E=OXx?!8DD6K{W@`d&%VMWTOQ-Hvp@LdFMs)mf9tn?>suy{ zih5GHTs-AZSp?cq{8Q%m$=w z@^_;B#EwonKr@M}-eZIgG(M@Q198b@otSi6!;Po@slf@f`s;lc*i7zu2dqA^!>Za( z=$)AlZnc=UFVt5!=i3h~c0iAc-N1^SV?<8=7gei5!Tyyi* zo~im6iluSZl83eo1nc|_^5T@d1gV=ZBexF~sC zt)d+ISQd@8>p+Pza$|zdGpqP33Ab4U<3e26fHY#{dM?Wjv}ma)EoXkbUM{{py!`Oe zS$oXfF9P8ciN4fHoIm%O4H18flv~N7ol~1ufL>%ZVE}$|Vu&uayZoG(5riKs1m{>;~X{#=7 zfvfkQ^cY+y{P^Q`0aX_skn)RE#plrCg7DCW9u9M>iFm0E?o*0KDlYV~c!ZgSeoru@ z0){VaVN1dMyp7DBX}XG_1uT`b2AQ$a4C+e zS_Ag`x;H1Z^z*si{7I>|OBej%-Q)rV1^c28=?(eB1%Xxp<7sM*z>P~Gh@9`&Ma+G?l8A_5=I(_&G&LlY?zU#^+{1^85%0IPd&cS+% z#Yo|0H?9<)L7P;cA#nn7QlS8)mrHTbE@+E!1vX|EF%k%UKvW5Uv(#J%vzOxJhEt+Z ztL!1Sr*D)93l%HA@|CVS_T)pwht~2f1LS-bU_px1TNmSUmW)ZwQN8l2C$%g+2l*b{ z=)KADV?t{W^oZdoTw`3Nu?~;97nrWOdp7Fo^bb?wSTY_@ws!Y0Rj!ts2AZZ%5?2w} zfSQ7JVb&4#LY-``wY&vYddc5YVubm*4ufQ&cR|{!LSJL{wTymkaZsFESf#jVmyQ_e%>WjBB)Wu=v~q@qz>LV= z!jWFqBWE1Q8q4O+Km5s0e)2=pwi=b)$aU#^6@mRf*?g~>@i*RCP0CxJefHV?H?Lm( z`!DUPqm~YjPG8xK+?N7Bp)hH`ddWmLFgpNrqCu{|zM-c}#`Z@)s2ZJZmTa-TBw>pr zDX-^Q9A(iRwDX1Cmm%nWO6LF{h(pC;^{PB=rOnu$f6Vbjd3ezI%a&q941D`ts^0!3 zC|crwk;&Z(gvzfhr%e6J2>RGg8;KJaN>Qh*n8#md_{)tEkAhWrO)WGN^Y&6e#oT{J z+e<*vHOtKPJ^A-dryb#sY4&!MMi*oKFO+fRTek}kQ^gf{jDQgIzdR5i(YeLIs!$XM zc(PB<7|>sCh)MlxUj3^Ya(hX!#17*_e{F<*&R>tvJCAL2k)%6n`i_`|OhfuuCnCeG z8VWBkEBh`BXjb2jf9dbJ#G$CXA0v#UATKgbYs{RIkay`?Cc z+V#k%Pz@ao!M8$5GwrAzyyyHSiK3~D?Y3Pe{J>T`(ccU9HS|lXrxJlI*IaXr_NAwf zGr8injj+X=FkZciIJI4B!3r(mf?-$-;;&e-zF{nx6yH}O7Ji&+dWwE?TIj4QQcy?(4Ti$;vBr)@lI zYdPzT)+csqJhR&z_R==Od&rq?k_W;*&)lb3F4j)1x%36|OR9A1jZ0Zz6hFqSx@yp9 zg2*dw=+jnhaH3Q8A9eYsxki^^0Hmo7PPdw2zxJE|g*TnTPqhh>WJ0OFQ6A$$zzGXD^=rhELCZxq-t0=YhSJN$bN?do{_cS5aeesS9_V=(^*&gB{NKAD z8!m`febp3LrMvdyU$5!{_hos_69;VF!^aN|KPaZ4VoVh|CGLTJ`i(8*5C(^bir^z8 zsk%39QV`s2s74*|?Nd(}o3Rg2sBx*U72KNc4S8svcl=>OYxBI3zn z>|3-GNMFbnVfH_hlVfPZ+=b)}D2d_nq>*4Ly5=Ic#T2%?4|!E}U@R zrcTUb%w&|UP0+OlF?Q50PR{E6*-i0OpkIf0Isd{A@EA{usT%$0HI^DvjMT4npLw5h zt`N+tX!OEBkdVJ^bA|avLPl|g=dZD}+@prCrFr#7!|RuQ+p8=3j=u1zU5GFnS|cz= z8}#FrzKrJ;gd=v5zV$V6)rmgsRT<>apWa?IxCGaVIR#xObBM9zv9lio_5R?nzWv5; z`3kqLef~<|eBKMkHOBL0Nj@W0e!Aj=qfSa`4G#O zHplC6z5Zz8xS_s>uDLtwd{M@wA8+!$;?f6&qvlhL485g~{&FkUg ze3hGlUfDg}dJYEt%(Dnc=zjK0ZJ(n=y^X9=rWkZ_jpB(I)N>?`JzCSAJo$QPqh!cg zFJmQ)b0@#p*Z|#1jZtG~!EbHLCJYy6oNqc(P1km#J+FlR<#;;Y;yGGyxhiZ5c@C0Z zB?yZ#LZZy^P<}MAR&bbq(f3OJI!=3L3?$mhYe2=D(Xm^{r^e5I zL;v_3610ZjUeK43AJ8`zS4Z4@^Y0q#KeZ17jcC_FV%6*PeTzU|XZ*go{Wsp zr46IXFxXlY}QNGy-jdopxvMY3-vXUdw;ycYgC{Hc9`k z!=di24I6!$O{d|eC^mlzUcr%-8DjA3<4MUL6i9^cs*dJS#l%Bj-={!LR( zu1_C3@MddMJ%!})#9dZHBqdBg^;Lo_^Md%T5#9hKL=`Hu%yI))%z+qk-J0sD&&`Cb z`0m2tdr(JtXdO3dJgQZhTCOr9eH=R#m*s?OZu&)yG6?WAbOf-4*Q&LBu^Bd^-aKf9 z$*$d?`zjcRxTtuTAE_f z=+k&d!n>l#qG!<6g-@1dIqKU!z+$s}QfC8{F@w9|a%pn|9uM= zVs@D`S|_Bs=FTHNZSAexxxBUOK1Xn8@GveejeIDRy{&Y_K_(f4Dj(zq0yc*B96=M> zg(YK`AF^T}`FX@stI#uy6C+5sFuQpHDt?2m6bxE`12>Hh>ES#Be^v$uVpIl_WSmI< z1(*3x`S#tr|CT}ihg1)f!4ROaY~<^7)gz#peAWHD_OC}keMato_0Rmy@BQp&|LFex zd;jqRdld*%k_r2(U;U~}E=nvFoT%2VTX$>=p+CXgbg-4_+xb<33q3QbJptWx#P&?B z5g^qB(Nv$cjP;Aqrt;HI*}fFI*@D^cpGDJXv(;K*v`wvUy<%JMc);4?q4?H*+jgfU z9rO)bvUmNEfb!y3DJFNO^hh<%P!Qj&TO$-Be6s(`JD<^fRI zu>*UVs?4A)&_DRWuQf5oEPj#LN*7PUP3NngCSe89tM2si-lz9(+_CL`#D}e_&uv?L zYTIoFG1V;{$obRI(?*T=Ib*0tFC1d);p1{k!Ixirne9!7Yr8h(aDwfbzE#-xY!78i zpw@!gInu})p*EvZ<(fku!kDqtHc9x=H=&j7sJ31~_Hnnn&D`|pnHY>u{B#Fg(!SEp zKod{7VFDH>G9tGe=G>UZw8DyCX%$O3nrJbO`&%J#AFPkdHkqJ?SCm%iu5B-V^yqP0 zYgo@2Pl(;I_-X65&qEuVN{?*|uwQ^M0kT_eP4~d&Me)vP4xfSWj*H3Jn&F4%Q$;W`0nY$Qe#+SudF`HnUD(iW@ zg>7#hvJF}13Bun3w_D8sOnX@vgljI4F7i53={G-SV|6Z z*I$k7U>l?r07hjXRrzEv|L<9Bm12%3bIb(syv{FPwOG$%^tK%4nbs2dBPGP^!GpM^ z5KKSU4x?#$7IX0-ehnzi1NXE%VkdO47BH3sj5oPVcDnh76Nx-cJpby;FaF^Fvt%Uh zAK^rc4{Uv`c%8lv5xBku;QP=^TOTckBxa5J`PD~wWX?7r?Q&Y{hHjAFrF zUUZvMO|EkFy3yA9s~A5NnyCCcm7`V|H{o7+X%)F4&Ms$P67hP`SLvy-nP_6;1)umq zY|PefDMeBWM`g(9q};`|}vT!tv`eX3B!K&M_)<*N)|xJrXA`;?Vt4D?2; z0+dx(E+xUKYmFmJkI$hF+m!0hrdr+r&n4hHNqN-rdr53#-9ds_1GcRkl(mXPATXc# zX#OY{uL&I#hBe^;_bx28ja;97eY7w?bh19{AQ%2xW3!!uqqet}H_|oG8zzL4fTffZ zvw8b~6-Polc2BTpjXPRqSk%=Amd6;Au{Hd=q3(E$Z*S8VJL)uEE^31bh1`zPS=kvz z8l)9aGE`=*C+ef@SUVv)u&ZP(2dGmhsTR3vjokF|qdU1|4Jg1wn5v+f{Eo;1k#?%G zT24if7G$`E{Ka5n3)GnpBaB)eO8TlJ6m2cQ$MT&cA_rIR_ZQ3 z881poapA|v9cvX{`>4`Yu6%w1*19m{#R8C0t*E)|P{Xz^F+eTP__2-=ec;scg!Gf8 zk@l`Nj|9G(qIM8&c7nQVBe&&MzPzuJZli)I10*T;$`a|Ev%bzAz8!qnPDHaj@R-$%kB8mIq$=zqhWM3 zce)%U`MUO~W7mAB4YMt{P$SPO3PZ*4TeVPLYO{tD^^lD4k6Ng8N-v4pD6Jq?yQ$+pljg&L?J%Iznnoi>>o)Q6}NT_|(!-VyK}q6S43URHM-b z9oV=%LEA!sKC>r!yWPmq-u+cQ_VDidW%%pCd>p2muBMps;&YjNduH)79fcIw({h**7s_YV9dnXSX& z>&+Xy_NYAp_bq=6u+83+hoB#JiK+UD_}sVK1C*9e{)K7bi74_--&Xv=v_L8=K!)NSC zAMLnnZ&6t#FdDzH{>E)NVC5g>x~4J4V=l8UoVR7)+myE zuUo`TcK=E4Rc0>7{ER`#?PfY|hJXEUE&gh)9J}>)rM14G;$Oek^UvcJ05DJN4Fw!WSNMB= ze4wosqcVOs3JYPSr?4+B661({j4_RmBSkTGbc@aSwtyqKCC>)LZCgutoiT~qBI>O; z8UxO=V~pmIgY(OeTNW6Np?U1QEN?ns{P`fXjP*n71h1#l?IVsEaG&_v0G)BXJnu6m zx2$LlVP0^1m5eb5u!|0Eq2hdn6u7)i${ND0u1Xw3_u4DH!lD=DoR?|tp*wER2%aC~ z92in@3(TP+YB=6nb#{4p!?HLv( z#;d-hPZ4o#=C8&mb^jq_ji4F5-@RrOaz5~)t)4q2zL1QQv#|2)87GSNW7ouZ->^>H zm=7;s^8AQ{WHQ9Kj&thnmx+0Mj>bp0dSKvu);JHl96k_R+D*Sj`>qw-{;UDwEPeQQ+kQlon;AdJpZ$_{7)K>WD;8SUmt2zCocD#PgVfAS3WPp(rkElr=Td!Y(fgcGTe!}C}=o;*cA0B({ zWIZ`2)j2MF_e{Pf$Lxz2d8`TeaVkdmzt^9xeKI9qln46+Xaq-`L_5gUmevLhE|-aQ zP`q0_i(_Zr9GG}#R{Y?{)AQ3jw$GPbBe~F}@$#BUTGHM)6ht9qJwuc_LLTiBTx3-Im*P zD#x$jGtT^vA}L9%oF)YdWb%=!dK-BtOo*xKW83V|R*iE%Cn`&|`WMl9-h$(C{G~sc zE4Kb=Y}f^WA*J2hx9|RIM*rXKL#NYpK9g5QxlUI)0&!{VO1pUNTtKl?3}Y~^o0-UCglsUQ8tU+tfSVObS0i&K3lxD^8)o+ z6iyC<;hTDbHd`}_OR^{OLdaPnj={@4_=|7VB5WE<3v|qhQDgGVW z*N{R?jT7xq@G3IpxEi87e2o*$eZS!|PWoq*uae@N(phDqJo^|_*`!AKyu^i(_{{jO zL-uCG`SqBO@guiNRi7*UxQsv5r&cP^i+<2%C1owrrrf)cUR=}@da zJ-)3(p~L0&o6t{qxLpiVNC?T<6Jxf++LEEYCb>(x)Tf@((lm0SG<+Gai5 z;(B6|n|h;j1yz69fFD;8gU@0Sv%Oyg0K?+pf5ibtvbu_a4>Bhmx+)CixjiXbiN6dQ z!3@}6Qz##$s&p3@KEuX8t{PvG%#F6C7;}f)_YVC@ruopWKpRPa^^#9IAp<@9)y3o@ zr5+r!1l4yDB)J{%oI8FEW1_@1MN^w)osbH841)k*@+XmGelSe`z%|#6r+#-0Z`c|g zdW1oYauY9lSH&2YXzPE4Sm7#-tKk0ES@R9~8fWTit$b2TtB&4J*v4ntIV1U@BdPjh zM7S=iCw)ioZC)7*b@--TBD%dn8slny=wSB)PfsV!<9zFvG>rLOds?vjYj*q0jk#ZA zG)ldnTD6Bp>L*XCXgZ-B!#VVW_Y-`Nr+`-fjuTr`Z{F-EDj#mGnc`-UP>?KD2s;;#-Gyu8hJBiq8+g#E54pA}LE>#R z?6@JR-_P-bP-=ZejyRKU-QpM(F<9zLoX_d}W3rFK5N|qFcxC5hx@mP8Z|Qr%PRLNg znp5XZlAVbT`BgD8uUu`QPFXtuk#!vc7Uk$`$7JI(=f}|yVEkmSmZu`GHZMrBX8CkP z9Kfkkx78W%rNt%MQrD7&5TZ_w{<~9lnfja(J4`gJsx~)%^Y?%M_y4&+`IA5SAB{${ z^^q~^DogA-zhV)%z5sB=I^+y@G|@c&@sEG}o3>v4-9gS)92OnYy?e1Sfa)`Q)tBzK z=0`)jf62h(1pp*X!Ypu)?p^6H+{^rtj=or+trY(eie$TRElTO9;=-?h2w%?!FeTfl^xcKzSUYDk{ zJoyl_Wme&Y1Z|=>$T6gg+sUfFz>#8^7Yaj&rf+^&+{PRXMM)T%NJjJ zB*fdBm)T{mgYEl_Q)0-P8y9rxQ-U*&Hy2oZjcZT`5X;v&-a3^Mx?|jobZUMNspWy; zm6YLAj4@ub*%KI&{8b)Q>4-dNv|aNi;t)DSBM(f_ef*TI(H`?V;X8=u&-=P1tE)Wj zLrzYpZm_zZ)H3%G-@P5jXy}(5m!y1rotzBEkAFS8fi`{f>iD)HFiQ0Yk!a*j$Am!{ zPc`oF-zv%FGlUr@v`MaV%=5D8(DAr=RrFpGSBY^E?vB02lf>Hh>Ki*=f1srJ*FJe4 zruie#t7rWp-P`Dl*lVoV%9|5Q$Mcm=;;%lCq~i#-I@5|^-n3hwCQU`qk86%AMBr1E zIo0Ny*_qhQ0Buk^STA{fD{lF`(Ro1=jsjck`FJgPVW9zCzt`$HRAYTwc7Y)LJY$B7UGRw&=MWVOZcg0B^n*vA^M}X(**+|gsM5!ZCslyC zP9He}*S7$C}39^F@ zK48Y6sNF)6`?#PWG0LPA-S;72XMbZz?z}gpXImeDiW|Ag6|nm8-}v~>hhWm4kJ^^- z-d#BGgA-9sJln~?{5F{oI}mOS9}8F}PEBX)+!6QgH4PdxQ9k+zLtayr5nXzfLswb3 zurW`1O(k&g(aC;~hcIJ)ainKg=K82UYu&FUdr7P^wLF4PwQoe9QB!+7SLF=3J;+|Jq5juL34eC3+cHV<(0QArviM_N!$`Q5NVC2VqEfvIt*X= zv{9{2jf0h{f6EBt&5z>Ch^=m7jSt`yuuQ&7?m(3wO{TU7CVe;PueMsS6_U{ z%pp3*jcoqWn5ewLrs+R6saT(I#9-1R{U;^VLx!l85RLv7PWStUrEyd=q!Z&L`V zO*qp0OSt5h2aB!31tn}~{KmS6o%k#S#-T5_l~;j1TAGyatGudt`!XwMI?;l-)h7Gm z>_dao$CgJ+Vtw#+N*j-jq3LL9wp$s}OYOx$M|TlsxEKL>yNLBKs^TIATe?MFxRtSzwe zHmtU9$oR#UaOq~zVcTS!n1-NmOEpRS{PWL!%P2O#vF%yo7RY8Y3whWp3fcDk;K73i z{PN2$Te%ey-wv9M5e3?yZL74ypSU_4y&{m#;8?=8WF1u4=BxW}PwCv}<1am}q3;NgSzY!0^KGREE| zaT`hKwvbt@v_<<}bV`;rT+b0f0C+%$zxVQWVFG8abxveozi|-X@+02c~UYizxeWt^TtbQ6vsGTyGdgF zpSRYJ?M1ezq&lWU<_JBvMn6A#^r*qvUai-AGOnN67cg!rG)~if~Mb^*Hz)m2T}28rceu9zSa5&EMMmTECvpo~^`uHQ#;R;kFe!-`k|B zxG)>J&>q^@(R^nt>bSyuGd*Z+N<3Il6|HwqRuY~)4F{mF2s9dQv@^!ij5x9N^ zz*Xpl(>`$6nLoE4eE#_#8NENP$SQTVz4NiklrLn*%{QyzbY>n!eQc-icOZ8HVrND(H(qTU1 zBj_#0hq4zl=blqNjc)XbzhO9G?q7{F(ND(|{aiP@JnebhHPyh@a!2f!s`e8x*jM5l zVxhP|Xm!f=!85mzh4pwf(1l5ME);uDaJ4Gpu6CYO-U4wFxdymAtgzc7Jj1Jh@~#O_ z@>N!PMVsqb-qJwt(WPlGkGW*N;htRG17ARoxhY?3+fOT5q0zK*@7(b`)}$D z5$z%ezd<;`;>Q3@)<-I1kQn1SUA+jr-I?*#YscRtk2Jw<+C4Z-zVnl&q)QBywGOki zMTw$3MXl;5e30=_NoDAh$Ow#Dg-8^AXRJlCnE6|t4r8<#o>ZP)eOCo3zvIidA6DrFUZb$vd5_E<1zA>yKUcy7hk^Ddc+7yk5`v&ZSSNQG_|hs5UoJ(W6(^I#xeuH#ApWeeAK7Rq|TKnrJN-AYGZNPOY>i zm-smoR{6|7s=tlQCN4(?#LGptU2IUJ#4ma1SHCMS04rYX_gc-UbW@WM`8Kt-7)#}C zvr=}gB9tz95cL^tFX1EPL`{0<{Dc^6iJSrb&LAxStuUFYbd{9{bUA*O#7Xs8)$j11}7rFkrS`oP30dTeYWA>&_Aj}oCSZ3Z!>*}wa|@xdU(f;Yivd$8XRT12%q7?O{@lS<8-p#!9LbINo|de8H0IHUjGHya0u!f79+0L3sA;x!qQvbB1Ues?JWS z|Ad*d)y+U|Pq07Q7&*?pbn6G6`fYhBpg2hHinS!W>s@7?X@s#2NFPb15~I ztpF$125wyxnOlDPu_UPCxvoWJAcb0o^7Re35Yg8Ls^{_aE4S%*TaC?|v$V#uhAVLk z$iqi@JFpSIvfEz4b%c=0gX3eOj4>ZaR)?$cYw`LQtonc2XKxpPlwRjoECSaT0Ipbv zEZkvD-h6)hw}1O@8~xW8_)apZdXVl67B)VygMwr-I2uyRoJ++L zi-rzB+$QAf5?{Hn1S;;$)rqD~s7Hn2H$OxsQK`s03=1=xqP_KmrwgZCCtOg<#;zo+ zsl-qI2;%qorPynGwgQ+_i39VlB{Kle^%P~R_Nve8LE^|Z?~_*F+`7f7bkT+J8W-%kY!7J$!B~Wq6!Xna(itzwuDlxlkXV1y@BM!H5u-8GsbL6vVN-t1 zd6$Ti_?^y4n10+wi~~cvxJR}nHLej<2t21aF)NA3J^>*MPB#$0(J) zA?AhW*u~e}rUm4!NyL0R{<=C`o$b$cV(at7z&BrI<5Gpk#mAOOC2O#UY&Ykn{sYWx zS0)wL6#|!z6{PUJstir%e{&(Rjw5`~+U3vgDC;?Gr>(^1#hd&=vFLAM$XT<&E5^;& z(r##AH0J6B09sPxD?P1+O<|)lteN1xENerT zT9x2y9Kt7lBp0)CT&RhDa)x*fG*M0Fj@|d~2_^Q4vFI8{UxSniPVs@o@nsN8X)>LE z!N+Djwh2c2>iiddle>{_Noxc+bSO^vq-XxvA9Mc5*#9|@>C+V%<2qfv2*jV4uAX1l z9<3vw33lt=-Md_MJ5C>a`eA?NgH~tdfLeKM5@cINZ4QdX3dN?s%Bf7Q@+3zU)+(f@c<{fQG&?KrGWYga*>m&28&EM;rmCQbRh^Y= zr7g>xfC47P$!M*2CxRdKz4C0d)>ZDd&?5b6N~MX{ZK@yflSWHdNcC^o>XdUzoCx3t z2kh`?rC+paL*<%RiHTCZ42dVRs!K<$ize)ldk0Pk9IgrmfvFM$ZX$lz>g4^gar=R;rZu1r zI?PK~W;ryc6O8?EIGi8-`c$yW$?@i&acx-SNe;LRLr- zMkB=9wANkmty~QbL>T%s#$|KDl%F~YQcik zZCL4;KX5}1QvIYhhSvryTw+2lP)qDqgCsql)d9CZQv9<&{NWFIiY31%H7UTW{GLZX zs;|@gh(P&M%KK1vt>Zh2K%GcF$!z=AAL#^82VvQUj)0$k@TC4l;_#rqCeQM~U4toE z@uaS9ljJW7`pbsf7Rc=QRYZuE{OY7mu6)tNKV#lBf9$Uauk9+i{@<3OM^+!K{CApn%O>m^lS{~b5%?mRecGADB~ z>7QO6RAmMKMnu%Q9|6=~)u`1g@u}uyh9C8>9E>*~X9U_(kLP~-`Kz#Suk1F2{WP{n zPkgWavM(E)+J5Y+&Gq&bFnw!fY$H~@!AoYg#cqOzvvncb68)gR+|UBH`FnU;b7+C# zEb~&*GlZryN3j|T--x4*@CH_esT^FGMw!MA?TKyqlVOAVtqiF|ySb!Gx@EJ~b4;YR zwv7|Hy0u?=l-IcWIwi&`&o&Tq{dJ157wg?hAZWG!h0->C{2z7(39yr^uQhg8Fm??w z0(|AQIFqo$I;!ohqD99UxAYHxRcHIH5?jzuCUpcr%IXM;rdzvSVAq&arY%*xhz@#? zjU)u)aePw(vD9DisvYE_KCe;XUt3Sik^1Hv`GrOPdg}Ad%Gho5)!Ik7jtTV-x%vzr zT1(Xgh@N83t5$vQ$0*~PzTNRwbn-@x)+Q6v7#q6=fsrEntGu3raLc0-JGfMzee=K0 zrU`sdTxgU2atp1tm$GnLZ)NL}T(R$-7U?9a&)`%a3bGf=9O*8=4Q=!|8I4;CTC%$DIxzxjV%MrG zylZc<_~-xnumAeLF)~rpr+Hws>vRPpaD4&b3U*4mq)D{NfBfSg|GI4@;}!r3+0KjVqA7Xu`}Rs79XwfFv`C>2(e$X%ors|9 znH^6oT2}P?_mWocbK;~c4j7^N_3Jp%(po1qEKYF(6?$L2>h}%1S=q$}Os4tmsg9%l zghUHrLfu`yVu2s2(#NZgNN{WHINUng1W1-@jxb4i$P_n% zZQnZJK;T>|4qYKPoG|+BEM1=zaG#`J(dM*f#7>A`+iPv?0M0<5oi(-gTVtx7ig}x| z>?oHG#>s4_o}dVKNsd15WmIC0*6}F1a`BLpKb)|?jUUn_-KTQd6V9IjGHj9+=Z4*r z*-y0Y+cOHZ2Jz$lnfbdN1>D#CNsM1>8*O^^j30c~3-%+9bxi5y6E2e2m>4=JMgy2W zwnJioRl~KM$}6^(osDAiZBDE+_5R>02akV8sp7$LB46jj{7eBbc!AuI`*B1Rp~%;M z!@*9h$8nFXjt}tWH1|Jz#$omKDeZMXX6dnx93Q2QxZZ1r`_|O&TDtFajFZiH>uQ?uxQNr&$hghQth!;XgTNQUO-nCS|De8pt>>hSKRCNYlvu}76*V={^Q-`22pFV)wPZ_Rx2PG6~@UEdv?~qf^Es|W^C&50c^bh zz_}RbQz+r$p|?F1+MMJzfdAIj#bjXs@>!WQ@^!ij5$L}ZuYxz%J{(0r6YA!}Pe1*I z0Z;bbyx{FC?{gF*o~)2-f>%2t`97};wF$glC#7i8N#bae8hWrDdGb*RAr*IDI7tZ%2QHF( zDq;|l?%DDS6@1)GBBI6JQZp_MX;0P2A?jnrDlVXC%0)jrFM@*)(Ca6wOTBs0Q{yz@ zYLC~kFu}a9W}52Wk5-XaPEVT`>nzO-e5Hy5S(PWZ#apXOV2vo7DP*_#`5^b&`wPO5DND@ILfX_#0pQE zoGh$XT<~gtfSc=Gww2zhAdi)!B+4^|`ti8hI6yyS+mvrG*b|0w0z?yju8V$2K8Cq2 zCY1A2`W9cJ)hjE_)071(TE~m#{!1Zxi!(7_!`-vz0VW)fl1$ggtv+(cd#Fze)ILLRqmTO|C|`E`0n5T<-w*__G zgqWo0VcArU0or8S$nvj^_!xc~&j!d4b8llhLsrxjs*#OhqdDOCsB*MN`sJC+q6VK0fRtSXrJvg70fToscq(A-IwCC&_VMim$+9(|E>&A4ux1u%Lq$eupNobq#f}DQkW>0AE74 zl!d*pMw!%Pq8i`kgQF7EW7hGak)uO(ZxD7(L8DdO#ir!^c8v5HsN~>l4pB~*LtBI* z%H>;_@IU+zyO{9UIGr{s=H!;zcx3cp7fhyr%6N=(=clZ2EQddhXHt5QGq`#iR%^1iX0+@(C?T0W z*Kc52-E3tu#dGPzgeGiJ;>+ZMxcmlJnngewLG}Rj$Y#vKK+~z%$fp|d^!;~!@{^zZ z!C(I6U;ZEN6pr-|dXC(Lf1N%;1Zuy31oPJ>cOsyE(G@jv_F!?r=9Az5+0Xtjn_fSP z`Gtl)si~2es7!obJrl|9=PCD=u~_Ng%%V{5$?~mpxlnKq<|npA7Kd2)o<6bHk(j+Y zo8jHQbGL0-0{8seX9LE4P^%EOGTV(FJ$&dmSZsCGqQrs&A0WPJn^V~8*R9HZNN!^L zg~Xd1pX7E_YP9cLd+iJ5Z3=wQv+TfeksRd=;25aZIPZ=|C{W=zXzHn~fqUuq9!FH! zw*BBiY@s9`I^dv)?T=cF*}_&|^VmE@CH?2gwt?I0hKX<~no|^qC)=)wer~lrum3?DGbC`t(WO z^GU22XWb?voOssph#+Iqx4~H%JX_?{A8g;%_Eg%#^A!N0og`e$&G6^Gz3%S8rw<&% zEjeGk^(q(C_Q~VuchXRD zr74o+>D=cQa;BsAXvjG)RWLT{ws2yp`70f@AD8jLOva7zw0sRL(gckLKKVjMhh8L!^>;)`GTm~G?3$ZEf6xDi?z zkMMhwYdJIqpK`^9jxHBiwMdeY-zi(3hqrI!7+?-?@?W0;z%gZ&x`hztbz!uNCh>}~ zer)T=7-P{^bh_`!zGpG7+rk;=ij(Zu_)&Iy8Gsq0T;*M9J8Qs-OF^;{N?>s)kEl3vA# z5X)orr0TyMj$ZpQX=dj+Z;6RNI&2VQ0Kc%V)76YXPCIvwvVVT}CqMZ^n~~+mKm0tnX?ii?H%T<*p|Gta1z!z= zu65?HG>9H``=%hK>x+tT13z(Ojqft^7YlsX>8zZ$S{b>Qp-D-A|RhmI@^Vy;m3Xn*vB9}EQ4k%78C z&5`2AfUAJ9EVWPv5mQfHw8|eftZHiiy5)>!I6lVgFTncOG`Zb45fHZVvVg3{XyrNG z2tzgtl>ad~6(F;xv)Z_{tSES!Q#fepL{Uh4TY~K!RN9&yqPBPrEQRF%+h28nhLF{!*9ISWxUA4ZXt zpB}3^;7w3GbPzMeZKZQ)KmAaA@47R7uoZ{7{VC_Ri;k`@1=3d_g27SSM6G-QqaDnl zNXoOCSsCwUOYO&Onl@Z_K8K#mdh?6Yjt^P!$-7*5ubB6kjb3eP*y(O;*n5OZF1KM0Bme3T_Cf5q4+>~NRpT@F+GvvbcE>rm!h~|S!E6P)Y z@Ux9XyF8egFF9|7*%9A8!Ki)0Cd@=v9n7z?NQhw|1CwG3KnHUo>W@TFtSh64Y+9GoX(W#Fl@p4_(q^%~*Jx($heQ-t3xt zX=NicNya^Bjlp2Cgq<2hc&ZDRzDyoA7hnSwd{$BE2vw|<2qS*v#ZLh(Z+^gb-@Jtl zA--~FxB+m9l}NVLeM${6C-F1vtKJM)0o_d{b*miVkrlsUv9N9>5@V{kS{_kRbG~h#SGJ|ka}QqFZ+Zc?ez7t!#dGN3J*S)>%REU& z_4ky!ZPUekJxau9Zhvn`m+bAQIW)o&s%e^i(BrxwTN%1r20DL?5s7B znBtRTE`316$DRZmt`WLuAyJe)&*fBiq40YT$cs?b8?$VG^RX*c{|~NYyX8;yZ7fII z$M#AS{Ssw0B^o})4n+60@}y^xclTpRX1i)zy3XF$?hmr|k%}rKONeaib-$j?OQYrrtxeG8+ws)KcHtCd<6mM zg5>ur;(#lUg>WUVg9kW#b-Pw*@iCgW!-Q*l+?^x{i+;5u_?_Uc*cMVDYFvZBMoUpYMS z)sKuLe4)-(TE)z7_psL`tDKe6ttL1!<5sBiSdKF@J3gAkkGbB%Fx9l_S&)wk8RTO51#r4-rn|o+Te4~FzBsM zDYpmkv~l4$!;)yg*dN^;A4*Ir_1J+#DiAZc=-Qs9_@V(nwgs`a5o(e1j8@PC4 zyEr`6-JbwI3HkU*{jOSk%kd>*6vJw}4cVpLrs9u%jamHYZIvgHv(3DpPuh0*Z@;lV z=-ZqtdTf8?^fLXTEx=G&Cp1Sp3f~N8YmJikVGcUBxySwOG(Ord+)9^tHTSj82|SM_ zUd>4ryMCL5=_M|8j=L@UVl2{zxa}g=f?w8K0MxOjkGWOfDiBX855L`fei`NB*I4ww}q!v`l^x7mi0HV$5wNuG1;;6(b#Y>{wms ztD^i6Pq(S4zAq0a2g1S%KH)X~D*5(zb)PL?e9cE+s; z3Tm_ujI^d1!w#=&wzofk8T1#a82@kl_7lZ|bMSh+mVHclkGaXX)lhwk{^6MAV>x_- z=k_O8@i^9Ldo#s1Pyha({^_6oFGj*)PUQILqzlP~v{A3qM~Ohcnfjxczc#uP0j-GJ z{N_!6{og!`n9x4)3j>wjznp9a3(BDp+4M1IT%D2j~9ZPNaQV_-(7n{Tx zWM3aAK>$KeuBlNu_(mW9nq0x+m-NskkE6R{687*ruN&U|H#(-1-L*hSMd3So)bf;( zAa3}lpUlbz3X!jIkrDYAnC`RJ%_|N9-o_Mgb0$>vvCy|4T~fTrX%5)Xr^CwvSV0M!;v`T%hP*#%pe5p>AIDM0d!>3pcJsz7`*Ed&en)zs zl3L3GN}ciiL2+RCWRl2yA}K!kUd)kM$uS;c9EBF@I!Mp>cJHlWI6V}u#>m~J9r9gm zN>c5Qu*i3e{jSgAso$ZYX==RXR~R+z*`+7vqLkKP%lp7613;;A8rC$l#w z-8&I%!Ijyne4=fyZ?ZfXtp8rKy;wn)Y^po`g*i9Gi0GguXPnGbg5SJ zXuq7OoUvTnJ95Sv@mKP)s;^@_cw2~xeblZhBiU81@gbEFqgcN+zw)uMX*W|39@5jttkkTpl$pZ z12oP(*Nu)$_(S|R&+!zX(4B~f3M*Wvs}+HMsq$+1b?wa<0iQ&_^;^I7bDQM-DKOKVoEL~o=iR(W zXy%TtL=9d|wcel3D+kHr!y+}LS~m#QN{W?ykQgegiCPzx4*;*EMhA~vEu47QQga{* ziYvZ1mVqVXZQ=%Qn6x1X+2W29UUtAa6fyH{-~W)Pe91`{c|9bCt@sg9&Y5$(iLqXL zv&w|O(QwN8xLg2B-y63B#9ic}Z)i7i2ZJa)@BR<6#e&9YA)PGx*fx~A;Aq(yMb}jG z+88d|6^w$^5N`Y|xI+6H=CTtF+rV302b9{Db2Y5T%z=@KwKp~=*rwY0@LWezgRN*~ znCS=KMtvt_Nc->#0%qOx6l-4>lXg~YhW_hjPnYCaM^;y4Te+hYs}$XfJuzCQ1=-uX z1fNI5hp>n8`Z-^|BE7v*1H;-pxAVM^O9^=-g->VU0f(0ve3ve(rQ zAA#!~03SZ)=US{2$IFi&KK#SGcjF1EzAX%@Ciy!~D=kd?1%j>kN~o`9_NFh}ru3JM zw%DW+{yHH2{DqFKrfnUN|BhwhPa)-5jcmV>$Z7o^M21Sr8#dJQqtXsyGHPY&;VZda zQMGtD#w7d+=%%lFLD_AiF@4Ze;xWw?RP?ulLl}nD{$heckDaS1SVzrYvW_CRJ^{q< zWZI$l;0xOTBSl+vr5PSL5{o`h@g}(yqi;1kyL_c^p-_a1jr!LRZEM6PuRwb93KV^z zu~&6rIo@KSE7g&^XU^kxl=QS*&89t!%g{-il+@1940XKX?mG<<{*tZH2rL57H4@`m zlck`4`)xjP*gB))h>QMm7QXVAu4tE!om4|ve>q^T$xCU z(5<=9x$6?+i}8$$%&)o)xyO~YDt`V*%j}z5O(MFy?tMrpehlbM6R~EdCIiQs8g*1e z3wrO&G)J1;vuDp6_KrOrU26z_zp$}MLYL+c_1Y%tN{kcryJ$7nI3m|t5C4uGb2XQ^ z4aO7gx3)_LCw%;STnuf0!uMB%85oSe<}D<%(FnZp_SE&4`9^z)$0lWvXkWc;MNh~F z@BW!yl(fGF8olX5`69V$)YJzf>RMIAmT3kP7QD3SCv-Fcyuc zV_C&iztGK^b@y(+6(`!1b|>4Ch3s$UNIrJjI?0%|epC`2sDCh;a-|@6J^yoSL;j{iZiJ0LElTMp*=Wm%4f@4|9z%5;)ctP=Tbc_>jmX?}S!q1t;rk z?hE&~F|9xBmLJ`+JGa;SGt9C>nIKcsM_Q<_jSF#|f6xeAUjX=^;XY|Brcj?ann2{Y z?DaB#Cr;!zk?Hq8`!r)(NqWbOLkz=uF+;V$gL&l(A*D#YWOBKy7=|BN|u~7p1?VU?ZJ7FpshOL#7#~fbs{KD z=m`Ct(qv2_Cz7xDfouN)m*1xQ?kDkDEbQ_F?A06FI+-YFJ$2pCq6_f7BO;Eq-pj&8 zZm+-5k5Pl*tDmTvTlAm$j(k2@4$r&ZbqUnqn8P@M;dMxLM1Ejc9+I zywq5z96uB~mQr}j36xGYM6KQ_Hr#u7=XNfKCd&PH!oB*&`tN4nf-0G?clw)H*4H&w z3Fd_k?i|RU+YcUG$9ug-5Iix`{HpTuFa?5 z@<*A7LoS~>aU07m2DJBc>W*^iu%__ZRQ*Wtd7=}t&HR;awVzzH)OSI~58_J`w{2-n zVlLDR0CjGh(yJB-N1b^S&*dOqY}dAVVvGT681G*?&yrd%3^_E_!H|$?7tXBC>$9|N zmo*IEUB>jPeaLZQuJI(DuHW>{YZ~}DAzN|0LREbo@v|RMrcgE*$1%^MWt4BkXaRNs#6NfQ5BjXceMb8sJxj!4I3I%*rG1WMx>D9ONPxL2-(ck-S z60@~gCOZ{2T-7KM88iQ_}N?a~}?Km4#&>{K=Qy|GK1 zE2x#X)Ea7F#7{m@XZ-mEV=5(KR~JS`9dBG-I#_>v?ruzv1a1#|^vGTlo#f~C9HKXN zk(TLG`$jHE`u@G%Pu!QUamkpF56Ek6kz5Z@qjVBR8aWE+t;Kl=5*`m4YCe~l~$$oULvt$v+8as>Kn_mLgBHs2zkNdrL> z$8($9++Sm9nn$%-%K;uFVEY2P)zY-i4iq(Bto)gGx-JLZlL8mLkSHnw!&*`?3ptai z)L5jdz0v}L;Z;uJ(w@{8SdaoE-Q-<&AhaNMe4s+rMq$939;$)wIkW>gcV?fDbWOHy zds6O|GsRQ1s+-DEL{6;YlIZLey+y=5ck|%Z72WklYCZwiN%vL(~0GvTf%fyn=Avu<13grb6xbK zd`||tK9U!9&c#2KbHW=Z(1uhOX5Q$=LO(x_l_@B2G2qptouP1arRYmp#gd`3n~r!AYHo52lEC?Xx*D}nI%|$9)Xpm3 z(o|J_x@<<|>PmjZD=-xuexA$*MROqz3})3pkR=`n z$ZY_17eM1(7p_(Wu6F=jt^SZVOgXJGUbd&4e_|8vpY-kdHt*I`PvSj~E84MK`&OYz z7Ck<{7Yk?C(w*_E@r9wken8S9U)WycN+CGXh&gD5TJKUW*-B9mK)F$^C_64;Z3-^m z*iviZ0wyPmANGFYrZ_mSoIJRzRlduH-l+2(ZIsr~KVuxn!;R7h>ZM54Qc&WrAyHQR znww5`=NjHc556R;d<|uLcK0YA#B{zLA**iwQj1N70{uizee|Q-tg@@}+}9%}_1KA$ zKe!KOI<@l6*~Jn4)j&GkSFe~p6uA05O_EX$xMF)9UD`fQ-|DaT0~~WW;6_7_#sL-- zhZw*4*FN<6!?WDcHnuc22Y*Vt(dQ{$lx~@>2Yq+RkMl;Y&6AGI`KyGI+DaW?+Fwwb zd|=_J4{BQ_Cw((d^tZ|7g1q&*h*hEahTV;Aqxqn&*fYxLi8V5=$5*|Wmp?02)xC+>7vkP-{Ux90)PSX(Y8x^tlPh`OzZQ0c0PT`?BL(5-%>$10`?4~1BxeXSw3X0G{zX;ZnUqv3$zfZ^p*k8}E;nVS}g zb{G1QoocJ=j)o|r$ZO4Py2K3V;Wtp$U)gFpj9$wqakV?Tg(Z*j4p4efI3;g~i7B3) z9lHE`P|IJ-$K74>qsvCAid9UbTLHT*mUe7xyryiVwA!W+gqf@%?`NIFsp;*puD%ua zwh@DL&Y=`wtVoY?>8j(X@!XN>jnI4fgv~1Yq~Fiw{QM%?*=(6g+a1RRfd9@u%!K)4 zBu|qOk+1U)8G*7?eMqy{wss<*$->8te)h9}`u5Em|9vjgwmcYgZwjFW zp5+hA>(SUY$@U`_s_h~iGu5OvY0nH>g7kxhdq3;e(v6Zc2ppv9fojk0T2dlYd@F8-Ycv#K0T|O~%3b|x_HA)u1#gFY_)dIupkK-N z{#kM9VfA0|v88x(0E~lRZE?h1aYWvqYoP8hH5x}~YzI-N=Y8;4wU~bTfY<0o`s%B% z4I8%}FxRwI82NgSXWgRA7R*=PhoR%R_t%&z^NHQQqivI{9lXj)>3)56ojq5g{g@XZ z^E!T?KIK(d^L?n3z!k5m=S%l?k6FX4CSEOf(7IIho{~GouNJtaN#l>Pv)4~m=BQk4 z*D^BHZ6F^n0PvNVz`|F$`0p_%29F*+Y8u%ltrJ(@)~v-O5w&f_<0ii>lr~iP71q~K zv#GVEVUmV#&ciR)%czQVxA`*_SPOKU1oNz}6RyAEo3%}$Li|4eyb#Ru1^WHVqgY)d zdCjc0-xJqg*=q`OV4Um1pqy8{>bOpuzW(~_fj?HN_i5GukIM0e`|`C05o3)3^e{fu zAK%z(C$$~Fg&zfM?b8gVrsqs?R7Z zjb@@Y&vCzPVpeVnx@;?jTG>sRxCk*~TCElAw!!l1P>#cGT%{iQT=ODc9jcJ-r>N)Z z1K$j^g0yUM5HYk1wiGu4jWB|&-T%{{{n?-W>&E8Z1P=C`#PH#24q$z(c%43G1Y$;h zOb@PYD+0{D-Os&ycXRu{AU206u2Rm&6YP!rJS$U;TiDYTbUP`j;FV7woHje8dghx3 ziN9gE=i~L<-k63ig*|MOYS>0e8)H3c@~iOd02MaGBUgD=KG%yLFB;#^ZUX21=-Ug| zapQHs0T?I9?_!DA#?QEQ47R$#b!;@h*SGlITXY+?PK}w?yp6JT zu%WIwqXJ=suXT{M6`at#@v&#Reau*%j){(pbWtX$UUiEWi1-$NmJxR5S6tO|RIWOz z{7gTk1AL85#aCUmTrsOMSmBkgaL`Wi_vH4y9wqkLCfkSn{lOYW#3yGQ)m%I`^!BYB zTN5??uv_tF8tt2{&-GCTiHyoIu!*&~qc$s-ZRVQMy1LMxQ!mWH?0}|ww{SMLv}ae@ zISlE^*qkG>!BnS-Nemfay(~K$;einA`an9X;&wo)8RWQCU89`*N;c|lsw!%ES72bs znStn)X1^7-7*U$t6W9~%2t`wd_EQq_AdwTij{J^Xl<1rzTdsFh-fFEd=^f}p<;YSf z8R3)SUL9aVrDHawb8&;P0@oJ+K2)SH zw^Jw0O?#*i+iEi9%p4ZJ?QGprOx(1(V!oDuAd5BU@<6%oM4-~nO2m1KTso9rqC(L+ zM+R(3(Z))wW#Xl}@(>d-sW2AMz(malX+PALEW6u@I zk4fuHR$Y0(HFAn@0n}y`#D3A*?>^Qz)UqU{Shm1;oQz;OWPK#k@`cNd=LIfCPH97! z_)|6D7+M>{!t!!AoqQf^@db8NeAb*TZ6b zAMrLg2d^a#Sg9uD_#QRbc-<0Rrk{6JPkPz)xNxVfOS8p9g@Qq(lPGQF21=_GY@;39 z7+|eZ^i*+qIN(I=d1H&@gbh{PMV?r)OT@OCKL3s1_>G?%d1r=f1+FO{Cj!^c0Qfj@ zKeAdU4Q0QI_Waqie=3BSnejJ6e)+^n&CYB!vOn?N*?BLSm8tTo6rRdX;y%Q$7TfAD z?xK#`eyz4-ZBhFaws*0OSc&a&ai7?cRiVlC6y+Gkv_nsJg&VOs)r#1X4VDuLURkIu z&$Lfl_;wFQs6R;llznZXH2>J1zMrZxV!+AR8{6{8tp}9C3!X5|Nswj2 z_2#SDH{I%Y^grXt!PUXEdX%x1Q880r36DQY%)O@D?0U*P#&3`uCn0kDNgPl9SE4Uw zd*Z$CTyd=Zg$?MH*4`3f^!_Fjkc&i{Xq)1lZKO>xW)?DgBspUIhYub$dB(rSEZe@> zDy{V8S2lMuG4^;~GcDrVQ_;DpS6&97`dqYheW|v-p@Cabd|Wo81~Pr9tciKZtFnj{ zcD!r=Wc5^Wc+7FlW!mj;$v~0u^h&_0cP=xZm|tqkgHIo{TiBRGvIYNMu9xVbO_E{z z_;Y&$Pqd*1YPM$exuFY)NVJvX4)a>cx6V_^u|qrAX~Vy!+g$M1?4}TLsBsxRw3-u6 zIoA2Z9N+Y|JdO3(rOZEWC37E*%`vL^YIWTv<&w0=K9?p$)E_-GygB3(e^nVg{8oRW z-;cWqW*oR*ribyQE&Jg7?Ft^FI99sv)kPXRdj_s34 zMiR*FVtOqb`ur*R#)1*yq2{u<;|`{GzzWBIM!LgDsIp1A0K zesB*TK4>O67ibk3wVZRoH+hVi$4T?V`joZL9z5rg&|M!jtJGs?lE^mp{MH$NQh&Um zi1VV^CSniF4sU=_((`S=er0Q}{Bl{Zh!PTG&DIw4U1OQ}=$1dwGj^@va%xD--(iho zTs@caF{;5ni%j+EuZ;tCEdSs3VT#NjpgEJQbe%q01o}w+Xf9kE-ibh)G~qE9?wQt>cDOud_nW0Tn8%RqZ9G%vBC7(|1BLYJpfTauD{g- zGy0^P?os?ZdR^kjq_2 zldrmUlC+Y+e?GM5mc1iciIdrQ;GZaw*Y=4MZB*}zI_BQ|B*~@_Gf%#YzshT05BwIt zpsJs|MqJ^4z*McNot@VEDO`$+bK9OzhpX>uOX=$}J;xTxAc|dw6}%iUt=Z6O>?zj^ z6isRB+syK!bHGBcDFab$4X*&3@Zdz^@{pQ<%F$$a1kGiLQN~B(z{d>=;8~*Wet)~w z;pyaCm**UG;;W8DpGSG@sO_WdKUC3WWsiCKkXc)I!n4UJ#Ejd-Ijmxr9~V_8^AEc6 zraJo-{}eBjzp)DdwT(_g2ZiQ*k16Cx)v9Rpp9~po4e4M?hOcXe%TKZJ5n{Rq-J_OL zTN_=<8dR~U{fT)WFV@%1H#MusV6??fe2BQ9I(c!ah6kwE_3BUvmI^!w_U-2?<@nkd zM}_K#;b^Ol!+xx`{*|rTet4^RsY$JE3)>B|;6a-C;ZLWk6me3`e7CtQVZ5H#C$Ag4 zOLPYs%?mL6t9DNKH*Eh_lY;LAgdXYSoG_T#$`aISpP4`+SQF7Xbb%`w%95Bujl?Sg7m# zJC8s=i~P<_T^l$f0-7j8{)5Ml|F+3Ov()E}FOsYp&fyE5<#7}OY^mcNW|W$Du7eN1 zF2Nou60@#&qFtP;HCw^=?r$;Su5V0lEuPmz4uO+0Rh1Xpy-&CH2AKE#9)<&Fj+nGT zCUaG)mP5agLRYJt0Ggf^tHR!~j89v?E>+r{bX}=1%JxkmX@h1pPI z<@nXg2l~M1Q(;H4Jt|cq%16A0)(MF6#;}5D=L`~ItNaHnAH@ha6pK^1q0O|(1yYJk zpN-PAkWko77uF?L`9K_}0+mGqqy|*BbSZ~d%M~jge0|1Z*hrptr zGYEaNoglrp?=-P-u+c4e{Ok1yfja;Z=PUC}&+2g?)X>jXDr=2YyZSjc*fE;_fPp8z z%e%y&E8NOR3yX?iT=1<}q%KLKsFEnxx^OKQ^CYlKl-M*P1+~ZW`h$4DoLkv*NA4Cq zG`Yr4W#t9>-4st4g54k#7Hz<>0n=-+#?*k2d&Nt9byAmG(K>gx)Wp1RTW!QUx$`LX znxG>^HRV+TTBoP{h9JEwaPam-L|0xkmEt~kpHswlivAm7!$ad`S{ph=?GKDx0MM+_ z2vdDy@tW{)BA`n>AIJK&RYhQ)7^-`I>-Nn*5U{V-aaG?ZaGQO=TcwM5sq;X*d}Z6) zZQG+K9VnwVeESv3{3{6u)OLk@Ky53!N(x6?I`P1_qvn-wPEzVqXse80QMIe8tJDwX z{3Ym>ZBdRk*Q?yv8j=$>)B!?;t#oQ5`uf$-@hP-~te!nS40x$ZtWtVcKddk;G zVkMzx{`wVFabif-k6tE5+74M`L|JVhrfQQUYn?Yd2+Loa)egmKaKpyn#x(;---w0Y z#}$5P%lV9}b^iLNtMAaiwJm!}b-O*fb=w7V#bq*V#D#d0T04@b$o5!>Vqi7S@X(2bD9YFZ{38C^cG*gZnDEru4D`Prp~HV?wk`a(cRN&m8|^rI@t% zv;u!2V&_a)VvI!}0I)quv*p_h33sGI)o!0cWt}2l5fZMTcZqY%r>rE0Vo3a#J2C08i%1o5j&pQBeVT8Y2@}>V8fGaojiqEe zc{6Ve?tBB|H{}!$Iu#K;7uV<{;weKxIq~h*3=aw{x7)~M+cju1iVv126>a>}%q7kQh}S2d@OGt;_U-XI&4@0Y zdwm2mKA!RRfpliiE>hT5QcS#VbaB8uw^%fVQY=B`t?^N)x*4xp1M2NvlgN$d&z=u& zhLDc-*MApiQnI}DC?US%FB?*KG$QC@G;uH6TaKDaFKjH8IW;1;wUEVlYO#!Q!<%}P z);$>))NbFgTlOSc>@r`iPyY$i!2y^LM$}zayiQjo0@oJ+u1ZgA{m`Vbi)puV`~L{? zxt9|#IJKgQt-Q@upNTAX-@eiXfKhL)`90ZoUl56f`t@tOkN8AlQLOh#abVX%X)EZB zIEcr|iGG-(a(#y{D}mhz*wzN|T+j z%4?K#0v`vu{zFx=^xCnOuG4?8)^2vzv2@&97C!{t2jI=9D!Ld@bY+pXy}e4uXm*EX zcj%H*QC-FGDPj}UILWSvv5f0n0BCWw`s%(*q|nR7E+v1h6?FS>pdU~Ego^$E!^f03 z1NWn3dEgANc=N{Y_ZAMBdcmXWt7UMj zd!vVTn1I?;WosFJcu}cY?h|3bOp^K0lFF8ILf7Y)-9yRq58z8@EjI<{7nr~FlgHn5 zk-v2^(ROkR9M`Y^;L5hr_E(>SrWpSKPfV&wiudz@L?!&<55 zm85I$H9I$UI*C3uZui*RtKyXS(c5BcMB-PPW|UZ)>hnq@8#!^bB%zFx(~{-o>8m|M zWTMj#_1PI;KjC$p^qX!`8iM)e)g+KImJLZ4)Ij@1gn=6sjzRbBb-|tt$TKF|vs@-K(ZBh#apEBliE|jA z%L{(k=nU3B)#zVe&ke+}RqG|^fb|bat|PxBTWKo$Z3t8h#F{JL#sA0bz02=y*>>Oc zJFlwqtSZMLwvi3DEXzb#L8298OGFC7bVG+Y0ohR^xYL)GsgUmIp$n*t-XK0^E>BU-`ew>-=1@h@g0vj z=3}kB_FQYN^4ZRyKPwE7tZdaJt6o$dC44@VXX3Cn=*J2vOf@1L*-T6nLY7bSwJ742 z;rW?PqiBHnQIBVCDAoB+9PRJP2OoU!bM_pQ|0j(2)Fm*7vq$S6n;;s4^Mc+)gFDw)xzU5m5$KJb83ZOQ%t ze3wO2s;(IbZ))y<_k-LlloBB+|J)8I*6rkaBW8q(b^Ho+OEl?UwXgc&=8WLDCL~#S zUS71RVh&v{KvyGO%9%ivRR>>WhO!J*K5)tI@I@Xv0HXBFHpOf7sy;VUKZV6jzmzs)bB(6Rh$dLCgHTZ6QkPU=dGK4^vYVx%_do%r1~25E zd^nKZvchJHT0!bo-iU`D_>%M7K2&!*1d+cwt|;4I;ylo;wle67`bWxj(pxT7rzyu@ zZL-Fq9|>u|IgL06lcPX(Zy1VV>!f=HTdnPo9kS#VL&?uV(>&BA5a$!sw#oswPeH}7 zJV||GJ2zw+pl8m@j|3%UEOg~YpTI-4S9)nwwOexmKJ?d-gRZP(!GU9~%l6WU7;A>| zx!lovCFDj~tJNv8CwK$;!-r3P$;9do09n$6Q2hqlFCw=Qfy)kn+lb=r>Zp^WmD>&g zYW|LP4EAY!hhGVkZpBx#}#9xIIGA;I;H{H)?C}3`e{RHd&LZj(K9gT`3|!gOKhVx zGWyg$*H!2l$tJ7V5jaT)qwTKY!u(j~y}jN?3Rd$~aqw!1S#zruN6$fOhy6E>t(J@j0sY0kEOxB$m z#RVHlCZxVFhEx@CFIM`FnOS5}m!w-A$D*OD4VAU)2xZXjxbx@{0*=vH&s#gZ4M(=swZl_aZdOlk2z*nj{|O{U|w-4>w(PXim~)w7wpHT%dha8OrK6pEvevrc(6* zo_3tbPah`&TTVTqUWh7=p%nFueA^Ea8pf~t7dwE|zGIWdod8we^7beC*b?oWMCDS? z4dZ?^pvP)QsFJG>L2sXS3!&Q8jZotJsE@0e=e{<-8K1rHbZgZZhU&}6iJHckTbABt zRKC+=eH>V<8V=nNhtWzC&urjkB@2lg3XQ#v(cF@0u1)P2^bVC~&;4H< z*nPW}p_y!TJ38AbwUvwQaxd&YrBz}Jl_n>6y|RgkTwg!-z_zXXMFmsBfwJBcRrNlx zR~E_cmuB-z+orsq)J#K^;y!+lW5PwAZGPI$$GxO%V^i`)$>MfS(!t~;wyEm}g^j#Z9sPbcBXR;)Zwmn@KdKDn>;p5z?IWVGy--_7p{0Bl9D4Zi&bxhk zo3;lcPMd0ul5^V$mh{M8*QR+$Tl2MD!OqX8Po5@@3rV`qkolUoJjj_Q#Xa@9pBx;% z-*!@a2h&ERKcIPp2IdJ5mRLnvvZ|NusK%-%A&i}KwxF}c_E8%z$gf_3idDAnYb!1| z*0|eBis9M%Y)-Le)OG5yy-Mq$t^I_LXFYuKNq>dp*?Mmoxa`Vn*x)uAtp&7K54*|AYRU}tac zN>=&!#azCG?|L;J&k50|I|l zq3`LlXTdkx<`x#mHE`21){K~hDEKX%jpn!1C0}OZmM(uCU*mDE)5lITWEr=t@OnAY zpvD4+jMiK(TyRUKlAbFu$fbfTI6P}!$l7Rct;ol(&L@Dp`!44ep@U})(8tu7TX`$q zWCyu!wN?fyuo0JpEzZFco^_@A$~+AJgzkB0EP6|^qsGz)KlMTMpTcNa`%%bnw z?TYGa{CV);K4bXr{@(BX-hX5oISJv9t6KcAQ1FMES(TA4=}kp|FPxY3_9IXaMJkiu zdGcr8`y~j(M%WrdbzY+PijER5sr-hXI#++;B_i11uL3#9`S^+9RJQq z4YrMb2y-o+f26LFQm;9)d^j5RSs~xoQ&jfivFVO0rP)Gt?evL_DJrp~N%gOonToQy zb@(tW6MU~&1Up@x9MdEc`giV;zmhF@3H)(UK3cLvc`7}V`=Y8NJm8jq_Wc&rNg#XCT0CC z^cZQdM@58|`w`sQtAA@Pw?RGjQx@c&>bv>5zy)qY+7QfYrPZ>tCc>_h!C0cFbefz;TYQ+1?Z=+OKqkb)?73ZNAS9JCE#D|0K9bM1gD*isBPCVutJ z)(&<6XrPl{(vMjLF1G;um~}{VCnt^ly3Ktwp85>heu03unVd~7v#uaXrwhkg;SMJm zQs`o4kj1f{FxdaqXWJauKE^X8I4JTAU}F(NcWRCjG2D^XT?*gV7X zO8Aggy_0^qLu9ri&D8B3n5#0COYRr2T0L!1_autyZ9Gf9lY434$=C5D-PNa!E_$-$ zZ8V=2Z9P&~TOpkC*=EvZ3*jWr=vD_?eL8_(v1c>Y=N+r3$&-@V?ohW1br-P@HHUh| zb(NLcTv?D#JGn)iQge#G)p(jxbyDu_oMbCdtOj0v!Ae~Ow63{xUAh+e%H#s(q$BK# zH}`MV3w}2a6*!e?n#Rw>T2yzeRA=1@1=&=jQ(0K(JtkH4i7c-4;x%kY=?Rg5#hQ2o z6>m^$V~BAUJo`+Lq3Za`MvVm@qf$7qq^PSL*o(^X5bpXGqNhptC>4{x9RJQcLqNeK zylYv;b^+HI(_6FYMHZT!sXkQ$G!duj#^KHR!3lxDPg3VkV7Kok20KG#YDeewQf`VXet_BFz^782I{I3tgyhGyC00JDrTUd^Q~ifn1Fw=!T?iPuEkw{1imeA#O0Tb`Sq zcWn!A_;<(U+3xM3Nnfp2Y@!pq+Qb$@e}btCwJrP2Fx$-P7E+gSUFBqGYLoOg75(X` zCWk(qtYepL#UxzySG37b=_DsLn^B2RK`U)27r>*3k8=C?SGHBQZl9|%MWm8jSzo5x z>+@PrhkLWi}LC2U13AXHO%OgB)vru}@c0St{ zOwt$ZRhHWmwB?!kz?NzfetyPV07w{xAmd(U zC1^x%U!0i76TOL9ygrgG*%CA2vi)qE_pPeN$4|E0Dls0^Z)~}~XX~3QG}C@-4=z|@ ztf&uyvi^K*w@GRHH!}6D4D*`1(x6blQ@ffAKEKmf)-+oemGmTMVMS;Ct>aDGvuB*} zuPdM1RqfLzwKbpAZI0A<`P`>>?LWhT@we*i#F@NwMw?W_vdY2nPbV$tpxgXntUR&X zd4&Jyqdty;xc*G7SpYXwmV&a&{K{lvtX0l4@(b2I=f|;8YZ~}!A>LRL6yh3j3d}Yl ztj}pN3iMGShRw#pR^x@ITa8uQth(}W8eP%{ua6Gva=XPqsfIYN6@eY`b{_Zjh=WEd;`xdg^6{E&%G z@G;c_;SehqXd4Ids%CCS5$~fP#q9-Ev3LpcRwi{M8!v9@48F!5wK~o@cJ*62?N+i+ zJ{k3N^_w6JEPP2q7NBNg9SlF>`4w6VIfm+32H3|=Q^xI6jJF|bYxQYoE}&n|(RSU2 z=(wUwF@jFH)xY_}Km31x(Z>CABXbJ?CsFk_05s^&ikI}JBhXf$H*NM}Zzlq*e8ZnT zG>)rik=g<%faYH;lmeZ|%OYAAvsvnhC#Y>fgTG?vr^hrEGM1l;0hYbX2)OM7DVjk z0*s%{7V)K+*0Ru)O>G`boKeAVwXr^*U{AD!fJjZvfwynR9GG}$DgAA)O0*Lj(jY@4P*{_- z$#;;@p{;!mLtg`@{p@s z<$951Xs=7HKcow8EmP-YFO&J{7aE$sGd6KnsN?MjS7mOyT8XiCQ4>fg}s zt?#QpPHqo;L&hN}J4|lD5IaC^S8DOBs%qb9J%W#~l~-k!Wx3$Yp|XaTwpXv?88{(- z$6jq3K2RT1wNS@S)rExWO`{X4thDM&i0a;9R@ua05yO-@RWQTod&v_>Gik<0%%y%~ zH}PWuyvCLCL{u{(Hd^g#SlHXhOQz-i+=eDUV>J(1=ku90EBlj9RgsrZ_2I(sbzx*y z$J*;MFei)|Xe=c}W7MB8p1HF>iEOcITDaI_7X!i;U&5tq7 zZ(_8<4ayHI<1x5FiXBVkHvgk86jfF3h(Qf%qo&R!m=vmIyF8<_QwC zxR1u6OfiUksyR76X6eFuUBy^{WuB9t9p5$%EL*TaLD+BBiqmKZtfi4tMAz3&IwuOVB71H3T?7#2@8o;j11rnys__=)U2L zC(oon4TDq57SeLi3BmYGvQ$*&R0dN2#7-R|+#q-JvvMa&*-#O2%HxEpsyRn$QgY*} zY*qW$Q6~98W*|!QfY?^(`a!Nh>O!;ujbo-rEsOHzdQzcEvQj+PMSm^14OkBk19vre zS2P^iqrVm>kCo!T;_kUOXp|92`N%EJ0ieGIQd7I^1Q?Wc)MiKxxa(Jn=8ccn*CvT} ze4;uS5Pv^c6cqc7iUv^pTvM&o`B&*OxuS_@`8C}Q^F{tuizn^Zz%*m?v1hKQPo_M+ ztZtPM7XJf`6DJ)>tz%&!H>jKWbA8m-+$f+@1lp^;wD#Zl1+tNy#>D{jN~dILTj(a> zIX^+8TS?ABmmQsxBB&$ADf?7sclAr@HG80@m7ySr@_|VOMq?%>3(4AuEeS<#$v=;1 zJ5Klklfkb)1Z$c4CB3BxTy_AwrNB3{^F&a+=D!FwrXyd@qO_czCdFw$Tn#m(fxfjZ zK&!-6Tb`g#Lf76FaR=;td}&)+^{W%x=x|zo)wR8zAMneAHdF5&G))grntl1DX)~qZ z{PYF`mi(#C-q!jnSYrlc`Kc@L_=fK!q!d%Ho3r=XmTU}u;i$iqz|U08nM`c+HKP6Z z?OF}^;!b-CJNoS3QlA2p!E3^@A-d}6gcZcAG%K_1|L)gueJ_7qaLh_c2=AzxG{;k( z^~(e88OF}JN;X=Jph1HU#iZcaUKnqrho+SO>0?W!B3idvhMg7+DQ(8@+P3mO7GDle$$pW$M{L&xuiZ)Qlai)OS=A@nsa!Iw z1%3NCZ>JdF=FwW=o={dD{j}X_)^{%#YmNAd5nB?!wbz1@?$#X+>U-zJP6++7#IHix z2UgCl`1Y$HV=wx!&MjGO%F^SYGge|(qOGT83o&S4FWcC=SA3$4+P>?!!lS;I*kZ3Z zd)sZk%--$EUWZPM>UM9ROLY#lvg0gfnJOlU=NJ4qVo!Zn(lLcPPsm=bcLFbRjVieh5=*>ihS zLZM+x@+xbtQzz*b8_L-)R@QM-ZGf+Y4(UOEwIK|CeEG$fS6|&_F0(D%&c6tFOKHn+ zlE;>xJH7l|uy~V$J7I74v0D`U7kJ~cYcO^$VZW`5MhEKiu-W1l|F7EHQQX(mY{;fp zq@g-gI+S_sc8$+&1?SA4vYNDlY=6Q zau!bxkj!Nkl;KNU!5$SynLe*MHfT-Tmg8V`q!%6wo-cY$trmND{u&wzmV(`FZ;#>) z$AJZBEU{>QZ5J$D+DJv(4+?sX8V6B-U$6x!e-{U=1+WQ{rGHl8%W-3c%~tUPqyp2_ z_^B!SBrxeVP9*phSLu`|p1&pvdTVnx0C7JlM)e>wqdEBMAWMwKabRfqIt!jt*ML!+ zydFBS^WrOOsOje-jb3Ak+C1U0@{#Xgl{kPa+&ou8uZF|)7xqjGrH#H8Yhs0*T;(L< zTl-S#T?t0Yogi(D58F)MPRtpHoCJ^r@5oxJK^+z1N2MI=37sxOY%SqSf6dk%d)~&E z%NWjjoYx(v4fS-WOk+TvO_6!R8vLJSDHz z!&1!ajgT|rWKr^nM{KcMFEWLu)1Nvy1BoZntbFU;X`L&?{ADyT49?ci(6Sv9J@MBp zFm^bOs2$7)w`cmfAjVqzDv5R}R`*P|#sn8xb)lC2+`Rx`o@q_PN6kBYVodo+G+%{@ z9#U&SVZGJmNUJhd_QnJ;dAGevlrLR%9H4bbuX4MW`J}nW#Vjt6#<7jIj<-y0x}r5r z&5}>O9!EWz3ys)&|5`l!3v9cHo(rHWFmw|L^(|OZzQ_Z&U_k25SY=1ofC)YHvyM?v zYW$f4-+Z$bI`V@t_WASv#W}2upBh8xr;NU87qqN``IfcM38IERe3!fhdb1XrsHgZ5 zBP!rEwtbDIvy0j&(rzvg#hS|+`@z!>66de9vvxw@B6U1hCoZgV9w~Lu!&ZFxugB5w z%vEHOf@Gbj1~f$Fp;z1a@y}mz*8sd`TG)6kX8eFfjGuqTZRVtx?UkT`^z#Bg*9)FQ z+DBX<63*t1-=5wPvXvAkbgGUconpwWWFf`9VEv?v8r10v#=&;35x)1_k>vlh}Q~!!~$gg5k4J<0{Y^d22 zk@`-$hV4D5)A3{F3ue_uL5e$!CyBWp@Dx-jSXeMBJ7}jqrR)^$8hm$RP3~T z^p%2&UGXbzscTkHL0+x^F=v|S)!L#AOn(dZbx5+P9NAk^y>=r9qFK>+_T1n;r`BRl zb;qiw4J(pzRM}rv(JQ-6z`fj3=wyAxd zxB9Nm10#DZg?6`55>$S}_=I>@(o3@3DZSKqlam8~sBHUgV#}E4$;*Q54KS`h_7PVwA;C&DN@G0&fVCpk%N7se?x*~Ad0r0vadp*6H2$~e0+b>;E1IT#iu}m(XUoqNmnYK`NPNp&S$jRReIIr{VICbR}6I@Dp5wz zW{QS(HNRTsPIs@0pfzCZ!|@jYN47>(T+!gRGe(tRWn}7D^egK|SF8qjgvIf)RpXji z1>Fd0-^PCHEAJqcQDEhY#W!c0B5aCBu5#K%yjH9nzvlH{Im&<-kl4wGDq4B9&8{_n zkgFB^QQX|;I=Ct@%Zg4CN+DXps3FbmD!`#Q$He_W`h)`g0*kWzRlaGKm&yl&Qw1~N z`k10iyK%(_48PNgr`**EzAHWhnuEsRE=xZhJ8pV zaqw6-Yc2Ebw{1?+UjM~dPAPooSJ`sbNZ%y|-Snv6s9Mkh+w?na@!|8SVX(Uw>(q^p z=3HHayFhi{$61 zPqikU4vQY^m}=Q}5lSBV*Zy7z+C(lnm+{!m(xE$d3Zj1p=L5Q%X>qo;O#V5cf)%V1BLkCg^y$T}5BlVHAm-1|F;i9V<9C1T zVJF*Rg4M2%<2FXHw1<)!bAI3RCoG%=e@H;@(rhm>z zC}Hr|KdXAuq*!Pbdx=qY75z( zdM5q1+L||kSfI2xGD-E?7#5xw&NNt`%!h?n@H5esVE3!MilEf*6$tJSz;Lx6UC`z>4FwhFjcQzvyC zaAH?-Z&T`S1>ZH`YML?2>Pgu1uVXtJhod|IHM4!e5831wBZFLXqgg5_^;RgR51&4z z4*f1k>~Yvem~@dnXB}(UXz1fd{Xk~ zB)IDq0rNecp%4d;r4LL3!x6tb97OGp=T0at`AQ`-pjSL$i|uj#3P~BZTr!2^xQ`60 z#Pa}rte8y_=1FWTeflI`Im8_KVZL%G_^q3Vd64#6A?9)5{SzU0%EgkhrNRBx(VAkw)##p_Ww*=i1QC=xWZF&c>ReY^#W0LXj&qFxdhpHW+rnr%Lom6a# zjc15F*Oup5tk`4S zGlV+4pX4D6J5i)+?7WO z2U@%J3ON|Wc%Bo2`s*`G+(VJY!P=qQ0iY>fl&>?cWe58`ejX{gib{|jAM*q%d7AdBR{_>iA`9aZ_7G%-c^`@d!YbOxM z_P`{>3lhzMNGJJDau$NZcAV68ib}#$50re;m-{`7;!9I#fZFt=?-dc{K#lf<3aZs) zSa~vpynZfKyOID-f+XD>vUV~C{7k~%#Qc)omTqzd7>nkqpDJUn*nk?d7MacE~D6iucUlR01K?oiHqqD00 zQr=UHk|Ums+;Wad{Z-YoD|uXNV?;xx${dx`#;kZH)Gz~ds5Bv?LEf?&16W~}V$z`v zbeHLrUV>q%5K1w4GoVnrTu2SFS($iAm!gRe`5a8*M(|iW*0sTI4P)h%Q`p&BJ74iA zOKqe~n$Bn5vuBz8v`I6W^@p-Dxv5^#+lavB0>Imddbe38fhI@K?_9+N0K<>B^H;G^ z8RyZ5b2H&bsL+3e2sJ|Rm6x(y7hT3wS1x+XOLKIhFd@b&_;%t2K5UN=cSq*(s0%^H z#GK?@P&6=sTiS4#^aOSvC!IY9TmQkS^0e(#0zc!rM^;kBV`kb;tK2n-s+)+{HlbWD zrJ?raFYB$a!40yNTg$VSUOx*pfTDL+Z609V#L%{ipi*1gc#34~ZD$`_jlxVKKRm79<1|tQHv1+7b z$ZOf{w2w+(m4Qiq&Z{0P<57i7?6E2!)NJbr8EOM2g!#mxUuKKSAabqBtIDs8g_}M0 zD~!=(+Tr%0I`CS~%1pG_^e?45Mv~kD*(Ok2qJ%jFIMLg}xwx(5KpEOFaIR5#HD8ck zYB1koEea>~r5MNNivhdNE)^H_t+c=ByJ7K^n>y6Ra<}g^oaAQV*r`DVs1Y`zE1IkA z$X=S{kJjkMYi3?mY4AyD);wK1Y9f$xA%+{^ z__KG=#osS}_`Uzxw)6Z;R$rWu986kc!9D6+wh8fCA5R9>);a3+zKAOfo1Cu+iCGC?*H$e<#tFT>C1e6p%qMxA)gW_5S(0)Qf|UDrX%oy zEr78#Jq&zdTcxC9B`VEw<1nyprT;qZ7SxWla;LH_U)u=hZLl?#Y%Q(ZerwF=E2aAU z0@28cm$tA{XG@pbj(zz+ztrtx$mwJ)`wRsn1;%o=5epa0_+*=_TO+w`N#hdP zhqlF7ZF+96=2~sxr9JPlC9{pLPd@|K)~*^u6gG?(^P_@+ z=hhClr@)JGlq={l40e&k%-2wwY}lK$tB#{{<*N+Ajyb&R=D^7Ascln#wRS61H4G?G zeNZ@6VjR5}^TUjBtaxEtsu8Ar;cGL9Gc7T$wdH81SXT`V~DYlZL_9WrW6dpZ%lw&W2xj-SgJE9iBqMy3f=8m2}Ih+^wyt zcH>PIA;od7-jdN};diNCYSxmFg@O(Y$2rY$V&vnzX;s;gt|Igh({2?$38R3#%e*`Yi0C@c|zK#X0eaKk(&YznnQz~P+RCF?8CaqYb2nXrd z0Af>iGijxc1A9*HX<`dQ4h}s^JnSzWr0BBK!5oAwh?1N0JGSis8oG;WnebdP-F~z@ zJ~3c_=~fo(B&1#ZfZf{V@kv2f-#)^Nk&>;eWG{Cs#k6sjs+%-l-Z?RHAFM1LoGNj0 zkD!VH-mQHPynyDB_U#@^KR-2r3tenwIAOir{a zCoDOaC)m*5YN(V^GvK-@G%t5aS`S=@@PkB!bA6xk8_inrM9a+$ zU%2v{eJ}@^^CaAjV${0MBa;9CKmbWZK~(vin-G*}tLAc`(NK~rEA>_snf7wgb&zdL zVxliiu|}{~gk@c*1*$JEo7l;SY~tvY2D=mQbyx9b<1~b&wqI zR(nZ2@-;OPux;S#I7hYG9dKC*nK;LC1>;z;hD!%}rCZ{vllQ)X+kMrTkj$&*Zl|l? zRTuuOf#$Dv0Mw~j%e#6njMp21%Pjz}H@@d+XCmkiyvp}%p8qqu3XC}?rXPQmQ>SF5 zJE$^eW!J990l(YmUt-Jpw9$HWl5@m!MctU07#ou{3I(@upb;Fui|Y(+O24kel7lcYG}V5GVC)XD3fYkW$DPpT}G1Of)TfwCk#2fBYwr+pe*%nqS(<1 z!jVeBl@C~m`WqO#0d=B&g@Aq?IO<*#z)lt;Xbv)|6HiwOnmZ zxTrFWa zmaE!p`B>_$aCh9J+iP=y)jxclMKx5MXO$y97*AD5v^ig3LicQ87QC%`kHpp@yv8@C zjze4A+85!JuTd+i#kUD{@8}cg@wbt>zLAZJH%d6=uki;6m4UGK_B1RI$Eu9m0K2#b zS@KWGu9}jHvd|ltBnnySd@pt05n>W5)h^XbjonozenU#3C}hWoS|=M>N+ zZOR9^C2>GZm94-)87haq55fB{HSLTi zuE28o5vh8eU&4(@8^znR;~E6A%B(Z>!84|QHK9xjz4p}I7kXN4*bVOXvwMxKtaY4U zATZ^AyNCHzFBkx)pTBw;wrJm=BAn^UDABgbOgtqv8qKHL==zX3CrvuBp8?MdYKnm)#IZ2NU>HjeZlUgmUtvNKw4C%Gl} zk3$PJE=t8(KrBc>jbBW7Lbb=psNrQ`7&rVuBYts<5;U4GO+4}WU0eGexDMJ|w|hSS z%=)Q8V_t`D=DThwaGTyi!DAa{IO3LrC?(BPi1Dh`P+r6vzGv^g`%d%sj^A!m*V2yK zFX4B+HDYfb;cbt~LSF@VqmKm`Z#>OC5>Ni_YY=Y^2|o@kE}VEfB<~+{6>rt_>WS!k zt>J0fKITlAt?70Atu@AY)tK|+A5OG!d*9}~dW*Mj?Dgs3V;S?%w=%SBYynUD*X=jp z<&W#FgSLo*t6SJvXTsl@uNuEtde@%7uf!A808-t&s+3p#u=N~|3!k3&uenW$a=6Rn&IDX*{o>!(OIcI-54fJ?-b_?@hb7_f3bkQiwej#8?1FCFJf;6bO-`B1rsl?y|#IAN+|2mYyqZ?z9s zcv+qE4HiI65`d<1%jv&Ie8IEk-~(e>$xj4bU=kAG$1yio@itTICiND%T9cSo_|i_; z)vrD-+xR0c_CHe@mfp`pU&CzRm>Xf43rT(arK%_thkPcW!_{E77z`d5n@si=9=;)uQ{~jHKI=k@5MN7T1UE+ zuPx0fe8^)~D$7E%Vp)=+P~+M`loD7G&wv}&tKRceAhAtBUGqBH`PM`= zd#|;U=;4-T@;xbd6{oVaRk@j0L^sw3F=BDE@~Ymrce6-5n9tCivgwolkW?iA-0wa}Qd-fyf`w`+L= z9EaV4?IaziGCvCD3441sZcIP0cfdMn&cA3HDst0=vJ(nDEIXJTvg=HqDWIJ7>ZKFa z1C`MQmfWJO6xTMY+5|6rD!eMTb-}6SDsm~mp$PP!bKj7`iC; zWP&S=`jR^MJ2il-wt7$u@@g+KY-82QN!2yu-0on6%4DeWVP(?;w~6hJc)gk*b_2*( zQ2iKM+fdC?M!xFEvN=$H*>`Md1MRN|<-Ia;@M^rEWg6O^)S;kwlILIhH9^4i+X%vo zcW!R#J$bHpXTO}QrQ_VE){*fL%itYtmuKs}LPEc(E!vXb)O91RlrrXg%mM44HitK0 zBlxd*=(jRcaP*hU7!$jCx_|oZP$c(~bnSWDa=%vh=8s!Dmx?N>Y!ea5-~zCc00u}bxUpdjSWj77&c-k8v% zUp1L`dF+O7fAtr4jc1M}w~^$Zj!T1HGeI4nz|}S3Hf*1zL(Np)glbhWfffDIxR&Ml zoRw*#_qE|@kGGwEv@l0^?jN6<#<&ekmF9~w#ZtNG_~awUuNIOp0?- z-TJ|CuKr3-Ty%a?eWD*-UvnncBYDy2i8DH+jHPg%_ZIb%F=((6(4ldod`J8=+2Tms zWrsFNr_-VnyPmn;Ia%^~Z9bB|#n&c-?z%fG1r`O3Dc4bsf7J0S`hM)U+K2R$b^!E# zY5_#ae$oyAArOkjWDkgS$=_rIE*AjaWVAS=YcEHEPe1uI zI3^hjO?*1;owzrTN&3YXpF1wNAyUgt{n=Sv5RQasP}rH9)}`<-U+vP#trT+m(g7nT ziVOD;e)?&Pi?N?Pc^V6^Dba$(frbNw?&C(w{Ka|O9brq0IK4$#I^mx36AUNvy5#}c zdVe;rTRJDb`_8+urP#nM=sK}vasKqvPf`WrfVPsb6>X$G_P#KV%N`54Y_Ch1+$f63 zCUL?@0$NXapv8Tm;RCmlNIzRk{ni6Omd-HaIKP+COD1D&w2xL(JEI+qIAlVK$O(#{ zY&62dM>dBV==tZ*hm)CTN0_Vkt=6*ilFznkow2w+VW}Eyj2fp1fj=l9kWqfHokb7MMmb30ZYOmHqCC4b&k$qy+W~+z|MPjL~x!l*UHDeZg zWb2lWF~t12jjL7S)|6Q9;)}fQVaI=6yhP5Q8`5qg;NIY;yu!>9W9pMU-o&=yyQUL= z@Xep4VddFBWM3ua96>QbYz8=v`4}&gy%hajW4GnE3Y(IO-D^RzIImlYGN`7uiU`0aZ(cN}5?jPt$}aQdka z=D+*{t9~bzAAj7(&Ym9D=tqwiuaj*8noeAp`OIFe>uJ-#S^G(V3z)Be#{7R^V^0qu zniXB@Zz%%(Ea5FV@EtX~cPxS)yt^-l3jiLcCZ%62oTfQQuCg=MA3*kuuO(iEy08Rk zK_0Ed*V2=)CKLyyHJXEp`1Q-~Xiv2R=cHb_i}cDFp_h^K@~+yuRr-g6q=*ZRv{eU* z&JKa9!{gHcih9r{&N1339MB`z zS(Q&&e?V(_6ph-7FPd`6QJ;JlD>;GbWUJ4l&P=I^bVe1WlVYD^5}kM=pOs6u^0S0) z1>|1ahYty_G_|!nSQ;6*E=Uaa2pOe4W)r|=3QbB22_+&+;ly?_Uk>#JQMlX6xAsm> zq|x+N!N5Pk?pOgW*YQfn_r10wBC)RM2BjTE(yjcY<~==gC#ND4Sjad@GE|1NAq?E1 zVo+kam%_w?zaXWK!YjpOtFX4arn*cjqcGu;2eMasX&@KGh#wh)^vFuF5d(Qh1Tg!F z^1!`zWpA9HRCHulJmpJeFb@H<)SbtVAM<@iiv_v#q$l^EEfQpt#KLvy6`&GEX+Yn#{dyMjvqR8+pU^xCMHt)87U)co|U zmS;hDjX$$qxh}mvkW{&b)>rXuVGh#C7y`JF-O1o)@|@f5YP^=umA@X&EljSvh868x z)h4@b{z_q?A;4c5Y1HQc+(gAi<~2m1E~T$Q{YB+Y1c*C-T!7(^_X6=}kNNh$ zcmBd(_?7>`7W02u3p(2n;}v1iWuhD2w^wO9-`95ECR>b{&@2!S^HommbzyCbHgbEZ zZZ~2pRNu0+Cn0+Hrke6xTpF7!X#%yJgSq=Y(Y?A+Z*LK>t#ZE2)Oc)j;x%Y-^7PWS zDB4!bW~hqR*!450is;6jx?ybyqkC(%VRWj}^>0eMvN2%5Z-3c$sYWeQaA}yP;h01GC~+jy61VLHF|!;JUSWuV1SVL%9f);sDvNOcJg7@X@1eBU|Kh zJD#<{xAT^^Y;-pr#6;SM@v;$E6PS;RGxBWPRb1J29rM+;1KO5-UZF)|{62a5Bo%&U z<5&0GB~iF19rx*HHqQNx933U!J0+$C^Fe-iLBJ%&c1Xz)zj0g%C_#{-NotjYKL;1G zJb}L7Zh&0KplYrhbgVKkRsBz{=`g*et@p)nEgb%>!z*S$7i(+(0Ul@v79V?6v zZV8aSPd{s0dy)6sLZUnAMo-PpXKkB5w9iaN_iJMz^;>&b!{h_o+iSaAA9xk!+!<>w zRE=vC5F^!z5k3v6so_<6Y@f#uZfVGE<_7-Y{rBB>+RCji+E!1m#9AC0bt}Z6KLa#S z&#F8w+pfRx*Zb|iqN$o8&a{JebGrtK`fB_BAlJOgezLmvlj$Q7Kc9zQYagZn9lTOC z)`@shJ!?OVuD_RD(yH7l$N3_@p|{4IV^T;kR?U9vCuBR%Yp#wD`=!{nkoBT;$ykaK}}%`Ug4@EXcU@r)A6An{+x%UR8Wvxiz%vLF?9 z+t#xjSDb(7Rvh}}p^Z@`e~Jdp_f>Hq;-po7H&bp3F6m|wxI6>kW^sERQY?@9<9_L9 z{WW{ci#Q;x4Esd4FJ&=%I?k8fw(uiYiYqlqLtiX7E|v6~7HHHwvn#EZcP#=MQV`H9 z-K?W}4I2j>7DlbDU^XgJ-##ZHSvY~+We4Nfk2G>v*jw-qg~S~CmN5~1tsK~%kJ$Bb zVNKEa083IHwKF~^fvI|i0<&pCjA_q{1io>w>;5h}MZ!g{7WW>WRWYZ^ijks28}Wqa z4e-D$KZUn)O~{0|eDXI%hCVQAK5)w_LQTk~wple0O$O{aTo>?FqG5X`-xc4rm6c`D zly{xIE$~Rqp{SHkqXWTsOY%ogY;)I5J~R-^5D9p#)v|G^Cfel9L^p#ytr-q1N`qGj zhVA~1Wo(>I=g#9joR5{}d~bu9-Ia8ma_`eN?059dzO~?uf>%w-l@L}I?g>4O?l%~ec0vCpYLPN)eGC4 z+hru^t|F*s#1xVjO;sB^txXd^I?`|WBKCqyC3(=XrbZju7{m!u3%18LDBjk=cB3l` z?G^`h)Y_cjLyp;MYb83+hRUF-RZap{LQb>!`<@jDg#Mtnz)8G$_wmaUpTWY36N!!L4PoO^ zv>x0x4hcS?#@Qz@(shizjuL6y=CtIrCLx4Y-t{RWLc-wyBP4lX3;_t9{ndjBGD^~? z`eD8RFGz_ti^u zgS+h#1{90j;{n6lt7W0mfC%CL zK<;uH3mptxv4q;OjzJF%Ye^-4vhrX<2y&swD@gQ8t8%ZYEVEU8rPXr9s9g9yj?8^p zP{=~=5IcJ)4GO3%;^MJi&<91=t;q1J=}nKbaLSLffvZTIQXa7%-h$CY)M=^V*blhZ zO>-pYI{J=|3y1cO-5o@k%ZKavfMyAlr{uNejc%lk-(@{;byaNAf30fOQ_D@sf>hKS z_ZYC`kFkn2Z`x~C#wH&bTJV}{tF>1?htwq7sz*E~3jU6|*Lply6H7taA=C0BG3U)6 zYh4@aEavsr154hKhQZ4t;I3vneivRF{Yk030howd7|@k*R@vs{`wfU2vn$d8{nZRf z(gd^?Y}t_@p;Z4s^8_s0Xn z*e0HWs+8NiMcB>p3}48{AXSmwQ6fu_Ck?pe#PLH1*O_IqLI@MuB?-Y zk)FytdI~qLmQ%v`OI>O4`WEddk9m2jwQWx$f%a{jUfX`K%@4nbv_FyFw)>Gh@^rF~ z&6Yc7@!{a0`#6w`xm7TDddfaEZ9ff8`^26l*O8<+T|gyq_3evR_FpAdx4kqSP27Ja zP`^X-CpJgxY>dY5Wx~#Rx7<0{Z z|3?pP!)@r#D{i>;!!5c1L+Ji}yTzlqrERaRy2!!`v7{k z`3SChvz=rqI2jb7TLb2H$c|t)$bC&~V^eFyN)5k`>6f(HpA{?jq0t0xmC@6=y}j~R zacMcX3iM+KZ=KN=+Zd-V>xQI27FxZoSsptFU;86o1&dBDT2));sbb7(P9o17Ytw%_ zfCiP_>DX!3bc1Gw3UuA19N?y}z<} z&^%ODnoa^%LIchGZQv#Zr&JJPEvR_pv>&*D&b@%s|^5 z^u^f`^(3b)P@7xJ0CIB67q#OFjNUx37+ME`FnsEBZbHMGPW$hFyi!Rz$yMgy#^So8 z;gn(LbJApYwRLP3?_52xd%>%%dH@cAtz`~U{<v(PXDyiDz6H<_U%= zWDH1u*A@P5>k)5mA^6t(s0p2LzizM9syxcbXiQY?Dra7BvyNJ4WN^WWu4FFKXW6a< zhQC?0LW~KliC4=V>HeU*le|Q~3>UiEGY%5wf?3-ZjL&QOhIze3Z7BWZmNtA77fEmq zd&NmE0Qj>)U|Dc<0xP#1s>;Sn5QuH6G#%}$#c0TN6bI;tF14fid@}0-|4yadIld!^ zm#nKVzh%5k9($VrgfMG6>m}ohSk?;wF=r$Y`1qV@t&g$p3%Wdc7k<6hUGee(_c zEoP~z5%FIq{j~kOm^Hs$huurA21Zq4X%1TUWlUDvF{g^TWcFD(?o^uWLi zq~D5GzJT(hhII6{iV`}r)(WnptrHyMbdi+U>ow6xsxL5*I!ej4PdqL>N=|*-Wy2pn zIluJhGnhWU;13r7ngE%&j8cWAVVB#+uf>QT*K6#>|GW&=eRo!`HcuGY5LW?wnLDj2(~;Y|K(}cBZ#_oSd8h z?Z}=@_CN_O-=|nwYyt+|O2kRAt!1gnGaBi0spOtjXnx7^DVd=er@)OLx{G8fZdC+x z198HJI*7wnB!p~{bew2|&~+-a0@anHi?WQawimY^?md;@r6*FYb}d0~ia5_*Ij4Mb zay6!QpeI^@lqvVQAR~>IlA0U)&mT7PfDE&1`C%`X=K-cHxo6-H%E1bcGtqxZPV#}!pSI2ngyCBHBz zZLcEM`BPOQK^?y66$|-JT*YlL4qAQyG}yScRPrW6vZ}6{`+=^SFdeLY_)R->K&2k# z_*Fx6)a8K~)%Nu0{3X~F#nt7>K0-A)yJOI&-^ZpFSE-@Q)$AAMBwDy~E*}QaS(Wg^ zx0N=1P;|9ihrCo}Or0$73ng~J(*`bJhyUT%DrV=lFYV2Ra}}6Bc;UB+`1W1%noc!( zb~t6xyb)n+jAe|E*$s5WFBXl1TKg9*AJKKZjGU=P+wg-_%RzAxx?+b%_@!Snfb?G% zqcQk`8kEHj8?T)G5waaV*Tt#v!oRcIi-Oi(Vn?D&Hc2so0pIIcXvBf(-3kS;{6mai zGUZ>5Wjf+AR%0$OHY{DcO-;vMMxXgFNa(h*lA5mOF}{4;uQ5D60!YAs^}2|Dm=>E{ zZ6#)1oKoFQb+hqN?Yxe%`VHN6oFCbWmTeLKD=s@-h=avdzoMt^J;6bhA{b!rC*zCi zP*ubnv;uz~gG0bKxvTrxKV*Dz8;A;FhR^Dm1G#u%uYTdIoA1r_|9xa%- zRM!@EwD^Mnou^DDoI7KWK7LKnE$}LJ&HM^t~7ui?6;`Yj5_3JSWeBr-7pZ1?y zX*?L_uAd{AtV@y8{-(mmo76#XDN9)pr{}PIjchy6*y&Q+R(Cr>AI5pPB_QSvaSdZS zr>S$HTa6Msx?bFk-6+DVO*1y@tt~AQ!%j%`4cPEC`dMw1j=(iVM(iaaDlxdos4dkF z5;2CFtrCBDbr&r=59%=p{^>M%H&xYF_@=c%w+}6Kn}70Oxo|o!rLMM$SMyjWf&y;V zQ?Vz?dZrn{)87e^3;T6P;Bo=rbw~L6`tR5dfcGCieq!6Cp8Ek872MzY*tWF$nqVKF zeDd+2nHkmB3reDplWJY%;4rDbY_E`Vw5@Xb!gVy^*+#2Hi7xB=`AmQ+yHV z_X^vy47gveCAO+@Zz&5dU(ATb>Ezxgj73lRjg;I%X)koh_} zryPCAYHZ`=z(71WdHUS$k?q{I4o(K_B9Jp#biZr|Y~=dEHx_PRl-Fv`r}lxfkO?OK zV~hUg%fAkGoJ4Sc>X2GoJ!%X9OB_hp0;uuw#g|`LOyKYl7}ZuM2s#M>qogsW`X=vc zy9t5_y>)^h*!=t(-G47oXrwRh+m{8f95^|Fh zzIAc~+9R0EOMiF(79tHX?Brm!cnH}cH)li9PdBN%x4WzRKmL^6DjPTD=(kq`aQ;^DjP+llE&gjrD_h3 zt7v*e=T<1z6LkB<6q`E@dG(piDJ9oxALNq?L3+-D!C=kYSgxQ$7z6r(NB`w^{tj^! z9OLiaiHm96QtIvI5tVm4}S0ib6HM_dS-57jE$%5Rj%my!4H3UEbLunw=0@YiW#rct8kL z`n#-HVeoXQfa(M<*$4fszK$5hSO(4bjkdq^_-LJAoPYXhfA!+CXHRqN(6`K;p1Mt^ zSrrP$1J;8%b#yODj=WNJjyezxbIhHDo4h- zb<8Mqp>vgZji|;Y=f-thuJ8vm?EpA{Tv%^90{y8PZ`$&A*WTW747&f>WdDFkJ5StR z?^BDIFg*>GPCm1g(yhR+*0Udj zs)i#=*#R(B5|o7moIIh}ilpq@EsSFZH?u9US^!HCK>J&J?gyoIL>~Kh!l&+5nAp2j z!Ikd;jlT~$=Cq9udz!CBKk9nCkSEHorM$^kJ+4qj$yFXeD=Pwqi2Gw7o+o@;8z~8# z1q~IRvW@2%0o13l(i8WHO2G@vyyxJG(6T{defQ%eUn5~z5(nJ zc6v(2JwI3Jh*R|UEN|$e_=X(>cPM$)MwWN)Yf zp1(epM?Ib1 zom)-C&WIgrTS530Q_G`o=hh|-Q}tXew~xC$M{FMH0)VD6x&Ch7^^3-{B5=6?a8{UJ zJ4lnl4c)hQW**Os0e-?cCh;CVCOBWe*<=mQ^F6pMEmii#BG+KyTaMZgtp>0;E~r&( zPgli6x$4|*cG9`q zx9L+B4wQ{7B+$Dn*%+Sff>1L44Za@9jqbc!O+MUoSnc@>PgB%W1{FLUP$+w&*v6Ucba_+yrI%SwW%}To1znX zwQB~=b#SJ1MV5*o4{Qwx{n=+W^^Uu>!O zFM&;elbPwGezeV=jfiy(dkZ=Dbu9r^?8El%i{FTYpMKo4cqnelrI1jKKFQOWE{Rnh zW8S0U%8JIWt_cRVAc8&+qOu^R4tY`A)4OGmWY8FVrimiD@@}G2dMP_HH5MMCr8=(! zl^0ezLj6$06ArD?T{a0>7|CIv33d%5dZBlaQ)NG&_3a~1ac)mjx3rk2oq63--yU(Z zh2y&RF8DJdaJdEGj3B*6NS$Gnz5e`*=kN8`*@bnsS%30L+e+^X^5_+U+a=)672CCx z*sisGl70LNP`59!1&u91(phuYrX+188iB;xzgM<1Od<`{gV zE&g_jCgpu$jh6q7?6`V63+?sWoD7Ie+*2L5%Mi0nGGYYQs<_Q3h{Q|xcqjff6qq-) zZ}v>Q^Ui~eB^ZtgO5D1kTdI5wYFpyr)2CYhcfaUKQOln_dzQBpF z3wAtDCB`c#`OSRlZL(L!+St*tl4E3zk+#Ys|CQZ}lUtD?>?B6b;hb&0GEUEVlf#Y~ zAB&4-FX4<$^$GpPc4m@~^CAH+`p}*YWd3P;@Q7PjZ6t8Qay!SYPU7cOnj*QTlw-E8n?Nr@98HU^O)1j4-)Z=;|N}1^TpKQ2gtj{8QfWI21>A~^Kh0fq*RrBHOD)wk zqOV~~K-KcnZ`YA6_#^gh;YxP?ZeSq5w&5=O}F&NeaRzRH86hC zbV9G=(%%jq>D_2c7h33EL;Sf{}f602z=Tvun0>zbQra z0@T_Fr*eA_ggE_B`L*07XDKNK9X#|5iMs#iqw{cUPL(8NxUU)#paKasREi(;uBUww zG=jbocDu-cl&rKGsZo7gw@Pi7k6Gw;qCB2$b$zl2>`kQb$=bsHFq&k6iI&L|MWy*@ zS#*t|`0rq0s5IQl`2ept*QUAOZd*Q_Rv|A3qoFTVfj|*`z#`_5;W@c{{E_g$`A?zh7u{`=M+I3o9f+$wtOH5#w zYM76Hswd9SxfmUZ_I2NDdm13y)n&w4=NlK+utVxnM{8gAJXFL;)jXZm3T9j!)9krh5 z;g-3`Cwi?e{V9O$N@OYvzZyz%D-eJDO}a#lR@+g_v_TIAjQ;BuqJ33c=TX|_G{!E! zsR@2Tm100*zVbr>tf6fM8$2XpExThE07mBuI27k+jYSemVIyfl+3oit=SBKMGzuGK zV!p=wq|E>*4&*AYyb@2iS_f}+zSGd!`42YfZABuie@8#HDt||t|6|kaIw?5suJ6n| z%SjYmKb>3!QYA?K(oPlIc)!YTPj=9W_}$=;_F}(IpKd0Ar#q?I13lKe{CHy1>EQxWMuR-FAkA?vb+=dKs{PR2KMSlA4@=?9 z!liVxyHYKOrIlQm3qEC)I+jzsUL&G2We}2r!**PgDV^ljtlq{br*~PH^b?BYQBiD^ z7xpB2#MwZ990;wQ9wW-ViEZ3wAO>D6ENTEkldxEu{9N2d@CJnNowkk38uZ}pD{bp< z1Drzd>p6#mb_KpzqFAbE%CSr#D{vwO8Q+&|!QN;-H6d)b=Bm|lh_C)LO}sfD)>RjZ zb_Z^0wFIT|&$Wo=DmQyH)q$P?7win^WZ)I6)u5G!c(yGq+Fa}jMSLw_&w6) zDY7$Xk0v`?{FWl@VJWz-0%(D~Isb72LUd@7@!(p0Vv`lb9}jekOZC@(IiK)JXdim`VHmIoot%E-vk+ zmZr@Na&pMJO3t<>JslbO`ow(SzSMLdEg~+{&Nz8ecTKvY2#qaws*^vk?RW}{wme5m zq(NNjjiUoXvnS+`t8u#+qTX~1dqQveZIT%a*WY`PzGoYilC~aFXG^BqM*hvNZ<21g z)SJ+Qw)GRv3!dOFoEfkt4F_C+1UQ-FYK6j^jjE4x3(7} zRue-_Uum$bX$6M2BZT9zXdD3qi>c4a%O+Tj>4bK8YI?YhM_w)H!#(ISS}ahzMCeL+t_DKJrAGg6;$ct~mt zIneYeK7py9RSx=G4=lHOeD+yv`^PoE4Wy~*eHd+LnopDq5bCcs|& z{Q2iOc5*y~P_uZHNf8#U&)cVaVJz$E{3R1D_#W|mY2%0|TtnrF@%N&yX-D=2ypR!% z`5x<7%*lG33v@)3Ta6N@t;YhC9ADfYqp=p%waNo({<(|xsqhh_FFoe8FWyWbm5fWp zJ>~E)!PE$>DCDBVJqf6+7xRSYxS@~TfH4tGYfjkyzhq%zccuzV>IJWVcw@{n&Ff;~{PXXpYn;fFC?Z3ml)O1eN zs1{xnJAwEq{(yM{g1Ed@K(_$8ug(xReAyTXf_<)VjcF`m#ORF9>fC&#%|-O0o`h<1C_*q}}02>I&w;9S8$&v9>M{MI>^-6r_d z)|{jJ@)%Vs_fdOEuOR}L3jnVnsIRK>?!BvroJ{jh;d2*=y_xfp_XjtJw03X(<3yU1 zfvRO*;TKX(F?#Qs(|n;Ku}};rM#hhmh^>xF$&*Zuq$Cu#a$QvFNdyOJeOZRCSLTI+ z#ixQG8w-e&WLtRQHga*&nKHsjUUkc97466N{(bnAy-iZy6lb-xNqq|s8Si_mB=*oL zom87$!(ltW+!}%V)^&oxNwCIWZ3F6kjPagkOPmA>M-M!D*k7X*@8)$&hO~w^C&~Kj zo8q-add>j{-WT8MA{`bq3ZlIC*;Ehg7(I=25@0w9fGuUdMC>MT4O>qLbi$7uU--F4 zmTm-kzuY6v7h;dE@vzckiDXYcPi&lcGHmS=-Mb7IU%zSNg|VS8HOT42Bg*{?lwWT7 zhDMJ`6mI9C%o;2m_@ng=Df+XnL#fGRU+_f%=Uhp0h5{C{?QYmU)LspomSfwKWv!`J zb)lwnRj#xK^K7Qi^}zaCny(zI@?)9)tV_eoZ{w80wYszVQf~dBb{LZ{<`E=alxuC2 z`l%}Neje?>8mG9@4Ze<@TIXHx>R)orNh=Pi%CjD69MFHman7XVHfzQ^cA<(o>(-2O z1LmfWFsHMN^^4UpnRCdL zyDyGIiBeJMq_|f&V#%x5xL7M}w7=JCNqUr5UMtINcVq$TqytSIbY_Y6=r}yjb&eMb zTXd7n=;mw2Q@)$MQf$_@lo=F=1#a>m1kX--w>{s6i}3p529&W9XC3WgWs_$teAzDc zx|JhS;%Qu}-sW0XU3aNjvDAz^)|xAgI`Vj)fR4}j>OVp>0p{Lj{cOCdCHo>RcWy6@ zg8caORbA8?d!22xtYJnCq3Bv8?RPXy{uk0;mhzObg7kowFVH6JR1FQ)M%9VhsqR&X zt~;6YI8q%3R#YD1Jna@0<9o7D{Wam8;CTqtnsvV zUrRnb7)8cfSP7he!q#6FlF)!#9YsT`5lv`mQC)oHm0!!^5AGz<3UxTta>b~;Qd}Lf zvLe1x#TfClV#)@&?08v10z_SQMBV06(sHRuCq{|xl7J(>51u`K!3#pJayL+bR)n6D zWn_6x@(b3V2GZ7j^;`{3EnBN3q+AW1>!PuLJzs)M{OR@LMlZ`w!CB$D5?wMhBOiCAaM0Fzu0U=N9@8TMg|69c$#s-DGzq3 zHN0gc81~n)Mu5>$5zO%^UdS!K}w-b0q|Nt zROKluf=x?eLHEsbJh8)xK&ReT$=0@qrmHRxwR~GQsIS(zfpSl`p{3GmEkq>12n~L0zHV zTcNa&&w6V628;q>fuR`sMQmDdjt26`S^^lrV2Vc#j5h?(i6prixK{QD3Re1#Dy(!% zy;R}V)k@DY021OVdoiA%4~sO!%qbkuqd(XwAkq43Fa4HJ`?dDeK5!MT*FI#s<*v)y zZzU;qfv`7_nxgn+IbtARSGWfE48QOAuWGUVY$Mkc)HZ=(-10VzU-=h*7o_=T&V`I(=&`l$~-@Ta2Jr@y>d5D=f!j(ecF6?m0)}j~=y}bRa(h?%8fQHH8>)-0TPYt$0w1l2U}nZF*8j zJG4EW7>>X4H{F)$9K>5t+K1{qAF+zR)y4GVZ$IDcdd?|TciC;@zi69H?9_9JBVDok z>0oR$VfjdP97A|PE8?6+zp8_g57kfPz28E_tFiObi)~1RMBPDzP_--FAvJ#P``U5n zl0L(iA2vJYQhIw4xLg2ud$HFVy7AzfZ(lxQizEwUe34->DEBg!NnM3E3xF~Aw&qkh z%2g#>AvL)J=v%mN^NvRmu%?#`T+uCB(sxg<7VLr z1l}EOgep^Y3D1gJ@kuX*)H&H}K6)$)d{rbib%9@DM8isTI~RQbFeW#spBfYN2U}C$ zwJq{W@tU$w#=X#WYvdPS*z*BO%Fr3yCCL2-cssZ|u6oQNT#iUV$lzrXta z_y0dXciFaZJdRX8N3->T*T@pnVNUg?pjUgzS<7l6T9Mmbwb9)G06+jqL_t)J{MMt? z)%kp(4!;_+DNRHc#1Ddw<8oJAZk^NlNq?3^QDQ}EobqrkLCW*D5eCO^#dDB-aI4JM z?Up#cV1H@n7F*x#*+A`C9MbDw`kU^&&Cm4Rx7!YRh7ZY~@8YHDBYnz30%pn+dp)3T zOQ7xT6?ciB3&``u8$U?+o443FrGF4p#w7_{Ui|QAI@NhRUxYp&#f7YRWwaCO2cd5b z;0GIiyilsov!FfdPhHf&KIfr&9*E?W!&4lf?X?M>_YH-sD_sD+uKb?`Vw?QkW`5#-#2%#4?-%B4&C zRX-=MGN;_C*s8nH-JRLjipw&F3@v@gP;Rnwvs|o+_>v0X8mS&=b>JSvo565Lh6co@ z0?L!g_AmM(l1on_E7O}_3F{^;s76~7wd>Jd;&G9oZvEKY zj&jkoF8Y^UesEZ&(MCfwJ^GSL-s2!<)m98wGG%A2`D-6JR7PA zyXtotNFFl%%fIyBR&Ey!=34+zt&Tr#g;SooP85o7{21;@`O+>#0fcY$-Z_o2$R^PRVjwLKv_e!}a-oh#;m*pJjonJR z9X>j~y0M9kG#Ch64^fwT@9DFvzy8<$+SR}FZ~yhHpa1!vm&A7}=gPgGFny<5-5Yyp z|E2#g-c)Jd8Sd!ArTOh&i-?x!;}B+ba9yU9Ob`E1<5U z5UX$#bw50i1(yaWWTgQ1$s=g4bzQsu6_JiE#FND;Rm&#@M*yP4^#&@4Df8sElj-bR zN<;bW8@X+#f5P=$I^fNBFfr%hi~O$|)e(ro{t5My;Q$e`;Mw6FE#dIVtl!K(ef?scP)pOfmzf)08{8^lix02X#V#Tt`r z-SUIzsgZL+@5+6N4E`lBxWWuUaAN1c>orQ!*r~2|szQVG-h0ok{>E?p*41zQ)^GYf z{*HYMAH-hs;4PSc>n1Plzx4lQ{4W-%w>~r9j01m~=l|dPd;iVVfBrkaQ@1nw!G+%B zjVHYMgfizd++s&Lj#90^Id+pcZpZ(q>l3zwnuu7v+DncVpyk1hPd%dtmbPH?rS;Ln zN4evT^Qze0AVuwKt*PKV=zcpdV$SN#FlhJlsMZ{G_^W@ztetY{x#D&ctIknS-}o)- z1bC#*uwtnM*FSR`z43|jpkKHY&4M_i)?{a4je}a+@#xW`+@?=IFW>HgNU_HG7I7!h z7d(67RAR0u{$S(zT3cV?4b-6mcb)3<0OB@Z1h>LZpFYit0i1)Mx_xwQEvR;``5*k@ zAO7Lrw;J!$)E_Sc;uQouNP@q$BgVL-+mFCy2f*#e^pwW7bS`1=SVG^`RLtW-O2rpG ztw~8?sJd}WGU^yNU7a-nzY)FA&_SxYg0lXUZsjg(!fAUgHdBEQDi#a>AR|8q z;^_)4tp~zc!hLYxG0=jrvBj7F#`7F48_+-!mkT++@mK$?tH1rX|Glf9{^_5d&A;*b zW*)q858kf9OZzo4-)=v=E$f&5zx4mx8hfv5m)!t75cc2y?ccU9eP8)$$f`G?i9Q_r z>|p0deq)M1_~gXOtQWltaw#mGMZTP7Ae~}H*Xe?CaUGLDq(&uT(JuY$z?`n<5~Bn< zEHy#NCn2~)b%>exMF%cnyX&f;b$Nqz!iKsQhDxKZ0iWA|zdAouzHdyN{wq8&rmt$8 zJ4&VN<>(swf89X*J>TbJA50IoHZO!St| z>r=I%DeLciQ){H_U9--hZl!g?x3_@MgzEcNLo>Tt`3^nGP&#T~yyBwlKFAT}vn`-i zbmniECk3u`fJt+01^4!~My_0*X+ROx0e}P4ZkmO6fxRyg2{HRz$YVdr?zh>8(Ap3t z6thGcdB@KNaTOta_2>Wmzi{>U|GWS8>hJuW|M=Fw{IACIiq>5DiOW`PUD1Bx8r~YT zYU_%2Y5z}V1Xg{$qWxsHp5w-L9bVCX(%S!PzxJzF|K)%2JN^LNdQ|dbSBc3sIu?m` za2n(kAQz|{2(^ABEM&z?ahm=A*?YTK&9bz<@0|MVuAc7c8JOvi`H;*F4(5VO&=4$f zX^bOBafBUZ6lP?TC_u!89pEMrMmUZo3xh3RgdQy6TsRj=WTe=!Ek{CfVkzJ+8IXd6 z@eD$q0cNJFtGc?Xx~lU0o@f2nde+)&@ArLAol|wH`(0J%U28qx>sg!Ya?x_21}_0x?8Ly z!jez7i4i( zh7MgYB>Ch@4F+J&VSHk%yx!u;maO*n&`P~XuziZH{tA!YV;7DEmx;CryzO0E@xvH! z814x1j^*A^t6Psr$P_ni)RL7}n;pTG0Y-CCr4tl=vDGdTpWn}@~c z#-}&Af@3TGW9nDR0qzP^z6bmRW8`&z-p#BOh0=KPHm>lk

    5>D`25>yd*W73XqsD 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

    6y4WI^?s!)x^{i$+d@&%kMMZZx>0;1g~A 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

    F>k8&0&;NBRH`eH-Pg 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%0Ds}>)6ZYnJG%Hvfa(?}!D@m% z`&`rBjC7;`44BrCDQw4ir%U*NRf){YA{a`68P10anh z3)PdcSpm*VRCQB*MS)9O5o0+Kn~RGY81t~6i$kZoQFEWc5BJ1d3Fc9YG*{H6jhCl2 zYdK2zc6HJiMxNdWuT|wT@6Epbq?t&X?{Y0t>!h;^bPx8+MO)3<9N?0*zFsjW&>785 zDT9<98D|ldbqk+w zR%|XIVvrJt<5DNGgMRiauU@|Vt{1Of{bd{T;hhije7Gm&$Ne*d#{pProWqP6P|vF&!5JZ-$1xPprSh__NZd z6R6XKye$fSz!cj$PacRd3;q0wef^HSuzCU?}CB?4uCfV;{{b~d*^_58)l z?|S{_&HrU~cxdOx_d)s4PnsY4Ip)JZZGjWhV4c!e>df=ytiW0Y&Nl$os&mTLDv-9h zHPtFxvw^J2?RNtFvK>EezZc+l+J)=40HF#i+^o|<&6M6d|Ih`wMr*mn|H`>#M{DeT zziAj;UGvQZRWg4PoZc$Zl&Ts7dAFpj`?WV+h{?2d+k(cGvp?pSN4|^mYiX=|x z!%I5{pwco$M*b%=i({3!>EOld0AHnIdu#?UQx@I;U>zJAzTDr`WK}TA9dN7yGjSL0 z7#wLB>OoObtCI=2%Cui(vz0W8mG1V^TVT*vo9p1n(up;dbEDx3_4uZCocR@i4?v`K z!L_rl&mUsg3q2Q)+$tWq<$0m(O4PfSn&NY{-XyeVCs;_=CUo@DDsR zM2kimqx1Ewz(xhmHvl#Ybc$Ci!0mBWDGAj^`>N)A+cdwluLS%L&z?Q~&9;$$>r7b< zqrYEsY5Rbj3vZVE!aNJ}%1eNnuU1iGSEE^6l^@EQLpF!sRo@lsc!xIx$8UxaC8FwS z#5s;ub37jDx?w5-6^4EZ^}G~2}?HYtqQ zCsiK%Dg$rUT#tJvsVUQ5&uiVI{HL*|kB1oL&-{Szbs(^f_u8#ts*IlifD3=Ps{5mA z?dt<$mU~9D!jk#uWnCph)ewwHW z)_SJj;#*Vvz-7o=bCUvYBTC9k3fQB=i2kPcW=MXe%arjHZgx%pC9}&)(3q) z@GDPkfI}>1k}0D(=~;oz3XGQVrqQWzR-jZs*AKTpP<&322i|1D@iZOJ%9s>#>Vd+Z z^s}#j{pMbKeeNooux z?_>(l;r1S?J@o0L*s5#|JM>_To2K&Sb?>Gi23ZkxdiOubkzkEA9(uS@#7*$@hEZc6Zj_brGAK2YdZMPiuK;emZM);UFe``Z*F5;)1;${OPuvUxDZLSG;gF zcDVwNCrZL*?hAd>Q>;;6p=eJ%m9dY%+QDXj)0|zep~lCUs=`VKhz)5)(L|FHtQuh| z_uIu+NdV}7<$&wJg2YG4(GcsqyK*gaj=iV@+EDh<(7m%XPWTq*{}}lIw=LiOi!QRK z#jd=x*u(J+p3o~%>jf?2b~4-?@VM~iU={{$T1U|E?*|HjTxI}sf!iZ5-uOq3QRyzf z5Tw^|=zr#|AozpBG>&Bc;KRLv#lpZVT3?&s@pZdK)Ev>h@r-P*FB(0<=$eIfo4Xw)0bKj+6Pm8bcNrsK5I*y4BX!`j#!^zE>HACx!z4 zk)~1VpQk+Xz&Y{<+^9puH_~}1p2sq9rtx*_#jYt@7d2I^Q9tuwn|TS)7&1M40KZ~{OX}KbvnGT8z8SC?0N&H1bN3NWU8dJ@XNRoE`q)Ou@$x)< z?Y!rbU@csgg&D{1abP0$7Qo%Rzxd+itM9h^)&JMHd}!wbJRa=P=L0?;_({p99&~97 zEG$DkY^Y}*hG#n~u)hN58vy%jb!yHlaO6th*U|x0#>nHZ*zW}RwKs3y{;2(4fN!-6 z$ltNV2ooB%i;QvfBE2-6avFxI%px~Yr8c-17yOVFZ*uZU{} z^{B|#&J{O$oBo)lEtf@&yr70ax(OB!mOIpvO046=TnrcSEi`V!@S0E7(QP~!3R31N zTGGckzBfnT53UGC{cs5!h0|H9ee5y1`5gr<`WoL62l{lBSZ~lnoHpG<|nCoA3KJT68IGt(sk`YE^61Znd=bUO{Tt zo-txqi<+%XjM{sb5VJL6Z-O9d1raNPi16m~eUIP&NscG?{aoWbuW?S83U2FGc|BAN z^ewU6@;Qr~n@{N&DOzsIYPo?)zkO6x=O!Gk9qQ`3J|!;-oy%AuzKT@0NyUdR&}yHE@64nEn&Lvrdm z+>R!Xf1*&%C^(T>;UHYhJ6Hkj*7sQd^QMEO1^Y?)bM`rL!}^<*;E8tdlN{|;TLa-C z4qbEC7czYuhFp(ZO0>0n>p#=QNKo$PP}&dd@Pj<9mRBSlrkL)Fd0Ll6$*uN_a)tl; zqx9}klNiOLsx+DBhElNX$*(F>yPsHP_{>Tl5|sikhAd)#hloJSR9Xj&ihiuxZr$g9 zMn5emgpsWp*o2zFs4nlL_>`h4r$R9xWXALjQ_ zsAkuj;pbBsLrD%h3y&FezxGqir@btkzO&Q)QV2gq4RrRH%nndMtBO|> zKHrkv{_zE_1Q}p=-T4*9xd{EvRmAgG zi3T0vnU0K7bgx9MMyWNrerZ(Pi5U}3Sbq0pCw1GcNdu6UOBU(M(-=q84n`Y}aPnC@ znX=@I*r-HjlUyP@K6h;Hogi~giRHb2uSn;0hskiS94L&^66e?{$`p~ff2bQijl|0T zs$Ha+6Ww>hE!ao+)<9v#ueq!>gTw2tOUs(nOQaLcY#9P`OD@4*{Uf$6Hb&q1DoSrv zy2p6|9Ay!QUFp2}1aub7)BMJ-%g~j}-QA-8rryF-C`;gGe&}dBYUnBgcv2*s{g*y4 zUS*t*MMC1yjYSuuG-+K26EWA0-nSaYx%sDNv0(8RoJBXa`S@X7N@g8)FX&)yax^s9 zP2W2b918K`ZVa$Sooly>o+sr-aprHX$qnhVtX3ACK?pe4H&>=CKb3TLf)Sa`;^_dh zRlZ*i=pKfLy+gXBs2ea4r?o%JQyb1GIV&%bL>;r^o}zUJ8k2Xz;e21t-tsBUOUS$p z{cBU1+E>9oTvGycf0#>uoNSU)ss|X*6}TNIy6>;^RPf2cKn6lqeRvH0?>mzM-u|$C z!_7r)^Rwd8a{Zk!u!Ewk&b*WMH1F<^5~#E*ZD4lw8&}*T+^q`K`dfdUai5e}{Twk` z=JF(B4aSM%Gl`nDCfA=`Kfvg@xniAZ)8}8Sq#sh||EBR4R>xO_UsA-=v98fn+aOjI zg+;rH@c#<8j%PDy9k+shNCGl~8*&GJJf}I6vpr{J;n(82e$8pP{QFPk!Loxnt88s~ zhj1nk8;|@szAr;KSVZ_}%!Bdh%6(8|xO-Ow4K`mM?9BSO^g!u`7uCIikGwCUKX~m< zoC`nDgB}n6v)c<=#Y0+xPkN$Rf`?{iJnL7lNOPYbJRqmF1R9FZ7p3xeCR*JP5w^3> z1tL3*$6y`F8p13OGVc&jC*l*P>By3&2w_o?qn)Z8CEPm5s2gahBjVw3RfgIs0f zxRe0+ON;Lnkl32)dgrb$kK9!dxqqL+vgoZWDw?(A^1F2)3cEo2J=3R_SB69pc_y3l z26AU(qvD{LQ|d{gfykxdm;LGFXm#GZ5Ec`{ZtII*J_UkDJQU0k?-%}llXH%$>SC{+ zw)j`0Tqw&|$olH`^8K9Is<+J(4ucxGDS3$baHcSO>HML#6ZP*e_d>2KQgmruZ(b_Cn(o!dL@|rA6@3`M_=r;%9v^KMt zyf=g#(_sI_G{e;Wt7+tzw(wnLakfNyen7c#s`@2yJ>=Lv1S+p1N4H=3vT znHWX(Z|JV@$p_+R8Gt>sRG5CbXLrWq#RtB>96P<{@Ciqwv0g(jcYg3o>DaK>EVJmQ zco($|xfWvF7Ns6Z3=?||dJ~!q!R_>jgrGye8gzl)hzqLJ#Gch$bRKz){Zx|Cij_VM z&DAbz@0@a3?pC}$Q^}qEhVdskSMoHatjL!+DusZOG}(t_J0K$~O9!J$R0@a6X7;3? z7hWjD#c$%QCS53i@96{Noya1`>oKK%BHj~WF?b80)icuFKzy`F8IaIT5%e!i?%;G~ z0n8iRNdM9~l!ZREO+aS({9P9i^w01*tHfy^zXM}n%zaZRJ)17P#wMRWjz2`CF2+jB z=NrpSj#^^QUKm|Zz2LkTqR(0n0K{kGX=Dn+=tJ@k*{~2+)!~aAAwbVxJ8U?nV#@{+o zU0wdeT^hPU>lfI^jVI7hKEpD_^9)W+jd~j_i!({0D4NtcHIw|vCBhN0szkj2WD;qT zqWAQEi`Iou{`w>Z;`OZKu)_HlBO1J=zQVa}-`TGn#jiKrd>;0Ea<$H5FLOCv@9ARqRLp_`<_M zP}!N>C1{QF?U%zbnHQ;nkMr$+PGT&kyi)3j=+oE#j4ieR)afy(t)dc<-&ds0Y8o8d zyhVkb_DousOMYs1kXtDYm@#ZQR4~%lr+Eqw*re1XvI1~1V$MhIHD2Mm=vy>% z!oGzdaBRPMeoUpigQm#uQMa-cI_drVc2VWzt&9asd)ND={RV`QqnVSmZsICs6y1R! z=ITh_W4{80?_}B3eCNPE!MF64X>qN0^GBI!mP<4po_~{h>F%suEUZj!F!^n;wwM{! z@x{5LyblpK>S-7}jsMKUHt^+;Uz)Xt4VS$MoS|0nHJaB|QlX1$c2GKA?vWAY^H<@g znQ0wi%DS&Fhi;@%e}P!d`6AeEl0ETnSc#GDMwTX7qPBbWc^nx zq(40`A z5Wc6QY4X%AC%ja@;62_b2}Y?|f5b84+VI=+Q$2ZDRUGOb#b=TXMzU zw+BxHKQTm1#kK|XhH{elXby8j4?|OXEk^InGcEM2eDhYiPRH3Z8R%sZd_7Dqnj2r} zX@IEM60~H95&HT!!g+Yl}`TP5E*py8L~es~g>rn8K+UVPvR z!%2O0AlC7+lVvoUglo*X5bt6WZ=;}Nx}h8V@IkPJ#tdSSEmgMT&b!{{CB6dhU-jMP78cIOI{QqRE+KDHxK={x;zwOvsNHj642%ni(P z=Q+_eQP@Jm(R_y49n|(GY5EPSp1a*pa3 z7ymW?XgrbM3vM17#evf$^8ydmdGNytN*sWdl!U$Tn;11T8>7ZsX=H5{v0IT z{nlkvw%c^y#8oiPV6c!YU}UBAw&;r&!<|9fm-mE$?Pp!b(-T0k+oGo-aUb84JCh3n zPPw(Ga|I7R+<#xRu}!A-T!54^lK;5^Mg0)s3%+v)ZY!6d=6DgdwX)Tr{z1F{Xub~Y zJxck*BOI9yiM=96H&!*$^NjYzpToUHzuzx9W2b(-(L3mUK3!(^TkQ@@yR9+k*lW30 z@wu0Ee(qP``j9}RRD#6Pt^QY0<%EqGKeYxoI_VQ;v3aX5)|W>2uY7^O>zNfC6nFK+ z0zRB&h+g{FGzc_-l&s?Nla=Pog~w2p{CHr?ciaQ|b~o9OLv@O`TD|W1CRx3KIkH2b zA!v{I93ZYcBHp4NTjn>8Xjo)mNEjBsU$FZtkdb4@tetiCD_Br%ra65xq0$}oh-PIe zs#mexX6^m__vfA^^hymvHd*g>lL3ENIF|L*FvQHa(HR9HWAy;SS6AHaxY2Zru-(cI z|8Tj7K024r+InJw0qwm%UG1RPdRDsGDY$S^z_mSKsDk>AQMB*|UAE~xPRIO6#ktH%6^ zy0sh^#q~~5ZDeKQ&z)8}AE8~3L)J$(PIRdp+_7(Dit=Zh<=MkD?-Mz?r2Ups`++SF z!*?6#8rI}3-D&RJvrDd{56@Gom>gKDBX>{bw>Y|7OvnA}L{sH!G|nxztiNyG-lF<6 zHeK|hB=MB(iaNApeu}j*hD$3>%uheD`2a8A_(Y|n8x3>ieeAV%%Pq0cEc(^pi>KAW zS3+=mjT?_HWjZX1xDHh+DswNslE{W8P$%41@K)W){S4yJ+whoa)}?9&OZ6EF3~ByYeY+x_~dYncX90`3I57Y9TWdlr!` z^5j-3G|EvPe~D5H*W3T4QAU5Tv-&27o)EA}rp{rVZ*zQqfoI3qeg9+3_n25SX-;-= zUTy{o2+lL`GVhVhJ`K~^DH{cOQ%T2N3OT>2Tu%27i=&1eXVWOc&fH{huaIbP&K%cg zHt+fTRUv^!Pvm6zZ|}y%{YT``8{*W>`+V`m+n90ggT-(vsvia+=~PUqUecy8Ov#HZUZY&&rdhb#k7b&B}{Q6CGrCka5 zB75fI_4kYjxqw&UYP!$%f zx-?^N_*Y1FyA)P`9Ym7w;^QsUEh&SY$7hS>yonZ@I+~E1XV{Ztfc4Qoi_dLZ5hmYZ z;Tt0L8lJIccZvC|KJtf$>zGR;xn24_0y(MKKl_x!eT-v;{l22FR-_X3sUiO0zy3r3 zwTsuq0w92Q9}b{2^WQ{=(PIPB&|z|XC`R&F{o${!Fv)@=90AdBF&%u=pJ;cP6qtMf zH3!&U^06v75qtM&OIm!frn_&1e>l>s&m}WAeVzHzH^5v=No_U@N1b1iP;G;jxeW?srzdt^FSHp1kZV=^9mgS%FA(!F2-O?-HB%b6w zyY?{e!>1=--Z@!xTwVVXNr}9x;>3H{hBz5{{i^G1W(l?EXlRISYbo|08G4-~wQXK* zP-)VzT^~HND<``F+z5^$DSq;Q0PvpEYkgA}kEDsW2WmGg87ign3f}nQC9~GA2KQK% z><@zA?WOMr9L3C@?Ong^eY~D6C?**$9f+IR&$+x z1*xN2iq`-AdmKeo8o3qzNU4j|?G`i?T<5NMBAYrq)WEI5r!jDFa17qPrSwRZ(3Wt( zI_CQH7IARIYBoL`wv?4Fs|T&}Eo*9zSfD$MjuV;jsRFiLpvjFZXI+1E{-WQ5%{x_3 zRBr}unePK&i_Ydpfahj#dQe8(4&ybMXOw~o&lr3Yua@b0eB?o90_5JPRAU6?ktT<` zcnoCE3<$hzL67DHvl+%z_k25iKjM#+k+JX6CfD7|rb>`l=j zfMzu`iRHNdwCt7{n=SQqPdV0;R<8Jm#SUg3o_riubqVazL7mmUEt*mtuD3)y2a(=0 zT81)9DrJz>>4S1ugtiQP0jul4>3tWQTc&=IUp3ou|DY~bX1!!o=={F zE`#sW*9G0hZs(=4Y$up>1Hcx0-`cJB3@@f_FF8i7K9o&))EwRprG3Xqw@{E^bB{29d# zs~TVd|0b7aoRj^o*zab|+9h1C6`T4Srm;U}sbS6&&?G+?0PGbQF!?ZIF%~{zR?3;N zRCPjXusDAhmI?oq10VH83&vM$K-3BwY%T}xRTtHb=`{k#{oXsK)iz>RrxmZURuO%# z7|*v;gBO(pZ9IWng!N;ALiwL7rR!ceLcgx;`ccHG;=3N7pM@bpaEB4Kn+^YJI4i=6 ziN-1GCO6Wi=AQoUusN>Bs()A?A1Jhw_lvXH_r2MA!Tdfr#-J#0uZP)rf$ra7P&4aZ zWbX_}GNK<9i~QlR>p;?Ub#cAu;mrv+8(qO%py>0xEjtb^7jK@nE<>|fE{sq9qUhzNIf#V1YB}HS7CytCdO4Dp3{z=+X>q&@IXQguERp-iq;Mg$tVEW=GJuo@|ri(C_g(Oj?AY zd3bxgQly0~IK2C4%VjCN_;mY#K@|Hjh`zY+38=2J@dq1TWt0lEQP~qdrHopt3i5t6zU+7bb9yyqSZnne4r`M;Dl91+GXn&bgYA`)dtr_b(+E zMAOX8GgZ&@gAezdiJHzhIW?{L!jA+6X5&t#5UErL47ns`Ee6;v7(gK=c>>A5XN1PD>5gp zVNQHOI*q<7*GO@j4u<#p0~+iK*V({vRhh(9X6Fn=M;G}0R&TIq?+W3KZ%cYJFYgdA z&?_9l`f`d-qNG$YiP5qqp7>=x8ug*cZ6 z-f$sG<|P?gyk|~Z;L|fAl!0*|Xks1EtIspg>RcnH!^t*s)V}Jr+&9_FC2^4kv=D7v zCrod-;>(GZ2wD1%m~9(b)pfZk{1Q@p{K)vcsHcN;RF4Vv2qbO$SeYX%bnV&>sm#FU zeg3t3nx}%!(4oM?zo=SSgRiI-<>XkZPzj|FS_;=&vR8x|`s~hjiW2RGs)Vzw0di3m z{&5mp2T7;#tD3YyujIxrst<~r`cnbp=~CGrjN>bhK)O0~l5BN;655W6 zj>n~W9D2je;IDikG;^NZW zflT;gGlGlKV>Z%^ELe+PqBhG#@E+qi?vNlxi5Nc-`woG{4urBhR`*C0$g?93 zPl{J*q(?p#_<6Eldf}oGcCsHdbR}c~1eii7+h)9!1$O;Q^v z=i5OMh~yIUC4&Ue{kZeG(OC5XK6CkKe<lrASZ^_8EBYtZif(pI8@A~1Lrrivp|IU44ca}6Hp7S@ zRgWm%V!C{|q^{EhJ6>NSO~S0rGtu8#nDJ%)?l~;yGlJMe5+{HBL&&zxIg4;M zi9Cm}L?n*pm|cL=mgtG*{VY3kbUTzj-0ihfJ>EfooN4dN(SFx!W!V4wDP?gM%4xab z_n}9EmZ##dQo5Rbn@iEmb3eeE58sG6mqOzFScyws$zV3vbasYBX+&>*M{i@h{V(_~ zV{KT)D&L@3#54ZgwN%aJMSfh>H3Hcsw8asn2 z7)XItIwb@@(8DIk;h$IA2LO?ADYZvIA7--sbymT;&VdvRC>eGuv|AVc*Li7sE$D=z z(~~W65Iw?SsqT|^2-)CJ>QN=`iRQXrI9kv2N3{Ynz-+;T*4q<3++T{((0 z?MM~BYZqYbL5IT=wW8r(@jQM9|D}p>391W? zg#uDnQ!`OPJT>4?_G0JuL{04)WSJ1_ApN0sM!ns567<2D2lt}^)td=+X)hJBXs9XW zvOrq%w<2C8a>yI;ipko0bM^h(IAt&b1N4}Z2Hu{!!XSbBCU+Ra&~&AJ+he=8?^Uhg8W+VgX_ zRzNnzrYR{;ikd1up)+t)J>Em-3EtptAF&M7-zs^MXZBKf6BicJx<7hcftH-X)Mj=~ zqvcKxF~Zqxr?dwjhk)_*ZLa-;N@-anyJRbVu?I`PN|VhSZDfn>TMD8ivM^x{B=dm5Kde|~ zuCD}(PHlRMfs2>cW*%}c<2P*NX_$48-yfGb ze}-l1U`%Mp87ybj2rMwIZ`u)a$+e%HDh#}6D<8BAmZ%B?YOn6xcu`)0Pn)2hXiMQ& z1SRTiMX=5rF5%v{e=VoUj)yb9!3)RbV2zB%$dGRSn`+U@)0H$1D$ z(B%)#3SPM0z~Cw9oEip;V5`DC^`k{0`MTMY6PLk4#6iiv?GvoR`0sBU>d+y`>hQ>aMNqc@82G{?X+X`FY2`~tW5X8{^oTrA-Z@u z*Si5wva6nf6)>+S%(Q(@J^HeA^RjY6WG!bEoLLm(N_{?$Lyrc_X3t&@Zx%<~8urh( zM4iucR3)VZ;eIsFCu0*(;idt9QUxULOverhotI#5=^j-^!%MqLe0LOG7y>5?N`(dW zFtr-S^4{%AKB(cM3!%6zm=#hPJA<7}0HBjxZ)m!A%?;Sp;qPiD&dBL8_lT%smFwSI z9zraSliURa9S(zxkJTIQTc@-c+5Jv!j=LG3=El~fZ%RS&M5rCgd|#)f z@Tt`@8h+-A7kS@HH%@nKFL-6o-O_HOPcq5)ap-%mFw{0B94AZncyGV{;qh}W zT{?V|s$Mc8C-iIEi?P)gj9m|5{_9zp9_q#Ww=aPPE+cB-wLOERaOA zJLN?OFZsSnc->lSwIjY; zd-X%tWzMk}Q)3ADDeY+p5!nOF#?jXC_`pvj)?9u(gIA3_4K?I;GvMzR zQPQ1F+A_R7HC468tiyMkf_X01wiwqkQEboXt$N>ooYSz+$+~;ZCfp{~#CvYbd&!uq z(BLAd8O?=TI%A;<=Vsz~u;yBGv~nNOq`IzX^)vcdV$aitJ9~sfZt&J(M4)!Q@70Lm z#{feBHkIV>d_4EaCs_UM60}X8u9-)#%MKjER5}vZwG2wWaLR#C$2g>nyU!Qzkx)|; zO;Ip>2aK>#ao%`rzbyG}3*>e_f`K3}kIwLz%e%pUM?2Q4XL;Jfrz}mK{c>Fbo&W9i z1Rd)F5uQ8Xb2t7~m9i4DjqoaeI(0ZqRE2zf_~OofKf;GQ#e3}gf_P3};LowYC|H5U zMOy@Aa~qza^JZ{t?!mFSyEZXjQ?8j1`B`Gbam~%(in$&;*M)@x$|q?`Oxi z%M_Y&bE{z`m09)zV=3C2mP{PDkKXW0Bv>HlXxr|bS*W#Lml@h5Y2VV~8X2!Un)oX_ zYb&n|A;c=ptw}qwTQ)(+8)h%O?foY;C1dPz#``+61W`hwxH+{KPT5#d@ezY($C9_> z{WWtF^CCe9&p`6ifQ#)t@j)``fm!(5k3-aZQg1 z^Eo&C^+C(Q@;?BgEAO({O1^M?ssgYrrv^@$2n18x*YQjj1+spqRy1WhC07HqHPQnnBz`$~ zv5Z*JR0)*L?hBS>Yp>`v8^_zViL$VcS6-}H9DnZt&&txnbu&puEv7#6tdH)W_lKFp zofY^Cmq)z%eDT~yN_JDZ$n%EKNcEcUyP)mrp3AY|S&R5gjjcwIMzkM&9~ugbE%1o( zD2SD_BhiH5wBr75m?};ehbn4x6}PPWmIf@e@%rR6j7Wpo#fWr}>bc+3zHzP;tlA?M z7zqlGoOHRvZ1vZM6US+I|EtLa-eY&6)=4(&wp%C)*7HLs&Bj4vV1B1>%n;~3q8($U zQk>8_YSTf&C}b-h|2E6--_>?@om=ou%M7x66fTJuvpu5;v3)@-^S^}9e#@`sq~37Z zoxH>fA2edGr1#cjc_Brd@+)&Th!EOs9|xr<Ypa3Fg;=9MNVKOQ;Dzi(6#~1N&V-i@%cOl$^!E`~d2nC6$@uP0DR;$6hWEI9= zWJC^D$!UHvzqx1ayU9+E2D{$BcVZ|5dS!|_1=lv(al4+22Dv~`pZ4IGbqwTCDtp_7 zUjp;pPk1Jwt@}B<^wLU^7+Mam>n|4P`9rgQbc*0s7>tvRpB{YU4gEfTP7}nn`pe`W zqwLF@NRJ257IhG2Zch)k3Yc-PHv{LVrDkknw`i%mGHkgNS^{f_hU@QyV@1+DZao}t z%!Z9Ho76KX+VqYkdoVJ#Mp-%T%Jz)*7K|RJp1!XWyPN_gN@cy$;A)l+uz;}S_fMm3 zhdps`J)}Aa9d!6-em6^GF&dwKH-EInGQyMiimzC5Oxit!7Op<^8GJRQJ1RF3A%8{E z7Xi1xN>Af5Rbt$~E)fE@XAK6fw74UaX$@Wz%2d}*KiN< zAUL#WAUuKT@pr6s&4CSj$N!t<;#Vy!hpEmuE52R+c-P3xNT%QT=TVDBC$GKPRskSN zwaPKH3TEDZt*?kWsEYXI-?t8-XO$6@1WJXOX&`8Q9Q@6%4%l727^GQUL&BUch6s0) zVXWgp3*`YeWUv={WeOzVm(u8*^7dEW17y^jBEV6Qvb29%J9^bsBoAsK#Qs_HX41&T zw1{iu`889GW;#G^-7qn>aK0kv-Zywn1-!0p1df-9fVq}AW{sPa^PPKKqc8ZBY^SFR zj>_sSo96plup^^#Sz@wlLZg0MI6D_y4&22}d3j1DshaOJhDv!@Jx;Flbb=n&SuXnif+&OjJ!>2FBz5Cvv2(=k|xUy5rY3(MW_}pYCn4g(+DQj}1=r5F! zA>ozMsJb204?;zhRuP4uswaWz>&%czzI)1oT-nj(WZs!?rCO>wF1 zoclYYKyoNGIoA&bes{eVAm{?SP)P$mkGv~i#j3lAyu%b2xo3^e?o#huZg5Or*Ap|C zHsk$0d1rq^TEAPP^&4?sH}>4SyFbEsMz$qo*qQzzCTs$viv9(*0?s1ow}W;&v}Yu@ z!MAn8O+5NW8n{TU8lKlarpm;QOkvNgZL;^;p4PHHlTeYE6DxDX4m2IR@5r#n1Btdd zo?Bck+^Bau_OFKvo*f(;aZ%mUCD;p`UMinUB+;*q`URbClSh%8fP_R*Rn5nPCWdAw zCJ8})p)M^fCyi*B7s7vnBml4)D6%xh6#kUB)+%_lh+Il}`EY|W^mb@#H6YcgZTnk` zJQli$7qh<<5X;`?lE5_D-%y3kn`Og;Sx$SqieT zpPWlhaYlfdv3j@`JCAuf8KNLu{k782q^Da*pUn$G-!_jS?IvLnwM#Pfn`S&)t0mO(8 zKGqb^dQOXe&(0RtcZW^^(It(tUZ`Cu@mZqk16%1{| z4FaTfpVEYeDlDNZRU65O6iwYq_R9KDmvrZ2*N?G${;oTpGa8zg66#7#dxP0e16qR! zgz0I7<+rmz_gDL>BAtd)1~L;6*B$4K-O$yER9Eu_b5&HN)c(#>yRFN?cAnIkN_jVl z$Cn|VWbMCooHt(X0RYV<))Eg^D_h7^(wFX192V(4l#^M|lhBd=NfzQV4RqR>Ux}Q3 z>BId37cvLneRr`K{yz(4!P z7`&EpL1+&}MA|4p#Pg12v)bVacPB2hP%o%YOp{HLMsY~cYnPilXRg!Y77J_@nA8Wm zgemsgD)eoa`XT3e7sbHYp~Gh7??&@-a#y3}!UyFm44K!L;9d*eayl$_$ib3=O6dED zrqZUV$`48V_?$-^x2FAtH3X_wXD=)=hc4`%9h6>Rxx0f^&$~8B4iCTNy|zqggch)@ zm{$zKVTCNGWzZuh&m0r5)11C(s$Ojcexi^4>XH)GIZFuCz5Hb{50%_FB2|0NW>6UU z1oW>j`qUIb{lQt4p27pdLW(OEsaFT}j5dET>~H+@a(2O+1^9>f#2nSu-gLUV`AbfC z6+#7!rM&`-9RNmN%I*K@nVzQ24Q6l#a*(9{vW9Qeoqmt&ij8|a>PptB>hUT4d`u|X z!{!j1LP4(}&;}Ou^~;isw~|vqF5uL{X1WN?u&-=-O3K$EQKV4R;p_;BgU^>kbyRxL z-4v2CF}qSfb$KdIR&(f(<*6S!v$y+fyF}@6G`|7nXNRs|$jTDI=h?3CT?W2n#m6?6 zQl>Zj=NGpRS9sm$Fa9hpVozLM_ktLL!&hh}d?e%}HIFZWL!y_cK+Oevq5g%Zi^7YO zQ!wX*Hbe7eJF^SWqKIhRk&L-sF>L2~N`pC@UsxwntrMLN^@|&#qf)(kDDugy?xc(b zTLqZ8NfK-6t&PVE`1|!$t$1Su)14>GRAPLe?HB%6zIFqF?$BSYl6yu^Dm-12ne)nBkw7y*mZzTli+I^UUG+Yb+k+mzH66RHZ+a45eR|&uSZMkX7@`wj51Hem z5mxK{(G|PQm)B^OxkuS4X|AMOvo+kCkx%qJ>=&Cl7sDN*3zbejN_z3`toi(!Ah~k< zEII)$u53UEg%6A5Jal5ln>nsHHV@9>^g0C3SUaTSYe}Ymak}RXTUoCnVjOy%Ewn2C z1neGov#;#fGUdbxy)$wxOJCzzS$bW2Z=~gYIU&s#l&NSbnaY_fHUD~BHGf?&2XBSj zXO0$;brP8ia#~mq8d=a;;?lO?nZxrQ3kd}=s>{cD4#sz_D%Z-ZDv=K1wVjvR0nV4a z?zlAk}NZ`IH z#4E@>tssAdWz)se>~JjAaw^biux>`b8fcL*XX@JTRE{?=Ycv)0<~tDlBHT^&go|c@ zlwKZPezGSd+`%d_wHIG4Y`(M@DAIoaBB+45y5(dw97k%h|2c|&9J1F&`ZLEW#b#f&?9Zs#o#26;+OlYR7GZF z`Rj--z7(T`MwMnya!AqqfKByic;E3wZ@7W zZ1d8D^Pmp6+@;0Fe0gxDj2kxV!*Sv%tGY!fB$QYEUs4l&HQ=c~3wk8;Va>Y--0U;; zN#-4giWCj&?*q+%j)NC7JOJ3nzuM_4OB8Ikk*8Fg>4ejPh);ixSGXvGj!nrbt8UFn^>Z6f{?ts`>RZPYl~cN#vlDz5{M+9> zhr{%9TW+F1*q96{=Tx2-$+5lqSJDU?`E1PnW5-fIZLwLJURte=EQu|z_EA@@<1 zFI;bONqC6yvR0d&AJ1QQtG2y;%CL_VHK{sRqARP^biEjInlb0hadZJAsNdr32sLD` zk+?&Adj9)W((#J0?2biy9PkT4ppGcOQMK9~#CcfXv=qR6r$MWuy|t~ay{+{Y$)NG{ zdv>%Y-e2}%ae<d!*& z<%Eo(bk{?DQbI-%djb?sZ-+8b5m-w>)6Ro8-H^_M9v=;nIxQ<3-tmdpxfGE4BnXpI z2jHHzH+LyVViEP|pgAZSNaWB3vfVu-KTZ*XV2$Uq z3t3Bc-v_wVSMt=W!+>$q4nBaC1~MjlZtT-08sj|klOd$@LkPRAH}y5A$6DO{Rd#%B zpn zK~CrYJ%z%u`@Q|V!z5#u@68*!P;$9E8H#D}0yH9E+k`?n^8!2nM&tjv7gIwggrjUc z1hifRi(1!0RwLuSQe6E)A5ID33^Xg!nhS{_|BOjfyt(k}(bePB$Fy+6~#- zdLd0-A|1@|OZ>tk>xr0|`XTV~-dw$=Fs(BS^rNBnA`hnV;X{g08cLTCcC8n0K0OW% zIREW6=L9t$ocUrd*NE__6omQ<^k}(5Kv27MWCdVg6r2LU&ufC1;G7OEC)t7M+G@^8 zJ9!nP$H*-9PVHL?D)JfUUL9#>ka_A|t?68i$?$2c+;V_ws?K1mTz{H34MGc%w&^63 z#)Tzz6wa^(FF=cN8L4_qeh?))s@Z~{10I%e3GVg(Sz3iy+;KgM`*n& zKxzdDDIjKGeu@;Dt8;BwmsExIj*_#avf;CYA!jns(&O+c!6FblF3^hAxgZq+gH`5B zpyeC#5%{uSTciHhyipH|$p(_oU_cr<;{}CUH%`O0Wg+Bi5#C;GpMH9oQjOm6ZFVu! zc`w&Ig(@)RUzd6u$eig(i>T)HxHlj1Y%O6wk+FH9H7o0tC4z!{eiTq$PMv9 z19_j{Zw|mxd#~Y7a2~M>(Ibw@gfc0@HUJyh$4S34*UZh{LK&?b1)@0&)NHLoBqjb65ju|fKpq-fy;^k`cB(Z_FY+iHSG4Q zUB}Lty`Gl+_FMF%wcCE*m@_$f2gtUR`(b-6KC&@#*w`@UmC4VuRCanGz_d1b%eJSx zO1*ni|K2Ie)H#3CGUYxnNccCN+#dSr#@cVU%2w3hoo@v$bN5s-eRV3)UF`fPdS7(T z&F2DppJw0QxuoLi#?5DK-af86t-gJsGXqn82ZQ;Jd=8t#P7mZ*qK@b0p8g69{b^on zm*#K%`b+=j)n${8XI!l|yZJik_^R8le=Yo1TK@`^Xca!};M^j%(pysg(fvMw_>PVWYhyFVm%hi-Y;RZXVtcUAr#jTFt96{{`Ewf1Nk~?bbJ6 z*Z+O>YvbQn7FQr8Z>G?W^G;>i_ov+to4w-NzU;4C?MmzOcRIg)*A6V?=Scty*!efu zlw&#OG&sE9@&2Z1%IdYLuYX#-P5!m!XvrJXo6^;5<=;Yr;GMt=-4=eu4PuM0-`kx1 zChT_D?Be{N`I(zRIlF?Jqrmx(SdPmwhDKqb2y68O%iSNya6kEd?9Q$F)%P#o)=J@Fz-hNXarSUpd<{b)JK`4;V_yWMl-``p)gt^4#k?`!9;-{{})7RGVEc? Mp00i_>zopr02iEMN&o-= diff --git a/crates/zed/resources/flatpak/manifest-template.json b/crates/zed/resources/flatpak/manifest-template.json deleted file mode 100644 index 0a14a1c2b0..0000000000 --- a/crates/zed/resources/flatpak/manifest-template.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - "id": "$APP_ID", - "runtime": "org.freedesktop.Platform", - "runtime-version": "23.08", - "sdk": "org.freedesktop.Sdk", - "sdk-extensions": [ - "org.freedesktop.Sdk.Extension.rust-stable" - ], - "command": "zed", - "finish-args": [ - "--talk-name=org.freedesktop.Flatpak", - "--device=dri", - "--share=ipc", - "--share=network", - "--socket=wayland", - "--socket=fallback-x11", - "--socket=pulseaudio", - "--filesystem=host" - ], - "build-options": { - "append-path": "/usr/lib/sdk/rust-stable/bin" - }, - "modules": [ - { - "name": "zed", - "buildsystem": "simple", - "build-options": { - "env": { - "APP_ID": "$APP_ID", - "APP_ICON": "$APP_ID", - "APP_NAME": "$APP_NAME", - "BRANDING_LIGHT": "$BRANDING_LIGHT", - "BRANDING_DARK": "$BRANDING_DARK", - "APP_CLI": "zed", - "APP_ARGS": "--foreground %U", - "DO_STARTUP_NOTIFY": "false" - } - }, - "build-commands": [ - "install -Dm644 $ICON_FILE.png /app/share/icons/hicolor/512x512/apps/$APP_ID.png", - "envsubst < zed.desktop.in > zed.desktop && install -Dm755 zed.desktop /app/share/applications/$APP_ID.desktop", - "envsubst < flatpak/zed.metainfo.xml.in > zed.metainfo.xml && install -Dm644 zed.metainfo.xml /app/share/metainfo/$APP_ID.metainfo.xml", - "sed -i -e '/@release_info@/{r flatpak/release-info/$CHANNEL' -e 'd}' /app/share/metainfo/$APP_ID.metainfo.xml", - "install -Dm755 bin/zed /app/bin/zed", - "install -Dm755 libexec/zed-editor /app/libexec/zed-editor", - "install -Dm755 lib/* -t /app/lib" - ], - "sources": [ - { - "type": "archive", - "path": "./target/release/$ARCHIVE" - }, - { - "type": "dir", - "path": "./crates/zed/resources" - } - ] - } - ] -} diff --git a/crates/zed/resources/flatpak/release-info/dev b/crates/zed/resources/flatpak/release-info/dev deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/crates/zed/resources/flatpak/release-info/nightly b/crates/zed/resources/flatpak/release-info/nightly deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/crates/zed/resources/flatpak/release-info/preview b/crates/zed/resources/flatpak/release-info/preview deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/crates/zed/resources/flatpak/release-info/stable b/crates/zed/resources/flatpak/release-info/stable deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/crates/zed/resources/flatpak/zed.metainfo.xml.in b/crates/zed/resources/flatpak/zed.metainfo.xml.in deleted file mode 100644 index b8a88d9221..0000000000 --- a/crates/zed/resources/flatpak/zed.metainfo.xml.in +++ /dev/null @@ -1,85 +0,0 @@ - - - $APP_ID - MIT - AGPL-3.0-or-later and Apache-2.0 and GPL-3.0-or-later - - $APP_NAME -

    High-performance, multiplayer code editor - - Zed Industries, Inc. - - -

    - Productive coding starts with a tool that stays out of your way. Zed blends the power of an IDE with the speed of a lightweight editor for productivity you can feel under your fingertips. -

    -

    Features:

    -
      -
    • Performance: Efficiently uses every CPU core and your GPU for instant startup, quick file loading, and responsive keystrokes.
    • -
    • Language-aware: Maintains a syntax tree for precise highlighting, and auto-indent, with LSP support for autocompletion and refactoring.
    • -
    • Collaboration: Real-time editing and navigation for multiple developers in a shared workspace.
    • -
    • AI Integration: Integrates GitHub Copilot and GPT-4 for natural language code generation.
    • -
    -
    - - $APP_ID.desktop - - - $BRANDING_LIGHT - $BRANDING_DARK - - - - intense - intense - - - https://zed.dev - https://github.com/zed-industries/zed/issues - https://zed.dev/faq - https://zed.dev/docs/getting-started - https://zed.dev/community-links - https://github.com/zed-industries/zed - https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md - - - offline-only - - - pointing - keyboard - 768 - - - - - Zed with a large project open, showing language server and gitblame support - https://zed.dev/img/flatpak/flatpak-1.png - - - Zed with a file open and a channel message thread in the right sidebar - https://zed.dev/img/flatpak/flatpak-2.png - - - Example of a channel's shared document - https://zed.dev/img/flatpak/flatpak-3.png - - - Zed's extension list - https://zed.dev/img/flatpak/flatpak-4.png - - - Theme switcher UI and example theme - https://zed.dev/img/flatpak/flatpak-5.png - - - - - @release_info@ - - -

    Dummy release to keep flatpak-builder AppStream metadata validation from complaining

    -
    -
    -
    - diff --git a/crates/zed/resources/info/DocumentTypes.plist b/crates/zed/resources/info/DocumentTypes.plist deleted file mode 100644 index 0a509d9ddc..0000000000 --- a/crates/zed/resources/info/DocumentTypes.plist +++ /dev/null @@ -1,63 +0,0 @@ -CFBundleDocumentTypes - - - CFBundleTypeIconFile - Document - CFBundleTypeRole - Editor - LSHandlerRank - Alternate - LSItemContentTypes - - public.folder - public.plain-text - public.text - public.utf8-plain-text - - - - CFBundleTypeIconFile - Document - CFBundleTypeName - Zed Text Document - CFBundleTypeRole - Editor - CFBundleTypeOSTypes - - **** - - LSHandlerRank - Default - CFBundleTypeExtensions - - Gemfile - c - c++ - cc - cpp - css - erb - ex - exs - go - h - h++ - hh - hpp - html - js - json - jsx - md - py - rb - rkt - rs - scm - toml - ts - tsx - txt - - - diff --git a/crates/zed/resources/info/Permissions.plist b/crates/zed/resources/info/Permissions.plist deleted file mode 100644 index bded5a82e2..0000000000 --- a/crates/zed/resources/info/Permissions.plist +++ /dev/null @@ -1,24 +0,0 @@ -NSSystemAdministrationUsageDescription -The operation being performed by a program in Zed requires elevated permission. -NSAppleEventsUsageDescription -An application in Zed wants to use AppleScript. -NSBluetoothAlwaysUsageDescription -An application in Zed wants to use Bluetooth. -NSCalendarsUsageDescription -An application in Zed wants to use Calendar data. -NSCameraUsageDescription -An application in Zed wants to use the camera. -NSContactsUsageDescription -An application in Zed wants to use your contacts. -NSLocationAlwaysUsageDescription -An application in Zed wants to use your location information, even in the background. -NSLocationUsageDescription -An application in Zed wants to use your location information. -NSLocationWhenInUseUsageDescription -An application in Zed wants to use your location information while active. -NSMicrophoneUsageDescription -An application in Zed wants to use your microphone. -NSSpeechRecognitionUsageDescription -An application in Zed wants to use speech recognition. -NSRemindersUsageDescription -An application in Zed wants to use your reminders. 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/resources/snap/snapcraft.yaml.in b/crates/zed/resources/snap/snapcraft.yaml.in deleted file mode 100644 index 4c94a9fd03..0000000000 --- a/crates/zed/resources/snap/snapcraft.yaml.in +++ /dev/null @@ -1,59 +0,0 @@ -name: zed -title: Zed -base: core24 -version: "$RELEASE_VERSION" -summary: The editor for what's next -description: | - Zed is a modern open-source code editor, built from the ground up in Rust with - a GPU-accelerated renderer. We help you build software faster than ever before. -grade: stable -confinement: classic -compression: lzo -website: https://zed.dev/ -source-code: https://github.com/zed-industries/zed -issues: https://github.com/zed-industries/zed/issues -contact: https://zed.dev/community-links#support-and-feedback - -parts: - zed: - plugin: dump - source: "https://github.com/zed-industries/zed/releases/download/v$RELEASE_VERSION/zed-linux-x86_64.tar.gz" - - organize: - # These renames seem to not be necessary, but it's tidier. - bin: usr/bin - libexec: usr/libexec - - stage-packages: - - libasound2t64 - # snapcraft has a lint that this is unused, but without it Zed exits with - # "Missing Vulkan entry points: LibraryLoadFailure" in blade_graphics. - - libvulkan1 - # snapcraft has a lint that this is unused, but without it Zed exits with - # "NoWaylandLib" when run with Wayland. - - libwayland-client0 - - libxcb1 - - libxkbcommon-x11-0 - - libxkbcommon0 - - build-attributes: - - enable-patchelf - - prime: - # Omit unneeded files from the tarball - - -lib - - -licenses.md - - -share - - # Omit unneeded files from stage-packages - - -etc - - -usr/share/doc - - -usr/share/lintian - - -usr/share/man - -apps: - zed: - command: usr/bin/zed - common-id: dev.zed.Zed - environment: - ZED_BUNDLE_TYPE: snap diff --git a/crates/zed/resources/windows/app-icon-dev.ico b/crates/zed/resources/windows/app-icon-dev.ico deleted file mode 100644 index 1d6367b78853393ab14407748a823898c93e8667..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 158013 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

    #{-h5M#f4x2;euryTOuxmq=`qgS)`|IET@rJwZ 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 MLdCD3nMnKp0pNdgasU7T diff --git a/crates/zed/resources/windows/app-icon-nightly.ico b/crates/zed/resources/windows/app-icon-nightly.ico deleted file mode 100644 index 875c0d7b3546ad22f626a3b1d9236de5ea244594..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 193385 zcmV)xK$E`!00966000000096X0JD<;09F710Dyo10096X04N9n011=<06;(h0096X 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 {${+129H{a~8L6U&rS@^ErI@13!+# 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
    !7-a?$%81csQzY%4@ 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 Mv-*%E4+a|I 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�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 z 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 vQ+LxAZXd!u=%{;b?>BIc4jU%T{d7j}p?>+1D zJm)#jd7kGy=cMe~I$N5}W=qGPVc2X-@#H_R`2Ugrmz`p>_3Yo!)Anljbtf)O39dVF z_@tYrxhtnmxpC@sx40)ZcVXJ88%9|!kVqb^DtlV$2U4Ku8?LRN} zy~!%G+jf$zM{3VAZQo|uY|oNXHrx19@t!SSy&!{pPqo>)iKN8SwlPN#_q{Pk+fr@g zms^$0x@N`1gVklTe6!hRre@o!?06~2v2C1#XLYvi!M`)d{MVRc+uak{5B9WGT4~A8 zzx1@_;XAw1VUvJbnGV|=)LU#gyBzDYLpM}~Q1r~5CZ5DyqHZ-{bAt?LPrd%S&~-Lj z<^Xb$!)Ch}&+kz->7N(ubha#JD=0vQ7Z?)Bv-!WoKd;T!cy^^MC~yR>wuQ*Qq*FPx zcM}Q%0A$qnmf_9*x?LK^L?^Mov_S73DPeDP_(gvCe_-@8C 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%0Dp9l9UHbz>0p)8oD$yadUxDri%u;=MC6@Bm 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

    yRH}+csv)mI&?`TH(E}=JbkD&r*opWh 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 z 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

    NEIXl`zB5% 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Π|LacluCr6q{xz0&OD)Zyk^8-S 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 v8Q8l!>9slo!guzf! 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

    i(vjx|1{!1Mv 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

    #ZnvP>G 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 z zFqhEZ)=c$ruT~KMG}}^}Bq%kxJv~Lfx{6SBPjNRFoHB^Pga8;TO$}&s z8POT<+k04JH%EEP!&Op~$&tEnowVUIeC45vDlwl;j!mwcB~^NybiU>DeYDTx7YAty z$G6|wrm4rRQ%&>-=9c@VN0t$dxtdu^TgP>py`G`R2*gs`HKQwsy)vWAemdujx3kqm z#dxTwdLap;&zP?VXlMJD5Yx6?E26)bZh6UU^>bZ*<<)h*!uoK4!6Lb2y5lv9VhqkI z4^@{l$PCP!TpJ!STrEj79ZL=Km6NYmC7NS-7<=A$(?Hs79nEfP#w635PNv|b*vZ{G zYPgJ|`BL7vKuLMsf~oQoLuU30k)z`(s)~IzfrVsu*jHPpOE6bbB?D1A{+(GTcVD-d zDM_78-bu>!pj=-|?>XNzOXdE0GqQGx+4$P}8VanQaoG`!nPdPV_1x5=!Z4L~uCG)Y+lxu&*RkURx)F7B#lL6ir2yQGdXedTm{}Tn1Gx z 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 diff --git a/crates/zed/resources/windows/bin/x64/conpty.dll b/crates/zed/resources/windows/bin/x64/conpty.dll deleted file mode 100644 index 555d6bf655a7cb0427b630f1052bb873c837a152..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 98304 zcmeFadtg-6wfH}I3<-~Tf-)KvVbD>cK}`v2VvNj?M9#oOqKIHchzb!EHH8^S1td60 zVLXm%HIa@4sJ^ zIgkBXd+oK?T6?Xv_o?~eN=KH%;mGD+JnnF;u+{fO`7b!>6Tfu&&tai;IXUz z^?kot@Ycc4Pfh%r*09g>GSAZ*4tQQt*ZrQ)xxT!t?$rBLzRymb$#qi0Ap^X`!!he8{TvsqKI!^Io_&s!-2*d^8s*r}o0ZnvRS$8^ zSNBaBa*_K?hohf*mAX2%lL``xvolGq^Cb$|W$l8gixiitif3myhEQMCbs3H-u8!3i zjy=-Q$_&SgR=Yg#*OuXEB7ODP3`c|Yrq^$iFT+vDn}!QB99{*eot&TH*e3)0Eq`YP zXEz2(`|mIKk@lqTLJVntZil1pta-Dp4qokW?3zOYbvka~dMGUuw_g4~%gW~P4&sGi zOzN7I4v@;%c2g>*OIP{2f!lDcZPhaM$r@i2qr?0K2(@5*sB7eN|7pNSsSve+J zPhVM2OO97*FY}bhpBMRWhok$+pF12@`Mh7s^CQV;=YwaeGB;Q?bTrU##QD{aRr^<2 zPs7scbH18ORD$leUqf4ih+Z1wFuLUssPmfIZRo_swL^b|~`)?&Z$svf;Ht{b10d}J<) z$KwHGbHM1-jhA&JS2wCWb$WPHhSXUoCEan?jLZy2OWQiK#x=dYrLFNO{n{?QeA~i) z#3`^sLW8873KT*mBP59n;!scY5iduLGnm0r5Z^+7hd zjDvb4!+1>(x96HSQ1Skbj9^|XqfF8^bI`qVo$e{tK>+RbhBkdXCQGfQx^YO5K1VOw zA)VN9D0q^->=1Zax=>3F#BxR~<}7Kek7VfOUxo%)eJP!JWryC`CP=KW*M>JMs;v(% zuFQloy&0kZ)}z^#GJfPs4Br;%k9*4HQ+=2Frs~V~*1`^S!+H5lbgX299%=S?dE+;B z>BeUP<5N(gHP=a=mQ7moRBlQ>s*jvbi8~hx!rFo-drQxJfcxO_0S3UJ6Px3%PDh5L z-dA>(*5Z+}(TcN9$#570TQ&vtaCb(qNPdnAAIu1ONt;Jn?7aPz*)k4S@W8tXa?z@K zV#DqCXIk%%O}x(`)4`K793?w_YlY?M(SXNmZhMpAprYVuRmP%0Ksn5Yay+@gfy)TRgjwyzi*vz$ZDgrWam2Jy>ZvLuR%<7%z}V1kx>h; zPy=o>smseX^zCFRSgbSG3XMvyR1%0f{|JzN<9Rb%hVVF5b->s}!r$$L*w=Apju|kF zl~g75xJdmAscQaona^1n;;s<@p!z9IlvYI+dx~XB8I@J4En^$y?oE{2YW`Hpg%=e# zLPvAa9if3-iXB?Bv_sb4C$e^#*O8TO6-etxlk|&JQou^m6G``^l7657n^;ImsQqO~ zBzUZVCf&B+=rZ%j6FXTqx}^M3BuIJlz*oE-#M95^shg*gHr=>W(Vkwi#XRpWdTQM7 zxlcxR*#;p}-%Q_(nKQ0p4)3MGO_9OIbLJl8wlCt#_8aZKP9L0@+~Ey{DPo7}ZGIK> zw}9 z{PB9!^)+u)!LOuX#Q9IM#&RhB2say@W&CDHUWo7o-FQCk`iBf2EXZOFK~Jg890WdC z>0FDIx^?42VK6N_Ll^5te?jS(wKBo?Y3&)|o{Z3Ox-mTN8cCzTUb0hxFhW4+L`cbH zyh6k33~2~5oeMcDE);-U7sp*69h9-J&bEtwERV)MfmH953V>0QR?j>_tB z*QBRc_f3CpzFohnRp^CbY2l^xGPh)B+*SCM!?8~Awe|Eq^nyHrD+;eLoL=;;1XtvZ z_PFx~s%dW1TDDO*0NZx!a1J*QOib4@#%Z1Cy>Zv`sQ!BNq?u%IY15kTkqm~&U>H!b zZ!KbkOq<9ANVm$IF18u@Npg|SJnT5UbK0oKic#(!bAFcjP`i;;O4gAz!jN#e7!$xW`y3ZX=&4=6XLp7{oF<& z)cfWMp&A{~Zq&J&2Q>|)`%J^tcW`C;@tBk#di>ObsKCkb>@&YU^&vc>d0!2ax(>x% zn-56e>4vNSaG6Ew8R)bsnGMW7CdV&?f)Ae=RujgKWd}1*29^#B4q0~4>2)+|ErS54 zm49u<`i@q3gIt##IyN(>NxS=YP!Vj5F8e0lv$VqGL0kR?zwnyT@52x8^J$MaJBm+) zaTs?t<#P3Bb;aLq&9iGe&c2>+U&q*2CzzMkPO+bt+Sd~MdQOG~4btC&=C*8Lo)Gjd zJ9Hs%1&fy*x=60>WrxPgHGkQmd0azPt@AUjlGoZbTy0-jnW_F=k*INylX5pLJ2XkI zS1miVPOdfRZ&h0BJ45H^Xsw?O1w>;#CF8>@9F80~zqV$lv7XN5v?0l>x06mne@Ez~ zmYuhqM&h#Pc$9WZ$k)*tUS&1eOvkLWP-C_|Nc42r~mJ7a_t$=oBq9MPAGK7r*T&nT6{}e zNK}jB`x$D{gBvX!IKeZjOE%dO=y~%+C~NKgBJY?7wW91!gEx5(S0A@1eb+0Q^tkJH zpQ|>`NVb76DNM2?s;@3-E7@7H$y|`Ay`}B8Ty0I8P(+i|pPbi;37wKhqTjox`Y!WL zQMw`;u$G@0KseFC&W$3Jd`|xXs*f8DLSiche)|aN= zRtp4zRZupeP?&*gI&dQebaSGZ&c|u#zr(oI-S<# zKs4LxvN35InSzIPLAUC!lceMO;42UvqYC?@qmK1wnKjWh_W`TlILe>Z?a%tqpB2-y zV*aeX{;ZFxOFq)G=7Lo!x*%MZ5$f+R``apV&o7f#ryCoS8j0T+7I)qIsVEW&-QaGX zY9eRVL~dsJ_mZf;t^@J7G-SL{V_cL=6?)W{jTG9Yx9rxMFL%mlPc4`b9hFrRU09%( zzY&}vOVcartIEy@PNpZx^-WdT+;N%K`X&(dsQ%QHZ$&m*uav+481J6H97o|Vg9VwW4VWCmnK8fWJ6Wy8NnL`cS_z?mCnt~NYIba$Fq7- zhySn!i)pYpHc=Y%(4o+fxN8tov(s@b7>m1lK65x?2}oiueLEzo9)aW*f#e2(WHyj| zFBnRJp|m#)D`+%Id9Rgb{1P+*uRFx-@e|L+Bh4lH_aSiXWJ60n%uC-JfS z#c#)ls;BSBd@BqrU=l>|izZMHh07&&N&3jlOM_%aIwY5TJ0$6~{V&I-0h~1S#)-Kv zg_9S#PvWHWg>T1+s^|Z5d>VnJF*Ye3E7Q|p(bHim{|;DGJ;0(hF9yv*FALR#{gEOV zLA`_LKJ)d+lV}=1P3w)I&QDSZ`aAbY1pRsEw61d22uwo zb-h8kAPtnlbWl$G4p3A*0tyUyXYfjen<;{kd2p}=VxfVZM8|}b5G(4?%QwzDSBHcX zOr-PUwElA+8yGM)3OoDrj#U5ktT)rxSy8NHXMkGC)V@`?>U0zdmVtGmQp`c*yJaQq zyUI7+QZ1DJqL#)bah3qlA(s8jcojK~Z6pITSAiZ`CxU{@DlX=$@N!woMxJO>Z=dLr zi&Ed!QZu1SsH?3{h^cUGZ9b)M+(PD}JuI)@{$Xl)MT#C>C-s%=*2{)#Eq8Mpi24dz z9V}>NE#x(QAJHxvPN}TCu+m^;^C=UeXg#9l$SZf;71>LPYxiM|!zy9^dS5)=+!pFj z=T;rfB7FiYT&n_vkkqCsVwsMTQE1g?%ckv8<7FPVEE&&{I*sEPMwU+^xUe^}e8FTP zkh4~5ULx?`?~y+GjVlZM#(cNmc(>;8^##!^y-rR zB5TK_WbKK%@jJaAt@~{{2 z%Ep$m7rW^=>V?&H)!lFH=7qfgE#*m%{H;yei~Q~1chk|U^dVCfQ)r3&&F-cv)lG|n zQ@s;f?vDI4ZLKQ=4c-2LrJ5i%hqdq5Sz7-10&Q?* zftEk1P#Zk4kmpjKOL^9L)_I=B^E964@H~g-MxGnB{JU2Gpf=c0m;B|zI<&!IxuD_S z{eZd-Hsq>Der65F&(v@j7R?%Wt0yo#SdWCQ42Hbg7nc%kR>3a07|Yd@akqN1OIR5U zc_k%ep2}>qc$0F4D5pr;NZID_@ZP!_vF3wC#ev0Hsj?ra~`6M2XjL(kD1nn|K>MVmM7W{C#NpdTx4wlHCFLj%4#ij)!Po`0pHnJ$!XVBNmLB_{b#veVaRq~c<)Z=Zp3)L; zEPY1S$zs-)>Mi;Zmk|%Es6pAz{g{0p9HN?_=vznxX=R)FUW`iexp`7&l|S6LW{+?WQs?ICFPG)c{ou#A`|-yRcXUH#!l^zWRJV*$v}~*_RHgNZ{V%ajeFFns4SIMmQlUR zon^foZ@<)VmSiFtrOIPSDx06Xc$hEzRhHHw3d?1jw7oSnms8=m_Pr+EluMtS(2sC#y>giI>wh zEoO)MH&uK(omPSv0i0gD)||NOOe$EZTnjQ97(kNR5WWTTcPL`XnPJ^1dvKd%Hk{L> z9vC`Z?iY8x17+|s%dXtQ;#T$2Vtq2s;!zXw*9KR4++z3Xk423u?ndjS*oR6J4_>j| zcysI|OeVNau1pvmPn1ZbVwJ26 zCDLFPWzDxu$s68_e%S1}Pimt8qCG+BVt2M_T~Z3o)r0$>N6I%=8K|7ELVVIwX{gSe zBF0u}i8o?=o!vhJ4zbg3Y=*Zv3~w$KPSc|z{wB%FaN zkETk|mNR|x=z{n>tt zMD<$&#_PT*Cwx&4tV^JN5z{gE5@ z39h(VwC4%&P1HoL-DSK`6Pdp~P}EivS=5#CFYD3U4}D|#7mo93YqE3enGJd3m=3pR z>fsMFLT@M~+4l`fa=WN`o9iQmsvTudsV^%G4$#AG#Jxw-j!_qt+du@xW+gH zVhghz!yW$iPlgAiUf;5J7=@*JlNQv z=q$74L!`+*G`A~s<4(^Cia~*O{fJ32x>`jVm@nYzG&XjhL1l4QKOE71BgFjNA3P=Q zI`D=J&hv&DcTFcnTtYb99mdwU>q(MIKB|e3*VwGkR39#N1p5n${6$+7hSHEznTDBN z!I5ZwKLZE}9%zXN3qZ>N84pdyBfd?L;xBqV^e);aYZ^5o;H0`Gp1_I1hw8dk{ejPT z1q?{nm%ky z`0G;7W@BI6HH#D=s}nBW#cO|LNiHmk=`3RqKub_z1}GDA)bl?))41x!Bc4WewVc7C zY*--+ti0qsE3W|!T$gQ-3E;y&Y!6TC*8V_t8<1@=yX3cb!fbgZf1>|bQ7p`!NdbHI zD76DS{;RU~yP6>j_rzuWgx{TUpTit!{iXE$n=j+>?ka{-NU%V8*DY`7wVRE5V$g};{y{~{HB&RrZ?0S~t<+EON_5EvR9cRje<;iwQg z)0%%v&es2u_Wr;R4SA#yxIP71tpZ7?AIqYVN6BXD(g-egM|iQv;|S)oKE{1)RO zjBqo5q_7HGKor-jUetwp?~g7^@UUYOQwA0WOIn}l7q;{!(F%l0eIq6WB7#y3%lj^(-Fkeg}!J@V`}5A zv55{Drx*1Ei|`&lEl!)F=kPlU2R$Bi4HmwZFM||+UdBlef07Y;2d<~a$r>butR#|l zz9vXg{Cjv>2OlQRrxlDjOdq{7dW=tEGo^hd&GY8R%nYLkURRV>;>VN_lz?=HxmOv# zU}8VprDnYzds?}$o2|#w^lO=f`n6EK-v0J*ePEYAqNz!9t(BpudkyrGsNLdHyQov~ zCt-cot7rSZdQ4tf{A$YO)&dRX!Cl4Due#)4#<*PKwIV)#KMX{!&bM83+M03R`iQSU zdpvW6?*tMU{@X=f=@B0*tJ_7Hvf8;UzwtP6pE)iH${N(j&wDS#QOVcYR7olR;_n7HRSZ)uhC4^lndQDv3X- zbN$j`3pT?F0`y0R}^6N z)Yka8?Zz_Rq_@AHt!I6%m%kY-(;*aH4|jNVqde|9e${i&X{DNxit!3cDSw={TERayTa&S1Yhg|JRB_DQ1_x?R4^ zkiB|YV{YhuJvuR47o$-sN3uJKc)0ZVUH9lV-$r5G-74-=!WW=htyxp^=+b<%3w2pH zmePT^)_jl|5gzLZ4NjU-{!*#TC^_NVYI&{Y=V*g3s%3RGDW8SZ#W?$O^v-swh~r(> zjdoJ!BvTtzyi=CMJ7w*%CWDSF)9w<+v@vVC9!G>M%}uR8nid0=9`)nTht}vh5wS?4 zFLZ4jtbTv+a?l$(D(<=y^jh;JRw2ac(#t=dhtUA{2{A{T%~@-iL#GFfXO&K4-un-0 z7HuTk{G;Qp8ddycfQY-wxZhYcEY8*g3#Gkn#L1GkfT}$sBW%A1DGcgJQo{TsCHM_RP~!t-<-u{j73T(wt(YHm1LnNV z)DSfUfMo|F3z-BbYQw$I{6q+Yj(`Q4mU-hHuunHw3xc?}QG(x@D8RJ^M|hj}hw}H2 z$3irqWD`6tdPRQhXOBuz;w70rE90)aaDEwi)9Frmm)6Yo7{^l(4GSB54taiF$J@gP zv$ZJtt|Q_c!wd6aG;d3H(VE*NZ$rKwZX`;zlF=L6x{~r1>mTGP3lXwx1+m^hU8DWRCT$I-yXX+u$UE8otQ~>!=Yl6z z8I}1}WxnhMxyJb1GGA`6f^`zbp9^)9{p5I?wq_zep97!PMHjyad~vPrzEir1-QB~fE+M`v0^C#sCs(9~9B$VJOB0dA8&=+J%XsK@3=Rhehxx3zF2* zy&fZvr7x&>?}SA+`iRGiDQ|7t5X9@7tEr@D4?1y`5b37@<4=XWFNxPjo@%4Kcg2!6 z-S|y0?~H)}U1(Hh1Ij4#UIwSiFag;uwj^WQS`pXbMY$PTRhO(o)(IU56%?VE$J=>T ztQ*l%DiR_dP+tZ+XEhaaDi#`dRc(htFn%d*pb9HYT@_mDEQ{xhwh)NSen5|eR?C|? zwhgsZnA`ao2xPOnbaXY^Xm$byqgjPFPots-4BY;XKyP9cro=&8;Ie z*D|77?bcF33}xQ=$#|=qBbq7(s%!<88C8$oE2CvlHgh@5P=I}s}34Rg|1u=01w`jAB; zPG_MOw^A>9JLNt>S4TrR1`j46=L5AWs zGM~Y=y1&^Ynsi3!1f3C80I`G*&7ihr8w7IMr4qf<(?llouVQlU_2->3S-G2JI|{6+ zSXk3`VNI2fP=o64I`M5ve_PsMdNsyAB~Js!Q^HM3cGpLKBjmc`s6|RO)t*4{Hr}Pw`IzGv#h!u#t?q}RB zK1GD4#inKnn<5@leqd8eU{gzAQwW0ZVpCsYHWJasY`>4$uG?(tDq&OO?X(0#StuC{ z=N$WWUp95Qm0|2*<;GgCr`fL$V^bqYEn_UT=pZPf^rT18<3gv=&VAenXanP}ubC>^ z3RT4vZO4jRC#+YwOfKEjitJkk|iTFf>9On5&P1vE4eP)AY`W8KhtNy(K>+N*I_8;r^^JvCv&PJ#9AtNzxuzK(c7su{;+;dWvd1f1TD$TqX#ULR|hQZ2;aFR6$ z%|2?9HNc&gIh0OiNvD|dD+S198w6I_G1DPdJ*9k$_ z0jWGXsD<}R1vt$gIm*bu{hEPF+_GP5K2Dgqn1|n1eGFdQ?c|LpvtX#$LfdW9Ia$^y zm^QYK5rn21-$rl)rap5#?ZzHTB$)T{sO&2Of9oq^7@pnhm9iCIDzB{gYIjpbrC^u= z)mqrf!;SNEuQe%HTbN)`c7a?eDR4?6b=q{clG%7>d)aAp!NPN%jprKjg5RAoylEa>FNpTp`ct ze=@a``zHzT2}rpA!IJ&0GQrit#UCj~Mw#JFLe9J3qoGp-_GwIcu}qljamQ}%%;K$7>o;nt}zEibgn@NQBY+8^^y( z`FBmEa$bF;azQ;SuW3XAcvNkJD~San254|4F+jxr5XC@1wQjsAWSFOB@1hPNsNiLK z%SZAClD*z~GfKD^kMwi)@Zllt=nQ|(>>;J^6ekx zEq^j?+?MeudVQ^!F3PtOJI*K-%hW6ty$klbUpIzH`={&1mn?Q!%udQTn+P9?yIxx_ z@FWaLFY+WS;a37gxr8_7>g8XAIwbhbE2bpOJW??j#t^l^jh?3B1FT8m%paW;3~k_^ zXsW+{BpQmv(&1u-q7MQWB&3E`q66+Dr&%xhM$|b;qR}EJt*W3Nk{(c%+~Zj#*U0_W zHR?P_w4cD?mON|iwAEbAu_p-b8wt@@n0Bb1B}}D<+Ep8sQaLQMO$s76HebdSVaTIQ zvO7JyxP!Z!*Ayn!kGaC6U{H%a1yhodLD9LMeaJ+@#i-8d`hnf`Y9bH*RTY_qCKNsM z6?U>gf#T0EuGenK)94>mj$pxUJIf=n4-VgNd?VG{S?2BHv4)u=EE* z4maE71{GFbN2Vt#o|FJB*ePcM2)-|?kh z{<(I0d$x4zKOnFfmkH$^%30vYtT9F5jyVBEwzF&8k?(NK zE5bX%QqkWd4wkT)i#cuhf@7t}_!AQwg78B_8(Yj7mrK{F-GymdMA{&#q~Y2BlAk?s z*Pq(RV05u%bdMgL@~^n-b*MQAQz^3B}o>4LDCtOabnDr2^TWUzeHGLmK z#kZE>aKw(~aT$-Y&(O6}z0cx(-%+s!;_{K-Zy+uPY{7KZcMZcxv3n)!;s5D)Yyr_*%;%YmeQ}bc)ZrfpUWHQ6IZYubK^&L=zXfKj0>&qtL5QQrXAy@8 zd2=8xGU#v$#wW<|+!Ptsm}yja{1K>7TmBfdVtzPTY|YqgYjTDEOss|dM(_6sBVjGf zVG{FM_Al}(52EEOATsM-oW!YC+rFjMKOCXe1$XtK{kmst>e9nY967o+ehHx0^q_-q zv_kk`Je0jUBbGV3jb4_@q!k?9J`E*j~je87{}~k7oj}B%5sa z*%O6qM zjz&m|+;9C)H6=a6Vk0S8veWGTjg)=(X)$o0tm^p%_rflQ#j?4$>o1CUF4QCEOv7iJ z7%A;1E5z@ZWE$zCrQLp=r1j+++D|Sfp%4DtG{;~(XD&U38<7>q0>?_zTPeTi^PAF` zh)-gL?~gv*BTX#ZyHDZL`5y5L)|y@fmu~!1V&IJR?Pk95nuO>!NJ#1&R^xSQZEz1v zq+Sf>hPr|O+|G@JDIqIPhn>_!ZrmP-Mw_S%`6Gc$GLT4#CJ`5ngfew8uLxyot2Xu` zWAuPl-HrHZJi{?ch+OZiUQtY87~KkzR}v}f$nUIPHI6H_+=tYven4baq4`7R8XjWe zDx$dnEeoNA2S!i#7GWL4&%aA$k=fG(1RF5jsMe)^ULj;^;O(?8FbrXz7|=;O zw42{%!$R|3DppOCt~|8~Y?^4DB?h^$wC^^ryzFY$R32dg5N!sl?%}hLp3qiU#|}JG7OH;hLWQIg=(^ zioro3T5->Nf{K0NJq5n-`y5q}8ypet$qybA?kT{Ze;e5F8!{aZ#_~D3<6NFa6jXdf zeMa8!k_^Xe-N-(58jZCg3u!UC%NUedmUkyhyxYpCBFv5X=6x8MD;9Da4|xKSQy0j~ zD{zdH;j_O0T4<8=GVgMEUrlD|6yamio}j;)h2}Gtd9DcnYy}PE6RU@}3S2d!lV5*$rR`}-ul_$E25+zu+tIf{p z?F~F|+k<(ju3nqJE4yC1txan+b0gJ*q+@!dWu>%WRC~F7fLqqlYvsj~I^so?C$>{@ z9px_FuU-h_37u*!NS;-t6-6`5`}fFbZZp5ZDBJxflB7R)NdeyqNm9|aO*B9aLsR7O zH04^ckU7kMQPSv%;rp#2WGX7aiD|!>-AP;ueZ&Xgga)Nz$td?$^9iq43bXSYV#?m@)&ncB$(hJ$uyk5zj8JbY->l2tjuZ zGZ#f$7@(M&7<@J58=0=CeHg{rQH4J2H(nG~*KcfaH@+? zO{?c{&4iD~==%m&csVJ?FU%pPqPNQSSuGl3; zvu8Uk8!rUt8!8#$#?Yi7t`OpMo5Li8N@7E?0VJ`sjv7QNzAPwuawn#^{{G0h-q9cV z(3hm1^yw4;40+jWj-AI#Z!@vOAT4;6ZX6}TJb084aR0dLcv1rlt8p=l8{Vq}YcM)x zsQ0%)#24dIPD?O}Zwy3k%a6N0mOD|R6wi?NMo0J9Bq5*AT}pLPcZu^BoFykG$*#0y zH!{*E%8Iz;ee2{THJ?`19=xi)WoP4LiA-*+;rt%uXQ)S>jAoLzf0*xE+YfBYi!o|p zR}T^`b3okn<)dmmj^|t$b(cD)fq}^K5nr#2BBerL$5kr=^WytW!uPWg>G1jN)7>^1 z9RSuQLDl_LNS11{Wha%16+*kWO#uRUk}#U1X#kLY78WpAYcA*mOd1?_%knGS>l6v* zH{|Pdra=RWLdRL~Fwm+4ms43 zH$CysR${)CjJqD-DP_U?y`IcbQtjgUP+dyk`kY7wSkLWid_VC?{J~X9s=XWOoba1>i3_)P52WYqbvCO|d<@%2Q_cbL8E>SYvR7wb zf48txF@e%3!ay85NSRuP2ohe^$t#3~I_5-c9-!dK5W%K)Zlj4^Ce~E1JMQ`|G%WhF zwu0~|fROqsq#kX#n6Ej%Q9{F03+|{mtKk4zec5nXad%oP?oX{|i+c1O=sFmZW#wJn z&Uq({>;hKa`yY|oQ8N-N?=RE~vCd7%o~b$6!_}Q%Acw=^{CO2xZzG@xv6v8zLYB?M2*DAG6LSFC>3L@&y;O$tNR8)Z`H4@`vu zeLqo*J=h-}7Hs3c;;OKLj?6wbd5hy^w0irdS`M)2_G#5$Yt6EaK_)UQBYUj9GP%)G z?a_sgQkNQC7Ftk&U=Jde(QRWw$mlly*c#dLXSL;{7*46HI8eSPSmHOvaMl3_9>iTQ zJY)ekF##M>I_|2rfxDMfe`Kg9hDbU+kwbxh3QuY1Bs|RvI;&}#O$VcptCX~cUh4Zo z>ak!KWZ}e^$3VOVp z?<$^J3p~`+C1g)QrPQJO{N)=LUKDal)eCJB=+WeC0jfG%+(l5qR+g^DF2+y6sDd+5 z$H(#{?rYgR?vIR(EzpH4$lp>`+Y@qJtlnZSIIcc&UhAS9j0>J8tmADzk`3JUyL@bH zPbhZ0kStpDo5Cg=8Nkl%qOoo^HN5i1=VFP2%+}Lb$}jXJPvQacN^qrxR`{x(APPkj zRA$aaMy5s=ej9Z@_yR>EnQA$+Wqt2tD;`(H7yFF@-56lJ;5W+Rt|>Ab815VqD2%uz zFaJhOt)~e|Dsdq~qI*#%bW+|izaNpbl3!9RbwCLaTc3zJCkhl8F(VoK*JrRxNo2V> z<3f4K9`xkFxH7w~cn5-4p}pu0ZUP*PZ&;z}}hdp0Yw4L)52W5%&bOM_l2OV9{ z9h*=F_~ZCiWD^SJUiLe-aVW?)77=o=B7{{R`GG&`kNdUiO;n% zE$0K3Zd9xlB^hFa?q9;lB6MH#=X9bQ&y&#oFGvkUDprs`cCk&^g!Qr$zqqwrsC|G% z)+O8vtsrNepKrBjy8Kz8Y1zF1O@B;9>>Eg|f23MyS~j~wHeOk8$(}1h^wriKDl+fS z>rJpahOo#qr9r5Wtp{?YFU$6BXS}r28@ja98`oB8r+Xhjqj0!vEycD?5uW=gMh~^E zxxPxf#Tnm;d9zB~-VLl&5m$(JrE3VqL~B->-~2>6zN86@NRu#{TCvkMsd2}K%8O|% z_AKkf7*;N}AVM3$%8DE!Rpd}9b62fWXNqmEQ8_NoOPRviugm}?{AWqe1>yI8{AYgr zXWH^slo+$PT$)hrr3sJMpjuXuIZDj|%nE@|TmE-`15q!+Zpum>-KWlc+i8KJMF=;g zqG%6USST}CFKnA7XNE6{}_Rh zKn;Z`n9mfLm4Y^1*pAZf*frQFUK3l>L*iS5g9YiT70JeatJsuM45lTP#gyK1JU;MGO?N7ev=dyH$a~j>Cy!{f=P;K%P*!!T0X(T+Tb7#-+Uq*wZSlH zo2cymTk6KX;1s`cIr}_tTb+{_ea@sy4E#{b0pi1P+QsH%SsgL(?FvH9??Q$E>VG*$d>N2{Ky&DTqX6#kdvEkPOPuc)DSfBTT=c}#S)q3izF z4Lw1B%^xUG75Q6;wOdV_3(x{n*et}Cspb1}wZY?ZExDbL>9T&Z_5jTM?VdIk93$DCgvjoS6>tP@-1XS1Ua9@{ z^3C(J9}@@UPCedU&HfE{QvL&3>p(TJWVvX`|7YlUH2cJ6Y+CY?kXF$XHI-`HhurjP zqVy_F{ujuQ&Nx?IrptdX;kYQG#ky?Emp{beF4-AS-l;8@kTOvarj>l8hri5Oc(xw7 zf}MuG0+|o8$iNqIP+NmoaR}M*6BBO|4OzVj`MCiqQ)25mPby(|b*_tfzqD>04Pl-N~4GqO2`lfP#!Oq|a z5rNZGB+MM)v(wDk{Q!j3?X)LAu_HEIGL)(etg=N8&tf@X1coP>e&;${nB0A1z{Oi8s#tDkJqVuu#yg@ugbcr}X5?DO__u$egg?$9ZD;~lN|{LxP*NW*?~BG5XH7}-~-t$43Nz?5m(MQtu&-_;C zNA}MUldg`&eM+{^aHP|V$1;vS?cQhcMuXBnwIP|SR?KeGjeFH*ngIy6LStMuD$FR; z{JmJw#k=hG8C$HKG`IHMN%J;!T03dPBVQVK<^SfeSaf*PEJ?#_G>efV6dmPhwLE-h zD0%}GGsPqnJr8v1k!p6#;3Hmz4)>(!aHJ(d(f9tA42JU!`*q(?^vza=e;iisAJ*%w z_Uq%Sn?DL27LWcPWMQu``)2x?t|ws4Ri&q%ME(##d(LdTATVAp`LxIP$VQo~ekIHk zL~p2;(_Yn1nNarMg?sLnCeCQytwP%07-Ds!vGk-v+=kFM;UY4%&<_i!FgAlm*4ViS zQh$M4N9c$_>JoqPT|w%Y{xZ{GN*Fb4WzULr*-`*v!h7;1=hT$NgdEajlnNg?jS)|Qe*bPFA4V&NZ)SktNjPwS?sG+PsY3JvxNXv7C z^w#STC*~idH;H&`)jog7!Y_5Fogef!mThL;M(XDS(=#i4V2Y{ruG=ExFgmyrERoI9tMtS}bC| z}0+)6R2T{-8m0PcCw-IobQSZ}k+b3(=TGmo{G^c}`mDV%svN;;- zGSb9NTPJ;6LLDnb+Rj1RY746P5QHd`6#h4Y)>gGrDsvI!zQsIq6n(ZMdQoPShT>J) z1mtJw6XM{F+@cf;4LoejflxM6M5mBq{Ik)$yB zdjLx-v%4DtS9$ zjl0jS=<%Gjs!{Be_<%>0Xd3#{thxZ{v4Clr&Yn zgcEEG7^qds$SdbgHBsLBmW^fSy$bebo-|Y_=L7k*{)8W>^P?INL7JQ;A}m0eM$|Op z_}2;0#GxvKBTyt3ns;IP9K4!K+S)pVW zM4iJPkaAgSJ|G4p5?m&Day}3dL?F1V1d39@;3pgrLlf_jp4Akp#Q{~{__S5E(ZMbc z(2imtbK-svubC4vRS`iAy`@z`UZYs2S($s(JW5|>+Q{QVOIJUYc7`qi0o{WdlITs@ z$Gzb9Z@30+{4!3jDaQ!A#}GJ$f?T$Ic9{om7Py7Uz$pr4wKYJf(upNGmgifGT$Jhd z#gjH4%hDr@5cP}b;b!wKi(8_an=5XSe!>n|d`Z9mc#1r;!IMs>f0YCH*HKq;%B%GS zyZ5_*&m0aca5sOa`be8LI4ikxtFy8IKpgce{qeSOO&6eQvO{INz;GonNW?u z=_ATN;1d`6YlN1BhE{P5Qq)-{*ocgSx)}0=J)i@ozx^gre?lilIP}SRipnLYkkFZA zE@&p|?1|G<Wq%j$xk*3D6}7$VB1No{zK*H z6a>jw3{Sb_9{Z_uY@(`7zPjfxyCgSslrgZ3dO2A`^_Q4yyWa^Y0P||Vj? z^jD>~7Mt6T%weyP6DOjs*0*V2aoFN(d(#)=kJH0)Ua>CAwLXp)-s@#uS7q(*Y7>f5 zf{G9>>^x{fBHUJFh}s3)g>QK={ouIR$b)|waZ%!Tc#8*0%39cO9LtyI%m=O$EbP<&D%nrT1+ifFm2p(~8;jd7G<#wvmFZD|<9GurR*pFpLkC_eo8G z9Sk!H#HlPukEgVRhma06#s-2ISz(#?^W=|C6)k~4{_mqD+%ST1IOmAtFg|LJ!$rN? zDCNnjKYeH{{E7bz-IjS_9c#r19Xv)xr~^X~BP3i!eR>dibkNVlH~dUedmgvz<@*+* z5h>qj_77OD$mCC-&~Qr-u-W{ln$9JlN6ksEOh?pUIUW(?rR{frSthylJ;`LMcQOG6 zvOltvX_Q7B3@e{PLwlt9Pw~@cXt#5$1jNT8LJUTHW~3e-{-T)9EqXwUv}fV}4NbiM zEi~aq>y=q}^IWXZR#Cu;Q5X6)R$Yit{61uG?QqCo z)Dg+RYuIEkma2WJYLiML+)&VD4}B>s->Vq+jH6}TH_LD8Pw9the5Obsv+|kF^=xAK zxC%_5)9gOLMRdk$POu|RZlATIm|X!MsGXFvJ1wze9qMO^ovIZhI;&M=oLPLe_z)qh zje7XDf*g?<{iuu6oi0MGRlkMn?G>NcCu%e^A5Ohe<}+UfRXI_9Bqw`;d*%GZd>{F> zQU!=SovpyEW{-ZGR`s$!Yih2ZHD$P-wOucpS}>nure@~rMQT^%VD@O~k(rFscq9sZ zoc#lcUf)`5pQ3Xe{JV5`oBG}-UQs+0OGe6qa0;uF>fw^IO(b=AN!$?}C3GKkJ!cnr zT#4{0pOBcNihM#5EGXN|`z6H?p*a_p{+k9BaEbW= zwcV%XXUJNUX{_|IV)HycMu5@4x$rS+7gca#q3kr<$5^ja)K;lW*wOrH=&N-(v{f(X z*5i`!$}V0%gQ6Dxwe>JJFZ7ekf`0PD)I}tO#>QM*-<9uGs`K42xE>HtibXMJB{`A( zF)VchbWGMn<}22Kh{L{*w}JZq4eHeFVj?FOdriv)b+@0WOdq02H;Cl4<|Oe};+%mW z>Q7ngXfc2Fs59DdauK1nTX2g`wQzVKP17?=5KbGj&yz(D6JC9a7Vu%+aQ`ki1>){9&G8$8!|F z{8|1GwHbvE`6=C2oFcvgd(k56lTwa=J7BIyw^Pg`87m~^r9%n%_8(24P7b(xRnb0f zDgPXqTsAvKp~M}f7NcD$2m6bZ5%a+dkT&bEM!hpE)yPcpu2_WfIk`>|1=}JgvgEbh zP~TbQGP}wqtI9MgUZhi>SJj8s1Jxf^9~xdC5Eq^!;Ej>GqR#i2l|V_rYo$7;A6m|> zk1Q&bSp=Rfk=#8QE15_ZWGAY>?XLW=Sujo=|EcO=#nYbKeL6V>jmbZSPoZe*P-^3l z99^Ig`#(eLJ{t#%S!Xhnd!u#!{|c?R9MnjiZdI9v*7^S%v|b`;eFIBAD7lLn3zUbU zb-tk0>{kRO?C+G1tb;fp*lhlnltKfl6?#mfouh6*x$->PC|?*p47-AV-yAt_P%W8Q zwhO*l`r%i$`GQvr3a~2pAWT8c35;$sC(wz*w-)5+#=QImSpj3T{#3$#&LI)n@_z|% z;cs&Id|?Dl%eIxneEU&J_i6-KzA#@f`PK}QVKvA0A-o^m4i*kms~(lyu)S!u9T{4#xCh73$WF)`3(Tt7 zl0ms96p9-x9hbN;^Z5SMXTB@H*8J~ywM?1nPdNQ1`6GW;ZVQL0>le?p0RbE!RQ%qzl0d5=>UP4saOg z;xHhPuP=kWIQHmac#25Pf~RWwW8vsV3rF({O!q7cN6aRLBN!DYm`LoL`2|vQi}_5h z!clT^bCa--Tmq}H*yJcoUTIwNh%9J?jPRqZDAEl`O4-PQ=EPDYqu$5rxv;5E`!;_G zB!gI9V_m`w90YgL`Dy^)g;K+ZfoWo{IbbAjqt2NRNn(zc&*z{9k1ya&h4lvWJv?+$ zq4`fG!K0Jm(35oYzwD!-3r-MwnH%OUXdY_KOm{&=3)qqWwz2h2YiQ|y>!hXy$ut3o;JvVk^MzwA(!^*BHtr-t9o4{vg} zcW1}W4EJ~!9upj4bcMS!gD1$hqFmvFqhkZYJ&S?^D+aPk4Q6v7ty3K_D%LfCuMp@Y zPF^eIMZG#ni77rbd@w`1w_ST2*O~aGP@#9D{y;djS9Xlv#Cr2zHZI)eW6E~LM|&k2 zsAz|O$02|CP^NZgH}DyQjrHL@Z(-0p)8{zfAzS~#Jtxd}!hy3p>cfYobE0YZ(AioG zreMN8*0fPHd-0qqZOs+^m{R@tX3>|^gE?|JJ9LR82JuMyI69_hdT=7YX9ue#AMY~N zJC2j-IXiS575E7A4fl)<9w(Qxg88aCUW7P9)t4R0hHg5@CM{+y_DUwJA&QSyrR7DT zYT{(T?dRw0+@Xo%QjV#Y6KV2r;bihYWWvUJd9Q?DVLuQ9o~}!9wEUqj^F=U@y~6AC zg0}n|21GO+sNy+3)r<>;b*mh^#CpRr+_6mQZMZudhf})YE+z29^(4aXeefuOTgUi} zuVNE>3(JK38)eHs?I&v`Hym>JRIXx#HRMHaC-5_pCdMVPAiOj&I&`dT36;@jMR+;l zkT4xjIp)0_B#d#?gy;}UGNx<^eKo_8(&!2BVasNh@*gE^cI*!j(*Vp_5R=jc-@H}C zg9JrxF@Kernr-kezV`3Bt zYAjIql6v{2KL*QUe7%Ia)9bX&J)lemw&932AF5;sUy)zy&mO<4{=oPJCM93nsiJ>6 zFCh)}FrPw<4<6&F^Bd(2Co=o6*_T+KCFR5H#Yd74V#}hIwW^jr@S_a~xA2PW$;LX-i&o`{`b~e|nl_<4=%e04M4y>7&@6J7xPKLfi6G zN1+_?R5J((XSMQFOO)o-sM*iuB%IZdC4-aZtQN8y^qpjRZi*~>{yp!=mE26@-%aK! zyyIsAa&O)dmNd&SJt>-v!FEQ$#Ar5YOoEZ#F^s|*3yukRw0=Pqpw)l7S_;(zvDR1YXc)vETOV$L(3wJDf zSzFVuzASrm$i)$W?DFB0Kp)kVcg^c(hb5Q4#nJsjH+a$%8pZ-yK>%a^k2p&eYr1+_ zB3Ou3`J+Ri`{&Ky9}xSASSf+!R3?)o>r&M?Rh5O%WKGDHR83gt__PImf8Kl;ImTX@ zaghrabW5Zb4n6DZh~_2qwR=CEEQ!~Ye-bQ39#jh^)!}rg10(%Kd-WY(>t#3QYqx(U zxQ)nKCe0jNAGu&o>}mImJ*Zmexh~;Z*yx*w$E|l>JLi>}Ug^BrM zcRdL{&k{+-hYGE}WOZii@Kn5RzIAdHV!i+yw3MqUBD|QQXua&S*%1$v_bfaEUQj08 z7P#y>1c_tC*ApFdvH3Sd1Tfwl;|r_=*Rjx3BfZ&Nz@cPfj`z}ffK{t59J@9_?5KOw zWLbud{x{$#oxJ<3$CUK%|Gn%&!-Z`OWuZr0BK~}Cz-T4g!?I|QC0R)ulNAmpq-xvz z-k*24M3BWFbSK{u-qb)Gfm&aP6@yu|*i)dU9y}1o0H>F;f2zuGeaZ|;o|dGv=?O;m z_9|mYMpc<>E{gDNKSqPNmK*c0NT6`VRBq#;S!!n$D^e0q`-SXbuBAcIi*?2KS`_@2 zo*#7uu=dcuxa-8UOu<|9i1P$$qlyj|zh=LFD*7yM|A#!hbTRWg!1+l5PoXSD1tA<_ zigD6-S!saIx5&o#%Rqwq^l^hMwHPH%(iY}a2ikC>b+n>TIZR>0E3I9ME4WPzS_(g+ zlu|?Tdytpj4_qLw5S4}fv)A4lkEb8FV1?h+nNQ4zRQL`uxeak!967R8>cRL!$c_0> z;^-O$0*ylVV#Q`u3(cFD#|hA^bg_q8_Pez!^XbPThQK<<{L?qMlND66r->8+y-QtL zf8Xh8qLXTbq)@+F((RW-12v#8s1TC+mDtnk!x#977YP`yyG_av%?K7jxC$+m?I#t9 z)Bf*Kwek%|=y{x<&p#r!8$@{5@%=fjr)e#_ecFApUvcOY7iQ)(aRNUH zW^ln_N8Cs01t7PM=0|XWSs!dEwhVfoB`#YH1{khx-V_{Ve*Qax+)=jasdSo^7}~Ac z8zgALnPH!R&nMH(>x?8;mHq*~)j0!&=5G*CE1?s0mPZQlY5V2N^CGVkPLr>+I2Ou^jX7JgAVCLUN!$?j+OL_1p*(8@dF2y-?%r%*b>6c%ZA zU_G|`Jj84zI}iQA&;j1ZK1_DdvWqlT45g&Qcr|n#EBS>tn4$g(&4rxM>j5hH&ZapL z7yw3~JW8I+)pPJHuv&t@%6WQ&fYKGmXXSiJ=3vD6CKq{>D~}&=+30+f-wXu+5#sBd zv}+EdIW<-V>=0dBgFaVZ=Ip)#qH5Hy%^Uhdr4hf%v!3g1ITyM2v)3b!8>IZh6*mhu-e3Dfty?dlWA+=k7b{kRW!}!)kkxcAI zXZSfkK`IahBI#pNG_*5V#+PM-h4OQ3_z>II)uBPE1@kqGjWMVCu8{V)Af~WGVy%T% zMUY5P*1ZHj@>swmOvw-Bol)}Djb4jrg$O601v5>Xy9AgWC>3|?T7kXGyCx%Cu}mC; zTFbNib~=B=1GPEKp|s8xy-DU+`e}Z~K$wHMW1)1$T)E_($i>`Jp-A*}a~*Z69eq*P zp&M0t2N(0tq<1=pQmL#Ayrbg`&M;V=w>cgfYramcvK9#QVOTb6cbPXTFoO`~##Sky zq$>(?-o;eri|>d#PogS?BhH*}vnRQn;(?eI9~C>iJJgqi%?m_=Gc*b>B2tUJcN}DY z&uaqk(mboMmm)-(aB{tK0}19DJpBqY?r_BoyvPilYi_1zW~TK2NBnd;C-GkX=*eY( zTe8!8$2Rk3?)aR9`8=8VV(cF*803iHl>niuz2)ve#9I)lg|F$k37?FxHIAeClqsQ= zxx7@ply8c5zeoMiadDzo7J4yJ)(OUiEk&ng#Bw=T?FLxKE*NF893A>sGJ`PV6|iOg z2|!3f(M>G*l1T%A#!3puGeUgJG&EZAPbu~>wo5sX^xk%JGs8ohdu0@vd4$=)W68t$ zUDt!Vc+eS+XN4|ER$G$RV(?T-K4z7)pv4enK^Ax2L2(P}Sih3p0==8~ zB<7M_Q}ir@hX~*lJXYCE61el<>58adOjHbTiqi`bC?*-8@^IwG~9^}bZ zo17yUK2a6`grg!N&PT}rJ_g9^h;z{{E2o$HrKEH^r|?F7Hr+XiJDGKz&MWLYwOLNT zu}x;$B~r38J40Gdy*KiHjEkAIbGAG25<5%Y9}_PrSPY$*pWB)R#}3*t6kga9-{eIb}HwS0WUG~}aH_!GD++mn%S}*&2{Op@U z17Bdvy!LzJU}(mv`^LsKz0_3mm@G5pRwIpYkjx>{DG9qq+9{nnU{<6*2*#5#%_lF z6rj|@d%o#(p2VFsy2o)RqX-FPbE~y}Fl7F4GJVqN?Ae)2>gKl78RNGfdC7F57A~Oz z0Ls|T7vJ3a!0qA_aFaAt3x6dQqDi_v{VwRIwS2^*;e3?RkL?L}<#sxM%PlSmzEgKU zcfS4Y?%*}yE_bK1g-0e531RNmk!w3}-^hK{DVu=mYrwopp2|0cutd7A%^##|t^06+IyK(-wAOcynNQ?z zqQ0>{W3vxd(B7SYYTS|S2*2Gg;{1zDnq`|#2Xp#?9r`JGFYb`a^R{xiAY;KmJ^c1F zJU-2%ROsU4rcxmd?Kx(Ce=g_zo+O~)gEhkI=ZcNa;t}~Ov|mvh9H*qM%-Kd(RM?JA z=lL`%f0q9|cZwc;#N&mh%dRL;WxQ&0gWq1GxIwnDdF$zXyO?Pv%NW^sS+mc#c01hE zJh2jhd5pW-$;`**OtP`(ahI@KV>2@@?)sgiFw@4eeOA4XM)yc^-`R#=D{4xT%r~i4 zZrsA#Cac<8x+IYqS*u4{q$Pnxz4WcUPa0M%sbz(Lx@;WJhE)PdQAcaFE=H2;WkRQ% zr#DkrHPbq=^zm{Q22HYD;d@CWN=x3j>l1E~BY7y}!Q7~xWwt8e!LbM+i{qsPX9RC` zKYWg2uEHe>rsxL7m%R*!bL}>AJu&+CZ=LzeBJTLS0Ou#w;3uzUE#2BMy_F~ z(|7*S!FxixN^1wM_3WGpJB<#U{||NYp`*jKT61yyfG=?-dvgo=USX@__qWe4=Xmz6 z-Sd}V)t);PNiBQx1*dKo=B?3S2s}OuY8=lS09a3 zKHv5Fu^Ey6tAfXZT6+88{9}{IMFh>4tkjo**l!TuJwE4;MpE@}zVJHvm@f6^uRZsW z@EeX~4hqXD5!p0!V3)VdI?}l+oR7?t1|iJ| z`H|qy)%1HLVq!`p6(s`4Qwq5SIKRfMtxQlCc3ZAgvWyn2EpU<1OLF7_knJq!;b0~8 zhXdv8ErKPnx*Uf}Ba4nZS9@jmZDsYM=d0!0xIMRc&nyQA&{)ocS^4P(oFIVQ*<LNIhk-eC8s%7ABn%t_t`7uv_EHTv#c+y8Q@wi(7!OSC z7oTYVHgO@LeWwdl{qbn3`Wo!dMVqSE*7jHbD|?GkbXI>Q@IdueZ0<{rau6XLs>!+1 zUzfvytETD=xF4xWEemTkATQ;${dNCj$nXrAg`W3ioA$(`yIzO*vf$e%`OoJj5@Q~x75dcl!Af%3iN=mp18Co@Nq9S*8`0z019W4_cbj4miVEj&b{-_Veu8p#;%P5j@;9bywLeO9l5?iPClL; zrG}!Ff8huub3pFrnQ`p5PFEUPdhm3XoNx79<<50FemLgxRM7)oZroa}TRPv)Sq7H` zPV1T&DV-_CT_WIacrMaOD`Q_gBqMyRCUqmVCHl(88CHea9ttyMpqz)ouZevq{2LAS z*HeBGllmKCe8_!?bH4#voqv&+)HNuc;Y_uh0pMJXA8mO^F=pgMW7VmAM-JlQw1n<1 z!!e_Cg~m#hI6%1otf0v>P4`ev6x~sR)4&@2Wma_3w+LSo24-c9ES+ob7C)I_W%Ol{ zPW4*O`<=gvm^RSLX!JwpRe6^K8mupJb#*P0OzKC@3%E+R9O5>Bm)& zJ4sTJ@~F89(%z!ViRR}TD5JGniC$FQ?3Qbd{I3&pm!$urM4wG-eIWY}1 zlsiY6M?pMDRqp)Uz8ij=*0X1Pj3A7jrB0gg;hL0Fn|j@uUM45gHu&pOT$8zWN!_`B zI?keYB(*gf*ztSFCkrBs1XSzMX8_r6lk-ICcd4qUd@LOY?tD<(s-EJC$dN$b*8uu% zdx~2Ew(m8sg`UJ|co(u5T9b274~?TkXs>pD9REA+*mjw&+j1@+df!dBBu+;RKZ`f# z#&h%}Y>NK4h}K*LaAv0BiRjr~G-nW;slo#_IZr#Y&J>A;>!jH5nlnhhs7d`WH64xW z72%qk7wu?wMY--R+^f6_{G~)0Jycm4xbqyug`?jmaddN!VAZ5LL#!bm{T%OHm0Oqd z#Ikg0`*9OUirc*vLWL5Six@SSSe$xN2m@JIgzh>+!tl~PLqZ3aVv>{PwUF1s#3YeP zhq|7wJS8bRLPrOMs)NMuyX1gM1XI2PvXXs<;7^`?>{JeNQr1ZS711g|@o%2GoP&|k zA-rm={`nS6w4Qo!e~;|ydth~PhP#GI}6v;2`3_n_B}bu>4?fl z6B`WD)X1?9q>BDB2oc-xC!wn!?j`QPmD`*IX{CyOfgjsKM46879JY-(mz$7*L(6*@ zppn^s5l#K~v7%Jb{e{v_Rs@mnN^?g5NE6huMTSS=V!rZSDuC|Hurvn$xyIhLe+xqtTql zqm?gpoh#uZqk{-#6D1H~%K5_Uqfb^>9_acKEFe(zqtv6vyz-Ga(TYN4+OnQ}$)|&~ z0^GKl^gfBz_EsbITK%3f>lK}&Y=mj*@iR3`Om@y-`wd%hCLLq^{Rsn5V0)KG$ng>D zKYCpJ3$_2V;U!ywf$i54VChE+lleWMmuYv7+<_tH!NW;~=@PA?*J{BXMKXDrOXG_luY?mF)It;gm z5N67;%R%X3qMlk!05bYJsb4|zYhLP_Rxqo5#`)((qNBZIx<9bvOX9!uvcUFx3n(N@kU2txXGuJ{xMu0?<$>*=l!%goaAiVI7Zj2~ zr&-X*4T(r`&o~Q6^w?5~BYp08VqF^Zi+U6J80N$kkzILZzT+>Q_QaoG9^IdML-cT| zL+)}^z0_xeyX!-L_BFz1{`S$zov+E`%kucVJnrSe7rEs*#w*hGu0Q=+!~K-3-~BAI zw83|KF5;M|BzmZ&i=7u)cHx%h#3hI~$qi9mtgFkN0a}AxCe^g5gkt17#X9@5D&(8o zGKfEJzw}u>lLA+yqg|gB?H7L%dd@N%{A4$Y<0rx%E5F$Vx3KVCTCMZ^99ss{S7n_m z^JYP$V!2jqP-oy31d7^$C^9Y0`9%g{=X4Hj)~0sr5AxKq8IhfQhb1V?o5d%(%8;y3 z{fC}qn6J1-n?4XsZSkiraORTFUAxOp5I&m24yo<>(ki}swi8XL-afWlrz^XZqavY% znSm}b;tGmXlGg*ibJ>Wr274VEx-Iwmb8{V1m@oZ}eXVKW>t#K#DrWjSrCq0TfB!I+ zp6SPHQ{NjQFLcz})Hh@YW^L+MOquUvwC}&X$Ifs*W!)3gon5@9;yuot*oVTJxy#M?yFV%s z`nLKJ@^eqWcyu&zUQN?AH1_K`*!#>8WpphuWs^xPq>c9Z6dbT+UY97fsFtCvJm zSD<04AP*kC3b$aMdRcw-MW=_qHl-bkYFWE%bG0M#v&uf}mEs7{4OKoQ;J()sM6hNYo z%nCTdyNY?PO&3e&QJ}?>snF0qza7!=H#@&$T*d@};HeJ;FJs}poO~yk*58cKP))>d20W$rIFt>8(XsB^30i7FV z4Cw1wq8KOSn|J8uzWFjv+YSl?8)J2RyzhQ`=d;dUsr`YvolYf<2DsQ zAm)uq#Fw0PlCPx=!9>-z%k(_@l5LmidG%shDs0u-5zsPil3R@F5zk0k71Bg(TRO~g z4xMJlbkYW3D-q~^1Tj>vmbGy%`EPS{pWl<+=h;eHS$$ui z?|V>!3*Tv^os0D*#Mt_pz#Tv2cxbiT-{~n4>G2{US!H|R2UJuaby(orB?5R!#5}%_3W9Z(~5SgM+g)aP&zD^D7d;RTr8mv zi1;xuyLFK)7p-Q@dj8$}sHiVjqrR*jQ@*}iCnex4iSXWfDWP>Yt&=2D1(E)`GFo?R zbbn2MwI69OR#|sn;Lhu{Jc6W4Z;JgGifub6Eqh#Vavmy?Zt#wIpcUyu(WV=@i;xdI znp+xVGY<>ooUb`1JnL|6&QpQy`=#caKIqe8?jk(WVy=UYktKuy;eN*zs9 zLMem#-|@(x)LNhOZS68fsb3;vSW4AMibAbP+~1tzPq3i-xK9%JW8}E6Oz4(_=1bm% za5=t2AhO+8_FgE5p1r{(Uu4a@(a*iB{bgnnHS&dSp`ue2gnr8ryX7mdgAjs`p?zPp zY>@}&!-*pgoyBu$YdBe6wRBP9%&Mh1$skwj1d^QV`A94zs?{k~OD8Atk538(b{wac znfa~f@CR>8JN<#}uksQ__+&zU_xmK7EE#m%MBJ4@MLhYcU@-E3V8>?-;!t4wL*$VJ zlQ8WSAr1z~AoVKw=OZKX;jrDGHVHFZZMuO4r4|0#oJZjxvR*sEw0tm9dlVI#e&%#NHd_`62swEM$6oK6r<0R#IZw0C1py$9OiNZ}B$G!++ zvP>e5`W0$KCev_lkva7?<)1hY5OQgIS$z_6h6CHbqRYA{*#^aPx|+6qBs3R-Z@P&^ z*NWPvxNfj@&ZS4B^$*qM{5TN~JYc3peAx%~w4{#SWT#VdibPhG*&>h2VK4(B9dMBf zvJ&4U`Q{KNkY%)ih}#6HbaKHb-6%)-XZmGYBciyqvh#SZ7W65NA<*9<5 z6+TCgE&Z&h_{}X#AKD@g!guI+-nZT&^-sX&my2qap+cXy{DWX>9DsUrgF+g@_6i4 zr3U2 z78hJBQZ;>CrpM*_)S2kB(kaRjw6Iol)o?7a7M_M8GP%^Q(_Fx9ZNB8W@agO|I4({@ zLG=i!GC&r{Oye2nA|U4>Xb=JN?D?akumIgaBYg6b?Olcuu&(E?+J(ei@2T3!6(!QuyB_7>EPKF-?@v@-666Ek zP5FGo8^mFMO@FlFz$J{T|4u<`nmjcF*XjiEW2U`~-<)TY>%^wL9G zrjP-TN?V$rj7MaFHu9iI5Xm#qr-veyGzC|o#DQD(o760glqd5J$!Ah^q)HtrxtRT0 z1V5aZyluZ;)#cbssHSgAg-Vt>o0+K5@+FEMlT2)_lXaJdq!XF@Sh#PvF16pe4*i3C zJT2*WmJq$ar-ueci$_e;`|eT)GnIbZej{C${ZkJzcU6qhdhWWtY?vggQ%7CR{43JG zd*l(5??ObKZNHSZ4S&&6%Tvxg#%;NFXH!&el7JwWsl=(yci^8!>zDFU>eOX@zZ0e3 zUh9m~?^C>{dU~7zY}+B6ZM=`tEcza2g!)!lFIhoJ&f-=7-I|fkZ*(+0_|f9on5C71W+R?uffm=XNxTe73Inc(2nK8vEe9aA76^7T{+he&<*t+pc6V(r+pBY|qdet&i3l9Du05AqHzgXm zqy*$rf9=!+j>w5zVah%f0<>`e&!EbC8OisKCw4qh#RY*< z=izEu`{*SCpKy%y=M6;U9LJ^^HBC=MbEa^U45dI#<9x;s^S)4oomey#1ko=FIZe*( zCwvE?HsuQ*^RJgrA|T1ym=8%z$Cod)%$9uT>Yt-PI}Jir$)R^t&&^D=&GgDNhNFMa z9M!p*amnr&sOMGh7}%}btxR3N#8)=u69A&MhP_H7AIDz*Omcgq@4z!_%lzZj`XMuK z)oVE-qE|0`ZP9O+o)h?(D6P1t6TM+iq4UZAAW?LQ(VP?74cfPAh2j=Gs#2GfFnru` zQ?%)^+JWD}G9X>7pEru9Ap*Q+iq;f%Ece|9GmJ*NbI+&h3$(lD`JaB>(^PTf_S+C_1 zk<=SY=igD!ifj`TzI{IfP0r(ex0U!*;xz5_S>qIKT2tb4_GsXM<~>M9XwM41NVUy= zMGt#(M!D|m@#&TCyX}4!L^Zde#%RH^5mECU7EF(@_Q_y&(_AU>bKKgKF_p<;)=Xt& zgB-eoesA9^Ir|-2+?CTK{mX(JD@>e5gW_D-8#zg~+k4s8BDK&jK}ukC;~k=@X@6rZ ztm&T$;#mivQ|eJUi8eZnOwTS?)?itrsZQ;$VSfzwX4LfG%yzx2La>5hR)djoIK1$; z)eDc;p#79-{VUb7Y|W0yIE8(C>C!uvSErhSk;*rdvJx5owkJ~gRx*!up~wTCFrN*| zt=@h-IaAmG-R3FhIhwfa(YSq*ZYsz0w+?!0Qa(oAM43MjXS}bR*@j z?e8(3?7x)L`m_t#gqhti(rtouIoaM-p;z`&KV^FtRl}_Z{+h~q|K`n6?tI_;;Tlfc z3T@71E9JRQa`oly=cCLscURR^uJmuZN_sBBHoKRdQ?&vKk#F=sbm|l)Q76P3Bo+zp?Yf#_i;= z?%u+^OR8Xr!J0$+Wt2XeOHbBOvk*TmFhk*&SU9DPzx`2i$A*P7TcRo7yb9g@`|lB} zG8*j49$QP94p?1MkrT z953j4LGL6$TWNL@==qo3!!f&oP{+8aA@^(v{**<=8UOEG09>>bSGI}#Qg1Ghh*}oN z*zcCH_bHTmqjuoZU!D#J>U&0-XN}$hRg>C28r|hld-(E?IdR8=cwmRj)wyRQuw58l zDhXe|_skkKY?9t*6kHuS2!`D3qE`6Wm_oLf&+EG?s zYASyd*dYwB?xWw*`IaLva>4;-)1aB+xP}fhH~^NTvoo-eWIi2?eIcZVj$FSVA&;^X z;69f7Yd*SLqdxYfu|09M14W7DsFR*Rx@h8sGBjg3DQWA(+oN+?)pTKpDQypgs z!8DuK%w777$e}Vh)%+wBfsqUp{evJTh1|P%VZ0%U?UDpl&%Ph9Sx1`4R(m878OK z>UWbj_7!r{^4M<6}D zigZKL*|~yFAF-gr+YYQ9u7)KX+!hx9j&`Q-;xb4f_38`u*$r{h##HgnwAOZ^uRQQkRO* z44!0ngHuEu39$j{WQ||DoY(taEisp=$YUSJiaZ$e%wF5RGaf$wCQl| zna|ZU9k?oxK0mB zd&z<)d3v(iEC5vnxw@Le|7o}n^4V-(K*g-SQ8_EqFad9Ac10+bRq)E+V<+lDg4qey;DrM~^)4^|E>)=eFPM zql+P~uJ{y|>^j)7S7y4*Dt5ipcjRqzPe$*no z#jNclg4H{oWtol}N&1ewRn>oGP_{7LKv;sD6WA`x^AfB`g88@qEU@DcE^=Cml8zSt zu}m09vVOGqA>KHt9@sHkGROE)udi9SA7|9Sqshs9Kb7~RZ zciXFYiOa}w$+2njF2iP(gt!EtyehT7`piRou{Bpdu2tQCb+GTqk0LBp=p*FMMPm#qApbZEtN92CTe$hy9Y zY=0wBr*mwO(huHmkd}=W|9q~S=fL(GrMF7HmuTL>F^_SbYZE{`jlK=E#HxY-RD=bu<1v9>KjD!Ui|vyM5=r=`5V0ceuW&1 zX87k^du{~IK)6!~C;F1-S%1*s7z$Hi9|O^Nex(I9ZnHlWmRgW|!cu=o9ffusO!EAZ zhqzKh9a|vxL!DK{wYwsAg`}Edj4l^lL5nYLFw}X!beY&OKgE-h#?p+bD)kuUl?KU# z?IFeLr3h=VrvMRCCuBmJbKA#KKVkp%nYF3Bn$g2{y_v$iSk2lwO3Oq(&QUJ*N&Z@z zdcBNp?(D18rP1iT)sSv%{E`cao^m#mV=9kTpr@SMWn(kI5i?@y){*C&ONghPrC*~8 z$Hu;aqWeBArR$Q)87TS;z#*wJoW5e5dU4Wtn0|d7r_8wRI_^bMz}zqH(HL&TDOG&4 zcntREejLaC96i5p0BJ-|PEb@1rE(K^E${cyt1dGMotlx*a-3*qnQ1AV){_*yuTmxF z9m=6Qj2^3|9;p6T{}JS(*cd1p;+6F%5FGn)U*%}x7wObfMRI;VB70=%`qK}maz{Q6 z-0!2I6@4Brx0v4$`s(pjkEQ3Z(TFs@5|;wXX#3V5O*cSlo<=&O^WG!^9|GiFhgcL_{-Xy$KhzDAk&zWwyF zTIs)ai+)zi_)#MJ4Fk(wXsJ!jniqsLb(Jr4&sZPX_A50~H}DyS^c;CSM_iMruZT;- z^g}6qXj3OMhF>f9GLz2spZBnkHtRPQ-*}9{H%R+u6ZGLfewAfq&K#x?)X@_ZcNd|7 ze&G3@`(JfP@B#k`_)UrR-{Wxn z*+q8TAR@qt18m}J=PGjUl__M`uIG=-xQX2}a-i?hw>pxAq@J!tJ=?C7@rg0^OxV=SFW_XBkUQIdaUxQ@#= z_aDo>B6aVlW16Zzjf}o}@bh3t@0CDR`Fo8us4dU%V{dO& zkDb_TYQUxcV{aWH|3@VMMzWj{*T!FAK7N|e`MSLz5H*_$p7s}-2BU%$-lgr z{L7ok|LsR~{>38yVrAw(@XZsZ2yXtVn#L4$M4e7Y+ee+AgI>neW`NoZX4Yme)ZcWK zu0S_F%TXRogq0pDQ692F72(5ETO;-Ih{+>Pok?Ws%vdMH>VA|uBav4|Or80p&hAT> zSvSV~USsVs)?Q=nGuEK7hKx0=EzbxIS&f_XKK(BGpc}3iy;SNC(u2RqrakGu1H}*I zi=tGzX(2653hb6wI_r?mqIA{+dsx#*UFqjS|0@fTAy-bR?N_WY)5D^LRXgjaaCXCs z9|0b?vH-vJcvkI<790qCr(k`*|G=J90dbf4#6PcJ;)Q+q=Se&lZw5+ev8_KK489aqnIgnbNdglfU?t% zm*iLTic|j>=q!NRovXY;?R+8pD%i%=tgLgnaBX146E&4j1n#_?ajfss6FW64`IT4d z|AYz&E0GsfqFKpjKcSmlH>~8bw>!Obw$66%u;m?ryAIG$Omm!&xdiU|0s$<#z(+5& z)s?OzTp;2$F*;rwFSMxw`w&r1A-X{Bo>h73AKhbv}drkJy77C^g05)jtISu z2)&NH7kVAB^g3ecb;Q!^$p3SCEuN5Gw;y>Yy~HhKD3Yd^xMkBz+{V%C_9L0}5@?KG z0%@{YdZk(EIDRNCky)AW(}N$=4g1sRFz7E{=`Zcx5d!qmVgh&JYa8~&eD=cu`5vF{ zX@Sm_L0)bO@mMGPH6L@Wu-qGkXE~ z9d?qQnkm=jizO}0_=yFiS#)i_Skm5%pIAUc1`-Qs*g)E9 zx9mIJe*?j3Q9kaHbC~OErjBs6d$6X@2_D}cklQr5&Fz(uKoCFQ)JR}zK0Bjo&mH7u zrkednCnY`{nD03@L)Xq2{|?M&KjZv-jV%zYojM%dc2sDAdpt0|hC67grgA4u?bLA1 z=o2{B;}{l4c4E{{<(oW_3S2_s5)1?jrzXEC7;E~5w+71ho0NTtnHlNvVJ$K(IRs&H zgph#*i?=M?>U)#2NV&=_xd>RkbI8P_-CZQ z<35PU~HaPXB3abE>XAR9#PI z)VMNDP-GX8E{^jSJL`4~Zqa!^>|A~lOvehMTIHLODyCF2u}m;|N$=|<>`l)i@q z-=!@|-{Af!(^hHPeZyW<21R`>^>1-}qrw^a9P>r7-~Z?3E^4v#I_~5N|_Y^<8E5Y zuDYE2Z)IZ;N(waueS3fx)WbKYXXt3EBo%-HRp;2nB-?>lR^|eUVaBP1RZHsfa zacgx_yrc&1BW%BHY_5y4c(lZm*4?>^JI)gB5qv1eL;9%Sj$Vy^HF5Htu4ma97ZH?) z!g1Wyp=#zTVpWNis;(l-;k7alkZE_2>94Eky}dyt+Bs84;aSG#{8y^FeA^OzMGf?> zF6R@Rz9jp6aP0|&SVra?odM#)3=WeUGPcS_WX+fb#zB47`a-MUn$|R##sI4A+Y)_CrVc~d@x#8_PJ&Ud4!zsj9Uqs2`e7+|BK+#$n~%?E%8mmH4vFOZG8HI>Jc9M5^T zRaa9WS6@2&mb)~D#LUrRzeasPqn2XHHkvot5~bhAvsZNHMG+m-e1oN$s4_!L|IX@^ zhECiLW0zRi4X|&caj%uzMEHH^!^Xba+DmLaH~gMW#(%rDH(7g{wZC9(H~hm{@Y7lF z>2AEF!Mignz5lTA7i~WOKFj~@EPTqAqs-cFK3=r(|I5aYx#?QC$NK-m#v96t{~H_r zEE~Q+wD%dmyKVkmr^ju$zgYKQSo=YX_p8=^*e$oEx3cm6$@(X(|Gn1#@vMC9bK}{B zpU#3;WYK5D#-CyBi*5LjwF|T2d$ZchMYh~CY(76|?bj{+Hd(xb7XFyEzh%R1vi2;C z=Yv`0skiR!S@iw2%~wTMc`nI{zs}Ojt%twJS|qEscN3n-7FZXOj|dBo(=E00hwj|zG8%0q3#gyc~nkGNl&LwEj>*w@kCo^2N@2Z zZsPlG=Ibpy$-)g5&bM%@g&CgpuO|!MY2iZae~*P-dfjJX7vHxmoM*!iX88|g!NXbb zh=tw!p0Kc+KmQq~e8>sR`{MQ__i##*TQihb`>3*O4q(g-rW(+o#{cZhI-QFiIQ!tH|;XTUZ7j`88O0mH}0d zg=bs%o-F?dv;1Gm@;_nWGj06P9FyOf77klDWZ_l|pJCyh7M7t;e*3cG4_VmdFT)mg z>HnIA(G=+4NLF|?*W|~|UzvqnepF#$m;PZ3PqFywE$o&jZef>xdn_#DxBMQoaG3$s zaF)OSOp~8;t^X1WyZ9R{9J2ns7M^S2`z%~;;g>9Yj)nbY2LEXmF1N7Dua{U@OCyA- zx3J4Uv-xE#D}0ZIUHtc0*ll0?EbP)}$inU*nvFl3zh(2M?Ean24|dxA;_}Dr_CI3N zM~kX|YuB!8>~3jx$*0z?ZE9|5YRB$QG&eQ2x2MA;THCtE9M-N~-`JhVgg3P`B|5uY zm$hr-T`k=$U7K3gc64^M2;O$rbM4xe#&vCO`rVt`)+g3-_(T6lSHlal;6-UTp6I%0 zG5A2A=xS?9xbc!5jqPn4I$D~wgX=2=3w5+?w$W9nxuv}&(V{NJ;WA{&OD<7r4)2%Z zFb8w<2TaJ$cuQ9!{-Ig3cQ@p2CS=VH#sybm)Ea~Y8?0l`_k%l_sMVq%-tCmPqax13E}`NcM^ z%YudCXRBCZOT49Xy&Fu$+B@+Gbtf7VZB3yK$;Pf`?H3cGb!-?5w@K|C`zOO9c-p!WNhs0W)*WweY--uq(vb*lY>d0*Y-tVsjjR~DkA)kfRcI_ru`&O5 z%~GtXGrr|yM3ZtSn-ig~mPE3vLswI*I;vtTSFNhixxnAlYr0j8hbKlX?cZ(Jrb1d= zP{leocC>}04RyCktB8#e$@G-WN|pR&```S5aUo(G8#@{|v^4)MqGpGikf@U07+v2% zxiRXG3+K{5i_V#$3E#Y>qj4i$qphPY(FW(ZwWTYxzOyUjwhPz4G11b~mdI%SnW4aw zZanXrTsqu)%AAz@9d!H~_{a74L~B<|V{=BgS1vyb&7B(p{TSTTxwdh0W1D58Y4=Mm z6)pzvTm@eVRkd_Cb+yIE*n~A z+Kno%+Thq5RX5Mgymw$XsZQ0UT6kNJ%R1h=w12z005@ZF@zvjo->tTQu}QTO&%|EH zTZM_t^A?S}gJ-*1q})+WW#Ki!WzMjoy*tuQU8WK6V%=ACwX|qw3t!QaxW2JHY23ua zx?h*+E^>gl*QA^I@-7-Y9sW9~%bd7c7S2#WEI0Jao>$BK8Rj6BpeNAxoFrUH)x`1l zp2WSpdgAbsUui*-XE7qziY@cg5=<_p2qW?;E9SEIM2RdXM%+bCm3NVkPr-<)N!&%g z6PZlh1eW=Myo($pvYyCef_t{LPq%gmTf&QsAni=dER59THq3<>$=~}if^WHn>#-%x zl^BVC14i<@5fj3QE<)1l#E2XrYAC_G1+xqD5scvN!$`c3VWcddz#vknuUhy!*pkjO z7{T)*M)D|kbqgN3yj$YG3L!=0`Zc!X=a1Nu{-3P<-`4&Sgcf>y3r~sPm~YZ;w)T7y z6MWN2MCzmz8!t5*TkxKaEp>1fwuCFkmU>%+y&HQmwv_ck>*J@$Oi~M$4Sn?#ldn_zz z$#0*9CmB#3w(uMa4_jDhDZdjI7Fx^C|2~s`s{vKW!XXP+SXjy|zp#Zj7*I7>Sjs5B z9t#U?y#f+Nk_aSXlbM{DK#l{7N5@ zU&zAlys5&%(pKdcwy-;n)>~NGp8R4KmNqHBxP_$+%5SHI3k<04v9Rz1`R%hX%~1ay zw6Njc=%OurtMzv*EPY3QCoC*HMt;GCrhH6A^sn5)c@_>^Sa_QJVip#jB)_#O?)Y9Fp8k?J$Qg?^eBQ&>f33YTPLfy%Dyt6A|Cfu%nTSq9_ z4byXB3D~%)v8`Puu^?`27s*u5fI1LB>}(zPt_?{&HEVC{PJ|MjouT&54wzNiy)&5z zb*>LJcW!KKg9&K4#Nl%)ww~)FwVcLWj#f_>@5v!FA@R>De9S(t2hOYiMKwGv9_e*%9UGwm+D zf+m@UySHpy*V&GOMm$38Et^`}rNz>_la@(o#S(#|BYU`#4)O9u|+gqY7jd874vTB@hdZ~5ft(zQWVp!SO*4^S( zLPllYP*WSq0M+I;Gh^)5`M8)8Ny(ac#&K!WrIq?IrD9%w@tPYLU-cll=GK;GZ4%Oz z6KfdZHzE(TFI;(~<>7hJHWt)r*ysdHh;sQHSUQ50zAdv;t4Q=h!A|S1;SfmM8OO8zs zSfj2}b%ec`ytmWpGHt0D^J9-vuZP$0JY%mRw$yMdPoc>IO%Jy>p-&+F7W_>+=q8l3 z$gxm8>LRrme2uiyjrLtq>Y?>rg#F&=*WJ}b={p!mkqgJ(7Bt1(`O+FRQ_}aPH%NY@ z#^iTCe{Son!%bR7Qis{7!zHN84ZxxoZN)`gyKNhPukkmNTDR84Hj`GXj{RQJHI#K} z7Lw$2enqD%dFi6PCBQBDl~7$7`O<-T%%wKFd4_aMiPfn4jg&&lA--ZHKcX#mb13b% zIU`mZRA?nN)3RNkZena8H^Mpu>m6B(sujFkoZrTq!({y`sZFip^ zo7J@Ypxyr$&a&NbG3(BjHWU6o-`#$1?dgB6Khi1}EnM5$(vG0iZ9C<~A#{FL(8(UR zH?$yMwKd7^JYAkpudyqv9olAKl(E{M-Fe2g;n~oqVfOSH+s4Zde{pE8ntL7(`(uNj z4$mGxZL`f!+wOidcPi!1Ly(tbl{DVjjdq=#W)>E4Mo(n*e6@>p5Lt{voaxw*&@5;T zW7*{x_qRq>(LOfOj-*Gfv3#ORw;SO|V=c(UcyIM5{6H_pcddbsUcG9$nPg?~gR2wQ zAhNAs7UyXm z*nIkJ{({ynv3AJXlDQT8W@|N;m-ibnv##){Sr-_>x_+Vun~v$huP4h-bW}T0yzRzs zcb1>4FD8ioJ^u&R1~5OvJc0535uHEwPcV;R&H+9Z6T;+U6lMhX7cl!VH~&egRhT-= z3e4r03o++n=3q|6oc7=36;p)C!;Iqg-=pgSjzFyF?k|BF&=q?Ug3&NJPOo z5gjBQ_gBJ8PH#?c6}ax>{WsV$7y9`>!}c-%{$-`E`WmMvG5>-2!8aH?Fcsfasu%Md z=7;}EePb%_SL(?hvA#OY+BRmvD@v`#Jcs!;=G0%YCX3nkpGs}@dej#&k7EKk9yJ3q z8?y*=DW(b&#jL`75OWKr2eS+FNzA`s_F;y|WAGA_=Dj(rset==@?ZOFr4BMlp7ndB z?gsvq$D^j;zw)}O>;5%<_wFBje%n=#9N*LY$TeI4?7{!N$xQ3oI@a`Dw0O-*l!@$h zSz{(s3*yb|K%sw?JjLW%`-M)59d74%Hyv)zIJZ?g+&$ynbU0)}Rqvr38`ifqcT@Ia+VwfWT|y6~ zR8|wwd5(^AyGiG{KGSWL3D+>r&4fd~P$WZrYxSpU&Vb8sNA586#lP$( z6DN+_OU1_^3c?-Q2o&ujL;3LWf3-jWhV2%o|D~wO3(Us22SEnq#PMzYpQWcIA z4Ey7gVtMsSmBF`$sI%qJC-x&=yj(y+C-*HQP9RSOnkT6M_yP+j$EzMsHo^zC!bcH|C1(YWXm%%-mI0Y)uWpJ;m1^10SydP7g zQl2hvg{0YzQ)`mPN5pT+pT5f$qYsg~XI7z_)m)@zt-@YBSo?p#-Enl713=LOJQn{2<&i!4Fd61;$P`*sq z(A|WcPT1)SNhjb{ft8bWKBw2g_vY{@!-O$De~@f?&y&|vgCf3$g{Q8W+8cTc%ePRi z5F&ae=cvicCe`ObFS06iSMQnRRg*ktsM0g?)EW1mHc~o#YJ5hlSf_Iv_m?s!w~ro=4{`&B(LVQSfSyYMaSp7G{r^o<#e}3bWV^bD0iv zrIfwYmRE4Ba{WKZOWb==wtvF)mo%u)(zyj{?&4F_+^*9{W)06ADjA#+FOE&G7hHP@ z68jPl?-PPXdGe)AmgqLA+xHi6lXm_qY~42b#>?WM3OrBy756(C>%rp)p5F?dvy^sU zXWZ2_xaacl&NA*ce2KWv7Wa!(>FGJD&{MB+hiu#PGS@q=P@VUuD`|IEsWT4Nsu_to zRkZ3F<)d%zrT%}42@!pc->dx1S8MpEaGS;ACcE2+tB9+JtB9)zqlmAv<5T8Le5Iwr z7YbCt%DlMrNf~=f-<+)8eC=vgyy{w2y!aYb3~!h+0>6lj<=69*Lgvtu)l-QAHFa^J zn!2!Pc=AyGpl}aQQJyLSXVJp^VS(r5dR1;BU&RniQX-txn?95Ymj?YRxVVTtKx14; z=v+_O)*E@mFh1SK%Y7Gky@Q~cYWb0b>%qHRc)>Paydke=W`UYXnltIcGwH)K>6bGX z&KTBx{C;o6)OnNDyv6_Qo#(m78yuN3TsTxP=<+_nci6@E94~nnt1UU+=R~H`ZR1hW z*7&D_KM4K+_@}6u_sA#@Hg7M5O+uR@+r zdnb=Tr@`E~FP2l`tv9^4mZ*<1(CIR*2JlxPb9%Sss`cK|xs#yPO7PVxV#f1gre5l$ z9a&nB)Q|>EVmB;#&J>@TB76*5h+7PE8z_a3jfX)1&Bnzg%y>9Yhf&~C;LP;%=sFCj z;L5Q&)Z-505xgrlNBYxiepQe#JasS4^Z`uaN|TpN*o+l3(__UT`6yjjq{ynJ1NDMPTRbA~^s)aP?PWUPzDid`fF#*BZ<$@{R^ z|480yZ$Z-Qe>3lN-XB)Qle~omc08yrotLYodoELXvD^wD=}7(UBaJy17NB=psusL< zv6`K@R0UqFRKA0ks4!CNy_g|euJLdIeRU~)^iYn?EzG?-40W6pue8{VsZuVH&r6Zh zh0l$HzhK}n{&p;?D4pw3r;twxr}@VG$?DX_4eC_S)hfGBi98WnU(q1!R&1Y6wK0_~`v-A zV?2A1AhGXBniWRILS~da4vG7Z#C^S$FN4TL0p!amYWn@imy>hVs zghc;m%x{P~N9dQxgGO2I)aSTx3Rz`>aHaF})v2u_6P}?0jMXlmmNu2R-lM(%p7Psy zNnw12^`B&#@4Z;8y#;rwmEP-pmwA5%qMw0!>`UjMm^==BvnXz z^Q{W1-rMNgn0r7a?QJ{^&m+7dydu2H3U9{JX{z9PGnU4PpB`Vk^5T-;vKu_C+LGV% zd0C9{7R0wQ%=unIFyNTq#f{ChxE zzKzF61?QA~G#l?Xa=gelK9%2Vn9x~b9!o;N&1>Z_b+15uBlocz58a!{COVV#QXr`_fOd?RrGZp zF|YFFJ%A&F&4a*S%E{Z2@vm_KGJ_~kd9_tltlEc`*<+cNwG{!oU0zVOJ!$n#xj4C1H6 z-28u@Fn4^F^U&ZC+w$ppPrgUxuLG9;-DPUR=mRD(PQnuxBRfuGoGi|zd7p}*I$e-U&@(4mgBv2R*stDp&x-m44V8*B|nValugF%{1UG!xqo_vTfUk1bFK-KpnPJd z%L8Z4FY+*Ml8~)S^Sz(w*l~C@Ih0{l-4b!G(7gFbw zsdMB4{AS}fd!ecGd`S~tPzo<7g%^~rG-*p((zj+k%DVLdQwJZ#-iQ%fN+fo^?w9bB ztbQr}KfwROgC^`%VuuPr2lj&z`djG4QN~O?AKA3}n*eQP ziVA#ka@_0nsT|s$H|B|J{!>9YGvq9{9?qxTd;l{<`w&~#Um_n``c>(wNzgM->T=kO zr|3?Q?Mlx>U%2>#>NL;w(511NpT!+Cm#pKcyafaUWUtPCr?bmY=)+SHgSwi`r@2!^MC2B=`rLC&n*p zzVge&|Fkm>wFX@LTO%v0@-Gh*s&%IOAQPVG?dzGIXPWbm=xpo@&kTaz6duf95$T&AdK2BglC#fGD znTJdN7-1*+?a9i|P67>m@|__MXN8k2@B6!Vh8vk6T=4F9hLbgF`MGqE_8H3xCv(;N z$oG3KtKbU_$dh&jT_*a;nzZ~f)0J;mW&VNyq8685X1KwuaMFJ7A)S-;5sB~8-#JNs zB#ivrbY5c(Oj2f6qD=$v9?W5k+$p#(20yX3TsbKEo{C0zDeiL9pj_%A*A9xGxC#6_ z;(FJ6R54}&<_b(BW*g=oG2g-rVP41NvF0`t^F9n*i?JdHxhfZ_nz$GvWKs`lkIxTxpbu^%SrG1ohYpJI9Uu%pCl&6^-iBi_iDBj zDzd>I<9ar1Pm~9{cs{XLys?XY+!OoBd8~pmF78BRpUV2_En{xYxYbW%`j`>!F5Cu#?4w{k)ct)Kw-9bBBkmAxJ>VP4 zi2E1ZqPWRgs{5NO12l2VGs0Ejwima<8Ti)Y7A7x)?PGDD#BB+B#PG&3x8LCQAa3_$ z#PxN1RFJr>8E)s`HcVc6GTc_-HiBDyMjAc19VTv1M*i-VaOAHwBiup3N4T9CZc~!v z>lE;1)?h4*z=(v*{=Q zr{2RHR9wGf&Q{5$`KviP9g&+;mg|w|x)yF2Ywhf6sqVQ!?y8aFSLXVjBE|JQwH$2I zr;gC2x#LtiitPJN>U}wJym%G-h6kcpL_=GpC)&Q?A6z$j2mgO)?QrCaPN2B{y8B4>J(z{S&3hjX`si zoEAuDnUL(N6FJ1>7fB|4;EY>fNU5Gv&=L2ze!iuW&YY%DQAQ~wDA|-^xG@0*pWN1y zMHy0_#2K#UmgTLDU8_3lIOE#3j3egAQMx3{sE%8tv^Cut-q6xj&yg^Qr5fXH=Wpyj zp93l9H&IjPx0|D6?VXLyEnVlgUvU1y^D7pJ%=M0t-CH`ES|!8bv^B-=XIHg#^?*x9jmQ}<>L{C6bQqhD8R zAx1~%cq(@#&hJWckHE&0=7JDhP}$a*nFp`p)Z_BTIG5+OAePDwOnM}4wVj=}B;zaW zRR+~uFt7!!rTAQLZU~&o&*E`TrnpIHBW(q}b^$-Rua%#M#c}07&yH$szk5qF+5LudPl2j%sed;q^ zaw&6_zAiKC&Me&0^Q*!2)UlrGLgL_ zP0p`LTYuLun#!gY-Zfk@(bk?BU<0S`^~qTH+U06(cT4M9@`Kx2VNWHBZq>2AZ38W7 zZAT-ASvRlcJnbg#+>$=&-frl6wRCVJW@m?-pk>i&TnO%I<*LKA`t}gH&6F!vH_)O; z$NIRJfvCCkT7A(-i|b_~olai55^I@6f(XvPwn@Kma#iOCu7)Oz9Ov8-@yjmNwWz5}LFL zr1WfB8ld1YbesqU4lNM0$TdyxWnYq0u~QZ0fU3uPEDQ#w$ca^=EqWA(iBiGOcXuyb z+H_zXNB`kwzVrFsbI(2d?6bSi?%n>LXVq_b=f(5cLv$L}06`+}-#dm5SJD#WQpLWy z-N!*o;vDNQ#p_zUpW~bluP_!f!(WSz5AxKkjWCG8YKpU$7o?pWh}$Sk-NEI-mn9qy6L zy`$A`E$2kqI_6xs;C$NE&sow==U(SkT+PF^(KSkT_m z5;^1(3K_L$>1tw~_89LV_BxeAa%i@lX3axXdZ-_gYdH;9nxS+L*K#5qN+~&2AGU-U z(`Jm>yLn5PF>*0;@DjX(=DE^oELGq53XGYH})9yQEtWQ2z+S8W@cP;0EWW|?$_36KR z9j*S$d+WbBxV~$<-4CMv$L{~JDr2bH8%L@Cef27z<-I@Jd+X-&?SBePnLmRyIsAGbyw1uwR=Mh7 zyz%}1I=z=LQlqiHH|jfHvyNGQgLh>-s=>x8&;LpBzWLnt(pdclI;#e6YZNOwR=JvV zh^vO>t43POZ?yjbvfMx!&%G~=`3b;Hs1qNDyO3g%HSz7aXcN8;{u;&c12Fap!?5w< zgJ=++g$>6WMjqb?=bb>j$9&S^9;7^aUeht3 zawhd8JqGWrqHgkV$(PwS_|iCk#sBX_L~^*?kDr3)(%$$L(*7_2PddkMPpDn^4DyIu z(Pn%Wp8XZW=*8E;1loU*Admi_|@kTB8#Lnk_Bfd0#y)+hH zG4e|pP7x!IxB{i{;tgmAUi=n%3NPM=cH_kdk@AS+7?Pp%0KA`g_hEc#towG-CSJ&X zdkJIP;Ahbp_#k{2)!><=$N18vjD^I9VKKg3oc%S3iY%W#xILAzR7kZU93VA>bWlm&p{{R!>|~mEcT!p(#0&Q$BV^S zWN~U2`y1(E2yMcPUqrq5FzmmT@8dJ@iQ8y9!fZ=pd=>lq8`Ma;_*=9FFTRX6;Pdc= z+r9QwY{W%(uiDF894+IW{A0$2j~4nn=v6aX0ON!t(dN7q_eU-SfIx7t+NgXgywRLfv?A zg`XaQHzW7l3X3tj;-v4gzmZ>@f^v9qDjLLx;2e4FWAGuQdMdu-fbWg(8E$N@j<3>~ zUB&LcwwYsxh*|Meva7(*zS0ZG+Ge%+Asod-NJS!JqC~Np>FJ}0XPMz z9}dCmx6(I~CkpRF`|ug~3^JCoE#NV|Ui~M)ugY@w|V7= z)xH<6^?fHiGU2t)IJg;&qYh%$PZ$5}`+e}_q?f-E&PHlyJ&p1PKV1){9Prb{Kc(pR zDRT&3ou=Qzx5G^)+Xk=aSH6sNEtiKAEw6k%((+BTk34^etv2D=c=2C-wAJNoyMD^d z@E!abc+wAOYkVzShE%t9m_piz#XY_+z?vU&jF4Z9{D?NfcR~~Cy92Q9$9zV*xN19_ z2p@r$?cg)~Qg{#2KGqHQA}wnld#N_k9Z9{RnMw1?>iJe~dn@ zfnx_=ypz5fzYtFSC3zZYE4b_l+6dnauX>uc!?(lRo?%;%eB>Yv+&&KygDmpvKguVdS>b~FVNQ1r+$!QA1S>6gD-k*E1tTWvFYRyKmIc3P|BPL zHzRGY`rmPkyh7c`QwayrQhXrKbr-6{hv4bH55qaWUk@KeTJBExw(k$X3tnZtSl{{Z zE~Gr0;7h*W4JZG>OP>PQBJB^I@I~bQR@y@!jI=GpTYVpc+kKyfNB_~wqxnwyQ5VbA z^J8D~(-rsHhEz^Fd zhLmR`>__UWGw?azKM((jREGobg}=}bG!eH0$N!bSg5{RM1~i1X;Vplo9mum0=8%>v zjw^UxT5sE7E#p;c@#pt|(KF)}4$(9BX>Q?p#!bkH9D@XS<7K zZ~6NG)T4L1DMxe;cFNWVt%AHa(m%^&3aB5EXm4PMkhe&rE0 zj$e61jpdgYpFquc4W4}q<@kM}v56XwC@-!-^=fnYf}fs;M;+ni(Qp!t_sDVn5;cxv z3jMRF@fj*d)YuGpQR6G*MU6$+Lw-?X4R+F2qT=HVcu_I;wH#lf;_dZaQL+BYBPz~c z<%o~_Ua{Q;r2OJX#?vh?Y=T57t2r`NIHO)=T zbJ;Sh;<}cOirMFU^w?^i4A!#pN+vR=fNMHtRq#Z+vpbfx5*gDmjR&uJ)A|y6vGVMu zj+LifdwPZD4{uw}^AHx5E{&ps!D=2(*s(^pb^OvMzfWm1C%fO{n;kq4f%{?C`R_^5 z;}uv+OLN`owrlxol6i(lJ}mwCJogSKt36+nk9$*)&#GwZIN!ZLxVj>^wr$Qb1!v5v zSl-m$(NYndK2lEg^usTuditn3RZkyTIlf*!z1VKN8NCt5eivYyv)u|?hqoHNWxavk zU~j0msyEzQ+gsObB+3%yiC|(VQAi9YjAU6dkStGzlHp`svN0J+Mw78*Jef&mlLN_o zvXC@VfmARRN`+H(sm4?!6-~ub@l+<2O%0^-sY1#~2hzcGC>>7Mr5n?cbTl1H$J3c~ zHa(EerweJr446SPWQNT;v(b#0Q8Q-7&5W5f2h6-#FbykU1+9=3w(6`#D`G{hm=(7& zR@NG@@>anz?0_A#Lw4A%vm5P*9kpY2+|Jlpd%(`y1>0}}PS6QCVW-Y%bRtgFi8*m6 z<7AxyC+`#-qc6}G><4F53><4E2D><6587> -; About LanguageID, to reference link: -; https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/a9eac961-e77d-41a6-90a5-ce1a8b0cdb9c -LanguageID=$0804 -; About CodePage, to reference link: -; https://docs.microsoft.com/en-us/windows/win32/intl/code-page-identifiers -LanguageCodePage=936 -; If the language you are translating to requires special font faces or -; sizes, uncomment any of the following entries and change them accordingly. -;DialogFontName= -;DialogFontSize=8 -;WelcomeFontName=Verdana -;WelcomeFontSize=12 -;TitleFontName=Arial -;TitleFontSize=29 -;CopyrightFontName=Arial -;CopyrightFontSize=8 - -[Messages] - -; *** 应用程序标题 -SetupAppTitle=安装 -SetupWindowTitle=安装 - %1 -UninstallAppTitle=卸载 -UninstallAppFullTitle=%1 卸载 - -; *** Misc. common -InformationTitle=信息 -ConfirmTitle=确认 -ErrorTitle=错误 - -; *** SetupLdr messages -SetupLdrStartupMessage=现在将安装 %1。您想要继续吗? -LdrCannotCreateTemp=无法创建临时文件。安装程序已中止 -LdrCannotExecTemp=无法执行临时目录中的文件。安装程序已中止 -HelpTextNote= - -; *** 启动错误消息 -LastErrorMessage=%1。%n%n错误 %2: %3 -SetupFileMissing=安装目录中缺少文件 %1。请修正这个问题或者获取程序的新副本。 -SetupFileCorrupt=安装文件已损坏。请获取程序的新副本。 -SetupFileCorruptOrWrongVer=安装文件已损坏,或是与这个安装程序的版本不兼容。请修正这个问题或获取新的程序副本。 -InvalidParameter=无效的命令行参数:%n%n%1 -SetupAlreadyRunning=安装程序正在运行。 -WindowsVersionNotSupported=此程序不支持当前计算机运行的 Windows 版本。 -WindowsServicePackRequired=此程序需要 %1 服务包 %2 或更高版本。 -NotOnThisPlatform=此程序不能在 %1 上运行。 -OnlyOnThisPlatform=此程序只能在 %1 上运行。 -OnlyOnTheseArchitectures=此程序只能安装到为下列处理器架构设计的 Windows 版本中:%n%n%1 -WinVersionTooLowError=此程序需要 %1 版本 %2 或更高。 -WinVersionTooHighError=此程序不能安装于 %1 版本 %2 或更高。 -AdminPrivilegesRequired=在安装此程序时您必须以管理员身份登录。 -PowerUserPrivilegesRequired=在安装此程序时您必须以管理员身份或有权限的用户组身份登录。 -SetupAppRunningError=安装程序发现 %1 当前正在运行。%n%n请先关闭正在运行的程序,然后点击“确定”继续,或点击“取消”退出。 -UninstallAppRunningError=卸载程序发现 %1 当前正在运行。%n%n请先关闭正在运行的程序,然后点击“确定”继续,或点击“取消”退出。 - -; *** 启动问题 -PrivilegesRequiredOverrideTitle=选择安装程序模式 -PrivilegesRequiredOverrideInstruction=选择安装模式 -PrivilegesRequiredOverrideText1=%1 可以为所有用户安装(需要管理员权限),或仅为您安装。 -PrivilegesRequiredOverrideText2=%1 只能为您安装,或为所有用户安装(需要管理员权限)。 -PrivilegesRequiredOverrideAllUsers=为所有用户安装(&A) -PrivilegesRequiredOverrideAllUsersRecommended=为所有用户安装(&A) (建议选项) -PrivilegesRequiredOverrideCurrentUser=只为我安装(&M) -PrivilegesRequiredOverrideCurrentUserRecommended=只为我安装(&M) (建议选项) - -; *** 其他错误 -ErrorCreatingDir=安装程序无法创建目录“%1” -ErrorTooManyFilesInDir=无法在目录“%1”中创建文件,因为里面包含太多文件 - -; *** 安装程序公共消息 -ExitSetupTitle=退出安装程序 -ExitSetupMessage=安装程序尚未完成。如果现在退出,将不会安装该程序。%n%n您之后可以再次运行安装程序完成安装。%n%n现在退出安装程序吗? -AboutSetupMenuItem=关于安装程序(&A)... -AboutSetupTitle=关于安装程序 -AboutSetupMessage=%1 版本 %2%n%3%n%n%1 主页:%n%4 -AboutSetupNote= -TranslatorNote=简体中文翻译由Kira(847320916@qq.com)维护。项目地址:https://github.com/kira-96/Inno-Setup-Chinese-Simplified-Translation - -; *** 按钮 -ButtonBack=< 上一步(&B) -ButtonNext=下一步(&N) > -ButtonInstall=安装(&I) -ButtonOK=确定 -ButtonCancel=取消 -ButtonYes=是(&Y) -ButtonYesToAll=全是(&A) -ButtonNo=否(&N) -ButtonNoToAll=全否(&O) -ButtonFinish=完成(&F) -ButtonBrowse=浏览(&B)... -ButtonWizardBrowse=浏览(&R)... -ButtonNewFolder=新建文件夹(&M) - -; *** “选择语言”对话框消息 -SelectLanguageTitle=选择安装语言 -SelectLanguageLabel=选择安装时使用的语言。 - -; *** 公共向导文字 -ClickNext=点击“下一步”继续,或点击“取消”退出安装程序。 -BeveledLabel= -BrowseDialogTitle=浏览文件夹 -BrowseDialogLabel=在下面的列表中选择一个文件夹,然后点击“确定”。 -NewFolderName=新建文件夹 - -; *** “欢迎”向导页 -WelcomeLabel1=欢迎使用 [name] 安装向导 -WelcomeLabel2=现在将安装 [name/ver] 到您的电脑中。%n%n建议您在继续安装前关闭所有其他应用程序。 - -; *** “密码”向导页 -WizardPassword=密码 -PasswordLabel1=这个安装程序有密码保护。 -PasswordLabel3=请输入密码,然后点击“下一步”继续。密码区分大小写。 -PasswordEditLabel=密码(&P): -IncorrectPassword=您输入的密码不正确,请重新输入。 - -; *** “许可协议”向导页 -WizardLicense=许可协议 -LicenseLabel=请在继续安装前阅读以下重要信息。 -LicenseLabel3=请仔细阅读下列许可协议。在继续安装前您必须同意这些协议条款。 -LicenseAccepted=我同意此协议(&A) -LicenseNotAccepted=我不同意此协议(&D) - -; *** “信息”向导页 -WizardInfoBefore=信息 -InfoBeforeLabel=请在继续安装前阅读以下重要信息。 -InfoBeforeClickLabel=准备好继续安装后,点击“下一步”。 -WizardInfoAfter=信息 -InfoAfterLabel=请在继续安装前阅读以下重要信息。 -InfoAfterClickLabel=准备好继续安装后,点击“下一步”。 - -; *** “用户信息”向导页 -WizardUserInfo=用户信息 -UserInfoDesc=请输入您的信息。 -UserInfoName=用户名(&U): -UserInfoOrg=组织(&O): -UserInfoSerial=序列号(&S): -UserInfoNameRequired=您必须输入用户名。 - -; *** “选择目标目录”向导页 -WizardSelectDir=选择目标位置 -SelectDirDesc=您想将 [name] 安装在哪里? -SelectDirLabel3=安装程序将安装 [name] 到下面的文件夹中。 -SelectDirBrowseLabel=点击“下一步”继续。如果您想选择其他文件夹,点击“浏览”。 -DiskSpaceGBLabel=至少需要有 [gb] GB 的可用磁盘空间。 -DiskSpaceMBLabel=至少需要有 [mb] MB 的可用磁盘空间。 -CannotInstallToNetworkDrive=安装程序无法安装到一个网络驱动器。 -CannotInstallToUNCPath=安装程序无法安装到一个 UNC 路径。 -InvalidPath=您必须输入一个带驱动器卷标的完整路径,例如:%n%nC:\APP%n%n或UNC路径:%n%n\\server\share -InvalidDrive=您选定的驱动器或 UNC 共享不存在或不能访问。请选择其他位置。 -DiskSpaceWarningTitle=磁盘空间不足 -DiskSpaceWarning=安装程序至少需要 %1 KB 的可用空间才能安装,但选定驱动器只有 %2 KB 的可用空间。%n%n您一定要继续吗? -DirNameTooLong=文件夹名称或路径太长。 -InvalidDirName=文件夹名称无效。 -BadDirName32=文件夹名称不能包含下列任何字符:%n%n%1 -DirExistsTitle=文件夹已存在 -DirExists=文件夹:%n%n%1%n%n已经存在。您一定要安装到这个文件夹中吗? -DirDoesntExistTitle=文件夹不存在 -DirDoesntExist=文件夹:%n%n%1%n%n不存在。您想要创建此文件夹吗? - -; *** “选择组件”向导页 -WizardSelectComponents=选择组件 -SelectComponentsDesc=您想安装哪些程序组件? -SelectComponentsLabel2=选中您想安装的组件;取消您不想安装的组件。然后点击“下一步”继续。 -FullInstallation=完全安装 -; if possible don't translate 'Compact' as 'Minimal' (I mean 'Minimal' in your language) -CompactInstallation=简洁安装 -CustomInstallation=自定义安装 -NoUninstallWarningTitle=组件已存在 -NoUninstallWarning=安装程序检测到下列组件已安装在您的电脑中:%n%n%1%n%n取消选中这些组件不会卸载它们。%n%n确定要继续吗? -ComponentSize1=%1 KB -ComponentSize2=%1 MB -ComponentsDiskSpaceGBLabel=当前选择的组件需要至少 [gb] GB 的磁盘空间。 -ComponentsDiskSpaceMBLabel=当前选择的组件需要至少 [mb] MB 的磁盘空间。 - -; *** “选择附加任务”向导页 -WizardSelectTasks=选择附加任务 -SelectTasksDesc=您想要安装程序执行哪些附加任务? -SelectTasksLabel2=选择您想要安装程序在安装 [name] 时执行的附加任务,然后点击“下一步”。 - -; *** “选择开始菜单文件夹”向导页 -WizardSelectProgramGroup=选择开始菜单文件夹 -SelectStartMenuFolderDesc=安装程序应该在哪里放置程序的快捷方式? -SelectStartMenuFolderLabel3=安装程序将在下列“开始”菜单文件夹中创建程序的快捷方式。 -SelectStartMenuFolderBrowseLabel=点击“下一步”继续。如果您想选择其他文件夹,点击“浏览”。 -MustEnterGroupName=您必须输入一个文件夹名。 -GroupNameTooLong=文件夹名或路径太长。 -InvalidGroupName=无效的文件夹名字。 -BadGroupName=文件夹名不能包含下列任何字符:%n%n%1 -NoProgramGroupCheck2=不创建开始菜单文件夹(&D) - -; *** “准备安装”向导页 -WizardReady=准备安装 -ReadyLabel1=安装程序准备就绪,现在可以开始安装 [name] 到您的电脑。 -ReadyLabel2a=点击“安装”继续此安装程序。如果您想重新考虑或修改任何设置,点击“上一步”。 -ReadyLabel2b=点击“安装”继续此安装程序。 -ReadyMemoUserInfo=用户信息: -ReadyMemoDir=目标位置: -ReadyMemoType=安装类型: -ReadyMemoComponents=已选择组件: -ReadyMemoGroup=开始菜单文件夹: -ReadyMemoTasks=附加任务: - -; *** TExtractionWizardPage wizard page and Extract7ZipArchive -ExtractionLabel=正在提取附加文件... -ButtonStopExtraction=停止提取(&S) -StopExtraction=您确定要停止提取吗? -ErrorExtractionAborted=提取已中止 -ErrorExtractionFailed=提取失败:%1 - -; *** TDownloadWizardPage wizard page and DownloadTemporaryFile -DownloadingLabel=正在下载附加文件... -ButtonStopDownload=停止下载(&S) -StopDownload=您确定要停止下载吗? -ErrorDownloadAborted=下载已中止 -ErrorDownloadFailed=下载失败:%1 %2 -ErrorDownloadSizeFailed=获取下载大小失败:%1 %2 -ErrorFileHash1=校验文件哈希失败:%1 -ErrorFileHash2=无效的文件哈希:预期 %1,实际 %2 -ErrorProgress=无效的进度:%1 / %2 -ErrorFileSize=文件大小错误:预期 %1,实际 %2 - -; *** “正在准备安装”向导页 -WizardPreparing=正在准备安装 -PreparingDesc=安装程序正在准备安装 [name] 到您的电脑。 -PreviousInstallNotCompleted=先前的程序安装或卸载未完成,您需要重启您的电脑以完成。%n%n在重启电脑后,再次运行安装程序以完成 [name] 的安装。 -CannotContinue=安装程序不能继续。请点击“取消”退出。 -ApplicationsFound=以下应用程序正在使用将由安装程序更新的文件。建议您允许安装程序自动关闭这些应用程序。 -ApplicationsFound2=以下应用程序正在使用将由安装程序更新的文件。建议您允许安装程序自动关闭这些应用程序。安装完成后,安装程序将尝试重新启动这些应用程序。 -CloseApplications=自动关闭应用程序(&A) -DontCloseApplications=不要关闭应用程序(&D) -ErrorCloseApplications=安装程序无法自动关闭所有应用程序。建议您在继续之前,关闭所有在使用需要由安装程序更新的文件的应用程序。 -PrepareToInstallNeedsRestart=安装程序必须重启您的计算机。计算机重启后,请再次运行安装程序以完成 [name] 的安装。%n%n是否立即重新启动? - -; *** “正在安装”向导页 -WizardInstalling=正在安装 -InstallingLabel=安装程序正在安装 [name] 到您的电脑,请稍候。 - -; *** “安装完成”向导页 -FinishedHeadingLabel=[name] 安装完成 -FinishedLabelNoIcons=安装程序已在您的电脑中安装了 [name]。 -FinishedLabel=安装程序已在您的电脑中安装了 [name]。您可以通过已安装的快捷方式运行此应用程序。 -ClickFinish=点击“完成”退出安装程序。 -FinishedRestartLabel=为完成 [name] 的安装,安装程序必须重新启动您的电脑。要立即重启吗? -FinishedRestartMessage=为完成 [name] 的安装,安装程序必须重新启动您的电脑。%n%n要立即重启吗? -ShowReadmeCheck=是,我想查阅自述文件 -YesRadio=是,立即重启电脑(&Y) -NoRadio=否,稍后重启电脑(&N) -; used for example as 'Run MyProg.exe' -RunEntryExec=运行 %1 -; used for example as 'View Readme.txt' -RunEntryShellExec=查阅 %1 - -; *** “安装程序需要下一张磁盘”提示 -ChangeDiskTitle=安装程序需要下一张磁盘 -SelectDiskLabel2=请插入磁盘 %1 并点击“确定”。%n%n如果这个磁盘中的文件可以在下列文件夹之外的文件夹中找到,请输入正确的路径或点击“浏览”。 -PathLabel=路径(&P): -FileNotInDir2=“%2”中找不到文件“%1”。请插入正确的磁盘或选择其他文件夹。 -SelectDirectoryLabel=请指定下一张磁盘的位置。 - -; *** 安装状态消息 -SetupAborted=安装程序未完成安装。%n%n请修正这个问题并重新运行安装程序。 -AbortRetryIgnoreSelectAction=选择操作 -AbortRetryIgnoreRetry=重试(&T) -AbortRetryIgnoreIgnore=忽略错误并继续(&I) -AbortRetryIgnoreCancel=关闭安装程序 - -; *** 安装状态消息 -StatusClosingApplications=正在关闭应用程序... -StatusCreateDirs=正在创建目录... -StatusExtractFiles=正在解压缩文件... -StatusCreateIcons=正在创建快捷方式... -StatusCreateIniEntries=正在创建 INI 条目... -StatusCreateRegistryEntries=正在创建注册表条目... -StatusRegisterFiles=正在注册文件... -StatusSavingUninstall=正在保存卸载信息... -StatusRunProgram=正在完成安装... -StatusRestartingApplications=正在重启应用程序... -StatusRollback=正在撤销更改... - -; *** 其他错误 -ErrorInternal2=内部错误:%1 -ErrorFunctionFailedNoCode=%1 失败 -ErrorFunctionFailed=%1 失败;错误代码 %2 -ErrorFunctionFailedWithMessage=%1 失败;错误代码 %2.%n%3 -ErrorExecutingProgram=无法执行文件:%n%1 - -; *** 注册表错误 -ErrorRegOpenKey=打开注册表项时出错:%n%1\%2 -ErrorRegCreateKey=创建注册表项时出错:%n%1\%2 -ErrorRegWriteKey=写入注册表项时出错:%n%1\%2 - -; *** INI 错误 -ErrorIniEntry=在文件“%1”中创建 INI 条目时出错。 - -; *** 文件复制错误 -FileAbortRetryIgnoreSkipNotRecommended=跳过此文件(&S) (不推荐) -FileAbortRetryIgnoreIgnoreNotRecommended=忽略错误并继续(&I) (不推荐) -SourceIsCorrupted=源文件已损坏 -SourceDoesntExist=源文件“%1”不存在 -ExistingFileReadOnly2=无法替换现有文件,它是只读的。 -ExistingFileReadOnlyRetry=移除只读属性并重试(&R) -ExistingFileReadOnlyKeepExisting=保留现有文件(&K) -ErrorReadingExistingDest=尝试读取现有文件时出错: -FileExistsSelectAction=选择操作 -FileExists2=文件已经存在。 -FileExistsOverwriteExisting=覆盖已存在的文件(&O) -FileExistsKeepExisting=保留现有的文件(&K) -FileExistsOverwriteOrKeepAll=为所有冲突文件执行此操作(&D) -ExistingFileNewerSelectAction=选择操作 -ExistingFileNewer2=现有的文件比安装程序将要安装的文件还要新。 -ExistingFileNewerOverwriteExisting=覆盖已存在的文件(&O) -ExistingFileNewerKeepExisting=保留现有的文件(&K) (推荐) -ExistingFileNewerOverwriteOrKeepAll=为所有冲突文件执行此操作(&D) -ErrorChangingAttr=尝试更改下列现有文件的属性时出错: -ErrorCreatingTemp=尝试在目标目录创建文件时出错: -ErrorReadingSource=尝试读取下列源文件时出错: -ErrorCopying=尝试复制下列文件时出错: -ErrorReplacingExistingFile=尝试替换现有文件时出错: -ErrorRestartReplace=重启并替换失败: -ErrorRenamingTemp=尝试重命名下列目标目录中的一个文件时出错: -ErrorRegisterServer=无法注册 DLL/OCX:%1 -ErrorRegSvr32Failed=RegSvr32 失败;退出代码 %1 -ErrorRegisterTypeLib=无法注册类库:%1 - -; *** 卸载显示名字标记 -; used for example as 'My Program (32-bit)' -UninstallDisplayNameMark=%1 (%2) -; used for example as 'My Program (32-bit, All users)' -UninstallDisplayNameMarks=%1 (%2, %3) -UninstallDisplayNameMark32Bit=32 位 -UninstallDisplayNameMark64Bit=64 位 -UninstallDisplayNameMarkAllUsers=所有用户 -UninstallDisplayNameMarkCurrentUser=当前用户 - -; *** 安装后错误 -ErrorOpeningReadme=尝试打开自述文件时出错。 -ErrorRestartingComputer=安装程序无法重启电脑,请手动重启。 - -; *** 卸载消息 -UninstallNotFound=文件“%1”不存在。无法卸载。 -UninstallOpenError=文件“%1”不能被打开。无法卸载。 -UninstallUnsupportedVer=此版本的卸载程序无法识别卸载日志文件“%1”的格式。无法卸载 -UninstallUnknownEntry=卸载日志中遇到一个未知条目 (%1) -ConfirmUninstall=您确认要完全移除 %1 及其所有组件吗? -UninstallOnlyOnWin64=仅允许在 64 位 Windows 中卸载此程序。 -OnlyAdminCanUninstall=仅使用管理员权限的用户能完成此卸载。 -UninstallStatusLabel=正在从您的电脑中移除 %1,请稍候。 -UninstalledAll=已顺利从您的电脑中移除 %1。 -UninstalledMost=%1 卸载完成。%n%n有部分内容未能被删除,但您可以手动删除它们。 -UninstalledAndNeedsRestart=为完成 %1 的卸载,需要重启您的电脑。%n%n立即重启电脑吗? -UninstallDataCorrupted=文件“%1”已损坏。无法卸载 - -; *** 卸载状态消息 -ConfirmDeleteSharedFileTitle=删除共享的文件吗? -ConfirmDeleteSharedFile2=系统表示下列共享的文件已不有其他程序使用。您希望卸载程序删除这些共享的文件吗?%n%n如果删除这些文件,但仍有程序在使用这些文件,则这些程序可能出现异常。如果您不能确定,请选择“否”,在系统中保留这些文件以免引发问题。 -SharedFileNameLabel=文件名: -SharedFileLocationLabel=位置: -WizardUninstalling=卸载状态 -StatusUninstalling=正在卸载 %1... - -; *** Shutdown block reasons -ShutdownBlockReasonInstallingApp=正在安装 %1。 -ShutdownBlockReasonUninstallingApp=正在卸载 %1。 - -; The custom messages below aren't used by Setup itself, but if you make -; use of them in your scripts, you'll want to translate them. - -[CustomMessages] - -NameAndVersion=%1 版本 %2 -AdditionalIcons=附加快捷方式: -CreateDesktopIcon=创建桌面快捷方式(&D) -CreateQuickLaunchIcon=创建快速启动栏快捷方式(&Q) -ProgramOnTheWeb=%1 网站 -UninstallProgram=卸载 %1 -LaunchProgram=运行 %1 -AssocFileExtension=将 %2 文件扩展名与 %1 建立关联(&A) -AssocingFileExtension=正在将 %2 文件扩展名与 %1 建立关联... -AutoStartProgramGroupDescription=启动: -AutoStartProgram=自动启动 %1 -AddonHostProgramNotFound=您选择的文件夹中无法找到 %1。%n%n您要继续吗? diff --git a/crates/zed/resources/windows/messages/en.isl b/crates/zed/resources/windows/messages/en.isl deleted file mode 100644 index 2e82bea4ff..0000000000 --- a/crates/zed/resources/windows/messages/en.isl +++ /dev/null @@ -1,15 +0,0 @@ -[Messages] -FinishedLabel=Setup has finished installing [name] on your computer. The application may be launched by selecting the installed shortcuts. -ConfirmUninstall=Are you sure you want to completely remove %1 and all of its components? - -[CustomMessages] -AdditionalIcons=Additional icons: -CreateDesktopIcon=Create a &desktop icon -AddContextMenuFiles=Add "Open with %1" action to Windows Explorer file context menu -AddContextMenuFolders=Add "Open with %1" action to Windows Explorer directory context menu -AssociateWithFiles=Register %1 as an editor for supported file types -AddToPath=Add to PATH (requires shell restart) -RunAfter=Run %1 after installation -Other=Other: -SourceFile=%1 Source File -OpenWithContextMenu=Open w&ith %1 diff --git a/crates/zed/resources/windows/messages/zh-cn.isl b/crates/zed/resources/windows/messages/zh-cn.isl deleted file mode 100644 index 50c03ccaaf..0000000000 --- a/crates/zed/resources/windows/messages/zh-cn.isl +++ /dev/null @@ -1,9 +0,0 @@ -[CustomMessages] -AddContextMenuFiles=将“通过 %1 打开”操作添加到 Windows 资源管理器文件上下文菜单 -AddContextMenuFolders=将“通过 %1 打开”操作添加到 Windows 资源管理器目录上下文菜单 -AssociateWithFiles=将 %1 注册为受支持的文件类型的编辑器 -AddToPath=添加到 PATH (重启后生效) -RunAfter=安装后运行 %1 -Other=其他: -SourceFile=%1 源文件 -OpenWithContextMenu=通过 %1 打开 diff --git a/crates/zed/resources/windows/sign.ps1 b/crates/zed/resources/windows/sign.ps1 deleted file mode 100644 index d00b33c0fc..0000000000 --- a/crates/zed/resources/windows/sign.ps1 +++ /dev/null @@ -1,53 +0,0 @@ -param ( - [Parameter(Mandatory = $true)] - [string]$filePath -) - -$params = @{} - -$endpoint = $ENV:ENDPOINT -if ([string]::IsNullOrWhiteSpace($endpoint)) { - throw "The 'ENDPOINT' env is required." -} -$params["Endpoint"] = $endpoint - -$trustedSigningAccountName = $ENV:ACCOUNT_NAME -if ([string]::IsNullOrWhiteSpace($trustedSigningAccountName)) { - throw "The 'ACCOUNT_NAME' env is required." -} -$params["CodeSigningAccountName"] = $trustedSigningAccountName - -$certificateProfileName = $ENV:CERT_PROFILE_NAME -if ([string]::IsNullOrWhiteSpace($certificateProfileName)) { - throw "The 'CERT_PROFILE_NAME' env is required." -} -$params["CertificateProfileName"] = $certificateProfileName - -$fileDigest = $ENV:FILE_DIGEST -if ([string]::IsNullOrWhiteSpace($fileDigest)) { - throw "The 'FILE_DIGEST' env is required." -} -$params["FileDigest"] = $fileDigest - -$timeStampDigest = $ENV:TIMESTAMP_DIGEST -if ([string]::IsNullOrWhiteSpace($timeStampDigest)) { - throw "The 'TIMESTAMP_DIGEST' env is required." -} -$params["TimestampDigest"] = $timeStampDigest - -$timeStampServer = $ENV:TIMESTAMP_SERVER -if ([string]::IsNullOrWhiteSpace($timeStampServer)) { - throw "The 'TIMESTAMP_SERVER' env is required." -} -$params["TimestampRfc3161"] = $timeStampServer - -$params["Files"] = $filePath - -$trace = $ENV:TRACE -if (-Not [string]::IsNullOrWhiteSpace($trace)) { - if ([System.Convert]::ToBoolean($trace)) { - Set-PSDebug -Trace 2 - } -} - -Invoke-TrustedSigning @params diff --git a/crates/zed/resources/windows/zed.iss b/crates/zed/resources/windows/zed.iss deleted file mode 100644 index 9df6d3b228..0000000000 --- a/crates/zed/resources/windows/zed.iss +++ /dev/null @@ -1,1421 +0,0 @@ -[Setup] -AppId={#AppId} -AppName={#AppName} -AppVerName={#AppDisplayName} -AppPublisher=Zed Industries -AppPublisherURL=https://www.zed.dev/ -AppSupportURL=https://www.zed.dev/ -AppUpdatesURL=https://www.zed.dev/ -DefaultGroupName={#AppName} -DisableProgramGroupPage=yes -DisableReadyPage=yes -AllowNoIcons=yes -OutputDir={#OutputDir} -OutputBaseFilename={#AppSetupName} -Compression=lzma -SolidCompression=yes -AppMutex={code:GetAppMutex} -SetupMutex={#AppMutex}Setup -; WizardImageFile="{#ResourcesDir}\inno-100.bmp,{#ResourcesDir}\inno-125.bmp,{#ResourcesDir}\inno-150.bmp,{#ResourcesDir}\inno-175.bmp,{#ResourcesDir}\inno-200.bmp,{#ResourcesDir}\inno-225.bmp,{#ResourcesDir}\inno-250.bmp" -; WizardSmallImageFile="{#ResourcesDir}\inno-small-100.bmp,{#ResourcesDir}\inno-small-125.bmp,{#ResourcesDir}\inno-small-150.bmp,{#ResourcesDir}\inno-small-175.bmp,{#ResourcesDir}\inno-small-200.bmp,{#ResourcesDir}\inno-small-225.bmp,{#ResourcesDir}\inno-small-250.bmp" -SetupIconFile={#ResourcesDir}\{#AppIconName}.ico -UninstallDisplayIcon={app}\{#AppExeName}.exe -ChangesEnvironment=true -ChangesAssociations=true -MinVersion=10.0.16299 -SourceDir={#SourceDir} -AppVersion={#Version} -VersionInfoVersion={#Version} -ShowLanguageDialog=auto -WizardStyle=modern - -CloseApplications=force - -#if GetEnv("CI") != "" -SignTool=Defaultsign -#endif - -DefaultDirName={autopf}\{#AppName} -PrivilegesRequired=lowest - -ArchitecturesAllowed=x64compatible -ArchitecturesInstallIn64BitMode=x64compatible - -[Languages] -Name: "english"; MessagesFile: "compiler:Default.isl,{#ResourcesDir}\messages\en.isl"; LicenseFile: "script\terms\terms.rtf" -Name: "simplifiedChinese"; MessagesFile: "{#ResourcesDir}\messages\Default.zh-cn.isl,{#ResourcesDir}\messages\zh-cn.isl"; LicenseFile: "script\terms\terms.rtf" - -[UninstallDelete] -; Delete logs -Type: filesandordirs; Name: "{app}\tools" -Type: filesandordirs; Name: "{app}\updates" -; Delete newer files which may not have been added by the initial installation -Type: filesandordirs; Name: "{app}\x64" -Type: filesandordirs; Name: "{app}\arm64" - - -[Tasks] -Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked -Name: "addcontextmenufiles"; Description: "{cm:AddContextMenuFiles,{#AppDisplayName}}"; GroupDescription: "{cm:Other}" -Name: "addcontextmenufolders"; Description: "{cm:AddContextMenuFolders,{#AppDisplayName}}"; GroupDescription: "{cm:Other}"; Flags: unchecked; Check: not IsWindows11OrLater -Name: "associatewithfiles"; Description: "{cm:AssociateWithFiles,{#AppDisplayName}}"; GroupDescription: "{cm:Other}" -Name: "addtopath"; Description: "{cm:AddToPath}"; GroupDescription: "{cm:Other}" - -[Dirs] -Name: "{app}"; AfterInstall: DisableAppDirInheritance - -[Files] -Source: "{#ResourcesDir}\Zed.exe"; DestDir: "{code:GetInstallDir}"; Flags: ignoreversion -Source: "{#ResourcesDir}\bin\*"; DestDir: "{code:GetInstallDir}\bin"; Flags: ignoreversion -Source: "{#ResourcesDir}\tools\*"; DestDir: "{app}\tools"; Flags: ignoreversion -Source: "{#ResourcesDir}\appx\*"; DestDir: "{app}\appx"; BeforeInstall: RemoveAppxPackage; AfterInstall: AddAppxPackage; Flags: ignoreversion; Check: IsWindows11OrLater -#ifexist ResourcesDir + "\amd_ags_x64.dll" -Source: "{#ResourcesDir}\amd_ags_x64.dll"; DestDir: "{app}"; Flags: ignoreversion -#endif -#ifexist ResourcesDir + "\x64\OpenConsole.exe" -Source: "{#ResourcesDir}\x64\OpenConsole.exe"; DestDir: "{code:GetInstallDir}\x64"; Flags: ignoreversion -#endif -#ifexist ResourcesDir + "\arm64\OpenConsole.exe" -Source: "{#ResourcesDir}\arm64\OpenConsole.exe"; DestDir: "{code:GetInstallDir}\arm64"; Flags: ignoreversion -#endif -Source: "{#ResourcesDir}\conpty.dll"; DestDir: "{code:GetInstallDir}"; Flags: ignoreversion - -[Icons] -Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}.exe"; AppUserModelID: "{#AppUserId}" -Name: "{autodesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}.exe"; Tasks: desktopicon; AppUserModelID: "{#AppUserId}" - -[Run] -Filename: "{app}\{#AppExeName}.exe"; Description: "{cm:LaunchProgram,{#AppName}}"; Flags: nowait postinstall; Check: WizardNotSilent - -[UninstallRun] -Filename: "powershell.exe"; Parameters: "Invoke-Command -ScriptBlock {{Remove-AppxPackage -Package ""{#AppxFullName}""}"; Check: IsWindows11OrLater; Flags: shellexec waituntilterminated runhidden - -[Registry] -Root: HKCU; Subkey: "Software\Classes\.ascx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.ascx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ascx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ascx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ASCX}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ascx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ascx\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ascx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ascx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.asp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.asp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.asp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.asp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ASP}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.asp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.asp\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.asp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.asp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.aspx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.aspx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.aspx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.aspx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ASPX}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.aspx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.aspx\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.aspx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.aspx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.bash\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.bash\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bash"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.bash_login\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.bash_login\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bash_login"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_login"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash Login}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_login"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_login\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_login\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_login\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.bash_logout\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.bash_logout\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bash_logout"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_logout"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash Logout}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_logout"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_logout\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_logout\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_logout\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.bash_profile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.bash_profile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bash_profile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_profile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash Profile}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_profile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_profile\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_profile\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bash_profile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.bashrc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.bashrc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bashrc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bashrc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bash RC}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bashrc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bashrc\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bashrc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bashrc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.bib\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.bib\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bib"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bib"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,BibTeX}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bib"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bib\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bib\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bib\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.bowerrc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.bowerrc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.bowerrc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bowerrc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Bower RC}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bowerrc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bowerrc\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bowerrc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.bowerrc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.c++\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.c++\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.c++"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.c++"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.c++"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.c++\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.c++\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.c\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.c\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.c"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.c"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.c"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.c\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.c\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.c\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.cc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.cc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cc\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.cfg\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.cfg\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cfg"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cfg"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Configuration}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cfg"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cfg\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cfg\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cfg\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.cjs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.cjs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cjs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cjs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cjs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cjs\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cjs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cjs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.clj\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.clj\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.clj"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.clj"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Clojure}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.clj"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.clj\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.clj\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.clj\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.cljs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.cljs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cljs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cljs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ClojureScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cljs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cljs\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cljs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cljs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.cljx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.cljx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cljx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cljx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CLJX}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cljx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cljx\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cljx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cljx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.clojure\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.clojure\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.clojure"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.clojure"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Clojure}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.clojure"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.clojure\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.clojure\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.clojure\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.cls\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.cls\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cls"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cls"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,LaTeX}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cls"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cls\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cls\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cls\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.code-workspace\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.code-workspace\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.code-workspace"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.code-workspace"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Code Workspace}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.code"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.code-workspace\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.code-workspace\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.code-workspace\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.cmake\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.cmake\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cmake"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cmake"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CMake}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cmake"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cmake\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cmake\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cmake\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.coffee\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.coffee\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.coffee"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.coffee"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CoffeeScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.coffee"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.coffee\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.coffee\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.coffee\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.config\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.config\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.config"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.config"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Configuration}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.config"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.config\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.config\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.config\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.containerfile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.containerfile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.containerfile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.containerfile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Containerfile}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.containerfile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.containerfile\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.containerfile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.cpp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.cpp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cpp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cpp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cpp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cpp\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cpp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cpp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.cs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.cs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C#}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cs\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.cshtml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.cshtml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cshtml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cshtml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CSHTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cshtml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cshtml\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cshtml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cshtml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.csproj\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.csproj\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.csproj"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csproj"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C# Project}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csproj"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csproj\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csproj\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csproj\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.css\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.css\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.css"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.css"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CSS}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.css"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.css\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.css\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.css\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.csv\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.csv\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.csv"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csv"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Comma Separated Values}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csv"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csv\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csv\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csv\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.csx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.csx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.csx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C# Script}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csx\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.csx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.ctp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.ctp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ctp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ctp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,CakePHP Template}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ctp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ctp\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ctp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ctp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.cxx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.cxx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.cxx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cxx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cxx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cxx\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cxx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.cxx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.dart\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.dart\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.dart"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dart"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Dart}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dart"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dart\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dart\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dart\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.diff\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.diff\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.diff"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.diff"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Diff}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.diff"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.diff\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.diff\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.diff\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.dockerfile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.dockerfile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.dockerfile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dockerfile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Dockerfile}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dockerfile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dockerfile\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dockerfile\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dockerfile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.dot\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.dot\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.dot"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dot"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Dot}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dot"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dot\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dot\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dot\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.dtd\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.dtd\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.dtd"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dtd"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Document Type Definition}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dtd"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dtd\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dtd\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.dtd\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.editorconfig\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.editorconfig\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.editorconfig"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.editorconfig"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Editor Config}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.editorconfig"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.editorconfig\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.editorconfig\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.editorconfig\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.edn\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.edn\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.edn"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.edn"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Extensible Data Notation}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.edn"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.edn\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.edn\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.edn\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.erb\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.erb\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.erb"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.erb"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Ruby}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.erb"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.erb\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.erb\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.erb\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.eyaml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.eyaml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.eyaml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.eyaml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Hiera Eyaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.eyaml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.eyaml\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.eyaml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.eyaml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.eyml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.eyml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.eyml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.eyml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Hiera Eyaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.eyml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.eyml\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.eyml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.eyml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.fs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.fs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.fs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,F#}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fs\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.fsi\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.fsi\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.fsi"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsi"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,F# Signature}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsi"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsi\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsi\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsi\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.fsscript\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.fsscript\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.fsscript"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsscript"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,F# Script}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsscript"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsscript\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsscript\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsscript\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.fsx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.fsx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.fsx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,F# Script}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsx\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.fsx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.gemspec\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.gemspec\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.gemspec"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gemspec"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Gemspec}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gemspec"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gemspec\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gemspec\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gemspec\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.gitattributes\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.gitattributes\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.gitattributes"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitattributes"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Git Attributes}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitattributes"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitattributes"; ValueType: string; ValueName: "AlwaysShowExt"; ValueData: ""; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitattributes\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitattributes\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitattributes\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.gitconfig\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.gitconfig\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.gitconfig"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitconfig"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Git Config}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitconfig"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitconfig"; ValueType: string; ValueName: "AlwaysShowExt"; ValueData: ""; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitconfig\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitconfig\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitconfig\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.gitignore\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.gitignore\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.gitignore"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitignore"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Git Ignore}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitignore"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitignore"; ValueType: string; ValueName: "AlwaysShowExt"; ValueData: ""; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitignore\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitignore\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gitignore\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.go\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.go\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.go"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.go"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Go}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.go"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.go\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.go\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.go\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.gradle\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.gradle\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.gradle"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gradle"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Gradle}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gradle"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gradle\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gradle\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.gradle\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.groovy\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.groovy\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.groovy"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.groovy"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Groovy}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.groovy"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.groovy\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.groovy\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.groovy\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.h\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.h\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.h"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.h"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.h"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.h\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.h\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.h\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.handlebars\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.handlebars\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.handlebars"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.handlebars"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Handlebars}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.handlebars"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.handlebars\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.handlebars\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.handlebars\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.hbs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.hbs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.hbs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hbs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Handlebars}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hbs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hbs\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hbs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hbs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.h++\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.h++\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.h++"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.h++"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++ Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.h++"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.h++\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.h++\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.hh\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.hh\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.hh"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hh"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++ Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hh"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hh\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hh\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hh\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.hpp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.hpp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.hpp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hpp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++ Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hpp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hpp\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hpp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hpp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.htm\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.htm\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.htm"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.htm"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,HTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.htm"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.htm\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.htm\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.htm\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.html\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.html\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.html"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.html"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,HTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.html"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.html\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.html\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.html\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.hxx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.hxx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.hxx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hxx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,C++ Header}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hxx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hxx\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hxx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.hxx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.ini\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.ini\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ini"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ini"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,INI}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ini"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ini\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ini\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ini\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.ipynb\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.ipynb\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ipynb"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ipynb"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Jupyter}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ipynb"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ipynb\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ipynb\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ipynb\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.jade\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.jade\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jade"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jade"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Jade}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jade"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jade\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jade\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jade\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.jav\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.jav\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jav"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jav"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Java}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jav"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jav\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jav\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jav\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.java\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.java\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.java"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.java"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Java}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.java"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.java\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.java\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.java\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.js\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.js\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.js"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.js"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.js"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.js\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.js\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.js\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.jsx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.jsx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jsx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jsx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jsx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jsx\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jsx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jsx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.jscsrc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.jscsrc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jscsrc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jscsrc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JSCS RC}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jscsrc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jscsrc\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jscsrc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jscsrc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.jshintrc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.jshintrc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jshintrc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jshintrc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JSHint RC}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jshintrc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jshintrc\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jshintrc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jshintrc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.jshtm\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.jshtm\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jshtm"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jshtm"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript HTML Template}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jshtm"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jshtm\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jshtm\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jshtm\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.json\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.json\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.json"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.json"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JSON}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.json"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.json\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.json\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.json\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.jsp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.jsp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.jsp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jsp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Java Server Pages}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jsp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jsp\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jsp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.jsp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.less\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.less\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.less"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.less"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,LESS}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.less"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.less\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.less\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.less\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.log\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.log\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.log"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.log"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Log file}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.log"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.log\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.log\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.log\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.lua\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.lua\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.lua"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.lua"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Lua}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.lua"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.lua\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.lua\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.lua\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.m\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.m\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.m"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.m"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Objective C}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.m"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.m\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.m\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.m\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.makefile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.makefile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.makefile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.makefile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Makefile}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.makefile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.makefile\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.makefile\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.makefile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.markdown\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.markdown\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.markdown"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.markdown"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.markdown"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.markdown\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.markdown\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.markdown\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.md\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.md\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.md"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.md"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.md"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.md\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.md\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.md\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.mdoc\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.mdoc\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdoc"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdoc"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,MDoc}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdoc"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdoc\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdoc\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdoc\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.mdown\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.mdown\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdown"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdown"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdown"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdown\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdown\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdown\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.mdtext\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.mdtext\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdtext"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdtext"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdtext"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdtext\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdtext\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdtext\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.mdtxt\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.mdtxt\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdtxt"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdtxt"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdtxt"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdtxt\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdtxt\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdtxt\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.mdwn\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.mdwn\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mdwn"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdwn"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdwn"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdwn\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdwn\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mdwn\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.mk\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.mk\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mk"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mk"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Makefile}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mk"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mk\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mk\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mk\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.mkd\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.mkd\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mkd"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mkd"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mkd"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mkd\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mkd\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mkd\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.mkdn\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.mkdn\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mkdn"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mkdn"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Markdown}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mkdn"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mkdn\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mkdn\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mkdn\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.ml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.ml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,OCaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ml\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.mli\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.mli\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mli"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mli"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,OCaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mli"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mli\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mli\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mli\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.mjs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.mjs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.mjs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mjs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,JavaScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mjs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mjs\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mjs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.mjs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.npmignore\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.npmignore\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.npmignore"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.npmignore"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,NPM Ignore}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.npmignore"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.npmignore"; ValueType: string; ValueName: "AlwaysShowExt"; ValueData: ""; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.npmignore\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.npmignore\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.npmignore\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.php\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.php\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.php"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.php"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PHP}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.php"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.php\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.php\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.php\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.phtml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.phtml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.phtml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.phtml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PHP HTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.phtml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.phtml\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.phtml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.phtml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.pl\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.pl\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pl"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pl"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pl"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pl\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pl\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pl\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.pl6\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.pl6\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pl6"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pl6"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl 6}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pl6"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pl6\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pl6\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pl6\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.plist\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.plist\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.plist"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.plist"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Properties file}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.plist"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.plist\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.plist\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.plist\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.pm\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.pm\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pm"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pm"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl Module}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pm"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pm\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pm\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pm\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.pm6\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.pm6\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pm6"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pm6"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl 6 Module}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pm6"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pm6\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pm6\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pm6\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.pod\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.pod\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pod"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pod"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl POD}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pod"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pod\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pod\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pod\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.pp\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.pp\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pp"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pp"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pp"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pp\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pp\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pp\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.profile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.profile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.profile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.profile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Profile}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.profile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.profile\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.profile\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.profile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.properties\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.properties\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.properties"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.properties"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Properties}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.properties"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.properties\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.properties\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.properties\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.ps1\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.ps1\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ps1"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ps1"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PowerShell}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ps1"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ps1\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ps1\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ps1\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.psd1\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.psd1\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.psd1"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psd1"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PowerShell Module Manifest}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psd1"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psd1\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psd1\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psd1\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.psgi\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.psgi\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.psgi"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psgi"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl CGI}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psgi"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psgi\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psgi\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psgi\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.psm1\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.psm1\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.psm1"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psm1"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,PowerShell Module}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psm1"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psm1\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psm1\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.psm1\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.py\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.py\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.py"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.py"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Python}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.py"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.py\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.py\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.py\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.pyi\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.pyi\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.pyi"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pyi"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Python}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pyi"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pyi\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pyi\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.pyi\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.r\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.r\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.r"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.r"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,R}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.r"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.r\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.r\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.r\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.rb\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.rb\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rb"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rb"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Ruby}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rb"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rb\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rb\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rb\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.rhistory\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.rhistory\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rhistory"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rhistory"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,R History}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rhistory"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rhistory\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rhistory\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rhistory\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.rprofile\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.rprofile\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rprofile"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rprofile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,R Profile}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rprofile"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rprofile\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rprofile\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rprofile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.rs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.rs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Rust}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rs\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.rst\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.rst\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rst"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rst"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Restructured Text}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rst"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rst\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rst\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rst\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.rt\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.rt\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.rt"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rt"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Rich Text}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rt"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rt\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rt\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.rt\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.sass\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.sass\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.sass"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sass"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Sass}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sass"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sass\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sass\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sass\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.scss\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.scss\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.scss"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.scss"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Sass}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.scss"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.scss\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.scss\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.scss\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.sh\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.sh\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.sh"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sh"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SH}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sh"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sh\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sh\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sh\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.shtml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.shtml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.shtml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.shtml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SHTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.shtml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.shtml\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.shtml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.shtml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.sql\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.sql\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.sql"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sql"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SQL}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sql"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sql\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sql\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.sql\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.svg\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.svg\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.svg"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.svg"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,SVG}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.svg"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.svg\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.svg\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.svg\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.t\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.t\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.t"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.t"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Perl}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.t"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.t\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.t\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.t\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.tex\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.tex\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.tex"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.tex"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,LaTeX}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.tex"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.tex\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.tex\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.tex\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.ts\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.ts\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.ts"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ts"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,TypeScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ts"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ts\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ts\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.ts\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.toml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.toml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.toml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.toml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Toml}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.toml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.toml\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.toml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.toml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.tsx\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.tsx\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.tsx"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.tsx"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,TypeScript}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.tsx"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.tsx\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.tsx\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.tsx\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.txt\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.txt\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.txt"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.txt"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Text}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.txt"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.txt\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.txt\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.txt\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.vb\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.vb\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.vb"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.vb"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Visual Basic}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.vb"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.vb\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.vb\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.vb\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.vue\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.vue\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.vue"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.vue"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,VUE}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.vue"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.vue\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.vue\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.vue\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.wxi\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.wxi\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.wxi"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxi"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,WiX Include}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxi"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxi\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxi\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxi\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.wxl\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.wxl\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.wxl"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxl"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,WiX Localization}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxl"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxl\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxl\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxl\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.wxs\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.wxs\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.wxs"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxs"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,WiX}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxs"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxs\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxs\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.wxs\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.xaml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.xaml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.xaml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xaml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,XAML}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xaml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xaml\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xaml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xaml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.xhtml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.xhtml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.xhtml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xhtml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,HTML}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xhtml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xhtml\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xhtml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xhtml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.xml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.xml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.xml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,XML}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xml\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.xml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.yaml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.yaml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.yaml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.yaml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Yaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.yaml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.yaml\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.yaml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.yaml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.yml\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.yml\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.yml"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.yml"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,Yaml}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.yml"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.yml\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.yml\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.yml\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\.zsh\OpenWithProgids"; ValueType: none; ValueName: "{#RegValueName}"; Flags: deletevalue uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\.zsh\OpenWithProgids"; ValueType: string; ValueName: "{#RegValueName}.zsh"; ValueData: ""; Flags: uninsdeletevalue; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.zsh"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,ZSH}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.zsh"; ValueType: string; ValueName: "AppUserModelID"; ValueData: "{#AppUserId}"; Flags: uninsdeletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.zsh\DefaultIcon"; ValueType: none; Flags: deletekey; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.zsh\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe"""; Tasks: associatewithfiles -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}.zsh\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: associatewithfiles - -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}SourceFile"; ValueType: string; ValueName: ""; ValueData: "{cm:SourceFile,{#AppName}}"; Flags: uninsdeletekey -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}SourceFile\DefaultIcon"; ValueType: none; Flags: deletekey -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}SourceFile\shell\open"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe""" -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}SourceFile\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1""" - -Root: HKCU; Subkey: "Software\Classes\Applications\{#AppExeName}.exe"; ValueType: none; ValueName: ""; Flags: uninsdeletekey -Root: HKCU; Subkey: "Software\Classes\Applications\{#AppExeName}.exe\DefaultIcon"; ValueType: none; Flags: deletekey -Root: HKCU; Subkey: "Software\Classes\Applications\{#AppExeName}.exe\shell\open"; ValueType: string; ValueName: "Icon"; ValueData: """{app}\{#AppExeName}.exe""" -Root: HKCU; Subkey: "Software\Classes\Applications\{#AppExeName}.exe\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1""" - -Root: HKCU; Subkey: "Software\Classes\{#RegValueName}ContextMenu"; ValueType: expandsz; ValueName: "Title"; ValueData: "{cm:OpenWithContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufiles; Flags: uninsdeletekey; Check: IsWindows11OrLater -Root: HKCU; Subkey: "Software\Classes\*\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufiles; Flags: uninsdeletekey; Check: not IsWindows11OrLater -Root: HKCU; Subkey: "Software\Classes\*\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#AppExeName}.exe"; Tasks: addcontextmenufiles; Check: not IsWindows11OrLater -Root: HKCU; Subkey: "Software\Classes\*\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%1"""; Tasks: addcontextmenufiles; Check: not IsWindows11OrLater -Root: HKCU; Subkey: "Software\Classes\directory\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: IsWindows11OrLater -Root: HKCU; Subkey: "Software\Classes\directory\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#AppExeName}.exe"; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater -Root: HKCU; Subkey: "Software\Classes\directory\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater -Root: HKCU; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: not IsWindows11OrLater -Root: HKCU; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#AppExeName}.exe"; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater -Root: HKCU; Subkey: "Software\Classes\directory\background\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater -Root: HKCU; Subkey: "Software\Classes\Drive\shell\{#RegValueName}"; ValueType: expandsz; ValueName: ""; ValueData: "{cm:OpenWithContextMenu,{#ShellNameShort}}"; Tasks: addcontextmenufolders; Flags: uninsdeletekey; Check: not IsWindows11OrLater -Root: HKCU; Subkey: "Software\Classes\Drive\shell\{#RegValueName}"; ValueType: expandsz; ValueName: "Icon"; ValueData: "{app}\{#AppExeName}.exe"; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater -Root: HKCU; Subkey: "Software\Classes\Drive\shell\{#RegValueName}\command"; ValueType: expandsz; ValueName: ""; ValueData: """{app}\{#AppExeName}.exe"" ""%V"""; Tasks: addcontextmenufolders; Check: not IsWindows11OrLater - -; Environment -Root: HKCU; Subkey: "Environment"; ValueType: expandsz; ValueName: "Path"; ValueData: "{code:AddToPath|{app}\bin}"; Tasks: addtopath; Check: NeedsAddToPath(ExpandConstant('{app}\bin')) - -; URI Scheme -Root: HKCU; Subkey: "Software\Classes\zed"; ValueType: "string"; ValueData: "URL:zed Protocol"; Flags: uninsdeletekey -Root: HKCU; Subkey: "Software\Classes\zed"; ValueType: "string"; ValueName: "URL Protocol"; ValueData: "" -Root: HKCU; Subkey: "Software\Classes\zed\DefaultIcon"; ValueType: "string"; ValueData: "{app}\Zed.exe,1" -Root: HKCU; Subkey: "Software\Classes\zed\shell\open\command"; ValueType: "string"; ValueData: """{app}\Zed.exe"" ""%1""" - -[Code] -function WizardNotSilent(): Boolean; -begin - Result := not WizardSilent(); -end; - -function IsWindows11OrLater(): Boolean; -begin - Result := (GetWindowsVersion >= $0A0055F0); -end; - -// https://stackoverflow.com/a/23838239/261019 -procedure Explode(var Dest: TArrayOfString; Text: String; Separator: String); -var - i, p: Integer; -begin - i := 0; - repeat - SetArrayLength(Dest, i+1); - p := Pos(Separator,Text); - if p > 0 then begin - Dest[i] := Copy(Text, 1, p-1); - Text := Copy(Text, p + Length(Separator), Length(Text)); - i := i + 1; - end else begin - Dest[i] := Text; - Text := ''; - end; - until Length(Text)=0; -end; - -function NeedsAddToPath(path: string): boolean; -var - OrigPath: string; -begin - if not RegQueryStringValue(HKCU, 'Environment', 'Path', OrigPath) - then begin - Result := True; - exit; - end; - Result := Pos(';' + path + ';', ';' + OrigPath + ';') = 0; -end; - -function AddToPath(path: string): string; -var - OrigPath: string; -begin - RegQueryStringValue(HKCU, 'Environment', 'Path', OrigPath) - - if (Length(OrigPath) > 0) and (OrigPath[Length(OrigPath)] = ';') then - Result := OrigPath + path - else - Result := OrigPath + ';' + path -end; - -procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); -var - Path: string; - InstalledPath: string; - Parts: TArrayOfString; - NewPath: string; - i: Integer; -begin - if not CurUninstallStep = usUninstall then begin - exit; - end; - if not RegQueryStringValue(HKCU, 'Environment', 'Path', Path) - then begin - exit; - end; - NewPath := ''; - InstalledPath := ExpandConstant('{app}\bin') - Explode(Parts, Path, ';'); - for i:=0 to GetArrayLength(Parts)-1 do begin - if CompareText(Parts[i], InstalledPath) <> 0 then begin - NewPath := NewPath + Parts[i]; - - if i < GetArrayLength(Parts) - 1 then begin - NewPath := NewPath + ';'; - end; - end; - end; - RegWriteExpandStringValue(HKCU, 'Environment', 'Path', NewPath); -end; - -// https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/icacls -// https://docs.microsoft.com/en-US/windows/security/identity-protection/access-control/security-identifiers -procedure DisableAppDirInheritance(); -var - ResultCode: Integer; - Permissions: string; -begin - Permissions := '/grant:r "*S-1-5-18:(OI)(CI)F" /grant:r "*S-1-5-32-544:(OI)(CI)F" /grant:r "*S-1-5-11:(OI)(CI)RX" /grant:r "*S-1-5-32-545:(OI)(CI)RX"'; - - Permissions := Permissions + Format(' /grant:r "*S-1-3-0:(OI)(CI)F" /grant:r "%s:(OI)(CI)F"', [GetUserNameString()]); - - Exec(ExpandConstant('{sys}\icacls.exe'), ExpandConstant('"{app}" /inheritancelevel:r ') + Permissions, '', SW_HIDE, ewWaitUntilTerminated, ResultCode); -end; - -procedure AddAppxPackage(); -var - AddAppxPackageResultCode: Integer; -begin - if WizardIsTaskSelected('addcontextmenufiles') then begin - ShellExec('', 'powershell.exe', '-Command ' + AddQuotes('Add-AppxPackage -Path ''' + ExpandConstant('{app}\appx\zed_explorer_command_injector.appx') + ''' -ExternalLocation ''' + ExpandConstant('{app}\appx') + ''''), '', SW_HIDE, ewWaitUntilTerminated, AddAppxPackageResultCode); - RegDeleteKeyIncludingSubkeys(HKCU, 'Software\Classes\*\shell\{#RegValueName}'); - RegDeleteKeyIncludingSubkeys(HKCU, 'Software\Classes\directory\shell\{#RegValueName}'); - RegDeleteKeyIncludingSubkeys(HKCU, 'Software\Classes\directory\background\shell\{#RegValueName}'); - RegDeleteKeyIncludingSubkeys(HKCU, 'Software\Classes\Drive\shell\{#RegValueName}'); - end; -end; - -procedure RemoveAppxPackage(); -var - RemoveAppxPackageResultCode: Integer; -begin - ShellExec('', 'powershell.exe', '-Command ' + AddQuotes('Remove-AppxPackage -Package ''{#AppxFullName}'''), '', SW_HIDE, ewWaitUntilTerminated, RemoveAppxPackageResultCode); - if not WizardIsTaskSelected('addcontextmenufiles') then begin - RegDeleteKeyIncludingSubkeys(HKCU, 'Software\Classes\{#RegValueName}ContextMenu'); - end; -end; - -function SwitchHasValue(Name: string; Value: string): Boolean; -begin - Result := CompareText(ExpandConstant('{param:' + Name + '}'), Value) = 0; -end; - -function IsUpdating(): Boolean; -begin - Result := SwitchHasValue('update', 'true') and WizardSilent(); -end; - -procedure CurStepChanged(CurStep: TSetupStep); -begin - if CurStep = ssPostInstall then - begin - if IsUpdating() then - begin - SaveStringToFile(ExpandConstant('{app}\updates\versions.txt'), '{#Version}' + #13#10, True); - end - end; -end; - -function GetAppMutex(Param: string): string; -begin - if IsUpdating() then - Result := '' - else - Result := '{#AppMutex}'; -end; - -function GetInstallDir(Param: string): string; -begin - if IsUpdating() then - Result := ExpandConstant('{app}\install') - else - Result := ExpandConstant('{app}'); -end; diff --git a/crates/zed/resources/windows/zed.sh b/crates/zed/resources/windows/zed.sh deleted file mode 100644 index 734b1a7eb0..0000000000 --- a/crates/zed/resources/windows/zed.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env sh - -if [ "$ZED_WSL_DEBUG_INFO" = true ]; then - set -x -fi - -ZED_PATH="$(dirname "$(realpath "$0")")" - -IN_WSL=false -if [ -n "$WSL_DISTRO_NAME" ]; then - # $WSL_DISTRO_NAME is available since WSL builds 18362, also for WSL2 - IN_WSL=true -fi - -if [ $IN_WSL = true ]; then - WSL_USER="$USER" - if [ -z "$WSL_USER" ]; then - WSL_USER="$USERNAME" - fi - "$ZED_PATH/zed.exe" --wsl "$WSL_USER@$WSL_DISTRO_NAME" "$@" - exit $? -else - "$ZED_PATH/zed.exe" "$@" - exit $? -fi diff --git a/crates/zed/resources/zed.desktop.in b/crates/zed/resources/zed.desktop.in deleted file mode 100644 index eaace153dd..0000000000 --- a/crates/zed/resources/zed.desktop.in +++ /dev/null @@ -1,21 +0,0 @@ -[Desktop Entry] -Version=1.0 -Type=Application -Name=$APP_NAME -GenericName=Text Editor -Comment=A high-performance, multiplayer code editor. -TryExec=$APP_CLI -StartupNotify=$DO_STARTUP_NOTIFY -Exec=$APP_CLI $APP_ARGS -Icon=$APP_ICON -Categories=Utility;TextEditor;Development;IDE; -Keywords=zed; -# To add Zed to "Open Folder With..." context menu, add `inode/directory` to the MimeType field (semicolon separated) -# Arch linux users have reported this setting Zed as default file browser. See https://github.com/zed-industries/zed/pull/39076 and related issues. -# If this happens to you, an unconfirmed fix may be to install Arch's `gnome-defaults-list` package. -MimeType=text/plain;application/x-zerosize;x-scheme-handler/zed; -Actions=NewWorkspace; - -[Desktop Action NewWorkspace] -Exec=$APP_CLI --new $APP_ARGS -Name=Open a new workspace diff --git a/crates/zed/resources/zed.entitlements b/crates/zed/resources/zed.entitlements deleted file mode 100644 index 2a16afe755..0000000000 --- a/crates/zed/resources/zed.entitlements +++ /dev/null @@ -1,30 +0,0 @@ - - - - - com.apple.security.automation.apple-events - - com.apple.security.cs.allow-jit - - com.apple.security.cs.allow-unsigned-executable-memory - - - com.apple.security.device.audio-input - - com.apple.security.device.camera - - com.apple.security.personal-information.addressbook - - com.apple.security.personal-information.calendars - - com.apple.security.personal-information.location - - com.apple.security.personal-information.photos-library - - com.apple.security.files.user-selected.read-write - - com.apple.security.files.downloads.read-write - - - diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs deleted file mode 100644 index dfbc57d293..0000000000 --- a/crates/zed/src/main.rs +++ /dev/null @@ -1,1532 +0,0 @@ -mod reliability; -mod zed; - -use agent_ui::AgentPanel; -use anyhow::{Context as _, Error, Result}; -use clap::Parser; -use cli::FORCE_CLI_MODE_ENV_VAR_NAME; -use client::{Client, ProxySettings, UserStore, parse_zed_link}; -use collab_ui::channel_view::ChannelView; -use collections::HashMap; -use crashes::InitCrashHandler; -use db::kvp::{GLOBAL_KEY_VALUE_STORE, KEY_VALUE_STORE}; -use editor::Editor; -use extension::ExtensionHostProxy; -use fs::{Fs, RealFs}; -use futures::{StreamExt, channel::oneshot, future}; -use git::GitHostingProviderRegistry; -use gpui::{App, AppContext, Application, AsyncApp, Focusable as _, QuitMode, UpdateGlobal as _}; - -use gpui_tokio::Tokio; -use language::LanguageRegistry; -use onboarding::{FIRST_OPEN, show_onboarding_view}; -use prompt_store::PromptBuilder; -use remote::RemoteConnectionOptions; -use reqwest_client::ReqwestClient; - -use assets::Assets; -use node_runtime::{NodeBinaryOptions, NodeRuntime}; -use parking_lot::Mutex; -use project::project_settings::ProjectSettings; -use recent_projects::{SshSettings, open_remote_project}; -use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; -use session::{AppSession, Session}; -use settings::{BaseKeymap, Settings, SettingsStore, watch_config_file}; -use std::{ - env, - io::{self, IsTerminal}, - path::{Path, PathBuf}, - process, - sync::{Arc, OnceLock}, - time::Instant, -}; -use theme::{ActiveTheme, GlobalTheme, ThemeRegistry}; -use util::{ResultExt, TryFutureExt, maybe}; -use uuid::Uuid; -use workspace::{ - AppState, PathList, SerializedWorkspaceLocation, Toast, Workspace, WorkspaceSettings, - WorkspaceStore, notifications::NotificationId, -}; -use zed::{ - OpenListener, OpenRequest, RawOpenRequest, app_menus, build_window_options, - derive_paths_with_position, edit_prediction_registry, handle_cli_connection, - handle_keymap_file_changes, handle_settings_file_changes, initialize_workspace, - open_paths_with_positions, -}; - -use crate::zed::{OpenRequestKind, eager_load_active_theme_and_icon_theme}; - -#[cfg(feature = "mimalloc")] -#[global_allocator] -static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; - -fn files_not_created_on_launch(errors: HashMap>) { - let message = "Zed failed to launch"; - let error_details = errors - .into_iter() - .flat_map(|(kind, paths)| { - #[allow(unused_mut)] // for non-unix platforms - let mut error_kind_details = match paths.len() { - 0 => return None, - 1 => format!( - "{kind} when creating directory {:?}", - paths.first().expect("match arm checks for a single entry") - ), - _many => format!("{kind} when creating directories {paths:?}"), - }; - - #[cfg(unix)] - { - if kind == io::ErrorKind::PermissionDenied { - error_kind_details.push_str("\n\nConsider using chown and chmod tools for altering the directories permissions if your user has corresponding rights.\ - \nFor example, `sudo chown $(whoami):staff ~/.config` and `chmod +uwrx ~/.config`"); - } - } - - Some(error_kind_details) - }) - .collect::>().join("\n\n"); - - eprintln!("{message}: {error_details}"); - Application::new() - .with_quit_mode(QuitMode::Explicit) - .run(move |cx| { - if let Ok(window) = cx.open_window(gpui::WindowOptions::default(), |_, cx| { - cx.new(|_| gpui::Empty) - }) { - window - .update(cx, |_, window, cx| { - let response = window.prompt( - gpui::PromptLevel::Critical, - message, - Some(&error_details), - &["Exit"], - cx, - ); - - cx.spawn_in(window, async move |_, cx| { - response.await?; - cx.update(|_, cx| cx.quit()) - }) - .detach_and_log_err(cx); - }) - .log_err(); - } else { - fail_to_open_window(anyhow::anyhow!("{message}: {error_details}"), cx) - } - }) -} - -fn fail_to_open_window_async(e: anyhow::Error, cx: &mut AsyncApp) { - cx.update(|cx| fail_to_open_window(e, cx)).log_err(); -} - -fn fail_to_open_window(e: anyhow::Error, _cx: &mut App) { - eprintln!( - "Zed failed to open a window: {e:?}. See https://zed.dev/docs/linux for troubleshooting steps." - ); - #[cfg(not(any(target_os = "linux", target_os = "freebsd")))] - { - process::exit(1); - } - - // Maybe unify this with gpui::platform::linux::platform::ResultExt::notify_err(..)? - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - { - use ashpd::desktop::notification::{Notification, NotificationProxy, Priority}; - _cx.spawn(async move |_cx| { - let Ok(proxy) = NotificationProxy::new().await else { - process::exit(1); - }; - - let notification_id = "dev.zed.Oops"; - proxy - .add_notification( - notification_id, - Notification::new("Zed failed to launch") - .body(Some( - format!( - "{e:?}. See https://zed.dev/docs/linux for troubleshooting steps." - ) - .as_str(), - )) - .priority(Priority::High) - .icon(ashpd::desktop::Icon::with_names(&[ - "dialog-question-symbolic", - ])), - ) - .await - .ok(); - - process::exit(1); - }) - .detach(); - } -} -pub static STARTUP_TIME: OnceLock = OnceLock::new(); - -pub fn main() { - STARTUP_TIME.get_or_init(|| Instant::now()); - - #[cfg(unix)] - util::prevent_root_execution(); - - let args = Args::parse(); - - // `zed --askpass` Makes zed operate in nc/netcat mode for use with askpass - #[cfg(not(target_os = "windows"))] - if let Some(socket) = &args.askpass { - askpass::main(socket); - return; - } - - // `zed --crash-handler` Makes zed operate in minidump crash handler mode - if let Some(socket) = &args.crash_handler { - crashes::crash_server(socket.as_path()); - return; - } - - // `zed --nc` Makes zed operate in nc/netcat mode for use with MCP - if let Some(socket) = &args.nc { - match nc::main(socket) { - Ok(()) => return, - Err(err) => { - eprintln!("Error: {}", err); - process::exit(1); - } - } - } - - #[cfg(all(not(debug_assertions), target_os = "windows"))] - unsafe { - use windows::Win32::System::Console::{ATTACH_PARENT_PROCESS, AttachConsole}; - - if args.foreground { - let _ = AttachConsole(ATTACH_PARENT_PROCESS); - } - } - - // `zed --printenv` Outputs environment variables as JSON to stdout - if args.printenv { - util::shell_env::print_env(); - return; - } - - if args.dump_all_actions { - dump_all_gpui_actions(); - return; - } - - // Set custom data directory. - if let Some(dir) = &args.user_data_dir { - paths::set_custom_data_dir(dir); - } - - #[cfg(target_os = "windows")] - match util::get_zed_cli_path() { - Ok(path) => askpass::set_askpass_program(path), - Err(err) => { - eprintln!("Error: {}", err); - if std::option_env!("ZED_BUNDLE").is_some() { - process::exit(1); - } - } - } - - let file_errors = init_paths(); - if !file_errors.is_empty() { - files_not_created_on_launch(file_errors); - return; - } - - zlog::init(); - - if stdout_is_a_pty() { - zlog::init_output_stdout(); - } else { - let result = zlog::init_output_file(paths::log_file(), Some(paths::old_log_file())); - if let Err(err) = result { - eprintln!("Could not open log file: {}... Defaulting to stdout", err); - zlog::init_output_stdout(); - }; - } - ztracing::init(); - - let version = option_env!("ZED_BUILD_ID"); - let app_commit_sha = - option_env!("ZED_COMMIT_SHA").map(|commit_sha| AppCommitSha::new(commit_sha.to_string())); - let app_version = AppVersion::load(env!("CARGO_PKG_VERSION"), version, app_commit_sha.clone()); - - if args.system_specs { - let system_specs = system_specs::SystemSpecs::new_stateless( - app_version, - app_commit_sha, - *release_channel::RELEASE_CHANNEL, - ); - println!("Zed System Specs (from CLI):\n{}", system_specs); - return; - } - - rayon::ThreadPoolBuilder::new() - .num_threads(std::thread::available_parallelism().map_or(1, |n| n.get().div_ceil(2))) - .stack_size(10 * 1024 * 1024) - .thread_name(|ix| format!("RayonWorker{}", ix)) - .build_global() - .unwrap(); - - log::info!( - "========== starting zed version {}, sha {} ==========", - app_version, - app_commit_sha - .as_ref() - .map(|sha| sha.short()) - .as_deref() - .unwrap_or("unknown"), - ); - - #[cfg(windows)] - check_for_conpty_dll(); - - let app = Application::new().with_assets(Assets); - - let system_id = app.background_executor().spawn(system_id()); - let installation_id = app.background_executor().spawn(installation_id()); - let session_id = Uuid::new_v4().to_string(); - let session = app - .background_executor() - .spawn(Session::new(session_id.clone())); - - app.background_executor() - .spawn(crashes::init(InitCrashHandler { - session_id, - zed_version: app_version.to_string(), - binary: "zed".to_string(), - release_channel: release_channel::RELEASE_CHANNEL_NAME.clone(), - commit_sha: app_commit_sha - .as_ref() - .map(|sha| sha.full()) - .unwrap_or_else(|| "no sha".to_owned()), - })) - .detach(); - - let (open_listener, mut open_rx) = OpenListener::new(); - - let failed_single_instance_check = if *zed_env_vars::ZED_STATELESS - || *release_channel::RELEASE_CHANNEL == ReleaseChannel::Dev - { - false - } else { - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - { - crate::zed::listen_for_cli_connections(open_listener.clone()).is_err() - } - - #[cfg(target_os = "windows")] - { - !crate::zed::windows_only_instance::handle_single_instance(open_listener.clone(), &args) - } - - #[cfg(target_os = "macos")] - { - use zed::mac_only_instance::*; - ensure_only_instance() != IsOnlyInstance::Yes - } - }; - if failed_single_instance_check { - println!("zed is already running"); - return; - } - - let git_hosting_provider_registry = Arc::new(GitHostingProviderRegistry::new()); - let git_binary_path = - if cfg!(target_os = "macos") && option_env!("ZED_BUNDLE").as_deref() == Some("true") { - app.path_for_auxiliary_executable("git") - .context("could not find git binary path") - .log_err() - } else { - None - }; - if let Some(git_binary_path) = &git_binary_path { - log::info!("Using git binary path: {:?}", git_binary_path); - } - - let fs = Arc::new(RealFs::new(git_binary_path, app.background_executor())); - let user_settings_file_rx = watch_config_file( - &app.background_executor(), - fs.clone(), - paths::settings_file().clone(), - ); - let global_settings_file_rx = watch_config_file( - &app.background_executor(), - fs.clone(), - paths::global_settings_file().clone(), - ); - let user_keymap_file_rx = watch_config_file( - &app.background_executor(), - fs.clone(), - paths::keymap_file().clone(), - ); - - let (shell_env_loaded_tx, shell_env_loaded_rx) = oneshot::channel(); - if !stdout_is_a_pty() { - app.background_executor() - .spawn(async { - #[cfg(unix)] - util::load_login_shell_environment().await.log_err(); - shell_env_loaded_tx.send(()).ok(); - }) - .detach() - } else { - drop(shell_env_loaded_tx) - } - - app.on_open_urls({ - let open_listener = open_listener.clone(); - move |urls| { - open_listener.open(RawOpenRequest { - urls, - diff_paths: Vec::new(), - ..Default::default() - }) - } - }); - app.on_reopen(move |cx| { - if let Some(app_state) = AppState::try_global(cx).and_then(|app_state| app_state.upgrade()) - { - cx.spawn({ - let app_state = app_state; - async move |cx| { - if let Err(e) = restore_or_create_workspace(app_state, cx).await { - fail_to_open_window_async(e, cx) - } - } - }) - .detach(); - } - }); - - app.run(move |cx| { - menu::init(); - zed_actions::init(); - - release_channel::init(app_version, cx); - gpui_tokio::init(cx); - if let Some(app_commit_sha) = app_commit_sha { - AppCommitSha::set_global(app_commit_sha, cx); - } - settings::init(cx); - zlog_settings::init(cx); - handle_settings_file_changes(user_settings_file_rx, global_settings_file_rx, cx); - handle_keymap_file_changes(user_keymap_file_rx, cx); - - let user_agent = format!( - "Zed/{} ({}; {})", - AppVersion::global(cx), - std::env::consts::OS, - std::env::consts::ARCH - ); - let proxy_url = ProxySettings::get_global(cx).proxy_url(); - let http = { - let _guard = Tokio::handle(cx).enter(); - - ReqwestClient::proxy_and_user_agent(proxy_url, &user_agent) - .expect("could not start HTTP client") - }; - cx.set_http_client(Arc::new(http)); - - ::set_global(fs.clone(), cx); - - GitHostingProviderRegistry::set_global(git_hosting_provider_registry, cx); - git_hosting_providers::init(cx); - - OpenListener::set_global(cx, open_listener.clone()); - - extension::init(cx); - let extension_host_proxy = ExtensionHostProxy::global(cx); - - let client = Client::production(cx); - cx.set_http_client(client.http_client()); - let mut languages = LanguageRegistry::new(cx.background_executor().clone()); - languages.set_language_server_download_dir(paths::languages_dir().clone()); - let languages = Arc::new(languages); - let (mut tx, rx) = watch::channel(None); - cx.observe_global::(move |cx| { - let settings = &ProjectSettings::get_global(cx).node; - let options = NodeBinaryOptions { - allow_path_lookup: !settings.ignore_system_version, - // TODO: Expose this setting - allow_binary_download: true, - use_paths: settings.path.as_ref().map(|node_path| { - let node_path = PathBuf::from(shellexpand::tilde(node_path).as_ref()); - let npm_path = settings - .npm_path - .as_ref() - .map(|path| PathBuf::from(shellexpand::tilde(&path).as_ref())); - ( - node_path.clone(), - npm_path.unwrap_or_else(|| { - let base_path = PathBuf::new(); - node_path.parent().unwrap_or(&base_path).join("npm") - }), - ) - }), - }; - tx.send(Some(options)).log_err(); - }) - .detach(); - let node_runtime = NodeRuntime::new(client.http_client(), Some(shell_env_loaded_rx), rx); - - debug_adapter_extension::init(extension_host_proxy.clone(), cx); - languages::init(languages.clone(), fs.clone(), node_runtime.clone(), cx); - let user_store = cx.new(|cx| UserStore::new(client.clone(), cx)); - let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx)); - - language_extension::init( - language_extension::LspAccess::ViaWorkspaces({ - let workspace_store = workspace_store.clone(); - Arc::new(move |cx: &mut App| { - workspace_store.update(cx, |workspace_store, cx| { - workspace_store - .workspaces() - .iter() - .map(|workspace| { - workspace.update(cx, |workspace, _, cx| { - workspace.project().read(cx).lsp_store() - }) - }) - .collect() - }) - }) - }), - extension_host_proxy.clone(), - languages.clone(), - ); - - Client::set_global(client.clone(), cx); - - zed::init(cx); - project::Project::init(&client, cx); - debugger_ui::init(cx); - debugger_tools::init(cx); - client::init(&client, cx); - - let system_id = cx.background_executor().block(system_id).ok(); - let installation_id = cx.background_executor().block(installation_id).ok(); - let session = cx.background_executor().block(session); - - let telemetry = client.telemetry(); - telemetry.start( - system_id.as_ref().map(|id| id.to_string()), - installation_id.as_ref().map(|id| id.to_string()), - session.id().to_owned(), - cx, - ); - - // We should rename these in the future to `first app open`, `first app open for release channel`, and `app open` - if let (Some(system_id), Some(installation_id)) = (&system_id, &installation_id) { - match (&system_id, &installation_id) { - (IdType::New(_), IdType::New(_)) => { - telemetry::event!("App First Opened"); - telemetry::event!("App First Opened For Release Channel"); - } - (IdType::Existing(_), IdType::New(_)) => { - telemetry::event!("App First Opened For Release Channel"); - } - (_, IdType::Existing(_)) => { - telemetry::event!("App Opened"); - } - } - } - let app_session = cx.new(|cx| AppSession::new(session, cx)); - - let app_state = Arc::new(AppState { - languages, - client: client.clone(), - user_store, - fs: fs.clone(), - build_window_options, - workspace_store, - node_runtime, - session: app_session, - }); - AppState::set_global(Arc::downgrade(&app_state), cx); - - auto_update::init(client.clone(), cx); - dap_adapters::init(cx); - auto_update_ui::init(cx); - reliability::init(client.clone(), cx); - extension_host::init( - extension_host_proxy.clone(), - app_state.fs.clone(), - app_state.client.clone(), - app_state.node_runtime.clone(), - cx, - ); - - theme::init(theme::LoadThemes::All(Box::new(Assets)), cx); - eager_load_active_theme_and_icon_theme(fs.clone(), cx); - theme_extension::init( - extension_host_proxy, - ThemeRegistry::global(cx), - cx.background_executor().clone(), - ); - command_palette::init(cx); - let copilot_language_server_id = app_state.languages.next_language_server_id(); - copilot::init( - copilot_language_server_id, - app_state.fs.clone(), - app_state.client.http_client(), - app_state.node_runtime.clone(), - cx, - ); - supermaven::init(app_state.client.clone(), cx); - language_model::init(app_state.client.clone(), cx); - language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx); - acp_tools::init(cx); - edit_prediction_ui::init(cx); - web_search::init(cx); - web_search_providers::init(app_state.client.clone(), cx); - snippet_provider::init(cx); - edit_prediction_registry::init(app_state.client.clone(), app_state.user_store.clone(), cx); - let prompt_builder = PromptBuilder::load(app_state.fs.clone(), stdout_is_a_pty(), cx); - agent_ui::init( - app_state.fs.clone(), - app_state.client.clone(), - prompt_builder.clone(), - app_state.languages.clone(), - false, - cx, - ); - repl::init(app_state.fs.clone(), cx); - recent_projects::init(cx); - - load_embedded_fonts(cx); - - editor::init(cx); - image_viewer::init(cx); - repl::notebook::init(cx); - diagnostics::init(cx); - - audio::init(cx); - workspace::init(app_state.clone(), cx); - ui_prompt::init(cx); - - go_to_line::init(cx); - file_finder::init(cx); - tab_switcher::init(cx); - outline::init(cx); - project_symbols::init(cx); - project_panel::init(cx); - outline_panel::init(cx); - tasks_ui::init(cx); - snippets_ui::init(cx); - channel::init(&app_state.client.clone(), app_state.user_store.clone(), cx); - search::init(cx); - vim::init(cx); - terminal_view::init(cx); - journal::init(app_state.clone(), cx); - language_selector::init(cx); - line_ending_selector::init(cx); - toolchain_selector::init(cx); - theme_selector::init(cx); - settings_profile_selector::init(cx); - language_tools::init(cx); - call::init(app_state.client.clone(), app_state.user_store.clone(), cx); - notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx); - collab_ui::init(&app_state, cx); - git_ui::init(cx); - feedback::init(cx); - markdown_preview::init(cx); - svg_preview::init(cx); - onboarding::init(cx); - settings_ui::init(cx); - keymap_editor::init(cx); - extensions_ui::init(cx); - edit_prediction::init(cx); - inspector_ui::init(app_state.clone(), cx); - json_schema_store::init(cx); - miniprofiler_ui::init(*STARTUP_TIME.get().unwrap(), cx); - - cx.observe_global::({ - let http = app_state.client.http_client(); - let client = app_state.client.clone(); - move |cx| { - for &mut window in cx.windows().iter_mut() { - let background_appearance = cx.theme().window_background_appearance(); - window - .update(cx, |_, window, _| { - window.set_background_appearance(background_appearance) - }) - .ok(); - } - - let new_host = &client::ClientSettings::get_global(cx).server_url; - if &http.base_url() != new_host { - http.set_base_url(new_host); - if client.status().borrow().is_connected() { - client.reconnect(&cx.to_async()); - } - } - } - }) - .detach(); - app_state.languages.set_theme(cx.theme().clone()); - cx.observe_global::({ - let languages = app_state.languages.clone(); - move |cx| { - languages.set_theme(cx.theme().clone()); - } - }) - .detach(); - telemetry::event!( - "Settings Changed", - setting = "theme", - value = cx.theme().name.to_string() - ); - telemetry::event!( - "Settings Changed", - setting = "keymap", - value = BaseKeymap::get_global(cx).to_string() - ); - telemetry.flush_events().detach(); - - let fs = app_state.fs.clone(); - load_user_themes_in_background(fs.clone(), cx); - watch_themes(fs.clone(), cx); - watch_languages(fs.clone(), app_state.languages.clone(), cx); - - let menus = app_menus(cx); - cx.set_menus(menus); - initialize_workspace(app_state.clone(), prompt_builder, cx); - - cx.activate(true); - - cx.spawn({ - let client = app_state.client.clone(); - async move |cx| authenticate(client, cx).await - }) - .detach_and_log_err(cx); - - let urls: Vec<_> = args - .paths_or_urls - .iter() - .map(|arg| parse_url_arg(arg, cx)) - .collect(); - - let diff_paths: Vec<[String; 2]> = args - .diff - .chunks(2) - .map(|chunk| [chunk[0].clone(), chunk[1].clone()]) - .collect(); - - #[cfg(target_os = "windows")] - let wsl = args.wsl; - #[cfg(not(target_os = "windows"))] - let wsl = None; - - if !urls.is_empty() || !diff_paths.is_empty() { - open_listener.open(RawOpenRequest { - urls, - diff_paths, - wsl, - }) - } - - match open_rx - .try_next() - .ok() - .flatten() - .and_then(|request| OpenRequest::parse(request, cx).log_err()) - { - Some(request) => { - handle_open_request(request, app_state.clone(), cx); - } - None => { - cx.spawn({ - let app_state = app_state.clone(); - async move |cx| { - if let Err(e) = restore_or_create_workspace(app_state, cx).await { - fail_to_open_window_async(e, cx) - } - } - }) - .detach(); - } - } - - let app_state = app_state.clone(); - - crate::zed::component_preview::init(app_state.clone(), cx); - - cx.spawn(async move |cx| { - while let Some(urls) = open_rx.next().await { - cx.update(|cx| { - if let Some(request) = OpenRequest::parse(urls, cx).log_err() { - handle_open_request(request, app_state.clone(), cx); - } - }) - .ok(); - } - }) - .detach(); - }); -} - -fn handle_open_request(request: OpenRequest, app_state: Arc, cx: &mut App) { - if let Some(kind) = request.kind { - match kind { - OpenRequestKind::CliConnection(connection) => { - cx.spawn(async move |cx| handle_cli_connection(connection, app_state, cx).await) - .detach(); - } - OpenRequestKind::Extension { extension_id } => { - cx.spawn(async move |cx| { - let workspace = - workspace::get_any_active_workspace(app_state, cx.clone()).await?; - workspace.update(cx, |_, window, cx| { - window.dispatch_action( - Box::new(zed_actions::Extensions { - category_filter: None, - id: Some(extension_id), - }), - cx, - ); - }) - }) - .detach_and_log_err(cx); - } - OpenRequestKind::AgentPanel => { - cx.spawn(async move |cx| { - let workspace = - workspace::get_any_active_workspace(app_state, cx.clone()).await?; - workspace.update(cx, |workspace, window, cx| { - if let Some(panel) = workspace.panel::(cx) { - panel.focus_handle(cx).focus(window); - } - }) - }) - .detach_and_log_err(cx); - } - OpenRequestKind::DockMenuAction { index } => { - cx.perform_dock_menu_action(index); - } - OpenRequestKind::BuiltinJsonSchema { schema_path } => { - workspace::with_active_or_new_workspace(cx, |_workspace, window, cx| { - cx.spawn_in(window, async move |workspace, cx| { - let res = async move { - let json = app_state.languages.language_for_name("JSONC").await.ok(); - let json_schema_content = - json_schema_store::resolve_schema_request_inner( - &app_state.languages, - &schema_path, - cx, - )?; - let json_schema_content = - serde_json::to_string_pretty(&json_schema_content) - .context("Failed to serialize JSON Schema as JSON")?; - let buffer_task = workspace.update(cx, |workspace, cx| { - workspace - .project() - .update(cx, |project, cx| project.create_buffer(false, cx)) - })?; - - let buffer = buffer_task.await?; - - workspace.update_in(cx, |workspace, window, cx| { - buffer.update(cx, |buffer, cx| { - buffer.set_language(json, cx); - buffer.edit([(0..0, json_schema_content)], None, cx); - buffer.edit( - [(0..0, format!("// {} JSON Schema\n", schema_path))], - None, - cx, - ); - }); - - workspace.add_item_to_active_pane( - Box::new(cx.new(|cx| { - let mut editor = - editor::Editor::for_buffer(buffer, None, window, cx); - editor.set_read_only(true); - editor - })), - None, - true, - window, - cx, - ); - }) - } - .await; - res.context("Failed to open builtin JSON Schema").log_err(); - }) - .detach(); - }); - } - OpenRequestKind::Setting { setting_path } => { - // zed://settings/languages/$(language)/tab_size - DONT SUPPORT - // zed://settings/languages/Rust/tab_size - SUPPORT - // languages.$(language).tab_size - // [ languages $(language) tab_size] - cx.spawn(async move |cx| { - let workspace = - workspace::get_any_active_workspace(app_state, cx.clone()).await?; - - workspace.update(cx, |_, window, cx| match setting_path { - None => window.dispatch_action(Box::new(zed_actions::OpenSettings), cx), - Some(setting_path) => window.dispatch_action( - Box::new(zed_actions::OpenSettingsAt { path: setting_path }), - cx, - ), - }) - }) - .detach_and_log_err(cx); - } - } - - return; - } - - if let Some(connection_options) = request.remote_connection { - cx.spawn(async move |cx| { - let paths: Vec = request.open_paths.into_iter().map(PathBuf::from).collect(); - open_remote_project( - connection_options, - paths, - app_state, - workspace::OpenOptions::default(), - cx, - ) - .await - }) - .detach_and_log_err(cx); - return; - } - - let mut task = None; - if !request.open_paths.is_empty() || !request.diff_paths.is_empty() { - let app_state = app_state.clone(); - task = Some(cx.spawn(async move |cx| { - let paths_with_position = - derive_paths_with_position(app_state.fs.as_ref(), request.open_paths).await; - let (_window, results) = open_paths_with_positions( - &paths_with_position, - &request.diff_paths, - app_state, - workspace::OpenOptions::default(), - cx, - ) - .await?; - for result in results.into_iter().flatten() { - if let Err(err) = result { - log::error!("Error opening path: {err}",); - } - } - anyhow::Ok(()) - })); - } - - if !request.open_channel_notes.is_empty() || request.join_channel.is_some() { - cx.spawn(async move |cx| { - let result = maybe!(async { - if let Some(task) = task { - task.await?; - } - let client = app_state.client.clone(); - // we continue even if authentication fails as join_channel/ open channel notes will - // show a visible error message. - authenticate(client, cx).await.log_err(); - - if let Some(channel_id) = request.join_channel { - cx.update(|cx| { - workspace::join_channel( - client::ChannelId(channel_id), - app_state.clone(), - None, - cx, - ) - })? - .await?; - } - - let workspace_window = - workspace::get_any_active_workspace(app_state, cx.clone()).await?; - let workspace = workspace_window.entity(cx)?; - - let mut promises = Vec::new(); - for (channel_id, heading) in request.open_channel_notes { - promises.push(cx.update_window(workspace_window.into(), |_, window, cx| { - ChannelView::open( - client::ChannelId(channel_id), - heading, - workspace.clone(), - window, - cx, - ) - .log_err() - })?) - } - future::join_all(promises).await; - anyhow::Ok(()) - }) - .await; - if let Err(err) = result { - fail_to_open_window_async(err, cx); - } - }) - .detach() - } else if let Some(task) = task { - cx.spawn(async move |cx| { - if let Err(err) = task.await { - fail_to_open_window_async(err, cx); - } - }) - .detach(); - } -} - -async fn authenticate(client: Arc, cx: &AsyncApp) -> Result<()> { - if stdout_is_a_pty() { - if client::IMPERSONATE_LOGIN.is_some() { - client.sign_in_with_optional_connect(false, cx).await?; - } else if client.has_credentials(cx).await { - client.sign_in_with_optional_connect(true, cx).await?; - } - } else if client.has_credentials(cx).await { - client.sign_in_with_optional_connect(true, cx).await?; - } - - Ok(()) -} - -async fn system_id() -> Result { - let key_name = "system_id".to_string(); - - if let Ok(Some(system_id)) = GLOBAL_KEY_VALUE_STORE.read_kvp(&key_name) { - return Ok(IdType::Existing(system_id)); - } - - let system_id = Uuid::new_v4().to_string(); - - GLOBAL_KEY_VALUE_STORE - .write_kvp(key_name, system_id.clone()) - .await?; - - Ok(IdType::New(system_id)) -} - -async fn installation_id() -> Result { - let legacy_key_name = "device_id".to_string(); - let key_name = "installation_id".to_string(); - - // Migrate legacy key to new key - if let Ok(Some(installation_id)) = KEY_VALUE_STORE.read_kvp(&legacy_key_name) { - KEY_VALUE_STORE - .write_kvp(key_name, installation_id.clone()) - .await?; - KEY_VALUE_STORE.delete_kvp(legacy_key_name).await?; - return Ok(IdType::Existing(installation_id)); - } - - if let Ok(Some(installation_id)) = KEY_VALUE_STORE.read_kvp(&key_name) { - return Ok(IdType::Existing(installation_id)); - } - - let installation_id = Uuid::new_v4().to_string(); - - KEY_VALUE_STORE - .write_kvp(key_name, installation_id.clone()) - .await?; - - Ok(IdType::New(installation_id)) -} - -async fn restore_or_create_workspace(app_state: Arc, cx: &mut AsyncApp) -> Result<()> { - if let Some(locations) = restorable_workspace_locations(cx, &app_state).await { - let use_system_window_tabs = cx - .update(|cx| WorkspaceSettings::get_global(cx).use_system_window_tabs) - .unwrap_or(false); - let mut results: Vec> = Vec::new(); - let mut tasks = Vec::new(); - - for (index, (location, paths)) in locations.into_iter().enumerate() { - match location { - SerializedWorkspaceLocation::Local => { - let app_state = app_state.clone(); - let task = cx.spawn(async move |cx| { - let open_task = cx.update(|cx| { - workspace::open_paths( - &paths.paths(), - app_state, - workspace::OpenOptions::default(), - cx, - ) - })?; - open_task.await.map(|_| ()) - }); - - // If we're using system window tabs and this is the first workspace, - // wait for it to finish so that the other windows can be added as tabs. - if use_system_window_tabs && index == 0 { - results.push(task.await); - } else { - tasks.push(task); - } - } - SerializedWorkspaceLocation::Remote(mut connection_options) => { - let app_state = app_state.clone(); - if let RemoteConnectionOptions::Ssh(options) = &mut connection_options { - cx.update(|cx| { - SshSettings::get_global(cx) - .fill_connection_options_from_settings(options) - })?; - } - let task = cx.spawn(async move |cx| { - recent_projects::open_remote_project( - connection_options, - paths.paths().into_iter().map(PathBuf::from).collect(), - app_state, - workspace::OpenOptions::default(), - cx, - ) - .await - .map_err(|e| anyhow::anyhow!(e)) - }); - tasks.push(task); - } - } - } - - // Wait for all workspaces to open concurrently - results.extend(future::join_all(tasks).await); - - // Show notifications for any errors that occurred - let mut error_count = 0; - for result in results { - if let Err(e) = result { - log::error!("Failed to restore workspace: {}", e); - error_count += 1; - } - } - - if error_count > 0 { - let message = if error_count == 1 { - "Failed to restore 1 workspace. Check logs for details.".to_string() - } else { - format!( - "Failed to restore {} workspaces. Check logs for details.", - error_count - ) - }; - - // Try to find an active workspace to show the toast - let toast_shown = cx - .update(|cx| { - if let Some(window) = cx.active_window() - && let Some(workspace) = window.downcast::() - { - workspace - .update(cx, |workspace, _, cx| { - workspace.show_toast( - Toast::new(NotificationId::unique::<()>(), message), - cx, - ) - }) - .ok(); - return true; - } - false - }) - .unwrap_or(false); - - // If we couldn't show a toast (no windows opened successfully), - // we've already logged the errors above, so the user can check logs - if !toast_shown { - log::error!( - "Failed to show notification for window restoration errors, because no workspace windows were available." - ); - } - } - } else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) { - cx.update(|cx| show_onboarding_view(app_state, cx))?.await?; - } else { - cx.update(|cx| { - workspace::open_new( - Default::default(), - app_state, - cx, - |workspace, window, cx| { - Editor::new_file(workspace, &Default::default(), window, cx) - }, - ) - })? - .await?; - } - - Ok(()) -} - -pub(crate) async fn restorable_workspace_locations( - cx: &mut AsyncApp, - app_state: &Arc, -) -> Option> { - let mut restore_behavior = cx - .update(|cx| WorkspaceSettings::get(None, cx).restore_on_startup) - .ok()?; - - let session_handle = app_state.session.clone(); - let (last_session_id, last_session_window_stack) = cx - .update(|cx| { - let session = session_handle.read(cx); - - ( - session.last_session_id().map(|id| id.to_string()), - session.last_session_window_stack(), - ) - }) - .ok()?; - - if last_session_id.is_none() - && matches!( - restore_behavior, - workspace::RestoreOnStartupBehavior::LastSession - ) - { - restore_behavior = workspace::RestoreOnStartupBehavior::LastWorkspace; - } - - match restore_behavior { - workspace::RestoreOnStartupBehavior::LastWorkspace => { - workspace::last_opened_workspace_location() - .await - .map(|location| vec![location]) - } - workspace::RestoreOnStartupBehavior::LastSession => { - if let Some(last_session_id) = last_session_id { - let ordered = last_session_window_stack.is_some(); - - let mut locations = workspace::last_session_workspace_locations( - &last_session_id, - last_session_window_stack, - ) - .filter(|locations| !locations.is_empty()); - - // Since last_session_window_order returns the windows ordered front-to-back - // we need to open the window that was frontmost last. - if ordered && let Some(locations) = locations.as_mut() { - locations.reverse(); - } - - locations - } else { - None - } - } - _ => None, - } -} - -fn init_paths() -> HashMap> { - [ - paths::config_dir(), - paths::extensions_dir(), - paths::languages_dir(), - paths::debug_adapters_dir(), - paths::database_dir(), - paths::logs_dir(), - paths::temp_dir(), - paths::hang_traces_dir(), - ] - .into_iter() - .fold(HashMap::default(), |mut errors, path| { - if let Err(e) = std::fs::create_dir_all(path) { - errors.entry(e.kind()).or_insert_with(Vec::new).push(path); - } - errors - }) -} - -pub fn stdout_is_a_pty() -> bool { - std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_none() && io::stdout().is_terminal() -} - -#[derive(Parser, Debug)] -#[command(name = "zed", disable_version_flag = true, max_term_width = 100)] -struct Args { - /// A sequence of space-separated paths or urls that you want to open. - /// - /// Use `path:line:row` syntax to open a file at a specific location. - /// Non-existing paths and directories will ignore `:line:row` suffix. - /// - /// URLs can either be `file://` or `zed://` scheme, or relative to . - paths_or_urls: Vec, - - /// Pairs of file paths to diff. Can be specified multiple times. - #[arg(long, action = clap::ArgAction::Append, num_args = 2, value_names = ["OLD_PATH", "NEW_PATH"])] - diff: Vec, - - /// Sets a custom directory for all user data (e.g., database, extensions, logs). - /// - /// This overrides the default platform-specific data directory location. - /// On macOS, the default is `~/Library/Application Support/Zed`. - /// On Linux/FreeBSD, the default is `$XDG_DATA_HOME/zed`. - /// On Windows, the default is `%LOCALAPPDATA%\Zed`. - #[arg(long, value_name = "DIR", verbatim_doc_comment)] - user_data_dir: Option, - - /// The username and WSL distribution to use when opening paths. If not specified, - /// Zed will attempt to open the paths directly. - /// - /// The username is optional, and if not specified, the default user for the distribution - /// will be used. - /// - /// Example: `me@Ubuntu` or `Ubuntu`. - /// - /// WARN: You should not fill in this field by hand. - #[cfg(target_os = "windows")] - #[arg(long, value_name = "USER@DISTRO")] - wsl: Option, - - /// Instructs zed to run as a dev server on this machine. (not implemented) - #[arg(long)] - dev_server_token: Option, - - /// Prints system specs. - /// - /// Useful for submitting issues on GitHub when encountering a bug that - /// prevents Zed from starting, so you can't run `zed: copy system specs to - /// clipboard` - #[arg(long)] - system_specs: bool, - - /// Used for the MCP Server, to remove the need for netcat as a dependency, - /// by having Zed act like netcat communicating over a Unix socket. - #[arg(long, hide = true)] - nc: Option, - - /// Used for recording minidumps on crashes by having Zed run a separate - /// process communicating over a socket. - #[arg(long, hide = true)] - crash_handler: Option, - - /// Run zed in the foreground, only used on Windows, to match the behavior on macOS. - #[arg(long)] - #[cfg(target_os = "windows")] - #[arg(hide = true)] - foreground: bool, - - /// The dock action to perform. This is used on Windows only. - #[arg(long)] - #[cfg(target_os = "windows")] - #[arg(hide = true)] - dock_action: Option, - - /// Used for SSH/Git password authentication, to remove the need for netcat as a dependency, - /// by having Zed act like netcat communicating over a Unix socket. - #[arg(long)] - #[cfg(not(target_os = "windows"))] - #[arg(hide = true)] - askpass: Option, - - #[arg(long, hide = true)] - dump_all_actions: bool, - - /// Output current environment variables as JSON to stdout - #[arg(long, hide = true)] - printenv: bool, -} - -#[derive(Clone, Debug)] -enum IdType { - New(String), - Existing(String), -} - -impl ToString for IdType { - fn to_string(&self) -> String { - match self { - IdType::New(id) | IdType::Existing(id) => id.clone(), - } - } -} - -fn parse_url_arg(arg: &str, cx: &App) -> String { - match std::fs::canonicalize(Path::new(&arg)) { - Ok(path) => format!("file://{}", path.display()), - Err(_) => { - if arg.starts_with("file://") - || arg.starts_with("zed-cli://") - || arg.starts_with("ssh://") - || parse_zed_link(arg, cx).is_some() - { - arg.into() - } else { - format!("file://{arg}") - } - } - } -} - -fn load_embedded_fonts(cx: &App) { - let asset_source = cx.asset_source(); - let font_paths = asset_source.list("fonts").unwrap(); - let embedded_fonts = Mutex::new(Vec::new()); - let executor = cx.background_executor(); - - executor.block(executor.scoped(|scope| { - for font_path in &font_paths { - if !font_path.ends_with(".ttf") { - continue; - } - - scope.spawn(async { - let font_bytes = asset_source.load(font_path).unwrap().unwrap(); - embedded_fonts.lock().push(font_bytes); - }); - } - })); - - cx.text_system() - .add_fonts(embedded_fonts.into_inner()) - .unwrap(); -} - -/// Spawns a background task to load the user themes from the themes directory. -fn load_user_themes_in_background(fs: Arc, cx: &mut App) { - cx.spawn({ - let fs = fs.clone(); - async move |cx| { - if let Some(theme_registry) = cx.update(|cx| ThemeRegistry::global(cx)).log_err() { - let themes_dir = paths::themes_dir().as_ref(); - match fs - .metadata(themes_dir) - .await - .ok() - .flatten() - .map(|m| m.is_dir) - { - Some(is_dir) => { - anyhow::ensure!(is_dir, "Themes dir path {themes_dir:?} is not a directory") - } - None => { - fs.create_dir(themes_dir).await.with_context(|| { - format!("Failed to create themes dir at path {themes_dir:?}") - })?; - } - } - theme_registry.load_user_themes(themes_dir, fs).await?; - cx.update(GlobalTheme::reload_theme)?; - } - anyhow::Ok(()) - } - }) - .detach_and_log_err(cx); -} - -/// Spawns a background task to watch the themes directory for changes. -fn watch_themes(fs: Arc, cx: &mut App) { - use std::time::Duration; - cx.spawn(async move |cx| { - let (mut events, _) = fs - .watch(paths::themes_dir(), Duration::from_millis(100)) - .await; - - while let Some(paths) = events.next().await { - for event in paths { - if fs.metadata(&event.path).await.ok().flatten().is_some() - && let Some(theme_registry) = - cx.update(|cx| ThemeRegistry::global(cx)).log_err() - && let Some(()) = theme_registry - .load_user_theme(&event.path, fs.clone()) - .await - .log_err() - { - cx.update(GlobalTheme::reload_theme).log_err(); - } - } - } - }) - .detach() -} - -#[cfg(debug_assertions)] -fn watch_languages(fs: Arc, languages: Arc, cx: &mut App) { - use std::time::Duration; - - cx.background_spawn(async move { - let languages_src = Path::new("crates/languages/src"); - let Some(languages_src) = fs.canonicalize(languages_src).await.log_err() else { - return; - }; - - let (mut events, watcher) = fs.watch(&languages_src, Duration::from_millis(100)).await; - - // add subdirectories since fs.watch is not recursive on Linux - if let Some(mut paths) = fs.read_dir(&languages_src).await.log_err() { - while let Some(path) = paths.next().await { - if let Some(path) = path.log_err() - && fs.is_dir(&path).await - { - watcher.add(&path).log_err(); - } - } - } - - while let Some(event) = events.next().await { - let has_language_file = event - .iter() - .any(|event| event.path.extension().is_some_and(|ext| ext == "scm")); - if has_language_file { - languages.reload(); - } - } - }) - .detach(); -} - -#[cfg(not(debug_assertions))] -fn watch_languages(_fs: Arc, _languages: Arc, _cx: &mut App) {} - -fn dump_all_gpui_actions() { - #[derive(Debug, serde::Serialize)] - struct ActionDef { - name: &'static str, - human_name: String, - aliases: &'static [&'static str], - documentation: Option<&'static str>, - } - let mut actions = gpui::generate_list_of_all_registered_actions() - .map(|action| ActionDef { - name: action.name, - human_name: command_palette::humanize_action_name(action.name), - aliases: action.deprecated_aliases, - documentation: action.documentation, - }) - .collect::>(); - - actions.sort_by_key(|a| a.name); - - io::Write::write( - &mut std::io::stdout(), - serde_json::to_string_pretty(&actions).unwrap().as_bytes(), - ) - .unwrap(); -} - -#[cfg(target_os = "windows")] -fn check_for_conpty_dll() { - use windows::{ - Win32::{Foundation::FreeLibrary, System::LibraryLoader::LoadLibraryW}, - core::w, - }; - - if let Ok(hmodule) = unsafe { LoadLibraryW(w!("conpty.dll")) } { - unsafe { - FreeLibrary(hmodule) - .context("Failed to free conpty.dll") - .log_err(); - } - } else { - log::warn!("Failed to load conpty.dll. Terminal will work with reduced functionality."); - } -} diff --git a/crates/zed/src/reliability.rs b/crates/zed/src/reliability.rs deleted file mode 100644 index da8dffa85d..0000000000 --- a/crates/zed/src/reliability.rs +++ /dev/null @@ -1,337 +0,0 @@ -use anyhow::{Context as _, Result}; -use client::{Client, telemetry::MINIDUMP_ENDPOINT}; -use futures::{AsyncReadExt, TryStreamExt}; -use gpui::{App, AppContext as _, SerializedThreadTaskTimings}; -use http_client::{self, AsyncBody, HttpClient, Request}; -use log::info; -use project::Project; -use proto::{CrashReport, GetCrashFilesResponse}; -use reqwest::multipart::{Form, Part}; -use smol::stream::StreamExt; -use std::{ffi::OsStr, fs, sync::Arc, thread::ThreadId, time::Duration}; -use util::ResultExt; - -use crate::STARTUP_TIME; - -pub fn init(client: Arc, cx: &mut App) { - monitor_hangs(cx); - - if client.telemetry().diagnostics_enabled() { - let client = client.clone(); - cx.background_spawn(async move { - upload_previous_minidumps(client).await.warn_on_err(); - }) - .detach() - } - - cx.observe_new(move |project: &mut Project, _, cx| { - let client = client.clone(); - - let Some(remote_client) = project.remote_client() else { - return; - }; - remote_client.update(cx, |remote_client, cx| { - if !client.telemetry().diagnostics_enabled() { - return; - } - let request = remote_client - .proto_client() - .request(proto::GetCrashFiles {}); - cx.background_spawn(async move { - let GetCrashFilesResponse { crashes } = request.await?; - - let Some(endpoint) = MINIDUMP_ENDPOINT.as_ref() else { - return Ok(()); - }; - for CrashReport { - metadata, - minidump_contents, - } in crashes - { - if let Some(metadata) = serde_json::from_str(&metadata).log_err() { - upload_minidump(client.clone(), endpoint, minidump_contents, &metadata) - .await - .log_err(); - } - } - - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - }) - }) - .detach(); -} - -fn monitor_hangs(cx: &App) { - let main_thread_id = std::thread::current().id(); - - let foreground_executor = cx.foreground_executor(); - let background_executor = cx.background_executor(); - - // 3 seconds hang - let (mut tx, mut rx) = futures::channel::mpsc::channel(3); - foreground_executor - .spawn(async move { while (rx.next().await).is_some() {} }) - .detach(); - - background_executor - .spawn({ - let background_executor = background_executor.clone(); - async move { - let mut hang_time = None; - - let mut hanging = false; - loop { - background_executor.timer(Duration::from_secs(1)).await; - match tx.try_send(()) { - Ok(_) => { - hang_time = None; - hanging = false; - continue; - } - Err(e) => { - let is_full = e.into_send_error().is_full(); - if is_full && !hanging { - hanging = true; - hang_time = Some(chrono::Local::now()); - } - - if is_full { - save_hang_trace( - main_thread_id, - &background_executor, - hang_time.unwrap(), - ); - } - } - } - } - } - }) - .detach(); -} - -fn save_hang_trace( - main_thread_id: ThreadId, - background_executor: &gpui::BackgroundExecutor, - hang_time: chrono::DateTime, -) { - let thread_timings = background_executor.dispatcher.get_all_timings(); - let thread_timings = thread_timings - .into_iter() - .map(|mut timings| { - if timings.thread_id == main_thread_id { - timings.thread_name = Some("main".to_string()); - } - - SerializedThreadTaskTimings::convert(*STARTUP_TIME.get().unwrap(), timings) - }) - .collect::>(); - - let trace_path = paths::hang_traces_dir().join(&format!( - "hang-{}.miniprof", - hang_time.format("%Y-%m-%d_%H-%M-%S") - )); - - let Some(timings) = serde_json::to_string(&thread_timings) - .context("hang timings serialization") - .log_err() - else { - return; - }; - - std::fs::write(&trace_path, timings) - .context("hang trace file writing") - .log_err(); - - info!( - "hang detected, trace file saved at: {}", - trace_path.display() - ); -} - -pub async fn upload_previous_minidumps(client: Arc) -> anyhow::Result<()> { - let Some(minidump_endpoint) = MINIDUMP_ENDPOINT.as_ref() else { - log::warn!("Minidump endpoint not set"); - return Ok(()); - }; - - let mut children = smol::fs::read_dir(paths::logs_dir()).await?; - while let Some(child) = children.next().await { - let child = child?; - let child_path = child.path(); - if child_path.extension() != Some(OsStr::new("dmp")) { - continue; - } - let mut json_path = child_path.clone(); - json_path.set_extension("json"); - if let Ok(metadata) = serde_json::from_slice(&smol::fs::read(&json_path).await?) - && upload_minidump( - client.clone(), - minidump_endpoint, - smol::fs::read(&child_path) - .await - .context("Failed to read minidump")?, - &metadata, - ) - .await - .log_err() - .is_some() - { - fs::remove_file(child_path).ok(); - fs::remove_file(json_path).ok(); - } - } - Ok(()) -} - -async fn upload_minidump( - client: Arc, - endpoint: &str, - minidump: Vec, - metadata: &crashes::CrashInfo, -) -> Result<()> { - let mut form = Form::new() - .part( - "upload_file_minidump", - Part::bytes(minidump) - .file_name("minidump.dmp") - .mime_str("application/octet-stream")?, - ) - .text( - "sentry[tags][channel]", - metadata.init.release_channel.clone(), - ) - .text("sentry[tags][version]", metadata.init.zed_version.clone()) - .text("sentry[tags][binary]", metadata.init.binary.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()); - } - if let Some(minidump_error) = metadata.minidump_error.clone() { - form = form.text("minidump_error", minidump_error); - } - - if let Some(id) = client.telemetry().metrics_id() { - form = form.text("sentry[user][id]", id.to_string()); - form = form.text( - "sentry[user][is_staff]", - if client.telemetry().is_staff().unwrap_or_default() { - "true" - } else { - "false" - }, - ); - } else if let Some(id) = client.telemetry().installation_id() { - form = form.text("sentry[user][id]", format!("installation-{}", 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 stream = form - .into_stream() - .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) - .into_async_read(); - let body = AsyncBody::from_reader(stream); - let req = Request::builder().uri(endpoint).body(body)?; - let mut response_text = String::new(); - let mut response = client.http_client().send(req).await?; - response - .body_mut() - .read_to_string(&mut response_text) - .await?; - if !response.status().is_success() { - anyhow::bail!("failed to upload minidump: {response_text}"); - } - log::info!("Uploaded minidump. event id: {response_text}"); - 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, - } - } -} diff --git a/crates/zed/src/zed-main.rs b/crates/zed/src/zed-main.rs deleted file mode 100644 index 6c49c197dd..0000000000 --- a/crates/zed/src/zed-main.rs +++ /dev/null @@ -1,8 +0,0 @@ -// Disable command line from opening on release mode -#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] - -pub fn main() { - // separated out so that the file containing the main function can be imported by other crates, - // while having all gpui resources that are registered in main (primarily actions) initialized - zed::main(); -} diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs deleted file mode 100644 index 71653124b1..0000000000 --- a/crates/zed/src/zed.rs +++ /dev/null @@ -1,5163 +0,0 @@ -mod app_menus; -pub mod component_preview; -pub mod edit_prediction_registry; -#[cfg(target_os = "macos")] -pub(crate) mod mac_only_instance; -mod migrate; -mod open_listener; -mod quick_action_bar; -#[cfg(target_os = "windows")] -pub(crate) mod windows_only_instance; - -use agent_ui::{AgentDiffToolbar, AgentPanelDelegate}; -use anyhow::Context as _; -pub use app_menus::*; -use assets::Assets; -use audio::{AudioSettings, REPLAY_DURATION}; -use breadcrumbs::Breadcrumbs; -use client::zed_urls; -use collections::VecDeque; -use debugger_ui::debugger_panel::DebugPanel; -use editor::{Editor, MultiBuffer}; -use extension_host::ExtensionStore; -use feature_flags::{FeatureFlagAppExt, PanicFeatureFlag}; -use fs::Fs; -use futures::FutureExt as _; -use futures::future::Either; -use futures::{StreamExt, channel::mpsc, select_biased}; -use git_ui::commit_view::CommitViewToolbar; -use git_ui::git_panel::GitPanel; -use git_ui::project_diff::ProjectDiffToolbar; -use gpui::{ - Action, App, AppContext as _, AsyncWindowContext, Context, DismissEvent, Element, Entity, - Focusable, KeyBinding, ParentElement, PathPromptOptions, PromptLevel, ReadGlobal, SharedString, - Styled, Task, TitlebarOptions, UpdateGlobal, WeakEntity, Window, WindowKind, WindowOptions, - actions, image_cache, point, px, retain_all, -}; -use image_viewer::ImageInfo; -use language::Capability; -use language_onboarding::BasedPyrightBanner; -use language_tools::lsp_button::{self, LspButton}; -use language_tools::lsp_log_view::LspLogToolbarItemView; -use migrate::{MigrationBanner, MigrationEvent, MigrationNotification, MigrationType}; -use migrator::migrate_keymap; -use onboarding::DOCS_URL; -use onboarding::multibuffer_hint::MultibufferHint; -pub use open_listener::*; -use outline_panel::OutlinePanel; -use paths::{ - local_debug_file_relative_path, local_settings_file_relative_path, - local_tasks_file_relative_path, -}; -use project::{DirectoryLister, DisableAiSettings, ProjectItem}; -use project_panel::ProjectPanel; -use prompt_store::PromptBuilder; -use quick_action_bar::QuickActionBar; -use recent_projects::open_remote_project; -use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; -use rope::Rope; -use search::project_search::ProjectSearchBar; -use settings::{ - BaseKeymap, DEFAULT_KEYMAP_PATH, InvalidSettingsError, KeybindSource, KeymapFile, - KeymapFileLoadResult, MigrationStatus, Settings, SettingsStore, VIM_KEYMAP_PATH, - initial_local_debug_tasks_content, initial_project_settings_content, initial_tasks_content, - update_settings_file, -}; -use std::time::Duration; -use std::{ - borrow::Cow, - path::{Path, PathBuf}, - sync::Arc, - sync::atomic::{self, AtomicBool}, -}; -use terminal_view::terminal_panel::{self, TerminalPanel}; -use theme::{ActiveTheme, GlobalTheme, SystemAppearance, ThemeRegistry, ThemeSettings}; -use ui::{PopoverMenuHandle, prelude::*}; -use util::markdown::MarkdownString; -use util::rel_path::RelPath; -use util::{ResultExt, asset_str}; -use uuid::Uuid; -use vim_mode_setting::VimModeSetting; -use workspace::notifications::{ - NotificationId, SuppressEvent, dismiss_app_notification, show_app_notification, -}; -use workspace::{ - AppState, NewFile, NewWindow, OpenLog, Toast, Workspace, WorkspaceSettings, - create_and_open_local_file, notifications::simple_message_notification::MessageNotification, - open_new, -}; -use workspace::{ - CloseIntent, CloseWindow, NotificationFrame, RestoreBanner, with_active_or_new_workspace, -}; -use workspace::{Pane, notifications::DetachAndPromptErr}; -use zed_actions::{ - OpenAccountSettings, OpenBrowser, OpenDocs, OpenServerSettings, OpenSettingsFile, OpenZedUrl, - Quit, -}; - -actions!( - zed, - [ - /// Opens the element inspector for debugging UI. - DebugElements, - /// Hides the application window. - Hide, - /// Hides all other application windows. - HideOthers, - /// Minimizes the current window. - Minimize, - /// Opens the default settings file. - OpenDefaultSettings, - /// Opens project-specific settings file. - OpenProjectSettingsFile, - /// Opens the project tasks configuration. - OpenProjectTasks, - /// Opens the tasks panel. - OpenTasks, - /// Opens debug tasks configuration. - OpenDebugTasks, - /// Resets the application database. - ResetDatabase, - /// Shows all hidden windows. - ShowAll, - /// Toggles fullscreen mode. - ToggleFullScreen, - /// Zooms the window. - Zoom, - /// Triggers a test panic for debugging. - TestPanic, - /// Triggers a hard crash for debugging. - TestCrash, - ] -); - -actions!( - dev, - [ - /// Stores last 30s of audio from zed staff using the experimental rodio - /// audio system (including yourself) on the current call in a tar file - /// in the current working directory. - CaptureRecentAudio, - ] -); - -pub fn init(cx: &mut App) { - #[cfg(target_os = "macos")] - cx.on_action(|_: &Hide, cx| cx.hide()); - #[cfg(target_os = "macos")] - cx.on_action(|_: &HideOthers, cx| cx.hide_other_apps()); - #[cfg(target_os = "macos")] - cx.on_action(|_: &ShowAll, cx| cx.unhide_other_apps()); - cx.on_action(quit); - - cx.on_action(|_: &RestoreBanner, cx| title_bar::restore_banner(cx)); - let flag = cx.wait_for_flag::(); - cx.spawn(async |cx| { - if cx - .update(|cx| ReleaseChannel::global(cx) == ReleaseChannel::Dev) - .unwrap_or_default() - || flag.await - { - cx.update(|cx| { - cx.on_action(|_: &TestPanic, _| panic!("Ran the TestPanic action")); - cx.on_action(|_: &TestCrash, _| { - unsafe extern "C" { - fn puts(s: *const i8); - } - unsafe { - puts(0xabad1d3a as *const i8); - } - }); - }) - .ok(); - }; - }) - .detach(); - cx.on_action(|_: &OpenLog, cx| { - with_active_or_new_workspace(cx, |workspace, window, cx| { - open_log_file(workspace, window, cx); - }); - }); - cx.on_action(|_: &workspace::RevealLogInFileManager, cx| { - cx.reveal_path(paths::log_file().as_path()); - }); - cx.on_action(|_: &zed_actions::OpenLicenses, cx| { - with_active_or_new_workspace(cx, |workspace, window, cx| { - open_bundled_file( - workspace, - asset_str::("licenses.md"), - "Open Source License Attribution", - "Markdown", - window, - cx, - ); - }); - }); - cx.on_action(|_: &zed_actions::OpenTelemetryLog, cx| { - with_active_or_new_workspace(cx, |workspace, window, cx| { - open_telemetry_log_file(workspace, window, cx); - }); - }); - cx.on_action(|&zed_actions::OpenKeymapFile, cx| { - with_active_or_new_workspace(cx, |_, window, cx| { - open_settings_file( - paths::keymap_file(), - || settings::initial_keymap_content().as_ref().into(), - window, - cx, - ); - }); - }); - cx.on_action(|_: &OpenSettingsFile, cx| { - with_active_or_new_workspace(cx, |_, window, cx| { - open_settings_file( - paths::settings_file(), - || settings::initial_user_settings_content().as_ref().into(), - window, - cx, - ); - }); - }); - cx.on_action(|_: &OpenAccountSettings, cx| { - with_active_or_new_workspace(cx, |_, _, cx| { - cx.open_url(&zed_urls::account_url(cx)); - }); - }); - cx.on_action(|_: &OpenTasks, cx| { - with_active_or_new_workspace(cx, |_, window, cx| { - open_settings_file( - paths::tasks_file(), - || settings::initial_tasks_content().as_ref().into(), - window, - cx, - ); - }); - }); - cx.on_action(|_: &OpenDebugTasks, cx| { - with_active_or_new_workspace(cx, |_, window, cx| { - open_settings_file( - paths::debug_scenarios_file(), - || settings::initial_debug_tasks_content().as_ref().into(), - window, - cx, - ); - }); - }); - cx.on_action(|_: &OpenDefaultSettings, cx| { - with_active_or_new_workspace(cx, |workspace, window, cx| { - open_bundled_file( - workspace, - settings::default_settings(), - "Default Settings", - "JSON", - window, - cx, - ); - }); - }); - cx.on_action(|_: &zed_actions::OpenDefaultKeymap, cx| { - with_active_or_new_workspace(cx, |workspace, window, cx| { - open_bundled_file( - workspace, - settings::default_keymap(), - "Default Key Bindings", - "JSON", - window, - cx, - ); - }); - }); - cx.on_action(|_: &zed_actions::About, cx| { - with_active_or_new_workspace(cx, |workspace, window, cx| { - about(workspace, window, cx); - }); - }); -} - -fn bind_on_window_closed(cx: &mut App) -> Option { - #[cfg(target_os = "macos")] - { - WorkspaceSettings::get_global(cx) - .on_last_window_closed - .is_quit_app() - .then(|| { - cx.on_window_closed(|cx| { - if cx.windows().is_empty() { - cx.quit(); - } - }) - }) - } - #[cfg(not(target_os = "macos"))] - { - Some(cx.on_window_closed(|cx| { - if cx.windows().is_empty() { - cx.quit(); - } - })) - } -} - -pub fn build_window_options(display_uuid: Option, cx: &mut App) -> WindowOptions { - let display = display_uuid.and_then(|uuid| { - cx.displays() - .into_iter() - .find(|display| display.uuid().ok() == Some(uuid)) - }); - let app_id = ReleaseChannel::global(cx).app_id(); - let window_decorations = match std::env::var("ZED_WINDOW_DECORATIONS") { - Ok(val) if val == "server" => gpui::WindowDecorations::Server, - Ok(val) if val == "client" => gpui::WindowDecorations::Client, - _ => match WorkspaceSettings::get_global(cx).window_decorations { - settings::WindowDecorations::Server => gpui::WindowDecorations::Server, - settings::WindowDecorations::Client => gpui::WindowDecorations::Client, - }, - }; - - let use_system_window_tabs = WorkspaceSettings::get_global(cx).use_system_window_tabs; - - WindowOptions { - titlebar: Some(TitlebarOptions { - title: None, - appears_transparent: true, - traffic_light_position: Some(point(px(9.0), px(9.0))), - }), - window_bounds: None, - focus: false, - show: false, - kind: WindowKind::Normal, - is_movable: true, - display_id: display.map(|display| display.id()), - window_background: cx.theme().window_background_appearance(), - app_id: Some(app_id.to_owned()), - window_decorations: Some(window_decorations), - window_min_size: Some(gpui::Size { - width: px(360.0), - height: px(240.0), - }), - tabbing_identifier: if use_system_window_tabs { - Some(String::from("zed")) - } else { - None - }, - ..Default::default() - } -} - -pub fn initialize_workspace( - app_state: Arc, - prompt_builder: Arc, - cx: &mut App, -) { - let mut _on_close_subscription = bind_on_window_closed(cx); - cx.observe_global::(move |cx| { - _on_close_subscription = bind_on_window_closed(cx); - }) - .detach(); - - cx.observe_new(move |workspace: &mut Workspace, window, cx| { - let Some(window) = window else { - return; - }; - - let workspace_handle = cx.entity(); - let center_pane = workspace.active_pane().clone(); - initialize_pane(workspace, ¢er_pane, window, cx); - - cx.subscribe_in(&workspace_handle, window, { - move |workspace, _, event, window, cx| match event { - workspace::Event::PaneAdded(pane) => { - initialize_pane(workspace, pane, window, cx); - } - workspace::Event::OpenBundledFile { - text, - title, - language, - } => open_bundled_file(workspace, text.clone(), title, language, window, cx), - _ => {} - } - }) - .detach(); - - #[cfg(not(target_os = "macos"))] - initialize_file_watcher(window, cx); - - 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 - ); - } - } - - #[cfg(target_os = "windows")] - unstable_version_notification(cx); - - let edit_prediction_menu_handle = PopoverMenuHandle::default(); - let edit_prediction_ui = cx.new(|cx| { - edit_prediction_ui::EditPredictionButton::new( - app_state.fs.clone(), - app_state.user_store.clone(), - edit_prediction_menu_handle.clone(), - app_state.client.clone(), - cx, - ) - }); - workspace.register_action({ - move |_, _: &edit_prediction_ui::ToggleMenu, window, cx| { - edit_prediction_menu_handle.toggle(window, cx); - } - }); - - let search_button = cx.new(|_| search::search_status_button::SearchButton::new()); - let diagnostic_summary = - cx.new(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx)); - let activity_indicator = activity_indicator::ActivityIndicator::new( - workspace, - workspace.project().read(cx).languages().clone(), - window, - cx, - ); - let active_buffer_language = - cx.new(|_| language_selector::ActiveBufferLanguage::new(workspace)); - let active_toolchain_language = - cx.new(|cx| toolchain_selector::ActiveToolchain::new(workspace, window, cx)); - let vim_mode_indicator = cx.new(|cx| vim::ModeIndicator::new(window, cx)); - let image_info = cx.new(|_cx| ImageInfo::new(workspace)); - - let lsp_button_menu_handle = PopoverMenuHandle::default(); - let lsp_button = - cx.new(|cx| LspButton::new(workspace, lsp_button_menu_handle.clone(), window, cx)); - workspace.register_action({ - move |_, _: &lsp_button::ToggleMenu, window, cx| { - lsp_button_menu_handle.toggle(window, cx); - } - }); - - let cursor_position = - cx.new(|_| go_to_line::cursor_position::CursorPosition::new(workspace)); - let line_ending_indicator = - cx.new(|_| line_ending_selector::LineEndingIndicator::default()); - workspace.status_bar().update(cx, |status_bar, cx| { - status_bar.add_left_item(search_button, window, cx); - status_bar.add_left_item(lsp_button, window, cx); - status_bar.add_left_item(diagnostic_summary, window, cx); - status_bar.add_left_item(activity_indicator, window, cx); - status_bar.add_right_item(edit_prediction_ui, window, cx); - status_bar.add_right_item(active_buffer_language, window, cx); - status_bar.add_right_item(active_toolchain_language, window, cx); - status_bar.add_right_item(line_ending_indicator, window, cx); - status_bar.add_right_item(vim_mode_indicator, window, cx); - status_bar.add_right_item(cursor_position, window, cx); - status_bar.add_right_item(image_info, window, cx); - }); - - let handle = cx.entity().downgrade(); - window.on_window_should_close(cx, move |window, cx| { - handle - .update(cx, |workspace, cx| { - // We'll handle closing asynchronously - workspace.close_window(&CloseWindow, window, cx); - false - }) - .unwrap_or(true) - }); - - initialize_panels(prompt_builder.clone(), window, cx); - register_actions(app_state.clone(), workspace, window, cx); - - workspace.focus_handle(cx).focus(window); - }) - .detach(); -} - -#[cfg(target_os = "windows")] -fn unstable_version_notification(cx: &mut App) { - if !matches!( - ReleaseChannel::try_global(cx), - Some(ReleaseChannel::Nightly) - ) { - return; - } - let db_key = "zed_windows_nightly_notif_shown_at".to_owned(); - let time = chrono::Utc::now(); - if let Some(last_shown) = db::kvp::KEY_VALUE_STORE - .read_kvp(&db_key) - .log_err() - .flatten() - .and_then(|timestamp| chrono::DateTime::parse_from_rfc3339(×tamp).ok()) - { - if time.fixed_offset() - last_shown < chrono::Duration::days(7) { - return; - } - } - cx.spawn(async move |_| { - db::kvp::KEY_VALUE_STORE - .write_kvp(db_key, time.to_rfc3339()) - .await - }) - .detach_and_log_err(cx); - struct WindowsNightly; - show_app_notification(NotificationId::unique::(), cx, |cx| { - cx.new(|cx| { - MessageNotification::new("You're using an unstable version of Zed (Nightly)", cx) - .primary_message("Download Stable") - .primary_icon_color(Color::Accent) - .primary_icon(IconName::Download) - .primary_on_click(|window, cx| { - window.dispatch_action( - zed_actions::OpenBrowser { - url: "https://zed.dev/download".to_string(), - } - .boxed_clone(), - cx, - ); - cx.emit(DismissEvent); - }) - }) - }); -} - -#[cfg(any(target_os = "linux", target_os = "freebsd"))] -fn initialize_file_watcher(window: &mut Window, cx: &mut Context) { - if let Err(e) = fs::fs_watcher::global(|_| {}) { - let message = format!( - db::indoc! {r#" - inotify_init returned {} - - This may be due to system-wide limits on inotify instances. For troubleshooting see: https://zed.dev/docs/linux - "#}, - e - ); - let prompt = window.prompt( - PromptLevel::Critical, - "Could not start inotify", - Some(&message), - &["Troubleshoot and Quit"], - cx, - ); - cx.spawn(async move |_, cx| { - if prompt.await == Ok(0) { - cx.update(|cx| { - cx.open_url("https://zed.dev/docs/linux#could-not-start-inotify"); - cx.quit(); - }) - .ok(); - } - }) - .detach() - } -} - -#[cfg(target_os = "windows")] -fn initialize_file_watcher(window: &mut Window, cx: &mut Context) { - if let Err(e) = fs::fs_watcher::global(|_| {}) { - let message = format!( - db::indoc! {r#" - ReadDirectoryChangesW initialization failed: {} - - This may occur on network filesystems and WSL paths. For troubleshooting see: https://zed.dev/docs/windows - "#}, - e - ); - let prompt = window.prompt( - PromptLevel::Critical, - "Could not start ReadDirectoryChangesW", - Some(&message), - &["Troubleshoot and Quit"], - cx, - ); - cx.spawn(async move |_, cx| { - if prompt.await == Ok(0) { - cx.update(|cx| { - cx.open_url("https://zed.dev/docs/windows"); - cx.quit() - }) - .ok(); - } - }) - .detach() - } -} - -fn show_software_emulation_warning_if_needed( - specs: gpui::GpuSpecs, - window: &mut Window, - cx: &mut Context, -) { - if specs.is_software_emulated && std::env::var("ZED_ALLOW_EMULATED_GPU").is_err() { - let (graphics_api, docs_url, open_url) = if cfg!(target_os = "windows") { - ( - "DirectX", - "https://zed.dev/docs/windows", - "https://zed.dev/docs/windows", - ) - } else { - ( - "Vulkan", - "https://zed.dev/docs/linux", - "https://zed.dev/docs/linux#zed-fails-to-open-windows", - ) - }; - let message = format!( - db::indoc! {r#" - Zed uses {} for rendering and requires a compatible GPU. - - Currently you are using a software emulated GPU ({}) which - will result in awful performance. - - For troubleshooting see: {} - Set ZED_ALLOW_EMULATED_GPU=1 env var to permanently override. - "#}, - graphics_api, specs.device_name, docs_url - ); - let prompt = window.prompt( - PromptLevel::Critical, - "Unsupported GPU", - Some(&message), - &["Skip", "Troubleshoot and Quit"], - cx, - ); - cx.spawn(async move |_, cx| { - if prompt.await == Ok(1) { - cx.update(|cx| { - cx.open_url(open_url); - cx.quit(); - }) - .ok(); - } - }) - .detach() - } -} - -fn initialize_panels( - prompt_builder: Arc, - window: &mut Window, - cx: &mut Context, -) { - cx.spawn_in(window, async move |workspace_handle, cx| { - let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone()); - let outline_panel = OutlinePanel::load(workspace_handle.clone(), cx.clone()); - let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone()); - let git_panel = GitPanel::load(workspace_handle.clone(), cx.clone()); - let channels_panel = - collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone()); - let notification_panel = collab_ui::notification_panel::NotificationPanel::load( - workspace_handle.clone(), - cx.clone(), - ); - let debug_panel = DebugPanel::load(workspace_handle.clone(), cx); - - async fn add_panel_when_ready( - panel_task: impl Future>> + 'static, - workspace_handle: WeakEntity, - mut cx: gpui::AsyncWindowContext, - ) { - if let Some(panel) = panel_task.await.context("failed to load panel").log_err() - { - workspace_handle - .update_in(&mut cx, |workspace, window, cx| { - workspace.add_panel(panel, window, cx); - }) - .log_err(); - } - } - - futures::join!( - add_panel_when_ready(project_panel, workspace_handle.clone(), cx.clone()), - add_panel_when_ready(outline_panel, workspace_handle.clone(), cx.clone()), - add_panel_when_ready(terminal_panel, workspace_handle.clone(), cx.clone()), - add_panel_when_ready(git_panel, workspace_handle.clone(), cx.clone()), - add_panel_when_ready(channels_panel, workspace_handle.clone(), cx.clone()), - add_panel_when_ready(notification_panel, workspace_handle.clone(), cx.clone()), - add_panel_when_ready(debug_panel, workspace_handle.clone(), cx.clone()), - initialize_agent_panel(workspace_handle, prompt_builder, cx.clone()).map(|r| r.log_err()) - ); - - anyhow::Ok(()) - }) - .detach(); -} - -async fn initialize_agent_panel( - workspace_handle: WeakEntity, - prompt_builder: Arc, - mut cx: AsyncWindowContext, -) -> anyhow::Result<()> { - fn setup_or_teardown_agent_panel( - workspace: &mut Workspace, - prompt_builder: Arc, - window: &mut Window, - cx: &mut Context, - ) -> Task> { - let disable_ai = SettingsStore::global(cx) - .get::(None) - .disable_ai - || cfg!(test); - let existing_panel = workspace.panel::(cx); - match (disable_ai, existing_panel) { - (false, None) => cx.spawn_in(window, async move |workspace, cx| { - let panel = - agent_ui::AgentPanel::load(workspace.clone(), prompt_builder, cx.clone()) - .await?; - workspace.update_in(cx, |workspace, window, cx| { - let disable_ai = SettingsStore::global(cx) - .get::(None) - .disable_ai; - let have_panel = workspace.panel::(cx).is_some(); - if !disable_ai && !have_panel { - workspace.add_panel(panel, window, cx); - } - }) - }), - (true, Some(existing_panel)) => { - workspace.remove_panel::(&existing_panel, window, cx); - Task::ready(Ok(())) - } - _ => Task::ready(Ok(())), - } - } - - workspace_handle - .update_in(&mut cx, |workspace, window, cx| { - setup_or_teardown_agent_panel(workspace, prompt_builder.clone(), window, cx) - })? - .await?; - - workspace_handle.update_in(&mut cx, |workspace, window, cx| { - cx.observe_global_in::(window, { - let prompt_builder = prompt_builder.clone(); - move |workspace, window, cx| { - setup_or_teardown_agent_panel(workspace, prompt_builder.clone(), window, cx) - .detach_and_log_err(cx); - } - }) - .detach(); - - // Register the actions that are shared between `assistant` and `assistant2`. - // - // We need to do this here instead of within the individual `init` - // functions so that we only register the actions once. - // - // Once we ship `assistant2` we can push this back down into `agent::agent_panel::init`. - if !cfg!(test) { - ::set_global( - Arc::new(agent_ui::ConcreteAssistantPanelDelegate), - cx, - ); - - workspace - .register_action(agent_ui::AgentPanel::toggle_focus) - .register_action(agent_ui::InlineAssistant::inline_assist); - } - })?; - - anyhow::Ok(()) -} - -fn register_actions( - app_state: Arc, - workspace: &mut Workspace, - _: &mut Window, - cx: &mut Context, -) { - workspace - .register_action(|_, _: &OpenDocs, _, cx| cx.open_url(DOCS_URL)) - .register_action(|_, _: &Minimize, window, _| { - window.minimize_window(); - }) - .register_action(|_, _: &Zoom, window, _| { - window.zoom_window(); - }) - .register_action(|_, _: &ToggleFullScreen, window, _| { - window.toggle_fullscreen(); - }) - .register_action(|_, action: &OpenZedUrl, _, cx| { - OpenListener::global(cx).open(RawOpenRequest { - urls: vec![action.url.clone()], - ..Default::default() - }) - }) - .register_action(|workspace, action: &OpenBrowser, _window, cx| { - // Parse and validate the URL to ensure it's properly formatted - match url::Url::parse(&action.url) { - Ok(parsed_url) => { - // Use the parsed URL's string representation which is properly escaped - cx.open_url(parsed_url.as_str()); - } - Err(e) => { - workspace.show_error( - &anyhow::anyhow!( - "Opening this URL in a browser failed because the URL is invalid: {}\n\nError was: {e}", - action.url - ), - cx, - ); - } - } - }) - .register_action(|workspace, _: &workspace::Open, window, cx| { - telemetry::event!("Project Opened"); - let paths = workspace.prompt_for_open_path( - PathPromptOptions { - files: true, - directories: true, - multiple: true, - prompt: None, - }, - DirectoryLister::Local( - workspace.project().clone(), - workspace.app_state().fs.clone(), - ), - window, - cx, - ); - - cx.spawn_in(window, async move |this, cx| { - let Some(paths) = paths.await.log_err().flatten() else { - return; - }; - - if let Some(task) = this - .update_in(cx, |this, window, cx| { - this.open_workspace_for_paths(false, paths, window, cx) - }) - .log_err() - { - task.await.log_err(); - } - }) - .detach() - }) - .register_action(|workspace, action: &zed_actions::OpenRemote, window, cx| { - if !action.from_existing_connection { - cx.propagate(); - return; - } - // You need existing remote connection to open it this way - if workspace.project().read(cx).is_local() { - return; - } - telemetry::event!("Project Opened"); - let paths = workspace.prompt_for_open_path( - PathPromptOptions { - files: true, - directories: true, - multiple: true, - prompt: None, - }, - DirectoryLister::Project(workspace.project().clone()), - window, - cx, - ); - cx.spawn_in(window, async move |this, cx| { - let Some(paths) = paths.await.log_err().flatten() else { - return; - }; - if let Some(task) = this - .update_in(cx, |this, window, cx| { - open_new_ssh_project_from_project(this, paths, window, cx) - }) - .log_err() - { - task.await.log_err(); - } - }) - .detach() - }) - .register_action({ - let fs = app_state.fs.clone(); - move |_, action: &zed_actions::IncreaseUiFontSize, _window, cx| { - if action.persist { - update_settings_file(fs.clone(), cx, move |settings, cx| { - let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx) + px(1.0); - let _ = settings - .theme - .ui_font_size - .insert(theme::clamp_font_size(ui_font_size).into()); - }); - } else { - theme::adjust_ui_font_size(cx, |size| size + px(1.0)); - } - } - }) - .register_action({ - let fs = app_state.fs.clone(); - move |_, action: &zed_actions::DecreaseUiFontSize, _window, cx| { - if action.persist { - update_settings_file(fs.clone(), cx, move |settings, cx| { - let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx) - px(1.0); - let _ = settings - .theme - .ui_font_size - .insert(theme::clamp_font_size(ui_font_size).into()); - }); - } else { - theme::adjust_ui_font_size(cx, |size| size - px(1.0)); - } - } - }) - .register_action({ - let fs = app_state.fs.clone(); - move |_, action: &zed_actions::ResetUiFontSize, _window, cx| { - if action.persist { - update_settings_file(fs.clone(), cx, move |settings, _| { - settings.theme.ui_font_size = None; - }); - } else { - theme::reset_ui_font_size(cx); - } - } - }) - .register_action({ - let fs = app_state.fs.clone(); - move |_, action: &zed_actions::IncreaseBufferFontSize, _window, cx| { - if action.persist { - update_settings_file(fs.clone(), cx, move |settings, cx| { - let buffer_font_size = - ThemeSettings::get_global(cx).buffer_font_size(cx) + px(1.0); - let _ = settings - .theme - .buffer_font_size - .insert(theme::clamp_font_size(buffer_font_size).into()); - }); - } else { - theme::adjust_buffer_font_size(cx, |size| size + px(1.0)); - } - } - }) - .register_action({ - let fs = app_state.fs.clone(); - move |_, action: &zed_actions::DecreaseBufferFontSize, _window, cx| { - if action.persist { - update_settings_file(fs.clone(), cx, move |settings, cx| { - let buffer_font_size = - ThemeSettings::get_global(cx).buffer_font_size(cx) - px(1.0); - let _ = settings - .theme - .buffer_font_size - .insert(theme::clamp_font_size(buffer_font_size).into()); - }); - } else { - theme::adjust_buffer_font_size(cx, |size| size - px(1.0)); - } - } - }) - .register_action({ - let fs = app_state.fs.clone(); - move |_, action: &zed_actions::ResetBufferFontSize, _window, cx| { - if action.persist { - update_settings_file(fs.clone(), cx, move |settings, _| { - settings.theme.buffer_font_size = None; - }); - } else { - theme::reset_buffer_font_size(cx); - } - } - }) - .register_action({ - let fs = app_state.fs.clone(); - move |_, action: &zed_actions::ResetAllZoom, _window, cx| { - if action.persist { - update_settings_file(fs.clone(), cx, move |settings, _| { - settings.theme.ui_font_size = None; - settings.theme.buffer_font_size = None; - settings.theme.agent_ui_font_size = None; - settings.theme.agent_buffer_font_size = None; - }); - } else { - theme::reset_ui_font_size(cx); - theme::reset_buffer_font_size(cx); - theme::reset_agent_ui_font_size(cx); - theme::reset_agent_buffer_font_size(cx); - } - } - }) - .register_action(|_, _: &install_cli::RegisterZedScheme, window, cx| { - cx.spawn_in(window, async move |workspace, cx| { - install_cli::register_zed_scheme(cx).await?; - workspace.update_in(cx, |workspace, _, cx| { - struct RegisterZedScheme; - - workspace.show_toast( - Toast::new( - NotificationId::unique::(), - format!( - "zed:// links will now open in {}.", - ReleaseChannel::global(cx).display_name() - ), - ), - cx, - ) - })?; - Ok(()) - }) - .detach_and_prompt_err( - "Error registering zed:// scheme", - window, - cx, - |_, _, _| None, - ); - }) - .register_action(open_project_settings_file) - .register_action(open_project_tasks_file) - .register_action(open_project_debug_tasks_file) - .register_action( - |workspace: &mut Workspace, - _: &zed_actions::project_panel::ToggleFocus, - window: &mut Window, - cx: &mut Context| { - workspace.toggle_panel_focus::(window, cx); - }, - ) - .register_action( - |workspace: &mut Workspace, - _: &outline_panel::ToggleFocus, - window: &mut Window, - cx: &mut Context| { - workspace.toggle_panel_focus::(window, cx); - }, - ) - .register_action( - |workspace: &mut Workspace, - _: &collab_ui::collab_panel::ToggleFocus, - window: &mut Window, - cx: &mut Context| { - workspace.toggle_panel_focus::(window, cx); - }, - ) - .register_action( - |workspace: &mut Workspace, - _: &collab_ui::notification_panel::ToggleFocus, - window: &mut Window, - cx: &mut Context| { - workspace.toggle_panel_focus::( - window, cx, - ); - }, - ) - .register_action( - |workspace: &mut Workspace, - _: &terminal_panel::ToggleFocus, - window: &mut Window, - cx: &mut Context| { - workspace.toggle_panel_focus::(window, cx); - }, - ) - .register_action({ - let app_state = Arc::downgrade(&app_state); - move |_, _: &NewWindow, _, cx| { - if let Some(app_state) = app_state.upgrade() { - open_new( - Default::default(), - app_state, - cx, - |workspace, window, cx| { - cx.activate(true); - Editor::new_file(workspace, &Default::default(), window, cx) - }, - ) - .detach(); - } - } - }) - .register_action({ - let app_state = Arc::downgrade(&app_state); - move |_, _: &NewFile, _, cx| { - if let Some(app_state) = app_state.upgrade() { - open_new( - Default::default(), - app_state, - cx, - |workspace, window, cx| { - Editor::new_file(workspace, &Default::default(), window, cx) - }, - ) - .detach(); - } - } - }) - .register_action(|workspace, _: &CaptureRecentAudio, window, cx| { - capture_recent_audio(workspace, window, cx); - }); - - #[cfg(not(target_os = "windows"))] - workspace.register_action(install_cli); - - if workspace.project().read(cx).is_via_remote_server() { - workspace.register_action({ - move |workspace, _: &OpenServerSettings, window, cx| { - let open_server_settings = workspace - .project() - .update(cx, |project, cx| project.open_server_settings(cx)); - - cx.spawn_in(window, async move |workspace, cx| { - let buffer = open_server_settings.await?; - - workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_path( - buffer - .read(cx) - .project_path(cx) - .expect("Settings file must have a location"), - None, - true, - window, - cx, - ) - })? - .await?; - - anyhow::Ok(()) - }) - .detach_and_log_err(cx); - } - }); - } -} - -fn initialize_pane( - workspace: &Workspace, - pane: &Entity, - window: &mut Window, - cx: &mut Context, -) { - pane.update(cx, |pane, cx| { - pane.toolbar().update(cx, |toolbar, cx| { - let multibuffer_hint = cx.new(|_| MultibufferHint::new()); - toolbar.add_item(multibuffer_hint, window, cx); - let breadcrumbs = cx.new(|_| Breadcrumbs::new()); - toolbar.add_item(breadcrumbs, window, cx); - let buffer_search_bar = cx.new(|cx| { - search::BufferSearchBar::new( - Some(workspace.project().read(cx).languages().clone()), - window, - cx, - ) - }); - toolbar.add_item(buffer_search_bar.clone(), window, cx); - let quick_action_bar = - cx.new(|cx| QuickActionBar::new(buffer_search_bar, workspace, cx)); - toolbar.add_item(quick_action_bar, window, cx); - let diagnostic_editor_controls = cx.new(|_| diagnostics::ToolbarControls::new()); - toolbar.add_item(diagnostic_editor_controls, window, cx); - let project_search_bar = cx.new(|_| ProjectSearchBar::new()); - toolbar.add_item(project_search_bar, window, cx); - let lsp_log_item = cx.new(|_| LspLogToolbarItemView::new()); - toolbar.add_item(lsp_log_item, window, cx); - let dap_log_item = cx.new(|_| debugger_tools::DapLogToolbarItemView::new()); - toolbar.add_item(dap_log_item, window, cx); - let acp_tools_item = cx.new(|_| acp_tools::AcpToolsToolbarItemView::new()); - toolbar.add_item(acp_tools_item, window, cx); - let syntax_tree_item = cx.new(|_| language_tools::SyntaxTreeToolbarItemView::new()); - toolbar.add_item(syntax_tree_item, window, cx); - let migration_banner = cx.new(|cx| MigrationBanner::new(workspace, cx)); - toolbar.add_item(migration_banner, window, cx); - let project_diff_toolbar = cx.new(|cx| ProjectDiffToolbar::new(workspace, cx)); - toolbar.add_item(project_diff_toolbar, window, cx); - let commit_view_toolbar = cx.new(|_| CommitViewToolbar::new()); - toolbar.add_item(commit_view_toolbar, window, cx); - let agent_diff_toolbar = cx.new(AgentDiffToolbar::new); - toolbar.add_item(agent_diff_toolbar, window, cx); - let basedpyright_banner = cx.new(|cx| BasedPyrightBanner::new(workspace, cx)); - toolbar.add_item(basedpyright_banner, window, cx); - }) - }); -} - -fn about(_: &mut Workspace, window: &mut Window, cx: &mut Context) { - use std::fmt::Write; - let release_channel = ReleaseChannel::global(cx).display_name(); - let full_version = AppVersion::global(cx); - let version = env!("CARGO_PKG_VERSION"); - let debug = if cfg!(debug_assertions) { - "(debug)" - } else { - "" - }; - let message = format!("{release_channel} {version} {debug}"); - - let mut detail = AppCommitSha::try_global(cx) - .map(|sha| sha.full()) - .unwrap_or_default(); - if !detail.is_empty() { - detail.push('\n'); - } - _ = write!(&mut detail, "\n{full_version}"); - - let detail = Some(detail); - - let prompt = window.prompt( - PromptLevel::Info, - &message, - detail.as_deref(), - &["Copy", "OK"], - cx, - ); - cx.spawn(async move |_, cx| { - if let Ok(0) = prompt.await { - let content = format!("{}\n{}", message, detail.as_deref().unwrap_or("")); - cx.update(|cx| { - cx.write_to_clipboard(gpui::ClipboardItem::new_string(content)); - }) - .ok(); - } - }) - .detach(); -} - -#[cfg(not(target_os = "windows"))] -fn install_cli( - _: &mut Workspace, - _: &install_cli::InstallCliBinary, - window: &mut Window, - cx: &mut Context, -) { - install_cli::install_cli_binary(window, cx) -} - -static WAITING_QUIT_CONFIRMATION: AtomicBool = AtomicBool::new(false); -fn quit(_: &Quit, cx: &mut App) { - if WAITING_QUIT_CONFIRMATION.load(atomic::Ordering::Acquire) { - return; - } - - let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit; - cx.spawn(async move |cx| { - let mut workspace_windows = cx.update(|cx| { - cx.windows() - .into_iter() - .filter_map(|window| window.downcast::()) - .collect::>() - })?; - - // If multiple windows have unsaved changes, and need a save prompt, - // prompt in the active window before switching to a different window. - cx.update(|cx| { - workspace_windows.sort_by_key(|window| window.is_active(cx) == Some(false)); - }) - .log_err(); - - if should_confirm && let Some(workspace) = workspace_windows.first() { - let answer = workspace - .update(cx, |_, window, cx| { - window.prompt( - PromptLevel::Info, - "Are you sure you want to quit?", - None, - &["Quit", "Cancel"], - cx, - ) - }) - .log_err(); - - if let Some(answer) = answer { - WAITING_QUIT_CONFIRMATION.store(true, atomic::Ordering::Release); - let answer = answer.await.ok(); - WAITING_QUIT_CONFIRMATION.store(false, atomic::Ordering::Release); - if answer != Some(0) { - return Ok(()); - } - } - } - - // If the user cancels any save prompt, then keep the app open. - for window in workspace_windows { - if let Some(should_close) = window - .update(cx, |workspace, window, cx| { - workspace.prepare_to_close(CloseIntent::Quit, window, cx) - }) - .log_err() - && !should_close.await? - { - return Ok(()); - } - } - cx.update(|cx| cx.quit())?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx); -} - -fn open_log_file(workspace: &mut Workspace, window: &mut Window, cx: &mut Context) { - const MAX_LINES: usize = 1000; - workspace - .with_local_workspace(window, cx, move |workspace, window, cx| { - let app_state = workspace.app_state(); - let languages = app_state.languages.clone(); - let fs = app_state.fs.clone(); - cx.spawn_in(window, async move |workspace, cx| { - let (old_log, new_log, log_language) = futures::join!( - fs.load(paths::old_log_file()), - fs.load(paths::log_file()), - languages.language_for_name("log") - ); - let log = match (old_log, new_log) { - (Err(_), Err(_)) => None, - (old_log, new_log) => { - let mut lines = VecDeque::with_capacity(MAX_LINES); - for line in old_log - .iter() - .flat_map(|log| log.lines()) - .chain(new_log.iter().flat_map(|log| log.lines())) - { - if lines.len() == MAX_LINES { - lines.pop_front(); - } - lines.push_back(line); - } - Some( - lines - .into_iter() - .flat_map(|line| [line, "\n"]) - .collect::(), - ) - } - }; - let log_language = log_language.ok(); - - workspace - .update_in(cx, |workspace, window, cx| { - let Some(log) = log else { - struct OpenLogError; - - workspace.show_notification( - NotificationId::unique::(), - cx, - |cx| { - cx.new(|cx| { - MessageNotification::new( - format!( - "Unable to access/open log file at path {:?}", - paths::log_file().as_path() - ), - cx, - ) - }) - }, - ); - return; - }; - let project = workspace.project().clone(); - let buffer = project.update(cx, |project, cx| { - project.create_local_buffer(&log, log_language, false, cx) - }); - - let buffer = cx - .new(|cx| MultiBuffer::singleton(buffer, cx).with_title("Log".into())); - let editor = cx.new(|cx| { - let mut editor = - Editor::for_multibuffer(buffer, Some(project), window, cx); - editor.set_read_only(true); - editor.set_breadcrumb_header(format!( - "Last {} lines in {}", - MAX_LINES, - paths::log_file().display() - )); - editor - }); - - editor.update(cx, |editor, cx| { - let last_multi_buffer_offset = editor.buffer().read(cx).len(cx); - editor.change_selections(Default::default(), window, cx, |s| { - s.select_ranges(Some( - last_multi_buffer_offset..last_multi_buffer_offset, - )); - }) - }); - - workspace.add_item_to_active_pane(Box::new(editor), None, true, window, cx); - }) - .log_err(); - }) - .detach(); - }) - .detach(); -} - -fn notify_settings_errors(result: settings::SettingsParseResult, is_user: bool, cx: &mut App) { - if let settings::ParseStatus::Failed { error: err } = &result.parse_status { - let settings_type = if is_user { "user" } else { "global" }; - log::error!("Failed to load {} settings: {err}", settings_type); - } - - let error = match result.parse_status { - settings::ParseStatus::Failed { error } => Some(anyhow::format_err!(error)), - settings::ParseStatus::Success => None, - }; - let id = NotificationId::Named(format!("failed-to-parse-settings-{is_user}").into()); - - let showed_parse_error = match error { - Some(error) => { - if let Some(InvalidSettingsError::LocalSettings { .. }) = - error.downcast_ref::() - { - false - // Local settings errors are displayed by the projects - } else { - show_app_notification(id, cx, move |cx| { - cx.new(|cx| { - MessageNotification::new(format!("Invalid user settings file\n{error}"), cx) - .primary_message("Open Settings File") - .primary_icon(IconName::Settings) - .primary_on_click(|window, cx| { - window.dispatch_action( - zed_actions::OpenSettingsFile.boxed_clone(), - cx, - ); - cx.emit(DismissEvent); - }) - }) - }); - true - } - } - None => { - dismiss_app_notification(&id, cx); - false - } - }; - let id = NotificationId::Named(format!("failed-to-migrate-settings-{is_user}").into()); - - match result.migration_status { - settings::MigrationStatus::Succeeded | settings::MigrationStatus::NotNeeded => { - dismiss_app_notification(&id, cx); - } - settings::MigrationStatus::Failed { error: err } => { - if !showed_parse_error { - show_app_notification(id, cx, move |cx| { - cx.new(|cx| { - MessageNotification::new( - format!( - "Failed to migrate settings\n\ - {err}" - ), - cx, - ) - .primary_message("Open Settings File") - .primary_icon(IconName::Settings) - .primary_on_click(|window, cx| { - window.dispatch_action(zed_actions::OpenSettingsFile.boxed_clone(), cx); - cx.emit(DismissEvent); - }) - }) - }); - } - } - }; -} - -pub fn handle_settings_file_changes( - mut user_settings_file_rx: mpsc::UnboundedReceiver, - mut global_settings_file_rx: mpsc::UnboundedReceiver, - cx: &mut App, -) { - MigrationNotification::set_global(cx.new(|_| MigrationNotification), cx); - - // Initial load of both settings files - let global_content = cx - .background_executor() - .block(global_settings_file_rx.next()) - .unwrap(); - let user_content = cx - .background_executor() - .block(user_settings_file_rx.next()) - .unwrap(); - - SettingsStore::update_global(cx, |store, cx| { - notify_settings_errors(store.set_user_settings(&user_content, cx), true, cx); - notify_settings_errors(store.set_global_settings(&global_content, cx), false, cx); - }); - - // Watch for changes in both files - cx.spawn(async move |cx| { - let mut settings_streams = futures::stream::select( - global_settings_file_rx.map(Either::Left), - user_settings_file_rx.map(Either::Right), - ); - - while let Some(content) = settings_streams.next().await { - let (content, is_user) = match content { - Either::Left(content) => (content, false), - Either::Right(content) => (content, true), - }; - - let result = cx.update_global(|store: &mut SettingsStore, cx| { - let result = if is_user { - store.set_user_settings(&content, cx) - } else { - store.set_global_settings(&content, cx) - }; - let migrating_in_memory = - matches!(&result.migration_status, MigrationStatus::Succeeded); - notify_settings_errors(result, is_user, cx); - if let Some(notifier) = MigrationNotification::try_global(cx) { - notifier.update(cx, |_, cx| { - cx.emit(MigrationEvent::ContentChanged { - migration_type: MigrationType::Settings, - migrating_in_memory, - }); - }); - } - cx.refresh_windows(); - }); - - if result.is_err() { - break; // App dropped - } - } - }) - .detach(); -} - -pub fn handle_keymap_file_changes( - mut user_keymap_file_rx: mpsc::UnboundedReceiver, - cx: &mut App, -) { - let (base_keymap_tx, mut base_keymap_rx) = mpsc::unbounded(); - let (keyboard_layout_tx, mut keyboard_layout_rx) = mpsc::unbounded(); - let mut old_base_keymap = *BaseKeymap::get_global(cx); - let mut old_vim_enabled = VimModeSetting::get_global(cx).0; - let mut old_helix_enabled = vim_mode_setting::HelixModeSetting::get_global(cx).0; - - cx.observe_global::(move |cx| { - let new_base_keymap = *BaseKeymap::get_global(cx); - let new_vim_enabled = VimModeSetting::get_global(cx).0; - let new_helix_enabled = vim_mode_setting::HelixModeSetting::get_global(cx).0; - - if new_base_keymap != old_base_keymap - || new_vim_enabled != old_vim_enabled - || new_helix_enabled != old_helix_enabled - { - old_base_keymap = new_base_keymap; - old_vim_enabled = new_vim_enabled; - old_helix_enabled = new_helix_enabled; - - base_keymap_tx.unbounded_send(()).unwrap(); - } - }) - .detach(); - - #[cfg(target_os = "windows")] - { - let mut current_layout_id = cx.keyboard_layout().id().to_string(); - 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(); - keyboard_layout_tx.unbounded_send(()).ok(); - } - }) - .detach(); - } - - #[cfg(not(target_os = "windows"))] - { - let mut current_mapping = cx.keyboard_mapper().get_key_equivalents().cloned(); - cx.on_keyboard_layout_change(move |cx| { - let next_mapping = cx.keyboard_mapper().get_key_equivalents(); - if current_mapping.as_ref() != next_mapping { - current_mapping = next_mapping.cloned(); - keyboard_layout_tx.unbounded_send(()).ok(); - } - }) - .detach(); - } - - load_default_keymap(cx); - - struct KeymapParseErrorNotification; - let notification_id = NotificationId::unique::(); - - cx.spawn(async move |cx| { - let mut user_keymap_content = String::new(); - let mut migrating_in_memory = false; - loop { - select_biased! { - _ = base_keymap_rx.next() => {}, - _ = keyboard_layout_rx.next() => {}, - content = user_keymap_file_rx.next() => { - if let Some(content) = content { - if let Ok(Some(migrated_content)) = migrate_keymap(&content) { - user_keymap_content = migrated_content; - migrating_in_memory = true; - } else { - user_keymap_content = content; - migrating_in_memory = false; - } - } - } - }; - cx.update(|cx| { - if let Some(notifier) = MigrationNotification::try_global(cx) { - notifier.update(cx, |_, cx| { - cx.emit(MigrationEvent::ContentChanged { - migration_type: MigrationType::Keymap, - migrating_in_memory, - }); - }); - } - let load_result = KeymapFile::load(&user_keymap_content, cx); - match load_result { - KeymapFileLoadResult::Success { key_bindings } => { - reload_keymaps(cx, key_bindings); - dismiss_app_notification(¬ification_id.clone(), cx); - } - KeymapFileLoadResult::SomeFailedToLoad { - key_bindings, - error_message, - } => { - if !key_bindings.is_empty() { - reload_keymaps(cx, key_bindings); - } - show_keymap_file_load_error(notification_id.clone(), error_message, cx); - } - KeymapFileLoadResult::JsonParseFailure { error } => { - show_keymap_file_json_error(notification_id.clone(), &error, cx) - } - } - }) - .ok(); - } - }) - .detach(); -} - -fn show_keymap_file_json_error( - notification_id: NotificationId, - error: &anyhow::Error, - cx: &mut App, -) { - let message: SharedString = - format!("JSON parse error in keymap file. Bindings not reloaded.\n\n{error}").into(); - show_app_notification(notification_id, cx, move |cx| { - cx.new(|cx| { - MessageNotification::new(message.clone(), cx) - .primary_message("Open Keymap File") - .primary_on_click(|window, cx| { - window.dispatch_action(zed_actions::OpenKeymapFile.boxed_clone(), cx); - cx.emit(DismissEvent); - }) - }) - }); -} - -fn show_keymap_file_load_error( - notification_id: NotificationId, - error_message: MarkdownString, - cx: &mut App, -) { - show_markdown_app_notification( - notification_id, - error_message, - "Open Keymap File".into(), - |window, cx| { - window.dispatch_action(zed_actions::OpenKeymapFile.boxed_clone(), cx); - cx.emit(DismissEvent); - }, - cx, - ) -} - -fn show_markdown_app_notification( - notification_id: NotificationId, - message: MarkdownString, - primary_button_message: SharedString, - primary_button_on_click: F, - cx: &mut App, -) where - F: 'static + Send + Sync + Fn(&mut Window, &mut Context), -{ - let parsed_markdown = cx.background_spawn(async move { - let file_location_directory = None; - let language_registry = None; - markdown_preview::markdown_parser::parse_markdown( - &message.0, - file_location_directory, - language_registry, - ) - .await - }); - - cx.spawn(async move |cx| { - let parsed_markdown = Arc::new(parsed_markdown.await); - let primary_button_message = primary_button_message.clone(); - let primary_button_on_click = Arc::new(primary_button_on_click); - cx.update(|cx| { - show_app_notification(notification_id, cx, move |cx| { - let workspace_handle = cx.entity().downgrade(); - let parsed_markdown = parsed_markdown.clone(); - let primary_button_message = primary_button_message.clone(); - let primary_button_on_click = primary_button_on_click.clone(); - cx.new(move |cx| { - MessageNotification::new_from_builder(cx, move |window, cx| { - image_cache(retain_all("notification-cache")) - .text_xs() - .child(markdown_preview::markdown_renderer::render_parsed_markdown( - &parsed_markdown.clone(), - Some(workspace_handle.clone()), - window, - cx, - )) - .into_any() - }) - .primary_message(primary_button_message) - .primary_on_click_arc(primary_button_on_click) - }) - }) - }) - .ok(); - }) - .detach(); -} - -fn reload_keymaps(cx: &mut App, mut user_key_bindings: Vec) { - cx.clear_key_bindings(); - load_default_keymap(cx); - - for key_binding in &mut user_key_bindings { - key_binding.set_meta(KeybindSource::User.meta()); - } - cx.bind_keys(user_key_bindings); - - let menus = app_menus(cx); - cx.set_menus(menus); - // On Windows, this is set in the `update_jump_list` method of the `HistoryManager`. - #[cfg(not(target_os = "windows"))] - cx.set_dock_menu(vec![gpui::MenuItem::action( - "New Window", - workspace::NewWindow, - )]); - // todo: nicer api here? - keymap_editor::KeymapEventChannel::trigger_keymap_changed(cx); -} - -pub fn load_default_keymap(cx: &mut App) { - let base_keymap = *BaseKeymap::get_global(cx); - if base_keymap == BaseKeymap::None { - return; - } - - cx.bind_keys( - KeymapFile::load_asset(DEFAULT_KEYMAP_PATH, Some(KeybindSource::Default), cx).unwrap(), - ); - - if let Some(asset_path) = base_keymap.asset_path() { - cx.bind_keys(KeymapFile::load_asset(asset_path, Some(KeybindSource::Base), cx).unwrap()); - } - - if VimModeSetting::get_global(cx).0 || vim_mode_setting::HelixModeSetting::get_global(cx).0 { - cx.bind_keys( - KeymapFile::load_asset(VIM_KEYMAP_PATH, Some(KeybindSource::Vim), cx).unwrap(), - ); - } -} - -pub fn open_new_ssh_project_from_project( - workspace: &mut Workspace, - paths: Vec, - window: &mut Window, - cx: &mut Context, -) -> Task> { - let app_state = workspace.app_state().clone(); - let Some(ssh_client) = workspace.project().read(cx).remote_client() else { - return Task::ready(Err(anyhow::anyhow!("Not an ssh project"))); - }; - let connection_options = ssh_client.read(cx).connection_options(); - cx.spawn_in(window, async move |_, cx| { - open_remote_project( - connection_options, - paths, - app_state, - workspace::OpenOptions { - open_new_workspace: Some(true), - ..Default::default() - }, - cx, - ) - .await - }) -} - -fn open_project_settings_file( - workspace: &mut Workspace, - _: &OpenProjectSettingsFile, - window: &mut Window, - cx: &mut Context, -) { - open_local_file( - workspace, - local_settings_file_relative_path(), - initial_project_settings_content(), - window, - cx, - ) -} - -fn open_project_tasks_file( - workspace: &mut Workspace, - _: &OpenProjectTasks, - window: &mut Window, - cx: &mut Context, -) { - open_local_file( - workspace, - local_tasks_file_relative_path(), - initial_tasks_content(), - window, - cx, - ) -} - -fn open_project_debug_tasks_file( - workspace: &mut Workspace, - _: &zed_actions::OpenProjectDebugTasks, - window: &mut Window, - cx: &mut Context, -) { - open_local_file( - workspace, - local_debug_file_relative_path(), - initial_local_debug_tasks_content(), - window, - cx, - ) -} - -fn open_local_file( - workspace: &mut Workspace, - settings_relative_path: &'static RelPath, - initial_contents: Cow<'static, str>, - window: &mut Window, - cx: &mut Context, -) { - let project = workspace.project().clone(); - let worktree = project - .read(cx) - .visible_worktrees(cx) - .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree)); - if let Some(worktree) = worktree { - let tree_id = worktree.read(cx).id(); - cx.spawn_in(window, async move |workspace, cx| { - // Check if the file actually exists on disk (even if it's excluded from worktree) - let file_exists = { - let full_path = worktree.read_with(cx, |tree, _| { - tree.abs_path().join(settings_relative_path.as_std_path()) - })?; - - let fs = project.read_with(cx, |project, _| project.fs().clone())?; - - fs.metadata(&full_path) - .await - .ok() - .flatten() - .is_some_and(|metadata| !metadata.is_dir && !metadata.is_fifo) - }; - - if !file_exists { - if let Some(dir_path) = settings_relative_path.parent() - && worktree.read_with(cx, |tree, _| tree.entry_for_path(dir_path).is_none())? - { - project - .update(cx, |project, cx| { - project.create_entry((tree_id, dir_path), true, cx) - })? - .await - .context("worktree was removed")?; - } - - if worktree.read_with(cx, |tree, _| { - tree.entry_for_path(settings_relative_path).is_none() - })? { - project - .update(cx, |project, cx| { - project.create_entry((tree_id, settings_relative_path), false, cx) - })? - .await - .context("worktree was removed")?; - } - } - - let editor = workspace - .update_in(cx, |workspace, window, cx| { - workspace.open_path((tree_id, settings_relative_path), None, true, window, cx) - })? - .await? - .downcast::() - .context("unexpected item type: expected editor item")?; - - editor - .downgrade() - .update(cx, |editor, cx| { - if let Some(buffer) = editor.buffer().read(cx).as_singleton() - && buffer.read(cx).is_empty() - { - buffer.update(cx, |buffer, cx| { - buffer.edit([(0..0, initial_contents)], None, cx) - }); - } - }) - .ok(); - - anyhow::Ok(()) - }) - .detach(); - } else { - struct NoOpenFolders; - - workspace.show_notification(NotificationId::unique::(), cx, |cx| { - cx.new(|cx| MessageNotification::new("This project has no folders open.", cx)) - }) - } -} - -fn open_telemetry_log_file( - workspace: &mut Workspace, - window: &mut Window, - cx: &mut Context, -) { - const HEADER: &str = concat!( - "// Zed collects anonymous usage data to help us understand how people are using the app.\n", - "// Telemetry can be disabled via the `settings.json` file.\n", - "// Here is the data that has been reported for the current session:\n", - ); - workspace - .with_local_workspace(window, cx, move |workspace, window, cx| { - let app_state = workspace.app_state().clone(); - cx.spawn_in(window, async move |workspace, cx| { - async fn fetch_log_string(app_state: &Arc) -> Option { - let path = client::telemetry::Telemetry::log_file_path(); - app_state.fs.load(&path).await.log_err() - } - - let log = fetch_log_string(&app_state) - .await - .unwrap_or_else(|| "// No data has been collected yet".to_string()); - - const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024; - let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN); - if let Some(newline_offset) = log[start_offset..].find('\n') { - start_offset += newline_offset + 1; - } - let log_suffix = &log[start_offset..]; - let content = format!("{}\n{}", HEADER, log_suffix); - let json = app_state - .languages - .language_for_name("JSON") - .await - .log_err(); - - workspace - .update_in(cx, |workspace, window, cx| { - let project = workspace.project().clone(); - let buffer = project.update(cx, |project, cx| { - project.create_local_buffer(&content, json, false, cx) - }); - let buffer = cx.new(|cx| { - MultiBuffer::singleton(buffer, cx).with_title("Telemetry Log".into()) - }); - workspace.add_item_to_active_pane( - Box::new(cx.new(|cx| { - let mut editor = - Editor::for_multibuffer(buffer, Some(project), window, cx); - editor.set_read_only(true); - editor.set_breadcrumb_header("Telemetry Log".into()); - editor - })), - None, - true, - window, - cx, - ); - }) - .log_err()?; - - Some(()) - }) - .detach(); - }) - .detach(); -} - -fn open_bundled_file( - workspace: &Workspace, - text: Cow<'static, str>, - title: &'static str, - language: &'static str, - window: &mut Window, - cx: &mut Context, -) { - let language = workspace.app_state().languages.language_for_name(language); - cx.spawn_in(window, async move |workspace, cx| { - let language = language.await.log_err(); - workspace - .update_in(cx, |workspace, window, cx| { - workspace.with_local_workspace(window, cx, |workspace, window, cx| { - let project = workspace.project(); - let buffer = project.update(cx, move |project, cx| { - let buffer = - project.create_local_buffer(text.as_ref(), language, false, cx); - buffer.update(cx, |buffer, cx| { - buffer.set_capability(Capability::ReadOnly, cx); - }); - buffer - }); - let buffer = - cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(title.into())); - workspace.add_item_to_active_pane( - Box::new(cx.new(|cx| { - let mut editor = - Editor::for_multibuffer(buffer, Some(project.clone()), window, cx); - editor.set_read_only(true); - editor.set_should_serialize(false, cx); - editor.set_breadcrumb_header(title.into()); - editor - })), - None, - true, - window, - cx, - ); - }) - })? - .await - }) - .detach_and_log_err(cx); -} - -fn open_settings_file( - abs_path: &'static Path, - default_content: impl FnOnce() -> Rope + Send + 'static, - window: &mut Window, - cx: &mut Context, -) { - cx.spawn_in(window, async move |workspace, cx| { - let (worktree_creation_task, settings_open_task) = workspace - .update_in(cx, |workspace, window, cx| { - workspace.with_local_workspace(window, cx, move |workspace, window, cx| { - let worktree_creation_task = workspace.project().update(cx, |project, cx| { - // Set up a dedicated worktree for settings, since - // otherwise we're dropping and re-starting LSP servers - // for each file inside on every settings file - // close/open - - // TODO: Do note that all other external files (e.g. - // drag and drop from OS) still have their worktrees - // released on file close, causing LSP servers' - // restarts. - project.find_or_create_worktree(paths::config_dir().as_path(), false, cx) - }); - let settings_open_task = - create_and_open_local_file(abs_path, window, cx, default_content); - (worktree_creation_task, settings_open_task) - }) - })? - .await?; - let _ = worktree_creation_task.await?; - let _ = settings_open_task.await?; - anyhow::Ok(()) - }) - .detach_and_log_err(cx); -} - -fn capture_recent_audio(workspace: &mut Workspace, _: &mut Window, cx: &mut Context) { - struct CaptureRecentAudioNotification { - focus_handle: gpui::FocusHandle, - save_result: Option>, - _save_task: Task>, - } - - impl gpui::EventEmitter for CaptureRecentAudioNotification {} - impl gpui::EventEmitter for CaptureRecentAudioNotification {} - impl gpui::Focusable for CaptureRecentAudioNotification { - fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle { - self.focus_handle.clone() - } - } - impl workspace::notifications::Notification for CaptureRecentAudioNotification {} - - impl Render for CaptureRecentAudioNotification { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let message = match &self.save_result { - None => format!( - "Saving up to {} seconds of recent audio", - REPLAY_DURATION.as_secs(), - ), - Some(Ok((path, duration))) => format!( - "Saved {} seconds of all audio to {}", - duration.as_secs(), - path.display(), - ), - Some(Err(e)) => format!("Error saving audio replays: {e:?}"), - }; - - NotificationFrame::new() - .with_title(Some("Saved Audio")) - .show_suppress_button(false) - .on_close(cx.listener(|_, _, _, cx| { - cx.emit(DismissEvent); - })) - .with_content(message) - } - } - - impl CaptureRecentAudioNotification { - fn new(cx: &mut Context) -> Self { - if AudioSettings::get_global(cx).rodio_audio { - let executor = cx.background_executor().clone(); - let save_task = cx.default_global::().save_replays(executor); - let _save_task = cx.spawn(async move |this, cx| { - let res = save_task.await; - this.update(cx, |this, cx| { - this.save_result = Some(res); - cx.notify(); - }) - }); - - Self { - focus_handle: cx.focus_handle(), - _save_task, - save_result: None, - } - } else { - Self { - focus_handle: cx.focus_handle(), - _save_task: Task::ready(Ok(())), - save_result: Some(Err(anyhow::anyhow!( - "Capturing recent audio is only supported on the experimental rodio audio pipeline" - ))), - } - } - } - } - - workspace.show_notification( - NotificationId::unique::(), - cx, - |cx| cx.new(CaptureRecentAudioNotification::new), - ); -} - -/// Eagerly loads the active theme and icon theme based on the selections in the -/// theme settings. -/// -/// This fast path exists to load these themes as soon as possible so the user -/// doesn't see the default themes while waiting on extensions to load. -pub(crate) fn eager_load_active_theme_and_icon_theme(fs: Arc, cx: &mut App) { - let extension_store = ExtensionStore::global(cx); - let theme_registry = ThemeRegistry::global(cx); - let theme_settings = ThemeSettings::get_global(cx); - let appearance = SystemAppearance::global(cx).0; - - enum LoadTarget { - Theme(PathBuf), - IconTheme((PathBuf, PathBuf)), - } - - let theme_name = theme_settings.theme.name(appearance); - let icon_theme_name = theme_settings.icon_theme.name(appearance); - let themes_to_load = [ - theme_registry - .get(&theme_name.0) - .is_err() - .then(|| { - extension_store - .read(cx) - .path_to_extension_theme(&theme_name.0) - }) - .flatten() - .map(LoadTarget::Theme), - theme_registry - .get_icon_theme(&icon_theme_name.0) - .is_err() - .then(|| { - extension_store - .read(cx) - .path_to_extension_icon_theme(&icon_theme_name.0) - }) - .flatten() - .map(LoadTarget::IconTheme), - ]; - - enum ReloadTarget { - Theme, - IconTheme, - } - - let executor = cx.background_executor(); - let reload_tasks = parking_lot::Mutex::new(Vec::with_capacity(themes_to_load.len())); - - let mut themes_to_load = themes_to_load.into_iter().flatten().peekable(); - - if themes_to_load.peek().is_none() { - return; - } - - executor.block(executor.scoped(|scope| { - for load_target in themes_to_load { - let theme_registry = &theme_registry; - let reload_tasks = &reload_tasks; - let fs = fs.clone(); - - scope.spawn(async { - match load_target { - LoadTarget::Theme(theme_path) => { - if theme_registry - .load_user_theme(&theme_path, fs) - .await - .log_err() - .is_some() - { - reload_tasks.lock().push(ReloadTarget::Theme); - } - } - LoadTarget::IconTheme((icon_theme_path, icons_root_path)) => { - if theme_registry - .load_icon_theme(&icon_theme_path, &icons_root_path, fs) - .await - .log_err() - .is_some() - { - reload_tasks.lock().push(ReloadTarget::IconTheme); - } - } - } - }); - } - })); - - for reload_target in reload_tasks.into_inner() { - match reload_target { - ReloadTarget::Theme => GlobalTheme::reload_theme(cx), - ReloadTarget::IconTheme => GlobalTheme::reload_icon_theme(cx), - }; - } -} - -#[cfg(test)] -mod tests { - use super::*; - use assets::Assets; - use collections::HashSet; - use editor::{ - DisplayPoint, Editor, MultiBufferOffset, SelectionEffects, display_map::DisplayRow, - }; - use gpui::{ - Action, AnyWindowHandle, App, AssetSource, BorrowAppContext, TestAppContext, UpdateGlobal, - VisualTestContext, WindowHandle, actions, - }; - use language::LanguageRegistry; - use languages::{markdown_lang, rust_lang}; - use pretty_assertions::{assert_eq, assert_ne}; - use project::{Project, ProjectPath}; - use semver::Version; - use serde_json::json; - use settings::{SettingsStore, watch_config_file}; - use std::{ - path::{Path, PathBuf}, - time::Duration, - }; - use theme::ThemeRegistry; - use util::{ - path, - rel_path::{RelPath, rel_path}, - }; - use workspace::{ - NewFile, OpenOptions, OpenVisible, SERIALIZATION_THROTTLE_TIME, SaveIntent, SplitDirection, - WorkspaceHandle, - item::SaveOptions, - item::{Item, ItemHandle}, - open_new, open_paths, pane, - }; - - #[gpui::test] - async fn test_open_non_existing_file(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - path!("/root"), - json!({ - "a": { - }, - }), - ) - .await; - - cx.update(|cx| { - open_paths( - &[PathBuf::from(path!("/root/a/new"))], - app_state.clone(), - workspace::OpenOptions::default(), - cx, - ) - }) - .await - .unwrap(); - assert_eq!(cx.read(|cx| cx.windows().len()), 1); - - let workspace = cx.windows()[0].downcast::().unwrap(); - workspace - .update(cx, |workspace, _, cx| { - assert!(workspace.active_item_as::(cx).is_some()) - }) - .unwrap(); - } - - #[gpui::test] - async fn test_open_paths_action(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - "/root", - json!({ - "a": { - "aa": null, - "ab": null, - }, - "b": { - "ba": null, - "bb": null, - }, - "c": { - "ca": null, - "cb": null, - }, - "d": { - "da": null, - "db": null, - }, - "e": { - "ea": null, - "eb": null, - } - }), - ) - .await; - - cx.update(|cx| { - open_paths( - &[PathBuf::from("/root/a"), PathBuf::from("/root/b")], - app_state.clone(), - workspace::OpenOptions::default(), - cx, - ) - }) - .await - .unwrap(); - assert_eq!(cx.read(|cx| cx.windows().len()), 1); - - cx.update(|cx| { - open_paths( - &[PathBuf::from("/root/a")], - app_state.clone(), - workspace::OpenOptions::default(), - cx, - ) - }) - .await - .unwrap(); - assert_eq!(cx.read(|cx| cx.windows().len()), 1); - let workspace_1 = cx - .read(|cx| cx.windows()[0].downcast::()) - .unwrap(); - cx.run_until_parked(); - workspace_1 - .update(cx, |workspace, window, cx| { - assert_eq!(workspace.worktrees(cx).count(), 2); - assert!(workspace.left_dock().read(cx).is_open()); - assert!( - workspace - .active_pane() - .read(cx) - .focus_handle(cx) - .is_focused(window) - ); - }) - .unwrap(); - - cx.update(|cx| { - open_paths( - &[PathBuf::from("/root/c"), PathBuf::from("/root/d")], - app_state.clone(), - workspace::OpenOptions::default(), - cx, - ) - }) - .await - .unwrap(); - assert_eq!(cx.read(|cx| cx.windows().len()), 2); - - // Replace existing windows - let window = cx - .update(|cx| cx.windows()[0].downcast::()) - .unwrap(); - cx.update(|cx| { - open_paths( - &[PathBuf::from("/root/e")], - app_state, - workspace::OpenOptions { - replace_window: Some(window), - ..Default::default() - }, - cx, - ) - }) - .await - .unwrap(); - cx.background_executor.run_until_parked(); - assert_eq!(cx.read(|cx| cx.windows().len()), 2); - let workspace_1 = cx - .update(|cx| cx.windows()[0].downcast::()) - .unwrap(); - workspace_1 - .update(cx, |workspace, window, cx| { - assert_eq!( - workspace - .worktrees(cx) - .map(|w| w.read(cx).abs_path()) - .collect::>(), - &[Path::new("/root/e").into()] - ); - assert!(workspace.left_dock().read(cx).is_open()); - assert!(workspace.active_pane().focus_handle(cx).is_focused(window)); - }) - .unwrap(); - } - - #[gpui::test] - async fn test_open_add_new(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - path!("/root"), - json!({"a": "hey", "b": "", "dir": {"c": "f"}}), - ) - .await; - - cx.update(|cx| { - open_paths( - &[PathBuf::from(path!("/root/dir"))], - app_state.clone(), - workspace::OpenOptions::default(), - cx, - ) - }) - .await - .unwrap(); - assert_eq!(cx.update(|cx| cx.windows().len()), 1); - - cx.update(|cx| { - open_paths( - &[PathBuf::from(path!("/root/a"))], - app_state.clone(), - workspace::OpenOptions { - open_new_workspace: Some(false), - ..Default::default() - }, - cx, - ) - }) - .await - .unwrap(); - assert_eq!(cx.update(|cx| cx.windows().len()), 1); - - cx.update(|cx| { - open_paths( - &[PathBuf::from(path!("/root/dir/c"))], - app_state.clone(), - workspace::OpenOptions { - open_new_workspace: Some(true), - ..Default::default() - }, - cx, - ) - }) - .await - .unwrap(); - assert_eq!(cx.update(|cx| cx.windows().len()), 2); - } - - #[gpui::test] - async fn test_open_file_in_many_spaces(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - path!("/root"), - json!({"dir1": {"a": "b"}, "dir2": {"c": "d"}}), - ) - .await; - - cx.update(|cx| { - open_paths( - &[PathBuf::from(path!("/root/dir1/a"))], - app_state.clone(), - workspace::OpenOptions::default(), - cx, - ) - }) - .await - .unwrap(); - assert_eq!(cx.update(|cx| cx.windows().len()), 1); - let window1 = cx.update(|cx| cx.active_window().unwrap()); - - cx.update(|cx| { - open_paths( - &[PathBuf::from(path!("/root/dir2/c"))], - app_state.clone(), - workspace::OpenOptions::default(), - cx, - ) - }) - .await - .unwrap(); - assert_eq!(cx.update(|cx| cx.windows().len()), 1); - - cx.update(|cx| { - open_paths( - &[PathBuf::from(path!("/root/dir2"))], - app_state.clone(), - workspace::OpenOptions::default(), - cx, - ) - }) - .await - .unwrap(); - assert_eq!(cx.update(|cx| cx.windows().len()), 2); - let window2 = cx.update(|cx| cx.active_window().unwrap()); - assert!(window1 != window2); - cx.update_window(window1, |_, window, _| window.activate_window()) - .unwrap(); - - cx.update(|cx| { - open_paths( - &[PathBuf::from(path!("/root/dir2/c"))], - app_state.clone(), - workspace::OpenOptions::default(), - cx, - ) - }) - .await - .unwrap(); - assert_eq!(cx.update(|cx| cx.windows().len()), 2); - // should have opened in window2 because that has dir2 visibly open (window1 has it open, but not in the project panel) - assert!(cx.update(|cx| cx.active_window().unwrap()) == window2); - } - - #[gpui::test] - async fn test_window_edit_state_restoring_disabled(cx: &mut TestAppContext) { - let executor = cx.executor(); - let app_state = init_test(cx); - - cx.update(|cx| { - SettingsStore::update_global(cx, |store, cx| { - store.update_user_settings(cx, |settings| { - settings - .session - .get_or_insert_default() - .restore_unsaved_buffers = Some(false) - }); - }); - }); - - app_state - .fs - .as_fake() - .insert_tree(path!("/root"), json!({"a": "hey"})) - .await; - - cx.update(|cx| { - open_paths( - &[PathBuf::from(path!("/root/a"))], - app_state.clone(), - workspace::OpenOptions::default(), - cx, - ) - }) - .await - .unwrap(); - assert_eq!(cx.update(|cx| cx.windows().len()), 1); - - // When opening the workspace, the window is not in a edited state. - let window = cx.update(|cx| cx.windows()[0].downcast::().unwrap()); - - let window_is_edited = |window: WindowHandle, cx: &mut TestAppContext| { - cx.update(|cx| window.read(cx).unwrap().is_edited()) - }; - let pane = window - .read_with(cx, |workspace, _| workspace.active_pane().clone()) - .unwrap(); - let editor = window - .read_with(cx, |workspace, cx| { - workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap() - }) - .unwrap(); - - assert!(!window_is_edited(window, cx)); - - // Editing a buffer marks the window as edited. - window - .update(cx, |_, window, cx| { - editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx)); - }) - .unwrap(); - - assert!(window_is_edited(window, cx)); - - // Undoing the edit restores the window's edited state. - window - .update(cx, |_, window, cx| { - editor.update(cx, |editor, cx| { - editor.undo(&Default::default(), window, cx) - }); - }) - .unwrap(); - assert!(!window_is_edited(window, cx)); - - // Redoing the edit marks the window as edited again. - window - .update(cx, |_, window, cx| { - editor.update(cx, |editor, cx| { - editor.redo(&Default::default(), window, cx) - }); - }) - .unwrap(); - assert!(window_is_edited(window, cx)); - let weak = editor.downgrade(); - - // Closing the item restores the window's edited state. - let close = window - .update(cx, |_, window, cx| { - pane.update(cx, |pane, cx| { - drop(editor); - pane.close_active_item(&Default::default(), window, cx) - }) - }) - .unwrap(); - executor.run_until_parked(); - - cx.simulate_prompt_answer("Don't Save"); - close.await.unwrap(); - - // Advance the clock to ensure that the item has been serialized and dropped from the queue - cx.executor().advance_clock(Duration::from_secs(1)); - - weak.assert_released(); - assert!(!window_is_edited(window, cx)); - // Opening the buffer again doesn't impact the window's edited state. - cx.update(|cx| { - open_paths( - &[PathBuf::from(path!("/root/a"))], - app_state, - workspace::OpenOptions::default(), - cx, - ) - }) - .await - .unwrap(); - executor.run_until_parked(); - - window - .update(cx, |workspace, _, cx| { - let editor = workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap(); - - editor.update(cx, |editor, cx| { - assert_eq!(editor.text(cx), "hey"); - }); - }) - .unwrap(); - - let editor = window - .read_with(cx, |workspace, cx| { - workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap() - }) - .unwrap(); - assert!(!window_is_edited(window, cx)); - - // Editing the buffer marks the window as edited. - window - .update(cx, |_, window, cx| { - editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx)); - }) - .unwrap(); - executor.run_until_parked(); - assert!(window_is_edited(window, cx)); - - // Ensure closing the window via the mouse gets preempted due to the - // buffer having unsaved changes. - assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close()); - executor.run_until_parked(); - assert_eq!(cx.update(|cx| cx.windows().len()), 1); - - // The window is successfully closed after the user dismisses the prompt. - cx.simulate_prompt_answer("Don't Save"); - executor.run_until_parked(); - assert_eq!(cx.update(|cx| cx.windows().len()), 0); - } - - #[gpui::test] - async fn test_window_edit_state_restoring_enabled(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree(path!("/root"), json!({"a": "hey"})) - .await; - - cx.update(|cx| { - open_paths( - &[PathBuf::from(path!("/root/a"))], - app_state.clone(), - workspace::OpenOptions::default(), - cx, - ) - }) - .await - .unwrap(); - - assert_eq!(cx.update(|cx| cx.windows().len()), 1); - - // When opening the workspace, the window is not in a edited state. - let window = cx.update(|cx| cx.windows()[0].downcast::().unwrap()); - - let window_is_edited = |window: WindowHandle, cx: &mut TestAppContext| { - cx.update(|cx| window.read(cx).unwrap().is_edited()) - }; - - let editor = window - .read_with(cx, |workspace, cx| { - workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap() - }) - .unwrap(); - - assert!(!window_is_edited(window, cx)); - - // Editing a buffer marks the window as edited. - window - .update(cx, |_, window, cx| { - editor.update(cx, |editor, cx| editor.insert("EDIT", window, cx)); - }) - .unwrap(); - - assert!(window_is_edited(window, cx)); - cx.run_until_parked(); - - // Advance the clock to make sure the workspace is serialized - cx.executor().advance_clock(Duration::from_secs(1)); - - // When closing the window, no prompt shows up and the window is closed. - // buffer having unsaved changes. - assert!(!VisualTestContext::from_window(window.into(), cx).simulate_close()); - cx.run_until_parked(); - assert_eq!(cx.update(|cx| cx.windows().len()), 0); - - // When we now reopen the window, the edited state and the edited buffer are back - cx.update(|cx| { - open_paths( - &[PathBuf::from(path!("/root/a"))], - app_state.clone(), - workspace::OpenOptions::default(), - cx, - ) - }) - .await - .unwrap(); - - assert_eq!(cx.update(|cx| cx.windows().len()), 1); - assert!(cx.update(|cx| cx.active_window().is_some())); - - cx.run_until_parked(); - - // When opening the workspace, the window is not in a edited state. - let window = cx.update(|cx| cx.active_window().unwrap().downcast::().unwrap()); - assert!(window_is_edited(window, cx)); - - window - .update(cx, |workspace, _, cx| { - let editor = workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap(); - editor.update(cx, |editor, cx| { - assert_eq!(editor.text(cx), "EDIThey"); - assert!(editor.is_dirty(cx)); - }); - - editor - }) - .unwrap(); - } - - #[gpui::test] - async fn test_new_empty_workspace(cx: &mut TestAppContext) { - let app_state = init_test(cx); - cx.update(|cx| { - open_new( - Default::default(), - app_state.clone(), - cx, - |workspace, window, cx| { - Editor::new_file(workspace, &Default::default(), window, cx) - }, - ) - }) - .await - .unwrap(); - cx.run_until_parked(); - - let workspace = cx - .update(|cx| cx.windows().first().unwrap().downcast::()) - .unwrap(); - - let editor = workspace - .update(cx, |workspace, _, cx| { - let editor = workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap(); - editor.update(cx, |editor, cx| { - assert!(editor.text(cx).is_empty()); - assert!(!editor.is_dirty(cx)); - }); - - editor - }) - .unwrap(); - - let save_task = workspace - .update(cx, |workspace, window, cx| { - workspace.save_active_item(SaveIntent::Save, window, cx) - }) - .unwrap(); - app_state.fs.create_dir(Path::new("/root")).await.unwrap(); - cx.background_executor.run_until_parked(); - cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name"))); - save_task.await.unwrap(); - workspace - .update(cx, |_, _, cx| { - editor.update(cx, |editor, cx| { - assert!(!editor.is_dirty(cx)); - assert_eq!(editor.title(cx), "the-new-name"); - }); - }) - .unwrap(); - } - - #[gpui::test] - async fn test_open_entry(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - path!("/root"), - json!({ - "a": { - "file1": "contents 1", - "file2": "contents 2", - "file3": "contents 3", - }, - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - project.update(cx, |project, _cx| project.languages().add(markdown_lang())); - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let workspace = window.root(cx).unwrap(); - - let entries = cx.read(|cx| workspace.file_project_paths(cx)); - let file1 = entries[0].clone(); - let file2 = entries[1].clone(); - let file3 = entries[2].clone(); - - // Open the first entry - let entry_1 = window - .update(cx, |w, window, cx| { - w.open_path(file1.clone(), None, true, window, cx) - }) - .unwrap() - .await - .unwrap(); - cx.read(|cx| { - let pane = workspace.read(cx).active_pane().read(cx); - assert_eq!( - pane.active_item().unwrap().project_path(cx), - Some(file1.clone()) - ); - assert_eq!(pane.items_len(), 1); - }); - - // Open the second entry - window - .update(cx, |w, window, cx| { - w.open_path(file2.clone(), None, true, window, cx) - }) - .unwrap() - .await - .unwrap(); - cx.read(|cx| { - let pane = workspace.read(cx).active_pane().read(cx); - assert_eq!( - pane.active_item().unwrap().project_path(cx), - Some(file2.clone()) - ); - assert_eq!(pane.items_len(), 2); - }); - - // Open the first entry again. The existing pane item is activated. - let entry_1b = window - .update(cx, |w, window, cx| { - w.open_path(file1.clone(), None, true, window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(entry_1.item_id(), entry_1b.item_id()); - - cx.read(|cx| { - let pane = workspace.read(cx).active_pane().read(cx); - assert_eq!( - pane.active_item().unwrap().project_path(cx), - Some(file1.clone()) - ); - assert_eq!(pane.items_len(), 2); - }); - - // Split the pane with the first entry, then open the second entry again. - window - .update(cx, |w, window, cx| { - w.split_and_clone(w.active_pane().clone(), SplitDirection::Right, window, cx) - }) - .unwrap() - .await - .unwrap(); - window - .update(cx, |w, window, cx| { - w.open_path(file2.clone(), None, true, window, cx) - }) - .unwrap() - .await - .unwrap(); - - window - .read_with(cx, |w, cx| { - assert_eq!( - w.active_pane() - .read(cx) - .active_item() - .unwrap() - .project_path(cx), - Some(file2.clone()) - ); - }) - .unwrap(); - - // Open the third entry twice concurrently. Only one pane item is added. - let (t1, t2) = window - .update(cx, |w, window, cx| { - ( - w.open_path(file3.clone(), None, true, window, cx), - w.open_path(file3.clone(), None, true, window, cx), - ) - }) - .unwrap(); - t1.await.unwrap(); - t2.await.unwrap(); - cx.read(|cx| { - let pane = workspace.read(cx).active_pane().read(cx); - assert_eq!( - pane.active_item().unwrap().project_path(cx), - Some(file3.clone()) - ); - let pane_entries = pane - .items() - .map(|i| i.project_path(cx).unwrap()) - .collect::>(); - assert_eq!(pane_entries, &[file1, file2, file3]); - }); - } - - #[gpui::test] - async fn test_open_paths(cx: &mut TestAppContext) { - let app_state = init_test(cx); - - app_state - .fs - .as_fake() - .insert_tree( - path!("/"), - json!({ - "dir1": { - "a.txt": "" - }, - "dir2": { - "b.txt": "" - }, - "dir3": { - "c.txt": "" - }, - "d.txt": "" - }), - ) - .await; - - cx.update(|cx| { - open_paths( - &[PathBuf::from(path!("/dir1/"))], - app_state, - workspace::OpenOptions::default(), - cx, - ) - }) - .await - .unwrap(); - cx.run_until_parked(); - assert_eq!(cx.update(|cx| cx.windows().len()), 1); - let window = cx.update(|cx| cx.windows()[0].downcast::().unwrap()); - let workspace = window.root(cx).unwrap(); - - #[track_caller] - fn assert_project_panel_selection( - workspace: &Workspace, - expected_worktree_path: &Path, - expected_entry_path: &RelPath, - cx: &App, - ) { - let project_panel = [ - workspace.left_dock().read(cx).panel::(), - workspace.right_dock().read(cx).panel::(), - workspace.bottom_dock().read(cx).panel::(), - ] - .into_iter() - .find_map(std::convert::identity) - .expect("found no project panels") - .read(cx); - let (selected_worktree, selected_entry) = project_panel - .selected_entry(cx) - .expect("project panel should have a selected entry"); - assert_eq!( - selected_worktree.abs_path().as_ref(), - expected_worktree_path, - "Unexpected project panel selected worktree path" - ); - assert_eq!( - selected_entry.path.as_ref(), - expected_entry_path, - "Unexpected project panel selected entry path" - ); - } - - // Open a file within an existing worktree. - window - .update(cx, |workspace, window, cx| { - workspace.open_paths( - vec![path!("/dir1/a.txt").into()], - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - None, - window, - cx, - ) - }) - .unwrap() - .await; - cx.run_until_parked(); - cx.read(|cx| { - let workspace = workspace.read(cx); - assert_project_panel_selection( - workspace, - Path::new(path!("/dir1")), - rel_path("a.txt"), - cx, - ); - assert_eq!( - workspace - .active_pane() - .read(cx) - .active_item() - .unwrap() - .act_as::(cx) - .unwrap() - .read(cx) - .title(cx), - "a.txt" - ); - }); - - // Open a file outside of any existing worktree. - window - .update(cx, |workspace, window, cx| { - workspace.open_paths( - vec![path!("/dir2/b.txt").into()], - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - None, - window, - cx, - ) - }) - .unwrap() - .await; - cx.run_until_parked(); - cx.read(|cx| { - let workspace = workspace.read(cx); - assert_project_panel_selection( - workspace, - Path::new(path!("/dir2/b.txt")), - rel_path(""), - cx, - ); - let worktree_roots = workspace - .worktrees(cx) - .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) - .collect::>(); - assert_eq!( - worktree_roots, - vec![path!("/dir1"), path!("/dir2/b.txt")] - .into_iter() - .map(Path::new) - .collect(), - ); - assert_eq!( - workspace - .active_pane() - .read(cx) - .active_item() - .unwrap() - .act_as::(cx) - .unwrap() - .read(cx) - .title(cx), - "b.txt" - ); - }); - - // Ensure opening a directory and one of its children only adds one worktree. - window - .update(cx, |workspace, window, cx| { - workspace.open_paths( - vec![path!("/dir3").into(), path!("/dir3/c.txt").into()], - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - None, - window, - cx, - ) - }) - .unwrap() - .await; - cx.run_until_parked(); - cx.read(|cx| { - let workspace = workspace.read(cx); - assert_project_panel_selection( - workspace, - Path::new(path!("/dir3")), - rel_path("c.txt"), - cx, - ); - let worktree_roots = workspace - .worktrees(cx) - .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) - .collect::>(); - assert_eq!( - worktree_roots, - vec![path!("/dir1"), path!("/dir2/b.txt"), path!("/dir3")] - .into_iter() - .map(Path::new) - .collect(), - ); - assert_eq!( - workspace - .active_pane() - .read(cx) - .active_item() - .unwrap() - .act_as::(cx) - .unwrap() - .read(cx) - .title(cx), - "c.txt" - ); - }); - - // Ensure opening invisibly a file outside an existing worktree adds a new, invisible worktree. - window - .update(cx, |workspace, window, cx| { - workspace.open_paths( - vec![path!("/d.txt").into()], - OpenOptions { - visible: Some(OpenVisible::None), - ..Default::default() - }, - None, - window, - cx, - ) - }) - .unwrap() - .await; - cx.run_until_parked(); - cx.read(|cx| { - let workspace = workspace.read(cx); - assert_project_panel_selection(workspace, Path::new(path!("/d.txt")), rel_path(""), cx); - let worktree_roots = workspace - .worktrees(cx) - .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) - .collect::>(); - assert_eq!( - worktree_roots, - vec![ - path!("/dir1"), - path!("/dir2/b.txt"), - path!("/dir3"), - path!("/d.txt") - ] - .into_iter() - .map(Path::new) - .collect(), - ); - - let visible_worktree_roots = workspace - .visible_worktrees(cx) - .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) - .collect::>(); - assert_eq!( - visible_worktree_roots, - vec![path!("/dir1"), path!("/dir2/b.txt"), path!("/dir3")] - .into_iter() - .map(Path::new) - .collect(), - ); - - assert_eq!( - workspace - .active_pane() - .read(cx) - .active_item() - .unwrap() - .act_as::(cx) - .unwrap() - .read(cx) - .title(cx), - "d.txt" - ); - }); - } - - #[gpui::test] - async fn test_opening_excluded_paths(cx: &mut TestAppContext) { - let app_state = init_test(cx); - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |project_settings| { - project_settings.project.worktree.file_scan_exclusions = - Some(vec!["excluded_dir".to_string(), "**/.git".to_string()]); - }); - }); - }); - app_state - .fs - .as_fake() - .insert_tree( - path!("/root"), - json!({ - ".gitignore": "ignored_dir\n", - ".git": { - "HEAD": "ref: refs/heads/main", - }, - "regular_dir": { - "file": "regular file contents", - }, - "ignored_dir": { - "ignored_subdir": { - "file": "ignored subfile contents", - }, - "file": "ignored file contents", - }, - "excluded_dir": { - "file": "excluded file contents", - "ignored_subdir": { - "file": "ignored subfile contents", - }, - }, - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - project.update(cx, |project, _cx| project.languages().add(markdown_lang())); - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let workspace = window.root(cx).unwrap(); - - let initial_entries = cx.read(|cx| workspace.file_project_paths(cx)); - let paths_to_open = [ - PathBuf::from(path!("/root/excluded_dir/file")), - PathBuf::from(path!("/root/.git/HEAD")), - PathBuf::from(path!("/root/excluded_dir/ignored_subdir")), - ]; - let (opened_workspace, new_items) = cx - .update(|cx| { - workspace::open_paths( - &paths_to_open, - app_state, - workspace::OpenOptions::default(), - cx, - ) - }) - .await - .unwrap(); - - assert_eq!( - opened_workspace.root(cx).unwrap().entity_id(), - workspace.entity_id(), - "Excluded files in subfolders of a workspace root should be opened in the workspace" - ); - let mut opened_paths = cx.read(|cx| { - assert_eq!( - new_items.len(), - paths_to_open.len(), - "Expect to get the same number of opened items as submitted paths to open" - ); - new_items - .iter() - .zip(paths_to_open.iter()) - .map(|(i, path)| { - match i { - Some(Ok(i)) => Some(i.project_path(cx).map(|p| p.path)), - Some(Err(e)) => panic!("Excluded file {path:?} failed to open: {e:?}"), - None => None, - } - .flatten() - }) - .collect::>() - }); - opened_paths.sort(); - assert_eq!( - opened_paths, - vec![ - None, - Some(rel_path(".git/HEAD").into()), - Some(rel_path("excluded_dir/file").into()), - ], - "Excluded files should get opened, excluded dir should not get opened" - ); - - let entries = cx.read(|cx| workspace.file_project_paths(cx)); - assert_eq!( - initial_entries, entries, - "Workspace entries should not change after opening excluded files and directories paths" - ); - - cx.read(|cx| { - let pane = workspace.read(cx).active_pane().read(cx); - let mut opened_buffer_paths = pane - .items() - .map(|i| { - i.project_path(cx) - .expect("all excluded files that got open should have a path") - .path - }) - .collect::>(); - opened_buffer_paths.sort(); - assert_eq!( - opened_buffer_paths, - vec![rel_path(".git/HEAD").into(), rel_path("excluded_dir/file").into()], - "Despite not being present in the worktrees, buffers for excluded files are opened and added to the pane" - ); - }); - } - - #[gpui::test] - async fn test_save_conflicting_item(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree(path!("/root"), json!({ "a.txt": "" })) - .await; - - let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - project.update(cx, |project, _cx| project.languages().add(markdown_lang())); - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let workspace = window.root(cx).unwrap(); - - // Open a file within an existing worktree. - window - .update(cx, |workspace, window, cx| { - workspace.open_paths( - vec![PathBuf::from(path!("/root/a.txt"))], - OpenOptions { - visible: Some(OpenVisible::All), - ..Default::default() - }, - None, - window, - cx, - ) - }) - .unwrap() - .await; - let editor = cx.read(|cx| { - let pane = workspace.read(cx).active_pane().read(cx); - let item = pane.active_item().unwrap(); - item.downcast::().unwrap() - }); - - window - .update(cx, |_, window, cx| { - editor.update(cx, |editor, cx| editor.handle_input("x", window, cx)); - }) - .unwrap(); - - app_state - .fs - .as_fake() - .insert_file(path!("/root/a.txt"), b"changed".to_vec()) - .await; - - cx.run_until_parked(); - cx.read(|cx| assert!(editor.is_dirty(cx))); - cx.read(|cx| assert!(editor.has_conflict(cx))); - - let save_task = window - .update(cx, |workspace, window, cx| { - workspace.save_active_item(SaveIntent::Save, window, cx) - }) - .unwrap(); - cx.background_executor.run_until_parked(); - cx.simulate_prompt_answer("Overwrite"); - save_task.await.unwrap(); - window - .update(cx, |_, _, cx| { - editor.update(cx, |editor, cx| { - assert!(!editor.is_dirty(cx)); - assert!(!editor.has_conflict(cx)); - }); - }) - .unwrap(); - } - - #[gpui::test] - async fn test_open_and_save_new_file(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .create_dir(Path::new(path!("/root"))) - .await - .unwrap(); - - let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - project.update(cx, |project, _| { - project.languages().add(markdown_lang()); - project.languages().add(rust_lang()); - }); - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let worktree = cx.update(|cx| window.read(cx).unwrap().worktrees(cx).next().unwrap()); - - // Create a new untitled buffer - cx.dispatch_action(window.into(), NewFile); - let editor = window - .read_with(cx, |workspace, cx| { - workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap() - }) - .unwrap(); - - window - .update(cx, |_, window, cx| { - editor.update(cx, |editor, cx| { - assert!(!editor.is_dirty(cx)); - assert_eq!(editor.title(cx), "untitled"); - assert!(Arc::ptr_eq( - &editor - .buffer() - .read(cx) - .language_at(MultiBufferOffset(0), cx) - .unwrap(), - &languages::PLAIN_TEXT - )); - editor.handle_input("hi", window, cx); - assert!(editor.is_dirty(cx)); - }); - }) - .unwrap(); - - // Save the buffer. This prompts for a filename. - let save_task = window - .update(cx, |workspace, window, cx| { - workspace.save_active_item(SaveIntent::Save, window, cx) - }) - .unwrap(); - cx.background_executor.run_until_parked(); - cx.simulate_new_path_selection(|parent_dir| { - assert_eq!(parent_dir, Path::new(path!("/root"))); - Some(parent_dir.join("the-new-name.rs")) - }); - cx.read(|cx| { - assert!(editor.is_dirty(cx)); - assert_eq!(editor.read(cx).title(cx), "hi"); - }); - - // When the save completes, the buffer's title is updated and the language is assigned based - // on the path. - save_task.await.unwrap(); - window - .update(cx, |_, _, cx| { - editor.update(cx, |editor, cx| { - assert!(!editor.is_dirty(cx)); - assert_eq!(editor.title(cx), "the-new-name.rs"); - assert_eq!( - editor - .buffer() - .read(cx) - .language_at(MultiBufferOffset(0), cx) - .unwrap() - .name(), - "Rust".into() - ); - }); - }) - .unwrap(); - - // Edit the file and save it again. This time, there is no filename prompt. - window - .update(cx, |_, window, cx| { - editor.update(cx, |editor, cx| { - editor.handle_input(" there", window, cx); - assert!(editor.is_dirty(cx)); - }); - }) - .unwrap(); - - let save_task = window - .update(cx, |workspace, window, cx| { - workspace.save_active_item(SaveIntent::Save, window, cx) - }) - .unwrap(); - save_task.await.unwrap(); - - assert!(!cx.did_prompt_for_new_path()); - window - .update(cx, |_, _, cx| { - editor.update(cx, |editor, cx| { - assert!(!editor.is_dirty(cx)); - assert_eq!(editor.title(cx), "the-new-name.rs") - }); - }) - .unwrap(); - - // Open the same newly-created file in another pane item. The new editor should reuse - // the same buffer. - cx.dispatch_action(window.into(), NewFile); - window - .update(cx, |workspace, window, cx| { - workspace.split_and_clone( - workspace.active_pane().clone(), - SplitDirection::Right, - window, - cx, - ) - }) - .unwrap() - .await - .unwrap(); - window - .update(cx, |workspace, window, cx| { - workspace.open_path( - (worktree.read(cx).id(), rel_path("the-new-name.rs")), - None, - true, - window, - cx, - ) - }) - .unwrap() - .await - .unwrap(); - let editor2 = window - .update(cx, |workspace, _, cx| { - workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap() - }) - .unwrap(); - cx.read(|cx| { - assert_eq!( - editor2.read(cx).buffer().read(cx).as_singleton().unwrap(), - editor.read(cx).buffer().read(cx).as_singleton().unwrap() - ); - }) - } - - #[gpui::test] - async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state.fs.create_dir(Path::new("/root")).await.unwrap(); - - let project = Project::test(app_state.fs.clone(), [], cx).await; - project.update(cx, |project, _| { - project.languages().add(language::rust_lang()); - project.languages().add(language::markdown_lang()); - }); - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - - // Create a new untitled buffer - cx.dispatch_action(window.into(), NewFile); - let editor = window - .read_with(cx, |workspace, cx| { - workspace - .active_item(cx) - .unwrap() - .downcast::() - .unwrap() - }) - .unwrap(); - window - .update(cx, |_, window, cx| { - editor.update(cx, |editor, cx| { - assert!(Arc::ptr_eq( - &editor - .buffer() - .read(cx) - .language_at(MultiBufferOffset(0), cx) - .unwrap(), - &languages::PLAIN_TEXT - )); - editor.handle_input("hi", window, cx); - assert!(editor.is_dirty(cx)); - }); - }) - .unwrap(); - - // Save the buffer. This prompts for a filename. - let save_task = window - .update(cx, |workspace, window, cx| { - workspace.save_active_item(SaveIntent::Save, window, cx) - }) - .unwrap(); - cx.background_executor.run_until_parked(); - cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name.rs"))); - save_task.await.unwrap(); - // The buffer is not dirty anymore and the language is assigned based on the path. - window - .update(cx, |_, _, cx| { - editor.update(cx, |editor, cx| { - assert!(!editor.is_dirty(cx)); - assert_eq!( - editor - .buffer() - .read(cx) - .language_at(MultiBufferOffset(0), cx) - .unwrap() - .name(), - "Rust".into() - ) - }); - }) - .unwrap(); - } - - #[gpui::test] - async fn test_pane_actions(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - path!("/root"), - json!({ - "a": { - "file1": "contents 1", - "file2": "contents 2", - "file3": "contents 3", - }, - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - project.update(cx, |project, _cx| project.languages().add(markdown_lang())); - let window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let workspace = window.root(cx).unwrap(); - - let entries = cx.read(|cx| workspace.file_project_paths(cx)); - let file1 = entries[0].clone(); - - let pane_1 = cx.read(|cx| workspace.read(cx).active_pane().clone()); - - window - .update(cx, |w, window, cx| { - w.open_path(file1.clone(), None, true, window, cx) - }) - .unwrap() - .await - .unwrap(); - - let (editor_1, buffer) = window - .update(cx, |_, window, cx| { - pane_1.update(cx, |pane_1, cx| { - let editor = pane_1.active_item().unwrap().downcast::().unwrap(); - assert_eq!(editor.project_path(cx), Some(file1.clone())); - let buffer = editor.update(cx, |editor, cx| { - editor.insert("dirt", window, cx); - editor.buffer().downgrade() - }); - (editor.downgrade(), buffer) - }) - }) - .unwrap(); - - cx.dispatch_action(window.into(), pane::SplitRight); - let editor_2 = cx.update(|cx| { - let pane_2 = workspace.read(cx).active_pane().clone(); - assert_ne!(pane_1, pane_2); - - let pane2_item = pane_2.read(cx).active_item().unwrap(); - assert_eq!(pane2_item.project_path(cx), Some(file1.clone())); - - pane2_item.downcast::().unwrap().downgrade() - }); - cx.dispatch_action( - window.into(), - workspace::CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - ); - - cx.background_executor.run_until_parked(); - window - .read_with(cx, |workspace, _| { - assert_eq!(workspace.panes().len(), 1); - assert_eq!(workspace.active_pane(), &pane_1); - }) - .unwrap(); - - cx.dispatch_action( - window.into(), - workspace::CloseActiveItem { - save_intent: None, - close_pinned: false, - }, - ); - cx.background_executor.run_until_parked(); - cx.simulate_prompt_answer("Don't Save"); - cx.background_executor.run_until_parked(); - - window - .update(cx, |workspace, _, cx| { - assert_eq!(workspace.panes().len(), 1); - assert!(workspace.active_item(cx).is_none()); - }) - .unwrap(); - - cx.background_executor - .advance_clock(SERIALIZATION_THROTTLE_TIME); - cx.update(|_| {}); - editor_1.assert_released(); - editor_2.assert_released(); - buffer.assert_released(); - } - - #[gpui::test] - async fn test_navigation(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - path!("/root"), - json!({ - "a": { - "file1": "contents 1\n".repeat(20), - "file2": "contents 2\n".repeat(20), - "file3": "contents 3\n".repeat(20), - }, - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - project.update(cx, |project, _cx| project.languages().add(markdown_lang())); - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - let pane = workspace - .read_with(cx, |workspace, _| workspace.active_pane().clone()) - .unwrap(); - - let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx)); - let file1 = entries[0].clone(); - let file2 = entries[1].clone(); - let file3 = entries[2].clone(); - - let editor1 = workspace - .update(cx, |w, window, cx| { - w.open_path(file1.clone(), None, true, window, cx) - }) - .unwrap() - .await - .unwrap() - .downcast::() - .unwrap(); - workspace - .update(cx, |_, window, cx| { - editor1.update(cx, |editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.select_display_ranges([DisplayPoint::new(DisplayRow(10), 0) - ..DisplayPoint::new(DisplayRow(10), 0)]) - }); - }); - }) - .unwrap(); - - let editor2 = workspace - .update(cx, |w, window, cx| { - w.open_path(file2.clone(), None, true, window, cx) - }) - .unwrap() - .await - .unwrap() - .downcast::() - .unwrap(); - let editor3 = workspace - .update(cx, |w, window, cx| { - w.open_path(file3.clone(), None, true, window, cx) - }) - .unwrap() - .await - .unwrap() - .downcast::() - .unwrap(); - - workspace - .update(cx, |_, window, cx| { - editor3.update(cx, |editor, cx| { - editor.change_selections(Default::default(), window, cx, |s| { - s.select_display_ranges([DisplayPoint::new(DisplayRow(12), 0) - ..DisplayPoint::new(DisplayRow(12), 0)]) - }); - editor.newline(&Default::default(), window, cx); - editor.newline(&Default::default(), window, cx); - editor.move_down(&Default::default(), window, cx); - editor.move_down(&Default::default(), window, cx); - editor.save( - SaveOptions { - format: true, - autosave: false, - }, - project.clone(), - window, - cx, - ) - }) - }) - .unwrap() - .await - .unwrap(); - workspace - .update(cx, |_, window, cx| { - editor3.update(cx, |editor, cx| { - editor.set_scroll_position(point(0., 12.5), window, cx) - }); - }) - .unwrap(); - assert_eq!( - active_location(&workspace, cx), - (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5) - ); - - workspace - .update(cx, |w, window, cx| { - w.go_back(w.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!( - active_location(&workspace, cx), - (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.) - ); - - workspace - .update(cx, |w, window, cx| { - w.go_back(w.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!( - active_location(&workspace, cx), - (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.) - ); - - workspace - .update(cx, |w, window, cx| { - w.go_back(w.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!( - active_location(&workspace, cx), - (file1.clone(), DisplayPoint::new(DisplayRow(10), 0), 0.) - ); - - workspace - .update(cx, |w, window, cx| { - w.go_back(w.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!( - active_location(&workspace, cx), - (file1.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.) - ); - - // Go back one more time and ensure we don't navigate past the first item in the history. - workspace - .update(cx, |w, window, cx| { - w.go_back(w.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!( - active_location(&workspace, cx), - (file1.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.) - ); - - workspace - .update(cx, |w, window, cx| { - w.go_forward(w.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!( - active_location(&workspace, cx), - (file1.clone(), DisplayPoint::new(DisplayRow(10), 0), 0.) - ); - - workspace - .update(cx, |w, window, cx| { - w.go_forward(w.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!( - active_location(&workspace, cx), - (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.) - ); - - // Go forward to an item that has been closed, ensuring it gets re-opened at the same - // location. - workspace - .update(cx, |_, window, cx| { - pane.update(cx, |pane, cx| { - let editor3_id = editor3.entity_id(); - drop(editor3); - pane.close_item_by_id(editor3_id, SaveIntent::Close, window, cx) - }) - }) - .unwrap() - .await - .unwrap(); - workspace - .update(cx, |w, window, cx| { - w.go_forward(w.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!( - active_location(&workspace, cx), - (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.) - ); - - workspace - .update(cx, |w, window, cx| { - w.go_forward(w.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!( - active_location(&workspace, cx), - (file3.clone(), DisplayPoint::new(DisplayRow(16), 0), 12.5) - ); - - workspace - .update(cx, |w, window, cx| { - w.go_back(w.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!( - active_location(&workspace, cx), - (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.) - ); - - // Go back to an item that has been closed and removed from disk - workspace - .update(cx, |_, window, cx| { - pane.update(cx, |pane, cx| { - let editor2_id = editor2.entity_id(); - drop(editor2); - pane.close_item_by_id(editor2_id, SaveIntent::Close, window, cx) - }) - }) - .unwrap() - .await - .unwrap(); - app_state - .fs - .remove_file(Path::new(path!("/root/a/file2")), Default::default()) - .await - .unwrap(); - cx.background_executor.run_until_parked(); - - workspace - .update(cx, |w, window, cx| { - w.go_back(w.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!( - active_location(&workspace, cx), - (file2.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.) - ); - workspace - .update(cx, |w, window, cx| { - w.go_forward(w.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!( - active_location(&workspace, cx), - (file3.clone(), DisplayPoint::new(DisplayRow(0), 0), 0.) - ); - - // Modify file to collapse multiple nav history entries into the same location. - // Ensure we don't visit the same location twice when navigating. - workspace - .update(cx, |_, window, cx| { - editor1.update(cx, |editor, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_display_ranges([DisplayPoint::new(DisplayRow(15), 0) - ..DisplayPoint::new(DisplayRow(15), 0)]) - }) - }); - }) - .unwrap(); - for _ in 0..5 { - workspace - .update(cx, |_, window, cx| { - editor1.update(cx, |editor, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_display_ranges([DisplayPoint::new(DisplayRow(3), 0) - ..DisplayPoint::new(DisplayRow(3), 0)]) - }); - }); - }) - .unwrap(); - - workspace - .update(cx, |_, window, cx| { - editor1.update(cx, |editor, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_display_ranges([DisplayPoint::new(DisplayRow(13), 0) - ..DisplayPoint::new(DisplayRow(13), 0)]) - }) - }); - }) - .unwrap(); - } - workspace - .update(cx, |_, window, cx| { - editor1.update(cx, |editor, cx| { - editor.transact(window, cx, |editor, window, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_display_ranges([DisplayPoint::new(DisplayRow(2), 0) - ..DisplayPoint::new(DisplayRow(14), 0)]) - }); - editor.insert("", window, cx); - }) - }); - }) - .unwrap(); - - workspace - .update(cx, |_, window, cx| { - editor1.update(cx, |editor, cx| { - editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| { - s.select_display_ranges([DisplayPoint::new(DisplayRow(1), 0) - ..DisplayPoint::new(DisplayRow(1), 0)]) - }) - }); - }) - .unwrap(); - workspace - .update(cx, |w, window, cx| { - w.go_back(w.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!( - active_location(&workspace, cx), - (file1.clone(), DisplayPoint::new(DisplayRow(2), 0), 0.) - ); - workspace - .update(cx, |w, window, cx| { - w.go_back(w.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!( - active_location(&workspace, cx), - (file1.clone(), DisplayPoint::new(DisplayRow(3), 0), 0.) - ); - - fn active_location( - workspace: &WindowHandle, - cx: &mut TestAppContext, - ) -> (ProjectPath, DisplayPoint, f64) { - workspace - .update(cx, |workspace, _, cx| { - let item = workspace.active_item(cx).unwrap(); - let editor = item.downcast::().unwrap(); - let (selections, scroll_position) = editor.update(cx, |editor, cx| { - ( - editor - .selections - .display_ranges(&editor.display_snapshot(cx)), - editor.scroll_position(cx), - ) - }); - ( - item.project_path(cx).unwrap(), - selections[0].start, - scroll_position.y, - ) - }) - .unwrap() - } - } - - #[gpui::test] - async fn test_reopening_closed_items(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - path!("/root"), - json!({ - "a": { - "file1": "", - "file2": "", - "file3": "", - "file4": "", - }, - }), - ) - .await; - - let project = Project::test(app_state.fs.clone(), [path!("/root").as_ref()], cx).await; - project.update(cx, |project, _cx| project.languages().add(markdown_lang())); - let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - let pane = workspace - .read_with(cx, |workspace, _| workspace.active_pane().clone()) - .unwrap(); - - let entries = cx.update(|cx| workspace.root(cx).unwrap().file_project_paths(cx)); - let file1 = entries[0].clone(); - let file2 = entries[1].clone(); - let file3 = entries[2].clone(); - let file4 = entries[3].clone(); - - let file1_item_id = workspace - .update(cx, |w, window, cx| { - w.open_path(file1.clone(), None, true, window, cx) - }) - .unwrap() - .await - .unwrap() - .item_id(); - let file2_item_id = workspace - .update(cx, |w, window, cx| { - w.open_path(file2.clone(), None, true, window, cx) - }) - .unwrap() - .await - .unwrap() - .item_id(); - let file3_item_id = workspace - .update(cx, |w, window, cx| { - w.open_path(file3.clone(), None, true, window, cx) - }) - .unwrap() - .await - .unwrap() - .item_id(); - let file4_item_id = workspace - .update(cx, |w, window, cx| { - w.open_path(file4.clone(), None, true, window, cx) - }) - .unwrap() - .await - .unwrap() - .item_id(); - assert_eq!(active_path(&workspace, cx), Some(file4.clone())); - - // Close all the pane items in some arbitrary order. - workspace - .update(cx, |_, window, cx| { - pane.update(cx, |pane, cx| { - pane.close_item_by_id(file1_item_id, SaveIntent::Close, window, cx) - }) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(active_path(&workspace, cx), Some(file4.clone())); - - workspace - .update(cx, |_, window, cx| { - pane.update(cx, |pane, cx| { - pane.close_item_by_id(file4_item_id, SaveIntent::Close, window, cx) - }) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(active_path(&workspace, cx), Some(file3.clone())); - - workspace - .update(cx, |_, window, cx| { - pane.update(cx, |pane, cx| { - pane.close_item_by_id(file2_item_id, SaveIntent::Close, window, cx) - }) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(active_path(&workspace, cx), Some(file3.clone())); - workspace - .update(cx, |_, window, cx| { - pane.update(cx, |pane, cx| { - pane.close_item_by_id(file3_item_id, SaveIntent::Close, window, cx) - }) - }) - .unwrap() - .await - .unwrap(); - - assert_eq!(active_path(&workspace, cx), None); - - // Reopen all the closed items, ensuring they are reopened in the same order - // in which they were closed. - workspace - .update(cx, Workspace::reopen_closed_item) - .unwrap() - .await - .unwrap(); - assert_eq!(active_path(&workspace, cx), Some(file3.clone())); - - workspace - .update(cx, Workspace::reopen_closed_item) - .unwrap() - .await - .unwrap(); - assert_eq!(active_path(&workspace, cx), Some(file2.clone())); - - workspace - .update(cx, Workspace::reopen_closed_item) - .unwrap() - .await - .unwrap(); - assert_eq!(active_path(&workspace, cx), Some(file4.clone())); - - workspace - .update(cx, Workspace::reopen_closed_item) - .unwrap() - .await - .unwrap(); - assert_eq!(active_path(&workspace, cx), Some(file1.clone())); - - // Reopening past the last closed item is a no-op. - workspace - .update(cx, Workspace::reopen_closed_item) - .unwrap() - .await - .unwrap(); - assert_eq!(active_path(&workspace, cx), Some(file1.clone())); - - // Reopening closed items doesn't interfere with navigation history. - workspace - .update(cx, |workspace, window, cx| { - workspace.go_back(workspace.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(active_path(&workspace, cx), Some(file4.clone())); - - workspace - .update(cx, |workspace, window, cx| { - workspace.go_back(workspace.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(active_path(&workspace, cx), Some(file2.clone())); - - workspace - .update(cx, |workspace, window, cx| { - workspace.go_back(workspace.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(active_path(&workspace, cx), Some(file3.clone())); - - workspace - .update(cx, |workspace, window, cx| { - workspace.go_back(workspace.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(active_path(&workspace, cx), Some(file4.clone())); - - workspace - .update(cx, |workspace, window, cx| { - workspace.go_back(workspace.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(active_path(&workspace, cx), Some(file3.clone())); - - workspace - .update(cx, |workspace, window, cx| { - workspace.go_back(workspace.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(active_path(&workspace, cx), Some(file2.clone())); - - workspace - .update(cx, |workspace, window, cx| { - workspace.go_back(workspace.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(active_path(&workspace, cx), Some(file1.clone())); - - workspace - .update(cx, |workspace, window, cx| { - workspace.go_back(workspace.active_pane().downgrade(), window, cx) - }) - .unwrap() - .await - .unwrap(); - assert_eq!(active_path(&workspace, cx), Some(file1.clone())); - - fn active_path( - workspace: &WindowHandle, - cx: &TestAppContext, - ) -> Option { - workspace - .read_with(cx, |workspace, cx| { - let item = workspace.active_item(cx)?; - item.project_path(cx) - }) - .unwrap() - } - } - - fn init_keymap_test(cx: &mut TestAppContext) -> Arc { - cx.update(|cx| { - let app_state = AppState::test(cx); - - theme::init(theme::LoadThemes::JustBase, cx); - client::init(&app_state.client, cx); - workspace::init(app_state.clone(), cx); - onboarding::init(cx); - app_state - }) - } - - actions!(test_only, [ActionA, ActionB]); - - #[gpui::test] - async fn test_base_keymap(cx: &mut gpui::TestAppContext) { - let executor = cx.executor(); - let app_state = init_keymap_test(cx); - let project = Project::test(app_state.fs.clone(), [], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - // From the Atom keymap - use workspace::ActivatePreviousPane; - // From the JetBrains keymap - use workspace::ActivatePreviousItem; - - app_state - .fs - .save( - "/settings.json".as_ref(), - &r#"{"base_keymap": "Atom"}"#.into(), - Default::default(), - ) - .await - .unwrap(); - - app_state - .fs - .save( - "/keymap.json".as_ref(), - &r#"[{"bindings": {"backspace": "test_only::ActionA"}}]"#.into(), - Default::default(), - ) - .await - .unwrap(); - executor.run_until_parked(); - cx.update(|cx| { - let settings_rx = watch_config_file( - &executor, - app_state.fs.clone(), - PathBuf::from("/settings.json"), - ); - let keymap_rx = watch_config_file( - &executor, - app_state.fs.clone(), - PathBuf::from("/keymap.json"), - ); - let global_settings_rx = watch_config_file( - &executor, - app_state.fs.clone(), - PathBuf::from("/global_settings.json"), - ); - handle_settings_file_changes(settings_rx, global_settings_rx, cx); - handle_keymap_file_changes(keymap_rx, cx); - }); - workspace - .update(cx, |workspace, _, cx| { - workspace.register_action(|_, _: &ActionA, _window, _cx| {}); - workspace.register_action(|_, _: &ActionB, _window, _cx| {}); - workspace.register_action(|_, _: &ActivatePreviousPane, _window, _cx| {}); - workspace.register_action(|_, _: &ActivatePreviousItem, _window, _cx| {}); - cx.notify(); - }) - .unwrap(); - executor.run_until_parked(); - // Test loading the keymap base at all - assert_key_bindings_for( - workspace.into(), - cx, - vec![("backspace", &ActionA), ("k", &ActivatePreviousPane)], - line!(), - ); - - // Test modifying the users keymap, while retaining the base keymap - app_state - .fs - .save( - "/keymap.json".as_ref(), - &r#"[{"bindings": {"backspace": "test_only::ActionB"}}]"#.into(), - Default::default(), - ) - .await - .unwrap(); - - executor.run_until_parked(); - - assert_key_bindings_for( - workspace.into(), - cx, - vec![("backspace", &ActionB), ("k", &ActivatePreviousPane)], - line!(), - ); - - // Test modifying the base, while retaining the users keymap - app_state - .fs - .save( - "/settings.json".as_ref(), - &r#"{"base_keymap": "JetBrains"}"#.into(), - Default::default(), - ) - .await - .unwrap(); - - executor.run_until_parked(); - - assert_key_bindings_for( - workspace.into(), - cx, - vec![("backspace", &ActionB), ("{", &ActivatePreviousItem)], - line!(), - ); - } - - #[gpui::test] - async fn test_disabled_keymap_binding(cx: &mut gpui::TestAppContext) { - let executor = cx.executor(); - let app_state = init_keymap_test(cx); - let project = Project::test(app_state.fs.clone(), [], cx).await; - let workspace = - cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - - // From the Atom keymap - use workspace::ActivatePreviousPane; - // From the JetBrains keymap - use diagnostics::Deploy; - - workspace - .update(cx, |workspace, _, _| { - workspace.register_action(|_, _: &ActionA, _window, _cx| {}); - workspace.register_action(|_, _: &ActionB, _window, _cx| {}); - workspace.register_action(|_, _: &Deploy, _window, _cx| {}); - }) - .unwrap(); - app_state - .fs - .save( - "/settings.json".as_ref(), - &r#"{"base_keymap": "Atom"}"#.into(), - Default::default(), - ) - .await - .unwrap(); - app_state - .fs - .save( - "/keymap.json".as_ref(), - &r#"[{"bindings": {"backspace": "test_only::ActionA"}}]"#.into(), - Default::default(), - ) - .await - .unwrap(); - - cx.update(|cx| { - let settings_rx = watch_config_file( - &executor, - app_state.fs.clone(), - PathBuf::from("/settings.json"), - ); - let keymap_rx = watch_config_file( - &executor, - app_state.fs.clone(), - PathBuf::from("/keymap.json"), - ); - - let global_settings_rx = watch_config_file( - &executor, - app_state.fs.clone(), - PathBuf::from("/global_settings.json"), - ); - handle_settings_file_changes(settings_rx, global_settings_rx, cx); - handle_keymap_file_changes(keymap_rx, cx); - }); - - cx.background_executor.run_until_parked(); - - cx.background_executor.run_until_parked(); - // Test loading the keymap base at all - assert_key_bindings_for( - workspace.into(), - cx, - vec![("backspace", &ActionA), ("k", &ActivatePreviousPane)], - line!(), - ); - - // Test disabling the key binding for the base keymap - app_state - .fs - .save( - "/keymap.json".as_ref(), - &r#"[{"bindings": {"backspace": null}}]"#.into(), - Default::default(), - ) - .await - .unwrap(); - - cx.background_executor.run_until_parked(); - - assert_key_bindings_for( - workspace.into(), - cx, - vec![("k", &ActivatePreviousPane)], - line!(), - ); - - // Test modifying the base, while retaining the users keymap - app_state - .fs - .save( - "/settings.json".as_ref(), - &r#"{"base_keymap": "JetBrains"}"#.into(), - Default::default(), - ) - .await - .unwrap(); - - cx.background_executor.run_until_parked(); - - assert_key_bindings_for(workspace.into(), cx, vec![("6", &Deploy)], line!()); - } - - #[gpui::test] - async fn test_generate_keymap_json_schema_for_registered_actions( - cx: &mut gpui::TestAppContext, - ) { - init_keymap_test(cx); - cx.update(|cx| { - // Make sure it doesn't panic. - KeymapFile::generate_json_schema_for_registered_actions(cx); - }); - } - - /// Checks that action namespaces are the expected set. The purpose of this is to prevent typos - /// and let you know when introducing a new namespace. - #[gpui::test] - async fn test_action_namespaces(cx: &mut gpui::TestAppContext) { - use itertools::Itertools; - - init_keymap_test(cx); - cx.update(|cx| { - let all_actions = cx.all_action_names(); - - let mut actions_without_namespace = Vec::new(); - let all_namespaces = all_actions - .iter() - .filter_map(|action_name| { - let namespace = action_name - .split("::") - .collect::>() - .into_iter() - .rev() - .skip(1) - .rev() - .join("::"); - if namespace.is_empty() { - actions_without_namespace.push(*action_name); - } - if &namespace == "test_only" || &namespace == "stories" { - None - } else { - Some(namespace) - } - }) - .sorted() - .dedup() - .collect::>(); - assert_eq!(actions_without_namespace, Vec::<&str>::new()); - - let expected_namespaces = vec![ - "action", - "activity_indicator", - "agent", - #[cfg(not(target_os = "macos"))] - "app_menu", - "assistant", - "assistant2", - "auto_update", - "branch_picker", - "bedrock", - "branches", - "buffer_search", - "channel_modal", - "cli", - "client", - "collab", - "collab_panel", - "command_palette", - "console", - "context_server", - "copilot", - "debug_panel", - "debugger", - "dev", - "diagnostics", - "edit_prediction", - "editor", - "feedback", - "file_finder", - "git", - "git_onboarding", - "git_panel", - "go_to_line", - "icon_theme_selector", - "inline_assistant", - "journal", - "keymap_editor", - "keystroke_input", - "language_selector", - "line_ending_selector", - "lsp_tool", - "markdown", - "menu", - "notebook", - "notification_panel", - "onboarding", - "outline", - "outline_panel", - "pane", - "panel", - "picker", - "project_panel", - "project_search", - "project_symbols", - "projects", - "repl", - "rules_library", - "search", - "settings_editor", - "settings_profile_selector", - "snippets", - "stash_picker", - "supermaven", - "svg", - "syntax_tree_view", - "tab_switcher", - "task", - "terminal", - "terminal_panel", - "theme_selector", - "toast", - "toolchain", - "variable_list", - "vim", - "window", - "workspace", - "zed", - "zed_actions", - "zed_predict_onboarding", - "zeta", - ]; - assert_eq!( - all_namespaces, - expected_namespaces - .into_iter() - .map(|namespace| namespace.to_string()) - .sorted() - .collect::>() - ); - }); - } - - #[gpui::test] - fn test_bundled_settings_and_themes(cx: &mut App) { - cx.text_system() - .add_fonts(vec![ - Assets - .load("fonts/lilex/Lilex-Regular.ttf") - .unwrap() - .unwrap(), - Assets - .load("fonts/ibm-plex-sans/IBMPlexSans-Regular.ttf") - .unwrap() - .unwrap(), - ]) - .unwrap(); - let themes = ThemeRegistry::default(); - settings::init(cx); - theme::init(theme::LoadThemes::JustBase, cx); - - let mut has_default_theme = false; - for theme_name in themes.list().into_iter().map(|meta| meta.name) { - let theme = themes.get(&theme_name).unwrap(); - assert_eq!(theme.name, theme_name); - if theme.name.as_ref() == "One Dark" { - has_default_theme = true; - } - } - assert!(has_default_theme); - } - - #[gpui::test] - async fn test_bundled_files_editor(cx: &mut TestAppContext) { - let app_state = init_test(cx); - cx.update(init); - - let project = Project::test(app_state.fs.clone(), [], cx).await; - let _window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx)); - - cx.update(|cx| { - cx.dispatch_action(&OpenDefaultSettings); - }); - cx.run_until_parked(); - - assert_eq!(cx.read(|cx| cx.windows().len()), 1); - - let workspace = cx.windows()[0].downcast::().unwrap(); - let active_editor = workspace - .update(cx, |workspace, _, cx| { - workspace.active_item_as::(cx) - }) - .unwrap(); - assert!( - active_editor.is_some(), - "Settings action should have opened an editor with the default file contents" - ); - - let active_editor = active_editor.unwrap(); - assert!( - active_editor.read_with(cx, |editor, cx| editor.read_only(cx)), - "Default settings should be readonly" - ); - assert!( - active_editor.read_with(cx, |editor, cx| editor.buffer().read(cx).read_only()), - "The underlying buffer should also be readonly for the shipped default settings" - ); - } - - #[gpui::test] - async fn test_bundled_languages(cx: &mut TestAppContext) { - let fs = fs::FakeFs::new(cx.background_executor.clone()); - env_logger::builder().is_test(true).try_init().ok(); - let settings = cx.update(SettingsStore::test); - cx.set_global(settings); - let languages = LanguageRegistry::test(cx.executor()); - let languages = Arc::new(languages); - let node_runtime = node_runtime::NodeRuntime::unavailable(); - cx.update(|cx| { - languages::init(languages.clone(), fs, node_runtime, cx); - }); - for name in languages.language_names() { - languages - .language_for_name(name.as_ref()) - .await - .with_context(|| format!("language name {name}")) - .unwrap(); - } - cx.run_until_parked(); - } - - pub(crate) fn init_test(cx: &mut TestAppContext) -> Arc { - init_test_with_state(cx, cx.update(AppState::test)) - } - - fn init_test_with_state( - cx: &mut TestAppContext, - mut app_state: Arc, - ) -> Arc { - cx.update(move |cx| { - env_logger::builder().is_test(true).try_init().ok(); - - let state = Arc::get_mut(&mut app_state).unwrap(); - state.build_window_options = build_window_options; - app_state.languages.add(markdown_lang()); - - gpui_tokio::init(cx); - theme::init(theme::LoadThemes::JustBase, cx); - audio::init(cx); - channel::init(&app_state.client, app_state.user_store.clone(), cx); - call::init(app_state.client.clone(), app_state.user_store.clone(), cx); - notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx); - workspace::init(app_state.clone(), cx); - release_channel::init(Version::new(0, 0, 0), cx); - command_palette::init(cx); - editor::init(cx); - collab_ui::init(&app_state, cx); - git_ui::init(cx); - project_panel::init(cx); - outline_panel::init(cx); - terminal_view::init(cx); - copilot::copilot_chat::init( - app_state.fs.clone(), - app_state.client.http_client(), - copilot::copilot_chat::CopilotChatConfiguration::default(), - cx, - ); - image_viewer::init(cx); - language_model::init(app_state.client.clone(), cx); - language_models::init(app_state.user_store.clone(), app_state.client.clone(), cx); - web_search::init(cx); - web_search_providers::init(app_state.client.clone(), cx); - let prompt_builder = PromptBuilder::load(app_state.fs.clone(), false, cx); - agent_ui::init( - app_state.fs.clone(), - app_state.client.clone(), - prompt_builder.clone(), - app_state.languages.clone(), - false, - cx, - ); - repl::init(app_state.fs.clone(), cx); - repl::notebook::init(cx); - tasks_ui::init(cx); - project::debugger::breakpoint_store::BreakpointStore::init( - &app_state.client.clone().into(), - ); - project::debugger::dap_store::DapStore::init(&app_state.client.clone().into(), cx); - debugger_ui::init(cx); - initialize_workspace(app_state.clone(), prompt_builder, cx); - search::init(cx); - app_state - }) - } - - #[track_caller] - fn assert_key_bindings_for( - window: AnyWindowHandle, - cx: &TestAppContext, - actions: Vec<(&'static str, &dyn Action)>, - line: u32, - ) { - let available_actions = cx - .update(|cx| window.update(cx, |_, window, cx| window.available_actions(cx))) - .unwrap(); - for (key, action) in actions { - let bindings = cx - .update(|cx| window.update(cx, |_, window, _| window.bindings_for_action(action))) - .unwrap(); - // assert that... - assert!( - available_actions.iter().any(|bound_action| { - // actions match... - bound_action.partial_eq(action) - }), - "On {} Failed to find {}", - line, - action.name(), - ); - assert!( - // and key strokes contain the given key - bindings - .into_iter() - .any(|binding| binding.keystrokes().iter().any(|k| k.key() == key)), - "On {} Failed to find {} with key binding {}", - line, - action.name(), - key - ); - } - } - - #[gpui::test] - async fn test_opening_project_settings_when_excluded(cx: &mut gpui::TestAppContext) { - // Use the proper initialization for runtime state - let app_state = init_keymap_test(cx); - - eprintln!("Running test_opening_project_settings_when_excluded"); - - // 1. Set up a project with some project settings - let settings_init = - r#"{ "UNIQUEVALUE": true, "git": { "inline_blame": { "enabled": false } } }"#; - app_state - .fs - .as_fake() - .insert_tree( - Path::new("/root"), - json!({ - ".zed": { - "settings.json": settings_init - } - }), - ) - .await; - - eprintln!("Created project with .zed/settings.json containing UNIQUEVALUE"); - - // 2. Create a project with the file system and load it - let project = Project::test(app_state.fs.clone(), [Path::new("/root")], cx).await; - - // Save original settings content for comparison - let original_settings = app_state - .fs - .load(Path::new("/root/.zed/settings.json")) - .await - .unwrap(); - - let original_settings_str = original_settings.clone(); - - // Verify settings exist on disk and have expected content - eprintln!("Original settings content: {}", original_settings_str); - assert!( - original_settings_str.contains("UNIQUEVALUE"), - "Test setup failed - settings file doesn't contain our marker" - ); - - // 3. Add .zed to file scan exclusions in user settings - cx.update_global::(|store, cx| { - store.update_user_settings(cx, |worktree_settings| { - worktree_settings.project.worktree.file_scan_exclusions = - Some(vec![".zed".to_string()]); - }); - }); - - eprintln!("Added .zed to file_scan_exclusions in settings"); - - // 4. Run tasks to apply settings - cx.background_executor.run_until_parked(); - - // 5. Critical: Verify .zed is actually excluded from worktree - let worktree = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap()); - - let has_zed_entry = - cx.update(|cx| worktree.read(cx).entry_for_path(rel_path(".zed")).is_some()); - - eprintln!( - "Is .zed directory visible in worktree after exclusion: {}", - has_zed_entry - ); - - // This assertion verifies the test is set up correctly to show the bug - // If .zed is not excluded, the test will fail here - assert!( - !has_zed_entry, - "Test precondition failed: .zed directory should be excluded but was found in worktree" - ); - - // 6. Create workspace and trigger the actual function that causes the bug - let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); - window - .update(cx, |workspace, window, cx| { - // Call the exact function that contains the bug - eprintln!("About to call open_project_settings_file"); - open_project_settings_file(workspace, &OpenProjectSettingsFile, window, cx); - }) - .unwrap(); - - // 7. Run background tasks until completion - cx.background_executor.run_until_parked(); - - // 8. Verify file contents after calling function - let new_content = app_state - .fs - .load(Path::new("/root/.zed/settings.json")) - .await - .unwrap(); - - let new_content_str = new_content; - eprintln!("New settings content: {}", new_content_str); - - // The bug causes the settings to be overwritten with empty settings - // So if the unique value is no longer present, the bug has been reproduced - let bug_exists = !new_content_str.contains("UNIQUEVALUE"); - eprintln!("Bug reproduced: {}", bug_exists); - - // This assertion should fail if the bug exists - showing the bug is real - assert!( - new_content_str.contains("UNIQUEVALUE"), - "BUG FOUND: Project settings were overwritten when opening via command - original custom content was lost" - ); - } - - #[gpui::test] - async fn test_prefer_focused_window(cx: &mut gpui::TestAppContext) { - let app_state = init_test(cx); - let paths = [PathBuf::from(path!("/dir/document.txt"))]; - - app_state - .fs - .as_fake() - .insert_tree( - path!("/dir"), - json!({ - "document.txt": "Some of the documentation's content." - }), - ) - .await; - - let project_a = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await; - let window_a = - cx.add_window(|window, cx| Workspace::test_new(project_a.clone(), window, cx)); - - let project_b = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await; - let window_b = - cx.add_window(|window, cx| Workspace::test_new(project_b.clone(), window, cx)); - - let project_c = Project::test(app_state.fs.clone(), [path!("/dir").as_ref()], cx).await; - let window_c = - cx.add_window(|window, cx| Workspace::test_new(project_c.clone(), window, cx)); - - for window in [window_a, window_b, window_c] { - let _ = cx.update_window(*window, |_, window, _| { - window.activate_window(); - }); - - cx.update(|cx| { - let open_options = OpenOptions { - prefer_focused_window: true, - ..Default::default() - }; - - workspace::open_paths(&paths, app_state.clone(), open_options, cx) - }) - .await - .unwrap(); - - cx.update_window(*window, |_, window, _| assert!(window.is_window_active())) - .unwrap(); - - let _ = window.read_with(cx, |workspace, cx| { - let pane = workspace.active_pane().read(cx); - let project_path = pane.active_item().unwrap().project_path(cx).unwrap(); - - assert_eq!( - project_path.path.as_ref().as_std_path().to_str().unwrap(), - path!("document.txt") - ) - }); - } - } -} diff --git a/crates/zed/src/zed/app_menus.rs b/crates/zed/src/zed/app_menus.rs deleted file mode 100644 index a7961ac6d4..0000000000 --- a/crates/zed/src/zed/app_menus.rs +++ /dev/null @@ -1,331 +0,0 @@ -use collab_ui::collab_panel; -use gpui::{App, Menu, MenuItem, OsAction}; -use release_channel::ReleaseChannel; -use terminal_view::terminal_panel; -use zed_actions::{ToggleFocus as ToggleDebugPanel, dev}; - -pub fn app_menus(cx: &mut App) -> Vec

    { - use zed_actions::Quit; - - let mut view_items = vec![ - MenuItem::action( - "Zoom In", - zed_actions::IncreaseBufferFontSize { persist: false }, - ), - MenuItem::action( - "Zoom Out", - zed_actions::DecreaseBufferFontSize { persist: false }, - ), - MenuItem::action( - "Reset Zoom", - zed_actions::ResetBufferFontSize { persist: false }, - ), - MenuItem::action( - "Reset All Zoom", - zed_actions::ResetAllZoom { persist: false }, - ), - MenuItem::separator(), - MenuItem::action("Toggle Left Dock", workspace::ToggleLeftDock), - MenuItem::action("Toggle Right Dock", workspace::ToggleRightDock), - MenuItem::action("Toggle Bottom Dock", workspace::ToggleBottomDock), - MenuItem::action("Toggle All Docks", workspace::ToggleAllDocks), - MenuItem::submenu(Menu { - name: "Editor Layout".into(), - items: vec![ - MenuItem::action("Split Up", workspace::SplitUp), - MenuItem::action("Split Down", workspace::SplitDown), - MenuItem::action("Split Left", workspace::SplitLeft), - MenuItem::action("Split Right", workspace::SplitRight), - ], - }), - MenuItem::separator(), - MenuItem::action("Project Panel", zed_actions::project_panel::ToggleFocus), - MenuItem::action("Outline Panel", outline_panel::ToggleFocus), - MenuItem::action("Collab Panel", collab_panel::ToggleFocus), - MenuItem::action("Terminal Panel", terminal_panel::ToggleFocus), - MenuItem::action("Debugger Panel", ToggleDebugPanel), - MenuItem::separator(), - MenuItem::action("Diagnostics", diagnostics::Deploy), - MenuItem::separator(), - ]; - - if ReleaseChannel::try_global(cx) == Some(ReleaseChannel::Dev) { - view_items.push(MenuItem::action( - "Toggle GPUI Inspector", - dev::ToggleInspector, - )); - view_items.push(MenuItem::separator()); - } - - vec![ - Menu { - name: "Zed".into(), - items: vec![ - MenuItem::action("About Zed", zed_actions::About), - MenuItem::action("Check for Updates", auto_update::Check), - MenuItem::separator(), - MenuItem::submenu(Menu { - name: "Settings".into(), - items: vec![ - MenuItem::action("Open Settings", zed_actions::OpenSettings), - MenuItem::action("Open Settings File", super::OpenSettingsFile), - MenuItem::action("Open Project Settings", zed_actions::OpenProjectSettings), - MenuItem::action( - "Open Project Settings File", - super::OpenProjectSettingsFile, - ), - MenuItem::action("Open Default Settings", super::OpenDefaultSettings), - MenuItem::separator(), - MenuItem::action("Open Keymap", zed_actions::OpenKeymap), - MenuItem::action("Open Keymap File", zed_actions::OpenKeymapFile), - MenuItem::action( - "Open Default Key Bindings", - zed_actions::OpenDefaultKeymap, - ), - MenuItem::separator(), - MenuItem::action( - "Select Theme...", - zed_actions::theme_selector::Toggle::default(), - ), - MenuItem::action( - "Select Icon Theme...", - zed_actions::icon_theme_selector::Toggle::default(), - ), - ], - }), - MenuItem::separator(), - #[cfg(target_os = "macos")] - MenuItem::os_submenu("Services", gpui::SystemMenuType::Services), - MenuItem::separator(), - MenuItem::action("Extensions", zed_actions::Extensions::default()), - #[cfg(not(target_os = "windows"))] - MenuItem::action("Install CLI", install_cli::InstallCliBinary), - MenuItem::separator(), - #[cfg(target_os = "macos")] - MenuItem::action("Hide Zed", super::Hide), - #[cfg(target_os = "macos")] - MenuItem::action("Hide Others", super::HideOthers), - #[cfg(target_os = "macos")] - MenuItem::action("Show All", super::ShowAll), - MenuItem::separator(), - MenuItem::action("Quit Zed", Quit), - ], - }, - Menu { - name: "File".into(), - items: vec![ - MenuItem::action("New", workspace::NewFile), - MenuItem::action("New Window", workspace::NewWindow), - MenuItem::separator(), - #[cfg(not(target_os = "macos"))] - MenuItem::action("Open File...", workspace::OpenFiles), - MenuItem::action( - if cfg!(not(target_os = "macos")) { - "Open Folder..." - } else { - "Open…" - }, - workspace::Open, - ), - MenuItem::action( - "Open Recent...", - zed_actions::OpenRecent { - create_new_window: false, - }, - ), - MenuItem::action( - "Open Remote...", - zed_actions::OpenRemote { - create_new_window: false, - from_existing_connection: false, - }, - ), - MenuItem::separator(), - MenuItem::action("Add Folder to Project…", workspace::AddFolderToProject), - MenuItem::separator(), - MenuItem::action("Save", workspace::Save { save_intent: None }), - MenuItem::action("Save As…", workspace::SaveAs), - MenuItem::action("Save All", workspace::SaveAll { save_intent: None }), - MenuItem::separator(), - MenuItem::action( - "Close Editor", - workspace::CloseActiveItem { - save_intent: None, - close_pinned: true, - }, - ), - MenuItem::action("Close Window", workspace::CloseWindow), - ], - }, - Menu { - name: "Edit".into(), - items: vec![ - MenuItem::os_action("Undo", editor::actions::Undo, OsAction::Undo), - MenuItem::os_action("Redo", editor::actions::Redo, OsAction::Redo), - MenuItem::separator(), - MenuItem::os_action("Cut", editor::actions::Cut, OsAction::Cut), - MenuItem::os_action("Copy", editor::actions::Copy, OsAction::Copy), - MenuItem::action("Copy and Trim", editor::actions::CopyAndTrim), - MenuItem::os_action("Paste", editor::actions::Paste, OsAction::Paste), - MenuItem::separator(), - MenuItem::action("Find", search::buffer_search::Deploy::find()), - MenuItem::action("Find in Project", workspace::DeploySearch::find()), - MenuItem::separator(), - MenuItem::action( - "Toggle Line Comment", - editor::actions::ToggleComments::default(), - ), - ], - }, - Menu { - name: "Selection".into(), - items: vec![ - MenuItem::os_action( - "Select All", - editor::actions::SelectAll, - OsAction::SelectAll, - ), - MenuItem::action("Expand Selection", editor::actions::SelectLargerSyntaxNode), - MenuItem::action("Shrink Selection", editor::actions::SelectSmallerSyntaxNode), - MenuItem::action("Select Next Sibling", editor::actions::SelectNextSyntaxNode), - MenuItem::action( - "Select Previous Sibling", - editor::actions::SelectPreviousSyntaxNode, - ), - MenuItem::separator(), - MenuItem::action( - "Add Cursor Above", - editor::actions::AddSelectionAbove { - skip_soft_wrap: true, - }, - ), - MenuItem::action( - "Add Cursor Below", - editor::actions::AddSelectionBelow { - skip_soft_wrap: true, - }, - ), - MenuItem::action( - "Select Next Occurrence", - editor::actions::SelectNext { - replace_newest: false, - }, - ), - MenuItem::action( - "Select Previous Occurrence", - editor::actions::SelectPrevious { - replace_newest: false, - }, - ), - MenuItem::action("Select All Occurrences", editor::actions::SelectAllMatches), - MenuItem::separator(), - MenuItem::action("Move Line Up", editor::actions::MoveLineUp), - MenuItem::action("Move Line Down", editor::actions::MoveLineDown), - MenuItem::action("Duplicate Selection", editor::actions::DuplicateLineDown), - ], - }, - Menu { - name: "View".into(), - items: view_items, - }, - Menu { - name: "Go".into(), - items: vec![ - MenuItem::action("Back", workspace::GoBack), - MenuItem::action("Forward", workspace::GoForward), - MenuItem::separator(), - MenuItem::action("Command Palette...", zed_actions::command_palette::Toggle), - MenuItem::separator(), - MenuItem::action("Go to File...", workspace::ToggleFileFinder::default()), - // MenuItem::action("Go to Symbol in Project", project_symbols::Toggle), - MenuItem::action( - "Go to Symbol in Editor...", - zed_actions::outline::ToggleOutline, - ), - MenuItem::action("Go to Line/Column...", editor::actions::ToggleGoToLine), - MenuItem::separator(), - MenuItem::action("Go to Definition", editor::actions::GoToDefinition), - MenuItem::action("Go to Declaration", editor::actions::GoToDeclaration), - MenuItem::action("Go to Type Definition", editor::actions::GoToTypeDefinition), - MenuItem::action( - "Find All References", - editor::actions::FindAllReferences::default(), - ), - MenuItem::separator(), - MenuItem::action("Next Problem", editor::actions::GoToDiagnostic::default()), - MenuItem::action( - "Previous Problem", - editor::actions::GoToPreviousDiagnostic::default(), - ), - ], - }, - Menu { - name: "Run".into(), - items: vec![ - MenuItem::action( - "Spawn Task", - zed_actions::Spawn::ViaModal { - reveal_target: None, - }, - ), - MenuItem::action("Start Debugger", debugger_ui::Start), - MenuItem::separator(), - MenuItem::action("Edit tasks.json...", crate::zed::OpenProjectTasks), - MenuItem::action("Edit debug.json...", zed_actions::OpenProjectDebugTasks), - MenuItem::separator(), - MenuItem::action("Continue", debugger_ui::Continue), - MenuItem::action("Step Over", debugger_ui::StepOver), - MenuItem::action("Step Into", debugger_ui::StepInto), - MenuItem::action("Step Out", debugger_ui::StepOut), - MenuItem::separator(), - MenuItem::action("Toggle Breakpoint", editor::actions::ToggleBreakpoint), - MenuItem::action("Edit Breakpoint", editor::actions::EditLogBreakpoint), - MenuItem::action("Clear All Breakpoints", debugger_ui::ClearAllBreakpoints), - ], - }, - Menu { - name: "Window".into(), - items: vec![ - MenuItem::action("Minimize", super::Minimize), - MenuItem::action("Zoom", super::Zoom), - MenuItem::separator(), - ], - }, - Menu { - name: "Help".into(), - items: vec![ - MenuItem::action( - "View Release Notes Locally", - auto_update_ui::ViewReleaseNotesLocally, - ), - MenuItem::action("View Telemetry", zed_actions::OpenTelemetryLog), - MenuItem::action("View Dependency Licenses", zed_actions::OpenLicenses), - MenuItem::action("Show Welcome", onboarding::ShowWelcome), - MenuItem::separator(), - MenuItem::action("File Bug Report...", zed_actions::feedback::FileBugReport), - MenuItem::action("Request Feature...", zed_actions::feedback::RequestFeature), - MenuItem::action("Email Us...", zed_actions::feedback::EmailZed), - MenuItem::separator(), - MenuItem::action( - "Documentation", - super::OpenBrowser { - url: "https://zed.dev/docs".into(), - }, - ), - MenuItem::action("Zed Repository", feedback::OpenZedRepo), - MenuItem::action( - "Zed Twitter", - super::OpenBrowser { - url: "https://twitter.com/zeddotdev".into(), - }, - ), - MenuItem::action( - "Join the Team", - super::OpenBrowser { - url: "https://zed.dev/jobs".into(), - }, - ), - ], - }, - ] -} diff --git a/crates/zed/src/zed/component_preview.rs b/crates/zed/src/zed/component_preview.rs deleted file mode 100644 index 14a46d8882..0000000000 --- a/crates/zed/src/zed/component_preview.rs +++ /dev/null @@ -1,1001 +0,0 @@ -//! # Component Preview -//! -//! A view for exploring Zed components. - -mod persistence; - -use client::UserStore; -use collections::HashMap; -use component::{ComponentId, ComponentMetadata, ComponentStatus, components}; -use gpui::{ - App, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity, Window, list, prelude::*, -}; -use gpui::{ListState, ScrollHandle, ScrollStrategy, UniformListScrollHandle}; -use languages::LanguageRegistry; -use notifications::status_toast::{StatusToast, ToastIcon}; -use persistence::COMPONENT_PREVIEW_DB; -use project::Project; -use std::{iter::Iterator, ops::Range, sync::Arc}; -use ui::{ButtonLike, Divider, HighlightedLabel, ListItem, ListSubHeader, Tooltip, prelude::*}; -use ui_input::InputField; -use workspace::{ - AppState, Item, ItemId, SerializableItem, Workspace, WorkspaceId, delete_unloaded_items, - item::ItemEvent, -}; - -pub fn init(app_state: Arc, cx: &mut App) { - workspace::register_serializable_item::(cx); - - cx.observe_new(move |workspace: &mut Workspace, _window, cx| { - let app_state = app_state.clone(); - let project = workspace.project().clone(); - let weak_workspace = cx.entity().downgrade(); - - workspace.register_action( - move |workspace, _: &workspace::OpenComponentPreview, window, cx| { - let app_state = app_state.clone(); - - let language_registry = app_state.languages.clone(); - let user_store = app_state.user_store.clone(); - - let component_preview = cx.new(|cx| { - ComponentPreview::new( - weak_workspace.clone(), - project.clone(), - language_registry, - user_store, - None, - None, - window, - cx, - ) - .expect("Failed to create component preview") - }); - - workspace.add_item_to_active_pane( - Box::new(component_preview), - None, - true, - window, - cx, - ) - }, - ); - }) - .detach(); -} - -enum PreviewEntry { - AllComponents, - Separator, - Component(ComponentMetadata, Option>), - SectionHeader(SharedString), -} - -impl From for PreviewEntry { - fn from(component: ComponentMetadata) -> Self { - PreviewEntry::Component(component, None) - } -} - -impl From for PreviewEntry { - fn from(section_header: SharedString) -> Self { - PreviewEntry::SectionHeader(section_header) - } -} - -#[derive(Default, Debug, Clone, PartialEq, Eq)] -enum PreviewPage { - #[default] - AllComponents, - Component(ComponentId), -} - -struct ComponentPreview { - active_page: PreviewPage, - reset_key: usize, - component_list: ListState, - entries: Vec, - component_map: HashMap, - components: Vec, - cursor_index: usize, - filter_editor: Entity, - filter_text: String, - focus_handle: FocusHandle, - language_registry: Arc, - nav_scroll_handle: UniformListScrollHandle, - project: Entity, - user_store: Entity, - workspace: WeakEntity, - workspace_id: Option, - _view_scroll_handle: ScrollHandle, -} - -impl ComponentPreview { - pub fn new( - workspace: WeakEntity, - project: Entity, - language_registry: Arc, - user_store: Entity, - selected_index: impl Into>, - active_page: Option, - window: &mut Window, - cx: &mut Context, - ) -> anyhow::Result { - let component_registry = Arc::new(components()); - let sorted_components = component_registry.sorted_components(); - let selected_index = selected_index.into().unwrap_or(0); - let active_page = active_page.unwrap_or(PreviewPage::AllComponents); - let filter_editor = cx.new(|cx| InputField::new(window, cx, "Find components or usages…")); - - let component_list = ListState::new( - sorted_components.len(), - gpui::ListAlignment::Top, - px(1500.0), - ); - - let mut component_preview = Self { - active_page, - reset_key: 0, - component_list, - entries: Vec::new(), - component_map: component_registry.component_map(), - components: sorted_components, - cursor_index: selected_index, - filter_editor, - filter_text: String::new(), - focus_handle: cx.focus_handle(), - language_registry, - nav_scroll_handle: UniformListScrollHandle::new(), - project, - user_store, - workspace, - workspace_id: None, - _view_scroll_handle: ScrollHandle::new(), - }; - - if component_preview.cursor_index > 0 { - component_preview.scroll_to_preview(component_preview.cursor_index, cx); - } - - component_preview.update_component_list(cx); - - let focus_handle = component_preview.filter_editor.read(cx).focus_handle(cx); - window.focus(&focus_handle); - - Ok(component_preview) - } - - pub fn active_page_id(&self, _cx: &App) -> ActivePageId { - match &self.active_page { - PreviewPage::AllComponents => ActivePageId::default(), - PreviewPage::Component(component_id) => ActivePageId(component_id.0.to_string()), - } - } - - fn scroll_to_preview(&mut self, ix: usize, cx: &mut Context) { - self.component_list.scroll_to_reveal_item(ix); - self.cursor_index = ix; - cx.notify(); - } - - fn set_active_page(&mut self, page: PreviewPage, cx: &mut Context) { - if self.active_page == page { - // Force the current preview page to render again - self.reset_key = self.reset_key.wrapping_add(1); - } else { - self.active_page = page; - cx.emit(ItemEvent::UpdateTab); - } - cx.notify(); - } - - fn filtered_components(&self) -> Vec { - if self.filter_text.is_empty() { - return self.components.clone(); - } - - let filter = self.filter_text.to_lowercase(); - self.components - .iter() - .filter(|component| { - let component_name = component.name().to_lowercase(); - let scope_name = component.scope().to_string().to_lowercase(); - let description = component - .description() - .map(|d| d.to_lowercase()) - .unwrap_or_default(); - - component_name.contains(&filter) - || scope_name.contains(&filter) - || description.contains(&filter) - }) - .cloned() - .collect() - } - - fn scope_ordered_entries(&self) -> Vec { - use collections::HashMap; - - let mut scope_groups: HashMap< - ComponentScope, - Vec<(ComponentMetadata, Option>)>, - > = HashMap::default(); - let lowercase_filter = self.filter_text.to_lowercase(); - - for component in &self.components { - if self.filter_text.is_empty() { - scope_groups - .entry(component.scope()) - .or_insert_with(Vec::new) - .push((component.clone(), None)); - continue; - } - - // let full_component_name = component.name(); - let scopeless_name = component.scopeless_name(); - let scope_name = component.scope().to_string(); - let description = component.description().unwrap_or_default(); - - let lowercase_scopeless = scopeless_name.to_lowercase(); - let lowercase_scope = scope_name.to_lowercase(); - let lowercase_desc = description.to_lowercase(); - - if lowercase_scopeless.contains(&lowercase_filter) - && let Some(index) = lowercase_scopeless.find(&lowercase_filter) - { - let end = index + lowercase_filter.len(); - - if end <= scopeless_name.len() { - let mut positions = Vec::new(); - for i in index..end { - if scopeless_name.is_char_boundary(i) { - positions.push(i); - } - } - - if !positions.is_empty() { - scope_groups - .entry(component.scope()) - .or_insert_with(Vec::new) - .push((component.clone(), Some(positions))); - continue; - } - } - } - - if lowercase_scopeless.contains(&lowercase_filter) - || lowercase_scope.contains(&lowercase_filter) - || lowercase_desc.contains(&lowercase_filter) - { - scope_groups - .entry(component.scope()) - .or_insert_with(Vec::new) - .push((component.clone(), None)); - } - } - - // Sort the components in each group - for components in scope_groups.values_mut() { - components.sort_by_key(|(c, _)| c.sort_name()); - } - - let mut entries = Vec::new(); - - // Always show all components first - entries.push(PreviewEntry::AllComponents); - - let mut scopes: Vec<_> = scope_groups - .keys() - .filter(|scope| !matches!(**scope, ComponentScope::None)) - .cloned() - .collect(); - - scopes.sort_by_key(|s| s.to_string()); - - for scope in scopes { - if let Some(components) = scope_groups.remove(&scope) - && !components.is_empty() - { - entries.push(PreviewEntry::Separator); - entries.push(PreviewEntry::SectionHeader(scope.to_string().into())); - - let mut sorted_components = components; - sorted_components.sort_by_key(|(component, _)| component.sort_name()); - - for (component, positions) in sorted_components { - entries.push(PreviewEntry::Component(component, positions)); - } - } - } - - // Add uncategorized components last - if let Some(components) = scope_groups.get(&ComponentScope::None) - && !components.is_empty() - { - entries.push(PreviewEntry::Separator); - entries.push(PreviewEntry::SectionHeader("Uncategorized".into())); - let mut sorted_components = components.clone(); - sorted_components.sort_by_key(|(c, _)| c.sort_name()); - - for (component, positions) in sorted_components { - entries.push(PreviewEntry::Component(component, positions)); - } - } - - entries - } - - fn update_component_list(&mut self, cx: &mut Context) { - let entries = self.scope_ordered_entries(); - let new_len = entries.len(); - - if new_len > 0 { - self.nav_scroll_handle - .scroll_to_item(0, ScrollStrategy::Top); - } - - let filtered_components = self.filtered_components(); - - if !self.filter_text.is_empty() - && !matches!(self.active_page, PreviewPage::AllComponents) - && let PreviewPage::Component(ref component_id) = self.active_page - { - let component_still_visible = filtered_components - .iter() - .any(|component| component.id() == *component_id); - - if !component_still_visible { - if !filtered_components.is_empty() { - let first_component = &filtered_components[0]; - self.set_active_page(PreviewPage::Component(first_component.id()), cx); - } else { - self.set_active_page(PreviewPage::AllComponents, cx); - } - } - } - - self.component_list = ListState::new(new_len, gpui::ListAlignment::Top, px(1500.0)); - self.entries = entries; - - cx.emit(ItemEvent::UpdateTab); - } - - fn render_sidebar_entry( - &self, - ix: usize, - entry: &PreviewEntry, - cx: &Context, - ) -> impl IntoElement + use<> { - match entry { - PreviewEntry::Component(component_metadata, highlight_positions) => { - let id = component_metadata.id(); - let selected = self.active_page == PreviewPage::Component(id.clone()); - let name = component_metadata.scopeless_name(); - - ListItem::new(ix) - .child(if let Some(_positions) = highlight_positions { - let name_lower = name.to_lowercase(); - let filter_lower = self.filter_text.to_lowercase(); - let valid_positions = if let Some(start) = name_lower.find(&filter_lower) { - let end = start + filter_lower.len(); - (start..end).collect() - } else { - Vec::new() - }; - if valid_positions.is_empty() { - Label::new(name).into_any_element() - } else { - HighlightedLabel::new(name, valid_positions).into_any_element() - } - } else { - Label::new(name).into_any_element() - }) - .selectable(true) - .toggle_state(selected) - .inset(true) - .on_click(cx.listener(move |this, _, _, cx| { - let id = id.clone(); - this.set_active_page(PreviewPage::Component(id), cx); - })) - .into_any_element() - } - PreviewEntry::SectionHeader(shared_string) => ListSubHeader::new(shared_string) - .inset(true) - .into_any_element(), - PreviewEntry::AllComponents => { - let selected = self.active_page == PreviewPage::AllComponents; - - ListItem::new(ix) - .child(Label::new("All Components")) - .selectable(true) - .toggle_state(selected) - .inset(true) - .on_click(cx.listener(move |this, _, _, cx| { - this.set_active_page(PreviewPage::AllComponents, cx); - })) - .into_any_element() - } - PreviewEntry::Separator => ListItem::new(ix) - .disabled(true) - .child(div().w_full().py_2().child(Divider::horizontal())) - .into_any_element(), - } - } - - fn render_scope_header( - &self, - _ix: usize, - title: SharedString, - _window: &Window, - _cx: &App, - ) -> impl IntoElement { - h_flex() - .w_full() - .h_10() - .child(Headline::new(title).size(HeadlineSize::XSmall)) - .child(Divider::horizontal()) - } - - fn render_preview( - &self, - component: &ComponentMetadata, - window: &mut Window, - cx: &mut App, - ) -> impl IntoElement { - let name = component.scopeless_name(); - let scope = component.scope(); - - let description = component.description(); - - // Build the content container - let mut preview_container = v_flex().py_2().child( - v_flex() - .border_1() - .border_color(cx.theme().colors().border) - .rounded_sm() - .w_full() - .gap_4() - .py_4() - .px_6() - .flex_none() - .child( - v_flex() - .gap_1() - .child( - h_flex() - .gap_1() - .text_xl() - .child(div().child(name)) - .when(!matches!(scope, ComponentScope::None), |this| { - this.child(div().opacity(0.5).child(format!("({})", scope))) - }), - ) - .when_some(description, |this, description| { - this.child( - div() - .text_ui_sm(cx) - .text_color(cx.theme().colors().text_muted) - .max_w(px(600.0)) - .child(description), - ) - }), - ), - ); - - if let Some(preview) = component.preview() { - preview_container = preview_container.children(preview(window, cx)); - } - - preview_container.into_any_element() - } - - fn render_all_components(&self, cx: &Context) -> impl IntoElement { - v_flex() - .id("component-list") - .px_8() - .pt_4() - .size_full() - .child( - if self.filtered_components().is_empty() && !self.filter_text.is_empty() { - div() - .size_full() - .items_center() - .justify_center() - .text_color(cx.theme().colors().text_muted) - .child(format!("No components matching '{}'.", self.filter_text)) - .into_any_element() - } else { - list( - self.component_list.clone(), - cx.processor(|this, ix, window, cx| { - if ix >= this.entries.len() { - return div().w_full().h_0().into_any_element(); - } - - let entry = &this.entries[ix]; - - match entry { - PreviewEntry::Component(component, _) => this - .render_preview(component, window, cx) - .into_any_element(), - PreviewEntry::SectionHeader(shared_string) => this - .render_scope_header(ix, shared_string.clone(), window, cx) - .into_any_element(), - PreviewEntry::AllComponents => { - div().w_full().h_0().into_any_element() - } - PreviewEntry::Separator => div().w_full().h_0().into_any_element(), - } - }), - ) - .flex_grow() - .with_sizing_behavior(gpui::ListSizingBehavior::Auto) - .into_any_element() - }, - ) - } - - fn render_component_page( - &mut self, - component_id: &ComponentId, - _window: &mut Window, - _cx: &mut Context, - ) -> impl IntoElement { - let component = self.component_map.get(component_id); - - if let Some(component) = component { - v_flex() - .id("render-component-page") - .flex_1() - .child(ComponentPreviewPage::new(component.clone(), self.reset_key)) - .into_any_element() - } else { - v_flex() - .size_full() - .items_center() - .justify_center() - .child("Component not found") - .into_any_element() - } - } - - fn test_status_toast(&self, cx: &mut Context) { - if let Some(workspace) = self.workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - let status_toast = - StatusToast::new("`zed/new-notification-system` created!", cx, |this, _cx| { - this.icon(ToastIcon::new(IconName::GitBranchAlt).color(Color::Muted)) - .action("Open Pull Request", |_, cx| { - cx.open_url("https://github.com/") - }) - }); - workspace.toggle_status_toast(status_toast, cx) - }); - } - } -} - -impl Render for ComponentPreview { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - // TODO: move this into the struct - let current_filter = self.filter_editor.update(cx, |input, cx| { - if input.is_empty(cx) { - String::new() - } else { - input.editor().read(cx).text(cx) - } - }); - - if current_filter != self.filter_text { - self.filter_text = current_filter; - self.update_component_list(cx); - } - let sidebar_entries = self.scope_ordered_entries(); - let active_page = self.active_page.clone(); - - h_flex() - .id("component-preview") - .key_context("ComponentPreview") - .items_start() - .overflow_hidden() - .size_full() - .track_focus(&self.focus_handle) - .bg(cx.theme().colors().editor_background) - .child( - v_flex() - .h_full() - .border_r_1() - .border_color(cx.theme().colors().border) - .child( - gpui::uniform_list( - "component-nav", - sidebar_entries.len(), - cx.processor(move |this, range: Range, _window, cx| { - range - .filter_map(|ix| { - if ix < sidebar_entries.len() { - Some(this.render_sidebar_entry( - ix, - &sidebar_entries[ix], - cx, - )) - } else { - None - } - }) - .collect() - }), - ) - .track_scroll(&self.nav_scroll_handle) - .p_2p5() - .w(px(231.)) // Matches perfectly with the size of the "Component Preview" tab, if that's the first one in the pane - .h_full() - .flex_1(), - ) - .child( - div() - .w_full() - .p_2p5() - .border_t_1() - .border_color(cx.theme().colors().border) - .child( - Button::new("toast-test", "Launch Toast") - .full_width() - .on_click(cx.listener({ - move |this, _, _window, cx| { - this.test_status_toast(cx); - cx.notify(); - } - })), - ), - ), - ) - .child( - v_flex() - .flex_1() - .size_full() - .child( - div() - .p_2() - .w_full() - .border_b_1() - .border_color(cx.theme().colors().border) - .child(self.filter_editor.clone()), - ) - .child( - div().id("content-area").flex_1().overflow_y_scroll().child( - match active_page { - PreviewPage::AllComponents => { - self.render_all_components(cx).into_any_element() - } - PreviewPage::Component(id) => self - .render_component_page(&id, window, cx) - .into_any_element(), - }, - ), - ), - ) - } -} - -impl EventEmitter for ComponentPreview {} - -impl Focusable for ComponentPreview { - fn focus_handle(&self, _: &App) -> gpui::FocusHandle { - self.focus_handle.clone() - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct ActivePageId(pub String); - -impl Default for ActivePageId { - fn default() -> Self { - ActivePageId("AllComponents".to_string()) - } -} - -impl From for ActivePageId { - fn from(id: ComponentId) -> Self { - Self(id.0.to_string()) - } -} - -impl Item for ComponentPreview { - type Event = ItemEvent; - - fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString { - "Component Preview".into() - } - - fn telemetry_event_text(&self) -> Option<&'static str> { - None - } - - fn show_toolbar(&self) -> bool { - false - } - - fn can_split(&self) -> bool { - true - } - - fn clone_on_split( - &self, - _workspace_id: Option, - window: &mut Window, - cx: &mut Context, - ) -> Task>> - where - Self: Sized, - { - let language_registry = self.language_registry.clone(); - let user_store = self.user_store.clone(); - let weak_workspace = self.workspace.clone(); - let project = self.project.clone(); - let selected_index = self.cursor_index; - let active_page = self.active_page.clone(); - - let self_result = Self::new( - weak_workspace, - project, - language_registry, - user_store, - selected_index, - Some(active_page), - window, - cx, - ); - - Task::ready(match self_result { - Ok(preview) => Some(cx.new(|_cx| preview)), - Err(e) => { - log::error!("Failed to clone component preview: {}", e); - None - } - }) - } - - fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) { - f(*event) - } - - fn added_to_workspace( - &mut self, - workspace: &mut Workspace, - window: &mut Window, - cx: &mut Context, - ) { - self.workspace_id = workspace.database_id(); - - let focus_handle = self.filter_editor.read(cx).focus_handle(cx); - window.focus(&focus_handle); - } -} - -impl SerializableItem for ComponentPreview { - fn serialized_item_kind() -> &'static str { - "ComponentPreview" - } - - fn deserialize( - project: Entity, - workspace: WeakEntity, - workspace_id: WorkspaceId, - item_id: ItemId, - window: &mut Window, - cx: &mut App, - ) -> Task>> { - let deserialized_active_page = - match COMPONENT_PREVIEW_DB.get_active_page(item_id, workspace_id) { - Ok(page) => { - if let Some(page) = page { - ActivePageId(page) - } else { - ActivePageId::default() - } - } - Err(_) => ActivePageId::default(), - }; - - let user_store = project.read(cx).user_store(); - let language_registry = project.read(cx).languages().clone(); - let preview_page = if deserialized_active_page.0 == ActivePageId::default().0 { - Some(PreviewPage::default()) - } else { - let component_str = deserialized_active_page.0; - let component_registry = components(); - let all_components = component_registry.components(); - let found_component = all_components.iter().find(|c| c.id().0 == component_str); - - if let Some(component) = found_component { - Some(PreviewPage::Component(component.id())) - } else { - Some(PreviewPage::default()) - } - }; - - window.spawn(cx, async move |cx| { - let user_store = user_store.clone(); - let language_registry = language_registry.clone(); - let weak_workspace = workspace.clone(); - let project = project.clone(); - cx.update(move |window, cx| { - Ok(cx.new(|cx| { - ComponentPreview::new( - weak_workspace, - project, - language_registry, - user_store, - None, - preview_page, - window, - cx, - ) - .expect("Failed to create component preview") - })) - })? - }) - } - - fn cleanup( - workspace_id: WorkspaceId, - alive_items: Vec, - _window: &mut Window, - cx: &mut App, - ) -> Task> { - delete_unloaded_items( - alive_items, - workspace_id, - "component_previews", - &COMPONENT_PREVIEW_DB, - cx, - ) - } - - fn serialize( - &mut self, - _workspace: &mut Workspace, - item_id: ItemId, - _closing: bool, - _window: &mut Window, - cx: &mut Context, - ) -> Option>> { - let active_page = self.active_page_id(cx); - let workspace_id = self.workspace_id?; - Some(cx.background_spawn(async move { - COMPONENT_PREVIEW_DB - .save_active_page(item_id, workspace_id, active_page.0) - .await - })) - } - - fn should_serialize(&self, event: &Self::Event) -> bool { - matches!(event, ItemEvent::UpdateTab) - } -} - -// TODO: use language registry to allow rendering markdown -#[derive(IntoElement)] -pub struct ComponentPreviewPage { - // languages: Arc, - component: ComponentMetadata, - reset_key: usize, -} - -impl ComponentPreviewPage { - pub fn new( - component: ComponentMetadata, - reset_key: usize, - // languages: Arc - ) -> Self { - Self { - // languages, - component, - reset_key, - } - } - - /// Renders the component status when it would be useful - /// - /// Doesn't render if the component is `ComponentStatus::Live` - /// as that is the default state - fn render_component_status(&self, cx: &App) -> Option { - let status = self.component.status(); - let status_description = status.description().to_string(); - - let color = match status { - ComponentStatus::Deprecated => Color::Error, - ComponentStatus::EngineeringReady => Color::Info, - ComponentStatus::Live => Color::Success, - ComponentStatus::WorkInProgress => Color::Warning, - }; - - if status != ComponentStatus::Live { - Some( - ButtonLike::new("component_status") - .child( - div() - .px_1p5() - .rounded_sm() - .bg(color.color(cx).alpha(0.12)) - .child( - Label::new(status.to_string()) - .size(LabelSize::Small) - .color(color), - ), - ) - .tooltip(Tooltip::text(status_description)) - .disabled(true), - ) - } else { - None - } - } - - fn render_header(&self, _: &Window, cx: &App) -> impl IntoElement { - v_flex() - .min_w_0() - .w_full() - .p_12() - .gap_6() - .bg(cx.theme().colors().surface_background) - .border_b_1() - .border_color(cx.theme().colors().border) - .child( - v_flex() - .gap_1() - .child( - Label::new(self.component.scope().to_string()) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .child( - h_flex() - .gap_2() - .child( - Headline::new(self.component.scopeless_name()) - .size(HeadlineSize::XLarge), - ) - .children(self.render_component_status(cx)), - ), - ) - .when_some(self.component.description(), |this, description| { - this.child(Label::new(description).size(LabelSize::Small)) - }) - } - - fn render_preview(&self, window: &mut Window, cx: &mut App) -> impl IntoElement { - let content = if let Some(preview) = self.component.preview() { - // Fall back to component preview - preview(window, cx).unwrap_or_else(|| { - div() - .child("Failed to load preview. This path should be unreachable") - .into_any_element() - }) - } else { - div().child("No preview available").into_any_element() - }; - - v_flex() - .id(("component-preview", self.reset_key)) - .size_full() - .flex_1() - .px_12() - .py_6() - .bg(cx.theme().colors().editor_background) - .child(content) - } -} - -impl RenderOnce for ComponentPreviewPage { - fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { - v_flex() - .size_full() - .flex_1() - .overflow_x_hidden() - .child(self.render_header(window, cx)) - .child(self.render_preview(window, cx)) - } -} diff --git a/crates/zed/src/zed/component_preview/persistence.rs b/crates/zed/src/zed/component_preview/persistence.rs deleted file mode 100644 index c37a4cc389..0000000000 --- a/crates/zed/src/zed/component_preview/persistence.rs +++ /dev/null @@ -1,59 +0,0 @@ -use anyhow::Result; -use db::{ - query, - sqlez::{domain::Domain, statement::Statement, thread_safe_connection::ThreadSafeConnection}, - 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!( - CREATE TABLE component_previews ( - workspace_id INTEGER, - item_id INTEGER UNIQUE, - active_page_id TEXT, - PRIMARY KEY(workspace_id, item_id), - 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, - item_id: ItemId, - workspace_id: WorkspaceId, - active_page_id: String, - ) -> Result<()> { - log::debug!( - "Saving active page: item_id={item_id:?}, workspace_id={workspace_id:?}, active_page_id={active_page_id}" - ); - let query = "INSERT INTO component_previews(item_id, workspace_id, active_page_id) - VALUES (?1, ?2, ?3) - ON CONFLICT DO UPDATE SET - active_page_id = ?3"; - self.write(move |conn| { - let mut statement = Statement::prepare(conn, query)?; - let mut next_index = statement.bind(&item_id, 1)?; - next_index = statement.bind(&workspace_id, next_index)?; - statement.bind(&active_page_id, next_index)?; - statement.exec() - }) - .await - } - - query! { - pub fn get_active_page(item_id: ItemId, workspace_id: WorkspaceId) -> Result> { - SELECT active_page_id - FROM component_previews - WHERE item_id = ? AND workspace_id = ? - } - } -} diff --git a/crates/zed/src/zed/edit_prediction_registry.rs b/crates/zed/src/zed/edit_prediction_registry.rs deleted file mode 100644 index 77a1f71596..0000000000 --- a/crates/zed/src/zed/edit_prediction_registry.rs +++ /dev/null @@ -1,256 +0,0 @@ -use client::{Client, UserStore}; -use codestral::CodestralEditPredictionDelegate; -use collections::HashMap; -use copilot::{Copilot, CopilotEditPredictionDelegate}; -use edit_prediction::{SweepFeatureFlag, ZedEditPredictionDelegate, Zeta2FeatureFlag}; -use editor::Editor; -use feature_flags::FeatureFlagAppExt; -use gpui::{AnyWindowHandle, App, AppContext as _, Context, Entity, WeakEntity}; -use language::language_settings::{EditPredictionProvider, all_language_settings}; -use language_models::MistralLanguageModelProvider; -use settings::{ - EXPERIMENTAL_MERCURY_EDIT_PREDICTION_PROVIDER_NAME, - EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME, - EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME, SettingsStore, -}; -use std::{cell::RefCell, rc::Rc, sync::Arc}; -use supermaven::{Supermaven, SupermavenEditPredictionDelegate}; -use ui::Window; - -pub fn init(client: Arc, user_store: Entity, cx: &mut App) { - let editors: Rc, AnyWindowHandle>>> = Rc::default(); - cx.observe_new({ - let editors = editors.clone(); - let client = client.clone(); - let user_store = user_store.clone(); - move |editor: &mut Editor, window, cx: &mut Context| { - if !editor.mode().is_full() { - return; - } - - register_backward_compatible_actions(editor, cx); - - let Some(window) = window else { - return; - }; - - let editor_handle = cx.entity().downgrade(); - cx.on_release({ - let editor_handle = editor_handle.clone(); - let editors = editors.clone(); - move |_, _| { - editors.borrow_mut().remove(&editor_handle); - } - }) - .detach(); - - editors - .borrow_mut() - .insert(editor_handle, window.window_handle()); - let provider = all_language_settings(None, cx).edit_predictions.provider; - assign_edit_prediction_provider( - editor, - provider, - &client, - user_store.clone(), - window, - cx, - ); - } - }) - .detach(); - - cx.on_action(clear_edit_prediction_store_edit_history); - - let mut provider = all_language_settings(None, cx).edit_predictions.provider; - cx.subscribe(&user_store, { - let editors = editors.clone(); - let client = client.clone(); - - move |user_store, event, cx| { - if let client::user::Event::PrivateUserInfoUpdated = event { - assign_edit_prediction_providers(&editors, provider, &client, user_store, cx); - } - } - }) - .detach(); - - cx.observe_global::({ - let user_store = user_store.clone(); - move |cx| { - let new_provider = all_language_settings(None, cx).edit_predictions.provider; - - if new_provider != provider { - telemetry::event!( - "Edit Prediction Provider Changed", - from = provider, - to = new_provider, - ); - - provider = new_provider; - assign_edit_prediction_providers( - &editors, - provider, - &client, - user_store.clone(), - cx, - ); - } - } - }) - .detach(); -} - -fn clear_edit_prediction_store_edit_history(_: &edit_prediction::ClearHistory, cx: &mut App) { - if let Some(ep_store) = edit_prediction::EditPredictionStore::try_global(cx) { - ep_store.update(cx, |ep_store, _| ep_store.clear_history()); - } -} - -fn assign_edit_prediction_providers( - editors: &Rc, AnyWindowHandle>>>, - provider: EditPredictionProvider, - client: &Arc, - user_store: Entity, - cx: &mut App, -) { - if provider == EditPredictionProvider::Codestral { - let mistral = MistralLanguageModelProvider::global(client.http_client(), cx); - mistral.load_codestral_api_key(cx).detach(); - } - for (editor, window) in editors.borrow().iter() { - _ = window.update(cx, |_window, window, cx| { - _ = editor.update(cx, |editor, cx| { - assign_edit_prediction_provider( - editor, - provider, - client, - user_store.clone(), - window, - cx, - ); - }) - }); - } -} - -fn register_backward_compatible_actions(editor: &mut Editor, cx: &mut Context) { - // We renamed some of these actions to not be copilot-specific, but that - // would have not been backwards-compatible. So here we are re-registering - // the actions with the old names to not break people's keymaps. - editor - .register_action(cx.listener( - |editor, _: &copilot::Suggest, window: &mut Window, cx: &mut Context| { - editor.show_edit_prediction(&Default::default(), window, cx); - }, - )) - .detach(); - editor - .register_action(cx.listener( - |editor, _: &copilot::NextSuggestion, window: &mut Window, cx: &mut Context| { - editor.next_edit_prediction(&Default::default(), window, cx); - }, - )) - .detach(); - editor - .register_action(cx.listener( - |editor, - _: &copilot::PreviousSuggestion, - window: &mut Window, - cx: &mut Context| { - editor.previous_edit_prediction(&Default::default(), window, cx); - }, - )) - .detach(); -} - -fn assign_edit_prediction_provider( - editor: &mut Editor, - provider: EditPredictionProvider, - client: &Arc, - user_store: Entity, - window: &mut Window, - cx: &mut Context, -) { - // TODO: Do we really want to collect data only for singleton buffers? - let singleton_buffer = editor.buffer().read(cx).as_singleton(); - - match provider { - EditPredictionProvider::None => { - editor.set_edit_prediction_provider::(None, window, cx); - } - EditPredictionProvider::Copilot => { - if let Some(copilot) = Copilot::global(cx) { - if let Some(buffer) = singleton_buffer - && buffer.read(cx).file().is_some() - { - copilot.update(cx, |copilot, cx| { - copilot.register_buffer(&buffer, cx); - }); - } - let provider = cx.new(|_| CopilotEditPredictionDelegate::new(copilot)); - editor.set_edit_prediction_provider(Some(provider), window, cx); - } - } - EditPredictionProvider::Supermaven => { - if let Some(supermaven) = Supermaven::global(cx) { - let provider = cx.new(|_| SupermavenEditPredictionDelegate::new(supermaven)); - editor.set_edit_prediction_provider(Some(provider), window, cx); - } - } - EditPredictionProvider::Codestral => { - let http_client = client.http_client(); - let provider = cx.new(|_| CodestralEditPredictionDelegate::new(http_client)); - editor.set_edit_prediction_provider(Some(provider), window, cx); - } - value @ (EditPredictionProvider::Experimental(_) | EditPredictionProvider::Zed) => { - let ep_store = edit_prediction::EditPredictionStore::global(client, &user_store, cx); - - if let Some(project) = editor.project() - && let Some(buffer) = &singleton_buffer - && buffer.read(cx).file().is_some() - { - let has_model = ep_store.update(cx, |ep_store, cx| { - let model = if let EditPredictionProvider::Experimental(name) = value { - if name == EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME - && cx.has_flag::() - { - edit_prediction::EditPredictionModel::Sweep - } else if name == EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME - && cx.has_flag::() - { - edit_prediction::EditPredictionModel::Zeta2 - } else if name == EXPERIMENTAL_MERCURY_EDIT_PREDICTION_PROVIDER_NAME - && cx.has_flag::() - { - edit_prediction::EditPredictionModel::Mercury - } else { - return false; - } - } else if user_store.read(cx).current_user().is_some() { - edit_prediction::EditPredictionModel::Zeta1 - } else { - return false; - }; - - ep_store.set_edit_prediction_model(model); - ep_store.register_buffer(buffer, project, cx); - true - }); - - if has_model { - let provider = cx.new(|cx| { - ZedEditPredictionDelegate::new( - project.clone(), - singleton_buffer, - &client, - &user_store, - cx, - ) - }); - editor.set_edit_prediction_provider(Some(provider), window, cx); - } - } - } - } -} diff --git a/crates/zed/src/zed/mac_only_instance.rs b/crates/zed/src/zed/mac_only_instance.rs deleted file mode 100644 index b7898fae17..0000000000 --- a/crates/zed/src/zed/mac_only_instance.rs +++ /dev/null @@ -1,151 +0,0 @@ -use std::{ - io::{Read, Write}, - net::{Ipv4Addr, SocketAddr, SocketAddrV4, TcpListener, TcpStream}, - thread, - time::Duration, -}; - -use sysinfo::System; - -use release_channel::ReleaseChannel; - -const LOCALHOST: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1); -const CONNECT_TIMEOUT: Duration = Duration::from_millis(10); -const RECEIVE_TIMEOUT: Duration = Duration::from_millis(35); -const SEND_TIMEOUT: Duration = Duration::from_millis(20); -const USER_BLOCK: u16 = 100; - -fn address() -> SocketAddr { - // These port numbers are offset by the user ID to avoid conflicts between - // different users on the same machine. In addition to that the ports for each - // release channel are spaced out by 100 to avoid conflicts between different - // users running different release channels on the same machine. This ends up - // interleaving the ports between different users and different release channels. - // - // On macOS user IDs start at 501 and on Linux they start at 1000. The first user - // on a Mac with ID 501 running a dev channel build will use port 44238, and the - // second user with ID 502 will use port 44239, and so on. User 501 will use ports - // 44338, 44438, and 44538 for the preview, stable, and nightly channels, - // respectively. User 502 will use ports 44339, 44439, and 44539 for the preview, - // stable, and nightly channels, respectively. - let port = match *release_channel::RELEASE_CHANNEL { - ReleaseChannel::Dev => 43737, - ReleaseChannel::Preview => 43737 + USER_BLOCK, - ReleaseChannel::Stable => 43737 + (2 * USER_BLOCK), - ReleaseChannel::Nightly => 43737 + (3 * USER_BLOCK), - }; - let mut user_port = port; - let mut sys = System::new_all(); - sys.refresh_all(); - if let Ok(current_pid) = sysinfo::get_current_pid() - && let Some(uid) = sys - .process(current_pid) - .and_then(|process| process.user_id()) - { - let uid_u32 = get_uid_as_u32(uid); - // Ensure that the user ID is not too large to avoid overflow when - // calculating the port number. This seems unlikely but it doesn't - // hurt to be safe. - let max_port = 65535; - let max_uid: u32 = max_port - port as u32; - let wrapped_uid: u16 = (uid_u32 % max_uid) as u16; - user_port += wrapped_uid; - } - - SocketAddr::V4(SocketAddrV4::new(LOCALHOST, user_port)) -} - -#[cfg(unix)] -fn get_uid_as_u32(uid: &sysinfo::Uid) -> u32 { - *uid.clone() -} - -#[cfg(windows)] -fn get_uid_as_u32(uid: &sysinfo::Uid) -> u32 { - // Extract the RID which is an integer - uid.to_string() - .rsplit('-') - .next() - .and_then(|rid| rid.parse::().ok()) - .unwrap_or(0) -} - -fn instance_handshake() -> &'static str { - match *release_channel::RELEASE_CHANNEL { - ReleaseChannel::Dev => "Zed Editor Dev Instance Running", - ReleaseChannel::Nightly => "Zed Editor Nightly Instance Running", - ReleaseChannel::Preview => "Zed Editor Preview Instance Running", - ReleaseChannel::Stable => "Zed Editor Stable Instance Running", - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum IsOnlyInstance { - Yes, - No, -} - -pub fn ensure_only_instance() -> IsOnlyInstance { - if check_got_handshake() { - return IsOnlyInstance::No; - } - - let listener = match TcpListener::bind(address()) { - Ok(listener) => listener, - - Err(err) => { - log::warn!("Error binding to single instance port: {err}"); - if check_got_handshake() { - return IsOnlyInstance::No; - } - - // Avoid failing to start when some other application by chance already has - // a claim on the port. This is sub-par as any other instance that gets launched - // will be unable to communicate with this instance and will duplicate - log::warn!("Backup handshake request failed, continuing without handshake"); - return IsOnlyInstance::Yes; - } - }; - - thread::Builder::new() - .name("EnsureSingleton".to_string()) - .spawn(move || { - for stream in listener.incoming() { - let mut stream = match stream { - Ok(stream) => stream, - Err(_) => return, - }; - - _ = stream.set_nodelay(true); - _ = stream.set_read_timeout(Some(SEND_TIMEOUT)); - _ = stream.write_all(instance_handshake().as_bytes()); - } - }) - .unwrap(); - - IsOnlyInstance::Yes -} - -fn check_got_handshake() -> bool { - match TcpStream::connect_timeout(&address(), CONNECT_TIMEOUT) { - Ok(mut stream) => { - let mut buf = vec![0u8; instance_handshake().len()]; - - stream.set_read_timeout(Some(RECEIVE_TIMEOUT)).unwrap(); - if let Err(err) = stream.read_exact(&mut buf) { - log::warn!("Connected to single instance port but failed to read: {err}"); - return false; - } - - if buf == instance_handshake().as_bytes() { - log::info!("Got instance handshake"); - return true; - } - - log::warn!("Got wrong instance handshake value"); - false - } - - Err(_) => false, - } -} diff --git a/crates/zed/src/zed/migrate.rs b/crates/zed/src/zed/migrate.rs deleted file mode 100644 index 2452f17d04..0000000000 --- a/crates/zed/src/zed/migrate.rs +++ /dev/null @@ -1,323 +0,0 @@ -use anyhow::{Context as _, Result}; -use editor::Editor; -use fs::Fs; -use migrator::{migrate_keymap, migrate_settings}; -use settings::{KeymapFile, Settings, SettingsStore}; -use util::ResultExt; -use workspace::notifications::NotifyTaskExt; - -use std::sync::Arc; - -use gpui::{Entity, EventEmitter, Global, Task, TextStyle, TextStyleRefinement}; -use markdown::{Markdown, MarkdownElement, MarkdownStyle}; -use theme::ThemeSettings; -use ui::prelude::*; -use workspace::item::ItemHandle; -use workspace::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace}; - -#[derive(Debug, Copy, Clone, PartialEq)] -pub enum MigrationType { - Keymap, - Settings, -} - -pub struct MigrationBanner { - migration_type: Option, - should_migrate_task: Option>, - markdown: Option>, -} - -pub enum MigrationEvent { - ContentChanged { - migration_type: MigrationType, - migrating_in_memory: bool, - }, -} - -pub struct MigrationNotification; - -impl EventEmitter for MigrationNotification {} - -impl MigrationNotification { - pub fn try_global(cx: &App) -> Option> { - cx.try_global::() - .map(|notifier| notifier.0.clone()) - } - - pub fn set_global(notifier: Entity, cx: &mut App) { - cx.set_global(GlobalMigrationNotification(notifier)); - } -} - -struct GlobalMigrationNotification(Entity); - -impl Global for GlobalMigrationNotification {} - -impl MigrationBanner { - pub fn new(_: &Workspace, cx: &mut Context) -> Self { - if let Some(notifier) = MigrationNotification::try_global(cx) { - cx.subscribe( - ¬ifier, - move |migrator_banner, _, event: &MigrationEvent, cx| { - migrator_banner.handle_notification(event, cx); - }, - ) - .detach(); - } - Self { - migration_type: None, - should_migrate_task: None, - markdown: None, - } - } - - fn handle_notification(&mut self, event: &MigrationEvent, cx: &mut Context) { - match event { - MigrationEvent::ContentChanged { - migration_type, - migrating_in_memory, - } => { - if *migrating_in_memory { - self.migration_type = Some(*migration_type); - self.show(cx); - } else { - cx.emit(ToolbarItemEvent::ChangeLocation( - ToolbarItemLocation::Hidden, - )); - self.reset(cx); - }; - } - } - } - - fn show(&mut self, cx: &mut Context) { - let (file_type, backup_file_name) = match self.migration_type { - Some(MigrationType::Keymap) => ( - "keymap", - paths::keymap_backup_file() - .file_name() - .unwrap_or_default() - .to_string_lossy() - .into_owned(), - ), - Some(MigrationType::Settings) => ( - "settings", - paths::settings_backup_file() - .file_name() - .unwrap_or_default() - .to_string_lossy() - .into_owned(), - ), - None => return, - }; - - let migration_text = format!( - "Your {} file uses deprecated settings which can be \ - automatically updated. A backup will be saved to `{}`", - file_type, backup_file_name - ); - - self.markdown = Some(cx.new(|cx| Markdown::new(migration_text.into(), None, None, cx))); - - cx.emit(ToolbarItemEvent::ChangeLocation( - ToolbarItemLocation::Secondary, - )); - cx.notify(); - } - - fn reset(&mut self, cx: &mut Context) { - self.should_migrate_task.take(); - self.migration_type.take(); - self.markdown.take(); - cx.notify(); - } -} - -impl EventEmitter for MigrationBanner {} - -impl ToolbarItemView for MigrationBanner { - fn set_active_pane_item( - &mut self, - active_pane_item: Option<&dyn ItemHandle>, - window: &mut Window, - cx: &mut Context, - ) -> ToolbarItemLocation { - self.reset(cx); - - let Some(target) = active_pane_item - .and_then(|item| item.act_as::(cx)) - .and_then(|editor| editor.update(cx, |editor, cx| editor.target_file_abs_path(cx))) - else { - return ToolbarItemLocation::Hidden; - }; - - if &target == paths::keymap_file() { - self.migration_type = Some(MigrationType::Keymap); - let fs = ::global(cx); - let should_migrate = cx.background_spawn(should_migrate_keymap(fs)); - self.should_migrate_task = Some(cx.spawn_in(window, async move |this, cx| { - if let Ok(true) = should_migrate.await { - this.update(cx, |this, cx| { - this.show(cx); - }) - .log_err(); - } - })); - } else if &target == paths::settings_file() { - self.migration_type = Some(MigrationType::Settings); - let fs = ::global(cx); - let should_migrate = cx.background_spawn(should_migrate_settings(fs)); - self.should_migrate_task = Some(cx.spawn_in(window, async move |this, cx| { - if let Ok(true) = should_migrate.await { - this.update(cx, |this, cx| { - this.show(cx); - }) - .log_err(); - } - })); - } - - ToolbarItemLocation::Hidden - } -} - -impl Render for MigrationBanner { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { - let migration_type = self.migration_type; - let settings = ThemeSettings::get_global(cx); - let ui_font_family = settings.ui_font.family.clone(); - let line_height = settings.ui_font_size(cx) * 1.3; - h_flex() - .py_1() - .pl_2() - .pr_1() - .justify_between() - .bg(cx.theme().status().info_background.opacity(0.6)) - .border_1() - .border_color(cx.theme().colors().border_variant) - .rounded_sm() - .child( - h_flex() - .gap_2() - .overflow_hidden() - .child( - Icon::new(IconName::Warning) - .size(IconSize::XSmall) - .color(Color::Warning), - ) - .child( - div() - .overflow_hidden() - .text_size(TextSize::Default.rems(cx)) - .max_h(2 * line_height) - .when_some(self.markdown.as_ref(), |this, markdown| { - this.child( - MarkdownElement::new( - markdown.clone(), - MarkdownStyle { - base_text_style: TextStyle { - color: cx.theme().colors().text, - font_family: ui_font_family, - ..Default::default() - }, - inline_code: TextStyleRefinement { - background_color: Some( - cx.theme().colors().background, - ), - ..Default::default() - }, - ..Default::default() - }, - ) - .into_any_element(), - ) - }), - ), - ) - .child( - Button::new("backup-and-migrate", "Backup and Update").on_click( - move |_, window, cx| { - let fs = ::global(cx); - match migration_type { - Some(MigrationType::Keymap) => { - cx.background_spawn(write_keymap_migration(fs.clone())) - .detach_and_notify_err(window, cx); - } - Some(MigrationType::Settings) => { - cx.background_spawn(write_settings_migration(fs.clone())) - .detach_and_notify_err(window, cx); - } - None => unreachable!(), - } - }, - ), - ) - .into_any_element() - } -} - -async fn should_migrate_keymap(fs: Arc) -> Result { - let old_text = KeymapFile::load_keymap_file(&fs).await?; - if let Ok(Some(_)) = migrate_keymap(&old_text) { - return Ok(true); - }; - Ok(false) -} - -async fn should_migrate_settings(fs: Arc) -> Result { - let old_text = SettingsStore::load_settings(&fs).await?; - if let Ok(Some(_)) = migrate_settings(&old_text) { - return Ok(true); - }; - Ok(false) -} - -async fn write_keymap_migration(fs: Arc) -> Result<()> { - let old_text = KeymapFile::load_keymap_file(&fs).await?; - let Ok(Some(new_text)) = migrate_keymap(&old_text) else { - return Ok(()); - }; - let keymap_path = paths::keymap_file().as_path(); - if fs.is_file(keymap_path).await { - fs.atomic_write(paths::keymap_backup_file().to_path_buf(), old_text) - .await - .with_context(|| "Failed to create settings backup in home directory".to_string())?; - let resolved_path = fs - .canonicalize(keymap_path) - .await - .with_context(|| format!("Failed to canonicalize keymap path {:?}", keymap_path))?; - fs.atomic_write(resolved_path.clone(), new_text) - .await - .with_context(|| format!("Failed to write keymap to file {:?}", resolved_path))?; - } else { - fs.atomic_write(keymap_path.to_path_buf(), new_text) - .await - .with_context(|| format!("Failed to write keymap to file {:?}", keymap_path))?; - } - Ok(()) -} - -async fn write_settings_migration(fs: Arc) -> Result<()> { - let old_text = SettingsStore::load_settings(&fs).await?; - let Ok(Some(new_text)) = migrate_settings(&old_text) else { - return Ok(()); - }; - let settings_path = paths::settings_file().as_path(); - if fs.is_file(settings_path).await { - fs.atomic_write(paths::settings_backup_file().to_path_buf(), old_text) - .await - .with_context(|| "Failed to create settings backup in home directory".to_string())?; - let resolved_path = fs - .canonicalize(settings_path) - .await - .with_context(|| format!("Failed to canonicalize settings path {:?}", settings_path))?; - fs.atomic_write(resolved_path.clone(), new_text) - .await - .with_context(|| format!("Failed to write settings to file {:?}", resolved_path))?; - } else { - fs.atomic_write(settings_path.to_path_buf(), new_text) - .await - .with_context(|| format!("Failed to write settings to file {:?}", settings_path))?; - } - Ok(()) -} diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs deleted file mode 100644 index 5e855aa5a9..0000000000 --- a/crates/zed/src/zed/open_listener.rs +++ /dev/null @@ -1,932 +0,0 @@ -use crate::handle_open_request; -use crate::restorable_workspace_locations; -use anyhow::{Context as _, Result, anyhow}; -use cli::{CliRequest, CliResponse, ipc::IpcSender}; -use cli::{IpcHandshake, ipc}; -use client::parse_zed_link; -use collections::HashMap; -use db::kvp::KEY_VALUE_STORE; -use editor::Editor; -use fs::Fs; -use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; -use futures::channel::{mpsc, oneshot}; -use futures::future::join_all; -use futures::{FutureExt, SinkExt, StreamExt}; -use git_ui::file_diff_view::FileDiffView; -use gpui::{App, AsyncApp, Global, WindowHandle}; -use language::Point; -use onboarding::FIRST_OPEN; -use onboarding::show_onboarding_view; -use recent_projects::{SshSettings, open_remote_project}; -use remote::{RemoteConnectionOptions, WslConnectionOptions}; -use settings::Settings; -use std::path::{Path, PathBuf}; -use std::sync::Arc; -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}; - -#[derive(Default, Debug)] -pub struct OpenRequest { - pub kind: Option, - pub open_paths: Vec, - pub diff_paths: Vec<[String; 2]>, - pub open_channel_notes: Vec<(u64, Option)>, - pub join_channel: Option, - pub remote_connection: Option, -} - -#[derive(Debug)] -pub enum OpenRequestKind { - CliConnection((mpsc::Receiver, IpcSender)), - Extension { - extension_id: String, - }, - AgentPanel, - DockMenuAction { - index: usize, - }, - BuiltinJsonSchema { - schema_path: String, - }, - Setting { - /// `None` opens settings without navigating to a specific path. - setting_path: Option, - }, -} - -impl OpenRequest { - pub fn parse(request: RawOpenRequest, cx: &App) -> Result { - let mut this = Self::default(); - - this.diff_paths = request.diff_paths; - if let Some(wsl) = request.wsl { - let (user, distro_name) = if let Some((user, distro)) = wsl.split_once('@') { - if user.is_empty() { - anyhow::bail!("user is empty in wsl argument"); - } - (Some(user.to_string()), distro.to_string()) - } else { - (None, wsl) - }; - this.remote_connection = Some(RemoteConnectionOptions::Wsl(WslConnectionOptions { - distro_name, - user, - })); - } - - for url in request.urls { - if let Some(server_name) = url.strip_prefix("zed-cli://") { - this.kind = Some(OpenRequestKind::CliConnection(connect_to_cli(server_name)?)); - } else if let Some(action_index) = url.strip_prefix("zed-dock-action://") { - this.kind = Some(OpenRequestKind::DockMenuAction { - index: action_index.parse()?, - }); - } else if let Some(file) = url.strip_prefix("file://") { - this.parse_file_path(file) - } else if let Some(file) = url.strip_prefix("zed://file") { - this.parse_file_path(file) - } else if let Some(file) = url.strip_prefix("zed://ssh") { - let ssh_url = "ssh:/".to_string() + file; - this.parse_ssh_file_path(&ssh_url, cx)? - } else if let Some(extension_id) = url.strip_prefix("zed://extension/") { - this.kind = Some(OpenRequestKind::Extension { - extension_id: extension_id.to_string(), - }); - } else if url == "zed://agent" { - this.kind = Some(OpenRequestKind::AgentPanel); - } else if let Some(schema_path) = url.strip_prefix("zed://schemas/") { - this.kind = Some(OpenRequestKind::BuiltinJsonSchema { - schema_path: schema_path.to_string(), - }); - } else if url == "zed://settings" || url == "zed://settings/" { - this.kind = Some(OpenRequestKind::Setting { setting_path: None }); - } else if let Some(setting_path) = url.strip_prefix("zed://settings/") { - this.kind = Some(OpenRequestKind::Setting { - setting_path: Some(setting_path.to_string()), - }); - } else if url.starts_with("ssh://") { - this.parse_ssh_file_path(&url, cx)? - } else if let Some(request_path) = parse_zed_link(&url, cx) { - this.parse_request_path(request_path).log_err(); - } else { - log::error!("unhandled url: {}", url); - } - } - - Ok(this) - } - - fn parse_file_path(&mut self, file: &str) { - if let Some(decoded) = urlencoding::decode(file).log_err() { - self.open_paths.push(decoded.into_owned()) - } - } - - fn parse_ssh_file_path(&mut self, file: &str, cx: &App) -> Result<()> { - let url = url::Url::parse(file)?; - let host = url - .host() - .with_context(|| format!("missing host in ssh url: {file}"))? - .to_string(); - let username = Some(url.username().to_string()).filter(|s| !s.is_empty()); - let port = url.port(); - anyhow::ensure!( - self.open_paths.is_empty(), - "cannot open both local and ssh paths" - ); - let mut connection_options = - SshSettings::get_global(cx).connection_options_for(host, port, username); - if let Some(password) = url.password() { - connection_options.password = Some(password.to_string()); - } - - let connection_options = RemoteConnectionOptions::Ssh(connection_options); - if let Some(ssh_connection) = &self.remote_connection { - anyhow::ensure!( - *ssh_connection == connection_options, - "cannot open multiple different remote connections" - ); - } - self.remote_connection = Some(connection_options); - self.parse_file_path(url.path()); - Ok(()) - } - - fn parse_request_path(&mut self, request_path: &str) -> Result<()> { - let mut parts = request_path.split('/'); - if parts.next() == Some("channel") - && let Some(slug) = parts.next() - && let Some(id_str) = slug.split('-').next_back() - && let Ok(channel_id) = id_str.parse::() - { - let Some(next) = parts.next() else { - self.join_channel = Some(channel_id); - return Ok(()); - }; - - if let Some(heading) = next.strip_prefix("notes#") { - self.open_channel_notes - .push((channel_id, Some(heading.to_string()))); - return Ok(()); - } - if next == "notes" { - self.open_channel_notes.push((channel_id, None)); - return Ok(()); - } - } - anyhow::bail!("invalid zed url: {request_path}") - } -} - -#[derive(Clone)] -pub struct OpenListener(UnboundedSender); - -#[derive(Default)] -pub struct RawOpenRequest { - pub urls: Vec, - pub diff_paths: Vec<[String; 2]>, - pub wsl: Option, -} - -impl Global for OpenListener {} - -impl OpenListener { - pub fn new() -> (Self, UnboundedReceiver) { - let (tx, rx) = mpsc::unbounded(); - (OpenListener(tx), rx) - } - - pub fn open(&self, request: RawOpenRequest) { - self.0 - .unbounded_send(request) - .context("no listener for open requests") - .log_err(); - } -} - -#[cfg(any(target_os = "linux", target_os = "freebsd"))] -pub fn listen_for_cli_connections(opener: OpenListener) -> Result<()> { - use release_channel::RELEASE_CHANNEL_NAME; - use std::os::unix::net::UnixDatagram; - - let sock_path = paths::data_dir().join(format!("zed-{}.sock", *RELEASE_CHANNEL_NAME)); - // remove the socket if the process listening on it has died - if let Err(e) = UnixDatagram::unbound()?.connect(&sock_path) - && e.kind() == std::io::ErrorKind::ConnectionRefused - { - std::fs::remove_file(&sock_path)?; - } - let listener = UnixDatagram::bind(&sock_path)?; - thread::spawn(move || { - let mut buf = [0u8; 1024]; - while let Ok(len) = listener.recv(&mut buf) { - opener.open(RawOpenRequest { - urls: vec![String::from_utf8_lossy(&buf[..len]).to_string()], - ..Default::default() - }); - } - }); - Ok(()) -} - -fn connect_to_cli( - server_name: &str, -) -> Result<(mpsc::Receiver, IpcSender)> { - let handshake_tx = cli::ipc::IpcSender::::connect(server_name.to_string()) - .context("error connecting to cli")?; - let (request_tx, request_rx) = ipc::channel::()?; - let (response_tx, response_rx) = ipc::channel::()?; - - handshake_tx - .send(IpcHandshake { - requests: request_tx, - responses: response_rx, - }) - .context("error sending ipc handshake")?; - - let (mut async_request_tx, async_request_rx) = - futures::channel::mpsc::channel::(16); - thread::spawn(move || { - while let Ok(cli_request) = request_rx.recv() { - if smol::block_on(async_request_tx.send(cli_request)).is_err() { - break; - } - } - anyhow::Ok(()) - }); - - Ok((async_request_rx, response_tx)) -} - -pub async fn open_paths_with_positions( - path_positions: &[PathWithPosition], - diff_paths: &[[String; 2]], - app_state: Arc, - open_options: workspace::OpenOptions, - cx: &mut AsyncApp, -) -> Result<( - WindowHandle, - Vec>>>, -)> { - let mut caret_positions = HashMap::default(); - - let paths = path_positions - .iter() - .map(|path_with_position| { - let path = path_with_position.path.clone(); - if let Some(row) = path_with_position.row - && path.is_file() - { - let row = row.saturating_sub(1); - let col = path_with_position.column.unwrap_or(0).saturating_sub(1); - caret_positions.insert(path.clone(), Point::new(row, col)); - } - path - }) - .collect::>(); - - let (workspace, mut items) = cx - .update(|cx| workspace::open_paths(&paths, app_state, open_options, cx))? - .await?; - - for diff_pair in diff_paths { - let old_path = Path::new(&diff_pair[0]).canonicalize()?; - let new_path = Path::new(&diff_pair[1]).canonicalize()?; - if let Ok(diff_view) = workspace.update(cx, |workspace, window, cx| { - FileDiffView::open(old_path, new_path, workspace, window, cx) - }) && let Some(diff_view) = diff_view.await.log_err() - { - items.push(Some(Ok(Box::new(diff_view)))) - } - } - - for (item, path) in items.iter_mut().zip(&paths) { - if let Some(Err(error)) = item { - *error = anyhow!("error opening {path:?}: {error}"); - continue; - } - let Some(Ok(item)) = item else { - continue; - }; - let Some(point) = caret_positions.remove(path) else { - continue; - }; - if let Some(active_editor) = item.downcast::() { - workspace - .update(cx, |_, window, cx| { - active_editor.update(cx, |editor, cx| { - editor.go_to_singleton_buffer_point(point, window, cx); - }); - }) - .log_err(); - } - } - - Ok((workspace, items)) -} - -pub async fn handle_cli_connection( - (mut requests, responses): (mpsc::Receiver, IpcSender), - app_state: Arc, - cx: &mut AsyncApp, -) { - if let Some(request) = requests.next().await { - match request { - CliRequest::Open { - urls, - paths, - diff_paths, - wait, - wsl, - open_new_workspace, - reuse, - env, - user_data_dir: _, - } => { - if !urls.is_empty() { - cx.update(|cx| { - match OpenRequest::parse( - RawOpenRequest { - urls, - diff_paths, - wsl, - }, - cx, - ) { - Ok(open_request) => { - handle_open_request(open_request, app_state.clone(), cx); - responses.send(CliResponse::Exit { status: 0 }).log_err(); - } - Err(e) => { - responses - .send(CliResponse::Stderr { - message: format!("{e}"), - }) - .log_err(); - responses.send(CliResponse::Exit { status: 1 }).log_err(); - } - }; - }) - .log_err(); - return; - } - - let open_workspace_result = open_workspaces( - paths, - diff_paths, - open_new_workspace, - reuse, - &responses, - wait, - app_state.clone(), - env, - cx, - ) - .await; - - let status = if open_workspace_result.is_err() { 1 } else { 0 }; - responses.send(CliResponse::Exit { status }).log_err(); - } - } - } -} - -async fn open_workspaces( - paths: Vec, - diff_paths: Vec<[String; 2]>, - open_new_workspace: Option, - reuse: bool, - responses: &IpcSender, - wait: bool, - app_state: Arc, - env: Option>, - cx: &mut AsyncApp, -) -> Result<()> { - let grouped_locations = if paths.is_empty() && diff_paths.is_empty() { - // If no paths are provided, restore from previous workspaces unless a new workspace is requested with -n - if open_new_workspace == Some(true) { - Vec::new() - } else { - restorable_workspace_locations(cx, &app_state) - .await - .unwrap_or_default() - } - } else { - vec![( - SerializedWorkspaceLocation::Local, - PathList::new(&paths.into_iter().map(PathBuf::from).collect::>()), - )] - }; - - if grouped_locations.is_empty() { - // If we have no paths to open, show the welcome screen if this is the first launch - if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) { - cx.update(|cx| show_onboarding_view(app_state, cx).detach()) - .log_err(); - } - // If not the first launch, show an empty window with empty editor - else { - cx.update(|cx| { - let open_options = OpenOptions { - env, - ..Default::default() - }; - workspace::open_new(open_options, app_state, cx, |workspace, window, cx| { - Editor::new_file(workspace, &Default::default(), window, cx) - }) - .detach(); - }) - .log_err(); - } - } else { - // 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 { - match location { - SerializedWorkspaceLocation::Local => { - let workspace_paths = workspace_paths - .paths() - .iter() - .map(|path| path.to_string_lossy().into_owned()) - .collect(); - - let workspace_failed_to_open = open_local_workspace( - workspace_paths, - diff_paths.clone(), - open_new_workspace, - reuse, - wait, - responses, - env.as_ref(), - &app_state, - cx, - ) - .await; - - if workspace_failed_to_open { - errored = true - } - } - SerializedWorkspaceLocation::Remote(mut connection) => { - let app_state = app_state.clone(); - if let RemoteConnectionOptions::Ssh(options) = &mut connection { - cx.update(|cx| { - SshSettings::get_global(cx) - .fill_connection_options_from_settings(options) - })?; - } - cx.spawn(async move |cx| { - open_remote_project( - connection, - workspace_paths.paths().to_vec(), - app_state, - OpenOptions::default(), - cx, - ) - .await - .log_err(); - }) - .detach(); - } - } - } - - anyhow::ensure!(!errored, "failed to open a workspace"); - } - - Ok(()) -} - -async fn open_local_workspace( - workspace_paths: Vec, - diff_paths: Vec<[String; 2]>, - open_new_workspace: Option, - reuse: bool, - wait: bool, - responses: &IpcSender, - env: Option<&HashMap>, - app_state: &Arc, - cx: &mut AsyncApp, -) -> bool { - let mut errored = false; - - let paths_with_position = - derive_paths_with_position(app_state.fs.as_ref(), workspace_paths).await; - - // Handle reuse flag by finding existing window to replace - let replace_window = if reuse { - cx.update(|cx| workspace::local_workspace_windows(cx).into_iter().next()) - .ok() - .flatten() - } else { - None - }; - - // For reuse, force new workspace creation but with replace_window set - let effective_open_new_workspace = if reuse { - Some(true) - } else { - open_new_workspace - }; - - match open_paths_with_positions( - &paths_with_position, - &diff_paths, - app_state.clone(), - workspace::OpenOptions { - open_new_workspace: effective_open_new_workspace, - replace_window, - prefer_focused_window: wait, - env: env.cloned(), - ..Default::default() - }, - cx, - ) - .await - { - Ok((workspace, items)) => { - let mut item_release_futures = Vec::new(); - - for item in items { - match item { - Some(Ok(item)) => { - cx.update(|cx| { - let released = oneshot::channel(); - item.on_release( - cx, - Box::new(move |_| { - let _ = released.0.send(()); - }), - ) - .detach(); - item_release_futures.push(released.1); - }) - .log_err(); - } - Some(Err(err)) => { - responses - .send(CliResponse::Stderr { - message: err.to_string(), - }) - .log_err(); - errored = true; - } - None => {} - } - } - - if wait { - let background = cx.background_executor().clone(); - let wait = async move { - if paths_with_position.is_empty() && diff_paths.is_empty() { - let (done_tx, done_rx) = oneshot::channel(); - let _subscription = workspace.update(cx, |_, _, cx| { - cx.on_release(move |_, _| { - let _ = done_tx.send(()); - }) - }); - let _ = done_rx.await; - } else { - let _ = futures::future::try_join_all(item_release_futures).await; - }; - } - .fuse(); - - futures::pin_mut!(wait); - - loop { - // Repeatedly check if CLI is still open to avoid wasting resources - // waiting for files or workspaces to close. - let mut timer = background.timer(Duration::from_secs(1)).fuse(); - futures::select_biased! { - _ = wait => break, - _ = timer => { - if responses.send(CliResponse::Ping).is_err() { - break; - } - } - } - } - } - } - Err(error) => { - errored = true; - responses - .send(CliResponse::Stderr { - message: format!("error opening {paths_with_position:?}: {error}"), - }) - .log_err(); - } - } - errored -} - -pub async fn derive_paths_with_position( - fs: &dyn Fs, - path_strings: impl IntoIterator>, -) -> Vec { - join_all(path_strings.into_iter().map(|path_str| async move { - let canonicalized = fs.canonicalize(Path::new(path_str.as_ref())).await; - (path_str, canonicalized) - })) - .await - .into_iter() - .map(|(original, canonicalized)| match canonicalized { - Ok(canonicalized) => PathWithPosition::from_path(canonicalized), - Err(_) => PathWithPosition::parse_str(original.as_ref()), - }) - .collect() -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::zed::{open_listener::open_local_workspace, tests::init_test}; - use cli::{ - CliResponse, - ipc::{self}, - }; - use editor::Editor; - use gpui::TestAppContext; - use language::LineEnding; - use remote::SshConnectionOptions; - use rope::Rope; - use serde_json::json; - use std::sync::Arc; - use util::path; - use workspace::{AppState, Workspace}; - - #[gpui::test] - fn test_parse_ssh_url(cx: &mut TestAppContext) { - let _app_state = init_test(cx); - let request = cx.update(|cx| { - OpenRequest::parse( - RawOpenRequest { - urls: vec!["ssh://me@localhost:/".into()], - ..Default::default() - }, - cx, - ) - .unwrap() - }); - assert_eq!( - request.remote_connection.unwrap(), - RemoteConnectionOptions::Ssh(SshConnectionOptions { - host: "localhost".into(), - username: Some("me".into()), - port: None, - password: None, - args: None, - port_forwards: None, - nickname: None, - upload_binary_over_ssh: false, - }) - ); - assert_eq!(request.open_paths, vec!["/"]); - } - - #[gpui::test] - async fn test_open_workspace_with_directory(cx: &mut TestAppContext) { - let app_state = init_test(cx); - - app_state - .fs - .as_fake() - .insert_tree( - path!("/root"), - json!({ - "dir1": { - "file1.txt": "content1", - "file2.txt": "content2", - }, - }), - ) - .await; - - assert_eq!(cx.windows().len(), 0); - - // First open the workspace directory - open_workspace_file(path!("/root/dir1"), None, app_state.clone(), cx).await; - - assert_eq!(cx.windows().len(), 1); - let workspace = cx.windows()[0].downcast::().unwrap(); - workspace - .update(cx, |workspace, _, cx| { - assert!(workspace.active_item_as::(cx).is_none()) - }) - .unwrap(); - - // Now open a file inside that workspace - open_workspace_file(path!("/root/dir1/file1.txt"), None, app_state.clone(), cx).await; - - assert_eq!(cx.windows().len(), 1); - workspace - .update(cx, |workspace, _, cx| { - assert!(workspace.active_item_as::(cx).is_some()); - }) - .unwrap(); - - // Now open a file inside that workspace, but tell Zed to open a new window - open_workspace_file( - path!("/root/dir1/file1.txt"), - Some(true), - app_state.clone(), - cx, - ) - .await; - - assert_eq!(cx.windows().len(), 2); - - let workspace_2 = cx.windows()[1].downcast::().unwrap(); - workspace_2 - .update(cx, |workspace, _, cx| { - assert!(workspace.active_item_as::(cx).is_some()); - let items = workspace.items(cx).collect::>(); - assert_eq!(items.len(), 1, "Workspace should have two items"); - }) - .unwrap(); - } - - #[gpui::test] - async fn test_open_workspace_with_nonexistent_files(cx: &mut TestAppContext) { - let app_state = init_test(cx); - - app_state - .fs - .as_fake() - .insert_tree(path!("/root"), json!({})) - .await; - - assert_eq!(cx.windows().len(), 0); - - // Test case 1: Open a single file that does not exist yet - open_workspace_file(path!("/root/file5.txt"), None, app_state.clone(), cx).await; - - assert_eq!(cx.windows().len(), 1); - let workspace_1 = cx.windows()[0].downcast::().unwrap(); - workspace_1 - .update(cx, |workspace, _, cx| { - assert!(workspace.active_item_as::(cx).is_some()) - }) - .unwrap(); - - // Test case 2: Open a single file that does not exist yet, - // but tell Zed to add it to the current workspace - open_workspace_file(path!("/root/file6.txt"), Some(false), app_state.clone(), cx).await; - - assert_eq!(cx.windows().len(), 1); - workspace_1 - .update(cx, |workspace, _, cx| { - let items = workspace.items(cx).collect::>(); - assert_eq!(items.len(), 2, "Workspace should have two items"); - }) - .unwrap(); - - // Test case 3: Open a single file that does not exist yet, - // but tell Zed to NOT add it to the current workspace - open_workspace_file(path!("/root/file7.txt"), Some(true), app_state.clone(), cx).await; - - assert_eq!(cx.windows().len(), 2); - let workspace_2 = cx.windows()[1].downcast::().unwrap(); - workspace_2 - .update(cx, |workspace, _, cx| { - let items = workspace.items(cx).collect::>(); - assert_eq!(items.len(), 1, "Workspace should have two items"); - }) - .unwrap(); - } - - async fn open_workspace_file( - path: &str, - open_new_workspace: Option, - app_state: Arc, - cx: &TestAppContext, - ) { - let (response_tx, _) = ipc::channel::().unwrap(); - - let workspace_paths = vec![path.to_owned()]; - - let errored = cx - .spawn(|mut cx| async move { - open_local_workspace( - workspace_paths, - vec![], - open_new_workspace, - false, - false, - &response_tx, - None, - &app_state, - &mut cx, - ) - .await - }) - .await; - - assert!(!errored); - } - - #[gpui::test] - async fn test_reuse_flag_functionality(cx: &mut TestAppContext) { - let app_state = init_test(cx); - - let root_dir = if cfg!(windows) { "C:\\root" } else { "/root" }; - let file1_path = if cfg!(windows) { - "C:\\root\\file1.txt" - } else { - "/root/file1.txt" - }; - let file2_path = if cfg!(windows) { - "C:\\root\\file2.txt" - } else { - "/root/file2.txt" - }; - - app_state.fs.create_dir(Path::new(root_dir)).await.unwrap(); - app_state - .fs - .create_file(Path::new(file1_path), Default::default()) - .await - .unwrap(); - app_state - .fs - .save( - Path::new(file1_path), - &Rope::from("content1"), - LineEnding::Unix, - ) - .await - .unwrap(); - app_state - .fs - .create_file(Path::new(file2_path), Default::default()) - .await - .unwrap(); - app_state - .fs - .save( - Path::new(file2_path), - &Rope::from("content2"), - LineEnding::Unix, - ) - .await - .unwrap(); - - // First, open a workspace normally - let (response_tx, _response_rx) = ipc::channel::().unwrap(); - let workspace_paths = vec![file1_path.to_string()]; - - let _errored = cx - .spawn({ - let app_state = app_state.clone(); - let response_tx = response_tx.clone(); - |mut cx| async move { - open_local_workspace( - workspace_paths, - vec![], - None, - false, - false, - &response_tx, - None, - &app_state, - &mut cx, - ) - .await - } - }) - .await; - - // Now test the reuse functionality - should replace the existing workspace - let workspace_paths_reuse = vec![file1_path.to_string()]; - - let errored_reuse = cx - .spawn({ - let app_state = app_state.clone(); - let response_tx = response_tx.clone(); - |mut cx| async move { - open_local_workspace( - workspace_paths_reuse, - vec![], - None, // open_new_workspace will be overridden by reuse logic - true, // reuse = true - false, - &response_tx, - None, - &app_state, - &mut cx, - ) - .await - } - }) - .await; - - assert!(!errored_reuse); - } -} diff --git a/crates/zed/src/zed/quick_action_bar.rs b/crates/zed/src/zed/quick_action_bar.rs deleted file mode 100644 index 2a52cc6972..0000000000 --- a/crates/zed/src/zed/quick_action_bar.rs +++ /dev/null @@ -1,730 +0,0 @@ -mod preview; -mod repl_menu; - -use agent_settings::AgentSettings; -use editor::actions::{ - AddSelectionAbove, AddSelectionBelow, CodeActionSource, DuplicateLineDown, GoToDiagnostic, - GoToHunk, GoToPreviousDiagnostic, GoToPreviousHunk, MoveLineDown, MoveLineUp, SelectAll, - SelectLargerSyntaxNode, SelectNext, SelectSmallerSyntaxNode, ToggleCodeActions, - ToggleDiagnostics, ToggleGoToLine, ToggleInlineDiagnostics, -}; -use editor::code_context_menus::{CodeContextMenu, ContextMenuOrigin}; -use editor::{Editor, EditorSettings}; -use gpui::{ - Action, AnchoredPositionMode, ClickEvent, Context, Corner, ElementId, Entity, EventEmitter, - FocusHandle, Focusable, InteractiveElement, ParentElement, Render, Styled, Subscription, - WeakEntity, Window, anchored, deferred, point, -}; -use project::{DisableAiSettings, project_settings::DiagnosticSeverity}; -use search::{BufferSearchBar, buffer_search}; -use settings::{Settings, SettingsStore}; -use ui::{ - ButtonStyle, ContextMenu, ContextMenuEntry, DocumentationEdge, DocumentationSide, IconButton, - IconName, IconSize, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*, -}; -use vim_mode_setting::{HelixModeSetting, VimModeSetting}; -use workspace::item::ItemBufferKind; -use workspace::{ - ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, item::ItemHandle, -}; -use zed_actions::{agent::AddSelectionToThread, assistant::InlineAssist, outline::ToggleOutline}; - -const MAX_CODE_ACTION_MENU_LINES: u32 = 16; - -pub struct QuickActionBar { - _inlay_hints_enabled_subscription: Option, - _ai_settings_subscription: Subscription, - active_item: Option>, - buffer_search_bar: Entity, - show: bool, - toggle_selections_handle: PopoverMenuHandle, - toggle_settings_handle: PopoverMenuHandle, - workspace: WeakEntity, -} - -impl QuickActionBar { - pub fn new( - buffer_search_bar: Entity, - workspace: &Workspace, - cx: &mut Context, - ) -> Self { - let mut was_agent_enabled = AgentSettings::get_global(cx).enabled(cx); - let mut was_agent_button = AgentSettings::get_global(cx).button; - - let ai_settings_subscription = cx.observe_global::(move |_, cx| { - let agent_settings = AgentSettings::get_global(cx); - let is_agent_enabled = agent_settings.enabled(cx); - - if was_agent_enabled != is_agent_enabled || was_agent_button != agent_settings.button { - was_agent_enabled = is_agent_enabled; - was_agent_button = agent_settings.button; - cx.notify(); - } - }); - - let mut this = Self { - _inlay_hints_enabled_subscription: None, - _ai_settings_subscription: ai_settings_subscription, - active_item: None, - buffer_search_bar, - show: true, - toggle_selections_handle: Default::default(), - toggle_settings_handle: Default::default(), - workspace: workspace.weak_handle(), - }; - this.apply_settings(cx); - cx.observe_global::(|this, cx| this.apply_settings(cx)) - .detach(); - this - } - - fn active_editor(&self) -> Option> { - self.active_item - .as_ref() - .and_then(|item| item.downcast::()) - } - - fn apply_settings(&mut self, cx: &mut Context) { - let new_show = EditorSettings::get_global(cx).toolbar.quick_actions; - if new_show != self.show { - self.show = new_show; - cx.emit(ToolbarItemEvent::ChangeLocation( - self.get_toolbar_item_location(), - )); - } - } - - fn get_toolbar_item_location(&self) -> ToolbarItemLocation { - if self.show && self.active_editor().is_some() { - ToolbarItemLocation::PrimaryRight - } else { - ToolbarItemLocation::Hidden - } - } -} - -impl Render for QuickActionBar { - fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let Some(editor) = self.active_editor() else { - return div().id("empty quick action bar"); - }; - - let supports_inlay_hints = editor.update(cx, |editor, cx| editor.supports_inlay_hints(cx)); - let editor_value = editor.read(cx); - let selection_menu_enabled = editor_value.selection_menu_enabled(cx); - let inlay_hints_enabled = editor_value.inlay_hints_enabled(); - let inline_values_enabled = editor_value.inline_values_enabled(); - let supports_diagnostics = editor_value.mode().is_full(); - let diagnostics_enabled = editor_value.diagnostics_max_severity != DiagnosticSeverity::Off; - let supports_inline_diagnostics = editor_value.inline_diagnostics_enabled(); - let inline_diagnostics_enabled = editor_value.show_inline_diagnostics(); - let git_blame_inline_enabled = editor_value.git_blame_inline_enabled(); - let show_git_blame_gutter = editor_value.show_git_blame_gutter(); - let auto_signature_help_enabled = editor_value.auto_signature_help_enabled(cx); - let show_line_numbers = editor_value.line_numbers_enabled(cx); - let has_edit_prediction_provider = editor_value.edit_prediction_provider().is_some(); - let show_edit_predictions = editor_value.edit_predictions_enabled(); - let edit_predictions_enabled_at_cursor = - editor_value.edit_predictions_enabled_at_cursor(cx); - let supports_minimap = editor_value.supports_minimap(cx); - let minimap_enabled = supports_minimap && editor_value.minimap().is_some(); - let has_available_code_actions = editor_value.has_available_code_actions(); - let code_action_enabled = editor_value.code_actions_enabled_for_toolbar(cx); - let focus_handle = editor_value.focus_handle(cx); - - let search_button = (editor.buffer_kind(cx) == ItemBufferKind::Singleton).then(|| { - QuickActionBarButton::new( - "toggle buffer search", - search::SEARCH_ICON, - !self.buffer_search_bar.read(cx).is_dismissed(), - Box::new(buffer_search::Deploy::find()), - focus_handle.clone(), - "Buffer Search", - { - let buffer_search_bar = self.buffer_search_bar.clone(); - move |_, window, cx| { - buffer_search_bar.update(cx, |search_bar, cx| { - search_bar.toggle(&buffer_search::Deploy::find(), window, cx) - }); - } - }, - ) - }); - - let assistant_button = QuickActionBarButton::new( - "toggle inline assistant", - IconName::ZedAssistant, - false, - Box::new(InlineAssist::default()), - focus_handle, - "Inline Assist", - move |_, window, cx| { - window.dispatch_action(Box::new(InlineAssist::default()), cx); - }, - ); - - let code_actions_dropdown = code_action_enabled.then(|| { - let focus = editor.focus_handle(cx); - let is_deployed = { - let menu_ref = editor.read(cx).context_menu().borrow(); - let code_action_menu = menu_ref - .as_ref() - .filter(|menu| matches!(menu, CodeContextMenu::CodeActions(..))); - code_action_menu - .as_ref() - .is_some_and(|menu| matches!(menu.origin(), ContextMenuOrigin::QuickActionBar)) - }; - let code_action_element = is_deployed - .then(|| { - editor.update(cx, |editor, cx| { - editor.render_context_menu(MAX_CODE_ACTION_MENU_LINES, window, cx) - }) - }) - .flatten(); - v_flex() - .child( - IconButton::new("toggle_code_actions_icon", IconName::BoltOutlined) - .icon_size(IconSize::Small) - .style(ButtonStyle::Subtle) - .disabled(!has_available_code_actions) - .toggle_state(is_deployed) - .when(!is_deployed, |this| { - this.when(has_available_code_actions, |this| { - this.tooltip(Tooltip::for_action_title( - "Code Actions", - &ToggleCodeActions::default(), - )) - }) - .when( - !has_available_code_actions, - |this| { - this.tooltip(Tooltip::for_action_title( - "No Code Actions Available", - &ToggleCodeActions::default(), - )) - }, - ) - }) - .on_click({ - let focus = focus; - move |_, window, cx| { - focus.dispatch_action( - &ToggleCodeActions { - deployed_from: Some(CodeActionSource::QuickActionBar), - quick_launch: false, - }, - window, - cx, - ); - } - }), - ) - .children(code_action_element.map(|menu| { - deferred( - anchored() - .position_mode(AnchoredPositionMode::Local) - .position(point(px(20.), px(20.))) - .anchor(Corner::TopRight) - .child(menu), - ) - })) - }); - - let editor_selections_dropdown = selection_menu_enabled.then(|| { - let has_diff_hunks = editor - .read(cx) - .buffer() - .read(cx) - .snapshot(cx) - .has_diff_hunks(); - let has_selection = editor.update(cx, |editor, cx| { - editor.has_non_empty_selection(&editor.display_snapshot(cx)) - }); - - let focus = editor.focus_handle(cx); - - let disable_ai = DisableAiSettings::get_global(cx).disable_ai; - - PopoverMenu::new("editor-selections-dropdown") - .trigger_with_tooltip( - IconButton::new("toggle_editor_selections_icon", IconName::CursorIBeam) - .icon_size(IconSize::Small) - .style(ButtonStyle::Subtle) - .toggle_state(self.toggle_selections_handle.is_deployed()), - Tooltip::text("Selection Controls"), - ) - .with_handle(self.toggle_selections_handle.clone()) - .anchor(Corner::TopRight) - .menu(move |window, cx| { - let focus = focus.clone(); - let menu = ContextMenu::build(window, cx, move |menu, _, _| { - menu.context(focus.clone()) - .action("Select All", Box::new(SelectAll)) - .action( - "Select Next Occurrence", - Box::new(SelectNext { - replace_newest: false, - }), - ) - .action("Expand Selection", Box::new(SelectLargerSyntaxNode)) - .action("Shrink Selection", Box::new(SelectSmallerSyntaxNode)) - .action( - "Add Cursor Above", - Box::new(AddSelectionAbove { - skip_soft_wrap: true, - }), - ) - .action( - "Add Cursor Below", - Box::new(AddSelectionBelow { - skip_soft_wrap: true, - }), - ) - .when(!disable_ai, |this| { - this.separator().action_disabled_when( - !has_selection, - "Add to Agent Thread", - Box::new(AddSelectionToThread), - ) - }) - .separator() - .action("Go to Symbol", Box::new(ToggleOutline)) - .action("Go to Line/Column", Box::new(ToggleGoToLine)) - .separator() - .action("Next Problem", Box::new(GoToDiagnostic::default())) - .action( - "Previous Problem", - Box::new(GoToPreviousDiagnostic::default()), - ) - .separator() - .action_disabled_when(!has_diff_hunks, "Next Hunk", Box::new(GoToHunk)) - .action_disabled_when( - !has_diff_hunks, - "Previous Hunk", - Box::new(GoToPreviousHunk), - ) - .separator() - .action("Move Line Up", Box::new(MoveLineUp)) - .action("Move Line Down", Box::new(MoveLineDown)) - .action("Duplicate Selection", Box::new(DuplicateLineDown)) - }); - Some(menu) - }) - }); - - let editor_focus_handle = editor.focus_handle(cx); - let editor = editor.downgrade(); - let editor_settings_dropdown = { - let vim_mode_enabled = VimModeSetting::get_global(cx).0; - let helix_mode_enabled = HelixModeSetting::get_global(cx).0; - - PopoverMenu::new("editor-settings") - .trigger_with_tooltip( - IconButton::new("toggle_editor_settings_icon", IconName::Sliders) - .icon_size(IconSize::Small) - .style(ButtonStyle::Subtle) - .toggle_state(self.toggle_settings_handle.is_deployed()), - Tooltip::text("Editor Controls"), - ) - .anchor(Corner::TopRight) - .with_handle(self.toggle_settings_handle.clone()) - .menu(move |window, cx| { - let menu = ContextMenu::build(window, cx, { - let focus_handle = editor_focus_handle.clone(); - |mut menu, _, _| { - menu = menu.context(focus_handle); - - if supports_inlay_hints { - menu = menu.toggleable_entry( - "Inlay Hints", - inlay_hints_enabled, - IconPosition::Start, - Some(editor::actions::ToggleInlayHints.boxed_clone()), - { - let editor = editor.clone(); - move |window, cx| { - editor - .update(cx, |editor, cx| { - editor.toggle_inlay_hints( - &editor::actions::ToggleInlayHints, - window, - cx, - ); - }) - .ok(); - } - }, - ); - - menu = menu.toggleable_entry( - "Inline Values", - inline_values_enabled, - IconPosition::Start, - Some(editor::actions::ToggleInlineValues.boxed_clone()), - { - let editor = editor.clone(); - move |window, cx| { - editor - .update(cx, |editor, cx| { - editor.toggle_inline_values( - &editor::actions::ToggleInlineValues, - window, - cx, - ); - }) - .ok(); - } - } - ); - } - - if supports_minimap { - menu = menu.toggleable_entry("Minimap", minimap_enabled, IconPosition::Start, Some(editor::actions::ToggleMinimap.boxed_clone()), { - let editor = editor.clone(); - move |window, cx| { - editor - .update(cx, |editor, cx| { - editor.toggle_minimap( - &editor::actions::ToggleMinimap, - window, - cx, - ); - }) - .ok(); - } - },) - } - - if has_edit_prediction_provider { - let mut edit_prediction_entry = ContextMenuEntry::new("Edit Predictions") - .toggleable(IconPosition::Start, edit_predictions_enabled_at_cursor && show_edit_predictions) - .disabled(!edit_predictions_enabled_at_cursor) - .action( - editor::actions::ToggleEditPrediction.boxed_clone(), - ).handler({ - let editor = editor.clone(); - move |window, cx| { - editor - .update(cx, |editor, cx| { - editor.toggle_edit_predictions( - &editor::actions::ToggleEditPrediction, - window, - cx, - ); - }) - .ok(); - } - }); - if !edit_predictions_enabled_at_cursor { - edit_prediction_entry = edit_prediction_entry.documentation_aside(DocumentationSide::Left, DocumentationEdge::Top, |_| { - Label::new("You can't toggle edit predictions for this file as it is within the excluded files list.").into_any_element() - }); - } - - menu = menu.item(edit_prediction_entry); - } - - menu = menu.separator(); - - if supports_diagnostics { - menu = menu.toggleable_entry( - "Diagnostics", - diagnostics_enabled, - IconPosition::Start, - Some(ToggleDiagnostics.boxed_clone()), - { - let editor = editor.clone(); - move |window, cx| { - editor - .update(cx, |editor, cx| { - editor.toggle_diagnostics( - &ToggleDiagnostics, - window, - cx, - ); - }) - .ok(); - } - }, - ); - - if supports_inline_diagnostics { - let mut inline_diagnostics_item = ContextMenuEntry::new("Inline Diagnostics") - .toggleable(IconPosition::Start, diagnostics_enabled && inline_diagnostics_enabled) - .action(ToggleInlineDiagnostics.boxed_clone()) - .handler({ - let editor = editor.clone(); - move |window, cx| { - editor - .update(cx, |editor, cx| { - editor.toggle_inline_diagnostics( - &ToggleInlineDiagnostics, - window, - cx, - ); - }) - .ok(); - } - }); - if !diagnostics_enabled { - inline_diagnostics_item = inline_diagnostics_item.disabled(true).documentation_aside(DocumentationSide::Left, DocumentationEdge::Top, |_| Label::new("Inline diagnostics are not available until regular diagnostics are enabled.").into_any_element()); - } - menu = menu.item(inline_diagnostics_item) - } - - menu = menu.separator(); - } - - menu = menu.toggleable_entry( - "Line Numbers", - show_line_numbers, - IconPosition::Start, - Some(editor::actions::ToggleLineNumbers.boxed_clone()), - { - let editor = editor.clone(); - move |window, cx| { - editor - .update(cx, |editor, cx| { - editor.toggle_line_numbers( - &editor::actions::ToggleLineNumbers, - window, - cx, - ); - }) - .ok(); - } - }, - ); - - menu = menu.toggleable_entry( - "Selection Menu", - selection_menu_enabled, - IconPosition::Start, - Some(editor::actions::ToggleSelectionMenu.boxed_clone()), - { - let editor = editor.clone(); - move |window, cx| { - editor - .update(cx, |editor, cx| { - editor.toggle_selection_menu( - &editor::actions::ToggleSelectionMenu, - window, - cx, - ) - }) - .ok(); - } - }, - ); - - menu = menu.toggleable_entry( - "Auto Signature Help", - auto_signature_help_enabled, - IconPosition::Start, - Some(editor::actions::ToggleAutoSignatureHelp.boxed_clone()), - { - let editor = editor.clone(); - move |window, cx| { - editor - .update(cx, |editor, cx| { - editor.toggle_auto_signature_help_menu( - &editor::actions::ToggleAutoSignatureHelp, - window, - cx, - ); - }) - .ok(); - } - }, - ); - - menu = menu.separator(); - - menu = menu.toggleable_entry( - "Inline Git Blame", - git_blame_inline_enabled, - IconPosition::Start, - Some(editor::actions::ToggleGitBlameInline.boxed_clone()), - { - let editor = editor.clone(); - move |window, cx| { - editor - .update(cx, |editor, cx| { - editor.toggle_git_blame_inline( - &editor::actions::ToggleGitBlameInline, - window, - cx, - ) - }) - .ok(); - } - }, - ); - - menu = menu.toggleable_entry( - "Column Git Blame", - show_git_blame_gutter, - IconPosition::Start, - Some(git::Blame.boxed_clone()), - { - let editor = editor.clone(); - move |window, cx| { - editor - .update(cx, |editor, cx| { - editor.toggle_git_blame( - &git::Blame, - window, - cx, - ) - }) - .ok(); - } - }, - ); - - menu = menu.separator(); - - menu = menu.toggleable_entry( - "Vim Mode", - vim_mode_enabled, - IconPosition::Start, - None, - { - move |window, cx| { - let new_value = !vim_mode_enabled; - VimModeSetting::override_global(VimModeSetting(new_value), cx); - HelixModeSetting::override_global(HelixModeSetting(false), cx); - window.refresh(); - } - }, - ); - menu = menu.toggleable_entry( - "Helix Mode", - helix_mode_enabled, - IconPosition::Start, - None, - { - move |window, cx| { - let new_value = !helix_mode_enabled; - HelixModeSetting::override_global(HelixModeSetting(new_value), cx); - VimModeSetting::override_global(VimModeSetting(false), cx); - window.refresh(); - } - } - ); - - menu - } - }); - Some(menu) - }) - }; - - h_flex() - .id("quick action bar") - .gap(DynamicSpacing::Base01.rems(cx)) - .children(self.render_repl_menu(cx)) - .children(self.render_preview_button(self.workspace.clone(), cx)) - .children(search_button) - .when( - AgentSettings::get_global(cx).enabled(cx) && AgentSettings::get_global(cx).button, - |bar| bar.child(assistant_button), - ) - .children(code_actions_dropdown) - .children(editor_selections_dropdown) - .child(editor_settings_dropdown) - } -} - -impl EventEmitter for QuickActionBar {} - -#[derive(IntoElement)] -struct QuickActionBarButton { - id: ElementId, - icon: IconName, - toggled: bool, - action: Box, - focus_handle: FocusHandle, - tooltip: SharedString, - on_click: Box, -} - -impl QuickActionBarButton { - fn new( - id: impl Into, - icon: IconName, - toggled: bool, - action: Box, - focus_handle: FocusHandle, - tooltip: impl Into, - on_click: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static, - ) -> Self { - Self { - id: id.into(), - icon, - toggled, - action, - focus_handle, - tooltip: tooltip.into(), - on_click: Box::new(on_click), - } - } -} - -impl RenderOnce for QuickActionBarButton { - fn render(self, _window: &mut Window, _: &mut App) -> impl IntoElement { - let tooltip = self.tooltip.clone(); - let action = self.action.boxed_clone(); - - IconButton::new(self.id.clone(), self.icon) - .icon_size(IconSize::Small) - .style(ButtonStyle::Subtle) - .toggle_state(self.toggled) - .tooltip(move |_window, cx| { - Tooltip::for_action_in(tooltip.clone(), &*action, &self.focus_handle, cx) - }) - .on_click(move |event, window, cx| (self.on_click)(event, window, cx)) - } -} - -impl ToolbarItemView for QuickActionBar { - fn set_active_pane_item( - &mut self, - active_pane_item: Option<&dyn ItemHandle>, - _: &mut Window, - cx: &mut Context, - ) -> ToolbarItemLocation { - self.active_item = active_pane_item.map(ItemHandle::boxed_clone); - if let Some(active_item) = active_pane_item { - self._inlay_hints_enabled_subscription.take(); - - if let Some(editor) = active_item.downcast::() { - let (mut inlay_hints_enabled, mut supports_inlay_hints) = - editor.update(cx, |editor, cx| { - ( - editor.inlay_hints_enabled(), - editor.supports_inlay_hints(cx), - ) - }); - self._inlay_hints_enabled_subscription = - Some(cx.observe(&editor, move |_, editor, cx| { - let (new_inlay_hints_enabled, new_supports_inlay_hints) = - editor.update(cx, |editor, cx| { - ( - editor.inlay_hints_enabled(), - editor.supports_inlay_hints(cx), - ) - }); - let should_notify = inlay_hints_enabled != new_inlay_hints_enabled - || supports_inlay_hints != new_supports_inlay_hints; - inlay_hints_enabled = new_inlay_hints_enabled; - supports_inlay_hints = new_supports_inlay_hints; - if should_notify { - cx.notify() - } - })); - } - } - self.get_toolbar_item_location() - } -} diff --git a/crates/zed/src/zed/quick_action_bar/preview.rs b/crates/zed/src/zed/quick_action_bar/preview.rs deleted file mode 100644 index 5d43e79542..0000000000 --- a/crates/zed/src/zed/quick_action_bar/preview.rs +++ /dev/null @@ -1,96 +0,0 @@ -use gpui::{AnyElement, Modifiers, WeakEntity}; -use markdown_preview::{ - OpenPreview as MarkdownOpenPreview, OpenPreviewToTheSide as MarkdownOpenPreviewToTheSide, - markdown_preview_view::MarkdownPreviewView, -}; -use svg_preview::{ - OpenPreview as SvgOpenPreview, OpenPreviewToTheSide as SvgOpenPreviewToTheSide, - svg_preview_view::SvgPreviewView, -}; -use ui::{Tooltip, prelude::*, text_for_keystroke}; -use workspace::Workspace; - -use super::QuickActionBar; - -#[derive(Clone, Copy)] -enum PreviewType { - Markdown, - Svg, -} - -impl QuickActionBar { - pub fn render_preview_button( - &self, - workspace_handle: WeakEntity, - cx: &mut Context, - ) -> Option { - let mut preview_type = None; - - if let Some(workspace) = self.workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - if MarkdownPreviewView::resolve_active_item_as_markdown_editor(workspace, cx) - .is_some() - { - preview_type = Some(PreviewType::Markdown); - } else if SvgPreviewView::resolve_active_item_as_svg_buffer(workspace, cx).is_some() - { - preview_type = Some(PreviewType::Svg); - } - }); - } - - let preview_type = preview_type?; - - let (button_id, tooltip_text, open_action, open_to_side_action, open_action_for_tooltip) = - match preview_type { - PreviewType::Markdown => ( - "toggle-markdown-preview", - "Preview Markdown", - Box::new(MarkdownOpenPreview) as Box, - Box::new(MarkdownOpenPreviewToTheSide) as Box, - &markdown_preview::OpenPreview as &dyn gpui::Action, - ), - PreviewType::Svg => ( - "toggle-svg-preview", - "Preview SVG", - Box::new(SvgOpenPreview) as Box, - Box::new(SvgOpenPreviewToTheSide) as Box, - &svg_preview::OpenPreview as &dyn gpui::Action, - ), - }; - - let alt_click = gpui::Keystroke { - key: "click".into(), - modifiers: Modifiers::alt(), - ..Default::default() - }; - - let button = IconButton::new(button_id, IconName::Eye) - .icon_size(IconSize::Small) - .style(ButtonStyle::Subtle) - .tooltip(move |_window, cx| { - 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) - ), - cx, - ) - }) - .on_click(move |_, window, cx| { - if let Some(workspace) = workspace_handle.upgrade() { - workspace.update(cx, |_, cx| { - if window.modifiers().alt { - window.dispatch_action(open_to_side_action.boxed_clone(), cx); - } else { - window.dispatch_action(open_action.boxed_clone(), cx); - } - }); - } - }); - - Some(button.into_any_element()) - } -} diff --git a/crates/zed/src/zed/quick_action_bar/repl_menu.rs b/crates/zed/src/zed/quick_action_bar/repl_menu.rs deleted file mode 100644 index 1ebdf35bb9..0000000000 --- a/crates/zed/src/zed/quick_action_bar/repl_menu.rs +++ /dev/null @@ -1,474 +0,0 @@ -use gpui::ElementId; -use gpui::{AnyElement, Entity}; -use picker::Picker; -use repl::{ - ExecutionState, JupyterSettings, Kernel, KernelSpecification, KernelStatus, Session, - SessionSupport, - components::{KernelPickerDelegate, KernelSelector}, - worktree_id_for_editor, -}; -use ui::{ - ButtonLike, CommonAnimationExt, ContextMenu, IconWithIndicator, Indicator, IntoElement, - PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*, -}; -use util::ResultExt; - -use super::QuickActionBar; - -const ZED_REPL_DOCUMENTATION: &str = "https://zed.dev/docs/repl"; - -struct ReplMenuState { - tooltip: SharedString, - icon: IconName, - icon_color: Color, - icon_is_animating: bool, - popover_disabled: bool, - indicator: Option, - - status: KernelStatus, - kernel_name: SharedString, - kernel_language: SharedString, -} - -impl QuickActionBar { - pub fn render_repl_menu(&self, cx: &mut Context) -> Option { - if !JupyterSettings::enabled(cx) { - return None; - } - - let editor = self.active_editor()?; - - let is_local_project = editor - .read(cx) - .workspace() - .map(|workspace| workspace.read(cx).project().read(cx).is_local()) - .unwrap_or(false); - - if !is_local_project { - return None; - } - - let has_nonempty_selection = { - editor.update(cx, |this, cx| { - this.selections - .count() - .ne(&0) - .then(|| { - let snapshot = this.display_snapshot(cx); - let latest = this.selections.newest_display(&snapshot); - !latest.is_empty() - }) - .unwrap_or_default() - }) - }; - - let session = repl::session(editor.downgrade(), cx); - let session = match session { - SessionSupport::ActiveSession(session) => session, - SessionSupport::Inactive(spec) => { - return self.render_repl_launch_menu(spec, cx); - } - SessionSupport::RequiresSetup(language) => { - return self.render_repl_setup(language.as_ref(), cx); - } - SessionSupport::Unsupported => return None, - }; - - let menu_state = session_state(session.clone(), cx); - - let id = "repl-menu"; - - let element_id = |suffix| ElementId::Name(format!("{}-{}", id, suffix).into()); - - let editor = editor.downgrade(); - let dropdown_menu = PopoverMenu::new(element_id("menu")) - .menu(move |window, cx| { - let editor = editor.clone(); - let session = session.clone(); - ContextMenu::build(window, cx, move |menu, _, cx| { - let menu_state = session_state(session, cx); - let status = menu_state.status; - let editor = editor.clone(); - - menu.map(|menu| { - if status.is_connected() { - let status = status.clone(); - menu.custom_row(move |_window, _cx| { - h_flex() - .child( - Label::new(format!( - "kernel: {} ({})", - menu_state.kernel_name, menu_state.kernel_language - )) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .into_any_element() - }) - .custom_row(move |_window, _cx| { - h_flex() - .child( - Label::new(status.clone().to_string()) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .into_any_element() - }) - } else { - let status = status.clone(); - menu.custom_row(move |_window, _cx| { - h_flex() - .child( - Label::new(format!("{}...", status.to_string())) - .size(LabelSize::Small) - .color(Color::Muted), - ) - .into_any_element() - }) - } - }) - .separator() - .custom_entry( - move |_window, _cx| { - Label::new(if has_nonempty_selection { - "Run Selection" - } else { - "Run Line" - }) - .into_any_element() - }, - { - let editor = editor.clone(); - move |window, cx| { - repl::run(editor.clone(), true, window, cx).log_err(); - } - }, - ) - .custom_entry( - move |_window, _cx| { - Label::new("Interrupt") - .size(LabelSize::Small) - .color(Color::Error) - .into_any_element() - }, - { - let editor = editor.clone(); - move |_, cx| { - repl::interrupt(editor.clone(), cx); - } - }, - ) - .custom_entry( - move |_window, _cx| { - Label::new("Clear Outputs") - .size(LabelSize::Small) - .color(Color::Muted) - .into_any_element() - }, - { - let editor = editor.clone(); - move |_, cx| { - repl::clear_outputs(editor.clone(), cx); - } - }, - ) - .separator() - .custom_entry( - move |_window, _cx| { - Label::new("Shut Down Kernel") - .size(LabelSize::Small) - .color(Color::Error) - .into_any_element() - }, - { - let editor = editor.clone(); - move |window, cx| { - repl::shutdown(editor.clone(), window, cx); - } - }, - ) - .custom_entry( - move |_window, _cx| { - Label::new("Restart Kernel") - .size(LabelSize::Small) - .color(Color::Error) - .into_any_element() - }, - { - move |window, cx| { - repl::restart(editor.clone(), window, cx); - } - }, - ) - .separator() - .action("View Sessions", Box::new(repl::Sessions)) - // TODO: Add shut down all kernels action - // .action("Shut Down all Kernels", Box::new(gpui::NoAction)) - }) - .into() - }) - .trigger_with_tooltip( - ButtonLike::new_rounded_right(element_id("dropdown")) - .child( - Icon::new(IconName::ChevronDown) - .size(IconSize::XSmall) - .color(Color::Muted), - ) - .width(rems(1.)) - .disabled(menu_state.popover_disabled), - Tooltip::text("REPL Menu"), - ); - - let button = ButtonLike::new_rounded_left("toggle_repl_icon") - .child(if menu_state.icon_is_animating { - Icon::new(menu_state.icon) - .color(menu_state.icon_color) - .with_rotate_animation(5) - .into_any_element() - } else { - IconWithIndicator::new( - Icon::new(IconName::ReplNeutral).color(menu_state.icon_color), - menu_state.indicator, - ) - .indicator_border_color(Some(cx.theme().colors().toolbar_background)) - .into_any_element() - }) - .size(ButtonSize::Compact) - .style(ButtonStyle::Subtle) - .tooltip(Tooltip::text(menu_state.tooltip)) - .on_click(|_, window, cx| window.dispatch_action(Box::new(repl::Run {}), cx)) - .into_any_element(); - - Some( - h_flex() - .child(self.render_kernel_selector(cx)) - .child(button) - .child(dropdown_menu) - .into_any_element(), - ) - } - pub fn render_repl_launch_menu( - &self, - kernel_specification: KernelSpecification, - cx: &mut Context, - ) -> Option { - let tooltip: SharedString = - SharedString::from(format!("Start REPL for {}", kernel_specification.name())); - - Some( - h_flex() - .child(self.render_kernel_selector(cx)) - .child( - IconButton::new("toggle_repl_icon", IconName::ReplNeutral) - .size(ButtonSize::Compact) - .icon_color(Color::Muted) - .style(ButtonStyle::Subtle) - .tooltip(Tooltip::text(tooltip)) - .on_click(|_, window, cx| { - window.dispatch_action(Box::new(repl::Run {}), cx) - }), - ) - .into_any_element(), - ) - } - - pub fn render_kernel_selector(&self, cx: &mut Context) -> impl IntoElement { - let editor = if let Some(editor) = self.active_editor() { - editor - } else { - return div().into_any_element(); - }; - - let Some(worktree_id) = worktree_id_for_editor(editor.downgrade(), cx) else { - return div().into_any_element(); - }; - - let session = repl::session(editor.downgrade(), cx); - - let current_kernelspec = match session { - SessionSupport::ActiveSession(session) => { - Some(session.read(cx).kernel_specification.clone()) - } - SessionSupport::Inactive(kernel_specification) => Some(kernel_specification), - SessionSupport::RequiresSetup(_language_name) => None, - SessionSupport::Unsupported => None, - }; - - let current_kernel_name = current_kernelspec.as_ref().map(|spec| spec.name()); - - let menu_handle: PopoverMenuHandle> = - PopoverMenuHandle::default(); - KernelSelector::new( - { - Box::new(move |kernelspec, window, cx| { - repl::assign_kernelspec(kernelspec, editor.downgrade(), window, cx).ok(); - }) - }, - worktree_id, - ButtonLike::new("kernel-selector") - .style(ButtonStyle::Subtle) - .size(ButtonSize::Compact) - .child( - h_flex() - .w_full() - .gap_0p5() - .child( - div() - .overflow_x_hidden() - .flex_grow() - .whitespace_nowrap() - .child( - Label::new(if let Some(name) = current_kernel_name { - name - } else { - SharedString::from("Select Kernel") - }) - .size(LabelSize::Small) - .color(if current_kernelspec.is_some() { - Color::Default - } else { - Color::Placeholder - }) - .into_any_element(), - ), - ) - .child( - Icon::new(IconName::ChevronDown) - .color(Color::Muted) - .size(IconSize::XSmall), - ), - ), - Tooltip::text("Select Kernel"), - ) - .with_handle(menu_handle) - .into_any_element() - } - - pub fn render_repl_setup(&self, language: &str, cx: &mut Context) -> Option { - let tooltip: SharedString = SharedString::from(format!("Setup Zed REPL for {}", language)); - Some( - h_flex() - .gap(DynamicSpacing::Base06.rems(cx)) - .child(self.render_kernel_selector(cx)) - .child( - IconButton::new("toggle_repl_icon", IconName::ReplNeutral) - .style(ButtonStyle::Subtle) - .shape(ui::IconButtonShape::Square) - .icon_size(ui::IconSize::Small) - .icon_color(Color::Muted) - .tooltip(Tooltip::text(tooltip)) - .on_click(|_, _window, cx| { - cx.open_url(&format!("{}#installation", ZED_REPL_DOCUMENTATION)) - }), - ) - .into_any_element(), - ) - } -} - -fn session_state(session: Entity, cx: &mut App) -> ReplMenuState { - let session = session.read(cx); - - let kernel_name = session.kernel_specification.name(); - let kernel_language: SharedString = session.kernel_specification.language(); - - let fill_fields = || { - ReplMenuState { - tooltip: "Nothing running".into(), - icon: IconName::ReplNeutral, - icon_color: Color::Default, - icon_is_animating: false, - popover_disabled: false, - indicator: None, - kernel_name: kernel_name.clone(), - kernel_language: kernel_language.clone(), - // TODO: Technically not shutdown, but indeterminate - status: KernelStatus::Shutdown, - // current_delta: Duration::default(), - } - }; - - let transitional = - |tooltip: SharedString, animating: bool, popover_disabled: bool| ReplMenuState { - tooltip, - icon_is_animating: animating, - popover_disabled, - icon_color: Color::Muted, - indicator: Some(Indicator::dot().color(Color::Muted)), - status: session.kernel.status(), - ..fill_fields() - }; - - let starting = || transitional(format!("{} is starting", kernel_name).into(), true, true); - let restarting = || transitional(format!("Restarting {}", kernel_name).into(), true, true); - let shutting_down = || { - transitional( - format!("{} is shutting down", kernel_name).into(), - false, - true, - ) - }; - let auto_restarting = || { - transitional( - format!("Auto-restarting {}", kernel_name).into(), - true, - true, - ) - }; - let unknown = || transitional(format!("{} state unknown", kernel_name).into(), false, true); - let other = |state: &str| { - transitional( - format!("{} state: {}", kernel_name, state).into(), - false, - true, - ) - }; - - let shutdown = || ReplMenuState { - tooltip: "Nothing running".into(), - icon: IconName::ReplNeutral, - icon_color: Color::Default, - icon_is_animating: false, - popover_disabled: false, - indicator: None, - status: KernelStatus::Shutdown, - ..fill_fields() - }; - - match &session.kernel { - Kernel::Restarting => restarting(), - Kernel::RunningKernel(kernel) => match &kernel.execution_state() { - ExecutionState::Idle => ReplMenuState { - tooltip: format!("Run code on {} ({})", kernel_name, kernel_language).into(), - indicator: Some(Indicator::dot().color(Color::Success)), - status: session.kernel.status(), - ..fill_fields() - }, - ExecutionState::Busy => ReplMenuState { - tooltip: format!("Interrupt {} ({})", kernel_name, kernel_language).into(), - icon_is_animating: true, - popover_disabled: false, - indicator: None, - status: session.kernel.status(), - ..fill_fields() - }, - ExecutionState::Unknown => unknown(), - ExecutionState::Starting => starting(), - ExecutionState::Restarting => restarting(), - ExecutionState::Terminating => shutting_down(), - ExecutionState::AutoRestarting => auto_restarting(), - ExecutionState::Dead => shutdown(), - ExecutionState::Other(state) => other(state), - }, - Kernel::StartingKernel(_) => starting(), - Kernel::ErroredLaunch(e) => ReplMenuState { - tooltip: format!("Error with kernel {}: {}", kernel_name, e).into(), - popover_disabled: false, - indicator: Some(Indicator::dot().color(Color::Error)), - status: session.kernel.status(), - ..fill_fields() - }, - Kernel::ShuttingDown => shutting_down(), - Kernel::Shutdown => shutdown(), - } -} diff --git a/crates/zed/src/zed/windows_only_instance.rs b/crates/zed/src/zed/windows_only_instance.rs deleted file mode 100644 index f3eab15441..0000000000 --- a/crates/zed/src/zed/windows_only_instance.rs +++ /dev/null @@ -1,217 +0,0 @@ -use std::{sync::Arc, thread::JoinHandle}; - -use anyhow::Context; -use cli::{CliRequest, CliResponse, IpcHandshake, ipc::IpcOneShotServer}; -use parking_lot::Mutex; -use release_channel::app_identifier; -use util::ResultExt; -use windows::{ - Win32::{ - Foundation::{CloseHandle, ERROR_ALREADY_EXISTS, GENERIC_WRITE, GetLastError, HANDLE}, - Storage::FileSystem::{ - CreateFileW, FILE_FLAGS_AND_ATTRIBUTES, FILE_SHARE_MODE, OPEN_EXISTING, - PIPE_ACCESS_INBOUND, ReadFile, WriteFile, - }, - System::{ - Pipes::{ - ConnectNamedPipe, CreateNamedPipeW, DisconnectNamedPipe, PIPE_READMODE_MESSAGE, - PIPE_TYPE_MESSAGE, PIPE_WAIT, - }, - Threading::CreateMutexW, - }, - }, - core::HSTRING, -}; - -use crate::{Args, OpenListener, RawOpenRequest}; - -#[inline] -fn is_first_instance() -> bool { - unsafe { - CreateMutexW( - None, - false, - &HSTRING::from(format!("{}-Instance-Mutex", app_identifier())), - ) - .expect("Unable to create instance mutex.") - }; - unsafe { GetLastError() != ERROR_ALREADY_EXISTS } -} - -pub fn handle_single_instance(opener: OpenListener, args: &Args) -> bool { - let is_first_instance = is_first_instance(); - if is_first_instance { - // We are the first instance, listen for messages sent from other instances - std::thread::Builder::new() - .name("EnsureSingleton".to_owned()) - .spawn(move || { - with_pipe(|url| { - opener.open(RawOpenRequest { - urls: vec![url], - ..Default::default() - }) - }) - }) - .unwrap(); - } else if !args.foreground { - // We are not the first instance, send args to the first instance - send_args_to_instance(args).log_err(); - } - - is_first_instance -} - -fn with_pipe(f: impl Fn(String)) { - let pipe = unsafe { - CreateNamedPipeW( - &HSTRING::from(format!("\\\\.\\pipe\\{}-Named-Pipe", app_identifier())), - PIPE_ACCESS_INBOUND, - PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, - 1, - 128, - 128, - 0, - None, - ) - }; - if pipe.is_invalid() { - log::error!("Failed to create named pipe: {:?}", unsafe { - GetLastError() - }); - return; - } - - loop { - if let Some(message) = retrieve_message_from_pipe(pipe) - .context("Failed to read from named pipe") - .log_err() - { - f(message); - } - } -} - -fn retrieve_message_from_pipe(pipe: HANDLE) -> anyhow::Result { - unsafe { ConnectNamedPipe(pipe, None)? }; - let message = retrieve_message_from_pipe_inner(pipe); - unsafe { DisconnectNamedPipe(pipe).log_err() }; - message -} - -fn retrieve_message_from_pipe_inner(pipe: HANDLE) -> anyhow::Result { - let mut buffer = [0u8; 128]; - unsafe { - ReadFile(pipe, Some(&mut buffer), None, None)?; - } - let message = std::ffi::CStr::from_bytes_until_nul(&buffer)?; - Ok(message.to_string_lossy().into_owned()) -} - -// This part of code is mostly from crates/cli/src/main.rs -fn send_args_to_instance(args: &Args) -> anyhow::Result<()> { - if let Some(dock_menu_action_idx) = args.dock_action { - let url = format!("zed-dock-action://{}", dock_menu_action_idx); - return write_message_to_instance_pipe(url.as_bytes()); - } - - let (server, server_name) = - IpcOneShotServer::::new().context("Handshake before Zed spawn")?; - let url = format!("zed-cli://{server_name}"); - - let request = { - let mut paths = vec![]; - let mut urls = vec![]; - let mut diff_paths = vec![]; - for path in args.paths_or_urls.iter() { - match std::fs::canonicalize(&path) { - Ok(path) => paths.push(path.to_string_lossy().into_owned()), - Err(error) => { - if path.starts_with("zed://") - || path.starts_with("http://") - || path.starts_with("https://") - || path.starts_with("file://") - || path.starts_with("ssh://") - { - urls.push(path.clone()); - } else { - log::error!("error parsing path argument: {}", error); - } - } - } - } - - for path in args.diff.chunks(2) { - let old = std::fs::canonicalize(&path[0]).log_err(); - let new = std::fs::canonicalize(&path[1]).log_err(); - if let Some((old, new)) = old.zip(new) { - diff_paths.push([ - old.to_string_lossy().into_owned(), - new.to_string_lossy().into_owned(), - ]); - } - } - - CliRequest::Open { - paths, - urls, - diff_paths, - wait: false, - wsl: args.wsl.clone(), - open_new_workspace: None, - reuse: false, - env: None, - user_data_dir: args.user_data_dir.clone(), - } - }; - - let exit_status = Arc::new(Mutex::new(None)); - let sender: JoinHandle> = std::thread::Builder::new() - .name("CliReceiver".to_owned()) - .spawn({ - let exit_status = exit_status.clone(); - move || { - let (_, handshake) = server.accept().context("Handshake after Zed spawn")?; - let (tx, rx) = (handshake.requests, handshake.responses); - - tx.send(request)?; - - while let Ok(response) = rx.recv() { - match response { - CliResponse::Ping => {} - CliResponse::Stdout { message } => log::info!("{message}"), - CliResponse::Stderr { message } => log::error!("{message}"), - CliResponse::Exit { status } => { - exit_status.lock().replace(status); - return Ok(()); - } - } - } - Ok(()) - } - }) - .unwrap(); - - write_message_to_instance_pipe(url.as_bytes())?; - sender.join().unwrap()?; - if let Some(exit_status) = exit_status.lock().take() { - std::process::exit(exit_status); - } - Ok(()) -} - -fn write_message_to_instance_pipe(message: &[u8]) -> anyhow::Result<()> { - unsafe { - let pipe = CreateFileW( - &HSTRING::from(format!("\\\\.\\pipe\\{}-Named-Pipe", app_identifier())), - GENERIC_WRITE.0, - FILE_SHARE_MODE::default(), - None, - OPEN_EXISTING, - FILE_FLAGS_AND_ATTRIBUTES::default(), - None, - )?; - WriteFile(pipe, Some(message), None, None)?; - CloseHandle(pipe)?; - } - Ok(()) -} diff --git a/crates/zed_actions/Cargo.toml b/crates/zed_actions/Cargo.toml deleted file mode 100644 index 1a140c483f..0000000000 --- a/crates/zed_actions/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "zed_actions" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[dependencies] -gpui.workspace = true -schemars.workspace = true -serde.workspace = true -uuid.workspace = true diff --git a/crates/zed_actions/LICENSE-GPL b/crates/zed_actions/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/zed_actions/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/zed_actions/src/lib.rs b/crates/zed_actions/src/lib.rs deleted file mode 100644 index a89e943e02..0000000000 --- a/crates/zed_actions/src/lib.rs +++ /dev/null @@ -1,597 +0,0 @@ -use gpui::{Action, actions}; -use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; - -// If the zed binary doesn't use anything in this crate, it will be optimized away -// and the actions won't initialize. So we just provide an empty initialization function -// to be called from main. -// -// These may provide relevant context: -// https://github.com/rust-lang/rust/issues/47384 -// https://github.com/mmastrac/rust-ctor/issues/280 -pub fn init() {} - -/// Opens a URL in the system's default web browser. -#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)] -#[action(namespace = zed)] -#[serde(deny_unknown_fields)] -pub struct OpenBrowser { - pub url: String, -} - -/// Opens a zed:// URL within the application. -#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)] -#[action(namespace = zed)] -#[serde(deny_unknown_fields)] -pub struct OpenZedUrl { - pub url: String, -} - -/// Opens the keymap to either add a keybinding or change an existing one -#[derive(PartialEq, Clone, Default, Action, JsonSchema, Serialize, Deserialize)] -#[action(namespace = zed, no_json, no_register)] -pub struct ChangeKeybinding { - pub action: String, -} - -actions!( - zed, - [ - /// Opens the settings editor. - #[action(deprecated_aliases = ["zed_actions::OpenSettingsEditor"])] - OpenSettings, - /// Opens the settings JSON file. - #[action(deprecated_aliases = ["zed_actions::OpenSettings"])] - OpenSettingsFile, - /// Opens project-specific settings. - #[action(deprecated_aliases = ["zed_actions::OpenProjectSettings"])] - OpenProjectSettings, - /// Opens the default keymap file. - OpenDefaultKeymap, - /// Opens the user keymap file. - #[action(deprecated_aliases = ["zed_actions::OpenKeymap"])] - OpenKeymapFile, - /// Opens the keymap editor. - #[action(deprecated_aliases = ["zed_actions::OpenKeymapEditor"])] - OpenKeymap, - /// Opens account settings. - OpenAccountSettings, - /// Opens server settings. - OpenServerSettings, - /// Quits the application. - Quit, - /// Shows information about Zed. - About, - /// Opens the documentation website. - OpenDocs, - /// Views open source licenses. - OpenLicenses, - /// Opens the telemetry log. - OpenTelemetryLog, - /// Opens the performance profiler. - OpenPerformanceProfiler, - ] -); - -#[derive(PartialEq, Clone, Copy, Debug, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum ExtensionCategoryFilter { - Themes, - IconThemes, - Languages, - Grammars, - LanguageServers, - ContextServers, - AgentServers, - SlashCommands, - IndexedDocsProviders, - Snippets, - DebugAdapters, -} - -/// Opens the extensions management interface. -#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] -#[action(namespace = zed)] -#[serde(deny_unknown_fields)] -pub struct Extensions { - /// Filters the extensions page down to extensions that are in the specified category. - #[serde(default)] - pub category_filter: Option, - /// Focuses just the extension with the specified ID. - #[serde(default)] - pub id: Option, -} - -/// Decreases the font size in the editor buffer. -#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] -#[action(namespace = zed)] -#[serde(deny_unknown_fields)] -pub struct DecreaseBufferFontSize { - #[serde(default)] - pub persist: bool, -} - -/// Increases the font size in the editor buffer. -#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] -#[action(namespace = zed)] -#[serde(deny_unknown_fields)] -pub struct IncreaseBufferFontSize { - #[serde(default)] - pub persist: bool, -} - -/// Increases the font size in the editor buffer. -#[derive(PartialEq, Clone, Debug, Deserialize, JsonSchema, Action)] -#[action(namespace = zed)] -#[serde(deny_unknown_fields)] -pub struct OpenSettingsAt { - /// A path to a specific setting (e.g. `theme.mode`) - pub path: String, -} - -/// Resets the buffer font size to the default value. -#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] -#[action(namespace = zed)] -#[serde(deny_unknown_fields)] -pub struct ResetBufferFontSize { - #[serde(default)] - pub persist: bool, -} - -/// Decreases the font size of the user interface. -#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] -#[action(namespace = zed)] -#[serde(deny_unknown_fields)] -pub struct DecreaseUiFontSize { - #[serde(default)] - pub persist: bool, -} - -/// Increases the font size of the user interface. -#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] -#[action(namespace = zed)] -#[serde(deny_unknown_fields)] -pub struct IncreaseUiFontSize { - #[serde(default)] - pub persist: bool, -} - -/// Resets the UI font size to the default value. -#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] -#[action(namespace = zed)] -#[serde(deny_unknown_fields)] -pub struct ResetUiFontSize { - #[serde(default)] - pub persist: bool, -} - -/// Resets all zoom levels (UI and buffer font sizes, including in the agent panel) to their default values. -#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] -#[action(namespace = zed)] -#[serde(deny_unknown_fields)] -pub struct ResetAllZoom { - #[serde(default)] - pub persist: bool, -} - -pub mod dev { - use gpui::actions; - - actions!( - dev, - [ - /// Toggles the developer inspector for debugging UI elements. - ToggleInspector - ] - ); -} - -pub mod workspace { - use gpui::actions; - - actions!( - 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, - ] - ); -} - -pub mod git { - use gpui::actions; - - actions!( - git, - [ - /// Checks out a different git branch. - CheckoutBranch, - /// Switches to a different git branch. - Switch, - /// Selects a different repository. - SelectRepo, - /// Filter remotes. - FilterRemotes, - /// Create a git remote. - CreateRemote, - /// Opens the git branch selector. - #[action(deprecated_aliases = ["branches::OpenRecent"])] - Branch, - /// Opens the git stash selector. - ViewStash, - /// Opens the git worktree selector. - Worktree - ] - ); -} - -pub mod toast { - use gpui::actions; - - actions!( - toast, - [ - /// Runs the action associated with a toast notification. - RunAction - ] - ); -} - -pub mod command_palette { - use gpui::actions; - - actions!( - command_palette, - [ - /// Toggles the command palette. - Toggle, - ] - ); -} - -pub mod project_panel { - use gpui::actions; - - actions!( - project_panel, - [ - /// Toggles focus on the project panel. - ToggleFocus - ] - ); -} -pub mod feedback { - use gpui::actions; - - actions!( - feedback, - [ - /// Opens email client to send feedback to Zed support. - EmailZed, - /// Opens the bug report form. - FileBugReport, - /// Opens the feature request form. - RequestFeature - ] - ); -} - -pub mod theme_selector { - use gpui::Action; - use schemars::JsonSchema; - use serde::Deserialize; - - /// Toggles the theme selector interface. - #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] - #[action(namespace = theme_selector)] - #[serde(deny_unknown_fields)] - pub struct Toggle { - /// A list of theme names to filter the theme selector down to. - pub themes_filter: Option>, - } -} - -pub mod icon_theme_selector { - use gpui::Action; - use schemars::JsonSchema; - use serde::Deserialize; - - /// Toggles the icon theme selector interface. - #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] - #[action(namespace = icon_theme_selector)] - #[serde(deny_unknown_fields)] - pub struct Toggle { - /// A list of icon theme names to filter the theme selector down to. - pub themes_filter: Option>, - } -} - -pub mod settings_profile_selector { - use gpui::Action; - use schemars::JsonSchema; - use serde::Deserialize; - - #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] - #[action(namespace = settings_profile_selector)] - pub struct Toggle; -} - -pub mod agent { - use gpui::actions; - - actions!( - agent, - [ - /// Opens the agent settings panel. - #[action(deprecated_aliases = ["agent::OpenConfiguration"])] - OpenSettings, - /// Opens the agent onboarding modal. - OpenOnboardingModal, - /// Opens the ACP onboarding modal. - OpenAcpOnboardingModal, - /// Opens the Claude Code onboarding modal. - OpenClaudeCodeOnboardingModal, - /// 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, - /// Add the current selection as context for threads in the agent panel. - #[action(deprecated_aliases = ["assistant::QuoteSelection", "agent::QuoteSelection"])] - AddSelectionToThread, - /// Resets the agent panel zoom levels (agent UI and buffer font sizes). - ResetAgentZoom, - ] - ); -} - -pub mod assistant { - use gpui::{Action, actions}; - use schemars::JsonSchema; - use serde::Deserialize; - use uuid::Uuid; - - actions!( - agent, - [ - #[action(deprecated_aliases = ["assistant::ToggleFocus"])] - ToggleFocus - ] - ); - - actions!( - assistant, - [ - /// Shows the assistant configuration panel. - ShowConfiguration - ] - ); - - /// Opens the rules library for managing agent rules and prompts. - #[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)] - #[action(namespace = agent, deprecated_aliases = ["assistant::OpenRulesLibrary", "assistant::DeployPromptLibrary"])] - #[serde(deny_unknown_fields)] - pub struct OpenRulesLibrary { - #[serde(skip)] - pub prompt_to_select: Option, - } - - /// Deploys the assistant interface with the specified configuration. - #[derive(Clone, Default, Deserialize, PartialEq, JsonSchema, Action)] - #[action(namespace = assistant)] - #[serde(deny_unknown_fields)] - pub struct InlineAssist { - pub prompt: Option, - } -} - -pub mod debugger { - use gpui::actions; - - actions!( - debugger, - [ - /// Opens the debugger onboarding modal. - OpenOnboardingModal, - /// Resets the debugger onboarding state. - ResetOnboarding - ] - ); -} - -/// Opens the recent projects interface. -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] -#[action(namespace = projects)] -#[serde(deny_unknown_fields)] -pub struct OpenRecent { - #[serde(default)] - pub create_new_window: bool, -} - -/// Creates a project from a selected template. -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] -#[action(namespace = projects)] -#[serde(deny_unknown_fields)] -pub struct OpenRemote { - #[serde(default)] - pub from_existing_connection: bool, - #[serde(default)] - pub create_new_window: bool, -} - -/// Opens the dev container connection modal. -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] -#[action(namespace = projects)] -#[serde(deny_unknown_fields)] -pub struct OpenDevContainer; - -/// Where to spawn the task in the UI. -#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum RevealTarget { - /// In the central pane group, "main" editor area. - Center, - /// In the terminal dock, "regular" terminal items' place. - #[default] - Dock, -} - -/// Spawns a task with name or opens tasks modal. -#[derive(Debug, PartialEq, Clone, Deserialize, JsonSchema, Action)] -#[action(namespace = task)] -#[serde(untagged)] -pub enum Spawn { - /// Spawns a task by the name given. - ByName { - task_name: String, - #[serde(default)] - reveal_target: Option, - }, - /// Spawns a task by the name given. - ByTag { - task_tag: String, - #[serde(default)] - reveal_target: Option, - }, - /// Spawns a task via modal's selection. - ViaModal { - /// Selected task's `reveal_target` property override. - #[serde(default)] - reveal_target: Option, - }, -} - -impl Spawn { - pub fn modal() -> Self { - Self::ViaModal { - reveal_target: None, - } - } -} - -/// Reruns the last task. -#[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] -#[action(namespace = task)] -#[serde(deny_unknown_fields)] -pub struct Rerun { - /// Controls whether the task context is reevaluated prior to execution of a task. - /// If it is not, environment variables such as ZED_COLUMN, ZED_FILE are gonna be the same as in the last execution of a task - /// If it is, these variables will be updated to reflect current state of editor at the time task::Rerun is executed. - /// default: false - #[serde(default)] - pub reevaluate_context: bool, - /// Overrides `allow_concurrent_runs` property of the task being reran. - /// Default: null - #[serde(default)] - pub allow_concurrent_runs: Option, - /// Overrides `use_new_terminal` property of the task being reran. - /// Default: null - #[serde(default)] - pub use_new_terminal: Option, - - /// If present, rerun the task with this ID, otherwise rerun the last task. - #[serde(skip)] - pub task_id: Option, -} - -pub mod outline { - use std::sync::OnceLock; - - use gpui::{AnyView, App, Window, actions}; - - actions!( - outline, - [ - #[action(name = "Toggle")] - ToggleOutline - ] - ); - /// A pointer to outline::toggle function, exposed here to sewer the breadcrumbs <-> outline dependency. - pub static TOGGLE_OUTLINE: OnceLock = OnceLock::new(); -} - -actions!( - zed_predict_onboarding, - [ - /// Opens the Zed Predict onboarding modal. - OpenZedPredictOnboarding - ] -); -actions!( - git_onboarding, - [ - /// Opens the git integration onboarding modal. - OpenGitIntegrationOnboarding - ] -); - -actions!( - debug_panel, - [ - /// Toggles focus on the debug panel. - ToggleFocus - ] -); -actions!( - debugger, - [ - /// Toggles the enabled state of a breakpoint. - ToggleEnableBreakpoint, - /// Removes a breakpoint. - UnsetBreakpoint, - /// Opens the project debug tasks configuration. - OpenProjectDebugTasks, - ] -); - -pub mod vim { - use gpui::actions; - - actions!( - vim, - [ - /// Opens the default keymap file. - OpenDefaultKeymap - ] - ); -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct WslConnectionOptions { - pub distro_name: String, - pub user: Option, -} - -#[cfg(target_os = "windows")] -pub mod wsl_actions { - use gpui::Action; - use schemars::JsonSchema; - use serde::Deserialize; - - /// Opens a folder inside Wsl. - #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] - #[action(namespace = projects)] - #[serde(deny_unknown_fields)] - pub struct OpenFolderInWsl { - #[serde(default)] - pub create_new_window: bool, - } - - /// Open a wsl distro. - #[derive(PartialEq, Clone, Deserialize, Default, JsonSchema, Action)] - #[action(namespace = projects)] - #[serde(deny_unknown_fields)] - pub struct OpenWsl { - #[serde(default)] - pub create_new_window: bool, - } -} diff --git a/crates/zed_env_vars/Cargo.toml b/crates/zed_env_vars/Cargo.toml deleted file mode 100644 index 1cf32174c3..0000000000 --- a/crates/zed_env_vars/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "zed_env_vars" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/zed_env_vars.rs" - -[features] -default = [] - -[dependencies] -gpui.workspace = true diff --git a/crates/zed_env_vars/LICENSE-GPL b/crates/zed_env_vars/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/zed_env_vars/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/zed_env_vars/src/zed_env_vars.rs b/crates/zed_env_vars/src/zed_env_vars.rs deleted file mode 100644 index 53b9c22bb2..0000000000 --- a/crates/zed_env_vars/src/zed_env_vars.rs +++ /dev/null @@ -1,44 +0,0 @@ -use gpui::SharedString; -use std::sync::LazyLock; - -/// Whether Zed is running in stateless mode. -/// When true, Zed will use in-memory databases instead of persistent storage. -pub static ZED_STATELESS: LazyLock = bool_env_var!("ZED_STATELESS"); - -pub struct EnvVar { - pub name: SharedString, - /// Value of the environment variable. Also `None` when set to an empty string. - pub value: Option, -} - -impl EnvVar { - pub fn new(name: SharedString) -> Self { - let value = std::env::var(name.as_str()).ok(); - if value.as_ref().is_some_and(|v| v.is_empty()) { - Self { name, value: None } - } else { - Self { name, value } - } - } - - pub fn or(self, other: EnvVar) -> EnvVar { - if self.value.is_some() { self } else { other } - } -} - -/// Creates a `LazyLock` expression for use in a `static` declaration. -#[macro_export] -macro_rules! env_var { - ($name:expr) => { - LazyLock::new(|| $crate::EnvVar::new(($name).into())) - }; -} - -/// Generates a `LazyLock` expression for use in a `static` declaration. Checks if the -/// environment variable exists and is non-empty. -#[macro_export] -macro_rules! bool_env_var { - ($name:expr) => { - LazyLock::new(|| $crate::EnvVar::new(($name).into()).value.is_some()) - }; -} diff --git a/crates/zeta_prompt/Cargo.toml b/crates/zeta_prompt/Cargo.toml deleted file mode 100644 index c9b1e2d784..0000000000 --- a/crates/zeta_prompt/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "zeta_prompt" -version = "0.1.0" -publish.workspace = true -edition.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/zeta_prompt.rs" - -[dependencies] -serde.workspace = true \ No newline at end of file diff --git a/crates/zeta_prompt/LICENSE-GPL b/crates/zeta_prompt/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/zeta_prompt/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/zeta_prompt/src/zeta_prompt.rs b/crates/zeta_prompt/src/zeta_prompt.rs deleted file mode 100644 index 21fbca1ae1..0000000000 --- a/crates/zeta_prompt/src/zeta_prompt.rs +++ /dev/null @@ -1,165 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::fmt::Write; -use std::ops::Range; -use std::path::Path; -use std::sync::Arc; - -pub const CURSOR_MARKER: &str = "<|user_cursor|>"; - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct ZetaPromptInput { - pub cursor_path: Arc, - pub cursor_excerpt: Arc, - pub editable_range_in_excerpt: Range, - pub cursor_offset_in_excerpt: usize, - pub events: Vec>, - pub related_files: Arc<[RelatedFile]>, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(tag = "event")] -pub enum Event { - BufferChange { - path: Arc, - old_path: Arc, - diff: String, - predicted: bool, - in_open_source_repo: bool, - }, -} - -pub fn write_event(prompt: &mut String, event: &Event) { - fn write_path_as_unix_str(prompt: &mut String, path: &Path) { - for component in path.components() { - prompt.push('/'); - write!(prompt, "{}", component.as_os_str().display()).ok(); - } - } - match event { - Event::BufferChange { - path, - old_path, - diff, - predicted, - in_open_source_repo: _, - } => { - if *predicted { - prompt.push_str("// User accepted prediction:\n"); - } - prompt.push_str("--- a"); - write_path_as_unix_str(prompt, old_path.as_ref()); - prompt.push_str("\n+++ b"); - write_path_as_unix_str(prompt, path.as_ref()); - prompt.push('\n'); - prompt.push_str(diff); - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct RelatedFile { - pub path: Arc, - pub max_row: u32, - pub excerpts: Vec, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct RelatedExcerpt { - pub row_range: Range, - pub text: String, -} - -pub fn format_zeta_prompt(input: &ZetaPromptInput) -> String { - let mut prompt = String::new(); - write_related_files(&mut prompt, &input.related_files); - write_edit_history_section(&mut prompt, input); - write_cursor_excerpt_section(&mut prompt, input); - prompt -} - -pub fn write_related_files(prompt: &mut String, related_files: &[RelatedFile]) { - push_delimited(prompt, "related_files", &[], |prompt| { - for file in related_files { - let path_str = file.path.to_string_lossy(); - push_delimited(prompt, "related_file", &[("path", &path_str)], |prompt| { - for excerpt in &file.excerpts { - push_delimited( - prompt, - "related_excerpt", - &[( - "lines", - &format!( - "{}-{}", - excerpt.row_range.start + 1, - excerpt.row_range.end + 1 - ), - )], - |prompt| { - prompt.push_str(&excerpt.text); - prompt.push('\n'); - }, - ); - } - }); - } - }); -} - -fn write_edit_history_section(prompt: &mut String, input: &ZetaPromptInput) { - push_delimited(prompt, "edit_history", &[], |prompt| { - if input.events.is_empty() { - prompt.push_str("(No edit history)"); - } else { - for event in &input.events { - write_event(prompt, event); - } - } - }); -} - -fn write_cursor_excerpt_section(prompt: &mut String, input: &ZetaPromptInput) { - push_delimited(prompt, "cursor_excerpt", &[], |prompt| { - let path_str = input.cursor_path.to_string_lossy(); - push_delimited(prompt, "file", &[("path", &path_str)], |prompt| { - prompt.push_str(&input.cursor_excerpt[..input.editable_range_in_excerpt.start]); - push_delimited(prompt, "editable_region", &[], |prompt| { - prompt.push_str( - &input.cursor_excerpt - [input.editable_range_in_excerpt.start..input.cursor_offset_in_excerpt], - ); - prompt.push_str(CURSOR_MARKER); - prompt.push_str( - &input.cursor_excerpt - [input.cursor_offset_in_excerpt..input.editable_range_in_excerpt.end], - ); - }); - prompt.push_str(&input.cursor_excerpt[input.editable_range_in_excerpt.end..]); - }); - }); -} - -fn push_delimited( - prompt: &mut String, - tag: &'static str, - arguments: &[(&str, &str)], - cb: impl FnOnce(&mut String), -) { - if !prompt.ends_with("\n") { - prompt.push('\n'); - } - prompt.push('<'); - prompt.push_str(tag); - for (arg_name, arg_value) in arguments { - write!(prompt, " {}=\"{}\"", arg_name, arg_value).ok(); - } - prompt.push_str(">\n"); - - cb(prompt); - - if !prompt.ends_with('\n') { - prompt.push('\n'); - } - prompt.push_str("\n"); -} diff --git a/crates/zlog/Cargo.toml b/crates/zlog/Cargo.toml deleted file mode 100644 index 2799592c8e..0000000000 --- a/crates/zlog/Cargo.toml +++ /dev/null @@ -1,24 +0,0 @@ -[package] -name = "zlog" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/zlog.rs" - -[features] -default = [] - -[dependencies] -collections.workspace = true -chrono.workspace = true -log.workspace = true -anyhow.workspace = true - -[dev-dependencies] -tempfile.workspace = true diff --git a/crates/zlog/LICENSE-GPL b/crates/zlog/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/zlog/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/zlog/README.md b/crates/zlog/README.md deleted file mode 100644 index 6d0fef147c..0000000000 --- a/crates/zlog/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Zlog - -Use the `ZED_LOG` environment variable to control logging output for Zed -applications and libraries. The variable accepts a comma-separated list of -directives that specify logging levels for different modules (crates). The -general format is for instance: - -``` -ZED_LOG=info,project=debug,agent=off -``` - -- Levels can be one of: `off`/`none`, `error`, `warn`, `info`, `debug`, or - `trace`. -- You don't need to specify the global level, default is `trace` in the crate - and `info` set by `RUST_LOG` in Zed. diff --git a/crates/zlog/src/env_config.rs b/crates/zlog/src/env_config.rs deleted file mode 100644 index 38d3adc179..0000000000 --- a/crates/zlog/src/env_config.rs +++ /dev/null @@ -1,122 +0,0 @@ -use anyhow::Result; - -pub struct EnvFilter { - pub level_global: Option, - pub directive_names: Vec, - pub directive_levels: Vec, -} - -pub fn parse(filter: &str) -> Result { - let mut max_level = None; - let mut directive_names = Vec::new(); - let mut directive_levels = Vec::new(); - - for directive in filter.split(',') { - match directive.split_once('=') { - Some((name, level)) => { - anyhow::ensure!(!level.contains('='), "Invalid directive: {directive}"); - let level = parse_level(level.trim())?; - directive_names.push(name.trim().trim_end_matches(".rs").to_string()); - directive_levels.push(level); - } - None => { - let Ok(level) = parse_level(directive.trim()) else { - directive_names.push(directive.trim().trim_end_matches(".rs").to_string()); - directive_levels.push(log::LevelFilter::max() /* Enable all levels */); - continue; - }; - anyhow::ensure!(max_level.is_none(), "Cannot set multiple max levels"); - max_level.replace(level); - } - }; - } - - Ok(EnvFilter { - level_global: max_level, - directive_names, - directive_levels, - }) -} - -fn parse_level(level: &str) -> Result { - if level.eq_ignore_ascii_case("TRACE") { - return Ok(log::LevelFilter::Trace); - } - if level.eq_ignore_ascii_case("DEBUG") { - return Ok(log::LevelFilter::Debug); - } - if level.eq_ignore_ascii_case("INFO") { - return Ok(log::LevelFilter::Info); - } - if level.eq_ignore_ascii_case("WARN") { - return Ok(log::LevelFilter::Warn); - } - if level.eq_ignore_ascii_case("ERROR") { - return Ok(log::LevelFilter::Error); - } - if level.eq_ignore_ascii_case("OFF") || level.eq_ignore_ascii_case("NONE") { - return Ok(log::LevelFilter::Off); - } - anyhow::bail!("Invalid level: {level}") -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn global_level() { - let input = "info"; - let filter = parse(input).unwrap(); - - assert_eq!(filter.level_global.unwrap(), log::LevelFilter::Info); - assert!(filter.directive_names.is_empty()); - assert!(filter.directive_levels.is_empty()); - } - - #[test] - fn directive_level() { - let input = "my_module=debug"; - let filter = parse(input).unwrap(); - - assert_eq!(filter.level_global, None); - assert_eq!(filter.directive_names, vec!["my_module".to_string()]); - assert_eq!(filter.directive_levels, vec![log::LevelFilter::Debug]); - } - - #[test] - fn global_level_and_directive_level() { - let input = "info,my_module=debug"; - let filter = parse(input).unwrap(); - - assert_eq!(filter.level_global.unwrap(), log::LevelFilter::Info); - assert_eq!(filter.directive_names, vec!["my_module".to_string()]); - assert_eq!(filter.directive_levels, vec![log::LevelFilter::Debug]); - } - - #[test] - fn global_level_and_bare_module() { - let input = "info,my_module"; - let filter = parse(input).unwrap(); - - assert_eq!(filter.level_global.unwrap(), log::LevelFilter::Info); - assert_eq!(filter.directive_names, vec!["my_module".to_string()]); - assert_eq!(filter.directive_levels, vec![log::LevelFilter::max()]); - } - - #[test] - fn err_when_multiple_max_levels() { - let input = "info,warn"; - let result = parse(input); - - assert!(result.is_err()); - } - - #[test] - fn err_when_invalid_level() { - let input = "my_module=foobar"; - let result = parse(input); - - assert!(result.is_err()); - } -} diff --git a/crates/zlog/src/filter.rs b/crates/zlog/src/filter.rs deleted file mode 100644 index 0be6f4ead5..0000000000 --- a/crates/zlog/src/filter.rs +++ /dev/null @@ -1,843 +0,0 @@ -use collections::HashMap; -use std::collections::VecDeque; -use std::sync::{ - OnceLock, RwLock, - atomic::{AtomicU8, Ordering}, -}; - -use crate::{SCOPE_DEPTH_MAX, SCOPE_STRING_SEP_STR, ScopeAlloc, ScopeRef, env_config, private}; - -use log; - -static ENV_FILTER: OnceLock = OnceLock::new(); -static SCOPE_MAP: RwLock = RwLock::new(ScopeMap::empty()); - -pub const LEVEL_ENABLED_MAX_DEFAULT: log::LevelFilter = log::LevelFilter::Info; -/// The maximum log level of verbosity that is enabled by default. -/// All messages more verbose than this level will be discarded -/// by default unless specially configured. -/// -/// This is used instead of the `log::max_level` as we need to tell the `log` -/// crate that the max level is everything, so that we can dynamically enable -/// logs that are more verbose than this level without the `log` crate throwing -/// them away before we see them -static LEVEL_ENABLED_MAX_STATIC: AtomicU8 = AtomicU8::new(LEVEL_ENABLED_MAX_DEFAULT as u8); - -/// A cache of the true maximum log level that _could_ be printed. This is based -/// on the maximally verbose level that is configured by the user, and is used -/// to filter out logs more verbose than any configured level. -/// -/// E.g. if `LEVEL_ENABLED_MAX_STATIC `is 'info' but a user has configured some -/// scope to print at a `debug` level, then this will be `debug`, and all -/// `trace` logs will be discarded. -/// Therefore, it should always be `>= LEVEL_ENABLED_MAX_STATIC` -// PERF: this doesn't need to be an atomic, we don't actually care about race conditions here -pub static LEVEL_ENABLED_MAX_CONFIG: AtomicU8 = AtomicU8::new(LEVEL_ENABLED_MAX_DEFAULT as u8); - -const DEFAULT_FILTERS: &[(&str, log::LevelFilter)] = &[ - #[cfg(any(target_os = "linux", target_os = "freebsd"))] - ("zbus", log::LevelFilter::Warn), - #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "windows"))] - ("blade_graphics", log::LevelFilter::Warn), - #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "windows"))] - ("naga::back::spv::writer", log::LevelFilter::Warn), - // usvg prints a lot of warnings on rendering an SVG with partial errors, which - // can happen a lot with the SVG preview - ("usvg::parser::style", log::LevelFilter::Error), -]; - -pub fn init_env_filter(filter: env_config::EnvFilter) { - if let Some(level_max) = filter.level_global { - LEVEL_ENABLED_MAX_STATIC.store(level_max as u8, Ordering::Release) - } - if ENV_FILTER.set(filter).is_err() { - panic!("Environment filter cannot be initialized twice"); - } -} - -pub fn is_possibly_enabled_level(level: log::Level) -> bool { - level as u8 <= LEVEL_ENABLED_MAX_CONFIG.load(Ordering::Acquire) -} - -pub fn is_scope_enabled( - scope: &ScopeRef<'_>, - module_path: Option<&str>, - level: log::Level, -) -> bool { - // TODO: is_always_allowed_level that checks against LEVEL_ENABLED_MIN_CONFIG - if !is_possibly_enabled_level(level) { - // [FAST PATH] - // if the message is above the maximum enabled log level - // (where error < warn < info etc) then disable without checking - // scope map - return false; - } - let is_enabled_by_default = level as u8 <= LEVEL_ENABLED_MAX_STATIC.load(Ordering::Acquire); - let global_scope_map = SCOPE_MAP.read().unwrap_or_else(|err| { - SCOPE_MAP.clear_poison(); - err.into_inner() - }); - - if global_scope_map.is_empty() { - // if no scopes are enabled, return false because it's not <= LEVEL_ENABLED_MAX_STATIC - return is_enabled_by_default; - } - let enabled_status = global_scope_map.is_enabled(scope, module_path, level); - match enabled_status { - EnabledStatus::NotConfigured => is_enabled_by_default, - EnabledStatus::Enabled => true, - EnabledStatus::Disabled => false, - } -} - -pub fn refresh_from_settings(settings: &HashMap) { - let env_config = ENV_FILTER.get(); - let map_new = ScopeMap::new_from_settings_and_env(settings, env_config, DEFAULT_FILTERS); - let mut level_enabled_max = LEVEL_ENABLED_MAX_STATIC.load(Ordering::Acquire); - for entry in &map_new.entries { - if let Some(level) = entry.enabled { - level_enabled_max = level_enabled_max.max(level as u8); - } - } - LEVEL_ENABLED_MAX_CONFIG.store(level_enabled_max, Ordering::Release); - - { - let mut global_map = SCOPE_MAP.write().unwrap_or_else(|err| { - SCOPE_MAP.clear_poison(); - err.into_inner() - }); - *global_map = map_new; - } - log::trace!("Log configuration updated"); -} - -fn level_filter_from_str(level_str: &str) -> Option { - use log::LevelFilter::*; - let level = match level_str.to_ascii_lowercase().as_str() { - "" => Trace, - "trace" => Trace, - "debug" => Debug, - "info" => Info, - "warn" => Warn, - "error" => Error, - "off" => Off, - "disable" | "no" | "none" | "disabled" => { - crate::warn!( - "Invalid log level \"{level_str}\", to disable logging set to \"off\". Defaulting to \"off\"." - ); - Off - } - _ => { - crate::warn!("Invalid log level \"{level_str}\", ignoring"); - return None; - } - }; - Some(level) -} - -fn scope_alloc_from_scope_str(scope_str: &str) -> Option { - let mut scope_buf = [""; SCOPE_DEPTH_MAX]; - let mut index = 0; - let mut scope_iter = scope_str.split(SCOPE_STRING_SEP_STR); - while index < SCOPE_DEPTH_MAX { - let Some(scope) = scope_iter.next() else { - break; - }; - if scope.is_empty() { - continue; - } - scope_buf[index] = scope; - index += 1; - } - if index == 0 { - return None; - } - if scope_iter.next().is_some() { - crate::warn!( - "Invalid scope key, too many nested scopes: '{scope_str}'. Max depth is {SCOPE_DEPTH_MAX}", - ); - return None; - } - let scope = scope_buf.map(|s| s.to_string()); - Some(scope) -} - -#[derive(Debug, PartialEq, Eq)] -pub struct ScopeMap { - entries: Vec, - modules: Vec<(String, log::LevelFilter)>, - root_count: usize, -} - -#[derive(Debug, PartialEq, Eq)] -pub struct ScopeMapEntry { - scope: String, - enabled: Option, - descendants: std::ops::Range, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum EnabledStatus { - Enabled, - Disabled, - NotConfigured, -} - -impl ScopeMap { - pub fn new_from_settings_and_env( - items_input_map: &HashMap, - env_config: Option<&env_config::EnvFilter>, - default_filters: &[(&str, log::LevelFilter)], - ) -> Self { - let mut items = Vec::<(ScopeAlloc, log::LevelFilter)>::with_capacity( - items_input_map.len() - + env_config.map_or(0, |c| c.directive_names.len()) - + default_filters.len(), - ); - let mut modules = Vec::with_capacity(4); - - let env_filters = env_config.iter().flat_map(|env_filter| { - env_filter - .directive_names - .iter() - .zip(env_filter.directive_levels.iter()) - .map(|(scope_str, level_filter)| (scope_str.as_str(), *level_filter)) - }); - - let new_filters = items_input_map.iter().filter_map(|(scope_str, level_str)| { - let level_filter = level_filter_from_str(level_str)?; - Some((scope_str.as_str(), level_filter)) - }); - - let all_filters = default_filters - .iter() - .cloned() - .chain(env_filters) - .chain(new_filters); - - for (scope_str, level_filter) in all_filters { - if scope_str.contains("::") { - if let Some(idx) = modules.iter().position(|(module, _)| module == scope_str) { - modules[idx].1 = level_filter; - } else { - modules.push((scope_str.to_string(), level_filter)); - } - continue; - } - let Some(scope) = scope_alloc_from_scope_str(scope_str) else { - continue; - }; - if let Some(idx) = items - .iter() - .position(|(scope_existing, _)| scope_existing == &scope) - { - items[idx].1 = level_filter; - } else { - items.push((scope, level_filter)); - } - } - - items.sort_by(|a, b| a.0.cmp(&b.0)); - modules.sort_by(|(a_name, _), (b_name, _)| a_name.cmp(b_name)); - - let mut this = Self { - entries: Vec::with_capacity(items.len() * SCOPE_DEPTH_MAX), - modules, - root_count: 0, - }; - - let items_count = items.len(); - - struct ProcessQueueEntry { - parent_index: usize, - depth: usize, - items_range: std::ops::Range, - } - let mut process_queue = VecDeque::new(); - process_queue.push_back(ProcessQueueEntry { - parent_index: usize::MAX, - depth: 0, - items_range: 0..items_count, - }); - - let empty_range = 0..0; - - while let Some(process_entry) = process_queue.pop_front() { - let ProcessQueueEntry { - items_range, - depth, - parent_index, - } = process_entry; - let mut cursor = items_range.start; - let res_entries_start = this.entries.len(); - while cursor < items_range.end { - let sub_items_start = cursor; - cursor += 1; - let scope_name = &items[sub_items_start].0[depth]; - while cursor < items_range.end && &items[cursor].0[depth] == scope_name { - cursor += 1; - } - let sub_items_end = cursor; - if scope_name.is_empty() { - assert_eq!(sub_items_start + 1, sub_items_end); - assert_ne!(depth, 0); - assert_ne!(parent_index, usize::MAX); - assert!(this.entries[parent_index].enabled.is_none()); - this.entries[parent_index].enabled = Some(items[sub_items_start].1); - continue; - } - let is_valid_scope = !scope_name.is_empty(); - let is_last = depth + 1 == SCOPE_DEPTH_MAX || !is_valid_scope; - let mut enabled = None; - if is_last { - assert_eq!( - sub_items_start + 1, - sub_items_end, - "Expected one item: got: {:?}", - &items[items_range] - ); - enabled = Some(items[sub_items_start].1); - } else { - let entry_index = this.entries.len(); - process_queue.push_back(ProcessQueueEntry { - items_range: sub_items_start..sub_items_end, - parent_index: entry_index, - depth: depth + 1, - }); - } - this.entries.push(ScopeMapEntry { - scope: scope_name.to_owned(), - enabled, - descendants: empty_range.clone(), - }); - } - let res_entries_end = this.entries.len(); - if parent_index != usize::MAX { - this.entries[parent_index].descendants = res_entries_start..res_entries_end; - } else { - this.root_count = res_entries_end; - } - } - - this - } - - pub fn is_empty(&self) -> bool { - self.entries.is_empty() && self.modules.is_empty() - } - - pub fn is_enabled( - &self, - scope: &[S; SCOPE_DEPTH_MAX], - module_path: Option<&str>, - level: log::Level, - ) -> EnabledStatus - where - S: AsRef, - { - fn search(map: &ScopeMap, scope: &[S; SCOPE_DEPTH_MAX]) -> Option - where - S: AsRef, - { - let mut enabled = None; - let mut cur_range = &map.entries[0..map.root_count]; - let mut depth = 0; - 'search: while !cur_range.is_empty() - && depth < SCOPE_DEPTH_MAX - && scope[depth].as_ref() != "" - { - for entry in cur_range { - if entry.scope == scope[depth].as_ref() { - enabled = entry.enabled.or(enabled); - cur_range = &map.entries[entry.descendants.clone()]; - depth += 1; - continue 'search; - } - } - break 'search; - } - enabled - } - - let mut enabled = search(self, scope); - - if let Some(module_path) = module_path { - let scope_is_empty = scope[0].as_ref().is_empty(); - - if enabled.is_none() && scope_is_empty { - let crate_name = private::extract_crate_name_from_module_path(module_path); - let mut crate_name_scope = [""; SCOPE_DEPTH_MAX]; - crate_name_scope[0] = crate_name; - enabled = search(self, &crate_name_scope); - } - - if !self.modules.is_empty() { - let crate_name = private::extract_crate_name_from_module_path(module_path); - let is_scope_just_crate_name = - scope[0].as_ref() == crate_name && scope[1].as_ref() == ""; - if enabled.is_none() || is_scope_just_crate_name { - for (module, filter) in &self.modules { - if module == module_path { - enabled.replace(*filter); - break; - } - } - } - } - } - - if let Some(enabled_filter) = enabled { - if level <= enabled_filter { - return EnabledStatus::Enabled; - } - return EnabledStatus::Disabled; - } - EnabledStatus::NotConfigured - } - - const fn empty() -> ScopeMap { - ScopeMap { - entries: vec![], - modules: vec![], - root_count: 0, - } - } -} - -#[cfg(test)] -mod tests { - use log::LevelFilter; - - use crate::Scope; - use crate::private::scope_new; - - use super::*; - - fn scope_map_from_keys(kv: &[(&str, &str)]) -> ScopeMap { - let hash_map: HashMap = kv - .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect(); - ScopeMap::new_from_settings_and_env(&hash_map, None, &[]) - } - - #[test] - fn test_initialization() { - let map = scope_map_from_keys(&[("a.b.c.d", "trace")]); - assert_eq!(map.root_count, 1); - assert_eq!(map.entries.len(), 4); - - let map = scope_map_from_keys(&[]); - assert_eq!(map.root_count, 0); - assert_eq!(map.entries.len(), 0); - - let map = scope_map_from_keys(&[("", "trace")]); - assert_eq!(map.root_count, 0); - assert_eq!(map.entries.len(), 0); - - let map = scope_map_from_keys(&[("foo..bar", "trace")]); - assert_eq!(map.root_count, 1); - assert_eq!(map.entries.len(), 2); - - let map = scope_map_from_keys(&[ - ("a.b.c.d", "trace"), - ("e.f.g.h", "debug"), - ("i.j.k.l", "info"), - ("m.n.o.p", "warn"), - ("q.r.s.t", "error"), - ]); - assert_eq!(map.root_count, 5); - assert_eq!(map.entries.len(), 20); - assert_eq!(map.entries[0].scope, "a"); - assert_eq!(map.entries[1].scope, "e"); - assert_eq!(map.entries[2].scope, "i"); - assert_eq!(map.entries[3].scope, "m"); - assert_eq!(map.entries[4].scope, "q"); - } - - fn scope_from_scope_str(scope_str: &'static str) -> Scope { - let mut scope_buf = [""; SCOPE_DEPTH_MAX]; - let mut index = 0; - let mut scope_iter = scope_str.split(SCOPE_STRING_SEP_STR); - while index < SCOPE_DEPTH_MAX { - let Some(scope) = scope_iter.next() else { - break; - }; - if scope.is_empty() { - continue; - } - scope_buf[index] = scope; - index += 1; - } - assert_ne!(index, 0); - assert!(scope_iter.next().is_none()); - scope_buf - } - - #[test] - fn test_is_enabled() { - let map = scope_map_from_keys(&[ - ("a.b.c.d", "trace"), - ("e.f.g.h", "debug"), - ("i.j.k.l", "info"), - ("m.n.o.p", "warn"), - ("q.r.s.t", "error"), - ]); - use log::Level; - assert_eq!( - map.is_enabled(&scope_from_scope_str("a.b.c.d"), None, Level::Trace), - EnabledStatus::Enabled - ); - assert_eq!( - map.is_enabled(&scope_from_scope_str("a.b.c.d"), None, Level::Debug), - EnabledStatus::Enabled - ); - - assert_eq!( - map.is_enabled(&scope_from_scope_str("e.f.g.h"), None, Level::Debug), - EnabledStatus::Enabled - ); - assert_eq!( - map.is_enabled(&scope_from_scope_str("e.f.g.h"), None, Level::Info), - EnabledStatus::Enabled - ); - assert_eq!( - map.is_enabled(&scope_from_scope_str("e.f.g.h"), None, Level::Trace), - EnabledStatus::Disabled - ); - - assert_eq!( - map.is_enabled(&scope_from_scope_str("i.j.k.l"), None, Level::Info), - EnabledStatus::Enabled - ); - assert_eq!( - map.is_enabled(&scope_from_scope_str("i.j.k.l"), None, Level::Warn), - EnabledStatus::Enabled - ); - assert_eq!( - map.is_enabled(&scope_from_scope_str("i.j.k.l"), None, Level::Debug), - EnabledStatus::Disabled - ); - - assert_eq!( - map.is_enabled(&scope_from_scope_str("m.n.o.p"), None, Level::Warn), - EnabledStatus::Enabled - ); - assert_eq!( - map.is_enabled(&scope_from_scope_str("m.n.o.p"), None, Level::Error), - EnabledStatus::Enabled - ); - assert_eq!( - map.is_enabled(&scope_from_scope_str("m.n.o.p"), None, Level::Info), - EnabledStatus::Disabled - ); - - assert_eq!( - map.is_enabled(&scope_from_scope_str("q.r.s.t"), None, Level::Error), - EnabledStatus::Enabled - ); - assert_eq!( - map.is_enabled(&scope_from_scope_str("q.r.s.t"), None, Level::Warn), - EnabledStatus::Disabled - ); - } - - #[test] - fn test_is_enabled_module() { - let mut map = scope_map_from_keys(&[("a", "trace")]); - map.modules = [("a::b::c", "trace"), ("a::b::d", "debug")] - .map(|(k, v)| (k.to_string(), v.parse().unwrap())) - .to_vec(); - use log::Level; - assert_eq!( - map.is_enabled( - &scope_from_scope_str("__unused__"), - Some("a::b::c"), - Level::Trace - ), - EnabledStatus::Enabled - ); - assert_eq!( - map.is_enabled( - &scope_from_scope_str("__unused__"), - Some("a::b::d"), - Level::Debug - ), - EnabledStatus::Enabled - ); - assert_eq!( - map.is_enabled( - &scope_from_scope_str("__unused__"), - Some("a::b::d"), - Level::Trace, - ), - EnabledStatus::Disabled - ); - assert_eq!( - map.is_enabled( - &scope_from_scope_str("__unused__"), - Some("a::e"), - Level::Info - ), - EnabledStatus::NotConfigured - ); - // when scope is just crate name, more specific module path overrides it - assert_eq!( - map.is_enabled(&scope_from_scope_str("a"), Some("a::b::d"), Level::Trace), - EnabledStatus::Disabled, - ); - // but when it is scoped, the scope overrides the module path - assert_eq!( - map.is_enabled( - &scope_from_scope_str("a.scope"), - Some("a::b::d"), - Level::Trace - ), - EnabledStatus::Enabled, - ); - } - - fn scope_map_from_keys_and_env(kv: &[(&str, &str)], env: &env_config::EnvFilter) -> ScopeMap { - let hash_map: HashMap = kv - .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect(); - ScopeMap::new_from_settings_and_env(&hash_map, Some(env), &[]) - } - - #[test] - fn test_initialization_with_env() { - let env_filter = env_config::parse("a.b=debug,u=error").unwrap(); - let map = scope_map_from_keys_and_env(&[], &env_filter); - assert_eq!(map.root_count, 2); - assert_eq!(map.entries.len(), 3); - assert_eq!( - map.is_enabled(&scope_new(&["a"]), None, log::Level::Debug), - EnabledStatus::NotConfigured - ); - assert_eq!( - map.is_enabled(&scope_new(&["a", "b"]), None, log::Level::Debug), - EnabledStatus::Enabled - ); - assert_eq!( - map.is_enabled(&scope_new(&["a", "b", "c"]), None, log::Level::Trace), - EnabledStatus::Disabled - ); - - let env_filter = env_config::parse("a.b=debug,e.f.g.h=trace,u=error").unwrap(); - let map = scope_map_from_keys_and_env( - &[ - ("a.b.c.d", "trace"), - ("e.f.g.h", "debug"), - ("i.j.k.l", "info"), - ("m.n.o.p", "warn"), - ("q.r.s.t", "error"), - ], - &env_filter, - ); - assert_eq!(map.root_count, 6); - assert_eq!(map.entries.len(), 21); - assert_eq!(map.entries[0].scope, "a"); - assert_eq!(map.entries[1].scope, "e"); - assert_eq!(map.entries[2].scope, "i"); - assert_eq!(map.entries[3].scope, "m"); - assert_eq!(map.entries[4].scope, "q"); - assert_eq!(map.entries[5].scope, "u"); - assert_eq!( - map.is_enabled(&scope_new(&["a", "b", "c", "d"]), None, log::Level::Trace), - EnabledStatus::Enabled - ); - assert_eq!( - map.is_enabled(&scope_new(&["a", "b", "c"]), None, log::Level::Trace), - EnabledStatus::Disabled - ); - assert_eq!( - map.is_enabled(&scope_new(&["u", "v"]), None, log::Level::Warn), - EnabledStatus::Disabled - ); - // settings override env - assert_eq!( - map.is_enabled(&scope_new(&["e", "f", "g", "h"]), None, log::Level::Trace), - EnabledStatus::Disabled, - ); - } - - fn scope_map_from_all( - kv: &[(&str, &str)], - env: &env_config::EnvFilter, - default_filters: &[(&str, log::LevelFilter)], - ) -> ScopeMap { - let hash_map: HashMap = kv - .iter() - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect(); - ScopeMap::new_from_settings_and_env(&hash_map, Some(env), default_filters) - } - - #[test] - fn precedence() { - // Test precedence: kv > env > default - - // Default filters - these should be overridden by env and kv when they overlap - let default_filters = &[ - ("a.b.c", log::LevelFilter::Debug), // Should be overridden by env - ("p.q.r", log::LevelFilter::Info), // Should be overridden by kv - ("x.y.z", log::LevelFilter::Warn), // Not overridden - ("crate::module::default", log::LevelFilter::Error), // Module in default - ("crate::module::user", log::LevelFilter::Off), // Module disabled in default - ]; - - // Environment filters - these should override default but be overridden by kv - let env_filter = - env_config::parse("a.b.c=trace,p.q=debug,m.n.o=error,crate::module::env=debug") - .unwrap(); - - // Key-value filters (highest precedence) - these should override everything - let kv_filters = &[ - ("p.q.r", "trace"), // Overrides default - ("m.n.o", "warn"), // Overrides env - ("j.k.l", "info"), // New filter - ("crate::module::env", "trace"), // Overrides env for module - ("crate::module::kv", "trace"), // New module filter - ]; - - let map = scope_map_from_all(kv_filters, &env_filter, default_filters); - - // Test scope precedence - use log::Level; - - // KV overrides all for scopes - assert_eq!( - map.is_enabled(&scope_from_scope_str("p.q.r"), None, Level::Trace), - EnabledStatus::Enabled, - "KV should override default filters for scopes" - ); - assert_eq!( - map.is_enabled(&scope_from_scope_str("m.n.o"), None, Level::Warn), - EnabledStatus::Enabled, - "KV should override env filters for scopes" - ); - assert_eq!( - map.is_enabled(&scope_from_scope_str("m.n.o"), None, Level::Debug), - EnabledStatus::Disabled, - "KV correctly limits log level" - ); - - // ENV overrides default but not KV for scopes - assert_eq!( - map.is_enabled(&scope_from_scope_str("a.b.c"), None, Level::Trace), - EnabledStatus::Enabled, - "ENV should override default filters for scopes" - ); - - // Default is used when no override exists for scopes - assert_eq!( - map.is_enabled(&scope_from_scope_str("x.y.z"), None, Level::Warn), - EnabledStatus::Enabled, - "Default filters should work when not overridden" - ); - assert_eq!( - map.is_enabled(&scope_from_scope_str("x.y.z"), None, Level::Info), - EnabledStatus::Disabled, - "Default filters correctly limit log level" - ); - - // KV overrides all for modules - assert_eq!( - map.is_enabled(&scope_new(&[""]), Some("crate::module::env"), Level::Trace), - EnabledStatus::Enabled, - "KV should override env filters for modules" - ); - assert_eq!( - map.is_enabled(&scope_new(&[""]), Some("crate::module::kv"), Level::Trace), - EnabledStatus::Enabled, - "KV module filters should work" - ); - - // ENV overrides default for modules - assert_eq!( - map.is_enabled(&scope_new(&[""]), Some("crate::module::env"), Level::Debug), - EnabledStatus::Enabled, - "ENV should override default for modules" - ); - - // Default is used when no override exists for modules - assert_eq!( - map.is_enabled( - &scope_new(&[""]), - Some("crate::module::default"), - Level::Error - ), - EnabledStatus::Enabled, - "Default filters should work for modules" - ); - assert_eq!( - map.is_enabled( - &scope_new(&[""]), - Some("crate::module::default"), - Level::Warn - ), - EnabledStatus::Disabled, - "Default filters correctly limit log level for modules" - ); - - assert_eq!( - map.is_enabled(&scope_new(&[""]), Some("crate::module::user"), Level::Error), - EnabledStatus::Disabled, - "Module turned off in default filters is not enabled" - ); - - assert_eq!( - map.is_enabled( - &scope_new(&["crate"]), - Some("crate::module::user"), - Level::Error - ), - EnabledStatus::Disabled, - "Module turned off in default filters is not enabled, even with crate name as scope" - ); - - // Test non-conflicting but similar paths - - // Test that "a.b" and "a.b.c" don't conflict (different depth) - assert_eq!( - map.is_enabled(&scope_from_scope_str("a.b.c.d"), None, Level::Trace), - EnabledStatus::Enabled, - "Scope a.b.c should inherit from a.b env filter" - ); - assert_eq!( - map.is_enabled(&scope_from_scope_str("a.b.c"), None, Level::Trace), - EnabledStatus::Enabled, - "Scope a.b.c.d should use env filter level (trace)" - ); - - // Test that similar module paths don't conflict - assert_eq!( - map.is_enabled(&scope_new(&[""]), Some("crate::module"), Level::Error), - EnabledStatus::NotConfigured, - "Module crate::module should not be affected by crate::module::default filter" - ); - assert_eq!( - map.is_enabled( - &scope_new(&[""]), - Some("crate::module::default::sub"), - Level::Error - ), - EnabledStatus::NotConfigured, - "Module crate::module::default::sub should not be affected by crate::module::default filter" - ); - } - - #[test] - fn default_filter_crate() { - let default_filters = &[("crate", LevelFilter::Off)]; - let map = scope_map_from_all(&[], &env_config::parse("").unwrap(), default_filters); - - use log::Level; - assert_eq!( - map.is_enabled(&scope_new(&[""]), Some("crate::submodule"), Level::Error), - EnabledStatus::Disabled, - "crate::submodule should be disabled by disabling `crate` filter" - ); - } -} diff --git a/crates/zlog/src/sink.rs b/crates/zlog/src/sink.rs deleted file mode 100644 index 07e87be1b0..0000000000 --- a/crates/zlog/src/sink.rs +++ /dev/null @@ -1,324 +0,0 @@ -use std::{ - fs, - io::{self, Write}, - path::PathBuf, - sync::{ - Mutex, OnceLock, - atomic::{AtomicBool, AtomicU64, Ordering}, - }, -}; - -use crate::{SCOPE_STRING_SEP_CHAR, ScopeRef}; - -// ANSI color escape codes for log levels -const ANSI_RESET: &str = "\x1b[0m"; -const ANSI_BOLD: &str = "\x1b[1m"; -const ANSI_RED: &str = "\x1b[31m"; -const ANSI_YELLOW: &str = "\x1b[33m"; -const ANSI_GREEN: &str = "\x1b[32m"; -const ANSI_BLUE: &str = "\x1b[34m"; -const ANSI_MAGENTA: &str = "\x1b[35m"; - -/// Is Some(file) if file output is enabled. -static ENABLED_SINKS_FILE: Mutex> = Mutex::new(None); -static SINK_FILE_PATH: OnceLock<&'static PathBuf> = OnceLock::new(); -static SINK_FILE_PATH_ROTATE: OnceLock<&'static PathBuf> = OnceLock::new(); - -// NB: Since this can be accessed in tests, we probably should stick to atomics here. -/// Whether stdout output is enabled. -static ENABLED_SINKS_STDOUT: AtomicBool = AtomicBool::new(false); -/// Whether stderr output is enabled. -static ENABLED_SINKS_STDERR: AtomicBool = AtomicBool::new(false); -/// Atomic counter for the size of the log file in bytes. -static SINK_FILE_SIZE_BYTES: AtomicU64 = AtomicU64::new(0); -/// Maximum size of the log file before it will be rotated, in bytes. -const SINK_FILE_SIZE_BYTES_MAX: u64 = 1024 * 1024; // 1 MB - -pub struct Record<'a> { - pub scope: ScopeRef<'a>, - pub level: log::Level, - pub message: &'a std::fmt::Arguments<'a>, - pub module_path: Option<&'a str>, - pub line: Option, -} - -pub fn init_output_stdout() { - // Use atomics here instead of just a `static mut`, since in the context - // of tests these accesses can be multi-threaded. - ENABLED_SINKS_STDOUT.store(true, Ordering::Release); -} - -pub fn init_output_stderr() { - ENABLED_SINKS_STDERR.store(true, Ordering::Release); -} - -pub fn init_output_file( - path: &'static PathBuf, - path_rotate: Option<&'static PathBuf>, -) -> io::Result<()> { - let mut file = std::fs::OpenOptions::new() - .create(true) - .append(true) - .open(path)?; - - SINK_FILE_PATH - .set(path) - .expect("Init file output should only be called once"); - if let Some(path_rotate) = path_rotate { - SINK_FILE_PATH_ROTATE - .set(path_rotate) - .expect("Init file output should only be called once"); - } - - let mut enabled_sinks_file = ENABLED_SINKS_FILE - .try_lock() - .expect("Log file lock is available during init"); - - let size_bytes = file.metadata().map_or(0, |metadata| metadata.len()); - if size_bytes >= SINK_FILE_SIZE_BYTES_MAX { - rotate_log_file(&mut file, Some(path), path_rotate, &SINK_FILE_SIZE_BYTES); - } else { - SINK_FILE_SIZE_BYTES.store(size_bytes, Ordering::Release); - } - - *enabled_sinks_file = Some(file); - - Ok(()) -} - -const LEVEL_OUTPUT_STRINGS: [&str; 6] = [ - " ", // nop: ERROR = 1 - "ERROR", // - "WARN ", // - "INFO ", // - "DEBUG", // - "TRACE", // -]; - -// Colors for different log levels -static LEVEL_ANSI_COLORS: [&str; 6] = [ - "", // nop - ANSI_RED, // Error: Red - ANSI_YELLOW, // Warn: Yellow - ANSI_GREEN, // Info: Green - ANSI_BLUE, // Debug: Blue - ANSI_MAGENTA, // Trace: Magenta -]; - -// PERF: batching -pub fn submit(mut record: Record) { - if record.module_path.is_none_or(|p| !p.ends_with(".rs")) { - // Only render line numbers for actual rust files emitted by `log_err` and friends - record.line.take(); - } - if ENABLED_SINKS_STDOUT.load(Ordering::Acquire) { - let mut stdout = std::io::stdout().lock(); - _ = writeln!( - &mut stdout, - "{} {ANSI_BOLD}{}{}{ANSI_RESET} {} {}", - chrono::Local::now().format("%Y-%m-%dT%H:%M:%S%:z"), - LEVEL_ANSI_COLORS[record.level as usize], - LEVEL_OUTPUT_STRINGS[record.level as usize], - SourceFmt { - scope: record.scope, - module_path: record.module_path, - line: record.line, - ansi: true, - }, - record.message - ); - } else if ENABLED_SINKS_STDERR.load(Ordering::Acquire) { - let mut stdout = std::io::stderr().lock(); - _ = writeln!( - &mut stdout, - "{} {ANSI_BOLD}{}{}{ANSI_RESET} {} {}", - chrono::Local::now().format("%Y-%m-%dT%H:%M:%S%:z"), - LEVEL_ANSI_COLORS[record.level as usize], - LEVEL_OUTPUT_STRINGS[record.level as usize], - SourceFmt { - scope: record.scope, - module_path: record.module_path, - line: record.line, - ansi: true, - }, - record.message - ); - } - let mut file = ENABLED_SINKS_FILE.lock().unwrap_or_else(|handle| { - ENABLED_SINKS_FILE.clear_poison(); - handle.into_inner() - }); - if let Some(file) = file.as_mut() { - struct SizedWriter<'a> { - file: &'a mut std::fs::File, - written: u64, - } - impl io::Write for SizedWriter<'_> { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.file.write(buf)?; - self.written += buf.len() as u64; - Ok(buf.len()) - } - - fn flush(&mut self) -> io::Result<()> { - self.file.flush() - } - } - let file_size_bytes = { - let mut writer = SizedWriter { file, written: 0 }; - _ = writeln!( - &mut writer, - "{} {} {} {}", - chrono::Local::now().format("%Y-%m-%dT%H:%M:%S%:z"), - LEVEL_OUTPUT_STRINGS[record.level as usize], - SourceFmt { - scope: record.scope, - module_path: record.module_path, - line: record.line, - ansi: false, - }, - record.message - ); - SINK_FILE_SIZE_BYTES.fetch_add(writer.written, Ordering::AcqRel) + writer.written - }; - if file_size_bytes > SINK_FILE_SIZE_BYTES_MAX { - rotate_log_file( - file, - SINK_FILE_PATH.get(), - SINK_FILE_PATH_ROTATE.get(), - &SINK_FILE_SIZE_BYTES, - ); - } - } -} - -pub fn flush() { - if ENABLED_SINKS_STDOUT.load(Ordering::Acquire) { - _ = std::io::stdout().lock().flush(); - } - let mut file = ENABLED_SINKS_FILE.lock().unwrap_or_else(|handle| { - ENABLED_SINKS_FILE.clear_poison(); - handle.into_inner() - }); - if let Some(file) = file.as_mut() - && let Err(err) = file.flush() - { - eprintln!("Failed to flush log file: {}", err); - } -} - -struct SourceFmt<'a> { - scope: ScopeRef<'a>, - module_path: Option<&'a str>, - line: Option, - ansi: bool, -} - -impl std::fmt::Display for SourceFmt<'_> { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - use std::fmt::Write; - f.write_char('[')?; - if self.ansi { - f.write_str(ANSI_BOLD)?; - } - // NOTE: if no longer prefixing scopes with their crate name, check if scope[0] is empty - if (self.scope[1].is_empty() && self.module_path.is_some()) || self.scope[0].is_empty() { - f.write_str(self.module_path.unwrap_or("?"))?; - } else { - f.write_str(self.scope[0])?; - for subscope in &self.scope[1..] { - if subscope.is_empty() { - break; - } - f.write_char(SCOPE_STRING_SEP_CHAR)?; - f.write_str(subscope)?; - } - } - if let Some(line) = self.line { - f.write_char(':')?; - line.fmt(f)?; - } - if self.ansi { - f.write_str(ANSI_RESET)?; - } - f.write_char(']')?; - Ok(()) - } -} - -fn rotate_log_file( - file: &mut fs::File, - path: Option, - path_rotate: Option, - atomic_size: &AtomicU64, -) where - PathRef: AsRef, -{ - if let Err(err) = file.flush() { - eprintln!( - "Failed to flush log file before rotating, some logs may be lost: {}", - err - ); - } - let rotation_error = match (path, path_rotate) { - (Some(_), None) => Some(anyhow::anyhow!("No rotation log file path configured")), - (None, _) => Some(anyhow::anyhow!("No log file path configured")), - (Some(path), Some(path_rotate)) => fs::copy(path, path_rotate) - .err() - .map(|err| anyhow::anyhow!(err)), - }; - if let Some(err) = rotation_error { - eprintln!("Log file rotation failed. Truncating log file anyways: {err}",); - } - _ = file.set_len(0); - - // SAFETY: It is safe to set size to 0 even if set_len fails as - // according to the documentation, it only fails if: - // - the file is not writeable: should never happen, - // - the size would cause an overflow (implementation specific): 0 should never cause an overflow - atomic_size.store(0, Ordering::Release); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_rotate_log_file() { - let temp_dir = tempfile::tempdir().unwrap(); - let log_file_path = temp_dir.path().join("log.txt"); - let rotation_log_file_path = temp_dir.path().join("log_rotated.txt"); - - let mut file = fs::File::create(&log_file_path).unwrap(); - let contents = String::from("Hello, world!"); - file.write_all(contents.as_bytes()).unwrap(); - - let size = AtomicU64::new(contents.len() as u64); - - rotate_log_file( - &mut file, - Some(&log_file_path), - Some(&rotation_log_file_path), - &size, - ); - - assert!(log_file_path.exists()); - assert_eq!(log_file_path.metadata().unwrap().len(), 0); - assert!(rotation_log_file_path.exists()); - assert_eq!( - std::fs::read_to_string(&rotation_log_file_path).unwrap(), - contents, - ); - assert_eq!(size.load(Ordering::Acquire), 0); - } - - /// Regression test, ensuring that if log level values change we are made aware - #[test] - fn test_log_level_names() { - assert_eq!(LEVEL_OUTPUT_STRINGS[log::Level::Error as usize], "ERROR"); - assert_eq!(LEVEL_OUTPUT_STRINGS[log::Level::Warn as usize], "WARN "); - assert_eq!(LEVEL_OUTPUT_STRINGS[log::Level::Info as usize], "INFO "); - assert_eq!(LEVEL_OUTPUT_STRINGS[log::Level::Debug as usize], "DEBUG"); - assert_eq!(LEVEL_OUTPUT_STRINGS[log::Level::Trace as usize], "TRACE"); - } -} diff --git a/crates/zlog/src/zlog.rs b/crates/zlog/src/zlog.rs deleted file mode 100644 index 3c154f7908..0000000000 --- a/crates/zlog/src/zlog.rs +++ /dev/null @@ -1,409 +0,0 @@ -//! # logger -pub use log as log_impl; - -mod env_config; -pub mod filter; -pub mod sink; - -pub use sink::{flush, init_output_file, init_output_stderr, init_output_stdout}; - -pub const SCOPE_DEPTH_MAX: usize = 4; - -pub fn init() { - if let Err(err) = try_init(None) { - log::error!("{err}"); - eprintln!("{err}"); - } -} - -pub fn try_init(filter: Option) -> anyhow::Result<()> { - log::set_logger(&ZLOG)?; - log::set_max_level(log::LevelFilter::max()); - process_env(filter); - filter::refresh_from_settings(&std::collections::HashMap::default()); - Ok(()) -} - -pub fn init_test() { - if get_env_config().is_some() && try_init(None).is_ok() { - init_output_stdout(); - } -} - -fn get_env_config() -> Option { - std::env::var("ZED_LOG") - .or_else(|_| std::env::var("RUST_LOG")) - .ok() - .or_else(|| { - if std::env::var("CI").is_ok() { - Some("info".to_owned()) - } else { - None - } - }) -} - -pub fn process_env(filter: Option) { - let Some(env_config) = get_env_config().or(filter) else { - return; - }; - match env_config::parse(&env_config) { - Ok(filter) => { - filter::init_env_filter(filter); - } - Err(err) => { - eprintln!("Failed to parse log filter: {}", err); - } - } -} - -static ZLOG: Zlog = Zlog {}; - -pub struct Zlog {} - -impl log::Log for Zlog { - fn enabled(&self, metadata: &log::Metadata) -> bool { - filter::is_possibly_enabled_level(metadata.level()) - } - - fn log(&self, record: &log::Record) { - if !self.enabled(record.metadata()) { - return; - } - let module_path = record.module_path().or(record.file()); - let (crate_name_scope, module_scope) = match module_path { - Some(module_path) => { - let crate_name = private::extract_crate_name_from_module_path(module_path); - let crate_name_scope = private::scope_ref_new(&[crate_name]); - let module_scope = private::scope_ref_new(&[module_path]); - (crate_name_scope, module_scope) - } - None => { - // TODO: when do we hit this - (private::scope_new(&[]), private::scope_new(&["*unknown*"])) - } - }; - let level = record.metadata().level(); - if !filter::is_scope_enabled(&crate_name_scope, Some(record.target()), level) { - return; - } - sink::submit(sink::Record { - scope: module_scope, - level, - message: record.args(), - // PERF(batching): store non-static paths in a cache + leak them and pass static str here - module_path, - line: record.line(), - }); - } - - fn flush(&self) { - sink::flush(); - } -} - -#[macro_export] -macro_rules! log { - ($logger:expr, $level:expr, $($arg:tt)+) => { - let level = $level; - let logger = $logger; - let enabled = $crate::filter::is_scope_enabled(&logger.scope, Some(module_path!()), level); - if enabled { - $crate::sink::submit($crate::sink::Record { - scope: logger.scope, - level, - message: &format_args!($($arg)+), - module_path: Some(module_path!()), - line: Some(line!()), - }); - } - } -} - -#[macro_export] -macro_rules! trace { - ($logger:expr => $($arg:tt)+) => { - $crate::log!($logger, $crate::log_impl::Level::Trace, $($arg)+); - }; - ($($arg:tt)+) => { - $crate::log!($crate::default_logger!(), $crate::log_impl::Level::Trace, $($arg)+); - }; -} - -#[macro_export] -macro_rules! debug { - ($logger:expr => $($arg:tt)+) => { - $crate::log!($logger, $crate::log_impl::Level::Debug, $($arg)+); - }; - ($($arg:tt)+) => { - $crate::log!($crate::default_logger!(), $crate::log_impl::Level::Debug, $($arg)+); - }; -} - -#[macro_export] -macro_rules! info { - ($logger:expr => $($arg:tt)+) => { - $crate::log!($logger, $crate::log_impl::Level::Info, $($arg)+); - }; - ($($arg:tt)+) => { - $crate::log!($crate::default_logger!(), $crate::log_impl::Level::Info, $($arg)+); - }; -} - -#[macro_export] -macro_rules! warn { - ($logger:expr => $($arg:tt)+) => { - $crate::log!($logger, $crate::log_impl::Level::Warn, $($arg)+); - }; - ($($arg:tt)+) => { - $crate::log!($crate::default_logger!(), $crate::log_impl::Level::Warn, $($arg)+); - }; -} - -#[macro_export] -macro_rules! error { - ($logger:expr => $($arg:tt)+) => { - $crate::log!($logger, $crate::log_impl::Level::Error, $($arg)+); - }; - ($($arg:tt)+) => { - $crate::log!($crate::default_logger!(), $crate::log_impl::Level::Error, $($arg)+); - }; -} - -/// Creates a timer that logs the duration it was active for either when -/// it is dropped, or when explicitly stopped using the `end` method. -/// Logs at the `trace` level. -/// Note that it will include time spent across await points -/// (i.e. should not be used to measure the performance of async code) -/// However, this is a feature not a bug, as it allows for a more accurate -/// understanding of how long the action actually took to complete, including -/// interruptions, which can help explain why something may have timed out, -/// why it took longer to complete than it would have had the await points resolved -/// immediately, etc. -#[macro_export] -macro_rules! time { - ($logger:expr => $name:expr) => { - $crate::Timer::new($logger, $name) - }; - ($name:expr) => { - $crate::time!($crate::default_logger!() => $name) - }; -} - -#[macro_export] -macro_rules! scoped { - ($parent:expr => $name:expr) => {{ - $crate::scoped_logger($parent, $name) - }}; - ($name:expr) => { - $crate::scoped!($crate::default_logger!() => $name) - }; -} - -pub const fn scoped_logger(parent: Logger, name: &'static str) -> Logger { - let mut scope = parent.scope; - let mut index = 1; // always have crate/module name - while index < scope.len() && !scope[index].is_empty() { - index += 1; - } - if index >= scope.len() { - #[cfg(debug_assertions)] - { - panic!("Scope overflow trying to add scope... ignoring scope"); - } - } - scope[index] = name; - Logger { scope } -} - -#[macro_export] -macro_rules! default_logger { - () => { - $crate::Logger { - scope: $crate::private::scope_new(&[$crate::crate_name!()]), - } - }; -} - -#[macro_export] -macro_rules! crate_name { - () => { - $crate::private::extract_crate_name_from_module_path(module_path!()) - }; -} - -/// functions that are used in macros, and therefore must be public, -/// but should not be used directly -pub mod private { - use super::*; - - pub const fn extract_crate_name_from_module_path(module_path: &str) -> &str { - let mut i = 0; - let mod_path_bytes = module_path.as_bytes(); - let mut index = mod_path_bytes.len(); - while i + 1 < mod_path_bytes.len() { - if mod_path_bytes[i] == b':' && mod_path_bytes[i + 1] == b':' { - index = i; - break; - } - i += 1; - } - let Some((crate_name, _)) = module_path.split_at_checked(index) else { - return module_path; - }; - crate_name - } - - pub const fn scope_new(scopes: &[&'static str]) -> Scope { - scope_ref_new(scopes) - } - - pub const fn scope_ref_new<'a>(scopes: &[&'a str]) -> ScopeRef<'a> { - assert!(scopes.len() <= SCOPE_DEPTH_MAX); - let mut scope = [""; SCOPE_DEPTH_MAX]; - let mut i = 0; - while i < scopes.len() { - scope[i] = scopes[i]; - i += 1; - } - scope - } - - pub fn scope_alloc_new(scopes: &[&str]) -> ScopeAlloc { - assert!(scopes.len() <= SCOPE_DEPTH_MAX); - let mut scope = [""; SCOPE_DEPTH_MAX]; - scope[0..scopes.len()].copy_from_slice(scopes); - scope.map(|s| s.to_string()) - } - - pub fn scope_to_alloc(scope: &Scope) -> ScopeAlloc { - scope.map(|s| s.to_string()) - } -} - -pub type Scope = [&'static str; SCOPE_DEPTH_MAX]; -pub type ScopeRef<'a> = [&'a str; SCOPE_DEPTH_MAX]; -pub type ScopeAlloc = [String; SCOPE_DEPTH_MAX]; -const SCOPE_STRING_SEP_STR: &str = "."; -const SCOPE_STRING_SEP_CHAR: char = '.'; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub struct Logger { - pub scope: Scope, -} - -impl log::Log for Logger { - fn enabled(&self, metadata: &log::Metadata) -> bool { - filter::is_possibly_enabled_level(metadata.level()) - } - - fn log(&self, record: &log::Record) { - if !self.enabled(record.metadata()) { - return; - } - let level = record.metadata().level(); - if !filter::is_scope_enabled(&self.scope, Some(record.target()), level) { - return; - } - sink::submit(sink::Record { - scope: self.scope, - level, - message: record.args(), - module_path: record.module_path(), - line: record.line(), - }); - } - - fn flush(&self) { - sink::flush(); - } -} - -pub struct Timer { - pub logger: Logger, - pub start_time: std::time::Instant, - pub name: &'static str, - pub warn_if_longer_than: Option, - pub done: bool, -} - -impl Drop for Timer { - fn drop(&mut self) { - self.finish(); - } -} - -impl Timer { - #[must_use = "Timer will stop when dropped, the result of this function should be saved in a variable prefixed with `_` if it should stop when dropped"] - pub fn new(logger: Logger, name: &'static str) -> Self { - Self { - logger, - name, - start_time: std::time::Instant::now(), - warn_if_longer_than: None, - done: false, - } - } - - pub fn warn_if_gt(mut self, warn_limit: std::time::Duration) -> Self { - self.warn_if_longer_than = Some(warn_limit); - self - } - - pub fn end(mut self) { - self.finish(); - } - - fn finish(&mut self) { - if self.done { - return; - } - let elapsed = self.start_time.elapsed(); - if let Some(warn_limit) = self.warn_if_longer_than - && elapsed > warn_limit - { - crate::warn!( - self.logger => - "Timer '{}' took {:?}. Which was longer than the expected limit of {:?}", - self.name, - elapsed, - warn_limit - ); - self.done = true; - return; - } - crate::trace!( - self.logger => - "Timer '{}' finished in {:?}", - self.name, - elapsed - ); - self.done = true; - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_crate_name() { - assert_eq!(crate_name!(), "zlog"); - assert_eq!( - private::extract_crate_name_from_module_path("my_speedy_⚡️_crate::some_module"), - "my_speedy_⚡️_crate" - ); - assert_eq!( - private::extract_crate_name_from_module_path("my_speedy_crate_⚡️::some_module"), - "my_speedy_crate_⚡️" - ); - assert_eq!( - private::extract_crate_name_from_module_path("my_speedy_crate_:⚡️:some_module"), - "my_speedy_crate_:⚡️:some_module" - ); - assert_eq!( - private::extract_crate_name_from_module_path("my_speedy_crate_::⚡️some_module"), - "my_speedy_crate_" - ); - } -} diff --git a/crates/zlog_settings/Cargo.toml b/crates/zlog_settings/Cargo.toml deleted file mode 100644 index 39c3b6a193..0000000000 --- a/crates/zlog_settings/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "zlog_settings" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[lib] -path = "src/zlog_settings.rs" - -[features] -default = [] - -[dependencies] -gpui.workspace = true -collections.workspace = true -settings.workspace = true -zlog.workspace = true diff --git a/crates/zlog_settings/LICENSE-GPL b/crates/zlog_settings/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/zlog_settings/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/zlog_settings/src/zlog_settings.rs b/crates/zlog_settings/src/zlog_settings.rs deleted file mode 100644 index cb09375b9a..0000000000 --- a/crates/zlog_settings/src/zlog_settings.rs +++ /dev/null @@ -1,30 +0,0 @@ -//! # zlog_settings -use collections::HashMap; - -use gpui::App; -use settings::{RegisterSetting, Settings, SettingsStore}; - -pub fn init(cx: &mut App) { - cx.observe_global::(|cx| { - let zlog_settings = ZlogSettings::get_global(cx); - zlog::filter::refresh_from_settings(&zlog_settings.scopes); - }) - .detach(); -} - -#[derive(Clone, Debug, RegisterSetting)] -pub struct ZlogSettings { - /// A map of log scopes to the desired log level. - /// Useful for filtering out noisy logs or enabling more verbose logging. - /// - /// Example: {"log": {"client": "warn"}} - pub scopes: HashMap, -} - -impl Settings for ZlogSettings { - fn from_settings(content: &settings::SettingsContent) -> Self { - ZlogSettings { - scopes: content.log.clone().unwrap(), - } - } -} diff --git a/crates/ztracing/Cargo.toml b/crates/ztracing/Cargo.toml deleted file mode 100644 index 0d9f15b9af..0000000000 --- a/crates/ztracing/Cargo.toml +++ /dev/null @@ -1,21 +0,0 @@ -[package] -name = "ztracing" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[features] -tracy = ["tracing-tracy"] - -[dependencies] -zlog.workspace = true -tracing.workspace = true - -tracing-subscriber = "0.3.22" -tracing-tracy = { version = "0.11.4", optional = true, features = ["enable", "ondemand"] } - -ztracing_macro.workspace = true diff --git a/crates/ztracing/LICENSE-AGPL b/crates/ztracing/LICENSE-AGPL deleted file mode 120000 index 5f5cf25dc4..0000000000 --- a/crates/ztracing/LICENSE-AGPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-AGPL \ No newline at end of file diff --git a/crates/ztracing/LICENSE-APACHE b/crates/ztracing/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/crates/ztracing/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/ztracing/LICENSE-GPL b/crates/ztracing/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/ztracing/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/ztracing/build.rs b/crates/ztracing/build.rs deleted file mode 100644 index dc0d0ad704..0000000000 --- a/crates/ztracing/build.rs +++ /dev/null @@ -1,9 +0,0 @@ -use std::env; - -fn main() { - if env::var_os("ZTRACING").is_some() { - println!(r"cargo::rustc-cfg=ztracing"); - } - println!("cargo::rerun-if-changed=build.rs"); - println!("cargo::rerun-if-env-changed=ZTRACING"); -} diff --git a/crates/ztracing/src/lib.rs b/crates/ztracing/src/lib.rs deleted file mode 100644 index b9b318cc35..0000000000 --- a/crates/ztracing/src/lib.rs +++ /dev/null @@ -1,52 +0,0 @@ -pub use tracing::Level; - -#[cfg(ztracing)] -pub use tracing::{ - debug_span, error_span, event, info_span, instrument, span, trace_span, warn_span, -}; -#[cfg(not(ztracing))] -pub use ztracing_macro::instrument; - -#[cfg(not(ztracing))] -pub use __consume_all_tokens as trace_span; -#[cfg(not(ztracing))] -pub use __consume_all_tokens as info_span; -#[cfg(not(ztracing))] -pub use __consume_all_tokens as debug_span; -#[cfg(not(ztracing))] -pub use __consume_all_tokens as warn_span; -#[cfg(not(ztracing))] -pub use __consume_all_tokens as error_span; -#[cfg(not(ztracing))] -pub use __consume_all_tokens as event; -#[cfg(not(ztracing))] -pub use __consume_all_tokens as span; - -#[cfg(not(ztracing))] -#[macro_export] -macro_rules! __consume_all_tokens { - ($($t:tt)*) => { - $crate::FakeSpan - }; -} - -pub struct FakeSpan; -impl FakeSpan { - pub fn enter(&self) {} -} - -// #[cfg(not(ztracing))] -// pub use span; - -#[cfg(ztracing)] -pub fn init() { - zlog::info!("Starting tracy subscriber, you can now connect the profiler"); - use tracing_subscriber::prelude::*; - tracing::subscriber::set_global_default( - tracing_subscriber::registry().with(tracing_tracy::TracyLayer::default()), - ) - .expect("setup tracy layer"); -} - -#[cfg(not(ztracing))] -pub fn init() {} diff --git a/crates/ztracing_macro/Cargo.toml b/crates/ztracing_macro/Cargo.toml deleted file mode 100644 index dbd7adce5f..0000000000 --- a/crates/ztracing_macro/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "ztracing_macro" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lib] -proc-macro = true - -[dependencies] diff --git a/crates/ztracing_macro/LICENSE-AGPL b/crates/ztracing_macro/LICENSE-AGPL deleted file mode 120000 index 5f5cf25dc4..0000000000 --- a/crates/ztracing_macro/LICENSE-AGPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-AGPL \ No newline at end of file diff --git a/crates/ztracing_macro/LICENSE-APACHE b/crates/ztracing_macro/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/crates/ztracing_macro/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/crates/ztracing_macro/LICENSE-GPL b/crates/ztracing_macro/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/crates/ztracing_macro/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/crates/ztracing_macro/src/lib.rs b/crates/ztracing_macro/src/lib.rs deleted file mode 100644 index d9b073ed13..0000000000 --- a/crates/ztracing_macro/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -#[proc_macro_attribute] -pub fn instrument( - _attr: proc_macro::TokenStream, - item: proc_macro::TokenStream, -) -> proc_macro::TokenStream { - item -} diff --git a/docker-compose.sql b/docker-compose.sql deleted file mode 100644 index 7de55a1f98..0000000000 --- a/docker-compose.sql +++ /dev/null @@ -1,2 +0,0 @@ -create database zed; -create database zed_llm; diff --git a/extensions/.gitignore b/extensions/.gitignore deleted file mode 100644 index 84cbee6b39..0000000000 --- a/extensions/.gitignore +++ /dev/null @@ -1 +0,0 @@ -grammars diff --git a/extensions/EXTRACTION.md b/extensions/EXTRACTION.md deleted file mode 100644 index 2bcc8e35d5..0000000000 --- a/extensions/EXTRACTION.md +++ /dev/null @@ -1,181 +0,0 @@ -# Extracting an extension to dedicated repo - -These are some notes of how to extract an extension from the main zed repository and generate a new repository which preserves the history as best as possible. In the this example we will be extracting the `ruby` extension, substitute as appropriate. - -## Pre-requisites - -Install [git-filter-repo](https://github.com/newren/git-filter-repo/blob/main/INSTALL.md): - -``` -brew install git-filter-repo -``` - -## Process - -We are going to use a `$LANGNAME` variable for all these steps. Make sure it is set correctly. - -> **Note** -> If you get `zsh: command not found: #` errors, run: -> `setopt interactive_comments && echo "setopt interactive_comments" >> ~/.zshrc` - -1. Create a clean clone the zed repository, delete tags and delete branches. - -```sh -LANGNAME=your_language_name_here - -rm -rf $LANGNAME -git clone --single-branch --no-tags git@github.com:zed-industries/zed.git $LANGNAME -cd $LANGNAME -``` - -2. Create an expressions.txt file somewhere (e.g. `~/projects/$LANGNAME.txt`) - -This file takes the form of `patern==>replacement`, where the replacement is optional. -Note whitespace matters so `ruby: ==>` is removing the `ruby:` prefix from a commit messages and adding a space after `==> ` means the replacement begins with a space. Regex capture groups are numbered `\1`, `\2`, etc. - -See: [Git Filter Repo Docs](https://htmlpreview.github.io/?https://github.com/newren/git-filter-repo/blob/docs/html/git-filter-repo.html) for more. - -```sh -# Create regex mapping for rewriting commit messages (edit as appropriate) -mkdir -p ~/projects -echo "${LANGNAME}: ==> -extension: ==> -chore: ==> -zed_extension_api: ==> -"'regex:(?zed-industries/zed\1' \ - > ~/projects/${LANGNAME}.txt - -# This removes the LICENSE symlink -git filter-repo --invert-paths --path extensions/$LANGNAME/LICENSE-APACHE - -# This does the work -git filter-repo \ - --use-mailmap \ - --subdirectory-filter extensions/$LANGNAME/ \ - --path LICENSE-APACHE \ - --replace-message ~/projects/${LANGNAME}.txt -``` - -3. Review the commits. - -This is your last chance to make any modifications. -If you don't fix it now, it'll be wrong forever. - -For example, a previous commit message was `php/ruby: bump version to 0.0.5` -which was replaced with `php/bump version to 0.0.5` -so I added a new line to expressions.txt with `php/==>` -and next run it became `bump version to 0.0.5`. - -4. [Optional] Generate tags - -You can always add tags later, but it's a nice touch. - -Show you all commits that mention a version number: - -```sh -git log --grep="(\d+\.\d+\.\d+)" --perl-regexp --oneline --reverse -``` - -Then just: - -``` -git tag v0.0.2 abcd1234 -git tag v0.0.3 deadbeef -``` - -Usually the initial extraction didn't mention a version number so you can just do that one manually. - -4. [Optional] Add a README.md and commit. - -5. Push to the new repo - -Create a new empty repo on github under the [zed-extensions](https://github.com/organizations/zed-extensions/repositories/new) organization. - -``` -git remote add origin git@github.com:zed-extensions/$LANGNAME -git push origin main --tags -git branch --set-upstream-to=origin/main main -``` - -6. Setup the new repository: - -- Go to the repository settings: - - Disable Wikis - - Uncheck "Allow Merge Commits" - - Check "Allow Squash Merging" - - Default commit message: "Pull request title and description" - -7. Publish a new version of the extension. - -```sh -OLD_VERSION=$(grep '^version = ' extension.toml | cut -d'"' -f2) -NEW_VERSION=$(echo "$OLD_VERSION" | awk -F. '{$NF = $NF + 1;} 1' OFS=.) -echo $OLD_VERSION $NEW_VERSION -perl -i -pe "s/$OLD_VERSION/$NEW_VERSION/" extension.toml -perl -i -pe "s#https://github.com/zed-industries/zed#https://github.com/zed-extensions/${LANGNAME}#g" extension.toml - -# if there's rust code, update this too. -test -f Cargo.toml && perl -i -pe "s/$OLD_VERSION/$NEW_VERSION/" Cargo.toml -# remove workspace Cargo.toml lines -test -f Cargo.toml && perl -ni -e 'print unless /^.*(workspace\s*=\s*true|\[lints\])\s*$/' Cargo.toml -test -f Cargo.toml && cargo check - -# add a .gitignore -echo "target/ -grammars/ -*.wasm" > .gitignore - -# commit and push -git add -u -git checkout -b "bump_${NEW_VERSION}" -git commit -m "Bump to v${NEW_VERSION}" -git push -gh pr create --title "Bump to v${NEW_VERSION}" --web - -# merge PR in web interface -git checkout main -git pull -git tag v${NEW_VERSION} -git push origin v${NEW_VERSION} -``` - -7. In zed repository, remove the old extension and push a PR. - -```sh -rm -rf extensions/$LANGNAME -sed -i '' "/extensions\/$LANGNAME/d" Cargo.toml -cargo check -git checkout -b remove_$LANGNAME -git add extensions/$LANGNAME -git add Cargo.toml Cargo.lock extensions/$LANGNAME -git commit -m "Migrate to $LANGNAME extension to zed-extensions/$LANGNAME" -git push -gh pr create --web -``` - -8. Update extensions repository: - -```sh -cd ../extensions -git checkout main -git pull -git submodule init -git submodule update -git status - -git checkout -b ${LANGNAME}_v${NEW_VERSION} -git submodule add https://github.com/zed-extensions/${LANGNAME}.git extensions/${LANGNAME} -pnpm sort-extensions - -# edit extensions.toml: -# - bump version -# - change `submodule` from `extensions/zed` to new path -# - remove `path` line all together - -git add extensions.toml .gitmodules extensions/${LANGNAME} -git diff --cached -git commit -m "Bump ${LANGNAME} to v${NEW_VERSION}" -git push -``` - -Create PR and reference the Zed PR with removal from tree. diff --git a/extensions/README.md b/extensions/README.md deleted file mode 100644 index c677e0b909..0000000000 --- a/extensions/README.md +++ /dev/null @@ -1,63 +0,0 @@ -# Zed Extensions - -This directory contains extensions for Zed that are largely maintained by the Zed team. They currently live in the Zed repository for ease of maintenance. - -If you are looking for the Zed extension registry, see the [`zed-industries/extensions`](https://github.com/zed-industries/extensions) repo. - -## Structure - -Currently, Zed includes support for a number of languages without requiring installing an extension. Those languages can be found under [`crates/languages/src`](https://github.com/zed-industries/zed/tree/main/crates/languages/src). - -Support for all other languages is done via extensions. This directory ([extensions/](https://github.com/zed-industries/zed/tree/main/extensions/)) contains a number of officially maintained extensions. These extensions use the same [zed_extension_api](https://docs.rs/zed_extension_api/latest/zed_extension_api/) available to all [Zed Extensions](https://zed.dev/extensions) for providing [language servers](https://zed.dev/docs/extensions/languages#language-servers), [tree-sitter grammars](https://zed.dev/docs/extensions/languages#grammar) and [tree-sitter queries](https://zed.dev/docs/extensions/languages#tree-sitter-queries). - -## Dev Extensions - -See the docs for [Developing an Extension Locally](https://zed.dev/docs/extensions/developing-extensions#developing-an-extension-locally) for how to work with one of these extensions. - -## Updating - -> [!NOTE] -> This update process is usually handled by Zed staff. -> Community contributors should just submit a PR (step 1) and we'll take it from there. - -The process for updating an extension in this directory has three parts. - -1. Create a PR with your changes. (Merge it) -2. Bump the extension version in: - - - extensions/{language_name}/extension.toml - - extensions/{language_name}/Cargo.toml - - Cargo.lock - - You can do this manually, or with a script: - - ```sh - # Output the current version for a given language - ./script/language-extension-version - - # Update the version in `extension.toml` and `Cargo.toml` and trigger a `cargo check` - ./script/language-extension-version - ``` - - Commit your changes to a branch, push a PR and merge it. - -3. Open a PR to [`zed-industries/extensions`](https://github.com/zed-industries/extensions) repo that updates the extension in question - -Edit [`extensions.toml`](https://github.com/zed-industries/extensions/blob/main/extensions.toml) in the extensions repo to reflect the new version you set above and update the submodule latest Zed commit. - -```sh -# Go into your clone of the extensions repo -cd ../extensions - -# Update -git checkout main -git pull -just init-submodule extensions/zed - -# Update the Zed submodule -cd extensions/zed -git checkout main -git pull -cd - -git add extensions.toml extensions/zed -``` diff --git a/extensions/glsl/Cargo.toml b/extensions/glsl/Cargo.toml deleted file mode 100644 index 6dddeea358..0000000000 --- a/extensions/glsl/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "zed_glsl" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "Apache-2.0" - -[lints] -workspace = true - -[lib] -path = "src/glsl.rs" -crate-type = ["cdylib"] - -[dependencies] -zed_extension_api = "0.1.0" diff --git a/extensions/glsl/LICENSE-APACHE b/extensions/glsl/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/extensions/glsl/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/glsl/extension.toml b/extensions/glsl/extension.toml deleted file mode 100644 index 7cf9e218c8..0000000000 --- a/extensions/glsl/extension.toml +++ /dev/null @@ -1,15 +0,0 @@ -id = "glsl" -name = "GLSL" -description = "GLSL support." -version = "0.1.0" -schema_version = 1 -authors = ["Mikayla Maki "] -repository = "https://github.com/zed-industries/zed" - -[language_servers.glsl_analyzer] -name = "GLSL Analyzer LSP" -language = "GLSL" - -[grammars.glsl] -repository = "https://github.com/theHamsta/tree-sitter-glsl" -commit = "31064ce53385150f894a6c72d61b94076adf640a" diff --git a/extensions/glsl/languages/glsl/config.toml b/extensions/glsl/languages/glsl/config.toml deleted file mode 100644 index 0c71419c91..0000000000 --- a/extensions/glsl/languages/glsl/config.toml +++ /dev/null @@ -1,20 +0,0 @@ -name = "GLSL" -grammar = "glsl" -path_suffixes = [ - # Traditional rasterization pipeline shaders - "vert", "frag", "tesc", "tese", "geom", - # Compute shaders - "comp", - # Ray tracing pipeline shaders - "rgen", "rint", "rahit", "rchit", "rmiss", "rcall", - # Other - "glsl" - ] -first_line_pattern = '^#version \d+' -line_comments = ["// "] -block_comment = { start = "/* ", prefix = "* ", end = "*/", tab_size = 1 } -brackets = [ - { start = "{", end = "}", close = true, newline = true }, - { start = "[", end = "]", close = true, newline = true }, - { start = "(", end = ")", close = true, newline = true }, -] diff --git a/extensions/glsl/languages/glsl/highlights.scm b/extensions/glsl/languages/glsl/highlights.scm deleted file mode 100644 index 09f94d4fb5..0000000000 --- a/extensions/glsl/languages/glsl/highlights.scm +++ /dev/null @@ -1,117 +0,0 @@ -"break" @keyword -"case" @keyword -"const" @keyword -"continue" @keyword -"default" @keyword -"do" @keyword -"else" @keyword -"enum" @keyword -"extern" @keyword -"for" @keyword -"if" @keyword -"inline" @keyword -"return" @keyword -"sizeof" @keyword -"static" @keyword -"struct" @keyword -"switch" @keyword -"typedef" @keyword -"union" @keyword -"volatile" @keyword -"while" @keyword - -"#define" @keyword -"#elif" @keyword -"#else" @keyword -"#endif" @keyword -"#if" @keyword -"#ifdef" @keyword -"#ifndef" @keyword -"#include" @keyword -(preproc_directive) @keyword - -"--" @operator -"-" @operator -"-=" @operator -"->" @operator -"=" @operator -"!=" @operator -"*" @operator -"&" @operator -"&&" @operator -"+" @operator -"++" @operator -"+=" @operator -"<" @operator -"==" @operator -">" @operator -"||" @operator - -"." @delimiter -";" @delimiter - -(string_literal) @string -(system_lib_string) @string - -(null) @constant -(number_literal) @number -(char_literal) @number - -(identifier) @variable - -(field_identifier) @property -(statement_identifier) @label -(type_identifier) @type -(primitive_type) @type -(sized_type_specifier) @type - -(call_expression - function: (identifier) @function) -(call_expression - function: (field_expression - field: (field_identifier) @function)) -(function_declarator - declarator: (identifier) @function) -(preproc_function_def - name: (identifier) @function.special) - -((identifier) @constant - (#match? @constant "^[A-Z][A-Z\\d_]*$")) - -(comment) @comment - -[ - "in" - "out" - "inout" - "uniform" - "shared" - "layout" - "attribute" - "varying" - "buffer" - "coherent" - "readonly" - "writeonly" - "precision" - "highp" - "mediump" - "lowp" - "centroid" - "sample" - "patch" - "smooth" - "flat" - "noperspective" - "invariant" - "precise" -] @type.qualifier - -"subroutine" @keyword.function - -(extension_storage_class) @storageclass - -( - (identifier) @variable.builtin - (#match? @variable.builtin "^gl_") -) diff --git a/extensions/glsl/src/glsl.rs b/extensions/glsl/src/glsl.rs deleted file mode 100644 index 77865564cc..0000000000 --- a/extensions/glsl/src/glsl.rs +++ /dev/null @@ -1,131 +0,0 @@ -use std::fs; -use zed::settings::LspSettings; -use zed_extension_api::{self as zed, LanguageServerId, Result, serde_json}; - -struct GlslExtension { - cached_binary_path: Option, -} - -impl GlslExtension { - fn language_server_binary_path( - &mut self, - language_server_id: &LanguageServerId, - worktree: &zed::Worktree, - ) -> Result { - if let Some(path) = worktree.which("glsl_analyzer") { - return Ok(path); - } - - if let Some(path) = &self.cached_binary_path - && fs::metadata(path).is_ok_and(|stat| stat.is_file()) - { - return Ok(path.clone()); - } - - zed::set_language_server_installation_status( - language_server_id, - &zed::LanguageServerInstallationStatus::CheckingForUpdate, - ); - let release = zed::latest_github_release( - "nolanderc/glsl_analyzer", - zed::GithubReleaseOptions { - require_assets: true, - pre_release: false, - }, - )?; - - let (platform, arch) = zed::current_platform(); - let asset_name = format!( - "{arch}-{os}.zip", - arch = match arch { - zed::Architecture::Aarch64 => "aarch64", - zed::Architecture::X86 => "x86", - zed::Architecture::X8664 => "x86_64", - }, - os = match platform { - zed::Os::Mac => "macos", - zed::Os::Linux => "linux-musl", - zed::Os::Windows => "windows", - } - ); - - let asset = release - .assets - .iter() - .find(|asset| asset.name == asset_name) - .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?; - - let version_dir = format!("glsl_analyzer-{}", release.version); - fs::create_dir_all(&version_dir) - .map_err(|err| format!("failed to create directory '{version_dir}': {err}"))?; - let binary_path = format!("{version_dir}/bin/glsl_analyzer"); - - if !fs::metadata(&binary_path).is_ok_and(|stat| stat.is_file()) { - zed::set_language_server_installation_status( - language_server_id, - &zed::LanguageServerInstallationStatus::Downloading, - ); - - zed::download_file( - &asset.download_url, - &version_dir, - match platform { - zed::Os::Mac | zed::Os::Linux => zed::DownloadedFileType::Zip, - zed::Os::Windows => zed::DownloadedFileType::Zip, - }, - ) - .map_err(|e| format!("failed to download file: {e}"))?; - - zed::make_file_executable(&binary_path)?; - - let entries = - fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?; - for entry in entries { - let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?; - if entry.file_name().to_str() != Some(&version_dir) { - fs::remove_dir_all(entry.path()).ok(); - } - } - } - - self.cached_binary_path = Some(binary_path.clone()); - Ok(binary_path) - } -} - -impl zed::Extension for GlslExtension { - fn new() -> Self { - Self { - cached_binary_path: None, - } - } - - fn language_server_command( - &mut self, - language_server_id: &zed::LanguageServerId, - worktree: &zed::Worktree, - ) -> Result { - Ok(zed::Command { - command: self.language_server_binary_path(language_server_id, worktree)?, - args: vec![], - env: Default::default(), - }) - } - - fn language_server_workspace_configuration( - &mut self, - _language_server_id: &zed::LanguageServerId, - worktree: &zed::Worktree, - ) -> Result> { - let settings = LspSettings::for_worktree("glsl_analyzer", worktree) - .ok() - .and_then(|lsp_settings| lsp_settings.settings) - .unwrap_or_default(); - - Ok(Some(serde_json::json!({ - "glsl_analyzer": settings - }))) - } -} - -zed::register_extension!(GlslExtension); diff --git a/extensions/html/Cargo.toml b/extensions/html/Cargo.toml deleted file mode 100644 index 22cdb401a7..0000000000 --- a/extensions/html/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "zed_html" -version = "0.2.3" -edition.workspace = true -publish.workspace = true -license = "Apache-2.0" - -[lints] -workspace = true - -[lib] -path = "src/html.rs" -crate-type = ["cdylib"] - -[dependencies] -zed_extension_api = "0.7.0" diff --git a/extensions/html/LICENSE-APACHE b/extensions/html/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/extensions/html/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/html/extension.toml b/extensions/html/extension.toml deleted file mode 100644 index 1ded7af641..0000000000 --- a/extensions/html/extension.toml +++ /dev/null @@ -1,19 +0,0 @@ -id = "html" -name = "HTML" -description = "HTML support." -version = "0.2.3" -schema_version = 1 -authors = ["Isaac Clayton "] -repository = "https://github.com/zed-industries/zed" - -[language_servers.vscode-html-language-server] -name = "vscode-html-language-server" -language = "HTML" - -[language_servers.vscode-html-language-server.language_ids] -"HTML" = "html" -"CSS" = "css" - -[grammars.html] -repository = "https://github.com/tree-sitter/tree-sitter-html" -commit = "bfa075d83c6b97cd48440b3829ab8d24a2319809" diff --git a/extensions/html/languages/html/brackets.scm b/extensions/html/languages/html/brackets.scm deleted file mode 100644 index 53d6a6bb23..0000000000 --- a/extensions/html/languages/html/brackets.scm +++ /dev/null @@ -1,5 +0,0 @@ -("<" @open "/>" @close) -("" @close) -("<" @open ">" @close) -(("\"" @open "\"" @close) (#set! rainbow.exclude)) -((element (start_tag) @open (end_tag) @close) (#set! newline.only) (#set! rainbow.exclude)) diff --git a/extensions/html/languages/html/config.toml b/extensions/html/languages/html/config.toml deleted file mode 100644 index fc7d557198..0000000000 --- a/extensions/html/languages/html/config.toml +++ /dev/null @@ -1,19 +0,0 @@ -name = "HTML" -grammar = "html" -path_suffixes = ["html", "htm", "shtml"] -autoclose_before = ">})" -block_comment = { start = "", tab_size = 0 } -wrap_characters = { start_prefix = "<", start_suffix = ">", end_prefix = "" } -brackets = [ - { start = "{", end = "}", close = true, newline = true }, - { start = "[", end = "]", close = true, newline = true }, - { start = "(", end = ")", close = true, newline = true }, - { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] }, - { start = "<", end = ">", close = false, newline = true, not_in = ["comment", "string"] }, - { start = "!--", end = " --", close = true, newline = false, not_in = ["comment", "string"] }, -] -completion_query_characters = ["-"] -prettier_parser_name = "html" - -[overrides.default] -linked_edit_characters = ["-"] diff --git a/extensions/html/languages/html/highlights.scm b/extensions/html/languages/html/highlights.scm deleted file mode 100644 index 1cc0601b76..0000000000 --- a/extensions/html/languages/html/highlights.scm +++ /dev/null @@ -1,19 +0,0 @@ -(tag_name) @tag -(doctype) @tag.doctype -(attribute_name) @attribute -[ - "\"" - "'" - (attribute_value) -] @string -(comment) @comment - -"=" @punctuation.delimiter.html - -[ - "<" - ">" - "" -] @punctuation.bracket.html diff --git a/extensions/html/languages/html/indents.scm b/extensions/html/languages/html/indents.scm deleted file mode 100644 index 436663dba3..0000000000 --- a/extensions/html/languages/html/indents.scm +++ /dev/null @@ -1,6 +0,0 @@ -(start_tag ">" @end) @indent -(self_closing_tag "/>" @end) @indent - -(element - (start_tag) @start - (end_tag)? @end) @indent diff --git a/extensions/html/languages/html/injections.scm b/extensions/html/languages/html/injections.scm deleted file mode 100644 index 525b3efe29..0000000000 --- a/extensions/html/languages/html/injections.scm +++ /dev/null @@ -1,21 +0,0 @@ -((comment) @injection.content - (#set! injection.language "comment") -) - -(script_element - (raw_text) @injection.content - (#set! injection.language "javascript")) - -(style_element - (raw_text) @injection.content - (#set! injection.language "css")) - -(attribute - (attribute_name) @_attribute_name (#match? @_attribute_name "^style$") - (quoted_attribute_value (attribute_value) @injection.content) - (#set! injection.language "css")) - -(attribute - (attribute_name) @_attribute_name (#match? @_attribute_name "^on[a-z]+$") - (quoted_attribute_value (attribute_value) @injection.content) - (#set! injection.language "javascript")) diff --git a/extensions/html/languages/html/outline.scm b/extensions/html/languages/html/outline.scm deleted file mode 100644 index e7f9dc4fab..0000000000 --- a/extensions/html/languages/html/outline.scm +++ /dev/null @@ -1,5 +0,0 @@ -(comment) @annotation - -(element - (start_tag - (tag_name) @name)) @item diff --git a/extensions/html/languages/html/overrides.scm b/extensions/html/languages/html/overrides.scm deleted file mode 100644 index 434f610e70..0000000000 --- a/extensions/html/languages/html/overrides.scm +++ /dev/null @@ -1,7 +0,0 @@ -(comment) @comment -(quoted_attribute_value) @string - -[ - (start_tag) - (end_tag) -] @default diff --git a/extensions/html/src/html.rs b/extensions/html/src/html.rs deleted file mode 100644 index 337689ebdd..0000000000 --- a/extensions/html/src/html.rs +++ /dev/null @@ -1,115 +0,0 @@ -use std::{env, fs}; -use zed::settings::LspSettings; -use zed_extension_api::{self as zed, LanguageServerId, Result, serde_json::json}; - -const BINARY_NAME: &str = "vscode-html-language-server"; -const SERVER_PATH: &str = - "node_modules/@zed-industries/vscode-langservers-extracted/bin/vscode-html-language-server"; -const PACKAGE_NAME: &str = "@zed-industries/vscode-langservers-extracted"; - -struct HtmlExtension { - cached_binary_path: Option, -} - -impl HtmlExtension { - fn server_exists(&self) -> bool { - fs::metadata(SERVER_PATH).is_ok_and(|stat| stat.is_file()) - } - - fn server_script_path(&mut self, language_server_id: &LanguageServerId) -> Result { - let server_exists = self.server_exists(); - if self.cached_binary_path.is_some() && server_exists { - return Ok(SERVER_PATH.to_string()); - } - - zed::set_language_server_installation_status( - language_server_id, - &zed::LanguageServerInstallationStatus::CheckingForUpdate, - ); - let version = zed::npm_package_latest_version(PACKAGE_NAME)?; - - if !server_exists - || zed::npm_package_installed_version(PACKAGE_NAME)?.as_ref() != Some(&version) - { - zed::set_language_server_installation_status( - language_server_id, - &zed::LanguageServerInstallationStatus::Downloading, - ); - let result = zed::npm_install_package(PACKAGE_NAME, &version); - match result { - Ok(()) => { - if !self.server_exists() { - Err(format!( - "installed package '{PACKAGE_NAME}' did not contain expected path '{SERVER_PATH}'", - ))?; - } - } - Err(error) => { - if !self.server_exists() { - Err(error)?; - } - } - } - } - Ok(SERVER_PATH.to_string()) - } -} - -impl zed::Extension for HtmlExtension { - fn new() -> Self { - Self { - cached_binary_path: None, - } - } - - fn language_server_command( - &mut self, - language_server_id: &LanguageServerId, - worktree: &zed::Worktree, - ) -> Result { - let server_path = if let Some(path) = worktree.which(BINARY_NAME) { - return Ok(zed::Command { - command: path, - args: vec!["--stdio".to_string()], - env: Default::default(), - }); - } else { - let server_path = self.server_script_path(language_server_id)?; - env::current_dir() - .unwrap() - .join(&server_path) - .to_string_lossy() - .to_string() - }; - self.cached_binary_path = Some(server_path.clone()); - - Ok(zed::Command { - command: zed::node_binary_path()?, - args: vec![server_path, "--stdio".to_string()], - env: Default::default(), - }) - } - - fn language_server_workspace_configuration( - &mut self, - server_id: &LanguageServerId, - worktree: &zed::Worktree, - ) -> Result> { - let settings = LspSettings::for_worktree(server_id.as_ref(), worktree) - .ok() - .and_then(|lsp_settings| lsp_settings.settings) - .unwrap_or_default(); - Ok(Some(settings)) - } - - fn language_server_initialization_options( - &mut self, - _server_id: &LanguageServerId, - _worktree: &zed_extension_api::Worktree, - ) -> Result> { - let initialization_options = json!({"provideFormatter": true }); - Ok(Some(initialization_options)) - } -} - -zed::register_extension!(HtmlExtension); diff --git a/extensions/proto/Cargo.toml b/extensions/proto/Cargo.toml deleted file mode 100644 index 1013d62cfa..0000000000 --- a/extensions/proto/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "zed_proto" -version = "0.2.3" -edition.workspace = true -publish.workspace = true -license = "Apache-2.0" - -[lints] -workspace = true - -[lib] -path = "src/proto.rs" -crate-type = ["cdylib"] - -[dependencies] -zed_extension_api = "0.1.0" diff --git a/extensions/proto/LICENSE-APACHE b/extensions/proto/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/extensions/proto/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/proto/extension.toml b/extensions/proto/extension.toml deleted file mode 100644 index 9bb8625065..0000000000 --- a/extensions/proto/extension.toml +++ /dev/null @@ -1,15 +0,0 @@ -id = "proto" -name = "Proto" -description = "Protocol Buffers support." -version = "0.2.3" -schema_version = 1 -authors = ["Zed Industries "] -repository = "https://github.com/zed-industries/zed" - -[grammars.proto] -repository = "https://github.com/zed-industries/tree-sitter-proto" -commit = "0848bd30a64be48772e15fbb9d5ba8c0cc5772ad" - -[language_servers.protobuf-language-server] -name = "Protobuf Language Server" -languages = ["Proto"] diff --git a/extensions/proto/languages/proto/config.toml b/extensions/proto/languages/proto/config.toml deleted file mode 100644 index 6d25c23da5..0000000000 --- a/extensions/proto/languages/proto/config.toml +++ /dev/null @@ -1,13 +0,0 @@ -name = "Proto" -grammar = "proto" -path_suffixes = ["proto"] -line_comments = ["// "] -autoclose_before = ";:.,=}])>" -brackets = [ - { start = "{", end = "}", close = true, newline = true }, - { start = "[", end = "]", close = true, newline = true }, - { start = "(", end = ")", close = true, newline = true }, - { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] }, - { start = "'", end = "'", close = true, newline = false, not_in = ["comment", "string"] }, - { start = "/*", end = " */", close = true, newline = false, not_in = ["comment", "string"] }, -] diff --git a/extensions/proto/languages/proto/highlights.scm b/extensions/proto/languages/proto/highlights.scm deleted file mode 100644 index 5d0a513bee..0000000000 --- a/extensions/proto/languages/proto/highlights.scm +++ /dev/null @@ -1,61 +0,0 @@ -[ - "syntax" - "package" - "option" - "optional" - "import" - "service" - "rpc" - "returns" - "message" - "enum" - "oneof" - "repeated" - "reserved" - "to" -] @keyword - -[ - (key_type) - (type) - (message_name) - (enum_name) - (service_name) - (rpc_name) - (message_or_enum_type) -] @type - -(enum_field - (identifier) @constant) - -[ - (string) - "\"proto3\"" -] @string - -(int_lit) @number - -[ - (true) - (false) -] @boolean - -(comment) @comment - -[ - "(" - ")" - "[" - "]" - "{" - "}" - "<" - ">" -] @punctuation.bracket - -[ - ";" - "," -] @punctuation.delimiter - -"=" @operator diff --git a/extensions/proto/languages/proto/indents.scm b/extensions/proto/languages/proto/indents.scm deleted file mode 100644 index acb44a5e1e..0000000000 --- a/extensions/proto/languages/proto/indents.scm +++ /dev/null @@ -1,3 +0,0 @@ -(_ "{" "}" @end) @indent -(_ "[" "]" @end) @indent -(_ "(" ")" @end) @indent diff --git a/extensions/proto/languages/proto/outline.scm b/extensions/proto/languages/proto/outline.scm deleted file mode 100644 index f90b1bae33..0000000000 --- a/extensions/proto/languages/proto/outline.scm +++ /dev/null @@ -1,19 +0,0 @@ -(message - "message" @context - (message_name - (identifier) @name)) @item - -(service - "service" @context - (service_name - (identifier) @name)) @item - -(rpc - "rpc" @context - (rpc_name - (identifier) @name)) @item - -(enum - "enum" @context - (enum_name - (identifier) @name)) @item diff --git a/extensions/proto/languages/proto/textobjects.scm b/extensions/proto/languages/proto/textobjects.scm deleted file mode 100644 index 90ea84282d..0000000000 --- a/extensions/proto/languages/proto/textobjects.scm +++ /dev/null @@ -1,18 +0,0 @@ -(message (message_body - "{" - (_)* @class.inside - "}")) @class.around -(enum (enum_body - "{" - (_)* @class.inside - "}")) @class.around -(service - "service" - (_) - "{" - (_)* @class.inside - "}") @class.around - -(rpc) @function.around - -(comment)+ @comment.around diff --git a/extensions/proto/src/proto.rs b/extensions/proto/src/proto.rs deleted file mode 100644 index 36ba0faf5f..0000000000 --- a/extensions/proto/src/proto.rs +++ /dev/null @@ -1,86 +0,0 @@ -use zed_extension_api::{self as zed, Result, settings::LspSettings}; - -const PROTOBUF_LANGUAGE_SERVER_NAME: &str = "protobuf-language-server"; - -struct ProtobufLanguageServerBinary { - path: String, - args: Option>, -} - -struct ProtobufExtension; - -impl ProtobufExtension { - fn language_server_binary( - &self, - _language_server_id: &zed::LanguageServerId, - worktree: &zed::Worktree, - ) -> Result { - let binary_settings = LspSettings::for_worktree("protobuf-language-server", worktree) - .ok() - .and_then(|lsp_settings| lsp_settings.binary); - let binary_args = binary_settings - .as_ref() - .and_then(|binary_settings| binary_settings.arguments.clone()); - - if let Some(path) = binary_settings.and_then(|binary_settings| binary_settings.path) { - return Ok(ProtobufLanguageServerBinary { - path, - args: binary_args, - }); - } - - if let Some(path) = worktree.which(PROTOBUF_LANGUAGE_SERVER_NAME) { - return Ok(ProtobufLanguageServerBinary { - path, - args: binary_args, - }); - } - - Err(format!("{PROTOBUF_LANGUAGE_SERVER_NAME} not found in PATH",)) - } -} - -impl zed::Extension for ProtobufExtension { - fn new() -> Self { - Self - } - - fn language_server_command( - &mut self, - language_server_id: &zed_extension_api::LanguageServerId, - worktree: &zed_extension_api::Worktree, - ) -> zed_extension_api::Result { - let binary = self.language_server_binary(language_server_id, worktree)?; - Ok(zed::Command { - command: binary.path, - args: binary - .args - .unwrap_or_else(|| vec!["-logs".into(), "".into()]), - env: Default::default(), - }) - } - - fn language_server_workspace_configuration( - &mut self, - server_id: &zed::LanguageServerId, - worktree: &zed::Worktree, - ) -> Result> { - let settings = LspSettings::for_worktree(server_id.as_ref(), worktree) - .ok() - .and_then(|lsp_settings| lsp_settings.settings); - Ok(settings) - } - - fn language_server_initialization_options( - &mut self, - server_id: &zed::LanguageServerId, - worktree: &zed::Worktree, - ) -> Result> { - let initialization_options = LspSettings::for_worktree(server_id.as_ref(), worktree) - .ok() - .and_then(|lsp_settings| lsp_settings.initialization_options); - Ok(initialization_options) - } -} - -zed::register_extension!(ProtobufExtension); diff --git a/extensions/slash-commands-example/Cargo.toml b/extensions/slash-commands-example/Cargo.toml deleted file mode 100644 index 03b22af254..0000000000 --- a/extensions/slash-commands-example/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "slash_commands_example" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "Apache-2.0" - -[lints] -workspace = true - -[lib] -path = "src/slash_commands_example.rs" -crate-type = ["cdylib"] - -[dependencies] -zed_extension_api = "0.1.0" diff --git a/extensions/slash-commands-example/LICENSE-APACHE b/extensions/slash-commands-example/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/extensions/slash-commands-example/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/slash-commands-example/README.md b/extensions/slash-commands-example/README.md deleted file mode 100644 index 8c16a4e168..0000000000 --- a/extensions/slash-commands-example/README.md +++ /dev/null @@ -1,84 +0,0 @@ -# Slash Commands Example Extension - -This is an example extension showcasing how to write slash commands. - -See: [Extensions: Slash Commands](https://zed.dev/docs/extensions/slash-commands) in the Zed Docs. - -## Pre-requisites - -[Install Rust Toolchain](https://www.rust-lang.org/tools/install): - -```sh -curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -``` - -## Setup - -```sh -git clone https://github.com/zed-industries/zed.git -cp -RL zed/extensions/slash-commands-example . - -cd slash-commands-example/ - -# Update Cargo.toml to make it standalone -cat > Cargo.toml << EOF -[package] -name = "slash_commands_example" -version = "0.1.0" -edition = "2021" -license = "Apache-2.0" - -[lib] -path = "src/slash_commands_example.rs" -crate-type = ["cdylib"] - -[dependencies] -zed_extension_api = "0.1.0" -EOF - -curl -O https://raw.githubusercontent.com/rust-lang/rust/master/LICENSE-APACHE -echo "# Zed Slash Commands Example Extension" > README.md -echo "Cargo.lock" > .gitignore -echo "target/" >> .gitignore -echo "*.wasm" >> .gitignore - -git init -git add . -git commit -m "Initial commit" - -cd .. -mv slash-commands-example MY-SUPER-COOL-ZED-EXTENSION -zed $_ -``` - -## Installation - -1. Open the command palette (`cmd-shift-p` or `ctrl-shift-p`). -2. Launch `zed: install dev extension` -3. Select the extension folder created above - -## Test - -Open the assistant and type `/echo` and `/pick-one` at the beginning of a line. - -## Customization - -Open the `extensions.toml` file and set the `id`, `name`, `description`, `authors` and `repository` fields. - -Rename `slash-commands-example.rs` you'll also have to update `Cargo.toml` - -## Rebuild - -Rebuild to see these changes reflected: - -1. Open Zed Extensions (`cmd-shift-x` or `ctrl-shift-x`). -2. Click `Rebuild` next to your Dev Extension (formerly "Slash Command Example") - -## Troubleshooting / Logs - -- [zed.dev docs: Troubleshooting](https://zed.dev/docs/troubleshooting) - -## Documentation - -- [zed.dev docs: Extensions: Developing Extensions](https://zed.dev/docs/extensions/developing-extensions) -- [zed.dev docs: Extensions: Slash Commands](https://zed.dev/docs/extensions/slash-commands) diff --git a/extensions/slash-commands-example/extension.toml b/extensions/slash-commands-example/extension.toml deleted file mode 100644 index 888c776d01..0000000000 --- a/extensions/slash-commands-example/extension.toml +++ /dev/null @@ -1,15 +0,0 @@ -id = "slash-commands-example" -name = "Slash Commands Example" -description = "An example extension showcasing slash commands." -version = "0.1.0" -schema_version = 1 -authors = ["Zed Industries "] -repository = "https://github.com/zed-industries/zed" - -[slash_commands.echo] -description = "echoes the provided input" -requires_argument = true - -[slash_commands.pick-one] -description = "pick one of three options" -requires_argument = true diff --git a/extensions/slash-commands-example/src/slash_commands_example.rs b/extensions/slash-commands-example/src/slash_commands_example.rs deleted file mode 100644 index 5b170d63ee..0000000000 --- a/extensions/slash-commands-example/src/slash_commands_example.rs +++ /dev/null @@ -1,90 +0,0 @@ -use zed_extension_api::{ - self as zed, SlashCommand, SlashCommandArgumentCompletion, SlashCommandOutput, - SlashCommandOutputSection, Worktree, -}; - -struct SlashCommandsExampleExtension; - -impl zed::Extension for SlashCommandsExampleExtension { - fn new() -> Self { - SlashCommandsExampleExtension - } - - fn complete_slash_command_argument( - &self, - command: SlashCommand, - _args: Vec, - ) -> Result, String> { - match command.name.as_str() { - "echo" => Ok(vec![]), - "pick-one" => Ok(vec![ - SlashCommandArgumentCompletion { - label: "Option One".to_string(), - new_text: "option-1".to_string(), - run_command: true, - }, - SlashCommandArgumentCompletion { - label: "Option Two".to_string(), - new_text: "option-2".to_string(), - run_command: true, - }, - SlashCommandArgumentCompletion { - label: "Option Three".to_string(), - new_text: "option-3".to_string(), - run_command: true, - }, - ]), - command => Err(format!("unknown slash command: \"{command}\"")), - } - } - - fn run_slash_command( - &self, - command: SlashCommand, - args: Vec, - _worktree: Option<&Worktree>, - ) -> Result { - match command.name.as_str() { - "echo" => { - if args.is_empty() { - return Err("nothing to echo".to_string()); - } - - let text = args.join(" "); - - Ok(SlashCommandOutput { - sections: vec![SlashCommandOutputSection { - range: (0..text.len()).into(), - label: "Echo".to_string(), - }], - text, - }) - } - "pick-one" => { - let Some(selection) = args.first() else { - return Err("no option selected".to_string()); - }; - - match selection.as_str() { - "option-1" | "option-2" | "option-3" => {} - invalid_option => { - return Err(format!("{invalid_option} is not a valid option")); - } - } - - let text = format!("You chose {selection}."); - - Ok(SlashCommandOutput { - sections: vec![SlashCommandOutputSection { - range: (0..text.len()).into(), - label: format!("Pick One: {selection}"), - }], - text, - }) - } - command => Err(format!("unknown slash command: \"{command}\"")), - } - } -} - -zed::register_extension!(SlashCommandsExampleExtension); diff --git a/extensions/test-extension/Cargo.toml b/extensions/test-extension/Cargo.toml deleted file mode 100644 index 7d2412b98f..0000000000 --- a/extensions/test-extension/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "zed_test_extension" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "Apache-2.0" - -[lints] -workspace = true - -[lib] -path = "src/test_extension.rs" -crate-type = ["cdylib"] - -[dependencies] -zed_extension_api = { path = "../../crates/extension_api" } diff --git a/extensions/test-extension/LICENSE-APACHE b/extensions/test-extension/LICENSE-APACHE deleted file mode 120000 index 1cd601d0a3..0000000000 --- a/extensions/test-extension/LICENSE-APACHE +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-APACHE \ No newline at end of file diff --git a/extensions/test-extension/README.md b/extensions/test-extension/README.md deleted file mode 100644 index 5941f23ec4..0000000000 --- a/extensions/test-extension/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Test Extension - -This is a test extension that we use in the tests for the `extension` crate. - -Originally based off the Gleam extension. diff --git a/extensions/test-extension/extension.toml b/extensions/test-extension/extension.toml deleted file mode 100644 index 0cb5afac7f..0000000000 --- a/extensions/test-extension/extension.toml +++ /dev/null @@ -1,25 +0,0 @@ -id = "test-extension" -name = "Test Extension" -description = "An extension for use in tests." -version = "0.1.0" -schema_version = 1 -authors = ["Marshall Bowers "] -repository = "https://github.com/zed-industries/zed" - -[language_servers.gleam] -name = "Gleam LSP" -language = "Gleam" - -[grammars.gleam] -repository = "https://github.com/gleam-lang/tree-sitter-gleam" -commit = "8432ffe32ccd360534837256747beb5b1c82fca1" - -[[capabilities]] -kind = "process:exec" -command = "echo" -args = ["hello from a child process!"] - -[[capabilities]] -kind = "process:exec" -command = "cmd" -args = ["/C", "echo", "hello from a child process!"] diff --git a/extensions/test-extension/languages/gleam/config.toml b/extensions/test-extension/languages/gleam/config.toml deleted file mode 100644 index 51874945e2..0000000000 --- a/extensions/test-extension/languages/gleam/config.toml +++ /dev/null @@ -1,12 +0,0 @@ -name = "Gleam" -grammar = "gleam" -path_suffixes = ["gleam"] -line_comments = ["// ", "/// "] -autoclose_before = ";:.,=}])>" -brackets = [ - { start = "{", end = "}", close = true, newline = true }, - { start = "[", end = "]", close = true, newline = true }, - { start = "(", end = ")", close = true, newline = true }, - { start = "\"", end = "\"", close = true, newline = false, not_in = ["string", "comment"] }, -] -tab_size = 2 diff --git a/extensions/test-extension/languages/gleam/highlights.scm b/extensions/test-extension/languages/gleam/highlights.scm deleted file mode 100644 index 4b85b88d01..0000000000 --- a/extensions/test-extension/languages/gleam/highlights.scm +++ /dev/null @@ -1,130 +0,0 @@ -; Comments -(module_comment) @comment -(statement_comment) @comment -(comment) @comment - -; Constants -(constant - name: (identifier) @constant) - -; Variables -(identifier) @variable -(discard) @comment.unused - -; Modules -(module) @module -(import alias: (identifier) @module) -(remote_type_identifier - module: (identifier) @module) -(remote_constructor_name - module: (identifier) @module) -((field_access - record: (identifier) @module - field: (label) @function) - (#is-not? local)) - -; Functions -(unqualified_import (identifier) @function) -(unqualified_import "type" (type_identifier) @type) -(unqualified_import (type_identifier) @constructor) -(function - name: (identifier) @function) -(external_function - name: (identifier) @function) -(function_parameter - name: (identifier) @variable.parameter) -((function_call - function: (identifier) @function) - (#is-not? local)) -((binary_expression - operator: "|>" - right: (identifier) @function) - (#is-not? local)) - -; "Properties" -; Assumed to be intended to refer to a name for a field; something that comes -; before ":" or after "." -; e.g. record field names, tuple indices, names for named arguments, etc -(label) @property -(tuple_access - index: (integer) @property) - -; Attributes -(attribute - "@" @attribute - name: (identifier) @attribute) - -(attribute_value (identifier) @constant) - -; Type names -(remote_type_identifier) @type -(type_identifier) @type - -; Data constructors -(constructor_name) @constructor - -; Literals -(string) @string -((escape_sequence) @warning - ; Deprecated in v0.33.0-rc2: - (#eq? @warning "\\e")) -(escape_sequence) @string.escape -(bit_string_segment_option) @function.builtin -(integer) @number -(float) @number - -; Reserved identifiers -; TODO: when tree-sitter supports `#any-of?` in the Rust bindings, -; refactor this to use `#any-of?` rather than `#match?` -((identifier) @warning - (#match? @warning "^(auto|delegate|derive|else|implement|macro|test|echo)$")) - -; Keywords -[ - (visibility_modifier) ; "pub" - (opacity_modifier) ; "opaque" - "as" - "assert" - "case" - "const" - ; DEPRECATED: 'external' was removed in v0.30. - "external" - "fn" - "if" - "import" - "let" - "panic" - "todo" - "type" - "use" -] @keyword - -; Operators -(binary_expression - operator: _ @operator) -(boolean_negation "!" @operator) -(integer_negation "-" @operator) - -; Punctuation -[ - "(" - ")" - "[" - "]" - "{" - "}" - "<<" - ">>" -] @punctuation.bracket -[ - "." - "," - ;; Controversial -- maybe some are operators? - ":" - "#" - "=" - "->" - ".." - "-" - "<-" -] @punctuation.delimiter diff --git a/extensions/test-extension/languages/gleam/indents.scm b/extensions/test-extension/languages/gleam/indents.scm deleted file mode 100644 index 112b414aa4..0000000000 --- a/extensions/test-extension/languages/gleam/indents.scm +++ /dev/null @@ -1,3 +0,0 @@ -(_ "[" "]" @end) @indent -(_ "{" "}" @end) @indent -(_ "(" ")" @end) @indent diff --git a/extensions/test-extension/languages/gleam/outline.scm b/extensions/test-extension/languages/gleam/outline.scm deleted file mode 100644 index 5df7a6af80..0000000000 --- a/extensions/test-extension/languages/gleam/outline.scm +++ /dev/null @@ -1,31 +0,0 @@ -(external_type - (visibility_modifier)? @context - "type" @context - (type_name) @name) @item - -(type_definition - (visibility_modifier)? @context - (opacity_modifier)? @context - "type" @context - (type_name) @name) @item - -(data_constructor - (constructor_name) @name) @item - -(data_constructor_argument - (label) @name) @item - -(type_alias - (visibility_modifier)? @context - "type" @context - (type_name) @name) @item - -(function - (visibility_modifier)? @context - "fn" @context - name: (_) @name) @item - -(constant - (visibility_modifier)? @context - "const" @context - name: (_) @name) @item diff --git a/extensions/test-extension/src/test_extension.rs b/extensions/test-extension/src/test_extension.rs deleted file mode 100644 index 0b96f47038..0000000000 --- a/extensions/test-extension/src/test_extension.rs +++ /dev/null @@ -1,209 +0,0 @@ -use std::fs; -use zed::lsp::CompletionKind; -use zed::{CodeLabel, CodeLabelSpan, LanguageServerId}; -use zed_extension_api::process::Command; -use zed_extension_api::{self as zed, Result}; - -struct TestExtension { - cached_binary_path: Option, -} - -impl TestExtension { - fn language_server_binary_path( - &mut self, - language_server_id: &LanguageServerId, - _worktree: &zed::Worktree, - ) -> Result { - let (platform, arch) = zed::current_platform(); - - let current_dir = std::env::current_dir().unwrap(); - println!("current_dir: {}", current_dir.display()); - assert_eq!( - current_dir.file_name().unwrap().to_str().unwrap(), - "test-extension" - ); - - fs::create_dir_all(current_dir.join("dir-created-with-abs-path")).unwrap(); - fs::create_dir_all("./dir-created-with-rel-path").unwrap(); - fs::write("file-created-with-rel-path", b"contents 1").unwrap(); - fs::write( - current_dir.join("file-created-with-abs-path"), - b"contents 2", - ) - .unwrap(); - assert_eq!( - fs::read("file-created-with-rel-path").unwrap(), - b"contents 1" - ); - assert_eq!( - fs::read("file-created-with-abs-path").unwrap(), - b"contents 2" - ); - - let command = match platform { - zed::Os::Linux | zed::Os::Mac => Command::new("echo"), - zed::Os::Windows => Command::new("cmd").args(["/C", "echo"]), - }; - let output = command.arg("hello from a child process!").output()?; - println!( - "command output: {}", - String::from_utf8_lossy(&output.stdout).trim() - ); - - if let Some(path) = &self.cached_binary_path - && fs::metadata(path).is_ok_and(|stat| stat.is_file()) - { - return Ok(path.clone()); - } - - zed::set_language_server_installation_status( - language_server_id, - &zed::LanguageServerInstallationStatus::CheckingForUpdate, - ); - let release = zed::latest_github_release( - "gleam-lang/gleam", - zed::GithubReleaseOptions { - require_assets: true, - pre_release: false, - }, - )?; - - let ext = "tar.gz"; - let download_type = zed::DownloadedFileType::GzipTar; - - // Do this if you want to actually run this extension - - // the actual asset is a .zip. But the integration test is simpler - // if every platform uses .tar.gz. - // - // ext = "zip"; - // download_type = zed::DownloadedFileType::Zip; - - let asset_name = format!( - "gleam-{version}-{arch}-{os}.{ext}", - version = release.version, - arch = match arch { - zed::Architecture::Aarch64 => "aarch64", - zed::Architecture::X86 => "x86", - zed::Architecture::X8664 => "x86_64", - }, - os = match platform { - zed::Os::Mac => "apple-darwin", - zed::Os::Linux => "unknown-linux-musl", - zed::Os::Windows => "pc-windows-msvc", - }, - ); - - let asset = release - .assets - .iter() - .find(|asset| asset.name == asset_name) - .ok_or_else(|| format!("no asset found matching {:?}", asset_name))?; - - let version_dir = format!("gleam-{}", release.version); - let binary_path = format!("{version_dir}/gleam"); - - if !fs::metadata(&binary_path).is_ok_and(|stat| stat.is_file()) { - zed::set_language_server_installation_status( - language_server_id, - &zed::LanguageServerInstallationStatus::Downloading, - ); - - zed::download_file(&asset.download_url, &version_dir, download_type) - .map_err(|e| format!("failed to download file: {e}"))?; - - zed::set_language_server_installation_status( - language_server_id, - &zed::LanguageServerInstallationStatus::None, - ); - - let entries = - fs::read_dir(".").map_err(|e| format!("failed to list working directory {e}"))?; - for entry in entries { - let entry = entry.map_err(|e| format!("failed to load directory entry {e}"))?; - let filename = entry.file_name(); - let filename = filename.to_str().unwrap(); - if filename.starts_with("gleam-") && filename != version_dir { - fs::remove_dir_all(entry.path()).ok(); - } - } - } - - self.cached_binary_path = Some(binary_path.clone()); - Ok(binary_path) - } -} - -impl zed::Extension for TestExtension { - fn new() -> Self { - Self { - cached_binary_path: None, - } - } - - fn language_server_command( - &mut self, - language_server_id: &LanguageServerId, - worktree: &zed::Worktree, - ) -> Result { - Ok(zed::Command { - command: self.language_server_binary_path(language_server_id, worktree)?, - args: vec!["lsp".to_string()], - env: Default::default(), - }) - } - - fn label_for_completion( - &self, - _language_server_id: &LanguageServerId, - completion: zed::lsp::Completion, - ) -> Option { - let name = &completion.label; - let ty = strip_newlines_from_detail(&completion.detail?); - let let_binding = "let a"; - let colon = ": "; - let assignment = " = "; - let call = match completion.kind? { - CompletionKind::Function | CompletionKind::Constructor => "()", - _ => "", - }; - let code = format!("{let_binding}{colon}{ty}{assignment}{name}{call}"); - - Some(CodeLabel { - spans: vec![ - CodeLabelSpan::code_range({ - let start = let_binding.len() + colon.len() + ty.len() + assignment.len(); - start..start + name.len() - }), - CodeLabelSpan::code_range({ - let start = let_binding.len(); - start..start + colon.len() - }), - CodeLabelSpan::code_range({ - let start = let_binding.len() + colon.len(); - start..start + ty.len() - }), - ], - filter_range: (0..name.len()).into(), - code, - }) - } -} - -zed::register_extension!(TestExtension); - -/// Removes newlines from the completion detail. -/// -/// The Gleam LSP can return types containing newlines, which causes formatting -/// issues within the Zed completions menu. -fn strip_newlines_from_detail(detail: &str) -> String { - let without_newlines = detail - .replace("->\n ", "-> ") - .replace("\n ", "") - .replace(",\n", ""); - - let comma_delimited_parts = without_newlines.split(','); - comma_delimited_parts - .map(|part| part.trim()) - .collect::>() - .join(", ") -} diff --git a/extensions/workflows/bump_version.yml b/extensions/workflows/bump_version.yml deleted file mode 100644 index 7f4318dcf5..0000000000 --- a/extensions/workflows/bump_version.yml +++ /dev/null @@ -1,52 +0,0 @@ -# Generated from xtask::workflows::extensions::bump_version within the Zed repository. -# Rebuild with `cargo xtask workflows`. -name: extensions::bump_version -on: - pull_request: - types: - - labeled - push: - branches: - - main - paths-ignore: - - .github/** - workflow_dispatch: {} -jobs: - determine_bump_type: - runs-on: namespace-profile-16x32-ubuntu-2204 - steps: - - id: get-bump-type - name: extensions::bump_version::get_bump_type - run: | - if [ "$HAS_MAJOR_LABEL" = "true" ]; then - bump_type="major" - elif [ "$HAS_MINOR_LABEL" = "true" ]; then - bump_type="minor" - else - bump_type="patch" - fi - echo "bump_type=$bump_type" >> $GITHUB_OUTPUT - shell: bash -euxo pipefail {0} - env: - HAS_MAJOR_LABEL: |- - ${{ (github.event.action == 'labeled' && github.event.label.name == 'major') || - (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'major')) }} - HAS_MINOR_LABEL: |- - ${{ (github.event.action == 'labeled' && github.event.label.name == 'minor') || - (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'minor')) }} - outputs: - bump_type: ${{ steps.get-bump-type.outputs.bump_type }} - call_bump_version: - needs: - - determine_bump_type - if: github.event.action != 'labeled' || needs.determine_bump_type.outputs.bump_type != 'patch' - uses: zed-industries/zed/.github/workflows/extension_bump.yml@main - secrets: - app-id: ${{ secrets.ZED_ZIPPY_APP_ID }} - app-secret: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }} - with: - bump-type: ${{ needs.determine_bump_type.outputs.bump_type }} - force-bump: true -concurrency: - group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}labels - cancel-in-progress: true diff --git a/extensions/workflows/release_version.yml b/extensions/workflows/release_version.yml deleted file mode 100644 index f752931917..0000000000 --- a/extensions/workflows/release_version.yml +++ /dev/null @@ -1,13 +0,0 @@ -# Generated from xtask::workflows::extensions::release_version within the Zed repository. -# Rebuild with `cargo xtask workflows`. -name: extensions::release_version -on: - push: - tags: - - v** -jobs: - call_release_version: - uses: zed-industries/zed/.github/workflows/extension_release.yml@main - secrets: - app-id: ${{ secrets.ZED_ZIPPY_APP_ID }} - app-secret: ${{ secrets.ZED_ZIPPY_APP_PRIVATE_KEY }} diff --git a/extensions/workflows/run_tests.yml b/extensions/workflows/run_tests.yml deleted file mode 100644 index 81ba76c483..0000000000 --- a/extensions/workflows/run_tests.yml +++ /dev/null @@ -1,16 +0,0 @@ -# Generated from xtask::workflows::extensions::run_tests within the Zed repository. -# Rebuild with `cargo xtask workflows`. -name: extensions::run_tests -on: - pull_request: - branches: - - '**' - push: - branches: - - main -jobs: - call_extension_tests: - uses: zed-industries/zed/.github/workflows/extension_tests.yml@main -concurrency: - group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }}pr - cancel-in-progress: true diff --git a/livekit.yaml b/livekit.yaml deleted file mode 100644 index f2d1946721..0000000000 --- a/livekit.yaml +++ /dev/null @@ -1,10 +0,0 @@ -development: true -port: 7880 -rtc: - udp_port: 7882 - tcp_port: 7881 - use_external_ip: false -keys: - devkey: secret -logging: - level: debug diff --git a/tooling/xtask/Cargo.toml b/tooling/xtask/Cargo.toml deleted file mode 100644 index 13179b2eb6..0000000000 --- a/tooling/xtask/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -name = "xtask" -version = "0.1.0" -edition.workspace = true -publish.workspace = true -license = "GPL-3.0-or-later" - -[lints] -workspace = true - -[dependencies] -anyhow.workspace = true -backtrace.workspace = true -cargo_metadata.workspace = true -cargo_toml.workspace = true -clap = { workspace = true, features = ["derive"] } -toml.workspace = true -indoc.workspace = true -indexmap.workspace = true -serde.workspace = true -serde_json.workspace = true -toml_edit.workspace = true -gh-workflow.workspace = true diff --git a/tooling/xtask/LICENSE-GPL b/tooling/xtask/LICENSE-GPL deleted file mode 120000 index 89e542f750..0000000000 --- a/tooling/xtask/LICENSE-GPL +++ /dev/null @@ -1 +0,0 @@ -../../LICENSE-GPL \ No newline at end of file diff --git a/tooling/xtask/src/main.rs b/tooling/xtask/src/main.rs deleted file mode 100644 index 6f83927d67..0000000000 --- a/tooling/xtask/src/main.rs +++ /dev/null @@ -1,38 +0,0 @@ -mod tasks; -mod workspace; - -use anyhow::Result; -use clap::{Parser, Subcommand}; - -#[derive(Parser)] -#[command(name = "cargo xtask")] -struct Args { - #[command(subcommand)] - command: CliCommand, -} - -#[derive(Subcommand)] -enum CliCommand { - /// Runs `cargo clippy`. - Clippy(tasks::clippy::ClippyArgs), - Licenses(tasks::licenses::LicensesArgs), - /// Checks that packages conform to a set of standards. - PackageConformity(tasks::package_conformity::PackageConformityArgs), - /// Publishes GPUI and its dependencies to crates.io. - PublishGpui(tasks::publish_gpui::PublishGpuiArgs), - Workflows(tasks::workflows::GenerateWorkflowArgs), -} - -fn main() -> Result<()> { - let args = Args::parse(); - - match args.command { - CliCommand::Clippy(args) => tasks::clippy::run_clippy(args), - CliCommand::Licenses(args) => tasks::licenses::run_licenses(args), - CliCommand::PackageConformity(args) => { - tasks::package_conformity::run_package_conformity(args) - } - CliCommand::PublishGpui(args) => tasks::publish_gpui::run_publish_gpui(args), - CliCommand::Workflows(args) => tasks::workflows::run_workflows(args), - } -} diff --git a/tooling/xtask/src/tasks.rs b/tooling/xtask/src/tasks.rs deleted file mode 100644 index 01b3907f04..0000000000 --- a/tooling/xtask/src/tasks.rs +++ /dev/null @@ -1,5 +0,0 @@ -pub mod clippy; -pub mod licenses; -pub mod package_conformity; -pub mod publish_gpui; -pub mod workflows; diff --git a/tooling/xtask/src/tasks/clippy.rs b/tooling/xtask/src/tasks/clippy.rs deleted file mode 100644 index 517223ceb8..0000000000 --- a/tooling/xtask/src/tasks/clippy.rs +++ /dev/null @@ -1,64 +0,0 @@ -#![allow(clippy::disallowed_methods, reason = "tooling is exempt")] -use std::process::Command; - -use anyhow::{Context as _, Result, bail}; -use clap::Parser; - -#[derive(Parser)] -pub struct ClippyArgs { - /// Automatically apply lint suggestions (`clippy --fix`). - #[arg(long)] - fix: bool, - - /// The package to run Clippy against (`cargo -p clippy`). - #[arg(long, short)] - package: Option, -} - -pub fn run_clippy(args: ClippyArgs) -> Result<()> { - let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); - - let mut clippy_command = Command::new(&cargo); - clippy_command.arg("clippy"); - - if let Some(package) = args.package.as_ref() { - clippy_command.args(["--package", package]); - } else { - clippy_command.arg("--workspace"); - } - - clippy_command - .arg("--release") - .arg("--all-targets") - .arg("--all-features"); - - if args.fix { - clippy_command.arg("--fix"); - } - - clippy_command.arg("--"); - - // Deny all warnings. - clippy_command.args(["--deny", "warnings"]); - - eprintln!( - "running: {cargo} {}", - clippy_command - .get_args() - .map(|arg| arg.to_str().unwrap()) - .collect::>() - .join(" ") - ); - - let exit_status = clippy_command - .spawn() - .context("failed to spawn child process")? - .wait() - .context("failed to wait for child process")?; - - if !exit_status.success() { - bail!("clippy failed: {}", exit_status); - } - - Ok(()) -} diff --git a/tooling/xtask/src/tasks/licenses.rs b/tooling/xtask/src/tasks/licenses.rs deleted file mode 100644 index 449c774d45..0000000000 --- a/tooling/xtask/src/tasks/licenses.rs +++ /dev/null @@ -1,45 +0,0 @@ -use std::path::{Path, PathBuf}; - -use anyhow::{Context as _, Result}; -use clap::Parser; - -use crate::workspace::load_workspace; - -#[derive(Parser)] -pub struct LicensesArgs {} - -pub fn run_licenses(_args: LicensesArgs) -> Result<()> { - const LICENSE_FILES: &[&str] = &["LICENSE-APACHE", "LICENSE-GPL", "LICENSE-AGPL"]; - - let workspace = load_workspace()?; - - for package in workspace.workspace_packages() { - let crate_dir = package - .manifest_path - .parent() - .with_context(|| format!("no crate directory for {}", package.name))?; - - if let Some(license_file) = first_license_file(crate_dir, LICENSE_FILES) { - if !license_file.is_symlink() { - println!("{} is not a symlink", license_file.display()); - } - - continue; - } - - println!("Missing license: {}", package.name); - } - - Ok(()) -} - -fn first_license_file(path: impl AsRef, license_files: &[&str]) -> Option { - for license_file in license_files { - let path_to_license = path.as_ref().join(license_file); - if path_to_license.exists() { - return Some(path_to_license); - } - } - - None -} diff --git a/tooling/xtask/src/tasks/package_conformity.rs b/tooling/xtask/src/tasks/package_conformity.rs deleted file mode 100644 index e1fd15112f..0000000000 --- a/tooling/xtask/src/tasks/package_conformity.rs +++ /dev/null @@ -1,75 +0,0 @@ -use std::collections::BTreeMap; -use std::fs; -use std::path::Path; - -use anyhow::{Context as _, Result}; -use cargo_toml::{Dependency, Manifest}; -use clap::Parser; - -use crate::workspace::load_workspace; - -#[derive(Parser)] -pub struct PackageConformityArgs {} - -pub fn run_package_conformity(_args: PackageConformityArgs) -> Result<()> { - let workspace = load_workspace()?; - - let mut non_workspace_dependencies = BTreeMap::new(); - - for package in workspace.workspace_packages() { - let is_extension = package - .manifest_path - .parent() - .and_then(|parent| parent.parent()) - .is_some_and(|grandparent_dir| grandparent_dir.ends_with("extensions")); - - let cargo_toml = read_cargo_toml(&package.manifest_path)?; - - let is_using_workspace_lints = cargo_toml.lints.is_some_and(|lints| lints.workspace); - if !is_using_workspace_lints { - eprintln!( - "{package:?} is not using workspace lints", - package = package.name - ); - } - - // Extensions should not use workspace dependencies. - if is_extension || package.name == "zed_extension_api" { - continue; - } - - for dependencies in [ - &cargo_toml.dependencies, - &cargo_toml.dev_dependencies, - &cargo_toml.build_dependencies, - ] { - for (name, dependency) in dependencies { - if let Dependency::Inherited(_) = dependency { - continue; - } - - non_workspace_dependencies - .entry(name.to_owned()) - .or_insert_with(Vec::new) - .push(package.name.clone()); - } - } - } - - for (dependency, packages) in non_workspace_dependencies { - eprintln!( - "{dependency} is being used as a non-workspace dependency: {}", - packages.join(", ") - ); - } - - Ok(()) -} - -/// Returns the contents of the `Cargo.toml` file at the given path. -fn read_cargo_toml(path: impl AsRef) -> Result { - let path = path.as_ref(); - let cargo_toml_bytes = fs::read(path)?; - Manifest::from_slice(&cargo_toml_bytes) - .with_context(|| format!("reading Cargo.toml at {path:?}")) -} diff --git a/tooling/xtask/src/tasks/publish_gpui.rs b/tooling/xtask/src/tasks/publish_gpui.rs deleted file mode 100644 index 2740f75a48..0000000000 --- a/tooling/xtask/src/tasks/publish_gpui.rs +++ /dev/null @@ -1,442 +0,0 @@ -#![allow(clippy::disallowed_methods, reason = "tooling is exempt")] -use std::io::{self, Write}; -use std::process::{Command, Output, Stdio}; - -use anyhow::{Context as _, Result, bail}; -use clap::Parser; - -#[derive(Parser)] -pub struct PublishGpuiArgs { - /// Perform a dry-run and wait for user confirmation before each publish - #[arg(long)] - dry_run: bool, - - /// Skip to a specific package (by package name or crate name) and start from there - #[arg(long)] - skip_to: Option, -} - -pub fn run_publish_gpui(args: PublishGpuiArgs) -> Result<()> { - println!( - "Starting GPUI publish process{}...", - if args.dry_run { " (with dry-run)" } else { "" } - ); - - let start_time = std::time::Instant::now(); - check_workspace_root()?; - - if args.skip_to.is_none() { - check_git_clean()?; - } else { - println!("Skipping git clean check due to --skip-to flag"); - } - - let version = read_gpui_version()?; - println!("Updating GPUI to version: {}", version); - publish_dependencies(&version, args.dry_run, args.skip_to.as_deref())?; - publish_gpui(&version, args.dry_run)?; - println!("GPUI published in {}s", start_time.elapsed().as_secs_f32()); - Ok(()) -} - -fn read_gpui_version() -> Result { - let gpui_cargo_toml_path = "crates/gpui/Cargo.toml"; - let contents = std::fs::read_to_string(gpui_cargo_toml_path) - .context("Failed to read crates/gpui/Cargo.toml")?; - - let cargo_toml: toml::Value = - toml::from_str(&contents).context("Failed to parse crates/gpui/Cargo.toml")?; - - let version = cargo_toml - .get("package") - .and_then(|p| p.get("version")) - .and_then(|v| v.as_str()) - .context("Failed to find version in crates/gpui/Cargo.toml")?; - - Ok(version.to_string()) -} - -fn publish_dependencies(new_version: &str, dry_run: bool, skip_to: Option<&str>) -> Result<()> { - let gpui_dependencies = vec![ - ("collections", "gpui_collections", "crates"), - ("perf", "gpui_perf", "tooling"), - ("util_macros", "gpui_util_macros", "crates"), - ("util", "gpui_util", "crates"), - ("gpui_macros", "gpui-macros", "crates"), - ("http_client", "gpui_http_client", "crates"), - ( - "derive_refineable", - "gpui_derive_refineable", - "crates/refineable", - ), - ("refineable", "gpui_refineable", "crates"), - ("semantic_version", "gpui_semantic_version", "crates"), - ("sum_tree", "gpui_sum_tree", "crates"), - ("media", "gpui_media", "crates"), - ]; - - let mut should_skip = skip_to.is_some(); - let skip_target = skip_to.unwrap_or(""); - - for (package_name, crate_name, package_dir) in gpui_dependencies { - if should_skip { - if package_name == skip_target || crate_name == skip_target { - println!("Found skip target: {} ({})", crate_name, package_name); - should_skip = false; - } else { - println!("Skipping: {} ({})", crate_name, package_name); - continue; - } - } - - println!( - "Publishing dependency: {} (package: {})", - crate_name, package_name - ); - - update_crate_cargo_toml(package_name, crate_name, package_dir, new_version)?; - update_workspace_dependency_version(package_name, crate_name, new_version)?; - publish_crate(crate_name, dry_run)?; - } - - if should_skip { - bail!( - "Could not find package or crate named '{}' to skip to", - skip_target - ); - } - - Ok(()) -} - -fn publish_gpui(new_version: &str, dry_run: bool) -> Result<()> { - update_crate_cargo_toml("gpui", "gpui", "crates", new_version)?; - - publish_crate("gpui", dry_run)?; - - Ok(()) -} - -fn update_crate_cargo_toml( - package_name: &str, - crate_name: &str, - package_dir: &str, - new_version: &str, -) -> Result<()> { - let cargo_toml_path = format!("{}/{}/Cargo.toml", package_dir, package_name); - let contents = std::fs::read_to_string(&cargo_toml_path) - .context(format!("Failed to read {}", cargo_toml_path))?; - - let updated = update_crate_package_fields(&contents, crate_name, new_version)?; - - std::fs::write(&cargo_toml_path, updated) - .context(format!("Failed to write {}", cargo_toml_path))?; - - Ok(()) -} - -fn update_crate_package_fields( - toml_contents: &str, - crate_name: &str, - new_version: &str, -) -> Result { - let mut doc = toml_contents - .parse::() - .context("Failed to parse TOML")?; - - let package = doc - .get_mut("package") - .and_then(|p| p.as_table_like_mut()) - .context("Failed to find [package] section")?; - - package.insert("name", toml_edit::value(crate_name)); - package.insert("version", toml_edit::value(new_version)); - package.insert("publish", toml_edit::value(true)); - - Ok(doc.to_string()) -} - -fn publish_crate(crate_name: &str, dry_run: bool) -> Result<()> { - let publish_crate_impl = |crate_name, dry_run| { - let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); - - let mut command = Command::new(&cargo); - command - .arg("publish") - .arg("--allow-dirty") - .args(["-p", crate_name]); - - if dry_run { - command.arg("--dry-run"); - } - - run_command(&mut command)?; - - anyhow::Ok(()) - }; - - if dry_run { - publish_crate_impl(crate_name, true)?; - - print!("Press Enter to publish for real (or ctrl-c to abort)..."); - io::stdout().flush()?; - - let mut input = String::new(); - io::stdin().read_line(&mut input)?; - } - - publish_crate_impl(crate_name, false)?; - - Ok(()) -} - -fn update_workspace_dependency_version( - package_name: &str, - crate_name: &str, - new_version: &str, -) -> Result<()> { - let workspace_cargo_toml_path = "Cargo.toml"; - let contents = std::fs::read_to_string(workspace_cargo_toml_path) - .context("Failed to read workspace Cargo.toml")?; - - let mut doc = contents - .parse::() - .context("Failed to parse TOML")?; - - update_dependency_version_in_doc(&mut doc, package_name, crate_name, new_version)?; - update_profile_override_in_doc(&mut doc, package_name, crate_name)?; - - std::fs::write(workspace_cargo_toml_path, doc.to_string()) - .context("Failed to write workspace Cargo.toml")?; - - Ok(()) -} - -fn update_dependency_version_in_doc( - doc: &mut toml_edit::DocumentMut, - package_name: &str, - crate_name: &str, - new_version: &str, -) -> Result<()> { - let dependency = doc - .get_mut("workspace") - .and_then(|w| w.get_mut("dependencies")) - .and_then(|d| d.get_mut(package_name)) - .context(format!( - "Failed to find {} in workspace dependencies", - package_name - ))?; - - if let Some(dep_table) = dependency.as_table_like_mut() { - dep_table.insert("version", toml_edit::value(new_version)); - dep_table.insert("package", toml_edit::value(crate_name)); - } else { - bail!("{} is not a table in workspace dependencies", package_name); - } - - Ok(()) -} - -fn update_profile_override_in_doc( - doc: &mut toml_edit::DocumentMut, - package_name: &str, - crate_name: &str, -) -> Result<()> { - if let Some(profile_dev_package) = doc - .get_mut("profile") - .and_then(|p| p.get_mut("dev")) - .and_then(|d| d.get_mut("package")) - .and_then(|p| p.as_table_like_mut()) - { - if let Some(old_entry) = profile_dev_package.get(package_name) { - let old_entry_clone = old_entry.clone(); - profile_dev_package.remove(package_name); - profile_dev_package.insert(crate_name, old_entry_clone); - } - } - - Ok(()) -} - -fn check_workspace_root() -> Result<()> { - let cwd = std::env::current_dir().context("Failed to get current directory")?; - - // Check if Cargo.toml exists in the current directory - let cargo_toml_path = cwd.join("Cargo.toml"); - if !cargo_toml_path.exists() { - bail!( - "Cargo.toml not found in current directory. Please run this command from the workspace root." - ); - } - - // Check if it's a workspace by looking for [workspace] section - let contents = - std::fs::read_to_string(&cargo_toml_path).context("Failed to read Cargo.toml")?; - - if !contents.contains("[workspace]") { - bail!( - "Current directory does not appear to be a workspace root. Please run this command from the workspace root." - ); - } - - Ok(()) -} - -fn check_git_clean() -> Result<()> { - let output = run_command( - Command::new("git") - .args(["status", "--porcelain"]) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()), - )?; - - if !output.status.success() { - bail!("git status command failed"); - } - - let stdout = String::from_utf8_lossy(&output.stdout); - if !stdout.trim().is_empty() { - bail!( - "Working directory is not clean. Please commit or stash your changes before publishing." - ); - } - - Ok(()) -} - -fn run_command(command: &mut Command) -> Result { - let command_str = { - let program = command.get_program().to_string_lossy(); - let args = command - .get_args() - .map(|arg| arg.to_string_lossy()) - .collect::>() - .join(" "); - - if args.is_empty() { - program.to_string() - } else { - format!("{} {}", program, args) - } - }; - eprintln!("+ {}", command_str); - - let output = command - .spawn() - .context("failed to spawn child process")? - .wait_with_output() - .context("failed to wait for child process")?; - - if !output.status.success() { - bail!("Command failed with status {}", output.status); - } - - Ok(output) -} - -#[cfg(test)] -mod tests { - use indoc::indoc; - - use super::*; - - #[test] - fn test_update_dependency_version_in_toml() { - let input = indoc! {r#" - [workspace] - resolver = "2" - - [workspace.dependencies] - # here's a comment - collections = { path = "crates/collections" } - - util = { path = "crates/util", package = "zed-util", version = "0.1.0" } - "#}; - - let mut doc = input.parse::().unwrap(); - - update_dependency_version_in_doc(&mut doc, "collections", "gpui_collections", "0.2.0") - .unwrap(); - - let result = doc.to_string(); - - let output = indoc! {r#" - [workspace] - resolver = "2" - - [workspace.dependencies] - # here's a comment - collections = { path = "crates/collections" , version = "0.2.0", package = "gpui_collections" } - - util = { path = "crates/util", package = "zed-util", version = "0.1.0" } - "#}; - - assert_eq!(result, output); - } - - #[test] - fn test_update_crate_package_fields() { - let input = indoc! {r#" - [package] - name = "collections" - version = "0.1.0" - edition = "2021" - publish = false - # some comment about the license - license = "GPL-3.0-or-later" - - [dependencies] - serde = "1.0" - "#}; - - let result = update_crate_package_fields(input, "gpui_collections", "0.2.0").unwrap(); - - let output = indoc! {r#" - [package] - name = "gpui_collections" - version = "0.2.0" - edition = "2021" - publish = true - # some comment about the license - license = "GPL-3.0-or-later" - - [dependencies] - serde = "1.0" - "#}; - - assert_eq!(result, output); - } - - #[test] - fn test_update_profile_override_in_toml() { - let input = indoc! {r#" - [profile.dev] - split-debuginfo = "unpacked" - - [profile.dev.package] - taffy = { opt-level = 3 } - collections = { codegen-units = 256 } - refineable = { codegen-units = 256 } - util = { codegen-units = 256 } - "#}; - - let mut doc = input.parse::().unwrap(); - - update_profile_override_in_doc(&mut doc, "collections", "gpui_collections").unwrap(); - - let result = doc.to_string(); - - let output = indoc! {r#" - [profile.dev] - split-debuginfo = "unpacked" - - [profile.dev.package] - taffy = { opt-level = 3 } - refineable = { codegen-units = 256 } - util = { codegen-units = 256 } - gpui_collections = { codegen-units = 256 } - "#}; - - assert_eq!(result, output); - } -} diff --git a/tooling/xtask/src/tasks/workflows.rs b/tooling/xtask/src/tasks/workflows.rs deleted file mode 100644 index 717517402d..0000000000 --- a/tooling/xtask/src/tasks/workflows.rs +++ /dev/null @@ -1,138 +0,0 @@ -use anyhow::{Context, Result}; -use clap::Parser; -use gh_workflow::Workflow; -use std::fs; -use std::path::{Path, PathBuf}; - -mod after_release; -mod cherry_pick; -mod compare_perf; -mod danger; -mod extension_bump; -mod extension_release; -mod extension_tests; -mod extensions; -mod nix_build; -mod release_nightly; -mod run_bundling; - -mod release; -mod run_agent_evals; -mod run_tests; -mod runners; -mod steps; -mod vars; - -#[derive(Parser)] -pub struct GenerateWorkflowArgs {} - -struct WorkflowFile { - source: fn() -> Workflow, - r#type: WorkflowType, -} - -impl WorkflowFile { - fn zed(f: fn() -> Workflow) -> WorkflowFile { - WorkflowFile { - source: f, - r#type: WorkflowType::Zed, - } - } - fn extension(f: fn() -> Workflow) -> WorkflowFile { - WorkflowFile { - source: f, - r#type: WorkflowType::Extensions, - } - } - - fn generate_file(&self) -> Result<()> { - let workflow = (self.source)(); - let workflow_folder = self.r#type.folder_path(); - let workflow_name = workflow - .name - .as_ref() - .expect("Workflow must have a name at this point"); - let filename = format!( - "{}.yml", - workflow_name.rsplit("::").next().unwrap_or(workflow_name) - ); - - let workflow_path = workflow_folder.join(filename); - - let content = workflow - .to_string() - .map_err(|e| anyhow::anyhow!("{:?}: {:?}", workflow_path, e))?; - - let disclaimer = self.r#type.disclaimer(workflow_name); - - let content = [disclaimer, content].join("\n"); - fs::write(&workflow_path, content).map_err(Into::into) - } -} - -enum WorkflowType { - Zed, - Extensions, -} - -impl WorkflowType { - fn disclaimer(&self, workflow_name: &str) -> String { - format!( - concat!( - "# Generated from xtask::workflows::{}{}\n", - "# Rebuild with `cargo xtask workflows`.", - ), - workflow_name, - matches!(self, WorkflowType::Extensions) - .then_some(" within the Zed repository.") - .unwrap_or_default(), - ) - } - - fn folder_path(&self) -> PathBuf { - match self { - WorkflowType::Zed => PathBuf::from(".github/workflows"), - WorkflowType::Extensions => PathBuf::from("extensions/workflows"), - } - } -} - -pub fn run_workflows(_: GenerateWorkflowArgs) -> Result<()> { - if !Path::new("crates/zed/").is_dir() { - anyhow::bail!("xtask workflows must be ran from the project root"); - } - let workflow_dir = Path::new(".github/workflows"); - let extension_workflow_dir = Path::new("extensions/workflows"); - - let workflows = [ - WorkflowFile::zed(danger::danger), - WorkflowFile::zed(run_bundling::run_bundling), - WorkflowFile::zed(release_nightly::release_nightly), - WorkflowFile::zed(run_tests::run_tests), - WorkflowFile::zed(release::release), - WorkflowFile::zed(cherry_pick::cherry_pick), - WorkflowFile::zed(compare_perf::compare_perf), - WorkflowFile::zed(run_agent_evals::run_unit_evals), - WorkflowFile::zed(run_agent_evals::run_cron_unit_evals), - WorkflowFile::zed(run_agent_evals::run_agent_evals), - WorkflowFile::zed(after_release::after_release), - WorkflowFile::zed(extension_tests::extension_tests), - WorkflowFile::zed(extension_bump::extension_bump), - WorkflowFile::zed(extension_release::extension_release), - /* workflows used for CI/CD in extension repositories */ - WorkflowFile::extension(extensions::run_tests::run_tests), - WorkflowFile::extension(extensions::bump_version::bump_version), - WorkflowFile::extension(extensions::release_version::release_version), - ]; - - for directory in [&workflow_dir, &extension_workflow_dir] { - fs::create_dir_all(directory) - .with_context(|| format!("Failed to create directory: {}", directory.display()))?; - } - - for workflow_file in workflows { - workflow_file.generate_file()?; - } - - Ok(()) -} diff --git a/tooling/xtask/src/tasks/workflows/after_release.rs b/tooling/xtask/src/tasks/workflows/after_release.rs deleted file mode 100644 index c475617197..0000000000 --- a/tooling/xtask/src/tasks/workflows/after_release.rs +++ /dev/null @@ -1,164 +0,0 @@ -use gh_workflow::*; - -use crate::tasks::workflows::{ - release::{self, notify_on_failure}, - runners, - steps::{CommonJobConditions, NamedJob, checkout_repo, dependant_job, named}, - vars::{self, StepOutput, WorkflowInput}, -}; - -const TAG_NAME: &str = "${{ github.event.release.tag_name || inputs.tag_name }}"; -const IS_PRERELEASE: &str = "${{ github.event.release.prerelease || inputs.prerelease }}"; -const RELEASE_BODY: &str = "${{ github.event.release.body || inputs.body }}"; - -pub fn after_release() -> Workflow { - let tag_name = WorkflowInput::string("tag_name", None); - let prerelease = WorkflowInput::bool("prerelease", None); - let body = WorkflowInput::string("body", Some(String::new())); - - let refresh_zed_dev = rebuild_releases_page(); - let post_to_discord = post_to_discord(&[&refresh_zed_dev]); - let publish_winget = publish_winget(); - let create_sentry_release = create_sentry_release(); - let notify_on_failure = notify_on_failure(&[ - &refresh_zed_dev, - &post_to_discord, - &publish_winget, - &create_sentry_release, - ]); - - named::workflow() - .on(Event::default() - .release(Release::default().types(vec![ReleaseType::Published])) - .workflow_dispatch( - WorkflowDispatch::default() - .add_input(tag_name.name, tag_name.input()) - .add_input(prerelease.name, prerelease.input()) - .add_input(body.name, body.input()), - )) - .add_job(refresh_zed_dev.name, refresh_zed_dev.job) - .add_job(post_to_discord.name, post_to_discord.job) - .add_job(publish_winget.name, publish_winget.job) - .add_job(create_sentry_release.name, create_sentry_release.job) - .add_job(notify_on_failure.name, notify_on_failure.job) -} - -fn rebuild_releases_page() -> NamedJob { - fn refresh_cloud_releases() -> Step { - named::bash(format!( - "curl -fX POST https://cloud.zed.dev/releases/refresh?expect_tag={TAG_NAME}" - )) - } - - fn redeploy_zed_dev() -> Step { - named::bash("npm exec --yes -- vercel@37 --token=\"$VERCEL_TOKEN\" --scope zed-industries redeploy https://zed.dev") - .add_env(("VERCEL_TOKEN", vars::VERCEL_TOKEN)) - } - - named::job( - Job::default() - .runs_on(runners::LINUX_SMALL) - .with_repository_owner_guard() - .add_step(refresh_cloud_releases()) - .add_step(redeploy_zed_dev()), - ) -} - -fn post_to_discord(deps: &[&NamedJob]) -> NamedJob { - fn get_release_url() -> Step { - named::bash(format!( - r#"if [ "{IS_PRERELEASE}" == "true" ]; then - URL="https://zed.dev/releases/preview" -else - URL="https://zed.dev/releases/stable" -fi - -echo "URL=$URL" >> "$GITHUB_OUTPUT" -"# - )) - .id("get-release-url") - } - - fn get_content() -> Step { - named::uses( - "2428392", - "gh-truncate-string-action", - "b3ff790d21cf42af3ca7579146eedb93c8fb0757", // v1.4.1 - ) - .id("get-content") - .add_with(( - "stringToTruncate", - format!( - "📣 Zed [{TAG_NAME}](<${{{{ steps.get-release-url.outputs.URL }}}}>) was just released!\n\n{RELEASE_BODY}\n" - ), - )) - .add_with(("maxLength", 2000)) - .add_with(("truncationSymbol", "...")) - } - - fn discord_webhook_action() -> Step { - named::uses( - "tsickert", - "discord-webhook", - "c840d45a03a323fbc3f7507ac7769dbd91bfb164", // v5.3.0 - ) - .add_with(("webhook-url", vars::DISCORD_WEBHOOK_RELEASE_NOTES)) - .add_with(("content", "${{ steps.get-content.outputs.string }}")) - } - let job = dependant_job(deps) - .runs_on(runners::LINUX_SMALL) - .with_repository_owner_guard() - .add_step(get_release_url()) - .add_step(get_content()) - .add_step(discord_webhook_action()); - named::job(job) -} - -fn publish_winget() -> NamedJob { - fn set_package_name() -> (Step, StepOutput) { - let script = format!( - r#"if ("{IS_PRERELEASE}" -eq "true") {{ - $PACKAGE_NAME = "ZedIndustries.Zed.Preview" -}} else {{ - $PACKAGE_NAME = "ZedIndustries.Zed" -}} - -echo "PACKAGE_NAME=$PACKAGE_NAME" >> $env:GITHUB_OUTPUT -"# - ); - let step = named::pwsh(&script).id("set-package-name"); - - let output = StepOutput::new(&step, "PACKAGE_NAME"); - (step, output) - } - - fn winget_releaser(package_name: &StepOutput) -> Step { - named::uses( - "vedantmgoyal9", - "winget-releaser", - "19e706d4c9121098010096f9c495a70a7518b30f", // v2 - ) - .add_with(("identifier", package_name.to_string())) - .add_with(("release-tag", TAG_NAME)) - .add_with(("max-versions-to-keep", 5)) - .add_with(("token", vars::WINGET_TOKEN)) - } - - let (set_package_name, package_name) = set_package_name(); - - named::job( - Job::default() - .runs_on(runners::WINDOWS_DEFAULT) - .add_step(set_package_name) - .add_step(winget_releaser(&package_name)), - ) -} - -fn create_sentry_release() -> NamedJob { - let job = Job::default() - .runs_on(runners::LINUX_SMALL) - .with_repository_owner_guard() - .add_step(checkout_repo()) - .add_step(release::create_sentry_release()); - named::job(job) -} diff --git a/tooling/xtask/src/tasks/workflows/cherry_pick.rs b/tooling/xtask/src/tasks/workflows/cherry_pick.rs deleted file mode 100644 index 105bf74c41..0000000000 --- a/tooling/xtask/src/tasks/workflows/cherry_pick.rs +++ /dev/null @@ -1,66 +0,0 @@ -use gh_workflow::*; - -use crate::tasks::workflows::{ - runners, - steps::{self, NamedJob, named}, - vars::{self, StepOutput, WorkflowInput}, -}; - -pub fn cherry_pick() -> Workflow { - let branch = WorkflowInput::string("branch", None); - let commit = WorkflowInput::string("commit", None); - let channel = WorkflowInput::string("channel", None); - let pr_number = WorkflowInput::string("pr_number", None); - let cherry_pick = run_cherry_pick(&branch, &commit, &channel); - named::workflow() - .run_name(format!("cherry_pick to {channel} #{pr_number}")) - .on(Event::default().workflow_dispatch( - WorkflowDispatch::default() - .add_input(commit.name, commit.input()) - .add_input(branch.name, branch.input()) - .add_input(channel.name, channel.input()) - .add_input(pr_number.name, pr_number.input()), - )) - .add_job(cherry_pick.name, cherry_pick.job) -} - -fn run_cherry_pick( - branch: &WorkflowInput, - commit: &WorkflowInput, - channel: &WorkflowInput, -) -> NamedJob { - fn authenticate_as_zippy() -> (Step, StepOutput) { - let step = named::uses( - "actions", - "create-github-app-token", - "bef1eaf1c0ac2b148ee2a0a74c65fbe6db0631f1", - ) // v2 - .add_with(("app-id", vars::ZED_ZIPPY_APP_ID)) - .add_with(("private-key", vars::ZED_ZIPPY_APP_PRIVATE_KEY)) - .id("get-app-token"); - let output = StepOutput::new(&step, "token"); - (step, output) - } - - fn cherry_pick( - branch: &WorkflowInput, - commit: &WorkflowInput, - channel: &WorkflowInput, - token: &StepOutput, - ) -> Step { - named::bash(&format!("./script/cherry-pick {branch} {commit} {channel}")) - .add_env(("GIT_COMMITTER_NAME", "Zed Zippy")) - .add_env(("GIT_COMMITTER_EMAIL", "hi@zed.dev")) - .add_env(("GITHUB_TOKEN", token)) - } - - let (authenticate, token) = authenticate_as_zippy(); - - named::job( - Job::default() - .runs_on(runners::LINUX_SMALL) - .add_step(steps::checkout_repo()) - .add_step(authenticate) - .add_step(cherry_pick(branch, commit, channel, &token)), - ) -} diff --git a/tooling/xtask/src/tasks/workflows/compare_perf.rs b/tooling/xtask/src/tasks/workflows/compare_perf.rs deleted file mode 100644 index 1d111acc4f..0000000000 --- a/tooling/xtask/src/tasks/workflows/compare_perf.rs +++ /dev/null @@ -1,67 +0,0 @@ -use gh_workflow::*; - -use crate::tasks::workflows::run_bundling::upload_artifact; -use crate::tasks::workflows::steps::FluentBuilder; -use crate::tasks::workflows::{ - runners, - steps::{self, NamedJob, named}, - vars::WorkflowInput, -}; - -pub fn compare_perf() -> Workflow { - let head = WorkflowInput::string("head", None); - let base = WorkflowInput::string("base", None); - let crate_name = WorkflowInput::string("crate_name", Some("".to_owned())); - let run_perf = run_perf(&base, &head, &crate_name); - named::workflow() - .on(Event::default().workflow_dispatch( - WorkflowDispatch::default() - .add_input(head.name, head.input()) - .add_input(base.name, base.input()) - .add_input(crate_name.name, crate_name.input()), - )) - .add_job(run_perf.name, run_perf.job) -} - -pub fn run_perf( - base: &WorkflowInput, - head: &WorkflowInput, - crate_name: &WorkflowInput, -) -> NamedJob { - fn cargo_perf_test(ref_name: &WorkflowInput, crate_name: &WorkflowInput) -> Step { - named::bash(&format!( - " - if [ -n \"{crate_name}\" ]; then - cargo perf-test -p {crate_name} -- --json={ref_name}; - else - cargo perf-test -p vim -- --json={ref_name}; - fi" - )) - } - - fn install_hyperfine() -> Step { - named::uses("taiki-e", "install-action", "hyperfine") - } - - fn compare_runs(head: &WorkflowInput, base: &WorkflowInput) -> Step { - named::bash(&format!( - "cargo perf-compare --save=results.md {base} {head}" - )) - } - - named::job( - Job::default() - .runs_on(runners::LINUX_DEFAULT) - .add_step(steps::checkout_repo()) - .add_step(steps::setup_cargo_config(runners::Platform::Linux)) - .map(steps::install_linux_dependencies) - .add_step(install_hyperfine()) - .add_step(steps::git_checkout(base)) - .add_step(cargo_perf_test(base, crate_name)) - .add_step(steps::git_checkout(head)) - .add_step(cargo_perf_test(head, crate_name)) - .add_step(compare_runs(head, base)) - .add_step(upload_artifact("results.md")) - .add_step(steps::cleanup_cargo_config(runners::Platform::Linux)), - ) -} diff --git a/tooling/xtask/src/tasks/workflows/danger.rs b/tooling/xtask/src/tasks/workflows/danger.rs deleted file mode 100644 index 8b3bf0ac3a..0000000000 --- a/tooling/xtask/src/tasks/workflows/danger.rs +++ /dev/null @@ -1,57 +0,0 @@ -use gh_workflow::*; - -use crate::tasks::workflows::steps::{CommonJobConditions, NamedJob, named}; - -use super::{runners, steps}; - -/// Generates the danger.yml workflow -pub fn danger() -> Workflow { - let danger = danger_job(); - - named::workflow() - .on( - Event::default().pull_request(PullRequest::default().add_branch("main").types([ - PullRequestType::Opened, - PullRequestType::Synchronize, - PullRequestType::Reopened, - PullRequestType::Edited, - ])), - ) - .add_job(danger.name, danger.job) -} - -fn danger_job() -> NamedJob { - pub fn install_deps() -> Step { - named::bash("pnpm install --dir script/danger") - } - - pub fn run() -> Step { - named::bash("pnpm run --dir script/danger danger ci") - // This GitHub token is not used, but the value needs to be here to prevent - // Danger from throwing an error. - .add_env(("GITHUB_TOKEN", "not_a_real_token")) - // All requests are instead proxied through an instance of - // https://github.com/maxdeviant/danger-proxy that allows Danger to securely - // authenticate with GitHub while still being able to run on PRs from forks. - .add_env(( - "DANGER_GITHUB_API_BASE_URL", - "https://danger-proxy.fly.dev/github", - )) - } - - NamedJob { - name: "danger".to_string(), - job: Job::default() - .with_repository_owner_guard() - .runs_on(runners::LINUX_SMALL) - .add_step(steps::checkout_repo()) - .add_step(steps::setup_pnpm()) - .add_step( - steps::setup_node() - .add_with(("cache", "pnpm")) - .add_with(("cache-dependency-path", "script/danger/pnpm-lock.yaml")), - ) - .add_step(install_deps()) - .add_step(run()), - } -} diff --git a/tooling/xtask/src/tasks/workflows/extension_bump.rs b/tooling/xtask/src/tasks/workflows/extension_bump.rs deleted file mode 100644 index 34fcf80990..0000000000 --- a/tooling/xtask/src/tasks/workflows/extension_bump.rs +++ /dev/null @@ -1,306 +0,0 @@ -use gh_workflow::*; -use indoc::indoc; - -use crate::tasks::workflows::{ - extension_release::extension_workflow_secrets, - extension_tests::{self}, - runners, - steps::{ - self, CommonJobConditions, DEFAULT_REPOSITORY_OWNER_GUARD, FluentBuilder, NamedJob, named, - }, - vars::{ - JobOutput, StepOutput, WorkflowInput, WorkflowSecret, one_workflow_per_non_main_branch, - }, -}; - -const VERSION_CHECK: &str = r#"sed -n 's/version = \"\(.*\)\"/\1/p' < extension.toml"#; - -// This is used by various extensions repos in the zed-extensions org to bump extension versions. -pub(crate) fn extension_bump() -> Workflow { - let bump_type = WorkflowInput::string("bump-type", Some("patch".to_owned())); - // TODO: Ideally, this would have a default of `false`, but this is currently not - // supported in gh-workflows - let force_bump = WorkflowInput::bool("force-bump", None); - - let (app_id, app_secret) = extension_workflow_secrets(); - let (check_bump_needed, needs_bump, current_version) = check_bump_needed(); - - let needs_bump = needs_bump.as_job_output(&check_bump_needed); - let current_version = current_version.as_job_output(&check_bump_needed); - - let dependencies = [&check_bump_needed]; - let bump_version = bump_extension_version( - &dependencies, - ¤t_version, - &bump_type, - &needs_bump, - &force_bump, - &app_id, - &app_secret, - ); - let create_label = create_version_label( - &dependencies, - &needs_bump, - ¤t_version, - &app_id, - &app_secret, - ); - - named::workflow() - .add_event( - Event::default().workflow_call( - WorkflowCall::default() - .add_input(bump_type.name, bump_type.call_input()) - .add_input(force_bump.name, force_bump.call_input()) - .secrets([ - (app_id.name.to_owned(), app_id.secret_configuration()), - ( - app_secret.name.to_owned(), - app_secret.secret_configuration(), - ), - ]), - ), - ) - .concurrency(one_workflow_per_non_main_branch()) - .add_env(("CARGO_TERM_COLOR", "always")) - .add_env(("RUST_BACKTRACE", 1)) - .add_env(("CARGO_INCREMENTAL", 0)) - .add_env(( - "ZED_EXTENSION_CLI_SHA", - extension_tests::ZED_EXTENSION_CLI_SHA, - )) - .add_job(check_bump_needed.name, check_bump_needed.job) - .add_job(bump_version.name, bump_version.job) - .add_job(create_label.name, create_label.job) -} - -fn check_bump_needed() -> (NamedJob, StepOutput, StepOutput) { - let (compare_versions, version_changed, current_version) = compare_versions(); - - let job = Job::default() - .with_repository_owner_guard() - .outputs([ - (version_changed.name.to_owned(), version_changed.to_string()), - ( - current_version.name.to_string(), - current_version.to_string(), - ), - ]) - .runs_on(runners::LINUX_SMALL) - .timeout_minutes(1u32) - .add_step(steps::checkout_repo().add_with(("fetch-depth", 0))) - .add_step(compare_versions); - - (named::job(job), version_changed, current_version) -} - -fn create_version_label( - dependencies: &[&NamedJob], - needs_bump: &JobOutput, - current_version: &JobOutput, - app_id: &WorkflowSecret, - app_secret: &WorkflowSecret, -) -> NamedJob { - let (generate_token, generated_token) = generate_token(app_id, app_secret, None); - let job = steps::dependant_job(dependencies) - .cond(Expression::new(format!( - "{DEFAULT_REPOSITORY_OWNER_GUARD} && github.event_name == 'push' && github.ref == 'refs/heads/main' && {} == 'false'", - needs_bump.expr(), - ))) - .runs_on(runners::LINUX_LARGE) - .timeout_minutes(1u32) - .add_step(generate_token) - .add_step(steps::checkout_repo()) - .add_step(create_version_tag(current_version, generated_token)); - - named::job(job) -} - -fn create_version_tag(current_version: &JobOutput, generated_token: StepOutput) -> Step { - named::uses("actions", "github-script", "v7").with( - Input::default() - .add( - "script", - format!( - indoc! {r#" - github.rest.git.createRef({{ - owner: context.repo.owner, - repo: context.repo.repo, - ref: 'refs/tags/v{}', - sha: context.sha - }})"# - }, - current_version - ), - ) - .add("github-token", generated_token.to_string()), - ) -} - -/// Compares the current and previous commit and checks whether versions changed inbetween. -fn compare_versions() -> (Step, StepOutput, StepOutput) { - let check_needs_bump = named::bash(format!( - indoc! { - r#" - CURRENT_VERSION="$({})" - PR_PARENT_SHA="${{{{ github.event.pull_request.head.sha }}}}" - - if [[ -n "$PR_PARENT_SHA" ]]; then - git checkout "$PR_PARENT_SHA" - elif BRANCH_PARENT_SHA="$(git merge-base origin/main origin/zed-zippy-autobump)"; then - git checkout "$BRANCH_PARENT_SHA" - else - git checkout "$(git log -1 --format=%H)"~1 - fi - - PARENT_COMMIT_VERSION="$({})" - - [[ "$CURRENT_VERSION" == "$PARENT_COMMIT_VERSION" ]] && \ - echo "needs_bump=true" >> "$GITHUB_OUTPUT" || \ - echo "needs_bump=false" >> "$GITHUB_OUTPUT" - - echo "current_version=${{CURRENT_VERSION}}" >> "$GITHUB_OUTPUT" - "# - }, - VERSION_CHECK, VERSION_CHECK - )) - .id("compare-versions-check"); - - let needs_bump = StepOutput::new(&check_needs_bump, "needs_bump"); - let current_version = StepOutput::new(&check_needs_bump, "current_version"); - - (check_needs_bump, needs_bump, current_version) -} - -fn bump_extension_version( - dependencies: &[&NamedJob], - current_version: &JobOutput, - bump_type: &WorkflowInput, - needs_bump: &JobOutput, - force_bump: &WorkflowInput, - app_id: &WorkflowSecret, - app_secret: &WorkflowSecret, -) -> NamedJob { - let (generate_token, generated_token) = generate_token(app_id, app_secret, None); - let (bump_version, new_version) = bump_version(current_version, bump_type); - - let job = steps::dependant_job(dependencies) - .cond(Expression::new(format!( - "{DEFAULT_REPOSITORY_OWNER_GUARD} &&\n({} == 'true' || {} == 'true')", - force_bump.expr(), - needs_bump.expr(), - ))) - .runs_on(runners::LINUX_LARGE) - .timeout_minutes(1u32) - .add_step(generate_token) - .add_step(steps::checkout_repo()) - .add_step(install_bump_2_version()) - .add_step(bump_version) - .add_step(create_pull_request(new_version, generated_token)); - - named::job(job) -} - -pub(crate) fn generate_token( - app_id: &WorkflowSecret, - app_secret: &WorkflowSecret, - repository_target: Option, -) -> (Step, StepOutput) { - let step = named::uses("actions", "create-github-app-token", "v2") - .id("generate-token") - .add_with( - Input::default() - .add("app-id", app_id.to_string()) - .add("private-key", app_secret.to_string()) - .when_some( - repository_target, - |input, - RepositoryTarget { - owner, - repositories, - }| { - input.add("owner", owner).add("repositories", repositories) - }, - ), - ); - - let generated_token = StepOutput::new(&step, "token"); - - (step, generated_token) -} - -fn install_bump_2_version() -> Step { - named::run(runners::Platform::Linux, "pip install bump2version") -} - -fn bump_version(current_version: &JobOutput, bump_type: &WorkflowInput) -> (Step, StepOutput) { - let step = named::bash(format!( - indoc! {r#" - OLD_VERSION="{}" - - BUMP_FILES=("extension.toml") - if [[ -f "Cargo.toml" ]]; then - BUMP_FILES+=("Cargo.toml") - fi - - bump2version --verbose --current-version "$OLD_VERSION" --no-configured-files {} "${{BUMP_FILES[@]}}" - - if [[ -f "Cargo.toml" ]]; then - cargo update --workspace - fi - - NEW_VERSION="$({})" - - echo "new_version=${{NEW_VERSION}}" >> "$GITHUB_OUTPUT" - "# - }, - current_version, bump_type, VERSION_CHECK - )) - .id("bump-version"); - - let new_version = StepOutput::new(&step, "new_version"); - (step, new_version) -} - -fn create_pull_request(new_version: StepOutput, generated_token: StepOutput) -> Step { - let formatted_version = format!("v{}", new_version); - - named::uses("peter-evans", "create-pull-request", "v7").with( - Input::default() - .add("title", format!("Bump version to {}", new_version)) - .add( - "body", - format!( - "This PR bumps the version of this extension to {}", - formatted_version - ), - ) - .add( - "commit-message", - format!("Bump version to {}", formatted_version), - ) - .add("branch", "zed-zippy-autobump") - .add( - "committer", - "zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>", - ) - .add("base", "main") - .add("delete-branch", true) - .add("token", generated_token.to_string()) - .add("sign-commits", true), - ) -} - -pub(crate) struct RepositoryTarget { - owner: String, - repositories: String, -} - -impl RepositoryTarget { - pub fn new(owner: T, repositories: &[&str]) -> Self { - Self { - owner: owner.to_string(), - repositories: repositories.join("\n"), - } - } -} diff --git a/tooling/xtask/src/tasks/workflows/extension_release.rs b/tooling/xtask/src/tasks/workflows/extension_release.rs deleted file mode 100644 index c55fed0cb8..0000000000 --- a/tooling/xtask/src/tasks/workflows/extension_release.rs +++ /dev/null @@ -1,72 +0,0 @@ -use gh_workflow::{Event, Job, Run, Step, Use, Workflow, WorkflowCall}; -use indoc::indoc; - -use crate::tasks::workflows::{ - extension_bump::{RepositoryTarget, generate_token}, - runners, - steps::{CommonJobConditions, NamedJob, checkout_repo, named}, - vars::{StepOutput, WorkflowSecret}, -}; - -pub(crate) fn extension_release() -> Workflow { - let (app_id, app_secret) = extension_workflow_secrets(); - - let create_release = create_release(&app_id, &app_secret); - named::workflow() - .on( - Event::default().workflow_call(WorkflowCall::default().secrets([ - (app_id.name.to_owned(), app_id.secret_configuration()), - ( - app_secret.name.to_owned(), - app_secret.secret_configuration(), - ), - ])), - ) - .add_job(create_release.name, create_release.job) -} - -fn create_release(app_id: &WorkflowSecret, app_secret: &WorkflowSecret) -> NamedJob { - let extension_registry = RepositoryTarget::new("zed-industries", &["extensions"]); - let (generate_token, generated_token) = - generate_token(&app_id, &app_secret, Some(extension_registry)); - let (get_extension_id, extension_id) = get_extension_id(); - - let job = Job::default() - .with_repository_owner_guard() - .runs_on(runners::LINUX_LARGE) - .add_step(generate_token) - .add_step(checkout_repo()) - .add_step(get_extension_id) - .add_step(release_action(extension_id, generated_token)); - - named::job(job) -} - -fn get_extension_id() -> (Step, StepOutput) { - let step = named::bash(indoc! { - r#" - EXTENSION_ID="$(sed -n 's/id = \"\(.*\)\"/\1/p' < extension.toml)" - - echo "extension_id=${EXTENSION_ID}" >> "$GITHUB_OUTPUT" - "#}) - .id("get-extension-id"); - - let extension_id = StepOutput::new(&step, "extension_id"); - - (step, extension_id) -} - -fn release_action(extension_id: StepOutput, generated_token: StepOutput) -> Step { - named::uses("huacnlee", "zed-extension-action", "v2") - .add_with(("extension-name", extension_id.to_string())) - .add_with(("push-to", "zed-industries/extensions")) - .add_env(("COMMITTER_TOKEN", generated_token.to_string())) -} - -pub(crate) fn extension_workflow_secrets() -> (WorkflowSecret, WorkflowSecret) { - let app_id = WorkflowSecret::new("app-id", "The app ID used to create the PR"); - let app_secret = - WorkflowSecret::new("app-secret", "The app secret for the corresponding app ID"); - - (app_id, app_secret) -} diff --git a/tooling/xtask/src/tasks/workflows/extension_tests.rs b/tooling/xtask/src/tasks/workflows/extension_tests.rs deleted file mode 100644 index 4805591214..0000000000 --- a/tooling/xtask/src/tasks/workflows/extension_tests.rs +++ /dev/null @@ -1,114 +0,0 @@ -use gh_workflow::*; -use indoc::indoc; - -use crate::tasks::workflows::{ - run_tests::{orchestrate, tests_pass}, - runners, - steps::{self, CommonJobConditions, FluentBuilder, NamedJob, named}, - vars::{PathCondition, StepOutput, one_workflow_per_non_main_branch}, -}; - -pub(crate) const ZED_EXTENSION_CLI_SHA: &str = "7cfce605704d41ca247e3f84804bf323f6c6caaf"; - -// This is used by various extensions repos in the zed-extensions org to run automated tests. -pub(crate) fn extension_tests() -> Workflow { - let should_check_rust = PathCondition::new("check_rust", r"^(Cargo.lock|Cargo.toml|.*\.rs)$"); - let should_check_extension = PathCondition::new("check_extension", r"^.*\.scm$"); - - let orchestrate = orchestrate(&[&should_check_rust, &should_check_extension]); - - let jobs = [ - orchestrate, - should_check_rust.guard(check_rust()), - should_check_extension.guard(check_extension()), - ]; - - let tests_pass = tests_pass(&jobs); - - named::workflow() - .add_event(Event::default().workflow_call(WorkflowCall::default())) - .concurrency(one_workflow_per_non_main_branch()) - .add_env(("CARGO_TERM_COLOR", "always")) - .add_env(("RUST_BACKTRACE", 1)) - .add_env(("CARGO_INCREMENTAL", 0)) - .add_env(("ZED_EXTENSION_CLI_SHA", ZED_EXTENSION_CLI_SHA)) - .map(|workflow| { - jobs.into_iter() - .chain([tests_pass]) - .fold(workflow, |workflow, job| { - workflow.add_job(job.name, job.job) - }) - }) -} - -fn run_clippy() -> Step { - named::bash("cargo clippy --release --all-targets --all-features -- --deny warnings") -} - -fn check_rust() -> NamedJob { - let job = Job::default() - .with_repository_owner_guard() - .runs_on(runners::LINUX_DEFAULT) - .timeout_minutes(3u32) - .add_step(steps::checkout_repo()) - .add_step(steps::cache_rust_dependencies_namespace()) - .add_step(steps::cargo_fmt()) - .add_step(run_clippy()) - .add_step(steps::cargo_install_nextest()) - .add_step( - steps::cargo_nextest(runners::Platform::Linux).add_env(("NEXTEST_NO_TESTS", "warn")), - ); - - named::job(job) -} - -pub(crate) fn check_extension() -> NamedJob { - let (cache_download, cache_hit) = cache_zed_extension_cli(); - let job = Job::default() - .with_repository_owner_guard() - .runs_on(runners::LINUX_SMALL) - .timeout_minutes(2u32) - .add_step(steps::checkout_repo()) - .add_step(cache_download) - .add_step(download_zed_extension_cli(cache_hit)) - .add_step(check()); - - named::job(job) -} - -pub fn cache_zed_extension_cli() -> (Step, StepOutput) { - let step = named::uses( - "actions", - "cache", - "0057852bfaa89a56745cba8c7296529d2fc39830", - ) - .id("cache-zed-extension-cli") - .with( - Input::default() - .add("path", "zed-extension") - .add("key", "zed-extension-${{ env.ZED_EXTENSION_CLI_SHA }}"), - ); - let output = StepOutput::new(&step, "cache-hit"); - (step, output) -} - -pub fn download_zed_extension_cli(cache_hit: StepOutput) -> Step { - named::bash( - indoc! { - r#" - wget --quiet "https://zed-extension-cli.nyc3.digitaloceanspaces.com/$ZED_EXTENSION_CLI_SHA/x86_64-unknown-linux-gnu/zed-extension" - chmod +x zed-extension - "#, - } - ).if_condition(Expression::new(format!("{} != 'true'", cache_hit.expr()))) -} - -pub fn check() -> Step { - named::bash(indoc! { - r#" - mkdir -p /tmp/ext-scratch - mkdir -p /tmp/ext-output - ./zed-extension --source-dir . --scratch-dir /tmp/ext-scratch --output-dir /tmp/ext-output - "# - }) -} diff --git a/tooling/xtask/src/tasks/workflows/extensions/bump_version.rs b/tooling/xtask/src/tasks/workflows/extensions/bump_version.rs deleted file mode 100644 index 1564fef448..0000000000 --- a/tooling/xtask/src/tasks/workflows/extensions/bump_version.rs +++ /dev/null @@ -1,103 +0,0 @@ -use gh_workflow::{ - Event, Expression, Input, Job, PullRequest, PullRequestType, Push, Run, Step, UsesJob, - Workflow, WorkflowDispatch, -}; -use indexmap::IndexMap; -use indoc::indoc; - -use crate::tasks::workflows::{ - runners, - steps::{NamedJob, named}, - vars::{self, JobOutput, StepOutput, one_workflow_per_non_main_branch_and_token}, -}; - -pub(crate) fn bump_version() -> Workflow { - let (determine_bump_type, bump_type) = determine_bump_type(); - let bump_type = bump_type.as_job_output(&determine_bump_type); - - let call_bump_version = call_bump_version(&determine_bump_type, bump_type); - - named::workflow() - .on(Event::default() - .push( - Push::default() - .add_branch("main") - .add_ignored_path(".github/**"), - ) - .pull_request(PullRequest::default().add_type(PullRequestType::Labeled)) - .workflow_dispatch(WorkflowDispatch::default())) - .concurrency(one_workflow_per_non_main_branch_and_token("labels")) - .add_job(determine_bump_type.name, determine_bump_type.job) - .add_job(call_bump_version.name, call_bump_version.job) -} - -pub(crate) fn call_bump_version( - depending_job: &NamedJob, - bump_type: JobOutput, -) -> NamedJob { - let job = Job::default() - .cond(Expression::new(format!( - "github.event.action != 'labeled' || {} != 'patch'", - bump_type.expr() - ))) - .uses( - "zed-industries", - "zed", - ".github/workflows/extension_bump.yml", - "main", - ) - .add_need(depending_job.name.clone()) - .with( - Input::default() - .add("bump-type", bump_type.to_string()) - .add("force-bump", true), - ) - .secrets(IndexMap::from([ - ("app-id".to_owned(), vars::ZED_ZIPPY_APP_ID.to_owned()), - ( - "app-secret".to_owned(), - vars::ZED_ZIPPY_APP_PRIVATE_KEY.to_owned(), - ), - ])); - - named::job(job) -} - -fn determine_bump_type() -> (NamedJob, StepOutput) { - let (get_bump_type, output) = get_bump_type(); - let job = Job::default() - .runs_on(runners::LINUX_DEFAULT) - .add_step(get_bump_type) - .outputs([(output.name.to_owned(), output.to_string())]); - (named::job(job), output) -} - -fn get_bump_type() -> (Step, StepOutput) { - let step = named::bash( - indoc! {r#" - if [ "$HAS_MAJOR_LABEL" = "true" ]; then - bump_type="major" - elif [ "$HAS_MINOR_LABEL" = "true" ]; then - bump_type="minor" - else - bump_type="patch" - fi - echo "bump_type=$bump_type" >> $GITHUB_OUTPUT - "#}, - ) - .add_env(("HAS_MAJOR_LABEL", - indoc!{ - "${{ (github.event.action == 'labeled' && github.event.label.name == 'major') || - (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'major')) }}" - })) - .add_env(("HAS_MINOR_LABEL", - indoc!{ - "${{ (github.event.action == 'labeled' && github.event.label.name == 'minor') || - (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'minor')) }}" - })) - .id("get-bump-type"); - - let step_output = StepOutput::new(&step, "bump_type"); - - (step, step_output) -} diff --git a/tooling/xtask/src/tasks/workflows/extensions/mod.rs b/tooling/xtask/src/tasks/workflows/extensions/mod.rs deleted file mode 100644 index d26100f09d..0000000000 --- a/tooling/xtask/src/tasks/workflows/extensions/mod.rs +++ /dev/null @@ -1,24 +0,0 @@ -use gh_workflow::{Job, UsesJob}; -use indexmap::IndexMap; - -use crate::tasks::workflows::vars; - -pub(crate) mod bump_version; -pub(crate) mod release_version; -pub(crate) mod run_tests; - -pub(crate) trait WithAppSecrets: Sized { - fn with_app_secrets(self) -> Self; -} - -impl WithAppSecrets for Job { - fn with_app_secrets(self) -> Self { - self.secrets(IndexMap::from([ - ("app-id".to_owned(), vars::ZED_ZIPPY_APP_ID.to_owned()), - ( - "app-secret".to_owned(), - vars::ZED_ZIPPY_APP_PRIVATE_KEY.to_owned(), - ), - ])) - } -} diff --git a/tooling/xtask/src/tasks/workflows/extensions/release_version.rs b/tooling/xtask/src/tasks/workflows/extensions/release_version.rs deleted file mode 100644 index ebeb6959a9..0000000000 --- a/tooling/xtask/src/tasks/workflows/extensions/release_version.rs +++ /dev/null @@ -1,26 +0,0 @@ -use gh_workflow::{Event, Job, Push, UsesJob, Workflow}; - -use crate::tasks::workflows::{ - extensions::WithAppSecrets, - steps::{NamedJob, named}, -}; - -pub(crate) fn release_version() -> Workflow { - let create_release = call_release_version(); - named::workflow() - .on(Event::default().push(Push::default().add_tag("v**"))) - .add_job(create_release.name, create_release.job) -} - -pub(crate) fn call_release_version() -> NamedJob { - let job = Job::default() - .uses( - "zed-industries", - "zed", - ".github/workflows/extension_release.yml", - "main", - ) - .with_app_secrets(); - - named::job(job) -} diff --git a/tooling/xtask/src/tasks/workflows/extensions/run_tests.rs b/tooling/xtask/src/tasks/workflows/extensions/run_tests.rs deleted file mode 100644 index 885a8fd09f..0000000000 --- a/tooling/xtask/src/tasks/workflows/extensions/run_tests.rs +++ /dev/null @@ -1,27 +0,0 @@ -use gh_workflow::{Event, Job, PullRequest, Push, UsesJob, Workflow}; - -use crate::tasks::workflows::{ - steps::{NamedJob, named}, - vars::one_workflow_per_non_main_branch_and_token, -}; - -pub(crate) fn run_tests() -> Workflow { - let call_extension_tests = call_extension_tests(); - named::workflow() - .on(Event::default() - .pull_request(PullRequest::default().add_branch("**")) - .push(Push::default().add_branch("main"))) - .concurrency(one_workflow_per_non_main_branch_and_token("pr")) - .add_job(call_extension_tests.name, call_extension_tests.job) -} - -pub(crate) fn call_extension_tests() -> NamedJob { - let job = Job::default().uses( - "zed-industries", - "zed", - ".github/workflows/extension_tests.yml", - "main", - ); - - named::job(job) -} diff --git a/tooling/xtask/src/tasks/workflows/nix_build.rs b/tooling/xtask/src/tasks/workflows/nix_build.rs deleted file mode 100644 index ff98852d19..0000000000 --- a/tooling/xtask/src/tasks/workflows/nix_build.rs +++ /dev/null @@ -1,104 +0,0 @@ -use crate::tasks::workflows::{ - runners::{Arch, Platform}, - steps::{CommonJobConditions, NamedJob}, -}; - -use super::{runners, steps, steps::named, vars}; -use gh_workflow::*; -use indoc::indoc; - -pub(crate) fn build_nix( - platform: Platform, - arch: Arch, - flake_output: &str, - cachix_filter: Option<&str>, - deps: &[&NamedJob], -) -> NamedJob { - // on our macs we manually install nix. for some reason the cachix action is running - // under a non-login /bin/bash shell which doesn't source the proper script to add the - // nix profile to PATH, so we manually add them here - pub fn set_path() -> Step { - named::bash(indoc! {r#" - echo "/nix/var/nix/profiles/default/bin" >> "$GITHUB_PATH" - echo "/Users/administrator/.nix-profile/bin" >> "$GITHUB_PATH" - "#}) - } - - pub fn install_nix() -> Step { - named::uses( - "cachix", - "install-nix-action", - "02a151ada4993995686f9ed4f1be7cfbb229e56f", // v31 - ) - .add_with(("github_access_token", vars::GITHUB_TOKEN)) - } - - pub fn cachix_action(cachix_filter: Option<&str>) -> Step { - let mut step = named::uses( - "cachix", - "cachix-action", - "0fc020193b5a1fa3ac4575aa3a7d3aa6a35435ad", // v16 - ) - .add_with(("name", "zed")) - .add_with(("authToken", vars::CACHIX_AUTH_TOKEN)) - .add_with(("cachixArgs", "-v")); - if let Some(cachix_filter) = cachix_filter { - step = step.add_with(("pushFilter", cachix_filter)); - } - step - } - - pub fn build(flake_output: &str) -> Step { - named::bash(&format!( - "nix build .#{} -L --accept-flake-config", - flake_output - )) - } - - pub fn limit_store() -> Step { - named::bash(indoc! {r#" - if [ "$(du -sm /nix/store | cut -f1)" -gt 50000 ]; then - nix-collect-garbage -d || true - fi"# - }) - } - - let runner = match platform { - Platform::Windows => unimplemented!(), - Platform::Linux => runners::LINUX_X86_BUNDLER, - Platform::Mac => runners::MAC_DEFAULT, - }; - let mut job = Job::default() - .timeout_minutes(60u32) - .continue_on_error(true) - .with_repository_owner_guard() - .runs_on(runner) - .add_env(("ZED_CLIENT_CHECKSUM_SEED", vars::ZED_CLIENT_CHECKSUM_SEED)) - .add_env(("ZED_MINIDUMP_ENDPOINT", vars::ZED_SENTRY_MINIDUMP_ENDPOINT)) - .add_env(( - "ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON", - vars::ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON, - )) - .add_env(("GIT_LFS_SKIP_SMUDGE", "1")) // breaks the livekit rust sdk examples which we don't actually depend on - .add_step(steps::checkout_repo()); - - if deps.len() > 0 { - job = job.needs(deps.iter().map(|d| d.name.clone()).collect::>()); - } - - job = if platform == Platform::Linux { - job.add_step(install_nix()) - .add_step(cachix_action(cachix_filter)) - .add_step(build(&flake_output)) - } else { - job.add_step(set_path()) - .add_step(cachix_action(cachix_filter)) - .add_step(build(&flake_output)) - .add_step(limit_store()) - }; - - NamedJob { - name: format!("build_nix_{platform}_{arch}"), - job, - } -} diff --git a/tooling/xtask/src/tasks/workflows/release.rs b/tooling/xtask/src/tasks/workflows/release.rs deleted file mode 100644 index e06a713401..0000000000 --- a/tooling/xtask/src/tasks/workflows/release.rs +++ /dev/null @@ -1,195 +0,0 @@ -use gh_workflow::{Event, Expression, Push, Run, Step, Use, Workflow}; - -use crate::tasks::workflows::{ - run_bundling::{bundle_linux, bundle_mac, bundle_windows}, - run_tests, - runners::{self, Arch}, - steps::{self, FluentBuilder, NamedJob, dependant_job, named, release_job}, - vars::{self, assets}, -}; - -pub(crate) fn release() -> Workflow { - let macos_tests = run_tests::run_platform_tests(runners::Platform::Mac); - let linux_tests = run_tests::run_platform_tests(runners::Platform::Linux); - let windows_tests = run_tests::run_platform_tests(runners::Platform::Windows); - let check_scripts = run_tests::check_scripts(); - - let create_draft_release = create_draft_release(); - - let bundle = ReleaseBundleJobs { - linux_aarch64: bundle_linux(Arch::AARCH64, None, &[&linux_tests, &check_scripts]), - linux_x86_64: bundle_linux(Arch::X86_64, None, &[&linux_tests, &check_scripts]), - mac_aarch64: bundle_mac(Arch::AARCH64, None, &[&macos_tests, &check_scripts]), - mac_x86_64: bundle_mac(Arch::X86_64, None, &[&macos_tests, &check_scripts]), - windows_aarch64: bundle_windows(Arch::AARCH64, None, &[&windows_tests, &check_scripts]), - windows_x86_64: bundle_windows(Arch::X86_64, None, &[&windows_tests, &check_scripts]), - }; - - let upload_release_assets = upload_release_assets(&[&create_draft_release], &bundle); - - let auto_release_preview = auto_release_preview(&[&upload_release_assets]); - let notify_on_failure = notify_on_failure(&[&upload_release_assets, &auto_release_preview]); - - named::workflow() - .on(Event::default().push(Push::default().tags(vec!["v*".to_string()]))) - .concurrency(vars::one_workflow_per_non_main_branch()) - .add_env(("CARGO_TERM_COLOR", "always")) - .add_env(("RUST_BACKTRACE", "1")) - .add_job(macos_tests.name, macos_tests.job) - .add_job(linux_tests.name, linux_tests.job) - .add_job(windows_tests.name, windows_tests.job) - .add_job(check_scripts.name, check_scripts.job) - .add_job(create_draft_release.name, create_draft_release.job) - .map(|mut workflow| { - for job in bundle.into_jobs() { - workflow = workflow.add_job(job.name, job.job); - } - workflow - }) - .add_job(upload_release_assets.name, upload_release_assets.job) - .add_job(auto_release_preview.name, auto_release_preview.job) - .add_job(notify_on_failure.name, notify_on_failure.job) -} - -pub(crate) struct ReleaseBundleJobs { - pub linux_aarch64: NamedJob, - pub linux_x86_64: NamedJob, - pub mac_aarch64: NamedJob, - pub mac_x86_64: NamedJob, - pub windows_aarch64: NamedJob, - pub windows_x86_64: NamedJob, -} - -impl ReleaseBundleJobs { - pub fn jobs(&self) -> Vec<&NamedJob> { - vec![ - &self.linux_aarch64, - &self.linux_x86_64, - &self.mac_aarch64, - &self.mac_x86_64, - &self.windows_aarch64, - &self.windows_x86_64, - ] - } - - pub fn into_jobs(self) -> Vec { - vec![ - self.linux_aarch64, - self.linux_x86_64, - self.mac_aarch64, - self.mac_x86_64, - self.windows_aarch64, - self.windows_x86_64, - ] - } -} - -pub(crate) fn create_sentry_release() -> Step { - named::uses( - "getsentry", - "action-release", - "526942b68292201ac6bbb99b9a0747d4abee354c", // v3 - ) - .add_env(("SENTRY_ORG", "zed-dev")) - .add_env(("SENTRY_PROJECT", "zed")) - .add_env(("SENTRY_AUTH_TOKEN", vars::SENTRY_AUTH_TOKEN)) - .add_with(("environment", "production")) -} - -fn auto_release_preview(deps: &[&NamedJob; 1]) -> NamedJob { - named::job( - dependant_job(deps) - .runs_on(runners::LINUX_SMALL) - .cond(Expression::new(indoc::indoc!( - r#"startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-pre') && !endsWith(github.ref, '.0-pre')"# - ))) - .add_step( - steps::script( - r#"gh release edit "$GITHUB_REF_NAME" --repo=zed-industries/zed --draft=false"#, - ) - .add_env(("GITHUB_TOKEN", vars::GITHUB_TOKEN)), - ) - ) -} - -pub(crate) fn download_workflow_artifacts() -> Step { - named::uses( - "actions", - "download-artifact", - "018cc2cf5baa6db3ef3c5f8a56943fffe632ef53", // v6.0.0 - ) - .add_with(("path", "./artifacts/")) -} - -pub(crate) fn prep_release_artifacts() -> Step { - let mut script_lines = vec!["mkdir -p release-artifacts/\n".to_string()]; - for asset in assets::all() { - let mv_command = format!("mv ./artifacts/{asset}/{asset} release-artifacts/{asset}"); - script_lines.push(mv_command) - } - - named::bash(&script_lines.join("\n")) -} - -fn upload_release_assets(deps: &[&NamedJob], bundle: &ReleaseBundleJobs) -> NamedJob { - let mut deps = deps.to_vec(); - deps.extend(bundle.jobs()); - - named::job( - dependant_job(&deps) - .runs_on(runners::LINUX_MEDIUM) - .add_step(download_workflow_artifacts()) - .add_step(steps::script("ls -lR ./artifacts")) - .add_step(prep_release_artifacts()) - .add_step( - steps::script("gh release upload \"$GITHUB_REF_NAME\" --repo=zed-industries/zed release-artifacts/*") - .add_env(("GITHUB_TOKEN", vars::GITHUB_TOKEN)), - ), - ) -} - -fn create_draft_release() -> NamedJob { - fn generate_release_notes() -> Step { - named::bash( - r#"node --redirect-warnings=/dev/null ./script/draft-release-notes "$RELEASE_VERSION" "$RELEASE_CHANNEL" > target/release-notes.md"#, - ) - } - - fn create_release() -> Step { - named::bash("script/create-draft-release target/release-notes.md") - .add_env(("GITHUB_TOKEN", vars::GITHUB_TOKEN)) - } - - named::job( - release_job(&[]) - .runs_on(runners::LINUX_SMALL) - // We need to fetch more than one commit so that `script/draft-release-notes` - // is able to diff between the current and previous tag. - // - // 25 was chosen arbitrarily. - .add_step( - steps::checkout_repo() - .add_with(("fetch-depth", 25)) - .add_with(("clean", false)) - .add_with(("ref", "${{ github.ref }}")), - ) - .add_step(steps::script("script/determine-release-channel")) - .add_step(steps::script("mkdir -p target/")) - .add_step(generate_release_notes()) - .add_step(create_release()), - ) -} - -pub(crate) fn notify_on_failure(deps: &[&NamedJob]) -> NamedJob { - fn notify_slack() -> Step { - named::bash( - "curl -X POST -H 'Content-type: application/json'\\\n --data '{\"text\":\"${{ github.workflow }} failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}' \"$SLACK_WEBHOOK\"" - ).add_env(("SLACK_WEBHOOK", vars::SLACK_WEBHOOK_WORKFLOW_FAILURES)) - } - - let job = dependant_job(deps) - .runs_on(runners::LINUX_SMALL) - .cond(Expression::new("failure()")) - .add_step(notify_slack()); - named::job(job) -} diff --git a/tooling/xtask/src/tasks/workflows/release_nightly.rs b/tooling/xtask/src/tasks/workflows/release_nightly.rs deleted file mode 100644 index 73cdbe3f3e..0000000000 --- a/tooling/xtask/src/tasks/workflows/release_nightly.rs +++ /dev/null @@ -1,131 +0,0 @@ -use crate::tasks::workflows::{ - nix_build::build_nix, - release::{ - ReleaseBundleJobs, create_sentry_release, download_workflow_artifacts, notify_on_failure, - prep_release_artifacts, - }, - run_bundling::{bundle_linux, bundle_mac, bundle_windows}, - run_tests::run_platform_tests, - runners::{Arch, Platform, ReleaseChannel}, - steps::{CommonJobConditions, FluentBuilder, NamedJob}, -}; - -use super::{runners, steps, steps::named, vars}; -use gh_workflow::*; - -/// Generates the release_nightly.yml workflow -pub fn release_nightly() -> Workflow { - let style = check_style(); - // run only on windows as that's our fastest platform right now. - let tests = run_platform_tests(Platform::Windows); - let nightly = Some(ReleaseChannel::Nightly); - - let bundle = ReleaseBundleJobs { - linux_aarch64: bundle_linux(Arch::AARCH64, nightly, &[&style, &tests]), - linux_x86_64: bundle_linux(Arch::X86_64, nightly, &[&style, &tests]), - mac_aarch64: bundle_mac(Arch::AARCH64, nightly, &[&style, &tests]), - mac_x86_64: bundle_mac(Arch::X86_64, nightly, &[&style, &tests]), - windows_aarch64: bundle_windows(Arch::AARCH64, nightly, &[&style, &tests]), - windows_x86_64: bundle_windows(Arch::X86_64, nightly, &[&style, &tests]), - }; - - let nix_linux_x86 = build_nix( - Platform::Linux, - Arch::X86_64, - "default", - None, - &[&style, &tests], - ); - let nix_mac_arm = build_nix( - Platform::Mac, - Arch::AARCH64, - "default", - None, - &[&style, &tests], - ); - let update_nightly_tag = update_nightly_tag_job(&bundle); - let notify_on_failure = notify_on_failure(&bundle.jobs()); - - named::workflow() - .on(Event::default() - // Fire every day at 7:00am UTC (Roughly before EU workday and after US workday) - .schedule([Schedule::new("0 7 * * *")]) - .push(Push::default().add_tag("nightly"))) - .add_env(("CARGO_TERM_COLOR", "always")) - .add_env(("RUST_BACKTRACE", "1")) - .add_job(style.name, style.job) - .add_job(tests.name, tests.job) - .map(|mut workflow| { - for job in bundle.into_jobs() { - workflow = workflow.add_job(job.name, job.job); - } - workflow - }) - .add_job(nix_linux_x86.name, nix_linux_x86.job) - .add_job(nix_mac_arm.name, nix_mac_arm.job) - .add_job(update_nightly_tag.name, update_nightly_tag.job) - .add_job(notify_on_failure.name, notify_on_failure.job) -} - -fn check_style() -> NamedJob { - let job = release_job(&[]) - .runs_on(runners::MAC_DEFAULT) - .add_step( - steps::checkout_repo() - .add_with(("clean", false)) - .add_with(("fetch-depth", 0)), - ) - .add_step(steps::cargo_fmt()) - .add_step(steps::script("./script/clippy")); - - named::job(job) -} - -fn release_job(deps: &[&NamedJob]) -> Job { - let job = Job::default() - .with_repository_owner_guard() - .timeout_minutes(60u32); - if deps.len() > 0 { - job.needs(deps.iter().map(|j| j.name.clone()).collect::>()) - } else { - job - } -} - -fn update_nightly_tag_job(bundle: &ReleaseBundleJobs) -> NamedJob { - fn update_nightly_tag() -> Step { - named::bash(indoc::indoc! {r#" - if [ "$(git rev-parse nightly)" = "$(git rev-parse HEAD)" ]; then - echo "Nightly tag already points to current commit. Skipping tagging." - exit 0 - fi - git config user.name github-actions - git config user.email github-actions@github.com - git tag -f nightly - git push origin nightly --force - "#}) - } - - NamedJob { - name: "update_nightly_tag".to_owned(), - job: steps::release_job(&bundle.jobs()) - .runs_on(runners::LINUX_MEDIUM) - .add_step(steps::checkout_repo().add_with(("fetch-depth", 0))) - .add_step(download_workflow_artifacts()) - .add_step(steps::script("ls -lR ./artifacts")) - .add_step(prep_release_artifacts()) - .add_step( - steps::script("./script/upload-nightly") - .add_env(( - "DIGITALOCEAN_SPACES_ACCESS_KEY", - vars::DIGITALOCEAN_SPACES_ACCESS_KEY, - )) - .add_env(( - "DIGITALOCEAN_SPACES_SECRET_KEY", - vars::DIGITALOCEAN_SPACES_SECRET_KEY, - )), - ) - .add_step(update_nightly_tag()) - .add_step(create_sentry_release()), - } -} diff --git a/tooling/xtask/src/tasks/workflows/run_agent_evals.rs b/tooling/xtask/src/tasks/workflows/run_agent_evals.rs deleted file mode 100644 index 667ea6a90b..0000000000 --- a/tooling/xtask/src/tasks/workflows/run_agent_evals.rs +++ /dev/null @@ -1,165 +0,0 @@ -use gh_workflow::{ - Event, Expression, Job, Run, Schedule, Step, Strategy, Use, Workflow, WorkflowDispatch, -}; -use serde_json::json; - -use crate::tasks::workflows::{ - runners::{self, Platform}, - steps::{self, FluentBuilder as _, NamedJob, named, setup_cargo_config}, - vars::{self, WorkflowInput}, -}; - -pub(crate) fn run_agent_evals() -> Workflow { - let agent_evals = agent_evals(); - let model_name = WorkflowInput::string("model_name", None); - - named::workflow() - .on(Event::default().workflow_dispatch( - WorkflowDispatch::default().add_input(model_name.name, model_name.input()), - )) - .concurrency(vars::one_workflow_per_non_main_branch()) - .add_env(("CARGO_TERM_COLOR", "always")) - .add_env(("CARGO_INCREMENTAL", 0)) - .add_env(("RUST_BACKTRACE", 1)) - .add_env(("ANTHROPIC_API_KEY", vars::ANTHROPIC_API_KEY)) - .add_env(("OPENAI_API_KEY", vars::OPENAI_API_KEY)) - .add_env(("GOOGLE_AI_API_KEY", vars::GOOGLE_AI_API_KEY)) - .add_env(("GOOGLE_CLOUD_PROJECT", vars::GOOGLE_CLOUD_PROJECT)) - .add_env(("ZED_CLIENT_CHECKSUM_SEED", vars::ZED_CLIENT_CHECKSUM_SEED)) - .add_env(("ZED_EVAL_TELEMETRY", 1)) - .add_env(("MODEL_NAME", model_name.to_string())) - .add_job(agent_evals.name, agent_evals.job) -} - -pub(crate) fn run_unit_evals() -> Workflow { - let model_name = WorkflowInput::string("model_name", None); - let commit_sha = WorkflowInput::string("commit_sha", None); - - let unit_evals = named::job(unit_evals(Some(&commit_sha))); - - named::workflow() - .name("run_unit_evals") - .on(Event::default().workflow_dispatch( - WorkflowDispatch::default() - .add_input(model_name.name, model_name.input()) - .add_input(commit_sha.name, commit_sha.input()), - )) - .concurrency(vars::allow_concurrent_runs()) - .add_env(("CARGO_TERM_COLOR", "always")) - .add_env(("CARGO_INCREMENTAL", 0)) - .add_env(("RUST_BACKTRACE", 1)) - .add_env(("ZED_CLIENT_CHECKSUM_SEED", vars::ZED_CLIENT_CHECKSUM_SEED)) - .add_env(("ZED_EVAL_TELEMETRY", 1)) - .add_env(("MODEL_NAME", model_name.to_string())) - .add_job(unit_evals.name, unit_evals.job) -} - -fn add_api_keys(step: Step) -> Step { - step.add_env(("ANTHROPIC_API_KEY", vars::ANTHROPIC_API_KEY)) - .add_env(("OPENAI_API_KEY", vars::OPENAI_API_KEY)) - .add_env(("GOOGLE_AI_API_KEY", vars::GOOGLE_AI_API_KEY)) - .add_env(("GOOGLE_CLOUD_PROJECT", vars::GOOGLE_CLOUD_PROJECT)) -} - -fn agent_evals() -> NamedJob { - fn run_eval() -> Step { - named::bash( - "cargo run --package=eval -- --repetitions=8 --concurrency=1 --model \"${MODEL_NAME}\"", - ) - } - - named::job( - Job::default() - .runs_on(runners::LINUX_DEFAULT) - .timeout_minutes(60_u32 * 10) - .add_step(steps::checkout_repo()) - .add_step(steps::cache_rust_dependencies_namespace()) - .map(steps::install_linux_dependencies) - .add_step(setup_cargo_config(Platform::Linux)) - .add_step(steps::script("cargo build --package=eval")) - .add_step(add_api_keys(run_eval())) - .add_step(steps::cleanup_cargo_config(Platform::Linux)), - ) -} - -pub(crate) fn run_cron_unit_evals() -> Workflow { - let unit_evals = cron_unit_evals(); - - named::workflow() - .name("run_cron_unit_evals") - .on(Event::default() - .schedule([ - // GitHub might drop jobs at busy times, so we choose a random time in the middle of the night. - Schedule::default().cron("47 1 * * 2"), - ]) - .workflow_dispatch(WorkflowDispatch::default())) - .concurrency(vars::one_workflow_per_non_main_branch()) - .add_env(("CARGO_TERM_COLOR", "always")) - .add_env(("CARGO_INCREMENTAL", 0)) - .add_env(("RUST_BACKTRACE", 1)) - .add_env(("ZED_CLIENT_CHECKSUM_SEED", vars::ZED_CLIENT_CHECKSUM_SEED)) - .add_job(unit_evals.name, unit_evals.job) -} - -fn cron_unit_evals() -> NamedJob { - fn send_failure_to_slack() -> Step { - named::uses( - "slackapi", - "slack-github-action", - "b0fa283ad8fea605de13dc3f449259339835fc52", - ) - .if_condition(Expression::new("${{ failure() }}")) - .add_with(("method", "chat.postMessage")) - .add_with(("token", vars::SLACK_APP_ZED_UNIT_EVALS_BOT_TOKEN)) - .add_with(("payload", indoc::indoc!{r#" - channel: C04UDRNNJFQ - text: "Unit Evals Failed: https://github.com/zed-industries/zed/actions/runs/${{ github.run_id }}" - "#})) - } - - named::job(cron_unit_evals_job().add_step(send_failure_to_slack())) -} - -const UNIT_EVAL_MODELS: &[&str] = &[ - "anthropic/claude-sonnet-4-5-latest", - "anthropic/claude-opus-4-5-latest", - "google/gemini-3-pro", - "openai/gpt-5", -]; - -fn cron_unit_evals_job() -> Job { - let script_step = add_api_keys(steps::script("./script/run-unit-evals")) - .add_env(("ZED_AGENT_MODEL", "${{ matrix.model }}")); - - Job::default() - .runs_on(runners::LINUX_DEFAULT) - .strategy(Strategy::default().fail_fast(false).matrix(json!({ - "model": UNIT_EVAL_MODELS - }))) - .add_step(steps::checkout_repo()) - .add_step(steps::setup_cargo_config(Platform::Linux)) - .add_step(steps::cache_rust_dependencies_namespace()) - .map(steps::install_linux_dependencies) - .add_step(steps::cargo_install_nextest()) - .add_step(steps::clear_target_dir_if_large(Platform::Linux)) - .add_step(script_step) - .add_step(steps::cleanup_cargo_config(Platform::Linux)) -} - -fn unit_evals(commit: Option<&WorkflowInput>) -> Job { - let script_step = add_api_keys(steps::script("./script/run-unit-evals")); - - Job::default() - .runs_on(runners::LINUX_DEFAULT) - .add_step(steps::checkout_repo()) - .add_step(steps::setup_cargo_config(Platform::Linux)) - .add_step(steps::cache_rust_dependencies_namespace()) - .map(steps::install_linux_dependencies) - .add_step(steps::cargo_install_nextest()) - .add_step(steps::clear_target_dir_if_large(Platform::Linux)) - .add_step(match commit { - Some(commit) => script_step.add_env(("UNIT_EVAL_COMMIT", commit)), - None => script_step, - }) - .add_step(steps::cleanup_cargo_config(Platform::Linux)) -} diff --git a/tooling/xtask/src/tasks/workflows/run_bundling.rs b/tooling/xtask/src/tasks/workflows/run_bundling.rs deleted file mode 100644 index a0793ffb68..0000000000 --- a/tooling/xtask/src/tasks/workflows/run_bundling.rs +++ /dev/null @@ -1,195 +0,0 @@ -use std::path::Path; - -use crate::tasks::workflows::{ - release::ReleaseBundleJobs, - runners::{Arch, Platform, ReleaseChannel}, - steps::{FluentBuilder, NamedJob, dependant_job, named}, - vars::{assets, bundle_envs}, -}; - -use super::{runners, steps}; -use gh_workflow::*; -use indoc::indoc; - -pub fn run_bundling() -> Workflow { - let bundle = ReleaseBundleJobs { - linux_aarch64: bundle_linux(Arch::AARCH64, None, &[]), - linux_x86_64: bundle_linux(Arch::X86_64, None, &[]), - mac_aarch64: bundle_mac(Arch::AARCH64, None, &[]), - mac_x86_64: bundle_mac(Arch::X86_64, None, &[]), - windows_aarch64: bundle_windows(Arch::AARCH64, None, &[]), - windows_x86_64: bundle_windows(Arch::X86_64, None, &[]), - }; - named::workflow() - .on(Event::default().pull_request( - PullRequest::default().types([PullRequestType::Labeled, PullRequestType::Synchronize]), - )) - .concurrency( - Concurrency::new(Expression::new( - "${{ github.workflow }}-${{ github.head_ref || github.ref }}", - )) - .cancel_in_progress(true), - ) - .add_env(("CARGO_TERM_COLOR", "always")) - .add_env(("RUST_BACKTRACE", "1")) - .map(|mut workflow| { - for job in bundle.into_jobs() { - workflow = workflow.add_job(job.name, job.job); - } - workflow - }) -} - -fn bundle_job(deps: &[&NamedJob]) -> Job { - dependant_job(deps) - .when(deps.len() == 0, |job| - job.cond(Expression::new( - indoc! { - r#"(github.event.action == 'labeled' && github.event.label.name == 'run-bundling') || - (github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))"#, - }))) - .timeout_minutes(60u32) -} - -pub(crate) fn bundle_mac( - arch: Arch, - release_channel: Option, - deps: &[&NamedJob], -) -> NamedJob { - pub fn bundle_mac(arch: Arch) -> Step { - named::bash(&format!("./script/bundle-mac {arch}-apple-darwin")) - } - let platform = Platform::Mac; - let artifact_name = match arch { - Arch::X86_64 => assets::MAC_X86_64, - Arch::AARCH64 => assets::MAC_AARCH64, - }; - let remote_server_artifact_name = match arch { - Arch::X86_64 => assets::REMOTE_SERVER_MAC_X86_64, - Arch::AARCH64 => assets::REMOTE_SERVER_MAC_AARCH64, - }; - NamedJob { - name: format!("bundle_mac_{arch}"), - job: bundle_job(deps) - .runs_on(runners::MAC_DEFAULT) - .envs(bundle_envs(platform)) - .add_step(steps::checkout_repo()) - .when_some(release_channel, |job, release_channel| { - job.add_step(set_release_channel(platform, release_channel)) - }) - .add_step(steps::setup_node()) - .add_step(steps::setup_sentry()) - .add_step(steps::clear_target_dir_if_large(runners::Platform::Mac)) - .add_step(bundle_mac(arch)) - .add_step(upload_artifact(&format!( - "target/{arch}-apple-darwin/release/{artifact_name}" - ))) - .add_step(upload_artifact(&format!( - "target/{remote_server_artifact_name}" - ))), - } -} - -pub fn upload_artifact(path: &str) -> Step { - let name = Path::new(path).file_name().unwrap().to_str().unwrap(); - Step::new(format!("@actions/upload-artifact {}", name)) - .uses( - "actions", - "upload-artifact", - "330a01c490aca151604b8cf639adc76d48f6c5d4", // v5 - ) - // N.B. "name" is the name for the asset. The uploaded - // file retains its filename. - .add_with(("name", name)) - .add_with(("path", path)) - .add_with(("if-no-files-found", "error")) -} - -pub(crate) fn bundle_linux( - arch: Arch, - release_channel: Option, - deps: &[&NamedJob], -) -> NamedJob { - let platform = Platform::Linux; - let artifact_name = match arch { - Arch::X86_64 => assets::LINUX_X86_64, - Arch::AARCH64 => assets::LINUX_AARCH64, - }; - let remote_server_artifact_name = match arch { - Arch::X86_64 => assets::REMOTE_SERVER_LINUX_X86_64, - Arch::AARCH64 => assets::REMOTE_SERVER_LINUX_AARCH64, - }; - NamedJob { - name: format!("bundle_linux_{arch}"), - job: bundle_job(deps) - .runs_on(arch.linux_bundler()) - .envs(bundle_envs(platform)) - .add_step(steps::checkout_repo()) - .when_some(release_channel, |job, release_channel| { - job.add_step(set_release_channel(platform, release_channel)) - }) - .add_step(steps::setup_sentry()) - .map(steps::install_linux_dependencies) - .add_step(steps::script("./script/bundle-linux")) - .add_step(upload_artifact(&format!("target/release/{artifact_name}"))) - .add_step(upload_artifact(&format!( - "target/{remote_server_artifact_name}" - ))), - } -} - -pub(crate) fn bundle_windows( - arch: Arch, - release_channel: Option, - deps: &[&NamedJob], -) -> NamedJob { - let platform = Platform::Windows; - pub fn bundle_windows(arch: Arch) -> Step { - let step = match arch { - Arch::X86_64 => named::pwsh("script/bundle-windows.ps1 -Architecture x86_64"), - Arch::AARCH64 => named::pwsh("script/bundle-windows.ps1 -Architecture aarch64"), - }; - step.working_directory("${{ env.ZED_WORKSPACE }}") - } - let artifact_name = match arch { - Arch::X86_64 => assets::WINDOWS_X86_64, - Arch::AARCH64 => assets::WINDOWS_AARCH64, - }; - NamedJob { - name: format!("bundle_windows_{arch}"), - job: bundle_job(deps) - .runs_on(runners::WINDOWS_DEFAULT) - .envs(bundle_envs(platform)) - .add_step(steps::checkout_repo()) - .when_some(release_channel, |job, release_channel| { - job.add_step(set_release_channel(platform, release_channel)) - }) - .add_step(steps::setup_sentry()) - .add_step(bundle_windows(arch)) - .add_step(upload_artifact(&format!("target/{artifact_name}"))), - } -} - -fn set_release_channel(platform: Platform, release_channel: ReleaseChannel) -> Step { - match release_channel { - ReleaseChannel::Nightly => set_release_channel_to_nightly(platform), - } -} - -fn set_release_channel_to_nightly(platform: Platform) -> Step { - match platform { - Platform::Linux | Platform::Mac => named::bash(indoc::indoc! {r#" - set -eu - version=$(git rev-parse --short HEAD) - echo "Publishing version: ${version} on release channel nightly" - echo "nightly" > crates/zed/RELEASE_CHANNEL - "#}), - Platform::Windows => named::pwsh(indoc::indoc! {r#" - $ErrorActionPreference = "Stop" - $version = git rev-parse --short HEAD - Write-Host "Publishing version: $version on release channel nightly" - "nightly" | Set-Content -Path "crates/zed/RELEASE_CHANNEL" - "#}) - .working_directory("${{ env.ZED_WORKSPACE }}"), - } -} diff --git a/tooling/xtask/src/tasks/workflows/run_tests.rs b/tooling/xtask/src/tasks/workflows/run_tests.rs deleted file mode 100644 index 0bb3e152fb..0000000000 --- a/tooling/xtask/src/tasks/workflows/run_tests.rs +++ /dev/null @@ -1,496 +0,0 @@ -use gh_workflow::{ - Concurrency, Event, Expression, Job, PullRequest, Push, Run, Step, Use, Workflow, -}; -use indexmap::IndexMap; - -use crate::tasks::workflows::{ - nix_build::build_nix, - runners::Arch, - steps::{BASH_SHELL, CommonJobConditions, repository_owner_guard_expression}, - vars::{self, PathCondition}, -}; - -use super::{ - runners::{self, Platform}, - steps::{self, FluentBuilder, NamedJob, named, release_job}, -}; - -pub(crate) fn run_tests() -> Workflow { - // Specify anything which should potentially skip full test suite in this regex: - // - docs/ - // - script/update_top_ranking_issues/ - // - .github/ISSUE_TEMPLATE/ - // - .github/workflows/ (except .github/workflows/ci.yml) - let should_run_tests = PathCondition::inverted( - "run_tests", - r"^(docs/|script/update_top_ranking_issues/|\.github/(ISSUE_TEMPLATE|workflows/(?!run_tests)))", - ); - let should_check_docs = PathCondition::new("run_docs", r"^(docs/|crates/.*\.rs)"); - let should_check_scripts = PathCondition::new( - "run_action_checks", - r"^\.github/(workflows/|actions/|actionlint.yml)|tooling/xtask|script/", - ); - let should_check_licences = - PathCondition::new("run_licenses", r"^(Cargo.lock|script/.*licenses)"); - let should_build_nix = PathCondition::new( - "run_nix", - r"^(nix/|flake\.|Cargo\.|rust-toolchain.toml|\.cargo/config.toml)", - ); - - let orchestrate = orchestrate(&[ - &should_check_scripts, - &should_check_docs, - &should_check_licences, - &should_build_nix, - &should_run_tests, - ]); - - let mut jobs = vec![ - orchestrate, - check_style(), - should_run_tests.guard(run_platform_tests(Platform::Windows)), - should_run_tests.guard(run_platform_tests(Platform::Linux)), - should_run_tests.guard(run_platform_tests(Platform::Mac)), - should_run_tests.guard(doctests()), - should_run_tests.guard(check_workspace_binaries()), - should_run_tests.guard(check_dependencies()), // could be more specific here? - should_check_docs.guard(check_docs()), - should_check_licences.guard(check_licenses()), - should_check_scripts.guard(check_scripts()), - should_build_nix.guard(build_nix( - Platform::Linux, - Arch::X86_64, - "debug", - // *don't* cache the built output - Some("-zed-editor-[0-9.]*-nightly"), - &[], - )), - should_build_nix.guard(build_nix( - Platform::Mac, - Arch::AARCH64, - "debug", - // *don't* cache the built output - Some("-zed-editor-[0-9.]*-nightly"), - &[], - )), - ]; - let tests_pass = tests_pass(&jobs); - - jobs.push(should_run_tests.guard(check_postgres_and_protobuf_migrations())); // could be more specific here? - - named::workflow() - .add_event( - Event::default() - .push( - Push::default() - .add_branch("main") - .add_branch("v[0-9]+.[0-9]+.x"), - ) - .pull_request(PullRequest::default().add_branch("**")), - ) - .concurrency( - Concurrency::default() - .group(concat!( - "${{ github.workflow }}-${{ github.ref_name }}-", - "${{ github.ref_name == 'main' && github.sha || 'anysha' }}" - )) - .cancel_in_progress(true), - ) - .add_env(("CARGO_TERM_COLOR", "always")) - .add_env(("RUST_BACKTRACE", 1)) - .add_env(("CARGO_INCREMENTAL", 0)) - .map(|mut workflow| { - for job in jobs { - workflow = workflow.add_job(job.name, job.job) - } - workflow - }) - .add_job(tests_pass.name, tests_pass.job) -} - -// Generates a bash script that checks changed files against regex patterns -// and sets GitHub output variables accordingly -pub fn orchestrate(rules: &[&PathCondition]) -> NamedJob { - let name = "orchestrate".to_owned(); - let step_name = "filter".to_owned(); - let mut script = String::new(); - - script.push_str(indoc::indoc! {r#" - if [ -z "$GITHUB_BASE_REF" ]; then - echo "Not in a PR context (i.e., push to main/stable/preview)" - COMPARE_REV="$(git rev-parse HEAD~1)" - else - echo "In a PR context comparing to pull_request.base.ref" - git fetch origin "$GITHUB_BASE_REF" --depth=350 - COMPARE_REV="$(git merge-base "origin/${GITHUB_BASE_REF}" HEAD)" - fi - CHANGED_FILES="$(git diff --name-only "$COMPARE_REV" ${{ github.sha }})" - - check_pattern() { - local output_name="$1" - local pattern="$2" - local grep_arg="$3" - - echo "$CHANGED_FILES" | grep "$grep_arg" "$pattern" && \ - echo "${output_name}=true" >> "$GITHUB_OUTPUT" || \ - echo "${output_name}=false" >> "$GITHUB_OUTPUT" - } - - "#}); - - let mut outputs = IndexMap::new(); - - for rule in rules { - assert!( - rule.set_by_step - .borrow_mut() - .replace(name.clone()) - .is_none() - ); - assert!( - outputs - .insert( - rule.name.to_owned(), - format!("${{{{ steps.{}.outputs.{} }}}}", step_name, rule.name) - ) - .is_none() - ); - - let grep_arg = if rule.invert { "-qvP" } else { "-qP" }; - script.push_str(&format!( - "check_pattern \"{}\" '{}' {}\n", - rule.name, rule.pattern, grep_arg - )); - } - - let job = Job::default() - .runs_on(runners::LINUX_SMALL) - .with_repository_owner_guard() - .outputs(outputs) - .add_step(steps::checkout_repo().add_with(( - "fetch-depth", - "${{ github.ref == 'refs/heads/main' && 2 || 350 }}", - ))) - .add_step( - Step::new(step_name.clone()) - .run(script) - .id(step_name) - .shell(BASH_SHELL), - ); - - NamedJob { name, job } -} - -pub fn tests_pass(jobs: &[NamedJob]) -> NamedJob { - let mut script = String::from(indoc::indoc! {r#" - set +x - EXIT_CODE=0 - - check_result() { - echo "* $1: $2" - if [[ "$2" != "skipped" && "$2" != "success" ]]; then EXIT_CODE=1; fi - } - - "#}); - - script.push_str( - &jobs - .iter() - .map(|job| { - format!( - "check_result \"{}\" \"${{{{ needs.{}.result }}}}\"", - job.name, job.name - ) - }) - .collect::>() - .join("\n"), - ); - - script.push_str("\n\nexit $EXIT_CODE\n"); - - let job = Job::default() - .runs_on(runners::LINUX_SMALL) - .needs( - jobs.iter() - .map(|j| j.name.to_string()) - .collect::>(), - ) - .cond(repository_owner_guard_expression(true)) - .add_step(named::bash(&script)); - - named::job(job) -} - -fn check_style() -> NamedJob { - fn check_for_typos() -> Step { - named::uses( - "crate-ci", - "typos", - "2d0ce569feab1f8752f1dde43cc2f2aa53236e06", - ) // v1.40.0 - .with(("config", "./typos.toml")) - } - named::job( - release_job(&[]) - .runs_on(runners::LINUX_MEDIUM) - .add_step(steps::checkout_repo()) - .add_step(steps::cache_rust_dependencies_namespace()) - .add_step(steps::setup_pnpm()) - .add_step(steps::script("./script/prettier")) - .add_step(steps::script("./script/check-todos")) - .add_step(steps::script("./script/check-keymaps")) - .add_step(check_for_typos()) - .add_step(steps::cargo_fmt()), - ) -} - -fn check_dependencies() -> NamedJob { - fn install_cargo_machete() -> Step { - named::uses( - "clechasseur", - "rs-cargo", - "8435b10f6e71c2e3d4d3b7573003a8ce4bfc6386", // v2 - ) - .add_with(("command", "install")) - .add_with(("args", "cargo-machete@0.7.0")) - } - - fn run_cargo_machete() -> Step { - named::uses( - "clechasseur", - "rs-cargo", - "8435b10f6e71c2e3d4d3b7573003a8ce4bfc6386", // v2 - ) - .add_with(("command", "machete")) - } - - fn check_cargo_lock() -> Step { - named::bash("cargo update --locked --workspace") - } - - fn check_vulnerable_dependencies() -> Step { - named::uses( - "actions", - "dependency-review-action", - "67d4f4bd7a9b17a0db54d2a7519187c65e339de8", // v4 - ) - .if_condition(Expression::new("github.event_name == 'pull_request'")) - .with(("license-check", false)) - } - - named::job( - release_job(&[]) - .runs_on(runners::LINUX_SMALL) - .add_step(steps::checkout_repo()) - .add_step(steps::cache_rust_dependencies_namespace()) - .add_step(install_cargo_machete()) - .add_step(run_cargo_machete()) - .add_step(check_cargo_lock()) - .add_step(check_vulnerable_dependencies()), - ) -} - -fn check_workspace_binaries() -> NamedJob { - named::job( - release_job(&[]) - .runs_on(runners::LINUX_LARGE) - .add_step(steps::checkout_repo()) - .add_step(steps::setup_cargo_config(Platform::Linux)) - .add_step(steps::cache_rust_dependencies_namespace()) - .map(steps::install_linux_dependencies) - .add_step(steps::script("cargo build -p collab")) - .add_step(steps::script("cargo build --workspace --bins --examples")) - .add_step(steps::cleanup_cargo_config(Platform::Linux)), - ) -} - -pub(crate) fn run_platform_tests(platform: Platform) -> NamedJob { - let runner = match platform { - Platform::Windows => runners::WINDOWS_DEFAULT, - Platform::Linux => runners::LINUX_DEFAULT, - Platform::Mac => runners::MAC_DEFAULT, - }; - NamedJob { - name: format!("run_tests_{platform}"), - job: release_job(&[]) - .runs_on(runner) - .add_step(steps::checkout_repo()) - .add_step(steps::setup_cargo_config(platform)) - .when(platform == Platform::Linux, |this| { - this.add_step(steps::cache_rust_dependencies_namespace()) - }) - .when( - platform == Platform::Linux, - steps::install_linux_dependencies, - ) - .add_step(steps::setup_node()) - .add_step(steps::clippy(platform)) - .when(platform == Platform::Linux, |job| { - job.add_step(steps::cargo_install_nextest()) - }) - .add_step(steps::clear_target_dir_if_large(platform)) - .add_step(steps::cargo_nextest(platform)) - .add_step(steps::cleanup_cargo_config(platform)), - } -} - -pub(crate) fn check_postgres_and_protobuf_migrations() -> NamedJob { - fn remove_untracked_files() -> Step { - named::bash("git clean -df") - } - - fn ensure_fresh_merge() -> Step { - named::bash(indoc::indoc! {r#" - if [ -z "$GITHUB_BASE_REF" ]; - then - echo "BUF_BASE_BRANCH=$(git merge-base origin/main HEAD)" >> "$GITHUB_ENV" - else - git checkout -B temp - git merge -q "origin/$GITHUB_BASE_REF" -m "merge main into temp" - echo "BUF_BASE_BRANCH=$GITHUB_BASE_REF" >> "$GITHUB_ENV" - fi - "#}) - } - - fn bufbuild_setup_action() -> Step { - named::uses("bufbuild", "buf-setup-action", "v1") - .add_with(("version", "v1.29.0")) - .add_with(("github_token", vars::GITHUB_TOKEN)) - } - - fn bufbuild_breaking_action() -> Step { - named::uses("bufbuild", "buf-breaking-action", "v1").add_with(("input", "crates/proto/proto/")) - .add_with(("against", "https://github.com/${GITHUB_REPOSITORY}.git#branch=${BUF_BASE_BRANCH},subdir=crates/proto/proto/")) - } - - named::job( - release_job(&[]) - .runs_on(runners::LINUX_DEFAULT) - .add_env(("GIT_AUTHOR_NAME", "Protobuf Action")) - .add_env(("GIT_AUTHOR_EMAIL", "ci@zed.dev")) - .add_env(("GIT_COMMITTER_NAME", "Protobuf Action")) - .add_env(("GIT_COMMITTER_EMAIL", "ci@zed.dev")) - .add_step(steps::checkout_repo().with(("fetch-depth", 0))) // fetch full history - .add_step(remove_untracked_files()) - .add_step(ensure_fresh_merge()) - .add_step(bufbuild_setup_action()) - .add_step(bufbuild_breaking_action()), - ) -} - -fn doctests() -> NamedJob { - fn run_doctests() -> Step { - named::bash(indoc::indoc! {r#" - cargo test --workspace --doc --no-fail-fast - "#}) - .id("run_doctests") - } - - named::job( - release_job(&[]) - .runs_on(runners::LINUX_DEFAULT) - .add_step(steps::checkout_repo()) - .add_step(steps::cache_rust_dependencies_namespace()) - .map(steps::install_linux_dependencies) - .add_step(steps::setup_cargo_config(Platform::Linux)) - .add_step(run_doctests()) - .add_step(steps::cleanup_cargo_config(Platform::Linux)), - ) -} - -fn check_licenses() -> NamedJob { - named::job( - Job::default() - .runs_on(runners::LINUX_SMALL) - .add_step(steps::checkout_repo()) - .add_step(steps::cache_rust_dependencies_namespace()) - .add_step(steps::script("./script/check-licenses")) - .add_step(steps::script("./script/generate-licenses")), - ) -} - -fn check_docs() -> NamedJob { - fn lychee_link_check(dir: &str) -> Step { - named::uses( - "lycheeverse", - "lychee-action", - "82202e5e9c2f4ef1a55a3d02563e1cb6041e5332", - ) // v2.4.1 - .add_with(("args", format!("--no-progress --exclude '^http' '{dir}'"))) - .add_with(("fail", true)) - .add_with(("jobSummary", false)) - } - - fn install_mdbook() -> Step { - named::uses( - "peaceiris", - "actions-mdbook", - "ee69d230fe19748b7abf22df32acaa93833fad08", // v2 - ) - .with(("mdbook-version", "0.4.37")) - } - - fn build_docs() -> Step { - named::bash(indoc::indoc! {r#" - mkdir -p target/deploy - mdbook build ./docs --dest-dir=../target/deploy/docs/ - "#}) - } - - named::job( - release_job(&[]) - .runs_on(runners::LINUX_LARGE) - .add_step(steps::checkout_repo()) - .add_step(steps::setup_cargo_config(Platform::Linux)) - // todo(ci): un-inline build_docs/action.yml here - .add_step(steps::cache_rust_dependencies_namespace()) - .add_step( - lychee_link_check("./docs/src/**/*"), // check markdown links - ) - .map(steps::install_linux_dependencies) - .add_step(install_mdbook()) - .add_step(build_docs()) - .add_step( - lychee_link_check("target/deploy/docs"), // check links in generated html - ), - ) -} - -pub(crate) fn check_scripts() -> NamedJob { - fn download_actionlint() -> Step { - named::bash( - "bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)", - ) - } - - fn run_actionlint() -> Step { - named::bash(indoc::indoc! {r#" - ${{ steps.get_actionlint.outputs.executable }} -color - "#}) - } - - fn run_shellcheck() -> Step { - named::bash("./script/shellcheck-scripts error") - } - - fn check_xtask_workflows() -> Step { - named::bash(indoc::indoc! {r#" - cargo xtask workflows - if ! git diff --exit-code .github; then - echo "Error: .github directory has uncommitted changes after running 'cargo xtask workflows'" - echo "Please run 'cargo xtask workflows' locally and commit the changes" - exit 1 - fi - "#}) - } - - named::job( - release_job(&[]) - .runs_on(runners::LINUX_SMALL) - .add_step(steps::checkout_repo()) - .add_step(run_shellcheck()) - .add_step(download_actionlint().id("get_actionlint")) - .add_step(run_actionlint()) - .add_step(check_xtask_workflows()), - ) -} diff --git a/tooling/xtask/src/tasks/workflows/runners.rs b/tooling/xtask/src/tasks/workflows/runners.rs deleted file mode 100644 index df98826f8a..0000000000 --- a/tooling/xtask/src/tasks/workflows/runners.rs +++ /dev/null @@ -1,66 +0,0 @@ -pub const LINUX_SMALL: Runner = Runner("namespace-profile-2x4-ubuntu-2404"); -pub const LINUX_DEFAULT: Runner = LINUX_XL; -pub const LINUX_XL: Runner = Runner("namespace-profile-16x32-ubuntu-2204"); -pub const LINUX_LARGE: Runner = Runner("namespace-profile-8x16-ubuntu-2204"); -pub const LINUX_MEDIUM: Runner = Runner("namespace-profile-4x8-ubuntu-2204"); - -// Using Ubuntu 20.04 for minimal glibc version -pub const LINUX_X86_BUNDLER: Runner = Runner("namespace-profile-32x64-ubuntu-2004"); -pub const LINUX_ARM_BUNDLER: Runner = Runner("namespace-profile-8x32-ubuntu-2004-arm-m4"); - -pub const MAC_DEFAULT: Runner = Runner("self-mini-macos"); -pub const WINDOWS_DEFAULT: Runner = Runner("self-32vcpu-windows-2022"); - -pub struct Runner(&'static str); - -impl Into for Runner { - fn into(self) -> gh_workflow::RunsOn { - self.0.into() - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum Arch { - X86_64, - AARCH64, -} - -impl std::fmt::Display for Arch { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Arch::X86_64 => write!(f, "x86_64"), - Arch::AARCH64 => write!(f, "aarch64"), - } - } -} - -impl Arch { - pub fn linux_bundler(&self) -> Runner { - match self { - Arch::X86_64 => LINUX_X86_BUNDLER, - Arch::AARCH64 => LINUX_ARM_BUNDLER, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum Platform { - Windows, - Linux, - Mac, -} - -impl std::fmt::Display for Platform { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Platform::Windows => write!(f, "windows"), - Platform::Linux => write!(f, "linux"), - Platform::Mac => write!(f, "mac"), - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum ReleaseChannel { - Nightly, -} diff --git a/tooling/xtask/src/tasks/workflows/steps.rs b/tooling/xtask/src/tasks/workflows/steps.rs deleted file mode 100644 index 722a5f0704..0000000000 --- a/tooling/xtask/src/tasks/workflows/steps.rs +++ /dev/null @@ -1,336 +0,0 @@ -use gh_workflow::*; - -use crate::tasks::workflows::{runners::Platform, vars}; - -pub const BASH_SHELL: &str = "bash -euxo pipefail {0}"; -// https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idstepsshell -pub const PWSH_SHELL: &str = "pwsh"; - -pub fn checkout_repo() -> Step { - named::uses( - "actions", - "checkout", - "11bd71901bbe5b1630ceea73d27597364c9af683", // v4 - ) - // prevent checkout action from running `git clean -ffdx` which - // would delete the target directory - .add_with(("clean", false)) -} - -pub fn setup_pnpm() -> Step { - named::uses( - "pnpm", - "action-setup", - "fe02b34f77f8bc703788d5817da081398fad5dd2", // v4.0.0 - ) - .add_with(("version", "9")) -} - -pub fn setup_node() -> Step { - named::uses( - "actions", - "setup-node", - "49933ea5288caeca8642d1e84afbd3f7d6820020", // v4 - ) - .add_with(("node-version", "20")) -} - -pub fn setup_sentry() -> Step { - named::uses( - "matbour", - "setup-sentry-cli", - "3e938c54b3018bdd019973689ef984e033b0454b", - ) - .add_with(("token", vars::SENTRY_AUTH_TOKEN)) -} - -pub fn cargo_fmt() -> Step { - named::bash("cargo fmt --all -- --check") -} - -pub fn cargo_install_nextest() -> Step { - named::uses("taiki-e", "install-action", "nextest") -} - -pub fn cargo_nextest(platform: Platform) -> Step { - named::run(platform, "cargo nextest run --workspace --no-fail-fast") -} - -pub fn setup_cargo_config(platform: Platform) -> Step { - match platform { - Platform::Windows => named::pwsh(indoc::indoc! {r#" - New-Item -ItemType Directory -Path "./../.cargo" -Force - Copy-Item -Path "./.cargo/ci-config.toml" -Destination "./../.cargo/config.toml" - "#}), - - Platform::Linux | Platform::Mac => named::bash(indoc::indoc! {r#" - mkdir -p ./../.cargo - cp ./.cargo/ci-config.toml ./../.cargo/config.toml - "#}), - } -} - -pub fn cleanup_cargo_config(platform: Platform) -> Step { - let step = match platform { - Platform::Windows => named::pwsh(indoc::indoc! {r#" - Remove-Item -Recurse -Path "./../.cargo" -Force -ErrorAction SilentlyContinue - "#}), - Platform::Linux | Platform::Mac => named::bash(indoc::indoc! {r#" - rm -rf ./../.cargo - "#}), - }; - - step.if_condition(Expression::new("always()")) -} - -pub fn clear_target_dir_if_large(platform: Platform) -> Step { - match platform { - Platform::Windows => named::pwsh("./script/clear-target-dir-if-larger-than.ps1 250"), - Platform::Linux => named::bash("./script/clear-target-dir-if-larger-than 250"), - Platform::Mac => named::bash("./script/clear-target-dir-if-larger-than 300"), - } -} - -pub fn clippy(platform: Platform) -> Step { - match platform { - Platform::Windows => named::pwsh("./script/clippy.ps1"), - _ => named::bash("./script/clippy"), - } -} - -pub fn cache_rust_dependencies_namespace() -> Step { - named::uses("namespacelabs", "nscloud-cache-action", "v1").add_with(("cache", "rust")) -} - -pub fn setup_linux() -> Step { - named::bash("./script/linux") -} - -fn install_mold() -> Step { - named::bash("./script/install-mold") -} - -fn download_wasi_sdk() -> Step { - named::bash("./script/download-wasi-sdk") -} - -pub(crate) fn install_linux_dependencies(job: Job) -> Job { - job.add_step(setup_linux()) - .add_step(install_mold()) - .add_step(download_wasi_sdk()) -} - -pub fn script(name: &str) -> Step { - if name.ends_with(".ps1") { - Step::new(name).run(name).shell(PWSH_SHELL) - } else { - Step::new(name).run(name).shell(BASH_SHELL) - } -} - -pub struct NamedJob { - pub name: String, - pub job: Job, -} - -// impl NamedJob { -// pub fn map(self, f: impl FnOnce(Job) -> Job) -> Self { -// NamedJob { -// name: self.name, -// job: f(self.job), -// } -// } -// } - -pub(crate) const DEFAULT_REPOSITORY_OWNER_GUARD: &str = - "(github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')"; - -pub fn repository_owner_guard_expression(trigger_always: bool) -> Expression { - Expression::new(format!( - "{}{}", - DEFAULT_REPOSITORY_OWNER_GUARD, - trigger_always.then_some(" && always()").unwrap_or_default() - )) -} - -pub trait CommonJobConditions: Sized { - fn with_repository_owner_guard(self) -> Self; -} - -impl CommonJobConditions for Job { - fn with_repository_owner_guard(self) -> Self { - self.cond(repository_owner_guard_expression(false)) - } -} - -pub(crate) fn release_job(deps: &[&NamedJob]) -> Job { - dependant_job(deps) - .with_repository_owner_guard() - .timeout_minutes(60u32) -} - -pub(crate) fn dependant_job(deps: &[&NamedJob]) -> Job { - let job = Job::default(); - if deps.len() > 0 { - job.needs(deps.iter().map(|j| j.name.clone()).collect::>()) - } else { - job - } -} - -impl FluentBuilder for Job {} -impl FluentBuilder for Workflow {} -impl FluentBuilder for Input {} - -/// A helper trait for building complex objects with imperative conditionals in a fluent style. -/// Copied from GPUI to avoid adding GPUI as dependency -/// todo(ci) just put this in gh-workflow -#[allow(unused)] -pub trait FluentBuilder { - /// Imperatively modify self with the given closure. - fn map(self, f: impl FnOnce(Self) -> U) -> U - where - Self: Sized, - { - f(self) - } - - /// Conditionally modify self with the given closure. - fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self - where - Self: Sized, - { - self.map(|this| if condition { then(this) } else { this }) - } - - /// Conditionally modify self with the given closure. - fn when_else( - self, - condition: bool, - then: impl FnOnce(Self) -> Self, - else_fn: impl FnOnce(Self) -> Self, - ) -> Self - where - Self: Sized, - { - self.map(|this| if condition { then(this) } else { else_fn(this) }) - } - - /// Conditionally unwrap and modify self with the given closure, if the given option is Some. - fn when_some(self, option: Option, then: impl FnOnce(Self, T) -> Self) -> Self - where - Self: Sized, - { - self.map(|this| { - if let Some(value) = option { - then(this, value) - } else { - this - } - }) - } - /// Conditionally unwrap and modify self with the given closure, if the given option is None. - fn when_none(self, option: &Option, then: impl FnOnce(Self) -> Self) -> Self - where - Self: Sized, - { - self.map(|this| if option.is_some() { this } else { then(this) }) - } -} - -// (janky) helper to generate steps with a name that corresponds -// to the name of the calling function. -pub mod named { - use super::*; - - /// Returns a uses step with the same name as the enclosing function. - /// (You shouldn't inline this function into the workflow definition, you must - /// wrap it in a new function.) - pub fn uses(owner: &str, repo: &str, ref_: &str) -> Step { - Step::new(function_name(1)).uses(owner, repo, ref_) - } - - /// Returns a bash-script step with the same name as the enclosing function. - /// (You shouldn't inline this function into the workflow definition, you must - /// wrap it in a new function.) - pub fn bash(script: impl AsRef) -> Step { - Step::new(function_name(1)) - .run(script.as_ref()) - .shell(BASH_SHELL) - } - - /// Returns a pwsh-script step with the same name as the enclosing function. - /// (You shouldn't inline this function into the workflow definition, you must - /// wrap it in a new function.) - pub fn pwsh(script: &str) -> Step { - Step::new(function_name(1)).run(script).shell(PWSH_SHELL) - } - - /// Runs the command in either powershell or bash, depending on platform. - /// (You shouldn't inline this function into the workflow definition, you must - /// wrap it in a new function.) - pub fn run(platform: Platform, script: &str) -> Step { - match platform { - Platform::Windows => Step::new(function_name(1)).run(script).shell(PWSH_SHELL), - Platform::Linux | Platform::Mac => { - Step::new(function_name(1)).run(script).shell(BASH_SHELL) - } - } - } - - /// Returns a Workflow with the same name as the enclosing module. - pub fn workflow() -> Workflow { - Workflow::default().name( - named::function_name(1) - .split("::") - .collect::>() - .into_iter() - .rev() - .skip(1) - .rev() - .collect::>() - .join("::"), - ) - } - - /// Returns a Job with the same name as the enclosing function. - /// (note job names may not contain `::`) - pub fn job(job: Job) -> NamedJob { - NamedJob { - name: function_name(1).split("::").last().unwrap().to_owned(), - job, - } - } - - /// Returns the function name N callers above in the stack - /// (typically 1). - /// This only works because xtask always runs debug builds. - pub fn function_name(i: usize) -> String { - let mut name = "".to_string(); - let mut count = 0; - backtrace::trace(|frame| { - if count < i + 3 { - count += 1; - return true; - } - backtrace::resolve_frame(frame, |cb| { - if let Some(s) = cb.name() { - name = s.to_string() - } - }); - false - }); - - name.split("::") - .skip_while(|s| s != &"workflows") - .skip(1) - .collect::>() - .join("::") - } -} - -pub fn git_checkout(ref_name: &dyn std::fmt::Display) -> Step { - named::bash(&format!( - "git fetch origin {ref_name} && git checkout {ref_name}" - )) -} diff --git a/tooling/xtask/src/tasks/workflows/vars.rs b/tooling/xtask/src/tasks/workflows/vars.rs deleted file mode 100644 index adcd252465..0000000000 --- a/tooling/xtask/src/tasks/workflows/vars.rs +++ /dev/null @@ -1,344 +0,0 @@ -use std::cell::RefCell; - -use gh_workflow::{ - Concurrency, Env, Expression, Step, WorkflowCallInput, WorkflowCallSecret, - WorkflowDispatchInput, -}; - -use crate::tasks::workflows::{runners::Platform, steps::NamedJob}; - -macro_rules! secret { - ($secret_name:ident) => { - pub const $secret_name: &str = concat!("${{ secrets.", stringify!($secret_name), " }}"); - }; -} - -macro_rules! var { - ($var_name:ident) => { - pub const $var_name: &str = concat!("${{ vars.", stringify!($var_name), " }}"); - }; -} - -secret!(ANTHROPIC_API_KEY); -secret!(OPENAI_API_KEY); -secret!(GOOGLE_AI_API_KEY); -secret!(GOOGLE_CLOUD_PROJECT); -secret!(APPLE_NOTARIZATION_ISSUER_ID); -secret!(APPLE_NOTARIZATION_KEY); -secret!(APPLE_NOTARIZATION_KEY_ID); -secret!(AZURE_SIGNING_CLIENT_ID); -secret!(AZURE_SIGNING_CLIENT_SECRET); -secret!(AZURE_SIGNING_TENANT_ID); -secret!(CACHIX_AUTH_TOKEN); -secret!(DIGITALOCEAN_SPACES_ACCESS_KEY); -secret!(DIGITALOCEAN_SPACES_SECRET_KEY); -secret!(GITHUB_TOKEN); -secret!(MACOS_CERTIFICATE); -secret!(MACOS_CERTIFICATE_PASSWORD); -secret!(SENTRY_AUTH_TOKEN); -secret!(ZED_CLIENT_CHECKSUM_SEED); -secret!(ZED_CLOUD_PROVIDER_ADDITIONAL_MODELS_JSON); -secret!(ZED_SENTRY_MINIDUMP_ENDPOINT); -secret!(SLACK_APP_ZED_UNIT_EVALS_BOT_TOKEN); -secret!(ZED_ZIPPY_APP_ID); -secret!(ZED_ZIPPY_APP_PRIVATE_KEY); -secret!(DISCORD_WEBHOOK_RELEASE_NOTES); -secret!(WINGET_TOKEN); -secret!(VERCEL_TOKEN); -secret!(SLACK_WEBHOOK_WORKFLOW_FAILURES); - -// todo(ci) make these secrets too... -var!(AZURE_SIGNING_ACCOUNT_NAME); -var!(AZURE_SIGNING_CERT_PROFILE_NAME); -var!(AZURE_SIGNING_ENDPOINT); - -pub fn bundle_envs(platform: Platform) -> Env { - let env = Env::default() - .add("CARGO_INCREMENTAL", 0) - .add("ZED_CLIENT_CHECKSUM_SEED", ZED_CLIENT_CHECKSUM_SEED) - .add("ZED_MINIDUMP_ENDPOINT", ZED_SENTRY_MINIDUMP_ENDPOINT); - - match platform { - Platform::Linux => env, - Platform::Mac => env - .add("MACOS_CERTIFICATE", MACOS_CERTIFICATE) - .add("MACOS_CERTIFICATE_PASSWORD", MACOS_CERTIFICATE_PASSWORD) - .add("APPLE_NOTARIZATION_KEY", APPLE_NOTARIZATION_KEY) - .add("APPLE_NOTARIZATION_KEY_ID", APPLE_NOTARIZATION_KEY_ID) - .add("APPLE_NOTARIZATION_ISSUER_ID", APPLE_NOTARIZATION_ISSUER_ID), - Platform::Windows => env - .add("AZURE_TENANT_ID", AZURE_SIGNING_TENANT_ID) - .add("AZURE_CLIENT_ID", AZURE_SIGNING_CLIENT_ID) - .add("AZURE_CLIENT_SECRET", AZURE_SIGNING_CLIENT_SECRET) - .add("ACCOUNT_NAME", AZURE_SIGNING_ACCOUNT_NAME) - .add("CERT_PROFILE_NAME", AZURE_SIGNING_CERT_PROFILE_NAME) - .add("ENDPOINT", AZURE_SIGNING_ENDPOINT) - .add("FILE_DIGEST", "SHA256") - .add("TIMESTAMP_DIGEST", "SHA256") - .add("TIMESTAMP_SERVER", "http://timestamp.acs.microsoft.com"), - } -} - -pub fn one_workflow_per_non_main_branch() -> Concurrency { - one_workflow_per_non_main_branch_and_token("") -} - -pub fn one_workflow_per_non_main_branch_and_token>(token: T) -> Concurrency { - Concurrency::default() - .group(format!( - concat!( - "${{{{ github.workflow }}}}-${{{{ github.ref_name }}}}-", - "${{{{ github.ref_name == 'main' && github.sha || 'anysha' }}}}{}" - ), - token.as_ref() - )) - .cancel_in_progress(true) -} - -pub(crate) fn allow_concurrent_runs() -> Concurrency { - Concurrency::default() - .group("${{ github.workflow }}-${{ github.ref_name }}-${{ github.run_id }}") - .cancel_in_progress(true) -} - -// Represents a pattern to check for changed files and corresponding output variable -pub struct PathCondition { - pub name: &'static str, - pub pattern: &'static str, - pub invert: bool, - pub set_by_step: RefCell>, -} -impl PathCondition { - pub fn new(name: &'static str, pattern: &'static str) -> Self { - Self { - name, - pattern, - invert: false, - set_by_step: Default::default(), - } - } - pub fn inverted(name: &'static str, pattern: &'static str) -> Self { - Self { - name, - pattern, - invert: true, - set_by_step: Default::default(), - } - } - pub fn guard(&self, job: NamedJob) -> NamedJob { - let set_by_step = self - .set_by_step - .borrow() - .clone() - .unwrap_or_else(|| panic!("condition {},is never set", self.name)); - NamedJob { - name: job.name, - job: job - .job - .add_need(set_by_step.clone()) - .cond(Expression::new(format!( - "needs.{}.outputs.{} == 'true'", - &set_by_step, self.name - ))), - } - } -} - -pub(crate) struct StepOutput { - pub name: &'static str, - step_id: String, -} - -impl StepOutput { - pub fn new(step: &Step, name: &'static str) -> Self { - Self { - name, - step_id: step - .value - .id - .clone() - .expect("Steps that produce outputs must have an ID"), - } - } - - pub fn expr(&self) -> String { - format!("steps.{}.outputs.{}", self.step_id, self.name) - } - - pub fn as_job_output(self, job: &NamedJob) -> JobOutput { - JobOutput { - job_name: job.name.clone(), - name: self.name, - } - } -} - -impl serde::Serialize for StepOutput { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl std::fmt::Display for StepOutput { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "${{{{ {} }}}}", self.expr()) - } -} - -pub(crate) struct JobOutput { - job_name: String, - name: &'static str, -} - -impl JobOutput { - pub fn expr(&self) -> String { - format!("needs.{}.outputs.{}", self.job_name, self.name) - } -} - -impl serde::Serialize for JobOutput { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl std::fmt::Display for JobOutput { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "${{{{ {} }}}}", self.expr()) - } -} - -pub struct WorkflowInput { - pub input_type: &'static str, - pub name: &'static str, - pub default: Option, -} - -impl WorkflowInput { - pub fn string(name: &'static str, default: Option) -> Self { - Self { - input_type: "string", - name, - default, - } - } - - pub fn bool(name: &'static str, default: Option) -> Self { - Self { - input_type: "boolean", - name, - default: default.as_ref().map(ToString::to_string), - } - } - - pub fn input(&self) -> WorkflowDispatchInput { - WorkflowDispatchInput { - description: self.name.to_owned(), - required: self.default.is_none(), - input_type: self.input_type.to_owned(), - default: self.default.clone(), - } - } - - pub fn call_input(&self) -> WorkflowCallInput { - WorkflowCallInput { - description: self.name.to_owned(), - required: self.default.is_none(), - input_type: self.input_type.to_owned(), - default: self.default.clone(), - } - } - - pub(crate) fn expr(&self) -> String { - format!("inputs.{}", self.name) - } -} - -impl std::fmt::Display for WorkflowInput { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "${{{{ {} }}}}", self.expr()) - } -} - -impl serde::Serialize for WorkflowInput { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -pub(crate) struct WorkflowSecret { - pub name: &'static str, - description: String, - required: bool, -} - -impl WorkflowSecret { - pub fn new(name: &'static str, description: impl ToString) -> Self { - Self { - name, - description: description.to_string(), - required: true, - } - } - - pub fn secret_configuration(&self) -> WorkflowCallSecret { - WorkflowCallSecret { - description: self.description.clone(), - required: self.required, - } - } -} - -impl std::fmt::Display for WorkflowSecret { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "${{{{ secrets.{} }}}}", self.name) - } -} - -impl serde::Serialize for WorkflowSecret { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -pub mod assets { - // NOTE: these asset names also exist in the zed.dev codebase. - pub const MAC_AARCH64: &str = "Zed-aarch64.dmg"; - pub const MAC_X86_64: &str = "Zed-x86_64.dmg"; - pub const LINUX_AARCH64: &str = "zed-linux-aarch64.tar.gz"; - pub const LINUX_X86_64: &str = "zed-linux-x86_64.tar.gz"; - pub const WINDOWS_X86_64: &str = "Zed-x86_64.exe"; - pub const WINDOWS_AARCH64: &str = "Zed-aarch64.exe"; - - pub const REMOTE_SERVER_MAC_AARCH64: &str = "zed-remote-server-macos-aarch64.gz"; - pub const REMOTE_SERVER_MAC_X86_64: &str = "zed-remote-server-macos-x86_64.gz"; - pub const REMOTE_SERVER_LINUX_AARCH64: &str = "zed-remote-server-linux-aarch64.gz"; - pub const REMOTE_SERVER_LINUX_X86_64: &str = "zed-remote-server-linux-x86_64.gz"; - - pub fn all() -> Vec<&'static str> { - vec![ - MAC_AARCH64, - MAC_X86_64, - LINUX_AARCH64, - LINUX_X86_64, - WINDOWS_X86_64, - WINDOWS_AARCH64, - REMOTE_SERVER_MAC_AARCH64, - REMOTE_SERVER_MAC_X86_64, - REMOTE_SERVER_LINUX_AARCH64, - REMOTE_SERVER_LINUX_X86_64, - ] - } -} diff --git a/tooling/xtask/src/workspace.rs b/tooling/xtask/src/workspace.rs deleted file mode 100644 index fd71aa6bbd..0000000000 --- a/tooling/xtask/src/workspace.rs +++ /dev/null @@ -1,9 +0,0 @@ -use anyhow::{Context as _, Result}; -use cargo_metadata::{Metadata, MetadataCommand}; - -/// Returns the Cargo workspace. -pub fn load_workspace() -> Result { - MetadataCommand::new() - .exec() - .context("failed to load cargo metadata") -}

    -rToxsYl`DZ2ZvE}s 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

    kJw8y5UHQz||R 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

    M}Tm% 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%0AI@bk6h_uoJ! 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 _O0(+_OUIapt95iO!NNHpihZ&Oi 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

    KGn&^letCrM)5Y-2j-Kvr%(b-@*Fj~ohf)j+` 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¢K(v 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 z zi*ux}v1*4dWZ5qA-l}+7@B?CI?GBjFuYGcVjaC0vT_DcIZ@699%2DX;Ok zUu_?Zn>CZRrU4AN?W@H9mhDe?Z>?2V$!%V7t;Pt5|4e!d0d|D$hY|F{+9(@PAihQp zcUxCJSI8wKWf>T2Z!N51oy*TwTPzph`j6i{JmQUhl<=D?8U< z{e1eK)=Ap1?TXgJYSDJnlMGxfJI%LebXn8yYW^JO6`hj2bP&&;H`HyvN7oZt`DD}h zvicwi;5@*72jF$annkd6G6uVTe-pnu_J4VzUXy?AWyG27@AY-G%un!Ha7MTfIZ%U$ z3^p+)RhG#~7hsIcHDr>NxuyN*Uh6)kfmLvnJ=z%q?amH}_Q9*u<7dCuE3L(~fojl) z@KV5zs$TA5Y!Ms>$BF)z)(!W-3O@|W{3UcGMrvWzV0s+C zWWVrBzhd9}{*!(7{i7|;s7s`~=jCFJ)l{EhslLqle5hd1#R$?&2x^(3XEGMbKJ3iTk-!NA8Uc*ZXDw^^D)7KG*8&;*o1F!d>8tT*Tj-}JwUaiU6Kq56 zw(OSY!Fs6A@zAc7(yfg?KByY${>_Ki`|DV4iA}A5MB!?5Gi3h_hZ4`70j&gKTZ%+?Oyoj2kL;9`*g`Ec+Yy2+L`MVd8@h9` z$`MZ1OAAy-?h5;(*F*_5MY|J6)g26gTI&uV1rr;yvA)X*{S5ZmDn-{{cnDuk#m+}L zJbv%XiH6e;8?YMo1XEnR#(@xbM%K3g1~LTpy1-(d>%aHjT_EB8jd|fC;~+~WnBgn} z3d4b<0K<;CkdvK6K`-(7k%u8lF*c4rAs!8z&w!}|otQ9|rbl;lKO7LBkwTxu6% zqXL!{2N99?2wqm!pQcSPgJa~}L5+OY6+GLoZZV%au1?-D?{ww?Zd*8-=K?Ja{ zj*_eoX0wnf7_8+{6451)S~ak{pZdo5DnZxtgFKT4#MLTGk0KT{*uCHo!#MtivC}z>Ci-IWl?QjVr_IVq1{iv5sTK0wgClm4uD#p&oMfB z{t{xLKdQM9nXjP0mOc&wWN;;Xoxt$439w^~@Eu_*G}8hu>={#Kv|eK1<##h`$4}?8 zJv>AUUD~i7yT?4P1dMUXp#^jyHp%lLxB1f|G17?jtVVE)>gik~A>7HE;yAcAj|cyL z4-fA?v#wfdNwqf56YJ zM*wo3#CUvK?A4TD=bW5(xwy#jB5=B({4Ytf1Ko8z{yU#_j$U}q*wK!slkKlQva8z) zv91pSBw~KfS_)a$(i&QoK!N{4+BABd2e?%ju!c?&&hRW@W!U?RDD5gSLqIf%Xc>X! z%I|loI>ys|u-PckcZ1(?UUxXp*&#{6rw3hL7J$IpyE6EDPgtW)&$eKYc%YnVgP9$c zUPOpR7nJ~@O>+&v!Nd?-W7vs7!cx=}oef~_cz-l|A*i>?1)WfX&ovfp=E#0PqG}DW zXVkUTrcrwl?G^E!ST)*@Lx8s~^P zS6eY{69}I76=?G3Rw4z=2>R7UB@YBk63Q`{^jeJFul!kj%^i4Tqm)!#4>Xo29Y{Ru zS)O~LUo;N#9nS=9&=nw9r(|8wXE2rc3TX4BT684xaq-00BAWx9uSCXd85@HP39FVM z6R2CJ_N*m9TZwx!u2?qyXfO@kU)qT!%hsT1I~d)bjKJ~efB|kXH^#+>*ZcGO006vi zuW2#1vl*&C1OBs}Q|)LpqLPH-^(3hwwo0xU3)_eq1SOpG7D5$!FK;WUcMuCya3rA^S!0UaDc@GN_YxHEMTZ6)ffi1AY>G!P9>TP=5 zV2)j6EUCA9I8AuBYX3|IAN_>kL|^3Wg%dg{<3KST9`-!IKsAs0R)1xQ&6yOQ;Si{v z4NGVem0!p<#-EfjIz%g+Y|FTj)i5GO;5L#L_CIzet zKakRfW?N#dwqS(8JPwdy*aS8`Tin{ug|jT+iW&jcb(yGTkWmL1flcCCbd3pjI6TXd zBrxRJ0XQ|H9D+`m?Bxq~ZaAJ?6*-C3lK38Xh){btq1%K_E5VvUSs4VH%yv;tm*80v zi-Uu=5LI7El{EDR=8GV}{_21=;6MNl+N}VZ@t!y*r3W_xOIle$6YSAg;b+V;^dOnD z_WGk27(f5)py03c)VHx7G_KznMMW3sfoc!nTlO+Cjus^!!O@6dvxWZlEQsA+(}rh3 zoi)qQgVUDjvB<2%HPpt=e*&N@g065*!%+-d8jlh8EE_6BB>Gr)fFQmzbX;^Vx^Lc> zT0ge9f7sl&{yNav4MstXWe)4FGWZ;tka0;x!Dm^J~fb;d(6*-i%E+x;H*-^y)_kL5zK7ONYj zV#%WCeG*MRO0Qz6aJ&hwhGW~xwXhdr4Y>fWKBbZbUN5q@sjVxEIn?@(d2h^tBthK( zU#cy1M}4;&Y*{=zEtdvLmmH{n|)0dEW95qV+H7$s`^QZ6r8*047BL1BS;Z#B3riEpRLuwj|?8OMDMUp zE}DlK{#EvSZ{=n_N5L-ebLMEN0U=MumVBadBUlTid)Zs}*PMN@h?4Itvh`x2v0!bz zXMz$cvtbvafyo1mC$A4IVYrCB<;ejL@b9#KqjZnpTM9wy22M%q3FAY5aDX4f(g+8) z;nSg2;KZ>;U3nw`w08XdW{LVG0ZCzlWzYCb1CuAxi=dFGkLjJ(NBwWE8UOSxJNjHN zk4>VDDz#l-b{97)fyYL@q^8zl_K2J-`((O-2Gg?!&D(D^zkcFi{3T#09r~ z+{EPh@Or;q9{_;&?d8F+J1rwOdGuoi{UD8{7(tO}`=ROyw9>y1=g6*JmJW1AiyWi#3F?y>4+I`Su>XS` zIH=f;usK6!I~p8Fpq^jh`!hsV0O#Wna^ne6KUchUN2r1%2(WPA%oJ^dV45SpJ6YCx zSGTY5oZfdHYVGwq$|Pn*4Z<>v;yW0-C^01=LcQ$0FKqb;8U#4lU3R|~k+;f`7(ulP1eY;l!3 zj{AXKBnrDfAe-;mCU+JVwihCJkop0fvqffRaQavbNZ~=qz+n$Z#MFYSv_ma2QYuY|(O9;RwoM-T{z+-u)Zx zHxdvV03}loQiLsAVbVbdg)m8UH|{{6bE@`bT{7p?9N%T;I@BMihg!hts@i+4T$%GS z<{01j#y8r2x$GLzSRa?A0Nh3uohb5W_xGg3W=Dv((=%O`e70@#m4N!==hFe}u4??< z%>ykL*AIa@NZhP6F(mGnO3GBIZoC&~JT$JR|b|LgzlH{(MO1ggYA$y{Lzqp9E9arq1JxO^|3*FWu8SavaZ?e9IK ze!R(+aq#WPcn(EvT#B*~qN4gU@bQnNRHC(}J0b`ig0n^T3xg_`bA4j3RNJU3vBqd4kPHQj%)ZQ;ud4zbgM6AV3w4eD)1erqNib~+HCDxU zWByPNdWlglKZcA!fNL)YEq`S86c`)L7?kxAqrP|GB-7YN)%#zd(Jk8pA!BW51=lpj z*z=pk*b3Gs0L_VW1DhzIqMA-@d)0J*q4CFyY^d+jf`j%$S+*uHu?=knmUlr2OH2tk znxd~v%ahm#746Aj|1@TyHfs?vKp&L0RcWf!@2Zctp~K*xiP2zHb&Tug&8Ru5Wd6Y3 z7J{7<*K&U9;c<$;^AG>FSUoc+!GQ5`hFrAX9AHI^7OHIzzF^-0PWLsv+CF%J%`r+v z5Qr4q;{=h|1;G#j0S0$Dj$eWHpMrlc@!P-oJD~rcPnY<~@Bb0K3zhaUv$<*=DNFA9 z^}-+{wS8usV@KG^?ec9v2+*Yht}kwPw24?4M?6QlT;tTQF?S&K&_Rg=0Y1L042U!b zP2zN^k5ij_*((?X5T9Lj9^>`nw{dv?1Y+9VQ2~@09w_ZQjB+cqJ*yOd#aiDon12Q9 zkK0Fj3+2|{3M@miW+7+*2HD*JIadVRSH0i!M?to1OIR~#I^M_;0zUDj0`uqi5{_?llPYqAgqprRI0L9Tj+xfnnNx zhztT8I8sy(zRm&TWnh)|{-#TS8GmAMGC83+!k_dDEbJHIvJmaT%(ze5vtVAl(uXMa zK}=!)$ngg_LtkyX{4v)3G~Z}1%z4BcKz*PMYdnh?v=g|Ra8?~@zp>3L)^bUpq_`wW ztrf9{@;Z<5ebg}rN0H7DpW*V6Yqe0fwY;>BCM7_IYvU54DtjJbvF*~{H1Cz;t83FY z=;L{9WV!b5UNd>7WE}woIle_Kl`!MNmWN4;rfw$g;*)KQvrM5>r)gFsjKI9u2>6w3 z@2##HDZ)L?1};9oK9$#J0N@jQS^1Z~WeW$(bU;@I#*GfI1@Yi~25J7o03iFZBKT>l zCt@%*i}EfbyHQLoy8*DKe%54*1p%sTBvCMjCD z8N4W@dV^z;pNq4yJ_k9Nr`_e6AMv}nKB9hUgM)evw~}JU(9g~iiZWV|+2Li4d#1Bs<0H(nYzIdBA?G3_5xYwC15IU&LSn$saz=J7+;F$`v^~zqo3|`3M7-EMLgJ7!vGJDN0yaqEw z%C1dBvI!Iju*%Ef@U!3~cENC>Ljc82X?1WD;g)u+jWY#)8ppn#?jP>~hK)br4`>2cje?Tc3&G6AagiKA|G}k% zJ9rc+>@t(;>x6gNB&%`gr$q@fZKnWBmE)U7Xj~zSb^qD(m_x_Fsv;p5K8m zgQ@~Yc&C{s%crXEdFq_N8Lw;!MS7?+_dy%tteTwqXo`_q8^PcJ2d#8Xx(fF~II~Pg zikT7BcuusXsGSrjve(=zxF7#^V0zT^nf@ykbaYcO0>cs?fUIsMS_Rq`8-|MKWy~zF zCq+`RC)jRulv}_iH2??Iz1Lxri@-LO;*8Re7R*%MYL}FKe=CcL69E0p4|E8N%D3vK zz`luL+UM%{L_n;vr4%!4vNyPWi$qD~yzx)2&s9F|?u;^cW$UUjsW;+Wt-MgAGy~1H z0G!)U+k3)ZTn-iMh);K6=^MuPti8oP*jx|F&|ADs^j+E(?*rMuk%e2mghyaVQPvpF zb!)ePe5F1}w+T;Ack#FW_J0ARwMC+G`BD0q~e{Ay6oTyDz?Y0GRju{2_kpH-3oBf2#fe_(y-#@5Nm#sP-LV z#&Ipqdd5Z@+USquwWg;YV4YarP-UYjY2k~b$}?TG?*3h`FR$^%yT?A4haSAw zIGwNDOA?KwFzY2;4q)-iB7KDf9NOg=FCWhF?)`l{+#k6o40Pr>g!ty5fS19xMG5m9 z1DIX~{v~*V_bm+aib@jU_bdcO>Qy#5?Z=7sPIikn^{oBXzbwU&6EIFYr?Zo1s{kr= zl~Kt60!;a6Vk9EVi_G&o1=g9t{zx6A9E0O~j1dT)+ht!Hqkf-2_F#-8xSOpOQ670g z#yGnUQE0Lvabx|X5Q{F{|Gh9F!p!|HWV?5ka%)ogKgHTVO?xF*^_lZInXPpon9y7< z@D@e71~J<8Ciw86Gu30S7Lq<&Lv|UhBvUBwQ3;M*N76jNIp|h?EbYL(tJ@H5@f1<_ zpqYZfYIBFycvs$Ec9BwIA`FsMGJE-~SJ~P_nAHMM4yC{9)?h#v0JRcO7_U3oM1kpG z|4dw$W`3;aVA;e~XZ=Yw!Q*flpi&#{!)@D~u-W~D;dkp!QCd4!Xi>FH;8fZ~=mub& zpX7($vyJ&^_xkzusk}Y|0H4^asE!$G?4lvNEek2;+X#59GS2oK`IZL>!&$NXAgUFE z0R}CF1`V`is*V>?DqZtj!<4GV!wmkK0u3<=aMX&zpKS%}#Z)KNIm>a*2S64SkHPx; zN&W6K+nF_eMRnw?Q=j0Rfi9m0_x!3P6&;G^zNrv%nS?$UI;WZ87Jxz(=*&J;wmDoT zmJ*7@vt3T;rvN|ra zb#U@3*DnJTBi3Xya4O~z#*4P#g%aq~`TGf4FC+=Jm9KipqMwqH(7KO}s*LC+HqiouEiM5~*Ch`9N*sJq%`* zIr5K2?QBt!_ZSY&xJRf(QEvbZWASYS@X}9r0HB``*l-3)wf`g7a&T%OpzLvOA=UoB z{ulpUygZ+f4Mrw0o#XBR9p``7Gi-gYKRm>xXNE54ZvX&X&wnaseNpfo5kojqB|;e} zqywy)^zHJZ`|I|iXQQ&Vupq!YF*7R1HHELhs>#|%bvtn@A_9tXWMa8_-ZSTUe1F8K z)77M9a56!_^oeWSOr^2hT-LJ0UMgV6I`o{CK}EP_q}H-#hhsi5TQa|wVAutW33ry@ zXBa??`UC_^EK@43FKz1F5>>ZB`^Va0Ki9;)RwPP=_swIrz5f#+o?v%Epvg(h47T5z zK)YoS!WN!xL3LDJ@>y722H82z&-?usk5|<_9kp*{2P_U+q-@{37x;a<_+mQ#XN=#H z077@>CsDR9=btKb+UJm6`Bg_#1x%*8AzpyZDFy=e_>;O9!%-!_@8hQ?wDRR+d?|tIlR;hVMA8@F@-g879uNfxZlcSFu{E$~VLup}5)^ zz`hgy47zft9)kf{NQiiX|6vg}0)`XWh8wf%O=5|(B`UR1Kv}#NHTI~F-^g?-xUUE3 z)6k%hZARUo1@jQ1n6uhO2iHZm1A(C^?8t_%H|sD6a2I9qm7V7PEV7*;e?n(2#*pJ) zyslf3J-jNwJp_|7HCP(SQYo&DWN4gV`%@SZj0E_-2Y`2EaL}J&8Iy9_nQ%h)2&x_^b3wrg3(VsVC9FqP;jurDR@lZx@UK=<`@MVuR&af!%f=5{%+Cin=J= zusjk(nnuY}!TN>XfJ;3A0BytfC)ltA%TrX2iyQJjyGk1p{UOY>ZkT^*`O@M2xl{l^?7UBq{KspLl_y!EDvGD&xUm z1_z4;RwkkB$$GeMpe(}KOL`>E7*flo#fNE8{kSv2barr%XZA(b0f(s>CWTHeYxyVn z@p35iT+9L_g_t@7R2vsPE$4lr!^VK~#+e8QAvc*~XDboR&x-dIUkF z)MBXJM^kBy5N|ssdr)`1$qI2l6gesn4?L)^JrkB4Ck9Kvb;YR;odQt+q2y#udl?9V z(OgHXvb14P#ms}VoRlj8ltIhiQFsQsjZTinxlCmTNTLkK-(qJ)9!fO@*oQZ!*}=ZY zS=;9(_=mGFeqoWoIbObfiI<+;p8%Lhz^P=G>36YCmQ!~Ka*i*rs14*kWI)vhT;2}X8=e?aUOmlvj4FIXznddXU~q`YtTuYgB?Qd36jOD#+G~Ea74Qaq99;}g?8d< zsA8O0$0D*w<<&9$leqj z^Z*+AD<`m>5{RxFSwM_qJ^-*tLD`!0;8lACrsGN*gz~1~d0FJ#hl6>YJs}4_&nL|b zg4K2l$oo02Fc09ei@8``We){zbE87-6>$}Hf z)!fC058lh|W{ir!LOb-c34b^P%dm;mwsCo9)r-T`%rK_z~J=Z?j_Yj8<=HNa|dfsfo>w={RxQ}Wu>{Z)S0DE4?pfH$StFGg62aS=s0!;En!63krU1D$1O0;fd=Zk>j zOrqKUFIx9zpoYK)fauge-k?eUBV|1mbhJo~-JiU~ObD178_PZlIaMR1hq%SQ~8GQ4Zw9Qi424 zR(35wFq`+Oj38P~LC`QjHKQRFa$6TTb^#Va1^ZxIMMG9kS=O@$9h(rs+d+%%bdpoI z5J4?6n4n84DCyT*lA_o%>B(qVKU`F=enmSthEXS?{6U0y@3kAu##W)QMU-}k-N>0v zL%y(88G@xt_Aut0)eBn+v0v|%S@Co#?;gOwKxwDz*(*yc&MzNF$wIyWz62d`4nJvf z89=ceP($#-fN(*}qT~DTMLk~;7*5&Z@2WsX&MVu^v91Ńq}G3r~DNiA|H=~zY# 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%0AXb>v#0G50%sH+2Nw~S}iV&|2~3wkpOT# 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%0AM841wHY77Qh$}I`#_I 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%0A)d9F7za<&it_LuDD zcFX*~y0KsS0#pXo`mABCfZa`9T4b-iKw9;uj|U1-KxqyfYyB1Ym1BNI6N{i&B2sx) zG?riuh)6{spDciejw4_~LuaG~NU~t8^VEnx8-SBmY$B*}qqe)J5bPcp6aWAa@xnc3 zSchG#uf>#&#@=BN;26hDdah`-8+2%iszuEI$)|U$%^w+bO-73X+5^r2pV|n6RC?57 zZ`TWvz7$gpw#yrvLZY#FVsG9MNR{mo_^4oC*=e4YJPx$yX$k<^g)m<}gU-2;`?49z z;vxyq)6u`SQYX*r9YMz*44z!=5xv&8ghO?9q7;!q>cF+~oU}2|uKJvk5v+wbu9aKwK^LoEh8_NpzmMdH+sE2Q#SKweIfOg(&yql_?{S(hq9sC8JA3Hloss3Iqc#VLR0%Q2je6|d1m$ZRK?E_b$ zDB1@a7z&{L6Bw=Y7HbN(9%FoDwjM`ebVEYtr5G-hy@fqkFPzVG@}bAk6Byjm>)U zUXDJfNr%ihHPVzPI<8uLZ2Y#PI9E`>0}9z62(k=<0)y1eG%=&ipof7W{JW9Hpl`NNJf(Fn^>!njtuJ!2-J z${kemHOgUWC1fI6@QDW;D5f~T!uabzTSx+WZf#?qTfc(ye$VXSP7FzbcT0vwc1=hC^I)I* z+`C(5d$GH>XZs(1^Y`s%-ut)g@B9a!w_o{x|KYZe{+C+!F1-x3IL%?}R;4uCIHUd6 z0X9ADQP`(@tOaJ+q|X7W?R92!th7n!m}hz$HQHI*KG+se%nkvCZdaCM3$5SnelKsaX8V23ixj^h{s+}~uu2fXiF#c66tpKg!%?bbH7CxJEnE|zH0+6d z2!RT@Spmju!JB{*eBVQMH1Y8g>nk4TOi;zyk}(4soV6I=3(ktcwVU^Qn#WtaReVTW z9io%$X`kQ}f{-4aEaJ0)@Tn{>pLqtD^Bg>ygaz?PuuHMWvaFD$16VgVyb-jC^(=u| zO>9O4iOw^bAVAidw^{e?US)?)I=zc3uJg`ms~c+IbMP7gX`X=qen5f0uvLS4M`!Fe zLq=LmE&!_XeLPD-uveGVEP>fQ(&p{7T=EgZ7Toa6Ihe6)2^~3%orErPpWK@mfAf6i z^F?-iz)yhh8Fm;7^Eij5&?Lcx&&uzNy1VQ5*Oo6;>j_Ena<^JKaf#3I> zXWRqMs%ZZ7j%By1^lR^OzFY3Y*rP0YaR42q{lp+1Ye}VeV%0A6Rdl}bbai&wY49Bf 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⪼kE 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%0A4Gn8~x_5{nIBn(YvKUlflq|wgIWN+LH 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α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 diff --git a/crates/zed/resources/app-icon-preview.png b/crates/zed/resources/app-icon-preview.png deleted file mode 100644 index b76e57885835fdb4fdba114fb812fd16f9637001..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 191163 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

    8!`W( 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

    st+#lG+CB&9^@`p`No{l9XDf|FUTR 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�-#0{aBGt0>_@sV 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≤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

Some text link more text